From 7a4d7118f7228f52f6284301017692705b19f7a9 Mon Sep 17 00:00:00 2001 From: Frank Praznik Date: Mon, 14 Apr 2025 13:10:45 -0400 Subject: [PATCH] Add support for non-constrained and non-grabbing popups By default, popups are automatically constrained to be completely within display bounds, so as not to cut off information and result in an unusable menu, or unreadable tooltip. In some cases, however, this is not wanted, so a property to toggle this behavior is added. There are also cases where the client may not want a popup menu to implicitly grab the keyboard focus, as is the default behavior, so popup menus now respect the focusable flag/property, as well as being able to toggle focus grabbing via SDL_SetWindowFocusable(). --- include/SDL3/SDL_video.h | 13 +++ src/video/SDL_sysvideo.h | 6 ++ src/video/SDL_video.c | 43 ++++++++++ src/video/cocoa/SDL_cocoawindow.h | 1 - src/video/cocoa/SDL_cocoawindow.m | 51 ++++++------ src/video/wayland/SDL_waylandevents.c | 2 +- src/video/wayland/SDL_waylandvideo.c | 1 + src/video/wayland/SDL_waylandwindow.c | 53 ++++++++----- src/video/wayland/SDL_waylandwindow.h | 3 +- src/video/windows/SDL_windowsevents.c | 2 +- src/video/windows/SDL_windowswindow.c | 109 ++++++++++++++------------ src/video/windows/SDL_windowswindow.h | 1 - src/video/x11/SDL_x11window.c | 99 ++++++++++++----------- src/video/x11/SDL_x11window.h | 1 - test/testpopup.c | 48 ++++++++++-- 15 files changed, 277 insertions(+), 156 deletions(-) diff --git a/include/SDL3/SDL_video.h b/include/SDL3/SDL_video.h index e3ea31e04aca2..aba0abd6fbee1 100644 --- a/include/SDL3/SDL_video.h +++ b/include/SDL3/SDL_video.h @@ -1188,6 +1188,15 @@ extern SDL_DECLSPEC SDL_Window * SDLCALL SDL_CreateWindow(const char *title, int * Popup windows implicitly do not have a border/decorations and do not appear * on the taskbar/dock or in lists of windows such as alt-tab menus. * + * By default, popup window positions will automatically be constrained to keep + * the entire window within display bounds. This can be overridden with the + * `SDL_PROP_WINDOW_CREATE_CONSTRAIN_POPUP_BOOLEAN` property. + * + * By default, popup menus will automatically grab keyboard focus from the parent + * when shown. This behavior can be overridden by setting the `SDL_WINDOW_NOT_FOCUSABLE` + * flag, setting the `SDL_PROP_WINDOW_CREATE_FOCUSABLE_BOOLEAN` property to false, or + * toggling it after creation via the `SDL_SetWindowFocusable()` function. + * * If a parent window is hidden or destroyed, any child popup windows will be * recursively hidden or destroyed as well. Child popup windows not explicitly * hidden will be restored when the parent is shown. @@ -1228,6 +1237,9 @@ extern SDL_DECLSPEC SDL_Window * SDLCALL SDL_CreatePopupWindow(SDL_Window *paren * be always on top * - `SDL_PROP_WINDOW_CREATE_BORDERLESS_BOOLEAN`: true if the window has no * window decoration + * - `SDL_PROP_WINDOW_CREATE_CONSTRAIN_POPUP_BOOLEAN`: true if the "tooltip" and + * "menu" window types should be automatically constrained to be entirely within + * display bounds (default), false if no constraints on the position are desired. * - `SDL_PROP_WINDOW_CREATE_EXTERNAL_GRAPHICS_CONTEXT_BOOLEAN`: true if the * window will be used with an externally managed graphics context. * - `SDL_PROP_WINDOW_CREATE_FOCUSABLE_BOOLEAN`: true if the window should @@ -1356,6 +1368,7 @@ extern SDL_DECLSPEC SDL_Window * SDLCALL SDL_CreateWindowWithProperties(SDL_Prop #define SDL_PROP_WINDOW_CREATE_ALWAYS_ON_TOP_BOOLEAN "SDL.window.create.always_on_top" #define SDL_PROP_WINDOW_CREATE_BORDERLESS_BOOLEAN "SDL.window.create.borderless" +#define SDL_PROP_WINDOW_CREATE_CONSTRAIN_POPUP_BOOLEAN "SDL.window.create.constrain_popup" #define SDL_PROP_WINDOW_CREATE_FOCUSABLE_BOOLEAN "SDL.window.create.focusable" #define SDL_PROP_WINDOW_CREATE_EXTERNAL_GRAPHICS_CONTEXT_BOOLEAN "SDL.window.create.external_graphics_context" #define SDL_PROP_WINDOW_CREATE_FLAGS_NUMBER "SDL.window.create.flags" diff --git a/src/video/SDL_sysvideo.h b/src/video/SDL_sysvideo.h index 5fe87a86d8b50..f04c91436ac85 100644 --- a/src/video/SDL_sysvideo.h +++ b/src/video/SDL_sysvideo.h @@ -104,6 +104,7 @@ struct SDL_Window bool last_position_pending; // This should NOT be cleared by the backend, as it is used for fullscreen positioning. bool last_size_pending; // This should be cleared by the backend if the new size cannot be applied. bool update_fullscreen_on_display_changed; + bool constrain_popup; bool is_destroying; bool is_dropping; // drag/drop in progress, expecting SDL_SendDropComplete(). @@ -133,6 +134,9 @@ struct SDL_Window SDL_WindowData *internal; + // If a toplevel window, holds the current keyboard focus for grabbing popups. + SDL_Window *keyboard_focus; + SDL_Window *prev; SDL_Window *next; @@ -571,6 +575,8 @@ extern bool SDL_RecreateWindow(SDL_Window *window, SDL_WindowFlags flags); extern 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 bool SDL_ShouldFocusPopup(SDL_Window *window); +extern bool SDL_ShouldRelinquishPopupFocus(SDL_Window *window, SDL_Window **new_focus); extern void SDL_OnDisplayAdded(SDL_VideoDisplay *display); extern void SDL_OnDisplayMoved(SDL_VideoDisplay *display); diff --git a/src/video/SDL_video.c b/src/video/SDL_video.c index db2637bce13c6..26ae67bc2a87c 100644 --- a/src/video/SDL_video.c +++ b/src/video/SDL_video.c @@ -2491,6 +2491,7 @@ SDL_Window *SDL_CreateWindowWithProperties(SDL_PropertiesID props) window->is_destroying = false; window->last_displayID = SDL_GetDisplayForWindow(window); window->external_graphics_context = external_graphics_context; + window->constrain_popup = SDL_GetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_CONSTRAIN_POPUP_BOOLEAN, true); if (_this->windows) { _this->windows->prev = window; @@ -3692,6 +3693,48 @@ bool SDL_SetWindowModal(SDL_Window *window, bool modal) return _this->SetWindowModal(_this, window, modal); } +bool SDL_ShouldRelinquishPopupFocus(SDL_Window *window, SDL_Window **new_focus) +{ + SDL_Window *focus = window->parent; + bool set_focus = !!(window->flags & SDL_WINDOW_INPUT_FOCUS); + + // Find the highest level window, up to the toplevel parent, that isn't being hidden or destroyed, and can grab the keyboard focus. + while (SDL_WINDOW_IS_POPUP(focus) && ((focus->flags & SDL_WINDOW_NOT_FOCUSABLE) || focus->is_hiding || focus->is_destroying)) { + focus = focus->parent; + + // If some window in the chain currently had focus, set it to the new lowest-level window. + if (!set_focus) { + set_focus = !!(focus->flags & SDL_WINDOW_INPUT_FOCUS); + } + } + + *new_focus = focus; + return set_focus; +} + +bool SDL_ShouldFocusPopup(SDL_Window *window) +{ + SDL_Window *toplevel_parent; + for (toplevel_parent = window->parent; SDL_WINDOW_IS_POPUP(toplevel_parent); toplevel_parent = toplevel_parent->parent) { + } + + SDL_Window *current_focus = toplevel_parent->keyboard_focus; + bool found_higher_focus = false; + + /* Traverse the window tree from the currently focused window to the toplevel parent and see if we encounter + * the new focus request. If the new window is found, a higher-level window already has focus. + */ + SDL_Window *w; + for (w = current_focus; w != toplevel_parent; w = w->parent) { + if (w == window) { + found_higher_focus = true; + break; + } + } + + return !found_higher_focus || w == toplevel_parent; +} + bool SDL_SetWindowFocusable(SDL_Window *window, bool focusable) { CHECK_WINDOW_MAGIC(window, false); diff --git a/src/video/cocoa/SDL_cocoawindow.h b/src/video/cocoa/SDL_cocoawindow.h index 6df69f442a080..67f1519eec49c 100644 --- a/src/video/cocoa/SDL_cocoawindow.h +++ b/src/video/cocoa/SDL_cocoawindow.h @@ -146,7 +146,6 @@ typedef enum @property(nonatomic) BOOL was_zoomed; @property(nonatomic) NSInteger window_number; @property(nonatomic) NSInteger flash_request; -@property(nonatomic) SDL_Window *keyboard_focus; @property(nonatomic) SDL3Cocoa_WindowListener *listener; @property(nonatomic) NSModalSession modal_session; @property(nonatomic) SDL_CocoaVideoData *videodata; diff --git a/src/video/cocoa/SDL_cocoawindow.m b/src/video/cocoa/SDL_cocoawindow.m index 800db71fdbfae..5affe9231535f 100644 --- a/src/video/cocoa/SDL_cocoawindow.m +++ b/src/video/cocoa/SDL_cocoawindow.m @@ -707,10 +707,7 @@ static void Cocoa_UpdateClipCursor(SDL_Window *window) static void Cocoa_SetKeyboardFocus(SDL_Window *window, bool set_active_focus) { SDL_Window *toplevel = GetParentToplevelWindow(window); - SDL_CocoaWindowData *toplevel_data; - - toplevel_data = (__bridge SDL_CocoaWindowData *)toplevel->internal; - toplevel_data.keyboard_focus = window; + toplevel->keyboard_focus = window; if (set_active_focus && !window->is_hiding && !window->is_destroying) { SDL_SetKeyboardFocus(window); @@ -1252,7 +1249,7 @@ - (void)windowDidBecomeKey:(NSNotification *)aNotification // We're going to get keyboard events, since we're key. // This needs to be done before restoring the relative mouse mode. - Cocoa_SetKeyboardFocus(_data.keyboard_focus ? _data.keyboard_focus : window, true); + Cocoa_SetKeyboardFocus(window->keyboard_focus ? window->keyboard_focus : window, true); // If we just gained focus we need the updated mouse position if (!(window->flags & SDL_WINDOW_MOUSE_RELATIVE_MODE)) { @@ -2211,8 +2208,8 @@ then immediately ordering out (removing) the window does work. */ if (window->flags & SDL_WINDOW_TOOLTIP) { [nswindow setIgnoresMouseEvents:YES]; [nswindow setAcceptsMouseMovedEvents:NO]; - } else if (window->flags & SDL_WINDOW_POPUP_MENU) { - Cocoa_SetKeyboardFocus(window, window->parent == SDL_GetKeyboardFocus()); + } else if ((window->flags & SDL_WINDOW_POPUP_MENU) && !(window->flags & SDL_WINDOW_NOT_FOCUSABLE)) { + Cocoa_SetKeyboardFocus(window, true); } } @@ -2301,7 +2298,7 @@ bool Cocoa_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_Properti rect.origin.y -= screenRect.origin.y; // Constrain the popup - if (SDL_WINDOW_IS_POPUP(window)) { + if (SDL_WINDOW_IS_POPUP(window) && window->constrain_popup) { if (rect.origin.x + rect.size.width > screenRect.origin.x + screenRect.size.width) { rect.origin.x -= (rect.origin.x + rect.size.width) - (screenRect.origin.x + screenRect.size.width); } @@ -2457,7 +2454,7 @@ bool Cocoa_SetWindowPosition(SDL_VideoDevice *_this, SDL_Window *window) ConvertNSRect(&rect); // Position and constrain the popup - if (SDL_WINDOW_IS_POPUP(window)) { + if (SDL_WINDOW_IS_POPUP(window) && window->constrain_popup) { NSRect screenRect = [ScreenForRect(&rect) frame]; if (rect.origin.x + rect.size.width > screenRect.origin.x + screenRect.size.width) { @@ -2629,20 +2626,9 @@ void Cocoa_HideWindow(SDL_VideoDevice *_this, SDL_Window *window) Cocoa_SetWindowModal(_this, window, false); // Transfer keyboard focus back to the parent when closing a popup menu - if (window->flags & SDL_WINDOW_POPUP_MENU) { - SDL_Window *new_focus = window->parent; - bool set_focus = window == SDL_GetKeyboardFocus(); - - // Find the highest level window, up to the toplevel parent, that isn't being hidden or destroyed. - while (SDL_WINDOW_IS_POPUP(new_focus) && (new_focus->is_hiding || new_focus->is_destroying)) { - new_focus = new_focus->parent; - - // If some window in the chain currently had focus, set it to the new lowest-level window. - if (!set_focus) { - set_focus = new_focus == SDL_GetKeyboardFocus(); - } - } - + if ((window->flags & SDL_WINDOW_POPUP_MENU) && !(window->flags & SDL_WINDOW_NOT_FOCUSABLE)) { + SDL_Window *new_focus; + const bool set_focus = SDL_ShouldRelinquishPopupFocus(window, &new_focus); Cocoa_SetKeyboardFocus(new_focus, set_focus); } else if (window->parent && waskey) { /* Key status is not automatically set on the parent when a child is hidden. Check if the @@ -3068,20 +3054,19 @@ void Cocoa_DestroyWindow(SDL_VideoDevice *_this, SDL_Window *window) #endif // SDL_VIDEO_OPENGL SDL_Window *topmost = GetParentToplevelWindow(window); - SDL_CocoaWindowData *topmost_data = (__bridge SDL_CocoaWindowData *)topmost->internal; /* Reset the input focus of the root window if this window is still set as keyboard focus. * SDL_DestroyWindow will have already taken care of reassigning focus if this is the SDL * keyboard focus, this ensures that an inactive window with this window set as input focus * does not try to reference it the next time it gains focus. */ - if (topmost_data.keyboard_focus == window) { + if (topmost->keyboard_focus == window) { SDL_Window *new_focus = window; while (SDL_WINDOW_IS_POPUP(new_focus) && (new_focus->is_hiding || new_focus->is_destroying)) { new_focus = new_focus->parent; } - topmost_data.keyboard_focus = new_focus; + topmost->keyboard_focus = new_focus; } if ([data.listener isInFullscreenSpace]) { @@ -3246,6 +3231,20 @@ bool Cocoa_FlashWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_FlashOper bool Cocoa_SetWindowFocusable(SDL_VideoDevice *_this, SDL_Window *window, bool focusable) { + if (window->flags & SDL_WINDOW_POPUP_MENU) { + if (!(window->flags & SDL_WINDOW_HIDDEN)) { + if (!focusable && (window->flags & SDL_WINDOW_INPUT_FOCUS)) { + SDL_Window *new_focus; + const bool set_focus = SDL_ShouldRelinquishPopupFocus(window, &new_focus); + Cocoa_SetKeyboardFocus(new_focus, set_focus); + } else if (focusable) { + if (SDL_ShouldFocusPopup(window)) { + Cocoa_SetKeyboardFocus(window, true); + } + } + } + } + return true; // just succeed, the real work is done elsewhere. } diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c index 00b77dd532c6e..4f6d3a7754b1d 100644 --- a/src/video/wayland/SDL_waylandevents.c +++ b/src/video/wayland/SDL_waylandevents.c @@ -1824,7 +1824,7 @@ static void keyboard_handle_enter(void *data, struct wl_keyboard *keyboard, seat->keyboard.focus = window; // Restore the keyboard focus to the child popup that was holding it - SDL_SetKeyboardFocus(window->keyboard_focus ? window->keyboard_focus : window->sdlwindow); + SDL_SetKeyboardFocus(window->sdlwindow->keyboard_focus ? window->sdlwindow->keyboard_focus : window->sdlwindow); // Update the keyboard grab and any relative pointer grabs related to this keyboard focus. Wayland_SeatUpdateKeyboardGrab(seat); diff --git a/src/video/wayland/SDL_waylandvideo.c b/src/video/wayland/SDL_waylandvideo.c index c185cb5967a5f..8cc9a9c4f1f9f 100644 --- a/src/video/wayland/SDL_waylandvideo.c +++ b/src/video/wayland/SDL_waylandvideo.c @@ -629,6 +629,7 @@ static SDL_VideoDevice *Wayland_CreateDevice(bool require_preferred_protocols) device->HasScreenKeyboardSupport = Wayland_HasScreenKeyboardSupport; device->ShowWindowSystemMenu = Wayland_ShowWindowSystemMenu; device->SyncWindow = Wayland_SyncWindow; + device->SetWindowFocusable = Wayland_SetWindowFocusable; #ifdef SDL_USE_LIBDBUS if (SDL_SystemTheme_Init()) diff --git a/src/video/wayland/SDL_waylandwindow.c b/src/video/wayland/SDL_waylandwindow.c index 8ef5794a6f4b4..b019eff2f92f0 100644 --- a/src/video/wayland/SDL_waylandwindow.c +++ b/src/video/wayland/SDL_waylandwindow.c @@ -1677,7 +1677,7 @@ static const struct wp_color_management_surface_feedback_v1_listener color_manag feedback_surface_preferred_changed }; -static void SetKeyboardFocus(SDL_Window *window, bool set_focus) +static void Wayland_SetKeyboardFocus(SDL_Window *window, bool set_focus) { SDL_Window *toplevel = window; @@ -1686,7 +1686,7 @@ static void SetKeyboardFocus(SDL_Window *window, bool set_focus) toplevel = toplevel->parent; } - toplevel->internal->keyboard_focus = window; + toplevel->keyboard_focus = window; if (set_focus && !window->is_hiding && !window->is_destroying) { SDL_SetKeyboardFocus(window); @@ -1916,8 +1916,9 @@ void Wayland_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window) data->shell_surface.xdg.popup.xdg_positioner = xdg_wm_base_create_positioner(c->shell.xdg); xdg_positioner_set_anchor(data->shell_surface.xdg.popup.xdg_positioner, XDG_POSITIONER_ANCHOR_TOP_LEFT); xdg_positioner_set_anchor_rect(data->shell_surface.xdg.popup.xdg_positioner, 0, 0, parent->internal->current.logical_width, parent->internal->current.logical_width); - xdg_positioner_set_constraint_adjustment(data->shell_surface.xdg.popup.xdg_positioner, - XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y); + + const Uint32 constraint = window->constrain_popup ? (XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y) : XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_NONE; + xdg_positioner_set_constraint_adjustment(data->shell_surface.xdg.popup.xdg_positioner, constraint); xdg_positioner_set_gravity(data->shell_surface.xdg.popup.xdg_positioner, XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT); xdg_positioner_set_size(data->shell_surface.xdg.popup.xdg_positioner, data->current.logical_width, data->current.logical_height); @@ -1946,8 +1947,8 @@ void Wayland_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window) wl_region_add(region, 0, 0, 0, 0); wl_surface_set_input_region(data->surface, region); wl_region_destroy(region); - } else if (window->flags & SDL_WINDOW_POPUP_MENU) { - SetKeyboardFocus(window, window->parent == SDL_GetKeyboardFocus()); + } else if ((window->flags & SDL_WINDOW_POPUP_MENU) && !(window->flags & SDL_WINDOW_NOT_FOCUSABLE)) { + Wayland_SetKeyboardFocus(window, true); } SDL_SetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_XDG_POPUP_POINTER, data->shell_surface.xdg.popup.xdg_popup); @@ -2094,21 +2095,10 @@ static void Wayland_ReleasePopup(SDL_VideoDevice *_this, SDL_Window *popup) return; } - if (popup->flags & SDL_WINDOW_POPUP_MENU) { - SDL_Window *new_focus = popup->parent; - bool set_focus = popup == SDL_GetKeyboardFocus(); - - // Find the highest level window, up to the toplevel parent, that isn't being hidden or destroyed. - while (SDL_WINDOW_IS_POPUP(new_focus) && (new_focus->is_hiding || new_focus->is_destroying)) { - new_focus = new_focus->parent; - - // If some window in the chain currently had focus, set it to the new lowest-level window. - if (!set_focus) { - set_focus = new_focus == SDL_GetKeyboardFocus(); - } - } - - SetKeyboardFocus(new_focus, set_focus); + if ((popup->flags & SDL_WINDOW_POPUP_MENU) && !(popup->flags & SDL_WINDOW_NOT_FOCUSABLE)) { + SDL_Window *new_focus; + const bool set_focus = SDL_ShouldRelinquishPopupFocus(popup, &new_focus); + Wayland_SetKeyboardFocus(new_focus, set_focus); } xdg_popup_destroy(popupdata->shell_surface.xdg.popup.xdg_popup); @@ -3002,6 +2992,27 @@ bool Wayland_SyncWindow(SDL_VideoDevice *_this, SDL_Window *window) return true; } +bool Wayland_SetWindowFocusable(SDL_VideoDevice *_this, SDL_Window *window, bool focusable) +{ + if (window->flags & SDL_WINDOW_POPUP_MENU) { + if (!(window->flags & SDL_WINDOW_HIDDEN)) { + if (!focusable && (window->flags & SDL_WINDOW_INPUT_FOCUS)) { + SDL_Window *new_focus; + const bool set_focus = SDL_ShouldRelinquishPopupFocus(window, &new_focus); + Wayland_SetKeyboardFocus(new_focus, set_focus); + } else if (focusable) { + if (SDL_ShouldFocusPopup(window)) { + Wayland_SetKeyboardFocus(window, true); + } + } + } + + return true; + } + + return SDL_SetError("wayland: focus can only be toggled on popup menu windows"); +} + void Wayland_ShowWindowSystemMenu(SDL_Window *window, int x, int y) { SDL_WindowData *wind = window->internal; diff --git a/src/video/wayland/SDL_waylandwindow.h b/src/video/wayland/SDL_waylandwindow.h index 327d29655e791..343a0ed251514 100644 --- a/src/video/wayland/SDL_waylandwindow.h +++ b/src/video/wayland/SDL_waylandwindow.h @@ -126,8 +126,6 @@ struct SDL_WindowData SDL_DisplayData **outputs; int num_outputs; - SDL_Window *keyboard_focus; - char *app_id; double scale_factor; @@ -249,6 +247,7 @@ extern void Wayland_ShowWindowSystemMenu(SDL_Window *window, int x, int y); extern void Wayland_DestroyWindow(SDL_VideoDevice *_this, SDL_Window *window); extern bool Wayland_SuspendScreenSaver(SDL_VideoDevice *_this); extern bool Wayland_SetWindowIcon(SDL_VideoDevice *_this, SDL_Window *window, SDL_Surface *icon); +extern bool Wayland_SetWindowFocusable(SDL_VideoDevice *_this, SDL_Window *window, bool focusable); extern float Wayland_GetWindowContentScale(SDL_VideoDevice *_this, SDL_Window *window); extern void *Wayland_GetWindowICCProfile(SDL_VideoDevice *_this, SDL_Window *window, size_t *size); diff --git a/src/video/windows/SDL_windowsevents.c b/src/video/windows/SDL_windowsevents.c index 7c0c5590e1a59..44112e69091b8 100644 --- a/src/video/windows/SDL_windowsevents.c +++ b/src/video/windows/SDL_windowsevents.c @@ -389,7 +389,7 @@ static void WIN_UpdateFocus(SDL_Window *window, bool expect_focus) } } - SDL_SetKeyboardFocus(data->keyboard_focus ? data->keyboard_focus : window); + SDL_SetKeyboardFocus(window->keyboard_focus ? window->keyboard_focus : window); // In relative mode we are guaranteed to have mouse focus if we have keyboard focus if (!SDL_GetMouse()->relative_mode) { diff --git a/src/video/windows/SDL_windowswindow.c b/src/video/windows/SDL_windowswindow.c index a11d74be8b966..b38425804d64e 100644 --- a/src/video/windows/SDL_windowswindow.c +++ b/src/video/windows/SDL_windowswindow.c @@ -666,7 +666,7 @@ static void CleanupWindowData(SDL_VideoDevice *_this, SDL_Window *window) static void WIN_ConstrainPopup(SDL_Window *window, bool output_to_pending) { - // Clamp popup windows to the output borders + // Possibly clamp popup windows to the output borders if (SDL_WINDOW_IS_POPUP(window)) { SDL_Window *w; SDL_DisplayID displayID; @@ -677,29 +677,31 @@ static void WIN_ConstrainPopup(SDL_Window *window, bool output_to_pending) const int height = window->last_size_pending ? window->pending.h : window->floating.h; int offset_x = 0, offset_y = 0; - // Calculate the total offset from the parents - for (w = window->parent; SDL_WINDOW_IS_POPUP(w); w = w->parent) { + if (window->constrain_popup) { + // Calculate the total offset from the parents + for (w = window->parent; SDL_WINDOW_IS_POPUP(w); w = w->parent) { + offset_x += w->x; + offset_y += w->y; + } + offset_x += w->x; offset_y += w->y; + abs_x += offset_x; + abs_y += offset_y; + + // Constrain the popup window to the display of the toplevel parent + displayID = SDL_GetDisplayForWindow(w); + SDL_GetDisplayBounds(displayID, &rect); + if (abs_x + width > rect.x + rect.w) { + abs_x -= (abs_x + width) - (rect.x + rect.w); + } + if (abs_y + height > rect.y + rect.h) { + abs_y -= (abs_y + height) - (rect.y + rect.h); + } + abs_x = SDL_max(abs_x, rect.x); + abs_y = SDL_max(abs_y, rect.y); } - offset_x += w->x; - offset_y += w->y; - abs_x += offset_x; - abs_y += offset_y; - - // Constrain the popup window to the display of the toplevel parent - displayID = SDL_GetDisplayForWindow(w); - SDL_GetDisplayBounds(displayID, &rect); - if (abs_x + width > rect.x + rect.w) { - abs_x -= (abs_x + width) - (rect.x + rect.w); - } - if (abs_y + height > rect.y + rect.h) { - abs_y -= (abs_y + height) - (rect.y + rect.h); - } - abs_x = SDL_max(abs_x, rect.x); - abs_y = SDL_max(abs_y, rect.y); - if (output_to_pending) { window->pending.x = abs_x - offset_x; window->pending.y = abs_y - offset_y; @@ -723,7 +725,7 @@ static void WIN_SetKeyboardFocus(SDL_Window *window, bool set_active_focus) toplevel = toplevel->parent; } - toplevel->internal->keyboard_focus = window; + toplevel->keyboard_focus = window; if (set_active_focus && !window->is_hiding && !window->is_destroying) { SDL_SetKeyboardFocus(window); @@ -1082,8 +1084,8 @@ void WIN_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window) SetWindowPos(hwnd, NULL, 0, 0, 0, 0, window->internal->copybits_flag | SWP_SHOWWINDOW | SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER); } - if (window->flags & SDL_WINDOW_POPUP_MENU && bActivate) { - WIN_SetKeyboardFocus(window, window->parent == SDL_GetKeyboardFocus()); + if ((window->flags & SDL_WINDOW_POPUP_MENU) && !(window->flags & SDL_WINDOW_NOT_FOCUSABLE) && bActivate) { + WIN_SetKeyboardFocus(window, true); } if (window->flags & SDL_WINDOW_MODAL) { WIN_SetWindowModal(_this, window, true); @@ -1100,21 +1102,10 @@ void WIN_HideWindow(SDL_VideoDevice *_this, SDL_Window *window) ShowWindow(hwnd, SW_HIDE); - // Transfer keyboard focus back to the parent - if (window->flags & SDL_WINDOW_POPUP_MENU) { - SDL_Window *new_focus = window->parent; - bool set_focus = window == SDL_GetKeyboardFocus(); - - // Find the highest level window, up to the toplevel parent, that isn't being hidden or destroyed. - while (SDL_WINDOW_IS_POPUP(new_focus) && (new_focus->is_hiding || new_focus->is_destroying)) { - new_focus = new_focus->parent; - - // If some window in the chain currently had keyboard focus, set it to the new lowest-level window. - if (!set_focus) { - set_focus = new_focus == SDL_GetKeyboardFocus(); - } - } - + // Transfer keyboard focus back to the parent from a grabbing popup. + if ((window->flags & SDL_WINDOW_POPUP_MENU) && !(window->flags & SDL_WINDOW_NOT_FOCUSABLE)) { + SDL_Window *new_focus; + const bool set_focus = SDL_ShouldRelinquishPopupFocus(window, &new_focus); WIN_SetKeyboardFocus(new_focus, set_focus); } } @@ -1152,7 +1143,7 @@ void WIN_RaiseWindow(SDL_VideoDevice *_this, SDL_Window *window) } if (bActivate) { SetForegroundWindow(hwnd); - if (window->flags & SDL_WINDOW_POPUP_MENU) { + if ((window->flags & SDL_WINDOW_POPUP_MENU) && !(window->flags & SDL_WINDOW_NOT_FOCUSABLE)) { WIN_SetKeyboardFocus(window, window->parent == SDL_GetKeyboardFocus()); } } else { @@ -2319,24 +2310,40 @@ void WIN_ShowWindowSystemMenu(SDL_Window *window, int x, int y) bool WIN_SetWindowFocusable(SDL_VideoDevice *_this, SDL_Window *window, bool focusable) { - SDL_WindowData *data = window->internal; - HWND hwnd = data->hwnd; - const LONG style = GetWindowLong(hwnd, GWL_EXSTYLE); + if (!SDL_WINDOW_IS_POPUP(window)) { + SDL_WindowData *data = window->internal; + HWND hwnd = data->hwnd; + const LONG style = GetWindowLong(hwnd, GWL_EXSTYLE); - SDL_assert(style != 0); + SDL_assert(style != 0); - if (focusable) { - if (style & WS_EX_NOACTIVATE) { - if (SetWindowLong(hwnd, GWL_EXSTYLE, style & ~WS_EX_NOACTIVATE) == 0) { - return WIN_SetError("SetWindowLong()"); + if (focusable) { + if (style & WS_EX_NOACTIVATE) { + if (SetWindowLong(hwnd, GWL_EXSTYLE, style & ~WS_EX_NOACTIVATE) == 0) { + return WIN_SetError("SetWindowLong()"); + } + } + } else { + if (!(style & WS_EX_NOACTIVATE)) { + if (SetWindowLong(hwnd, GWL_EXSTYLE, style | WS_EX_NOACTIVATE) == 0) { + return WIN_SetError("SetWindowLong()"); + } } } - } else { - if (!(style & WS_EX_NOACTIVATE)) { - if (SetWindowLong(hwnd, GWL_EXSTYLE, style | WS_EX_NOACTIVATE) == 0) { - return WIN_SetError("SetWindowLong()"); + } else if (window->flags & SDL_WINDOW_POPUP_MENU) { + if (!(window->flags & SDL_WINDOW_HIDDEN)) { + if (!focusable && (window->flags & SDL_WINDOW_INPUT_FOCUS)) { + SDL_Window *new_focus; + const bool set_focus = SDL_ShouldRelinquishPopupFocus(window, &new_focus); + WIN_SetKeyboardFocus(new_focus, set_focus); + } else if (focusable) { + if (SDL_ShouldFocusPopup(window)) { + WIN_SetKeyboardFocus(window, true); + } } } + + return true; } return true; diff --git a/src/video/windows/SDL_windowswindow.h b/src/video/windows/SDL_windowswindow.h index d23d83c04e46b..65f6373fe4b7d 100644 --- a/src/video/windows/SDL_windowswindow.h +++ b/src/video/windows/SDL_windowswindow.h @@ -91,7 +91,6 @@ struct SDL_WindowData bool destroy_parent_with_window; SDL_DisplayID last_displayID; WCHAR *ICMFileName; - SDL_Window *keyboard_focus; SDL_WindowEraseBackgroundMode hint_erase_background_mode; bool taskbar_button_created; struct SDL_VideoData *videodata; diff --git a/src/video/x11/SDL_x11window.c b/src/video/x11/SDL_x11window.c index ca340953ede37..6dd7ce02eb067 100644 --- a/src/video/x11/SDL_x11window.c +++ b/src/video/x11/SDL_x11window.c @@ -215,28 +215,30 @@ static void X11_ConstrainPopup(SDL_Window *window, bool output_to_pending) int abs_y = window->last_position_pending ? window->pending.y : window->floating.y; int offset_x = 0, offset_y = 0; - // Calculate the total offset from the parents - for (w = window->parent; SDL_WINDOW_IS_POPUP(w); w = w->parent) { + if (window->constrain_popup) { + // Calculate the total offset from the parents + for (w = window->parent; SDL_WINDOW_IS_POPUP(w); w = w->parent) { + offset_x += w->x; + offset_y += w->y; + } + offset_x += w->x; offset_y += w->y; - } - - offset_x += w->x; - offset_y += w->y; - abs_x += offset_x; - abs_y += offset_y; + abs_x += offset_x; + abs_y += offset_y; - displayID = SDL_GetDisplayForWindow(w); + displayID = SDL_GetDisplayForWindow(w); - SDL_GetDisplayBounds(displayID, &rect); - if (abs_x + window->w > rect.x + rect.w) { - abs_x -= (abs_x + window->w) - (rect.x + rect.w); - } - if (abs_y + window->h > rect.y + rect.h) { - abs_y -= (abs_y + window->h) - (rect.y + rect.h); + SDL_GetDisplayBounds(displayID, &rect); + if (abs_x + window->w > rect.x + rect.w) { + abs_x -= (abs_x + window->w) - (rect.x + rect.w); + } + if (abs_y + window->h > rect.y + rect.h) { + abs_y -= (abs_y + window->h) - (rect.y + rect.h); + } + abs_x = SDL_max(abs_x, rect.x); + abs_y = SDL_max(abs_y, rect.y); } - abs_x = SDL_max(abs_x, rect.x); - abs_y = SDL_max(abs_y, rect.y); if (output_to_pending) { window->pending.x = abs_x - offset_x; @@ -257,7 +259,7 @@ static void X11_SetKeyboardFocus(SDL_Window *window, bool set_active_focus) toplevel = toplevel->parent; } - toplevel->internal->keyboard_focus = window; + toplevel->keyboard_focus = window; if (set_active_focus && !window->is_hiding && !window->is_destroying) { SDL_SetKeyboardFocus(window); @@ -1550,9 +1552,9 @@ void X11_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window) X11_XFlush(display); } - // Popup menus grab the keyboard - if (window->flags & SDL_WINDOW_POPUP_MENU) { - X11_SetKeyboardFocus(window, window->parent == SDL_GetKeyboardFocus()); + // Grabbing popup menus get keyboard focus. + if ((window->flags & SDL_WINDOW_POPUP_MENU) && !(window->flags & SDL_WINDOW_NOT_FOCUSABLE)) { + X11_SetKeyboardFocus(window, true); } // Get some valid border values, if we haven't received them yet @@ -1609,20 +1611,9 @@ void X11_HideWindow(SDL_VideoDevice *_this, SDL_Window *window) } // Transfer keyboard focus back to the parent - if (window->flags & SDL_WINDOW_POPUP_MENU) { - SDL_Window *new_focus = window->parent; - bool set_focus = window == SDL_GetKeyboardFocus(); - - // Find the highest level window, up to the toplevel parent, that isn't being hidden or destroyed. - while (SDL_WINDOW_IS_POPUP(new_focus) && (new_focus->is_hiding || new_focus->is_destroying)) { - new_focus = new_focus->parent; - - // If some window in the chain currently had focus, set it to the new lowest-level window. - if (!set_focus) { - set_focus = new_focus == SDL_GetKeyboardFocus(); - } - } - + if ((window->flags & SDL_WINDOW_POPUP_MENU) && !(window->flags & SDL_WINDOW_NOT_FOCUSABLE)) { + SDL_Window *new_focus; + const bool set_focus = SDL_ShouldRelinquishPopupFocus(window, &new_focus); X11_SetKeyboardFocus(new_focus, set_focus); } @@ -2340,20 +2331,36 @@ bool X11_SyncWindow(SDL_VideoDevice *_this, SDL_Window *window) bool X11_SetWindowFocusable(SDL_VideoDevice *_this, SDL_Window *window, bool focusable) { - SDL_WindowData *data = window->internal; - Display *display = data->videodata->display; - XWMHints *wmhints; + if (!SDL_WINDOW_IS_POPUP(window)) { + SDL_WindowData *data = window->internal; + Display *display = data->videodata->display; + XWMHints *wmhints; - wmhints = X11_XGetWMHints(display, data->xwindow); - if (!wmhints) { - return SDL_SetError("Couldn't get WM hints"); - } + wmhints = X11_XGetWMHints(display, data->xwindow); + if (!wmhints) { + return SDL_SetError("Couldn't get WM hints"); + } - wmhints->input = focusable ? True : False; - wmhints->flags |= InputHint; + wmhints->input = focusable ? True : False; + wmhints->flags |= InputHint; - X11_XSetWMHints(display, data->xwindow, wmhints); - X11_XFree(wmhints); + X11_XSetWMHints(display, data->xwindow, wmhints); + X11_XFree(wmhints); + } else if (window->flags & SDL_WINDOW_POPUP_MENU) { + if (!(window->flags & SDL_WINDOW_HIDDEN)) { + if (!focusable && (window->flags & SDL_WINDOW_INPUT_FOCUS)) { + SDL_Window *new_focus; + const bool set_focus = SDL_ShouldRelinquishPopupFocus(window, &new_focus); + X11_SetKeyboardFocus(new_focus, set_focus); + } else if (focusable) { + if (SDL_ShouldFocusPopup(window)) { + X11_SetKeyboardFocus(window, true); + } + } + } + + return true; + } return true; } diff --git a/src/video/x11/SDL_x11window.h b/src/video/x11/SDL_x11window.h index 2d7b3239d37a1..a8f0b08e16b0d 100644 --- a/src/video/x11/SDL_x11window.h +++ b/src/video/x11/SDL_x11window.h @@ -74,7 +74,6 @@ struct SDL_WindowData Window xdnd_source; bool flashing_window; Uint64 flash_cancel_time; - SDL_Window *keyboard_focus; #ifdef SDL_VIDEO_OPENGL_EGL EGLSurface egl_surface; #endif diff --git a/test/testpopup.c b/test/testpopup.c index 786f03c20642c..987eab8dd9d19 100644 --- a/test/testpopup.c +++ b/test/testpopup.c @@ -49,6 +49,9 @@ struct PopupWindow static struct PopupWindow *menus; static struct PopupWindow tooltip; +static bool no_constraints; +static bool no_grab; + /* Call this instead of exit(), so we can clean up SDL: atexit() is evil. */ static void quit(int rc) { @@ -95,14 +98,27 @@ static bool create_popup(struct PopupWindow *new_popup, bool is_menu) const int w = is_menu ? MENU_WIDTH : TOOLTIP_WIDTH; const int h = is_menu ? MENU_HEIGHT : TOOLTIP_HEIGHT; const int v_off = is_menu ? 0 : 32; - const SDL_WindowFlags flags = is_menu ? SDL_WINDOW_POPUP_MENU : SDL_WINDOW_TOOLTIP; float x, y; focus = SDL_GetMouseFocus(); SDL_GetMouseState(&x, &y); - new_win = SDL_CreatePopupWindow(focus, - (int)x, (int)y + v_off, w, h, flags); + + SDL_PropertiesID props = SDL_CreateProperties(); + SDL_SetPointerProperty(props, SDL_PROP_WINDOW_CREATE_PARENT_POINTER, focus); + SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_CONSTRAIN_POPUP_BOOLEAN, !no_constraints); + SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_FOCUSABLE_BOOLEAN, !no_grab); + if (is_menu) { + SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_MENU_BOOLEAN, true); + } else { + SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_TOOLTIP_BOOLEAN, true); + } + SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, w); + SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, h); + SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X_NUMBER, (int)x); + SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_Y_NUMBER, (int)y + v_off); + new_win = SDL_CreateWindowWithProperties(props); + SDL_DestroyProperties(props); if (new_win) { new_renderer = SDL_CreateRenderer(new_win, state->renderdriver); @@ -249,8 +265,30 @@ int main(int argc, char *argv[]) } /* Parse commandline */ - if (!SDLTest_CommonDefaultArgs(state, argc, argv)) { - return 1; + for (i = 1; i < argc;) { + int consumed; + + consumed = SDLTest_CommonArg(state, i); + if (consumed == 0) { + consumed = -1; + if (SDL_strcasecmp(argv[i], "--no-constraints") == 0) { + no_constraints = true; + consumed = 1; + } else if (SDL_strcasecmp(argv[i], "--no-grab") == 0) { + no_grab = true; + consumed = 1; + } + } + if (consumed < 0) { + static const char *options[] = { + "[--no-constraints]", + "[--no-grab]", + NULL + }; + SDLTest_CommonLogUsage(state, argv[0], options); + return 1; + } + i += consumed; } if (!SDLTest_CommonInit(state)) {