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:
@@ -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),
|
||||
+ 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);
|
||||
+ if (!NILP (face))
|
||||
+ {
|
||||
+ /* Found highlighted text. Extract the full line
|
||||
+ containing this position. */
|
||||
+ ptrdiff_t line_start = pos;
|
||||
+ while (line_start > 0)
|
||||
+ {
|
||||
+ /* Check character before line_start. */
|
||||
+ Lisp_Object ch
|
||||
+ = Faref (str, make_fixnum (line_start - 1));
|
||||
+ if (FIXNUMP (ch) && XFIXNUM (ch) == '\n')
|
||||
+ break;
|
||||
+ line_start--;
|
||||
+ }
|
||||
+
|
||||
+ ptrdiff_t line_end = pos;
|
||||
+ while (line_end < len)
|
||||
+ /* 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 ch
|
||||
+ = Faref (str, make_fixnum (line_end));
|
||||
+ if (FIXNUMP (ch) && XFIXNUM (ch) == '\n')
|
||||
+ break;
|
||||
+ line_end++;
|
||||
+ }
|
||||
+
|
||||
+ 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 = @{
|
||||
+ /* 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 (
|
||||
+ self,
|
||||
+ NSApp,
|
||||
+ NSAccessibilityAnnouncementRequestedNotification,
|
||||
+ info);
|
||||
+ annInfo);
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user