Add system tray support (#10873)

This commit is contained in:
Semphriss 2024-12-24 13:36:39 -05:00 committed by GitHub
parent 17a029502a
commit 01b9b0edb7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 3125 additions and 1 deletions

View file

@ -78,6 +78,7 @@ LOCAL_SRC_FILES := \
$(wildcard $(LOCAL_PATH)/src/time/unix/*.c) \ $(wildcard $(LOCAL_PATH)/src/time/unix/*.c) \
$(wildcard $(LOCAL_PATH)/src/timer/*.c) \ $(wildcard $(LOCAL_PATH)/src/timer/*.c) \
$(wildcard $(LOCAL_PATH)/src/timer/unix/*.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/*.c) \
$(wildcard $(LOCAL_PATH)/src/video/android/*.c) \ $(wildcard $(LOCAL_PATH)/src/video/android/*.c) \
$(wildcard $(LOCAL_PATH)/src/video/yuv2rgb/*.c)) $(wildcard $(LOCAL_PATH)/src/video/yuv2rgb/*.c))

View file

@ -1567,6 +1567,9 @@ elseif(UNIX AND NOT APPLE AND NOT RISCOS AND NOT HAIKU)
CheckVivante() CheckVivante()
CheckVulkan() CheckVulkan()
CheckQNXScreen() CheckQNXScreen()
sdl_glob_sources("${SDL3_SOURCE_DIR}/src/tray/unix/*.c")
set(HAVE_SDL_TRAY TRUE)
endif() endif()
if(UNIX) if(UNIX)
@ -2075,6 +2078,9 @@ elseif(WINDOWS)
set(HAVE_RENDER_VULKAN TRUE) set(HAVE_RENDER_VULKAN TRUE)
endif() endif()
endif() endif()
sdl_glob_sources("${SDL3_SOURCE_DIR}/src/tray/windows/*.c")
set(HAVE_SDL_TRAY TRUE)
endif() endif()
if(SDL_HIDAPI) if(SDL_HIDAPI)
@ -2351,6 +2357,11 @@ elseif(APPLE)
endif() endif()
endif() endif()
endif() endif()
if(MACOS)
sdl_glob_sources("${SDL3_SOURCE_DIR}/src/tray/cocoa/*.m")
set(HAVE_SDL_TRAY TRUE)
endif()
endif() endif()
# Minimum version for $<LINK_LIBRARY:feature,library-list> # Minimum version for $<LINK_LIBRARY:feature,library-list>
@ -2973,6 +2984,8 @@ if(SDL_VIDEO)
endif() endif()
endif() endif()
sdl_glob_sources(${SDL3_SOURCE_DIR}/src/tray/*.c)
if(SDL_GPU) if(SDL_GPU)
if(HAVE_D3D11_H) if(HAVE_D3D11_H)
sdl_glob_sources("${SDL3_SOURCE_DIR}/src/gpu/d3d11/*.c") sdl_glob_sources("${SDL3_SOURCE_DIR}/src/gpu/d3d11/*.c")
@ -3055,6 +3068,10 @@ if(NOT HAVE_SDL_PROCESS)
set(SDL_PROCESS_DUMMY 1) set(SDL_PROCESS_DUMMY 1)
sdl_glob_sources(${SDL3_SOURCE_DIR}/src/process/dummy/*.c) sdl_glob_sources(${SDL3_SOURCE_DIR}/src/process/dummy/*.c)
endif() 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) if(NOT HAVE_CAMERA)
set(SDL_CAMERA_DRIVER_DUMMY 1) set(SDL_CAMERA_DRIVER_DUMMY 1)
sdl_glob_sources("${SDL3_SOURCE_DIR}/src/camera/dummy/*.c") sdl_glob_sources("${SDL3_SOURCE_DIR}/src/camera/dummy/*.c")

View file

@ -592,6 +592,7 @@
<ClInclude Include="..\..\src\video\SDL_vulkan_internal.h" /> <ClInclude Include="..\..\src\video\SDL_vulkan_internal.h" />
<ClInclude Include="..\..\src\video\SDL_yuv_c.h" /> <ClInclude Include="..\..\src\video\SDL_yuv_c.h" />
<ClInclude Include="..\..\src\video\windows\SDL_msctf.h" /> <ClInclude Include="..\..\src\video\windows\SDL_msctf.h" />
<ClInclude Include="..\..\src\video\windows\SDL_surface_utils.h" />
<ClInclude Include="..\..\src\video\windows\SDL_windowsclipboard.h" /> <ClInclude Include="..\..\src\video\windows\SDL_windowsclipboard.h" />
<ClInclude Include="..\..\src\video\windows\SDL_windowsevents.h" /> <ClInclude Include="..\..\src\video\windows\SDL_windowsevents.h" />
<ClInclude Include="..\..\src\video\windows\SDL_windowsframebuffer.h" /> <ClInclude Include="..\..\src\video\windows\SDL_windowsframebuffer.h" />
@ -827,6 +828,16 @@
<ClCompile Include="..\..\src\timer\windows\SDL_systimer.c" /> <ClCompile Include="..\..\src\timer\windows\SDL_systimer.c" />
<ClCompile Include="..\..\src\time\SDL_time.c" /> <ClCompile Include="..\..\src\time\SDL_time.c" />
<ClCompile Include="..\..\src\time\windows\SDL_systime.c" /> <ClCompile Include="..\..\src\time\windows\SDL_systime.c" />
<ClCompile Include="..\..\src\tray\dummy\SDL_tray.c">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Gaming.Desktop.x64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Gaming.Desktop.x64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\src\tray\windows\SDL_tray.c">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Gaming.Xbox.Scarlett.x64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Gaming.Xbox.Scarlett.x64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Gaming.Xbox.XboxOne.x64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Gaming.Xbox.XboxOne.x64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\src\video\dummy\SDL_nullevents.c" /> <ClCompile Include="..\..\src\video\dummy\SDL_nullevents.c" />
<ClCompile Include="..\..\src\video\dummy\SDL_nullframebuffer.c" /> <ClCompile Include="..\..\src\video\dummy\SDL_nullframebuffer.c" />
<ClCompile Include="..\..\src\video\dummy\SDL_nullvideo.c" /> <ClCompile Include="..\..\src\video\dummy\SDL_nullvideo.c" />
@ -855,6 +866,7 @@
<ClCompile Include="..\..\src\video\SDL_video_unsupported.c" /> <ClCompile Include="..\..\src\video\SDL_video_unsupported.c" />
<ClCompile Include="..\..\src\video\SDL_vulkan_utils.c" /> <ClCompile Include="..\..\src\video\SDL_vulkan_utils.c" />
<ClCompile Include="..\..\src\video\SDL_yuv.c" /> <ClCompile Include="..\..\src\video\SDL_yuv.c" />
<ClCompile Include="..\..\src\video\windows\SDL_surface_utils.c" />
<ClCompile Include="..\..\src\video\windows\SDL_windowsclipboard.c" /> <ClCompile Include="..\..\src\video\windows\SDL_windowsclipboard.c" />
<ClCompile Include="..\..\src\video\windows\SDL_windowsevents.c" /> <ClCompile Include="..\..\src\video\windows\SDL_windowsevents.c" />
<ClCompile Include="..\..\src\video\windows\SDL_windowsframebuffer.c" /> <ClCompile Include="..\..\src\video\windows\SDL_windowsframebuffer.c" />
@ -889,4 +901,4 @@
<ImportGroup Label="ExtensionTargets"> <ImportGroup Label="ExtensionTargets">
<Import Project="$(VCTargetsPath)\BuildCustomizations\masm.targets" /> <Import Project="$(VCTargetsPath)\BuildCustomizations\masm.targets" />
</ImportGroup> </ImportGroup>
</Project> </Project>

View file

@ -181,6 +181,7 @@
<ClCompile Include="..\..\src\video\SDL_video_unsupported.c" /> <ClCompile Include="..\..\src\video\SDL_video_unsupported.c" />
<ClCompile Include="..\..\src\video\SDL_vulkan_utils.c" /> <ClCompile Include="..\..\src\video\SDL_vulkan_utils.c" />
<ClCompile Include="..\..\src\video\SDL_yuv.c" /> <ClCompile Include="..\..\src\video\SDL_yuv.c" />
<ClCompile Include="..\..\src\video\windows\SDL_surface_utils.c" />
<ClCompile Include="..\..\src\video\windows\SDL_windowsclipboard.c" /> <ClCompile Include="..\..\src\video\windows\SDL_windowsclipboard.c" />
<ClCompile Include="..\..\src\video\windows\SDL_windowsevents.c" /> <ClCompile Include="..\..\src\video\windows\SDL_windowsevents.c" />
<ClCompile Include="..\..\src\video\windows\SDL_windowsframebuffer.c" /> <ClCompile Include="..\..\src\video\windows\SDL_windowsframebuffer.c" />
@ -217,6 +218,8 @@
<ClCompile Include="..\..\src\storage\SDL_storage.c" /> <ClCompile Include="..\..\src\storage\SDL_storage.c" />
<ClCompile Include="..\..\src\time\SDL_time.c" /> <ClCompile Include="..\..\src\time\SDL_time.c" />
<ClCompile Include="..\..\src\time\windows\SDL_systime.c" /> <ClCompile Include="..\..\src\time\windows\SDL_systime.c" />
<ClCompile Include="..\..\src\tray\dummy\SDL_tray.c" />
<ClCompile Include="..\..\src\tray\windows\SDL_tray.c" />
<ClCompile Include="..\..\src\video\yuv2rgb\yuv_rgb_lsx.c" /> <ClCompile Include="..\..\src\video\yuv2rgb\yuv_rgb_lsx.c" />
<ClCompile Include="..\..\src\video\yuv2rgb\yuv_rgb_sse.c" /> <ClCompile Include="..\..\src\video\yuv2rgb\yuv_rgb_sse.c" />
<ClCompile Include="..\..\src\video\yuv2rgb\yuv_rgb_std.c" /> <ClCompile Include="..\..\src\video\yuv2rgb\yuv_rgb_std.c" />
@ -435,6 +438,7 @@
<ClInclude Include="..\..\src\video\SDL_vulkan_internal.h" /> <ClInclude Include="..\..\src\video\SDL_vulkan_internal.h" />
<ClInclude Include="..\..\src\video\SDL_yuv_c.h" /> <ClInclude Include="..\..\src\video\SDL_yuv_c.h" />
<ClInclude Include="..\..\src\video\windows\SDL_msctf.h" /> <ClInclude Include="..\..\src\video\windows\SDL_msctf.h" />
<ClInclude Include="..\..\src\video\windows\SDL_surface_utils.h" />
<ClInclude Include="..\..\src\video\windows\SDL_windowsclipboard.h" /> <ClInclude Include="..\..\src\video\windows\SDL_windowsclipboard.h" />
<ClInclude Include="..\..\src\video\windows\SDL_windowsevents.h" /> <ClInclude Include="..\..\src\video\windows\SDL_windowsevents.h" />
<ClInclude Include="..\..\src\video\windows\SDL_windowsframebuffer.h" /> <ClInclude Include="..\..\src\video\windows\SDL_windowsframebuffer.h" />

View file

@ -489,6 +489,7 @@
<ClInclude Include="..\..\src\video\SDL_vulkan_internal.h" /> <ClInclude Include="..\..\src\video\SDL_vulkan_internal.h" />
<ClInclude Include="..\..\src\video\SDL_yuv_c.h" /> <ClInclude Include="..\..\src\video\SDL_yuv_c.h" />
<ClInclude Include="..\..\src\video\windows\SDL_msctf.h" /> <ClInclude Include="..\..\src\video\windows\SDL_msctf.h" />
<ClInclude Include="..\..\src\video\windows\SDL_surface_utils.h" />
<ClInclude Include="..\..\src\video\windows\SDL_windowsclipboard.h" /> <ClInclude Include="..\..\src\video\windows\SDL_windowsclipboard.h" />
<ClInclude Include="..\..\src\video\windows\SDL_windowsevents.h" /> <ClInclude Include="..\..\src\video\windows\SDL_windowsevents.h" />
<ClInclude Include="..\..\src\video\windows\SDL_windowsframebuffer.h" /> <ClInclude Include="..\..\src\video\windows\SDL_windowsframebuffer.h" />
@ -671,6 +672,7 @@
<ClCompile Include="..\..\src\timer\windows\SDL_systimer.c" /> <ClCompile Include="..\..\src\timer\windows\SDL_systimer.c" />
<ClCompile Include="..\..\src\time\SDL_time.c" /> <ClCompile Include="..\..\src\time\SDL_time.c" />
<ClCompile Include="..\..\src\time\windows\SDL_systime.c" /> <ClCompile Include="..\..\src\time\windows\SDL_systime.c" />
<ClCompile Include="..\..\src\tray\windows\SDL_tray.c" />
<ClCompile Include="..\..\src\video\dummy\SDL_nullevents.c" /> <ClCompile Include="..\..\src\video\dummy\SDL_nullevents.c" />
<ClCompile Include="..\..\src\video\dummy\SDL_nullframebuffer.c" /> <ClCompile Include="..\..\src\video\dummy\SDL_nullframebuffer.c" />
<ClCompile Include="..\..\src\video\dummy\SDL_nullvideo.c" /> <ClCompile Include="..\..\src\video\dummy\SDL_nullvideo.c" />
@ -701,6 +703,7 @@
<ClCompile Include="..\..\src\video\SDL_video_unsupported.c" /> <ClCompile Include="..\..\src\video\SDL_video_unsupported.c" />
<ClCompile Include="..\..\src\video\SDL_vulkan_utils.c" /> <ClCompile Include="..\..\src\video\SDL_vulkan_utils.c" />
<ClCompile Include="..\..\src\video\SDL_yuv.c" /> <ClCompile Include="..\..\src\video\SDL_yuv.c" />
<ClCompile Include="..\..\src\video\windows\SDL_surface_utils.c" />
<ClCompile Include="..\..\src\video\windows\SDL_windowsclipboard.c" /> <ClCompile Include="..\..\src\video\windows\SDL_windowsclipboard.c" />
<ClCompile Include="..\..\src\video\windows\SDL_windowsevents.c" /> <ClCompile Include="..\..\src\video\windows\SDL_windowsevents.c" />
<ClCompile Include="..\..\src\video\windows\SDL_windowsframebuffer.c" /> <ClCompile Include="..\..\src\video\windows\SDL_windowsframebuffer.c" />

View file

@ -690,6 +690,9 @@
<ClInclude Include="..\..\src\video\yuv2rgb\yuv_rgb_std_func.h"> <ClInclude Include="..\..\src\video\yuv2rgb\yuv_rgb_std_func.h">
<Filter>video\yuv2rgb</Filter> <Filter>video\yuv2rgb</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="..\..\src\video\windows\SDL_surface_utils.h">
<Filter>video\windows</Filter>
</ClInclude>
<ClInclude Include="..\..\src\video\windows\SDL_windowsclipboard.h"> <ClInclude Include="..\..\src\video\windows\SDL_windowsclipboard.h">
<Filter>video\windows</Filter> <Filter>video\windows</Filter>
</ClInclude> </ClInclude>
@ -1229,6 +1232,9 @@
<ClCompile Include="..\..\src\time\windows\SDL_systime.c"> <ClCompile Include="..\..\src\time\windows\SDL_systime.c">
<Filter>time\windows</Filter> <Filter>time\windows</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="..\..\src\tray\windows\SDL_tray.c">
<Filter>video</Filter>
</ClCompile>
<ClCompile Include="..\..\src\video\SDL_RLEaccel.c"> <ClCompile Include="..\..\src\video\SDL_RLEaccel.c">
<Filter>video</Filter> <Filter>video</Filter>
</ClCompile> </ClCompile>
@ -1301,6 +1307,9 @@
<ClCompile Include="..\..\src\video\dummy\SDL_nullvideo.c"> <ClCompile Include="..\..\src\video\dummy\SDL_nullvideo.c">
<Filter>video\dummy</Filter> <Filter>video\dummy</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="..\..\src\video\windows\SDL_surface_utils.c">
<Filter>video\windows</Filter>
</ClCompile>
<ClCompile Include="..\..\src\video\windows\SDL_windowsclipboard.c"> <ClCompile Include="..\..\src\video\windows\SDL_windowsclipboard.c">
<Filter>video\windows</Filter> <Filter>video\windows</Filter>
</ClCompile> </ClCompile>

View file

@ -81,6 +81,7 @@
#include <SDL3/SDL_thread.h> #include <SDL3/SDL_thread.h>
#include <SDL3/SDL_time.h> #include <SDL3/SDL_time.h>
#include <SDL3/SDL_timer.h> #include <SDL3/SDL_timer.h>
#include <SDL3/SDL_tray.h>
#include <SDL3/SDL_touch.h> #include <SDL3/SDL_touch.h>
#include <SDL3/SDL_version.h> #include <SDL3/SDL_version.h>
#include <SDL3/SDL_video.h> #include <SDL3/SDL_video.h>

431
include/SDL3/SDL_tray.h Normal file
View file

@ -0,0 +1,431 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2024 Sam Lantinga <slouken@libsdl.org>
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 <SDL3/SDL_error.h>
#include <SDL3/SDL_video.h>
#include <SDL3/SDL_begin_code.h>
/* 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 <SDL3/SDL_close_code.h>
#endif /* SDL_tray_h_ */

View file

@ -1208,6 +1208,27 @@ SDL3_0.0.0 {
SDL_RenderTextureAffine; SDL_RenderTextureAffine;
SDL_WaitAndAcquireGPUSwapchainTexture; SDL_WaitAndAcquireGPUSwapchainTexture;
SDL_RenderDebugTextFormat; 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) # extra symbols go here (don't modify this line)
local: *; local: *;
}; };

