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 bbc123c..029edd4 100644 --- a/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch +++ b/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch @@ -1,98 +1,86 @@ From: Martin Sukany -Date: Wed, 26 Feb 2026 00:00:00 +0100 -Subject: [PATCH] ns: add macOS Zoom cursor tracking and VoiceOver accessibility +Date: Tue, 25 Feb 2026 21:00:00 +0100 +Subject: [PATCH] ns: implement macOS Zoom cursor tracking and VoiceOver + support -Dual accessibility: UAZoomChangeFocus + virtual element tree + typing echo. -MRC compatible (unsafe_unretained, proper retain/release). +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). --- ---- 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) -+ -+ ========================================================================== */ -+ +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; +#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 09:23:14.570493387 +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,75 @@ + /* 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. /* 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. -+ 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. */ ++ ++ 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 */ + { + 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. */ @@ -103,35 +91,53 @@ MRC compatible (unsafe_unretained, proper retain/release). + + if (curbuf && BUF_MODIFF (curbuf) != view->lastAccessibilityModiff) + { -+ /* Buffer content changed — typing echo. Post ValueChanged -+ with rich userInfo on the VIEW so VoiceOver can speak. -+ kAXTextStateChangeTypeEdit = 1, kAXTextEditTypeTyping = 3. */ ++ /* 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 */ + view->lastAccessibilityModiff = BUF_MODIFF (curbuf); + -+ NSString *changedText = @""; -+ ptrdiff_t pt = BUF_PT (curbuf); -+ if (pt > BUF_BEGV (curbuf)) ++ if (@available (macOS 10.11, *)) + { -+ NSRange charRange = NSMakeRange ( -+ (NSUInteger)(pt - BUF_BEGV (curbuf) - 1), 1); -+ changedText = [view accessibilityStringForRange:charRange]; -+ if (!changedText) -+ changedText = @""; -+ } ++ /* 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 = @""; ++ } + -+ NSDictionary *change = @{ -+ @"AXTextEditType": @3, -+ @"AXTextChangeValue": changedText -+ }; -+ NSDictionary *userInfo = @{ -+ @"AXTextStateChangeType": @1, -+ @"AXTextChangeValues": @[change] -+ }; -+ NSAccessibilityPostNotificationWithUserInfo ( -+ view, NSAccessibilityValueChangedNotification, userInfo); ++ /* 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); ++ } + } + -+ /* Always notify cursor movement. */ ++ /* Always notify that cursor position (selection) changed. */ + NSAccessibilityPostNotification ( + view, NSAccessibilitySelectedTextChangedNotification); + @@ -158,318 +164,51 @@ MRC compatible (unsafe_unretained, proper retain/release). ns_focus (f, NULL, 0); NSGraphicsContext *ctx = [NSGraphicsContext currentContext]; -@@ -6849,6 +6923,637 @@ - - /* ========================================================================== - -+ Accessibility virtual elements (macOS / Cocoa only) -+ -+ ========================================================================== */ +@@ -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 + +#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; + } + + -+/* ---- Helper: extract visible text from glyph rows of a window ---- */ -+static NSString * -+ns_ax_text_from_glyph_rows (struct window *w) ++ ++#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 +{ -+ 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]; ++ /* Must return NO so assistive technology discovers this view. */ ++ return NO; +} + +- (BOOL)isAccessibilityElement @@ -477,512 +216,32 @@ MRC compatible (unsafe_unretained, proper retain/release). + return YES; +} + -+@end -+ -+ -+@implementation EmacsAccessibilityBuffer -+ -+/* ---- NSAccessibility protocol ---- */ -+ -+- (NSAccessibilityRole)accessibilityRole ++- (id)accessibilityFocusedUIElement +{ ++ /* EmacsView is the focused element — there are no child accessible ++ elements within the custom-drawn view. */ ++ return self; ++} ++ ++- (NSString *)accessibilityRole ++{ ++ /* TextArea role enables VoiceOver's text navigation commands ++ (VO+arrows for character/word/line reading). */ + return NSAccessibilityTextAreaRole; +} + +- (NSString *)accessibilityRoleDescription +{ -+ return @"editor"; ++ return NSAccessibilityRoleDescription (NSAccessibilityTextAreaRole, nil); +} + -+- (NSString *)accessibilityLabel -+{ -+ struct window *w = self.emacsWindow; -+ if (w && WINDOW_LEAF_P(w)) -+ { -+ struct buffer *b = XBUFFER(w->contents); -+ if (b) -+ { -+ Lisp_Object name = BVAR(b, name); -+ if (STRINGP(name)) -+ return [NSString stringWithLispString:name]; -+ } -+ } -+ return @"buffer"; -+} -+ -+- (id)accessibilityValue -+{ -+ struct window *w = self.emacsWindow; -+ if (!w) -+ return @""; -+ return ns_ax_text_from_glyph_rows(w); -+} -+ -+- (NSInteger)accessibilityNumberOfCharacters -+{ -+ NSString *text = [self accessibilityValue]; -+ return [text length]; -+} -+ -+- (NSString *)accessibilitySelectedText -+{ -+ struct window *w = self.emacsWindow; -+ if (!w || !WINDOW_LEAF_P(w)) -+ return @""; -+ -+ struct buffer *b = XBUFFER(w->contents); -+ if (!b || NILP(BVAR(b, mark_active))) -+ return @""; -+ -+ /* Return the selected region text. */ -+ NSString *text = [self accessibilityValue]; -+ NSRange sel = [self accessibilitySelectedTextRange]; -+ if (sel.location == NSNotFound || sel.location + sel.length > [text length]) -+ return @""; -+ return [text substringWithRange:sel]; -+} -+ -+- (NSRange)accessibilitySelectedTextRange -+{ -+ struct window *w = self.emacsWindow; -+ if (!w || !WINDOW_LEAF_P(w)) -+ return NSMakeRange(0, 0); -+ -+ struct buffer *b = XBUFFER(w->contents); -+ if (!b) -+ return NSMakeRange(0, 0); -+ -+ NSUInteger point_idx = ns_ax_index_for_point(w); -+ -+ if (NILP(BVAR(b, mark_active))) -+ return NSMakeRange(point_idx, 0); -+ -+ /* With active mark, report the selection range. Map mark -+ position to accessibility index using the same glyph-based -+ mapping as point. */ -+ ptrdiff_t mark_pos = marker_position (BVAR (b, mark)); -+ ptrdiff_t pt_pos = BUF_PT (b); -+ ptrdiff_t begv = BUF_BEGV (b); -+ ptrdiff_t sel_start = (mark_pos < pt_pos) ? mark_pos : pt_pos; -+ ptrdiff_t sel_end = (mark_pos < pt_pos) ? pt_pos : mark_pos; -+ NSUInteger start_idx = (NSUInteger) (sel_start - begv); -+ NSUInteger len = (NSUInteger) (sel_end - sel_start); -+ return NSMakeRange(start_idx, len); -+} -+ -+- (NSInteger)accessibilityInsertionPointLineNumber -+{ -+ struct window *w = self.emacsWindow; -+ if (!w) -+ return 0; -+ NSUInteger idx = ns_ax_index_for_point(w); -+ return ns_ax_line_for_index(w, idx); -+} -+ -+- (NSString *)accessibilityStringForRange:(NSRange)range -+{ -+ NSString *text = [self accessibilityValue]; -+ if (range.location + range.length > [text length]) -+ return @""; -+ return [text substringWithRange:range]; -+} -+ -+- (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]); -+} -+ -+- (BOOL)isAccessibilityFocused -+{ -+ struct window *w = self.emacsWindow; -+ if (!w) -+ return NO; -+ EmacsView *view = self.emacsView; -+ if (!view || !view->emacsframe) -+ return NO; -+ struct frame *f = view->emacsframe; -+ return (w == XWINDOW(f->selected_window)); -+} -+ -+- (id)accessibilityParent -+{ -+ return NSAccessibilityUnignoredAncestor (self.emacsView); -+} -+ -+- (NSRect)accessibilityFrame -+{ -+ struct window *w = self.emacsWindow; -+ if (!w) -+ return NSZeroRect; -+ return [self screenRectFromEmacsX:w->pixel_left -+ y:w->pixel_top -+ width:w->pixel_width -+ height:w->pixel_height]; -+} -+ -+/* ---- Notification dispatch ---- */ -+ -+- (void)postAccessibilityUpdatesForWindow:(struct window *)w -+ frame:(struct frame *)f -+{ -+ if (!w || !WINDOW_LEAF_P(w)) -+ return; -+ -+ struct buffer *b = XBUFFER(w->contents); -+ if (!b) -+ return; -+ -+ ptrdiff_t modiff = BUF_MODIFF(b); -+ ptrdiff_t point = BUF_PT(b); -+ -+ /* Text content changed? */ -+ if (modiff != self.cachedModiff) -+ { -+ self.cachedModiff = modiff; -+ NSAccessibilityPostNotification(self, -+ NSAccessibilityValueChangedNotification); -+ -+ /* Rich typing echo for VoiceOver. -+ 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 +7594,7 @@ - [layer release]; - #endif - -+ [accessibilityElements release]; - [[self menu] release]; - [super dealloc]; - } -@@ -9474,6 +10180,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) ---- */ ++/* ---- Text content methods for VoiceOver ---- */ + +- (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 @""; + @@ -995,6 +254,10 @@ MRC compatible (unsafe_unretained, proper retain/release). + 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; @@ -1013,8 +276,49 @@ MRC compatible (unsafe_unretained, proper retain/release). + 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); + @@ -1027,8 +331,40 @@ MRC compatible (unsafe_unretained, proper retain/release). + 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 @""; + @@ -1047,82 +383,49 @@ MRC compatible (unsafe_unretained, proper retain/release). + + 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; ++ ptrdiff_t range = end - start; ++ ptrdiff_t byte_range = end_byte - start_byte; + + Lisp_Object str; + if (! NILP (BVAR (curbuf, enable_multibyte_characters))) -+ str = make_uninit_multibyte_string (char_range, brange); ++ str = make_uninit_multibyte_string (range, byte_range); + else -+ str = make_uninit_string (char_range); -+ memcpy (SDATA (str), BUF_BYTE_ADDRESS (curbuf, start_byte), brange); ++ str = make_uninit_string (range); ++ memcpy (SDATA (str), BUF_BYTE_ADDRESS (curbuf, start_byte), byte_range); + + return [NSString stringWithLispString:str]; +} + -+- (NSInteger)accessibilityNumberOfCharacters ++- (NSAttributedString *)accessibilityAttributedStringForRange:(NSRange)nsrange +{ -+ 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]; ++ /* 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]; +} + +- (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; @@ -1132,33 +435,136 @@ MRC compatible (unsafe_unretained, proper retain/release). + && 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)); +} + -+/* ---- Legacy parameterized attribute APIs (Zoom uses these) ---- */ ++- (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]; ++} + +- (NSArray *)accessibilityParameterizedAttributeNames +{ @@ -1190,24 +596,8 @@ MRC compatible (unsafe_unretained, proper retain/release). + + return [super accessibilityAttributeValue:attribute forParameter:parameter]; +} -+ +#endif /* NS_IMPL_COCOA */ + @end /* EmacsView */ -@@ -9941,6 +11032,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.