patches: restructure per reviewer feedback

Major changes:
1. Zoom separated into standalone patch 0000
   - UAZoomChangeFocus in ns_draw_window_cursor
   - Fallback in ns_update_end for window-switch tracking
   - No overlayZoomActive (source of split/switch/move bug)

2. VoiceOver patches 0001-0008 are now Zoom-free
   - All UAZoom*, overlayZoom*, kUAZoomFocus references removed
   - lastAccessibilityCursorRect kept for VoiceOver bounds queries
   - Commit messages cleaned of Zoom references

3. README.txt and TESTING.txt rewritten for new structure

Addresses reviewer (Stéphane Marks) feedback:
- Keep Zoom patch separate from VoiceOver work
- Design discussion needed for non-Zoom patches
- Performance: ns-accessibility-enabled=nil for zero overhead
This commit is contained in:
2026-02-28 22:28:35 +01:00
parent bbe683e752
commit d9b4cbb87a
11 changed files with 496 additions and 894 deletions

View File

@@ -1,4 +1,4 @@
From aed0e5447ad6bfb5dc15f7d47b1793e735afd995 Mon Sep 17 00:00:00 2001
From 992bd78124b769fdcb4c1e3231afd13349e066c9 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 16:01:29 +0100
Subject: [PATCH 8/8] ns: announce child frame completion candidates for
@@ -31,9 +31,7 @@ the child frame handler and cleared on the parent's next accessibility
cycle when no child frame is visible (via FOR_EACH_FRAME).
Announce via AnnouncementRequested to NSApp with High priority.
Use direct UAZoomChangeFocus because the child frame renders
independently --- its ns_update_end runs after the parent's
draw_window_cursor, so the last Zoom call wins.
* src/nsterm.h (EmacsView): Add announceChildFrameCompletion,
childFrameCompletionActive flag.
@@ -42,23 +40,66 @@ childFrameCompletionActive flag.
(EmacsView postAccessibilityUpdates): Dispatch to child frame handler,
refocus parent buffer element when child frame closes.
---
src/nsterm.h | 2 +
src/nsterm.m | 255 ++++++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 256 insertions(+), 1 deletion(-)
doc/emacs/macos.texi | 6 -
etc/NEWS | 4 +-
src/nsterm.h | 4 +-
src/nsterm.m | 310 ++++++++++++++++++++++++++++++++-----------
4 files changed, 238 insertions(+), 86 deletions(-)
diff --git a/doc/emacs/macos.texi b/doc/emacs/macos.texi
index 4825cf9..97777e2 100644
--- a/doc/emacs/macos.texi
+++ b/doc/emacs/macos.texi
@@ -278,7 +278,6 @@ restart Emacs to access newly-available services.
@cindex VoiceOver
@cindex accessibility (macOS)
@cindex screen reader (macOS)
-@cindex Zoom, cursor tracking (macOS)
When built with the Cocoa interface on macOS, Emacs exposes buffer
content, cursor position, mode lines, and interactive elements to the
@@ -309,11 +308,6 @@ Shift-modified movement announces selected or deselected text.
The @file{*Completions*} buffer announces each completion candidate
as you navigate, even while keyboard focus remains in the minibuffer.
- macOS Zoom (System Settings, Accessibility, Zoom) tracks the Emacs
-cursor automatically when set to follow keyboard focus. The cursor
-position is communicated via @code{UAZoomChangeFocus} and the
-@code{AXBoundsForRange} accessibility attribute.
-
@vindex ns-accessibility-enabled
To disable the accessibility interface entirely (for instance, to
eliminate overhead on systems where assistive technology is not in
diff --git a/etc/NEWS b/etc/NEWS
index e76ee93..c3e0b40 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -4393,8 +4393,8 @@ send user data to Apple's speech recognition servers.
** VoiceOver accessibility support on macOS.
Emacs now exposes buffer content, cursor position, and interactive
elements to the macOS accessibility subsystem (VoiceOver). This
-includes AXBoundsForRange for macOS Zoom cursor tracking, line and
-word navigation announcements, Tab-navigable interactive spans
+includes line and word navigation announcements, Tab-navigable
+interactive spans
(buttons, links, completion candidates), and completion announcements
for the *Completions* buffer. The implementation uses a virtual
accessibility tree with per-window elements, hybrid SelectedTextChanged
diff --git a/src/nsterm.h b/src/nsterm.h
index a007925..1a8a84d 100644
index a007925..80bc8ff 100644
--- a/src/nsterm.h
+++ b/src/nsterm.h
@@ -598,6 +598,7 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType)
@@ -596,8 +596,7 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType)
BOOL accessibilityUpdating;
@public /* Accessed by ns_draw_phys_cursor (C function). */
NSRect lastAccessibilityCursorRect;
BOOL overlayZoomActive;
NSRect overlayZoomRect;
- BOOL overlayZoomActive;
- NSRect overlayZoomRect;
+ BOOL childFrameCompletionActive;
#endif
BOOL font_panel_active;
NSFont *font_panel_result;
@@ -661,6 +662,7 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType)
@@ -661,6 +660,7 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType)
- (void)rebuildAccessibilityTree;
- (void)invalidateAccessibilityTree;
- (void)postAccessibilityUpdates;
@@ -67,10 +108,60 @@ index a007925..1a8a84d 100644
@end
diff --git a/src/nsterm.m b/src/nsterm.m
index 7025e6e..dba0e49 100644
index 7025e6e..d5d33b0 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -7058,6 +7058,112 @@ visual line index for Zoom (skip whitespace-only lines
@@ -3239,44 +3239,14 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors.
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;
}
@@ -183,7 +274,63 @@ index 7025e6e..dba0e49 100644
/* Build accessibility text for window W, skipping invisible text.
Populates *OUT_START with the buffer start charpos.
Populates *OUT_RUNS with an array of visible runs and *OUT_NRUNS
@@ -12336,6 +12442,105 @@ - (id)accessibilityFocusedUIElement
@@ -8988,7 +9064,6 @@ Text property changes (e.g. face updates from
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
previous redisplay cycle. Rebuilding first would create fresh
elements with current values, making change detection impossible. */
@@ -191,9 +338,7 @@ index 7025e6e..dba0e49 100644
+/* Announce the selected candidate in a child frame completion popup.
+ Handles Corfu, Company-box, and similar frameworks that render
+ candidates in a separate child frame rather than as overlay strings
+ in the minibuffer. Uses direct UAZoomChangeFocus (not the
+ overlayZoomRect flag) because the child frame's ns_update_end runs
+ after the parent's draw_window_cursor. */
+ in the minibuffer. */
+- (void)announceChildFrameCompletion
+{
+ static char *lastCandidate;
@@ -261,35 +406,12 @@ index 7025e6e..dba0e49 100644
+ parentView->childFrameCompletionActive = YES;
+ }
+
+ /* Zoom tracking: focus on the selected row in the child frame.
+ Use direct UAZoomChangeFocus rather than overlayZoomRect because
+ the child frame renders independently of the parent. */
+ if (selected_line >= 0 && UAZoomEnabled ())
+ {
+ int line_h = FRAME_LINE_HEIGHT (emacsframe);
+ int y_off = selected_line * line_h;
+ NSRect r = NSMakeRect (
+ WINDOW_TEXT_TO_FRAME_PIXEL_X (w, 0),
+ WINDOW_TO_FRAME_PIXEL_Y (w, y_off),
+ FRAME_COLUMN_WIDTH (emacsframe),
+ line_h);
+ NSRect winRect = [self convertRect:r toView:nil];
+ NSRect screenRect
+ = [[self window] convertRectToScreen:winRect];
+ 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);
+ }
+}
+
- (void)postAccessibilityUpdates
{
NSTRACE ("[EmacsView postAccessibilityUpdates]");
@@ -12346,11 +12551,59 @@ - (void)postAccessibilityUpdates
@@ -12346,11 +12456,59 @@ - (void)postAccessibilityUpdates
/* Re-entrance guard: VoiceOver callbacks during notification posting
can trigger redisplay, which calls ns_update_end, which calls us