fixed name of element 'beamer' to 'teleporter'
[rocksndiamonds.git] / src / events.c
index 0be6815b798acfd855e5644d1e745e3c721efcc9..7a3f9987d4b09a80ebdc48c970c8eb118f3b4727 100644 (file)
@@ -1,15 +1,13 @@
-/***********************************************************
-* Rocks'n'Diamonds -- McDuffin Strikes Back!               *
-*----------------------------------------------------------*
-* (c) 1995-2006 Artsoft Entertainment                      *
-*               Holger Schemel                             *
-*               Detmolder Strasse 189                      *
-*               33604 Bielefeld                            *
-*               Germany                                    *
-*               e-mail: info@artsoft.org                   *
-*----------------------------------------------------------*
-* events.c                                                 *
-***********************************************************/
+// ============================================================================
+// Rocks'n'Diamonds - McDuffin Strikes Back!
+// ----------------------------------------------------------------------------
+// (c) 1995-2014 by Artsoft Entertainment
+//                         Holger Schemel
+//                 info@artsoft.org
+//                 http://www.artsoft.org/
+// ----------------------------------------------------------------------------
+// events.c
+// ============================================================================
 
 #include "libgame/libgame.h"
 
 #include "editor.h"
 #include "files.h"
 #include "tape.h"
+#include "anim.h"
 #include "network.h"
 
 
-#define        DEBUG_EVENTS            1
+#define        DEBUG_EVENTS            0
+
+#define DEBUG_EVENTS_BUTTON    (DEBUG_EVENTS   * 0)
+#define DEBUG_EVENTS_MOTION    (DEBUG_EVENTS   * 0)
+#define DEBUG_EVENTS_WHEEL     (DEBUG_EVENTS   * 1)
+#define DEBUG_EVENTS_WINDOW    (DEBUG_EVENTS   * 0)
+#define DEBUG_EVENTS_FINGER    (DEBUG_EVENTS   * 0)
+#define DEBUG_EVENTS_TEXT      (DEBUG_EVENTS   * 1)
+#define DEBUG_EVENTS_KEY       (DEBUG_EVENTS   * 1)
 
 
 static boolean cursor_inside_playfield = FALSE;
-static boolean playfield_cursor_set = FALSE;
-static unsigned int playfield_cursor_delay = 0;
+static int cursor_mode_last = CURSOR_DEFAULT;
+static unsigned int special_cursor_delay = 0;
+static unsigned int special_cursor_delay_value = 1000;
+
+
+/* forward declarations for internal use */
+static void HandleNoEvent(void);
+static void HandleEventActions(void);
 
 
 /* event filter especially needed for SDL event filtering due to
@@ -39,7 +52,7 @@ static unsigned int playfield_cursor_delay = 0;
 /* event filter addition for SDL2: as SDL2 does not have a function to enable
    or disable keyboard auto-repeat, filter repeated keyboard events instead */
 
-static int FilterEventsExt(const Event *event)
+static int FilterEvents(const Event *event)
 {
   MotionEvent *motion;
 
@@ -51,6 +64,18 @@ static int FilterEventsExt(const Event *event)
     return 0;
 #endif
 
+  if (event->type == EVENT_BUTTONPRESS ||
+      event->type == EVENT_BUTTONRELEASE)
+  {
+    ((ButtonEvent *)event)->x -= video.screen_xoffset;
+    ((ButtonEvent *)event)->y -= video.screen_yoffset;
+  }
+  else if (event->type == EVENT_MOTIONNOTIFY)
+  {
+    ((MotionEvent *)event)->x -= video.screen_xoffset;
+    ((MotionEvent *)event)->y -= video.screen_yoffset;
+  }
+
   /* non-motion events are directly passed to event handler functions */
   if (event->type != EVENT_MOTIONNOTIFY)
     return 1;
@@ -59,11 +84,18 @@ static int FilterEventsExt(const Event *event)
   cursor_inside_playfield = (motion->x >= SX && motion->x < SX + SXSIZE &&
                             motion->y >= SY && motion->y < SY + SYSIZE);
 
-  if (game_status == GAME_MODE_PLAYING && playfield_cursor_set)
+  /* do no reset mouse cursor before all pending events have been processed */
+  if (gfx.cursor_mode == cursor_mode_last &&
+      ((game_status == GAME_MODE_TITLE &&
+       gfx.cursor_mode == CURSOR_NONE) ||
+       (game_status == GAME_MODE_PLAYING &&
+       gfx.cursor_mode == CURSOR_PLAYFIELD)))
   {
     SetMouseCursor(CURSOR_DEFAULT);
-    playfield_cursor_set = FALSE;
-    DelayReached(&playfield_cursor_delay, 0);
+
+    DelayReached(&special_cursor_delay, 0);
+
+    cursor_mode_last = CURSOR_DEFAULT;
   }
 
   /* skip mouse motion events without pressed button outside level editor */
@@ -74,31 +106,18 @@ static int FilterEventsExt(const Event *event)
   return 1;
 }
 
-#if defined(TARGET_SDL2)
-int FilterEvents(void *userdata, Event *event)
-{
-  return FilterEventsExt(event);
-}
-#else
-int FilterEvents(const Event *event)
-{
-  return FilterEventsExt(event);
-}
-#endif
-
 /* to prevent delay problems, skip mouse motion events if the very next
    event is also a mouse motion event (and therefore effectively only
    handling the last of a row of mouse motion events in the event queue) */
 
-boolean SkipPressedMouseMotionEvent(const Event *event)
+static boolean SkipPressedMouseMotionEvent(const Event *event)
 {
   /* nothing to do if the current event is not a mouse motion event */
   if (event->type != EVENT_MOTIONNOTIFY)
     return FALSE;
 
-  /* only skip motion events with pressed button outside level editor */
-  if (button_status == MB_RELEASED ||
-      game_status == GAME_MODE_EDITOR || game_status == GAME_MODE_PLAYING)
+  /* only skip motion events with pressed button outside the game */
+  if (button_status == MB_RELEASED || game_status == GAME_MODE_PLAYING)
     return FALSE;
 
   if (PendingEvent())
@@ -115,111 +134,97 @@ boolean SkipPressedMouseMotionEvent(const Event *event)
   return FALSE;
 }
 
-/* this is only really needed for non-SDL targets to filter unwanted events;
-   when using SDL with properly installed event filter, this function can be
-   replaced with a simple "NextEvent()" call, but it doesn't hurt either */
-
-static boolean NextValidEvent(Event *event)
+static boolean WaitValidEvent(Event *event)
 {
-  while (PendingEvent())
-  {
-    boolean handle_this_event = FALSE;
+  WaitEvent(event);
+
+  if (!FilterEvents(event))
+    return FALSE;
 
-    NextEvent(event);
+  if (SkipPressedMouseMotionEvent(event))
+    return FALSE;
 
-    if (FilterEventsExt(event))
-      handle_this_event = TRUE;
+  return TRUE;
+}
 
-    if (SkipPressedMouseMotionEvent(event))
-      handle_this_event = FALSE;
+/* this is especially needed for event modifications for the Android target:
+   if mouse coordinates should be modified in the event filter function,
+   using a properly installed SDL event filter does not work, because in
+   the event filter, mouse coordinates in the event structure are still
+   physical pixel positions, not logical (scaled) screen positions, so this
+   has to be handled at a later stage in the event processing functions
+   (when device pixel positions are already converted to screen positions) */
 
-    if (handle_this_event)
+boolean NextValidEvent(Event *event)
+{
+  while (PendingEvent())
+    if (WaitValidEvent(event))
       return TRUE;
-  }
 
   return FALSE;
 }
 
-void EventLoop(void)
+void HandleEvents()
 {
-  while (1)
+  Event event;
+  unsigned int event_frame_delay = 0;
+  unsigned int event_frame_delay_value = GAME_FRAME_DELAY;
+
+  ResetDelayCounter(&event_frame_delay);
+
+  while (NextValidEvent(&event))
   {
-    if (PendingEvent())                /* got event */
+    switch (event.type)
     {
-      Event event;
+      case EVENT_BUTTONPRESS:
+      case EVENT_BUTTONRELEASE:
+       HandleButtonEvent((ButtonEvent *) &event);
+       break;
 
-      while (NextValidEvent(&event))
-      {
-       switch (event.type)
-       {
-         case EVENT_BUTTONPRESS:
-         case EVENT_BUTTONRELEASE:
-           HandleButtonEvent((ButtonEvent *) &event);
-           break;
-  
-         case EVENT_MOTIONNOTIFY:
-           HandleMotionEvent((MotionEvent *) &event);
-           break;
+      case EVENT_MOTIONNOTIFY:
+       HandleMotionEvent((MotionEvent *) &event);
+       break;
 
 #if defined(TARGET_SDL2)
-         case EVENT_FINGERPRESS:
-         case EVENT_FINGERRELEASE:
-         case EVENT_FINGERMOTION:
-           HandleFingerEvent((FingerEvent *) &event);
-           break;
-#endif
+      case EVENT_WHEELMOTION:
+       HandleWheelEvent((WheelEvent *) &event);
+       break;
 
-         case EVENT_KEYPRESS:
-         case EVENT_KEYRELEASE:
-           HandleKeyEvent((KeyEvent *) &event);
-           break;
-  
-         default:
-           HandleOtherEvents(&event);
-           break;
-       }
-      }
-    }
-    else
-    {
-      /* when playing, display a special mouse pointer inside the playfield */
-      if (game_status == GAME_MODE_PLAYING && !tape.pausing)
-      {
-       if (!playfield_cursor_set && cursor_inside_playfield &&
-           DelayReached(&playfield_cursor_delay, 1000))
-       {
-         SetMouseCursor(CURSOR_PLAYFIELD);
-         playfield_cursor_set = TRUE;
-       }
-      }
-      else if (playfield_cursor_set)
-      {
-       SetMouseCursor(CURSOR_DEFAULT);
-       playfield_cursor_set = FALSE;
-      }
+      case SDL_WINDOWEVENT:
+       HandleWindowEvent((WindowEvent *) &event);
+       break;
 
-      HandleNoEvent();
-    }
+      case EVENT_FINGERPRESS:
+      case EVENT_FINGERRELEASE:
+      case EVENT_FINGERMOTION:
+       HandleFingerEvent((FingerEvent *) &event);
+       break;
 
-    /* don't use all CPU time when idle; the main loop while playing
-       has its own synchronization and is CPU friendly, too */
+      case EVENT_TEXTINPUT:
+       HandleTextEvent((TextEvent *) &event);
+       break;
 
-    if (game_status == GAME_MODE_PLAYING)
-    {
-      HandleGameActions();
-    }
-    else
-    {
-      SyncDisplay();
-      if (!PendingEvent())     /* delay only if no pending events */
-       Delay(10);
-    }
+      case SDL_APP_WILLENTERBACKGROUND:
+      case SDL_APP_DIDENTERBACKGROUND:
+      case SDL_APP_WILLENTERFOREGROUND:
+      case SDL_APP_DIDENTERFOREGROUND:
+       HandlePauseResumeEvent((PauseResumeEvent *) &event);
+       break;
+#endif
 
-    /* refresh window contents from drawing buffer, if needed */
-    BackToFront();
+      case EVENT_KEYPRESS:
+      case EVENT_KEYRELEASE:
+       HandleKeyEvent((KeyEvent *) &event);
+       break;
 
-    if (game_status == GAME_MODE_QUIT)
-      return;
+      default:
+       HandleOtherEvents(&event);
+       break;
+    }
+
+    // do not handle events for longer than standard frame delay period
+    if (DelayReached(&event_frame_delay, event_frame_delay_value))
+      break;
   }
 }
 
