diff --git a/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch b/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch index 4537e14..5a6efc9 100644 --- a/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch +++ b/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch @@ -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 -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 nav, completions, interactive spans) @@ -14,15 +14,17 @@ Subject: [PATCH] ns: implement VoiceOver accessibility (AXBoundsForRange, line - Line and character navigation announcements - Interactive span Tab navigation (buttons, links, completions) - Completion announcement with 4-fallback chain - - Thread-safe: AX getters use cachedText/visibleRuns built on - main thread; no Lisp calls from AX server thread - - GC-safe: span-scanning symbols registered via DEFSYM - - No double-announcement: SelectedTextChanged for cursor moves, - AnnouncementRequested (Medium priority) for line text only + - Thread-safe: AX getters use cachedText/visibleRuns (main thread); + no Lisp calls from AX server thread + - GC-safe: span-scanning symbols via DEFSYM in syms_of_nsterm + - SelectedTextChanged only for focused element (prevents double-speech + in completions buffer) + - Evil block-cursor: AnnouncementRequested(char AT point) posted + after SelectedTextChanged; VoiceOver cancels its own reading --- src/nsterm.h | 109 ++ - src/nsterm.m | 2682 +++++++++++++++++++++++++++++++++++++++++++++++--- - 2 files changed, 2642 insertions(+), 149 deletions(-) + src/nsterm.m | 2724 +++++++++++++++++++++++++++++++++++++++++++++++--- + 2 files changed, 2684 insertions(+), 149 deletions(-) diff --git a/src/nsterm.h b/src/nsterm.h index 7c1ee4c..6c95673 100644 @@ -159,7 +161,7 @@ index 7c1ee4c..6c95673 100644 diff --git a/src/nsterm.m b/src/nsterm.m -index 932d209..e2532af 100644 +index 932d209..9455730 100644 --- a/src/nsterm.m +++ b/src/nsterm.m @@ -46,6 +46,7 @@ Updated by Christian Limpach (chris@nice.ch) @@ -220,7 +222,7 @@ index 932d209..e2532af 100644 ns_focus (f, NULL, 0); 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,22 +1981,64 @@ index 932d209..e2532af 100644 + granularity = ns_ax_text_selection_granularity_line; + } + -+ NSDictionary *moveInfo = @{ -+ @"AXTextStateChangeType": @(ns_ax_text_state_change_selection_move), -+ @"AXTextSelectionDirection": @(direction), -+ @"AXTextSelectionGranularity": @(granularity), -+ @"AXTextChangeElement": self -+ }; -+ NSAccessibilityPostNotificationWithUserInfo ( -+ self, -+ NSAccessibilitySelectedTextChangedNotification, -+ moveInfo); ++ /* 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 = @{ ++ @"AXTextStateChangeType": @(ns_ax_text_state_change_selection_move), ++ @"AXTextSelectionDirection": @(direction), ++ @"AXTextSelectionGranularity": @(granularity), ++ @"AXTextChangeElement": self ++ }; ++ NSAccessibilityPostNotificationWithUserInfo ( ++ self, ++ NSAccessibilitySelectedTextChangedNotification, ++ moveInfo); + -+ /* Character navigation: NSAccessibilitySelectedTextChangedNotification -+ (posted above) is sufficient — VoiceOver reads the char at the new -+ cursor position from accessibilityStringForRange. An additional -+ AnnouncementRequested would cause double-speech. The SelectedTextChanged -+ path also correctly signals braille displays. */ ++ /* Character navigation: announce the character AT point. ++ VoiceOver's default for SelectedTextChanged reads the character ++ BEFORE the new cursor position — correct for insert-mode but ++ wrong for evil block-cursor which sits ON the character at point. ++ 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. + Triggering on granularity=line covers ALL line-motion commands @@ -2605,7 +2649,7 @@ index 932d209..e2532af 100644 unsigned fnKeysym = 0; static NSMutableArray *nsEvArray; 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); kbd_buffer_store_event (&event); 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; } @@ -2942,7 +2986,7 @@ index 932d209..e2532af 100644 @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_handle_drag_motion, "ns-handle-drag-motion");