UI: add menu search functionality to operator search menu

This has some advantages over operator search:

- Some operators need options set to be usefully accessed.
- Shows key bindings to access menus
  (for actions that don't have key bindings themselves).
- Non operator actions such as check-boxes are also shown.
- Menu items can control execution context, using invoke or execute
  where appropriate so we can control how the operator runs.

Part of the design task T74157.

This can be tested using the 'Experimental' preferences section
or selected in the key-map editor.
This commit is contained in:
Campbell Barton 2020-03-24 11:34:18 +11:00
parent 94b8166a8b
commit c46dcdf887
Notes: blender-bot 2023-02-14 05:43:04 +01:00
Referenced by commit 1239cab11f, Fix T75203: Crash when changing active keying set
Referenced by issue #75325, Crash after removing a "target" from any search button [e.g.UVMap node] and clicking again on the same button
Referenced by issue #75203, Crash when changing active keying set.
Referenced by issue #74157, Use menus for operator search & various improvements
16 changed files with 828 additions and 72 deletions

View File

@ -207,7 +207,8 @@ class TOPBAR_MT_editor_menus(Menu):
def draw(self, context):
layout = self.layout
if context.area.show_menus:
# Allow calling this menu directly (this might not be a header area).
if getattr(context.area, "show_menus"):
layout.menu("TOPBAR_MT_app", text="", icon='BLENDER')
else:
layout.menu("TOPBAR_MT_app", text="Blender")

View File

@ -2134,6 +2134,21 @@ class ExperimentalPanel:
url_prefix = "https://developer.blender.org/"
def _draw_items(self, context, items):
prefs = context.preferences
experimental = prefs.experimental
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
for prop_keywords, task in items:
split = layout.split(factor=0.66)
col = split.split()
col.prop(experimental, **prop_keywords)
col = split.split()
col.operator("wm.url_open", text=task, icon='URL').url = self.url_prefix + task
"""
# Example panel, leave it here so we always have a template to follow even
# after the features are gone from the experimental panel.
@ -2142,46 +2157,34 @@ class USERPREF_PT_experimental_virtual_reality(ExperimentalPanel, Panel):
bl_label = "Virtual Reality"
def draw(self, context):
prefs = context.preferences
experimental = prefs.experimental
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
task = "T71347"
split = layout.split(factor=0.66)
col = split.split()
col.prop(experimental, "use_virtual_reality_scene_inspection", text="Scene Inspection")
col = split.split()
col.operator("wm.url_open", text=task, icon='URL').url = self.url_prefix + task
task = "T71348"
split = layout.split(factor=0.66)
col = split.column()
col.prop(experimental, "use_virtual_reality_immersive_drawing", text="Continuous Immersive Drawing")
col = split.column()
col.operator("wm.url_open", text=task, icon='URL').url = self.url_prefix + task
self._draw_items(
context, (
({"property": "use_virtual_reality_scene_inspection"}, "T71347"),
({"property": "use_virtual_reality_immersive_drawing"}, "T71348"),
)
)
"""
class USERPREF_PT_experimental_ui(ExperimentalPanel, Panel):
bl_label = "UI"
def draw(self, context):
self._draw_items(
context, (
({"property": "use_menu_search"}, "T74157"),
),
)
class USERPREF_PT_experimental_system(ExperimentalPanel, Panel):
bl_label = "System"
def draw(self, context):
prefs = context.preferences
experimental = prefs.experimental
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
task = "T60695"
split = layout.split(factor=0.66)
col = split.split()
col.prop(experimental, "use_undo_speedup")
col = split.split()
col.operator("wm.url_open", text=task, icon='URL').url = self.url_prefix + task
self._draw_items(
context, (
({"property": "use_undo_speedup"}, "T60695"),
),
)
# -----------------------------------------------------------------------------
@ -2274,6 +2277,7 @@ classes = (
# Popovers.
USERPREF_PT_ndof_settings,
USERPREF_PT_experimental_ui,
USERPREF_PT_experimental_system,
# Add dynamically generated editor theme panels last,

View File

@ -506,6 +506,9 @@ typedef void (*uiButSearchFunc)(const struct bContext *C,
void *arg,
const char *str,
uiSearchItems *items);
typedef void (*uiButSearchArgFreeFunc)(void *arg);
/* Must return allocated string. */
typedef char *(*uiButToolTipFunc)(struct bContext *C, void *argN, const char *tip);
typedef int (*uiButPushedStateFunc)(struct bContext *C, void *arg);
@ -1565,13 +1568,13 @@ eAutoPropButsReturn uiDefAutoButsRNA(uiLayout *layout,
const bool compact);
/* use inside searchfunc to add items */
bool UI_search_item_add(uiSearchItems *items, const char *name, void *poin, int iconid);
bool UI_search_item_add(uiSearchItems *items, const char *name, void *poin, int iconid, int state);
/* bfunc gets search item *poin as arg2, or if NULL the old string */
void UI_but_func_search_set(uiBut *but,
uiButSearchCreateFunc cfunc,
uiButSearchFunc sfunc,
void *arg,
bool free_arg,
uiButSearchArgFreeFunc search_arg_free_func,
uiButHandleFunc bfunc,
void *active);
/* height in pixels, it's using hardcoded values still */
@ -2035,6 +2038,10 @@ void uiTemplateImageInfo(uiLayout *layout,
void uiTemplateRunningJobs(uiLayout *layout, struct bContext *C);
void UI_but_func_operator_search(uiBut *but);
void uiTemplateOperatorSearch(uiLayout *layout);
void UI_but_func_menu_search(uiBut *but);
void uiTemplateMenuSearch(uiLayout *layout);
eAutoPropButsReturn uiTemplateOperatorPropertyButs(const struct bContext *C,
uiLayout *layout,
struct wmOperator *op,

View File

@ -3207,8 +3207,9 @@ static void ui_but_free(const bContext *C, uiBut *but)
MEM_freeN(but->hold_argN);
}
if (but->free_search_arg) {
MEM_SAFE_FREE(but->search_arg);
if (but->search_arg_free_func) {
but->search_arg_free_func(but->search_arg);
but->search_arg = NULL;
}
if (but->active) {
@ -6336,7 +6337,7 @@ void UI_but_func_search_set(uiBut *but,
uiButSearchCreateFunc search_create_func,
uiButSearchFunc search_func,
void *arg,
bool free_arg,
uiButSearchArgFreeFunc search_arg_free_func,
uiButHandleFunc bfunc,
void *active)
{
@ -6346,14 +6347,16 @@ void UI_but_func_search_set(uiBut *but,
search_create_func = ui_searchbox_create_generic;
}
if (but->free_search_arg) {
MEM_SAFE_FREE(but->search_arg);
if (but->search_arg_free_func != NULL) {
but->search_arg_free_func(but->search_arg);
but->search_arg = NULL;
}
but->search_create_func = search_create_func;
but->search_func = search_func;
but->search_arg = arg;
but->free_search_arg = free_arg;
but->search_arg_free_func = search_arg_free_func;
if (bfunc) {
#ifdef DEBUG
@ -6404,8 +6407,7 @@ static void operator_enum_search_cb(const struct bContext *C,
/* note: need to give the index rather than the
* identifier because the enum can be freed */
if (BLI_strcasestr(item->name, str)) {
if (false ==
UI_search_item_add(items, item->name, POINTER_FROM_INT(item->value), item->icon)) {
if (!UI_search_item_add(items, item->name, POINTER_FROM_INT(item->value), item->icon, 0)) {
break;
}
}
@ -6462,7 +6464,7 @@ uiBut *uiDefSearchButO_ptr(uiBlock *block,
ui_searchbox_create_generic,
operator_enum_search_cb,
but,
false,
NULL,
operator_enum_call_cb,
NULL);

View File

@ -421,6 +421,9 @@ typedef struct uiAfterFunc {
PointerRNA rnapoin;
PropertyRNA *rnaprop;
void *search_arg;
uiButSearchArgFreeFunc search_arg_free_func;
bContextStore *context;
char undostr[BKE_UNDO_STR_MAX];
@ -755,6 +758,11 @@ static void ui_apply_but_func(bContext *C, uiBut *but)
after->rnapoin = but->rnapoin;
after->rnaprop = but->rnaprop;
after->search_arg_free_func = but->search_arg_free_func;
after->search_arg = but->search_arg;
but->search_arg_free_func = NULL;
but->search_arg = NULL;
if (but->context) {
after->context = CTX_store_copy(but->context);
}
@ -921,6 +929,10 @@ static void ui_apply_but_funcs_after(bContext *C)
MEM_freeN(after.rename_orig);
}
if (after.search_arg_free_func) {
after.search_arg_free_func(after.search_arg);
}
ui_afterfunc_update_preferences_dirty(&after);
if (after.undostr[0]) {

View File

@ -203,8 +203,8 @@ struct uiBut {
uiButSearchCreateFunc search_create_func;
uiButSearchFunc search_func;
bool free_search_arg;
void *search_arg;
uiButSearchArgFreeFunc search_arg_free_func;
uiButHandleRenameFunc rename_func;
void *rename_arg1;
@ -649,6 +649,8 @@ ColorPicker *ui_block_colorpicker_create(struct uiBlock *block);
/* Searchbox for string button */
ARegion *ui_searchbox_create_generic(struct bContext *C, struct ARegion *butregion, uiBut *but);
ARegion *ui_searchbox_create_operator(struct bContext *C, struct ARegion *butregion, uiBut *but);
ARegion *ui_searchbox_create_menu(struct bContext *C, struct ARegion *butregion, uiBut *but);
bool ui_searchbox_inside(struct ARegion *region, int x, int y);
int ui_searchbox_find_index(struct ARegion *region, const char *name);
void ui_searchbox_update(struct bContext *C, struct ARegion *region, uiBut *but, const bool reset);

View File

@ -2610,7 +2610,7 @@ void ui_but_add_search(
ui_searchbox_create_generic,
ui_rna_collection_search_cb,
coll_search,
true,
MEM_freeN,
NULL,
NULL);
}

View File

@ -73,6 +73,7 @@ struct uiSearchItems {
char **names;
void **pointers;
int *icons;
int *states;
AutoComplete *autocpl;
void *active;
@ -95,9 +96,18 @@ typedef struct uiSearchboxData {
#define SEARCH_ITEMS 10
/* exported for use by search callbacks */
/* returns zero if nothing to add */
bool UI_search_item_add(uiSearchItems *items, const char *name, void *poin, int iconid)
/**
* Public function exported for functions that use #UI_BTYPE_SEARCH_MENU.
*
* \param items: Stores the items.
* \param name: Text to display for the item.
* \param poin: Opaque pointer (for use by the caller).
* \param iconid: The icon, #ICON_NONE for no icon.
* \param state: The buttons state flag, compatible with #uiBut.flag,
* typically #UI_BUT_DISABLED / #UI_BUT_INACTIVE.
* \return false if there is nothing to add.
*/
bool UI_search_item_add(uiSearchItems *items, const char *name, void *poin, int iconid, int state)
{
/* hijack for autocomplete */
if (items->autocpl) {
@ -135,6 +145,13 @@ bool UI_search_item_add(uiSearchItems *items, const char *name, void *poin, int
items->icons[items->totitem] = iconid;
}
/* Limit flags that can be set so flags such as 'UI_SELECT' aren't accidentally set
* which will cause problems, add others as needed. */
BLI_assert((state & ~(UI_BUT_DISABLED | UI_BUT_INACTIVE | UI_BUT_REDALERT)) == 0);
if (items->states) {
items->states[items->totitem] = state;
}
items->totitem++;
return true;
@ -421,17 +438,16 @@ static void ui_searchbox_region_draw_cb(const bContext *C, ARegion *region)
if (data->preview) {
/* draw items */
for (a = 0; a < data->items.totitem; a++) {
const int state = ((a == data->active) ? UI_ACTIVE : 0) | data->items.states[a];
/* ensure icon is up-to-date */
ui_icon_ensure_deferred(C, data->items.icons[a], data->preview);
ui_searchbox_butrect(&rect, data, a);
/* widget itself */
ui_draw_preview_item(&data->fstyle,
&rect,
data->items.names[a],
data->items.icons[a],
(a == data->active) ? UI_ACTIVE : 0);
ui_draw_preview_item(
&data->fstyle, &rect, data->items.names[a], data->items.icons[a], state);
}
/* indicate more */
@ -451,6 +467,8 @@ static void ui_searchbox_region_draw_cb(const bContext *C, ARegion *region)
else {
/* draw items */
for (a = 0; a < data->items.totitem; a++) {
const int state = ((a == data->active) ? UI_ACTIVE : 0) | data->items.states[a];
ui_searchbox_butrect(&rect, data, a);
/* widget itself */
@ -458,7 +476,7 @@ static void ui_searchbox_region_draw_cb(const bContext *C, ARegion *region)
&rect,
data->items.names[a],
data->items.icons[a],
(a == data->active) ? UI_ACTIVE : 0,
state,
data->use_sep);
}
/* indicate more */
@ -490,6 +508,7 @@ static void ui_searchbox_region_free_cb(ARegion *region)
MEM_freeN(data->items.names);
MEM_freeN(data->items.pointers);
MEM_freeN(data->items.icons);
MEM_freeN(data->items.states);
MEM_freeN(data);
region->regiondata = NULL;
@ -657,6 +676,7 @@ ARegion *ui_searchbox_create_generic(bContext *C, ARegion *butregion, uiBut *but
data->items.names = MEM_callocN(data->items.maxitem * sizeof(void *), "search names");
data->items.pointers = MEM_callocN(data->items.maxitem * sizeof(void *), "search pointers");
data->items.icons = MEM_callocN(data->items.maxitem * sizeof(int), "search icons");
data->items.states = MEM_callocN(data->items.maxitem * sizeof(int), "search flags");
for (i = 0; i < data->items.maxitem; i++) {
data->items.names[i] = MEM_callocN(but->hardmax + 1, "search pointers");
}
@ -718,9 +738,9 @@ static void ui_searchbox_region_draw_cb__operator(const bContext *UNUSED(C), ARe
/* widget itself */
/* NOTE: i18n messages extracting tool does the same, please keep it in sync. */
{
wmOperatorType *ot = data->items.pointers[a];
const int state = ((a == data->active) ? UI_ACTIVE : 0) | data->items.states[a];
int state = (a == data->active) ? UI_ACTIVE : 0;
wmOperatorType *ot = data->items.pointers[a];
char text_pre[128];
char *text_pre_p = strstr(ot->idname, "_OT_");
if (text_pre_p == NULL) {
@ -780,6 +800,25 @@ void ui_searchbox_free(bContext *C, ARegion *region)
ui_region_temp_remove(C, CTX_wm_screen(C), region);
}
static void ui_searchbox_region_draw_cb__menu(const bContext *UNUSED(C), ARegion *UNUSED(region))
{
/* Currently unused. */
}
ARegion *ui_searchbox_create_menu(bContext *C, ARegion *butregion, uiBut *but)
{
ARegion *region;
UI_but_drawflag_enable(but, UI_BUT_HAS_SHORTCUT);
region = ui_searchbox_create_generic(C, butregion, but);
if (false) {
region->type->draw = ui_searchbox_region_draw_cb__menu;
}
return region;
}
/* sets red alert if button holds a string it can't find */
/* XXX weak: search_func adds all partial matches... */
void ui_but_search_refresh(uiBut *but)

View File

@ -38,13 +38,17 @@
#include "DNA_texture_types.h"
#include "BLI_alloca.h"
#include "BLI_dynstr.h"
#include "BLI_fnmatch.h"
#include "BLI_ghash.h"
#include "BLI_linklist.h"
#include "BLI_listbase.h"
#include "BLI_math.h"
#include "BLI_memarena.h"
#include "BLI_path_util.h"
#include "BLI_rect.h"
#include "BLI_string.h"
#include "BLI_string_utils.h"
#include "BLI_timecode.h"
#include "BLI_utildefines.h"
@ -99,6 +103,9 @@
#include "PIL_time.h"
/* For key-map item access. */
#include "wm_event_system.h"
// #define USE_OP_RESET_BUT // we may want to make this optional, disable for now.
/* defines for templateID/TemplateSearch */
@ -281,7 +288,7 @@ static uiBlock *template_common_search_menu(const bContext *C,
"");
}
UI_but_func_search_set(
but, ui_searchbox_create_generic, search_func, search_arg, false, handle_func, active_item);
but, ui_searchbox_create_generic, search_func, search_arg, NULL, handle_func, active_item);
UI_block_bounds_set_normal(block, 0.3f * U.widget_unit);
UI_block_direction_set(block, UI_DIR_DOWN);
@ -363,7 +370,7 @@ static bool id_search_add(const bContext *C,
int iconid = ui_id_icon_get(C, id, template_ui->preview);
if (false == UI_search_item_add(items, name_ui, id, iconid)) {
if (!UI_search_item_add(items, name_ui, id, iconid, 0)) {
return false;
}
}
@ -6643,7 +6650,7 @@ static void operator_search_cb(const bContext *C,
}
}
if (false == UI_search_item_add(items, name, ot, 0)) {
if (!UI_search_item_add(items, name, ot, ICON_NONE, 0)) {
break;
}
}
@ -6673,6 +6680,646 @@ void uiTemplateOperatorSearch(uiLayout *layout)
/** \} */
/* -------------------------------------------------------------------- */
/** \name Menu Search Template
* \{ */
struct MenuSearch_Parent {
struct MenuSearch_Parent *parent;
MenuType *parent_mt;
/* Set while writing menu items only. */
struct MenuSearch_Parent *temp_child;
const char *drawstr;
};
struct MenuSearch_Item {
struct MenuSearch_Item *next, *prev;
const char *drawstr;
const char *drawwstr_full;
/** Support a single level sub-menu nesting (for operator buttons that expand). */
const char *drawstr_submenu;
int icon;
int state;
struct MenuSearch_Parent *menu_parent;
MenuType *mt;
enum {
MENU_SEARCH_TYPE_OP = 1,
MENU_SEARCH_TYPE_RNA = 2,
} type;
union {
/* Operator menu item. */
struct {
wmOperatorType *type;
PointerRNA *opptr;
short opcontext;
bContextStore *context;
} op;
/* Property (only for check-boxe/boolean). */
struct {
PointerRNA ptr;
PropertyRNA *prop;
int index;
/** Only for enum buttons. */
int enum_value;
} rna;
};
};
struct MenuSearch_Data {
/** MenuSearch_Item */
ListBase items;
/** Use for all small allocations. */
MemArena *memarena;
};
static int menu_item_sort_by_drawstr_full(const void *menu_item_a_v, const void *menu_item_b_v)
{
const struct MenuSearch_Item *menu_item_a = menu_item_a_v;
const struct MenuSearch_Item *menu_item_b = menu_item_b_v;
return strcmp(menu_item_a->drawwstr_full, menu_item_b->drawwstr_full);
}
static const char *strdup_memarena(MemArena *memarena, const char *str)
{
const uint str_size = strlen(str) + 1;
char *str_dst = BLI_memarena_alloc(memarena, str_size);
memcpy(str_dst, str, str_size);
return str_dst;
}
static const char *strdup_memarena_from_dynstr(MemArena *memarena, DynStr *dyn_str)
{
const uint str_size = BLI_dynstr_get_len(dyn_str) + 1;
char *str_dst = BLI_memarena_alloc(memarena, str_size);
BLI_dynstr_get_cstring_ex(dyn_str, str_dst);
return str_dst;
}
static bool menu_items_from_ui_create_item_from_button(struct MenuSearch_Data *data,
MemArena *memarena,
struct MenuType *mt,
const char *drawstr_submenu,
uiBut *but)
{
struct MenuSearch_Item *item = NULL;
if (but->optype != NULL) {
item = BLI_memarena_calloc(memarena, sizeof(*item));
item->type = MENU_SEARCH_TYPE_OP;
item->op.type = but->optype;
item->op.opcontext = but->opcontext;
item->op.context = but->context;
item->op.opptr = but->opptr;
but->opptr = NULL;
}
else if (but->rnaprop != NULL) {
const int prop_type = RNA_property_type(but->rnaprop);
if (!ELEM(prop_type, PROP_BOOLEAN, PROP_ENUM)) {
/* Note that these buttons are not prevented,
* but aren't typically used in menus. */
printf("Button '%s' in menu '%s' is a menu item with unsupported RNA type %d\n",
but->drawstr,
mt->idname,
prop_type);
}
else {
item = BLI_memarena_calloc(memarena, sizeof(*item));
item->type = MENU_SEARCH_TYPE_RNA;
item->rna.ptr = but->rnapoin;
item->rna.prop = but->rnaprop;
item->rna.index = but->rnaindex;
if (prop_type == PROP_ENUM) {
item->rna.enum_value = (int)but->hardmax;
}
}
}
if (item != NULL) {
/* Handle shared settings. */
item->drawstr = strdup_memarena(memarena, but->drawstr);
item->icon = but->icon;
item->state = (but->flag & (UI_BUT_DISABLED | UI_BUT_INACTIVE | UI_BUT_REDALERT));
item->mt = mt;
item->drawstr_submenu = drawstr_submenu ? strdup_memarena(memarena, drawstr_submenu) : NULL;
BLI_addtail(&data->items, item);
return true;
}
return false;
}
/**
* Populate \a menu_stack with menus from inspecting active key-maps for this context.
*/
static void menu_types_add_from_keymap_items(bContext *C,
wmWindow *win,
ScrArea *sa,
ARegion *region,
LinkNode **menuid_stack_p,
GHash *menu_to_kmi,
GSet *menu_tagged)
{
wmWindowManager *wm = CTX_wm_manager(C);
ListBase *handlers[] = {
&region->handlers,
&sa->handlers,
&win->handlers,
};
for (int handler_index = 0; handler_index < ARRAY_SIZE(handlers); handler_index++) {
LISTBASE_FOREACH (wmEventHandler *, handler_base, handlers[handler_index]) {
/* During this loop, ui handlers for nested menus can tag multiple handlers free. */
if (handler_base->flag & WM_HANDLER_DO_FREE) {
continue;
}
if (handler_base->type != WM_HANDLER_TYPE_KEYMAP) {
continue;
}
else if (handler_base->poll == NULL || handler_base->poll(region, win->eventstate)) {
wmEventHandler_Keymap *handler = (wmEventHandler_Keymap *)handler_base;
wmKeyMap *keymap = WM_event_get_keymap_from_handler(wm, handler);
if (keymap && WM_keymap_poll(C, keymap)) {
LISTBASE_FOREACH (wmKeyMapItem *, kmi, &keymap->items) {
if (kmi->flag & KMI_INACTIVE) {
continue;
}
if (STR_ELEM(kmi->idname, "WM_OT_call_menu", "WM_OT_call_menu_pie")) {
char menu_idname[MAX_NAME];
RNA_string_get(kmi->ptr, "name", menu_idname);
MenuType *mt = WM_menutype_find(menu_idname, false);
if (mt && BLI_gset_add(menu_tagged, mt)) {
/* Unlikely, but possible this will be included twice. */
BLI_linklist_prepend(menuid_stack_p, mt);
void **kmi_p;
if (!BLI_ghash_ensure_p(menu_to_kmi, mt, &kmi_p)) {
*kmi_p = kmi;
}
}
}
}
}
}
}
}
}
/**
* Create #MenuSearch_Data by inspecting the current context, this uses two methods:
*
* - Look-up pre-defined editor-menus.
* - Look-up key-map items which call menus.
*/
static struct MenuSearch_Data *menu_items_from_ui_create(bContext *C,
wmWindow *win,
ScrArea *sa,
ARegion *region)
{
MemArena *memarena = BLI_memarena_new(BLI_MEMARENA_STD_BUFSIZE, __func__);
/** Map (#MenuType to #MenuSearch_Parent) */
GHash *menu_parent_map = BLI_ghash_ptr_new(__func__);
GHash *menu_display_name_map = BLI_ghash_ptr_new(__func__);
const uiStyle *style = UI_style_get_dpi();
/* Convert into non-ui structure. */
struct MenuSearch_Data *data = MEM_callocN(sizeof(*data), __func__);
DynStr *dyn_str = BLI_dynstr_new_memarena();
/* Use a stack of menus to handle and discover new menus in passes. */
LinkNode *menu_stack = NULL;
/* Tag menu types not to add, either because they have already been added
* or they have been blacklisted.
* Set of #MenuType. */
GSet *menu_tagged = BLI_gset_ptr_new(__func__);
/** Map (#MenuType -> #wmKeyMapItem). */
GHash *menu_to_kmi = BLI_ghash_ptr_new(__func__);
/* Blacklist menus we don't want to show. */
{
const char *idname_array[] = {
/* While we could include this, it's just showing filenames to load. */
"TOPBAR_MT_file_open_recent",
};
for (int i = 0; i < ARRAY_SIZE(idname_array); i++) {
MenuType *mt = WM_menutype_find(idname_array[i], false);
if (mt != NULL) {
BLI_gset_add(menu_tagged, mt);
}
}
}
/* 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[] = {
"TOPBAR_MT_editor_menus",
/* Optional second menu for the space-type. */
NULL,
};
int idname_array_len = 1;
#define SPACE_MENU_MAP(space_type, menu_id) \
case space_type: \
idname_array[idname_array_len++] = menu_id; \
break
#define SPACE_MENU_NOP(space_type) \
case space_type: \
break
if (sa != NULL) {
switch (sa->spacetype) {
SPACE_MENU_MAP(SPACE_VIEW3D, "VIEW3D_MT_editor_menus");
SPACE_MENU_MAP(SPACE_GRAPH, "GRAPH_MT_editor_menus");
SPACE_MENU_MAP(SPACE_OUTLINER, "OUTLINER_MT_editor_menus");
SPACE_MENU_NOP(SPACE_PROPERTIES);
SPACE_MENU_MAP(SPACE_FILE, "FILE_MT_editor_menus");
SPACE_MENU_MAP(SPACE_IMAGE, "IMAGE_MT_editor_menus");
SPACE_MENU_MAP(SPACE_INFO, "INFO_MT_editor_menus");
SPACE_MENU_MAP(SPACE_SEQ, "SEQUENCER_MT_editor_menus");
SPACE_MENU_MAP(SPACE_TEXT, "TEXT_MT_editor_menus");
SPACE_MENU_MAP(SPACE_ACTION, "ACTION_MT_editor_menus");
SPACE_MENU_MAP(SPACE_NLA, "NLA_MT_editor_menus");
SPACE_MENU_MAP(SPACE_NODE, "NODE_MT_editor_menus");
SPACE_MENU_MAP(SPACE_CONSOLE, "CONSOLE_MT_editor_menus");
SPACE_MENU_MAP(SPACE_USERPREF, "USERPREF_MT_editor_menus");
SPACE_MENU_MAP(SPACE_CLIP,
(((const SpaceClip *)sa->spacedata.first)->mode == SC_MODE_TRACKING) ?
"CLIP_MT_tracking_editor_menus" :
"CLIP_MT_masking_editor_menus");
SPACE_MENU_NOP(SPACE_TOPBAR);
SPACE_MENU_NOP(SPACE_STATUSBAR);
default:
printf("Unknown space type '%d'\n", sa->spacetype);
}
}
for (int i = 0; i < idname_array_len; i++) {
MenuType *mt = WM_menutype_find(idname_array[i], false);
if (mt != NULL) {
BLI_linklist_prepend(&menu_stack, mt);
BLI_gset_add(menu_tagged, mt);
}
}
}
#undef SPACE_MENU_MAP
#undef SPACE_MENU_NOP
bool has_keymap_menu_items = false;
GHashIterator iter;
while (menu_stack != NULL) {
MenuType *mt = BLI_linklist_pop(&menu_stack);
if (!WM_menutype_poll(C, mt)) {
continue;
}
uiBlock *block = UI_block_begin(C, region, __func__, UI_EMBOSS);
uiLayout *layout = UI_block_layout(
block, UI_LAYOUT_VERTICAL, UI_LAYOUT_MENU, 0, 0, 200, 0, UI_MENU_PADDING, style);
UI_block_flag_enable(block, UI_BLOCK_SHOW_SHORTCUT_ALWAYS);
uiLayoutSetOperatorContext(layout, WM_OP_EXEC_REGION_WIN);
UI_menutype_draw(C, mt, layout);
UI_block_end(C, block);
LISTBASE_FOREACH (uiBut *, but, &block->buttons) {
MenuType *mt_from_but = NULL;
/* Support menu titles with dynamic from initial labels
* (used by edit-mesh context menu). */
if (but->type == UI_BTYPE_LABEL) {
/* Check if the label is the title. */
uiBut *but_test = but->prev;
while (but_test && but_test->type == UI_BTYPE_SEPR) {
but_test = but_test->prev;
}
if (but_test == NULL) {
BLI_ghash_insert(
menu_display_name_map, mt, (void *)strdup_memarena(memarena, but->drawstr));
}
}
else if (menu_items_from_ui_create_item_from_button(data, memarena, mt, NULL, but)) {
/* pass */
}
else if ((mt_from_but = UI_but_menutype_get(but))) {
if (BLI_gset_add(menu_tagged, mt_from_but)) {
BLI_linklist_prepend(&menu_stack, mt_from_but);
}
if (!BLI_ghash_haskey(menu_parent_map, mt_from_but)) {
struct MenuSearch_Parent *menu_parent = BLI_memarena_calloc(memarena,
sizeof(*menu_parent));
/* Use brackets for menu key shortcuts,
* converting "Text|Some-Shortcut" to "Text (Some-Shortcut)".
* This is needed so we don't right align sub-menu contents
* we only want to do that for the last menu item, not the path that leads to it.
*/
const char *drawstr_sep = but->flag & UI_BUT_HAS_SEP_CHAR ?
strrchr(but->drawstr, UI_SEP_CHAR) :
NULL;
bool drawstr_is_empty = false;
if (drawstr_sep != NULL) {
BLI_assert(BLI_dynstr_get_len(dyn_str) == 0);
/* Detect empty string, fallback to menu name. */
const char *drawstr = but->drawstr;
int drawstr_len = drawstr_sep - but->drawstr;
if (UNLIKELY(drawstr_len == 0)) {
drawstr = CTX_IFACE_(mt_from_but->translation_context, mt_from_but->label);
drawstr_len = strlen(drawstr);
if (drawstr[0] == '\0') {
drawstr_is_empty = true;
}
}
BLI_dynstr_nappend(dyn_str, drawstr, drawstr_len);
BLI_dynstr_appendf(dyn_str, " (%s)", drawstr_sep + 1);
menu_parent->drawstr = strdup_memarena_from_dynstr(memarena, dyn_str);
BLI_dynstr_clear(dyn_str);
}
else {
const char *drawstr = but->drawstr;
if (UNLIKELY(drawstr[0] == '\0')) {
drawstr = CTX_IFACE_(mt_from_but->translation_context, mt_from_but->label);
if (drawstr[0] == '\0') {
drawstr_is_empty = true;
}
}
menu_parent->drawstr = strdup_memarena(memarena, drawstr);
}
menu_parent->parent_mt = mt;
BLI_ghash_insert(menu_parent_map, mt_from_but, menu_parent);
if (drawstr_is_empty) {
printf("Warning: '%s' menu has empty 'bl_label'.\n", mt_from_but->idname);
}
}
}
else if (but->menu_create_func != NULL) {
/* A non 'MenuType' menu button. */
/* Only expand one level deep, this is mainly for expanding operator menus. */
const char *drawstr_submenu = but->drawstr;
/* +1 to avoid overlap with the current 'block'. */
uiBlock *sub_block = UI_block_begin(C, region, __func__ + 1, UI_EMBOSS);
uiLayout *sub_layout = UI_block_layout(
sub_block, UI_LAYOUT_VERTICAL, UI_LAYOUT_MENU, 0, 0, 200, 0, UI_MENU_PADDING, style);
UI_block_flag_enable(sub_block, UI_BLOCK_SHOW_SHORTCUT_ALWAYS);
uiLayoutSetOperatorContext(sub_layout, WM_OP_INVOKE_REGION_WIN);
but->menu_create_func(C, sub_layout, but->poin);
UI_block_end(C, sub_block);
LISTBASE_FOREACH (uiBut *, sub_but, &sub_block->buttons) {
menu_items_from_ui_create_item_from_button(data, memarena, mt, drawstr_submenu, sub_but);
}
BLI_remlink(&region->uiblocks, sub_block);
UI_block_free(NULL, sub_block);
}
}
BLI_remlink(&region->uiblocks, block);
UI_block_free(NULL, 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 == NULL) && (has_keymap_menu_items == false)) {
has_keymap_menu_items = true;
menu_types_add_from_keymap_items(C, win, sa, region, &menu_stack, menu_to_kmi, menu_tagged);
}
}
LISTBASE_FOREACH (struct MenuSearch_Item *, item, &data->items) {
item->menu_parent = BLI_ghash_lookup(menu_parent_map, item->mt);
}
GHASH_ITER (iter, menu_parent_map) {
struct MenuSearch_Parent *menu_parent = BLI_ghashIterator_getValue(&iter);
menu_parent->parent = BLI_ghash_lookup(menu_parent_map, menu_parent->parent_mt);
}
/* NOTE: currently this builds the full path for each menu item,
* that could be moved into the parent menu. */
/* Unicode arrow. */
#define MENU_SEP "\xe2\x86\x92"
/* Set names as full paths. */
LISTBASE_FOREACH (struct MenuSearch_Item *, item, &data->items) {
if (item->menu_parent != NULL) {
struct MenuSearch_Parent *menu_parent = item->menu_parent;
menu_parent->temp_child = NULL;
while (menu_parent && menu_parent->parent) {
menu_parent->parent->temp_child = menu_parent;
menu_parent = menu_parent->parent;
}
BLI_assert(BLI_dynstr_get_len(dyn_str) == 0);
while (menu_parent) {
BLI_dynstr_append(dyn_str, menu_parent->drawstr);
BLI_dynstr_append(dyn_str, " " MENU_SEP " ");
menu_parent = menu_parent->temp_child;
}
}
else {
BLI_assert(BLI_dynstr_get_len(dyn_str) == 0);
const char *drawstr = BLI_ghash_lookup(menu_display_name_map, item->mt);
if (drawstr == NULL) {
drawstr = CTX_IFACE_(item->mt->translation_context, item->mt->label);
}
BLI_dynstr_append(dyn_str, drawstr);
wmKeyMapItem *kmi = BLI_ghash_lookup(menu_to_kmi, item->mt);
if (kmi != NULL) {
char kmi_str[128];
WM_keymap_item_to_string(kmi, false, kmi_str, sizeof(kmi_str));
BLI_dynstr_appendf(dyn_str, " (%s)", kmi_str);
}
BLI_dynstr_append(dyn_str, " " MENU_SEP " ");
}
/* Optional nested menu. */
if (item->drawstr_submenu != NULL) {
BLI_dynstr_append(dyn_str, item->drawstr_submenu);
BLI_dynstr_append(dyn_str, " " MENU_SEP " ");
}
BLI_dynstr_append(dyn_str, item->drawstr);
item->drawwstr_full = strdup_memarena_from_dynstr(memarena, dyn_str);
BLI_dynstr_clear(dyn_str);
}
BLI_dynstr_free(dyn_str);
#undef MENU_SEP
/* Finally sort menu items.
*
* Note: we might want to keep the in-menu order, for now sort all. */
BLI_listbase_sort(&data->items, menu_item_sort_by_drawstr_full);
BLI_ghash_free(menu_parent_map, NULL, NULL);
BLI_ghash_free(menu_display_name_map, NULL, NULL);
BLI_ghash_free(menu_to_kmi, NULL, NULL);
BLI_gset_free(menu_tagged, NULL);
data->memarena = memarena;
return data;
}
static void menu_items_from_ui_destroy(void *data_v)
{
struct MenuSearch_Data *data = data_v;
LISTBASE_FOREACH (struct MenuSearch_Item *, item, &data->items) {
switch (item->type) {
case MENU_SEARCH_TYPE_OP: {
if (item->op.opptr != NULL) {
WM_operator_properties_free(item->op.opptr);
MEM_freeN(item->op.opptr);
}
}
case MENU_SEARCH_TYPE_RNA: {
break;
}
}
}
BLI_memarena_free(data->memarena);
MEM_freeN(data);
}
static void menu_call_fn(bContext *C, void *UNUSED(arg1), void *arg2)
{
struct MenuSearch_Item *item = arg2;
if (item == NULL) {
return;
}
if (item->state & UI_BUT_DISABLED) {
return;
}
switch (item->type) {
case MENU_SEARCH_TYPE_OP: {
CTX_store_set(C, item->op.context);
WM_operator_name_call_ptr(C, item->op.type, item->op.opcontext, item->op.opptr);
CTX_store_set(C, NULL);
break;
}
case MENU_SEARCH_TYPE_RNA: {
PointerRNA *ptr = &item->rna.ptr;
PropertyRNA *prop = item->rna.prop;
int index = item->rna.index;
const int prop_type = RNA_property_type(prop);
bool changed = false;
if (prop_type == PROP_BOOLEAN) {
const bool is_array = RNA_property_array_check(prop);
if (is_array) {
const bool value = RNA_property_boolean_get_index(ptr, prop, index);
RNA_property_boolean_set_index(ptr, prop, index, !value);
}
else {
const bool value = RNA_property_boolean_get(ptr, prop);
RNA_property_boolean_set(ptr, prop, !value);
}
changed = true;
}
else if (prop_type == PROP_ENUM) {
RNA_property_enum_set(ptr, prop, item->rna.enum_value);
changed = true;
}
if (changed) {
RNA_property_update(C, ptr, prop);
}
break;
}
}
}
static void menu_search_cb(const bContext *UNUSED(C),
void *arg,
const char *str,
uiSearchItems *items)
{
struct MenuSearch_Data *data = arg;
const size_t str_len = strlen(str);
const int words_max = (str_len / 2) + 1;
int(*words)[2] = BLI_array_alloca(words, words_max);
const int words_len = BLI_string_find_split_words(str, str_len, ' ', words, words_max);
for (struct MenuSearch_Item *item = data->items.first; item; item = item->next) {
int index;
/* match name against all search words */
for (index = 0; index < words_len; index++) {
if (!has_word_prefix(item->drawwstr_full, str + words[index][0], words[index][1])) {
break;
}
}
if (index == words_len) {
if (!UI_search_item_add(items, item->drawwstr_full, item, item->icon, item->state)) {
break;
}
}
}
}
void UI_but_func_menu_search(uiBut *but)
{
bContext *C = but->block->evil_C;
wmWindow *win = CTX_wm_window(C);
ScrArea *sa = CTX_wm_area(C);
ARegion *region = CTX_wm_region(C);
struct MenuSearch_Data *data = menu_items_from_ui_create(C, win, sa, region);
UI_but_func_search_set(but,
ui_searchbox_create_menu,
menu_search_cb,
data,
menu_items_from_ui_destroy,
menu_call_fn,
NULL);
}
void uiTemplateMenuSearch(uiLayout *layout)
{
uiBlock *block;
uiBut *but;
static char search[256] = "";
block = uiLayoutGetBlock(layout);
UI_block_layout_set_current(block, layout);
but = uiDefSearchBut(
block, search, 0, ICON_VIEWZOOM, sizeof(search), 0, 0, UI_UNIT_X * 6, UI_UNIT_Y, 0, 0, "");
UI_but_func_menu_search(but);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Operator Redo Properties Template
* \{ */

View File

@ -445,7 +445,7 @@ void ui_rna_collection_search_cb(const struct bContext *C,
/* add search items from temporary list */
for (cis = items_list->first; cis; cis = cis->next) {
if (UI_search_item_add(items, cis->name, cis->data, cis->iconid) == false) {
if (!UI_search_item_add(items, cis->name, cis->data, cis->iconid, 0)) {
break;
}
}

View File

@ -1131,7 +1131,7 @@ static void node_find_cb(const struct bContext *C,
else {
BLI_strncpy(name, node->name, 256);
}
if (false == UI_search_item_add(items, name, node, 0)) {
if (!UI_search_item_add(items, name, node, ICON_NONE, 0)) {
break;
}
}
@ -1178,7 +1178,7 @@ static uiBlock *node_find_menu(bContext *C, ARegion *region, void *arg_op)
0,
0,
"");
UI_but_func_search_set(but, NULL, node_find_cb, op->type, false, node_find_call_cb, NULL);
UI_but_func_search_set(but, NULL, node_find_cb, op->type, NULL, node_find_call_cb, NULL);
UI_but_flag_enable(but, UI_BUT_ACTIVATE_ON_INIT);
/* fake button, it holds space for search items */

View File

@ -528,7 +528,7 @@ static void merged_element_search_cb_recursive(
/* Don't allow duplicate named items */
if (UI_search_items_find_index(items, name) == -1) {
if (!UI_search_item_add(items, name, te, iconid)) {
if (!UI_search_item_add(items, name, te, iconid, 0)) {
break;
}
}
@ -589,7 +589,7 @@ static uiBlock *merged_element_search_menu(bContext *C, ARegion *region, void *d
but = uiDefSearchBut(
block, search, 0, ICON_VIEWZOOM, sizeof(search), 10, 10, menu_width, UI_UNIT_Y, 0, 0, "");
UI_but_func_search_set(
but, NULL, merged_element_search_cb, data, false, merged_element_search_call_cb, NULL);
but, NULL, merged_element_search_cb, data, NULL, merged_element_search_call_cb, NULL);
UI_but_flag_enable(but, UI_BUT_ACTIVATE_ON_INIT);
/* Fake button to hold space for search items */

View File

@ -619,7 +619,9 @@ typedef struct UserDef_FileSpaceData {
typedef struct UserDef_Experimental {
char use_undo_speedup;
char _pad0[7]; /* makesdna does not allow empty structs. */
char use_menu_search;
/** `makesdna` does not allow empty structs. */
char _pad0[6];
} UserDef_Experimental;
#define USER_EXPERIMENTAL_TEST(userdef, member) \

View File

@ -1474,6 +1474,7 @@ void RNA_api_ui_layout(StructRNA *srna)
RNA_def_function_flag(func, FUNC_USE_CONTEXT);
RNA_def_function(srna, "template_operator_search", "uiTemplateOperatorSearch");
RNA_def_function(srna, "template_menu_search", "uiTemplateMenuSearch");
func = RNA_def_function(srna, "template_header_3D_mode", "uiTemplateHeader3D_mode");
RNA_def_function_flag(func, FUNC_USE_CONTEXT);

View File

@ -6051,6 +6051,10 @@ static void rna_def_userdef_experimental(BlenderRNA *brna)
prop,
"Undo Speedup",
"Use new undo speedup (WARNING: can lead to crashes and serious .blend file corruption)");
prop = RNA_def_property(srna, "use_menu_search", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_boolean_sdna(prop, NULL, "use_menu_search", 1);
RNA_def_property_ui_text(prop, "Menu Search", "Search actions by menus instead of operators");
}
static void rna_def_userdef_addon_collection(BlenderRNA *brna, PropertyRNA *cprop)

View File

@ -1687,10 +1687,15 @@ static void WM_OT_operator_defaults(wmOperatorType *ot)
/** \} */
/* -------------------------------------------------------------------- */
/** \name Operator Search Menu
/** \name Operator/Menu Search Operator
* \{ */
struct SearchPopupInit_Data {
enum {
SEARCH_TYPE_OPERATOR = 0,
SEARCH_TYPE_MENU = 1,
} search_type;
int size[2];
};
@ -1717,7 +1722,17 @@ static uiBlock *wm_block_search_menu(bContext *C, ARegion *region, void *userdat
0,
0,
"");
UI_but_func_operator_search(but);
if (init_data->search_type == SEARCH_TYPE_OPERATOR) {
UI_but_func_operator_search(but);
}
else if (init_data->search_type == SEARCH_TYPE_MENU) {
UI_but_func_menu_search(but);
}
else {
BLI_assert(0);
}
UI_but_flag_enable(but, UI_BUT_ACTIVATE_ON_INIT);
/* fake button, it holds space for search items */
@ -1747,7 +1762,7 @@ static int wm_search_menu_exec(bContext *UNUSED(C), wmOperator *UNUSED(op))
return OPERATOR_FINISHED;
}
static int wm_search_menu_invoke(bContext *C, wmOperator *UNUSED(op), const wmEvent *event)
static int wm_search_menu_invoke(bContext *C, wmOperator *op, const wmEvent *event)
{
/* Exception for launching via spacebar */
if (event->type == EVT_SPACEKEY) {
@ -1775,9 +1790,20 @@ static int wm_search_menu_invoke(bContext *C, wmOperator *UNUSED(op), const wmEv
}
}
PropertyRNA *prop = op->type->prop;
int search_type;
if (RNA_property_is_set(op->ptr, prop)) {
search_type = RNA_property_enum_get(op->ptr, prop);
}
else {
search_type = U.experimental.use_menu_search ? SEARCH_TYPE_MENU : SEARCH_TYPE_OPERATOR;
}
static struct SearchPopupInit_Data data;
data.size[0] = UI_searchbox_size_x() * 2;
data.size[1] = UI_searchbox_size_y();
data = (struct SearchPopupInit_Data){
.search_type = search_type,
.size = {UI_searchbox_size_x() * 2, UI_searchbox_size_y()},
};
UI_popup_block_invoke(C, wm_block_search_menu, &data, NULL);
@ -1793,6 +1819,15 @@ static void WM_OT_search_menu(wmOperatorType *ot)
ot->invoke = wm_search_menu_invoke;
ot->exec = wm_search_menu_exec;
ot->poll = WM_operator_winactive;
static const EnumPropertyItem search_type_items[] = {
{SEARCH_TYPE_OPERATOR, "OPERATOR", 0, "Operator", "Search all operators"},
{SEARCH_TYPE_MENU, "MENU", 0, "Menu", "Search active menu items"},
{0, NULL, 0, NULL, NULL},
};
/* properties */
ot->prop = RNA_def_enum(ot->srna, "type", search_type_items, SEARCH_TYPE_OPERATOR, "Type", "");
}
static int wm_call_menu_exec(bContext *C, wmOperator *op)