changed comments from old to new style (one-line comments only)
[rocksndiamonds.git] / src / events.c
index c8f7248fb5a6664d22f5453eb5d9ffa8603297cc..5a0d57757190049faa3f55349d201346c48ce4cf 100644 (file)
@@ -40,7 +40,7 @@ static unsigned int special_cursor_delay = 0;
 static unsigned int special_cursor_delay_value = 1000;
 
 
-/* forward declarations for internal use */
+// forward declarations for internal use
 static void HandleNoEvent(void);
 static void HandleEventActions(void);
 
@@ -57,7 +57,7 @@ static int FilterEvents(const Event *event)
   MotionEvent *motion;
 
 #if defined(TARGET_SDL2)
-  /* skip repeated key press events if keyboard auto-repeat is disabled */
+  // skip repeated key press events if keyboard auto-repeat is disabled
   if (event->type == EVENT_KEYPRESS &&
       event->key.repeat &&
       !keyrepeat_status)
@@ -76,7 +76,7 @@ static int FilterEvents(const Event *event)
     ((MotionEvent *)event)->y -= video.screen_yoffset;
   }
 
-  /* non-motion events are directly passed to event handler functions */
+  // non-motion events are directly passed to event handler functions
   if (event->type != EVENT_MOTIONNOTIFY)
     return 1;
 
@@ -84,7 +84,7 @@ static int FilterEvents(const Event *event)
   cursor_inside_playfield = (motion->x >= SX && motion->x < SX + SXSIZE &&
                             motion->y >= SY && motion->y < SY + SYSIZE);
 
-  /* do no reset mouse cursor before all pending events have been processed */
+  // 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) ||
@@ -98,7 +98,7 @@ static int FilterEvents(const Event *event)
     cursor_mode_last = CURSOR_DEFAULT;
   }
 
-  /* skip mouse motion events without pressed button outside level editor */
+  // skip mouse motion events without pressed button outside level editor
   if (button_status == MB_RELEASED &&
       game_status != GAME_MODE_EDITOR && game_status != GAME_MODE_PLAYING)
     return 0;
@@ -112,11 +112,11 @@ static int FilterEvents(const Event *event)
 
 static boolean SkipPressedMouseMotionEvent(const Event *event)
 {
-  /* nothing to do if the current event is not a mouse motion 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 the game */
+  // only skip motion events with pressed button outside the game
   if (button_status == MB_RELEASED || game_status == GAME_MODE_PLAYING)
     return FALSE;
 
@@ -126,7 +126,7 @@ static boolean SkipPressedMouseMotionEvent(const Event *event)
 
     PeekEvent(&next_event);
 
-    /* if next event is also a mouse motion event, skip the current one */
+    // if next event is also a mouse motion event, skip the current one
     if (next_event.type == EVENT_MOTIONNOTIFY)
       return TRUE;
   }
@@ -164,7 +164,7 @@ boolean NextValidEvent(Event *event)
   return FALSE;
 }
 
-void HandleEvents()
+static void HandleEvents(void)
 {
   Event event;
   unsigned int event_frame_delay = 0;
@@ -262,7 +262,7 @@ void HandleOtherEvents(Event *event)
 
       HandleSpecialGameControllerButtons(event);
 
-      /* FALL THROUGH */
+      // FALL THROUGH
     case SDL_CONTROLLERDEVICEADDED:
     case SDL_CONTROLLERDEVICEREMOVED:
     case SDL_CONTROLLERAXISMOTION:
@@ -283,11 +283,11 @@ void HandleOtherEvents(Event *event)
   }
 }
 
-void HandleMouseCursor()
+static void HandleMouseCursor(void)
 {
   if (game_status == GAME_MODE_TITLE)
   {
-    /* when showing title screens, hide mouse pointer (if not moved) */
+    // when showing title screens, hide mouse pointer (if not moved)
 
     if (gfx.cursor_mode != CURSOR_NONE &&
        DelayReached(&special_cursor_delay, special_cursor_delay_value))
@@ -298,13 +298,14 @@ void HandleMouseCursor()
   else if (game_status == GAME_MODE_PLAYING && (!tape.pausing ||
                                                tape.single_step))
   {
-    /* when playing, display a special mouse pointer inside the playfield */
+    // 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)
+      if (level.game_engine_type != GAME_ENGINE_TYPE_MM ||
+         tile_cursor.enabled)
        SetMouseCursor(CURSOR_PLAYFIELD);
     }
   }
@@ -313,7 +314,7 @@ void HandleMouseCursor()
     SetMouseCursor(CURSOR_DEFAULT);
   }
 
