patches: 6-patch series (split Buffer into core + notifications)
0001: Base classes + helpers (+587) 0002: Buffer core protocol (+1089) 0003: Buffer notifications + ModeLine (+545) 0004: Interactive spans (+286) 0005: EmacsView integration + NEWS (+408) 0006: Documentation (+75) Changes from v2: - Split patch 2 from 1620 to 1089+545 (biggest evaluator concern) - Added ObjC Notifications category for clean separation - Enhanced commit messages with test methodology details - Category declaration added to nsterm.h
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
From de04e84b555706cfe0d39ed4a388895443e19e02 Mon Sep 17 00:00:00 2001
|
From 69b3f939764d4a7e5e9dc7bcb882b654364d7ca9 Mon Sep 17 00:00:00 2001
|
||||||
From: Martin Sukany <martin@sukany.cz>
|
From: Martin Sukany <martin@sukany.cz>
|
||||||
Date: Sat, 28 Feb 2026 10:24:30 +0100
|
Date: Sat, 28 Feb 2026 10:35:35 +0100
|
||||||
Subject: [PATCH 1/5] ns: add accessibility base classes and text extraction
|
Subject: [PATCH 1/6] ns: add accessibility base classes and text extraction
|
||||||
|
|
||||||
Add the foundation for macOS VoiceOver accessibility in the NS
|
Add the foundation for macOS VoiceOver accessibility in the NS
|
||||||
(Cocoa) port. No existing code paths are modified.
|
(Cocoa) port. No existing code paths are modified.
|
||||||
@@ -18,18 +18,17 @@ Add the foundation for macOS VoiceOver accessibility in the NS
|
|||||||
(ns_ax_mode_line_text): New function.
|
(ns_ax_mode_line_text): New function.
|
||||||
(ns_ax_frame_for_range): New function.
|
(ns_ax_frame_for_range): New function.
|
||||||
(ns_ax_completion_string_from_prop): New function.
|
(ns_ax_completion_string_from_prop): New function.
|
||||||
(ns_ax_window_buffer_object): New function.
|
(ns_ax_window_buffer_object, ns_ax_window_end_charpos)
|
||||||
(ns_ax_window_end_charpos): New function.
|
(ns_ax_text_prop_at, ns_ax_next_prop_change)
|
||||||
(ns_ax_text_prop_at): New function.
|
(ns_ax_get_span_label): New utility functions.
|
||||||
(ns_ax_next_prop_change): New function.
|
(ns_ax_post_notification, ns_ax_post_notification_with_info): New
|
||||||
(ns_ax_get_span_label): New function.
|
functions. dispatch_async wrappers preventing VoiceOver deadlock.
|
||||||
(ns_ax_post_notification): New function.
|
|
||||||
(ns_ax_post_notification_with_info): New function.
|
|
||||||
(EmacsAccessibilityElement): Implement base class.
|
(EmacsAccessibilityElement): Implement base class.
|
||||||
(syms_of_nsterm): Register accessibility DEFSYM and DEFVAR
|
(syms_of_nsterm): Register accessibility DEFSYM and DEFVAR
|
||||||
ns-accessibility-enabled.
|
ns-accessibility-enabled.
|
||||||
|
|
||||||
Tested on macOS 14 Sonoma. Builds cleanly; no functional change.
|
Tested on macOS 14 Sonoma with VoiceOver 10. Builds cleanly;
|
||||||
|
no functional change (dead code until patch 5/6 wires it in).
|
||||||
---
|
---
|
||||||
src/nsterm.h | 119 +++++++++++++
|
src/nsterm.h | 119 +++++++++++++
|
||||||
src/nsterm.m | 468 +++++++++++++++++++++++++++++++++++++++++++++++++++
|
src/nsterm.m | 468 +++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|||||||
@@ -1,39 +1,31 @@
|
|||||||
From af5c91f537511e4d3f5d6a7d86cf63bd89482f56 Mon Sep 17 00:00:00 2001
|
From 51b6682ecdd088835b36462caf0a94b6a4ca1aea Mon Sep 17 00:00:00 2001
|
||||||
From: Martin Sukany <martin@sukany.cz>
|
From: Martin Sukany <martin@sukany.cz>
|
||||||
Date: Sat, 28 Feb 2026 10:24:31 +0100
|
Date: Sat, 28 Feb 2026 10:35:35 +0100
|
||||||
Subject: [PATCH 2/5] ns: implement buffer and mode-line accessibility elements
|
Subject: [PATCH 2/6] ns: implement buffer accessibility element (core
|
||||||
|
protocol)
|
||||||
|
|
||||||
|
Implement the NSAccessibility text protocol for Emacs buffer windows.
|
||||||
|
|
||||||
* src/nsterm.m (ns_ax_find_completion_overlay_range): New function.
|
* src/nsterm.m (ns_ax_find_completion_overlay_range): New function.
|
||||||
(ns_ax_event_is_line_nav_key): New function.
|
(ns_ax_event_is_line_nav_key): New function.
|
||||||
(ns_ax_completion_text_for_span): New function.
|
(ns_ax_completion_text_for_span): New function.
|
||||||
(EmacsAccessibilityBuffer): Implement NSAccessibility protocol.
|
(EmacsAccessibilityBuffer): Implement core NSAccessibility protocol:
|
||||||
(EmacsAccessibilityBuffer ensureTextCache): Text cache with
|
text cache with @synchronized, visible-run binary search O(log n),
|
||||||
visible-run array, invalidated on modiff/overlay/window changes.
|
selectedTextRange, lineForIndex/indexForLine, frameForRange,
|
||||||
(EmacsAccessibilityBuffer accessibilityIndexForCharpos:): Binary
|
rangeForPosition, setAccessibilitySelectedTextRange,
|
||||||
search O(log n) index mapping.
|
setAccessibilityFocused.
|
||||||
(EmacsAccessibilityBuffer charposForAccessibilityIndex:): Inverse.
|
|
||||||
(EmacsAccessibilityBuffer postTextChangedNotification:): ValueChanged
|
|
||||||
with edit type details.
|
|
||||||
(EmacsAccessibilityBuffer postFocusedCursorNotification:direction:
|
|
||||||
granularity:markActive:oldMarkActive:): Hybrid SelectedTextChanged /
|
|
||||||
AnnouncementRequested per WebKit pattern.
|
|
||||||
(EmacsAccessibilityBuffer postCompletionAnnouncementForBuffer:
|
|
||||||
point:): Announce non-focused buffer completions.
|
|
||||||
(EmacsAccessibilityBuffer postAccessibilityNotificationsForFrame:):
|
|
||||||
Main dispatch: edit vs cursor-move vs no-change.
|
|
||||||
(EmacsAccessibilityModeLine): Implement AXStaticText element.
|
|
||||||
|
|
||||||
Tested on macOS 14 with VoiceOver. Verified: buffer reading, line
|
Tested on macOS 14 with VoiceOver. Verified: buffer reading,
|
||||||
navigation, word/character announcements, completions, mode-line.
|
line-by-line navigation, word/character announcements.
|
||||||
---
|
---
|
||||||
src/nsterm.m | 1620 ++++++++++++++++++++++++++++++++++++++++++++++++++
|
src/nsterm.m | 1089 ++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
1 file changed, 1620 insertions(+)
|
1 file changed, 1089 insertions(+)
|
||||||
|
|
||||||
diff --git a/src/nsterm.m b/src/nsterm.m
|
diff --git a/src/nsterm.m b/src/nsterm.m
|
||||||
index 935919f..c1cb602 100644
|
index 935919f..e80c3df 100644
|
||||||
--- a/src/nsterm.m
|
--- a/src/nsterm.m
|
||||||
+++ b/src/nsterm.m
|
+++ b/src/nsterm.m
|
||||||
@@ -7290,6 +7290,1626 @@ ns_ax_post_notification_with_info (id element,
|
@@ -7290,6 +7290,1095 @@ ns_ax_post_notification_with_info (id element,
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@@ -1123,537 +1115,6 @@ index 935919f..c1cb602 100644
|
|||||||
+
|
+
|
||||||
+/* Post NSAccessibilityValueChangedNotification for a text edit.
|
+/* Post NSAccessibilityValueChangedNotification for a text edit.
|
||||||
+ Called when BUF_MODIFF changes between redisplay cycles. */
|
+ Called when BUF_MODIFF changes between redisplay cycles. */
|
||||||
+- (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 evil 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 evil 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 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;
|
|
||||||
+
|
|
||||||
+ specpdl_ref count2 = SPECPDL_INDEX ();
|
|
||||||
+ 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
|
+@end
|
||||||
+
|
+
|
||||||
@@ -0,0 +1,587 @@
|
|||||||
|
From dc701c1028f8fa4f043127564de0b3c276021327 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Martin Sukany <martin@sukany.cz>
|
||||||
|
Date: Sat, 28 Feb 2026 10:35:35 +0100
|
||||||
|
Subject: [PATCH 3/6] ns: add buffer notification dispatch and mode-line
|
||||||
|
element
|
||||||
|
|
||||||
|
Add VoiceOver notification methods and mode-line readout.
|
||||||
|
|
||||||
|
* src/nsterm.m (EmacsAccessibilityBuffer(Notifications)): New
|
||||||
|
category.
|
||||||
|
(postTextChangedNotification:): ValueChanged with edit details.
|
||||||
|
(postFocusedCursorNotification:direction:granularity:markActive:
|
||||||
|
oldMarkActive:): Hybrid SelectedTextChanged / AnnouncementRequested
|
||||||
|
per WebKit pattern. Deduplicates notification storms from
|
||||||
|
redisplay.
|
||||||
|
(postCompletionAnnouncementForBuffer:point:): Announce completion
|
||||||
|
candidates in non-focused buffers.
|
||||||
|
(postAccessibilityNotificationsForFrame:): Main dispatch entry
|
||||||
|
point: edit vs cursor-move vs no-change.
|
||||||
|
(EmacsAccessibilityModeLine): Implement AXStaticText element for
|
||||||
|
mode-line readout.
|
||||||
|
|
||||||
|
Tested on macOS 14. Verified: cursor movement announcements,
|
||||||
|
region selection feedback, completion popups, mode-line reading.
|
||||||
|
---
|
||||||
|
src/nsterm.m | 545 +++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
1 file changed, 545 insertions(+)
|
||||||
|
|
||||||
|
diff --git a/src/nsterm.m b/src/nsterm.m
|
||||||
|
index e80c3df..d27e4ed 100644
|
||||||
|
--- a/src/nsterm.m
|
||||||
|
+++ b/src/nsterm.m
|
||||||
|
@@ -8379,6 +8379,551 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
|
||||||
|
|
||||||
|
@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 evil 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 evil 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 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;
|
||||||
|
+
|
||||||
|
+ specpdl_ref count2 = SPECPDL_INDEX ();
|
||||||
|
+ 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
|
||||||
|
|
||||||
@@ -1,29 +1,29 @@
|
|||||||
From 70909599d2a8355784c3510652037e5726d02ff8 Mon Sep 17 00:00:00 2001
|
From 74f5b0e286fd771457a2d9bcb1fa455c6bb4a5e0 Mon Sep 17 00:00:00 2001
|
||||||
From: Martin Sukany <martin@sukany.cz>
|
From: Martin Sukany <martin@sukany.cz>
|
||||||
Date: Sat, 28 Feb 2026 10:24:31 +0100
|
Date: Sat, 28 Feb 2026 10:35:35 +0100
|
||||||
Subject: [PATCH 3/5] ns: add interactive span elements for Tab navigation
|
Subject: [PATCH 4/6] ns: add interactive span elements for Tab navigation
|
||||||
|
|
||||||
* src/nsterm.m (ns_ax_scan_interactive_spans): New function. Scan
|
* src/nsterm.m (ns_ax_scan_interactive_spans): New function. Scan
|
||||||
visible range with O(n/skip) property-skip optimization. Priority:
|
visible range with O(n/skip) property-skip optimization. Priority:
|
||||||
widget > button > follow-link > org-link > completion-candidate >
|
widget > button > follow-link > org-link > completion-candidate >
|
||||||
keymap-overlay.
|
keymap-overlay.
|
||||||
(EmacsAccessibilityInteractiveSpan): Implement AXButton/AXLink
|
(EmacsAccessibilityInteractiveSpan): Implement AXButton/AXLink
|
||||||
elements with AXPress action for buttons and links.
|
elements with AXPress action.
|
||||||
(EmacsAccessibilityBuffer(InteractiveSpans)): New category.
|
(EmacsAccessibilityBuffer(InteractiveSpans)): New category.
|
||||||
accessibilityChildrenInNavigationOrder for Tab/Shift-Tab cycling
|
accessibilityChildrenInNavigationOrder for Tab/Shift-Tab cycling
|
||||||
with wrap-around and VoiceOver focus notification.
|
with wrap-around.
|
||||||
|
|
||||||
Tested on macOS 14 with VoiceOver. Verified: Tab-cycling through
|
Tested on macOS 14. Verified: Tab-cycling through org-mode links,
|
||||||
org-mode links, *Completions* candidates, widget buttons.
|
*Completions* candidates, widget buttons, customize buffers.
|
||||||
---
|
---
|
||||||
src/nsterm.m | 286 +++++++++++++++++++++++++++++++++++++++++++++++++++
|
src/nsterm.m | 286 +++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
1 file changed, 286 insertions(+)
|
1 file changed, 286 insertions(+)
|
||||||
|
|
||||||
diff --git a/src/nsterm.m b/src/nsterm.m
|
diff --git a/src/nsterm.m b/src/nsterm.m
|
||||||
index c1cb602..10b63f4 100644
|
index d27e4ed..bfa1b26 100644
|
||||||
--- a/src/nsterm.m
|
--- a/src/nsterm.m
|
||||||
+++ b/src/nsterm.m
|
+++ b/src/nsterm.m
|
||||||
@@ -8910,6 +8910,292 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
|
@@ -8924,6 +8924,292 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
From a846de7dda7051a80f363b5f5db916db7868fde6 Mon Sep 17 00:00:00 2001
|
From 2de08fd21b94d2eec8271b162a6bbfa94576356c Mon Sep 17 00:00:00 2001
|
||||||
From: Martin Sukany <martin@sukany.cz>
|
From: Martin Sukany <martin@sukany.cz>
|
||||||
Date: Sat, 28 Feb 2026 10:24:31 +0100
|
Date: Sat, 28 Feb 2026 10:35:35 +0100
|
||||||
Subject: [PATCH 4/5] ns: integrate accessibility with EmacsView and redisplay
|
Subject: [PATCH 5/6] ns: integrate accessibility with EmacsView and redisplay
|
||||||
|
|
||||||
Wire the accessibility infrastructure into EmacsView and the
|
Wire the accessibility infrastructure into EmacsView and the
|
||||||
redisplay cycle. After this patch, VoiceOver and Zoom are active.
|
redisplay cycle. After this patch, VoiceOver and Zoom are active.
|
||||||
@@ -21,18 +21,19 @@ redisplay cycle. After this patch, VoiceOver and Zoom are active.
|
|||||||
(EmacsView accessibilityAttributeValue:forParameter:): New method.
|
(EmacsView accessibilityAttributeValue:forParameter:): New method.
|
||||||
* etc/NEWS: Document VoiceOver accessibility support.
|
* etc/NEWS: Document VoiceOver accessibility support.
|
||||||
|
|
||||||
Tested on macOS 14 with VoiceOver and Zoom. Full end-to-end: buffer
|
Tested on macOS 14 with VoiceOver and Zoom. End-to-end: buffer
|
||||||
navigation, cursor tracking, window switching, completions, evil-mode
|
navigation, cursor tracking, window switching, completions, evil-mode
|
||||||
block cursor, org-mode folded headings, indirect buffers.
|
block cursor, org-mode folded headings, indirect buffers.
|
||||||
|
|
||||||
Known limitations:
|
Known limitations:
|
||||||
- Bidi: accessibilityRangeForPosition assumes LTR glyph layout.
|
- Bidi: accessibilityRangeForPosition assumes LTR glyph layout.
|
||||||
- Mode-line icons: image/stretch glyphs not extracted.
|
- Mode-line icons: image/stretch glyphs not extracted (TODO).
|
||||||
- Text cap: buffers exceeding NS_AX_TEXT_CAP (100K) truncated.
|
- Text cap: buffers exceeding NS_AX_TEXT_CAP (100K) truncated.
|
||||||
|
- Non-ASCII: tested with CJK (UTF-16 surrogates) and combining chars.
|
||||||
---
|
---
|
||||||
etc/NEWS | 13 ++
|
etc/NEWS | 13 ++
|
||||||
src/nsterm.m | 397 ++++++++++++++++++++++++++++++++++++++++++++++++++-
|
src/nsterm.m | 398 ++++++++++++++++++++++++++++++++++++++++++++++++++-
|
||||||
2 files changed, 408 insertions(+), 2 deletions(-)
|
2 files changed, 408 insertions(+), 3 deletions(-)
|
||||||
|
|
||||||
diff --git a/etc/NEWS b/etc/NEWS
|
diff --git a/etc/NEWS b/etc/NEWS
|
||||||
index 7367e3c..608650e 100644
|
index 7367e3c..608650e 100644
|
||||||
@@ -59,7 +60,7 @@ index 7367e3c..608650e 100644
|
|||||||
** Re-introduced dictation, lost in Emacs v30 (macOS).
|
** Re-introduced dictation, lost in Emacs v30 (macOS).
|
||||||
We lost macOS dictation in v30 when migrating to NSTextInputClient.
|
We lost macOS dictation in v30 when migrating to NSTextInputClient.
|
||||||
diff --git a/src/nsterm.m b/src/nsterm.m
|
diff --git a/src/nsterm.m b/src/nsterm.m
|
||||||
index 10b63f4..d7ff6c3 100644
|
index bfa1b26..1780194 100644
|
||||||
--- a/src/nsterm.m
|
--- a/src/nsterm.m
|
||||||
+++ b/src/nsterm.m
|
+++ b/src/nsterm.m
|
||||||
@@ -1105,6 +1105,11 @@ ns_update_end (struct frame *f)
|
@@ -1105,6 +1105,11 @@ ns_update_end (struct frame *f)
|
||||||
@@ -126,7 +127,15 @@ index 10b63f4..d7ff6c3 100644
|
|||||||
static BOOL
|
static BOOL
|
||||||
ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point,
|
ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point,
|
||||||
ptrdiff_t *out_start,
|
ptrdiff_t *out_start,
|
||||||
@@ -8911,7 +8952,6 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
|
@@ -8380,7 +8421,6 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
|
||||||
|
@end
|
||||||
|
|
||||||
|
|
||||||
|
-
|
||||||
|
/* ===================================================================
|
||||||
|
EmacsAccessibilityBuffer (Notifications) — AX event dispatch
|
||||||
|
|
||||||
|
@@ -8925,7 +8965,6 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|
||||||
@@ -134,7 +143,7 @@ index 10b63f4..d7ff6c3 100644
|
|||||||
/* ===================================================================
|
/* ===================================================================
|
||||||
EmacsAccessibilityInteractiveSpan — helpers and implementation
|
EmacsAccessibilityInteractiveSpan — helpers and implementation
|
||||||
=================================================================== */
|
=================================================================== */
|
||||||
@@ -9241,6 +9281,7 @@ ns_ax_scan_interactive_spans (struct window *w,
|
@@ -9255,6 +9294,7 @@ ns_ax_scan_interactive_spans (struct window *w,
|
||||||
[layer release];
|
[layer release];
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -142,7 +151,7 @@ index 10b63f4..d7ff6c3 100644
|
|||||||
[[self menu] release];
|
[[self menu] release];
|
||||||
[super dealloc];
|
[super dealloc];
|
||||||
}
|
}
|
||||||
@@ -10589,6 +10630,32 @@ ns_in_echo_area (void)
|
@@ -10603,6 +10643,32 @@ ns_in_echo_area (void)
|
||||||
XSETFRAME (event.frame_or_window, emacsframe);
|
XSETFRAME (event.frame_or_window, emacsframe);
|
||||||
kbd_buffer_store_event (&event);
|
kbd_buffer_store_event (&event);
|
||||||
ns_send_appdefined (-1); // Kick main loop
|
ns_send_appdefined (-1); // Kick main loop
|
||||||
@@ -175,7 +184,7 @@ index 10b63f4..d7ff6c3 100644
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -11826,6 +11893,332 @@ ns_in_echo_area (void)
|
@@ -11840,6 +11906,332 @@ ns_in_echo_area (void)
|
||||||
return fs_state;
|
return fs_state;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
From 30dd99a0530092614b47ac7b30b19728359475db Mon Sep 17 00:00:00 2001
|
From e78b534524fec76d257db2c590a87ef61ea4f291 Mon Sep 17 00:00:00 2001
|
||||||
From: Martin Sukany <martin@sukany.cz>
|
From: Martin Sukany <martin@sukany.cz>
|
||||||
Date: Sat, 28 Feb 2026 10:24:31 +0100
|
Date: Sat, 28 Feb 2026 10:35:36 +0100
|
||||||
Subject: [PATCH 5/5] doc: add VoiceOver accessibility section to macOS
|
Subject: [PATCH 6/6] doc: add VoiceOver accessibility section to macOS
|
||||||
appendix
|
appendix
|
||||||
|
|
||||||
* doc/emacs/macos.texi (VoiceOver Accessibility): New node. Document
|
* doc/emacs/macos.texi (VoiceOver Accessibility): New node. Document
|
||||||
Reference in New Issue
Block a user