Fix VO cursor sync, word double-read, and VO->Emacs positioning

patch 0002: Do not activate mark in setAccessibilitySelectedTextRange.
VoiceOver range.length is an internal word-boundary hint, not a text
selection.  Activating the mark made accessibilitySelectedTextRange
return a non-zero length, causing VoiceOver to position its browse
cursor at the END of the selection instead of the START.

patch 0003: Fix word announcement double-read and punctuation.
- Explicit word announcement only for Emacs-initiated (discontiguous)
  moves; VO-initiated (sequential) word navigation relies on VO
  auto-speech from the granularity=word notification, preventing
  double-read.
- Strip trailing/leading punctuation from word announcements using
  punctuationCharacterSet so 'Ahoj,' is announced as 'Ahoj'.

patch 0008: Post FocusedUIElementChangedNotification on the EmacsView
(not just on line-granularity moves) for all Emacs-initiated cursor
movements.  Posting on the view causes VoiceOver to re-query
accessibilityFocusedUIElement and re-anchor its browse cursor at the
current accessibilitySelectedTextRange.  Also fixes a pre-existing
off-by-one in the macos.texi hunk header.
This commit is contained in:
2026-03-02 11:12:48 +01:00
parent 760992224c
commit 91981bf77e
3 changed files with 24 additions and 33 deletions

View File

@@ -30,7 +30,7 @@ diff --git a/src/nsterm.m b/src/nsterm.m
index 852e7f9..3e1ac74 100644 index 852e7f9..3e1ac74 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -7631,6 +7631,1129 @@ - (id)accessibilityTopLevelUIElement @@ -7631,6 +7631,1121 @@ - (id)accessibilityTopLevelUIElement
@end @end
@@ -833,20 +833,12 @@ index 852e7f9..3e1ac74 100644
+ +
+ SET_PT_BOTH (charpos, CHAR_TO_BYTE (charpos)); + SET_PT_BOTH (charpos, CHAR_TO_BYTE (charpos));
+ +
+ /* Keep mark state aligned with requested selection range. */ + /* Always deactivate mark: VoiceOver range.length is an internal
+ if (range.length > 0) + word boundary hint, not a text selection. Activating the mark
+ { + makes accessibilitySelectedTextRange return a non-zero length,
+ ptrdiff_t mark_charpos = [self charposForAccessibilityIndex: + which confuses VoiceOver into positioning its browse cursor at
+ range.location + range.length]; + the END of the selection instead of the start. */
+ if (mark_charpos > BUF_ZV (b))
+ mark_charpos = BUF_ZV (b);
+ Fset_marker (BVAR (b, mark), make_fixnum (mark_charpos),
+ Fcurrent_buffer ());
+ bset_mark_active (b, Qt);
+ }
+ else
+ bset_mark_active (b, Qnil); + bset_mark_active (b, Qnil);
+
+ unbind_to (count, Qnil); + unbind_to (count, Qnil);
+ +
+ /* Update cached state so the next notification cycle doesn't + /* Update cached state so the next notification cycle doesn't

View File

@@ -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,605 @@ - (NSRect)accessibilityFrame @@ -8758,6 +8758,609 @@ - (NSRect)accessibilityFrame
@end @end
@@ -162,8 +162,8 @@ index 3e1ac74..d3015e2 100644
+ user expectation ("w" jumps to next word and reads it). */ + user expectation ("w" jumps to next word and reads it). */
+ BOOL isWordMove + BOOL isWordMove
+ = (!markActive && !oldMarkActive + = (!markActive && !oldMarkActive
+ && granularity + && granularity == ns_ax_text_selection_granularity_word
+ == ns_ax_text_selection_granularity_word); + && direction == ns_ax_text_selection_direction_discontiguous);
+ if (isWordMove && cachedText) + if (isWordMove && cachedText)
+ { + {
+ NSCharacterSet *ws + NSCharacterSet *ws
@@ -191,7 +191,11 @@ index 3e1ac74..d3015e2 100644
+ NSString *word + NSString *word
+ = [cachedText substringWithRange: + = [cachedText substringWithRange:
+ NSMakeRange (wstart, wend - wstart)]; + NSMakeRange (wstart, wend - wstart)];
+ word = [word stringByTrimmingCharactersInSet: ws]; + NSMutableCharacterSet *trims
+ = [ws mutableCopy];
+ [trims formUnionWithCharacterSet:
+ [NSCharacterSet punctuationCharacterSet]];
+ word = [word stringByTrimmingCharactersInSet:trims];
+ if ([word length] > 0) + if ([word length] > 0)
+ { + {
+ NSDictionary *annInfo = @{ + NSDictionary *annInfo = @{

View File

@@ -70,7 +70,7 @@ diff --git a/etc/NEWS b/etc/NEWS
index 2b1f9e6..5766428 100644 index 2b1f9e6..5766428 100644
--- a/etc/NEWS --- a/etc/NEWS
+++ b/etc/NEWS +++ b/etc/NEWS
@@ -4400,16 +4400,20 @@ allowing Emacs users access to speech recognition utilities. @@ -4400,16 +4400,19 @@ allowing Emacs users access to speech recognition utilities.
Note: Accepting this permission allows the use of system APIs, which may Note: Accepting this permission allows the use of system APIs, which may
send user data to Apple's speech recognition servers. send user data to Apple's speech recognition servers.
@@ -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,41 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property @@ -9488,6 +9670,36 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
granularity = ns_ax_text_selection_granularity_line; granularity = ns_ax_text_selection_granularity_line;
} }
@@ -542,19 +542,14 @@ index 8d44b5f..29b646d 100644
+ 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 + /* Post FocusedUIElementChanged on the containing NSView whenever
+ across a line boundary so VoiceOver re-anchors its browse + Emacs moves the cursor independently of VoiceOver. Posting on
+ cursor at the new accessibilitySelectedTextRange. + the view causes VoiceOver to re-query accessibilityFocusedUIElement
+ SelectedTextChanged with discontiguous direction alone is + and re-anchor its browse cursor at the new selection range, without
+ not sufficient; VoiceOver requires this notification to + re-announcing the element name. Required for all granularities. */
+ re-query the focused element and update its internal + if (emacsMovedCursor && [self isAccessibilityFocused])
+ browse position. */
+ if (emacsMovedCursor
+ && granularity
+ == ns_ax_text_selection_granularity_line
+ && [self isAccessibilityFocused])
+ ns_ax_post_notification ( + ns_ax_post_notification (
+ self, + self.emacsView,
+ NSAccessibilityFocusedUIElementChangedNotification); + NSAccessibilityFocusedUIElementChangedNotification);
+ +
/* Post notifications for focused and non-focused elements. */ /* Post notifications for focused and non-focused elements. */