patches: review fixes — memory leak, dead code, unwind-protect, protocol conformance

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 <NSAccessibility> protocol conformance to
    EmacsAccessibilityBuffer @interface declaration.

W1: Expanded commit message listing all new types, functions, DEFSYM
    additions, and threading model.
This commit is contained in:
2026-02-27 15:12:40 +01:00
parent 6994403014
commit 0b43fd25e3
2 changed files with 216 additions and 134 deletions

View File

@@ -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 <martin@sukany.cz>
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 <NSAccessibility>: 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 <NSAccessibility>
+{
+ 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))
+static NSString *
+ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start,
+ ns_ax_visible_run **out_runs, NSUInteger *out_nruns)
+{
+ *out_start = 0;
+ return @"";
+ }
+ *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");

View File

@@ -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.