From 5c9f9720dc2ecc839d4fe5df424ea17bab568ddc Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 17 Jun 2025 12:49:36 +0200 Subject: [PATCH] Settings battle stations vs macOS idiocracy --- VibeTunnel/Utilities/SettingsOpener.swift | 135 ++++++++++++++++++---- 1 file changed, 111 insertions(+), 24 deletions(-) diff --git a/VibeTunnel/Utilities/SettingsOpener.swift b/VibeTunnel/Utilities/SettingsOpener.swift index c1ff2757..aa8dde51 100644 --- a/VibeTunnel/Utilities/SettingsOpener.swift +++ b/VibeTunnel/Utilities/SettingsOpener.swift @@ -1,4 +1,4 @@ -import AppKit +@preconcurrency import AppKit import Foundation import SwiftUI @@ -15,36 +15,122 @@ enum SettingsOpener { /// 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() { - // Temporarily switch to regular app to ensure window comes to front + // Use modal approach for guaranteed focus + openSettingsWithModal() + } + + /// Opens settings window using modal session for guaranteed focus + static func openSettingsWithModal() { + // Store current activation policy let currentPolicy = NSApp.activationPolicy() + + // Switch to regular app mode NSApp.setActivationPolicy(.regular) NSApp.activate(ignoringOtherApps: true) - - // 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() - - // Restore activation policy after a delay - try? await Task.sleep(for: .milliseconds(200)) - NSApp.setActivationPolicy(currentPolicy) - } - } else { + + // Try the direct menu item approach first + let openedViaMenu = openSettingsViaMenuItem() + + if !openedViaMenu { // Fallback to notification approach NotificationCenter.default.post(name: .openSettingsRequest, object: nil) - - Task { - try? await Task.sleep(for: .milliseconds(150)) - focusSettingsWindow() - - // Restore activation policy after a delay - try? await Task.sleep(for: .milliseconds(200)) + } + + // Wait for window to appear and run modal session + Task { @MainActor in + // Give time for window creation + try? await Task.sleep(for: .milliseconds(200)) + + if let settingsWindow = findSettingsWindow() { + // Configure window for modal presentation + settingsWindow.center() + settingsWindow.makeKeyAndOrderFront(nil) + settingsWindow.level = .modalPanel + settingsWindow.collectionBehavior = [.moveToActiveSpace, .canJoinAllSpaces] + + // Begin modal session + let session = NSApp.beginModalSession(for: settingsWindow) + + // Set up observer to end modal when window closes + let closeObserver = NotificationCenter.default.addObserver( + forName: NSWindow.willCloseNotification, + object: settingsWindow, + queue: .main + ) { _ in + Task { @MainActor in + NSApp.endModalSession(session) + settingsWindow.level = .normal + settingsWindow.collectionBehavior = [] + + // Restore activation policy + NSApp.setActivationPolicy(currentPolicy) + } + } + + // Run modal loop in background to not block + Task { @MainActor in + // Small initial delay + try? await Task.sleep(for: .milliseconds(100)) + + while settingsWindow.isVisible { + // Run one iteration of the modal session + let result = NSApp.runModalSession(session) + if result != .continue { + break + } + + // Small delay to prevent busy loop + try? await Task.sleep(for: .milliseconds(100)) + } + + // Clean up when loop exits + NSApp.endModalSession(session) + settingsWindow.level = .normal + settingsWindow.collectionBehavior = [] + NotificationCenter.default.removeObserver(closeObserver) + + // Restore activation policy if window closed + if !settingsWindow.isVisible { + NSApp.setActivationPolicy(currentPolicy) + } + } + + // Ensure window is properly focused after modal setup + settingsWindow.makeKeyAndOrderFront(nil) + NSApp.activate(ignoringOtherApps: true) + + } else { + // No window found, restore activation policy NSApp.setActivationPolicy(currentPolicy) } } } + + /// Finds the settings window using multiple detection methods + private static func findSettingsWindow() -> NSWindow? { + // Try multiple methods to find the window + return NSApp.windows.first { window in + // Check by identifier + if window.identifier?.rawValue == settingsWindowIdentifier { + return true + } + + // Check by title + if window.isVisible && window.styleMask.contains(.titled) && + (window.title.localizedCaseInsensitiveContains("settings") || + window.title.localizedCaseInsensitiveContains("preferences")) { + return true + } + + // Check by content view controller type + if let contentVC = window.contentViewController, + String(describing: type(of: contentVC)).contains("Settings") { + return true + } + + return false + } + } /// Opens settings via the native menu item (more reliable) private static func openSettingsViaMenuItem() -> Bool { @@ -129,11 +215,12 @@ enum SettingsOpener { /// Opens the Settings window and navigates to a specific tab static func openSettingsTab(_ tab: SettingsTab) { - openSettings() + // Use modal approach for guaranteed focus + openSettingsWithModal() Task { // Small delay to ensure the settings window is fully initialized - try? await Task.sleep(for: .milliseconds(150)) + try? await Task.sleep(for: .milliseconds(300)) NotificationCenter.default.post( name: .openSettingsTab, object: tab