patches: fix AX enum mapping + completion announcement source

This commit is contained in:
2026-02-26 18:06:14 +01:00
parent b3a6141831
commit 74b9691856

View File

@@ -1,13 +1,13 @@
From 991ef1c9f04ccfdd71482ef3df54050f314e8ab5 Mon Sep 17 00:00:00 2001
From 5fb855925912142401db0c732fff2014d21c3362 Mon Sep 17 00:00:00 2001
From: Daneel <daneel@sukany.cz>
Date: Thu, 26 Feb 2026 17:49:52 +0100
Date: Thu, 26 Feb 2026 18:06:10 +0100
Subject: [PATCH] ns: implement AXBoundsForRange and VoiceOver interaction
fixes
---
nsterm.h | 70 ++
nsterm.m | 2058 +++++++++++++++++++++++++++++++++++++++++++++++++-----
2 files changed, 1960 insertions(+), 168 deletions(-)
nsterm.m | 2090 +++++++++++++++++++++++++++++++++++++++++++++++++-----
2 files changed, 1997 insertions(+), 163 deletions(-)
diff --git a/src/nsterm.h b/src/nsterm.h
index 7c1ee4c..97da979 100644
@@ -105,7 +105,7 @@ index 7c1ee4c..97da979 100644
diff --git a/src/nsterm.m b/src/nsterm.m
index 932d209..8673194 100644
index 932d209..e7af9a3 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -1104,6 +1104,11 @@ ns_update_end (struct frame *f)
@@ -158,7 +158,7 @@ index 932d209..8673194 100644
ns_focus (f, NULL, 0);
NSGraphicsContext *ctx = [NSGraphicsContext currentContext];
@@ -6849,245 +6885,1611 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action)
@@ -6849,240 +6885,1646 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action)
/* ==========================================================================
@@ -462,6 +462,27 @@ index 932d209..8673194 100644
-#endif
-- (Lisp_Object) showFontPanel
+/* AX enum numeric compatibility for NSAccessibility notifications.
+ Values match WebKit AXObjectCacheMac fallback enums
+ (AXTextStateChangeType / AXTextEditType / AXTextSelectionDirection /
+ AXTextSelectionGranularity). */
+enum {
+ ns_ax_text_state_change_unknown = 0,
+ ns_ax_text_state_change_edit = 1,
+ ns_ax_text_state_change_selection_move = 2,
+
+ ns_ax_text_edit_type_typing = 3,
+
+ ns_ax_text_selection_direction_unknown = 0,
+ ns_ax_text_selection_direction_previous = 3,
+ ns_ax_text_selection_direction_next = 4,
+ ns_ax_text_selection_direction_discontiguous = 5,
+
+ ns_ax_text_selection_granularity_unknown = 0,
+ ns_ax_text_selection_granularity_character = 1,
+ ns_ax_text_selection_granularity_line = 3,
+};
+
+static NSUInteger
+ns_ax_utf16_length_for_buffer_range (struct buffer *b, ptrdiff_t start,
+ ptrdiff_t end)
@@ -727,11 +748,6 @@ index 932d209..8673194 100644
- /* XXX: There is an occasional condition in which, when Emacs display
- updates a different frame from the current one, and temporarily
- selects it, then processes some interrupt-driven input
- (dispnew.c:3878), OS will send the event to the correct NSWindow, but
- for some reason that window has its first responder set to the NSView
- most recently updated (I guess), which is not the correct one. */
- [(EmacsView *)[[theEvent window] delegate] keyDown: theEvent];
- return;
+ ptrdiff_t start;
+ ns_ax_visible_run *runs = NULL;
+ NSUInteger nruns = 0;
@@ -1006,8 +1022,9 @@ index 932d209..8673194 100644
+
+ /* Post SelectedTextChanged so VoiceOver reads the current line
+ upon entering text interaction mode.
+ kAXTextStateChangeTypeSelectionMove = 1. */
+ NSDictionary *info = @{@"AXTextStateChangeType": @1};
+ WebKit AXObjectCacheMac fallback enum: SelectionMove = 2. */
+ NSDictionary *info = @{@"AXTextStateChangeType": @(ns_ax_text_state_change_selection_move),
+ @"AXTextChangeElement": self};
+ NSAccessibilityPostNotificationWithUserInfo (
+ self, NSAccessibilitySelectedTextChangedNotification, info);
+}
@@ -1261,7 +1278,7 @@ index 932d209..8673194 100644
+ BOOL markActive = !NILP (BVAR (b, mark_active));
+
+ /* --- Text changed → typing echo ---
+ kAXTextStateChangeTypeEdit = 0, kAXTextEditTypeTyping = 3. */
+ WebKit AXObjectCacheMac fallback enum: Edit = 1, Typing = 3. */
+ if (modiff != self.cachedModiff)
+ {
+ /* Capture changed char before invalidating cache. */
@@ -1293,20 +1310,21 @@ index 932d209..8673194 100644
+ self.cachedPoint = point;
+
+ NSDictionary *change = @{
+ @"AXTextEditType": @3,
+ @"AXTextEditType": @(ns_ax_text_edit_type_typing),
+ @"AXTextChangeValue": changedChar,
+ @"AXTextChangeValueLength": @([changedChar length])
+ };
+ NSDictionary *userInfo = @{
+ @"AXTextStateChangeType": @0,
+ @"AXTextChangeValues": @[change]
+ @"AXTextStateChangeType": @(ns_ax_text_state_change_edit),
+ @"AXTextChangeValues": @[change],
+ @"AXTextChangeElement": self
+ };
+ NSAccessibilityPostNotificationWithUserInfo (
+ self, NSAccessibilityValueChangedNotification, userInfo);
+ }
+
+ /* --- Cursor moved or selection changed → line reading ---
+ kAXTextStateChangeTypeSelectionMove = 1.
+ WebKit AXObjectCacheMac fallback enum: SelectionMove = 2.
+ Use 'else if' — edits and selection moves are mutually exclusive
+ per the WebKit/Chromium pattern. VoiceOver gets confused if
+ both notifications arrive in the same runloop iteration. */
@@ -1316,23 +1334,23 @@ index 932d209..8673194 100644
+ self.cachedPoint = point;
+ self.cachedMarkActive = markActive;
+
+ /* Compute direction: 3=Previous, 4=Next, 5=Discontiguous. */
+ NSInteger direction = 5;
+ /* Compute direction. */
+ NSInteger direction = ns_ax_text_selection_direction_discontiguous;
+ if (point > oldPoint)
+ direction = 4;
+ direction = ns_ax_text_selection_direction_next;
+ else if (point < oldPoint)
+ direction = 3;
+ direction = ns_ax_text_selection_direction_previous;
+
+ /* Compute granularity from movement distance.
+ Prefer robust line-range comparison for vertical movement,
+ otherwise single char (1) or unknown (0). */
+ NSInteger granularity = 0;
+ NSInteger granularity = ns_ax_text_selection_granularity_unknown;
+ [self ensureTextCache];
+ if (cachedText && oldPoint > 0)
+ {
+ ptrdiff_t delta = point - oldPoint;
+ if (delta == 1 || delta == -1)
+ granularity = 1; /* Character. */
+ granularity = ns_ax_text_selection_granularity_character; /* Character. */
+ else
+ {
+ NSUInteger tlen = [cachedText length];
@@ -1348,15 +1366,16 @@ index 932d209..8673194 100644
+ NSRange newLine = [cachedText lineRangeForRange:
+ NSMakeRange (newIdx, 0)];
+ if (oldLine.location != newLine.location)
+ granularity = 3; /* Line. */
+ granularity = ns_ax_text_selection_granularity_line; /* Line. */
+
+ }
+ }
+
+ NSDictionary *moveInfo = @{
+ @"AXTextStateChangeType": @1,
+ @"AXTextStateChangeType": @(ns_ax_text_state_change_selection_move),
+ @"AXTextSelectionDirection": @(direction),
+ @"AXTextSelectionGranularity": @(granularity)
+ @"AXTextSelectionGranularity": @(granularity),
+ @"AXTextChangeElement": self
+ };
+ NSAccessibilityPostNotificationWithUserInfo (
+ self,
@@ -1451,13 +1470,20 @@ index 932d209..8673194 100644
+ ptrdiff_t ov_end = OVERLAY_END (ov);
+ if (ov_end > ov_start)
+ {
+ NSUInteger ax_s = [self accessibilityIndexForCharpos:
+ NSUInteger ax_idx = [self accessibilityIndexForCharpos:
+ ov_start];
+ NSUInteger ax_e = [self accessibilityIndexForCharpos:
+ ov_end];
+ if (ax_e > ax_s && ax_e <= [cachedText length])
+ announceText = [cachedText substringWithRange:
+ NSMakeRange (ax_s, ax_e - ax_s)];
+ if (ax_idx <= [cachedText length])
+ {
+ NSInteger lineNum = [self accessibilityLineForIndex:ax_idx];
+ NSRange lineRange = [self accessibilityRangeForLine:lineNum];
+ if (lineRange.location != NSNotFound
+ && lineRange.length > 0
+ && lineRange.location + lineRange.length
+ <= [cachedText length])
+ announceText = [cachedText substringWithRange:lineRange];
+ }
+ currentOverlayStart = ov_start;
+ currentOverlayEnd = ov_end;
+ }
+ break;
+ }
@@ -1472,17 +1498,21 @@ index 932d209..8673194 100644
+ ptrdiff_t ov_end = 0;
+ if (ns_ax_find_completion_overlay_range (b, point, &ov_start, &ov_end))
+ {
+ NSUInteger ax_s = [self accessibilityIndexForCharpos:ov_start];
+ NSUInteger ax_e = [self accessibilityIndexForCharpos:ov_end];
+ if (ax_e > ax_s && ax_e <= [cachedText length])
+ NSUInteger ax_idx = [self accessibilityIndexForCharpos:ov_start];
+ if (ax_idx <= [cachedText length])
+ {
+ announceText = [cachedText substringWithRange:
+ NSMakeRange (ax_s, ax_e - ax_s)];
+ NSInteger lineNum = [self accessibilityLineForIndex:ax_idx];
+ NSRange lineRange = [self accessibilityRangeForLine:lineNum];
+ if (lineRange.location != NSNotFound
+ && lineRange.length > 0
+ && lineRange.location + lineRange.length
+ <= [cachedText length])
+ announceText = [cachedText substringWithRange:lineRange];
+ }
+ currentOverlayStart = ov_start;
+ currentOverlayEnd = ov_end;
+ }
+ }
+ }
+
+ if (b != oldb2)
+ set_buffer_internal_1 (oldb2);
@@ -1571,13 +1601,18 @@ index 932d209..8673194 100644
+ &currentOverlayStart,
+ &currentOverlayEnd))
+ {
+ NSUInteger ax_s = [self accessibilityIndexForCharpos:
+ NSUInteger ax_idx = [self accessibilityIndexForCharpos:
+ currentOverlayStart];
+ NSUInteger ax_e = [self accessibilityIndexForCharpos:
+ currentOverlayEnd];
+ if (ax_e > ax_s && ax_e <= [cachedText length])
+ announceText = [cachedText substringWithRange:
+ NSMakeRange (ax_s, ax_e - ax_s)];
+ if (ax_idx <= [cachedText length])
+ {
+ NSInteger lineNum = [self accessibilityLineForIndex:ax_idx];
+ NSRange lineRange = [self accessibilityRangeForLine:lineNum];
+ if (lineRange.location != NSNotFound
+ && lineRange.length > 0
+ && lineRange.location + lineRange.length
+ <= [cachedText length])
+ announceText = [cachedText substringWithRange:lineRange];
+ }
+ }
+
+ if (b != oldb2)
@@ -1930,15 +1965,10 @@ index 932d209..8673194 100644
+ /* XXX: There is an occasional condition in which, when Emacs display
+ updates a different frame from the current one, and temporarily
+ selects it, then processes some interrupt-driven input
+ (dispnew.c:3878), OS will send the event to the correct NSWindow, but
+ for some reason that window has its first responder set to the NSView
+ most recently updated (I guess), which is not the correct one. */
+ [(EmacsView *)[[theEvent window] delegate] keyDown: theEvent];
+ return;
}
if (nsEvArray == nil)
@@ -8237,6 +9639,27 @@ ns_in_echo_area (void)
(dispnew.c:3878), OS will send the event to the correct NSWindow, but
for some reason that window has its first responder set to the NSView
most recently updated (I guess), which is not the correct one. */
@@ -8237,6 +9679,28 @@ ns_in_echo_area (void)
XSETFRAME (event.frame_or_window, emacsframe);
kbd_buffer_store_event (&event);
ns_send_appdefined (-1); // Kick main loop
@@ -1954,7 +1984,8 @@ index 932d209..8673194 100644
+ {
+ NSAccessibilityPostNotification (focused,
+ NSAccessibilityFocusedUIElementChangedNotification);
+ NSDictionary *info = @{@"AXTextStateChangeType": @1};
+ NSDictionary *info = @{@"AXTextStateChangeType": @(ns_ax_text_state_change_selection_move),
+ @"AXTextChangeElement": focused};
+ NSAccessibilityPostNotificationWithUserInfo (focused,
+ NSAccessibilitySelectedTextChangedNotification, info);
+ }
@@ -1966,7 +1997,7 @@ index 932d209..8673194 100644
}
@@ -9474,6 +10897,297 @@ ns_in_echo_area (void)
@@ -9474,6 +10938,298 @@ ns_in_echo_area (void)
return fs_state;
}
@@ -2184,7 +2215,8 @@ index 932d209..8673194 100644
+ {
+ NSAccessibilityPostNotification (focused,
+ NSAccessibilityFocusedUIElementChangedNotification);
+ NSDictionary *info = @{@"AXTextStateChangeType": @1};
+ NSDictionary *info = @{@"AXTextStateChangeType": @(ns_ax_text_state_change_selection_move),
+ @"AXTextChangeElement": focused};
+ NSAccessibilityPostNotificationWithUserInfo (focused,
+ NSAccessibilitySelectedTextChangedNotification, info);
+ }
@@ -2264,7 +2296,7 @@ index 932d209..8673194 100644
@end /* EmacsView */
@@ -9941,6 +11655,14 @@ nswindow_orderedIndex_sort (id w1, id w2, void *c)
@@ -9941,6 +11697,14 @@ nswindow_orderedIndex_sort (id w1, id w2, void *c)
return [super accessibilityAttributeValue:attribute];
}