Skip to content

Commit 329b32b

Browse files
authored
Merge pull request #5654 from NicksWorld/refactor/hotkey_module
Rework hotkey support
2 parents d24e8f1 + bfee603 commit 329b32b

File tree

15 files changed

+781
-425
lines changed

15 files changed

+781
-425
lines changed

docs/builtins/keybinding.rst

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ Like any other command, it can be used at any time from the console, but
99
bindings are not remembered between runs of the game unless re-created in
1010
:file:`dfhack-config/init/dfhack.init`.
1111

12-
Hotkeys can be any combinations of Ctrl/Alt/Shift with A-Z, 0-9, F1-F12, or `
13-
(the key below the :kbd:`Esc` key on most keyboards). You can also represent
14-
mouse buttons beyond the first three with ``MOUSE4`` through ``MOUSE15``.
12+
Hotkeys can be any combinations of Ctrl/Alt/Super/Shift with any key recognized by SDL.
13+
You can also represent mouse buttons beyond the first three with ``MOUSE4``
14+
through ``MOUSE15``.
1515

1616
Usage
1717
-----
@@ -27,12 +27,13 @@ Usage
2727
``keybinding set <key> "<cmdline>" ["<cmdline>" ...]``
2828
Clear, and then add bindings for the specified key.
2929

30-
The ``<key>`` parameter above has the following **case-sensitive** syntax::
30+
The ``<key>`` parameter above has the following case-insensitive syntax::
3131

32-
[Ctrl-][Alt-][Shift-]KEY[@context[|context...]]
32+
[Ctrl-][Alt-][Super-][Shift-]KEY[@context[|context...]]
3333

3434
where the ``KEY`` part can be any recognized key and :kbd:`[`:kbd:`]` denote
35-
optional parts.
35+
optional parts. It is important to note that the key is the non-shifted version
36+
of the key. For example ``!`` would be defined as ``Shift-0``.
3637

3738
DFHack commands can advertise the contexts in which they can be usefully run.
3839
For example, a command that acts on a selected unit can tell `keybinding` that

docs/changelog.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,22 @@ Template for new versions:
6666

6767
## Misc Improvements
6868
- `createitem`: created items can now be placed onto/into tables, nests, bookcases, display cases, and altars
69+
- `keybinding`: keybinds may now include the super key, and are no longer limited to particular keys ranges of keys, allowing any recognized by SDL.
6970
- The ``fpause`` console command can now be used to force world generation to pause (as it did prior to version 50).
7071

7172
## Documentation
7273

7374
## API
75+
- ``Hotkey``: New module for hotkey functionality
7476

7577
## Lua
7678
- The ``Lua interactive interpreter`` banner now documents keywords such as ``unit`` and ``item`` which reference the currently-selected object in the DF UI.
79+
- ``dfhack.hotkey.addKeybind``: Creates new keybindings
80+
- ``dfhack.hotkey.removeKeybind``: Removes existing keybindings
81+
- ``dfhack.hotkey.listActiveKeybinds``: Lists all keybinds for the current context
82+
- ``dfhack.hotkey.listAllKeybinds``: Lists all keybinds for all contexts
83+
- ``dfhack.hotkey.requestKeybindingInput``: Requests the next keybind-compatible input is saved
84+
- ``dfhack.hotkey.getKeybindingInput``: Reads the input saved in response to a request.
7785

7886
## Removed
7987

docs/dev/Lua API.rst

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1430,6 +1430,53 @@ Job module
14301430

14311431
Returns the job's description, as seen in the Units and Jobs screens.
14321432

1433+
Hotkey module
1434+
-------------
1435+
1436+
* ``dfhack.hotkey.addKeybind(keyspec, command)``
1437+
1438+
Creates a new keybind with the provided keyspec (see the `keybinding` documentation
1439+
for details on format).
1440+
Returns false on failure to create keybind.
1441+
1442+
* ``dfhack.hotkey.removeKeybind(keyspec, [match_focus=true, command])``
1443+
1444+
Removes keybinds matching the provided keyspec.
1445+
If match_focus is set, the focus portion of the keyspec is matched against.
1446+
If command is provided and not an empty string, the command is matched against.
1447+
Returns false if no keybinds were removed.
1448+
1449+
* ``dfhack.hotkey.listActiveKeybinds()``
1450+
1451+
Returns a list of keybinds active within the current context.
1452+
The items are tables with the following attributes:
1453+
:spec: The keyspec for the hotkey
1454+
:command: The command the hotkey runs when pressed
1455+
1456+
* ``dfhack.hotkey.listAllKeybinds()``
1457+
1458+
Returns a list of all keybinds currently registered.
1459+
The items are tables with the following attributes:
1460+
:spec: The keyspec for the hotkey
1461+
:command: The command the hotkey runs when pressed
1462+
1463+
* ``dfhack.hotkey.requestKeybindingInput([cancel=false])``
1464+
1465+
Enqueues or cancels a request that the next hotkey-compatible input is saved
1466+
and not processed, retrievable with ``dfhack.hotkey.getKeybindingInput()``.
1467+
If cancel is true, any current request is cancelled.
1468+
1469+
* ``dfhack.hotkey.getKeybindingInput()``
1470+
1471+
Reads the latest saved keybind input that was requested.
1472+
Returns a keyspec string for the input, or nil if no input has been saved.
1473+
1474+
* ``dfhack.hotkey.isDisruptiveKeybind(keyspec)``
1475+
1476+
Determines if the provided keyspec could be disruptive to the game experience.
1477+
This includes the majority of standard characters and special keys such as escape,
1478+
backspace, and return when lacking modifiers other than Shift.
1479+
14331480
Units module
14341481
------------
14351482

@@ -3494,7 +3541,7 @@ and are only documented here for completeness:
34943541
* ``dfhack.internal.getModifiers()``
34953542

34963543
Returns the state of the keyboard modifier keys in a table of string ->
3497-
boolean. The keys are ``ctrl``, ``shift``, and ``alt``.
3544+
boolean. The keys are ``ctrl``, ``shift``, ``super``, and ``alt``.
34983545

34993546
* ``dfhack.internal.getSuppressDuplicateKeyboardEvents()``
35003547
* ``dfhack.internal.setSuppressDuplicateKeyboardEvents(suppress)``

library/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ set(MODULE_HEADERS
160160
include/modules/Graphic.h
161161
include/modules/Gui.h
162162
include/modules/GuiHooks.h
163+
include/modules/Hotkey.h
163164
include/modules/Items.h
164165
include/modules/Job.h
165166
include/modules/Kitchen.h
@@ -190,6 +191,7 @@ set(MODULE_SOURCES
190191
modules/Filesystem.cpp
191192
modules/Graphic.cpp
192193
modules/Gui.cpp
194+
modules/Hotkey.cpp
193195
modules/Items.cpp
194196
modules/Job.cpp
195197
modules/Kitchen.cpp

library/Commands.cpp

Lines changed: 43 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include "RemoteTools.h"
1010

1111
#include "modules/Gui.h"
12+
#include "modules/Hotkey.h"
1213
#include "modules/World.h"
1314

1415
#include "df/viewscreen_new_regionst.h"
@@ -282,56 +283,62 @@ namespace DFHack
282283

283284
command_result Commands::keybinding(color_ostream& con, Core& core, const std::string& first, const std::vector<std::string>& parts)
284285
{
285-
if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add"))
286-
{
287-
std::string keystr = parts[1];
286+
using Hotkey::KeySpec;
287+
auto hotkey_mgr = core.getHotkeyManager();
288+
std::string parse_error;
289+
if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) {
290+
const std::string& keystr = parts[1];
288291
if (parts[0] == "set")
289-
core.ClearKeyBindings(keystr);
290-
// for (int i = parts.size()-1; i >= 2; i--)
291-
for (const auto& part : parts | std::views::drop(2) | std::views::reverse)
292-
{
293-
if (!core.AddKeyBinding(keystr, part))
294-
{
295-
con.printerr("Invalid key spec: %s\n", keystr.c_str());
296-
return CR_FAILURE;
292+
hotkey_mgr->removeKeybind(keystr);
293+
for (const auto& part : parts | std::views::drop(2) | std::views::reverse) {
294+
auto spec = KeySpec::parse(keystr, &parse_error);
295+
if (!spec.has_value()) {
296+
con.printerr("%s\n", parse_error.c_str());
297+
break;
298+
}
299+
if (!hotkey_mgr->addKeybind(spec.value(), part)) {
300+
con.printerr("Invalid command: '%s'\n", part.c_str());
301+
break;
297302
}
298303
}
299304
}
300-
else if (parts.size() >= 2 && parts[0] == "clear")
301-
{
302-
// for (size_t i = 1; i < parts.size(); i++)
303-
for (const auto& part : parts | std::views::drop(1))
304-
{
305-
if (!core.ClearKeyBindings(part))
306-
{
307-
con.printerr("Invalid key spec: %s\n", part.c_str());
308-
return CR_FAILURE;
305+
else if (parts.size() >= 2 && parts[0] == "clear") {
306+
for (const auto& part : parts | std::views::drop(1)) {
307+
auto spec = KeySpec::parse(part, &parse_error);
308+
if (!spec.has_value()) {
309+
con.printerr("%s\n", parse_error.c_str());
310+
}
311+
if (!hotkey_mgr->removeKeybind(spec.value())) {
312+
con.printerr("No matching keybinds to remove\n");
313+
break;
309314
}
310315
}
311316
}
312-
else if (parts.size() == 2 && parts[0] == "list")
313-
{
314-
std::vector<std::string> list = core.ListKeyBindings(parts[1]);
317+
else if (parts.size() == 2 && parts[0] == "list") {
318+
auto spec = KeySpec::parse(parts[1], &parse_error);
319+
if (!spec.has_value()) {
320+
con.printerr("%s\n", parse_error.c_str());
321+
return CR_FAILURE;
322+
}
323+
std::vector<std::string> list = hotkey_mgr->listKeybinds(spec.value());
315324
if (list.empty())
316325
con << "No bindings." << std::endl;
317326
for (const auto& kb : list)
318327
con << " " << kb << std::endl;
319328
}
320-
else
321-
{
322-
con << "Usage:" << std::endl
323-
<< " keybinding list <key>" << std::endl
324-
<< " keybinding clear <key>[@context]..." << std::endl
325-
<< " keybinding set <key>[@context] \"cmdline\" \"cmdline\"..." << std::endl
326-
<< " keybinding add <key>[@context] \"cmdline\" \"cmdline\"..." << std::endl
327-
<< "Later adds, and earlier items within one command have priority." << std::endl
328-
<< "Supported keys: [Ctrl-][Alt-][Shift-](A-Z, 0-9, F1-F12, `, or Enter)." << std::endl
329-
<< "Context may be used to limit the scope of the binding, by" << std::endl
330-
<< "requiring the current context to have a certain prefix." << std::endl
331-
<< "Current UI context is: " << std::endl
329+
else {
330+
con << "Usage:\n"
331+
<< " keybinding list <key>\n"
332+
<< " keybinding clear <key>[@context]...\n"
333+
<< " keybinding set <key>[@context] \"cmdline\" \"cmdline\"...\n"
334+
<< " keybinding add <key>[@context] \"cmdline\" \"cmdline\"...\n"
335+
<< "Later adds, and earlier items within one command have priority.\n"
336+
<< "Key format: [Ctrl-][Alt-][Super-][Shift-](A-Z, 0-9, F1-F12, `, etc.).\n"
337+
<< "Context may be used to limit the scope of the binding, by\n"
338+
<< "requiring the current context to have a certain prefix.\n"
339+
<< "Current UI context is: \n"
332340
<< join_strings("\n", Gui::getCurFocus(true)) << std::endl;
333341
}
334-
335342
return CR_OK;
336343
}
337344

0 commit comments

Comments
 (0)