Line data Source code
1 : /* Copyright (C) 2021 Wildfire Games.
2 : * This file is part of 0 A.D.
3 : *
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.
8 : *
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.
13 : *
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/>.
16 : */
17 :
18 : #include "precompiled.h"
19 : #include "Hotkey.h"
20 :
21 : #include <boost/tokenizer.hpp>
22 :
23 : #include "lib/external_libraries/libsdl.h"
24 : #include "ps/CConsole.h"
25 : #include "ps/CLogger.h"
26 : #include "ps/CStr.h"
27 : #include "ps/ConfigDB.h"
28 : #include "ps/Globals.h"
29 : #include "ps/KeyName.h"
30 :
31 : static bool unified[UNIFIED_LAST - UNIFIED_SHIFT];
32 :
33 1 : std::unordered_map<int, KeyMapping> g_HotkeyMap;
34 :
35 : namespace {
36 1 : std::unordered_map<std::string, bool> g_HotkeyStatus;
37 :
38 : struct PressedHotkey
39 : {
40 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).
44 : bool retriggered;
45 : };
46 :
47 : struct ReleasedHotkey
48 : {
49 19 : ReleasedHotkey(const char* n, bool t) : name(n), wasRetriggered(t) {};
50 : const char* name;
51 : bool wasRetriggered;
52 : };
53 :
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 1 : 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;
62 :
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 1 : std::vector<PressedHotkey> pressedHotkeys;
67 :
68 : // List of active keys relevant for hotkeys.
69 1 : std::vector<SDL_Scancode_> activeScancodes;
70 : }
71 :
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);
75 :
76 : // Look up each key binding in the config file and set the mappings for
77 : // all key combinations that trigger it.
78 4 : static void LoadConfigBindings(CConfigDB& configDB)
79 : {
80 16 : for (const std::pair<const CStr, CConfigValueSet>& configPair : configDB.GetValuesWithPrefix(CFG_COMMAND, "hotkey."))
81 : {
82 24 : std::string hotkeyName = configPair.first.substr(7); // strip the "hotkey." prefix
83 :
84 : // "unused" is kept or the A23->24 migration, this can likely be removed in A25.
85 12 : if (configPair.second.empty() || (configPair.second.size() == 1 && configPair.second.front() == "unused"))
86 : {
87 : // Unused hotkeys must still be registered in the map to appear in the hotkey editor.
88 0 : SHotkeyMapping unusedCode;
89 0 : unusedCode.name = hotkeyName;
90 0 : unusedCode.primary = SKey{ UNUSED_HOTKEY_CODE };
91 0 : g_HotkeyMap[UNUSED_HOTKEY_CODE].push_back(unusedCode);
92 0 : continue;
93 : }
94 :
95 27 : for (const CStr& hotkey : configPair.second)
96 : {
97 30 : std::vector<SKey> keyCombination;
98 :
99 : // Iterate through multiple-key bindings (e.g. Ctrl+I)
100 30 : boost::char_separator<char> sep("+");
101 : typedef boost::tokenizer<boost::char_separator<char> > tokenizer;
102 30 : tokenizer tok(hotkey, sep);
103 40 : for (tokenizer::iterator it = tok.begin(); it != tok.end(); ++it)
104 : {
105 : // Attempt decode as key name
106 25 : SDL_Scancode scancode = FindScancode(it->c_str());
107 25 : if (!scancode)
108 : {
109 0 : LOGWARNING("Hotkey mapping used invalid key '%s'", hotkey.c_str());
110 0 : continue;
111 : }
112 :
113 25 : SKey key = { scancode };
114 25 : keyCombination.push_back(key);
115 : }
116 :
117 15 : std::vector<SKey>::iterator itKey, itKey2;
118 40 : for (itKey = keyCombination.begin(); itKey != keyCombination.end(); ++itKey)
119 : {
120 50 : SHotkeyMapping bindCode;
121 :
122 25 : bindCode.name = hotkeyName;
123 25 : bindCode.primary = SKey{ itKey->code };
124 :
125 74 : for (itKey2 = keyCombination.begin(); itKey2 != keyCombination.end(); ++itKey2)
126 49 : if (itKey != itKey2) // Push any auxiliary keys
127 24 : bindCode.requires.push_back(*itKey2);
128 :
129 25 : g_HotkeyMap[itKey->code].push_back(bindCode);
130 : }
131 : }
132 : }
133 4 : }
134 :
135 4 : void LoadHotkeys(CConfigDB& configDB)
136 : {
137 4 : pressedHotkeys.clear();
138 4 : LoadConfigBindings(configDB);
139 4 : }
140 :
141 5 : void UnloadHotkeys()
142 : {
143 5 : pressedHotkeys.clear();
144 5 : g_HotkeyMap.clear();
145 5 : g_HotkeyStatus.clear();
146 5 : }
147 :
148 85 : bool isPressed(const SKey& key)
149 : {
150 : // Normal keycodes are below EXTRA_KEYS_BASE
151 85 : if ((int)key.code < EXTRA_KEYS_BASE)
152 85 : return g_scancodes[key.code];
153 : // Mouse 'keycodes' are after the modifier keys
154 0 : else if ((int)key.code < MOUSE_LAST && (int)key.code > MOUSE_BASE)
155 0 : return g_mouse_buttons[key.code - MOUSE_BASE];
156 : // Modifier keycodes are between the normal keys and the mouse 'keys'
157 0 : else if ((int)key.code < UNIFIED_LAST && (int)key.code > SDL_NUM_SCANCODES)
158 0 : return unified[key.code - UNIFIED_SHIFT];
159 : // This codepath shouldn't be taken, but not having it triggers warnings.
160 : else
161 0 : return false;
162 : }
163 :
164 78 : InReaction HotkeyStateChange(const SDL_Event_* ev)
165 : {
166 78 : if (ev->ev.type == SDL_HOTKEYPRESS || ev->ev.type == SDL_HOTKEYPRESS_SILENT)
167 33 : g_HotkeyStatus[static_cast<const char*>(ev->ev.user.data1)] = true;
168 45 : else if (ev->ev.type == SDL_HOTKEYUP || ev->ev.type == SDL_HOTKEYUP_SILENT)
169 19 : g_HotkeyStatus[static_cast<const char*>(ev->ev.user.data1)] = false;
170 78 : return IN_PASS;
171 : }
172 :
173 44 : InReaction HotkeyInputPrepHandler(const SDL_Event_* ev)
174 : {
175 44 : int scancode = SDL_SCANCODE_UNKNOWN;
176 :
177 : // Restore default state.
178 44 : newPressedHotkeys.clear();
179 44 : currentEvent = nullptr;
180 :
181 44 : switch(ev->ev.type)
182 : {
183 40 : case SDL_KEYDOWN:
184 : case SDL_KEYUP:
185 40 : scancode = ev->ev.key.keysym.scancode;
186 40 : break;
187 :
188 0 : 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 0 : if (ev->ev.button.button >= SDL_BUTTON_X1)
193 0 : scancode = MOUSE_BASE + (int)ev->ev.button.button + 2;
194 : else
195 0 : scancode = MOUSE_BASE + (int)ev->ev.button.button;
196 0 : break;
197 :
198 0 : case SDL_MOUSEWHEEL:
199 0 : if (ev->ev.wheel.y > 0)
200 : {
201 0 : scancode = MOUSE_WHEELUP;
202 0 : break;
203 : }
204 0 : else if (ev->ev.wheel.y < 0)
205 : {
206 0 : scancode = MOUSE_WHEELDOWN;
207 0 : break;
208 : }
209 0 : else if (ev->ev.wheel.x > 0)
210 : {
211 0 : scancode = MOUSE_X2;
212 0 : break;
213 : }
214 0 : else if (ev->ev.wheel.x < 0)
215 : {
216 0 : scancode = MOUSE_X1;
217 0 : break;
218 : }
219 0 : return IN_PASS;
220 :
221 :
222 4 : default:
223 4 : return IN_PASS;
224 : }
225 :
226 : // Somewhat hackish:
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.
229 :
230 : SDL_Event_ phantom;
231 40 : phantom.ev.type = ((ev->ev.type == SDL_KEYDOWN) || (ev->ev.type == SDL_MOUSEBUTTONDOWN)) ? SDL_KEYDOWN : SDL_KEYUP;
232 40 : if (phantom.ev.type == SDL_KEYDOWN)
233 22 : phantom.ev.key.repeat = ev->ev.type == SDL_KEYDOWN ? ev->ev.key.repeat : 0;
234 :
235 40 : if (scancode == SDL_SCANCODE_LSHIFT || scancode == SDL_SCANCODE_RSHIFT)
236 : {
237 0 : phantom.ev.key.keysym.scancode = static_cast<SDL_Scancode>(UNIFIED_SHIFT);
238 0 : unified[0] = (phantom.ev.type == SDL_KEYDOWN);
239 0 : return HotkeyInputPrepHandler(&phantom);
240 : }
241 40 : else if (scancode == SDL_SCANCODE_LCTRL || scancode == SDL_SCANCODE_RCTRL)
242 : {
243 0 : phantom.ev.key.keysym.scancode = static_cast<SDL_Scancode>(UNIFIED_CTRL);
244 0 : unified[1] = (phantom.ev.type == SDL_KEYDOWN);
245 0 : return HotkeyInputPrepHandler(&phantom);
246 : }
247 40 : else if (scancode == SDL_SCANCODE_LALT || scancode == SDL_SCANCODE_RALT)
248 : {
249 0 : phantom.ev.key.keysym.scancode = static_cast<SDL_Scancode>(UNIFIED_ALT);
250 0 : unified[2] = (phantom.ev.type == SDL_KEYDOWN);
251 0 : return HotkeyInputPrepHandler(&phantom);
252 : }
253 40 : else if (scancode == SDL_SCANCODE_LGUI || scancode == SDL_SCANCODE_RGUI)
254 : {
255 0 : phantom.ev.key.keysym.scancode = static_cast<SDL_Scancode>(UNIFIED_SUPER);
256 0 : unified[3] = (phantom.ev.type == SDL_KEYDOWN);
257 0 : return HotkeyInputPrepHandler(&phantom);
258 : }
259 :
260 : // Check whether we have any hotkeys registered that include this scancode.
261 40 : if (g_HotkeyMap.find(scancode) == g_HotkeyMap.end())
262 0 : return IN_PASS;
263 :
264 40 : currentEvent = ev;
265 :
266 : /**
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.
287 : */
288 :
289 40 : bool isReleasedKey = ev->ev.type == SDL_KEYUP || ev->ev.type == SDL_MOUSEBUTTONUP;
290 : // Wheel events are pressed & released in the same go.
291 40 : bool isInstantaneous = ev->ev.type == SDL_MOUSEWHEEL;
292 :
293 40 : if (!isInstantaneous)
294 : {
295 40 : 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 40 : if (isReleasedKey && it != activeScancodes.end())
298 18 : activeScancodes.erase(it);
299 22 : else if (!isReleasedKey && it == activeScancodes.end())
300 19 : activeScancodes.emplace_back(scancode);
301 : }
302 :
303 80 : std::vector<SDL_Scancode_> triggers;
304 40 : if (!isReleasedKey || isInstantaneous)
305 22 : triggers.push_back(scancode);
306 : else
307 : // If the key is released, we need to check all less precise hotkeys again, to see if we should retrigger some.
308 40 : for (SDL_Scancode_ code : activeScancodes)
309 22 : triggers.push_back(code);
310 :
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.
313 40 : closestMapMatch = 0;
314 84 : for (SDL_Scancode_ code : triggers)
315 123 : for (const SHotkeyMapping& hotkey : g_HotkeyMap[code])
316 : {
317 : // Ensure no duplications in the new list.
318 237 : if (std::find_if(newPressedHotkeys.begin(), newPressedHotkeys.end(),
319 284 : [&hotkey](const PressedHotkey& v){ return v.mapping->name == hotkey.name; }) != newPressedHotkeys.end())
320 7 : continue;
321 :
322 72 : bool accept = true;
323 89 : for (const SKey& k : hotkey.requires)
324 : {
325 54 : accept = isPressed(k);
326 54 : if (!accept)
327 37 : break;
328 : }
329 72 : if (!accept)
330 37 : continue;
331 :
332 : // Check if this is an equally precise or more precise match
333 35 : if (hotkey.requires.size() + 1 >= closestMapMatch)
334 : {
335 : // Check if more precise
336 35 : if (hotkey.requires.size() + 1 > closestMapMatch)
337 : {
338 : // Throw away the old less-precise matches
339 31 : newPressedHotkeys.clear();
340 31 : closestMapMatch = hotkey.requires.size() + 1;
341 : }
342 35 : newPressedHotkeys.emplace_back(&hotkey, isReleasedKey);
343 : }
344 : }
345 :
346 40 : return IN_PASS;
347 : }
348 :
349 44 : InReaction HotkeyInputActualHandler(const SDL_Event_* ev)
350 : {
351 44 : if (!currentEvent)
352 4 : return IN_PASS;
353 :
354 40 : bool isInstantaneous = ev->ev.type == SDL_MOUSEWHEEL;
355 :
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 80 : std::vector<ReleasedHotkey> releasedHotkeys;
359 :
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 40 : if (!isInstantaneous)
364 : {
365 73 : for (PressedHotkey& hotkey : pressedHotkeys)
366 : {
367 66 : bool addingAnew = std::find_if(newPressedHotkeys.begin(), newPressedHotkeys.end(),
368 126 : [&hotkey](const PressedHotkey& v){ return v.mapping->name == hotkey.mapping->name; }) != newPressedHotkeys.end();
369 :
370 : // Update the triggered status to match our current state.
371 33 : if (addingAnew)
372 18 : std::find_if(newPressedHotkeys.begin(), newPressedHotkeys.end(),
373 28 : [&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 29 : else if (hotkey.mapping->requires.size() + 1 < closestMapMatch)
376 : {
377 5 : releasedHotkeys.emplace_back(hotkey.mapping->name.c_str(), hotkey.retriggered);
378 5 : continue;
379 : }
380 :
381 : // Check that the hotkey still matches all active keys.
382 28 : bool accept = isPressed(hotkey.mapping->primary);
383 28 : if (accept)
384 15 : for (const SKey& k : hotkey.mapping->requires)
385 : {
386 3 : accept = isPressed(k);
387 3 : if (!accept)
388 1 : break;
389 : }
390 28 : if (!accept && !addingAnew)
391 14 : releasedHotkeys.emplace_back(hotkey.mapping->name.c_str(), hotkey.retriggered);
392 14 : else if (accept)
393 : {
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 12 : if (hotkey.mapping->requires.size() + 1 > closestMapMatch)
398 : {
399 3 : closestMapMatch = hotkey.mapping->requires.size() + 1;
400 3 : newPressedHotkeys.clear();
401 3 : newPressedHotkeys.emplace_back(hotkey.mapping, hotkey.retriggered);
402 : }
403 9 : else if (!addingAnew)
404 2 : newPressedHotkeys.emplace_back(hotkey.mapping, hotkey.retriggered);
405 : }
406 : }
407 :
408 40 : pressedHotkeys.swap(newPressedHotkeys);
409 : }
410 :
411 74 : for (const PressedHotkey& hotkey : isInstantaneous ? newPressedHotkeys : pressedHotkeys)
412 : {
413 : // Send a KeyPress event when a hotkey is pressed initially and on mouseButton and mouseWheel events.
414 34 : if (ev->ev.type != SDL_KEYDOWN || ev->ev.key.repeat == 0)
415 : {
416 : SDL_Event_ hotkeyPressNotification;
417 33 : hotkeyPressNotification.ev.type = hotkey.retriggered ? SDL_HOTKEYPRESS_SILENT : SDL_HOTKEYPRESS;
418 33 : hotkeyPressNotification.ev.user.data1 = const_cast<char*>(hotkey.mapping->name.c_str());
419 33 : in_push_priority_event(&hotkeyPressNotification);
420 : }
421 :
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 34 : if (ev->ev.key.repeat == 0 && hotkey.retriggered)
430 11 : continue;
431 : SDL_Event_ hotkeyDownNotification;
432 23 : hotkeyDownNotification.ev.type = SDL_HOTKEYDOWN;
433 23 : hotkeyDownNotification.ev.user.data1 = const_cast<char*>(hotkey.mapping->name.c_str());
434 23 : in_push_priority_event(&hotkeyDownNotification);
435 : }
436 :
437 : // Release instantaneous events (e.g. mouse wheel) right away.
438 40 : if (isInstantaneous)
439 0 : for (const PressedHotkey& hotkey : newPressedHotkeys)
440 0 : releasedHotkeys.emplace_back(hotkey.mapping->name.c_str(), false);
441 :
442 59 : for (const ReleasedHotkey& hotkey : releasedHotkeys)
443 : {
444 : SDL_Event_ hotkeyNotification;
445 19 : hotkeyNotification.ev.type = hotkey.wasRetriggered ? SDL_HOTKEYUP_SILENT : SDL_HOTKEYUP;
446 19 : hotkeyNotification.ev.user.data1 = const_cast<char*>(hotkey.name);
447 19 : in_push_priority_event(&hotkeyNotification);
448 : }
449 :
450 40 : return IN_PASS;
451 : }
452 :
453 0 : bool EventWillFireHotkey(const SDL_Event_* ev, const CStr& keyname)
454 : {
455 : // Sanity check of sort. This parameter mostly exists because it looks right from the caller's perspective.
456 0 : if (ev != currentEvent || !currentEvent)
457 0 : return false;
458 :
459 0 : return std::find_if(newPressedHotkeys.begin(), newPressedHotkeys.end(),
460 0 : [&keyname](const PressedHotkey& v){ return v.mapping->name == keyname; }) != newPressedHotkeys.end();
461 : }
462 :
463 0 : void ResetActiveHotkeys()
464 : {
465 0 : newPressedHotkeys.clear();
466 0 : for (const PressedHotkey& hotkey : pressedHotkeys)
467 : {
468 : SDL_Event_ hotkeyNotification;
469 0 : hotkeyNotification.ev.type = hotkey.retriggered ? SDL_HOTKEYUP_SILENT : SDL_HOTKEYUP;
470 0 : hotkeyNotification.ev.user.data1 = const_cast<char*>(hotkey.mapping->name.c_str());
471 0 : in_push_priority_event(&hotkeyNotification);
472 : }
473 0 : pressedHotkeys.clear();
474 0 : activeScancodes.clear();
475 0 : currentEvent = nullptr;
476 0 : }
477 :
478 92 : bool HotkeyIsPressed(const CStr& keyname)
479 : {
480 92 : return g_HotkeyStatus[keyname];
481 3 : }
|