v15.3: fix VoiceOver cursor not following during typing

Root cause (confirmed via WebKit/Chromium source): ValueChanged (edit)
and SelectedTextChanged (cursor move) are MUTUALLY EXCLUSIVE — apps
must never send both for the same user action. VoiceOver enters
'typing mode' on Edit notifications and suppresses/ignores concurrent
SelectionMove notifications, causing the cursor to appear stuck.

Fix: (1) Update cachedPoint inside the modiff branch so the
selection-move check doesn't trigger for edit-caused point changes.
(2) Change 'if' to 'else if' for explicit mutual exclusion.

Source: WebKit AXObjectCacheMac.mm — postTextStateChangePlatformNotification
vs postTextSelectionChangePlatformNotification are separate code paths
that never fire for the same event.
This commit is contained in:
2026-02-26 14:32:19 +01:00
parent 9d963a6ab1
commit fd523f501f

View File

@@ -73,7 +73,7 @@ index 7c1ee4c..4abeafe 100644
diff --git a/src/nsterm.m b/src/nsterm.m
index 932d209..792f7c5 100644
index 932d209..5252e6d 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -1104,6 +1104,11 @@ ns_update_end (struct frame *f)
@@ -126,7 +126,7 @@ index 932d209..792f7c5 100644
ns_focus (f, NULL, 0);
NSGraphicsContext *ctx = [NSGraphicsContext currentContext];
@@ -6847,6 +6883,756 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action)
@@ -6847,6 +6883,764 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action)
}
#endif
@@ -743,6 +743,11 @@ index 932d209..792f7c5 100644
+ }
+
+ self.cachedModiff = modiff;
+ /* Update cachedPoint here so the selection-move branch below
+ does NOT fire for point changes caused by edits. WebKit and
+ Chromium never send both ValueChanged and SelectedTextChanged
+ for the same user action — they are mutually exclusive. */
+ self.cachedPoint = point;
+
+ NSDictionary *change = @{
+ @"AXTextEditType": @3,
@@ -758,8 +763,11 @@ index 932d209..792f7c5 100644
+ }
+
+ /* --- Cursor moved or selection changed → line reading ---
+ kAXTextStateChangeTypeSelectionMove = 2. */
+ if (point != self.cachedPoint || markActive != self.cachedMarkActive)
+ kAXTextStateChangeTypeSelectionMove = 2.
+ Use 'else if' — edits and selection moves are mutually exclusive
+ per the WebKit/Chromium pattern. VoiceOver gets confused if
+ both notifications arrive in the same runloop iteration. */
+ else if (point != self.cachedPoint || markActive != self.cachedMarkActive)
+ {
+ ptrdiff_t oldPoint = self.cachedPoint;
+ self.cachedPoint = point;
@@ -883,7 +891,7 @@ index 932d209..792f7c5 100644
/* ==========================================================================
EmacsView implementation
@@ -6889,6 +7675,7 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action)
@@ -6889,6 +7683,7 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action)
[layer release];
#endif
@@ -891,7 +899,7 @@ index 932d209..792f7c5 100644
[[self menu] release];
[super dealloc];
}
@@ -8237,6 +9024,18 @@ ns_in_echo_area (void)
@@ -8237,6 +9032,18 @@ ns_in_echo_area (void)
XSETFRAME (event.frame_or_window, emacsframe);
kbd_buffer_store_event (&event);
ns_send_appdefined (-1); // Kick main loop
@@ -910,7 +918,7 @@ index 932d209..792f7c5 100644
}
@@ -9474,6 +10273,290 @@ ns_in_echo_area (void)
@@ -9474,6 +10281,290 @@ ns_in_echo_area (void)
return fs_state;
}
@@ -1201,7 +1209,7 @@ index 932d209..792f7c5 100644
@end /* EmacsView */
@@ -9941,6 +11024,14 @@ nswindow_orderedIndex_sort (id w1, id w2, void *c)
@@ -9941,6 +11032,14 @@ nswindow_orderedIndex_sort (id w1, id w2, void *c)
return [super accessibilityAttributeValue:attribute];
}