added clearing virtual buttons when clearing player action
[rocksndiamonds.git] / src / events.c
index 05c4f2a019ff7ce1cbf7b3a9981a2184fe7303ab..69efaf508189e9631cb3b7f3b08f608f364922f5 100644 (file)
@@ -4,7 +4,7 @@
 // (c) 1995-2014 by Artsoft Entertainment
 //                         Holger Schemel
 //                 info@artsoft.org
-//                 http://www.artsoft.org/
+//                 https://www.artsoft.org/
 // ----------------------------------------------------------------------------
 // events.c
 // ============================================================================
@@ -39,14 +39,33 @@ static int cursor_mode_last = CURSOR_DEFAULT;
 static unsigned int special_cursor_delay = 0;
 static unsigned int special_cursor_delay_value = 1000;
 
-static boolean virtual_button_pressed = FALSE;
+static boolean stop_processing_events = FALSE;
 
 
 // forward declarations for internal use
+static void ClearTouchInfo(void);
 static void HandleNoEvent(void);
 static void HandleEventActions(void);
 
 
+// event filter to set mouse x/y position (for pointer class global animations)
+// (this is especially required to ensure smooth global animation mouse pointer
+// movement when the screen is updated without handling events; this can happen
+// when drawing door/envelope request animations, for example)
+
+int FilterMouseMotionEvents(void *userdata, Event *event)
+{
+  if (event->type == EVENT_MOTIONNOTIFY)
+  {
+    int mouse_x = ((MotionEvent *)event)->x;
+    int mouse_y = ((MotionEvent *)event)->y;
+
+    UpdateRawMousePosition(mouse_x, mouse_y);
+  }
+
+  return 1;
+}
+
 // event filter especially needed for SDL event filtering due to
 // delay problems with lots of mouse motion events when mouse button
 // not pressed (X11 can handle this with 'PointerMotionHintMask')
@@ -74,9 +93,25 @@ static int FilterEvents(const Event *event)
   {
     ((MotionEvent *)event)->x -= video.screen_xoffset;
     ((MotionEvent *)event)->y -= video.screen_yoffset;
+  }
 
-    gfx.mouse_x = ((MotionEvent *)event)->x;
-    gfx.mouse_y = ((MotionEvent *)event)->y;
+  if (event->type == EVENT_BUTTONPRESS ||
+      event->type == EVENT_BUTTONRELEASE ||
+      event->type == EVENT_MOTIONNOTIFY)
+  {
+    // do not 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);
+
+      DelayReached(&special_cursor_delay, 0);
+
+      cursor_mode_last = CURSOR_DEFAULT;
+    }
   }
 
   // non-motion events are directly passed to event handler functions
@@ -87,19 +122,14 @@ 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
-  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);
-
-    DelayReached(&special_cursor_delay, 0);
-
-    cursor_mode_last = CURSOR_DEFAULT;
-  }
+  // set correct mouse x/y position (for pointer class global animations)
+  // (this is required in rare cases where the mouse x/y position calculated
+  // from raw values (to apply logical screen size scaling corrections) does
+  // not match the final mouse event x/y position -- this may happen because
+  // the SDL renderer's viewport position is internally represented as float,
+  // but only accessible as integer, which may lead to rounding errors)
+  gfx.mouse_x = motion->x;
+  gfx.mouse_y = motion->y;
 
   // skip mouse motion events without pressed button outside level editor
   if (button_status == MB_RELEASED &&
@@ -167,6 +197,11 @@ boolean NextValidEvent(Event *event)
   return FALSE;
 }
 
+void StopProcessingEvents(void)
+{
+  stop_processing_events = TRUE;
+}
+
 static void HandleEvents(void)
 {
   Event event;
@@ -175,6 +210,8 @@ static void HandleEvents(void)
 
   ResetDelayCounter(&event_frame_delay);
 
+  stop_processing_events = FALSE;
+
   while (NextValidEvent(&event))
   {
     switch (event.type)
@@ -218,6 +255,10 @@ static void HandleEvents(void)
        HandleKeyEvent((KeyEvent *) &event);
        break;
 
+      case EVENT_USER:
+       HandleUserEvent((UserEvent *) &event);
+       break;
+
       default:
        HandleOtherEvents(&event);
        break;
@@ -226,6 +267,10 @@ static void HandleEvents(void)
     // do not handle events for longer than standard frame delay period
     if (DelayReached(&event_frame_delay, event_frame_delay_value))
       break;
+
+    // do not handle any further events if triggered by a special flag
+    if (stop_processing_events)
+      break;
   }
 }
 
@@ -283,6 +328,10 @@ static void HandleMouseCursor(void)
   {
     // when playing, display a special mouse pointer inside the playfield
 
+    // display normal pointer if mouse pressed
+    if (button_status != MB_RELEASED)
+      DelayReached(&special_cursor_delay, 0);
+
     if (gfx.cursor_mode != CURSOR_PLAYFIELD &&
        cursor_inside_playfield &&
        DelayReached(&special_cursor_delay, special_cursor_delay_value))
@@ -394,6 +443,10 @@ void ClearPlayerAction(void)
     stored_player[i].snap_action = 0;
   }
 
+  // simulate finger release events for still pressed virtual buttons
+  overlay.grid_button_action = JOY_NO_ACTION;
+
+  ClearTouchInfo();
   ClearJoystickState();
   ClearPlayerMouseAction();
 }
