Skip to content

Commit 17de1f4

Browse files
committed
Fix Cmd+` cursor warp timing issue with AX observer
- Replace synchronous appActivated() call with AX observer approach - Add kAXFocusedWindowChangedNotification listener for proper timing - Implement waitingForWindowChange flag to filter notifications - Add timeout mechanism to prevent stuck states - Move cursor warp to happen after window actually becomes focused - Maintain existing filtering for task-switcher-initiated changes only This fixes the race condition where cursor would warp to the wrong window (previous window A instead of target window B) when using Cmd+` to switch between windows of the same application.
1 parent ea3721b commit 17de1f4

File tree

1 file changed

+120
-23
lines changed

1 file changed

+120
-23
lines changed

AutoRaise.mm

Lines changed: 120 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@
9191
CFMachPortRef eventTap = NULL;
9292
static char pathBuffer[PROC_PIDPATHINFO_MAXSIZE];
9393
static bool activated_by_task_switcher = false;
94+
static bool waitingForWindowChange = false;
95+
static AXObserverRef windowObserver = NULL;
9496
static AXUIElementRef _accessibility_object = AXUIElementCreateSystemWide();
9597
static AXUIElementRef _previousFinderWindow = NULL;
9698
static AXUIElementRef _dock_app = NULL;
@@ -862,8 +864,74 @@ - (void)onWindowFocused:(NSNumber *)_window {
862864
} else if (verbose) { NSLog(@"Ignoring window focused event"); }
863865
}
864866
#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+
}
865905
@end // MDWorkspaceWatcher
866906

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+
867935
//----------------------------------------------configuration-----------------------------------------------
868936

869937
const NSString *kDelay = @"delay";
@@ -1349,49 +1417,53 @@ void onTick() {
13491417
}
13501418

13511419
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-
13621420
static bool commandTabPressed = false;
13631421
if (type == kCGEventFlagsChanged && commandTabPressed) {
13641422
if (!activated_by_task_switcher) {
13651423
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;
13761426
}
13771427
}
13781428

13791429
commandTabPressed = false;
1380-
commandGravePressed = false;
13811430
if (type == kCGEventKeyDown) {
13821431
CGKeyCode keycode = (CGKeyCode) CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode);
13831432
if (keycode == kVK_Tab) {
13841433
CGEventFlags flags = CGEventGetFlags(event);
13851434
commandTabPressed = (flags & kCGEventFlagMaskCommand) == kCGEventFlagMaskCommand;
1386-
} else if (warpMouse && keycode == kVK_ANSI_Grave) {
1435+
} else if ((warpMouse || focusOnDemand) && keycode == kVK_ANSI_Grave) {
13871436
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+
}
13891451
}
13901452
} else if (type == kCGEventTapDisabledByTimeout || type == kCGEventTapDisabledByUserInput) {
13911453
if (verbose) { NSLog(@"Got event tap disabled event, re-enabling..."); }
13921454
CGEventTapEnable(eventTap, true);
13931455
}
13941456

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+
13951467
return event;
13961468
}
13971469

@@ -1547,6 +1619,31 @@ int main(int argc, const char * argv[]) {
15471619
if (verbose) { NSLog(@"Got run loop source: %s", runLoopSource ? "YES" : "NO"); }
15481620

15491621
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+
}
15501647
#ifdef FOCUS_FIRST
15511648
if (altTaskSwitcher || raiseDelayCount || delayCount) {
15521649
#else

0 commit comments

Comments
 (0)