Stub @implementation added in 0001 was never removed when 0004 added the full implementation, causing Clang to error: reimplementation of category 'InteractiveSpans' Remove the stub block in 0004 (interactive span elements for Tab).
345 lines
11 KiB
Diff
345 lines
11 KiB
Diff
From 9d99df6f95ac2011a1a9f3de448f2f0ec4c27145 Mon Sep 17 00:00:00 2001
|
|
From: Martin Sukany <martin@sukany.cz>
|
|
Date: Wed, 4 Mar 2026 15:23:55 +0100
|
|
Subject: [PATCH 5/9] ns: add interactive span elements for Tab
|
|
|
|
* src/nsterm.m (ns_ax_scan_interactive_spans): New function; scans the
|
|
visible portion of a buffer for interactive text properties
|
|
(ns-ax-widget, ns-ax-button, ns-ax-follow-link, ns-ax-org-link,
|
|
mouse-face, overlay keymap) and builds EmacsAccessibilityInteractiveSpan
|
|
elements.
|
|
(EmacsAccessibilityInteractiveSpan): Implement AXButton and AXLink
|
|
elements with an AXPress action that sends a synthetic TAB keystroke.
|
|
(EmacsAccessibilityBuffer(InteractiveSpans)): New category.
|
|
(accessibilityChildrenInNavigationOrder): Return cached span array,
|
|
rebuilding lazily when interactiveSpansDirty is set.
|
|
---
|
|
src/nsterm.m | 304 +++++++++++++++++++++++++++++++++++++++++++++++++--
|
|
1 file changed, 293 insertions(+), 11 deletions(-)
|
|
|
|
diff --git a/src/nsterm.m b/src/nsterm.m
|
|
index 84a32a05cb..b327102521 100644
|
|
--- a/src/nsterm.m
|
|
+++ b/src/nsterm.m
|
|
@@ -7637,17 +7637,6 @@ - (id)accessibilityTopLevelUIElement
|
|
|
|
@end
|
|
|
|
-/* Stub implementation of InteractiveSpans category.
|
|
- The full implementation is added in a later patch. */
|
|
-@implementation EmacsAccessibilityBuffer (InteractiveSpans)
|
|
-
|
|
-- (void)invalidateInteractiveSpans
|
|
-{
|
|
- /* Stub: full implementation added in patch 0004. */
|
|
-}
|
|
-
|
|
-@end
|
|
-
|
|
static BOOL
|
|
ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point,
|
|
ptrdiff_t *out_start,
|
|
@@ -9385,6 +9374,299 @@ - (NSRect)accessibilityFrame
|
|
|
|
@end
|
|
|
|
+
|
|
+
|
|
+/* ===================================================================
|
|
+ EmacsAccessibilityInteractiveSpan --- helpers and implementation
|
|
+ =================================================================== */
|
|
+
|
|
+/* Scan visible range of window W for interactive spans.
|
|
+ Returns NSArray<EmacsAccessibilityInteractiveSpan *>.
|
|
+
|
|
+ Priority when properties overlap:
|
|
+ widget > button > follow-link > org-link >
|
|
+ completion-candidate > keymap-overlay. */
|
|
+static NSArray *
|
|
+ns_ax_scan_interactive_spans (struct window *w,
|
|
+ EmacsAccessibilityBuffer *parent_buf)
|
|
+{
|
|
+ if (!w)
|
|
+ return @[];
|
|
+
|
|
+ Lisp_Object buf_obj = ns_ax_window_buffer_object (w);
|
|
+ if (NILP (buf_obj))
|
|
+ return @[];
|
|
+
|
|
+ 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);
|
|
+
|
|
+ if (vis_start < BUF_BEGV (b)) vis_start = BUF_BEGV (b);
|
|
+ if (vis_end > BUF_ZV (b)) vis_end = BUF_ZV (b);
|
|
+ if (vis_start >= vis_end)
|
|
+ return @[];
|
|
+
|
|
+ block_input ();
|
|
+ specpdl_ref blk_count = SPECPDL_INDEX ();
|
|
+ record_unwind_protect_void (unblock_input);
|
|
+
|
|
+ /* Symbols are interned once at startup via DEFSYM in syms_of_nsterm;
|
|
+ reference them directly here (GC-safe, no repeated obarray lookup). */
|
|
+
|
|
+ BOOL is_completion_buf
|
|
+ = EQ (BVAR (b, major_mode), Qns_ax_completion_list_mode);
|
|
+
|
|
+ NSMutableArray *spans = [NSMutableArray array];
|
|
+ ptrdiff_t pos = vis_start;
|
|
+
|
|
+ while (pos < vis_end)
|
|
+ {
|
|
+ Lisp_Object plist = Ftext_properties_at (make_fixnum (pos), buf_obj);
|
|
+ EmacsAXSpanType span_type = EmacsAXSpanTypeNone;
|
|
+ Lisp_Object limit_prop = Qnil;
|
|
+
|
|
+ /* Fplist_get third arg Qnil: use `eq' predicate (the default). */
|
|
+ if (!NILP (Fplist_get (plist, Qns_ax_widget, Qnil)))
|
|
+ {
|
|
+ span_type = EmacsAXSpanTypeWidget;
|
|
+ limit_prop = Qns_ax_widget;
|
|
+ }
|
|
+ else if (!NILP (Fplist_get (plist, Qns_ax_button, Qnil)))
|
|
+ {
|
|
+ span_type = EmacsAXSpanTypeButton;
|
|
+ limit_prop = Qns_ax_button;
|
|
+ }
|
|
+ else if (!NILP (Fplist_get (plist, Qns_ax_follow_link, Qnil)))
|
|
+ {
|
|
+ span_type = EmacsAXSpanTypeLink;
|
|
+ limit_prop = Qns_ax_follow_link;
|
|
+ }
|
|
+ else if (!NILP (Fplist_get (plist, Qns_ax_org_link, Qnil)))
|
|
+ {
|
|
+ span_type = EmacsAXSpanTypeLink;
|
|
+ limit_prop = Qns_ax_org_link;
|
|
+ }
|
|
+ else if (is_completion_buf
|
|
+ && !NILP (Fplist_get (plist, Qmouse_face, Qnil)))
|
|
+ {
|
|
+ /* For completions, use completion--string as boundary so we
|
|
+ don't accidentally merge two column-adjacent candidates
|
|
+ whose mouse-face regions may share padding whitespace.
|
|
+ Fall back to mouse-face if completion--string is absent. */
|
|
+ Lisp_Object cs_sym = Qns_ax_completion__string;
|
|
+ Lisp_Object cs_val = ns_ax_text_prop_at (pos, cs_sym, buf_obj);
|
|
+ span_type = EmacsAXSpanTypeCompletionItem;
|
|
+ limit_prop = NILP (cs_val) ? Qmouse_face : cs_sym;
|
|
+ }
|
|
+ else
|
|
+ {
|
|
+ /* Check overlays for keymap. */
|
|
+ Lisp_Object ovs
|
|
+ = Foverlays_in (make_fixnum (pos), make_fixnum (pos + 1));
|
|
+ while (CONSP (ovs))
|
|
+ {
|
|
+ if (!NILP (Foverlay_get (XCAR (ovs), Qkeymap)))
|
|
+ {
|
|
+ span_type = EmacsAXSpanTypeButton;
|
|
+ limit_prop = Qkeymap;
|
|
+ break;
|
|
+ }
|
|
+ ovs = XCDR (ovs);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (span_type == EmacsAXSpanTypeNone)
|
|
+ {
|
|
+ /* 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;
|
|
+ }
|
|
+
|
|
+ ptrdiff_t span_end = !NILP (limit_prop)
|
|
+ ? ns_ax_next_prop_change (pos, limit_prop, buf_obj, vis_end)
|
|
+ : pos + 1;
|
|
+
|
|
+ if (span_end > vis_end) span_end = vis_end;
|
|
+ if (span_end <= pos) span_end = pos + 1;
|
|
+
|
|
+ EmacsAccessibilityInteractiveSpan *span
|
|
+ = [[EmacsAccessibilityInteractiveSpan alloc] init];
|
|
+ span.charposStart = pos;
|
|
+ span.charposEnd = span_end;
|
|
+ span.spanType = span_type;
|
|
+ span.parentBuffer = parent_buf;
|
|
+ span.emacsView = parent_buf.emacsView;
|
|
+ span.lispWindow = parent_buf.lispWindow;
|
|
+ span.spanLabel = ns_ax_get_span_label (pos, span_end, buf_obj);
|
|
+
|
|
+ [spans addObject: span];
|
|
+ [span release];
|
|
+
|
|
+ pos = span_end;
|
|
+ }
|
|
+
|
|
+ unbind_to (blk_count, Qnil);
|
|
+ return [[spans copy] autorelease];
|
|
+}
|
|
+
|
|
+@implementation EmacsAccessibilityInteractiveSpan
|
|
+@synthesize spanLabel;
|
|
+@synthesize spanValue;
|
|
+
|
|
+- (void)dealloc
|
|
+{
|
|
+ [spanLabel release];
|
|
+ [spanValue release];
|
|
+ [super dealloc];
|
|
+}
|
|
+
|
|
+- (BOOL) isAccessibilityElement { return YES; }
|
|
+
|
|
+- (NSAccessibilityRole) accessibilityRole
|
|
+{
|
|
+ switch (self.spanType)
|
|
+ {
|
|
+ case EmacsAXSpanTypeLink: return NSAccessibilityLinkRole;
|
|
+ default: return NSAccessibilityButtonRole;
|
|
+ }
|
|
+}
|
|
+
|
|
+- (NSString *) accessibilityLabel { return self.spanLabel ?: @""; }
|
|
+- (NSString *) accessibilityValue { return self.spanValue; }
|
|
+
|
|
+- (NSRect) accessibilityFrame
|
|
+{
|
|
+ EmacsAccessibilityBuffer *pb = self.parentBuffer;
|
|
+ if (!pb || ![self validWindow])
|
|
+ return NSZeroRect;
|
|
+ NSUInteger ax_s = [pb accessibilityIndexForCharpos: self.charposStart];
|
|
+ NSUInteger ax_e = [pb accessibilityIndexForCharpos: self.charposEnd];
|
|
+ if (ax_e < ax_s) ax_e = ax_s;
|
|
+ return [pb accessibilityFrameForRange: NSMakeRange (ax_s, ax_e - ax_s)];
|
|
+}
|
|
+
|
|
+- (BOOL) isAccessibilityFocused
|
|
+{
|
|
+ /* Read the cached point stored by EmacsAccessibilityBuffer on the main
|
|
+ thread --- safe to read from any thread (plain ptrdiff_t,
|
|
+ no Lisp calls). */
|
|
+ EmacsAccessibilityBuffer *pb = self.parentBuffer;
|
|
+ if (!pb)
|
|
+ return NO;
|
|
+ ptrdiff_t pt = pb.cachedPoint;
|
|
+ return pt >= self.charposStart && pt < self.charposEnd;
|
|
+}
|
|
+
|
|
+- (void) setAccessibilityFocused: (BOOL) focused
|
|
+{
|
|
+ if (!focused)
|
|
+ return;
|
|
+ ptrdiff_t target = self.charposStart;
|
|
+ Lisp_Object lwin = self.lispWindow;
|
|
+ dispatch_async (dispatch_get_main_queue (), ^{
|
|
+ /* lwin is a Lisp_Object captured by value. This is GC-safe
|
|
+ because Lisp_Objects are tagged integers/pointers that
|
|
+ remain valid across GC --- GC does not relocate objects in
|
|
+ Emacs. The WINDOW_LIVE_P check below guards against the
|
|
+ window being deleted between capture and execution. */
|
|
+ if (!WINDOWP (lwin) || NILP (Fwindow_live_p (lwin)))
|
|
+ return;
|
|
+ /* 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);
|
|
+ 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));
|
|
+ unbind_to (count, Qnil);
|
|
+ });
|
|
+}
|
|
+
|
|
+@end
|
|
+
|
|
+/* EmacsAccessibilityBuffer --- InteractiveSpans category.
|
|
+ Methods are kept here (same .m file) so they access the ivars
|
|
+ declared in the @interface ivar block. */
|
|
+@implementation EmacsAccessibilityBuffer (InteractiveSpans)
|
|
+
|
|
+- (void) invalidateInteractiveSpans
|
|
+{
|
|
+ interactiveSpansDirty = YES;
|
|
+}
|
|
+
|
|
+- (NSArray *) accessibilityChildrenInNavigationOrder
|
|
+{
|
|
+ if (!interactiveSpansDirty && cachedInteractiveSpans != nil)
|
|
+ return cachedInteractiveSpans;
|
|
+
|
|
+ if (![NSThread isMainThread])
|
|
+ {
|
|
+ __block NSArray *result;
|
|
+ dispatch_sync (dispatch_get_main_queue (), ^{
|
|
+ result = [self accessibilityChildrenInNavigationOrder];
|
|
+ });
|
|
+ return result;
|
|
+ }
|
|
+
|
|
+ struct window *w = [self validWindow];
|
|
+ if (!w)
|
|
+ return cachedInteractiveSpans ? cachedInteractiveSpans : @[];
|
|
+
|
|
+ /* Validate buffer before scanning. The Lisp calls inside
|
|
+ ns_ax_scan_interactive_spans (Ftext_properties_at, Fplist_get,
|
|
+ Fnext_single_property_change) do not signal on valid buffers
|
|
+ with valid positions. Verify those preconditions here so we
|
|
+ never enter the scan with invalid state, which could longjmp
|
|
+ out of a dispatch_sync block and deadlock the AX thread. */
|
|
+ if (!BUFFERP (w->contents) || !XBUFFER (w->contents))
|
|
+ return cachedInteractiveSpans ? cachedInteractiveSpans : @[];
|
|
+
|
|
+ NSArray *spans = ns_ax_scan_interactive_spans (w, self);
|
|
+
|
|
+ if (!cachedInteractiveSpans)
|
|
+ cachedInteractiveSpans = [[NSMutableArray alloc] init];
|
|
+ [cachedInteractiveSpans setArray: spans];
|
|
+ interactiveSpansDirty = NO;
|
|
+
|
|
+ return cachedInteractiveSpans;
|
|
+}
|
|
+
|
|
+@end
|
|
+
|
|
#endif /* NS_IMPL_COCOA */
|
|
|
|
|
|
--
|
|
2.43.0
|
|
|