diff --git a/lib/awful/hotkeys_popup/init.lua b/lib/awful/hotkeys_popup/init.lua index 058bae1282..9b019e4398 100644 --- a/lib/awful/hotkeys_popup/init.lua +++ b/lib/awful/hotkeys_popup/init.lua @@ -22,6 +22,7 @@ local hotkeys_popup = { -- see `awful.hotkeys_popup.widget.show_help` for more information -- @tparam[opt] client c The hostkeys for the client "c". -- @tparam[opt] screen s The screen. +-- @tparam[opt=true] boolean show_args.enable_find Enable find. -- @tparam[opt=true] boolean show_args.show_awesome_keys Show AwesomeWM hotkeys. -- When set to `false` only app-specific hotkeys will be shown. -- @staticfct awful.hotkeys_popup.show_help diff --git a/lib/awful/hotkeys_popup/widget.lua b/lib/awful/hotkeys_popup/widget.lua index 20fb3d77bc..081da28ee8 100644 --- a/lib/awful/hotkeys_popup/widget.lua +++ b/lib/awful/hotkeys_popup/widget.lua @@ -69,6 +69,8 @@ local capi = { screen = screen, client = client, } +local table = table +local string = string local awful = require("awful") local gtable = require("gears.table") local gstring = require("gears.string") @@ -81,9 +83,15 @@ local matcher = require("gears.matcher")() -- Stripped copy of this module https://github.com/copycat-killer/lain/blob/master/util/markup.lua: local markup = {} +function markup.font_start(font) + return '' +end +function markup.font_end() + return '' +end -- Set the font. function markup.font(font, text) - return '' .. tostring(text) ..'' + return markup.font_start(font) .. tostring(text) .. markup.font_end() end -- Set the foreground. function markup.fg(color, text) @@ -266,6 +274,34 @@ widget.labels = { -- @beautiful beautiful.hotkeys_group_margin -- @tparam int hotkeys_group_margin +--- The highlighted text background color. +-- @beautiful beautiful.hotkeys_highlight_bg +-- @tparam color hotkeys_highlight_bg + +--- The highlighted text foreground color. +-- @beautiful beautiful.hotkeys_highlight_fg +-- @tparam color hotkeys_highlight_fg + +--- The find prompt cursor foreground color. +-- @beautiful beautiful.hotkeys_find_fg_cursor +-- @tparam color hotkeys_find_fg_cursor + +--- The find prompt cursor background color. +-- @beautiful beautiful.hotkeys_find_bg_cursor +-- @tparam color hotkeys_find_bg_cursor + +--- The find prompt cursor underline style. +-- @beautiful beautiful.hotkeys_find_ul_cursor +-- @tparam string hotkeys_find_ul_cursor + +--- The find prompt text font. +-- @beautiful beautiful.hotkeys_find_font +-- @tparam string|lgi.Pango.FontDescription hotkeys_find_font + +--- Margin around the find prompt. +-- @beautiful beautiful.hotkeys_find_margin +-- @tparam int hotkeys_find_margin + --- Create an instance of widget with hotkeys help. -- @tparam[opt] table args Configuration options for the widget. @@ -288,6 +324,13 @@ widget.labels = { -- @tparam[opt] color args.label_fg Foreground color used for group and other -- labels. -- @tparam[opt] int args.group_margin Margin between hotkeys groups. +-- @tparam[opt] color args.highlight_bg The highlighted text background color. +-- @tparam[opt] color args.highlight_fg The highlighted text foreground color. +-- @tparam[opt] color args.find_fg_cursor The find prompt cursor foreground color. +-- @tparam[opt] color args.find_bg_cursor The find prompt cursor background color. +-- @tparam[opt] string args.find_ul_cursor The find prompt cursor underline style. +-- @tparam[opt] string|lgi.Pango.FontDescription args.find_font The find prompt text font. +-- @tparam[opt] int args.find_margin Margin around the find prompt. -- @tparam[opt] table args.labels Labels used for displaying human-readable keynames. -- @tparam[opt] table args.group_rules Rules for showing 3rd-party hotkeys. @see `awful.hotkeys_popup.keys.vim`. -- @return Widget instance. @@ -303,6 +346,13 @@ widget.labels = { -- @usebeautiful beautiful.hotkeys_font -- @usebeautiful beautiful.hotkeys_description_font -- @usebeautiful beautiful.hotkeys_group_margin +-- @usebeautiful beautiful.hotkeys_highlight_bg +-- @usebeautiful beautiful.hotkeys_highlight_fg +-- @usebeautiful beautiful.hotkeys_find_fg_cursor +-- @usebeautiful beautiful.hotkeys_find_bg_cursor +-- @usebeautiful beautiful.hotkeys_find_ul_cursor +-- @usebeautiful beautiful.hotkeys_find_font +-- @usebeautiful beautiful.hotkeys_find_margin -- @usebeautiful beautiful.bg_normal Fallback. -- @usebeautiful beautiful.fg_normal Fallback. -- @usebeautiful beautiful.fg_minimize Fallback. @@ -371,6 +421,20 @@ function widget.new(args) beautiful.hotkeys_description_font or "Monospace 8" self.group_margin = args.group_margin or beautiful.hotkeys_group_margin or dpi(6) + self.highlight_bg = args.highlight_bg or + beautiful.hotkeys_highlight_bg or beautiful.bg_urgent + self.highlight_fg = args.highlight_fg or + beautiful.hotkeys_highlight_fg or beautiful.fg_urgent + self.find_fg_cursor = args.find_fg_cursor or + beautiful.hotkeys_find_fg_cursor + self.find_bg_cursor = args.find_bg_cursor or + beautiful.hotkeys_find_bg_cursor + self.find_ul_cursor = args.find_ul_cursor or + beautiful.hotkeys_find_ul_cursor + self.find_font = args.find_font or + beautiful.hotkeys_find_font or self.font + self.find_margin = args.find_margin or + beautiful.hotkeys_find_margin or self.group_margin self.label_colors = beautiful.xresources.get_current_theme() self._widget_settings_loaded = true end @@ -523,14 +587,95 @@ function widget.new(args) return margin end - function widget_instance:_create_group_columns(column_layouts, group, keys, s, wibox_height) + function widget_instance:_render_all_hotkeys(labels, find_keywords) + local rendered_hotkeys = {} + for _, label in ipairs(labels) do + table.insert(rendered_hotkeys, self:_render_hotkey(label, find_keywords)) + end + return table.concat(rendered_hotkeys, "\n") + end + + function widget_instance:_render_hotkey(label, find_keywords) + local rendered_text = label.text or "" + + if #rendered_text > 0 and find_keywords and #find_keywords > 0 then + local text = string.lower(rendered_text) + + local parts = {} + local found_keyword_count = 0 + + local function is_available(from, to) + for _, s in ipairs(parts) do + if from <= s.to and to >= s.from then + return false + end + end + return true + end + + for _, keyword in ipairs(find_keywords) do + local from = 1 + local to + while true do + from, to = string.find(text, keyword, from, true) + if not from then + break + end + if is_available(from, to) then + table.insert(parts, { highlight = true, from = from, to = to }) + found_keyword_count = found_keyword_count + 1 + break + end + from = to + 1 + end + end + + if found_keyword_count == #find_keywords then + table.sort(parts, function(a, b) return a.from < b.from end) + + local merged_parts = {} + local length = #text + local next_part = parts[1] + local i = 1 + while i <= length do + if next_part then + if next_part.from == i then + table.insert(merged_parts, next_part) + i = next_part.to + 1 + table.remove(parts, 1) + next_part = parts[1] + else + table.insert(merged_parts, { from = i, to = next_part.from - 1 }) + i = next_part.from + end + else + table.insert(merged_parts, { from = i, to = length }) + break + end + end + + rendered_text = table.concat(gtable.map(function(part) + local capture = string.sub(rendered_text, part.from, part.to) + if part.highlight then + return markup.bg(self.highlight_bg, markup.fg(self.highlight_fg, capture)) + else + return capture + end + end, merged_parts), "") + end + end + + return label.prefix .. rendered_text .. label.suffix + end + + function widget_instance:_create_group_columns(column_layouts, group, keys, s, wibox_height, find_data) local line_height = math.max( beautiful.get_font_height(self.font), beautiful.get_font_height(self.description_font) ) local group_label_height = line_height + self.group_margin -- -1 for possible pagination: - local max_height_px = wibox_height - group_label_height + local max_height_px = wibox_height - group_label_height - find_data.container_height local joined_descriptions = "" for i, key in ipairs(keys) do @@ -569,9 +714,8 @@ function widget.new(args) current_column.layout:add(self:_group_label(group)) local function insert_keys(ik_keys, ik_add_new_column) - local max_label_width = 0 - local joined_labels = "" - for i, key in ipairs(ik_keys) do + local labels = {} + for _, key in ipairs(ik_keys) do local modifiers = key.mod if not modifiers or modifiers == "none" then modifiers = "" @@ -589,25 +733,29 @@ function widget.new(args) elseif key.key then key_label = gstring.xml_escape(key.key) end - local rendered_hotkey = markup.font(self.font, - modifiers .. key_label .. " " - ) .. markup.font(self.description_font, - key.description or "" - ) - local label_width = wibox.widget.textbox.get_markup_geometry(rendered_hotkey, s).width - if label_width > max_label_width then - max_label_width = label_width - end - joined_labels = joined_labels .. rendered_hotkey .. (i~=#ik_keys and "\n" or "") - end - current_column.layout:add(wibox.widget.textbox(joined_labels)) + local label = { + prefix = markup.font(self.font, modifiers .. key_label .. " ") .. + markup.font_start(self.description_font), + suffix = markup.font_end(), + text = tostring(key.description or ""), + } + table.insert(labels, label) + end + local rendered_hotkeys = self:_render_all_hotkeys(labels) + local textbox = wibox.widget.textbox(rendered_hotkeys) + current_column.layout:add(textbox) + table.insert(find_data.groups, { + textbox = textbox, + labels = labels, + }) + local max_label_width = wibox.widget.textbox.get_markup_geometry(rendered_hotkeys, s).width local max_width = max_label_width + self.group_margin if not current_column.max_width or (max_width > current_column.max_width) then current_column.max_width = max_width end -- +1 for group label: current_column.height_px = (current_column.height_px or 0) + - gstring.linecount(joined_labels)*line_height + group_label_height + gstring.linecount(rendered_hotkeys)*line_height + group_label_height if ik_add_new_column then table.insert(column_layouts, current_column) end @@ -620,13 +768,7 @@ function widget.new(args) end end - function widget_instance:_create_wibox(s, available_groups, show_awesome_keys) - s = get_screen(s) - local wa = s.workarea - local wibox_height = (self.height < wa.height) and self.height or - (wa.height - self.border_width * 2) - local wibox_width = (self.width < wa.width) and self.width or - (wa.width - self.border_width * 2) + function widget_instance:_create_pages(s, available_groups, show_awesome_keys, wibox_width, wibox_height, find_data) -- arrange hotkey groups into columns local column_layouts = {} @@ -636,7 +778,7 @@ function widget.new(args) self._additional_hotkeys[group] ) if #keys > 0 then - self:_create_group_columns(column_layouts, group, keys, s, wibox_height) + self:_create_group_columns(column_layouts, group, keys, s, wibox_height, find_data) end end @@ -667,6 +809,43 @@ function widget.new(args) end table.insert(pages, columns) + return pages + end + + function widget_instance:_create_find_data(find_args) + local data = { + enabled = find_args.enabled, + textbox = wibox.widget.textbox(), + groups = {}, + last_query = "", + } + + if find_args.enabled then + local margin = self.find_margin + data.container = wibox.container.margin(data.textbox, margin, margin, margin, margin) + data.container_height = beautiful.get_font_height(self.find_font) + 2 * margin + else + data.container = wibox.widget.empty + data.container_height = 0 + end + + return data + end + + function widget_instance:_create_wibox(s, available_groups, show_awesome_keys, find_args) + s = get_screen(s) + local wa = s.workarea + local wibox_height = (self.height < wa.height) and self.height or + (wa.height - self.border_width * 2) + local wibox_width = (self.width < wa.width) and self.width or + (wa.width - self.border_width * 2) + + local find_data = self:_create_find_data(find_args) + + local pages = self:_create_pages(s, available_groups, show_awesome_keys, wibox_width, wibox_height, find_data) + + local popup_widget = wibox.layout.align.vertical(nil, pages[1], find_data.container) + -- Function to place the widget in the center and account for the -- workarea. This will be called in the placement field of the -- awful.popup constructor. @@ -676,7 +855,7 @@ function widget.new(args) -- Construct the popup with the widget local mypopup = awful.popup { - widget = pages[1], + widget = popup_widget, ontop = true, bg=self.bg, fg=self.fg, @@ -693,33 +872,93 @@ function widget.new(args) local widget_obj = { current_page = 1, popup = mypopup, + find_data = find_data, } + local function set_page(page) + if page < 1 then + page = 1 + elseif page >= #pages then + page = #pages + end + + if widget_obj.current_page == page then + return + end + widget_obj.current_page = page + + popup_widget:set_middle(pages[page]) + end + -- Set up the mouse buttons to hide the popup - -- Any keybinding except what the keygrabber wants wil hide the popup - -- too mypopup.buttons = { awful.button({ }, 1, function () widget_obj:hide() end), awful.button({ }, 3, function () widget_obj:hide() end) } function widget_obj.page_next(w_self) - if w_self.current_page == #pages then return end - w_self.current_page = w_self.current_page + 1 - w_self.popup:set_widget(pages[w_self.current_page]) + set_page(w_self.current_page + 1) end function widget_obj.page_prev(w_self) - if w_self.current_page == 1 then return end - w_self.current_page = w_self.current_page - 1 - w_self.popup:set_widget(pages[w_self.current_page]) + set_page(w_self.current_page - 1) end function widget_obj.show(w_self) + w_self:find(nil) + awful.prompt.run { + textbox = w_self.find_data.textbox, + prompt = find_args.prompt, + fg_cursor = self.find_fg_cursor, + bg_cursor = self.find_bg_cursor, + ul_cursor = self.find_ul_cursor, + font = self.find_font, + changed_callback = function(input) + w_self:find(input) + end, + done_callback = function() + w_self:hide() + end, + keypressed_callback = function(_, key) + if key == "Prior" then + w_self:page_prev() + elseif key == "Next" then + w_self:page_next() + elseif not w_self.find_data.enabled then + w_self:hide() + end + end, + } w_self.popup.visible = true end function widget_obj.hide(w_self) + awful.keygrabber.stop() w_self.popup.visible = false - if w_self.keygrabber then - awful.keygrabber.stop(w_self.keygrabber) + end + function widget_obj.find(w_self, input) + if not w_self.find_data.enabled then + return + end + + local keywords = {} + for keyword in string.gmatch(input or "", "([^%s]+)") do + keyword = string.lower(keyword) + if #keyword > 0 and not keywords[keyword] then + keywords[keyword] = true + if pcall(string.find, "", keyword) then + table.insert(keywords, keyword) + end + end + end + + table.sort(keywords, function(a, b) return #a > #b end) + + local query = table.concat(keywords, " ") + if w_self.find_data.last_query == query then + return + end + w_self.find_data.last_query = query + + for _, group in ipairs(w_self.find_data.groups) do + group.textbox:set_markup(self:_render_all_hotkeys(group.labels, keywords)) end end @@ -731,15 +970,26 @@ function widget.new(args) -- @tparam[opt=client.focus] client c Client. -- @tparam[opt=c.screen] screen s Screen. -- @tparam[opt={}] table show_args Additional arguments. + -- @tparam[opt=true] boolean show_args.enable_find Enable find feature. + -- @tparam[opt="Find: "] string show_args.find_prompt The find prompt text. -- @tparam[opt=true] boolean show_args.show_awesome_keys Show AwesomeWM hotkeys. -- When set to `false` only app-specific hotkeys will be shown. - -- @treturn awful.keygrabber The keybrabber used to detect when the key is - -- released. + -- @noreturn -- @method show_help function widget_instance:show_help(c, s, show_args) show_args = show_args or {} local show_awesome_keys = show_args.show_awesome_keys ~= false + local find_args = { + enabled = show_args.enable_find ~= false, + prompt = type(show_args.find_prompt) == "string" + and show_args.find_prompt + or "Find: ", + get_cache_key = function(self) + return tostring(self.enabled) .. self.prompt + end, + } + self:_import_awful_keys() self:_load_widget_settings() @@ -767,29 +1017,15 @@ function widget.new(args) if not need_match then table.insert(available_groups, group) end end - local joined_groups = join_plus_sort(available_groups)..tostring(show_awesome_keys) + local cache_key = join_plus_sort(available_groups)..tostring(show_awesome_keys)..find_args:get_cache_key() if not self._cached_wiboxes[s] then self._cached_wiboxes[s] = {} end - if not self._cached_wiboxes[s][joined_groups] then - self._cached_wiboxes[s][joined_groups] = self:_create_wibox(s, available_groups, show_awesome_keys) + if not self._cached_wiboxes[s][cache_key] then + self._cached_wiboxes[s][cache_key] = self:_create_wibox(s, available_groups, show_awesome_keys, find_args) end - local help_wibox = self._cached_wiboxes[s][joined_groups] + local help_wibox = self._cached_wiboxes[s][cache_key] help_wibox:show() - - help_wibox.keygrabber = awful.keygrabber.run(function(_, key, event) - if event == "release" then return end - if key then - if key == "Next" then - help_wibox:page_next() - elseif key == "Prior" then - help_wibox:page_prev() - else - help_wibox:hide() - end - end - end) - return help_wibox.keygrabber end --- Add hotkey descriptions for third-party applications. @@ -839,13 +1075,13 @@ end -- @tparam[opt] client c Client. -- @tparam[opt] screen s Screen. -- @tparam[opt] table args Additional arguments. +-- @tparam[opt=true] boolean args.enable_find Enable find. -- @tparam[opt=true] boolean args.show_awesome_keys Show AwesomeWM hotkeys. -- When set to `false` only app-specific hotkeys will be shown. --- @treturn awful.keygrabber The keybrabber used to detect when the key is --- released. +-- @noreturn -- @staticfct awful.hotkeys_popup.widget.show_help function widget.show_help(...) - return get_default_widget():show_help(...) + get_default_widget():show_help(...) end --- Add hotkey descriptions for third-party applications diff --git a/tests/test-awesomerc.lua b/tests/test-awesomerc.lua index 448d25c2b1..407bb2877d 100644 --- a/tests/test-awesomerc.lua +++ b/tests/test-awesomerc.lua @@ -282,12 +282,12 @@ local steps = { if count == 2 then assert(hotkeys_popup ~= nil) assert(hotkeys_popup.visible) - -- Should disappear on anykey - root.fake_input("key_press", "Super_L") + -- Should disappear on escape (awful.prompt) + root.fake_input("key_press", "Escape") elseif count == 3 then assert(not hotkeys_popup.visible) - root.fake_input("key_release", "Super_L") + root.fake_input("key_release", "Escape") test_context.hotkeys01_clients_before = #client.get() -- imitate fake client with name "vim" -- so hotkeys widget will offer vim hotkeys: @@ -314,11 +314,11 @@ local steps = { assert(visible_hotkeys_widget ~= nil) assert(visible_hotkeys_widget.current_page == 2) root.fake_input("key_release", "Next") - -- Should disappear on anykey - root.fake_input("key_press", "Super_L") + -- Should disappear on escape (awful.prompt) + root.fake_input("key_press", "Escape") elseif (count - test_context.hotkeys01_count_vim) == 3 then assert(not visible_hotkeys_widget) - root.fake_input("key_release", "Super_L") + root.fake_input("key_release", "Escape") return true end end