Browse Source

video: Add the concept of child popup windows

Add the CreatePopupWindow function to allow the creation of child tooltip and menu popup windows. Popup windows must be created as either a tooltip or popup menu and cannot be minimized, maximized, made fullscreen, or grab the mouse.

Child popup windows are tracked and will be recursively hidden, shown, or destroyed in tandem with the parent window.
Frank Praznik 2 years ago
parent
commit
e987c4a463

+ 47 - 0
include/SDL3/SDL_video.h

@@ -658,11 +658,58 @@ extern DECLSPEC Uint32 SDLCALL SDL_GetWindowPixelFormat(SDL_Window *window);
  *
  * \since This function is available since SDL 3.0.0.
  *
+ * \sa SDL_CreatePopupWindow
  * \sa SDL_CreateWindowFrom
  * \sa SDL_DestroyWindow
  */
 extern DECLSPEC SDL_Window *SDLCALL SDL_CreateWindow(const char *title, int w, int h, Uint32 flags);
 
+/**
+ * Create a child popup window of the specified parent window.
+ *
+ * 'flags' **must** contain exactly one of the following:
+ * - 'SDL_WINDOW_TOOLTIP': The popup window is a tooltip and will not pass any input events
+ * - 'SDL_WINDOW_POPUP_MENU': The popup window is a popup menu
+ *
+ * The following flags are not valid for popup windows and will be ignored:
+ * - 'SDL_WINDOW_MINIMIZED'
+ * - 'SDL_WINDOW_MAXIMIZED'
+ * - 'SDL_WINDOW_FULLSCREEN'
+ * - `SDL_WINDOW_BORDERLESS`
+ * - `SDL_WINDOW_MOUSE_GRABBED`
+ *
+ * The parent parameter **must** be non-null and a valid window.
+ * The parent of a popup window can be either a regular, toplevel window,
+ * or another popup window.
+ *
+ * Popup windows cannot be minimized, maximized, made fullscreen, or grab
+ * the mouse. Attempts to do so will fail.
+ *
+ * If a parent window is hidden, any child popup windows will be recursively
+ * hidden as well. Child popup windows not explicitly hidden will be restored
+ * when the parent is shown.
+ *
+ * If the parent window is destroyed, any child popup windows will be
+ * recursively destroyed as well.
+ *
+ * \param parent the parent of the window, must not be NULL
+ * \param offset_x the x position of the popup window relative to the origin
+ *                 of the parent, in screen coordinates
+ * \param offset_y the y position of the popup window relative to the origin
+ *                 of the parent window, in screen coordinates
+ * \param w the width of the window, in screen coordinates
+ * \param h the height of the window, in screen coordinates
+ * \param flags 0, or one or more SDL_WindowFlags OR'd together
+ * \returns the window that was created or NULL on failure; call
+ *          SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 3.0.0.
+ *
+ * \sa SDL_CreateWindow
+ * \sa SDL_DestroyWindow
+ */
+extern DECLSPEC SDL_Window *SDLCALL SDL_CreatePopupWindow(SDL_Window *parent, int offset_x, int offset_y, int w, int h, Uint32 flags);
+
 /**
  * Create an SDL window from an existing native window.
  *

+ 1 - 0
src/dynapi/SDL_dynapi.sym

@@ -839,6 +839,7 @@ SDL3_0.0.0 {
     SDL_GetRenderScale;
     SDL_GetRenderWindowSize;
     SDL_GetSystemTheme;
+    SDL_CreatePopupWindow;
     # extra symbols go here (don't modify this line)
   local: *;
 };

+ 1 - 0
src/dynapi/SDL_dynapi_overrides.h

@@ -866,3 +866,4 @@
 #define SDL_GetRenderScale SDL_GetRenderScale_REAL
 #define SDL_GetRenderWindowSize SDL_GetRenderWindowSize_REAL
 #define SDL_GetSystemTheme SDL_GetSystemTheme_REAL
+#define SDL_CreatePopupWindow SDL_CreatePopupWindow_REAL

+ 1 - 0
src/dynapi/SDL_dynapi_procs.h

@@ -911,3 +911,4 @@ SDL_DYNAPI_PROC(int,SDL_SetRenderScale,(SDL_Renderer *a, float b, float c),(a,b,
 SDL_DYNAPI_PROC(int,SDL_GetRenderScale,(SDL_Renderer *a, float *b, float *c),(a,b,c),return)
 SDL_DYNAPI_PROC(int,SDL_GetRenderWindowSize,(SDL_Renderer *a, int *b, int *c),(a,b,c),return)
 SDL_DYNAPI_PROC(SDL_SystemTheme,SDL_GetSystemTheme,(void),(),return)
+SDL_DYNAPI_PROC(SDL_Window*,SDL_CreatePopupWindow,(SDL_Window *a, int b, int c, int d, int e, Uint32 f),(a,b,c,d,e,f),return)

+ 11 - 3
src/events/SDL_windowevents.c

@@ -212,10 +212,18 @@ int SDL_SendWindowEvent(SDL_Window *window, SDL_EventType windowevent,
         break;
     }
 
-    if (windowevent == SDL_EVENT_WINDOW_CLOSE_REQUESTED) {
-        if (!window->prev && !window->next) {
+    if (windowevent == SDL_EVENT_WINDOW_CLOSE_REQUESTED && window->parent == NULL) {
+        int toplevel_count = 0;
+        SDL_Window *n;
+        for (n = SDL_GetVideoDevice()->windows; n != NULL; n = n->next) {
+            if (n->parent == NULL) {
+                ++toplevel_count;
+            }
+        }
+
+        if (toplevel_count == 1) {
             if (SDL_GetHintBoolean(SDL_HINT_QUIT_ON_LAST_WINDOW_CLOSE, SDL_TRUE)) {
-                SDL_SendQuit(); /* This is the last window in the list so send the SDL_EVENT_QUIT event */
+                SDL_SendQuit(); /* This is the last toplevel window in the list so send the SDL_EVENT_QUIT event */
             }
         }
     }

