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:
@@ -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;
|
+ return true;
|
||||||
+ }
|
+ }
|
||||||
+ if (c == 16) /* C-p */
|
+ /* 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)
|
+ if (which) *which = -1;
|
||||||
+ *which = -1;
|
|
||||||
+ return true;
|
+ return true;
|
||||||
+ }
|
+ }
|
||||||
+ if (c == 9) /* Tab — next completion/link */
|
+ }
|
||||||
|
+
|
||||||
|
+ /* 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)
|
+ if (which) *which = -1;
|
||||||
+ *which = 1;
|
+ return true;
|
||||||
|
+ }
|
||||||
|
+ if (FIXNUMP (ev) && XFIXNUM (ev) == 9) /* Tab */
|
||||||
|
+ {
|
||||||
|
+ 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;
|
||||||
+ }
|
+ }
|
||||||
+
|
+
|
||||||
@@ -1102,9 +1136,6 @@ index 932d209f..ea2de6f2 100644
|
|||||||
+ switch (self.spanType)
|
+ switch (self.spanType)
|
||||||
+ {
|
+ {
|
||||||
+ case EmacsAXSpanTypeLink: return NSAccessibilityLinkRole;
|
+ case EmacsAXSpanTypeLink: return NSAccessibilityLinkRole;
|
||||||
+ case EmacsAXSpanTypeCheckBox: return NSAccessibilityCheckBoxRole;
|
|
||||||
+ case EmacsAXSpanTypeTextField: return NSAccessibilityTextFieldRole;
|
|
||||||
+ case EmacsAXSpanTypePopUpButton: return NSAccessibilityPopUpButtonRole;
|
|
||||||
+ default: return NSAccessibilityButtonRole;
|
+ 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)];
|
||||||
|
+ NSUInteger next = NSMaxRange (lr);
|
||||||
|
+ if (next <= scan) break; /* safety */
|
||||||
|
+ if (next > point_idx) break;
|
||||||
+ line++;
|
+ 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)];
|
||||||
|
+ NSUInteger next = NSMaxRange (lr);
|
||||||
|
+ if (next <= scan) break;
|
||||||
|
+ if (next > idx) break;
|
||||||
+ line++;
|
+ 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));
|
||||||
|
|||||||
@@ -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 --
|
||||||
|
|||||||
Reference in New Issue
Block a user