View file

@ -1233,3 +1233,24 @@
#define SDL_RenderTextureAffine SDL_RenderTextureAffine_REAL #define SDL_RenderTextureAffine SDL_RenderTextureAffine_REAL
#define SDL_WaitAndAcquireGPUSwapchainTexture SDL_WaitAndAcquireGPUSwapchainTexture_REAL #define SDL_WaitAndAcquireGPUSwapchainTexture SDL_WaitAndAcquireGPUSwapchainTexture_REAL
#define SDL_RenderDebugTextFormat SDL_RenderDebugTextFormat_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

View file

@ -1241,3 +1241,24 @@ SDL_DYNAPI_PROC(bool,SDL_WaitAndAcquireGPUSwapchainTexture,(SDL_GPUCommandBuffer
#ifndef SDL_DYNAPI_PROC_NO_VARARGS #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) 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 #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)

458
src/tray/cocoa/SDL_tray.m Normal file
View file

@ -0,0 +1,458 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2024 Sam Lantinga <slouken@libsdl.org>
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 <Cocoa/Cocoa.h>
#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 <NSApplicationDelegate>
- (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);
}

139
src/tray/dummy/SDL_tray.c Normal file
View file

@ -0,0 +1,139 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2024 Sam Lantinga <slouken@libsdl.org>
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();
}

