OXYGENHUB
◆ main openmsx / src / events / InputEventGenerator.cc
15.9 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
e8555c89 Andy McCall 1
#include "InputEventGenerator.hh"
2
3
#include "Event.hh"
4
#include "EventDistributor.hh"
5
#include "FileOperations.hh"
6
#include "GlobalSettings.hh"
7
#include "IntegerSetting.hh"
8
#include "SDLKey.hh"
9
10
#include "one_of.hh"
11
#include "outer.hh"
12
#include "unreachable.hh"
13
#include "utf8_unchecked.hh"
14
15
#include <utility>
16
17
namespace openmsx {
18
19
InputEventGenerator::InputEventGenerator(CommandController& commandController,
20
                                         EventDistributor& eventDistributor_)
21
	: eventDistributor(eventDistributor_)
22
	, joystickManager(eventDistributor.getReactor())
23
	, grabInput(
24
		commandController, "grabinput",
25
		"This setting controls if openMSX takes over mouse and keyboard input",
26
		false, Setting::Save::NO)
27
	, escapeGrabCmd(commandController)
28
{
29
	eventDistributor.registerEventListener(EventType::WINDOW, *this);
30
}
31
32
InputEventGenerator::~InputEventGenerator()
33
{
34
	eventDistributor.unregisterEventListener(EventType::WINDOW, *this);
35
}
36
37
void InputEventGenerator::wait()
38
{
39
	// SDL bug workaround
40
	if (!SDL_WasInit(SDL_INIT_VIDEO)) {
41
		SDL_Delay(100);
42
	}
43
44
	if (SDL_WaitEvent(nullptr)) {
45
		poll();
46
	}
47
}
48
49
void InputEventGenerator::poll(std::optional<int> timeoutMs)
50
{
51
	// Heuristic to emulate the old SDL1 behavior:
52
	//
53
	// SDL1 had a unicode field on each KEYDOWN event. In SDL2 that
54
	// information is moved to the (new) SDL_TEXTINPUT events.
55
	//
56
	// Though our MSX keyboard emulation code needs to relate KEYDOWN
57
	// events with the associated unicode. We try to mimic this by the
58
	// following heuristic:
59
	//   When two successive events in a single batch (so, the same
60
	//   invocation of poll()) are KEYDOWN followed by TEXTINPUT, then copy
61
	//   the unicode (of the first character) of the TEXT event to the
62
	//   KEYDOWN event.
63
	// Implementing this requires a lookahead of 1 event. So the code below
64
	// deals with a 'current' and a 'previous' event, and keeps track of
65
	// whether the previous event is still pending (not yet processed).
66
	//
67
	// In a previous version we also added the constraint that these two
68
	// consecutive events must have the same timestamp, but that had mixed
69
	// results:
70
	// - on Linux it worked fine
71
	// - on Windows the timestamps sometimes did not match
72
	// - on Mac the timestamps mostly did not match
73
	// So we removed this constraint.
74
	//
75
	// We also split SDL_TEXTINPUT events into (possibly) multiple KEYDOWN
76
	// events because a single event type makes it easier to handle higher
77
	// priority listeners that can block the event for lower priority
78
	// listener (console > hotkey > msx).
79
80
	SDL_Event event1, event2;
81
	auto* prev = &event1;
82
	auto* curr = &event2;
83
	bool pending = false;
84
85
	// For the first event, optionally wait with timeout. This allows
86
	// efficient blocking when the emulator is paused, waking immediately
87
	// on any SDL event (mouse, keyboard, etc.) or on timeout.
88
	auto getNextEvent = [&] {
89
		return timeoutMs
90
			? SDL_WaitEventTimeout(curr, *std::exchange(timeoutMs, {}))
91
			: SDL_PollEvent(curr);
92
	};
93
94
	while (getNextEvent()) {
95
		// Ignore SDL_USEREVENT, used to wake SDL_WaitEventTimeout from other
96
		// threads (see Reactor::enterMainLoop).
97
		if (curr->type == SDL_USEREVENT) {
98
			continue;
99
		}
100
		if (pending) {
101
			pending = false;
102
			if ((prev->type == SDL_KEYDOWN) && (curr->type == SDL_TEXTINPUT)) {
103
				const char* utf8 = curr->text.text;
104
				auto unicode = utf8::unchecked::next(utf8);
105
				handleKeyDown(prev->key, unicode);
106
				if (unicode) { // possibly there are more characters
107
					splitText(curr->text.timestamp, utf8);
108
				}
109
				eventDistributor.distributeEvent(TextEvent(*curr));
110
				continue;
111
			} else {
112
				handle(*prev);
113
			}
114
		}
115
		if (curr->type == SDL_KEYDOWN) {
116
			pending = true;
117
			std::swap(curr, prev);
118
		} else {
119
			handle(*curr);
120
		}
121
	}
122
	if (pending) {
123
		handle(*prev);
124
	}
125
}
126
127
void InputEventGenerator::setNewOsdControlButtonState(unsigned newState)
128
{
129
	unsigned deltaState = osdControlButtonsState ^ newState;
130
	using enum OsdControlEvent::Button;
131
	for (auto b : {LEFT, RIGHT, UP, DOWN, A, B}) {
132
		if (deltaState & (1 << std::to_underlying(b))) {
133
			if (newState & (1 << std::to_underlying(b))) {
134
				eventDistributor.distributeEvent(OsdControlReleaseEvent(b));
135
			} else {
136
				eventDistributor.distributeEvent(OsdControlPressEvent(b));
137
			}
138
		}
139
	}
140
	osdControlButtonsState = newState;
141
}
142
143
void InputEventGenerator::triggerOsdControlEventsFromJoystickAxisMotion(
144
	unsigned axis, int value)
145
{
146
	auto [neg_button, pos_button] = [&] {
147
		switch (axis) {
148
		using enum OsdControlEvent::Button;
149
		case 0:
150
			return std::pair{1u << std::to_underlying(LEFT),
151
			                 1u << std::to_underlying(RIGHT)};
152
		case 1:
153
			return std::pair{1u << std::to_underlying(UP),
154
			                 1u << std::to_underlying(DOWN)};
155
		default:
156
			// Ignore all other axis (3D joysticks and flight joysticks may
157
			// have more than 2 axis)
158
			return std::pair{0u, 0u};
159
		}
160
	}();
161
162
	if (value > 0) {
163
		// release negative button, press positive button
164
		setNewOsdControlButtonState(
165
			(osdControlButtonsState | neg_button) & ~pos_button);
166
	} else if (value < 0) {
167
		// press negative button, release positive button
168
		setNewOsdControlButtonState(
169
			(osdControlButtonsState | pos_button) & ~neg_button);
170
	} else {
171
		// release both buttons
172
		setNewOsdControlButtonState(
173
			osdControlButtonsState | neg_button | pos_button);
174
	}
175
}
176
177
void InputEventGenerator::triggerOsdControlEventsFromJoystickHat(int value)
178
{
179
	using enum OsdControlEvent::Button;
180
	unsigned dir = 0;
181
	if (!(value & SDL_HAT_UP   )) dir |= 1 << std::to_underlying(UP);
182
	if (!(value & SDL_HAT_DOWN )) dir |= 1 << std::to_underlying(DOWN);
183
	if (!(value & SDL_HAT_LEFT )) dir |= 1 << std::to_underlying(LEFT);
184
	if (!(value & SDL_HAT_RIGHT)) dir |= 1 << std::to_underlying(RIGHT);
185
	unsigned ab = osdControlButtonsState & ((1 << std::to_underlying(A)) |
186
	                                        (1 << std::to_underlying(B)));
187
	setNewOsdControlButtonState(ab | dir);
188
}
189
190
void InputEventGenerator::osdControlChangeButton(bool down, unsigned changedButtonMask)
191
{
192
	auto newButtonState = down
193
		? osdControlButtonsState & ~changedButtonMask
194
		: osdControlButtonsState | changedButtonMask;
195
	setNewOsdControlButtonState(newButtonState);
196
}
197
198
void InputEventGenerator::triggerOsdControlEventsFromJoystickButtonEvent(unsigned button, bool down)
199
{
200
	using enum OsdControlEvent::Button;
201
	osdControlChangeButton(
202
		down,
203
		((button & 1) ? (1 << std::to_underlying(B))
204
		              : (1 << std::to_underlying(A))));
205
}
206
207
void InputEventGenerator::triggerOsdControlEventsFromKeyEvent(SDLKey key, bool repeat)
208
{
209
	unsigned buttonMask = [&] {
210
		switch (key.sym.sym) {
211
		using enum OsdControlEvent::Button;
212
		case SDLK_LEFT:   return 1 << std::to_underlying(LEFT);
213
		case SDLK_RIGHT:  return 1 << std::to_underlying(RIGHT);
214
		case SDLK_UP:     return 1 << std::to_underlying(UP);
215
		case SDLK_DOWN:   return 1 << std::to_underlying(DOWN);
216
		case SDLK_SPACE:  return 1 << std::to_underlying(A);
217
		case SDLK_RETURN: return 1 << std::to_underlying(A);
218
		case SDLK_ESCAPE: return 1 << std::to_underlying(B);
219
		default: return 0;
220
		}
221
	}();
222
	if (buttonMask) {
223
		if (repeat) {
224
			osdControlChangeButton(!key.down, buttonMask);
225
		}
226
		osdControlChangeButton(key.down, buttonMask);
227
	}
228
}
229
230
static constexpr Uint16 normalizeModifier(SDL_Keycode sym, Uint16 mod)
231
{
232
	// Apparently modifier-keys also have the corresponding
233
	// modifier attribute set. See here for a discussion:
234
	//     https://github.com/openMSX/openMSX/issues/1202
235
	// As a solution, on pure modifier keys, we now clear the
236
	// modifier attributes.
237
	return (sym == one_of(SDLK_LCTRL, SDLK_LSHIFT, SDLK_LALT, SDLK_LGUI,
238
	                      SDLK_RCTRL, SDLK_RSHIFT, SDLK_RALT, SDLK_RGUI,
239
	                      SDLK_MODE))
240
		? 0
241
		: mod;
242
}
243
244
void InputEventGenerator::handleKeyDown(const SDL_KeyboardEvent& key, uint32_t unicode)
245
{
246
	/*if (PLATFORM_ANDROID && evt.key.keysym.sym == SDLK_WORLD_93) {
247
		event = JoystickButtonDownEvent(0, 0);
248
		triggerOsdControlEventsFromJoystickButtonEvent(
249
			0, false);
250
		androidButtonA = true;
251
	} else if (PLATFORM_ANDROID && evt.key.keysym.sym == SDLK_WORLD_94) {
252
		event = JoystickButtonDownEvent(0, 1);
253
		triggerOsdControlEventsFromJoystickButtonEvent(
254
			1, false);
255
		androidButtonB = true;
256
	} else*/ {
257
		SDL_Event evt;
258
		evt.key = SDL_KeyboardEvent{};
259
		memcpy(&evt.key, &key, sizeof(key));
260
		evt.key.keysym.mod = normalizeModifier(key.keysym.sym, key.keysym.mod);
261
		evt.key.keysym.unused = unicode;
262
		Event event = KeyDownEvent(evt);
263
		triggerOsdControlEventsFromKeyEvent({.sym = key.keysym, .down = true}, key.repeat);
264
		eventDistributor.distributeEvent(std::move(event));
265
	}
266
}
267
268
void InputEventGenerator::splitText(uint32_t timestamp, const char* utf8)
269
{
270
	while (true) {
271
		auto unicode = utf8::unchecked::next(utf8);
272
		if (unicode == 0) return;
273
		eventDistributor.distributeEvent(
274
			KeyDownEvent::create(timestamp, unicode));
275
	}
276
}
277
278
void InputEventGenerator::handle(SDL_Event& evt)
279
{
280
	// This is different between e.g. KDE and GNOME (and probably others)
281
	// Sometimes SDL sends both:
282
	// * SDL_QUIT and
283
	// * SDL_WINDOWEVENT_CLOSE-for-the-main-window.
284
	// Sometimes only one of these events. For both events openMSX should
285
	// exit, but we should only react to the first of these events, not
286
	// both.
287
	auto quitOnce = [&] -> std::optional<Event> {
288
		if (sendQuit) return {};
289
		sendQuit = true;
290
		return QuitEvent();
291
	};
292
293
	std::optional<Event> event;
294
	switch (evt.type) {
295
	case SDL_KEYUP:
296
		// Virtual joystick of SDL Android port does not have joystick
297
		// buttons. It has however up to 6 virtual buttons that can be
298
		// mapped to SDL keyboard events. Two of these virtual buttons
299
		// will be mapped to keys SDLK_WORLD_93 and 94 and are
300
		// interpreted here as joystick buttons (respectively button 0
301
		// and 1).
302
		// TODO Android code should be rewritten for SDL2
303
		/*if (PLATFORM_ANDROID && evt.key.keysym.sym == SDLK_WORLD_93) {
304
			event = JoystickButtonUpEvent(0, 0);
305
			triggerOsdControlEventsFromJoystickButtonEvent(0, true);
306
			androidButtonA = false;
307
		} else if (PLATFORM_ANDROID && evt.key.keysym.sym == SDLK_WORLD_94) {
308
			event = JoystickButtonUpEvent(0, 1);
309
			triggerOsdControlEventsFromJoystickButtonEvent(1, true);
310
			androidButtonB = false;
311
		} else*/ {
312
			SDL_Event e;
313
			e.key = SDL_KeyboardEvent{};
314
			memcpy(&e.key, &evt.key, sizeof(evt.key));
315
			e.key.keysym.mod = normalizeModifier(evt.key.keysym.sym, evt.key.keysym.mod);
316
			event = KeyUpEvent(e);
317
			bool down = false;
318
			bool repeat = false;
319
			triggerOsdControlEventsFromKeyEvent({.sym = e.key.keysym, .down = down}, repeat);
320
		}
321
		break;
322
	case SDL_KEYDOWN:
323
		handleKeyDown(evt.key, 0);
324
		break;
325
326
	case SDL_KEYMAPCHANGED:
327
		event = KeyMapChangedEvent();
328
		break;
329
330
	case SDL_MOUSEBUTTONUP:
331
		event = MouseButtonUpEvent(evt);
332
		break;
333
	case SDL_MOUSEBUTTONDOWN:
334
		event = MouseButtonDownEvent(evt);
335
		break;
336
	case SDL_MOUSEWHEEL:
337
		event = MouseWheelEvent(evt);
338
		break;
339
	case SDL_MOUSEMOTION:
340
		event = MouseMotionEvent(evt);
341
		if (auto* window = SDL_GL_GetCurrentWindow(); SDL_GetWindowGrab(window)) {
342
			int w, h;
343
			SDL_GetWindowSize(window, &w, &h);
344
			int x, y;
345
			SDL_GetMouseState(&x, &y);
346
			// There seems to be a bug in Windows in which the mouse can be locked on the right edge
347
			// only when moving fast to the right (not so fast actually) when grabbing is enabled
348
			// which stops generating mouse events
349
			// When moving to the left, events resume, and then moving even slower to the right fixes it
350
			// This only occurs when grabbing is explicitly enabled in windowed mode,
351
			// not in fullscreen mode (though not sure what happens with multiple monitors)
352
			// To reduce the impact of this bug, long range warping (e.g. to the middle of the window)
353
			// was attempted but that caused race conditions with fading in of gui elements
354
			// So, in the end it was decided that to go for the least kind of trouble
355
			// The value of 10 below is a heuristic value which seems to balance all factors
356
			// such as font size and the overall size of gui elements
357
			// and the speed of crossing virtual barriers
358
			static constexpr int MARGIN = 10;
359
			int xn = std::clamp(x, MARGIN, w - 1 - MARGIN);
360
			int yn = std::clamp(y, MARGIN, h - 1 - MARGIN);
361
			if (xn != x || yn != y) SDL_WarpMouseInWindow(window, xn, yn);
362
		}
363
		break;
364
	case SDL_JOYBUTTONUP:
365
		if (joystickManager.translateSdlInstanceId(evt)) {
366
			event = JoystickButtonUpEvent(evt);
367
			triggerOsdControlEventsFromJoystickButtonEvent(
368
				evt.jbutton.button, false);
369
		}
370
		break;
371
	case SDL_JOYBUTTONDOWN:
372
		if (joystickManager.translateSdlInstanceId(evt)) {
373
			event = JoystickButtonDownEvent(evt);
374
			triggerOsdControlEventsFromJoystickButtonEvent(
375
				evt.jbutton.button, true);
376
		}
377
		break;
378
	case SDL_JOYAXISMOTION: {
379
		if (auto joyId = joystickManager.translateSdlInstanceId(evt)) {
380
			event = JoystickAxisMotionEvent(evt);
381
382
			int threshold = joystickManager.getJoyDeadThreshold(*joyId);
383
			auto value = (evt.jaxis.value < -threshold) ? evt.jaxis.value
384
			           : (evt.jaxis.value >  threshold) ? evt.jaxis.value
385
			                                            : 0;
386
			triggerOsdControlEventsFromJoystickAxisMotion(
387
				evt.jaxis.axis, value);
388
		}
389
		break;
390
	}
391
	case SDL_JOYHATMOTION:
392
		if (auto joyId = joystickManager.translateSdlInstanceId(evt)) {
393
			event = JoystickHatEvent(evt);
394
			triggerOsdControlEventsFromJoystickHat(evt.jhat.value);
395
		}
396
		break;
397
398
	case SDL_JOYDEVICEADDED:
399
		joystickManager.add(evt.jdevice.which);
400
		break;
401
402
	case SDL_JOYDEVICEREMOVED:
403
		joystickManager.remove(evt.jdevice.which);
404
		break;
405
406
	case SDL_TEXTINPUT:
407
		splitText(evt.text.timestamp, evt.text.text);
408
		event = TextEvent(evt);
409
		break;
410
411
	case SDL_WINDOWEVENT:
412
		switch (evt.window.event) {
413
		case SDL_WINDOWEVENT_CLOSE:
414
			if (WindowEvent::isMainWindow(evt.window.windowID)) {
415
				event = quitOnce();
416
				break;
417
			}
418
			[[fallthrough]];
419
		default:
420
			event = WindowEvent(evt);
421
			break;
422
		}
423
		break;
424
425
	case SDL_DROPFILE:
426
		event = FileDropEvent(
427
			FileOperations::getConventionalPath(evt.drop.file));
428
		SDL_free(evt.drop.file);
429
		break;
430
431
	case SDL_QUIT:
432
		event = quitOnce();
433
		break;
434
435
	default:
436
		break;
437
	}
438
439
#if 0
440
	if (event) {
441
		std::cerr << "SDL event converted to: " << toString(event) << '\n';
442
	} else {
443
		std::cerr << "SDL event was of unknown type, not converted to an openMSX event\n";
444
	}
445
#endif
446
447
	if (event) eventDistributor.distributeEvent(std::move(*event));
448
}
449
450
451
void InputEventGenerator::updateGrab(bool grab)
452
{
453
	escapeGrabState = ESCAPE_GRAB_WAIT_CMD;
454
	setGrabInput(grab);
455
}
456
457
bool InputEventGenerator::signalEvent(const Event& event)
458
{
459
	std::visit(overloaded{
460
		[&](const WindowEvent& e) {
461
			const auto& evt = e.getSdlWindowEvent();
462
			if (e.isMainWindow() &&
463
			    evt.event == one_of(SDL_WINDOWEVENT_FOCUS_GAINED, SDL_WINDOWEVENT_FOCUS_LOST)) {
464
				switch (escapeGrabState) {
465
					case ESCAPE_GRAB_WAIT_CMD:
466
						// nothing
467
						break;
468
					case ESCAPE_GRAB_WAIT_LOST:
469
						if (evt.event == SDL_WINDOWEVENT_FOCUS_LOST) {
470
							escapeGrabState = ESCAPE_GRAB_WAIT_GAIN;
471
						}
472
						break;
473
					case ESCAPE_GRAB_WAIT_GAIN:
474
						if (evt.event == SDL_WINDOWEVENT_FOCUS_GAINED) {
475
							escapeGrabState = ESCAPE_GRAB_WAIT_CMD;
476
						}
477
						setGrabInput(true);
478
						break;
479
					default:
480
						UNREACHABLE;
481
				}
482
			}
483
		},
484
		[](const EventBase&) {
485
			// correct but causes excessive clang compile-time
486
			// UNREACHABLE;
487
		}
488
	}, event);
489
	return false;
490
}
491
492
void InputEventGenerator::initializeGrab() const
493
{
494
	setGrabInput(grabInput.getBoolean());
495
}
496
497
void InputEventGenerator::setGrabInput(bool grab) const
498
{
499
	SDL_SetWindowGrab(SDL_GL_GetCurrentWindow(), grab ? SDL_TRUE : SDL_FALSE);
500
}
501
502
503
// class EscapeGrabCmd
504
505
InputEventGenerator::EscapeGrabCmd::EscapeGrabCmd(
506
		CommandController& commandController_)
507
	: Command(commandController_, "escape_grab")
508
{
509
}
510
511
void InputEventGenerator::EscapeGrabCmd::execute(
512
	std::span<const TclObject> /*tokens*/, TclObject& /*result*/)
513
{
514
	auto& inputEventGenerator = OUTER(InputEventGenerator, escapeGrabCmd);
515
	if (inputEventGenerator.grabInput.getBoolean()) {
516
		inputEventGenerator.escapeGrabState =
517
			InputEventGenerator::ESCAPE_GRAB_WAIT_LOST;
518
		inputEventGenerator.setGrabInput(false);
519
	}
520
}
521
522
std::string InputEventGenerator::EscapeGrabCmd::help(
523
	std::span<const TclObject> /*tokens*/) const
524
{
525
	return "Temporarily release input grab.";
526
}
527
528
} // namespace openmsx