Fix three VoiceOver bugs: crash, cursor sync, word announcement
Bug 1 (crash): postEchoAreaAnnouncementIfNeeded called Fbuffer_string() without block_input, allowing timer events to interleave with Lisp calls and corrupt buffer state. Added block_input/unblock_input via record_unwind_protect_void. Also removed unpaired unblock_input() in postCompletionAnnouncementForBuffer (patch 0003) that became a double-unblock after patch 0008 added block_input + unwind protect. Bug 2 (cursor sync): VoiceOver browse cursor did not follow Emacs keyboard cursor because SelectedTextChanged with discontiguous direction alone is not sufficient for VoiceOver to re-anchor. Added NSAccessibilityFocusedUIElementChangedNotification post when Emacs moves the cursor across a line boundary, forcing VoiceOver to re-query accessibilitySelectedTextRange. Bug 3 (word off-by-one): Evil w (next-word) read the previous word instead of the destination word. VO auto-speech from SelectedTextChanged with direction=next+granularity=word reads the traversed word, not the arrived-at word. Added explicit word announcement (like char moves) that reads the word AT the new cursor position using whitespace-delimited word boundary scan.
This commit is contained in:
@@ -29,7 +29,7 @@ diff --git a/src/nsterm.m b/src/nsterm.m
|
||||
index 3e1ac74..d3015e2 100644
|
||||
--- a/src/nsterm.m
|
||||
+++ b/src/nsterm.m
|
||||
@@ -8758,6 +8758,552 @@ - (NSRect)accessibilityFrame
|
||||
@@ -8758,6 +8758,605 @@ - (NSRect)accessibilityFrame
|
||||
|
||||
@end
|
||||
|
||||
@@ -154,6 +154,60 @@ index 3e1ac74..d3015e2 100644
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ /* For word moves: explicit announcement of word AT new point.
|
||||
+ VO auto-speech from SelectedTextChanged with direction=next
|
||||
+ and granularity=word reads the word that was traversed (the
|
||||
+ source word), not the word arrived at. This explicit
|
||||
+ announcement reads the destination word instead, matching
|
||||
+ user expectation ("w" jumps to next word and reads it). */
|
||||
+ BOOL isWordMove
|
||||
+ = (!markActive && !oldMarkActive
|
||||
+ && granularity
|
||||
+ == ns_ax_text_selection_granularity_word);
|
||||
+ if (isWordMove && cachedText)
|
||||
+ {
|
||||
+ NSCharacterSet *ws
|
||||
+ = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
||||
+ NSUInteger point_idx
|
||||
+ = [self accessibilityIndexForCharpos:point];
|
||||
+ NSUInteger tlen = [cachedText length];
|
||||
+ if (point_idx < tlen
|
||||
+ && ![ws characterIsMember:
|
||||
+ [cachedText characterAtIndex:point_idx]])
|
||||
+ {
|
||||
+ /* Find word boundaries around point. */
|
||||
+ NSUInteger wstart = point_idx;
|
||||
+ while (wstart > 0
|
||||
+ && ![ws characterIsMember:
|
||||
+ [cachedText characterAtIndex:wstart - 1]])
|
||||
+ wstart--;
|
||||
+ NSUInteger wend = point_idx;
|
||||
+ while (wend < tlen
|
||||
+ && ![ws characterIsMember:
|
||||
+ [cachedText characterAtIndex:wend]])
|
||||
+ wend++;
|
||||
+ if (wend > wstart)
|
||||
+ {
|
||||
+ NSString *word
|
||||
+ = [cachedText substringWithRange:
|
||||
+ NSMakeRange (wstart, wend - wstart)];
|
||||
+ word = [word stringByTrimmingCharactersInSet: ws];
|
||||
+ if ([word length] > 0)
|
||||
+ {
|
||||
+ NSDictionary *annInfo = @{
|
||||
+ NSAccessibilityAnnouncementKey: word,
|
||||
+ NSAccessibilityPriorityKey:
|
||||
+ @(NSAccessibilityPriorityHigh)
|
||||
+ };
|
||||
+ ns_ax_post_notification_with_info (
|
||||
+ NSApp,
|
||||
+ NSAccessibilityAnnouncementRequestedNotification,
|
||||
+ annInfo);
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ /* For focused line moves: always announce line text explicitly.
|
||||
+ SelectedTextChanged with granularity=line works for arrow keys,
|
||||
+ but C-n/C-p need the explicit announcement (VoiceOver processes
|
||||
@@ -317,7 +371,6 @@ index 3e1ac74..d3015e2 100644
|
||||
+ }
|
||||
+
|
||||
+ unbind_to (count2, Qnil);
|
||||
+ unblock_input ();
|
||||
+
|
||||
+ /* Final fallback: read current line at point. */
|
||||
+ if (!announceText)
|
||||
|
||||
@@ -435,9 +435,9 @@ index 8d44b5f..29b646d 100644
|
||||
specpdl_ref count2 = SPECPDL_INDEX ();
|
||||
+ /* Register unblock_input as an unwind action so that if any Lisp
|
||||
+ call below signals (triggering a longjmp through unbind_to),
|
||||
+ block_input is always paired with an unblock_input. The explicit
|
||||
+ unblock_input() at the end of the function is still needed for
|
||||
+ the normal (non-signal) path. */
|
||||
+ block_input is always paired with an unblock_input. The
|
||||
+ unbind_to call at the end of the function unwinds this.
|
||||
+ record_unwind_protect_void plus unbind_to is idempotent. */
|
||||
+ record_unwind_protect_void (unblock_input);
|
||||
record_unwind_current_buffer ();
|
||||
if (b != current_buffer)
|
||||
@@ -518,7 +518,7 @@ index 8d44b5f..29b646d 100644
|
||||
NSInteger direction = ns_ax_text_selection_direction_discontiguous;
|
||||
if (point > oldPoint)
|
||||
direction = ns_ax_text_selection_direction_next;
|
||||
@@ -9488,6 +9670,26 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
|
||||
@@ -9488,6 +9670,41 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
|
||||
granularity = ns_ax_text_selection_granularity_line;
|
||||
}
|
||||
|
||||
@@ -541,6 +541,21 @@ index 8d44b5f..29b646d 100644
|
||||
+ handles them correctly. */
|
||||
+ if (emacsMovedCursor && !isCtrlNP)
|
||||
+ direction = ns_ax_text_selection_direction_discontiguous;
|
||||
+
|
||||
+ /* Post FocusedUIElementChanged when Emacs moved the cursor
|
||||
+ across a line boundary so VoiceOver re-anchors its browse
|
||||
+ cursor at the new accessibilitySelectedTextRange.
|
||||
+ SelectedTextChanged with discontiguous direction alone is
|
||||
+ not sufficient; VoiceOver requires this notification to
|
||||
+ re-query the focused element and update its internal
|
||||
+ browse position. */
|
||||
+ if (emacsMovedCursor
|
||||
+ && granularity
|
||||
+ == ns_ax_text_selection_granularity_line
|
||||
+ && [self isAccessibilityFocused])
|
||||
+ ns_ax_post_notification (
|
||||
+ self,
|
||||
+ NSAccessibilityFocusedUIElementChangedNotification);
|
||||
+
|
||||
/* Post notifications for focused and non-focused elements. */
|
||||
if ([self isAccessibilityFocused])
|
||||
@@ -592,7 +607,7 @@ index 8d44b5f..29b646d 100644
|
||||
scrollbarsNeedingUpdate = 0;
|
||||
fs_state = FULLSCREEN_NONE;
|
||||
fs_before_fs = next_maximized = -1;
|
||||
@@ -12688,6 +12909,152 @@ - (id)accessibilityFocusedUIElement
|
||||
@@ -12688,6 +12909,154 @@ - (id)accessibilityFocusedUIElement
|
||||
The existing elements carry cached state (modiff, point) from the
|
||||
previous redisplay cycle. Rebuilding first would create fresh
|
||||
elements with current values, making change detection impossible. */
|
||||
@@ -631,7 +646,9 @@ index 8d44b5f..29b646d 100644
|
||||
+ set_buffer_internal_1 is preferred over set_buffer_internal in
|
||||
+ a redisplay context: it skips point-motion hooks that could
|
||||
+ trigger further redisplay or modify buffer state unexpectedly. */
|
||||
+ block_input ();
|
||||
+ specpdl_ref count = SPECPDL_INDEX ();
|
||||
+ record_unwind_protect_void (unblock_input);
|
||||
+ record_unwind_current_buffer ();
|
||||
+ set_buffer_internal_1 (eb);
|
||||
+ Lisp_Object ls = Fbuffer_string ();
|
||||
|
||||
Reference in New Issue
Block a user