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> From: Daneel <daneel@sukany.cz>
Date: Sun, 22 Feb 2026 23:21:32 +0100 Date: Mon, 23 Feb 2026 10:03:39 +0100
Subject: [PATCH] ns: implement AXBoundsForRange for macOS Zoom cursor tracking Subject: [PATCH] ns: implement NSAccessibility cursor tracking for macOS Zoom
Add NSAccessibilityBoundsForRangeParameterizedAttribute support to Add full NSAccessibility support to EmacsView so that macOS Zoom
EmacsView so that macOS Zoom and other accessibility tools can track 'Follow keyboard focus' tracks the text cursor position.
the text cursor position.
Implements both the old parameterized attribute API Key insight: macOS Zoom is event-driven. It only calls
(accessibilityAttributeValue:forParameter:) and the new accessibilityBoundsForRange: / AXBoundsForRange AFTER receiving
NSAccessibilityProtocol (accessibilityBoundsForRange:) so that Zoom NSAccessibilitySelectedTextChangedNotification. Without posting that
works on both older and newer macOS versions. notification, Zoom never queries cursor position -- even if the bounds
methods are correctly implemented.
Changes: Changes:
- Add lastAccessibilityCursorRect ivar to EmacsView (nsterm.h) - 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 accessibilityIsIgnored, accessibilityFocusedUIElement
- Implement accessibilityRole (NSAccessibilityTextAreaRole) - Implement accessibilityRole (NSAccessibilityTextAreaRole)
- Implement isAccessibilityElement (new API, returns YES) - Implement isAccessibilityElement (new API, returns YES)
- Implement accessibilityParameterizedAttributeNames (old API) - Implement accessibilityAttributeNames + accessibilityAttributeValue:
- Implement accessibilityAttributeValue:forParameter: for BoundsForRange (old API) responds to NSAccessibilitySelectedTextRangeAttribute with {0,0}
- Implement accessibilityBoundsForRange: (new NSAccessibilityProtocol) - Implement accessibilityBoundsForRange: (new NSAccessibilityProtocol)
- Implement accessibilityParameterizedAttributeNames + old API fallback
See also: https://github.com/nicowillis/Ghostty/issues/4053 See also: https://github.com/nicowillis/Ghostty/issues/4053
--- ---
src/nsterm.h | 3 +++ src/nsterm.h | 3 ++
src/nsterm.m | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/nsterm.m | 139 +++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 89 insertions(+) 2 files changed, 142 insertions(+)
diff --git a/src/nsterm.h b/src/nsterm.h diff --git a/src/nsterm.h b/src/nsterm.h
index 7c1ee4c..6c1ff34 100644 index 7c1ee4cf535..6c1ff3434a3 100644
--- a/src/nsterm.h --- a/src/nsterm.h
+++ b/src/nsterm.h +++ b/src/nsterm.h
@@ -485,6 +485,9 @@ enum ns_return_frame_mode @@ -485,6 +485,9 @@ enum ns_return_frame_mode
@@ -43,26 +45,49 @@ index 7c1ee4c..6c1ff34 100644
/* AppKit-side interface. */ /* AppKit-side interface. */
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index 932d209..c900d71 100644 index 932d209f56b..906c76bc468 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/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. */ /* Prevent the cursor from being drawn outside the text area. */
r = NSIntersectionRect (r, ns_row_rect (w, glyph_row, TEXT_AREA)); r = NSIntersectionRect (r, ns_row_rect (w, glyph_row, TEXT_AREA));
+#ifdef NS_IMPL_COCOA +#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); + EmacsView *view = FRAME_NS_VIEW (f);
+ if (view) + if (view)
+ {
+ view->lastAccessibilityCursorRect = r; + view->lastAccessibilityCursorRect = r;
+ NSAccessibilityPostNotification (view,
+ NSAccessibilitySelectedTextChangedNotification);
+ }
+ } + }
+#endif +#endif
+ +
ns_focus (f, NULL, 0); ns_focus (f, NULL, 0);
NSGraphicsContext *ctx = [NSGraphicsContext currentContext]; 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; return fs_state;
} }
@@ -92,6 +117,35 @@ index 932d209..c900d71 100644
+ return NSAccessibilityTextAreaRole; + 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. */ +/* New NSAccessibilityProtocol (macOS 10.10+) — preferred by Zoom. */
+- (NSRect)accessibilityBoundsForRange:(NSRange)range +- (NSRect)accessibilityBoundsForRange:(NSRange)range
+{ +{
@@ -156,3 +210,4 @@ index 932d209..c900d71 100644
-- --
2.43.0 2.43.0