Compare commits

10 Commits

Author SHA1 Message Date
Martin Sukany
3d5d9a3f1d Fix VoiceOver losing focus after completion popup closes
When a child frame completion popup appeared, VoiceOver could
navigate into its accessibility tree.  When the child frame
was then hidden or destroyed, VoiceOver held a stale reference
to the child frame's elements and stopped announcing typed
characters in the parent frame.

The root cause was that EmacsView reported all child frame
views as accessible elements (isAccessibilityElement=YES)
with a full accessibility tree.  VoiceOver tracked into
these elements; when they were destroyed, it lost track of
the parent frame's buffer element.

Fix this following macOS accessibility best practices for
transient popup UI:

1. Make child frame EmacsView invisible to VoiceOver.
2. Post UIElementDestroyedNotification on child frame close.
3. Always post FocusedUIElementChanged on the parent.

* src/nsterm.m (accessibilityRole): Return nil for child
frame EmacsView instances.
(isAccessibilityElement): Return NO for child frames.
(accessibilityChildren): Return empty array for child
frames.
(accessibilityFocusedUIElement): Return self for child
frames.
(ns_ax_maybe_refocus_after_child_frame_close): Rewrite.
Post UIElementDestroyedNotification on the child view.
Post FocusedUIElementChanged unconditionally.
(ns_make_frame_invisible): Update comment.
(ns_destroy_window): Update comment.
(postAccessibilityUpdates): Simplify child-frame-gone
path; always post FocusedUIElementChanged.
2026-04-17 14:11:40 +02:00
3bce527500 ns: announce child frame and echo area content to VoiceOver
Add VoiceOver announcements for child-frame completion popups and
echo-area messages.

* doc/emacs/macos.texi: Rewrite Accessibility section; add echo area
and completion suppression documentation.
* src/nsterm.h: Add child-frame and echo-area tracking ivars;
declare postEchoAreaAnnouncementIfNeeded and
announceChildFrameCompletion methods.
* src/nsterm.m (NS_AX_ECHO_SUPPRESS_SECONDS): New macro.
(ns_ax_selected_child_frame_text): New static function.
(ns_ax_maybe_refocus_after_child_frame_close): New static
function; restores VoiceOver focus to the parent buffer element
when a child frame completion popup is hidden or destroyed.
(ns_make_frame_invisible): Call refocus helper for child frames.
(ns_destroy_window): Call refocus helper for child frames.
(postEchoAreaAnnouncementIfNeeded): New EmacsView method.
(announceChildFrameCompletion): New EmacsView method.
(postAccessibilityUpdates): Major expansion; dispatch echo
area and child-frame announcements, track cursor-movement
timestamps.
(postAccessibilityNotificationsForFrame:): Clear
cachedCompletionAnnouncement when overlay candidate is nil.
(EmacsAccessibilityBuffer): Add accessibilityChildren method.
Various code quality improvements: eassert guards, specbind
inhibit-quit, memory attribute fixes.
2026-04-17 14:11:40 +02:00
3c631d29d4 ns: announce overlay completions to VoiceOver
Add VoiceOver announcements for overlay-based completion frameworks
(e.g. vertico, icomplete) so the selected candidate is spoken.

* src/nsterm.m (ns_ax_scan_string_lines): New static function.
(ns_ax_selected_overlay_text): New static function; finds and
extracts the selected overlay candidate text.
(EmacsAccessibilityBuffer): Add accessibilityStringForRange: and
accessibilityAttributedStringForRange: methods.
(postAccessibilityNotificationsForFrame:): Add overlay completion
announcement branch; check BUF_OVERLAY_MODIFF independently.
(postAccessibilityUpdates): Update comment about tree rebuild.
2026-04-17 14:11:40 +02:00
5e35667794 doc: add VoiceOver section to macOS appendix
* doc/emacs/emacs.texi: Add Accessibility to top-level menu.
* doc/emacs/macos.texi: New node documenting VoiceOver navigation,
Zoom cursor tracking, ns-accessibility-enabled variable, and known
limitations.
2026-04-17 14:11:39 +02:00
aa17b8b340 ns: wire accessibility into EmacsView
Connect the virtual accessibility tree to EmacsView so VoiceOver can
discover and navigate Emacs content.

