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