@@ -249,6 +254,19 @@ void HandleOtherEvents(Event *event)
       break;
 
 #if defined(TARGET_SDL)
+#if defined(TARGET_SDL2)
+    case SDL_CONTROLLERBUTTONDOWN:
+    case SDL_CONTROLLERBUTTONUP:
+      // for any game controller button event, disable overlay buttons
+      SetOverlayEnabled(FALSE);
+
+      HandleSpecialGameControllerButtons(event);
+
+      /* FALL THROUGH */
+    case SDL_CONTROLLERDEVICEADDED:
+    case SDL_CONTROLLERDEVICEREMOVED:
+    case SDL_CONTROLLERAXISMOTION:
+#endif
     case SDL_JOYAXISMOTION:
     case SDL_JOYBUTTONDOWN:
     case SDL_JOYBUTTONUP:
@@ -265,14 +283,76 @@ void HandleOtherEvents(Event *event)
   }
 }
 
-void ClearEventQueue()
+void HandleMouseCursor()
 {
-  while (PendingEvent())
+  if (game_status == GAME_MODE_TITLE)
   {
-    Event event;
+    /* when showing title screens, hide mouse pointer (if not moved) */
+
+    if (gfx.cursor_mode != CURSOR_NONE &&
+       DelayReached(&special_cursor_delay, special_cursor_delay_value))
+    {
+      SetMouseCursor(CURSOR_NONE);
+    }
+  }
+  else if (game_status == GAME_MODE_PLAYING && (!tape.pausing ||
+                                               tape.single_step))
+  {
+    /* when playing, display a special mouse pointer inside the playfield */
+
+    if (gfx.cursor_mode != CURSOR_PLAYFIELD &&
+       cursor_inside_playfield &&
+       DelayReached(&special_cursor_delay, special_cursor_delay_value))
+    {
+      if (level.game_engine_type != GAME_ENGINE_TYPE_MM ||
+         tile_cursor.enabled)
+       SetMouseCursor(CURSOR_PLAYFIELD);
+    }
+  }
+  else if (gfx.cursor_mode != CURSOR_DEFAULT)
+  {
+    SetMouseCursor(CURSOR_DEFAULT);
+  }
+
+  /* this is set after all pending events have been processed */
+  cursor_mode_last = gfx.cursor_mode;
+}
+
+void EventLoop(void)
+{
+  while (1)
+  {
+    if (PendingEvent())
+      HandleEvents();
+    else
+      HandleNoEvent();
+
+    /* execute event related actions after pending events have been processed */
+    HandleEventActions();
+
+    /* don't use all CPU time when idle; the main loop while playing
+       has its own synchronization and is CPU friendly, too */
+
+    if (game_status == GAME_MODE_PLAYING)
+      HandleGameActions();
+
+    /* always copy backbuffer to visible screen for every video frame */
+    BackToFront();
+
+    /* reset video frame delay to default (may change again while playing) */
+    SetVideoFrameDelay(MenuFrameDelay);
+
+    if (game_status == GAME_MODE_QUIT)
+      return;
+  }
+}
 