-  /* this is set after all pending events have been processed */
+  // this is set after all pending events have been processed
   cursor_mode_last = gfx.cursor_mode;
 }
 
@@ -326,7 +327,7 @@ void EventLoop(void)
     else
       HandleNoEvent();
 
-    /* execute event related actions after pending events have been processed */
+    // execute event related actions after pending events have been processed
     HandleEventActions();
 
     /* don't use all CPU time when idle; the main loop while playing
@@ -335,10 +336,10 @@ void EventLoop(void)
     if (game_status == GAME_MODE_PLAYING)
       HandleGameActions();
 
-    /* always copy backbuffer to visible screen for every video frame */
+    // always copy backbuffer to visible screen for every video frame
     BackToFront();
 
-    /* reset video frame delay to default (may change again while playing) */
+    // reset video frame delay to default (may change again while playing)
     SetVideoFrameDelay(MenuFrameDelay);
 
     if (game_status == GAME_MODE_QUIT)
@@ -346,7 +347,26 @@ void EventLoop(void)
   }
 }
 
-void ClearEventQueue()
+void ClearAutoRepeatKeyEvents(void)
+{
+#if defined(TARGET_SDL2)
+  while (PendingEvent())
+  {
+    Event next_event;
+
+    PeekEvent(&next_event);
+
+    // if event is repeated key press event, remove it from event queue
+    if (next_event.type == EVENT_KEYPRESS &&
+       next_event.key.repeat)
+      WaitEvent(&next_event);
+    else
+      break;
+  }
+#endif
+}
+
+void ClearEventQueue(void)
 {
   Event event;
 
@@ -376,18 +396,18 @@ void ClearEventQueue()
   }
 }
 
-void ClearPlayerMouseAction()
+static void ClearPlayerMouseAction(void)
 {
   local_player->mouse_action.lx = 0;
   local_player->mouse_action.ly = 0;
   local_player->mouse_action.button = 0;
 }
 
-void ClearPlayerAction()
+void ClearPlayerAction(void)
 {
   int i;
 
-  /* simulate key release events for still pressed keys */
+  // simulate key release events for still pressed keys
   key_joystick_mapping = 0;
   for (i = 0; i < MAX_PLAYERS; i++)
     stored_player[i].action = 0;
@@ -396,10 +416,14 @@ void ClearPlayerAction()
   ClearPlayerMouseAction();
 }
 
-void SetPlayerMouseAction(int mx, int my, int button)
+static 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);
+
+  if (local_player->mouse_action.button_hint)
+    button = local_player->mouse_action.button_hint;
 
   ClearPlayerMouseAction();
 
@@ -412,13 +436,15 @@ void SetPlayerMouseAction(int mx, int my, int button)
 
   if (tape.recording && tape.pausing && tape.use_mouse)
   {
-    /* prevent button release or motion events from un-pausing a paused game */
-    if (button && !motion_status)
-      TapeTogglePause(TAPE_TOGGLE_MANUAL);
+    // 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()
+void SleepWhileUnmapped(void)
 {
   boolean window_unmapped = TRUE;
 
@@ -482,6 +508,9 @@ void HandleButtonEvent(ButtonEvent *event)
        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;
@@ -626,10 +655,30 @@ void HandleWindowEvent(WindowEvent *event)
       if (new_display_width  != video.display_width ||
          new_display_height != video.display_height)
       {
+       int nr = GRID_ACTIVE_NR();      // previous screen orientation
+
        video.display_width  = new_display_width;
        video.display_height = new_display_height;
 
        SDLSetScreenProperties();
+
+       // check if screen orientation has changed (should always be true here)
+       if (nr != GRID_ACTIVE_NR())
+       {
+         int x, y;
+
+         if (game_status == GAME_MODE_SETUP)
+           RedrawSetupScreenAfterScreenRotation(nr);
+
+         nr = GRID_ACTIVE_NR();
+
+         overlay.grid_xsize = setup.touch.grid_xsize[nr];
+         overlay.grid_ysize = setup.touch.grid_ysize[nr];
+
+         for (x = 0; x < MAX_GRID_XSIZE; x++)
+           for (y = 0; y < MAX_GRID_YSIZE; y++)
+             overlay.grid_button[x][y] = setup.touch.grid_button[nr][x][y];
+       }
       }
     }
 #endif
@@ -646,8 +695,21 @@ static struct
   Key key;
 } touch_info[NUM_TOUCH_FINGERS];
 
-void HandleFingerEvent_VirtualButtons(FingerEvent *event)
+static void HandleFingerEvent_VirtualButtons(FingerEvent *event)
 {
+#if 1
+  int x = event->x * overlay.grid_xsize;
+  int y = event->y * overlay.grid_ysize;
+  int grid_button = overlay.grid_button[x][y];
+  int grid_button_action = GET_ACTION_FROM_GRID_BUTTON(grid_button);
+  Key key = (grid_button == CHAR_GRID_BUTTON_LEFT  ? setup.input[0].key.left :
+            grid_button == CHAR_GRID_BUTTON_RIGHT ? setup.input[0].key.right :
+            grid_button == CHAR_GRID_BUTTON_UP    ? setup.input[0].key.up :
+            grid_button == CHAR_GRID_BUTTON_DOWN  ? setup.input[0].key.down :
+            grid_button == CHAR_GRID_BUTTON_SNAP  ? setup.input[0].key.snap :
+            grid_button == CHAR_GRID_BUTTON_DROP  ? setup.input[0].key.drop :
+            KSYM_UNDEFINED);
+#else
   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);
@@ -670,6 +732,7 @@ void HandleFingerEvent_VirtualButtons(FingerEvent *event)
             event_y > 2.0 / 3.0 && event_y < 1 ?
             setup.input[0].key.down :
             KSYM_UNDEFINED);
+#endif
   int key_status = (event->type == EVENT_FINGERRELEASE ? KEY_RELEASED :
                    KEY_PRESSED);
   char *key_status_name = (key_status == KEY_RELEASED ? "KEY_RELEASED" :
@@ -682,6 +745,11 @@ void HandleFingerEvent_VirtualButtons(FingerEvent *event)
   Error(ERR_DEBUG, "::: key '%s' was '%s' [fingerId: %lld]",
        getKeyNameFromKey(key), key_status_name, event->fingerId);
 
+  if (key_status == KEY_PRESSED)
+    overlay.grid_button_action |= grid_button_action;
+  else
+    overlay.grid_button_action &= ~grid_button_action;
+
   // check if we already know this touch event's finger id
   for (i = 0; i < NUM_TOUCH_FINGERS; i++)
   {
@@ -787,7 +855,7 @@ void HandleFingerEvent_VirtualButtons(FingerEvent *event)
   }
 }
 
-void HandleFingerEvent_WipeGestures(FingerEvent *event)
+static void HandleFingerEvent_WipeGestures(FingerEvent *event)
 {
   static Key motion_key_x = KSYM_UNDEFINED;
   static Key motion_key_y = KSYM_UNDEFINED;
@@ -953,7 +1021,16 @@ void HandleFingerEvent(FingerEvent *event)
     return;
 
   if (level.game_engine_type == GAME_ENGINE_TYPE_MM)
+  {
+    if (strEqual(setup.touch.control_type, TOUCH_CONTROL_OFF))
+      local_player->mouse_action.button_hint =
+       (event->type == EVENT_FINGERRELEASE ? MB_NOT_PRESSED :
+        event->x < 0.5                     ? MB_LEFTBUTTON  :
+        event->x > 0.5                     ? MB_RIGHTBUTTON :
+        MB_NOT_PRESSED);
+
     return;
+  }
 
   if (strEqual(setup.touch.control_type, TOUCH_CONTROL_VIRTUAL_BUTTONS))
     HandleFingerEvent_VirtualButtons(event);
@@ -1312,6 +1389,8 @@ static void HandleButtonOrFinger(int mx, int my, int button)
       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_VIRTUAL_BUTTONS))
