SDL: video: Fix focus when non-focused popup windows are hidden

From 8ea4b326b375666cb78df317487c578ffeab0e92 Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Wed, 1 Jan 2025 16:18:24 -0500
Subject: [PATCH] video: Fix focus when non-focused popup windows are hidden

Fix focus issues when a popup that is the child of a window that does not currently have keyboard focus is hidden or destroyed.
---
 src/video/cocoa/SDL_cocoawindow.m     | 30 ++++++++++++++-------------
 src/video/wayland/SDL_waylandwindow.c | 26 ++++++++++++-----------
 src/video/windows/SDL_windowswindow.c | 30 +++++++++++++--------------
 src/video/x11/SDL_x11window.c         | 26 ++++++++++++-----------
 4 files changed, 59 insertions(+), 53 deletions(-)

diff --git a/src/video/cocoa/SDL_cocoawindow.m b/src/video/cocoa/SDL_cocoawindow.m
index 4f51a514fe716..cfc1d2315985f 100644
--- a/src/video/cocoa/SDL_cocoawindow.m
+++ b/src/video/cocoa/SDL_cocoawindow.m
@@ -634,7 +634,7 @@ static void Cocoa_UpdateClipCursor(SDL_Window *window)
     return toplevel;
 }
 
-static void Cocoa_SetKeyboardFocus(SDL_Window *window)
+static void Cocoa_SetKeyboardFocus(SDL_Window *window, bool set_active_focus)
 {
     SDL_Window *toplevel = GetParentToplevelWindow(window);
     SDL_CocoaWindowData *toplevel_data;
@@ -642,7 +642,7 @@ static void Cocoa_SetKeyboardFocus(SDL_Window *window)
     toplevel_data = (__bridge SDL_CocoaWindowData *)toplevel->internal;
     toplevel_data.keyboard_focus = window;
 
-    if (!window->is_hiding && !window->is_destroying) {
+    if (set_active_focus && !window->is_hiding && !window->is_destroying) {
     	SDL_SetKeyboardFocus(window);
     }
 }
@@ -1175,7 +1175,7 @@ - (void)windowDidBecomeKey:(NSNotification *)aNotification
 
     // We're going to get keyboard events, since we're key.
     // This needs to be done before restoring the relative mouse mode.
-    Cocoa_SetKeyboardFocus(_data.keyboard_focus ? _data.keyboard_focus : window);
+    Cocoa_SetKeyboardFocus(_data.keyboard_focus ? _data.keyboard_focus : window, true);
 
     // If we just gained focus we need the updated mouse position
     if (!(window->flags & SDL_WINDOW_MOUSE_RELATIVE_MODE)) {
@@ -2131,15 +2131,13 @@ then immediately ordering out (removing) the window does work. */
         if (!SDL_WINDOW_IS_POPUP(window)) {
             if ([nswindow isKeyWindow]) {
                 window->flags |= SDL_WINDOW_INPUT_FOCUS;
-                Cocoa_SetKeyboardFocus(data.window);
+                Cocoa_SetKeyboardFocus(data.window, true);
             }
         } else {
             if (window->flags & SDL_WINDOW_TOOLTIP) {
                 [nswindow setIgnoresMouseEvents:YES];
             } else if (window->flags & SDL_WINDOW_POPUP_MENU) {
-                if (window->parent == SDL_GetKeyboardFocus()) {
-                    Cocoa_SetKeyboardFocus(window);
-                }
+                Cocoa_SetKeyboardFocus(window, window->parent == SDL_GetKeyboardFocus());
             }
         }
 
@@ -2555,16 +2553,20 @@ void Cocoa_HideWindow(SDL_VideoDevice *_this, SDL_Window *window)
 
         // Transfer keyboard focus back to the parent when closing a popup menu
         if (window->flags & SDL_WINDOW_POPUP_MENU) {
-            if (window == SDL_GetKeyboardFocus()) {
-                SDL_Window *new_focus = window->parent;
+            SDL_Window *new_focus = window->parent;
+            bool set_focus = window == SDL_GetKeyboardFocus();
 
-                // Find the highest level window, up to the next toplevel, that isn't being hidden or destroyed.
-                while (SDL_WINDOW_IS_POPUP(new_focus) && (new_focus->is_hiding || new_focus->is_destroying)) {
-                    new_focus = new_focus->parent;
-                }
+            // Find the highest level window, up to the toplevel parent, that isn't being hidden or destroyed.
+            while (SDL_WINDOW_IS_POPUP(new_focus) && (new_focus->is_hiding || new_focus->is_destroying)) {
+                new_focus = new_focus->parent;
 
-                Cocoa_SetKeyboardFocus(new_focus);
+                // If some window in the chain currently had focus, set it to the new lowest-level window.
+                if (!set_focus) {
+                    set_focus = new_focus == SDL_GetKeyboardFocus();
+                }
             }
+
+            Cocoa_SetKeyboardFocus(new_focus, set_focus);
         } else if (window->parent && waskey) {
             /* Key status is not automatically set on the parent when a child is hidden. Check if the
              * child window was key, and set the first visible parent to be key if so.
diff --git a/src/video/wayland/SDL_waylandwindow.c b/src/video/wayland/SDL_waylandwindow.c
index 75e80af4ee499..6d463b3bdbab0 100644
--- a/src/video/wayland/SDL_waylandwindow.c
+++ b/src/video/wayland/SDL_waylandwindow.c
@@ -1629,7 +1629,7 @@ static const struct frog_color_managed_surface_listener frog_surface_listener =
     frog_preferred_metadata_handler
 };
 
-static void SetKeyboardFocus(SDL_Window *window)
+static void SetKeyboardFocus(SDL_Window *window, bool set_focus)
 {
     SDL_Window *toplevel = window;
 
@@ -1640,7 +1640,7 @@ static void SetKeyboardFocus(SDL_Window *window)
 
     toplevel->internal->keyboard_focus = window;
 
-    if (!window->is_hiding && !window->is_destroying) {
+    if (set_focus && !window->is_hiding && !window->is_destroying) {
         SDL_SetKeyboardFocus(window);
     }
 }
@@ -1895,9 +1895,7 @@ void Wayland_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window)
                 wl_surface_set_input_region(data->surface, region);
                 wl_region_destroy(region);
             } else if (window->flags & SDL_WINDOW_POPUP_MENU) {
-                if (window->parent == SDL_GetKeyboardFocus()) {
-                    SetKeyboardFocus(window);
-                }
+                SetKeyboardFocus(window, window->parent == SDL_GetKeyboardFocus());
             }
 
             SDL_SetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_XDG_POPUP_POINTER, data->shell_surface.xdg.popup.xdg_popup);
@@ -2042,16 +2040,20 @@ static void Wayland_ReleasePopup(SDL_VideoDevice *_this, SDL_Window *popup)
     }
 
     if (popup->flags & SDL_WINDOW_POPUP_MENU) {
-        if (popup == SDL_GetKeyboardFocus()) {
-            SDL_Window *new_focus = popup->parent;
+        SDL_Window *new_focus = popup->parent;
+        bool set_focus = popup == SDL_GetKeyboardFocus();
 
-            // Find the highest level window, up to the toplevel parent, that isn't being hidden or destroyed.
-            while (SDL_WINDOW_IS_POPUP(new_focus) && new_focus->parent && (new_focus->is_hiding || new_focus->is_destroying)) {
-                new_focus = new_focus->parent;
-            }
+        // Find the highest level window, up to the toplevel parent, that isn't being hidden or destroyed.
+        while (SDL_WINDOW_IS_POPUP(new_focus) && (new_focus->is_hiding || new_focus->is_destroying)) {
+            new_focus = new_focus->parent;
 
-            SetKeyboardFocus(new_focus);
+            // If some window in the chain currently had focus, set it to the new lowest-level window.
+            if (!set_focus) {
+                set_focus = new_focus == SDL_GetKeyboardFocus();
+            }
         }
+
+        SetKeyboardFocus(new_focus, set_focus);
     }
 
     xdg_popup_destroy(popupdata->shell_surface.xdg.popup.xdg_popup);
diff --git a/src/video/windows/SDL_windowswindow.c b/src/video/windows/SDL_windowswindow.c
index d4456cab3b2cb..459f23d875d38 100644
--- a/src/video/windows/SDL_windowswindow.c
+++ b/src/video/windows/SDL_windowswindow.c
@@ -723,7 +723,7 @@ static void WIN_ConstrainPopup(SDL_Window *window, bool output_to_pending)
     }
 }
 
-static void WIN_SetKeyboardFocus(SDL_Window *window)
+static void WIN_SetKeyboardFocus(SDL_Window *window, bool set_active_focus)
 {
     SDL_Window *toplevel = window;
 
@@ -734,7 +734,7 @@ static void WIN_SetKeyboardFocus(SDL_Window *window)
 
     toplevel->internal->keyboard_focus = window;
 
-    if (!window->is_hiding && !window->is_destroying) {
+    if (set_active_focus && !window->is_hiding && !window->is_destroying) {
     	SDL_SetKeyboardFocus(window);
     }
 }
@@ -1097,9 +1097,7 @@ void WIN_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window)
     }
 
     if (window->flags & SDL_WINDOW_POPUP_MENU && bActivate) {
-        if (window->parent == SDL_GetKeyboardFocus()) {
-            WIN_SetKeyboardFocus(window);
-        }
+	    WIN_SetKeyboardFocus(window, window->parent == SDL_GetKeyboardFocus());
     }
     if (window->flags & SDL_WINDOW_MODAL) {
         WIN_SetWindowModal(_this, window, true);
@@ -1118,16 +1116,20 @@ void WIN_HideWindow(SDL_VideoDevice *_this, SDL_Window *window)
 
     // Transfer keyboard focus back to the parent
     if (window->flags & SDL_WINDOW_POPUP_MENU) {
-        if (window == SDL_GetKeyboardFocus()) {
-            SDL_Window *new_focus = window->parent;
+        SDL_Window *new_focus = window->parent;
+        bool set_focus = window == SDL_GetKeyboardFocus();
 
-            // Find the highest level window, up to the toplevel parent, that isn't being hidden or destroyed.
-            while (SDL_WINDOW_IS_POPUP(new_focus) && (new_focus->is_hiding || new_focus->is_destroying)) {
-                new_focus = new_focus->parent;
-            }
+        // Find the highest level window, up to the toplevel parent, that isn't being hidden or destroyed.
+        while (SDL_WINDOW_IS_POPUP(new_focus) && (new_focus->is_hiding || new_focus->is_destroying)) {
+            new_focus = new_focus->parent;
 
-            WIN_SetKeyboardFocus(new_focus);
+            // If some window in the chain currently had keyboard focus, set it to the new lowest-level window.
+            if (!set_focus) {
+                set_focus = new_focus == SDL_GetKeyboardFocus();
+            }
         }
+
+        WIN_SetKeyboardFocus(new_focus, set_focus);
     }
 }
 
@@ -1165,9 +1167,7 @@ void WIN_RaiseWindow(SDL_VideoDevice *_this, SDL_Window *window)
     if (bActivate) {
         SetForegroundWindow(hwnd);
         if (window->flags & SDL_WINDOW_POPUP_MENU) {
-            if (window->parent == SDL_GetKeyboardFocus()) {
-                WIN_SetKeyboardFocus(window);
-            }
+            WIN_SetKeyboardFocus(window, window->parent == SDL_GetKeyboardFocus());
         }
     } else {
         SetWindowPos(hwnd, HWND_TOP, 0, 0, 0, 0, data->copybits_flag | SWP_NOMOVE | SWP_NOSIZE | SWP_NOOWNERZORDER | SWP_NOACTIVATE);
diff --git a/src/video/x11/SDL_x11window.c b/src/video/x11/SDL_x11window.c
index 791d515e19538..81b8434a265e4 100644
--- a/src/video/x11/SDL_x11window.c
+++ b/src/video/x11/SDL_x11window.c
@@ -240,7 +240,7 @@ static void X11_ConstrainPopup(SDL_Window *window, bool output_to_pending)
     }
 }
 
-static void X11_SetKeyboardFocus(SDL_Window *window)
+static void X11_SetKeyboardFocus(SDL_Window *window, bool set_active_focus)
 {
     SDL_Window *toplevel = window;
 
@@ -251,7 +251,7 @@ static void X11_SetKeyboardFocus(SDL_Window *window)
 
     toplevel->internal->keyboard_focus = window;
 
-    if (!window->is_hiding && !window->is_destroying) {
+    if (set_active_focus && !window->is_hiding && !window->is_destroying) {
         SDL_SetKeyboardFocus(window);
     }
 }
@@ -1474,9 +1474,7 @@ void X11_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window)
 
     // Popup menus grab the keyboard
     if (window->flags & SDL_WINDOW_POPUP_MENU) {
-        if (window->parent == SDL_GetKeyboardFocus()) {
-            X11_SetKeyboardFocus(window);
-        }
+        X11_SetKeyboardFocus(window, window->parent == SDL_GetKeyboardFocus());
     }
 
     // Get some valid border values, if we haven't received them yet
@@ -1532,16 +1530,20 @@ void X11_HideWindow(SDL_VideoDevice *_this, SDL_Window *window)
 
     // Transfer keyboard focus back to the parent
     if (window->flags & SDL_WINDOW_POPUP_MENU) {
-        if (window == SDL_GetKeyboardFocus()) {
-            SDL_Window *new_focus = window->parent;
+        SDL_Window *new_focus = window->parent;
+        bool set_focus = window == SDL_GetKeyboardFocus();
 
-            // Find the highest level window that isn't being hidden or destroyed.
-            while (SDL_WINDOW_IS_POPUP(new_focus) && (new_focus->is_hiding || new_focus->is_destroying)) {
-                new_focus = new_focus->parent;
-            }
+        // Find the highest level window, up to the toplevel parent, that isn't being hidden or destroyed.
+        while (SDL_WINDOW_IS_POPUP(new_focus) && (new_focus->is_hiding || new_focus->is_destroying)) {
+            new_focus = new_focus->parent;
 
-            X11_SetKeyboardFocus(new_focus);
+            // If some window in the chain currently had focus, set it to the new lowest-level window.
+            if (!set_focus) {
+                set_focus = new_focus == SDL_GetKeyboardFocus();
+            }
         }
+
+        X11_SetKeyboardFocus(new_focus, set_focus);
     }
 
     X11_XSync(display, False);