From ed3564f400d900facc16e737728b491bf2ca6596 Mon Sep 17 00:00:00 2001 From: Tony Di Croce Date: Tue, 22 Mar 2022 17:03:20 -0400 Subject: [PATCH 01/14] Drive trays message queue from existing main loop. --- tray/include/core/linux/tray.hpp | 1 + tray/include/core/traybase.hpp | 1 + tray/include/core/windows/tray.hpp | 1 + tray/src/core/linux/tray.cpp | 5 +++++ tray/src/core/windows/tray.cpp | 10 ++++++++++ 5 files changed, 18 insertions(+) diff --git a/tray/include/core/linux/tray.hpp b/tray/include/core/linux/tray.hpp index 6436ce6..e10bc97 100755 --- a/tray/include/core/linux/tray.hpp +++ b/tray/include/core/linux/tray.hpp @@ -22,6 +22,7 @@ namespace Tray void run() override; void exit() override; void update() override; + void pump() override; }; } // namespace Tray #endif \ No newline at end of file diff --git a/tray/include/core/traybase.hpp b/tray/include/core/traybase.hpp index 8583afa..247bed6 100755 --- a/tray/include/core/traybase.hpp +++ b/tray/include/core/traybase.hpp @@ -37,6 +37,7 @@ namespace Tray virtual void run() = 0; virtual void exit() = 0; virtual void update() = 0; + virtual void pump() = 0; std::vector> getEntries(); }; } // namespace Tray \ No newline at end of file diff --git a/tray/include/core/windows/tray.hpp b/tray/include/core/windows/tray.hpp index ab59499..c2a6eb5 100755 --- a/tray/include/core/windows/tray.hpp +++ b/tray/include/core/windows/tray.hpp @@ -30,6 +30,7 @@ namespace Tray void run() override; void exit() override; void update() override; + void pump() override; }; } // namespace Tray #endif \ No newline at end of file diff --git a/tray/src/core/linux/tray.cpp b/tray/src/core/linux/tray.cpp index 975c8d6..24b058d 100755 --- a/tray/src/core/linux/tray.cpp +++ b/tray/src/core/linux/tray.cpp @@ -168,4 +168,9 @@ void Tray::Tray::run() } } +void Tray::Tray::pump() +{ + gtk_main_iteration_do(false); +} + #endif \ No newline at end of file diff --git a/tray/src/core/windows/tray.cpp b/tray/src/core/windows/tray.cpp index 3811108..9a54ceb 100755 --- a/tray/src/core/windows/tray.cpp +++ b/tray/src/core/windows/tray.cpp @@ -219,4 +219,14 @@ void Tray::Tray::run() } } +void Tray::Tray::pump() +{ + static MSG msg; + if(PeekMessage(&msg, hwnd, 0, 0, PM_REMOVE)) + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } +} + #endif \ No newline at end of file From ad75b76058296a647327b81e87a61887c3080fdb Mon Sep 17 00:00:00 2001 From: Tony Di Croce Date: Wed, 15 Jun 2022 15:10:04 -0400 Subject: [PATCH 02/14] port to ayatana-appindicator --- CMakeLists.txt | 4 ++-- tray/include/core/linux/tray.hpp | 4 ++-- tray/src/core/linux/tray.cpp | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ae4cb42..02d7942 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,7 +12,7 @@ add_library(tray STATIC ${src}) if (UNIX) find_package(PkgConfig REQUIRED) pkg_check_modules(GTK3 REQUIRED gtk+-3.0) - pkg_check_modules(APPINDICATOR REQUIRED appindicator3-0.1) + pkg_check_modules(APPINDICATOR REQUIRED ayatana-appindicator3-0.1) target_link_libraries(tray INTERFACE ${GTK3_LIBRARIES} ${APPINDICATOR_LIBRARIES}) target_compile_options(tray PRIVATE -Wall -Wextra -Werror -pedantic -Wno-unused-lambda-capture) @@ -28,4 +28,4 @@ set_target_properties(tray PROPERTIES CXX_STANDARD_REQUIRED ON) set_target_properties(tray PROPERTIES VERSION ${PROJECT_VERSION}) -set_target_properties(tray PROPERTIES PROJECT_NAME ${PROJECT_NAME}) \ No newline at end of file +set_target_properties(tray PROPERTIES PROJECT_NAME ${PROJECT_NAME}) diff --git a/tray/include/core/linux/tray.hpp b/tray/include/core/linux/tray.hpp index e10bc97..a94bbd2 100755 --- a/tray/include/core/linux/tray.hpp +++ b/tray/include/core/linux/tray.hpp @@ -1,7 +1,7 @@ #pragma once #if defined(__linux__) #include -#include +#include namespace Tray { @@ -25,4 +25,4 @@ namespace Tray void pump() override; }; } // namespace Tray -#endif \ No newline at end of file +#endif diff --git a/tray/src/core/linux/tray.cpp b/tray/src/core/linux/tray.cpp index 24b058d..9197f32 100755 --- a/tray/src/core/linux/tray.cpp +++ b/tray/src/core/linux/tray.cpp @@ -1,6 +1,6 @@ #if defined(__linux__) #include -#include +#include #include #include @@ -173,4 +173,4 @@ void Tray::Tray::pump() gtk_main_iteration_do(false); } -#endif \ No newline at end of file +#endif From b81b2e4c36e33edbb3a182ee31597147a8b50e72 Mon Sep 17 00:00:00 2001 From: Tony Di Croce Date: Fri, 19 Aug 2022 09:14:32 -0400 Subject: [PATCH 03/14] updated theme path --- tray/src/core/linux/tray.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tray/src/core/linux/tray.cpp b/tray/src/core/linux/tray.cpp index 9197f32..77e9a83 100755 --- a/tray/src/core/linux/tray.cpp +++ b/tray/src/core/linux/tray.cpp @@ -20,6 +20,18 @@ Tray::Tray::Tray(std::string identifier, Icon icon) : BaseTray(std::move(identif } appIndicator = app_indicator_new(this->identifier.c_str(), this->icon, APP_INDICATOR_CATEGORY_APPLICATION_STATUS); + + gchar *current_dir = g_get_current_dir(); + g_print("%s current_dir:%s\n", G_STRFUNC, current_dir); + gchar *theme_path = g_build_path(G_DIR_SEPARATOR_S, + current_dir, + "data", + NULL); + g_print("%s path:%s\n", G_STRFUNC, theme_path); + app_indicator_set_icon_theme_path(appIndicator, theme_path); + g_free(current_dir); + g_free(theme_path); + app_indicator_set_status(appIndicator, APP_INDICATOR_STATUS_ACTIVE); } From eb380e47f70f1d069d88a0c16e661043fec1bbdd Mon Sep 17 00:00:00 2001 From: Tony Di Croce Date: Mon, 22 Aug 2022 07:19:11 -0400 Subject: [PATCH 04/14] commented out data dir. --- tray/src/core/linux/tray.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tray/src/core/linux/tray.cpp b/tray/src/core/linux/tray.cpp index 77e9a83..8721f61 100755 --- a/tray/src/core/linux/tray.cpp +++ b/tray/src/core/linux/tray.cpp @@ -21,6 +21,7 @@ Tray::Tray::Tray(std::string identifier, Icon icon) : BaseTray(std::move(identif appIndicator = app_indicator_new(this->identifier.c_str(), this->icon, APP_INDICATOR_CATEGORY_APPLICATION_STATUS); +#if 0 gchar *current_dir = g_get_current_dir(); g_print("%s current_dir:%s\n", G_STRFUNC, current_dir); gchar *theme_path = g_build_path(G_DIR_SEPARATOR_S, @@ -31,6 +32,7 @@ Tray::Tray::Tray(std::string identifier, Icon icon) : BaseTray(std::move(identif app_indicator_set_icon_theme_path(appIndicator, theme_path); g_free(current_dir); g_free(theme_path); +#endif app_indicator_set_status(appIndicator, APP_INDICATOR_STATUS_ACTIVE); } From 5a0a0996a743f2aeeeb35a4df6fd6acf42dd6a58 Mon Sep 17 00:00:00 2001 From: Tony Di Croce Date: Fri, 28 Mar 2025 10:37:37 -0400 Subject: [PATCH 05/14] Update CMakeLists.txt --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 02d7942..6495dff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.1) +cmake_minimum_required(VERSION 3.14) project(tray VERSION 0.2 DESCRIPTION "A cross-platform C++ system tray library") file(GLOB src From 7e2033935e84395eece622979315a3d9c366f6b0 Mon Sep 17 00:00:00 2001 From: Tony Di Croce Date: Tue, 1 Jul 2025 06:15:20 -0400 Subject: [PATCH 06/14] progress --- CMakeLists.txt | 49 +++++++++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6495dff..7961e34 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,31 +1,32 @@ -cmake_minimum_required(VERSION 3.14) -project(tray VERSION 0.2 DESCRIPTION "A cross-platform C++ system tray library") +if(UNIX) + find_package(PkgConfig REQUIRED) -file(GLOB src - "tray/src/*.cpp" - "tray/src/*/*.cpp" - "tray/src/*/*/*.cpp" -) + pkg_check_modules(GTK3 REQUIRED gtk+-3.0) + pkg_check_modules(APPINDICATOR REQUIRED ayatana-appindicator3-0.1) -add_library(tray STATIC ${src}) + # Headers → propagate to users of tray + target_include_directories(tray + PUBLIC + ${GTK3_INCLUDE_DIRS} + ${APPINDICATOR_INCLUDE_DIRS} + ${PROJECT_SOURCE_DIR}/tray/include # your own headers + SYSTEM PUBLIC # mark as system to silence warnings + ${GTK3_INCLUDE_DIRS} ${APPINDICATOR_INCLUDE_DIRS} + ) -if (UNIX) - find_package(PkgConfig REQUIRED) - pkg_check_modules(GTK3 REQUIRED gtk+-3.0) - pkg_check_modules(APPINDICATOR REQUIRED ayatana-appindicator3-0.1) + # Link flags → propagate to users of tray + target_link_libraries(tray + PUBLIC # <── this is the key change + ${GTK3_LIBRARIES} + ${APPINDICATOR_LIBRARIES} + ) - target_link_libraries(tray INTERFACE ${GTK3_LIBRARIES} ${APPINDICATOR_LIBRARIES}) - target_compile_options(tray PRIVATE -Wall -Wextra -Werror -pedantic -Wno-unused-lambda-capture) - target_include_directories(tray SYSTEM PUBLIC ${GTK3_INCLUDE_DIRS} ${APPINDICATOR_INCLUDE_DIRS} ${PROJECT_SOURCE_DIR}) + # CFLAGS from pkg-config (e.g. -pthread, -D_REENTRANT) + target_compile_options(tray + PUBLIC ${GTK3_CFLAGS_OTHER} ${APPINDICATOR_CFLAGS_OTHER} + PRIVATE -Wall -Wextra -Werror -pedantic -Wno-unused-lambda-capture + ) endif() -target_include_directories(tray SYSTEM PUBLIC "tray/include") - +target_include_directories(tray PUBLIC tray/include) target_compile_features(tray PRIVATE cxx_std_17) -set_target_properties(tray PROPERTIES - CXX_STANDARD 17 - CXX_EXTENSIONS OFF - CXX_STANDARD_REQUIRED ON) - -set_target_properties(tray PROPERTIES VERSION ${PROJECT_VERSION}) -set_target_properties(tray PROPERTIES PROJECT_NAME ${PROJECT_NAME}) From 543836a7c2df9e5c293c370ce2e168ed86498edc Mon Sep 17 00:00:00 2001 From: Tony Di Croce Date: Tue, 1 Jul 2025 06:19:46 -0400 Subject: [PATCH 07/14] progress --- CMakeLists.txt | 64 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7961e34..25c602e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,32 +1,46 @@ -if(UNIX) - find_package(PkgConfig REQUIRED) +cmake_minimum_required(VERSION 3.14) +project(tray VERSION 0.2 LANGUAGES CXX) + +# ① Gather sources – use CONFIGURE_DEPENDS so the list regenerates after git pull +file(GLOB src CONFIGURE_DEPENDS + "src/*.cpp" # <- drop the extra “tray/” prefix; we are already in that dir + "src/*/*.cpp" + "src/*/*/*.cpp") + +# ② Create the target *before* you start adding properties to it +add_library(tray STATIC ${src}) + +# ③ Common include path (visible everywhere) +target_include_directories(tray + PUBLIC + $ + $) # for “make install” +# ④ Linux-only extras +if(UNIX AND NOT APPLE) + find_package(PkgConfig REQUIRED) pkg_check_modules(GTK3 REQUIRED gtk+-3.0) pkg_check_modules(APPINDICATOR REQUIRED ayatana-appindicator3-0.1) - # Headers → propagate to users of tray - target_include_directories(tray - PUBLIC - ${GTK3_INCLUDE_DIRS} - ${APPINDICATOR_INCLUDE_DIRS} - ${PROJECT_SOURCE_DIR}/tray/include # your own headers - SYSTEM PUBLIC # mark as system to silence warnings - ${GTK3_INCLUDE_DIRS} ${APPINDICATOR_INCLUDE_DIRS} - ) - - # Link flags → propagate to users of tray - target_link_libraries(tray - PUBLIC # <── this is the key change - ${GTK3_LIBRARIES} - ${APPINDICATOR_LIBRARIES} - ) - - # CFLAGS from pkg-config (e.g. -pthread, -D_REENTRANT) - target_compile_options(tray - PUBLIC ${GTK3_CFLAGS_OTHER} ${APPINDICATOR_CFLAGS_OTHER} - PRIVATE -Wall -Wextra -Werror -pedantic -Wno-unused-lambda-capture - ) + # Propagate compile flags that pkg-config emits (-pthread, -D_REENTRANT, …) + target_compile_options(tray PUBLIC + ${GTK3_CFLAGS_OTHER} + ${APPINDICATOR_CFLAGS_OTHER} + -Wall -Wextra -Werror -pedantic + -Wno-unused-lambda-capture) + + # Make the GTK / AppIndicator libs flow through to anything that links tray + target_link_libraries(tray PUBLIC + ${GTK3_LIBRARIES} + ${APPINDICATOR_LIBRARIES}) endif() -target_include_directories(tray PUBLIC tray/include) +# ⑤ C++ standard & misc properties target_compile_features(tray PRIVATE cxx_std_17) +set_target_properties(tray PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED YES + CXX_EXTENSIONS NO + VERSION ${PROJECT_VERSION}) + + From fb28f5a03de455fcaa3f212497af4c13a00f1eb2 Mon Sep 17 00:00:00 2001 From: Tony Di Croce Date: Tue, 1 Jul 2025 06:21:57 -0400 Subject: [PATCH 08/14] progress --- CMakeLists.txt | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 25c602e..d6090d9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,41 +1,35 @@ cmake_minimum_required(VERSION 3.14) project(tray VERSION 0.2 LANGUAGES CXX) -# ① Gather sources – use CONFIGURE_DEPENDS so the list regenerates after git pull -file(GLOB src CONFIGURE_DEPENDS - "src/*.cpp" # <- drop the extra “tray/” prefix; we are already in that dir - "src/*/*.cpp" - "src/*/*/*.cpp") +# --- source files ----------------------------------------------------------- +file(GLOB_RECURSE src CONFIGURE_DEPENDS + "tray/src/*.cpp") # ← matches tray/src/* and deeper -# ② Create the target *before* you start adding properties to it add_library(tray STATIC ${src}) -# ③ Common include path (visible everywhere) +# --- public headers --------------------------------------------------------- target_include_directories(tray PUBLIC - $ - $) # for “make install” + $ + $) -# ④ Linux-only extras +# --- Linux-only (GTK/AppIndicator) bits ------------------------------------- if(UNIX AND NOT APPLE) find_package(PkgConfig REQUIRED) pkg_check_modules(GTK3 REQUIRED gtk+-3.0) pkg_check_modules(APPINDICATOR REQUIRED ayatana-appindicator3-0.1) - # Propagate compile flags that pkg-config emits (-pthread, -D_REENTRANT, …) target_compile_options(tray PUBLIC ${GTK3_CFLAGS_OTHER} ${APPINDICATOR_CFLAGS_OTHER} - -Wall -Wextra -Werror -pedantic - -Wno-unused-lambda-capture) + -Wall -Wextra -Werror -pedantic -Wno-unused-lambda-capture) - # Make the GTK / AppIndicator libs flow through to anything that links tray target_link_libraries(tray PUBLIC ${GTK3_LIBRARIES} ${APPINDICATOR_LIBRARIES}) endif() -# ⑤ C++ standard & misc properties +# --- misc properties -------------------------------------------------------- target_compile_features(tray PRIVATE cxx_std_17) set_target_properties(tray PROPERTIES CXX_STANDARD 17 @@ -43,4 +37,3 @@ set_target_properties(tray PROPERTIES CXX_EXTENSIONS NO VERSION ${PROJECT_VERSION}) - From c481dfd710ef80e65f2828be207f3cc06881ba04 Mon Sep 17 00:00:00 2001 From: Tony Di Croce Date: Tue, 1 Jul 2025 06:31:37 -0400 Subject: [PATCH 09/14] progress --- CMakeLists.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index d6090d9..50a0fef 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,6 +18,11 @@ if(UNIX AND NOT APPLE) find_package(PkgConfig REQUIRED) pkg_check_modules(GTK3 REQUIRED gtk+-3.0) pkg_check_modules(APPINDICATOR REQUIRED ayatana-appindicator3-0.1) + + target_include_directories(tray + PUBLIC + ${GTK3_INCLUDE_DIRS} + ${APPINDICATOR_INCLUDE_DIRS}) target_compile_options(tray PUBLIC ${GTK3_CFLAGS_OTHER} From c10def7d7ba252c97608add39827ddcbd00bb412 Mon Sep 17 00:00:00 2001 From: Tony Di Croce Date: Tue, 1 Jul 2025 06:34:17 -0400 Subject: [PATCH 10/14] progress --- CMakeLists.txt | 72 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 50a0fef..47dce33 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,44 +1,66 @@ cmake_minimum_required(VERSION 3.14) -project(tray VERSION 0.2 LANGUAGES CXX) -# --- source files ----------------------------------------------------------- -file(GLOB_RECURSE src CONFIGURE_DEPENDS - "tray/src/*.cpp") # ← matches tray/src/* and deeper +# ────────────────────────────────────────────────────────────────────────────── +project(tray VERSION 0.2 DESCRIPTION "A cross-platform C++ system-tray library" + LANGUAGES CXX) + +# ─── source files ───────────────────────────────────────────────────────────── +# Everything under tray/src/ (one recursive glob keeps it readable) +file(GLOB_RECURSE src CONFIGURE_DEPENDS "tray/src/*.cpp") add_library(tray STATIC ${src}) -# --- public headers --------------------------------------------------------- +# ─── public headers ─────────────────────────────────────────────────────────── target_include_directories(tray PUBLIC $ $) -# --- Linux-only (GTK/AppIndicator) bits ------------------------------------- +# ─── platform-specific dependencies ─────────────────────────────────────────── if(UNIX AND NOT APPLE) find_package(PkgConfig REQUIRED) - pkg_check_modules(GTK3 REQUIRED gtk+-3.0) - pkg_check_modules(APPINDICATOR REQUIRED ayatana-appindicator3-0.1) - - target_include_directories(tray - PUBLIC - ${GTK3_INCLUDE_DIRS} - ${APPINDICATOR_INCLUDE_DIRS}) - target_compile_options(tray PUBLIC - ${GTK3_CFLAGS_OTHER} - ${APPINDICATOR_CFLAGS_OTHER} - -Wall -Wextra -Werror -pedantic -Wno-unused-lambda-capture) + # Ask pkg-config to give us IMPORTED targets that already carry their flags + pkg_check_modules(GTK3 REQUIRED IMPORTED_TARGET gtk+-3.0) + pkg_check_modules(APPINDICATOR REQUIRED IMPORTED_TARGET ayatana-appindicator3-0.1) + + # Link – using the IMPORTED targets automatically propagates include paths, + # link libs (-lgtk-3, -lgdk-3, -lcairo, …) and extra compile flags (-pthread) + target_link_libraries(tray + PUBLIC # make the deps visible to consumers + PkgConfig::GTK3 + PkgConfig::APPINDICATOR) + + # Extra warnings (but **no -Werror** on external code) + if(CMAKE_CXX_COMPILER_ID MATCHES "^(GNU|Clang)$") + target_compile_options(tray PRIVATE -Wall -Wextra -pedantic) + endif() - target_link_libraries(tray PUBLIC - ${GTK3_LIBRARIES} - ${APPINDICATOR_LIBRARIES}) + # Only add -Wno-unused-lambda-capture if the compiler supports it + include(CheckCXXCompilerFlag) + check_cxx_compiler_flag("-Wno-unused-lambda-capture" HAS_WNO_UNUSED_LAMBDA) + if(HAS_WNO_UNUSED_LAMBDA) + target_compile_options(tray PRIVATE -Wno-unused-lambda-capture) + endif() endif() -# --- misc properties -------------------------------------------------------- +# ─── language & misc properties ─────────────────────────────────────────────── target_compile_features(tray PRIVATE cxx_std_17) + set_target_properties(tray PROPERTIES - CXX_STANDARD 17 - CXX_STANDARD_REQUIRED YES - CXX_EXTENSIONS NO - VERSION ${PROJECT_VERSION}) + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED YES + CXX_EXTENSIONS NO + VERSION ${PROJECT_VERSION}) + +# (Optional) install rules – comment out if you don’t need “make install” +install(TARGETS tray + EXPORT trayTargets + ARCHIVE DESTINATION lib + INCLUDES DESTINATION include) + +install(DIRECTORY tray/include/ DESTINATION include) + +export(EXPORT trayTargets + FILE "${CMAKE_CURRENT_BINARY_DIR}/trayTargets.cmake") From 28a9efc17a55d524508bf7a35eb8ee413f5196cb Mon Sep 17 00:00:00 2001 From: Tony Di Croce Date: Tue, 1 Jul 2025 06:40:21 -0400 Subject: [PATCH 11/14] progress --- CMakeLists.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 47dce33..2f1fd48 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,13 +23,15 @@ if(UNIX AND NOT APPLE) # Ask pkg-config to give us IMPORTED targets that already carry their flags pkg_check_modules(GTK3 REQUIRED IMPORTED_TARGET gtk+-3.0) pkg_check_modules(APPINDICATOR REQUIRED IMPORTED_TARGET ayatana-appindicator3-0.1) + pkg_check_modules(CAIRO REQUIRED IMPORTED_TARGET cairo) # Link – using the IMPORTED targets automatically propagates include paths, # link libs (-lgtk-3, -lgdk-3, -lcairo, …) and extra compile flags (-pthread) target_link_libraries(tray PUBLIC # make the deps visible to consumers PkgConfig::GTK3 - PkgConfig::APPINDICATOR) + PkgConfig::APPINDICATOR + PkgConfig::CAIRO) # Extra warnings (but **no -Werror** on external code) if(CMAKE_CXX_COMPILER_ID MATCHES "^(GNU|Clang)$") From 59b298685767179edb79b2057a08b6b61c37316b Mon Sep 17 00:00:00 2001 From: Tony Di Croce Date: Mon, 10 Nov 2025 14:20:22 -0500 Subject: [PATCH 12/14] macos support --- .claude/settings.local.json | 9 ++ CMakeLists.txt | 19 +++ README.md | 13 +- tray/include/core/icon.hpp | 12 ++ tray/include/core/image.hpp | 13 ++ tray/include/core/macos/tray.hpp | 43 +++++ tray/include/tray.hpp | 2 + tray/src/core/macos/icon.mm | 41 +++++ tray/src/core/macos/image.mm | 43 +++++ tray/src/core/macos/tray.mm | 268 +++++++++++++++++++++++++++++++ 10 files changed, 460 insertions(+), 3 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 tray/include/core/macos/tray.hpp create mode 100644 tray/src/core/macos/icon.mm create mode 100644 tray/src/core/macos/image.mm create mode 100644 tray/src/core/macos/tray.mm diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..c14d84d --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(mkdir:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/CMakeLists.txt b/CMakeLists.txt index 2f1fd48..1898ea2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,6 +7,11 @@ project(tray VERSION 0.2 DESCRIPTION "A cross-platform C++ system-tray library" # ─── source files ───────────────────────────────────────────────────────────── # Everything under tray/src/ (one recursive glob keeps it readable) file(GLOB_RECURSE src CONFIGURE_DEPENDS "tray/src/*.cpp") +# macOS uses Objective-C++ (.mm files) +if(APPLE) + file(GLOB_RECURSE src_mm CONFIGURE_DEPENDS "tray/src/*.mm") + list(APPEND src ${src_mm}) +endif() add_library(tray STATIC ${src}) @@ -44,6 +49,20 @@ if(UNIX AND NOT APPLE) if(HAS_WNO_UNUSED_LAMBDA) target_compile_options(tray PRIVATE -Wno-unused-lambda-capture) endif() +elseif(APPLE) + # macOS: Enable Objective-C++ for .mm files + enable_language(OBJCXX) + set_source_files_properties(${src_mm} PROPERTIES + COMPILE_FLAGS "-x objective-c++") + + # Find and link Cocoa framework + find_library(COCOA_LIBRARY Cocoa REQUIRED) + target_link_libraries(tray PUBLIC ${COCOA_LIBRARY}) + + # Extra warnings + if(CMAKE_CXX_COMPILER_ID MATCHES "^(GNU|Clang)$") + target_compile_options(tray PRIVATE -Wall -Wextra -pedantic) + endif() endif() # ─── language & misc properties ─────────────────────────────────────────────── diff --git a/README.md b/README.md index bafa635..73d3b27 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,13 @@ A cross-platform C++17 library that allows you to create simple tray menus. | -------- | -------------- | | Windows | WinAPI | | Linux | AppIndicator | +| macOS | NSStatusBar | ## Dependencies - Linux - libappindicator-gtk3 +- macOS + - Cocoa framework (included with macOS SDK) ## Basic Usage ```cpp @@ -29,7 +32,9 @@ int main() return 0; } ``` -> On Windows it is not necessary to pass an icon path as icon, you can also use an icon-resource or an existing HICON. +> **Platform-specific notes:** +> - **Windows**: You can pass an icon path, icon-resource, or an existing HICON +> - **macOS**: Icon should be a path to a PNG file or a system icon name (e.g., "NSStatusAvailable") ## Menu components ### Button @@ -44,11 +49,13 @@ Button(std::string text, std::function callback); ImageButton(std::string text, Image image, std::function callback); ``` **Parameters:** -- `image` - The image tho show +- `image` - The image to show - Windows > Image should either be a path to a bitmap or an HBITMAP - Linux - > Image should either be a path to a png or a GtkImage + > Image should either be a path to a PNG or a GtkImage + - macOS + > Image should be a path to a PNG file or NSImage pointer - `callback` - The function that is called when the button is pressed ---- ### Toggle diff --git a/tray/include/core/icon.hpp b/tray/include/core/icon.hpp index 8184c11..eba2923 100755 --- a/tray/include/core/icon.hpp +++ b/tray/include/core/icon.hpp @@ -29,5 +29,17 @@ namespace Tray operator HICON(); }; +#elif defined(__APPLE__) + class Icon + { + void *nsImage; // NSImage* + + public: + ~Icon(); + Icon(const char *path); + Icon(const std::string &path); + + operator void*(); + }; #endif } // namespace Tray \ No newline at end of file diff --git a/tray/include/core/image.hpp b/tray/include/core/image.hpp index 7526987..3ce1475 100755 --- a/tray/include/core/image.hpp +++ b/tray/include/core/image.hpp @@ -34,5 +34,18 @@ namespace Tray operator HBITMAP(); }; +#elif defined(__APPLE__) + class Image + { + void *nsImage; // NSImage* + + public: + ~Image(); + Image(void *image); + Image(const char *path); + Image(const std::string &path); + + operator void*(); + }; #endif } // namespace Tray \ No newline at end of file diff --git a/tray/include/core/macos/tray.hpp b/tray/include/core/macos/tray.hpp new file mode 100644 index 0000000..882bd1c --- /dev/null +++ b/tray/include/core/macos/tray.hpp @@ -0,0 +1,43 @@ +#pragma once +#if defined(__APPLE__) +#include + +// Forward declarations to avoid Objective-C in header +#ifdef __OBJC__ +@class NSStatusItem; +@class NSMenu; +@class NSMenuItem; +@class TrayDelegate; +#else +typedef void NSStatusItem; +typedef void NSMenu; +typedef void NSMenuItem; +typedef void TrayDelegate; +#endif + +namespace Tray +{ + class Tray : public BaseTray + { + NSStatusItem *statusItem; + NSMenu *menu; + TrayDelegate *delegate; + + static NSMenu *construct(const std::vector> &, Tray *parent); + static void menuItemClicked(void *context); + + public: + ~Tray(); + Tray(std::string identifier, Icon icon); + template Tray(std::string identifier, Icon icon, const T &...entries) : Tray(identifier, icon) + { + addEntries(entries...); + } + + void run() override; + void exit() override; + void update() override; + void pump() override; + }; +} // namespace Tray +#endif diff --git a/tray/include/tray.hpp b/tray/include/tray.hpp index e83f80d..227b8ca 100755 --- a/tray/include/tray.hpp +++ b/tray/include/tray.hpp @@ -11,4 +11,6 @@ #include #elif defined(__linux__) #include +#elif defined(__APPLE__) +#include #endif \ No newline at end of file diff --git a/tray/src/core/macos/icon.mm b/tray/src/core/macos/icon.mm new file mode 100644 index 0000000..ad00374 --- /dev/null +++ b/tray/src/core/macos/icon.mm @@ -0,0 +1,41 @@ +#if defined(__APPLE__) +#import +#include + +Tray::Icon::Icon(const std::string &path) +{ + NSString *nsPath = [NSString stringWithUTF8String:path.c_str()]; + NSImage *image = [[NSImage alloc] initWithContentsOfFile:nsPath]; + + if (image) { + // Resize to standard status bar icon size (22x22 points for @1x) + [image setSize:NSMakeSize(22.0, 22.0)]; + nsImage = (__bridge_retained void*)image; + } else { + // If file loading fails, try loading from system resources + image = [NSImage imageNamed:nsPath]; + if (image) { + image = [image copy]; // Make a copy to own it + [image setSize:NSMakeSize(22.0, 22.0)]; + nsImage = (__bridge_retained void*)image; + } else { + nsImage = nullptr; + } + } +} + +Tray::Icon::Icon(const char *path) : Icon(std::string(path)) {} + +Tray::Icon::operator void*() +{ + return nsImage; +} + +Tray::Icon::~Icon() +{ + if (nsImage) { + CFRelease(nsImage); + } +} + +#endif diff --git a/tray/src/core/macos/image.mm b/tray/src/core/macos/image.mm new file mode 100644 index 0000000..84876bb --- /dev/null +++ b/tray/src/core/macos/image.mm @@ -0,0 +1,43 @@ +#if defined(__APPLE__) +#import +#include + +Tray::Image::Image(void *image) : nsImage(image) {} + +Tray::Image::Image(const char *path) : Image(std::string(path)) {} + +Tray::Image::Image(const std::string &path) +{ + NSString *nsPath = [NSString stringWithUTF8String:path.c_str()]; + NSImage *image = [[NSImage alloc] initWithContentsOfFile:nsPath]; + + if (image) { + // Resize to standard menu item icon size (16x16 points) + [image setSize:NSMakeSize(16.0, 16.0)]; + nsImage = (__bridge_retained void*)image; + } else { + // Try loading from system resources + image = [NSImage imageNamed:nsPath]; + if (image) { + image = [image copy]; // Make a copy to own it + [image setSize:NSMakeSize(16.0, 16.0)]; + nsImage = (__bridge_retained void*)image; + } else { + nsImage = nullptr; + } + } +} + +Tray::Image::operator void*() +{ + return nsImage; +} + +Tray::Image::~Image() +{ + if (nsImage) { + CFRelease(nsImage); + } +} + +#endif diff --git a/tray/src/core/macos/tray.mm b/tray/src/core/macos/tray.mm new file mode 100644 index 0000000..605b276 --- /dev/null +++ b/tray/src/core/macos/tray.mm @@ -0,0 +1,268 @@ +#if defined(__APPLE__) +#import +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +// Objective-C delegate class to bridge menu item clicks to C++ +@interface TrayDelegate : NSObject +{ + @public + Tray::Tray *tray; +} +- (void)menuItemClicked:(id)sender; +@end + +@implementation TrayDelegate +- (void)menuItemClicked:(id)sender +{ + NSMenuItem *menuItem = (NSMenuItem *)sender; + Tray::TrayEntry *item = (__bridge Tray::TrayEntry *)menuItem.representedObject; + + if (auto *button = dynamic_cast(item); button) + { + button->clicked(); + } + else if (auto *toggle = dynamic_cast(item); toggle) + { + toggle->onToggled(); + if (tray) + { + tray->update(); + } + } + else if (auto *syncedToggle = dynamic_cast(item); syncedToggle) + { + syncedToggle->onToggled(); + if (tray) + { + tray->update(); + } + } +} +@end + +Tray::Tray::Tray(std::string identifier, Icon icon) : BaseTray(std::move(identifier), icon) +{ + @autoreleasepool { + // Initialize NSApplication if not already done + [NSApplication sharedApplication]; + + // Create status bar item + statusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain]; + if (!statusItem) + { + throw std::runtime_error("Failed to create status item"); + } + + // Set the icon + NSImage *nsIcon = (__bridge NSImage *)static_cast(this->icon); + if (nsIcon) + { + statusItem.button.image = nsIcon; + } + + // Create menu + menu = [[NSMenu alloc] init]; + menu.autoenablesItems = NO; // We'll manage enabled state manually + + // Create delegate for callbacks + delegate = [[TrayDelegate alloc] init]; + delegate->tray = this; + } +} + +Tray::Tray::~Tray() +{ + @autoreleasepool { + if (statusItem) + { + [[NSStatusBar systemStatusBar] removeStatusItem:statusItem]; + [statusItem release]; + statusItem = nullptr; + } + + if (menu) + { + [menu release]; + menu = nullptr; + } + + if (delegate) + { + [delegate release]; + delegate = nullptr; + } + } +} + +void Tray::Tray::exit() +{ + @autoreleasepool { + if (statusItem) + { + [[NSStatusBar systemStatusBar] removeStatusItem:statusItem]; + [statusItem release]; + statusItem = nullptr; + } + + [NSApp stop:nil]; + + // Post a dummy event to wake up the event loop + NSEvent *event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined + location:NSMakePoint(0, 0) + modifierFlags:0 + timestamp:0 + windowNumber:0 + context:nil + subtype:0 + data1:0 + data2:0]; + [NSApp postEvent:event atStart:YES]; + } +} + +void Tray::Tray::update() +{ + @autoreleasepool { + // Clear existing menu items + [menu removeAllItems]; + + // Reconstruct the menu + NSMenu *newMenu = construct(entries, this); + + // Copy items to our menu + for (NSMenuItem *item in newMenu.itemArray) + { + [menu addItem:item]; + } + + // Set the menu on the status item + statusItem.menu = menu; + + [newMenu release]; + } +} + +NSMenu *Tray::Tray::construct(const std::vector> &entries, Tray *parent) +{ + @autoreleasepool { + NSMenu *nsMenu = [[NSMenu alloc] init]; + nsMenu.autoenablesItems = NO; + + for (const auto &entry : entries) + { + auto *item = entry.get(); + NSMenuItem *nsItem = nil; + + if (auto *toggle = dynamic_cast(item); toggle) + { + nsItem = [[NSMenuItem alloc] initWithTitle:[NSString stringWithUTF8String:toggle->getText().c_str()] + action:@selector(menuItemClicked:) + keyEquivalent:@""]; + nsItem.target = parent->delegate; + nsItem.state = toggle->isToggled() ? NSControlStateValueOn : NSControlStateValueOff; + nsItem.representedObject = (__bridge id)(void*)item; + } + else if (auto *syncedToggle = dynamic_cast(item); syncedToggle) + { + nsItem = [[NSMenuItem alloc] initWithTitle:[NSString stringWithUTF8String:syncedToggle->getText().c_str()] + action:@selector(menuItemClicked:) + keyEquivalent:@""]; + nsItem.target = parent->delegate; + nsItem.state = syncedToggle->isToggled() ? NSControlStateValueOn : NSControlStateValueOff; + nsItem.representedObject = (__bridge id)(void*)item; + } + else if (auto *submenu = dynamic_cast(item); submenu) + { + nsItem = [[NSMenuItem alloc] initWithTitle:[NSString stringWithUTF8String:submenu->getText().c_str()] + action:nil + keyEquivalent:@""]; + NSMenu *subMenu = construct(submenu->getEntries(), parent); + nsItem.submenu = subMenu; + } + else if (auto *imageButton = dynamic_cast(item); imageButton) + { + nsItem = [[NSMenuItem alloc] initWithTitle:[NSString stringWithUTF8String:imageButton->getText().c_str()] + action:@selector(menuItemClicked:) + keyEquivalent:@""]; + nsItem.target = parent->delegate; + nsItem.representedObject = (__bridge id)(void*)item; + + NSImage *nsImage = (__bridge NSImage *)static_cast(imageButton->getImage()); + if (nsImage) + { + nsItem.image = nsImage; + } + } + else if (dynamic_cast