SDL: Add a function to display the system menu for a window

From 70323a835023fb9427ffc4128957a40ef2f89ce7 Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Sun, 30 Jul 2023 11:24:24 -0400
Subject: [PATCH] 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).
---
 include/SDL3/SDL_video.h              | 23 +++++++++++++++++++++++
 src/dynapi/SDL_dynapi.sym             |  1 +
 src/dynapi/SDL_dynapi_overrides.h     |  1 +
 src/dynapi/SDL_dynapi_procs.h         |  1 +
 src/video/SDL_sysvideo.h              |  3 +++
 src/video/SDL_video.c                 | 13 +++++++++++++
 src/video/wayland/SDL_waylanddyn.h    |  1 +
 src/video/wayland/SDL_waylandevents.c | 13 +++++++++++++
 src/video/wayland/SDL_waylandsym.h    |  1 +
 src/video/wayland/SDL_waylandvideo.c  |  1 +
 src/video/wayland/SDL_waylandwindow.c | 17 +++++++++++++++++
 src/video/wayland/SDL_waylandwindow.h |  1 +
 src/video/windows/SDL_windowsvideo.c  |  1 +
 src/video/windows/SDL_windowswindow.c | 19 +++++++++++++++++++
 src/video/windows/SDL_windowswindow.h |  1 +
 src/video/x11/SDL_x11video.c          |  1 +
 src/video/x11/SDL_x11window.c         | 25 +++++++++++++++++++++++++
 src/video/x11/SDL_x11window.h         |  1 +
 18 files changed, 124 insertions(+)

diff --git a/include/SDL3/SDL_video.h b/include/SDL3/SDL_video.h
index 528ce8b3810c..ac4e8dab47c3 100644
--- a/include/SDL3/SDL_video.h
+++ b/include/SDL3/SDL_video.h
@@ -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.
  *
diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym
index da0735115d7e..59123eaa1fef 100644
--- a/src/dynapi/SDL_dynapi.sym
+++ b/src/dynapi/SDL_dynapi.sym
@@ -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: *;
 };
diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h
index 0cc9bb23a7d9..da10b99114d6 100644
--- a/src/dynapi/SDL_dynapi_overrides.h
+++ b/src/dynapi/SDL_dynapi_overrides.h
@@ -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
diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h
index 45d90eed0be8..00521cc50087 100644
--- a/src/dynapi/SDL_dynapi_procs.h
+++ b/src/dynapi/SDL_dynapi_procs.h
@@ -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)
diff --git a/src/video/SDL_sysvideo.h b/src/video/SDL_sysvideo.h
index 034d802b0031..26d39120682f 100644
--- a/src/video/SDL_sysvideo.h
+++ b/src/video/SDL_sysvideo.h
@@ -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;
diff --git a/src/video/SDL_video.c b/src/video/SDL_video.c
index 32cec7965aee..708cbc3768b9 100644
--- a/src/video/SDL_video.c
+++ b/src/video/SDL_video.c
@@ -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);
diff --git a/src/video/wayland/SDL_waylanddyn.h b/src/video/wayland/SDL_waylanddyn.h
index fa6b50035738..76cfb295c9fc 100644
--- a/src/video/wayland/SDL_waylanddyn.h
+++ b/src/video/wayland/SDL_waylanddyn.h
@@ -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)
diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c
index 9ee285f899f1..5a14655eaf86 100644
--- a/src/video/wayland/SDL_waylandevents.c
+++ b/src/video/wayland/SDL_waylandevents.c
@@ -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;
     }
diff --git a/src/video/wayland/SDL_waylandsym.h b/src/video/wayland/SDL_waylandsym.h
index 2d0eb735579d..494104b15df2 100644
--- a/src/video/wayland/SDL_waylandsym.h
+++ b/src/video/wayland/SDL_waylandsym.h
@@ -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 *))
diff --git a/src/video/wayland/SDL_waylandvideo.c b/src/video/wayland/SDL_waylandvideo.c
index cec877530ce5..a891b9079e9f 100644
--- a/src/video/wayland/SDL_waylandvideo.c
+++ b/src/video/wayland/SDL_waylandvideo.c
@@ -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())
diff --git a/src/video/wayland/SDL_waylandwindow.c b/src/video/wayland/SDL_waylandwindow.c
index 442224f21a3d..4568ecb4d49b 100644
--- a/src/video/wayland/SDL_waylandwindow.c
+++ b/src/video/wayland/SDL_waylandwindow.c
@@ -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;
diff --git a/src/video/wayland/SDL_waylandwindow.h b/src/video/wayland/SDL_waylandwindow.h
index 25e42fb231f8..98dc8b757b0a 100644
--- a/src/video/wayland/SDL_waylandwindow.h
+++ b/src/video/wayland/SDL_waylandwindow.h
@@ -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);
 
diff --git a/src/video/windows/SDL_windowsvideo.c b/src/video/windows/SDL_windowsvideo.c
index bb7fe1524084..52bbb8e67e33 100644
--- a/src/video/windows/SDL_windowsvideo.c
+++ b/src/video/windows/SDL_windowsvideo.c
@@ -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;
diff --git a/src/video/windows/SDL_windowswindow.c b/src/video/windows/SDL_windowswindow.c
index 076cccee3e4d..6218078e28df 100644
--- a/src/video/windows/SDL_windowswindow.c
+++ b/src/video/windows/SDL_windowswindow.c
@@ -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)
diff --git a/src/video/windows/SDL_windowswindow.h b/src/video/windows/SDL_windowswindow.h
index 956c5ec304fc..f6f0331a3e74 100644
--- a/src/video/windows/SDL_windowswindow.h
+++ b/src/video/windows/SDL_windowswindow.h
@@ -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
diff --git a/src/video/x11/SDL_x11video.c b/src/video/x11/SDL_x11video.c
index 8b14e55c45cc..70cc85a35ea1 100644
--- a/src/video/x11/SDL_x11video.c
+++ b/src/video/x11/SDL_x11video.c
@@ -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;
diff --git a/src/video/x11/SDL_x11window.c b/src/video/x11/SDL_x11window.c
index 36e7c9f2c999..e7d04a8ccb89 100644
--- a/src/video/x11/SDL_x11window.c
+++ b/src/video/x11/SDL_x11window.c
@@ -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 */
diff --git a/src/video/x11/SDL_x11window.h b/src/video/x11/SDL_x11window.h
index b5da61a4cea7..ff6b98ba37ff 100644
--- a/src/video/x11/SDL_x11window.h
+++ b/src/video/x11/SDL_x11window.h
@@ -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);