+      SetPlayerMouseAction(mx, my, button);    // special case
   }
   else
   {
@@ -1322,7 +1401,7 @@ static void HandleButtonOrFinger(int mx, int my, int button)
 
 #if defined(TARGET_SDL2)
 
-static boolean checkTextInputKeyModState()
+static boolean checkTextInputKeyModState(void)
 {
   // when playing, only handle raw key events and ignore text input
   if (game_status == GAME_MODE_PLAYING)
@@ -1496,22 +1575,22 @@ void HandleButton(int mx, int my, int button, int button_nr)
      strEqual(setup.touch.control_type, TOUCH_CONTROL_FOLLOW_FINGER));
 #endif
 
-  if (handle_gadgets && HandleGadgets(mx, my, button))
+  if (HandleGlobalAnimClicks(mx, my, button))
   {
-    /* do not handle this button event anymore */
-    mx = my = -32;     /* force mouse event to be outside screen tiles */
+    // do not handle this button event anymore
+    return;            // force mouse event not to be handled at all
   }
 
-  if (HandleGlobalAnimClicks(mx, my, button))
+  if (handle_gadgets && HandleGadgets(mx, my, button))
   {
-    /* do not handle this button event anymore */
-    return;            /* force mouse event not to be handled at all */
+    // do not handle this button event anymore
+    mx = my = -32;     // force mouse event to be outside screen tiles
   }
 
   if (button_hold && game_status == GAME_MODE_PLAYING && tape.pausing)
     return;
 
-  /* do not use scroll wheel button events for anything other than gadgets */
+  // do not use scroll wheel button events for anything other than gadgets
   if (IS_WHEEL_BUTTON(button_nr))
     return;
 
@@ -1617,6 +1696,11 @@ static void HandleKeysSpecial(Key key)
     {
       InsertSolutionTape();
     }
+    else if (is_string_suffix(cheat_input, ":play-solution-tape") ||
+            is_string_suffix(cheat_input, ":pst"))
+    {
+      PlaySolutionTape();
+    }
     else if (is_string_suffix(cheat_input, ":reload-graphics") ||
             is_string_suffix(cheat_input, ":rg"))
     {
@@ -1661,7 +1745,7 @@ static void HandleKeysSpecial(Key key)
         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 */
+      // remove player input actions for all players but the first one
       for (i = 1; i < MAX_PLAYERS; i++)
        tape.player_participates[i] = FALSE;
 
@@ -1697,6 +1781,15 @@ static void HandleKeysSpecial(Key key)
       DumpBrush_Small();
     }
   }
+
+  // special key shortcuts for all game modes
+  if (is_string_suffix(cheat_input, ":dump-event-actions") ||
+      is_string_suffix(cheat_input, ":dea") ||
+      is_string_suffix(cheat_input, ":DEA"))
+  {
+    DumpGadgetIdentifiers();
+    DumpScreenIdentifiers();
+  }
 }
 
 void HandleKeysDebug(Key key)
@@ -1778,7 +1871,7 @@ void HandleKey(Key key, int key_status)
   int i;
 
 #if defined(TARGET_SDL2)
-  /* map special keys (media keys / remote control buttons) to default keys */
+  // map special keys (media keys / remote control buttons) to default keys
   if (key == KSYM_PlayPause)
     key = KSYM_space;
   else if (key == KSYM_Select)
