patches: address review B1-B4 and N1,N3
B4: Shorten all subject lines to <=50 chars (preference from CONTRIBUTE).
B3: Fix intermediate BUF_CHARS_MODIFF state in 0002: use BUF_MODIFF
from the start, eliminating the wrong-then-corrected pattern across
patches 0002+0007.
B2: Wrap long NEWS line in Zoom entry (was 80 chars, now <=79).
B1: Long block_input comment already fixed by 0004 in both branches;
confirmed no change needed in final state.
N1: Fix DEFVAR doc in 0001 to reflect auto-detection at startup.
N3: Convert VoiceOver NEWS bullet list to prose paragraphs.
N2: Skip (existing file uses 80-char separators throughout; changing
only new ones would be inconsistent).
This commit is contained in:
326
patches/0004-ns-add-interactive-span-elements-for-Tab.patch
Normal file
326
patches/0004-ns-add-interactive-span-elements-for-Tab.patch
Normal file
@@ -0,0 +1,326 @@
|
||||
From 7f99354ef3e5281426a4d57c345c78fa40c389a7 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 | 293 +++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
1 file changed, 293 insertions(+)
|
||||
|
||||
diff --git a/src/nsterm.m b/src/nsterm.m
|
||||
index 84a32a05cb..c713943b30 100644
|
||||
--- a/src/nsterm.m
|
||||
+++ b/src/nsterm.m
|
||||
@@ -9385,6 +9385,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
|
||||
|
||||
Reference in New Issue
Block a user