Skip to content

Commit 5adb439

Browse files
committed
Add tray icon example and context menu support on macOS
1 parent fbe94c9 commit 5adb439

File tree

5 files changed

+291
-7
lines changed

5 files changed

+291
-7
lines changed

CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ add_subdirectory(src)
77

88
# Add example programs subdirectory
99
add_subdirectory(examples/display_example)
10-
add_subdirectory(examples/window_example)
1110
add_subdirectory(examples/keyboard_example)
11+
add_subdirectory(examples/window_example)
12+
add_subdirectory(examples/tray_icon_example)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
cmake_minimum_required(VERSION 3.10)
2+
3+
project(tray_icon_example VERSION 0.0.1 LANGUAGES CXX)
4+
5+
# Set C++ standard
6+
set(CMAKE_CXX_STANDARD 17)
7+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
8+
9+
# Add example program
10+
add_executable(tray_icon_example
11+
"main.cpp"
12+
)
13+
14+
# Link main library
15+
target_link_libraries(tray_icon_example PRIVATE libnativeapi)
16+
17+
# Set example program properties
18+
set_target_properties(tray_icon_example PROPERTIES
19+
OUTPUT_NAME "tray_icon_example"
20+
)
21+
22+
# Set example program compile options (macOS only)
23+
if(APPLE)
24+
set_source_files_properties("main.cpp"
25+
PROPERTIES
26+
COMPILE_FLAGS "-x objective-c++"
27+
)
28+
endif()
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
#include <iostream>
2+
#include <memory>
3+
#include <thread>
4+
#include <chrono>
5+
6+
#include "../../src/tray_icon.h"
7+
#include "../../src/tray_manager.h"
8+
#include "../../src/menu.h"
9+
10+
#ifdef __APPLE__
11+
#import <Cocoa/Cocoa.h>
12+
#endif
13+
14+
using namespace nativeapi;
15+
using nativeapi::Menu;
16+
using nativeapi::MenuItem;
17+
using nativeapi::MenuItemSelectedEvent;
18+
using nativeapi::MenuItemType;
19+
20+
int main() {
21+
std::cout << "Starting TrayIcon Example..." << std::endl;
22+
23+
#ifdef __APPLE__
24+
// Initialize Cocoa application for macOS
25+
[NSApplication sharedApplication];
26+
[NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory];
27+
#endif
28+
29+
// Get TrayManager instance (singleton pattern)
30+
TrayManager& trayManager = TrayManager::GetInstance();
31+
if (!trayManager.IsSupported()) {
32+
std::cerr << "Tray icons are not supported on this platform!" << std::endl;
33+
return 1;
34+
}
35+
36+
// Create a tray icon
37+
auto trayIcon = trayManager.Create();
38+
if (!trayIcon) {
39+
std::cerr << "Failed to create tray icon!" << std::endl;
40+
return 1;
41+
}
42+
43+
// Set up the tray icon
44+
trayIcon->SetTitle("Test App");
45+
trayIcon->SetTooltip("This is a test tray icon");
46+
47+
// Try to set a system icon (using a system-provided icon)
48+
trayIcon->SetIcon("NSImageNameStatusAvailable");
49+
50+
// Set up click handlers
51+
trayIcon->SetOnLeftClick([]() {
52+
std::cout << "*** TRAY ICON LEFT CLICKED! ***" << std::endl;
53+
std::cout << "This is the left click handler working!" << std::endl;
54+
});
55+
56+
trayIcon->SetOnRightClick([&trayIcon]() {
57+
std::cout << "*** TRAY ICON RIGHT CLICKED! ***" << std::endl;
58+
std::cout << "This is the right click handler working!" << std::endl;
59+
// Context menu will be shown automatically
60+
});
61+
62+
trayIcon->SetOnDoubleClick([]() {
63+
std::cout << "*** TRAY ICON DOUBLE CLICKED! ***" << std::endl;
64+
std::cout << "This is the double click handler working!" << std::endl;
65+
});
66+
67+
// Create context menu
68+
auto context_menu = Menu::Create();
69+
70+
// Add menu items
71+
auto status_item = MenuItem::Create("Status: Running", MenuItemType::Normal);
72+
status_item->SetOnClick([](const MenuItemSelectedEvent& event) {
73+
std::cout << "Status clicked from context menu" << std::endl;
74+
});
75+
context_menu->AddItem(status_item);
76+
77+
// Add separator
78+
context_menu->AddSeparator();
79+
80+
// Add settings item
81+
auto settings_item = MenuItem::Create("Settings...", MenuItemType::Normal);
82+
settings_item->SetOnClick([](const MenuItemSelectedEvent& event) {
83+
std::cout << "Settings clicked from context menu" << std::endl;
84+
std::cout << "Opening settings dialog..." << std::endl;
85+
});
86+
context_menu->AddItem(settings_item);
87+
88+
// Add about item
89+
auto about_item = MenuItem::Create("About", MenuItemType::Normal);
90+
about_item->SetOnClick([](const MenuItemSelectedEvent& event) {
91+
std::cout << "About clicked from context menu" << std::endl;
92+
std::cout << "TrayIcon Example v1.0 - Native API Demo" << std::endl;
93+
});
94+
context_menu->AddItem(about_item);
95+
96+
// Add another separator
97+
context_menu->AddSeparator();
98+
99+
// Add exit item
100+
auto exit_item = MenuItem::Create("Exit", MenuItemType::Normal);
101+
bool* should_exit = new bool(false);
102+
exit_item->SetOnClick([should_exit](const MenuItemSelectedEvent& event) {
103+
std::cout << "Exit clicked from context menu" << std::endl;
104+
*should_exit = true;
105+
});
106+
context_menu->AddItem(exit_item);
107+
108+
// Set the context menu to the tray icon
109+
trayIcon->SetContextMenu(context_menu);
110+
111+
// Show the tray icon
112+
if (trayIcon->Show()) {
113+
std::cout << "Tray icon is now visible!" << std::endl;
114+
} else {
115+
std::cerr << "Failed to show tray icon!" << std::endl;
116+
return 1;
117+
}
118+
119+
// Get and display bounds
120+
Rectangle bounds = trayIcon->GetBounds();
121+
std::cout << "Tray icon bounds: x=" << bounds.x
122+
<< ", y=" << bounds.y
123+
<< ", width=" << bounds.width
124+
<< ", height=" << bounds.height << std::endl;
125+
126+
std::cout << "========================================" << std::endl;
127+
std::cout << "Tray icon example is now running!" << std::endl;
128+
std::cout << "Try clicking on the tray icon:" << std::endl;
129+
std::cout << "- Left click: Single click" << std::endl;
130+
std::cout << "- Right click: Shows context menu" << std::endl;
131+
std::cout << "- Double click: Quick double click" << std::endl;
132+
std::cout << "- Context menu: Right-click to see options including Exit" << std::endl;
133+
std::cout << "The application will run for 60 seconds, or until you click Exit." << std::endl;
134+
std::cout << "========================================" << std::endl;
135+
136+
// Keep the application running for 60 seconds or until exit is clicked
137+
int countdown = 60;
138+
while (countdown > 0 && !*should_exit) {
139+
std::this_thread::sleep_for(std::chrono::seconds(1));
140+
countdown--;
141+
142+
// Check if tray icon is still visible
143+
if (!trayIcon->IsVisible()) {
144+
std::cout << "Tray icon is no longer visible!" << std::endl;
145+
break;
146+
}
147+
148+
// Print countdown every 10 seconds
149+
if (countdown % 10 == 0) {
150+
std::cout << "Application will exit in " << countdown << " seconds..." << std::endl;
151+
}
152+
}
153+
154+
if (*should_exit) {
155+
std::cout << "Exit requested from context menu." << std::endl;
156+
}
157+
158+
// Hide the tray icon before exiting
159+
trayIcon->Hide();
160+
std::cout << "Exiting TrayIcon Example..." << std::endl;
161+
162+
// Cleanup
163+
delete should_exit;
164+
return 0;
165+
}

