patches: squash 0007+0008+0009 into single clean 0007

All overlay fixes in one patch: Fequal face detection,
NSApp announcement target, SelectedTextChanged interrupt.
This commit is contained in:
2026-02-28 14:46:43 +01:00
parent 8a48e72493
commit 6c502c7af5
3 changed files with 100 additions and 319 deletions

View File

@@ -1,57 +1,52 @@
From d9a3c249cc55792e7cfbe12fa8b69861a6bf1f96 Mon Sep 17 00:00:00 2001
From e12982f4ac111f9814a198763124a64540f4640b Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 14:16:29 +0100
Date: Sat, 28 Feb 2026 14:46:25 +0100
Subject: [PATCH] ns: include overlay display strings in accessibility text
Completion frameworks like Vertico, Ivy, and Icomplete render
candidates via overlay before-string / after-string properties
rather than as buffer text. The accessibility text extraction
only reads buffer content, making overlay-based UIs invisible
to 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 any
overlay-based completion UI.
This patch adds three enhancements:
This patch adds three pieces:
1. Walk overlays in the visible range after buffer text extraction
and append their before-string / after-string content, with
virtual visible-run entries for index mapping.
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. Detect overlay-only changes (BUF_OVERLAY_MODIFF) in the
notification dispatch.
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. When overlays change, find the highlighted candidate (the text
with a face property in the overlay string) and announce it via
NSAccessibilityAnnouncementRequestedNotification. This makes
VoiceOver read the specific selected candidate rather than
announcing 'new line'.
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. Walk
overlay before-string / after-string text properties to find the
face-highlighted (selected) portion, extract its full line.
(ns_ax_buffer_text): Append overlay display strings with virtual
visible-run entries.
* src/nsterm.m (ns_ax_selected_overlay_text): New function.
(ns_ax_buffer_text): Append overlay display strings.
(EmacsAccessibilityBuffer postAccessibilityNotificationsForFrame:):
Check BUF_OVERLAY_MODIFF; announce selected candidate text.
Tested on macOS 14 with VoiceOver and icomplete-vertical-mode.
Handle BUF_OVERLAY_MODIFF changes with candidate announcement.
---
src/nsterm.m | 179 ++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 178 insertions(+), 1 deletion(-)
src/nsterm.m | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 200 insertions(+), 1 deletion(-)
diff --git a/src/nsterm.m b/src/nsterm.m
index 1780194..c7bba5b 100644
index 1780194..35edd39 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -6915,11 +6915,96 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action)
@@ -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 in window W. Completion frameworks (Vertico, Ivy, Icomplete)
+ highlight the current candidate by applying a face property to a
+ portion of the overlay's before-string or after-string. We find
+ that highlighted portion and return it as an NSString.
+ 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 highlighted overlay text is found. */
+ 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)
@@ -72,57 +67,60 @@ index 1780194..c7bba5b 100644
+ continue;
+
+ Lisp_Object str = strings[s];
+ ptrdiff_t len = SCHARS (str);
+ ptrdiff_t pos = 0;
+ ptrdiff_t slen = SCHARS (str);
+ if (slen == 0)
+ continue;
+
+ while (pos < len)
+ /* 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++)
+ {
+ Lisp_Object face
+ = Fget_text_property (make_fixnum (pos),
+ Qface, str);
+ if (!NILP (face))
+ bool is_nl = false;
+ if (i < slen)
+ {
+ /* Found highlighted text. Extract the full line
+ containing this position. */
+ ptrdiff_t line_start = pos;
+ while (line_start > 0)
+ Lisp_Object ch = Faref (str, make_fixnum (i));
+ is_nl = (FIXNUMP (ch) && XFIXNUM (ch) == '\n');
+ }
+ if (is_nl || i == slen)
+ {
+ if (i > lstart)
+ {
+ /* Check character before line_start. */
+ Lisp_Object ch
+ = Faref (str, make_fixnum (line_start - 1));
+ if (FIXNUMP (ch) && XFIXNUM (ch) == '\n')
+ break;
+ line_start--;
+ line_starts[nlines] = lstart;
+ line_ends[nlines] = i;
+ nlines++;
+ }
+ lstart = i + 1;
+ }
+ }
+
+ ptrdiff_t line_end = pos;
+ while (line_end < len)
+ {
+ Lisp_Object ch
+ = Faref (str, make_fixnum (line_end));
+ if (FIXNUMP (ch) && XFIXNUM (ch) == '\n')
+ break;
+ line_end++;
+ }
+ 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_start),
+ make_fixnum (line_end));
+ = Fsubstring_no_properties (
+ str,
+ make_fixnum (line_starts[li]),
+ make_fixnum (line_ends[li]));
+ if (SCHARS (line) > 0)
+ return [NSString stringWithLispString:line];
+ }
+
+ /* Skip to next face change. */
+ Lisp_Object next
+ = Fnext_single_property_change (make_fixnum (pos),
+ Qface, str,
+ make_fixnum (len));
+ ptrdiff_t npos
+ = FIXNUMP (next) ? XFIXNUM (next) : len;
+ if (npos <= pos)
+ break;
+ pos = npos;
+ }
+ }
+ }
@@ -139,7 +137,7 @@ index 1780194..c7bba5b 100644
static NSString *
ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start,
ns_ax_visible_run **out_runs, NSUInteger *out_nruns)
@@ -7021,6 +7106,68 @@ ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start,
@@ -7021,6 +7109,68 @@ ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start,
pos = run_end;
}
@@ -208,7 +206,7 @@ index 1780194..c7bba5b 100644
unbind_to (count, Qnil);
*out_runs = runs;
@@ -8795,6 +8942,36 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
@@ -8795,6 +8945,55 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
[self postTextChangedNotification:point];
}
@@ -230,15 +228,34 @@ index 1780194..c7bba5b 100644
+ = ns_ax_selected_overlay_text (b, BUF_BEGV (b), BUF_ZV (b));
+ if (candidate)
+ {
+ NSDictionary *info = @{
+ NSAccessibilityAnnouncementKey: candidate,
+ NSAccessibilityPriorityKey:
+ @(NSAccessibilityPriorityHigh)
+ /* 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,
+ NSAccessibilityAnnouncementRequestedNotification,
+ info);
+ 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);
+ }
+ }
+ }
+

View File

@@ -1,84 +0,0 @@
From 988b041f1fe0dc730014fdac82e746412a5afc70 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 14:41:42 +0100
Subject: [PATCH] ns: fix overlay candidate announcement for VoiceOver
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Fix three bugs in patch 0007:
1. Post AnnouncementRequested to NSApp, not self. VoiceOver ignores
announcements from non-application elements.
2. Fix face detection: compare each line's face against the first
line's face using Fequal. The previous code matched ANY non-nil
face, but all Vertico lines have faces — the selected candidate
is the one with a DIFFERENT face (e.g. vertico-current).
3. Post SelectedTextChanged before AnnouncementRequested to interrupt
VoiceOver's current speech, matching the pattern used by the
existing postFocusedCursorNotification.
* src/nsterm.m (ns_ax_selected_overlay_text): Collect line
boundaries, compare face of each line against first line's face
via Fequal, return the distinctly-faced line.
(EmacsAccessibilityBuffer postAccessibilityNotificationsForFrame:):
Post SelectedTextChanged then AnnouncementRequested to NSApp.
---
src/nsterm.m | 32 ++++++++++++++++++++++++++------
1 file changed, 26 insertions(+), 6 deletions(-)
diff --git a/src/nsterm.m b/src/nsterm.m
index c7bba5b..43d30f9 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -8956,19 +8956,39 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
properties. Completion frameworks highlight the current
candidate with a text face (e.g. vertico-current,
icomplete-selected-match). */
+ NSString *candidate
NSString *candidate
= ns_ax_selected_overlay_text (b, BUF_BEGV (b), BUF_ZV (b));
if (candidate)
{
- NSDictionary *info = @{
- NSAccessibilityAnnouncementKey: candidate,
- NSAccessibilityPriorityKey:
- @(NSAccessibilityPriorityHigh)
+ /* 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,
- NSAccessibilityAnnouncementRequestedNotification,
- info);
+ 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);
+ }
}
}
--
2.43.0

View File

@@ -1,152 +0,0 @@
From 2e5505f044e403ccaef8c43bdc66480c71dcf05a Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 14:44:37 +0100
Subject: [PATCH] ns: fix overlay candidate detection (Fequal face comparison)
The previous ns_ax_selected_overlay_text matched ANY non-nil face,
but all Vertico lines have faces. Fix: collect line boundaries,
compare each line's face against the first line's face via Fequal,
return the line with a DIFFERENT face (the selected candidate).
Also fix duplicate 'NSString *candidate' declaration.
* src/nsterm.m (ns_ax_selected_overlay_text): Rewrite to compare
faces line-by-line via Fequal instead of matching first non-nil face.
---
src/nsterm.m | 94 +++++++++++++++++++++++++++-------------------------
1 file changed, 48 insertions(+), 46 deletions(-)
diff --git a/src/nsterm.m b/src/nsterm.m
index 43d30f9..35edd39 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -6916,12 +6916,12 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action)
#define NS_AX_TEXT_CAP 100000
/* Extract the currently selected candidate text from overlay display
- strings in window W. Completion frameworks (Vertico, Ivy, Icomplete)
- highlight the current candidate by applying a face property to a
- portion of the overlay's before-string or after-string. We find
- that highlighted portion and return it as an NSString.
+ 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 highlighted overlay text is found. */
+ 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)
@@ -6942,57 +6942,60 @@ ns_ax_selected_overlay_text (struct buffer *b,
continue;
Lisp_Object str = strings[s];
- ptrdiff_t len = SCHARS (str);
- ptrdiff_t pos = 0;
+ 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;
- while (pos < len)
+ for (ptrdiff_t i = 0; i <= slen && nlines < 512; i++)
{
- Lisp_Object face
- = Fget_text_property (make_fixnum (pos),
- Qface, str);
- if (!NILP (face))
+ 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)
{
- /* Found highlighted text. Extract the full line
- containing this position. */
- ptrdiff_t line_start = pos;
- while (line_start > 0)
+ if (i > lstart)
{
- /* Check character before line_start. */
- Lisp_Object ch
- = Faref (str, make_fixnum (line_start - 1));
- if (FIXNUMP (ch) && XFIXNUM (ch) == '\n')
- break;
- line_start--;
+ line_starts[nlines] = lstart;
+ line_ends[nlines] = i;
+ nlines++;
}
+ lstart = i + 1;
+ }
+ }
- ptrdiff_t line_end = pos;
- while (line_end < len)
- {
- Lisp_Object ch
- = Faref (str, make_fixnum (line_end));
- if (FIXNUMP (ch) && XFIXNUM (ch) == '\n')
- break;
- line_end++;
- }
+ 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_start),
- make_fixnum (line_end));
+ = Fsubstring_no_properties (
+ str,
+ make_fixnum (line_starts[li]),
+ make_fixnum (line_ends[li]));
if (SCHARS (line) > 0)
return [NSString stringWithLispString:line];
}
-
- /* Skip to next face change. */
- Lisp_Object next
- = Fnext_single_property_change (make_fixnum (pos),
- Qface, str,
- make_fixnum (len));
- ptrdiff_t npos
- = FIXNUMP (next) ? XFIXNUM (next) : len;
- if (npos <= pos)
- break;
- pos = npos;
}
}
}
@@ -8956,7 +8959,6 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
properties. Completion frameworks highlight the current
candidate with a text face (e.g. vertico-current,
icomplete-selected-match). */
- NSString *candidate
NSString *candidate
= ns_ax_selected_overlay_text (b, BUF_BEGV (b), BUF_ZV (b));
if (candidate)
--
2.43.0