From 6c075e29fccc7dc6e9df693c4123ce0001a2dbfc Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 12:58:11 +0100 Subject: [PATCH 1/8] ns: add accessibility base classes and text extraction Add the foundation for macOS VoiceOver accessibility in the NS (Cocoa) port. No existing code paths are modified. * src/nsterm.h (ns_ax_visible_run): New struct. (EmacsAccessibilityElement): New base Objective-C class. (EmacsAccessibilityBuffer, EmacsAccessibilityModeLine) (EmacsAccessibilityInteractiveSpan): Forward-declare new classes. (EmacsAXSpanType): New enum for interactive span types. (EmacsView): New ivars for accessibility element tree. * src/nsterm.m: Include intervals.h for TEXT_PROP_MEANS_INVISIBLE. (ns_ax_buffer_text): New function; build visible-text string and run array for a window, skipping invisible character regions. (ns_ax_mode_line_text): New function; extract mode-line text. (ns_ax_frame_for_range): New function; map charpos range to screen rect via glyph matrix. (ns_ax_completion_string_from_prop) (ns_ax_window_buffer_object, ns_ax_window_end_charpos) (ns_ax_text_prop_at, ns_ax_next_prop_change) (ns_ax_get_span_label, ns_ax_post_notification) (ns_ax_post_notification_with_info): New helper functions. (EmacsAccessibilityElement): Implement base class. (syms_of_nsterm): Register accessibility DEFSYMs. Add DEFVAR_BOOL ns-accessibility-enabled with corrected doc: initial value is nil, set non-nil automatically when an AT is detected at startup. --- src/nsterm.h | 131 +++++++++++++++ src/nsterm.m | 454 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 585 insertions(+) diff --git a/src/nsterm.h b/src/nsterm.h index ea6e7ba4f5..5746e9e9bd 100644 --- a/src/nsterm.h +++ b/src/nsterm.h @@ -453,6 +453,124 @@ enum ns_return_frame_mode @end +/* ========================================================================== + + Accessibility virtual elements (macOS / Cocoa only) + + ========================================================================= */ + +#ifdef NS_IMPL_COCOA +@class EmacsView; + +/* Base class for virtual accessibility elements attached to EmacsView. */ +@interface EmacsAccessibilityElement : NSAccessibilityElement +@property (nonatomic, unsafe_unretained) EmacsView *emacsView; +/* Lisp window object --- safe across GC cycles. + GC safety: these Lisp_Objects are NOT visible to GC via staticpro + or the specpdl stack. This is safe because: + (1) Emacs GC runs only on the main thread, at well-defined safe + points during Lisp evaluation --- never during redisplay. + (2) Accessibility elements are owned by EmacsView which belongs to + an active frame; windows referenced here are always reachable + from the frame's window tree until rebuildAccessibilityTree + updates them during the next redisplay cycle. + (3) AX getters dispatch_sync to main before accessing Lisp state, + so GC cannot run concurrently with any access to lispWindow. + (4) validWindow checks WINDOW_LIVE_P before dereferencing. */ +@property (nonatomic, assign) Lisp_Object lispWindow; +- (struct window *)validWindow; /* Returns live window or NULL. */ +- (NSRect)screenRectFromEmacsX:(int)x y:(int)y width:(int)w height:(int)h; +@end + +/* A visible run: maps a contiguous range of accessibility indices + to a contiguous range of buffer character positions. Invisible + text is skipped, so ax_start values are consecutive across runs + while charpos values may have gaps. */ +typedef struct ns_ax_visible_run +{ + ptrdiff_t charpos; /* Buffer charpos where this visible run starts. */ + ptrdiff_t length; /* Number of visible Emacs characters in this run. */ + NSUInteger ax_start; /* Starting index in the accessibility string. */ + NSUInteger ax_length; /* Length in accessibility string (UTF-16 units). */ +} ns_ax_visible_run; + +/* Virtual AXTextArea element --- one per visible Emacs window (buffer). */ +@interface EmacsAccessibilityBuffer + : EmacsAccessibilityElement +{ + ns_ax_visible_run *visibleRuns; + NSUInteger visibleRunCount; + NSUInteger *lineStartOffsets; /* AX index for each line. */ + NSUInteger lineCount; /* Entries in lineStartOffsets. */ + NSMutableArray *cachedInteractiveSpans; + BOOL interactiveSpansDirty; +} +@property (nonatomic, retain) NSString *cachedText; +@property (nonatomic, assign) ptrdiff_t cachedTextModiff; +@property (nonatomic, assign) ptrdiff_t cachedOverlayModiff; +@property (nonatomic, assign) ptrdiff_t cachedTextStart; +@property (nonatomic, assign) ptrdiff_t cachedModiff; +@property (nonatomic, assign) ptrdiff_t cachedPoint; +@property (nonatomic, assign) BOOL cachedMarkActive; +@property (nonatomic, copy) NSString *cachedCompletionAnnouncement; +@property (nonatomic, assign) ptrdiff_t cachedCompletionOverlayStart; +@property (nonatomic, assign) ptrdiff_t cachedCompletionOverlayEnd; +@property (nonatomic, assign) ptrdiff_t cachedCompletionPoint; +- (void)invalidateTextCache; +- (NSInteger)lineForAXIndex:(NSUInteger)idx; +- (NSRange)rangeForLine:(NSUInteger)line textLength:(NSUInteger)tlen; +- (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx; +- (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos; +@end + +@interface EmacsAccessibilityBuffer (Notifications) +- (void)postTextChangedNotification:(ptrdiff_t)point; +- (void)postAccessibilityNotificationsForFrame:(struct frame *)f; +@end + +@interface EmacsAccessibilityBuffer (InteractiveSpans) +- (void)invalidateInteractiveSpans; +@end + +/* Virtual AXStaticText element --- one per mode line. */ +@interface EmacsAccessibilityModeLine : EmacsAccessibilityElement +@end + +/* Span types for interactive AX child elements. */ +typedef NS_ENUM (NSInteger, EmacsAXSpanType) +{ + EmacsAXSpanTypeNone = -1, + EmacsAXSpanTypeButton = 0, + EmacsAXSpanTypeLink = 1, + EmacsAXSpanTypeCompletionItem = 2, + EmacsAXSpanTypeWidget = 3, +}; + +/* A lightweight AX element representing one interactive text span + (button, link, checkbox, completion candidate, etc.) within a buffer + window. Exposed as AX child of EmacsAccessibilityBuffer so VoiceOver + Tab navigation can reach individual interactive elements. */ +@interface EmacsAccessibilityInteractiveSpan : EmacsAccessibilityElement + +@property (nonatomic, assign) ptrdiff_t charposStart; +@property (nonatomic, assign) ptrdiff_t charposEnd; +@property (nonatomic, assign) EmacsAXSpanType spanType; +@property (nonatomic, copy) NSString *spanLabel; +@property (nonatomic, copy) NSString *spanValue; +@property (nonatomic, unsafe_unretained) + EmacsAccessibilityBuffer *parentBuffer; + +- (NSAccessibilityRole) accessibilityRole; +- (NSString *) accessibilityLabel; +- (NSRect) accessibilityFrame; +- (BOOL) isAccessibilityElement; +- (BOOL) isAccessibilityFocused; +- (void) setAccessibilityFocused: (BOOL) focused; + +@end +#endif /* NS_IMPL_COCOA */ + + /* ========================================================================== The main Emacs view @@ -471,6 +589,12 @@ enum ns_return_frame_mode #ifdef NS_IMPL_COCOA char *old_title; BOOL maximizing_resize; + NSMutableArray *accessibilityElements; + /* See GC safety comment on EmacsAccessibilityElement.lispWindow. */ + Lisp_Object lastSelectedWindow; + Lisp_Object lastRootWindow; + BOOL accessibilityTreeValid; + BOOL accessibilityUpdating; #endif BOOL font_panel_active; NSFont *font_panel_result; @@ -534,6 +658,13 @@ enum ns_return_frame_mode - (void)windowWillExitFullScreen; - (void)windowDidExitFullScreen; - (void)windowDidBecomeKey; + +#ifdef NS_IMPL_COCOA +/* Accessibility support. */ +- (void)rebuildAccessibilityTree; +- (void)invalidateAccessibilityTree; +- (void)postAccessibilityUpdates; +#endif @end diff --git a/src/nsterm.m b/src/nsterm.m index 88c9251c18..9d36de66f9 100644 --- a/src/nsterm.m +++ b/src/nsterm.m @@ -46,6 +46,7 @@ Updated by Christian Limpach (chris@nice.ch) #include "blockinput.h" #include "sysselect.h" #include "nsterm.h" +#include "intervals.h" /* TEXT_PROP_MEANS_INVISIBLE */ #include "systime.h" #include "character.h" #include "xwidget.h" @@ -7201,6 +7202,432 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg } #endif +/* ========================================================================== + + Accessibility virtual elements (macOS / Cocoa only) + + ========================================================================= */ + +#ifdef NS_IMPL_COCOA + +/* ---- Helper: extract buffer text for accessibility ---- */ + +/* Build accessibility text for window W, skipping invisible text. + Populates *OUT_START with the buffer start charpos. + Populates *OUT_RUNS with an array of visible runs and *OUT_NRUNS + with the count. Caller must free *OUT_RUNS with xfree(). */ + +static NSString * +ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start, + ns_ax_visible_run **out_runs, NSUInteger *out_nruns) +{ + *out_runs = NULL; + *out_nruns = 0; + + if (!w || !WINDOW_LEAF_P (w)) + { + *out_start = 0; + return @""; + } + + struct buffer *b = XBUFFER (w->contents); + if (!b) + { + *out_start = 0; + return @""; + } + + ptrdiff_t begv = BUF_BEGV (b); + ptrdiff_t zv = BUF_ZV (b); + + *out_start = begv; + + if (zv <= begv) + return @""; + + specpdl_ref count = SPECPDL_INDEX (); + record_unwind_current_buffer (); + record_unwind_protect_void (unblock_input); + block_input (); + if (b != current_buffer) + set_buffer_internal_1 (b); + + /* First pass: count visible runs to allocate the mapping array. */ + NSUInteger run_capacity = 64; + ns_ax_visible_run *runs = xmalloc (run_capacity + * sizeof (ns_ax_visible_run)); + NSUInteger nruns = 0; + NSUInteger ax_offset = 0; + + NSMutableString *result = [NSMutableString string]; + ptrdiff_t pos = begv; + + while (pos < zv) + { + /* Check invisible property (text properties + overlays). + Use TEXT_PROP_MEANS_INVISIBLE which respects buffer-invisibility-spec, + matching the logic in xdisp.c. This correctly handles org-mode, + outline-mode, hideshow and any mode using spec-controlled + invisibility (not just `invisible t'). */ + Lisp_Object invis = Fget_char_property (make_fixnum (pos), + Qinvisible, Qnil); + if (TEXT_PROP_MEANS_INVISIBLE (invis)) + { + /* Skip to the next position where invisible changes. */ + Lisp_Object next = Fnext_single_char_property_change ( + make_fixnum (pos), Qinvisible, Qnil, make_fixnum (zv)); + pos = FIXNUMP (next) ? XFIXNUM (next) : zv; + continue; + } + + /* Find end of this visible run: where invisible property changes. */ + Lisp_Object next = Fnext_single_char_property_change ( + make_fixnum (pos), Qinvisible, Qnil, make_fixnum (zv)); + ptrdiff_t run_end = FIXNUMP (next) ? XFIXNUM (next) : zv; + + ptrdiff_t run_len = run_end - pos; + + /* Extract this visible run's text. Use + Fbuffer_substring_no_properties which correctly handles the + buffer gap --- raw BUF_BYTE_ADDRESS reads across the gap would + include garbage bytes when the run spans the gap position. */ + Lisp_Object lstr = Fbuffer_substring_no_properties ( + make_fixnum (pos), make_fixnum (run_end)); + NSString *nsstr = [NSString stringWithLispString:lstr]; + NSUInteger ns_len = [nsstr length]; + [result appendString:nsstr]; + + /* Record this visible run in the mapping. */ + if (nruns >= run_capacity) + { + run_capacity *= 2; + runs = xrealloc (runs, run_capacity + * sizeof (ns_ax_visible_run)); + } + runs[nruns].charpos = pos; + runs[nruns].length = run_len; + runs[nruns].ax_start = ax_offset; + runs[nruns].ax_length = ns_len; + nruns++; + + ax_offset += ns_len; + pos = run_end; + } + + unbind_to (count, Qnil); + + *out_runs = runs; + *out_nruns = nruns; + return result; +} + + +/* ---- Helper: extract mode line text from glyph rows ---- */ + +/* TODO: Only CHAR_GLYPH characters (>= 32) are extracted. Image + glyphs, stretch glyphs, and composed glyphs are silently skipped. + Mode lines using icon fonts (e.g. nerd-font icons) + will produce incomplete accessibility text. */ +static NSString * +ns_ax_mode_line_text (struct window *w) +{ + if (!w || !w->current_matrix) + return @""; + + struct glyph_matrix *matrix = w->current_matrix; + NSMutableString *text = [NSMutableString string]; + + for (int i = 0; i < matrix->nrows; i++) + { + struct glyph_row *row = matrix->rows + i; + if (!row->enabled_p || !row->mode_line_p) + continue; + + struct glyph *g = row->glyphs[TEXT_AREA]; + struct glyph *end = g + row->used[TEXT_AREA]; + for (; g < end; g++) + { + if (g->type == CHAR_GLYPH && g->u.ch >= 32) + { + unichar uch = (unichar) g->u.ch; + [text appendString:[NSString stringWithCharacters:&uch + length:1]]; + } + } + } + return text; +} + + +/* ---- Helper: screen rect for a character range via glyph matrix ---- */ + +static NSRect +ns_ax_frame_for_range (struct window *w, EmacsView *view, + ptrdiff_t charpos_start, + ptrdiff_t charpos_len) +{ + if (!w || !w->current_matrix || !view) + return NSZeroRect; + + /* charpos_start and charpos_len are already in buffer charpos + space --- the caller maps AX string indices through + charposForAccessibilityIndex which handles invisible text. */ + ptrdiff_t cp_start = charpos_start; + ptrdiff_t cp_end = cp_start + charpos_len; + + struct glyph_matrix *matrix = w->current_matrix; + NSRect result = NSZeroRect; + BOOL found = NO; + + for (int i = 0; i < matrix->nrows; i++) + { + struct glyph_row *row = matrix->rows + i; + if (!row->enabled_p || row->mode_line_p) + continue; + if (!row->displays_text_p && !row->ends_at_zv_p) + continue; + + ptrdiff_t row_start = MATRIX_ROW_START_CHARPOS (row); + ptrdiff_t row_end = MATRIX_ROW_END_CHARPOS (row); + + if (row_start < cp_end && row_end > cp_start) + { + int window_x, window_y, window_width; + window_box (w, TEXT_AREA, &window_x, &window_y, + &window_width, 0); + + NSRect rowRect; + rowRect.origin.x = window_x; + rowRect.origin.y = WINDOW_TO_FRAME_PIXEL_Y (w, MAX (0, row->y)); + rowRect.origin.y = MAX (rowRect.origin.y, window_y); + rowRect.size.width = window_width; + rowRect.size.height = row->height; + + if (!found) + { + result = rowRect; + found = YES; + } + else + result = NSUnionRect (result, rowRect); + } + } + + if (!found) + return NSZeroRect; + + /* Clip result to text area bounds. */ + { + int text_area_x, text_area_y, text_area_w, text_area_h; + window_box (w, TEXT_AREA, &text_area_x, &text_area_y, + &text_area_w, &text_area_h); + CGFloat max_y = WINDOW_TO_FRAME_PIXEL_Y (w, text_area_y + text_area_h); + if (NSMaxY (result) > max_y) + result.size.height = max_y - result.origin.y; + } + + /* Convert from EmacsView (flipped) coords to screen coords. */ + NSRect winRect = [view convertRect:result toView:nil]; + return [[view window] convertRectToScreen:winRect]; +} + +/* AX enum numeric compatibility for NSAccessibility notifications. + Values match WebKit AXObjectCacheMac fallback enums + (AXTextStateChangeType / AXTextEditType / AXTextSelectionDirection / + AXTextSelectionGranularity). */ +enum { + ns_ax_text_state_change_unknown = 0, + ns_ax_text_state_change_edit = 1, + ns_ax_text_state_change_selection_move = 2, + + ns_ax_text_edit_type_typing = 3, + + ns_ax_text_selection_direction_unknown = 0, + ns_ax_text_selection_direction_previous = 3, + ns_ax_text_selection_direction_next = 4, + ns_ax_text_selection_direction_discontiguous = 5, + + ns_ax_text_selection_granularity_unknown = 0, + ns_ax_text_selection_granularity_character = 1, + ns_ax_text_selection_granularity_word = 2, + ns_ax_text_selection_granularity_line = 3, +}; + +/* Extract announcement string from completion--string property value. + The property can be a plain Lisp string (simple completion) or + a list ("candidate" "annotation") for annotated completions. + Returns nil on failure. */ +static NSString * +ns_ax_completion_string_from_prop (Lisp_Object cstr) +{ + if (STRINGP (cstr)) + return [NSString stringWithLispString: cstr]; + if (CONSP (cstr) && STRINGP (XCAR (cstr))) + return [NSString stringWithLispString: XCAR (cstr)]; + return nil; +} + +/* Return the Emacs buffer Lisp object for window W, or Qnil. */ +static Lisp_Object +ns_ax_window_buffer_object (struct window *w) +{ + if (!w) + return Qnil; + if (!BUFFERP (w->contents)) + return Qnil; + return w->contents; +} + +/* Compute visible-end charpos for window W. + Emacs stores it as BUF_Z - window_end_pos. + Falls back to BUF_ZV when window_end_valid is false (e.g., when + called from an AX getter before the next redisplay cycle). */ +static ptrdiff_t +ns_ax_window_end_charpos (struct window *w, struct buffer *b) +{ + if (!w->window_end_valid) + return BUF_ZV (b); + return BUF_Z (b) - w->window_end_pos; +} + +/* Fetch text property PROP at charpos POS in BUF_OBJ. */ +static Lisp_Object +ns_ax_text_prop_at (ptrdiff_t pos, Lisp_Object prop, Lisp_Object buf_obj) +{ + Lisp_Object plist = Ftext_properties_at (make_fixnum (pos), buf_obj); + /* Third argument to Fplist_get is PREDICATE (Emacs 29+), not a + default value. Qnil selects the default `eq' comparison. */ + return Fplist_get (plist, prop, Qnil); +} + +/* Next charpos where PROP changes, capped at LIMIT. */ +static ptrdiff_t +ns_ax_next_prop_change (ptrdiff_t pos, Lisp_Object prop, + Lisp_Object buf_obj, ptrdiff_t limit) +{ + Lisp_Object result + = Fnext_single_property_change (make_fixnum (pos), prop, + buf_obj, make_fixnum (limit)); + return FIXNUMP (result) ? XFIXNUM (result) : limit; +} + +/* Build label for span [START, END) in BUF_OBJ. + Priority: completion--string -> buffer text -> help-echo. */ +static NSString * +ns_ax_get_span_label (ptrdiff_t start, ptrdiff_t end, + Lisp_Object buf_obj) +{ + Lisp_Object cs = ns_ax_text_prop_at (start, Qns_ax_completion__string, + buf_obj); + if (STRINGP (cs)) + return [NSString stringWithLispString: cs]; + + if (end > start) + { + Lisp_Object substr = Fbuffer_substring_no_properties ( + make_fixnum (start), make_fixnum (end)); + if (STRINGP (substr)) + { + NSString *s = [NSString stringWithLispString: substr]; + s = [s stringByTrimmingCharactersInSet: + [NSCharacterSet whitespaceAndNewlineCharacterSet]]; + if (s.length > 0) + return s; + } + } + + Lisp_Object he = ns_ax_text_prop_at (start, Qhelp_echo, buf_obj); + if (STRINGP (he)) + return [NSString stringWithLispString: he]; + + return @""; +} + +/* Post AX notifications asynchronously to prevent deadlock. + NSAccessibilityPostNotification may synchronously invoke VoiceOver + callbacks that dispatch_sync back to the main queue. If we are + already on the main queue (e.g., inside postAccessibilityUpdates + called from ns_update_end), that dispatch_sync deadlocks. + Deferring via dispatch_async lets the current method return first, + freeing the main queue for VoiceOver's dispatch_sync calls. */ + +static inline void +ns_ax_post_notification (id element, + NSAccessibilityNotificationName name) +{ + dispatch_async (dispatch_get_main_queue (), ^{ + NSAccessibilityPostNotification (element, name); + }); +} + +static inline void +ns_ax_post_notification_with_info (id element, + NSAccessibilityNotificationName name, + NSDictionary *info) +{ + dispatch_async (dispatch_get_main_queue (), ^{ + NSAccessibilityPostNotificationWithUserInfo (element, name, info); + }); +} + +@implementation EmacsAccessibilityElement + +- (instancetype)init +{ + self = [super init]; + if (self) + self.lispWindow = Qnil; + return self; +} + +/* Return the associated Emacs window if it is still live, else NULL. + Use this instead of storing a raw struct window * which can become a + dangling pointer after delete-window or kill-buffer. */ +- (struct window *)validWindow +{ + if (NILP (self.lispWindow) || !WINDOW_LIVE_P (self.lispWindow)) + return NULL; + return XWINDOW (self.lispWindow); +} + +- (NSRect)screenRectFromEmacsX:(int)x y:(int)y width:(int)ew height:(int)eh +{ + EmacsView *view = self.emacsView; + if (!view || ![view window]) + return NSZeroRect; + + NSRect r = NSMakeRect (x, y, ew, eh); + NSRect winRect = [view convertRect:r toView:nil]; + return [[view window] convertRectToScreen:winRect]; +} + +- (BOOL)isAccessibilityElement +{ + return YES; +} + +/* ---- Hierarchy plumbing (required for VoiceOver to find us) ---- */ + +- (id)accessibilityParent +{ + return NSAccessibilityUnignoredAncestor (self.emacsView); +} + +- (id)accessibilityWindow +{ + return [self.emacsView window]; +} + +- (id)accessibilityTopLevelUIElement +{ + return [self.emacsView window]; +} + +@end + +#endif /* NS_IMPL_COCOA */ + + /* ========================================================================== EmacsView implementation @@ -11657,6 +12084,24 @@ Convert an X font name (XLFD) to an NS font name. DEFSYM (Qns_drag_operation_generic, "ns-drag-operation-generic"); DEFSYM (Qns_handle_drag_motion, "ns-handle-drag-motion"); + /* Accessibility: line navigation command symbols for + ns_ax_event_is_line_nav_key (hot path, avoid intern per call). */ + DEFSYM (Qns_ax_next_line, "next-line"); + DEFSYM (Qns_ax_previous_line, "previous-line"); + DEFSYM (Qns_ax_dired_next_line, "dired-next-line"); + DEFSYM (Qns_ax_dired_previous_line, "dired-previous-line"); + + /* Accessibility span scanning symbols. */ + DEFSYM (Qns_ax_widget, "widget"); + DEFSYM (Qns_ax_button, "button"); + DEFSYM (Qns_ax_follow_link, "follow-link"); + DEFSYM (Qns_ax_org_link, "org-link"); + DEFSYM (Qns_ax_completion_list_mode, "completion-list-mode"); + DEFSYM (Qns_ax_completion__string, "completion--string"); + DEFSYM (Qns_ax_completion, "completion"); + DEFSYM (Qns_ax_completions_highlight, "completions-highlight"); + DEFSYM (Qns_ax_backtab, "backtab"); + /* Qmouse_face and Qkeymap are defined in textprop.c / keymap.c. */ Fput (Qalt, Qmodifier_value, make_fixnum (alt_modifier)); Fput (Qhyper, Qmodifier_value, make_fixnum (hyper_modifier)); Fput (Qmeta, Qmodifier_value, make_fixnum (meta_modifier)); @@ -11805,6 +12250,15 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with This variable is ignored on Mac OS X < 10.7 and GNUstep. */); 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. */); + ns_accessibility_enabled = NO; + DEFVAR_BOOL ("ns-use-mwheel-acceleration", ns_use_mwheel_acceleration, doc: /* Non-nil means use macOS's standard mouse wheel acceleration. -- 2.43.0