From 9c049ab8166a4faf1d2adafa830de011a711626a Mon Sep 17 00:00:00 2001 From: Kevin Ratzel Date: Sat, 24 Oct 2026 13:39:20 -0400 Subject: [PATCH 1/3] Make the Mac Catalyst app feel more at home on macOS - Remove default menus that don't apply (Format, Open, Open Recent, document items, Toolbar, Sidebar, Find) - Add a Map menu with keyboard shortcuts for zoom, location, search, add node, display options and upload - Keep the map screen in the responder chain so menu items don't all become disabled after a sheet such as Upload is dismissed Co-Authored-By: Claude Opus 5.5 --- src/App/AppDelegate.swift | 56 +++++++++++++++++++ src/App/Localizable.xcstrings | 31 ++++++++++- src/App/MainViewController.swift | 94 ++++++++++++++++++++++++++++++++ 3 files changed, 179 insertions(+), 2 deletions(-) diff --git a/src/App/AppDelegate.swift b/src/App/AppDelegate.swift index 65a334d52..dfa629009 100644 --- a/src/App/AppDelegate.swift +++ b/src/App/AppDelegate.swift @@ -197,6 +197,62 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Insert it into the application menu builder.replace(menu: .preferences, with: settingsMenu) + + builder.remove(menu: .format) + builder.remove(menu: .open) + builder.remove(menu: .openRecent) + builder.remove(menu: .toolbar) + builder.remove(menu: .sidebar) + builder.remove(menu: .find) + if #available(iOS 16.0, *) { + builder.remove(menu: .document) + } + + builder.insertSibling(mapMenu(), afterMenu: .view) + } + + private func mapMenu() -> UIMenu { + func command(_ title: String, + _ action: Selector, + _ input: String, + _ modifiers: UIKeyModifierFlags = .command) -> UIKeyCommand + { + return UIKeyCommand(title: title, action: action, input: input, modifierFlags: modifiers) + } + func section(_ children: [UIMenuElement]) -> UIMenu { + return UIMenu(title: "", options: .displayInline, children: children) + } + let mvc = MainViewController.self + return UIMenu( + title: NSLocalizedString("Map", comment: "Map menu in Mac menu bar"), + children: [ + section([ + command(NSLocalizedString("Zoom In", comment: "Mac menu item"), + #selector(mvc.zoomInMenuItem(_:)), "+"), + command(NSLocalizedString("Zoom Out", comment: "Mac menu item"), + #selector(mvc.zoomOutMenuItem(_:)), "-") + ]), + section([ + command(NSLocalizedString("Center on My Location", comment: "Mac menu item"), + #selector(mvc.centerOnLocationMenuItem(_:)), "l"), + command(NSLocalizedString("Follow My Location", comment: "Mac menu item"), + #selector(mvc.followLocationMenuItem(_:)), "l", [.command, .alternate]), + command(NSLocalizedString("Face North", comment: "Mac menu item"), + #selector(mvc.faceNorthMenuItem(_:)), "n", [.command, .shift]) + ]), + section([ + command(NSLocalizedString("Search…", comment: "Mac menu item"), + #selector(mvc.searchMenuItem(_:)), "f"), + command(NSLocalizedString("Add Node", comment: "Mac menu item"), + #selector(mvc.addNodeMenuItem(_:)), "n") + ]), + section([ + command(NSLocalizedString("Display Options…", comment: "Mac menu item"), + #selector(mvc.displayOptionsMenuItem(_:)), "d"), + command(NSLocalizedString("Upload Changes…", comment: "Mac menu item"), + #selector(mvc.uploadMenuItem(_:)), "u") + ]) + ]) } @objc func openPreferences() { diff --git a/src/App/Localizable.xcstrings b/src/App/Localizable.xcstrings index 0f4e6c25c..b09d6a999 100644 --- a/src/App/Localizable.xcstrings +++ b/src/App/Localizable.xcstrings @@ -19450,7 +19450,7 @@ } }, "Add Node" : { - "comment" : "Edit action" + "comment" : "Edit action\nMac menu item" }, "add node to way" : { "localizations" : { @@ -26410,6 +26410,9 @@ } } }, + "Center on My Location" : { + "comment" : "Mac menu item" + }, "Change" : { "comment" : "Tag value will change", "localizations" : { @@ -32398,6 +32401,9 @@ } } }, + "Display Options…" : { + "comment" : "Mac menu item" + }, "Distance: %1$@ ± %2$@ meters" : { "comment" : "Distance to an object with an error value", "localizations" : { @@ -35138,6 +35144,9 @@ } } }, + "Face North" : { + "comment" : "Mac menu item" + }, "Filter" : { "comment" : "Header for quest filter section" }, @@ -35267,6 +35276,9 @@ } } }, + "Follow My Location" : { + "comment" : "Mac menu item" + }, "ft" : { "comment" : "Height or width in feet, please abbreviate", "localizations" : { @@ -40485,6 +40497,9 @@ } } }, + "Map" : { + "comment" : "Map menu in Mac menu bar" + }, "Map Tiles" : { "comment" : "Delete cached data", "localizations" : { @@ -54873,6 +54888,9 @@ } } }, + "Search…" : { + "comment" : "Mac menu item" + }, "Select a highway approaching the intersection" : { "localizations" : { "ar" : { @@ -64043,6 +64061,9 @@ } } }, + "Upload Changes…" : { + "comment" : "Mac menu item" + }, "Upload succeeded but the server returned mismatched data.\n\nYou should use Clear Cache > OSM Data before continuing to map." : { "comment" : "'Clear Cache' and 'OSM Data' should match what your language uses for the same terms in Display settings", "localizations" : { @@ -67718,6 +67739,12 @@ } } }, + "Zoom In" : { + "comment" : "Mac menu item" + }, + "Zoom Out" : { + "comment" : "Mac menu item" + }, "Zoom to Edit" : { "localizations" : { "ar" : { @@ -67862,4 +67889,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/src/App/MainViewController.swift b/src/App/MainViewController.swift index a0e3abe98..9b71cecc0 100644 --- a/src/App/MainViewController.swift +++ b/src/App/MainViewController.swift @@ -428,6 +428,9 @@ final class MainViewController: UIViewController, DPadDelegate, override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) +#if targetEnvironment(macCatalyst) + reclaimMenuFocus() +#endif if !isInitialized { isInitialized = true @@ -981,11 +984,99 @@ final class MainViewController: UIViewController, DPadDelegate, return mapView.selectedPrimary != nil && mapView.selections.relation == nil case #selector(showHelp(_:)): return true +#if targetEnvironment(macCatalyst) + case #selector(zoomInMenuItem(_:)), + #selector(zoomOutMenuItem(_:)), + #selector(followLocationMenuItem(_:)), + #selector(faceNorthMenuItem(_:)), + #selector(searchMenuItem(_:)), + #selector(displayOptionsMenuItem(_:)): + return presentedViewController == nil + case #selector(centerOnLocationMenuItem(_:)): + return presentedViewController == nil && LocationProvider.shared.currentLocation != nil + case #selector(addNodeMenuItem(_:)): + return presentedViewController == nil && !addNodeButton.isHidden + case #selector(uploadMenuItem(_:)): + return presentedViewController == nil && uploadButton.isEnabled +#endif default: return false } } +#if targetEnvironment(macCatalyst) + override var canBecomeFirstResponder: Bool { + return true + } + + func reclaimMenuFocus() { + guard presentedViewController == nil, + let window = view.window + else { + return + } + if !window.isKeyWindow { + window.makeKey() + } + if !isFirstResponder { + becomeFirstResponder() + } + } + + override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { + super.dismiss(animated: flag, completion: { [weak self] in + completion?() + self?.reclaimMenuFocus() + }) + } + + override func validate(_ command: UICommand) { + super.validate(command) + if command.action == #selector(followLocationMenuItem(_:)) { + command.state = gpsState == .NONE ? .off : .on + } + } + + @objc func zoomInMenuItem(_ sender: Any?) { + viewPort.adjustZoom(by: 2.0, aroundScreenPoint: mapView.bounds.center()) + } + + @objc func zoomOutMenuItem(_ sender: Any?) { + viewPort.adjustZoom(by: 0.5, aroundScreenPoint: mapView.bounds.center()) + } + + @objc func centerOnLocationMenuItem(_ sender: Any?) { + centerOnGPS(sender as Any) + } + + @objc func followLocationMenuItem(_ sender: Any?) { + toggleLocationButton() + } + + @objc func faceNorthMenuItem(_ sender: Any?) { + if gpsState == .HEADING { + gpsState = .LOCATION + } + viewPort.rotateToHeading(0.0) + } + + @objc func searchMenuItem(_ sender: Any?) { + searchButton.sendActions(for: .touchUpInside) + } + + @objc func addNodeMenuItem(_ sender: Any?) { + mapView.addNode(at: mapView.bounds.center()) + } + + @objc func displayOptionsMenuItem(_ sender: Any?) { + displayButton.sendActions(for: .touchUpInside) + } + + @objc func uploadMenuItem(_ sender: Any?) { + uploadButton.sendActions(for: .touchUpInside) + } +#endif + @objc func undo(_ sender: Any?) { mapView.undo(sender) } @@ -1164,6 +1255,9 @@ final class MainViewController: UIViewController, DPadDelegate, } @objc func handleTapGesture(_ tap: UITapGestureRecognizer) { +#if targetEnvironment(macCatalyst) + reclaimMenuFocus() +#endif mapView.handleTapGesture(tap) } From f7a3b7cb3d012e26ea543f032ac39488c86100a7 Mon Sep 17 00:00:00 2001 From: Kevin Ratzel Date: Sat, 24 Oct 2026 21:22:28 -0400 Subject: [PATCH 2/3] Use the buttons' VoiceOver strings for Map menu items Search, Add node, Display options, Upload your changes and GPS now share their localized strings with the corresponding buttons' accessibility labels, so translators only see each string once. Co-Authored-By: Claude Opus 5.5 --- src/App/AppDelegate.swift | 10 +++++----- src/App/Localizable.xcstrings | 14 +------------- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/src/App/AppDelegate.swift b/src/App/AppDelegate.swift index dfa629009..839299dd9 100644 --- a/src/App/AppDelegate.swift +++ b/src/App/AppDelegate.swift @@ -235,21 +235,21 @@ class AppDelegate: UIResponder, UIApplicationDelegate { section([ command(NSLocalizedString("Center on My Location", comment: "Mac menu item"), #selector(mvc.centerOnLocationMenuItem(_:)), "l"), - command(NSLocalizedString("Follow My Location", comment: "Mac menu item"), + command(NSLocalizedString("GPS", comment: "GPS button label"), #selector(mvc.followLocationMenuItem(_:)), "l", [.command, .alternate]), command(NSLocalizedString("Face North", comment: "Mac menu item"), #selector(mvc.faceNorthMenuItem(_:)), "n", [.command, .shift]) ]), section([ - command(NSLocalizedString("Search…", comment: "Mac menu item"), + command(NSLocalizedString("Search", comment: "Search button label") + "…", #selector(mvc.searchMenuItem(_:)), "f"), - command(NSLocalizedString("Add Node", comment: "Mac menu item"), + command(NSLocalizedString("Add node", comment: ""), #selector(mvc.addNodeMenuItem(_:)), "n") ]), section([ - command(NSLocalizedString("Display Options…", comment: "Mac menu item"), + command(NSLocalizedString("Display options", comment: "") + "…", #selector(mvc.displayOptionsMenuItem(_:)), "d"), - command(NSLocalizedString("Upload Changes…", comment: "Mac menu item"), + command(NSLocalizedString("Upload your changes", comment: "") + "…", #selector(mvc.uploadMenuItem(_:)), "u") ]) ]) diff --git a/src/App/Localizable.xcstrings b/src/App/Localizable.xcstrings index b09d6a999..c4ec6a2c9 100644 --- a/src/App/Localizable.xcstrings +++ b/src/App/Localizable.xcstrings @@ -19450,7 +19450,7 @@ } }, "Add Node" : { - "comment" : "Edit action\nMac menu item" + "comment" : "Edit action" }, "add node to way" : { "localizations" : { @@ -32401,9 +32401,6 @@ } } }, - "Display Options…" : { - "comment" : "Mac menu item" - }, "Distance: %1$@ ± %2$@ meters" : { "comment" : "Distance to an object with an error value", "localizations" : { @@ -35276,9 +35273,6 @@ } } }, - "Follow My Location" : { - "comment" : "Mac menu item" - }, "ft" : { "comment" : "Height or width in feet, please abbreviate", "localizations" : { @@ -54888,9 +54882,6 @@ } } }, - "Search…" : { - "comment" : "Mac menu item" - }, "Select a highway approaching the intersection" : { "localizations" : { "ar" : { @@ -64061,9 +64052,6 @@ } } }, - "Upload Changes…" : { - "comment" : "Mac menu item" - }, "Upload succeeded but the server returned mismatched data.\n\nYou should use Clear Cache > OSM Data before continuing to map." : { "comment" : "'Clear Cache' and 'OSM Data' should match what your language uses for the same terms in Display settings", "localizations" : { From e876f7265f01ce21cfe6dd3154cc565e2f61b9f4 Mon Sep 17 00:00:00 2001 From: Kevin Ratzel Date: Sat, 24 Oct 2026 21:37:21 -0400 Subject: [PATCH 3/3] =?UTF-8?q?Add=20Tags=E2=80=A6=20(Command-T)=20to=20th?= =?UTF-8?q?e=20Map=20menu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opens the tag editor for the selected object or dropped pin, reusing the existing "Tags" edit action string. Co-Authored-By: Claude Opus 5.5 --- src/App/AppDelegate.swift | 4 +++- src/App/MainViewController.swift | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/App/AppDelegate.swift b/src/App/AppDelegate.swift index 839299dd9..5d1d00139 100644 --- a/src/App/AppDelegate.swift +++ b/src/App/AppDelegate.swift @@ -244,7 +244,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { command(NSLocalizedString("Search", comment: "Search button label") + "…", #selector(mvc.searchMenuItem(_:)), "f"), command(NSLocalizedString("Add node", comment: ""), - #selector(mvc.addNodeMenuItem(_:)), "n") + #selector(mvc.addNodeMenuItem(_:)), "n"), + command(NSLocalizedString("Tags", comment: "Edit action") + "…", + #selector(mvc.tagsMenuItem(_:)), "t") ]), section([ command(NSLocalizedString("Display options", comment: "") + "…", diff --git a/src/App/MainViewController.swift b/src/App/MainViewController.swift index 9b71cecc0..b0f36d0e1 100644 --- a/src/App/MainViewController.swift +++ b/src/App/MainViewController.swift @@ -996,6 +996,8 @@ final class MainViewController: UIViewController, DPadDelegate, return presentedViewController == nil && LocationProvider.shared.currentLocation != nil case #selector(addNodeMenuItem(_:)): return presentedViewController == nil && !addNodeButton.isHidden + case #selector(tagsMenuItem(_:)): + return presentedViewController == nil && mapView.pushPin != nil case #selector(uploadMenuItem(_:)): return presentedViewController == nil && uploadButton.isEnabled #endif @@ -1068,6 +1070,10 @@ final class MainViewController: UIViewController, DPadDelegate, mapView.addNode(at: mapView.bounds.center()) } + @objc func tagsMenuItem(_ sender: Any?) { + mapView.presentTagEditor(sender) + } + @objc func displayOptionsMenuItem(_ sender: Any?) { displayButton.sendActions(for: .touchUpInside) }