Merge pull request #350 from Beingpax/refactor-window-management

Refactor window management
This commit is contained in:
Prakash Joshi Pax 2025-10-31 11:15:14 +05:45 committed by GitHub
commit 2dedff0c84
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 138 additions and 164 deletions

View File

@ -3,46 +3,30 @@ import SwiftUI
import UniformTypeIdentifiers import UniformTypeIdentifiers
class AppDelegate: NSObject, NSApplicationDelegate { class AppDelegate: NSObject, NSApplicationDelegate {
weak var menuBarManager: MenuBarManager?
func applicationDidFinishLaunching(_ notification: Notification) { func applicationDidFinishLaunching(_ notification: Notification) {
updateActivationPolicy() menuBarManager?.applyActivationPolicy()
} }
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
updateActivationPolicy() menuBarManager?.applyActivationPolicy()
if !flag { if !flag, let menuBarManager = menuBarManager, !menuBarManager.isMenuBarOnly {
createMainWindowIfNeeded() if WindowManager.shared.showMainWindow() != nil {
return false
}
} }
return true return true
} }
func applicationDidBecomeActive(_ notification: Notification) { func applicationDidBecomeActive(_ notification: Notification) {
updateActivationPolicy() menuBarManager?.applyActivationPolicy()
} }
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return false 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 // Stash URL when app cold-starts to avoid spawning a new window/tab
var pendingOpenFileURL: URL? var pendingOpenFileURL: URL?
@ -52,15 +36,15 @@ class AppDelegate: NSObject, NSApplicationDelegate {
return 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. // Cold start: do NOT create a window here to avoid extra window/tab.
// Defer to SwiftUIs WindowGroup-created ContentView and let it process this later. // Defer to SwiftUIs WindowGroup-created ContentView and let it process this later.
pendingOpenFileURL = url pendingOpenFileURL = url
} else { } else {
// Running: focus current window and route in-place to Transcribe Audio // 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"]) NotificationCenter.default.post(name: .navigateToDestination, object: nil, userInfo: ["destination": "Transcribe Audio"])
DispatchQueue.main.async { DispatchQueue.main.async {
NotificationCenter.default.post(name: .openFileForTranscription, object: nil, userInfo: ["url": url]) NotificationCenter.default.post(name: .openFileForTranscription, object: nil, userInfo: ["url": url])

View File

@ -1,6 +1,4 @@
import SwiftUI import SwiftUI
import LaunchAtLogin
import SwiftData
import AppKit import AppKit
class MenuBarManager: ObservableObject { 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, init() {
whisperState: WhisperState,
container: ModelContainer,
enhancementService: AIEnhancementService,
aiService: AIService,
hotkeyManager: HotkeyManager) {
self.isMenuBarOnly = UserDefaults.standard.bool(forKey: "IsMenuBarOnly") 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() updateAppActivationPolicy()
} }
@ -39,23 +19,37 @@ class MenuBarManager: ObservableObject {
isMenuBarOnly.toggle() 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() { private func updateAppActivationPolicy() {
DispatchQueue.main.async { [weak self] in let applyPolicy = { [weak self] in
guard let self = self else { return } guard let self else { return }
let application = NSApplication.shared
// 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
if self.isMenuBarOnly { if self.isMenuBarOnly {
NSApp.setActivationPolicy(.accessory) application.setActivationPolicy(.accessory)
WindowManager.shared.hideMainWindow()
} else { } else {
NSApp.setActivationPolicy(.regular) application.setActivationPolicy(.regular)
WindowManager.shared.showMainWindow()
} }
} }
if Thread.isMainThread {
applyPolicy()
} else {
DispatchQueue.main.async(execute: applyPolicy)
}
} }
func openMainWindowAndNavigate(to destination: String) { func openMainWindowAndNavigate(to destination: String) {
@ -64,31 +58,13 @@ class MenuBarManager: ObservableObject {
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
guard let self = self else { return } guard let self = self else { return }
if self.isMenuBarOnly { self.applyActivationPolicy()
NSApp.setActivationPolicy(.accessory)
} else { guard WindowManager.shared.showMainWindow() != nil else {
NSApp.setActivationPolicy(.regular) 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 // Post a notification to navigate to the desired destination
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
NotificationCenter.default.post( NotificationCenter.default.post(
@ -100,47 +76,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()
}
}

View File

@ -180,6 +180,7 @@ struct MenuBarView: View {
Button("Copy Last Transcription") { Button("Copy Last Transcription") {
LastTranscriptionService.copyLastTranscription(from: whisperState.modelContext) LastTranscriptionService.copyLastTranscription(from: whisperState.modelContext)
} }
.keyboardShortcut("c", modifiers: [.command, .shift])
Button("History") { Button("History") {
menuBarManager.openMainWindowAndNavigate(to: "History") menuBarManager.openMainWindowAndNavigate(to: "History")
@ -194,6 +195,7 @@ struct MenuBarView: View {
Button(menuBarManager.isMenuBarOnly ? "Show Dock Icon" : "Hide Dock Icon") { Button(menuBarManager.isMenuBarOnly ? "Show Dock Icon" : "Hide Dock Icon") {
menuBarManager.toggleMenuBarOnly() menuBarManager.toggleMenuBarOnly()
} }
.keyboardShortcut("d", modifiers: [.command, .shift])
Toggle("Launch at Login", isOn: $launchAtLoginEnabled) Toggle("Launch at Login", isOn: $launchAtLoginEnabled)
.onChange(of: launchAtLoginEnabled) { oldValue, newValue in .onChange(of: launchAtLoginEnabled) { oldValue, newValue in

View File

@ -79,15 +79,9 @@ struct VoiceInkApp: App {
let hotkeyManager = HotkeyManager(whisperState: whisperState) let hotkeyManager = HotkeyManager(whisperState: whisperState)
_hotkeyManager = StateObject(wrappedValue: hotkeyManager) _hotkeyManager = StateObject(wrappedValue: hotkeyManager)
let menuBarManager = MenuBarManager( let menuBarManager = MenuBarManager()
updaterViewModel: updaterViewModel,
whisperState: whisperState,
container: container,
enhancementService: enhancementService,
aiService: aiService,
hotkeyManager: hotkeyManager
)
_menuBarManager = StateObject(wrappedValue: menuBarManager) _menuBarManager = StateObject(wrappedValue: menuBarManager)
appDelegate.menuBarManager = menuBarManager
let activeWindowService = ActiveWindowService.shared let activeWindowService = ActiveWindowService.shared
activeWindowService.configure(with: enhancementService) activeWindowService.configure(with: enhancementService)
@ -157,8 +151,7 @@ struct VoiceInkApp: App {
.environmentObject(enhancementService) .environmentObject(enhancementService)
.frame(minWidth: 880, minHeight: 780) .frame(minWidth: 880, minHeight: 780)
.background(WindowAccessor { window in .background(WindowAccessor { window in
// Ensure this is called only once or is idempotent if window.identifier == nil || window.identifier != NSUserInterfaceItemIdentifier("com.prakashjoshipax.voiceink.onboardingWindow") {
if window.title != "VoiceInk Onboarding" { // Prevent re-configuration
WindowManager.shared.configureOnboardingPanel(window) WindowManager.shared.configureOnboardingPanel(window)
} }
}) })

View File

@ -1,12 +1,27 @@
import SwiftUI import SwiftUI
import AppKit import AppKit
class WindowManager { class WindowManager: NSObject {
static let shared = WindowManager() static let shared = WindowManager()
private init() {} private static let mainWindowIdentifier = NSUserInterfaceItemIdentifier("com.prakashjoshipax.voiceink.mainWindow")
private static let onboardingWindowIdentifier = NSUserInterfaceItemIdentifier("com.prakashjoshipax.voiceink.onboardingWindow")
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) { func configureWindow(_ window: NSWindow) {
if let existingWindow = NSApplication.shared.windows.first(where: { $0.identifier == Self.mainWindowIdentifier && $0 != window }) {
window.close()
existingWindow.makeKeyAndOrderFront(nil)
return
}
let requiredStyleMask: NSWindow.StyleMask = [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView] let requiredStyleMask: NSWindow.StyleMask = [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView]
window.styleMask.formUnion(requiredStyleMask) window.styleMask.formUnion(requiredStyleMask)
window.titlebarAppearsTransparent = true window.titlebarAppearsTransparent = true
@ -19,10 +34,17 @@ class WindowManager {
window.isOpaque = true window.isOpaque = true
window.isMovableByWindowBackground = false window.isMovableByWindowBackground = false
window.minSize = NSSize(width: 0, height: 0) window.minSize = NSSize(width: 0, height: 0)
window.setFrameAutosaveName(Self.mainWindowAutosaveName)
applyInitialPlacementIfNeeded(to: window)
registerMainWindowIfNeeded(window)
window.orderFrontRegardless() window.orderFrontRegardless()
} }
func configureOnboardingPanel(_ window: NSWindow) { func configureOnboardingPanel(_ window: NSWindow) {
if window.identifier == nil || window.identifier != Self.onboardingWindowIdentifier {
window.identifier = Self.onboardingWindowIdentifier
}
let requiredStyleMask: NSWindow.StyleMask = [.titled, .fullSizeContentView, .resizable] let requiredStyleMask: NSWindow.StyleMask = [.titled, .fullSizeContentView, .resizable]
window.styleMask.formUnion(requiredStyleMask) window.styleMask.formUnion(requiredStyleMask)
window.titlebarAppearsTransparent = true window.titlebarAppearsTransparent = true
@ -37,38 +59,78 @@ class WindowManager {
window.minSize = NSSize(width: 900, height: 780) window.minSize = NSSize(width: 900, height: 780)
window.makeKeyAndOrderFront(nil) window.makeKeyAndOrderFront(nil)
} }
func registerMainWindow(_ window: NSWindow) {
mainWindow = window
window.identifier = Self.mainWindowIdentifier
window.delegate = self
}
func createMainWindow(contentView: NSView) -> NSWindow { func showMainWindow() -> NSWindow? {
let defaultSize = NSSize(width: 940, height: 780) guard let window = resolveMainWindow() else {
let screenFrame = NSScreen.main?.visibleFrame ?? NSRect(x: 0, y: 0, width: 1200, height: 800) return nil
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
window.makeKeyAndOrderFront(nil)
NSApplication.shared.activate(ignoringOtherApps: true)
return window return window
} }
}
func hideMainWindow() {
class WindowStateDelegate: NSObject, NSWindowDelegate { guard let window = resolveMainWindow() else {
func windowWillClose(_ notification: Notification) { return
guard let window = notification.object as? NSWindow else { return } }
window.orderOut(nil) 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)
mainWindow = nil
didApplyInitialPlacement = false
}
}
func windowDidBecomeKey(_ notification: Notification) { func windowDidBecomeKey(_ notification: Notification) {
guard let _ = notification.object as? NSWindow else { return } guard let window = notification.object as? NSWindow,
NSApp.activate(ignoringOtherApps: true) window.identifier == Self.mainWindowIdentifier else { return }
NSApplication.shared.activate(ignoringOtherApps: true)
} }
} }