added support for undo/redo using Ctrl-z/Ctrl-Shift-z in level editor
[rocksndiamonds.git] / src / events.c
1 // ============================================================================
2 // Rocks'n'Diamonds - McDuffin Strikes Back!
3 // ----------------------------------------------------------------------------
4 // (c) 1995-2014 by Artsoft Entertainment
5 //                  Holger Schemel
6 //                  info@artsoft.org
7 //                  http://www.artsoft.org/
8 // ----------------------------------------------------------------------------
9 // events.c
10 // ============================================================================
11
12 #include "libgame/libgame.h"
13
14 #include "events.h"
15 #include "init.h"
16 #include "screens.h"
17 #include "tools.h"
18 #include "game.h"
19 #include "editor.h"
20 #include "files.h"
21 #include "tape.h"
22 #include "anim.h"
23 #include "network.h"
24
25
26 #define DEBUG_EVENTS            0
27
28 #define DEBUG_EVENTS_BUTTON     (DEBUG_EVENTS   * 0)
29 #define DEBUG_EVENTS_MOTION     (DEBUG_EVENTS   * 0)
30 #define DEBUG_EVENTS_WHEEL      (DEBUG_EVENTS   * 1)
31 #define DEBUG_EVENTS_WINDOW     (DEBUG_EVENTS   * 0)
32 #define DEBUG_EVENTS_FINGER     (DEBUG_EVENTS   * 0)
33 #define DEBUG_EVENTS_TEXT       (DEBUG_EVENTS   * 1)
34 #define DEBUG_EVENTS_KEY        (DEBUG_EVENTS   * 1)
35
36
37 static boolean cursor_inside_playfield = FALSE;
38 static int cursor_mode_last = CURSOR_DEFAULT;
39 static unsigned int special_cursor_delay = 0;
40 static unsigned int special_cursor_delay_value = 1000;
41
42 static boolean virtual_button_pressed = FALSE;
43 static boolean stop_processing_events = FALSE;
44
45
46 // forward declarations for internal use
47 static void HandleNoEvent(void);
48 static void HandleEventActions(void);
49
50
51 // event filter to set mouse x/y position (for pointer class global animations)
52 // (this is especially required to ensure smooth global animation mouse pointer
53 // movement when the screen is updated without handling events; this can happen
54 // when drawing door/envelope request animations, for example)
55
56 int FilterMouseMotionEvents(void *userdata, Event *event)
57 {
58   if (event->type == EVENT_MOTIONNOTIFY)
59   {
60     int mouse_x = ((MotionEvent *)event)->x;
61     int mouse_y = ((MotionEvent *)event)->y;
62
63     UpdateRawMousePosition(mouse_x, mouse_y);
64   }
65
66   return 1;
67 }
68
69 // event filter especially needed for SDL event filtering due to
70 // delay problems with lots of mouse motion events when mouse button
71 // not pressed (X11 can handle this with 'PointerMotionHintMask')
72
73 // event filter addition for SDL2: as SDL2 does not have a function to enable
74 // or disable keyboard auto-repeat, filter repeated keyboard events instead
75
76 static int FilterEvents(const Event *event)
77 {
78   MotionEvent *motion;
79
80   // skip repeated key press events if keyboard auto-repeat is disabled
81   if (event->type == EVENT_KEYPRESS &&
82       event->key.repeat &&
83       !keyrepeat_status)
84     return 0;
85
86   if (event->type == EVENT_BUTTONPRESS ||
87       event->type == EVENT_BUTTONRELEASE)
88   {
89     ((ButtonEvent *)event)->x -= video.screen_xoffset;
90     ((ButtonEvent *)event)->y -= video.screen_yoffset;
91   }
92   else if (event->type == EVENT_MOTIONNOTIFY)
93   {
94     ((MotionEvent *)event)->x -= video.screen_xoffset;
95     ((MotionEvent *)event)->y -= video.screen_yoffset;
96   }
97
98   // non-motion events are directly passed to event handler functions
99   if (event->type != EVENT_MOTIONNOTIFY)
100     return 1;
101
102   motion = (MotionEvent *)event;
103   cursor_inside_playfield = (motion->x >= SX && motion->x < SX + SXSIZE &&
104                              motion->y >= SY && motion->y < SY + SYSIZE);
105
106   // set correct mouse x/y position (for pointer class global animations)
107   // (this is required in rare cases where the mouse x/y position calculated
108   // from raw values (to apply logical screen size scaling corrections) does
109   // not match the final mouse event x/y position -- this may happen because
110   // the SDL renderer's viewport position is internally represented as float,
111   // but only accessible as integer, which may lead to rounding errors)
112   gfx.mouse_x = motion->x;
113   gfx.mouse_y = motion->y;
114
115   // do no reset mouse cursor before all pending events have been processed
116   if (gfx.cursor_mode == cursor_mode_last &&
117       ((game_status == GAME_MODE_TITLE &&
118         gfx.cursor_mode == CURSOR_NONE) ||
119        (game_status == GAME_MODE_PLAYING &&
120         gfx.cursor_mode == CURSOR_PLAYFIELD)))
121   {
122     SetMouseCursor(CURSOR_DEFAULT);
123
124     DelayReached(&special_cursor_delay, 0);
125
126     cursor_mode_last = CURSOR_DEFAULT;
127   }
128
129   // skip mouse motion events without pressed button outside level editor
130   if (button_status == MB_RELEASED &&
131       game_status != GAME_MODE_EDITOR && game_status != GAME_MODE_PLAYING)
132     return 0;
133
134   return 1;
135 }
136
137 // to prevent delay problems, skip mouse motion events if the very next
138 // event is also a mouse motion event (and therefore effectively only
139 // handling the last of a row of mouse motion events in the event queue)
140
141 static boolean SkipPressedMouseMotionEvent(const Event *event)
142 {
143   // nothing to do if the current event is not a mouse motion event
144   if (event->type != EVENT_MOTIONNOTIFY)
145     return FALSE;
146
147   // only skip motion events with pressed button outside the game
148   if (button_status == MB_RELEASED || game_status == GAME_MODE_PLAYING)
149     return FALSE;
150
151   if (PendingEvent())
152   {
153     Event next_event;
154
155     PeekEvent(&next_event);
156
157     // if next event is also a mouse motion event, skip the current one
158     if (next_event.type == EVENT_MOTIONNOTIFY)
159       return TRUE;
160   }
161
162   return FALSE;
163 }
164
165 static boolean WaitValidEvent(Event *event)
166 {
167   WaitEvent(event);
168
169   if (!FilterEvents(event))
170     return FALSE;
171
172   if (SkipPressedMouseMotionEvent(event))
173     return FALSE;
174
175   return TRUE;
176 }
177
178 /* this is especially needed for event modifications for the Android target:
179    if mouse coordinates should be modified in the event filter function,
180    using a properly installed SDL event filter does not work, because in
181    the event filter, mouse coordinates in the event structure are still
182    physical pixel positions, not logical (scaled) screen positions, so this
183    has to be handled at a later stage in the event processing functions
184    (when device pixel positions are already converted to screen positions) */
185
186 boolean NextValidEvent(Event *event)
187 {
188   while (PendingEvent())
189     if (WaitValidEvent(event))
190       return TRUE;
191
192   return FALSE;
193 }
194
195 void StopProcessingEvents(void)
196 {
197   stop_processing_events = TRUE;
198 }
199
200 static void HandleEvents(void)
201 {
202   Event event;
203   unsigned int event_frame_delay = 0;
204   unsigned int event_frame_delay_value = GAME_FRAME_DELAY;
205
206   ResetDelayCounter(&event_frame_delay);
207
208   stop_processing_events = FALSE;
209
210   while (NextValidEvent(&event))
211   {
212     switch (event.type)
213     {
214       case EVENT_BUTTONPRESS:
215       case EVENT_BUTTONRELEASE:
216         HandleButtonEvent((ButtonEvent *) &event);
217         break;
218
219       case EVENT_MOTIONNOTIFY:
220         HandleMotionEvent((MotionEvent *) &event);
221         break;
222
223       case EVENT_WHEELMOTION:
224         HandleWheelEvent((WheelEvent *) &event);
225         break;
226
227       case SDL_WINDOWEVENT:
228         HandleWindowEvent((WindowEvent *) &event);
229         break;
230
231       case EVENT_FINGERPRESS:
232       case EVENT_FINGERRELEASE:
233       case EVENT_FINGERMOTION:
234         HandleFingerEvent((FingerEvent *) &event);
235         break;
236
237       case EVENT_TEXTINPUT:
238         HandleTextEvent((TextEvent *) &event);
239         break;
240
241       case SDL_APP_WILLENTERBACKGROUND:
242       case SDL_APP_DIDENTERBACKGROUND:
243       case SDL_APP_WILLENTERFOREGROUND:
244       case SDL_APP_DIDENTERFOREGROUND:
245         HandlePauseResumeEvent((PauseResumeEvent *) &event);
246         break;
247
248       case EVENT_KEYPRESS:
249       case EVENT_KEYRELEASE:
250         HandleKeyEvent((KeyEvent *) &event);
251         break;
252
253       case EVENT_USER:
254         HandleUserEvent((UserEvent *) &event);
255         break;
256
257       default:
258         HandleOtherEvents(&event);
259         break;
260     }
261
262     // do not handle events for longer than standard frame delay period
263     if (DelayReached(&event_frame_delay, event_frame_delay_value))
264       break;
265
266     // do not handle any further events if triggered by a special flag
267     if (stop_processing_events)
268       break;
269   }
270 }
271
272 void HandleOtherEvents(Event *event)
273 {
274   switch (event->type)
275   {
276     case SDL_CONTROLLERBUTTONDOWN:
277     case SDL_CONTROLLERBUTTONUP:
278       // for any game controller button event, disable overlay buttons
279       SetOverlayEnabled(FALSE);
280
281       HandleSpecialGameControllerButtons(event);
282
283       // FALL THROUGH
284     case SDL_CONTROLLERDEVICEADDED:
285     case SDL_CONTROLLERDEVICEREMOVED:
286     case SDL_CONTROLLERAXISMOTION:
287     case SDL_JOYAXISMOTION:
288     case SDL_JOYBUTTONDOWN:
289     case SDL_JOYBUTTONUP:
290       HandleJoystickEvent(event);
291       break;
292
293     case SDL_DROPBEGIN:
294     case SDL_DROPCOMPLETE:
295     case SDL_DROPFILE:
296     case SDL_DROPTEXT:
297       HandleDropEvent(event);
298       break;
299
300     case EVENT_QUIT:
301       CloseAllAndExit(0);
302       break;
303
304     default:
305       break;
306   }
307 }
308
309 static void HandleMouseCursor(void)
310 {
311   if (game_status == GAME_MODE_TITLE)
312   {
313     // when showing title screens, hide mouse pointer (if not moved)
314
315     if (gfx.cursor_mode != CURSOR_NONE &&
316         DelayReached(&special_cursor_delay, special_cursor_delay_value))
317     {
318       SetMouseCursor(CURSOR_NONE);
319     }
320   }
321   else if (game_status == GAME_MODE_PLAYING && (!tape.pausing ||
322                                                 tape.single_step))
323   {
324     // when playing, display a special mouse pointer inside the playfield
325
326     if (gfx.cursor_mode != CURSOR_PLAYFIELD &&
327         cursor_inside_playfield &&
328         DelayReached(&special_cursor_delay, special_cursor_delay_value))
329     {
330       if (level.game_engine_type != GAME_ENGINE_TYPE_MM ||
331           tile_cursor.enabled)
332         SetMouseCursor(CURSOR_PLAYFIELD);
333     }
334   }
335   else if (gfx.cursor_mode != CURSOR_DEFAULT)
336   {
337     SetMouseCursor(CURSOR_DEFAULT);
338   }
339
340   // this is set after all pending events have been processed
341   cursor_mode_last = gfx.cursor_mode;
342 }
343
344 void EventLoop(void)
345 {
346   while (1)
347   {
348     if (PendingEvent())
349       HandleEvents();
350     else
351       HandleNoEvent();
352
353     // execute event related actions after pending events have been processed
354     HandleEventActions();
355
356     // don't use all CPU time when idle; the main loop while playing
357     // has its own synchronization and is CPU friendly, too
358
359     if (game_status == GAME_MODE_PLAYING)
360       HandleGameActions();
361
362     // always copy backbuffer to visible screen for every video frame
363     BackToFront();
364
365     // reset video frame delay to default (may change again while playing)
366     SetVideoFrameDelay(MenuFrameDelay);
367
368     if (game_status == GAME_MODE_QUIT)
369       return;
370   }
371 }
372
373 void ClearAutoRepeatKeyEvents(void)
374 {
375   while (PendingEvent())
376   {
377     Event next_event;
378
379     PeekEvent(&next_event);
380
381     // if event is repeated key press event, remove it from event queue
382     if (next_event.type == EVENT_KEYPRESS &&
383         next_event.key.repeat)
384       WaitEvent(&next_event);
385     else
386       break;
387   }
388 }
389
390 void ClearEventQueue(void)
391 {
392   Event event;
393
394   while (NextValidEvent(&event))
395   {
396     switch (event.type)
397     {
398       case EVENT_BUTTONRELEASE:
399         button_status = MB_RELEASED;
400         break;
401
402       case EVENT_KEYRELEASE:
403         ClearPlayerAction();
404         break;
405
406       case SDL_CONTROLLERBUTTONUP:
407         HandleJoystickEvent(&event);
408         ClearPlayerAction();
409         break;
410
411       default:
412         HandleOtherEvents(&event);
413         break;
414     }
415   }
416 }
417
418 static void ClearPlayerMouseAction(void)
419 {
420   local_player->mouse_action.lx = 0;
421   local_player->mouse_action.ly = 0;
422   local_player->mouse_action.button = 0;
423 }
424
425 void ClearPlayerAction(void)
426 {
427   int i;
428
429   // simulate key release events for still pressed keys
430   key_joystick_mapping = 0;
431   for (i = 0; i < MAX_PLAYERS; i++)
432   {
433     stored_player[i].action = 0;
434     stored_player[i].snap_action = 0;
435   }
436
437   ClearJoystickState();
438   ClearPlayerMouseAction();
439 }
440
441 static void SetPlayerMouseAction(int mx, int my, int button)
442 {
443   int lx = getLevelFromScreenX(mx);
444   int ly = getLevelFromScreenY(my);
445   int new_button = (!local_player->mouse_action.button && button);
446
447   if (local_player->mouse_action.button_hint)
448     button = local_player->mouse_action.button_hint;
449
450   ClearPlayerMouseAction();
451
452   if (!IN_GFX_FIELD_PLAY(mx, my) || !IN_LEV_FIELD(lx, ly))
453     return;
454
455   local_player->mouse_action.lx = lx;
456   local_player->mouse_action.ly = ly;
457   local_player->mouse_action.button = button;
458
459   if (tape.recording && tape.pausing && tape.use_mouse)
460   {
461     // un-pause a paused game only if mouse button was newly pressed down
462     if (new_button)
463       TapeTogglePause(TAPE_TOGGLE_AUTOMATIC);
464   }
465
466   SetTileCursorXY(lx, ly);
467 }
468
469 void HandleButtonEvent(ButtonEvent *event)
470 {
471 #if DEBUG_EVENTS_BUTTON
472   Error(ERR_DEBUG, "BUTTON EVENT: button %d %s, x/y %d/%d\n",
473         event->button,
474         event->type == EVENT_BUTTONPRESS ? "pressed" : "released",
475         event->x, event->y);
476 #endif
477
478   // for any mouse button event, disable playfield tile cursor
479   SetTileCursorEnabled(FALSE);
480
481 #if defined(HAS_SCREEN_KEYBOARD)
482   if (video.shifted_up)
483     event->y += video.shifted_up_pos;
484 #endif
485
486   motion_status = FALSE;
487
488   if (event->type == EVENT_BUTTONPRESS)
489     button_status = event->button;
490   else
491     button_status = MB_RELEASED;
492
493   HandleButton(event->x, event->y, button_status, event->button);
494 }
495
496 void HandleMotionEvent(MotionEvent *event)
497 {
498   if (button_status == MB_RELEASED && game_status != GAME_MODE_EDITOR)
499     return;
500
501   motion_status = TRUE;
502
503 #if DEBUG_EVENTS_MOTION
504   Error(ERR_DEBUG, "MOTION EVENT: button %d moved, x/y %d/%d\n",
505         button_status, event->x, event->y);
506 #endif
507
508   HandleButton(event->x, event->y, button_status, button_status);
509 }
510
511 void HandleWheelEvent(WheelEvent *event)
512 {
513   int button_nr;
514
515 #if DEBUG_EVENTS_WHEEL
516 #if 1
517   Error(ERR_DEBUG, "WHEEL EVENT: mouse == %d, x/y == %d/%d\n",
518         event->which, event->x, event->y);
519 #else
520   // (SDL_MOUSEWHEEL_NORMAL/SDL_MOUSEWHEEL_FLIPPED needs SDL 2.0.4 or newer)
521   Error(ERR_DEBUG, "WHEEL EVENT: mouse == %d, x/y == %d/%d, direction == %s\n",
522         event->which, event->x, event->y,
523         (event->direction == SDL_MOUSEWHEEL_NORMAL ? "SDL_MOUSEWHEEL_NORMAL" :
524          "SDL_MOUSEWHEEL_FLIPPED"));
525 #endif
526 #endif
527
528   button_nr = (event->x < 0 ? MB_WHEEL_LEFT :
529                event->x > 0 ? MB_WHEEL_RIGHT :
530                event->y < 0 ? MB_WHEEL_DOWN :
531                event->y > 0 ? MB_WHEEL_UP : 0);
532
533 #if defined(PLATFORM_WIN32) || defined(PLATFORM_MACOSX)
534   // accelerated mouse wheel available on Mac and Windows
535   wheel_steps = (event->x ? ABS(event->x) : ABS(event->y));
536 #else
537   // no accelerated mouse wheel available on Unix/Linux
538   wheel_steps = DEFAULT_WHEEL_STEPS;
539 #endif
540
541   motion_status = FALSE;
542
543   button_status = button_nr;
544   HandleButton(0, 0, button_status, -button_nr);
545
546   button_status = MB_RELEASED;
547   HandleButton(0, 0, button_status, -button_nr);
548 }
549
550 void HandleWindowEvent(WindowEvent *event)
551 {
552 #if DEBUG_EVENTS_WINDOW
553   int subtype = event->event;
554
555   char *event_name =
556     (subtype == SDL_WINDOWEVENT_SHOWN ? "SDL_WINDOWEVENT_SHOWN" :
557      subtype == SDL_WINDOWEVENT_HIDDEN ? "SDL_WINDOWEVENT_HIDDEN" :
558      subtype == SDL_WINDOWEVENT_EXPOSED ? "SDL_WINDOWEVENT_EXPOSED" :
559      subtype == SDL_WINDOWEVENT_MOVED ? "SDL_WINDOWEVENT_MOVED" :
560      subtype == SDL_WINDOWEVENT_SIZE_CHANGED ? "SDL_WINDOWEVENT_SIZE_CHANGED" :
561      subtype == SDL_WINDOWEVENT_RESIZED ? "SDL_WINDOWEVENT_RESIZED" :
562      subtype == SDL_WINDOWEVENT_MINIMIZED ? "SDL_WINDOWEVENT_MINIMIZED" :
563      subtype == SDL_WINDOWEVENT_MAXIMIZED ? "SDL_WINDOWEVENT_MAXIMIZED" :
564      subtype == SDL_WINDOWEVENT_RESTORED ? "SDL_WINDOWEVENT_RESTORED" :
565      subtype == SDL_WINDOWEVENT_ENTER ? "SDL_WINDOWEVENT_ENTER" :
566      subtype == SDL_WINDOWEVENT_LEAVE ? "SDL_WINDOWEVENT_LEAVE" :
567      subtype == SDL_WINDOWEVENT_FOCUS_GAINED ? "SDL_WINDOWEVENT_FOCUS_GAINED" :
568      subtype == SDL_WINDOWEVENT_FOCUS_LOST ? "SDL_WINDOWEVENT_FOCUS_LOST" :
569      subtype == SDL_WINDOWEVENT_CLOSE ? "SDL_WINDOWEVENT_CLOSE" :
570      "(UNKNOWN)");
571
572   Error(ERR_DEBUG, "WINDOW EVENT: '%s', %ld, %ld",
573         event_name, event->data1, event->data2);
574 #endif
575
576 #if 0
577   // (not needed, as the screen gets redrawn every 20 ms anyway)
578   if (event->event == SDL_WINDOWEVENT_SIZE_CHANGED ||
579       event->event == SDL_WINDOWEVENT_RESIZED ||
580       event->event == SDL_WINDOWEVENT_EXPOSED)
581     SDLRedrawWindow();
582 #endif
583
584   if (event->event == SDL_WINDOWEVENT_RESIZED)
585   {
586     if (!video.fullscreen_enabled)
587     {
588       int new_window_width  = event->data1;
589       int new_window_height = event->data2;
590
591       // if window size has changed after resizing, calculate new scaling factor
592       if (new_window_width  != video.window_width ||
593           new_window_height != video.window_height)
594       {
595         int new_xpercent = 100.0 * new_window_width  / video.screen_width  + .5;
596         int new_ypercent = 100.0 * new_window_height / video.screen_height + .5;
597
598         // (extreme window scaling allowed, but cannot be saved permanently)
599         video.window_scaling_percent = MIN(new_xpercent, new_ypercent);
600         setup.window_scaling_percent =
601           MIN(MAX(MIN_WINDOW_SCALING_PERCENT, video.window_scaling_percent),
602               MAX_WINDOW_SCALING_PERCENT);
603
604         video.window_width  = new_window_width;
605         video.window_height = new_window_height;
606
607         if (game_status == GAME_MODE_SETUP)
608           RedrawSetupScreenAfterFullscreenToggle();
609
610         UpdateMousePosition();
611
612         SetWindowTitle();
613       }
614     }
615 #if defined(PLATFORM_ANDROID)
616     else
617     {
618       int new_display_width  = event->data1;
619       int new_display_height = event->data2;
620
621       // if fullscreen display size has changed, device has been rotated
622       if (new_display_width  != video.display_width ||
623           new_display_height != video.display_height)
624       {
625         int nr = GRID_ACTIVE_NR();      // previous screen orientation
626
627         video.display_width  = new_display_width;
628         video.display_height = new_display_height;
629
630         SDLSetScreenProperties();
631
632         // check if screen orientation has changed (should always be true here)
633         if (nr != GRID_ACTIVE_NR())
634         {
635           int x, y;
636
637           if (game_status == GAME_MODE_SETUP)
638             RedrawSetupScreenAfterScreenRotation(nr);
639
640           nr = GRID_ACTIVE_NR();
641
642           overlay.grid_xsize = setup.touch.grid_xsize[nr];
643           overlay.grid_ysize = setup.touch.grid_ysize[nr];
644
645           for (x = 0; x < MAX_GRID_XSIZE; x++)
646             for (y = 0; y < MAX_GRID_YSIZE; y++)
647               overlay.grid_button[x][y] = setup.touch.grid_button[nr][x][y];
648         }
649       }
650     }
651 #endif
652   }
653 }
654
655 #define NUM_TOUCH_FINGERS               3
656
657 static struct
658 {
659   boolean touched;
660   SDL_FingerID finger_id;
661   int counter;
662   Key key;
663   byte action;
664 } touch_info[NUM_TOUCH_FINGERS];
665
666 static void HandleFingerEvent_VirtualButtons(FingerEvent *event)
667 {
668 #if 1
669   int x = event->x * overlay.grid_xsize;
670   int y = event->y * overlay.grid_ysize;
671   int grid_button = overlay.grid_button[x][y];
672   int grid_button_action = GET_ACTION_FROM_GRID_BUTTON(grid_button);
673   Key key = (grid_button == CHAR_GRID_BUTTON_LEFT  ? setup.input[0].key.left :
674              grid_button == CHAR_GRID_BUTTON_RIGHT ? setup.input[0].key.right :
675              grid_button == CHAR_GRID_BUTTON_UP    ? setup.input[0].key.up :
676              grid_button == CHAR_GRID_BUTTON_DOWN  ? setup.input[0].key.down :
677              grid_button == CHAR_GRID_BUTTON_SNAP  ? setup.input[0].key.snap :
678              grid_button == CHAR_GRID_BUTTON_DROP  ? setup.input[0].key.drop :
679              KSYM_UNDEFINED);
680 #else
681   float ypos = 1.0 - 1.0 / 3.0 * video.display_width / video.display_height;
682   float event_x = (event->x);
683   float event_y = (event->y - ypos) / (1 - ypos);
684   Key key = (event_x > 0         && event_x < 1.0 / 6.0 &&
685              event_y > 2.0 / 3.0 && event_y < 1 ?
686              setup.input[0].key.snap :
687              event_x > 1.0 / 6.0 && event_x < 1.0 / 3.0 &&
688              event_y > 2.0 / 3.0 && event_y < 1 ?
689              setup.input[0].key.drop :
690              event_x > 7.0 / 9.0 && event_x < 8.0 / 9.0 &&
691              event_y > 0         && event_y < 1.0 / 3.0 ?
692              setup.input[0].key.up :
693              event_x > 6.0 / 9.0 && event_x < 7.0 / 9.0 &&
694              event_y > 1.0 / 3.0 && event_y < 2.0 / 3.0 ?
695              setup.input[0].key.left :
696              event_x > 8.0 / 9.0 && event_x < 1 &&
697              event_y > 1.0 / 3.0 && event_y < 2.0 / 3.0 ?
698              setup.input[0].key.right :
699              event_x > 7.0 / 9.0 && event_x < 8.0 / 9.0 &&
700              event_y > 2.0 / 3.0 && event_y < 1 ?
701              setup.input[0].key.down :
702              KSYM_UNDEFINED);
703 #endif
704   int key_status = (event->type == EVENT_FINGERRELEASE ? KEY_RELEASED :
705                     KEY_PRESSED);
706   char *key_status_name = (key_status == KEY_RELEASED ? "KEY_RELEASED" :
707                            "KEY_PRESSED");
708   int i;
709
710   virtual_button_pressed = (key_status == KEY_PRESSED && key != KSYM_UNDEFINED);
711
712   // for any touch input event, enable overlay buttons (if activated)
713   SetOverlayEnabled(TRUE);
714
715   Error(ERR_DEBUG, "::: key '%s' was '%s' [fingerId: %lld]",
716         getKeyNameFromKey(key), key_status_name, event->fingerId);
717
718   if (key_status == KEY_PRESSED)
719     overlay.grid_button_action |= grid_button_action;
720   else
721     overlay.grid_button_action &= ~grid_button_action;
722
723   // check if we already know this touch event's finger id
724   for (i = 0; i < NUM_TOUCH_FINGERS; i++)
725   {
726     if (touch_info[i].touched &&
727         touch_info[i].finger_id == event->fingerId)
728     {
729       // Error(ERR_DEBUG, "MARK 1: %d", i);
730
731       break;
732     }
733   }
734
735   if (i >= NUM_TOUCH_FINGERS)
736   {
737     if (key_status == KEY_PRESSED)
738     {
739       int oldest_pos = 0, oldest_counter = touch_info[0].counter;
740
741       // unknown finger id -- get new, empty slot, if available
742       for (i = 0; i < NUM_TOUCH_FINGERS; i++)
743       {
744         if (touch_info[i].counter < oldest_counter)
745         {
746           oldest_pos = i;
747           oldest_counter = touch_info[i].counter;
748
749           // Error(ERR_DEBUG, "MARK 2: %d", i);
750         }
751
752         if (!touch_info[i].touched)
753         {
754           // Error(ERR_DEBUG, "MARK 3: %d", i);
755
756           break;
757         }
758       }
759
760       if (i >= NUM_TOUCH_FINGERS)
761       {
762         // all slots allocated -- use oldest slot
763         i = oldest_pos;
764
765         // Error(ERR_DEBUG, "MARK 4: %d", i);
766       }
767     }
768     else
769     {
770       // release of previously unknown key (should not happen)
771
772       if (key != KSYM_UNDEFINED)
773       {
774         HandleKey(key, KEY_RELEASED);
775
776         Error(ERR_DEBUG, "=> key == '%s', key_status == '%s' [slot %d] [1]",
777               getKeyNameFromKey(key), "KEY_RELEASED", i);
778       }
779     }
780   }
781
782   if (i < NUM_TOUCH_FINGERS)
783   {
784     if (key_status == KEY_PRESSED)
785     {
786       if (touch_info[i].key != key)
787       {
788         if (touch_info[i].key != KSYM_UNDEFINED)
789         {
790           HandleKey(touch_info[i].key, KEY_RELEASED);
791
792           // undraw previous grid button when moving finger away
793           overlay.grid_button_action &= ~touch_info[i].action;
794
795           Error(ERR_DEBUG, "=> key == '%s', key_status == '%s' [slot %d] [2]",
796                 getKeyNameFromKey(touch_info[i].key), "KEY_RELEASED", i);
797         }
798
799         if (key != KSYM_UNDEFINED)
800         {
801           HandleKey(key, KEY_PRESSED);
802
803           Error(ERR_DEBUG, "=> key == '%s', key_status == '%s' [slot %d] [3]",
804                 getKeyNameFromKey(key), "KEY_PRESSED", i);
805         }
806       }
807
808       touch_info[i].touched = TRUE;
809       touch_info[i].finger_id = event->fingerId;
810       touch_info[i].counter = Counter();
811       touch_info[i].key = key;
812       touch_info[i].action = grid_button_action;
813     }
814     else
815     {
816       if (touch_info[i].key != KSYM_UNDEFINED)
817       {
818         HandleKey(touch_info[i].key, KEY_RELEASED);
819
820         Error(ERR_DEBUG, "=> key == '%s', key_status == '%s' [slot %d] [4]",
821               getKeyNameFromKey(touch_info[i].key), "KEY_RELEASED", i);
822       }
823
824       touch_info[i].touched = FALSE;
825       touch_info[i].finger_id = 0;
826       touch_info[i].counter = 0;
827       touch_info[i].key = 0;
828       touch_info[i].action = JOY_NO_ACTION;
829     }
830   }
831 }
832
833 static void HandleFingerEvent_WipeGestures(FingerEvent *event)
834 {
835   static Key motion_key_x = KSYM_UNDEFINED;
836   static Key motion_key_y = KSYM_UNDEFINED;
837   static Key button_key = KSYM_UNDEFINED;
838   static float motion_x1, motion_y1;
839   static float button_x1, button_y1;
840   static SDL_FingerID motion_id = -1;
841   static SDL_FingerID button_id = -1;
842   int move_trigger_distance_percent = setup.touch.move_distance;
843   int drop_trigger_distance_percent = setup.touch.drop_distance;
844   float move_trigger_distance = (float)move_trigger_distance_percent / 100;
845   float drop_trigger_distance = (float)drop_trigger_distance_percent / 100;
846   float event_x = event->x;
847   float event_y = event->y;
848
849   if (event->type == EVENT_FINGERPRESS)
850   {
851     if (event_x > 1.0 / 3.0)
852     {
853       // motion area
854
855       motion_id = event->fingerId;
856
857       motion_x1 = event_x;
858       motion_y1 = event_y;
859
860       motion_key_x = KSYM_UNDEFINED;
861       motion_key_y = KSYM_UNDEFINED;
862
863       Error(ERR_DEBUG, "---------- MOVE STARTED (WAIT) ----------");
864     }
865     else
866     {
867       // button area
868
869       button_id = event->fingerId;
870
871       button_x1 = event_x;
872       button_y1 = event_y;
873
874       button_key = setup.input[0].key.snap;
875
876       HandleKey(button_key, KEY_PRESSED);
877
878       Error(ERR_DEBUG, "---------- SNAP STARTED ----------");
879     }
880   }
881   else if (event->type == EVENT_FINGERRELEASE)
882   {
883     if (event->fingerId == motion_id)
884     {
885       motion_id = -1;
886
887       if (motion_key_x != KSYM_UNDEFINED)
888         HandleKey(motion_key_x, KEY_RELEASED);
889       if (motion_key_y != KSYM_UNDEFINED)
890         HandleKey(motion_key_y, KEY_RELEASED);
891
892       motion_key_x = KSYM_UNDEFINED;
893       motion_key_y = KSYM_UNDEFINED;
894
895       Error(ERR_DEBUG, "---------- MOVE STOPPED ----------");
896     }
897     else if (event->fingerId == button_id)
898     {
899       button_id = -1;
900
901       if (button_key != KSYM_UNDEFINED)
902         HandleKey(button_key, KEY_RELEASED);
903
904       button_key = KSYM_UNDEFINED;
905
906       Error(ERR_DEBUG, "---------- SNAP STOPPED ----------");
907     }
908   }
909   else if (event->type == EVENT_FINGERMOTION)
910   {
911     if (event->fingerId == motion_id)
912     {
913       float distance_x = ABS(event_x - motion_x1);
914       float distance_y = ABS(event_y - motion_y1);
915       Key new_motion_key_x = (event_x < motion_x1 ? setup.input[0].key.left :
916                               event_x > motion_x1 ? setup.input[0].key.right :
917                               KSYM_UNDEFINED);
918       Key new_motion_key_y = (event_y < motion_y1 ? setup.input[0].key.up :
919                               event_y > motion_y1 ? setup.input[0].key.down :
920                               KSYM_UNDEFINED);
921
922       if (distance_x < move_trigger_distance / 2 ||
923           distance_x < distance_y)
924         new_motion_key_x = KSYM_UNDEFINED;
925
926       if (distance_y < move_trigger_distance / 2 ||
927           distance_y < distance_x)
928         new_motion_key_y = KSYM_UNDEFINED;
929
930       if (distance_x > move_trigger_distance ||
931           distance_y > move_trigger_distance)
932       {
933         if (new_motion_key_x != motion_key_x)
934         {
935           if (motion_key_x != KSYM_UNDEFINED)
936             HandleKey(motion_key_x, KEY_RELEASED);
937           if (new_motion_key_x != KSYM_UNDEFINED)
938             HandleKey(new_motion_key_x, KEY_PRESSED);
939         }
940
941         if (new_motion_key_y != motion_key_y)
942         {
943           if (motion_key_y != KSYM_UNDEFINED)
944             HandleKey(motion_key_y, KEY_RELEASED);
945           if (new_motion_key_y != KSYM_UNDEFINED)
946             HandleKey(new_motion_key_y, KEY_PRESSED);
947         }
948
949         motion_x1 = event_x;
950         motion_y1 = event_y;
951
952         motion_key_x = new_motion_key_x;
953         motion_key_y = new_motion_key_y;
954
955         Error(ERR_DEBUG, "---------- MOVE STARTED (MOVE) ----------");
956       }
957     }
958     else if (event->fingerId == button_id)
959     {
960       float distance_x = ABS(event_x - button_x1);
961       float distance_y = ABS(event_y - button_y1);
962
963       if (distance_x < drop_trigger_distance / 2 &&
964           distance_y > drop_trigger_distance)
965       {
966         if (button_key == setup.input[0].key.snap)
967           HandleKey(button_key, KEY_RELEASED);
968
969         button_x1 = event_x;
970         button_y1 = event_y;
971
972         button_key = setup.input[0].key.drop;
973
974         HandleKey(button_key, KEY_PRESSED);
975
976         Error(ERR_DEBUG, "---------- DROP STARTED ----------");
977       }
978     }
979   }
980 }
981
982 void HandleFingerEvent(FingerEvent *event)
983 {
984 #if DEBUG_EVENTS_FINGER
985   Error(ERR_DEBUG, "FINGER EVENT: finger was %s, touch ID %lld, finger ID %lld, x/y %f/%f, dx/dy %f/%f, pressure %f",
986         event->type == EVENT_FINGERPRESS ? "pressed" :
987         event->type == EVENT_FINGERRELEASE ? "released" : "moved",
988         event->touchId,
989         event->fingerId,
990         event->x, event->y,
991         event->dx, event->dy,
992         event->pressure);
993 #endif
994
995   runtime.uses_touch_device = TRUE;
996
997   if (game_status != GAME_MODE_PLAYING)
998     return;
999
1000   if (level.game_engine_type == GAME_ENGINE_TYPE_MM)
1001   {
1002     if (strEqual(setup.touch.control_type, TOUCH_CONTROL_OFF))
1003       local_player->mouse_action.button_hint =
1004         (event->type == EVENT_FINGERRELEASE ? MB_NOT_PRESSED :
1005          event->x < 0.5                     ? MB_LEFTBUTTON  :
1006          event->x > 0.5                     ? MB_RIGHTBUTTON :
1007          MB_NOT_PRESSED);
1008
1009     return;
1010   }
1011
1012   if (strEqual(setup.touch.control_type, TOUCH_CONTROL_VIRTUAL_BUTTONS))
1013     HandleFingerEvent_VirtualButtons(event);
1014   else if (strEqual(setup.touch.control_type, TOUCH_CONTROL_WIPE_GESTURES))
1015     HandleFingerEvent_WipeGestures(event);
1016 }
1017
1018 static void HandleButtonOrFinger_WipeGestures_MM(int mx, int my, int button)
1019 {
1020   static int old_mx = 0, old_my = 0;
1021   static int last_button = MB_LEFTBUTTON;
1022   static boolean touched = FALSE;
1023   static boolean tapped = FALSE;
1024
1025   // screen tile was tapped (but finger not touching the screen anymore)
1026   // (this point will also be reached without receiving a touch event)
1027   if (tapped && !touched)
1028   {
1029     SetPlayerMouseAction(old_mx, old_my, MB_RELEASED);
1030
1031     tapped = FALSE;
1032   }
1033
1034   // stop here if this function was not triggered by a touch event
1035   if (button == -1)
1036     return;
1037
1038   if (button == MB_PRESSED && IN_GFX_FIELD_PLAY(mx, my))
1039   {
1040     // finger started touching the screen
1041
1042     touched = TRUE;
1043     tapped = TRUE;
1044
1045     if (!motion_status)
1046     {
1047       old_mx = mx;
1048       old_my = my;
1049
1050       ClearPlayerMouseAction();
1051
1052       Error(ERR_DEBUG, "---------- TOUCH ACTION STARTED ----------");
1053     }
1054   }
1055   else if (button == MB_RELEASED && touched)
1056   {
1057     // finger stopped touching the screen
1058
1059     touched = FALSE;
1060
1061     if (tapped)
1062       SetPlayerMouseAction(old_mx, old_my, last_button);
1063     else
1064       SetPlayerMouseAction(old_mx, old_my, MB_RELEASED);
1065
1066     Error(ERR_DEBUG, "---------- TOUCH ACTION STOPPED ----------");
1067   }
1068
1069   if (touched)
1070   {
1071     // finger moved while touching the screen
1072
1073     int old_x = getLevelFromScreenX(old_mx);
1074     int old_y = getLevelFromScreenY(old_my);
1075     int new_x = getLevelFromScreenX(mx);
1076     int new_y = getLevelFromScreenY(my);
1077
1078     if (new_x != old_x || new_y != old_y)
1079       tapped = FALSE;
1080
1081     if (new_x != old_x)
1082     {
1083       // finger moved left or right from (horizontal) starting position
1084
1085       int button_nr = (new_x < old_x ? MB_LEFTBUTTON : MB_RIGHTBUTTON);
1086
1087       SetPlayerMouseAction(old_mx, old_my, button_nr);
1088
1089       last_button = button_nr;
1090
1091       Error(ERR_DEBUG, "---------- TOUCH ACTION: ROTATING ----------");
1092     }
1093     else
1094     {
1095       // finger stays at or returned to (horizontal) starting position
1096
1097       SetPlayerMouseAction(old_mx, old_my, MB_RELEASED);
1098
1099       Error(ERR_DEBUG, "---------- TOUCH ACTION PAUSED ----------");
1100     }
1101   }
1102 }
1103
1104 static void HandleButtonOrFinger_FollowFinger_MM(int mx, int my, int button)
1105 {
1106   static int old_mx = 0, old_my = 0;
1107   static int last_button = MB_LEFTBUTTON;
1108   static boolean touched = FALSE;
1109   static boolean tapped = FALSE;
1110
1111   // screen tile was tapped (but finger not touching the screen anymore)
1112   // (this point will also be reached without receiving a touch event)
1113   if (tapped && !touched)
1114   {
1115     SetPlayerMouseAction(old_mx, old_my, MB_RELEASED);
1116
1117     tapped = FALSE;
1118   }
1119
1120   // stop here if this function was not triggered by a touch event
1121   if (button == -1)
1122     return;
1123
1124   if (button == MB_PRESSED && IN_GFX_FIELD_PLAY(mx, my))
1125   {
1126     // finger started touching the screen
1127
1128     touched = TRUE;
1129     tapped = TRUE;
1130
1131     if (!motion_status)
1132     {
1133       old_mx = mx;
1134       old_my = my;
1135
1136       ClearPlayerMouseAction();
1137
1138       Error(ERR_DEBUG, "---------- TOUCH ACTION STARTED ----------");
1139     }
1140   }
1141   else if (button == MB_RELEASED && touched)
1142   {
1143     // finger stopped touching the screen
1144
1145     touched = FALSE;
1146
1147     if (tapped)
1148       SetPlayerMouseAction(old_mx, old_my, last_button);
1149     else
1150       SetPlayerMouseAction(old_mx, old_my, MB_RELEASED);
1151
1152     Error(ERR_DEBUG, "---------- TOUCH ACTION STOPPED ----------");
1153   }
1154
1155   if (touched)
1156   {
1157     // finger moved while touching the screen
1158
1159     int old_x = getLevelFromScreenX(old_mx);
1160     int old_y = getLevelFromScreenY(old_my);
1161     int new_x = getLevelFromScreenX(mx);
1162     int new_y = getLevelFromScreenY(my);
1163
1164     if (new_x != old_x || new_y != old_y)
1165     {
1166       // finger moved away from starting position
1167
1168       int button_nr = getButtonFromTouchPosition(old_x, old_y, mx, my);
1169
1170       // quickly alternate between clicking and releasing for maximum speed
1171       if (FrameCounter % 2 == 0)
1172         button_nr = MB_RELEASED;
1173
1174       SetPlayerMouseAction(old_mx, old_my, button_nr);
1175
1176       if (button_nr)
1177         last_button = button_nr;
1178
1179       tapped = FALSE;
1180
1181       Error(ERR_DEBUG, "---------- TOUCH ACTION: ROTATING ----------");
1182     }
1183     else
1184     {
1185       // finger stays at or returned to starting position
1186
1187       SetPlayerMouseAction(old_mx, old_my, MB_RELEASED);
1188
1189       Error(ERR_DEBUG, "---------- TOUCH ACTION PAUSED ----------");
1190     }
1191   }
1192 }
1193
1194 static void HandleButtonOrFinger_FollowFinger(int mx, int my, int button)
1195 {
1196   static int old_mx = 0, old_my = 0;
1197   static Key motion_key_x = KSYM_UNDEFINED;
1198   static Key motion_key_y = KSYM_UNDEFINED;
1199   static boolean touched = FALSE;
1200   static boolean started_on_player = FALSE;
1201   static boolean player_is_dropping = FALSE;
1202   static int player_drop_count = 0;
1203   static int last_player_x = -1;
1204   static int last_player_y = -1;
1205
1206   if (button == MB_PRESSED && IN_GFX_FIELD_PLAY(mx, my))
1207   {
1208     touched = TRUE;
1209
1210     old_mx = mx;
1211     old_my = my;
1212
1213     if (!motion_status)
1214     {
1215       started_on_player = FALSE;
1216       player_is_dropping = FALSE;
1217       player_drop_count = 0;
1218       last_player_x = -1;
1219       last_player_y = -1;
1220
1221       motion_key_x = KSYM_UNDEFINED;
1222       motion_key_y = KSYM_UNDEFINED;
1223
1224       Error(ERR_DEBUG, "---------- TOUCH ACTION STARTED ----------");
1225     }
1226   }
1227   else if (button == MB_RELEASED && touched)
1228   {
1229     touched = FALSE;
1230
1231     old_mx = 0;
1232     old_my = 0;
1233
1234     if (motion_key_x != KSYM_UNDEFINED)
1235       HandleKey(motion_key_x, KEY_RELEASED);
1236     if (motion_key_y != KSYM_UNDEFINED)
1237       HandleKey(motion_key_y, KEY_RELEASED);
1238
1239     if (started_on_player)
1240     {
1241       if (player_is_dropping)
1242       {
1243         Error(ERR_DEBUG, "---------- DROP STOPPED ----------");
1244
1245         HandleKey(setup.input[0].key.drop, KEY_RELEASED);
1246       }
1247       else
1248       {
1249         Error(ERR_DEBUG, "---------- SNAP STOPPED ----------");
1250
1251         HandleKey(setup.input[0].key.snap, KEY_RELEASED);
1252       }
1253     }
1254
1255     motion_key_x = KSYM_UNDEFINED;
1256     motion_key_y = KSYM_UNDEFINED;
1257
1258     Error(ERR_DEBUG, "---------- TOUCH ACTION STOPPED ----------");
1259   }
1260
1261   if (touched)
1262   {
1263     int src_x = local_player->jx;
1264     int src_y = local_player->jy;
1265     int dst_x = getLevelFromScreenX(old_mx);
1266     int dst_y = getLevelFromScreenY(old_my);
1267     int dx = dst_x - src_x;
1268     int dy = dst_y - src_y;
1269     Key new_motion_key_x = (dx < 0 ? setup.input[0].key.left :
1270                             dx > 0 ? setup.input[0].key.right :
1271                             KSYM_UNDEFINED);
1272     Key new_motion_key_y = (dy < 0 ? setup.input[0].key.up :
1273                             dy > 0 ? setup.input[0].key.down :
1274                             KSYM_UNDEFINED);
1275
1276     if (dx != 0 && dy != 0 && ABS(dx) != ABS(dy) &&
1277         (last_player_x != local_player->jx ||
1278          last_player_y != local_player->jy))
1279     {
1280       // in case of asymmetric diagonal movement, use "preferred" direction
1281
1282       int last_move_dir = (ABS(dx) > ABS(dy) ? MV_VERTICAL : MV_HORIZONTAL);
1283
1284       if (level.game_engine_type == GAME_ENGINE_TYPE_EM)
1285         level.native_em_level->ply[0]->last_move_dir = last_move_dir;
1286       else
1287         local_player->last_move_dir = last_move_dir;
1288
1289       // (required to prevent accidentally forcing direction for next movement)
1290       last_player_x = local_player->jx;
1291       last_player_y = local_player->jy;
1292     }
1293
1294     if (button == MB_PRESSED && !motion_status && dx == 0 && dy == 0)
1295     {
1296       started_on_player = TRUE;
1297       player_drop_count = getPlayerInventorySize(0);
1298       player_is_dropping = (player_drop_count > 0);
1299
1300       if (player_is_dropping)
1301       {
1302         Error(ERR_DEBUG, "---------- DROP STARTED ----------");
1303
1304         HandleKey(setup.input[0].key.drop, KEY_PRESSED);
1305       }
1306       else
1307       {
1308         Error(ERR_DEBUG, "---------- SNAP STARTED ----------");
1309
1310         HandleKey(setup.input[0].key.snap, KEY_PRESSED);
1311       }
1312     }
1313     else if (dx != 0 || dy != 0)
1314     {
1315       if (player_is_dropping &&
1316           player_drop_count == getPlayerInventorySize(0))
1317       {
1318         Error(ERR_DEBUG, "---------- DROP -> SNAP ----------");
1319
1320         HandleKey(setup.input[0].key.drop, KEY_RELEASED);
1321         HandleKey(setup.input[0].key.snap, KEY_PRESSED);
1322
1323         player_is_dropping = FALSE;
1324       }
1325     }
1326
1327     if (new_motion_key_x != motion_key_x)
1328     {
1329       Error(ERR_DEBUG, "---------- %s %s ----------",
1330             started_on_player && !player_is_dropping ? "SNAPPING" : "MOVING",
1331             dx < 0 ? "LEFT" : dx > 0 ? "RIGHT" : "PAUSED");
1332
1333       if (motion_key_x != KSYM_UNDEFINED)
1334         HandleKey(motion_key_x, KEY_RELEASED);
1335       if (new_motion_key_x != KSYM_UNDEFINED)
1336         HandleKey(new_motion_key_x, KEY_PRESSED);
1337     }
1338
1339     if (new_motion_key_y != motion_key_y)
1340     {
1341       Error(ERR_DEBUG, "---------- %s %s ----------",
1342             started_on_player && !player_is_dropping ? "SNAPPING" : "MOVING",
1343             dy < 0 ? "UP" : dy > 0 ? "DOWN" : "PAUSED");
1344
1345       if (motion_key_y != KSYM_UNDEFINED)
1346         HandleKey(motion_key_y, KEY_RELEASED);
1347       if (new_motion_key_y != KSYM_UNDEFINED)
1348         HandleKey(new_motion_key_y, KEY_PRESSED);
1349     }
1350
1351     motion_key_x = new_motion_key_x;
1352     motion_key_y = new_motion_key_y;
1353   }
1354 }
1355
1356 static void HandleButtonOrFinger(int mx, int my, int button)
1357 {
1358   if (game_status != GAME_MODE_PLAYING)
1359     return;
1360
1361   if (level.game_engine_type == GAME_ENGINE_TYPE_MM)
1362   {
1363     if (strEqual(setup.touch.control_type, TOUCH_CONTROL_WIPE_GESTURES))
1364       HandleButtonOrFinger_WipeGestures_MM(mx, my, button);
1365     else if (strEqual(setup.touch.control_type, TOUCH_CONTROL_FOLLOW_FINGER))
1366       HandleButtonOrFinger_FollowFinger_MM(mx, my, button);
1367     else if (strEqual(setup.touch.control_type, TOUCH_CONTROL_VIRTUAL_BUTTONS))
1368       SetPlayerMouseAction(mx, my, button);     // special case
1369   }
1370   else
1371   {
1372     if (strEqual(setup.touch.control_type, TOUCH_CONTROL_FOLLOW_FINGER))
1373       HandleButtonOrFinger_FollowFinger(mx, my, button);
1374   }
1375 }
1376
1377 static boolean checkTextInputKeyModState(void)
1378 {
1379   // when playing, only handle raw key events and ignore text input
1380   if (game_status == GAME_MODE_PLAYING)
1381     return FALSE;
1382
1383   return ((GetKeyModState() & KMOD_TextInput) != KMOD_None);
1384 }
1385
1386 void HandleTextEvent(TextEvent *event)
1387 {
1388   char *text = event->text;
1389   Key key = getKeyFromKeyName(text);
1390
1391 #if DEBUG_EVENTS_TEXT
1392   Error(ERR_DEBUG, "TEXT EVENT: text == '%s' [%d byte(s), '%c'/%d], resulting key == %d (%s) [%04x]",
1393         text,
1394         strlen(text),
1395         text[0], (int)(text[0]),
1396         key,
1397         getKeyNameFromKey(key),
1398         GetKeyModState());
1399 #endif
1400
1401 #if !defined(HAS_SCREEN_KEYBOARD)
1402   // non-mobile devices: only handle key input with modifier keys pressed here
1403   // (every other key input is handled directly as physical key input event)
1404   if (!checkTextInputKeyModState())
1405     return;
1406 #endif
1407
1408   // process text input as "classic" (with uppercase etc.) key input event
1409   HandleKey(key, KEY_PRESSED);
1410   HandleKey(key, KEY_RELEASED);
1411 }
1412
1413 void HandlePauseResumeEvent(PauseResumeEvent *event)
1414 {
1415   if (event->type == SDL_APP_WILLENTERBACKGROUND)
1416   {
1417     Mix_PauseMusic();
1418   }
1419   else if (event->type == SDL_APP_DIDENTERFOREGROUND)
1420   {
1421     Mix_ResumeMusic();
1422   }
1423 }
1424
1425 void HandleKeyEvent(KeyEvent *event)
1426 {
1427   int key_status = (event->type == EVENT_KEYPRESS ? KEY_PRESSED : KEY_RELEASED);
1428   boolean with_modifiers = (game_status == GAME_MODE_PLAYING ? FALSE : TRUE);
1429   Key key = GetEventKey(event, with_modifiers);
1430   Key keymod = (with_modifiers ? GetEventKey(event, FALSE) : key);
1431
1432 #if DEBUG_EVENTS_KEY
1433   Error(ERR_DEBUG, "KEY EVENT: key was %s, keysym.scancode == %d, keysym.sym == %d, keymod = %d, GetKeyModState() = 0x%04x, resulting key == %d (%s)",
1434         event->type == EVENT_KEYPRESS ? "pressed" : "released",
1435         event->keysym.scancode,
1436         event->keysym.sym,
1437         keymod,
1438         GetKeyModState(),
1439         key,
1440         getKeyNameFromKey(key));
1441 #endif
1442
1443 #if defined(PLATFORM_ANDROID)
1444   if (key == KSYM_Back)
1445   {
1446     // always map the "back" button to the "escape" key on Android devices
1447     key = KSYM_Escape;
1448   }
1449   else if (key == KSYM_Menu)
1450   {
1451     // the "menu" button can be used to toggle displaying virtual buttons
1452     if (key_status == KEY_PRESSED)
1453       SetOverlayEnabled(!GetOverlayEnabled());
1454   }
1455   else
1456   {
1457     // for any other "real" key event, disable virtual buttons
1458     SetOverlayEnabled(FALSE);
1459   }
1460 #endif
1461
1462   HandleKeyModState(keymod, key_status);
1463
1464   // only handle raw key input without text modifier keys pressed
1465   if (!checkTextInputKeyModState())
1466     HandleKey(key, key_status);
1467 }
1468
1469 static int HandleDropFileEvent(char *filename)
1470 {
1471   Error(ERR_DEBUG, "DROP FILE EVENT: '%s'", filename);
1472
1473   // check and extract dropped zip files into correct user data directory
1474   if (!strSuffixLower(filename, ".zip"))
1475   {
1476     Error(ERR_WARN, "file '%s' not supported", filename);
1477
1478     return TREE_TYPE_UNDEFINED;
1479   }
1480
1481   TreeInfo *tree_node = NULL;
1482   int tree_type = GetZipFileTreeType(filename);
1483   char *directory = TREE_USERDIR(tree_type);
1484
1485   if (directory == NULL)
1486   {
1487     Error(ERR_WARN, "zip file '%s' has invalid content!", filename);
1488
1489     return TREE_TYPE_UNDEFINED;
1490   }
1491
1492   if (tree_type == TREE_TYPE_LEVEL_DIR &&
1493       game_status == GAME_MODE_LEVELS &&
1494       leveldir_current->node_parent != NULL)
1495   {
1496     // extract new level set next to currently selected level set
1497     tree_node = leveldir_current;
1498
1499     // get parent directory of currently selected level set directory
1500     directory = getLevelDirFromTreeInfo(leveldir_current->node_parent);
1501
1502     // use private level directory instead of top-level package level directory
1503     if (strPrefix(directory, options.level_directory) &&
1504         strEqual(leveldir_current->node_parent->fullpath, "."))
1505       directory = getUserLevelDir(NULL);
1506   }
1507
1508   // extract level or artwork set from zip file to target directory
1509   char *top_dir = ExtractZipFileIntoDirectory(filename, directory, tree_type);
1510
1511   if (top_dir == NULL)
1512   {
1513     // error message already issued by "ExtractZipFileIntoDirectory()"
1514
1515     return TREE_TYPE_UNDEFINED;
1516   }
1517
1518   // add extracted level or artwork set to tree info structure
1519   AddTreeSetToTreeInfo(tree_node, directory, top_dir, tree_type);
1520
1521   // update menu screen (and possibly change current level set)
1522   DrawScreenAfterAddingSet(top_dir, tree_type);
1523
1524   return tree_type;
1525 }
1526
1527 static void HandleDropTextEvent(char *text)
1528 {
1529   Error(ERR_DEBUG, "DROP TEXT EVENT: '%s'", text);
1530 }
1531
1532 static void HandleDropCompleteEvent(int num_level_sets_succeeded,
1533                                     int num_artwork_sets_succeeded,
1534                                     int num_files_failed)
1535 {
1536   // only show request dialog if no other request dialog already active
1537   if (game.request_active)
1538     return;
1539
1540   // this case can happen with drag-and-drop with older SDL versions
1541   if (num_level_sets_succeeded == 0 &&
1542       num_artwork_sets_succeeded == 0 &&
1543       num_files_failed == 0)
1544     return;
1545
1546   char message[100];
1547
1548   if (num_level_sets_succeeded > 0 || num_artwork_sets_succeeded > 0)
1549   {
1550     char message_part1[50];
1551
1552     sprintf(message_part1, "New %s set%s added",
1553             (num_artwork_sets_succeeded == 0 ? "level" :
1554              num_level_sets_succeeded == 0 ? "artwork" : "level and artwork"),
1555             (num_level_sets_succeeded +
1556              num_artwork_sets_succeeded > 1 ? "s" : ""));
1557
1558     if (num_files_failed > 0)
1559       sprintf(message, "%s, but %d dropped file%s failed!",
1560               message_part1, num_files_failed, num_files_failed > 1 ? "s" : "");
1561     else
1562       sprintf(message, "%s!", message_part1);
1563   }
1564   else if (num_files_failed > 0)
1565   {
1566     sprintf(message, "Failed to process dropped file%s!",
1567             num_files_failed > 1 ? "s" : "");
1568   }
1569
1570   Request(message, REQ_CONFIRM);
1571 }
1572
1573 void HandleDropEvent(Event *event)
1574 {
1575   static boolean confirm_on_drop_complete = FALSE;
1576   static int num_level_sets_succeeded = 0;
1577   static int num_artwork_sets_succeeded = 0;
1578   static int num_files_failed = 0;
1579
1580   switch (event->type)
1581   {
1582     case SDL_DROPBEGIN:
1583     {
1584       confirm_on_drop_complete = TRUE;
1585       num_level_sets_succeeded = 0;
1586       num_artwork_sets_succeeded = 0;
1587       num_files_failed = 0;
1588
1589       break;
1590     }
1591
1592     case SDL_DROPFILE:
1593     {
1594       int tree_type = HandleDropFileEvent(event->drop.file);
1595
1596       if (tree_type == TREE_TYPE_LEVEL_DIR)
1597         num_level_sets_succeeded++;
1598       else if (tree_type == TREE_TYPE_GRAPHICS_DIR ||
1599                tree_type == TREE_TYPE_SOUNDS_DIR ||
1600                tree_type == TREE_TYPE_MUSIC_DIR)
1601         num_artwork_sets_succeeded++;
1602       else
1603         num_files_failed++;
1604
1605       // SDL_DROPBEGIN / SDL_DROPCOMPLETE did not exist in older SDL versions
1606       if (!confirm_on_drop_complete)
1607       {
1608         // process all remaining events, including further SDL_DROPFILE events
1609         ClearEventQueue();
1610
1611         HandleDropCompleteEvent(num_level_sets_succeeded,
1612                                 num_artwork_sets_succeeded,
1613                                 num_files_failed);
1614
1615         num_level_sets_succeeded = 0;
1616         num_artwork_sets_succeeded = 0;
1617         num_files_failed = 0;
1618       }
1619
1620       break;
1621     }
1622
1623     case SDL_DROPTEXT:
1624     {
1625       HandleDropTextEvent(event->drop.file);
1626
1627       break;
1628     }
1629
1630     case SDL_DROPCOMPLETE:
1631     {
1632       HandleDropCompleteEvent(num_level_sets_succeeded,
1633                               num_artwork_sets_succeeded,
1634                               num_files_failed);
1635
1636       break;
1637     }
1638   }
1639
1640   if (event->drop.file != NULL)
1641     SDL_free(event->drop.file);
1642 }
1643
1644 void HandleUserEvent(UserEvent *event)
1645 {
1646   switch (event->code)
1647   {
1648     case USEREVENT_ANIM_DELAY_ACTION:
1649     case USEREVENT_ANIM_EVENT_ACTION:
1650       // execute action functions until matching action was found
1651       if (DoKeysymAction(event->value1) ||
1652           DoGadgetAction(event->value1) ||
1653           DoScreenAction(event->value1))
1654         return;
1655       break;
1656
1657     default:
1658       break;
1659   }
1660 }
1661
1662 void HandleButton(int mx, int my, int button, int button_nr)
1663 {
1664   static int old_mx = 0, old_my = 0;
1665   boolean button_hold = FALSE;
1666   boolean handle_gadgets = TRUE;
1667
1668   if (button_nr < 0)
1669   {
1670     mx = old_mx;
1671     my = old_my;
1672     button_nr = -button_nr;
1673     button_hold = TRUE;
1674   }
1675   else
1676   {
1677     old_mx = mx;
1678     old_my = my;
1679   }
1680
1681 #if defined(PLATFORM_ANDROID)
1682   // when playing, only handle gadgets when using "follow finger" controls
1683   // or when using touch controls in combination with the MM game engine
1684   // or when using gadgets that do not overlap with virtual buttons
1685   handle_gadgets =
1686     (game_status != GAME_MODE_PLAYING ||
1687      level.game_engine_type == GAME_ENGINE_TYPE_MM ||
1688      strEqual(setup.touch.control_type, TOUCH_CONTROL_FOLLOW_FINGER) ||
1689      (strEqual(setup.touch.control_type, TOUCH_CONTROL_VIRTUAL_BUTTONS) &&
1690       !virtual_button_pressed));
1691 #endif
1692
1693   if (HandleGlobalAnimClicks(mx, my, button, FALSE))
1694   {
1695     // do not handle this button event anymore
1696     return;             // force mouse event not to be handled at all
1697   }
1698
1699   if (handle_gadgets && HandleGadgets(mx, my, button))
1700   {
1701     // do not handle this button event anymore
1702     mx = my = -32;      // force mouse event to be outside screen tiles
1703   }
1704
1705   if (button_hold && game_status == GAME_MODE_PLAYING && tape.pausing)
1706     return;
1707
1708   // do not use scroll wheel button events for anything other than gadgets
1709   if (IS_WHEEL_BUTTON(button_nr))
1710     return;
1711
1712   switch (game_status)
1713   {
1714     case GAME_MODE_TITLE:
1715       HandleTitleScreen(mx, my, 0, 0, button);
1716       break;
1717
1718     case GAME_MODE_MAIN:
1719       HandleMainMenu(mx, my, 0, 0, button);
1720       break;
1721
1722     case GAME_MODE_PSEUDO_TYPENAME:
1723       HandleTypeName(0, KSYM_Return);
1724       break;
1725
1726     case GAME_MODE_LEVELS:
1727       HandleChooseLevelSet(mx, my, 0, 0, button);
1728       break;
1729
1730     case GAME_MODE_LEVELNR:
1731       HandleChooseLevelNr(mx, my, 0, 0, button);
1732       break;
1733
1734     case GAME_MODE_SCORES:
1735       HandleHallOfFame(0, 0, 0, 0, button);
1736       break;
1737
1738     case GAME_MODE_EDITOR:
1739       HandleLevelEditorIdle();
1740       break;
1741
1742     case GAME_MODE_INFO:
1743       HandleInfoScreen(mx, my, 0, 0, button);
1744       break;
1745
1746     case GAME_MODE_SETUP:
1747       HandleSetupScreen(mx, my, 0, 0, button);
1748       break;
1749
1750     case GAME_MODE_PLAYING:
1751       if (!strEqual(setup.touch.control_type, TOUCH_CONTROL_OFF))
1752         HandleButtonOrFinger(mx, my, button);
1753       else
1754         SetPlayerMouseAction(mx, my, button);
1755
1756 #ifdef DEBUG
1757       if (button == MB_PRESSED && !motion_status && !button_hold &&
1758           IN_GFX_FIELD_PLAY(mx, my) && GetKeyModState() & KMOD_Control)
1759         DumpTileFromScreen(mx, my);
1760 #endif
1761
1762       break;
1763
1764     default:
1765       break;
1766   }
1767 }
1768
1769 static boolean is_string_suffix(char *string, char *suffix)
1770 {
1771   int string_len = strlen(string);
1772   int suffix_len = strlen(suffix);
1773
1774   if (suffix_len > string_len)
1775     return FALSE;
1776
1777   return (strEqual(&string[string_len - suffix_len], suffix));
1778 }
1779
1780 #define MAX_CHEAT_INPUT_LEN     32
1781
1782 static void HandleKeysSpecial(Key key)
1783 {
1784   static char cheat_input[2 * MAX_CHEAT_INPUT_LEN + 1] = "";
1785   char letter = getCharFromKey(key);
1786   int cheat_input_len = strlen(cheat_input);
1787   int i;
1788
1789   if (letter == 0)
1790     return;
1791
1792   if (cheat_input_len >= 2 * MAX_CHEAT_INPUT_LEN)
1793   {
1794     for (i = 0; i < MAX_CHEAT_INPUT_LEN + 1; i++)
1795       cheat_input[i] = cheat_input[MAX_CHEAT_INPUT_LEN + i];
1796
1797     cheat_input_len = MAX_CHEAT_INPUT_LEN;
1798   }
1799
1800   cheat_input[cheat_input_len++] = letter;
1801   cheat_input[cheat_input_len] = '\0';
1802
1803 #if DEBUG_EVENTS_KEY
1804   Error(ERR_DEBUG, "SPECIAL KEY '%s' [%d]\n", cheat_input, cheat_input_len);
1805 #endif
1806
1807   if (game_status == GAME_MODE_MAIN)
1808   {
1809     if (is_string_suffix(cheat_input, ":insert-solution-tape") ||
1810         is_string_suffix(cheat_input, ":ist"))
1811     {
1812       InsertSolutionTape();
1813     }
1814     else if (is_string_suffix(cheat_input, ":play-solution-tape") ||
1815              is_string_suffix(cheat_input, ":pst"))
1816     {
1817       PlaySolutionTape();
1818     }
1819     else if (is_string_suffix(cheat_input, ":reload-graphics") ||
1820              is_string_suffix(cheat_input, ":rg"))
1821     {
1822       ReloadCustomArtwork(1 << ARTWORK_TYPE_GRAPHICS);
1823       DrawMainMenu();
1824     }
1825     else if (is_string_suffix(cheat_input, ":reload-sounds") ||
1826              is_string_suffix(cheat_input, ":rs"))
1827     {
1828       ReloadCustomArtwork(1 << ARTWORK_TYPE_SOUNDS);
1829       DrawMainMenu();
1830     }
1831     else if (is_string_suffix(cheat_input, ":reload-music") ||
1832              is_string_suffix(cheat_input, ":rm"))
1833     {
1834       ReloadCustomArtwork(1 << ARTWORK_TYPE_MUSIC);
1835       DrawMainMenu();
1836     }
1837     else if (is_string_suffix(cheat_input, ":reload-artwork") ||
1838              is_string_suffix(cheat_input, ":ra"))
1839     {
1840       ReloadCustomArtwork(1 << ARTWORK_TYPE_GRAPHICS |
1841                           1 << ARTWORK_TYPE_SOUNDS |
1842                           1 << ARTWORK_TYPE_MUSIC);
1843       DrawMainMenu();
1844     }
1845     else if (is_string_suffix(cheat_input, ":dump-level") ||
1846              is_string_suffix(cheat_input, ":dl"))
1847     {
1848       DumpLevel(&level);
1849     }
1850     else if (is_string_suffix(cheat_input, ":dump-tape") ||
1851              is_string_suffix(cheat_input, ":dt"))
1852     {
1853       DumpTape(&tape);
1854     }
1855     else if (is_string_suffix(cheat_input, ":fix-tape") ||
1856              is_string_suffix(cheat_input, ":ft"))
1857     {
1858       /* fix single-player tapes that contain player input for more than one
1859          player (due to a bug in 3.3.1.2 and earlier versions), which results
1860          in playing levels with more than one player in multi-player mode,
1861          even though the tape was originally recorded in single-player mode */
1862
1863       // remove player input actions for all players but the first one
1864       for (i = 1; i < MAX_PLAYERS; i++)
1865         tape.player_participates[i] = FALSE;
1866
1867       tape.changed = TRUE;
1868     }
1869     else if (is_string_suffix(cheat_input, ":save-native-level") ||
1870              is_string_suffix(cheat_input, ":snl"))
1871     {
1872       SaveNativeLevel(&level);
1873     }
1874     else if (is_string_suffix(cheat_input, ":frames-per-second") ||
1875              is_string_suffix(cheat_input, ":fps"))
1876     {
1877       global.show_frames_per_second = !global.show_frames_per_second;
1878     }
1879   }
1880   else if (game_status == GAME_MODE_PLAYING)
1881   {
1882 #ifdef DEBUG
1883     if (is_string_suffix(cheat_input, ".q"))
1884       DEBUG_SetMaximumDynamite();
1885 #endif
1886   }
1887   else if (game_status == GAME_MODE_EDITOR)
1888   {
1889     if (is_string_suffix(cheat_input, ":dump-brush") ||
1890         is_string_suffix(cheat_input, ":DB"))
1891     {
1892       DumpBrush();
1893     }
1894     else if (is_string_suffix(cheat_input, ":DDB"))
1895     {
1896       DumpBrush_Small();
1897     }
1898
1899     if (GetKeyModState() & (KMOD_Control | KMOD_Meta))
1900     {
1901       if (letter == 'x')        // copy brush to clipboard (small size)
1902       {
1903         CopyBrushToClipboard_Small();
1904       }
1905       else if (letter == 'c')   // copy brush to clipboard (normal size)
1906       {
1907         CopyBrushToClipboard();
1908       }
1909       else if (letter == 'v')   // paste brush from Clipboard
1910       {
1911         CopyClipboardToBrush();
1912       }
1913       else if (letter == 'z')   // undo or redo last operation
1914       {
1915         if (GetKeyModState() & KMOD_Shift)
1916           RedoLevelEditorOperation();
1917         else
1918           UndoLevelEditorOperation();
1919       }
1920     }
1921   }
1922
1923   // special key shortcuts for all game modes
1924   if (is_string_suffix(cheat_input, ":dump-event-actions") ||
1925       is_string_suffix(cheat_input, ":dea") ||
1926       is_string_suffix(cheat_input, ":DEA"))
1927   {
1928     DumpGadgetIdentifiers();
1929     DumpScreenIdentifiers();
1930   }
1931 }
1932
1933 boolean HandleKeysDebug(Key key, int key_status)
1934 {
1935 #ifdef DEBUG
1936   int i;
1937
1938   if (key_status != KEY_PRESSED)
1939     return FALSE;
1940
1941   if (game_status == GAME_MODE_PLAYING || !setup.debug.frame_delay_game_only)
1942   {
1943     boolean mod_key_pressed = ((GetKeyModState() & KMOD_Valid) != KMOD_None);
1944
1945     for (i = 0; i < NUM_DEBUG_FRAME_DELAY_KEYS; i++)
1946     {
1947       if (key == setup.debug.frame_delay_key[i] &&
1948           (mod_key_pressed == setup.debug.frame_delay_use_mod_key))
1949       {
1950         GameFrameDelay = (GameFrameDelay != setup.debug.frame_delay[i] ?
1951                           setup.debug.frame_delay[i] : setup.game_frame_delay);
1952
1953         if (!setup.debug.frame_delay_game_only)
1954           MenuFrameDelay = GameFrameDelay;
1955
1956         SetVideoFrameDelay(GameFrameDelay);
1957
1958         if (GameFrameDelay > ONE_SECOND_DELAY)
1959           Error(ERR_INFO, "frame delay == %d ms", GameFrameDelay);
1960         else if (GameFrameDelay != 0)
1961           Error(ERR_INFO, "frame delay == %d ms (max. %d fps / %d %%)",
1962                 GameFrameDelay, ONE_SECOND_DELAY / GameFrameDelay,
1963                 GAME_FRAME_DELAY * 100 / GameFrameDelay);
1964         else
1965           Error(ERR_INFO, "frame delay == 0 ms (maximum speed)");
1966
1967         return TRUE;
1968       }
1969     }
1970   }
1971
1972   if (game_status == GAME_MODE_PLAYING)
1973   {
1974     if (key == KSYM_d)
1975     {
1976       options.debug = !options.debug;
1977
1978       Error(ERR_INFO, "debug mode %s",
1979             (options.debug ? "enabled" : "disabled"));
1980
1981       return TRUE;
1982     }
1983     else if (key == KSYM_v)
1984     {
1985       Error(ERR_INFO, "currently using game engine version %d",
1986             game.engine_version);
1987
1988       return TRUE;
1989     }
1990   }
1991 #endif
1992
1993   return FALSE;
1994 }
1995
1996 void HandleKey(Key key, int key_status)
1997 {
1998   boolean anyTextGadgetActiveOrJustFinished = anyTextGadgetActive();
1999   static boolean ignore_repeated_key = FALSE;
2000   static struct SetupKeyboardInfo ski;
2001   static struct SetupShortcutInfo ssi;
2002   static struct
2003   {
2004     Key *key_custom;
2005     Key *key_snap;
2006     Key key_default;
2007     byte action;
2008   } key_info[] =
2009   {
2010     { &ski.left,  &ssi.snap_left,  DEFAULT_KEY_LEFT,  JOY_LEFT        },
2011     { &ski.right, &ssi.snap_right, DEFAULT_KEY_RIGHT, JOY_RIGHT       },
2012     { &ski.up,    &ssi.snap_up,    DEFAULT_KEY_UP,    JOY_UP          },
2013     { &ski.down,  &ssi.snap_down,  DEFAULT_KEY_DOWN,  JOY_DOWN        },
2014     { &ski.snap,  NULL,            DEFAULT_KEY_SNAP,  JOY_BUTTON_SNAP },
2015     { &ski.drop,  NULL,            DEFAULT_KEY_DROP,  JOY_BUTTON_DROP }
2016   };
2017   int joy = 0;
2018   int i;
2019
2020   if (HandleKeysDebug(key, key_status))
2021     return;             // do not handle already processed keys again
2022
2023   // map special keys (media keys / remote control buttons) to default keys
2024   if (key == KSYM_PlayPause)
2025     key = KSYM_space;
2026   else if (key == KSYM_Select)
2027     key = KSYM_Return;
2028
2029   HandleSpecialGameControllerKeys(key, key_status);
2030
2031   if (game_status == GAME_MODE_PLAYING)
2032   {
2033     // only needed for single-step tape recording mode
2034     static boolean has_snapped[MAX_PLAYERS] = { FALSE, FALSE, FALSE, FALSE };
2035     int pnr;
2036
2037     for (pnr = 0; pnr < MAX_PLAYERS; pnr++)
2038     {
2039       byte key_action = 0;
2040       byte key_snap_action = 0;
2041
2042       if (setup.input[pnr].use_joystick)
2043         continue;
2044
2045       ski = setup.input[pnr].key;
2046
2047       for (i = 0; i < NUM_PLAYER_ACTIONS; i++)
2048         if (key == *key_info[i].key_custom)
2049           key_action |= key_info[i].action;
2050
2051       // use combined snap+direction keys for the first player only
2052       if (pnr == 0)
2053       {
2054         ssi = setup.shortcut;
2055
2056         // also remember normal snap key when handling snap+direction keys
2057         key_snap_action |= key_action & JOY_BUTTON_SNAP;
2058
2059         for (i = 0; i < NUM_DIRECTIONS; i++)
2060         {
2061           if (key == *key_info[i].key_snap)
2062           {
2063             key_action      |= key_info[i].action | JOY_BUTTON_SNAP;
2064             key_snap_action |= key_info[i].action;
2065           }
2066         }
2067       }
2068
2069       if (key_status == KEY_PRESSED)
2070       {
2071         stored_player[pnr].action      |= key_action;
2072         stored_player[pnr].snap_action |= key_snap_action;
2073       }
2074       else
2075       {
2076         stored_player[pnr].action      &= ~key_action;
2077         stored_player[pnr].snap_action &= ~key_snap_action;
2078       }
2079
2080       // restore snap action if one of several pressed snap keys was released
2081       if (stored_player[pnr].snap_action)
2082         stored_player[pnr].action |= JOY_BUTTON_SNAP;
2083
2084       if (tape.single_step && tape.recording && tape.pausing && !tape.use_mouse)
2085       {
2086         if (key_status == KEY_PRESSED && key_action & KEY_MOTION)
2087         {
2088           TapeTogglePause(TAPE_TOGGLE_AUTOMATIC);
2089
2090           // if snap key already pressed, keep pause mode when releasing
2091           if (stored_player[pnr].action & KEY_BUTTON_SNAP)
2092             has_snapped[pnr] = TRUE;
2093         }
2094         else if (key_status == KEY_PRESSED && key_action & KEY_BUTTON_DROP)
2095         {
2096           TapeTogglePause(TAPE_TOGGLE_AUTOMATIC);
2097
2098           if (level.game_engine_type == GAME_ENGINE_TYPE_SP &&
2099               getRedDiskReleaseFlag_SP() == 0)
2100           {
2101             // add a single inactive frame before dropping starts
2102             stored_player[pnr].action &= ~KEY_BUTTON_DROP;
2103             stored_player[pnr].force_dropping = TRUE;
2104           }
2105         }
2106         else if (key_status == KEY_RELEASED && key_action & KEY_BUTTON_SNAP)
2107         {
2108           // if snap key was pressed without direction, leave pause mode
2109           if (!has_snapped[pnr])
2110             TapeTogglePause(TAPE_TOGGLE_AUTOMATIC);
2111
2112           has_snapped[pnr] = FALSE;
2113         }
2114       }
2115       else if (tape.recording && tape.pausing && !tape.use_mouse)
2116       {
2117         // prevent key release events from un-pausing a paused game
2118         if (key_status == KEY_PRESSED && key_action & KEY_ACTION)
2119           TapeTogglePause(TAPE_TOGGLE_MANUAL);
2120       }
2121
2122       // for MM style levels, handle in-game keyboard input in HandleJoystick()
2123       if (level.game_engine_type == GAME_ENGINE_TYPE_MM)
2124         joy |= key_action;
2125     }
2126   }
2127   else
2128   {
2129     for (i = 0; i < NUM_PLAYER_ACTIONS; i++)
2130       if (key == key_info[i].key_default)
2131         joy |= key_info[i].action;
2132   }
2133
2134   if (joy)
2135   {
2136     if (key_status == KEY_PRESSED)
2137       key_joystick_mapping |= joy;
2138     else
2139       key_joystick_mapping &= ~joy;
2140
2141     HandleJoystick();
2142   }
2143
2144   if (game_status != GAME_MODE_PLAYING)
2145     key_joystick_mapping = 0;
2146
2147   if (key_status == KEY_RELEASED)
2148   {
2149     // reset flag to ignore repeated "key pressed" events after key release
2150     ignore_repeated_key = FALSE;
2151
2152     return;
2153   }
2154
2155   if ((key == KSYM_F11 ||
2156        ((key == KSYM_Return ||
2157          key == KSYM_KP_Enter) && (GetKeyModState() & KMOD_Alt))) &&
2158       video.fullscreen_available &&
2159       !ignore_repeated_key)
2160   {
2161     setup.fullscreen = !setup.fullscreen;
2162
2163     ToggleFullscreenOrChangeWindowScalingIfNeeded();
2164
2165     if (game_status == GAME_MODE_SETUP)
2166       RedrawSetupScreenAfterFullscreenToggle();
2167
2168     UpdateMousePosition();
2169
2170     // set flag to ignore repeated "key pressed" events
2171     ignore_repeated_key = TRUE;
2172
2173     return;
2174   }
2175
2176   if ((key == KSYM_0     || key == KSYM_KP_0 ||
2177        key == KSYM_minus || key == KSYM_KP_Subtract ||
2178        key == KSYM_plus  || key == KSYM_KP_Add ||
2179        key == KSYM_equal) &&    // ("Shift-=" is "+" on US keyboards)
2180       (GetKeyModState() & (KMOD_Control | KMOD_Meta)) &&
2181       video.window_scaling_available &&
2182       !video.fullscreen_enabled)
2183   {
2184     if (key == KSYM_0 || key == KSYM_KP_0)
2185       setup.window_scaling_percent = STD_WINDOW_SCALING_PERCENT;
2186     else if (key == KSYM_minus || key == KSYM_KP_Subtract)
2187       setup.window_scaling_percent -= STEP_WINDOW_SCALING_PERCENT;
2188     else
2189       setup.window_scaling_percent += STEP_WINDOW_SCALING_PERCENT;
2190
2191     if (setup.window_scaling_percent < MIN_WINDOW_SCALING_PERCENT)
2192       setup.window_scaling_percent = MIN_WINDOW_SCALING_PERCENT;
2193     else if (setup.window_scaling_percent > MAX_WINDOW_SCALING_PERCENT)
2194       setup.window_scaling_percent = MAX_WINDOW_SCALING_PERCENT;
2195
2196     ToggleFullscreenOrChangeWindowScalingIfNeeded();
2197
2198     if (game_status == GAME_MODE_SETUP)
2199       RedrawSetupScreenAfterFullscreenToggle();
2200
2201     UpdateMousePosition();
2202
2203     return;
2204   }
2205
2206   // some key events are handled like clicks for global animations
2207   boolean click = (key == KSYM_space ||
2208                    key == KSYM_Return ||
2209                    key == KSYM_Escape);
2210
2211   if (click && HandleGlobalAnimClicks(-1, -1, MB_LEFTBUTTON, TRUE))
2212   {
2213     // do not handle this key event anymore
2214     if (key != KSYM_Escape)     // always allow ESC key to be handled
2215       return;
2216   }
2217
2218   if (game_status == GAME_MODE_PLAYING && game.all_players_gone &&
2219       (key == KSYM_Return || key == setup.shortcut.toggle_pause))
2220   {
2221     GameEnd();
2222
2223     return;
2224   }
2225
2226   if (game_status == GAME_MODE_MAIN &&
2227       (key == setup.shortcut.toggle_pause || key == KSYM_space))
2228   {
2229     StartGameActions(network.enabled, setup.autorecord, level.random_seed);
2230
2231     return;
2232   }
2233
2234   if (game_status == GAME_MODE_MAIN || game_status == GAME_MODE_PLAYING)
2235   {
2236     if (key == setup.shortcut.save_game)
2237       TapeQuickSave();
2238     else if (key == setup.shortcut.load_game)
2239       TapeQuickLoad();
2240     else if (key == setup.shortcut.toggle_pause)
2241       TapeTogglePause(TAPE_TOGGLE_MANUAL | TAPE_TOGGLE_PLAY_PAUSE);
2242
2243     HandleTapeButtonKeys(key);
2244     HandleSoundButtonKeys(key);
2245   }
2246
2247   if (game_status == GAME_MODE_PLAYING && !network_playing)
2248   {
2249     int centered_player_nr_next = -999;
2250
2251     if (key == setup.shortcut.focus_player_all)
2252       centered_player_nr_next = -1;
2253     else
2254       for (i = 0; i < MAX_PLAYERS; i++)
2255         if (key == setup.shortcut.focus_player[i])
2256           centered_player_nr_next = i;
2257
2258     if (centered_player_nr_next != -999)
2259     {
2260       game.centered_player_nr_next = centered_player_nr_next;
2261       game.set_centered_player = TRUE;
2262
2263       if (tape.recording)
2264       {
2265         tape.centered_player_nr_next = game.centered_player_nr_next;
2266         tape.set_centered_player = TRUE;
2267       }
2268     }
2269   }
2270
2271   HandleKeysSpecial(key);
2272
2273   if (HandleGadgetsKeyInput(key))
2274     return;             // do not handle already processed keys again
2275
2276   switch (game_status)
2277   {
2278     case GAME_MODE_PSEUDO_TYPENAME:
2279       HandleTypeName(0, key);
2280       break;
2281
2282     case GAME_MODE_TITLE:
2283     case GAME_MODE_MAIN:
2284     case GAME_MODE_LEVELS:
2285     case GAME_MODE_LEVELNR:
2286     case GAME_MODE_SETUP:
2287     case GAME_MODE_INFO:
2288     case GAME_MODE_SCORES:
2289
2290       if (anyTextGadgetActiveOrJustFinished && key != KSYM_Escape)
2291         break;
2292
2293       switch (key)
2294       {
2295         case KSYM_space:
2296         case KSYM_Return:
2297           if (game_status == GAME_MODE_TITLE)
2298             HandleTitleScreen(0, 0, 0, 0, MB_MENU_CHOICE);
2299           else if (game_status == GAME_MODE_MAIN)
2300             HandleMainMenu(0, 0, 0, 0, MB_MENU_CHOICE);
2301           else if (game_status == GAME_MODE_LEVELS)
2302             HandleChooseLevelSet(0, 0, 0, 0, MB_MENU_CHOICE);
2303           else if (game_status == GAME_MODE_LEVELNR)
2304             HandleChooseLevelNr(0, 0, 0, 0, MB_MENU_CHOICE);
2305           else if (game_status == GAME_MODE_SETUP)
2306             HandleSetupScreen(0, 0, 0, 0, MB_MENU_CHOICE);
2307           else if (game_status == GAME_MODE_INFO)
2308             HandleInfoScreen(0, 0, 0, 0, MB_MENU_CHOICE);
2309           else if (game_status == GAME_MODE_SCORES)
2310             HandleHallOfFame(0, 0, 0, 0, MB_MENU_CHOICE);
2311           break;
2312
2313         case KSYM_Escape:
2314           if (game_status != GAME_MODE_MAIN)
2315             FadeSkipNextFadeIn();
2316
2317           if (game_status == GAME_MODE_TITLE)
2318             HandleTitleScreen(0, 0, 0, 0, MB_MENU_LEAVE);
2319           else if (game_status == GAME_MODE_LEVELS)
2320             HandleChooseLevelSet(0, 0, 0, 0, MB_MENU_LEAVE);
2321           else if (game_status == GAME_MODE_LEVELNR)
2322             HandleChooseLevelNr(0, 0, 0, 0, MB_MENU_LEAVE);
2323           else if (game_status == GAME_MODE_SETUP)
2324             HandleSetupScreen(0, 0, 0, 0, MB_MENU_LEAVE);
2325           else if (game_status == GAME_MODE_INFO)
2326             HandleInfoScreen(0, 0, 0, 0, MB_MENU_LEAVE);
2327           else if (game_status == GAME_MODE_SCORES)
2328             HandleHallOfFame(0, 0, 0, 0, MB_MENU_LEAVE);
2329           break;
2330
2331         case KSYM_Page_Up:
2332           if (game_status == GAME_MODE_LEVELS)
2333             HandleChooseLevelSet(0, 0, 0, -1 * SCROLL_PAGE, MB_MENU_MARK);
2334           else if (game_status == GAME_MODE_LEVELNR)
2335             HandleChooseLevelNr(0, 0, 0, -1 * SCROLL_PAGE, MB_MENU_MARK);
2336           else if (game_status == GAME_MODE_SETUP)
2337             HandleSetupScreen(0, 0, 0, -1 * SCROLL_PAGE, MB_MENU_MARK);
2338           else if (game_status == GAME_MODE_INFO)
2339             HandleInfoScreen(0, 0, 0, -1 * SCROLL_PAGE, MB_MENU_MARK);
2340           else if (game_status == GAME_MODE_SCORES)
2341             HandleHallOfFame(0, 0, 0, -1 * SCROLL_PAGE, MB_MENU_MARK);
2342           break;
2343
2344         case KSYM_Page_Down:
2345           if (game_status == GAME_MODE_LEVELS)
2346             HandleChooseLevelSet(0, 0, 0, +1 * SCROLL_PAGE, MB_MENU_MARK);
2347           else if (game_status == GAME_MODE_LEVELNR)
2348             HandleChooseLevelNr(0, 0, 0, +1 * SCROLL_PAGE, MB_MENU_MARK);
2349           else if (game_status == GAME_MODE_SETUP)
2350             HandleSetupScreen(0, 0, 0, +1 * SCROLL_PAGE, MB_MENU_MARK);
2351           else if (game_status == GAME_MODE_INFO)
2352             HandleInfoScreen(0, 0, 0, +1 * SCROLL_PAGE, MB_MENU_MARK);
2353           else if (game_status == GAME_MODE_SCORES)
2354             HandleHallOfFame(0, 0, 0, +1 * SCROLL_PAGE, MB_MENU_MARK);
2355           break;
2356
2357         default:
2358           break;
2359       }
2360       break;
2361
2362     case GAME_MODE_EDITOR:
2363       if (!anyTextGadgetActiveOrJustFinished || key == KSYM_Escape)
2364         HandleLevelEditorKeyInput(key);
2365       break;
2366
2367     case GAME_MODE_PLAYING:
2368     {
2369       switch (key)
2370       {
2371         case KSYM_Escape:
2372           RequestQuitGame(setup.ask_on_escape);
2373           break;
2374
2375         default:
2376           break;
2377       }
2378       break;
2379     }
2380
2381     default:
2382       if (key == KSYM_Escape)
2383       {
2384         SetGameStatus(GAME_MODE_MAIN);
2385
2386         DrawMainMenu();
2387
2388         return;
2389       }
2390   }
2391 }
2392
2393 void HandleNoEvent(void)
2394 {
2395   HandleMouseCursor();
2396
2397   switch (game_status)
2398   {
2399     case GAME_MODE_PLAYING:
2400       HandleButtonOrFinger(-1, -1, -1);
2401       break;
2402   }
2403 }
2404
2405 void HandleEventActions(void)
2406 {
2407   // if (button_status && game_status != GAME_MODE_PLAYING)
2408   if (button_status && (game_status != GAME_MODE_PLAYING ||
2409                         tape.pausing ||
2410                         level.game_engine_type == GAME_ENGINE_TYPE_MM))
2411   {
2412     HandleButton(0, 0, button_status, -button_status);
2413   }
2414   else
2415   {
2416     HandleJoystick();
2417   }
2418
2419   if (network.enabled)
2420     HandleNetworking();
2421
2422   switch (game_status)
2423   {
2424     case GAME_MODE_MAIN:
2425       DrawPreviewLevelAnimation();
2426       break;
2427
2428     case GAME_MODE_EDITOR:
2429       HandleLevelEditorIdle();
2430       break;
2431
2432     default:
2433       break;
2434   }
2435 }
2436
2437 static void HandleTileCursor(int dx, int dy, int button)
2438 {
2439   if (!dx || !button)
2440     ClearPlayerMouseAction();
2441
2442   if (!dx && !dy)
2443     return;
2444
2445   if (button)
2446   {
2447     SetPlayerMouseAction(tile_cursor.x, tile_cursor.y,
2448                          (dx < 0 ? MB_LEFTBUTTON :
2449                           dx > 0 ? MB_RIGHTBUTTON : MB_RELEASED));
2450   }
2451   else if (!tile_cursor.moving)
2452   {
2453     int old_xpos = tile_cursor.xpos;
2454     int old_ypos = tile_cursor.ypos;
2455     int new_xpos = old_xpos;
2456     int new_ypos = old_ypos;
2457
2458     if (IN_LEV_FIELD(old_xpos + dx, old_ypos))
2459       new_xpos = old_xpos + dx;
2460
2461     if (IN_LEV_FIELD(old_xpos, old_ypos + dy))
2462       new_ypos = old_ypos + dy;
2463
2464     SetTileCursorTargetXY(new_xpos, new_ypos);
2465   }
2466 }
2467
2468 static int HandleJoystickForAllPlayers(void)
2469 {
2470   int i;
2471   int result = 0;
2472   boolean no_joysticks_configured = TRUE;
2473   boolean use_as_joystick_nr = (game_status != GAME_MODE_PLAYING);
2474   static byte joy_action_last[MAX_PLAYERS];
2475
2476   for (i = 0; i < MAX_PLAYERS; i++)
2477     if (setup.input[i].use_joystick)
2478       no_joysticks_configured = FALSE;
2479
2480   // if no joysticks configured, map connected joysticks to players
2481   if (no_joysticks_configured)
2482     use_as_joystick_nr = TRUE;
2483
2484   for (i = 0; i < MAX_PLAYERS; i++)
2485   {
2486     byte joy_action = 0;
2487
2488     joy_action = JoystickExt(i, use_as_joystick_nr);
2489     result |= joy_action;
2490
2491     if ((setup.input[i].use_joystick || no_joysticks_configured) &&
2492         joy_action != joy_action_last[i])
2493       stored_player[i].action = joy_action;
2494
2495     joy_action_last[i] = joy_action;
2496   }
2497
2498   return result;
2499 }
2500
2501 void HandleJoystick(void)
2502 {
2503   static unsigned int joytest_delay = 0;
2504   static unsigned int joytest_delay_value = GADGET_FRAME_DELAY;
2505   static int joytest_last = 0;
2506   int delay_value_first = GADGET_FRAME_DELAY_FIRST;
2507   int delay_value       = GADGET_FRAME_DELAY;
2508   int joystick  = HandleJoystickForAllPlayers();
2509   int keyboard  = key_joystick_mapping;
2510   int joy       = (joystick | keyboard);
2511   int joytest   = joystick;
2512   int left      = joy & JOY_LEFT;
2513   int right     = joy & JOY_RIGHT;
2514   int up        = joy & JOY_UP;
2515   int down      = joy & JOY_DOWN;
2516   int button    = joy & JOY_BUTTON;
2517   int newbutton = (AnyJoystickButton() == JOY_BUTTON_NEW_PRESSED);
2518   int dx        = (left ? -1    : right ? 1     : 0);
2519   int dy        = (up   ? -1    : down  ? 1     : 0);
2520   boolean use_delay_value_first = (joytest != joytest_last);
2521
2522   if (HandleGlobalAnimClicks(-1, -1, newbutton, FALSE))
2523   {
2524     // do not handle this button event anymore
2525     return;
2526   }
2527
2528   if (newbutton && (game_status == GAME_MODE_PSEUDO_TYPENAME ||
2529                     anyTextGadgetActive()))
2530   {
2531     // leave name input in main menu or text input gadget
2532     HandleKey(KSYM_Escape, KEY_PRESSED);
2533     HandleKey(KSYM_Escape, KEY_RELEASED);
2534
2535     return;
2536   }
2537
2538   if (level.game_engine_type == GAME_ENGINE_TYPE_MM)
2539   {
2540     if (game_status == GAME_MODE_PLAYING)
2541     {
2542       // when playing MM style levels, also use delay for keyboard events
2543       joytest |= keyboard;
2544
2545       // only use first delay value for new events, but not for changed events
2546       use_delay_value_first = (!joytest != !joytest_last);
2547
2548       // only use delay after the initial keyboard event
2549       delay_value = 0;
2550     }
2551
2552     // for any joystick or keyboard event, enable playfield tile cursor
2553     if (dx || dy || button)
2554       SetTileCursorEnabled(TRUE);
2555   }
2556
2557   if (joytest && !button && !DelayReached(&joytest_delay, joytest_delay_value))
2558   {
2559     // delay joystick/keyboard actions if axes/keys continually pressed
2560     newbutton = dx = dy = 0;
2561   }
2562   else
2563   {
2564     // first start with longer delay, then continue with shorter delay
2565     joytest_delay_value =
2566       (use_delay_value_first ? delay_value_first : delay_value);
2567   }
2568
2569   joytest_last = joytest;
2570
2571   switch (game_status)
2572   {
2573     case GAME_MODE_TITLE:
2574     case GAME_MODE_MAIN:
2575     case GAME_MODE_LEVELS:
2576     case GAME_MODE_LEVELNR:
2577     case GAME_MODE_SETUP:
2578     case GAME_MODE_INFO:
2579     case GAME_MODE_SCORES:
2580     {
2581       if (anyTextGadgetActive())
2582         break;
2583
2584       if (game_status == GAME_MODE_TITLE)
2585         HandleTitleScreen(0,0,dx,dy, newbutton ? MB_MENU_CHOICE : MB_MENU_MARK);
2586       else if (game_status == GAME_MODE_MAIN)
2587         HandleMainMenu(0,0,dx,dy, newbutton ? MB_MENU_CHOICE : MB_MENU_MARK);
2588       else if (game_status == GAME_MODE_LEVELS)
2589         HandleChooseLevelSet(0,0,dx,dy,newbutton?MB_MENU_CHOICE : MB_MENU_MARK);
2590       else if (game_status == GAME_MODE_LEVELNR)
2591         HandleChooseLevelNr(0,0,dx,dy,newbutton? MB_MENU_CHOICE : MB_MENU_MARK);
2592       else if (game_status == GAME_MODE_SETUP)
2593         HandleSetupScreen(0,0,dx,dy, newbutton ? MB_MENU_CHOICE : MB_MENU_MARK);
2594       else if (game_status == GAME_MODE_INFO)
2595         HandleInfoScreen(0,0,dx,dy, newbutton ? MB_MENU_CHOICE : MB_MENU_MARK);
2596       else if (game_status == GAME_MODE_SCORES)
2597         HandleHallOfFame(0,0,dx,dy, newbutton ? MB_MENU_CHOICE : MB_MENU_MARK);
2598
2599       break;
2600     }
2601
2602     case GAME_MODE_PLAYING:
2603 #if 0
2604       // !!! causes immediate GameEnd() when solving MM level with keyboard !!!
2605       if (tape.playing || keyboard)
2606         newbutton = ((joy & JOY_BUTTON) != 0);
2607 #endif
2608
2609       if (newbutton && game.all_players_gone)
2610       {
2611         GameEnd();
2612
2613         return;
2614       }
2615
2616       if (tape.single_step && tape.recording && tape.pausing && !tape.use_mouse)
2617       {
2618         if (joystick & JOY_ACTION)
2619           TapeTogglePause(TAPE_TOGGLE_AUTOMATIC);
2620       }
2621       else if (tape.recording && tape.pausing && !tape.use_mouse)
2622       {
2623         if (joystick & JOY_ACTION)
2624           TapeTogglePause(TAPE_TOGGLE_MANUAL);
2625       }
2626
2627       if (level.game_engine_type == GAME_ENGINE_TYPE_MM)
2628         HandleTileCursor(dx, dy, button);
2629
2630       break;
2631
2632     default:
2633       break;
2634   }
2635 }
2636
2637 void HandleSpecialGameControllerButtons(Event *event)
2638 {
2639   int key_status;
2640   Key key;
2641
2642   switch (event->type)
2643   {
2644     case SDL_CONTROLLERBUTTONDOWN:
2645       key_status = KEY_PRESSED;
2646       break;
2647
2648     case SDL_CONTROLLERBUTTONUP:
2649       key_status = KEY_RELEASED;
2650       break;
2651
2652     default:
2653       return;
2654   }
2655
2656   switch (event->cbutton.button)
2657   {
2658     case SDL_CONTROLLER_BUTTON_START:
2659       key = KSYM_space;
2660       break;
2661
2662     case SDL_CONTROLLER_BUTTON_BACK:
2663       key = KSYM_Escape;
2664       break;
2665
2666     default:
2667       return;
2668   }
2669
2670   HandleKey(key, key_status);
2671 }
2672
2673 void HandleSpecialGameControllerKeys(Key key, int key_status)
2674 {
2675 #if defined(KSYM_Rewind) && defined(KSYM_FastForward)
2676   int button = SDL_CONTROLLER_BUTTON_INVALID;
2677
2678   // map keys to joystick buttons (special hack for Amazon Fire TV remote)
2679   if (key == KSYM_Rewind)
2680     button = SDL_CONTROLLER_BUTTON_A;
2681   else if (key == KSYM_FastForward || key == KSYM_Menu)
2682     button = SDL_CONTROLLER_BUTTON_B;
2683
2684   if (button != SDL_CONTROLLER_BUTTON_INVALID)
2685   {
2686     Event event;
2687
2688     event.type = (key_status == KEY_PRESSED ? SDL_CONTROLLERBUTTONDOWN :
2689                   SDL_CONTROLLERBUTTONUP);
2690
2691     event.cbutton.which = 0;    // first joystick (Amazon Fire TV remote)
2692     event.cbutton.button = button;
2693     event.cbutton.state = (key_status == KEY_PRESSED ? SDL_PRESSED :
2694                            SDL_RELEASED);
2695
2696     HandleJoystickEvent(&event);
2697   }
2698 #endif
2699 }
2700
2701 boolean DoKeysymAction(int keysym)
2702 {
2703   if (keysym < 0)
2704   {
2705     Key key = (Key)(-keysym);
2706
2707     HandleKey(key, KEY_PRESSED);
2708     HandleKey(key, KEY_RELEASED);
2709
2710     return TRUE;
2711   }
2712
2713   return FALSE;
2714 }