SDL: Add SDL_WINDOW_NOT_FOCUSABLE flag to set that the window should not be able to gain key focus

From a5e7214795958dcfb2cbb2f98a0a7590357858d9 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Wed, 30 Aug 2023 20:29:51 -0700
Subject: [PATCH] Add SDL_WINDOW_NOT_FOCUSABLE flag to set that the window
 should not be able to gain key focus

- Also adds SDL_SetWindowFocusable() to set/clear flag on existing windows
---
 include/SDL3/SDL_video.h              | 16 ++++++++++++-
 src/dynapi/SDL_dynapi.sym             |  1 +
 src/dynapi/SDL_dynapi_overrides.h     |  1 +
 src/dynapi/SDL_dynapi_procs.h         |  1 +
 src/video/SDL_sysvideo.h              |  1 +
 src/video/SDL_video.c                 | 20 +++++++++++++++-
 src/video/cocoa/SDL_cocoavideo.m      |  1 +
 src/video/cocoa/SDL_cocoawindow.h     |  1 +
 src/video/cocoa/SDL_cocoawindow.m     |  7 +++++-
 src/video/windows/SDL_windowsvideo.c  |  1 +
 src/video/windows/SDL_windowswindow.c | 34 +++++++++++++++++++++++----
 src/video/windows/SDL_windowswindow.h |  1 +
 src/video/x11/SDL_x11video.c          |  1 +
 src/video/x11/SDL_x11window.c         | 22 ++++++++++++++++-
 src/video/x11/SDL_x11window.h         |  1 +
 15 files changed, 101 insertions(+), 8 deletions(-)

diff --git a/include/SDL3/SDL_video.h b/include/SDL3/SDL_video.h
index 64d026bcaea1..2703d87cbdc2 100644
--- a/include/SDL3/SDL_video.h
+++ b/include/SDL3/SDL_video.h
@@ -150,7 +150,8 @@ typedef enum
     SDL_WINDOW_KEYBOARD_GRABBED     = 0x00100000,   /**< window has grabbed keyboard input */
     SDL_WINDOW_VULKAN               = 0x10000000,   /**< window usable for Vulkan surface */
     SDL_WINDOW_METAL                = 0x20000000,   /**< window usable for Metal view */
-    SDL_WINDOW_TRANSPARENT          = 0x40000000    /**< window with transparent buffer */
+    SDL_WINDOW_TRANSPARENT          = 0x40000000,   /**< window with transparent buffer */
+    SDL_WINDOW_NOT_FOCUSABLE        = 0x80000000,   /**< window should not be focusable */
 
 } SDL_WindowFlags;
 
@@ -1676,6 +1677,19 @@ extern DECLSPEC int SDLCALL SDL_SetWindowModalFor(SDL_Window *modal_window, SDL_
  */
 extern DECLSPEC int SDLCALL SDL_SetWindowInputFocus(SDL_Window *window);
 
+/**
+ * Set whether the window may have input focus.
+ *
+ * \param window the window to set focusable state
+ * \param focusable SDL_TRUE to allow input focus, SDL_FALSE to not allow input focus
+ * \returns 0 on success or a negative error code on failure; call
+ *          SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 3.0.0.
+ */
+extern DECLSPEC int SDLCALL SDL_SetWindowFocusable(SDL_Window *window, SDL_bool focusable);
+
+
 /**
  * Display the system-level window menu.
  *
diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym
index bf92d4b9a78a..0887e4ba36e1 100644
--- a/src/dynapi/SDL_dynapi.sym
+++ b/src/dynapi/SDL_dynapi.sym
@@ -901,6 +901,7 @@ SDL3_0.0.0 {
     SDL_WriteS64LE;
     SDL_WriteS64BE;
     SDL_GDKGetDefaultUser;
+    SDL_SetWindowFocusable;
     # extra symbols go here (don't modify this line)
   local: *;
 };
diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h
index 1d038f6fd85b..93d5db6b85df 100644
--- a/src/dynapi/SDL_dynapi_overrides.h
+++ b/src/dynapi/SDL_dynapi_overrides.h
@@ -926,3 +926,4 @@
 #define SDL_WriteS64LE SDL_WriteS64LE_REAL
 #define SDL_WriteS64BE SDL_WriteS64BE_REAL
 #define SDL_GDKGetDefaultUser SDL_GDKGetDefaultUser_REAL
+#define SDL_SetWindowFocusable SDL_SetWindowFocusable_REAL
diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h
index f4deb2db7db3..61bf6a26ce62 100644
--- a/src/dynapi/SDL_dynapi_procs.h
+++ b/src/dynapi/SDL_dynapi_procs.h
@@ -972,3 +972,4 @@ SDL_DYNAPI_PROC(SDL_bool,SDL_WriteS64BE,(SDL_RWops *a, Sint64 b),(a,b),return)
 #ifdef __GDK__
 SDL_DYNAPI_PROC(int,SDL_GDKGetDefaultUser,(XUserHandle *a),(a),return)
 #endif
+SDL_DYNAPI_PROC(int,SDL_SetWindowFocusable,(SDL_Window *a, SDL_bool b),(a,b),return)
diff --git a/src/video/SDL_sysvideo.h b/src/video/SDL_sysvideo.h
index d41ba2138041..a1bde5773eb0 100644
--- a/src/video/SDL_sysvideo.h
+++ b/src/video/SDL_sysvideo.h
@@ -264,6 +264,7 @@ struct SDL_VideoDevice
     void (*DestroyWindowFramebuffer)(SDL_VideoDevice *_this, SDL_Window *window);
     void (*OnWindowEnter)(SDL_VideoDevice *_this, SDL_Window *window);
     int (*FlashWindow)(SDL_VideoDevice *_this, SDL_Window *window, SDL_FlashOperation operation);
+    int (*SetWindowFocusable)(SDL_VideoDevice *_this, SDL_Window *window, SDL_bool focusable);
 
     /* * * */
     /*
diff --git a/src/video/SDL_video.c b/src/video/SDL_video.c
index 708cbc3768b9..616e9d7567f2 100644
--- a/src/video/SDL_video.c
+++ b/src/video/SDL_video.c
@@ -1740,7 +1740,7 @@ Uint32 SDL_GetWindowPixelFormat(SDL_Window *window)
 }
 
 #define CREATE_FLAGS \
-    (SDL_WINDOW_OPENGL | SDL_WINDOW_BORDERLESS | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_ALWAYS_ON_TOP | SDL_WINDOW_POPUP_MENU | SDL_WINDOW_UTILITY | SDL_WINDOW_TOOLTIP | SDL_WINDOW_VULKAN | SDL_WINDOW_MINIMIZED | SDL_WINDOW_METAL | SDL_WINDOW_TRANSPARENT)
+    (SDL_WINDOW_OPENGL | SDL_WINDOW_BORDERLESS | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_ALWAYS_ON_TOP | SDL_WINDOW_POPUP_MENU | SDL_WINDOW_UTILITY | SDL_WINDOW_TOOLTIP | SDL_WINDOW_VULKAN | SDL_WINDOW_MINIMIZED | SDL_WINDOW_METAL | SDL_WINDOW_TRANSPARENT | SDL_WINDOW_NOT_FOCUSABLE)
 
 static SDL_INLINE SDL_bool IsAcceptingDragAndDrop(void)
 {
@@ -3205,6 +3205,24 @@ int SDL_SetWindowInputFocus(SDL_Window *window)
     return _this->SetWindowInputFocus(_this, window);
 }
 
+int SDL_SetWindowFocusable(SDL_Window *window, SDL_bool focusable)
+{
+    CHECK_WINDOW_MAGIC(window, -1);
+
+    const int want = (focusable != SDL_FALSE); /* normalize the flag. */
+    const int have = !(window->flags & SDL_WINDOW_NOT_FOCUSABLE);
+    if ((want != have) && (_this->SetWindowFocusable)) {
+        if (want) {
+            window->flags &= ~SDL_WINDOW_NOT_FOCUSABLE;
+        } else {
+            window->flags |= SDL_WINDOW_NOT_FOCUSABLE;
+        }
+        _this->SetWindowFocusable(_this, window, (SDL_bool)want);
+    }
+
+    return 0;
+}
+
 void SDL_UpdateWindowGrab(SDL_Window *window)
 {
     SDL_bool keyboard_grabbed, mouse_grabbed;
diff --git a/src/video/cocoa/SDL_cocoavideo.m b/src/video/cocoa/SDL_cocoavideo.m
index d59590aee623..3b6092b00cb2 100644
--- a/src/video/cocoa/SDL_cocoavideo.m
+++ b/src/video/cocoa/SDL_cocoavideo.m
@@ -119,6 +119,7 @@ static void Cocoa_DeleteDevice(SDL_VideoDevice *device)
         device->SetWindowHitTest = Cocoa_SetWindowHitTest;
         device->AcceptDragAndDrop = Cocoa_AcceptDragAndDrop;
         device->FlashWindow = Cocoa_FlashWindow;
+        device->SetWindowFocusable = Cocoa_SetWindowFocusable;
 
         device->shape_driver.CreateShaper = Cocoa_CreateShaper;
         device->shape_driver.SetWindowShape = Cocoa_SetWindowShape;
diff --git a/src/video/cocoa/SDL_cocoawindow.h b/src/video/cocoa/SDL_cocoawindow.h
index ceb27db92a5c..1408784b28e1 100644
--- a/src/video/cocoa/SDL_cocoawindow.h
+++ b/src/video/cocoa/SDL_cocoawindow.h
@@ -169,5 +169,6 @@ extern int Cocoa_GetWindowWMInfo(SDL_VideoDevice *_this, SDL_Window *window, str
 extern int Cocoa_SetWindowHitTest(SDL_Window *window, SDL_bool enabled);
 extern void Cocoa_AcceptDragAndDrop(SDL_Window *window, SDL_bool accept);
 extern int Cocoa_FlashWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_FlashOperation operation);
+extern int Cocoa_SetWindowFocusable(SDL_VideoDevice *_this, SDL_Window *window, SDL_bool focusable);
 
 #endif /* SDL_cocoawindow_h_ */
diff --git a/src/video/cocoa/SDL_cocoawindow.m b/src/video/cocoa/SDL_cocoawindow.m
index 7a3af36234e7..3dcff6bb203d 100644
--- a/src/video/cocoa/SDL_cocoawindow.m
+++ b/src/video/cocoa/SDL_cocoawindow.m
@@ -116,7 +116,7 @@ - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
 - (BOOL)canBecomeKeyWindow
 {
     SDL_Window *window = [self findSDLWindow];
-    if (window && !(window->flags & SDL_WINDOW_TOOLTIP)) {
+    if (window && !(window->flags & (SDL_WINDOW_TOOLTIP | SDL_WINDOW_NOT_FOCUSABLE))) {
         return YES;
     } else {
         return NO;
@@ -2678,6 +2678,11 @@ int Cocoa_FlashWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_FlashOpera
     }
 }
 
+int Cocoa_SetWindowFocusable(SDL_VideoDevice *_this, SDL_Window *window, SDL_bool focusable)
+{
+    return 0; /* just succeed, the real work is done elsewhere. */
+}
+
 int Cocoa_SetWindowOpacity(SDL_VideoDevice *_this, SDL_Window *window, float opacity)
 {
     @autoreleasepool {
diff --git a/src/video/windows/SDL_windowsvideo.c b/src/video/windows/SDL_windowsvideo.c
index daa2bed5214a..66ad8e9b18a0 100644
--- a/src/video/windows/SDL_windowsvideo.c
+++ b/src/video/windows/SDL_windowsvideo.c
@@ -211,6 +211,7 @@ static SDL_VideoDevice *WIN_CreateDevice(void)
     device->AcceptDragAndDrop = WIN_AcceptDragAndDrop;
     device->FlashWindow = WIN_FlashWindow;
     device->ShowWindowSystemMenu = WIN_ShowWindowSystemMenu;
+    device->SetWindowFocusable = WIN_SetWindowFocusable;
 
     device->shape_driver.CreateShaper = Win32_CreateShaper;
     device->shape_driver.SetWindowShape = Win32_SetWindowShape;
diff --git a/src/video/windows/SDL_windowswindow.c b/src/video/windows/SDL_windowswindow.c
index 75b4fa02a01e..79cf00f6b9a1 100644
--- a/src/video/windows/SDL_windowswindow.c
+++ b/src/video/windows/SDL_windowswindow.c
@@ -144,10 +144,11 @@ static DWORD GetWindowStyleEx(SDL_Window *window)
 {
     DWORD style = 0;
 
-    if (SDL_WINDOW_IS_POPUP(window)) {
-        style = WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE;
-    } else if (window->flags & SDL_WINDOW_UTILITY) {
-        style = WS_EX_TOOLWINDOW;
+    if (SDL_WINDOW_IS_POPUP(window) || (window->flags & SDL_WINDOW_UTILITY)) {
+        style |= WS_EX_TOOLWINDOW;
+    }
+    if (SDL_WINDOW_IS_POPUP(window) || (window->flags & SDL_WINDOW_NOT_FOCUSABLE)) {
+        style |= WS_EX_NOACTIVATE;
     }
     return style;
 }
@@ -1539,6 +1540,31 @@ void WIN_ShowWindowSystemMenu(SDL_Window *window, int x, int y)
     ClientToScreen(data->hwnd, &pt);
     SendMessage(data->hwnd, WM_POPUPSYSTEMMENU, 0, MAKELPARAM(pt.x, pt.y));
 }
+
+int WIN_SetWindowFocusable(SDL_VideoDevice *_this, SDL_Window *window, SDL_bool focusable)
+{
+    SDL_WindowData *data = window->driverdata;
+    HWND hwnd = data->hwnd;
+    const LONG style = GetWindowLong(hwnd, GWL_EXSTYLE);
+
+    SDL_assert(style != 0);
+
+    if (focusable) {
+        if (style & WS_EX_NOACTIVATE) {
+            if (SetWindowLong(hwnd, GWL_EXSTYLE, style & ~WS_EX_NOACTIVATE) == 0) {
+                return WIN_SetError("SetWindowLong()");
+            }
+        }
+    } else {
+        if (!(style & WS_EX_NOACTIVATE)) {
+            if (SetWindowLong(hwnd, GWL_EXSTYLE, style | WS_EX_NOACTIVATE) == 0) {
+                return WIN_SetError("SetWindowLong()");
+            }
+        }
+    }
+
+    return 0;
+}
 #endif /*!defined(__XBOXONE__) && !defined(__XBOXSERIES__)*/
 
 void WIN_UpdateDarkModeForHWND(HWND hwnd)
diff --git a/src/video/windows/SDL_windowswindow.h b/src/video/windows/SDL_windowswindow.h
index f6f0331a3e74..527fee78d17f 100644
--- a/src/video/windows/SDL_windowswindow.h
+++ b/src/video/windows/SDL_windowswindow.h
@@ -109,6 +109,7 @@ extern int WIN_FlashWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_Flash
 extern void WIN_UpdateDarkModeForHWND(HWND hwnd);
 extern int WIN_SetWindowPositionInternal(SDL_Window *window, UINT flags);
 extern void WIN_ShowWindowSystemMenu(SDL_Window *window, int x, int y);
+extern int WIN_SetWindowFocusable(SDL_VideoDevice *_this, SDL_Window *window, SDL_bool focusable);
 
 /* Ends C function definitions when using C++ */
 #ifdef __cplusplus
diff --git a/src/video/x11/SDL_x11video.c b/src/video/x11/SDL_x11video.c
index 5122bcf58907..4192bf331c56 100644
--- a/src/video/x11/SDL_x11video.c
+++ b/src/video/x11/SDL_x11video.c
@@ -212,6 +212,7 @@ static SDL_VideoDevice *X11_CreateDevice(void)
     device->AcceptDragAndDrop = X11_AcceptDragAndDrop;
     device->FlashWindow = X11_FlashWindow;
     device->ShowWindowSystemMenu = X11_ShowWindowSystemMenu;
+    device->SetWindowFocusable = X11_SetWindowFocusable;
 
 #ifdef SDL_VIDEO_DRIVER_X11_XFIXES
     device->SetWindowMouseRect = X11_SetWindowMouseRect;
diff --git a/src/video/x11/SDL_x11window.c b/src/video/x11/SDL_x11window.c
index 664de3603ea1..7c211de23d4c 100644
--- a/src/video/x11/SDL_x11window.c
+++ b/src/video/x11/SDL_x11window.c
@@ -623,7 +623,7 @@ int X11_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window)
 
     /* Setup the input hints so we get keyboard input */
     wmhints = X11_XAllocWMHints();
-    wmhints->input = True;
+    wmhints->input = !(window->flags & SDL_WINDOW_NOT_FOCUSABLE) ? True : False;
     wmhints->window_group = data->window_group;
     wmhints->flags = InputHint | WindowGroupHint;
 
@@ -1982,4 +1982,24 @@ void X11_ShowWindowSystemMenu(SDL_Window *window, int x, int y)
     X11_XFlush(display);
 }
 
+int X11_SetWindowFocusable(SDL_VideoDevice *_this, SDL_Window *window, SDL_bool focusable)
+{
+    SDL_WindowData *data = window->driverdata;
+    Display *display = data->videodata->display;
+    XWMHints *wmhints;
+
+    wmhints = X11_XGetWMHints(display, data->xwindow);
+    if (wmhints == NULL) {
+        return SDL_SetError("Couldn't get WM hints");
+    }
+
+    wmhints->input = focusable ? True : False;
+    wmhints->flags |= InputHint;
+
+    X11_XSetWMHints(display, data->xwindow, wmhints);
+    X11_XFree(wmhints);
+
+    return 0;
+}
+
 #endif /* SDL_VIDEO_DRIVER_X11 */
diff --git a/src/video/x11/SDL_x11window.h b/src/video/x11/SDL_x11window.h
index ff6b98ba37ff..b9d6ab0d0ee3 100644
--- a/src/video/x11/SDL_x11window.h
+++ b/src/video/x11/SDL_x11window.h
@@ -116,6 +116,7 @@ extern int X11_SetWindowHitTest(SDL_Window *window, SDL_bool enabled);
 extern void X11_AcceptDragAndDrop(SDL_Window *window, SDL_bool accept);
 extern int X11_FlashWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_FlashOperation operation);
 extern void X11_ShowWindowSystemMenu(SDL_Window *window, int x, int y);
+extern int X11_SetWindowFocusable(SDL_VideoDevice *_this, SDL_Window *window, SDL_bool focusable);
 
 int SDL_X11_SetWindowTitle(Display *display, Window xwindow, char *title);
 void X11_UpdateWindowPosition(SDL_Window *window);