@@ -416,7 +469,7 @@ static void SetPlayerMouseAction(int mx, int my, int button)
   local_player->mouse_action.ly = ly;
   local_player->mouse_action.button = button;
 
-  if (tape.recording && tape.pausing && tape.use_mouse)
+  if (tape.recording && tape.pausing && tape.use_mouse_actions)
   {
     // un-pause a paused game only if mouse button was newly pressed down
     if (new_button)
@@ -426,10 +479,36 @@ static void SetPlayerMouseAction(int mx, int my, int button)
   SetTileCursorXY(lx, ly);
 }
 
+static Key GetKeyFromGridButton(int grid_button)
+{
+  return (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);
+}
+
+#if defined(PLATFORM_ANDROID)
+static boolean CheckVirtualButtonPressed(int mx, int my, int button)
+{
+  float touch_x = (float)(mx + video.screen_xoffset) / video.screen_width;
+  float touch_y = (float)(my + video.screen_yoffset) / video.screen_height;
+  int x = touch_x * overlay.grid_xsize;
+  int y = touch_y * overlay.grid_ysize;
+  int grid_button = overlay.grid_button[x][y];
+  Key key = GetKeyFromGridButton(grid_button);
+  int key_status = (button == MB_RELEASED ? KEY_RELEASED : KEY_PRESSED);
+
+  return (key_status == KEY_PRESSED && key != KSYM_UNDEFINED);
+}
+#endif
+
 void HandleButtonEvent(ButtonEvent *event)
 {
 #if DEBUG_EVENTS_BUTTON
-  Error(ERR_DEBUG, "BUTTON EVENT: button %d %s, x/y %d/%d\n",
+  Debug("event:button", "button %d %s, x/y %d/%d\n",
        event->button,
        event->type == EVENT_BUTTONPRESS ? "pressed" : "released",
        event->x, event->y);
@@ -461,7 +540,7 @@ void HandleMotionEvent(MotionEvent *event)
   motion_status = TRUE;
 
 #if DEBUG_EVENTS_MOTION
-  Error(ERR_DEBUG, "MOTION EVENT: button %d moved, x/y %d/%d\n",
+  Debug("event:motion", "button %d moved, x/y %d/%d\n",
        button_status, event->x, event->y);
 #endif
 
@@ -474,11 +553,11 @@ void HandleWheelEvent(WheelEvent *event)
 
 #if DEBUG_EVENTS_WHEEL
 #if 1
-  Error(ERR_DEBUG, "WHEEL EVENT: mouse == %d, x/y == %d/%d\n",
+  Debug("event:wheel", "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",
+  Debug("event:wheel", "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"));
@@ -529,7 +608,7 @@ void HandleWindowEvent(WindowEvent *event)
      subtype == SDL_WINDOWEVENT_CLOSE ? "SDL_WINDOWEVENT_CLOSE" :
      "(UNKNOWN)");
 
-  Error(ERR_DEBUG, "WINDOW EVENT: '%s', %ld, %ld",
+  Debug("event:window", "name: '%s', data1: %ld, data2: %ld",
        event_name, event->data1, event->data2);
 #endif
 
@@ -567,6 +646,8 @@ void HandleWindowEvent(WindowEvent *event)
        if (game_status == GAME_MODE_SETUP)
          RedrawSetupScreenAfterFullscreenToggle();
 
+       UpdateMousePosition();
+
        SetWindowTitle();
       }
     }
@@ -586,6 +667,7 @@ void HandleWindowEvent(WindowEvent *event)
        video.display_height = new_display_height;
 
        SDLSetScreenProperties();
+       SetGadgetsPosition_OverlayTouchButtons();
 
        // check if screen orientation has changed (should always be true here)
        if (nr != GRID_ACTIVE_NR())
@@ -621,56 +703,41 @@ static struct
   byte action;
 } touch_info[NUM_TOUCH_FINGERS];
 
+static void SetTouchInfo(int pos, SDL_FingerID finger_id, int counter,
+                        Key key, byte action)
+{
+  touch_info[pos].touched = (action != JOY_NO_ACTION);
+  touch_info[pos].finger_id = finger_id;
+  touch_info[pos].counter = counter;
+  touch_info[pos].key = key;
+  touch_info[pos].action = action;
+}
+
+static void ClearTouchInfo(void)
+{
+  int i;
+
+  for (i = 0; i < NUM_TOUCH_FINGERS; i++)
+    SetTouchInfo(i, 0, 0, 0, JOY_NO_ACTION);
+}
+
 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);
-  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);
-#endif
+  Key key = GetKeyFromGridButton(grid_button);
   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;
 
-  virtual_button_pressed = (key_status == KEY_PRESSED && key != KSYM_UNDEFINED);
-
   // for any touch input event, enable overlay buttons (if activated)
   SetOverlayEnabled(TRUE);
 
-  Error(ERR_DEBUG, "::: key '%s' was '%s' [fingerId: %lld]",
+  Debug("event:finger", "key '%s' was '%s' [fingerId: %lld]",
        getKeyNameFromKey(key), key_status_name, event->fingerId);
 
   if (key_status == KEY_PRESSED)
@@ -684,7 +751,7 @@ static void HandleFingerEvent_VirtualButtons(FingerEvent *event)
     if (touch_info[i].touched &&
        touch_info[i].finger_id == event->fingerId)
     {
-      // Error(ERR_DEBUG, "MARK 1: %d", i);
+      // Debug("event:finger", "MARK 1: %d", i);
 
       break;
     }
@@ -704,12 +771,12 @@ static void HandleFingerEvent_VirtualButtons(FingerEvent *event)
          oldest_pos = i;
          oldest_counter = touch_info[i].counter;
 
-         // Error(ERR_DEBUG, "MARK 2: %d", i);
+         // Debug("event:finger", "MARK 2: %d", i);
        }
 
        if (!touch_info[i].touched)
        {
-         // Error(ERR_DEBUG, "MARK 3: %d", i);
+         // Debug("event:finger", "MARK 3: %d", i);
 
          break;
        }
@@ -720,7 +787,7 @@ static void HandleFingerEvent_VirtualButtons(FingerEvent *event)
        // all slots allocated -- use oldest slot
        i = oldest_pos;
 
-       // Error(ERR_DEBUG, "MARK 4: %d", i);
+       // Debug("event:finger", "MARK 4: %d", i);
       }
     }
     else
@@ -731,7 +798,7 @@ static void HandleFingerEvent_VirtualButtons(FingerEvent *event)
       {
        HandleKey(key, KEY_RELEASED);
 
-       Error(ERR_DEBUG, "=> key == '%s', key_status == '%s' [slot %d] [1]",
+       Debug("event:finger", "key == '%s', key_status == '%s' [slot %d] [1]",
              getKeyNameFromKey(key), "KEY_RELEASED", i);
       }
     }
@@ -750,7 +817,7 @@ static void HandleFingerEvent_VirtualButtons(FingerEvent *event)
          // undraw previous grid button when moving finger away
          overlay.grid_button_action &= ~touch_info[i].action;
 
-         Error(ERR_DEBUG, "=> key == '%s', key_status == '%s' [slot %d] [2]",
+         Debug("event:finger", "key == '%s', key_status == '%s' [slot %d] [2]",
                getKeyNameFromKey(touch_info[i].key), "KEY_RELEASED", i);
        }
 
@@ -758,16 +825,12 @@ static void HandleFingerEvent_VirtualButtons(FingerEvent *event)
        {
          HandleKey(key, KEY_PRESSED);
 
-         Error(ERR_DEBUG, "=> key == '%s', key_status == '%s' [slot %d] [3]",
+         Debug("event:finger", "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;
-      touch_info[i].action = grid_button_action;
+      SetTouchInfo(i, event->fingerId, Counter(), key, grid_button_action);
     }
     else
     {
@@ -775,15 +838,11 @@ static void HandleFingerEvent_VirtualButtons(FingerEvent *event)
       {
        HandleKey(touch_info[i].key, KEY_RELEASED);
 
-       Error(ERR_DEBUG, "=> key == '%s', key_status == '%s' [slot %d] [4]",
+       Debug("event:finger", "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;
-      touch_info[i].action = JOY_NO_ACTION;
+      SetTouchInfo(i, 0, 0, 0, JOY_NO_ACTION);
     }
   }
 }
@@ -818,7 +877,7 @@ static void HandleFingerEvent_WipeGestures(FingerEvent *event)
       motion_key_x = KSYM_UNDEFINED;
       motion_key_y = KSYM_UNDEFINED;
 
-      Error(ERR_DEBUG, "---------- MOVE STARTED (WAIT) ----------");
+      Debug("event:finger", "---------- MOVE STARTED (WAIT) ----------");
     }
     else
     {
@@ -833,7 +892,7 @@ static void HandleFingerEvent_WipeGestures(FingerEvent *event)
 
       HandleKey(button_key, KEY_PRESSED);
 
-      Error(ERR_DEBUG, "---------- SNAP STARTED ----------");
+      Debug("event:finger", "---------- SNAP STARTED ----------");
     }
   }
   else if (event->type == EVENT_FINGERRELEASE)
@@ -850,7 +909,7 @@ static void HandleFingerEvent_WipeGestures(FingerEvent *event)
       motion_key_x = KSYM_UNDEFINED;
       motion_key_y = KSYM_UNDEFINED;
 
-      Error(ERR_DEBUG, "---------- MOVE STOPPED ----------");
+      Debug("event:finger", "---------- MOVE STOPPED ----------");
     }
     else if (event->fingerId == button_id)
     {
@@ -861,7 +920,7 @@ static void HandleFingerEvent_WipeGestures(FingerEvent *event)
 
       button_key = KSYM_UNDEFINED;
 
-      Error(ERR_DEBUG, "---------- SNAP STOPPED ----------");
+      Debug("event:finger", "---------- SNAP STOPPED ----------");
     }
   }
   else if (event->type == EVENT_FINGERMOTION)
@@ -910,7 +969,7 @@ static void HandleFingerEvent_WipeGestures(FingerEvent *event)
        motion_key_x = new_motion_key_x;
        motion_key_y = new_motion_key_y;
 
-       Error(ERR_DEBUG, "---------- MOVE STARTED (MOVE) ----------");
+       Debug("event:finger", "---------- MOVE STARTED (MOVE) ----------");
       }
     }
     else if (event->fingerId == button_id)
@@ -931,7 +990,7 @@ static void HandleFingerEvent_WipeGestures(FingerEvent *event)
 
        HandleKey(button_key, KEY_PRESSED);
 
