sdl12-compat: Attempt to merge KEYDOWN and TEXTINPUT events to prevent duplicates

From ebfcc6e76383d8bc234b268650cda9c29a312fc0 Mon Sep 17 00:00:00 2001
From: David Gow <[EMAIL REDACTED]>
Date: Tue, 13 Apr 2021 17:09:51 +0800
Subject: [PATCH] Attempt to merge KEYDOWN and TEXTINPUT events to prevent
 duplicates

This change makes all KEYDOWN events be stored temporarily as a "pending
keydown event". If a TEXTINPUT event comes in before the next time
PumpEvents() is called (or another KEYDOWN or KEYUP event appears), then
one of the events generated from the 'text' string is merged with the
KEYDOWN event, thus having both a valid 'sym' and 'unicode' value.

Also add some basic support for UTF-16 surrogate pairs, as that was
annoying me.

Note that this doesn't quite match what SDL 1.2 does, which depends
pretty heavily on what backend was being used. The closest backend (XIM,
I think) does basically the same thing as this, but puts the real keysym
on the last character of a mult-character TEXTINPUT, whereas we put it
in the first one. (The last one may actually be more sensible, in case
there are bugs where a 'last key pressed' is being maintained by an
application, and the correct one is being overwritten by SDLK_UNKNOWN,
but this was easier to implement for now.)
---
 src/SDL12_compat.c | 93 +++++++++++++++++++++++++++++++++++++++++-----
 1 file changed, 84 insertions(+), 9 deletions(-)

diff --git a/src/SDL12_compat.c b/src/SDL12_compat.c
index c4b0830..3bd9395 100644
--- a/src/SDL12_compat.c
+++ b/src/SDL12_compat.c
@@ -807,6 +807,8 @@ static EventQueueType *EventQueueHead = NULL;
 static EventQueueType *EventQueueTail = NULL;
 static EventQueueType *EventQueueAvailable = NULL;
 
+/* This is a KEYDOWN event which is being held for a follow-up TEXTINPUT */
+static SDL12_Event PendingKeydownEvent;
 
 /* Obviously we can't use SDL_LoadObject() to load SDL2.  :)  */
 static char loaderror[256];
@@ -1427,6 +1429,8 @@ Init12Video(void)
     EventQueueHead = EventQueueTail = NULL;
     EventQueueAvailable = EventQueuePool;
 
+    SDL_memset(&PendingKeydownEvent, 0, sizeof(SDL12_Event));
+
     SDL_memset(EventStates, SDL_ENABLE, sizeof (EventStates)); /* on by default */
     EventStates[SDL12_SYSWMEVENT] = SDL_IGNORE;  /* off by default. */
 
@@ -2207,6 +2211,23 @@ static int DecodeUTF8Char(char **ptr)
     return value;
 }
 
