From 850e715682c228bf1c43eb1dea715779a84f0381 Mon Sep 17 00:00:00 2001 From: Campbell Barton Date: Sat, 6 Jul 2024 14:49:39 +1000 Subject: [PATCH] UI: add UILayout.template_popup_confirm(..) function This makes it possible for popups to have their confirm & cancel buttons defined in the operator's draw callback. When used with popups created by: `WindowManager::invoke_props_dialog()` to override the default confirm/cancel buttons. In the case of `WindowManager::popover(..)` & `bpy.ops.wm.call_panel()` this can be used to add confirm/cancel buttons. Details: - When the confirm or cancel text argument is a blank string the button isn't shown, making it possible to only show a single button. - The template is similar to UILayout::operator in that it returns the operator properties for the confirm action. - MS-Windows alternate ordering of Confirm/Cancel is followed. Needed to resolve #124098. Ref !124139 --- .../blender/editors/include/UI_interface_c.hh | 32 +++++ .../editors/interface/interface_query.cc | 10 ++ .../regions/interface_region_menu_popup.cc | 136 ++++++++++++++++++ source/blender/makesrna/intern/rna_ui_api.cc | 59 ++++++++ .../windowmanager/intern/wm_operators.cc | 6 +- 5 files changed, 241 insertions(+), 2 deletions(-) diff --git a/source/blender/editors/include/UI_interface_c.hh b/source/blender/editors/include/UI_interface_c.hh index 116365f7445..bf2878245be 100644 --- a/source/blender/editors/include/UI_interface_c.hh +++ b/source/blender/editors/include/UI_interface_c.hh @@ -674,6 +674,11 @@ bool UI_but_is_utf8(const uiBut *but); bool UI_block_is_empty_ex(const uiBlock *block, bool skip_title); bool UI_block_is_empty(const uiBlock *block); bool UI_block_can_add_separator(const uiBlock *block); +/** + * Return true when the block has a default button. + * Use this for popups to detect when pressing "Return" will run an action. + */ +bool UI_block_has_active_default_button(const uiBlock *block); uiList *UI_list_find_mouse_over(const ARegion *region, const wmEvent *event); @@ -801,6 +806,33 @@ void UI_popup_block_ex(bContext *C, uiBlockCancelFunc cancel_func, void *arg, wmOperator *op); + +/** + * Return true when #UI_popup_block_template_confirm and related functions are supported. + */ +bool UI_popup_block_template_confirm_is_supported(const uiBlock *block); +/** + * Create confirm & cancel buttons in a popup using callback functions. + */ +void UI_popup_block_template_confirm(uiBlock *block, + bool cancel_default, + blender::FunctionRef confirm_fn, + blender::FunctionRef cancel_fn); +/** + * Create confirm & cancel buttons in a popup using an operator. + * + * \param confirm_text: The text to confirm, null for default text or an empty string to hide. + * \param cancel_text: The text to cancel, null for default text or an empty string to hide. + * \param r_ptr: The pointer for operator properties, set a "confirm" button has been created. + */ +void UI_popup_block_template_confirm_op(uiLayout *layout, + wmOperatorType *ot, + const char *confirm_text, + const char *cancel_text, + const int icon, + bool cancel_default, + PointerRNA *r_ptr); + #if 0 /* UNUSED */ void uiPupBlockOperator(bContext *C, uiBlockCreateFunc func, diff --git a/source/blender/editors/interface/interface_query.cc b/source/blender/editors/interface/interface_query.cc index 6163fce4968..b8b93ee1d8e 100644 --- a/source/blender/editors/interface/interface_query.cc +++ b/source/blender/editors/interface/interface_query.cc @@ -694,6 +694,16 @@ bool UI_block_can_add_separator(const uiBlock *block) return true; } +bool UI_block_has_active_default_button(const uiBlock *block) +{ + LISTBASE_FOREACH (const uiBut *, but, &block->buttons) { + if ((but->flag & UI_BUT_ACTIVE_DEFAULT) && ((but->flag & UI_HIDDEN) == 0)) { + return true; + } + } + return false; +} + /** \} */ /* -------------------------------------------------------------------- */ diff --git a/source/blender/editors/interface/regions/interface_region_menu_popup.cc b/source/blender/editors/interface/regions/interface_region_menu_popup.cc index 8cb725f8a51..ed924ff243f 100644 --- a/source/blender/editors/interface/regions/interface_region_menu_popup.cc +++ b/source/blender/editors/interface/regions/interface_region_menu_popup.cc @@ -712,6 +712,142 @@ void UI_popup_block_ex(bContext *C, WM_event_add_mousemove(window); } +static void popup_block_template_close_cb(bContext *C, void *arg1, void * /*arg2*/) +{ + uiBlock *block = (uiBlock *)arg1; + + uiPopupBlockHandle *handle = block->handle; + if (handle == nullptr) { + printf("Error: used outside of a popup!\n"); + return; + } + + wmWindow *win = CTX_wm_window(C); + UI_popup_menu_retval_set(block, UI_RETURN_CANCEL, true); + + if (handle->cancel_func) { + handle->cancel_func(C, handle->popup_arg); + } + + UI_popup_block_close(C, win, block); +} + +bool UI_popup_block_template_confirm_is_supported(const uiBlock *block) +{ + if (block->flag & (UI_BLOCK_KEEP_OPEN | UI_BLOCK_POPOVER)) { + return true; + } + return false; +} + +void UI_popup_block_template_confirm(uiBlock *block, + const bool cancel_default, + blender::FunctionRef confirm_fn, + blender::FunctionRef cancel_fn) +{ +#ifdef _WIN32 + const bool windows_layout = true; +#else + const bool windows_layout = false; +#endif + blender::FunctionRef *button_functions[2]; + if (windows_layout) { + ARRAY_SET_ITEMS(button_functions, &confirm_fn, &cancel_fn); + } + else { + ARRAY_SET_ITEMS(button_functions, &cancel_fn, &confirm_fn); + } + + for (int i = 0; i < ARRAY_SIZE(button_functions); i++) { + blender::FunctionRef *but_fn = button_functions[i]; + if (uiBut *but = (*but_fn)()) { + const bool is_cancel = (but_fn == &cancel_fn); + if ((block->flag & UI_BLOCK_LOOP) == 0) { + UI_but_func_set(but, popup_block_template_close_cb, block, nullptr); + } + if (is_cancel == cancel_default) { + /* An active button shouldn't exist, if it does, never set another. */ + if (!UI_block_has_active_default_button(block)) { + UI_but_flag_enable(but, UI_BUT_ACTIVE_DEFAULT); + } + } + } + } +} + +void UI_popup_block_template_confirm_op(uiLayout *layout, + wmOperatorType *ot, + const char *confirm_text, + const char *cancel_text, + const int icon, + bool cancel_default, + PointerRNA *r_ptr) +{ + uiBlock *block = uiLayoutGetBlock(layout); + + if (confirm_text == nullptr) { + confirm_text = IFACE_("OK"); + } + if (cancel_text == nullptr) { + cancel_text = IFACE_("Cancel"); + } + + /* Use a split so both buttons are the same size. */ + const bool show_confirm = confirm_text[0] != '\0'; + const bool show_cancel = cancel_text[0] != '\0'; + uiLayout *row = (show_confirm && show_cancel) ? uiLayoutSplit(layout, 0.5f, false) : layout; + + /* When only one button is shown, make it default. */ + if (!show_confirm) { + cancel_default = true; + } + + auto confirm_fn = [&row, &ot, &confirm_text, &icon, &r_ptr, &show_confirm]() -> uiBut * { + if (!show_confirm) { + return nullptr; + } + uiBlock *block = uiLayoutGetBlock(row); + const uiBut *but_ref = (uiBut *)block->buttons.last; + uiItemFullO_ptr(row, + ot, + confirm_text, + icon, + nullptr, + uiLayoutGetOperatorContext(row), + UI_ITEM_NONE, + r_ptr); + + if (but_ref == block->buttons.last) { + return nullptr; + } + return static_cast(block->buttons.last); + }; + + auto cancel_fn = [&row, &cancel_text, &show_cancel]() -> uiBut * { + if (!show_cancel) { + return nullptr; + } + uiBlock *block = uiLayoutGetBlock(row); + uiBut *but = uiDefIconTextBut(block, + UI_BTYPE_BUT, + 1, + ICON_NONE, + cancel_text, + 0, + 0, + UI_UNIT_X, /* Ignored, as a split is used. */ + UI_UNIT_Y, + nullptr, + 0.0, + 0.0, + ""); + + return but; + }; + + UI_popup_block_template_confirm(block, cancel_default, confirm_fn, cancel_fn); +} + #if 0 /* UNUSED */ void uiPupBlockOperator(bContext *C, uiBlockCreateFunc func, diff --git a/source/blender/makesrna/intern/rna_ui_api.cc b/source/blender/makesrna/intern/rna_ui_api.cc index 662c7d405de..e98d8adcc3b 100644 --- a/source/blender/makesrna/intern/rna_ui_api.cc +++ b/source/blender/makesrna/intern/rna_ui_api.cc @@ -976,6 +976,46 @@ void rna_uiTemplateAssetShelfPopover(uiLayout *layout, blender::ui::template_asset_shelf_popover(*layout, *C, asset_shelf_id, name, icon); } +PointerRNA rna_uiTemplatePopupConfirm(uiLayout *layout, + ReportList *reports, + const char *opname, + const char *text, + const char *text_ctxt, + bool translate, + int icon, + const char *cancel_text, + bool cancel_default) +{ + PointerRNA opptr = PointerRNA_NULL; + + /* This allows overriding buttons in `WM_operator_props_dialog_popup` and other popups. */ + wmOperatorType *ot = nullptr; + if (opname[0]) { + /* Confirming is optional. */ + ot = WM_operatortype_find(opname, false); /* print error next */ + } + else { + text = ""; + } + + if (opname[0] ? (!ot || !ot->srna) : false) { + RNA_warning("%s '%s'", ot ? "operator missing srna" : "unknown operator", opname); + } + else if (!UI_popup_block_template_confirm_is_supported(uiLayoutGetBlock(layout))) { + BKE_reportf(reports, RPT_ERROR, "template_popup_confirm used outside of a popup"); + } + else { + text = rna_translate_ui_text(text, text_ctxt, nullptr, nullptr, translate); + if (cancel_text && cancel_text[0]) { + cancel_text = rna_translate_ui_text(cancel_text, text_ctxt, nullptr, nullptr, translate); + } + + UI_popup_block_template_confirm_op( + layout, ot, text, cancel_text, icon, cancel_default, &opptr); + } + return opptr; +} + #else static void api_ui_item_common_heading(FunctionRNA *func) @@ -2324,6 +2364,25 @@ void RNA_api_ui_layout(StructRNA *srna) RNA_def_property_ui_text(parm, "Icon", "Override automatic icon of the item"); parm = RNA_def_property(func, "icon_value", PROP_INT, PROP_UNSIGNED); RNA_def_property_ui_text(parm, "Icon Value", "Override automatic icon of the item"); + + /* A version of `operator` that defines a [Cancel, Confirm] pair of buttons. */ + func = RNA_def_function(srna, "template_popup_confirm", "rna_uiTemplatePopupConfirm"); + api_ui_item_op_common(func); + parm = RNA_def_string(func, + "cancel_text", + nullptr, + 0, + "", + "Optional text to use for the cancel, not shown when an empty string"); + RNA_def_boolean(func, "cancel_default", false, "", "Cancel button by default"); + RNA_def_function_ui_description( + func, "Add confirm & cancel buttons into a popup which will close the popup when pressed"); + RNA_def_function_flag(func, FUNC_USE_REPORTS); + + parm = RNA_def_pointer( + func, "properties", "OperatorProperties", "", "Operator properties to fill in"); + RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED | PARM_RNAPTR); + RNA_def_function_return(func, parm); } #endif diff --git a/source/blender/windowmanager/intern/wm_operators.cc b/source/blender/windowmanager/intern/wm_operators.cc index 9db4ec3d746..fe8c2de35c2 100644 --- a/source/blender/windowmanager/intern/wm_operators.cc +++ b/source/blender/windowmanager/intern/wm_operators.cc @@ -1589,8 +1589,10 @@ static uiBlock *wm_block_dialog_create(bContext *C, ARegion *region, void *user_ const bool windows_layout = false; #endif - /* New column so as not to interfere with custom layouts, see: #26436. */ - { + /* Check there are no active default buttons, allowing a dialog to define it's own + * confirmation buttons which are shown instead of these, see: #124098. */ + if (!UI_block_has_active_default_button(uiLayoutGetBlock(layout))) { + /* New column so as not to interfere with custom layouts, see: #26436. */ uiLayout *col = uiLayoutColumn(layout, false); uiBlock *col_block = uiLayoutGetBlock(col); uiBut *confirm_but;