Self-heal off-screen main window instead of requiring quit/relaunch
macOS's built-in window-restore (Resume) can place the window at coordinates for a display that's no longer connected, leaving it visible-but-invisible with no way to recover short of quitting. Now re-centers automatically on app activate/Dock reopen, plus a manual "Reset Window Position" menu command as a guaranteed fallback.
This commit is contained in:
@@ -53,6 +53,41 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
return shouldTerminate ? .terminateNow : .terminateCancel
|
return shouldTerminate ? .terminateNow : .terminateCancel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// macOS's built-in window-restoration (Resume) can place the main window at coordinates
|
||||||
|
// that no longer correspond to any connected display (e.g. it was last positioned on an
|
||||||
|
// external monitor that's now disconnected). The window is still technically "visible"
|
||||||
|
// (isVisible == true), so clicking the Dock icon or Cmd+Tab-ing to the app doesn't trigger
|
||||||
|
// any of the usual recovery paths — the process runs fine, there's just nothing on screen.
|
||||||
|
// Self-heal by re-centering any main-sized window that doesn't intersect a current screen,
|
||||||
|
// both whenever the app becomes active and when the Dock icon is clicked with the app
|
||||||
|
// already running.
|
||||||
|
func applicationDidBecomeActive(_ notification: Notification) {
|
||||||
|
AppDelegate.repositionOffscreenWindows()
|
||||||
|
}
|
||||||
|
|
||||||
|
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
|
||||||
|
AppDelegate.repositionOffscreenWindows()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pure geometry check, kept separate from AppKit so it's directly testable.
|
||||||
|
nonisolated static func isFrameOffscreen(_ frame: CGRect, screenFrames: [CGRect]) -> Bool {
|
||||||
|
!screenFrames.contains { $0.intersects(frame) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Re-centers the main window if it's off-screen, or unconditionally when `force` is true
|
||||||
|
/// (used by the manual "Reset Window Position" menu command).
|
||||||
|
static func repositionOffscreenWindows(force: Bool = false) {
|
||||||
|
let screenFrames = NSScreen.screens.map(\.frame)
|
||||||
|
for window in NSApp.windows where window.isVisible && !(window is NSPanel) && window.frame.width > 200 && window.frame.height > 200 {
|
||||||
|
if force || isFrameOffscreen(window.frame, screenFrames: screenFrames) {
|
||||||
|
window.center()
|
||||||
|
}
|
||||||
|
window.makeKeyAndOrderFront(nil)
|
||||||
|
}
|
||||||
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
|
}
|
||||||
|
|
||||||
// `chatViewModel` is wired from `ContentView.onAppear`, not from `oAIApp.init()` — reading
|
// `chatViewModel` is wired from `ContentView.onAppear`, not from `oAIApp.init()` — reading
|
||||||
// the `@State private var chatViewModel` there returns a throwaway instance distinct from
|
// the `@State private var chatViewModel` there returns a throwaway instance distinct from
|
||||||
// the one SwiftUI actually renders (confirmed via ObjectIdentifier logging: two different
|
// the one SwiftUI actually renders (confirmed via ObjectIdentifier logging: two different
|
||||||
@@ -217,6 +252,16 @@ struct oAIApp: App {
|
|||||||
chatViewModel.onlineMode.toggle()
|
chatViewModel.onlineMode.toggle()
|
||||||
}
|
}
|
||||||
.keyboardShortcut("o", modifiers: [.command, .shift])
|
.keyboardShortcut("o", modifiers: [.command, .shift])
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
// Fallback for when the window ends up off-screen (stale macOS window-restore
|
||||||
|
// coordinates, e.g. after an external monitor is disconnected) — see
|
||||||
|
// AppDelegate.repositionOffscreenWindows. No keyboard shortcut assigned yet;
|
||||||
|
// add one once it's been live-verified against macOS's reserved bindings.
|
||||||
|
Button("Reset Window Position") {
|
||||||
|
AppDelegate.repositionOffscreenWindows(force: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Help menu ─────────────────────────────────────────────────
|
// ── Help menu ─────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
//
|
||||||
|
// WindowPositionTests.swift
|
||||||
|
// oAITests
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
||||||
|
// Copyright (C) 2026 Rune Olsen
|
||||||
|
|
||||||
|
import Testing
|
||||||
|
import Foundation
|
||||||
|
import CoreGraphics
|
||||||
|
@testable import Confab
|
||||||
|
|
||||||
|
@Suite("Off-screen window detection")
|
||||||
|
struct WindowPositionTests {
|
||||||
|
|
||||||
|
private let mainScreen = CGRect(x: 0, y: 0, width: 2560, height: 1664)
|
||||||
|
|
||||||
|
@Test("Window inside the only connected screen is not off-screen")
|
||||||
|
func onscreenWindow() {
|
||||||
|
let frame = CGRect(x: 100, y: 100, width: 1024, height: 800)
|
||||||
|
#expect(AppDelegate.isFrameOffscreen(frame, screenFrames: [mainScreen]) == false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Window at coordinates from a now-disconnected external display is off-screen")
|
||||||
|
func offscreenWindow() {
|
||||||
|
// e.g. previously positioned on an external monitor to the right of the built-in display
|
||||||
|
let frame = CGRect(x: 3000, y: 200, width: 1024, height: 800)
|
||||||
|
#expect(AppDelegate.isFrameOffscreen(frame, screenFrames: [mainScreen]) == true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Window merely partially overlapping a screen counts as on-screen")
|
||||||
|
func partiallyOverlappingWindow() {
|
||||||
|
let frame = CGRect(x: 2500, y: 100, width: 1024, height: 800)
|
||||||
|
#expect(AppDelegate.isFrameOffscreen(frame, screenFrames: [mainScreen]) == false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Off-screen relative to one display but onscreen on a second connected display")
|
||||||
|
func multiMonitorOnscreen() {
|
||||||
|
let secondScreen = CGRect(x: 2560, y: 0, width: 1920, height: 1080)
|
||||||
|
let frame = CGRect(x: 3000, y: 200, width: 1024, height: 800)
|
||||||
|
#expect(AppDelegate.isFrameOffscreen(frame, screenFrames: [mainScreen, secondScreen]) == false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("No connected screens means every window is off-screen")
|
||||||
|
func noScreens() {
|
||||||
|
let frame = CGRect(x: 100, y: 100, width: 1024, height: 800)
|
||||||
|
#expect(AppDelegate.isFrameOffscreen(frame, screenFrames: []) == true)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user