patches: rewrite 0007 overlay support
Key changes from previous version: - Remove overlay text from ns_ax_buffer_text (was causing spurious 'new line' announcements via VoiceOver text diff) - Do NOT invalidate text cache on overlay change - Two-reference face detection (handles selected at any position) - SDATA scan instead of per-char Faref for newline detection - Zoom tracking via UAZoomChangeFocus for selected candidate row - Deduplication via cachedCompletionAnnouncement
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
From 313b4e4489a617fdd074f577ba024dec88eda87e 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
|
||||
|
||||
Completion frameworks such as Vertico, Ivy, and Icomplete render
|
||||
candidates via overlay before-string/after-string properties rather
|
||||
than buffer text. Without this, VoiceOver cannot read overlay-based
|
||||
completion UIs.
|
||||
|
||||
Add ns_ax_selected_overlay_text to extract the currently highlighted
|
||||
candidate from overlay strings. The function determines the normal
|
||||
(non-selected) face by comparing the first and last lines via Fequal,
|
||||
then returns the line with a different face (the selected candidate).
|
||||
|
||||
In the notification dispatch, detect overlay-only changes via
|
||||
BUF_OVERLAY_MODIFF (which is bumped by overlay property changes but
|
||||
not buffer text edits). Crucially, do NOT invalidate the text cache
|
||||
on overlay changes --- the buffer text is unchanged and cache
|
||||
invalidation causes VoiceOver to diff old vs new text, announcing
|
||||
spurious newlines. Instead, post SelectedTextChanged to interrupt
|
||||
current speech, then AnnouncementRequested to NSApp with the
|
||||
candidate text.
|
||||
|
||||
Add Zoom tracking for overlay candidates: find the glyph row
|
||||
corresponding to the selected candidate and call UAZoomChangeFocus
|
||||
so the Zoom lens follows the selection.
|
||||
|
||||
* src/nsterm.m (ns_ax_selected_overlay_text): New function.
|
||||
Returns the selected overlay candidate text and its line index.
|
||||
(EmacsAccessibilityBuffer postAccessibilityNotificationsForFrame:):
|
||||
Handle BUF_OVERLAY_MODIFF changes with candidate announcement and
|
||||
Zoom focus tracking.
|
||||
---
|
||||
src/nsterm.m | 240 ++++++++++++++++++++++++++++++++++++++++++++++++++-
|
||||
1 file changed, 239 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/src/nsterm.m b/src/nsterm.m
|
||||
index 1780194..4cf7b0c 100644
|
||||
--- a/src/nsterm.m
|
||||
+++ b/src/nsterm.m
|
||||
@@ -6915,11 +6915,146 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action)
|
||||
are truncated for accessibility purposes. */
|
||||
#define NS_AX_TEXT_CAP 100000
|
||||
|
||||
+/* Extract the currently selected candidate text from overlay display
|
||||
+ strings. Completion frameworks (Vertico, Ivy, Icomplete) render
|
||||
+ candidates as overlay before-string/after-string and highlight the
|
||||
+ current candidate with a distinct face (e.g. vertico-current).
|
||||
+
|
||||
+ Strategy: collect line boundaries in the overlay string, determine
|
||||
+ the "normal" (non-selected) face by comparing the first and last
|
||||
+ lines, then find the outlier line whose face differs.
|
||||
+
|
||||
+ Also return the 0-based line index of the selected candidate in
|
||||
+ *OUT_LINE_INDEX (or -1 if not found) for Zoom positioning.
|
||||
+
|
||||
+ Returns nil if no distinctly-faced line 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
|
||||
+ (avoids per-character Faref Lisp calls). */
|
||||
+ const unsigned char *data = SDATA (str);
|
||||
+ ptrdiff_t byte_len = SBYTES (str);
|
||||
+ 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++;
|
||||
+ }
|
||||
+ /* Last line (no trailing newline). */
|
||||
+ if (char_pos > lstart && nlines < 512)
|
||||
+ {
|
||||
+ line_starts[nlines] = lstart;
|
||||
+ line_ends[nlines] = char_pos;
|
||||
+ nlines++;
|
||||
+ }
|
||||
+
|
||||
+ if (nlines < 2)
|
||||
+ continue;
|
||||
+
|
||||
+ /* Determine the "normal" face using two references.
|
||||
+ If first and last line share the same face, any line
|
||||
+ that differs is the selected candidate. If they differ,
|
||||
+ compare against the second line to resolve which end
|
||||
+ is the outlier. */
|
||||
+ Lisp_Object face_first
|
||||
+ = Fget_text_property (make_fixnum (line_starts[0]),
|
||||
+ Qface, str);
|
||||
+ Lisp_Object face_last
|
||||
+ = Fget_text_property (make_fixnum (line_starts[nlines - 1]),
|
||||
+ Qface, str);
|
||||
+
|
||||
+ Lisp_Object normal_face;
|
||||
+ if (!NILP (Fequal (face_first, face_last)))
|
||||
+ {
|
||||
+ normal_face = face_first;
|
||||
+ }
|
||||
+ else if (nlines >= 3)
|
||||
+ {
|
||||
+ Lisp_Object face_second
|
||||
+ = Fget_text_property (make_fixnum (line_starts[1]),
|
||||
+ Qface, str);
|
||||
+ if (!NILP (Fequal (face_second, face_first)))
|
||||
+ normal_face = face_first;
|
||||
+ else
|
||||
+ normal_face = face_last;
|
||||
+ }
|
||||
+ else
|
||||
+ {
|
||||
+ /* Only 2 lines, different faces --- use second as normal
|
||||
+ (in most UIs, selected item is shown first). */
|
||||
+ normal_face = face_last;
|
||||
+ }
|
||||
+
|
||||
+ for (int li = 0; li < nlines; li++)
|
||||
+ {
|
||||
+ Lisp_Object lf
|
||||
+ = Fget_text_property (make_fixnum (line_starts[li]),
|
||||
+ Qface, str);
|
||||
+ if (NILP (Fequal (lf, normal_face)))
|
||||
+ {
|
||||
+ *out_line_index = li;
|
||||
+ Lisp_Object line
|
||||
+ = Fsubstring_no_properties (
|
||||
+ str,
|
||||
+ make_fixnum (line_starts[li]),
|
||||
+ make_fixnum (line_ends[li]));
|
||||
+ if (SCHARS (line) > 0)
|
||||
+ return [NSString stringWithLispString:line];
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ 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)
|
||||
@@ -8795,6 +8930,109 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
|
||||
[self postTextChangedNotification:point];
|
||||
}
|
||||
|
||||
+ /* --- Overlay content changed (e.g. Vertico/Ivy candidate switch) ---
|
||||
+ Overlay-only changes (before-string, after-string, display) bump
|
||||
+ BUF_OVERLAY_MODIFF but not BUF_MODIFF. Do NOT invalidate the
|
||||
+ text cache here --- the buffer text itself has not changed, and
|
||||
+ cache invalidation would cause VoiceOver to diff the old vs new
|
||||
+ AX text and announce spurious "new line" from newlines.
|
||||
+ Instead, announce the selected candidate explicitly. */
|
||||
+ else 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)
|
||||
+ {
|
||||
+ candidate = [candidate
|
||||
+ stringByTrimmingCharactersInSet:
|
||||
+ [NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||
+
|
||||
+ /* Deduplicate: only announce when the candidate changed. */
|
||||
+ if ([candidate length] > 0
|
||||
+ && ![candidate isEqualToString:
|
||||
+ self.cachedCompletionAnnouncement])
|
||||
+ {
|
||||
+ self.cachedCompletionAnnouncement = candidate;
|
||||
+
|
||||
+ /* Post SelectedTextChanged to interrupt VoiceOver's
|
||||
+ current speech, then announce the candidate text
|
||||
+ via NSApp (Apple requires app-level element). */
|
||||
+ NSDictionary *moveInfo = @{
|
||||
+ @"AXTextStateChangeType":
|
||||
+ @(ns_ax_text_state_change_selection_move),
|
||||
+ @"AXTextChangeElement": self
|
||||
+ };
|
||||
+ ns_ax_post_notification_with_info (
|
||||
+ self,
|
||||
+ NSAccessibilitySelectedTextChangedNotification,
|
||||
+ moveInfo);
|
||||
+
|
||||
+ NSDictionary *annInfo = @{
|
||||
+ NSAccessibilityAnnouncementKey: candidate,
|
||||
+ NSAccessibilityPriorityKey:
|
||||
+ @(NSAccessibilityPriorityHigh)
|
||||
+ };
|
||||
+ ns_ax_post_notification_with_info (
|
||||
+ NSApp,
|
||||
+ NSAccessibilityAnnouncementRequestedNotification,
|
||||
+ annInfo);
|
||||
+
|
||||
+ /* --- Zoom tracking for overlay candidates ---
|
||||
+ The overlay candidates appear as visual lines in the
|
||||
+ minibuffer window. Row 0 is the input line; overlay
|
||||
+ candidates start from row 1 onward. Find the glyph
|
||||
+ row for the selected candidate and focus Zoom there. */
|
||||
+#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \
|
||||
+ && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
|
||||
+ if (selected_line >= 0 && UAZoomEnabled ())
|
||||
+ {
|
||||
+ struct window *w2 = [self validWindow];
|
||||
+ if (w2 && w2->current_matrix)
|
||||
+ {
|
||||
+ EmacsView *view = self.emacsView;
|
||||
+ int target_vrow = selected_line + 1;
|
||||
+ int nrows = w2->current_matrix->nrows;
|
||||
+ if (target_vrow < nrows)
|
||||
+ {
|
||||
+ struct glyph_row *row
|
||||
+ = w2->current_matrix->rows + target_vrow;
|
||||
+ if (row->enabled_p
|
||||
+ && row->visible_height > 0)
|
||||
+ {
|
||||
+ NSRect r = NSMakeRect (
|
||||
+ w2->pixel_left,
|
||||
+ WINDOW_TOP_EDGE_Y (w2) + row->y,
|
||||
+ w2->pixel_width,
|
||||
+ row->visible_height);
|
||||
+ NSRect winRect
|
||||
+ = [view convertRect:r toView:nil];
|
||||
+ NSRect screenRect
|
||||
+ = [[view 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);
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+#endif
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
/* --- Cursor moved or selection changed ---
|
||||
Use 'else if' — edits and selection moves are mutually exclusive
|
||||
per the WebKit/Chromium pattern. */
|
||||
--
|
||||
2.43.0
|
||||
|
||||
@@ -1,267 +0,0 @@
|
||||
From e12982f4ac111f9814a198763124a64540f4640b 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: include overlay display strings in accessibility text
|
||||
|
||||
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 any
|
||||
overlay-based completion UI.
|
||||
|
||||
This patch adds three pieces:
|
||||
|
||||
1. ns_ax_selected_overlay_text: extract the currently highlighted
|
||||
candidate from overlay strings. Compare each line's face against
|
||||
the first line's face via Fequal; the line with a different face
|
||||
(e.g. vertico-current) is the selected candidate.
|
||||
|
||||
2. ns_ax_buffer_text: append overlay before-string and after-string
|
||||
content after the buffer text, with virtual visible-run entries
|
||||
anchored at the overlay's buffer position.
|
||||
|
||||
3. Notification dispatch: detect overlay-only changes via
|
||||
BUF_OVERLAY_MODIFF. Post SelectedTextChanged to interrupt
|
||||
VoiceOver, then AnnouncementRequested (to NSApp) with the
|
||||
candidate text.
|
||||
|
||||
* src/nsterm.m (ns_ax_selected_overlay_text): New function.
|
||||
(ns_ax_buffer_text): Append overlay display strings.
|
||||
(EmacsAccessibilityBuffer postAccessibilityNotificationsForFrame:):
|
||||
Handle BUF_OVERLAY_MODIFF changes with candidate announcement.
|
||||
---
|
||||
src/nsterm.m | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++-
|
||||
1 file changed, 200 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/src/nsterm.m b/src/nsterm.m
|
||||
index 1780194..35edd39 100644
|
||||
--- a/src/nsterm.m
|
||||
+++ b/src/nsterm.m
|
||||
@@ -6915,11 +6915,99 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action)
|
||||
are truncated for accessibility purposes. */
|
||||
#define NS_AX_TEXT_CAP 100000
|
||||
|
||||
+/* Extract the currently selected candidate text from overlay display
|
||||
+ strings. Completion frameworks (Vertico, Ivy, Icomplete) highlight
|
||||
+ the current candidate with a distinct face. We find the line whose
|
||||
+ face DIFFERS from the first line's face (the "normal" candidate
|
||||
+ face) — that is the selected candidate.
|
||||
+
|
||||
+ Returns nil if no distinctly-faced line is found. */
|
||||
+static NSString *
|
||||
+ns_ax_selected_overlay_text (struct buffer *b,
|
||||
+ ptrdiff_t beg, ptrdiff_t end)
|
||||
+{
|
||||
+ 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;
|
||||
+
|
||||
+ /* Collect line boundaries. */
|
||||
+ ptrdiff_t line_starts[512];
|
||||
+ ptrdiff_t line_ends[512];
|
||||
+ int nlines = 0;
|
||||
+ ptrdiff_t lstart = 0;
|
||||
+
|
||||
+ for (ptrdiff_t i = 0; i <= slen && nlines < 512; i++)
|
||||
+ {
|
||||
+ bool is_nl = false;
|
||||
+ if (i < slen)
|
||||
+ {
|
||||
+ Lisp_Object ch = Faref (str, make_fixnum (i));
|
||||
+ is_nl = (FIXNUMP (ch) && XFIXNUM (ch) == '\n');
|
||||
+ }
|
||||
+ if (is_nl || i == slen)
|
||||
+ {
|
||||
+ if (i > lstart)
|
||||
+ {
|
||||
+ line_starts[nlines] = lstart;
|
||||
+ line_ends[nlines] = i;
|
||||
+ nlines++;
|
||||
+ }
|
||||
+ lstart = i + 1;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ if (nlines < 2)
|
||||
+ continue;
|
||||
+
|
||||
+ /* Get the face of the first line (the "normal" face). */
|
||||
+ Lisp_Object normal_face
|
||||
+ = Fget_text_property (make_fixnum (line_starts[0]),
|
||||
+ Qface, str);
|
||||
+
|
||||
+ /* Find the first line with a DIFFERENT face. */
|
||||
+ for (int li = 0; li < nlines; li++)
|
||||
+ {
|
||||
+ Lisp_Object line_face
|
||||
+ = Fget_text_property (make_fixnum (line_starts[li]),
|
||||
+ Qface, str);
|
||||
+ if (NILP (Fequal (line_face, normal_face)))
|
||||
+ {
|
||||
+ Lisp_Object line
|
||||
+ = Fsubstring_no_properties (
|
||||
+ str,
|
||||
+ make_fixnum (line_starts[li]),
|
||||
+ make_fixnum (line_ends[li]));
|
||||
+ if (SCHARS (line) > 0)
|
||||
+ return [NSString stringWithLispString:line];
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ 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)
|
||||
@@ -7021,6 +7109,68 @@ ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start,
|
||||
pos = run_end;
|
||||
}
|
||||
|
||||
+ /* Append overlay display strings (before-string, after-string).
|
||||
+ Many completion frameworks (Vertico, Ivy, Icomplete) render
|
||||
+ candidates via overlay properties rather than buffer text.
|
||||
+ Without this, VoiceOver cannot read overlay-based content.
|
||||
+
|
||||
+ We append overlay strings after the buffer text and give each
|
||||
+ a virtual run mapped to the overlay's buffer position, so cursor
|
||||
+ tracking and index mapping remain functional. */
|
||||
+ {
|
||||
+ Lisp_Object ov_list = Foverlays_in (make_fixnum (begv),
|
||||
+ make_fixnum (zv));
|
||||
+ for (Lisp_Object tail = ov_list; CONSP (tail); tail = XCDR (tail))
|
||||
+ {
|
||||
+ Lisp_Object ov = XCAR (tail);
|
||||
+ Lisp_Object props[2];
|
||||
+ ptrdiff_t anchors[2];
|
||||
+
|
||||
+ props[0] = Foverlay_get (ov, intern_c_string ("before-string"));
|
||||
+ anchors[0] = XFIXNUM (Foverlay_start (ov));
|
||||
+ props[1] = Foverlay_get (ov, intern_c_string ("after-string"));
|
||||
+ anchors[1] = XFIXNUM (Foverlay_end (ov));
|
||||
+
|
||||
+ for (int k = 0; k < 2; k++)
|
||||
+ {
|
||||
+ if (!STRINGP (props[k]))
|
||||
+ continue;
|
||||
+
|
||||
+ /* Cap total text. */
|
||||
+ if (ax_offset >= NS_AX_TEXT_CAP)
|
||||
+ break;
|
||||
+
|
||||
+ NSString *nsstr
|
||||
+ = [NSString stringWithLispString:props[k]];
|
||||
+ NSUInteger ns_len = [nsstr length];
|
||||
+ if (ns_len == 0)
|
||||
+ continue;
|
||||
+
|
||||
+ if (ax_offset + ns_len > NS_AX_TEXT_CAP)
|
||||
+ ns_len = NS_AX_TEXT_CAP - ax_offset;
|
||||
+
|
||||
+ if (ns_len < [nsstr length])
|
||||
+ nsstr = [nsstr substringToIndex:ns_len];
|
||||
+
|
||||
+ [result appendString:nsstr];
|
||||
+
|
||||
+ if (nruns >= run_capacity)
|
||||
+ {
|
||||
+ run_capacity *= 2;
|
||||
+ runs = xrealloc (runs, run_capacity
|
||||
+ * sizeof (ns_ax_visible_run));
|
||||
+ }
|
||||
+ runs[nruns].charpos = anchors[k];
|
||||
+ runs[nruns].length = 0; /* virtual — no buffer chars */
|
||||
+ runs[nruns].ax_start = ax_offset;
|
||||
+ runs[nruns].ax_length = ns_len;
|
||||
+ nruns++;
|
||||
+
|
||||
+ ax_offset += ns_len;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
unbind_to (count, Qnil);
|
||||
|
||||
*out_runs = runs;
|
||||
@@ -8795,6 +8945,55 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
|
||||
[self postTextChangedNotification:point];
|
||||
}
|
||||
|
||||
+ /* --- Overlay content changed (e.g. Vertico/Ivy candidate switch) ---
|
||||
+ Overlay-only changes (before-string, after-string, display) bump
|
||||
+ BUF_OVERLAY_MODIFF but not BUF_MODIFF. Instead of a generic
|
||||
+ ValueChanged (which causes VoiceOver to say "new line"), find the
|
||||
+ highlighted candidate in the overlay and announce it directly. */
|
||||
+ else if (BUF_OVERLAY_MODIFF (b) != self.cachedOverlayModiff)
|
||||
+ {
|
||||
+ self.cachedOverlayModiff = BUF_OVERLAY_MODIFF (b);
|
||||
+ [self invalidateTextCache];
|
||||
+
|
||||
+ /* Find the selected candidate text from overlay face
|
||||
+ properties. Completion frameworks highlight the current
|
||||
+ candidate with a text face (e.g. vertico-current,
|
||||
+ icomplete-selected-match). */
|
||||
+ NSString *candidate
|
||||
+ = ns_ax_selected_overlay_text (b, BUF_BEGV (b), BUF_ZV (b));
|
||||
+ if (candidate)
|
||||
+ {
|
||||
+ /* Post SelectedTextChanged first to interrupt VoiceOver,
|
||||
+ then AnnouncementRequested with candidate text.
|
||||
+ Target NSApp for announcements (Apple docs require it). */
|
||||
+ NSDictionary *moveInfo = @{
|
||||
+ @"AXTextStateChangeType":
|
||||
+ @(ns_ax_text_state_change_selection_move),
|
||||
+ @"AXTextChangeElement": self
|
||||
+ };
|
||||
+ ns_ax_post_notification_with_info (
|
||||
+ self,
|
||||
+ NSAccessibilitySelectedTextChangedNotification,
|
||||
+ moveInfo);
|
||||
+
|
||||
+ candidate = [candidate
|
||||
+ stringByTrimmingCharactersInSet:
|
||||
+ [NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||
+ if ([candidate length] > 0)
|
||||
+ {
|
||||
+ NSDictionary *annInfo = @{
|
||||
+ NSAccessibilityAnnouncementKey: candidate,
|
||||
+ NSAccessibilityPriorityKey:
|
||||
+ @(NSAccessibilityPriorityHigh)
|
||||
+ };
|
||||
+ ns_ax_post_notification_with_info (
|
||||
+ NSApp,
|
||||
+ NSAccessibilityAnnouncementRequestedNotification,
|
||||
+ annInfo);
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
/* --- Cursor moved or selection changed ---
|
||||
Use 'else if' — edits and selection moves are mutually exclusive
|
||||
per the WebKit/Chromium pattern. */
|
||||
--
|
||||
2.43.0
|
||||
|
||||
Reference in New Issue
Block a user