From 01b9b0edb7fe67d333cc26744a525c9ee0fd578e Mon Sep 17 00:00:00 2001 From: Semphriss <66701383+Semphriss@users.noreply.github.com> Date: Tue, 24 Dec 2024 13:36:39 -0500 Subject: [PATCH] Add system tray support (#10873) --- Android.mk | 1 + CMakeLists.txt | 17 + VisualC-GDK/SDL/SDL.vcxproj | 14 +- VisualC-GDK/SDL/SDL.vcxproj.filters | 4 + VisualC/SDL/SDL.vcxproj | 3 + VisualC/SDL/SDL.vcxproj.filters | 9 + include/SDL3/SDL.h | 1 + include/SDL3/SDL_tray.h | 431 +++++++++++++++++ src/dynapi/SDL_dynapi.sym | 21 + src/dynapi/SDL_dynapi_overrides.h | 21 + src/dynapi/SDL_dynapi_procs.h | 21 + src/tray/cocoa/SDL_tray.m | 458 ++++++++++++++++++ src/tray/dummy/SDL_tray.c | 139 ++++++ src/tray/unix/SDL_tray.c | 664 ++++++++++++++++++++++++++ src/tray/windows/SDL_tray.c | 589 +++++++++++++++++++++++ src/video/windows/SDL_surface_utils.c | 95 ++++ src/video/windows/SDL_surface_utils.h | 38 ++ test/CMakeLists.txt | 1 + test/sdl-test_round.bmp | Bin 0 -> 147594 bytes test/testtray.c | 599 +++++++++++++++++++++++ 20 files changed, 3125 insertions(+), 1 deletion(-) create mode 100644 include/SDL3/SDL_tray.h create mode 100644 src/tray/cocoa/SDL_tray.m create mode 100644 src/tray/dummy/SDL_tray.c create mode 100644 src/tray/unix/SDL_tray.c create mode 100644 src/tray/windows/SDL_tray.c create mode 100644 src/video/windows/SDL_surface_utils.c create mode 100644 src/video/windows/SDL_surface_utils.h create mode 100644 test/sdl-test_round.bmp create mode 100644 test/testtray.c diff --git a/Android.mk b/Android.mk index 56c817a2b6..3e584f9a5b 100644 --- a/Android.mk +++ b/Android.mk @@ -78,6 +78,7 @@ LOCAL_SRC_FILES := \ $(wildcard $(LOCAL_PATH)/src/time/unix/*.c) \ $(wildcard $(LOCAL_PATH)/src/timer/*.c) \ $(wildcard $(LOCAL_PATH)/src/timer/unix/*.c) \ + $(wildcard $(LOCAL_PATH)/src/tray/dummy/*.c) \ $(wildcard $(LOCAL_PATH)/src/video/*.c) \ $(wildcard $(LOCAL_PATH)/src/video/android/*.c) \ $(wildcard $(LOCAL_PATH)/src/video/yuv2rgb/*.c)) diff --git a/CMakeLists.txt b/CMakeLists.txt index 695b6ed665..5a6cad0280 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1567,6 +1567,9 @@ elseif(UNIX AND NOT APPLE AND NOT RISCOS AND NOT HAIKU) CheckVivante() CheckVulkan() CheckQNXScreen() + + sdl_glob_sources("${SDL3_SOURCE_DIR}/src/tray/unix/*.c") + set(HAVE_SDL_TRAY TRUE) endif() if(UNIX) @@ -2075,6 +2078,9 @@ elseif(WINDOWS) set(HAVE_RENDER_VULKAN TRUE) endif() endif() + + sdl_glob_sources("${SDL3_SOURCE_DIR}/src/tray/windows/*.c") + set(HAVE_SDL_TRAY TRUE) endif() if(SDL_HIDAPI) @@ -2351,6 +2357,11 @@ elseif(APPLE) endif() endif() endif() + + if(MACOS) + sdl_glob_sources("${SDL3_SOURCE_DIR}/src/tray/cocoa/*.m") + set(HAVE_SDL_TRAY TRUE) + endif() endif() # Minimum version for $ @@ -2973,6 +2984,8 @@ if(SDL_VIDEO) endif() endif() +sdl_glob_sources(${SDL3_SOURCE_DIR}/src/tray/*.c) + if(SDL_GPU) if(HAVE_D3D11_H) sdl_glob_sources("${SDL3_SOURCE_DIR}/src/gpu/d3d11/*.c") @@ -3055,6 +3068,10 @@ if(NOT HAVE_SDL_PROCESS) set(SDL_PROCESS_DUMMY 1) sdl_glob_sources(${SDL3_SOURCE_DIR}/src/process/dummy/*.c) endif() +if(NOT HAVE_SDL_TRAY) + set(SDL_TRAY_DUMMY 1) + sdl_glob_sources(${SDL3_SOURCE_DIR}/src/tray/dummy/*.c) +endif() if(NOT HAVE_CAMERA) set(SDL_CAMERA_DRIVER_DUMMY 1) sdl_glob_sources("${SDL3_SOURCE_DIR}/src/camera/dummy/*.c") diff --git a/VisualC-GDK/SDL/SDL.vcxproj b/VisualC-GDK/SDL/SDL.vcxproj index 008af3dff8..2b6437fb7e 100644 --- a/VisualC-GDK/SDL/SDL.vcxproj +++ b/VisualC-GDK/SDL/SDL.vcxproj @@ -592,6 +592,7 @@ + @@ -827,6 +828,16 @@ + + true + true + + + true + true + true + true + @@ -855,6 +866,7 @@ + @@ -889,4 +901,4 @@ - \ No newline at end of file + diff --git a/VisualC-GDK/SDL/SDL.vcxproj.filters b/VisualC-GDK/SDL/SDL.vcxproj.filters index 230c95d2c2..5ae2609c80 100644 --- a/VisualC-GDK/SDL/SDL.vcxproj.filters +++ b/VisualC-GDK/SDL/SDL.vcxproj.filters @@ -181,6 +181,7 @@ + @@ -217,6 +218,8 @@ + + @@ -435,6 +438,7 @@ + diff --git a/VisualC/SDL/SDL.vcxproj b/VisualC/SDL/SDL.vcxproj index 306a883d04..42d0c13daa 100644 --- a/VisualC/SDL/SDL.vcxproj +++ b/VisualC/SDL/SDL.vcxproj @@ -489,6 +489,7 @@ + @@ -671,6 +672,7 @@ + @@ -701,6 +703,7 @@ + diff --git a/VisualC/SDL/SDL.vcxproj.filters b/VisualC/SDL/SDL.vcxproj.filters index aa86194fab..d1d24865f5 100644 --- a/VisualC/SDL/SDL.vcxproj.filters +++ b/VisualC/SDL/SDL.vcxproj.filters @@ -690,6 +690,9 @@ video\yuv2rgb + + video\windows + video\windows @@ -1229,6 +1232,9 @@ time\windows + + video + video @@ -1301,6 +1307,9 @@ video\dummy + + video\windows + video\windows diff --git a/include/SDL3/SDL.h b/include/SDL3/SDL.h index e36c67b6db..0b4388eb5c 100644 --- a/include/SDL3/SDL.h +++ b/include/SDL3/SDL.h @@ -81,6 +81,7 @@ #include #include #include +#include #include #include #include diff --git a/include/SDL3/SDL_tray.h b/include/SDL3/SDL_tray.h new file mode 100644 index 0000000000..b7c5020706 --- /dev/null +++ b/include/SDL3/SDL_tray.h @@ -0,0 +1,431 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2024 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +/** + * # CategoryTray + * + * System tray menu support. + */ + +#ifndef SDL_tray_h_ +#define SDL_tray_h_ + +#include + +#include + +#include +/* Set up for C function definitions, even when using C++ */ +#ifdef __cplusplus +extern "C" { +#endif + +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 required; 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 required flag. + * + * \since This datatype is available since SDL 3.0.0. + * + * \sa SDL_InsertTrayEntryAt + */ +typedef Uint32 SDL_TrayEntryFlags; + +#define SDL_TRAYENTRY_BUTTON 0x00000001u /**< Make the entry a simple button. Required. */ +#define SDL_TRAYENTRY_CHECKBOX 0x00000002u /**< Make the entry a checkbox. Required. */ +#define SDL_TRAYENTRY_SUBMENU 0x00000004u /**< Prepare the entry to have a submenu. Required */ +#define SDL_TRAYENTRY_DISABLED 0x80000000u /**< Make the entry disabled. Optional. */ +#define SDL_TRAYENTRY_CHECKED 0x40000000u /**< Make the entry checked. This is valid only for checkboxes. Optional. */ + +/** + * 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. + * + * \sa SDL_SetTrayEntryCallback + */ +typedef void (SDLCALL *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. Avoid needlessly creating a tray icon, as the user may + * feel like it clutters their interface. + * + * Using tray icons require the video subsystem. + * + * \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_CreateTrayMenu + * \sa SDL_GetTrayMenu + * \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 + */ +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 + */ +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_GetTrayMenu + * \sa SDL_GetTrayMenuParentTray + */ +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_InsertTrayEntryAt + * \sa SDL_GetTraySubmenu + * \sa SDL_GetTrayMenuParentEntry + */ +extern SDL_DECLSPEC SDL_TrayMenu *SDLCALL SDL_CreateTraySubmenu(SDL_TrayEntry *entry); + +/** + * Gets a previously created tray menu. + * + * You should have called SDL_CreateTrayMenu() on the tray object. This + * function allows you to fetch it again later. + * + * This function does the same thing as SDL_GetTraySubmenu(), 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 entry 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_CreateTrayMenu + */ +extern SDL_DECLSPEC SDL_TrayMenu *SDLCALL SDL_GetTrayMenu(SDL_Tray *tray); + +/** + * Gets a previously created tray entry submenu. + * + * You should have called SDL_CreateTraySubenu() on the entry object. This + * function allows you to fetch it again later. + * + * This function does the same thing as SDL_GetTrayMenu(), 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_InsertTrayEntryAt + * \sa SDL_CreateTraySubmenu + */ +extern SDL_DECLSPEC SDL_TrayMenu *SDLCALL SDL_GetTraySubmenu(SDL_TrayEntry *entry); + +/** + * Returns a list of entries in the menu, in order. + * + * \param menu The menu to get entries from. + * \param size An optional pointer to obtain the number of entries in the menu. + * \returns the entries within the given menu. The pointer becomes invalid when + * any function that inserts or deletes entries in the menu is called. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_RemoveTrayEntry + * \sa SDL_InsertTrayEntryAt + */ +extern SDL_DECLSPEC const SDL_TrayEntry **SDLCALL SDL_GetTrayEntries(SDL_TrayMenu *menu, int *size); + +/** + * Removes a tray entry. + * + * \param entry The entry to be deleted. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_GetTrayEntries + * \sa SDL_InsertTrayEntryAt + */ +extern SDL_DECLSPEC void SDLCALL SDL_RemoveTrayEntry(SDL_TrayEntry *entry); + +/** + * Insert a tray entry at a given position. + * + * If label is NULL, the entry will be a separator. Many functions won't work + * for an entry that is a separator. + * + * 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 pos the desired position for the new entry. Entries at or following + * this place will be moved. If pos is -1, the entry is appended. + * \param label the text to be displayed on the entry, or NULL for a separator. + * \param flags a combination of flags, some of which are mandatory. + * \returns the newly created entry, or NULL if pos is out of bounds. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_TrayEntryFlags + * \sa SDL_GetTrayEntries + * \sa SDL_RemoveTrayEntry + * \sa SDL_GetTrayEntryParent + */ +extern SDL_DECLSPEC SDL_TrayEntry *SDLCALL SDL_InsertTrayEntryAt(SDL_TrayMenu *menu, int pos, const char *label, SDL_TrayEntryFlags flags); + +/** + * Sets the label of an entry. + * + * An entry cannot change between a separator and an ordinary entry; that is, + * it is not possible to set a non-NULL label on an entry that has a NULL label + * (separators), or to set a NULL label to an entry that has a non-NULL label. + * The function will silently fail if that happens. + * + * \param entry the entry to be updated. + * \param label the new label for the entry. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_GetTrayEntries + * \sa SDL_InsertTrayEntryAt + * \sa SDL_GetTrayEntryLabel + */ +extern SDL_DECLSPEC void SDLCALL SDL_SetTrayEntryLabel(SDL_TrayEntry *entry, const char *label); + +/** + * Gets the label of an entry. + * + * If the returned value is NULL, the entry is a separator. + * + * \param entry the entry to be read. + * \returns the label of the entry. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_GetTrayEntries + * \sa SDL_InsertTrayEntryAt + * \sa SDL_SetTrayEntryLabel + */ +extern SDL_DECLSPEC const char *SDLCALL SDL_GetTrayEntryLabel(SDL_TrayEntry *entry); + +/** + * 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_GetTrayEntries + * \sa SDL_InsertTrayEntryAt + * \sa SDL_GetTrayEntryChecked + */ +extern SDL_DECLSPEC void SDLCALL SDL_SetTrayEntryChecked(SDL_TrayEntry *entry, 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_GetTrayEntries + * \sa SDL_InsertTrayEntryAt + * \sa SDL_SetTrayEntryChecked + */ +extern SDL_DECLSPEC 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_GetTrayEntries + * \sa SDL_InsertTrayEntryAt + * \sa SDL_GetTrayEntryEnabled + */ +extern SDL_DECLSPEC void SDLCALL SDL_SetTrayEntryEnabled(SDL_TrayEntry *entry, 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_GetTrayEntries + * \sa SDL_InsertTrayEntryAt + * \sa SDL_SetTrayEntryEnabled + */ +extern SDL_DECLSPEC 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_GetTrayEntries + * \sa SDL_InsertTrayEntryAt + */ +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); + +/** + * Gets the menu contianing a certain tray entry. + * + * \param entry the entry for which to get the parent menu. + * \returns the parent menu. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_InsertTrayEntryAt + */ +extern SDL_DECLSPEC SDL_TrayMenu *SDLCALL SDL_GetTrayEntryParent(SDL_TrayEntry *entry); + +/** + * Gets the entry for which the menu is a submenu, if the current menu is a + * submenu. + * + * Either this function or SDL_GetTrayMenuParentTray() will return non-NULL for + * any given menu. + * + * \param menu the menu for which to get the parent entry. + * \returns the parent entry, or NULL if this menu is not a submenu. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_CreateTraySubmenu + * \sa SDL_GetTrayMenuParentTray + */ +extern SDL_DECLSPEC SDL_TrayEntry *SDLCALL SDL_GetTrayMenuParentEntry(SDL_TrayMenu *menu); + +/** + * Gets the tray for which this menu is the first-level menu, if the current + * menu isn't a submenu. + * + * Either this function or SDL_GetTrayMenuParentEntry() will return non-NULL for + * any given menu. + * + * \param menu the menu for which to get the parent enttrayry. + * \returns the parent tray, or NULL if this menu is a submenu. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_CreateTrayMenu + * \sa SDL_GetTrayMenuParentEntry + */ +extern SDL_DECLSPEC SDL_Tray *SDLCALL SDL_GetTrayMenuParentTray(SDL_TrayMenu *menu); + +/* Ends C function definitions when using C++ */ +#ifdef __cplusplus +} +#endif +#include + +#endif /* SDL_tray_h_ */ diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym index f3fe1c33c2..37a018e75f 100644 --- a/src/dynapi/SDL_dynapi.sym +++ b/src/dynapi/SDL_dynapi.sym @@ -1208,6 +1208,27 @@ SDL3_0.0.0 { SDL_RenderTextureAffine; SDL_WaitAndAcquireGPUSwapchainTexture; SDL_RenderDebugTextFormat; + SDL_CreateTray; + SDL_SetTrayIcon; + SDL_SetTrayTooltip; + SDL_CreateTrayMenu; + SDL_CreateTraySubmenu; + SDL_GetTrayMenu; + SDL_GetTraySubmenu; + SDL_GetTrayEntries; + SDL_RemoveTrayEntry; + SDL_InsertTrayEntryAt; + SDL_SetTrayEntryLabel; + SDL_GetTrayEntryLabel; + SDL_SetTrayEntryChecked; + SDL_GetTrayEntryChecked; + SDL_SetTrayEntryEnabled; + SDL_GetTrayEntryEnabled; + SDL_SetTrayEntryCallback; + SDL_DestroyTray; + SDL_GetTrayEntryParent; + SDL_GetTrayMenuParentEntry; + SDL_GetTrayMenuParentTray; # 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 12e77270ca..9fa340a26b 100644 --- a/src/dynapi/SDL_dynapi_overrides.h +++ b/src/dynapi/SDL_dynapi_overrides.h @@ -1233,3 +1233,24 @@ #define SDL_RenderTextureAffine SDL_RenderTextureAffine_REAL #define SDL_WaitAndAcquireGPUSwapchainTexture SDL_WaitAndAcquireGPUSwapchainTexture_REAL #define SDL_RenderDebugTextFormat SDL_RenderDebugTextFormat_REAL +#define SDL_CreateTray SDL_CreateTray_REAL +#define SDL_SetTrayIcon SDL_SetTrayIcon_REAL +#define SDL_SetTrayTooltip SDL_SetTrayTooltip_REAL +#define SDL_CreateTrayMenu SDL_CreateTrayMenu_REAL +#define SDL_CreateTraySubmenu SDL_CreateTraySubmenu_REAL +#define SDL_GetTrayMenu SDL_GetTrayMenu_REAL +#define SDL_GetTraySubmenu SDL_GetTraySubmenu_REAL +#define SDL_GetTrayEntries SDL_GetTrayEntries_REAL +#define SDL_RemoveTrayEntry SDL_RemoveTrayEntry_REAL +#define SDL_InsertTrayEntryAt SDL_InsertTrayEntryAt_REAL +#define SDL_SetTrayEntryLabel SDL_SetTrayEntryLabel_REAL +#define SDL_GetTrayEntryLabel SDL_GetTrayEntryLabel_REAL +#define SDL_SetTrayEntryChecked SDL_SetTrayEntryChecked_REAL +#define SDL_GetTrayEntryChecked SDL_GetTrayEntryChecked_REAL +#define SDL_SetTrayEntryEnabled SDL_SetTrayEntryEnabled_REAL +#define SDL_GetTrayEntryEnabled SDL_GetTrayEntryEnabled_REAL +#define SDL_SetTrayEntryCallback SDL_SetTrayEntryCallback_REAL +#define SDL_DestroyTray SDL_DestroyTray_REAL +#define SDL_GetTrayEntryParent SDL_GetTrayEntryParent_REAL +#define SDL_GetTrayMenuParentEntry SDL_GetTrayMenuParentEntry_REAL +#define SDL_GetTrayMenuParentTray SDL_GetTrayMenuParentTray_REAL diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h index 189158744e..eec6229fff 100644 --- a/src/dynapi/SDL_dynapi_procs.h +++ b/src/dynapi/SDL_dynapi_procs.h @@ -1241,3 +1241,24 @@ SDL_DYNAPI_PROC(bool,SDL_WaitAndAcquireGPUSwapchainTexture,(SDL_GPUCommandBuffer #ifndef SDL_DYNAPI_PROC_NO_VARARGS SDL_DYNAPI_PROC(bool,SDL_RenderDebugTextFormat,(SDL_Renderer *a,float b,float c,SDL_PRINTF_FORMAT_STRING const char *d,...),(a,b,c,d),return) #endif +SDL_DYNAPI_PROC(SDL_Tray*,SDL_CreateTray,(SDL_Surface *a,const char *b),(a,b),return) +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),) +SDL_DYNAPI_PROC(SDL_TrayMenu*,SDL_CreateTrayMenu,(SDL_Tray *a),(a),return) +SDL_DYNAPI_PROC(SDL_TrayMenu*,SDL_CreateTraySubmenu,(SDL_TrayEntry *a),(a),return) +SDL_DYNAPI_PROC(SDL_TrayMenu*,SDL_GetTrayMenu,(SDL_Tray *a),(a),return) +SDL_DYNAPI_PROC(SDL_TrayMenu*,SDL_GetTraySubmenu,(SDL_TrayEntry *a),(a),return) +SDL_DYNAPI_PROC(const SDL_TrayEntry**,SDL_GetTrayEntries,(SDL_TrayMenu *a,int *b),(a,b),return) +SDL_DYNAPI_PROC(void,SDL_RemoveTrayEntry,(SDL_TrayEntry *a),(a),) +SDL_DYNAPI_PROC(SDL_TrayEntry*,SDL_InsertTrayEntryAt,(SDL_TrayMenu *a,int b,const char *c,SDL_TrayEntryFlags d),(a,b,c,d),return) +SDL_DYNAPI_PROC(void,SDL_SetTrayEntryLabel,(SDL_TrayEntry *a,const char *b),(a,b),) +SDL_DYNAPI_PROC(const char*,SDL_GetTrayEntryLabel,(SDL_TrayEntry *a),(a),return) +SDL_DYNAPI_PROC(void,SDL_SetTrayEntryChecked,(SDL_TrayEntry *a,bool b),(a,b),) +SDL_DYNAPI_PROC(bool,SDL_GetTrayEntryChecked,(SDL_TrayEntry *a),(a),return) +SDL_DYNAPI_PROC(void,SDL_SetTrayEntryEnabled,(SDL_TrayEntry *a,bool b),(a,b),) +SDL_DYNAPI_PROC(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(SDL_TrayMenu*,SDL_GetTrayEntryParent,(SDL_TrayEntry *a),(a),return) +SDL_DYNAPI_PROC(SDL_TrayEntry*,SDL_GetTrayMenuParentEntry,(SDL_TrayMenu *a),(a),return) +SDL_DYNAPI_PROC(SDL_Tray*,SDL_GetTrayMenuParentTray,(SDL_TrayMenu *a),(a),return) diff --git a/src/tray/cocoa/SDL_tray.m b/src/tray/cocoa/SDL_tray.m new file mode 100644 index 0000000000..515ee6527c --- /dev/null +++ b/src/tray/cocoa/SDL_tray.m @@ -0,0 +1,458 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2024 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#include "SDL_internal.h" + +#include + +#include "../../video/SDL_surface_c.h" + +/* applicationDockMenu */ + +struct SDL_TrayMenu { + NSMenu *nsmenu; + + size_t nEntries; + SDL_TrayEntry **entries; + + SDL_Tray *parent_tray; + SDL_TrayEntry *parent_entry; +}; + +struct SDL_TrayEntry { + NSMenuItem *nsitem; + + SDL_TrayEntryFlags flags; + SDL_TrayCallback callback; + void *userdata; + SDL_TrayMenu *submenu; + + SDL_TrayMenu *parent; +}; + +struct SDL_Tray { + NSStatusBar *statusBar; + NSStatusItem *statusItem; + + SDL_TrayMenu *menu; +}; + +static NSApplication *app = NULL; + +@interface AppDelegate: NSObject + - (IBAction)menu:(id)sender; +@end + +@implementation AppDelegate{} + - (IBAction)menu:(id)sender + { + SDL_TrayEntry *entry = [[sender representedObject] pointerValue]; + + if (!entry) { + return; + } + + if (entry->flags & SDL_TRAYENTRY_CHECKBOX) { + SDL_SetTrayEntryChecked(entry, !SDL_GetTrayEntryChecked(entry)); + } + + if (entry->callback) { + entry->callback(entry->userdata, entry); + } + } +@end + +static void DestroySDLMenu(SDL_TrayMenu *menu) +{ + for (int i = 0; i < menu->nEntries; i++) { + if (menu->entries[i] && menu->entries[i]->submenu) { + DestroySDLMenu(menu->entries[i]->submenu); + } + SDL_free(menu->entries[i]); + } + + SDL_free(menu->entries); + + if (menu->parent_entry) { + [menu->parent_entry->parent->nsmenu setSubmenu:nil forItem:menu->parent_entry->nsitem]; + } else if (menu->parent_tray) { + [menu->parent_tray->statusItem setMenu:nil]; + } + + SDL_free(menu); +} + +SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip) +{ + SDL_Tray *tray = (SDL_Tray *) SDL_malloc(sizeof(SDL_Tray)); + + AppDelegate *delegate = [[AppDelegate alloc] init]; + app = [NSApplication sharedApplication]; + [app setDelegate:delegate]; + + if (!tray) { + return NULL; + } + + SDL_memset((void *) tray, 0, sizeof(*tray)); + + tray->statusItem = nil; + tray->statusBar = [NSStatusBar systemStatusBar]; + 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) { + goto skip_putting_an_icon; + } + + 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); + } + +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) { + 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) +{ + SDL_TrayMenu *menu = SDL_malloc(sizeof(SDL_TrayMenu)); + + if (!menu) { + return NULL; + } + + SDL_memset((void *) menu, 0, sizeof(*menu)); + + NSMenu *nsmenu = [[NSMenu alloc] init]; + [nsmenu setAutoenablesItems:FALSE]; + + [tray->statusItem setMenu:nsmenu]; + + tray->menu = menu; + menu->nsmenu = nsmenu; + menu->nEntries = 0; + menu->entries = NULL; + menu->parent_tray = tray; + menu->parent_entry = NULL; + + return menu; +} + +SDL_TrayMenu *SDL_GetTrayMenu(SDL_Tray *tray) +{ + return tray->menu; +} + +SDL_TrayMenu *SDL_CreateTraySubmenu(SDL_TrayEntry *entry) +{ + if (entry->submenu) { + SDL_SetError("Tray entry submenu already exists"); + return NULL; + } + + if (!(entry->flags & SDL_TRAYENTRY_SUBMENU)) { + SDL_SetError("Cannot create submenu for entry not created with SDL_TRAYENTRY_SUBMENU"); + return NULL; + } + + SDL_TrayMenu *menu = SDL_malloc(sizeof(SDL_TrayMenu)); + + if (!menu) { + return NULL; + } + + SDL_memset((void *) menu, 0, sizeof(*menu)); + + NSMenu *nsmenu = [[NSMenu alloc] init]; + [nsmenu setAutoenablesItems:FALSE]; + + entry->submenu = menu; + menu->nsmenu = nsmenu; + menu->nEntries = 0; + menu->entries = NULL; + menu->parent_tray = NULL; + menu->parent_entry = entry; + + [entry->parent->nsmenu setSubmenu:nsmenu forItem:entry->nsitem]; + + return menu; +} + +SDL_TrayMenu *SDL_GetTraySubmenu(SDL_TrayEntry *entry) +{ + return entry->submenu; +} + +const SDL_TrayEntry **SDL_GetTrayEntries(SDL_TrayMenu *menu, int *size) +{ + if (size) { + *size = menu->nEntries; + } + + return (const SDL_TrayEntry **) menu->entries; +} + +void SDL_RemoveTrayEntry(SDL_TrayEntry *entry) +{ + if (!entry) { + return; + } + + SDL_TrayMenu *menu = entry->parent; + + bool found = false; + for (int i = 0; i < menu->nEntries - 1; i++) { + if (menu->entries[i] == entry) { + found = true; + } + + if (found) { + menu->entries[i] = menu->entries[i + 1]; + } + } + + if (entry->submenu) { + DestroySDLMenu(entry->submenu); + } + + menu->nEntries--; + SDL_TrayEntry ** new_entries = SDL_realloc(menu->entries, menu->nEntries * sizeof(SDL_TrayEntry *)); + + /* Not sure why shrinking would fail, but even if it does, we can live with a "too big" array */ + if (new_entries) { + menu->entries = new_entries; + } + + [menu->nsmenu removeItem:entry->nsitem]; + + SDL_free(entry); +} + +SDL_TrayEntry *SDL_InsertTrayEntryAt(SDL_TrayMenu *menu, int pos, const char *label, SDL_TrayEntryFlags flags) +{ + if (pos < -1 || pos > (int) menu->nEntries) { + SDL_InvalidParamError("pos"); + return NULL; + } + + if (pos == -1) { + pos = menu->nEntries; + } + + SDL_TrayEntry *entry = SDL_malloc(sizeof(SDL_TrayEntry)); + + if (!entry) { + return NULL; + } + + SDL_memset((void *) entry, 0, sizeof(*entry)); + + SDL_TrayEntry **new_entries = (SDL_TrayEntry **) SDL_realloc(menu->entries, (menu->nEntries + 1) * sizeof(SDL_TrayEntry *)); + + if (!new_entries) { + SDL_free(entry); + return NULL; + } + + menu->entries = new_entries; + menu->nEntries++; + + for (int i = menu->nEntries - 1; i > pos; i--) { + menu->entries[i] = menu->entries[i - 1]; + } + + new_entries[pos] = entry; + + NSMenuItem *nsitem; + if (label == NULL) { + nsitem = [NSMenuItem separatorItem]; + } else { + nsitem = [[NSMenuItem alloc] initWithTitle:[NSString stringWithUTF8String:label] action:@selector(menu:) keyEquivalent:@""]; + [nsitem setEnabled:((flags & SDL_TRAYENTRY_DISABLED) ? FALSE : TRUE)]; + [nsitem setState:((flags & SDL_TRAYENTRY_CHECKED) ? NSControlStateValueOn : NSControlStateValueOff)]; + [nsitem setRepresentedObject:[NSValue valueWithPointer:entry]]; + } + + [menu->nsmenu insertItem:nsitem atIndex:pos]; + + entry->nsitem = nsitem; + entry->flags = flags; + entry->callback = NULL; + entry->userdata = NULL; + entry->submenu = NULL; + entry->parent = menu; + + return entry; +} + +void SDL_SetTrayEntryLabel(SDL_TrayEntry *entry, const char *label) +{ + [entry->nsitem setTitle:[NSString stringWithUTF8String:label]]; +} + +const char *SDL_GetTrayEntryLabel(SDL_TrayEntry *entry) +{ + return [[entry->nsitem title] UTF8String]; +} + +void SDL_SetTrayEntryChecked(SDL_TrayEntry *entry, bool checked) +{ + [entry->nsitem setState:(checked ? NSControlStateValueOn : NSControlStateValueOff)]; +} + +bool SDL_GetTrayEntryChecked(SDL_TrayEntry *entry) +{ + return entry->nsitem.state == NSControlStateValueOn; +} + +void SDL_SetTrayEntryEnabled(SDL_TrayEntry *entry, bool enabled) +{ + if (!(entry->flags & SDL_TRAYENTRY_CHECKBOX)) { + SDL_SetError("Cannot update check for entry not created with SDL_TRAYENTRY_CHECKBOX"); + return; + } + + [entry->nsitem setEnabled:(enabled ? YES : NO)]; +} + +bool SDL_GetTrayEntryEnabled(SDL_TrayEntry *entry) +{ + if (!(entry->flags & SDL_TRAYENTRY_CHECKBOX)) { + SDL_SetError("Cannot fetch check for entry not created with SDL_TRAYENTRY_CHECKBOX"); + return false; + } + + return entry->nsitem.enabled; +} + +void SDL_SetTrayEntryCallback(SDL_TrayEntry *entry, SDL_TrayCallback callback, void *userdata) +{ + entry->callback = callback; + entry->userdata = userdata; +} + +SDL_TrayMenu *SDL_GetTrayEntryParent(SDL_TrayEntry *entry) +{ + return entry->parent; +} + +SDL_TrayEntry *SDL_GetTrayMenuParentEntry(SDL_TrayMenu *menu) +{ + return menu->parent_entry; +} + +SDL_Tray *SDL_GetTrayMenuParentTray(SDL_TrayMenu *menu) +{ + return menu->parent_tray; +} + +void SDL_DestroyTray(SDL_Tray *tray) +{ + if (!tray) { + return; + } + + [[NSStatusBar systemStatusBar] removeStatusItem:tray->statusItem]; + + if (tray->menu) { + DestroySDLMenu(tray->menu); + } + + SDL_free(tray); +} diff --git a/src/tray/dummy/SDL_tray.c b/src/tray/dummy/SDL_tray.c new file mode 100644 index 0000000000..3a105ad46a --- /dev/null +++ b/src/tray/dummy/SDL_tray.c @@ -0,0 +1,139 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2024 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#include "SDL_internal.h" + +SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip) +{ + SDL_Unsupported(); + return NULL; +} + +void SDL_SetTrayIcon(SDL_Tray *tray, SDL_Surface *icon) +{ + SDL_Unsupported(); +} + +void SDL_SetTrayTooltip(SDL_Tray *tray, const char *tooltip) +{ + SDL_Unsupported(); +} + +SDL_TrayMenu *SDL_CreateTrayMenu(SDL_Tray *tray) +{ + SDL_Unsupported(); + return NULL; +} + +SDL_TrayMenu *SDL_GetTrayMenu(SDL_Tray *tray) +{ + SDL_Unsupported(); + return NULL; +} + +SDL_TrayMenu *SDL_CreateTraySubmenu(SDL_TrayEntry *entry) +{ + SDL_Unsupported(); + return NULL; +} + +SDL_TrayMenu *SDL_GetTraySubmenu(SDL_TrayEntry *entry) +{ + return NULL; +} + +const SDL_TrayEntry **SDL_GetTrayEntries(SDL_TrayMenu *menu, int *size) +{ + SDL_Unsupported(); + return NULL; +} + +void SDL_RemoveTrayEntry(SDL_TrayEntry *entry) +{ + SDL_Unsupported(); +} + +SDL_TrayEntry *SDL_InsertTrayEntryAt(SDL_TrayMenu *menu, int pos, const char *label, SDL_TrayEntryFlags flags) +{ + SDL_Unsupported(); + return NULL; +} + +void SDL_SetTrayEntryLabel(SDL_TrayEntry *entry, const char *label) +{ + SDL_Unsupported(); +} + +const char *SDL_GetTrayEntryLabel(SDL_TrayEntry *entry) +{ + SDL_Unsupported(); + return NULL; +} + +void SDL_SetTrayEntryChecked(SDL_TrayEntry *entry, bool checked) +{ + SDL_Unsupported(); +} + +bool SDL_GetTrayEntryChecked(SDL_TrayEntry *entry) +{ + SDL_Unsupported(); + return false; +} + +void SDL_SetTrayEntryEnabled(SDL_TrayEntry *entry, bool enabled) +{ + SDL_Unsupported(); +} + +bool SDL_GetTrayEntryEnabled(SDL_TrayEntry *entry) +{ + SDL_Unsupported(); + return false; +} + +void SDL_SetTrayEntryCallback(SDL_TrayEntry *entry, SDL_TrayCallback callback, void *userdata) +{ + SDL_Unsupported(); +} + +SDL_TrayMenu *SDL_GetTrayEntryParent(SDL_TrayEntry *entry) +{ + SDL_Unsupported(); + return NULL; +} + +SDL_TrayEntry *SDL_GetTrayMenuParentEntry(SDL_TrayMenu *menu) +{ + SDL_Unsupported(); + return NULL; +} + +SDL_Tray *SDL_GetTrayMenuParentTray(SDL_TrayMenu *menu) +{ + SDL_Unsupported(); + return NULL; +} + +void SDL_DestroyTray(SDL_Tray *tray) +{ + SDL_Unsupported(); +} diff --git a/src/tray/unix/SDL_tray.c b/src/tray/unix/SDL_tray.c new file mode 100644 index 0000000000..b2e81640d9 --- /dev/null +++ b/src/tray/unix/SDL_tray.c @@ -0,0 +1,664 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2024 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#include "SDL_internal.h" + +#include + +/* getpid() */ +#include + +/* APPINDICATOR_HEADER is not exposed as a build setting, but the code has been + written nevertheless to make future maintenance easier. */ +#ifdef APPINDICATOR_HEADER +#include APPINDICATOR_HEADER +#else +/* ------------------------------------------------------------------------- */ +/* BEGIN THIRD-PARTY HEADER CONTENT */ +/* ------------------------------------------------------------------------- */ +/* Glib 2.0 */ + +typedef unsigned long gulong; +typedef void* gpointer; +typedef char gchar; +typedef int gint; +typedef unsigned int guint; +typedef gint gboolean; +typedef void (*GCallback)(void); +typedef struct _GClosure GClosure; +typedef void (*GClosureNotify) (gpointer data, GClosure *closure); +typedef gboolean (*GSourceFunc) (gpointer user_data); +typedef enum +{ + G_CONNECT_AFTER = 1 << 0, + G_CONNECT_SWAPPED = 1 << 1 +} GConnectFlags; +gulong (*g_signal_connect_data)(gpointer instance, const gchar *detailed_signal, GCallback c_handler, gpointer data, GClosureNotify destroy_data, GConnectFlags connect_flags); + +#define g_signal_connect(instance, detailed_signal, c_handler, data) \ + g_signal_connect_data ((instance), (detailed_signal), (c_handler), (data), NULL, (GConnectFlags) 0) + +#define _G_TYPE_CIC(ip, gt, ct) ((ct*) ip) + +#define G_TYPE_CHECK_INSTANCE_CAST(instance, g_type, c_type) (_G_TYPE_CIC ((instance), (g_type), c_type)) + +#define G_CALLBACK(f) ((GCallback) (f)) + +#define FALSE 0 +#define TRUE 1 + +/* GTK 3.0 */ + +typedef struct _GtkMenu GtkMenu; +typedef struct _GtkMenuItem GtkMenuItem; +typedef struct _GtkMenuShell GtkMenuShell; +typedef struct _GtkWidget GtkWidget; +typedef struct _GtkCheckMenuItem GtkCheckMenuItem; + +gboolean (*gtk_init_check)(int *argc, char ***argv); +void (*gtk_main)(void); + +GtkWidget* (*gtk_menu_new)(void); +GtkWidget* (*gtk_separator_menu_item_new)(void); +GtkWidget* (*gtk_menu_item_new_with_label)(const gchar *label); +void (*gtk_menu_item_set_submenu)(GtkMenuItem *menu_item, GtkWidget *submenu); +GtkWidget* (*gtk_check_menu_item_new_with_label)(const gchar *label); +void (*gtk_check_menu_item_set_active)(GtkCheckMenuItem *check_menu_item, gboolean is_active); +void (*gtk_widget_set_sensitive)(GtkWidget *widget, gboolean sensitive); +void (*gtk_widget_show)(GtkWidget *widget); +void (*gtk_menu_shell_append)(GtkMenuShell *menu_shell, GtkWidget *child); +void (*gtk_menu_shell_insert)(GtkMenuShell *menu_shell, GtkWidget *child, gint position); +void (*gtk_widget_destroy)(GtkWidget *widget); +const gchar *(*gtk_menu_item_get_label)(GtkMenuItem *menu_item); +void (*gtk_menu_item_set_label)(GtkMenuItem *menu_item, const gchar *label); +gboolean (*gtk_check_menu_item_get_active)(GtkCheckMenuItem *check_menu_item); +gboolean (*gtk_widget_get_sensitive)(GtkWidget *widget); + +#define GTK_MENU_ITEM(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_MENU_ITEM, GtkMenuItem)) +#define GTK_WIDGET(widget) (G_TYPE_CHECK_INSTANCE_CAST ((widget), GTK_TYPE_WIDGET, GtkWidget)) +#define GTK_CHECK_MENU_ITEM(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_CHECK_MENU_ITEM, GtkCheckMenuItem)) +#define GTK_MENU(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_MENU, GtkMenu)) + +/* AppIndicator */ + +typedef enum { + APP_INDICATOR_CATEGORY_APPLICATION_STATUS, + APP_INDICATOR_CATEGORY_COMMUNICATIONS, + APP_INDICATOR_CATEGORY_SYSTEM_SERVICES, + APP_INDICATOR_CATEGORY_HARDWARE, + APP_INDICATOR_CATEGORY_OTHER +} AppIndicatorCategory; + +typedef enum { + APP_INDICATOR_STATUS_PASSIVE, + APP_INDICATOR_STATUS_ACTIVE, + APP_INDICATOR_STATUS_ATTENTION +} AppIndicatorStatus; + +typedef struct _AppIndicator AppIndicator; +AppIndicator *(*app_indicator_new)(const gchar *id, const gchar *icon_name, AppIndicatorCategory category); +void (*app_indicator_set_status)(AppIndicator *self, AppIndicatorStatus status); +void (*app_indicator_set_icon)(AppIndicator *self, const gchar *icon_name); +void (*app_indicator_set_menu)(AppIndicator *self, GtkMenu *menu); +/* ------------------------------------------------------------------------- */ +/* END THIRD-PARTY HEADER CONTENT */ +/* ------------------------------------------------------------------------- */ +#endif + +static int main_gtk_thread(void *data) +{ + gtk_main(); + return 0; +} + +#ifdef APPINDICATOR_HEADER + +static void quit_gtk(void) +{ +} + +static bool init_gtk(void) +{ + SDL_DetachThread(SDL_CreateThread(main_gtk_thread, "tray gtk", NULL)); +} + +#else + +static bool gtk_is_init = false; + +static void *libappindicator = NULL; +static void *libgtk = NULL; +static void *libgdk = NULL; + +static void quit_gtk(void) +{ + if (libappindicator) { + dlclose(libappindicator); + libappindicator = NULL; + } + + if (libgtk) { + dlclose(libgtk); + libgtk = NULL; + } + + if (libgdk) { + dlclose(libgdk); + libgdk = NULL; + } + + gtk_is_init = false; +} + +const char *appindicator_names[] = { + "libayatana-appindicator3.so", + "libayatana-appindicator3.so.1", + "libayatana-appindicator.so", + "libappindicator3.so", + "libappindicator3.so.1", + "libappindicator.so", + "libappindicator.so.1", + NULL +}; + +const char *gtk_names[] = { + "libgtk-3.so", + "libgtk-3.so.0", + NULL +}; + +const char *gdk_names[] = { + "libgdk-3.so", + "libgdk-3.so.0", + NULL +}; + +static void *find_lib(const char **names) +{ + const char **name_ptr = names; + void *handle = NULL; + + do { + handle = dlopen(*name_ptr, RTLD_LAZY); + } while (*++name_ptr && !handle); + + return handle; +} + +static bool init_gtk(void) +{ + if (gtk_is_init) { + return true; + } + + libappindicator = find_lib(appindicator_names); + libgtk = find_lib(gtk_names); + libgdk = find_lib(gdk_names); + + if (!libappindicator || !libgtk || !libgdk) { + quit_gtk(); + return SDL_SetError("Could not load GTK/AppIndicator libraries"); + } + + gtk_init_check = dlsym(libgtk, "gtk_init_check"); + gtk_main = dlsym(libgtk, "gtk_main"); + gtk_menu_new = dlsym(libgtk, "gtk_menu_new"); + gtk_separator_menu_item_new = dlsym(libgtk, "gtk_separator_menu_item_new"); + gtk_menu_item_new_with_label = dlsym(libgtk, "gtk_menu_item_new_with_label"); + gtk_menu_item_set_submenu = dlsym(libgtk, "gtk_menu_item_set_submenu"); + gtk_check_menu_item_new_with_label = dlsym(libgtk, "gtk_check_menu_item_new_with_label"); + gtk_check_menu_item_set_active = dlsym(libgtk, "gtk_check_menu_item_set_active"); + gtk_widget_set_sensitive = dlsym(libgtk, "gtk_widget_set_sensitive"); + gtk_widget_show = dlsym(libgtk, "gtk_widget_show"); + gtk_menu_shell_append = dlsym(libgtk, "gtk_menu_shell_append"); + gtk_menu_shell_insert = dlsym(libgtk, "gtk_menu_shell_insert"); + gtk_widget_destroy = dlsym(libgtk, "gtk_widget_destroy"); + gtk_menu_item_get_label = dlsym(libgtk, "gtk_menu_item_get_label"); + gtk_menu_item_set_label = dlsym(libgtk, "gtk_menu_item_set_label"); + gtk_check_menu_item_get_active = dlsym(libgtk, "gtk_check_menu_item_get_active"); + gtk_widget_get_sensitive = dlsym(libgtk, "gtk_widget_get_sensitive"); + + g_signal_connect_data = dlsym(libgdk, "g_signal_connect_data"); + + app_indicator_new = dlsym(libappindicator, "app_indicator_new"); + app_indicator_set_status = dlsym(libappindicator, "app_indicator_set_status"); + app_indicator_set_icon = dlsym(libappindicator, "app_indicator_set_icon"); + app_indicator_set_menu = dlsym(libappindicator, "app_indicator_set_menu"); + + if (!gtk_init_check || + !gtk_main || + !gtk_menu_new || + !gtk_separator_menu_item_new || + !gtk_menu_item_new_with_label || + !gtk_menu_item_set_submenu || + !gtk_check_menu_item_new_with_label || + !gtk_check_menu_item_set_active || + !gtk_widget_set_sensitive || + !gtk_widget_show || + !gtk_menu_shell_append || + !gtk_menu_shell_insert || + !gtk_widget_destroy || + !g_signal_connect_data || + !app_indicator_new || + !app_indicator_set_status || + !app_indicator_set_icon || + !app_indicator_set_menu || + !gtk_menu_item_get_label || + !gtk_menu_item_set_label || + !gtk_check_menu_item_get_active || + !gtk_widget_get_sensitive) { + quit_gtk(); + return SDL_SetError("Could not load GTK/AppIndicator functions"); + } + + if (gtk_init_check(0, NULL) == FALSE) { + quit_gtk(); + return SDL_SetError("Could not init GTK"); + } + + gtk_is_init = true; + + SDL_DetachThread(SDL_CreateThread(main_gtk_thread, "tray gtk", NULL)); + + return true; +} +#endif + +struct SDL_TrayMenu { + GtkMenuShell *menu; + + size_t nEntries; + SDL_TrayEntry **entries; + + SDL_Tray *parent_tray; + SDL_TrayEntry *parent_entry; +}; + +struct SDL_TrayEntry { + SDL_TrayMenu *parent; + GtkWidget *item; + + /* Checkboxes are "activated" when programmatically checked/unchecked; this + is a workaround. */ + bool ignore_signal; + + SDL_TrayEntryFlags flags; + SDL_TrayCallback callback; + void *userdata; + SDL_TrayMenu *submenu; +}; + +struct SDL_Tray { + AppIndicator *indicator; + SDL_TrayMenu *menu; + char icon_path[256]; +}; + +static void call_callback(GtkMenuItem *item, gpointer ptr) +{ + SDL_TrayEntry *entry = ptr; + + /* Not needed with AppIndicator, may be needed with other frameworks */ + /* if (entry->flags & SDL_TRAYENTRY_CHECKBOX) { + SDL_SetTrayEntryChecked(entry, !SDL_GetTrayEntryChecked(entry)); + } */ + + if (entry->ignore_signal) { + return; + } + + if (entry->callback) { + entry->callback(entry->userdata, entry); + } +} + +/* Since AppIndicator deals only in filenames, which are inherently subject to + timing attacks, don't bother generating a secure filename. */ +static bool get_tmp_filename(char *buffer, size_t size) +{ + static int count = 0; + + if (size < 64) { + return SDL_SetError("Can't create temporary file for icon: size %ld < 64", size); + } + + int would_have_written = SDL_snprintf(buffer, size, "/tmp/sdl_appindicator_icon_%d_%d.bmp", getpid(), count++); + + return would_have_written > 0 && would_have_written < size - 1; +} + +static const char *get_appindicator_id(void) +{ + static int count = 0; + static char buffer[256]; + + int would_have_written = SDL_snprintf(buffer, sizeof(buffer), "sdl-appindicator-%d-%d", getpid(), count++); + + if (would_have_written <= 0 || would_have_written >= sizeof(buffer) - 1) { + SDL_SetError("Couldn't fit %d bytes in buffer of size %ld", would_have_written, sizeof(buffer)); + return NULL; + } + + return buffer; +} + +static void DestroySDLMenu(SDL_TrayMenu *menu) +{ + for (int i = 0; i < menu->nEntries; i++) { + if (menu->entries[i] && menu->entries[i]->submenu) { + DestroySDLMenu(menu->entries[i]->submenu); + } + SDL_free(menu->entries[i]); + } + SDL_free(menu->entries); + SDL_free(menu); +} + +SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip) +{ + if (init_gtk() != true) { + return NULL; + } + + SDL_Tray *tray = (SDL_Tray *) SDL_malloc(sizeof(SDL_Tray)); + + if (!tray) { + return NULL; + } + + SDL_memset((void *) tray, 0, sizeof(*tray)); + + get_tmp_filename(tray->icon_path, sizeof(tray->icon_path)); + SDL_SaveBMP(icon, tray->icon_path); + + tray->indicator = app_indicator_new(get_appindicator_id(), tray->icon_path, + APP_INDICATOR_CATEGORY_APPLICATION_STATUS); + + app_indicator_set_status(tray->indicator, APP_INDICATOR_STATUS_ACTIVE); + + return tray; +} + +void SDL_SetTrayIcon(SDL_Tray *tray, SDL_Surface *icon) +{ + if (*tray->icon_path) { + SDL_RemovePath(tray->icon_path); + } + + /* AppIndicator caches the icon files; always change filename to avoid caching */ + + if (icon) { + get_tmp_filename(tray->icon_path, sizeof(tray->icon_path)); + SDL_SaveBMP(icon, tray->icon_path); + app_indicator_set_icon(tray->indicator, tray->icon_path); + } else { + *tray->icon_path = '\0'; + 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 = SDL_malloc(sizeof(SDL_TrayMenu)); + + if (!tray->menu) { + return NULL; + } + + tray->menu->menu = (GtkMenuShell *)gtk_menu_new(); + tray->menu->parent_tray = tray; + tray->menu->parent_entry = NULL; + tray->menu->nEntries = 0; + tray->menu->entries = NULL; + + app_indicator_set_menu(tray->indicator, GTK_MENU(tray->menu->menu)); + + return tray->menu; +} + +SDL_TrayMenu *SDL_GetTrayMenu(SDL_Tray *tray) +{ + return tray->menu; +} + +SDL_TrayMenu *SDL_CreateTraySubmenu(SDL_TrayEntry *entry) +{ + if (entry->submenu) { + SDL_SetError("Tray entry submenu already exists"); + return NULL; + } + + if (!(entry->flags & SDL_TRAYENTRY_SUBMENU)) { + SDL_SetError("Cannot create submenu for entry not created with SDL_TRAYENTRY_SUBMENU"); + return NULL; + } + + entry->submenu = SDL_malloc(sizeof(SDL_TrayMenu)); + + if (!entry->submenu) { + return NULL; + } + + entry->submenu->menu = (GtkMenuShell *)gtk_menu_new(); + entry->submenu->parent_tray = NULL; + entry->submenu->parent_entry = entry; + entry->submenu->nEntries = 0; + entry->submenu->entries = NULL; + + gtk_menu_item_set_submenu(GTK_MENU_ITEM(entry->item), GTK_WIDGET(entry->submenu->menu)); + + return entry->submenu; +} + +SDL_TrayMenu *SDL_GetTraySubmenu(SDL_TrayEntry *entry) +{ + return entry->submenu; +} + +const SDL_TrayEntry **SDL_GetTrayEntries(SDL_TrayMenu *menu, int *size) +{ + if (size) { + *size = menu->nEntries; + } + + return (const SDL_TrayEntry **) menu->entries; +} + +void SDL_RemoveTrayEntry(SDL_TrayEntry *entry) +{ + if (!entry) { + return; + } + + SDL_TrayMenu *menu = entry->parent; + + bool found = false; + for (int i = 0; i < menu->nEntries - 1; i++) { + if (menu->entries[i] == entry) { + found = true; + } + + if (found) { + menu->entries[i] = menu->entries[i + 1]; + } + } + + if (entry->submenu) { + DestroySDLMenu(entry->submenu); + } + + menu->nEntries--; + SDL_TrayEntry ** new_entries = SDL_realloc(menu->entries, menu->nEntries * sizeof(SDL_TrayEntry *)); + + /* Not sure why shrinking would fail, but even if it does, we can live with a "too big" array */ + if (new_entries) { + menu->entries = new_entries; + } + + gtk_widget_destroy(entry->item); + SDL_free(entry); +} + +SDL_TrayEntry *SDL_InsertTrayEntryAt(SDL_TrayMenu *menu, int pos, const char *label, SDL_TrayEntryFlags flags) +{ + if (pos < -1 || pos > (int) menu->nEntries) { + SDL_InvalidParamError("pos"); + return NULL; + } + + if (pos == -1) { + pos = menu->nEntries; + } + + SDL_TrayEntry *entry = SDL_malloc(sizeof(SDL_TrayEntry)); + + if (!entry) { + return NULL; + } + + SDL_memset((void *) entry, 0, sizeof(*entry)); + entry->parent = menu; + entry->item = NULL; + entry->ignore_signal = false; + entry->flags = flags; + entry->callback = NULL; + entry->userdata = NULL; + entry->submenu = NULL; + + if (label == NULL) { + entry->item = gtk_separator_menu_item_new(); + } else if (flags & SDL_TRAYENTRY_CHECKBOX) { + entry->item = gtk_check_menu_item_new_with_label(label); + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(entry->item), !!(flags & SDL_TRAYENTRY_CHECKED)); + } else { + entry->item = gtk_menu_item_new_with_label(label); + } + + gtk_widget_set_sensitive(entry->item, !(flags & SDL_TRAYENTRY_DISABLED)); + + SDL_TrayEntry **new_entries = (SDL_TrayEntry **) SDL_realloc(menu->entries, (menu->nEntries + 1) * sizeof(SDL_TrayEntry *)); + + if (!new_entries) { + SDL_free(entry); + return NULL; + } + + menu->entries = new_entries; + menu->nEntries++; + + for (int i = menu->nEntries - 1; i > pos; i--) { + menu->entries[i] = menu->entries[i - 1]; + } + + new_entries[pos] = entry; + + gtk_widget_show(entry->item); + gtk_menu_shell_insert(menu->menu, entry->item, (pos == menu->nEntries) ? -1 : pos); + + g_signal_connect(entry->item, "activate", G_CALLBACK(call_callback), entry); + + return entry; +} + +void SDL_SetTrayEntryLabel(SDL_TrayEntry *entry, const char *label) +{ + gtk_menu_item_set_label(GTK_MENU_ITEM(entry->item), label); +} + +const char *SDL_GetTrayEntryLabel(SDL_TrayEntry *entry) +{ + return gtk_menu_item_get_label(GTK_MENU_ITEM(entry->item)); +} + +void SDL_SetTrayEntryChecked(SDL_TrayEntry *entry, bool checked) +{ + if (!(entry->flags & SDL_TRAYENTRY_CHECKBOX)) { + SDL_SetError("Cannot update check for entry not created with SDL_TRAYENTRY_CHECKBOX"); + return; + } + + entry->ignore_signal = true; + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(entry->item), checked); + entry->ignore_signal = false; +} + +bool SDL_GetTrayEntryChecked(SDL_TrayEntry *entry) +{ + if (!(entry->flags & SDL_TRAYENTRY_CHECKBOX)) { + SDL_SetError("Cannot fetch check for entry not created with SDL_TRAYENTRY_CHECKBOX"); + return false; + } + + return gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(entry->item)); +} + +void SDL_SetTrayEntryEnabled(SDL_TrayEntry *entry, bool enabled) +{ + gtk_widget_set_sensitive(entry->item, enabled); +} + +bool SDL_GetTrayEntryEnabled(SDL_TrayEntry *entry) +{ + return gtk_widget_get_sensitive(entry->item); +} + +void SDL_SetTrayEntryCallback(SDL_TrayEntry *entry, SDL_TrayCallback callback, void *userdata) +{ + entry->callback = callback; + entry->userdata = userdata; +} + +SDL_TrayMenu *SDL_GetTrayEntryParent(SDL_TrayEntry *entry) +{ + return entry->parent; +} + +SDL_TrayEntry *SDL_GetTrayMenuParentEntry(SDL_TrayMenu *menu) +{ + return menu->parent_entry; +} + +SDL_Tray *SDL_GetTrayMenuParentTray(SDL_TrayMenu *menu) +{ + return menu->parent_tray; +} + +void SDL_DestroyTray(SDL_Tray *tray) +{ + if (!tray) { + return; + } + + if (tray->menu) { + DestroySDLMenu(tray->menu); + } + + if (*tray->icon_path) { + SDL_RemovePath(tray->icon_path); + } + + SDL_free(tray); +} diff --git a/src/tray/windows/SDL_tray.c b/src/tray/windows/SDL_tray.c new file mode 100644 index 0000000000..a09a086ca4 --- /dev/null +++ b/src/tray/windows/SDL_tray.c @@ -0,0 +1,589 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2024 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#include "SDL_internal.h" + +#include "../../video/windows/SDL_surface_utils.h" + +#include +#include +#include + +#include + +#define WM_TRAYICON (WM_USER + 1) + +struct SDL_TrayMenu { + HMENU hMenu; + + size_t nEntries; + SDL_TrayEntry **entries; + + SDL_Tray *parent_tray; + SDL_TrayEntry *parent_entry; +}; + +struct SDL_TrayEntry { + SDL_TrayMenu *parent; + UINT_PTR id; + + char label_cache[4096]; + SDL_TrayEntryFlags flags; + SDL_TrayCallback callback; + void *userdata; + SDL_TrayMenu *submenu; +}; + +struct SDL_Tray { + NOTIFYICONDATAW nid; + HWND hwnd; + HICON icon; + SDL_TrayMenu *menu; +}; + +static UINT_PTR get_next_id(void) +{ + static UINT_PTR next_id = 0; + return ++next_id; +} + +static SDL_TrayEntry *find_entry_in_menu(SDL_TrayMenu *menu, UINT_PTR id) +{ + for (size_t i = 0; i < menu->nEntries; i++) { + SDL_TrayEntry *entry = menu->entries[i]; + + if (entry->id == id) { + return entry; + } + + if (entry->submenu) { + SDL_TrayEntry *e = find_entry_in_menu(entry->submenu, id); + + if (e) { + return e; + } + } + } + + return NULL; +} + +static SDL_TrayEntry *find_entry_with_id(SDL_Tray *tray, UINT_PTR id) +{ + if (!tray->menu) { + return NULL; + } + + return find_entry_in_menu(tray->menu, id); +} + +LRESULT CALLBACK TrayWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { + SDL_Tray *tray = (SDL_Tray *) GetWindowLongPtr(hwnd, GWLP_USERDATA); + SDL_TrayEntry *entry = NULL; + + if (!tray) { + return DefWindowProc(hwnd, uMsg, wParam, lParam); + } + + switch (uMsg) { + case WM_TRAYICON: + if (LOWORD(lParam) == WM_CONTEXTMENU || LOWORD(lParam) == WM_LBUTTONUP) { + SetForegroundWindow(hwnd); + TrackPopupMenu(tray->menu->hMenu, TPM_BOTTOMALIGN | TPM_RIGHTALIGN, GET_X_LPARAM(wParam), GET_Y_LPARAM(wParam), 0, hwnd, NULL); + } + break; + + case WM_COMMAND: + entry = find_entry_with_id(tray, LOWORD(wParam)); + + if (entry && (entry->flags & SDL_TRAYENTRY_CHECKBOX)) { + SDL_SetTrayEntryChecked(entry, !SDL_GetTrayEntryChecked(entry)); + } + + if (entry && entry->callback) { + entry->callback(entry->userdata, entry); + } + break; + + default: + return DefWindowProc(hwnd, uMsg, wParam, lParam); + } + return 0; +} + +static void DestroySDLMenu(SDL_TrayMenu *menu) +{ + for (size_t i = 0; i < menu->nEntries; i++) { + if (menu->entries[i] && menu->entries[i]->submenu) { + DestroySDLMenu(menu->entries[i]->submenu); + } + SDL_free(menu->entries[i]); + } + SDL_free(menu->entries); + DestroyMenu(menu->hMenu); + SDL_free(menu); +} + +static wchar_t *convert_label(const char *in) +{ + const char *c; + char *c2; + int len = 0; + + for (c = in; *c; c++) { + len += (*c == '&') ? 2 : 1; + } + + char *escaped = SDL_malloc(SDL_strlen(in) + len + 1); + + if (!escaped) { + return NULL; + } + + for (c = in, c2 = escaped; *c;) { + if (*c == '&') { + *c2++ = *c; + } + + *c2++ = *c++; + } + + *c2 = '\0'; + + int len_w = MultiByteToWideChar(CP_UTF8, 0, escaped, len + 1, NULL, 0); + wchar_t *out = (wchar_t *)SDL_malloc(len_w * sizeof(wchar_t)); + + if (!out) { + SDL_free(escaped); + return NULL; + } + + MultiByteToWideChar(CP_UTF8, 0, escaped, -1, out, len_w); + + SDL_free(escaped); + + return out; +} + +static void register_tray_window_class(void) +{ + static bool init = false; + + if (init) { + return; + } + + HINSTANCE hInstance = GetModuleHandle(NULL); + WNDCLASSW wc; + ZeroMemory(&wc, sizeof(WNDCLASS)); + wc.lpfnWndProc = TrayWindowProc; + wc.hInstance = hInstance; + wc.lpszClassName = L"SDLTrayRunner"; + + RegisterClassW(&wc); + + init = true; +} + +SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip) +{ + SDL_Tray *tray = SDL_malloc(sizeof(SDL_Tray)); + + if (!tray) { + return NULL; + } + + tray->hwnd = NULL; + tray->menu = NULL; + + register_tray_window_class(); + + HINSTANCE hInstance = GetModuleHandle(NULL); + tray->hwnd = CreateWindowExW(0, L"SDLTrayRunner", NULL, 0, 0, 0, 0, 0, HWND_MESSAGE, NULL, hInstance, NULL); + + ZeroMemory(&tray->nid, sizeof(NOTIFYICONDATAW)); + tray->nid.cbSize = sizeof(NOTIFYICONDATAW); + tray->nid.hWnd = tray->hwnd; + tray->nid.uID = (UINT) get_next_id(); + tray->nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP | NIF_SHOWTIP; + tray->nid.uCallbackMessage = WM_TRAYICON; + tray->nid.uVersion = NOTIFYICON_VERSION_4; + mbstowcs_s(NULL, tray->nid.szTip, sizeof(tray->nid.szTip) / sizeof(*tray->nid.szTip), tooltip, _TRUNCATE); + + if (icon) { + tray->nid.hIcon = CreateIconFromSurface(icon); + + if (!tray->nid.hIcon) { + tray->nid.hIcon = LoadIcon(NULL, IDI_APPLICATION); + } + + tray->icon = tray->nid.hIcon; + } else { + tray->nid.hIcon = LoadIcon(NULL, IDI_APPLICATION); + tray->icon = tray->nid.hIcon; + } + + Shell_NotifyIconW(NIM_ADD, &tray->nid); + Shell_NotifyIconW(NIM_SETVERSION, &tray->nid); + + SetWindowLongPtr(tray->hwnd, GWLP_USERDATA, (LONG_PTR) tray); + + return tray; +} + +void SDL_SetTrayIcon(SDL_Tray *tray, SDL_Surface *icon) +{ + if (tray->icon) { + DestroyIcon(tray->icon); + } + + if (icon) { + tray->nid.hIcon = CreateIconFromSurface(icon); + + if (!tray->nid.hIcon) { + tray->nid.hIcon = LoadIcon(NULL, IDI_APPLICATION); + } + + 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 = SDL_malloc(sizeof(SDL_TrayMenu)); + + if (!tray->menu) { + return NULL; + } + + SDL_memset((void *) tray->menu, 0, sizeof(*tray->menu)); + + tray->menu->hMenu = CreatePopupMenu(); + tray->menu->parent_tray = tray; + + return tray->menu; +} + +SDL_TrayMenu *SDL_GetTrayMenu(SDL_Tray *tray) +{ + return tray->menu; +} + +SDL_TrayMenu *SDL_CreateTraySubmenu(SDL_TrayEntry *entry) +{ + if (!entry->submenu) { + SDL_SetError("Cannot create submenu for entry not created with SDL_TRAYENTRY_SUBMENU"); + } + + return entry->submenu; +} + +SDL_TrayMenu *SDL_GetTraySubmenu(SDL_TrayEntry *entry) +{ + return entry->submenu; +} + +const SDL_TrayEntry **SDL_GetTrayEntries(SDL_TrayMenu *menu, int *size) +{ + if (size) { + *size = (int) menu->nEntries; + } + + return (const SDL_TrayEntry **) menu->entries; +} + +void SDL_RemoveTrayEntry(SDL_TrayEntry *entry) +{ + if (!entry) { + return; + } + + SDL_TrayMenu *menu = entry->parent; + + bool found = false; + for (size_t i = 0; i < menu->nEntries - 1; i++) { + if (menu->entries[i] == entry) { + found = true; + } + + if (found) { + menu->entries[i] = menu->entries[i + 1]; + } + } + + if (entry->submenu) { + DestroySDLMenu(entry->submenu); + } + + menu->nEntries--; + SDL_TrayEntry ** new_entries = SDL_realloc(menu->entries, menu->nEntries * sizeof(SDL_TrayEntry *)); + + /* Not sure why shrinking would fail, but even if it does, we can live with a "too big" array */ + if (new_entries) { + menu->entries = new_entries; + } + + if (!DeleteMenu(menu->hMenu, (UINT) entry->id, MF_BYCOMMAND)) { + /* This is somewhat useless since we don't return anything, but might help with eventual bugs */ + SDL_SetError("Couldn't destroy tray entry"); + } + + SDL_free(entry); +} + +SDL_TrayEntry *SDL_InsertTrayEntryAt(SDL_TrayMenu *menu, int pos, const char *label, SDL_TrayEntryFlags flags) +{ + if (pos < -1 || pos > (int) menu->nEntries) { + SDL_InvalidParamError("pos"); + return NULL; + } + + int windows_compatible_pos = pos; + + if (pos == -1) { + pos = (int) menu->nEntries; + } else if (pos == menu->nEntries) { + windows_compatible_pos = -1; + } + + SDL_TrayEntry *entry = SDL_malloc(sizeof(SDL_TrayEntry)); + + if (!entry) { + return NULL; + } + + wchar_t *label_w = NULL; + + if (label && !(label_w = convert_label(label))) { + SDL_free(entry); + return NULL; + } + + entry->parent = menu; + entry->flags = flags; + entry->callback = NULL; + entry->userdata = NULL; + entry->submenu = NULL; + SDL_snprintf(entry->label_cache, sizeof(entry->label_cache), "%s", label ? label : ""); + + if (label != NULL && flags & SDL_TRAYENTRY_SUBMENU) { + entry->submenu = SDL_malloc(sizeof(SDL_TrayMenu)); + + if (!entry->submenu) { + SDL_free(entry); + SDL_free(label_w); + return NULL; + } + + entry->submenu->hMenu = CreatePopupMenu(); + entry->submenu->nEntries = 0; + entry->submenu->entries = NULL; + + entry->id = (UINT_PTR) entry->submenu->hMenu; + } else { + entry->id = get_next_id(); + } + + SDL_TrayEntry **new_entries = (SDL_TrayEntry **) SDL_realloc(menu->entries, (menu->nEntries + 1) * sizeof(SDL_TrayEntry **)); + + if (!new_entries) { + SDL_free(entry); + SDL_free(label_w); + if (entry->submenu) { + DestroyMenu(entry->submenu->hMenu); + SDL_free(entry->submenu); + } + return NULL; + } + + menu->entries = new_entries; + menu->nEntries++; + + for (int i = (int) menu->nEntries - 1; i > pos; i--) { + menu->entries[i] = menu->entries[i - 1]; + } + + new_entries[pos] = entry; + + if (label == NULL) { + InsertMenuW(menu->hMenu, windows_compatible_pos, MF_SEPARATOR | MF_BYPOSITION, entry->id, NULL); + } else { + UINT mf = MF_STRING | MF_BYPOSITION; + if (flags & SDL_TRAYENTRY_SUBMENU) { + mf = MF_POPUP; + } + + if (flags & SDL_TRAYENTRY_DISABLED) { + mf |= MF_DISABLED | MF_GRAYED; + } + + if (flags & SDL_TRAYENTRY_CHECKED) { + mf |= MF_CHECKED; + } + + InsertMenuW(menu->hMenu, windows_compatible_pos, mf, entry->id, label_w); + + SDL_free(label_w); + } + + return entry; +} + +void SDL_SetTrayEntryLabel(SDL_TrayEntry *entry, const char *label) +{ + SDL_snprintf(entry->label_cache, sizeof(entry->label_cache), "%s", label); + + wchar_t *label_w = convert_label(label); + + if (!label_w) { + return; + } + + MENUITEMINFOW mii; + mii.cbSize = sizeof(MENUITEMINFOW); + mii.fMask = MIIM_STRING; + + mii.dwTypeData = label_w; + mii.cch = (UINT) wcslen(label_w); + + if (!SetMenuItemInfoW(entry->parent->hMenu, (UINT) entry->id, TRUE, &mii)) { + SDL_SetError("Couldn't update tray entry label"); + } + + SDL_free(label_w); +} + +const char *SDL_GetTrayEntryLabel(SDL_TrayEntry *entry) +{ + return entry->label_cache; +} + +void SDL_SetTrayEntryChecked(SDL_TrayEntry *entry, bool checked) +{ + if (!(entry->flags & SDL_TRAYENTRY_CHECKBOX)) { + SDL_SetError("Can't check/uncheck tray entry not created with SDL_TRAYENTRY_CHECKBOX"); + return; + } + + CheckMenuItem(entry->parent->hMenu, (UINT) entry->id, checked ? MF_CHECKED : MF_UNCHECKED); +} + +bool SDL_GetTrayEntryChecked(SDL_TrayEntry *entry) +{ + if (!(entry->flags & SDL_TRAYENTRY_CHECKBOX)) { + SDL_SetError("Can't get check status of tray entry not created with SDL_TRAYENTRY_CHECKBOX"); + return false; + } + + MENUITEMINFOW mii; + mii.cbSize = sizeof(MENUITEMINFOW); + mii.fMask = MIIM_STATE; + + GetMenuItemInfoW(entry->parent->hMenu, (UINT) entry->id, FALSE, &mii); + + return !!(mii.fState & MFS_CHECKED); +} + +void SDL_SetTrayEntryEnabled(SDL_TrayEntry *entry, bool enabled) +{ + if (!(entry->flags & SDL_TRAYENTRY_CHECKBOX)) { + SDL_SetError("Cannot update check for entry not created with SDL_TRAYENTRY_CHECKBOX"); + return; + } + + EnableMenuItem(entry->parent->hMenu, (UINT) entry->id, MF_BYCOMMAND | (enabled ? MF_ENABLED : (MF_DISABLED | MF_GRAYED))); +} + +bool SDL_GetTrayEntryEnabled(SDL_TrayEntry *entry) +{ + if (!(entry->flags & SDL_TRAYENTRY_CHECKBOX)) { + SDL_SetError("Cannot fetch check for entry not created with SDL_TRAYENTRY_CHECKBOX"); + return false; + } + + MENUITEMINFOW mii; + mii.cbSize = sizeof(MENUITEMINFOW); + mii.fMask = MIIM_STATE; + + GetMenuItemInfoW(entry->parent->hMenu, (UINT) entry->id, FALSE, &mii); + + return !!(mii.fState & MFS_ENABLED); +} + +void SDL_SetTrayEntryCallback(SDL_TrayEntry *entry, SDL_TrayCallback callback, void *userdata) +{ + entry->callback = callback; + entry->userdata = userdata; +} + +SDL_TrayMenu *SDL_GetTrayEntryParent(SDL_TrayEntry *entry) +{ + return entry->parent; +} + +SDL_TrayEntry *SDL_GetTrayMenuParentEntry(SDL_TrayMenu *menu) +{ + return menu->parent_entry; +} + +SDL_Tray *SDL_GetTrayMenuParentTray(SDL_TrayMenu *menu) +{ + return menu->parent_tray; +} + +void SDL_DestroyTray(SDL_Tray *tray) +{ + if (!tray) { + return; + } + + Shell_NotifyIconW(NIM_DELETE, &tray->nid); + + if (tray->menu) { + DestroySDLMenu(tray->menu); + } + + if (tray->icon) { + DestroyIcon(tray->icon); + } + + if (tray->hwnd) { + DestroyWindow(tray->hwnd); + } + + SDL_free(tray); +} diff --git a/src/video/windows/SDL_surface_utils.c b/src/video/windows/SDL_surface_utils.c new file mode 100644 index 0000000000..bdcac106b8 --- /dev/null +++ b/src/video/windows/SDL_surface_utils.c @@ -0,0 +1,95 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2024 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#include "SDL_surface_utils.h" + +#include "../SDL_surface_c.h" + +HICON CreateIconFromSurface(SDL_Surface *surface) +{ + SDL_Surface *s = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_RGBA32); + if (!s) { + return NULL; + } + + /* The dimensions will be needed after s is freed */ + const int width = s->w; + const int height = s->h; + + BITMAPINFO bmpInfo; + ZeroMemory(&bmpInfo, sizeof(BITMAPINFO)); + bmpInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + bmpInfo.bmiHeader.biWidth = width; + bmpInfo.bmiHeader.biHeight = -height; /* Top-down bitmap */ + bmpInfo.bmiHeader.biPlanes = 1; + bmpInfo.bmiHeader.biBitCount = 32; + bmpInfo.bmiHeader.biCompression = BI_RGB; + + HDC hdc = GetDC(NULL); + void* pBits = NULL; + HBITMAP hBitmap = CreateDIBSection(hdc, &bmpInfo, DIB_RGB_COLORS, &pBits, NULL, 0); + if (!hBitmap) { + ReleaseDC(NULL, hdc); + SDL_DestroySurface(s); + return NULL; + } + + SDL_memcpy(pBits, s->pixels, width * height * 4); + + SDL_DestroySurface(s); + + HBITMAP hMask = CreateBitmap(width, height, 1, 1, NULL); + if (!hMask) { + DeleteObject(hBitmap); + ReleaseDC(NULL, hdc); + return NULL; + } + + HDC hdcMem = CreateCompatibleDC(hdc); + HGDIOBJ oldBitmap = SelectObject(hdcMem, hMask); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + BYTE* pixel = (BYTE*)pBits + (y * width + x) * 4; + BYTE alpha = pixel[3]; + COLORREF maskColor = (alpha == 0) ? RGB(0, 0, 0) : RGB(255, 255, 255); + SetPixel(hdcMem, x, y, maskColor); + } + } + + ICONINFO iconInfo; + iconInfo.fIcon = TRUE; + iconInfo.xHotspot = 0; + iconInfo.yHotspot = 0; + iconInfo.hbmMask = hMask; + iconInfo.hbmColor = hBitmap; + + HICON hIcon = CreateIconIndirect(&iconInfo); + + SelectObject(hdcMem, oldBitmap); + DeleteDC(hdcMem); + DeleteObject(hBitmap); + DeleteObject(hMask); + ReleaseDC(NULL, hdc); + + return hIcon; +} diff --git a/src/video/windows/SDL_surface_utils.h b/src/video/windows/SDL_surface_utils.h new file mode 100644 index 0000000000..5c05a4c8d1 --- /dev/null +++ b/src/video/windows/SDL_surface_utils.h @@ -0,0 +1,38 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2024 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_surface_utils_h_ +#define SDL_surface_utils_h_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern HICON CreateIconFromSurface(SDL_Surface *surface); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index fe6d8e36f2..1a799482b0 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -407,6 +407,7 @@ add_sdl_test_executable(testdialog SOURCES testdialog.c) add_sdl_test_executable(testtime SOURCES testtime.c) add_sdl_test_executable(testmanymouse SOURCES testmanymouse.c) add_sdl_test_executable(testmodal SOURCES testmodal.c) +add_sdl_test_executable(testtray SOURCES testtray.c) add_sdl_test_executable(testprocess diff --git a/test/sdl-test_round.bmp b/test/sdl-test_round.bmp new file mode 100644 index 0000000000000000000000000000000000000000..edb60497249fcb048281e1bed2126b735f968abd GIT binary patch literal 147594 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 literal 0 HcmV?d00001 diff --git a/test/testtray.c b/test/testtray.c new file mode 100644 index 0000000000..df3a5488f6 --- /dev/null +++ b/test/testtray.c @@ -0,0 +1,599 @@ +#include +#include +#include + +static void SDLCALL tray_quit(void *ptr, SDL_TrayEntry *entry) +{ + SDL_Event e; + e.type = SDL_EVENT_QUIT; + SDL_PushEvent(&e); +} + +static void SDLCALL apply_icon(void *ptr, const char * const *filelist, int filter) +{ + if (!*filelist) { + return; + } + + SDL_Surface *icon = SDL_LoadBMP(*filelist); + + if (!icon) { + SDL_Log("Couldn't load icon '%s': %s", *filelist, SDL_GetError()); + return; + } + + SDL_Tray *tray = (SDL_Tray *) ptr; + SDL_SetTrayIcon(tray, icon); + + SDL_DestroySurface(icon); +} + +static void SDLCALL change_icon(void *ptr, SDL_TrayEntry *entry) +{ + SDL_DialogFileFilter filters[] = { + { "BMP image files", "bmp" }, + { "All files", "*" }, + }; + + SDL_ShowOpenFileDialog(apply_icon, ptr, NULL, filters, 2, NULL, 0); +} + +static void SDLCALL print_entry(void *ptr, SDL_TrayEntry *entry) +{ + SDL_Log("Clicked on button '%s'\n", SDL_GetTrayEntryLabel(entry)); +} + +static void SDLCALL set_entry_enabled(void *ptr, SDL_TrayEntry *entry) +{ + SDL_TrayEntry *target = (SDL_TrayEntry *) ptr; + SDL_SetTrayEntryEnabled(target, true); +} + +static void SDLCALL set_entry_disabled(void *ptr, SDL_TrayEntry *entry) +{ + SDL_TrayEntry *target = (SDL_TrayEntry *) ptr; + SDL_SetTrayEntryEnabled(target, false); +} + +static void SDLCALL set_entry_checked(void *ptr, SDL_TrayEntry *entry) +{ + SDL_TrayEntry *target = (SDL_TrayEntry *) ptr; + SDL_SetTrayEntryChecked(target, true); +} + +static void SDLCALL set_entry_unchecked(void *ptr, SDL_TrayEntry *entry) +{ + SDL_TrayEntry *target = (SDL_TrayEntry *) ptr; + SDL_SetTrayEntryChecked(target, false); +} + +static void SDLCALL remove_entry(void *ptr, SDL_TrayEntry *entry) +{ + SDL_TrayEntry *target = (SDL_TrayEntry *) ptr; + SDL_RemoveTrayEntry(target); + + SDL_TrayMenu *ctrl_submenu = SDL_GetTrayEntryParent(entry); + SDL_TrayEntry *ctrl_entry = SDL_GetTrayMenuParentEntry(ctrl_submenu); + + if (!ctrl_entry) { + SDL_Log("Attempt to remove a menu that isn't a submenu. This shouldn't happen.\n"); + return; + } + + SDL_RemoveTrayEntry(ctrl_entry); +} + +static void SDLCALL append_button_to(void *ptr, SDL_TrayEntry *entry) +{ + SDL_TrayMenu *menu = (SDL_TrayMenu *) ptr; + SDL_TrayMenu *submenu; + SDL_TrayEntry *new_ctrl; + SDL_TrayEntry *new_ctrl_remove; + SDL_TrayEntry *new_ctrl_enabled; + SDL_TrayEntry *new_ctrl_disabled; + SDL_TrayEntry *new_example; + + new_ctrl = SDL_InsertTrayEntryAt(SDL_GetTrayEntryParent(entry), -1, "New button", SDL_TRAYENTRY_SUBMENU); + + if (!new_ctrl) { + SDL_Log("Couldn't insert entry in control tray: %s\n", SDL_GetError()); + return; + } + + /* ---------- */ + + submenu = SDL_CreateTraySubmenu(new_ctrl); + + if (!new_ctrl) { + SDL_Log("Couldn't create control tray entry submenu: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + return; + } + + /* ---------- */ + + new_example = SDL_InsertTrayEntryAt(menu, -1, "New button", SDL_TRAYENTRY_BUTTON); + + if (new_example == NULL) { + SDL_Log("Couldn't insert entry in example tray: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + return; + } + + SDL_SetTrayEntryCallback(new_example, print_entry, NULL); + + /* ---------- */ + + new_ctrl_remove = SDL_InsertTrayEntryAt(submenu, -1, "Remove", SDL_TRAYENTRY_BUTTON); + + if (new_ctrl_remove == NULL) { + SDL_Log("Couldn't insert new_ctrl_remove: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + SDL_RemoveTrayEntry(new_example); + return; + } + + SDL_SetTrayEntryCallback(new_ctrl_remove, remove_entry, new_example); + + /* ---------- */ + + new_ctrl_enabled = SDL_InsertTrayEntryAt(submenu, -1, "Enable", SDL_TRAYENTRY_BUTTON); + + if (new_ctrl_enabled == NULL) { + SDL_Log("Couldn't insert new_ctrl_enabled: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + SDL_RemoveTrayEntry(new_example); + return; + } + + SDL_SetTrayEntryCallback(new_ctrl_enabled, set_entry_enabled, new_example); + + /* ---------- */ + + new_ctrl_disabled = SDL_InsertTrayEntryAt(submenu, -1, "Disable", SDL_TRAYENTRY_BUTTON); + + if (new_ctrl_disabled == NULL) { + SDL_Log("Couldn't insert new_ctrl_disabled: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + SDL_RemoveTrayEntry(new_example); + return; + } + + SDL_SetTrayEntryCallback(new_ctrl_disabled, set_entry_disabled, new_example); +} + +static void SDLCALL append_checkbox_to(void *ptr, SDL_TrayEntry *entry) +{ + SDL_TrayMenu *menu = (SDL_TrayMenu *) ptr; + SDL_TrayMenu *submenu; + SDL_TrayEntry *new_ctrl; + SDL_TrayEntry *new_ctrl_remove; + SDL_TrayEntry *new_ctrl_enabled; + SDL_TrayEntry *new_ctrl_disabled; + SDL_TrayEntry *new_ctrl_checked; + SDL_TrayEntry *new_ctrl_unchecked; + SDL_TrayEntry *new_example; + + new_ctrl = SDL_InsertTrayEntryAt(SDL_GetTrayEntryParent(entry), -1, "New checkbox", SDL_TRAYENTRY_SUBMENU); + + if (!new_ctrl) { + SDL_Log("Couldn't insert entry in control tray: %s\n", SDL_GetError()); + return; + } + + /* ---------- */ + + submenu = SDL_CreateTraySubmenu(new_ctrl); + + if (!new_ctrl) { + SDL_Log("Couldn't create control tray entry submenu: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + return; + } + + /* ---------- */ + + new_example = SDL_InsertTrayEntryAt(menu, -1, "New checkbox", SDL_TRAYENTRY_CHECKBOX); + + if (new_example == NULL) { + SDL_Log("Couldn't insert entry in example tray: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + return; + } + + SDL_SetTrayEntryCallback(new_example, print_entry, NULL); + + /* ---------- */ + + new_ctrl_remove = SDL_InsertTrayEntryAt(submenu, -1, "Remove", SDL_TRAYENTRY_BUTTON); + + if (new_ctrl_remove == NULL) { + SDL_Log("Couldn't insert new_ctrl_remove: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + SDL_RemoveTrayEntry(new_example); + return; + } + + SDL_SetTrayEntryCallback(new_ctrl_remove, remove_entry, new_example); + + /* ---------- */ + + new_ctrl_enabled = SDL_InsertTrayEntryAt(submenu, -1, "Enable", SDL_TRAYENTRY_BUTTON); + + if (new_ctrl_enabled == NULL) { + SDL_Log("Couldn't insert new_ctrl_enabled: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + SDL_RemoveTrayEntry(new_example); + return; + } + + SDL_SetTrayEntryCallback(new_ctrl_enabled, set_entry_enabled, new_example); + + /* ---------- */ + + new_ctrl_disabled = SDL_InsertTrayEntryAt(submenu, -1, "Disable", SDL_TRAYENTRY_BUTTON); + + if (new_ctrl_disabled == NULL) { + SDL_Log("Couldn't insert new_ctrl_disabled: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + SDL_RemoveTrayEntry(new_example); + return; + } + + SDL_SetTrayEntryCallback(new_ctrl_disabled, set_entry_disabled, new_example); + + /* ---------- */ + + new_ctrl_checked = SDL_InsertTrayEntryAt(submenu, -1, "Check", SDL_TRAYENTRY_BUTTON); + + if (new_ctrl_checked == NULL) { + SDL_Log("Couldn't insert new_ctrl_checked: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + SDL_RemoveTrayEntry(new_example); + return; + } + + SDL_SetTrayEntryCallback(new_ctrl_checked, set_entry_checked, new_example); + + /* ---------- */ + + new_ctrl_unchecked = SDL_InsertTrayEntryAt(submenu, -1, "Uncheck", SDL_TRAYENTRY_BUTTON); + + if (new_ctrl_unchecked == NULL) { + SDL_Log("Couldn't insert new_ctrl_unchecked: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + SDL_RemoveTrayEntry(new_example); + return; + } + + SDL_SetTrayEntryCallback(new_ctrl_unchecked, set_entry_unchecked, new_example); +} + +static void SDLCALL append_separator_to(void *ptr, SDL_TrayEntry *entry) +{ + SDL_TrayMenu *menu = (SDL_TrayMenu *) ptr; + SDL_TrayMenu *submenu; + SDL_TrayEntry *new_ctrl; + SDL_TrayEntry *new_ctrl_remove; + SDL_TrayEntry *new_example; + + new_ctrl = SDL_InsertTrayEntryAt(SDL_GetTrayEntryParent(entry), -1, "[Separator]", SDL_TRAYENTRY_SUBMENU); + + if (!new_ctrl) { + SDL_Log("Couldn't insert entry in control tray: %s\n", SDL_GetError()); + return; + } + + /* ---------- */ + + submenu = SDL_CreateTraySubmenu(new_ctrl); + + if (!new_ctrl) { + SDL_Log("Couldn't create control tray entry submenu: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + return; + } + + /* ---------- */ + + new_example = SDL_InsertTrayEntryAt(menu, -1, NULL, SDL_TRAYENTRY_BUTTON); + + if (new_example == NULL) { + SDL_Log("Couldn't insert separator in example tray: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + return; + } + + /* ---------- */ + + new_ctrl_remove = SDL_InsertTrayEntryAt(submenu, -1, "Remove", SDL_TRAYENTRY_BUTTON); + + if (new_ctrl_remove == NULL) { + SDL_Log("Couldn't insert new_ctrl_remove: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + SDL_RemoveTrayEntry(new_example); + return; + } + + SDL_SetTrayEntryCallback(new_ctrl_remove, remove_entry, new_example); +} + +static void SDLCALL append_submenu_to(void *ptr, SDL_TrayEntry *entry) +{ + SDL_TrayMenu *menu = (SDL_TrayMenu *) ptr; + SDL_TrayMenu *submenu; + SDL_TrayMenu *entry_submenu; + SDL_TrayEntry *new_ctrl; + SDL_TrayEntry *new_ctrl_remove; + SDL_TrayEntry *new_ctrl_enabled; + SDL_TrayEntry *new_ctrl_disabled; + SDL_TrayEntry *new_example; + + new_ctrl = SDL_InsertTrayEntryAt(SDL_GetTrayEntryParent(entry), -1, "New submenu", SDL_TRAYENTRY_SUBMENU); + + if (!new_ctrl) { + SDL_Log("Couldn't insert entry in control tray: %s\n", SDL_GetError()); + return; + } + + /* ---------- */ + + submenu = SDL_CreateTraySubmenu(new_ctrl); + + if (!new_ctrl) { + SDL_Log("Couldn't create control tray entry submenu: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + return; + } + + /* ---------- */ + + new_example = SDL_InsertTrayEntryAt(menu, -1, "New submenu", SDL_TRAYENTRY_SUBMENU); + + if (new_example == NULL) { + SDL_Log("Couldn't insert entry in example tray: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + return; + } + + SDL_SetTrayEntryCallback(new_example, print_entry, NULL); + + /* ---------- */ + + entry_submenu = SDL_CreateTraySubmenu(new_example); + + if (entry_submenu == NULL) { + SDL_Log("Couldn't create new entry submenu: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + SDL_RemoveTrayEntry(new_example); + return; + } + + /* ---------- */ + + new_ctrl_remove = SDL_InsertTrayEntryAt(submenu, -1, "Remove", SDL_TRAYENTRY_BUTTON); + + if (new_ctrl_remove == NULL) { + SDL_Log("Couldn't insert new_ctrl_remove: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + SDL_RemoveTrayEntry(new_example); + return; + } + + SDL_SetTrayEntryCallback(new_ctrl_remove, remove_entry, new_example); + + /* ---------- */ + + new_ctrl_enabled = SDL_InsertTrayEntryAt(submenu, -1, "Enable", SDL_TRAYENTRY_BUTTON); + + if (new_ctrl_enabled == NULL) { + SDL_Log("Couldn't insert new_ctrl_enabled: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + SDL_RemoveTrayEntry(new_example); + return; + } + + SDL_SetTrayEntryCallback(new_ctrl_enabled, set_entry_enabled, new_example); + + /* ---------- */ + + new_ctrl_disabled = SDL_InsertTrayEntryAt(submenu, -1, "Disable", SDL_TRAYENTRY_BUTTON); + + if (new_ctrl_disabled == NULL) { + SDL_Log("Couldn't insert new_ctrl_disabled: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + SDL_RemoveTrayEntry(new_example); + return; + } + + SDL_SetTrayEntryCallback(new_ctrl_disabled, set_entry_disabled, new_example); + + /* ---------- */ + + SDL_InsertTrayEntryAt(submenu, -1, NULL, 0); + + /* ---------- */ + + SDL_TrayEntry *entry_newbtn = SDL_InsertTrayEntryAt(submenu, -1, "Create button", SDL_TRAYENTRY_BUTTON); + + if (entry_newbtn == NULL) { + SDL_Log("Couldn't insert entry_newbtn: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + SDL_RemoveTrayEntry(new_example); + return; + } + + SDL_SetTrayEntryCallback(entry_newbtn, append_button_to, entry_submenu); + + /* ---------- */ + + SDL_TrayEntry *entry_newchk = SDL_InsertTrayEntryAt(submenu, -1, "Create checkbox", SDL_TRAYENTRY_BUTTON); + + if (entry_newchk == NULL) { + SDL_Log("Couldn't insert entry_newchk: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + SDL_RemoveTrayEntry(new_example); + return; + } + + SDL_SetTrayEntryCallback(entry_newchk, append_checkbox_to, entry_submenu); + + /* ---------- */ + + SDL_TrayEntry *entry_newsub = SDL_InsertTrayEntryAt(submenu, -1, "Create submenu", SDL_TRAYENTRY_BUTTON); + + if (entry_newsub == NULL) { + SDL_Log("Couldn't insert entry_newsub: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + SDL_RemoveTrayEntry(new_example); + return; + } + + SDL_SetTrayEntryCallback(entry_newsub, append_submenu_to, entry_submenu); + + /* ---------- */ + + SDL_TrayEntry *entry_newsep = SDL_InsertTrayEntryAt(submenu, -1, "Create separator", SDL_TRAYENTRY_BUTTON); + + if (entry_newsep == NULL) { + SDL_Log("Couldn't insert entry_newsep: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + SDL_RemoveTrayEntry(new_example); + return; + } + + SDL_SetTrayEntryCallback(entry_newsep, append_separator_to, entry_submenu); + + /* ---------- */ + + SDL_InsertTrayEntryAt(submenu, -1, NULL, 0); +} + +int main(int argc, char **argv) +{ + SDLTest_CommonState *state; + int i; + + /* Initialize test framework */ + state = SDLTest_CommonCreateState(argv, 0); + if (state == NULL) { + return 1; + } + + /* Parse commandline */ + for (i = 1; i < argc;) { + int consumed; + + consumed = SDLTest_CommonArg(state, i); + + if (consumed <= 0) { + static const char *options[] = { NULL }; + SDLTest_CommonLogUsage(state, argv[0], options); + return 1; + } + + i += consumed; + } + + if (!SDL_Init(SDL_INIT_VIDEO)) { + SDL_Log("SDL_Init failed (%s)", SDL_GetError()); + return 1; + } + + /* TODO: Resource paths? */ + SDL_Surface *icon = SDL_LoadBMP("../test/sdl-test_round.bmp"); + + if (!icon) { + SDL_Log("Couldn't load icon 1, proceeding without: %s", SDL_GetError()); + } + + SDL_Surface *icon2 = SDL_LoadBMP("../test/speaker.bmp"); + + if (!icon2) { + SDL_Log("Couldn't load icon 2, proceeding without: %s", SDL_GetError()); + } + + SDL_Tray *tray = SDL_CreateTray(icon, "SDL Tray control menu"); + + if (!tray) { + SDL_Log("Couldn't create control tray: %s", SDL_GetError()); + goto quit; + } + + SDL_Tray *tray2 = SDL_CreateTray(icon2, "SDL Tray example"); + + if (!tray2) { + SDL_Log("Couldn't create example tray: %s", SDL_GetError()); + goto clean_tray1; + } + + SDL_DestroySurface(icon); + SDL_DestroySurface(icon2); + +#define CHECK(name) \ + if (!name) { \ + SDL_Log("Couldn't create " #name ": %s", SDL_GetError()); \ + goto clean_all; \ + } + + SDL_TrayMenu *menu = SDL_CreateTrayMenu(tray); + CHECK(menu); + + SDL_TrayMenu *menu2 = SDL_CreateTrayMenu(tray2); + CHECK(menu2); + + SDL_TrayEntry *entry_quit = SDL_InsertTrayEntryAt(menu, -1, "Quit", SDL_TRAYENTRY_BUTTON); + CHECK(entry_quit); + + SDL_SetTrayEntryCallback(entry_quit, tray_quit, NULL); + + SDL_InsertTrayEntryAt(menu, -1, NULL, 0); + + SDL_TrayEntry *entry_icon = SDL_InsertTrayEntryAt(menu, -1, "Change icon", SDL_TRAYENTRY_BUTTON); + CHECK(entry_icon); + + SDL_SetTrayEntryCallback(entry_icon, change_icon, tray2); + + SDL_InsertTrayEntryAt(menu, -1, NULL, 0); + + SDL_TrayEntry *entry_newbtn = SDL_InsertTrayEntryAt(menu, -1, "Create button", SDL_TRAYENTRY_BUTTON); + CHECK(entry_newbtn); + + SDL_SetTrayEntryCallback(entry_newbtn, append_button_to, menu2); + + SDL_TrayEntry *entry_newchk = SDL_InsertTrayEntryAt(menu, -1, "Create checkbox", SDL_TRAYENTRY_BUTTON); + CHECK(entry_newchk); + + SDL_SetTrayEntryCallback(entry_newchk, append_checkbox_to, menu2); + + SDL_TrayEntry *entry_newsub = SDL_InsertTrayEntryAt(menu, -1, "Create submenu", SDL_TRAYENTRY_BUTTON); + CHECK(entry_newsub); + + SDL_SetTrayEntryCallback(entry_newsub, append_submenu_to, menu2); + + SDL_TrayEntry *entry_newsep = SDL_InsertTrayEntryAt(menu, -1, "Create separator", SDL_TRAYENTRY_BUTTON); + CHECK(entry_newsep); + + SDL_SetTrayEntryCallback(entry_newsep, append_separator_to, menu2); + + SDL_InsertTrayEntryAt(menu, -1, NULL, 0); + + SDL_Event e; + while (SDL_WaitEvent(&e)) { + if (e.type == SDL_EVENT_QUIT) { + break; + } + } + +clean_all: + SDL_DestroyTray(tray2); + +clean_tray1: + SDL_DestroyTray(tray); + +quit: + SDL_Quit(); + SDLTest_CommonDestroyState(state); + + return 0; +}