-       Error(ERR_DEBUG, "---------- DROP STARTED ----------");
+       Debug("event:finger", "---------- DROP STARTED ----------");
       }
     }
   }
@@ -940,7 +999,7 @@ static void HandleFingerEvent_WipeGestures(FingerEvent *event)
 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",
+  Debug("event:finger", "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,
@@ -1007,7 +1066,7 @@ static void HandleButtonOrFinger_WipeGestures_MM(int mx, int my, int button)
 
       ClearPlayerMouseAction();
 
-      Error(ERR_DEBUG, "---------- TOUCH ACTION STARTED ----------");
+      Debug("event:finger", "---------- TOUCH ACTION STARTED ----------");
     }
   }
   else if (button == MB_RELEASED && touched)
@@ -1021,7 +1080,7 @@ static void HandleButtonOrFinger_WipeGestures_MM(int mx, int my, int button)
     else
       SetPlayerMouseAction(old_mx, old_my, MB_RELEASED);
 
-    Error(ERR_DEBUG, "---------- TOUCH ACTION STOPPED ----------");
+    Debug("event:finger", "---------- TOUCH ACTION STOPPED ----------");
   }
 
   if (touched)
@@ -1046,7 +1105,7 @@ static void HandleButtonOrFinger_WipeGestures_MM(int mx, int my, int button)
 
       last_button = button_nr;
 
-      Error(ERR_DEBUG, "---------- TOUCH ACTION: ROTATING ----------");
+      Debug("event:finger", "---------- TOUCH ACTION: ROTATING ----------");
     }
     else
     {
@@ -1054,7 +1113,7 @@ static void HandleButtonOrFinger_WipeGestures_MM(int mx, int my, int button)
 
       SetPlayerMouseAction(old_mx, old_my, MB_RELEASED);
 
-      Error(ERR_DEBUG, "---------- TOUCH ACTION PAUSED ----------");
+      Debug("event:finger", "---------- TOUCH ACTION PAUSED ----------");
     }
   }
 }
@@ -1093,7 +1152,7 @@ static void HandleButtonOrFinger_FollowFinger_MM(int mx, int my, int button)
 
       ClearPlayerMouseAction();
 
-      Error(ERR_DEBUG, "---------- TOUCH ACTION STARTED ----------");
+      Debug("event:finger", "---------- TOUCH ACTION STARTED ----------");
     }
   }
   else if (button == MB_RELEASED && touched)
@@ -1107,7 +1166,7 @@ static void HandleButtonOrFinger_FollowFinger_MM(int mx, int my, int button)
     else
       SetPlayerMouseAction(old_mx, old_my, MB_RELEASED);
 
-    Error(ERR_DEBUG, "---------- TOUCH ACTION STOPPED ----------");
+    Debug("event:finger", "---------- TOUCH ACTION STOPPED ----------");
   }
 
   if (touched)
@@ -1136,7 +1195,7 @@ static void HandleButtonOrFinger_FollowFinger_MM(int mx, int my, int button)
 
       tapped = FALSE;
 
-      Error(ERR_DEBUG, "---------- TOUCH ACTION: ROTATING ----------");
+      Debug("event:finger", "---------- TOUCH ACTION: ROTATING ----------");
     }
     else
     {
@@ -1144,7 +1203,7 @@ static void HandleButtonOrFinger_FollowFinger_MM(int mx, int my, int button)
 
       SetPlayerMouseAction(old_mx, old_my, MB_RELEASED);
 
-      Error(ERR_DEBUG, "---------- TOUCH ACTION PAUSED ----------");
+      Debug("event:finger", "---------- TOUCH ACTION PAUSED ----------");
     }
   }
 }
@@ -1179,7 +1238,7 @@ static void HandleButtonOrFinger_FollowFinger(int mx, int my, int button)
       motion_key_x = KSYM_UNDEFINED;
       motion_key_y = KSYM_UNDEFINED;
 
-      Error(ERR_DEBUG, "---------- TOUCH ACTION STARTED ----------");
+      Debug("event:finger", "---------- TOUCH ACTION STARTED ----------");
     }
   }
   else if (button == MB_RELEASED && touched)
@@ -1198,13 +1257,13 @@ static void HandleButtonOrFinger_FollowFinger(int mx, int my, int button)
     {
       if (player_is_dropping)
       {
-       Error(ERR_DEBUG, "---------- DROP STOPPED ----------");
+       Debug("event:finger", "---------- DROP STOPPED ----------");
 
        HandleKey(setup.input[0].key.drop, KEY_RELEASED);
       }
       else
       {
-       Error(ERR_DEBUG, "---------- SNAP STOPPED ----------");
+       Debug("event:finger", "---------- SNAP STOPPED ----------");
 
        HandleKey(setup.input[0].key.snap, KEY_RELEASED);
       }
@@ -1213,7 +1272,7 @@ static void HandleButtonOrFinger_FollowFinger(int mx, int my, int button)
     motion_key_x = KSYM_UNDEFINED;
     motion_key_y = KSYM_UNDEFINED;
 
-    Error(ERR_DEBUG, "---------- TOUCH ACTION STOPPED ----------");
+    Debug("event:finger", "---------- TOUCH ACTION STOPPED ----------");
   }
 
   if (touched)
@@ -1240,7 +1299,7 @@ static void HandleButtonOrFinger_FollowFinger(int mx, int my, int button)
       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;
