patches: fix all review blockers (iteration 2)

Fixes from Opus maintainer review:
1. [BLOCKER] Zoom code completely removed from ALL intermediate patches
   (0005-0007 no longer have UAZoom/overlayZoom at any commit point)
2. [BLOCKER] Unified cursor rect ivar: lastCursorRect (was split
   between lastZoomCursorRect and lastAccessibilityCursorRect)
3. [HIGH] Child frame static vars moved to EmacsView ivars
   (childFrameLastCandidate/Buffer/Modiff — no cross-frame interference)
4. [HIGH] intern_c_string replaced with Qbefore_string/Qafter_string
5. [MEDIUM] Zoom fallback gated by zoomCursorUpdated flag (no double call)
This commit is contained in:
2026-02-28 22:39:57 +01:00
parent d9b4cbb87a
commit 9d2b1da729
9 changed files with 135 additions and 317 deletions

View File

@@ -1,34 +1,34 @@
From 402d2959cc569ebe740f07687c37283323fce314 Mon Sep 17 00:00:00 2001 From 085a2c40d1335819b7a0d43b67581cc7b547088f Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 22:08:55 +0100 Date: Sat, 28 Feb 2026 22:39:35 +0100
Subject: [PATCH] ns: integrate with macOS Zoom for cursor tracking Subject: [PATCH] ns: integrate with macOS Zoom for cursor tracking
Inform macOS Zoom of the text cursor position so the zoomed viewport Inform macOS Zoom of the text cursor position so the zoomed viewport
follows keyboard focus in Emacs. follows keyboard focus in Emacs.
* src/nsterm.h (EmacsView): Add lastZoomCursorRect ivar. * src/nsterm.h (EmacsView): Add lastCursorRect, zoomCursorUpdated.
* src/nsterm.m (ns_draw_window_cursor): Store cursor rect in * src/nsterm.m (ns_draw_window_cursor): Store cursor rect in
lastZoomCursorRect; call UAZoomChangeFocus with CG-space lastCursorRect; call UAZoomChangeFocus with CG-space coordinates
coordinates when UAZoomEnabled returns true. when UAZoomEnabled returns true. Set zoomCursorUpdated flag.
(ns_update_end): Call UAZoomChangeFocus as fallback after each (ns_update_end): Call UAZoomChangeFocus as fallback when cursor
redisplay cycle to ensure Zoom tracks cursor across window was not physically redrawn in this cycle (e.g., after C-x o window
switches (C-x o) where the physical cursor may not be redrawn. switch). Gated by zoomCursorUpdated to avoid double calls.
Coordinate conversion: EmacsView pixels (AppKit, flipped) -> Coordinate conversion: EmacsView pixels (AppKit, flipped) ->
NSWindow -> NSScreen -> CGRect with y-flip for CoreGraphics NSWindow -> NSScreen -> CGRect with y-flip for CoreGraphics
top-left origin. UAZoomChangeFocus is available since macOS 10.4 top-left origin. UAZoomEnabled returns false when Zoom is inactive,
(ApplicationServices umbrella framework). so overhead is a single function call per redisplay cycle.
Tested on macOS 14 with Zoom enabled: cursor tracking works across Tested on macOS 14 with Zoom enabled: cursor tracking works across
window splits, switches, and normal navigation. window splits, switches (C-x o), and normal navigation.
--- ---
etc/NEWS | 8 ++++++ etc/NEWS | 8 +++++++
src/nsterm.h | 4 +++ src/nsterm.h | 6 +++++
src/nsterm.m | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/nsterm.m | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 82 insertions(+) 3 files changed, 82 insertions(+)
diff --git a/etc/NEWS b/etc/NEWS diff --git a/etc/NEWS b/etc/NEWS
index ef36df5..e80e124 100644 index ef36df5..f10d17e 100644
--- a/etc/NEWS --- a/etc/NEWS
+++ b/etc/NEWS +++ b/etc/NEWS
@@ -82,6 +82,14 @@ other directory on your system. You can also invoke the @@ -82,6 +82,14 @@ other directory on your system. You can also invoke the
@@ -37,7 +37,7 @@ index ef36df5..e80e124 100644
++++ ++++
+** The macOS NS port now integrates with macOS Zoom. +** The macOS NS port now integrates with macOS Zoom.
+When macOS Zoom is enabled (System Settings -> Accessibility -> Zoom -> +When macOS Zoom is enabled (System Settings, Accessibility, Zoom,
+Follow keyboard focus), Emacs informs Zoom of the text cursor position +Follow keyboard focus), Emacs informs Zoom of the text cursor position
+after every cursor redraw via 'UAZoomChangeFocus'. The zoomed viewport +after every cursor redraw via 'UAZoomChangeFocus'. The zoomed viewport
+automatically tracks the insertion point across window splits and +automatically tracks the insertion point across window splits and
@@ -47,41 +47,42 @@ index ef36df5..e80e124 100644
** 'line-spacing' now supports specifying spacing above the line. ** 'line-spacing' now supports specifying spacing above the line.
Previously, only spacing below the line could be specified. The user Previously, only spacing below the line could be specified. The user
diff --git a/src/nsterm.h b/src/nsterm.h diff --git a/src/nsterm.h b/src/nsterm.h
index 7c1ee4c..f2755e4 100644 index 7c1ee4c..ea6e7ba 100644
--- a/src/nsterm.h --- a/src/nsterm.h
+++ b/src/nsterm.h +++ b/src/nsterm.h
@@ -484,6 +484,10 @@ enum ns_return_frame_mode @@ -484,6 +484,12 @@ enum ns_return_frame_mode
@public @public
struct frame *emacsframe; struct frame *emacsframe;
int scrollbarsNeedingUpdate; int scrollbarsNeedingUpdate;
+#ifdef NS_IMPL_COCOA +#ifdef NS_IMPL_COCOA
+ /* Cached cursor rect for macOS Zoom integration. */ + /* Cached cursor rect for macOS Zoom integration. Set by
+ NSRect lastZoomCursorRect; + ns_draw_window_cursor, used by ns_update_end fallback. */
+ NSRect lastCursorRect;
+ BOOL zoomCursorUpdated;
+#endif +#endif
NSRect ns_userRect; NSRect ns_userRect;
} }
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index 74e4ad5..eb1649b 100644 index 74e4ad5..cd721c8 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -1104,6 +1104,34 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen) @@ -1104,6 +1104,35 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen)
unblock_input (); unblock_input ();
ns_updating_frame = NULL; ns_updating_frame = NULL;
+ +
+#ifdef NS_IMPL_COCOA +#ifdef NS_IMPL_COCOA
+ /* Zoom fallback: ensure Zoom tracks the cursor after window + /* Zoom fallback: ensure Zoom tracks the cursor after window
+ switches (C-x o) and other operations where the physical cursor + switches (C-x o) where the physical cursor may not be redrawn.
+ may not be redrawn but the focused window changed. This uses + Only fires when ns_draw_window_cursor did NOT run in this cycle
+ lastZoomCursorRect which was set by ns_draw_window_cursor + (zoomCursorUpdated is NO). */
+ during the current or a previous redisplay cycle. */
+#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \ +#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \
+ && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 + && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
+ if (view && UAZoomEnabled () + if (view && !view->zoomCursorUpdated && UAZoomEnabled ()
+ && !NSIsEmptyRect (view->lastZoomCursorRect)) + && !NSIsEmptyRect (view->lastCursorRect))
+ { + {
+ NSRect r = view->lastZoomCursorRect; + NSRect r = view->lastCursorRect;
+ NSRect windowRect = [view convertRect:r toView:nil]; + NSRect windowRect = [view convertRect:r toView:nil];
+ NSRect screenRect + NSRect screenRect
+ = [[view window] convertRectToScreen:windowRect]; + = [[view window] convertRectToScreen:windowRect];
@@ -95,34 +96,33 @@ index 74e4ad5..eb1649b 100644
+ UAZoomChangeFocus (&cgRect, &cgRect, + UAZoomChangeFocus (&cgRect, &cgRect,
+ kUAZoomFocusTypeInsertionPoint); + kUAZoomFocusTypeInsertionPoint);
+ } + }
+#endif /* MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 */ + if (view)
+ view->zoomCursorUpdated = NO;
+#endif
+#endif /* NS_IMPL_COCOA */ +#endif /* NS_IMPL_COCOA */
} }
static void static void
@@ -3232,6 +3260,48 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors. @@ -3232,6 +3261,45 @@ 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));
+#ifdef NS_IMPL_COCOA +#ifdef NS_IMPL_COCOA
+ /* Accessibility: store cursor rect for macOS Zoom integration. + /* Zoom integration: inform macOS Zoom of the cursor position.
+ Zoom (System Settings -> Accessibility -> Zoom) tracks a focus + Zoom (System Settings -> Accessibility -> Zoom) tracks a focus
+ element to keep the zoomed viewport centered on the cursor. + element to keep the zoomed viewport centered on the cursor.
+ UAZoomChangeFocus() informs Zoom of the cursor position after
+ every physical cursor redraw.
+ +
+ The coordinate conversion chain: + Coordinate conversion:
+ EmacsView pixels (AppKit, flipped, origin at top-left) + EmacsView pixels (AppKit, flipped, top-left origin)
+ -> NSWindow coordinates (convertRect:toView:nil) + -> NSWindow (convertRect:toView:nil)
+ -> NSScreen coordinates (convertRectToScreen:) + -> NSScreen (convertRectToScreen:)
+ -> CGRect (NSRectToCGRect, same values) + -> CGRect with y-flip for CoreGraphics top-left origin. */
+ -> CG y-flip (CoreGraphics uses top-left origin on
+ the primary screen; AppKit uses bottom-left). */
+ { + {
+ EmacsView *view = FRAME_NS_VIEW (f); + EmacsView *view = FRAME_NS_VIEW (f);
+ if (view && on_p && active_p) + if (view && on_p && active_p)
+ { + {
+ view->lastZoomCursorRect = r; + view->lastCursorRect = r;
+ view->zoomCursorUpdated = YES;
+ +
+#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \ +#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \
+ && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 + && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
@@ -141,7 +141,7 @@ index 74e4ad5..eb1649b 100644
+ UAZoomChangeFocus (&cgRect, &cgRect, + UAZoomChangeFocus (&cgRect, &cgRect,
+ kUAZoomFocusTypeInsertionPoint); + kUAZoomFocusTypeInsertionPoint);
+ } + }
+#endif /* MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 */ +#endif
+ } + }
+ } + }
+#endif /* NS_IMPL_COCOA */ +#endif /* NS_IMPL_COCOA */

