fix: bad fix for multi-window

This commit is contained in:
Michael Freno
2026-01-15 21:49:27 -05:00
parent d7d009d27a
commit 77bc2f9a92
5 changed files with 111 additions and 37 deletions

View File

@@ -18,6 +18,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
private var updateManager: UpdateManager? private var updateManager: UpdateManager?
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
private var hasStartedTimers = false private var hasStartedTimers = false
private var isSettingsWindowOpen = false
private var isOnboardingWindowOpen = false
// Logging manager // Logging manager
private let logger = LoggingManager.shared private let logger = LoggingManager.shared
@@ -31,6 +33,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
self.serviceContainer = ServiceContainer.shared self.serviceContainer = ServiceContainer.shared
self.windowManager = WindowManager.shared self.windowManager = WindowManager.shared
super.init() super.init()
// Setup window close observers
setupWindowCloseObservers()
} }
/// Initializer for testing with injectable dependencies /// Initializer for testing with injectable dependencies
@@ -54,6 +59,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
// Setup smart mode services through container // Setup smart mode services through container
serviceContainer.setupSmartModeServices() 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 // Initialize update manager after onboarding is complete
if settingsManager.settings.hasCompletedOnboarding { if settingsManager.settings.hasCompletedOnboarding {
updateManager = UpdateManager.shared updateManager = UpdateManager.shared
@@ -206,7 +217,20 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
} }
func openSettings(tab: Int = 0) { 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() handleMenuDismissal()
isSettingsWindowOpen = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
guard let self else { return } guard let self else { return }
@@ -215,7 +239,18 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
} }
func openOnboarding() { 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() 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 DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
guard let self else { return } guard let self else { return }
@@ -228,4 +263,31 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
windowManager.dismissOverlayReminder() 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
}
} }

View File

@@ -39,8 +39,16 @@ final class OnboardingWindowPresenter {
windowController = nil windowController = nil
return false return false
} }
// Ensure the window is brought to front and focused properly for menu bar apps
window.makeKeyAndOrderFront(nil) window.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true) NSApp.activate(ignoringOtherApps: true)
// Additional focus handling for menu bar applications
if let window = windowController?.window {
window.makeMain()
}
return true return true
} }
@@ -88,6 +96,9 @@ final class OnboardingWindowPresenter {
NotificationCenter.default.removeObserver(closeObserver) NotificationCenter.default.removeObserver(closeObserver)
} }
self?.closeObserver = nil self?.closeObserver = nil
// Notify AppDelegate that onboarding window closed
NotificationCenter.default.post(name: Notification.Name("OnboardingWindowDidClose"), object: nil)
} }
} }

View File

