diff --git a/TRex.xcodeproj/project.pbxproj b/TRex.xcodeproj/project.pbxproj index de0d640..9704781 100644 --- a/TRex.xcodeproj/project.pbxproj +++ b/TRex.xcodeproj/project.pbxproj @@ -38,6 +38,7 @@ FA66345C25DEFD500087CCEA /* NSImage+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA66345B25DEFD500087CCEA /* NSImage+Extension.swift */; }; FA66345F25DF05950087CCEA /* PagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA66345E25DF05950087CCEA /* PagerView.swift */; }; FA87AF11292A5C7600E50225 /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = FA87AF10292A5C7600E50225 /* LaunchAtLogin */; }; + FA8A957C2B67CA41008C0B7B /* UniversalShowSettingsHack.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA8A957B2B67CA41008C0B7B /* UniversalShowSettingsHack.swift */; }; FACA7A152B46119B004C5000 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = FACA7A142B46119B004C5000 /* Helpers.swift */; }; FAE335C125E0A81500CFFDAF /* NotificationName+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE335C025E0A81500CFFDAF /* NotificationName+Extension.swift */; }; FAFAC6F5272C4CAE00011ED6 /* AboutSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAFAC6F4272C4CAE00011ED6 /* AboutSettingsView.swift */; }; @@ -112,6 +113,7 @@ FA5F876D25DDE3E400B1AF77 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; FA66345B25DEFD500087CCEA /* NSImage+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSImage+Extension.swift"; sourceTree = ""; }; FA66345E25DF05950087CCEA /* PagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagerView.swift; sourceTree = ""; }; + FA8A957B2B67CA41008C0B7B /* UniversalShowSettingsHack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniversalShowSettingsHack.swift; sourceTree = ""; }; FACA7A142B46119B004C5000 /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; FAE335C025E0A81500CFFDAF /* NotificationName+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationName+Extension.swift"; sourceTree = ""; }; FAFAC6F4272C4CAE00011ED6 /* AboutSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutSettingsView.swift; sourceTree = ""; }; @@ -282,6 +284,7 @@ isa = PBXGroup; children = ( FA5F876125DD63AA00B1AF77 /* KeyboardShortcuts+Extension.swift */, + FA8A957B2B67CA41008C0B7B /* UniversalShowSettingsHack.swift */, FA58E39627AC7F630034837F /* Utils.swift */, FA5F876825DDE18B00B1AF77 /* UI */, ); @@ -590,6 +593,7 @@ FA66345C25DEFD500087CCEA /* NSImage+Extension.swift in Sources */, FA5F875B25DD59D000B1AF77 /* MenuBarItem.swift in Sources */, FA5F876225DD63AA00B1AF77 /* KeyboardShortcuts+Extension.swift in Sources */, + FA8A957C2B67CA41008C0B7B /* UniversalShowSettingsHack.swift in Sources */, FA5F874B25DCB50A00B1AF77 /* SettingsView.swift in Sources */, FAFAC6F9272C4CEA00011ED6 /* GeneralSettingsView.swift in Sources */, FA48BA3025DDEBCE00E98F77 /* EnumPicker.swift in Sources */, @@ -860,7 +864,7 @@ CODE_SIGN_INJECT_BASE_ENTITLEMENTS = YES; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 103; + CURRENT_PROJECT_VERSION = 108; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"TRex/Preview Content\""; DEVELOPMENT_TEAM = X93LWC49WV; @@ -890,7 +894,7 @@ CODE_SIGN_INJECT_BASE_ENTITLEMENTS = YES; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 103; + CURRENT_PROJECT_VERSION = 108; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"TRex/Preview Content\""; DEVELOPMENT_TEAM = X93LWC49WV; diff --git a/TRex/App/UI/MenuBarItem.swift b/TRex/App/UI/MenuBarItem.swift index 2a8dbec..d51b7a4 100644 --- a/TRex/App/UI/MenuBarItem.swift +++ b/TRex/App/UI/MenuBarItem.swift @@ -13,7 +13,7 @@ class MenubarItem: NSObject { let captureTextAndTriggerAutomationItem = NSMenuItem(title: "Capture & Run Automation", action: #selector(captureScreenAndTriggerAutomation), keyEquivalent: "") let captureFromClipboard = NSMenuItem(title: "Capture from Clipboard", action: #selector(captureClipboard), keyEquivalent: "") let ignoreLineBreaksItem = NSMenuItem(title: "Ignore Line Breaks", action: #selector(ignoreLineBreaks), keyEquivalent: "") - let preferencesItem = NSMenuItem(title: "Preferences...", action: #selector(showPreferences), keyEquivalent: "") + let preferencesItem = NSMenuItem(title: "Settings...", action: #selector(showPreferences), keyEquivalent: ",") let quitItem = NSMenuItem(title: "Quit", action: #selector(quit), keyEquivalent: "q") let aboutItem = NSMenuItem(title: "About TRex", action: #selector(showAbout), keyEquivalent: "") @@ -50,17 +50,7 @@ class MenubarItem: NSObject { statusBarmenu.addItem(captureFromClipboard) statusBarmenu.addItem(ignoreLineBreaksItem) statusBarmenu.addItem(NSMenuItem.separator()) - if #available(macOS 13.0, *) { - if let menu = NSApp.mainMenu?.items.first, let item = menu.submenu?.items[2] { - menu.submenu?.removeItem(item) - statusBarmenu.addItem(item) - } - } else { - if let menu = NSApp.mainMenu?.items.first, let item = menu.submenu?.items.first { - menu.submenu?.removeItem(item) - statusBarmenu.addItem(item) - } - } + statusBarmenu.addItem(preferencesItem) statusBarmenu.addItem(aboutItem) statusBarmenu.addItem(NSMenuItem.separator()) statusBarmenu.addItem(quitItem) @@ -93,7 +83,17 @@ class MenubarItem: NSObject { } @objc func showPreferences() { - NSApp.sendAction(Selector(("showPreferencesWindow:")), to: self, from: self) + NSApp.openSettings() + return +// if #available(macOS 14.0, *) { +// NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) +// return +// } +// if #available(macOS 13.0, *) { +// NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) +// return +// } +// NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil) } } diff --git a/TRex/App/UniversalShowSettingsHack.swift b/TRex/App/UniversalShowSettingsHack.swift new file mode 100644 index 0000000..676894d --- /dev/null +++ b/TRex/App/UniversalShowSettingsHack.swift @@ -0,0 +1,126 @@ +import Cocoa + +// https://stackoverflow.com/a/76714125 + +private let kAppMenuInternalIdentifier = "app" +private let kSettingsLocalizedStringKey = "Settings\\U2026" + +extension NSApplication { + /// Open the application settings/preferences window. + func openSettings() { + // macOS 14 Sonoma + if let internalItemAction = NSApp.mainMenu?.item( + withInternalIdentifier: kAppMenuInternalIdentifier + )?.submenu?.item( + withLocalizedTitle: kSettingsLocalizedStringKey + )?.internalItemAction { + internalItemAction() + return + } + + guard let delegate = NSApp.delegate else { return } + + // macOS 13 Ventura + var selector = Selector(("showSettingsWindow:")) + if delegate.responds(to: selector) { + delegate.perform(selector, with: nil, with: nil) + return + } + + // macOS 12 Monterrey + selector = Selector(("showPreferencesWindow:")) + if delegate.responds(to: selector) { + delegate.perform(selector, with: nil, with: nil) + return + } + } +} + +// MARK: - NSMenuItem (Private) + +extension NSMenuItem { + /// An internal SwiftUI menu item identifier that should be a public property on `NSMenuItem`. + var internalIdentifier: String? { + guard let id = Mirror.firstChild( + withLabel: "id", in: self + )?.value else { + return nil + } + + return "\(id)" + } + + /// A callback which is associated directly with this `NSMenuItem`. + var internalItemAction: (() -> Void)? { + guard + let platformItemAction = Mirror.firstChild( + withLabel: "platformItemAction", in: self + )?.value, + let typeErasedCallback = Mirror.firstChild( + in: platformItemAction)?.value + else { + return nil + } + + return Mirror.firstChild( + in: typeErasedCallback + )?.value as? () -> Void + } +} + +// MARK: - NSMenu (Private) + +extension NSMenu { + /// Get the first `NSMenuItem` whose internal identifier string matches the given value. + func item(withInternalIdentifier identifier: String) -> NSMenuItem? { + items.first(where: { + $0.internalIdentifier?.elementsEqual(identifier) ?? false + }) + } + + /// Get the first `NSMenuItem` whose title is equivalent to the localized string referenced + /// by the given localized string key in the localization table identified by the given table name + /// from the bundle located at the given bundle path. + func item( + withLocalizedTitle localizedTitleKey: String, + inTable tableName: String = "MenuCommands", + fromBundle bundlePath: String = "/System/Library/Frameworks/AppKit.framework" + ) -> NSMenuItem? { + guard let localizationResource = Bundle(path: bundlePath) else { + return nil + } + + return item(withTitle: NSLocalizedString( + localizedTitleKey, + tableName: tableName, + bundle: localizationResource, + comment: "" + )) + } +} + +// MARK: - Mirror (Helper) + +private extension Mirror { + /// The unconditional first child of the reflection subject. + var firstChild: Child? { children.first } + + /// The first child of the reflection subject whose label matches the given string. + func firstChild(withLabel label: String) -> Child? { + children.first(where: { + $0.label?.elementsEqual(label) ?? false + }) + } + + /// The unconditional first child of the given subject. + static func firstChild(in subject: Any) -> Child? { + Mirror(reflecting: subject).firstChild + } + + /// The first child of the given subject whose label matches the given string. + static func firstChild( + withLabel label: String, in subject: Any + ) -> Child? { + Mirror(reflecting: subject).firstChild(withLabel: label) + } +} diff --git a/TRex/Resources/TRex.entitlements b/TRex/Resources/TRex.entitlements index 81ca2a8..1fb5ebe 100644 --- a/TRex/Resources/TRex.entitlements +++ b/TRex/Resources/TRex.entitlements @@ -5,9 +5,7 @@ com.apple.security.app-sandbox com.apple.security.application-groups - - X93LWC49WV.TRex.preferences - + com.apple.security.automation.apple-events com.apple.security.files.user-selected.read-only