diff --git a/scripts/modules/bl_i18n_utils/bl_extract_messages.py b/scripts/modules/bl_i18n_utils/bl_extract_messages.py index 1c3b26651b5..ea9664b3da9 100644 --- a/scripts/modules/bl_i18n_utils/bl_extract_messages.py +++ b/scripts/modules/bl_i18n_utils/bl_extract_messages.py @@ -588,6 +588,7 @@ def dump_py_messages_from_files(msgs, reports, files, settings): ), "message": (), "heading": (), + "placeholder": ((("text_ctxt",), _ctxt_to_ctxt),), } context_kw_set = {} @@ -621,7 +622,8 @@ def dump_py_messages_from_files(msgs, reports, files, settings): for arg_pos, (arg_kw, arg) in enumerate(func.parameters.items()): if (not arg.is_output) and (arg.type == 'STRING'): for msgid, msgctxts in context_kw_set.items(): - if arg_kw in msgctxts: + # The msgid can be missing if it is used in only some UILayout functions but not all + if arg_kw in msgctxts and msgid in func_translate_args[func_id]: func_translate_args[func_id][msgid][1][arg_kw] = arg_pos # The report() func of operators. for func_id, func in bpy.types.Operator.bl_rna.functions.items(): diff --git a/source/blender/editors/include/UI_interface_c.hh b/source/blender/editors/include/UI_interface_c.hh index ef4ed7d1c7e..8a863af1fb9 100644 --- a/source/blender/editors/include/UI_interface_c.hh +++ b/source/blender/editors/include/UI_interface_c.hh @@ -933,6 +933,11 @@ void UI_but_disable(uiBut *but, const char *disabled_hint); void UI_but_type_set_menu_from_pulldown(uiBut *but); +/** + * Set at hint that describes the expected value when empty. + */ +void UI_but_placeholder_set(uiBut *but, const char *placeholder_text) ATTR_NONNULL(1); + /** * Special button case, only draw it when used actively, for outliner etc. * @@ -2775,7 +2780,8 @@ void uiItemFullR(uiLayout *layout, int value, eUI_Item_Flag flag, const char *name, - int icon); + int icon, + const char *placeholder = nullptr); /** * Use a wrapper function since re-implementing all the logic in this function would be messy. */ diff --git a/source/blender/editors/interface/interface.cc b/source/blender/editors/interface/interface.cc index 1b7799e84e7..f76402b2a00 100644 --- a/source/blender/editors/interface/interface.cc +++ b/source/blender/editors/interface/interface.cc @@ -61,6 +61,7 @@ #include "WM_types.hh" #include "RNA_access.hh" +#include "RNA_enum_types.hh" #ifdef WITH_PYTHON # include "BPY_extern_run.h" @@ -3477,6 +3478,10 @@ static void ui_but_free(const bContext *C, uiBut *but) MEM_freeN(but->hold_argN); } + if (but->placeholder) { + MEM_freeN(but->placeholder); + } + ui_but_free_type_specific(but); if (but->active) { @@ -5906,6 +5911,36 @@ void UI_but_disable(uiBut *but, const char *disabled_hint) but->disabled_info = disabled_hint; } +void UI_but_placeholder_set(uiBut *but, const char *placeholder_text) +{ + MEM_SAFE_FREE(but->placeholder); + but->placeholder = BLI_strdup_null(placeholder_text); +} + +const char *ui_but_placeholder_get(uiBut *but) +{ + const char *placeholder = (but->placeholder) ? but->placeholder : nullptr; + + if (!placeholder && but->rnaprop) { + if (but->type == UI_BTYPE_SEARCH_MENU) { + StructRNA *type = RNA_property_pointer_type(&but->rnapoin, but->rnaprop); + const short idcode = RNA_type_to_ID_code(type); + if (idcode != 0) { + RNA_enum_name(rna_enum_id_type_items, idcode, &placeholder); + placeholder = CTX_IFACE_(BLT_I18NCONTEXT_ID_ID, placeholder); + } + } + else if (but->type == UI_BTYPE_TEXT) { + const char *identifier = RNA_property_identifier(but->rnaprop); + if (STR_ELEM(identifier, "search_filter", "filter_text", "filter_search")) { + placeholder = CTX_IFACE_(BLT_I18NCONTEXT_ID_WINDOWMANAGER, "Search"); + } + } + } + + return placeholder; +} + void UI_but_type_set_menu_from_pulldown(uiBut *but) { BLI_assert(but->type == UI_BTYPE_PULLDOWN); diff --git a/source/blender/editors/interface/interface_intern.hh b/source/blender/editors/interface/interface_intern.hh index ce133482a87..7a7d9307571 100644 --- a/source/blender/editors/interface/interface_intern.hh +++ b/source/blender/editors/interface/interface_intern.hh @@ -175,6 +175,8 @@ struct uiBut { char strdata[UI_MAX_NAME_STR] = ""; char drawstr[UI_MAX_DRAW_STR] = ""; + char *placeholder = nullptr; + rctf rect = {}; /* block relative coords */ char *poin = nullptr; @@ -742,6 +744,11 @@ void ui_but_active_string_clear_and_exit(bContext *C, uiBut *but) ATTR_NONNULL() void ui_but_set_string_interactive(bContext *C, uiBut *but, const char *value); uiBut *ui_but_drag_multi_edit_get(uiBut *but); +/** + * Get the hint that describes the expected value when empty. + */ +const char *ui_but_placeholder_get(uiBut *but); + void ui_def_but_icon(uiBut *but, int icon, int flag); /** * Avoid using this where possible since it's better not to ask for an icon in the first place. diff --git a/source/blender/editors/interface/interface_layout.cc b/source/blender/editors/interface/interface_layout.cc index cb0fb486317..a3d173db486 100644 --- a/source/blender/editors/interface/interface_layout.cc +++ b/source/blender/editors/interface/interface_layout.cc @@ -2055,7 +2055,8 @@ void uiItemFullR(uiLayout *layout, int value, eUI_Item_Flag flag, const char *name, - int icon) + int icon, + const char *placeholder) { uiBlock *block = layout->root->block; char namestr[UI_MAX_NAME_STR]; @@ -2468,6 +2469,10 @@ void uiItemFullR(uiLayout *layout, UI_but_flag_enable(but, UI_BUT_LIST_ITEM); } + if (but && placeholder) { + UI_but_placeholder_set(but, placeholder); + } + #ifdef UI_PROP_DECORATE if (ui_decorate.use_prop_decorate) { uiBut *but_decorate = ui_decorate.but ? ui_decorate.but->next : diff --git a/source/blender/editors/interface/interface_widgets.cc b/source/blender/editors/interface/interface_widgets.cc index ce9e184b086..81d6f6a67ed 100644 --- a/source/blender/editors/interface/interface_widgets.cc +++ b/source/blender/editors/interface/interface_widgets.cc @@ -2187,6 +2187,22 @@ static void widget_draw_text(const uiFontStyle *fstyle, } } + /* Show placeholder text if the input is empty and not being edited. */ + if (!drawstr[0] && !but->editstr && ELEM(but->type, UI_BTYPE_TEXT, UI_BTYPE_SEARCH_MENU)) { + const char *placeholder = ui_but_placeholder_get(but); + if (placeholder && placeholder[0]) { + uiFontStyleDraw_Params params{}; + params.align = align; + uiFontStyle style = *fstyle; + style.shadow = 0; + uchar col[4]; + copy_v4_v4_uchar(col, wcol->text); + col[3] *= 0.33f; + UI_fontstyle_draw_ex( + &style, rect, placeholder, strlen(placeholder), col, ¶ms, nullptr, nullptr, nullptr); + } + } + /* part text right aligned */ if (drawstr_right) { uchar col[4]; diff --git a/source/blender/makesrna/intern/rna_ui_api.cc b/source/blender/makesrna/intern/rna_ui_api.cc index c0ace2e8fbb..b04e1ba5c3b 100644 --- a/source/blender/makesrna/intern/rna_ui_api.cc +++ b/source/blender/makesrna/intern/rna_ui_api.cc @@ -84,6 +84,7 @@ static void rna_uiItemR(uiLayout *layout, const char *text_ctxt, bool translate, int icon, + const char *placeholder, bool expand, bool slider, int toggle, @@ -109,6 +110,7 @@ static void rna_uiItemR(uiLayout *layout, /* Get translated name (label). */ name = rna_translate_ui_text(name, text_ctxt, nullptr, prop, translate); + placeholder = rna_translate_ui_text(placeholder, text_ctxt, nullptr, prop, translate); if (slider) { flag |= UI_ITEM_R_SLIDER; @@ -140,7 +142,7 @@ static void rna_uiItemR(uiLayout *layout, flag |= UI_ITEM_R_CHECKBOX_INVERT; } - uiItemFullR(layout, ptr, prop, index, 0, flag, name, icon); + uiItemFullR(layout, ptr, prop, index, 0, flag, name, icon, placeholder); } static void rna_uiItemR_with_popover(uiLayout *layout, @@ -1147,6 +1149,8 @@ void RNA_api_ui_layout(StructRNA *srna) RNA_def_function_ui_description(func, "Item. Exposes an RNA item and places it into the layout"); api_ui_item_rna_common(func); api_ui_item_common(func); + RNA_def_string( + func, "placeholder", nullptr, 0, "", "Hint describing the expected value when empty"); RNA_def_boolean(func, "expand", false, "", "Expand button to show more detail"); RNA_def_boolean(func, "slider", false, "", "Use slider widget for numeric values"); RNA_def_int(func,