patches: maintainer review R2 fixes — all must-fix items resolved
- unwind-protect in ns_ax_utf16_length_for_buffer_range - unwind-protect in ns_ax_completion_text_for_span - unwind-protect in postAccessibilityNotificationsForFrame - NSTRACE added to all 4 key functions (3 were missing) - O(n) mouse-face scan → Fprevious/Fnext_single_char_property_change - etc/NEWS entry added to patch - Main-thread invariant comment in ensureTextCache
This commit is contained in:
@@ -1,39 +1,64 @@
|
||||
From 900c20da8271f503ed3c224795a001dfca8b92f1 Mon Sep 17 00:00:00 2001
|
||||
From 2daab9a7e1018505a93bf72591a0d695352648d7 Mon Sep 17 00:00:00 2001
|
||||
From: Martin Sukany <martin@sukany.cz>
|
||||
Date: Fri, 27 Feb 2026 14:29:32 +0100
|
||||
Date: Fri, 27 Feb 2026 14:34:58 +0100
|
||||
Subject: [PATCH] ns: implement VoiceOver accessibility (AXBoundsForRange, line
|
||||
nav, completions, interactive spans)
|
||||
|
||||
* src/nsterm.h (EmacsAccessibilityElement): New base class for virtual
|
||||
accessibility elements.
|
||||
(EmacsAccessibilityBuffer): AXTextArea/AXTextField per Emacs window with
|
||||
cached text, visible runs, and interactive span support.
|
||||
(EmacsAccessibilityModeLine): AXStaticText exposing mode line content.
|
||||
(EmacsAccessibilityInteractiveSpan): Focusable element for buttons,
|
||||
links, completion candidates, and keymap overlays.
|
||||
(ns_ax_visible_run): Maps contiguous visible buffer ranges to AX string
|
||||
indices, skipping invisible text.
|
||||
* src/nsterm.h (EmacsAccessibilityElement): New base class.
|
||||
(EmacsAccessibilityBuffer): AXTextArea/AXTextField per window.
|
||||
(EmacsAccessibilityModeLine): AXStaticText for mode line.
|
||||
(EmacsAccessibilityInteractiveSpan): Focusable spans.
|
||||
(ns_ax_visible_run): Visible buffer range to AX index mapping.
|
||||
|
||||
* src/nsterm.m (ns_ax_buffer_text): Build accessibility string with
|
||||
* src/nsterm.m (ns_ax_buffer_text): Build AX string with
|
||||
record_unwind_current_buffer for exception safety.
|
||||
(ns_ax_utf16_length_for_buffer_range): UTF-16 length with
|
||||
record_unwind_current_buffer.
|
||||
(ns_ax_completion_text_for_span): Completion text extraction with
|
||||
record_unwind_current_buffer.
|
||||
(ns_ax_find_completion_overlay_range): Fast 3-probe + Foverlays_in
|
||||
bulk fallback.
|
||||
(ns_ax_event_is_line_nav_key): Detect C-n/C-p/Tab/backtab.
|
||||
(ns_ax_scan_interactive_spans): Forward scan for interactive properties.
|
||||
(ns_ax_find_completion_overlay_range): Fast 3-probe + Foverlays_in bulk
|
||||
fallback (no per-character scan).
|
||||
(postAccessibilityUpdates): Re-entrance-guarded dispatcher with NSTRACE.
|
||||
(postAccessibilityNotificationsForFrame:): Hybrid notification strategy.
|
||||
(invalidateTextCache, ensureTextCache): @synchronized for thread safety
|
||||
of visibleRuns/cachedText accessed from AX server thread.
|
||||
(ns_ax_scan_interactive_spans): Forward scan for interactive props.
|
||||
(postAccessibilityUpdates): Re-entrance-guarded dispatcher.
|
||||
(rebuildAccessibilityTree): Build virtual AX element tree.
|
||||
(postAccessibilityNotificationsForFrame:): Hybrid notification
|
||||
strategy with record_unwind_current_buffer.
|
||||
(invalidateTextCache, ensureTextCache): @synchronized for
|
||||
thread safety of visibleRuns/cachedText.
|
||||
(accessibilityIndexForCharpos:, charposForAccessibilityIndex:):
|
||||
Thread-safe index mapping with @synchronized.
|
||||
(ns_draw_phys_cursor): UAZoomChangeFocus with MAC_OS_X_VERSION guard.
|
||||
(ns_draw_phys_cursor): UAZoomChangeFocus with version guard.
|
||||
|
||||
* etc/NEWS: New feature: VoiceOver accessibility on macOS.
|
||||
* etc/NEWS: Announce VoiceOver accessibility on macOS.
|
||||
---
|
||||
etc/NEWS | 11 +
|
||||
src/nsterm.h | 108 ++
|
||||
src/nsterm.m | 2727 +++++++++++++++++++++++++++++++++++++++++++++++---
|
||||
2 files changed, 2693 insertions(+), 142 deletions(-)
|
||||
3 files changed, 2704 insertions(+), 142 deletions(-)
|
||||
|
||||
diff --git a/etc/NEWS b/etc/NEWS
|
||||
index 7367e3c..0e4480a 100644
|
||||
--- a/etc/NEWS
|
||||
+++ b/etc/NEWS
|
||||
@@ -4374,6 +4374,17 @@ allowing Emacs users access to speech recognition utilities.
|
||||
Note: Accepting this permission allows the use of system APIs, which may
|
||||
send user data to Apple's speech recognition servers.
|
||||
|
||||
+---
|
||||
+** VoiceOver accessibility support on macOS.
|
||||
+Emacs now exposes buffer content, cursor position, and interactive
|
||||
+elements to the macOS accessibility subsystem (VoiceOver). This
|
||||
+includes AXBoundsForRange for macOS Zoom cursor tracking, line and
|
||||
+word navigation announcements, Tab-navigable interactive spans
|
||||
+(buttons, links, completion candidates), and completion announcements
|
||||
+for the *Completions* buffer. The implementation uses a virtual
|
||||
+accessibility tree with per-window elements, hybrid SelectedTextChanged
|
||||
+and AnnouncementRequested notifications, and thread-safe text caching.
|
||||
+
|
||||
---
|
||||
** Re-introduced dictation, lost in Emacs v30 (macOS).
|
||||
We lost macOS dictation in v30 when migrating to NSTextInputClient.
|
||||
diff --git a/src/nsterm.h b/src/nsterm.h
|
||||
index 7c1ee4c..6455547 100644
|
||||
--- a/src/nsterm.h
|
||||
@@ -168,7 +193,7 @@ index 7c1ee4c..6455547 100644
|
||||
|
||||
|
||||
diff --git a/src/nsterm.m b/src/nsterm.m
|
||||
index 932d209..248511b 100644
|
||||
index 932d209..fb5af6a 100644
|
||||
--- a/src/nsterm.m
|
||||
+++ b/src/nsterm.m
|
||||
@@ -46,6 +46,7 @@ Updated by Christian Limpach (chris@nice.ch)
|
||||
@@ -566,7 +591,8 @@ index 932d209..248511b 100644
|
||||
- if (font_panel_result)
|
||||
- [font_panel_result release];
|
||||
- font_panel_result = nil;
|
||||
+ struct buffer *oldb = current_buffer;
|
||||
+ specpdl_ref count = SPECPDL_INDEX ();
|
||||
+ record_unwind_current_buffer ();
|
||||
+ if (b != current_buffer)
|
||||
+ set_buffer_internal_1 (b);
|
||||
|
||||
@@ -576,9 +602,7 @@ index 932d209..248511b 100644
|
||||
+ NSString *nsstr = [NSString stringWithLispString:lstr];
|
||||
+ NSUInteger len = [nsstr length];
|
||||
+
|
||||
+ if (b != oldb)
|
||||
+ set_buffer_internal_1 (oldb);
|
||||
+
|
||||
+ unbind_to (count, Qnil);
|
||||
+ return len;
|
||||
}
|
||||
-#endif
|
||||
@@ -1127,7 +1151,8 @@ index 932d209..248511b 100644
|
||||
+ return nil;
|
||||
+
|
||||
+ NSString *text = nil;
|
||||
+ struct buffer *oldb = current_buffer;
|
||||
+ specpdl_ref count = SPECPDL_INDEX ();
|
||||
+ record_unwind_current_buffer ();
|
||||
+ if (b != current_buffer)
|
||||
+ set_buffer_internal_1 (b);
|
||||
+
|
||||
@@ -1162,8 +1187,7 @@ index 932d209..248511b 100644
|
||||
+ text = [cachedText substringWithRange:NSMakeRange (ax_s, ax_e - ax_s)];
|
||||
+ }
|
||||
+
|
||||
+ if (b != oldb)
|
||||
+ set_buffer_internal_1 (oldb);
|
||||
+ unbind_to (count, Qnil);
|
||||
+
|
||||
+ if (text)
|
||||
+ {
|
||||
@@ -1276,6 +1300,11 @@ index 932d209..248511b 100644
|
||||
+- (void)ensureTextCache
|
||||
+{
|
||||
+ 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
|
||||
+ write section at the end needs synchronization to protect
|
||||
+ against concurrent reads from AX server thread. */
|
||||
+ struct window *w = [self validWindow];
|
||||
+ if (!w || !WINDOW_LEAF_P (w))
|
||||
+ return;
|
||||
@@ -2175,7 +2204,8 @@ index 932d209..248511b 100644
|
||||
+ ptrdiff_t currentOverlayStart = 0;
|
||||
+ ptrdiff_t currentOverlayEnd = 0;
|
||||
+
|
||||
+ struct buffer *oldb2 = current_buffer;
|
||||
+ specpdl_ref count2 = SPECPDL_INDEX ();
|
||||
+ record_unwind_current_buffer ();
|
||||
+ if (b != current_buffer)
|
||||
+ set_buffer_internal_1 (b);
|
||||
+
|
||||
@@ -2198,28 +2228,24 @@ index 932d209..248511b 100644
|
||||
+ {
|
||||
+ ptrdiff_t begv2 = BUF_BEGV (b);
|
||||
+ ptrdiff_t zv2 = BUF_ZV (b);
|
||||
+ ptrdiff_t s2 = point;
|
||||
+ ptrdiff_t e2 = point;
|
||||
+
|
||||
+ while (s2 > begv2)
|
||||
+ {
|
||||
+ Lisp_Object prev = Fget_char_property (
|
||||
+ make_fixnum (s2 - 1), Qmouse_face, Qnil);
|
||||
+ if (!NILP (Fequal (prev, mf)))
|
||||
+ s2--;
|
||||
+ else
|
||||
+ break;
|
||||
+ }
|
||||
+ /* Find mouse-face span boundaries using property
|
||||
+ change functions — O(log n) instead of O(n). */
|
||||
+ Lisp_Object prev_change
|
||||
+ = Fprevious_single_char_property_change (
|
||||
+ make_fixnum (point + 1), Qmouse_face,
|
||||
+ Qnil, make_fixnum (begv2));
|
||||
+ ptrdiff_t s2
|
||||
+ = FIXNUMP (prev_change) ? XFIXNUM (prev_change)
|
||||
+ : begv2;
|
||||
+
|
||||
+ while (e2 < zv2)
|
||||
+ {
|
||||
+ Lisp_Object cur = Fget_char_property (
|
||||
+ make_fixnum (e2), Qmouse_face, Qnil);
|
||||
+ if (!NILP (Fequal (cur, mf)))
|
||||
+ e2++;
|
||||
+ else
|
||||
+ break;
|
||||
+ }
|
||||
+ Lisp_Object next_change
|
||||
+ = Fnext_single_char_property_change (
|
||||
+ make_fixnum (point), Qmouse_face,
|
||||
+ Qnil, make_fixnum (zv2));
|
||||
+ ptrdiff_t e2
|
||||
+ = FIXNUMP (next_change) ? XFIXNUM (next_change)
|
||||
+ : zv2;
|
||||
+
|
||||
+ if (e2 > s2)
|
||||
+ {
|
||||
@@ -2279,8 +2305,7 @@ index 932d209..248511b 100644
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ if (b != oldb2)
|
||||
+ set_buffer_internal_1 (oldb2);
|
||||
+ unbind_to (count2, Qnil);
|
||||
+
|
||||
+ /* Final fallback: read the current line at point. */
|
||||
+ if (!announceText)
|
||||
|
||||
Reference in New Issue
Block a user