patches: fix VoiceOver rotor cursor sync + echo area announcements

This commit is contained in:
2026-03-01 14:41:40 +01:00
parent bcb25088dd
commit 3502cfaf25

View File

@@ -1,4 +1,4 @@
From d3955e2fa0cd7e39d5100edf5818608b3be53f20 Mon Sep 17 00:00:00 2001 From eeb92b97dfd0ad3736f023703227ea2c78758530 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
@@ -20,8 +20,8 @@ element when a child frame completion closes.
doc/emacs/macos.texi | 6 - doc/emacs/macos.texi | 6 -
etc/NEWS | 4 +- etc/NEWS | 4 +-
src/nsterm.h | 9 ++ src/nsterm.h | 9 ++
src/nsterm.m | 329 +++++++++++++++++++++++++++++++++++++++++-- src/nsterm.m | 337 +++++++++++++++++++++++++++++++++++++++++--
4 files changed, 330 insertions(+), 18 deletions(-) 4 files changed, 333 insertions(+), 23 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..f47929e 100644 index 6514dfc..f47929e 100644
@@ -97,7 +97,7 @@ index 21a93bc..bbce9fe 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..7b254ca 100644 index 8d44b5f..251a03e 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -7415,6 +7415,112 @@ visual line index for Zoom (skip whitespace-only lines @@ -7415,6 +7415,112 @@ visual line index for Zoom (skip whitespace-only lines
@@ -274,28 +274,35 @@ index 8d44b5f..7b254ca 100644
cachedTextStart = start; cachedTextStart = start;
if (visibleRuns) if (visibleRuns)
@@ -9072,6 +9198,20 @@ derive its own speech (it would read the wrong character @@ -9060,11 +9186,14 @@ - (void)postFocusedCursorNotification:(ptrdiff_t)point
= @(ns_ax_text_state_change_selection_move);
moveInfo[@"AXTextSelectionDirection"] = @(direction);
moveInfo[@"AXTextChangeElement"] = self;
- /* Omit granularity for character moves so VoiceOver does not
- derive its own speech (it would read the wrong character
- for evil 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 of text. Omit for character moves (we
+ announce the character explicitly below) and for discontiguous
+ jumps (the destination line is announced explicitly; omitting
+ granularity lets VoiceOver use default behaviour and re-anchor
+ its browse cursor to accessibilitySelectedTextRange). */
+ if (!isCharMove
+ && direction != ns_ax_text_selection_direction_discontiguous)
moveInfo[@"AXTextSelectionGranularity"] = @(granularity);
ns_ax_post_notification_with_info (
@@ -9072,6 +9201,7 @@ derive its own speech (it would read the wrong character
NSAccessibilitySelectedTextChangedNotification, NSAccessibilitySelectedTextChangedNotification,
moveInfo); moveInfo);
+ /* For large programmatic jumps (not C-n/C-p arrow-key line moves),
+ also post LayoutChanged so VoiceOver synchronises its rotor browse
+ cursor with the new accessibilitySelectedTextRange.
+ SelectedTextChanged alone moves VoiceOver's reading cursor but
+ does not update the rotor browse cursor for multi-line jumps
+ (e.g. org ]] / [[ heading navigation, imenu, xref). */
+ if (granularity != ns_ax_text_selection_granularity_character
+ && direction != ns_ax_text_selection_direction_discontiguous
+ && !isCtrlNP)
+ NSAccessibilityPostNotificationWithUserInfo (
+ self,
+ NSAccessibilityLayoutChangedNotification,
+ @{ NSAccessibilityUIElementsKey: @[self] });
+ +
/* For character moves: explicit announcement of char AT point. /* For character moves: explicit announcement of char AT point.
This is the ONLY speech source for character navigation. This is the ONLY speech source for character navigation.
Correct for evil block-cursor (cursor ON the character) Correct for evil block-cursor (cursor ON the character)
@@ -9175,6 +9315,7 @@ - (void)postCompletionAnnouncementForBuffer:(struct buffer *)b @@ -9175,6 +9305,7 @@ - (void)postCompletionAnnouncementForBuffer:(struct buffer *)b
ptrdiff_t currentOverlayStart = 0; ptrdiff_t currentOverlayStart = 0;
ptrdiff_t currentOverlayEnd = 0; ptrdiff_t currentOverlayEnd = 0;
@@ -303,38 +310,38 @@ index 8d44b5f..7b254ca 100644
specpdl_ref count2 = SPECPDL_INDEX (); specpdl_ref count2 = SPECPDL_INDEX ();
record_unwind_current_buffer (); record_unwind_current_buffer ();
if (b != current_buffer) if (b != current_buffer)
@@ -9352,6 +9493,45 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f @@ -9352,6 +9483,44 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f
if (!b) if (!b)
return; return;
+ /* --- Echo area announcements --- + /* --- Echo area announcements ---
+ When the minibuffer is not active for user input (minibuf_level == 0) + When the minibuffer is not active for user input (minibuf_level == 0)
+ and its text changes, post an AX announcement so VoiceOver reads the + and its character content changes, announce the new text to VoiceOver.
+ new message. This covers error messages, warnings, and informational + This surfaces error messages, informational echoes, and git/process
+ echoes. Skipped while minibuf_level > 0 (user is typing a command) + status updates. We skip the rest of the notification cycle (cursor
+ to avoid interrupting prompt reading or completion. */ + tracking etc.) because an inactive minibuffer has no meaningful cursor.
+ When minibuf_level > 0 the user is composing a command; fall through
+ to normal processing so prompt and completion announcements work. */
+ if (MINI_WINDOW_P (w) && minibuf_level == 0) + if (MINI_WINDOW_P (w) && minibuf_level == 0)
+ { + {
+ ptrdiff_t echo_chars_modiff = BUF_CHARS_MODIFF (b); + ptrdiff_t echo_chars = BUF_CHARS_MODIFF (b);
+ if (echo_chars_modiff != self.cachedCharsModiff) + if (echo_chars != self.cachedCharsModiff
+ && BUF_ZV (b) > BUF_BEGV (b))
+ { + {
+ self.cachedCharsModiff = echo_chars_modiff; + self.cachedCharsModiff = echo_chars;
+ ptrdiff_t dummy_start = 0; + struct buffer *prev = current_buffer;
+ NSUInteger nruns = 0; + set_buffer_internal (b);
+ ns_ax_visible_run *runs = NULL; + Lisp_Object ls = Fbuffer_string ();
+ NSString *msg = ns_ax_buffer_text (w, &dummy_start, + set_buffer_internal (prev);
+ &runs, &nruns); + NSString *raw = [NSString stringWithUTF8String: SSDATA (ls)];
+ if (runs) xfree (runs); + NSString *msg = [raw stringByTrimmingCharactersInSet:
+ if (msg)
+ {
+ NSString *trimmed = [msg stringByTrimmingCharactersInSet:
+ [NSCharacterSet whitespaceAndNewlineCharacterSet]]; + [NSCharacterSet whitespaceAndNewlineCharacterSet]];
+ if ([trimmed length] > 0) + if ([msg length] > 0)
+ { + {
+ NSDictionary *info = @{ + NSDictionary *info = @{
+ NSAccessibilityAnnouncementKey: trimmed, + NSAccessibilityAnnouncementKey: msg,
+ NSAccessibilityPriorityKey: + NSAccessibilityPriorityKey:
+ @(NSAccessibilityPriorityMedium) + @(NSAccessibilityPriorityHigh)
+ }; + };
+ ns_ax_post_notification_with_info ( + ns_ax_post_notification_with_info (
+ NSApp, + NSApp,
@@ -342,14 +349,29 @@ index 8d44b5f..7b254ca 100644
+ info); + info);
+ } + }
+ } + }
+ } + return;
+ return; /* no cursor tracking for inactive minibuffer */
+ } + }
+ +
ptrdiff_t modiff = BUF_MODIFF (b); ptrdiff_t modiff = BUF_MODIFF (b);
ptrdiff_t point = BUF_PT (b); ptrdiff_t point = BUF_PT (b);
BOOL markActive = !NILP (BVAR (b, mark_active)); BOOL markActive = !NILP (BVAR (b, mark_active));
@@ -9931,6 +10111,10 @@ - (void)dealloc @@ -9488,6 +9657,15 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
granularity = ns_ax_text_selection_granularity_line;
}
+ /* Non-sequential jumps that cross a line boundary (e.g. ]], [[,
+ M-<, xref, imenu) are discontiguous: the cursor moved to an
+ arbitrary location, not one sequential step. Reporting direction
+ discontiguous causes VoiceOver to re-anchor its rotor browse
+ cursor to the new accessibilitySelectedTextRange instead of
+ advancing linearly from its previous position. */
+ if (!isCtrlNP && granularity == ns_ax_text_selection_granularity_line)
+ direction = ns_ax_text_selection_direction_discontiguous;
+
/* Post notifications for focused and non-focused elements. */
if ([self isAccessibilityFocused])
[self postFocusedCursorNotification:point
@@ -9931,6 +10109,10 @@ - (void)dealloc
#endif #endif
[accessibilityElements release]; [accessibilityElements release];
@@ -360,7 +382,7 @@ index 8d44b5f..7b254ca 100644
[[self menu] release]; [[self menu] release];
[super dealloc]; [super dealloc];
} }
@@ -11380,6 +11564,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f @@ -11380,6 +11562,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f
windowClosing = NO; windowClosing = NO;
processingCompose = NO; processingCompose = NO;
@@ -370,7 +392,7 @@ index 8d44b5f..7b254ca 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 +12875,80 @@ - (id)accessibilityFocusedUIElement @@ -12688,6 +12873,80 @@ - (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. */
@@ -451,7 +473,7 @@ index 8d44b5f..7b254ca 100644
- (void)postAccessibilityUpdates - (void)postAccessibilityUpdates
{ {
NSTRACE ("[EmacsView postAccessibilityUpdates]"); NSTRACE ("[EmacsView postAccessibilityUpdates]");
@@ -12698,11 +12959,59 @@ - (void)postAccessibilityUpdates @@ -12698,11 +12957,59 @@ - (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