View File

@@ -1,4 +1,4 @@
From 84a96f2f60f2db10cc3d38bc68ccb7d2404b6ad5 Mon Sep 17 00:00:00 2001 From cd6ad89e786fc79f68bc0843b8122e088e8766ba Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100 Date: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 1/8] ns: add accessibility base classes and text extraction Subject: [PATCH 1/8] ns: add accessibility base classes and text extraction

View File

@@ -1,4 +1,4 @@
From 99ea6d5d2599c54da764ce94125e2da874de8e16 Mon Sep 17 00:00:00 2001 From 68ce438269f04570f21e92bd2c49f2ff83244cb8 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100 Date: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 2/8] ns: implement buffer accessibility element (core Subject: [PATCH 2/8] ns: implement buffer accessibility element (core

View File

@@ -1,4 +1,4 @@
From 1d346b87020909a27cb39f34110ba226dd8d92fe Mon Sep 17 00:00:00 2001 From f5ce42e931a3ed1668e6fb8260ef736442d8d2c9 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100 Date: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 3/8] ns: add buffer notification dispatch and mode-line Subject: [PATCH 3/8] ns: add buffer notification dispatch and mode-line

View File

@@ -1,4 +1,4 @@
From 9da1c8d18d2e70fc53f1f8e061a963c3adf8d960 Mon Sep 17 00:00:00 2001 From 8675f0f75a33e4a3621e0b1e15aab7eff2c81369 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100 Date: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 4/8] ns: add interactive span elements for Tab navigation Subject: [PATCH 4/8] ns: add interactive span elements for Tab navigation

View File

