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> 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 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 Add cursor tracking support for macOS Zoom "Follow keyboard focus" and
other assistive technology tools (VoiceOver, etc.). other assistive technology (VoiceOver, etc.).
Two mechanisms are implemented: EmacsView now implements the NSAccessibility protocol: it reports itself
as a TextArea role, exposes accessibilityFrame returning the cursor's
1. NSAccessibility (primary): EmacsView reports as TextArea, exposes screen coordinates, and posts SelectedTextChanged and
accessibilityFrame returning cursor screen coordinates, and posts FocusedUIElementChanged notifications on cursor movement and window
SelectedTextChanged/FocusedUIElementChanged notifications. This is focus changes. This is the standard approach used by Terminal.app,
the standard approach used by Terminal.app, iTerm2, and Firefox. iTerm2, and Firefox — macOS Zoom monitors these notifications and
Works regardless of how Emacs is launched (app bundle or src/emacs). queries accessibilityFrame on the focused element to position its
viewport.
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.
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.h | 3 +
src/nsterm.m | 219 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/nsterm.m | 180 +++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 222 insertions(+) 2 files changed, 183 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 7c1ee4c..6c1ff34 100644
@@ -41,10 +41,10 @@ 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..335d140 100644 index 932d209..9b73f65 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/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. */ /* 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));
@@ -54,24 +54,13 @@ index 932d209..335d140 100644
+ Emacs is a custom-drawn view: AppKit does not know where the text + Emacs is a custom-drawn view: AppKit does not know where the text
+ cursor is, so we must explicitly notify assistive technology. + cursor is, so we must explicitly notify assistive technology.
+ +
+ Two mechanisms are used: + When the cursor moves, we store the cursor rectangle and post
+ + NSAccessibilitySelectedTextChangedNotification. macOS Zoom (and
+ 1. NSAccessibility notifications (PRIMARY, always active): + VoiceOver) respond by querying accessibilityFrame on the focused
+ Post NSAccessibilitySelectedTextChangedNotification so that + element (EmacsView) to find the cursor position. This is the
+ macOS Zoom (and VoiceOver) queries the focused element's + standard approach used by Terminal.app, iTerm2, and Firefox.
+ accessibilityFrame / accessibilityBoundsForRange: to find + It works regardless of how Emacs is launched (app bundle or
+ the cursor position. This is how Terminal.app, iTerm2, + src/emacs binary). */
+ 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. */
+ { + {
+ EmacsView *view = FRAME_NS_VIEW (f); + EmacsView *view = FRAME_NS_VIEW (f);
+ if (view) + if (view)
@@ -80,46 +69,18 @@ index 932d209..335d140 100644
+ accessibilityBoundsForRange: queries. */ + accessibilityBoundsForRange: queries. */
+ view->lastAccessibilityCursorRect = r; + view->lastAccessibilityCursorRect = r;
+ +
+ /* Mechanism 1: Standard NSAccessibility notification. + /* Post SelectedTextChanged so Zoom and other AT tools query
+ Zoom monitors this and reads accessibilityFrame from the + accessibilityFrame on the focused element (this view). */
+ focused element (EmacsView) to determine cursor position. */
+ NSAccessibilityPostNotification (view, + NSAccessibilityPostNotification (view,
+ NSAccessibilitySelectedTextChangedNotification); + 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 +#endif
+
+ +
ns_focus (f, NULL, 0); ns_focus (f, NULL, 0);
NSGraphicsContext *ctx = [NSGraphicsContext currentContext]; 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); XSETFRAME (event.frame_or_window, emacsframe);
kbd_buffer_store_event (&event); kbd_buffer_store_event (&event);
ns_send_appdefined (-1); // Kick main loop 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; return fs_state;
} }
@@ -152,8 +113,8 @@ index 932d209..335d140 100644
+ How it works: + How it works:
+ - EmacsView declares itself as a TextArea role (the standard role + - EmacsView declares itself as a TextArea role (the standard role
+ for editable text regions). + for editable text regions).
+ - When the cursor moves, ns_draw_window_cursor posts + - When the cursor moves, ns_draw_window_cursor stores the cursor
+ NSAccessibilitySelectedTextChangedNotification. + rect and posts NSAccessibilitySelectedTextChangedNotification.
+ - Zoom (and VoiceOver) respond by querying accessibilityFrame and/or + - Zoom (and VoiceOver) respond by querying accessibilityFrame and/or
+ accessibilityBoundsForRange: on the focused element (this view). + accessibilityBoundsForRange: on the focused element (this view).
+ - We return the stored cursor rectangle in screen coordinates. + - We return the stored cursor rectangle in screen coordinates.