+       game_em.ply[0]->last_move_dir = last_move_dir;
       else
        local_player->last_move_dir = last_move_dir;
 
@@ -1257,13 +1316,13 @@ static void HandleButtonOrFinger_FollowFinger(int mx, int my, int button)
 
       if (player_is_dropping)
       {
-       Error(ERR_DEBUG, "---------- DROP STARTED ----------");
+       Debug("event:finger", "---------- DROP STARTED ----------");
 
        HandleKey(setup.input[0].key.drop, KEY_PRESSED);
       }
       else
       {
-       Error(ERR_DEBUG, "---------- SNAP STARTED ----------");
+       Debug("event:finger", "---------- SNAP STARTED ----------");
 
        HandleKey(setup.input[0].key.snap, KEY_PRESSED);
       }
@@ -1273,7 +1332,7 @@ static void HandleButtonOrFinger_FollowFinger(int mx, int my, int button)
       if (player_is_dropping &&
          player_drop_count == getPlayerInventorySize(0))
       {
-       Error(ERR_DEBUG, "---------- DROP -> SNAP ----------");
+       Debug("event:finger", "---------- DROP -> SNAP ----------");
 
        HandleKey(setup.input[0].key.drop, KEY_RELEASED);
        HandleKey(setup.input[0].key.snap, KEY_PRESSED);
@@ -1284,7 +1343,7 @@ static void HandleButtonOrFinger_FollowFinger(int mx, int my, int button)
 
     if (new_motion_key_x != motion_key_x)
     {
-      Error(ERR_DEBUG, "---------- %s %s ----------",
+      Debug("event:finger", "---------- %s %s ----------",
            started_on_player && !player_is_dropping ? "SNAPPING" : "MOVING",
            dx < 0 ? "LEFT" : dx > 0 ? "RIGHT" : "PAUSED");
 
@@ -1296,7 +1355,7 @@ static void HandleButtonOrFinger_FollowFinger(int mx, int my, int button)
 
     if (new_motion_key_y != motion_key_y)
     {
-      Error(ERR_DEBUG, "---------- %s %s ----------",
+      Debug("event:finger", "---------- %s %s ----------",
            started_on_player && !player_is_dropping ? "SNAPPING" : "MOVING",
            dy < 0 ? "UP" : dy > 0 ? "DOWN" : "PAUSED");
 
@@ -1313,6 +1372,8 @@ static void HandleButtonOrFinger_FollowFinger(int mx, int my, int button)
 
 static void HandleButtonOrFinger(int mx, int my, int button)
 {
+  boolean valid_mouse_event = (mx != -1 && my != -1 && button != -1);
+
   if (game_status != GAME_MODE_PLAYING)
     return;
 
@@ -1329,16 +1390,27 @@ static void HandleButtonOrFinger(int mx, int my, int button)
   {
     if (strEqual(setup.touch.control_type, TOUCH_CONTROL_FOLLOW_FINGER))
       HandleButtonOrFinger_FollowFinger(mx, my, button);
+    else if (game.use_mouse_actions && valid_mouse_event)
+      SetPlayerMouseAction(mx, my, button);
   }
 }
 
-static boolean checkTextInputKeyModState(void)
+static boolean checkTextInputKey(Key key)
 {
   // 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);
+  // if Shift or right Alt key is pressed, handle key as text input
+  if ((GetKeyModState() & KMOD_TextInput) != KMOD_None)
+    return TRUE;
+
+  // ignore raw keys as text input when not in text input mode
+  if (KSYM_RAW(key) && !textinput_status)
+    return FALSE;
+
+  // else handle all printable keys as text input
+  return KSYM_PRINTABLE(key);
 }
 
 void HandleTextEvent(TextEvent *event)
@@ -1347,7 +1419,7 @@ void HandleTextEvent(TextEvent *event)
   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]",
+  Debug("event:text", "text == '%s' [%d byte(s), '%c'/%d], resulting key == %d (%s) [%04x]",
        text,
        strlen(text),
        text[0], (int)(text[0]),
@@ -1356,16 +1428,12 @@ void HandleTextEvent(TextEvent *event)
        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);
+  if (checkTextInputKey(key))
+  {
+    // process printable keys (with uppercase etc.) in text input mode
+    HandleKey(key, KEY_PRESSED);
+    HandleKey(key, KEY_RELEASED);
+  }
 }
 
 void HandlePauseResumeEvent(PauseResumeEvent *event)
@@ -1388,7 +1456,7 @@ void HandleKeyEvent(KeyEvent *event)
   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)",
+  Debug("event:key", "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,
@@ -1414,24 +1482,27 @@ void HandleKeyEvent(KeyEvent *event)
   {
     // for any other "real" key event, disable virtual buttons
     SetOverlayEnabled(FALSE);
+
+    // for any other "real" key event, disable overlay touch buttons
+    runtime.uses_touch_device = FALSE;
   }
 #endif
 
   HandleKeyModState(keymod, key_status);
 
-  // only handle raw key input without text modifier keys pressed
-  if (!checkTextInputKeyModState())
+  // process all keys if not in text input mode or if non-printable keys
+  if (!checkTextInputKey(key))
     HandleKey(key, key_status);
 }
 
 static int HandleDropFileEvent(char *filename)
 {
-  Error(ERR_DEBUG, "DROP FILE EVENT: '%s'", filename);
+  Debug("event:dropfile", "filename == '%s'", filename);
 
   // check and extract dropped zip files into correct user data directory
   if (!strSuffixLower(filename, ".zip"))
   {
-    Error(ERR_WARN, "file '%s' not supported", filename);
+    Warn("file '%s' not supported", filename);
 
     return TREE_TYPE_UNDEFINED;
   }
@@ -1442,7 +1513,7 @@ static int HandleDropFileEvent(char *filename)
 
   if (directory == NULL)
   {
-    Error(ERR_WARN, "zip file '%s' has invalid content!", filename);
+    Warn("zip file '%s' has invalid content!", filename);
 
     return TREE_TYPE_UNDEFINED;
   }
@@ -1484,7 +1555,7 @@ static int HandleDropFileEvent(char *filename)
 
 static void HandleDropTextEvent(char *text)
 {
-  Error(ERR_DEBUG, "DROP TEXT EVENT: '%s'", text);
+  Debug("event:droptext", "text == '%s'", text);
 }
 
 static void HandleDropCompleteEvent(int num_level_sets_succeeded,
@@ -1599,6 +1670,24 @@ void HandleDropEvent(Event *event)
     SDL_free(event->drop.file);
 }
 
+void HandleUserEvent(UserEvent *event)
+{
+  switch (event->code)
+  {
+    case USEREVENT_ANIM_DELAY_ACTION:
+    case USEREVENT_ANIM_EVENT_ACTION:
+      // execute action functions until matching action was found
+      if (DoKeysymAction(event->value1) ||
+         DoGadgetAction(event->value1) ||
+         DoScreenAction(event->value1))
+       return;
+      break;
+
+    default:
+      break;
+  }
+}
+
 void HandleButton(int mx, int my, int button, int button_nr)
 {
   static int old_mx = 0, old_my = 0;
@@ -1627,7 +1716,15 @@ void HandleButton(int mx, int my, int button, int button_nr)
      level.game_engine_type == GAME_ENGINE_TYPE_MM ||
      strEqual(setup.touch.control_type, TOUCH_CONTROL_FOLLOW_FINGER) ||
      (strEqual(setup.touch.control_type, TOUCH_CONTROL_VIRTUAL_BUTTONS) &&
-      !virtual_button_pressed));
+      !CheckVirtualButtonPressed(mx, my, button)));
+
+  // always recognize potentially releasing already pressed gadgets
+  if (button == MB_RELEASED)
+    handle_gadgets = TRUE;
+
+  // always recognize pressing or releasing overlay touch buttons
+  if (CheckPosition_OverlayTouchButtons(mx, my, button) && !motion_status)
+    handle_gadgets = TRUE;
 #endif
 
   if (HandleGlobalAnimClicks(mx, my, button, FALSE))
@@ -1706,17 +1803,6 @@ void HandleButton(int mx, int my, int button, int button_nr)
   }
 }
 
