patches: 0008 focus restore after child frame close + corfu delay 2s

- childFrameCompletionActive flag: set by child frame handler,
  cleared when no child frame visible on parent's next cycle
- Posts FocusedUIElementChanged on parent buffer element to
  restore VoiceOver character echo after corfu closes
- corfu-auto-delay: 1.0 → 2.0 (reduce popup noise)
This commit is contained in:
2026-02-28 17:21:44 +01:00
parent afa65a8201
commit 4c7ce352cd
2 changed files with 86 additions and 31 deletions

View File

@@ -630,7 +630,7 @@ Skip for beamer exports — beamer uses adjustbox on plain tabular."
(after! corfu
(setq corfu-auto t
corfu-auto-delay 1.0
corfu-auto-delay 2.0
corfu-auto-prefix 2
corfu-cycle t
corfu-preselect 'prompt

View File

@@ -1,4 +1,4 @@
From e11d0688f119046827cd1895008d3a93e22f6d0d Mon Sep 17 00:00:00 2001
From 4b4049b4a48b80fb2f786c8777a77bd03a9cf5dc Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 16:01:29 +0100
Subject: [PATCH 2/2] ns: announce child frame completion candidates for
@@ -16,42 +16,49 @@ find the selected candidate. Reuse ns_ax_face_is_selected from
the overlay patch to identify "current", "selected", and
"selection" faces.
Safety measures:
- Use record_unwind_current_buffer / set_buffer_internal_1 to
temporarily switch to the child frame buffer before calling
Fbuffer_substring_no_properties (which operates on current_buffer).
All return paths call unbind_to to restore the original buffer.
- The re-entrance guard (accessibilityUpdating) MUST come before the
child frame dispatch, because Lisp calls in the scan function can
trigger redisplay.
- BUF_MODIFF gating prevents redundant scans on every redisplay tick
and provides a secondary re-entrance guard.
- Frame state validation (WINDOWP, BUFFERP) handles partially
initialized child frames during creation.
- Buffer size limit (10000 chars) skips non-completion child frames
such as eldoc documentation or which-key popups.
Safety:
- record_unwind_current_buffer / set_buffer_internal_1 to switch to
the child frame buffer for Fbuffer_substring_no_properties.
- Re-entrance guard (accessibilityUpdating) before child frame dispatch.
- BUF_MODIFF gating prevents redundant scans.
- WINDOWP, BUFFERP validation for partially initialized frames.
- Buffer size limit (10000 chars) skips non-completion child frames.
When the child frame closes, post FocusedUIElementChangedNotification
on the parent buffer element to restore VoiceOver's character echo
and cursor tracking. The flag childFrameCompletionActive is set by
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 (not the overlayZoomRect flag used
for minibuffer overlay completion) because the child frame renders
independently --- its ns_update_end runs after the parent frame's
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.
* src/nsterm.h (EmacsView): Add announceChildFrameCompletion,
childFrameCompletionActive flag.
* src/nsterm.m (ns_ax_selected_child_frame_text): New function.
(EmacsView announceChildFrameCompletion): New method.
(EmacsView postAccessibilityUpdates): Dispatch to child frame
handler for FRAME_PARENT_FRAME frames, under re-entrance guard.
(EmacsView announceChildFrameCompletion): New method, set parent flag.
(EmacsView postAccessibilityUpdates): Dispatch to child frame handler,
refocus parent buffer element when child frame closes.
---
src/nsterm.h | 1 +
src/nsterm.m | 205 ++++++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 205 insertions(+), 1 deletion(-)
src/nsterm.h | 2 +
src/nsterm.m | 253 ++++++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 254 insertions(+), 1 deletion(-)
diff --git a/src/nsterm.h b/src/nsterm.h
index 5c15639..21b2823 100644
index 5c15639..8b34300 100644
--- a/src/nsterm.h
+++ b/src/nsterm.h
@@ -657,6 +657,7 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType)
@@ -594,6 +594,7 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType)
NSRect lastAccessibilityCursorRect;
BOOL overlayZoomActive;
NSRect overlayZoomRect;
+ BOOL childFrameCompletionActive;
#endif
BOOL font_panel_active;
NSFont *font_panel_result;
@@ -657,6 +658,7 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType)
- (void)rebuildAccessibilityTree;
- (void)invalidateAccessibilityTree;
- (void)postAccessibilityUpdates;
@@ -60,7 +67,7 @@ index 5c15639..21b2823 100644
@end
diff --git a/src/nsterm.m b/src/nsterm.m
index d13c5c7..092bc11 100644
index d13c5c7..03ed77c 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -7066,6 +7066,110 @@ ns_ax_selected_overlay_text (struct buffer *b,
@@ -174,7 +181,7 @@ index d13c5c7..092bc11 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
@@ -12299,6 +12403,93 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view,
@@ -12299,6 +12403,105 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view,
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. */
@@ -240,6 +247,18 @@ index d13c5c7..092bc11 100644
+ NSAccessibilityAnnouncementRequestedNotification,
+ annInfo);
+
+ /* Mark the parent as having an active child frame completion.
+ When the child frame closes, the parent's next accessibility
+ cycle will post FocusedUIElementChanged to restore VoiceOver's
+ focus to the buffer text element. */
+ struct frame *parent = FRAME_PARENT_FRAME (emacsframe);
+ if (parent)
+ {
+ EmacsView *parentView = FRAME_NS_VIEW (parent);
+ if (parentView)
+ 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. */
@@ -268,7 +287,7 @@ index d13c5c7..092bc11 100644
- (void)postAccessibilityUpdates
{
NSTRACE ("[EmacsView postAccessibilityUpdates]");
@@ -12309,11 +12500,23 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view,
@@ -12309,11 +12512,59 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view,
/* Re-entrance guard: VoiceOver callbacks during notification posting
can trigger redisplay, which calls ns_update_end, which calls us
@@ -289,6 +308,42 @@ index d13c5c7..092bc11 100644
+ accessibilityUpdating = NO;
+ return;
+ }
+
+ /* If a child frame completion was recently active but no child
+ frame is visible anymore, refocus VoiceOver on the buffer
+ element so character echo and cursor tracking resume.
+ Skip if a child frame still exists (completion still open). */
+ if (childFrameCompletionActive)
+ {
+ Lisp_Object tail, frame;
+ BOOL childStillVisible = NO;
+ FOR_EACH_FRAME (tail, frame)
+ if (FRAME_PARENT_FRAME (XFRAME (frame)) == emacsframe
+ && FRAME_VISIBLE_P (XFRAME (frame)))
+ {
+ childStillVisible = YES;
+ break;
+ }
+
+ if (!childStillVisible)
+ {
+ childFrameCompletionActive = NO;
+ EmacsBufferAccessibilityElement *focused = nil;
+ for (id elem in accessibilityElements)
+ if ([elem isKindOfClass:
+ [EmacsBufferAccessibilityElement class]]
+ && [(EmacsBufferAccessibilityElement *)elem
+ isAccessibilityFocused])
+ {
+ focused = elem;
+ break;
+ }
+ if (focused)
+ ns_ax_post_notification (
+ focused,
+ NSAccessibilityFocusedUIElementChangedNotification);
+ }
+ }
+
/* Detect window tree change (split, delete, new buffer). Compare
FRAME_ROOT_WINDOW --- if it changed, the tree structure changed. */