openMSX
HotKey.cc
Go to the documentation of this file.
1#include "HotKey.hh"
4#include "CommandException.hh"
5#include "EventDistributor.hh"
6#include "CliComm.hh"
7#include "Event.hh"
8#include "TclArgParser.hh"
9#include "TclObject.hh"
10#include "SettingsConfig.hh"
11#include "one_of.hh"
12#include "outer.hh"
13#include "ranges.hh"
14#include "view.hh"
15#include "build-info.hh"
16#include <array>
17#include <cassert>
18#include <memory>
19
20using std::string;
21
22// This file implements all Tcl key bindings. These are the 'classical' hotkeys
23// (e.g. F12 to (un)mute sound) and the more recent input layers. The idea
24// behind an input layer is something like an OSD widget that (temporarily)
25// takes semi-exclusive access to the input. So while the widget is active
26// keyboard (and joystick) input is no longer passed to the emulated MSX.
27// However the classical hotkeys or the openMSX console still receive input.
28
29namespace openmsx {
30
31static constexpr bool META_HOT_KEYS =
32#ifdef __APPLE__
33 true;
34#else
35 false;
36#endif
37
39 GlobalCommandController& commandController_,
40 EventDistributor& eventDistributor_)
41 : RTSchedulable(rtScheduler)
42 , bindCmd (commandController_, *this, false)
43 , bindDefaultCmd (commandController_, *this, true)
44 , unbindCmd (commandController_, *this, false)
45 , unbindDefaultCmd(commandController_, *this, true)
46 , activateCmd (commandController_)
47 , deactivateCmd (commandController_)
48 , commandController(commandController_)
49 , eventDistributor(eventDistributor_)
50{
51 initDefaultBindings();
52
53 eventDistributor.registerEventListener(
55 eventDistributor.registerEventListener(
57 eventDistributor.registerEventListener(
59 eventDistributor.registerEventListener(
61 eventDistributor.registerEventListener(
63 eventDistributor.registerEventListener(
65 eventDistributor.registerEventListener(
67 eventDistributor.registerEventListener(
69 eventDistributor.registerEventListener(
71 eventDistributor.registerEventListener(
73 eventDistributor.registerEventListener(
75 eventDistributor.registerEventListener(
77 eventDistributor.registerEventListener(
79 eventDistributor.registerEventListener(
81}
82
84{
87 eventDistributor.unregisterEventListener(EventType::FILE_DROP, *this);
88 eventDistributor.unregisterEventListener(EventType::FOCUS, *this);
92 eventDistributor.unregisterEventListener(EventType::JOY_HAT, *this);
93 eventDistributor.unregisterEventListener(EventType::MOUSE_WHEEL, *this);
96 eventDistributor.unregisterEventListener(EventType::MOUSE_MOTION, *this);
97 eventDistributor.unregisterEventListener(EventType::KEY_UP, *this);
98 eventDistributor.unregisterEventListener(EventType::KEY_DOWN, *this);
99}
100
101void HotKey::initDefaultBindings()
102{
103 // TODO move to Tcl script?
104
105 if constexpr (META_HOT_KEYS) {
106 // Hot key combos using Mac's Command key.
107 bindDefault(HotKeyInfo(Event::create<KeyDownEvent>(
109 "screenshot -guess-name"));
110 bindDefault(HotKeyInfo(Event::create<KeyDownEvent>(
112 "toggle pause"));
113 bindDefault(HotKeyInfo(Event::create<KeyDownEvent>(
115 "toggle fastforward"));
116 bindDefault(HotKeyInfo(Event::create<KeyDownEvent>(
118 "toggle console"));
119 bindDefault(HotKeyInfo(Event::create<KeyDownEvent>(
121 "toggle mute"));
122 bindDefault(HotKeyInfo(Event::create<KeyDownEvent>(
124 "toggle fullscreen"));
125 bindDefault(HotKeyInfo(Event::create<KeyDownEvent>(
127 "exit"));
128 } else {
129 // Hot key combos for typical PC keyboards.
130 bindDefault(HotKeyInfo(Event::create<KeyDownEvent>(Keys::K_PRINT),
131 "screenshot -guess-name"));
132 bindDefault(HotKeyInfo(Event::create<KeyDownEvent>(Keys::K_PAUSE),
133 "toggle pause"));
134 bindDefault(HotKeyInfo(Event::create<KeyDownEvent>(Keys::K_F9),
135 "toggle fastforward"));
136 bindDefault(HotKeyInfo(Event::create<KeyDownEvent>(Keys::K_F10),
137 "toggle console"));
138 bindDefault(HotKeyInfo(Event::create<KeyDownEvent>(Keys::K_F11),
139 "toggle fullscreen"));
140 bindDefault(HotKeyInfo(Event::create<KeyDownEvent>(Keys::K_F12),
141 "toggle mute"));
142 bindDefault(HotKeyInfo(Event::create<KeyDownEvent>(
144 "exit"));
145 bindDefault(HotKeyInfo(Event::create<KeyDownEvent>(
147 "exit"));
148 bindDefault(HotKeyInfo(Event::create<KeyDownEvent>(
150 "toggle fullscreen"));
151 // and for Android
152 bindDefault(HotKeyInfo(Event::create<KeyDownEvent>(Keys::K_BACK),
153 "quitmenu::quit_menu"));
154 }
155}
156
157static Event createEvent(const TclObject& obj, Interpreter& interp)
158{
159 auto event = InputEventFactory::createInputEvent(obj, interp);
167 throw CommandException("Unsupported event type");
168 }
169 return event;
170}
171static Event createEvent(std::string_view str, Interpreter& interp)
172{
173 return createEvent(TclObject(str), interp);
174}
175
177{
178 // restore default bindings
179 unboundKeys.clear();
180 boundKeys.clear();
181 cmdMap = defaultMap;
182}
183
184void HotKey::loadBind(std::string_view key, std::string_view cmd, bool repeat, bool event)
185{
186 bind(HotKeyInfo(createEvent(key, commandController.getInterpreter()),
187 std::string(cmd), repeat, event));
188}
189
190void HotKey::loadUnbind(std::string_view key)
191{
192 unbind(createEvent(key, commandController.getInterpreter()));
193}
194
196 EqualEvent(const Event& event_) : event(event_) {}
197 bool operator()(const Event& e) const {
198 return event == e;
199 }
200 bool operator()(const HotKey::HotKeyInfo& info) const {
201 return event == info.event;
202 }
203 const Event& event;
204};
205
206static bool contains(auto&& range, const Event& event)
207{
208 return ranges::any_of(range, EqualEvent(event));
209}
210
211template<typename T>
212static void erase(std::vector<T>& v, const Event& event)
213{
214 if (auto it = ranges::find_if(v, EqualEvent(event)); it != end(v)) {
215 move_pop_back(v, it);
216 }
217}
218
219static void insert(HotKey::KeySet& set, const Event& event)
220{
221 if (auto it = ranges::find_if(set, EqualEvent(event)); it != end(set)) {
222 *it = event;
223 } else {
224 set.push_back(event);
225 }
226}
227
228template<typename HotKeyInfo>
229static void insert(HotKey::BindMap& map, HotKeyInfo&& info)
230{
231 if (auto it = ranges::find_if(map, EqualEvent(info.event)); it != end(map)) {
232 *it = std::forward<HotKeyInfo>(info);
233 } else {
234 map.push_back(std::forward<HotKeyInfo>(info));
235 }
236}
237
238void HotKey::bind(HotKeyInfo&& info)
239{
240 erase(unboundKeys, info.event);
241 erase(defaultMap, info.event);
242 insert(boundKeys, info.event);
243 insert(cmdMap, std::move(info));
244}
245
246void HotKey::unbind(const Event& event)
247{
248 if (auto it1 = ranges::find_if(boundKeys, EqualEvent(event));
249 it1 == end(boundKeys)) {
250 // only when not a regular bound event
251 insert(unboundKeys, event);
252 } else {
253 //erase(boundKeys, *event);
254 move_pop_back(boundKeys, it1);
255 }
256
257 erase(defaultMap, event);
258 erase(cmdMap, event);
259}
260
261void HotKey::bindDefault(HotKeyInfo&& info)
262{
263 if (!contains( boundKeys, info.event) &&
264 !contains(unboundKeys, info.event)) {
265 // not explicitly bound or unbound
266 insert(cmdMap, info);
267 }
268 insert(defaultMap, std::move(info));
269}
270
271void HotKey::unbindDefault(const Event& event)
272{
273 if (!contains( boundKeys, event) &&
274 !contains(unboundKeys, event)) {
275 // not explicitly bound or unbound
276 erase(cmdMap, event);
277 }
278 erase(defaultMap, event);
279}
280
281void HotKey::bindLayer(HotKeyInfo&& info, const string& layer)
282{
283 insert(layerMap[layer], std::move(info));
284}
285
286void HotKey::unbindLayer(const Event& event, const string& layer)
287{
288 erase(layerMap[layer], event);
289}
290
291void HotKey::unbindFullLayer(const string& layer)
292{
293 layerMap.erase(layer);
294}
295
296void HotKey::activateLayer(std::string layer, bool blocking)
297{
298 // Insert new activation record at the end of the list.
299 // (it's not an error if the same layer was already active, in such
300 // as case it will now appear twice in the list of active layer,
301 // and it must also be deactivated twice).
302 activeLayers.push_back({std::move(layer), blocking});
303}
304
305void HotKey::deactivateLayer(std::string_view layer)
306{
307 // remove the first matching activation record from the end
308 // (it's not an error if there is no match at all)
309 if (auto it = ranges::find(view::reverse(activeLayers), layer, &LayerInfo::layer);
310 it != activeLayers.rend()) {
311 // 'reverse_iterator' -> 'iterator' conversion is a bit tricky
312 activeLayers.erase((it + 1).base());
313 }
314}
315
316static HotKey::BindMap::const_iterator findMatch(
317 const HotKey::BindMap& map, const Event& event)
318{
319 return ranges::find_if(map, [&](auto& p) {
320 return matches(p.event, event);
321 });
322}
323
324void HotKey::executeRT()
325{
326 if (lastEvent) executeEvent(lastEvent);
327}
328
329int HotKey::signalEvent(const Event& event)
330{
331 if (lastEvent.getPtr() != event.getPtr()) {
332 // If the newly received event is different from the repeating
333 // event, we stop the repeat process.
334 // Except when we're repeating a OsdControlEvent and the
335 // received event was actually the 'generating' event for the
336 // Osd event. E.g. a cursor-keyboard-down event will generate
337 // a corresponding osd event (the osd event is send before the
338 // original event). Without this hack, key-repeat will not work
339 // for osd key bindings.
340 if (lastEvent && isRepeatStopper(lastEvent, event)) {
341 stopRepeat();
342 }
343 }
344 return executeEvent(event);
345}
346
347int HotKey::executeEvent(const Event& event)
348{
349 // First search in active layers (from back to front)
350 bool blocking = false;
351 for (auto& info : view::reverse(activeLayers)) {
352 auto& cmap = layerMap[info.layer]; // ok, if this entry doesn't exist yet
353 if (auto it = findMatch(cmap, event); it != end(cmap)) {
354 executeBinding(event, *it);
355 // Deny event to MSX listeners, also don't pass event
356 // to other layers (including the default layer).
358 }
359 blocking = info.blocking;
360 if (blocking) break; // don't try lower layers
361 }
362
363 // If the event was not yet handled, try the default layer.
364 if (auto it = findMatch(cmdMap, event); it != end(cmdMap)) {
365 executeBinding(event, *it);
366 return EventDistributor::MSX; // deny event to MSX listeners
367 }
368
369 // Event is not handled, only let it pass to the MSX if there was no
370 // blocking layer active.
371 return blocking ? EventDistributor::MSX : 0;
372}
373
374void HotKey::executeBinding(const Event& event, const HotKeyInfo& info)
375{
376 if (info.repeat) {
377 startRepeat(event);
378 }
379 try {
380 // Make a copy of the command string because executing the
381 // command could potentially execute (un)bind commands so
382 // that the original string becomes invalid.
383 // Valgrind complained about this in the following scenario:
384 // - open the OSD menu
385 // - activate the 'Exit openMSX' item
386 // The latter is triggered from e.g. a 'OSDControl A PRESS'
387 // event. The Tcl script bound to that event closes the main
388 // menu and reopens a new quit_menu. This will re-bind the
389 // action for the 'OSDControl A PRESS' event.
390 TclObject command(info.command);
391 if (info.passEvent) {
392 // Add event as the last argument to the command.
393 // (If needed) the current command string is first
394 // converted to a list (thus split in a command name
395 // and arguments).
396 command.addListElement(toTclList(event));
397 }
398
399 // ignore return value
400 command.executeCommand(commandController.getInterpreter());
401 } catch (CommandException& e) {
402 commandController.getCliComm().printWarning(
403 "Error executing hot key command: ", e.getMessage());
404 }
405}
406
407void HotKey::startRepeat(const Event& event)
408{
409 // I initially thought about using the builtin SDL key-repeat feature,
410 // but that won't work for example on joystick buttons. So we have to
411 // code it ourselves.
412
413 // On android, because of the sensitivity of the touch screen it's
414 // very hard to have touches of short durations. So half a second is
415 // too short for the key-repeat-delay. A full second should be fine.
416 static constexpr unsigned DELAY = PLATFORM_ANDROID ? 1000 : 500;
417 // Repeat period.
418 static constexpr unsigned PERIOD = 30;
419
420 unsigned delay = (lastEvent ? PERIOD : DELAY) * 1000;
421 lastEvent = event;
422 scheduleRT(delay);
423}
424
425void HotKey::stopRepeat()
426{
427 lastEvent = Event{};
428 cancelRT();
429}
430
431
432// class BindCmd
433
434static constexpr std::string_view getBindCmdName(bool defaultCmd)
435{
436 return defaultCmd ? "bind_default" : "bind";
437}
438
439HotKey::BindCmd::BindCmd(CommandController& commandController_, HotKey& hotKey_,
440 bool defaultCmd_)
441 : Command(commandController_, getBindCmdName(defaultCmd_))
442 , hotKey(hotKey_)
443 , defaultCmd(defaultCmd_)
444{
445}
446
447static string formatBinding(const HotKey::HotKeyInfo& info)
448{
449 return strCat(toString(info.event), (info.repeat ? " [repeat]" : ""),
450 (info.passEvent ? " [event]" : ""), ": ", info.command, '\n');
451}
452
453void HotKey::BindCmd::execute(std::span<const TclObject> tokens, TclObject& result)
454{
455 string layer;
456 bool layers = false;
457 bool repeat = false;
458 bool passEvent = false;
459 std::array parserInfo = {
460 valueArg("-layer", layer),
461 flagArg("-layers", layers),
462 flagArg("-repeat", repeat),
463 flagArg("-event", passEvent),
464 };
465 auto arguments = parseTclArgs(getInterpreter(), tokens.subspan<1>(), parserInfo);
466 if (defaultCmd && !layer.empty()) {
467 throw CommandException("Layers are not supported for default bindings");
468 }
469
470 auto& cMap = defaultCmd
471 ? hotKey.defaultMap
472 : layer.empty() ? hotKey.cmdMap
473 : hotKey.layerMap[layer];
474
475 if (layers) {
476 for (const auto& [layerName, bindings] : hotKey.layerMap) {
477 // An alternative for this test is to always properly
478 // prune layerMap. ATM this approach seems simpler.
479 if (!bindings.empty()) {
480 result.addListElement(layerName);
481 }
482 }
483 return;
484 }
485
486 switch (arguments.size()) {
487 case 0: {
488 // show all bounded keys (for this layer)
489 string r;
490 for (auto& p : cMap) {
491 r += formatBinding(p);
492 }
493 result = r;
494 break;
495 }
496 case 1: {
497 // show bindings for this key (in this layer)
498 auto it = ranges::find_if(cMap,
499 EqualEvent(createEvent(arguments[0], getInterpreter())));
500 if (it == end(cMap)) {
501 throw CommandException("Key not bound");
502 }
503 result = formatBinding(*it);
504 break;
505 }
506 default: {
507 // make a new binding
508 string command(arguments[1].getString());
509 for (const auto& arg : view::drop(arguments, 2)) {
510 strAppend(command, ' ', arg.getString());
511 }
512 HotKey::HotKeyInfo info(
513 createEvent(arguments[0], getInterpreter()),
514 command, repeat, passEvent);
515 if (defaultCmd) {
516 hotKey.bindDefault(std::move(info));
517 } else if (layer.empty()) {
518 hotKey.bind(std::move(info));
519 } else {
520 hotKey.bindLayer(std::move(info), layer);
521 }
522 break;
523 }
524 }
525}
526string HotKey::BindCmd::help(std::span<const TclObject> /*tokens*/) const
527{
528 auto cmd = getBindCmdName(defaultCmd);
529 return strCat(
530 cmd, " : show all bounded keys\n",
531 cmd, " <key> : show binding for this key\n",
532 cmd, " <key> [-repeat] [-event] <cmd> : bind key to command, optionally "
533 "repeat command while key remains pressed and also optionally "
534 "give back the event as argument (a list) to <cmd>\n"
535 "These 3 take an optional '-layer <layername>' option, "
536 "see activate_input_layer.\n",
537 cmd, " -layers : show a list of layers with bound keys\n");
538}
539
540
541// class UnbindCmd
542
543static constexpr std::string_view getUnbindCmdName(bool defaultCmd)
544{
545 return defaultCmd ? "unbind_default" : "unbind";
546}
547
548HotKey::UnbindCmd::UnbindCmd(CommandController& commandController_,
549 HotKey& hotKey_, bool defaultCmd_)
550 : Command(commandController_, getUnbindCmdName(defaultCmd_))
551 , hotKey(hotKey_)
552 , defaultCmd(defaultCmd_)
553{
554}
555
556void HotKey::UnbindCmd::execute(std::span<const TclObject> tokens, TclObject& /*result*/)
557{
558 string layer;
559 std::array info = {valueArg("-layer", layer)};
560 auto arguments = parseTclArgs(getInterpreter(), tokens.subspan<1>(), info);
561 if (defaultCmd && !layer.empty()) {
562 throw CommandException("Layers are not supported for default bindings");
563 }
564
565 if ((arguments.size() > 1) || (layer.empty() && (arguments.size() != 1))) {
566 throw SyntaxError();
567 }
568
569 Event event;
570 if (arguments.size() == 1) {
571 event = createEvent(arguments[0], getInterpreter());
572 }
573
574 if (defaultCmd) {
575 assert(event);
576 hotKey.unbindDefault(event);
577 } else if (layer.empty()) {
578 assert(event);
579 hotKey.unbind(event);
580 } else {
581 if (event) {
582 hotKey.unbindLayer(event, layer);
583 } else {
584 hotKey.unbindFullLayer(layer);
585 }
586 }
587}
588string HotKey::UnbindCmd::help(std::span<const TclObject> /*tokens*/) const
589{
590 auto cmd = getUnbindCmdName(defaultCmd);
591 return strCat(
592 cmd, " <key> : unbind this key\n",
593 cmd, " -layer <layername> <key> : unbind key in a specific layer\n",
594 cmd, " -layer <layername> : unbind all keys in this layer\n");
595}
596
597
598// class ActivateCmd
599
600HotKey::ActivateCmd::ActivateCmd(CommandController& commandController_)
601 : Command(commandController_, "activate_input_layer")
602{
603}
604
605void HotKey::ActivateCmd::execute(std::span<const TclObject> tokens, TclObject& result)
606{
607 bool blocking = false;
608 std::array info = {flagArg("-blocking", blocking)};
609 auto args = parseTclArgs(getInterpreter(), tokens.subspan(1), info);
610
611 auto& hotKey = OUTER(HotKey, activateCmd);
612 switch (args.size()) {
613 case 0: {
614 string r;
615 for (auto& layerInfo : view::reverse(hotKey.activeLayers)) {
616 r += layerInfo.layer;
617 if (layerInfo.blocking) {
618 r += " -blocking";
619 }
620 r += '\n';
621 }
622 result = r;
623 break;
624 }
625 case 1: {
626 std::string_view layer = args[0].getString();
627 hotKey.activateLayer(string(layer), blocking);
628 break;
629 }
630 default:
631 throw SyntaxError();
632 }
633}
634
635string HotKey::ActivateCmd::help(std::span<const TclObject> /*tokens*/) const
636{
637 return "activate_input_layer "
638 ": show list of active layers (most recent on top)\n"
639 "activate_input_layer [-blocking] <layername> "
640 ": activate new layer, optionally in blocking mode\n";
641}
642
643
644// class DeactivateCmd
645
646HotKey::DeactivateCmd::DeactivateCmd(CommandController& commandController_)
647 : Command(commandController_, "deactivate_input_layer")
648{
649}
650
651void HotKey::DeactivateCmd::execute(std::span<const TclObject> tokens, TclObject& /*result*/)
652{
653 checkNumArgs(tokens, 2, "layer");
654 auto& hotKey = OUTER(HotKey, deactivateCmd);
655 hotKey.deactivateLayer(tokens[1].getString());
656}
657
658string HotKey::DeactivateCmd::help(std::span<const TclObject> /*tokens*/) const
659{
660 return "deactivate_input_layer <layername> : deactivate the given input layer";
661}
662
663
664} // namespace openmsx
#define PLATFORM_ANDROID
Definition: build-info.hh:17
Definition: one_of.hh:7
void printWarning(std::string_view message)
Definition: CliComm.cc:10
void unregisterEventListener(EventType type, EventListener &listener)
Unregisters a previously registered event listener.
void registerEventListener(EventType type, EventListener &listener, Priority priority=OTHER)
Registers a given object to receive certain events.
const RcEvent * getPtr() const
Definition: Event.hh:41
HotKey(RTScheduler &rtScheduler, GlobalCommandController &commandController, EventDistributor &eventDistributor)
Definition: HotKey.cc:38
void loadUnbind(std::string_view key)
Definition: HotKey.cc:190
std::vector< HotKeyInfo > BindMap
Definition: HotKey.hh:34
void loadBind(std::string_view key, std::string_view cmd, bool repeat, bool event)
Definition: HotKey.cc:184
void loadInit()
Definition: HotKey.cc:176
std::vector< Event > KeySet
Definition: HotKey.hh:35
void scheduleRT(uint64_t delta)
constexpr double e
Definition: Math.hh:21
Event createInputEvent(const TclObject &str, Interpreter &interp)
KeyCode combine(KeyCode key, KeyCode modifier)
Convenience method to create key combinations (hides ugly casts).
Definition: Keys.hh:234
@ K_RETURN
Definition: Keys.hh:34
This file implemented 3 utility functions:
Definition: Autofire.cc:9
bool isRepeatStopper(const Event &self, const Event &other)
Should 'bind -repeat' be stopped by 'other' event.
Definition: Event.cc:185
bool matches(const Event &self, const Event &other)
Does this event 'match' the given event.
Definition: Event.cc:216
ArgsInfo valueArg(std::string_view name, T &value)
Definition: TclArgParser.hh:85
std::vector< TclObject > parseTclArgs(Interpreter &interp, std::span< const TclObject > inArgs, std::span< const ArgsInfo > table)
TclObject toTclList(const Event &event)
Similar to toString(), but retains the structure of the event.
Definition: Event.cc:85
EventType getType(const Event &event)
Definition: Event.hh:647
ArgsInfo flagArg(std::string_view name, bool &flag)
Definition: TclArgParser.hh:72
std::string toString(const Event &event)
Get a string representation of this event.
Definition: Event.cc:180
bool any_of(InputRange &&range, UnaryPredicate pred)
Definition: ranges.hh:192
auto find_if(InputRange &&range, UnaryPredicate pred)
Definition: ranges.hh:173
auto find(InputRange &&range, const T &value)
Definition: ranges.hh:160
constexpr auto reverse(Range &&range)
Definition: view.hh:452
constexpr auto drop(Range &&range, size_t n)
Definition: view.hh:440
#define OUTER(type, member)
Definition: outer.hh:41
void move_pop_back(VECTOR &v, typename VECTOR::iterator it)
Erase the pointed to element from the given vector.
Definition: stl.hh:125
std::string strCat(Ts &&...ts)
Definition: strCat.hh:542
void strAppend(std::string &result, Ts &&...ts)
Definition: strCat.hh:620
const Event & event
Definition: HotKey.cc:203
EqualEvent(const Event &event_)
Definition: HotKey.cc:196
bool operator()(const HotKey::HotKeyInfo &info) const
Definition: HotKey.cc:200
bool operator()(const Event &e) const
Definition: HotKey.cc:197
constexpr void repeat(T n, Op op)
Repeat the given operation 'op' 'n' times.
Definition: xrange.hh:147
constexpr auto end(const zstring_view &x)