1 /* Copyright (C) 2021 Wildfire Games.
2 * This file is part of 0 A.D.
4 * 0 A.D. is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 2 of the License, or
7 * (at your option) any later version.
9 * 0 A.D. is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
18 #include "precompiled.h"
21 #include <boost/tokenizer.hpp>
23 #include "lib/external_libraries/libsdl.h"
24 #include "ps/CConsole.h"
25 #include "ps/CLogger.h"
27 #include "ps/ConfigDB.h"
28 #include "ps/Globals.h"
29 #include "ps/KeyName.h"
31 static bool unified
[UNIFIED_LAST
- UNIFIED_SHIFT
];
33 std::unordered_map
<int, KeyMapping
> g_HotkeyMap
;
36 std::unordered_map
<std::string
, bool> g_HotkeyStatus
;
40 PressedHotkey(const SHotkeyMapping
* m
, bool t
) : mapping(m
), retriggered(t
) {};
41 // NB: this points to one of g_HotkeyMap's mappings. It works because that std::unordered_map is stable once constructed.
42 const SHotkeyMapping
* mapping
;
43 // Whether the hotkey was triggered by a key release (silences "press" and "up" events).
49 ReleasedHotkey(const char* n
, bool t
) : name(n
), wasRetriggered(t
) {};
54 // 'In-flight' state used because the hotkey triggering process is split in two phase.
55 // These hotkeys may still be stopped if the event responsible for triggering them is handled
56 // before it can be used to generate the hotkeys.
57 std::vector
<PressedHotkey
> newPressedHotkeys
;
58 // Stores the 'specificity' of the newly pressed hotkeys.
59 size_t closestMapMatch
= 0;
60 // This is merely used to ensure consistency in EventWillFireHotkey.
61 const SDL_Event_
* currentEvent
;
63 // List of currently pressed hotkeys. This is used to quickly reset hotkeys.
64 // This is an unsorted vector because there will generally be very few elements,
65 // so it's presumably faster than std::set.
66 std::vector
<PressedHotkey
> pressedHotkeys
;
68 // List of active keys relevant for hotkeys.
69 std::vector
<SDL_Scancode_
> activeScancodes
;
72 static_assert(std::is_integral
<std::underlying_type
<SDL_Scancode
>::type
>::value
, "SDL_Scancode is not an integral enum.");
73 static_assert(SDL_USEREVENT_
== SDL_USEREVENT
, "SDL_USEREVENT_ is not the same type as the real SDL_USEREVENT");
74 static_assert(UNUSED_HOTKEY_CODE
== SDL_SCANCODE_UNKNOWN
);
76 // Look up each key binding in the config file and set the mappings for
77 // all key combinations that trigger it.
78 static void LoadConfigBindings(CConfigDB
& configDB
)
80 for (const std::pair
<const CStr
, CConfigValueSet
>& configPair
: configDB
.GetValuesWithPrefix(CFG_COMMAND
, "hotkey."))
82 std::string hotkeyName
= configPair
.first
.substr(7); // strip the "hotkey." prefix
84 // "unused" is kept or the A23->24 migration, this can likely be removed in A25.
85 if (configPair
.second
.empty() || (configPair
.second
.size() == 1 && configPair
.second
.front() == "unused"))
87 // Unused hotkeys must still be registered in the map to appear in the hotkey editor.
88 SHotkeyMapping unusedCode
;
89 unusedCode
.name
= hotkeyName
;
90 unusedCode
.primary
= SKey
{ UNUSED_HOTKEY_CODE
};
91 g_HotkeyMap
[UNUSED_HOTKEY_CODE
].push_back(unusedCode
);
95 for (const CStr
& hotkey
: configPair
.second
)
97 std::vector
<SKey
> keyCombination
;
99 // Iterate through multiple-key bindings (e.g. Ctrl+I)
100 boost::char_separator
<char> sep("+");
101 typedef boost::tokenizer
<boost::char_separator
<char> > tokenizer
;
102 tokenizer
tok(hotkey
, sep
);
103 for (tokenizer::iterator it
= tok
.begin(); it
!= tok
.end(); ++it
)
105 // Attempt decode as key name
106 SDL_Scancode scancode
= FindScancode(it
->c_str());
109 LOGWARNING("Hotkey mapping used invalid key '%s'", hotkey
.c_str());
113 SKey key
= { scancode
};
114 keyCombination
.push_back(key
);
117 std::vector
<SKey
>::iterator itKey
, itKey2
;
118 for (itKey
= keyCombination
.begin(); itKey
!= keyCombination
.end(); ++itKey
)
120 SHotkeyMapping bindCode
;
122 bindCode
.name
= hotkeyName
;
123 bindCode
.primary
= SKey
{ itKey
->code
};
125 for (itKey2
= keyCombination
.begin(); itKey2
!= keyCombination
.end(); ++itKey2
)
126 if (itKey
!= itKey2
) // Push any auxiliary keys
127 bindCode
.requires
.push_back(*itKey2
);
129 g_HotkeyMap
[itKey
->code
].push_back(bindCode
);
135 void LoadHotkeys(CConfigDB
& configDB
)
137 pressedHotkeys
.clear();
138 LoadConfigBindings(configDB
);
143 pressedHotkeys
.clear();
145 g_HotkeyStatus
.clear();
148 bool isPressed(const SKey
& key
)
150 // Normal keycodes are below EXTRA_KEYS_BASE
151 if ((int)key
.code
< EXTRA_KEYS_BASE
)
152 return g_scancodes
[key
.code
];
153 // Mouse 'keycodes' are after the modifier keys
154 else if ((int)key
.code
< MOUSE_LAST
&& (int)key
.code
> MOUSE_BASE
)
155 return g_mouse_buttons
[key
.code
- MOUSE_BASE
];
156 // Modifier keycodes are between the normal keys and the mouse 'keys'
157 else if ((int)key
.code
< UNIFIED_LAST
&& (int)key
.code
> SDL_NUM_SCANCODES
)
158 return unified
[key
.code
- UNIFIED_SHIFT
];
159 // This codepath shouldn't be taken, but not having it triggers warnings.
164 InReaction
HotkeyStateChange(const SDL_Event_
* ev
)
166 if (ev
->ev
.type
== SDL_HOTKEYPRESS
|| ev
->ev
.type
== SDL_HOTKEYPRESS_SILENT
)
167 g_HotkeyStatus
[static_cast<const char*>(ev
->ev
.user
.data1
)] = true;
168 else if (ev
->ev
.type
== SDL_HOTKEYUP
|| ev
->ev
.type
== SDL_HOTKEYUP_SILENT
)
169 g_HotkeyStatus
[static_cast<const char*>(ev
->ev
.user
.data1
)] = false;
173 InReaction
HotkeyInputPrepHandler(const SDL_Event_
* ev
)
175 int scancode
= SDL_SCANCODE_UNKNOWN
;
177 // Restore default state.
178 newPressedHotkeys
.clear();
179 currentEvent
= nullptr;
185 scancode
= ev
->ev
.key
.keysym
.scancode
;
188 case SDL_MOUSEBUTTONDOWN
:
189 case SDL_MOUSEBUTTONUP
:
190 // Mousewheel events are no longer buttons, but we want to maintain the order
191 // expected by g_mouse_buttons for compatibility
192 if (ev
->ev
.button
.button
>= SDL_BUTTON_X1
)
193 scancode
= MOUSE_BASE
+ (int)ev
->ev
.button
.button
+ 2;
195 scancode
= MOUSE_BASE
+ (int)ev
->ev
.button
.button
;
199 if (ev
->ev
.wheel
.y
> 0)
201 scancode
= MOUSE_WHEELUP
;
204 else if (ev
->ev
.wheel
.y
< 0)
206 scancode
= MOUSE_WHEELDOWN
;
209 else if (ev
->ev
.wheel
.x
> 0)
214 else if (ev
->ev
.wheel
.x
< 0)
227 // Create phantom 'unified-modifier' events when left- or right- modifier keys are pressed
228 // Just send them to this handler; don't let the imaginary event codes leak back to real SDL.
231 phantom
.ev
.type
= ((ev
->ev
.type
== SDL_KEYDOWN
) || (ev
->ev
.type
== SDL_MOUSEBUTTONDOWN
)) ? SDL_KEYDOWN
: SDL_KEYUP
;
232 if (phantom
.ev
.type
== SDL_KEYDOWN
)
233 phantom
.ev
.key
.repeat
= ev
->ev
.type
== SDL_KEYDOWN
? ev
->ev
.key
.repeat
: 0;
235 if (scancode
== SDL_SCANCODE_LSHIFT
|| scancode
== SDL_SCANCODE_RSHIFT
)
237 phantom
.ev
.key
.keysym
.scancode
= static_cast<SDL_Scancode
>(UNIFIED_SHIFT
);
238 unified
[0] = (phantom
.ev
.type
== SDL_KEYDOWN
);
239 return HotkeyInputPrepHandler(&phantom
);
241 else if (scancode
== SDL_SCANCODE_LCTRL
|| scancode
== SDL_SCANCODE_RCTRL
)
243 phantom
.ev
.key
.keysym
.scancode
= static_cast<SDL_Scancode
>(UNIFIED_CTRL
);
244 unified
[1] = (phantom
.ev
.type
== SDL_KEYDOWN
);
245 return HotkeyInputPrepHandler(&phantom
);
247 else if (scancode
== SDL_SCANCODE_LALT
|| scancode
== SDL_SCANCODE_RALT
)
249 phantom
.ev
.key
.keysym
.scancode
= static_cast<SDL_Scancode
>(UNIFIED_ALT
);
250 unified
[2] = (phantom
.ev
.type
== SDL_KEYDOWN
);
251 return HotkeyInputPrepHandler(&phantom
);
253 else if (scancode
== SDL_SCANCODE_LGUI
|| scancode
== SDL_SCANCODE_RGUI
)
255 phantom
.ev
.key
.keysym
.scancode
= static_cast<SDL_Scancode
>(UNIFIED_SUPER
);
256 unified
[3] = (phantom
.ev
.type
== SDL_KEYDOWN
);
257 return HotkeyInputPrepHandler(&phantom
);
260 // Check whether we have any hotkeys registered that include this scancode.
261 if (g_HotkeyMap
.find(scancode
) == g_HotkeyMap
.end())
267 * Hotkey behaviour spec (see also tests):
268 * - If both 'F' and 'Ctrl+F' are hotkeys, and Ctrl & F keys are down, then the more specific one only is fired ('Ctrl+F' here).
269 * - If 'Ctrl+F' and 'Ctrl+A' are both hotkeys, both may fire simulatenously (respectively without Ctrl).
270 * - However, per the first point, 'Ctrl+Shift+F' would fire alone in that situation.
271 * - "Press" is sent once, when the hotkey is initially triggered.
272 * - "Up" is sent once, when the hotkey is released or superseded by a more specific hotkey.
273 * - "Down" is sent repeatedly, and is also sent alongside the inital "Press".
274 * - As a special case (see below), "Down" is not sent alongside "PressSilent".
275 * - If 'Ctrl+F' is active, and 'Ctrl' is released, 'F' must become active again.
276 * - However, the "Press" event is _not_ fired. Instead, "PressSilent" is.
277 * - Likewise, once 'F' is released, the "Up" event will be a "UpSilent".
278 * (the reason is that it is unexpected to trigger a press on key release).
279 * - Hotkeys are allowed to fire with extra keys (e.g. Ctrl+F+A still triggers 'Ctrl+F').
280 * - If 'F' and 'Ctrl+F' trigger the same hotkey, adding 'Ctrl' _and_ releasing 'Ctrl' will trigger new 'Press' events.
281 * The "Up" event is only sent when both Ctrl & F are released.
282 * - This is somewhat unexpected/buggy, but it makes the implementation easier and is easily avoidable for players.
283 * - Wheel scrolling is 'instantaneous' behaviour and is essentially entirely separate from the above.
284 * - It won't untrigger other hotkeys, and fires/releases on the same 'key event'.
285 * Note that mouse buttons/wheel inputs can fire hotkeys, in combinations with keys.
286 * ...Yes, this is all surprisingly complex.
289 bool isReleasedKey
= ev
->ev
.type
== SDL_KEYUP
|| ev
->ev
.type
== SDL_MOUSEBUTTONUP
;
290 // Wheel events are pressed & released in the same go.
291 bool isInstantaneous
= ev
->ev
.type
== SDL_MOUSEWHEEL
;
293 if (!isInstantaneous
)
295 std::vector
<SDL_Scancode_
>::iterator it
= std::find(activeScancodes
.begin(), activeScancodes
.end(), scancode
);
296 // This prevents duplicates, assuming we might end up in a weird state - feels safer with input.
297 if (isReleasedKey
&& it
!= activeScancodes
.end())
298 activeScancodes
.erase(it
);
299 else if (!isReleasedKey
&& it
== activeScancodes
.end())
300 activeScancodes
.emplace_back(scancode
);
303 std::vector
<SDL_Scancode_
> triggers
;
304 if (!isReleasedKey
|| isInstantaneous
)
305 triggers
.push_back(scancode
);
307 // If the key is released, we need to check all less precise hotkeys again, to see if we should retrigger some.
308 for (SDL_Scancode_ code
: activeScancodes
)
309 triggers
.push_back(code
);
311 // Now check if we need to trigger new hotkeys / retrigger hotkeys.
312 // We'll need the match-level and the keys in play to release currently pressed hotkeys.
314 for (SDL_Scancode_ code
: triggers
)
315 for (const SHotkeyMapping
& hotkey
: g_HotkeyMap
[code
])
317 // Ensure no duplications in the new list.
318 if (std::find_if(newPressedHotkeys
.begin(), newPressedHotkeys
.end(),
319 [&hotkey
](const PressedHotkey
& v
){ return v
.mapping
->name
== hotkey
.name
; }) != newPressedHotkeys
.end())
323 for (const SKey
& k
: hotkey
.requires
)
325 accept
= isPressed(k
);
332 // Check if this is an equally precise or more precise match
333 if (hotkey
.requires
.size() + 1 >= closestMapMatch
)
335 // Check if more precise
336 if (hotkey
.requires
.size() + 1 > closestMapMatch
)
338 // Throw away the old less-precise matches
339 newPressedHotkeys
.clear();
340 closestMapMatch
= hotkey
.requires
.size() + 1;
342 newPressedHotkeys
.emplace_back(&hotkey
, isReleasedKey
);
349 InReaction
HotkeyInputActualHandler(const SDL_Event_
* ev
)
354 bool isInstantaneous
= ev
->ev
.type
== SDL_MOUSEWHEEL
;
356 // TODO: it's probably possible to break hotkeys somewhat if the "Up" event that would release a hotkey is handled
357 // by a priori handler - it might be safer to do that in the 'Prep' phase.
358 std::vector
<ReleasedHotkey
> releasedHotkeys
;
360 // For instantaneous events, we don't update the pressedHotkeys (i.e. currently active hotkeys),
361 // we just fire/release the triggered hotkeys transiently.
362 // Therefore, skip the whole 'check pressedHotkeys & swap with newPressedHotkeys' logic.
363 if (!isInstantaneous
)
365 for (PressedHotkey
& hotkey
: pressedHotkeys
)
367 bool addingAnew
= std::find_if(newPressedHotkeys
.begin(), newPressedHotkeys
.end(),
368 [&hotkey
](const PressedHotkey
& v
){ return v
.mapping
->name
== hotkey
.mapping
->name
; }) != newPressedHotkeys
.end();
370 // Update the triggered status to match our current state.
372 std::find_if(newPressedHotkeys
.begin(), newPressedHotkeys
.end(),
373 [&hotkey
](const PressedHotkey
& v
){ return v
.mapping
->name
== hotkey
.mapping
->name
; })->retriggered
= hotkey
.retriggered
;
374 // If the already-pressed hotkey has a lower specificity than the new hotkey(s), de-activate it.
375 else if (hotkey
.mapping
->requires
.size() + 1 < closestMapMatch
)
377 releasedHotkeys
.emplace_back(hotkey
.mapping
->name
.c_str(), hotkey
.retriggered
);
381 // Check that the hotkey still matches all active keys.
382 bool accept
= isPressed(hotkey
.mapping
->primary
);
384 for (const SKey
& k
: hotkey
.mapping
->requires
)
386 accept
= isPressed(k
);
390 if (!accept
&& !addingAnew
)
391 releasedHotkeys
.emplace_back(hotkey
.mapping
->name
.c_str(), hotkey
.retriggered
);
394 // If this hotkey has higher specificity than the new hotkeys we wanted to trigger/retrigger,
395 // then discard this new addition(s). This works because at any given time, all hotkeys
396 // active must have the same specificity.
397 if (hotkey
.mapping
->requires
.size() + 1 > closestMapMatch
)
399 closestMapMatch
= hotkey
.mapping
->requires
.size() + 1;
400 newPressedHotkeys
.clear();
401 newPressedHotkeys
.emplace_back(hotkey
.mapping
, hotkey
.retriggered
);
403 else if (!addingAnew
)
404 newPressedHotkeys
.emplace_back(hotkey
.mapping
, hotkey
.retriggered
);
408 pressedHotkeys
.swap(newPressedHotkeys
);
411 for (const PressedHotkey
& hotkey
: isInstantaneous
? newPressedHotkeys
: pressedHotkeys
)
413 // Send a KeyPress event when a hotkey is pressed initially and on mouseButton and mouseWheel events.
414 if (ev
->ev
.type
!= SDL_KEYDOWN
|| ev
->ev
.key
.repeat
== 0)
416 SDL_Event_ hotkeyPressNotification
;
417 hotkeyPressNotification
.ev
.type
= hotkey
.retriggered
? SDL_HOTKEYPRESS_SILENT
: SDL_HOTKEYPRESS
;
418 hotkeyPressNotification
.ev
.user
.data1
= const_cast<char*>(hotkey
.mapping
->name
.c_str());
419 in_push_priority_event(&hotkeyPressNotification
);
422 // Send a HotkeyDown event on every key, mouseButton and mouseWheel event.
423 // The exception is on the first retriggering: hotkeys may fire transiently
424 // while a user lifts fingers off multi-key hotkeys, and listeners to "hotkeydown"
425 // generally don't expect that to trigger then.
426 // (It might be better to check for HotkeyIsPressed, however).
427 // For keys the event is repeated depending on hardware and OS configured interval.
428 // On linux, modifier keys (shift, alt, ctrl) are not repeated, see https://github.com/SFML/SFML/issues/122.
429 if (ev
->ev
.key
.repeat
== 0 && hotkey
.retriggered
)
431 SDL_Event_ hotkeyDownNotification
;
432 hotkeyDownNotification
.ev
.type
= SDL_HOTKEYDOWN
;
433 hotkeyDownNotification
.ev
.user
.data1
= const_cast<char*>(hotkey
.mapping
->name
.c_str());
434 in_push_priority_event(&hotkeyDownNotification
);
437 // Release instantaneous events (e.g. mouse wheel) right away.
439 for (const PressedHotkey
& hotkey
: newPressedHotkeys
)
440 releasedHotkeys
.emplace_back(hotkey
.mapping
->name
.c_str(), false);
442 for (const ReleasedHotkey
& hotkey
: releasedHotkeys
)
444 SDL_Event_ hotkeyNotification
;
445 hotkeyNotification
.ev
.type
= hotkey
.wasRetriggered
? SDL_HOTKEYUP_SILENT
: SDL_HOTKEYUP
;
446 hotkeyNotification
.ev
.user
.data1
= const_cast<char*>(hotkey
.name
);
447 in_push_priority_event(&hotkeyNotification
);
453 bool EventWillFireHotkey(const SDL_Event_
* ev
, const CStr
& keyname
)
455 // Sanity check of sort. This parameter mostly exists because it looks right from the caller's perspective.
456 if (ev
!= currentEvent
|| !currentEvent
)
459 return std::find_if(newPressedHotkeys
.begin(), newPressedHotkeys
.end(),
460 [&keyname
](const PressedHotkey
& v
){ return v
.mapping
->name
== keyname
; }) != newPressedHotkeys
.end();
463 void ResetActiveHotkeys()
465 newPressedHotkeys
.clear();
466 for (const PressedHotkey
& hotkey
: pressedHotkeys
)
468 SDL_Event_ hotkeyNotification
;
469 hotkeyNotification
.ev
.type
= hotkey
.retriggered
? SDL_HOTKEYUP_SILENT
: SDL_HOTKEYUP
;
470 hotkeyNotification
.ev
.user
.data1
= const_cast<char*>(hotkey
.mapping
->name
.c_str());
471 in_push_priority_event(&hotkeyNotification
);
473 pressedHotkeys
.clear();
474 activeScancodes
.clear();
475 currentEvent
= nullptr;
478 bool HotkeyIsPressed(const CStr
& keyname
)
480 return g_HotkeyStatus
[keyname
];