v4 patch: UAZoomChangeFocus (Apple documented) + NSAccessibility, with doc refs

Based on verified research:
- UAZoomChangeFocus IS officially documented by Apple (UniversalAccess.h)
- iTerm2 uses UAZoomChangeFocus for Zoom tracking (PTYTextView.m)
- NSAccessibility alone insufficient for Zoom custom view tracking
- Added Apple documentation URLs in code comments
This commit is contained in:
2026-02-25 18:17:53 +01:00
parent 6933d8f9c0
commit d7aae4b7d1

View File

@@ -1,61 +1,55 @@
From 8d439114e6faf50c053eac290ae0e60176dfb3bf Mon Sep 17 00:00:00 2001
From ea017e0a4427ad3e8d04025ae2ed511a542eeeeb Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
Date: Wed, 25 Feb 2026 18:10:58 +0100
Subject: [PATCH] ns: implement macOS Zoom cursor tracking + NSAccessibility
support
Date: Wed, 25 Feb 2026 18:17:42 +0100
Subject: [PATCH] ns: implement macOS Zoom cursor tracking via
UAZoomChangeFocus + NSAccessibility
Add cursor tracking for macOS Zoom 'Follow keyboard focus' and
full NSAccessibility support for VoiceOver and other AT tools.
Add cursor tracking support for macOS Zoom 'Follow keyboard focus' and
other assistive technology tools (VoiceOver, etc.).
Two complementary mechanisms are used:
1. UAZoomChangeFocus() from ApplicationServices/UniversalAccess.h:
Directly tells macOS Zoom where to position its viewport. This
is Apple's documented API for applications to control Zoom focus.
Same approach used by iTerm2 (PTYTextView.m:refreshAccessibility).
Ref: developer.apple.com/documentation/applicationservices/universalaccess_h
Ref: developer.apple.com/documentation/applicationservices/1458830-uazoomchangefocus
Directly tells macOS Zoom where to move its viewport. This is the
Apple-documented API for controlling Zoom focus, used by iTerm2
(PTYTextView.m, refreshAccessibility method).
Ref: https://developer.apple.com/documentation/applicationservices/universalaccess_h
Ref: https://developer.apple.com/documentation/applicationservices/1458830-uazoomchangefocus
2. NSAccessibility protocol on EmacsView: reports as TextArea role,
exposes accessibilityFrame / accessibilityBoundsForRange: returning
cursor screen coordinates, and posts SelectedTextChanged and
FocusedUIElementChanged notifications. Serves VoiceOver and AT
tools that query the accessibility tree directly.
exposes accessibilityFrame and accessibilityBoundsForRange: returning
cursor screen coordinates, posts SelectedTextChanged and
FocusedUIElementChanged notifications. Serves VoiceOver and other AT
tools that query the accessibility tree.
Ref: https://developer.apple.com/documentation/appkit/nsaccessibilityprotocol
Ref: developer.apple.com/documentation/appkit/nsaccessibilityprotocol
Both mechanisms are needed: UAZoomChangeFocus serves Zoom's Follow
keyboard focus (which does not reliably track custom views through
NSAccessibility alone); NSAccessibility serves VoiceOver and other
screen readers.
Both mechanisms are needed: UAZoomChangeFocus drives Zoom viewport
positioning for custom-drawn views; NSAccessibility notifications serve
VoiceOver, screen readers, and Accessibility Inspector.
---
src/nsterm.h | 6 ++
src/nsterm.m | 286 +++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 292 insertions(+)
src/nsterm.h | 3 +
src/nsterm.m | 221 +++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 224 insertions(+)
diff --git a/src/nsterm.h b/src/nsterm.h
index 7c1ee4c..664bce3 100644
index 7c1ee4c..6c1ff34 100644
--- a/src/nsterm.h
+++ b/src/nsterm.h
@@ -485,6 +485,12 @@ enum ns_return_frame_mode
@@ -485,6 +485,9 @@ enum ns_return_frame_mode
struct frame *emacsframe;
int scrollbarsNeedingUpdate;
NSRect ns_userRect;
+#ifdef NS_IMPL_COCOA
+ NSRect lastAccessibilityCursorRect;
+#endif
+#ifdef NS_IMPL_COCOA
+ NSRect lastAccessibilityCursorRect;
+#endif
}
/* AppKit-side interface. */
diff --git a/src/nsterm.m b/src/nsterm.m
index 932d209..96b9d65 100644
index 932d209..933f8cb 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -3232,6 +3232,131 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors.
@@ -3232,6 +3232,76 @@ 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));
@@ -80,14 +74,14 @@ index 932d209..96b9d65 100644
+ application can tell the macOS Universal Access zoom feature
+ what part of its user interface needs focus."
+
+ Ref: https://developer.apple.com/documentation/applicationservices/universalaccess_h
+ Ref: https://developer.apple.com/documentation/applicationservices/1458830-uazoomchangefocus
+ Ref: developer.apple.com/documentation/applicationservices/universalaccess_h
+ Ref: developer.apple.com/documentation/applicationservices/1458830-uazoomchangefocus
+
+ This is the same approach used by iTerm2 (PTYTextView.m,
+ method refreshAccessibility).
+
+ Both mechanisms are needed: NSAccessibility serves VoiceOver and
+ screen readers; UAZoomChangeFocus serves macOS Zoom's "Follow
+ screen readers; UAZoomChangeFocus serves macOS Zoom "Follow
+ keyboard focus" feature, which does not reliably track custom views
+ through NSAccessibility notifications alone. */
+ {
@@ -108,63 +102,8 @@ index 932d209..96b9d65 100644
+ primary screen (CG accessibility coordinate space).
+ convertRectToScreen: returns Quartz coordinates (origin at
+ bottom-left), so we flip the y axis. This coordinate
+ conversion follows the same pattern used by iTerm2's
+ accessibilityConvertScreenRect: method. */
+ if (UAZoomEnabled ())
+ {
+ NSRect windowRect = [view convertRect:r toView:nil];
+ NSRect screenRect = [[view window] convertRectToScreen:windowRect];
+ CGRect cgRect = NSRectToCGRect (screenRect);
+
+ CGFloat primaryH
+ = [[[NSScreen screens] firstObject] frame].size.height;
+ cgRect.origin.y
+ = primaryH - cgRect.origin.y - cgRect.size.height;
+
+ UAZoomChangeFocus (&cgRect, &cgRect,
+ kUAZoomFocusTypeInsertionPoint);
+ }
+ }
+ }
+#endif
+
+
+#ifdef NS_IMPL_COCOA
+ /* Accessibility: update cursor tracking for macOS Zoom and VoiceOver.
+
+ Emacs is a custom-drawn view. AppKit has no knowledge of where the
+ text cursor is, so we must explicitly notify assistive technology.
+
+ Two complementary mechanisms are used, matching the pattern used by
+ iTerm2 (PTYTextView.m:refreshAccessibility):
+
+ 1. NSAccessibility notifications -- inform AT clients (VoiceOver,
+ Zoom, Switch Control) that the selection/cursor changed.
+ See: Apple NSAccessibility Protocol Reference
+ https://developer.apple.com/documentation/appkit/nsaccessibilityprotocol
+
+ 2. UAZoomChangeFocus() -- explicitly tell macOS Zoom where to move
+ its viewport. This is the Apple-documented mechanism for custom
+ views to control the Zoom focus. NSAccessibility notifications
+ alone are NOT sufficient for Zoom cursor tracking in custom views.
+ See: Apple UniversalAccess.h Reference
+ https://developer.apple.com/documentation/applicationservices/universalaccess_h
+ https://developer.apple.com/documentation/applicationservices/1458830-uazoomchangefocus */
+ {
+ EmacsView *view = FRAME_NS_VIEW (f);
+ if (view)
+ {
+ view->lastAccessibilityCursorRect = r;
+
+ /* Post NSAccessibility notifications for VoiceOver and other AT. */
+ NSAccessibilityPostNotification (view,
+ NSAccessibilitySelectedTextChangedNotification);
+
+ /* Tell macOS Zoom where the cursor is. UAZoomChangeFocus expects
+ screen coordinates with origin at top-left of the primary screen
+ (accessibility coordinate space). We convert from Quartz screen
+ coordinates (origin bottom-left) by flipping the y axis, matching
+ the pattern used by iTerm2's accessibilityConvertScreenRect. */
+ conversion follows the same pattern used by iTerm2
+ (PTYTextView.m, accessibilityConvertScreenRect:). */
+ if (UAZoomEnabled ())
+ {
+ NSRect windowRect = [view convertRect:r toView:nil];
@@ -187,7 +126,7 @@ index 932d209..96b9d65 100644
ns_focus (f, NULL, 0);
NSGraphicsContext *ctx = [NSGraphicsContext currentContext];
@@ -8237,6 +8362,25 @@ - (void)windowDidBecomeKey /* for direct calls */
@@ -8237,6 +8307,15 @@ - (void)windowDidBecomeKey /* for direct calls */
XSETFRAME (event.frame_or_window, emacsframe);
kbd_buffer_store_event (&event);
ns_send_appdefined (-1); // Kick main loop
@@ -199,25 +138,14 @@ index 932d209..96b9d65 100644
+ to announce the newly focused element. */
+ NSAccessibilityPostNotification (self,
+ NSAccessibilityFocusedUIElementChangedNotification);
+#endif
+
+#ifdef NS_IMPL_COCOA
+ /* Notify AT that the focused UI element changed to this Emacs view.
+ macOS Zoom uses this to activate keyboard focus tracking when the
+ window gains focus.
+ See: NSAccessibilityFocusedUIElementChangedNotification
+ https://developer.apple.com/documentation/appkit/nsaccessibilityfocuseduielementchangednotification */
+ NSAccessibilityPostNotification (self,
+ NSAccessibilityFocusedUIElementChangedNotification);
+#endif
}
@@ -9474,6 +9618,148 @@ - (int) fullscreenState
return fs_state;
}
@@ -9476,6 +9555,148 @@ - (int) fullscreenState
@end /* EmacsView */
+
+#ifdef NS_IMPL_COCOA
+/* ----------------------------------------------------------------
+ Accessibility support for macOS Zoom, VoiceOver, and other AT tools.
@@ -359,9 +287,10 @@ index 932d209..96b9d65 100644
+}
+#endif /* NS_IMPL_COCOA */
+
@end /* EmacsView */
+
/* ==========================================================================
--
2.43.0