patches: fix double-speech and evil block cursor char reading

- SelectedTextChanged posted only for focused element: prevents completion
  buffer from triggering double-speech (old-candidate + new-candidate)
- AnnouncementRequested for char navigation restored (evil block cursor fix):
  posted AFTER SelectedTextChanged so VoiceOver cancels its own reading
  and uses our explicit char-at-point announcement
- Priority: Medium (was High)
This commit is contained in:
2026-02-27 12:16:32 +01:00
parent 5f98a78467
commit edad606809

View File

@@ -1,6 +1,6 @@
From 0c1c656992847c0d9da8351fb2268bd5171982f2 Mon Sep 17 00:00:00 2001 From b206fdbfb23830030e31a8075e296d3b8df0b0d4 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Fri, 27 Feb 2026 12:07:06 +0100 Date: Fri, 27 Feb 2026 12:16:23 +0100
Subject: [PATCH] ns: implement VoiceOver accessibility (AXBoundsForRange, line Subject: [PATCH] ns: implement VoiceOver accessibility (AXBoundsForRange, line
nav, completions, interactive spans) nav, completions, interactive spans)
@@ -14,15 +14,17 @@ Subject: [PATCH] ns: implement VoiceOver accessibility (AXBoundsForRange, line
- Line and character navigation announcements - Line and character navigation announcements
- Interactive span Tab navigation (buttons, links, completions) - Interactive span Tab navigation (buttons, links, completions)
- Completion announcement with 4-fallback chain - Completion announcement with 4-fallback chain
- Thread-safe: AX getters use cachedText/visibleRuns built on - Thread-safe: AX getters use cachedText/visibleRuns (main thread);
main thread; no Lisp calls from AX server thread no Lisp calls from AX server thread
- GC-safe: span-scanning symbols registered via DEFSYM - GC-safe: span-scanning symbols via DEFSYM in syms_of_nsterm
- No double-announcement: SelectedTextChanged for cursor moves, - SelectedTextChanged only for focused element (prevents double-speech
AnnouncementRequested (Medium priority) for line text only in completions buffer)
- Evil block-cursor: AnnouncementRequested(char AT point) posted
after SelectedTextChanged; VoiceOver cancels its own reading
--- ---
src/nsterm.h | 109 ++ src/nsterm.h | 109 ++
src/nsterm.m | 2682 +++++++++++++++++++++++++++++++++++++++++++++++--- src/nsterm.m | 2724 +++++++++++++++++++++++++++++++++++++++++++++++---
2 files changed, 2642 insertions(+), 149 deletions(-) 2 files changed, 2684 insertions(+), 149 deletions(-)
diff --git a/src/nsterm.h b/src/nsterm.h diff --git a/src/nsterm.h b/src/nsterm.h
index 7c1ee4c..6c95673 100644 index 7c1ee4c..6c95673 100644
@@ -159,7 +161,7 @@ index 7c1ee4c..6c95673 100644
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index 932d209..e2532af 100644 index 932d209..9455730 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -46,6 +46,7 @@ Updated by Christian Limpach (chris@nice.ch) @@ -46,6 +46,7 @@ Updated by Christian Limpach (chris@nice.ch)
@@ -220,7 +222,7 @@ index 932d209..e2532af 100644
ns_focus (f, NULL, 0); ns_focus (f, NULL, 0);
NSGraphicsContext *ctx = [NSGraphicsContext currentContext]; NSGraphicsContext *ctx = [NSGraphicsContext currentContext];
@@ -6849,219 +6886,2235 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg @@ -6849,219 +6886,2277 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg
/* ========================================================================== /* ==========================================================================
@@ -1979,6 +1981,14 @@ index 932d209..e2532af 100644
+ granularity = ns_ax_text_selection_granularity_line; + granularity = ns_ax_text_selection_granularity_line;
+ } + }
+ +
+ /* Post SelectedTextChanged only for the FOCUSED element.
+ For non-focused buffers (e.g. *Completions* while minibuffer has
+ focus), SelectedTextChanged causes VoiceOver to read the old
+ selection text, followed by our AnnouncementRequested reading the
+ new candidate — resulting in double speech. Non-focused buffers
+ use only AnnouncementRequested (see below). */
+ if ([self isAccessibilityFocused])
+ {
+ NSDictionary *moveInfo = @{ + NSDictionary *moveInfo = @{
+ @"AXTextStateChangeType": @(ns_ax_text_state_change_selection_move), + @"AXTextStateChangeType": @(ns_ax_text_state_change_selection_move),
+ @"AXTextSelectionDirection": @(direction), + @"AXTextSelectionDirection": @(direction),
@@ -1990,11 +2000,45 @@ index 932d209..e2532af 100644
+ NSAccessibilitySelectedTextChangedNotification, + NSAccessibilitySelectedTextChangedNotification,
+ moveInfo); + moveInfo);
+ +
+ /* Character navigation: NSAccessibilitySelectedTextChangedNotification + /* Character navigation: announce the character AT point.
+ (posted above) is sufficient — VoiceOver reads the char at the new + VoiceOver's default for SelectedTextChanged reads the character
+ cursor position from accessibilityStringForRange. An additional + BEFORE the new cursor position — correct for insert-mode but
+ AnnouncementRequested would cause double-speech. The SelectedTextChanged + wrong for evil block-cursor which sits ON the character at point.
+ path also correctly signals braille displays. */ + Post AnnouncementRequested AFTER SelectedTextChanged: VoiceOver
+ cancels its SelectedTextChanged speech in favour of the explicit
+ announcement when both arrive in the same runloop cycle. */
+ if (cachedText
+ && granularity == ns_ax_text_selection_granularity_character
+ && (direction == ns_ax_text_selection_direction_next
+ || direction == ns_ax_text_selection_direction_previous))
+ {
+ NSUInteger point_idx = [self accessibilityIndexForCharpos:point];
+ NSUInteger tlen = [cachedText length];
+ if (point_idx < tlen)
+ {
+ NSRange charRange = [cachedText
+ rangeOfComposedCharacterSequenceAtIndex: point_idx];
+ if (charRange.location != NSNotFound && charRange.length > 0
+ && NSMaxRange (charRange) <= tlen)
+ {
+ NSString *ch = [cachedText substringWithRange: charRange];
+ /* Skip bare newlines — VoiceOver says "new line". */
+ if (![ch isEqualToString: @"\n"])
+ {
+ NSDictionary *annInfo = @{
+ NSAccessibilityAnnouncementKey: ch,
+ NSAccessibilityPriorityKey:
+ @(NSAccessibilityPriorityMedium)
+ };
+ NSAccessibilityPostNotificationWithUserInfo (
+ NSApp,
+ NSAccessibilityAnnouncementRequestedNotification,
+ annInfo);
+ }
+ }
+ }
+ }
+ }
+ +
+ /* Emit an explicit announcement whenever point lands on a new line. + /* Emit an explicit announcement whenever point lands on a new line.
+ Triggering on granularity=line covers ALL line-motion commands + Triggering on granularity=line covers ALL line-motion commands
@@ -2605,7 +2649,7 @@ index 932d209..e2532af 100644
unsigned fnKeysym = 0; unsigned fnKeysym = 0;
static NSMutableArray *nsEvArray; static NSMutableArray *nsEvArray;
unsigned int flags = [theEvent modifierFlags]; unsigned int flags = [theEvent modifierFlags];
@@ -8237,6 +10290,28 @@ - (void)windowDidBecomeKey /* for direct calls */ @@ -8237,6 +10332,28 @@ - (void)windowDidBecomeKey /* for direct calls */
XSETFRAME (event.frame_or_window, emacsframe); XSETFRAME (event.frame_or_window, emacsframe);
kbd_buffer_store_event (&event); kbd_buffer_store_event (&event);
ns_send_appdefined (-1); // Kick main loop ns_send_appdefined (-1); // Kick main loop
@@ -2634,7 +2678,7 @@ index 932d209..e2532af 100644
} }
@@ -9474,6 +11549,307 @@ - (int) fullscreenState @@ -9474,6 +11591,307 @@ - (int) fullscreenState
return fs_state; return fs_state;
} }
@@ -2942,7 +2986,7 @@ index 932d209..e2532af 100644
@end /* EmacsView */ @end /* EmacsView */
@@ -11303,6 +13679,14 @@ Convert an X font name (XLFD) to an NS font name. @@ -11303,6 +13721,14 @@ Convert an X font name (XLFD) to an NS font name.
DEFSYM (Qns_drag_operation_generic, "ns-drag-operation-generic"); DEFSYM (Qns_drag_operation_generic, "ns-drag-operation-generic");
DEFSYM (Qns_handle_drag_motion, "ns-handle-drag-motion"); DEFSYM (Qns_handle_drag_motion, "ns-handle-drag-motion");