@@ -1,4 +1,4 @@
From 3304705947559e6ba83e462aed5a17f1c195c641 Mon Sep 17 00:00:00 2001 From 9e7fa018ef779610b2fb54c1ff951d0bf6bf7652 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100 Date: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 5/8] ns: integrate accessibility with EmacsView and redisplay Subject: [PATCH 5/8] ns: integrate accessibility with EmacsView and redisplay
@@ -23,8 +23,9 @@ block cursor, org-mode folded headings, indirect buffers.
Known limitations documented in patch 6 Texinfo node. Known limitations documented in patch 6 Texinfo node.
--- ---
etc/NEWS | 13 ++ etc/NEWS | 13 ++
src/nsterm.m | 398 ++++++++++++++++++++++++++++++++++++++++++++++++++- src/nsterm.h | 2 +-
2 files changed, 408 insertions(+), 3 deletions(-) src/nsterm.m | 373 ++++++++++++++++++++++++++++++++++++++++++++++++++-
3 files changed, 384 insertions(+), 4 deletions(-)
diff --git a/etc/NEWS b/etc/NEWS diff --git a/etc/NEWS b/etc/NEWS
index ef36df5..e76ee93 100644 index ef36df5..e76ee93 100644
@@ -50,8 +51,21 @@ index ef36df5..e76ee93 100644
--- ---
** Re-introduced dictation, lost in Emacs v30 (macOS). ** Re-introduced dictation, lost in Emacs v30 (macOS).
We lost macOS dictation in v30 when migrating to NSTextInputClient. We lost macOS dictation in v30 when migrating to NSTextInputClient.
diff --git a/src/nsterm.h b/src/nsterm.h
index 5298386..ec7b587 100644
--- a/src/nsterm.h
+++ b/src/nsterm.h
@@ -594,7 +594,7 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType)
BOOL accessibilityTreeValid;
BOOL accessibilityUpdating;
@public /* Accessed by ns_draw_phys_cursor (C function). */
- NSRect lastAccessibilityCursorRect;
+ NSRect lastCursorRect;
#endif
BOOL font_panel_active;
NSFont *font_panel_result;
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index c852929..b3bef4b 100644 index c852929..f0e8751 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -1105,6 +1105,11 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen) @@ -1105,6 +1105,11 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen)
@@ -66,51 +80,26 @@ index c852929..b3bef4b 100644
} }
static void static void
@@ -3233,6 +3238,43 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors. @@ -3233,6 +3238,18 @@ 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));
+#ifdef NS_IMPL_COCOA +#ifdef NS_IMPL_COCOA
+ /* Accessibility: store cursor rect for Zoom and bounds queries. + /* Accessibility: store cursor rect for VoiceOver bounds queries.
+ Skipped when ns-accessibility-enabled is nil to avoid overhead. + accessibilityBoundsForRange: / accessibilityFrameForRange:
+ VoiceOver notifications are handled solely by + use this as a fallback when no valid window/glyph data is
+ postAccessibilityUpdates (called from ns_update_end) + available. Skipped when ns-accessibility-enabled is nil. */
+ to avoid duplicate notifications and mid-redisplay fragility. */
+ { + {
+ EmacsView *view = FRAME_NS_VIEW (f); + EmacsView *view = FRAME_NS_VIEW (f);
+ if (view && on_p && active_p && ns_accessibility_enabled) + if (view && on_p && active_p && ns_accessibility_enabled)
+ { + view->lastCursorRect = r;
+ view->lastAccessibilityCursorRect = r;
+
+ /* Tell macOS Zoom where the cursor is. UAZoomChangeFocus()
+ expects top-left origin (CG coordinate space).
+ These APIs are available since macOS 10.4 (Universal Access
+ framework, linked via ApplicationServices umbrella). */
+#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \
+ && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
+ if (UAZoomEnabled ())
+ {
+ NSRect windowRect = [view convertRect:r toView:nil];
+ NSRect screenRect = [[view window] convertRectToScreen:windowRect];
+ CGRect cgRect = NSRectToCGRect (screenRect);
+
+ CGFloat primaryH
+ = [[[NSScreen screens] firstObject] frame].size.height;
+ cgRect.origin.y
+ = primaryH - cgRect.origin.y - cgRect.size.height;
+
+ UAZoomChangeFocus (&cgRect, &cgRect,
+ kUAZoomFocusTypeInsertionPoint);
+ }
+#endif /* MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 */
+ }
+ } + }
+#endif +#endif
+ +
ns_focus (f, NULL, 0); ns_focus (f, NULL, 0);
NSGraphicsContext *ctx = [NSGraphicsContext currentContext]; NSGraphicsContext *ctx = [NSGraphicsContext currentContext];
@@ -7281,7 +7323,6 @@ - (id)accessibilityTopLevelUIElement @@ -7281,7 +7298,6 @@ - (id)accessibilityTopLevelUIElement
@@ -118,7 +107,7 @@ index c852929..b3bef4b 100644
static BOOL static BOOL
ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point,
ptrdiff_t *out_start, ptrdiff_t *out_start,
@@ -8375,7 +8416,6 @@ - (NSRect)accessibilityFrame @@ -8375,7 +8391,6 @@ - (NSRect)accessibilityFrame
@end @end
@@ -126,7 +115,7 @@ index c852929..b3bef4b 100644
/* =================================================================== /* ===================================================================
EmacsAccessibilityBuffer (Notifications) — AX event dispatch EmacsAccessibilityBuffer (Notifications) — AX event dispatch
@@ -8920,7 +8960,6 @@ - (NSRect)accessibilityFrame @@ -8920,7 +8935,6 @@ - (NSRect)accessibilityFrame
@end @end
@@ -134,7 +123,7 @@ index c852929..b3bef4b 100644
/* =================================================================== /* ===================================================================
EmacsAccessibilityInteractiveSpan — helpers and implementation EmacsAccessibilityInteractiveSpan — helpers and implementation
=================================================================== */ =================================================================== */
@@ -9250,6 +9289,7 @@ - (void)dealloc @@ -9250,6 +9264,7 @@ - (void)dealloc
[layer release]; [layer release];
#endif #endif
@@ -142,7 +131,7 @@ index c852929..b3bef4b 100644
[[self menu] release]; [[self menu] release];
[super dealloc]; [super dealloc];
} }
@@ -10598,6 +10638,32 @@ - (void)windowDidBecomeKey /* for direct calls */ @@ -10598,6 +10613,32 @@ - (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
@@ -175,7 +164,7 @@ index c852929..b3bef4b 100644
} }
@@ -11835,6 +11901,332 @@ - (int) fullscreenState @@ -11835,6 +11876,332 @@ - (int) fullscreenState
return fs_state; return fs_state;
} }
@@ -436,7 +425,7 @@ index c852929..b3bef4b 100644
+ return bufRect; + return bufRect;
+ } + }
+ +
+ NSRect viewRect = lastAccessibilityCursorRect; + NSRect viewRect = lastCursorRect;
+ +
+ if (viewRect.size.width < 1) + if (viewRect.size.width < 1)
+ viewRect.size.width = 1; + viewRect.size.width = 1;

View File

@@ -1,4 +1,4 @@
From c08f128e25cb645a722c9772e9e4f3a505655d26 Mon Sep 17 00:00:00 2001 From 683d7497cc3414a231b44363dd28d2748780c38a Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100 Date: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 6/8] doc: add VoiceOver accessibility section to macOS Subject: [PATCH 6/8] doc: add VoiceOver accessibility section to macOS

View File

