Skip to content

Commit bafc5e0

Browse files
committed
ActionUIAppKitApplication: add predefined menu item sentinels
Constructing menu additions in SwiftUI requires the presence of certain predefined items in the menus. If we don't want to display them, we can create "sentinels", which are non-displaying separators and yet they exist for CommandGroup placementTarget to anchor the custom additions. Additional improvement: action callback triggered from menu bar will pass the windowUUID for the front window if it exists. This way we can target the actions to current user context.
1 parent b1a8186 commit bafc5e0

2 files changed

Lines changed: 103 additions & 9 deletions

File tree

ActionUIAppKitApplication/ActionUIApp.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,13 @@ var appIcon: NSImage? = nil
8181

8282
private var windows: [String: NSWindow] = [:]
8383

84+
/// Returns the UUID of the key (frontmost) window, or an empty string if none.
85+
@MainActor
86+
func keyWindowUUID() -> String {
87+
guard let keyWindow = NSApp.keyWindow else { return "" }
88+
return windows.first(where: { $0.value === keyWindow })?.key ?? ""
89+
}
90+
8491
// MARK: - Application delegate
8592

8693
final class ActionUIApplicationDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {

ActionUIAppKitApplication/ActionUIAppMenuBar.swift

Lines changed: 96 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -85,18 +85,34 @@ func installDefaultMenuBar(appName: String? = nil) {
8585
private func buildAppMenu(appName: String) -> NSMenuItem {
8686
let menu = NSMenu() // title ignored for app menu
8787

88+
// --- appInfo ---
89+
let appInfoSentinel = NSMenuItem.separator()
90+
appInfoSentinel.tag = MenuPlacementTag.appInfo.rawValue
91+
menu.addItem(appInfoSentinel)
92+
8893
menu.addItem(withTitle: "About \(appName)",
8994
action: #selector(ActionUIApplicationDelegate.showAboutPanel(_:)),
9095
keyEquivalent: "")
9196

92-
menu.addItem(.separator())
97+
// --- appSettings ---
98+
let appSettingsSentinel = NSMenuItem.separator()
99+
appSettingsSentinel.tag = MenuPlacementTag.appSettings.rawValue
100+
menu.addItem(appSettingsSentinel)
101+
102+
// --- systemServices ---
103+
let systemServicesSentinel = NSMenuItem.separator()
104+
systemServicesSentinel.tag = MenuPlacementTag.systemServices.rawValue
105+
menu.addItem(systemServicesSentinel)
93106

94107
let servicesMenu = NSMenu(title: "Services")
95108
let servicesItem = menu.addItem(withTitle: "Services", action: nil, keyEquivalent: "")
96109
servicesItem.submenu = servicesMenu
97110
NSApp.servicesMenu = servicesMenu
98111

99-
menu.addItem(.separator())
112+
// --- appVisibility ---
113+
let appVisibilitySentinel = NSMenuItem.separator()
114+
appVisibilitySentinel.tag = MenuPlacementTag.appVisibility.rawValue
115+
menu.addItem(appVisibilitySentinel)
100116

101117
menu.addItem(withTitle: "Hide \(appName)",
102118
action: #selector(NSApplication.hide(_:)),
@@ -111,7 +127,10 @@ private func buildAppMenu(appName: String) -> NSMenuItem {
111127
action: #selector(NSApplication.unhideAllApplications(_:)),
112128
keyEquivalent: "")
113129

114-
menu.addItem(.separator())
130+
// --- appTermination ---
131+
let appTerminationSentinel = NSMenuItem.separator()
132+
appTerminationSentinel.tag = MenuPlacementTag.appTermination.rawValue
133+
menu.addItem(appTerminationSentinel)
115134

116135
menu.addItem(withTitle: "Quit \(appName)",
117136
action: #selector(NSApplication.terminate(_:)),
@@ -125,10 +144,30 @@ private func buildAppMenu(appName: String) -> NSMenuItem {
125144
private func buildFileMenu() -> NSMenuItem {
126145
let menu = NSMenu(title: "File")
127146

147+
// --- New / Open / Open Recent group ---
148+
let newItemSentinel = NSMenuItem.separator()
149+
newItemSentinel.tag = MenuPlacementTag.newItem.rawValue
150+
menu.addItem(newItemSentinel)
151+
152+
// --- Close / Save group ---
128153
menu.addItem(withTitle: "Close",
129154
action: #selector(NSWindow.performClose(_:)),
130155
keyEquivalent: "w")
131156

157+
let saveItemSentinel = NSMenuItem.separator()
158+
saveItemSentinel.tag = MenuPlacementTag.saveItem.rawValue
159+
menu.addItem(saveItemSentinel)
160+
161+
// --- Import / Export group ---
162+
let importExportSentinel = NSMenuItem.separator()
163+
importExportSentinel.tag = MenuPlacementTag.importExport.rawValue
164+
menu.addItem(importExportSentinel)
165+
166+
// --- Print group ---
167+
let printItemSentinel = NSMenuItem.separator()
168+
printItemSentinel.tag = MenuPlacementTag.printItem.rawValue
169+
menu.addItem(printItemSentinel)
170+
132171
let item = NSMenuItem()
133172
item.submenu = menu
134173
return item
@@ -137,6 +176,11 @@ private func buildFileMenu() -> NSMenuItem {
137176
private func buildEditMenu() -> NSMenuItem {
138177
let menu = NSMenu(title: "Edit")
139178

179+
// --- undoRedo ---
180+
let undoRedoSentinel = NSMenuItem.separator()
181+
undoRedoSentinel.tag = MenuPlacementTag.undoRedo.rawValue
182+
menu.addItem(undoRedoSentinel)
183+
140184
menu.addItem(withTitle: "Undo",
141185
action: Selector(("undo:")),
142186
keyEquivalent: "z")
@@ -146,7 +190,10 @@ private func buildEditMenu() -> NSMenuItem {
146190
keyEquivalent: "z")
147191
redo.keyEquivalentModifierMask = [.command, .shift]
148192

149-
menu.addItem(.separator())
193+
// --- pasteboard ---
194+
let pasteboardSentinel = NSMenuItem.separator()
195+
pasteboardSentinel.tag = MenuPlacementTag.pasteboard.rawValue
196+
menu.addItem(pasteboardSentinel)
150197

151198
menu.addItem(withTitle: "Cut",
152199
action: #selector(NSText.cut(_:)),
@@ -168,6 +215,16 @@ private func buildEditMenu() -> NSMenuItem {
168215
action: #selector(NSText.selectAll(_:)),
169216
keyEquivalent: "a")
170217

218+
// --- textEditing ---
219+
let textEditingSentinel = NSMenuItem.separator()
220+
textEditingSentinel.tag = MenuPlacementTag.textEditing.rawValue
221+
menu.addItem(textEditingSentinel)
222+
223+
// --- textFormatting ---
224+
let textFormattingSentinel = NSMenuItem.separator()
225+
textFormattingSentinel.tag = MenuPlacementTag.textFormatting.rawValue
226+
menu.addItem(textFormattingSentinel)
227+
171228
let item = NSMenuItem()
172229
item.submenu = menu
173230
return item
@@ -176,6 +233,11 @@ private func buildEditMenu() -> NSMenuItem {
176233
private func buildWindowMenu() -> (NSMenuItem, NSMenu) {
177234
let menu = NSMenu(title: "Window")
178235

236+
// --- windowSize ---
237+
let windowSizeSentinel = NSMenuItem.separator()
238+
windowSizeSentinel.tag = MenuPlacementTag.windowSize.rawValue
239+
menu.addItem(windowSizeSentinel)
240+
179241
menu.addItem(withTitle: "Minimize",
180242
action: #selector(NSWindow.performMiniaturize(_:)),
181243
keyEquivalent: "m")
@@ -184,12 +246,24 @@ private func buildWindowMenu() -> (NSMenuItem, NSMenu) {
184246
action: #selector(NSWindow.performZoom(_:)),
185247
keyEquivalent: "")
186248

187-
menu.addItem(.separator())
249+
// --- windowArrangement ---
250+
let windowArrangementSentinel = NSMenuItem.separator()
251+
windowArrangementSentinel.tag = MenuPlacementTag.windowArrangement.rawValue
252+
menu.addItem(windowArrangementSentinel)
188253

189254
menu.addItem(withTitle: "Bring All to Front",
190255
action: #selector(NSApplication.arrangeInFront(_:)),
191256
keyEquivalent: "")
192257

258+
// --- windowList / singleWindowList ---
259+
let windowListSentinel = NSMenuItem.separator()
260+
windowListSentinel.tag = MenuPlacementTag.windowList.rawValue
261+
menu.addItem(windowListSentinel)
262+
263+
let singleWindowListSentinel = NSMenuItem.separator()
264+
singleWindowListSentinel.tag = MenuPlacementTag.singleWindowList.rawValue
265+
menu.addItem(singleWindowListSentinel)
266+
193267
let item = NSMenuItem()
194268
item.submenu = menu
195269
return (item, menu)
@@ -198,6 +272,11 @@ private func buildWindowMenu() -> (NSMenuItem, NSMenu) {
198272
private func buildHelpMenu(appName: String) -> (NSMenuItem, NSMenu) {
199273
let menu = NSMenu(title: "Help")
200274

275+
// --- help ---
276+
let helpSentinel = NSMenuItem.separator()
277+
helpSentinel.tag = MenuPlacementTag.help.rawValue
278+
menu.addItem(helpSentinel)
279+
201280
menu.addItem(withTitle: "\(appName) Help",
202281
action: #selector(NSApplication.showHelp(_:)),
203282
keyEquivalent: "?")
@@ -297,9 +376,11 @@ private final class MenuActionTarget: NSObject {
297376

298377
@objc func performAction(_ sender: NSMenuItem) {
299378
guard let actionID = actionIDsByTag[sender.tag] else { return }
379+
// Resolve the front window's UUID so menu actions target the key window
380+
let windowUUID = keyWindowUUID()
300381
ActionUIModel.shared.actionHandler(
301382
actionID,
302-
windowUUID: "", // menu actions are not window-scoped
383+
windowUUID: windowUUID,
303384
viewID: sender.tag,
304385
viewPartID: 0,
305386
context: nil
@@ -384,9 +465,15 @@ private func applyCommandMenu(properties: [String: Any],
384465
let menuItem = NSMenuItem()
385466
menuItem.submenu = menu
386467

387-
// Insert before the Help menu (last item), or append if no items
388-
let helpIndex = mainMenu.items.count > 0 ? mainMenu.items.count - 1 : 0
389-
mainMenu.insertItem(menuItem, at: helpIndex)
468+
// Insert before Window menu (or Help if no Window menu found).
469+
// This keeps Window and Help as the rightmost menus per macOS convention.
470+
var insertIndex = mainMenu.items.count
471+
if let windowIndex = mainMenu.items.firstIndex(where: { $0.submenu?.title == "Window" }) {
472+
insertIndex = windowIndex
473+
} else if let helpIndex = mainMenu.items.firstIndex(where: { $0.submenu?.title == "Help" }) {
474+
insertIndex = helpIndex
475+
}
476+
mainMenu.insertItem(menuItem, at: insertIndex)
390477
}
391478

392479
// MARK: - CommandGroup application

0 commit comments

Comments
 (0)