+/* Add the pending KEYDOWN event to the EventQueue, possibly with 'unicode' set
+ * Returns 1 if there was a pending event. */
+static int FlushPendingKeydownEvent(Uint32 unicode)
+{
+    if (PendingKeydownEvent.type != SDL12_KEYDOWN)
+        return 0;
+
+    PendingKeydownEvent.key.keysym.unicode = unicode;
+
+    PushEventIfNotFiltered(&PendingKeydownEvent);
+    
+    /* Reset the event. */
+    SDL_memset(&PendingKeydownEvent, 0, sizeof(SDL12_Event));
+
+    return 1;
+}
+
 static int SDLCALL
 EventFilter20to12(void *data, SDL_Event *event20)
 {
@@ -2285,7 +2306,6 @@ EventFilter20to12(void *data, SDL_Event *event20)
         case SDL_SYSWMEVENT: FIXME("write me"); return 1;
 
         case SDL_KEYUP:
-        case SDL_KEYDOWN:
             if (event20->key.repeat) {
                 return 1;  /* ignore 2.0-style key repeat events */
             }
@@ -2304,8 +2324,41 @@ EventFilter20to12(void *data, SDL_Event *event20)
             event12.key.keysym.scancode = 0;
             event12.key.keysym.mod = event20->key.keysym.mod;  /* these match up between 1.2 and 2.0! */
             event12.key.keysym.unicode = 0;
+
+            /* If there's a pending KEYDOWN event, flush it on KEYUP. */
+            FlushPendingKeydownEvent(0);
             break;
 
+        case SDL_KEYDOWN:
+
+            FlushPendingKeydownEvent(0);
+
+            if (event20->key.repeat) {
+                return 1;  /* ignore 2.0-style key repeat events */
+            }
+
+            PendingKeydownEvent.key.keysym.sym = Keysym20to12(event20->key.keysym.sym);
+            if (PendingKeydownEvent.key.keysym.sym == SDLK12_UNKNOWN) {
+                return 1;  /* drop it if we can't map it */
+            }
+
+            KeyState[PendingKeydownEvent.key.keysym.sym] = event20->key.state;
+
+            PendingKeydownEvent.type = (event20->type == SDL_KEYDOWN) ? SDL12_KEYDOWN : SDL12_KEYUP;
+            PendingKeydownEvent.key.which = 0;
+            PendingKeydownEvent.key.state = event20->key.state;
+            FIXME("SDL1.2 and SDL2.0 scancodes are incompatible");
+            /* turns out that some apps actually made use of the hardware scancodes (checking for platform beforehand) */
+            PendingKeydownEvent.key.keysym.scancode = 0;
+            PendingKeydownEvent.key.keysym.mod = event20->key.keysym.mod;  /* these match up between 1.2 and 2.0! */
+            PendingKeydownEvent.key.keysym.unicode = 0;
+
+            /* If Unicode is not enabled, flush all KEYDOWN events immediately. */
+            if (!EnabledUnicode)
+                FlushPendingKeydownEvent(0);
+
+            return 1;
+
         case SDL_TEXTEDITING: return 1;
         case SDL_TEXTINPUT:
         {
@@ -2313,14 +2366,31 @@ EventFilter20to12(void *data, SDL_Event *event20)
             int codePoint = 0;
             while ((codePoint = DecodeUTF8Char(&text)) != 0)
             {
-                if (codePoint > 0xFFFF)
-                    FIXME("Support for UTF-16 surrgate pairs");
-                event12.type = SDL12_KEYDOWN;
-                event12.key.state = SDL12_PRESSED;
-                event12.key.keysym.scancode = 0;
-                event12.key.keysym.sym = SDLK12_UNKNOWN;
-                event12.key.keysym.unicode = codePoint;
-                PushEventIfNotFiltered(&event12);
+                if (codePoint > 0xFFFF) {
+                    /* We need to send a UTF-16 surrogate pair. */
+                    Uint16 firstChar = ((codePoint - 0x10000) >> 10) + 0xD800;
+                    Uint16 secondChar = ((codePoint - 0x10000) & 0x3FF) + 0xDC00;
+                    event12.type = SDL12_KEYDOWN;
+                    event12.key.state = SDL12_PRESSED;
+                    event12.key.keysym.scancode = 0;
+                    event12.key.keysym.sym = SDLK12_UNKNOWN;
+                    event12.key.keysym.unicode = firstChar;
+                    if (!FlushPendingKeydownEvent(firstChar))
+                         PushEventIfNotFiltered(&event12);
+                    event12.key.keysym.unicode = secondChar;
+                    PushEventIfNotFiltered(&event12);
+            }
+            else
+            {
+                    if (!FlushPendingKeydownEvent(codePoint)) {
+                        event12.type = SDL12_KEYDOWN;
+                        event12.key.state = SDL12_PRESSED;
+                        event12.key.keysym.scancode = 0;
+                        event12.key.keysym.sym = SDLK12_UNKNOWN;
+                        event12.key.keysym.unicode = codePoint;
+                        PushEventIfNotFiltered(&event12);
+                    }
+            }
             }
         }
         return 1;
@@ -3777,6 +3847,11 @@ SDL_PumpEvents(void)
         PresentScreen();
     }
     while (SDL20_PollEvent(&e)) {  /* spin to drain the SDL2 event queue. */ }
+
+    /* If there's a pending KEYDOWN event, and we haven't got a TEXTINPUT
+     * event which matches it, then let it through now. */
+    if (PendingKeydownEvent.type == SDL12_KEYDOWN)
+        FlushPendingKeydownEvent(0);
 }
 
 DECLSPEC void SDLCALL