@@ -1,4 +1,4 @@
From 2222c14590612835529f88c0062fefeedc8f7187 Mon Sep 17 00:00:00 2001 From 5143187e2fa42fa8f5c28e4f39be08ca981692c9 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 14:46:25 +0100 Date: Sat, 28 Feb 2026 14:46:25 +0100
Subject: [PATCH 7/8] ns: announce overlay completion candidates for VoiceOver Subject: [PATCH 7/8] ns: announce overlay completion candidates for VoiceOver
@@ -44,12 +44,12 @@ Key implementation details:
(EmacsAccessibilityBuffer postAccessibilityNotificationsForFrame:): (EmacsAccessibilityBuffer postAccessibilityNotificationsForFrame:):
Independent overlay branch, BUF_CHARS_MODIFF gating, candidate Independent overlay branch, BUF_CHARS_MODIFF gating, candidate
--- ---
src/nsterm.h | 3 + src/nsterm.h | 1 +
src/nsterm.m | 359 ++++++++++++++++++++++++++++++++++++++++++++++----- src/nsterm.m | 312 +++++++++++++++++++++++++++++++++++++++++++++------
2 files changed, 327 insertions(+), 35 deletions(-) 2 files changed, 279 insertions(+), 34 deletions(-)
diff --git a/src/nsterm.h b/src/nsterm.h diff --git a/src/nsterm.h b/src/nsterm.h
index 5298386..a007925 100644 index ec7b587..19a7e7a 100644
--- a/src/nsterm.h --- a/src/nsterm.h
+++ b/src/nsterm.h +++ b/src/nsterm.h
@@ -509,6 +509,7 @@ typedef struct ns_ax_visible_run @@ -509,6 +509,7 @@ typedef struct ns_ax_visible_run
@@ -60,34 +60,11 @@ index 5298386..a007925 100644
@property (nonatomic, assign) ptrdiff_t cachedPoint; @property (nonatomic, assign) ptrdiff_t cachedPoint;
@property (nonatomic, assign) BOOL cachedMarkActive; @property (nonatomic, assign) BOOL cachedMarkActive;
@property (nonatomic, copy) NSString *cachedCompletionAnnouncement; @property (nonatomic, copy) NSString *cachedCompletionAnnouncement;
@@ -595,6 +596,8 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType)
BOOL accessibilityUpdating;
@public /* Accessed by ns_draw_phys_cursor (C function). */
NSRect lastAccessibilityCursorRect;
+ BOOL overlayZoomActive;
+ NSRect overlayZoomRect;
#endif
BOOL font_panel_active;
NSFont *font_panel_result;
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index b3bef4b..7025e6e 100644 index f0e8751..3d72b5d 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -3258,7 +3258,12 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors. @@ -6884,11 +6884,154 @@ Accessibility virtual elements (macOS / Cocoa only)
&& MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
if (UAZoomEnabled ())
{
- NSRect windowRect = [view convertRect:r toView:nil];
+ /* When overlay completion is active (e.g. Vertico),
+ focus Zoom on the selected candidate row instead
+ of the text cursor. */
+ NSRect zoomSrc = view->overlayZoomActive
+ ? view->overlayZoomRect : r;
+ NSRect windowRect = [view convertRect:zoomSrc toView:nil];
NSRect screenRect = [[view window] convertRectToScreen:windowRect];
CGRect cgRect = NSRectToCGRect (screenRect);
@@ -6909,11 +6914,154 @@ Accessibility virtual elements (macOS / Cocoa only)
/* ---- Helper: extract buffer text for accessibility ---- */ /* ---- Helper: extract buffer text for accessibility ---- */
@@ -148,8 +125,8 @@ index b3bef4b..7025e6e 100644
+ { + {
+ Lisp_Object ov = XCAR (tail); + Lisp_Object ov = XCAR (tail);
+ Lisp_Object strings[2]; + Lisp_Object strings[2];
+ strings[0] = Foverlay_get (ov, intern_c_string ("before-string")); + strings[0] = Foverlay_get (ov, Qbefore_string);
+ strings[1] = Foverlay_get (ov, intern_c_string ("after-string")); + strings[1] = Foverlay_get (ov, Qafter_string);
+ +
+ for (int s = 0; s < 2; s++) + for (int s = 0; s < 2; s++)
+ { + {
@@ -243,7 +220,7 @@ index b3bef4b..7025e6e 100644
static NSString * static NSString *
ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start, ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start,
ns_ax_visible_run **out_runs, NSUInteger *out_nruns) ns_ax_visible_run **out_runs, NSUInteger *out_nruns)
@@ -6984,7 +7132,7 @@ Accessibility virtual elements (macOS / Cocoa only) @@ -6959,7 +7102,7 @@ Accessibility virtual elements (macOS / Cocoa only)
/* Extract this visible run's text. Use /* Extract this visible run's text. Use
Fbuffer_substring_no_properties which correctly handles the Fbuffer_substring_no_properties which correctly handles the
@@ -252,7 +229,7 @@ index b3bef4b..7025e6e 100644
include garbage bytes when the run spans the gap position. */ include garbage bytes when the run spans the gap position. */
Lisp_Object lstr = Fbuffer_substring_no_properties ( Lisp_Object lstr = Fbuffer_substring_no_properties (
make_fixnum (pos), make_fixnum (run_end)); make_fixnum (pos), make_fixnum (run_end));
@@ -7065,7 +7213,7 @@ Mode lines using icon fonts (e.g. doom-modeline with nerd-font) @@ -7040,7 +7183,7 @@ Mode lines using icon fonts (e.g. doom-modeline with nerd-font)
return NSZeroRect; return NSZeroRect;
/* charpos_start and charpos_len are already in buffer charpos /* charpos_start and charpos_len are already in buffer charpos
@@ -261,7 +238,7 @@ index b3bef4b..7025e6e 100644
charposForAccessibilityIndex which handles invisible text. */ charposForAccessibilityIndex which handles invisible text. */
ptrdiff_t cp_start = charpos_start; ptrdiff_t cp_start = charpos_start;
ptrdiff_t cp_end = cp_start + charpos_len; ptrdiff_t cp_end = cp_start + charpos_len;
@@ -7544,6 +7692,7 @@ @implementation EmacsAccessibilityBuffer @@ -7519,6 +7662,7 @@ @implementation EmacsAccessibilityBuffer
@synthesize cachedOverlayModiff; @synthesize cachedOverlayModiff;
@synthesize cachedTextStart; @synthesize cachedTextStart;
@synthesize cachedModiff; @synthesize cachedModiff;
@@ -269,7 +246,7 @@ index b3bef4b..7025e6e 100644
@synthesize cachedPoint; @synthesize cachedPoint;
@synthesize cachedMarkActive; @synthesize cachedMarkActive;
@synthesize cachedCompletionAnnouncement; @synthesize cachedCompletionAnnouncement;
@@ -7641,7 +7790,7 @@ - (void)ensureTextCache @@ -7616,7 +7760,7 @@ - (void)ensureTextCache
NSTRACE ("EmacsAccessibilityBuffer ensureTextCache"); NSTRACE ("EmacsAccessibilityBuffer ensureTextCache");
/* This method is only called from the main thread (AX getters /* This method is only called from the main thread (AX getters
dispatch_sync to main first). Reads of cachedText/cachedTextModiff dispatch_sync to main first). Reads of cachedText/cachedTextModiff
@@ -278,7 +255,7 @@ index b3bef4b..7025e6e 100644
write section at the end needs synchronization to protect write section at the end needs synchronization to protect
against concurrent reads from AX server thread. */ against concurrent reads from AX server thread. */
eassert ([NSThread isMainThread]); eassert ([NSThread isMainThread]);
@@ -7654,16 +7803,15 @@ - (void)ensureTextCache @@ -7629,16 +7773,15 @@ - (void)ensureTextCache
return; return;
ptrdiff_t modiff = BUF_MODIFF (b); ptrdiff_t modiff = BUF_MODIFF (b);
@@ -301,7 +278,7 @@ index b3bef4b..7025e6e 100644
&& cachedTextStart == BUF_BEGV (b) && cachedTextStart == BUF_BEGV (b)
&& pt >= cachedTextStart && pt >= cachedTextStart
&& (textLen == 0 && (textLen == 0
@@ -7680,7 +7828,6 @@ - (void)ensureTextCache @@ -7655,7 +7798,6 @@ - (void)ensureTextCache
[cachedText release]; [cachedText release];
cachedText = [text retain]; cachedText = [text retain];
cachedTextModiff = modiff; cachedTextModiff = modiff;
@@ -309,7 +286,7 @@ index b3bef4b..7025e6e 100644
cachedTextStart = start; cachedTextStart = start;
if (visibleRuns) if (visibleRuns)
@@ -7745,7 +7892,7 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos @@ -7720,7 +7862,7 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos
/* Binary search: runs are sorted by charpos (ascending). Find the /* Binary search: runs are sorted by charpos (ascending). Find the
run whose [charpos, charpos+length) range contains the target, run whose [charpos, charpos+length) range contains the target,
or the nearest run after an invisible gap. O(log n) instead of or the nearest run after an invisible gap. O(log n) instead of
@@ -318,7 +295,7 @@ index b3bef4b..7025e6e 100644
NSUInteger lo = 0, hi = visibleRunCount; NSUInteger lo = 0, hi = visibleRunCount;
while (lo < hi) while (lo < hi)
{ {
@@ -7758,7 +7905,7 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos @@ -7733,7 +7875,7 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos
else else
{ {
/* Found: charpos is inside this run. Compute UTF-16 delta /* Found: charpos is inside this run. Compute UTF-16 delta
@@ -327,7 +304,7 @@ index b3bef4b..7025e6e 100644
NSUInteger chars_in = (NSUInteger)(charpos - r->charpos); NSUInteger chars_in = (NSUInteger)(charpos - r->charpos);
if (chars_in == 0 || !cachedText) if (chars_in == 0 || !cachedText)
return r->ax_start; return r->ax_start;
@@ -7783,10 +7930,10 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos @@ -7758,10 +7900,10 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos
/* Convert accessibility string index to buffer charpos. /* Convert accessibility string index to buffer charpos.
Safe to call from any thread: uses only cachedText (NSString) and Safe to call from any thread: uses only cachedText (NSString) and
@@ -340,7 +317,7 @@ index b3bef4b..7025e6e 100644
@synchronized (self) @synchronized (self)
{ {
if (visibleRunCount == 0) if (visibleRunCount == 0)
@@ -7820,7 +7967,7 @@ - (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx @@ -7795,7 +7937,7 @@ - (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx
return cp; return cp;
} }
} }
@@ -349,7 +326,7 @@ index b3bef4b..7025e6e 100644
if (lo > 0) if (lo > 0)
{ {
ns_ax_visible_run *last = &visibleRuns[visibleRunCount - 1]; ns_ax_visible_run *last = &visibleRuns[visibleRunCount - 1];
@@ -7842,7 +7989,7 @@ - (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx @@ -7817,7 +7959,7 @@ - (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx
deadlocking the AX server thread. This is prevented by: deadlocking the AX server thread. This is prevented by:
1. validWindow checks WINDOW_LIVE_P and BUFFERP before every 1. validWindow checks WINDOW_LIVE_P and BUFFERP before every
@@ -358,7 +335,7 @@ index b3bef4b..7025e6e 100644
2. All dispatch_sync blocks run on the main thread where no 2. All dispatch_sync blocks run on the main thread where no
concurrent Lisp code can modify state between checks. concurrent Lisp code can modify state between checks.
3. block_input prevents timer events and process output from 3. block_input prevents timer events and process output from
@@ -8196,6 +8343,50 @@ - (NSInteger)accessibilityInsertionPointLineNumber @@ -8171,6 +8313,50 @@ - (NSInteger)accessibilityInsertionPointLineNumber
return [self lineForAXIndex:point_idx]; return [self lineForAXIndex:point_idx];
} }
@@ -409,7 +386,7 @@ index b3bef4b..7025e6e 100644
- (NSRange)accessibilityRangeForLine:(NSInteger)line - (NSRange)accessibilityRangeForLine:(NSInteger)line
{ {
if (![NSThread isMainThread]) if (![NSThread isMainThread])
@@ -8417,7 +8608,7 @@ - (NSRect)accessibilityFrame @@ -8392,7 +8578,7 @@ - (NSRect)accessibilityFrame
/* =================================================================== /* ===================================================================
@@ -418,7 +395,7 @@ index b3bef4b..7025e6e 100644
These methods notify VoiceOver of text and selection changes. These methods notify VoiceOver of text and selection changes.
Called from the redisplay cycle (postAccessibilityUpdates). Called from the redisplay cycle (postAccessibilityUpdates).
@@ -8432,7 +8623,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point @@ -8407,7 +8593,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point
if (point > self.cachedPoint if (point > self.cachedPoint
&& point - self.cachedPoint == 1) && point - self.cachedPoint == 1)
{ {
@@ -427,7 +404,7 @@ index b3bef4b..7025e6e 100644
[self invalidateTextCache]; [self invalidateTextCache];
[self ensureTextCache]; [self ensureTextCache];
if (cachedText) if (cachedText)
@@ -8451,7 +8642,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point @@ -8426,7 +8612,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point
/* Update cachedPoint here so the selection-move branch does NOT /* Update cachedPoint here so the selection-move branch does NOT
fire for point changes caused by edits. WebKit and Chromium fire for point changes caused by edits. WebKit and Chromium
never send both ValueChanged and SelectedTextChanged for the never send both ValueChanged and SelectedTextChanged for the
@@ -436,7 +413,7 @@ index b3bef4b..7025e6e 100644
self.cachedPoint = point; self.cachedPoint = point;
NSDictionary *change = @{ NSDictionary *change = @{
@@ -8784,14 +8975,112 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f @@ -8759,14 +8945,72 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f
BOOL markActive = !NILP (BVAR (b, mark_active)); BOOL markActive = !NILP (BVAR (b, mark_active));
/* --- Text changed (edit) --- */ /* --- Text changed (edit) --- */
@@ -454,7 +431,6 @@ index b3bef4b..7025e6e 100644
+ if (chars_modiff != self.cachedCharsModiff) + if (chars_modiff != self.cachedCharsModiff)
+ { + {
+ self.cachedCharsModiff = chars_modiff; + self.cachedCharsModiff = chars_modiff;
+ self.emacsView->overlayZoomActive = NO;
+ [self postTextChangedNotification:point]; + [self postTextChangedNotification:point];
+ } + }
+ } + }
@@ -502,46 +478,7 @@ index b3bef4b..7025e6e 100644
+ NSAccessibilityAnnouncementRequestedNotification, + NSAccessibilityAnnouncementRequestedNotification,
+ annInfo); + annInfo);
+ +
+ /* --- Zoom tracking for overlay candidates ---
+ Store the candidate row rect so draw_window_cursor
+ focuses Zoom there instead of on the text cursor.
+ Cleared when the user types (chars_modiff change).
+
+ Use default line height to compute the Y offset:
+ row 0 is the input line, overlay candidates start
+ from row 1. This avoids fragile glyph matrix row
+ index mapping which can be off when group titles
+ or wrapped lines shift row numbering. */
+ if (selected_line >= 0)
+ {
+ struct window *w2 = [self validWindow];
+ if (w2)
+ {
+ EmacsView *view = self.emacsView;
+ struct frame *f2 = XFRAME (w2->frame);
+ int line_h = FRAME_LINE_HEIGHT (f2);
+ int y_off = (selected_line + 1) * line_h;
+
+ if (y_off < w2->pixel_height)
+ {
+ view->overlayZoomRect = NSMakeRect (
+ WINDOW_TEXT_TO_FRAME_PIXEL_X (w2, 0),
+ WINDOW_TO_FRAME_PIXEL_Y (w2, y_off),
+ FRAME_COLUMN_WIDTH (f2),
+ line_h);
+ view->overlayZoomActive = YES;
+ }
+ }
+ }
+ } + }
+ }
+ else
+ {
+ /* No selected candidate --- overlay completion ended
+ (minibuffer exit, C-g, etc.) or overlay has no
+ recognizable selection face. Return Zoom to the
+ text cursor. */
+ self.emacsView->overlayZoomActive = NO;
+ } + }
} }
@@ -551,7 +488,7 @@ index b3bef4b..7025e6e 100644
per the WebKit/Chromium pattern. */ per the WebKit/Chromium pattern. */
else if (point != self.cachedPoint || markActive != self.cachedMarkActive) else if (point != self.cachedPoint || markActive != self.cachedMarkActive)
{ {
@@ -8961,7 +9250,7 @@ - (NSRect)accessibilityFrame @@ -8936,7 +9180,7 @@ - (NSRect)accessibilityFrame
/* =================================================================== /* ===================================================================
@@ -560,7 +497,7 @@ index b3bef4b..7025e6e 100644
=================================================================== */ =================================================================== */
/* Scan visible range of window W for interactive spans. /* Scan visible range of window W for interactive spans.
@@ -9152,7 +9441,7 @@ - (NSRect) accessibilityFrame @@ -9127,7 +9371,7 @@ - (NSRect) accessibilityFrame
- (BOOL) isAccessibilityFocused - (BOOL) isAccessibilityFocused
{ {
/* Read the cached point stored by EmacsAccessibilityBuffer on the main /* Read the cached point stored by EmacsAccessibilityBuffer on the main
@@ -569,7 +506,7 @@ index b3bef4b..7025e6e 100644
EmacsAccessibilityBuffer *pb = self.parentBuffer; EmacsAccessibilityBuffer *pb = self.parentBuffer;
if (!pb) if (!pb)
return NO; return NO;
@@ -9169,7 +9458,7 @@ - (void) setAccessibilityFocused: (BOOL) focused @@ -9144,7 +9388,7 @@ - (void) setAccessibilityFocused: (BOOL) focused
dispatch_async (dispatch_get_main_queue (), ^{ dispatch_async (dispatch_get_main_queue (), ^{
/* lwin is a Lisp_Object captured by value. This is GC-safe /* lwin is a Lisp_Object captured by value. This is GC-safe
because Lisp_Objects are tagged integers/pointers that because Lisp_Objects are tagged integers/pointers that
@@ -578,7 +515,7 @@ index b3bef4b..7025e6e 100644
Emacs. The WINDOW_LIVE_P check below guards against the Emacs. The WINDOW_LIVE_P check below guards against the
window being deleted between capture and execution. */ window being deleted between capture and execution. */
if (!WINDOWP (lwin) || NILP (Fwindow_live_p (lwin))) if (!WINDOWP (lwin) || NILP (Fwindow_live_p (lwin)))
@@ -9195,7 +9484,7 @@ - (void) setAccessibilityFocused: (BOOL) focused @@ -9170,7 +9414,7 @@ - (void) setAccessibilityFocused: (BOOL) focused
@end @end
@@ -587,7 +524,7 @@ index b3bef4b..7025e6e 100644
Methods are kept here (same .m file) so they access the ivars Methods are kept here (same .m file) so they access the ivars
declared in the @interface ivar block. */ declared in the @interface ivar block. */
@implementation EmacsAccessibilityBuffer (InteractiveSpans) @implementation EmacsAccessibilityBuffer (InteractiveSpans)
@@ -10515,13 +10804,13 @@ - (NSSize)windowWillResize: (NSWindow *)sender toSize: (NSSize)frameSize @@ -10490,13 +10734,13 @@ - (NSSize)windowWillResize: (NSWindow *)sender toSize: (NSSize)frameSize
if (old_title == 0) if (old_title == 0)
{ {
char *t = strdup ([[[self window] title] UTF8String]); char *t = strdup ([[[self window] title] UTF8String]);
@@ -603,7 +540,7 @@ index b3bef4b..7025e6e 100644
[window setTitle: [NSString stringWithUTF8String: size_title]]; [window setTitle: [NSString stringWithUTF8String: size_title]];
[window display]; [window display];
xfree (size_title); xfree (size_title);
@@ -11917,7 +12206,7 @@ - (int) fullscreenState @@ -11892,7 +12136,7 @@ - (int) fullscreenState
if (WINDOW_LEAF_P (w)) if (WINDOW_LEAF_P (w))
{ {
@@ -612,7 +549,7 @@ index b3bef4b..7025e6e 100644
EmacsAccessibilityBuffer *elem EmacsAccessibilityBuffer *elem
= [existing objectForKey:[NSValue valueWithPointer:w]]; = [existing objectForKey:[NSValue valueWithPointer:w]];
if (!elem) if (!elem)
@@ -11951,7 +12240,7 @@ - (int) fullscreenState @@ -11926,7 +12170,7 @@ - (int) fullscreenState
} }
else else
{ {
@@ -621,7 +558,7 @@ index b3bef4b..7025e6e 100644
Lisp_Object child = w->contents; Lisp_Object child = w->contents;
while (!NILP (child)) while (!NILP (child))
{ {
@@ -12063,7 +12352,7 @@ - (void)postAccessibilityUpdates @@ -12038,7 +12282,7 @@ - (void)postAccessibilityUpdates
accessibilityUpdating = YES; accessibilityUpdating = YES;
/* Detect window tree change (split, delete, new buffer). Compare /* Detect window tree change (split, delete, new buffer). Compare
@@ -630,7 +567,7 @@ index b3bef4b..7025e6e 100644
Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe); Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe);
if (!EQ (curRoot, lastRootWindow)) if (!EQ (curRoot, lastRootWindow))
{ {
@@ -12072,12 +12361,12 @@ - (void)postAccessibilityUpdates @@ -12047,12 +12291,12 @@ - (void)postAccessibilityUpdates
} }
/* If tree is stale, rebuild FIRST so we don't iterate freed /* If tree is stale, rebuild FIRST so we don't iterate freed

View File

@@ -1,4 +1,4 @@
From 992bd78124b769fdcb4c1e3231afd13349e066c9 Mon Sep 17 00:00:00 2001 From f842c3813d3b9fd106d668fa872b1cb3b187e9cf Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 16:01:29 +0100 Date: Sat, 28 Feb 2026 16:01:29 +0100
Subject: [PATCH 8/8] ns: announce child frame completion candidates for Subject: [PATCH 8/8] ns: announce child frame completion candidates for
@@ -40,11 +40,11 @@ childFrameCompletionActive flag.
(EmacsView postAccessibilityUpdates): Dispatch to child frame handler, (EmacsView postAccessibilityUpdates): Dispatch to child frame handler,
refocus parent buffer element when child frame closes. refocus parent buffer element when child frame closes.
--- ---
doc/emacs/macos.texi | 6 - doc/emacs/macos.texi | 6 --
etc/NEWS | 4 +- etc/NEWS | 4 +-
src/nsterm.h | 4 +- src/nsterm.h | 5 +
src/nsterm.m | 310 ++++++++++++++++++++++++++++++++----------- src/nsterm.m | 227 ++++++++++++++++++++++++++++++++++++++++++-
4 files changed, 238 insertions(+), 86 deletions(-) 4 files changed, 233 insertions(+), 9 deletions(-)
diff --git a/doc/emacs/macos.texi b/doc/emacs/macos.texi diff --git a/doc/emacs/macos.texi b/doc/emacs/macos.texi
index 4825cf9..97777e2 100644 index 4825cf9..97777e2 100644
@@ -86,20 +86,21 @@ index e76ee93..c3e0b40 100644
for the *Completions* buffer. The implementation uses a virtual for the *Completions* buffer. The implementation uses a virtual
accessibility tree with per-window elements, hybrid SelectedTextChanged accessibility tree with per-window elements, hybrid SelectedTextChanged
diff --git a/src/nsterm.h b/src/nsterm.h diff --git a/src/nsterm.h b/src/nsterm.h
index a007925..80bc8ff 100644 index 19a7e7a..49e8f00 100644
--- a/src/nsterm.h --- a/src/nsterm.h
+++ b/src/nsterm.h +++ b/src/nsterm.h
@@ -596,8 +596,7 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType) @@ -596,6 +596,10 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType)
BOOL accessibilityUpdating; BOOL accessibilityUpdating;
@public /* Accessed by ns_draw_phys_cursor (C function). */ @public /* Accessed by ns_draw_phys_cursor (C function). */
NSRect lastAccessibilityCursorRect; NSRect lastCursorRect;
- BOOL overlayZoomActive;
- NSRect overlayZoomRect;
+ BOOL childFrameCompletionActive; + BOOL childFrameCompletionActive;
+ char *childFrameLastCandidate;
+ struct buffer *childFrameLastBuffer;
+ EMACS_INT childFrameLastModiff;
#endif #endif
BOOL font_panel_active; BOOL font_panel_active;
NSFont *font_panel_result; NSFont *font_panel_result;
@@ -661,6 +660,7 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType) @@ -659,6 +663,7 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType)
- (void)rebuildAccessibilityTree; - (void)rebuildAccessibilityTree;
- (void)invalidateAccessibilityTree; - (void)invalidateAccessibilityTree;
- (void)postAccessibilityUpdates; - (void)postAccessibilityUpdates;
@@ -108,60 +109,10 @@ index a007925..80bc8ff 100644
@end @end
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index 7025e6e..d5d33b0 100644 index 3d72b5d..fefe2a7 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -3239,44 +3239,14 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors. @@ -7028,6 +7028,112 @@ visual line index for Zoom (skip whitespace-only lines
r = NSIntersectionRect (r, ns_row_rect (w, glyph_row, TEXT_AREA));
#ifdef NS_IMPL_COCOA
- /* Accessibility: store cursor rect for Zoom and bounds queries.
- Skipped when ns-accessibility-enabled is nil to avoid overhead.
- VoiceOver notifications are handled solely by
- postAccessibilityUpdates (called from ns_update_end)
- to avoid duplicate notifications and mid-redisplay fragility. */
+ /* Accessibility: store cursor rect for VoiceOver bounds queries.
+ accessibilityBoundsForRange: / accessibilityFrameForRange:
+ use this as a fallback when no valid window/glyph data is
+ available. Skipped when ns-accessibility-enabled is nil. */
{
EmacsView *view = FRAME_NS_VIEW (f);
if (view && on_p && active_p && ns_accessibility_enabled)
- {
- view->lastAccessibilityCursorRect = r;
-
- /* Tell macOS Zoom where the cursor is. UAZoomChangeFocus()
- expects top-left origin (CG coordinate space).
- These APIs are available since macOS 10.4 (Universal Access
- framework, linked via ApplicationServices umbrella). */
-#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \
- && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
- if (UAZoomEnabled ())
- {
- /* When overlay completion is active (e.g. Vertico),
- focus Zoom on the selected candidate row instead
- of the text cursor. */
- NSRect zoomSrc = view->overlayZoomActive
- ? view->overlayZoomRect : r;
- NSRect windowRect = [view convertRect:zoomSrc toView:nil];
- NSRect screenRect = [[view window] convertRectToScreen:windowRect];
- CGRect cgRect = NSRectToCGRect (screenRect);
-
- CGFloat primaryH
- = [[[NSScreen screens] firstObject] frame].size.height;
- cgRect.origin.y
- = primaryH - cgRect.origin.y - cgRect.size.height;
-
- UAZoomChangeFocus (&cgRect, &cgRect,
- kUAZoomFocusTypeInsertionPoint);
- }
-#endif /* MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 */
- }
+ view->lastAccessibilityCursorRect = r;
}
#endif
@@ -7058,6 +7028,112 @@ visual line index for Zoom (skip whitespace-only lines
return nil; return nil;
} }
@@ -274,63 +225,7 @@ index 7025e6e..d5d33b0 100644
/* Build accessibility text for window W, skipping invisible text. /* Build accessibility text for window W, skipping invisible text.
Populates *OUT_START with the buffer start charpos. Populates *OUT_START with the buffer start charpos.
Populates *OUT_RUNS with an array of visible runs and *OUT_NRUNS Populates *OUT_RUNS with an array of visible runs and *OUT_NRUNS
@@ -8988,7 +9064,6 @@ Text property changes (e.g. face updates from @@ -12266,6 +12372,77 @@ - (id)accessibilityFocusedUIElement
if (chars_modiff != self.cachedCharsModiff)
{
self.cachedCharsModiff = chars_modiff;
- self.emacsView->overlayZoomActive = NO;
[self postTextChangedNotification:point];
}
}
@@ -9036,47 +9111,8 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
NSAccessibilityAnnouncementRequestedNotification,
annInfo);
- /* --- Zoom tracking for overlay candidates ---
- Store the candidate row rect so draw_window_cursor
- focuses Zoom there instead of on the text cursor.
- Cleared when the user types (chars_modiff change).
-
- Use default line height to compute the Y offset:
- row 0 is the input line, overlay candidates start
- from row 1. This avoids fragile glyph matrix row
- index mapping which can be off when group titles
- or wrapped lines shift row numbering. */
- if (selected_line >= 0)
- {
- struct window *w2 = [self validWindow];
- if (w2)
- {
- EmacsView *view = self.emacsView;
- struct frame *f2 = XFRAME (w2->frame);
- int line_h = FRAME_LINE_HEIGHT (f2);
- int y_off = (selected_line + 1) * line_h;
-
- if (y_off < w2->pixel_height)
- {
- view->overlayZoomRect = NSMakeRect (
- WINDOW_TEXT_TO_FRAME_PIXEL_X (w2, 0),
- WINDOW_TO_FRAME_PIXEL_Y (w2, y_off),
- FRAME_COLUMN_WIDTH (f2),
- line_h);
- view->overlayZoomActive = YES;
- }
- }
- }
}
}
- else
- {
- /* No selected candidate --- overlay completion ended
- (minibuffer exit, C-g, etc.) or overlay has no
- recognizable selection face. Return Zoom to the
- text cursor. */
- self.emacsView->overlayZoomActive = NO;
- }
}
/* --- Cursor moved or selection changed ---
@@ -12336,6 +12372,80 @@ - (id)accessibilityFocusedUIElement
The existing elements carry cached state (modiff, point) from the The existing elements carry cached state (modiff, point) from the
previous redisplay cycle. Rebuilding first would create fresh previous redisplay cycle. Rebuilding first would create fresh
elements with current values, making change detection impossible. */ elements with current values, making change detection impossible. */
@@ -341,9 +236,6 @@ index 7025e6e..d5d33b0 100644
+ in the minibuffer. */ + in the minibuffer. */
+- (void)announceChildFrameCompletion +- (void)announceChildFrameCompletion
+{ +{
+ static char *lastCandidate;
+ static struct buffer *lastBuffer;
+ static EMACS_INT lastModiff;
+ +
+ /* Validate frame state --- child frames may be partially + /* Validate frame state --- child frames may be partially
+ initialized during creation. */ + initialized during creation. */
@@ -359,10 +251,10 @@ index 7025e6e..d5d33b0 100644
+ also guards against re-entrance: if Lisp calls below + also guards against re-entrance: if Lisp calls below
+ trigger redisplay, the modiff check short-circuits. */ + trigger redisplay, the modiff check short-circuits. */
+ EMACS_INT modiff = BUF_MODIFF (b); + EMACS_INT modiff = BUF_MODIFF (b);
+ if (b == lastBuffer && modiff == lastModiff) + if (b == childFrameLastBuffer && modiff == childFrameLastModiff)
+ return; + return;
+ lastBuffer = b; + childFrameLastBuffer = b;
+ lastModiff = modiff; + childFrameLastModiff = modiff;
+ +
+ /* Skip buffers larger than a typical completion popup. + /* Skip buffers larger than a typical completion popup.
+ This avoids scanning eldoc, which-key, or other child + This avoids scanning eldoc, which-key, or other child
@@ -379,10 +271,10 @@ index 7025e6e..d5d33b0 100644
+ +
+ /* Deduplicate --- avoid re-announcing the same candidate. */ + /* Deduplicate --- avoid re-announcing the same candidate. */
+ const char *cstr = [candidate UTF8String]; + const char *cstr = [candidate UTF8String];
+ if (lastCandidate && strcmp (cstr, lastCandidate) == 0) + if (childFrameLastCandidate && strcmp (cstr, childFrameLastCandidate) == 0)
+ return; + return;
+ xfree (lastCandidate); + xfree (childFrameLastCandidate);
+ lastCandidate = xstrdup (cstr); + childFrameLastCandidate = xstrdup (cstr);
+ +
+ NSDictionary *annInfo = @{ + NSDictionary *annInfo = @{
+ NSAccessibilityAnnouncementKey: candidate, + NSAccessibilityAnnouncementKey: candidate,
@@ -411,7 +303,7 @@ index 7025e6e..d5d33b0 100644
- (void)postAccessibilityUpdates - (void)postAccessibilityUpdates
{ {
NSTRACE ("[EmacsView postAccessibilityUpdates]"); NSTRACE ("[EmacsView postAccessibilityUpdates]");
@@ -12346,11 +12456,59 @@ - (void)postAccessibilityUpdates @@ -12276,11 +12453,59 @@ - (void)postAccessibilityUpdates
/* Re-entrance guard: VoiceOver callbacks during notification posting /* Re-entrance guard: VoiceOver callbacks during notification posting
can trigger redisplay, which calls ns_update_end, which calls us can trigger redisplay, which calls ns_update_end, which calls us