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