diff --git a/include/SDL3/SDL_video.h b/include/SDL3/SDL_video.h index aca10b6453..70faf9da12 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 5fe87a86d8..f04c91436a 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 db2637bce1..26ae67bc2a 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 6df69f442a..67f1519eec 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 7201c0e692..3636756401 100644 --- a/src/video/cocoa/SDL_cocoawindow.m +++ b/src/video/cocoa/SDL_cocoawindow.m @@ -707,10 +707,7 @@ static SDL_Window *GetParentToplevelWindow(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 @@ static NSCursor *Cocoa_GetDesiredCursor(void) // 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)) { @@ -2244,7 +2241,9 @@ static bool SetupWindowData(SDL_VideoDevice *_this, SDL_Window *window, NSWindow [nswindow setIgnoresMouseEvents:YES]; [nswindow setAcceptsMouseMovedEvents:NO]; } else if ((window->flags & SDL_WINDOW_POPUP_MENU) && !(window->flags & SDL_WINDOW_HIDDEN)) { - Cocoa_SetKeyboardFocus(window, window->parent == SDL_GetKeyboardFocus()); + if (!(window->flags & SDL_WINDOW_NOT_FOCUSABLE)) { + Cocoa_SetKeyboardFocus(window, true); + } Cocoa_UpdateMouseFocus(); } } @@ -2334,7 +2333,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); } @@ -2490,7 +2489,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) { @@ -2631,7 +2630,9 @@ void Cocoa_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window) } } } else if (window->flags & SDL_WINDOW_POPUP_MENU) { - Cocoa_SetKeyboardFocus(window, window->parent == SDL_GetKeyboardFocus()); + if (!(window->flags & SDL_WINDOW_NOT_FOCUSABLE)) { + Cocoa_SetKeyboardFocus(window, true); + } Cocoa_UpdateMouseFocus(); } } @@ -2665,20 +2666,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); Cocoa_UpdateMouseFocus(); } else if (window->parent && waskey) { @@ -3105,20 +3095,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]) { @@ -3283,6 +3272,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 7f0f5f01cf..2ed845bcf8 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 e2de400102..516419c281 100644 --- a/src/video/wayland/SDL_waylandvideo.c +++ b/src/video/wayland/SDL_waylandvideo.c @@ -632,6 +632,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 8ef5794a6f..b019eff2f9 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 327d29655e..343a0ed251 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 e8dae32b63..97bf3918ff 100644 --- a/src/video/windows/SDL_windowsevents.c +++ b/src/video/windows/SDL_windowsevents.c @@ -354,7 +354,7 @@ static void WIN_UpdateFocus(SDL_Window *window, bool expect_focus, DWORD pos) } } - 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 173af204d1..34b0561c10 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,28 +677,30 @@ 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; - 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); + // 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 (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; @@ -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 e15e5780f7..216ef13723 100644 --- a/src/video/windows/SDL_windowswindow.h +++ b/src/video/windows/SDL_windowswindow.h @@ -92,7 +92,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 ca340953ed..6dd7ce02eb 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; - } + abs_x += offset_x; + abs_y += offset_y; - offset_x += w->x; - offset_y += w->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); + 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); } - 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); 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,21 +2331,37 @@ 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; + + 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; } - wmhints->input = focusable ? True : False; - wmhints->flags |= InputHint; - - X11_XSetWMHints(display, data->xwindow, wmhints); - X11_XFree(wmhints); - return true; } diff --git a/src/video/x11/SDL_x11window.h b/src/video/x11/SDL_x11window.h index e84daced36..e7ca1ce4c6 100644 --- a/src/video/x11/SDL_x11window.h +++ b/src/video/x11/SDL_x11window.h @@ -75,7 +75,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 786f03c206..987eab8dd9 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)) {