tray: Add icon click callbacks for Windows and macOS (#14964)

This commit is contained in:
Jesse Chounard
2026-02-04 14:50:00 -06:00
committed by GitHub
parent bd472b43f5
commit 70e16a8d13
8 changed files with 343 additions and 11 deletions

View File

@@ -34,6 +34,7 @@
#include <SDL3/SDL_stdinc.h>
#include <SDL3/SDL_error.h>
#include <SDL3/SDL_properties.h>
#include <SDL3/SDL_surface.h>
#include <SDL3/SDL_video.h>
@@ -96,6 +97,23 @@ typedef Uint32 SDL_TrayEntryFlags;
*/
typedef void (SDLCALL *SDL_TrayCallback)(void *userdata, SDL_TrayEntry *entry);
/**
* A callback that is invoked when the tray icon is clicked.
*
* \param userdata an optional pointer to pass extra data to the callback when
* it will be invoked. May be NULL.
* \param tray the tray that was clicked.
* \returns true to show the tray menu after the callback returns, false to
* skip showing the menu. This return value is only used for left
* and right click callbacks; other mouse events ignore the return
* value.
*
* \since This datatype is available since SDL 3.6.0.
*
* \sa SDL_CreateTrayWithProperties
*/
typedef bool (SDLCALL *SDL_TrayClickCallback)(void *userdata, SDL_Tray *tray);
/**
* Create an icon to be placed in the operating system's tray, or equivalent.
*
@@ -114,12 +132,69 @@ typedef void (SDLCALL *SDL_TrayCallback)(void *userdata, SDL_TrayEntry *entry);
*
* \since This function is available since SDL 3.2.0.
*
* \sa SDL_CreateTrayWithProperties
* \sa SDL_CreateTrayMenu
* \sa SDL_GetTrayMenu
* \sa SDL_DestroyTray
*/
extern SDL_DECLSPEC SDL_Tray * SDLCALL SDL_CreateTray(SDL_Surface *icon, const char *tooltip);
/**
* Create an icon to be placed in the operating system's tray, or equivalent.
*
* Many platforms advise not using a system tray unless persistence is a
* necessary feature. Avoid needlessly creating a tray icon, as the user may
* feel like it clutters their interface.
*
* Using tray icons require the video subsystem.
*
* These are the supported properties:
*
* - `SDL_PROP_TRAY_CREATE_ICON_POINTER`: an SDL_Surface to be used as the
* tray icon. May be NULL.
* - `SDL_PROP_TRAY_CREATE_TOOLTIP_STRING`: a tooltip to be displayed when
* the mouse hovers the icon in UTF-8 encoding. Not supported on all
* platforms. May be NULL.
* - `SDL_PROP_TRAY_CREATE_USERDATA_POINTER`: an optional pointer to
* associate with the tray, which will be passed to click callbacks.
* May be NULL.
* - `SDL_PROP_TRAY_CREATE_LEFTCLICK_CALLBACK_POINTER`: an SDL_TrayClickCallback
* to be invoked when the tray icon is left-clicked. Not supported on all
* platforms. The callback should return true to show the default menu, or
* false to skip showing it. May be NULL.
* - `SDL_PROP_TRAY_CREATE_RIGHTCLICK_CALLBACK_POINTER`: an SDL_TrayClickCallback
* to be invoked when the tray icon is right-clicked. Not supported on all
* platforms. The callback should return true to show the default menu, or
* false to skip showing it. May be NULL.
* - `SDL_PROP_TRAY_CREATE_MIDDLECLICK_CALLBACK_POINTER`: an SDL_TrayClickCallback
* to be invoked when the tray icon is middle-clicked. Not supported on all
* platforms. May be NULL.
* - `SDL_PROP_TRAY_CREATE_DOUBLECLICK_CALLBACK_POINTER`: an SDL_TrayClickCallback
* to be invoked when the tray icon is double-clicked. Not supported on all
* platforms. May be NULL.
*
* \param props the properties to use.
* \returns The newly created system tray icon.
*
* \threadsafety This function should only be called on the main thread.
*
* \since This function is available since SDL 3.6.0.
*
* \sa SDL_CreateTray
* \sa SDL_CreateTrayMenu
* \sa SDL_GetTrayMenu
* \sa SDL_DestroyTray
*/
extern SDL_DECLSPEC SDL_Tray * SDLCALL SDL_CreateTrayWithProperties(SDL_PropertiesID props);
#define SDL_PROP_TRAY_CREATE_ICON_POINTER "SDL.tray.create.icon"
#define SDL_PROP_TRAY_CREATE_TOOLTIP_STRING "SDL.tray.create.tooltip"
#define SDL_PROP_TRAY_CREATE_USERDATA_POINTER "SDL.tray.create.userdata"
#define SDL_PROP_TRAY_CREATE_LEFTCLICK_CALLBACK_POINTER "SDL.tray.create.leftclick_callback"
#define SDL_PROP_TRAY_CREATE_RIGHTCLICK_CALLBACK_POINTER "SDL.tray.create.rightclick_callback"
#define SDL_PROP_TRAY_CREATE_MIDDLECLICK_CALLBACK_POINTER "SDL.tray.create.middleclick_callback"
#define SDL_PROP_TRAY_CREATE_DOUBLECLICK_CALLBACK_POINTER "SDL.tray.create.doubleclick_callback"
/**
* Updates the system tray icon's icon.
*

View File

@@ -1279,6 +1279,7 @@ SDL3_0.0.0 {
SDL_OpenXR_LoadLibrary;
SDL_OpenXR_UnloadLibrary;
SDL_OpenXR_GetXrGetInstanceProcAddr;
SDL_CreateTrayWithProperties;
# extra symbols go here (don't modify this line)
local: *;
};

View File

@@ -1305,3 +1305,4 @@
#define SDL_OpenXR_LoadLibrary SDL_OpenXR_LoadLibrary_REAL
#define SDL_OpenXR_UnloadLibrary SDL_OpenXR_UnloadLibrary_REAL
#define SDL_OpenXR_GetXrGetInstanceProcAddr SDL_OpenXR_GetXrGetInstanceProcAddr_REAL
#define SDL_CreateTrayWithProperties SDL_CreateTrayWithProperties_REAL

View File

@@ -1313,3 +1313,4 @@ SDL_DYNAPI_PROC(XrResult,SDL_DestroyGPUXRSwapchain,(SDL_GPUDevice *a,XrSwapchain
SDL_DYNAPI_PROC(bool,SDL_OpenXR_LoadLibrary,(void),(),return)
SDL_DYNAPI_PROC(void,SDL_OpenXR_UnloadLibrary,(void),(),)
SDL_DYNAPI_PROC(PFN_xrGetInstanceProcAddr,SDL_OpenXR_GetXrGetInstanceProcAddr,(void),(),return)
SDL_DYNAPI_PROC(SDL_Tray*,SDL_CreateTrayWithProperties,(SDL_PropertiesID a),(a),return)

View File

@@ -28,7 +28,18 @@
#include "../SDL_tray_utils.h"
#include "../../video/SDL_surface_c.h"
/* applicationDockMenu */
/* Forward declaration */
struct SDL_Tray;
/* Objective-C helper class to handle status item button clicks */
@interface SDLTrayClickHandler : NSObject
@property (nonatomic, assign) struct SDL_Tray *tray;
@property (nonatomic, assign) NSTimeInterval lastLeftClickTime;
@property (nonatomic, strong) id middleClickMonitor;
- (void)handleClick:(id)sender;
- (void)startMonitoringMiddleClicks;
- (void)stopMonitoringMiddleClicks;
@end
struct SDL_TrayMenu {
NSMenu *nsmenu;
@@ -56,8 +67,105 @@ struct SDL_Tray {
NSStatusItem *statusItem;
SDL_TrayMenu *menu;
SDLTrayClickHandler *clickHandler;
void *userdata;
SDL_TrayClickCallback left_click_callback;
SDL_TrayClickCallback right_click_callback;
SDL_TrayClickCallback middle_click_callback;
SDL_TrayClickCallback double_click_callback;
};
@implementation SDLTrayClickHandler
- (void)handleClick:(id)sender
{
if (!self.tray) {
return;
}
NSEvent *event = [NSApp currentEvent];
NSUInteger buttonNumber = [event buttonNumber];
bool show_menu = false;
if (buttonNumber == 0) {
/* Left click - check for double-click ourselves */
NSTimeInterval now = [NSDate timeIntervalSinceReferenceDate];
NSTimeInterval doubleClickInterval = [NSEvent doubleClickInterval];
if (self.tray->double_click_callback && (now - self.lastLeftClickTime) <= doubleClickInterval) {
/* Double-click */
self.tray->double_click_callback(self.tray->userdata, self.tray);
self.lastLeftClickTime = 0; /* Reset to prevent triple-click from triggering another double */
} else {
/* Single left click */
self.lastLeftClickTime = now;
if (self.tray->left_click_callback) {
show_menu = self.tray->left_click_callback(self.tray->userdata, self.tray);
} else {
show_menu = true;
}
}
} else if (buttonNumber == 1) {
/* Right click */
if (self.tray->right_click_callback) {
show_menu = self.tray->right_click_callback(self.tray->userdata, self.tray);
} else {
show_menu = true;
}
} else if (buttonNumber == 2) {
/* Middle click */
if (self.tray->middle_click_callback) {
self.tray->middle_click_callback(self.tray->userdata, self.tray);
}
}
if (show_menu && self.tray->menu) {
[self.tray->statusItem popUpStatusItemMenu:self.tray->menu->nsmenu];
}
}
- (void)startMonitoringMiddleClicks
{
if (self.middleClickMonitor) {
return;
}
__weak SDLTrayClickHandler *weakSelf = self;
self.middleClickMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskOtherMouseUp handler:^NSEvent *(NSEvent *event) {
SDLTrayClickHandler *strongSelf = weakSelf;
if (!strongSelf || !strongSelf.tray || [event buttonNumber] != 2) {
return event;
}
/* Check if the click is within the status item's button bounds */
NSPoint clickLocation = [event locationInWindow];
NSWindow *statusItemWindow = strongSelf.tray->statusItem.button.window;
if (statusItemWindow && event.window == statusItemWindow) {
NSPoint localPoint = [strongSelf.tray->statusItem.button convertPoint:clickLocation fromView:nil];
if (NSPointInRect(localPoint, strongSelf.tray->statusItem.button.bounds)) {
if (strongSelf.tray->middle_click_callback) {
strongSelf.tray->middle_click_callback(strongSelf.tray->userdata, strongSelf.tray);
}
}
}
return event;
}];
}
- (void)stopMonitoringMiddleClicks
{
if (self.middleClickMonitor) {
[NSEvent removeMonitor:self.middleClickMonitor];
self.middleClickMonitor = nil;
}
}
@end
static void DestroySDLMenu(SDL_TrayMenu *menu)
{
for (int i = 0; i < menu->nEntries; i++) {
@@ -82,13 +190,16 @@ void SDL_UpdateTrays(void)
{
}
SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip)
SDL_Tray *SDL_CreateTrayWithProperties(SDL_PropertiesID props)
{
if (!SDL_IsMainThread()) {
SDL_SetError("This function should be called on the main thread");
return NULL;
}
SDL_Surface *icon = (SDL_Surface *)SDL_GetPointerProperty(props, SDL_PROP_TRAY_CREATE_ICON_POINTER, NULL);
const char *tooltip = SDL_GetStringProperty(props, SDL_PROP_TRAY_CREATE_TOOLTIP_STRING, NULL);
if (icon) {
icon = SDL_ConvertSurface(icon, SDL_PIXELFORMAT_RGBA32);
if (!icon) {
@@ -102,6 +213,12 @@ SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip)
return NULL;
}
tray->userdata = SDL_GetPointerProperty(props, SDL_PROP_TRAY_CREATE_USERDATA_POINTER, NULL);
tray->left_click_callback = (SDL_TrayClickCallback)SDL_GetPointerProperty(props, SDL_PROP_TRAY_CREATE_LEFTCLICK_CALLBACK_POINTER, NULL);
tray->right_click_callback = (SDL_TrayClickCallback)SDL_GetPointerProperty(props, SDL_PROP_TRAY_CREATE_RIGHTCLICK_CALLBACK_POINTER, NULL);
tray->middle_click_callback = (SDL_TrayClickCallback)SDL_GetPointerProperty(props, SDL_PROP_TRAY_CREATE_MIDDLECLICK_CALLBACK_POINTER, NULL);
tray->double_click_callback = (SDL_TrayClickCallback)SDL_GetPointerProperty(props, SDL_PROP_TRAY_CREATE_DOUBLECLICK_CALLBACK_POINTER, NULL);
tray->statusItem = nil;
tray->statusBar = [NSStatusBar systemStatusBar];
tray->statusItem = [tray->statusBar statusItemWithLength:NSVariableStatusItemLength];
@@ -140,11 +257,40 @@ SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip)
SDL_DestroySurface(icon);
}
/* Create click handler and set up button to receive clicks */
tray->clickHandler = [[SDLTrayClickHandler alloc] init];
tray->clickHandler.tray = tray;
[tray->statusItem.button setTarget:tray->clickHandler];
[tray->statusItem.button setAction:@selector(handleClick:)];
[tray->statusItem.button sendActionOn:(NSEventMaskLeftMouseUp | NSEventMaskRightMouseUp)];
/* Start monitoring for middle clicks since status items don't receive them via the normal action mechanism */
[tray->clickHandler startMonitoringMiddleClicks];
SDL_RegisterTray(tray);
return tray;
}
SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip)
{
SDL_Tray *tray;
SDL_PropertiesID props = SDL_CreateProperties();
if (!props) {
return NULL;
}
if (icon) {
SDL_SetPointerProperty(props, SDL_PROP_TRAY_CREATE_ICON_POINTER, icon);
}
if (tooltip) {
SDL_SetStringProperty(props, SDL_PROP_TRAY_CREATE_TOOLTIP_STRING, tooltip);
}
tray = SDL_CreateTrayWithProperties(props);
SDL_DestroyProperties(props);
return tray;
}
void SDL_SetTrayIcon(SDL_Tray *tray, SDL_Surface *icon)
{
if (!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) {
@@ -216,7 +362,7 @@ SDL_TrayMenu *SDL_CreateTrayMenu(SDL_Tray *tray)
NSMenu *nsmenu = [[NSMenu alloc] init];
[nsmenu setAutoenablesItems:FALSE];
[tray->statusItem setMenu:nsmenu];
/* Don't set menu on statusItem - we handle menu display manually in the click handler */
tray->menu = menu;
menu->nsmenu = nsmenu;
@@ -518,6 +664,12 @@ void SDL_DestroyTray(SDL_Tray *tray)
DestroySDLMenu(tray->menu);
}
if (tray->clickHandler) {
[tray->clickHandler stopMonitoringMiddleClicks];
tray->clickHandler.tray = NULL;
tray->clickHandler = nil;
}
SDL_free(tray);
}

