fix: better tabs detection + fix issues with some apps (closes #1540)
Maybe fixed #647 #718
This commit is contained in:
1 parent
1a9fc593d1
commit
abd54b3939
6 files changed
+36
-94
No files matched your search
@@ -169,40 +169,6 @@ extension pid_t {
|
||||
}
|
||||
return kinfo.kp_proc.p_stat == SZOMB
|
||||
}
|
||||
|
||||
// the algorithm used by updateTabs is incorrect if 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] {
|
||||
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 {
|
||||
|
||||
@@ -216,27 +216,28 @@ enum CGSSpaceType: Int {
|
||||
@_silgen_name("CGSSpaceGetType")
|
||||
func CGSSpaceGetType(_ cid: CGSConnectionID, _ sid: CGSSpaceID) -> CGSSpaceType
|
||||
|
||||
// returns true if the current screen is animating
|
||||
// useful to detect Spaces transitions, windows going fullscreen, etc
|
||||
@_silgen_name("SLSManagedDisplayIsAnimating")
|
||||
func SLSManagedDisplayIsAnimating(_ cid: CGSConnectionID, _ displayUuid: CFString) -> Bool
|
||||
|
||||
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// below are some notes on some private APIs I experimented with
|
||||
// ------------------------------------------------------------
|
||||
|
||||
// returns true if the display is animating
|
||||
//@_silgen_name("CGSManagedDisplayIsAnimating") @discardableResult
|
||||
//func CGSManagedDisplayIsAnimating(_ cid: CGSConnectionID, _ displayUuid: CFString) -> Bool
|
||||
//// move the windows on the given Space. Note: doesn't move fullscreen windows
|
||||
//// * macOS 10.12+
|
||||
//@_silgen_name("CGSMoveWorkspaceWindowList")
|
||||
//func CGSMoveWorkspaceWindowList(_ cid: CGSConnectionID, _ windowList: CFArray, _ windowCount: UInt, _ sid: CGSSpaceID) -> OSStatus
|
||||
|
||||
//// returns true if the current screen is animating
|
||||
//// useful to detect Spaces transitions, windows going fullscreen, etc
|
||||
//@_silgen_name("SLSManagedDisplayIsAnimating")
|
||||
//func SLSManagedDisplayIsAnimating(_ cid: CGSConnectionID, _ displayUuid: CFString) -> Bool
|
||||
|
||||
//@_silgen_name("CGSGetSymbolicHotKeyValue")
|
||||
//func CGSGetSymbolicHotKeyValue(_ hotKey: Int, _ options: inout UInt32, _ keyCode: inout UInt32, _ modifiers: inout UInt32) -> CGError
|
||||
//
|
||||
|
||||
//@_silgen_name("CGSIsSymbolicHotKeyEnabled")
|
||||
//func CGSIsSymbolicHotKeyEnabled(_ hotKey: Int) -> Bool
|
||||
//
|
||||
|
||||
//// listen to some window server events
|
||||
//// most interesting events for Mission Control seem to be [1204, 1401, 1508]. It seems that these are all the valid events, as the response is .success with these: //// [100, 101, 102, 103, 104, 106, 107, 108, 111, 115, 116, 117, 118, 119, 120, 121, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517, 518, 519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559, 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573, 574, 575, 576, 577, 578, 579, 580, 581, 582, 583, 584, 585, 586, 587, 588, 589, 590, 591, 592, 593, 594, 595, 596, 597, 598, 599, 600, 601, 602, 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613, 614, 615, 616, 617, 618, 619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 641, 642, 643, 644, 645, 646, 647, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657, 658, 659, 660, 661, 662, 663, 664, 665, 666, 667, 668, 669, 670, 671, 672, 673, 674, 675, 676, 677, 678, 679, 680, 681, 682, 683, 684, 685, 686, 687, 688, 689, 690, 691, 692, 693, 694, 695, 696, 697, 698, 699, 701, 702, 703, 704, 705, 706, 707, 708, 709, 710, 711, 712, 713, 714, 715, 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 726, 727, 728, 729, 730, 731, 732, 733, 734, 735, 736, 737, 738, 739, 740, 741, 742, 743, 744, 745, 746, 747, 748, 749, 750, 751, 752, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, 784, 785, 786, 787, 788, 789, 790, 791, 792, 793, 794, 795, 796, 797, 798, 799, 800, 801, 802, 803, 804, 805, 806, 807, 808, 809, 810, 811, 812, 813, 814, 815, 816, 817, 818, 819, 820, 821, 822, 823, 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, 838, 839, 840, 841, 842, 843, 844, 845, 846, 847, 848, 849, 850, 851, 852, 853, 854, 855, 856, 857, 858, 859, 860, 861, 862, 863, 864, 865, 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878, 879, 880, 881, 882, 883, 884, 885, 886, 887, 888, 889, 890, 891, 892, 893, 894, 895, 896, 897, 898, 899, 900, 901, 903, 904, 905, 906, 907, 908, 909, 910, 911, 912, 913, 915, 916, 917, 918, 919, 920, 921, 922, 923, 924, 925, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 936, 937, 938, 939, 940, 941, 942, 943, 944, 945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958, 959, 960, 961, 962, 963, 964, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974, 975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 986, 987, 988, 989, 990, 991, 992, 993, 994, 995, 996, 997, 998, 999, 1200, 1201, 1202, 1203, 1204, 1205, 1206, 1207, 1208, 1209, 1210, 1211, 1212, 1213, 1214, 1215, 1216, 1217, 1218, 1219, 1220, 1221, 1222, 1223, 1224, 1225, 1226, 1227, 1228, 1229, 1230, 1231, 1232, 1233, 1234, 1235, 1236, 1237, 1238, 1239, 1240, 1241, 1242, 1243, 1244, 1245, 1246, 1247, 1248, 1249, 1250, 1251, 1252, 1253, 1254, 1255, 1256, 1257, 1258, 1259, 1260, 1261, 1262, 1263, 1264, 1265, 1266, 1267, 1268, 1269, 1270, 1271, 1272, 1273, 1274, 1275, 1276, 1277, 1278, 1279, 1280, 1281, 1282, 1283, 1284, 1285, 1286, 1287, 1288, 1289, 1290, 1291, 1292, 1293, 1294, 1295, 1296, 1297, 1298, 1299, 1300, 1301, 1302, 1303, 1304, 1305, 1306, 1307, 1308, 1309, 1310, 1311, 1312, 1313, 1314, 1315, 1316, 1317, 1318, 1319, 1320, 1321, 1322, 1323, 1324, 1325, 1326, 1327, 1328, 1329, 1330, 1331, 1332, 1333, 1334, 1335, 1336, 1337, 1338, 1339, 1340, 1341, 1342, 1343, 1344, 1345, 1346Line truncated
|
||||
//// example calls: `SLSRegisterNotifyProc(callbackFn2, $0, nil)` `SLSRegisterConnectionNotifyProc(cgsMainConnectionId, callbackFn1, $0, nil)`
|
||||
@@ -295,6 +296,7 @@ func SLSManagedDisplayIsAnimating(_ cid: CGSConnectionID, _ displayUuid: CFStrin
|
||||
//func CGSManagedDisplaySetCurrentSpace(_ cid: CGSConnectionID, _ display: CFString, _ sid: CGSSpaceID) -> Void
|
||||
//
|
||||
//// show provided spaces on top of the current space. It show windows from the provided spaces in the current space. Very weird behaviour and graphical glitch will happen when triggering Mission Control
|
||||
//// even though the windows are shown, we can't grab their AXref. The windows are only *visually* on the space
|
||||
//// * macOS 10.10+
|
||||
//@_silgen_name("CGSShowSpaces")
|
||||
//func CGSShowSpaces(_ cid: CGSConnectionID, _ sids: NSArray) -> Void
|
||||
|
||||
@@ -16,29 +16,6 @@ class Application: NSObject {
|
||||
var wasLaunchedBeforeAltTab = false
|
||||
var focusedWindow: Window? = nil
|
||||
|
||||
static func notifications(_ app: NSRunningApplication) -> [String] {
|
||||
let n = [
|
||||
kAXApplicationActivatedNotification,
|
||||
kAXMainWindowChangedNotification,
|
||||
kAXFocusedWindowChangedNotification,
|
||||
kAXWindowCreatedNotification,
|
||||
kAXApplicationHiddenNotification,
|
||||
kAXApplicationShownNotification,
|
||||
kAXFocusedUIElementChangedNotification,
|
||||
]
|
||||
// workaround: some apps exhibit bugs when we subscribe to its kAXFocusedUIElementChangedNotification
|
||||
// we don't know what's happening; we avoid this subscription to make these app usable
|
||||
if app.bundleIdentifier == "edu.stanford.protege" ||
|
||||
app.bundleIdentifier == "com.mathworks.matlab" ||
|
||||
app.bundleIdentifier?.range(of: "^com\\.install4j\\..+?$", options: .regularExpression) != nil ||
|
||||
app.bundleIdentifier?.range(of: "^com\\.live2d\\.cubism\\..+?$", options: .regularExpression) != nil ||
|
||||
app.bundleIdentifier?.range(of: "^org\\.libreoffice\\..+?$", options: .regularExpression) != nil ||
|
||||
app.bundleIdentifier?.range(of: "^com\\.(jetbrains\\.|google\\.android\\.studio).*?$", options: .regularExpression) != nil {
|
||||
return n.filter { $0 != kAXFocusedUIElementChangedNotification }
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
init(_ runningApplication: NSRunningApplication, _ wasLaunchedBeforeAltTab: Bool = false) {
|
||||
self.runningApplication = runningApplication
|
||||
self.wasLaunchedBeforeAltTab = wasLaunchedBeforeAltTab
|
||||
@@ -160,7 +137,14 @@ class Application: NSObject {
|
||||
|
||||
private func observeEvents() {
|
||||
guard let axObserver = axObserver else { return }
|
||||
for notification in Application.notifications(runningApplication) {
|
||||
for notification in [
|
||||
kAXApplicationActivatedNotification,
|
||||
kAXMainWindowChangedNotification,
|
||||
kAXFocusedWindowChangedNotification,
|
||||
kAXWindowCreatedNotification,
|
||||
kAXApplicationHiddenNotification,
|
||||
kAXApplicationShownNotification,
|
||||
] {
|
||||
retryAxCallUntilTimeout { [weak self] in
|
||||
guard let self = self else { return }
|
||||
try self.axUiElement!.subscribeToNotification(axObserver, notification, {
|
||||
|
||||
@@ -159,6 +159,13 @@ class Windows {
|
||||
list.forEachAsync { $0.updatesWindowSpace() }
|
||||
}
|
||||
|
||||
static func detectTabbedWindows() {
|
||||
let cgsWindowIds = Spaces.windowsInSpaces(Spaces.idsAndIndexes.map { $0.0 })
|
||||
list.forEach {
|
||||
$0.isTabbed = !$0.isMinimized && !$0.isHidden && !cgsWindowIds.contains($0.cgWindowId)
|
||||
}
|
||||
}
|
||||
|
||||
static func sortByLevel() {
|
||||
var windowLevelMap = [CGWindowID: Int]()
|
||||
for (index, cgWindowId) in Spaces.windowsInSpaces([Spaces.currentSpaceId]).enumerated() {
|
||||
|
||||
@@ -8,10 +8,10 @@ func axObserverCallback(observer: AXObserver, element: AXUIElement, notification
|
||||
}
|
||||
|
||||
fileprivate func handleEvent(_ type: String, _ element: AXUIElement) throws {
|
||||
debugPrint("Accessibility event", type, type != kAXFocusedUIElementChangedNotification ? (try element.title() ?? "nil") : "nil")
|
||||
debugPrint("Accessibility event", type, try element.title() ?? "nil")
|
||||
// events are handled concurrently, thus we check that the app is still running
|
||||
if let pid = try element.pid(),
|
||||
try pid != ProcessInfo.processInfo.processIdentifier || (element.subrole() != kAXUnknownSubrole && type != kAXFocusedUIElementChangedNotification) {
|
||||
try pid != ProcessInfo.processInfo.processIdentifier || (element.subrole() != kAXUnknownSubrole) {
|
||||
switch type {
|
||||
case kAXApplicationActivatedNotification: try applicationActivated(element, pid)
|
||||
case kAXApplicationHiddenNotification,
|
||||
@@ -19,26 +19,17 @@ fileprivate func handleEvent(_ type: String, _ element: AXUIElement) throws {
|
||||
case kAXWindowCreatedNotification: try windowCreated(element, pid)
|
||||
case kAXMainWindowChangedNotification,
|
||||
kAXFocusedWindowChangedNotification: try focusedWindowChanged(element, pid)
|
||||
case kAXUIElementDestroyedNotification: try windowDestroyed(element, pid)
|
||||
case kAXUIElementDestroyedNotification: try windowDestroyed(element)
|
||||
case kAXWindowMiniaturizedNotification,
|
||||
kAXWindowDeminiaturizedNotification: try windowMiniaturizedOrDeminiaturized(element, type)
|
||||
case kAXTitleChangedNotification: try windowTitleChanged(element, pid)
|
||||
case kAXWindowResizedNotification: try windowResized(element)
|
||||
case kAXWindowMovedNotification: try windowMoved(element)
|
||||
case kAXFocusedUIElementChangedNotification: try focusedUiElementChanged(pid)
|
||||
default: return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func focusedUiElementChanged(_ pid: pid_t) throws {
|
||||
// this event is the only event triggered when the user tabs a window. However, macOS implementation is lazy: a
|
||||
// window freshly tabbed will still return the size/position/screenshot as when it was still a standalone
|
||||
// window. Only when the user clicks on the tab, and it gets displayed, will the tabbed window actually resize
|
||||
// (it will send the windowResized event)
|
||||
pid.retryToRefreshTabsUntilScreenIsNotAnimating { App.app.refreshOpenUi($0) }
|
||||
}
|
||||
|
||||
fileprivate func applicationActivated(_ element: AXUIElement, _ pid: pid_t) throws {
|
||||
let appFocusedWindow = try element.focusedWindow()
|
||||
let wid = try appFocusedWindow?.cgWindowId()
|
||||
@@ -138,7 +129,7 @@ fileprivate func focusedWindowChanged(_ element: AXUIElement, _ pid: pid_t) thro
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func windowDestroyed(_ element: AXUIElement, _ pid: pid_t) throws {
|
||||
fileprivate func windowDestroyed(_ element: AXUIElement) throws {
|
||||
let wid = try element.cgWindowId()
|
||||
DispatchQueue.main.async {
|
||||
if let index = (Windows.list.firstIndex { $0.isEqualRobust(element, wid) }) {
|
||||
@@ -146,15 +137,8 @@ fileprivate func windowDestroyed(_ element: AXUIElement, _ pid: pid_t) throws {
|
||||
Windows.removeAndUpdateFocus(window)
|
||||
let windowlessApp = window.application.addWindowslessAppsIfNeeded()
|
||||
if Windows.list.count > 0 {
|
||||
// closing a tab may make another tab visible; we refresh tab status
|
||||
pid.retryToRefreshTabsUntilScreenIsNotAnimating { windows in
|
||||
Windows.moveFocusedWindowIndexAfterWindowDestroyedInBackground(index)
|
||||
if let windowlessApp = windowlessApp {
|
||||
App.app.refreshOpenUi(windows + windowlessApp)
|
||||
} else {
|
||||
App.app.refreshOpenUi(windows)
|
||||
}
|
||||
}
|
||||
Windows.moveFocusedWindowIndexAfterWindowDestroyedInBackground(index)
|
||||
App.app.refreshOpenUi(windowlessApp)
|
||||
} else {
|
||||
App.app.hideUi()
|
||||
}
|
||||
@@ -180,10 +164,7 @@ fileprivate func windowTitleChanged(_ element: AXUIElement, _ pid: pid_t) throws
|
||||
if let window = (Windows.list.first { $0.isEqualRobust(element, wid) }),
|
||||
newTitle != nil && newTitle != window.title {
|
||||
window.title = newTitle!
|
||||
// refreshing tabs during title change helps mitigate some false positives of tab detection
|
||||
pid.retryToRefreshTabsUntilScreenIsNotAnimating {
|
||||
App.app.refreshOpenUi($0)
|
||||
}
|
||||
App.app.refreshOpenUi([window])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,6 +247,8 @@ class App: AppCenterApplication, NSApplicationDelegate {
|
||||
debugPrint("showUiOrCycleSelection: isFirstSummon")
|
||||
isFirstSummon = false
|
||||
if Windows.list.count == 0 || CGWindow.isMissionControlActive() { hideUi(); return }
|
||||
// TODO: can the CGS call inside detectTabbedWindows introduce latency when WindowServer is busy?
|
||||
Windows.detectTabbedWindows()
|
||||
// TODO: find a way to update space info when spaces are changed, instead of on every trigger
|
||||
// replace with:
|
||||
// So far, the best signal I've found is to watch com.apple.dock for the uiElementDestroyed notification.
|
||||
|
||||
Reference in new issue
Block a user