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