664
src/tray/unix/SDL_tray.c Normal file
View file

@ -0,0 +1,664 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2024 Sam Lantinga <slouken@libsdl.org>
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 <dlfcn.h>
/* getpid() */
#include <unistd.h>
/* 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);
}

589
src/tray/windows/SDL_tray.c Normal file
View file

@ -0,0 +1,589 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2024 Sam Lantinga <slouken@libsdl.org>
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 <windows.h>
#include <windowsx.h>
#include <shellapi.h>
#include <stdlib.h>
#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);
}

View file

@ -0,0 +1,95 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2024 Sam Lantinga <slouken@libsdl.org>
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;
}

View file

@ -0,0 +1,38 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2024 Sam Lantinga <slouken@libsdl.org>
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 <windows.h>
#ifdef __cplusplus
extern "C" {
#endif
extern HICON CreateIconFromSurface(SDL_Surface *surface);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -407,6 +407,7 @@ add_sdl_test_executable(testdialog SOURCES testdialog.c)
add_sdl_test_executable(testtime SOURCES testtime.c) add_sdl_test_executable(testtime SOURCES testtime.c)
add_sdl_test_executable(testmanymouse SOURCES testmanymouse.c) add_sdl_test_executable(testmanymouse SOURCES testmanymouse.c)
add_sdl_test_executable(testmodal SOURCES testmodal.c) add_sdl_test_executable(testmodal SOURCES testmodal.c)
add_sdl_test_executable(testtray SOURCES testtray.c)
add_sdl_test_executable(testprocess add_sdl_test_executable(testprocess

BIN
test/sdl-test_round.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

599
test/testtray.c Normal file
View file

@ -0,0 +1,599 @@
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <SDL3/SDL_test.h>
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;
}