From 9a3f80b8f2069d439b753e4ce4dafffd2bd0a8d7 Mon Sep 17 00:00:00 2001 From: Harley Acheson Date: Sat, 16 Aug 2025 02:45:36 +0200 Subject: [PATCH] UI: Generalized Alert and Popup Block Error Indication This PR adds an optional block alert level, defaulting to none. Then for error-level alerts only it adds a red line and a color icon. Dialogs with other alert types do not change at all. This also adds a UI_alert function to create such dialogs. Pull Request: https://projects.blender.org/blender/blender/pulls/134084 --- release/datafiles/icons_svg/cancel_large.svg | 2 +- .../blender/editors/include/UI_interface.hh | 2 + source/blender/editors/interface/interface.cc | 5 + .../editors/interface/interface_intern.hh | 4 + .../editors/interface/interface_layout.cc | 16 +++ .../editors/interface/interface_widgets.cc | 43 +++++- .../regions/interface_region_popup.cc | 136 ++++++++++++++++++ 7 files changed, 206 insertions(+), 2 deletions(-) diff --git a/release/datafiles/icons_svg/cancel_large.svg b/release/datafiles/icons_svg/cancel_large.svg index 8d015987075..6f1459c3881 100644 --- a/release/datafiles/icons_svg/cancel_large.svg +++ b/release/datafiles/icons_svg/cancel_large.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/source/blender/editors/include/UI_interface.hh b/source/blender/editors/include/UI_interface.hh index e90a435ec81..5bd604feec0 100644 --- a/source/blender/editors/include/UI_interface.hh +++ b/source/blender/editors/include/UI_interface.hh @@ -292,3 +292,5 @@ blender::ui::AbstractTreeView *UI_block_add_view( uiBlock &block, blender::StringRef idname, std::unique_ptr tree_view); + +void UI_alert(bContext *C, std::string title, std::string message, eAlertIcon icon, bool compact); diff --git a/source/blender/editors/interface/interface.cc b/source/blender/editors/interface/interface.cc index 58cdbc6f2ae..8421471bf7b 100644 --- a/source/blender/editors/interface/interface.cc +++ b/source/blender/editors/interface/interface.cc @@ -5105,6 +5105,11 @@ uiBut *uiDefButAlert(uiBlock *block, int icon, int x, int y, short width, short { ImBuf *ibuf = UI_icon_alert_imbuf_get((eAlertIcon)icon, float(width)); if (ibuf) { + if (icon == ALERT_ICON_ERROR) { + uchar color[4]; + UI_GetThemeColor4ubv(TH_ERROR, color); + return uiDefButImage(block, ibuf, x, y, ibuf->x, ibuf->y, color); + } bTheme *btheme = UI_GetTheme(); return uiDefButImage(block, ibuf, x, y, ibuf->x, ibuf->y, btheme->tui.wcol_menu_back.text); } diff --git a/source/blender/editors/interface/interface_intern.hh b/source/blender/editors/interface/interface_intern.hh index 257f822a4aa..a80f03ee39a 100644 --- a/source/blender/editors/interface/interface_intern.hh +++ b/source/blender/editors/interface/interface_intern.hh @@ -578,6 +578,8 @@ struct uiBlockDynamicListener { void (*listener_func)(const wmRegionListenerParams *params); }; +enum class uiBlockAlertLevel : int8_t { None, Info, Success, Warning, Error }; + struct uiBlock { uiBlock *next, *prev; @@ -609,6 +611,8 @@ struct uiBlock { rctf rect; float aspect; + uiBlockAlertLevel alert_level = uiBlockAlertLevel::None; + /** Unique hash used to implement popup menu memory. */ uint puphash; diff --git a/source/blender/editors/interface/interface_layout.cc b/source/blender/editors/interface/interface_layout.cc index bfcdc74b453..68f82e3a73c 100644 --- a/source/blender/editors/interface/interface_layout.cc +++ b/source/blender/editors/interface/interface_layout.cc @@ -6115,6 +6115,22 @@ uiLayout *uiItemsAlertBox(uiBlock *block, 0, style); + if (icon == ALERT_ICON_INFO) { + block->alert_level = uiBlockAlertLevel::Info; + } + else if (icon == ALERT_ICON_WARNING) { + block->alert_level = uiBlockAlertLevel::Warning; + } + else if (icon == ALERT_ICON_QUESTION) { + block->alert_level = uiBlockAlertLevel::Warning; + } + else if (icon == ALERT_ICON_ERROR) { + block->alert_level = uiBlockAlertLevel::Error; + } + else { + block->alert_level = uiBlockAlertLevel::None; + } + /* Split layout to put alert icon on left side. */ uiLayout *split_block = &block_layout.split(split_factor, false); diff --git a/source/blender/editors/interface/interface_widgets.cc b/source/blender/editors/interface/interface_widgets.cc index 9c5434cd595..9ea6c66b3ec 100644 --- a/source/blender/editors/interface/interface_widgets.cc +++ b/source/blender/editors/interface/interface_widgets.cc @@ -5359,6 +5359,40 @@ static void ui_draw_clip_tri(uiBlock *block, const rcti *rect, uiWidgetType *wt) } } +static void ui_draw_dialog_alert(uiBlock *block, const rcti *rect) +{ + if (block->alert_level != uiBlockAlertLevel::Error) { + return; + } + + float color[4]; + switch (block->alert_level) { + case uiBlockAlertLevel::Error: + UI_GetThemeColor4fv(TH_ERROR, color); + break; + case uiBlockAlertLevel::Warning: + UI_GetThemeColor4fv(TH_WARNING, color); + break; + case uiBlockAlertLevel::Success: + UI_GetThemeColor4fv(TH_SUCCESS, color); + break; + default: + UI_GetThemeColor4fv(TH_INFO, color); + } + + bTheme *btheme = UI_GetTheme(); + const float bg_radius = btheme->tui.wcol_menu_back.roundness * U.widget_unit; + const float line_width = 3.0f * UI_SCALE_FAC; + const float radius = (bg_radius > (line_width * 2.0f)) ? 0.0f : bg_radius; + const float padding = (bg_radius > (line_width * 2.0f)) ? bg_radius : 0.0f; + rctf line_rect; + BLI_rctf_rcti_copy(&line_rect, rect); + line_rect.ymin = line_rect.ymax - line_width; + BLI_rctf_pad(&line_rect, -padding, 0.0f); + UI_draw_roundbox_corner_set(UI_CNR_TOP_LEFT | UI_CNR_TOP_RIGHT); + UI_draw_roundbox_4fv(&line_rect, true, radius, color); +} + void ui_draw_menu_back(uiStyle * /*style*/, uiBlock *block, const rcti *rect) { uiWidgetType *wt = widget_type(UI_WTYPE_MENU_BACK); @@ -5366,7 +5400,14 @@ void ui_draw_menu_back(uiStyle * /*style*/, uiBlock *block, const rcti *rect) wt->state(wt, &STATE_INFO_NULL, blender::ui::EmbossType::Undefined); if (block) { const float zoom = 1.0f / block->aspect; - wt->draw_block(&wt->wcol, rect, block->flag, block->direction, zoom); + wt->draw_block(&wt->wcol, + rect, + block->flag, + block->alert_level == uiBlockAlertLevel::None ? block->direction : UI_DIR_DOWN, + zoom); + if (block->alert_level != uiBlockAlertLevel::None) { + ui_draw_dialog_alert(block, rect); + } } else { wt->draw_block(&wt->wcol, rect, 0, 0, 1.0f); diff --git a/source/blender/editors/interface/regions/interface_region_popup.cc b/source/blender/editors/interface/regions/interface_region_popup.cc index 85502e84c9e..f64d555a442 100644 --- a/source/blender/editors/interface/regions/interface_region_popup.cc +++ b/source/blender/editors/interface/regions/interface_region_popup.cc @@ -17,6 +17,8 @@ #include "DNA_userdef_types.h" +#include "BLF_api.hh" + #include "BLI_listbase.h" #include "BLI_math_vector.h" #include "BLI_rect.h" @@ -1000,4 +1002,138 @@ void ui_popup_block_free(bContext *C, uiPopupBlockHandle *handle) MEM_delete(handle); } +struct ui_alert_data { + eAlertIcon icon; + std::string title; + std::string message; + bool compact; + bool okay_button; + bool mouse_move_quit; +}; + +static void ui_alert_ok_cb(bContext *C, void *arg1, void *arg2) +{ + ui_alert_data *data = static_cast(arg1); + MEM_delete(data); + uiBlock *block = static_cast(arg2); + UI_popup_menu_retval_set(block, UI_RETURN_OK, true); + wmWindow *win = CTX_wm_window(C); + UI_popup_block_close(C, win, block); +} + +static void ui_alert_ok(bContext * /*C*/, void *arg, int /*retval*/) +{ + ui_alert_data *data = static_cast(arg); + MEM_delete(data); +} + +static void ui_alert_cancel(bContext * /*C*/, void *user_data) +{ + ui_alert_data *data = static_cast(user_data); + MEM_delete(data); +} + +static uiBlock *ui_alert_create(bContext *C, ARegion *region, void *user_data) +{ + ui_alert_data *data = static_cast(user_data); + + const uiStyle *style = UI_style_get_dpi(); + const short icon_size = (data->compact ? 32 : 40) * UI_SCALE_FAC; + const int max_width = int((data->compact ? 250.0f : 350.0f) * UI_SCALE_FAC); + const int min_width = int(120.0f * UI_SCALE_FAC); + + uiBlock *block = UI_block_begin(C, region, __func__, blender::ui::EmbossType::Emboss); + UI_block_theme_style_set(block, UI_BLOCK_THEME_STYLE_POPUP); + UI_block_flag_disable(block, UI_BLOCK_LOOP); + UI_block_emboss_set(block, blender::ui::EmbossType::Emboss); + UI_popup_dummy_panel_set(region, block); + + UI_block_flag_enable(block, UI_BLOCK_KEEP_OPEN | UI_BLOCK_NUMSELECT); + if (data->mouse_move_quit) { + UI_block_flag_enable(block, UI_BLOCK_MOVEMOUSE_QUIT); + } + + const uiFontStyle *fstyle = UI_FSTYLE_WIDGET; + + UI_fontstyle_set(&style->widget); + /* Width based on the text lengths. */ + int text_width = BLF_width(style->widget.uifont_id, data->title.c_str(), data->title.size()); + + blender::Vector messages = BLF_string_wrap( + fstyle->uifont_id, data->message, max_width, BLFWrapMode::Typographical); + + for (auto &st_ref : messages) { + const std::string &st = st_ref; + text_width = std::max(text_width, + int(BLF_width(style->widget.uifont_id, st.c_str(), st.size()))); + } + + int dialog_width = std::max(text_width + int(style->columnspace * 2.5), min_width); + + uiLayout *layout; + layout = uiItemsAlertBox(block, style, dialog_width + icon_size, data->icon, icon_size); + + uiLayout *content = &layout->column(false); + content->scale_y_set(0.75f); + + /* Title. */ + uiItemL_ex(content, data->title, ICON_NONE, true, false); + + content->separator(1.0f); + + /* Message lines. */ + for (auto &st : messages) { + content->label(st, ICON_NONE); + } + + if (data->okay_button) { + + layout->separator(2.0f); + + /* Clear so the OK button is left alone. */ + UI_block_func_set(block, nullptr, nullptr, nullptr); + + const float pad = std::max((1.0f - ((200.0f * UI_SCALE_FAC) / float(text_width))) / 2.0f, + 0.01f); + uiLayout *split = &layout->split(pad, true); + split->column(true); + uiLayout *buttons = &split->split(1.0f - (pad * 2.0f), true); + buttons->scale_y_set(1.2f); + + uiBlock *buttons_block = layout->block(); + uiBut *okay_but = uiDefBut( + buttons_block, ButType::But, 0, "OK", 0, 0, 0, UI_UNIT_Y, nullptr, 0, 0, ""); + UI_but_func_set(okay_but, ui_alert_ok_cb, user_data, block); + UI_but_flag_enable(okay_but, UI_BUT_ACTIVE_DEFAULT); + } + + const int padding = (data->compact ? 10 : 14) * UI_SCALE_FAC; + + if (data->mouse_move_quit) { + const float button_center_x = -0.5f; + const float button_center_y = data->okay_button ? 4.0f : 2.0f; + const int bounds_offset[2] = {int(button_center_x * layout->width()), + int(button_center_y * UI_UNIT_X)}; + UI_block_bounds_set_popup(block, padding, bounds_offset); + } + else { + UI_block_bounds_set_centered(block, padding); + } + + return block; +} + +void UI_alert(bContext *C, std::string title, std::string message, eAlertIcon icon, bool compact) +{ + ui_alert_data *data = MEM_new(__func__); + data->title = title; + data->message = message; + data->icon = icon; + data->compact = compact; + data->okay_button = true; + data->mouse_move_quit = compact; + + UI_popup_block_ex(C, ui_alert_create, ui_alert_ok, ui_alert_cancel, data, nullptr); +} + /** \} */