From c7030276eb6ed55ddd846fdf86c877823641ab58 Mon Sep 17 00:00:00 2001 From: Beingpax Date: Thu, 30 Oct 2025 21:12:59 +0545 Subject: [PATCH] Refactor: Centralize window and menu bar management --- VoiceInk/AppDelegate.swift | 36 +++------- VoiceInk/MenuBarManager.swift | 130 ++++++++-------------------------- VoiceInk/VoiceInk.swift | 10 +-- VoiceInk/WindowManager.swift | 103 ++++++++++++++++++++------- 4 files changed, 118 insertions(+), 161 deletions(-) diff --git a/VoiceInk/AppDelegate.swift b/VoiceInk/AppDelegate.swift index 6fa851e..fb15222 100644 --- a/VoiceInk/AppDelegate.swift +++ b/VoiceInk/AppDelegate.swift @@ -3,46 +3,28 @@ import SwiftUI import UniformTypeIdentifiers class AppDelegate: NSObject, NSApplicationDelegate { + weak var menuBarManager: MenuBarManager? + func applicationDidFinishLaunching(_ notification: Notification) { - updateActivationPolicy() + menuBarManager?.applyActivationPolicy() } func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { - updateActivationPolicy() + menuBarManager?.applyActivationPolicy() if !flag { - createMainWindowIfNeeded() + menuBarManager?.focusMainWindow() } return true } func applicationDidBecomeActive(_ notification: Notification) { - updateActivationPolicy() + menuBarManager?.applyActivationPolicy() } func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return false } - - private func updateActivationPolicy() { - let isMenuBarOnly = UserDefaults.standard.bool(forKey: "IsMenuBarOnly") - if isMenuBarOnly { - NSApp.setActivationPolicy(.accessory) - } else { - NSApp.setActivationPolicy(.regular) - } - } - - private func createMainWindowIfNeeded() { - if NSApp.windows.isEmpty { - let contentView = ContentView() - let hostingView = NSHostingView(rootView: contentView) - let window = WindowManager.shared.createMainWindow(contentView: hostingView) - window.makeKeyAndOrderFront(nil) - } else { - NSApp.windows.first?.makeKeyAndOrderFront(nil) - } - } // Stash URL when app cold-starts to avoid spawning a new window/tab var pendingOpenFileURL: URL? @@ -52,15 +34,15 @@ class AppDelegate: NSObject, NSApplicationDelegate { return } - NSApp.activate(ignoringOtherApps: true) + NSApplication.shared.activate(ignoringOtherApps: true) - if NSApp.windows.isEmpty { + if WindowManager.shared.currentMainWindow() == nil { // Cold start: do NOT create a window here to avoid extra window/tab. // Defer to SwiftUI’s WindowGroup-created ContentView and let it process this later. pendingOpenFileURL = url } else { // Running: focus current window and route in-place to Transcribe Audio - NSApp.windows.first?.makeKeyAndOrderFront(nil) + menuBarManager?.focusMainWindow() NotificationCenter.default.post(name: .navigateToDestination, object: nil, userInfo: ["destination": "Transcribe Audio"]) DispatchQueue.main.async { NotificationCenter.default.post(name: .openFileForTranscription, object: nil, userInfo: ["url": url]) diff --git a/VoiceInk/MenuBarManager.swift b/VoiceInk/MenuBarManager.swift index 358eb30..5507ab8 100644 --- a/VoiceInk/MenuBarManager.swift +++ b/VoiceInk/MenuBarManager.swift @@ -1,6 +1,4 @@ import SwiftUI -import LaunchAtLogin -import SwiftData import AppKit class MenuBarManager: ObservableObject { @@ -11,27 +9,9 @@ class MenuBarManager: ObservableObject { } } - private var updaterViewModel: UpdaterViewModel - private var whisperState: WhisperState - private var container: ModelContainer - private var enhancementService: AIEnhancementService - private var aiService: AIService - private var hotkeyManager: HotkeyManager - private var mainWindow: NSWindow? // Store window reference - init(updaterViewModel: UpdaterViewModel, - whisperState: WhisperState, - container: ModelContainer, - enhancementService: AIEnhancementService, - aiService: AIService, - hotkeyManager: HotkeyManager) { + init() { self.isMenuBarOnly = UserDefaults.standard.bool(forKey: "IsMenuBarOnly") - self.updaterViewModel = updaterViewModel - self.whisperState = whisperState - self.container = container - self.enhancementService = enhancementService - self.aiService = aiService - self.hotkeyManager = hotkeyManager updateAppActivationPolicy() } @@ -39,23 +19,36 @@ class MenuBarManager: ObservableObject { isMenuBarOnly.toggle() } + func applyActivationPolicy() { + updateAppActivationPolicy() + } + + func focusMainWindow() { + applyActivationPolicy() + DispatchQueue.main.async { + if WindowManager.shared.showMainWindow() == nil { + print("MenuBarManager: Unable to locate main window to focus") + } + } + } + private func updateAppActivationPolicy() { - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - - // Clean up existing window if switching to menu bar mode - if self.isMenuBarOnly && self.mainWindow != nil { - self.mainWindow?.close() - self.mainWindow = nil - } - - // Update activation policy + let applyPolicy = { [weak self] in + guard let self else { return } + let application = NSApplication.shared if self.isMenuBarOnly { - NSApp.setActivationPolicy(.accessory) + application.setActivationPolicy(.accessory) + WindowManager.shared.hideMainWindow() } else { - NSApp.setActivationPolicy(.regular) + application.setActivationPolicy(.regular) } } + + if Thread.isMainThread { + applyPolicy() + } else { + DispatchQueue.main.async(execute: applyPolicy) + } } func openMainWindowAndNavigate(to destination: String) { @@ -64,31 +57,13 @@ class MenuBarManager: ObservableObject { DispatchQueue.main.async { [weak self] in guard let self = self else { return } - if self.isMenuBarOnly { - NSApp.setActivationPolicy(.accessory) - } else { - NSApp.setActivationPolicy(.regular) + self.applyActivationPolicy() + + guard WindowManager.shared.showMainWindow() != nil else { + print("MenuBarManager: Unable to show main window for navigation") + return } - // Activate the app - NSApp.activate(ignoringOtherApps: true) - - // Clean up existing window if it's no longer valid - if let existingWindow = self.mainWindow, !existingWindow.isVisible { - self.mainWindow = nil - } - - // Get or create main window - if self.mainWindow == nil { - self.mainWindow = self.createMainWindow() - } - - guard let window = self.mainWindow else { return } - - // Make the window key and order front - window.makeKeyAndOrderFront(nil) - window.center() // Always center the window for consistent positioning - // Post a notification to navigate to the desired destination DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { NotificationCenter.default.post( @@ -100,47 +75,4 @@ class MenuBarManager: ObservableObject { } } } - - private func createMainWindow() -> NSWindow { - print("MenuBarManager: Creating new main window") - - // Create the content view with all required environment objects - let contentView = ContentView() - .environmentObject(whisperState) - .environmentObject(hotkeyManager) - .environmentObject(self) - .environmentObject(updaterViewModel) - .environmentObject(enhancementService) - .environmentObject(aiService) - .environment(\.modelContext, ModelContext(container)) - - // Create window using WindowManager - let hostingView = NSHostingView(rootView: contentView) - let window = WindowManager.shared.createMainWindow(contentView: hostingView) - - // Set window delegate to handle window closing - let delegate = WindowDelegate { [weak self] in - self?.mainWindow = nil - } - window.delegate = delegate - - print("MenuBarManager: Window setup complete") - - return window - } } - -// Window delegate to handle window closing -class WindowDelegate: NSObject, NSWindowDelegate { - let onClose: () -> Void - - init(onClose: @escaping () -> Void) { - self.onClose = onClose - super.init() - } - - func windowWillClose(_ notification: Notification) { - onClose() - } -} - diff --git a/VoiceInk/VoiceInk.swift b/VoiceInk/VoiceInk.swift index 87ff8f1..dbe22ac 100644 --- a/VoiceInk/VoiceInk.swift +++ b/VoiceInk/VoiceInk.swift @@ -79,15 +79,9 @@ struct VoiceInkApp: App { let hotkeyManager = HotkeyManager(whisperState: whisperState) _hotkeyManager = StateObject(wrappedValue: hotkeyManager) - let menuBarManager = MenuBarManager( - updaterViewModel: updaterViewModel, - whisperState: whisperState, - container: container, - enhancementService: enhancementService, - aiService: aiService, - hotkeyManager: hotkeyManager - ) + let menuBarManager = MenuBarManager() _menuBarManager = StateObject(wrappedValue: menuBarManager) + appDelegate.menuBarManager = menuBarManager let activeWindowService = ActiveWindowService.shared activeWindowService.configure(with: enhancementService) diff --git a/VoiceInk/WindowManager.swift b/VoiceInk/WindowManager.swift index 1d152a3..a5a6f49 100644 --- a/VoiceInk/WindowManager.swift +++ b/VoiceInk/WindowManager.swift @@ -1,10 +1,18 @@ import SwiftUI import AppKit -class WindowManager { +class WindowManager: NSObject { static let shared = WindowManager() - private init() {} + private static let mainWindowIdentifier = NSUserInterfaceItemIdentifier("com.prakashjoshipax.voiceink.mainWindow") + private static let mainWindowAutosaveName = NSWindow.FrameAutosaveName("VoiceInkMainWindowFrame") + + private weak var mainWindow: NSWindow? + private var didApplyInitialPlacement = false + + private override init() { + super.init() + } func configureWindow(_ window: NSWindow) { let requiredStyleMask: NSWindow.StyleMask = [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView] @@ -19,6 +27,9 @@ class WindowManager { window.isOpaque = true window.isMovableByWindowBackground = false window.minSize = NSSize(width: 0, height: 0) + window.setFrameAutosaveName(Self.mainWindowAutosaveName) + applyInitialPlacementIfNeeded(to: window) + registerMainWindowIfNeeded(window) window.orderFrontRegardless() } @@ -37,38 +48,76 @@ class WindowManager { window.minSize = NSSize(width: 900, height: 780) window.makeKeyAndOrderFront(nil) } + + func registerMainWindow(_ window: NSWindow) { + mainWindow = window + window.identifier = Self.mainWindowIdentifier + window.delegate = self + } - func createMainWindow(contentView: NSView) -> NSWindow { - let defaultSize = NSSize(width: 940, height: 780) - let screenFrame = NSScreen.main?.visibleFrame ?? NSRect(x: 0, y: 0, width: 1200, height: 800) - let xPosition = (screenFrame.width - defaultSize.width) / 2 + screenFrame.minX - let yPosition = (screenFrame.height - defaultSize.height) / 2 + screenFrame.minY - - let window = NSWindow( - contentRect: NSRect(x: xPosition, y: yPosition, width: defaultSize.width, height: defaultSize.height), - styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], - backing: .buffered, - defer: false - ) - - configureWindow(window) - window.contentView = contentView - - let delegate = WindowStateDelegate() - window.delegate = delegate + func showMainWindow() -> NSWindow? { + guard let window = resolveMainWindow() else { + return nil + } + window.makeKeyAndOrderFront(nil) + NSApplication.shared.activate(ignoringOtherApps: true) return window } -} - -class WindowStateDelegate: NSObject, NSWindowDelegate { - func windowWillClose(_ notification: Notification) { - guard let window = notification.object as? NSWindow else { return } + + func hideMainWindow() { + guard let window = resolveMainWindow() else { + return + } window.orderOut(nil) } + func currentMainWindow() -> NSWindow? { + resolveMainWindow() + } + + private func registerMainWindowIfNeeded(_ window: NSWindow) { + // Only register the primary content window, identified by the hidden title bar style + if window.identifier == nil || window.identifier != Self.mainWindowIdentifier { + registerMainWindow(window) + } + } + + private func applyInitialPlacementIfNeeded(to window: NSWindow) { + guard !didApplyInitialPlacement else { return } + // Attempt to restore previous frame if one exists; otherwise fall back to a centered placement + if !window.setFrameUsingName(Self.mainWindowAutosaveName) { + window.center() + } + didApplyInitialPlacement = true + } + + private func resolveMainWindow() -> NSWindow? { + if let window = mainWindow { + return window + } + + if let window = NSApplication.shared.windows.first(where: { $0.identifier == Self.mainWindowIdentifier }) { + mainWindow = window + window.delegate = self + return window + } + + return nil + } +} + +extension WindowManager: NSWindowDelegate { + func windowWillClose(_ notification: Notification) { + guard let window = notification.object as? NSWindow else { return } + if window.identifier == Self.mainWindowIdentifier { + window.orderOut(nil) + } + } + func windowDidBecomeKey(_ notification: Notification) { - guard let _ = notification.object as? NSWindow else { return } - NSApp.activate(ignoringOtherApps: true) + guard let window = notification.object as? NSWindow, + window.identifier == Self.mainWindowIdentifier else { return } + NSApplication.shared.activate(ignoringOtherApps: true) } }