patches: v8 0007 - child frame completion (Corfu) + Zoom fix

Added: child frame buffer scanning for Corfu/Company-box.
Fixed: Zoom rect at text area left edge (not window center).
Added: 'selection' face match for company-tooltip-selection.
This commit is contained in:
2026-02-28 15:59:11 +01:00
parent be4e0bb5be
commit 92188ab008

View File

@@ -1,62 +1,67 @@
From 877cfc076d708c0222ef0b6e1b47da9e94a74af7 Mon Sep 17 00:00:00 2001
From 8aa35132a10eaa12d0ff40389973c6b84e2ef659 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 14:46:25 +0100
Subject: [PATCH] ns: announce overlay completion candidates for VoiceOver
Subject: [PATCH] ns: announce overlay and child frame completion for VoiceOver
Completion frameworks such as Vertico, Ivy, and Icomplete render
candidates via overlay before-string/after-string properties rather
than buffer text. Without this patch, VoiceOver cannot read
overlay-based completion UIs.
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.
Identify the selected candidate by scanning overlay strings for a
face whose symbol name contains "current", "selected", or
"selection" --- this matches vertico-current, icomplete-selected-match,
ivy-current-match, company-tooltip-selection, and similar framework
faces without hard-coding any specific name.
Overlay completion (minibuffer):
Key implementation details:
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
(via text property changes in vertico--prompt-selection) and
BUF_OVERLAY_MODIFF (via overlay-put) in the same command cycle.
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 condition where VoiceOver AX queries silently
consume the overlay change before the notification dispatch runs.
to prevent a race where AX queries consume the overlay change
before the notification dispatch runs.
- Announce via AnnouncementRequested to NSApp with High priority.
Do not post SelectedTextChanged (that reads the AX text at cursor
position, which is the minibuffer input, not the candidate).
- Zoom tracking: store the selected candidate's rect (at the text
area left edge, computed from FRAME_LINE_HEIGHT) in overlayZoomRect.
ns_draw_window_cursor checks overlayZoomActive and uses the stored
rect instead of the text cursor rect, keeping Zoom focused on the
candidate line start. The flag is cleared when the user types
(BUF_CHARS_MODIFF changes) or when no candidate is found
(minibuffer exit, C-g).
- 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.
* src/nsterm.h (EmacsView): Add overlayZoomActive, overlayZoomRect.
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. Match
"current", "selected", and "selection" in face symbol names.
(ns_ax_selected_overlay_text): New function.
* 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, BUF_CHARS_MODIFF gating, candidate
announcement with overlay Zoom rect storage.
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 | 3 +
src/nsterm.m | 319 +++++++++++++++++++++++++++++++++++++++++++++------
2 files changed, 286 insertions(+), 36 deletions(-)
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..5c15639 100644
index 51c30ca..21b2823 100644
--- a/src/nsterm.h
+++ b/src/nsterm.h
@@ -507,6 +507,7 @@ typedef struct ns_ax_visible_run
@@ -76,8 +81,16 @@ index 51c30ca..5c15639 100644
#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..d13c5c7 100644
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,
@@ -94,7 +107,7 @@ index 1780194..d13c5c7 100644
NSRect screenRect = [[view window] convertRectToScreen:windowRect];
CGRect cgRect = NSRectToCGRect (screenRect);
@@ -6915,11 +6920,156 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action)
@@ -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
@@ -243,6 +256,98 @@ index 1780194..d13c5c7 100644
+ 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.
@@ -252,7 +357,7 @@ index 1780194..d13c5c7 100644
static NSString *
ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start,
ns_ax_visible_run **out_runs, NSUInteger *out_nruns)
@@ -6996,7 +7146,7 @@ ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start,
@@ -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
@@ -261,7 +366,7 @@ index 1780194..d13c5c7 100644
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 +7227,7 @@ ns_ax_frame_for_range (struct window *w, EmacsView *view,
@@ -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
@@ -270,7 +375,7 @@ index 1780194..d13c5c7 100644
charposForAccessibilityIndex which handles invisible text. */
ptrdiff_t cp_start = charpos_start;
ptrdiff_t cp_end = cp_start + charpos_len;
@@ -7556,6 +7706,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
@@ -7556,6 +7798,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
@synthesize cachedOverlayModiff;
@synthesize cachedTextStart;
@synthesize cachedModiff;
@@ -278,7 +383,7 @@ index 1780194..d13c5c7 100644
@synthesize cachedPoint;
@synthesize cachedMarkActive;
@synthesize cachedCompletionAnnouncement;
@@ -7596,7 +7747,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
@@ -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
@@ -287,7 +392,7 @@ index 1780194..d13c5c7 100644
write section at the end needs synchronization to protect
against concurrent reads from AX server thread. */
eassert ([NSThread isMainThread]);
@@ -7609,16 +7760,15 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
@@ -7609,16 +7852,15 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
return;
ptrdiff_t modiff = BUF_MODIFF (b);
@@ -310,7 +415,7 @@ index 1780194..d13c5c7 100644
&& cachedTextStart == BUF_BEGV (b)
&& pt >= cachedTextStart
&& (textLen == 0
@@ -7635,7 +7785,6 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
@@ -7635,7 +7877,6 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
[cachedText release];
cachedText = [text retain];
cachedTextModiff = modiff;
@@ -318,7 +423,7 @@ index 1780194..d13c5c7 100644
cachedTextStart = start;
if (visibleRuns)
@@ -7661,7 +7810,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
@@ -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
@@ -327,7 +432,7 @@ index 1780194..d13c5c7 100644
NSUInteger lo = 0, hi = visibleRunCount;
while (lo < hi)
{
@@ -7674,7 +7823,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
@@ -7674,7 +7915,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
else
{
/* Found: charpos is inside this run. Compute UTF-16 delta
@@ -336,7 +441,7 @@ index 1780194..d13c5c7 100644
NSUInteger chars_in = (NSUInteger)(charpos - r->charpos);
if (chars_in == 0 || !cachedText)
return r->ax_start;
@@ -7699,10 +7848,10 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
@@ -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
@@ -349,7 +454,7 @@ index 1780194..d13c5c7 100644
@synchronized (self)
{
if (visibleRunCount == 0)
@@ -7736,7 +7885,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
@@ -7736,7 +7977,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
return cp;
}
}
@@ -358,7 +463,7 @@ index 1780194..d13c5c7 100644
if (lo > 0)
{
ns_ax_visible_run *last = &visibleRuns[visibleRunCount - 1];
@@ -7758,7 +7907,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
@@ -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
@@ -367,7 +472,7 @@ index 1780194..d13c5c7 100644
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 +8315,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
@@ -8166,7 +8407,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
if (idx > [cachedText length])
idx = [cachedText length];
@@ -376,7 +481,7 @@ index 1780194..d13c5c7 100644
NSInteger line = 0;
NSUInteger scan = 0;
NSUInteger len = [cachedText length];
@@ -8422,7 +8571,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
@@ -8422,7 +8663,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
/* ===================================================================
@@ -385,7 +490,7 @@ index 1780194..d13c5c7 100644
These methods notify VoiceOver of text and selection changes.
Called from the redisplay cycle (postAccessibilityUpdates).
@@ -8437,7 +8586,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
@@ -8437,7 +8678,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
if (point > self.cachedPoint
&& point - self.cachedPoint == 1)
{
@@ -394,7 +499,7 @@ index 1780194..d13c5c7 100644
[self invalidateTextCache];
[self ensureTextCache];
if (cachedText)
@@ -8456,7 +8605,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
@@ -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
@@ -403,7 +508,7 @@ index 1780194..d13c5c7 100644
self.cachedPoint = point;
NSDictionary *change = @{
@@ -8789,14 +8938,112 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
@@ -8789,14 +9030,112 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
BOOL markActive = !NILP (BVAR (b, mark_active));
/* --- Text changed (edit) --- */
@@ -518,7 +623,7 @@ index 1780194..d13c5c7 100644
per the WebKit/Chromium pattern. */
else if (point != self.cachedPoint || markActive != self.cachedMarkActive)
{
@@ -8966,7 +9213,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
@@ -8966,7 +9305,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
/* ===================================================================
@@ -527,7 +632,7 @@ index 1780194..d13c5c7 100644
=================================================================== */
/* Scan visible range of window W for interactive spans.
@@ -9157,7 +9404,7 @@ ns_ax_scan_interactive_spans (struct window *w,
@@ -9157,7 +9496,7 @@ ns_ax_scan_interactive_spans (struct window *w,
- (BOOL) isAccessibilityFocused
{
/* Read the cached point stored by EmacsAccessibilityBuffer on the main
@@ -536,7 +641,7 @@ index 1780194..d13c5c7 100644
EmacsAccessibilityBuffer *pb = self.parentBuffer;
if (!pb)
return NO;
@@ -9174,7 +9421,7 @@ ns_ax_scan_interactive_spans (struct window *w,
@@ -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
@@ -545,7 +650,7 @@ index 1780194..d13c5c7 100644
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 +9447,7 @@ ns_ax_scan_interactive_spans (struct window *w,
@@ -9200,7 +9539,7 @@ ns_ax_scan_interactive_spans (struct window *w,
@end
@@ -554,7 +659,7 @@ index 1780194..d13c5c7 100644
Methods are kept here (same .m file) so they access the ivars
declared in the @interface ivar block. */
@implementation EmacsAccessibilityBuffer (InteractiveSpans)
@@ -10520,13 +10767,13 @@ ns_in_echo_area (void)
@@ -10520,13 +10859,13 @@ ns_in_echo_area (void)
if (old_title == 0)
{
char *t = strdup ([[[self window] title] UTF8String]);
@@ -570,7 +675,7 @@ index 1780194..d13c5c7 100644
[window setTitle: [NSString stringWithUTF8String: size_title]];
[window display];
xfree (size_title);
@@ -11922,7 +12169,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view,
@@ -11922,7 +12261,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view,
if (WINDOW_LEAF_P (w))
{
@@ -579,7 +684,7 @@ index 1780194..d13c5c7 100644
EmacsAccessibilityBuffer *elem
= [existing objectForKey:[NSValue valueWithPointer:w]];
if (!elem)
@@ -11956,7 +12203,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view,
@@ -11956,7 +12295,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view,
}
else
{
@@ -588,7 +693,92 @@ index 1780194..d13c5c7 100644
Lisp_Object child = w->contents;
while (!NILP (child))
{
@@ -12068,7 +12315,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view,
@@ -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
@@ -597,7 +787,7 @@ index 1780194..d13c5c7 100644
Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe);
if (!EQ (curRoot, lastRootWindow))
{
@@ -12077,12 +12324,12 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view,
@@ -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