* etc/NEWS: Add VoiceOver documentation block.
* src/nsterm.h: Declare rebuildAccessibilityTree,
invalidateAccessibilityTree, postAccessibilityUpdates.
* src/nsterm.m (NS_AX_MAX_COMPLETION_CHARS): New macro.
(ns_ax_post_notification): New static inline function.
(ns_update_accessibility_state): New EmacsApp method.
(ns_accessibility_did_change:): New handler.
(ns_ax_collect_windows): New static function; initialize
cachedBuffer to NULL so the buffer-switch branch fires on the first
notification cycle.
(rebuildAccessibilityTree): New EmacsView method.
(invalidateAccessibilityTree): New EmacsView method.
(postAccessibilityUpdates): New EmacsView method; post
SelectedTextChanged alongside FocusedUIElementChanged after tree
rebuild so VoiceOver learns the cursor position immediately.
(ns_update_end): Call postAccessibilityUpdates.
(applicationDidFinishLaunching:): Register AT observer.
(dealloc): Release accessibility elements.
(windowDidBecomeKey): Post VoiceOver focus notification.
(initFrameFromEmacs:): Initialize accessibility ivars.
* test/src/nsterm-accessibility-tests.el: New file.
2026-04-17 14:11:39 +02:00
fb88f2a9dc ns: add interactive span elements for Tab navigation
Implement EmacsAccessibilityInteractiveSpan, exposed as AXButton
children of EmacsAccessibilityBuffer so VoiceOver Tab navigation can
reach individual interactive elements (buttons, links, completion items).

* src/nsterm.h (EmacsAccessibilityInteractiveSpan): Remove spanValue
property (redundant with accessibilityLabel).
* src/nsterm.m (ns_ax_window_buffer_object): New static function.
(ns_ax_window_end_charpos): New static function.
(ns_ax_text_prop_at): New static function.
(ns_ax_next_prop_change): New static function.
(ns_ax_get_span_label): New static function; builds label for
interactive span, handling invisible text.
(ns_ax_scan_interactive_spans): New static function; scans buffer
for interactive spans.
(EmacsAccessibilityInteractiveSpan): Full implementation.
(EmacsAccessibilityBuffer InteractiveSpans): Category adding
accessibilityChildrenInNavigationOrder and invalidateInteractiveSpans.
(EmacsAccessibilityBuffer dealloc): Add span cleanup.
(syms_of_nsterm): Add DEFSYMs for widget/button/completion-list.
2026-04-17 14:11:39 +02:00
2371c8d694 ns: add AX notifications and mode-line element
Add the notification engine that posts AX events after redisplay, and
implement the mode-line accessibility element.

* src/nsterm.h (EmacsAccessibilityBuffer): Add cachedCharsModiff
property; declare cursor and completion notification methods.
* src/nsterm.m (ns_ax_mode_line_text): New static function; extracts
mode-line text from glyph matrix.
(ns_ax_completion_string_from_prop): New static function.
(ns_ax_find_completion_overlay_range): New static function.
(ns_ax_event_is_line_nav_key): New static function.
(ns_ax_completion_text_for_span): New static function.
(EmacsAccessibilityBuffer Notifications): Category implementing
postTextChangedNotification, postFocusedCursorNotification,
postCompletionAnnouncementForBuffer, and
postAccessibilityNotificationsForFrame.
(postAccessibilityNotificationsForFrame:): Detect buffer switch and
reset cached state with explicit VoiceOver notifications, so that
split-window scenarios do not leave VoiceOver silent.
(EmacsAccessibilityModeLine): Full implementation.
(syms_of_nsterm): Add DEFSYMs for navigation and completion symbols.
2026-04-17 14:11:39 +02:00
623b974e56 ns: implement buffer accessibility element
Implement EmacsAccessibilityBuffer, a virtual NSAccessibility text
element (AXTextArea role) that exposes buffer contents to VoiceOver.

