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 382d6b5..468de4b 100644 --- a/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch +++ b/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch @@ -1,5 +1,5 @@ diff --git a/src/nsterm.h b/src/nsterm.h -index 7c1ee4c..8bf21f6 100644 +index 7c1ee4c..4abeafe 100644 --- a/src/nsterm.h +++ b/src/nsterm.h @@ -453,6 +453,40 @@ enum ns_return_frame_mode @@ -43,20 +43,22 @@ index 7c1ee4c..8bf21f6 100644 /* ========================================================================== The main Emacs view -@@ -471,6 +505,12 @@ enum ns_return_frame_mode +@@ -471,6 +505,14 @@ enum ns_return_frame_mode #ifdef NS_IMPL_COCOA char *old_title; BOOL maximizing_resize; + NSMutableArray *accessibilityElements; + Lisp_Object lastSelectedWindow; ++ Lisp_Object lastRootWindow; + BOOL accessibilityTreeValid; ++ BOOL accessibilityUpdating; + @public + NSRect lastAccessibilityCursorRect; + @protected #endif BOOL font_panel_active; NSFont *font_panel_result; -@@ -528,6 +568,13 @@ enum ns_return_frame_mode +@@ -528,6 +570,13 @@ enum ns_return_frame_mode - (void)windowWillExitFullScreen; - (void)windowDidExitFullScreen; - (void)windowDidBecomeKey; @@ -71,7 +73,7 @@ index 7c1ee4c..8bf21f6 100644 diff --git a/src/nsterm.m b/src/nsterm.m -index 932d209..6543e3b 100644 +index 932d209..e830f54 100644 --- a/src/nsterm.m +++ b/src/nsterm.m @@ -1104,6 +1104,11 @@ ns_update_end (struct frame *f) @@ -124,7 +126,7 @@ index 932d209..6543e3b 100644 ns_focus (f, NULL, 0); NSGraphicsContext *ctx = [NSGraphicsContext currentContext]; -@@ -6847,6 +6883,717 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action) +@@ -6847,6 +6883,756 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action) } #endif @@ -759,13 +761,52 @@ index 932d209..6543e3b 100644 + kAXTextStateChangeTypeSelectionMove = 1. */ + if (point != self.cachedPoint || markActive != self.cachedMarkActive) + { ++ ptrdiff_t oldPoint = self.cachedPoint; + self.cachedPoint = point; + self.cachedMarkActive = markActive; + ++ /* Compute direction: 2=Previous, 3=Next, 4=Discontiguous. */ ++ NSInteger direction = 4; ++ if (point > oldPoint) ++ direction = 3; ++ else if (point < oldPoint) ++ direction = 2; ++ ++ /* Compute granularity from movement distance. ++ Check if we crossed a newline → line movement (2). ++ Otherwise single char (0) or discontiguous (5=unknown). */ ++ NSInteger granularity = 5; ++ [self ensureTextCache]; ++ if (cachedText && oldPoint > 0) ++ { ++ ptrdiff_t delta = point - oldPoint; ++ if (delta == 1 || delta == -1) ++ granularity = 0; /* Character. */ ++ else ++ { ++ /* Check for line crossing by looking for newlines ++ between old and new position. */ ++ NSUInteger lo = (NSUInteger) ++ (MIN (oldPoint, point) - cachedTextStart); ++ NSUInteger hi = (NSUInteger) ++ (MAX (oldPoint, point) - cachedTextStart); ++ NSUInteger tlen = [cachedText length]; ++ if (lo < tlen && hi <= tlen) ++ { ++ NSRange searchRange = NSMakeRange (lo, hi - lo); ++ NSRange nl = [cachedText rangeOfString:@"\n" ++ options:0 ++ range:searchRange]; ++ if (nl.location != NSNotFound) ++ granularity = 2; /* Line. */ ++ } ++ } ++ } ++ + NSDictionary *moveInfo = @{ + @"AXTextStateChangeType": @1, -+ @"AXTextSelectionDirection": @4, -+ @"AXTextSelectionGranularity": @3 ++ @"AXTextSelectionDirection": @(direction), ++ @"AXTextSelectionGranularity": @(granularity) + }; + NSAccessibilityPostNotificationWithUserInfo ( + self, @@ -842,7 +883,7 @@ index 932d209..6543e3b 100644 /* ========================================================================== EmacsView implementation -@@ -6889,6 +7636,7 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action) +@@ -6889,6 +7675,7 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action) [layer release]; #endif @@ -850,7 +891,7 @@ index 932d209..6543e3b 100644 [[self menu] release]; [super dealloc]; } -@@ -8237,6 +8985,18 @@ ns_in_echo_area (void) +@@ -8237,6 +9024,18 @@ ns_in_echo_area (void) XSETFRAME (event.frame_or_window, emacsframe); kbd_buffer_store_event (&event); ns_send_appdefined (-1); // Kick main loop @@ -869,7 +910,7 @@ index 932d209..6543e3b 100644 } -@@ -9474,6 +10234,259 @@ ns_in_echo_area (void) +@@ -9474,6 +10273,290 @@ ns_in_echo_area (void) return fs_state; } @@ -1028,37 +1069,68 @@ index 932d209..6543e3b 100644 + if (!emacsframe) + return; + ++ /* Re-entrance guard: VoiceOver callbacks during notification posting ++ can trigger redisplay, which calls ns_update_end, which calls us ++ again. Prevent infinite recursion. */ ++ if (accessibilityUpdating) ++ return; ++ accessibilityUpdating = YES; ++ ++ /* Detect window tree change (split, delete, new buffer). Compare ++ FRAME_ROOT_WINDOW — if it changed, the tree structure changed. */ ++ Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe); ++ if (!EQ (curRoot, lastRootWindow)) ++ { ++ lastRootWindow = curRoot; ++ accessibilityTreeValid = NO; ++ } ++ ++ /* If tree is stale, rebuild FIRST so we don't iterate freed ++ window pointers. Skip notifications for this cycle — the ++ freshly-built elements have no previous state to diff against. */ ++ if (!accessibilityTreeValid) ++ { ++ [self rebuildAccessibilityTree]; ++ ++ /* Post focus change so VoiceOver picks up the new tree. */ ++ id focused = [self accessibilityFocusedUIElement]; ++ if (focused && focused != self) ++ NSAccessibilityPostNotification (focused, ++ NSAccessibilityFocusedUIElementChangedNotification); ++ ++ lastSelectedWindow = emacsframe->selected_window; ++ accessibilityUpdating = NO; ++ return; ++ } ++ + /* Post per-buffer notifications using EXISTING elements that have -+ cached state from the previous cycle. */ ++ cached state from the previous cycle. Validate each window ++ pointer before use. */ + for (EmacsAccessibilityElement *elem in accessibilityElements) + { + if ([elem isKindOfClass:[EmacsAccessibilityBuffer class]]) + { + struct window *w = elem.emacsWindow; -+ if (w && WINDOW_LEAF_P (w)) ++ if (w && WINDOW_LEAF_P (w) ++ && BUFFERP (w->contents) && XBUFFER (w->contents)) + [(EmacsAccessibilityBuffer *) elem + postAccessibilityNotificationsForFrame:emacsframe]; + } + } + -+ /* Check for window switch (C-x o) before rebuild. */ ++ /* Check for window switch (C-x o). */ + Lisp_Object curSel = emacsframe->selected_window; + BOOL windowSwitched = !EQ (curSel, lastSelectedWindow); + if (windowSwitched) -+ lastSelectedWindow = curSel; -+ -+ /* Rebuild tree only if window configuration changed. */ -+ if (!accessibilityTreeValid) -+ [self rebuildAccessibilityTree]; -+ -+ /* Post focus change AFTER rebuild so the new element exists. */ -+ if (windowSwitched) + { ++ lastSelectedWindow = curSel; + id focused = [self accessibilityFocusedUIElement]; + if (focused && focused != self) + NSAccessibilityPostNotification (focused, + NSAccessibilityFocusedUIElementChangedNotification); + } ++ ++ accessibilityUpdating = NO; +} + +/* ---- Cursor position for Zoom (via accessibilityBoundsForRange:) ---- @@ -1129,7 +1201,7 @@ index 932d209..6543e3b 100644 @end /* EmacsView */ -@@ -9941,6 +10954,14 @@ nswindow_orderedIndex_sort (id w1, id w2, void *c) +@@ -9941,6 +11024,14 @@ nswindow_orderedIndex_sort (id w1, id w2, void *c) return [super accessibilityAttributeValue:attribute]; }