v3 patch: NSAccessibility-only, drop UAZoomChangeFocus per reviewer feedback

This commit is contained in:
2026-02-25 17:50:04 +01:00
parent 8a8e71764d
commit f3b8ff661c

View File

@@ -1,30 +1,30 @@
From 33b2603ba61eb0e42286a5805d7acf5791ff6384 Mon Sep 17 00:00:00 2001
From fe9a8b85d064d68ce5676dcf5532583d2de82dd3 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
Date: Wed, 25 Feb 2026 17:02:36 +0100
Date: Wed, 25 Feb 2026 17:45:43 +0100
Subject: [PATCH] ns: implement macOS Zoom cursor tracking via NSAccessibility
+ UAZoomChangeFocus
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Add cursor tracking support for macOS Zoom 'Follow keyboard focus' and
other assistive technology tools (VoiceOver, etc.).
Add cursor tracking support for macOS Zoom "Follow keyboard focus" and
other assistive technology (VoiceOver, etc.).
Two mechanisms are implemented:
1. NSAccessibility (primary): EmacsView reports as TextArea, exposes
accessibilityFrame returning cursor screen coordinates, and posts
SelectedTextChanged/FocusedUIElementChanged notifications. This is
the standard approach used by Terminal.app, iTerm2, and Firefox.
Works regardless of how Emacs is launched (app bundle or src/emacs).
2. UAZoomChangeFocus (supplementary): Direct push to Zoom via
HIServices/UniversalAccess.h. Only called when running from a
proper .app bundle (detected via bundleIdentifier check), because
the window server identifies callers by CFBundleIdentifier and
silently ignores bare binaries.
EmacsView now implements the NSAccessibility protocol: it reports itself
as a TextArea role, exposes accessibilityFrame returning the cursor's
screen coordinates, and posts SelectedTextChanged and
FocusedUIElementChanged notifications on cursor movement and window
focus changes. This is the standard approach used by Terminal.app,
iTerm2, and Firefox — macOS Zoom monitors these notifications and
queries accessibilityFrame on the focused element to position its
viewport.
Both the modern protocol API (accessibilityBoundsForRange:, 10.10+) and
the legacy parameterized attribute API (AXBoundsForRange) are implemented
for compatibility with all AT tools.
---
src/nsterm.h | 3 +
src/nsterm.m | 219 +++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 222 insertions(+)
src/nsterm.m | 180 +++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 183 insertions(+)
diff --git a/src/nsterm.h b/src/nsterm.h
index 7c1ee4c..6c1ff34 100644
@@ -41,10 +41,10 @@ index 7c1ee4c..6c1ff34 100644
/* AppKit-side interface. */
diff --git a/src/nsterm.m b/src/nsterm.m
index 932d209..335d140 100644
index 932d209..9b73f65 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -3232,6 +3232,74 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors.
@@ -3232,6 +3232,35 @@ 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));
@@ -54,24 +54,13 @@ index 932d209..335d140 100644
+ Emacs is a custom-drawn view: AppKit does not know where the text
+ cursor is, so we must explicitly notify assistive technology.
+
+ Two mechanisms are used:
+
+ 1. NSAccessibility notifications (PRIMARY, always active):
+ Post NSAccessibilitySelectedTextChangedNotification so that
+ macOS Zoom (and VoiceOver) queries the focused element's
+ accessibilityFrame / accessibilityBoundsForRange: to find
+ the cursor position. This is how Terminal.app, iTerm2,
+ and Firefox implement Zoom cursor tracking. Works regardless
+ of how Emacs is launched (app bundle, src/emacs, etc.).
+
+ 2. UAZoomChangeFocus (SUPPLEMENTARY, bundle-only):
+ Directly tells Zoom the cursor's screen coordinates via
+ HIServices/UniversalAccess.h. This only works when Emacs
+ runs from a proper .app bundle -- the window server identifies
+ callers by CFBundleIdentifier, and bare binaries (src/emacs)
+ are silently ignored even when AXIsProcessTrusted() returns
+ true. We detect this by checking [[NSBundle mainBundle]
+ bundleIdentifier] and skip the call when nil. */
+ When the cursor moves, we store the cursor rectangle and post
+ NSAccessibilitySelectedTextChangedNotification. macOS Zoom (and
+ VoiceOver) respond by querying accessibilityFrame on the focused
+ element (EmacsView) to find the cursor position. This is the
+ standard approach used by Terminal.app, iTerm2, and Firefox.
+ It works regardless of how Emacs is launched (app bundle or
+ src/emacs binary). */
+ {
+ EmacsView *view = FRAME_NS_VIEW (f);
+ if (view)
@@ -80,46 +69,18 @@ index 932d209..335d140 100644
+ accessibilityBoundsForRange: queries. */
+ view->lastAccessibilityCursorRect = r;
+
+ /* Mechanism 1: Standard NSAccessibility notification.
+ Zoom monitors this and reads accessibilityFrame from the
+ focused element (EmacsView) to determine cursor position. */
+ /* Post SelectedTextChanged so Zoom and other AT tools query
+ accessibilityFrame on the focused element (this view). */
+ NSAccessibilityPostNotification (view,
+ NSAccessibilitySelectedTextChangedNotification);
+
+ /* Mechanism 2: UAZoomChangeFocus -- direct push to Zoom.
+ Only effective from app bundles (window server ignores bare
+ binaries). UAZoomChangeFocus expects coordinates with origin
+ at the top-left of the primary screen. convertRectToScreen:
+ returns Quartz coordinates (origin bottom-left), so we flip
+ the y axis manually. */
+ if (UAZoomEnabled ()
+ && [[NSBundle mainBundle] bundleIdentifier] != nil)
+ {
+ NSRect windowRect = [view convertRect:r toView:nil];
+ NSRect screenRect = [[view window] convertRectToScreen:windowRect];
+ CGRect cgRect = NSRectToCGRect (screenRect);
+
+ /* Flip y: Quartz (bottom-left origin) -> accessibility
+ coordinate space (top-left origin). Uses primary screen
+ height as reference -- correct for all screens in the
+ global Quartz coordinate space. */
+ CGFloat primaryH
+ = [[[NSScreen screens] firstObject] frame].size.height;
+ cgRect.origin.y
+ = primaryH - cgRect.origin.y - cgRect.size.height;
+
+ UAZoomChangeFocus (&cgRect, &cgRect,
+ kUAZoomFocusTypeInsertionPoint);
+ }
+ }
+ }
+#endif
+
+
ns_focus (f, NULL, 0);
NSGraphicsContext *ctx = [NSGraphicsContext currentContext];
@@ -8237,6 +8305,17 @@ - (void)windowDidBecomeKey /* for direct calls */
@@ -8237,6 +8266,17 @@ - (void)windowDidBecomeKey /* for direct calls */
XSETFRAME (event.frame_or_window, emacsframe);
kbd_buffer_store_event (&event);
ns_send_appdefined (-1); // Kick main loop
@@ -137,7 +98,7 @@ index 932d209..335d140 100644
}
@@ -9474,6 +9553,146 @@ - (int) fullscreenState
@@ -9474,6 +9514,146 @@ - (int) fullscreenState
return fs_state;
}
@@ -152,8 +113,8 @@ index 932d209..335d140 100644
+ How it works:
+ - EmacsView declares itself as a TextArea role (the standard role
+ for editable text regions).
+ - When the cursor moves, ns_draw_window_cursor posts
+ NSAccessibilitySelectedTextChangedNotification.
+ - When the cursor moves, ns_draw_window_cursor stores the cursor
+ rect and posts NSAccessibilitySelectedTextChangedNotification.
+ - Zoom (and VoiceOver) respond by querying accessibilityFrame and/or
+ accessibilityBoundsForRange: on the focused element (this view).
+ - We return the stored cursor rectangle in screen coordinates.