-    NextEvent(&event);
+void ClearEventQueue()
+{
+  Event event;
 
+  while (NextValidEvent(&event))
+  {
     switch (event.type)
     {
       case EVENT_BUTTONRELEASE:
@@ -280,13 +360,16 @@ void ClearEventQueue()
        break;
 
       case EVENT_KEYRELEASE:
-#if 1
        ClearPlayerAction();
-#else
-       key_joystick_mapping = 0;
-#endif
        break;
 
+#if defined(TARGET_SDL2)
+      case SDL_CONTROLLERBUTTONUP:
+       HandleJoystickEvent(&event);
+       ClearPlayerAction();
+       break;
+#endif
+
       default:
        HandleOtherEvents(&event);
        break;
@@ -294,6 +377,13 @@ void ClearEventQueue()
   }
 }
 
+void ClearPlayerMouseAction()
+{
+  local_player->mouse_action.lx = 0;
+  local_player->mouse_action.ly = 0;
+  local_player->mouse_action.button = 0;
+}
+
 void ClearPlayerAction()
 {
   int i;
@@ -302,6 +392,34 @@ void ClearPlayerAction()
   key_joystick_mapping = 0;
   for (i = 0; i < MAX_PLAYERS; i++)
     stored_player[i].action = 0;
+
+  ClearJoystickState();
+  ClearPlayerMouseAction();
+}
+
+void SetPlayerMouseAction(int mx, int my, int button)
+{
+  int lx = getLevelFromScreenX(mx);
+  int ly = getLevelFromScreenY(my);
+  int new_button = (!local_player->mouse_action.button && button);
+
+  ClearPlayerMouseAction();
+
+  if (!IN_GFX_FIELD_PLAY(mx, my) || !IN_LEV_FIELD(lx, ly))
+    return;
+
+  local_player->mouse_action.lx = lx;
+  local_player->mouse_action.ly = ly;
+  local_player->mouse_action.button = button;
+
+  if (tape.recording && tape.pausing && tape.use_mouse)
+  {
+    /* un-pause a paused game only if mouse button was newly pressed down */
+    if (new_button)
+      TapeTogglePause(TAPE_TOGGLE_AUTOMATIC);
+  }
+
+  SetTileCursorXY(lx, ly);
 }
 
 void SleepWhileUnmapped()
@@ -314,7 +432,8 @@ void SleepWhileUnmapped()
   {
     Event event;
 
-    NextEvent(&event);
+    if (!WaitValidEvent(&event))
+      continue;
 
     switch (event.type)
     {
@@ -326,6 +445,13 @@ void SleepWhileUnmapped()
        key_joystick_mapping = 0;
        break;
 
+#if defined(TARGET_SDL2)
+      case SDL_CONTROLLERBUTTONUP:
+       HandleJoystickEvent(&event);
+       key_joystick_mapping = 0;
+       break;
+#endif
+
       case EVENT_MAPNOTIFY:
        window_unmapped = FALSE;
        break;
@@ -349,21 +475,25 @@ void SleepWhileUnmapped()
 
 void HandleExposeEvent(ExposeEvent *event)
 {
-#if !defined(TARGET_SDL)
-  RedrawPlayfield(FALSE, event->x, event->y, event->width, event->height);
-  FlushDisplay();
-#endif
 }
 
 void HandleButtonEvent(ButtonEvent *event)
 {
-#if DEBUG_EVENTS
+#if DEBUG_EVENTS_BUTTON
   Error(ERR_DEBUG, "BUTTON EVENT: button %d %s, x/y %d/%d\n",
        event->button,
        event->type == EVENT_BUTTONPRESS ? "pressed" : "released",
        event->x, event->y);
 #endif
 
+  // for any mouse button event, disable playfield tile cursor
+  SetTileCursorEnabled(FALSE);
+
+#if defined(HAS_SCREEN_KEYBOARD)
+  if (video.shifted_up)
+    event->y += video.shifted_up_pos;
+#endif
+
   motion_status = FALSE;
 
   if (event->type == EVENT_BUTTONPRESS)
@@ -376,155 +506,939 @@ void HandleButtonEvent(ButtonEvent *event)
 
 void HandleMotionEvent(MotionEvent *event)
 {
-  if (!PointerInWindow(window))
-    return;    /* window and pointer are on different screens */
-
   if (button_status == MB_RELEASED && game_status != GAME_MODE_EDITOR)
     return;
 
   motion_status = TRUE;
 
+#if DEBUG_EVENTS_MOTION
   Error(ERR_DEBUG, "MOTION EVENT: button %d moved, x/y %d/%d\n",
        button_status, event->x, event->y);
+#endif
 
   HandleButton(event->x, event->y, button_status, button_status);
 }
 
 #if defined(TARGET_SDL2)
-void HandleFingerEvent(FingerEvent *event)
-{
-#if 0
-  static int num_events = 0;
-  int max_events = 10;
-#endif
-
-#if DEBUG_EVENTS
-  Error(ERR_DEBUG, "FINGER EVENT: finger was %s, touch ID %lld, finger ID %lld, x/y %f/%f, dx/dy %f/%f, pressure %f",
-       event->type == EVENT_FINGERPRESS ? "pressed" :
-       event->type == EVENT_FINGERRELEASE ? "released" : "moved",
-       event->touchId,
-       event->fingerId,
-       event->x, event->y,
-       event->dx, event->dy,
-       event->pressure);
-#endif
 
-#if 0
-  int x = (int)(event->x * video.width);
-  int y = (int)(event->y * video.height);
-  int button = MB_LEFTBUTTON;
+void HandleWheelEvent(WheelEvent *event)
+{
+  int button_nr;
 
-  Error(ERR_DEBUG, "=> screen x/y %d/%d", x, y);
+#if DEBUG_EVENTS_WHEEL
+#if 1
+  Error(ERR_DEBUG, "WHEEL EVENT: mouse == %d, x/y == %d/%d\n",
+       event->which, event->x, event->y);
+#else
+  // (SDL_MOUSEWHEEL_NORMAL/SDL_MOUSEWHEEL_FLIPPED needs SDL 2.0.4 or newer)
+  Error(ERR_DEBUG, "WHEEL EVENT: mouse == %d, x/y == %d/%d, direction == %s\n",
+       event->which, event->x, event->y,
+       (event->direction == SDL_MOUSEWHEEL_NORMAL ? "SDL_MOUSEWHEEL_NORMAL" :
+        "SDL_MOUSEWHEEL_FLIPPED"));
 #endif
-
-#if 0
-  if (++num_events >= max_events)
-    CloseAllAndExit(0);
 #endif
 
-#if 1
-#if 0
-  if (event->type == EVENT_FINGERPRESS ||
-      event->type == EVENT_FINGERMOTION)
-    button_status = button;
-  else
-    button_status = MB_RELEASED;
-
-  int max_x = SX + SXSIZE;
-  int max_y = SY + SYSIZE;
-#endif
+  button_nr = (event->x < 0 ? MB_WHEEL_LEFT :
+              event->x > 0 ? MB_WHEEL_RIGHT :
+              event->y < 0 ? MB_WHEEL_DOWN :
+              event->y > 0 ? MB_WHEEL_UP : 0);
 
-#if 1
-  if (game_status == GAME_MODE_PLAYING)
-#else
-  if (game_status == GAME_MODE_PLAYING &&
-      x < max_x)
-#endif
-  {
-    int key_status = (event->type == EVENT_FINGERRELEASE ? KEY_RELEASED :
-                     KEY_PRESSED);
-#if 1
-    Key key = (event->y < 1.0 / 3.0 ? setup.input[0].key.up :
-              event->y > 2.0 / 3.0 ? setup.input[0].key.down :
-              event->x < 1.0 / 3.0 ? setup.input[0].key.left :
-              event->x > 2.0 / 3.0 ? setup.input[0].key.right :
-              setup.input[0].key.drop);
+#if defined(PLATFORM_WIN32) || defined(PLATFORM_MACOSX)
+  // accelerated mouse wheel available on Mac and Windows
+  wheel_steps = (event->x ? ABS(event->x) : ABS(event->y));
 #else
-    Key key = (y <     max_y / 3 ? setup.input[0].key.up :
-              y > 2 * max_y / 3 ? setup.input[0].key.down :
-              x <     max_x / 3 ? setup.input[0].key.left :
-              x > 2 * max_x / 3 ? setup.input[0].key.right :
-              setup.input[0].key.drop);
+  // no accelerated mouse wheel available on Unix/Linux
+  wheel_steps = DEFAULT_WHEEL_STEPS;
 #endif
 
-    Error(ERR_DEBUG, "=> key == %d, key_status == %d", key, key_status);
+  motion_status = FALSE;
 
-    HandleKey(key, key_status);
-  }
-  else
-  {
-#if 0
-    Error(ERR_DEBUG, "::: button_status == %d, button == %d\n",
-         button_status, button);
+  button_status = button_nr;
+  HandleButton(0, 0, button_status, -button_nr);
 
-    HandleButton(x, y, button_status, button);
-#endif
-  }
-#endif
+  button_status = MB_RELEASED;
+  HandleButton(0, 0, button_status, -button_nr);
 }
-#endif
 
-void HandleKeyEvent(KeyEvent *event)
+void HandleWindowEvent(WindowEvent *event)
 {
-  int key_status = (event->type == EVENT_KEYPRESS ? KEY_PRESSED : KEY_RELEASED);
-  boolean with_modifiers = (game_status == GAME_MODE_PLAYING ? FALSE : TRUE);
-  Key key = GetEventKey(event, with_modifiers);
-  Key keymod = (with_modifiers ? GetEventKey(event, FALSE) : key);
-
-#if DEBUG_EVENTS
-  Error(ERR_DEBUG, "KEY EVENT: key was %s, keysym.scancode == %d, keysym.sym == %d, resulting key == %d (%s)",
-       event->type == EVENT_KEYPRESS ? "pressed" : "released",
-       event->keysym.scancode,
-       event->keysym.sym,
-       GetEventKey(event, TRUE),
-       getKeyNameFromKey(key));
+#if DEBUG_EVENTS_WINDOW
+  int subtype = event->event;
+
+  char *event_name =
+    (subtype == SDL_WINDOWEVENT_SHOWN ? "SDL_WINDOWEVENT_SHOWN" :
+     subtype == SDL_WINDOWEVENT_HIDDEN ? "SDL_WINDOWEVENT_HIDDEN" :
+     subtype == SDL_WINDOWEVENT_EXPOSED ? "SDL_WINDOWEVENT_EXPOSED" :
+     subtype == SDL_WINDOWEVENT_MOVED ? "SDL_WINDOWEVENT_MOVED" :
+     subtype == SDL_WINDOWEVENT_SIZE_CHANGED ? "SDL_WINDOWEVENT_SIZE_CHANGED" :
+     subtype == SDL_WINDOWEVENT_RESIZED ? "SDL_WINDOWEVENT_RESIZED" :
+     subtype == SDL_WINDOWEVENT_MINIMIZED ? "SDL_WINDOWEVENT_MINIMIZED" :
+     subtype == SDL_WINDOWEVENT_MAXIMIZED ? "SDL_WINDOWEVENT_MAXIMIZED" :
+     subtype == SDL_WINDOWEVENT_RESTORED ? "SDL_WINDOWEVENT_RESTORED" :
+     subtype == SDL_WINDOWEVENT_ENTER ? "SDL_WINDOWEVENT_ENTER" :
+     subtype == SDL_WINDOWEVENT_LEAVE ? "SDL_WINDOWEVENT_LEAVE" :
+     subtype == SDL_WINDOWEVENT_FOCUS_GAINED ? "SDL_WINDOWEVENT_FOCUS_GAINED" :
+     subtype == SDL_WINDOWEVENT_FOCUS_LOST ? "SDL_WINDOWEVENT_FOCUS_LOST" :
+     subtype == SDL_WINDOWEVENT_CLOSE ? "SDL_WINDOWEVENT_CLOSE" :
+     "(UNKNOWN)");
+
+  Error(ERR_DEBUG, "WINDOW EVENT: '%s', %ld, %ld",
+       event_name, event->data1, event->data2);
 #endif
 
 #if 0
-  if (key == KSYM_Menu)
-    Error(ERR_DEBUG, "menu key pressed");
-  else if (key == KSYM_Back)
-    Error(ERR_DEBUG, "back key pressed");
+  // (not needed, as the screen gets redrawn every 20 ms anyway)
+  if (event->event == SDL_WINDOWEVENT_SIZE_CHANGED ||
+      event->event == SDL_WINDOWEVENT_RESIZED ||
+      event->event == SDL_WINDOWEVENT_EXPOSED)
+    SDLRedrawWindow();
 #endif
 
+  if (event->event == SDL_WINDOWEVENT_RESIZED)
+  {
+    if (!video.fullscreen_enabled)
+    {
+      int new_window_width  = event->data1;
+      int new_window_height = event->data2;
+
+      // if window size has changed after resizing, calculate new scaling factor
+      if (new_window_width  != video.window_width ||
+         new_window_height != video.window_height)
+      {
+       int new_xpercent = 100.0 * new_window_width  / video.screen_width  + .5;
+       int new_ypercent = 100.0 * new_window_height / video.screen_height + .5;
+
+       // (extreme window scaling allowed, but cannot be saved permanently)
+       video.window_scaling_percent = MIN(new_xpercent, new_ypercent);
+       setup.window_scaling_percent =
+         MIN(MAX(MIN_WINDOW_SCALING_PERCENT, video.window_scaling_percent),
+             MAX_WINDOW_SCALING_PERCENT);
+
+       video.window_width  = new_window_width;
+       video.window_height = new_window_height;
+
+       if (game_status == GAME_MODE_SETUP)
+         RedrawSetupScreenAfterFullscreenToggle();
+
+       SetWindowTitle();
+      }
+    }
 #if defined(PLATFORM_ANDROID)
-  // always map the "back" button to the "escape" key on Android devices
-  if (key == KSYM_Back)
-    key = KSYM_Escape;
-#endif
+    else
+    {
+      int new_display_width  = event->data1;
+      int new_display_height = event->data2;
 
-  HandleKeyModState(keymod, key_status);
-  HandleKey(key, key_status);
+      // if fullscreen display size has changed, device has been rotated
+      if (new_display_width  != video.display_width ||
+         new_display_height != video.display_height)
+      {
+       video.display_width  = new_display_width;
+       video.display_height = new_display_height;
+
+       SDLSetScreenProperties();
+      }
+    }
+#endif
+  }
 }
 
-void HandleFocusEvent(FocusChangeEvent *event)
+#define NUM_TOUCH_FINGERS              3
+
+static struct
 {
-  static int old_joystick_status = -1;
+  boolean touched;
+  SDL_FingerID finger_id;
+  int counter;
+  Key key;
+} touch_info[NUM_TOUCH_FINGERS];
 
-  if (event->type == EVENT_FOCUSOUT)
+void HandleFingerEvent_VirtualButtons(FingerEvent *event)
+{
+  float ypos = 1.0 - 1.0 / 3.0 * video.display_width / video.display_height;
+  float event_x = (event->x);
+  float event_y = (event->y - ypos) / (1 - ypos);
+  Key key = (event_x > 0         && event_x < 1.0 / 6.0 &&
+            event_y > 2.0 / 3.0 && event_y < 1 ?
+            setup.input[0].key.snap :
+            event_x > 1.0 / 6.0 && event_x < 1.0 / 3.0 &&
+            event_y > 2.0 / 3.0 && event_y < 1 ?
+            setup.input[0].key.drop :
+            event_x > 7.0 / 9.0 && event_x < 8.0 / 9.0 &&
+            event_y > 0         && event_y < 1.0 / 3.0 ?
+            setup.input[0].key.up :
+            event_x > 6.0 / 9.0 && event_x < 7.0 / 9.0 &&
+            event_y > 1.0 / 3.0 && event_y < 2.0 / 3.0 ?
+            setup.input[0].key.left :
+            event_x > 8.0 / 9.0 && event_x < 1 &&
+            event_y > 1.0 / 3.0 && event_y < 2.0 / 3.0 ?
+            setup.input[0].key.right :
+            event_x > 7.0 / 9.0 && event_x < 8.0 / 9.0 &&
+            event_y > 2.0 / 3.0 && event_y < 1 ?
+            setup.input[0].key.down :
+            KSYM_UNDEFINED);
+  int key_status = (event->type == EVENT_FINGERRELEASE ? KEY_RELEASED :
+                   KEY_PRESSED);
+  char *key_status_name = (key_status == KEY_RELEASED ? "KEY_RELEASED" :
+                          "KEY_PRESSED");
+  int i;
+
+  // for any touch input event, enable overlay buttons (if activated)
+  SetOverlayEnabled(TRUE);
+
+  Error(ERR_DEBUG, "::: key '%s' was '%s' [fingerId: %lld]",
+       getKeyNameFromKey(key), key_status_name, event->fingerId);
+
+  // check if we already know this touch event's finger id
+  for (i = 0; i < NUM_TOUCH_FINGERS; i++)
   {
-    KeyboardAutoRepeatOn();
-    old_joystick_status = joystick.status;
-    joystick.status = JOYSTICK_NOT_AVAILABLE;
+    if (touch_info[i].touched &&
+       touch_info[i].finger_id == event->fingerId)
+    {
+      // Error(ERR_DEBUG, "MARK 1: %d", i);
 
-    ClearPlayerAction();
+      break;
+    }
   }
-  else if (event->type == EVENT_FOCUSIN)
+
+  if (i >= NUM_TOUCH_FINGERS)
   {
-    /* When there are two Rocks'n'Diamonds windows which overlap and
-       the player moves the pointer from one game window to the other,
-       a 'FocusOut' event is generated for the window the pointer is
-       leaving and a 'FocusIn' event is generated for the window the
+    if (key_status == KEY_PRESSED)
+    {
+      int oldest_pos = 0, oldest_counter = touch_info[0].counter;
+
+      // unknown finger id -- get new, empty slot, if available
+      for (i = 0; i < NUM_TOUCH_FINGERS; i++)
+      {
+       if (touch_info[i].counter < oldest_counter)
+       {
+         oldest_pos = i;
+         oldest_counter = touch_info[i].counter;
+
+         // Error(ERR_DEBUG, "MARK 2: %d", i);
+       }
+
+       if (!touch_info[i].touched)
+       {
+         // Error(ERR_DEBUG, "MARK 3: %d", i);
+
+         break;
+       }
+      }
+
+      if (i >= NUM_TOUCH_FINGERS)
+      {
+       // all slots allocated -- use oldest slot
+       i = oldest_pos;
+
+       // Error(ERR_DEBUG, "MARK 4: %d", i);
+      }
+    }
+    else
+    {
+      // release of previously unknown key (should not happen)
+
+      if (key != KSYM_UNDEFINED)
+      {
+       HandleKey(key, KEY_RELEASED);
+
+       Error(ERR_DEBUG, "=> key == '%s', key_status == '%s' [slot %d] [1]",
+             getKeyNameFromKey(key), "KEY_RELEASED", i);
+      }
+    }
+  }
+
+  if (i < NUM_TOUCH_FINGERS)
+  {
+    if (key_status == KEY_PRESSED)
+    {
+      if (touch_info[i].key != key)
+      {
+       if (touch_info[i].key != KSYM_UNDEFINED)
+       {
+         HandleKey(touch_info[i].key, KEY_RELEASED);
+
+         Error(ERR_DEBUG, "=> key == '%s', key_status == '%s' [slot %d] [2]",
+               getKeyNameFromKey(touch_info[i].key), "KEY_RELEASED", i);
+       }
+
+       if (key != KSYM_UNDEFINED)
+       {
+         HandleKey(key, KEY_PRESSED);
+
+         Error(ERR_DEBUG, "=> key == '%s', key_status == '%s' [slot %d] [3]",
+               getKeyNameFromKey(key), "KEY_PRESSED", i);
+       }
+      }
+
+      touch_info[i].touched = TRUE;
+      touch_info[i].finger_id = event->fingerId;
+      touch_info[i].counter = Counter();
+      touch_info[i].key = key;
+    }
+    else
+    {
+      if (touch_info[i].key != KSYM_UNDEFINED)
+      {
+       HandleKey(touch_info[i].key, KEY_RELEASED);
+
+       Error(ERR_DEBUG, "=> key == '%s', key_status == '%s' [slot %d] [4]",
+             getKeyNameFromKey(touch_info[i].key), "KEY_RELEASED", i);
+      }
+
+      touch_info[i].touched = FALSE;
+      touch_info[i].finger_id = 0;
+      touch_info[i].counter = 0;
+      touch_info[i].key = 0;
+    }
+  }
+}
+
+void HandleFingerEvent_WipeGestures(FingerEvent *event)
+{
+  static Key motion_key_x = KSYM_UNDEFINED;
+  static Key motion_key_y = KSYM_UNDEFINED;
+  static Key button_key = KSYM_UNDEFINED;
+  static float motion_x1, motion_y1;
+  static float button_x1, button_y1;
+  static SDL_FingerID motion_id = -1;
+  static SDL_FingerID button_id = -1;
+  int move_trigger_distance_percent = setup.touch.move_distance;
+  int drop_trigger_distance_percent = setup.touch.drop_distance;
+  float move_trigger_distance = (float)move_trigger_distance_percent / 100;
+  float drop_trigger_distance = (float)drop_trigger_distance_percent / 100;
+  float event_x = event->x;
+  float event_y = event->y;
+
+  if (event->type == EVENT_FINGERPRESS)
+  {
+    if (event_x > 1.0 / 3.0)
+    {
+      // motion area
+
+      motion_id = event->fingerId;
+
+      motion_x1 = event_x;
+      motion_y1 = event_y;
+
+      motion_key_x = KSYM_UNDEFINED;
+      motion_key_y = KSYM_UNDEFINED;
+
+      Error(ERR_DEBUG, "---------- MOVE STARTED (WAIT) ----------");
+    }
+    else
+    {
+      // button area
+
+      button_id = event->fingerId;
+
+      button_x1 = event_x;
+      button_y1 = event_y;
+
+      button_key = setup.input[0].key.snap;
+
+      HandleKey(button_key, KEY_PRESSED);
+
+      Error(ERR_DEBUG, "---------- SNAP STARTED ----------");
+    }
+  }
+  else if (event->type == EVENT_FINGERRELEASE)
+  {
+    if (event->fingerId == motion_id)
+    {
+      motion_id = -1;
+
+      if (motion_key_x != KSYM_UNDEFINED)
+       HandleKey(motion_key_x, KEY_RELEASED);
+      if (motion_key_y != KSYM_UNDEFINED)
+       HandleKey(motion_key_y, KEY_RELEASED);
+
+      motion_key_x = KSYM_UNDEFINED;
+      motion_key_y = KSYM_UNDEFINED;
+
+      Error(ERR_DEBUG, "---------- MOVE STOPPED ----------");
+    }
+    else if (event->fingerId == button_id)
+    {
+      button_id = -1;
+
+      if (button_key != KSYM_UNDEFINED)
+       HandleKey(button_key, KEY_RELEASED);
+
+      button_key = KSYM_UNDEFINED;
+
+      Error(ERR_DEBUG, "---------- SNAP STOPPED ----------");
+    }
+  }
+  else if (event->type == EVENT_FINGERMOTION)
+  {
+    if (event->fingerId == motion_id)
+    {
+      float distance_x = ABS(event_x - motion_x1);
+      float distance_y = ABS(event_y - motion_y1);
+      Key new_motion_key_x = (event_x < motion_x1 ? setup.input[0].key.left :
+                             event_x > motion_x1 ? setup.input[0].key.right :
+                             KSYM_UNDEFINED);
+      Key new_motion_key_y = (event_y < motion_y1 ? setup.input[0].key.up :
+                             event_y > motion_y1 ? setup.input[0].key.down :
+                             KSYM_UNDEFINED);
+
+      if (distance_x < move_trigger_distance / 2 ||
+         distance_x < distance_y)
+       new_motion_key_x = KSYM_UNDEFINED;
+
+      if (distance_y < move_trigger_distance / 2 ||
+         distance_y < distance_x)
+       new_motion_key_y = KSYM_UNDEFINED;
+
+      if (distance_x > move_trigger_distance ||
+         distance_y > move_trigger_distance)
+      {
+       if (new_motion_key_x != motion_key_x)
+       {
+         if (motion_key_x != KSYM_UNDEFINED)
+           HandleKey(motion_key_x, KEY_RELEASED);
+         if (new_motion_key_x != KSYM_UNDEFINED)
+           HandleKey(new_motion_key_x, KEY_PRESSED);
+       }
+
+       if (new_motion_key_y != motion_key_y)
+       {
+         if (motion_key_y != KSYM_UNDEFINED)
+           HandleKey(motion_key_y, KEY_RELEASED);
+         if (new_motion_key_y != KSYM_UNDEFINED)
+           HandleKey(new_motion_key_y, KEY_PRESSED);
+       }
+
+       motion_x1 = event_x;
+       motion_y1 = event_y;
+
+       motion_key_x = new_motion_key_x;
+       motion_key_y = new_motion_key_y;
+
+       Error(ERR_DEBUG, "---------- MOVE STARTED (MOVE) ----------");
+      }
+    }
+    else if (event->fingerId == button_id)
+    {
+      float distance_x = ABS(event_x - button_x1);
+      float distance_y = ABS(event_y - button_y1);
+
+      if (distance_x < drop_trigger_distance / 2 &&
+         distance_y > drop_trigger_distance)
+      {
+       if (button_key == setup.input[0].key.snap)
+         HandleKey(button_key, KEY_RELEASED);
+
+       button_x1 = event_x;
+       button_y1 = event_y;
+
+       button_key = setup.input[0].key.drop;
+
+       HandleKey(button_key, KEY_PRESSED);
+
+       Error(ERR_DEBUG, "---------- DROP STARTED ----------");
+      }
+    }
+  }
+}
+
+void HandleFingerEvent(FingerEvent *event)
+{
+#if DEBUG_EVENTS_FINGER
+  Error(ERR_DEBUG, "FINGER EVENT: finger was %s, touch ID %lld, finger ID %lld, x/y %f/%f, dx/dy %f/%f, pressure %f",
+       event->type == EVENT_FINGERPRESS ? "pressed" :
+       event->type == EVENT_FINGERRELEASE ? "released" : "moved",
+       event->touchId,
+       event->fingerId,
+       event->x, event->y,
+       event->dx, event->dy,
+       event->pressure);
+#endif
+
+  if (game_status != GAME_MODE_PLAYING)
+    return;
+
+  if (level.game_engine_type == GAME_ENGINE_TYPE_MM)
+    return;
+
+  if (strEqual(setup.touch.control_type, TOUCH_CONTROL_VIRTUAL_BUTTONS))
+    HandleFingerEvent_VirtualButtons(event);
+  else if (strEqual(setup.touch.control_type, TOUCH_CONTROL_WIPE_GESTURES))
+    HandleFingerEvent_WipeGestures(event);
+}
+
+#endif
+
+static void HandleButtonOrFinger_WipeGestures_MM(int mx, int my, int button)
+{
+  static int old_mx = 0, old_my = 0;
+  static int last_button = MB_LEFTBUTTON;
+  static boolean touched = FALSE;
+  static boolean tapped = FALSE;
+
+  // screen tile was tapped (but finger not touching the screen anymore)
+  // (this point will also be reached without receiving a touch event)
+  if (tapped && !touched)
+  {
+    SetPlayerMouseAction(old_mx, old_my, MB_RELEASED);
+
+    tapped = FALSE;
+  }
+
+  // stop here if this function was not triggered by a touch event
+  if (button == -1)
+    return;
+
+  if (button == MB_PRESSED && IN_GFX_FIELD_PLAY(mx, my))
+  {
+    // finger started touching the screen
+
+    touched = TRUE;
+    tapped = TRUE;
+
+    if (!motion_status)
+    {
+      old_mx = mx;
+      old_my = my;
+
+      ClearPlayerMouseAction();
+
+      Error(ERR_DEBUG, "---------- TOUCH ACTION STARTED ----------");
+    }
+  }
+  else if (button == MB_RELEASED && touched)
+  {
+    // finger stopped touching the screen
+
+    touched = FALSE;
+
+    if (tapped)
+      SetPlayerMouseAction(old_mx, old_my, last_button);
+    else
+      SetPlayerMouseAction(old_mx, old_my, MB_RELEASED);
+
+    Error(ERR_DEBUG, "---------- TOUCH ACTION STOPPED ----------");
+  }
+
+  if (touched)
+  {
+    // finger moved while touching the screen
+
+    int old_x = getLevelFromScreenX(old_mx);
+    int old_y = getLevelFromScreenY(old_my);
+    int new_x = getLevelFromScreenX(mx);
+    int new_y = getLevelFromScreenY(my);
+
+    if (new_x != old_x || new_y != old_y)
+      tapped = FALSE;
+
+    if (new_x != old_x)
+    {
+      // finger moved left or right from (horizontal) starting position
+
+      int button_nr = (new_x < old_x ? MB_LEFTBUTTON : MB_RIGHTBUTTON);
+
+      SetPlayerMouseAction(old_mx, old_my, button_nr);
+
+      last_button = button_nr;
+
+      Error(ERR_DEBUG, "---------- TOUCH ACTION: ROTATING ----------");
+    }
+    else
+    {
+      // finger stays at or returned to (horizontal) starting position
+
+      SetPlayerMouseAction(old_mx, old_my, MB_RELEASED);
+
+      Error(ERR_DEBUG, "---------- TOUCH ACTION PAUSED ----------");
+    }
+  }
+}
+
+static void HandleButtonOrFinger_FollowFinger_MM(int mx, int my, int button)
+{
+  static int old_mx = 0, old_my = 0;
+  static int last_button = MB_LEFTBUTTON;
+  static boolean touched = FALSE;
+  static boolean tapped = FALSE;
+
+  // screen tile was tapped (but finger not touching the screen anymore)
+  // (this point will also be reached without receiving a touch event)
+  if (tapped && !touched)
+  {
+    SetPlayerMouseAction(old_mx, old_my, MB_RELEASED);
+
+    tapped = FALSE;
+  }
+
+  // stop here if this function was not triggered by a touch event
+  if (button == -1)
+    return;
+
+  if (button == MB_PRESSED && IN_GFX_FIELD_PLAY(mx, my))
+  {
+    // finger started touching the screen
+
+    touched = TRUE;
+    tapped = TRUE;
+
+    if (!motion_status)
+    {
+      old_mx = mx;
+      old_my = my;
+
+      ClearPlayerMouseAction();
+
+      Error(ERR_DEBUG, "---------- TOUCH ACTION STARTED ----------");
+    }
+  }
+  else if (button == MB_RELEASED && touched)
+  {
+    // finger stopped touching the screen
+
+    touched = FALSE;
+
+    if (tapped)
+      SetPlayerMouseAction(old_mx, old_my, last_button);
+    else
+      SetPlayerMouseAction(old_mx, old_my, MB_RELEASED);
+
+    Error(ERR_DEBUG, "---------- TOUCH ACTION STOPPED ----------");
+  }
+
+  if (touched)
+  {
+    // finger moved while touching the screen
+
+    int old_x = getLevelFromScreenX(old_mx);
+    int old_y = getLevelFromScreenY(old_my);
+    int new_x = getLevelFromScreenX(mx);
+    int new_y = getLevelFromScreenY(my);
+
+    if (new_x != old_x || new_y != old_y)
+    {
+      // finger moved away from starting position
+
+      int button_nr = getButtonFromTouchPosition(old_x, old_y, mx, my);
+
+      // quickly alternate between clicking and releasing for maximum speed
+      if (FrameCounter % 2 == 0)
+       button_nr = MB_RELEASED;
+
+      SetPlayerMouseAction(old_mx, old_my, button_nr);
+
+      if (button_nr)
+       last_button = button_nr;
+
+      tapped = FALSE;
+
+      Error(ERR_DEBUG, "---------- TOUCH ACTION: ROTATING ----------");
+    }
+    else
+    {
+      // finger stays at or returned to starting position
+
+      SetPlayerMouseAction(old_mx, old_my, MB_RELEASED);
+
+      Error(ERR_DEBUG, "---------- TOUCH ACTION PAUSED ----------");
+    }
+  }
+}
+
+static void HandleButtonOrFinger_FollowFinger(int mx, int my, int button)
+{
+  static int old_mx = 0, old_my = 0;
+  static Key motion_key_x = KSYM_UNDEFINED;
+  static Key motion_key_y = KSYM_UNDEFINED;
+  static boolean touched = FALSE;
+  static boolean started_on_player = FALSE;
+  static boolean player_is_dropping = FALSE;
+  static int player_drop_count = 0;
+  static int last_player_x = -1;
+  static int last_player_y = -1;
+
+  if (button == MB_PRESSED && IN_GFX_FIELD_PLAY(mx, my))
+  {
+    touched = TRUE;
+
+    old_mx = mx;
+    old_my = my;
+
+    if (!motion_status)
+    {
+      started_on_player = FALSE;
+      player_is_dropping = FALSE;
+      player_drop_count = 0;
+      last_player_x = -1;
+      last_player_y = -1;
+
+      motion_key_x = KSYM_UNDEFINED;
+      motion_key_y = KSYM_UNDEFINED;
+
+      Error(ERR_DEBUG, "---------- TOUCH ACTION STARTED ----------");
+    }
+  }
+  else if (button == MB_RELEASED && touched)
+  {
+    touched = FALSE;
+
+    old_mx = 0;
+    old_my = 0;
+
+    if (motion_key_x != KSYM_UNDEFINED)
+      HandleKey(motion_key_x, KEY_RELEASED);
+    if (motion_key_y != KSYM_UNDEFINED)
+      HandleKey(motion_key_y, KEY_RELEASED);
+
+    if (started_on_player)
+    {
+      if (player_is_dropping)
+      {
+       Error(ERR_DEBUG, "---------- DROP STOPPED ----------");
+
+       HandleKey(setup.input[0].key.drop, KEY_RELEASED);
+      }
+      else
+      {
+       Error(ERR_DEBUG, "---------- SNAP STOPPED ----------");
+
+       HandleKey(setup.input[0].key.snap, KEY_RELEASED);
+      }
+    }
+
+    motion_key_x = KSYM_UNDEFINED;
+    motion_key_y = KSYM_UNDEFINED;
+
+    Error(ERR_DEBUG, "---------- TOUCH ACTION STOPPED ----------");
+  }
+
+  if (touched)
+  {
+    int src_x = local_player->jx;
+    int src_y = local_player->jy;
+    int dst_x = getLevelFromScreenX(old_mx);
+    int dst_y = getLevelFromScreenY(old_my);
+    int dx = dst_x - src_x;
+    int dy = dst_y - src_y;
+    Key new_motion_key_x = (dx < 0 ? setup.input[0].key.left :
+                           dx > 0 ? setup.input[0].key.right :
+                           KSYM_UNDEFINED);
+    Key new_motion_key_y = (dy < 0 ? setup.input[0].key.up :
+                           dy > 0 ? setup.input[0].key.down :
+                           KSYM_UNDEFINED);
+
+    if (dx != 0 && dy != 0 && ABS(dx) != ABS(dy) &&
+       (last_player_x != local_player->jx ||
+        last_player_y != local_player->jy))
+    {
+      // in case of asymmetric diagonal movement, use "preferred" direction
+
+      int last_move_dir = (ABS(dx) > ABS(dy) ? MV_VERTICAL : MV_HORIZONTAL);
+
+      if (level.game_engine_type == GAME_ENGINE_TYPE_EM)
+       level.native_em_level->ply[0]->last_move_dir = last_move_dir;
+      else
+       local_player->last_move_dir = last_move_dir;
+
+      // (required to prevent accidentally forcing direction for next movement)
+      last_player_x = local_player->jx;
+      last_player_y = local_player->jy;
+    }
+
+    if (button == MB_PRESSED && !motion_status && dx == 0 && dy == 0)
+    {
+      started_on_player = TRUE;
+      player_drop_count = getPlayerInventorySize(0);
+      player_is_dropping = (player_drop_count > 0);
+
+      if (player_is_dropping)
+      {
+       Error(ERR_DEBUG, "---------- DROP STARTED ----------");
+
+       HandleKey(setup.input[0].key.drop, KEY_PRESSED);
+      }
+      else
+      {
+       Error(ERR_DEBUG, "---------- SNAP STARTED ----------");
+
+       HandleKey(setup.input[0].key.snap, KEY_PRESSED);
+      }
+    }
+    else if (dx != 0 || dy != 0)
+    {
+      if (player_is_dropping &&
+         player_drop_count == getPlayerInventorySize(0))
+      {
+       Error(ERR_DEBUG, "---------- DROP -> SNAP ----------");
+
+       HandleKey(setup.input[0].key.drop, KEY_RELEASED);
+       HandleKey(setup.input[0].key.snap, KEY_PRESSED);
+
+       player_is_dropping = FALSE;
+      }
+    }
+
+    if (new_motion_key_x != motion_key_x)
+    {
+      Error(ERR_DEBUG, "---------- %s %s ----------",
+           started_on_player && !player_is_dropping ? "SNAPPING" : "MOVING",
+           dx < 0 ? "LEFT" : dx > 0 ? "RIGHT" : "PAUSED");
+
+      if (motion_key_x != KSYM_UNDEFINED)
+       HandleKey(motion_key_x, KEY_RELEASED);
+      if (new_motion_key_x != KSYM_UNDEFINED)
+       HandleKey(new_motion_key_x, KEY_PRESSED);
+    }
+
+    if (new_motion_key_y != motion_key_y)
+    {
+      Error(ERR_DEBUG, "---------- %s %s ----------",
+           started_on_player && !player_is_dropping ? "SNAPPING" : "MOVING",
+           dy < 0 ? "UP" : dy > 0 ? "DOWN" : "PAUSED");
+
+      if (motion_key_y != KSYM_UNDEFINED)
+       HandleKey(motion_key_y, KEY_RELEASED);
+      if (new_motion_key_y != KSYM_UNDEFINED)
+       HandleKey(new_motion_key_y, KEY_PRESSED);
+    }
+
+    motion_key_x = new_motion_key_x;
+    motion_key_y = new_motion_key_y;
+  }
+}
+
+static void HandleButtonOrFinger(int mx, int my, int button)
+{
+  if (game_status != GAME_MODE_PLAYING)
+    return;
+
+  if (level.game_engine_type == GAME_ENGINE_TYPE_MM)
+  {
+    if (strEqual(setup.touch.control_type, TOUCH_CONTROL_WIPE_GESTURES))
+      HandleButtonOrFinger_WipeGestures_MM(mx, my, button);
+    else if (strEqual(setup.touch.control_type, TOUCH_CONTROL_FOLLOW_FINGER))
+      HandleButtonOrFinger_FollowFinger_MM(mx, my, button);
+  }
+  else
+  {
+    if (strEqual(setup.touch.control_type, TOUCH_CONTROL_FOLLOW_FINGER))
+      HandleButtonOrFinger_FollowFinger(mx, my, button);
+  }
+}
+
+#if defined(TARGET_SDL2)
+
+static boolean checkTextInputKeyModState()
+{
+  // when playing, only handle raw key events and ignore text input
+  if (game_status == GAME_MODE_PLAYING)
+    return FALSE;
+
+  return ((GetKeyModState() & KMOD_TextInput) != KMOD_None);
+}
+
+void HandleTextEvent(TextEvent *event)
+{
+  char *text = event->text;
+  Key key = getKeyFromKeyName(text);
+
+#if DEBUG_EVENTS_TEXT
+  Error(ERR_DEBUG, "TEXT EVENT: text == '%s' [%d byte(s), '%c'/%d], resulting key == %d (%s) [%04x]",
+       text,
+       strlen(text),
+       text[0], (int)(text[0]),
+       key,
+       getKeyNameFromKey(key),
+       GetKeyModState());
+#endif
+
+#if !defined(HAS_SCREEN_KEYBOARD)
+  // non-mobile devices: only handle key input with modifier keys pressed here
+  // (every other key input is handled directly as physical key input event)
+  if (!checkTextInputKeyModState())
+    return;
+#endif
+
+  // process text input as "classic" (with uppercase etc.) key input event
+  HandleKey(key, KEY_PRESSED);
+  HandleKey(key, KEY_RELEASED);
+}
+
+void HandlePauseResumeEvent(PauseResumeEvent *event)
+{
+  if (event->type == SDL_APP_WILLENTERBACKGROUND)
+  {
+    Mix_PauseMusic();
+  }
+  else if (event->type == SDL_APP_DIDENTERFOREGROUND)
+  {
+    Mix_ResumeMusic();
+  }
+}
+
+#endif
+
+void HandleKeyEvent(KeyEvent *event)
+{
+  int key_status = (event->type == EVENT_KEYPRESS ? KEY_PRESSED : KEY_RELEASED);
+  boolean with_modifiers = (game_status == GAME_MODE_PLAYING ? FALSE : TRUE);
+  Key key = GetEventKey(event, with_modifiers);
+  Key keymod = (with_modifiers ? GetEventKey(event, FALSE) : key);
+
+#if DEBUG_EVENTS_KEY
+  Error(ERR_DEBUG, "KEY EVENT: key was %s, keysym.scancode == %d, keysym.sym == %d, keymod = %d, GetKeyModState() = 0x%04x, resulting key == %d (%s)",
+       event->type == EVENT_KEYPRESS ? "pressed" : "released",
+       event->keysym.scancode,
+       event->keysym.sym,
+       keymod,
+       GetKeyModState(),
+       key,
+       getKeyNameFromKey(key));
+#endif
+
+#if defined(PLATFORM_ANDROID)
+  if (key == KSYM_Back)
+  {
+    // always map the "back" button to the "escape" key on Android devices
+    key = KSYM_Escape;
+  }
+  else
+  {
+    // for any key event other than "back" button, disable overlay buttons
+    SetOverlayEnabled(FALSE);
+  }
+#endif
+
+  HandleKeyModState(keymod, key_status);
+
+#if defined(TARGET_SDL2)
+  // only handle raw key input without text modifier keys pressed
+  if (!checkTextInputKeyModState())
+    HandleKey(key, key_status);
+#else
+  HandleKey(key, key_status);
+#endif
+}
+
+void HandleFocusEvent(FocusChangeEvent *event)
+{
+  static int old_joystick_status = -1;
+
+  if (event->type == EVENT_FOCUSOUT)
+  {
+    KeyboardAutoRepeatOn();
+    old_joystick_status = joystick.status;
+    joystick.status = JOYSTICK_NOT_AVAILABLE;
+
+    ClearPlayerAction();
+  }
+  else if (event->type == EVENT_FOCUSIN)
+  {
+    /* When there are two Rocks'n'Diamonds windows which overlap and
+       the player moves the pointer from one game window to the other,
+       a 'FocusOut' event is generated for the window the pointer is
+       leaving and a 'FocusIn' event is generated for the window the
        pointer is entering. In some cases, it can happen that the
        'FocusIn' event is handled by the one game process before the
        'FocusOut' event by the other game process. In this case the
@@ -564,12 +1478,15 @@ void HandleWindowManagerEvent(Event *event)
 void HandleButton(int mx, int my, int button, int button_nr)
 {
   static int old_mx = 0, old_my = 0;
+  boolean button_hold = FALSE;
+  boolean handle_gadgets = TRUE;
 
-  if (button < 0)
+  if (button_nr < 0)
   {
     mx = old_mx;
     my = old_my;
-    button = -button;
+    button_nr = -button_nr;
+    button_hold = TRUE;
   }
   else
   {
@@ -577,18 +1494,34 @@ void HandleButton(int mx, int my, int button, int button_nr)
     old_my = my;
   }
 
-  if (HandleGadgets(mx, my, button))
+#if defined(PLATFORM_ANDROID)
+  // when playing, only handle gadgets when using "follow finger" controls
+  // or when using touch controls in combination with the MM game engine
+  handle_gadgets =
+    (game_status != GAME_MODE_PLAYING ||
+     level.game_engine_type == GAME_ENGINE_TYPE_MM ||
+     strEqual(setup.touch.control_type, TOUCH_CONTROL_FOLLOW_FINGER));
+#endif
+
+  if (handle_gadgets && HandleGadgets(mx, my, button))
   {
     /* do not handle this button event anymore */
     mx = my = -32;     /* force mouse event to be outside screen tiles */
   }
 
+  if (HandleGlobalAnimClicks(mx, my, button))
+  {
+    /* do not handle this button event anymore */
+    return;            /* force mouse event not to be handled at all */
+  }
+
+  if (button_hold && game_status == GAME_MODE_PLAYING && tape.pausing)
+    return;
+
   /* do not use scroll wheel button events for anything other than gadgets */
   if (IS_WHEEL_BUTTON(button_nr))
     return;
 
-  Error(ERR_DEBUG, "::: game_status == %d", game_status);
-
   switch (game_status)
   {
     case GAME_MODE_TITLE:
@@ -628,10 +1561,17 @@ void HandleButton(int mx, int my, int button, int button_nr)
       break;
 
     case GAME_MODE_PLAYING:
+      if (!strEqual(setup.touch.control_type, TOUCH_CONTROL_OFF))
+       HandleButtonOrFinger(mx, my, button);
+      else
+       SetPlayerMouseAction(mx, my, button);
+
 #ifdef DEBUG
-      if (button == MB_PRESSED && !motion_status && IN_GFX_SCREEN(mx, my))
-       DumpTile(LEVELX((mx - SX) / TILEX), LEVELY((my - SY) / TILEY));
+      if (button == MB_PRESSED && !motion_status && !button_hold &&
+         IN_GFX_FIELD_PLAY(mx, my) && GetKeyModState() & KMOD_Control)
+       DumpTileFromScreen(mx, my);
 #endif
+
       break;
 
     default:
@@ -673,7 +1613,7 @@ static void HandleKeysSpecial(Key key)
   cheat_input[cheat_input_len++] = letter;
   cheat_input[cheat_input_len] = '\0';
 
-#if DEBUG_EVENTS
+#if DEBUG_EVENTS_KEY
   Error(ERR_DEBUG, "SPECIAL KEY '%s' [%d]\n", cheat_input, cheat_input_len);
 #endif
 
@@ -720,11 +1660,30 @@ static void HandleKeysSpecial(Key key)
     {
       DumpTape(&tape);
     }
+    else if (is_string_suffix(cheat_input, ":fix-tape") ||
+            is_string_suffix(cheat_input, ":ft"))
+    {
+      /* fix single-player tapes that contain player input for more than one
+        player (due to a bug in 3.3.1.2 and earlier versions), which results
+        in playing levels with more than one player in multi-player mode,
+        even though the tape was originally recorded in single-player mode */
+
+      /* remove player input actions for all players but the first one */
+      for (i = 1; i < MAX_PLAYERS; i++)
+       tape.player_participates[i] = FALSE;
+
+      tape.changed = TRUE;
+    }
     else if (is_string_suffix(cheat_input, ":save-native-level") ||
             is_string_suffix(cheat_input, ":snl"))
     {
       SaveNativeLevel(&level);
     }
+    else if (is_string_suffix(cheat_input, ":frames-per-second") ||
+            is_string_suffix(cheat_input, ":fps"))
+    {
+      global.show_frames_per_second = !global.show_frames_per_second;
+    }
   }
   else if (game_status == GAME_MODE_PLAYING)
   {
@@ -747,9 +1706,64 @@ static void HandleKeysSpecial(Key key)
   }
 }
 
+void HandleKeysDebug(Key key)
+{
+#ifdef DEBUG
+  int i;
+
+  if (game_status == GAME_MODE_PLAYING || !setup.debug.frame_delay_game_only)
+  {
+    boolean mod_key_pressed = ((GetKeyModState() & KMOD_Valid) != KMOD_None);
+
+    for (i = 0; i < NUM_DEBUG_FRAME_DELAY_KEYS; i++)
+    {
+      if (key == setup.debug.frame_delay_key[i] &&
+         (mod_key_pressed == setup.debug.frame_delay_use_mod_key))
+      {
+       GameFrameDelay = (GameFrameDelay != setup.debug.frame_delay[i] ?
+                         setup.debug.frame_delay[i] : setup.game_frame_delay);
+
+       if (!setup.debug.frame_delay_game_only)
+         MenuFrameDelay = GameFrameDelay;
+
+       SetVideoFrameDelay(GameFrameDelay);
+
+       if (GameFrameDelay > ONE_SECOND_DELAY)
+         Error(ERR_DEBUG, "frame delay == %d ms", GameFrameDelay);
+       else if (GameFrameDelay != 0)
+         Error(ERR_DEBUG, "frame delay == %d ms (max. %d fps / %d %%)",
+               GameFrameDelay, ONE_SECOND_DELAY / GameFrameDelay,
+               GAME_FRAME_DELAY * 100 / GameFrameDelay);
+       else
+         Error(ERR_DEBUG, "frame delay == 0 ms (maximum speed)");
+
+       break;
+      }
+    }
+  }
+
+  if (game_status == GAME_MODE_PLAYING)
+  {
+    if (key == KSYM_d)
+    {
+      options.debug = !options.debug;
+
+      Error(ERR_DEBUG, "debug mode %s",
+           (options.debug ? "enabled" : "disabled"));
+    }
+    else if (key == KSYM_v)
+    {
+      Error(ERR_DEBUG, "currently using game engine version %d",
+           game.engine_version);
+    }
+  }
+#endif
+}
+
 void HandleKey(Key key, int key_status)
 {
   boolean anyTextGadgetActiveOrJustFinished = anyTextGadgetActive();
+  static boolean ignore_repeated_key = FALSE;
   static struct SetupKeyboardInfo ski;
   static struct SetupShortcutInfo ssi;
   static struct
@@ -770,13 +1784,20 @@ void HandleKey(Key key, int key_status)
   int joy = 0;
   int i;
 
+#if defined(TARGET_SDL2)
+  /* map special keys (media keys / remote control buttons) to default keys */
+  if (key == KSYM_PlayPause)
+    key = KSYM_space;
+  else if (key == KSYM_Select)
+    key = KSYM_Return;
+#endif
+
+  HandleSpecialGameControllerKeys(key, key_status);
+
   if (game_status == GAME_MODE_PLAYING)
   {
     /* only needed for single-step tape recording mode */
-    static boolean clear_snap_button[MAX_PLAYERS] = { FALSE,FALSE,FALSE,FALSE };
-    static boolean clear_drop_button[MAX_PLAYERS] = { FALSE,FALSE,FALSE,FALSE };
-    static boolean element_snapped[MAX_PLAYERS] = { FALSE,FALSE,FALSE,FALSE };
-    static boolean element_dropped[MAX_PLAYERS] = { FALSE,FALSE,FALSE,FALSE };
+    static boolean has_snapped[MAX_PLAYERS] = { FALSE, FALSE, FALSE, FALSE };
     int pnr;
 
     for (pnr = 0; pnr < MAX_PLAYERS; pnr++)
@@ -802,105 +1823,52 @@ void HandleKey(Key key, int key_status)
            key_action |= key_info[i].action | JOY_BUTTON_SNAP;
       }
 
-      /* clear delayed snap and drop actions in single step mode (see below) */
-      if (tape.single_step)
-      {
-       if (clear_snap_button[pnr])
-       {
-         stored_player[pnr].action &= ~KEY_BUTTON_SNAP;
-         clear_snap_button[pnr] = FALSE;
-       }
-
-       if (clear_drop_button[pnr])
-       {
-         stored_player[pnr].action &= ~KEY_BUTTON_DROP;
-         clear_drop_button[pnr] = FALSE;
-       }
-      }
-
       if (key_status == KEY_PRESSED)
        stored_player[pnr].action |= key_action;
       else
        stored_player[pnr].action &= ~key_action;
 
-      if (tape.single_step && tape.recording && tape.pausing)
+      if (tape.single_step && tape.recording && tape.pausing && !tape.use_mouse)
       {
        if (key_status == KEY_PRESSED && key_action & KEY_MOTION)
        {
          TapeTogglePause(TAPE_TOGGLE_AUTOMATIC);
 
-         /* if snap key already pressed, don't snap when releasing (below) */
+         /* if snap key already pressed, keep pause mode when releasing */
          if (stored_player[pnr].action & KEY_BUTTON_SNAP)
-           element_snapped[pnr] = TRUE;
-
-         /* if drop key already pressed, don't drop when releasing (below) */
-         if (stored_player[pnr].action & KEY_BUTTON_DROP)
-           element_dropped[pnr] = TRUE;
+           has_snapped[pnr] = TRUE;
        }
-#if 1
        else if (key_status == KEY_PRESSED && key_action & KEY_BUTTON_DROP)
        {
-         if (level.game_engine_type == GAME_ENGINE_TYPE_EM ||
-             level.game_engine_type == GAME_ENGINE_TYPE_SP)
-         {
-#if 0
-           printf("::: drop key pressed\n");
-#endif
-
-           if (level.game_engine_type == GAME_ENGINE_TYPE_SP &&
-               getRedDiskReleaseFlag_SP() == 0)
-             stored_player[pnr].action &= ~KEY_BUTTON_DROP;
+         TapeTogglePause(TAPE_TOGGLE_AUTOMATIC);
 
-           TapeTogglePause(TAPE_TOGGLE_AUTOMATIC);
+         if (level.game_engine_type == GAME_ENGINE_TYPE_SP &&
+             getRedDiskReleaseFlag_SP() == 0)
+         {
+           /* add a single inactive frame before dropping starts */
+           stored_player[pnr].action &= ~KEY_BUTTON_DROP;
+           stored_player[pnr].force_dropping = TRUE;
          }
        }
-#endif
-       else if (key_status == KEY_RELEASED && key_action & KEY_BUTTON)
+       else if (key_status == KEY_RELEASED && key_action & KEY_BUTTON_SNAP)
        {
-         if (key_action & KEY_BUTTON_SNAP)
-         {
-           /* if snap key was released without moving (see above), snap now */
-           if (!element_snapped[pnr])
-           {
-             TapeTogglePause(TAPE_TOGGLE_AUTOMATIC);
-
-             stored_player[pnr].action |= KEY_BUTTON_SNAP;
-
-             /* clear delayed snap button on next event */
-             clear_snap_button[pnr] = TRUE;
-           }
-
-           element_snapped[pnr] = FALSE;
-         }
-
-#if 1
-         if (key_action & KEY_BUTTON_DROP &&
-             level.game_engine_type == GAME_ENGINE_TYPE_RND)
-         {
-           /* if drop key was released without moving (see above), drop now */
-           if (!element_dropped[pnr])
-           {
-             TapeTogglePause(TAPE_TOGGLE_AUTOMATIC);
-
-             if (level.game_engine_type != GAME_ENGINE_TYPE_SP ||
-                 getRedDiskReleaseFlag_SP() != 0)
-               stored_player[pnr].action |= KEY_BUTTON_DROP;
-
-             /* clear delayed drop button on next event */
-             clear_drop_button[pnr] = TRUE;
-           }
+         /* if snap key was pressed without direction, leave pause mode */
+         if (!has_snapped[pnr])
+           TapeTogglePause(TAPE_TOGGLE_AUTOMATIC);
 
-           element_dropped[pnr] = FALSE;
-         }
-#endif
+         has_snapped[pnr] = FALSE;
        }
       }
-      else if (tape.recording && tape.pausing)
+      else if (tape.recording && tape.pausing && !tape.use_mouse)
       {
        /* prevent key release events from un-pausing a paused game */
        if (key_status == KEY_PRESSED && key_action & KEY_ACTION)
          TapeTogglePause(TAPE_TOGGLE_MANUAL);
       }
+
+      // for MM style levels, handle in-game keyboard input in HandleJoystick()
+      if (level.game_engine_type == GAME_ENGINE_TYPE_MM)
+       joy |= key_action;
     }
   }
   else
@@ -924,14 +1892,53 @@ void HandleKey(Key key, int key_status)
     key_joystick_mapping = 0;
 
   if (key_status == KEY_RELEASED)
+  {
+    // reset flag to ignore repeated "key pressed" events after key release
+    ignore_repeated_key = FALSE;
+
     return;
+  }
 
-  if ((key == KSYM_Return || key == KSYM_KP_Enter) &&
-      (GetKeyModState() & KMOD_Alt) && video.fullscreen_available)
+  if ((key == KSYM_F11 ||
+       ((key == KSYM_Return ||
+        key == KSYM_KP_Enter) && (GetKeyModState() & KMOD_Alt))) &&
+      video.fullscreen_available &&
+      !ignore_repeated_key)
   {
     setup.fullscreen = !setup.fullscreen;
 
-    ToggleFullscreenIfNeeded();
+    ToggleFullscreenOrChangeWindowScalingIfNeeded();
+
+    if (game_status == GAME_MODE_SETUP)
+      RedrawSetupScreenAfterFullscreenToggle();
+
+    // set flag to ignore repeated "key pressed" events
+    ignore_repeated_key = TRUE;
+
+    return;
+  }
+
+  if ((key == KSYM_0     || key == KSYM_KP_0 ||
+       key == KSYM_minus || key == KSYM_KP_Subtract ||
+       key == KSYM_plus  || key == KSYM_KP_Add ||
+       key == KSYM_equal) &&   // ("Shift-=" is "+" on US keyboards)
+      (GetKeyModState() & (KMOD_Control | KMOD_Meta)) &&
+      video.window_scaling_available &&
+      !video.fullscreen_enabled)
+  {
+    if (key == KSYM_0 || key == KSYM_KP_0)
+      setup.window_scaling_percent = STD_WINDOW_SCALING_PERCENT;
+    else if (key == KSYM_minus || key == KSYM_KP_Subtract)
+      setup.window_scaling_percent -= STEP_WINDOW_SCALING_PERCENT;
+    else
+      setup.window_scaling_percent += STEP_WINDOW_SCALING_PERCENT;
+
+    if (setup.window_scaling_percent < MIN_WINDOW_SCALING_PERCENT)
+      setup.window_scaling_percent = MIN_WINDOW_SCALING_PERCENT;
+    else if (setup.window_scaling_percent > MAX_WINDOW_SCALING_PERCENT)
+      setup.window_scaling_percent = MAX_WINDOW_SCALING_PERCENT;
+
+    ToggleFullscreenOrChangeWindowScalingIfNeeded();
 
     if (game_status == GAME_MODE_SETUP)
       RedrawSetupScreenAfterFullscreenToggle();
@@ -939,13 +1946,17 @@ void HandleKey(Key key, int key_status)
     return;
   }
 
-#if 0
-  if (game_status == GAME_MODE_PLAYING && local_player->LevelSolved_GameEnd &&
-      (key == KSYM_Return || key == setup.shortcut.toggle_pause))
-#else
+  if (HandleGlobalAnimClicks(-1, -1, (key == KSYM_space ||
+                                     key == KSYM_Return ||
+                                     key == KSYM_Escape)))
+  {
+    /* do not handle this key event anymore */
+    if (key != KSYM_Escape)    /* always allow ESC key to be handled */
+      return;
+  }
+
   if (game_status == GAME_MODE_PLAYING && AllPlayersGone &&
       (key == KSYM_Return || key == setup.shortcut.toggle_pause))
-#endif
   {
     GameEnd();
 
@@ -967,7 +1978,7 @@ void HandleKey(Key key, int key_status)
     else if (key == setup.shortcut.load_game)
       TapeQuickLoad();
     else if (key == setup.shortcut.toggle_pause)
-      TapeTogglePause(TAPE_TOGGLE_MANUAL);
+      TapeTogglePause(TAPE_TOGGLE_MANUAL | TAPE_TOGGLE_PLAY_PAUSE);
 
     HandleTapeButtonKeys(key);
     HandleSoundButtonKeys(key);
@@ -1082,18 +2093,6 @@ void HandleKey(Key key, int key_status)
            HandleHallOfFame(0, 0, 0, +1 * SCROLL_PAGE, MB_MENU_MARK);
          break;
 
-#ifdef DEBUG
-       case KSYM_0:
-         GameFrameDelay = (GameFrameDelay == 500 ? GAME_FRAME_DELAY : 500);
-         break;
-
-       case KSYM_b:
-         setup.sp_show_border_elements = !setup.sp_show_border_elements;
-         printf("Supaplex border elements %s\n",
-                setup.sp_show_border_elements ? "enabled" : "disabled");
-         break;
-#endif
-
        default:
          break;
       }
@@ -1112,91 +2111,6 @@ void HandleKey(Key key, int key_status)
          RequestQuitGame(setup.ask_on_escape);
          break;
 
-#ifdef DEBUG
-       case KSYM_0:
-#if 0
-       case KSYM_1:
-       case KSYM_2:
-       case KSYM_3:
-       case KSYM_4:
-       case KSYM_5:
-       case KSYM_6:
-       case KSYM_7:
-       case KSYM_8:
-       case KSYM_9:
-#endif
-         if (key == KSYM_0)
-         {
-           if (GameFrameDelay == 500)
-             GameFrameDelay = GAME_FRAME_DELAY;
-           else
-             GameFrameDelay = 500;
-         }
-         else
-           GameFrameDelay = (key - KSYM_0) * 10;
-         printf("Game speed == %d%% (%d ms delay between two frames)\n",
-                GAME_FRAME_DELAY * 100 / GameFrameDelay, GameFrameDelay);
-         break;
-
-       case KSYM_d:
-         if (options.debug)
-         {
-           options.debug = FALSE;
-           printf("debug mode disabled\n");
-         }
-         else
-         {
-           options.debug = TRUE;
-           printf("debug mode enabled\n");
-         }
-         break;
-
-       case KSYM_s:
-         if (!global.fps_slowdown)
-         {
-           global.fps_slowdown = TRUE;
-           global.fps_slowdown_factor = 2;
-           printf("fps slowdown enabled -- display only every 2nd frame\n");
-         }
-         else if (global.fps_slowdown_factor == 2)
-         {
-           global.fps_slowdown_factor = 4;
-           printf("fps slowdown enabled -- display only every 4th frame\n");
-         }
-         else
-         {
-           global.fps_slowdown = FALSE;
-           global.fps_slowdown_factor = 1;
-           printf("fps slowdown disabled\n");
-         }
-         break;
-
-       case KSYM_f:
-         ScrollStepSize = TILEX / 8;
-         printf("ScrollStepSize == %d (1/8)\n", ScrollStepSize);
-         break;
-
-       case KSYM_g:
-         ScrollStepSize = TILEX / 4;
-         printf("ScrollStepSize == %d (1/4)\n", ScrollStepSize);
-         break;
-
-       case KSYM_h:
-         ScrollStepSize = TILEX / 2;
-         printf("ScrollStepSize == %d (1/2)\n", ScrollStepSize);
-         break;
-
-       case KSYM_l:
-         ScrollStepSize = TILEX;
-         printf("ScrollStepSize == %d (1/1)\n", ScrollStepSize);
-         break;
-
-       case KSYM_v:
-         printf("::: currently using game engine version %d\n",
-                game.engine_version);
-         break;
-#endif
-
        default:
          break;
       }
@@ -1206,21 +2120,41 @@ void HandleKey(Key key, int key_status)
     default:
       if (key == KSYM_Escape)
       {
-       game_status = GAME_MODE_MAIN;
+       SetGameStatus(GAME_MODE_MAIN);
+
        DrawMainMenu();
 
        return;
       }
   }
+
+  HandleKeysDebug(key);
 }
 
 void HandleNoEvent()
 {
-  if (button_status && game_status != GAME_MODE_PLAYING)
+  HandleMouseCursor();
+
+  switch (game_status)
   {
-    HandleButton(0, 0, -button_status, button_status);
+    case GAME_MODE_PLAYING:
+      HandleButtonOrFinger(-1, -1, -1);
+      break;
+  }
+}
 
-    return;
+void HandleEventActions()
+{
+  // if (button_status && game_status != GAME_MODE_PLAYING)
+  if (button_status && (game_status != GAME_MODE_PLAYING ||
+                       tape.pausing ||
+                       level.game_engine_type == GAME_ENGINE_TYPE_MM))
+  {
+    HandleButton(0, 0, button_status, -button_status);
+  }
+  else
+  {
+    HandleJoystick();
   }
 
 #if defined(NETWORK_AVALIABLE)
@@ -1228,30 +2162,80 @@ void HandleNoEvent()
     HandleNetworking();
 #endif
 
-  HandleJoystick();
+  switch (game_status)
+  {
+    case GAME_MODE_MAIN:
+      DrawPreviewLevelAnimation();
+      break;
+
+    case GAME_MODE_EDITOR:
+      HandleLevelEditorIdle();
+      break;
+
+    default:
+      break;
+  }
+}
+
+static void HandleTileCursor(int dx, int dy, int button)
+{
+  if (!dx || !button)
+    ClearPlayerMouseAction();
+
+  if (!dx && !dy)
+    return;
+
+  if (button)
+  {
+    SetPlayerMouseAction(tile_cursor.x, tile_cursor.y,
+                        (dx < 0 ? MB_LEFTBUTTON :
+                         dx > 0 ? MB_RIGHTBUTTON : MB_RELEASED));
+  }
+  else if (!tile_cursor.moving)
+  {
+    int old_xpos = tile_cursor.xpos;
+    int old_ypos = tile_cursor.ypos;
+    int new_xpos = old_xpos;
+    int new_ypos = old_ypos;
+
+    if (IN_LEV_FIELD(old_xpos + dx, old_ypos))
+      new_xpos = old_xpos + dx;
+
+    if (IN_LEV_FIELD(old_xpos, old_ypos + dy))
+      new_ypos = old_ypos + dy;
+
+    SetTileCursorTargetXY(new_xpos, new_ypos);
+  }
 }
 
 static int HandleJoystickForAllPlayers()
 {
   int i;
   int result = 0;
+  boolean no_joysticks_configured = TRUE;
+  boolean use_as_joystick_nr = (game_status != GAME_MODE_PLAYING);
+  static byte joy_action_last[MAX_PLAYERS];
+
+  for (i = 0; i < MAX_PLAYERS; i++)
+    if (setup.input[i].use_joystick)
+      no_joysticks_configured = FALSE;
+
+  /* if no joysticks configured, map connected joysticks to players */
+  if (no_joysticks_configured)
+    use_as_joystick_nr = TRUE;
 
   for (i = 0; i < MAX_PLAYERS; i++)
   {
     byte joy_action = 0;
 
-    /*
-    if (!setup.input[i].use_joystick)
-      continue;
-      */
-
-    joy_action = Joystick(i);
+    joy_action = JoystickExt(i, use_as_joystick_nr);
     result |= joy_action;
 
-    if (!setup.input[i].use_joystick)
-      continue;
+    if ((setup.input[i].use_joystick || no_joysticks_configured) &&
+       joy_action != joy_action_last[i])
+      stored_player[i].action = joy_action;
 
-    stored_player[i].action = joy_action;
+    joy_action_last[i] = joy_action;
   }
 
   return result;
@@ -1259,9 +2243,15 @@ static int HandleJoystickForAllPlayers()
 
 void HandleJoystick()
 {
+  static unsigned int joytest_delay = 0;
+  static unsigned int joytest_delay_value = GADGET_FRAME_DELAY;
+  static int joytest_last = 0;
+  int delay_value_first = GADGET_FRAME_DELAY_FIRST;
+  int delay_value       = GADGET_FRAME_DELAY;
   int joystick = HandleJoystickForAllPlayers();
   int keyboard = key_joystick_mapping;
   int joy      = (joystick | keyboard);
+  int joytest   = joystick;
   int left     = joy & JOY_LEFT;
   int right    = joy & JOY_RIGHT;
   int up       = joy & JOY_UP;
@@ -1270,6 +2260,46 @@ void HandleJoystick()
   int newbutton        = (AnyJoystickButton() == JOY_BUTTON_NEW_PRESSED);
   int dx       = (left ? -1    : right ? 1     : 0);
   int dy       = (up   ? -1    : down  ? 1     : 0);
+  boolean use_delay_value_first = (joytest != joytest_last);
+
+  if (HandleGlobalAnimClicks(-1, -1, newbutton))
+  {
+    /* do not handle this button event anymore */
+    return;
+  }
+
+  if (level.game_engine_type == GAME_ENGINE_TYPE_MM)
+  {
+    if (game_status == GAME_MODE_PLAYING)
+    {
+      // when playing MM style levels, also use delay for keyboard events
+      joytest |= keyboard;
+
+      // only use first delay value for new events, but not for changed events
+      use_delay_value_first = (!joytest != !joytest_last);
+
+      // only use delay after the initial keyboard event
+      delay_value = 0;
+    }
+
+    // for any joystick or keyboard event, enable playfield tile cursor
+    if (dx || dy || button)
+      SetTileCursorEnabled(TRUE);
+  }
+
+  if (joytest && !button && !DelayReached(&joytest_delay, joytest_delay_value))
+  {
+    /* delay joystick/keyboard actions if axes/keys continually pressed */
+    newbutton = dx = dy = 0;
+  }
+  else
+  {
+    /* first start with longer delay, then continue with shorter delay */
+    joytest_delay_value =
+      (use_delay_value_first ? delay_value_first : delay_value);
+  }
+
+  joytest_last = joytest;
 
   switch (game_status)
   {
@@ -1279,13 +2309,8 @@ void HandleJoystick()
     case GAME_MODE_LEVELNR:
     case GAME_MODE_SETUP:
     case GAME_MODE_INFO:
+    case GAME_MODE_SCORES:
     {
-      static unsigned int joystickmove_delay = 0;
-
-      if (joystick && !button &&
-         !DelayReached(&joystickmove_delay, GADGET_FRAME_DELAY))
-       newbutton = dx = dy = 0;
-
       if (game_status == GAME_MODE_TITLE)
        HandleTitleScreen(0,0,dx,dy, newbutton ? MB_MENU_CHOICE : MB_MENU_MARK);
       else if (game_status == GAME_MODE_MAIN)
@@ -1298,35 +2323,97 @@ void HandleJoystick()
        HandleSetupScreen(0,0,dx,dy, newbutton ? MB_MENU_CHOICE : MB_MENU_MARK);
       else if (game_status == GAME_MODE_INFO)
        HandleInfoScreen(0,0,dx,dy, newbutton ? MB_MENU_CHOICE : MB_MENU_MARK);
-      break;
-    }
-
-    case GAME_MODE_SCORES:
-      HandleHallOfFame(0, 0, dx, dy, !newbutton);
-      break;
+      else if (game_status == GAME_MODE_SCORES)
+       HandleHallOfFame(0,0,dx,dy, newbutton ? MB_MENU_CHOICE : MB_MENU_MARK);
 
-    case GAME_MODE_EDITOR:
-      HandleLevelEditorIdle();
       break;
+    }
 
     case GAME_MODE_PLAYING:
+#if 0
+      // !!! causes immediate GameEnd() when solving MM level with keyboard !!!
       if (tape.playing || keyboard)
        newbutton = ((joy & JOY_BUTTON) != 0);
-
-#if 0
-      if (local_player->LevelSolved_GameEnd && newbutton)
-#else
-      if (AllPlayersGone && newbutton)
 #endif
+
+      if (newbutton && AllPlayersGone)
       {
        GameEnd();
 
        return;
       }
 
+      if (tape.single_step && tape.recording && tape.pausing && !tape.use_mouse)
+      {
+       if (joystick & JOY_ACTION)
+         TapeTogglePause(TAPE_TOGGLE_AUTOMATIC);
+      }
+      else if (tape.recording && tape.pausing && !tape.use_mouse)
+      {
+       if (joystick & JOY_ACTION)
+         TapeTogglePause(TAPE_TOGGLE_MANUAL);
+      }
+
+      if (level.game_engine_type == GAME_ENGINE_TYPE_MM)
+       HandleTileCursor(dx, dy, button);
+
       break;
 
     default:
       break;
   }
 }
+
+void HandleSpecialGameControllerButtons(Event *event)
+{
+#if defined(TARGET_SDL2)
+  switch (event->type)
+  {
+    case SDL_CONTROLLERBUTTONDOWN:
+      if (event->cbutton.button == SDL_CONTROLLER_BUTTON_START)
+       HandleKey(KSYM_space, KEY_PRESSED);
+      else if (event->cbutton.button == SDL_CONTROLLER_BUTTON_BACK)
+       HandleKey(KSYM_Escape, KEY_PRESSED);
+
+      break;
+
+    case SDL_CONTROLLERBUTTONUP:
+      if (event->cbutton.button == SDL_CONTROLLER_BUTTON_START)
+       HandleKey(KSYM_space, KEY_RELEASED);
+      else if (event->cbutton.button == SDL_CONTROLLER_BUTTON_BACK)
+       HandleKey(KSYM_Escape, KEY_RELEASED);
+
+      break;
+  }
+#endif
+}
+
+void HandleSpecialGameControllerKeys(Key key, int key_status)
+{
+#if defined(TARGET_SDL2)
+#if defined(KSYM_Rewind) && defined(KSYM_FastForward)
+  int button = SDL_CONTROLLER_BUTTON_INVALID;
+
+  /* map keys to joystick buttons (special hack for Amazon Fire TV remote) */
+  if (key == KSYM_Rewind)
+    button = SDL_CONTROLLER_BUTTON_A;
+  else if (key == KSYM_FastForward || key == KSYM_Menu)
+    button = SDL_CONTROLLER_BUTTON_B;
+
+  if (button != SDL_CONTROLLER_BUTTON_INVALID)
+  {
+    Event event;
+
+    event.type = (key_status == KEY_PRESSED ? SDL_CONTROLLERBUTTONDOWN :
+                 SDL_CONTROLLERBUTTONUP);
+
+    event.cbutton.which = 0;   /* first joystick (Amazon Fire TV remote) */
+    event.cbutton.button = button;
+    event.cbutton.state = (key_status == KEY_PRESSED ? SDL_PRESSED :
+                          SDL_RELEASED);
+
+    HandleJoystickEvent(&event);
+  }
+#endif
+#endif
+}