+ 13 - 0
src/video/SDL_sysvideo.h

@@ -98,6 +98,7 @@ struct SDL_Window
     SDL_bool surface_valid;
 
     SDL_bool is_hiding;
+    SDL_bool restore_on_show; /* Child was hidden recursively by the parent, restore when shown. */
     SDL_bool is_destroying;
     SDL_bool is_dropping; /* drag/drop in progress, expecting SDL_SendDropComplete(). */
 
@@ -114,12 +115,21 @@ struct SDL_Window
 
     SDL_Window *prev;
     SDL_Window *next;
+
+    SDL_Window *parent;
+    SDL_Window *first_child;
+    SDL_Window *prev_sibling;
+    SDL_Window *next_sibling;
 };
 #define SDL_WINDOW_FULLSCREEN_VISIBLE(W)        \
     ((((W)->flags & SDL_WINDOW_FULLSCREEN) != 0) &&   \
      (((W)->flags & SDL_WINDOW_HIDDEN) == 0) && \
      (((W)->flags & SDL_WINDOW_MINIMIZED) == 0))
 
+#define SDL_WINDOW_IS_POPUP(W)                   \
+    ((((W)->flags & SDL_WINDOW_TOOLTIP) != 0) || \
+    (((W)->flags & SDL_WINDOW_POPUP_MENU) != 0)) \
+                                                 \
 /*
  * Define the SDL display structure.
  * This corresponds to physical monitors attached to the system.
@@ -153,6 +163,7 @@ typedef enum
 {
     VIDEO_DEVICE_QUIRK_MODE_SWITCHING_EMULATED = 0x01,
     VIDEO_DEVICE_QUIRK_DISABLE_UNSET_FULLSCREEN_ON_MINIMIZE = 0x02,
+    VIDEO_DEVICE_QUIRK_HAS_POPUP_WINDOW_SUPPORT = 0x04,
 } DeviceQuirkFlags;
 
 struct SDL_VideoDevice
@@ -496,6 +507,8 @@ extern void SDL_GL_DeduceMaxSupportedESProfile(int *major, int *minor);
 
 extern int SDL_RecreateWindow(SDL_Window *window, Uint32 flags);
 extern SDL_bool SDL_HasWindows(void);
+extern void SDL_RelativeToGlobalForWindow(SDL_Window *window, int rel_x, int rel_y, int *abs_x, int *abs_y);
+extern void SDL_GlobalToRelativeForWindow(SDL_Window *window, int abs_x, int abs_y, int *rel_x, int *rel_y);
 
 extern void SDL_OnWindowShown(SDL_Window *window);
 extern void SDL_OnWindowHidden(SDL_Window *window);

+ 150 - 5
src/video/SDL_video.c

@@ -145,6 +145,12 @@ static VideoBootStrap *bootstrap[] = {
         return retval;                                                  \
     }                                                                   \
 
+#define CHECK_WINDOW_NOT_POPUP(window, retval)                          \
+    if (SDL_WINDOW_IS_POPUP(window)) {                                  \
+        SDL_SetError("Operation invalid on popup windows");             \
+        return retval;                                                  \
+    }
+
 #if defined(__MACOS__) && defined(SDL_VIDEO_DRIVER_COCOA)
 /* Support for macOS fullscreen spaces */
 extern SDL_bool Cocoa_IsWindowInFullscreenSpace(SDL_Window *window);
