UI: support searching in menus #110855

Merged
Jacques Lucke merged 38 commits from JacquesLucke/blender:single-menu-search into main 2023-09-06 18:16:52 +02:00
14 changed files with 196 additions and 51 deletions

View File

@ -29,6 +29,7 @@ class DATA_PT_modifiers(ModifierButtonsPanel, Panel):
class OBJECT_MT_modifier_add(Menu):
bl_label = "Add Modifier"
bl_options = {'SEARCH_ON_KEY_PRESS'}
def draw(self, context):
layout = self.layout

View File

@ -225,6 +225,7 @@ class NODE_MT_add(bpy.types.Menu):
bl_space_type = 'NODE_EDITOR'
bl_label = "Add"
JacquesLucke marked this conversation as resolved Outdated

It's more consistent to use bl_options to set some flags.

It's more consistent to use `bl_options` to set some flags.
bl_translation_context = i18n_contexts.operator_default
bl_options = {'SEARCH_ON_KEY_PRESS'}
def draw(self, context):
import nodeitems_utils
@ -234,27 +235,16 @@ class NODE_MT_add(bpy.types.Menu):
snode = context.space_data
if snode.tree_type == 'GeometryNodeTree':
props = layout.operator("node.add_search", text="Search...", icon='VIEWZOOM')
layout.separator()
layout.menu_contents("NODE_MT_geometry_node_add_all")
JacquesLucke marked this conversation as resolved Outdated

The compositor can be changed now too.

The compositor can be changed now too.
elif snode.tree_type == 'CompositorNodeTree':
props = layout.operator("node.add_search", text="Search...", icon='VIEWZOOM')
layout.separator()
layout.menu_contents("NODE_MT_compositor_node_add_all")
elif snode.tree_type == 'ShaderNodeTree':
props = layout.operator("node.add_search", text="Search...", icon='VIEWZOOM')
layout.separator()
layout.menu_contents("NODE_MT_shader_node_add_all")
elif snode.tree_type == 'TextureNodeTree':
props = layout.operator("node.add_search", text="Search...", icon='VIEWZOOM')
layout.separator()
layout.menu_contents("NODE_MT_texture_node_add_all")
elif nodeitems_utils.has_node_categories(context):
props = layout.operator("node.add_search", text="Search...", icon='VIEWZOOM')
props.use_transform = True
layout.separator()
# Actual node sub-menus are defined by draw functions from node categories.
nodeitems_utils.draw_node_categories_menu(self, context)

View File

@ -2409,6 +2409,7 @@ class VIEW3D_MT_grease_pencil_add(Menu):
class VIEW3D_MT_add(Menu):
bl_label = "Add"
bl_translation_context = i18n_contexts.operator_default
bl_options = {'SEARCH_ON_KEY_PRESS'}
def draw(self, context):
layout = self.layout

View File

@ -405,6 +405,10 @@ enum class MenuTypeFlag {
* dependent, menu search has to scan it in different contexts.
*/
ContextDependent = (1 << 0),
/**
* Automatically start searching in the menu when pressing a key.
*/
SearchOnKeyPress = (1 << 1),
};
ENUM_OPERATORS(MenuTypeFlag, MenuTypeFlag::ContextDependent)

View File

