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:
646
patches/0003-ns-add-AX-notifications-and-mode-line-element.patch
Normal file
646
patches/0003-ns-add-AX-notifications-and-mode-line-element.patch
Normal file
@@ -0,0 +1,646 @@
|
||||
From 9898a3a6aa07871824685a63926d151da8c115f7 Mon Sep 17 00:00:00 2001
|
||||
From: Martin Sukany <martin@sukany.cz>
|
||||
Date: Wed, 4 Mar 2026 15:23:55 +0100
|
||||
Subject: [PATCH 4/9] ns: add AX notifications 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 e4b3fb17a0..84a32a05cb 100644
|
||||
--- a/src/nsterm.m
|
||||
+++ b/src/nsterm.m
|
||||
@@ -8779,6 +8779,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
|
||||
|
||||
Reference in New Issue
Block a user