From a6a3aca678eca72a355d7546b4da19d6fdaba9b2 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Fri, 27 Feb 2026 15:47:21 +0100 Subject: [PATCH] remove some stale files --- ...sForRange-for-macOS-Zoom-cursor-.patch.bak | 1250 ----------------- 1 file changed, 1250 deletions(-) delete mode 100644 patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch.bak diff --git a/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch.bak b/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch.bak deleted file mode 100644 index 25f621a..0000000 --- a/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch.bak +++ /dev/null @@ -1,1250 +0,0 @@ -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. ---- ---- a/src/nsterm.h 2026-02-26 08:46:18.118172281 +0100 -+++ b/src/nsterm.h 2026-02-26 09:08:57.204955708 +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,12 @@ - #ifdef NS_IMPL_COCOA - char *old_title; - BOOL maximizing_resize; -+ NSMutableArray *accessibilityElements; -+ Lisp_Object lastSelectedWindow; -+ @public -+ NSRect lastAccessibilityCursorRect; -+ ptrdiff_t lastAccessibilityModiff; -+ @protected - #endif - BOOL font_panel_active; - NSFont *font_panel_result; -@@ -528,6 +562,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 10:46:54.607568839 +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 -@@ -3232,6 +3237,82 @@ - /* 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 cursor tracking for macOS Zoom and VoiceOver. -+ Only notify AT when drawing the cursor in the active (selected) -+ window. Without this guard, C-x o triggers UAZoomChangeFocus -+ for the old window last, snapping Zoom back. */ -+ { -+ EmacsView *view = FRAME_NS_VIEW (f); -+ if (view && on_p && active_p) -+ { -+ /* Store cursor rect for accessibilityBoundsForRange: queries. */ -+ view->lastAccessibilityCursorRect = r; -+ -+ /* Find the focused virtual element — VoiceOver tracks IT, -+ not the EmacsView (AXGroup). Notifications must come from -+ the element VoiceOver is monitoring. */ -+ id axTarget = [view accessibilityFocusedUIElement]; -+ if (!axTarget) -+ axTarget = view; -+ -+ struct buffer *curbuf -+ = XBUFFER (XWINDOW (f->selected_window)->contents); -+ -+ if (curbuf && BUF_MODIFF (curbuf) != view->lastAccessibilityModiff) -+ { -+ /* Buffer content changed — typing echo. Post ValueChanged -+ with rich userInfo on the FOCUSED ELEMENT. -+ kAXTextStateChangeTypeEdit = 1, kAXTextEditTypeTyping = 3. */ -+ view->lastAccessibilityModiff = BUF_MODIFF (curbuf); -+ -+ NSString *changedText = @""; -+ ptrdiff_t pt = BUF_PT (curbuf); -+ if (pt > BUF_BEGV (curbuf)) -+ { -+ NSRange charRange = NSMakeRange ( -+ (NSUInteger)(pt - BUF_BEGV (curbuf) - 1), 1); -+ changedText = [view accessibilityStringForRange:charRange]; -+ if (!changedText) -+ changedText = @""; -+ } -+ -+ NSDictionary *change = @{ -+ @"AXTextEditType": @3, -+ @"AXTextChangeValue": changedText -+ }; -+ NSDictionary *userInfo = @{ -+ @"AXTextStateChangeType": @1, -+ @"AXTextChangeValues": @[change] -+ }; -+ NSAccessibilityPostNotificationWithUserInfo ( -+ axTarget, NSAccessibilityValueChangedNotification, userInfo); -+ } -+ -+ /* Always notify cursor movement on the focused element. */ -+ NSAccessibilityPostNotification ( -+ axTarget, NSAccessibilitySelectedTextChangedNotification); -+ -+ /* 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]; -@@ -6849,6 +6930,646 @@ - - /* ========================================================================== - -+ 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; -+} -+ -+/* ---- 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 -+ -+/* ---- 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"; -+} -+ -+- (BOOL)isAccessibilityFocused -+{ -+ /* Return YES when this buffer's window is the selected window. */ -+ 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 -+{ -+ 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]; -+} -+ -+- (NSAttributedString *)accessibilityAttributedStringForRange:(NSRange)range -+{ -+ NSString *str = [self accessibilityStringForRange:range]; -+ return [[[NSAttributedString alloc] initWithString:str] autorelease]; -+} -+ -+- (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]); -+} -+ -+- (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. -+ kAXTextStateChangeTypeEdit = 1, kAXTextEditTypeTyping = 3. -+ Must include AXTextChangeValues array for VoiceOver to speak. */ -+ NSString *changedText = @""; -+ ptrdiff_t pt = BUF_PT (b); -+ if (pt > BUF_BEGV (b)) -+ { -+ NSRange charRange = NSMakeRange ( -+ (NSUInteger)(pt - BUF_BEGV (b) - 1), 1); -+ changedText = [self accessibilityStringForRange:charRange]; -+ if (!changedText) -+ changedText = @""; -+ } -+ -+ NSDictionary *change = @{ -+ @"AXTextEditType": @3, -+ @"AXTextChangeValue": changedText -+ }; -+ NSDictionary *userInfo = @{ -+ @"AXTextStateChangeType": @1, -+ @"AXTextChangeValues": @[change] -+ }; -+ 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 +7610,7 @@ - [layer release]; - #endif - -+ [accessibilityElements release]; - [[self menu] release]; - [super dealloc]; - } -@@ -8237,6 +8959,18 @@ - 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 +10208,391 @@ - 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); -+ } -+} -+ -+/* ---- 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)accessibilityFrame -+{ -+ return [super accessibilityFrame]; -+} -+ -+- (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]; -+} -+ -+/* ---- Text content methods (for Zoom and legacy AT) ---- */ -+ -+- (id)accessibilityValue -+{ -+ if (!emacsframe) -+ return @""; -+ -+ struct buffer *curbuf -+ = XBUFFER (XWINDOW (emacsframe->selected_window)->contents); -+ if (!curbuf) -+ return @""; -+ -+ ptrdiff_t start_byte = BUF_BEGV_BYTE (curbuf); -+ ptrdiff_t byte_range = BUF_ZV_BYTE (curbuf) - start_byte; -+ ptrdiff_t range = BUF_ZV (curbuf) - BUF_BEGV (curbuf); -+ -+ if (range > 10000) -+ { -+ range = 10000; -+ ptrdiff_t end_byte = buf_charpos_to_bytepos (curbuf, -+ BUF_BEGV (curbuf) + range); -+ byte_range = end_byte - start_byte; -+ } -+ -+ Lisp_Object str; -+ if (! NILP (BVAR (curbuf, enable_multibyte_characters))) -+ str = make_uninit_multibyte_string (range, byte_range); -+ else -+ str = make_uninit_string (range); -+ memcpy (SDATA (str), BYTE_POS_ADDR (start_byte), byte_range); -+ -+ return [NSString stringWithLispString:str]; -+} -+ -+- (NSRange)accessibilitySelectedTextRange -+{ -+ if (!emacsframe) -+ return NSMakeRange (0, 0); -+ -+ struct buffer *curbuf -+ = XBUFFER (XWINDOW (emacsframe->selected_window)->contents); -+ if (!curbuf) -+ return NSMakeRange (0, 0); -+ -+ ptrdiff_t pt = BUF_PT (curbuf) - BUF_BEGV (curbuf); -+ return NSMakeRange ((NSUInteger) pt, 0); -+} -+ -+- (NSString *)accessibilityStringForRange:(NSRange)nsrange -+{ -+ if (!emacsframe) -+ return @""; -+ -+ struct buffer *curbuf -+ = XBUFFER (XWINDOW (emacsframe->selected_window)->contents); -+ if (!curbuf) -+ return @""; -+ -+ ptrdiff_t start = BUF_BEGV (curbuf) + (ptrdiff_t) nsrange.location; -+ ptrdiff_t end = start + (ptrdiff_t) nsrange.length; -+ ptrdiff_t buf_end = BUF_ZV (curbuf); -+ -+ if (start < BUF_BEGV (curbuf)) start = BUF_BEGV (curbuf); -+ if (end > buf_end) end = buf_end; -+ if (start >= end) return @""; -+ -+ ptrdiff_t start_byte = buf_charpos_to_bytepos (curbuf, start); -+ ptrdiff_t end_byte = buf_charpos_to_bytepos (curbuf, end); -+ ptrdiff_t char_range = end - start; -+ ptrdiff_t brange = end_byte - start_byte; -+ -+ Lisp_Object str; -+ if (! NILP (BVAR (curbuf, enable_multibyte_characters))) -+ str = make_uninit_multibyte_string (char_range, brange); -+ else -+ str = make_uninit_string (char_range); -+ memcpy (SDATA (str), BUF_BYTE_ADDRESS (curbuf, start_byte), brange); -+ -+ return [NSString stringWithLispString:str]; -+} -+ -+- (NSInteger)accessibilityNumberOfCharacters -+{ -+ if (!emacsframe) -+ return 0; -+ struct buffer *curbuf -+ = XBUFFER (XWINDOW (emacsframe->selected_window)->contents); -+ if (!curbuf) -+ return 0; -+ ptrdiff_t range = BUF_ZV (curbuf) - BUF_BEGV (curbuf); -+ return (NSInteger) MIN (range, 10000); -+} -+ -+- (NSString *)accessibilitySelectedText -+{ -+ if (!emacsframe) -+ return @""; -+ struct buffer *curbuf -+ = XBUFFER (XWINDOW (emacsframe->selected_window)->contents); -+ if (!curbuf || NILP (BVAR (curbuf, mark_active))) -+ return @""; -+ return @""; -+} -+ -+- (NSInteger)accessibilityInsertionPointLineNumber -+{ -+ if (!emacsframe) -+ return 0; -+ struct window *w = XWINDOW (emacsframe->selected_window); -+ if (!w) -+ return 0; -+ return (NSInteger) (w->cursor.vpos); -+} -+ -+- (NSRange)accessibilityVisibleCharacterRange -+{ -+ if (!emacsframe) -+ return NSMakeRange (0, 0); -+ struct buffer *curbuf -+ = XBUFFER (XWINDOW (emacsframe->selected_window)->contents); -+ if (!curbuf) -+ return NSMakeRange (0, 0); -+ ptrdiff_t range = BUF_ZV (curbuf) - BUF_BEGV (curbuf); -+ return NSMakeRange (0, (NSUInteger) MIN (range, 10000)); -+} -+ -+- (NSAttributedString *)accessibilityAttributedStringForRange:(NSRange)range -+{ -+ NSString *str = [self accessibilityStringForRange:range]; -+ return [[[NSAttributedString alloc] initWithString:str] autorelease]; -+} -+ -+- (NSInteger)accessibilityLineForIndex:(NSInteger)index -+{ -+ if (!emacsframe) -+ return 0; -+ struct window *w = XWINDOW (emacsframe->selected_window); -+ if (!w || !w->current_matrix) -+ return 0; -+ struct buffer *curbuf = XBUFFER (w->contents); -+ if (!curbuf) -+ return 0; -+ ptrdiff_t charpos = BUF_BEGV (curbuf) + (ptrdiff_t) index; -+ 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) -+ continue; -+ if (MATRIX_ROW_START_CHARPOS (row) <= charpos -+ && charpos < MATRIX_ROW_END_CHARPOS (row)) -+ return (NSInteger) i; -+ } -+ return 0; -+} -+ -+- (NSRange)accessibilityRangeForLine:(NSInteger)line -+{ -+ if (!emacsframe) -+ return NSMakeRange (0, 0); -+ struct window *w = XWINDOW (emacsframe->selected_window); -+ if (!w || !w->current_matrix) -+ return NSMakeRange (0, 0); -+ struct buffer *curbuf = XBUFFER (w->contents); -+ if (!curbuf) -+ return NSMakeRange (0, 0); -+ struct glyph_matrix *matrix = w->current_matrix; -+ if (line < 0 || line >= matrix->nrows) -+ return NSMakeRange (0, 0); -+ struct glyph_row *row = matrix->rows + line; -+ if (!row->enabled_p) -+ return NSMakeRange (0, 0); -+ ptrdiff_t start = MATRIX_ROW_START_CHARPOS (row) - BUF_BEGV (curbuf); -+ ptrdiff_t end = MATRIX_ROW_END_CHARPOS (row) - BUF_BEGV (curbuf); -+ if (start < 0) start = 0; -+ if (end < start) end = start; -+ return NSMakeRange ((NSUInteger) start, (NSUInteger) (end - start)); -+} -+ -+/* ---- 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 +11060,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.