Add a function to display the system menu for a window

Add SDL_ShowWindowSystemMenu() to display the system-level menu for windows. Typically, this is done by right-clicking on the system provided window decorations, however, if an application is rendering its own client-side decorations, there is currently no way to display it. This menu is provided by the system and can provide privileged desktop functionality such as moving or pinning a window to a specific workspace or display, setting the always-on-top property, or taking screenshots. In many cases, there are no APIs which allow applications to perform these actions manually.

Implemented for Wayland via functionality provided by the xdg_toplevel protocol, Win32 via the undocumented message 0x313 (typically called WM_POPUPSYSTEMMENU), and X11 via the "_GTK_SHOW_WINDOW_MENU" atom (supported in GNOME and KDE).
main
Frank Praznik 2023-07-30 11:24:24 -04:00
parent be5f66c84e
commit 70323a8350
18 changed files with 124 additions and 0 deletions

View File

@ -118,6 +118,7 @@ typedef enum
* \sa SDL_SetWindowResizable()
* \sa SDL_SetWindowTitle()
* \sa SDL_ShowWindow()
* \sa SDL_ShowWindowSystemMenu()
*/
typedef struct SDL_Window SDL_Window;
@ -1675,6 +1676,28 @@ extern DECLSPEC int SDLCALL SDL_SetWindowModalFor(SDL_Window *modal_window, SDL_
*/
extern DECLSPEC int SDLCALL SDL_SetWindowInputFocus(SDL_Window *window);
/**
* Display the system-level window menu.
*
* This default window menu is provided by the system and on some platforms
* provides functionality for setting or changing privileged state on the
* window, such as moving it between workspaces or displays, or toggling the
* always-on-top property.
*
* On platforms or desktops where this is unsupported, this function
* does nothing.
*
* \param window the window for which the menu will be displayed
* \param x the x coordinate of the menu, relative to the origin (top-left) of the client area
* \param y the y coordinate of the menu, relative to the origin (top-left) of the client area
* \returns 0 on success or a negative error code on failure; call
* SDL_GetError() for more information.
*
* \since This function is available since SDL 3.0.0.
*
*/
extern DECLSPEC int SDLCALL SDL_ShowWindowSystemMenu(SDL_Window *window, int x, int y);
/**
* Possible return values from the SDL_HitTest callback.
*

View File

@ -887,6 +887,7 @@ SDL3_0.0.0 {
SDL_UnpauseAudioDevice;
SDL_IsAudioDevicePaused;
SDL_GetAudioStreamBinding;
SDL_ShowWindowSystemMenu;
# extra symbols go here (don't modify this line)
local: *;
};

View File

@ -912,3 +912,4 @@
#define SDL_UnpauseAudioDevice SDL_UnpauseAudioDevice_REAL
#define SDL_IsAudioDevicePaused SDL_IsAudioDevicePaused_REAL
#define SDL_GetAudioStreamBinding SDL_GetAudioStreamBinding_REAL
#define SDL_ShowWindowSystemMenu SDL_ShowWindowSystemMenu_REAL

View File

@ -956,3 +956,4 @@ SDL_DYNAPI_PROC(int,SDL_PauseAudioDevice,(SDL_AudioDeviceID a),(a),return)
SDL_DYNAPI_PROC(int,SDL_UnpauseAudioDevice,(SDL_AudioDeviceID a),(a),return)
SDL_DYNAPI_PROC(SDL_bool,SDL_IsAudioDevicePaused,(SDL_AudioDeviceID a),(a),return)
SDL_DYNAPI_PROC(SDL_AudioDeviceID,SDL_GetAudioStreamBinding,(SDL_AudioStream *a),(a),return)
SDL_DYNAPI_PROC(int,SDL_ShowWindowSystemMenu,(SDL_Window *a, int b, int c),(a,b,c),return)

View File

@ -354,6 +354,9 @@ struct SDL_VideoDevice
/* Tell window that app enabled drag'n'drop events */
void (*AcceptDragAndDrop)(SDL_Window *window, SDL_bool accept);
/* Display the system-level window menu */
void (*ShowWindowSystemMenu)(SDL_Window *window, int x, int y);
/* * * */
/* Data common to all drivers */
SDL_threadID thread;

View File

@ -5054,6 +5054,19 @@ SDL_bool SDL_ShouldAllowTopmost(void)
return SDL_GetHintBoolean(SDL_HINT_ALLOW_TOPMOST, SDL_TRUE);
}
int SDL_ShowWindowSystemMenu(SDL_Window *window, int x, int y)
{
CHECK_WINDOW_MAGIC(window, -1)
CHECK_WINDOW_NOT_POPUP(window, -1)
if (_this->ShowWindowSystemMenu) {
_this->ShowWindowSystemMenu(window, x, y);
return 0;
}
return SDL_Unsupported();
}
int SDL_SetWindowHitTest(SDL_Window *window, SDL_HitTest callback, void *callback_data)
{
CHECK_WINDOW_MAGIC(window, -1);

View File

@ -158,6 +158,7 @@ void SDL_WAYLAND_UnloadSymbols(void);
#define libdecor_frame_is_visible (*WAYLAND_libdecor_frame_is_visible)
#define libdecor_frame_is_floating (*WAYLAND_libdecor_frame_is_floating)
#define libdecor_frame_set_parent (*WAYLAND_libdecor_frame_set_parent)
#define libdecor_frame_show_window_menu (*WAYLAND_libdecor_frame_show_window_menu)
#define libdecor_frame_get_xdg_surface (*WAYLAND_libdecor_frame_get_xdg_surface)
#define libdecor_frame_get_xdg_toplevel (*WAYLAND_libdecor_frame_get_xdg_toplevel)
#define libdecor_frame_translate_coordinate (*WAYLAND_libdecor_frame_translate_coordinate)

View File

@ -554,6 +554,19 @@ static void pointer_handle_leave(void *data, struct wl_pointer *pointer,
}
if (input->pointer_focus) {
SDL_WindowData *wind = (SDL_WindowData *)wl_surface_get_user_data(surface);
if (wind) {
/* Clear the capture flag and raise all buttons */
wind->sdlwindow->flags &= ~SDL_WINDOW_MOUSE_CAPTURE;
SDL_SendMouseButton(Wayland_GetPointerTimestamp(input, 0), wind->sdlwindow, 0, SDL_RELEASED, SDL_BUTTON_LEFT);
SDL_SendMouseButton(Wayland_GetPointerTimestamp(input, 0), wind->sdlwindow, 0, SDL_RELEASED, SDL_BUTTON_RIGHT);
SDL_SendMouseButton(Wayland_GetPointerTimestamp(input, 0), wind->sdlwindow, 0, SDL_RELEASED, SDL_BUTTON_MIDDLE);
SDL_SendMouseButton(Wayland_GetPointerTimestamp(input, 0), wind->sdlwindow, 0, SDL_RELEASED, SDL_BUTTON_X1);
SDL_SendMouseButton(Wayland_GetPointerTimestamp(input, 0), wind->sdlwindow, 0, SDL_RELEASED, SDL_BUTTON_X2);
}
SDL_SetMouseFocus(NULL);
input->pointer_focus = NULL;
}

View File

@ -204,6 +204,7 @@ SDL_WAYLAND_SYM(bool, libdecor_frame_is_visible, (struct libdecor_frame *))
SDL_WAYLAND_SYM(bool, libdecor_frame_is_floating, (struct libdecor_frame *))
SDL_WAYLAND_SYM(void, libdecor_frame_set_parent, (struct libdecor_frame *,\
struct libdecor_frame *))
SDL_WAYLAND_SYM(void, libdecor_frame_show_window_menu, (struct libdecor_frame *, struct wl_seat *, uint32_t, int, int))
SDL_WAYLAND_SYM(struct xdg_surface *, libdecor_frame_get_xdg_surface, (struct libdecor_frame *))
SDL_WAYLAND_SYM(struct xdg_toplevel *, libdecor_frame_get_xdg_toplevel, (struct libdecor_frame *))
SDL_WAYLAND_SYM(void, libdecor_frame_translate_coordinate, (struct libdecor_frame *, int, int, int *, int *))

View File

@ -209,6 +209,7 @@ static SDL_VideoDevice *Wayland_CreateDevice(void)
device->SetWindowHitTest = Wayland_SetWindowHitTest;
device->FlashWindow = Wayland_FlashWindow;
device->HasScreenKeyboardSupport = Wayland_HasScreenKeyboardSupport;
device->ShowWindowSystemMenu = Wayland_ShowWindowSystemMenu;
#ifdef SDL_USE_LIBDBUS
if (SDL_SystemTheme_Init())

View File

@ -2357,6 +2357,23 @@ void Wayland_SetWindowTitle(SDL_VideoDevice *_this, SDL_Window *window)
WAYLAND_wl_display_flush(viddata->display);
}
void Wayland_ShowWindowSystemMenu(SDL_Window *window, int x, int y)
{
SDL_WindowData *wind = window->driverdata;
#ifdef HAVE_LIBDECOR_H
if (wind->shell_surface_type == WAYLAND_SURFACE_LIBDECOR) {
if (wind->shell_surface.libdecor.frame) {
libdecor_frame_show_window_menu(wind->shell_surface.libdecor.frame, wind->waylandData->input->seat, wind->waylandData->input->last_implicit_grab_serial, x, y);
}
} else
#endif
if (wind->shell_surface_type == WAYLAND_SURFACE_XDG_TOPLEVEL) {
if (wind->shell_surface.xdg.roleobj.toplevel) {
xdg_toplevel_show_window_menu(wind->shell_surface.xdg.roleobj.toplevel, wind->waylandData->input->seat, wind->waylandData->input->last_implicit_grab_serial, x, y);
}
}
}
int Wayland_SuspendScreenSaver(SDL_VideoDevice *_this)
{
SDL_VideoData *data = _this->driverdata;

View File

@ -150,6 +150,7 @@ extern void Wayland_SetWindowMaximumSize(SDL_VideoDevice *_this, SDL_Window *win
extern void Wayland_GetWindowSizeInPixels(SDL_VideoDevice *_this, SDL_Window *window, int *w, int *h);
extern int Wayland_SetWindowModalFor(SDL_VideoDevice *_this, SDL_Window *modal_window, SDL_Window *parent_window);
extern void Wayland_SetWindowTitle(SDL_VideoDevice *_this, SDL_Window *window);
extern void Wayland_ShowWindowSystemMenu(SDL_Window *window, int x, int y);
extern void Wayland_DestroyWindow(SDL_VideoDevice *_this, SDL_Window *window);
extern int Wayland_SuspendScreenSaver(SDL_VideoDevice *_this);

View File

@ -207,6 +207,7 @@ static SDL_VideoDevice *WIN_CreateDevice(void)
device->SetWindowHitTest = WIN_SetWindowHitTest;
device->AcceptDragAndDrop = WIN_AcceptDragAndDrop;
device->FlashWindow = WIN_FlashWindow;
device->ShowWindowSystemMenu = WIN_ShowWindowSystemMenu;
device->shape_driver.CreateShaper = Win32_CreateShaper;
device->shape_driver.SetWindowShape = Win32_SetWindowShape;

View File

@ -51,6 +51,14 @@ typedef HRESULT (WINAPI *DwmSetWindowAttribute_t)(HWND hwnd, DWORD dwAttribute,
#define SWP_NOCOPYBITS 0
#endif
/* An undocumented message to create a popup system menu
* - wParam is always 0
* - lParam = MAKELONG(x, y) where x and y are the screen coordinates where the menu should be displayed
*/
#ifndef WM_POPUPSYSTEMMENU
#define WM_POPUPSYSTEMMENU 0x313
#endif
/* #define HIGHDPI_DEBUG */
/* Fake window to help with DirectInput events. */
@ -1485,6 +1493,17 @@ int WIN_FlashWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_FlashOperati
return 0;
}
void WIN_ShowWindowSystemMenu(SDL_Window *window, int x, int y)
{
const SDL_WindowData *data = window->driverdata;
POINT pt;
pt.x = x;
pt.y = y;
ClientToScreen(data->hwnd, &pt);
SendMessage(data->hwnd, WM_POPUPSYSTEMMENU, 0, MAKELPARAM(pt.x, pt.y));
}
#endif /*!defined(__XBOXONE__) && !defined(__XBOXSERIES__)*/
void WIN_UpdateDarkModeForHWND(HWND hwnd)

View File

@ -108,6 +108,7 @@ extern void WIN_AcceptDragAndDrop(SDL_Window *window, SDL_bool accept);
extern int WIN_FlashWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_FlashOperation operation);
extern void WIN_UpdateDarkModeForHWND(HWND hwnd);
extern int WIN_SetWindowPositionInternal(SDL_Window *window, UINT flags);
extern void WIN_ShowWindowSystemMenu(SDL_Window *window, int x, int y);
/* Ends C function definitions when using C++ */
#ifdef __cplusplus

View File

@ -213,6 +213,7 @@ static SDL_VideoDevice *X11_CreateDevice(void)
device->SetWindowHitTest = X11_SetWindowHitTest;
device->AcceptDragAndDrop = X11_AcceptDragAndDrop;
device->FlashWindow = X11_FlashWindow;
device->ShowWindowSystemMenu = X11_ShowWindowSystemMenu;
#ifdef SDL_VIDEO_DRIVER_X11_XFIXES
device->SetWindowMouseRect = X11_SetWindowMouseRect;

View File

@ -1957,4 +1957,29 @@ int SDL_X11_SetWindowTitle(Display *display, Window xwindow, char *title)
return 0;
}
void X11_ShowWindowSystemMenu(SDL_Window *window, int x, int y)
{
SDL_WindowData *data = window->driverdata;
SDL_DisplayData *displaydata = SDL_GetDisplayDriverDataForWindow(window);
Display *display = data->videodata->display;
Window root = RootWindow(display, displaydata->screen);
XClientMessageEvent e;
Window childReturn;
int wx, wy;
SDL_zero(e);
X11_XTranslateCoordinates(display, data->xwindow, root, x, y, &wx, &wy, &childReturn);
e.type = ClientMessage;
e.window = data->xwindow;
e.message_type = X11_XInternAtom(display, "_GTK_SHOW_WINDOW_MENU", 0);
e.data.l[0] = 0; /* GTK device ID (unused) */
e.data.l[1] = wx; /* X coordinate relative to root */
e.data.l[2] = wy; /* Y coordinate relative to root */
e.format = 32;
X11_XSendEvent(display, root, False, SubstructureRedirectMask | SubstructureNotifyMask, (XEvent *)&e);
X11_XFlush(display);
}
#endif /* SDL_VIDEO_DRIVER_X11 */

View File

@ -115,6 +115,7 @@ extern int X11_GetWindowWMInfo(SDL_VideoDevice *_this, SDL_Window *window, struc
extern int X11_SetWindowHitTest(SDL_Window *window, SDL_bool enabled);
extern void X11_AcceptDragAndDrop(SDL_Window *window, SDL_bool accept);
extern int X11_FlashWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_FlashOperation operation);
extern void X11_ShowWindowSystemMenu(SDL_Window *window, int x, int y);
int SDL_X11_SetWindowTitle(Display *display, Window xwindow, char *title);
void X11_UpdateWindowPosition(SDL_Window *window);