From 8e5f6c671573f2dc58bc2e3010f80ee1fd32ad6e Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Wed, 14 Jan 2026 20:15:24 -0500 Subject: [PATCH] fix: settings nav button bug --- Gaze/AppDelegate.swift | 130 +++++++++--------- Gaze/Constants/EyeTrackingConstants.swift | 8 +- Gaze/Models/SettingsSection.swift | 10 +- .../Views/Containers/SettingsWindowView.swift | 2 - 4 files changed, 77 insertions(+), 73 deletions(-) diff --git a/Gaze/AppDelegate.swift b/Gaze/AppDelegate.swift index 9245313..4c098f8 100644 --- a/Gaze/AppDelegate.swift +++ b/Gaze/AppDelegate.swift @@ -5,9 +5,9 @@ // Created by Mike Freno on 1/7/26. // -import SwiftUI import AppKit import Combine +import SwiftUI @MainActor class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { @@ -19,36 +19,36 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { private var settingsWindowController: NSWindowController? private var cancellables = Set() private var hasStartedTimers = false - + // Smart Mode services private var fullscreenService: FullscreenDetectionService? private var idleService: IdleMonitoringService? private var usageTrackingService: UsageTrackingService? - + func applicationDidFinishLaunching(_ notification: Notification) { // Set activation policy to hide dock icon NSApplication.shared.setActivationPolicy(.accessory) - + timerEngine = TimerEngine(settingsManager: settingsManager) - + // Initialize Smart Mode services setupSmartModeServices() - + // Initialize update manager after onboarding is complete if settingsManager.settings.hasCompletedOnboarding { updateManager = UpdateManager.shared } - + setupLifecycleObservers() - + observeSettingsChanges() - + // Start timers if onboarding is complete if settingsManager.settings.hasCompletedOnboarding { startTimers() } } - + private func setupSmartModeServices() { fullscreenService = FullscreenDetectionService() idleService = IdleMonitoringService( @@ -57,49 +57,50 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { usageTrackingService = UsageTrackingService( resetThresholdMinutes: settingsManager.settings.smartMode.usageResetAfterMinutes ) - + // Connect idle service to usage tracking if let idleService = idleService { usageTrackingService?.setupIdleMonitoring(idleService) } - + // Connect services to timer engine timerEngine?.setupSmartMode( fullscreenService: fullscreenService, idleService: idleService ) - + // Observe smart mode settings changes settingsManager.$settings .map { $0.smartMode } .removeDuplicates() .sink { [weak self] smartMode in self?.idleService?.updateThreshold(minutes: smartMode.idleThresholdMinutes) - self?.usageTrackingService?.updateResetThreshold(minutes: smartMode.usageResetAfterMinutes) - + self?.usageTrackingService?.updateResetThreshold( + minutes: smartMode.usageResetAfterMinutes) + // Force state check when settings change to apply immediately self?.fullscreenService?.forceUpdate() self?.idleService?.forceUpdate() } .store(in: &cancellables) } - + func onboardingCompleted() { startTimers() - + // Start update checks after onboarding if updateManager == nil { updateManager = UpdateManager.shared } } - + private func startTimers() { guard !hasStartedTimers else { return } hasStartedTimers = true timerEngine?.start() observeReminderEvents() } - + private func observeSettingsChanges() { settingsManager.$settings .sink { [weak self] settings in @@ -114,12 +115,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { } .store(in: &cancellables) } - + func applicationWillTerminate(_ notification: Notification) { settingsManager.saveImmediately() timerEngine?.stop() } - + private func setupLifecycleObservers() { NSWorkspace.shared.notificationCenter.addObserver( self, @@ -127,7 +128,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { name: NSWorkspace.willSleepNotification, object: nil ) - + NSWorkspace.shared.notificationCenter.addObserver( self, selector: #selector(systemDidWake), @@ -135,16 +136,16 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { object: nil ) } - + @objc private func systemWillSleep() { timerEngine?.handleSystemSleep() settingsManager.saveImmediately() } - + @objc private func systemDidWake() { timerEngine?.handleSystemWake() } - + private func observeReminderEvents() { timerEngine?.$activeReminder .sink { [weak self] reminder in @@ -156,11 +157,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { } .store(in: &cancellables) } - + private func showReminder(_ event: ReminderEvent) { let contentView: AnyView let requiresFocus: Bool - + switch event { case .lookAwayTriggered(let countdownSeconds): contentView = AnyView( @@ -196,20 +197,21 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { } else { let sizePercentage = settingsManager.settings.subtleReminderSize.percentage contentView = AnyView( - UserTimerReminderView(timer: timer, sizePercentage: sizePercentage) { [weak self] in + UserTimerReminderView(timer: timer, sizePercentage: sizePercentage) { + [weak self] in self?.timerEngine?.dismissReminder() } ) requiresFocus = false } } - + showReminderWindow(contentView, requiresFocus: requiresFocus, isOverlay: requiresFocus) } - + private func showReminderWindow(_ content: AnyView, requiresFocus: Bool, isOverlay: Bool) { guard let screen = NSScreen.main else { return } - + let window: NSWindow if requiresFocus { window = KeyableWindow( @@ -226,29 +228,29 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { defer: false ) } - + window.identifier = WindowIdentifiers.reminder window.level = .floating window.isOpaque = false window.backgroundColor = .clear window.contentView = NSHostingView(rootView: content) window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] - + // Allow mouse events only for overlay reminders (they need dismiss button) // Subtle reminders should be completely transparent to mouse input window.acceptsMouseMovedEvents = requiresFocus window.ignoresMouseEvents = !requiresFocus - + let windowController = NSWindowController(window: window) windowController.showWindow(nil) - + if requiresFocus { window.makeKeyAndOrderFront(nil) NSApp.activate(ignoringOtherApps: true) } else { window.orderFront(nil) } - + // Track overlay and subtle reminders separately if isOverlay { overlayReminderWindowController?.close() @@ -258,53 +260,53 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { subtleReminderWindowController = windowController } } - + private func dismissOverlayReminder() { overlayReminderWindowController?.close() overlayReminderWindowController = nil } - + private func dismissSubtleReminder() { subtleReminderWindowController?.close() subtleReminderWindowController = nil } - + // Public method to open settings window func openSettings(tab: Int = 0) { // Post notification to close menu bar popover NotificationCenter.default.post(name: Notification.Name("CloseMenuBarPopover"), object: nil) - + // Dismiss overlay reminders to prevent them from blocking settings window // Overlay reminders are at .floating level which would sit above settings dismissOverlayReminder() - + // Small delay to allow menu bar to close before opening settings DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in self?.openSettingsWindow(tab: tab) } } - + // Public method to reopen onboarding window func openOnboarding() { NotificationCenter.default.post(name: Notification.Name("CloseMenuBarPopover"), object: nil) - + // Dismiss overlay reminders to prevent blocking onboarding window dismissOverlayReminder() - + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in guard let self = self else { return } - + if self.activateWindow(withIdentifier: WindowIdentifiers.onboarding) { return } - + let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 700, height: 700), styleMask: [.titled, .closable, .miniaturizable, .fullSizeContentView], backing: .buffered, defer: false ) - + window.identifier = WindowIdentifiers.onboarding window.titleVisibility = .hidden window.titlebarAppearsTransparent = true @@ -313,12 +315,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { window.contentView = NSHostingView( rootView: OnboardingContainerView(settingsManager: self.settingsManager) ) - + window.makeKeyAndOrderFront(nil) NSApp.activate(ignoringOtherApps: true) } } - + private func openSettingsWindow(tab: Int) { if let existingWindow = findWindow(withIdentifier: WindowIdentifiers.settings) { NotificationCenter.default.post( @@ -329,30 +331,34 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { NSApp.activate(ignoringOtherApps: true) return } - + let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 700, height: 700), - styleMask: [.titled, .closable, .miniaturizable, .resizable], + styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], backing: .buffered, defer: false ) - + window.identifier = WindowIdentifiers.settings - window.title = "Settings" + window.titleVisibility = .hidden + window.titlebarAppearsTransparent = true + window.toolbarStyle = .unified + window.toolbar = NSToolbar() window.center() window.setFrameAutosaveName("SettingsWindow") window.isReleasedWhenClosed = false + window.contentView = NSHostingView( rootView: SettingsWindowView(settingsManager: settingsManager, initialTab: tab) ) - + let windowController = NSWindowController(window: window) windowController.showWindow(nil) - + settingsWindowController = windowController - + NSApp.activate(ignoringOtherApps: true) - + NotificationCenter.default.addObserver( self, selector: #selector(settingsWindowWillCloseNotification(_:)), @@ -360,16 +366,16 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { object: window ) } - + @objc private func settingsWindowWillCloseNotification(_ notification: Notification) { settingsWindowController = nil } - + /// Finds a window by its identifier private func findWindow(withIdentifier identifier: NSUserInterfaceItemIdentifier) -> NSWindow? { return NSApplication.shared.windows.first { $0.identifier == identifier } } - + /// Brings window to front if it exists, returns true if found private func activateWindow(withIdentifier identifier: NSUserInterfaceItemIdentifier) -> Bool { guard let window = findWindow(withIdentifier: identifier) else { @@ -386,7 +392,7 @@ class KeyableWindow: NSWindow { override var canBecomeKey: Bool { return true } - + override var canBecomeMain: Bool { return true } @@ -397,7 +403,7 @@ class NonKeyWindow: NSWindow { override var canBecomeKey: Bool { return false } - + override var canBecomeMain: Bool { return false } diff --git a/Gaze/Constants/EyeTrackingConstants.swift b/Gaze/Constants/EyeTrackingConstants.swift index 9b81ae9..694b3b4 100644 --- a/Gaze/Constants/EyeTrackingConstants.swift +++ b/Gaze/Constants/EyeTrackingConstants.swift @@ -25,20 +25,20 @@ enum EyeTrackingConstants { /// Pitch threshold for looking UP (above screen). /// Since camera is at top, looking at screen is negative pitch. /// Values > 0.1 imply looking straight ahead or up (away from screen). - static let pitchUpThreshold: Double = 0.1 + static let pitchUpThreshold: Double = 0.5 /// Pitch threshold for looking DOWN (at keyboard/lap). /// Values < -0.45 imply looking too far down. - static let pitchDownThreshold: Double = -0.2 + static let pitchDownThreshold: Double = -0.9 // MARK: - Pupil Tracking Thresholds /// Minimum horizontal pupil ratio (0.0 = right edge, 1.0 = left edge) /// Values below this are considered looking right (camera view) /// Tightened from 0.25 to 0.35 - static let minPupilRatio: Double = 0.35 + static let minPupilRatio: Double = 0.45 /// Maximum horizontal pupil ratio /// Values above this are considered looking left (camera view) /// Tightened from 0.75 to 0.65 - static let maxPupilRatio: Double = 0.65 + static let maxPupilRatio: Double = 0.55 } diff --git a/Gaze/Models/SettingsSection.swift b/Gaze/Models/SettingsSection.swift index ef10429..5e91dec 100644 --- a/Gaze/Models/SettingsSection.swift +++ b/Gaze/Models/SettingsSection.swift @@ -12,12 +12,12 @@ enum SettingsSection: Int, CaseIterable, Identifiable { case lookAway = 1 case blink = 2 case posture = 3 - case enforceMode = 4 - case userTimers = 5 + case userTimers = 4 + case enforceMode = 5 case smartMode = 6 - + var id: Int { rawValue } - + var title: String { switch self { case .general: return "General" @@ -29,7 +29,7 @@ enum SettingsSection: Int, CaseIterable, Identifiable { case .smartMode: return "Smart Mode" } } - + var iconName: String { switch self { case .general: return "gearshape.fill" diff --git a/Gaze/Views/Containers/SettingsWindowView.swift b/Gaze/Views/Containers/SettingsWindowView.swift index 8872d9d..035bb9b 100644 --- a/Gaze/Views/Containers/SettingsWindowView.swift +++ b/Gaze/Views/Containers/SettingsWindowView.swift @@ -24,12 +24,10 @@ struct SettingsWindowView: View { Label(section.title, systemImage: section.iconName) } } - .navigationTitle("Settings") .listStyle(.sidebar) } detail: { detailView(for: selectedSection) } - .navigationSplitViewColumnWidth(min: 200, ideal: 250, max: 300) Divider()