fixed ignoring mouse actions with MM engine using virtual buttons
[rocksndiamonds.git] / src / events.c
index b450c2b0d547a79f587da1a82cd8821ed83bb86f..8e368b613686a1331f71d396dd47c0daaef87e62 100644 (file)
@@ -39,6 +39,12 @@ 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
    delay problems with lots of mouse motion events when mouse button
    not pressed (X11 can handle this with 'PointerMotionHintMask') */
@@ -128,6 +134,19 @@ static boolean SkipPressedMouseMotionEvent(const Event *event)
   return FALSE;
 }
 
+static boolean WaitValidEvent(Event *event)
+{
+  WaitEvent(event);
+
+  if (!FilterEvents(event))
+    return FALSE;
+
+  if (SkipPressedMouseMotionEvent(event))
+    return FALSE;
+
+  return TRUE;
+}
+
 /* 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
@@ -139,20 +158,8 @@ static boolean SkipPressedMouseMotionEvent(const Event *event)
 boolean NextValidEvent(Event *event)
 {
   while (PendingEvent())
-  {
-    boolean handle_this_event = FALSE;
-
-    NextEvent(event);
-
-    if (FilterEvents(event))
-      handle_this_event = TRUE;
-
-    if (SkipPressedMouseMotionEvent(event))
-      handle_this_event = FALSE;
-
-    if (handle_this_event)
+    if (WaitValidEvent(event))
       return TRUE;
-  }
 
   return FALSE;
 }
@@ -297,7 +304,9 @@ void HandleMouseCursor()
        cursor_inside_playfield &&
        DelayReached(&special_cursor_delay, special_cursor_delay_value))
     {
-      SetMouseCursor(CURSOR_PLAYFIELD);
+      if (level.game_engine_type != GAME_ENGINE_TYPE_MM ||
+         tile_cursor.enabled)
+       SetMouseCursor(CURSOR_PLAYFIELD);
     }
   }
   else if (gfx.cursor_mode != CURSOR_DEFAULT)
@@ -316,10 +325,10 @@ void EventLoop(void)
     if (PendingEvent())
       HandleEvents();
     else
-      HandleMouseCursor();
+      HandleNoEvent();
 
-    /* also execute after pending events have been processed before */
-    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 */
@@ -340,12 +349,10 @@ void EventLoop(void)
 
 void ClearEventQueue()
 {
-  while (PendingEvent())
-  {
-    Event event;
-
-    NextEvent(&event);
+  Event event;
 
+  while (NextValidEvent(&event))
+  {
     switch (event.type)
     {
       case EVENT_BUTTONRELEASE:
@@ -370,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;
@@ -380,6 +394,35 @@ void ClearPlayerAction()
     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);
+
+  if (local_player->mouse_action.button_hint)
+    button = local_player->mouse_action.button_hint;
+
+  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()
@@ -392,7 +435,8 @@ void SleepWhileUnmapped()
   {
     Event event;
 
-    NextEvent(&event);
+    if (!WaitValidEvent(&event))
+      continue;
 
     switch (event.type)
     {
@@ -445,6 +489,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;
@@ -589,10 +636,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
@@ -609,185 +676,181 @@ static struct
   Key key;
 } touch_info[NUM_TOUCH_FINGERS];
 
-void HandleFingerEvent(FingerEvent *event)
+void HandleFingerEvent_VirtualButtons(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 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);
+#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
+  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;
 
-  if (game_status != GAME_MODE_PLAYING)
-    return;
+  // for any touch input event, enable overlay buttons (if activated)
+  SetOverlayEnabled(TRUE);
 
-  if (strEqual(setup.touch.control_type, TOUCH_CONTROL_FOLLOW_FINGER))
-    return;
+  Error(ERR_DEBUG, "::: key '%s' was '%s' [fingerId: %lld]",
+       getKeyNameFromKey(key), key_status_name, event->fingerId);
 
-  if (strEqual(setup.touch.control_type, TOUCH_CONTROL_VIRTUAL_BUTTONS))
+  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++)
   {
-    int key_status = (event->type == EVENT_FINGERRELEASE ? KEY_RELEASED :
-                     KEY_PRESSED);
-    float ypos = 1.0 - 1.0 / 3.0 * video.display_width / video.display_height;
-
-    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);
-
-    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++)
+    if (touch_info[i].touched &&
+       touch_info[i].finger_id == event->fingerId)
     {
-      if (touch_info[i].touched &&
-         touch_info[i].finger_id == event->fingerId)
-      {
-       // Error(ERR_DEBUG, "MARK 1: %d", i);
+      // Error(ERR_DEBUG, "MARK 1: %d", i);
 
-       break;
-      }
+      break;
     }
+  }
 
-    if (i >= NUM_TOUCH_FINGERS)
+  if (i >= NUM_TOUCH_FINGERS)
+  {
+    if (key_status == KEY_PRESSED)
     {
-      if (key_status == KEY_PRESSED)
-      {
-       int oldest_pos = 0, oldest_counter = touch_info[0].counter;
+      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++)
+      // unknown finger id -- get new, empty slot, if available
+      for (i = 0; i < NUM_TOUCH_FINGERS; i++)
+      {
+       if (touch_info[i].counter < oldest_counter)
        {
-         if (touch_info[i].counter < oldest_counter)
-         {
-           oldest_pos = i;
-           oldest_counter = touch_info[i].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;
-         }
+         // Error(ERR_DEBUG, "MARK 2: %d", i);
        }
 
-       if (i >= NUM_TOUCH_FINGERS)
+       if (!touch_info[i].touched)
        {
-         // all slots allocated -- use oldest slot
-         i = oldest_pos;
+         // Error(ERR_DEBUG, "MARK 3: %d", i);
 
-         // Error(ERR_DEBUG, "MARK 4: %d", i);
+         break;
        }
       }
-      else
-      {
-       // release of previously unknown key (should not happen)
 
-       if (key != KSYM_UNDEFINED)
-       {
-         HandleKey(key, KEY_RELEASED);
+      if (i >= NUM_TOUCH_FINGERS)
+      {
+       // all slots allocated -- use oldest slot
+       i = oldest_pos;
 
-         Error(ERR_DEBUG, "=> key == '%s', key_status == '%s' [slot %d] [1]",
-               getKeyNameFromKey(key), "KEY_RELEASED", i);
-       }
+       // Error(ERR_DEBUG, "MARK 4: %d", i);
       }
     }
-
-    if (i < NUM_TOUCH_FINGERS)
+    else
     {
-      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);
-         }
+      // release of previously unknown key (should not happen)
 
-         if (key != KSYM_UNDEFINED)
-         {
-           HandleKey(key, KEY_PRESSED);
-
-           Error(ERR_DEBUG, "=> key == '%s', key_status == '%s' [slot %d] [3]",
-                 getKeyNameFromKey(key), "KEY_PRESSED", i);
-         }
-       }
+      if (key != KSYM_UNDEFINED)
+      {
+       HandleKey(key, KEY_RELEASED);
 
-       touch_info[i].touched = TRUE;
-       touch_info[i].finger_id = event->fingerId;
-       touch_info[i].counter = Counter();
-       touch_info[i].key = key;
+       Error(ERR_DEBUG, "=> key == '%s', key_status == '%s' [slot %d] [1]",
+             getKeyNameFromKey(key), "KEY_RELEASED", i);
       }
-      else
+    }
+  }
+
+  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] [4]",
+         Error(ERR_DEBUG, "=> key == '%s', key_status == '%s' [slot %d] [2]",
                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;
+       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);
 
