diff --git a/patches/0000-ns-integrate-with-macOS-Zoom-for-cursor-tracking.patch b/patches/0000-ns-integrate-with-macOS-Zoom-for-cursor-tracking.patch index 9450dfb..84eab78 100644 --- a/patches/0000-ns-integrate-with-macOS-Zoom-for-cursor-tracking.patch +++ b/patches/0000-ns-integrate-with-macOS-Zoom-for-cursor-tracking.patch @@ -1,45 +1,35 @@ -From 03a3e77f9ff5f46429964863a2f320e119c0686c Mon Sep 17 00:00:00 2001 +From 5538b9a843f1c56607235fe399562d48541ca4e8 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 22:39:35 +0100 Subject: [PATCH 0/8] ns: integrate with macOS Zoom for cursor tracking Inform macOS Zoom of the text cursor position so the zoomed viewport -follows keyboard focus in Emacs. +follows keyboard focus in Emacs. Also track completion candidates so +Zoom follows the selected item (Vertico, Corfu, etc.) during completion. -Basic cursor tracking: +* etc/NEWS: Document Zoom integration. * src/nsterm.h (EmacsView): Add lastCursorRect, zoomCursorUpdated. -* src/nsterm.m (ns_draw_window_cursor): Store cursor rect in -lastCursorRect; call UAZoomChangeFocus with CG-space coordinates -when UAZoomEnabled returns true. Set zoomCursorUpdated flag. -(ns_update_end): Call UAZoomChangeFocus as fallback when cursor -was not physically redrawn (e.g. after C-x o window switch). -Gated by zoomCursorUpdated to avoid double calls. - -Completion candidate tracking: -* src/nsterm.m (ns_zoom_face_is_selected): New predicate. -Match 'current', 'selected', and 'selection' in face symbol -names to identify the highlighted completion candidate. -(ns_zoom_find_overlay_candidate_line): Scan overlay -before-string/after-string for the selected candidate line. -Handles Vertico, Icomplete, Ivy, and similar overlay frameworks. -(ns_zoom_find_child_frame_candidate): Scan child frame buffer -text for the selected candidate. Handles Corfu, Company-box, -and similar child frame frameworks. -(ns_zoom_track_completion): Called from ns_update_end after -cursor tracking. Overrides Zoom focus to the selected -completion candidate when one is found. - -Coordinate conversion: EmacsView pixels (AppKit, flipped) -> -NSWindow -> NSScreen -> CGRect with y-flip for CoreGraphics -top-left origin. - -Tested on macOS 14 with Zoom enabled: cursor tracking works across -window splits, switches (C-x o), and completion frameworks. +* src/nsterm.m: Include ApplicationServices for UAZoomEnabled and +UAZoomChangeFocus (UniversalAccess sub-framework). +[NS_IMPL_COCOA]: Define NS_AX_MAX_COMPLETION_BUFFER_CHARS. +(ns_zoom_enabled_p): New static function; caches UAZoomEnabled with +1-second TTL to avoid per-frame Mach IPC overhead. +(ns_zoom_face_is_selected): New static predicate; matches 'current', +'selected', 'selection' in face symbol names. +(ns_zoom_find_overlay_candidate_line): New static function; scans +minibuffer overlays for the selected completion candidate line. +(ns_zoom_find_child_frame_candidate): New static function; scans +child frame buffers for a selected candidate; guards against partially +initialized frames with WINDOWP and BUFFERP checks. +(ns_zoom_track_completion): New static function; overrides Zoom focus +to the selected completion candidate after normal cursor tracking. +(ns_update_end): Call ns_zoom_track_completion. +(ns_draw_window_cursor): Store cursor rect; call UAZoomChangeFocus. --- etc/NEWS | 11 ++ src/nsterm.h | 6 + - src/nsterm.m | 336 +++++++++++++++++++++++++++++++++++++++++++++++++++ - 3 files changed, 353 insertions(+) + src/nsterm.m | 353 +++++++++++++++++++++++++++++++++++++++++++++++++++ + 3 files changed, 370 insertions(+) diff --git a/etc/NEWS b/etc/NEWS index ef36df5..80661a9 100644 @@ -81,10 +71,22 @@ index 7c1ee4c..ea6e7ba 100644 } diff --git a/src/nsterm.m b/src/nsterm.m -index 74e4ad5..5498d7a 100644 +index 74e4ad5..fc75910 100644 --- a/src/nsterm.m +++ b/src/nsterm.m -@@ -1081,6 +1081,268 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen) +@@ -71,6 +71,11 @@ Updated by Christian Limpach (chris@nice.ch) + #include "macfont.h" + #include + #include ++/* ApplicationServices provides UAZoomEnabled and UAZoomChangeFocus ++ (UniversalAccess sub-framework). Carbon.h already pulls in ++ ApplicationServices on most SDK versions, but the explicit import ++ makes the dependency visible and guards against SDK changes. */ ++#import + #endif + + static EmacsMenu *dockMenu; +@@ -1081,6 +1086,280 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen) } @@ -93,6 +95,12 @@ index 74e4ad5..5498d7a 100644 +#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \ + && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 + ++/* Maximum buffer size (in characters) for a window that we consider ++ a candidate for a completion popup. Completion popups are small; ++ if the buffer is larger than this, it is not a popup and we skip it ++ to avoid O(buffer-size) work per redisplay cycle. */ ++#define NS_AX_MAX_COMPLETION_BUFFER_CHARS 10000 ++ +/* Cached wrapper around ns_zoom_enabled_p (). + ns_zoom_enabled_p () performs a synchronous Mach IPC roundtrip to the + macOS Accessibility server (~50-200 µs per call). With call sites @@ -209,19 +217,25 @@ index 74e4ad5..5498d7a 100644 +ns_zoom_find_child_frame_candidate (struct frame *f, + struct frame **child_frame) +{ -+ Lisp_Object frames, tail; ++ Lisp_Object frame, tail; + -+ FOR_EACH_FRAME (tail, frames) ++ FOR_EACH_FRAME (tail, frame) + { -+ struct frame *cf = XFRAME (frames); ++ struct frame *cf = XFRAME (frame); + if (!FRAME_NS_P (cf) || !FRAME_LIVE_P (cf)) + continue; + if (FRAME_PARENT_FRAME (cf) != f) + continue; -+ /* Small buffer = likely completion popup. */ ++ /* Small buffer = likely completion popup. Guard against ++ partially initialized frames where selected_window or its ++ buffer may not yet be live. */ ++ if (!WINDOWP (cf->selected_window)) ++ continue; + struct window *cw = XWINDOW (cf->selected_window); ++ if (!BUFFERP (cw->contents)) ++ continue; + struct buffer *b = XBUFFER (cw->contents); -+ if (BUF_ZV (b) - BUF_BEGV (b) > 10000) ++ if (BUF_ZV (b) - BUF_BEGV (b) > NS_AX_MAX_COMPLETION_BUFFER_CHARS) + continue; + + ptrdiff_t beg = BUF_BEGV (b); @@ -353,7 +367,7 @@ index 74e4ad5..5498d7a 100644 static void ns_update_end (struct frame *f) /* -------------------------------------------------------------------------- -@@ -1104,6 +1366,41 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen) +@@ -1104,6 +1383,41 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen) unblock_input (); ns_updating_frame = NULL; @@ -395,7 +409,7 @@ index 74e4ad5..5498d7a 100644 } static void -@@ -3232,6 +3529,45 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors. +@@ -3232,6 +3546,45 @@ 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)); diff --git a/patches/0001-ns-add-accessibility-base-classes-and-text-extractio.patch b/patches/0001-ns-add-accessibility-base-classes-and-text-extractio.patch index cf5be3f..b1559eb 100644 --- a/patches/0001-ns-add-accessibility-base-classes-and-text-extractio.patch +++ b/patches/0001-ns-add-accessibility-base-classes-and-text-extractio.patch @@ -1,43 +1,42 @@ -From 23f582e52ede92fb6d04bfd0062557757bea0971 Mon Sep 17 00:00:00 2001 +From 63788743619d25f4f41cb90b2eea5b48e0fcbc15 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 12:58:11 +0100 Subject: [PATCH 1/8] ns: add accessibility base classes and text extraction -Add the foundation for macOS VoiceOver accessibility in the NS -(Cocoa) port. No existing code paths are modified. +Add the foundation for macOS VoiceOver accessibility in the NS (Cocoa) +port. No existing code paths are modified. * src/nsterm.h (ns_ax_visible_run): New struct. -(EmacsAccessibilityElement): New base class. +(EmacsAccessibilityElement): New base Objective-C class. (EmacsAccessibilityBuffer, EmacsAccessibilityModeLine) -(EmacsAccessibilityInteractiveSpan): Forward declarations. -(EmacsAccessibilityBuffer(Notifications)): New category interface. -(EmacsAccessibilityBuffer(InteractiveSpans)): New category interface. -(EmacsAXSpanType): New enum. -(EmacsView): New ivars for accessibility state. +(EmacsAccessibilityInteractiveSpan): Forward-declare new classes. +(EmacsAXSpanType): New enum for interactive span types. +(EmacsView): New ivars for accessibility element tree. * src/nsterm.m: Include intervals.h for TEXT_PROP_MEANS_INVISIBLE. - -(ns_ax_buffer_text, ns_ax_mode_line_text, ns_ax_frame_for_range) -(ns_ax_completion_string_from_prop, ns_ax_window_buffer_object) -(ns_ax_window_end_charpos, ns_ax_text_prop_at) -(ns_ax_next_prop_change, ns_ax_get_span_label) -(ns_ax_post_notification, ns_ax_post_notification_with_info): New -functions. +(ns_ax_buffer_text): New function; build visible-text string and +run array for a window, skipping invisible character regions. +(ns_ax_mode_line_text): New function; extract mode-line text. +(ns_ax_frame_for_range): New function; map charpos range to screen +rect via glyph matrix. +(ns_ax_completion_string_from_prop) +(ns_ax_window_buffer_object, ns_ax_window_end_charpos) +(ns_ax_text_prop_at, ns_ax_next_prop_change) +(ns_ax_get_span_label, ns_ax_post_notification) +(ns_ax_post_notification_with_info): New helper functions. (EmacsAccessibilityElement): Implement base class. -(syms_of_nsterm): Register accessibility DEFSYM and DEFVAR -ns-accessibility-enabled. - -Tested on macOS 14 Sonoma with VoiceOver 10. Builds cleanly; -no functional change (dead code until patch 5/6 wires it in). +(syms_of_nsterm): Register accessibility DEFSYMs. Add DEFVAR_BOOL +ns-accessibility-enabled with corrected doc: initial value is nil, +set non-nil automatically when an AT is detected at startup. --- - src/nsterm.h | 129 ++++++++++++++ + src/nsterm.h | 130 +++++++++++++++ src/nsterm.m | 462 ++++++++++++++++++++++++++++++++++++++++++++++++++- - 2 files changed, 588 insertions(+), 3 deletions(-) + 2 files changed, 589 insertions(+), 3 deletions(-) diff --git a/src/nsterm.h b/src/nsterm.h -index ea6e7ba..6e830de 100644 +index ea6e7ba..7adbb92 100644 --- a/src/nsterm.h +++ b/src/nsterm.h -@@ -453,6 +453,122 @@ enum ns_return_frame_mode +@@ -453,6 +453,123 @@ enum ns_return_frame_mode @end @@ -83,7 +82,8 @@ index ea6e7ba..6e830de 100644 +} ns_ax_visible_run; + +/* Virtual AXTextArea element — one per visible Emacs window (buffer). */ -+@interface EmacsAccessibilityBuffer : EmacsAccessibilityElement ++@interface EmacsAccessibilityBuffer ++ : EmacsAccessibilityElement +{ + ns_ax_visible_run *visibleRuns; + NSUInteger visibleRunCount; @@ -160,7 +160,7 @@ index ea6e7ba..6e830de 100644 /* ========================================================================== The main Emacs view -@@ -471,6 +587,12 @@ enum ns_return_frame_mode +@@ -471,6 +588,12 @@ enum ns_return_frame_mode #ifdef NS_IMPL_COCOA char *old_title; BOOL maximizing_resize; @@ -173,7 +173,7 @@ index ea6e7ba..6e830de 100644 #endif BOOL font_panel_active; NSFont *font_panel_result; -@@ -534,6 +656,13 @@ enum ns_return_frame_mode +@@ -534,6 +657,13 @@ enum ns_return_frame_mode - (void)windowWillExitFullScreen; - (void)windowDidExitFullScreen; - (void)windowDidBecomeKey; @@ -188,7 +188,7 @@ index ea6e7ba..6e830de 100644 diff --git a/src/nsterm.m b/src/nsterm.m -index 5498d7a..e516946 100644 +index fc75910..e9ebac0 100644 --- a/src/nsterm.m +++ b/src/nsterm.m @@ -46,6 +46,7 @@ Updated by Christian Limpach (chris@nice.ch) @@ -199,7 +199,7 @@ index 5498d7a..e516946 100644 #include "systime.h" #include "character.h" #include "xwidget.h" -@@ -7192,6 +7193,430 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg +@@ -7209,6 +7210,430 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg } #endif @@ -630,7 +630,7 @@ index 5498d7a..e516946 100644 /* ========================================================================== EmacsView implementation -@@ -11648,6 +12073,28 @@ Convert an X font name (XLFD) to an NS font name. +@@ -11665,6 +12090,28 @@ Convert an X font name (XLFD) to an NS font name. DEFSYM (Qns_drag_operation_generic, "ns-drag-operation-generic"); DEFSYM (Qns_handle_drag_motion, "ns-handle-drag-motion"); @@ -659,7 +659,7 @@ index 5498d7a..e516946 100644 Fput (Qalt, Qmodifier_value, make_fixnum (alt_modifier)); Fput (Qhyper, Qmodifier_value, make_fixnum (hyper_modifier)); Fput (Qmeta, Qmodifier_value, make_fixnum (meta_modifier)); -@@ -11780,7 +12227,7 @@ Convert an X font name (XLFD) to an NS font name. +@@ -11797,7 +12244,7 @@ Convert an X font name (XLFD) to an NS font name. doc: /* Non-nil means to use native fullscreen on Mac OS X 10.7 and later. Nil means use fullscreen the old (< 10.7) way. The old way works better with multiple monitors, but lacks tool bar. This variable is ignored on @@ -668,7 +668,7 @@ index 5498d7a..e516946 100644 ns_use_native_fullscreen = YES; ns_last_use_native_fullscreen = ns_use_native_fullscreen; -@@ -11796,10 +12243,19 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with +@@ -11813,10 +12260,19 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with This variable is ignored on Mac OS X < 10.7 and GNUstep. */); ns_use_srgb_colorspace = YES; @@ -689,7 +689,7 @@ index 5498d7a..e516946 100644 ns_use_mwheel_acceleration = YES; DEFVAR_LISP ("ns-mwheel-line-height", ns_mwheel_line_height, -@@ -11810,7 +12266,7 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with +@@ -11827,7 +12283,7 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with DEFVAR_BOOL ("ns-use-mwheel-momentum", ns_use_mwheel_momentum, doc: /* Non-nil means mouse wheel scrolling uses momentum. diff --git a/patches/0002-ns-implement-buffer-accessibility-element-core-proto.patch b/patches/0002-ns-implement-buffer-accessibility-element-core-proto.patch index 4fafdd7..0890caa 100644 --- a/patches/0002-ns-implement-buffer-accessibility-element-core-proto.patch +++ b/patches/0002-ns-implement-buffer-accessibility-element-core-proto.patch @@ -1,4 +1,4 @@ -From 77ba59fea45fca76430d2aafbf79dc7e31ac0041 Mon Sep 17 00:00:00 2001 +From 5273b52fe8e4c596574eff4392416d30c2942b7d Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 12:58:11 +0100 Subject: [PATCH 2/8] ns: implement buffer accessibility element (core @@ -7,25 +7,30 @@ Subject: [PATCH 2/8] ns: implement buffer accessibility element (core Implement the NSAccessibility text protocol for Emacs buffer windows. * src/nsterm.m (ns_ax_find_completion_overlay_range): New function. -(ns_ax_event_is_line_nav_key): New function. -(ns_ax_completion_text_for_span): New function. -(EmacsAccessibilityBuffer): Implement core NSAccessibility protocol: -text cache with @synchronized, visible-run binary search O(log n), -selectedTextRange, lineForIndex/indexForLine, frameForRange, -rangeForPosition, setAccessibilitySelectedTextRange, -setAccessibilityFocused. - -Tested on macOS 14 with VoiceOver. Verified: buffer reading, -line-by-line navigation, word/character announcements. +(ns_ax_event_is_line_nav_key, ns_ax_completion_text_for_span): New +functions. +(EmacsAccessibilityBuffer): Implement core NSAccessibility protocol. +(ensureTextCache): Validity gated on BUF_CHARS_MODIFF, not BUF_MODIFF, +to avoid O(buffer-size) rebuilds on every font-lock pass. Add +explanatory comment on why lineRangeForRange: in the lineStartOffsets +loop is safe: it runs only on actual character modifications. +(accessibilityIndexForCharpos:): O(1) fast path for pure-ASCII runs +(ax_length == length); fall back to sequence walk for multi-byte runs. +(charposForAccessibilityIndex:): Symmetric O(1) fast path. +(accessibilitySelectedTextRange, accessibilityLineForIndex:) +(accessibilityIndexForLine:, accessibilityRangeForIndex:) +(accessibilityStringForRange:, accessibilityFrameForRange:) +(accessibilityRangeForPosition:, setAccessibilitySelectedTextRange:) +(setAccessibilityFocused:): Implement NSAccessibility protocol methods. --- - src/nsterm.m | 1123 ++++++++++++++++++++++++++++++++++++++++++++++++++ - 1 file changed, 1123 insertions(+) + src/nsterm.m | 1127 ++++++++++++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 1127 insertions(+) diff --git a/src/nsterm.m b/src/nsterm.m -index e516946..cfd0715 100644 +index e9ebac0..64a6011 100644 --- a/src/nsterm.m +++ b/src/nsterm.m -@@ -7614,6 +7614,1129 @@ - (id)accessibilityTopLevelUIElement +@@ -7631,6 +7631,1133 @@ - (id)accessibilityTopLevelUIElement @end @@ -406,9 +411,13 @@ index e516946..cfd0715 100644 + visibleRunCount = nruns; + + /* Build line-start index for O(log L) line queries. -+ Walk the cached text once, recording the start offset -+ of each line. This runs once per cache rebuild (on text -+ change or narrowing), not per cursor move. */ ++ Walk the cached text once, recording the start offset of each ++ line. Uses NSString lineRangeForRange: --- O(N) in the total ++ text --- but this loop runs only on cache rebuild, which is ++ gated on BUF_CHARS_MODIFF: actual character insertions or ++ deletions. Font-lock (text property changes) does not trigger ++ a rebuild, so the hot path (cursor movement, redisplay) never ++ enters this code. */ + if (lineStartOffsets) + xfree (lineStartOffsets); + lineStartOffsets = NULL; diff --git a/patches/0003-ns-add-buffer-notification-dispatch-and-mode-line-el.patch b/patches/0003-ns-add-buffer-notification-dispatch-and-mode-line-el.patch index 0cd4878..0ea92b6 100644 --- a/patches/0003-ns-add-buffer-notification-dispatch-and-mode-line-el.patch +++ b/patches/0003-ns-add-buffer-notification-dispatch-and-mode-line-el.patch @@ -1,33 +1,35 @@ -From df8710e72d6d988b86079d2eec624fd5bde23b71 Mon Sep 17 00:00:00 2001 +From 5f8b5394ec9bfdd344dbc10aee7514b1891b00d8 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 12:58:11 +0100 Subject: [PATCH 3/8] ns: add buffer notification dispatch and mode-line element -Add VoiceOver notification methods and mode-line readout. +Add VoiceOver notification dispatch and mode-line readout. -* src/nsterm.m (EmacsAccessibilityBuffer(Notifications)): New -category. -(postTextChangedNotification:): ValueChanged with edit details. +* src/nsterm.m (EmacsAccessibilityBuffer(Notifications)): New category. +(postTextChangedNotification:): Post NSAccessibilityValueChangedNotification +with AXTextEditType/AXTextChangeValue details. (postFocusedCursorNotification:direction:granularity:markActive: -oldMarkActive:): Hybrid SelectedTextChanged / AnnouncementRequested -per WebKit pattern. +oldMarkActive:): Post NSAccessibilitySelectedTextChangedNotification +following the WebKit hybrid pattern; announce character at point for +character moves. (postCompletionAnnouncementForBuffer:point:): Announce completion -candidates in non-focused buffers. -(postAccessibilityNotificationsForFrame:): Main dispatch entry point. -(EmacsAccessibilityModeLine): Implement AXStaticText element. - -Tested on macOS 14. Verified: cursor movement announcements, -region selection feedback, completion popups, mode-line reading. +candidates in non-focused (completion) buffers. Lisp/buffer +access is performed inside block_input; ObjC AX calls are made after +unblock_input to avoid holding block_input during @synchronized. +(postAccessibilityNotificationsForFrame:): Main dispatch entry point; +detects text edit, cursor/mark change, or overlay change. +(EmacsAccessibilityModeLine): Implement AXStaticText element for the +mode line. --- - src/nsterm.m | 545 +++++++++++++++++++++++++++++++++++++++++++++++++++ - 1 file changed, 545 insertions(+) + src/nsterm.m | 546 +++++++++++++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 546 insertions(+) diff --git a/src/nsterm.m b/src/nsterm.m -index cfd0715..fee3e49 100644 +index 64a6011..350111a 100644 --- a/src/nsterm.m +++ b/src/nsterm.m -@@ -8737,6 +8737,551 @@ - (NSRect)accessibilityFrame +@@ -8758,6 +8758,552 @@ - (NSRect)accessibilityFrame @end @@ -315,6 +317,7 @@ index cfd0715..fee3e49 100644 + } + + unbind_to (count2, Qnil); ++ unblock_input (); + + /* Final fallback: read current line at point. */ + if (!announceText) diff --git a/patches/0004-ns-add-interactive-span-elements-for-Tab-navigation.patch b/patches/0004-ns-add-interactive-span-elements-for-Tab-navigation.patch index 8b39463..6d17e25 100644 --- a/patches/0004-ns-add-interactive-span-elements-for-Tab-navigation.patch +++ b/patches/0004-ns-add-interactive-span-elements-for-Tab-navigation.patch @@ -1,26 +1,27 @@ -From e73e311a95d86d6e88a78185aab42ca65b65e066 Mon Sep 17 00:00:00 2001 +From 9c7e408085f52f1e44b6cb71e64448162e5c3e68 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 12:58:11 +0100 Subject: [PATCH 4/8] ns: add interactive span elements for Tab navigation -* src/nsterm.m (ns_ax_scan_interactive_spans): New function. -(EmacsAccessibilityInteractiveSpan): Implement AXButton/AXLink -elements with AXPress action. +* src/nsterm.m (ns_ax_scan_interactive_spans): New function; scans the +visible portion of a buffer for interactive text properties +(ns-ax-widget, ns-ax-button, ns-ax-follow-link, ns-ax-org-link, +mouse-face, overlay keymap) and builds EmacsAccessibilityInteractiveSpan +elements. +(EmacsAccessibilityInteractiveSpan): Implement AXButton and AXLink +elements with an AXPress action that sends a synthetic TAB keystroke. (EmacsAccessibilityBuffer(InteractiveSpans)): New category. -accessibilityChildrenInNavigationOrder for Tab/Shift-Tab cycling -with wrap-around. - -Tested on macOS 14. Verified: Tab-cycling through org-mode links, -*Completions* candidates, widget buttons, customize buffers. +(accessibilityChildrenInNavigationOrder): Return cached span array, +rebuilding lazily when interactiveSpansDirty is set. --- src/nsterm.m | 286 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 286 insertions(+) diff --git a/src/nsterm.m b/src/nsterm.m -index fee3e49..8c26e27 100644 +index 350111a..992a5ce 100644 --- a/src/nsterm.m +++ b/src/nsterm.m -@@ -9282,6 +9282,292 @@ - (NSRect)accessibilityFrame +@@ -9304,6 +9304,292 @@ - (NSRect)accessibilityFrame @end diff --git a/patches/0005-ns-integrate-accessibility-with-EmacsView-and-redisp.patch b/patches/0005-ns-integrate-accessibility-with-EmacsView-and-redisp.patch index a4fe6fc..ed07556 100644 --- a/patches/0005-ns-integrate-accessibility-with-EmacsView-and-redisp.patch +++ b/patches/0005-ns-integrate-accessibility-with-EmacsView-and-redisp.patch @@ -1,26 +1,26 @@ -From 6f37c729a3646dc0ac4c68825edac8f6a81cd9ec Mon Sep 17 00:00:00 2001 +From 0e8d5540c8993b2e91c437d20e47e7abeb12543f Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 12:58:11 +0100 Subject: [PATCH 5/8] ns: integrate accessibility with EmacsView and redisplay -Wire the accessibility infrastructure into EmacsView and the +Wire the accessibility element tree into EmacsView and hook it into +the redisplay cycle. -* src/nsterm.m (ns_update_end): Call [view postAccessibilityUpdates]. +* etc/NEWS: Document VoiceOver accessibility support. +* src/nsterm.m (ns_update_end): Call -[EmacsView postAccessibilityUpdates]. +(EmacsApp ns_update_accessibility_state): New method; query +AXIsProcessTrustedWithOptions and UAZoomEnabled to set +ns_accessibility_enabled automatically. +(EmacsApp ns_accessibility_did_change:): New method; handle +com.apple.accessibility.api distributed notification. (EmacsView dealloc): Release accessibilityElements. -(EmacsView windowDidBecomeKey): Post accessibility focus notification. +(EmacsView windowDidBecomeKey:): Post accessibility focus notification. (ns_ax_collect_windows): New function. (EmacsView rebuildAccessibilityTree, invalidateAccessibilityTree) (accessibilityChildren, accessibilityFocusedUIElement) (postAccessibilityUpdates, accessibilityBoundsForRange:) (accessibilityParameterizedAttributeNames) (accessibilityAttributeValue:forParameter:): New methods. -* etc/NEWS: Document VoiceOver accessibility support. - -Tested on macOS 14 with VoiceOver. End-to-end: buffer -navigation, cursor tracking, window switching, completions, evil-mode -block cursor, org-mode folded headings, indirect buffers. - -Known limitations documented in patch 6 Texinfo node. --- etc/NEWS | 13 ++ src/nsterm.m | 430 +++++++++++++++++++++++++++++++++++++++++++++++++-- @@ -51,10 +51,10 @@ index 80661a9..2b1f9e6 100644 ** Re-introduced dictation, lost in Emacs v30 (macOS). We lost macOS dictation in v30 when migrating to NSTextInputClient. diff --git a/src/nsterm.m b/src/nsterm.m -index 8c26e27..70c7521 100644 +index 992a5ce..d9b8ecd 100644 --- a/src/nsterm.m +++ b/src/nsterm.m -@@ -1258,7 +1258,7 @@ If a completion candidate is selected (overlay or child frame), +@@ -1275,7 +1275,7 @@ If a completion candidate is selected (overlay or child frame), static void ns_zoom_track_completion (struct frame *f, EmacsView *view) { @@ -63,7 +63,7 @@ index 8c26e27..70c7521 100644 return; if (!WINDOWP (f->selected_window)) return; -@@ -1375,7 +1375,8 @@ so the visual offset is (ov_line + 1) * line_h from +@@ -1392,7 +1392,8 @@ so the visual offset is (ov_line + 1) * line_h from (zoomCursorUpdated is NO). */ #if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \ && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 @@ -73,7 +73,7 @@ index 8c26e27..70c7521 100644 && !NSIsEmptyRect (view->lastCursorRect)) { NSRect r = view->lastCursorRect; -@@ -1402,6 +1403,9 @@ so the visual offset is (ov_line + 1) * line_h from +@@ -1419,6 +1420,9 @@ so the visual offset is (ov_line + 1) * line_h from if (view) ns_zoom_track_completion (f, view); #endif /* NS_IMPL_COCOA */ @@ -83,7 +83,7 @@ index 8c26e27..70c7521 100644 } static void -@@ -3549,7 +3553,7 @@ EmacsView pixels (AppKit, flipped, top-left origin) +@@ -3566,7 +3570,7 @@ EmacsView pixels (AppKit, flipped, top-left origin) #if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \ && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 @@ -92,7 +92,7 @@ index 8c26e27..70c7521 100644 { NSRect windowRect = [view convertRect:r toView:nil]; NSRect screenRect -@@ -6714,9 +6718,56 @@ - (void)applicationDidFinishLaunching: (NSNotification *)notification +@@ -6731,9 +6735,56 @@ - (void)applicationDidFinishLaunching: (NSNotification *)notification } #endif @@ -149,7 +149,7 @@ index 8c26e27..70c7521 100644 - (void)antialiasThresholdDidChange:(NSNotification *)notification { #ifdef NS_IMPL_COCOA -@@ -7617,7 +7668,6 @@ - (id)accessibilityTopLevelUIElement +@@ -7634,7 +7685,6 @@ - (id)accessibilityTopLevelUIElement @@ -157,7 +157,7 @@ index 8c26e27..70c7521 100644 static BOOL ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, ptrdiff_t *out_start, -@@ -8738,7 +8788,6 @@ - (NSRect)accessibilityFrame +@@ -8759,7 +8809,6 @@ - (NSRect)accessibilityFrame @end @@ -165,7 +165,7 @@ index 8c26e27..70c7521 100644 /* =================================================================== EmacsAccessibilityBuffer (Notifications) — AX event dispatch -@@ -9283,7 +9332,6 @@ - (NSRect)accessibilityFrame +@@ -9305,7 +9354,6 @@ - (NSRect)accessibilityFrame @end @@ -173,7 +173,7 @@ index 8c26e27..70c7521 100644 /* =================================================================== EmacsAccessibilityInteractiveSpan — helpers and implementation =================================================================== */ -@@ -9613,6 +9661,7 @@ - (void)dealloc +@@ -9635,6 +9683,7 @@ - (void)dealloc [layer release]; #endif @@ -181,7 +181,7 @@ index 8c26e27..70c7521 100644 [[self menu] release]; [super dealloc]; } -@@ -10961,6 +11010,32 @@ - (void)windowDidBecomeKey /* for direct calls */ +@@ -10983,6 +11032,32 @@ - (void)windowDidBecomeKey /* for direct calls */ XSETFRAME (event.frame_or_window, emacsframe); kbd_buffer_store_event (&event); ns_send_appdefined (-1); // Kick main loop @@ -214,7 +214,7 @@ index 8c26e27..70c7521 100644 } -@@ -12198,6 +12273,332 @@ - (int) fullscreenState +@@ -12220,6 +12295,332 @@ - (int) fullscreenState return fs_state; } @@ -547,7 +547,7 @@ index 8c26e27..70c7521 100644 @end /* EmacsView */ -@@ -14198,12 +14599,17 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with +@@ -14220,12 +14621,17 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with ns_use_srgb_colorspace = YES; DEFVAR_BOOL ("ns-accessibility-enabled", ns_accessibility_enabled, diff --git a/patches/0006-doc-add-VoiceOver-accessibility-section-to-macOS-app.patch b/patches/0006-doc-add-VoiceOver-accessibility-section-to-macOS-app.patch index 6db7602..a42c7b6 100644 --- a/patches/0006-doc-add-VoiceOver-accessibility-section-to-macOS-app.patch +++ b/patches/0006-doc-add-VoiceOver-accessibility-section-to-macOS-app.patch @@ -1,17 +1,22 @@ -From b40de953e11fce0df19bfe7c77b2b009246228ac Mon Sep 17 00:00:00 2001 +From 4b77c5a182863322da1d42b4f4f2ba5a2ce7179d Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 12:58:11 +0100 Subject: [PATCH 6/8] doc: add VoiceOver accessibility section to macOS appendix -* doc/emacs/macos.texi (VoiceOver Accessibility): New node. Document -screen reader usage, keyboard navigation, completion announcements, +* doc/emacs/macos.texi (VoiceOver Accessibility): New node between +'Mac / GNUstep Events' and 'GNUstep Support'. Document screen reader +usage, keyboard navigation, completion announcements, ns-accessibility- +enabled, and known limitations. Use @xref for cross-reference at +sentence start. Correct description of ns-accessibility-enabled +default: initial value is nil, set automatically at startup. --- - doc/emacs/macos.texi | 75 ++++++++++++++++++++++++++++++++++++++++++++ - 1 file changed, 75 insertions(+) + doc/emacs/macos.texi | 76 ++++++++++++++++++++++++++++++++++++++++++++ + src/nsterm.m | 10 ++++-- + 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/doc/emacs/macos.texi b/doc/emacs/macos.texi -index 6bd334f..4825cf9 100644 +index 6bd334f..6514dfc 100644 --- a/doc/emacs/macos.texi +++ b/doc/emacs/macos.texi @@ -36,6 +36,7 @@ Support}), but we hope to improve it in the future. @@ -22,7 +27,7 @@ index 6bd334f..4825cf9 100644 * GNUstep Support:: Details on status of GNUstep support. @end menu -@@ -272,6 +273,80 @@ and return the result as a string. You can also use the Lisp function +@@ -272,6 +273,81 @@ and return the result as a string. You can also use the Lisp function services and receive the results back. Note that you may need to restart Emacs to access newly-available services. @@ -70,8 +75,9 @@ index 6bd334f..4825cf9 100644 +@vindex ns-accessibility-enabled + To disable the accessibility interface entirely (for instance, to +eliminate overhead on systems where assistive technology is not in -+use), set @code{ns-accessibility-enabled} to @code{nil}. The default -+is @code{t}. ++use), set @code{ns-accessibility-enabled} to @code{nil}. Emacs ++detects the presence of assistive technology at startup and sets this ++variable automatically; the initial value is @code{nil}. + +@subheading Known Limitations + @@ -94,8 +100,8 @@ index 6bd334f..4825cf9 100644 +@end itemize + + This support is available only on the Cocoa build; GNUstep has a -+different accessibility model and is not yet supported -+(@pxref{GNUstep Support}). Evil-mode block cursors are handled ++different accessibility model and is not yet supported; ++@xref{GNUstep Support}. Evil-mode block cursors are handled +correctly: character navigation announces the character at the cursor +position, not the character before it. + @@ -103,6 +109,27 @@ index 6bd334f..4825cf9 100644 @node GNUstep Support @section GNUstep Support +diff --git a/src/nsterm.m b/src/nsterm.m +index d9b8ecd..7d48e6b 100644 +--- a/src/nsterm.m ++++ b/src/nsterm.m +@@ -14622,9 +14622,13 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with + + DEFVAR_BOOL ("ns-accessibility-enabled", ns_accessibility_enabled, + doc: /* Non-nil enables Zoom cursor tracking and VoiceOver support. +-Emacs sets this automatically at startup when macOS Zoom is active or +-any assistive technology (VoiceOver, Switch Control, etc.) is connected, +-and updates it whenever that state changes. You can override manually: ++Emacs detects at startup whether macOS Zoom is active or an assistive ++technology (VoiceOver, Switch Control, etc.) is connected, and sets ++this variable accordingly. It updates automatically when accessibility ++state changes. The initial value is nil; it becomes non-nil only when ++an AT is detected. ++ ++You can override the auto-detection: + + (setq ns-accessibility-enabled t) ; always on + (setq ns-accessibility-enabled nil) ; always off -- 2.43.0 diff --git a/patches/0007-ns-announce-overlay-completion-candidates-for-VoiceO.patch b/patches/0007-ns-announce-overlay-completion-candidates-for-VoiceO.patch index b32eba6..096e295 100644 --- a/patches/0007-ns-announce-overlay-completion-candidates-for-VoiceO.patch +++ b/patches/0007-ns-announce-overlay-completion-candidates-for-VoiceO.patch @@ -1,58 +1,31 @@ -From 6c7d852b4667ec72a190d9a3008a46bbf3a78729 Mon Sep 17 00:00:00 2001 +From c383dc0e225d831283db7fdfccc22c12951a1077 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 14:46:25 +0100 Subject: [PATCH 7/8] ns: announce overlay completion candidates for VoiceOver Completion frameworks such as Vertico, Ivy, and Icomplete render -candidates via overlay before-string/after-string properties rather -than buffer text. Without this patch, VoiceOver cannot read -overlay-based completion UIs. +candidates via overlay before-string/after-string properties. Without +this change VoiceOver cannot read overlay-based completion UIs. -Identify the selected candidate by scanning overlay strings for a -face whose symbol name contains "current", "selected", or -"selection" --- this matches vertico-current, icomplete-selected-match, -ivy-current-match, company-tooltip-selection, and similar framework -faces without hard-coding any specific name. - -Key implementation details: - -- The overlay detection branch runs independently (if, not else-if) - of the text-change branch, because Vertico bumps both BUF_MODIFF - (via text property changes in vertico--prompt-selection) and - BUF_OVERLAY_MODIFF (via overlay-put) in the same command cycle. - -- Use BUF_CHARS_MODIFF to gate ValueChanged notifications, since - text property changes bump BUF_MODIFF but not BUF_CHARS_MODIFF. - -- Remove BUF_OVERLAY_MODIFF from ensureTextCache validity checks - to prevent a race condition where VoiceOver AX queries silently - consume the overlay change before the notification dispatch runs. - -- Announce via AnnouncementRequested to NSApp with High priority. - Do not post SelectedTextChanged (that reads the AX text at cursor - position, which is the minibuffer input, not the candidate). - - candidate line start. The flag is cleared when the user types - (BUF_CHARS_MODIFF changes) or when no candidate is found - (minibuffer exit, C-g). - -(EmacsAccessibilityBuffer): Add cachedCharsModiff. -* src/nsterm.m (ns_ax_face_is_selected): New predicate. Match -"current", "selected", and "selection" in face symbol names. -(ns_ax_selected_overlay_text): New function. -(EmacsAccessibilityBuffer ensureTextCache): Remove overlay_modiff. -(EmacsAccessibilityBuffer postAccessibilityNotificationsForFrame:): -Independent overlay branch, BUF_CHARS_MODIFF gating, candidate +* src/nsterm.m (ns_ax_face_is_selected): New static function; matches +'current', 'selected', 'selection' in face symbol names. +(ns_ax_selected_overlay_text): New function; scan overlay strings in +the window for a line with a selected face; return its text. +(EmacsAccessibilityBuffer(Notifications) +postAccessibilityNotificationsForFrame:): Handle BUF_OVERLAY_MODIFF +changes independently of text changes. Use BUF_CHARS_MODIFF to gate +ValueChanged; keep overlay_modiff out of ensureTextCache to prevent a +race where an AX query consumes the change before notification. --- src/nsterm.h | 1 + src/nsterm.m | 330 ++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 289 insertions(+), 42 deletions(-) diff --git a/src/nsterm.h b/src/nsterm.h -index 6e830de..2102fb9 100644 +index 7adbb92..483fed3 100644 --- a/src/nsterm.h +++ b/src/nsterm.h -@@ -509,6 +509,7 @@ typedef struct ns_ax_visible_run +@@ -510,6 +510,7 @@ typedef struct ns_ax_visible_run @property (nonatomic, assign) ptrdiff_t cachedOverlayModiff; @property (nonatomic, assign) ptrdiff_t cachedTextStart; @property (nonatomic, assign) ptrdiff_t cachedModiff; @@ -61,10 +34,10 @@ index 6e830de..2102fb9 100644 @property (nonatomic, assign) BOOL cachedMarkActive; @property (nonatomic, copy) NSString *cachedCompletionAnnouncement; diff --git a/src/nsterm.m b/src/nsterm.m -index 70c7521..a3104d0 100644 +index 7d48e6b..20ba0b9 100644 --- a/src/nsterm.m +++ b/src/nsterm.m -@@ -7254,11 +7254,154 @@ Accessibility virtual elements (macOS / Cocoa only) +@@ -7271,11 +7271,154 @@ Accessibility virtual elements (macOS / Cocoa only) /* ---- Helper: extract buffer text for accessibility ---- */ @@ -220,7 +193,7 @@ index 70c7521..a3104d0 100644 static NSString * ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start, ns_ax_visible_run **out_runs, NSUInteger *out_nruns) -@@ -7329,7 +7472,7 @@ Accessibility virtual elements (macOS / Cocoa only) +@@ -7346,7 +7489,7 @@ Accessibility virtual elements (macOS / Cocoa only) /* Extract this visible run's text. Use Fbuffer_substring_no_properties which correctly handles the @@ -229,7 +202,7 @@ index 70c7521..a3104d0 100644 include garbage bytes when the run spans the gap position. */ Lisp_Object lstr = Fbuffer_substring_no_properties ( make_fixnum (pos), make_fixnum (run_end)); -@@ -7410,7 +7553,7 @@ Mode lines using icon fonts (e.g. doom-modeline with nerd-font) +@@ -7427,7 +7570,7 @@ Mode lines using icon fonts (e.g. doom-modeline with nerd-font) return NSZeroRect; /* charpos_start and charpos_len are already in buffer charpos @@ -238,7 +211,7 @@ index 70c7521..a3104d0 100644 charposForAccessibilityIndex which handles invisible text. */ ptrdiff_t cp_start = charpos_start; ptrdiff_t cp_end = cp_start + charpos_len; -@@ -7889,6 +8032,7 @@ @implementation EmacsAccessibilityBuffer +@@ -7906,6 +8049,7 @@ @implementation EmacsAccessibilityBuffer @synthesize cachedOverlayModiff; @synthesize cachedTextStart; @synthesize cachedModiff; @@ -246,7 +219,7 @@ index 70c7521..a3104d0 100644 @synthesize cachedPoint; @synthesize cachedMarkActive; @synthesize cachedCompletionAnnouncement; -@@ -7986,7 +8130,7 @@ - (void)ensureTextCache +@@ -8003,7 +8147,7 @@ - (void)ensureTextCache NSTRACE ("EmacsAccessibilityBuffer ensureTextCache"); /* This method is only called from the main thread (AX getters dispatch_sync to main first). Reads of cachedText/cachedTextModiff @@ -255,7 +228,7 @@ index 70c7521..a3104d0 100644 write section at the end needs synchronization to protect against concurrent reads from AX server thread. */ eassert ([NSThread isMainThread]); -@@ -7998,25 +8142,16 @@ - (void)ensureTextCache +@@ -8015,25 +8159,16 @@ - (void)ensureTextCache if (!b) return; @@ -289,7 +262,7 @@ index 70c7521..a3104d0 100644 && cachedTextStart == BUF_BEGV (b) && pt >= cachedTextStart && (textLen == 0 -@@ -8032,7 +8167,7 @@ included in the cached AX text (it is handled separately via +@@ -8049,7 +8184,7 @@ included in the cached AX text (it is handled separately via { [cachedText release]; cachedText = [text retain]; @@ -298,7 +271,7 @@ index 70c7521..a3104d0 100644 cachedTextStart = start; if (visibleRuns) -@@ -8097,7 +8232,7 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos +@@ -8118,7 +8253,7 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos /* Binary search: runs are sorted by charpos (ascending). Find the run whose [charpos, charpos+length) range contains the target, or the nearest run after an invisible gap. O(log n) instead of @@ -307,7 +280,7 @@ index 70c7521..a3104d0 100644 NSUInteger lo = 0, hi = visibleRunCount; while (lo < hi) { -@@ -8146,10 +8281,10 @@ by run length (visible window), not total buffer size. */ +@@ -8167,10 +8302,10 @@ by run length (visible window), not total buffer size. */ /* Convert accessibility string index to buffer charpos. Safe to call from any thread: uses only cachedText (NSString) and @@ -320,7 +293,7 @@ index 70c7521..a3104d0 100644 @synchronized (self) { if (visibleRunCount == 0) -@@ -8191,7 +8326,7 @@ the slow path (composed character sequence walk), which is +@@ -8212,7 +8347,7 @@ the slow path (composed character sequence walk), which is return cp; } } @@ -329,7 +302,7 @@ index 70c7521..a3104d0 100644 if (lo > 0) { ns_ax_visible_run *last = &visibleRuns[visibleRunCount - 1]; -@@ -8213,7 +8348,7 @@ the slow path (composed character sequence walk), which is +@@ -8234,7 +8369,7 @@ the slow path (composed character sequence walk), which is deadlocking the AX server thread. This is prevented by: 1. validWindow checks WINDOW_LIVE_P and BUFFERP before every @@ -338,7 +311,7 @@ index 70c7521..a3104d0 100644 2. All dispatch_sync blocks run on the main thread where no concurrent Lisp code can modify state between checks. 3. block_input prevents timer events and process output from -@@ -8567,6 +8702,50 @@ - (NSInteger)accessibilityInsertionPointLineNumber +@@ -8588,6 +8723,50 @@ - (NSInteger)accessibilityInsertionPointLineNumber return [self lineForAXIndex:point_idx]; } @@ -389,7 +362,7 @@ index 70c7521..a3104d0 100644 - (NSRange)accessibilityRangeForLine:(NSInteger)line { if (![NSThread isMainThread]) -@@ -8789,7 +8968,7 @@ - (NSRect)accessibilityFrame +@@ -8810,7 +8989,7 @@ - (NSRect)accessibilityFrame /* =================================================================== @@ -398,7 +371,7 @@ index 70c7521..a3104d0 100644 These methods notify VoiceOver of text and selection changes. Called from the redisplay cycle (postAccessibilityUpdates). -@@ -8804,7 +8983,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point +@@ -8825,7 +9004,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point if (point > self.cachedPoint && point - self.cachedPoint == 1) { @@ -407,7 +380,7 @@ index 70c7521..a3104d0 100644 [self invalidateTextCache]; [self ensureTextCache]; if (cachedText) -@@ -8823,7 +9002,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point +@@ -8844,7 +9023,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point /* Update cachedPoint here so the selection-move branch does NOT fire for point changes caused by edits. WebKit and Chromium never send both ValueChanged and SelectedTextChanged for the @@ -416,7 +389,7 @@ index 70c7521..a3104d0 100644 self.cachedPoint = point; NSDictionary *change = @{ -@@ -9156,16 +9335,83 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f +@@ -9178,16 +9357,83 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f BOOL markActive = !NILP (BVAR (b, mark_active)); /* --- Text changed (edit) --- */ @@ -504,7 +477,7 @@ index 70c7521..a3104d0 100644 { ptrdiff_t oldPoint = self.cachedPoint; BOOL oldMarkActive = self.cachedMarkActive; -@@ -9333,7 +9579,7 @@ - (NSRect)accessibilityFrame +@@ -9355,7 +9601,7 @@ - (NSRect)accessibilityFrame /* =================================================================== @@ -513,7 +486,7 @@ index 70c7521..a3104d0 100644 =================================================================== */ /* Scan visible range of window W for interactive spans. -@@ -9541,7 +9787,7 @@ - (void) setAccessibilityFocused: (BOOL) focused +@@ -9563,7 +9809,7 @@ - (void) setAccessibilityFocused: (BOOL) focused dispatch_async (dispatch_get_main_queue (), ^{ /* lwin is a Lisp_Object captured by value. This is GC-safe because Lisp_Objects are tagged integers/pointers that @@ -522,7 +495,7 @@ index 70c7521..a3104d0 100644 Emacs. The WINDOW_LIVE_P check below guards against the window being deleted between capture and execution. */ if (!WINDOWP (lwin) || NILP (Fwindow_live_p (lwin))) -@@ -9567,7 +9813,7 @@ - (void) setAccessibilityFocused: (BOOL) focused +@@ -9589,7 +9835,7 @@ - (void) setAccessibilityFocused: (BOOL) focused @end @@ -531,7 +504,7 @@ index 70c7521..a3104d0 100644 Methods are kept here (same .m file) so they access the ivars declared in the @interface ivar block. */ @implementation EmacsAccessibilityBuffer (InteractiveSpans) -@@ -12289,7 +12535,7 @@ - (int) fullscreenState +@@ -12311,7 +12557,7 @@ - (int) fullscreenState if (WINDOW_LEAF_P (w)) { @@ -540,7 +513,7 @@ index 70c7521..a3104d0 100644 EmacsAccessibilityBuffer *elem = [existing objectForKey:[NSValue valueWithPointer:w]]; if (!elem) -@@ -12323,7 +12569,7 @@ - (int) fullscreenState +@@ -12345,7 +12591,7 @@ - (int) fullscreenState } else { @@ -549,7 +522,7 @@ index 70c7521..a3104d0 100644 Lisp_Object child = w->contents; while (!NILP (child)) { -@@ -12435,7 +12681,7 @@ - (void)postAccessibilityUpdates +@@ -12457,7 +12703,7 @@ - (void)postAccessibilityUpdates accessibilityUpdating = YES; /* Detect window tree change (split, delete, new buffer). Compare @@ -558,7 +531,7 @@ index 70c7521..a3104d0 100644 Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe); if (!EQ (curRoot, lastRootWindow)) { -@@ -12444,12 +12690,12 @@ - (void)postAccessibilityUpdates +@@ -12466,12 +12712,12 @@ - (void)postAccessibilityUpdates } /* If tree is stale, rebuild FIRST so we don't iterate freed diff --git a/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch b/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch index 0a5c741..e79e889 100644 --- a/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch +++ b/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch @@ -1,53 +1,30 @@ -From fb4d1411fcc4a18cefae80dbed856fda8fe8c85e Mon Sep 17 00:00:00 2001 +From 2f655a0fa3071046169011ecdc97f0a3f7c1105c Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 16:01:29 +0100 Subject: [PATCH 8/8] ns: announce child frame completion candidates for VoiceOver -Completion frameworks such as Corfu, Company-box, and similar -render candidates in a child frame rather than as overlay strings -in the minibuffer. This patch extends the overlay announcement -support (patch 7/8) to handle child frame popups. +Completion frameworks such as Corfu and Company-box render candidates +in a child frame. This extends the overlay announcement support to +handle child frame popups. -Detect child frames via FRAME_PARENT_FRAME in postAccessibilityUpdates. -Scan the child frame buffer text line by line using Fget_char_property -(which checks both text properties and overlay face properties) to -find the selected candidate. Reuse ns_ax_face_is_selected from -the overlay patch to identify "current", "selected", and -"selection" faces. - -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. -independently --- its ns_update_end runs after the parent's - -* src/nsterm.h (EmacsView): Add announceChildFrameCompletion, -childFrameCompletionActive flag. -* src/nsterm.m (ns_ax_selected_child_frame_text): New function. -(EmacsView announceChildFrameCompletion): New method, set parent flag. -(EmacsView postAccessibilityUpdates): Dispatch to child frame handler, -refocus parent buffer element when child frame closes. +* src/nsterm.m (ns_ax_selected_child_frame_text): New function; scan +a child frame buffer line by line using Fget_char_property to find the +selected candidate; uses record_unwind_current_buffer for safety. +(EmacsView announceChildFrameCompletion): New method. +(EmacsView postAccessibilityUpdates): Detect child frames via +FRAME_PARENT_FRAME; call announceChildFrameCompletion. Post +NSAccessibilityFocusedUIElementChangedNotification on the parent buffer +element when a child frame completion closes. --- doc/emacs/macos.texi | 6 - etc/NEWS | 4 +- src/nsterm.h | 5 + - src/nsterm.m | 266 +++++++++++++++++++++++++++++++++++++++++-- - 4 files changed, 263 insertions(+), 18 deletions(-) + src/nsterm.m | 265 +++++++++++++++++++++++++++++++++++++++++-- + 4 files changed, 262 insertions(+), 18 deletions(-) diff --git a/doc/emacs/macos.texi b/doc/emacs/macos.texi -index 4825cf9..97777e2 100644 +index 6514dfc..f47929e 100644 --- a/doc/emacs/macos.texi +++ b/doc/emacs/macos.texi @@ -278,7 +278,6 @@ restart Emacs to access newly-available services. @@ -86,10 +63,10 @@ index 2b1f9e6..8a40850 100644 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 2102fb9..dd98d56 100644 +index 483fed3..8bf867a 100644 --- a/src/nsterm.h +++ b/src/nsterm.h -@@ -594,6 +594,10 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType) +@@ -595,6 +595,10 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType) Lisp_Object lastRootWindow; BOOL accessibilityTreeValid; BOOL accessibilityUpdating; @@ -100,7 +77,7 @@ index 2102fb9..dd98d56 100644 #endif BOOL font_panel_active; NSFont *font_panel_result; -@@ -663,6 +667,7 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType) +@@ -664,6 +668,7 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType) - (void)rebuildAccessibilityTree; - (void)invalidateAccessibilityTree; - (void)postAccessibilityUpdates; @@ -109,10 +86,10 @@ index 2102fb9..dd98d56 100644 @end diff --git a/src/nsterm.m b/src/nsterm.m -index a3104d0..6e8a226 100644 +index 20ba0b9..f911d93 100644 --- a/src/nsterm.m +++ b/src/nsterm.m -@@ -7398,6 +7398,112 @@ visual line index for Zoom (skip whitespace-only lines +@@ -7415,6 +7415,112 @@ visual line index for Zoom (skip whitespace-only lines return nil; } @@ -225,7 +202,7 @@ index a3104d0..6e8a226 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 -@@ -8142,16 +8248,25 @@ - (void)ensureTextCache +@@ -8159,16 +8265,25 @@ - (void)ensureTextCache if (!b) return; @@ -259,7 +236,7 @@ index a3104d0..6e8a226 100644 && cachedTextStart == BUF_BEGV (b) && pt >= cachedTextStart && (textLen == 0 -@@ -8167,7 +8282,7 @@ included in the cached AX text (it is handled separately via +@@ -8184,7 +8299,7 @@ included in the cached AX text (it is handled separately via { [cachedText release]; cachedText = [text retain]; @@ -268,7 +245,7 @@ index a3104d0..6e8a226 100644 cachedTextStart = start; if (visibleRuns) -@@ -9154,6 +9269,7 @@ - (void)postCompletionAnnouncementForBuffer:(struct buffer *)b +@@ -9175,6 +9290,7 @@ - (void)postCompletionAnnouncementForBuffer:(struct buffer *)b ptrdiff_t currentOverlayStart = 0; ptrdiff_t currentOverlayEnd = 0; @@ -276,15 +253,7 @@ index a3104d0..6e8a226 100644 specpdl_ref count2 = SPECPDL_INDEX (); record_unwind_current_buffer (); if (b != current_buffer) -@@ -9312,6 +9428,7 @@ - (void)postCompletionAnnouncementForBuffer:(struct buffer *)b - self.cachedCompletionOverlayEnd = 0; - self.cachedCompletionPoint = 0; - } -+ unblock_input (); - } - - /* ---- Notification dispatch (main entry point) ---- */ -@@ -9908,6 +10025,10 @@ - (void)dealloc +@@ -9930,6 +10046,10 @@ - (void)dealloc #endif [accessibilityElements release]; @@ -295,7 +264,7 @@ index a3104d0..6e8a226 100644 [[self menu] release]; [super dealloc]; } -@@ -11357,6 +11478,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f +@@ -11379,6 +11499,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f windowClosing = NO; processingCompose = NO; @@ -305,7 +274,7 @@ index a3104d0..6e8a226 100644 scrollbarsNeedingUpdate = 0; fs_state = FULLSCREEN_NONE; fs_before_fs = next_maximized = -1; -@@ -12665,6 +12789,80 @@ - (id)accessibilityFocusedUIElement +@@ -12687,6 +12810,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. */ @@ -386,7 +355,7 @@ index a3104d0..6e8a226 100644 - (void)postAccessibilityUpdates { NSTRACE ("[EmacsView postAccessibilityUpdates]"); -@@ -12675,11 +12873,59 @@ - (void)postAccessibilityUpdates +@@ -12697,11 +12894,59 @@ - (void)postAccessibilityUpdates /* Re-entrance guard: VoiceOver callbacks during notification posting can trigger redisplay, which calls ns_update_end, which calls us