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
|
index 3e1ac74..d3015e2 100644
|
||||||
--- a/src/nsterm.m
|
--- a/src/nsterm.m
|
||||||
+++ b/src/nsterm.m
|
+++ b/src/nsterm.m
|
||||||
@@ -8758,6 +8758,552 @@ - (NSRect)accessibilityFrame
|
@@ -8758,6 +8758,605 @@ - (NSRect)accessibilityFrame
|
||||||
|
|
||||||
@end
|
@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.
|
+ /* For focused line moves: always announce line text explicitly.
|
||||||
+ SelectedTextChanged with granularity=line works for arrow keys,
|
+ SelectedTextChanged with granularity=line works for arrow keys,
|
||||||
+ but C-n/C-p need the explicit announcement (VoiceOver processes
|
+ but C-n/C-p need the explicit announcement (VoiceOver processes
|
||||||
@@ -317,7 +371,6 @@ index 3e1ac74..d3015e2 100644
|
|||||||
+ }
|
+ }
|
||||||
+
|
+
|
||||||
+ unbind_to (count2, Qnil);
|
+ unbind_to (count2, Qnil);
|
||||||
+ unblock_input ();
|
|
||||||
+
|
+
|
||||||
+ /* Final fallback: read current line at point. */
|
+ /* Final fallback: read current line at point. */
|
||||||
+ if (!announceText)
|
+ if (!announceText)
|
||||||
|
|||||||
@@ -435,9 +435,9 @@ index 8d44b5f..29b646d 100644
|
|||||||
specpdl_ref count2 = SPECPDL_INDEX ();
|
specpdl_ref count2 = SPECPDL_INDEX ();
|
||||||
+ /* Register unblock_input as an unwind action so that if any Lisp
|
+ /* Register unblock_input as an unwind action so that if any Lisp
|
||||||
+ call below signals (triggering a longjmp through unbind_to),
|
+ call below signals (triggering a longjmp through unbind_to),
|
||||||
+ block_input is always paired with an unblock_input. The explicit
|
+ block_input is always paired with an unblock_input. The
|
||||||
+ unblock_input() at the end of the function is still needed for
|
+ unbind_to call at the end of the function unwinds this.
|
||||||
+ the normal (non-signal) path. */
|
+ record_unwind_protect_void plus unbind_to is idempotent. */
|
||||||
+ record_unwind_protect_void (unblock_input);
|
+ record_unwind_protect_void (unblock_input);
|
||||||
record_unwind_current_buffer ();
|
record_unwind_current_buffer ();
|
||||||
if (b != current_buffer)
|
if (b != current_buffer)
|
||||||
@@ -518,7 +518,7 @@ index 8d44b5f..29b646d 100644
|
|||||||
NSInteger direction = ns_ax_text_selection_direction_discontiguous;
|
NSInteger direction = ns_ax_text_selection_direction_discontiguous;
|
||||||
if (point > oldPoint)
|
if (point > oldPoint)
|
||||||
direction = ns_ax_text_selection_direction_next;
|
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;
|
granularity = ns_ax_text_selection_granularity_line;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -541,6 +541,21 @@ index 8d44b5f..29b646d 100644
|
|||||||
+ handles them correctly. */
|
+ handles them correctly. */
|
||||||
+ if (emacsMovedCursor && !isCtrlNP)
|
+ if (emacsMovedCursor && !isCtrlNP)
|
||||||
+ direction = ns_ax_text_selection_direction_discontiguous;
|
+ 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. */
|
/* Post notifications for focused and non-focused elements. */
|
||||||
if ([self isAccessibilityFocused])
|
if ([self isAccessibilityFocused])
|
||||||
@@ -592,7 +607,7 @@ index 8d44b5f..29b646d 100644
|
|||||||
scrollbarsNeedingUpdate = 0;
|
scrollbarsNeedingUpdate = 0;
|
||||||
fs_state = FULLSCREEN_NONE;
|
fs_state = FULLSCREEN_NONE;
|
||||||
fs_before_fs = next_maximized = -1;
|
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
|
The existing elements carry cached state (modiff, point) from the
|
||||||
previous redisplay cycle. Rebuilding first would create fresh
|
previous redisplay cycle. Rebuilding first would create fresh
|
||||||
elements with current values, making change detection impossible. */
|
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
|
+ set_buffer_internal_1 is preferred over set_buffer_internal in
|
||||||
+ a redisplay context: it skips point-motion hooks that could
|
+ a redisplay context: it skips point-motion hooks that could
|
||||||
+ trigger further redisplay or modify buffer state unexpectedly. */
|
+ trigger further redisplay or modify buffer state unexpectedly. */
|
||||||
|
+ block_input ();
|
||||||
+ specpdl_ref count = SPECPDL_INDEX ();
|
+ specpdl_ref count = SPECPDL_INDEX ();
|
||||||
|
+ record_unwind_protect_void (unblock_input);
|
||||||
+ record_unwind_current_buffer ();
|
+ record_unwind_current_buffer ();
|
||||||
+ set_buffer_internal_1 (eb);
|
+ set_buffer_internal_1 (eb);
|
||||||
+ Lisp_Object ls = Fbuffer_string ();
|
+ Lisp_Object ls = Fbuffer_string ();
|
||||||
|
|||||||
Reference in New Issue
Block a user