From e58a12ba049cf575b3c87cce7105a4b9e20b4c7b Mon Sep 17 00:00:00 2001 From: Frans de Jonge Date: Tue, 6 Dec 2022 22:02:21 +0100 Subject: [PATCH] TouchMenu: Search menu to search the menu (#9876) Fixes #9800. --- frontend/apps/filemanager/filemanager.lua | 7 + frontend/apps/reader/readerui.lua | 7 + frontend/dispatcher.lua | 2 + .../elements/common_settings_menu_table.lua | 8 + .../ui/elements/filemanager_menu_order.lua | 2 + frontend/ui/elements/reader_menu_order.lua | 2 + frontend/ui/widget/touchmenu.lua | 382 +++++++++++++++++- 7 files changed, 409 insertions(+), 1 deletion(-) diff --git a/frontend/apps/filemanager/filemanager.lua b/frontend/apps/filemanager/filemanager.lua index d977543bd..f134b5bc6 100644 --- a/frontend/apps/filemanager/filemanager.lua +++ b/frontend/apps/filemanager/filemanager.lua @@ -1385,4 +1385,11 @@ function FileManager:onRefreshContent() self:onRefresh() end +function FileManager:onMenuSearch() + if not self.ui then + self.menu:onShowMenu() + end + self.menu.menu_container[1]:onShowMenuSearch() +end + return FileManager diff --git a/frontend/apps/reader/readerui.lua b/frontend/apps/reader/readerui.lua index 9f0f6d819..358cee775 100644 --- a/frontend/apps/reader/readerui.lua +++ b/frontend/apps/reader/readerui.lua @@ -871,4 +871,11 @@ function ReaderUI:getCurrentPage() end end +function ReaderUI:onMenuSearch() + if not self.ui then + self.menu:onShowMenu() + end + self.menu.menu_container[1]:onShowMenuSearch() +end + return ReaderUI diff --git a/frontend/dispatcher.lua b/frontend/dispatcher.lua index 704fd154c..62002c5fa 100644 --- a/frontend/dispatcher.lua +++ b/frontend/dispatcher.lua @@ -97,6 +97,7 @@ local settingsList = { fulltext_search = {category="none", event="ShowFulltextSearchInput", title=_("Fulltext search"), general=true}, file_search = {category="none", event="ShowFileSearch", title=_("File search"), general=true, separator=true}, show_menu = {category="none", event="ShowMenu", title=_("Show menu"), general=true}, + menu_search = {category="none", event="MenuSearch", title=_("Menu search"), general=true}, favorites = {category="none", event="ShowColl", arg="favorites", title=_("Favorites"), general=true}, screenshot = {category="none", event="Screenshot", title=_("Screenshot"), general=true, separator=true}, @@ -227,6 +228,7 @@ local dispatcher_menu_order = { "file_search", "show_menu", + "menu_search", "screenshot", "exit_screensaver", diff --git a/frontend/ui/elements/common_settings_menu_table.lua b/frontend/ui/elements/common_settings_menu_table.lua index b89234db2..28c0d35c7 100644 --- a/frontend/ui/elements/common_settings_menu_table.lua +++ b/frontend/ui/elements/common_settings_menu_table.lua @@ -609,4 +609,12 @@ common_settings.units = { }, } +common_settings.search_menu = { + text = _("Menu search"), + callback = function() + UIManager:sendEvent(Event:new("ShowMenuSearch")) + end, + keep_menu_open = true, +} + return common_settings diff --git a/frontend/ui/elements/filemanager_menu_order.lua b/frontend/ui/elements/filemanager_menu_order.lua index 09098bb3e..8ac70362d 100644 --- a/frontend/ui/elements/filemanager_menu_order.lua +++ b/frontend/ui/elements/filemanager_menu_order.lua @@ -169,6 +169,8 @@ local order = { help = { "quickstart_guide", "----------------------------", + "search_menu", + "----------------------------", "report_bug", "----------------------------", "system_statistics", -- if enabled (Plugin) diff --git a/frontend/ui/elements/reader_menu_order.lua b/frontend/ui/elements/reader_menu_order.lua index ca5f8d5ba..9b8c4d7a3 100644 --- a/frontend/ui/elements/reader_menu_order.lua +++ b/frontend/ui/elements/reader_menu_order.lua @@ -222,6 +222,8 @@ local order = { help = { "quickstart_guide", "----------------------------", + "search_menu", + "----------------------------", "report_bug", "----------------------------", "system_statistics", -- if enabled (Plugin) diff --git a/frontend/ui/widget/touchmenu.lua b/frontend/ui/widget/touchmenu.lua index bf3aa5a20..cbfd0cc16 100644 --- a/frontend/ui/widget/touchmenu.lua +++ b/frontend/ui/widget/touchmenu.lua @@ -26,12 +26,14 @@ local Size = require("ui/size") local TextWidget = require("ui/widget/textwidget") local UIManager = require("ui/uimanager") local UnderlineContainer = require("ui/widget/container/underlinecontainer") +local Utf8Proc = require("ffi/utf8proc") local VerticalGroup = require("ui/widget/verticalgroup") local VerticalSpan = require("ui/widget/verticalspan") local datetime = require("datetime") local getMenuText = require("ui/widget/menu").getMenuText local _ = require("gettext") -local T = require("ffi/util").template +local ffiUtil = require("ffi/util") +local T = ffiUtil.template local Input = Device.input local Screen = Device.screen @@ -477,6 +479,7 @@ function TouchMenu:init() end self.layout = {} + self.search_layout = {} self.ges_events.TapCloseAllMenus = { GestureRange:new{ @@ -672,8 +675,10 @@ function TouchMenu:updateItems() self:_recalculatePageLayout() self.item_group:clear() self.layout = {} + self.search_layout = {} table.insert(self.item_group, self.bar) table.insert(self.layout, self.bar.icon_widgets) -- for the focusmanager + table.insert(self.search_layout, self.bar.icon_widgets) -- for the menu search for c = 1, self.perpage do -- calculate index in item_table @@ -681,6 +686,7 @@ function TouchMenu:updateItems() if i <= #self.item_table then local item = self.item_table[i] local item_tmp = TouchMenuItem:new{ + name = "touch_menu_item " .. tostring(item.text) .. " xxx", -- xxx testing only, squish later item = item, menu = self, dimen = Geom:new{ @@ -693,6 +699,7 @@ function TouchMenu:updateItems() if item_tmp:isEnabled() then table.insert(self.layout, {[self.cur_tab] = item_tmp}) -- for the focusmanager end + table.insert(self.search_layout, {[self.cur_tab] = item_tmp}) -- for the menu search if item.separator and c ~= self.perpage and i ~= #self.item_table then -- insert split line table.insert(self.item_group, self.split_line) @@ -835,6 +842,18 @@ function TouchMenu:onLastPage() return true end +function TouchMenu:onGotoPage(nb) + if nb > self.page_num then + self.page = self.page_num + elseif nb < 1 then + self.page = 1 + else + self.page = nb + end + self:updateItems() + return true +end + function TouchMenu:onSwipe(arg, ges_ev) local direction = BD.flipDirectionIfMirroredUILayout(ges_ev.direction) if direction == "west" then @@ -973,4 +992,365 @@ function TouchMenu:onBack() self:backToUpperMenu() end +------ the menu search functionality +function TouchMenu:search(search_for) + local found_menu_items = {} + + local MAX_MENU_DEPTH = 20 -- currently our menu needs at least 12 here + local function recurse(val, path, text, icon, depth) + depth = depth + 1 + if depth > MAX_MENU_DEPTH then + return + end + + for i,v in ipairs(val) do + if type(v) == "table" then + local entry_text = v.text_func and v.text_func() or v.text + local indent = text and ((" "):rep(math.min(depth-1, 6)) .. "→ ") or "→ " -- all spaces here are Hair Space U+200A + local next_text = text and (text .. "\n" .. indent .. entry_text) or (indent .. entry_text) + local next_path = path .. "." .. i + recurse(val[i], next_path, next_text, icon, depth) + if Utf8Proc.lowercase(entry_text):find(search_for) then + table.insert(found_menu_items, {entry_text, icon, next_path, next_text}) + end + end + end + + if val.sub_item_table_func then + local sub_item_table = val.sub_item_table_func() + local perpage = "" + if sub_item_table.max_per_page then + perpage = "[" .. sub_item_table.max_per_page .."]" + end + recurse(sub_item_table, path .. ".sub_item_table_func" .. perpage, text, icon, depth) + elseif val.sub_item_table then + local perpage = "" + if val.sub_item_table.max_per_page then + perpage = "[" .. val.sub_item_table.max_per_page .."]" + end + recurse(val.sub_item_table, path .. ".sub_item_table" .. perpage, text, icon, depth) + end + end -- recurse + + -- initial call of recurse + for i = 1, #self.tab_item_table do + recurse(self.tab_item_table[i], i, self.tab_item_table[i].text, self.tab_item_table[i].icon, 0) + end + +--[[ + for i = 1, #found_menu_items do + print("xxxxxxx->", i, + found_menu_items[i][1], + found_menu_items[i][2], + found_menu_items[i][3]) + end +]] + return found_menu_items +end + +-- maybe this could be placed in UIManager? +local function highlightWidget(widget, unhilight_in_s) + if not widget then return end + local highlight_dimen = widget.dimen + if highlight_dimen.w == 0 then + highlight_dimen.w = widget.width + end + + UIManager:nextTick(function() + -- Highlight + widget.invert = true + UIManager:widgetInvert(widget, highlight_dimen.x, highlight_dimen.y, highlight_dimen.w) + UIManager:setDirty(nil, "fast", highlight_dimen) + + UIManager:forceRePaint() + UIManager:yieldToEPDC() + end) + + if unhilight_in_s then + -- Unhighlight + UIManager:scheduleIn(unhilight_in_s, function() + widget.invert = false + UIManager:widgetInvert(widget, highlight_dimen.x, highlight_dimen.y, highlight_dimen.w) + UIManager:setDirty(nil, "ui", highlight_dimen) + end) + end +end + +function TouchMenu:_get_widget(tab_nb, nb) + return self.search_layout[nb + 1][tab_nb] +end + +function TouchMenu:openMenu(path) + local TrapWidget = require("ui/widget/trapwidget") + + local animation_time_s = G_reader_settings:readSetting("menu_search_animation_time_s", 1.0) + + -- first switch to correct MenuTab + local sep_pos = path:find("%.") + local tab_nb = tonumber(path:sub(1, sep_pos - 1)) + + if not tab_nb then return end + + path = path:sub(sep_pos + 1) + local item = self.tab_item_table[tab_nb] + + self:switchMenuTab(tab_nb) + self.bar:switchToTab(tab_nb) + self:onMenuSelect(self.item_table) + + -- Now go the menu path down the way + local items_to_show = {} + local dummy, num_of_sep = path:gsub("%.", "%.") + while num_of_sep > 1 do + sep_pos = path:find("%.") + local identifier = path:sub(1, sep_pos -1) + local item_nb = tonumber(identifier) + path = path:sub(sep_pos + 1) + if item_nb then + self:updateItems() + item = item[item_nb] + table.insert(items_to_show, {item_nb, item}) + elseif identifier:find("sub_item_table_func") then + item = item.sub_item_table_func() + elseif identifier:find("sub_item_table") then + item = item.sub_item_table + end + num_of_sep = num_of_sep - 1 + end + + -- Now we are in the right menu, but maybe in the wrong page + sep_pos = path:find("%.") + if not sep_pos then + local logger = require("logger") + logger.err("TouchMenu: search; internal error") -- should not happen + return + end + local identifier = path:sub(sep_pos + 1) + local item_nb = tonumber(identifier) + + local perpage + if path:find("sub_item_table_func%[") then + perpage = path:sub(#("sub_item_table_func%["), sep_pos - 2) + elseif path:find("sub_item_table%[") then + perpage = path:sub(#("sub_item_table%["), sep_pos - 2) + end + perpage = tonumber(perpage) or self.perpage + + local function open_final_menu() + print("xxx", #items_to_show) + if #items_to_show > 0 then -- can be zero, if in the last menu + self:onMenuSelect(items_to_show[#items_to_show][2]) + end + local page_nb = math.floor((item_nb - 1) / perpage) + 1 + self:onGotoPage(page_nb) + end + + if not (animation_time_s and animation_time_s > 0.0) then + open_final_menu() + else + self.trap_widget = TrapWidget:new{ + dismiss_callback = function() + animation_time_s = 0 + self.trap_widget = nil + end + } + UIManager:show(self.trap_widget) -- suppress taps during animaton + + -- Animation functions + local function open_next_page(pages_to_show, force_first_page) + if animation_time_s == 0.0 then + UIManager.close(self.trap_widget) + self.trap_widget = nil + open_final_menu() + -- end of the animation here! + return + end + print("xxx pages_to_show", pages_to_show, force_first_page) + if force_first_page then + self:onFirstPage() + end + + if pages_to_show == 1 then -- if we are on the last page + if self.page_num > 1 then + highlightWidget(self.page_info_right_chev, animation_time_s) + end + highlightWidget(self:_get_widget(tab_nb, (item_nb - 1) % perpage + 1)) + -- end of the animation here! + else + if self.page_num ~= 1 then + highlightWidget(self.page_info_right_chev) + end + UIManager:scheduleIn(animation_time_s, function() + self:onNextPage() + if pages_to_show > 1 then + UIManager:nextTick(open_next_page, pages_to_show - 1, false) + end + end) + end + end + + local function open_next_menu() + if animation_time_s == 0.0 then + UIManager:close(self.trap_widget) + self.trap_widget = nil + open_final_menu() + -- end of the animation here! + return + end + + local x = table.remove(items_to_show, 1) + local next_menu_num, next_menu_item = x and x[1], x and x[2] -- might be nil + + if next_menu_item then + highlightWidget(self:_get_widget(tab_nb, next_menu_num)) + UIManager:scheduleIn(animation_time_s, function() + self:onMenuSelect(next_menu_item) + UIManager:nextTick(open_next_menu) + end) + else + -- no items left, but maybe on another page + local page_nb = math.floor(item_nb / perpage) + 1 + UIManager:nextTick(open_next_page, page_nb, page_nb > 1) + end + end + + -- Animate + UIManager:nextTick(open_next_menu) + end +end + +function TouchMenu:onShowMenuSearch() + local InputDialog = require("ui/widget/inputdialog") + local CheckButton = require("ui/widget/checkbutton") + local ConfirmBox = require("ui/widget/confirmbox") + local Menu = require("ui/widget/menu") + + local function show_search_results(search_string) + local found_menu_items = self:search(search_string) + + local function get_current_search_results() + local function item_callback(i) + UIManager:close(self.results_menu_container) + UIManager:setDirty(nil, "ui") + self:openMenu(found_menu_items[i][3]) + end + local function item_hold_callback(i) + local confirm_box + confirm_box = ConfirmBox:new{ + text = T(_("Open menu entry:\n'%1'\n\n%2"), found_menu_items[i][1], found_menu_items[i][4]), + icon = found_menu_items[i][2], + ok_text = _("Open"), + ok_callback = function() + UIManager:close(confirm_box) + item_callback(i) + end, + cancel_text = _("Dismiss"), + width_percent = 0.95, + } + UIManager:show(confirm_box) + end + + local result_items = {} + for i = 1, #found_menu_items do + table.insert(result_items, + { + text = found_menu_items[i][1], + callback = function() item_callback(i) end, + hold_callback = function() item_hold_callback(i) end, + } + ) + end + return result_items + end -- get_current_search_results() + + if #found_menu_items > 0 then + local results_menu = Menu:new{ + title = _("Search results"), + item_table = get_current_search_results(found_menu_items), + item_shortcuts = {}, + width = math.floor(Screen:getWidth() * 0.9), + height = math.floor(Screen:getHeight() * 0.9), + single_line = true, + items_per_page = 10, + items_font_size = Menu.getItemFontSize(10), + onMenuSelect = function(item, pos) + if pos.callback then pos.callback() end + end, + onMenuHold = function(item, pos) + if pos.hold_callback then pos.hold_callback() end + end, + close_callback = function() + UIManager:close(self.results_menu_container) + end + } + + -- build container + self.results_menu_container = CenterContainer:new{ + dimen = Screen:getSize(), + results_menu, + } + + results_menu.show_parent = self.results_menu_container + + UIManager:show(self.results_menu_container) + + else + UIManager:show(InfoMessage:new{ + text = T(_("No menus containing '%1' found."), search_string), + }) + end + end -- show_search_results() + + local search_dialog + search_dialog = InputDialog:new{ + title = _("Search menu entry"), + description = _("Search for a menu entry containing the following text (case insensitive)."), + input = G_reader_settings:readSetting("menu_search_string", _("Help")), + buttons = { + { + { + text = _("Cancel"), + id = "close", + callback = function() + UIManager:close(search_dialog) + end, + }, + { + text = _("Search"), + callback = function() + local search_for = search_dialog:getInputText() + local status, err = pcall( function() ("test_string"):find(search_for) end) + if status then + search_for = Utf8Proc.lowercase(search_for) + G_reader_settings:saveSetting("menu_search_string", search_for) + UIManager:close(search_dialog) + show_search_results(search_for) + else + err = err:sub(err:find("lua") + 10) -- 10 = strlen("lua:1165: ") + UIManager:show(InfoMessage:new{ + text = T(_("Malformed message:\n%1"), err) + }) + end + end, + }, + } + }, + } + + local animation_time_s = G_reader_settings:readSetting("menu_search_animation_time_s", 1.0) + local check_button_animation = CheckButton:new{ + text = _("Animation"), + checked = animation_time_s ~= 0.0, + parent = search_dialog, + callback = function() + animation_time_s = animation_time_s ~= 0.0 and 0.0 or 1.0 + G_reader_settings:saveSetting("menu_search_animation_time_s", animation_time_s) + end, + } + search_dialog:addWidget(check_button_animation) + + UIManager:show(search_dialog) + search_dialog:onShowKeyboard() +end + return TouchMenu