patches: comprehensive review fixes — B1/W1-4/M1-4

B1: setAccessibilityFocused: on EmacsAccessibilityBuffer now checks
    ![NSThread isMainThread] and dispatches to main via dispatch_async.
    Prevents data race + AppKit thread violation from AX server thread.

W1: accessibilityInsertionPointLineNumber and accessibilityLineForIndex:
    now use lineRangeForRange iteration — O(lines) instead of O(chars).

W2: ns_ax_scan_interactive_spans skips non-interactive regions using
    Fnext_single_property_change for each scannable property and
    Fnext_single_char_property_change for keymap overlays.

W3: ns_ax_event_is_line_nav_key inspects Vthis_command against known
    navigation command symbols (next-line, previous-line, evil variants,
    dired variants) instead of raw key codes. Tab/backtab fallback
    retained via last_command_event.

W4: DEFSYM symbols renamed with ns_ax_ prefix (Qns_ax_button, etc.)
    to avoid linker collisions with other Emacs source files.
    Lisp symbol strings unchanged.

M3: Removed dead enum values (CheckBox, TextField, PopUpButton) and
    corresponding dead switch cases.

M4: Improved accessibilityStyleRangeForIndex: comment documenting the
    line-granularity simplification.

README: Updated stats, KNOWN LIMITATIONS, DEFSYM docs, test numbering.
This commit is contained in:
2026-02-27 16:14:47 +01:00
parent a6a3aca678
commit 936c251f11
2 changed files with 163 additions and 103 deletions

View File

