From cbfb2a62fb65b26171f0e82742cf418ba03a8ef8 Mon Sep 17 00:00:00 2001 From: Tomas Klaen <47283320+tomasklaen@users.noreply.github.com> Date: Sat, 30 Sep 2023 17:24:03 +0200 Subject: [PATCH] feat(api): added `search_submenus` prop to menus (#655) `search_submenus` makes uosc's internal search handler (when no `on_search` callback is defined) look into submenus as well, effectively flattening the menu for the duration of the search. --- README.md | 2 ++ scripts/uosc/elements/Menu.lua | 45 +++++++++++++++++++++++++--------- scripts/uosc/lib/menus.lua | 2 +- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index bd2c75c..a49aebe 100644 --- a/README.md +++ b/README.md @@ -465,6 +465,8 @@ While the menu is open this value will be available in `user-data/uosc/menu/type `search_debounce` controls how soon the search happens after the last character was entered in milliseconds. Entering new character resets the timer. Defaults to `300`. It can also have a special value `'submit'`, which triggers a search only after `ctrl+enter` was pressed. +`search_submenus` makes uosc's internal search handler (when no `on_search` callback is defined) look into submenus as well, effectively flattening the menu for the duration of the search. + `search_suggestion` fills menu search with initial query string. Useful for example when you want to implement something like subtitle downloader, you'd set it to current file name. `item.icon` property accepts icon names. You can pick one from here: [Google Material Icons](https://fonts.google.com/icons)\ diff --git a/scripts/uosc/elements/Menu.lua b/scripts/uosc/elements/Menu.lua index d6bf2cc..f25c46b 100644 --- a/scripts/uosc/elements/Menu.lua +++ b/scripts/uosc/elements/Menu.lua @@ -1,13 +1,13 @@ local Element = require('elements/Element') -- Menu data structure accepted by `Menu:open(menu)`. ----@alias MenuData {id?: string; type?: string; title?: string; hint?: string; palette?: boolean; keep_open?: boolean; separator?: boolean; items?: MenuDataItem[]; selected_index?: integer; on_search?: string|string[]|fun(search_text: string), search_debounce?: number|string; search_suggestion?: string} +---@alias MenuData {id?: string; type?: string; title?: string; hint?: string; palette?: boolean; keep_open?: boolean; separator?: boolean; items?: MenuDataItem[]; selected_index?: integer; on_search?: string|string[]|fun(search_text: string); search_debounce?: number|string; search_submenus?: boolean; search_suggestion?: string} ---@alias MenuDataItem MenuDataValue|MenuData ---@alias MenuDataValue {title?: string; hint?: string; icon?: string; value: any; bold?: boolean; italic?: boolean; muted?: boolean; active?: boolean; keep_open?: boolean; separator?: boolean; selectable?: boolean; align?: 'left'|'center'|'right'} ---@alias MenuOptions {mouse_nav?: boolean; on_open?: fun(); on_close?: fun(); on_back?: fun(); on_move_item?: fun(from_index: integer, to_index: integer, submenu_path: integer[]); on_delete_item?: fun(index: integer, submenu_path: integer[])} -- Internal data structure created from `Menu`. ----@alias MenuStack {id?: string; type?: string; title?: string; hint?: string; palette?: boolean, selected_index?: number; keep_open?: boolean; separator?: boolean; items: MenuStackItem[]; on_search?: string|string[]|fun(search_text: string); search_debounce?: number|string; search_suggestion?: string; parent_menu?: MenuStack; submenu_path: integer[]; active?: boolean; width: number; height: number; top: number; scroll_y: number; scroll_height: number; title_width: number; hint_width: number; max_width: number; is_root?: boolean; fling?: Fling, search?: Search, ass_safe_title?: string} +---@alias MenuStack {id?: string; type?: string; title?: string; hint?: string; palette?: boolean, selected_index?: number; keep_open?: boolean; separator?: boolean; items: MenuStackItem[]; on_search?: string|string[]|fun(search_text: string); search_debounce?: number|string; search_submenus?: boolean; search_suggestion?: string; parent_menu?: MenuStack; submenu_path: integer[]; active?: boolean; width: number; height: number; top: number; scroll_y: number; scroll_height: number; title_width: number; hint_width: number; max_width: number; is_root?: boolean; fling?: Fling, search?: Search, ass_safe_title?: string} ---@alias MenuStackItem MenuStackValue|MenuStack ---@alias MenuStackValue {title?: string; hint?: string; icon?: string; value: any; active?: boolean; bold?: boolean; italic?: boolean; muted?: boolean; keep_open?: boolean; separator?: boolean; selectable?: boolean; align?: 'left'|'center'|'right'; title_width: number; hint_width: number} ---@alias Fling {y: number, distance: number, time: number, easing: fun(x: number), duration: number, update_cursor?: boolean} @@ -144,7 +144,9 @@ function Menu:update(data) local new_by_id = {} local menus_to_serialize = {{new_root, data}} local old_current_id = self.current and self.current.id - local menu_props_to_copy = {'title', 'hint', 'keep_open', 'palette', 'on_search', 'search_suggestion'} + local menu_props_to_copy = { + 'title', 'hint', 'keep_open', 'palette', 'on_search', 'search_submenus', 'search_suggestion' + } local item_props_to_copy = itable_join(menu_props_to_copy, { 'icon', 'active', 'bold', 'italic', 'muted', 'value', 'separator', 'selectable', 'align' }) @@ -670,14 +672,7 @@ function Menu:search_internal(menu) -- Reset menu state to what it was before search for key, value in pairs(menu.search.source) do menu[key] = value end else - menu.items = itable_filter(menu.search.source.items, function(item) - if item.selectable == false then return false end - local title = item.title and item.title:lower() - local hint = item.hint and item.hint:lower() - return title and title:find(query, 1, true) or hint and hint:find(query, 1, true) or - title and table.concat(initials(title)):find(query, 1, true) or - hint and table.concat(initials(hint)):find(query, 1, true) - end) + menu.items = search_items(menu.search.source.items, query, menu.search_submenus) -- Select 1st item in search results menu.scroll_y = 0 self:select_index(1, menu) @@ -685,6 +680,34 @@ function Menu:search_internal(menu) self:update_content_dimensions() end +---@param items MenuStackItem[] +---@param query string +---@param recursive? boolean +---@param prefix? string +---@return MenuStackItem[] +function search_items(items, query, recursive, prefix) + local result = {} + for _, item in ipairs(items) do + if item.selectable ~= false then + local prefixed_title = prefix and prefix .. ' / ' .. (item.title or '') or item.title + if item.items and recursive then + itable_append(result, search_items(item.items, query, recursive, prefixed_title)) + else + local title = item.title and item.title:lower() + local hint = item.hint and item.hint:lower() + if title and title:find(query, 1, true) or hint and hint:find(query, 1, true) or + title and table.concat(initials(title)):find(query, 1, true) or + hint and table.concat(initials(hint)):find(query, 1, true) then + item = table_shallow_copy(item) + item.title = prefixed_title + result[#result + 1] = item + end + end + end + end + return result +end + ---@param menu? MenuStack function Menu:search_submit(menu) menu = menu or self.current diff --git a/scripts/uosc/lib/menus.lua b/scripts/uosc/lib/menus.lua index ad8c720..2d8f875 100644 --- a/scripts/uosc/lib/menus.lua +++ b/scripts/uosc/lib/menus.lua @@ -23,7 +23,7 @@ end ---@param opts? {submenu?: string; mouse_nav?: boolean; on_close?: string | string[]} function toggle_menu_with_items(opts) if Menu:is_open('menu') then Menu:close() - else open_command_menu({type = 'menu', items = config.menu_items}, opts) end + else open_command_menu({type = 'menu', items = config.menu_items, search_submenus = true}, opts) end end ---@param options {type: string; title: string; list_prop: string; active_prop?: string; serializer: fun(list: any, active: any): MenuDataItem[]; on_select: fun(value: any); on_move_item?: fun(from_index: integer, to_index: integer, submenu_path: integer[]); on_delete_item?: fun(index: integer, submenu_path: integer[])}