diff --git a/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch b/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch index b874d6c..88e7e8d 100644 --- a/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch +++ b/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch @@ -1,18 +1,8 @@ -From: Martin Sukany -Date: Wed, 26 Feb 2026 00:00:00 +0100 -Subject: [PATCH] ns: add macOS Zoom cursor tracking and VoiceOver accessibility - -Dual accessibility: UAZoomChangeFocus for Zoom + virtual element tree for -VoiceOver. Notifications target the focused virtual element, not EmacsView. -Full hierarchy plumbing (accessibilityWindow, accessibilityParent, etc.). -MRC compatible. Single notification path (post-redisplay only) with -glyph-consistent coordinate system throughout. ---- diff --git a/src/nsterm.h b/src/nsterm.h -index 7c1ee4c..1f9639c 100644 +index 7c1ee4c..8bf21f6 100644 --- a/src/nsterm.h +++ b/src/nsterm.h -@@ -453,6 +453,34 @@ enum ns_return_frame_mode +@@ -453,6 +453,40 @@ enum ns_return_frame_mode @end @@ -25,21 +15,27 @@ index 7c1ee4c..1f9639c 100644 +#ifdef NS_IMPL_COCOA +@class EmacsView; + -+/* Base class for virtual accessibility elements attached to 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). */ ++/* Virtual AXTextArea element — one per visible Emacs window (buffer). */ +@interface EmacsAccessibilityBuffer : EmacsAccessibilityElement ++@property (nonatomic, retain) NSString *cachedText; ++@property (nonatomic, assign) ptrdiff_t cachedTextModiff; ++@property (nonatomic, assign) ptrdiff_t cachedTextStart; +@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; ++@property (nonatomic, assign) BOOL cachedMarkActive; ++- (void)invalidateTextCache; ++- (void)postAccessibilityNotificationsForFrame:(struct frame *)f; ++@end ++ ++/* Virtual AXStaticText element — one per mode line. */ ++@interface EmacsAccessibilityModeLine : EmacsAccessibilityElement +@end +#endif /* NS_IMPL_COCOA */ + @@ -47,19 +43,20 @@ index 7c1ee4c..1f9639c 100644 /* ========================================================================== The main Emacs view -@@ -471,6 +499,11 @@ enum ns_return_frame_mode +@@ -471,6 +505,12 @@ enum ns_return_frame_mode #ifdef NS_IMPL_COCOA char *old_title; BOOL maximizing_resize; + NSMutableArray *accessibilityElements; + Lisp_Object lastSelectedWindow; ++ BOOL accessibilityTreeValid; + @public + NSRect lastAccessibilityCursorRect; + @protected #endif BOOL font_panel_active; NSFont *font_panel_result; -@@ -528,6 +561,12 @@ enum ns_return_frame_mode +@@ -528,6 +568,13 @@ enum ns_return_frame_mode - (void)windowWillExitFullScreen; - (void)windowDidExitFullScreen; - (void)windowDidBecomeKey; @@ -67,16 +64,17 @@ index 7c1ee4c..1f9639c 100644 +#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 932d209..077f092 100644 +index 932d209..dd134dd 100644 --- a/src/nsterm.m +++ b/src/nsterm.m -@@ -1104,6 +1104,11 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen) +@@ -1104,6 +1104,11 @@ ns_update_end (struct frame *f) unblock_input (); ns_updating_frame = NULL; @@ -88,14 +86,14 @@ index 932d209..077f092 100644 } static void -@@ -3232,6 +3237,37 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors. +@@ -3232,6 +3237,37 @@ ns_draw_window_cursor (struct window *w, struct glyph_row *glyph_row, /* Prevent the cursor from being drawn outside the text area. */ r = NSIntersectionRect (r, ns_row_rect (w, glyph_row, TEXT_AREA)); +#ifdef NS_IMPL_COCOA + /* Accessibility: store cursor rect for Zoom and bounds queries. + VoiceOver notifications are handled solely by -+ postAccessibilityUpdatesForWindow: (called from ns_update_end) ++ postAccessibilityUpdates (called from ns_update_end) + to avoid duplicate notifications and mid-redisplay fragility. */ + { + EmacsView *view = FRAME_NS_VIEW (f); @@ -126,7 +124,7 @@ index 932d209..077f092 100644 ns_focus (f, NULL, 0); NSGraphicsContext *ctx = [NSGraphicsContext currentContext]; -@@ -6847,6 +6883,652 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg +@@ -6847,6 +6883,713 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action) } #endif @@ -138,172 +136,106 @@ index 932d209..077f092 100644 + +#ifdef NS_IMPL_COCOA + -+/* ---- Helper: extract visible text from glyph rows of a window ---- */ ++/* ---- Helper: extract buffer text for accessibility ---- */ ++ ++/* Maximum characters exposed via accessibilityValue. */ ++#define NS_AX_TEXT_CAP 100000 ++ +static NSString * -+ns_ax_text_from_glyph_rows (struct window *w) ++ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start) ++{ ++ 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); ++ ptrdiff_t len = zv - begv; ++ ++ /* Cap at NS_AX_TEXT_CAP characters, centered on point. */ ++ if (len > NS_AX_TEXT_CAP) ++ { ++ ptrdiff_t pt = BUF_PT (b); ++ ptrdiff_t half = NS_AX_TEXT_CAP / 2; ++ ptrdiff_t start = MAX (begv, pt - half); ++ ptrdiff_t end = MIN (zv, start + NS_AX_TEXT_CAP); ++ start = MAX (begv, end - NS_AX_TEXT_CAP); ++ begv = start; ++ zv = end; ++ } ++ ++ *out_start = begv; ++ ++ if (zv <= begv) ++ return @""; ++ ++ ptrdiff_t begv_byte = buf_charpos_to_bytepos (b, begv); ++ ptrdiff_t zv_byte = buf_charpos_to_bytepos (b, zv); ++ unsigned char *data = BUF_BYTE_ADDRESS (b, begv_byte); ++ ptrdiff_t nbytes = zv_byte - begv_byte; ++ ++ Lisp_Object lstr = make_string_from_bytes ((char *) data, ++ zv - begv, nbytes); ++ return [NSString stringWithLispString:lstr]; ++} ++ ++ ++/* ---- Helper: extract mode line text from glyph rows ---- */ ++ ++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 stringWithCapacity:4096]; -+ int nrows = matrix->nrows; ++ NSMutableString *text = [NSMutableString string]; + -+ for (int i = 0; i < nrows; i++) ++ 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; ++ if (!row->enabled_p || !row->mode_line_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"]; -+ } ++ 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]]; ++ } ++ } + } -+ -+ /* Cap at 32KB */ -+ if ([text length] > 32768) -+ return [text substringToIndex:32768]; -+ + return text; +} + + -+/* ---- Row geometry helpers ---- */ ++/* ---- Helper: screen rect for a character range via glyph matrix ---- */ + -+/* 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) ++ns_ax_frame_for_range (struct window *w, EmacsView *view, ++ ptrdiff_t text_start, NSRange range) +{ + if (!w || !w->current_matrix || !view) + return NSZeroRect; ++ ++ /* Convert range indices back to buffer charpos. */ ++ ptrdiff_t cp_start = text_start + (ptrdiff_t) range.location; ++ ptrdiff_t cp_end = cp_start + (ptrdiff_t) range.length; ++ + struct glyph_matrix *matrix = w->current_matrix; -+ NSUInteger pos = 0; + NSRect result = NSZeroRect; + BOOL found = NO; + @@ -311,46 +243,34 @@ index 932d209..077f092 100644 + { + struct glyph_row *row = matrix->rows + i; + if (!row->enabled_p || row->mode_line_p) -+ continue; ++ continue; + if (!row->displays_text_p && !row->ends_at_zv_p) -+ continue; ++ 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++; -+ } -+ } ++ ptrdiff_t row_start = MATRIX_ROW_START_CHARPOS (row); ++ ptrdiff_t row_end = MATRIX_ROW_END_CHARPOS (row); + -+ 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); ++ 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->visible_height; ++ 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) ++ { ++ result = rowRect; ++ found = YES; ++ } ++ else ++ result = NSUnionRect (result, rowRect); ++ } + } + + if (!found) @@ -362,82 +282,6 @@ index 932d209..077f092 100644 +} + + -+/* Compute the glyph-based character index for an arbitrary buffer -+ charpos within the visible glyph rows of window W. All accessibility -+ attribute methods must use this (or its wrapper ns_ax_index_for_point) -+ to stay in the same coordinate space as accessibilityValue. */ -+static NSUInteger -+ns_ax_index_for_charpos (struct window *w, ptrdiff_t charpos) -+{ -+ if (!w || !w->current_matrix || !WINDOW_LEAF_P(w)) -+ return 0; -+ -+ 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 (charpos >= row_start && charpos < row_end) -+ { -+ /* Charpos is within this row. Count visible glyphs whose -+ buffer charpos is before the target. */ -+ 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 < charpos) -+ { -+ 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; -+} -+ -+/* Convenience wrapper: glyph-based index for buffer point. */ -+static NSUInteger -+ns_ax_index_for_point (struct window *w) -+{ -+ if (!w || !WINDOW_LEAF_P(w)) -+ return 0; -+ struct buffer *b = XBUFFER(w->contents); -+ if (!b) -+ return 0; -+ return ns_ax_index_for_charpos (w, BUF_PT(b)); -+} -+ -+ +@implementation EmacsAccessibilityElement + +- (NSRect)screenRectFromEmacsX:(int)x y:(int)y width:(int)ew height:(int)eh @@ -446,7 +290,7 @@ index 932d209..077f092 100644 + if (!view || ![view window]) + return NSZeroRect; + -+ NSRect r = NSMakeRect(x, y, ew, eh); ++ NSRect r = NSMakeRect (x, y, ew, eh); + NSRect winRect = [view convertRect:r toView:nil]; + return [[view window] convertRectToScreen:winRect]; +} @@ -478,6 +322,43 @@ index 932d209..077f092 100644 + +@implementation EmacsAccessibilityBuffer + ++- (void)dealloc ++{ ++ [cachedText release]; ++ [super dealloc]; ++} ++ ++/* ---- Text cache ---- */ ++ ++- (void)invalidateTextCache ++{ ++ [cachedText release]; ++ cachedText = nil; ++} ++ ++- (void)ensureTextCache ++{ ++ struct window *w = self.emacsWindow; ++ if (!w || !WINDOW_LEAF_P (w)) ++ return; ++ ++ struct buffer *b = XBUFFER (w->contents); ++ if (!b) ++ return; ++ ++ ptrdiff_t modiff = BUF_MODIFF (b); ++ if (cachedText && cachedTextModiff == modiff) ++ return; ++ ++ ptrdiff_t start; ++ NSString *text = ns_ax_buffer_text (w, &start); ++ ++ [cachedText release]; ++ cachedText = [text retain]; ++ cachedTextModiff = modiff; ++ cachedTextStart = start; ++} ++ +/* ---- NSAccessibility protocol ---- */ + +- (NSAccessibilityRole)accessibilityRole @@ -487,28 +368,33 @@ index 932d209..077f092 100644 + +- (NSString *)accessibilityRoleDescription +{ ++ struct window *w = self.emacsWindow; ++ if (w && MINI_WINDOW_P (w)) ++ return @"minibuffer"; + return @"editor"; +} + +- (NSString *)accessibilityLabel +{ + struct window *w = self.emacsWindow; -+ if (w && WINDOW_LEAF_P(w)) ++ if (w && WINDOW_LEAF_P (w)) + { -+ struct buffer *b = XBUFFER(w->contents); ++ if (MINI_WINDOW_P (w)) ++ return @"Minibuffer"; ++ ++ struct buffer *b = XBUFFER (w->contents); + if (b) -+ { -+ Lisp_Object name = BVAR(b, name); -+ if (STRINGP(name)) -+ return [NSString stringWithLispString:name]; -+ } ++ { ++ Lisp_Object name = BVAR (b, name); ++ if (STRINGP (name)) ++ return [NSString stringWithLispString:name]; ++ } + } + return @"buffer"; +} + +- (BOOL)isAccessibilityFocused +{ -+ /* Return YES when this buffer's window is the selected window. */ + struct window *w = self.emacsWindow; + if (!w) + return NO; @@ -516,61 +402,58 @@ index 932d209..077f092 100644 + if (!view || !view->emacsframe) + return NO; + struct frame *f = view->emacsframe; -+ return (w == XWINDOW(f->selected_window)); ++ return (w == XWINDOW (f->selected_window)); +} + +- (id)accessibilityValue +{ -+ struct window *w = self.emacsWindow; -+ if (!w) -+ return @""; -+ return ns_ax_text_from_glyph_rows(w); ++ [self ensureTextCache]; ++ return cachedText ? cachedText : @""; +} + +- (NSInteger)accessibilityNumberOfCharacters +{ -+ NSString *text = [self accessibilityValue]; -+ return [text length]; ++ [self ensureTextCache]; ++ return cachedText ? [cachedText length] : 0; +} + +- (NSString *)accessibilitySelectedText +{ + struct window *w = self.emacsWindow; -+ if (!w || !WINDOW_LEAF_P(w)) ++ if (!w || !WINDOW_LEAF_P (w)) + return @""; + -+ struct buffer *b = XBUFFER(w->contents); -+ if (!b || NILP(BVAR(b, mark_active))) ++ 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]) ++ [self ensureTextCache]; ++ if (!cachedText || sel.location == NSNotFound ++ || sel.location + sel.length > [cachedText length]) + return @""; -+ return [text substringWithRange:sel]; ++ return [cachedText substringWithRange:sel]; +} + +- (NSRange)accessibilitySelectedTextRange +{ + struct window *w = self.emacsWindow; -+ if (!w || !WINDOW_LEAF_P(w)) -+ return NSMakeRange(0, 0); ++ if (!w || !WINDOW_LEAF_P (w)) ++ return NSMakeRange (0, 0); + -+ struct buffer *b = XBUFFER(w->contents); ++ struct buffer *b = XBUFFER (w->contents); + if (!b) -+ return NSMakeRange(0, 0); ++ return NSMakeRange (0, 0); + -+ NSUInteger point_idx = ns_ax_index_for_point(w); ++ [self ensureTextCache]; ++ ptrdiff_t pt = BUF_PT (b); ++ NSUInteger point_idx = (NSUInteger) (pt - cachedTextStart); + -+ if (NILP(BVAR(b, mark_active))) -+ return NSMakeRange(point_idx, 0); ++ if (NILP (BVAR (b, mark_active))) ++ return NSMakeRange (point_idx, 0); + -+ /* With active mark, report the selection range. Both endpoints -+ must be mapped to glyph-based indices consistent with -+ accessibilityValue (via ns_ax_index_for_charpos). */ + ptrdiff_t mark_pos = marker_position (BVAR (b, mark)); -+ NSUInteger mark_idx = ns_ax_index_for_charpos (w, mark_pos); ++ NSUInteger mark_idx = (NSUInteger) (mark_pos - cachedTextStart); + NSUInteger start_idx = MIN (point_idx, mark_idx); + NSUInteger end_idx = MAX (point_idx, mark_idx); + return NSMakeRange (start_idx, end_idx - start_idx); @@ -579,18 +462,38 @@ index 932d209..077f092 100644 +- (NSInteger)accessibilityInsertionPointLineNumber +{ + struct window *w = self.emacsWindow; -+ if (!w) ++ if (!w || !WINDOW_LEAF_P (w)) + return 0; -+ NSUInteger idx = ns_ax_index_for_point(w); -+ return ns_ax_line_for_index(w, idx); ++ ++ struct buffer *b = XBUFFER (w->contents); ++ if (!b) ++ return 0; ++ ++ [self ensureTextCache]; ++ if (!cachedText) ++ return 0; ++ ++ ptrdiff_t pt = BUF_PT (b); ++ NSUInteger point_idx = (NSUInteger) (pt - cachedTextStart); ++ if (point_idx > [cachedText length]) ++ point_idx = [cachedText length]; ++ ++ /* Count newlines from start to point_idx. */ ++ NSInteger line = 0; ++ for (NSUInteger i = 0; i < point_idx; i++) ++ { ++ if ([cachedText characterAtIndex:i] == '\n') ++ line++; ++ } ++ return line; +} + +- (NSString *)accessibilityStringForRange:(NSRange)range +{ -+ NSString *text = [self accessibilityValue]; -+ if (range.location + range.length > [text length]) ++ [self ensureTextCache]; ++ if (!cachedText || range.location + range.length > [cachedText length]) + return @""; -+ return [text substringWithRange:range]; ++ return [cachedText substringWithRange:range]; +} + +- (NSAttributedString *)accessibilityAttributedStringForRange:(NSRange)range @@ -601,18 +504,68 @@ index 932d209..077f092 100644 + +- (NSInteger)accessibilityLineForIndex:(NSInteger)index +{ -+ struct window *w = self.emacsWindow; -+ if (!w) ++ [self ensureTextCache]; ++ if (!cachedText || index < 0) + return 0; -+ return ns_ax_line_for_index(w, (NSUInteger)index); ++ ++ NSUInteger idx = (NSUInteger) index; ++ if (idx > [cachedText length]) ++ idx = [cachedText length]; ++ ++ /* Count newlines from start of cachedText to idx. */ ++ NSInteger line = 0; ++ for (NSUInteger i = 0; i < idx; i++) ++ { ++ if ([cachedText characterAtIndex:i] == '\n') ++ line++; ++ } ++ return line; +} + +- (NSRange)accessibilityRangeForLine:(NSInteger)line +{ -+ struct window *w = self.emacsWindow; -+ if (!w) -+ return NSMakeRange(0, 0); -+ return ns_ax_range_for_line(w, (int)line); ++ [self ensureTextCache]; ++ if (!cachedText || line < 0) ++ return NSMakeRange (NSNotFound, 0); ++ ++ NSUInteger len = [cachedText length]; ++ NSInteger cur_line = 0; ++ NSUInteger line_start = 0; ++ ++ for (NSUInteger i = 0; i <= len; i++) ++ { ++ if (cur_line == line) ++ { ++ /* Find end of this line. */ ++ NSUInteger line_end = i; ++ while (line_end < len ++ && [cachedText characterAtIndex:line_end] != '\n') ++ line_end++; ++ return NSMakeRange (i, line_end - i); ++ } ++ if (i < len && [cachedText characterAtIndex:i] == '\n') ++ { ++ cur_line++; ++ line_start = i + 1; ++ } ++ } ++ return NSMakeRange (NSNotFound, 0); ++} ++ ++- (NSRange)accessibilityRangeForIndex:(NSInteger)index ++{ ++ [self ensureTextCache]; ++ if (!cachedText || index < 0 ++ || (NSUInteger) index >= [cachedText length]) ++ return NSMakeRange (NSNotFound, 0); ++ return NSMakeRange ((NSUInteger) index, 1); ++} ++ ++- (NSRange)accessibilityStyleRangeForIndex:(NSInteger)index ++{ ++ /* Return the range of the current line — simple approach. */ ++ NSInteger line = [self accessibilityLineForIndex:index]; ++ return [self accessibilityRangeForLine:line]; +} + +- (NSRect)accessibilityFrameForRange:(NSRange)range @@ -621,14 +574,12 @@ index 932d209..077f092 100644 + EmacsView *view = self.emacsView; + if (!w || !view) + return NSZeroRect; -+ return ns_ax_frame_for_range(w, view, range); ++ return ns_ax_frame_for_range (w, view, cachedTextStart, range); +} + -+ +- (NSRange)accessibilityRangeForPosition:(NSPoint)screenPoint +{ -+ /* Hit test: convert screen point to buffer character index. -+ Used by VoiceOver for mouse/trackpad exploration. */ ++ /* Hit test: convert screen point to buffer character index. */ + struct window *w = self.emacsWindow; + EmacsView *view = self.emacsView; + if (!w || !view || !w->current_matrix) @@ -638,7 +589,7 @@ index 932d209..077f092 100644 + NSPoint windowPoint = [[view window] convertPointFromScreen:screenPoint]; + NSPoint viewPoint = [view convertPoint:windowPoint fromView:nil]; + -+ /* Convert to Emacs pixel coordinates (EmacsView is flipped). */ ++ /* Convert to window-relative pixel coordinates. */ + int x = (int) viewPoint.x - w->pixel_left; + int y = (int) viewPoint.y - w->pixel_top; + @@ -648,19 +599,19 @@ index 932d209..077f092 100644 + /* 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 (!row->enabled_p || !row->displays_text_p || row->mode_line_p) ++ continue; ++ int row_top = WINDOW_TO_FRAME_PIXEL_Y (w, MAX (0, row->y)); ++ if ((int) viewPoint.y >= row_top ++ && (int) viewPoint.y < row_top + row->visible_height) ++ { ++ hit_row = row; ++ break; ++ } + } + + if (!hit_row) @@ -675,27 +626,62 @@ index 932d209..077f092 100644 + 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; -+ } ++ { ++ 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 glyph-based accessibility index, -+ consistent with accessibilityValue coordinate space. */ -+ NSUInteger ax_idx = ns_ax_index_for_charpos (w, best_charpos); ++ /* Convert buffer charpos to accessibility index. */ ++ [self ensureTextCache]; ++ NSUInteger ax_idx = (NSUInteger) (best_charpos - cachedTextStart); ++ if (cachedText && ax_idx > [cachedText length]) ++ ax_idx = [cachedText length]; + return NSMakeRange (ax_idx, 1); +} + +- (NSRange)accessibilityVisibleCharacterRange +{ -+ NSString *text = [self accessibilityValue]; -+ return NSMakeRange(0, [text length]); ++ struct window *w = self.emacsWindow; ++ if (!w || !w->current_matrix) ++ { ++ [self ensureTextCache]; ++ return NSMakeRange (0, cachedText ? [cachedText length] : 0); ++ } ++ ++ [self ensureTextCache]; ++ if (!cachedText) ++ return NSMakeRange (0, 0); ++ ++ /* Compute visible range from window start to last visible row. */ ++ ptrdiff_t vis_start = w->start_charpos; ++ ptrdiff_t vis_end = vis_start; ++ ++ struct glyph_matrix *matrix = w->current_matrix; ++ for (int i = matrix->nrows - 1; i >= 0; 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)) ++ { ++ vis_end = MATRIX_ROW_END_CHARPOS (row); ++ break; ++ } ++ } ++ ++ NSUInteger loc = (NSUInteger) (vis_start - cachedTextStart); ++ NSUInteger end = (NSUInteger) (vis_end - cachedTextStart); ++ NSUInteger text_len = [cachedText length]; ++ if (loc > text_len) loc = text_len; ++ if (end > text_len) end = text_len; ++ if (end < loc) end = loc; ++ ++ return NSMakeRange (loc, end - loc); +} + +- (NSRect)accessibilityFrame @@ -704,82 +690,155 @@ index 932d209..077f092 100644 + if (!w) + return NSZeroRect; + return [self screenRectFromEmacsX:w->pixel_left -+ y:w->pixel_top -+ width:w->pixel_width -+ height:w->pixel_height]; ++ y:w->pixel_top ++ width:w->pixel_width ++ height:w->pixel_height]; +} + +/* ---- Notification dispatch ---- */ + -+- (void)postAccessibilityUpdatesForWindow:(struct window *)w -+ frame:(struct frame *)f ++- (void)postAccessibilityNotificationsForFrame:(struct frame *)f +{ -+ if (!w || !WINDOW_LEAF_P(w)) ++ struct window *w = self.emacsWindow; ++ if (!w || !WINDOW_LEAF_P (w)) + return; + -+ struct buffer *b = XBUFFER(w->contents); ++ struct buffer *b = XBUFFER (w->contents); + if (!b) + return; + -+ ptrdiff_t modiff = BUF_MODIFF(b); -+ ptrdiff_t point = BUF_PT(b); ++ ptrdiff_t modiff = BUF_MODIFF (b); ++ ptrdiff_t point = BUF_PT (b); ++ BOOL markActive = !NILP (BVAR (b, mark_active)); + -+ /* Text content changed? Post ValueChanged with typing echo userInfo. -+ Single notification (no bare ValueChanged first) — Chromium pattern. -+ kAXTextStateChangeTypeEdit = 1, kAXTextEditTypeTyping = 3 -+ (verified from WebKit AXTextStateChangeIntent.h). */ ++ /* --- Text changed → typing echo --- ++ kAXTextStateChangeTypeEdit = 0, kAXTextEditTypeTyping = 3. */ + if (modiff != self.cachedModiff) + { ++ /* Capture changed char before invalidating cache. */ ++ NSString *changedChar = @""; ++ if (point > self.cachedPoint ++ && point - self.cachedPoint == 1) ++ { ++ /* Single char inserted — refresh cache and grab it. */ ++ [self invalidateTextCache]; ++ [self ensureTextCache]; ++ if (cachedText) ++ { ++ NSUInteger idx = (NSUInteger) (point - 1 - cachedTextStart); ++ if (idx < [cachedText length]) ++ changedChar = [cachedText substringWithRange: ++ NSMakeRange (idx, 1)]; ++ } ++ } ++ else ++ { ++ [self invalidateTextCache]; ++ } ++ + self.cachedModiff = modiff; + -+ NSString *changedText = @""; -+ NSUInteger point_idx = ns_ax_index_for_point (w); -+ if (point_idx > 0) -+ { -+ NSRange charRange = NSMakeRange (point_idx - 1, 1); -+ changedText = [self accessibilityStringForRange:charRange]; -+ if (!changedText) -+ changedText = @""; -+ } -+ + NSDictionary *change = @{ -+ @"AXTextEditType": @3, -+ @"AXTextChangeValue": changedText ++ @"AXTextEditType": @3, ++ @"AXTextChangeValue": changedChar, ++ @"AXTextChangeValueLength": @([changedChar length]) + }; + NSDictionary *userInfo = @{ -+ @"AXTextStateChangeType": @1, -+ @"AXTextChangeValues": @[change] ++ @"AXTextStateChangeType": @0, ++ @"AXTextChangeValues": @[change] + }; -+ NSAccessibilityPostNotificationWithUserInfo( -+ self, NSAccessibilityValueChangedNotification, userInfo); ++ NSAccessibilityPostNotificationWithUserInfo ( ++ self, NSAccessibilityValueChangedNotification, userInfo); + } + -+ /* Cursor moved? Post with userInfo so VoiceOver distinguishes -+ navigation from programmatic selection changes and triggers -+ line-by-line reading. -+ kAXTextStateChangeTypeSelectionMove = 2. */ -+ if (point != self.cachedPoint) ++ /* --- Cursor moved or selection changed → line reading --- ++ kAXTextStateChangeTypeSelectionMove = 1. */ ++ if (point != self.cachedPoint || markActive != self.cachedMarkActive) + { + self.cachedPoint = point; ++ self.cachedMarkActive = markActive; ++ + NSDictionary *moveInfo = @{ -+ @"AXTextStateChangeType": @2 ++ @"AXTextStateChangeType": @1, ++ @"AXTextSelectionDirection": @4, ++ @"AXTextSelectionGranularity": @3 + }; -+ NSAccessibilityPostNotificationWithUserInfo( -+ self, -+ NSAccessibilitySelectedTextChangedNotification, -+ moveInfo); ++ NSAccessibilityPostNotificationWithUserInfo ( ++ self, ++ NSAccessibilitySelectedTextChangedNotification, ++ moveInfo); + } +} + +@end + ++ ++@implementation EmacsAccessibilityModeLine ++ ++- (NSAccessibilityRole)accessibilityRole ++{ ++ return NSAccessibilityStaticTextRole; ++} ++ ++- (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)) ++ { ++ NSString *bufName = [NSString stringWithLispString:name]; ++ return [NSString stringWithFormat:@"Mode Line - %@", bufName]; ++ } ++ } ++ } ++ return @"Mode Line"; ++} ++ ++- (id)accessibilityValue ++{ ++ struct window *w = self.emacsWindow; ++ if (!w) ++ return @""; ++ return ns_ax_mode_line_text (w); ++} ++ ++- (NSRect)accessibilityFrame ++{ ++ struct window *w = self.emacsWindow; ++ if (!w || !w->current_matrix) ++ return NSZeroRect; ++ ++ /* Find the mode line row and return its screen rect. */ ++ struct glyph_matrix *matrix = w->current_matrix; ++ for (int i = 0; i < matrix->nrows; i++) ++ { ++ struct glyph_row *row = matrix->rows + i; ++ if (row->enabled_p && row->mode_line_p) ++ { ++ return [self screenRectFromEmacsX:w->pixel_left ++ y:WINDOW_TO_FRAME_PIXEL_Y (w, ++ MAX (0, row->y)) ++ width:w->pixel_width ++ height:row->visible_height]; ++ } ++ } ++ return NSZeroRect; ++} ++ ++@end ++ +#endif /* NS_IMPL_COCOA */ + + /* ========================================================================== EmacsView implementation -@@ -6889,6 +7571,7 @@ - (void)dealloc +@@ -6889,6 +7632,7 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action) [layer release]; #endif @@ -787,7 +846,7 @@ index 932d209..077f092 100644 [[self menu] release]; [super dealloc]; } -@@ -8237,6 +8920,18 @@ - (void)windowDidBecomeKey /* for direct calls */ +@@ -8237,6 +8981,18 @@ ns_in_echo_area (void) XSETFRAME (event.frame_or_window, emacsframe); kbd_buffer_store_event (&event); ns_send_appdefined (-1); // Kick main loop @@ -800,13 +859,13 @@ index 932d209..077f092 100644 + id focused = [self accessibilityFocusedUIElement]; + if (focused) + NSAccessibilityPostNotification (focused, -+ NSAccessibilityFocusedUIElementChangedNotification); ++ NSAccessibilityFocusedUIElementChangedNotification); + } +#endif } -@@ -9474,6 +10169,213 @@ - (int) fullscreenState +@@ -9474,6 +10230,259 @@ ns_in_echo_area (void) return fs_state; } @@ -816,7 +875,8 @@ index 932d209..077f092 100644 + +static void +ns_ax_collect_windows (Lisp_Object window, EmacsView *view, -+ NSMutableArray *elements) ++ NSMutableArray *elements, ++ NSDictionary *existing) +{ + if (NILP (window)) + return; @@ -825,33 +885,51 @@ index 932d209..077f092 100644 + + if (WINDOW_LEAF_P (w)) + { -+ if (MINI_WINDOW_P (w)) -+ return; /* Skip minibuffer for MVP. */ ++ /* Buffer element — reuse existing if available. */ ++ EmacsAccessibilityBuffer *elem ++ = [existing objectForKey:[NSValue valueWithPointer:w]]; ++ if (!elem) ++ { ++ elem = [[EmacsAccessibilityBuffer alloc] init]; ++ elem.emacsView = view; + -+ EmacsAccessibilityBuffer *elem = [[EmacsAccessibilityBuffer alloc] init]; -+ elem.emacsView = view; ++ /* 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); ++ elem.cachedMarkActive = !NILP (BVAR (b, mark_active)); ++ } ++ } ++ else ++ { ++ [elem retain]; ++ } + 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]; -+ [elem release]; /* array retains; balance the alloc */ ++ [elem release]; ++ ++ /* Mode line element (skip for minibuffer). */ ++ if (!MINI_WINDOW_P (w)) ++ { ++ EmacsAccessibilityModeLine *ml ++ = [[EmacsAccessibilityModeLine alloc] init]; ++ ml.emacsView = view; ++ ml.emacsWindow = w; ++ [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); -+ child = XWINDOW (child)->next; -+ } ++ { ++ ns_ax_collect_windows (child, view, elements, existing); ++ child = XWINDOW (child)->next; ++ } + } +} + @@ -860,11 +938,39 @@ index 932d209..077f092 100644 + if (!emacsframe) + return; + -+ NSMutableArray *newElements = [NSMutableArray arrayWithCapacity:4]; ++ /* 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]] ++ && elem.emacsWindow) ++ [existing setObject:elem ++ forKey:[NSValue valueWithPointer: ++ elem.emacsWindow]]; ++ } ++ } ++ ++ NSMutableArray *newElements = [NSMutableArray arrayWithCapacity:8]; ++ ++ /* Collect from main window tree. */ + Lisp_Object root = FRAME_ROOT_WINDOW (emacsframe); -+ ns_ax_collect_windows (root, self, newElements); ++ 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 @@ -884,7 +990,7 @@ index 932d209..077f092 100644 + +- (NSArray *)accessibilityChildren +{ -+ if (!accessibilityElements || [accessibilityElements count] == 0) ++ if (!accessibilityElements || !accessibilityTreeValid) + [self rebuildAccessibilityTree]; + return accessibilityElements; +} @@ -894,16 +1000,15 @@ index 932d209..077f092 100644 + if (!emacsframe) + return self; + -+ /* Ensure tree exists (lazy init); avoid redundant rebuild since -+ postAccessibilityUpdates already rebuilds each cycle. */ -+ if (!accessibilityElements || [accessibilityElements count] == 0) ++ if (!accessibilityElements || !accessibilityTreeValid) + [self rebuildAccessibilityTree]; + + struct window *sel = XWINDOW (emacsframe->selected_window); -+ for (EmacsAccessibilityBuffer *elem in accessibilityElements) ++ for (EmacsAccessibilityElement *elem in accessibilityElements) + { -+ if (elem.emacsWindow == sel) -+ return elem; ++ if ([elem isKindOfClass:[EmacsAccessibilityBuffer class]] ++ && elem.emacsWindow == sel) ++ return elem; + } + return self; +} @@ -921,11 +1026,15 @@ index 932d209..077f092 100644 + + /* Post per-buffer notifications using EXISTING elements that have + cached state from the previous cycle. */ -+ for (EmacsAccessibilityBuffer *elem in accessibilityElements) ++ for (EmacsAccessibilityElement *elem in accessibilityElements) + { -+ struct window *w = elem.emacsWindow; -+ if (w && WINDOW_LEAF_P (w)) -+ [elem postAccessibilityUpdatesForWindow:w frame:emacsframe]; ++ if ([elem isKindOfClass:[EmacsAccessibilityBuffer class]]) ++ { ++ struct window *w = elem.emacsWindow; ++ if (w && WINDOW_LEAF_P (w)) ++ [(EmacsAccessibilityBuffer *) elem ++ postAccessibilityNotificationsForFrame:emacsframe]; ++ } + } + + /* Check for window switch (C-x o) before rebuild. */ @@ -934,16 +1043,17 @@ index 932d209..077f092 100644 + if (windowSwitched) + lastSelectedWindow = curSel; + -+ /* Now rebuild tree to pick up window configuration changes. */ -+ [self rebuildAccessibilityTree]; ++ /* Rebuild tree only if window configuration changed. */ ++ if (!accessibilityTreeValid) ++ [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); ++ NSAccessibilityPostNotification (focused, ++ NSAccessibilityFocusedUIElementChangedNotification); + } +} + @@ -953,11 +1063,6 @@ index 932d209..077f092 100644 + The cursor location is exposed through accessibilityBoundsForRange: + which AT tools query using the selectedTextRange. */ + -+- (NSRect)accessibilityFrame -+{ -+ return [super accessibilityFrame]; -+} -+ +- (NSRect)accessibilityBoundsForRange:(NSRange)range +{ + /* Return cursor screen rect. AT tools call this with the @@ -1020,7 +1125,7 @@ index 932d209..077f092 100644 @end /* EmacsView */ -@@ -9941,6 +10843,14 @@ - (id)accessibilityAttributeValue:(NSString *)attribute +@@ -9941,6 +10950,14 @@ nswindow_orderedIndex_sort (id w1, id w2, void *c) return [super accessibilityAttributeValue:attribute]; } diff --git a/patches/v15-accessibility.patch b/patches/v15-accessibility.patch deleted file mode 100644 index 6265cc7..0000000 --- a/patches/v15-accessibility.patch +++ /dev/null @@ -1,1142 +0,0 @@ -diff --git a/nsterm.h b/nsterm.h -index 7c1ee4c..8bf21f6 100644 ---- a/nsterm.h -+++ b/nsterm.h -@@ -453,6 +453,40 @@ 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; -+@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, retain) NSString *cachedText; -+@property (nonatomic, assign) ptrdiff_t cachedTextModiff; -+@property (nonatomic, assign) ptrdiff_t cachedTextStart; -+@property (nonatomic, assign) ptrdiff_t cachedModiff; -+@property (nonatomic, assign) ptrdiff_t cachedPoint; -+@property (nonatomic, assign) BOOL cachedMarkActive; -+- (void)invalidateTextCache; -+- (void)postAccessibilityNotificationsForFrame:(struct frame *)f; -+@end -+ -+/* Virtual AXStaticText element — one per mode line. */ -+@interface EmacsAccessibilityModeLine : EmacsAccessibilityElement -+@end -+#endif /* NS_IMPL_COCOA */ -+ -+ - /* ========================================================================== - - The main Emacs view -@@ -471,6 +505,12 @@ enum ns_return_frame_mode - #ifdef NS_IMPL_COCOA - char *old_title; - BOOL maximizing_resize; -+ NSMutableArray *accessibilityElements; -+ Lisp_Object lastSelectedWindow; -+ BOOL accessibilityTreeValid; -+ @public -+ NSRect lastAccessibilityCursorRect; -+ @protected - #endif - BOOL font_panel_active; - NSFont *font_panel_result; -@@ -528,6 +568,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/nsterm.m b/nsterm.m -index 932d209..dd134dd 100644 ---- a/nsterm.m -+++ b/nsterm.m -@@ -1104,6 +1104,11 @@ ns_update_end (struct frame *f) - - unblock_input (); - ns_updating_frame = NULL; -+ -+#ifdef NS_IMPL_COCOA -+ /* Post accessibility notifications after each redisplay cycle. */ -+ [view postAccessibilityUpdates]; -+#endif - } - - static void -@@ -3232,6 +3237,37 @@ ns_draw_window_cursor (struct window *w, struct glyph_row *glyph_row, - /* Prevent the cursor from being drawn outside the text area. */ - r = NSIntersectionRect (r, ns_row_rect (w, glyph_row, TEXT_AREA)); - -+#ifdef NS_IMPL_COCOA -+ /* Accessibility: store cursor rect for Zoom and bounds queries. -+ VoiceOver notifications are handled solely by -+ postAccessibilityUpdates (called from ns_update_end) -+ to avoid duplicate notifications and mid-redisplay fragility. */ -+ { -+ EmacsView *view = FRAME_NS_VIEW (f); -+ if (view && on_p && active_p) -+ { -+ view->lastAccessibilityCursorRect = r; -+ -+ /* Tell macOS Zoom where the cursor is. UAZoomChangeFocus() -+ expects top-left origin (CG coordinate space). */ -+ if (UAZoomEnabled ()) -+ { -+ NSRect windowRect = [view convertRect:r toView:nil]; -+ NSRect screenRect = [[view window] convertRectToScreen:windowRect]; -+ CGRect cgRect = NSRectToCGRect (screenRect); -+ -+ CGFloat primaryH -+ = [[[NSScreen screens] firstObject] frame].size.height; -+ cgRect.origin.y -+ = primaryH - cgRect.origin.y - cgRect.size.height; -+ -+ UAZoomChangeFocus (&cgRect, &cgRect, -+ kUAZoomFocusTypeInsertionPoint); -+ } -+ } -+ } -+#endif -+ - ns_focus (f, NULL, 0); - - NSGraphicsContext *ctx = [NSGraphicsContext currentContext]; -@@ -6847,6 +6883,713 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action) - } - #endif - -+/* ========================================================================== -+ -+ Accessibility virtual elements (macOS / Cocoa only) -+ -+ ========================================================================== */ -+ -+#ifdef NS_IMPL_COCOA -+ -+/* ---- Helper: extract buffer text for accessibility ---- */ -+ -+/* Maximum characters exposed via accessibilityValue. */ -+#define NS_AX_TEXT_CAP 100000 -+ -+static NSString * -+ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start) -+{ -+ 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); -+ ptrdiff_t len = zv - begv; -+ -+ /* Cap at NS_AX_TEXT_CAP characters, centered on point. */ -+ if (len > NS_AX_TEXT_CAP) -+ { -+ ptrdiff_t pt = BUF_PT (b); -+ ptrdiff_t half = NS_AX_TEXT_CAP / 2; -+ ptrdiff_t start = MAX (begv, pt - half); -+ ptrdiff_t end = MIN (zv, start + NS_AX_TEXT_CAP); -+ start = MAX (begv, end - NS_AX_TEXT_CAP); -+ begv = start; -+ zv = end; -+ } -+ -+ *out_start = begv; -+ -+ if (zv <= begv) -+ return @""; -+ -+ ptrdiff_t begv_byte = buf_charpos_to_bytepos (b, begv); -+ ptrdiff_t zv_byte = buf_charpos_to_bytepos (b, zv); -+ unsigned char *data = BUF_BYTE_ADDRESS (b, begv_byte); -+ ptrdiff_t nbytes = zv_byte - begv_byte; -+ -+ Lisp_Object lstr = make_string_from_bytes ((char *) data, -+ zv - begv, nbytes); -+ return [NSString stringWithLispString:lstr]; -+} -+ -+ -+/* ---- Helper: extract mode line text from glyph rows ---- */ -+ -+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 text_start, NSRange range) -+{ -+ if (!w || !w->current_matrix || !view) -+ return NSZeroRect; -+ -+ /* Convert range indices back to buffer charpos. */ -+ ptrdiff_t cp_start = text_start + (ptrdiff_t) range.location; -+ ptrdiff_t cp_end = cp_start + (ptrdiff_t) range.length; -+ -+ 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->visible_height; -+ -+ if (!found) -+ { -+ result = rowRect; -+ found = YES; -+ } -+ else -+ result = NSUnionRect (result, rowRect); -+ } -+ } -+ -+ if (!found) -+ return NSZeroRect; -+ -+ /* Convert from EmacsView (flipped) coords to screen coords. */ -+ NSRect winRect = [view convertRect:result toView:nil]; -+ return [[view window] convertRectToScreen:winRect]; -+} -+ -+ -+@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; -+} -+ -+/* ---- 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 -+ -+ -+@implementation EmacsAccessibilityBuffer -+ -+- (void)dealloc -+{ -+ [cachedText release]; -+ [super dealloc]; -+} -+ -+/* ---- Text cache ---- */ -+ -+- (void)invalidateTextCache -+{ -+ [cachedText release]; -+ cachedText = nil; -+} -+ -+- (void)ensureTextCache -+{ -+ struct window *w = self.emacsWindow; -+ if (!w || !WINDOW_LEAF_P (w)) -+ return; -+ -+ struct buffer *b = XBUFFER (w->contents); -+ if (!b) -+ return; -+ -+ ptrdiff_t modiff = BUF_MODIFF (b); -+ if (cachedText && cachedTextModiff == modiff) -+ return; -+ -+ ptrdiff_t start; -+ NSString *text = ns_ax_buffer_text (w, &start); -+ -+ [cachedText release]; -+ cachedText = [text retain]; -+ cachedTextModiff = modiff; -+ cachedTextStart = start; -+} -+ -+/* ---- NSAccessibility protocol ---- */ -+ -+- (NSAccessibilityRole)accessibilityRole -+{ -+ return NSAccessibilityTextAreaRole; -+} -+ -+- (NSString *)accessibilityRoleDescription -+{ -+ struct window *w = self.emacsWindow; -+ if (w && MINI_WINDOW_P (w)) -+ return @"minibuffer"; -+ return @"editor"; -+} -+ -+- (NSString *)accessibilityLabel -+{ -+ struct window *w = self.emacsWindow; -+ if (w && WINDOW_LEAF_P (w)) -+ { -+ if (MINI_WINDOW_P (w)) -+ return @"Minibuffer"; -+ -+ struct buffer *b = XBUFFER (w->contents); -+ if (b) -+ { -+ Lisp_Object name = BVAR (b, name); -+ if (STRINGP (name)) -+ return [NSString stringWithLispString:name]; -+ } -+ } -+ return @"buffer"; -+} -+ -+- (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)accessibilityValue -+{ -+ [self ensureTextCache]; -+ return cachedText ? cachedText : @""; -+} -+ -+- (NSInteger)accessibilityNumberOfCharacters -+{ -+ [self ensureTextCache]; -+ return cachedText ? [cachedText length] : 0; -+} -+ -+- (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 @""; -+ -+ NSRange sel = [self accessibilitySelectedTextRange]; -+ [self ensureTextCache]; -+ if (!cachedText || sel.location == NSNotFound -+ || sel.location + sel.length > [cachedText length]) -+ return @""; -+ return [cachedText 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); -+ -+ [self ensureTextCache]; -+ ptrdiff_t pt = BUF_PT (b); -+ NSUInteger point_idx = (NSUInteger) (pt - cachedTextStart); -+ -+ if (NILP (BVAR (b, mark_active))) -+ return NSMakeRange (point_idx, 0); -+ -+ ptrdiff_t mark_pos = marker_position (BVAR (b, mark)); -+ NSUInteger mark_idx = (NSUInteger) (mark_pos - cachedTextStart); -+ NSUInteger start_idx = MIN (point_idx, mark_idx); -+ NSUInteger end_idx = MAX (point_idx, mark_idx); -+ return NSMakeRange (start_idx, end_idx - start_idx); -+} -+ -+- (NSInteger)accessibilityInsertionPointLineNumber -+{ -+ struct window *w = self.emacsWindow; -+ if (!w || !WINDOW_LEAF_P (w)) -+ return 0; -+ -+ struct buffer *b = XBUFFER (w->contents); -+ if (!b) -+ return 0; -+ -+ [self ensureTextCache]; -+ if (!cachedText) -+ return 0; -+ -+ ptrdiff_t pt = BUF_PT (b); -+ NSUInteger point_idx = (NSUInteger) (pt - cachedTextStart); -+ if (point_idx > [cachedText length]) -+ point_idx = [cachedText length]; -+ -+ /* Count newlines from start to point_idx. */ -+ NSInteger line = 0; -+ for (NSUInteger i = 0; i < point_idx; i++) -+ { -+ if ([cachedText characterAtIndex:i] == '\n') -+ line++; -+ } -+ return line; -+} -+ -+- (NSString *)accessibilityStringForRange:(NSRange)range -+{ -+ [self ensureTextCache]; -+ if (!cachedText || range.location + range.length > [cachedText length]) -+ return @""; -+ return [cachedText substringWithRange:range]; -+} -+ -+- (NSAttributedString *)accessibilityAttributedStringForRange:(NSRange)range -+{ -+ NSString *str = [self accessibilityStringForRange:range]; -+ return [[[NSAttributedString alloc] initWithString:str] autorelease]; -+} -+ -+- (NSInteger)accessibilityLineForIndex:(NSInteger)index -+{ -+ [self ensureTextCache]; -+ if (!cachedText || index < 0) -+ return 0; -+ -+ NSUInteger idx = (NSUInteger) index; -+ if (idx > [cachedText length]) -+ idx = [cachedText length]; -+ -+ /* Count newlines from start of cachedText to idx. */ -+ NSInteger line = 0; -+ for (NSUInteger i = 0; i < idx; i++) -+ { -+ if ([cachedText characterAtIndex:i] == '\n') -+ line++; -+ } -+ return line; -+} -+ -+- (NSRange)accessibilityRangeForLine:(NSInteger)line -+{ -+ [self ensureTextCache]; -+ if (!cachedText || line < 0) -+ return NSMakeRange (NSNotFound, 0); -+ -+ NSUInteger len = [cachedText length]; -+ NSInteger cur_line = 0; -+ NSUInteger line_start = 0; -+ -+ for (NSUInteger i = 0; i <= len; i++) -+ { -+ if (cur_line == line) -+ { -+ /* Find end of this line. */ -+ NSUInteger line_end = i; -+ while (line_end < len -+ && [cachedText characterAtIndex:line_end] != '\n') -+ line_end++; -+ return NSMakeRange (i, line_end - i); -+ } -+ if (i < len && [cachedText characterAtIndex:i] == '\n') -+ { -+ cur_line++; -+ line_start = i + 1; -+ } -+ } -+ return NSMakeRange (NSNotFound, 0); -+} -+ -+- (NSRange)accessibilityRangeForIndex:(NSInteger)index -+{ -+ [self ensureTextCache]; -+ if (!cachedText || index < 0 -+ || (NSUInteger) index >= [cachedText length]) -+ return NSMakeRange (NSNotFound, 0); -+ return NSMakeRange ((NSUInteger) index, 1); -+} -+ -+- (NSRange)accessibilityStyleRangeForIndex:(NSInteger)index -+{ -+ /* Return the range of the current line — simple approach. */ -+ NSInteger line = [self accessibilityLineForIndex:index]; -+ return [self accessibilityRangeForLine: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, cachedTextStart, range); -+} -+ -+- (NSRange)accessibilityRangeForPosition:(NSPoint)screenPoint -+{ -+ /* Hit test: convert screen point to buffer character index. */ -+ 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 window-relative pixel coordinates. */ -+ 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; -+ -+ for (int i = 0; i < matrix->nrows; i++) -+ { -+ struct glyph_row *row = matrix->rows + i; -+ if (!row->enabled_p || !row->displays_text_p || row->mode_line_p) -+ continue; -+ int row_top = WINDOW_TO_FRAME_PIXEL_Y (w, MAX (0, row->y)); -+ if ((int) viewPoint.y >= row_top -+ && (int) viewPoint.y < row_top + row->visible_height) -+ { -+ hit_row = row; -+ break; -+ } -+ } -+ -+ 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. */ -+ [self ensureTextCache]; -+ NSUInteger ax_idx = (NSUInteger) (best_charpos - cachedTextStart); -+ if (cachedText && ax_idx > [cachedText length]) -+ ax_idx = [cachedText length]; -+ return NSMakeRange (ax_idx, 1); -+} -+ -+- (NSRange)accessibilityVisibleCharacterRange -+{ -+ struct window *w = self.emacsWindow; -+ if (!w || !w->current_matrix) -+ { -+ [self ensureTextCache]; -+ return NSMakeRange (0, cachedText ? [cachedText length] : 0); -+ } -+ -+ [self ensureTextCache]; -+ if (!cachedText) -+ return NSMakeRange (0, 0); -+ -+ /* Compute visible range from window start to last visible row. */ -+ ptrdiff_t vis_start = w->start_charpos; -+ ptrdiff_t vis_end = vis_start; -+ -+ struct glyph_matrix *matrix = w->current_matrix; -+ for (int i = matrix->nrows - 1; i >= 0; 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)) -+ { -+ vis_end = MATRIX_ROW_END_CHARPOS (row); -+ break; -+ } -+ } -+ -+ NSUInteger loc = (NSUInteger) (vis_start - cachedTextStart); -+ NSUInteger end = (NSUInteger) (vis_end - cachedTextStart); -+ NSUInteger text_len = [cachedText length]; -+ if (loc > text_len) loc = text_len; -+ if (end > text_len) end = text_len; -+ if (end < loc) end = loc; -+ -+ return NSMakeRange (loc, end - loc); -+} -+ -+- (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)postAccessibilityNotificationsForFrame:(struct frame *)f -+{ -+ struct window *w = self.emacsWindow; -+ 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); -+ BOOL markActive = !NILP (BVAR (b, mark_active)); -+ -+ /* --- Text changed → typing echo --- -+ kAXTextStateChangeTypeEdit = 0, kAXTextEditTypeTyping = 3. */ -+ if (modiff != self.cachedModiff) -+ { -+ /* Capture changed char before invalidating cache. */ -+ NSString *changedChar = @""; -+ if (point > self.cachedPoint -+ && point - self.cachedPoint == 1) -+ { -+ /* Single char inserted — refresh cache and grab it. */ -+ [self invalidateTextCache]; -+ [self ensureTextCache]; -+ if (cachedText) -+ { -+ NSUInteger idx = (NSUInteger) (point - 1 - cachedTextStart); -+ if (idx < [cachedText length]) -+ changedChar = [cachedText substringWithRange: -+ NSMakeRange (idx, 1)]; -+ } -+ } -+ else -+ { -+ [self invalidateTextCache]; -+ } -+ -+ self.cachedModiff = modiff; -+ -+ NSDictionary *change = @{ -+ @"AXTextEditType": @3, -+ @"AXTextChangeValue": changedChar, -+ @"AXTextChangeValueLength": @([changedChar length]) -+ }; -+ NSDictionary *userInfo = @{ -+ @"AXTextStateChangeType": @0, -+ @"AXTextChangeValues": @[change] -+ }; -+ NSAccessibilityPostNotificationWithUserInfo ( -+ self, NSAccessibilityValueChangedNotification, userInfo); -+ } -+ -+ /* --- Cursor moved or selection changed → line reading --- -+ kAXTextStateChangeTypeSelectionMove = 1. */ -+ if (point != self.cachedPoint || markActive != self.cachedMarkActive) -+ { -+ self.cachedPoint = point; -+ self.cachedMarkActive = markActive; -+ -+ NSDictionary *moveInfo = @{ -+ @"AXTextStateChangeType": @1, -+ @"AXTextSelectionDirection": @4, -+ @"AXTextSelectionGranularity": @3 -+ }; -+ NSAccessibilityPostNotificationWithUserInfo ( -+ self, -+ NSAccessibilitySelectedTextChangedNotification, -+ moveInfo); -+ } -+} -+ -+@end -+ -+ -+@implementation EmacsAccessibilityModeLine -+ -+- (NSAccessibilityRole)accessibilityRole -+{ -+ return NSAccessibilityStaticTextRole; -+} -+ -+- (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)) -+ { -+ NSString *bufName = [NSString stringWithLispString:name]; -+ return [NSString stringWithFormat:@"Mode Line - %@", bufName]; -+ } -+ } -+ } -+ return @"Mode Line"; -+} -+ -+- (id)accessibilityValue -+{ -+ struct window *w = self.emacsWindow; -+ if (!w) -+ return @""; -+ return ns_ax_mode_line_text (w); -+} -+ -+- (NSRect)accessibilityFrame -+{ -+ struct window *w = self.emacsWindow; -+ if (!w || !w->current_matrix) -+ return NSZeroRect; -+ -+ /* Find the mode line row and return its screen rect. */ -+ struct glyph_matrix *matrix = w->current_matrix; -+ for (int i = 0; i < matrix->nrows; i++) -+ { -+ struct glyph_row *row = matrix->rows + i; -+ if (row->enabled_p && row->mode_line_p) -+ { -+ return [self screenRectFromEmacsX:w->pixel_left -+ y:WINDOW_TO_FRAME_PIXEL_Y (w, -+ MAX (0, row->y)) -+ width:w->pixel_width -+ height:row->visible_height]; -+ } -+ } -+ return NSZeroRect; -+} -+ -+@end -+ -+#endif /* NS_IMPL_COCOA */ -+ -+ - /* ========================================================================== - - EmacsView implementation -@@ -6889,6 +7632,7 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action) - [layer release]; - #endif - -+ [accessibilityElements release]; - [[self menu] release]; - [super dealloc]; - } -@@ -8237,6 +8981,18 @@ ns_in_echo_area (void) - XSETFRAME (event.frame_or_window, emacsframe); - kbd_buffer_store_event (&event); - ns_send_appdefined (-1); // Kick main loop -+ -+#ifdef NS_IMPL_COCOA -+ /* Notify VoiceOver that the focused accessibility element changed. -+ Post on the focused virtual element so VoiceOver starts tracking it. -+ This is critical for initial focus and app-switch scenarios. */ -+ { -+ id focused = [self accessibilityFocusedUIElement]; -+ if (focused) -+ NSAccessibilityPostNotification (focused, -+ NSAccessibilityFocusedUIElementChangedNotification); -+ } -+#endif - } - - -@@ -9474,6 +10230,259 @@ ns_in_echo_area (void) - return fs_state; - } - -+#ifdef NS_IMPL_COCOA -+ -+/* ---- Accessibility: walk the Emacs window tree ---- */ -+ -+static void -+ns_ax_collect_windows (Lisp_Object window, EmacsView *view, -+ NSMutableArray *elements, -+ NSDictionary *existing) -+{ -+ if (NILP (window)) -+ return; -+ -+ struct window *w = XWINDOW (window); -+ -+ if (WINDOW_LEAF_P (w)) -+ { -+ /* Buffer element — reuse existing if available. */ -+ EmacsAccessibilityBuffer *elem -+ = [existing objectForKey:[NSValue valueWithPointer:w]]; -+ if (!elem) -+ { -+ elem = [[EmacsAccessibilityBuffer alloc] init]; -+ elem.emacsView = view; -+ -+ /* Initialize cached state to trigger first notification. */ -+ struct buffer *b = XBUFFER (w->contents); -+ if (b) -+ { -+ elem.cachedModiff = BUF_MODIFF (b); -+ elem.cachedPoint = BUF_PT (b); -+ elem.cachedMarkActive = !NILP (BVAR (b, mark_active)); -+ } -+ } -+ else -+ { -+ [elem retain]; -+ } -+ elem.emacsWindow = w; -+ [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.emacsWindow = w; -+ [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 -+{ -+ 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]] -+ && elem.emacsWindow) -+ [existing setObject:elem -+ forKey:[NSValue valueWithPointer: -+ elem.emacsWindow]]; -+ } -+ } -+ -+ 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]; -+ -+ struct window *sel = XWINDOW (emacsframe->selected_window); -+ for (EmacsAccessibilityElement *elem in accessibilityElements) -+ { -+ if ([elem isKindOfClass:[EmacsAccessibilityBuffer class]] -+ && 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 (EmacsAccessibilityElement *elem in accessibilityElements) -+ { -+ if ([elem isKindOfClass:[EmacsAccessibilityBuffer class]]) -+ { -+ struct window *w = elem.emacsWindow; -+ if (w && WINDOW_LEAF_P (w)) -+ [(EmacsAccessibilityBuffer *) elem -+ postAccessibilityNotificationsForFrame: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; -+ -+ /* Rebuild tree only if window configuration changed. */ -+ if (!accessibilityTreeValid) -+ [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); -+ } -+} -+ -+/* ---- 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 -+{ -+ /* Return cursor screen rect. AT tools call this with the -+ selectedTextRange to locate the insertion point. */ -+ NSRect viewRect = lastAccessibilityCursorRect; -+ -+ if (viewRect.size.width < 1) -+ viewRect.size.width = 1; -+ if (viewRect.size.height < 1) -+ viewRect.size.height = 8; -+ -+ NSWindow *win = [self window]; -+ if (win == nil) -+ return NSZeroRect; -+ -+ NSRect windowRect = [self convertRect:viewRect toView:nil]; -+ return [win convertRectToScreen:windowRect]; -+} -+ -+- (NSRect)accessibilityFrameForRange:(NSRange)range -+{ -+ return [self accessibilityBoundsForRange:range]; -+} -+ -+/* ---- 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 */ - - -@@ -9941,6 +10950,14 @@ nswindow_orderedIndex_sort (id w1, id w2, void *c) - - 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.