patches: fix VO browse cursor sync — Emacs→VoiceOver direction

This commit is contained in:
2026-03-01 21:18:34 +01:00
parent 57086b88ef
commit 93ea536942

View File

@@ -1,4 +1,4 @@
From 10d8d56ed0364c9bc387600431a66dabe37b3e2a Mon Sep 17 00:00:00 2001 From 830710de8a7bac560d71ae802dcf7df60517c57b Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 16:01:29 +0100 Date: Sat, 28 Feb 2026 16:01:29 +0100
Subject: [PATCH 8/8] ns: announce child frame completion candidates for Subject: [PATCH 8/8] ns: announce child frame completion candidates for
@@ -19,9 +19,9 @@ element when a child frame completion closes.
--- ---
doc/emacs/macos.texi | 18 +- doc/emacs/macos.texi | 18 +-
etc/NEWS | 18 +- etc/NEWS | 18 +-
src/nsterm.h | 13 ++ src/nsterm.h | 21 ++
src/nsterm.m | 468 +++++++++++++++++++++++++++++++++++++++---- src/nsterm.m | 496 +++++++++++++++++++++++++++++++++++++++----
4 files changed, 466 insertions(+), 51 deletions(-) 4 files changed, 501 insertions(+), 52 deletions(-)
diff --git a/doc/emacs/macos.texi b/doc/emacs/macos.texi diff --git a/doc/emacs/macos.texi b/doc/emacs/macos.texi
index 6514dfc..bcf74b3 100644 index 6514dfc..bcf74b3 100644
@@ -99,10 +99,21 @@ index 2b1f9e6..5766428 100644
interface and eliminate the associated overhead. interface and eliminate the associated overhead.
diff --git a/src/nsterm.h b/src/nsterm.h diff --git a/src/nsterm.h b/src/nsterm.h
index 21a93bc..8f2143b 100644 index 21a93bc..d435e42 100644
--- a/src/nsterm.h --- a/src/nsterm.h
+++ b/src/nsterm.h +++ b/src/nsterm.h
@@ -507,6 +507,10 @@ typedef struct ns_ax_visible_run @@ -504,9 +504,21 @@ typedef struct ns_ax_visible_run
NSUInteger lineCount; /* Entries in lineStartOffsets. */
NSMutableArray *cachedInteractiveSpans;
BOOL interactiveSpansDirty;
+ /* Set to YES in setAccessibilitySelectedTextRange: (VoiceOver moved
+ the cursor); reset to NO in postAccessibilityNotificationsForFrame:.
+ When YES, cursor notifications use sequential direction so VoiceOver
+ continues smooth line/character navigation without re-anchoring.
+ When NO, Emacs moved the cursor independently; use discontiguous
+ direction so VoiceOver re-anchors its browse cursor to the new
+ accessibilitySelectedTextRange. */
+ BOOL voiceoverSetPoint;
} }
@property (nonatomic, retain) NSString *cachedText; @property (nonatomic, retain) NSString *cachedText;
@property (nonatomic, assign) ptrdiff_t cachedTextModiff; @property (nonatomic, assign) ptrdiff_t cachedTextModiff;
@@ -113,7 +124,7 @@ index 21a93bc..8f2143b 100644
@property (nonatomic, assign) ptrdiff_t cachedOverlayModiff; @property (nonatomic, assign) ptrdiff_t cachedOverlayModiff;
@property (nonatomic, assign) ptrdiff_t cachedTextStart; @property (nonatomic, assign) ptrdiff_t cachedTextStart;
@property (nonatomic, assign) ptrdiff_t cachedModiff; @property (nonatomic, assign) ptrdiff_t cachedModiff;
@@ -596,6 +600,14 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType) @@ -596,6 +608,14 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType)
Lisp_Object lastRootWindow; Lisp_Object lastRootWindow;
BOOL accessibilityTreeValid; BOOL accessibilityTreeValid;
BOOL accessibilityUpdating; BOOL accessibilityUpdating;
@@ -128,7 +139,7 @@ index 21a93bc..8f2143b 100644
#endif #endif
BOOL font_panel_active; BOOL font_panel_active;
NSFont *font_panel_result; NSFont *font_panel_result;
@@ -665,6 +677,7 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType) @@ -665,6 +685,7 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType)
- (void)rebuildAccessibilityTree; - (void)rebuildAccessibilityTree;
- (void)invalidateAccessibilityTree; - (void)invalidateAccessibilityTree;
- (void)postAccessibilityUpdates; - (void)postAccessibilityUpdates;
@@ -137,7 +148,7 @@ index 21a93bc..8f2143b 100644
@end @end
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index 8d44b5f..34d7ee2 100644 index 8d44b5f..29b646d 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -1126,24 +1126,19 @@ Uses CFAbsoluteTimeGetCurrent() (~5 ns, a VDSO read) for timing. */ @@ -1126,24 +1126,19 @@ Uses CFAbsoluteTimeGetCurrent() (~5 ns, a VDSO read) for timing. */
@@ -362,7 +373,19 @@ index 8d44b5f..34d7ee2 100644
cachedTextStart = start; cachedTextStart = start;
if (visibleRuns) if (visibleRuns)
@@ -9060,11 +9187,13 @@ - (void)postFocusedCursorNotification:(ptrdiff_t)point @@ -8597,6 +8724,11 @@ - (void)setAccessibilitySelectedTextRange:(NSRange)range
[self ensureTextCache];
+ /* Record that VoiceOver (not Emacs) is moving the cursor so that the
+ subsequent postAccessibilityNotificationsForFrame: call can use the
+ correct sequential direction rather than forcing a re-anchor. */
+ voiceoverSetPoint = YES;
+
specpdl_ref count = SPECPDL_INDEX ();
record_unwind_current_buffer ();
/* Ensure block_input is always matched by unblock_input even if
@@ -9060,11 +9192,13 @@ - (void)postFocusedCursorNotification:(ptrdiff_t)point
= @(ns_ax_text_state_change_selection_move); = @(ns_ax_text_state_change_selection_move);
moveInfo[@"AXTextSelectionDirection"] = @(direction); moveInfo[@"AXTextSelectionDirection"] = @(direction);
moveInfo[@"AXTextChangeElement"] = self; moveInfo[@"AXTextChangeElement"] = self;
@@ -381,7 +404,7 @@ index 8d44b5f..34d7ee2 100644
moveInfo[@"AXTextSelectionGranularity"] = @(granularity); moveInfo[@"AXTextSelectionGranularity"] = @(granularity);
ns_ax_post_notification_with_info ( ns_ax_post_notification_with_info (
@@ -9107,12 +9236,17 @@ derive its own speech (it would read the wrong character @@ -9107,12 +9241,17 @@ derive its own speech (it would read the wrong character
} }
} }
@@ -404,7 +427,7 @@ index 8d44b5f..34d7ee2 100644
if (cachedText if (cachedText
&& granularity == ns_ax_text_selection_granularity_line) && granularity == ns_ax_text_selection_granularity_line)
{ {
@@ -9175,7 +9309,14 @@ - (void)postCompletionAnnouncementForBuffer:(struct buffer *)b @@ -9175,7 +9314,14 @@ - (void)postCompletionAnnouncementForBuffer:(struct buffer *)b
ptrdiff_t currentOverlayStart = 0; ptrdiff_t currentOverlayStart = 0;
ptrdiff_t currentOverlayEnd = 0; ptrdiff_t currentOverlayEnd = 0;
@@ -419,7 +442,7 @@ index 8d44b5f..34d7ee2 100644
record_unwind_current_buffer (); record_unwind_current_buffer ();
if (b != current_buffer) if (b != current_buffer)
set_buffer_internal_1 (b); set_buffer_internal_1 (b);
@@ -9352,12 +9493,29 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f @@ -9352,12 +9498,29 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f
if (!b) if (!b)
return; return;
@@ -449,7 +472,7 @@ index 8d44b5f..34d7ee2 100644
if (modiff != self.cachedModiff) if (modiff != self.cachedModiff)
{ {
self.cachedModiff = modiff; self.cachedModiff = modiff;
@@ -9371,6 +9529,7 @@ Text property changes (e.g. face updates from @@ -9371,6 +9534,7 @@ Text property changes (e.g. face updates from
{ {
self.cachedCharsModiff = chars_modiff; self.cachedCharsModiff = chars_modiff;
[self postTextChangedNotification:point]; [self postTextChangedNotification:point];
@@ -457,7 +480,7 @@ index 8d44b5f..34d7ee2 100644
} }
} }
@@ -9393,8 +9552,15 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property @@ -9393,8 +9557,15 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
displayed in the minibuffer. In normal editing buffers, displayed in the minibuffer. In normal editing buffers,
font-lock and other modes change BUF_OVERLAY_MODIFF on font-lock and other modes change BUF_OVERLAY_MODIFF on
every redisplay, triggering O(overlays) work per keystroke. every redisplay, triggering O(overlays) work per keystroke.
@@ -475,7 +498,27 @@ index 8d44b5f..34d7ee2 100644
goto skip_overlay_scan; goto skip_overlay_scan;
int selected_line = -1; int selected_line = -1;
@@ -9488,6 +9654,16 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property @@ -9440,7 +9611,18 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
self.cachedPoint = point;
self.cachedMarkActive = markActive;
- /* Compute direction. */
+ /* Compute direction.
+ When VoiceOver moved the cursor via setAccessibilitySelectedTextRange:
+ (voiceoverSetPoint == YES), use sequential next/previous so VoiceOver
+ continues smooth navigation from its current position.
+ When Emacs moved the cursor independently (voiceoverSetPoint == NO),
+ force discontiguous direction so VoiceOver re-anchors its browse
+ cursor to accessibilitySelectedTextRange; without this, VoiceOver's
+ internal browse position diverges from the Emacs insertion point and
+ subsequent VO+arrow navigation starts from the wrong location. */
+ BOOL emacsMovedCursor = !voiceoverSetPoint;
+ voiceoverSetPoint = NO; /* Consume the flag. */
+
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
granularity = ns_ax_text_selection_granularity_line; granularity = ns_ax_text_selection_granularity_line;
} }
@@ -488,11 +531,21 @@ index 8d44b5f..34d7ee2 100644
+ from its previous internal position. */ + from its previous internal position. */
+ if (!isCtrlNP && granularity == ns_ax_text_selection_granularity_line) + if (!isCtrlNP && granularity == ns_ax_text_selection_granularity_line)
+ direction = ns_ax_text_selection_direction_discontiguous; + 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.
+ 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)
+ direction = ns_ax_text_selection_direction_discontiguous;
+ +
/* Post notifications for focused and non-focused elements. */ /* Post notifications for focused and non-focused elements. */
if ([self isAccessibilityFocused]) if ([self isAccessibilityFocused])
[self postFocusedCursorNotification:point [self postFocusedCursorNotification:point
@@ -9630,6 +9806,17 @@ - (NSRect)accessibilityFrame @@ -9630,6 +9832,17 @@ - (NSRect)accessibilityFrame
if (vis_start >= vis_end) if (vis_start >= vis_end)
return @[]; return @[];
@@ -510,7 +563,7 @@ index 8d44b5f..34d7ee2 100644
/* Symbols are interned once at startup via DEFSYM in syms_of_nsterm; /* Symbols are interned once at startup via DEFSYM in syms_of_nsterm;
reference them directly here (GC-safe, no repeated obarray lookup). */ reference them directly here (GC-safe, no repeated obarray lookup). */
@@ -9750,6 +9937,7 @@ than O(chars). Fall back to pos+1 as safety net. */ @@ -9750,6 +9963,7 @@ than O(chars). Fall back to pos+1 as safety net. */
pos = span_end; pos = span_end;
} }
@@ -518,7 +571,7 @@ index 8d44b5f..34d7ee2 100644
return [[spans copy] autorelease]; return [[spans copy] autorelease];
} }
@@ -9931,6 +10119,10 @@ - (void)dealloc @@ -9931,6 +10145,10 @@ - (void)dealloc
#endif #endif
[accessibilityElements release]; [accessibilityElements release];
@@ -529,7 +582,7 @@ index 8d44b5f..34d7ee2 100644
[[self menu] release]; [[self menu] release];
[super dealloc]; [super dealloc];
} }
@@ -11380,6 +11572,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f @@ -11380,6 +11598,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f
windowClosing = NO; windowClosing = NO;
processingCompose = NO; processingCompose = NO;
@@ -539,7 +592,7 @@ index 8d44b5f..34d7ee2 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 +12883,152 @@ - (id)accessibilityFocusedUIElement @@ -12688,6 +12909,152 @@ - (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. */
@@ -692,7 +745,7 @@ index 8d44b5f..34d7ee2 100644
- (void)postAccessibilityUpdates - (void)postAccessibilityUpdates
{ {
NSTRACE ("[EmacsView postAccessibilityUpdates]"); NSTRACE ("[EmacsView postAccessibilityUpdates]");
@@ -12698,11 +13039,64 @@ - (void)postAccessibilityUpdates @@ -12698,11 +13065,64 @@ - (void)postAccessibilityUpdates
/* Re-entrance guard: VoiceOver callbacks during notification posting /* Re-entrance guard: VoiceOver callbacks during notification posting
can trigger redisplay, which calls ns_update_end, which calls us can trigger redisplay, which calls ns_update_end, which calls us