-static boolean is_string_suffix(char *string, char *suffix)
-{
-  int string_len = strlen(string);
-  int suffix_len = strlen(suffix);
-
-  if (suffix_len > string_len)
-    return FALSE;
-
-  return (strEqual(&string[string_len - suffix_len], suffix));
-}
-
 #define MAX_CHEAT_INPUT_LEN    32
 
 static void HandleKeysSpecial(Key key)
@@ -1741,78 +1827,74 @@ static void HandleKeysSpecial(Key key)
   cheat_input[cheat_input_len] = '\0';
 
 #if DEBUG_EVENTS_KEY
-  Error(ERR_DEBUG, "SPECIAL KEY '%s' [%d]\n", cheat_input, cheat_input_len);
+  Debug("event:key:special", "'%s' [%d]", cheat_input, cheat_input_len);
 #endif
 
   if (game_status == GAME_MODE_MAIN)
   {
-    if (is_string_suffix(cheat_input, ":insert-solution-tape") ||
-       is_string_suffix(cheat_input, ":ist"))
+    if (strSuffix(cheat_input, ":insert-solution-tape") ||
+       strSuffix(cheat_input, ":ist"))
     {
       InsertSolutionTape();
     }
-    else if (is_string_suffix(cheat_input, ":play-solution-tape") ||
-            is_string_suffix(cheat_input, ":pst"))
+    else if (strSuffix(cheat_input, ":play-solution-tape") ||
+            strSuffix(cheat_input, ":pst"))
     {
       PlaySolutionTape();
     }
-    else if (is_string_suffix(cheat_input, ":reload-graphics") ||
-            is_string_suffix(cheat_input, ":rg"))
+    else if (strSuffix(cheat_input, ":reload-graphics") ||
+            strSuffix(cheat_input, ":rg"))
     {
       ReloadCustomArtwork(1 << ARTWORK_TYPE_GRAPHICS);
       DrawMainMenu();
     }
-    else if (is_string_suffix(cheat_input, ":reload-sounds") ||
-            is_string_suffix(cheat_input, ":rs"))
+    else if (strSuffix(cheat_input, ":reload-sounds") ||
+            strSuffix(cheat_input, ":rs"))
     {
       ReloadCustomArtwork(1 << ARTWORK_TYPE_SOUNDS);
       DrawMainMenu();
     }
-    else if (is_string_suffix(cheat_input, ":reload-music") ||
-            is_string_suffix(cheat_input, ":rm"))
+    else if (strSuffix(cheat_input, ":reload-music") ||
+            strSuffix(cheat_input, ":rm"))
     {
       ReloadCustomArtwork(1 << ARTWORK_TYPE_MUSIC);
       DrawMainMenu();
     }
-    else if (is_string_suffix(cheat_input, ":reload-artwork") ||
-            is_string_suffix(cheat_input, ":ra"))
+    else if (strSuffix(cheat_input, ":reload-artwork") ||
+            strSuffix(cheat_input, ":ra"))
     {
       ReloadCustomArtwork(1 << ARTWORK_TYPE_GRAPHICS |
                          1 << ARTWORK_TYPE_SOUNDS |
                          1 << ARTWORK_TYPE_MUSIC);
       DrawMainMenu();
     }
-    else if (is_string_suffix(cheat_input, ":dump-level") ||
-            is_string_suffix(cheat_input, ":dl"))
+    else if (strSuffix(cheat_input, ":dump-level") ||
+            strSuffix(cheat_input, ":dl"))
     {
       DumpLevel(&level);
     }
-    else if (is_string_suffix(cheat_input, ":dump-tape") ||
-            is_string_suffix(cheat_input, ":dt"))
+    else if (strSuffix(cheat_input, ":dump-tape") ||
+            strSuffix(cheat_input, ":dt"))
     {
       DumpTape(&tape);
     }
-    else if (is_string_suffix(cheat_input, ":fix-tape") ||
-            is_string_suffix(cheat_input, ":ft"))
+    else if (strSuffix(cheat_input, ":undo-tape") ||
+            strSuffix(cheat_input, ":ut"))
     {
-      /* 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;
+      UndoTape();
+    }
+    else if (strSuffix(cheat_input, ":fix-tape") ||
+            strSuffix(cheat_input, ":ft"))
+    {
+      FixTape_ForceSinglePlayer();
     }
-    else if (is_string_suffix(cheat_input, ":save-native-level") ||
-            is_string_suffix(cheat_input, ":snl"))
+    else if (strSuffix(cheat_input, ":save-native-level") ||
+            strSuffix(cheat_input, ":snl"))
     {
       SaveNativeLevel(&level);
     }
-    else if (is_string_suffix(cheat_input, ":frames-per-second") ||
-            is_string_suffix(cheat_input, ":fps"))
+    else if (strSuffix(cheat_input, ":frames-per-second") ||
+            strSuffix(cheat_input, ":fps"))
     {
       global.show_frames_per_second = !global.show_frames_per_second;
     }
@@ -1820,18 +1902,18 @@ static void HandleKeysSpecial(Key key)
   else if (game_status == GAME_MODE_PLAYING)
   {
 #ifdef DEBUG
-    if (is_string_suffix(cheat_input, ".q"))
+    if (strSuffix(cheat_input, ".q"))
       DEBUG_SetMaximumDynamite();
 #endif
   }
   else if (game_status == GAME_MODE_EDITOR)
   {
-    if (is_string_suffix(cheat_input, ":dump-brush") ||
-       is_string_suffix(cheat_input, ":DB"))
+    if (strSuffix(cheat_input, ":dump-brush") ||
+       strSuffix(cheat_input, ":DB"))
     {
       DumpBrush();
     }
-    else if (is_string_suffix(cheat_input, ":DDB"))
+    else if (strSuffix(cheat_input, ":DDB"))
     {
       DumpBrush_Small();
     }
@@ -1850,13 +1932,20 @@ static void HandleKeysSpecial(Key key)
       {
        CopyClipboardToBrush();
       }
+      else if (letter == 'z')  // undo or redo last operation
+      {
+       if (GetKeyModState() & KMOD_Shift)
+         RedoLevelEditorOperation();
+       else
+         UndoLevelEditorOperation();
+      }
     }
   }
 
   // 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"))
+  if (strSuffix(cheat_input, ":dump-event-actions") ||
+      strSuffix(cheat_input, ":dea") ||
+      strSuffix(cheat_input, ":DEA"))
   {
     DumpGadgetIdentifiers();
     DumpScreenIdentifiers();
@@ -1889,13 +1978,13 @@ boolean HandleKeysDebug(Key key, int key_status)
        SetVideoFrameDelay(GameFrameDelay);
 
        if (GameFrameDelay > ONE_SECOND_DELAY)
-         Error(ERR_INFO, "frame delay == %d ms", GameFrameDelay);
+         Debug("event:key:debug", "frame delay == %d ms", GameFrameDelay);
        else if (GameFrameDelay != 0)
-         Error(ERR_INFO, "frame delay == %d ms (max. %d fps / %d %%)",
+         Debug("event:key:debug", "frame delay == %d ms (max. %d fps / %d %%)",
                GameFrameDelay, ONE_SECOND_DELAY / GameFrameDelay,
                GAME_FRAME_DELAY * 100 / GameFrameDelay);
        else
-         Error(ERR_INFO, "frame delay == 0 ms (maximum speed)");
+         Debug("event:key:debug", "frame delay == 0 ms (maximum speed)");
 
        return TRUE;
       }
@@ -1908,14 +1997,14 @@ boolean HandleKeysDebug(Key key, int key_status)
     {
       options.debug = !options.debug;
 
-      Error(ERR_INFO, "debug mode %s",
+      Debug("event:key:debug", "debug mode %s",
            (options.debug ? "enabled" : "disabled"));
 
       return TRUE;
     }
     else if (key == KSYM_v)
     {
-      Error(ERR_INFO, "currently using game engine version %d",
+      Debug("event:key:debug", "currently using game engine version %d",
            game.engine_version);
 
       return TRUE;
@@ -2014,43 +2103,46 @@ void HandleKey(Key key, int key_status)
       if (stored_player[pnr].snap_action)
        stored_player[pnr].action |= JOY_BUTTON_SNAP;
 
-      if (tape.single_step && tape.recording && tape.pausing && !tape.use_mouse)
+      if (tape.recording && tape.pausing && tape.use_key_actions)
       {
-       if (key_status == KEY_PRESSED && key_action & KEY_MOTION)
+       if (tape.single_step)
        {
-         TapeTogglePause(TAPE_TOGGLE_AUTOMATIC);
+         if (key_status == KEY_PRESSED && key_action & KEY_MOTION)
+         {
+           TapeTogglePause(TAPE_TOGGLE_AUTOMATIC);
 
-         // if snap key already pressed, keep pause mode when releasing
-         if (stored_player[pnr].action & KEY_BUTTON_SNAP)
-           has_snapped[pnr] = TRUE;
-       }
-       else if (key_status == KEY_PRESSED && key_action & KEY_BUTTON_DROP)
-       {
-         TapeTogglePause(TAPE_TOGGLE_AUTOMATIC);
+           // if snap key already pressed, keep pause mode when releasing
+           if (stored_player[pnr].action & KEY_BUTTON_SNAP)
+             has_snapped[pnr] = TRUE;
+         }
+         else if (key_status == KEY_PRESSED && key_action & KEY_BUTTON_DROP)
+         {
+           TapeTogglePause(TAPE_TOGGLE_AUTOMATIC);
 
-         if (level.game_engine_type == GAME_ENGINE_TYPE_SP &&
-             getRedDiskReleaseFlag_SP() == 0)
+           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;
+           }
+         }
+         else if (key_status == KEY_RELEASED && key_action & KEY_BUTTON_SNAP)
          {
-           // add a single inactive frame before dropping starts
-           stored_player[pnr].action &= ~KEY_BUTTON_DROP;
-           stored_player[pnr].force_dropping = TRUE;
+           // if snap key was pressed without direction, leave pause mode
+           if (!has_snapped[pnr])
+             TapeTogglePause(TAPE_TOGGLE_AUTOMATIC);
+
+           has_snapped[pnr] = FALSE;
          }
        }
-       else if (key_status == KEY_RELEASED && key_action & KEY_BUTTON_SNAP)
+       else
        {
-         // if snap key was pressed without direction, leave pause mode
-         if (!has_snapped[pnr])
-           TapeTogglePause(TAPE_TOGGLE_AUTOMATIC);
-
-         has_snapped[pnr] = FALSE;
+         // prevent key release events from un-pausing a paused game
+         if (key_status == KEY_PRESSED && key_action & KEY_ACTION)
+           TapeTogglePause(TAPE_TOGGLE_MANUAL);
        }
       }
-      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)
@@ -2093,11 +2185,13 @@ void HandleKey(Key key, int key_status)
   {
     setup.fullscreen = !setup.fullscreen;
 
-    ToggleFullscreenOrChangeWindowScalingIfNeeded();
+    ToggleFullscreenIfNeeded();
 
     if (game_status == GAME_MODE_SETUP)
       RedrawSetupScreenAfterFullscreenToggle();
 
+    UpdateMousePosition();
+
     // set flag to ignore repeated "key pressed" events
     ignore_repeated_key = TRUE;
 
@@ -2124,17 +2218,22 @@ void HandleKey(Key key, int key_status)
     else if (setup.window_scaling_percent > MAX_WINDOW_SCALING_PERCENT)
       setup.window_scaling_percent = MAX_WINDOW_SCALING_PERCENT;
 
-    ToggleFullscreenOrChangeWindowScalingIfNeeded();
+    ChangeWindowScalingIfNeeded();
 
     if (game_status == GAME_MODE_SETUP)
       RedrawSetupScreenAfterFullscreenToggle();
 
+    UpdateMousePosition();
+
     return;
   }
 
-  if (HandleGlobalAnimClicks(-1, -1, (key == KSYM_space ||
-                                     key == KSYM_Return ||
-                                     key == KSYM_Escape), TRUE))
+  // some key events are handled like clicks for global animations
+  boolean click = (key == KSYM_space ||
+                  key == KSYM_Return ||
+                  key == KSYM_Escape);
+
+  if (click && HandleGlobalAnimClicks(-1, -1, MB_LEFTBUTTON, TRUE))
   {
     // do not handle this key event anymore
     if (key != KSYM_Escape)    // always allow ESC key to be handled
@@ -2539,15 +2638,18 @@ void HandleJoystick(void)
        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 (tape.recording && tape.pausing && tape.use_key_actions)
       {
-       if (joystick & JOY_ACTION)
-         TapeTogglePause(TAPE_TOGGLE_MANUAL);
+       if (tape.single_step)
+       {
+         if (joystick & JOY_ACTION)
+           TapeTogglePause(TAPE_TOGGLE_AUTOMATIC);
+       }
+       else
+       {
+         if (joystick & JOY_ACTION)
+           TapeTogglePause(TAPE_TOGGLE_MANUAL);
+       }
       }
 
       if (level.game_engine_type == GAME_ENGINE_TYPE_MM)