When AXSelectedTextChanged is posted from the parent EmacsView (NSView)
with UIElementsKey pointing to the EmacsAXBuffer element, VoiceOver calls
accessibilityLineForIndex: on the VIEW rather than on the focused element.
In specialised buffers (org-agenda, org-super-agenda) where line geometry
differs from plain text, the view returns an incorrect range and VoiceOver
reads only the first word at the cursor (e.g. 'La' or 'Liga') instead of
the full line.
Plain text buffers were unaffected because the fallback geometry happened
to be correct for simple line layouts.
Fix: post AXSelectedTextChanged on self (the EmacsAXBuffer element)
instead of on self.emacsView. This causes VoiceOver to call
accessibilityLineForIndex: on the element that owns the selection, which
returns the correct line range in all buffer types. Remove UIElementsKey
(unnecessary when posting from the element itself).
This aligns with the pre-review code (51f5944) which always posted
AX notifications directly on the focused element.
648 lines
20 KiB
Diff
648 lines
20 KiB
Diff
From d3bd4a705068b2538cda29217eb6ea67c525f9d5 Mon Sep 17 00:00:00 2001
|
|
From: Martin Sukany <martin@sukany.cz>
|
|
Date: Sat, 28 Feb 2026 12:58:11 +0100
|
|
Subject: [PATCH 3/8] ns: add buffer notification dispatch and mode-line
|
|
element
|
|
|
|
Add VoiceOver notification dispatch and mode-line readout.
|
|
|
|
* src/nsterm.m (EmacsAccessibilityBuffer(Notifications)): New category.
|
|
(postTextChangedNotification:): Post NSAccessibilityValueChangedNotification
|
|
with AXTextEditType/AXTextChangeValue details.
|
|
(postFocusedCursorNotification:direction:granularity:markActive:
|
|
oldMarkActive:): Post NSAccessibilitySelectedTextChangedNotification
|
|
following the WebKit hybrid pattern; announce character at point for
|
|
character moves.
|
|
(postCompletionAnnouncementForBuffer:point:): Announce completion
|
|
candidates in non-focused (completion) buffers. Lisp/buffer
|
|
access is performed inside block_input; ObjC AX calls are made after
|
|
unblock_input to avoid holding block_input during @synchronized.
|
|
(postAccessibilityNotificationsForFrame:): Main dispatch entry point;
|
|
detects text edit, cursor/mark change, or overlay change.
|
|
(EmacsAccessibilityModeLine): Implement AXStaticText element for the
|
|
mode line.
|
|
---
|
|
src/nsterm.m | 606 +++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
1 file changed, 606 insertions(+)
|
|
|
|
diff --git a/src/nsterm.m b/src/nsterm.m
|
|
index 6256dbc22e..9e0e317237 100644
|
|
--- a/src/nsterm.m
|
|
+++ b/src/nsterm.m
|
|
@@ -8740,6 +8740,612 @@ - (NSRect)accessibilityFrame
|
|
|
|
@end
|
|
|
|
+
|
|
+
|
|
+/* ===================================================================
|
|
+ EmacsAccessibilityBuffer (Notifications) — AX event dispatch
|
|
+
|
|
+ These methods notify VoiceOver of text and selection changes.
|
|
+ Called from the redisplay cycle (postAccessibilityUpdates).
|
|
+ =================================================================== */
|
|
+
|
|
+@implementation EmacsAccessibilityBuffer (Notifications)
|
|
+
|
|
+- (void)postTextChangedNotification:(ptrdiff_t)point
|
|
+{
|
|
+ /* Capture changed char before invalidating cache. */
|
|
+ NSString *changedChar = @"";
|
|
+ if (point > self.cachedPoint
|
|
+ && point - self.cachedPoint == 1)
|
|
+ {
|
|
+ /* Single char inserted — refresh cache and grab it. */
|
|
+ [self invalidateTextCache];
|
|
+ [self ensureTextCache];
|
|
+ if (cachedText)
|
|
+ {
|
|
+ NSUInteger idx = [self accessibilityIndexForCharpos:point - 1];
|
|
+ if (idx < [cachedText length])
|
|
+ changedChar = [cachedText substringWithRange:
|
|
+ NSMakeRange (idx, 1)];
|
|
+ }
|
|
+ }
|
|
+ else
|
|
+ {
|
|
+ [self invalidateTextCache];
|
|
+ }
|
|
+
|
|
+ /* Update cachedPoint here so the selection-move branch does NOT
|
|
+ fire for point changes caused by edits. WebKit and Chromium
|
|
+ never send both ValueChanged and SelectedTextChanged for the
|
|
+ same user action — they are mutually exclusive. */
|
|
+ self.cachedPoint = point;
|
|
+
|
|
+ NSDictionary *change = @{
|
|
+ @"AXTextEditType": @(ns_ax_text_edit_type_typing),
|
|
+ @"AXTextChangeValue": changedChar,
|
|
+ @"AXTextChangeValueLength": @([changedChar length])
|
|
+ };
|
|
+ NSDictionary *userInfo = @{
|
|
+ @"AXTextStateChangeType": @(ns_ax_text_state_change_edit),
|
|
+ @"AXTextChangeValues": @[change],
|
|
+ @"AXTextChangeElement": self
|
|
+ };
|
|
+ ns_ax_post_notification_with_info (
|
|
+ self, NSAccessibilityValueChangedNotification, userInfo);
|
|
+}
|
|
+
|
|
+/* Post SelectedTextChanged and AnnouncementRequested for the
|
|
+ focused buffer element when point or mark changes. */
|
|
+- (void)postFocusedCursorNotification:(ptrdiff_t)point
|
|
+ direction:(NSInteger)direction
|
|
+ granularity:(NSInteger)granularity
|
|
+ markActive:(BOOL)markActive
|
|
+ oldMarkActive:(BOOL)oldMarkActive
|
|
+{
|
|
+ BOOL isCharMove
|
|
+ = (!markActive && !oldMarkActive
|
|
+ && granularity
|
|
+ == ns_ax_text_selection_granularity_character);
|
|
+
|
|
+ /* Always post SelectedTextChanged to interrupt VoiceOver reading
|
|
+ and update cursor tracking / braille displays. */
|
|
+ NSMutableDictionary *moveInfo = [NSMutableDictionary dictionary];
|
|
+ moveInfo[@"AXTextStateChangeType"]
|
|
+ = @(ns_ax_text_state_change_selection_move);
|
|
+ moveInfo[@"AXTextSelectionDirection"] = @(direction);
|
|
+ moveInfo[@"AXTextChangeElement"] = self;
|
|
+ /* Omit granularity for character moves so VoiceOver does not
|
|
+ derive its own speech (it would read the wrong character
|
|
+ for block-cursor mode). Include it for word/line/
|
|
+ selection so VoiceOver reads the appropriate text. */
|
|
+ if (!isCharMove)
|
|
+ moveInfo[@"AXTextSelectionGranularity"] = @(granularity);
|
|
+
|
|
+ ns_ax_post_notification_with_info (
|
|
+ self,
|
|
+ NSAccessibilitySelectedTextChangedNotification,
|
|
+ moveInfo);
|
|
+
|
|
+ /* For character moves: explicit announcement of char AT point.
|
|
+ This is the ONLY speech source for character navigation.
|
|
+ Correct for block-cursor (cursor ON the character)
|
|
+ and harmless for insert-mode. */
|
|
+ if (isCharMove && cachedText)
|
|
+ {
|
|
+ NSUInteger point_idx
|
|
+ = [self accessibilityIndexForCharpos:point];
|
|
+ NSUInteger tlen = [cachedText length];
|
|
+ if (point_idx < tlen)
|
|
+ {
|
|
+ NSRange charRange = [cachedText
|
|
+ rangeOfComposedCharacterSequenceAtIndex: point_idx];
|
|
+ if (charRange.location != NSNotFound
|
|
+ && charRange.length > 0
|
|
+ && NSMaxRange (charRange) <= tlen)
|
|
+ {
|
|
+ NSString *ch
|
|
+ = [cachedText substringWithRange: charRange];
|
|
+ if (![ch isEqualToString: @"\n"])
|
|
+ {
|
|
+ NSDictionary *annInfo = @{
|
|
+ NSAccessibilityAnnouncementKey: ch,
|
|
+ NSAccessibilityPriorityKey:
|
|
+ @(NSAccessibilityPriorityHigh)
|
|
+ };
|
|
+ ns_ax_post_notification_with_info (
|
|
+ NSApp,
|
|
+ NSAccessibilityAnnouncementRequestedNotification,
|
|
+ annInfo);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /* For word moves: explicit announcement of word AT new point.
|
|
+ VO auto-speech from SelectedTextChanged with direction=next
|
|
+ and granularity=word reads the word that was traversed (the
|
|
+ source word), not the word arrived at. This explicit
|
|
+ announcement reads the destination word instead, matching
|
|
+ user expectation ("w" jumps to next word and reads it). */
|
|
+ BOOL isWordMove
|
|
+ = (!markActive && !oldMarkActive
|
|
+ && granularity == ns_ax_text_selection_granularity_word
|
|
+ && direction == ns_ax_text_selection_direction_discontiguous);
|
|
+ if (isWordMove && cachedText)
|
|
+ {
|
|
+ NSCharacterSet *ws
|
|
+ = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
|
+ NSUInteger point_idx
|
|
+ = [self accessibilityIndexForCharpos:point];
|
|
+ NSUInteger tlen = [cachedText length];
|
|
+ if (point_idx < tlen
|
|
+ && ![ws characterIsMember:
|
|
+ [cachedText characterAtIndex:point_idx]])
|
|
+ {
|
|
+ /* Find word boundaries around point. */
|
|
+ NSUInteger wstart = point_idx;
|
|
+ while (wstart > 0
|
|
+ && ![ws characterIsMember:
|
|
+ [cachedText characterAtIndex:wstart - 1]])
|
|
+ wstart--;
|
|
+ NSUInteger wend = point_idx;
|
|
+ while (wend < tlen
|
|
+ && ![ws characterIsMember:
|
|
+ [cachedText characterAtIndex:wend]])
|
|
+ wend++;
|
|
+ if (wend > wstart)
|
|
+ {
|
|
+ NSString *word
|
|
+ = [cachedText substringWithRange:
|
|
+ NSMakeRange (wstart, wend - wstart)];
|
|
+ NSMutableCharacterSet *trims
|
|
+ = [ws mutableCopy];
|
|
+ [trims formUnionWithCharacterSet:
|
|
+ [NSCharacterSet punctuationCharacterSet]];
|
|
+ word = [word stringByTrimmingCharactersInSet:trims];
|
|
+ [trims release];
|
|
+ if ([word length] > 0)
|
|
+ {
|
|
+ NSDictionary *annInfo = @{
|
|
+ NSAccessibilityAnnouncementKey: word,
|
|
+ NSAccessibilityPriorityKey:
|
|
+ @(NSAccessibilityPriorityHigh)
|
|
+ };
|
|
+ ns_ax_post_notification_with_info (
|
|
+ NSApp,
|
|
+ NSAccessibilityAnnouncementRequestedNotification,
|
|
+ annInfo);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /* For focused line moves: always announce line text explicitly.
|
|
+ SelectedTextChanged with granularity=line works for arrow keys,
|
|
+ but C-n/C-p need the explicit announcement (VoiceOver processes
|
|
+ these keystrokes differently from arrows).
|
|
+ In completion-list-mode, read the completion candidate instead
|
|
+ of the whole line. */
|
|
+ if (cachedText
|
|
+ && granularity == ns_ax_text_selection_granularity_line)
|
|
+ {
|
|
+ NSString *announceText = nil;
|
|
+
|
|
+ /* 1. completion--string at point. */
|
|
+ Lisp_Object cstr
|
|
+ = Fget_char_property (make_fixnum (point),
|
|
+ Qns_ax_completion__string, Qnil);
|
|
+ announceText = ns_ax_completion_string_from_prop (cstr);
|
|
+
|
|
+ /* 2. Fallback: full line text. */
|
|
+ if (!announceText)
|
|
+ {
|
|
+ NSUInteger point_idx
|
|
+ = [self accessibilityIndexForCharpos:point];
|
|
+ if (point_idx <= [cachedText length])
|
|
+ {
|
|
+ NSInteger lineNum
|
|
+ = [self accessibilityLineForIndex:point_idx];
|
|
+ NSRange lineRange
|
|
+ = [self accessibilityRangeForLine:lineNum];
|
|
+ if (lineRange.location != NSNotFound
|
|
+ && lineRange.length > 0
|
|
+ && NSMaxRange (lineRange) <= [cachedText length])
|
|
+ announceText
|
|
+ = [cachedText substringWithRange:lineRange];
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (announceText)
|
|
+ {
|
|
+ announceText = [announceText
|
|
+ stringByTrimmingCharactersInSet:
|
|
+ [NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
+ if ([announceText length] > 0)
|
|
+ {
|
|
+ NSDictionary *annInfo = @{
|
|
+ NSAccessibilityAnnouncementKey: announceText,
|
|
+ NSAccessibilityPriorityKey:
|
|
+ @(NSAccessibilityPriorityHigh)
|
|
+ };
|
|
+ ns_ax_post_notification_with_info (
|
|
+ NSApp,
|
|
+ NSAccessibilityAnnouncementRequestedNotification,
|
|
+ annInfo);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
+/* Post AnnouncementRequested for non-focused buffers (typically
|
|
+ *Completions* while minibuffer has keyboard focus).
|
|
+ VoiceOver does not automatically read changes in non-focused
|
|
+ elements, so we announce the selected completion explicitly. */
|
|
+- (void)postCompletionAnnouncementForBuffer:(struct buffer *)b
|
|
+ point:(ptrdiff_t)point
|
|
+{
|
|
+ NSString *announceText = nil;
|
|
+ ptrdiff_t currentOverlayStart = 0;
|
|
+ ptrdiff_t currentOverlayEnd = 0;
|
|
+
|
|
+ block_input ();
|
|
+ specpdl_ref count2 = SPECPDL_INDEX ();
|
|
+ record_unwind_protect_void (unblock_input);
|
|
+ record_unwind_current_buffer ();
|
|
+ if (b != current_buffer)
|
|
+ set_buffer_internal_1 (b);
|
|
+
|
|
+ /* 1) Prefer explicit completion candidate property. */
|
|
+ Lisp_Object cstr = Fget_char_property (make_fixnum (point),
|
|
+ Qns_ax_completion__string,
|
|
+ Qnil);
|
|
+ announceText = ns_ax_completion_string_from_prop (cstr);
|
|
+
|
|
+ /* 2) Fallback: mouse-face span at point. */
|
|
+ if (!announceText)
|
|
+ {
|
|
+ Lisp_Object mf = Fget_char_property (make_fixnum (point),
|
|
+ Qmouse_face, Qnil);
|
|
+ if (!NILP (mf))
|
|
+ {
|
|
+ ptrdiff_t begv2 = BUF_BEGV (b);
|
|
+ ptrdiff_t zv2 = BUF_ZV (b);
|
|
+
|
|
+ Lisp_Object prev_change
|
|
+ = Fprevious_single_char_property_change (
|
|
+ make_fixnum (point + 1), Qmouse_face,
|
|
+ Qnil, make_fixnum (begv2));
|
|
+ ptrdiff_t s2
|
|
+ = FIXNUMP (prev_change) ? XFIXNUM (prev_change)
|
|
+ : begv2;
|
|
+
|
|
+ Lisp_Object next_change
|
|
+ = Fnext_single_char_property_change (
|
|
+ make_fixnum (point), Qmouse_face,
|
|
+ Qnil, make_fixnum (zv2));
|
|
+ ptrdiff_t e2
|
|
+ = FIXNUMP (next_change) ? XFIXNUM (next_change)
|
|
+ : zv2;
|
|
+
|
|
+ if (e2 > s2)
|
|
+ {
|
|
+ NSUInteger ax_s = [self accessibilityIndexForCharpos:s2];
|
|
+ NSUInteger ax_e = [self accessibilityIndexForCharpos:e2];
|
|
+ if (ax_e > ax_s && ax_e <= [cachedText length])
|
|
+ announceText = [cachedText substringWithRange:
|
|
+ NSMakeRange (ax_s, ax_e - ax_s)];
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /* 3) Fallback: completions-highlight overlay at point. */
|
|
+ if (!announceText)
|
|
+ {
|
|
+ Lisp_Object faceSym = Qns_ax_completions_highlight;
|
|
+ Lisp_Object overlays = Foverlays_at (make_fixnum (point), 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))))
|
|
+ {
|
|
+ ptrdiff_t ov_start = OVERLAY_START (ov);
|
|
+ ptrdiff_t ov_end = OVERLAY_END (ov);
|
|
+ if (ov_end > ov_start)
|
|
+ {
|
|
+ announceText = ns_ax_completion_text_for_span (self, b,
|
|
+ ov_start,
|
|
+ ov_end,
|
|
+ cachedText);
|
|
+ currentOverlayStart = ov_start;
|
|
+ currentOverlayEnd = ov_end;
|
|
+ }
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /* 4) Fallback: nearest completions-highlight overlay. */
|
|
+ if (!announceText)
|
|
+ {
|
|
+ ptrdiff_t ov_start = 0;
|
|
+ ptrdiff_t ov_end = 0;
|
|
+ if (ns_ax_find_completion_overlay_range (b, point,
|
|
+ &ov_start, &ov_end))
|
|
+ {
|
|
+ announceText = ns_ax_completion_text_for_span (self, b,
|
|
+ ov_start, ov_end,
|
|
+ cachedText);
|
|
+ currentOverlayStart = ov_start;
|
|
+ currentOverlayEnd = ov_end;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ unbind_to (count2, Qnil);
|
|
+
|
|
+ /* Final fallback: read current line at point. */
|
|
+ if (!announceText)
|
|
+ {
|
|
+ NSUInteger point_idx = [self accessibilityIndexForCharpos:point];
|
|
+ if (point_idx <= [cachedText length])
|
|
+ {
|
|
+ NSInteger lineNum = [self accessibilityLineForIndex:
|
|
+ point_idx];
|
|
+ NSRange lineRange = [self accessibilityRangeForLine:lineNum];
|
|
+ if (lineRange.location != NSNotFound
|
|
+ && lineRange.length > 0
|
|
+ && lineRange.location + lineRange.length
|
|
+ <= [cachedText length])
|
|
+ announceText = [cachedText substringWithRange:lineRange];
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /* Deduplicate: post only when text, overlay, or point changed. */
|
|
+ if (announceText)
|
|
+ {
|
|
+ announceText = [announceText stringByTrimmingCharactersInSet:
|
|
+ [NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
+ if ([announceText length] > 0)
|
|
+ {
|
|
+ BOOL textChanged = ![announceText isEqualToString:
|
|
+ self.cachedCompletionAnnouncement];
|
|
+ BOOL overlayChanged =
|
|
+ (currentOverlayStart != self.cachedCompletionOverlayStart
|
|
+ || currentOverlayEnd != self.cachedCompletionOverlayEnd);
|
|
+ BOOL pointChanged = (point != self.cachedCompletionPoint);
|
|
+ if (textChanged || overlayChanged || pointChanged)
|
|
+ {
|
|
+ NSDictionary *annInfo = @{
|
|
+ NSAccessibilityAnnouncementKey: announceText,
|
|
+ NSAccessibilityPriorityKey:
|
|
+ @(NSAccessibilityPriorityHigh)
|
|
+ };
|
|
+ ns_ax_post_notification_with_info (
|
|
+ NSApp,
|
|
+ NSAccessibilityAnnouncementRequestedNotification,
|
|
+ annInfo);
|
|
+ }
|
|
+ self.cachedCompletionAnnouncement = announceText;
|
|
+ self.cachedCompletionOverlayStart = currentOverlayStart;
|
|
+ self.cachedCompletionOverlayEnd = currentOverlayEnd;
|
|
+ self.cachedCompletionPoint = point;
|
|
+ }
|
|
+ else
|
|
+ {
|
|
+ self.cachedCompletionAnnouncement = nil;
|
|
+ self.cachedCompletionOverlayStart = 0;
|
|
+ self.cachedCompletionOverlayEnd = 0;
|
|
+ self.cachedCompletionPoint = 0;
|
|
+ }
|
|
+ }
|
|
+ else
|
|
+ {
|
|
+ self.cachedCompletionAnnouncement = nil;
|
|
+ self.cachedCompletionOverlayStart = 0;
|
|
+ self.cachedCompletionOverlayEnd = 0;
|
|
+ self.cachedCompletionPoint = 0;
|
|
+ }
|
|
+}
|
|
+
|
|
+/* ---- Notification dispatch (main entry point) ---- */
|
|
+
|
|
+/* Dispatch accessibility notifications after a redisplay cycle.
|
|
+ Detects three mutually exclusive events: text edit, cursor/mark
|
|
+ change, or no change. Delegates to helper methods above. */
|
|
+- (void)postAccessibilityNotificationsForFrame:(struct frame *)f
|
|
+{
|
|
+ NSTRACE ("[EmacsView postAccessibilityNotificationsForFrame:]");
|
|
+ struct window *w = [self validWindow];
|
|
+ if (!w || !WINDOW_LEAF_P (w))
|
|
+ return;
|
|
+
|
|
+ struct buffer *b = XBUFFER (w->contents);
|
|
+ if (!b)
|
|
+ return;
|
|
+
|
|
+ ptrdiff_t modiff = BUF_MODIFF (b);
|
|
+ ptrdiff_t point = BUF_PT (b);
|
|
+ BOOL markActive = !NILP (BVAR (b, mark_active));
|
|
+
|
|
+ /* --- Text changed (edit) --- */
|
|
+ if (modiff != self.cachedModiff)
|
|
+ {
|
|
+ self.cachedModiff = modiff;
|
|
+ [self postTextChangedNotification:point];
|
|
+ }
|
|
+
|
|
+ /* --- Cursor moved or selection changed ---
|
|
+ Use 'else if' — edits and selection moves are mutually exclusive
|
|
+ per the WebKit/Chromium pattern. */
|
|
+ else if (point != self.cachedPoint || markActive != self.cachedMarkActive)
|
|
+ {
|
|
+ ptrdiff_t oldPoint = self.cachedPoint;
|
|
+ BOOL oldMarkActive = self.cachedMarkActive;
|
|
+ self.cachedPoint = point;
|
|
+ self.cachedMarkActive = markActive;
|
|
+
|
|
+ /* Compute direction. */
|
|
+ NSInteger direction = ns_ax_text_selection_direction_discontiguous;
|
|
+ if (point > oldPoint)
|
|
+ direction = ns_ax_text_selection_direction_next;
|
|
+ else if (point < oldPoint)
|
|
+ direction = ns_ax_text_selection_direction_previous;
|
|
+
|
|
+ int ctrlNP = 0;
|
|
+ bool isCtrlNP = ns_ax_event_is_line_nav_key (&ctrlNP);
|
|
+
|
|
+ /* --- Granularity detection --- */
|
|
+ NSInteger granularity = ns_ax_text_selection_granularity_unknown;
|
|
+ [self ensureTextCache];
|
|
+ if (cachedText && oldPoint > 0)
|
|
+ {
|
|
+ NSUInteger tlen = [cachedText length];
|
|
+ NSUInteger oldIdx = [self accessibilityIndexForCharpos:oldPoint];
|
|
+ NSUInteger newIdx = [self accessibilityIndexForCharpos:point];
|
|
+ if (oldIdx > tlen) oldIdx = tlen;
|
|
+ if (newIdx > tlen) newIdx = tlen;
|
|
+
|
|
+ NSRange oldLine = [cachedText lineRangeForRange:
|
|
+ NSMakeRange (oldIdx, 0)];
|
|
+ NSRange newLine = [cachedText lineRangeForRange:
|
|
+ NSMakeRange (newIdx, 0)];
|
|
+ if (oldLine.location != newLine.location)
|
|
+ granularity = ns_ax_text_selection_granularity_line;
|
|
+ else
|
|
+ {
|
|
+ NSUInteger dist = (newIdx > oldIdx
|
|
+ ? newIdx - oldIdx
|
|
+ : oldIdx - newIdx);
|
|
+ if (dist > 1)
|
|
+ granularity = ns_ax_text_selection_granularity_word;
|
|
+ else if (dist == 1)
|
|
+ granularity = ns_ax_text_selection_granularity_character;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /* Force line semantics for explicit C-n/C-p / Tab / backtab. */
|
|
+ if (isCtrlNP)
|
|
+ {
|
|
+ direction = (ctrlNP > 0
|
|
+ ? ns_ax_text_selection_direction_next
|
|
+ : ns_ax_text_selection_direction_previous);
|
|
+ granularity = ns_ax_text_selection_granularity_line;
|
|
+ }
|
|
+
|
|
+ /* Post notifications for focused and non-focused elements. */
|
|
+ if ([self isAccessibilityFocused])
|
|
+ [self postFocusedCursorNotification:point
|
|
+ direction:direction
|
|
+ granularity:granularity
|
|
+ markActive:markActive
|
|
+ oldMarkActive:oldMarkActive];
|
|
+
|
|
+ if (![self isAccessibilityFocused] && cachedText)
|
|
+ [self postCompletionAnnouncementForBuffer:b point:point];
|
|
+ }
|
|
+ else
|
|
+ {
|
|
+ /* Nothing changed. Reset completion cache for focused buffer
|
|
+ to avoid stale announcements. */
|
|
+ if ([self isAccessibilityFocused])
|
|
+ {
|
|
+ self.cachedCompletionAnnouncement = nil;
|
|
+ self.cachedCompletionOverlayStart = 0;
|
|
+ self.cachedCompletionOverlayEnd = 0;
|
|
+ self.cachedCompletionPoint = 0;
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
+@end
|
|
+
|
|
+
|
|
+@implementation EmacsAccessibilityModeLine
|
|
+
|
|
+- (NSAccessibilityRole)accessibilityRole
|
|
+{
|
|
+ return NSAccessibilityStaticTextRole;
|
|
+}
|
|
+
|
|
+- (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))
|
|
+ {
|
|
+ struct buffer *b = XBUFFER (w->contents);
|
|
+ if (b)
|
|
+ {
|
|
+ Lisp_Object name = BVAR (b, name);
|
|
+ if (STRINGP (name))
|
|
+ {
|
|
+ NSString *bufName = [NSString stringWithLispString:name];
|
|
+ return [NSString stringWithFormat:@"Mode Line - %@", bufName];
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ return @"Mode Line";
|
|
+}
|
|
+
|
|
+- (id)accessibilityValue
|
|
+{
|
|
+ if (![NSThread isMainThread])
|
|
+ {
|
|
+ __block id result;
|
|
+ dispatch_sync (dispatch_get_main_queue (), ^{
|
|
+ result = [self accessibilityValue];
|
|
+ });
|
|
+ return result;
|
|
+ }
|
|
+ struct window *w = [self validWindow];
|
|
+ if (!w)
|
|
+ return @"";
|
|
+ return ns_ax_mode_line_text (w);
|
|
+}
|
|
+
|
|
+- (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 || !w->current_matrix)
|
|
+ return NSZeroRect;
|
|
+
|
|
+ /* Find the mode line row and return its screen rect. */
|
|
+ struct glyph_matrix *matrix = w->current_matrix;
|
|
+ for (int i = 0; i < matrix->nrows; i++)
|
|
+ {
|
|
+ struct glyph_row *row = matrix->rows + i;
|
|
+ if (row->enabled_p && row->mode_line_p)
|
|
+ {
|
|
+ return [self screenRectFromEmacsX:w->pixel_left
|
|
+ y:WINDOW_TO_FRAME_PIXEL_Y (w,
|
|
+ MAX (0, row->y))
|
|
+ width:w->pixel_width
|
|
+ height:row->visible_height];
|
|
+ }
|
|
+ }
|
|
+ return NSZeroRect;
|
|
+}
|
|
+
|
|
+@end
|
|
+
|
|
#endif /* NS_IMPL_COCOA */
|
|
|
|
|
|
--
|
|
2.43.0
|
|
|