ax: fix VoiceOver cursor sync and word double-read
Bug 1 (VO cursor not following Emacs cursor): - Remove FocusedUIElementChangedNotification on emacsView (was a no-op: VO re-queried the same element) - For Emacs-initiated char/word moves, keep natural next/previous direction instead of forcing discontiguous; SelectedTextChanged with direction=next advances VO browse cursor sequentially - Only force discontiguous for line-boundary crossings and large jumps Bug 2 (word double-read with punctuation): - Root cause was FocusedUIElementChanged causing VO re-anchor speech on top of the explicit word announcement - Removing FocusedUIElementChanged eliminates the duplicate speech - Add emacsInitiated parameter to postFocusedCursorNotification; omit AXTextSelectionGranularity for Emacs-initiated moves so VO does not auto-speak (only explicit announcements provide speech) - isWordMove now triggers on emacsInitiated flag (Emacs-initiated word moves always get explicit announcement)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
From 830710de8a7bac560d71ae802dcf7df60517c57b Mon Sep 17 00:00:00 2001
|
||||
From a6249e8ad3512f1e80e509c21111f6e3a241405f Mon Sep 17 00:00:00 2001
|
||||
From: Martin Sukany <martin@sukany.cz>
|
||||
Date: Sat, 28 Feb 2026 16:01:29 +0100
|
||||
Subject: [PATCH 8/8] ns: announce child frame completion candidates for
|
||||
@@ -20,11 +20,11 @@ element when a child frame completion closes.
|
||||
doc/emacs/macos.texi | 18 +-
|
||||
etc/NEWS | 18 +-
|
||||
src/nsterm.h | 21 ++
|
||||
src/nsterm.m | 496 +++++++++++++++++++++++++++++++++++++++----
|
||||
4 files changed, 501 insertions(+), 52 deletions(-)
|
||||
src/nsterm.m | 505 +++++++++++++++++++++++++++++++++++++++----
|
||||
4 files changed, 509 insertions(+), 53 deletions(-)
|
||||
|
||||
diff --git a/doc/emacs/macos.texi b/doc/emacs/macos.texi
|
||||
index 6514dfc..bcf74b3 100644
|
||||
index f3671354b5..03a657f970 100644
|
||||
--- a/doc/emacs/macos.texi
|
||||
+++ b/doc/emacs/macos.texi
|
||||
@@ -278,7 +278,6 @@ restart Emacs to access newly-available services.
|
||||
@@ -67,10 +67,10 @@ index 6514dfc..bcf74b3 100644
|
||||
correctly: character navigation announces the character at the cursor
|
||||
position, not the character before it.
|
||||
diff --git a/etc/NEWS b/etc/NEWS
|
||||
index 2b1f9e6..5766428 100644
|
||||
index 7f917f93b2..d7631fa6c7 100644
|
||||
--- a/etc/NEWS
|
||||
+++ b/etc/NEWS
|
||||
@@ -4400,15 +4400,19 @@ allowing Emacs users access to speech recognition utilities.
|
||||
@@ -4385,16 +4385,20 @@ allowing Emacs users access to speech recognition utilities.
|
||||
Note: Accepting this permission allows the use of system APIs, which may
|
||||
send user data to Apple's speech recognition servers.
|
||||
|
||||
@@ -99,7 +99,7 @@ index 2b1f9e6..5766428 100644
|
||||
interface and eliminate the associated overhead.
|
||||
|
||||
diff --git a/src/nsterm.h b/src/nsterm.h
|
||||
index 21a93bc..d435e42 100644
|
||||
index 21a93bc799..d435e42998 100644
|
||||
--- a/src/nsterm.h
|
||||
+++ b/src/nsterm.h
|
||||
@@ -504,9 +504,21 @@ typedef struct ns_ax_visible_run
|
||||
@@ -148,7 +148,7 @@ index 21a93bc..d435e42 100644
|
||||
@end
|
||||
|
||||
diff --git a/src/nsterm.m b/src/nsterm.m
|
||||
index 8d44b5f..29b646d 100644
|
||||
index b3daec31f1..33b72a0825 100644
|
||||
--- a/src/nsterm.m
|
||||
+++ b/src/nsterm.m
|
||||
@@ -1126,24 +1126,19 @@ Uses CFAbsoluteTimeGetCurrent() (~5 ns, a VDSO read) for timing. */
|
||||
@@ -199,7 +199,7 @@ index 8d44b5f..29b646d 100644
|
||||
}
|
||||
|
||||
#endif /* MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 */
|
||||
@@ -7415,6 +7416,112 @@ visual line index for Zoom (skip whitespace-only lines
|
||||
@@ -7406,6 +7407,112 @@ visual line index for Zoom (skip whitespace-only lines
|
||||
|
||||
return nil;
|
||||
}
|
||||
@@ -312,7 +312,7 @@ index 8d44b5f..29b646d 100644
|
||||
/* Build accessibility text for window W, skipping invisible text.
|
||||
Populates *OUT_START with the buffer start charpos.
|
||||
Populates *OUT_RUNS with an array of visible runs and *OUT_NRUNS
|
||||
@@ -8046,6 +8153,7 @@ that remapped bindings (e.g., C-j -> next-line) are recognized.
|
||||
@@ -8033,6 +8140,7 @@ that remapped bindings (e.g., C-j -> next-line) are recognized.
|
||||
@implementation EmacsAccessibilityBuffer
|
||||
@synthesize cachedText;
|
||||
@synthesize cachedTextModiff;
|
||||
@@ -320,7 +320,7 @@ index 8d44b5f..29b646d 100644
|
||||
@synthesize cachedOverlayModiff;
|
||||
@synthesize cachedTextStart;
|
||||
@synthesize cachedModiff;
|
||||
@@ -8159,16 +8267,34 @@ - (void)ensureTextCache
|
||||
@@ -8146,16 +8254,34 @@ - (void)ensureTextCache
|
||||
if (!b)
|
||||
return;
|
||||
|
||||
@@ -363,7 +363,7 @@ index 8d44b5f..29b646d 100644
|
||||
&& cachedTextStart == BUF_BEGV (b)
|
||||
&& pt >= cachedTextStart
|
||||
&& (textLen == 0
|
||||
@@ -8184,7 +8310,8 @@ included in the cached AX text (it is handled separately via
|
||||
@@ -8171,7 +8297,8 @@ included in the cached AX text (it is handled separately via
|
||||
{
|
||||
[cachedText release];
|
||||
cachedText = [text retain];
|
||||
@@ -373,7 +373,7 @@ index 8d44b5f..29b646d 100644
|
||||
cachedTextStart = start;
|
||||
|
||||
if (visibleRuns)
|
||||
@@ -8597,6 +8724,11 @@ - (void)setAccessibilitySelectedTextRange:(NSRange)range
|
||||
@@ -8584,6 +8711,11 @@ - (void)setAccessibilitySelectedTextRange:(NSRange)range
|
||||
|
||||
[self ensureTextCache];
|
||||
|
||||
@@ -385,7 +385,7 @@ index 8d44b5f..29b646d 100644
|
||||
specpdl_ref count = SPECPDL_INDEX ();
|
||||
record_unwind_current_buffer ();
|
||||
/* Ensure block_input is always matched by unblock_input even if
|
||||
@@ -9064,11 +9196,13 @@ - (void)postFocusedCursorNotification:(ptrdiff_t)point
|
||||
@@ -9040,11 +9172,14 @@ - (void)postFocusedCursorNotification:(ptrdiff_t)point
|
||||
= @(ns_ax_text_state_change_selection_move);
|
||||
moveInfo[@"AXTextSelectionDirection"] = @(direction);
|
||||
moveInfo[@"AXTextChangeElement"] = self;
|
||||
@@ -394,17 +394,18 @@ index 8d44b5f..29b646d 100644
|
||||
- for block-cursor mode). Include it for word/line/
|
||||
- selection so VoiceOver reads the appropriate text. */
|
||||
- if (!isCharMove)
|
||||
+ /* Include granularity for sequential moves so VoiceOver reads the
|
||||
+ appropriate unit. Omit for character moves (announced explicitly
|
||||
+ below) and for discontiguous jumps (destination line announced
|
||||
+ explicitly; omitting granularity lets VoiceOver use its default
|
||||
+ behaviour and re-anchor its browse cursor). */
|
||||
+ if (!isCharMove
|
||||
+ /* Include granularity for sequential VO-initiated moves so VoiceOver
|
||||
+ reads the appropriate unit. Omit for character moves (announced
|
||||
+ explicitly below), discontiguous jumps (destination line announced
|
||||
+ explicitly), and Emacs-initiated moves (announced explicitly;
|
||||
+ including granularity would cause VoiceOver to auto-speak,
|
||||
+ doubling the explicit announcement). */
|
||||
+ if (!isCharMove && !emacsInitiated
|
||||
+ && direction != ns_ax_text_selection_direction_discontiguous)
|
||||
moveInfo[@"AXTextSelectionGranularity"] = @(granularity);
|
||||
|
||||
ns_ax_post_notification_with_info (
|
||||
@@ -9111,12 +9245,17 @@ derive its own speech (it would read the wrong character
|
||||
@@ -9145,12 +9280,17 @@ user expectation ("w" jumps to next word and reads it). */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,7 +428,7 @@ index 8d44b5f..29b646d 100644
|
||||
if (cachedText
|
||||
&& granularity == ns_ax_text_selection_granularity_line)
|
||||
{
|
||||
@@ -9179,7 +9318,14 @@ - (void)postCompletionAnnouncementForBuffer:(struct buffer *)b
|
||||
@@ -9213,7 +9353,14 @@ - (void)postCompletionAnnouncementForBuffer:(struct buffer *)b
|
||||
ptrdiff_t currentOverlayStart = 0;
|
||||
ptrdiff_t currentOverlayEnd = 0;
|
||||
|
||||
@@ -442,7 +443,7 @@ index 8d44b5f..29b646d 100644
|
||||
record_unwind_current_buffer ();
|
||||
if (b != current_buffer)
|
||||
set_buffer_internal_1 (b);
|
||||
@@ -9356,12 +9502,29 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f
|
||||
@@ -9389,12 +9536,29 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f
|
||||
if (!b)
|
||||
return;
|
||||
|
||||
@@ -472,7 +473,7 @@ index 8d44b5f..29b646d 100644
|
||||
if (modiff != self.cachedModiff)
|
||||
{
|
||||
self.cachedModiff = modiff;
|
||||
@@ -9375,6 +9538,7 @@ Text property changes (e.g. face updates from
|
||||
@@ -9408,6 +9572,7 @@ Text property changes (e.g. face updates from
|
||||
{
|
||||
self.cachedCharsModiff = chars_modiff;
|
||||
[self postTextChangedNotification:point];
|
||||
@@ -480,7 +481,7 @@ index 8d44b5f..29b646d 100644
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9397,8 +9561,15 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
|
||||
@@ -9430,8 +9595,15 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
|
||||
displayed in the minibuffer. In normal editing buffers,
|
||||
font-lock and other modes change BUF_OVERLAY_MODIFF on
|
||||
every redisplay, triggering O(overlays) work per keystroke.
|
||||
@@ -498,7 +499,7 @@ index 8d44b5f..29b646d 100644
|
||||
goto skip_overlay_scan;
|
||||
|
||||
int selected_line = -1;
|
||||
@@ -9444,7 +9615,18 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
|
||||
@@ -9477,7 +9649,18 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
|
||||
self.cachedPoint = point;
|
||||
self.cachedMarkActive = markActive;
|
||||
|
||||
@@ -518,7 +519,7 @@ index 8d44b5f..29b646d 100644
|
||||
NSInteger direction = ns_ax_text_selection_direction_discontiguous;
|
||||
if (point > oldPoint)
|
||||
direction = ns_ax_text_selection_direction_next;
|
||||
@@ -9492,6 +9674,36 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
|
||||
@@ -9525,6 +9708,30 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
|
||||
granularity = ns_ax_text_selection_granularity_line;
|
||||
}
|
||||
|
||||
@@ -532,30 +533,33 @@ index 8d44b5f..29b646d 100644
|
||||
+ if (!isCtrlNP && granularity == ns_ax_text_selection_granularity_line)
|
||||
+ direction = ns_ax_text_selection_direction_discontiguous;
|
||||
+
|
||||
+ /* If Emacs moved the cursor (not VoiceOver), force discontiguous
|
||||
+ so VoiceOver re-anchors its browse cursor to the current
|
||||
+ accessibilitySelectedTextRange. This covers all Emacs-initiated
|
||||
+ moves: editing commands, ELisp, isearch, etc.
|
||||
+ /* If Emacs moved the cursor (not VoiceOver) and the move
|
||||
+ crosses a line boundary or jumps to an unknown position, force
|
||||
+ discontiguous so VoiceOver re-anchors its browse cursor.
|
||||
+ For character and word moves within a line, keep the natural
|
||||
+ next/previous direction: SelectedTextChanged with direction=next
|
||||
+ advances VoiceOver's browse cursor sequentially, and the explicit
|
||||
+ announcement in postFocusedCursorNotification provides speech.
|
||||
+ Exception: C-n/C-p (isCtrlNP) already uses next/previous with
|
||||
+ line granularity; those are already sequential and VoiceOver
|
||||
+ handles them correctly. */
|
||||
+ if (emacsMovedCursor && !isCtrlNP)
|
||||
+ line granularity; VoiceOver handles them correctly. */
|
||||
+ if (emacsMovedCursor && !isCtrlNP
|
||||
+ && (granularity == ns_ax_text_selection_granularity_line
|
||||
+ || granularity == ns_ax_text_selection_granularity_unknown))
|
||||
+ direction = ns_ax_text_selection_direction_discontiguous;
|
||||
+
|
||||
+ /* Post FocusedUIElementChanged on the containing NSView whenever
|
||||
+ Emacs moves the cursor independently of VoiceOver. Posting on
|
||||
+ the view causes VoiceOver to re-query accessibilityFocusedUIElement
|
||||
+ and re-anchor its browse cursor at the new selection range, without
|
||||
+ re-announcing the element name. Required for all granularities. */
|
||||
+ if (emacsMovedCursor && [self isAccessibilityFocused])
|
||||
+ ns_ax_post_notification (
|
||||
+ self.emacsView,
|
||||
+ NSAccessibilityFocusedUIElementChangedNotification);
|
||||
+
|
||||
/* Post notifications for focused and non-focused elements. */
|
||||
if ([self isAccessibilityFocused])
|
||||
[self postFocusedCursorNotification:point
|
||||
@@ -9634,6 +9836,17 @@ - (NSRect)accessibilityFrame
|
||||
@@ -9532,7 +9739,7 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
|
||||
granularity:granularity
|
||||
markActive:markActive
|
||||
oldMarkActive:oldMarkActive
|
||||
- emacsInitiated:NO];
|
||||
+ emacsInitiated:emacsMovedCursor];
|
||||
|
||||
if (![self isAccessibilityFocused] && cachedText)
|
||||
[self postCompletionAnnouncementForBuffer:b point:point];
|
||||
@@ -9668,6 +9875,17 @@ - (NSRect)accessibilityFrame
|
||||
if (vis_start >= vis_end)
|
||||
return @[];
|
||||
|
||||
@@ -573,7 +577,7 @@ index 8d44b5f..29b646d 100644
|
||||
/* Symbols are interned once at startup via DEFSYM in syms_of_nsterm;
|
||||
reference them directly here (GC-safe, no repeated obarray lookup). */
|
||||
|
||||
@@ -9754,6 +9967,7 @@ than O(chars). Fall back to pos+1 as safety net. */
|
||||
@@ -9788,6 +10006,7 @@ than O(chars). Fall back to pos+1 as safety net. */
|
||||
pos = span_end;
|
||||
}
|
||||
|
||||
@@ -581,7 +585,7 @@ index 8d44b5f..29b646d 100644
|
||||
return [[spans copy] autorelease];
|
||||
}
|
||||
|
||||
@@ -9935,6 +10149,10 @@ - (void)dealloc
|
||||
@@ -9969,6 +10188,10 @@ - (void)dealloc
|
||||
#endif
|
||||
|
||||
[accessibilityElements release];
|
||||
@@ -592,7 +596,7 @@ index 8d44b5f..29b646d 100644
|
||||
[[self menu] release];
|
||||
[super dealloc];
|
||||
}
|
||||
@@ -11384,6 +11602,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f
|
||||
@@ -11418,6 +11641,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f
|
||||
|
||||
windowClosing = NO;
|
||||
processingCompose = NO;
|
||||
@@ -602,7 +606,7 @@ index 8d44b5f..29b646d 100644
|
||||
scrollbarsNeedingUpdate = 0;
|
||||
fs_state = FULLSCREEN_NONE;
|
||||
fs_before_fs = next_maximized = -1;
|
||||
@@ -12692,6 +12913,154 @@ - (id)accessibilityFocusedUIElement
|
||||
@@ -12726,6 +12952,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. */
|
||||
@@ -757,7 +761,7 @@ index 8d44b5f..29b646d 100644
|
||||
- (void)postAccessibilityUpdates
|
||||
{
|
||||
NSTRACE ("[EmacsView postAccessibilityUpdates]");
|
||||
@@ -12702,11 +13069,64 @@ - (void)postAccessibilityUpdates
|
||||
@@ -12736,11 +13110,64 @@ - (void)postAccessibilityUpdates
|
||||
|
||||
/* Re-entrance guard: VoiceOver callbacks during notification posting
|
||||
can trigger redisplay, which calls ns_update_end, which calls us
|
||||
|
||||
Reference in New Issue
Block a user