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.
pull/9658/head
Semphris 2024-04-03 16:44:03 -04:00 committed by Frank Praznik
parent 97f7b4620a
commit c6a70d6898
20 changed files with 592 additions and 51 deletions

View File

@ -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().
*

View File

@ -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;

View File

@ -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) {

View File

@ -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

View File

@ -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_ */

View File

@ -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 {

View File

@ -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));

View File

@ -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);

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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 */

View File

@ -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

View File

@ -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);

View File

@ -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;

View File

@ -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;
}

View File

@ -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
test/testmodal.c Normal file
View File

@ -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;
}

View File

@ -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>