From f40d4e0bda1b0302dce26c9451feb1619de6173d Mon Sep 17 00:00:00 2001 From: Harley Acheson Date: Thu, 4 Jan 2024 18:42:21 +0100 Subject: [PATCH] UI: Improved Operator Confirmations Allow our current simple confirmations to be customized and extended with callbacks, allowing different behaviors, different icons, and showing more information. Makes no changes to any existing dialogs. Pull Request: https://projects.blender.org/blender/blender/pulls/104670 --- source/blender/windowmanager/WM_types.hh | 30 +++ .../windowmanager/intern/wm_operators.cc | 202 ++++++++++++++++++ 2 files changed, 232 insertions(+) diff --git a/source/blender/windowmanager/WM_types.hh b/source/blender/windowmanager/WM_types.hh index 6871fb0369b..7dfe27b742c 100644 --- a/source/blender/windowmanager/WM_types.hh +++ b/source/blender/windowmanager/WM_types.hh @@ -919,6 +919,31 @@ struct wmTimer { bool sleep; }; +typedef enum wmWarningSize { + WM_WARNING_SIZE_SMALL = 0, + WM_WARNING_SIZE_LARGE, +} wmWarningSize; + +typedef enum wmWarningPosition { + WM_WARNING_POSITION_MOUSE = 0, + WM_WARNING_POSITION_CENTER, +} wmWarningPosition; + +typedef struct wmWarningDetails { + char title[1024]; + char message[1024]; + char message2[1024]; + char confirm_button[256]; + char cancel_button[256]; + int icon; + wmWarningSize size; + wmWarningPosition position; + bool confirm_default; + bool cancel_default; + bool mouse_move_quit; + bool red_alert; +} wmWarningDetails; + /** * Communication/status data owned by the wmJob, and passed to the worker code when calling * `startjob` callback. @@ -1046,6 +1071,11 @@ struct wmOperatorType { */ std::string (*get_description)(bContext *C, wmOperatorType *ot, PointerRNA *ptr); + /** + * If using WM_operator_confirm the following can override all parts of the dialog. + */ + void (*warning)(struct bContext *C, struct wmOperator *, wmWarningDetails *warning); + /** RNA for properties */ StructRNA *srna; diff --git a/source/blender/windowmanager/intern/wm_operators.cc b/source/blender/windowmanager/intern/wm_operators.cc index 0763ec88b88..0dc01dcf9b3 100644 --- a/source/blender/windowmanager/intern/wm_operators.cc +++ b/source/blender/windowmanager/intern/wm_operators.cc @@ -1181,6 +1181,203 @@ int WM_enum_search_invoke(bContext *C, wmOperator *op, const wmEvent * /*event*/ return OPERATOR_INTERFACE; } +static void wm_operator_block_cancel(bContext *C, void *arg_op, void *arg_block) +{ + wmOperator *op = static_cast(arg_op); + uiBlock *block = static_cast(arg_block); + UI_popup_block_close(C, CTX_wm_window(C), block); + WM_redraw_windows(C); + if (op) { + if (op->type->cancel) { + op->type->cancel(C, op); + } + WM_operator_free(op); + } +} + +static void wm_operator_block_confirm(bContext *C, void *arg_op, void *arg_block) +{ + wmOperator *op = static_cast(arg_op); + uiBlock *block = static_cast(arg_block); + UI_popup_block_close(C, CTX_wm_window(C), block); + WM_redraw_windows(C); + if (op) { + WM_operator_call_ex(C, op, true); + } +} + +static uiBlock *wm_block_confirm_create(bContext *C, ARegion *region, void *arg_op) +{ + wmOperator *op = static_cast(arg_op); + + wmWarningDetails warning = {{0}}; + + STRNCPY(warning.title, WM_operatortype_description(C, op->type, op->ptr).c_str()); + STRNCPY(warning.confirm_button, WM_operatortype_name(op->type, op->ptr).c_str()); + STRNCPY(warning.cancel_button, IFACE_("Cancel")); + warning.icon = ALERT_ICON_WARNING; + warning.size = WM_WARNING_SIZE_SMALL; + warning.position = WM_WARNING_POSITION_MOUSE; + warning.confirm_default = true; + warning.cancel_default = false; + warning.mouse_move_quit = false; + warning.red_alert = false; + + /* uiBlock.flag */ + int block_flags = UI_BLOCK_KEEP_OPEN | UI_BLOCK_NO_WIN_CLIP | UI_BLOCK_NUMSELECT; + + if (op->type->warning) { + op->type->warning(C, op, &warning); + } + if (warning.mouse_move_quit) { + block_flags |= UI_BLOCK_MOVEMOUSE_QUIT; + } + if (warning.icon < ALERT_ICON_WARNING || warning.icon >= ALERT_ICON_MAX) { + warning.icon = ALERT_ICON_QUESTION; + } + + uiBlock *block = UI_block_begin(C, region, __func__, UI_EMBOSS); + UI_block_theme_style_set(block, UI_BLOCK_THEME_STYLE_POPUP); + UI_block_flag_enable(block, block_flags); + + const uiStyle *style = UI_style_get_dpi(); + int text_width = MAX2( + 120 * UI_SCALE_FAC, + BLF_width(style->widget.uifont_id, warning.title, ARRAY_SIZE(warning.title))); + if (warning.message[0]) { + text_width = MAX2( + text_width, + BLF_width(style->widget.uifont_id, warning.message, ARRAY_SIZE(warning.message))); + } + if (warning.message2[0]) { + text_width = MAX2( + text_width, + BLF_width(style->widget.uifont_id, warning.message2, ARRAY_SIZE(warning.message2))); + } + + const bool small = warning.size == WM_WARNING_SIZE_SMALL; + const int padding = (small ? 7 : 14) * UI_SCALE_FAC; + const short icon_size = (small ? (warning.message[0] ? 48 : 32) : 64) * UI_SCALE_FAC; + const int dialog_width = icon_size + text_width + (style->columnspace * 2.5); + const float split_factor = (float)icon_size / (float)(dialog_width - style->columnspace); + + uiLayout *block_layout = UI_block_layout( + block, UI_LAYOUT_VERTICAL, UI_LAYOUT_PANEL, 0, 0, dialog_width, UI_UNIT_Y, 0, style); + + /* Split layout to put alert icon on left side. */ + uiLayout *split_block = uiLayoutSplit(block_layout, split_factor, false); + + /* Alert icon on the left. */ + uiLayout *layout = uiLayoutRow(split_block, true); + /* Using 'align_left' with 'row' avoids stretching the icon along the width of column. */ + uiLayoutSetAlignment(layout, UI_LAYOUT_ALIGN_LEFT); + uiDefButAlert(block, warning.icon, 0, 0, icon_size, icon_size); + + /* The rest of the content on the right. */ + layout = uiLayoutColumn(split_block, true); + + if (warning.title[0]) { + if (!warning.message[0]) { + uiItemS(layout); + } + uiItemL_ex(layout, warning.title, ICON_NONE, true, false); + } + if (warning.message[0]) { + uiItemL(layout, warning.message, ICON_NONE); + } + if (warning.message2[0]) { + uiItemL(layout, warning.message2, ICON_NONE); + } + + uiItemS_ex(layout, small ? 0.5f : 4.0f); + + /* Buttons. */ + +#ifdef _WIN32 + const bool windows_layout = true; +#else + const bool windows_layout = false; +#endif + + uiBut *confirm = nullptr; + uiBut *cancel = nullptr; + int height = UI_UNIT_Y; + uiLayout *split = uiLayoutSplit(small ? block_layout : layout, 0.0f, true); + uiLayoutSetScaleY(split, small ? 1.1f : 1.2f); + uiLayoutColumn(split, false); + + if (windows_layout) { + confirm = uiDefIconTextBut(block, + UI_BTYPE_BUT, + 0, + 0, + warning.confirm_button, + 0, + 0, + 0, + height, + 0, + 0, + 0, + 0, + 0, + nullptr); + uiLayoutColumn(split, false); + } + + cancel = uiDefIconTextBut( + block, UI_BTYPE_BUT, 0, 0, warning.cancel_button, 0, 0, 0, height, 0, 0, 0, 0, 0, nullptr); + + if (!windows_layout) { + uiLayoutColumn(split, false); + confirm = uiDefIconTextBut(block, + UI_BTYPE_BUT, + 0, + 0, + warning.confirm_button, + 0, + 0, + 0, + height, + 0, + 0, + 0, + 0, + 0, + nullptr); + } + + UI_block_func_set(block, nullptr, nullptr, nullptr); + UI_but_func_set(confirm, wm_operator_block_confirm, op, block); + UI_but_func_set(cancel, wm_operator_block_cancel, op, block); + UI_but_drawflag_disable(confirm, UI_BUT_TEXT_LEFT); + UI_but_drawflag_disable(cancel, UI_BUT_TEXT_LEFT); + + if (warning.red_alert) { + UI_but_flag_enable(confirm, UI_BUT_REDALERT); + } + else { + if (warning.cancel_default) { + UI_but_flag_enable(cancel, UI_BUT_ACTIVE_DEFAULT); + } + else if (warning.confirm_default) { + UI_but_flag_enable(confirm, UI_BUT_ACTIVE_DEFAULT); + } + } + + if (warning.position == WM_WARNING_POSITION_MOUSE) { + int bounds_offset[2]; + bounds_offset[0] = uiLayoutGetWidth(layout) * (windows_layout ? -0.33f : -0.66f); + bounds_offset[1] = UI_UNIT_Y * (warning.message[0] ? 3.1 : 2.5); + UI_block_bounds_set_popup(block, padding, bounds_offset); + } + else if (warning.position == WM_WARNING_POSITION_CENTER) { + UI_block_bounds_set_centered(block, padding); + } + + return block; +} + int WM_operator_confirm_message_ex(bContext *C, wmOperator *op, const char *title, @@ -1208,6 +1405,11 @@ int WM_operator_confirm_message_ex(bContext *C, int WM_operator_confirm_message(bContext *C, wmOperator *op, const char *message) { + if (op->type->warning) { + UI_popup_block_invoke(C, wm_block_confirm_create, op, nullptr); + return OPERATOR_RUNNING_MODAL; + } + return WM_operator_confirm_message_ex( C, op, IFACE_("OK?"), ICON_QUESTION, message, WM_OP_EXEC_REGION_WIN); }