Skip to content

Commit f46372b

Browse files
committed
fix: alt-tab could crash if asked to close one of its own window
1 parent 33c6615 commit f46372b

File tree

3 files changed

+54
-30
lines changed

3 files changed

+54
-30
lines changed

src/logic/Window.swift

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
import Cocoa
22

33
class Window {
4-
static var globalCreationCounter = Int.zero
4+
private static let notifications = [
5+
kAXUIElementDestroyedNotification,
6+
kAXTitleChangedNotification,
7+
kAXWindowMiniaturizedNotification,
8+
kAXWindowDeminiaturizedNotification,
9+
kAXWindowResizedNotification,
10+
kAXWindowMovedNotification,
11+
]
12+
private static var globalCreationCounter = Int.zero
13+
514
var cgWindowId: CGWindowID?
615
var lastFocusOrder = Int.zero
716
var creationOrder = Int.zero
@@ -26,15 +35,6 @@ class Window {
2635
var rowIndex: Int?
2736
var debugId: String { "(wid:\(cgWindowId.map { String(describing: $0) } ?? "nil")) \(title ?? "nil")) \(application.debugId)" }
2837

29-
static let notifications = [
30-
kAXUIElementDestroyedNotification,
31-
kAXTitleChangedNotification,
32-
kAXWindowMiniaturizedNotification,
33-
kAXWindowDeminiaturizedNotification,
34-
kAXWindowResizedNotification,
35-
kAXWindowMovedNotification,
36-
]
37-
3838
init(_ axUiElement: AXUIElement, _ application: Application, _ wid: CGWindowID, _ axTitle: String?, _ isFullscreen: Bool, _ isMinimized: Bool, _ position: CGPoint?, _ size: CGSize?) {
3939
self.axUiElement = axUiElement
4040
self.application = application
@@ -68,17 +68,6 @@ class Window {
6868
Logger.debug(debugId)
6969
}
7070

71-
/// some apps will not trigger AXApplicationActivated, where we usually update application.focusedWindow
72-
/// workaround: we check and possibly do it here
73-
func checkIfFocused(_ application: Application, _ wid: CGWindowID) {
74-
AXUIElement.retryAxCallUntilTimeout(context: debugId, pid: application.pid, callType: .updateWindow) {
75-
let focusedWid = try application.axUiElement?.focusedWindow()?.cgWindowId()
76-
if wid == focusedWid {
77-
application.focusedWindow = self
78-
}
79-
}
80-
}
81-
8271
func isEqualRobust(_ otherWindowAxUiElement: AXUIElement, _ otherWindowWid: CGWindowID?) -> Bool {
8372
// the window can be deallocated by the OS, in which case its `CGWindowID` will be `-1`
8473
// we check for equality both on the AXUIElement, and the CGWindowID, in order to catch all scenarios
@@ -124,6 +113,10 @@ class Window {
124113
NSSound.beep()
125114
return
126115
}
116+
if let altTabWindow = altTabWindow() {
117+
altTabWindow.close()
118+
return
119+
}
127120
BackgroundWork.accessibilityCommandsQueue.addOperation { [weak self] in
128121
guard let self else { return }
129122
if self.isFullscreen {
@@ -144,6 +137,10 @@ class Window {
144137
NSSound.beep()
145138
return
146139
}
140+
if let altTabWindow = altTabWindow() {
141+
isMinimized ? altTabWindow.deminiaturize(nil) : altTabWindow.miniaturize(nil)
142+
return
143+
}
147144
BackgroundWork.accessibilityCommandsQueue.addOperation { [weak self] in
148145
guard let self else { return }
149146
if self.isFullscreen {
@@ -164,20 +161,23 @@ class Window {
164161
NSSound.beep()
165162
return
166163
}
164+
if let altTabWindow = altTabWindow() {
165+
altTabWindow.toggleFullScreen(nil)
166+
return
167+
}
167168
BackgroundWork.accessibilityCommandsQueue.addOperation { [weak self] in
168169
guard let self else { return }
169170
try? self.axUiElement!.setAttribute(kAXFullscreenAttribute, !self.isFullscreen)
170171
}
171172
}
172173

173174
func focus() {
174-
let bundleUrl = application.bundleURL
175-
if bundleUrl == App.bundleURL {
175+
if let altTabWindow = altTabWindow() {
176176
App.shared.activate(ignoringOtherApps: true)
177-
App.app.window(withWindowNumber: Int(cgWindowId!))?.makeKeyAndOrderFront(nil)
177+
altTabWindow.makeKeyAndOrderFront(nil)
178178
Windows.previewFocusedWindowIfNeeded()
179179
} else if isWindowlessApp || cgWindowId == nil || Preferences.onlyShowApplications() {
180-
if let bundleUrl, isWindowlessApp {
180+
if let bundleUrl = application.bundleURL, isWindowlessApp {
181181
if (try? NSWorkspace.shared.launchApplication(at: bundleUrl, configuration: [:])) == nil {
182182
application.runningApplication.activate(options: .activateAllWindows)
183183
}
@@ -204,7 +204,7 @@ class Window {
204204
}
205205

206206
/// The following function was ported from https://github.com/Hammerspoon/hammerspoon/issues/370#issuecomment-545545468
207-
func makeKeyWindow(_ psn: inout ProcessSerialNumber) -> Void {
207+
private func makeKeyWindow(_ psn: inout ProcessSerialNumber) -> Void {
208208
var bytes = [UInt8](repeating: 0, count: 0xf8)
209209
bytes[0x04] = 0xf8
210210
bytes[0x3a] = 0x10
@@ -273,4 +273,22 @@ class Window {
273273
}
274274
return false
275275
}
276+
277+
private func altTabWindow() -> NSWindow? {
278+
if application.bundleURL == App.bundleURL, let cgWindowId {
279+
return App.app.window(withWindowNumber: Int(cgWindowId))
280+
}
281+
return nil
282+
}
283+
284+
/// some apps will not trigger AXApplicationActivated, where we usually update application.focusedWindow
285+
/// workaround: we check and possibly do it here
286+
private func checkIfFocused(_ application: Application, _ wid: CGWindowID) {
287+
AXUIElement.retryAxCallUntilTimeout(context: debugId, pid: application.pid, callType: .updateWindow) {
288+
let focusedWid = try application.axUiElement?.focusedWindow()?.cgWindowId()
289+
if wid == focusedWid {
290+
application.focusedWindow = self
291+
}
292+
}
293+
}
276294
}

src/ui/App.swift

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,15 @@ class App: AppCenterApplication {
8686

8787
/// we don't want another window to become key when the thumbnailPanel is hidden
8888
func hideThumbnailPanelWithoutChangingKeyWindow() {
89-
preferencesWindow.canBecomeKey_ = false
90-
feedbackWindow.canBecomeKey_ = false
89+
allSecondaryWindowsCanBecomeKey(false)
9190
thumbnailsPanel.orderOut(nil)
92-
preferencesWindow.canBecomeKey_ = true
93-
feedbackWindow.canBecomeKey_ = true
91+
allSecondaryWindowsCanBecomeKey(true)
92+
}
93+
94+
private func allSecondaryWindowsCanBecomeKey(_ canBecomeKey_: Bool) {
95+
preferencesWindow.canBecomeKey_ = canBecomeKey_
96+
feedbackWindow.canBecomeKey_ = canBecomeKey_
97+
permissionsWindow.canBecomeKey_ = canBecomeKey_
9498
}
9599

96100
func closeSelectedWindow() {

src/ui/permission-window/PermissionsWindow.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import Cocoa
33
class PermissionsWindow: NSWindow {
44
var accessibilityView: PermissionView!
55
var screenRecordingView: PermissionView!
6+
var canBecomeKey_ = true
7+
override var canBecomeKey: Bool { canBecomeKey_ }
68

79
convenience init() {
810
self.init(contentRect: .zero, styleMask: [.titled, .miniaturizable, .closable], backing: .buffered, defer: false)

0 commit comments

Comments
 (0)