Skip to content

Commit 63c3d48

Browse files
committed
Refactor event callback instance tracking for menus and tray icons
Replaces Unmanaged pointer passing with native handle address mapping for Menu, MenuItem, and TrayIcon event callbacks. Adds static instance maps with locking to safely resolve Swift objects from native callbacks, improving safety and reliability. Updates Example to use submenu for view modes and adds event handlers for menu and submenu open/close events.
1 parent e34c621 commit 63c3d48

File tree

4 files changed

+207
-36
lines changed

4 files changed

+207
-36
lines changed

Examples/Example/main.swift

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,21 @@ import NativeAPI
6666
// Add separator
6767
contextMenu.addSeparator()
6868

69-
// Add radio button group for view mode selection
69+
// Add submenu for view modes
70+
let viewModeItem = MenuItem("视图模式", type: .submenu)
71+
let viewModeSubmenu = Menu()
72+
7073
let compactViewItem = MenuItem("紧凑视图", type: .radio)
71-
contextMenu.addItem(compactViewItem)
74+
viewModeSubmenu.addItem(compactViewItem)
7275

7376
let normalViewItem = MenuItem("普通视图", type: .radio)
74-
contextMenu.addItem(normalViewItem)
77+
viewModeSubmenu.addItem(normalViewItem)
7578

7679
let detailedViewItem = MenuItem("详细视图", type: .radio)
77-
contextMenu.addItem(detailedViewItem)
80+
viewModeSubmenu.addItem(detailedViewItem)
81+
82+
viewModeItem.submenu = viewModeSubmenu
83+
contextMenu.addItem(viewModeItem)
7884