@ -175,6 +175,8 @@ enum {
UI_BLOCK_SEARCH_ONLY = 1 << 25,
/** Hack for quick setup (splash screen) to draw text centered. */
UI_BLOCK_QUICK_SETUP = 1 << 26,
/** Don't accelerator keys for the items in the block. */
UI_BLOCK_NO_ACCELERATOR_KEYS = 1 << 27,
};
/** #uiPopupBlockHandle.menuretval */
@ -247,6 +249,15 @@ enum {
UI_BUT_OVERRIDDEN = 1u << 31u,
};
enum {
JacquesLucke marked this conversation as resolved Outdated

Prefer to keep this consistent with the existing button flags and using a bare enum with a similar prefix, say UI_BUT2_. Otherwise it make it hard to find these when you're used to the current convention, e.g. you'd typically start by typing UI_BUT and look at the auto-complete results when looking for a flag.

If we want a different convention that should be done separately.

Prefer to keep this consistent with the existing button flags and using a bare enum with a similar prefix, say `UI_BUT2_`. Otherwise it make it hard to find these when you're used to the current convention, e.g. you'd typically start by typing `UI_BUT` and look at the auto-complete results when looking for a flag. If we want a different convention that should be done separately.
/**
JacquesLucke marked this conversation as resolved Outdated

This could use some documentation. What is "on init"? What is "select"? And what is this used for?

This could use some documentation. What is "on init"? What is "select"? And what is this used for?
* This is used when `UI_BUT_ACTIVATE_ON_INIT` is used, which is used to activate e.g. a search
* box as soon as a popup opens. Usually, the text in the search box is selected by default.
* However, sometimes this behavior is not desired, so it can be disabled with this flag.
*/
UI_BUT2_ACTIVATE_ON_INIT_NO_SELECT = 1 << 0,
JacquesLucke marked this conversation as resolved Outdated

Maybe call this ..._NO_SELECT (picky)?

Maybe call this `..._NO_SELECT` (picky)?
};
/** #uiBut.dragflag */
enum {
/** By default only the left part of a button triggers dragging. A questionable design to make
@ -887,6 +898,7 @@ bool UI_but_active_drop_color(bContext *C);
void UI_but_flag_enable(uiBut *but, int flag);
void UI_but_flag_disable(uiBut *but, int flag);
bool UI_but_flag_is_set(uiBut *but, int flag);
void UI_but_flag2_enable(uiBut *but, int flag);
void UI_but_drawflag_enable(uiBut *but, int flag);
void UI_but_drawflag_disable(uiBut *but, int flag);
@ -2404,7 +2416,7 @@ void uiTemplateRunningJobs(uiLayout *layout, bContext *C);
void UI_but_func_operator_search(uiBut *but);
void uiTemplateOperatorSearch(uiLayout *layout);
void UI_but_func_menu_search(uiBut *but);
void UI_but_func_menu_search(uiBut *but, const char *single_menu_idname = nullptr);
void uiTemplateMenuSearch(uiLayout *layout);
/**

View File

@ -2001,7 +2001,9 @@ void UI_block_end_ex(const bContext *C, uiBlock *block, const int xy[2], int r_x
UI_block_layout_resolve(block, nullptr, nullptr);
}
ui_block_align_calc(block, CTX_wm_region(C));
if ((block->flag & UI_BLOCK_LOOP) && (block->flag & UI_BLOCK_NUMSELECT)) {
if ((block->flag & UI_BLOCK_LOOP) && (block->flag & UI_BLOCK_NUMSELECT) &&
(block->flag & UI_BLOCK_NO_ACCELERATOR_KEYS) == 0)
{
ui_menu_block_set_keyaccels(block); /* could use a different flag to check */
}
@ -5848,6 +5850,11 @@ void UI_but_flag_enable(uiBut *but, int flag)
but->flag |= flag;
}
void UI_but_flag2_enable(uiBut *but, int flag)
{
but->flag2 |= flag;
}
void UI_but_flag_disable(uiBut *but, int flag)
{
but->flag &= ~flag;

View File

@ -3480,7 +3480,12 @@ static void ui_textedit_begin(bContext *C, uiBut *but, uiHandleButtonData *data)
/* set cursor pos to the end of the text */
but->editstr = data->str;
but->pos = len;
but->selsta = 0;
if (bool(but->flag2 & UI_BUT2_ACTIVATE_ON_INIT_NO_SELECT)) {
but->selsta = len;
}
else {
but->selsta = 0;
}
but->selend = len;
/* Initialize undo history tracking. */
@ -4316,6 +4321,9 @@ static void ui_block_open_begin(bContext *C, uiBut *but, uiHandleButtonData *dat
}
else if (menufunc) {
data->menu = ui_popup_menu_create(C, data->region, but, menufunc, arg);
if (MenuType *mt = UI_but_menutype_get(but)) {
STRNCPY(data->menu->menu_idname, mt->idname);
}
if (but->block->handle) {
data->menu->popup = but->block->handle->popup;
}
@ -10269,6 +10277,41 @@ float ui_block_calc_pie_segment(uiBlock *block, const float event_xy[2])
return len;
}
static int ui_handle_menu_letter_press(
bContext *C, ARegion *region, uiPopupBlockHandle *menu, const wmEvent *event, uiBlock *block)
{
/* Start menu search on key press if enabled. */
if (menu->menu_idname[0]) {
MenuType *mt = WM_menutype_find(menu->menu_idname, false);
if (bool(mt->flag & MenuTypeFlag::SearchOnKeyPress)) {
uiAfterFunc *after = ui_afterfunc_new();
wmOperatorType *ot = WM_operatortype_find("WM_OT_search_single_menu", false);
after->optype = ot;
after->opcontext = WM_OP_INVOKE_DEFAULT;
after->opptr = MEM_cnew<PointerRNA>(__func__);
WM_operator_properties_create_ptr(after->opptr, ot);
RNA_string_set(after->opptr, "menu_idname", menu->menu_idname);
RNA_string_set(after->opptr, "initial_query", event->utf8_buf);
menu->menuretval = UI_RETURN_OK;
return WM_UI_HANDLER_BREAK;
}
}
/* Handle accelerator keys that allow "pressing" a menu entry by pressing a single key. */
LISTBASE_FOREACH (uiBut *, but, &block->buttons) {
if (!(but->flag & UI_BUT_DISABLED) && but->menu_key == event->type) {
if (but->type == UI_BTYPE_BUT) {
UI_but_execute(C, region, but);
}
else {
ui_handle_button_activate_by_type(C, region, but);
}
return WM_UI_HANDLER_BREAK;
}
}
return WM_UI_HANDLER_CONTINUE;
}
static int ui_handle_menu_event(bContext *C,
const wmEvent *event,
uiPopupBlockHandle *menu,
@ -10709,20 +10752,7 @@ static int ui_handle_menu_event(bContext *C,
menu, but, level, is_parent_menu, retval)) {
break;
}
for (but = static_cast<uiBut *>(block->buttons.first); but; but = but->next) {
if (!(but->flag & UI_BUT_DISABLED) && but->menu_key == event->type) {
if (but->type == UI_BTYPE_BUT) {
UI_but_execute(C, region, but);
}
else {
ui_handle_button_activate_by_type(C, region, but);
}
break;
}
}
retval = WM_UI_HANDLER_BREAK;
retval = ui_handle_menu_letter_press(C, region, menu, event, block);
}
break;
}

