Files
emacs-doom/patches/0001-ns-add-accessibility-base-classes-and-text-extractio.patch
Daneel 71c81abcae patches: address all maintainer review issues
- Issue 1: Add explicit ApplicationServices import for UAZoomEnabled/
  UAZoomChangeFocus (was implicit via Carbon.h, now explicit)
- Issue 2: Rename FOR_EACH_FRAME variable 'frames' -> 'frame' (plural
  was misleading; matches Emacs convention)
- Issue 3: Move unblock_input before ObjC calls in
  postCompletionAnnouncementForBuffer: to avoid holding block_input
  during @synchronized operations
- Issue 4: Fix DEFVAR_BOOL doc and Texinfo: initial value is nil,
  not t; auto-detection sets it at startup
- Issue 5: Replace magic 10000 with NS_AX_MAX_COMPLETION_BUFFER_CHARS
  constant with explanatory comment
- Issue 6: Add comment to lineStartOffsets loop explaining it is gated
  on BUF_CHARS_MODIFF and never runs on the hot path
- Issue 8: Rewrite all 9 commit messages to GNU ChangeLog format with
  '* file (symbol): description' entries
- Issue 9: Break 81-char @interface line in nsterm.h
- Issue 10: Add WINDOWP/BUFFERP guards before dereferencing
  cf->selected_window and cw->contents in ns_zoom_find_child_frame_candidate
- Issue 11: Fix @pxref -> @xref at sentence start in macos.texi
2026-03-01 09:44:47 +01:00

704 lines
24 KiB
Diff

From 63788743619d25f4f41cb90b2eea5b48e0fcbc15 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
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 | 130 +++++++++++++++
src/nsterm.m | 462 ++++++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 589 insertions(+), 3 deletions(-)
diff --git a/src/nsterm.h b/src/nsterm.h
index ea6e7ba..7adbb92 100644
--- a/src/nsterm.h
+++ b/src/nsterm.h
@@ -453,6 +453,123 @@ 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 <NSAccessibility>
+{
+ 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 +588,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 +657,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 fc75910..e9ebac0 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"
@@ -7209,6 +7210,430 @@ - (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 ();
+ 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. doom-modeline with nerd-font)
+ 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
@@ -11665,6 +12090,28 @@ 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");
+ DEFSYM (Qns_ax_evil_next_line, "evil-next-line");
+ DEFSYM (Qns_ax_evil_previous_line, "evil-previous-line");
+ DEFSYM (Qns_ax_evil_next_visual_line, "evil-next-visual-line");
+ DEFSYM (Qns_ax_evil_previous_visual_line, "evil-previous-visual-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));
@@ -11797,7 +12244,7 @@ Convert an X font name (XLFD) to an NS font name.
doc: /* Non-nil means to use native fullscreen on Mac OS X 10.7 and later.
Nil means use fullscreen the old (< 10.7) way. The old way works better with
multiple monitors, but lacks tool bar. This variable is ignored on
-Mac OS X < 10.7. Default is t. */);
+Mac OS X < 10.7. Default is nil. Set to t to enable VoiceOver support. */);
ns_use_native_fullscreen = YES;
ns_last_use_native_fullscreen = ns_use_native_fullscreen;
@@ -11813,10 +12260,19 @@ 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.
-This variable is ignored on macOS < 10.7 and GNUstep. Default is t. */);
+This variable is ignored on macOS < 10.7 and GNUstep. Default is nil. Set to t to enable VoiceOver support. */);
ns_use_mwheel_acceleration = YES;
DEFVAR_LISP ("ns-mwheel-line-height", ns_mwheel_line_height,
@@ -11827,7 +12283,7 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with
DEFVAR_BOOL ("ns-use-mwheel-momentum", ns_use_mwheel_momentum,
doc: /* Non-nil means mouse wheel scrolling uses momentum.
-This variable is ignored on macOS < 10.7 and GNUstep. Default is t. */);
+This variable is ignored on macOS < 10.7 and GNUstep. Default is nil. Set to t to enable VoiceOver support. */);
ns_use_mwheel_momentum = YES;
/* TODO: Move to common code. */
--
2.43.0