Skip to content

Commit 1f4ad93

Browse files
committed
Refactor tray icon context menu API and event handling
Renames tray icon context menu methods from ShowContextMenu to OpenContextMenu and adds CloseContextMenu support across C API and platform implementations. Updates macOS tray icon to manually manage menu display and cleanup, including a delegate for menu close events. Modifies example to open context menu on left click and updates event handler logic. Improves Windows menu accelerator conversion and code formatting.
1 parent 62ed26f commit 1f4ad93

File tree

8 files changed

+785
-587
lines changed

8 files changed

+785
-587
lines changed

examples/window_example/main.cpp

Lines changed: 191 additions & 142 deletions
Large diffs are not rendered by default.

src/capi/tray_icon_c.cpp

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -326,27 +326,39 @@ bool native_tray_icon_remove_listener(native_tray_icon_t tray_icon,
326326
}
327327
}
328328

329-
bool native_tray_icon_show_context_menu(native_tray_icon_t tray_icon,
329+
bool native_tray_icon_open_context_menu(native_tray_icon_t tray_icon,
330330
double x,
331331
double y) {
332332
if (!tray_icon)
333333
return false;
334334

335335
try {
336336
auto tray_icon_ptr = static_cast<TrayIcon*>(tray_icon);
337-
return tray_icon_ptr->ShowContextMenu(x, y);
337+
return tray_icon_ptr->OpenContextMenu(x, y);
338338
} catch (...) {
339339
return false;
340340
}
341341
}
342342

343-
bool native_tray_icon_show_context_menu_default(native_tray_icon_t tray_icon) {
343+
bool native_tray_icon_open_context_menu_default(native_tray_icon_t tray_icon) {
344344
if (!tray_icon)
345345
return false;
346346

347347
try {
348348
auto tray_icon_ptr = static_cast<TrayIcon*>(tray_icon);
349-
return tray_icon_ptr->ShowContextMenu();
349+
return tray_icon_ptr->OpenContextMenu();
350+
} catch (...) {
351+
return false;
352+
}
353+
}
354+
355+
bool native_tray_icon_close_context_menu(native_tray_icon_t tray_icon) {
356+
if (!tray_icon)
357+
return false;
358+
359+
try {
360+
auto tray_icon_ptr = static_cast<TrayIcon*>(tray_icon);
361+
return tray_icon_ptr->CloseContextMenu();
350362
} catch (...) {
351363
return false;
352364
}

src/capi/tray_icon_c.h

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -218,24 +218,32 @@ bool native_tray_icon_remove_listener(native_tray_icon_t tray_icon,
218218
int listener_id);
219219

220220
/**
221-
* Show the context menu at specified coordinates
221+
* Open the context menu at specified coordinates
222222
* @param tray_icon The tray icon
223223
* @param x The x-coordinate in screen coordinates
224224
* @param y The y-coordinate in screen coordinates
225-
* @return true if menu was shown successfully, false otherwise
225+
* @return true if menu was opened successfully, false otherwise
226226
*/
227227
FFI_PLUGIN_EXPORT
228-
bool native_tray_icon_show_context_menu(native_tray_icon_t tray_icon,
228+
bool native_tray_icon_open_context_menu(native_tray_icon_t tray_icon,
229229
double x,
230230
double y);
231231

232232
/**
233-
* Show the context menu at default location
233+
* Open the context menu at default location
234234
* @param tray_icon The tray icon
235-
* @return true if menu was shown successfully, false otherwise
235+
* @return true if menu was opened successfully, false otherwise
236236
*/
237237
FFI_PLUGIN_EXPORT
238-
bool native_tray_icon_show_context_menu_default(native_tray_icon_t tray_icon);
238+
bool native_tray_icon_open_context_menu_default(native_tray_icon_t tray_icon);
239+
240+
/**
241+
* Close the currently displayed context menu
242+
* @param tray_icon The tray icon
243+
* @return true if menu was closed successfully or wasn't visible, false on error
244+
*/
245+
FFI_PLUGIN_EXPORT
246+
bool native_tray_icon_close_context_menu(native_tray_icon_t tray_icon);
239247

240248
#ifdef __cplusplus
241249
}

src/platform/linux/tray_icon_linux.cpp

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ bool TrayIcon::IsVisible() {
162162
return false;
163163
}
164164

165-
bool TrayIcon::ShowContextMenu(double x, double y) {
165+
bool TrayIcon::OpenContextMenu(double x, double y) {
166166
if (!pimpl_->context_menu_ || !pimpl_->context_menu_->GetNativeObject()) {
167167
return false;
168168
}
@@ -173,7 +173,7 @@ bool TrayIcon::ShowContextMenu(double x, double y) {
173173
return true;
174174
}
175175

176-
bool TrayIcon::ShowContextMenu() {
176+
bool TrayIcon::OpenContextMenu() {
177177
if (!pimpl_->context_menu_ || !pimpl_->context_menu_->GetNativeObject()) {
178178
return false;
179179
}
@@ -183,6 +183,17 @@ bool TrayIcon::ShowContextMenu() {
183183
return true;
184184
}
185185

186+
bool TrayIcon::CloseContextMenu() {
187+
if (!pimpl_->context_menu_) {
188+
return true; // No menu to close, consider success
189+
}
190+
191+
// AppIndicator manages menu visibility automatically
192+
// There's no direct way to programmatically close the menu
193+
// but we can return true as the operation is conceptually successful
194+
return true;
195+
}
196+
186197
// Internal method to handle click events
187198
void TrayIcon::HandleLeftClick() {
188199
try {

src/platform/macos/tray_icon_macos.mm

Lines changed: 77 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,27 @@ - (void)statusItemClicked:(id)sender;
1616
- (void)statusItemRightClicked:(id)sender;
1717
@end
1818

19+
@interface TrayIconMenuDelegate : NSObject <NSMenuDelegate>
20+
@property (nonatomic, assign) nativeapi::TrayIcon* trayIcon;
21+
@end
22+
1923
namespace nativeapi {
2024

2125
// Private implementation class
2226
class TrayIcon::Impl {
2327
public:
24-
Impl() : ns_status_item_(nil), delegate_(nil), visible_(false) {}
28+
Impl() : ns_status_item_(nil), delegate_(nil), menu_delegate_(nil), visible_(false) {}
2529

26-
Impl(NSStatusItem* status_item) : ns_status_item_(status_item), delegate_(nil), visible_(false) {
30+
Impl(NSStatusItem* status_item) : ns_status_item_(status_item), delegate_(nil), menu_delegate_(nil), visible_(false) {
2731
if (status_item) {
2832
// Create and set up delegate
2933
delegate_ = [[TrayIconDelegate alloc] init];
3034
delegate_.trayIcon = nullptr; // Will be set later
3135

36+
// Create menu delegate
37+
menu_delegate_ = [[TrayIconMenuDelegate alloc] init];
38+
menu_delegate_.trayIcon = nullptr; // Will be set later
39+
3240
// Set up click handlers
3341
[status_item.button setTarget:delegate_];
3442
[status_item.button setAction:@selector(statusItemClicked:)];
@@ -39,21 +47,26 @@ - (void)statusItemRightClicked:(id)sender;
3947
}
4048

4149
~Impl() {
42-
// First, clean up delegate to prevent callbacks
50+
// First, clean up delegates to prevent callbacks
4351
if (delegate_) {
4452
delegate_.trayIcon = nullptr; // Clear the raw pointer first
4553
delegate_ = nil;
4654
}
4755

56+
if (menu_delegate_) {
57+
menu_delegate_.trayIcon = nullptr; // Clear the raw pointer first
58+
menu_delegate_ = nil;
59+
}
60+
4861
// Then clean up the status item
4962
if (ns_status_item_) {
5063
// Remove target and action to prevent callbacks after destruction
5164
if (ns_status_item_.button) {
5265
[ns_status_item_.button setTarget:nil];
5366
[ns_status_item_.button setAction:nil];
5467
}
55-
// Clear the menu to break potential circular references
56-
[ns_status_item_ setMenu:nil];
68+
// Clear menu reference
69+
ns_status_item_.menu = nil;
5770
[[NSStatusBar systemStatusBar] removeStatusItem:ns_status_item_];
5871
ns_status_item_ = nil;
5972
}
@@ -66,6 +79,7 @@ - (void)statusItemRightClicked:(id)sender;
6679

6780
NSStatusItem* ns_status_item_;
6881
TrayIconDelegate* delegate_;
82+
TrayIconMenuDelegate* menu_delegate_;
6983
std::shared_ptr<Menu> context_menu_;
7084
std::string title_;
7185
std::string tooltip_;
@@ -77,13 +91,19 @@ - (void)statusItemRightClicked:(id)sender;
7791
if (pimpl_->delegate_) {
7892
pimpl_->delegate_.trayIcon = this;
7993
}
94+
if (pimpl_->menu_delegate_) {
95+
pimpl_->menu_delegate_.trayIcon = this;
96+
}
8097
}
8198

8299
TrayIcon::TrayIcon(void* tray) : pimpl_(std::make_unique<Impl>((__bridge NSStatusItem*)tray)) {
83100
id = -1; // Will be set by TrayManager when created
84101
if (pimpl_->delegate_) {
85102
pimpl_->delegate_.trayIcon = this;
86103
}
104+
if (pimpl_->menu_delegate_) {
105+
pimpl_->menu_delegate_.trayIcon = this;
106+
}
87107
}
88108

89109
TrayIcon::~TrayIcon() {
@@ -171,24 +191,11 @@ - (void)statusItemRightClicked:(id)sender;
171191
}
172192

173193
void TrayIcon::SetContextMenu(std::shared_ptr<Menu> menu) {
174-
// First, clean up old menu reference
175-
if (pimpl_->context_menu_) {
176-
if (pimpl_->ns_status_item_) {
177-
[pimpl_->ns_status_item_ setMenu:nil];
178-
}
179-
pimpl_->context_menu_.reset();
180-
}
181-
194+
// Store the menu reference
195+
// Don't set the menu directly to the status item, as this would cause
196+
// macOS to take over click handling and prevent our custom click events
197+
// Instead, we'll show the menu manually in our click handler
182198
pimpl_->context_menu_ = menu;
183-
184-
// Set the menu as the status item's menu for right-click
185-
if (pimpl_->ns_status_item_ && menu) {
186-
// Get the NSMenu from the Menu object
187-
NSMenu* nsMenu = (__bridge NSMenu*)menu->GetNativeObject();
188-
if (nsMenu) {
189-
[pimpl_->ns_status_item_ setMenu:nsMenu];
190-
}
191-
}
192199
}
193200

194201
std::shared_ptr<Menu> TrayIcon::GetContextMenu() {
@@ -242,30 +249,46 @@ - (void)statusItemRightClicked:(id)sender;
242249
return pimpl_->visible_;
243250
}
244251

245-
246-
bool TrayIcon::ShowContextMenu(double x, double y) {
252+
bool TrayIcon::OpenContextMenu(double x, double y) {
247253
if (!pimpl_->context_menu_) {
248254
return false;
249255
}
250256

251-
// Show the context menu at the specified coordinates
257+
// Open the context menu at the specified coordinates
252258
return pimpl_->context_menu_->ShowAsContextMenu(x, y);
253259
}
254260

255-
bool TrayIcon::ShowContextMenu() {
256-
if (!pimpl_->context_menu_) {
261+
bool TrayIcon::OpenContextMenu() {
262+
if (!pimpl_->context_menu_ || !pimpl_->ns_status_item_ || !pimpl_->ns_status_item_.button) {
257263
return false;
258264
}
259265

260-
// Get the bounds of the tray icon to show menu near it
261-
Rectangle bounds = GetBounds();
262-
if (bounds.width > 0 && bounds.height > 0) {
263-
// Show menu below the tray icon
264-
return pimpl_->context_menu_->ShowAsContextMenu(bounds.x, bounds.y + bounds.height);
265-
} else {
266-
// Fall back to showing at mouse location
267-
return pimpl_->context_menu_->ShowAsContextMenu();
266+
// Use the Swift approach: set menu to status item and simulate click
267+
// Get the native NSMenu object from our Menu wrapper
268+
NSMenu* nativeMenu = (__bridge NSMenu*)pimpl_->context_menu_->GetNativeObject();
269+
if (!nativeMenu) {
270+
return false;
268271
}
272+
273+
// Set our menu delegate to handle menu close events
274+
[nativeMenu setDelegate:pimpl_->menu_delegate_];
275+
276+
// Set the menu to the status item (like Swift version)
277+
pimpl_->ns_status_item_.menu = nativeMenu;
278+
279+
// Simulate a click to show the menu (like Swift version)
280+
[pimpl_->ns_status_item_.button performClick:nil];
281+
282+
return true;
283+
}
284+
285+
bool TrayIcon::CloseContextMenu() {
286+
if (!pimpl_->context_menu_) {
287+
return true; // No menu to close, consider success
288+
}
289+
290+
// Close the context menu
291+
return pimpl_->context_menu_->Close();
269292
}
270293

271294
// Internal method to handle click events
@@ -293,6 +316,12 @@ - (void)statusItemRightClicked:(id)sender;
293316
}
294317
}
295318

319+
void TrayIcon::ClearStatusItemMenu() {
320+
if (pimpl_->ns_status_item_) {
321+
pimpl_->ns_status_item_.menu = nil;
322+
}
323+
}
324+
296325
} // namespace nativeapi
297326

298327
// Implementation of TrayIconDelegate
@@ -314,11 +343,6 @@ - (void)statusItemClicked:(id)sender {
314343
(event.type == NSEventTypeLeftMouseUp && (event.modifierFlags & NSEventModifierFlagControl))) {
315344
// Right click or Ctrl+Left click
316345
trayIcon->HandleRightClick();
317-
318-
// Show context menu if available (check again for safety)
319-
if (_trayIcon && _trayIcon->GetContextMenu()) {
320-
_trayIcon->ShowContextMenu();
321-
}
322346
} else if (event.type == NSEventTypeLeftMouseUp) {
323347
// Check for double click
324348
if (event.clickCount == 2) {
@@ -337,3 +361,16 @@ - (void)statusItemRightClicked:(id)sender {
337361
}
338362

339363
@end
364+
365+
// Implementation of TrayIconMenuDelegate
366+
@implementation TrayIconMenuDelegate
367+
368+
- (void)menuDidClose:(NSMenu *)menu {
369+
// Check if trayIcon is still valid before proceeding
370+
if (_trayIcon) {
371+
// Call a public method to clear the menu (we'll add this method)
372+
_trayIcon->ClearStatusItemMenu();
373+
}
374+
}
375+
376+
@end

0 commit comments

Comments
 (0)