View File

@@ -29,6 +29,12 @@ void SDL_UpdateTrays(void)
{
}
SDL_Tray *SDL_CreateTrayWithProperties(SDL_PropertiesID props)
{
SDL_Unsupported();
return NULL;
}
SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip)
{
SDL_Unsupported();

View File

@@ -239,7 +239,7 @@ void SDL_UpdateTrays(void)
}
}
SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip)
SDL_Tray *SDL_CreateTrayWithProperties(SDL_PropertiesID props)
{
if (!SDL_IsMainThread()) {
SDL_SetError("This function should be called on the main thread");
@@ -250,6 +250,8 @@ SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip)
return NULL;
}
SDL_Surface *icon = (SDL_Surface *)SDL_GetPointerProperty(props, SDL_PROP_TRAY_CREATE_ICON_POINTER, NULL);
SDL_Tray *tray = NULL;
SDL_GtkContext *gtk = SDL_Gtk_EnterContext();
if (!gtk) {
@@ -327,6 +329,24 @@ tray_error:
return NULL;
}
SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip)
{
SDL_Tray *tray;
SDL_PropertiesID props = SDL_CreateProperties();
if (!props) {
return NULL;
}
if (icon) {
SDL_SetPointerProperty(props, SDL_PROP_TRAY_CREATE_ICON_POINTER, icon);
}
if (tooltip) {
SDL_SetStringProperty(props, SDL_PROP_TRAY_CREATE_TOOLTIP_STRING, tooltip);
}
tray = SDL_CreateTrayWithProperties(props);
SDL_DestroyProperties(props);
return tray;
}
void SDL_SetTrayIcon(SDL_Tray *tray, SDL_Surface *icon)
{
if (!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) {

View File

@@ -62,6 +62,13 @@ struct SDL_Tray {
HWND hwnd;
HICON icon;
SDL_TrayMenu *menu;
void *userdata;
SDL_TrayClickCallback left_click_callback;
SDL_TrayClickCallback right_click_callback;
SDL_TrayClickCallback middle_click_callback;
SDL_TrayClickCallback double_click_callback;
bool ignore_next_left_up;
};
static UINT_PTR get_next_id(void)
@@ -119,10 +126,47 @@ LRESULT CALLBACK TrayWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lPar
switch (uMsg) {
case WM_TRAYICON:
if (LOWORD(lParam) == WM_CONTEXTMENU || LOWORD(lParam) == WM_LBUTTONUP) {
SetForegroundWindow(hwnd);
{
bool show_menu = false;
if (tray->menu) {
switch (LOWORD(lParam)) {
case WM_LBUTTONUP:
if (tray->ignore_next_left_up) {
tray->ignore_next_left_up = false;
} else if (tray->left_click_callback) {
show_menu = tray->left_click_callback(tray->userdata, tray);
} else {
show_menu = true;
}
break;
case WM_CONTEXTMENU:
if (tray->right_click_callback) {
show_menu = tray->right_click_callback(tray->userdata, tray);
} else {
show_menu = true;
}
break;
case WM_MBUTTONUP:
if (tray->middle_click_callback) {
tray->middle_click_callback(tray->userdata, tray);
}
break;
case WM_LBUTTONDBLCLK:
if (tray->double_click_callback) {
tray->double_click_callback(tray->userdata, tray);
/* Suppress the WM_LBUTTONUP that follows a double-click, so we
don't fire both double-click and left-click callbacks. This
matches the behavior on other platforms. */
tray->ignore_next_left_up = true;
}
break;
}
if (show_menu && tray->menu) {
SetForegroundWindow(hwnd);
TrackPopupMenu(tray->menu->hMenu, TPM_BOTTOMALIGN | TPM_RIGHTALIGN, GET_X_LPARAM(wParam), GET_Y_LPARAM(wParam), 0, hwnd, NULL);
}
}
@@ -267,7 +311,7 @@ static bool SDL_RegisterTrayClass(LPCWSTR className)
return true;
}
SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip)
SDL_Tray *SDL_CreateTrayWithProperties(SDL_PropertiesID props)
{
if (!SDL_IsMainThread()) {
SDL_SetError("This function should be called on the main thread");
@@ -280,9 +324,19 @@ SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip)
return NULL;
}
SDL_Surface *icon = (SDL_Surface *)SDL_GetPointerProperty(props, SDL_PROP_TRAY_CREATE_ICON_POINTER, NULL);
const char *tooltip = SDL_GetStringProperty(props, SDL_PROP_TRAY_CREATE_TOOLTIP_STRING, NULL);
tray->userdata = SDL_GetPointerProperty(props, SDL_PROP_TRAY_CREATE_USERDATA_POINTER, NULL);
tray->left_click_callback = (SDL_TrayClickCallback)SDL_GetPointerProperty(props, SDL_PROP_TRAY_CREATE_LEFTCLICK_CALLBACK_POINTER, NULL);
tray->right_click_callback = (SDL_TrayClickCallback)SDL_GetPointerProperty(props, SDL_PROP_TRAY_CREATE_RIGHTCLICK_CALLBACK_POINTER, NULL);
tray->middle_click_callback = (SDL_TrayClickCallback)SDL_GetPointerProperty(props, SDL_PROP_TRAY_CREATE_MIDDLECLICK_CALLBACK_POINTER, NULL);
tray->double_click_callback = (SDL_TrayClickCallback)SDL_GetPointerProperty(props, SDL_PROP_TRAY_CREATE_DOUBLECLICK_CALLBACK_POINTER, NULL);
tray->menu = NULL;
if (!SDL_RegisterTrayClass(TEXT("SDL_TRAY"))) {
SDL_SetError("Failed to register SDL_TRAY window class");
SDL_free(tray);
return NULL;
}
tray->hwnd = CreateWindowEx(0, TEXT("SDL_TRAY"), NULL, WS_OVERLAPPEDWINDOW,
@@ -297,9 +351,13 @@ SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip)
tray->nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP | NIF_SHOWTIP;
tray->nid.uCallbackMessage = WM_TRAYICON;
tray->nid.uVersion = NOTIFYICON_VERSION_4;
wchar_t *tooltipw = WIN_UTF8ToStringW(tooltip);
SDL_wcslcpy(tray->nid.szTip, tooltipw, sizeof(tray->nid.szTip) / sizeof(*tray->nid.szTip));
SDL_free(tooltipw);
if (tooltip) {
wchar_t *tooltipw = WIN_UTF8ToStringW(tooltip);
if(tooltipw) {
SDL_wcslcpy(tray->nid.szTip, tooltipw, sizeof(tray->nid.szTip) / sizeof(*tray->nid.szTip));
SDL_free(tooltipw);
}
}
if (icon) {
tray->nid.hIcon = WIN_CreateIconFromSurface(icon);
@@ -324,6 +382,24 @@ SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip)
return tray;
}
SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip)
{
SDL_Tray *tray;
SDL_PropertiesID props = SDL_CreateProperties();
if (!props) {
return NULL;
}
if (icon) {
SDL_SetPointerProperty(props, SDL_PROP_TRAY_CREATE_ICON_POINTER, icon);
}
if (tooltip) {
SDL_SetStringProperty(props, SDL_PROP_TRAY_CREATE_TOOLTIP_STRING, tooltip);
}
tray = SDL_CreateTrayWithProperties(props);
SDL_DestroyProperties(props);
return tray;
}
void SDL_SetTrayIcon(SDL_Tray *tray, SDL_Surface *icon)
{
if (!SDL_ObjectValid(tray, SDL_OBJECT_TYPE_TRAY)) {