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