7985
// Add event handlers for radio buttons
8086
compactViewItem.onClicked { event in
@@ -88,6 +94,15 @@ import NativeAPI
8894
detailedViewItem.onClicked { event in
8995
print("🔘 视图模式: 详细视图")
9096
}
97+
98+
// Add event handlers for submenu events
99+
viewModeItem.onSubmenuOpened { event in
100+
print("📂 子菜单已打开 (MenuItem ID: \(event.menuItemId))")
101+
}
102+
103+
viewModeItem.onSubmenuClosed { event in
104+
print("📂 子菜单已关闭 (MenuItem ID: \(event.menuItemId))")
105+
}
91106

92107
// Add separator
93108
contextMenu.addSeparator()
@@ -103,9 +118,10 @@ import NativeAPI
103118
// Set the context menu for tray icon
104119
trayIcon.contextMenu = contextMenu
105120

106-
// Configure click handlers
121+
// Configure tray icon event handlers
107122
trayIcon.onClicked { event in
108123
print("👆 托盘图标左键点击")
124+
trayIcon.openContextMenu()
109125
}
110126

111127
trayIcon.onRightClicked { event in
@@ -115,6 +131,15 @@ import NativeAPI
115131
trayIcon.onDoubleClicked { event in
116132
print("👆 托盘图标双击")
117133
}
134+
135+
// Configure menu event handlers
136+
contextMenu.onOpened { event in
137+
print("📋 菜单已打开 (Menu ID: \(event.menuId))")
138+
}
139+
140+
contextMenu.onClosed { event in
141+
print("📋 菜单已关闭 (Menu ID: \(event.menuId))")
142+
}
118143

119144
// Show the tray icon
120145
trayIcon.isVisible = true
@@ -189,7 +214,8 @@ import NativeAPI
189214
print("💡 功能测试:")
190215
print(" • 普通菜单项: 显示窗口、关于、设置")
191216
print(" • 复选框菜单项: 显示工具栏、自动保存")
192-
print(" • 单选按钮组: 紧凑视图、普通视图、详细视图")
217+
print(" • 子菜单: 视图模式 (包含单选按钮组)")
218+
print(" • 事件监听: 托盘图标点击事件、菜单打开/关闭事件、子菜单事件")
193219
print(" • 退出: 关闭应用程序")
194220

195221
// Create a minimal window to keep the app running

Sources/CNativeAPI

Sources/NativeAPI/Menu.swift

Lines changed: 109 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ public class MenuItem: BaseEventEmitter, NativeHandleWrapper {
7373

7474
public let nativeHandle: native_menu_item_t
7575
private var eventListeners: [Int32: Any] = [:]
76+
77+
// Static map to track instances by their native handle address
78+
// Note: Access is protected by instancesLock
79+
private nonisolated(unsafe) static var instances: [Int: MenuItem] = [:]
80+
private static let instancesLock = NSLock()
7681

7782
/// Unique identifier for this menu item.
7883
public var id: Int {
@@ -92,6 +97,12 @@ public class MenuItem: BaseEventEmitter, NativeHandleWrapper {
9297
self.nativeHandle = nativeItem
9398
super.init()
9499

100+
// Store instance in static map using handle address as key
101+
let handleAddress = Int(bitPattern: nativeHandle)
102+
MenuItem.instancesLock.lock()
103+
MenuItem.instances[handleAddress] = self
104+
MenuItem.instancesLock.unlock()
105+
95106
// Register event listeners
96107
setupEventListeners()
97108
}
@@ -108,6 +119,13 @@ public class MenuItem: BaseEventEmitter, NativeHandleWrapper {
108119
internal init(nativeItem: native_menu_item_t) {
109120
self.nativeHandle = nativeItem
110121
super.init()
122+
123+
// Store instance in static map using handle address as key
124+
let handleAddress = Int(bitPattern: nativeHandle)
125+
MenuItem.instancesLock.lock()
126+
MenuItem.instances[handleAddress] = self
127+
MenuItem.instancesLock.unlock()
128+
111129
setupEventListeners()
112130
}
113131

@@ -122,7 +140,7 @@ public class MenuItem: BaseEventEmitter, NativeHandleWrapper {
122140
nativeHandle,
123141
NATIVE_MENU_ITEM_EVENT_CLICKED,
124142
MenuItem.clickedCallback,
125-
Unmanaged.passUnretained(self).toOpaque()
143+
nativeHandle
126144
)
127145
if clickedListenerId >= 0 {
128146
eventListeners[clickedListenerId] = "clicked"
@@ -133,7 +151,7 @@ public class MenuItem: BaseEventEmitter, NativeHandleWrapper {
133151
nativeHandle,
134152
NATIVE_MENU_ITEM_EVENT_SUBMENU_OPENED,
135153
MenuItem.submenuOpenedCallback,
136-
Unmanaged.passUnretained(self).toOpaque()
154+
nativeHandle
137155
)
138156
if submenuOpenedListenerId >= 0 {
139157
eventListeners[submenuOpenedListenerId] = "submenuOpened"
@@ -144,7 +162,7 @@ public class MenuItem: BaseEventEmitter, NativeHandleWrapper {
144162
nativeHandle,
145163
NATIVE_MENU_ITEM_EVENT_SUBMENU_CLOSED,
146164
MenuItem.submenuClosedCallback,
147-
Unmanaged.passUnretained(self).toOpaque()
165+
nativeHandle
148166
)
149167
if submenuClosedListenerId >= 0 {
150168
eventListeners[submenuClosedListenerId] = "submenuClosed"
@@ -154,23 +172,47 @@ public class MenuItem: BaseEventEmitter, NativeHandleWrapper {
154172
// Static callback functions for native events
155173
private static let clickedCallback: native_menu_item_event_callback_t = { eventPtr, userDataPtr in
156174
guard let userDataPtr = userDataPtr else { return }
157-
let menuItem = Unmanaged<MenuItem>.fromOpaque(userDataPtr).takeUnretainedValue()
158-
print("Menu item clicked: \(menuItem.id)")
159-
menuItem.emitSync(MenuItemClickedEvent(menuItem.id))
175+
let handleAddress = Int(bitPattern: userDataPtr)
176+
177+
instancesLock.lock()
178+
guard let instance = instances[handleAddress] else {
179+
instancesLock.unlock()
180+
return
181+
}
182+
instancesLock.unlock()
183+
184+
print("Menu item clicked: \(instance.id)")
185+
instance.emitSync(MenuItemClickedEvent(instance.id))
160186
}
161187

162188
private static let submenuOpenedCallback: native_menu_item_event_callback_t = { eventPtr, userDataPtr in
163189
guard let userDataPtr = userDataPtr else { return }
164-
let menuItem = Unmanaged<MenuItem>.fromOpaque(userDataPtr).takeUnretainedValue()
165-
print("Menu item submenu opened: \(menuItem.id)")
166-
menuItem.emitSync(MenuItemSubmenuOpenedEvent(menuItem.id))
190+
let handleAddress = Int(bitPattern: userDataPtr)
191+
192+
instancesLock.lock()
193+
guard let instance = instances[handleAddress] else {
194+
instancesLock.unlock()
195+
return
196+
}
197+
instancesLock.unlock()
198+
199+
print("Menu item submenu opened: \(instance.id)")
200+
instance.emitSync(MenuItemSubmenuOpenedEvent(instance.id))
167201
}
168202

169203
private static let submenuClosedCallback: native_menu_item_event_callback_t = { eventPtr, userDataPtr in
170204
guard let userDataPtr = userDataPtr else { return }
171-
let menuItem = Unmanaged<MenuItem>.fromOpaque(userDataPtr).takeUnretainedValue()
172-
print("Menu item submenu closed: \(menuItem.id)")
173-
menuItem.emitSync(MenuItemSubmenuClosedEvent(menuItem.id))
205+
let handleAddress = Int(bitPattern: userDataPtr)
206+
207+
instancesLock.lock()
208+
guard let instance = instances[handleAddress] else {
209+
instancesLock.unlock()
210+
return
211+
}
212+
instancesLock.unlock()
213+
214+
print("Menu item submenu closed: \(instance.id)")
215+
instance.emitSync(MenuItemSubmenuClosedEvent(instance.id))
174216
}
175217

176218
// MARK: - Properties
@@ -313,6 +355,12 @@ public class MenuItem: BaseEventEmitter, NativeHandleWrapper {
313355
}
314356

315357
public func dispose() {
358+
// Remove instance from static map
359+
let handleAddress = Int(bitPattern: nativeHandle)
360+
MenuItem.instancesLock.lock()
361+
MenuItem.instances.removeValue(forKey: handleAddress)
362+
MenuItem.instancesLock.unlock()
363+
316364
// Remove native listeners
317365
for (listenerId, _) in eventListeners {
318366
native_menu_item_remove_listener(nativeHandle, listenerId)
@@ -335,6 +383,11 @@ public class Menu: BaseEventEmitter, NativeHandleWrapper {
335383

336384
public let nativeHandle: native_menu_t
337385
private var eventListeners: [Int32: Any] = [:]
386+
387+
// Static map to track instances by their native handle address
388+
// Note: Access is protected by instancesLock
389+
private nonisolated(unsafe) static var instances: [Int: Menu] = [:]
390+
private static let instancesLock = NSLock()
338391

339392
/// Unique identifier for this menu.
340393
public var id: Int {
@@ -350,12 +403,26 @@ public class Menu: BaseEventEmitter, NativeHandleWrapper {
350403
}
351404
self.nativeHandle = nativeMenu
352405
super.init()
406+
407+
// Store instance in static map using handle address as key
408+
let handleAddress = Int(bitPattern: nativeHandle)
409+
Menu.instancesLock.lock()
410+
Menu.instances[handleAddress] = self
411+
Menu.instancesLock.unlock()
412+
353413
setupEventListeners()
354414
}
355415

356416
internal init(nativeMenu: native_menu_t) {
357417
self.nativeHandle = nativeMenu
358418
super.init()
419+
420+
// Store instance in static map using handle address as key
421+
let handleAddress = Int(bitPattern: nativeHandle)
422+
Menu.instancesLock.lock()
423+
Menu.instances[handleAddress] = self
424+
Menu.instancesLock.unlock()
425+
359426
setupEventListeners()
360427
}
361428

@@ -370,7 +437,7 @@ public class Menu: BaseEventEmitter, NativeHandleWrapper {
370437
nativeHandle,
371438
NATIVE_MENU_EVENT_OPENED,
372439
Menu.openedCallback,
373-
Unmanaged.passUnretained(self).toOpaque()
440+
nativeHandle
374441
)
375442
if openedListenerId >= 0 {
376443
eventListeners[openedListenerId] = "opened"
@@ -381,7 +448,7 @@ public class Menu: BaseEventEmitter, NativeHandleWrapper {
381448
nativeHandle,
382449
NATIVE_MENU_EVENT_CLOSED,
383450
Menu.closedCallback,
384-
Unmanaged.passUnretained(self).toOpaque()
451+
nativeHandle
385452
)
386453
if closedListenerId >= 0 {
387454
eventListeners[closedListenerId] = "closed"
@@ -391,16 +458,32 @@ public class Menu: BaseEventEmitter, NativeHandleWrapper {
391458
// Static callback functions for native events
392459
private static let openedCallback: native_menu_event_callback_t = { eventPtr, userDataPtr in
393460
guard let userDataPtr = userDataPtr else { return }
394-
let menu = Unmanaged<Menu>.fromOpaque(userDataPtr).takeUnretainedValue()
395-
print("Menu opened: \(menu.id)")
396-
menu.emitSync(MenuOpenedEvent(menu.id))
461+
let handleAddress = Int(bitPattern: userDataPtr)
462+
463+
instancesLock.lock()
464+
guard let instance = instances[handleAddress] else {
465+
instancesLock.unlock()
466+
return
467+
}
468+
instancesLock.unlock()
469+
470+
print("Menu opened: \(instance.id)")
471+
instance.emitSync(MenuOpenedEvent(instance.id))
397472
}
398473

399474
private static let closedCallback: native_menu_event_callback_t = { eventPtr, userDataPtr in
400475
guard let userDataPtr = userDataPtr else { return }
401-
let menu = Unmanaged<Menu>.fromOpaque(userDataPtr).takeUnretainedValue()
402-
print("Menu closed: \(menu.id)")
403-
menu.emitSync(MenuClosedEvent(menu.id))
476+
let handleAddress = Int(bitPattern: userDataPtr)
477+
478+
instancesLock.lock()
479+
guard let instance = instances[handleAddress] else {
480+
instancesLock.unlock()
481+
return
482+
}
483+
instancesLock.unlock()
484+
485+
print("Menu closed: \(instance.id)")
486+
instance.emitSync(MenuClosedEvent(instance.id))
404487
}
405488

406489
// MARK: - Menu Item Management
@@ -476,6 +559,12 @@ public class Menu: BaseEventEmitter, NativeHandleWrapper {
476559
}
477560

478561
public func dispose() {
562+
// Remove instance from static map
563+
let handleAddress = Int(bitPattern: nativeHandle)
564+
Menu.instancesLock.lock()
565+
Menu.instances.removeValue(forKey: handleAddress)
566+
Menu.instancesLock.unlock()
567+
479568
// Remove native listeners
480569
for (listenerId, _) in eventListeners {
481570
native_menu_remove_listener(nativeHandle, listenerId)

0 commit comments

Comments
 (0)