From 3502cfaf25276536a91bb16a19db713a64ae43c1 Mon Sep 17 00:00:00 2001 From: Daneel Date: Sun, 1 Mar 2026 14:41:40 +0100 Subject: [PATCH] patches: fix VoiceOver rotor cursor sync + echo area announcements --- ...d-frame-completion-candidates-for-Vo.patch | 128 ++++++++++-------- 1 file changed, 75 insertions(+), 53 deletions(-) diff --git a/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch b/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch index 0cc8e17..efdf783 100644 --- a/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch +++ b/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch @@ -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 Date: Sat, 28 Feb 2026 16:01:29 +0100 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 - etc/NEWS | 4 +- src/nsterm.h | 9 ++ - src/nsterm.m | 329 +++++++++++++++++++++++++++++++++++++++++-- - 4 files changed, 330 insertions(+), 18 deletions(-) + src/nsterm.m | 337 +++++++++++++++++++++++++++++++++++++++++-- + 4 files changed, 333 insertions(+), 23 deletions(-) diff --git a/doc/emacs/macos.texi b/doc/emacs/macos.texi index 6514dfc..f47929e 100644 @@ -97,7 +97,7 @@ index 21a93bc..bbce9fe 100644 @end diff --git a/src/nsterm.m b/src/nsterm.m -index 8d44b5f..7b254ca 100644 +index 8d44b5f..251a03e 100644 --- a/src/nsterm.m +++ b/src/nsterm.m @@ -7415,6 +7415,112 @@ visual line index for Zoom (skip whitespace-only lines @@ -274,28 +274,35 @@ index 8d44b5f..7b254ca 100644 cachedTextStart = start; 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, 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. This is the ONLY speech source for character navigation. 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 currentOverlayEnd = 0; @@ -303,53 +310,68 @@ index 8d44b5f..7b254ca 100644 specpdl_ref count2 = SPECPDL_INDEX (); record_unwind_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) return; + /* --- Echo area announcements --- + When the minibuffer is not active for user input (minibuf_level == 0) -+ and its text changes, post an AX announcement so VoiceOver reads the -+ new message. This covers error messages, warnings, and informational -+ echoes. Skipped while minibuf_level > 0 (user is typing a command) -+ to avoid interrupting prompt reading or completion. */ ++ and its character content changes, announce the new text to VoiceOver. ++ This surfaces error messages, informational echoes, and git/process ++ status updates. We skip the rest of the notification cycle (cursor ++ 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) + { -+ ptrdiff_t echo_chars_modiff = BUF_CHARS_MODIFF (b); -+ if (echo_chars_modiff != self.cachedCharsModiff) ++ ptrdiff_t echo_chars = BUF_CHARS_MODIFF (b); ++ if (echo_chars != self.cachedCharsModiff ++ && BUF_ZV (b) > BUF_BEGV (b)) + { -+ self.cachedCharsModiff = echo_chars_modiff; -+ ptrdiff_t dummy_start = 0; -+ NSUInteger nruns = 0; -+ ns_ax_visible_run *runs = NULL; -+ NSString *msg = ns_ax_buffer_text (w, &dummy_start, -+ &runs, &nruns); -+ if (runs) xfree (runs); -+ if (msg) ++ self.cachedCharsModiff = echo_chars; ++ struct buffer *prev = current_buffer; ++ set_buffer_internal (b); ++ Lisp_Object ls = Fbuffer_string (); ++ set_buffer_internal (prev); ++ NSString *raw = [NSString stringWithUTF8String: SSDATA (ls)]; ++ NSString *msg = [raw stringByTrimmingCharactersInSet: ++ [NSCharacterSet whitespaceAndNewlineCharacterSet]]; ++ if ([msg length] > 0) + { -+ NSString *trimmed = [msg stringByTrimmingCharactersInSet: -+ [NSCharacterSet whitespaceAndNewlineCharacterSet]]; -+ if ([trimmed length] > 0) -+ { -+ NSDictionary *info = @{ -+ NSAccessibilityAnnouncementKey: trimmed, -+ NSAccessibilityPriorityKey: -+ @(NSAccessibilityPriorityMedium) -+ }; -+ ns_ax_post_notification_with_info ( -+ NSApp, -+ NSAccessibilityAnnouncementRequestedNotification, -+ info); -+ } ++ NSDictionary *info = @{ ++ NSAccessibilityAnnouncementKey: msg, ++ NSAccessibilityPriorityKey: ++ @(NSAccessibilityPriorityHigh) ++ }; ++ ns_ax_post_notification_with_info ( ++ NSApp, ++ NSAccessibilityAnnouncementRequestedNotification, ++ info); + } + } -+ return; /* no cursor tracking for inactive minibuffer */ ++ return; + } + ptrdiff_t modiff = BUF_MODIFF (b); ptrdiff_t point = BUF_PT (b); 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 [accessibilityElements release]; @@ -360,7 +382,7 @@ index 8d44b5f..7b254ca 100644 [[self menu] release]; [super dealloc]; } -@@ -11380,6 +11564,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f +@@ -11380,6 +11562,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f windowClosing = NO; processingCompose = NO; @@ -370,7 +392,7 @@ index 8d44b5f..7b254ca 100644 scrollbarsNeedingUpdate = 0; fs_state = FULLSCREEN_NONE; 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 previous redisplay cycle. Rebuilding first would create fresh elements with current values, making change detection impossible. */ @@ -451,7 +473,7 @@ index 8d44b5f..7b254ca 100644 - (void)postAccessibilityUpdates { NSTRACE ("[EmacsView postAccessibilityUpdates]"); -@@ -12698,11 +12959,59 @@ - (void)postAccessibilityUpdates +@@ -12698,11 +12957,59 @@ - (void)postAccessibilityUpdates /* Re-entrance guard: VoiceOver callbacks during notification posting can trigger redisplay, which calls ns_update_end, which calls us