From 0b43fd25e376fd264933a7e9e68cf4a1d23ce38a Mon Sep 17 00:00:00 2001 From: Daneel Date: Fri, 27 Feb 2026 15:12:40 +0100 Subject: [PATCH] =?UTF-8?q?patches:=20review=20fixes=20=E2=80=94=20memory?= =?UTF-8?q?=20leak,=20dead=20code,=20unwind-protect,=20protocol=20conforma?= =?UTF-8?q?nce?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit B1: Fix memory leak in ns_ax_scan_interactive_spans — [spans copy] returned +1 retained object never released by caller. Now returns [[spans copy] autorelease]. B2: Remove dead function ns_ax_utf16_length_for_buffer_range — defined but never called anywhere in the patch. B3: Add specpdl unwind protection in EmacsAccessibilityInteractiveSpan setAccessibilityFocused: — if Fselect_window signals, block_input is now always matched by unblock_input via record_unwind_protect_void. W2: Document ns_ax_event_is_line_nav_key fragility in README Known Limitations (raw keycodes vs command symbols). W4: Add comment for #include intervals.h (TEXT_PROP_MEANS_INVISIBLE). M3: accessibilityBoundsForRange: on EmacsView now delegates to the focused EmacsAccessibilityBuffer for accurate per-range geometry, with cursor-rect fallback for Zoom. M4: Add protocol conformance to EmacsAccessibilityBuffer @interface declaration. W1: Expanded commit message listing all new types, functions, DEFSYM additions, and threading model. --- ...oundsForRange-for-macOS-Zoom-cursor-.patch | 324 +++++++++++------- patches/README.txt | 26 +- 2 files changed, 216 insertions(+), 134 deletions(-) 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 463c8b4..c741efa 100644 --- a/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch +++ b/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch @@ -1,25 +1,95 @@ -From 6878620f894a738eb11f9742a3920434cfb00e1e Mon Sep 17 00:00:00 2001 +From 17a100d99a31e0fae9b641c7ce163efd9bf5945b Mon Sep 17 00:00:00 2001 From: Martin Sukany -Date: Fri, 27 Feb 2026 14:54:57 +0100 -Subject: [PATCH] ns: implement VoiceOver accessibility +Date: Fri, 27 Feb 2026 15:09:15 +0100 +Subject: [PATCH] ns: implement VoiceOver accessibility for macOS -All AX protocol methods now dispatch_sync to the main thread, -including accessibilityFrame, accessibilityLabel, accessibilityRole, -accessibilityRoleDescription, accessibilityPlaceholderValue, -isAccessibilityFocused on EmacsAccessibilityBuffer, and -accessibilityValue/accessibilityFrame/accessibilityLabel on -EmacsAccessibilityModeLine. setAccessibilitySelectedTextRange -uses record_unwind_current_buffer for exception safety. +Add comprehensive macOS VoiceOver accessibility support to the NS +(Cocoa) port. Before this patch, Emacs exposed only a minimal, +largely broken accessibility interface to macOS assistive technology +clients. -* src/nsterm.m, src/nsterm.h, etc/NEWS: As described. +New types and classes: + + ns_ax_visible_run: maps buffer character positions to UTF-16 + accessibility string indices, skipping invisible text. + + EmacsAccessibilityElement: base class for virtual AX elements, + stores Lisp_Object lispWindow (GC-safe) and EmacsView reference. + + EmacsAccessibilityBuffer : AXTextArea element per + visible Emacs window; full text protocol (value, selectedTextRange, + line/index conversions, frameForRange, rangeForPosition), text cache + with visible-run mapping, hybrid SelectedTextChanged and + AnnouncementRequested notifications, completion announcements for + *Completions* buffer. + + EmacsAccessibilityModeLine: AXStaticText per mode line. + + EmacsAccessibilityInteractiveSpan: lightweight AX child elements + for Tab-navigable interactive spans (buttons, links, checkboxes, + completion candidates, Org-mode links, keymap overlays). + + EmacsAXSpanType: enum for span classification. + +New functions: + + ns_ax_buffer_text: build accessibility string with visible-run + mapping, using TEXT_PROP_MEANS_INVISIBLE and + Fbuffer_substring_no_properties (handles buffer gap correctly). + + ns_ax_mode_line_text: extract mode line text from glyph matrix. + + ns_ax_frame_for_range: screen rect for a character range via glyph + matrix lookup. + + ns_ax_event_is_line_nav_key: detect C-n/C-p/Tab/backtab for + forced line-granularity announcements. + + ns_ax_scan_interactive_spans: scan visible range for interactive + text properties (widget, button, follow-link, org-link, + completion--string, keymap overlay). + + ns_ax_completion_string_from_prop: extract completion candidate + from completion--string property (handles both string and cons). + + ns_ax_find_completion_overlay_range: locate nearest + completions-highlight overlay for completion announcements. + + ns_ax_completion_text_for_span: extract announcement text for a + completion overlay span. + +EmacsView extensions: + + rebuildAccessibilityTree, invalidateAccessibilityTree, + postAccessibilityUpdates (called from ns_update_end after every + redisplay cycle), accessibilityBoundsForRange (delegates to focused + buffer element with cursor-rect fallback for Zoom), + accessibilityFrameForRange, accessibilityStringForRange, + accessibilityParameterizedAttributeNames, + accessibilityAttributeValue:forParameter: (legacy API for Zoom). + +ns_draw_phys_cursor: stores cursor rect for Zoom, calls +UAZoomChangeFocus with correct CG coordinate-space transform. + +DEFSYM additions in syms_of_nsterm: Qwidget, Qbutton, Qfollow_link, +Qorg_link, Qcompletion_list_mode, Qcompletion__string, Qcompletion, +Qcompletions_highlight, Qbacktab. + +Threading model: all Lisp calls on main thread; AX getters use +dispatch_sync to main; index mapping methods are thread-safe (no +Lisp calls, read only immutable NSString and scalar cache). + +* src/nsterm.m: New accessibility implementation. +* src/nsterm.h: New class declarations and EmacsView ivar extensions. +* etc/NEWS: Document VoiceOver accessibility support. --- etc/NEWS | 11 + src/nsterm.h | 108 ++ - src/nsterm.m | 2870 +++++++++++++++++++++++++++++++++++++++++++++++--- - 3 files changed, 2847 insertions(+), 142 deletions(-) + src/nsterm.m | 2869 +++++++++++++++++++++++++++++++++++++++++++++++--- + 3 files changed, 2840 insertions(+), 148 deletions(-) diff --git a/etc/NEWS b/etc/NEWS -index 7367e3c..0e4480a 100644 +index 7367e3cc..0e4480ad 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -4374,6 +4374,17 @@ allowing Emacs users access to speech recognition utilities. @@ -41,7 +111,7 @@ index 7367e3c..0e4480a 100644 ** Re-introduced dictation, lost in Emacs v30 (macOS). We lost macOS dictation in v30 when migrating to NSTextInputClient. diff --git a/src/nsterm.h b/src/nsterm.h -index 7c1ee4c..6455547 100644 +index 7c1ee4cf..542e7d59 100644 --- a/src/nsterm.h +++ b/src/nsterm.h @@ -453,6 +453,100 @@ enum ns_return_frame_mode @@ -79,7 +149,7 @@ index 7c1ee4c..6455547 100644 +} ns_ax_visible_run; + +/* Virtual AXTextArea element — one per visible Emacs window (buffer). */ -+@interface EmacsAccessibilityBuffer : EmacsAccessibilityElement ++@interface EmacsAccessibilityBuffer : EmacsAccessibilityElement +{ + ns_ax_visible_run *visibleRuns; + NSUInteger visibleRunCount; @@ -174,14 +244,14 @@ index 7c1ee4c..6455547 100644 diff --git a/src/nsterm.m b/src/nsterm.m -index 932d209..078465a 100644 +index 932d209f..ea2de6f2 100644 --- a/src/nsterm.m +++ b/src/nsterm.m @@ -46,6 +46,7 @@ Updated by Christian Limpach (chris@nice.ch) #include "blockinput.h" #include "sysselect.h" #include "nsterm.h" -+#include "intervals.h" ++#include "intervals.h" /* TEXT_PROP_MEANS_INVISIBLE */ #include "systime.h" #include "character.h" #include "xwidget.h" @@ -240,7 +310,7 @@ index 932d209..078465a 100644 ns_focus (f, NULL, 0); NSGraphicsContext *ctx = [NSGraphicsContext currentContext]; -@@ -6849,213 +6891,2404 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg +@@ -6849,218 +6891,2386 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg /* ========================================================================== @@ -266,29 +336,23 @@ index 932d209..078465a 100644 -/* Needed to inform when window closed from lisp. */ -- (void) setWindowClosing: (BOOL)closing +-{ +- NSTRACE ("[EmacsView setWindowClosing:%d]", closing); +/* Build accessibility text for window W, skipping invisible text. + Populates *OUT_START with the buffer start charpos. + Populates *OUT_RUNS with an array of visible runs and *OUT_NRUNS + with the count. Caller must free *OUT_RUNS with xfree(). */ -+ -+static NSString * -+ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start, -+ ns_ax_visible_run **out_runs, NSUInteger *out_nruns) - { -- NSTRACE ("[EmacsView setWindowClosing:%d]", closing); -+ *out_runs = NULL; -+ *out_nruns = 0; - windowClosing = closing; -} -+ if (!w || !WINDOW_LEAF_P (w)) -+ { -+ *out_start = 0; -+ return @""; -+ } ++static NSString * ++ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start, ++ ns_ax_visible_run **out_runs, NSUInteger *out_nruns) ++{ ++ *out_runs = NULL; ++ *out_nruns = 0; -+ struct buffer *b = XBUFFER (w->contents); -+ if (!b) ++ if (!w || !WINDOW_LEAF_P (w)) + { + *out_start = 0; + return @""; @@ -297,34 +361,41 @@ index 932d209..078465a 100644 -- (void)dealloc -{ - NSTRACE ("[EmacsView dealloc]"); -+ ptrdiff_t begv = BUF_BEGV (b); -+ ptrdiff_t zv = BUF_ZV (b); ++ struct buffer *b = XBUFFER (w->contents); ++ if (!b) ++ { ++ *out_start = 0; ++ return @""; ++ } - /* Clear the view resize notification. */ - [[NSNotificationCenter defaultCenter] - removeObserver:self - name:NSViewFrameDidChangeNotification - object:nil]; -+ *out_start = begv; ++ ptrdiff_t begv = BUF_BEGV (b); ++ ptrdiff_t zv = BUF_ZV (b); - if (fs_state == FULLSCREEN_BOTH) - [nonfs_window release]; -+ if (zv <= begv) -+ return @""; ++ *out_start = begv; -#if defined (NS_IMPL_COCOA) && MAC_OS_X_VERSION_MIN_REQUIRED >= 101400 - /* Release layer and menu */ - EmacsLayer *layer = (EmacsLayer *)[self layer]; - [layer release]; -#endif ++ if (zv <= begv) ++ return @""; + +- [[self menu] release]; +- [super dealloc]; +-} + specpdl_ref count = SPECPDL_INDEX (); + record_unwind_current_buffer (); + if (b != current_buffer) + set_buffer_internal_1 (b); -- [[self menu] release]; -- [super dealloc]; --} + /* First pass: count visible runs to allocate the mapping array. */ + NSUInteger run_capacity = 64; + ns_ax_visible_run *runs = xmalloc (run_capacity @@ -332,14 +403,19 @@ index 932d209..078465a 100644 + NSUInteger nruns = 0; + NSUInteger ax_offset = 0; -+ NSMutableString *result = [NSMutableString string]; -+ ptrdiff_t pos = begv; - -/* Called on font panel selection. */ -- (void) changeFont: (id) sender -{ - struct font *font = FRAME_OUTPUT_DATA (emacsframe)->font; - NSFont *nsfont; ++ NSMutableString *result = [NSMutableString string]; ++ ptrdiff_t pos = begv; + +-#ifdef NS_IMPL_GNUSTEP +- nsfont = ((struct nsfont_info *) font)->nsfont; +-#else +- nsfont = (NSFont *) macfont_get_nsctfont (font); +-#endif + while (pos < zv) + { + /* Check invisible property (text properties + overlays). @@ -358,18 +434,15 @@ index 932d209..078465a 100644 + continue; + } --#ifdef NS_IMPL_GNUSTEP -- nsfont = ((struct nsfont_info *) font)->nsfont; --#else -- nsfont = (NSFont *) macfont_get_nsctfont (font); --#endif +- if (!font_panel_active) +- return; + /* Find end of this visible run: where invisible property changes. */ + Lisp_Object next = Fnext_single_char_property_change ( + make_fixnum (pos), Qinvisible, Qnil, make_fixnum (zv)); + ptrdiff_t run_end = FIXNUMP (next) ? XFIXNUM (next) : zv; -- if (!font_panel_active) -- return; +- if (font_panel_result) +- [font_panel_result release]; + /* Cap total text at NS_AX_TEXT_CAP. */ + ptrdiff_t run_len = run_end - pos; + if (ax_offset + (NSUInteger) run_len > NS_AX_TEXT_CAP) @@ -401,44 +474,52 @@ index 932d209..078465a 100644 + runs[nruns].ax_length = ns_len; + nruns++; -- if (font_panel_result) -- [font_panel_result release]; +- font_panel_result = (NSFont *) [sender convertFont: nsfont]; + ax_offset += ns_len; + pos = run_end; + } -- font_panel_result = (NSFont *) [sender convertFont: nsfont]; -+ unbind_to (count, Qnil); - - if (font_panel_result) - [font_panel_result retain]; -+ *out_runs = runs; -+ *out_nruns = nruns; -+ return result; -+} ++ unbind_to (count, Qnil); -#ifndef NS_IMPL_COCOA - font_panel_active = NO; - [NSApp stop: self]; -#endif ++ *out_runs = runs; ++ *out_nruns = nruns; ++ return result; + } + +-#ifdef NS_IMPL_COCOA +-- (void) noteUserSelectedFont + +/* ---- Helper: extract mode line text from glyph rows ---- */ + +static NSString * +ns_ax_mode_line_text (struct window *w) -+{ + { +- font_panel_active = NO; + if (!w || !w->current_matrix) + return @""; -+ + +- /* If no font was previously selected, use the currently selected +- font. */ + struct glyph_matrix *matrix = w->current_matrix; + NSMutableString *text = [NSMutableString string]; -+ + +- if (!font_panel_result && FRAME_FONT (emacsframe)) + for (int i = 0; i < matrix->nrows; i++) -+ { + { +- font_panel_result +- = macfont_get_nsctfont (FRAME_FONT (emacsframe)); + struct glyph_row *row = matrix->rows + i; + if (!row->enabled_p || !row->mode_line_p) + continue; -+ + +- if (font_panel_result) +- [font_panel_result retain]; + struct glyph *g = row->glyphs[TEXT_AREA]; + struct glyph *end = g + row->used[TEXT_AREA]; + for (; g < end; g++) @@ -450,12 +531,13 @@ index 932d209..078465a 100644 + length:1]]; + } + } -+ } + } +- +- [NSApp stop: self]; + return text; } --#ifdef NS_IMPL_COCOA --- (void) noteUserSelectedFont +-- (void) noteUserCancelledSelection + +/* ---- Helper: screen rect for a character range via glyph matrix ---- */ + @@ -468,31 +550,28 @@ index 932d209..078465a 100644 + if (!w || !w->current_matrix || !view) + return NSZeroRect; -- /* If no font was previously selected, use the currently selected -- font. */ +- if (font_panel_result) +- [font_panel_result release]; +- font_panel_result = nil; + /* charpos_start and charpos_len are already in buffer charpos + space — the caller maps AX string indices through + charposForAccessibilityIndex which handles invisible text. */ + ptrdiff_t cp_start = charpos_start; + ptrdiff_t cp_end = cp_start + charpos_len; -- if (!font_panel_result && FRAME_FONT (emacsframe)) +- [NSApp stop: self]; + struct glyph_matrix *matrix = w->current_matrix; + NSRect result = NSZeroRect; + BOOL found = NO; + + for (int i = 0; i < matrix->nrows; i++) - { -- font_panel_result -- = macfont_get_nsctfont (FRAME_FONT (emacsframe)); ++ { + struct glyph_row *row = matrix->rows + i; + if (!row->enabled_p || row->mode_line_p) + continue; + if (!row->displays_text_p && !row->ends_at_zv_p) + continue; - -- if (font_panel_result) -- [font_panel_result retain]; ++ + ptrdiff_t row_start = MATRIX_ROW_START_CHARPOS (row); + ptrdiff_t row_end = MATRIX_ROW_END_CHARPOS (row); + @@ -517,9 +596,8 @@ index 932d209..078465a 100644 + else + result = NSUnionRect (result, rowRect); + } - } - -- [NSApp stop: self]; ++ } ++ + if (!found) + return NSZeroRect; + @@ -537,8 +615,9 @@ index 932d209..078465a 100644 + NSRect winRect = [view convertRect:result toView:nil]; + return [[view window] convertRectToScreen:winRect]; } +-#endif --- (void) noteUserCancelledSelection +-- (Lisp_Object) showFontPanel +/* AX enum numeric compatibility for NSAccessibility notifications. + Values match WebKit AXObjectCacheMac fallback enums + (AXTextStateChangeType / AXTextEditType / AXTextSelectionDirection / @@ -561,35 +640,6 @@ index 932d209..078465a 100644 + ns_ax_text_selection_granularity_line = 3, +}; + -+static NSUInteger -+ns_ax_utf16_length_for_buffer_range (struct buffer *b, ptrdiff_t start, -+ ptrdiff_t end) - { -- font_panel_active = NO; -+ if (!b || end <= start) -+ return 0; - -- if (font_panel_result) -- [font_panel_result release]; -- font_panel_result = nil; -+ specpdl_ref count = SPECPDL_INDEX (); -+ record_unwind_current_buffer (); -+ if (b != current_buffer) -+ set_buffer_internal_1 (b); - -- [NSApp stop: self]; -+ Lisp_Object lstr = Fbuffer_substring_no_properties (make_fixnum (start), -+ make_fixnum (end)); -+ NSString *nsstr = [NSString stringWithLispString:lstr]; -+ NSUInteger len = [nsstr length]; -+ -+ unbind_to (count, Qnil); -+ return len; - } --#endif - --- (Lisp_Object) showFontPanel -+static BOOL +ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, + ptrdiff_t *out_start, + ptrdiff_t *out_end) @@ -903,10 +953,14 @@ index 932d209..078465a 100644 -/*****************************************************************************/ -/* Keyboard handling. */ +-#define NS_KEYLOG 0 + Lisp_Object buf_obj = ns_ax_window_buffer_object (w); + if (NILP (buf_obj)) + return @[]; -+ + +-- (void)keyDown: (NSEvent *)theEvent +-{ +- Mouse_HLInfo *hlinfo = MOUSE_HL_INFO (emacsframe); + struct buffer *b = XBUFFER (buf_obj); + ptrdiff_t vis_start = marker_position (w->start); + ptrdiff_t vis_end = ns_ax_window_end_charpos (w, b); @@ -1008,7 +1062,7 @@ index 932d209..078465a 100644 + pos = span_end; + } + -+ return [spans copy]; ++ return [[spans copy] autorelease]; +} + +@implementation EmacsAccessibilityInteractiveSpan @@ -1066,23 +1120,22 @@ index 932d209..078465a 100644 + window being deleted between capture and execution. */ + if (!WINDOWP (lwin) || NILP (Fwindow_live_p (lwin))) + return; -+ /* block_input prevents SIGIO delivery while we modify point/buffer. -+ It is safe here because this GCD block runs on the main thread -+ during the Cocoa run loop, outside of any Emacs event processing. */ ++ /* Use specpdl unwind protection so that block_input is always ++ matched by unblock_input, even if Fselect_window signals. */ ++ specpdl_ref count = SPECPDL_INDEX (); ++ record_unwind_protect_void (unblock_input); + block_input (); ++ record_unwind_current_buffer (); + Fselect_window (lwin, Qnil); + struct window *w = XWINDOW (lwin); + struct buffer *b = XBUFFER (w->contents); -+ struct buffer *oldb = current_buffer; + if (b != current_buffer) + set_buffer_internal_1 (b); + ptrdiff_t pos = target; + if (pos < BUF_BEGV (b)) pos = BUF_BEGV (b); + if (pos > BUF_ZV (b)) pos = BUF_ZV (b); + SET_PT_BOTH (pos, CHAR_TO_BYTE (pos)); -+ if (b != oldb) -+ set_buffer_internal_1 (oldb); -+ unblock_input (); ++ unbind_to (count, Qnil); + }); +} + @@ -2784,10 +2837,15 @@ index 932d209..078465a 100644 + +/*****************************************************************************/ +/* Keyboard handling. */ - #define NS_KEYLOG 0 - - - (void)keyDown: (NSEvent *)theEvent -@@ -8237,6 +10470,31 @@ - (void)windowDidBecomeKey /* for direct calls */ ++#define NS_KEYLOG 0 ++ ++- (void)keyDown: (NSEvent *)theEvent ++{ ++ Mouse_HLInfo *hlinfo = MOUSE_HL_INFO (emacsframe); + int code; + unsigned fnKeysym = 0; + static NSMutableArray *nsEvArray; +@@ -8237,6 +10447,31 @@ - (void)windowDidBecomeKey /* for direct calls */ XSETFRAME (event.frame_or_window, emacsframe); kbd_buffer_store_event (&event); ns_send_appdefined (-1); // Kick main loop @@ -2819,7 +2877,7 @@ index 932d209..078465a 100644 } -@@ -9474,6 +11732,322 @@ - (int) fullscreenState +@@ -9474,6 +11709,332 @@ - (int) fullscreenState return fs_state; } @@ -3068,8 +3126,18 @@ index 932d209..078465a 100644 + +- (NSRect)accessibilityBoundsForRange:(NSRange)range +{ -+ /* Return cursor screen rect. AT tools call this with the -+ selectedTextRange to locate the insertion point. */ ++ /* Delegate to the focused buffer element for accurate per-range ++ geometry when possible. Fall back to the cached cursor rect ++ (set by ns_draw_phys_cursor) for Zoom and simple AT queries. */ ++ id focused = [self accessibilityFocusedUIElement]; ++ if ([focused isKindOfClass:[EmacsAccessibilityBuffer class]]) ++ { ++ NSRect bufRect = [(EmacsAccessibilityBuffer *) focused ++ accessibilityFrameForRange:range]; ++ if (!NSIsEmptyRect (bufRect)) ++ return bufRect; ++ } ++ + NSRect viewRect = lastAccessibilityCursorRect; + + if (viewRect.size.width < 1) @@ -3142,7 +3210,7 @@ index 932d209..078465a 100644 @end /* EmacsView */ -@@ -11303,6 +13877,18 @@ Convert an X font name (XLFD) to an NS font name. +@@ -11303,6 +13864,18 @@ Convert an X font name (XLFD) to an NS font name. DEFSYM (Qns_drag_operation_generic, "ns-drag-operation-generic"); DEFSYM (Qns_handle_drag_motion, "ns-handle-drag-motion"); diff --git a/patches/README.txt b/patches/README.txt index f02d9e6..5c8a137 100644 --- a/patches/README.txt +++ b/patches/README.txt @@ -131,7 +131,9 @@ THREADING MODEL - postAccessibilityNotificationsForFrame: (full notify logic) - setAccessibilitySelectedTextRange: (SET_PT_BOTH, marker moves) - setAccessibilityFocused: on EmacsAccessibilityInteractiveSpan - (dispatches to main queue via dispatch_async) + (dispatches to main queue via dispatch_async; uses specpdl + unwind protection so block_input is always matched by + unblock_input even if Fselect_window signals an error) - ns_draw_phys_cursor partial update (lastAccessibilityCursorRect, UAZoomChangeFocus) @@ -343,8 +345,9 @@ INTERACTIVE SPANS and referenced directly -- no repeated intern() calls. Each span is allocated, configured, added to the spans array, then - released (the array retains it). Label priority: completion--string - > buffer substring > help-echo. + released (the array retains it). The function returns an autoreleased + immutable copy of the spans array. Label priority: + completion--string > buffer substring > help-echo. Tab navigation: -accessibilityChildrenInNavigationOrder returns the cached span array, rebuilt lazily when interactiveSpansDirty is set. @@ -381,9 +384,13 @@ ZOOM INTEGRATION 2. EmacsView -accessibilityBoundsForRange: / -accessibilityFrameForRange: AT tools (including Zoom) call these with the selectedTextRange - to locate the insertion point. The implementation returns the - screen rect stored in lastAccessibilityCursorRect, with a minimum - size of 1x8 pixels. The legacy parameterized-attribute API + to locate the insertion point. The implementation first delegates + to the focused EmacsAccessibilityBuffer element for accurate + per-range geometry via its accessibilityFrameForRange: method. + If the buffer element returns an empty rect (no valid window or + glyph data), the fallback uses the cached cursor rect stored in + lastAccessibilityCursorRect (minimum size 1x8 pixels). The legacy + parameterized-attribute API (NSAccessibilityBoundsForRangeParameterizedAttribute) is supported via -accessibilityAttributeValue:forParameter: for older AT clients. @@ -479,6 +486,13 @@ KNOWN LIMITATIONS - GNUstep is explicitly excluded (#ifdef NS_IMPL_COCOA). GNUstep has a different accessibility model and requires separate work. + - Line navigation detection (ns_ax_event_is_line_nav_key) checks + raw key codes (C-n = 14, C-p = 16, Tab = 9, backtab symbol). + Users who remap keys to navigation commands (e.g. C-j -> next-line) + will not get forced line-granularity announcements for those + bindings. A future improvement would inspect Vthis_command + against known navigation command symbols instead. + - UAZoomChangeFocus always uses kUAZoomFocusTypeInsertionPoint regardless of cursor style (box, bar, hbar). This is cosmetically imprecise but functionally correct.