mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
Improve setting front logic
This commit is contained in:
parent
b5d043cba8
commit
936806edd6
1 changed files with 133 additions and 22 deletions
|
|
@ -5,38 +5,65 @@ import SwiftUI
|
||||||
/// Helper to open the Settings window programmatically when SettingsLink cannot be used
|
/// Helper to open the Settings window programmatically when SettingsLink cannot be used
|
||||||
@MainActor
|
@MainActor
|
||||||
enum SettingsOpener {
|
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
|
/// 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)
|
/// This is needed for cases where we can't use SettingsLink (e.g., from notifications)
|
||||||
static func openSettings() {
|
static func openSettings() {
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
|
|
||||||
// Post notification to trigger openSettings environment action
|
// Try the direct menu item approach first (from VibeMeter)
|
||||||
NotificationCenter.default.post(name: .openSettingsRequest, object: nil)
|
if openSettingsViaMenuItem() {
|
||||||
|
// Successfully opened via menu item
|
||||||
// Ensure settings window comes to front after opening
|
|
||||||
Task {
|
Task {
|
||||||
try? await Task.sleep(for: .milliseconds(100))
|
try? await Task.sleep(for: .milliseconds(100))
|
||||||
bringSettingsToFront()
|
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
|
/// Opens settings via the native menu item (more reliable)
|
||||||
static func bringSettingsToFront() {
|
private static func openSettingsViaMenuItem() -> Bool {
|
||||||
// Find the settings window by looking for the preferences window
|
let kAppMenuInternalIdentifier = "app"
|
||||||
if let settingsWindow = NSApp.windows.first(where: { window in
|
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.isVisible &&
|
||||||
(window.styleMask.contains(.titled) &&
|
window.styleMask.contains(.titled) &&
|
||||||
window.title.localizedCaseInsensitiveContains("settings") ||
|
(window.title.localizedCaseInsensitiveContains("settings") ||
|
||||||
window.title.localizedCaseInsensitiveContains("preferences"))
|
window.title.localizedCaseInsensitiveContains("preferences"))
|
||||||
}) {
|
}) {
|
||||||
settingsWindow.makeKeyAndOrderFront(nil)
|
settingsWindow.makeKeyAndOrderFront(nil)
|
||||||
settingsWindow.level = .floating
|
settingsWindow.orderFrontRegardless()
|
||||||
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
// Reset to normal level after a brief moment
|
|
||||||
Task {
|
|
||||||
try? await Task.sleep(for: .milliseconds(50))
|
|
||||||
settingsWindow.level = .normal
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,7 +73,7 @@ enum SettingsOpener {
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
// Small delay to ensure the settings window is fully initialized
|
// 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(
|
NotificationCenter.default.post(
|
||||||
name: .openSettingsTab,
|
name: .openSettingsTab,
|
||||||
object: tab
|
object: tab
|
||||||
|
|
@ -100,7 +127,7 @@ struct HiddenWindowView: View {
|
||||||
|
|
||||||
// Additional check to bring settings to front after environment action
|
// Additional check to bring settings to front after environment action
|
||||||
try? await Task.sleep(for: .milliseconds(150))
|
try? await Task.sleep(for: .milliseconds(150))
|
||||||
SettingsOpener.bringSettingsToFront()
|
SettingsOpener.focusSettingsWindow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -112,3 +139,87 @@ struct HiddenWindowView: View {
|
||||||
extension Notification.Name {
|
extension Notification.Name {
|
||||||
static let openSettingsRequest = Notification.Name("openSettingsRequest")
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue