From 74b9691856531b9774fa248fb8c617a5cac2fa54 Mon Sep 17 00:00:00 2001 From: Daneel Date: Thu, 26 Feb 2026 18:06:14 +0100 Subject: [PATCH] patches: fix AX enum mapping + completion announcement source --- ...oundsForRange-for-macOS-Zoom-cursor-.patch | 152 +++++++++++------- 1 file changed, 92 insertions(+), 60 deletions(-) diff --git a/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch b/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch index 9a14bf3..f8cf147 100644 --- a/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch +++ b/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch @@ -1,13 +1,13 @@ -From 991ef1c9f04ccfdd71482ef3df54050f314e8ab5 Mon Sep 17 00:00:00 2001 +From 5fb855925912142401db0c732fff2014d21c3362 Mon Sep 17 00:00:00 2001 From: Daneel -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: -+ 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)]; ++ NSUInteger ax_idx = [self accessibilityIndexForCharpos: ++ ov_start]; ++ 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,15 +1498,19 @@ 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)]; -+ currentOverlayStart = ov_start; -+ currentOverlayEnd = ov_end; ++ 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; + } + } + @@ -1571,13 +1601,18 @@ index 932d209..8673194 100644 + ¤tOverlayStart, + ¤tOverlayEnd)) + { -+ 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]; }