From 31fcc1a7113c54467b76c65e00d14c88ab88185b Mon Sep 17 00:00:00 2001 From: Daneel Date: Sat, 28 Feb 2026 16:11:52 +0100 Subject: [PATCH] patches: remove stale 0007 (merged overlay+child-frame variant) --- ...lay-and-child-frame-completion-for-V.patch | 807 ------------------ 1 file changed, 807 deletions(-) delete mode 100644 patches/0007-ns-announce-overlay-and-child-frame-completion-for-V.patch diff --git a/patches/0007-ns-announce-overlay-and-child-frame-completion-for-V.patch b/patches/0007-ns-announce-overlay-and-child-frame-completion-for-V.patch deleted file mode 100644 index 8dc43a4..0000000 --- a/patches/0007-ns-announce-overlay-and-child-frame-completion-for-V.patch +++ /dev/null @@ -1,807 +0,0 @@ -From 8aa35132a10eaa12d0ff40389973c6b84e2ef659 Mon Sep 17 00:00:00 2001 -From: Martin Sukany -Date: Sat, 28 Feb 2026 14:46:25 +0100 -Subject: [PATCH] ns: announce overlay and child frame completion for VoiceOver - -Completion frameworks render candidates either as overlay strings -(Vertico, Ivy, Icomplete in the minibuffer) or in a child frame -(Corfu, Company-box). Without this patch, VoiceOver cannot read -either type of completion UI. - -Overlay completion (minibuffer): - -Identify the selected candidate by scanning overlay before-string -and after-string properties for a face whose symbol name contains -"current", "selected", or "selection" --- matching vertico-current, -icomplete-selected-match, ivy-current-match, company-tooltip-selection, -and similar framework faces without hard-coding any specific name. - -- The overlay detection branch runs independently (if, not else-if) - of the text-change branch, because Vertico bumps both BUF_MODIFF - and BUF_OVERLAY_MODIFF in the same command cycle. - -- Use BUF_CHARS_MODIFF to gate ValueChanged notifications, since - text property changes bump BUF_MODIFF but not BUF_CHARS_MODIFF. - -- Remove BUF_OVERLAY_MODIFF from ensureTextCache validity checks - to prevent a race where AX queries consume the overlay change - before the notification dispatch runs. - -- Announce via AnnouncementRequested to NSApp with High priority. - -- Zoom tracking: store the candidate rect (at text area left edge, - computed from FRAME_LINE_HEIGHT) in overlayZoomRect. The flag - is cleared on typing or minibuffer exit. - -Child frame completion (in-buffer): - -Detect child frames in postAccessibilityUpdates via -FRAME_PARENT_FRAME. Scan the child frame buffer text for a line -with a selected face (via Fget_char_property, which checks both -text properties and overlay faces). Post announcement and call -UAZoomChangeFocus directly (the child frame renders after the -parent's draw_window_cursor, so the last Zoom call wins). - -* src/nsterm.h (EmacsView): Add overlayZoomActive, overlayZoomRect, -announceChildFrameCompletion. -(EmacsAccessibilityBuffer): Add cachedCharsModiff. -* src/nsterm.m (ns_ax_face_is_selected): New predicate. -(ns_ax_selected_overlay_text): New function (overlay strings). -(ns_ax_selected_child_frame_text): New function (buffer text). -(ns_draw_window_cursor): Use overlayZoomRect when active. -(EmacsAccessibilityBuffer ensureTextCache): Remove overlay_modiff. -(EmacsAccessibilityBuffer postAccessibilityNotificationsForFrame:): -Independent overlay branch with BUF_CHARS_MODIFF gating. -(EmacsView announceChildFrameCompletion): New method. -(EmacsView postAccessibilityUpdates): Dispatch to child frame -handler for FRAME_PARENT_FRAME frames. ---- - src/nsterm.h | 4 + - src/nsterm.m | 482 +++++++++++++++++++++++++++++++++++++++++++++++---- - 2 files changed, 450 insertions(+), 36 deletions(-) - -diff --git a/src/nsterm.h b/src/nsterm.h -index 51c30ca..21b2823 100644 ---- a/src/nsterm.h -+++ b/src/nsterm.h -@@ -507,6 +507,7 @@ typedef struct ns_ax_visible_run - @property (nonatomic, assign) ptrdiff_t cachedOverlayModiff; - @property (nonatomic, assign) ptrdiff_t cachedTextStart; - @property (nonatomic, assign) ptrdiff_t cachedModiff; -+@property (nonatomic, assign) ptrdiff_t cachedCharsModiff; - @property (nonatomic, assign) ptrdiff_t cachedPoint; - @property (nonatomic, assign) BOOL cachedMarkActive; - @property (nonatomic, copy) NSString *cachedCompletionAnnouncement; -@@ -591,6 +592,8 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType) - BOOL accessibilityUpdating; - @public /* Accessed by ns_draw_phys_cursor (C function). */ - NSRect lastAccessibilityCursorRect; -+ BOOL overlayZoomActive; -+ NSRect overlayZoomRect; - #endif - BOOL font_panel_active; - NSFont *font_panel_result; -@@ -654,6 +657,7 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType) - - (void)rebuildAccessibilityTree; - - (void)invalidateAccessibilityTree; - - (void)postAccessibilityUpdates; -+- (void)announceChildFrameCompletion; - #endif - @end - -diff --git a/src/nsterm.m b/src/nsterm.m -index 1780194..59dc14d 100644 ---- a/src/nsterm.m -+++ b/src/nsterm.m -@@ -3258,7 +3258,12 @@ ns_draw_window_cursor (struct window *w, struct glyph_row *glyph_row, - && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 - if (UAZoomEnabled ()) - { -- NSRect windowRect = [view convertRect:r toView:nil]; -+ /* When overlay completion is active (e.g. Vertico), -+ focus Zoom on the selected candidate row instead -+ of the text cursor. */ -+ NSRect zoomSrc = view->overlayZoomActive -+ ? view->overlayZoomRect : r; -+ NSRect windowRect = [view convertRect:zoomSrc toView:nil]; - NSRect screenRect = [[view window] convertRectToScreen:windowRect]; - CGRect cgRect = NSRectToCGRect (screenRect); - -@@ -6915,11 +6920,248 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action) - are truncated for accessibility purposes. */ - #define NS_AX_TEXT_CAP 100000 - -+/* Return true if FACE is or contains a face symbol whose name -+ includes "current" or "selected", indicating a highlighted -+ completion candidate. Works for vertico-current, -+ icomplete-selected-match, ivy-current-match, etc. */ -+static bool -+ns_ax_face_is_selected (Lisp_Object face) -+{ -+ if (SYMBOLP (face) && !NILP (face)) -+ { -+ const char *name = SSDATA (SYMBOL_NAME (face)); -+ /* Substring match is intentionally broad --- it catches -+ vertico-current, icomplete-selected-match, ivy-current-match, -+ company-tooltip-selection, and similar. False positives are -+ harmless since this runs only on overlay strings during -+ completion. */ -+ if (strstr (name, "current") || strstr (name, "selected") -+ || strstr (name, "selection")) -+ return true; -+ } -+ if (CONSP (face)) -+ { -+ for (Lisp_Object tail = face; CONSP (tail); tail = XCDR (tail)) -+ if (ns_ax_face_is_selected (XCAR (tail))) -+ return true; -+ } -+ return false; -+} -+ -+/* Extract the currently selected candidate text from overlay display -+ strings. Completion frameworks render candidates as overlay -+ before-string/after-string and highlight the current candidate -+ with a face whose name contains "current" or "selected" -+ (e.g. vertico-current, icomplete-selected-match, ivy-current-match). -+ -+ Scan all overlays in the buffer region [BEG, END), find the line -+ whose face matches the selection heuristic, and return it (already -+ trimmed of surrounding whitespace). -+ -+ Also set *OUT_LINE_INDEX to the 0-based visual line index of the -+ selected candidate (for Zoom positioning), counting only non-trivial -+ lines. Set to -1 if not found. -+ -+ Returns nil if no selected candidate is found. */ -+static NSString * -+ns_ax_selected_overlay_text (struct buffer *b, -+ ptrdiff_t beg, ptrdiff_t end, -+ int *out_line_index) -+{ -+ *out_line_index = -1; -+ -+ Lisp_Object ov_list = Foverlays_in (make_fixnum (beg), -+ make_fixnum (end)); -+ -+ for (Lisp_Object tail = ov_list; CONSP (tail); tail = XCDR (tail)) -+ { -+ Lisp_Object ov = XCAR (tail); -+ Lisp_Object strings[2]; -+ strings[0] = Foverlay_get (ov, intern_c_string ("before-string")); -+ strings[1] = Foverlay_get (ov, intern_c_string ("after-string")); -+ -+ for (int s = 0; s < 2; s++) -+ { -+ if (!STRINGP (strings[s])) -+ continue; -+ -+ Lisp_Object str = strings[s]; -+ ptrdiff_t slen = SCHARS (str); -+ if (slen == 0) -+ continue; -+ -+ /* Scan for newline positions using SDATA for efficiency. -+ The data pointer is used only in this loop, before any -+ Lisp calls (Fget_text_property etc.) that could trigger -+ GC and relocate string data. */ -+ const unsigned char *data = SDATA (str); -+ ptrdiff_t byte_len = SBYTES (str); -+ /* 512 lines is sufficient for any completion UI; -+ vertico-count defaults to 10. */ -+ ptrdiff_t line_starts[512]; -+ ptrdiff_t line_ends[512]; -+ int nlines = 0; -+ ptrdiff_t char_pos = 0, byte_pos = 0, lstart = 0; -+ -+ while (byte_pos < byte_len && nlines < 512) -+ { -+ if (data[byte_pos] == '\n') -+ { -+ if (char_pos > lstart) -+ { -+ line_starts[nlines] = lstart; -+ line_ends[nlines] = char_pos; -+ nlines++; -+ } -+ lstart = char_pos + 1; -+ } -+ if (STRING_MULTIBYTE (str)) -+ byte_pos += BYTES_BY_CHAR_HEAD (data[byte_pos]); -+ else -+ byte_pos++; -+ char_pos++; -+ } -+ if (char_pos > lstart && nlines < 512) -+ { -+ line_starts[nlines] = lstart; -+ line_ends[nlines] = char_pos; -+ nlines++; -+ } -+ -+ /* Find the line whose face indicates selection. Track -+ visual line index for Zoom (skip whitespace-only lines -+ like Vertico's leading cursor-space). */ -+ int candidate_idx = 0; -+ for (int li = 0; li < nlines; li++) -+ { -+ Lisp_Object face -+ = Fget_text_property (make_fixnum (line_starts[li]), -+ Qface, str); -+ if (ns_ax_face_is_selected (face)) -+ { -+ Lisp_Object line -+ = Fsubstring_no_properties ( -+ str, -+ make_fixnum (line_starts[li]), -+ make_fixnum (line_ends[li])); -+ NSString *text = [NSString stringWithLispString:line]; -+ text = [text stringByTrimmingCharactersInSet: -+ [NSCharacterSet -+ whitespaceAndNewlineCharacterSet]]; -+ if ([text length] > 0) -+ { -+ *out_line_index = candidate_idx; -+ return text; -+ } -+ } -+ -+ /* Count non-trivial lines as candidates for Zoom. */ -+ if (line_ends[li] - line_starts[li] > 1) -+ candidate_idx++; -+ } -+ } -+ } -+ -+ return nil; -+} -+ -+ -+/* Scan buffer text of a child frame for the selected completion -+ candidate. Used for frameworks that render candidates in a -+ child frame (e.g. Corfu, Company-box) rather than as overlay -+ strings. Check the effective face (text properties + overlays) -+ at the start of each line via Fget_char_property. -+ -+ Returns the candidate text (trimmed) or nil. Sets -+ *OUT_LINE_INDEX to the 0-based line index for Zoom. */ -+static NSString * -+ns_ax_selected_child_frame_text (struct buffer *b, Lisp_Object buf_obj, -+ int *out_line_index) -+{ -+ *out_line_index = -1; -+ ptrdiff_t beg = BUF_BEGV (b); -+ ptrdiff_t end = BUF_ZV (b); -+ -+ if (beg >= end) -+ return nil; -+ -+ /* Get buffer text as a Lisp string for efficient scanning. -+ The buffer is a small completion popup (typically < 20 lines). */ -+ Lisp_Object str -+ = Fbuffer_substring_no_properties (make_fixnum (beg), -+ make_fixnum (end)); -+ if (!STRINGP (str) || SCHARS (str) == 0) -+ return nil; -+ -+ /* Scan newlines (same pattern as ns_ax_selected_overlay_text). -+ The data pointer is used only in this loop, before Lisp calls. */ -+ const unsigned char *data = SDATA (str); -+ ptrdiff_t byte_len = SBYTES (str); -+ ptrdiff_t line_starts[128]; -+ ptrdiff_t line_ends[128]; -+ int nlines = 0; -+ ptrdiff_t char_pos = 0, byte_pos = 0, lstart = 0; -+ -+ while (byte_pos < byte_len && nlines < 128) -+ { -+ if (data[byte_pos] == '\n') -+ { -+ if (char_pos > lstart) -+ { -+ line_starts[nlines] = lstart; -+ line_ends[nlines] = char_pos; -+ nlines++; -+ } -+ lstart = char_pos + 1; -+ } -+ if (STRING_MULTIBYTE (str)) -+ byte_pos += BYTES_BY_CHAR_HEAD (data[byte_pos]); -+ else -+ byte_pos++; -+ char_pos++; -+ } -+ if (char_pos > lstart && nlines < 128) -+ { -+ line_starts[nlines] = lstart; -+ line_ends[nlines] = char_pos; -+ nlines++; -+ } -+ -+ /* Find the line with a selected face. Use Fget_char_property on -+ the BUFFER (not the string) so overlay faces are included. -+ Offset string positions by beg to get buffer positions. */ -+ for (int li = 0; li < nlines; li++) -+ { -+ ptrdiff_t buf_pos = beg + line_starts[li]; -+ Lisp_Object face -+ = Fget_char_property (make_fixnum (buf_pos), Qface, buf_obj); -+ -+ if (ns_ax_face_is_selected (face)) -+ { -+ Lisp_Object line -+ = Fsubstring_no_properties (str, -+ make_fixnum (line_starts[li]), -+ make_fixnum (line_ends[li])); -+ NSString *text = [NSString stringWithLispString:line]; -+ text = [text stringByTrimmingCharactersInSet: -+ [NSCharacterSet -+ whitespaceAndNewlineCharacterSet]]; -+ if ([text length] > 0) -+ { -+ *out_line_index = li; -+ return text; -+ } -+ } -+ } -+ -+ return nil; -+} -+ -+ - /* Build accessibility text for window W, skipping invisible text. - Populates *OUT_START with the buffer start charpos. - Populates *OUT_RUNS with an array of visible runs and *OUT_NRUNS - with the count. Caller must free *OUT_RUNS with xfree(). */ -- - static NSString * - ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start, - ns_ax_visible_run **out_runs, NSUInteger *out_nruns) -@@ -6996,7 +7238,7 @@ ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start, - - /* Extract this visible run's text. Use - Fbuffer_substring_no_properties which correctly handles the -- buffer gap — raw BUF_BYTE_ADDRESS reads across the gap would -+ buffer gap --- raw BUF_BYTE_ADDRESS reads across the gap would - include garbage bytes when the run spans the gap position. */ - Lisp_Object lstr = Fbuffer_substring_no_properties ( - make_fixnum (pos), make_fixnum (run_end)); -@@ -7077,7 +7319,7 @@ ns_ax_frame_for_range (struct window *w, EmacsView *view, - return NSZeroRect; - - /* charpos_start and charpos_len are already in buffer charpos -- space — the caller maps AX string indices through -+ space --- the caller maps AX string indices through - charposForAccessibilityIndex which handles invisible text. */ - ptrdiff_t cp_start = charpos_start; - ptrdiff_t cp_end = cp_start + charpos_len; -@@ -7556,6 +7798,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, - @synthesize cachedOverlayModiff; - @synthesize cachedTextStart; - @synthesize cachedModiff; -+@synthesize cachedCharsModiff; - @synthesize cachedPoint; - @synthesize cachedMarkActive; - @synthesize cachedCompletionAnnouncement; -@@ -7596,7 +7839,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, - NSTRACE ("EmacsAccessibilityBuffer ensureTextCache"); - /* This method is only called from the main thread (AX getters - dispatch_sync to main first). Reads of cachedText/cachedTextModiff -- below are therefore safe without @synchronized — only the -+ below are therefore safe without @synchronized --- only the - write section at the end needs synchronization to protect - against concurrent reads from AX server thread. */ - eassert ([NSThread isMainThread]); -@@ -7609,16 +7852,15 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, - return; - - ptrdiff_t modiff = BUF_MODIFF (b); -- ptrdiff_t overlay_modiff = BUF_OVERLAY_MODIFF (b); - ptrdiff_t pt = BUF_PT (b); - NSUInteger textLen = cachedText ? [cachedText length] : 0; -- /* Track both BUF_MODIFF and BUF_OVERLAY_MODIFF. Overlay-only -- changes (e.g., timer-based completion highlight move without -- text edit) bump overlay_modiff but not modiff. Also detect -- narrowing/widening which changes BUF_BEGV without bumping -- either modiff counter. */ -+ /* Cache validity: track BUF_MODIFF and buffer narrowing. -+ Do NOT track BUF_OVERLAY_MODIFF here --- overlay text is not -+ included in the cached AX text (it is handled separately via -+ explicit announcements). Including overlay_modiff would -+ silently update cachedOverlayModiff and prevent the -+ notification dispatch from detecting overlay changes. */ - if (cachedText && cachedTextModiff == modiff -- && cachedOverlayModiff == overlay_modiff - && cachedTextStart == BUF_BEGV (b) - && pt >= cachedTextStart - && (textLen == 0 -@@ -7635,7 +7877,6 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, - [cachedText release]; - cachedText = [text retain]; - cachedTextModiff = modiff; -- cachedOverlayModiff = overlay_modiff; - cachedTextStart = start; - - if (visibleRuns) -@@ -7661,7 +7902,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, - /* Binary search: runs are sorted by charpos (ascending). Find the - run whose [charpos, charpos+length) range contains the target, - or the nearest run after an invisible gap. O(log n) instead of -- O(n) — matters for org-mode with many folded sections. */ -+ O(n) --- matters for org-mode with many folded sections. */ - NSUInteger lo = 0, hi = visibleRunCount; - while (lo < hi) - { -@@ -7674,7 +7915,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, - else - { - /* Found: charpos is inside this run. Compute UTF-16 delta -- directly from cachedText — no Lisp calls needed. */ -+ directly from cachedText --- no Lisp calls needed. */ - NSUInteger chars_in = (NSUInteger)(charpos - r->charpos); - if (chars_in == 0 || !cachedText) - return r->ax_start; -@@ -7699,10 +7940,10 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, - - /* Convert accessibility string index to buffer charpos. - Safe to call from any thread: uses only cachedText (NSString) and -- visibleRuns — no Lisp calls. */ -+ visibleRuns --- no Lisp calls. */ - - (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx - { -- /* May be called from AX server thread — synchronize. */ -+ /* May be called from AX server thread --- synchronize. */ - @synchronized (self) - { - if (visibleRunCount == 0) -@@ -7736,7 +7977,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, - return cp; - } - } -- /* Past end — return last charpos. */ -+ /* Past end --- return last charpos. */ - if (lo > 0) - { - ns_ax_visible_run *last = &visibleRuns[visibleRunCount - 1]; -@@ -7758,7 +7999,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, - deadlocking the AX server thread. This is prevented by: - - 1. validWindow checks WINDOW_LIVE_P and BUFFERP before every -- Lisp access — the window and buffer are verified live. -+ Lisp access --- the window and buffer are verified live. - 2. All dispatch_sync blocks run on the main thread where no - concurrent Lisp code can modify state between checks. - 3. block_input prevents timer events and process output from -@@ -8166,7 +8407,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, - if (idx > [cachedText length]) - idx = [cachedText length]; - -- /* Count lines by iterating lineRangeForRange — O(lines). */ -+ /* Count lines by iterating lineRangeForRange --- O(lines). */ - NSInteger line = 0; - NSUInteger scan = 0; - NSUInteger len = [cachedText length]; -@@ -8422,7 +8663,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, - - - /* =================================================================== -- EmacsAccessibilityBuffer (Notifications) — AX event dispatch -+ EmacsAccessibilityBuffer (Notifications) --- AX event dispatch - - These methods notify VoiceOver of text and selection changes. - Called from the redisplay cycle (postAccessibilityUpdates). -@@ -8437,7 +8678,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, - if (point > self.cachedPoint - && point - self.cachedPoint == 1) - { -- /* Single char inserted — refresh cache and grab it. */ -+ /* Single char inserted --- refresh cache and grab it. */ - [self invalidateTextCache]; - [self ensureTextCache]; - if (cachedText) -@@ -8456,7 +8697,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, - /* Update cachedPoint here so the selection-move branch does NOT - fire for point changes caused by edits. WebKit and Chromium - never send both ValueChanged and SelectedTextChanged for the -- same user action — they are mutually exclusive. */ -+ same user action --- they are mutually exclusive. */ - self.cachedPoint = point; - - NSDictionary *change = @{ -@@ -8789,14 +9030,112 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, - BOOL markActive = !NILP (BVAR (b, mark_active)); - - /* --- Text changed (edit) --- */ -+ ptrdiff_t chars_modiff = BUF_CHARS_MODIFF (b); - if (modiff != self.cachedModiff) - { - self.cachedModiff = modiff; -- [self postTextChangedNotification:point]; -+ /* Only post ValueChanged when actual characters changed. -+ Text property changes (e.g. face updates from -+ vertico--prompt-selection) bump BUF_MODIFF but not -+ BUF_CHARS_MODIFF. Posting ValueChanged for property-only -+ changes causes VoiceOver to say "new line" when the diff -+ is non-empty due to overlay content changes. */ -+ if (chars_modiff != self.cachedCharsModiff) -+ { -+ self.cachedCharsModiff = chars_modiff; -+ self.emacsView->overlayZoomActive = NO; -+ [self postTextChangedNotification:point]; -+ } -+ } -+ -+ -+ /* --- Overlay content changed (e.g. Vertico/Ivy candidate switch) --- -+ Check independently of the modiff branch above, because -+ frameworks like Vertico bump BOTH BUF_MODIFF (via text property -+ changes in vertico--prompt-selection) and BUF_OVERLAY_MODIFF -+ (via overlay-put) in the same command cycle. If this were an -+ else-if, the modiff branch would always win and overlay -+ announcements would never fire. -+ Do NOT invalidate the text cache --- the buffer text has not -+ changed, and cache invalidation causes VoiceOver to diff old -+ vs new AX text and announce spurious "new line". */ -+ if (BUF_OVERLAY_MODIFF (b) != self.cachedOverlayModiff) -+ { -+ self.cachedOverlayModiff = BUF_OVERLAY_MODIFF (b); -+ -+ int selected_line = -1; -+ NSString *candidate -+ = ns_ax_selected_overlay_text (b, BUF_BEGV (b), BUF_ZV (b), -+ &selected_line); -+ if (candidate) -+ { -+ /* Deduplicate: only announce when the candidate changed. */ -+ if (![candidate isEqualToString: -+ self.cachedCompletionAnnouncement]) -+ { -+ self.cachedCompletionAnnouncement = candidate; -+ -+ /* Announce the candidate text directly via NSApp. -+ Do NOT post SelectedTextChanged --- that would cause -+ VoiceOver to read the AX text at the cursor position -+ (the minibuffer input line), not the overlay candidate. -+ AnnouncementRequested with High priority interrupts -+ any current speech and announces our text. */ -+ NSDictionary *annInfo = @{ -+ NSAccessibilityAnnouncementKey: candidate, -+ NSAccessibilityPriorityKey: -+ @(NSAccessibilityPriorityHigh) -+ }; -+ ns_ax_post_notification_with_info ( -+ NSApp, -+ NSAccessibilityAnnouncementRequestedNotification, -+ annInfo); -+ -+ /* --- Zoom tracking for overlay candidates --- -+ Store the candidate row rect so draw_window_cursor -+ focuses Zoom there instead of on the text cursor. -+ Cleared when the user types (chars_modiff change). -+ -+ Use default line height to compute the Y offset: -+ row 0 is the input line, overlay candidates start -+ from row 1. This avoids fragile glyph matrix row -+ index mapping which can be off when group titles -+ or wrapped lines shift row numbering. */ -+ if (selected_line >= 0) -+ { -+ struct window *w2 = [self validWindow]; -+ if (w2) -+ { -+ EmacsView *view = self.emacsView; -+ struct frame *f2 = XFRAME (w2->frame); -+ int line_h = FRAME_LINE_HEIGHT (f2); -+ int y_off = (selected_line + 1) * line_h; -+ -+ if (y_off < w2->pixel_height) -+ { -+ view->overlayZoomRect = NSMakeRect ( -+ WINDOW_TEXT_TO_FRAME_PIXEL_X (w2, 0), -+ WINDOW_TO_FRAME_PIXEL_Y (w2, y_off), -+ FRAME_COLUMN_WIDTH (f2), -+ line_h); -+ view->overlayZoomActive = YES; -+ } -+ } -+ } -+ } -+ } -+ else -+ { -+ /* No selected candidate --- overlay completion ended -+ (minibuffer exit, C-g, etc.) or overlay has no -+ recognizable selection face. Return Zoom to the -+ text cursor. */ -+ self.emacsView->overlayZoomActive = NO; -+ } - } - - /* --- Cursor moved or selection changed --- -- Use 'else if' — edits and selection moves are mutually exclusive -+ Use 'else if' --- edits and selection moves are mutually exclusive - per the WebKit/Chromium pattern. */ - else if (point != self.cachedPoint || markActive != self.cachedMarkActive) - { -@@ -8966,7 +9305,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, - - - /* =================================================================== -- EmacsAccessibilityInteractiveSpan — helpers and implementation -+ EmacsAccessibilityInteractiveSpan --- helpers and implementation - =================================================================== */ - - /* Scan visible range of window W for interactive spans. -@@ -9157,7 +9496,7 @@ ns_ax_scan_interactive_spans (struct window *w, - - (BOOL) isAccessibilityFocused - { - /* Read the cached point stored by EmacsAccessibilityBuffer on the main -- thread — safe to read from any thread (plain ptrdiff_t, no Lisp calls). */ -+ thread --- safe to read from any thread (plain ptrdiff_t, no Lisp calls). */ - EmacsAccessibilityBuffer *pb = self.parentBuffer; - if (!pb) - return NO; -@@ -9174,7 +9513,7 @@ ns_ax_scan_interactive_spans (struct window *w, - dispatch_async (dispatch_get_main_queue (), ^{ - /* lwin is a Lisp_Object captured by value. This is GC-safe - because Lisp_Objects are tagged integers/pointers that -- remain valid across GC — GC does not relocate objects in -+ remain valid across GC --- GC does not relocate objects in - Emacs. The WINDOW_LIVE_P check below guards against the - window being deleted between capture and execution. */ - if (!WINDOWP (lwin) || NILP (Fwindow_live_p (lwin))) -@@ -9200,7 +9539,7 @@ ns_ax_scan_interactive_spans (struct window *w, - - @end - --/* EmacsAccessibilityBuffer — InteractiveSpans category. -+/* EmacsAccessibilityBuffer --- InteractiveSpans category. - Methods are kept here (same .m file) so they access the ivars - declared in the @interface ivar block. */ - @implementation EmacsAccessibilityBuffer (InteractiveSpans) -@@ -10520,13 +10859,13 @@ ns_in_echo_area (void) - if (old_title == 0) - { - char *t = strdup ([[[self window] title] UTF8String]); -- char *pos = strstr (t, " — "); -+ char *pos = strstr (t, " --- "); - if (pos) - *pos = '\0'; - old_title = t; - } - size_title = xmalloc (strlen (old_title) + 40); -- esprintf (size_title, "%s — (%d × %d)", old_title, cols, rows); -+ esprintf (size_title, "%s --- (%d × %d)", old_title, cols, rows); - [window setTitle: [NSString stringWithUTF8String: size_title]]; - [window display]; - xfree (size_title); -@@ -11922,7 +12261,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, - - if (WINDOW_LEAF_P (w)) - { -- /* Buffer element — reuse existing if available. */ -+ /* Buffer element --- reuse existing if available. */ - EmacsAccessibilityBuffer *elem - = [existing objectForKey:[NSValue valueWithPointer:w]]; - if (!elem) -@@ -11956,7 +12295,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, - } - else - { -- /* Internal (combination) window — recurse into children. */ -+ /* Internal (combination) window --- recurse into children. */ - Lisp_Object child = w->contents; - while (!NILP (child)) - { -@@ -12052,6 +12391,68 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, - 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. */ -+ -+/* Announce the selected candidate in a child frame completion popup. -+ Handles Corfu, Company-box, and similar frameworks that render -+ candidates in a separate child frame rather than as overlay strings -+ in the minibuffer. Uses direct UAZoomChangeFocus (not the -+ overlayZoomRect flag) because the child frame's ns_update_end runs -+ after the parent's draw_window_cursor. */ -+- (void)announceChildFrameCompletion -+{ -+ static char *lastCandidate; -+ -+ struct window *w = XWINDOW (emacsframe->selected_window); -+ struct buffer *b = XBUFFER (w->contents); -+ int selected_line = -1; -+ NSString *candidate -+ = ns_ax_selected_child_frame_text (b, w->contents, &selected_line); -+ -+ if (!candidate) -+ return; -+ -+ /* Deduplicate --- avoid re-announcing the same candidate. */ -+ const char *cstr = [candidate UTF8String]; -+ if (lastCandidate && strcmp (cstr, lastCandidate) == 0) -+ return; -+ xfree (lastCandidate); -+ lastCandidate = xstrdup (cstr); -+ -+ NSDictionary *annInfo = @{ -+ NSAccessibilityAnnouncementKey: candidate, -+ NSAccessibilityPriorityKey: -+ @(NSAccessibilityPriorityHigh) -+ }; -+ ns_ax_post_notification_with_info ( -+ NSApp, -+ NSAccessibilityAnnouncementRequestedNotification, -+ annInfo); -+ -+ /* Zoom tracking: focus on the selected row in the child frame. -+ Use direct UAZoomChangeFocus rather than overlayZoomRect because -+ the child frame renders independently of the parent. */ -+ if (selected_line >= 0 && UAZoomEnabled ()) -+ { -+ int line_h = FRAME_LINE_HEIGHT (emacsframe); -+ int y_off = selected_line * line_h; -+ NSRect r = NSMakeRect ( -+ WINDOW_TEXT_TO_FRAME_PIXEL_X (w, 0), -+ WINDOW_TO_FRAME_PIXEL_Y (w, y_off), -+ FRAME_COLUMN_WIDTH (emacsframe), -+ line_h); -+ NSRect winRect = [self convertRect:r toView:nil]; -+ NSRect screenRect -+ = [[self window] convertRectToScreen:winRect]; -+ CGRect cgRect = NSRectToCGRect (screenRect); -+ CGFloat primaryH -+ = [[[NSScreen screens] firstObject] frame].size.height; -+ cgRect.origin.y -+ = primaryH - cgRect.origin.y - cgRect.size.height; -+ UAZoomChangeFocus (&cgRect, &cgRect, -+ kUAZoomFocusTypeInsertionPoint); -+ } -+} -+ - - (void)postAccessibilityUpdates - { - NSTRACE ("[EmacsView postAccessibilityUpdates]"); -@@ -12060,6 +12461,15 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, - if (!emacsframe || !ns_accessibility_enabled) - return; - -+ /* Child frame completion popup (Corfu, Company-box, etc.). -+ Child frames don't participate in the accessibility tree; -+ announce the selected candidate directly. */ -+ if (FRAME_PARENT_FRAME (emacsframe)) -+ { -+ [self announceChildFrameCompletion]; -+ return; -+ } -+ - /* Re-entrance guard: VoiceOver callbacks during notification posting - can trigger redisplay, which calls ns_update_end, which calls us - again. Prevent infinite recursion. */ -@@ -12068,7 +12478,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, - accessibilityUpdating = YES; - - /* Detect window tree change (split, delete, new buffer). Compare -- FRAME_ROOT_WINDOW — if it changed, the tree structure changed. */ -+ FRAME_ROOT_WINDOW --- if it changed, the tree structure changed. */ - Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe); - if (!EQ (curRoot, lastRootWindow)) - { -@@ -12077,12 +12487,12 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, - } - - /* If tree is stale, rebuild FIRST so we don't iterate freed -- window pointers. Skip notifications for this cycle — the -+ window pointers. Skip notifications for this cycle --- the - freshly-built elements have no previous state to diff against. */ - if (!accessibilityTreeValid) - { - [self rebuildAccessibilityTree]; -- /* Invalidate span cache — window layout changed. */ -+ /* Invalidate span cache --- window layout changed. */ - for (EmacsAccessibilityElement *elem in accessibilityElements) - if ([elem isKindOfClass: [EmacsAccessibilityBuffer class]]) - [(EmacsAccessibilityBuffer *) elem invalidateInteractiveSpans]; --- -2.43.0 -