* src/nsterm.m (ns_ax_free_runs_indirect): New unwind helper.
(ns_ax_buffer_text): New static function; builds visible text and
run array for accessibility clients.
(ns_ax_frame_for_range): New static function; maps char ranges to
screen rects.
(ns_ax_post_notification_with_info): New static inline; async AX
notification posting.
(EmacsAccessibilityBuffer): Full NSAccessibility text protocol
implementation including insertion point, selection, cursor
position, styled ranges, and frame-for-range.
2026-04-17 14:11:39 +02:00
6882b9a643 ns: add accessibility base classes and helpers
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 Objective-C class.
(EmacsAccessibilityBuffer): New class.
(EmacsAccessibilityModeLine): New class.
(EmacsAccessibilityInteractiveSpan): New class.
(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.
(EmacsAccessibilityElement): Implement base class.
(syms_of_nsterm): Add DEFVAR_BOOL ns-accessibility-enabled; initial
value is nil, set non-nil automatically when an assistive technology
(AT) is detected at startup.
2026-04-17 14:11:39 +02:00
c4e6ef142b ns: track completion candidates for macOS Zoom
When a completion framework displays candidates (overlay-based or
child-frame popup), override Zoom focus to the selected candidate
so the zoomed viewport follows the user's selection rather than the
text cursor.

* src/nsterm.m (NS_ZOOM_MAX_COMPLETION_CHARS): New macro.
(ns_face_name_matches_selected_p_1): New static helper; recursive
face name match with depth-limit guard.
(ns_face_name_matches_selected_p): New static predicate; matches
'current', 'selected', 'selection' in face names.
(ns_zoom_find_overlay_candidate_line): New static function; scans
minibuffer overlays for selected completion candidate.
(ns_zoom_find_child_frame_candidate): New static function; scans
child frame buffers for selected candidate.
(ns_zoom_track_completion): New static function; overrides Zoom
focus to selected completion candidate.
(ns_update_end): Call ns_zoom_track_completion.
* etc/NEWS: Document completion tracking for Zoom.
2026-04-17 14:11:27 +02:00
6 changed files with 5388 additions and 9 deletions

View File

@@ -1286,6 +1286,7 @@ Emacs and macOS / GNUstep
* Mac / GNUstep Basics:: Basic Emacs usage under GNUstep or macOS. * Mac / GNUstep Basics:: Basic Emacs usage under GNUstep or macOS.
* Mac / GNUstep Customization:: Customizations under GNUstep or macOS. * Mac / GNUstep Customization:: Customizations under GNUstep or macOS.
* Mac / GNUstep Events:: How window system events are handled. * Mac / GNUstep Events:: How window system events are handled.
* Accessibility:: Screen reader and Zoom support on macOS.
* GNUstep Support:: Details on status of GNUstep support. * GNUstep Support:: Details on status of GNUstep support.
Emacs and Haiku Emacs and Haiku

View File

@@ -36,6 +36,7 @@ Support}), but we hope to improve it in the future.
* Mac / GNUstep Basics:: Basic Emacs usage under GNUstep or macOS. * Mac / GNUstep Basics:: Basic Emacs usage under GNUstep or macOS.
* Mac / GNUstep Customization:: Customizations under GNUstep or macOS. * Mac / GNUstep Customization:: Customizations under GNUstep or macOS.
* Mac / GNUstep Events:: How window system events are handled. * Mac / GNUstep Events:: How window system events are handled.
* Accessibility:: Screen reader and Zoom support on macOS.
* GNUstep Support:: Details on status of GNUstep support. * GNUstep Support:: Details on status of GNUstep support.
@end menu @end menu
@@ -272,6 +273,107 @@ 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 services and receive the results back. Note that you may need to
restart Emacs to access newly-available services. restart Emacs to access newly-available services.
@node Accessibility
@section Accessibility (macOS)
@cindex VoiceOver
@cindex accessibility (macOS)
@cindex screen reader (macOS)
@cindex Zoom, cursor tracking (macOS)
@cindex accessibility, Zoom (macOS)
@cindex echo area, accessibility (macOS)
@cindex completion announcements, accessibility (macOS)
When built with the Cocoa interface on macOS, Emacs exposes buffer
content, cursor position, mode lines, and interactive elements to the
macOS accessibility subsystem. This enables use with VoiceOver,
Apple's built-in screen reader, and with other assistive technology
such as macOS Zoom.
Toggle VoiceOver with @kbd{Cmd-F5} (or via System Settings,
Accessibility, VoiceOver). When Emacs is focused, VoiceOver announces
the buffer name and current line. Standard Emacs navigation produces
speech feedback:
@itemize @bullet
@item
Arrow keys read individual characters (left/right) or full lines
(up/down).
@item
@kbd{M-f} and @kbd{M-b} announce words.
@item
@kbd{C-n} and @kbd{C-p} read the destination line.
@item
Shift-modified movement announces selected or deselected text.
@item
@key{TAB} and @kbd{S-@key{TAB}} navigate interactive elements
(buttons, completion candidates) within a buffer.
@end itemize
The @file{*Completions*} buffer announces each completion candidate
as you navigate, even while keyboard focus remains in the minibuffer.
Echo area messages are announced automatically. When a background
operation completes and displays a message (e.g., @samp{Git finished},
@samp{Wrote file}), VoiceOver reads it without requiring any action.
Messages are suppressed while the minibuffer is active (i.e., while
you are typing a command) to avoid interrupting prompt reading.
During keyboard navigation, echo area messages are temporarily
suppressed (for 0.5 seconds) to prevent them from interrupting the
cursor-position announcement.
VoiceOver's rotor browse cursor stays synchronized with the Emacs
cursor after large programmatic jumps (for example, heading navigation
in Org mode, @code{xref-find-definitions}, or @code{imenu}).
@subheading Zoom Cursor Tracking
When macOS Zoom is enabled (System Settings, Accessibility, Zoom,
with @samp{Follow keyboard focus} active), Emacs reports its cursor
position to the Zoom subsystem after every redisplay cycle. The
zoomed viewport automatically tracks the Emacs insertion point as you
type or navigate. During completion, Zoom follows the selected
candidate rather than the text cursor. This integration is always
active when Zoom is enabled; no Emacs-side toggle is needed.
@vindex ns-accessibility-enabled
@cindex disabling accessibility (macOS)
To disable the VoiceOver accessibility interface (e.g., to
eliminate overhead on systems where a screen reader is not in use),
set @code{ns-accessibility-enabled} to @code{nil}. The default value
is @code{nil}; Emacs sets it to @code{t} automatically at startup if
it detects an active assistive technology. Zoom cursor tracking
operates independently and is not affected by this variable.
@subheading Known Limitations
@itemize @bullet
@item
Very large buffers (tens of megabytes) may cause slow initial
accessibility text extraction. Once cached, subsequent queries
are fast.
@item
Mode-line text extraction handles only character glyphs. Mode lines
using icon fonts (e.g., icon-based mode lines)
produce incomplete accessibility text.
@item
Window configuration changes (splits, deletions, new buffers) trigger
a full rebuild of the accessibility virtual element tree, which may
cause a brief pause in VoiceOver announcements.
@item
Right-to-left (bidi) text is exposed correctly as buffer content,
but hit-testing of screen positions to character ranges assumes
left-to-right layout.
@item
Completion popup tracking (for Zoom and VoiceOver) is limited to
buffers smaller than 10,000 characters. Larger buffers are assumed
not to be completion popups and are skipped to avoid excessive work
per redisplay cycle.
@end itemize
This support is available only on the Cocoa build. GNUstep has a
different accessibility model and is not yet supported.
@node GNUstep Support @node GNUstep Support
@section GNUstep Support @section GNUstep Support