@@ -1789,7 +1882,7 @@ void HandleKey(Key key, int key_status)
 
   if (game_status == GAME_MODE_PLAYING)
   {
-    /* only needed for single-step tape recording mode */
+    // only needed for single-step tape recording mode
     static boolean has_snapped[MAX_PLAYERS] = { FALSE, FALSE, FALSE, FALSE };
     int pnr;
 
@@ -1806,7 +1899,7 @@ void HandleKey(Key key, int key_status)
        if (key == *key_info[i].key_custom)
          key_action |= key_info[i].action;
 
-      /* use combined snap+direction keys for the first player only */
+      // use combined snap+direction keys for the first player only
       if (pnr == 0)
       {
        ssi = setup.shortcut;
@@ -1821,13 +1914,13 @@ void HandleKey(Key key, int key_status)
       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, keep pause mode when releasing */
+         // if snap key already pressed, keep pause mode when releasing
          if (stored_player[pnr].action & KEY_BUTTON_SNAP)
            has_snapped[pnr] = TRUE;
        }
@@ -1838,14 +1931,14 @@ void HandleKey(Key key, int key_status)
          if (level.game_engine_type == GAME_ENGINE_TYPE_SP &&
              getRedDiskReleaseFlag_SP() == 0)
          {
-           /* add a single inactive frame before dropping starts */
+           // add a single inactive frame before dropping starts
            stored_player[pnr].action &= ~KEY_BUTTON_DROP;
            stored_player[pnr].force_dropping = TRUE;
          }
        }
        else if (key_status == KEY_RELEASED && key_action & KEY_BUTTON_SNAP)
        {
-         /* if snap key was pressed without direction, leave pause mode */
+         // if snap key was pressed without direction, leave pause mode
          if (!has_snapped[pnr])
            TapeTogglePause(TAPE_TOGGLE_AUTOMATIC);
 
@@ -1854,10 +1947,14 @@ void HandleKey(Key key, int key_status)
       }
       else if (tape.recording && tape.pausing && !tape.use_mouse)
       {
-       /* prevent key release events from un-pausing a paused game */
+       // 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
@@ -1939,8 +2036,8 @@ void HandleKey(Key key, int key_status)
                                      key == KSYM_Return ||
                                      key == KSYM_Escape)))
   {
-    /* do not handle this key event anymore */
-    if (key != KSYM_Escape)    /* always allow ESC key to be handled */
+    // do not handle this key event anymore
+    if (key != KSYM_Escape)    // always allow ESC key to be handled
       return;
   }
 
@@ -1955,7 +2052,7 @@ void HandleKey(Key key, int key_status)
   if (game_status == GAME_MODE_MAIN &&
       (key == setup.shortcut.toggle_pause || key == KSYM_space))
   {
-    StartGameActions(options.network, setup.autorecord, level.random_seed);
+    StartGameActions(network.enabled, setup.autorecord, level.random_seed);
 
     return;
   }
@@ -2001,7 +2098,7 @@ void HandleKey(Key key, int key_status)
 
   if (HandleGadgetsKeyInput(key))
   {
-    if (key != KSYM_Escape)    /* always allow ESC key to be handled */
+    if (key != KSYM_Escape)    // always allow ESC key to be handled
       key = KSYM_UNDEFINED;
   }
 
@@ -2018,6 +2115,10 @@ void HandleKey(Key key, int key_status)
     case GAME_MODE_SETUP:
     case GAME_MODE_INFO:
     case GAME_MODE_SCORES:
+
+      if (anyTextGadgetActiveOrJustFinished && key != KSYM_Escape)
+       break;
+
       switch (key)
       {
        case KSYM_space:
@@ -2120,7 +2221,7 @@ void HandleKey(Key key, int key_status)
   HandleKeysDebug(key);
 }
 
-void HandleNoEvent()
+void HandleNoEvent(void)
 {
   HandleMouseCursor();
 
@@ -2132,7 +2233,7 @@ void HandleNoEvent()
   }
 }
 