-    return;
+       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;
+    }
   }
+}
 
-  // use touch direction control
+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)
   {
@@ -922,24 +985,233 @@ void HandleFingerEvent(FingerEvent *event)
   }
 }
 
-static void HandleFollowFinger(int mx, int my, int button)
+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)
+  {
+    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);
+  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 (!strEqual(setup.touch.control_type, TOUCH_CONTROL_FOLLOW_FINGER))
-    return;
-
   if (button == MB_PRESSED && IN_GFX_FIELD_PLAY(mx, my))
   {
-    touch_info[0].touched = TRUE;
-    touch_info[0].key = 0;
+    touched = TRUE;
 
     old_mx = mx;
     old_my = my;
@@ -958,10 +1230,9 @@ static void HandleFollowFinger(int mx, int my, int button)
       Error(ERR_DEBUG, "---------- TOUCH ACTION STARTED ----------");
     }
   }
-  else if (button == MB_RELEASED && touch_info[0].touched)
+  else if (button == MB_RELEASED && touched)
   {
-    touch_info[0].touched = FALSE;
-    touch_info[0].key = 0;
+    touched = FALSE;
 
     old_mx = 0;
     old_my = 0;
@@ -993,7 +1264,7 @@ static void HandleFollowFinger(int mx, int my, int button)
     Error(ERR_DEBUG, "---------- TOUCH ACTION STOPPED ----------");
   }
 