View File

@@ -4544,6 +4544,24 @@ accessibility features. Enable Emacs via (System Settings, Privacy &
Security, Accessibility...) and add the Emacs.app installed directory to Security, Accessibility...) and add the Emacs.app installed directory to
the enabled application list. the enabled application list.
+++
** VoiceOver accessibility support on macOS.
Emacs now exposes buffer content, cursor position, and interactive
elements to the macOS accessibility subsystem (VoiceOver). Standard
navigation keys produce speech feedback: arrow keys read characters
and lines, 'M-f'/'M-b' announce words, and shift-modified movement
reports selected text. Echo area messages (e.g., "Wrote file",
"Compilation finished") are announced automatically without user
interaction. The VoiceOver rotor cursor stays synchronized after
large programmatic jumps such as xref, imenu, or Org heading
navigation. Pressing 'TAB' navigates interactive spans (buttons,
completion candidates) within a buffer. Completion
frameworks that render via overlays or child frames announce the
selected candidate.
Set 'ns-accessibility-enabled' to nil to disable the accessibility
interface and eliminate the associated overhead.
--- ---
** Process execution has been optimized on Android. ** Process execution has been optimized on Android.
The run-time performance of subprocesses on recent Android releases, The run-time performance of subprocesses on recent Android releases,
@@ -4679,11 +4697,3 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
Local variables:
coding: utf-8
mode: outline
mode: emacs-news
paragraph-separate: "[ ]"
end:

View File

