fix: add NSAccessibilityPostNotification (key missing piece)

macOS Zoom is event-driven -- it only queries AXBoundsForRange after
receiving NSAccessibilitySelectedTextChangedNotification. Previous
version implemented the bounds methods but never posted notifications,
so Zoom never triggered a cursor position query.

Added:
- NSAccessibilityPostNotification(SelectedTextChanged) in ns_draw_window_cursor
- NSAccessibilityPostNotification(FocusedUIElementChanged) in windowDidBecomeKey
- accessibilityAttributeNames + accessibilityAttributeValue: for
  NSAccessibilitySelectedTextRangeAttribute -> returns {0,0}
This commit is contained in:
2026-02-23 10:03:51 +01:00
parent 12869677e7
commit 56db072f06

View File

@@ -1,35 +1,37 @@
From 23fd2004cb00908a37ad44d0c41a1ca223eb7c55 Mon Sep 17 00:00:00 2001
From ea7573e32a140ffce0758485abd959954e74c271 Mon Sep 17 00:00:00 2001
From: Daneel <daneel@sukany.cz>
Date: Sun, 22 Feb 2026 23:21:32 +0100
Subject: [PATCH] ns: implement AXBoundsForRange for macOS Zoom cursor tracking
Date: Mon, 23 Feb 2026 10:03:39 +0100
Subject: [PATCH] ns: implement NSAccessibility cursor tracking for macOS Zoom
Add NSAccessibilityBoundsForRangeParameterizedAttribute support to
EmacsView so that macOS Zoom and other accessibility tools can track
the text cursor position.
Add full NSAccessibility support to EmacsView so that macOS Zoom
'Follow keyboard focus' tracks the text cursor position.
Implements both the old parameterized attribute API
(accessibilityAttributeValue:forParameter:) and the new
NSAccessibilityProtocol (accessibilityBoundsForRange:) so that Zoom
works on both older and newer macOS versions.
Key insight: macOS Zoom is event-driven. It only calls
accessibilityBoundsForRange: / AXBoundsForRange AFTER receiving
NSAccessibilitySelectedTextChangedNotification. Without posting that
notification, Zoom never queries cursor position -- even if the bounds
methods are correctly implemented.
Changes:
- Add lastAccessibilityCursorRect ivar to EmacsView (nsterm.h)
- Store cursor rect in ns_draw_window_cursor (nsterm.m)
- Store cursor rect in ns_draw_window_cursor + post SelectedTextChanged
- Post FocusedUIElementChanged in windowDidBecomeKey
- Implement accessibilityIsIgnored, accessibilityFocusedUIElement
- Implement accessibilityRole (NSAccessibilityTextAreaRole)
- Implement isAccessibilityElement (new API, returns YES)
- Implement accessibilityParameterizedAttributeNames (old API)
- Implement accessibilityAttributeValue:forParameter: for BoundsForRange (old API)
- Implement accessibilityAttributeNames + accessibilityAttributeValue:
responds to NSAccessibilitySelectedTextRangeAttribute with {0,0}
- Implement accessibilityBoundsForRange: (new NSAccessibilityProtocol)
- Implement accessibilityParameterizedAttributeNames + old API fallback
See also: https://github.com/nicowillis/Ghostty/issues/4053
---
src/nsterm.h | 3 +++
src/nsterm.m | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 89 insertions(+)
src/nsterm.h | 3 ++
src/nsterm.m | 139 +++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 142 insertions(+)
diff --git a/src/nsterm.h b/src/nsterm.h
index 7c1ee4c..6c1ff34 100644
index 7c1ee4cf535..6c1ff3434a3 100644
--- a/src/nsterm.h
+++ b/src/nsterm.h
@@ -485,6 +485,9 @@ enum ns_return_frame_mode
@@ -43,26 +45,49 @@ index 7c1ee4c..6c1ff34 100644
/* AppKit-side interface. */
diff --git a/src/nsterm.m b/src/nsterm.m
index 932d209..c900d71 100644
index 932d209f56b..906c76bc468 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -3232,6 +3232,15 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors.
@@ -3232,6 +3232,23 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors.
/* Prevent the cursor from being drawn outside the text area. */
r = NSIntersectionRect (r, ns_row_rect (w, glyph_row, TEXT_AREA));
+#ifdef NS_IMPL_COCOA
+ /* Store cursor rect for accessibility (AXBoundsForRange). */
+ /* Store cursor rect for accessibility and notify assistive tools
+ (e.g. macOS Zoom "Follow keyboard focus") that the cursor moved.
+ macOS Zoom is event-driven: it only calls accessibilityBoundsForRange:
+ AFTER receiving NSAccessibilitySelectedTextChangedNotification.
+ Without posting this notification, Zoom never queries cursor position. */
+ {
+ EmacsView *view = FRAME_NS_VIEW (f);
+ if (view)
+ view->lastAccessibilityCursorRect = r;
+ {
+ view->lastAccessibilityCursorRect = r;
+ NSAccessibilityPostNotification (view,
+ NSAccessibilitySelectedTextChangedNotification);
+ }
+ }
+#endif
+
ns_focus (f, NULL, 0);
NSGraphicsContext *ctx = [NSGraphicsContext currentContext];
@@ -9474,6 +9483,91 @@ - (int) fullscreenState
@@ -8237,6 +8254,14 @@ - (void)windowDidBecomeKey /* for direct calls */
XSETFRAME (event.frame_or_window, emacsframe);
kbd_buffer_store_event (&event);
ns_send_appdefined (-1); // Kick main loop
+
+#ifdef NS_IMPL_COCOA
+ /* Notify accessibility clients (e.g. macOS Zoom) that the focused
+ UI element changed to this Emacs view. Zoom uses this to activate
+ keyboard focus tracking when the window gains focus. */
+ NSAccessibilityPostNotification (self,
+ NSAccessibilityFocusedUIElementChangedNotification);
+#endif
}
@@ -9474,6 +9499,120 @@ - (int) fullscreenState
return fs_state;
}
@@ -92,6 +117,35 @@ index 932d209..c900d71 100644
+ return NSAccessibilityTextAreaRole;
+}
+
+- (NSArray *)accessibilityAttributeNames
+{
+ NSArray *superAttrs = [super accessibilityAttributeNames];
+ if (superAttrs == nil)
+ superAttrs = @[];
+ return [superAttrs arrayByAddingObjectsFromArray:
+ @[NSAccessibilityRoleAttribute,
+ NSAccessibilitySelectedTextRangeAttribute,
+ NSAccessibilityNumberOfCharactersAttribute]];
+}
+
+- (id)accessibilityAttributeValue:(NSString *)attribute
+{
+ if ([attribute isEqualToString:NSAccessibilityRoleAttribute])
+ return NSAccessibilityTextAreaRole;
+
+ /* macOS Zoom queries NSAccessibilitySelectedTextRangeAttribute before
+ calling AXBoundsForRange / accessibilityBoundsForRange:. We return
+ {0,0}; our bounds methods ignore the range and always return the
+ actual cursor rect, so any range value here works. */
+ if ([attribute isEqualToString:NSAccessibilitySelectedTextRangeAttribute])
+ return [NSValue valueWithRange:NSMakeRange (0, 0)];
+
+ if ([attribute isEqualToString:NSAccessibilityNumberOfCharactersAttribute])
+ return @(0);
+
+ return [super accessibilityAttributeValue:attribute];
+}
+
+/* New NSAccessibilityProtocol (macOS 10.10+) — preferred by Zoom. */
+- (NSRect)accessibilityBoundsForRange:(NSRange)range
+{
@@ -156,3 +210,4 @@ index 932d209..c900d71 100644
--
2.43.0