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::WINDOW, *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(KeyDownEvent::create(SDLK_d, KMOD_GUI),
108 "screenshot -guess-name"));
109 bindDefault(HotKeyInfo(KeyDownEvent::create(SDLK_p, KMOD_GUI),
110 "toggle pause"));
111 bindDefault(HotKeyInfo(KeyDownEvent::create(SDLK_t, KMOD_GUI),
112 "toggle fastforward"));
113 bindDefault(HotKeyInfo(KeyDownEvent::create(SDLK_l, KMOD_GUI),
114 "toggle console"));
115 bindDefault(HotKeyInfo(KeyDownEvent::create(SDLK_u, KMOD_GUI),
116 "toggle mute"));
117 bindDefault(HotKeyInfo(KeyDownEvent::create(SDLK_f, KMOD_GUI),
118 "toggle fullscreen"));
119 bindDefault(HotKeyInfo(KeyDownEvent::create(SDLK_q, KMOD_GUI),
120 "exit"));
121 } else {
122 // Hot key combos for typical PC keyboards.
123 bindDefault(HotKeyInfo(KeyDownEvent::create(SDLK_PRINTSCREEN),
124 "screenshot -guess-name"));
125 bindDefault(HotKeyInfo(KeyDownEvent::create(SDLK_PAUSE),
126 "toggle pause"));
127 bindDefault(HotKeyInfo(KeyDownEvent::create(SDLK_F9),
128 "toggle fastforward"));
129 bindDefault(HotKeyInfo(KeyDownEvent::create(SDLK_F10),
130 "toggle console"));
131 bindDefault(HotKeyInfo(KeyDownEvent::create(SDLK_F11),
132 "toggle fullscreen"));
133 bindDefault(HotKeyInfo(KeyDownEvent::create(SDLK_F12),
134 "toggle mute"));
135 bindDefault(HotKeyInfo(KeyDownEvent::create(SDLK_F4, KMOD_ALT),
136 "exit"));
137 bindDefault(HotKeyInfo(KeyDownEvent::create(SDLK_PAUSE, KMOD_CTRL),
138 "exit"));
139 bindDefault(HotKeyInfo(KeyDownEvent::create(SDLK_RETURN, KMOD_ALT),
140 "toggle fullscreen"));
141 }
142}
143
144static Event createEvent(const TclObject& obj, Interpreter& interp)
145{
146 auto event = InputEventFactory::createInputEvent(obj, interp);
154 throw CommandException("Unsupported event type");
155 }
156 return event;
157}
158static Event createEvent(std::string_view str, Interpreter& interp)
159{
160 return createEvent(TclObject(str), interp);
161}
162
164{
165 // restore default bindings
166 unboundKeys.clear();
167 boundKeys.clear();
168 cmdMap = defaultMap;
169}
170
171void HotKey::loadBind(std::string_view key, std::string_view cmd, bool repeat, bool event)
172{
173 bind(HotKeyInfo(createEvent(key, commandController.getInterpreter()),
174 std::string(cmd), repeat, event));
175}
176
177void HotKey::loadUnbind(std::string_view key)
178{
179 unbind(createEvent(key, commandController.getInterpreter()));
180}
181
183 EqualEvent(const Event& event_) : event(event_) {}
184 bool operator()(const Event& e) const {
185 return event == e;
186 }
187 bool operator()(const HotKey::HotKeyInfo& info) const {
188 return event == info.event;
189 }
190 const Event& event;
191};
192
193static bool contains(auto&& range, const Event& event)
194{
195 return ranges::any_of(range, EqualEvent(event));
196}
197
198template<typename T>
199static void erase(std::vector<T>& v, const Event& event)
200{
201 if (auto it = ranges::find_if(v, EqualEvent(event)); it != end(v)) {
202 move_pop_back(v, it);
203 }
204}
205
206static void insert(HotKey::KeySet& set, const Event& event)
207{
208 if (auto it = ranges::find_if(set, EqualEvent(event)); it != end(set)) {
209 *it = event;
210 } else {
211 set.push_back(event);
212 }
213}
214
215template<typename HotKeyInfo>
216static void insert(HotKey::BindMap& map, HotKeyInfo&& info)
217{
218 if (auto it = ranges::find_if(map, EqualEvent(info.event)); it != end(map)) {
219 *it = std::forward<HotKeyInfo>(info);
220 } else {
221 map.push_back(std::forward<HotKeyInfo>(info));
222 }
223}
224
225void HotKey::bind(HotKeyInfo&& info)
226{
227 erase(unboundKeys, info.event);
228 erase(defaultMap, info.event);
229 insert(boundKeys, info.event);
230 insert(cmdMap, std::move(info));
231}
232
233void HotKey::unbind(const Event& event)
234{
235 if (auto it1 = ranges::find_if(boundKeys, EqualEvent(event));
236 it1 == end(boundKeys)) {
237 // only when not a regular bound event
238 insert(unboundKeys, event);
239 } else {
240 //erase(boundKeys, *event);
241 move_pop_back(boundKeys, it1);
242 }
243
244 erase(defaultMap, event);
245 erase(cmdMap, event);
246}
247
248void HotKey::bindDefault(HotKeyInfo&& info)
249{
250 if (!contains( boundKeys, info.event) &&
251 !contains(unboundKeys, info.event)) {
252 // not explicitly bound or unbound
253 insert(cmdMap, info);
254 }
255 insert(defaultMap, std::move(info));
256}
257
258void HotKey::unbindDefault(const Event& event)
259{
260 if (!contains( boundKeys, event) &&
261 !contains(unboundKeys, event)) {
262 // not explicitly bound or unbound
263 erase(cmdMap, event);
264 }
265 erase(defaultMap, event);
266}
267
268void HotKey::bindLayer(HotKeyInfo&& info, const string& layer)
269{
270 insert(layerMap[layer], std::move(info));
271}
272
273void HotKey::unbindLayer(const Event& event, const string& layer)
274{
275 erase(layerMap[layer], event);
276}
277
278void HotKey::unbindFullLayer(const string& layer)
279{
280 layerMap.erase(layer);
281}
282
283void HotKey::activateLayer(std::string layer, bool blocking)
284{
285 // Insert new activation record at the end of the list.
286 // (it's not an error if the same layer was already active, in such
287 // as case it will now appear twice in the list of active layer,
288 // and it must also be deactivated twice).
289 activeLayers.push_back({std::move(layer), blocking});
290}
291
292void HotKey::deactivateLayer(std::string_view layer)
293{
294 // remove the first matching activation record from the end
295 // (it's not an error if there is no match at all)
296 if (auto it = ranges::find(view::reverse(activeLayers), layer, &LayerInfo::layer);
297 it != activeLayers.rend()) {
298 // 'reverse_iterator' -> 'iterator' conversion is a bit tricky
299 activeLayers.erase((it + 1).base());
300 }
301}
302
303static HotKey::BindMap::const_iterator findMatch(
304 const HotKey::BindMap& map, const Event& event)
305{
306 return ranges::find_if(map, [&](auto& p) {
307 return matches(p.event, event);
308 });
309}
310
311void HotKey::executeRT()
312{
313 if (lastEvent) executeEvent(*lastEvent);
314}
315
316int HotKey::signalEvent(const Event& event)
317{
318 if (lastEvent && *lastEvent != event) {
319 // If the newly received event is different from the repeating
320 // event, we stop the repeat process.
321 // Except when we're repeating a OsdControlEvent and the
322 // received event was actually the 'generating' event for the
323 // Osd event. E.g. a cursor-keyboard-down event will generate
324 // a corresponding osd event (the osd event is send before the
325 // original event). Without this hack, key-repeat will not work
326 // for osd key bindings.
327 stopRepeat();
328 }
329 return executeEvent(event);
330}
331
332int HotKey::executeEvent(const Event& event)
333{
334 // First search in active layers (from back to front)
335 bool blocking = false;
336 for (auto& info : view::reverse(activeLayers)) {
337 auto& cmap = layerMap[info.layer]; // ok, if this entry doesn't exist yet
338 if (auto it = findMatch(cmap, event); it != end(cmap)) {
339 executeBinding(event, *it);
340 // Deny event to MSX listeners, also don't pass event
341 // to other layers (including the default layer).
343 }
344 blocking = info.blocking;
345 if (blocking) break; // don't try lower layers
346 }
347
348 // If the event was not yet handled, try the default layer.
349 if (auto it = findMatch(cmdMap, event); it != end(cmdMap)) {
350 executeBinding(event, *it);
351 return EventDistributor::MSX; // deny event to MSX listeners
352 }
353
354 // Event is not handled, only let it pass to the MSX if there was no
355 // blocking layer active.
356 return blocking ? EventDistributor::MSX : 0;
357}
358
359void HotKey::executeBinding(const Event& event, const HotKeyInfo& info)
360{
361 if (info.repeat) {
362 startRepeat(event);
363 }
364 try {
365 // Make a copy of the command string because executing the
366 // command could potentially execute (un)bind commands so
367 // that the original string becomes invalid.
368 // Valgrind complained about this in the following scenario:
369 // - open the OSD menu
370 // - activate the 'Exit openMSX' item
371 // The latter is triggered from e.g. a 'OSDControl A PRESS'
372 // event. The Tcl script bound to that event closes the main
373 // menu and reopens a new quit_menu. This will re-bind the
374 // action for the 'OSDControl A PRESS' event.
375 TclObject command(info.command);
376 if (info.passEvent) {
377 // Add event as the last argument to the command.
378 // (If needed) the current command string is first
379 // converted to a list (thus split in a command name
380 // and arguments).
381 command.addListElement(toTclList(event));
382 }
383
384 // ignore return value
385 command.executeCommand(commandController.getInterpreter());
386 } catch (CommandException& e) {
387 commandController.getCliComm().printWarning(
388 "Error executing hot key command: ", e.getMessage());
389 }
390}
391
392void HotKey::startRepeat(const Event& event)
393{
394 // I initially thought about using the builtin SDL key-repeat feature,
395 // but that won't work for example on joystick buttons. So we have to
396 // code it ourselves.
397
398 // On android, because of the sensitivity of the touch screen it's
399 // very hard to have touches of short durations. So half a second is
400 // too short for the key-repeat-delay. A full second should be fine.
401 static constexpr unsigned DELAY = PLATFORM_ANDROID ? 1000 : 500;
402 // Repeat period.
403 static constexpr unsigned PERIOD = 30;
404
405 unsigned delay = (lastEvent ? PERIOD : DELAY) * 1000;
406 lastEvent = event;
407 scheduleRT(delay);
408}
409
410void HotKey::stopRepeat()
411{
412 lastEvent.reset();
413 cancelRT();
414}
415
416
417// class BindCmd
418
419static constexpr std::string_view getBindCmdName(bool defaultCmd)
420{
421 return defaultCmd ? "bind_default" : "bind";
422}
423
424HotKey::BindCmd::BindCmd(CommandController& commandController_, HotKey& hotKey_,
425 bool defaultCmd_)
426 : Command(commandController_, getBindCmdName(defaultCmd_))
427 , hotKey(hotKey_)
428 , defaultCmd(defaultCmd_)
429{
430}
431
432static string formatBinding(const HotKey::HotKeyInfo& info)
433{
434 return strCat(toString(info.event), (info.repeat ? " [repeat]" : ""),
435 (info.passEvent ? " [event]" : ""), ": ", info.command, '\n');
436}
437
438void HotKey::BindCmd::execute(std::span<const TclObject> tokens, TclObject& result)
439{
440 string layer;
441 bool layers = false;
442 bool repeat = false;
443 bool passEvent = false;
444 std::array parserInfo = {
445 valueArg("-layer", layer),
446 flagArg("-layers", layers),
447 flagArg("-repeat", repeat),
448 flagArg("-event", passEvent),
449 };
450 auto arguments = parseTclArgs(getInterpreter(), tokens.subspan<1>(), parserInfo);
451 if (defaultCmd && !layer.empty()) {
452 throw CommandException("Layers are not supported for default bindings");
453 }
454
455 auto& cMap = defaultCmd
456 ? hotKey.defaultMap
457 : layer.empty() ? hotKey.cmdMap
458 : hotKey.layerMap[layer];
459
460 if (layers) {
461 for (const auto& [layerName, bindings] : hotKey.layerMap) {
462 // An alternative for this test is to always properly
463 // prune layerMap. ATM this approach seems simpler.
464 if (!bindings.empty()) {
465 result.addListElement(layerName);
466 }
467 }
468 return;
469 }
470
471 switch (arguments.size()) {
472 case 0: {
473 // show all bounded keys (for this layer)
474 string r;
475 for (auto& p : cMap) {
476 r += formatBinding(p);
477 }
478 result = r;
479 break;
480 }
481 case 1: {
482 // show bindings for this key (in this layer)
483 auto it = ranges::find_if(cMap,
484 EqualEvent(createEvent(arguments[0], getInterpreter())));
485 if (it == end(cMap)) {
486 throw CommandException("Key not bound");
487 }
488 result = formatBinding(*it);
489 break;
490 }
491 default: {
492 // make a new binding
493 string command(arguments[1].getString());
494 for (const auto& arg : view::drop(arguments, 2)) {
495 strAppend(command, ' ', arg.getString());
496 }
497 HotKey::HotKeyInfo info(
498 createEvent(arguments[0], getInterpreter()),
499 command, repeat, passEvent);
500 if (defaultCmd) {
501 hotKey.bindDefault(std::move(info));
502 } else if (layer.empty()) {
503 hotKey.bind(std::move(info));
504 } else {
505 hotKey.bindLayer(std::move(info), layer);
506 }
507 break;
508 }
509 }
510}
511string HotKey::BindCmd::help(std::span<const TclObject> /*tokens*/) const
512{
513 auto cmd = getBindCmdName(defaultCmd);
514 return strCat(
515 cmd, " : show all bounded keys\n",
516 cmd, " <key> : show binding for this key\n",
517 cmd, " <key> [-repeat] [-event] <cmd> : bind key to command, optionally "
518 "repeat command while key remains pressed and also optionally "
519 "give back the event as argument (a list) to <cmd>\n"
520 "These 3 take an optional '-layer <layername>' option, "
521 "see activate_input_layer.\n",
522 cmd, " -layers : show a list of layers with bound keys\n");
523}
524
525
526// class UnbindCmd
527
528static constexpr std::string_view getUnbindCmdName(bool defaultCmd)
529{
530 return defaultCmd ? "unbind_default" : "unbind";
531}
532
533HotKey::UnbindCmd::UnbindCmd(CommandController& commandController_,
534 HotKey& hotKey_, bool defaultCmd_)
535 : Command(commandController_, getUnbindCmdName(defaultCmd_))
536 , hotKey(hotKey_)
537 , defaultCmd(defaultCmd_)
538{
539}
540
541void HotKey::UnbindCmd::execute(std::span<const TclObject> tokens, TclObject& /*result*/)
542{
543 string layer;
544 std::array info = {valueArg("-layer", layer)};
545 auto arguments = parseTclArgs(getInterpreter(), tokens.subspan<1>(), info);
546 if (defaultCmd && !layer.empty()) {
547 throw CommandException("Layers are not supported for default bindings");
548 }
549
550 if ((arguments.size() > 1) || (layer.empty() && (arguments.size() != 1))) {
551 throw SyntaxError();
552 }
553
554 std::optional<Event> event;
555 if (arguments.size() == 1) {
556 event = createEvent(arguments[0], getInterpreter());
557 }
558
559 if (defaultCmd) {
560 assert(event);
561 hotKey.unbindDefault(*event);
562 } else if (layer.empty()) {
563 assert(event);
564 hotKey.unbind(*event);
565 } else {
566 if (event) {
567 hotKey.unbindLayer(*event, layer);
568 } else {
569 hotKey.unbindFullLayer(layer);
570 }
571 }
572}
573string HotKey::UnbindCmd::help(std::span<const TclObject> /*tokens*/) const
574{
575 auto cmd = getUnbindCmdName(defaultCmd);
576 return strCat(
577 cmd, " <key> : unbind this key\n",
578 cmd, " -layer <layername> <key> : unbind key in a specific layer\n",
579 cmd, " -layer <layername> : unbind all keys in this layer\n");
580}
581
582
583// class ActivateCmd
584
585HotKey::ActivateCmd::ActivateCmd(CommandController& commandController_)
586 : Command(commandController_, "activate_input_layer")
587{
588}
589
590void HotKey::ActivateCmd::execute(std::span<const TclObject> tokens, TclObject& result)
591{
592 bool blocking = false;
593 std::array info = {flagArg("-blocking", blocking)};
594 auto args = parseTclArgs(getInterpreter(), tokens.subspan(1), info);
595
596 auto& hotKey = OUTER(HotKey, activateCmd);
597 switch (args.size()) {
598 case 0: {
599 string r;
600 for (auto& layerInfo : view::reverse(hotKey.activeLayers)) {
601 r += layerInfo.layer;
602 if (layerInfo.blocking) {
603 r += " -blocking";
604 }
605 r += '\n';
606 }
607 result = r;
608 break;
609 }
610 case 1: {
611 std::string_view layer = args[0].getString();
612 hotKey.activateLayer(string(layer), blocking);
613 break;
614 }
615 default:
616 throw SyntaxError();
617 }
618}
619
620string HotKey::ActivateCmd::help(std::span<const TclObject> /*tokens*/) const
621{
622 return "activate_input_layer "
623 ": show list of active layers (most recent on top)\n"
624 "activate_input_layer [-blocking] <layername> "
625 ": activate new layer, optionally in blocking mode\n";
626}
627
628
629// class DeactivateCmd
630
631HotKey::DeactivateCmd::DeactivateCmd(CommandController& commandController_)
632 : Command(commandController_, "deactivate_input_layer")
633{
634}
635
636void HotKey::DeactivateCmd::execute(std::span<const TclObject> tokens, TclObject& /*result*/)
637{
638 checkNumArgs(tokens, 2, "layer");
639 auto& hotKey = OUTER(HotKey, deactivateCmd);
640 hotKey.deactivateLayer(tokens[1].getString());
641}
642
643string HotKey::DeactivateCmd::help(std::span<const TclObject> /*tokens*/) const
644{
645 return "deactivate_input_layer <layername> : deactivate the given input layer";
646}
647
648
649} // namespace openmsx
#define PLATFORM_ANDROID
Definition build-info.hh:17
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.
HotKey(RTScheduler &rtScheduler, GlobalCommandController &commandController, EventDistributor &eventDistributor)
Definition HotKey.cc:38
void loadUnbind(std::string_view key)
Definition HotKey.cc:177
std::vector< HotKeyInfo > BindMap
Definition HotKey.hh:37
void loadBind(std::string_view key, std::string_view cmd, bool repeat, bool event)
Definition HotKey.cc:171
void loadInit()
Definition HotKey.cc:163
std::vector< Event > KeySet
Definition HotKey.hh:38
static KeyDownEvent create(SDL_Keycode code, SDL_Keymod mod=KMOD_NONE)
Definition Event.hh:80
void scheduleRT(uint64_t delta)
constexpr double e
Definition Math.hh:21
Event createInputEvent(const TclObject &str, Interpreter &interp)
This file implemented 3 utility functions:
Definition Autofire.cc:9
bool matches(const Event &self, const Event &other)
Does this event 'match' the given event.
Definition Event.cc:210
ArgsInfo valueArg(std::string_view name, T &value)
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:101
EventType getType(const Event &event)
Definition Event.hh:526
std::string toString(const BooleanInput &input)
std::variant< KeyUpEvent, KeyDownEvent, MouseMotionEvent, MouseButtonUpEvent, MouseButtonDownEvent, MouseWheelEvent, JoystickAxisMotionEvent, JoystickHatEvent, JoystickButtonUpEvent, JoystickButtonDownEvent, OsdControlReleaseEvent, OsdControlPressEvent, WindowEvent, TextEvent, FileDropEvent, QuitEvent, FinishFrameEvent, CliCommandEvent, GroupEvent, BootEvent, FrameDrawnEvent, BreakEvent, SwitchRendererEvent, TakeReverseSnapshotEvent, AfterTimedEvent, MachineLoadedEvent, MachineActivatedEvent, MachineDeactivatedEvent, MidiInReaderEvent, MidiInWindowsEvent, MidiInCoreMidiEvent, MidiInCoreMidiVirtualEvent, MidiInALSAEvent, Rs232TesterEvent, Rs232NetEvent, ImGuiDelayedActionEvent, ImGuiActiveEvent > Event
Definition Event.hh:454
ArgsInfo flagArg(std::string_view name, bool &flag)
bool any_of(InputRange &&range, UnaryPredicate pred)
Definition ranges.hh:198
auto find_if(InputRange &&range, UnaryPredicate pred)
Definition ranges.hh:173
auto find(InputRange &&range, const T &value)
Definition ranges.hh:160
Definition view.hh:15
constexpr auto reverse(Range &&range)
Definition view.hh:514
constexpr auto drop(Range &&range, size_t n)
Definition view.hh:502
#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:134
std::string strCat()
Definition strCat.hh:703
void strAppend(std::string &result, Ts &&...ts)
Definition strCat.hh:752
const Event & event
Definition HotKey.cc:190
EqualEvent(const Event &event_)
Definition HotKey.cc:183
bool operator()(const HotKey::HotKeyInfo &info) const
Definition HotKey.cc:187
bool operator()(const Event &e) const
Definition HotKey.cc:184
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)