Forráskód Böngészése

Add support for modal windows to more platforms

- Adds support for modal windows to Win32, Mac, and Haiku, and enhances functionality on Wayland and X11, which previous set only the parent window, but not the modal state.
- Windows can be declared modal at creation time, and the modal state can be toggled at any time via SDL_SetWindowModalFor() (tested with UE5 through sdl2-compat).
- Allows dynamic unparenting/reparenting of windows.
- Includes a modal window test.
Semphris 1 éve
szülő
commit
c6a70d6898

+ 14 - 3
include/SDL3/SDL_video.h

@@ -144,6 +144,7 @@ typedef Uint32 SDL_WindowFlags;
 #define SDL_WINDOW_INPUT_FOCUS          0x00000200U /**< window has input focus */
 #define SDL_WINDOW_MOUSE_FOCUS          0x00000400U /**< window has mouse focus */
 #define SDL_WINDOW_EXTERNAL             0x00000800U /**< window not created by SDL */
+#define SDL_WINDOW_MODAL                0x00001000U /**< window is modal */
 #define SDL_WINDOW_HIGH_PIXEL_DENSITY   0x00002000U /**< window uses high pixel density back buffer if possible */
 #define SDL_WINDOW_MOUSE_CAPTURE        0x00004000U /**< window has mouse captured (unrelated to MOUSE_GRABBED) */
 #define SDL_WINDOW_ALWAYS_ON_TOP        0x00008000U /**< window should always be above others */
@@ -907,13 +908,15 @@ extern DECLSPEC SDL_Window *SDLCALL SDL_CreatePopupWindow(SDL_Window *parent, in
  *   with Metal rendering
  * - `SDL_PROP_WINDOW_CREATE_MINIMIZED_BOOLEAN`: true if the window should
  *   start minimized
+ * - `SDL_PROP_WINDOW_CREATE_MODAL_BOOLEAN`: true if the window is modal to its
+ *   parent
  * - `SDL_PROP_WINDOW_CREATE_MOUSE_GRABBED_BOOLEAN`: true if the window starts
  *   with grabbed mouse focus
  * - `SDL_PROP_WINDOW_CREATE_OPENGL_BOOLEAN`: true if the window will be used
  *   with OpenGL rendering
  * - `SDL_PROP_WINDOW_CREATE_PARENT_POINTER`: an SDL_Window that will be the
- *   parent of this window, required for windows with the "toolip" and "menu"
- *   properties
+ *   parent of this window, required for windows with the "toolip", "menu", and
+ *   "modal" properties
  * - `SDL_PROP_WINDOW_CREATE_RESIZABLE_BOOLEAN`: true if the window should be
  *   resizable
  * - `SDL_PROP_WINDOW_CREATE_TITLE_STRING`: the title of the window, in UTF-8
@@ -1008,6 +1011,7 @@ extern DECLSPEC SDL_Window *SDLCALL SDL_CreateWindowWithProperties(SDL_Propertie
 #define SDL_PROP_WINDOW_CREATE_MENU_BOOLEAN                        "menu"
 #define SDL_PROP_WINDOW_CREATE_METAL_BOOLEAN                       "metal"
 #define SDL_PROP_WINDOW_CREATE_MINIMIZED_BOOLEAN                   "minimized"
+#define SDL_PROP_WINDOW_CREATE_MODAL_BOOLEAN                       "modal"
 #define SDL_PROP_WINDOW_CREATE_MOUSE_GRABBED_BOOLEAN               "mouse_grabbed"
 #define SDL_PROP_WINDOW_CREATE_OPENGL_BOOLEAN                      "opengl"
 #define SDL_PROP_WINDOW_CREATE_PARENT_POINTER                      "parent"
@@ -2000,7 +2004,12 @@ extern DECLSPEC int SDLCALL SDL_SetWindowOpacity(SDL_Window *window, float opaci
 extern DECLSPEC int SDLCALL SDL_GetWindowOpacity(SDL_Window *window, float *out_opacity);
 
 /**
- * Set the window as a modal for another window.
+ * Set the window as a modal to a parent window.
+ *
+ * If the window is already modal to an existing window, it will be reparented to the new owner.
+ * Setting the parent window to null unparents the modal window and removes modal status.
+ *
+ * Setting a window as modal to a parent that is a descendent of the modal window results in undefined behavior.
  *
  * \param modal_window the window that should be set modal
  * \param parent_window the parent window for the modal window
@@ -2181,6 +2190,8 @@ extern DECLSPEC int SDLCALL SDL_FlashWindow(SDL_Window *window, SDL_FlashOperati
 /**
  * Destroy a window.
  *
+ * Any popups or modal windows owned by the window will be recursively destroyed as well.
+ *
  * If `window` is NULL, this function will return immediately after setting
  * the SDL error message to "Invalid window". See SDL_GetError().
  *

+ 82 - 22
src/video/SDL_video.c

@@ -200,6 +200,33 @@ static void SDL_SyncIfRequired(SDL_Window *window)
     }
 }
 
+static void SDL_SetWindowParent(SDL_Window *window, SDL_Window *parent)
+{
+    /* Unlink the window from the existing parent. */
+    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;
+        }
+
+        window->parent = NULL;
+    }
+
+    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;
+    }
+}
+
 /* Support for framebuffer emulation using an accelerated renderer */
 
 #define SDL_PROP_WINDOW_TEXTUREDATA_POINTER "SDL.internal.window.texturedata"
@@ -2002,6 +2029,7 @@ static struct {
     { SDL_PROP_WINDOW_CREATE_MENU_BOOLEAN,               SDL_WINDOW_POPUP_MENU,          SDL_FALSE },
     { SDL_PROP_WINDOW_CREATE_METAL_BOOLEAN,              SDL_WINDOW_METAL,               SDL_FALSE },
     { SDL_PROP_WINDOW_CREATE_MINIMIZED_BOOLEAN,          SDL_WINDOW_MINIMIZED,           SDL_FALSE },
+    { SDL_PROP_WINDOW_CREATE_MODAL_BOOLEAN,              SDL_WINDOW_MODAL,               SDL_FALSE },
     { SDL_PROP_WINDOW_CREATE_MOUSE_GRABBED_BOOLEAN,      SDL_WINDOW_MOUSE_GRABBED,       SDL_FALSE },
     { SDL_PROP_WINDOW_CREATE_OPENGL_BOOLEAN,             SDL_WINDOW_OPENGL,              SDL_FALSE },
     { SDL_PROP_WINDOW_CREATE_RESIZABLE_BOOLEAN,          SDL_WINDOW_RESIZABLE,           SDL_FALSE },
@@ -2057,6 +2085,11 @@ SDL_Window *SDL_CreateWindowWithProperties(SDL_PropertiesID props)
         }
     }
 
