Files
emacs-doom/patches/0005-ns-integrate-accessibility-with-EmacsView-and-redisp.patch
Daneel 73563be72d patches: fix VoiceOver reads only first word in org-agenda
When AXSelectedTextChanged is posted from the parent EmacsView (NSView)
with UIElementsKey pointing to the EmacsAXBuffer element, VoiceOver calls
accessibilityLineForIndex: on the VIEW rather than on the focused element.
In specialised buffers (org-agenda, org-super-agenda) where line geometry
differs from plain text, the view returns an incorrect range and VoiceOver
reads only the first word at the cursor (e.g. 'La' or 'Liga') instead of
the full line.

Plain text buffers were unaffected because the fallback geometry happened
to be correct for simple line layouts.

Fix: post AXSelectedTextChanged on self (the EmacsAXBuffer element)
instead of on self.emacsView.  This causes VoiceOver to call
accessibilityLineForIndex: on the element that owns the selection, which
returns the correct line range in all buffer types.  Remove UIElementsKey
(unnecessary when posting from the element itself).

This aligns with the pre-review code (51f5944) which always posted
AX notifications directly on the focused element.
2026-03-03 09:43:26 +01:00

628 lines
21 KiB
Diff

From 31d94ecffbfc42a48c6222a1794b823fd0db7b3c Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 5/8] ns: integrate accessibility with EmacsView and redisplay
Wire the accessibility element tree into EmacsView and hook it into
the redisplay cycle.
* etc/NEWS: Document VoiceOver accessibility support.
* src/nsterm.m (ns_update_end): Call -[EmacsView postAccessibilityUpdates].
(EmacsApp ns_update_accessibility_state): New method; query
AXIsProcessTrustedWithOptions and UAZoomEnabled to set
ns_accessibility_enabled automatically.
(EmacsApp ns_accessibility_did_change:): New method; handle
com.apple.accessibility.api distributed notification.
(EmacsView dealloc): Release accessibilityElements.
(EmacsView windowDidBecomeKey:): Post accessibility focus notification.
(ns_ax_collect_windows): New function.
(EmacsView rebuildAccessibilityTree, invalidateAccessibilityTree)
(accessibilityChildren, accessibilityFocusedUIElement)
(postAccessibilityUpdates, accessibilityBoundsForRange:)
(accessibilityParameterizedAttributeNames)
(accessibilityAttributeValue:forParameter:): New methods.
---
etc/NEWS | 13 ++
src/nsterm.m | 474 +++++++++++++++++++++++++++++++++++++++++++++++++--
2 files changed, 475 insertions(+), 12 deletions(-)
diff --git a/etc/NEWS b/etc/NEWS
index 4c149e41d6..7f917f93b2 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -4385,6 +4385,19 @@ 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.
+Set 'ns-accessibility-enabled' to nil to disable the accessibility
+interface and eliminate the associated overhead.
+
---
** Re-introduced dictation, lost in Emacs v30 (macOS).
We lost macOS dictation in v30 when migrating to NSTextInputClient.
diff --git a/src/nsterm.m b/src/nsterm.m
index b460beb00c..7c118045bd 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -1275,7 +1275,7 @@ If a completion candidate is selected (overlay or child frame),
static void
ns_zoom_track_completion (struct frame *f, EmacsView *view)
{
- if (!ns_zoom_enabled_p ())
+ if (!ns_accessibility_enabled || !ns_zoom_enabled_p ())
return;
if (!WINDOWP (f->selected_window))
return;
@@ -1393,7 +1393,8 @@ so the visual offset is (ov_line + 1) * line_h from
(zoomCursorUpdated is NO). */
#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \
&& MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
- if (view && !view->zoomCursorUpdated && ns_zoom_enabled_p ()
+ if (ns_accessibility_enabled && view && !view->zoomCursorUpdated
+ && ns_zoom_enabled_p ()
&& !NSIsEmptyRect (view->lastCursorRect))
{
NSRect r = view->lastCursorRect;
@@ -1420,6 +1421,9 @@ so the visual offset is (ov_line + 1) * line_h from
if (view)
ns_zoom_track_completion (f, view);
#endif /* NS_IMPL_COCOA */
+
+ /* Post accessibility notifications after each redisplay cycle. */
+ [view postAccessibilityUpdates];
}
static void
@@ -3567,7 +3571,7 @@ EmacsView pixels (AppKit, flipped, top-left origin)
#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \
&& MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
- if (ns_zoom_enabled_p ())
+ if (ns_accessibility_enabled && ns_zoom_enabled_p ())
{
NSRect windowRect = [view convertRect:r toView:nil];
NSRect screenRect
@@ -6723,9 +6727,56 @@ - (void)applicationDidFinishLaunching: (NSNotification *)notification
}
#endif
+#ifdef NS_IMPL_COCOA
+ /* Auto-detect Zoom and VoiceOver at startup and whenever their state
+ changes. The "com.apple.accessibility.api" distributed notification
+ fires when any assistive technology connects or disconnects.
+ Both code paths set ns_accessibility_enabled so that one variable
+ gates all our accessibility overhead. */
+ [self ns_update_accessibility_state];
+ [[NSDistributedNotificationCenter defaultCenter]
+ addObserver: self
+ selector: @selector(ns_accessibility_did_change:)
+ name: @"com.apple.accessibility.api"
+ object: nil
+suspensionBehavior: NSNotificationSuspensionBehaviorDeliverImmediately];
+#endif
+
ns_send_appdefined (-2);
}
+#ifdef NS_IMPL_COCOA
+/* Set ns_accessibility_enabled based on current AT state.
+ Called at startup and from the "com.apple.accessibility.api"
+ distributed notification handler. Checks both UAZoomEnabled()
+ (Zoom) and AXIsProcessTrustedWithOptions() (VoiceOver and other
+ ATs that have connected to this process). */
+- (void) ns_update_accessibility_state
+{
+ NSTRACE ("[EmacsApp ns_update_accessibility_state]");
+ BOOL zoom_on = UAZoomEnabled ();
+ NSDictionary *opts = @{(__bridge id) kAXTrustedCheckOptionPrompt: @NO};
+ BOOL at_on = AXIsProcessTrustedWithOptions ((__bridge CFDictionaryRef) opts);
+ BOOL new_state = zoom_on || at_on;
+ if ((BOOL) ns_accessibility_enabled != new_state)
+ {
+ ns_accessibility_enabled = new_state;
+ /* Reset the UAZoomEnabled cache so ns_zoom_enabled_p() reflects
+ the new Zoom state on its next call. */
+ ns_zoom_cache_time = 0;
+ }
+}
+
+/* Handler for the "com.apple.accessibility.api" distributed notification,
+ posted by macOS when any AT (VoiceOver, Switch Control, etc.) starts
+ or stops. */
+- (void) ns_accessibility_did_change: (NSNotification *) notification
+{
+ NSTRACE ("[EmacsApp ns_accessibility_did_change:]");
+ [self ns_update_accessibility_state];
+}
+#endif
+
- (void)antialiasThresholdDidChange:(NSNotification *)notification
{
#ifdef NS_IMPL_COCOA
@@ -7628,7 +7679,6 @@ - (id)accessibilityTopLevelUIElement
-
static BOOL
ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point,
ptrdiff_t *out_start,
@@ -8741,7 +8791,6 @@ - (NSRect)accessibilityFrame
@end
-
/* ===================================================================
EmacsAccessibilityBuffer (Notifications) — AX event dispatch
@@ -9235,6 +9284,50 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f
granularity = ns_ax_text_selection_granularity_line;
}
+ /* Programmatic jumps that cross a line boundary (]], [[, M-<,
+ xref, imenu, …) are discontiguous: the cursor teleported to an
+ arbitrary position, not one sequential step forward/backward.
+ Reporting AXTextSelectionDirectionDiscontiguous causes VoiceOver
+ to re-anchor its rotor browse cursor at the new
+ accessibilitySelectedTextRange rather than advancing linearly
+ from its previous internal position. */
+ if (!isCtrlNP && granularity == ns_ax_text_selection_granularity_line)
+ direction = ns_ax_text_selection_direction_discontiguous;
+
+ /* If Emacs moved the cursor (not VoiceOver), force discontiguous
+ so VoiceOver re-anchors its browse cursor to the current
+ accessibilitySelectedTextRange. This covers all Emacs-initiated
+ moves: editing commands, ELisp, isearch, etc.
+ Exception: C-n/C-p (isCtrlNP) already uses next/previous with
+ line granularity; those are already sequential and VoiceOver
+ handles them correctly. */
+ if (emacsMovedCursor && !isCtrlNP)
+ direction = ns_ax_text_selection_direction_discontiguous;
+
+ /* Re-anchor VoiceOver's browse cursor for discontiguous (teleport)
+ moves only. For sequential C-n/C-p (isCtrlNP), posting
+ FocusedUIElementChanged on the window races with the
+ AXSelectedTextChanged(granularity=line) notification and
+ causes VoiceOver to drop the line-read speech. Sequential
+ moves are already handled correctly by AXSelectedTextChanged
+ with direction=next/previous + granularity=line. */
+ if (emacsMovedCursor && !isCtrlNP && [self isAccessibilityFocused])
+ {
+ NSWindow *win = [self.emacsView window];
+ if (win)
+ ns_ax_post_notification (
+ win,
+ NSAccessibilityFocusedUIElementChangedNotification);
+
+ NSDictionary *layoutInfo = @{
+ NSAccessibilityUIElementsKey: @[self]
+ };
+ ns_ax_post_notification_with_info (
+ self.emacsView,
+ NSAccessibilityLayoutChangedNotification,
+ layoutInfo);
+ }
+
/* Post notifications for focused and non-focused elements. */
if ([self isAccessibilityFocused])
[self postFocusedCursorNotification:point
@@ -9347,7 +9440,6 @@ - (NSRect)accessibilityFrame
@end
-
/* ===================================================================
EmacsAccessibilityInteractiveSpan --- helpers and implementation
=================================================================== */
@@ -9682,6 +9774,7 @@ - (void)dealloc
[layer release];
#endif
+ [accessibilityElements release];
[[self menu] release];
[super dealloc];
}
@@ -11030,6 +11123,32 @@ - (void)windowDidBecomeKey /* for direct calls */
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
}
@@ -12267,6 +12386,332 @@ - (int) fullscreenState
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 = lastCursorRect;
+
+ 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 */
@@ -14263,12 +14708,17 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with
ns_use_srgb_colorspace = YES;
DEFVAR_BOOL ("ns-accessibility-enabled", ns_accessibility_enabled,
- doc: /* Non-nil means expose buffer content to the macOS accessibility
-subsystem (VoiceOver, Zoom, and other assistive technology).
-When nil, the accessibility virtual element tree is not built and no
-notifications are posted, eliminating the associated overhead.
-Requires the Cocoa (NS) build on macOS; ignored on GNUstep.
-Default is nil. Set to t to enable VoiceOver support. */);
+ doc: /* Non-nil enables Zoom cursor tracking and VoiceOver support.
+Emacs sets this automatically at startup when macOS Zoom is active or
+any assistive technology (VoiceOver, Switch Control, etc.) is connected,
+and updates it whenever that state changes. You can override manually:
+
+ (setq ns-accessibility-enabled t) ; always on
+ (setq ns-accessibility-enabled nil) ; always off
+
+When nil, no AX tree is built and no notifications are posted,
+giving zero per-redisplay overhead.
+Requires the Cocoa (NS) build on macOS; ignored on GNUstep. */);
ns_accessibility_enabled = NO;
DEFVAR_BOOL ("ns-use-mwheel-acceleration",
--
2.43.0