@@ -453,6 +453,143 @@ enum ns_return_frame_mode
@end @end
/* ==========================================================================
Accessibility virtual elements (macOS / Cocoa only)
========================================================================== */
#ifdef NS_IMPL_COCOA
@class EmacsView;
/* Base class for virtual accessibility elements attached to EmacsView. */
@interface EmacsAccessibilityElement : NSAccessibilityElement
@property (nonatomic, unsafe_unretained) EmacsView *emacsView;
/* Lisp window object --- safe across GC cycles.
GC safety: these Lisp_Objects are NOT visible to GC via staticpro
or the specpdl stack. This is safe because:
(1) Emacs GC runs only on the main thread, at well-defined safe
points during Lisp evaluation --- never during redisplay.
(2) Accessibility elements are owned by EmacsView which belongs to
an active frame; windows referenced here are always reachable
from the frame's window tree until rebuildAccessibilityTree
updates them during the next redisplay cycle.
(3) AX getters dispatch_sync to main before accessing Lisp state,
so GC cannot run concurrently with any access to lispWindow.
(4) validWindow checks WINDOW_LIVE_P before dereferencing.
Note: this relies on non-moving GC. If Emacs adopts a moving
collector (MPS), these fields must be registered as GC roots. */
@property (nonatomic, assign) Lisp_Object lispWindow;
- (struct window *)validWindow; /* Returns live window or NULL. */
- (NSRect)screenRectFromEmacsX:(int)x y:(int)y width:(int)w height:(int)h;
@end
/* A visible run: maps a contiguous range of accessibility indices
to a contiguous range of buffer character positions. Invisible
text is skipped, so ax_start values are consecutive across runs
while charpos values may have gaps. */
typedef struct ns_ax_visible_run
{
ptrdiff_t charpos; /* Buffer charpos where this visible run starts. */
ptrdiff_t length; /* Number of visible Emacs characters in this run. */
NSUInteger ax_start; /* Starting index in the accessibility string. */
NSUInteger ax_length; /* Length in accessibility string (UTF-16 units). */
} ns_ax_visible_run;
/* Virtual AXTextArea element --- one per visible Emacs window (buffer). */
@interface EmacsAccessibilityBuffer
: EmacsAccessibilityElement <NSAccessibility>
{
ns_ax_visible_run *visibleRuns;
NSUInteger visibleRunCount;
NSUInteger *lineStartOffsets; /* AX index for each line. */
NSUInteger lineCount; /* Entries in lineStartOffsets. */
NSMutableArray *cachedInteractiveSpans;
BOOL interactiveSpansDirty;
/* Deferred overlay visibility detection. The overlay notification
branch invalidates the text cache and records the old visible text
length here; ensureTextCache posts ValueChanged after rebuild if
the length changed. This avoids running Lisp (ns_ax_buffer_text)
synchronously in the notification dispatch path, which interferes
with redisplay state. */
BOOL pendingOverlayCheck;
NSUInteger pendingOverlayTextLen;
}
@property (nonatomic, copy) NSString *cachedText;
@property (nonatomic, assign) modiff_count cachedTextModiff;
@property (nonatomic, assign) modiff_count cachedOverlayModiff;
@property (nonatomic, assign) ptrdiff_t cachedTextStart;
@property (nonatomic, assign) modiff_count cachedModiff;
@property (nonatomic, assign) modiff_count cachedCharsModiff;
@property (nonatomic, assign) ptrdiff_t cachedPoint;
@property (nonatomic, assign) BOOL cachedMarkActive;
@property (nonatomic, assign) struct buffer *cachedBuffer;
@property (nonatomic, copy) NSString *cachedCompletionAnnouncement;
@property (nonatomic, assign) ptrdiff_t cachedCompletionOverlayStart;
@property (nonatomic, assign) ptrdiff_t cachedCompletionOverlayEnd;
@property (nonatomic, assign) ptrdiff_t cachedCompletionPoint;
@property (nonatomic, assign) BOOL pendingEditNotification;
- (void)invalidateTextCache;
- (NSInteger)lineForAXIndex:(NSUInteger)idx;
- (NSRange)rangeForLine:(NSUInteger)line textLength:(NSUInteger)tlen;
- (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx;
- (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos;
@end
@interface EmacsAccessibilityBuffer (Notifications)
- (void)postTextChangedNotification:(ptrdiff_t)point;
- (void)postFocusedCursorNotification:(ptrdiff_t)point
direction:(NSInteger)direction
granularity:(NSInteger)granularity
markActive:(BOOL)markActive
oldMarkActive:(BOOL)oldMarkActive;
- (void)postCompletionAnnouncementForBuffer:(struct buffer *)b
point:(ptrdiff_t)point;
- (void)postAccessibilityNotificationsForFrame:(struct frame *)f;
@end
@interface EmacsAccessibilityBuffer (InteractiveSpans)
- (void)invalidateInteractiveSpans;
@end
/* Virtual AXStaticText element --- one per mode line. */
@interface EmacsAccessibilityModeLine : EmacsAccessibilityElement
@end
/* Span types for interactive AX child elements.
Used to distinguish span types during scanning; all map to AXButton role at present. */
typedef NS_ENUM(NSInteger, EmacsAXSpanType)
{
EmacsAXSpanTypeNone = -1,
EmacsAXSpanTypeButton = 0,
EmacsAXSpanTypeCompletionItem = 1,
EmacsAXSpanTypeWidget = 2,
};
/* A lightweight AX element representing one interactive text span
(button, link, checkbox, completion candidate, etc.) within a buffer
window. Exposed as AX child of EmacsAccessibilityBuffer so VoiceOver
Tab navigation can reach individual interactive elements. */
@interface EmacsAccessibilityInteractiveSpan : EmacsAccessibilityElement
@property (nonatomic, assign) ptrdiff_t charposStart;
@property (nonatomic, assign) ptrdiff_t charposEnd;
@property (nonatomic, assign) EmacsAXSpanType spanType;
@property (nonatomic, copy) NSString *spanLabel;
@property (nonatomic, unsafe_unretained)
EmacsAccessibilityBuffer *parentBuffer;
- (NSAccessibilityRole) accessibilityRole;
- (NSString *) accessibilityLabel;
- (NSRect) accessibilityFrame;
- (BOOL) isAccessibilityElement;
- (BOOL) isAccessibilityFocused;
- (void) setAccessibilityFocused: (BOOL) focused;
@end
#endif /* NS_IMPL_COCOA */
/* ========================================================================== /* ==========================================================================
The main Emacs view The main Emacs view
@@ -471,6 +608,37 @@ enum ns_return_frame_mode
#ifdef NS_IMPL_COCOA #ifdef NS_IMPL_COCOA
char *old_title; char *old_title;
BOOL maximizing_resize; BOOL maximizing_resize;
NSMutableArray *accessibilityElements;
/* See GC safety comment on EmacsAccessibilityElement.lispWindow. */
Lisp_Object lastSelectedWindow;
Lisp_Object lastRootWindow;
BOOL accessibilityTreeValid;
int lastLeafCount;
@public /* Accessed by ns_ax_post_updates_error (C function). */
BOOL accessibilityUpdating;
BOOL childFrameCompletionActive;
char *childFrameLastCandidate;
char *childFrameLastBufferName;
modiff_count childFrameLastModiff;
/* Last BUF_CHARS_MODIFF seen for echo_area_buffer[0]. Used by
postEchoAreaAnnouncementIfNeeded to detect new echo area messages
independently of the per-element notification cycle. */
modiff_count lastEchoCharsModiff;
/* Last known point of the selected window's buffer. Used to detect
cursor movement and suppress echo area announcements during
navigation (which would interrupt line reading). */
ptrdiff_t lastSelectedBufferPoint;
/* Timestamp of the most recent cursor movement. Echo area
announcements are suppressed for NS_AX_ECHO_SUPPRESS_SECONDS
after the last movement so that asynchronous updates (eldoc,
which-function-mode) arriving in subsequent redisplay cycles
do not interrupt VoiceOver line reading. */
NSTimeInterval lastCursorMoveTimestamp;
/* Timestamp of the most recent text edit (typing). Child frame
completion announcements are suppressed for a brief window after
text edits to prevent the completion candidate announcement from
interrupting VoiceOver character echo. */
NSTimeInterval lastTextEditTimestamp;
#endif #endif
BOOL font_panel_active; BOOL font_panel_active;
NSFont *font_panel_result; NSFont *font_panel_result;
@@ -528,6 +696,15 @@ enum ns_return_frame_mode
- (void)windowWillExitFullScreen; - (void)windowWillExitFullScreen;
- (void)windowDidExitFullScreen; - (void)windowDidExitFullScreen;
- (void)windowDidBecomeKey; - (void)windowDidBecomeKey;
#ifdef NS_IMPL_COCOA
/* Accessibility support. */
- (void)rebuildAccessibilityTree;
- (void)invalidateAccessibilityTree;
- (void)postAccessibilityUpdates;
- (void)postEchoAreaAnnouncementIfNeeded;
- (void)announceChildFrameCompletion;
#endif
@end @end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,257 @@
;;; nsterm-accessibility-tests.el --- ERT tests for NS accessibility -*- lexical-binding: t; -*-
;; Copyright (C) 2026 Free Software Foundation, Inc.
;; Author: Martin Sukany <martin@sukany.cz>
;; Keywords: accessibility, tests
;; This file is part of GNU Emacs.
;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; Tests for the NS (macOS Cocoa) accessibility interface.
;;
;; These tests verify:
;; - The `ns-accessibility-enabled' variable exists and is configurable
;; - DEFSYM symbols used by the accessibility code are correctly interned
;; - The accessibility code does not break standard Emacs operations
;;
;; Running:
;; make -C test TESTS=src/nsterm-accessibility-tests
;; Or from a running Emacs:
;; M-x ert RET t RET
;;
;; Note: GUI-dependent tests (VoiceOver interaction, Zoom tracking)
;; cannot be automated and must be tested manually. See the
;; ``Accessibility'' section in the macOS appendix of the Emacs manual.
;;; Code:
(require 'ert)
;; These tests only make sense on NS (Cocoa) builds.
(defvar ns-accessibility-tests-skip
(not (eq window-system 'ns))
"Non-nil means skip NS-specific tests (not an NS build).")
;;; --- Variable existence and type tests ---
(ert-deftest ns-accessibility-enabled-exists ()
"Test that `ns-accessibility-enabled' is bound."
(skip-unless (not ns-accessibility-tests-skip))
(should (boundp 'ns-accessibility-enabled)))
(ert-deftest ns-accessibility-enabled-is-boolean ()
"Test that `ns-accessibility-enabled' holds a boolean value."
(skip-unless (not ns-accessibility-tests-skip))
(should (booleanp ns-accessibility-enabled)))
(ert-deftest ns-accessibility-enabled-settable ()
"Test that `ns-accessibility-enabled' can be toggled."
(skip-unless (not ns-accessibility-tests-skip))
(let ((original ns-accessibility-enabled))
(unwind-protect
(progn
(setq ns-accessibility-enabled t)
(should (eq ns-accessibility-enabled t))
(setq ns-accessibility-enabled nil)
(should (eq ns-accessibility-enabled nil)))
(setq ns-accessibility-enabled original))))
;;; --- DEFSYM symbol tests ---
;; Verify that the accessibility DEFSYM symbols are correctly interned.
;; These symbols are used for text property scanning in interactive
;; span detection and completion candidate extraction.
;; These symbols are always present in standard Emacs (from widget.el,
;; button.el, etc.). The tests verify that DEFSYM did not use
;; namespaced names like 'ns-ax-widget' which would be wrong.
(ert-deftest ns-ax-defsym-widget ()
"Test that the `widget' symbol used by span scanning is interned."
(should (intern-soft "widget")))
(ert-deftest ns-ax-defsym-button ()
"Test that the `button' symbol used by span scanning is interned."
(should (intern-soft "button")))
(ert-deftest ns-ax-defsym-completion ()
"Test that the `completion' symbol is interned."
(should (intern-soft "completion")))
(ert-deftest ns-ax-defsym-completion-list-mode ()
"Test that `completion-list-mode' symbol is interned."
(should (intern-soft "completion-list-mode")))
(ert-deftest ns-ax-defsym-backtab ()
"Test that `backtab' symbol is interned."
(should (intern-soft "backtab")))
;;; --- Buffer operation safety tests ---
;; Ensure the accessibility code does not interfere with basic
;; buffer operations.
(ert-deftest ns-accessibility-buffer-create-kill ()
"Test that creating and killing buffers works with accessibility enabled."
(skip-unless (not ns-accessibility-tests-skip))
(let ((ns-accessibility-enabled t))
(let ((buf (generate-new-buffer " *ns-ax-test*")))
(should (buffer-live-p buf))
(with-current-buffer buf
(insert "Test content for accessibility.\n")
(should (= (point-max) 33)))
(kill-buffer buf)
(should-not (buffer-live-p buf)))))
(ert-deftest ns-accessibility-large-buffer ()
"Test that large buffers do not cause issues."
(skip-unless (not ns-accessibility-tests-skip))
(let ((ns-accessibility-enabled t))
(let ((buf (generate-new-buffer " *ns-ax-large-test*")))
(unwind-protect
(with-current-buffer buf
;; Insert ~100KB of text
(dotimes (_ 1000)
(insert (make-string 100 ?x) "\n"))
(should (> (buffer-size) 100000))
(goto-char (point-min))
(should (= (point) 1))
(goto-char (point-max))
(should (= (point) (point-max))))
(kill-buffer buf)))))
(ert-deftest ns-accessibility-multibyte-buffer ()
"Test that multibyte content is handled correctly."
(skip-unless (not ns-accessibility-tests-skip))
(let ((ns-accessibility-enabled t))
(let ((buf (generate-new-buffer " *ns-ax-mb-test*")))
(unwind-protect
(with-current-buffer buf
(insert "Hello řeřicha 日本語 🎉\n")
(should (multibyte-string-p (buffer-string)))
(goto-char (point-min))
(forward-word)
(should (> (point) 1)))
(kill-buffer buf)))))
(ert-deftest ns-accessibility-invisible-text ()
"Test that invisible text does not break buffer operations."
(skip-unless (not ns-accessibility-tests-skip))
(let ((ns-accessibility-enabled t))
(let ((buf (generate-new-buffer " *ns-ax-invis-test*")))
(unwind-protect
(with-current-buffer buf
(insert "visible")
(let ((start (point)))
(insert "invisible")
(put-text-property start (point) 'invisible t))
(insert "visible again\n")
;; Buffer should contain all text
(should (string-match "invisible"
(buffer-substring-no-properties
(point-min) (point-max))))
;; But invisible text should be invisible
(goto-char (point-min))
(should (not (get-text-property 1 'invisible)))
(should (get-text-property 8 'invisible)))
(kill-buffer buf)))))
;;; --- Widget/button text property tests ---
;; Verify that text properties used by span scanning work correctly.
(ert-deftest ns-accessibility-widget-property ()
"Test that `widget' text property can be set and read."
(let ((buf (generate-new-buffer " *ns-ax-widget-test*")))
(unwind-protect
(with-current-buffer buf
(insert "Click here")
(put-text-property 1 11 'widget '(test-widget))
(should (equal (get-text-property 1 'widget)
'(test-widget)))
(should (null (get-text-property 11 'widget))))
(kill-buffer buf))))
(ert-deftest ns-accessibility-button-property ()
"Test that `button' text property can be set and read."
(let ((buf (generate-new-buffer " *ns-ax-button-test*")))
(unwind-protect
(with-current-buffer buf
(insert "Press me")
(put-text-property 1 9 'button '(test-button))
(should (equal (get-text-property 1 'button)
'(test-button))))
(kill-buffer buf))))
;;; --- Window/frame safety tests ---
;; Note: this test only runs in interactive (GUI) sessions.
;; It is always skipped under 'make check' (batch mode).
(ert-deftest ns-accessibility-window-split-delete ()
"Test that window operations work with accessibility enabled."
(skip-unless (not noninteractive))
(save-window-excursion
(let ((ns-accessibility-enabled t))
(delete-other-windows)
(split-window)
(should (= (length (window-list)) 2))
(delete-other-windows)
(should (= (length (window-list)) 1)))))
(ert-deftest ns-accessibility-minibuffer ()
"Test that minibuffer is accessible."
(should (minibuffer-window))
(should (window-live-p (minibuffer-window))))
;;; --- Completion buffer tests ---
(ert-deftest ns-accessibility-completions-buffer ()
"Test that *Completions* buffer can be created with completion content."
(skip-unless (not ns-accessibility-tests-skip))
(let ((ns-accessibility-enabled t))
(let ((buf (get-buffer-create "*Completions*")))
(unwind-protect
(with-current-buffer buf
(display-completion-list '("alpha" "beta" "gamma"))
(should (string-match "alpha" (buffer-string)))
(should (string-match "gamma" (buffer-string))))
(kill-buffer buf)))))
;;; --- Batch-mode sanity tests ---
;; These tests verify basic variable/symbol sanity on NS builds.
(ert-deftest ns-accessibility-batch-no-crash ()
"Test that accessibility variable access does not crash in batch mode."
(skip-unless (not ns-accessibility-tests-skip))
(should (boundp 'ns-accessibility-enabled))
;; In batch mode, should be nil (no window system)
(when noninteractive
(should (eq ns-accessibility-enabled nil))))
(ert-deftest ns-accessibility-symbols-consistent ()
"Test that DEFSYM symbols match their expected Lisp names.
The C code uses Qns_ax_widget for symbol `widget', etc.
This test ensures the symbol names are standard Emacs names,
not namespaced (e.g., not `ns-ax-widget')."
;; These should be standard symbols that widget.el and button.el use
(should (eq (intern "widget") 'widget))
(should (eq (intern "button") 'button))
(should (eq (intern "completion") 'completion))
;; Verify namespaced versions are NOT the ones used
;; (they would be wrong since no Lisp code sets them)
(should-not (eq (intern "widget") (intern "ns-ax-widget"))))
(provide 'nsterm-accessibility-tests)
;;; nsterm-accessibility-tests.el ends here