diff --git a/docs/builtins/keybinding.rst b/docs/builtins/keybinding.rst index 3e6970b6fc..e8f206848d 100644 --- a/docs/builtins/keybinding.rst +++ b/docs/builtins/keybinding.rst @@ -9,9 +9,9 @@ Like any other command, it can be used at any time from the console, but bindings are not remembered between runs of the game unless re-created in :file:`dfhack-config/init/dfhack.init`. -Hotkeys can be any combinations of Ctrl/Alt/Shift with A-Z, 0-9, F1-F12, or ` -(the key below the :kbd:`Esc` key on most keyboards). You can also represent -mouse buttons beyond the first three with ``MOUSE4`` through ``MOUSE15``. +Hotkeys can be any combinations of Ctrl/Alt/Super/Shift with any key recognized by SDL. +You can also represent mouse buttons beyond the first three with ``MOUSE4`` +through ``MOUSE15``. Usage ----- @@ -27,12 +27,13 @@ Usage ``keybinding set "" ["" ...]`` Clear, and then add bindings for the specified key. -The ```` parameter above has the following **case-sensitive** syntax:: +The ```` parameter above has the following case-insensitive syntax:: - [Ctrl-][Alt-][Shift-]KEY[@context[|context...]] + [Ctrl-][Alt-][Super-][Shift-]KEY[@context[|context...]] where the ``KEY`` part can be any recognized key and :kbd:`[`:kbd:`]` denote -optional parts. +optional parts. It is important to note that the key is the non-shifted version +of the key. For example ``!`` would be defined as ``Shift-0``. DFHack commands can advertise the contexts in which they can be usefully run. For example, a command that acts on a selected unit can tell `keybinding` that diff --git a/docs/changelog.txt b/docs/changelog.txt index 1348adbb8f..e9b68fb94a 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -66,14 +66,22 @@ Template for new versions: ## Misc Improvements - `createitem`: created items can now be placed onto/into tables, nests, bookcases, display cases, and altars +- `keybinding`: keybinds may now include the super key, and are no longer limited to particular keys ranges of keys, allowing any recognized by SDL. - The ``fpause`` console command can now be used to force world generation to pause (as it did prior to version 50). ## Documentation ## API +- ``Hotkey``: New module for hotkey functionality ## Lua - The ``Lua interactive interpreter`` banner now documents keywords such as ``unit`` and ``item`` which reference the currently-selected object in the DF UI. +- ``dfhack.hotkey.addKeybind``: Creates new keybindings +- ``dfhack.hotkey.removeKeybind``: Removes existing keybindings +- ``dfhack.hotkey.listActiveKeybinds``: Lists all keybinds for the current context +- ``dfhack.hotkey.listAllKeybinds``: Lists all keybinds for all contexts +- ``dfhack.hotkey.requestKeybindingInput``: Requests the next keybind-compatible input is saved +- ``dfhack.hotkey.getKeybindingInput``: Reads the input saved in response to a request. ## Removed diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index cc7a90bb3f..3ba6389088 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1430,6 +1430,53 @@ Job module Returns the job's description, as seen in the Units and Jobs screens. +Hotkey module +------------- + +* ``dfhack.hotkey.addKeybind(keyspec, command)`` + + Creates a new keybind with the provided keyspec (see the `keybinding` documentation + for details on format). + Returns false on failure to create keybind. + +* ``dfhack.hotkey.removeKeybind(keyspec, [match_focus=true, command])`` + + Removes keybinds matching the provided keyspec. + If match_focus is set, the focus portion of the keyspec is matched against. + If command is provided and not an empty string, the command is matched against. + Returns false if no keybinds were removed. + +* ``dfhack.hotkey.listActiveKeybinds()`` + + Returns a list of keybinds active within the current context. + The items are tables with the following attributes: + :spec: The keyspec for the hotkey + :command: The command the hotkey runs when pressed + +* ``dfhack.hotkey.listAllKeybinds()`` + + Returns a list of all keybinds currently registered. + The items are tables with the following attributes: + :spec: The keyspec for the hotkey + :command: The command the hotkey runs when pressed + +* ``dfhack.hotkey.requestKeybindingInput([cancel=false])`` + + Enqueues or cancels a request that the next hotkey-compatible input is saved + and not processed, retrievable with ``dfhack.hotkey.getKeybindingInput()``. + If cancel is true, any current request is cancelled. + +* ``dfhack.hotkey.getKeybindingInput()`` + + Reads the latest saved keybind input that was requested. + Returns a keyspec string for the input, or nil if no input has been saved. + +* ``dfhack.hotkey.isDisruptiveKeybind(keyspec)`` + + Determines if the provided keyspec could be disruptive to the game experience. + This includes the majority of standard characters and special keys such as escape, + backspace, and return when lacking modifiers other than Shift. + Units module ------------ @@ -3494,7 +3541,7 @@ and are only documented here for completeness: * ``dfhack.internal.getModifiers()`` Returns the state of the keyboard modifier keys in a table of string -> - boolean. The keys are ``ctrl``, ``shift``, and ``alt``. + boolean. The keys are ``ctrl``, ``shift``, ``super``, and ``alt``. * ``dfhack.internal.getSuppressDuplicateKeyboardEvents()`` * ``dfhack.internal.setSuppressDuplicateKeyboardEvents(suppress)`` diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index d10978e97b..eebc0df8e9 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -160,6 +160,7 @@ set(MODULE_HEADERS include/modules/Graphic.h include/modules/Gui.h include/modules/GuiHooks.h + include/modules/Hotkey.h include/modules/Items.h include/modules/Job.h include/modules/Kitchen.h @@ -190,6 +191,7 @@ set(MODULE_SOURCES modules/Filesystem.cpp modules/Graphic.cpp modules/Gui.cpp + modules/Hotkey.cpp modules/Items.cpp modules/Job.cpp modules/Kitchen.cpp diff --git a/library/Commands.cpp b/library/Commands.cpp index 796a1f2701..289219c594 100644 --- a/library/Commands.cpp +++ b/library/Commands.cpp @@ -9,6 +9,7 @@ #include "RemoteTools.h" #include "modules/Gui.h" +#include "modules/Hotkey.h" #include "modules/World.h" #include "df/viewscreen_new_regionst.h" @@ -282,56 +283,62 @@ namespace DFHack command_result Commands::keybinding(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) { - if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) - { - std::string keystr = parts[1]; + using Hotkey::KeySpec; + auto hotkey_mgr = core.getHotkeyManager(); + std::string parse_error; + if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) { + const std::string& keystr = parts[1]; if (parts[0] == "set") - core.ClearKeyBindings(keystr); - // for (int i = parts.size()-1; i >= 2; i--) - for (const auto& part : parts | std::views::drop(2) | std::views::reverse) - { - if (!core.AddKeyBinding(keystr, part)) - { - con.printerr("Invalid key spec: %s\n", keystr.c_str()); - return CR_FAILURE; + hotkey_mgr->removeKeybind(keystr); + for (const auto& part : parts | std::views::drop(2) | std::views::reverse) { + auto spec = KeySpec::parse(keystr, &parse_error); + if (!spec.has_value()) { + con.printerr("%s\n", parse_error.c_str()); + break; + } + if (!hotkey_mgr->addKeybind(spec.value(), part)) { + con.printerr("Invalid command: '%s'\n", part.c_str()); + break; } } } - else if (parts.size() >= 2 && parts[0] == "clear") - { - // for (size_t i = 1; i < parts.size(); i++) - for (const auto& part : parts | std::views::drop(1)) - { - if (!core.ClearKeyBindings(part)) - { - con.printerr("Invalid key spec: %s\n", part.c_str()); - return CR_FAILURE; + else if (parts.size() >= 2 && parts[0] == "clear") { + for (const auto& part : parts | std::views::drop(1)) { + auto spec = KeySpec::parse(part, &parse_error); + if (!spec.has_value()) { + con.printerr("%s\n", parse_error.c_str()); + } + if (!hotkey_mgr->removeKeybind(spec.value())) { + con.printerr("No matching keybinds to remove\n"); + break; } } } - else if (parts.size() == 2 && parts[0] == "list") - { - std::vector list = core.ListKeyBindings(parts[1]); + else if (parts.size() == 2 && parts[0] == "list") { + auto spec = KeySpec::parse(parts[1], &parse_error); + if (!spec.has_value()) { + con.printerr("%s\n", parse_error.c_str()); + return CR_FAILURE; + } + std::vector list = hotkey_mgr->listKeybinds(spec.value()); if (list.empty()) con << "No bindings." << std::endl; for (const auto& kb : list) con << " " << kb << std::endl; } - else - { - con << "Usage:" << std::endl - << " keybinding list " << std::endl - << " keybinding clear [@context]..." << std::endl - << " keybinding set [@context] \"cmdline\" \"cmdline\"..." << std::endl - << " keybinding add [@context] \"cmdline\" \"cmdline\"..." << std::endl - << "Later adds, and earlier items within one command have priority." << std::endl - << "Supported keys: [Ctrl-][Alt-][Shift-](A-Z, 0-9, F1-F12, `, or Enter)." << std::endl - << "Context may be used to limit the scope of the binding, by" << std::endl - << "requiring the current context to have a certain prefix." << std::endl - << "Current UI context is: " << std::endl + else { + con << "Usage:\n" + << " keybinding list \n" + << " keybinding clear [@context]...\n" + << " keybinding set [@context] \"cmdline\" \"cmdline\"...\n" + << " keybinding add [@context] \"cmdline\" \"cmdline\"...\n" + << "Later adds, and earlier items within one command have priority.\n" + << "Key format: [Ctrl-][Alt-][Super-][Shift-](A-Z, 0-9, F1-F12, `, etc.).\n" + << "Context may be used to limit the scope of the binding, by\n" + << "requiring the current context to have a certain prefix.\n" + << "Current UI context is: \n" << join_strings("\n", Gui::getCurFocus(true)) << std::endl; } - return CR_OK; } diff --git a/library/Core.cpp b/library/Core.cpp index c21052c186..6a6b69b8b7 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -51,6 +51,7 @@ distribution. #include "modules/EventManager.h" #include "modules/Filesystem.h" #include "modules/Gui.h" +#include "modules/Hotkey.h" #include "modules/Textures.h" #include "modules/World.h" #include "modules/Persistence.h" @@ -108,11 +109,9 @@ using std::string; // FIXME: A lot of code in one file, all doing different things... there's something fishy about it. -static bool parseKeySpec(std::string keyspec, int *psym, int *pmod, std::string *pfocus = nullptr); static size_t loadScriptFiles(Core* core, color_ostream& out, std::span prefix, const std::filesystem::path& folder); namespace DFHack { - DBG_DECLARE(core, keybinding, DebugCategory::LINFO); DBG_DECLARE(core, script, DebugCategory::LINFO); @@ -278,34 +277,6 @@ struct IODATA PluginManager * plug_mgr; }; -// A thread function... for handling hotkeys. This is needed because -// all the plugin commands are expected to be run from foreign threads. -// Running them from one of the main DF threads will result in deadlock! -static void fHKthread(IODATA * iodata) -{ - Core * core = iodata->core; - PluginManager * plug_mgr = iodata->plug_mgr; - if(!plug_mgr || !core) - { - std::cerr << "Hotkey thread has croaked." << std::endl; - return; - } - bool keep_going = true; - while(keep_going) - { - std::string stuff = core->getHotkeyCmd(keep_going); // waits on mutex! - if(!stuff.empty()) - { - color_ostream_proxy out(core->getConsole()); - - auto rv = core->runCommand(out, stuff); - - if (rv == CR_NOT_IMPLEMENTED) - out.printerr("Invalid hotkey command: '%s'\n", stuff.c_str()); - } - } -} - std::string DFHack::dfhack_version_desc() { std::stringstream s; @@ -1026,8 +997,7 @@ Core::~Core() Core::Core() : d(std::make_unique()), script_path_mutex{}, - HotkeyMutex{}, - HotkeyCond{}, + armok_mutex{}, alias_mutex{}, started{false}, CoreSuspendMutex{}, @@ -1043,7 +1013,6 @@ Core::Core() : // set up hotkey capture suppress_duplicate_keyboard_events = true; - hotkey_set = NO; last_world_data_ptr = nullptr; last_local_map_ptr = nullptr; last_pause_state = false; @@ -1396,6 +1365,8 @@ bool Core::InitSimulationThread() plug_mgr->init(); std::cerr << "Starting the TCP listener.\n"; auto listen = ServerMain::listen(RemoteClient::GetDefaultPort()); + this->hotkey_mgr = new HotkeyManager(); + auto *temp = new IODATA; temp->core = this; temp->plug_mgr = plug_mgr; @@ -1413,8 +1384,6 @@ bool Core::InitSimulationThread() } std::cerr << "Starting DF input capture thread.\n"; - // set up hotkey capture - d->hotkeythread = std::thread(fHKthread, temp); started = true; modstate = 0; @@ -1488,31 +1457,6 @@ bool Core::InitSimulationThread() return true; } -/// sets the current hotkey command -bool Core::setHotkeyCmd( std::string cmd ) -{ - // access command - std::lock_guard lock(HotkeyMutex); - hotkey_set = SET; - hotkey_cmd = std::move(cmd); - HotkeyCond.notify_all(); - return true; -} -/// removes the hotkey command and gives it to the caller thread -std::string Core::getHotkeyCmd( bool &keep_going ) -{ - std::string returner; - std::unique_lock lock(HotkeyMutex); - HotkeyCond.wait(lock, [this]() -> bool {return this->hotkey_set;}); - if (hotkey_set == SHUTDOWN) { - keep_going = false; - return returner; - } - hotkey_set = NO; - returner = hotkey_cmd; - hotkey_cmd.clear(); - return returner; -} void Core::print(const char *format, ...) { @@ -1960,17 +1904,14 @@ int Core::Shutdown ( void ) con.shutdown(); } - if (d->hotkeythread.joinable()) { - std::unique_lock hot_lock(HotkeyMutex); - hotkey_set = SHUTDOWN; - HotkeyCond.notify_one(); - } - ServerMain::block(); - d->hotkeythread.join(); d->iothread.join(); + if (hotkey_mgr) { + delete hotkey_mgr; + } + if(plug_mgr) { delete plug_mgr; @@ -2045,16 +1986,24 @@ void Core::setSuppressDuplicateKeyboardEvents(bool suppress) { } void Core::setMortalMode(bool value) { - std::lock_guard lock(HotkeyMutex); - mortal_mode = value; + mortal_mode.exchange(value); +} + +bool Core::getMortalMode() { + return mortal_mode.load(); } void Core::setArmokTools(const std::vector &tool_names) { - std::lock_guard lock(HotkeyMutex); + std::lock_guard lock(armok_mutex); armok_tools.clear(); armok_tools.insert(tool_names.begin(), tool_names.end()); } +bool Core::isArmokTool(const std::string& tool) { + std::lock_guard lock(armok_mutex); + return armok_tools.contains(tool); +} + // returns true if the event is handled bool Core::DFH_SDL_Event(SDL_Event* ev) { uint32_t start_ms = p->getTickCount(); @@ -2095,11 +2044,13 @@ bool Core::doSdlInputEvent(SDL_Event* ev) modstate = (ev->type == SDL_KEYDOWN) ? modstate | DFH_MOD_CTRL : modstate & ~DFH_MOD_CTRL; else if (sym == SDLK_LALT || sym == SDLK_RALT) modstate = (ev->type == SDL_KEYDOWN) ? modstate | DFH_MOD_ALT : modstate & ~DFH_MOD_ALT; + else if (sym == SDLK_LGUI || sym == SDLK_RGUI) // Renamed to LMETA/RMETA in SDL3 + modstate = (ev->type == SDL_KEYDOWN) ? modstate | DFH_MOD_SUPER : modstate & ~DFH_MOD_SUPER; else if (ke.state == SDL_PRESSED && !hotkey_states[sym]) { // the check against hotkey_states[sym] ensures we only process keybindings once per keypress DEBUG(keybinding).print("key down: sym=%d (%c)\n", sym, sym); - if (SelectHotkey(sym, modstate)) { + if (hotkey_mgr->handleKeybind(sym, modstate)) { hotkey_states[sym] = true; if (modstate & (DFH_MOD_CTRL | DFH_MOD_ALT)) { DEBUG(keybinding).print("modifier key detected; not inhibiting SDL key down event\n"); @@ -2120,8 +2071,9 @@ bool Core::doSdlInputEvent(SDL_Event* ev) DEBUG(keybinding).print("mouse button down: button=%d\n", but.button); // don't mess with the first three buttons, which are critical elements of DF's control scheme if (but.button > 3) { - SDL_Keycode sym = SDLK_F13 + but.button - 4; - if (sym <= SDLK_F24 && SelectHotkey(sym, modstate)) + // We represent mouse buttons as a negative number, permitting buttons 4-15 + SDL_Keycode sym = -but.button; + if (sym >= -15 && sym <= -4 && hotkey_mgr->handleKeybind(sym, modstate)) return suppress_duplicate_keyboard_events; } } else if (ev->type == SDL_TEXTINPUT) { @@ -2141,236 +2093,6 @@ bool Core::doSdlInputEvent(SDL_Event* ev) return false; } -bool Core::SelectHotkey(int sym, int modifiers) -{ - // Find the topmost viewscreen - if (!df::global::gview || !df::global::plotinfo) - return false; - - df::viewscreen *screen = &df::global::gview->view; - while (screen->child) - screen = screen->child; - - if (sym == SDLK_KP_ENTER) - sym = SDLK_RETURN; - - std::string cmd; - - DEBUG(keybinding).print("checking hotkeys for sym=%d (%c), modifiers=%x\n", sym, sym, modifiers); - - { - std::lock_guard lock(HotkeyMutex); - - // Check the internal keybindings - std::vector &bindings = key_bindings[sym]; - //for (int i = bindings.size()-1; i >= 0; --i) { - for (const auto& binding : bindings | std::views::reverse) { - DEBUG(keybinding).print("examining hotkey with commandline: '%s'\n", binding.cmdline.c_str()); - - if (binding.modifiers != modifiers) { - DEBUG(keybinding).print("skipping keybinding due to modifiers mismatch: 0x%x != 0x%x\n", - binding.modifiers, modifiers); - continue; - } - if (!binding.focus.empty()) { - if (!Gui::matchFocusString(binding.focus)) { - std::vector focusStrings = Gui::getCurFocus(true); - DEBUG(keybinding).print("skipping keybinding due to focus string mismatch: '%s' != '%s'\n", - join_strings(", ", focusStrings).c_str(), binding.focus.c_str()); - continue; - } - } - if (!plug_mgr->CanInvokeHotkey(binding.command[0], screen)) { - DEBUG(keybinding).print("skipping keybinding due to hotkey guard rejection (command: '%s')\n", - binding.command[0].c_str()); - continue; - } - if (mortal_mode && armok_tools.contains(binding.command[0])) { - DEBUG(keybinding).print("skipping keybinding due to mortal mode (command: '%s')\n", - binding.command[0].c_str()); - continue; - } - - cmd = binding.cmdline; - DEBUG(keybinding).print("matched hotkey\n"); - break; - } - - if (cmd.empty()) { - // Check the hotkey keybindings - int idx = sym - SDLK_F1; - if(idx >= 0 && idx < 8) - { -/* TODO: understand how this changes for v50 - if (modifiers & 1) - idx += 8; - - if (strict_virtual_cast(screen) && - df::global::plotinfo->main.mode != ui_sidebar_mode::Hotkeys && - df::global::plotinfo->main.hotkeys[idx].cmd == df::ui_hotkey::T_cmd::None) - { - cmd = df::global::plotinfo->main.hotkeys[idx].name; - } -*/ - } - } - } - - if (!cmd.empty()) { - setHotkeyCmd(cmd); - return true; - } - - return false; -} - -static bool parseKeySpec(std::string keyspec, int *psym, int *pmod, std::string *pfocus) -{ - *pmod = 0; - - if (pfocus) - { - *pfocus = ""; - - size_t idx = keyspec.find('@'); - if (idx != std::string::npos) - { - *pfocus = keyspec.substr(idx+1); - keyspec = keyspec.substr(0, idx); - } - } - - // ugh, ugly - for (;;) { - if (keyspec.size() > 6 && keyspec.starts_with("Shift-")) { - *pmod |= 1; - keyspec = keyspec.substr(6); - } else if (keyspec.size() > 5 && keyspec.starts_with("Ctrl-")) { - *pmod |= 2; - keyspec = keyspec.substr(5); - } else if (keyspec.size() > 4 && keyspec.starts_with("Alt-")) { - *pmod |= 4; - keyspec = keyspec.substr(4); - } else - break; - } - - if (keyspec.size() == 1 && keyspec[0] >= 'A' && keyspec[0] <= 'Z') { - *psym = SDLK_a + (keyspec[0]-'A'); - return true; - } else if (keyspec.size() == 1 && keyspec[0] == '`') { - *psym = SDLK_BACKQUOTE; - return true; - } else if (keyspec.size() == 1 && keyspec[0] >= '0' && keyspec[0] <= '9') { - *psym = SDLK_0 + (keyspec[0]-'0'); - return true; - } else if (keyspec.size() == 2 && keyspec[0] == 'F' && keyspec[1] >= '1' && keyspec[1] <= '9') { - *psym = SDLK_F1 + (keyspec[1]-'1'); - return true; - } else if (keyspec.size() == 3 && keyspec.starts_with("F1") && keyspec[2] >= '0' && keyspec[2] <= '2') { - *psym = SDLK_F10 + (keyspec[2]-'0'); - return true; - } else if (keyspec.size() == 6 && keyspec.starts_with("MOUSE") && keyspec[5] >= '4' && keyspec[5] <= '9') { - *psym = SDLK_F13 + (keyspec[5]-'4'); - return true; - } else if (keyspec.size() == 7 && keyspec.starts_with("MOUSE1") && keyspec[5] >= '0' && keyspec[5] <= '5') { - *psym = SDLK_F19 + (keyspec[5]-'0'); - return true; - } else if (keyspec == "Enter") { - *psym = SDLK_RETURN; - return true; - } else - return false; -} - -bool Core::ClearKeyBindings(std::string keyspec) -{ - int sym, mod; - std::string focus; - if (!parseKeySpec(keyspec, &sym, &mod, &focus)) - return false; - - std::lock_guard lock(HotkeyMutex); - - std::vector &bindings = key_bindings[sym]; - for (int i = bindings.size()-1; i >= 0; --i) { - if (bindings[i].modifiers == mod && prefix_matches(focus, bindings[i].focus)) - bindings.erase(bindings.begin()+i); - } - - return true; -} - -bool Core::AddKeyBinding(std::string keyspec, std::string cmdline) -{ - size_t at_pos = keyspec.find('@'); - if (at_pos != std::string::npos) - { - std::string raw_spec = keyspec.substr(0, at_pos); - std::string raw_focus = keyspec.substr(at_pos + 1); - if (raw_focus.find('|') != std::string::npos) - { - std::vector focus_strings; - split_string(&focus_strings, raw_focus, "|"); - for (const auto& fs : focus_strings) - { - if (!AddKeyBinding(raw_spec + "@" + fs, cmdline)) - return false; - } - return true; - } - } - int sym; - KeyBinding binding; - if (!parseKeySpec(keyspec, &sym, &binding.modifiers, &binding.focus)) - return false; - - cheap_tokenise(cmdline, binding.command); - if (binding.command.empty()) - return false; - - std::lock_guard lock(HotkeyMutex); - - // Don't add duplicates - std::vector &bindings = key_bindings[sym]; - for (int i = bindings.size()-1; i >= 0; --i) { - if (bindings[i].modifiers == binding.modifiers && - bindings[i].cmdline == cmdline && - bindings[i].focus == binding.focus) - return true; - } - - binding.cmdline = cmdline; - bindings.push_back(binding); - return true; -} - -std::vector Core::ListKeyBindings(std::string keyspec) -{ - int sym, mod; - std::vector rv; - std::string focus; - if (!parseKeySpec(keyspec, &sym, &mod, &focus)) - return rv; - - std::lock_guard lock(HotkeyMutex); - - std::vector &bindings = key_bindings[sym]; - for (int i = bindings.size()-1; i >= 0; --i) { - if (focus.size() && focus != bindings[i].focus) - continue; - if (bindings[i].modifiers == mod) - { - std::string cmd = bindings[i].cmdline; - if (!bindings[i].focus.empty()) - cmd = "@" + bindings[i].focus + ": " + cmd; - rv.push_back(cmd); - } - } - - return rv; -} - bool Core::AddAlias(const std::string &name, const std::vector &command, bool replace) { std::lock_guard lock(alias_mutex); diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 450d03e95e..80278ba63b 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -48,6 +48,7 @@ distribution. #include "modules/EventManager.h" #include "modules/Filesystem.h" #include "modules/Gui.h" +#include "modules/Hotkey.h" #include "modules/Items.h" #include "modules/Job.h" #include "modules/Kitchen.h" @@ -1867,6 +1868,123 @@ static const luaL_Reg dfhack_gui_funcs[] = { { NULL, NULL } }; +/***** Hotkey module *****/ +static bool hotkey_addKeybind(const std::string spec, const std::string cmd) { + auto hotkey_mgr = Core::getInstance().getHotkeyManager(); + if (!hotkey_mgr) return false; + return hotkey_mgr->addKeybind(spec, cmd); +} + +static bool hotkey_isDisruptiveKeybind(const std::string spec) { + auto key = Hotkey::KeySpec::parse(spec); + if (!key.has_value()) + return true; + return key.value().isDisruptive(); +} + +static int hotkey_requestKeybindingInput(lua_State *L) { + auto hotkey_mgr = Core::getInstance().getHotkeyManager(); + if (!hotkey_mgr) return 0; + bool cancel = false; + if (lua_gettop(L) == 1) + cancel = lua_toboolean(L, -1); + hotkey_mgr->requestKeybindingInput(cancel); + return 0; +} + +static int hotkey_getKeybindingInput(lua_State *L) { + auto hotkey_mgr = Core::getInstance().getHotkeyManager(); + auto input = hotkey_mgr->getKeybindingInput(); + + if (input.empty()) { + lua_pushnil(L); + } else { + lua_pushlstring(L, input.data(), input.size()); + } + return 1; +} + +static int hotkey_removeKeybind(lua_State *L) { + auto hotkey_mgr = Core::getInstance().getHotkeyManager(); + if (!hotkey_mgr) { + lua_pushboolean(L, false); + return 1; + } + + bool res = false; + switch (lua_gettop(L)) { + case 1: + luaL_checkstring(L, -1); + res = hotkey_mgr->removeKeybind(lua_tostring(L, -1)); + break; + case 2: + luaL_checkstring(L, -2); + res = hotkey_mgr->removeKeybind(lua_tostring(L, -2), lua_toboolean(L, -1)); + break; + case 3: + luaL_checkstring(L, -3); + luaL_checkstring(L, -1); + res = hotkey_mgr->removeKeybind( + lua_tostring(L, -3), + lua_toboolean(L, -2), + lua_tostring(L, -1) + ); + break; + } + + lua_pushboolean(L, res); + return 1; +} + +void hotkey_pushBindArray(lua_State *L, const std::vector& binds) { + lua_createtable(L, binds.size(), 0); + int i = 1; + for (const auto& bind : binds) { + lua_createtable(L, 0, 2); + + lua_pushlstring(L, "spec", 4); + auto spec_str = bind.spec.toString(true); + lua_pushlstring(L, spec_str.data(), spec_str.size()); + lua_settable(L, -3); + + lua_pushlstring(L, "command", 7); + lua_pushlstring(L, bind.cmdline.data(), bind.cmdline.size()); + lua_settable(L, -3); + lua_rawseti(L, -2, i++); + } +} + +static int hotkey_listActiveKeybinds(lua_State *L) { + auto hotkey_mgr = Core::getInstance().getHotkeyManager(); + auto binds = hotkey_mgr->listActiveKeybinds(); + + hotkey_pushBindArray(L, binds); + return 1; +} + +static int hotkey_listAllKeybinds(lua_State *L) { + auto hotkey_mgr = Core::getInstance().getHotkeyManager(); + auto binds = hotkey_mgr->listAllKeybinds(); + + hotkey_pushBindArray(L, binds); + return 1; +} + +static const luaL_Reg dfhack_hotkey_funcs[] = { + { "removeKeybind", hotkey_removeKeybind }, + { "listActiveKeybinds", hotkey_listActiveKeybinds }, + { "listAllKeybinds", hotkey_listAllKeybinds }, + { "requestKeybindingInput", hotkey_requestKeybindingInput }, + { "getKeybindingInput", hotkey_getKeybindingInput }, + { NULL, NULL } +}; + +static const LuaWrapper::FunctionReg dfhack_hotkey_module[] = { + WRAPN(addKeybind, hotkey_addKeybind), + WRAPN(isDisruptiveKeybind, hotkey_isDisruptiveKeybind), + { NULL, NULL } +}; + /***** Job module *****/ static bool jobEqual(const df::job *job1, const df::job *job2) @@ -3965,6 +4083,9 @@ static int internal_getModifiers(lua_State *L) lua_pushstring(L, "alt"); lua_pushboolean(L, modstate & DFH_MOD_ALT); lua_settable(L, -3); + lua_pushstring(L, "super"); + lua_pushboolean(L, modstate & DFH_MOD_SUPER); + lua_settable(L, -3); return 1; } @@ -4306,6 +4427,7 @@ void OpenDFHackApi(lua_State *state) luaL_setfuncs(state, dfhack_funcs, 0); OpenModule(state, "translation", dfhack_translation_module); OpenModule(state, "gui", dfhack_gui_module, dfhack_gui_funcs); + OpenModule(state, "hotkey", dfhack_hotkey_module, dfhack_hotkey_funcs); OpenModule(state, "job", dfhack_job_module, dfhack_job_funcs); OpenModule(state, "textures", dfhack_textures_funcs); OpenModule(state, "units", dfhack_units_module, dfhack_units_funcs); diff --git a/library/include/Core.h b/library/include/Core.h index 27c96f00b5..51c9723542 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -59,6 +59,7 @@ namespace DFHack constexpr auto DFH_MOD_SHIFT = 1; constexpr auto DFH_MOD_CTRL = 2; constexpr auto DFH_MOD_ALT = 4; + constexpr auto DFH_MOD_SUPER = 8; class Process; class Module; @@ -69,6 +70,7 @@ namespace DFHack class Core; class ServerMain; class CoreSuspender; + class HotkeyManager; namespace Lua { namespace Core { DFHACK_EXPORT void Reset(color_ostream &out, const char *where); @@ -166,10 +168,6 @@ namespace DFHack Materials * getMaterials(); /// get the graphic module Graphic * getGraphic(); - /// sets the current hotkey command - bool setHotkeyCmd( std::string cmd ); - /// removes the hotkey command and gives it to the caller thread - std::string getHotkeyCmd( bool &keep_going ); command_result runCommand(color_ostream &out, const std::string &command, std::vector ¶meters, bool no_autocomplete = false); command_result runCommand(color_ostream& out, const std::string& command); @@ -185,11 +183,10 @@ namespace DFHack bool getSuppressDuplicateKeyboardEvents() const; void setSuppressDuplicateKeyboardEvents(bool suppress); void setMortalMode(bool value); + bool getMortalMode(); void setArmokTools(const std::vector &tool_names); + bool isArmokTool(const std::string& name); - bool ClearKeyBindings(std::string keyspec); - bool AddKeyBinding(std::string keyspec, std::string cmdline); - std::vector ListKeyBindings(std::string keyspec); int8_t getModstate() { return modstate; } bool AddAlias(const std::string &name, const std::vector &command, bool replace = false); @@ -216,6 +213,7 @@ namespace DFHack static void printerr(const char *format, ...) Wformat(printf,1,2); PluginManager* getPluginManager() const { return plug_mgr; } + HotkeyManager* getHotkeyManager() { return hotkey_mgr; } static void cheap_tokenise(std::string const& input, std::vector &output); @@ -294,39 +292,24 @@ namespace DFHack Graphic * pGraphic; } s_mods; std::vector> allModules; - DFHack::PluginManager * plug_mgr; + DFHack::PluginManager *plug_mgr; + + // Hotkey Manager + DFHack::HotkeyManager *hotkey_mgr; std::vector script_paths[3]; std::mutex script_path_mutex; - // hotkey-related stuff - struct KeyBinding { - int modifiers; - std::vector command; - std::string cmdline; - std::string focus; - }; int8_t modstate; bool suppress_duplicate_keyboard_events; - bool mortal_mode; + std::atomic mortal_mode; std::unordered_set armok_tools; - std::map > key_bindings; - std::string hotkey_cmd; - enum hotkey_set_t { - NO, - SET, - SHUTDOWN, - }; - hotkey_set_t hotkey_set; - std::mutex HotkeyMutex; - std::condition_variable HotkeyCond; + std::mutex armok_mutex; std::map> aliases; std::recursive_mutex alias_mutex; - bool SelectHotkey(int key, int modifiers); - // for state change tracking df::world_data *last_world_data_ptr; // for state change tracking diff --git a/library/include/modules/DFSDL.h b/library/include/modules/DFSDL.h index be40eff2c5..393877e095 100644 --- a/library/include/modules/DFSDL.h +++ b/library/include/modules/DFSDL.h @@ -6,6 +6,7 @@ #include #include #include +#include struct SDL_Surface; struct SDL_Rect; @@ -13,6 +14,7 @@ struct SDL_Renderer; struct SDL_PixelFormat; struct SDL_Window; union SDL_Event; +using SDL_Keycode = int32_t; namespace DFHack { @@ -70,6 +72,8 @@ namespace DFHack::DFSDL DFHACK_EXPORT char* DFSDL_GetPrefPath(const char* org, const char* app); DFHACK_EXPORT char* DFSDL_GetBasePath(); + DFHACK_EXPORT SDL_Keycode DFSDL_GetKeyFromName(const char* name); + DFHACK_EXPORT const char* DFSDL_GetKeyName(SDL_Keycode key); } namespace DFHack diff --git a/library/include/modules/Hotkey.h b/library/include/modules/Hotkey.h new file mode 100644 index 0000000000..25129b2ff5 --- /dev/null +++ b/library/include/modules/Hotkey.h @@ -0,0 +1,77 @@ +#pragma once + +#include "Export.h" +#include "ColorText.h" + +#include +#include +#include +#include +#include + +namespace DFHack { + namespace Hotkey { + class DFHACK_EXPORT KeySpec { + public: + int modifiers = 0; + // Negative numbers denote mouse buttons + int sym = 0; + std::vector focus; + + static std::optional parse(std::string spec, std::string* err = nullptr); + std::string toString(bool include_focus=true) const; + + // Determines if a keybind could be disruptive to normal gameplay, + // including typing and navigating the UI. + bool isDisruptive() const; + }; + + struct KeyBinding { + KeySpec spec; + std::string command; + std::string cmdline; + }; + } + class DFHACK_EXPORT HotkeyManager { + public: + HotkeyManager(); + ~HotkeyManager(); + + + bool addKeybind(std::string keyspec, std::string_view cmd); + bool addKeybind(Hotkey::KeySpec spec, std::string_view cmd); + // Clear a keybind with the given keyspec, optionally for any focus, or with a specific command + bool removeKeybind(std::string keyspec, bool match_focus=true, std::string_view cmdline=""); + bool removeKeybind(const Hotkey::KeySpec& spec, bool match_focus=true, std::string_view cmdline=""); + + std::vector listKeybinds(std::string keyspec); + std::vector listKeybinds(const Hotkey::KeySpec& spec); + + std::vector listActiveKeybinds(); + std::vector listAllKeybinds(); + + bool handleKeybind(int sym, int modifiers); + void setHotkeyCommand(std::string cmd); + + // Used to request the next hotkey-compatible input is saved. + // This is to allow for graphical keybinding menus. + void requestKeybindingInput(bool cancel=false); + // Returns the latest requested keybind input + std::string getKeybindingInput(); + + private: + std::thread hotkey_thread; + std::mutex lock; + std::condition_variable cond; + + bool keybind_save_requested = false; + std::string requested_keybind; + + uint8_t hotkey_sig = 0; + std::string queued_command; + + std::map> bindings; + + void hotkey_thread_fn(); + }; +} diff --git a/library/modules/DFSDL.cpp b/library/modules/DFSDL.cpp index 9543f81387..f23cde5511 100644 --- a/library/modules/DFSDL.cpp +++ b/library/modules/DFSDL.cpp @@ -7,6 +7,7 @@ #include "PluginManager.h" #include +#include #include @@ -67,6 +68,9 @@ uint32_t (*g_SDL_GetMouseState)(int* x, int* y) = nullptr; void (*g_SDL_RenderWindowToLogical)(SDL_Renderer* renderer, int windowX, int windowY, float* logicalX, float* logicalY); void (*g_SDL_RenderLogicalToWindow)(SDL_Renderer* renderer, float logicalX, float logicalY, int* windowX, int* windowY); +SDL_Keycode (*g_SDL_GetKeyFromName)(const char* name) = nullptr; +const char* (*g_SDL_GetKeyName)(SDL_Keycode key) = nullptr; + bool DFSDL::init(color_ostream &out) { for (auto &lib_str : SDL_LIBS) { if ((g_sdl_handle = OpenPlugin(lib_str.c_str()))) @@ -111,6 +115,8 @@ bool DFSDL::init(color_ostream &out) { bind(g_sdl_handle, SDL_ShowSimpleMessageBox); bind(g_sdl_handle, SDL_GetPrefPath); bind(g_sdl_handle, SDL_GetBasePath); + bind(g_sdl_handle, SDL_GetKeyFromName); + bind(g_sdl_handle, SDL_GetKeyName); bind(g_sdl_handle, SDL_GetMouseState); bind(g_sdl_handle, SDL_RenderWindowToLogical); bind(g_sdl_handle, SDL_RenderLogicalToWindow); @@ -216,6 +222,18 @@ int DFSDL::DFSDL_ShowSimpleMessageBox(uint32_t flags, const char *title, const c return g_SDL_ShowSimpleMessageBox(flags, title, message, window); } +SDL_Keycode DFSDL::DFSDL_GetKeyFromName(const char* name) { + if (!g_SDL_GetKeyFromName) + return SDLK_UNKNOWN; + return g_SDL_GetKeyFromName(name); +} + +const char* DFSDL::DFSDL_GetKeyName(SDL_Keycode key) { + if (!g_SDL_GetKeyName) + return ""; + return g_SDL_GetKeyName(key); +} + // convert tabs to spaces so they don't get converted to '?' static char * tabs_to_spaces(char *str) { for (char *c = str; *c; ++c) { diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp new file mode 100644 index 0000000000..6433879d85 --- /dev/null +++ b/library/modules/Hotkey.cpp @@ -0,0 +1,404 @@ +#include "modules/Hotkey.h" + +#include +#include + +#include "Core.h" +#include "ColorText.h" +#include "MiscUtils.h" +#include "PluginManager.h" + +#include "modules/DFSDL.h" +#include "modules/Gui.h" + +#include "df/global_objects.h" +#include "df/viewscreen.h" +#include "df/interfacest.h" + + +using namespace DFHack; +using Hotkey::KeySpec; +using Hotkey::KeyBinding; + +enum HotkeySignal : uint8_t { + None = 0, + CmdReady, + Shutdown, +}; + +static bool operator==(const KeySpec& a, const KeySpec& b) { + return a.modifiers == b.modifiers && a.sym == b.sym && + a.focus.size() == b.focus.size() && + std::equal(a.focus.begin(), a.focus.end(), b.focus.begin()); +} + +// Equality operator for key bindings +static bool operator==(const KeyBinding& a, const KeyBinding& b) { + return a.spec == b.spec && + a.command == b.command && + a.cmdline == b.cmdline; +} + +std::string KeySpec::toString(bool include_focus) const { + std::string out; + if (modifiers & DFH_MOD_CTRL) out += "Ctrl-"; + if (modifiers & DFH_MOD_ALT) out += "Alt-"; + if (modifiers & DFH_MOD_SUPER) out += "Super-"; + if (modifiers & DFH_MOD_SHIFT) out += "Shift-"; + + std::string key_name; + if (this->sym < 0) { + key_name = "MOUSE" + std::to_string(-this->sym); + } else { + key_name = DFSDL::DFSDL_GetKeyName(this->sym); + } + out += key_name; + + if (include_focus && !this->focus.empty()) { + out += "@"; + bool first = true; + for (const auto& fc : this->focus) { + if (first) { + first = false; + out += fc; + } else { + out += "|" + fc; + } + } + } + + return out; +} + +std::optional KeySpec::parse(std::string spec, std::string* err) { + KeySpec out; + + // Determine focus string, if present + size_t focus_idx = spec.find('@'); + if (focus_idx != std::string::npos) { + split_string(&out.focus, spec.substr(focus_idx + 1), "|"); + spec.erase(focus_idx); + } + + // Treat remaining keyspec as lowercase for case-insensitivity. + std::transform(spec.begin(), spec.end(), spec.begin(), tolower); + + // Determine modifier flags + auto match_modifier = [&out, &spec](std::string_view prefix, int mod) { + bool found = spec.starts_with(prefix); + if (found) { + out.modifiers |= mod; + spec.erase(0, prefix.size()); + } + return found; + }; + while (match_modifier("shift-", DFH_MOD_SHIFT) + || match_modifier("ctrl-", DFH_MOD_CTRL) + || match_modifier("alt-", DFH_MOD_ALT) + || match_modifier("super-", DFH_MOD_SUPER)) {} + + out.sym = DFSDL::DFSDL_GetKeyFromName(spec.c_str()); + if (out.sym != SDLK_UNKNOWN) + return out; + + // Attempt to parse as a mouse binding + if (spec.starts_with("mouse")) { + spec.erase(0, 5); + // Read button number, ensuring between 4 and 15 inclusive + try { + int mbutton = std::stoi(spec); + if (mbutton >= 4 && mbutton <= 15) { + out.sym = -mbutton; + return out; + } + } catch (...) { + // If integer parsing fails, it isn't valid + } + if (err) + *err = "Invalid mouse button '" + spec + "', only 4-15 are valid"; + return std::nullopt; + } + + if (err) + *err = "Unknown key '" + spec + "'"; + + // Invalid key binding + return std::nullopt; +} + +bool KeySpec::isDisruptive() const { + // SDLK enum uses the actual characters for a key as its value. + // Escaped values included are Return, Escape, Backspace, and Tab + const std::string essential_key_set = "\r\x1B\b\t -=[]\\;',./"; + + // Letters A-Z, 0-9, and other special keys such as return/escape, and other general typing keys + bool is_essential_key = (this->sym >= SDLK_a && this->sym <= SDLK_z) // A-Z + || (this->sym >= SDLK_0 && this->sym <= SDLK_9) // 0-9 + || (this->sym < CHAR_MAX && essential_key_set.find((char)this->sym) != std::string::npos) + || (this->sym >= SDLK_LEFT && this->sym <= SDLK_UP); // Arrow keys + + // Essential keys are safe, so long as they have a modifier that isn't Shift + if (is_essential_key && !(this->modifiers & ~DFH_MOD_SHIFT)) + return true; + + return false; +} + +// Hotkeys actions are executed from an external thread to avoid deadlocks +// that may occur if running commands from the render or simulation threads. +void HotkeyManager::hotkey_thread_fn() { + auto& core = DFHack::Core::getInstance(); + + std::unique_lock l(lock); + while (true) { + cond.wait(l, [this]() { return this->hotkey_sig != HotkeySignal::None; }); + if (hotkey_sig == HotkeySignal::Shutdown) + return; + if (hotkey_sig != HotkeySignal::CmdReady) + continue; + + // Copy and reset important data, then release the lock + this->hotkey_sig = HotkeySignal::None; + std::string cmd = this->queued_command; + this->queued_command.clear(); + l.unlock(); + + // Attempt execution of command + DFHack::color_ostream_proxy out(core.getConsole()); + auto res = core.runCommand(out, cmd); + if (res == DFHack::CR_NOT_IMPLEMENTED) + out.printerr("Invalid hotkey command: '%s'\n", cmd.c_str()); + l.lock(); + } +} + + +bool HotkeyManager::addKeybind(KeySpec spec, std::string_view cmd) { + // No point in a hotkey with no action + if (cmd.empty()) + return false; + + KeyBinding binding; + binding.spec = std::move(spec); + binding.cmdline = cmd; + size_t space_idx = cmd.find(' '); + binding.command = space_idx == std::string::npos ? cmd : cmd.substr(0, space_idx); + + std::lock_guard l(lock); + auto& bindings = this->bindings[binding.spec.sym]; + for (auto& bind : bindings) { + // Don't set a keybind twice, but return true as there isn't an issue + if (bind == binding) + return true; + } + + bindings.emplace_back(binding); + return true; +} + +bool HotkeyManager::addKeybind(std::string keyspec, std::string_view cmd) { + std::optional spec_opt = KeySpec::parse(std::move(keyspec)); + if (!spec_opt.has_value()) + return false; + return this->addKeybind(spec_opt.value(), cmd); +} + +bool HotkeyManager::removeKeybind(const KeySpec& spec, bool match_focus, std::string_view cmdline) { + std::lock_guard l(lock); + if (!bindings.contains(spec.sym)) + return false; + auto& binds = bindings[spec.sym]; + + auto new_end = std::remove_if(binds.begin(), binds.end(), [match_focus, spec, &cmdline](const auto& v) { + return v.spec.sym == spec.sym + && v.spec.modifiers == spec.modifiers + && (!match_focus || v.spec.focus == spec.focus) + && (cmdline.empty() || v.cmdline == cmdline); + }); + if (new_end == binds.end()) + return false; // No bindings removed + + binds.erase(new_end, binds.end()); + return true; +} + +bool HotkeyManager::removeKeybind(std::string keyspec, bool match_focus, std::string_view cmdline) { + std::optional spec_opt = KeySpec::parse(std::move(keyspec)); + if (!spec_opt.has_value()) + return false; + return this->removeKeybind(spec_opt.value(), match_focus, cmdline); +} + +std::vector HotkeyManager::listKeybinds(const KeySpec& spec) { + std::lock_guard l(lock); + if (!bindings.contains(spec.sym)) + return {}; + + std::vector out; + + auto& binds = bindings[spec.sym]; + for (const auto& bind : binds) { + if (bind.spec.modifiers != spec.modifiers) + continue; + + // If no focus is required, it is always active + if (spec.focus.empty() || bind.spec.focus.empty()) { + out.push_back(bind.cmdline); + continue; + } + + // If a focus is required, determine if search spec if the same or more specific + for (const auto& requested : spec.focus) { + for (const auto& to_match : bind.spec.focus) { + if (prefix_matches(to_match, requested)) + out.push_back("@" + to_match + ":" + bind.cmdline); + } + } + } + + return out; +} + +std::vector HotkeyManager::listKeybinds(std::string keyspec) { + std::optional spec_opt = KeySpec::parse(std::move(keyspec)); + if (!spec_opt.has_value()) + return {}; + return this->listKeybinds(spec_opt.value()); +} + +std::vector HotkeyManager::listActiveKeybinds() { + std::lock_guard l(lock); + std::vector out; + + for(const auto& [_, bind_set] : bindings) { + for (const auto& binding : bind_set) { + if (binding.spec.focus.empty()) { + // Binding always active + out.emplace_back(binding); + continue; + } + for (const auto& focus : binding.spec.focus) { + // Determine if focus string allows this binding + if (Gui::matchFocusString(focus)) { + out.emplace_back(binding); + break; + } + } + } + } + + return out; +} + +std::vector HotkeyManager::listAllKeybinds() { + std::lock_guard l(lock); + std::vector out; + + for (const auto& [_, bind_set] : bindings) { + for (const auto& bind : bind_set) { + out.emplace_back(bind); + } + } + return out; +} + +bool HotkeyManager::handleKeybind(int sym, int modifiers) { + // Ensure gamestate is ready + if (!df::global::gview || !df::global::plotinfo) + return false; + + // Get topmost active screen + df::viewscreen *screen = &df::global::gview->view; + while (screen->child) + screen = screen->child; + + // Map keypad return to return + if (sym == SDLK_KP_ENTER) + sym = SDLK_RETURN; + + std::unique_lock l(lock); + + // If reading input for a keybinding screen, save the input and exit early + if (keybind_save_requested) { + KeySpec spec; + spec.sym = sym; + spec.modifiers = modifiers; + requested_keybind = spec.toString(false); + keybind_save_requested = false; + return true; + } + + if (!bindings.contains(sym)) + return false; + auto& binds = bindings[sym]; + + auto& core = Core::getInstance(); + bool mortal_mode = core.getMortalMode(); + + // Iterate in reverse, prioritizing the last added keybinds + for (const auto& bind : binds | std::views::reverse) { + if (bind.spec.modifiers != modifiers) + continue; + + if (!bind.spec.focus.empty()) { + bool matched = false; + for (const auto& focus : bind.spec.focus) { + if (Gui::matchFocusString(focus)) { + matched = true; + break; + } + } + if (!matched) + continue; + } + + if (!core.getPluginManager()->CanInvokeHotkey(bind.command, screen)) + continue; + + if (mortal_mode && core.isArmokTool(bind.command)) + continue; + + queued_command = bind.cmdline; + hotkey_sig = HotkeySignal::CmdReady; + l.unlock(); + cond.notify_all(); + return true; + } + + return false; +} + +void HotkeyManager::setHotkeyCommand(std::string cmd) { + std::unique_lock l(lock); + queued_command = std::move(cmd); + hotkey_sig = HotkeySignal::CmdReady; + l.unlock(); + cond.notify_all(); +} + +void HotkeyManager::requestKeybindingInput(bool cancel) { + std::lock_guard l(lock); + keybind_save_requested = !cancel; + requested_keybind.clear(); +} + +std::string HotkeyManager::getKeybindingInput() { + std::lock_guard l(lock); + return requested_keybind; +} + +HotkeyManager::HotkeyManager() { + this->hotkey_thread = std::thread(&HotkeyManager::hotkey_thread_fn, this); +} + +HotkeyManager::~HotkeyManager() { + // Set shutdown signal and notify thread + { + std::lock_guard l(lock); + this->hotkey_sig = HotkeySignal::Shutdown; + } + cond.notify_all(); + + if (this->hotkey_thread.joinable()) + this->hotkey_thread.join(); +} diff --git a/plugins/blueprint.cpp b/plugins/blueprint.cpp index 98b4c1c155..330a0938f9 100644 --- a/plugins/blueprint.cpp +++ b/plugins/blueprint.cpp @@ -19,6 +19,7 @@ #include "modules/Constructions.h" #include "modules/Filesystem.h" #include "modules/Gui.h" +#include "modules/Hotkey.h" #include "modules/Maps.h" #include "modules/World.h" @@ -1668,7 +1669,7 @@ static command_result do_blueprint(color_ostream &out, string command_str = command.str(); out.print("launching %s\n", command_str.c_str()); - Core::getInstance().setHotkeyCmd(command_str); + Core::getInstance().getHotkeyManager()->setHotkeyCommand(command_str); return CR_OK; } diff --git a/plugins/embark-assistant/embark-assistant.cpp b/plugins/embark-assistant/embark-assistant.cpp index db04308d0b..5b9d11f78a 100644 --- a/plugins/embark-assistant/embark-assistant.cpp +++ b/plugins/embark-assistant/embark-assistant.cpp @@ -5,6 +5,7 @@ #include "PluginManager.h" #include "modules/Gui.h" +#include "modules/Hotkey.h" #include "modules/Screen.h" #include "../uicommon.h" @@ -161,7 +162,7 @@ struct start_site_hook : df::viewscreen_choose_start_sitest { { if (!embark_assist::main::state && input->count(interface_key::CUSTOM_A)) { - Core::getInstance().setHotkeyCmd("embark-assistant"); + Core::getInstance().getHotkeyManager()->setHotkeyCommand("embark-assistant"); return; } INTERPOSE_NEXT(feed)(input); diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index 13988a21f1..448d2a1c10 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -2,8 +2,10 @@ #include #include +#include "modules/DFSDL.h" #include "modules/Gui.h" #include "modules/Screen.h" +#include "modules/Hotkey.h" #include "Debug.h" #include "LuaTools.h" @@ -44,7 +46,7 @@ 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); + Core::getInstance().getHotkeyManager()->removeKeybind(keyspec); }); valid = false; sorted_keys.clear(); @@ -83,60 +85,17 @@ static void add_binding_if_valid(color_ostream &out, const string &sym, const st string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING; 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); + Core::getInstance().getHotkeyManager()->addKeybind(keyspec, binding); } static void find_active_keybindings(color_ostream &out, df::viewscreen *screen, bool filtermenu) { - DEBUG(log).print("scanning for active keybindings\n"); if (valid) cleanupHotkeys(NULL); - vector valid_keys; - - for (char c = '0'; c <= '9'; c++) { - valid_keys.push_back(string(&c, 1)); - } - - for (char c = 'A'; c <= 'Z'; c++) { - valid_keys.push_back(string(&c, 1)); - } - - for (int i = 1; i <= 12; i++) { - valid_keys.push_back('F' + int_to_string(i)); - } - - valid_keys.push_back("`"); - - 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-"; - if (shifted) sym += "Shift-"; - sym += *it; - - auto list = Core::getInstance().ListKeyBindings(sym); - for (auto invoke_cmd = list.begin(); invoke_cmd != list.end(); invoke_cmd++) { - 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(out, sym, *invoke_cmd, screen, filtermenu); - } - else { - vector tokens; - split_string(&tokens, *invoke_cmd, ":"); - string focus = tokens[0].substr(1); - if(Gui::matchFocusString(focus)) { - auto cmdline = trim(tokens[1]); - add_binding_if_valid(out, sym, cmdline, screen, filtermenu); - } - } - } - } - } - } + auto active_binds = Core::getInstance().getHotkeyManager()->listActiveKeybinds(); + for (const auto& bind : active_binds) { + string sym = bind.spec.toString(false); + add_binding_if_valid(out, sym, bind.cmdline, screen, filtermenu); } valid = true;