@@ -42,8 +42,9 @@ New functions:
ns_ax_frame_for_range: screen rect for a character range via glyph ns_ax_frame_for_range: screen rect for a character range via glyph
matrix lookup. matrix lookup.
ns_ax_event_is_line_nav_key: detect C-n/C-p/Tab/backtab for ns_ax_event_is_line_nav_key: detect line navigation commands
forced line-granularity announcements. via Vthis_command (next-line, previous-line, evil variants)
with Tab/backtab fallback via last_command_event.
ns_ax_scan_interactive_spans: scan visible range for interactive ns_ax_scan_interactive_spans: scan visible range for interactive
text properties (widget, button, follow-link, org-link, text properties (widget, button, follow-link, org-link,
@@ -71,9 +72,10 @@ EmacsView extensions:
ns_draw_phys_cursor: stores cursor rect for Zoom, calls ns_draw_phys_cursor: stores cursor rect for Zoom, calls
UAZoomChangeFocus with correct CG coordinate-space transform. UAZoomChangeFocus with correct CG coordinate-space transform.
DEFSYM additions in syms_of_nsterm: Qwidget, Qbutton, Qfollow_link, DEFSYM additions in syms_of_nsterm (ns_ax_ prefix to avoid
Qorg_link, Qcompletion_list_mode, Qcompletion__string, Qcompletion, collisions): Qns_ax_widget, Qns_ax_button, Qns_ax_follow_link,
Qcompletions_highlight, Qbacktab. Qns_ax_org_link, Qns_ax_completion_list_mode, Qns_ax_completion__string, Qns_ax_completion,
Qns_ax_completions_highlight, Qns_ax_backtab.
Threading model: all Lisp calls on main thread; AX getters use Threading model: all Lisp calls on main thread; AX getters use
dispatch_sync to main; index mapping methods are thread-safe (no dispatch_sync to main; index mapping methods are thread-safe (no
@@ -86,7 +88,7 @@ Lisp calls, read only immutable NSString and scalar cache).
etc/NEWS | 11 + etc/NEWS | 11 +
src/nsterm.h | 108 ++ src/nsterm.h | 108 ++
src/nsterm.m | 2870 +++++++++++++++++++++++++++++++++++++++++++++++--- src/nsterm.m | 2870 +++++++++++++++++++++++++++++++++++++++++++++++---
3 files changed, 2841 insertions(+), 148 deletions(-) 3 files changed, 2922 insertions(+), 149 deletions(-)
diff --git a/etc/NEWS b/etc/NEWS diff --git a/etc/NEWS b/etc/NEWS
index 7367e3cc..0e4480ad 100644 index 7367e3cc..0e4480ad 100644
@@ -114,7 +116,7 @@ diff --git a/src/nsterm.h b/src/nsterm.h
index 7c1ee4cf..542e7d59 100644 index 7c1ee4cf..542e7d59 100644
--- a/src/nsterm.h --- a/src/nsterm.h
+++ b/src/nsterm.h +++ b/src/nsterm.h
@@ -453,6 +453,100 @@ enum ns_return_frame_mode @@ -453,6 +453,97 @@ enum ns_return_frame_mode
@end @end
@@ -181,11 +183,8 @@ index 7c1ee4cf..542e7d59 100644
+{ +{
+ EmacsAXSpanTypeButton = 0, + EmacsAXSpanTypeButton = 0,
+ EmacsAXSpanTypeLink = 1, + EmacsAXSpanTypeLink = 1,
+ EmacsAXSpanTypeCheckBox = 2, + EmacsAXSpanTypeCompletionItem = 2,
+ EmacsAXSpanTypeTextField = 3, + EmacsAXSpanTypeWidget = 3,
+ EmacsAXSpanTypePopUpButton = 4,
+ EmacsAXSpanTypeCompletionItem = 5,
+ EmacsAXSpanTypeWidget = 6,
+}; +};
+ +
+/* A lightweight AX element representing one interactive text span +/* A lightweight AX element representing one interactive text span
@@ -215,7 +214,7 @@ index 7c1ee4cf..542e7d59 100644
/* ========================================================================== /* ==========================================================================
The main Emacs view The main Emacs view
@@ -471,6 +565,13 @@ enum ns_return_frame_mode @@ -471,6 +562,13 @@ enum ns_return_frame_mode
#ifdef NS_IMPL_COCOA #ifdef NS_IMPL_COCOA
char *old_title; char *old_title;
BOOL maximizing_resize; BOOL maximizing_resize;
@@ -310,7 +309,7 @@ index 932d209f..ea2de6f2 100644
ns_focus (f, NULL, 0); ns_focus (f, NULL, 0);
NSGraphicsContext *ctx = [NSGraphicsContext currentContext]; NSGraphicsContext *ctx = [NSGraphicsContext currentContext];
@@ -6849,218 +6891,2414 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg @@ -6849,218 +6891,2471 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg
/* ========================================================================== /* ==========================================================================
@@ -661,7 +660,7 @@ index 932d209f..ea2de6f2 100644
-#else -#else
- nsfont = (NSFont *) macfont_get_nsctfont (font); - nsfont = (NSFont *) macfont_get_nsctfont (font);
-#endif -#endif
+ Lisp_Object faceSym = Qcompletions_highlight; + Lisp_Object faceSym = Qns_ax_completions_highlight;
+ ptrdiff_t begv = BUF_BEGV (b); + ptrdiff_t begv = BUF_BEGV (b);
+ ptrdiff_t zv = BUF_ZV (b); + ptrdiff_t zv = BUF_ZV (b);
+ ptrdiff_t best_start = 0; + ptrdiff_t best_start = 0;
@@ -773,51 +772,59 @@ index 932d209f..ea2de6f2 100644
- if (font_panel_result) - if (font_panel_result)
- [font_panel_result autorelease]; - [font_panel_result autorelease];
+/* Detect line-level navigation commands. Inspects Vthis_command
+ (the command symbol being executed) rather than raw key codes so
+ that remapped bindings (e.g., C-j -> next-line) are recognized.
+ Falls back to last_command_event for Tab/backtab which are not
+ bound to a single canonical command symbol. */
+static bool +static bool
+ns_ax_event_is_line_nav_key (int *which) +ns_ax_event_is_line_nav_key (int *which)
+{ +{
+ Lisp_Object ev = last_command_event;
+ if (CONSP (ev))
+ ev = EVENT_HEAD (ev);
-#ifdef NS_IMPL_COCOA -#ifdef NS_IMPL_COCOA
- if (!canceled) - if (!canceled)
- font_panel_result = nil; - font_panel_result = nil;
-#endif -#endif
+ if (!FIXNUMP (ev))
+ {
+ /* Handle symbol events: backtab (S-Tab = previous completion). */
+ if (SYMBOLP (ev) && !NILP (ev))
+ {
+ if (EQ (ev, Qbacktab))
+ {
+ if (which)
+ *which = -1;
+ return true;
+ }
+ }
+ return false;
+ }
- result = font_panel_result; - result = font_panel_result;
- font_panel_result = nil; - font_panel_result = nil;
+ EMACS_INT c = XFIXNUM (ev); + /* 1. Check Vthis_command for known navigation command symbols. */
+ if (c == 14) /* C-n */ + if (SYMBOLP (Vthis_command) && !NILP (Vthis_command))
+ { + {
+ if (which) + Lisp_Object cmd = Vthis_command;
+ *which = 1; + /* Forward line commands. */
+ if (EQ (cmd, intern_c_string ("next-line"))
+ || EQ (cmd, intern_c_string ("dired-next-line"))
+ || EQ (cmd, intern_c_string ("evil-next-line"))
+ || EQ (cmd, intern_c_string ("evil-next-visual-line")))
+ {
+ if (which) *which = 1;
+ return true;
+ }
+ /* Backward line commands. */
+ if (EQ (cmd, intern_c_string ("previous-line"))
+ || EQ (cmd, intern_c_string ("dired-previous-line"))
+ || EQ (cmd, intern_c_string ("evil-previous-line"))
+ || EQ (cmd, intern_c_string ("evil-previous-visual-line")))
+ {
+ if (which) *which = -1;
+ return true;
+ }
+ }
+
+ /* 2. Fallback: check raw key events for Tab/backtab. */
+ Lisp_Object ev = last_command_event;
+ if (CONSP (ev))
+ ev = EVENT_HEAD (ev);
+
+ if (SYMBOLP (ev) && EQ (ev, Qns_ax_backtab))
+ {
+ if (which) *which = -1;
+ return true; + return true;
+ } + }
+ if (c == 16) /* C-p */ + if (FIXNUMP (ev) && XFIXNUM (ev) == 9) /* Tab */
+ { + {
+ if (which) + if (which) *which = 1;
+ *which = -1;
+ return true;
+ }
+ if (c == 9) /* Tab — next completion/link */
+ {
+ if (which)
+ *which = 1;
+ return true; + return true;
+ } + }
+ return false; + return false;
@@ -905,7 +912,7 @@ index 932d209f..ea2de6f2 100644
+ns_ax_get_span_label (ptrdiff_t start, ptrdiff_t end, +ns_ax_get_span_label (ptrdiff_t start, ptrdiff_t end,
+ Lisp_Object buf_obj) + Lisp_Object buf_obj)
+{ +{
+ Lisp_Object cs = ns_ax_text_prop_at (start, Qcompletion__string, + Lisp_Object cs = ns_ax_text_prop_at (start, Qns_ax_completion__string,
+ buf_obj); + buf_obj);
+ if (STRINGP (cs)) + if (STRINGP (cs))
+ return [NSString stringWithLispString: cs]; + return [NSString stringWithLispString: cs];
@@ -1001,7 +1008,7 @@ index 932d209f..ea2de6f2 100644
+ /* Symbols are interned once at startup via DEFSYM in syms_of_nsterm; + /* Symbols are interned once at startup via DEFSYM in syms_of_nsterm;
+ reference them directly here (GC-safe, no repeated obarray lookup). */ + reference them directly here (GC-safe, no repeated obarray lookup). */
+ +
+ BOOL is_completion_buf = EQ (BVAR (b, major_mode), Qcompletion_list_mode); + BOOL is_completion_buf = EQ (BVAR (b, major_mode), Qns_ax_completion_list_mode);
+ +
+ NSMutableArray *spans = [NSMutableArray array]; + NSMutableArray *spans = [NSMutableArray array];
+ ptrdiff_t pos = vis_start; + ptrdiff_t pos = vis_start;
@@ -1012,25 +1019,25 @@ index 932d209f..ea2de6f2 100644
+ EmacsAXSpanType span_type = (EmacsAXSpanType) -1; + EmacsAXSpanType span_type = (EmacsAXSpanType) -1;
+ Lisp_Object limit_prop = Qnil; + Lisp_Object limit_prop = Qnil;
+ +
+ if (!NILP (Fplist_get (plist, Qwidget, Qnil))) + if (!NILP (Fplist_get (plist, Qns_ax_widget, Qnil)))
+ { + {
+ span_type = EmacsAXSpanTypeWidget; + span_type = EmacsAXSpanTypeWidget;
+ limit_prop = Qwidget; + limit_prop = Qns_ax_widget;
+ } + }
+ else if (!NILP (Fplist_get (plist, Qbutton, Qnil))) + else if (!NILP (Fplist_get (plist, Qns_ax_button, Qnil)))
+ { + {
+ span_type = EmacsAXSpanTypeButton; + span_type = EmacsAXSpanTypeButton;
+ limit_prop = Qbutton; + limit_prop = Qns_ax_button;
+ } + }
+ else if (!NILP (Fplist_get (plist, Qfollow_link, Qnil))) + else if (!NILP (Fplist_get (plist, Qns_ax_follow_link, Qnil)))
+ { + {
+ span_type = EmacsAXSpanTypeLink; + span_type = EmacsAXSpanTypeLink;
+ limit_prop = Qfollow_link; + limit_prop = Qns_ax_follow_link;
+ } + }
+ else if (!NILP (Fplist_get (plist, Qorg_link, Qnil))) + else if (!NILP (Fplist_get (plist, Qns_ax_org_link, Qnil)))
+ { + {
+ span_type = EmacsAXSpanTypeLink; + span_type = EmacsAXSpanTypeLink;
+ limit_prop = Qorg_link; + limit_prop = Qns_ax_org_link;
+ } + }
+ else if (is_completion_buf + else if (is_completion_buf
+ && !NILP (Fplist_get (plist, Qmouse_face, Qnil))) + && !NILP (Fplist_get (plist, Qmouse_face, Qnil)))
@@ -1039,7 +1046,7 @@ index 932d209f..ea2de6f2 100644
+ don't accidentally merge two column-adjacent candidates + don't accidentally merge two column-adjacent candidates
+ whose mouse-face regions may share padding whitespace. + whose mouse-face regions may share padding whitespace.
+ Fall back to mouse-face if completion--string is absent. */ + Fall back to mouse-face if completion--string is absent. */
+ Lisp_Object cs_sym = Qcompletion__string; + Lisp_Object cs_sym = Qns_ax_completion__string;
+ Lisp_Object cs_val = ns_ax_text_prop_at (pos, cs_sym, buf_obj); + Lisp_Object cs_val = ns_ax_text_prop_at (pos, cs_sym, buf_obj);
+ span_type = EmacsAXSpanTypeCompletionItem; + span_type = EmacsAXSpanTypeCompletionItem;
+ limit_prop = NILP (cs_val) ? Qmouse_face : cs_sym; + limit_prop = NILP (cs_val) ? Qmouse_face : cs_sym;
@@ -1063,7 +1070,34 @@ index 932d209f..ea2de6f2 100644
+ +
+ if ((NSInteger) span_type == -1) + if ((NSInteger) span_type == -1)
+ { + {
+ pos++; + /* Skip to the next position where any interactive property
+ changes. Try each scannable property in turn and take
+ the nearest change point — O(properties) per gap rather
+ than O(chars). Fall back to pos+1 as safety net. */
+ ptrdiff_t next_interesting = vis_end;
+ Lisp_Object skip_props[5]
+ = { Qns_ax_widget, Qns_ax_button, Qns_ax_follow_link,
+ Qns_ax_org_link, Qmouse_face };
+ for (int sp = 0; sp < 5; sp++)
+ {
+ ptrdiff_t np
+ = ns_ax_next_prop_change (pos, skip_props[sp],
+ buf_obj, vis_end);
+ if (np > pos && np < next_interesting)
+ next_interesting = np;
+ }
+ /* Also check overlay keymap changes. */
+ Lisp_Object np_ov
+ = Fnext_single_char_property_change (make_fixnum (pos),
+ Qkeymap, buf_obj,
+ make_fixnum (vis_end));
+ if (FIXNUMP (np_ov))
+ {
+ ptrdiff_t npv = XFIXNUM (np_ov);
+ if (npv > pos && npv < next_interesting)
+ next_interesting = npv;
+ }
+ pos = (next_interesting > pos) ? next_interesting : pos + 1;
+ continue; + continue;
+ } + }
+ +
@@ -1101,11 +1135,8 @@ index 932d209f..ea2de6f2 100644
+{ +{
+ switch (self.spanType) + switch (self.spanType)
+ { + {
+ case EmacsAXSpanTypeLink: return NSAccessibilityLinkRole; + case EmacsAXSpanTypeLink: return NSAccessibilityLinkRole;
+ case EmacsAXSpanTypeCheckBox: return NSAccessibilityCheckBoxRole; + default: return NSAccessibilityButtonRole;
+ case EmacsAXSpanTypeTextField: return NSAccessibilityTextFieldRole;
+ case EmacsAXSpanTypePopUpButton: return NSAccessibilityPopUpButtonRole;
+ default: return NSAccessibilityButtonRole;
+ } + }
+} +}
+ +
@@ -1231,7 +1262,7 @@ index 932d209f..ea2de6f2 100644
+ { + {
+ ptrdiff_t p = probes[i]; + ptrdiff_t p = probes[i];
+ Lisp_Object cstr = Fget_char_property (make_fixnum (p), + Lisp_Object cstr = Fget_char_property (make_fixnum (p),
+ Qcompletion__string, + Qns_ax_completion__string,
+ Qnil); + Qnil);
+ if (STRINGP (cstr)) + if (STRINGP (cstr))
+ text = [NSString stringWithLispString:cstr]; + text = [NSString stringWithLispString:cstr];
@@ -1239,7 +1270,7 @@ index 932d209f..ea2de6f2 100644
+ { + {
+ /* Fallback: 'completion property used by display-completion-list. */ + /* Fallback: 'completion property used by display-completion-list. */
+ cstr = Fget_char_property (make_fixnum (p), + cstr = Fget_char_property (make_fixnum (p),
+ Qcompletion, + Qns_ax_completion,
+ Qnil); + Qnil);
+ if (STRINGP (cstr)) + if (STRINGP (cstr))
+ text = [NSString stringWithLispString:cstr]; + text = [NSString stringWithLispString:cstr];
@@ -1760,6 +1791,16 @@ index 932d209f..ea2de6f2 100644
+ if (!flag) + if (!flag)
+ return; + return;
+ +
+ /* VoiceOver may call this from the AX server thread.
+ All Lisp reads, block_input, and AppKit calls require main. */
+ if (![NSThread isMainThread])
+ {
+ dispatch_async (dispatch_get_main_queue (), ^{
+ [self setAccessibilityFocused:flag];
+ });
+ return;
+ }
+
+ struct window *w = [self validWindow]; + struct window *w = [self validWindow];
+ if (!w || !WINDOW_LEAF_P (w)) + if (!w || !WINDOW_LEAF_P (w))
+ return; + return;
@@ -1816,12 +1857,19 @@ index 932d209f..ea2de6f2 100644
+ if (point_idx > [cachedText length]) + if (point_idx > [cachedText length])
+ point_idx = [cachedText length]; + point_idx = [cachedText length];
+ +
+ /* Count newlines from start to point_idx. */ + /* Count lines by iterating lineRangeForRange from the start.
+ Each call jumps an entire line — O(lines) not O(chars). */
+ NSInteger line = 0; + NSInteger line = 0;
+ for (NSUInteger i = 0; i < point_idx; i++) + NSUInteger scan = 0;
+ NSUInteger len = [cachedText length];
+ while (scan < point_idx && scan < len)
+ { + {
+ if ([cachedText characterAtIndex:i] == '\n') + NSRange lr = [cachedText lineRangeForRange:NSMakeRange (scan, 0)];
+ line++; + NSUInteger next = NSMaxRange (lr);
+ if (next <= scan) break; /* safety */
+ if (next > point_idx) break;
+ line++;
+ scan = next;
+ } + }
+ return line; + return line;
+} +}
@@ -1866,12 +1914,18 @@ index 932d209f..ea2de6f2 100644
+ if (idx > [cachedText length]) + if (idx > [cachedText length])
+ idx = [cachedText length]; + idx = [cachedText length];
+ +
+ /* Count newlines from start of cachedText to idx. */ + /* Count lines by iterating lineRangeForRange — O(lines). */
+ NSInteger line = 0; + NSInteger line = 0;
+ for (NSUInteger i = 0; i < idx; i++) + NSUInteger scan = 0;
+ NSUInteger len = [cachedText length];
+ while (scan < idx && scan < len)
+ { + {
+ if ([cachedText characterAtIndex:i] == '\n') + NSRange lr = [cachedText lineRangeForRange:NSMakeRange (scan, 0)];
+ line++; + NSUInteger next = NSMaxRange (lr);
+ if (next <= scan) break;
+ if (next > idx) break;
+ line++;
+ scan = next;
+ } + }
+ return line; + return line;
+} +}
@@ -1938,7 +1992,9 @@ index 932d209f..ea2de6f2 100644
+ +
+- (NSRange)accessibilityStyleRangeForIndex:(NSInteger)index +- (NSRange)accessibilityStyleRangeForIndex:(NSInteger)index
+{ +{
+ /* Return the range of the current line — simple approach. */ + /* Return the range of the current line. A more accurate
+ implementation would return face/font property boundaries,
+ but line granularity is acceptable for VoiceOver. */
+ NSInteger line = [self accessibilityLineForIndex:index]; + NSInteger line = [self accessibilityLineForIndex:index];
+ return [self accessibilityRangeForLine:line]; + return [self accessibilityRangeForLine:line];
+} +}
@@ -2325,7 +2381,7 @@ index 932d209f..ea2de6f2 100644
+ /* 1. completion--string at point. */ + /* 1. completion--string at point. */
+ Lisp_Object cstr + Lisp_Object cstr
+ = Fget_char_property (make_fixnum (point), + = Fget_char_property (make_fixnum (point),
+ Qcompletion__string, Qnil); + Qns_ax_completion__string, Qnil);
+ announceText = ns_ax_completion_string_from_prop (cstr); + announceText = ns_ax_completion_string_from_prop (cstr);
+ +
+ /* 2. Fallback: full line text. */ + /* 2. Fallback: full line text. */
@@ -2393,7 +2449,7 @@ index 932d209f..ea2de6f2 100644
+ or a list ("candidate" "annotation") for annotated completions. + or a list ("candidate" "annotation") for annotated completions.
+ In the list case, use car (the completion itself). */ + In the list case, use car (the completion itself). */
+ Lisp_Object cstr = Fget_char_property (make_fixnum (point), + Lisp_Object cstr = Fget_char_property (make_fixnum (point),
+ Qcompletion__string, + Qns_ax_completion__string,
+ Qnil); + Qnil);
+ announceText = ns_ax_completion_string_from_prop (cstr); + announceText = ns_ax_completion_string_from_prop (cstr);
+ +
@@ -2440,7 +2496,7 @@ index 932d209f..ea2de6f2 100644
+ /* 3) Fallback: check completions-highlight overlay span at point. */ + /* 3) Fallback: check completions-highlight overlay span at point. */
+ if (!announceText) + if (!announceText)
+ { + {
+ Lisp_Object faceSym = Qcompletions_highlight; + Lisp_Object faceSym = Qns_ax_completions_highlight;
+ Lisp_Object overlays = Foverlays_at (make_fixnum (point), Qnil); + Lisp_Object overlays = Foverlays_at (make_fixnum (point), Qnil);
+ Lisp_Object tail; + Lisp_Object tail;
+ for (tail = overlays; CONSP (tail); tail = XCDR (tail)) + for (tail = overlays; CONSP (tail); tail = XCDR (tail))
@@ -2873,7 +2929,7 @@ index 932d209f..ea2de6f2 100644
int code; int code;
unsigned fnKeysym = 0; unsigned fnKeysym = 0;
static NSMutableArray *nsEvArray; static NSMutableArray *nsEvArray;
@@ -8237,6 +10475,31 @@ - (void)windowDidBecomeKey /* for direct calls */ @@ -8237,6 +10532,31 @@ - (void)windowDidBecomeKey /* for direct calls */
XSETFRAME (event.frame_or_window, emacsframe); XSETFRAME (event.frame_or_window, emacsframe);
kbd_buffer_store_event (&event); kbd_buffer_store_event (&event);
ns_send_appdefined (-1); // Kick main loop ns_send_appdefined (-1); // Kick main loop
@@ -3238,20 +3294,20 @@ index 932d209f..ea2de6f2 100644
@end /* EmacsView */ @end /* EmacsView */
@@ -11303,6 +13892,18 @@ Convert an X font name (XLFD) to an NS font name. @@ -11303,7 +13892,18 @@ Convert an X font name (XLFD) to an NS font name.
DEFSYM (Qns_drag_operation_generic, "ns-drag-operation-generic"); DEFSYM (Qns_drag_operation_generic, "ns-drag-operation-generic");
DEFSYM (Qns_handle_drag_motion, "ns-handle-drag-motion"); DEFSYM (Qns_handle_drag_motion, "ns-handle-drag-motion");
+ /* Accessibility span scanning symbols. */ + /* Accessibility span scanning symbols. */
+ DEFSYM (Qwidget, "widget"); + DEFSYM (Qns_ax_widget, "widget");
+ DEFSYM (Qbutton, "button"); + DEFSYM (Qns_ax_button, "button");
+ DEFSYM (Qfollow_link, "follow-link"); + DEFSYM (Qns_ax_follow_link, "follow-link");
+ DEFSYM (Qorg_link, "org-link"); + DEFSYM (Qns_ax_org_link, "org-link");
+ DEFSYM (Qcompletion_list_mode, "completion-list-mode"); + DEFSYM (Qns_ax_completion_list_mode, "completion-list-mode");
+ DEFSYM (Qcompletion__string, "completion--string"); + DEFSYM (Qns_ax_completion__string, "completion--string");
+ DEFSYM (Qcompletion, "completion"); + DEFSYM (Qns_ax_completion, "completion");
+ DEFSYM (Qcompletions_highlight, "completions-highlight"); + DEFSYM (Qns_ax_completions_highlight, "completions-highlight");
+ DEFSYM (Qbacktab, "backtab"); + DEFSYM (Qns_ax_backtab, "backtab");
+ /* Qmouse_face and Qkeymap are defined in textprop.c / keymap.c. */ + /* Qmouse_face and Qkeymap are defined in textprop.c / keymap.c. */
+ +
Fput (Qalt, Qmodifier_value, make_fixnum (alt_modifier)); Fput (Qalt, Qmodifier_value, make_fixnum (alt_modifier));

View File

@@ -3,7 +3,7 @@ EMACS NS VOICEOVER ACCESSIBILITY PATCH
patch: 0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch patch: 0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch
author: Martin Sukany <martin@sukany.cz> author: Martin Sukany <martin@sukany.cz>
files: src/nsterm.h (+108 lines) files: src/nsterm.h (+108 lines)
src/nsterm.m (+2588 ins, -140 del, +2448 net) src/nsterm.m (+2638 ins, -140 del, +2498 net)
OVERVIEW OVERVIEW
@@ -36,8 +36,6 @@ OVERVIEW
also covers completion announcements for the *Completions* buffer and also covers completion announcements for the *Completions* buffer and
Tab-navigable interactive spans for buttons, links, checkboxes, Tab-navigable interactive spans for buttons, links, checkboxes,
Org-mode links, completion candidates, and keymap overlays. Org-mode links, completion candidates, and keymap overlays.
(EmacsAXSpanTypeCheckBox is reserved for future use but not
currently scanned.)
ARCHITECTURE ARCHITECTURE
@@ -367,10 +365,10 @@ INTERACTIVE SPANS
column-adjacent completion candidates from being merged into one span column-adjacent completion candidates from being merged into one span
when their mouse-face regions share padding whitespace. when their mouse-face regions share padding whitespace.
All property symbols (Qwidget, Qbutton, Qfollow_link, Qorg_link, All property symbols are registered with DEFSYM in syms_of_nsterm
Qcompletion__string, Qcompletion, Qcompletions_highlight, Qbacktab, using ns_ax_ prefixed C variable names (e.g., Qns_ax_button for
Qcompletion_list_mode) are registered with DEFSYM in syms_of_nsterm "button") to avoid collisions with other Emacs source files.
and referenced directly -- no repeated intern() calls. Referenced directly -- no repeated intern() calls.
Each span is allocated, configured, added to the spans array, then Each span is allocated, configured, added to the spans array, then
released (the array retains it). The function returns an autoreleased released (the array retains it). The function returns an autoreleased
@@ -505,8 +503,8 @@ KNOWN LIMITATIONS
covers the common case, but overlay-only changes with a stationary covers the common case, but overlay-only changes with a stationary
point would be missed. A future fix would compare overlay_modiff. point would be missed. A future fix would compare overlay_modiff.
- Interactive span scan is O(n) in the visible buffer range. Every - Interactive span scan uses property-change jumps to skip
character position is visited to find property boundaries. For non-interactive regions, but still visits every property boundary. For
large visible buffers this scan runs on every redisplay cycle large visible buffers this scan runs on every redisplay cycle
whenever interactiveSpansDirty is set. An optimization would use whenever interactiveSpansDirty is set. An optimization would use
next_single_property_change to skip non-interactive regions in bulk. next_single_property_change to skip non-interactive regions in bulk.
@@ -516,6 +514,12 @@ KNOWN LIMITATIONS
Mode lines with icon fonts (e.g. doom-modeline with nerd-font) Mode lines with icon fonts (e.g. doom-modeline with nerd-font)
produce incomplete or garbled accessibility text. produce incomplete or garbled accessibility text.
- Line counting (accessibilityInsertionPointLineNumber,
accessibilityLineForIndex:) uses O(lines) iteration via
lineRangeForRange. For buffers with tens of thousands of visible
lines this is acceptable but not optimal. A line-number cache
keyed on cachedTextModiff could reduce this to O(1).
- Buffers larger than NS_AX_TEXT_CAP (100,000 UTF-16 units) are - Buffers larger than NS_AX_TEXT_CAP (100,000 UTF-16 units) are
truncated. The truncation is silent; AT tools navigating past the truncated. The truncation is silent; AT tools navigating past the
truncation boundary may behave unexpectedly. truncation boundary may behave unexpectedly.
@@ -527,11 +531,11 @@ KNOWN LIMITATIONS
has a different accessibility model and requires separate work. has a different accessibility model and requires separate work.
- Line navigation detection (ns_ax_event_is_line_nav_key) checks - Line navigation detection (ns_ax_event_is_line_nav_key) checks
raw key codes (C-n = 14, C-p = 16, Tab = 9, backtab symbol). Vthis_command against known navigation command symbols
Users who remap keys to navigation commands (e.g. C-j -> next-line) (next-line, previous-line, evil-next-line, etc.) and falls back
will not get forced line-granularity announcements for those to raw key codes for Tab/backtab. Custom navigation commands
bindings. A future improvement would inspect Vthis_command not in the recognized list will not get forced line-granularity
against known navigation command symbols instead. announcements.
- UAZoomChangeFocus always uses kUAZoomFocusTypeInsertionPoint - UAZoomChangeFocus always uses kUAZoomFocusTypeInsertionPoint
regardless of cursor style (box, bar, hbar). This is cosmetically regardless of cursor style (box, bar, hbar). This is cosmetically
@@ -600,9 +604,9 @@ TESTING CHECKLIST
C-x o to switch windows. Emacs must not hang. C-x o to switch windows. Emacs must not hang.
Stress test: Stress test:
26. Open a large file (>5000 lines). Navigate with C-v / M-v. 25. Open a large file (>5000 lines). Navigate with C-v / M-v.
Verify no significant lag in VoiceOver speech response. Verify no significant lag in VoiceOver speech response.
27. Open an org-mode file with many folded sections. Verify that 26. Open an org-mode file with many folded sections. Verify that
folded (invisible) text is not announced during navigation. folded (invisible) text is not announced during navigation.
-- end of README -- -- end of README --