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 029edd4..36809b0 100644 --- a/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch +++ b/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch @@ -1,86 +1,103 @@ From: Martin Sukany -Date: Tue, 25 Feb 2026 21:00:00 +0100 -Subject: [PATCH] ns: implement macOS Zoom cursor tracking and VoiceOver - support +Date: Wed, 26 Feb 2026 00:00:00 +0100 +Subject: [PATCH] ns: add macOS Zoom cursor tracking and VoiceOver accessibility -Add comprehensive accessibility support for macOS: - -1. UAZoomChangeFocus() from ApplicationServices/UniversalAccess.h: - Directly tells macOS Zoom where the cursor is. Only fires for the - active window cursor draw (on_p && active_p guard). - Ref: https://developer.apple.com/documentation/applicationservices/1458830-uazoomchangefocus - -2. NSAccessibility protocol on EmacsView (macOS 10.10+): - Full text area protocol for VoiceOver: - - Rich typing echo via NSAccessibilityPostNotificationWithUserInfo - with kAXTextEditTypeTyping (requires macOS 10.11+, falls back to - bare notification on 10.10) - - BUF_MODIFF tracking to distinguish content edits from cursor movement - - Text content: accessibilityValue, accessibilitySelectedText, - accessibilityNumberOfCharacters, accessibilityStringForRange: - - Text navigation: accessibilityLineForIndex:, accessibilityRangeForLine:, - accessibilityInsertionPointLineNumber, accessibilityVisibleCharacterRange - - Cursor geometry: accessibilityBoundsForRange:, accessibilityFrameForRange: - - Notifications: ValueChanged (edit), SelectedTextChanged (cursor), - FocusedUIElementChanged (window focus) - Ref: https://developer.apple.com/documentation/appkit/nsaccessibilityprotocol - -Uses raw string literals ("AXTextStateChangeType" etc.) for semi-private -notification keys to avoid type conflicts between SDK versions (these -symbols were added to public AppKit headers in macOS 26). +Dual accessibility support: +1. UAZoomChangeFocus + accessibilityBoundsForRange on EmacsView for Zoom +2. Virtual element tree (EmacsAccessibilityBuffer) for VoiceOver +3. Typing echo from ns_draw_window_cursor on EmacsView +4. Full hierarchy plumbing: accessibilityWindow, accessibilityTopLevelUIElement, + accessibilityParent, isAccessibilityFocused, FocusedUIElementChanged +MRC compatible. --- -diff --git a/src/nsterm.h b/src/nsterm.h -index 7c1ee4c..5e5f61b 100644 ---- a/src/nsterm.h -+++ b/src/nsterm.h -@@ -485,6 +485,10 @@ enum ns_return_frame_mode - struct frame *emacsframe; - int scrollbarsNeedingUpdate; - NSRect ns_userRect; +--- 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:19:39.112307036 +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 } - /* AppKit-side interface. */ -diff --git a/src/nsterm.m b/src/nsterm.m -index 932d209..6bfc080 100644 ---- a/src/nsterm.m -+++ b/src/nsterm.m -@@ -3232,6 +3232,115 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors. + static void +@@ -3232,6 +3237,75 @@ /* 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 -+ /* NSAccessibility keys for rich text change notifications. -+ Used by WebKit/Safari for VoiceOver typing echo. These symbols -+ have been exported by AppKit since macOS 10.11 but were only -+ added to the public headers in the macOS 26 SDK. We use raw -+ string literals here to avoid redeclaration type conflicts -+ across different SDK versions. */ -+ + /* Accessibility cursor tracking for macOS Zoom and VoiceOver. -+ -+ Emacs uses a custom-drawn cursor in a custom NSView. AppKit has no -+ knowledge of cursor position, so we must explicitly notify assistive -+ technology. Two complementary mechanisms: -+ -+ 1. NSAccessibility notifications (VoiceOver, screen readers): -+ - ValueChangedNotification with rich userInfo: tells VoiceOver -+ WHAT changed (typed character) so it can do typing echo. -+ We track BUF_MODIFF to distinguish content edits from cursor -+ movement. Only content changes get ValueChanged. -+ - SelectedTextChangedNotification: tells AT the cursor moved. -+ -+ 2. UAZoomChangeFocus() from UniversalAccess.h: -+ Directly tells macOS Zoom where to move its viewport. -+ Ref: https://developer.apple.com/documentation/applicationservices/1458830-uazoomchangefocus */ ++ 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); -+ /* 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. */ + if (view && on_p && active_p) + { + /* Store cursor rect for accessibilityBoundsForRange: queries. */ @@ -91,53 +108,35 @@ index 932d209..6bfc080 100644 + + if (curbuf && BUF_MODIFF (curbuf) != view->lastAccessibilityModiff) + { -+ /* Buffer content changed since last cursor draw — this is -+ an edit (typing, yank, undo, etc.). Post ValueChanged -+ with rich userInfo so VoiceOver can do typing echo. -+ -+ The semi-private keys (NSAccessibilityTextStateChangeTypeKey -+ etc.) are NSString* extern constants available in AppKit -+ since macOS 10.11. WebKit uses the same approach. -+ Ref: WebKit AXObjectCacheMac.mm */ ++ /* Buffer content changed — typing echo. Post ValueChanged ++ with rich userInfo on the VIEW so VoiceOver can speak. ++ kAXTextStateChangeTypeEdit = 1, kAXTextEditTypeTyping = 3. */ + view->lastAccessibilityModiff = BUF_MODIFF (curbuf); + -+ if (@available (macOS 10.11, *)) ++ NSString *changedText = @""; ++ ptrdiff_t pt = BUF_PT (curbuf); ++ if (pt > BUF_BEGV (curbuf)) + { -+ /* Get the just-typed character: char at cursor - 1. */ -+ 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 = @""; -+ } -+ -+ /* These are semi-private AppKit constants (available since -+ 10.11). The enum values match Apple's AX API headers: -+ kAXTextStateChangeTypeEdit = 1, kAXTextEditTypeTyping = 3. */ -+ -+ NSDictionary *change = @{ -+ @"AXTextEditType": @3, -+ @"AXTextChangeValue": changedText -+ }; -+ NSDictionary *userInfo = @{ -+ @"AXTextStateChangeType": @1, -+ @"AXTextChangeValues": @[change] -+ }; -+ NSAccessibilityPostNotificationWithUserInfo ( -+ view, NSAccessibilityValueChangedNotification, userInfo); -+ } -+ else -+ { -+ /* Fallback for macOS < 10.11: bare notification. */ -+ NSAccessibilityPostNotification ( -+ view, NSAccessibilityValueChangedNotification); ++ 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 ( ++ view, NSAccessibilityValueChangedNotification, userInfo); + } + -+ /* Always notify that cursor position (selection) changed. */ ++ /* Always notify cursor movement. */ + NSAccessibilityPostNotification ( + view, NSAccessibilitySelectedTextChangedNotification); + @@ -164,51 +163,318 @@ index 932d209..6bfc080 100644 ns_focus (f, NULL, 0); NSGraphicsContext *ctx = [NSGraphicsContext currentContext]; -@@ -8237,6 +8346,14 @@ - (void)windowDidBecomeKey /* for direct calls */ - XSETFRAME (event.frame_or_window, emacsframe); - kbd_buffer_store_event (&event); - ns_send_appdefined (-1); // Kick main loop +@@ -6849,6 +6923,650 @@ + + /* ========================================================================== + ++ Accessibility virtual elements (macOS / Cocoa only) ++ ++ ========================================================================== */ + +#ifdef NS_IMPL_COCOA -+ /* Notify assistive technology that the focused UI element changed. -+ macOS Zoom uses this to activate keyboard focus tracking; VoiceOver -+ uses it to announce the newly focused element. */ -+ NSAccessibilityPostNotification (self, -+ NSAccessibilityFocusedUIElementChangedNotification); -+#endif - } - - -@@ -9474,6 +9591,421 @@ - (int) fullscreenState - return fs_state; - } - + -+ -+#ifdef NS_IMPL_COCOA -+/* ---------------------------------------------------------------- -+ Accessibility support for macOS Zoom, VoiceOver, and other AT tools. -+ -+ EmacsView implements the NSAccessibility protocol so that: -+ - macOS Zoom can query cursor position (accessibilityBoundsForRange:) -+ - VoiceOver can read buffer contents, track cursor, echo typing -+ - Accessibility Inspector shows correct element hierarchy -+ -+ accessibilityFrame returns the VIEW's frame (standard behavior). -+ VoiceOver uses this for its focus ring around the entire text area. -+ The CURSOR position is exposed via accessibilityBoundsForRange: -+ which AT tools call with selectedTextRange to locate the insertion -+ point. Returning cursor rect from accessibilityFrame causes a -+ duplicate cursor overlay. -+ -+ Ref: https://developer.apple.com/documentation/appkit/nsaccessibilityprotocol -+ Ref: https://developer.apple.com/documentation/appkit/accessibility/nsaccessibility/text-specific_parameterized_attributes -+ ---------------------------------------------------------------- */ -+ -+- (BOOL)accessibilityIsIgnored ++/* ---- Helper: extract visible text from glyph rows of a window ---- */ ++static NSString * ++ns_ax_text_from_glyph_rows (struct window *w) +{ -+ /* Must return NO so assistive technology discovers this view. */ -+ return NO; ++ 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 @@ -216,32 +482,544 @@ index 932d209..6bfc080 100644 + return YES; +} + -+- (id)accessibilityFocusedUIElement ++/* ---- Hierarchy plumbing (required for VoiceOver to find us) ---- */ ++ ++- (id)accessibilityParent +{ -+ /* EmacsView is the focused element — there are no child accessible -+ elements within the custom-drawn view. */ -+ return self; ++ return NSAccessibilityUnignoredAncestor (self.emacsView); +} + -+- (NSString *)accessibilityRole ++- (id)accessibilityWindow ++{ ++ return [self.emacsView window]; ++} ++ ++- (id)accessibilityTopLevelUIElement ++{ ++ return [self.emacsView window]; ++} ++ ++@end ++ ++ ++@implementation EmacsAccessibilityBuffer ++ ++/* ---- NSAccessibility protocol ---- */ ++ ++- (NSAccessibilityRole)accessibilityRole +{ -+ /* TextArea role enables VoiceOver's text navigation commands -+ (VO+arrows for character/word/line reading). */ + return NSAccessibilityTextAreaRole; +} + +- (NSString *)accessibilityRoleDescription +{ -+ return NSAccessibilityRoleDescription (NSAccessibilityTextAreaRole, nil); ++ return @"editor"; +} + -+/* ---- Text content methods for VoiceOver ---- */ ++- (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)) ++ { ++ EmacsView *view = self.emacsView; ++ if (view) ++ { ++ NSRange charRange = NSMakeRange ( ++ (NSUInteger)(pt - BUF_BEGV (b) - 1), 1); ++ changedText = [view 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 +7607,7 @@ + [layer release]; + #endif + ++ [accessibilityElements release]; + [[self menu] release]; + [super dealloc]; + } +@@ -8237,6 +8956,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 +10205,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 +{ -+ /* Return visible buffer text (capped at 10000 chars for safety). -+ VoiceOver reads this when navigating to the text area and after -+ ValueChangedNotification. */ + if (!emacsframe) + return @""; + @@ -254,10 +1032,6 @@ index 932d209..6bfc080 100644 + ptrdiff_t byte_range = BUF_ZV_BYTE (curbuf) - start_byte; + ptrdiff_t range = BUF_ZV (curbuf) - BUF_BEGV (curbuf); + -+ /* Cap at 10000 characters to avoid performance issues with large -+ buffers. Recompute byte_range from the capped char range to -+ avoid truncating multibyte sequences (UTF-8 chars can be up to -+ 4 bytes, so byte_range != range for non-ASCII content). */ + if (range > 10000) + { + range = 10000; @@ -276,49 +1050,8 @@ index 932d209..6bfc080 100644 + 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 -+{ -+ /* Return text of the active region (Emacs selection). Empty string -+ if no mark is active. */ -+ if (!emacsframe) -+ return @""; -+ -+ struct buffer *curbuf -+ = XBUFFER (XWINDOW (emacsframe->selected_window)->contents); -+ if (!curbuf || NILP (BVAR (curbuf, mark_active))) -+ return @""; -+ -+ Lisp_Object str = ns_get_local_selection (QPRIMARY, QUTF8_STRING); -+ if (CONSP (str) && SYMBOLP (XCAR (str))) -+ { -+ str = XCDR (str); -+ if (CONSP (str) && NILP (XCDR (str))) -+ str = XCAR (str); -+ } -+ if (STRINGP (str)) -+ return [NSString stringWithLispString:str]; -+ -+ return @""; -+} -+ +- (NSRange)accessibilitySelectedTextRange +{ -+ /* Return cursor position as a zero-length range. VoiceOver uses -+ this with accessibilityBoundsForRange: to locate the caret. */ + if (!emacsframe) + return NSMakeRange (0, 0); + @@ -331,40 +1064,8 @@ index 932d209..6bfc080 100644 + return NSMakeRange ((NSUInteger) pt, 0); +} + -+- (NSInteger)accessibilityInsertionPointLineNumber -+{ -+ /* Return the visual line number of the cursor (vpos = line within -+ the window). VoiceOver uses this for line navigation. */ -+ if (!emacsframe) -+ return 0; -+ -+ struct window *w = XWINDOW (emacsframe->selected_window); -+ if (!w) -+ return 0; -+ -+ return (NSInteger) (w->cursor.vpos); -+} -+ -+- (NSRange)accessibilityVisibleCharacterRange -+{ -+ /* Return range of characters visible in the current window. -+ Simplified to 0..min(bufsize,10000) matching accessibilityValue. */ -+ 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)); -+} -+ +- (NSString *)accessibilityStringForRange:(NSRange)nsrange +{ -+ /* Return buffer text for the given character range. VoiceOver calls -+ this during character/word/line reading (VO+arrow keys). */ + if (!emacsframe) + return @""; + @@ -383,49 +1084,82 @@ index 932d209..6bfc080 100644 + + ptrdiff_t start_byte = buf_charpos_to_bytepos (curbuf, start); + ptrdiff_t end_byte = buf_charpos_to_bytepos (curbuf, end); -+ ptrdiff_t range = end - start; -+ ptrdiff_t byte_range = end_byte - start_byte; ++ 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 (range, byte_range); ++ str = make_uninit_multibyte_string (char_range, brange); + else -+ str = make_uninit_string (range); -+ memcpy (SDATA (str), BUF_BYTE_ADDRESS (curbuf, start_byte), byte_range); ++ str = make_uninit_string (char_range); ++ memcpy (SDATA (str), BUF_BYTE_ADDRESS (curbuf, start_byte), brange); + + return [NSString stringWithLispString:str]; +} + -+- (NSAttributedString *)accessibilityAttributedStringForRange:(NSRange)nsrange ++- (NSInteger)accessibilityNumberOfCharacters +{ -+ /* Return attributed string for the range. VoiceOver requires this -+ for text navigation. We return a plain (unstyled) attributed -+ string — sufficient for screen reader use. */ -+ NSString *str = [self accessibilityStringForRange:nsrange]; -+ return [[NSAttributedString alloc] initWithString:str]; ++ 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 +{ -+ /* Convert character index to visual line number. Used by VoiceOver -+ for line-by-line reading (VO+Up/Down). -+ -+ We walk the window's glyph matrix to find which row contains the -+ given buffer position. Falls back to 0 if not found. */ + 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; @@ -435,136 +1169,33 @@ index 932d209..6bfc080 100644 + && charpos < MATRIX_ROW_END_CHARPOS (row)) + return (NSInteger) i; + } -+ + return 0; +} + +- (NSRange)accessibilityRangeForLine:(NSInteger)line +{ -+ /* Return the character range for a visual line. VoiceOver uses -+ this to read an entire line when navigating by 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)); +} + -+- (NSRect)accessibilityFrameForRange:(NSRange)range -+{ -+ /* Return screen rect for a character range. This is the modern -+ NSAccessibilityProtocol equivalent of accessibilityBoundsForRange:. -+ We delegate to the same implementation. */ -+ return [self accessibilityBoundsForRange:range]; -+} -+ -+- (NSRange)accessibilityRangeForPosition:(NSPoint)point -+{ -+ /* Return character range at a screen point. VoiceOver uses this -+ for mouse-based exploration. Stub: returns empty range at 0. -+ A full implementation would need hit-testing against the glyph -+ matrix, which Emacs doesn't expose to AppKit. */ -+ (void) point; -+ return NSMakeRange (0, 0); -+} -+ -+/* ---- Cursor position for Zoom (via accessibilityBoundsForRange:) ---- -+ -+ accessibilityFrame intentionally returns the VIEW's frame (standard -+ behavior) so VoiceOver draws its focus ring around the text area. -+ The cursor location is exposed through accessibilityBoundsForRange: -+ which AT tools query using the selectedTextRange. */ -+ -+- (NSRect)accessibilityFrame -+{ -+ /* View's screen frame — standard NSView behavior. Do NOT return -+ the cursor rect here; that causes a duplicate cursor overlay. */ -+ return [super accessibilityFrame]; -+} -+ -+- (NSRect)accessibilityBoundsForRange:(NSRange)range -+{ -+ /* Return cursor screen rect. AT tools call this with the -+ selectedTextRange to locate the insertion point. We return the -+ cursor position regardless of requested range because Emacs does -+ not expose character-level geometry to AppKit. */ -+ 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]; -+} -+ -+/* ---- Legacy attribute APIs (pre-10.10 compatibility) ---- */ -+ -+- (NSArray *)accessibilityAttributeNames -+{ -+ NSArray *superAttrs = [super accessibilityAttributeNames]; -+ if (superAttrs == nil) -+ superAttrs = @[]; -+ return [superAttrs arrayByAddingObjectsFromArray: -+ @[NSAccessibilityRoleAttribute, -+ NSAccessibilityValueAttribute, -+ NSAccessibilitySelectedTextAttribute, -+ NSAccessibilitySelectedTextRangeAttribute, -+ NSAccessibilityNumberOfCharactersAttribute, -+ NSAccessibilityVisibleCharacterRangeAttribute, -+ NSAccessibilityInsertionPointLineNumberAttribute]]; -+} -+ -+- (id)accessibilityAttributeValue:(NSString *)attribute -+{ -+ if ([attribute isEqualToString:NSAccessibilityRoleAttribute]) -+ return NSAccessibilityTextAreaRole; -+ -+ if ([attribute isEqualToString:NSAccessibilityValueAttribute]) -+ return [self accessibilityValue]; -+ -+ if ([attribute isEqualToString:NSAccessibilitySelectedTextAttribute]) -+ return [self accessibilitySelectedText]; -+ -+ if ([attribute isEqualToString:NSAccessibilitySelectedTextRangeAttribute]) -+ return [NSValue valueWithRange:[self accessibilitySelectedTextRange]]; -+ -+ if ([attribute isEqualToString:NSAccessibilityNumberOfCharactersAttribute]) -+ return @([self accessibilityNumberOfCharacters]); -+ -+ if ([attribute isEqualToString:NSAccessibilityVisibleCharacterRangeAttribute]) -+ return [NSValue valueWithRange:[self accessibilityVisibleCharacterRange]]; -+ -+ if ([attribute isEqualToString:NSAccessibilityInsertionPointLineNumberAttribute]) -+ return @([self accessibilityInsertionPointLineNumber]); -+ -+ return [super accessibilityAttributeValue:attribute]; -+} ++/* ---- Legacy parameterized attribute APIs (Zoom uses these) ---- */ + +- (NSArray *)accessibilityParameterizedAttributeNames +{ @@ -596,8 +1227,24 @@ index 932d209..6bfc080 100644 + + return [super accessibilityAttributeValue:attribute forParameter:parameter]; +} ++ +#endif /* NS_IMPL_COCOA */ + @end /* EmacsView */ +@@ -9941,6 +11057,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.