Merge pull request #350 from Beingpax/refactor-window-management
Refactor window management
This commit is contained in:
commit
2dedff0c84
@ -3,46 +3,30 @@ 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()
|
||||
if !flag, let menuBarManager = menuBarManager, !menuBarManager.isMenuBarOnly {
|
||||
if WindowManager.shared.showMainWindow() != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
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 +36,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])
|
||||
|
||||
@ -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,37 @@ 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)
|
||||
WindowManager.shared.showMainWindow()
|
||||
}
|
||||
}
|
||||
|
||||
if Thread.isMainThread {
|
||||
applyPolicy()
|
||||
} else {
|
||||
DispatchQueue.main.async(execute: applyPolicy)
|
||||
}
|
||||
}
|
||||
|
||||
func openMainWindowAndNavigate(to destination: String) {
|
||||
@ -64,31 +58,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 +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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -180,6 +180,7 @@ struct MenuBarView: View {
|
||||
Button("Copy Last Transcription") {
|
||||
LastTranscriptionService.copyLastTranscription(from: whisperState.modelContext)
|
||||
}
|
||||
.keyboardShortcut("c", modifiers: [.command, .shift])
|
||||
|
||||
Button("History") {
|
||||
menuBarManager.openMainWindowAndNavigate(to: "History")
|
||||
@ -194,6 +195,7 @@ struct MenuBarView: View {
|
||||
Button(menuBarManager.isMenuBarOnly ? "Show Dock Icon" : "Hide Dock Icon") {
|
||||
menuBarManager.toggleMenuBarOnly()
|
||||
}
|
||||
.keyboardShortcut("d", modifiers: [.command, .shift])
|
||||
|
||||
Toggle("Launch at Login", isOn: $launchAtLoginEnabled)
|
||||
.onChange(of: launchAtLoginEnabled) { oldValue, newValue in
|
||||
|
||||
@ -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)
|
||||
@ -157,8 +151,7 @@ struct VoiceInkApp: App {
|
||||
.environmentObject(enhancementService)
|
||||
.frame(minWidth: 880, minHeight: 780)
|
||||
.background(WindowAccessor { window in
|
||||
// Ensure this is called only once or is idempotent
|
||||
if window.title != "VoiceInk Onboarding" { // Prevent re-configuration
|
||||
if window.identifier == nil || window.identifier != NSUserInterfaceItemIdentifier("com.prakashjoshipax.voiceink.onboardingWindow") {
|
||||
WindowManager.shared.configureOnboardingPanel(window)
|
||||
}
|
||||
})
|
||||
|
||||
@ -1,12 +1,27 @@
|
||||
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 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) {
|
||||
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]
|
||||
window.styleMask.formUnion(requiredStyleMask)
|
||||
window.titlebarAppearsTransparent = true
|
||||
@ -19,10 +34,17 @@ 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()
|
||||
}
|
||||
|
||||
func configureOnboardingPanel(_ window: NSWindow) {
|
||||
if window.identifier == nil || window.identifier != Self.onboardingWindowIdentifier {
|
||||
window.identifier = Self.onboardingWindowIdentifier
|
||||
}
|
||||
|
||||
let requiredStyleMask: NSWindow.StyleMask = [.titled, .fullSizeContentView, .resizable]
|
||||
window.styleMask.formUnion(requiredStyleMask)
|
||||
window.titlebarAppearsTransparent = true
|
||||
@ -37,38 +59,78 @@ 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)
|
||||
mainWindow = nil
|
||||
didApplyInitialPlacement = false
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user