@@ -90,6 +90,9 @@ final class SettingsWindowPresenter {
Task { @MainActor [weak self] in Task { @MainActor [weak self] in
self?.windowController = nil self?.windowController = nil
self?.removeCloseObserver() 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 #if DEBUG
private func retriggerOnboarding() { private func retriggerOnboarding() {
OnboardingWindowPresenter.shared.close()
SettingsWindowPresenter.shared.close() SettingsWindowPresenter.shared.close()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
OnboardingWindowPresenter.shared.show(settingsManager: self.settingsManager) settingsManager.settings.hasCompletedOnboarding = false
} }
} }
#endif #endif

View File

@@ -88,24 +88,27 @@ struct MenuBarContentView: View {
.padding(.horizontal) .padding(.horizontal)
.padding(.top, 8) .padding(.top, 8)
ForEach(getSortedTimerIdentifiers(timerEngine: timerEngine), id: \.self) { ForEach(
timerEngine.map { getSortedTimerIdentifiers(timerEngine: $0) } ?? [],
id: \.self
) {
identifier in identifier in
if timerEngine.timerStates[identifier] != nil { if let engine = timerEngine, engine.timerStates[identifier] != nil {
TimerStatusRowWithIndividualControls( TimerStatusRowWithIndividualControls(
identifier: identifier, identifier: identifier,
timerEngine: timerEngine, timerEngine: engine,
settingsManager: settingsManager, settingsManager: settingsManager,
onSkip: { onSkip: {
timerEngine.skipNext(identifier: identifier) engine.skipNext(identifier: identifier)
}, },
onDevTrigger: { onDevTrigger: {
timerEngine.triggerReminder(for: identifier) engine.triggerReminder(for: identifier)
}, },
onTogglePause: { isPaused in onTogglePause: { isPaused in
if isPaused { if isPaused {
timerEngine.pauseTimer(identifier: identifier) engine.pauseTimer(identifier: identifier)
} else { } else {
timerEngine.resumeTimer(identifier: identifier) engine.resumeTimer(identifier: identifier)
} }
}, },
onTap: { onTap: {
@@ -127,18 +130,21 @@ struct MenuBarContentView: View {
// Controls // Controls
VStack(spacing: 4) { VStack(spacing: 4) {
Button(action: { Button(action: {
if isAllPaused(timerEngine: timerEngine) { if let engine = timerEngine {
timerEngine.resume() if isAllPaused(timerEngine: engine) {
} else { engine.resume()
timerEngine.pause() } else {
engine.pause()
}
} }
}) { }) {
HStack { HStack {
Image( Image(
systemName: isAllPaused(timerEngine: timerEngine) systemName: timerEngine.map { isAllPaused(timerEngine: $0) }
?? false
? "play.circle" : "pause.circle") ? "play.circle" : "pause.circle")
Text( Text(
isAllPaused(timerEngine: timerEngine) timerEngine.map { isAllPaused(timerEngine: $0) } ?? false
? "Resume All Timers" : "Pause All Timers") ? "Resume All Timers" : "Pause All Timers")
Spacer() 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 // 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 } return !activeStates.isEmpty && activeStates.allSatisfy { $0.isPaused }
} }
private func getSortedTimerIdentifiers(timerEngine: TimerEngine) -> [TimerIdentifier] { private func getSortedTimerIdentifiers(timerEngine: TimerEngine?) -> [TimerIdentifier] {
return timerEngine.timerStates.keys.sorted { id1, id2 in guard let engine = timerEngine else { return [] }
return engine.timerStates.keys.sorted { id1, id2 in
// Sort built-in timers before user timers // Sort built-in timers before user timers
switch (id1, id2) { switch (id1, id2) {
case (.builtIn(let t1), .builtIn(let t2)): case (.builtIn(let t1), .builtIn(let t2)):

21
run
View File

@@ -102,20 +102,13 @@ launch_app() {
if [ -d "$app_path" ]; then if [ -d "$app_path" ]; then
echo "🚀 Launching: $app_path" echo "🚀 Launching: $app_path"
if [ "$VERBOSE" = true ]; then echo "📝 Capturing application logs (Ctrl+C to stop - won't kill app)..."
echo "📝 Capturing application logs in terminal (Ctrl+C to stop)..." open "$app_path" &
open "$app_path" &
sleep 2
echo "Logs from Gaze.app will appear below (Ctrl+C to stop):" sleep 2
echo "================================================================" echo "================================================================"
/usr/bin/log stream --predicate "subsystem contains \"$APP_SUBSYSTEM\"" \ /usr/bin/log stream --predicate "subsystem contains \"$APP_SUBSYSTEM\"" \
--style compact 2>/dev/null --style compact 2>/dev/null
echo "================================================================"
echo "Application runtime logging stopped."
else
open "$app_path"
fi
else else
echo "⚠️ App not found at expected location, trying fallback..." echo "⚠️ App not found at expected location, trying fallback..."
open "$HOME/Library/Developer/Xcode/DerivedData/Gaze-*/Build/Products/Debug/Gaze.app" open "$HOME/Library/Developer/Xcode/DerivedData/Gaze-*/Build/Products/Debug/Gaze.app"
@@ -205,8 +198,6 @@ done
# Default to run if no action specified # Default to run if no action specified
if [ -z "$ACTION" ]; then if [ -z "$ACTION" ]; then
ACTION="run" ACTION="run"
# Default run action is always verbose with full logging
VERBOSE=true
fi fi
# Main execution # Main execution