Files
emacs-doom/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch
Daneel e5bfce500f v13 patch: fix MRC retain/release for accessibilityElements array
- arrayWithCapacity: returns autorelease object, must retain for ivar
- Release old array before reassigning in rebuildAccessibilityTree
- Release in EmacsView dealloc
- Fixes crash: _NSPasteboardTypeCache countByEnumeratingWithState (dangling pointer)
2026-02-26 08:52:46 +01:00

869 lines
24 KiB
Diff

From: Martin Sukany <martin@sukany.cz>
Date: Wed, 26 Feb 2026 00:00:00 +0100
Subject: [PATCH] ns: add VoiceOver accessibility support (virtual element tree)
Implement an accessibility tree using virtual NSAccessibilityElement
subclasses, enabling VoiceOver to read buffer contents, track cursor
movement, and announce window switches on macOS.
New classes:
- EmacsAccessibilityElement: base class with coordinate conversion
- EmacsAccessibilityBuffer: AXTextArea virtual element per Emacs window
EmacsView becomes an AXGroup containing EmacsAccessibilityBuffer children.
Notification hooks fire on cursor movement, text edits, and window changes.
Uses unsafe_unretained references (MRC compatible) and proper retain/release.
---
--- a/src/nsterm.h 2026-02-26 08:46:18.118172281 +0100
+++ b/src/nsterm.h 2026-02-26 08:46:06.891980688 +0100
@@ -455,6 +455,34 @@
/* ==========================================================================
+ 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;
+@property (nonatomic, assign) struct window *emacsWindow;
+- (NSRect)screenRectFromEmacsX:(int)x y:(int)y width:(int)w height:(int)h;
+@end
+
+/* Virtual AXTextArea element — one per visible Emacs window (buffer). */
+@interface EmacsAccessibilityBuffer : EmacsAccessibilityElement
+@property (nonatomic, assign) ptrdiff_t cachedModiff;
+@property (nonatomic, assign) ptrdiff_t cachedPoint;
+@property (nonatomic, assign) Lisp_Object cachedSelectedWindow;
+- (void)
+ postAccessibilityUpdatesForWindow:(struct window *)w
+ frame:(struct frame *)f;
+@end
+#endif /* NS_IMPL_COCOA */
+
+
+/* ==========================================================================
+
The main Emacs view
========================================================================== */
@@ -471,6 +499,8 @@
#ifdef NS_IMPL_COCOA
char *old_title;
BOOL maximizing_resize;
+ NSMutableArray *accessibilityElements;
+ Lisp_Object lastSelectedWindow;
#endif
BOOL font_panel_active;
NSFont *font_panel_result;
@@ -528,6 +558,12 @@
- (void)windowWillExitFullScreen;
- (void)windowDidExitFullScreen;
- (void)windowDidBecomeKey;
+
+#ifdef NS_IMPL_COCOA
+/* Accessibility support. */
+- (void)rebuildAccessibilityTree;
+- (void)postAccessibilityUpdates;
+#endif
@end
--- a/src/nsterm.m 2026-02-26 08:46:18.124172384 +0100
+++ b/src/nsterm.m 2026-02-26 08:52:18.397374436 +0100
@@ -1104,6 +1104,11 @@
unblock_input ();
ns_updating_frame = NULL;
+
+#ifdef NS_IMPL_COCOA
+ /* Post accessibility notifications after each redisplay cycle. */
+ [view postAccessibilityUpdates];
+#endif
}
static void
@@ -6849,6 +6854,610 @@
/* ==========================================================================
+ Accessibility virtual elements (macOS / Cocoa only)
+
+ ========================================================================== */
+
+#ifdef NS_IMPL_COCOA
+
+/* ---- Helper: extract visible text from glyph rows of a window ---- */
+static NSString *
+ns_ax_text_from_glyph_rows (struct window *w)
+{
+ if (!w || !w->current_matrix)
+ return @"";
+
+ struct glyph_matrix *matrix = w->current_matrix;
+ NSMutableString *text = [NSMutableString stringWithCapacity:4096];
+ int nrows = matrix->nrows;
+
+ for (int i = 0; i < 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;
+
+ struct glyph *glyph = row->glyphs[TEXT_AREA];
+ struct glyph *end = glyph + row->used[TEXT_AREA];
+
+ for (; glyph < end; glyph++)
+ {
+ if (glyph->type == CHAR_GLYPH && !glyph->padding_p)
+ {
+ unsigned ch = glyph->u.ch;
+ if (ch == '\n' || ch == '\r')
+ continue; /* row boundary handles newlines */
+ if (ch >= 32)
+ {
+ unichar uch = (unichar)ch;
+ [text appendString:[NSString stringWithCharacters:&uch
+ length:1]];
+ }
+ }
+ }
+
+ /* Add newline between rows unless this is the last displayed row. */
+ if (i + 1 < nrows)
+ {
+ struct glyph_row *next = matrix->rows + i + 1;
+ if (next->enabled_p && (next->displays_text_p || next->ends_at_zv_p)
+ && !next->mode_line_p)
+ [text appendString:@"\n"];
+ }
+ }
+
+ /* Cap at 32KB */
+ if ([text length] > 32768)
+ return [text substringToIndex:32768];
+
+ return text;
+}
+
+
+/* ---- Row geometry helpers ---- */
+
+/* Count the number of visible text rows (excluding mode line). */
+static int
+ns_ax_visible_row_count (struct window *w)
+{
+ if (!w || !w->current_matrix)
+ return 0;
+ struct glyph_matrix *matrix = w->current_matrix;
+ int count = 0;
+ for (int i = 0; i < matrix->nrows; i++)
+ {
+ struct glyph_row *row = matrix->rows + i;
+ if (row->enabled_p && !row->mode_line_p
+ && (row->displays_text_p || row->ends_at_zv_p))
+ count++;
+ }
+ return count;
+}
+
+/* Map a character index (within the glyph-extracted text) to a visual
+ row number (0-based, text rows only). */
+static int
+ns_ax_line_for_index (struct window *w, NSUInteger idx)
+{
+ if (!w || !w->current_matrix)
+ return 0;
+ struct glyph_matrix *matrix = w->current_matrix;
+ NSUInteger pos = 0;
+ int line = 0;
+
+ 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;
+
+ /* Count characters in this row. */
+ int row_chars = 0;
+ struct glyph *g = row->glyphs[TEXT_AREA];
+ struct glyph *gend = g + row->used[TEXT_AREA];
+ for (; g < gend; g++)
+ {
+ if (g->type == CHAR_GLYPH && !g->padding_p)
+ {
+ unsigned ch = g->u.ch;
+ if (ch != '\n' && ch != '\r' && (ch >= 32))
+ row_chars++;
+ }
+ }
+
+ NSUInteger row_end = pos + row_chars + 1; /* +1 for newline */
+ if (idx < row_end)
+ return line;
+ pos = row_end;
+ line++;
+ }
+ return MAX(0, line - 1);
+}
+
+/* Return character range for a given visual line number. */
+static NSRange
+ns_ax_range_for_line (struct window *w, int target_line)
+{
+ if (!w || !w->current_matrix)
+ return NSMakeRange(0, 0);
+ struct glyph_matrix *matrix = w->current_matrix;
+ NSUInteger pos = 0;
+ int line = 0;
+
+ 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;
+
+ int row_chars = 0;
+ struct glyph *g = row->glyphs[TEXT_AREA];
+ struct glyph *gend = g + row->used[TEXT_AREA];
+ for (; g < gend; g++)
+ {
+ if (g->type == CHAR_GLYPH && !g->padding_p)
+ {
+ unsigned ch = g->u.ch;
+ if (ch != '\n' && ch != '\r' && (ch >= 32))
+ row_chars++;
+ }
+ }
+
+ if (line == target_line)
+ return NSMakeRange(pos, row_chars);
+
+ pos += row_chars + 1; /* +1 for newline */
+ line++;
+ }
+ return NSMakeRange(NSNotFound, 0);
+}
+
+/* Compute screen rect for a character range by unioning glyph row rects. */
+static NSRect
+ns_ax_frame_for_range (struct window *w, EmacsView *view, NSRange range)
+{
+ if (!w || !w->current_matrix || !view)
+ return NSZeroRect;
+ struct glyph_matrix *matrix = w->current_matrix;
+ NSUInteger pos = 0;
+ 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;
+
+ int row_chars = 0;
+ struct glyph *g = row->glyphs[TEXT_AREA];
+ struct glyph *gend = g + row->used[TEXT_AREA];
+ for (; g < gend; g++)
+ {
+ if (g->type == CHAR_GLYPH && !g->padding_p)
+ {
+ unsigned ch = g->u.ch;
+ if (ch != '\n' && ch != '\r' && (ch >= 32))
+ row_chars++;
+ }
+ }
+
+ NSUInteger row_end = pos + row_chars + 1;
+ if (pos < range.location + range.length && row_end > range.location)
+ {
+ /* This row overlaps the requested range. */
+ 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->visible_height;
+
+ if (!found)
+ {
+ result = rowRect;
+ found = YES;
+ }
+ else
+ result = NSUnionRect(result, rowRect);
+ }
+ pos = row_end;
+ }
+
+ if (!found)
+ return NSZeroRect;
+
+ /* Convert from EmacsView (flipped) coords to screen coords. */
+ NSRect winRect = [view convertRect:result toView:nil];
+ return [[view window] convertRectToScreen:winRect];
+}
+
+
+/* Compute the character index within glyph-extracted text that
+ corresponds to the buffer point position. */
+static NSUInteger
+ns_ax_index_for_point (struct window *w)
+{
+ if (!w || !w->current_matrix || !WINDOW_LEAF_P(w))
+ return 0;
+
+ struct buffer *b = XBUFFER(w->contents);
+ if (!b)
+ return 0;
+
+ ptrdiff_t point = BUF_PT(b);
+ struct glyph_matrix *matrix = w->current_matrix;
+ NSUInteger pos = 0;
+
+ 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 (point >= row_start && point < row_end)
+ {
+ /* Point is within this row. Count visible glyphs whose
+ buffer charpos is before point. */
+ int chars_before = 0;
+ struct glyph *g = row->glyphs[TEXT_AREA];
+ struct glyph *gend = g + row->used[TEXT_AREA];
+ for (; g < gend; g++)
+ {
+ if (g->type == CHAR_GLYPH && !g->padding_p
+ && g->charpos >= row_start
+ && g->charpos < point)
+ {
+ unsigned ch = g->u.ch;
+ if (ch != '\n' && ch != '\r' && ch >= 32)
+ chars_before++;
+ }
+ }
+ return pos + chars_before;
+ }
+
+ /* Count visible chars in this row + newline. */
+ int row_chars = 0;
+ struct glyph *g = row->glyphs[TEXT_AREA];
+ struct glyph *gend = g + row->used[TEXT_AREA];
+ for (; g < gend; g++)
+ {
+ if (g->type == CHAR_GLYPH && !g->padding_p)
+ {
+ unsigned ch = g->u.ch;
+ if (ch != '\n' && ch != '\r' && (ch >= 32))
+ row_chars++;
+ }
+ }
+ pos += row_chars + 1;
+ }
+ return pos > 0 ? pos - 1 : 0;
+}
+
+
+@implementation EmacsAccessibilityElement
+
+- (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;
+}
+
+@end
+
+
+@implementation EmacsAccessibilityBuffer
+
+/* ---- NSAccessibility protocol ---- */
+
+- (NSAccessibilityRole)accessibilityRole
+{
+ return NSAccessibilityTextAreaRole;
+}
+
+- (NSString *)accessibilityRoleDescription
+{
+ return @"editor";
+}
+
+- (NSString *)accessibilityLabel
+{
+ struct window *w = self.emacsWindow;
+ if (w && WINDOW_LEAF_P(w))
+ {
+ struct buffer *b = XBUFFER(w->contents);
+ if (b)
+ {
+ Lisp_Object name = BVAR(b, name);
+ if (STRINGP(name))
+ return [NSString stringWithLispString:name];
+ }
+ }
+ return @"buffer";
+}
+
+- (id)accessibilityValue
+{
+ struct window *w = self.emacsWindow;
+ if (!w)
+ return @"";
+ return ns_ax_text_from_glyph_rows(w);
+}
+
+- (NSInteger)accessibilityNumberOfCharacters
+{
+ NSString *text = [self accessibilityValue];
+ return [text length];
+}
+
+- (NSString *)accessibilitySelectedText
+{
+ struct window *w = self.emacsWindow;
+ if (!w || !WINDOW_LEAF_P(w))
+ return @"";
+
+ struct buffer *b = XBUFFER(w->contents);
+ if (!b || NILP(BVAR(b, mark_active)))
+ return @"";
+
+ /* Return the selected region text. */
+ NSString *text = [self accessibilityValue];
+ NSRange sel = [self accessibilitySelectedTextRange];
+ if (sel.location == NSNotFound || sel.location + sel.length > [text length])
+ return @"";
+ return [text substringWithRange:sel];
+}
+
+- (NSRange)accessibilitySelectedTextRange
+{
+ struct window *w = self.emacsWindow;
+ if (!w || !WINDOW_LEAF_P(w))
+ return NSMakeRange(0, 0);
+
+ struct buffer *b = XBUFFER(w->contents);
+ if (!b)
+ return NSMakeRange(0, 0);
+
+ NSUInteger point_idx = ns_ax_index_for_point(w);
+
+ if (NILP(BVAR(b, mark_active)))
+ return NSMakeRange(point_idx, 0);
+
+ /* With active mark, report the selection range. Map mark
+ position to accessibility index using the same glyph-based
+ mapping as point. */
+ ptrdiff_t mark_pos = marker_position (BVAR (b, mark));
+ ptrdiff_t pt_pos = BUF_PT (b);
+ ptrdiff_t begv = BUF_BEGV (b);
+ ptrdiff_t sel_start = (mark_pos < pt_pos) ? mark_pos : pt_pos;
+ ptrdiff_t sel_end = (mark_pos < pt_pos) ? pt_pos : mark_pos;
+ NSUInteger start_idx = (NSUInteger) (sel_start - begv);
+ NSUInteger len = (NSUInteger) (sel_end - sel_start);
+ return NSMakeRange(start_idx, len);
+}
+
+- (NSInteger)accessibilityInsertionPointLineNumber
+{
+ struct window *w = self.emacsWindow;
+ if (!w)
+ return 0;
+ NSUInteger idx = ns_ax_index_for_point(w);
+ return ns_ax_line_for_index(w, idx);
+}
+
+- (NSString *)accessibilityStringForRange:(NSRange)range
+{
+ NSString *text = [self accessibilityValue];
+ if (range.location + range.length > [text length])
+ return @"";
+ return [text substringWithRange:range];
+}
+
+- (NSInteger)accessibilityLineForIndex:(NSInteger)index
+{
+ struct window *w = self.emacsWindow;
+ if (!w)
+ return 0;
+ return ns_ax_line_for_index(w, (NSUInteger)index);
+}
+
+- (NSRange)accessibilityRangeForLine:(NSInteger)line
+{
+ struct window *w = self.emacsWindow;
+ if (!w)
+ return NSMakeRange(0, 0);
+ return ns_ax_range_for_line(w, (int)line);
+}
+
+- (NSRect)accessibilityFrameForRange:(NSRange)range
+{
+ struct window *w = self.emacsWindow;
+ EmacsView *view = self.emacsView;
+ if (!w || !view)
+ return NSZeroRect;
+ return ns_ax_frame_for_range(w, view, range);
+}
+
+
+- (NSRange)accessibilityRangeForPosition:(NSPoint)screenPoint
+{
+ /* Hit test: convert screen point to buffer character index.
+ Used by VoiceOver for mouse/trackpad exploration. */
+ struct window *w = self.emacsWindow;
+ EmacsView *view = self.emacsView;
+ if (!w || !view || !w->current_matrix)
+ return NSMakeRange (0, 0);
+
+ /* Convert screen point to EmacsView coordinates. */
+ NSPoint windowPoint = [[view window] convertPointFromScreen:screenPoint];
+ NSPoint viewPoint = [view convertPoint:windowPoint fromView:nil];
+
+ /* Convert to Emacs pixel coordinates (EmacsView is flipped). */
+ int x = (int) viewPoint.x - w->pixel_left;
+ int y = (int) viewPoint.y - w->pixel_top;
+
+ if (x < 0 || y < 0 || x >= w->pixel_width || y >= w->pixel_height)
+ return NSMakeRange (0, 0);
+
+ /* Find the glyph row at this y coordinate. */
+ struct glyph_matrix *matrix = w->current_matrix;
+ struct glyph_row *hit_row = NULL;
+ int row_y = 0;
+
+ for (int i = 0; i < matrix->nrows; i++)
+ {
+ struct glyph_row *row = matrix->rows + i;
+ if (!row->enabled_p || !row->displays_text_p)
+ continue;
+ if (y >= row_y && y < row_y + row->visible_height)
+ {
+ hit_row = row;
+ break;
+ }
+ row_y += row->visible_height;
+ }
+
+ if (!hit_row)
+ return NSMakeRange (0, 0);
+
+ /* Find the glyph at this x coordinate within the row. */
+ struct glyph *glyph = hit_row->glyphs[TEXT_AREA];
+ struct glyph *end = glyph + hit_row->used[TEXT_AREA];
+ int glyph_x = 0;
+ ptrdiff_t best_charpos = MATRIX_ROW_START_CHARPOS (hit_row);
+
+ for (; glyph < end; glyph++)
+ {
+ if (glyph->type == CHAR_GLYPH && glyph->charpos > 0)
+ {
+ if (x >= glyph_x && x < glyph_x + glyph->pixel_width)
+ {
+ best_charpos = glyph->charpos;
+ break;
+ }
+ best_charpos = glyph->charpos;
+ }
+ glyph_x += glyph->pixel_width;
+ }
+
+ /* Convert buffer charpos to accessibility index. */
+ struct buffer *b = XBUFFER (w->contents);
+ if (!b)
+ return NSMakeRange (0, 0);
+
+ ptrdiff_t idx = best_charpos - BUF_BEGV (b);
+ if (idx < 0) idx = 0;
+
+ return NSMakeRange ((NSUInteger) idx, 1);
+}
+
+- (NSRange)accessibilityVisibleCharacterRange
+{
+ NSString *text = [self accessibilityValue];
+ return NSMakeRange(0, [text length]);
+}
+
+- (BOOL)isAccessibilityFocused
+{
+ struct window *w = self.emacsWindow;
+ if (!w)
+ return NO;
+ EmacsView *view = self.emacsView;
+ if (!view || !view->emacsframe)
+ return NO;
+ struct frame *f = view->emacsframe;
+ return (w == XWINDOW(f->selected_window));
+}
+
+- (id)accessibilityParent
+{
+ return NSAccessibilityUnignoredAncestor (self.emacsView);
+}
+
+- (NSRect)accessibilityFrame
+{
+ struct window *w = self.emacsWindow;
+ if (!w)
+ return NSZeroRect;
+ return [self screenRectFromEmacsX:w->pixel_left
+ y:w->pixel_top
+ width:w->pixel_width
+ height:w->pixel_height];
+}
+
+/* ---- Notification dispatch ---- */
+
+- (void)postAccessibilityUpdatesForWindow:(struct window *)w
+ frame:(struct frame *)f
+{
+ if (!w || !WINDOW_LEAF_P(w))
+ return;
+
+ struct buffer *b = XBUFFER(w->contents);
+ if (!b)
+ return;
+
+ ptrdiff_t modiff = BUF_MODIFF(b);
+ ptrdiff_t point = BUF_PT(b);
+
+ /* Text content changed? */
+ if (modiff != self.cachedModiff)
+ {
+ self.cachedModiff = modiff;
+ NSAccessibilityPostNotification(self,
+ NSAccessibilityValueChangedNotification);
+
+ /* Rich typing echo for VoiceOver. */
+ NSDictionary *userInfo = @{
+ @"AXTextStateChangeType" : @1, /* AXTextStateChangeTypeEdit */
+ @"AXTextEditType" : @0 /* kAXTextEditTypeTyping */
+ };
+ NSAccessibilityPostNotificationWithUserInfo(
+ self, NSAccessibilityValueChangedNotification, userInfo);
+ }
+
+ /* Cursor moved? */
+ if (point != self.cachedPoint)
+ {
+ self.cachedPoint = point;
+ NSAccessibilityPostNotification(self,
+ NSAccessibilitySelectedTextChangedNotification);
+ }
+}
+
+@end
+
+#endif /* NS_IMPL_COCOA */
+
+
+/* ==========================================================================
+
EmacsView implementation
========================================================================== */
@@ -6889,6 +7498,7 @@
[layer release];
#endif
+ [accessibilityElements release];
[[self menu] release];
[super dealloc];
}
@@ -9474,6 +10084,144 @@
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)
+{
+ if (NILP (window))
+ return;
+
+ struct window *w = XWINDOW (window);
+
+ if (WINDOW_LEAF_P (w))
+ {
+ if (MINI_WINDOW_P (w))
+ return; /* Skip minibuffer for MVP. */
+
+ EmacsAccessibilityBuffer *elem = [[EmacsAccessibilityBuffer alloc] init];
+ elem.emacsView = view;
+ elem.emacsWindow = w;
+
+ /* Initialize cached state to trigger first notification. */
+ struct buffer *b = XBUFFER (w->contents);
+ if (b)
+ {
+ elem.cachedModiff = BUF_MODIFF (b);
+ elem.cachedPoint = BUF_PT (b);
+ }
+
+ [elements addObject:elem];
+ }
+ else
+ {
+ /* Internal (combination) window — recurse into children. */
+ Lisp_Object child = w->contents;
+ while (!NILP (child))
+ {
+ ns_ax_collect_windows (child, view, elements);
+ child = XWINDOW (child)->next;
+ }
+ }
+}
+
+- (void)rebuildAccessibilityTree
+{
+ if (!emacsframe)
+ return;
+
+ NSMutableArray *newElements = [NSMutableArray arrayWithCapacity:4];
+ Lisp_Object root = FRAME_ROOT_WINDOW (emacsframe);
+ ns_ax_collect_windows (root, self, newElements);
+ [accessibilityElements release];
+ accessibilityElements = [newElements retain];
+}
+
+- (NSAccessibilityRole)accessibilityRole
+{
+ return NSAccessibilityGroupRole;
+}
+
+- (NSString *)accessibilityLabel
+{
+ return @"Emacs";
+}
+
+- (BOOL)isAccessibilityElement
+{
+ return YES;
+}
+
+- (NSArray *)accessibilityChildren
+{
+ if (!accessibilityElements || [accessibilityElements count] == 0)
+ [self rebuildAccessibilityTree];
+ return accessibilityElements;
+}
+
+- (id)accessibilityFocusedUIElement
+{
+ if (!emacsframe)
+ return self;
+
+ /* Ensure tree exists (lazy init); avoid redundant rebuild since
+ postAccessibilityUpdates already rebuilds each cycle. */
+ if (!accessibilityElements || [accessibilityElements count] == 0)
+ [self rebuildAccessibilityTree];
+
+ struct window *sel = XWINDOW (emacsframe->selected_window);
+ for (EmacsAccessibilityBuffer *elem in accessibilityElements)
+ {
+ if (elem.emacsWindow == sel)
+ 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
+{
+ if (!emacsframe)
+ return;
+
+ /* Post per-buffer notifications using EXISTING elements that have
+ cached state from the previous cycle. */
+ for (EmacsAccessibilityBuffer *elem in accessibilityElements)
+ {
+ struct window *w = elem.emacsWindow;
+ if (w && WINDOW_LEAF_P (w))
+ [elem postAccessibilityUpdatesForWindow:w frame:emacsframe];
+ }
+
+ /* Check for window switch (C-x o) before rebuild. */
+ Lisp_Object curSel = emacsframe->selected_window;
+ BOOL windowSwitched = !EQ (curSel, lastSelectedWindow);
+ if (windowSwitched)
+ lastSelectedWindow = curSel;
+
+ /* Now rebuild tree to pick up window configuration changes. */
+ [self rebuildAccessibilityTree];
+
+ /* Post focus change AFTER rebuild so the new element exists. */
+ if (windowSwitched)
+ {
+ id focused = [self accessibilityFocusedUIElement];
+ if (focused && focused != self)
+ NSAccessibilityPostNotification (focused,
+ NSAccessibilityFocusedUIElementChangedNotification);
+ }
+}
+
+#endif /* NS_IMPL_COCOA */
+
@end /* EmacsView */
@@ -9941,6 +10689,14 @@
return [super accessibilityAttributeValue:attribute];
}
+
+- (id)accessibilityFocusedUIElement
+{
+ EmacsView *view = (EmacsView *)[self delegate];
+ if (view && [view respondsToSelector:@selector(accessibilityFocusedUIElement)])
+ return [view accessibilityFocusedUIElement];
+ return self;
+}
#endif /* NS_IMPL_COCOA */
/* Constrain size and placement of a frame.