|
91 | 91 | CFMachPortRef eventTap = NULL; |
92 | 92 | static char pathBuffer[PROC_PIDPATHINFO_MAXSIZE]; |
93 | 93 | static bool activated_by_task_switcher = false; |
| 94 | +static bool waitingForWindowChange = false; |
| 95 | +static AXObserverRef windowObserver = NULL; |
94 | 96 | static AXUIElementRef _accessibility_object = AXUIElementCreateSystemWide(); |
95 | 97 | static AXUIElementRef _previousFinderWindow = NULL; |
96 | 98 | static AXUIElementRef _dock_app = NULL; |
@@ -862,8 +864,74 @@ - (void)onWindowFocused:(NSNumber *)_window { |
862 | 864 | } else if (verbose) { NSLog(@"Ignoring window focused event"); } |
863 | 865 | } |
864 | 866 | #endif |
| 867 | + |
| 868 | +- (void)onWindowFocusChanged:(NSNumber *)elementPtr { |
| 869 | + if (!activated_by_task_switcher) { |
| 870 | + if (verbose) { NSLog(@"Window focus changed but not from task switcher, ignoring"); } |
| 871 | + return; // Double-check filter |
| 872 | + } |
| 873 | + |
| 874 | + if (verbose) { NSLog(@"Processing window focus change from task switcher"); } |
| 875 | + |
| 876 | + AXUIElementRef focusedWindow = (AXUIElementRef)elementPtr.unsignedLongValue; |
| 877 | + |
| 878 | + // Reuse existing warp logic but for the focused window |
| 879 | + if (warpMouse && focusedWindow) { |
| 880 | + CGPoint warpPoint = get_mousepoint(focusedWindow); |
| 881 | + if (warpPoint.x != 0 || warpPoint.y != 0) { |
| 882 | + if (verbose) { NSLog(@"Warping cursor to focused window"); } |
| 883 | + CGWarpMouseCursorPosition(warpPoint); |
| 884 | + } |
| 885 | + } |
| 886 | + |
| 887 | + // Handle cursor scaling if needed |
| 888 | + if (cursorScale != oldScale) { |
| 889 | + if (verbose) { NSLog(@"Scheduling cursor scaling"); } |
| 890 | + [self performSelector:@selector(onSetCursorScale:) |
| 891 | + withObject:[NSNumber numberWithFloat:cursorScale] |
| 892 | + afterDelay:SCALE_DELAY_MS/1000.0]; |
| 893 | + [self performSelector:@selector(onSetCursorScale:) |
| 894 | + withObject:[NSNumber numberWithFloat:oldScale] |
| 895 | + afterDelay:SCALE_DURATION_MS/1000.0]; |
| 896 | + } |
| 897 | +} |
| 898 | + |
| 899 | +- (void)clearWaitingForWindowChange { |
| 900 | + if (waitingForWindowChange) { |
| 901 | + if (verbose) { NSLog(@"Timeout: clearing waitingForWindowChange flag"); } |
| 902 | + waitingForWindowChange = false; |
| 903 | + } |
| 904 | +} |
865 | 905 | @end // MDWorkspaceWatcher |
866 | 906 |
|
| 907 | +//-----------------------------------------------AX observer callback---------------------------------------------- |
| 908 | + |
| 909 | +void windowFocusChangedCallback(AXObserverRef observer, AXUIElementRef element, |
| 910 | + CFStringRef notification, void *refcon) { |
| 911 | + if (!waitingForWindowChange) { |
| 912 | + if (verbose) { NSLog(@"Window focus notification received but not waiting, ignoring"); } |
| 913 | + return; // Filter: only when expecting change |
| 914 | + } |
| 915 | + |
| 916 | + if (verbose) { NSLog(@"Window focus changed notification received"); } |
| 917 | + |
| 918 | + waitingForWindowChange = false; |
| 919 | + |
| 920 | + // Get the workspace watcher from refcon (passed during setup) |
| 921 | + MDWorkspaceWatcher *watcher = (__bridge MDWorkspaceWatcher *)refcon; |
| 922 | + if (watcher) { |
| 923 | + // Cancel the timeout since we got the notification |
| 924 | + [NSObject cancelPreviousPerformRequestsWithTarget:watcher |
| 925 | + selector:@selector(clearWaitingForWindowChange) |
| 926 | + object:nil]; |
| 927 | + |
| 928 | + // Delay to match existing app activation timing |
| 929 | + [watcher performSelector:@selector(onWindowFocusChanged:) |
| 930 | + withObject:[NSNumber numberWithUnsignedLong:(uint64_t)element] |
| 931 | + afterDelay:ACTIVATE_DELAY_MS/1000.0]; |
| 932 | + } |
| 933 | +} |
| 934 | + |
867 | 935 | //----------------------------------------------configuration----------------------------------------------- |
868 | 936 |
|
869 | 937 | const NSString *kDelay = @"delay"; |
@@ -1349,49 +1417,53 @@ void onTick() { |
1349 | 1417 | } |
1350 | 1418 |
|
1351 | 1419 | CGEventRef eventTapHandler(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *userInfo) { |
1352 | | - // Focus-on-demand logic - handle BEFORE existing task switcher logic |
1353 | | - if (focusOnDemand && (type == kCGEventKeyDown || |
1354 | | - type == kCGEventFlagsChanged || |
1355 | | - type == kCGEventLeftMouseDown || |
1356 | | - type == kCGEventRightMouseDown || |
1357 | | - type == kCGEventOtherMouseDown || |
1358 | | - type == kCGEventScrollWheel)) { |
1359 | | - handleFocusOnDemand(event); |
1360 | | - } |
1361 | | - |
1362 | 1420 | static bool commandTabPressed = false; |
1363 | 1421 | if (type == kCGEventFlagsChanged && commandTabPressed) { |
1364 | 1422 | if (!activated_by_task_switcher) { |
1365 | 1423 | activated_by_task_switcher = true; |
1366 | | - ignoreTimes = 3; |
1367 | | - } |
1368 | | - } |
1369 | | - |
1370 | | - static bool commandGravePressed = false; |
1371 | | - if (type == kCGEventFlagsChanged && commandGravePressed) { |
1372 | | - if (!activated_by_task_switcher) { |
1373 | | - activated_by_task_switcher = true; |
1374 | | - ignoreTimes = 3; |
1375 | | - [workspaceWatcher onAppActivated]; |
| 1424 | + // Extend ignore period for focus-on-demand to prevent race condition |
| 1425 | + ignoreTimes = focusOnDemand ? 30 : 3; |
1376 | 1426 | } |
1377 | 1427 | } |
1378 | 1428 |
|
1379 | 1429 | commandTabPressed = false; |
1380 | | - commandGravePressed = false; |
1381 | 1430 | if (type == kCGEventKeyDown) { |
1382 | 1431 | CGKeyCode keycode = (CGKeyCode) CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode); |
1383 | 1432 | if (keycode == kVK_Tab) { |
1384 | 1433 | CGEventFlags flags = CGEventGetFlags(event); |
1385 | 1434 | commandTabPressed = (flags & kCGEventFlagMaskCommand) == kCGEventFlagMaskCommand; |
1386 | | - } else if (warpMouse && keycode == kVK_ANSI_Grave) { |
| 1435 | + } else if ((warpMouse || focusOnDemand) && keycode == kVK_ANSI_Grave) { |
1387 | 1436 | CGEventFlags flags = CGEventGetFlags(event); |
1388 | | - commandGravePressed = (flags & kCGEventFlagMaskCommand) == kCGEventFlagMaskCommand; |
| 1437 | + if ((flags & kCGEventFlagMaskCommand) == kCGEventFlagMaskCommand) { |
| 1438 | + if (!activated_by_task_switcher) { |
| 1439 | + activated_by_task_switcher = true; |
| 1440 | + waitingForWindowChange = true; // Set flag to wait for AX notification |
| 1441 | + // Extend ignore period for focus-on-demand to prevent race condition |
| 1442 | + ignoreTimes = focusOnDemand ? 30 : 3; |
| 1443 | + if (verbose) { NSLog(@"Cmd+` detected, waiting for window focus change notification"); } |
| 1444 | + // Schedule timeout to clear flag if notification doesn't arrive |
| 1445 | + [workspaceWatcher performSelector:@selector(clearWaitingForWindowChange) |
| 1446 | + withObject:nil |
| 1447 | + afterDelay:1.0]; // 1 second timeout |
| 1448 | + // Remove synchronous appActivated() call - now handled by AX observer |
| 1449 | + } |
| 1450 | + } |
1389 | 1451 | } |
1390 | 1452 | } else if (type == kCGEventTapDisabledByTimeout || type == kCGEventTapDisabledByUserInput) { |
1391 | 1453 | if (verbose) { NSLog(@"Got event tap disabled event, re-enabling..."); } |
1392 | 1454 | CGEventTapEnable(eventTap, true); |
1393 | 1455 | } |
1394 | 1456 |
|
| 1457 | + // Focus-on-demand logic - handle AFTER task switcher logic |
| 1458 | + if (focusOnDemand && !activated_by_task_switcher && (type == kCGEventKeyDown || |
| 1459 | + type == kCGEventFlagsChanged || |
| 1460 | + type == kCGEventLeftMouseDown || |
| 1461 | + type == kCGEventRightMouseDown || |
| 1462 | + type == kCGEventOtherMouseDown || |
| 1463 | + type == kCGEventScrollWheel)) { |
| 1464 | + handleFocusOnDemand(event); |
| 1465 | + } |
| 1466 | + |
1395 | 1467 | return event; |
1396 | 1468 | } |
1397 | 1469 |
|
@@ -1547,6 +1619,31 @@ int main(int argc, const char * argv[]) { |
1547 | 1619 | if (verbose) { NSLog(@"Got run loop source: %s", runLoopSource ? "YES" : "NO"); } |
1548 | 1620 |
|
1549 | 1621 | workspaceWatcher = [[MDWorkspaceWatcher alloc] init]; |
| 1622 | + |
| 1623 | + // Setup AX observer for window focus changes (needed for Cmd+` cursor warp) |
| 1624 | + // Must be done after workspaceWatcher is created so we can pass it as refcon |
| 1625 | + if (warpMouse || focusOnDemand) { |
| 1626 | + AXError axError = AXObserverCreate(getpid(), windowFocusChangedCallback, &windowObserver); |
| 1627 | + if (axError == kAXErrorSuccess && windowObserver) { |
| 1628 | + axError = AXObserverAddNotification(windowObserver, _accessibility_object, |
| 1629 | + kAXFocusedWindowChangedNotification, |
| 1630 | + (__bridge void *)workspaceWatcher); |
| 1631 | + if (axError == kAXErrorSuccess) { |
| 1632 | + CFRunLoopAddSource(CFRunLoopGetCurrent(), |
| 1633 | + AXObserverGetRunLoopSource(windowObserver), |
| 1634 | + kCFRunLoopDefaultMode); |
| 1635 | + if (verbose) { NSLog(@"AX observer for window focus changes: SUCCESS"); } |
| 1636 | + } else { |
| 1637 | + if (verbose) { NSLog(@"Failed to add AX notification: %d", axError); } |
| 1638 | + if (windowObserver) { |
| 1639 | + CFRelease(windowObserver); |
| 1640 | + windowObserver = NULL; |
| 1641 | + } |
| 1642 | + } |
| 1643 | + } else { |
| 1644 | + if (verbose) { NSLog(@"Failed to create AX observer: %d", axError); } |
| 1645 | + } |
| 1646 | + } |
1550 | 1647 | #ifdef FOCUS_FIRST |
1551 | 1648 | if (altTaskSwitcher || raiseDelayCount || delayCount) { |
1552 | 1649 | #else |
|
0 commit comments