Refactor: Centralize window and menu bar management

This commit is contained in:
Beingpax 2025-10-30 21:12:59 +05:45
parent c4c6c79bea
commit c7030276eb
4 changed files with 118 additions and 161 deletions

View File

@ -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 SwiftUIs 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])

View File

@ -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()
}
}

View File

@ -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)

View File

@ -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)
}
}