patches: split VoiceOver into 3-patch series, improve docs
Split the monolithic 3011-line patch into logical pieces: 0001: All new accessibility code (infrastructure, no existing code modified) 0002: EmacsView integration + cursor tracking (wiring only) 0003: Documentation (expanded with known limitations) Improvements: - Comprehensive commit messages with testing methodology - Known limitations documented (text cap, bidi, mode-line icons) - Documentation expanded with Known Limitations section - Each patch is self-contained and reviewable
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,481 @@
|
|||||||
|
From 92b00024559ff35a61d34f85e2c048a1845fca99 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Martin Sukany <martin@sukany.cz>
|
||||||
|
Date: Sat, 28 Feb 2026 09:32:52 +0100
|
||||||
|
Subject: [PATCH 2/3] ns: integrate accessibility with EmacsView and cursor
|
||||||
|
tracking
|
||||||
|
|
||||||
|
Wire the accessibility infrastructure from the previous patch into
|
||||||
|
the existing EmacsView class and the redisplay cycle. After this
|
||||||
|
patch, VoiceOver and Zoom support is fully active.
|
||||||
|
|
||||||
|
Integration points:
|
||||||
|
|
||||||
|
ns_update_end: call [view postAccessibilityUpdates] after each
|
||||||
|
redisplay cycle to dispatch accessibility notifications.
|
||||||
|
|
||||||
|
ns_draw_phys_cursor: store cursor rect for Zoom and call
|
||||||
|
UAZoomChangeFocus with correct CG coordinate-space transform
|
||||||
|
when ns-accessibility-enabled is non-nil.
|
||||||
|
|
||||||
|
EmacsView dealloc: release accessibilityElements array.
|
||||||
|
|
||||||
|
EmacsView windowDidBecomeKey: post
|
||||||
|
FocusedUIElementChangedNotification and SelectedTextChanged
|
||||||
|
so VoiceOver tracks the focused buffer on app/window switch.
|
||||||
|
|
||||||
|
EmacsView accessibility methods: rebuildAccessibilityTree walks
|
||||||
|
the Emacs window tree (ns_ax_collect_windows) to create/reuse
|
||||||
|
virtual elements. accessibilityChildren, accessibilityFocusedUI-
|
||||||
|
Element, postAccessibilityUpdates (the main notification dispatch
|
||||||
|
loop), accessibilityBoundsForRange (delegates to focused buffer
|
||||||
|
element with cursor-rect fallback for Zoom), and legacy
|
||||||
|
parameterized attribute APIs.
|
||||||
|
|
||||||
|
postAccessibilityUpdates detects three events: window tree change
|
||||||
|
(rebuild + layout notification), window switch (focus notification),
|
||||||
|
and per-buffer changes (delegated to each buffer element). A
|
||||||
|
re-entrance guard prevents infinite recursion from VoiceOver
|
||||||
|
callbacks that trigger redisplay.
|
||||||
|
|
||||||
|
* src/nsterm.m: EmacsView accessibility integration.
|
||||||
|
---
|
||||||
|
src/nsterm.m | 395 +++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
1 file changed, 395 insertions(+)
|
||||||
|
|
||||||
|
diff --git a/src/nsterm.m b/src/nsterm.m
|
||||||
|
index c47912d..34af842 100644
|
||||||
|
--- a/src/nsterm.m
|
||||||
|
+++ b/src/nsterm.m
|
||||||
|
@@ -1105,6 +1105,11 @@ ns_update_end (struct frame *f)
|
||||||
|
|
||||||
|
unblock_input ();
|
||||||
|
ns_updating_frame = NULL;
|
||||||
|
+
|
||||||
|
+#ifdef NS_IMPL_COCOA
|
||||||
|
+ /* Post accessibility notifications after each redisplay cycle. */
|
||||||
|
+ [view postAccessibilityUpdates];
|
||||||
|
+#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
@@ -3233,6 +3238,43 @@ ns_draw_window_cursor (struct window *w, struct glyph_row *glyph_row,
|
||||||
|
/* Prevent the cursor from being drawn outside the text area. */
|
||||||
|
r = NSIntersectionRect (r, ns_row_rect (w, glyph_row, TEXT_AREA));
|
||||||
|
|
||||||
|
+#ifdef NS_IMPL_COCOA
|
||||||
|
+ /* Accessibility: store cursor rect for Zoom and bounds queries.
|
||||||
|
+ Skipped when ns-accessibility-enabled is nil to avoid overhead.
|
||||||
|
+ VoiceOver notifications are handled solely by
|
||||||
|
+ postAccessibilityUpdates (called from ns_update_end)
|
||||||
|
+ to avoid duplicate notifications and mid-redisplay fragility. */
|
||||||
|
+ {
|
||||||
|
+ EmacsView *view = FRAME_NS_VIEW (f);
|
||||||
|
+ if (view && on_p && active_p && ns_accessibility_enabled)
|
||||||
|
+ {
|
||||||
|
+ view->lastAccessibilityCursorRect = r;
|
||||||
|
+
|
||||||
|
+ /* Tell macOS Zoom where the cursor is. UAZoomChangeFocus()
|
||||||
|
+ expects top-left origin (CG coordinate space).
|
||||||
|
+ These APIs are available since macOS 10.4 (Universal Access
|
||||||
|
+ framework, linked via ApplicationServices umbrella). */
|
||||||
|
+#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \
|
||||||
|
+ && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
|
||||||
|
+ if (UAZoomEnabled ())
|
||||||
|
+ {
|
||||||
|
+ NSRect windowRect = [view convertRect:r toView:nil];
|
||||||
|
+ NSRect screenRect = [[view window] convertRectToScreen:windowRect];
|
||||||
|
+ 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 /* MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 */
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+#endif
|
||||||
|
+
|
||||||
|
ns_focus (f, NULL, 0);
|
||||||
|
|
||||||
|
NSGraphicsContext *ctx = [NSGraphicsContext currentContext];
|
||||||
|
@@ -9204,6 +9246,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
|
||||||
|
[layer release];
|
||||||
|
#endif
|
||||||
|
|
||||||
|
+ [accessibilityElements release];
|
||||||
|
[[self menu] release];
|
||||||
|
[super dealloc];
|
||||||
|
}
|
||||||
|
@@ -10552,6 +10595,32 @@ ns_in_echo_area (void)
|
||||||
|
XSETFRAME (event.frame_or_window, emacsframe);
|
||||||
|
kbd_buffer_store_event (&event);
|
||||||
|
ns_send_appdefined (-1); // Kick main loop
|
||||||
|
+
|
||||||
|
+#ifdef NS_IMPL_COCOA
|
||||||
|
+ /* Notify VoiceOver that the focused accessibility element changed.
|
||||||
|
+ Post on the focused virtual element so VoiceOver starts tracking it.
|
||||||
|
+ This is critical for initial focus and app-switch scenarios. */
|
||||||
|
+ if (ns_accessibility_enabled)
|
||||||
|
+ {
|
||||||
|
+ id focused = [self accessibilityFocusedUIElement];
|
||||||
|
+ if (focused
|
||||||
|
+ && [focused isKindOfClass:[EmacsAccessibilityBuffer class]])
|
||||||
|
+ {
|
||||||
|
+ ns_ax_post_notification (focused,
|
||||||
|
+ NSAccessibilityFocusedUIElementChangedNotification);
|
||||||
|
+ NSDictionary *info = @{
|
||||||
|
+ @"AXTextStateChangeType":
|
||||||
|
+ @(ns_ax_text_state_change_selection_move),
|
||||||
|
+ @"AXTextChangeElement": focused
|
||||||
|
+ };
|
||||||
|
+ ns_ax_post_notification_with_info (focused,
|
||||||
|
+ NSAccessibilitySelectedTextChangedNotification, info);
|
||||||
|
+ }
|
||||||
|
+ else if (focused)
|
||||||
|
+ ns_ax_post_notification (focused,
|
||||||
|
+ NSAccessibilityFocusedUIElementChangedNotification);
|
||||||
|
+ }
|
||||||
|
+#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@@ -11789,6 +11858,332 @@ ns_in_echo_area (void)
|
||||||
|
return fs_state;
|
||||||
|
}
|
||||||
|
|
||||||
|
+#ifdef NS_IMPL_COCOA
|
||||||
|
+
|
||||||
|
+/* ---- Accessibility: walk the Emacs window tree ---- */
|
||||||
|
+
|
||||||
|
+static void
|
||||||
|
+ns_ax_collect_windows (Lisp_Object window, EmacsView *view,
|
||||||
|
+ NSMutableArray *elements,
|
||||||
|
+ NSDictionary *existing)
|
||||||
|
+{
|
||||||
|
+ if (NILP (window))
|
||||||
|
+ return;
|
||||||
|
+
|
||||||
|
+ struct window *w = XWINDOW (window);
|
||||||
|
+
|
||||||
|
+ if (WINDOW_LEAF_P (w))
|
||||||
|
+ {
|
||||||
|
+ /* Buffer element — reuse existing if available. */
|
||||||
|
+ EmacsAccessibilityBuffer *elem
|
||||||
|
+ = [existing objectForKey:[NSValue valueWithPointer:w]];
|
||||||
|
+ if (!elem)
|
||||||
|
+ {
|
||||||
|
+ elem = [[EmacsAccessibilityBuffer alloc] init];
|
||||||
|
+ elem.emacsView = view;
|
||||||
|
+
|
||||||
|
+ /* Initialize cached state to -1 to force first notification. */
|
||||||
|
+ elem.cachedModiff = -1;
|
||||||
|
+ elem.cachedPoint = -1;
|
||||||
|
+ elem.cachedMarkActive = NO;
|
||||||
|
+ }
|
||||||
|
+ else
|
||||||
|
+ {
|
||||||
|
+ [elem retain];
|
||||||
|
+ }
|
||||||
|
+ elem.lispWindow = window;
|
||||||
|
+ [elements addObject:elem];
|
||||||
|
+ [elem release];
|
||||||
|
+
|
||||||
|
+ /* Mode line element (skip for minibuffer). */
|
||||||
|
+ if (!MINI_WINDOW_P (w))
|
||||||
|
+ {
|
||||||
|
+ EmacsAccessibilityModeLine *ml
|
||||||
|
+ = [[EmacsAccessibilityModeLine alloc] init];
|
||||||
|
+ ml.emacsView = view;
|
||||||
|
+ ml.lispWindow = window;
|
||||||
|
+ [elements addObject:ml];
|
||||||
|
+ [ml release];
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ else
|
||||||
|
+ {
|
||||||
|
+ /* Internal (combination) window — recurse into children. */
|
||||||
|
+ Lisp_Object child = w->contents;
|
||||||
|
+ while (!NILP (child))
|
||||||
|
+ {
|
||||||
|
+ ns_ax_collect_windows (child, view, elements, existing);
|
||||||
|
+ child = XWINDOW (child)->next;
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+- (void)rebuildAccessibilityTree
|
||||||
|
+{
|
||||||
|
+ NSTRACE ("[EmacsView rebuildAccessibilityTree]");
|
||||||
|
+ if (!emacsframe)
|
||||||
|
+ return;
|
||||||
|
+
|
||||||
|
+ /* Build map of existing elements by window pointer for reuse. */
|
||||||
|
+ NSMutableDictionary *existing = [NSMutableDictionary dictionary];
|
||||||
|
+ if (accessibilityElements)
|
||||||
|
+ {
|
||||||
|
+ for (EmacsAccessibilityElement *elem in accessibilityElements)
|
||||||
|
+ {
|
||||||
|
+ if ([elem isKindOfClass:[EmacsAccessibilityBuffer class]]
|
||||||
|
+ && !NILP (elem.lispWindow))
|
||||||
|
+ [existing setObject:elem
|
||||||
|
+ forKey:[NSValue valueWithPointer:
|
||||||
|
+ XWINDOW (elem.lispWindow)]];
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ NSMutableArray *newElements = [NSMutableArray arrayWithCapacity:8];
|
||||||
|
+
|
||||||
|
+ /* Collect from main window tree. */
|
||||||
|
+ Lisp_Object root = FRAME_ROOT_WINDOW (emacsframe);
|
||||||
|
+ ns_ax_collect_windows (root, self, newElements, existing);
|
||||||
|
+
|
||||||
|
+ /* Include minibuffer. */
|
||||||
|
+ Lisp_Object mini = emacsframe->minibuffer_window;
|
||||||
|
+ if (!NILP (mini))
|
||||||
|
+ ns_ax_collect_windows (mini, self, newElements, existing);
|
||||||
|
+
|
||||||
|
+ [accessibilityElements release];
|
||||||
|
+ accessibilityElements = [newElements retain];
|
||||||
|
+ accessibilityTreeValid = YES;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+- (void)invalidateAccessibilityTree
|
||||||
|
+{
|
||||||
|
+ accessibilityTreeValid = NO;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+- (NSAccessibilityRole)accessibilityRole
|
||||||
|
+{
|
||||||
|
+ return NSAccessibilityGroupRole;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+- (NSString *)accessibilityLabel
|
||||||
|
+{
|
||||||
|
+ return @"Emacs";
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+- (BOOL)isAccessibilityElement
|
||||||
|
+{
|
||||||
|
+ return YES;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+- (NSArray *)accessibilityChildren
|
||||||
|
+{
|
||||||
|
+ if (!accessibilityElements || !accessibilityTreeValid)
|
||||||
|
+ [self rebuildAccessibilityTree];
|
||||||
|
+ return accessibilityElements;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+- (id)accessibilityFocusedUIElement
|
||||||
|
+{
|
||||||
|
+ if (!emacsframe)
|
||||||
|
+ return self;
|
||||||
|
+
|
||||||
|
+ if (!accessibilityElements || !accessibilityTreeValid)
|
||||||
|
+ [self rebuildAccessibilityTree];
|
||||||
|
+
|
||||||
|
+ for (EmacsAccessibilityElement *elem in accessibilityElements)
|
||||||
|
+ {
|
||||||
|
+ if ([elem isKindOfClass:[EmacsAccessibilityBuffer class]]
|
||||||
|
+ && EQ (elem.lispWindow, emacsframe->selected_window))
|
||||||
|
+ return elem;
|
||||||
|
+ }
|
||||||
|
+ return self;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+/* Called from ns_update_end to post AX notifications.
|
||||||
|
+
|
||||||
|
+ Important: post notifications BEFORE rebuilding the tree.
|
||||||
|
+ 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. */
|
||||||
|
+- (void)postAccessibilityUpdates
|
||||||
|
+{
|
||||||
|
+ NSTRACE ("[EmacsView postAccessibilityUpdates]");
|
||||||
|
+ eassert ([NSThread isMainThread]);
|
||||||
|
+
|
||||||
|
+ if (!emacsframe || !ns_accessibility_enabled)
|
||||||
|
+ return;
|
||||||
|
+
|
||||||
|
+ /* Re-entrance guard: VoiceOver callbacks during notification posting
|
||||||
|
+ can trigger redisplay, which calls ns_update_end, which calls us
|
||||||
|
+ again. Prevent infinite recursion. */
|
||||||
|
+ if (accessibilityUpdating)
|
||||||
|
+ return;
|
||||||
|
+ accessibilityUpdating = YES;
|
||||||
|
+
|
||||||
|
+ /* Detect window tree change (split, delete, new buffer). Compare
|
||||||
|
+ FRAME_ROOT_WINDOW — if it changed, the tree structure changed. */
|
||||||
|
+ Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe);
|
||||||
|
+ if (!EQ (curRoot, lastRootWindow))
|
||||||
|
+ {
|
||||||
|
+ lastRootWindow = curRoot;
|
||||||
|
+ accessibilityTreeValid = NO;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ /* If tree is stale, rebuild FIRST so we don't iterate freed
|
||||||
|
+ window pointers. Skip notifications for this cycle — the
|
||||||
|
+ freshly-built elements have no previous state to diff against. */
|
||||||
|
+ if (!accessibilityTreeValid)
|
||||||
|
+ {
|
||||||
|
+ [self rebuildAccessibilityTree];
|
||||||
|
+ /* Invalidate span cache — window layout changed. */
|
||||||
|
+ for (EmacsAccessibilityElement *elem in accessibilityElements)
|
||||||
|
+ if ([elem isKindOfClass: [EmacsAccessibilityBuffer class]])
|
||||||
|
+ [(EmacsAccessibilityBuffer *) elem invalidateInteractiveSpans];
|
||||||
|
+ ns_ax_post_notification (self,
|
||||||
|
+ NSAccessibilityLayoutChangedNotification);
|
||||||
|
+
|
||||||
|
+ /* Post focus change so VoiceOver picks up the new tree. */
|
||||||
|
+ id focused = [self accessibilityFocusedUIElement];
|
||||||
|
+ if (focused && focused != self)
|
||||||
|
+ ns_ax_post_notification (focused,
|
||||||
|
+ NSAccessibilityFocusedUIElementChangedNotification);
|
||||||
|
+
|
||||||
|
+ lastSelectedWindow = emacsframe->selected_window;
|
||||||
|
+ accessibilityUpdating = NO;
|
||||||
|
+ return;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ /* Post per-buffer notifications using EXISTING elements that have
|
||||||
|
+ cached state from the previous cycle. Validate each window
|
||||||
|
+ pointer before use. */
|
||||||
|
+ for (EmacsAccessibilityElement *elem in accessibilityElements)
|
||||||
|
+ {
|
||||||
|
+ if ([elem isKindOfClass:[EmacsAccessibilityBuffer class]])
|
||||||
|
+ {
|
||||||
|
+ struct window *w = [elem validWindow];
|
||||||
|
+ if (w && WINDOW_LEAF_P (w)
|
||||||
|
+ && BUFFERP (w->contents) && XBUFFER (w->contents))
|
||||||
|
+ [(EmacsAccessibilityBuffer *) elem
|
||||||
|
+ postAccessibilityNotificationsForFrame:emacsframe];
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ /* Check for window switch (C-x o). */
|
||||||
|
+ Lisp_Object curSel = emacsframe->selected_window;
|
||||||
|
+ BOOL windowSwitched = !EQ (curSel, lastSelectedWindow);
|
||||||
|
+ if (windowSwitched)
|
||||||
|
+ {
|
||||||
|
+ lastSelectedWindow = curSel;
|
||||||
|
+ id focused = [self accessibilityFocusedUIElement];
|
||||||
|
+ if (focused && focused != self
|
||||||
|
+ && [focused isKindOfClass:[EmacsAccessibilityBuffer class]])
|
||||||
|
+ {
|
||||||
|
+ ns_ax_post_notification (focused,
|
||||||
|
+ NSAccessibilityFocusedUIElementChangedNotification);
|
||||||
|
+ NSDictionary *info = @{
|
||||||
|
+ @"AXTextStateChangeType":
|
||||||
|
+ @(ns_ax_text_state_change_selection_move),
|
||||||
|
+ @"AXTextChangeElement": focused
|
||||||
|
+ };
|
||||||
|
+ ns_ax_post_notification_with_info (focused,
|
||||||
|
+ NSAccessibilitySelectedTextChangedNotification, info);
|
||||||
|
+ }
|
||||||
|
+ else if (focused && focused != self)
|
||||||
|
+ ns_ax_post_notification (focused,
|
||||||
|
+ NSAccessibilityFocusedUIElementChangedNotification);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ accessibilityUpdating = NO;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+/* ---- Cursor position for Zoom (via accessibilityBoundsForRange:) ----
|
||||||
|
+
|
||||||
|
+ accessibilityFrame returns the VIEW's frame (standard behavior).
|
||||||
|
+ The cursor location is exposed through accessibilityBoundsForRange:
|
||||||
|
+ which AT tools query using the selectedTextRange. */
|
||||||
|
+
|
||||||
|
+- (NSRect)accessibilityBoundsForRange:(NSRange)range
|
||||||
|
+{
|
||||||
|
+ /* Delegate to the focused buffer element for accurate per-range
|
||||||
|
+ geometry when possible. Fall back to the cached cursor rect
|
||||||
|
+ (set by ns_draw_phys_cursor) for Zoom and simple AT queries. */
|
||||||
|
+ id focused = [self accessibilityFocusedUIElement];
|
||||||
|
+ if ([focused isKindOfClass:[EmacsAccessibilityBuffer class]])
|
||||||
|
+ {
|
||||||
|
+ NSRect bufRect = [(EmacsAccessibilityBuffer *) focused
|
||||||
|
+ accessibilityFrameForRange:range];
|
||||||
|
+ if (!NSIsEmptyRect (bufRect))
|
||||||
|
+ return bufRect;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ NSRect viewRect = lastAccessibilityCursorRect;
|
||||||
|
+
|
||||||
|
+ if (viewRect.size.width < 1)
|
||||||
|
+ viewRect.size.width = 1;
|
||||||
|
+ if (viewRect.size.height < 1)
|
||||||
|
+ viewRect.size.height = 8;
|
||||||
|
+
|
||||||
|
+ NSWindow *win = [self window];
|
||||||
|
+ if (win == nil)
|
||||||
|
+ return NSZeroRect;
|
||||||
|
+
|
||||||
|
+ NSRect windowRect = [self convertRect:viewRect toView:nil];
|
||||||
|
+ return [win convertRectToScreen:windowRect];
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+/* Modern NSAccessibility protocol entry point. Delegates to
|
||||||
|
+ accessibilityBoundsForRange: which holds the real implementation
|
||||||
|
+ shared with the legacy parameterized-attribute API. */
|
||||||
|
+- (NSRect)accessibilityFrameForRange:(NSRange)range
|
||||||
|
+{
|
||||||
|
+ return [self accessibilityBoundsForRange:range];
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+/* Delegate to the focused virtual buffer element so both the modern
|
||||||
|
+ and legacy APIs return the correct string data. */
|
||||||
|
+- (NSString *)accessibilityStringForRange:(NSRange)range
|
||||||
|
+{
|
||||||
|
+ id focused = [self accessibilityFocusedUIElement];
|
||||||
|
+ if ([focused isKindOfClass:[EmacsAccessibilityBuffer class]])
|
||||||
|
+ return [(EmacsAccessibilityBuffer *) focused
|
||||||
|
+ accessibilityStringForRange:range];
|
||||||
|
+ return @"";
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+/* ---- Legacy parameterized attribute APIs (Zoom uses these) ---- */
|
||||||
|
+
|
||||||
|
+- (NSArray *)accessibilityParameterizedAttributeNames
|
||||||
|
+{
|
||||||
|
+ NSArray *superAttrs = [super accessibilityParameterizedAttributeNames];
|
||||||
|
+ if (superAttrs == nil)
|
||||||
|
+ superAttrs = @[];
|
||||||
|
+ return [superAttrs arrayByAddingObjectsFromArray:
|
||||||
|
+ @[NSAccessibilityBoundsForRangeParameterizedAttribute,
|
||||||
|
+ NSAccessibilityStringForRangeParameterizedAttribute]];
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+- (id)accessibilityAttributeValue:(NSString *)attribute
|
||||||
|
+ forParameter:(id)parameter
|
||||||
|
+{
|
||||||
|
+ if ([attribute isEqualToString:
|
||||||
|
+ NSAccessibilityBoundsForRangeParameterizedAttribute])
|
||||||
|
+ {
|
||||||
|
+ NSRange range = [(NSValue *) parameter rangeValue];
|
||||||
|
+ return [NSValue valueWithRect:
|
||||||
|
+ [self accessibilityBoundsForRange:range]];
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ if ([attribute isEqualToString:
|
||||||
|
+ NSAccessibilityStringForRangeParameterizedAttribute])
|
||||||
|
+ {
|
||||||
|
+ NSRange range = [(NSValue *) parameter rangeValue];
|
||||||
|
+ return [self accessibilityStringForRange:range];
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ return [super accessibilityAttributeValue:attribute forParameter:parameter];
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+#endif /* NS_IMPL_COCOA */
|
||||||
|
+
|
||||||
|
@end /* EmacsView */
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
2.43.0
|
||||||
|
|
||||||
@@ -1,21 +1,22 @@
|
|||||||
From ce3b2a8091c99f738ec59acd6f6ebf0d84826e34 Mon Sep 17 00:00:00 2001
|
From 12440652eb52520da0714f1762e037836bda7b5b Mon Sep 17 00:00:00 2001
|
||||||
From: Martin Sukany <martin@sukany.cz>
|
From: Martin Sukany <martin@sukany.cz>
|
||||||
Date: Fri, 27 Feb 2026 17:49:51 +0100
|
Date: Sat, 28 Feb 2026 09:33:23 +0100
|
||||||
Subject: [PATCH 2/2] doc: add VoiceOver accessibility section to macOS
|
Subject: [PATCH 3/3] doc: add VoiceOver accessibility section to macOS
|
||||||
appendix
|
appendix
|
||||||
|
|
||||||
Document the new VoiceOver accessibility support in the Emacs manual.
|
Document the new VoiceOver accessibility support in the Emacs manual.
|
||||||
Add a new section to the macOS appendix covering screen reader usage,
|
Add a new section to the macOS appendix covering screen reader usage,
|
||||||
keyboard navigation feedback, completion announcements, Zoom cursor
|
keyboard navigation feedback, completion announcements, Zoom cursor
|
||||||
tracking, and the ns-accessibility-enabled user option.
|
tracking, the ns-accessibility-enabled user option, and known
|
||||||
|
limitations (text cap, mode-line icon fonts, bidi hit-testing).
|
||||||
|
|
||||||
* doc/emacs/macos.texi (VoiceOver Accessibility): New section.
|
* doc/emacs/macos.texi (VoiceOver Accessibility): New section.
|
||||||
---
|
---
|
||||||
doc/emacs/macos.texi | 53 ++++++++++++++++++++++++++++++++++++++++++++
|
doc/emacs/macos.texi | 75 ++++++++++++++++++++++++++++++++++++++++++++
|
||||||
1 file changed, 53 insertions(+)
|
1 file changed, 75 insertions(+)
|
||||||
|
|
||||||
diff --git a/doc/emacs/macos.texi b/doc/emacs/macos.texi
|
diff --git a/doc/emacs/macos.texi b/doc/emacs/macos.texi
|
||||||
index 6bd334f..1d969f9 100644
|
index 6bd334f..c4dced5 100644
|
||||||
--- a/doc/emacs/macos.texi
|
--- a/doc/emacs/macos.texi
|
||||||
+++ b/doc/emacs/macos.texi
|
+++ b/doc/emacs/macos.texi
|
||||||
@@ -36,6 +36,7 @@ Support}), but we hope to improve it in the future.
|
@@ -36,6 +36,7 @@ Support}), but we hope to improve it in the future.
|
||||||
@@ -26,7 +27,7 @@ index 6bd334f..1d969f9 100644
|
|||||||
* GNUstep Support:: Details on status of GNUstep support.
|
* GNUstep Support:: Details on status of GNUstep support.
|
||||||
@end menu
|
@end menu
|
||||||
|
|
||||||
@@ -272,6 +273,58 @@ and return the result as a string. You can also use the Lisp function
|
@@ -272,6 +273,80 @@ and return the result as a string. You can also use the Lisp function
|
||||||
services and receive the results back. Note that you may need to
|
services and receive the results back. Note that you may need to
|
||||||
restart Emacs to access newly-available services.
|
restart Emacs to access newly-available services.
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ index 6bd334f..1d969f9 100644
|
|||||||
+@cindex VoiceOver
|
+@cindex VoiceOver
|
||||||
+@cindex accessibility (macOS)
|
+@cindex accessibility (macOS)
|
||||||
+@cindex screen reader (macOS)
|
+@cindex screen reader (macOS)
|
||||||
|
+@cindex Zoom, cursor tracking (macOS)
|
||||||
+
|
+
|
||||||
+ When built with the Cocoa interface on macOS, Emacs exposes buffer
|
+ When built with the Cocoa interface on macOS, Emacs exposes buffer
|
||||||
+content, cursor position, mode lines, and interactive elements to the
|
+content, cursor position, mode lines, and interactive elements to the
|
||||||
@@ -76,11 +78,32 @@ index 6bd334f..1d969f9 100644
|
|||||||
+use), set @code{ns-accessibility-enabled} to @code{nil}. The default
|
+use), set @code{ns-accessibility-enabled} to @code{nil}. The default
|
||||||
+is @code{t}.
|
+is @code{t}.
|
||||||
+
|
+
|
||||||
|
+@subheading Known Limitations
|
||||||
|
+
|
||||||
|
+@itemize @bullet
|
||||||
|
+@item
|
||||||
|
+Accessibility text is capped at 100,000 UTF-16 units per window.
|
||||||
|
+Buffers exceeding this limit are truncated for accessibility purposes;
|
||||||
|
+VoiceOver will announce ``end of text'' at the cap boundary.
|
||||||
|
+@item
|
||||||
|
+Mode-line text extraction handles only character glyphs. Mode lines
|
||||||
|
+using icon fonts (e.g., @code{doom-modeline} with nerd-font icons)
|
||||||
|
+produce incomplete accessibility text.
|
||||||
|
+@item
|
||||||
|
+The accessibility virtual element tree is rebuilt automatically on
|
||||||
|
+window configuration changes (splits, deletions, new buffers).
|
||||||
|
+@item
|
||||||
|
+Right-to-left (bidi) text is exposed correctly as buffer content,
|
||||||
|
+but @code{accessibilityRangeForPosition} hit-testing assumes
|
||||||
|
+left-to-right glyph layout.
|
||||||
|
+@end itemize
|
||||||
|
+
|
||||||
+ This support is available only on the Cocoa build; GNUstep has a
|
+ This support is available only on the Cocoa build; GNUstep has a
|
||||||
+different accessibility model and is not yet supported
|
+different accessibility model and is not yet supported
|
||||||
+(@pxref{GNUstep Support}). Evil-mode block cursors are handled
|
+(@pxref{GNUstep Support}). Evil-mode block cursors are handled
|
||||||
+correctly: character navigation announces the character at the cursor
|
+correctly: character navigation announces the character at the cursor
|
||||||
+position, not the character before it.
|
+position, not the character before it.
|
||||||
|
+
|
||||||
+
|
+
|
||||||
@node GNUstep Support
|
@node GNUstep Support
|
||||||
@section GNUstep Support
|
@section GNUstep Support
|
||||||
Reference in New Issue
Block a user