+    if ((flags & SDL_WINDOW_MODAL) && (!parent || parent->magic != &_this->window_magic)) {
+        SDL_SetError("Modal windows must specify a parent window");
+        return NULL;
+    }
+
     if ((flags & (SDL_WINDOW_TOOLTIP | SDL_WINDOW_POPUP_MENU)) != 0) {
         if (!(_this->device_caps & VIDEO_DEVICE_CAPS_HAS_POPUP_WINDOW_SUPPORT)) {
             SDL_Unsupported();
@@ -2074,7 +2107,7 @@ SDL_Window *SDL_CreateWindowWithProperties(SDL_PropertiesID props)
     }
 
     /* Ensure no more than one of these flags is set */
-    type_flags = flags & (SDL_WINDOW_UTILITY | SDL_WINDOW_TOOLTIP | SDL_WINDOW_POPUP_MENU);
+    type_flags = flags & (SDL_WINDOW_UTILITY | SDL_WINDOW_TOOLTIP | SDL_WINDOW_POPUP_MENU | SDL_WINDOW_MODAL);
     if (type_flags & (type_flags - 1)) {
         SDL_SetError("Conflicting window type flags specified: 0x%.8x", (unsigned int)type_flags);
         return NULL;
@@ -2200,14 +2233,9 @@ SDL_Window *SDL_CreateWindowWithProperties(SDL_PropertiesID props)
     }
     _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;
+    /* Set the parent before creation if this is non-modal, otherwise it will be set later. */
+    if (!(flags & SDL_WINDOW_MODAL)) {
+        SDL_SetWindowParent(window, parent);
     }
 
     if (_this->CreateSDLWindow && _this->CreateSDLWindow(_this, window, props) < 0) {
@@ -2236,6 +2264,9 @@ SDL_Window *SDL_CreateWindowWithProperties(SDL_PropertiesID props)
     flags = window->flags;
 #endif
 
+    if (flags & SDL_WINDOW_MODAL) {
+        SDL_SetWindowModalFor(window, parent);
+    }
     if (title) {
         SDL_SetWindowTitle(window, title);
     }
@@ -2293,6 +2324,7 @@ int SDL_RecreateWindow(SDL_Window *window, SDL_WindowFlags flags)
     SDL_bool need_vulkan_unload = SDL_FALSE;
     SDL_bool need_vulkan_load = SDL_FALSE;
     SDL_WindowFlags graphics_flags;
+    SDL_Window *parent = window->parent;
 
     /* ensure no more than one of these flags is set */
     graphics_flags = flags & (SDL_WINDOW_OPENGL | SDL_WINDOW_METAL | SDL_WINDOW_VULKAN);
@@ -2317,6 +2349,11 @@ int SDL_RecreateWindow(SDL_Window *window, SDL_WindowFlags flags)
         flags &= ~SDL_WINDOW_EXTERNAL;
     }
 
+    /* If this is a modal dialog, clear the modal status. */
+    if (window->flags & SDL_WINDOW_MODAL) {
+        SDL_SetWindowModalFor(window, NULL);
+    }
+
     /* Restore video mode, etc. */
     if (!(window->flags & SDL_WINDOW_EXTERNAL)) {
         const SDL_bool restore_on_show = window->restore_on_show;
@@ -2410,6 +2447,10 @@ int SDL_RecreateWindow(SDL_Window *window, SDL_WindowFlags flags)
         window->flags |= SDL_WINDOW_EXTERNAL;
     }
 
+    if (flags & SDL_WINDOW_MODAL) {
+        SDL_SetWindowModalFor(window, parent);
+    }
+
     if (_this->SetWindowTitle && window->title) {
         _this->SetWindowTitle(_this, window);
     }
@@ -3259,15 +3300,35 @@ int SDL_GetWindowOpacity(SDL_Window *window, float *out_opacity)
 int SDL_SetWindowModalFor(SDL_Window *modal_window, SDL_Window *parent_window)
 {
     CHECK_WINDOW_MAGIC(modal_window, -1);
-    CHECK_WINDOW_MAGIC(parent_window, -1);
     CHECK_WINDOW_NOT_POPUP(modal_window, -1);
-    CHECK_WINDOW_NOT_POPUP(parent_window, -1);
+
+    if (parent_window) {
+        CHECK_WINDOW_MAGIC(parent_window, -1);
+        CHECK_WINDOW_NOT_POPUP(parent_window, -1);
+    }
 
     if (!_this->SetWindowModalFor) {
         return SDL_Unsupported();
     }
 
-    return _this->SetWindowModalFor(_this, modal_window, parent_window);
+    if (parent_window) {
+        modal_window->flags |= SDL_WINDOW_MODAL;
+    } else if (modal_window->flags & SDL_WINDOW_MODAL) {
+        modal_window->flags &= ~SDL_WINDOW_MODAL;
+    } else {
+        return 0; /* Not modal; nothing to do. */
+    }
+
+    const int ret = _this->SetWindowModalFor(_this, modal_window, parent_window);
+
+    /* The existing parent might be needed when changing the modal status,
+     * so don't change the heirarchy until after setting the new modal state.
+     */
+    if (!ret) {
+        SDL_SetWindowParent(modal_window, !ret ? parent_window : NULL);
+    }
+
+    return ret;
 }
 
 int SDL_SetWindowInputFocus(SDL_Window *window)
@@ -3686,16 +3747,12 @@ void SDL_DestroyWindow(SDL_Window *window)
 
     SDL_DestroyProperties(window->props);
 
-    /* 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;
-        }
+    /* Clear the modal status, but don't unset the parent, as it may be
+     * needed later in the destruction process if a backend needs to
+     * update the input focus.
+     */
+    if (_this->SetWindowModalFor && (window->flags & SDL_WINDOW_MODAL)) {
+        _this->SetWindowModalFor(_this, window, NULL);
     }
 
     /* Restore video mode, etc. */
@@ -3765,6 +3822,9 @@ void SDL_DestroyWindow(SDL_Window *window)
     SDL_free(window->title);
     SDL_DestroySurface(window->icon);
 
+    /* Unlink the window from its siblings. */
+    SDL_SetWindowParent(window, NULL);
+
     /* Unlink the window from the list */
     if (window->next) {
         window->next->prev = window->prev;

+ 8 - 0
src/video/cocoa/SDL_cocoaevents.m

@@ -563,6 +563,14 @@ Uint64 Cocoa_GetEventTimestamp(NSTimeInterval nsTimestamp)
 
 int Cocoa_PumpEventsUntilDate(SDL_VideoDevice *_this, NSDate *expiration, bool accumulate)
 {
+    /* Run any existing modal sessions. */
+    for (SDL_Window *w = _this->windows; w; w = w->next) {
+        SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)w->driverdata;
+        if (data.modal_session) {
+            [NSApp runModalSession:data.modal_session];
+        }
+    }
+
     for (;;) {
         NSEvent *event = [NSApp nextEventMatchingMask:NSEventMaskAny untilDate:expiration inMode:NSDefaultRunLoopMode dequeue:YES];
         if (event == nil) {

+ 1 - 0
src/video/cocoa/SDL_cocoavideo.m

@@ -122,6 +122,7 @@ static SDL_VideoDevice *Cocoa_CreateDevice(void)
         device->UpdateWindowShape = Cocoa_UpdateWindowShape;
         device->FlashWindow = Cocoa_FlashWindow;
         device->SetWindowFocusable = Cocoa_SetWindowFocusable;
+        device->SetWindowModalFor = Cocoa_SetWindowModalFor;
         device->SyncWindow = Cocoa_SyncWindow;
 
 #ifdef SDL_VIDEO_OPENGL_CGL

+ 2 - 0
src/video/cocoa/SDL_cocoawindow.h

@@ -138,6 +138,7 @@ typedef enum
 @property(nonatomic) NSInteger flash_request;
 @property(nonatomic) SDL_Window *keyboard_focus;
 @property(nonatomic) Cocoa_WindowListener *listener;
+@property(nonatomic) NSModalSession modal_session;
 @property(nonatomic) SDL_CocoaVideoData *videodata;
 @property(nonatomic) SDL_bool send_floating_size;
 @property(nonatomic) SDL_bool send_floating_position;
@@ -178,6 +179,7 @@ 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);
+extern int Cocoa_SetWindowModalFor(SDL_VideoDevice *_this, SDL_Window *modal_window, SDL_Window *parent_window);
 extern int Cocoa_SyncWindow(SDL_VideoDevice *_this, SDL_Window *window);
 
 #endif /* SDL_cocoawindow_h_ */

+ 27 - 0
src/video/cocoa/SDL_cocoawindow.m

@@ -2369,6 +2369,10 @@ void Cocoa_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window)
                 NSWindow *nsparent = ((__bridge SDL_CocoaWindowData *)window->parent->driverdata).nswindow;
                 [nsparent addChildWindow:nswindow ordered:NSWindowAbove];
             } else {
+                if ((window->flags & SDL_WINDOW_MODAL) && window->parent) {
+                    Cocoa_SetWindowModalFor(_this, window, window->parent);
+                }
+
                 if (bActivate) {
                     [nswindow makeKeyAndOrderFront:nil];
                 } else {
@@ -2402,6 +2406,11 @@ void Cocoa_HideWindow(SDL_VideoDevice *_this, SDL_Window *window)
             [nswindow close];
         }
 
+        /* If this window is the source of a modal session, end it when
+         * hidden, or other windows will be prevented from closing.
+         */
+        Cocoa_SetWindowModalFor(_this, window, NULL);
+
         /* Transfer keyboard focus back to the parent */
         if (window->flags & SDL_WINDOW_POPUP_MENU) {
             if (window == SDL_GetKeyboardFocus()) {
@@ -2928,6 +2937,24 @@ void Cocoa_AcceptDragAndDrop(SDL_Window *window, SDL_bool accept)
     }
 }
 
+int Cocoa_SetWindowModalFor(SDL_VideoDevice *_this, SDL_Window *modal_window, SDL_Window *parent_window)
+{
+    @autoreleasepool {
+        SDL_CocoaWindowData *modal_data = (__bridge SDL_CocoaWindowData *)modal_window->driverdata;
+
+        if (modal_data.modal_session) {
+            [NSApp endModalSession:modal_data.modal_session];
+            modal_data.modal_session = nil;
+        }
+
+        if (parent_window) {
+            modal_data.modal_session = [NSApp beginModalSessionForWindow:modal_data.nswindow];
+        }
+    }
+
+    return 0;
+}
+
 int Cocoa_FlashWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_FlashOperation operation)
 {
     @autoreleasepool {

+ 21 - 2
src/video/haiku/SDL_bwindow.cc

@@ -39,7 +39,7 @@ static SDL_INLINE SDL_BLooper *_GetBeLooper() {
     return SDL_Looper;
 }
 
-static int _InitWindow(SDL_VideoDevice *_this, SDL_Window *window) {
+static int _InitWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID create_props) {
     uint32 flags = 0;
     window_look look = B_TITLED_WINDOW_LOOK;
 
@@ -77,7 +77,7 @@ static int _InitWindow(SDL_VideoDevice *_this, SDL_Window *window) {
 }
 
 int HAIKU_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID create_props) {
-    if (_InitWindow(_this, window) < 0) {
+    if (_InitWindow(_this, window, create_props) < 0) {
         return -1;
     }
 
@@ -171,6 +171,25 @@ int HAIKU_SetWindowMouseGrab(SDL_VideoDevice *_this, SDL_Window * window, SDL_bo
     return SDL_Unsupported();
 }
 
+int HAIKU_SetWindowModalFor(SDL_VideoDevice *_this, SDL_Window *modal_window, SDL_Window *parent_window) {
+    if (modal_window->parent && modal_window->parent != parent_window) {
+        /* Remove from the subset of a previous parent. */
+        _ToBeWin(modal_window)->RemoveFromSubset(_ToBeWin(modal_window->parent));
+    }
+
+    if (parent_window) {
+        _ToBeWin(modal_window)->SetLook(B_MODAL_WINDOW_LOOK);
+        _ToBeWin(modal_window)->SetFeel(B_MODAL_SUBSET_WINDOW_FEEL);
+        _ToBeWin(modal_window)->AddToSubset(_ToBeWin(parent_window));
+    } else {
+        window_look look = (modal_window->flags & SDL_WINDOW_BORDERLESS) ? B_NO_BORDER_WINDOW_LOOK : B_TITLED_WINDOW_LOOK;
+        _ToBeWin(modal_window)->SetLook(look);
+        _ToBeWin(modal_window)->SetFeel(B_NORMAL_WINDOW_FEEL);
+    }
+
+    return 0;
+}
+
 void HAIKU_DestroyWindow(SDL_VideoDevice *_this, SDL_Window * window) {
     _ToBeWin(window)->LockLooper();    /* This MUST be locked */
     _GetBeLooper()->ClearID(_ToBeWin(window));

+ 8 - 0
src/video/wayland/SDL_waylandvideo.c

@@ -57,6 +57,7 @@
 #include "viewporter-client-protocol.h"
 #include "xdg-activation-v1-client-protocol.h"
 #include "xdg-decoration-unstable-v1-client-protocol.h"
+#include "xdg-dialog-v1-client-protocol.h"
 #include "xdg-foreign-unstable-v2-client-protocol.h"
 #include "xdg-output-unstable-v1-client-protocol.h"
 #include "xdg-shell-client-protocol.h"
@@ -1088,6 +1089,8 @@ static void display_handle_global(void *data, struct wl_registry *registry, uint
         }
     } else if (SDL_strcmp(interface, "zxdg_exporter_v2") == 0) {
         d->zxdg_exporter_v2 = wl_registry_bind(d->registry, id, &zxdg_exporter_v2_interface, 1);
+    } else if (SDL_strcmp(interface, "xdg_wm_dialog_v1") == 0) {
+        d->xdg_wm_dialog_v1 = wl_registry_bind(d->registry, id, &xdg_wm_dialog_v1_interface, 1);
     } else if (SDL_strcmp(interface, "kde_output_order_v1") == 0) {
         d->kde_output_order = wl_registry_bind(d->registry, id, &kde_output_order_v1_interface, 1);
         kde_output_order_v1_add_listener(d->kde_output_order, &kde_output_order_listener, d);
@@ -1346,6 +1349,11 @@ static void Wayland_VideoCleanup(SDL_VideoDevice *_this)
         data->zxdg_exporter_v2 = NULL;
     }
 
+    if (data->xdg_wm_dialog_v1) {
+        xdg_wm_dialog_v1_destroy(data->xdg_wm_dialog_v1);
+        data->xdg_wm_dialog_v1 = NULL;
+    }
+
     if (data->kde_output_order) {
         Wayland_FlushOutputOrder(data);
         kde_output_order_v1_destroy(data->kde_output_order);

+ 1 - 0
src/video/wayland/SDL_waylandvideo.h

@@ -80,6 +80,7 @@ struct SDL_VideoData
     struct wp_fractional_scale_manager_v1 *fractional_scale_manager;
     struct zwp_input_timestamps_manager_v1 *input_timestamps_manager;
     struct zxdg_exporter_v2 *zxdg_exporter_v2;
+    struct xdg_wm_dialog_v1 *xdg_wm_dialog_v1;
     struct kde_output_order_v1 *kde_output_order;
 
     struct xkb_context *xkb_context;

+ 53 - 21
src/video/wayland/SDL_waylandwindow.c

@@ -39,6 +39,7 @@
 #include "viewporter-client-protocol.h"
 #include "fractional-scale-v1-client-protocol.h"
 #include "xdg-foreign-unstable-v2-client-protocol.h"
+#include "xdg-dialog-v1-client-protocol.h"
 
 #ifdef HAVE_LIBDECOR_H
 #include <libdecor.h>
@@ -654,6 +655,8 @@ static void surface_frame_done(void *data, struct wl_callback *cb, uint32_t time
         for (SDL_Window *w = wind->sdlwindow->first_child; w; w = w->next_sibling) {
             if (w->driverdata->surface_status == WAYLAND_SURFACE_STATUS_SHOW_PENDING) {
                 Wayland_ShowWindow(SDL_GetVideoDevice(), w);
+            } else if ((w->flags & SDL_WINDOW_MODAL) && w->driverdata->modal_reparenting_required) {
+                Wayland_SetWindowModalFor(SDL_GetVideoDevice(), w, w->parent);
             }
         }
 
@@ -1434,35 +1437,56 @@ int Wayland_SetWindowModalFor(SDL_VideoDevice *_this, SDL_Window *modal_window,
 {
     SDL_VideoData *viddata = _this->driverdata;
     SDL_WindowData *modal_data = modal_window->driverdata;
-    SDL_WindowData *parent_data = parent_window->driverdata;
+    SDL_WindowData *parent_data = parent_window ? parent_window->driverdata : NULL;
+    struct xdg_toplevel *modal_toplevel = NULL;
+    struct xdg_toplevel *parent_toplevel = NULL;
 
-    if (modal_data->shell_surface_type == WAYLAND_SURFACE_XDG_POPUP || parent_data->shell_surface_type == WAYLAND_SURFACE_XDG_POPUP) {
-        return SDL_SetError("Modal/Parent was a popup, not a toplevel");
+    modal_data->modal_reparenting_required = SDL_FALSE;
+
+    if (parent_data && parent_data->surface_status != WAYLAND_SURFACE_STATUS_SHOWN) {
+        /* Need to wait for the parent to become mapped, or it's the same as setting a null parent. */
+        modal_data->modal_reparenting_required = SDL_TRUE;
+        return 0;
     }
 
+    /* Libdecor crashes on attempts to unset the parent by passing null, which is allowed by the
+     * toplevel spec, so just use the raw xdg-toplevel instead (that's what libdecor does
+     * internally anyways).
+     */
 #ifdef HAVE_LIBDECOR_H
-    if (viddata->shell.libdecor) {
-        if (!modal_data->shell_surface.libdecor.frame) {
-            return SDL_SetError("Modal window was hidden");
-        }
-        if (!parent_data->shell_surface.libdecor.frame) {
-            return SDL_SetError("Parent window was hidden");
-        }
-        libdecor_frame_set_parent(modal_data->shell_surface.libdecor.frame,
-                                  parent_data->shell_surface.libdecor.frame);
+    if (modal_data->shell_surface_type == WAYLAND_SURFACE_LIBDECOR && modal_data->shell_surface.libdecor.frame) {
+        modal_toplevel = libdecor_frame_get_xdg_toplevel(modal_data->shell_surface.libdecor.frame);
     } else
 #endif
-        if (viddata->shell.xdg) {
-        if (modal_data->shell_surface.xdg.roleobj.toplevel == NULL) {
-            return SDL_SetError("Modal window was hidden");
+        if (modal_data->shell_surface_type == WAYLAND_SURFACE_XDG_TOPLEVEL && modal_data->shell_surface.xdg.roleobj.toplevel) {
+        modal_toplevel = modal_data->shell_surface.xdg.roleobj.toplevel;
+    }
+
+    if (parent_data) {
+#ifdef HAVE_LIBDECOR_H
+        if (parent_data->shell_surface_type == WAYLAND_SURFACE_LIBDECOR && parent_data->shell_surface.libdecor.frame) {
+            parent_toplevel = libdecor_frame_get_xdg_toplevel(parent_data->shell_surface.libdecor.frame);
+        } else
+#endif
+            if (parent_data->shell_surface_type == WAYLAND_SURFACE_XDG_TOPLEVEL && parent_data->shell_surface.xdg.roleobj.toplevel) {
+            parent_toplevel = parent_data->shell_surface.xdg.roleobj.toplevel;
         }
-        if (parent_data->shell_surface.xdg.roleobj.toplevel == NULL) {
-            return SDL_SetError("Parent window was hidden");
+    }
+
+    if (modal_toplevel) {
+        xdg_toplevel_set_parent(modal_toplevel, parent_toplevel);
+
+        if (viddata->xdg_wm_dialog_v1) {
+            if (parent_toplevel) {
+                if (!modal_data->xdg_dialog_v1) {
+                    modal_data->xdg_dialog_v1 = xdg_wm_dialog_v1_get_xdg_dialog(viddata->xdg_wm_dialog_v1, modal_toplevel);
+                }
+
+                xdg_dialog_v1_set_modal(modal_data->xdg_dialog_v1);
+            } else if (modal_data->xdg_dialog_v1) {
+                xdg_dialog_v1_unset_modal(modal_data->xdg_dialog_v1);
+            }
         }
-        xdg_toplevel_set_parent(modal_data->shell_surface.xdg.roleobj.toplevel,
-                                parent_data->shell_surface.xdg.roleobj.toplevel);
-    } else {
-        return SDL_Unsupported();
     }
 
     return 0;
@@ -1653,6 +1677,10 @@ void Wayland_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window)
     }
 
     /* Restore state that was set prior to this call */
+    if (window->flags & SDL_WINDOW_MODAL) {
+        Wayland_SetWindowModalFor(_this, window, window->parent);
+    }
+
     Wayland_SetWindowTitle(_this, window);
 
     /* We have to wait until the surface gets a "configure" event, or use of
@@ -2590,6 +2618,10 @@ void Wayland_DestroyWindow(SDL_VideoDevice *_this, SDL_Window *window)
             wp_fractional_scale_v1_destroy(wind->fractional_scale);
         }
 
+        if (wind->xdg_dialog_v1) {
+            xdg_dialog_v1_destroy(wind->xdg_dialog_v1);
+        }
+
         SDL_free(wind->outputs);
         SDL_free(wind->app_id);
 

+ 2 - 0
src/video/wayland/SDL_waylandwindow.h

@@ -96,6 +96,7 @@ struct SDL_WindowData
     struct wp_viewport *viewport;
     struct wp_fractional_scale_v1 *fractional_scale;
     struct zxdg_exported_v2 *exported;
+    struct xdg_dialog_v1 *xdg_dialog_v1;
 
     SDL_AtomicInt swap_interval_ready;
 
@@ -172,6 +173,7 @@ struct SDL_WindowData
     SDL_bool fullscreen_was_positioned;
     SDL_bool show_hide_sync_required;
     SDL_bool scale_to_display;
+    SDL_bool modal_reparenting_required;
 
     SDL_HitTestResult hit_test_result;
 

+ 1 - 0
src/video/windows/SDL_windowsvideo.c

@@ -202,6 +202,7 @@ static SDL_VideoDevice *WIN_CreateDevice(void)
     device->SetWindowResizable = WIN_SetWindowResizable;
     device->SetWindowAlwaysOnTop = WIN_SetWindowAlwaysOnTop;
     device->SetWindowFullscreen = WIN_SetWindowFullscreen;
+    device->SetWindowModalFor = WIN_SetWindowModalFor;
 #if !defined(SDL_PLATFORM_XBOXONE) && !defined(SDL_PLATFORM_XBOXSERIES)
     device->GetWindowICCProfile = WIN_GetWindowICCProfile;
     device->SetWindowMouseRect = WIN_SetWindowMouseRect;

+ 44 - 0
src/video/windows/SDL_windowswindow.c

@@ -984,6 +984,10 @@ void WIN_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window)
         WIN_SetWindowPosition(_this, window);
     }
 
+    if (window->flags & SDL_WINDOW_MODAL) {
+        EnableWindow(window->parent->driverdata->hwnd, FALSE);
+    }
+
     hwnd = window->driverdata->hwnd;
     style = GetWindowLong(hwnd, GWL_EXSTYLE);
     if (style & WS_EX_NOACTIVATE) {
@@ -1006,6 +1010,11 @@ void WIN_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window)
 void WIN_HideWindow(SDL_VideoDevice *_this, SDL_Window *window)
 {
     HWND hwnd = window->driverdata->hwnd;
+
+    if (window->flags & SDL_WINDOW_MODAL) {
+        EnableWindow(window->parent->driverdata->hwnd, TRUE);
+    }
+
     ShowWindow(hwnd, SW_HIDE);
 
     /* Transfer keyboard focus back to the parent */
@@ -1720,4 +1729,39 @@ void WIN_UpdateDarkModeForHWND(HWND hwnd)
     }
 }
 
+int WIN_SetWindowModalFor(SDL_VideoDevice *_this, SDL_Window *modal_window, SDL_Window *parent_window)
+{
+    SDL_WindowData *modal_data = modal_window->driverdata;
+    const LONG_PTR parent_hwnd = (LONG_PTR)(parent_window ? parent_window->driverdata->hwnd : NULL);
+    const LONG_PTR old_ptr = GetWindowLongPtr(modal_data->hwnd, GWLP_HWNDPARENT);
+    const DWORD style = GetWindowLong(modal_data->hwnd, GWL_STYLE);
+
+    if (old_ptr == parent_hwnd) {
+        return 0;
+    }
+
+    /* Reenable the old parent window. */
+    if (old_ptr) {
+        EnableWindow((HWND)old_ptr, TRUE);
+    }
+
+    if (!(style & WS_CHILD)) {
+        /* Despite the name, this changes the *owner* of a toplevel window, not
+         * the parent of a child window.
+         *
+         * https://devblogs.microsoft.com/oldnewthing/20100315-00/?p=14613
+         */
+        SetWindowLongPtr(modal_data->hwnd, GWLP_HWNDPARENT, parent_hwnd);
+    } else {
+        SetParent(modal_data->hwnd, (HWND)parent_hwnd);
+    }
+
+    /* Disable the new parent window if the modal window is visible. */
+    if (!(modal_window->flags & SDL_WINDOW_HIDDEN) && parent_hwnd) {
+        EnableWindow((HWND)parent_hwnd, FALSE);
+    }
+
+    return 0;
+}
+
 #endif /* SDL_VIDEO_DRIVER_WINDOWS */

+ 1 - 0
src/video/windows/SDL_windowswindow.h

@@ -118,6 +118,7 @@ extern void WIN_ShowWindowSystemMenu(SDL_Window *window, int x, int y);
 extern int WIN_SetWindowFocusable(SDL_VideoDevice *_this, SDL_Window *window, SDL_bool focusable);
 extern int WIN_AdjustWindowRect(SDL_Window *window, int *x, int *y, int *width, int *height, SDL_WindowRect rect_type);
 extern int WIN_AdjustWindowRectForHWND(HWND hwnd, LPRECT lpRect, UINT frame_dpi);
+extern int WIN_SetWindowModalFor(SDL_VideoDevice *_this, SDL_Window *modal_window, SDL_Window *parent_window);
 
 /* Ends C function definitions when using C++ */
 #ifdef __cplusplus

+ 2 - 0
src/video/x11/SDL_x11video.c

@@ -389,6 +389,7 @@ int X11_VideoInit(SDL_VideoDevice *_this)
     GET_ATOM(WM_DELETE_WINDOW);
     GET_ATOM(WM_TAKE_FOCUS);
     GET_ATOM(WM_NAME);
+    GET_ATOM(WM_TRANSIENT_FOR);
     GET_ATOM(_NET_WM_STATE);
     GET_ATOM(_NET_WM_STATE_HIDDEN);
     GET_ATOM(_NET_WM_STATE_FOCUSED);
@@ -398,6 +399,7 @@ int X11_VideoInit(SDL_VideoDevice *_this)
     GET_ATOM(_NET_WM_STATE_ABOVE);
     GET_ATOM(_NET_WM_STATE_SKIP_TASKBAR);
     GET_ATOM(_NET_WM_STATE_SKIP_PAGER);
+    GET_ATOM(_NET_WM_STATE_MODAL);
     GET_ATOM(_NET_WM_ALLOWED_ACTIONS);
     GET_ATOM(_NET_WM_ACTION_FULLSCREEN);
     GET_ATOM(_NET_WM_NAME);

+ 2 - 0
src/video/x11/SDL_x11video.h

@@ -67,6 +67,7 @@ struct SDL_VideoData
     Atom WM_DELETE_WINDOW;
     Atom WM_TAKE_FOCUS;
     Atom WM_NAME;
+    Atom WM_TRANSIENT_FOR;
     Atom _NET_WM_STATE;
     Atom _NET_WM_STATE_HIDDEN;
     Atom _NET_WM_STATE_FOCUSED;
@@ -76,6 +77,7 @@ struct SDL_VideoData
     Atom _NET_WM_STATE_ABOVE;
     Atom _NET_WM_STATE_SKIP_TASKBAR;
     Atom _NET_WM_STATE_SKIP_PAGER;
+    Atom _NET_WM_STATE_MODAL;
     Atom _NET_WM_ALLOWED_ACTIONS;
     Atom _NET_WM_ACTION_FULLSCREEN;
     Atom _NET_WM_NAME;

+ 40 - 3
src/video/x11/SDL_x11window.c

@@ -138,6 +138,7 @@ void X11_SetNetWMState(SDL_VideoDevice *_this, Window xwindow, SDL_WindowFlags f
     Atom _NET_WM_STATE_ABOVE = videodata->_NET_WM_STATE_ABOVE;
     Atom _NET_WM_STATE_SKIP_TASKBAR = videodata->_NET_WM_STATE_SKIP_TASKBAR;
     Atom _NET_WM_STATE_SKIP_PAGER = videodata->_NET_WM_STATE_SKIP_PAGER;
+    Atom _NET_WM_STATE_MODAL = videodata->_NET_WM_STATE_MODAL;
     Atom atoms[16];
     int count = 0;
 
@@ -167,6 +168,9 @@ void X11_SetNetWMState(SDL_VideoDevice *_this, Window xwindow, SDL_WindowFlags f
     if (flags & SDL_WINDOW_FULLSCREEN) {
         atoms[count++] = _NET_WM_STATE_FULLSCREEN;
     }
+    if (flags & SDL_WINDOW_MODAL) {
+        atoms[count++] = _NET_WM_STATE_MODAL;
+    }
 
     SDL_assert(count <= SDL_arraysize(atoms));
 
@@ -1204,10 +1208,43 @@ int X11_SetWindowOpacity(SDL_VideoDevice *_this, SDL_Window *window, float opaci
 int X11_SetWindowModalFor(SDL_VideoDevice *_this, SDL_Window *modal_window, SDL_Window *parent_window)
 {
     SDL_WindowData *data = modal_window->driverdata;
-    SDL_WindowData *parent_data = parent_window->driverdata;
-    Display *display = data->videodata->display;
+    SDL_WindowData *parent_data = parent_window ? parent_window->driverdata : NULL;
+    SDL_VideoData *video_data = _this->driverdata;
+    SDL_DisplayData *displaydata = SDL_GetDisplayDriverDataForWindow(modal_window);
+    Display *display = video_data->display;
+    Uint32 flags = modal_window->flags;
+    Atom _NET_WM_STATE = data->videodata->_NET_WM_STATE;
+    Atom _NET_WM_STATE_MODAL = data->videodata->_NET_WM_STATE_MODAL;
+
+    if (parent_data) {
+        flags |= SDL_WINDOW_MODAL;
+        X11_XSetTransientForHint(display, data->xwindow, parent_data->xwindow);
+    } else {
+        flags &= ~SDL_WINDOW_MODAL;
+        X11_XDeleteProperty(display, data->xwindow, video_data->WM_TRANSIENT_FOR);
+    }
+
+    if (X11_IsWindowMapped(_this, modal_window)) {
+        XEvent e;
+
+        SDL_zero(e);
+        e.xany.type = ClientMessage;
+        e.xclient.message_type = _NET_WM_STATE;
+        e.xclient.format = 32;
+        e.xclient.window = data->xwindow;
+        e.xclient.data.l[0] =
+            parent_data ? _NET_WM_STATE_ADD : _NET_WM_STATE_REMOVE;
+        e.xclient.data.l[1] = _NET_WM_STATE_MODAL;
+        e.xclient.data.l[3] = 0l;
+
+        X11_XSendEvent(display, RootWindow(display, displaydata->screen), 0,
+                       SubstructureNotifyMask | SubstructureRedirectMask, &e);
+    } else {
+        X11_SetNetWMState(_this, data->xwindow, flags);
+    }
+
+    X11_XFlush(display);
 
-    X11_XSetTransientForHint(display, data->xwindow, parent_data->xwindow);
     return 0;
 }
 

+ 1 - 0
test/CMakeLists.txt

@@ -414,6 +414,7 @@ add_sdl_test_executable(testpopup SOURCES testpopup.c)
 add_sdl_test_executable(testdialog SOURCES testdialog.c)
 add_sdl_test_executable(testtime SOURCES testtime.c)
 add_sdl_test_executable(testmanymouse SOURCES testmanymouse.c)
+add_sdl_test_executable(testmodal SOURCES testmodal.c)
 
 if (HAVE_WAYLAND)
     # Set the GENERATED property on the protocol file, since it is first created at build time

+ 172 - 0
test/testmodal.c

@@ -0,0 +1,172 @@
+/*
+  Copyright (C) 1997-2024 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely.
+*/
+/* Sample program:  Create a parent window and a modal child window. */
+
+#include <SDL3/SDL.h>
+#include <SDL3/SDL_main.h>
+#include <SDL3/SDL_test.h>
+#include <stdlib.h>
+
+int main(int argc, char *argv[])
+{
+    SDL_Window *w1 = NULL, *w2 = NULL;
+    SDL_Renderer *r1 = NULL, *r2 = NULL;
+    SDLTest_CommonState *state = NULL;
+    Uint64 show_deadline = 0;
+    int i;
+    int exit_code = 0;
+
+    /* Initialize test framework */
+    state = SDLTest_CommonCreateState(argv, 0);
+    if (state == NULL) {
+        return 1;
+    }
+
+    /* Enable standard application logging */
+    SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_INFO);
+
+    /* Parse commandline */
+    for (i = 1; i < argc;) {
+        int consumed;
+
+        consumed = SDLTest_CommonArg(state, i);
+
+        if (consumed <= 0) {
+            static const char *options[] = { NULL };
+            SDLTest_CommonLogUsage(state, argv[0], options);
+            return 1;
+        }
+
+        i += consumed;
+    }
+
+    if (SDL_Init(SDL_INIT_VIDEO) < 0) {
+        SDL_Log("SDL_Init failed (%s)", SDL_GetError());
+        return 1;
+    }
+
+    if (SDL_CreateWindowAndRenderer("Parent Window", 640, 480, 0, &w1, &r1)) {
+        SDL_Log("Failed to create parent window and/or renderer: %s\n", SDL_GetError());
+        exit_code = 1;
+        goto sdl_quit;
+    }
+
+    SDL_CreateWindowAndRenderer("Non-Modal Window", 320, 200, 0, &w2, &r2);
+    if (!w2) {
+        SDL_Log("Failed to create parent window and/or renderer: %s\n", SDL_GetError());
+        exit_code = 1;
+        goto sdl_quit;
+    }
+
+    if (!SDL_SetWindowModalFor(w2, w1)) {
+        SDL_SetWindowTitle(w2, "Modal Window");
+    }
+
+    while (1) {
+        int quit = 0;
+        SDL_Event e;
+        while (SDL_PollEvent(&e)) {
+            if (e.type == SDL_EVENT_QUIT) {
+                quit = 1;
+                break;
+            } else if (e.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) {
+                if (e.window.windowID == SDL_GetWindowID(w2)) {
+                    SDL_DestroyRenderer(r2);
+                    SDL_DestroyWindow(w2);
+                    r2 = NULL;
+                    w2 = NULL;
+                } else if (e.window.windowID == SDL_GetWindowID(w1)) {
+                    SDL_DestroyRenderer(r1);
+                    SDL_DestroyWindow(w1);
+                    r1 = NULL;
+                    w1 = NULL;
+                }
+            } else if (e.type == SDL_EVENT_KEY_DOWN) {
+                if ((e.key.keysym.sym == SDLK_m || e.key.keysym.sym == SDLK_n) && !w2) {
+                    if (SDL_CreateWindowAndRenderer("Non-Modal Window", 320, 200, SDL_WINDOW_HIDDEN, &w2, &r2) < 0) {
+                        SDL_Log("Failed to create modal window and/or renderer: %s\n", SDL_GetError());
+                        exit_code = 1;
+                        goto sdl_quit;
+                    }
+
+                    if (e.key.keysym.sym == SDLK_m) {
+                        if (!SDL_SetWindowModalFor(w2, w1)) {
+                            SDL_SetWindowTitle(w2, "Modal Window");
+                        }
+                    }
+                    SDL_ShowWindow(w2);
+                } else if (e.key.keysym.sym == SDLK_ESCAPE && w2) {
+                    SDL_DestroyWindow(w2);
+                    r2 = NULL;
+                    w2 = NULL;
+                } else if (e.key.keysym.sym == SDLK_h) {
+                    if (e.key.keysym.mod & SDL_KMOD_CTRL) {
+                        /* Hide the parent, which should hide the modal too. */
+                        show_deadline = SDL_GetTicksNS() + SDL_SECONDS_TO_NS(3);
+                        SDL_HideWindow(w1);
+                    } else if (w2) {
+                        /* Show/hide the modal window */
+                        if (SDL_GetWindowFlags(w2) & SDL_WINDOW_HIDDEN) {
+                            SDL_ShowWindow(w2);
+                        } else {
+                            SDL_HideWindow(w2);
+                        }
+                    }
+                } else if (e.key.keysym.sym == SDLK_p && w2) {
+                    if (SDL_GetWindowFlags(w2) & SDL_WINDOW_MODAL) {
+                        /* Unparent the window */
+                        if (!SDL_SetWindowModalFor(w2, NULL)) {
+                            SDL_SetWindowTitle(w2, "Non-Modal Window");
+                        }
+                    } else {
+                        /* Reparent the window */
+                        if (!SDL_SetWindowModalFor(w2, w1)) {
+                            SDL_SetWindowTitle(w2, "Modal Window");
+                        }
+                    }
+                }
+            }
+        }
+        if (quit) {
+            break;
+        }
+        SDL_Delay(100);
+
+        if (show_deadline && show_deadline <= SDL_GetTicksNS()) {
+            SDL_ShowWindow(w1);
+        }
+
+        /* Parent window is red */
+        if (r1) {
+            SDL_SetRenderDrawColor(r1, 224, 48, 12, SDL_ALPHA_OPAQUE);
+            SDL_RenderClear(r1);
+            SDL_RenderPresent(r1);
+        }
+
+        /* Child window is blue */
+        if (r2) {
+            SDL_SetRenderDrawColor(r2, 6, 76, 255, SDL_ALPHA_OPAQUE);
+            SDL_RenderClear(r2);
+            SDL_RenderPresent(r2);
+        }
+    }
+
+sdl_quit:
+    if (w1) {
+        /* The child window and renderer will be cleaned up automatically. */
+        SDL_DestroyWindow(w1);
+    }
+
+    SDL_Quit();
+    SDLTest_CommonDestroyState(state);
+    return exit_code;
+}

+ 110 - 0
wayland-protocols/xdg-dialog-v1.xml

@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="xdg_dialog_v1">
+  <copyright>
+    Copyright © 2023 Carlos Garnacho
+
+    Permission is hereby granted, free of charge, to any person obtaining a
+    copy of this software and associated documentation files (the "Software"),
+    to deal in the Software without restriction, including without limitation
+    the rights to use, copy, modify, merge, publish, distribute, sublicense,
+    and/or sell copies of the Software, and to permit persons to whom the
+    Software is furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice (including the next
+    paragraph) shall be included in all copies or substantial portions of the
+    Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+    THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+    DEALINGS IN THE SOFTWARE.
+  </copyright>
+
+  <interface name="xdg_wm_dialog_v1" version="1">
+    <description summary="create dialogs related to other toplevels">
+      The xdg_wm_dialog_v1 interface is exposed as a global object allowing
+      to register surfaces with a xdg_toplevel role as "dialogs" relative to
+      another toplevel.
+
+      The compositor may let this relation influence how the surface is
+      placed, displayed or interacted with.
+
+      Warning! The protocol described in this file is currently in the testing
+      phase. Backward compatible changes may be added together with the
+      corresponding interface version bump. Backward incompatible changes can
+      only be done by creating a new major version of the extension.
+    </description>
+
+    <enum name="error">
+      <entry name="already_used" value="0"
+             summary="the xdg_toplevel object has already been used to create a xdg_dialog_v1"/>
+    </enum>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the dialog manager object">
+        Destroys the xdg_wm_dialog_v1 object. This does not affect
+        the xdg_dialog_v1 objects generated through it.
+      </description>
+    </request>
+
+    <request name="get_xdg_dialog">
+      <description summary="create a dialog object">
+        Creates a xdg_dialog_v1 object for the given toplevel. See the interface
+        description for more details.
+
+	Compositors must raise an already_used error if clients attempt to
+	create multiple xdg_dialog_v1 objects for the same xdg_toplevel.
+      </description>
+      <arg name="id" type="new_id" interface="xdg_dialog_v1"/>
+      <arg name="toplevel" type="object" interface="xdg_toplevel"/>
+    </request>
+  </interface>
+
+  <interface name="xdg_dialog_v1" version="1">
+    <description summary="dialog object">
+      A xdg_dialog_v1 object is an ancillary object tied to a xdg_toplevel. Its
+      purpose is hinting the compositor that the toplevel is a "dialog" (e.g. a
+      temporary window) relative to another toplevel (see
+      xdg_toplevel.set_parent). If the xdg_toplevel is destroyed, the xdg_dialog_v1
+      becomes inert.
+
+      Through this object, the client may provide additional hints about
+      the purpose of the secondary toplevel. This interface has no effect
+      on toplevels that are not attached to a parent toplevel.
+    </description>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the dialog object">
+        Destroys the xdg_dialog_v1 object. If this object is destroyed
+        before the related xdg_toplevel, the compositor should unapply its
+        effects.
+      </description>
+    </request>
+
+    <request name="set_modal">
+      <description summary="mark dialog as modal">
+        Hints that the dialog has "modal" behavior. Modal dialogs typically
+        require to be fully addressed by the user (i.e. closed) before resuming
+        interaction with the parent toplevel, and may require a distinct
+        presentation.
+
+        Clients must implement the logic to filter events in the parent
+        toplevel on their own.
+
+        Compositors may choose any policy in event delivery to the parent
+        toplevel, from delivering all events unfiltered to using them for
+        internal consumption.
+      </description>
+    </request>
+
+    <request name="unset_modal">
+      <description summary="mark dialog as not modal">
+        Drops the hint that this dialog has "modal" behavior. See
+        xdg_dialog_v1.set_modal for more details.
+      </description>
+    </request>
+  </interface>
+</protocol>