From 6e005d4a8d869b40a464be69a09ad52ccc709972 Mon Sep 17 00:00:00 2001 From: myk002 Date: Fri, 4 Nov 2022 17:42:38 -0700 Subject: [PATCH 01/18] implement basic logic for hotspot menu --- plugins/hotkeys.cpp | 12 ++++ plugins/lua/hotkeys.lua | 150 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 plugins/lua/hotkeys.lua diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index e92bc61fe..6637c9931 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -349,6 +349,18 @@ static command_result hotkeys_cmd(color_ostream &out, vector & paramete return CR_OK; } +static int getHotkeys(lua_State *L) { + find_active_keybindings(Gui::getCurViewscreen(true)); + Lua::PushVector(L, sorted_keys); + Lua::Push(L, current_bindings); + return 2; +} + +DFHACK_PLUGIN_LUA_COMMANDS { + DFHACK_LUA_COMMAND(getHotkeys), + DFHACK_LUA_END +}; + DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) { diff --git a/plugins/lua/hotkeys.lua b/plugins/lua/hotkeys.lua new file mode 100644 index 000000000..562dfd719 --- /dev/null +++ b/plugins/lua/hotkeys.lua @@ -0,0 +1,150 @@ +local _ENV = mkmodule('plugins.hotkeys') + +local gui = require('gui') +local helpdb = require('helpdb') +local overlay = require('plugins.overlay') +local widgets = require('gui.widgets') + +-- ----------------- -- +-- HotspotMenuWidget -- +-- ----------------- -- + +HotspotMenuWidget = defclass(HotspotMenuWidget, overlay.OverlayWidget) +HotspotMenuWidget.ATTRS{ + default_pos={x=1,y=2}, + hotspot=true, + viewscreens={'dwarfmode'}, + overlay_onupdate_max_freq_seconds=0, + frame={w=2, h=1} +} + +function HotspotMenuWidget:init() + self:addviews{widgets.Label{text='!!'}} + self.mouseover = false +end + +function HotspotMenuWidget:overlay_onupdate() + local hasMouse = self:getMousePos() + if hasMouse and not self.mouseover then + self.mouseover = true + return true + end + self.mouseover = hasMouse +end + +function HotspotMenuWidget:overlay_trigger() + local hotkeys, bindings = getHotkeys() + return MenuScreen{ + hotspot_frame=self.frame, + hotkeys=hotkeys, + bindings=bindings}:show() +end + +-- register the menu hotspot with the overlay +OVERLAY_WIDGETS = {menu=HotspotMenuWidget} + +-- ---------- -- +-- MenuScreen -- +-- ---------- -- + +local ARROW = string.char(26) +local MENU_WIDTH = 40 +local MENU_HEIGHT = 10 + +MenuScreen = defclass(MenuScreen, gui.Screen) +MenuScreen.ATTRS{ + focus_path='hotkeys/menu', + hotspot_frame=DEFAULT_NIL, + hotkeys=DEFAULT_NIL, + bindings=DEFAULT_NIL, +} + +function MenuScreen:init() + self.mouseover = false + + local list_frame = copyall(self.hotspot_frame) + list_frame.w = MENU_WIDTH + list_frame.h = MENU_HEIGHT + + local help_frame = {w=MENU_WIDTH, l=list_frame.l, r=list_frame.r} + if list_frame.t then + help_frame.t = list_frame.t + MENU_HEIGHT + 1 + else + help_frame.b = list_frame.b + MENU_HEIGHT + 1 + end + + local bindings = self.bindings + local choices = {} + for _,hotkey in ipairs(self.hotkeys) do + local command = bindings[hotkey] + local choice_text = command .. (' (%s)'):format(hotkey) + local choice = { + icon=ARROW, text=choice_text, command=command} + table.insert(choices, list_frame.b and 1 or #choices + 1, choice) + end + + self:addviews{ + widgets.List{ + view_id='list', + frame=list_frame, + choices=choices, + icon_width=2, + on_select=self:callback('onSelect'), + on_submit=self:callback('onSubmit'), + on_submit2=self:callback('onSubmit2'), + }, + widgets.WrappedLabel{ + view_id='help', + frame=help_frame, + text_to_wrap='', + scroll_keys={}, + }, + } +end + +function MenuScreen:onSelect(_, choice) + if not choice then return end + local help = self.subviews.help + local first_word = choice.command:trim():split(' +')[1] + if not help or #first_word == 0 then return end + help.text_to_wrap = helpdb.get_entry_short_help(first_word) + help:updateLayout() +end + +function MenuScreen:onSubmit(_, choice) + self:dismiss() + dfhack.run_command(choice.command) +end + +function MenuScreen:onSubmit2(_, choice) + self:dismiss() + dfhack.run_script('gui/launcher', choice.command) +end + +function MenuScreen:onInput(keys) + if keys.LEAVESCREEN then + self:dismiss() + return true + end + return self:inputToSubviews(keys) +end + +function MenuScreen:onRenderBody(dc) + self:renderParent() + local list = self.subviews.list + local idx = list:getIdxUnderMouse() + if idx and idx ~= self.last_mouse_idx then + -- focus follows mouse, but if cursor keys were used to change the + -- selection, don't override the selection until the mouse moves to + -- another item + list:setSelected(idx) + self.mouseover = true + self.last_mouse_idx = idx + elseif not idx and self.mouseover then + -- once the mouse has entered the list area, leaving it again should + -- close the menu screen + self:dismiss() + end +end + +return _ENV From ae2d9008ef37e3d057493c569c1993c87351f41c Mon Sep 17 00:00:00 2001 From: myk002 Date: Sun, 6 Nov 2022 16:42:13 -0800 Subject: [PATCH 02/18] add frames around menu panels --- plugins/lua/hotkeys.lua | 55 +++++++++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/plugins/lua/hotkeys.lua b/plugins/lua/hotkeys.lua index 562dfd719..e83cf9586 100644 --- a/plugins/lua/hotkeys.lua +++ b/plugins/lua/hotkeys.lua @@ -48,8 +48,8 @@ OVERLAY_WIDGETS = {menu=HotspotMenuWidget} -- ---------- -- local ARROW = string.char(26) -local MENU_WIDTH = 40 -local MENU_HEIGHT = 10 +local MENU_WIDTH = 42 +local MENU_HEIGHT = 12 MenuScreen = defclass(MenuScreen, gui.Screen) MenuScreen.ATTRS{ @@ -84,31 +84,45 @@ function MenuScreen:init() end self:addviews{ - widgets.List{ - view_id='list', + widgets.ResizingPanel{ + autoarrange_subviews=true, frame=list_frame, - choices=choices, - icon_width=2, - on_select=self:callback('onSelect'), - on_submit=self:callback('onSubmit'), - on_submit2=self:callback('onSubmit2'), + frame_style=gui.GREY_LINE_FRAME, + frame_background=gui.CLEAR_PEN, + subviews={ + widgets.List{ + view_id='list', + choices=choices, + icon_width=2, + on_select=self:callback('onSelect'), + on_submit=self:callback('onSubmit'), + on_submit2=self:callback('onSubmit2'), + }, + }, }, - widgets.WrappedLabel{ - view_id='help', + widgets.ResizingPanel{ + view_id='help_panel', + autoarrange_subviews=true, frame=help_frame, - text_to_wrap='', - scroll_keys={}, + frame_style=gui.GREY_LINE_FRAME, + frame_background=gui.CLEAR_PEN, + subviews={ + widgets.WrappedLabel{ + view_id='help', + text_to_wrap='', + scroll_keys={}, + }, + }, }, } end function MenuScreen:onSelect(_, choice) - if not choice then return end - local help = self.subviews.help + if not choice or #self.subviews == 0 then return end local first_word = choice.command:trim():split(' +')[1] - if not help or #first_word == 0 then return end - help.text_to_wrap = helpdb.get_entry_short_help(first_word) - help:updateLayout() + self.subviews.help.text_to_wrap = helpdb.is_entry(first_word) and + helpdb.get_entry_short_help(first_word) or 'Command not found' + self.subviews.help_panel:updateLayout() end function MenuScreen:onSubmit(_, choice) @@ -129,8 +143,11 @@ function MenuScreen:onInput(keys) return self:inputToSubviews(keys) end -function MenuScreen:onRenderBody(dc) +function MenuScreen:onRenderFrame(dc, rect) self:renderParent() +end + +function MenuScreen:onRenderBody(dc) local list = self.subviews.list local idx = list:getIdxUnderMouse() if idx and idx ~= self.last_mouse_idx then From fb7b55fb111d0a2acf7ad13bbd5cbaad0ca12c19 Mon Sep 17 00:00:00 2001 From: myk002 Date: Sun, 6 Nov 2022 16:47:45 -0800 Subject: [PATCH 03/18] open gui/launcher with the command on right arrow --- plugins/lua/hotkeys.lua | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/lua/hotkeys.lua b/plugins/lua/hotkeys.lua index e83cf9586..921a1fd9b 100644 --- a/plugins/lua/hotkeys.lua +++ b/plugins/lua/hotkeys.lua @@ -126,11 +126,13 @@ function MenuScreen:onSelect(_, choice) end function MenuScreen:onSubmit(_, choice) + if not choice then return end self:dismiss() dfhack.run_command(choice.command) end function MenuScreen:onSubmit2(_, choice) + if not choice then return end self:dismiss() dfhack.run_script('gui/launcher', choice.command) end @@ -139,6 +141,9 @@ function MenuScreen:onInput(keys) if keys.LEAVESCREEN then self:dismiss() return true + elseif keys.STANDARDSCROLL_RIGHT then + self:onSubmit2(self.subviews.list:getSelected()) + return true end return self:inputToSubviews(keys) end From d8c86fd0b1fb1f20a3101e5ac67e58769f06d388 Mon Sep 17 00:00:00 2001 From: myk002 Date: Sun, 6 Nov 2022 16:54:05 -0800 Subject: [PATCH 04/18] allow commands with hotkey guards to work --- plugins/lua/hotkeys.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lua/hotkeys.lua b/plugins/lua/hotkeys.lua index 921a1fd9b..1cd0e74c4 100644 --- a/plugins/lua/hotkeys.lua +++ b/plugins/lua/hotkeys.lua @@ -127,8 +127,8 @@ end function MenuScreen:onSubmit(_, choice) if not choice then return end + dfhack.screen.hideGuard(self, dfhack.run_command, choice.command) self:dismiss() - dfhack.run_command(choice.command) end function MenuScreen:onSubmit2(_, choice) From c630a71c73076f7d2ffc827814f5d1191979aed2 Mon Sep 17 00:00:00 2001 From: myk002 Date: Sun, 6 Nov 2022 16:58:44 -0800 Subject: [PATCH 05/18] click on arrow to launch gui/launcher with command --- plugins/lua/hotkeys.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugins/lua/hotkeys.lua b/plugins/lua/hotkeys.lua index 1cd0e74c4..1750d5470 100644 --- a/plugins/lua/hotkeys.lua +++ b/plugins/lua/hotkeys.lua @@ -144,6 +144,13 @@ function MenuScreen:onInput(keys) elseif keys.STANDARDSCROLL_RIGHT then self:onSubmit2(self.subviews.list:getSelected()) return true + elseif keys._MOUSE_L then + local list = self.subviews.list + local x = list:getMousePos() + if x == 0 then -- clicked on icon + self:onSubmit2(list:getSelected()) + return true + end end return self:inputToSubviews(keys) end From 5d29da31b0a349f6da584f27cd23c72efd315192 Mon Sep 17 00:00:00 2001 From: myk002 Date: Mon, 7 Nov 2022 09:13:27 -0800 Subject: [PATCH 06/18] rework hotkeys plugin to support the widget --- plugins/hotkeys.cpp | 293 +++++--------------------------------------- 1 file changed, 31 insertions(+), 262 deletions(-) diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index 6637c9931..70f6a2f98 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -1,24 +1,27 @@ -#include "uicommon.h" -#include "listcolumn.h" +#include +#include +#include -#include "df/viewscreen_dwarfmodest.h" -#include "df/ui.h" - -#include "modules/Maps.h" -#include "modules/World.h" #include "modules/Gui.h" +#include "modules/Screen.h" #include "LuaTools.h" #include "PluginManager.h" DFHACK_PLUGIN("hotkeys"); -#define PLUGIN_VERSION 0.1 + +using std::map; +using std::string; +using std::vector; + +using namespace DFHack; + +static const std::string MENU_SCREEN_FOCUS_STRING = "dfhack/lua/hotkeys/menu"; static map current_bindings; static vector sorted_keys; -static bool show_usage = false; -static bool can_invoke(string cmdline, df::viewscreen *screen) +static bool can_invoke(const string &cmdline, df::viewscreen *screen) { vector cmd_parts; split_string(&cmd_parts, cmdline, " "); @@ -28,14 +31,14 @@ static bool can_invoke(string cmdline, df::viewscreen *screen) return Core::getInstance().getPluginManager()->CanInvokeHotkey(cmd_parts[0], screen); } -static void add_binding_if_valid(string sym, string cmdline, df::viewscreen *screen) +static void add_binding_if_valid(const string &sym, const string &cmdline, df::viewscreen *screen) { if (!can_invoke(cmdline, screen)) return; current_bindings[sym] = cmdline; sorted_keys.push_back(sym); - string keyspec = sym + "@dfhack/viewscreen_hotkeys"; + string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING; Core::getInstance().AddKeyBinding(keyspec, "hotkeys invoke " + int_to_string(sorted_keys.size() - 1)); } @@ -101,252 +104,39 @@ static void find_active_keybindings(df::viewscreen *screen) static bool close_hotkeys_screen() { auto screen = Core::getTopViewscreen(); - if (Gui::getFocusString(screen) != "dfhack/viewscreen_hotkeys") + if (Gui::getFocusString(screen) != MENU_SCREEN_FOCUS_STRING) return false; Screen::dismiss(Core::getTopViewscreen()); - for_each_(sorted_keys, [] (const string &sym) - { Core::getInstance().ClearKeyBindings(sym + "@dfhack/viewscreen_hotkeys"); }); + std::for_each(sorted_keys.begin(), sorted_keys.end(), [](const string &sym){ + Core::getInstance().ClearKeyBindings(sym + "@" + MENU_SCREEN_FOCUS_STRING); + }); sorted_keys.clear(); return true; } - -static void invoke_command(const size_t index) +static bool invoke_command(const size_t index) { if (sorted_keys.size() <= index) - return; + return false; auto cmd = current_bindings[sorted_keys[index]]; - if (close_hotkeys_screen()) - { + if (close_hotkeys_screen()) { Core::getInstance().setHotkeyCmd(cmd); + return true; } + return false; } -static std::string get_help(const std::string &command, bool full_help) -{ - auto L = Lua::Core::State; - color_ostream_proxy out(Core::getInstance().getConsole()); - Lua::StackUnwinder top(L); - - if (!lua_checkstack(L, 2) || - !Lua::PushModulePublic(out, L, "helpdb", - full_help ? "get_entry_long_help" : "get_entry_short_help")) - return "Help text unavailable."; - - Lua::Push(L, command); - - if (!Lua::SafeCall(out, L, 1, 1)) - return "Help text unavailable."; - - const char *s = lua_tostring(L, -1); - if (!s) - return "Help text unavailable."; - - return s; -} - -class ViewscreenHotkeys : public dfhack_viewscreen -{ -public: - ViewscreenHotkeys(df::viewscreen *top_screen) : top_screen(top_screen) - { - hotkeys_column.multiselect = false; - hotkeys_column.auto_select = true; - hotkeys_column.setTitle("Key Binding"); - hotkeys_column.bottom_margin = 4; - hotkeys_column.allow_search = false; - - focus = Gui::getFocusString(top_screen); - populateColumns(); - } - - void populateColumns() - { - hotkeys_column.clear(); - - size_t max_key_length = 0; - for_each_(sorted_keys, [&] (const string &sym) - { if (sym.length() > max_key_length) { max_key_length = sym.length(); } }); - int padding = max_key_length + 2; - - for (size_t i = 0; i < sorted_keys.size(); i++) - { - string text = pad_string(sorted_keys[i], padding, false); - text += current_bindings[sorted_keys[i]]; - hotkeys_column.add(text, i+1); - - } - - help_start = hotkeys_column.fixWidth() + 2; - hotkeys_column.filterDisplay(); - } - - void feed(set *input) - { - if (hotkeys_column.feed(input)) - return; - - if (input->count(interface_key::LEAVESCREEN)) - { - close_hotkeys_screen(); - } - else if (input->count(interface_key::SELECT)) - { - invoke_command(hotkeys_column.highlighted_index); - } - else if (input->count(interface_key::CUSTOM_U)) - { - show_usage = !show_usage; - } - } - - void render() - { - if (Screen::isDismissed(this)) - return; - - dfhack_viewscreen::render(); - - Screen::clear(); - Screen::drawBorder(" Hotkeys "); - - hotkeys_column.display(true); - - int32_t y = gps->dimy - 3; - int32_t x = 2; - OutputHotkeyString(x, y, "Leave", "Esc"); - - x += 3; - OutputHotkeyString(x, y, "Invoke", "Enter or Hotkey"); - - x += 3; - OutputToggleString(x, y, "Show Usage", "u", show_usage); - - y = gps->dimy - 4; - x = 2; - OutputHotkeyString(x, y, focus.c_str(), "Context", false, help_start, COLOR_WHITE, COLOR_BROWN); - - if (sorted_keys.size() == 0) - return; - - y = 2; - x = help_start; - - auto width = gps->dimx - help_start - 2; - vector parts; - Core::cheap_tokenise(current_bindings[sorted_keys[hotkeys_column.highlighted_index]], parts); - if(parts.size() == 0) - return; - - string first = parts[0]; - parts.erase(parts.begin()); - - if (first[0] == '#') - return; - - OutputString(COLOR_BROWN, x, y, "Help", true, help_start); - string help_text = get_help(first, show_usage); - vector lines; - split_string(&lines, help_text, "\n"); - for (auto it = lines.begin(); it != lines.end() && y < gps->dimy - 4; it++) - { - auto wrapped_lines = wrapString(*it, width); - for (auto wit = wrapped_lines.begin(); wit != wrapped_lines.end() && y < gps->dimy - 4; wit++) - { - OutputString(COLOR_WHITE, x, y, *wit, true, help_start); - } - } - } - - virtual std::string getFocusString() - { - return "viewscreen_hotkeys"; - } - -private: - ListColumn hotkeys_column; - df::viewscreen *top_screen; - string focus; - - int32_t help_start; - - void resize(int32_t x, int32_t y) - { - dfhack_viewscreen::resize(x, y); - hotkeys_column.resize(); - } - - static vector wrapString(string str, int width) - { - vector result; - string excess; - if (int(str.length()) > width) - { - auto cut_space = str.rfind(' ', width-1); - int excess_start; - if (cut_space == string::npos) - { - cut_space = width-1; - excess_start = cut_space; - } - else - { - excess_start = cut_space + 1; - } - - string line = str.substr(0, cut_space); - excess = str.substr(excess_start); - result.push_back(line); - auto excess_lines = wrapString(excess, width); - result.insert(result.end(), excess_lines.begin(), excess_lines.end()); - } - else - { - result.push_back(str); - } - - return result; - } -}; - - static command_result hotkeys_cmd(color_ostream &out, vector & parameters) { - if (parameters.empty()) - { - if (Maps::IsValid()) - { - auto top_screen = Core::getTopViewscreen(); - if (Gui::getFocusString(top_screen) != "dfhack/viewscreen_hotkeys") - { - find_active_keybindings(top_screen); - Screen::show(dts::make_unique(top_screen), plugin_self); - } - } - } - else - { - auto cmd = parameters[0][0]; - if (cmd == 'v') - { - out << "Hotkeys" << endl << "Version: " << PLUGIN_VERSION << endl; - } - else if (cmd == 'i') - { - int index; - stringstream index_raw(parameters[1]); - index_raw >> index; - invoke_command(index); - } - else - { - return CR_WRONG_USAGE; - } - } + if (parameters.size() != 2 || parameters[0] != "invoke") + return CR_WRONG_USAGE; - return CR_OK; + int index; + std::stringstream index_raw(parameters[1]); + index_raw >> index; + return invoke_command(index) ? CR_OK : CR_WRONG_USAGE; } static int getHotkeys(lua_State *L) { @@ -364,32 +154,11 @@ DFHACK_PLUGIN_LUA_COMMANDS { DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) { - if (!gps) - out.printerr("Could not insert hotkeys hooks!\n"); - commands.push_back( PluginCommand( "hotkeys", - "Show all dfhack keybindings in current context.", + "Invoke hotkeys from the interactive menu.", hotkeys_cmd)); return CR_OK; } - -DFhackCExport command_result plugin_shutdown ( color_ostream &out ) -{ - return CR_OK; -} - -DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) -{ - switch (event) { - case SC_MAP_LOADED: - sorted_keys.clear(); - break; - default: - break; - } - - return CR_OK; -} From 66d3409a655b38fa31462762742ac8c9a2be5806 Mon Sep 17 00:00:00 2001 From: myk002 Date: Mon, 7 Nov 2022 10:01:49 -0800 Subject: [PATCH 07/18] solve concurrency issues --- plugins/hotkeys.cpp | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index 70f6a2f98..930ffa362 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -5,11 +5,16 @@ #include "modules/Gui.h" #include "modules/Screen.h" +#include "Debug.h" #include "LuaTools.h" #include "PluginManager.h" DFHACK_PLUGIN("hotkeys"); +namespace DFHack { + DBG_DECLARE(hotkeys, log, DebugCategory::LINFO); +} + using std::map; using std::string; using std::vector; @@ -39,7 +44,9 @@ static void add_binding_if_valid(const string &sym, const string &cmdline, df::v current_bindings[sym] = cmdline; sorted_keys.push_back(sym); string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING; - Core::getInstance().AddKeyBinding(keyspec, "hotkeys invoke " + int_to_string(sorted_keys.size() - 1)); + string binding = "hotkeys invoke " + int_to_string(sorted_keys.size() - 1); + DEBUG(log).print("adding keybinding: %s -> %s\n", keyspec.c_str(), binding.c_str()); + Core::getInstance().AddKeyBinding(keyspec, binding); } static void find_active_keybindings(df::viewscreen *screen) @@ -101,31 +108,28 @@ static void find_active_keybindings(df::viewscreen *screen) } } -static bool close_hotkeys_screen() +static bool invoke_command(color_ostream &out, const size_t index) { auto screen = Core::getTopViewscreen(); - if (Gui::getFocusString(screen) != MENU_SCREEN_FOCUS_STRING) + if (sorted_keys.size() <= index || + Gui::getFocusString(screen) != MENU_SCREEN_FOCUS_STRING) return false; - Screen::dismiss(Core::getTopViewscreen()); + auto cmd = current_bindings[sorted_keys[index]]; + DEBUG(log).print("invoking command: '%s'\n", cmd.c_str()); + + { + Screen::Hide hideGuard(screen, Screen::Hide::RESTORE_AT_TOP); + Core::getInstance().runCommand(out, cmd); + } + + Screen::dismiss(screen); std::for_each(sorted_keys.begin(), sorted_keys.end(), [](const string &sym){ Core::getInstance().ClearKeyBindings(sym + "@" + MENU_SCREEN_FOCUS_STRING); }); sorted_keys.clear(); - return true; -} - -static bool invoke_command(const size_t index) -{ - if (sorted_keys.size() <= index) - return false; - auto cmd = current_bindings[sorted_keys[index]]; - if (close_hotkeys_screen()) { - Core::getInstance().setHotkeyCmd(cmd); - return true; - } - return false; + return true; } static command_result hotkeys_cmd(color_ostream &out, vector & parameters) @@ -133,10 +137,12 @@ static command_result hotkeys_cmd(color_ostream &out, vector & paramete if (parameters.size() != 2 || parameters[0] != "invoke") return CR_WRONG_USAGE; + CoreSuspender guard; + int index; std::stringstream index_raw(parameters[1]); index_raw >> index; - return invoke_command(index) ? CR_OK : CR_WRONG_USAGE; + return invoke_command(out, index) ? CR_OK : CR_WRONG_USAGE; } static int getHotkeys(lua_State *L) { From 234919ffe108ae0acd0c3cfe63fb6f53c3bae0b3 Mon Sep 17 00:00:00 2001 From: myk002 Date: Mon, 7 Nov 2022 11:36:15 -0800 Subject: [PATCH 08/18] replace hotkeys keybinding with menu keybinding --- data/init/dfhack.keybindings.init | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/data/init/dfhack.keybindings.init b/data/init/dfhack.keybindings.init index fd02635e2..697641ab1 100644 --- a/data/init/dfhack.keybindings.init +++ b/data/init/dfhack.keybindings.init @@ -12,9 +12,8 @@ keybinding add ` gui/launcher keybinding add Ctrl-Shift-D gui/launcher -# show all current key bindings -keybinding add Ctrl-F1 hotkeys -keybinding add Alt-F1 hotkeys +# show hotkey popup menu +keybinding add Ctrl-Shift-C "overlay trigger hotkeys.menu" # on-screen keyboard keybinding add Ctrl-Shift-K gui/cp437-table From 2b73d6e8e93b1c3b067b0afe45378a3f690c51d6 Mon Sep 17 00:00:00 2001 From: myk002 Date: Mon, 7 Nov 2022 12:18:08 -0800 Subject: [PATCH 09/18] allow hotkeys to be invoked as a hotkey also ensure keybindings are always cleaned up --- plugins/hotkeys.cpp | 24 ++++++++++++++++++------ plugins/lua/hotkeys.lua | 4 ++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index 930ffa362..e275c637a 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -124,11 +124,6 @@ static bool invoke_command(color_ostream &out, const size_t index) } Screen::dismiss(screen); - std::for_each(sorted_keys.begin(), sorted_keys.end(), [](const string &sym){ - Core::getInstance().ClearKeyBindings(sym + "@" + MENU_SCREEN_FOCUS_STRING); - }); - sorted_keys.clear(); - return true; } @@ -152,11 +147,27 @@ static int getHotkeys(lua_State *L) { return 2; } +static int cleanupHotkeys(lua_State *) { + std::for_each(sorted_keys.begin(), sorted_keys.end(), [](const string &sym) { + string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING; + DEBUG(log).print("clearing keybinding: %s\n", keyspec.c_str()); + Core::getInstance().ClearKeyBindings(keyspec); + }); + sorted_keys.clear(); + current_bindings.clear(); + return 0; +} + DFHACK_PLUGIN_LUA_COMMANDS { DFHACK_LUA_COMMAND(getHotkeys), + DFHACK_LUA_COMMAND(cleanupHotkeys), DFHACK_LUA_END }; +// allow "hotkeys" to be invoked as a hotkey from any screen +static bool hotkeys_anywhere(df::viewscreen *) { + return true; +} DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) { @@ -164,7 +175,8 @@ DFhackCExport command_result plugin_init ( color_ostream &out, std::vector Date: Mon, 7 Nov 2022 13:58:39 -0800 Subject: [PATCH 10/18] use a more natural ordering for modifier keys --- plugins/hotkeys.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index e275c637a..3cc2490b7 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -71,16 +71,16 @@ static void find_active_keybindings(df::viewscreen *screen) auto current_focus = Gui::getFocusString(screen); for (int shifted = 0; shifted < 2; shifted++) { - for (int ctrl = 0; ctrl < 2; ctrl++) + for (int alt = 0; alt < 2; alt++) { - for (int alt = 0; alt < 2; alt++) + for (int ctrl = 0; ctrl < 2; ctrl++) { for (auto it = valid_keys.begin(); it != valid_keys.end(); it++) { string sym; - if (shifted) sym += "Shift-"; if (ctrl) sym += "Ctrl-"; if (alt) sym += "Alt-"; + if (shifted) sym += "Shift-"; sym += *it; auto list = Core::getInstance().ListKeyBindings(sym); From 1fc30493c0eae4b3c7943d2a35b393ec65e9df4a Mon Sep 17 00:00:00 2001 From: myk002 Date: Mon, 7 Nov 2022 13:59:10 -0800 Subject: [PATCH 11/18] right align hotkeys for list items and combine hotkeys for identical commands and don't hide the menu until the mouse has left the frame and start the widget one tile closer to the edge so the mouse is already on the list instead of on the frame --- plugins/lua/hotkeys.lua | 98 +++++++++++++++++++++++++++++++++-------- 1 file changed, 79 insertions(+), 19 deletions(-) diff --git a/plugins/lua/hotkeys.lua b/plugins/lua/hotkeys.lua index 95ccfbe1f..bd88fdd89 100644 --- a/plugins/lua/hotkeys.lua +++ b/plugins/lua/hotkeys.lua @@ -11,7 +11,7 @@ local widgets = require('gui.widgets') HotspotMenuWidget = defclass(HotspotMenuWidget, overlay.OverlayWidget) HotspotMenuWidget.ATTRS{ - default_pos={x=1,y=2}, + default_pos={x=1,y=3}, hotspot=true, viewscreens={'dwarfmode'}, overlay_onupdate_max_freq_seconds=0, @@ -48,8 +48,8 @@ OVERLAY_WIDGETS = {menu=HotspotMenuWidget} -- ---------- -- local ARROW = string.char(26) -local MENU_WIDTH = 42 -local MENU_HEIGHT = 12 +local MAX_LIST_WIDTH = 45 +local MAX_LIST_HEIGHT = 15 MenuScreen = defclass(MenuScreen, gui.Screen) MenuScreen.ATTRS{ @@ -59,32 +59,90 @@ MenuScreen.ATTRS{ bindings=DEFAULT_NIL, } +-- get a map from the binding string to a list of hotkey strings that all +-- point to that binding +local function get_bindings_to_hotkeys(hotkeys, bindings) + local bindings_to_hotkeys = {} + for _,hotkey in ipairs(hotkeys) do + local binding = bindings[hotkey] + table.insert(ensure_key(bindings_to_hotkeys, binding), hotkey) + end + return bindings_to_hotkeys +end + +-- number of non-text tiles: icon, space, space between cmd and hk, scrollbar +local LIST_BUFFER = 2 + 1 + 1 + +local function get_choices(hotkeys, bindings, is_inverted) + local choices, max_width, seen = {}, 0, {} + local bindings_to_hotkeys = get_bindings_to_hotkeys(hotkeys, bindings) + + -- build list choices + for _,hotkey in ipairs(hotkeys) do + local command = bindings[hotkey] + if seen[command] then goto continue end + seen[command] = true + local hk_width, tokens = 0, {} + for _,hk in ipairs(bindings_to_hotkeys[command]) do + if hk_width ~= 0 then + table.insert(tokens, ', ') + hk_width = hk_width + 2 + end + table.insert(tokens, {text=hk, pen=COLOR_LIGHTGREEN}) + hk_width = hk_width + #hk + end + local command_str = command + if hk_width + #command + LIST_BUFFER > MAX_LIST_WIDTH then + local max_command_len = MAX_LIST_WIDTH - hk_width - LIST_BUFFER + command_str = command:sub(1, max_command_len - 3) .. '...' + end + table.insert(tokens, 1, {text=command_str}) + local choice = {icon=ARROW, command=command, text=tokens, + hk_width=hk_width} + max_width = math.max(max_width, hk_width + #command_str + LIST_BUFFER) + table.insert(choices, is_inverted and 1 or #choices + 1, choice) + ::continue:: + end + + -- adjust width of command fields so the hotkey tokens are right justified + for _,choice in ipairs(choices) do + local command_token = choice.text[1] + command_token.width = max_width - choice.hk_width - 3 + end + + return choices, max_width +end + function MenuScreen:init() self.mouseover = false - local list_frame = copyall(self.hotspot_frame) - list_frame.w = MENU_WIDTH - list_frame.h = MENU_HEIGHT + local choices,list_width = get_choices(self.hotkeys, self.bindings, + self.hotspot_frame.b) - local help_frame = {w=MENU_WIDTH, l=list_frame.l, r=list_frame.r} + local list_frame = copyall(self.hotspot_frame) + list_frame.w = list_width + 2 + list_frame.h = math.min(#choices, MAX_LIST_HEIGHT) + 2 if list_frame.t then - help_frame.t = list_frame.t + MENU_HEIGHT + 1 + list_frame.t = math.max(0, list_frame.t - 1) else - help_frame.b = list_frame.b + MENU_HEIGHT + 1 + list_frame.b = math.max(0, list_frame.b - 1) + end + if list_frame.l then + list_frame.l = math.max(0, list_frame.l - 1) + else + list_frame.r = math.max(0, list_frame.r - 1) end - local bindings = self.bindings - local choices = {} - for _,hotkey in ipairs(self.hotkeys) do - local command = bindings[hotkey] - local choice_text = command .. (' (%s)'):format(hotkey) - local choice = { - icon=ARROW, text=choice_text, command=command} - table.insert(choices, list_frame.b and 1 or #choices + 1, choice) + local help_frame = {w=list_frame.w, l=list_frame.l, r=list_frame.r} + if list_frame.t then + help_frame.t = list_frame.t + list_frame.h + 1 + else + help_frame.b = list_frame.b + list_frame.h + 1 end self:addviews{ widgets.ResizingPanel{ + view_id='list_panel', autoarrange_subviews=true, frame=list_frame, frame_style=gui.GREY_LINE_FRAME, @@ -164,6 +222,7 @@ function MenuScreen:onRenderFrame(dc, rect) end function MenuScreen:onRenderBody(dc) + local panel = self.subviews.list_panel local list = self.subviews.list local idx = list:getIdxUnderMouse() if idx and idx ~= self.last_mouse_idx then @@ -173,8 +232,9 @@ function MenuScreen:onRenderBody(dc) list:setSelected(idx) self.mouseover = true self.last_mouse_idx = idx - elseif not idx and self.mouseover then - -- once the mouse has entered the list area, leaving it again should + elseif not panel:getMousePos(gui.ViewRect{rect=panel.frame_rect}) + and self.mouseover then + -- once the mouse has entered the list area, leaving the frame should -- close the menu screen self:dismiss() end From de20603080a0c7ba66e4599c2e8c20263f2071e6 Mon Sep 17 00:00:00 2001 From: myk002 Date: Wed, 9 Nov 2022 12:16:44 -0800 Subject: [PATCH 12/18] implement CLI commands --- plugins/hotkeys.cpp | 143 +++++++++++++++++++++++++------------------- 1 file changed, 81 insertions(+), 62 deletions(-) diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index 3cc2490b7..de475ea46 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -23,11 +23,12 @@ using namespace DFHack; static const std::string MENU_SCREEN_FOCUS_STRING = "dfhack/lua/hotkeys/menu"; +static bool valid = false; // whether the following two vars contain valid data +static string current_focus; static map current_bindings; static vector sorted_keys; -static bool can_invoke(const string &cmdline, df::viewscreen *screen) -{ +static bool can_invoke(const string &cmdline, df::viewscreen *screen) { vector cmd_parts; split_string(&cmd_parts, cmdline, " "); if (toLower(cmd_parts[0]) == "hotkeys") @@ -36,8 +37,21 @@ static bool can_invoke(const string &cmdline, df::viewscreen *screen) return Core::getInstance().getPluginManager()->CanInvokeHotkey(cmd_parts[0], screen); } -static void add_binding_if_valid(const string &sym, const string &cmdline, df::viewscreen *screen) -{ +static int cleanupHotkeys(lua_State *) { + DEBUG(log).print("cleaning up old stub keybindings for %s\n", current_focus.c_str()); + std::for_each(sorted_keys.begin(), sorted_keys.end(), [](const string &sym) { + string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING; + DEBUG(log).print("clearing keybinding: %s\n", keyspec.c_str()); + Core::getInstance().ClearKeyBindings(keyspec); + }); + valid = false; + current_focus = ""; + sorted_keys.clear(); + current_bindings.clear(); + return 0; +} + +static void add_binding_if_valid(const string &sym, const string &cmdline, df::viewscreen *screen) { if (!can_invoke(cmdline, screen)) return; @@ -49,34 +63,28 @@ static void add_binding_if_valid(const string &sym, const string &cmdline, df::v Core::getInstance().AddKeyBinding(keyspec, binding); } -static void find_active_keybindings(df::viewscreen *screen) -{ - current_bindings.clear(); - sorted_keys.clear(); +static void find_active_keybindings(df::viewscreen *screen) { + DEBUG(log).print("scanning for active keybindings\n"); + if (valid) + cleanupHotkeys(NULL); vector valid_keys; - for (char c = 'A'; c <= 'Z'; c++) - { + for (char c = 'A'; c <= 'Z'; c++) { valid_keys.push_back(string(&c, 1)); } - for (int i = 1; i < 10; i++) - { + for (int i = 1; i < 10; i++) { valid_keys.push_back("F" + int_to_string(i)); } valid_keys.push_back("`"); - auto current_focus = Gui::getFocusString(screen); - for (int shifted = 0; shifted < 2; shifted++) - { - for (int alt = 0; alt < 2; alt++) - { - for (int ctrl = 0; ctrl < 2; ctrl++) - { - for (auto it = valid_keys.begin(); it != valid_keys.end(); it++) - { + current_focus = Gui::getFocusString(screen); + for (int shifted = 0; shifted < 2; shifted++) { + for (int alt = 0; alt < 2; alt++) { + for (int ctrl = 0; ctrl < 2; ctrl++) { + for (auto it = valid_keys.begin(); it != valid_keys.end(); it++) { string sym; if (ctrl) sym += "Ctrl-"; if (alt) sym += "Alt-"; @@ -84,19 +92,15 @@ static void find_active_keybindings(df::viewscreen *screen) sym += *it; auto list = Core::getInstance().ListKeyBindings(sym); - for (auto invoke_cmd = list.begin(); invoke_cmd != list.end(); invoke_cmd++) - { - if (invoke_cmd->find(":") == string::npos) - { + for (auto invoke_cmd = list.begin(); invoke_cmd != list.end(); invoke_cmd++) { + if (invoke_cmd->find(":") == string::npos) { add_binding_if_valid(sym, *invoke_cmd, screen); } - else - { + else { vector tokens; split_string(&tokens, *invoke_cmd, ":"); string focus = tokens[0].substr(1); - if (prefix_matches(focus, current_focus)) - { + if (prefix_matches(focus, current_focus)) { auto cmdline = trim(tokens[1]); add_binding_if_valid(sym, cmdline, screen); } @@ -106,10 +110,40 @@ static void find_active_keybindings(df::viewscreen *screen) } } } + + valid = true; +} + +static int getHotkeys(lua_State *L) { + find_active_keybindings(Gui::getCurViewscreen(true)); + Lua::PushVector(L, sorted_keys); + Lua::Push(L, current_bindings); + return 2; +} + +DFHACK_PLUGIN_LUA_COMMANDS { + DFHACK_LUA_COMMAND(getHotkeys), + DFHACK_LUA_COMMAND(cleanupHotkeys), + DFHACK_LUA_END +}; + +static void list(color_ostream &out) { + DEBUG(log).print("listing active hotkeys\n"); + bool was_valid = valid; + if (!valid) + find_active_keybindings(Gui::getCurViewscreen(true)); + + out.print("Valid keybindings for the current screen (%s)\n", + current_focus.c_str()); + std::for_each(sorted_keys.begin(), sorted_keys.end(), [&](const string &sym) { + out.print("%s: %s\n", sym.c_str(), current_bindings[sym].c_str()); + }); + + if (!was_valid) + cleanupHotkeys(NULL); } -static bool invoke_command(color_ostream &out, const size_t index) -{ +static bool invoke_command(color_ostream &out, const size_t index) { auto screen = Core::getTopViewscreen(); if (sorted_keys.size() <= index || Gui::getFocusString(screen) != MENU_SCREEN_FOCUS_STRING) @@ -127,50 +161,35 @@ static bool invoke_command(color_ostream &out, const size_t index) return true; } -static command_result hotkeys_cmd(color_ostream &out, vector & parameters) -{ +static command_result hotkeys_cmd(color_ostream &out, vector & parameters) { + if (!parameters.size()) { + static const string invokeOverlayCmd = "overlay trigger hotkeys.menu"; + DEBUG(log).print("invoking command: '%s'\n", invokeOverlayCmd.c_str()); + return Core::getInstance().runCommand(out, invokeOverlayCmd); + } + + if (parameters[0] == "list") { + list(out); + return CR_OK; + } + if (parameters.size() != 2 || parameters[0] != "invoke") return CR_WRONG_USAGE; CoreSuspender guard; - int index; - std::stringstream index_raw(parameters[1]); - index_raw >> index; + int index = string_to_int(parameters[1], -1); + if (index < 0) + return CR_WRONG_USAGE; return invoke_command(out, index) ? CR_OK : CR_WRONG_USAGE; } -static int getHotkeys(lua_State *L) { - find_active_keybindings(Gui::getCurViewscreen(true)); - Lua::PushVector(L, sorted_keys); - Lua::Push(L, current_bindings); - return 2; -} - -static int cleanupHotkeys(lua_State *) { - std::for_each(sorted_keys.begin(), sorted_keys.end(), [](const string &sym) { - string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING; - DEBUG(log).print("clearing keybinding: %s\n", keyspec.c_str()); - Core::getInstance().ClearKeyBindings(keyspec); - }); - sorted_keys.clear(); - current_bindings.clear(); - return 0; -} - -DFHACK_PLUGIN_LUA_COMMANDS { - DFHACK_LUA_COMMAND(getHotkeys), - DFHACK_LUA_COMMAND(cleanupHotkeys), - DFHACK_LUA_END -}; - // allow "hotkeys" to be invoked as a hotkey from any screen static bool hotkeys_anywhere(df::viewscreen *) { return true; } -DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) -{ +DFhackCExport command_result plugin_init (color_ostream &out, std::vector &commands) { commands.push_back( PluginCommand( "hotkeys", From a2efc41fef3fa9ed4c99619286b889a2a1cf654b Mon Sep 17 00:00:00 2001 From: myk002 Date: Wed, 9 Nov 2022 14:36:23 -0800 Subject: [PATCH 13/18] use new anywhere hotkey and filter out own hotkey --- plugins/hotkeys.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index de475ea46..72f2e0111 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -21,6 +21,7 @@ using std::vector; using namespace DFHack; +static const string INVOKE_MENU_COMMAND = "overlay trigger hotkeys.menu"; static const std::string MENU_SCREEN_FOCUS_STRING = "dfhack/lua/hotkeys/menu"; static bool valid = false; // whether the following two vars contain valid data @@ -55,6 +56,11 @@ static void add_binding_if_valid(const string &sym, const string &cmdline, df::v if (!can_invoke(cmdline, screen)) return; + if (cmdline == INVOKE_MENU_COMMAND) { + DEBUG(log).print("filtering out hotkey menu keybinding\n"); + return; + } + current_bindings[sym] = cmdline; sorted_keys.push_back(sym); string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING; @@ -163,9 +169,8 @@ static bool invoke_command(color_ostream &out, const size_t index) { static command_result hotkeys_cmd(color_ostream &out, vector & parameters) { if (!parameters.size()) { - static const string invokeOverlayCmd = "overlay trigger hotkeys.menu"; - DEBUG(log).print("invoking command: '%s'\n", invokeOverlayCmd.c_str()); - return Core::getInstance().runCommand(out, invokeOverlayCmd); + DEBUG(log).print("invoking command: '%s'\n", INVOKE_MENU_COMMAND.c_str()); + return Core::getInstance().runCommand(out, INVOKE_MENU_COMMAND ); } if (parameters[0] == "list") { @@ -184,18 +189,13 @@ static command_result hotkeys_cmd(color_ostream &out, vector & paramete return invoke_command(out, index) ? CR_OK : CR_WRONG_USAGE; } -// allow "hotkeys" to be invoked as a hotkey from any screen -static bool hotkeys_anywhere(df::viewscreen *) { - return true; -} - DFhackCExport command_result plugin_init (color_ostream &out, std::vector &commands) { commands.push_back( PluginCommand( "hotkeys", "Invoke hotkeys from the interactive menu.", hotkeys_cmd, - hotkeys_anywhere)); + Gui::anywhere_hotkey)); return CR_OK; } From 47d7c477b39edb30a414841d110391618bc811d4 Mon Sep 17 00:00:00 2001 From: myk002 Date: Wed, 9 Nov 2022 17:49:55 -0800 Subject: [PATCH 14/18] show menu hotkey for list but not on the menu --- plugins/hotkeys.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index 72f2e0111..77db827bd 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -52,11 +52,11 @@ static int cleanupHotkeys(lua_State *) { return 0; } -static void add_binding_if_valid(const string &sym, const string &cmdline, df::viewscreen *screen) { +static void add_binding_if_valid(const string &sym, const string &cmdline, df::viewscreen *screen, bool filtermenu) { if (!can_invoke(cmdline, screen)) return; - if (cmdline == INVOKE_MENU_COMMAND) { + if (filtermenu && cmdline == INVOKE_MENU_COMMAND) { DEBUG(log).print("filtering out hotkey menu keybinding\n"); return; } @@ -69,7 +69,7 @@ static void add_binding_if_valid(const string &sym, const string &cmdline, df::v Core::getInstance().AddKeyBinding(keyspec, binding); } -static void find_active_keybindings(df::viewscreen *screen) { +static void find_active_keybindings(df::viewscreen *screen, bool filtermenu) { DEBUG(log).print("scanning for active keybindings\n"); if (valid) cleanupHotkeys(NULL); @@ -100,7 +100,7 @@ static void find_active_keybindings(df::viewscreen *screen) { auto list = Core::getInstance().ListKeyBindings(sym); for (auto invoke_cmd = list.begin(); invoke_cmd != list.end(); invoke_cmd++) { if (invoke_cmd->find(":") == string::npos) { - add_binding_if_valid(sym, *invoke_cmd, screen); + add_binding_if_valid(sym, *invoke_cmd, screen, filtermenu); } else { vector tokens; @@ -108,7 +108,7 @@ static void find_active_keybindings(df::viewscreen *screen) { string focus = tokens[0].substr(1); if (prefix_matches(focus, current_focus)) { auto cmdline = trim(tokens[1]); - add_binding_if_valid(sym, cmdline, screen); + add_binding_if_valid(sym, cmdline, screen, filtermenu); } } } @@ -121,7 +121,7 @@ static void find_active_keybindings(df::viewscreen *screen) { } static int getHotkeys(lua_State *L) { - find_active_keybindings(Gui::getCurViewscreen(true)); + find_active_keybindings(Gui::getCurViewscreen(true), true); Lua::PushVector(L, sorted_keys); Lua::Push(L, current_bindings); return 2; @@ -137,7 +137,7 @@ static void list(color_ostream &out) { DEBUG(log).print("listing active hotkeys\n"); bool was_valid = valid; if (!valid) - find_active_keybindings(Gui::getCurViewscreen(true)); + find_active_keybindings(Gui::getCurViewscreen(true), false); out.print("Valid keybindings for the current screen (%s)\n", current_focus.c_str()); From aecc190b74f492794bff39113db62c67a22aa211 Mon Sep 17 00:00:00 2001 From: myk002 Date: Fri, 11 Nov 2022 18:04:42 -0800 Subject: [PATCH 15/18] update hotkeys docs --- docs/images/hotkeys.png | Bin 32376 -> 0 bytes docs/plugins/hotkeys.rst | 22 ++++++++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) delete mode 100644 docs/images/hotkeys.png diff --git a/docs/images/hotkeys.png b/docs/images/hotkeys.png deleted file mode 100644 index 524ce9a52f45de4e5dbdc7e0a9e0cf708ee928b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32376 zcmXVX1ytPL)AqKsNRbvP6nA%bC=_>F+zKr2?pxfU?cxr_Ve!SKxI=MY+}#~Me*gEo zIXO9*OeV?Pu`{_(go=_h`Uip!00018Rz^Y%06@?M0N#L+-@Z~-L|8$uhj$iYiedmj zRUGQ$_xG>oWTrA|iU5E&4FK>X7y!6`CH>e10NmIBfPG^CKp+_az;jG*QWbi=fM_Bw zEdhA>Z_92iguGHv9A$J|004}x|MoY14n<}FfS(J%6@!_$}ZGXvc z#?Yr6wyo=Ic`#=&4eq@fc*N(0Kg&pkPRM-J{bijCT2xRiTBn1 zbV!pR>+)G=g%`XKylrvp=#uVFj378p!oFyBFymYFB&K&FW9uks^P*c7H=}2Xa5VqV zh{xNJ$Nvk@`N_WN^70;+uiG0yJz2|S+*#_8)4}HoL7|J=4?>Md(;VA5!e5rR?ukkG zCxCtTzv8?um0vDJUrJw|jPzKNCW-5tX7~LtX&u0eiy}KNA`kahe=_s|RUH<7GZxHA zTZ&D|1d*LDOJh$CYA{Q> z&Og1PxVFo$P%QR;a=P)}3g{)L4lHiWe93Cv4-d$x_eRwV%w1^Of_mKi3>P`T z2Ol%H-`CyQc|0eHdY%NwZcY!@&qu^P?e?{85Vsrt_L&9W8)V|d+LXZJ?snt6cLd=% zhbvctFJ4F{adFfrqH7x@ugZI#6?v8gZwG{x`*rd9PsWsLwLNfGP&c`+b-o9?y1HIT z-Bl+&W&G_~1V8ceG<2syl8kVtjb_rdOGjhRwh>zFF5uz5`o-_ELPjAU~A4Wm2U!e5R{4bsfnT4?-r7Uqc{$ z{D&lRo1^@+sq8&J^gL?w0za?-?G07?c5a9?B^y0nc|32^%zCiAOG$}HnK${!4soO? z?^~+Vt{_^n^G=9i3ayjc;Hecioh!Y};5SX6d!G;!AI_vLF~T(1u^?_)0!637bCK?b zQa&^BDq}1lKCm`fJANg`K<$QS1;y?GQ42xWq5GuHp8%B(&W;)YG(T$?UAkev!HI+N zqvC#hK{0v6EXNmf2gQTV@~=d{VoXDo)Tgt_gFg+6nY&Pje__ zQIp*4?pXfIE-4{@r`xsMUN^TWg=4LPZe5atCUL3P{&f86jg!YMu6MYV(c26U`4I3Iue-!B=gvp{aN{lsUPoMgHHN5_BQl&Fn3C^ERa$#3Q=rm4@wbbxI(&8VC3E+(=@u zuFxl&gF08&JK3rkdJax*?g>8kLbv3-g=s?R*~0agZM?^jMT`Lf3xk1u(h?tAwdcHg zQm@Tn$ZP!#nbVF5MCJR4T|!ka{CSA z!L5Cxz}m2eyqNiSOibKFxwnygnC8Fokf`xLDJvi!GiW=KePKy`?w4V_$SM&4#srG~ z{LPME<9JC`YV`z*B;8WLbeH^IzCII&SBnxw23z{Kk;}G{6I-Cggy8qufFh>NTI zF|X&fp7Q>(jF6Ojwlp4uncPHseZ2x^|2>yX#-(j#o;|s9K9(?V61{uC(gh>fpT5DXEynh(Pryb0hWf zY%z^*_){(_<>iVdK=z3yF+!X`)0vJvyaRy*PI5kT451D2Pr9!TVbSE+TvS0E*}2`8 znug%gk%XiFTXCFDCS9J6r)B_8b^eOl_&FDzLiNx<`ED_{l1Br}LmG)N@rUfFc~}p# zjWM&H%ljFgNn#s#sG?q|7R*5c2``e>Z}j5N^TT|W|2J=9*lI_J$w5>JyFtma+YU9w zxv7nQi{omO(#>#=QQy1XntHVT^e7XyhtKc7Kekt6X8ypfg^dl}uC$^tWXMCSNz$Uc zymHad$SbQ81BAmqG5H)Cb9^9b7aeD_S5=*eS9OGaTKY%f=}X=K`ykYoENG$osS^)C z*GU}Ssrtlg$#fXmsf#sD)6nbKZ}p?p4T8_>fYBa~1&z&#WGxwSPn<#}gpPYHGYun< zK~tJ#hjHWn(qVf+sZ(b1slYDyd_?=B2%3LKe-B$X$Rai^u?uyt*&#W($YO5mo_5-W z5n~_Hbj7$OMh|va2w&i5op#fr6U)0vK8=TwgbTY}YoH7C%}K|sK92^by*mNKTX{c=Q3N89B&A{If9 zA_L(&?Y={JNB)ld%^QGTY10PS*Yu;apupI<=)tFoZD#qw%HNT;wh-1~6IY$u(|n$y z*1QQ{zl-lLlI3)B!^VX~rNG+eO4Mi_{rE_pQN%+x5}Dr9?RR^tsgP z?q7_50@Nmff~)$!X#}tL8R8{9j#T(rO$0V`f?|g{1W#Hnr7avYjyRURodos?!lho=eRz@^4IJGsn}{%p$HDekAhoo}d0qo6>EcL4LwH1ZFP&_~_T1)Q>^5EPz; zDCjv&TzhsoZkZt)p;K!IUl8>L{l^a-yXjnYV3krijy#SGT%U9J#8Du&Rumsy1tdLGJ+de5%TDLfm{qO7t)+tWH)y zA0HWFz-BF0BHTOeej(eJZf0_w%f6@qwoDidiZ8Pgx(3p@Z4Jl8lkugXj7%xSC@{Oy zl40kc(|!~_m4R#MlHBAG27Q^%exE9&9&};=-uy3oBCI?_)<%%t!nJ%S>*3O7;|W4} z-kyURI%(AcS(gY&Dfu>kpE?ps4@X7lR3Gv{~!l_iFmuWp#Z!8qeb3@U#CS~JfM^UY|rSPb1TqJ%? zGxFzdL$0Iv1(g27MSZHEH{+FpJY}89hhjOtblN|%&G^zI3u@XZeTlebQ9x~Bc^K;( z!3e7f-%p%!{;R`tgN>ulr(vQk`$UeWDP{MGr6E8u^y8tn+4gHthPeRnd|^K{jr321 zI($}QmbB5ywbl2@#t3)ur7o$J$Nv7R!f(F5^$;e(i?PUJs+$8XI5jP1Ndl4O#(54u z2H1Qw(lKQ2!b})?^d`#-YnBud(8})xqcKRpSxxhoP~Dq`0R5yyd&PowoatX+z)n(v@30Qf2UE)R-T) z@XgT5V{caR%`x%HInVvz%N;uS4rQJ?je}5%VRSY@>VIu|=jY&c=J$exId1W&ymB_J+@y^vRwpD>bMO5cIY4PJ zSWB1bq=dl0do5(KKbro%trPI7X8 zr-3qC+wOX_0CQutIrc(k#t$V>)lS^HU%`PdaV+sSOV19?`p(L)OSL}iT24M1oi@$-P4s*cFD4ii_+1%WQPf5QbCk!d4nk~Wros5w zi|iguHv6V+Ppx2kymBFT+o(z*|I1N7>+m<-s0YQE={qYQMP&_4CG!mjy>q+Lj?rG8 zRN#9{yrVT@;)!mIIYd*B_S(;OQxp!a8c&ay^?!e4wx|D-Y%$bYT==lcXA(UtAh_IJ z2J!JUyjnV5;5VQ6>1*K$(ru1?CcMvfV+v`cJ8U0K2PG~m#^}kH`E+eCKx06>-?n{4 znQu%A>l-jOEeZzb`x@1ig}@v2E#A2m`j{6pjy|gqG$cyYkJfHIH3%<9D`&#k=%W~q zU$0>LU!WeMGT&Vr=+c{q_CsiT`O%`{R32|Nqh^sn_g5`!Rhz_F2C2b6Q1Bmgk<{ z$?ckjpV>3pY_h~b1tM*!n{RV0ovrY}CiVgU2&96-zSL6BrZBX)Jz!oP_lhZy{f&xU z=ZR$+oiO#*KzY`*Nr}SfcGQ&v_Dq!hY6PQTl8#;RYbD1+0%S-`TTYv)OSc^DLa?XZ z|8cP{mu_JIV>r+x&bRHgL9fpsLQJ<$)~PPI*XyLb3A-9hb)bcXPT?u@jXq$RD75I@ zsF!l*K;Z)eeV;|DlKJ#p><*4Ap%;?)D|OJOJg)>2BF`Ud$Az>u4GRq%q(v_N`&AXZ z>Id^E^XFb3cpMdL{<~04{gdSLw|!k>wabz2dlhTWZmqg*rEq1M!b3cWOJ}zd)#o2F z5B%+XHYpUn?hSUeGBS+sVj^}=>w+@5?cJvL7MN(5s^|UE?mCI_SeM3i6Is{LD~beA zjT1gL)da>hDW+?9|5YoOrJw*eTuSamPJGU1P0Uz4gPeF%VEX+yj{7a7YfXJ+IdRC; z{rLQHDb@6lpOk#!IhB=j;xp?29`eb2<~Mpm^B$9jKam3qB-zxnA+&eji=&9NYff9i@s;sM}4{Q%MgD zjE)FVqua8COWlh!c?dlek5*_d`$pB!X^6w|wXr zACe+V?R5TDYNnog!m($FojVz|tjrz;bc#ttozJ^vsPD?AObI(4fCaT3O&=ZYOdUI` zCu9gFMOhg@nndgp52mhHMO_Pt_CJ?nJIfKDX4%;#tBK!-hD%i~weL9P z+0z7fwl9JRqbQH@Nd!32o9c7RuVdeeHKgJngx!bD?&=-SFProV*SFfP^jv&nXOzfG z9!Uwg$oOuKImXnvo?gmW1uIC$xd=&1fG4$S3mDbeqf5Q^jFLe18{2~%bZ!bpWuW2L z+;4PM!uxdRaGx{QAYmT&GPUz>$ba`C^KPBAoZ-0>gS zh`8;zd0wo?^fRol$#Wa4o@tbQI&)IKlk>h}1V7Wi>_(ADyKeEmY>oYMF}m}tMyjcr z@-{8S{AJ+XX#w8;=sTt?dJy(Jqt3eZamT%{Q20dpq=mm2(gV~Zx>!5QYj!xYDpOqVrg~D<(09=tABa3j@-O+E15TnOw$jFyS@_|+ij01?!%--w<%vPl}T@RcOKDN|{Kh~T{p(c2g!ExY4VqpMz4FQF8AY`-P- zw#Z%KtQ#i}j>$in5|;-?cg1nE1%2SN8{eUit(y+Mz@w6cg>?D^Ag2M|_!uEJ2Djne zzDwK#qv+%M%FDf$D62q5)~jtkuJ^GU47`2R7CAn4c)D1*kF2=gYc{&Sudw!Q_1&@X zz2jYeswVY3LpA-T{E|59dz8_B4Xb!u2@l_$G`bZwdScgtKVh$2e>COjc{v$sJD^Vr zWT;`ouc>0QBDxCE$bjq;n3>Xmwl+P@iZf!ms)Fg`yqU+CSY47331G<|#-k+|m1}?& zJP9ay8kB6!yv5ujX(=Im6^7@n?3A3;bxa8Ask1;EMtgIJbmOFp7&SS~oo^NcOkG3A zs}*;2XLK%{znb>_Buna6*6=lpJjT?l74e0!V8|!@;t~nT{(1BBd7&9?^u@&^`kV%& z_4J!juN_n;rF1-}(Z*dlnM_E;&S)Dbd^QR&WM_L-W90v&qdeiVBf2lL#Y3lviRHnyc2T;3#VVTdScYD zfeCSsF@AqTNk*kipKPS9A4Qw&VFK&1i(MK6GSE9>4MlFaK2Y(`Z&jc=dK1&Pe`-N2 zLmclo(LskK+^Ay3_cGV|(1B!-qE5yjQPPTMgI#wqeA;?tJmh zK3_7=bT}fh-BmrVi76=qXwYVjZJyvbc`uubQ7!{@nf)=6yBtbPeefk(oz$^BY4Nq> z^yK{JVLxV%j|?67_z$-r@kaAu|1f>2O=ZlX11J^*YT7 zyQpt_tngoZIVsINFcoez;jtkKH7mJ>-?YA*>%A;z52#9T1)EL0wvv2VrIQ?ikZu=X zWio$=l&qb!F$E-^t%ww<9UhVk1(vRHM-?I`pHMR%B5;iWt^P-E{t$3zK0`Q|u^7BE9Pl z0dcmYvfeZWYEu|miEgNIcegb`NF5usmKg)p(E_9=7kQM4KvhZSn#{ID4EPN>fZKAZ zB@%O*fDNC#^xstSYdoV(M9k8SRNZ*tKqn8*7K06b`f1fO`ZWu&^f0xG3jPiDXVA7J z!Mw1{*nKPR^8n8Godp{sZXIVQPGEwr^Y-t;HiFcb1TI6oln~9|=CP6ezUyygw1@U1fW)}1R<6esO7{A<^-sq!E~0z#b_-TI z7AZA5;>UYDgdB0dU{g|ce-<(cQx4MLf1U^LS{a#tP=eYgpSjm%YSJXH(;@g&0cL+-U4LWTvz6@&eXC6=w`mV1qLYQD-qvS_lPJxd`^m#G|gr>ek(`HAc zDpiXk{i3G!>q0KJG%)%DIMXV_2>(!QhC^hEW7KZ#zjA-gIc*6~kigln2u zv<8N1Kw=na+p{!j%gflyorPhG5o|-6O0j0eJ*8sVYtKr)P*H3!_2KDVCAXx z#d~YbX1ILE4)-J-JF#=@w-1})Nc#(1)O!~E>_-6v#(Z<9yYPue*21XhT=74}gYiurs%ggFG5(8aJgk>SreVv4Ff{Lh>wOTGeA9Wsq6R+U24A8CVm)HIW>nvB2x6 zRLm7I9lgQlU`;-l$kF{z)Q(%Bo5fp3Qn81hf+8Y+PXpMa+g6@vRU>B=FZFJSV!!BF zkT~4De$~4SMQl(dbU|OHqc&9pi#yzfMc2kgbqOF>jq1~d^-$B^j@&U$L3BuWhV`)5&{WrD#uTp1ph3$VRdqpM%J_$J9OILbU124 zKPAhWwo?4&iYeI-xD|wWBf#6rFx2u9Wi_T1Gz2X;IY$_Z8H$TOVQMW*%BuGW`j45g zMqGqs5#!V}W)a`!hU@<0A5-!iMzG{@^e8FFlQ0s3(He|r$5h(5l}ep4d2-d8bL*;y z;(V{(rdiHyEVI?gu{MAwi3N3dHh!t4;+}FI8BhJyZ-;t0MeOyJHnu;ZJhlM-(x|G! zt$oD#k7}>!7xF|=RST!!jJ*wMSqZl+UxTW1<8(?RGgNP8uva0uY_@a$TgDKMH7!;H zbJRiNM1q4;a8X}`?oa{Q&$ZK`vnlDq)QHI$Bb#o+3^!MAl?rHQl_qQ2krMhMO1QWl zh@L2|f9ft9Ms4(LF{o+mWy|1lBPp1sz{rjcj3}!K=2o=ZdJuj5hd}CI^tq9bjIUXF zB8WWBj}=GRb2yB4=Y&k)bS@{@rY$3+jLj!@Fz3|HBWA?B1{IH03RQ=0sFG<&MRPo` z1UEW3D%WxJ&O{R@bH8t7@Ugdy{e8`Ej)-4QcAjPEzE?ftx4-D+qx-t`2U@%ts`A+c zCVO!lS{sP%?ta@)Dgn`@<5N;#P460IiTV|sctTU(bEAuWL$)U8a%MvpNwnWu@19GS zUHYbKLZkX&SfWICo=XrEwWwLN;LKxOP?El%y1cN_W_A9OiWworPuhTsqM-~b3Dgep z8FM(AnzS3i6xSK#IMy2Irq)Pu8MMGB`!?BoISc zc;`R7jX}_=t_Ar+r5aL-5$Waa!7PR*YK&qLs%UWj9UFD?4@QNDHlKXGwUL7iVk3 zi19y?Ac8?)J{wf$;oLAG|n~1!g`D6WD{aznA6U+8&mHIi?4Z= zE)}kTxrF70+Ike#T;qutDGysxsT5;2IH>S!xhnzg;=-|u1^SV+D-*aF)=nydb)000 zX4r|2)gVP#LD>~`$3Ui<9()}wjA4oyw`e;apIStGIup|Jm&8MX%!ZL>tokR^y+@%r8#XPbs z8#swf(`H1mi&k-sgnla=NhzqYH6MP-Q?jd9?Ve)M9;SO3w%ojya|X`QasrF3&2c?< zW16z=?3{Qzd%Z(H8?Y^h2dFbbNMw$7H8>~##w$5bYjY?|zrmf>UQ*uab`r+Xob~Yg z6&{(Y3zRNMDcNHXTVdESGZ&L%=Fh9}POZlmESd;}<4LM|?tIuumx$3G&c(f~X}qZM z){TghO1=N$B43?CU(BO4ful&yvW=MSu9;Icp35!$r>Y5-?ias%~*MLW)Iu4 zAp>coc}bBbnohWJeiId_S|%-TcwM>Q}bsRsw~m}GEJf-wg4wwtSe<4dKmtn4^thFa2= zpZ;77H&kKk3YU^G`Xoh%Ez$9eP9%aNpa*z+^b-JPS74`tz8d*%~O5=wurF}Z;=moZU|Xg2D|(-$PEVYU0azyaB@p1DD& zmS0^4vIKuJqKi_Hx+-lhMVXx2VYa+!i_POWy--D3Fhv@n+5;NFB=)a-eCc=t4%nMN zAmVsBqDU)V46euZMwb$uoQy?tQsZ5&0fY zlq<)RT4}c3HZw-Pwd3o;pTKwutmK0ct0SaT9CG`HOH(1dc=*Ao}Rflu^JiOHR z8iIq2p!G~XVqQn=fIWMZ@K zJ%JM^FcI!-gi&8uvo()zeV{{Yp}!(bBuy>v&pOStt^;JYEHdYi|B1t;z8A-JqM@S$ z9O}tM18K7iIqM{{2bOaI-D5MbRSQelwZh-=DD`4etCt^APj`c?dxp5n+r2fTe)TwZ zSpq7a@-=l}2Yp5E=WI~h(1p>jkYh9_D&&r2i0W6u3m{C%4OIBA-21jnDed)dhz%-#oj;2E)VVT09lB~P<|OmvIXwa4h+@*MiLJWxKHxz;iNE_XB5bzMzxzQt_F zd{$jBHEF*kO)hnW6f5R9%w6tuQ@Q^V+~npuB7Ri__tSMK$NIjn^f{cT4nCr}Zt!n? z$h>KvLY2MHGecP-|da`x+EUiObI|+wq-M> zm0hO0v_EK7`_?%K&Aai|(mdCZ;x^F%O?hFEt4%Al25iZ!4{X~QlZlee_a?RN4a@H$ zuUrFZ%uAg?ulad6vIrIRq!rJ6lEb<~g{A9vpry965X3r9*N9^&(sj)DU81>+n@Q0h zS712{*lu`!ZxPfe&#Zi`qf9eHqpEU53nl)eppa|A6hFddpUFjcF^;wqyB0vTq%Rso z6J_O>c;SO=V{lh_v@esSxyWmGN6BMBT@qgHlL&iDa3TGEfB5=y;_VLwsu8W+i^|UV zNeyqb(mpyMM7OXIuc})$-+!{pAeGoer+FlVahQgv6}h*pGk3|7#fE{88_%Ojt~dcF zwnim*ZjGLsA?+7R3Z(^mp}4Pe$;pppUq}Wrf;v!BU3JDxZh+!L9-*B4k^UI8R#k{# zUzE+KUw|7=cfL_w=$nXPWQpunqH4K@Pc*I-{k`?!h5(H~mdSEtp3oavOFd<-D#hrK zZ|ot^%FexHaV2WtfrAb6hljJ^>8qKkQ@I+Kl24t3;{EgLwvj)|rCZHuG$&cOr~l5K z6Au|$m{MDm@Cr6{kcrl`6_3rJo)C%zl;S)@PPT=v(o$TUxf>=@vqwc%2Je>0vD|#Y zRSQ+op&>Zxuw@De(r^)oNH$^8nm%AhhkK3(&vXkEFyK@QzuWN=QzJ=@PYE&M>5j*s zk*fK!g@2X$*QlCrLz2(&G6*<%NHP=HKhKO-L8^DNzRUN8luD@sHA+=ejV3+iw72MiNx6ReXx^hLk-q0$++ zNCg%`Wmx3cPD)33iIA$wJ*z(8dI?`bItn3;ISO^s`hyj+XTZN3j}& z%EC#Vg~uxiN*p)C2P>{pakt%Ugk_5=nq@SSeJgRA5VW0){HYn)X~k&DjFcoVmr|S} zdcrDn(Yf~Bd{%2DBle28{CcGO?u*W$Rz=OZ1?+nAwp+j4OmA4^uYstx;Er`ZZ}q12 zRC!cdGc8hXLg^BCU?V*=h{j1OwN)E^a|E4%@oGE<|0o2D7f=71%3ATOB;1JScWiyq z@*kz*WjT2o9l6MyBEMWAK?TkU?k?lZP+jJ`Uup9(US*ncpJfw$uwV(h$xU9qdT->G zq6${m{?+?(c#UCc?@a!5=apdv%QI5z^$Dy*7!JC$fOKlVnaaQf$U%nqG+FKQ@uHFH z5eMf~R}z~zVFN_yg8>YUnxWOvIrk+RvwtEa>lq=+dL!agu5<{d!UWP2-AFO=0*GDm01^+;JS+l*1P(a6o$p&Z+Im1)4%5__1qSV z=CyK^stIih3a{j@RoggpI3djMqo*`4hA)$sr3KVo7PP9}CsVlw5Yp&HCQxfF@uR{< zZ7G}bN2w*IGJXkXylZno9kiD)gQ5ha`*<`Nt=!Y-DK=YeX^nANx_=`|cJHkHF1fp8 zq~PT#S2yM*z12umtnOQo@k=o6dxcJ|0wgn4NdpZov2$_XGu6k~#0drB#ColN6LK6o z5G6w+ED5EkSWHWo``*)c+~wAk66Z5mZrFT%(SH7Nne<>;qr;LkUH^4+KI@?jd@=Mt zc!k5lcM{-y*q|Z~gj?%I$!Z`^7sI``ATthh`Yw8lX!ZC8)~1-}%31tVD&bFDk%7## zx7Lixo0D4Mq$X5eRiI?7#8lWGg?jHX2)ti8!|oO zFXRe`VW2~74aLfNAw;%{IVAngLOeM&FJIW{kS@w9j(l7>k$wt3q~ssjiFTy8K>}T{ zdF3+%h+GIyuE&KW?j%HkDH8AZdPX(&tXu2r+X8bP5G#j?Xl?l?R{A4YEq!v{C zo8Wy+4c`33VQyU;iL7`(G3=*#@KnKp`}$HcezRKe2zDe-gcaB1x`xhDBQw&0{mD$P zohaw!dVMu)LS3>{TmjIBhB>D})F$MW&=cP4n z3Nb{7OPwe`(guv~fDGVU9IPl9^y%F0HaKTQ-(nosxuCE=UYRnX|YzXs4zagz@HD)LGyj@ zi_PsB_+l{-0O{DEluLbSd;1*49OO3u3tcqL_ge-~bjE2~DYGf40|qxTU{3U&WxFji zOJi52#10~se7tlAxJY>M^Kd;07&;k?9nkgx++XATfqRa z2ilZ>1OP1LF}zRG(=7OQS}qI~fl#y@40mzu9@%XSo;nl z)@Q+G-$26;Q&Adv1<}0@#~~-rwa&-Y({gE18tT*(QX(QE!_yM$5|9qZFHFoTL}35_ z7=QRdg77|KV4QegO6Ls+T?7Sn$zl91My!^jO-(^mpnjci0AP-B8!7DKt|P;PL?y$- zTcjo^=$o=xK?z7^uXtn%?G<bj>%hv?0P)60=V&UdZKKD)ycK)`0 z@JX=0dIoixeN|1DUOmEJ{=64%qq~Fwt-m*@0KjM{164_S&sDJLfr6k&oxWhi8g1KL zgh|(rcYo;_WqkKuy|sX{E5bZH!n6J?h_~l&UFpd8?*G2??=z;wPmK$iB@3eMxu_?m#8KUYRd?LM zk3N3w#VE8>tAnPb^G(E-VvhC+E84q9Qg1?TN!|ed((iSYF~~BgQ7jn_Hp>WbdMdAc zpZgSw4j3&XTDrR-eiex^O*Ts{OU0<5Mw=Sp{;lQ>!us(m!{57K5wCAKT0j&J7bmBo zN&Et&Q|Rm-?!yf+ooS98G2ZdtoFqLpe*eaJT`D5o@`DKXf*)TD^%xP%M6HA$%c5h#?OZk|sm25ShvYc|s zDIh|zh8_iY49;3@XPRNdcAMQ4#yVaHf0EyUZ~M&ShmF#A06?KasZ0hR?ypWf35fy; zDhdi`moSJPh)=>D4dCuZ`ojYMHQtH6B~b0J&6eWd?dGSw@Dh_SH#4VLPZtF7dzQw% z;y`_0KX5hrkA$4Y%mf?r*{I;oQb&iFZ(o0l2mAqA?|OF*qG;O5^tnf+%&4!ke7rq6 zuC z8wZ&+E8E#Bg5&4`N1mB+{w(Vdnl-$a$wBZGtJ6iYu`G0RZEXoy1m6y)b~HD0nGPyCZb3$=_2K$je@kg z7R2oAy9$Rk1ysTt2eB@kFEtx0>6Y!2+uiqi{SA8?`+^p;^T*oY>yH-A&qj?ej1Mk; ztk&U5bGv+&)&gO2 zPU39}uJkdB`#cjAL7H&()`mT=^trKTcR^mA< z)0M0y=k+G2#l??qZ4vt6d2gCIH!EsHO%LMspo)uor9Wfp1RGvV@2#RX47>~r;!44W znwcH~nQmT_{)?9aDaT{Px_xi>^3z;~Ili#^^H5-$luxK&bE0xW==ZL@kZ2|T7I0wF z9!m6V?NyfA*;#8W9$10w?*^SyW!huo;2+$UmoY-|mNRsK?l8?hD=b zjUZR>F_Vqnkdjx@s5sfi{qd;5Ue4tFvC_iI)I!t66Ml;l4^}*2DH0hQ;-&kEHA0TV z^jutwtEiX-H)?C#xluoHNR7KFbaIwa<)>-qeX_7is_rRMR-Y2sev16gDc_xDII$tq zft*~BoN}UhaUdfh(qi(uZ)S|QYScZhOSZm!ySS15$v6Gr&5s)A14>i1ocureg_M1$ z1%LC@LaWi%dSiNG{`?{2#hrB^3dsgFn(`;d#RgF_+VQ!qrbokWrlJQ@{au$OM3wvh zaN9Kr3dIE-*}6{D|MW18h(x^k-_t}L@Q$2tdRM~**}Y6ukfPuJy@E;hrSr5tYirEo zrkejtg^jvfIhTy!>RR77AdU?UUL$$9HDz?*v)K@A)*aV#S3ndWuzzrZyMQ#G8F~^G zH9$$>i*KfBxuXp^fdBH>V&-+_OjUIXWe_QD&9T{xd6W;xB zp&(^vJ9+Zz&_Oh|p}Ab-ZMi&Y*={y;bGt)cr0B(f^Y~n@+%MqWI_rdTEH${tlf(~l z#1Bz>Uld^b{p#;T_-aJ0BK#RZTy3%Qo>~FGw^w03Ecm*7!>rp}LQ3SdJ#Et4U5u(OG^aVx5(G>FuEnq-VB{xziYUEN})bQ9&F7W z#m=@O9tm;R+VC`bcufQVKI-J~`37T#aPHAL3Uks8_i1sQ7zqVexrhXn8c)GBp9yZ4 z2f~cVE=OYp2PuIjblsOdz)(Q6r`&7520CE7a!- zg;4a572AX)cGP!h%^W+&MCS6Xo=|=*J2iYXe?Gv&J3buK7W%qlc2zIp1V>t&pK3@h zjL1})K8{MRfDSn}oX?^akpm!uMJyZorvy5zI-^ZF= zc7yEul6_4Gh2eY0OYhI)_s4I_L*4s)p69*idCqyBeF}cqodXN!z?TStH*q2Dk?k9~ z2a5UB819U?uQ=>8BNsO9wo;D?njHg*CT~d?RkV$#%X0y90TedN zK~AD4leBe@1g#&pE9OvKJ(MG*ny$v9>iO@}caX;0uPCPnn>g{R+l`iq9e-t2^3naU zcWvZ7)Ws8>t=yA10^u;cepo^+))~73@{V_5p?xRT=mVDDQT$k|?#}1Dn`=Zo%-w*R z3he++=BBf*=3msbdZV%;m1`U#PQ%{?%G=Z#biAW;f7iKc*wM(70vZxz)$RTBz6C|{ zq}y$VZ4S^{5+-AK=3z*;fc&d`pO?i4VbOx@);R(jq;*uWHen zf_j-=I++6=JeHHS#-lm#x&u$a84a6St`o;z9H2oEINcKCEPT>j@c1dujnKq(k8Ijv zc$inOVf(6$k7DmaX^+`ZQcE&nWd=e3Pz{X$`#S!&;cY<>NJxkpv?E7W#3$@!qg;hD3J=K!l5GSWBOMt`&1fS$hSm)x`61laB9ULazR>kfb)QDRlKc z(U~7+6;cp9)~4O$O7^K~RmzFl=QLWy%n?yMy}_2t#zckpsYh7a?Pem$Ml83t-fYSD zE=2V0v<4PU|1kSKpL9Y3yaZsIP+#9$ls@Zn4jj^~|6>yrnFxor;wa8%Xaj~sgZ6`d zR|LIDY6FH65LD4nA8&u=3;F|ONLGbMy@Wz_6Qs_s@lk$XVnmonRX@5~kB3Q6QoMwa z>u+ggEj17z4dWm3DhIw;1>Iex<$qtF;BX$AZRfE=1*HSdVC7^gsu>$37Fy(Dg zi$n7nIe0Ihg)djrO*8o<0?6DVvQR`I=+6Zr>|8jWmxxAw^ypovAAVKZLC7|cBJ7(U z!>;no$p*`*8JPO_IN_+Vx&)46Y5*Lh2a2ODOU)sdN(@Z{;(p}dCCXO=hkXmB>7|YS z7L+p{{xdzhK5}*rW1gF-uNqnzj36_^u6>K*#Q>3tb7?ZI%$j-vfDRKl|8cfby$)uh z%7B!HBqoF>V#A+h3xzeih8*mwKRK}zudfkg!~rBUA8!dCEc;|XmYiDl$y!-jj|F6| z0~XOyv`GxpaI!9Ao3hw-xnNf;KE@< z7@FXjvY7HgU^qebed9*2?e>FBis^Hg!RJf@wK(8EU@-Sqz4xrdY6`@HCVsIqbf_5a zuW@r+-PtD0a1ininAil53Vvr?AGp1NV>)ia{{)>ZQk+)FoHa#kjun~KZor`i{@;7K zuTqmOgjnAZ!|$Oy=|@9}i)B4)4m(FS-%0LqFdY-Xgp}5fsqY5hCRt3N(AJa0D3lJF z1{CeyF~@30v!n7=eHAwGC4RU2U`&$FQaBVZ7G1Kc1Gl3#fm+OSP3gx_tQu6bNM^nL zz`ntahW1+2HJCg5f)%YB{}^5HgLKmbUbgt;c%|SB_vH>}vC1c1DIu?Nr+dsKEa5m2 zDZYzGVuOf(3H0z-?0&1p%`lI>e;Zs%T+uaG!vX&`Hi!`6mcVghf&L;_gkXc#mcVV)raDE30R#$kf>z zH`!480I61T6K_x3sE~_;Iux{6znaFGD`qlq_dGZ$f4^S3bPu=*0M2xKVl2Sb31Ppx z#7U$MgY{S|%c-eh^JIn7dM-vBs*t}`oybZ|fWV1hFwauSx2lPd5XzQ#Hy3NeMU-a5 zvf705_r*SwZEv0#VN0@ib3juefGk7J5C{;5m72o{6-Wz41~g6KrVVSeD20e$XI|5D zb15*}rLnUF+jP_1*AFGCbXWwfsTLE=+LLp0-3+R_cvE6*Z<% zleL@gY0Yb4eUoN?=27&`mbe-Xnc{)~wU}mtJKK2Ohd_e~COje-JMiEc)>WlcltpNS zvLHHms-Ih9M7F*CELN;06Wc!BjFa>o(t2(A+yl$$(>}zm*;!zFOL{yNR}ix4kdHYt zQ(qE3VhVkSe}`lqqF}gDdL#IXX+;#$c*B{HH?d|afO??#|m=?{E|1o8Ar zkj+UnqDu>Kk})@)pB|FsZNMKGnVqtMD|n|(n$`HwIr$Q$F1_Ak@O_#=Hp@ZB0t!*_ zCG~#N?HqPNJOrofSy@@kvCQvXupE|%2Q{wzC$v&tuMDF^f_xluKxa<=PUg!dQ|xcoq37{Go5!I;Kl&T`tmW{Oc;RJD2%!|Vmk*w0_dp(|NL3RDheQ8d})NW7t^XwtB zIbNSb&vunnzeXm)F(xDF!fV-MrZzGieJ5Gl7ss@|+k1HHpWj~>7O!Yl+$g{OB!}UA z+D#}CytP>wd z`0bvDAdw{ynN0EQdtua|(ofg8Sj=;>8B=Z9jR*bY#oKK;b`){^m4O|JwQjY^KUNhd zy><5afr$j8^mYmcP&BP+312HMR*gGX#Fa5fZ#ii~c58mV?j?z5w8>Gn%8k5uo~(}! zUs}|qZCG$(qZF2#&d*KQD{Eu+8*ESTaK_{AveJN313AURhEA|BSoER+;TQ?yW>1nQ zJ$!#AVpQ@(&I)%^a%gjBsSf?iHyiXu8b8nLGdD89Amdm5^#OsC1YbpH_^$+@?+pHf zcD0+!(UQt+#SjZg@FC=^@_PO5xbr>7t51Nt{JN}AnOVHW)kQ3*zXiMy09uW?I?72| zHHBU7`7pEzL@}~4{5*80x~nsBsp5USDbHB`bW2SC5;*xjI{6R}LqmlEL1JZbX6w2D zwGv6D#UygQ&EO1yC*5bilm7+)z>)W65s5Os=QTW%Ha_X`LBx@xqGYAp$lq=#5OVE5 zXEI1c6@} z(r~Xpm$_~I>xdj*UkxV_OrBF7Iij+ZU}QAJSC>NVB~fb`l$qkSC&Te+Da*LQ-*?r* z>9bCQvmS$=)QkdG|I^0Y!Xul7=i{gjAofA<;x^VE)FIb~xaqD16lLDRz_o79`c8;= z@ss_uY%gIAcT4}qG~-?)lJoD6+%Emil1SdIa-mWV%Qr&xGjV=p4X4|+HDj*b#h8QN zxplc3C-xtY7kB!{UDbU~@eEi}^I&TwYL^=fZs637>%zt+!Ye&!R^OHs6OP8xCeyEK zv>aAOzVTl6dUxNnQbfC`=DcGoHNs=|_(bR#Mos^wS}*{msc#nxWY+?P>i58rmWmsN zx>rW@&;VIe!%YpB2{DWA$0;RKxVaol4g`!zX`14B{KevIqbl46pFc-Zl^lPl)CtZm z+#W5TEnNKB1}klN(xVcevkr9Q-zKD28h+b!{o|23Wlh z-X&VUXVnZuo&ahL0WWF#PNw>Oy)*lB=)8k3!M4UYwrBan`3T?hKRu0$W9KtQfID@z zxj&KzFBS<1L_d(=4u`XY4g-n<5AK|;pC{2YtT_j??Wdk!DYg@63r`HClq1ek%vu9t zQotrkbs&*-c27o@EzGUVYk<~i-P(AS2#}Efm;@7!v{}d^fCg)v>cV`8Do_qwEoMDQ zlc;qZk0Y}H)@R=+X_S=0GBbf5YsTEVFwZ+S0(LZW56s+n@i3@NV3`c8f~52T9bRAh z7U~P&{?tRf8qT!;aT|}=vpl;m#tfjSM1`TWyM7Ny0KF6nBL6W+BOF9@613oxopOdd z9D@gK?&F;qFBmQAv9C;E>J`TEuGe5;iFhH(C)5ey2%ocRm}UHK-w@sW@ur;sGJi2k#9f@6jFf(fZ+qnD6!x62hc?rqhaxnymDq z$wcgiRM*uMunbkvlo>JH_a6loZtBg@324=)f5JubtZ=0ACIg^Hj^6zKtCZsI&heYZ zr`N;`t^&Yb2)%;(nW3dE^y2a*PT)k&aY=0*AgPoEhwQXq&BmZ7Li7NzYB*~b$8H>a zUw&#s7SMc2geUr4g|u{h9AvVy^^4)uz9|~B_dF1E&p zr8k0Vh(ylkmzhTgLQ}j%gxMXx%xssN=!<{?!d9%CuPqTw`qsQvgbsLdnpc;lcoKdo0IE*7{vRqXQ<70hdZ=zpb=;ga(ht~tx<(a%#hpa!-W>aTUPZEIn>Dw|0r`OieS%nH!IZh#3l!1Vh&lV*OyPv`Rp z{f50;J7phnrv~y-{+yYJg9fhH- zo4@wTL;}9=Gh-Qo92@fKz2vtOk!Xp8SaJKrE z;6Yf?rmR0875bna58r?Z1{*00@3UYB!?N=L#}|!})pLpUC6|sPvycDN)Z!U#bm_qe$i>WTx= zHs*-Fem_q_)kmHHn70S&#FJz(#Jt+=%6)lfItWf zb}w{bB~54!eEPi8wR{B%i*Clvm5*+ZH!x;r{ZuZ;TV5|xx|`uZwrB?D+e8S*!f#8N z@5(aAubFVoOeZ8@u%O`am(_p+F4Ns^Z(O?s;8+9Z5X_HSSoba*opwbjAV2Pn2jfZP zBO|6HSuAgr0mCn$=zL)zY%yaQ&Y3L{?1u|b^vyRIPNLrMlt-7TA#{j5yL4`y2@4%z z9lqI`mVRf6@G#xreGT+qvlj(4_T+S)uGWa7|3rykzPfXL=X%g?;W$UCnotl$0o>e}8|dWL5fmt!<&RAJJBj zFcY{=mCO^b)UzDL2OtffApv~+*oe4ncI9oAJ(&DVZ#Hl`)oy!%lX>2@0cdUcyL>%3 z)|#&S&RDUHsvO-b?>@gBAg!SjOQQO;i~Q+jzD)vLO=JkZ5E`18vJcIosmnt`T-PqF zu@?byt)<9*iz}8B;->cUY(j!y1Og#TZ--z1MLn=k1}E>Vmysb=FkyXS-iC!M6bcw` zA$iCP)0AMG1wlk~ks=}W7Ku#gMC|`@kUj!(JIm_4w&_8+xpu^Pc5iv{Vu)azgPekb zf}BtCSbzyAYZ8Y*Bq5Mr%WgnYG=*tE0xBGf?Jm1vjr!kcaz2K;CO%jte_aeva6CW% z%9Qs*Pus%xH^r@=)r^6Evt8oVL?E?)m1w{AZooHgv64T{SiTPcukZehdxJzJqMnr+ z@P1JJyRibvLk=3=pH2l76=d7O{Q-5w+V|l5o5VcZwq-XhUYfsnY4O|pNRF{!ne99= zBsXb%UKk&&%d2wX!v?&AH<+owbEX;Axz;t+UMzXC;Oo}8@A0}KkS+9Brlx!>jlR6D zA>3K8G@f4W5s;n)e72ZyTZ133Xx%fiyBVzL(9C&{LKuI&sewe6WpI|t6nCd=84VsEtrRfrfZn_;_^6?!ZRtcHMIKz zqK&(rxes;84rtOkV5j&0)KQ@j8fbU&rx*L+!n>5_u7Sp8Fg#=duihiNc(_AKr&Y%A zpywb^R0bz-2MjX*`>xucmFM4(NJ#-8#!C&N6M~U`gI_G`$9A=&AlM2pBV40ViNNJc zvb}#@!UvGb%Wkr|bT<~MJZSKvo6bm@{}H<__bn|fQ-z!!PAm(AXz(}8m~MT4fiaLL zW@eayYEg#b^UI&Dba{E$Dg$dlEo{Sp1pp>v)Df?f19>uJ1^?~r>XrN;aeO`(@Np@6>3L!9&DWVhC(nj1 z(b)Hnh85h4i21t#d}5H>(!^r_O^IB(hi=19CU_rwNi3_{>D3k^52`Fy%lom zLn|1T(iuiBhMT=BH?uKxwPv*Xb)i@{)kFo$14ka>?YTk%itfRMYsO4Q!JLR`+%N1(GQLN0#>lKmr(k z(v7H#$!Hm|XHZ1m%g%&E6c(#CaKS7DCs(NjqJG8u2*ZZlYQo7a5ZYw_BbZxqtx%^& ztA;l_1}>?RorqCuzyu66k*AY`d~uZuC5yKv{wqNmx@75MgJp$7To&6S z9)_~UT9aIEn#NMvYs!?i^$5V^`R7ug2DyEs=#+)Qp7PQTh(CkM23tJ12qiTIHe2c_ z1GeIxnXHaN#>G+cqCPfQkevu-kFycjNlU+UzG~m}I{Ss^96eO?r|TCS*mT!Cv86{L z{hjmD$|IpympNd3%W0$17GNr{y(aI)Fap0Ys`~?FyqZ)&e`${NB_}1%1_|4h&3kyn zHPu+yma~2_A#FA&(O~|Mz5Bu#jGxq`DXCCt_C+0eJi7UJZUjU5@~a{fmeo$*2iLzW zQjW^vLA-F6hXeIezBrI)pTtfNUFfS?VtlrK?>iJm2B+fG|oxaV?nM`b*lRtzxc43B6E;F!u&ZcEsfwy8rShl<)0V(rXxG z3EyKhDwqN9U?YGq-4xQl>9#9^V5f2Wh~^kGAgY1KiMg|1$gvkvQNg;{H4Cl#QmdVG zBvG2z!nt6bQoa|=MsWCu{Q509+M4L+YD*}FsNyy=w0VoJrkE^{TnT)(s3SW;jKe{0 zPwU2oT!&xY^i1QwB>M(Hcd3LvxMMyX$c=5~2Dfl?cPd=~EzyU5DB0xM9pE)U0c_yT z4PLmEc_$o1C%k5+HE$FWCd5|Jw4h7rV8*n3kF2Zti!lm`bf4`Ax+KPv5D@%CvuN2Y zNkN;W<=K7cd zc!chxFS&igCX8dT!=0Ts{~r5SGah-w`bJ1;s2!vu@dqEy^Y(ZAu&+!QmNPxU;mlf1L$TC!C6Ucxpjt^cq=zs)Eq+(MMHh-VIvW$%093P(s zXgshP+_DuAbt4GevXH@GMU$TJA_`q|dS}w1_mZa}aJ#kpcQNw6N zoZDYSQ#Y8&ERxcS$d?1$*GoYj6ErTiBXvndCf`vE1OEuSeFKTT9hF^O zAtEwfmvz5;$zK8TNQQxevlvv!V`LqY+7m^f110mxvwZ^qC2mwyr&Za~AZrp9ohU_0XPYl+aXte06k2o{@)l5k(NAF1(Qhhv82QIe{3dpq-2>ZwNS~K!ap8 zG4EXz@_5$ojp@!m_n9o8&ix4&3B+cJ+j5LKSaF=BIv=x&;{`Kh%#9xice#~5f$7A} z>(9@I#Qs&|xcKjwxXo&Ka-4mEFn-Ip}lg79&)0^)(WK~1d9Uop~=U&qs9`+`@wVD)N z)F@Nx)SD#XC3ix>gO@2=inf#({^Z-q$S{#dOxv#%wD{f8aPAgxnbCcb-(NCEj(a#v ze$gJDqtAZtE|&m zrj(;CT7<~>+Wba(Ru6*Z_=nySWQ^mblz z%)Vq1T0iQ}sYb-&Z=`-D=BW&R_4)E;Gg;cVkc=2gF_w-4tOd|@s!xo=^=?=1N0sO* zF>`W;>0jcTJ5S7(vl=iv9avn=X2&*J63*#uOD%lr8UW%@mYm8rLq3aCjTT9cl8j=> zYh_XuPEUYGrh1etR?UD}k3U96h}##DOz2Ro>7On|})i zcEkBB+iTWCaknb%ZI)A~1lZHXwQVy!vxFy-9G$+T;uAlN?28)LHxH`tD(VYQB_j^=LHYVgeH94Hx0HBas^ZPq9JSoN3USb5e3fYPEP~ z;)H{|{o`}8ulPb)J<&atb~ssK5oX%RiN0~!iPs$!+TA}VB`~!u9kum=grAqx@2{0T zmF_G3*EDDvc<#s_cW@HKG1i~nC$>4~So2-#v)6CJl||@oR}FFetV8~6UGrl|{?>J)?M`RXEeoeDp4m`& zuS_p5wgSvd@~$o3_L7J>T>72jdyESLwjK!R;HyyrtSjX= z8E?EK095S^>lCLa8?qxYbU@dcd_}#fiQnBUNoMtT&PJowEgR}cRI$e_bGhNA9EYy6 zQG2s62_uIp=fznU(Z2ccH^v>)A)AgD&F#!&;#S=Mo@+l>1zkmitQy^?~7iW^qQ7b(MS(tTbYM z{6PhQ$@JZ26h&K_Q%|gaSDau4}&sQLq*r) z8~RolO$~>5XAX)}dF-@PJK4OWaq@RYCr$R7-sznArQF9#te>yfy)fe2>8x(n-2N0m z&}2e&CG=6)BmYN5hd1v;%|+A`y}*1WvEjRNB75Cel8poipx6+=v_G4DQhCt(Vtqi^ zY^UAV;~xWkPzFBv>e584yC(-;LZNkmG7#in^Mj->%3T+-=UJ2}{-3=~03pIx#h`OL zm+t*FuNBF!`(}tmvFv%ms4fiycNQW*jpF9g{t!>C#YaK6GWDPKPAj*xL5sdG-T(AY zhg$2m|Lk)=4k9s9x`J3HK-*`gc6f!ChXg08PFJz8)OQD>3ZLMf+i#05uFmTXhl)ve zLsv7n60yiclFLW;pI3lF(MP2&DNzDpf87wYis~sAKYE%oI6FJB;o+fT`;135N3(td z=@+1?G8oDWB%dmg_l0AJv9Su25*N0@`yqihpJj-8t$>lv#id`P%gWj!7<2(=67#cDt-{_A{9S@Hwzg-2 z16W0EH?ZA%uOz1u&?{rx=17Jm&C^(!6N#{N^&!j4>sgL2IMWLi4cB+C8+ zD>Im>JSrFpwaUxN3z=;F?;aab%^Bme+37Zr?HJ1jYD;U*A^JZ*!skHQ#FyWa6Gv8> z*0@9j)%Y<m86bofVu8Vqy6~Jo>@V1Ven<4?&5k%SkpD5xjad_%Zatc~es5R#JT6 z&#l3+knp${`z!rTQQ#=>-C`~?7JX+NPBtl@$4X|`>A zA~F%4l#*vCnkNbi2;YqE83}v7vw;%)(|BOKQSP(#-iXIzwrVnAvU;3HxFdILvv0w( zvNR9(X-elra!|ag?}n#WiSN~Cv)DlmJzV1P!vh1SCYNb;?dfib1+B z)cNrBoml8$S1}X*A~FfRtC&B{TaOSz4-`*zYFj273!i$Y7Y(RDsq9M*3t84NlV{u% z5t)|BcWckxP7caP+9n;{rZR*o@659nWt8ss*oPIyW!Flbk~@59eEQ>Q{KItS=bQLm ztidX+9eyZxxmJg~?#2_p#&e*K+1GTB4d{9?$Z!IJ=0S^p*= z!>bSPC?7a{Y`E9L&A>-siacsaP`G6P?AM>>(|NPNc@0By^1LR)ak{8>0$&TN_}>L! z*5TR2Q=Q$8hZ6rzGolJJXC0;|2kx}_j&RNnWTr5|<;`C(cFP)BJ7h-L5SnO`4_6;ljGWIOY(+*mPEl zRWz%+>O(!q2m6uJj|LPwSy>Z~$?h?9S_tO#)4UGFe3jEsN{hj}5f3^t#2yO*Jd&2Fr-F-F#nh0uTU8ojblHe%P_Kd4({sI_*;O+=Tt$VFi zJa^S3(LPZ?Hl9xkG-ae#V=#>aNOnOV&C>Synck=DhmKfrjvUWDegbp!PRawpg5N$96Q><4EciGb=v^GcFC)nuI{Ft_v&EumfAR!*!i;9r}Xs8rq+kM%RBzDjSsU)q1U2h z{^`+^WM#X4u=A*#^#Q8eEZsPV_aQeAcS6?**yM`_xulrp60&+U=w%M8Ar()2xxo|8 z{cJG{W>B;b!FtU^XY4R1Ig4>Xq1~T^pl>jy{vrQxlp&io3k=N8w}b@zjX{3k^+{M0 zZ*k5q-S<-Z;VMlHI0F&dACkXxW6qYChwc@d=NZRT)h+WNR(8H@7N!2~O7;IS_uJ?w z=(j9FPiV0+>QjJxzG9!5M!3I zh<5_$&*Vpxvw*KevL)!N)1Mu?qWEctJAXtA;gqS^yF@KtAm8N82v{am*zT^GVTO$1&8fwDh20-@K%hAPpDy3kI;5p* zujMUQOBV+t7mHQh9eO(N9paJexkn4WtJQhS8vDbA5mO~@8-~XcK@hmWPXnBS?L>+1 z^iM|?8qnejSJnZBaHz9M?AQ3v@l|*Cuib4yJjn(71W|rHC+V!2N-Zfx;>jH&jX_#` zR%!c^#wjY1qq0IM-bEmk9MT4opdx)K|2~a3BO~|k6b7ktwBbf z9yfFIbl?xOngR&lQ^pXRFdb^;tST-@$@8JST2|EQ31@z8+^&f}i9}HYZ(MSF293j& zcso9o1Nm6n=40=h$P|uO!)bK3_h8y=hzAH=Z0FdVx1;LSk{~r7z2h+%ia$w2xE|_c zpfIm~eF$jAT<`v)nTz5nde_E(GW)FExn4Ezq}sLTds3*5^-3%vrG4!a^iv;-ki-H! z@Z#o?h%Kv*3tkgaPDqS?Y?yxqxmr9F1ToZmUr+h zh1^s)U%$_t*_^-V~4YED$ zCw!KOq!+vG%r+wAc|%7a{{}mj-dPX9EeEr+$MijJc4X5J6N`_p$UP2hE42P@5x?a% zGE?FEQ1v>-v$jXdcl$BAa*>;qRw&H3hR6GS*xMXavXn;=?X3*u3VshkAqpm$$D@Pc zl7>>Td-2;R89>Je3z}`$*c+9TSL-S&Yyv!F|NWq*+wMIQo4>tVLgQZXv|8_#XREnpod4}dpD2UuQ4w!wkf)fNaO)^zW`;!;vkQG#p`d{h#zcC^ zI&+f;9=)IWI9l@K>uIsskn}_lsRYNuXOU$+jS-cHqKiu63&yW0Lf-=|7!Qi3jBn^g zHfmtASuvc6cJt3z*>AB~3om-MyKd&C**y|kiRf_eW9l!sQ&wsiCj4>gw>f*|ctm$= z+?{k+f~B+h>F|-!ATR$Ex7U$eLfkeVbOjl_GAFnlOGzFgO^MBg1Z@-%7Bt9L@KY_j z+9}O}TH0Q}&x01SqLD2Jca|-*>^OyE4S7>AD%Yt>eY`$%p`H5~QR3WJuUo@;X_<2X zZ+k0;vi)3*U*D03j7Lp#hRF|lOBJctD|6{LU&aKy>+;p)=3bJ#rd0G1^zWhOmB^HH(!O=39xS60nSd#v-&WlSS-*BrxAzm`M+`!vL3ie|E@!_#WXBM*ohey$3K(U%9 z%-FOOaodRc&RE%v6%1{(w`lF>%2m^`G2ykjRps9gSTHlrC?J({C_`NalR;q_CgMgd8naW!M<%>`9>b2*2`*_v-=4SNlLHlS4fhKJj$= zb{exy{(%i?;ja4kt~qNwy%u4xcVFI7e1l-|1#IJ@WTQq?1T!3it%O@wbh;o~S%iGw+T2cJ$J7 zL|GNVrs{B1`ZC6|j4@rGXoKxfvMH+6hAS0TcrA01F*^8;iL)+IL0LJv?+40t~Pjj3Ie!WI7L^099y)_Fc}HS9aple{uUZ66a>d~DBc z+gk`GB3h6aj{^UDK5H?cknLk!NP|giW2lH5#)JWy=7fWEa{nq&t|I*IoXZxj4Yz~y z%I8RqTXm~xkv{LA^@d?G&rG#DK8}+YO+x`k0Go$`frpj3hqbt+n>Fx@K#*Sm!ov^Y5#rb77ZDc_ t6&HHQ#m_I!&u`{MWcdG?;OJ~+i}3ybp8(=&T*3Pqlod3fMRI1Z{vVCj0&D;P diff --git a/docs/plugins/hotkeys.rst b/docs/plugins/hotkeys.rst index 6780efca1..6b2ef3f0c 100644 --- a/docs/plugins/hotkeys.rst +++ b/docs/plugins/hotkeys.rst @@ -2,7 +2,7 @@ hotkeys ======= .. dfhack-tool:: - :summary: Show all dfhack keybindings for the current context. + :summary: Show all DFHack keybindings for the current context. :tags: dfhack The command opens an in-game screen showing which DFHack keybindings are active @@ -11,8 +11,22 @@ in the current context. See also `hotkey-notes`. Usage ----- -:: +``hotkeys`` + Show the list of keybindings for the current context in an in-game menu. +``hotkeys list`` + List the keybindings to the console. - hotkeys +Menu overlay widget +------------------- -.. image:: ../images/hotkeys.png +The in-game hotkeys menu is registered with the `overlay` framework and can be +enabled as a hotspot in the upper-left corner of the screen. You can bring up +the menu by hovering the mouse cursor over the hotspot and can select a command +to run from the list by clicking on it with the mouse or by using the keyboard +to select a command with the arrow keys and hitting :kbd:`Enter`. + +A short description of the command will appear in a nearby textbox. If you'd +like to see the full help text for the command or edit the command before +running, you can open it for editing in `gui/launcher` by right clicking on the +command, left clicking on the arrow to the left of the command, or by pressing +the right arrow key while the command is selected. From bdf201c6705619dfc0bfe0769ef119167e7ef726 Mon Sep 17 00:00:00 2001 From: myk002 Date: Fri, 11 Nov 2022 18:05:07 -0800 Subject: [PATCH 16/18] ensure keybinding shows up in hotkeys command not overlay --- data/init/dfhack.keybindings.init | 2 +- plugins/hotkeys.cpp | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/data/init/dfhack.keybindings.init b/data/init/dfhack.keybindings.init index 697641ab1..330260b06 100644 --- a/data/init/dfhack.keybindings.init +++ b/data/init/dfhack.keybindings.init @@ -13,7 +13,7 @@ keybinding add ` gui/launcher keybinding add Ctrl-Shift-D gui/launcher # show hotkey popup menu -keybinding add Ctrl-Shift-C "overlay trigger hotkeys.menu" +keybinding add Ctrl-Shift-C hotkeys # on-screen keyboard keybinding add Ctrl-Shift-K gui/cp437-table diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index 77db827bd..8f980cb2b 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -22,6 +22,7 @@ using std::vector; using namespace DFHack; static const string INVOKE_MENU_COMMAND = "overlay trigger hotkeys.menu"; +static const string INVOKE_HOTKEYS_COMMAND = "hotkeys"; static const std::string MENU_SCREEN_FOCUS_STRING = "dfhack/lua/hotkeys/menu"; static bool valid = false; // whether the following two vars contain valid data @@ -32,8 +33,6 @@ static vector sorted_keys; static bool can_invoke(const string &cmdline, df::viewscreen *screen) { vector cmd_parts; split_string(&cmd_parts, cmdline, " "); - if (toLower(cmd_parts[0]) == "hotkeys") - return false; return Core::getInstance().getPluginManager()->CanInvokeHotkey(cmd_parts[0], screen); } @@ -56,7 +55,8 @@ static void add_binding_if_valid(const string &sym, const string &cmdline, df::v if (!can_invoke(cmdline, screen)) return; - if (filtermenu && cmdline == INVOKE_MENU_COMMAND) { + if (filtermenu && (cmdline == INVOKE_MENU_COMMAND || + cmdline == INVOKE_HOTKEYS_COMMAND)) { DEBUG(log).print("filtering out hotkey menu keybinding\n"); return; } @@ -178,6 +178,7 @@ static command_result hotkeys_cmd(color_ostream &out, vector & paramete return CR_OK; } + // internal command -- intentionally undocumented if (parameters.size() != 2 || parameters[0] != "invoke") return CR_WRONG_USAGE; From 2093287bf009bfc120b00f55a370ae539d368afe Mon Sep 17 00:00:00 2001 From: myk002 Date: Fri, 11 Nov 2022 18:05:26 -0800 Subject: [PATCH 17/18] update changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 4465a0f12..e3a5bb5ba 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -55,6 +55,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - `blueprint`: when splitting output files, number them so they sort into the order you should apply them in - `ls`: indent tag listings and wrap them in the right column for better readability - `ls`: new ``--exclude`` option for hiding matched scripts from the output. this can be especially useful for modders who don't want their mod scripts to be included in ``ls`` output. +- `hotkeys`: hotkey screen has been transformed into an interactive `overlay` widget that you can bring up by moving the mouse cursor over the hotspot (in the upper left corner of the screen by default) - `digtype`: new ``-z`` option for digtype to restrict designations to the current z-level and down - UX: List widgets now have mouse-interactive scrollbars - UX: You can now hold down the mouse button on a scrollbar to make it scroll multiple times. From 6635b6489ba7f156c75ffa80b8ff301f707a2228 Mon Sep 17 00:00:00 2001 From: myk002 Date: Sat, 12 Nov 2022 09:57:32 -0800 Subject: [PATCH 18/18] handle commands like ':lua ' --- plugins/hotkeys.cpp | 4 +++- plugins/lua/hotkeys.lua | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index 8f980cb2b..85c304d82 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -99,7 +99,9 @@ static void find_active_keybindings(df::viewscreen *screen, bool filtermenu) { auto list = Core::getInstance().ListKeyBindings(sym); for (auto invoke_cmd = list.begin(); invoke_cmd != list.end(); invoke_cmd++) { - if (invoke_cmd->find(":") == string::npos) { + string::size_type colon_pos = invoke_cmd->find(":"); + // colons at location 0 are for commands like ":lua" + if (colon_pos == string::npos || colon_pos == 0) { add_binding_if_valid(sym, *invoke_cmd, screen, filtermenu); } else { diff --git a/plugins/lua/hotkeys.lua b/plugins/lua/hotkeys.lua index bd88fdd89..7a1a39115 100644 --- a/plugins/lua/hotkeys.lua +++ b/plugins/lua/hotkeys.lua @@ -182,6 +182,7 @@ end function MenuScreen:onSelect(_, choice) if not choice or #self.subviews == 0 then return end local first_word = choice.command:trim():split(' +')[1] + if first_word:startswith(':') then first_word = first_word:sub(2) end self.subviews.help.text_to_wrap = helpdb.is_entry(first_word) and helpdb.get_entry_short_help(first_word) or 'Command not found' self.subviews.help_panel:updateLayout()