diff --git a/Gaze/AppDelegate.swift b/Gaze/AppDelegate.swift index 609bc3f..c816ab5 100644 --- a/Gaze/AppDelegate.swift +++ b/Gaze/AppDelegate.swift @@ -18,6 +18,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { private var updateManager: UpdateManager? private var cancellables = Set() private var hasStartedTimers = false + private var isSettingsWindowOpen = false + private var isOnboardingWindowOpen = false // Logging manager private let logger = LoggingManager.shared @@ -26,11 +28,14 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { private var settingsManager: any SettingsProviding { serviceContainer.settingsManager } - + override init() { self.serviceContainer = ServiceContainer.shared self.windowManager = WindowManager.shared super.init() + + // Setup window close observers + setupWindowCloseObservers() } /// Initializer for testing with injectable dependencies @@ -39,7 +44,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { self.windowManager = windowManager super.init() } - + func applicationDidFinishLaunching(_ notification: Notification) { // Set activation policy to hide dock icon NSApplication.shared.setActivationPolicy(.accessory) @@ -54,6 +59,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { // Setup smart mode services through container serviceContainer.setupSmartModeServices() + // Check if onboarding needs to be shown automatically + if !settingsManager.settings.hasCompletedOnboarding { + // Set the flag to indicate we expect an onboarding window + isOnboardingWindowOpen = true + } + // Initialize update manager after onboarding is complete if settingsManager.settings.hasCompletedOnboarding { updateManager = UpdateManager.shared @@ -206,7 +217,20 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { } func openSettings(tab: Int = 0) { + // If settings window is already open, focus it instead of opening new one + if isSettingsWindowOpen { + // Try to focus existing window + DispatchQueue.main.async { + NotificationCenter.default.post( + name: Notification.Name("SwitchToSettingsTab"), + object: tab + ) + } + return + } + handleMenuDismissal() + isSettingsWindowOpen = true DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in guard let self else { return } @@ -215,7 +239,18 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { } func openOnboarding() { + // If onboarding window is already open, focus it instead of opening new one + if isOnboardingWindowOpen { + // Try to activate existing window + DispatchQueue.main.async { + OnboardingWindowPresenter.shared.activateIfPresent() + } + return + } + handleMenuDismissal() + // Explicitly set the flag to true when we're about to show the onboarding window + isOnboardingWindowOpen = true DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in guard let self else { return } @@ -227,5 +262,32 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { NotificationCenter.default.post(name: Notification.Name("CloseMenuBarPopover"), object: nil) windowManager.dismissOverlayReminder() } + + private func setupWindowCloseObservers() { + // Observe settings window closing + NotificationCenter.default.addObserver( + self, + selector: #selector(settingsWindowDidClose), + name: Notification.Name("SettingsWindowDidClose"), + object: nil + ) + + // Observe onboarding window closing + NotificationCenter.default.addObserver( + self, + selector: #selector(onboardingWindowDidClose), + name: Notification.Name("OnboardingWindowDidClose"), + object: nil + ) + } + + @objc private func settingsWindowDidClose() { + isSettingsWindowOpen = false + } + + @objc private func onboardingWindowDidClose() { + // Reset the flag when we receive the close notification + isOnboardingWindowOpen = false + } } diff --git a/Gaze/Views/Containers/OnboardingContainerView.swift b/Gaze/Views/Containers/OnboardingContainerView.swift index 76b29e1..1b004dd 100644 --- a/Gaze/Views/Containers/OnboardingContainerView.swift +++ b/Gaze/Views/Containers/OnboardingContainerView.swift @@ -39,8 +39,16 @@ final class OnboardingWindowPresenter { windowController = nil return false } + + // Ensure the window is brought to front and focused properly for menu bar apps window.makeKeyAndOrderFront(nil) NSApp.activate(ignoringOtherApps: true) + + // Additional focus handling for menu bar applications + if let window = windowController?.window { + window.makeMain() + } + return true } @@ -88,6 +96,9 @@ final class OnboardingWindowPresenter { NotificationCenter.default.removeObserver(closeObserver) } self?.closeObserver = nil + + // Notify AppDelegate that onboarding window closed + NotificationCenter.default.post(name: Notification.Name("OnboardingWindowDidClose"), object: nil) } } diff --git a/Gaze/Views/Containers/SettingsWindowView.swift b/Gaze/Views/Containers/SettingsWindowView.swift index 827823b..5e654aa 100644 --- a/Gaze/Views/Containers/SettingsWindowView.swift +++ b/Gaze/Views/Containers/SettingsWindowView.swift @@ -90,6 +90,9 @@ final class SettingsWindowPresenter { Task { @MainActor [weak self] in self?.windowController = nil self?.removeCloseObserver() + + // Notify AppDelegate that settings window closed + NotificationCenter.default.post(name: Notification.Name("SettingsWindowDidClose"), object: nil) } } } @@ -206,11 +209,10 @@ struct SettingsWindowView: View { #if DEBUG private func retriggerOnboarding() { - OnboardingWindowPresenter.shared.close() SettingsWindowPresenter.shared.close() DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - OnboardingWindowPresenter.shared.show(settingsManager: self.settingsManager) + settingsManager.settings.hasCompletedOnboarding = false } } #endif diff --git a/Gaze/Views/MenuBar/MenuBarContentView.swift b/Gaze/Views/MenuBar/MenuBarContentView.swift index 3203238..6fe7265 100644 --- a/Gaze/Views/MenuBar/MenuBarContentView.swift +++ b/Gaze/Views/MenuBar/MenuBarContentView.swift @@ -88,24 +88,27 @@ struct MenuBarContentView: View { .padding(.horizontal) .padding(.top, 8) - ForEach(getSortedTimerIdentifiers(timerEngine: timerEngine), id: \.self) { + ForEach( + timerEngine.map { getSortedTimerIdentifiers(timerEngine: $0) } ?? [], + id: \.self + ) { identifier in - if timerEngine.timerStates[identifier] != nil { + if let engine = timerEngine, engine.timerStates[identifier] != nil { TimerStatusRowWithIndividualControls( identifier: identifier, - timerEngine: timerEngine, + timerEngine: engine, settingsManager: settingsManager, onSkip: { - timerEngine.skipNext(identifier: identifier) + engine.skipNext(identifier: identifier) }, onDevTrigger: { - timerEngine.triggerReminder(for: identifier) + engine.triggerReminder(for: identifier) }, onTogglePause: { isPaused in if isPaused { - timerEngine.pauseTimer(identifier: identifier) + engine.pauseTimer(identifier: identifier) } else { - timerEngine.resumeTimer(identifier: identifier) + engine.resumeTimer(identifier: identifier) } }, onTap: { @@ -127,18 +130,21 @@ struct MenuBarContentView: View { // Controls VStack(spacing: 4) { Button(action: { - if isAllPaused(timerEngine: timerEngine) { - timerEngine.resume() - } else { - timerEngine.pause() + if let engine = timerEngine { + if isAllPaused(timerEngine: engine) { + engine.resume() + } else { + engine.pause() + } } }) { HStack { Image( - systemName: isAllPaused(timerEngine: timerEngine) + systemName: timerEngine.map { isAllPaused(timerEngine: $0) } + ?? false ? "play.circle" : "pause.circle") Text( - isAllPaused(timerEngine: timerEngine) + timerEngine.map { isAllPaused(timerEngine: $0) } ?? false ? "Resume All Timers" : "Pause All Timers") Spacer() } @@ -217,14 +223,16 @@ struct MenuBarContentView: View { } } - private func isAllPaused(timerEngine: TimerEngine) -> Bool { + private func isAllPaused(timerEngine: TimerEngine?) -> Bool { // Check if all timers are paused - let activeStates = timerEngine.timerStates.values.filter { $0.isActive } + guard let engine = timerEngine else { return false } + let activeStates = engine.timerStates.values.filter { $0.isActive } return !activeStates.isEmpty && activeStates.allSatisfy { $0.isPaused } } - private func getSortedTimerIdentifiers(timerEngine: TimerEngine) -> [TimerIdentifier] { - return timerEngine.timerStates.keys.sorted { id1, id2 in + private func getSortedTimerIdentifiers(timerEngine: TimerEngine?) -> [TimerIdentifier] { + guard let engine = timerEngine else { return [] } + return engine.timerStates.keys.sorted { id1, id2 in // Sort built-in timers before user timers switch (id1, id2) { case (.builtIn(let t1), .builtIn(let t2)): diff --git a/run b/run index ffe2c04..542ce30 100755 --- a/run +++ b/run @@ -102,20 +102,13 @@ launch_app() { if [ -d "$app_path" ]; then echo "🚀 Launching: $app_path" - if [ "$VERBOSE" = true ]; then - echo "📝 Capturing application logs in terminal (Ctrl+C to stop)..." - open "$app_path" & - sleep 2 - - echo "Logs from Gaze.app will appear below (Ctrl+C to stop):" - echo "================================================================" - /usr/bin/log stream --predicate "subsystem contains \"$APP_SUBSYSTEM\"" \ - --style compact 2>/dev/null - echo "================================================================" - echo "Application runtime logging stopped." - else - open "$app_path" - fi + echo "📝 Capturing application logs (Ctrl+C to stop - won't kill app)..." + open "$app_path" & + + sleep 2 + echo "================================================================" + /usr/bin/log stream --predicate "subsystem contains \"$APP_SUBSYSTEM\"" \ + --style compact 2>/dev/null else echo "⚠️ App not found at expected location, trying fallback..." open "$HOME/Library/Developer/Xcode/DerivedData/Gaze-*/Build/Products/Debug/Gaze.app" @@ -205,8 +198,6 @@ done # Default to run if no action specified if [ -z "$ACTION" ]; then ACTION="run" - # Default run action is always verbose with full logging - VERBOSE=true fi # Main execution