@@ -1230,6 +1236,46 @@ static SDL_DisplayID GetDisplayForRect(int x, int y, int w, int h)
     return closest;
 }
 
+void SDL_RelativeToGlobalForWindow(SDL_Window *window, int rel_x, int rel_y, int *abs_x, int *abs_y)
+{
+    SDL_Window *w;
+
+    if (SDL_WINDOW_IS_POPUP(window)) {
+        /* Calculate the total offset of the popup from the parents */
+        for (w = window->parent; w != NULL; w = w->parent) {
+            rel_x += w->x;
+            rel_y += w->y;
+        }
+    }
+
+    if (abs_x) {
+        *abs_x = rel_x;
+    }
+    if (abs_y) {
+        *abs_y = rel_y;
+    }
+}
+
+void SDL_GlobalToRelativeForWindow(SDL_Window *window, int abs_x, int abs_y, int *rel_x, int *rel_y)
+{
+    SDL_Window *w;
+
+    if (SDL_WINDOW_IS_POPUP(window)) {
+        /* Convert absolute window coordinates to relative for a popup */
+        for (w = window->parent; w != NULL; w = w->parent) {
+            abs_x -= w->x;
+            abs_y -= w->y;
+        }
+    }
+
+    if (rel_x) {
+        *rel_x = abs_x;
+    }
+    if (rel_y) {
+        *rel_y = abs_y;
+    }
+}
+
 SDL_DisplayID SDL_GetDisplayForPoint(const SDL_Point *point)
 {
     return GetDisplayForRect(point->x, point->y, 1, 1);
@@ -1242,6 +1288,7 @@ SDL_DisplayID SDL_GetDisplayForRect(const SDL_Rect *rect)
 
 static SDL_DisplayID SDL_GetDisplayForWindowPosition(SDL_Window *window)
 {
+    int x, y;
     SDL_DisplayID displayID = 0;
 
     CHECK_WINDOW_MAGIC(window, 0);
@@ -1254,8 +1301,10 @@ static SDL_DisplayID SDL_GetDisplayForWindowPosition(SDL_Window *window)
      * (for example if the window is off-screen), but other code may expect it
      * to succeed in that situation, so we fall back to a generic position-
      * based implementation in that case. */
+    SDL_RelativeToGlobalForWindow(window, window->x, window->y, &x, &y);
+
     if (!displayID) {
-        displayID = GetDisplayForRect(window->x, window->y, window->w, window->h);
+        displayID = GetDisplayForRect(x, y, window->w, window->h);
     }
     if (!displayID) {
         /* Use the primary display for a window if we can't find it anywhere else */
@@ -1536,6 +1585,7 @@ error:
 int SDL_SetWindowFullscreenMode(SDL_Window *window, const SDL_DisplayMode *mode)
 {
     CHECK_WINDOW_MAGIC(window, -1);
+    CHECK_WINDOW_NOT_POPUP(window, -1);
 
     if (mode) {
         if (!SDL_GetFullscreenModeMatch(mode)) {
@@ -1561,6 +1611,7 @@ int SDL_SetWindowFullscreenMode(SDL_Window *window, const SDL_DisplayMode *mode)
 const SDL_DisplayMode *SDL_GetWindowFullscreenMode(SDL_Window *window)
 {
     CHECK_WINDOW_MAGIC(window, NULL);
+    CHECK_WINDOW_NOT_POPUP(window, NULL);
 
     if (window->flags & SDL_WINDOW_FULLSCREEN) {
         return SDL_GetFullscreenModeMatch(&window->current_fullscreen_mode);
@@ -1668,12 +1719,10 @@ static int SDL_DllNotSupported(const char *name)
     return SDL_SetError("No dynamic %s support in current SDL video driver (%s)", name, _this->name);
 }
 
-SDL_Window *SDL_CreateWindow(const char *title, int w, int h, Uint32 flags)
+SDL_Window *SDL_CreateWindowInternal(const char *title, int x, int y, int w, int h, SDL_Window *parent, Uint32 flags)
 {
     SDL_Window *window;
     Uint32 type_flags, graphics_flags;
-    int x = SDL_WINDOWPOS_UNDEFINED;
-    int y = SDL_WINDOWPOS_UNDEFINED;
     SDL_bool undefined_x = SDL_FALSE;
     SDL_bool undefined_y = SDL_FALSE;
 
@@ -1701,6 +1750,12 @@ SDL_Window *SDL_CreateWindow(const char *title, int w, int h, Uint32 flags)
         return NULL;
     }
 
+    /* Tooltip and popup menu window must specify a parent window */
+    if (!parent && ((type_flags & SDL_WINDOW_TOOLTIP) || (type_flags & SDL_WINDOW_POPUP_MENU))) {
+        SDL_SetError("Tooltip and popup menu windows must specify a parent window");
+        return NULL;
+    }
+
     /* Some platforms can't create zero-sized windows */
     if (w < 1) {
         w = 1;
@@ -1824,6 +1879,16 @@ SDL_Window *SDL_CreateWindow(const char *title, int w, int h, Uint32 flags)
     }
     _this->windows = window;
 
+    if (parent) {
+        window->parent = parent;
+
+        window->next_sibling = parent->first_child;
+        if (parent->first_child) {
+            parent->first_child->prev_sibling = window;
+        }
+        parent->first_child = window;
+    }
+
     if (_this->CreateSDLWindow && _this->CreateSDLWindow(_this, window) < 0) {
         SDL_DestroyWindow(window);
         return NULL;
@@ -1866,6 +1931,33 @@ SDL_Window *SDL_CreateWindow(const char *title, int w, int h, Uint32 flags)
     return window;
 }
 
+SDL_Window *SDL_CreateWindow(const char *title, int w, int h, Uint32 flags)
+{
+    return SDL_CreateWindowInternal(title, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, w , h, NULL, flags);
+}
+
+SDL_Window *SDL_CreatePopupWindow(SDL_Window *parent, int offset_x, int offset_y, int w, int h, Uint32 flags)
+{
+    if (!(_this->quirk_flags & VIDEO_DEVICE_QUIRK_HAS_POPUP_WINDOW_SUPPORT)) {
+        SDL_Unsupported();
+        return NULL;
+    }
+
+    /* Parent must be a valid window */
+    CHECK_WINDOW_MAGIC(parent, NULL);
+
+    /* Remove invalid flags */
+    flags &= ~(SDL_WINDOW_MINIMIZED | SDL_WINDOW_MAXIMIZED | SDL_WINDOW_FULLSCREEN | SDL_WINDOW_MOUSE_GRABBED);
+
+    /* Popups must specify either the tooltip or popup menu window flags */
+    if ((flags & SDL_WINDOW_TOOLTIP) || (flags & SDL_WINDOW_POPUP_MENU)) {
+        return SDL_CreateWindowInternal(NULL, offset_x, offset_y, w, h, parent, flags);
+    }
+
+    SDL_SetError("Popup windows must specify either the 'SDL_WINDOW_TOOLTIP' or the 'SDL_WINDOW_POPUP_MENU' flag");
+    return NULL;
+}
+
 SDL_Window *SDL_CreateWindowFrom(const void *data)
 {
     SDL_Window *window;
@@ -2109,6 +2201,7 @@ Uint32 SDL_GetWindowFlags(SDL_Window *window)
 int SDL_SetWindowTitle(SDL_Window *window, const char *title)
 {
     CHECK_WINDOW_MAGIC(window, -1);
+    CHECK_WINDOW_NOT_POPUP(window, -1);
 
     if (title == window->title) {
         return 0;
@@ -2329,6 +2422,7 @@ int SDL_GetWindowPosition(SDL_Window *window, int *x, int *y)
 int SDL_SetWindowBordered(SDL_Window *window, SDL_bool bordered)
 {
     CHECK_WINDOW_MAGIC(window, -1);
+    CHECK_WINDOW_NOT_POPUP(window, -1);
     if (!(window->flags & SDL_WINDOW_FULLSCREEN)) {
         const int want = (bordered != SDL_FALSE); /* normalize the flag. */
         const int have = !(window->flags & SDL_WINDOW_BORDERLESS);
@@ -2347,6 +2441,7 @@ int SDL_SetWindowBordered(SDL_Window *window, SDL_bool bordered)
 int SDL_SetWindowResizable(SDL_Window *window, SDL_bool resizable)
 {
     CHECK_WINDOW_MAGIC(window, -1);
+    CHECK_WINDOW_NOT_POPUP(window, -1);
     if (!(window->flags & SDL_WINDOW_FULLSCREEN)) {
         const int want = (resizable != SDL_FALSE); /* normalize the flag. */
         const int have = ((window->flags & SDL_WINDOW_RESIZABLE) != 0);
@@ -2365,6 +2460,7 @@ int SDL_SetWindowResizable(SDL_Window *window, SDL_bool resizable)
 int SDL_SetWindowAlwaysOnTop(SDL_Window *window, SDL_bool on_top)
 {
     CHECK_WINDOW_MAGIC(window, -1);
+    CHECK_WINDOW_NOT_POPUP(window, -1);
     if (!(window->flags & SDL_WINDOW_FULLSCREEN)) {
         const int want = (on_top != SDL_FALSE); /* normalize the flag. */
         const int have = ((window->flags & SDL_WINDOW_ALWAYS_ON_TOP) != 0);
@@ -2583,27 +2679,54 @@ int SDL_GetWindowMaximumSize(SDL_Window *window, int *max_w, int *max_h)
 
 int SDL_ShowWindow(SDL_Window *window)
 {
+    SDL_Window *child;
     CHECK_WINDOW_MAGIC(window, -1);
 
     if (!(window->flags & SDL_WINDOW_HIDDEN)) {
         return 0;
     }
 
+    /* If the parent is hidden, set the flag to restore this when the parent is shown */
+    if (window->parent && (window->parent->flags & SDL_WINDOW_HIDDEN)) {
+        window->restore_on_show = SDL_TRUE;
+        return 0;
+    }
+
     if (_this->ShowWindow) {
         _this->ShowWindow(_this, window);
     }
     SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_SHOWN, 0, 0);
+
+    /* Restore child windows */
+    for (child = window->first_child; child != NULL; child = child->next_sibling) {
+        if (!child->restore_on_show && (child->flags & SDL_WINDOW_HIDDEN)) {
+            break;
+        }
+        SDL_ShowWindow(child);
+        child->restore_on_show = SDL_FALSE;
+    }
     return 0;
 }
 
 int SDL_HideWindow(SDL_Window *window)
 {
+    SDL_Window *child;
     CHECK_WINDOW_MAGIC(window, -1);
 
     if (window->flags & SDL_WINDOW_HIDDEN) {
+        window->restore_on_show = SDL_FALSE;
         return 0;
     }
 
+    /* Hide all child windows */
+    for (child = window->first_child; child != NULL; child = child->next_sibling) {
+        if (child->flags & SDL_WINDOW_HIDDEN) {
+            break;
+        }
+        SDL_HideWindow(child);
+        child->restore_on_show = SDL_TRUE;
+    }
+
     window->is_hiding = SDL_TRUE;
     if (_this->HideWindow) {
         _this->HideWindow(_this, window);
@@ -2629,6 +2752,7 @@ int SDL_RaiseWindow(SDL_Window *window)
 int SDL_MaximizeWindow(SDL_Window *window)
 {
     CHECK_WINDOW_MAGIC(window, -1);
+    CHECK_WINDOW_NOT_POPUP(window, -1);
 
     if (window->flags & SDL_WINDOW_MAXIMIZED) {
         return 0;
@@ -2644,7 +2768,7 @@ int SDL_MaximizeWindow(SDL_Window *window)
 
 static SDL_bool SDL_CanMinimizeWindow(SDL_Window *window)
 {
-    if (!_this->MinimizeWindow) {
+    if (!_this->MinimizeWindow || SDL_WINDOW_IS_POPUP(window)) {
         return SDL_FALSE;
     }
     return SDL_TRUE;
@@ -2653,6 +2777,7 @@ static SDL_bool SDL_CanMinimizeWindow(SDL_Window *window)
 int SDL_MinimizeWindow(SDL_Window *window)
 {
     CHECK_WINDOW_MAGIC(window, -1);
+    CHECK_WINDOW_NOT_POPUP(window, -1);
 
     if (window->flags & SDL_WINDOW_MINIMIZED) {
         return 0;
@@ -2675,6 +2800,7 @@ int SDL_MinimizeWindow(SDL_Window *window)
 int SDL_RestoreWindow(SDL_Window *window)
 {
     CHECK_WINDOW_MAGIC(window, -1);
+    CHECK_WINDOW_NOT_POPUP(window, -1);
 
     if (!(window->flags & (SDL_WINDOW_MAXIMIZED | SDL_WINDOW_MINIMIZED))) {
         return 0;
@@ -2692,6 +2818,7 @@ int SDL_SetWindowFullscreen(SDL_Window *window, SDL_bool fullscreen)
     Uint32 flags = fullscreen ? SDL_WINDOW_FULLSCREEN : 0;
 
     CHECK_WINDOW_MAGIC(window, -1);
+    CHECK_WINDOW_NOT_POPUP(window, -1);
 
     if (flags == (window->flags & SDL_WINDOW_FULLSCREEN)) {
         return 0;
@@ -2952,6 +3079,7 @@ void SDL_UpdateWindowGrab(SDL_Window *window)
 int SDL_SetWindowGrab(SDL_Window *window, SDL_bool grabbed)
 {
     CHECK_WINDOW_MAGIC(window, -1);
+    CHECK_WINDOW_NOT_POPUP(window, -1);
 
     SDL_SetWindowMouseGrab(window, grabbed);
 
@@ -3236,6 +3364,23 @@ void SDL_DestroyWindow(SDL_Window *window)
 
     window->is_destroying = SDL_TRUE;
 
+    /* Destroy any child windows of this window */
+    while (window->first_child) {
+        SDL_DestroyWindow(window->first_child);
+    }
+
+    /* If this is a child window, unlink it from its siblings */
+    if (window->parent) {
+        if (window->next_sibling) {
+            window->next_sibling->prev_sibling = window->prev_sibling;
+        }
+        if (window->prev_sibling) {
+            window->prev_sibling->next_sibling = window->next_sibling;
+        } else {
+            window->parent->first_child = window->next_sibling;
+        }
+    }
+
     /* Restore video mode, etc. */
     SDL_UpdateFullscreenMode(window, SDL_FALSE);
     if (!(window->flags & SDL_WINDOW_FOREIGN)) {