-void HandleEventActions()
+void HandleEventActions(void)
 {
   // if (button_status && game_status != GAME_MODE_PLAYING)
   if (button_status && (game_status != GAME_MODE_PLAYING ||
@@ -2146,10 +2247,8 @@ void HandleEventActions()
     HandleJoystick();
   }
 
-#if defined(NETWORK_AVALIABLE)
-  if (options.network)
+  if (network.enabled)
     HandleNetworking();
-#endif
 
   switch (game_status)
   {
@@ -2166,7 +2265,38 @@ void HandleEventActions()
   }
 }
 
-static int HandleJoystickForAllPlayers()
+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(void)
 {
   int i;
   int result = 0;
@@ -2178,7 +2308,7 @@ static int HandleJoystickForAllPlayers()
     if (setup.input[i].use_joystick)
       no_joysticks_configured = FALSE;
 
-  /* if no joysticks configured, map connected joysticks to players */
+  // if no joysticks configured, map connected joysticks to players
   if (no_joysticks_configured)
     use_as_joystick_nr = TRUE;
 
@@ -2199,11 +2329,17 @@ static int HandleJoystickForAllPlayers()
   return result;
 }
 
-void HandleJoystick()
+void HandleJoystick(void)
 {
+  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;
@@ -2212,13 +2348,47 @@ 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 */
+    // 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)
   {
     case GAME_MODE_TITLE:
@@ -2227,25 +2397,10 @@ void HandleJoystick()
     case GAME_MODE_LEVELNR:
     case GAME_MODE_SETUP:
     case GAME_MODE_INFO:
+    case GAME_MODE_SCORES:
     {
-      static unsigned int joystickmove_delay = 0;
-      static unsigned int joystickmove_delay_value = GADGET_FRAME_DELAY;
-      static int joystick_last = 0;
-
-      if (joystick && !button &&
-         !DelayReached(&joystickmove_delay, joystickmove_delay_value))
-      {
-       /* delay joystick actions if buttons/axes continually pressed */
-       newbutton = dx = dy = 0;
-      }
-      else
-      {
-       /* start with longer delay, then continue with shorter delay */
-       if (joystick != joystick_last)
-         joystickmove_delay_value = GADGET_FRAME_DELAY_FIRST;
-       else
-         joystickmove_delay_value = GADGET_FRAME_DELAY;
-      }
+      if (anyTextGadgetActive())
+       break;
 
       if (game_status == GAME_MODE_TITLE)
        HandleTitleScreen(0,0,dx,dy, newbutton ? MB_MENU_CHOICE : MB_MENU_MARK);
@@ -2259,19 +2414,18 @@ 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);
-
-      joystick_last = joystick;
+      else if (game_status == GAME_MODE_SCORES)
+       HandleHallOfFame(0,0,dx,dy, newbutton ? MB_MENU_CHOICE : MB_MENU_MARK);
 
       break;
     }
 
-    case GAME_MODE_SCORES:
-      HandleHallOfFame(0, 0, dx, dy, !newbutton);
-      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);
+#endif
 
       if (newbutton && AllPlayersGone)
       {
@@ -2280,12 +2434,20 @@ void HandleJoystick()
        return;
       }
 
-      if (tape.recording && tape.pausing)
+      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:
@@ -2296,24 +2458,38 @@ void HandleJoystick()
 void HandleSpecialGameControllerButtons(Event *event)
 {
 #if defined(TARGET_SDL2)
+  int key_status;
+  Key key;
+
   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);
-
+      key_status = 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);
+      key_status = KEY_RELEASED;
+      break;
+
+    default:
+      return;
+  }
 
+  switch (event->cbutton.button)
+  {
+    case SDL_CONTROLLER_BUTTON_START:
+      key = KSYM_space;
       break;
+
+    case SDL_CONTROLLER_BUTTON_BACK:
+      key = KSYM_Escape;
+      break;
+
+    default:
+      return;
   }
+
+  HandleKey(key, key_status);
 #endif
 }
 
@@ -2323,7 +2499,7 @@ void HandleSpecialGameControllerKeys(Key key, int key_status)
 #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) */
+  // 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)
@@ -2336,7 +2512,7 @@ void HandleSpecialGameControllerKeys(Key key, int key_status)
     event.type = (key_status == KEY_PRESSED ? SDL_CONTROLLERBUTTONDOWN :
                  SDL_CONTROLLERBUTTONUP);
 
-    event.cbutton.which = 0;   /* first joystick (Amazon Fire TV remote) */
+    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);
@@ -2346,3 +2522,18 @@ void HandleSpecialGameControllerKeys(Key key, int key_status)
 #endif
 #endif
 }
+
+boolean DoKeysymAction(int keysym)
+{
+  if (keysym < 0)
+  {
+    Key key = (Key)(-keysym);
+
+    HandleKey(key, KEY_PRESSED);
+    HandleKey(key, KEY_RELEASED);
+
+    return TRUE;
+  }
+
+  return FALSE;
+}