openMSX
HotKey.cc
Go to the documentation of this file.
1 #include "HotKey.hh"
2 #include "InputEventFactory.hh"
4 #include "Command.hh"
5 #include "CommandException.hh"
6 #include "EventDistributor.hh"
7 #include "CliComm.hh"
8 #include "InputEvents.hh"
9 #include "XMLElement.hh"
10 #include "TclObject.hh"
11 #include "SettingsConfig.hh"
12 #include "AlarmEvent.hh"
13 #include "memory.hh"
14 #include "unreachable.hh"
15 #include "build-info.hh"
16 #include <algorithm>
17 #include <cassert>
18 
19 using std::string;
20 using std::vector;
21 using std::make_shared;
22 
23 // This file implements all Tcl key bindings. These are the 'classical' hotkeys
24 // (e.g. F11 to (un)mute sound) and the more recent input layers. The idea
25 // behind an input layer is something like an OSD widget that (temporarily)
26 // takes semi-exclusive access to the input. So while the widget is active
27 // keyboard (and joystick) input is no longer passed to the emulated MSX.
28 // However the classical hotkeys or the openMSX console still receive input.
29 
30 namespace openmsx {
31 
32 const bool META_HOT_KEYS =
33 #ifdef __APPLE__
34  true;
35 #else
36  false;
37 #endif
38 
39 class BindCmd : public Command
40 {
41 public:
42  BindCmd(CommandController& commandController, HotKey& hotKey,
43  bool defaultCmd);
44  virtual string execute(const vector<string>& tokens);
45  virtual string help(const vector<string>& tokens) const;
46 private:
47  string formatBinding(const HotKey::BindMap::value_type& p);
48 
49  HotKey& hotKey;
50  const bool defaultCmd;
51 };
52 
53 class UnbindCmd : public Command
54 {
55 public:
56  UnbindCmd(CommandController& commandController, HotKey& hotKey,
57  bool defaultCmd);
58  virtual string execute(const vector<string>& tokens);
59  virtual string help(const vector<string>& tokens) const;
60 private:
61  HotKey& hotKey;
62  const bool defaultCmd;
63 };
64 
65 class ActivateCmd : public Command
66 {
67 public:
68  ActivateCmd(CommandController& commandController, HotKey& hotKey);
69  virtual string execute(const vector<string>& tokens);
70  virtual string help(const vector<string>& tokens) const;
71 private:
72  HotKey& hotKey;
73 };
74 
75 class DeactivateCmd : public Command
76 {
77 public:
78  DeactivateCmd(CommandController& commandController, HotKey& hotKey);
79  virtual string execute(const vector<string>& tokens);
80  virtual string help(const vector<string>& tokens) const;
81 private:
82  HotKey& hotKey;
83 };
84 
85 
87  EventDistributor& eventDistributor_)
88  : bindCmd(make_unique<BindCmd>(
89  commandController_, *this, false))
90  , unbindCmd(make_unique<UnbindCmd>(
91  commandController_, *this, false))
92  , bindDefaultCmd(make_unique<BindCmd>(
93  commandController_, *this, true))
94  , unbindDefaultCmd(make_unique<UnbindCmd>(
95  commandController_, *this, true))
96  , activateCmd(make_unique<ActivateCmd>(
97  commandController_, *this))
98  , deactivateCmd(make_unique<DeactivateCmd>(
99  commandController_, *this))
100  , repeatAlarm(make_unique<AlarmEvent>(
101  eventDistributor_, *this, OPENMSX_REPEAT_HOTKEY,
102  EventDistributor::HOTKEY))
103  , commandController(commandController_)
104  , eventDistributor(eventDistributor_)
105 {
106  initDefaultBindings();
107 
108  eventDistributor.registerEventListener(
110  eventDistributor.registerEventListener(
112  eventDistributor.registerEventListener(
114  eventDistributor.registerEventListener(
116  eventDistributor.registerEventListener(
118  eventDistributor.registerEventListener(
120  eventDistributor.registerEventListener(
122  eventDistributor.registerEventListener(
124  eventDistributor.registerEventListener(
126  eventDistributor.registerEventListener(
128  eventDistributor.registerEventListener(
130 }
131 
133 {
136  eventDistributor.unregisterEventListener(OPENMSX_FOCUS_EVENT, *this);
137  eventDistributor.unregisterEventListener(OPENMSX_JOY_BUTTON_UP_EVENT, *this);
142  eventDistributor.unregisterEventListener(OPENMSX_MOUSE_MOTION_EVENT, *this);
143  eventDistributor.unregisterEventListener(OPENMSX_KEY_UP_EVENT, *this);
144  eventDistributor.unregisterEventListener(OPENMSX_KEY_DOWN_EVENT, *this);
145 }
146 
147 void HotKey::initDefaultBindings()
148 {
149  // TODO move to Tcl script?
150 
151  if (META_HOT_KEYS) {
152  // Hot key combos using Mac's Command key.
153  bindDefault(make_shared<KeyDownEvent>(
155  HotKeyInfo("screenshot -guess-name"));
156  bindDefault(make_shared<KeyDownEvent>(
158  HotKeyInfo("toggle pause"));
159  bindDefault(make_shared<KeyDownEvent>(
161  HotKeyInfo("toggle throttle"));
162  bindDefault(make_shared<KeyDownEvent>(
164  HotKeyInfo("toggle console"));
165  bindDefault(make_shared<KeyDownEvent>(
167  HotKeyInfo("toggle mute"));
168  bindDefault(make_shared<KeyDownEvent>(
170  HotKeyInfo("toggle fullscreen"));
171  bindDefault(make_shared<KeyDownEvent>(
173  HotKeyInfo("exit"));
174  } else {
175  // Hot key combos for typical PC keyboards.
176  bindDefault(make_shared<KeyDownEvent>(Keys::K_PRINT),
177  HotKeyInfo("screenshot -guess-name"));
178  bindDefault(make_shared<KeyDownEvent>(Keys::K_PAUSE),
179  HotKeyInfo("toggle pause"));
180  bindDefault(make_shared<KeyDownEvent>(Keys::K_F9),
181  HotKeyInfo("toggle throttle"));
182  bindDefault(make_shared<KeyDownEvent>(Keys::K_F10),
183  HotKeyInfo("toggle console"));
184  bindDefault(make_shared<KeyDownEvent>(Keys::K_F11),
185  HotKeyInfo("toggle mute"));
186  bindDefault(make_shared<KeyDownEvent>(Keys::K_F12),
187  HotKeyInfo("toggle fullscreen"));
188  bindDefault(make_shared<KeyDownEvent>(
190  HotKeyInfo("exit"));
191  bindDefault(make_shared<KeyDownEvent>(
193  HotKeyInfo("exit"));
194  bindDefault(make_shared<KeyDownEvent>(
196  HotKeyInfo("toggle fullscreen"));
197 #if PLATFORM_ANDROID
198  // The follwing binding is specific for Android, in order
199  // to remap the android back button to an SDL KEY event.
200  // I could have put all Android key bindings in a separate
201  // else(...) clause. However, an Android user might actually
202  // be using a PC keyboard (through USB or Bluetooth) and in such
203  // case expect all default PC keybindings to exist as well
204  bindDefault(make_shared<KeyDownEvent>(Keys::K_WORLD_92),
205  HotKeyInfo("quitmenu::quit_menu"));
206 #endif
207  }
208 }
209 
210 static HotKey::EventPtr createEvent(const string& str)
211 {
212  auto event = InputEventFactory::createInputEvent(str);
213  if (!dynamic_cast<const KeyEvent*> (event.get()) &&
214  !dynamic_cast<const MouseButtonEvent*> (event.get()) &&
215  !dynamic_cast<const MouseMotionGroupEvent*>(event.get()) &&
216  !dynamic_cast<const JoystickEvent*> (event.get()) &&
217  !dynamic_cast<const OsdControlEvent*> (event.get()) &&
218  !dynamic_cast<const FocusEvent*> (event.get())) {
219  throw CommandException("Unsupported event type");
220  }
221  return event;
222 }
223 
224 void HotKey::loadBindings(const XMLElement& config)
225 {
226  // restore default bindings
227  unboundKeys.clear();
228  boundKeys.clear();
229  cmdMap.clear();
230  cmdMap.insert(defaultMap.begin(), defaultMap.end());
231 
232  // load bindings
233  auto* bindingsElement = config.findChild("bindings");
234  if (!bindingsElement) return;
235  auto copy = *bindingsElement; // dont iterate over changing container
236  for (auto& elem : copy.getChildren()) {
237  try {
238  if (elem.getName() == "bind") {
239  bind(createEvent(elem.getAttribute("key")),
240  HotKeyInfo(elem.getData(),
241  elem.getAttributeAsBool("repeat", false)));
242  } else if (elem.getName() == "unbind") {
243  unbind(createEvent(elem.getAttribute("key")));
244  }
245  } catch (MSXException& e) {
246  commandController.getCliComm().printWarning(
247  "Error while loading key bindings: " + e.getMessage());
248  }
249  }
250 }
251 
252 void HotKey::saveBindings(XMLElement& config) const
253 {
254  auto& bindingsElement = config.getCreateChild("bindings");
255  bindingsElement.removeAllChildren();
256 
257  // add explicit bind's
258  for (auto& k : boundKeys) {
259  auto it2 = cmdMap.find(k);
260  assert(it2 != cmdMap.end());
261  auto& info = it2->second;
262  XMLElement elem("bind", info.command);
263  elem.addAttribute("key", k->toString());
264  if (info.repeat) {
265  elem.addAttribute("repeat", "true");
266  }
267  bindingsElement.addChild(std::move(elem));
268  }
269  // add explicit unbind's
270  for (auto& k : unboundKeys) {
271  XMLElement elem("unbind");
272  elem.addAttribute("key", k->toString());
273  bindingsElement.addChild(std::move(elem));
274  }
275 }
276 
277 void HotKey::bind(const EventPtr& event, const HotKeyInfo& info)
278 {
279  unboundKeys.erase(event);
280  boundKeys.insert(event);
281  defaultMap.erase(event);
282  cmdMap[event] = info;
283 
284  saveBindings(commandController.getSettingsConfig().getXMLElement());
285 }
286 
287 void HotKey::unbind(const EventPtr& event)
288 {
289  if (boundKeys.find(event) == boundKeys.end()) {
290  // only when not a regular bound event
291  unboundKeys.insert(event);
292  }
293  boundKeys.erase(event);
294  defaultMap.erase(event);
295  cmdMap.erase(event);
296 
297  saveBindings(commandController.getSettingsConfig().getXMLElement());
298 }
299 
300 void HotKey::bindDefault(const EventPtr& event, const HotKeyInfo& info)
301 {
302  if ((unboundKeys.find(event) == unboundKeys.end()) &&
303  (boundKeys.find(event) == boundKeys.end())) {
304  // not explicity bound or unbound
305  cmdMap[event] = info;
306  }
307  defaultMap[event] = info;
308 }
309 
310 void HotKey::unbindDefault(const EventPtr& event)
311 {
312  if ((unboundKeys.find(event) == unboundKeys.end()) &&
313  (boundKeys.find(event) == boundKeys.end())) {
314  // not explicity bound or unbound
315  cmdMap.erase(event);
316  }
317  defaultMap.erase(event);
318 }
319 
320 void HotKey::bindLayer(const EventPtr& event, const HotKeyInfo& info,
321  const string& layer)
322 {
323  layerMap[layer][event] = info;
324 }
325 
326 void HotKey::unbindLayer(const EventPtr& event, const string& layer)
327 {
328  layerMap[layer].erase(event);
329 }
330 
331 void HotKey::unbindFullLayer(const string& layer)
332 {
333  layerMap.erase(layer);
334 }
335 
336 void HotKey::activateLayer(const std::string& layer, bool blocking)
337 {
338  // Insert new activattion record at the end of the list.
339  // (it's not an error if the same layer was already active, in such
340  // as case it will now appear twice in the list of active layer,
341  // and it must also be deactivated twice).
342  LayerInfo info;
343  info.layer = layer;
344  info.blocking = blocking;
345  activeLayers.push_back(info);
346 }
347 
348 void HotKey::deactivateLayer(const std::string& layer)
349 {
350  // remove the first matching activation record from the end
351  // (it's not an error if there is no match at all)
352  auto it = std::find_if(activeLayers.rbegin(), activeLayers.rend(),
353  [&](const LayerInfo& info) { return info.layer == layer; });
354  if (it != activeLayers.rend()) {
355  // 'reverse_iterator' -> 'iterator' conversion is a bit tricky
356  activeLayers.erase((it + 1).base());
357  }
358 }
359 
360 static HotKey::BindMap::const_iterator findMatch(
361  const HotKey::BindMap& map, const Event& event)
362 {
363  return find_if(map.begin(), map.end(),
364  [&](const HotKey::BindMap::value_type& p) {
365  return p.first->matches(event);
366  });
367 }
368 
369 int HotKey::signalEvent(const EventPtr& event_)
370 {
371  // Convert special 'repeat' event into the actual to-be-repeated event.
372  EventPtr event = event_;
373  if (event->getType() == OPENMSX_REPEAT_HOTKEY) {
374  if (!lastEvent.get()) return true;
375  event = lastEvent;
376  } else if (lastEvent.get() != event.get()) {
377  // If the newly received event is different from the repeating
378  // event, we stop the repeat process.
379  // Except when we're repeating a OsdControlEvent and the
380  // received event was actually the 'generating' event for the
381  // Osd event. E.g. a cursor-keyboard-down event will generate
382  // a corresponding osd event (the osd event is send before the
383  // original event). Without this hack, key-repeat will not work
384  // for osd key bindings.
385  if (lastEvent.get() && lastEvent->isRepeatStopper(*event)) {
386  stopRepeat();
387  }
388  }
389 
390  // First search in active layers (from back to front)
391  bool blocking = false;
392  for (auto it = activeLayers.rbegin(); it != activeLayers.rend(); ++it) {
393  auto& cmap = layerMap[it->layer]; // ok, if this entry doesn't exist yet
394  auto it2 = findMatch(cmap, *event);
395  if (it2 != cmap.end()) {
396  executeBinding(event, it2->second);
397  // Deny event to MSX listeners, also don't pass event
398  // to other layers (including the default layer).
399  return EventDistributor::MSX;
400  }
401  blocking = it->blocking;
402  if (blocking) break; // don't try lower layers
403  }
404 
405  // If the event was not yet handled, try the default layer.
406  auto it = findMatch(cmdMap, *event);
407  if (it != cmdMap.end()) {
408  executeBinding(event, it->second);
409  return EventDistributor::MSX; // deny event to MSX listeners
410  }
411 
412  // Event is not handled, only let it pass to the MSX if there was no
413  // blocking layer active.
414  return blocking ? EventDistributor::MSX : 0;
415 }
416 
417 void HotKey::executeBinding(const EventPtr& event, const HotKeyInfo& info)
418 {
419  if (info.repeat) {
420  startRepeat(event);
421  }
422  try {
423  // ignore return value
424  commandController.executeCommand(info.command);
425  } catch (CommandException& e) {
426  commandController.getCliComm().printWarning(
427  "Error executing hot key command: " + e.getMessage());
428  }
429 }
430 
431 void HotKey::startRepeat(const EventPtr& event)
432 {
433  // I initially thought about using the builtin SDL key-repeat feature,
434  // but that won't work for example on joystick buttons. So we have to
435  // code it ourselves.
436 
437  // On android, because of the sensitivity of the touch screen it's
438  // very hard to have touches of short durations. So half a second is
439  // too short for the key-repeat-delay. A full second should be fine.
440  static const unsigned DELAY = PLATFORM_ANDROID ? 1000 : 500;
441  // Repeat period.
442  static const unsigned PERIOD = 30;
443 
444  unsigned delay = (lastEvent.get() ? PERIOD : DELAY) * 1000;
445  lastEvent = event;
446  repeatAlarm->schedule(delay);
447 }
448 
449 void HotKey::stopRepeat()
450 {
451  lastEvent.reset();
452  repeatAlarm->cancel();
453 }
454 
455 
456 // class BindCmd
457 
458 static string getBindCmdName(bool defaultCmd)
459 {
460  return defaultCmd ? "bind_default" : "bind";
461 }
462 
463 BindCmd::BindCmd(CommandController& commandController, HotKey& hotKey_,
464  bool defaultCmd_)
465  : Command(commandController, getBindCmdName(defaultCmd_))
466  , hotKey(hotKey_)
467  , defaultCmd(defaultCmd_)
468 {
469 }
470 
471 string BindCmd::formatBinding(const HotKey::BindMap::value_type& p)
472 {
473  auto& info = p.second;
474  return p.first->toString() + (info.repeat ? " [repeat]" : "") +
475  ": " + info.command + '\n';
476 }
477 
478 static vector<string> parse(bool defaultCmd, vector<string> tokens,
479  string& layer, bool& layers)
480 {
481  layers = false;
482  for (size_t i = 1; i < tokens.size(); ) {
483  if (tokens[i] == "-layer") {
484  if (i == (tokens.size() - 1)) {
485  throw CommandException("Missing layer name");
486  }
487  if (defaultCmd) {
488  throw CommandException(
489  "Layers are not supported for default bindings");
490  }
491  layer = tokens[i + 1];
492 
493  auto it = tokens.begin() + i;
494  tokens.erase(it, it + 2);
495  } else if (tokens[i] == "-layers") {
496  layers = true;
497  tokens.erase(tokens.begin() + i);
498  } else {
499  ++i;
500  }
501  }
502  return tokens;
503 }
504 
505 string BindCmd::execute(const vector<string>& tokens_)
506 {
507  string layer;
508  bool layers;
509  auto tokens = parse(defaultCmd, tokens_, layer, layers);
510 
511  auto& cmdMap = defaultCmd
512  ? hotKey.defaultMap
513  : layer.empty() ? hotKey.cmdMap
514  : hotKey.layerMap[layer];
515 
516  if (layers) {
517  TclObject result;
518  for (auto& p : hotKey.layerMap) {
519  // An alternative for this test is to always properly
520  // prune layerMap. ATM this approach seems simpler.
521  if (!p.second.empty()) {
522  result.addListElement(p.first);
523  }
524  }
525  return result.getString().str();
526  }
527 
528  string result;
529  switch (tokens.size()) {
530  case 0:
531  UNREACHABLE;
532  case 1:
533  // show all bounded keys (for this layer)
534  for (auto& p : cmdMap) {
535  result += formatBinding(p);
536  }
537  break;
538  case 2: {
539  // show bindings for this key (in this layer)
540  auto it = cmdMap.find(createEvent(tokens[1]));
541  if (it == cmdMap.end()) {
542  throw CommandException("Key not bound");
543  }
544  result = formatBinding(*it);
545  break;
546  }
547  default: {
548  // make a new binding
549  string command;
550  bool repeat = false;
551  unsigned start = 2;
552  if (tokens[2] == "-repeat") {
553  repeat = true;
554  ++start;
555  }
556  for (unsigned i = start; i < tokens.size(); ++i) {
557  if (i != start) command += ' ';
558  command += tokens[i];
559  }
560  HotKey::HotKeyInfo info(command, repeat);
561  auto event = createEvent(tokens[1]);
562  if (defaultCmd) {
563  hotKey.bindDefault(event, info);
564  } else if (layer.empty()) {
565  hotKey.bind(event, info);
566  } else {
567  hotKey.bindLayer(event, info, layer);
568  }
569  break;
570  }
571  }
572  return result;
573 }
574 string BindCmd::help(const vector<string>& /*tokens*/) const
575 {
576  string cmd = getBindCmdName(defaultCmd);
577  return cmd + " : show all bounded keys\n" +
578  cmd + " <key> : show binding for this key\n" +
579  cmd + " <key> [-repeat] <cmd> : bind key to command, optionally "
580  "repeat command while key remains pressed\n"
581  "These 3 take an optional '-layer <layername>' option, "
582  "see activate_input_layer." +
583  cmd + " -layers : show a list of layers with bound keys\n";
584 }
585 
586 
587 // class UnbindCmd
588 
589 static string getUnbindCmdName(bool defaultCmd)
590 {
591  return defaultCmd ? "unbind_default" : "unbind";
592 }
593 
595  HotKey& hotKey_, bool defaultCmd_)
596  : Command(commandController, getUnbindCmdName(defaultCmd_))
597  , hotKey(hotKey_)
598  , defaultCmd(defaultCmd_)
599 {
600 }
601 
602 string UnbindCmd::execute(const vector<string>& tokens_)
603 {
604  string layer;
605  bool layers;
606  auto tokens = parse(defaultCmd, tokens_, layer, layers);
607  if (layers) {
608  throw SyntaxError();
609  }
610  if ((tokens.size() > 2) || (layer.empty() && (tokens.size() != 2))) {
611  throw SyntaxError();
612  }
613 
614  HotKey::EventPtr event;
615  if (tokens.size() == 2) {
616  event = createEvent(tokens[1]);
617  }
618 
619  if (defaultCmd) {
620  assert(event);
621  hotKey.unbindDefault(event);
622  } else if (layer.empty()) {
623  assert(event);
624  hotKey.unbind(event);
625  } else {
626  if (event) {
627  hotKey.unbindLayer(event, layer);
628  } else {
629  hotKey.unbindFullLayer(layer);
630  }
631  }
632  return "";
633 }
634 string UnbindCmd::help(const vector<string>& /*tokens*/) const
635 {
636  string cmd = getUnbindCmdName(defaultCmd);
637  return cmd + " <key> : unbind this key\n" +
638  cmd + " -layer <layername> <key> : unbind key in a specific layer\n" +
639  cmd + " -layer <layername> : unbind all keys in this layer\n";
640 }
641 
642 
643 // class ActivateCmd
644 
645 ActivateCmd::ActivateCmd(CommandController& commandController, HotKey& hotKey_)
646  : Command(commandController, "activate_input_layer")
647  , hotKey(hotKey_)
648 {
649 }
650 
651 string ActivateCmd::execute(const vector<string>& tokens)
652 {
653  string layer;
654  bool blocking = false;
655  for (size_t i = 1; i < tokens.size(); ++i) {
656  if (tokens[i] == "-blocking") {
657  blocking = true;
658  } else {
659  if (!layer.empty()) {
660  throw SyntaxError();
661  }
662  layer = tokens[i];
663  }
664  }
665 
666  string result;
667  if (layer.empty()) {
668  for (auto it = hotKey.activeLayers.rbegin();
669  it != hotKey.activeLayers.rend(); ++it) {
670  result += it->layer;
671  if (it->blocking) {
672  result += " -blocking";
673  }
674  result += '\n';
675  }
676  } else {
677  hotKey.activateLayer(layer, blocking);
678  }
679  return result;
680 }
681 
682 string ActivateCmd::help(const vector<string>& /*tokens*/) const
683 {
684  return "activate_input_layer "
685  ": show list of active layers (most recent on top)\n"
686  "activate_input_layer [-blocking] <layername> "
687  ": activate new layer, optionally in blocking mode\n";
688 }
689 
690 
691 // class DeactivateCmd
692 
694  : Command(commandController, "deactivate_input_layer")
695  , hotKey(hotKey_)
696 {
697 }
698 
699 string DeactivateCmd::execute(const vector<string>& tokens)
700 {
701  if (tokens.size() != 2) {
702  throw SyntaxError();
703  }
704  hotKey.deactivateLayer(tokens[1]);
705  return "";
706 }
707 
708 string DeactivateCmd::help(const vector<string>& /*tokens*/) const
709 {
710  return "deactivate_input_layer <layername> : deactive the given input layer";
711 }
712 
713 
714 } // namespace openmsx