The original commit a4adced9b5 had its commit message accidentally polluted with the entire Emacs git log (~15MB). The cherry-pick script copied this bloat into the new 0002 patch. Regenerated 0002 with a clean ChangeLog-only commit message (1385 bytes).
1194 lines
36 KiB
Diff
1194 lines
36 KiB
Diff
From 64859d37421bdaabe2ec416285b6f1847da0737c Mon Sep 17 00:00:00 2001
|
||
From: Martin Sukany <martin@sukany.cz>
|
||
Date: Wed, 4 Mar 2026 15:23:54 +0100
|
||
Subject: [PATCH 3/9] ns: implement buffer accessibility element
|
||
|
||
Implement the NSAccessibility text protocol for Emacs buffer windows.
|
||
|
||
* src/nsterm.m (ns_ax_find_completion_overlay_range): New function.
|
||
(ns_ax_event_is_line_nav_key, ns_ax_completion_text_for_span): New
|
||
functions.
|
||
(EmacsAccessibilityBuffer): Implement core NSAccessibility protocol.
|
||
(ensureTextCache): Validity gated on BUF_MODIFF to catch fold/unfold
|
||
commands (org-mode, outline-mode, hideshow-mode) that change the
|
||
'invisible text property without modifying character content.
|
||
BUF_CHARS_MODIFF would serve stale AX text after org-cycle or similar.
|
||
ensureTextCache is called only from AX getters at human interaction
|
||
speed, not from the redisplay notification path.
|
||
(accessibilityIndexForCharpos:): O(1) fast path for pure-ASCII runs.
|
||
(charposForAccessibilityIndex:): Symmetric O(1) fast path.
|
||
(accessibilityRole, accessibilityLabel, accessibilityValue)
|
||
(accessibilityNumberOfCharacters, accessibilitySelectedText)
|
||
(accessibilitySelectedTextRange, accessibilityInsertionPointLineNumber)
|
||
(accessibilityLineForIndex:, accessibilityRangeForLine:)
|
||
(accessibilityRangeForIndex:, accessibilityStyleRangeForIndex:)
|
||
(accessibilityFrameForRange:, accessibilityRangeForPosition:)
|
||
(accessibilityVisibleCharacterRange, accessibilityFrame)
|
||
(setAccessibilitySelectedTextRange:)
|
||
(setAccessibilityFocused:): Implement NSAccessibility protocol methods.
|
||
---
|
||
src/nsterm.m | 1135 +++++++++++++++++++++++++++++++++++++++++++++++++-
|
||
1 file changed, 1133 insertions(+), 2 deletions(-)
|
||
|
||
diff --git a/src/nsterm.m b/src/nsterm.m
|
||
index 9c53001e37..e4b3fb17a0 100644
|
||
--- a/src/nsterm.m
|
||
+++ b/src/nsterm.m
|
||
@@ -7648,6 +7648,1137 @@ - (void)invalidateInteractiveSpans
|
||
|
||
@end
|
||
|
||
+static BOOL
|
||
+ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point,
|
||
+ ptrdiff_t *out_start,
|
||
+ ptrdiff_t *out_end)
|
||
+{
|
||
+ if (!b || !out_start || !out_end)
|
||
+ return NO;
|
||
+
|
||
+ Lisp_Object faceSym = Qns_ax_completions_highlight;
|
||
+ ptrdiff_t begv = BUF_BEGV (b);
|
||
+ ptrdiff_t zv = BUF_ZV (b);
|
||
+ ptrdiff_t best_start = 0;
|
||
+ ptrdiff_t best_end = 0;
|
||
+ ptrdiff_t best_dist = PTRDIFF_MAX;
|
||
+ BOOL found = NO;
|
||
+
|
||
+ /* Fast path: look at point and immediate neighbors first.
|
||
+ Prefer point+1 over point-1: when Tab moves to a new completion,
|
||
+ point is at the START of the new entry while point-1 is still
|
||
+ inside the previous entry's overlay. Forward probe finds the
|
||
+ correct new entry; backward probe finds the wrong old one. */
|
||
+ ptrdiff_t probes[3] = { point, point + 1, point - 1 };
|
||
+ for (int i = 0; i < 3 && !found; i++)
|
||
+ {
|
||
+ ptrdiff_t p = probes[i];
|
||
+ if (p < begv || p > zv)
|
||
+ continue;
|
||
+
|
||
+ Lisp_Object overlays = Foverlays_at (make_fixnum (p), Qnil);
|
||
+ Lisp_Object tail;
|
||
+ for (tail = overlays; CONSP (tail); tail = XCDR (tail))
|
||
+ {
|
||
+ Lisp_Object ov = XCAR (tail);
|
||
+ Lisp_Object face = Foverlay_get (ov, Qface);
|
||
+ if (!(EQ (face, faceSym)
|
||
+ || (CONSP (face) && !NILP (Fmemq (faceSym, face)))))
|
||
+ continue;
|
||
+
|
||
+ ptrdiff_t ov_start = OVERLAY_START (ov);
|
||
+ ptrdiff_t ov_end = OVERLAY_END (ov);
|
||
+ if (ov_end <= ov_start)
|
||
+ continue;
|
||
+
|
||
+ best_start = ov_start;
|
||
+ best_end = ov_end;
|
||
+ best_dist = 0;
|
||
+ found = YES;
|
||
+ break;
|
||
+ }
|
||
+ }
|
||
+
|
||
+ if (!found)
|
||
+ {
|
||
+ /* Bulk query: get all overlays in the buffer at once.
|
||
+ Avoids the previous O(n) per-character Foverlays_at loop. */
|
||
+ Lisp_Object all = Foverlays_in (make_fixnum (begv),
|
||
+ make_fixnum (zv));
|
||
+ Lisp_Object tail;
|
||
+ for (tail = all; CONSP (tail); tail = XCDR (tail))
|
||
+ {
|
||
+ Lisp_Object ov = XCAR (tail);
|
||
+ Lisp_Object face = Foverlay_get (ov, Qface);
|
||
+ if (!(EQ (face, faceSym)
|
||
+ || (CONSP (face)
|
||
+ && !NILP (Fmemq (faceSym, face)))))
|
||
+ continue;
|
||
+
|
||
+ ptrdiff_t ov_start = OVERLAY_START (ov);
|
||
+ ptrdiff_t ov_end = OVERLAY_END (ov);
|
||
+ if (ov_end <= ov_start)
|
||
+ continue;
|
||
+
|
||
+ ptrdiff_t dist = 0;
|
||
+ if (point < ov_start)
|
||
+ dist = ov_start - point;
|
||
+ else if (point > ov_end)
|
||
+ dist = point - ov_end;
|
||
+
|
||
+ if (!found || dist < best_dist)
|
||
+ {
|
||
+ best_start = ov_start;
|
||
+ best_end = ov_end;
|
||
+ best_dist = dist;
|
||
+ found = YES;
|
||
+ }
|
||
+ }
|
||
+ }
|
||
+
|
||
+ if (!found)
|
||
+ return NO;
|
||
+
|
||
+ *out_start = best_start;
|
||
+ *out_end = best_end;
|
||
+ return YES;
|
||
+}
|
||
+
|
||
+/* 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
|
||
+ns_ax_event_is_line_nav_key (int *which)
|
||
+{
|
||
+ /* 1. Check Vthis_command for known navigation command symbols.
|
||
+ All symbols are registered via DEFSYM in syms_of_nsterm to avoid
|
||
+ per-call obarray lookups in this hot path (runs every cursor move). */
|
||
+ if (SYMBOLP (Vthis_command) && !NILP (Vthis_command))
|
||
+ {
|
||
+ Lisp_Object cmd = Vthis_command;
|
||
+ /* Forward line commands. */
|
||
+ if (EQ (cmd, Qnext_line)
|
||
+ || EQ (cmd, Qdired_next_line))
|
||
+ {
|
||
+ if (which) *which = 1;
|
||
+ return true;
|
||
+ }
|
||
+ /* Backward line commands. */
|
||
+ if (EQ (cmd, Qprevious_line)
|
||
+ || EQ (cmd, Qdired_previous_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;
|
||
+ }
|
||
+ if (FIXNUMP (ev) && XFIXNUM (ev) == 9) /* Tab */
|
||
+ {
|
||
+ if (which) *which = 1;
|
||
+ return true;
|
||
+ }
|
||
+ return false;
|
||
+}
|
||
+
|
||
+
|
||
+
|
||
+static NSString *
|
||
+ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
|
||
+ struct buffer *b,
|
||
+ ptrdiff_t start,
|
||
+ ptrdiff_t end,
|
||
+ NSString *cachedText)
|
||
+{
|
||
+ if (!elem || !b || !cachedText || end <= start)
|
||
+ return nil;
|
||
+
|
||
+ NSString *text = nil;
|
||
+ specpdl_ref count = SPECPDL_INDEX ();
|
||
+ record_unwind_current_buffer ();
|
||
+ /* Block input to prevent concurrent redisplay from modifying buffer
|
||
+ state while we read text properties. Unwind-protected so
|
||
+ block_input is always matched by unblock_input on signal. */
|
||
+ record_unwind_protect_void (unblock_input);
|
||
+ block_input ();
|
||
+ if (b != current_buffer)
|
||
+ set_buffer_internal_1 (b);
|
||
+
|
||
+ /* Prefer canonical completion candidate string from text property.
|
||
+ Try both completion--string (new API, set by minibuffer.el) and
|
||
+ completion (older API used by some modes). */
|
||
+ ptrdiff_t probes[2] = { start, end - 1 };
|
||
+ for (int i = 0; i < 2 && !text; i++)
|
||
+ {
|
||
+ ptrdiff_t p = probes[i];
|
||
+ Lisp_Object cstr = Fget_char_property (make_fixnum (p),
|
||
+ Qns_ax_completion__string,
|
||
+ Qnil);
|
||
+ if (STRINGP (cstr))
|
||
+ text = [NSString stringWithLispString:cstr];
|
||
+ if (!text)
|
||
+ {
|
||
+ /* Fallback: 'completion property used by display-completion-list. */
|
||
+ cstr = Fget_char_property (make_fixnum (p),
|
||
+ Qns_ax_completion,
|
||
+ Qnil);
|
||
+ if (STRINGP (cstr))
|
||
+ text = [NSString stringWithLispString:cstr];
|
||
+ }
|
||
+ }
|
||
+
|
||
+ if (!text)
|
||
+ {
|
||
+ NSUInteger ax_s = [elem accessibilityIndexForCharpos:start];
|
||
+ NSUInteger ax_e = [elem accessibilityIndexForCharpos:end];
|
||
+ if (ax_e > ax_s && ax_e <= [cachedText length])
|
||
+ text = [cachedText substringWithRange:NSMakeRange (ax_s, ax_e - ax_s)];
|
||
+ }
|
||
+
|
||
+ unbind_to (count, Qnil);
|
||
+
|
||
+ if (text)
|
||
+ {
|
||
+ text = [text stringByTrimmingCharactersInSet:
|
||
+ [NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||
+ if ([text length] == 0)
|
||
+ text = nil;
|
||
+ }
|
||
+
|
||
+ return text;
|
||
+}
|
||
+
|
||
+@implementation EmacsAccessibilityBuffer
|
||
+@synthesize cachedText;
|
||
+@synthesize cachedTextModiff;
|
||
+@synthesize cachedOverlayModiff;
|
||
+@synthesize cachedTextStart;
|
||
+@synthesize cachedModiff;
|
||
+@synthesize cachedPoint;
|
||
+@synthesize cachedMarkActive;
|
||
+@synthesize cachedCompletionAnnouncement;
|
||
+@synthesize cachedCompletionOverlayStart;
|
||
+@synthesize cachedCompletionOverlayEnd;
|
||
+@synthesize cachedCompletionPoint;
|
||
+
|
||
+- (void)dealloc
|
||
+{
|
||
+ [cachedText release];
|
||
+ [cachedCompletionAnnouncement release];
|
||
+ [cachedInteractiveSpans release];
|
||
+ if (visibleRuns)
|
||
+ xfree (visibleRuns);
|
||
+ if (lineStartOffsets)
|
||
+ xfree (lineStartOffsets);
|
||
+ [super dealloc];
|
||
+}
|
||
+
|
||
+/* ---- Text cache ---- */
|
||
+
|
||
+- (void)invalidateTextCache
|
||
+{
|
||
+ @synchronized (self)
|
||
+ {
|
||
+ [cachedText release];
|
||
+ cachedText = nil;
|
||
+ if (visibleRuns)
|
||
+ {
|
||
+ xfree (visibleRuns);
|
||
+ visibleRuns = NULL;
|
||
+ }
|
||
+ visibleRunCount = 0;
|
||
+ if (lineStartOffsets)
|
||
+ {
|
||
+ xfree (lineStartOffsets);
|
||
+ lineStartOffsets = NULL;
|
||
+ }
|
||
+ lineCount = 0;
|
||
+ }
|
||
+ [self invalidateInteractiveSpans];
|
||
+}
|
||
+
|
||
+/* ---- Line index helpers ---- */
|
||
+
|
||
+/* Return the line number for AX string index IDX using the
|
||
+ precomputed lineStartOffsets array. Binary search: O(log L)
|
||
+ where L is the number of lines in the cached text.
|
||
+
|
||
+ lineStartOffsets[i] holds the AX string index where line i
|
||
+ begins. Built once per cache rebuild in ensureTextCache. */
|
||
+- (NSInteger)lineForAXIndex:(NSUInteger)idx
|
||
+{
|
||
+ @synchronized (self)
|
||
+ {
|
||
+ if (!lineStartOffsets || lineCount == 0)
|
||
+ return 0;
|
||
+
|
||
+ /* Binary search for the largest line whose start offset <= idx. */
|
||
+ NSUInteger lo = 0, hi = lineCount;
|
||
+ while (lo < hi)
|
||
+ {
|
||
+ NSUInteger mid = lo + (hi - lo) / 2;
|
||
+ if (lineStartOffsets[mid] <= idx)
|
||
+ lo = mid + 1;
|
||
+ else
|
||
+ hi = mid;
|
||
+ }
|
||
+ return (NSInteger) (lo > 0 ? lo - 1 : 0);
|
||
+ }
|
||
+}
|
||
+
|
||
+/* Return the AX string range for LINE using the precomputed
|
||
+ lineStartOffsets array. O(1) lookup. */
|
||
+- (NSRange)rangeForLine:(NSUInteger)line textLength:(NSUInteger)tlen
|
||
+{
|
||
+ @synchronized (self)
|
||
+ {
|
||
+ if (!lineStartOffsets || lineCount == 0)
|
||
+ return NSMakeRange (NSNotFound, 0);
|
||
+
|
||
+ if (line >= lineCount)
|
||
+ return NSMakeRange (NSNotFound, 0);
|
||
+
|
||
+ NSUInteger start = lineStartOffsets[line];
|
||
+ NSUInteger end = (line + 1 < lineCount)
|
||
+ ? lineStartOffsets[line + 1]
|
||
+ : tlen;
|
||
+ return NSMakeRange (start, end - start);
|
||
+ }
|
||
+}
|
||
+
|
||
+- (void)ensureTextCache
|
||
+{
|
||
+ NSTRACE ("EmacsAccessibilityBuffer ensureTextCache");
|
||
+ /* This method is only called from the main thread (AX getters
|
||
+ dispatch_sync to main first). Reads of cachedText/cachedTextModiff
|
||
+ below are therefore safe without @synchronized --- only the
|
||
+ write section at the end needs synchronization to protect
|
||
+ against concurrent reads from AX server thread. */
|
||
+ eassert ([NSThread isMainThread]);
|
||
+ struct window *w = [self validWindow];
|
||
+ if (!w || !WINDOW_LEAF_P (w))
|
||
+ return;
|
||
+
|
||
+ struct buffer *b = XBUFFER (w->contents);
|
||
+ if (!b)
|
||
+ return;
|
||
+
|
||
+ /* Use BUF_MODIFF, not BUF_CHARS_MODIFF, for cache validity.
|
||
+ Fold/unfold commands (org-mode, outline-mode, hideshow-mode) change
|
||
+ text visibility by modifying the 'invisible text property via
|
||
+ put-text-property or add-text-properties. These bump BUF_MODIFF
|
||
+ but not BUF_CHARS_MODIFF. Using BUF_CHARS_MODIFF would serve stale
|
||
+ AX text across fold/unfold, causing VoiceOver to read the wrong
|
||
+ content after an org-cycle or similar command.
|
||
+ ensureTextCache is called exclusively from AX getters at human
|
||
+ interaction speed (never from the redisplay notification path), so
|
||
+ font-lock passes cause zero rebuild cost via the notification path.
|
||
+ Do NOT track BUF_OVERLAY_MODIFF here --- overlay text is not
|
||
+ included in the cached AX text (it is handled separately via
|
||
+ explicit announcements in postAccessibilityNotificationsForFrame).
|
||
+ Including overlay_modiff would silently update cachedOverlayModiff
|
||
+ and prevent the notification dispatch from detecting changes. */
|
||
+ ptrdiff_t modiff = BUF_MODIFF (b);
|
||
+ ptrdiff_t pt = BUF_PT (b);
|
||
+ NSUInteger textLen = cachedText ? [cachedText length] : 0;
|
||
+ if (cachedText && cachedTextModiff == modiff
|
||
+ && cachedTextStart == BUF_BEGV (b)
|
||
+ && pt >= cachedTextStart
|
||
+ && (textLen == 0
|
||
+ || [self accessibilityIndexForCharpos:pt] <= textLen))
|
||
+ return;
|
||
+
|
||
+ ptrdiff_t start;
|
||
+ ns_ax_visible_run *runs = NULL;
|
||
+ NSUInteger nruns = 0;
|
||
+ NSString *text = ns_ax_buffer_text (w, &start, &runs, &nruns);
|
||
+
|
||
+ @synchronized (self)
|
||
+ {
|
||
+ [cachedText release];
|
||
+ cachedText = [text retain];
|
||
+ cachedTextModiff = modiff;
|
||
+ cachedTextStart = start;
|
||
+
|
||
+ if (visibleRuns)
|
||
+ xfree (visibleRuns);
|
||
+ visibleRuns = runs;
|
||
+ visibleRunCount = nruns;
|
||
+
|
||
+ /* Build line-start index for O(log L) line queries.
|
||
+ Walk the cached text once, recording the start offset of each
|
||
+ line. Uses NSString lineRangeForRange: --- O(N) in the total
|
||
+ text --- but this loop runs only on cache rebuild, which is
|
||
+ gated on BUF_MODIFF. Font-lock passes trigger a rebuild only
|
||
+ when called from AX getters (human interaction speed), never
|
||
+ from the notification path. */
|
||
+ if (lineStartOffsets)
|
||
+ xfree (lineStartOffsets);
|
||
+ lineStartOffsets = NULL;
|
||
+ lineCount = 0;
|
||
+
|
||
+ NSUInteger tlen = [cachedText length];
|
||
+ if (tlen > 0)
|
||
+ {
|
||
+ NSUInteger cap = 256;
|
||
+ lineStartOffsets = xmalloc (cap * sizeof (NSUInteger));
|
||
+ lineStartOffsets[0] = 0;
|
||
+ lineCount = 1;
|
||
+ NSUInteger pos = 0;
|
||
+ while (pos < tlen)
|
||
+ {
|
||
+ NSRange lr = [cachedText lineRangeForRange:
|
||
+ NSMakeRange (pos, 0)];
|
||
+ NSUInteger next = NSMaxRange (lr);
|
||
+ if (next <= pos)
|
||
+ break; /* safety */
|
||
+ if (next < tlen)
|
||
+ {
|
||
+ if (lineCount >= cap)
|
||
+ {
|
||
+ cap *= 2;
|
||
+ lineStartOffsets = xrealloc (lineStartOffsets,
|
||
+ cap * sizeof (NSUInteger));
|
||
+ }
|
||
+ lineStartOffsets[lineCount] = next;
|
||
+ lineCount++;
|
||
+ }
|
||
+ pos = next;
|
||
+ }
|
||
+ }
|
||
+ }
|
||
+}
|
||
+
|
||
+/* ---- Index mapping ---- */
|
||
+
|
||
+/* Convert buffer charpos to accessibility string index. */
|
||
+- (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos
|
||
+{
|
||
+ /* This method may be called from the AX server thread.
|
||
+ Synchronize on self to prevent use-after-free if the main
|
||
+ thread invalidates the text cache concurrently. */
|
||
+ @synchronized (self)
|
||
+ {
|
||
+ if (visibleRunCount == 0)
|
||
+ return 0;
|
||
+
|
||
+ /* Binary search: runs are sorted by charpos (ascending). Find the
|
||
+ run whose [charpos, charpos+length) range contains the target,
|
||
+ or the nearest run after an invisible gap. O(log n) instead of
|
||
+ O(n) --- matters for org-mode with many folded sections. */
|
||
+ NSUInteger lo = 0, hi = visibleRunCount;
|
||
+ while (lo < hi)
|
||
+ {
|
||
+ NSUInteger mid = lo + (hi - lo) / 2;
|
||
+ ns_ax_visible_run *r = &visibleRuns[mid];
|
||
+ if (charpos < r->charpos)
|
||
+ hi = mid;
|
||
+ else if (charpos >= r->charpos + r->length)
|
||
+ lo = mid + 1;
|
||
+ else
|
||
+ {
|
||
+ /* Found: charpos is inside this run. Compute UTF-16 delta.
|
||
+ Fast path for pure-ASCII runs (ax_length == length): every
|
||
+ Emacs charpos maps to exactly one UTF-16 code unit, so the
|
||
+ conversion is O(1). This matters because ensureTextCache
|
||
+ calls this method on every redisplay frame to validate the
|
||
+ cache --- a O(cursor_position) loop here means O(position)
|
||
+ cost per frame even when the buffer is unchanged.
|
||
+ Multi-byte runs fall through to the sequence walk, bounded
|
||
+ by run length (visible window), not total buffer size. */
|
||
+ NSUInteger chars_in = (NSUInteger)(charpos - r->charpos);
|
||
+ if (chars_in == 0)
|
||
+ return r->ax_start;
|
||
+ if (r->ax_length == (NSUInteger) r->length)
|
||
+ return r->ax_start + chars_in;
|
||
+ if (!cachedText)
|
||
+ return r->ax_start;
|
||
+ NSUInteger run_end_ax = r->ax_start + r->ax_length;
|
||
+ NSUInteger scan = r->ax_start;
|
||
+ for (NSUInteger c = 0; c < chars_in && scan < run_end_ax; c++)
|
||
+ {
|
||
+ NSRange seq = [cachedText
|
||
+ rangeOfComposedCharacterSequenceAtIndex:scan];
|
||
+ scan = NSMaxRange (seq);
|
||
+ }
|
||
+ return (scan <= run_end_ax) ? scan : run_end_ax;
|
||
+ }
|
||
+ }
|
||
+ /* charpos falls in an invisible gap or past the end. */
|
||
+ if (lo < visibleRunCount)
|
||
+ return visibleRuns[lo].ax_start;
|
||
+ ns_ax_visible_run *last = &visibleRuns[visibleRunCount - 1];
|
||
+ return last->ax_start + last->ax_length;
|
||
+ } /* @synchronized */
|
||
+}
|
||
+
|
||
+/* Convert accessibility string index to buffer charpos.
|
||
+ Safe to call from any thread: uses only cachedText (NSString) and
|
||
+ visibleRuns --- no Lisp calls. */
|
||
+- (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx
|
||
+{
|
||
+ /* May be called from AX server thread --- synchronize. */
|
||
+ @synchronized (self)
|
||
+ {
|
||
+ if (visibleRunCount == 0)
|
||
+ return cachedTextStart;
|
||
+
|
||
+ /* Binary search: runs are sorted by ax_start (ascending). */
|
||
+ NSUInteger lo = 0, hi = visibleRunCount;
|
||
+ while (lo < hi)
|
||
+ {
|
||
+ NSUInteger mid = lo + (hi - lo) / 2;
|
||
+ ns_ax_visible_run *r = &visibleRuns[mid];
|
||
+ if (ax_idx < r->ax_start)
|
||
+ hi = mid;
|
||
+ else if (ax_idx >= r->ax_start + r->ax_length)
|
||
+ lo = mid + 1;
|
||
+ else
|
||
+ {
|
||
+ /* Found: ax_idx is inside this run.
|
||
+ Fast path for pure-ASCII runs: ax_length == length means
|
||
+ every Emacs charpos maps to exactly one AX string index.
|
||
+ The conversion is then O(1) instead of O(cursor_position).
|
||
+ Buffers with emoji, CJK, or other non-BMP characters use
|
||
+ the slow path (composed character sequence walk), which is
|
||
+ bounded by run length, not total buffer size. */
|
||
+ if (r->ax_length == (NSUInteger) r->length)
|
||
+ return r->charpos + (ptrdiff_t) (ax_idx - r->ax_start);
|
||
+
|
||
+ if (!cachedText)
|
||
+ return r->charpos;
|
||
+ NSUInteger scan = r->ax_start;
|
||
+ ptrdiff_t cp = r->charpos;
|
||
+ while (scan < ax_idx)
|
||
+ {
|
||
+ NSRange seq = [cachedText
|
||
+ rangeOfComposedCharacterSequenceAtIndex:scan];
|
||
+ scan = NSMaxRange (seq);
|
||
+ cp++;
|
||
+ }
|
||
+ return cp;
|
||
+ }
|
||
+ }
|
||
+ /* Past end --- return last charpos. */
|
||
+ if (lo > 0)
|
||
+ {
|
||
+ ns_ax_visible_run *last = &visibleRuns[visibleRunCount - 1];
|
||
+ return last->charpos + last->length;
|
||
+ }
|
||
+ return cachedTextStart;
|
||
+ } /* @synchronized */
|
||
+}
|
||
+
|
||
+/* --- Threading and signal safety ---
|
||
+
|
||
+ AX getter methods may be called from the VoiceOver server thread.
|
||
+ All methods that access Lisp objects or buffer state dispatch_sync
|
||
+ to the main thread where Emacs state is consistent.
|
||
+
|
||
+ Longjmp safety: Lisp functions called inside dispatch_sync blocks
|
||
+ (Fget_char_property, Fbuffer_substring_no_properties, etc.) could
|
||
+ theoretically signal and longjmp through the dispatch_sync frame,
|
||
+ deadlocking the AX server thread. This is prevented by:
|
||
+
|
||
+ 1. validWindow checks WINDOW_LIVE_P and BUFFERP before every
|
||
+ Lisp access --- the window and buffer are verified live.
|
||
+ 2. All dispatch_sync blocks run on the main thread where no
|
||
+ concurrent Lisp code can modify state between checks.
|
||
+ 3. block_input prevents timer events and process output from
|
||
+ running between precondition checks and Lisp calls.
|
||
+ 4. Buffer positions are clamped to BUF_BEGV/BUF_ZV before
|
||
+ use, preventing out-of-range signals.
|
||
+ 5. specpdl unwind protection ensures block_input is always
|
||
+ matched by unblock_input, even on longjmp.
|
||
+
|
||
+ This matches the safety model of existing nsterm.m F-function
|
||
+ calls (24 direct calls, none wrapped in internal_condition_case).
|
||
+
|
||
+ Known gap: if the Emacs window tree is modified between redisplay
|
||
+ cycles in a way that invalidates validWindow's cached result,
|
||
+ a stale dereference could occur. In practice this does not happen
|
||
+ because window tree modifications go through the event loop which
|
||
+ we are blocking via dispatch_sync. */
|
||
+
|
||
+/* ---- NSAccessibility protocol ---- */
|
||
+
|
||
+- (NSAccessibilityRole)accessibilityRole
|
||
+{
|
||
+ if (![NSThread isMainThread])
|
||
+ {
|
||
+ __block NSAccessibilityRole result;
|
||
+ dispatch_sync (dispatch_get_main_queue (), ^{
|
||
+ result = [self accessibilityRole];
|
||
+ });
|
||
+ return result;
|
||
+ }
|
||
+ struct window *w = [self validWindow];
|
||
+ if (w && MINI_WINDOW_P (w))
|
||
+ return NSAccessibilityTextFieldRole;
|
||
+ return NSAccessibilityTextAreaRole;
|
||
+}
|
||
+
|
||
+- (NSString *)accessibilityPlaceholderValue
|
||
+{
|
||
+ if (![NSThread isMainThread])
|
||
+ {
|
||
+ __block NSString *result;
|
||
+ dispatch_sync (dispatch_get_main_queue (), ^{
|
||
+ result = [self accessibilityPlaceholderValue];
|
||
+ });
|
||
+ return result;
|
||
+ }
|
||
+ struct window *w = [self validWindow];
|
||
+ if (!w || !MINI_WINDOW_P (w))
|
||
+ return nil;
|
||
+ Lisp_Object prompt = Fminibuffer_prompt ();
|
||
+ if (STRINGP (prompt))
|
||
+ return [NSString stringWithLispString: prompt];
|
||
+ return nil;
|
||
+}
|
||
+
|
||
+- (NSString *)accessibilityRoleDescription
|
||
+{
|
||
+ if (![NSThread isMainThread])
|
||
+ {
|
||
+ __block NSString *result;
|
||
+ dispatch_sync (dispatch_get_main_queue (), ^{
|
||
+ result = [self accessibilityRoleDescription];
|
||
+ });
|
||
+ return result;
|
||
+ }
|
||
+ struct window *w = [self validWindow];
|
||
+ if (w && MINI_WINDOW_P (w))
|
||
+ return @"minibuffer";
|
||
+ return @"editor";
|
||
+}
|
||
+
|
||
+- (NSString *)accessibilityLabel
|
||
+{
|
||
+ if (![NSThread isMainThread])
|
||
+ {
|
||
+ __block NSString *result;
|
||
+ dispatch_sync (dispatch_get_main_queue (), ^{
|
||
+ result = [self accessibilityLabel];
|
||
+ });
|
||
+ return result;
|
||
+ }
|
||
+ struct window *w = [self validWindow];
|
||
+ if (w && WINDOW_LEAF_P (w))
|
||
+ {
|
||
+ if (MINI_WINDOW_P (w))
|
||
+ return @"Minibuffer";
|
||
+
|
||
+ struct buffer *b = XBUFFER (w->contents);
|
||
+ if (b)
|
||
+ {
|
||
+ Lisp_Object name = BVAR (b, name);
|
||
+ if (STRINGP (name))
|
||
+ return [NSString stringWithLispString:name];
|
||
+ }
|
||
+ }
|
||
+ return @"buffer";
|
||
+}
|
||
+
|
||
+- (BOOL)isAccessibilityFocused
|
||
+{
|
||
+ if (![NSThread isMainThread])
|
||
+ {
|
||
+ __block BOOL result;
|
||
+ dispatch_sync (dispatch_get_main_queue (), ^{
|
||
+ result = [self isAccessibilityFocused];
|
||
+ });
|
||
+ return result;
|
||
+ }
|
||
+ struct window *w = [self validWindow];
|
||
+ if (!w)
|
||
+ return NO;
|
||
+ EmacsView *view = self.emacsView;
|
||
+ if (!view || !view->emacsframe)
|
||
+ return NO;
|
||
+ struct frame *f = view->emacsframe;
|
||
+ return (w == XWINDOW (f->selected_window));
|
||
+}
|
||
+
|
||
+- (id)accessibilityValue
|
||
+{
|
||
+ /* AX getters can be called from any thread by the AT subsystem.
|
||
+ Dispatch to main thread where Emacs buffer state is consistent. */
|
||
+ if (![NSThread isMainThread])
|
||
+ {
|
||
+ __block id result;
|
||
+ dispatch_sync (dispatch_get_main_queue (), ^{
|
||
+ result = [self accessibilityValue];
|
||
+ });
|
||
+ return result;
|
||
+ }
|
||
+ [self ensureTextCache];
|
||
+ return cachedText ? cachedText : @"";
|
||
+}
|
||
+
|
||
+- (NSInteger)accessibilityNumberOfCharacters
|
||
+{
|
||
+ if (![NSThread isMainThread])
|
||
+ {
|
||
+ __block NSInteger result;
|
||
+ dispatch_sync (dispatch_get_main_queue (), ^{
|
||
+ result = [self accessibilityNumberOfCharacters];
|
||
+ });
|
||
+ return result;
|
||
+ }
|
||
+ [self ensureTextCache];
|
||
+ return cachedText ? [cachedText length] : 0;
|
||
+}
|
||
+
|
||
+- (NSString *)accessibilitySelectedText
|
||
+{
|
||
+ if (![NSThread isMainThread])
|
||
+ {
|
||
+ __block NSString *result;
|
||
+ dispatch_sync (dispatch_get_main_queue (), ^{
|
||
+ result = [self accessibilitySelectedText];
|
||
+ });
|
||
+ return result;
|
||
+ }
|
||
+ struct window *w = [self validWindow];
|
||
+ if (!w || !WINDOW_LEAF_P (w))
|
||
+ return @"";
|
||
+
|
||
+ struct buffer *b = XBUFFER (w->contents);
|
||
+ if (!b || NILP (BVAR (b, mark_active)))
|
||
+ return @"";
|
||
+
|
||
+ NSRange sel = [self accessibilitySelectedTextRange];
|
||
+ [self ensureTextCache];
|
||
+ if (!cachedText || sel.location == NSNotFound
|
||
+ || sel.location + sel.length > [cachedText length])
|
||
+ return @"";
|
||
+ return [cachedText substringWithRange:sel];
|
||
+}
|
||
+
|
||
+- (NSRange)accessibilitySelectedTextRange
|
||
+{
|
||
+ if (![NSThread isMainThread])
|
||
+ {
|
||
+ __block NSRange result;
|
||
+ dispatch_sync (dispatch_get_main_queue (), ^{
|
||
+ result = [self accessibilitySelectedTextRange];
|
||
+ });
|
||
+ return result;
|
||
+ }
|
||
+ struct window *w = [self validWindow];
|
||
+ if (!w || !WINDOW_LEAF_P (w))
|
||
+ return NSMakeRange (0, 0);
|
||
+
|
||
+ if (!BUFFERP (w->contents))
|
||
+ return NSMakeRange (0, 0);
|
||
+ struct buffer *b = XBUFFER (w->contents);
|
||
+ if (!b)
|
||
+ return NSMakeRange (0, 0);
|
||
+
|
||
+ [self ensureTextCache];
|
||
+ ptrdiff_t pt = BUF_PT (b);
|
||
+ NSUInteger point_idx = [self accessibilityIndexForCharpos:pt];
|
||
+
|
||
+ if (NILP (BVAR (b, mark_active)))
|
||
+ return NSMakeRange (point_idx, 0);
|
||
+
|
||
+ ptrdiff_t mark_pos = marker_position (BVAR (b, mark));
|
||
+ NSUInteger mark_idx = [self accessibilityIndexForCharpos:mark_pos];
|
||
+ NSUInteger start_idx = MIN (point_idx, mark_idx);
|
||
+ NSUInteger end_idx = MAX (point_idx, mark_idx);
|
||
+ return NSMakeRange (start_idx, end_idx - start_idx);
|
||
+}
|
||
+
|
||
+- (void)setAccessibilitySelectedTextRange:(NSRange)range
|
||
+{
|
||
+ if (![NSThread isMainThread])
|
||
+ {
|
||
+ dispatch_async (dispatch_get_main_queue (), ^{
|
||
+ [self setAccessibilitySelectedTextRange:range];
|
||
+ });
|
||
+ return;
|
||
+ }
|
||
+ struct window *w = [self validWindow];
|
||
+ if (!w || !WINDOW_LEAF_P (w))
|
||
+ return;
|
||
+
|
||
+ struct buffer *b = XBUFFER (w->contents);
|
||
+ if (!b)
|
||
+ return;
|
||
+
|
||
+ [self ensureTextCache];
|
||
+
|
||
+ specpdl_ref count = SPECPDL_INDEX ();
|
||
+ record_unwind_current_buffer ();
|
||
+ /* Ensure block_input is always matched by unblock_input even if
|
||
+ Fset_marker or another Lisp call signals (longjmp). */
|
||
+ record_unwind_protect_void (unblock_input);
|
||
+ block_input ();
|
||
+
|
||
+ /* Convert accessibility index to buffer charpos via mapping. */
|
||
+ ptrdiff_t charpos = [self charposForAccessibilityIndex:range.location];
|
||
+
|
||
+ /* Clamp to buffer bounds. */
|
||
+ if (charpos < BUF_BEGV (b))
|
||
+ charpos = BUF_BEGV (b);
|
||
+ if (charpos > BUF_ZV (b))
|
||
+ charpos = BUF_ZV (b);
|
||
+
|
||
+ /* Move point directly in the buffer. */
|
||
+ if (b != current_buffer)
|
||
+ set_buffer_internal_1 (b);
|
||
+
|
||
+ SET_PT_BOTH (charpos, CHAR_TO_BYTE (charpos));
|
||
+
|
||
+ /* Always deactivate mark: VoiceOver range.length is an internal
|
||
+ word boundary hint, not a text selection. Activating the mark
|
||
+ makes accessibilitySelectedTextRange return a non-zero length,
|
||
+ which confuses VoiceOver into positioning its browse cursor at
|
||
+ the END of the selection instead of the start. */
|
||
+ bset_mark_active (b, Qnil);
|
||
+ unbind_to (count, Qnil);
|
||
+
|
||
+ /* Update cached state so the next notification cycle doesn't
|
||
+ re-announce this movement. */
|
||
+ self.cachedPoint = charpos;
|
||
+ self.cachedMarkActive = (range.length > 0);
|
||
+}
|
||
+
|
||
+- (void)setAccessibilityFocused:(BOOL)flag
|
||
+{
|
||
+ if (!flag)
|
||
+ 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];
|
||
+ if (!w || !WINDOW_LEAF_P (w))
|
||
+ return;
|
||
+
|
||
+ EmacsView *view = self.emacsView;
|
||
+ if (!view || !view->emacsframe)
|
||
+ return;
|
||
+
|
||
+ /* Use specpdl unwind protection for block_input safety. */
|
||
+ specpdl_ref count = SPECPDL_INDEX ();
|
||
+ record_unwind_protect_void (unblock_input);
|
||
+ block_input ();
|
||
+
|
||
+ /* Select the Emacs window so keyboard focus follows VoiceOver. */
|
||
+ struct frame *f = view->emacsframe;
|
||
+ if (w != XWINDOW (f->selected_window))
|
||
+ Fselect_window (self.lispWindow, Qnil);
|
||
+
|
||
+ /* Raise the frame's NS window to ensure keyboard focus. */
|
||
+ NSWindow *nswin = [view window];
|
||
+ if (nswin && ![nswin isKeyWindow])
|
||
+ [nswin makeKeyAndOrderFront:nil];
|
||
+
|
||
+ unbind_to (count, Qnil);
|
||
+
|
||
+ /* Post SelectedTextChanged so VoiceOver reads the current line
|
||
+ upon entering text interaction mode.
|
||
+ WebKit AXObjectCacheMac fallback enum: SelectionMove = 2. */
|
||
+ NSDictionary *info = @{
|
||
+ @"AXTextStateChangeType":
|
||
+ @(ns_ax_text_state_change_selection_move),
|
||
+ @"AXTextChangeElement": self
|
||
+ };
|
||
+ ns_ax_post_notification_with_info (
|
||
+ self, NSAccessibilitySelectedTextChangedNotification, info);
|
||
+}
|
||
+
|
||
+- (NSInteger)accessibilityInsertionPointLineNumber
|
||
+{
|
||
+ if (![NSThread isMainThread])
|
||
+ {
|
||
+ __block NSInteger result;
|
||
+ dispatch_sync (dispatch_get_main_queue (), ^{
|
||
+ result = [self accessibilityInsertionPointLineNumber];
|
||
+ });
|
||
+ return result;
|
||
+ }
|
||
+ struct window *w = [self validWindow];
|
||
+ if (!w || !WINDOW_LEAF_P (w))
|
||
+ return 0;
|
||
+
|
||
+ struct buffer *b = XBUFFER (w->contents);
|
||
+ if (!b)
|
||
+ return 0;
|
||
+
|
||
+ [self ensureTextCache];
|
||
+ if (!cachedText)
|
||
+ return 0;
|
||
+
|
||
+ ptrdiff_t pt = BUF_PT (b);
|
||
+ NSUInteger point_idx = [self accessibilityIndexForCharpos:pt];
|
||
+ if (point_idx > [cachedText length])
|
||
+ point_idx = [cachedText length];
|
||
+
|
||
+ return [self lineForAXIndex:point_idx];
|
||
+}
|
||
+
|
||
+- (NSRange)accessibilityRangeForLine:(NSInteger)line
|
||
+{
|
||
+ if (![NSThread isMainThread])
|
||
+ {
|
||
+ __block NSRange result;
|
||
+ dispatch_sync (dispatch_get_main_queue (), ^{
|
||
+ result = [self accessibilityRangeForLine:line];
|
||
+ });
|
||
+ return result;
|
||
+ }
|
||
+ [self ensureTextCache];
|
||
+ if (!cachedText || line < 0)
|
||
+ return NSMakeRange (NSNotFound, 0);
|
||
+
|
||
+ NSUInteger len = [cachedText length];
|
||
+ if (len == 0)
|
||
+ return (line == 0) ? NSMakeRange (0, 0)
|
||
+ : NSMakeRange (NSNotFound, 0);
|
||
+
|
||
+ return [self rangeForLine:(NSUInteger)line textLength:len];
|
||
+}
|
||
+
|
||
+- (NSInteger)accessibilityLineForIndex:(NSInteger)index
|
||
+{
|
||
+ if (![NSThread isMainThread])
|
||
+ {
|
||
+ __block NSInteger result;
|
||
+ dispatch_sync (dispatch_get_main_queue (), ^{
|
||
+ result = [self accessibilityLineForIndex:index];
|
||
+ });
|
||
+ return result;
|
||
+ }
|
||
+ [self ensureTextCache];
|
||
+ if (!cachedText || index < 0)
|
||
+ return 0;
|
||
+
|
||
+ NSUInteger idx = (NSUInteger) index;
|
||
+ if (idx > [cachedText length])
|
||
+ idx = [cachedText length];
|
||
+
|
||
+ return [self lineForAXIndex:idx];
|
||
+}
|
||
+
|
||
+- (NSRange)accessibilityRangeForIndex:(NSInteger)index
|
||
+{
|
||
+ if (![NSThread isMainThread])
|
||
+ {
|
||
+ __block NSRange result;
|
||
+ dispatch_sync (dispatch_get_main_queue (), ^{
|
||
+ result = [self accessibilityRangeForIndex:index];
|
||
+ });
|
||
+ return result;
|
||
+ }
|
||
+ [self ensureTextCache];
|
||
+ if (!cachedText || index < 0
|
||
+ || (NSUInteger) index >= [cachedText length])
|
||
+ return NSMakeRange (NSNotFound, 0);
|
||
+ return [cachedText
|
||
+ rangeOfComposedCharacterSequenceAtIndex:(NSUInteger)index];
|
||
+}
|
||
+
|
||
+- (NSRange)accessibilityStyleRangeForIndex:(NSInteger)index
|
||
+{
|
||
+ /* 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];
|
||
+ return [self accessibilityRangeForLine:line];
|
||
+}
|
||
+
|
||
+- (NSRect)accessibilityFrameForRange:(NSRange)range
|
||
+{
|
||
+ if (![NSThread isMainThread])
|
||
+ {
|
||
+ __block NSRect result;
|
||
+ dispatch_sync (dispatch_get_main_queue (), ^{
|
||
+ result = [self accessibilityFrameForRange:range];
|
||
+ });
|
||
+ return result;
|
||
+ }
|
||
+ struct window *w = [self validWindow];
|
||
+ EmacsView *view = self.emacsView;
|
||
+ if (!w || !view)
|
||
+ return NSZeroRect;
|
||
+ /* Convert ax-index range to charpos range for glyph lookup. */
|
||
+ [self ensureTextCache];
|
||
+ ptrdiff_t cp_start = [self charposForAccessibilityIndex:range.location];
|
||
+ ptrdiff_t cp_end = [self charposForAccessibilityIndex:
|
||
+ range.location + range.length];
|
||
+ return ns_ax_frame_for_range (w, view, cp_start,
|
||
+ cp_end - cp_start);
|
||
+}
|
||
+
|
||
+- (NSRange)accessibilityRangeForPosition:(NSPoint)screenPoint
|
||
+{
|
||
+ if (![NSThread isMainThread])
|
||
+ {
|
||
+ __block NSRange result;
|
||
+ dispatch_sync (dispatch_get_main_queue (), ^{
|
||
+ result
|
||
+ = [self accessibilityRangeForPosition:screenPoint];
|
||
+ });
|
||
+ return result;
|
||
+ }
|
||
+ /* Hit test: convert screen point to buffer character index. */
|
||
+ struct window *w = [self validWindow];
|
||
+ EmacsView *view = self.emacsView;
|
||
+ if (!w || !view || !w->current_matrix)
|
||
+ return NSMakeRange (0, 0);
|
||
+
|
||
+ /* Convert screen point to EmacsView coordinates. */
|
||
+ NSPoint windowPoint = [[view window] convertPointFromScreen:screenPoint];
|
||
+ NSPoint viewPoint = [view convertPoint:windowPoint fromView:nil];
|
||
+
|
||
+ /* Convert to window-relative pixel coordinates. */
|
||
+ int x = (int) viewPoint.x - w->pixel_left;
|
||
+ int y = (int) viewPoint.y - w->pixel_top;
|
||
+
|
||
+ if (x < 0 || y < 0 || x >= w->pixel_width || y >= w->pixel_height)
|
||
+ return NSMakeRange (0, 0);
|
||
+
|
||
+ /* Block input to prevent concurrent redisplay from modifying the
|
||
+ glyph matrix while we traverse it. Use specpdl unwind protection
|
||
+ so block_input is always matched by unblock_input, even if
|
||
+ ensureTextCache triggers a Lisp signal (longjmp). */
|
||
+ block_input ();
|
||
+ specpdl_ref count = SPECPDL_INDEX ();
|
||
+ record_unwind_protect_void (unblock_input);
|
||
+
|
||
+ /* Find the glyph row at this y coordinate. */
|
||
+ struct glyph_matrix *matrix = w->current_matrix;
|
||
+ struct glyph_row *hit_row = NULL;
|
||
+
|
||
+ for (int i = 0; i < matrix->nrows; i++)
|
||
+ {
|
||
+ struct glyph_row *row = matrix->rows + i;
|
||
+ if (!row->enabled_p || !row->displays_text_p || row->mode_line_p)
|
||
+ continue;
|
||
+ int row_top = WINDOW_TO_FRAME_PIXEL_Y (w, MAX (0, row->y));
|
||
+ if ((int) viewPoint.y >= row_top
|
||
+ && (int) viewPoint.y < row_top + row->visible_height)
|
||
+ {
|
||
+ hit_row = row;
|
||
+ break;
|
||
+ }
|
||
+ }
|
||
+
|
||
+ if (!hit_row)
|
||
+ {
|
||
+ unbind_to (count, Qnil);
|
||
+ return NSMakeRange (0, 0);
|
||
+ }
|
||
+
|
||
+ /* Find the glyph at this x coordinate within the row. */
|
||
+ struct glyph *glyph = hit_row->glyphs[TEXT_AREA];
|
||
+ struct glyph *end = glyph + hit_row->used[TEXT_AREA];
|
||
+ int glyph_x = 0;
|
||
+ ptrdiff_t best_charpos = MATRIX_ROW_START_CHARPOS (hit_row);
|
||
+
|
||
+ for (; glyph < end; glyph++)
|
||
+ {
|
||
+ if (glyph->type == CHAR_GLYPH && glyph->charpos > 0)
|
||
+ {
|
||
+ if (x >= glyph_x && x < glyph_x + glyph->pixel_width)
|
||
+ {
|
||
+ best_charpos = glyph->charpos;
|
||
+ break;
|
||
+ }
|
||
+ best_charpos = glyph->charpos;
|
||
+ }
|
||
+ glyph_x += glyph->pixel_width;
|
||
+ }
|
||
+
|
||
+ /* Convert buffer charpos to accessibility index via mapping. */
|
||
+ [self ensureTextCache];
|
||
+ NSUInteger ax_idx = [self accessibilityIndexForCharpos:best_charpos];
|
||
+ if (cachedText && ax_idx > [cachedText length])
|
||
+ ax_idx = [cachedText length];
|
||
+
|
||
+ unbind_to (count, Qnil);
|
||
+ return NSMakeRange (ax_idx, 1);
|
||
+}
|
||
+
|
||
+- (NSRange)accessibilityVisibleCharacterRange
|
||
+{
|
||
+ if (![NSThread isMainThread])
|
||
+ {
|
||
+ __block NSRange result;
|
||
+ dispatch_sync (dispatch_get_main_queue (), ^{
|
||
+ result = [self accessibilityVisibleCharacterRange];
|
||
+ });
|
||
+ return result;
|
||
+ }
|
||
+ /* Return the full cached text range. VoiceOver interprets the
|
||
+ visible range boundary as end-of-text, so we must expose the
|
||
+ entire buffer to avoid premature "end of text" announcements. */
|
||
+ [self ensureTextCache];
|
||
+ return NSMakeRange (0, cachedText ? [cachedText length] : 0);
|
||
+}
|
||
+
|
||
+- (NSRect)accessibilityFrame
|
||
+{
|
||
+ if (![NSThread isMainThread])
|
||
+ {
|
||
+ __block NSRect result;
|
||
+ dispatch_sync (dispatch_get_main_queue (), ^{
|
||
+ result = [self accessibilityFrame];
|
||
+ });
|
||
+ return result;
|
||
+ }
|
||
+ struct window *w = [self validWindow];
|
||
+ if (!w)
|
||
+ return NSZeroRect;
|
||
+
|
||
+ /* Subtract mode line height so the buffer element does not overlap it. */
|
||
+ int text_h = w->pixel_height;
|
||
+ if (w->current_matrix)
|
||
+ {
|
||
+ for (int i = w->current_matrix->nrows - 1; i >= 0; i--)
|
||
+ {
|
||
+ struct glyph_row *row = w->current_matrix->rows + i;
|
||
+ if (row->enabled_p && row->mode_line_p)
|
||
+ {
|
||
+ text_h -= row->visible_height;
|
||
+ break;
|
||
+ }
|
||
+ }
|
||
+ }
|
||
+ return [self screenRectFromEmacsX:w->pixel_left
|
||
+ y:w->pixel_top
|
||
+ width:w->pixel_width
|
||
+ height:text_h];
|
||
+}
|
||
+
|
||
+/* ---- Notification dispatch (helper methods) ---- */
|
||
+
|
||
+/* Post NSAccessibilityValueChangedNotification for a text edit.
|
||
+ Called when BUF_MODIFF changes between redisplay cycles. */
|
||
+
|
||
+@end
|
||
+
|
||
#endif /* NS_IMPL_COCOA */
|
||
|
||
|
||
@@ -8918,13 +10049,13 @@ - (NSSize)windowWillResize: (NSWindow *)sender toSize: (NSSize)frameSize
|
||
if (old_title == 0)
|
||
{
|
||
char *t = strdup ([[[self window] title] UTF8String]);
|
||
- char *pos = strstr (t, " — ");
|
||
+ char *pos = strstr (t, " --- ");
|
||
if (pos)
|
||
*pos = '\0';
|
||
old_title = t;
|
||
}
|
||
size_title = xmalloc (strlen (old_title) + 40);
|
||
- esprintf (size_title, "%s — (%d × %d)", old_title, cols, rows);
|
||
+ esprintf (size_title, "%s --- (%d × %d)", old_title, cols, rows);
|
||
[window setTitle: [NSString stringWithUTF8String: size_title]];
|
||
[window display];
|
||
xfree (size_title);
|
||
--
|
||
2.43.0
|
||
|