fix: better tabs detection + fix issues with some apps (closes #1540)

Maybe fixed #647 #718
This commit is contained in:
Louis Pontoise committed 2022-05-27 00:33:55 +09:00
1 parent 1a9fc593d1
commit abd54b3939
6 files changed
+36 -94

No files matched your search

-34
View File
@@ -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 {
+12 -10
View File
@@ -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
+8 -24
View File
@@ -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, {
+7
View File
@@ -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() {
+7 -26
View File
@@ -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])
}
}
}
+2
View File
@@ -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.