examples/window_example/main.cpp

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ using nativeapi::Display;
66
using nativeapi::DisplayAddedEvent;
77
using nativeapi::DisplayManager;
88
using nativeapi::DisplayRemovedEvent;
9+
using nativeapi::Menu;
10+
using nativeapi::MenuItem;
11+
using nativeapi::MenuItemSelectedEvent;
12+
using nativeapi::MenuItemType;
913
using nativeapi::TrayIcon;
1014
using nativeapi::TrayManager;
1115
using nativeapi::Window;
@@ -72,15 +76,70 @@ int main() {
7276
std::cout << "Tray ID: " << tray_icon.id << std::endl;
7377
std::cout << "Tray Title: " << tray_icon.GetTitle() << std::endl;
7478

79+
// Create context menu
80+
auto context_menu = Menu::Create();
81+
82+
// Add menu items
83+
auto show_window_item = MenuItem::Create("Show Window", MenuItemType::Normal);
84+
show_window_item->SetOnClick([window_ptr](const MenuItemSelectedEvent& event) {
85+
std::cout << "Show Window clicked from context menu" << std::endl;
86+
if (window_ptr) {
87+
window_ptr->Show();
88+
window_ptr->Focus();
89+
}
90+
});
91+
context_menu->AddItem(show_window_item);
92+
93+
auto hide_window_item = MenuItem::Create("Hide Window", MenuItemType::Normal);
94+
hide_window_item->SetOnClick([window_ptr](const MenuItemSelectedEvent& event) {
95+
std::cout << "Hide Window clicked from context menu" << std::endl;
96+
if (window_ptr) {
97+
window_ptr->Hide();
98+
}
99+
});
100+
context_menu->AddItem(hide_window_item);
101+
102+
// Add separator
103+
context_menu->AddSeparator();
104+
105+
// Add about item
106+
auto about_item = MenuItem::Create("About", MenuItemType::Normal);
107+
about_item->SetOnClick([](const MenuItemSelectedEvent& event) {
108+
std::cout << "About clicked from context menu" << std::endl;
109+
std::cout << "Window Example v1.0 - Native API Demo" << std::endl;
110+
});
111+
context_menu->AddItem(about_item);
112+
113+
// Add another separator
114+
context_menu->AddSeparator();
115+
116+
// Add exit item
117+
auto exit_item = MenuItem::Create("Exit", MenuItemType::Normal);
118+
exit_item->SetOnClick([&window_manager](const MenuItemSelectedEvent& event) {
119+
std::cout << "Exit clicked from context menu" << std::endl;
120+
// Get all windows and destroy them to trigger app exit
121+
auto windows = window_manager.GetAll();
122+
for (auto& window : windows) {
123+
window_manager.Destroy(window->id);
124+
}
125+
});
126+
context_menu->AddItem(exit_item);
127+
128+
// Set the context menu to the tray icon
129+
tray_icon.SetContextMenu(context_menu);
130+
75131
// Set up click handlers
76132
tray_icon.SetOnLeftClick([]() {
77133
std::cout << "*** TRAY ICON LEFT CLICKED! ***" << std::endl;
78134
std::cout << "This is the left click handler working!" << std::endl;
79135
});
80136

81-
tray_icon.SetOnRightClick([]() {
137+
tray_icon.SetOnRightClick([&tray_icon]() {
82138
std::cout << "*** TRAY ICON RIGHT CLICKED! ***" << std::endl;
83139
std::cout << "This is the right click handler working!" << std::endl;
140+
// Context menu will be shown automatically on right-click
141+
// But we can also manually show it if needed:
142+
// tray_icon.ShowContextMenu();
84143
});
85144

86145
tray_icon.SetOnDoubleClick([]() {

src/platform/macos/tray_icon_macos.mm

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,18 @@ - (void)statusItemRightClicked:(id)sender;
150150

151151
void TrayIcon::SetContextMenu(std::shared_ptr<Menu> menu) {
152152
pimpl_->context_menu_ = menu;
153-
// Note: Menu functionality is not implemented in this simplified version
153+
154+
// Set the menu as the status item's menu for right-click
155+
if (pimpl_->ns_status_item_ && menu) {
156+
// Get the NSMenu from the Menu object
157+
NSMenu* nsMenu = (__bridge NSMenu*)menu->GetNativeMenu();
158+
if (nsMenu) {
159+
[pimpl_->ns_status_item_ setMenu:nsMenu];
160+
}
161+
} else if (pimpl_->ns_status_item_) {
162+
// Remove the menu if null is passed
163+
[pimpl_->ns_status_item_ setMenu:nil];
164+
}
154165
}
155166

156167
std::shared_ptr<Menu> TrayIcon::GetContextMenu() {
@@ -217,13 +228,28 @@ - (void)statusItemRightClicked:(id)sender;
217228
}
218229

219230
bool TrayIcon::ShowContextMenu(double x, double y) {
220-
// Note: Context menu functionality is not implemented in this simplified version
221-
return false;
231+
if (!pimpl_->context_menu_) {
232+
return false;
233+
}
234+
235+
// Show the context menu at the specified coordinates
236+
return pimpl_->context_menu_->ShowAsContextMenu(x, y);
222237
}
223238

224239
bool TrayIcon::ShowContextMenu() {
225-
// Note: Context menu functionality is not implemented in this simplified version
226-
return false;
240+
if (!pimpl_->context_menu_) {
241+
return false;
242+
}
243+
244+
// Get the bounds of the tray icon to show menu near it
245+
Rectangle bounds = GetBounds();
246+
if (bounds.width > 0 && bounds.height > 0) {
247+
// Show menu below the tray icon
248+
return pimpl_->context_menu_->ShowAsContextMenu(bounds.x, bounds.y + bounds.height);
249+
} else {
250+
// Fall back to showing at mouse location
251+
return pimpl_->context_menu_->ShowAsContextMenu();
252+
}
227253
}
228254

229255
// Internal method to handle click events
@@ -261,6 +287,11 @@ - (void)statusItemClicked:(id)sender {
261287
(event.type == NSEventTypeLeftMouseUp && (event.modifierFlags & NSEventModifierFlagControl))) {
262288
// Right click or Ctrl+Left click
263289
_trayIcon->HandleRightClick();
290+
291+
// Show context menu if available
292+
if (_trayIcon->GetContextMenu()) {
293+
_trayIcon->ShowContextMenu();
294+
}
264295
} else if (event.type == NSEventTypeLeftMouseUp) {
265296
// Check for double click
266297
if (event.clickCount == 2) {

0 commit comments

Comments
 (0)