-  if (touch_info[0].touched)
+  if (touched)
   {
     int src_x = local_player->jx;
     int src_y = local_player->jy;
@@ -1088,6 +1359,29 @@ static void HandleFollowFinger(int mx, int my, int button)
   }
 }
 
+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_VIRTUAL_BUTTONS))
+      SetPlayerMouseAction(mx, my, button);    /* special case */
+  }
+  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
@@ -1238,6 +1532,7 @@ 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_nr < 0)
   {
@@ -1254,25 +1549,20 @@ void HandleButton(int mx, int my, int button, int button_nr)
 
 #if defined(PLATFORM_ANDROID)
   // when playing, only handle gadgets when using "follow finger" controls
-  boolean handle_gadgets =
+  // 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 */
-  }
-#else
-  if (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 */
+    return;            /* force mouse event not to be handled at all */
   }
-#endif
 
-  if (HandleGlobalAnimClicks(mx, my, button))
+  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 */
@@ -1323,14 +1613,15 @@ void HandleButton(int mx, int my, int button, int button_nr)
       HandleSetupScreen(mx, my, 0, 0, button);
       break;
 
-#if defined(TARGET_SDL2)
     case GAME_MODE_PLAYING:
-      HandleFollowFinger(mx, my, button);
-#endif
+      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_FIELD_PLAY(mx, my) &&
-         GetKeyModState() & KMOD_Control)
+      if (button == MB_PRESSED && !motion_status && !button_hold &&
+         IN_GFX_FIELD_PLAY(mx, my) && GetKeyModState() & KMOD_Control)
        DumpTileFromScreen(mx, my);
 #endif
 
@@ -1386,6 +1677,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"))
     {
@@ -1441,6 +1737,11 @@ static void HandleKeysSpecial(Key key)
     {
       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)
   {
@@ -1461,6 +1762,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)
@@ -1470,7 +1780,7 @@ void HandleKeysDebug(Key key)
 
   if (game_status == GAME_MODE_PLAYING || !setup.debug.frame_delay_game_only)
   {
-    boolean mod_key_pressed = (GetKeyModState() != KMOD_None);
+    boolean mod_key_pressed = ((GetKeyModState() & KMOD_Valid) != KMOD_None);
 
     for (i = 0; i < NUM_DEBUG_FRAME_DELAY_KEYS; i++)
     {
@@ -1554,10 +1864,7 @@ void HandleKey(Key key, int 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++)
@@ -1583,97 +1890,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;
        }
        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 (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;
          }
        }
-       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 (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;
-         }
+         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
@@ -1771,7 +2033,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;
   }
@@ -1937,9 +2199,23 @@ void HandleKey(Key key, int key_status)
 }
 
 void HandleNoEvent()
+{
+  HandleMouseCursor();
+
+  switch (game_status)
+  {
+    case GAME_MODE_PLAYING:
+      HandleButtonOrFinger(-1, -1, -1);
+      break;
+  }
+}
+
+void HandleEventActions()
 {
   // if (button_status && game_status != GAME_MODE_PLAYING)
-  if (button_status && (game_status != GAME_MODE_PLAYING || tape.pausing))
+  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);
   }
@@ -1948,10 +2224,8 @@ void HandleNoEvent()
     HandleJoystick();
   }
 
-#if defined(NETWORK_AVALIABLE)
-  if (options.network)
+  if (network.enabled)
     HandleNetworking();
-#endif
 
   switch (game_status)
   {
@@ -1963,17 +2237,42 @@ void HandleNoEvent()
       HandleLevelEditorIdle();
       break;
 
-#if defined(TARGET_SDL2)
-    case GAME_MODE_PLAYING:
-      HandleFollowFinger(-1, -1, -1);
-      break;
-#endif
-
     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;
@@ -2009,9 +2308,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;
@@ -2020,6 +2325,7 @@ 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))
   {
@@ -2027,6 +2333,39 @@ void HandleJoystick()
     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:
@@ -2035,26 +2374,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;
-      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 (game_status == GAME_MODE_TITLE)
        HandleTitleScreen(0,0,dx,dy, newbutton ? MB_MENU_CHOICE : MB_MENU_MARK);
       else if (game_status == GAME_MODE_MAIN)
@@ -2067,19 +2388,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)
       {
@@ -2088,12 +2408,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:
@@ -2104,24 +2432,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
 }
 
@@ -2154,3 +2496,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;
+}