View File

@ -151,6 +151,7 @@ struct uiBut {
/** Pointer back to the layout item holding this button. */
uiLayout *layout = nullptr;
int flag = 0;
int flag2 = 0;
int drawflag = 0;
eButType type = eButType(0);
eButPointerType pointype = UI_BUT_POIN_NONE;
@ -848,6 +849,8 @@ struct uiPopupBlockHandle {
bool is_grab;
int grab_xy_prev[2];
/* #endif */
char menu_idname[64];
};
/* -------------------------------------------------------------------- */

View File

@ -5918,9 +5918,12 @@ void UI_menutype_draw(bContext *C, MenuType *mt, uiLayout *layout)
printf("%s: opening menu \"%s\"\n", __func__, mt->idname);
}
uiBlock *block = uiLayoutGetBlock(layout);
if (bool(mt->flag & MenuTypeFlag::SearchOnKeyPress)) {
UI_block_flag_enable(block, UI_BLOCK_NO_ACCELERATOR_KEYS);
}
if (mt->listener) {
/* Forward the menu type listener to the block we're drawing in. */
uiBlock *block = uiLayoutGetBlock(layout);
uiBlockDynamicListener *listener = static_cast<uiBlockDynamicListener *>(
MEM_mallocN(sizeof(*listener), __func__));
listener->listener_func = mt->listener;

View File

@ -397,6 +397,12 @@ static uiPopupBlockHandle *ui_popup_menu_create(
if (but) {
pup->slideout = ui_block_is_menu(but->block);
pup->but = but;
if (MenuType *mt = UI_but_menutype_get(but)) {
if (bool(mt->flag & MenuTypeFlag::SearchOnKeyPress)) {
ED_workspace_status_text(C, TIP_("Type to search..."));
}
}
}
if (!but) {
@ -608,7 +614,12 @@ static void ui_popup_menu_create_from_menutype(bContext *C,
ui_item_menutype_func(C, layout, mt);
});
STRNCPY(handle->menu_idname, mt->idname);
handle->can_refresh = true;
if (bool(mt->flag & MenuTypeFlag::SearchOnKeyPress)) {
ED_workspace_status_text(C, TIP_("Type to search..."));
}
}
int UI_popup_menu_invoke(bContext *C, const char *idname, ReportList *reports)

View File

@ -575,6 +575,11 @@ uiBlock *ui_popup_block_refresh(bContext *C,
block = handle_create_func(C, handle, arg);
}
/* Don't create accelerator keys if the parent menu does not have them. */
if (but && but->block->flag & UI_BLOCK_NO_ACCELERATOR_KEYS) {
block->flag |= UI_BLOCK_NO_ACCELERATOR_KEYS;
}
/* callbacks _must_ leave this for us, otherwise we can't call UI_block_update_from_old */
BLI_assert(!block->endblock);
@ -815,6 +820,10 @@ uiPopupBlockHandle *ui_popup_block_create(bContext *C,
void ui_popup_block_free(bContext *C, uiPopupBlockHandle *handle)
{
/* This disables the status bar text that is set when opening a menu that supports search (see
JacquesLucke marked this conversation as resolved Outdated

Either add a check for MenuTypeFlag::SearchOnKeyPress here or add a comment that references it.

Either add a check for `MenuTypeFlag::SearchOnKeyPress` here or add a comment that references it.
* #MenuTypeFlag::SearchOnKeyPress). */
ED_workspace_status_text(C, nullptr);
/* If this popup is created from a popover which does NOT have keep-open flag set,
* then close the popover too. We could extend this to other popup types too. */
ARegion *region = handle->popup_create_vars.butregion;

View File

@ -428,8 +428,12 @@ static void menu_items_from_all_operators(bContext *C, MenuSearch_Data *data)
* - Look up predefined editor-menus.
* - Look up key-map items which call menus.
*/
static MenuSearch_Data *menu_items_from_ui_create(
bContext *C, wmWindow *win, ScrArea *area_init, ARegion *region_init, bool include_all_areas)
static MenuSearch_Data *menu_items_from_ui_create(bContext *C,
wmWindow *win,
ScrArea *area_init,
ARegion *region_init,
bool include_all_areas,
const char *single_menu_idname)
{
MemArena *memarena = BLI_memarena_new(BLI_MEMARENA_STD_BUFSIZE, __func__);
blender::Map<MenuType *, const char *> menu_display_name_map;
@ -591,11 +595,18 @@ static MenuSearch_Data *menu_items_from_ui_create(
region = region_init;
}
/* Populate menus from the editors,
* note that we could create a fake header, draw the header and extract the menus
* from the buttons, however this is quite involved and can be avoided as by convention
* each space-type has a single root-menu that headers use. */
{
if (single_menu_idname) {
if (MenuType *mt = WM_menutype_find(single_menu_idname, false)) {
if (menu_tagged.add(mt)) {
menu_stack.push({mt});
}
}
}
else {
/* Populate menus from the editors,
* note that we could create a fake header, draw the header and extract the menus
* from the buttons, however this is quite involved and can be avoided as by convention
* each space-type has a single root-menu that headers use. */
const char *idname_array[2] = {nullptr};
int idname_array_len = 0;
@ -798,12 +809,14 @@ static MenuSearch_Data *menu_items_from_ui_create(
}
UI_block_free(nullptr, block);
/* Add key-map items as a second pass,
* so all menus are accessed from the header & top-bar before key shortcuts are expanded. */
if (menu_stack.is_empty() && (has_keymap_menu_items == false)) {
has_keymap_menu_items = true;
menu_types_add_from_keymap_items(
C, win, area, region, menu_stack, menu_to_kmi, menu_tagged);
if (single_menu_idname == nullptr) {
/* Add key-map items as a second pass, so all menus are accessed from the header & top-bar
* before key shortcuts are expanded. */
if (menu_stack.is_empty() && (has_keymap_menu_items == false)) {
has_keymap_menu_items = true;
menu_types_add_from_keymap_items(
C, win, area, region, menu_stack, menu_to_kmi, menu_tagged);
}
}
}
}
@ -887,7 +900,7 @@ static MenuSearch_Data *menu_items_from_ui_create(
* - Many operators need options to be set to give useful results, see: #74157.
* - User who really prefer to list all operators can use #WM_OT_search_operator.
*/
if (U.flag & USER_DEVELOPER_UI) {
if ((U.flag & USER_DEVELOPER_UI) && single_menu_idname == nullptr) {
menu_items_from_all_operators(C, data);
}
@ -1112,15 +1125,17 @@ static ARegion *ui_search_menu_create_tooltip(
/** \name Menu Search Template Public API
* \{ */
void UI_but_func_menu_search(uiBut *but)
void UI_but_func_menu_search(uiBut *but, const char *single_menu_idname)
{
bContext *C = (bContext *)but->block->evil_C;
wmWindow *win = CTX_wm_window(C);
ScrArea *area = CTX_wm_area(C);
ARegion *region = CTX_wm_region(C);
/* When run from top-bar scan all areas in the current window. */
const bool include_all_areas = (area && (area->spacetype == SPACE_TOPBAR));
MenuSearch_Data *data = menu_items_from_ui_create(C, win, area, region, include_all_areas);
const bool include_all_areas = (area && (area->spacetype == SPACE_TOPBAR)) &&
!single_menu_idname;
MenuSearch_Data *data = menu_items_from_ui_create(
C, win, area, region, include_all_areas, single_menu_idname);
UI_but_func_search_set(but,
/* Generic callback. */
ui_searchbox_create_menu,

View File

@ -1995,6 +1995,15 @@ static void rna_def_menu(BlenderRNA *brna)
PropertyRNA *parm;
FunctionRNA *func;
static const EnumPropertyItem menu_flag_items[] = {
{int(MenuTypeFlag::SearchOnKeyPress),
"SEARCH_ON_KEY_PRESS",
0,
"Search on Key Press",
"Open a menu search when a key pressed while the menu is open"},
{0, nullptr, 0, nullptr, nullptr},
};
srna = RNA_def_struct(brna, "Menu", nullptr);
RNA_def_struct_ui_text(srna, "Menu", "Editor menu containing buttons");
RNA_def_struct_sdna(srna, "Menu");
@ -2059,6 +2068,12 @@ static void rna_def_menu(BlenderRNA *brna)
RNA_def_property_string_sdna(prop, nullptr, "type->owner_id");
RNA_def_property_flag(prop, PROP_REGISTER_OPTIONAL);
prop = RNA_def_property(srna, "bl_options", PROP_ENUM, PROP_NONE);
RNA_def_property_enum_sdna(prop, nullptr, "type->flag");
RNA_def_property_enum_items(prop, menu_flag_items);
RNA_def_property_flag(prop, PROP_REGISTER_OPTIONAL | PROP_ENUM_FLAG);
RNA_def_property_ui_text(prop, "Options", "Options for this menu type");
RNA_define_verify_sdna(true);
}

View File

@ -1741,27 +1741,30 @@ static void WM_OT_operator_defaults(wmOperatorType *ot)
enum SearchType {
SEARCH_TYPE_OPERATOR = 0,
SEARCH_TYPE_MENU = 1,
SEARCH_TYPE_SINGLE_MENU = 2,
};
struct SearchPopupInit_Data {
SearchType search_type;
int size[2];
std::string single_menu_idname;
};
static char g_search_text[256] = "";
static uiBlock *wm_block_search_menu(bContext *C, ARegion *region, void *userdata)
{
const SearchPopupInit_Data *init_data = static_cast<const SearchPopupInit_Data *>(userdata);
static char search[256] = "";
uiBlock *block = UI_block_begin(C, region, "_popup", UI_EMBOSS);
UI_block_flag_enable(block, UI_BLOCK_LOOP | UI_BLOCK_MOVEMOUSE_QUIT | UI_BLOCK_SEARCH_MENU);
UI_block_theme_style_set(block, UI_BLOCK_THEME_STYLE_POPUP);
uiBut *but = uiDefSearchBut(block,
search,
g_search_text,
0,
ICON_VIEWZOOM,
sizeof(search),
sizeof(g_search_text),
10,
10,
init_data->size[0],
@ -1776,6 +1779,10 @@ static uiBlock *wm_block_search_menu(bContext *C, ARegion *region, void *userdat
else if (init_data->search_type == SEARCH_TYPE_MENU) {
UI_but_func_menu_search(but);
}
else if (init_data->search_type == SEARCH_TYPE_SINGLE_MENU) {
UI_but_func_menu_search(but, init_data->single_menu_idname.c_str());
UI_but_flag2_enable(but, UI_BUT2_ACTIVATE_ON_INIT_NO_SELECT);
}
else {
BLI_assert_unreachable();
}
@ -1837,16 +1844,33 @@ static int wm_search_menu_invoke(bContext *C, wmOperator *op, const wmEvent *eve
}
}
int search_type;
SearchType search_type;
if (STREQ(op->type->idname, "WM_OT_search_menu")) {
search_type = SEARCH_TYPE_MENU;
}
else if (STREQ(op->type->idname, "WM_OT_search_single_menu")) {
search_type = SEARCH_TYPE_SINGLE_MENU;
}
else {
search_type = SEARCH_TYPE_OPERATOR;
}
static SearchPopupInit_Data data{};
data.search_type = SearchType(search_type);
if (search_type == SEARCH_TYPE_SINGLE_MENU) {
{
char *buffer = RNA_string_get_alloc(op->ptr, "menu_idname", nullptr, 0, nullptr);
data.single_menu_idname = buffer;
MEM_SAFE_FREE(buffer);
}
{
char *buffer = RNA_string_get_alloc(op->ptr, "initial_query", nullptr, 0, nullptr);
STRNCPY(g_search_text, buffer);
MEM_SAFE_FREE(buffer);
}
}
data.search_type = search_type;
data.size[0] = UI_searchbox_size_x() * 2;
data.size[1] = UI_searchbox_size_y();
@ -1877,6 +1901,25 @@ static void WM_OT_search_operator(wmOperatorType *ot)
ot->poll = WM_operator_winactive;
}
static void WM_OT_search_single_menu(wmOperatorType *ot)
{
ot->name = "Search Single Menu";
JacquesLucke marked this conversation as resolved Outdated

Not sure if I noted this before, but I'm unsure about using __func__ for the operator idname. This breaks BPY compatibility when renaming a function (!) and it uses an "implementation-defined format" in C++ (unlike in C). See https://stackoverflow.com/questions/4384765/whats-the-difference-between-pretty-function-function-func or https://en.cppreference.com/w/cpp/preprocessor/replace#Predefined_macros.

Not sure if I noted this before, but I'm unsure about using `__func__` for the operator idname. This breaks BPY compatibility when renaming a function (!) and it uses an "implementation-defined format" in C++ (unlike in C). See https://stackoverflow.com/questions/4384765/whats-the-difference-between-pretty-function-function-func or https://en.cppreference.com/w/cpp/preprocessor/replace#Predefined_macros.
ot->idname = "WM_OT_search_single_menu";
ot->description = "Pop-up a search for a menu in current context";
ot->invoke = wm_search_menu_invoke;
ot->exec = wm_search_menu_exec;
ot->poll = WM_operator_winactive;
RNA_def_string(ot->srna, "menu_idname", nullptr, 0, "Menu Name", "Menu to search in");
RNA_def_string(ot->srna,
"initial_query",
nullptr,
0,
"Initial Query",
"Query to insert into the search box");
}
static int wm_call_menu_exec(bContext *C, wmOperator *op)
{
char idname[BKE_ST_MAXNAME];
@ -3833,6 +3876,7 @@ void wm_operatortypes_register()
WM_operatortype_append(WM_OT_splash_about);
WM_operatortype_append(WM_OT_search_menu);
WM_operatortype_append(WM_OT_search_operator);
WM_operatortype_append(WM_OT_search_single_menu);
WM_operatortype_append(WM_OT_call_menu);
WM_operatortype_append(WM_OT_call_menu_pie);
WM_operatortype_append(WM_OT_call_panel);