Improve setting front logic

This commit is contained in:
Peter Steinberger 2025-06-16 17:32:06 +02:00
parent b5d043cba8
commit 936806edd6

View file

@ -5,38 +5,65 @@ import SwiftUI
/// Helper to open the Settings window programmatically when SettingsLink cannot be used
@MainActor
enum SettingsOpener {
/// SwiftUI's hardcoded settings window identifier
private static let settingsWindowIdentifier = "com_apple_SwiftUI_Settings_window"
/// Opens the Settings window using the environment action via notification
/// This is needed for cases where we can't use SettingsLink (e.g., from notifications)
static func openSettings() {
NSApp.activate(ignoringOtherApps: true)
// Post notification to trigger openSettings environment action
NotificationCenter.default.post(name: .openSettingsRequest, object: nil)
// Ensure settings window comes to front after opening
Task {
try? await Task.sleep(for: .milliseconds(100))
bringSettingsToFront()
// Try the direct menu item approach first (from VibeMeter)
if openSettingsViaMenuItem() {
// Successfully opened via menu item
Task {
try? await Task.sleep(for: .milliseconds(100))
focusSettingsWindow()
}
} else {
// Fallback to notification approach
NotificationCenter.default.post(name: .openSettingsRequest, object: nil)
Task {
try? await Task.sleep(for: .milliseconds(150))
focusSettingsWindow()
}
}
}
/// Brings the settings window to the front if it exists
static func bringSettingsToFront() {
// Find the settings window by looking for the preferences window
if let settingsWindow = NSApp.windows.first(where: { window in
/// Opens settings via the native menu item (more reliable)
private static func openSettingsViaMenuItem() -> Bool {
let kAppMenuInternalIdentifier = "app"
let kSettingsLocalizedStringKey = "Settings\\U2026"
if let internalItemAction = NSApp.mainMenu?.item(
withInternalIdentifier: kAppMenuInternalIdentifier)?.submenu?.item(
withLocalizedTitle: kSettingsLocalizedStringKey)?.internalItemAction {
internalItemAction()
return true
}
return false
}
/// Focuses the settings window without level manipulation
static func focusSettingsWindow() {
// First try the SwiftUI settings window identifier
if let settingsWindow = NSApp.windows.first(where: {
$0.identifier?.rawValue == settingsWindowIdentifier
}) {
settingsWindow.makeKeyAndOrderFront(nil)
settingsWindow.orderFrontRegardless()
NSApp.activate(ignoringOtherApps: true)
} else if let settingsWindow = NSApp.windows.first(where: { window in
// Fallback to title-based search
window.isVisible &&
(window.styleMask.contains(.titled) &&
window.title.localizedCaseInsensitiveContains("settings") ||
window.styleMask.contains(.titled) &&
(window.title.localizedCaseInsensitiveContains("settings") ||
window.title.localizedCaseInsensitiveContains("preferences"))
}) {
settingsWindow.makeKeyAndOrderFront(nil)
settingsWindow.level = .floating
// Reset to normal level after a brief moment
Task {
try? await Task.sleep(for: .milliseconds(50))
settingsWindow.level = .normal
}
settingsWindow.orderFrontRegardless()
NSApp.activate(ignoringOtherApps: true)
}
}
@ -46,7 +73,7 @@ enum SettingsOpener {
Task {
// Small delay to ensure the settings window is fully initialized
try? await Task.sleep(for: .milliseconds(100))
try? await Task.sleep(for: .milliseconds(150))
NotificationCenter.default.post(
name: .openSettingsTab,
object: tab
@ -100,7 +127,7 @@ struct HiddenWindowView: View {
// Additional check to bring settings to front after environment action
try? await Task.sleep(for: .milliseconds(150))
SettingsOpener.bringSettingsToFront()
SettingsOpener.focusSettingsWindow()
}
}
}
@ -111,4 +138,88 @@ struct HiddenWindowView: View {
extension Notification.Name {
static let openSettingsRequest = Notification.Name("openSettingsRequest")
}
// MARK: - NSMenuItem Extensions (Private)
extension NSMenuItem {
/// An internal SwiftUI menu item identifier that should be a public property on `NSMenuItem`.
fileprivate 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`.
fileprivate 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 Extensions (Private)
extension NSMenu {
/// Get the first `NSMenuItem` whose internal identifier string matches the given value.
fileprivate 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.
fileprivate 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 Extensions (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)
}
}