v3 patch: NSAccessibility-only, drop UAZoomChangeFocus per reviewer feedback
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user