From 921c3e3c6c310a192baeeb8376f4ebab2e7d225b Mon Sep 17 00:00:00 2001 From: Semphris Date: Tue, 17 Sep 2024 16:36:27 -0400 Subject: [PATCH] Document SDL_tray, add set icon/tooltip, fix bugs --- include/SDL3/SDL_tray.h | 249 +++++++++++++++++++++++++++++- src/dynapi/SDL_dynapi.sym | 2 + src/dynapi/SDL_dynapi_overrides.h | 2 + src/dynapi/SDL_dynapi_procs.h | 2 + src/tray/cocoa/SDL_tray.m | 55 +++++++ src/tray/unix/SDL_tray.c | 32 ++-- src/tray/windows/SDL_tray.c | 44 +++++- test/testtray.c | 1 - 8 files changed, 366 insertions(+), 21 deletions(-) diff --git a/include/SDL3/SDL_tray.h b/include/SDL3/SDL_tray.h index e13fb30ff..2102aa872 100644 --- a/include/SDL3/SDL_tray.h +++ b/include/SDL3/SDL_tray.h @@ -42,29 +42,266 @@ typedef struct SDL_Tray SDL_Tray; typedef struct SDL_TrayMenu SDL_TrayMenu; typedef struct SDL_TrayEntry SDL_TrayEntry; +/** + * Flags that control the creation of system tray entries. + * + * Some of these flags are mandatory; exactly one of them must be specified at + * the time a tray entry is created. Other flags are optional; zero or more of + * those can be OR'ed together with the mandatory flag. + */ typedef enum { - /* Mandatory; must be specified at creation time */ + /** Make the entry a simple button. This is a mandatory flag. */ SDL_TRAYENTRY_BUTTON = 0, - SDL_TRAYENTRY_CHECKBOX = (1 << 0), - SDL_TRAYENTRY_SUBMENU = (1 << 1), + /** Make the entry a checkbox. This is a mandatory flag. */ + SDL_TRAYENTRY_CHECKBOX, + /** Prepatre the entry to have a submenu. This is a mandatory flag. */ + SDL_TRAYENTRY_SUBMENU, - /* Optional; can be changed later */ - SDL_TRAYENTRY_DISABLED = (1 << 16), - SDL_TRAYENTRY_CHECKED = (1 << 17), + /** Make the entry disabled. */ + SDL_TRAYENTRY_DISABLED = (1 << 31), + /** Make the entry checked. This is valid only for checkboxes. */ + SDL_TRAYENTRY_CHECKED = (1 << 30), } SDL_TrayEntryFlags; +/** + * A callback that is invoked when a tray entry is selected. + * + * \param userdata an optional pointer to pass extra data to the callback when + * it will be invoked. + * \param entry the tray entry that was selected. + */ typedef void (*SDL_TrayCallback)(void *userdata, SDL_TrayEntry *entry); +/** + * 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. + * + * \param icon a surface to be used as icon. May be NULL. + * \param tooltip a tooltip to be displayed when the mouse hovers the icon. Not + * supported on all platforms. May be NULL. + * \returns The newly created system tray icon. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_SetTrayIcon + * \sa SDL_SetTrayTooltip + * \sa SDL_CreateTrayMenu + * \sa SDL_DestroyTray + */ extern SDL_DECLSPEC SDL_Tray *SDLCALL SDL_CreateTray(SDL_Surface *icon, const char *tooltip); + +/** + * Updates the system tray icon's icon. + * + * \param tray the tray icon to be updated. + * \param icon the new icon. May be NULL. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_CreateTray + * \sa SDL_SetTrayTooltip + * \sa SDL_CreateTrayMenu + * \sa SDL_DestroyTray + */ +extern SDL_DECLSPEC void SDLCALL SDL_SetTrayIcon(SDL_Tray *tray, SDL_Surface *icon); + +/** + * Updates the system tray icon's tooltip. + * + * \param tray the tray icon to be updated. + * \param tooltip the new tooltip. May be NULL. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_CreateTray + * \sa SDL_SetTrayIcon + * \sa SDL_CreateTrayMenu + * \sa SDL_DestroyTray + */ +extern SDL_DECLSPEC void SDLCALL SDL_SetTrayTooltip(SDL_Tray *tray, const char *tooltip); + +/** + * Create a menu for a system tray. + * + * This should be called at most once per tray icon. + * + * This function does the same thing as SDL_CreateTraySubmenu, except that it + * takes a SDL_Tray instead of a SDL_TrayEntry. + * + * A menu does not need to be destroyed; it will be destroyed with the tray. + * + * \param tray the tray to bind the menu to. + * \returns the newly created menu. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_CreateTray + * \sa SDL_CreateTraySubmenu + * \sa SDL_AppendTrayEntry + * \sa SDL_AppendTraySeparator + */ extern SDL_DECLSPEC SDL_TrayMenu *SDLCALL SDL_CreateTrayMenu(SDL_Tray *tray); + +/** + * Create a submenu for a system tray entry. + * + * This should be called at most once per tray entry. + * + * This function does the same thing as SDL_CreateTrayMenu, except that it + * takes a SDL_TrayEntry instead of a SDL_Tray. + * + * A menu does not need to be destroyed; it will be destroyed with the tray. + * + * \param entry the tray entry to bind the menu to. + * \returns the newly created menu. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_CreateTrayMenu + * \sa SDL_AppendTrayEntry + * \sa SDL_AppendTraySeparator + */ extern SDL_DECLSPEC SDL_TrayMenu *SDLCALL SDL_CreateTraySubmenu(SDL_TrayEntry *entry); + +/** + * Create a menu item (entry) and append it to the given menu. + * + * An entry does not need to be destroyed; it will be destroyed with the tray. + * + * \param menu the menu to append the entry to. + * \param label the label to be displayed on the entry. + * \param flags a combination of flags, some of which are mandatory. See SDL_TrayEntryFlags. + * \returns the newly created entry. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_TrayEntryFlags + * \sa SDL_CreateTrayMenu + * \sa SDL_CreateTraySubmenu + * \sa SDL_SetTrayEntryChecked + * \sa SDL_GetTrayEntryChecked + * \sa SDL_SetTrayEntryEnabled + * \sa SDL_GetTrayEntryEnabled + * \sa SDL_SetTrayEntryCallback + * \sa SDL_AppendTraySeparator + */ extern SDL_DECLSPEC SDL_TrayEntry *SDLCALL SDL_AppendTrayEntry(SDL_TrayMenu *menu, const char *label, SDL_TrayEntryFlags flags); + +/** + * Append a separator to the given menu. + * + * \param menu the menu to append the entry to. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_CreateTrayMenu + * \sa SDL_CreateTraySubmenu + * \sa SDL_AppendTrayEntry + */ extern SDL_DECLSPEC void SDLCALL SDL_AppendTraySeparator(SDL_TrayMenu *menu); + +/** + * Sets whether or not an entry is checked. + * + * The entry must have been created with the SDL_TRAYENTRY_CHECKBOX flag. + * + * \param entry the entry to be updated. + * \param checked SDL_TRUE if the entry should be checked; SDL_FALSE otherwise. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_TrayEntryFlags + * \sa SDL_AppendTrayEntry + * \sa SDL_GetTrayEntryChecked + * \sa SDL_SetTrayEntryEnabled + * \sa SDL_GetTrayEntryEnabled + * \sa SDL_SetTrayEntryCallback + */ extern SDL_DECLSPEC void SDLCALL SDL_SetTrayEntryChecked(SDL_TrayEntry *entry, SDL_bool checked); + +/** + * Gets whether or not an entry is checked. + * + * The entry must have been created with the SDL_TRAYENTRY_CHECKBOX flag. + * + * \param entry the entry to be read. + * \returns SDL_TRUE if the entry is checked; SDL_FALSE otherwise. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_TrayEntryFlags + * \sa SDL_AppendTrayEntry + * \sa SDL_SetTrayEntryChecked + * \sa SDL_SetTrayEntryEnabled + * \sa SDL_GetTrayEntryEnabled + * \sa SDL_SetTrayEntryCallback + */ extern SDL_DECLSPEC SDL_bool SDLCALL SDL_GetTrayEntryChecked(SDL_TrayEntry *entry); + +/** + * Sets whether or not an entry is enabled. + * + * \param entry the entry to be updated. + * \param enabled SDL_TRUE if the entry should be enabled; SDL_FALSE otherwise. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_AppendTrayEntry + * \sa SDL_SetTrayEntryChecked + * \sa SDL_GetTrayEntryChecked + * \sa SDL_GetTrayEntryEnabled + * \sa SDL_SetTrayEntryCallback + */ extern SDL_DECLSPEC void SDLCALL SDL_SetTrayEntryEnabled(SDL_TrayEntry *entry, SDL_bool enabled); + +/** + * Gets whether or not an entry is enabled. + * + * \param entry the entry to be read. + * \returns SDL_TRUE if the entry is enabled; SDL_FALSE otherwise. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_AppendTrayEntry + * \sa SDL_SetTrayEntryChecked + * \sa SDL_GetTrayEntryChecked + * \sa SDL_SetTrayEntryEnabled + * \sa SDL_SetTrayEntryCallback + */ extern SDL_DECLSPEC SDL_bool SDLCALL SDL_GetTrayEntryEnabled(SDL_TrayEntry *entry); + +/** + * Sets a callback to be invoked when the entry is selected. + * + * \param entry the entry to be updated. + * \param callback a callback to be invoked when the entry is selected. + * \param userdata an optional pointer to pass extra data to the callback when + * it will be invoked. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_TrayCallback + * \sa SDL_AppendTrayEntry + * \sa SDL_SetTrayEntryChecked + * \sa SDL_GetTrayEntryChecked + * \sa SDL_SetTrayEntryEnabled + * \sa SDL_GetTrayEntryEnabled + */ extern SDL_DECLSPEC void SDLCALL SDL_SetTrayEntryCallback(SDL_TrayEntry *entry, SDL_TrayCallback callback, void *userdata); + +/** + * Destroys a tray object. + * + * This also destroys all associated menus and entries. + * + * \param tray the tray icon to be destroyed. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_CreateTray + */ extern SDL_DECLSPEC void SDLCALL SDL_DestroyTray(SDL_Tray *tray); /* Ends C function definitions when using C++ */ diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym index f9f4ac238..78662e89c 100644 --- a/src/dynapi/SDL_dynapi.sym +++ b/src/dynapi/SDL_dynapi.sym @@ -1178,6 +1178,8 @@ SDL3_0.0.0 { SDL_GetTrayEntryEnabled; SDL_SetTrayEntryCallback; SDL_DestroyTray; + SDL_SetTrayIcon; + SDL_SetTrayTooltip; # 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 e6f97850a..386eeeb52 100644 --- a/src/dynapi/SDL_dynapi_overrides.h +++ b/src/dynapi/SDL_dynapi_overrides.h @@ -1203,3 +1203,5 @@ #define SDL_GetTrayEntryEnabled SDL_GetTrayEntryEnabled_REAL #define SDL_SetTrayEntryCallback SDL_SetTrayEntryCallback_REAL #define SDL_DestroyTray SDL_DestroyTray_REAL +#define SDL_SetTrayIcon SDL_SetTrayIcon_REAL +#define SDL_SetTrayTooltip SDL_SetTrayTooltip_REAL diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h index e802a136e..20148d6ae 100644 --- a/src/dynapi/SDL_dynapi_procs.h +++ b/src/dynapi/SDL_dynapi_procs.h @@ -1209,3 +1209,5 @@ SDL_DYNAPI_PROC(void,SDL_SetTrayEntryEnabled,(SDL_TrayEntry *a, SDL_bool b),(a,b SDL_DYNAPI_PROC(SDL_bool,SDL_GetTrayEntryEnabled,(SDL_TrayEntry *a),(a),return) SDL_DYNAPI_PROC(void,SDL_SetTrayEntryCallback,(SDL_TrayEntry *a, SDL_TrayCallback b, void *c),(a,b,c),) SDL_DYNAPI_PROC(void,SDL_DestroyTray,(SDL_Tray *a),(a),) +SDL_DYNAPI_PROC(void,SDL_SetTrayIcon,(SDL_Tray *a, SDL_Surface *b),(a,b),) +SDL_DYNAPI_PROC(void,SDL_SetTrayTooltip,(SDL_Tray *a, const char *b),(a,b),) diff --git a/src/tray/cocoa/SDL_tray.m b/src/tray/cocoa/SDL_tray.m index 5aefec7a4..1b13a1981 100644 --- a/src/tray/cocoa/SDL_tray.m +++ b/src/tray/cocoa/SDL_tray.m @@ -82,6 +82,12 @@ SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip) tray->statusItem = [tray->statusBar statusItemWithLength:NSVariableStatusItemLength]; [app activateIgnoringOtherApps:TRUE]; + if (tooltip) { + tray->statusItem.button.toolTip = [NSString stringWithUTF8String:tooltip]; + } else { + tray->statusItem.button.toolTip = nil; + } + if (icon) { SDL_Surface *iconfmt = SDL_ConvertSurface(icon, SDL_PIXELFORMAT_RGBA32); if (!iconfmt) { @@ -118,6 +124,55 @@ skip_putting_an_icon: return tray; } +void SDL_SetTrayIcon(SDL_Tray *tray, SDL_Surface *icon) +{ + if (!icon) { + tray->statusItem.button.image = nil; + return; + } + + SDL_Surface *iconfmt = SDL_ConvertSurface(icon, SDL_PIXELFORMAT_RGBA32); + if (!iconfmt) { + /* TODO: Ignore errors silently, as in SDL_CreateTray? */ + tray->statusItem.button.image = nil; + return; + } + + NSBitmapImageRep *bitmap = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:(unsigned char **)&iconfmt->pixels + pixelsWide:iconfmt->w + pixelsHigh:iconfmt->h + bitsPerSample:8 + samplesPerPixel:4 + hasAlpha:YES + isPlanar:NO + colorSpaceName:NSCalibratedRGBColorSpace + bytesPerRow:iconfmt->pitch + bitsPerPixel:32]; + NSImage *iconimg = [[NSImage alloc] initWithSize:NSMakeSize(iconfmt->w, iconfmt->h)]; + [iconimg addRepresentation:bitmap]; + + /* A typical icon size is 22x22 on macOS. Failing to resize the icon + may give oversized status bar buttons. */ + NSImage *iconimg22 = [[NSImage alloc] initWithSize:NSMakeSize(22, 22)]; + [iconimg22 lockFocus]; + [iconimg setSize:NSMakeSize(22, 22)]; + [iconimg drawInRect:NSMakeRect(0, 0, 22, 22)]; + [iconimg22 unlockFocus]; + + tray->statusItem.button.image = iconimg22; + + SDL_DestroySurface(iconfmt); +} + +void SDL_SetTrayTooltip(SDL_Tray *tray, const char *tooltip) +{ + if (tooltip) { + tray->statusItem.button.toolTip = [NSString stringWithUTF8String:tooltip]; + } else { + tray->statusItem.button.toolTip = nil; + } +} + SDL_TrayMenu *SDL_CreateTrayMenu(SDL_Tray *tray) { NSMenu *menu = [[NSMenu alloc] init]; diff --git a/src/tray/unix/SDL_tray.c b/src/tray/unix/SDL_tray.c index 8d70cd1d1..ce7d342c0 100644 --- a/src/tray/unix/SDL_tray.c +++ b/src/tray/unix/SDL_tray.c @@ -116,6 +116,12 @@ void (*app_indicator_set_menu)(AppIndicator *self, GtkMenu *menu); static SDL_bool gtk_is_init = SDL_FALSE; +static int main_gtk_thread(void *data) +{ + gtk_main(); + return 0; +} + static void *libappindicator = NULL; static void *libgtk = NULL; static void *libgdk = NULL; @@ -201,6 +207,8 @@ static SDL_bool init_gtk(void) gtk_is_init = SDL_TRUE; + SDL_DetachThread(SDL_CreateThread(main_gtk_thread, "tray gtk", NULL)); + return SDL_TRUE; } @@ -236,12 +244,6 @@ static void call_callback(GtkMenuItem *item, gpointer ptr) } } -static int main_gtk_thread(void *data) -{ - gtk_main(); - return 0; -} - /* TODO: Replace this with a safer alternative */ static SDL_bool get_tmp_filename(char *buffer, size_t size) { @@ -262,8 +264,6 @@ SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip) return NULL; } - SDL_DetachThread(SDL_CreateThread(main_gtk_thread, "tray gtk", NULL)); - SDL_Tray *tray = (SDL_Tray *) SDL_malloc(sizeof(SDL_Tray)); if (!tray) { @@ -278,11 +278,25 @@ SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip) tray->indicator = app_indicator_new(TRAY_APPINDICATOR_ID, tray->icon_path, APP_INDICATOR_CATEGORY_APPLICATION_STATUS); app_indicator_set_status(tray->indicator, APP_INDICATOR_STATUS_ACTIVE); - //app_indicator_set_icon(indicator, tray->icon); return tray; } +void SDL_SetTrayIcon(SDL_Tray *tray, SDL_Surface *icon) +{ + if (icon) { + SDL_SaveBMP(icon, tray->icon_path); + app_indicator_set_icon(tray->indicator, tray->icon_path); + } else { + app_indicator_set_icon(tray->indicator, NULL); + } +} + +void SDL_SetTrayTooltip(SDL_Tray *tray, const char *tooltip) +{ + /* AppIndicator provides no tooltip support. */ +} + SDL_TrayMenu *SDL_CreateTrayMenu(SDL_Tray *tray) { tray->menu.menu = (GtkMenuShell *)gtk_menu_new(); diff --git a/src/tray/windows/SDL_tray.c b/src/tray/windows/SDL_tray.c index 004b27ead..14d33a643 100644 --- a/src/tray/windows/SDL_tray.c +++ b/src/tray/windows/SDL_tray.c @@ -175,7 +175,7 @@ SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip) tray->entries = NULL; char classname[32]; - SDL_snprintf(classname, sizeof(classname), "SDLTray%lld", get_next_id()); + SDL_snprintf(classname, sizeof(classname), "SDLTray%d", (unsigned int) get_next_id()); HINSTANCE hInstance = GetModuleHandle(NULL); WNDCLASS wc; @@ -219,6 +219,40 @@ no_icon: return tray; } +void SDL_SetTrayIcon(SDL_Tray *tray, SDL_Surface *icon) +{ + if (tray->icon) { + DestroyIcon(tray->icon); + } + + if (icon) { + SDL_Surface *iconfmt = SDL_ConvertSurface(icon, SDL_PIXELFORMAT_RGBA32); + if (!iconfmt) { + /* TODO: Ignore errors silently, as in SDL_CreateTray? */ + return; + } + + tray->nid.hIcon = CreateIconFromRGBA(iconfmt->w, iconfmt->h, iconfmt->pixels); + tray->icon = tray->nid.hIcon; + } else { + tray->nid.hIcon = LoadIcon(NULL, IDI_APPLICATION); + tray->icon = tray->nid.hIcon; + } + + Shell_NotifyIconW(NIM_MODIFY, &tray->nid); +} + +void SDL_SetTrayTooltip(SDL_Tray *tray, const char *tooltip) +{ + if (tooltip) { + mbstowcs_s(NULL, tray->nid.szTip, sizeof(tray->nid.szTip) / sizeof(*tray->nid.szTip), tooltip, _TRUNCATE); + } else { + tray->nid.szTip[0] = '\0'; + } + + Shell_NotifyIconW(NIM_MODIFY, &tray->nid); +} + SDL_TrayMenu *SDL_CreateTrayMenu(SDL_Tray *tray) { tray->menu.hMenu = CreatePopupMenu(); @@ -286,7 +320,7 @@ void SDL_AppendTraySeparator(SDL_TrayMenu *menu) void SDL_SetTrayEntryChecked(SDL_TrayEntry *entry, SDL_bool checked) { - CheckMenuItem(entry->menu->hMenu, entry->id, checked ? MF_CHECKED : MF_UNCHECKED); + CheckMenuItem(entry->menu->hMenu, (UINT) entry->id, checked ? MF_CHECKED : MF_UNCHECKED); } SDL_bool SDL_GetTrayEntryChecked(SDL_TrayEntry *entry) @@ -295,14 +329,14 @@ SDL_bool SDL_GetTrayEntryChecked(SDL_TrayEntry *entry) mii.cbSize = sizeof(MENUITEMINFO); mii.fMask = MIIM_STATE; - GetMenuItemInfo(entry->menu->hMenu, entry->id, FALSE, &mii); + GetMenuItemInfo(entry->menu->hMenu, (UINT) entry->id, FALSE, &mii); return !!(mii.fState & MFS_CHECKED); } void SDL_SetTrayEntryEnabled(SDL_TrayEntry *entry, SDL_bool enabled) { - EnableMenuItem(entry->menu->hMenu, entry->id, MF_BYCOMMAND | (enabled ? MF_ENABLED : (MF_DISABLED | MF_GRAYED))); + EnableMenuItem(entry->menu->hMenu, (UINT) entry->id, MF_BYCOMMAND | (enabled ? MF_ENABLED : (MF_DISABLED | MF_GRAYED))); } SDL_bool SDL_GetTrayEntryEnabled(SDL_TrayEntry *entry) @@ -311,7 +345,7 @@ SDL_bool SDL_GetTrayEntryEnabled(SDL_TrayEntry *entry) mii.cbSize = sizeof(MENUITEMINFO); mii.fMask = MIIM_STATE; - GetMenuItemInfo(entry->menu->hMenu, entry->id, FALSE, &mii); + GetMenuItemInfo(entry->menu->hMenu, (UINT) entry->id, FALSE, &mii); return !!(mii.fState & MFS_ENABLED); } diff --git a/test/testtray.c b/test/testtray.c index c4305a062..be8717021 100644 --- a/test/testtray.c +++ b/test/testtray.c @@ -45,7 +45,6 @@ int main(int argc, char **argv) SDL_Event e; while (SDL_WaitEvent(&e)) { if (e.type == SDL_EVENT_QUIT) { - SDL_Log("Someone said quit\n"); break; } }