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>
|
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.
|
||||||
|
|||||||
Reference in New Issue
Block a user