From f3b8ff661c180ffa974f04d299f058274e8c1273 Mon Sep 17 00:00:00 2001 From: Daneel Date: Wed, 25 Feb 2026 17:50:04 +0100 Subject: [PATCH] v3 patch: NSAccessibility-only, drop UAZoomChangeFocus per reviewer feedback --- ...oundsForRange-for-macOS-Zoom-cursor-.patch | 109 ++++++------------ 1 file changed, 35 insertions(+), 74 deletions(-) diff --git a/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch b/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch index 5dc6684..0b546b2 100644 --- a/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch +++ b/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch @@ -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 -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.