fix: intellij fullscreen windows sometimes not showing (#824)
This commit is contained in:
1 parent
e86990065d
commit
4dcb6bbbc0
5 files changed
+65
-66
No files matched your search
@@ -169,6 +169,41 @@ extension pid_t {
|
|||||||
}
|
}
|
||||||
return kinfo.kp_proc.p_stat == SZOMB
|
return kinfo.kp_proc.p_stat == SZOMB
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the algorithm used by updateTabs is incorrect is the screen is in the middle of an animation (e.g. window going fullscreen)
|
||||||
|
// we retry until there is no animation, then we proceed
|
||||||
|
func retryToRefreshTabsUntilScreenIsNotAnimating(_ fn: @escaping ([Window]) -> Void) {
|
||||||
|
if NSRunningApplication(processIdentifier: self) != nil,
|
||||||
|
let mainScreen = NSScreen.main,
|
||||||
|
let uuid = mainScreen.uuid() {
|
||||||
|
retryAxCallUntilTimeout {
|
||||||
|
if SLSManagedDisplayIsAnimating(cgsMainConnectionId, uuid) {
|
||||||
|
throw AxError.runtimeError
|
||||||
|
}
|
||||||
|
let currentWindows = try AXUIElementCreateApplication(self).windows()
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
fn(self.updateTabs(currentWindows))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// when a window is tabbed, the AX call to get windows doesn't list it
|
||||||
|
// we compare a fresh call to get the windows (currentWindows) to the windows we have already (Windows.list)
|
||||||
|
// any window not in currentWindows is considered tabbed
|
||||||
|
private func updateTabs(_ currentWindows: [AXUIElement]?) -> [Window] {
|
||||||
|
debugPrint(try? Windows.list.map { w in (w.cgWindowId, w.title, w.spaceId == Spaces.currentSpaceId, try currentWindows?.map { try $0.cgWindowId() }) })
|
||||||
|
let windows = Windows.list.filter { w in
|
||||||
|
if w.application.pid == self && self != ProcessInfo.processInfo.processIdentifier &&
|
||||||
|
w.spaceId == Spaces.currentSpaceId {
|
||||||
|
let oldIsTabbed = w.isTabbed
|
||||||
|
w.isTabbed = (currentWindows?.first { $0 == w.axUiElement } == nil)
|
||||||
|
return oldIsTabbed != w.isTabbed
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return windows
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension String {
|
extension String {
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ class Spaces {
|
|||||||
debugPrint("OS event", "activeSpaceDidChangeNotification")
|
debugPrint("OS event", "activeSpaceDidChangeNotification")
|
||||||
refreshAllIdsAndIndexes()
|
refreshAllIdsAndIndexes()
|
||||||
updateCurrentSpace()
|
updateCurrentSpace()
|
||||||
|
// if UI was kept open during Space transition, the Spaces may be obsolete; we refresh them
|
||||||
|
Windows.list.forEachAsync { $0.updatesWindowSpace() }
|
||||||
})
|
})
|
||||||
NSWorkspace.shared.notificationCenter.addObserver(forName: NSApplication.didChangeScreenParametersNotification, object: nil, queue: nil, using: { _ in
|
NSWorkspace.shared.notificationCenter.addObserver(forName: NSApplication.didChangeScreenParametersNotification, object: nil, queue: nil, using: { _ in
|
||||||
debugPrint("OS event", "didChangeScreenParametersNotification")
|
debugPrint("OS event", "didChangeScreenParametersNotification")
|
||||||
|
|||||||
@@ -201,5 +201,20 @@ class Window {
|
|||||||
}
|
}
|
||||||
return application.runningApplication.localizedName ?? ""
|
return application.runningApplication.localizedName ?? ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updatesWindowSpace() {
|
||||||
|
// macOS bug: if you tab a window, then move the tab group to another space, other tabs from the tab group will stay on the current space
|
||||||
|
// you can use the Dock to focus one of the other tabs and it will teleport that tab in the current space, proving that it's a macOS bug
|
||||||
|
// note: for some reason, it behaves differently if you minimize the tab group after moving it to another space
|
||||||
|
let spaceIds = cgWindowId.spaces()
|
||||||
|
if spaceIds.count == 1 {
|
||||||
|
spaceId = spaceIds.first!
|
||||||
|
spaceIndex = Spaces.idsAndIndexes.first { $0.0 == spaceIds.first! }!.1
|
||||||
|
} else if spaceIds.count > 1 {
|
||||||
|
spaceId = Spaces.currentSpaceId
|
||||||
|
spaceIndex = Spaces.currentSpaceIndex
|
||||||
|
isOnAllSpaces = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
+1
-18
@@ -112,24 +112,7 @@ class Windows {
|
|||||||
// workaround: when Preferences > Mission Control > "Displays have separate Spaces" is unchecked,
|
// workaround: when Preferences > Mission Control > "Displays have separate Spaces" is unchecked,
|
||||||
// switching between displays doesn't trigger .activeSpaceDidChangeNotification; we get the latest manually
|
// switching between displays doesn't trigger .activeSpaceDidChangeNotification; we get the latest manually
|
||||||
Spaces.refreshCurrentSpaceId()
|
Spaces.refreshCurrentSpaceId()
|
||||||
list.forEachAsync { (window: Window) in
|
list.forEachAsync { $0.updatesWindowSpace() }
|
||||||
updatesWindowSpace(window)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static func updatesWindowSpace(_ window: Window) {
|
|
||||||
// macOS bug: if you tab a window, then move the tab group to another space, other tabs from the tab group will stay on the current space
|
|
||||||
// you can use the Dock to focus one of the other tabs and it will teleport that tab in the current space, proving that it's a macOS bug
|
|
||||||
// note: for some reason, it behaves differently if you minimize the tab group after moving it to another space
|
|
||||||
let spaceIds = window.cgWindowId.spaces()
|
|
||||||
if spaceIds.count == 1 {
|
|
||||||
window.spaceId = spaceIds.first!
|
|
||||||
window.spaceIndex = Spaces.idsAndIndexes.first { $0.0 == spaceIds.first! }!.1
|
|
||||||
} else if spaceIds.count > 1 {
|
|
||||||
window.spaceId = Spaces.currentSpaceId
|
|
||||||
window.spaceIndex = Spaces.currentSpaceIndex
|
|
||||||
window.isOnAllSpaces = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static func sortByLevel() {
|
static func sortByLevel() {
|
||||||
|
|||||||
@@ -15,61 +15,24 @@ fileprivate func handleEvent(_ type: String, _ element: AXUIElement) throws {
|
|||||||
switch type {
|
switch type {
|
||||||
case kAXApplicationActivatedNotification: try applicationActivated(element, pid)
|
case kAXApplicationActivatedNotification: try applicationActivated(element, pid)
|
||||||
case kAXApplicationHiddenNotification,
|
case kAXApplicationHiddenNotification,
|
||||||
kAXApplicationShownNotification: try applicationHiddenOrShown(element, pid, type)
|
kAXApplicationShownNotification: try applicationHiddenOrShown(pid, type)
|
||||||
case kAXWindowCreatedNotification: try windowCreated(element, pid)
|
case kAXWindowCreatedNotification: try windowCreated(element, pid)
|
||||||
case kAXMainWindowChangedNotification,
|
case kAXMainWindowChangedNotification,
|
||||||
kAXFocusedWindowChangedNotification: try focusedWindowChanged(element, pid)
|
kAXFocusedWindowChangedNotification: try focusedWindowChanged(element, pid)
|
||||||
case kAXUIElementDestroyedNotification: try windowDestroyed(element, pid)
|
case kAXUIElementDestroyedNotification: try windowDestroyed(element, pid)
|
||||||
case kAXWindowMiniaturizedNotification,
|
case kAXWindowMiniaturizedNotification,
|
||||||
kAXWindowDeminiaturizedNotification: try windowMiniaturizedOrDeminiaturized(element, type)
|
kAXWindowDeminiaturizedNotification: try windowMiniaturizedOrDeminiaturized(element, type)
|
||||||
case kAXTitleChangedNotification: try windowTitleChanged(element)
|
case kAXTitleChangedNotification: try windowTitleChanged(element, pid)
|
||||||
case kAXWindowResizedNotification: try windowResized(element)
|
case kAXWindowResizedNotification: try windowResized(element)
|
||||||
case kAXWindowMovedNotification: try windowMoved(element)
|
case kAXWindowMovedNotification: try windowMoved(element)
|
||||||
case kAXFocusedUIElementChangedNotification: try focusedUiElementChanged(element, pid)
|
case kAXFocusedUIElementChangedNotification: try focusedUiElementChanged(pid)
|
||||||
default: return
|
default: return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func focusedUiElementChanged(_ element: AXUIElement, _ pid: pid_t) throws {
|
fileprivate func focusedUiElementChanged(_ pid: pid_t) throws {
|
||||||
if NSRunningApplication(processIdentifier: pid) != nil {
|
pid.retryToRefreshTabsUntilScreenIsNotAnimating { App.app.refreshOpenUi($0) }
|
||||||
let currentWindows = try AXUIElementCreateApplication(pid).windows()
|
|
||||||
retryToRefreshTabsUntilScreenIsNotAnimating(pid, currentWindows) { windows in
|
|
||||||
App.app.refreshOpenUi(windows)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// the algorithm used by updateTabs is incorrect is the screen is in the middle of an animation (e.g. window going fullscreen)
|
|
||||||
// we retry until there is no animation, then we proceed
|
|
||||||
fileprivate func retryToRefreshTabsUntilScreenIsNotAnimating(_ pid: pid_t, _ currentWindows: [AXUIElement]?, _ fn: @escaping ([Window]) -> Void) {
|
|
||||||
if let mainScreen = NSScreen.main,
|
|
||||||
let uuid = mainScreen.uuid() {
|
|
||||||
retryAxCallUntilTimeout {
|
|
||||||
if SLSManagedDisplayIsAnimating(cgsMainConnectionId, uuid) {
|
|
||||||
throw AxError.runtimeError
|
|
||||||
}
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
fn(updateTabs(pid, currentWindows))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// when a window is tabbed, the AX call to get windows doesn't list it
|
|
||||||
// we compare a fresh call to get the windows (currentWindows) to the windows we have already (Windows.list)
|
|
||||||
// any window not in currentWindows is considered tabbed
|
|
||||||
fileprivate func updateTabs(_ pid: pid_t, _ currentWindows: [AXUIElement]?) -> [Window] {
|
|
||||||
let windows = Windows.list.filter { w in
|
|
||||||
if w.application.pid == pid && pid != ProcessInfo.processInfo.processIdentifier &&
|
|
||||||
w.spaceId == Spaces.currentSpaceId {
|
|
||||||
let oldIsTabbed = w.isTabbed
|
|
||||||
w.isTabbed = (currentWindows?.first { $0 == w.axUiElement } == nil)
|
|
||||||
return oldIsTabbed != w.isTabbed
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return windows
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func applicationActivated(_ element: AXUIElement, _ pid: pid_t) throws {
|
fileprivate func applicationActivated(_ element: AXUIElement, _ pid: pid_t) throws {
|
||||||
@@ -89,7 +52,7 @@ fileprivate func applicationActivated(_ element: AXUIElement, _ pid: pid_t) thro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func applicationHiddenOrShown(_ element: AXUIElement, _ pid: pid_t, _ type: String) throws {
|
fileprivate func applicationHiddenOrShown(_ pid: pid_t, _ type: String) throws {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if let app = (Applications.list.first { $0.pid == pid }) {
|
if let app = (Applications.list.first { $0.pid == pid }) {
|
||||||
app.isHidden = type == kAXApplicationHiddenNotification
|
app.isHidden = type == kAXApplicationHiddenNotification
|
||||||
@@ -167,15 +130,13 @@ fileprivate func focusedWindowChanged(_ element: AXUIElement, _ pid: pid_t) thro
|
|||||||
|
|
||||||
fileprivate func windowDestroyed(_ element: AXUIElement, _ pid: pid_t) throws {
|
fileprivate func windowDestroyed(_ element: AXUIElement, _ pid: pid_t) throws {
|
||||||
let wid = try element.cgWindowId()
|
let wid = try element.cgWindowId()
|
||||||
let appIsStillRunning = NSRunningApplication(processIdentifier: pid) != nil
|
|
||||||
let currentWindows = appIsStillRunning ? try AXUIElementCreateApplication(pid).windows() : []
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if let window = (Windows.list.first { $0.isEqualRobust(element, wid) }) {
|
if let window = (Windows.list.first { $0.isEqualRobust(element, wid) }) {
|
||||||
Windows.removeAndUpdateFocus(window)
|
Windows.removeAndUpdateFocus(window)
|
||||||
let windowlessApp = window.application.addWindowslessAppsIfNeeded()
|
let windowlessApp = window.application.addWindowslessAppsIfNeeded()
|
||||||
if Windows.list.count > 0 {
|
if Windows.list.count > 0 {
|
||||||
// closing a tab may make another tab visible; we refresh tab status
|
// closing a tab may make another tab visible; we refresh tab status
|
||||||
retryToRefreshTabsUntilScreenIsNotAnimating(pid, currentWindows) { windows in
|
pid.retryToRefreshTabsUntilScreenIsNotAnimating { windows in
|
||||||
Windows.moveFocusedWindowIndexAfterWindowDestroyedInBackground(window)
|
Windows.moveFocusedWindowIndexAfterWindowDestroyedInBackground(window)
|
||||||
if let windowlessApp = windowlessApp {
|
if let windowlessApp = windowlessApp {
|
||||||
App.app.refreshOpenUi(windows + windowlessApp)
|
App.app.refreshOpenUi(windows + windowlessApp)
|
||||||
@@ -201,14 +162,17 @@ fileprivate func windowMiniaturizedOrDeminiaturized(_ element: AXUIElement, _ ty
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func windowTitleChanged(_ element: AXUIElement) throws {
|
fileprivate func windowTitleChanged(_ element: AXUIElement, _ pid: pid_t) throws {
|
||||||
if let wid = try element.cgWindowId() {
|
if let wid = try element.cgWindowId() {
|
||||||
let newTitle = try element.title()
|
let newTitle = try element.title()
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if let window = (Windows.list.first { $0.isEqualRobust(element, wid) }),
|
if let window = (Windows.list.first { $0.isEqualRobust(element, wid) }),
|
||||||
newTitle != nil && newTitle != window.title {
|
newTitle != nil && newTitle != window.title {
|
||||||
window.title = newTitle!
|
window.title = newTitle!
|
||||||
App.app.refreshOpenUi([window])
|
// refreshing tabs during title change helps mitigate some false positives of tab detection
|
||||||
|
pid.retryToRefreshTabsUntilScreenIsNotAnimating {
|
||||||
|
App.app.refreshOpenUi($0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user