Bladeren bron

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

Jesse Chounard 4 weken geleden
bovenliggende
commit
70e16a8d13

+ 75 - 0
include/SDL3/SDL_tray.h

@@ -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.
  *

+ 1 - 0
src/dynapi/SDL_dynapi.sym

@@ -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: *;
 };

+ 1 - 0
src/dynapi/SDL_dynapi_overrides.h

@@ -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

+ 1 - 0
src/dynapi/SDL_dynapi_procs.h

@@ -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)

+ 155 - 3
src/tray/cocoa/SDL_tray.m

@@ -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);
 }
 

+ 6 - 0
src/tray/dummy/SDL_tray.c

@@ -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();

+ 21 - 1
src/tray/unix/SDL_tray.c

@@ -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)) {

+ 83 - 7
src/tray/windows/SDL_tray.c

@@ -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;
+
+                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 (tray->menu) {
+                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)) {