patches: review fixes — defvar, method extraction, GC safety, window_end_valid
Review-based improvements: - ns-accessibility-enabled DEFVAR_BOOL (disable AX overhead) - window_end_valid guard in ns_ax_window_end_charpos - GC safety comments on Lisp_Object ObjC ivars - postAccessibilityNotificationsForFrame split into 4 methods - block_input in ns_ax_completion_text_for_span - Fplist_get predicate comment - macos.texi VoiceOver section with defvar docs - README updated with USER OPTION + REVIEW CHANGES sections
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
From 17a100d99a31e0fae9b641c7ce163efd9bf5945b Mon Sep 17 00:00:00 2001
|
From c4c5ae47fd944cc04f7e229c2a66fb44fa9d006e Mon Sep 17 00:00:00 2001
|
||||||
From: Martin Sukany <martin@sukany.cz>
|
From: Martin Sukany <martin@sukany.cz>
|
||||||
Date: Fri, 27 Feb 2026 15:09:15 +0100
|
Date: Fri, 27 Feb 2026 17:48:49 +0100
|
||||||
Subject: [PATCH] ns: implement VoiceOver accessibility for macOS
|
Subject: [PATCH 1/2] ns: implement VoiceOver accessibility for macOS
|
||||||
|
|
||||||
Add comprehensive macOS VoiceOver accessibility support to the NS
|
Add comprehensive macOS VoiceOver accessibility support to the NS
|
||||||
(Cocoa) port. Before this patch, Emacs exposed only a minimal,
|
(Cocoa) port. Before this patch, Emacs exposed only a minimal,
|
||||||
@@ -29,8 +29,6 @@ New types and classes:
|
|||||||
for Tab-navigable interactive spans (buttons, links, checkboxes,
|
for Tab-navigable interactive spans (buttons, links, checkboxes,
|
||||||
completion candidates, Org-mode links, keymap overlays).
|
completion candidates, Org-mode links, keymap overlays).
|
||||||
|
|
||||||
EmacsAXSpanType: enum for span classification.
|
|
||||||
|
|
||||||
New functions:
|
New functions:
|
||||||
|
|
||||||
ns_ax_buffer_text: build accessibility string with visible-run
|
ns_ax_buffer_text: build accessibility string with visible-run
|
||||||
@@ -57,7 +55,7 @@ New functions:
|
|||||||
completions-highlight overlay for completion announcements.
|
completions-highlight overlay for completion announcements.
|
||||||
|
|
||||||
ns_ax_completion_text_for_span: extract announcement text for a
|
ns_ax_completion_text_for_span: extract announcement text for a
|
||||||
completion overlay span.
|
completion overlay span (with block_input/unblock_input protection).
|
||||||
|
|
||||||
EmacsView extensions:
|
EmacsView extensions:
|
||||||
|
|
||||||
@@ -72,10 +70,16 @@ EmacsView extensions:
|
|||||||
ns_draw_phys_cursor: stores cursor rect for Zoom, calls
|
ns_draw_phys_cursor: stores cursor rect for Zoom, calls
|
||||||
UAZoomChangeFocus with correct CG coordinate-space transform.
|
UAZoomChangeFocus with correct CG coordinate-space transform.
|
||||||
|
|
||||||
DEFSYM additions in syms_of_nsterm (ns_ax_ prefix to avoid
|
DEFSYM additions in syms_of_nsterm: line navigation command symbols
|
||||||
collisions): Qns_ax_widget, Qns_ax_button, Qns_ax_follow_link,
|
(Qns_ax_next_line, Qns_ax_previous_line, evil/dired variants) and
|
||||||
Qns_ax_org_link, Qns_ax_completion_list_mode, Qns_ax_completion__string, Qns_ax_completion,
|
span scanning symbols (Qns_ax_widget, Qns_ax_button,
|
||||||
Qns_ax_completions_highlight, Qns_ax_backtab.
|
Qns_ax_follow_link, Qns_ax_org_link, Qns_ax_completion_list_mode,
|
||||||
|
Qns_ax_completion__string, Qns_ax_completion,
|
||||||
|
Qns_ax_completions_highlight, Qns_ax_backtab).
|
||||||
|
|
||||||
|
New user option: ns-accessibility-enabled (default t). When nil,
|
||||||
|
the accessibility virtual element tree is not built and no
|
||||||
|
notifications are posted, eliminating overhead.
|
||||||
|
|
||||||
Threading model: all Lisp calls on main thread; AX getters use
|
Threading model: all Lisp calls on main thread; AX getters use
|
||||||
dispatch_sync to main; index mapping methods are thread-safe (no
|
dispatch_sync to main; index mapping methods are thread-safe (no
|
||||||
@@ -85,16 +89,16 @@ Lisp calls, read only immutable NSString and scalar cache).
|
|||||||
* src/nsterm.h: New class declarations and EmacsView ivar extensions.
|
* src/nsterm.h: New class declarations and EmacsView ivar extensions.
|
||||||
* etc/NEWS: Document VoiceOver accessibility support.
|
* etc/NEWS: Document VoiceOver accessibility support.
|
||||||
---
|
---
|
||||||
etc/NEWS | 11 +
|
etc/NEWS | 13 +
|
||||||
src/nsterm.h | 108 ++
|
src/nsterm.h | 119 ++
|
||||||
src/nsterm.m | 2870 +++++++++++++++++++++++++++++++++++++++++++++++---
|
src/nsterm.m | 3024 +++++++++++++++++++++++++++++++++++++++++++++++---
|
||||||
3 files changed, 2987 insertions(+), 149 deletions(-)
|
3 files changed, 3009 insertions(+), 147 deletions(-)
|
||||||
|
|
||||||
diff --git a/etc/NEWS b/etc/NEWS
|
diff --git a/etc/NEWS b/etc/NEWS
|
||||||
index 7367e3cc..0e4480ad 100644
|
index 7367e3c..608650e 100644
|
||||||
--- a/etc/NEWS
|
--- a/etc/NEWS
|
||||||
+++ b/etc/NEWS
|
+++ b/etc/NEWS
|
||||||
@@ -4374,6 +4374,17 @@ allowing Emacs users access to speech recognition utilities.
|
@@ -4374,6 +4374,19 @@ allowing Emacs users access to speech recognition utilities.
|
||||||
Note: Accepting this permission allows the use of system APIs, which may
|
Note: Accepting this permission allows the use of system APIs, which may
|
||||||
send user data to Apple's speech recognition servers.
|
send user data to Apple's speech recognition servers.
|
||||||
|
|
||||||
@@ -108,15 +112,17 @@ index 7367e3cc..0e4480ad 100644
|
|||||||
+for the *Completions* buffer. The implementation uses a virtual
|
+for the *Completions* buffer. The implementation uses a virtual
|
||||||
+accessibility tree with per-window elements, hybrid SelectedTextChanged
|
+accessibility tree with per-window elements, hybrid SelectedTextChanged
|
||||||
+and AnnouncementRequested notifications, and thread-safe text caching.
|
+and AnnouncementRequested notifications, and thread-safe text caching.
|
||||||
|
+Set 'ns-accessibility-enabled' to nil to disable the accessibility
|
||||||
|
+interface and eliminate the associated overhead.
|
||||||
+
|
+
|
||||||
---
|
---
|
||||||
** Re-introduced dictation, lost in Emacs v30 (macOS).
|
** Re-introduced dictation, lost in Emacs v30 (macOS).
|
||||||
We lost macOS dictation in v30 when migrating to NSTextInputClient.
|
We lost macOS dictation in v30 when migrating to NSTextInputClient.
|
||||||
diff --git a/src/nsterm.h b/src/nsterm.h
|
diff --git a/src/nsterm.h b/src/nsterm.h
|
||||||
index 7c1ee4cf..542e7d59 100644
|
index 7c1ee4c..393fc4c 100644
|
||||||
--- a/src/nsterm.h
|
--- a/src/nsterm.h
|
||||||
+++ b/src/nsterm.h
|
+++ b/src/nsterm.h
|
||||||
@@ -453,6 +453,97 @@ enum ns_return_frame_mode
|
@@ -453,6 +453,110 @@ enum ns_return_frame_mode
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|
||||||
@@ -132,7 +138,18 @@ index 7c1ee4cf..542e7d59 100644
|
|||||||
+/* Base class for virtual accessibility elements attached to EmacsView. */
|
+/* Base class for virtual accessibility elements attached to EmacsView. */
|
||||||
+@interface EmacsAccessibilityElement : NSAccessibilityElement
|
+@interface EmacsAccessibilityElement : NSAccessibilityElement
|
||||||
+@property (nonatomic, unsafe_unretained) EmacsView *emacsView;
|
+@property (nonatomic, unsafe_unretained) EmacsView *emacsView;
|
||||||
+/* Lisp window object — safe across GC cycles. NULL_LISP when unset. */
|
+/* 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. */
|
||||||
+@property (nonatomic, assign) Lisp_Object lispWindow;
|
+@property (nonatomic, assign) Lisp_Object lispWindow;
|
||||||
+- (struct window *)validWindow; /* Returns live window or NULL. */
|
+- (struct window *)validWindow; /* Returns live window or NULL. */
|
||||||
+- (NSRect)screenRectFromEmacsX:(int)x y:(int)y width:(int)w height:(int)h;
|
+- (NSRect)screenRectFromEmacsX:(int)x y:(int)y width:(int)w height:(int)h;
|
||||||
@@ -216,11 +233,12 @@ index 7c1ee4cf..542e7d59 100644
|
|||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
|
|
||||||
The main Emacs view
|
The main Emacs view
|
||||||
@@ -471,6 +562,13 @@ enum ns_return_frame_mode
|
@@ -471,6 +575,14 @@ 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;
|
+ NSMutableArray *accessibilityElements;
|
||||||
|
+ /* See GC safety comment on EmacsAccessibilityElement.lispWindow. */
|
||||||
+ Lisp_Object lastSelectedWindow;
|
+ Lisp_Object lastSelectedWindow;
|
||||||
+ Lisp_Object lastRootWindow;
|
+ Lisp_Object lastRootWindow;
|
||||||
+ BOOL accessibilityTreeValid;
|
+ BOOL accessibilityTreeValid;
|
||||||
@@ -230,7 +248,7 @@ index 7c1ee4cf..542e7d59 100644
|
|||||||
#endif
|
#endif
|
||||||
BOOL font_panel_active;
|
BOOL font_panel_active;
|
||||||
NSFont *font_panel_result;
|
NSFont *font_panel_result;
|
||||||
@@ -528,6 +626,13 @@ enum ns_return_frame_mode
|
@@ -528,6 +640,13 @@ enum ns_return_frame_mode
|
||||||
- (void)windowWillExitFullScreen;
|
- (void)windowWillExitFullScreen;
|
||||||
- (void)windowDidExitFullScreen;
|
- (void)windowDidExitFullScreen;
|
||||||
- (void)windowDidBecomeKey;
|
- (void)windowDidBecomeKey;
|
||||||
@@ -245,7 +263,7 @@ index 7c1ee4cf..542e7d59 100644
|
|||||||
|
|
||||||
|
|
||||||
diff --git a/src/nsterm.m b/src/nsterm.m
|
diff --git a/src/nsterm.m b/src/nsterm.m
|
||||||
index 932d209f..ea2de6f2 100644
|
index 932d209..6b27c6c 100644
|
||||||
--- a/src/nsterm.m
|
--- a/src/nsterm.m
|
||||||
+++ b/src/nsterm.m
|
+++ b/src/nsterm.m
|
||||||
@@ -46,6 +46,7 @@ Updated by Christian Limpach (chris@nice.ch)
|
@@ -46,6 +46,7 @@ Updated by Christian Limpach (chris@nice.ch)
|
||||||
@@ -268,18 +286,19 @@ index 932d209f..ea2de6f2 100644
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
@@ -3232,6 +3238,42 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors.
|
@@ -3232,6 +3238,43 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors.
|
||||||
/* Prevent the cursor from being drawn outside the text area. */
|
/* Prevent the cursor from being drawn outside the text area. */
|
||||||
r = NSIntersectionRect (r, ns_row_rect (w, glyph_row, TEXT_AREA));
|
r = NSIntersectionRect (r, ns_row_rect (w, glyph_row, TEXT_AREA));
|
||||||
|
|
||||||
+#ifdef NS_IMPL_COCOA
|
+#ifdef NS_IMPL_COCOA
|
||||||
+ /* Accessibility: store cursor rect for Zoom and bounds queries.
|
+ /* Accessibility: store cursor rect for Zoom and bounds queries.
|
||||||
|
+ Skipped when ns-accessibility-enabled is nil to avoid overhead.
|
||||||
+ VoiceOver notifications are handled solely by
|
+ VoiceOver notifications are handled solely by
|
||||||
+ postAccessibilityUpdates (called from ns_update_end)
|
+ postAccessibilityUpdates (called from ns_update_end)
|
||||||
+ to avoid duplicate notifications and mid-redisplay fragility. */
|
+ to avoid duplicate notifications and mid-redisplay fragility. */
|
||||||
+ {
|
+ {
|
||||||
+ EmacsView *view = FRAME_NS_VIEW (f);
|
+ EmacsView *view = FRAME_NS_VIEW (f);
|
||||||
+ if (view && on_p && active_p)
|
+ if (view && on_p && active_p && ns_accessibility_enabled)
|
||||||
+ {
|
+ {
|
||||||
+ view->lastAccessibilityCursorRect = r;
|
+ view->lastAccessibilityCursorRect = r;
|
||||||
+
|
+
|
||||||
@@ -311,7 +330,7 @@ index 932d209f..ea2de6f2 100644
|
|||||||
ns_focus (f, NULL, 0);
|
ns_focus (f, NULL, 0);
|
||||||
|
|
||||||
NSGraphicsContext *ctx = [NSGraphicsContext currentContext];
|
NSGraphicsContext *ctx = [NSGraphicsContext currentContext];
|
||||||
@@ -6849,218 +6891,2498 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg
|
@@ -6849,218 +6892,2522 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg
|
||||||
|
|
||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
|
|
||||||
@@ -786,14 +805,6 @@ index 932d209f..ea2de6f2 100644
|
|||||||
+static bool
|
+static bool
|
||||||
+ns_ax_event_is_line_nav_key (int *which)
|
+ns_ax_event_is_line_nav_key (int *which)
|
||||||
+{
|
+{
|
||||||
-
|
|
||||||
-#ifdef NS_IMPL_COCOA
|
|
||||||
- if (!canceled)
|
|
||||||
- font_panel_result = nil;
|
|
||||||
-#endif
|
|
||||||
-
|
|
||||||
- result = font_panel_result;
|
|
||||||
- font_panel_result = nil;
|
|
||||||
+ /* 1. Check Vthis_command for known navigation command symbols.
|
+ /* 1. Check Vthis_command for known navigation command symbols.
|
||||||
+ All symbols are registered via DEFSYM in syms_of_nsterm to avoid
|
+ All symbols are registered via DEFSYM in syms_of_nsterm to avoid
|
||||||
+ per-call obarray lookups in this hot path (runs every cursor move). */
|
+ per-call obarray lookups in this hot path (runs every cursor move). */
|
||||||
@@ -819,12 +830,18 @@ index 932d209f..ea2de6f2 100644
|
|||||||
+ return true;
|
+ return true;
|
||||||
+ }
|
+ }
|
||||||
+ }
|
+ }
|
||||||
+
|
|
||||||
|
-#ifdef NS_IMPL_COCOA
|
||||||
|
- if (!canceled)
|
||||||
|
- font_panel_result = nil;
|
||||||
|
-#endif
|
||||||
+ /* 2. Fallback: check raw key events for Tab/backtab. */
|
+ /* 2. Fallback: check raw key events for Tab/backtab. */
|
||||||
+ Lisp_Object ev = last_command_event;
|
+ Lisp_Object ev = last_command_event;
|
||||||
+ if (CONSP (ev))
|
+ if (CONSP (ev))
|
||||||
+ ev = EVENT_HEAD (ev);
|
+ ev = EVENT_HEAD (ev);
|
||||||
+
|
|
||||||
|
- result = font_panel_result;
|
||||||
|
- font_panel_result = nil;
|
||||||
+ if (SYMBOLP (ev) && EQ (ev, Qns_ax_backtab))
|
+ if (SYMBOLP (ev) && EQ (ev, Qns_ax_backtab))
|
||||||
+ {
|
+ {
|
||||||
+ if (which) *which = -1;
|
+ if (which) *which = -1;
|
||||||
@@ -875,12 +892,16 @@ index 932d209f..ea2de6f2 100644
|
|||||||
|
|
||||||
-- (BOOL)acceptsFirstResponder
|
-- (BOOL)acceptsFirstResponder
|
||||||
+/* Compute visible-end charpos for window W.
|
+/* Compute visible-end charpos for window W.
|
||||||
+ Emacs stores it as BUF_Z - window_end_pos. */
|
+ Emacs stores it as BUF_Z - window_end_pos.
|
||||||
|
+ Falls back to BUF_ZV when window_end_valid is false (e.g., when
|
||||||
|
+ called from an AX getter before the next redisplay cycle). */
|
||||||
+static ptrdiff_t
|
+static ptrdiff_t
|
||||||
+ns_ax_window_end_charpos (struct window *w, struct buffer *b)
|
+ns_ax_window_end_charpos (struct window *w, struct buffer *b)
|
||||||
{
|
{
|
||||||
- NSTRACE ("[EmacsView acceptsFirstResponder]");
|
- NSTRACE ("[EmacsView acceptsFirstResponder]");
|
||||||
- return YES;
|
- return YES;
|
||||||
|
+ if (!w->window_end_valid)
|
||||||
|
+ return BUF_ZV (b);
|
||||||
+ return BUF_Z (b) - w->window_end_pos;
|
+ return BUF_Z (b) - w->window_end_pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -894,6 +915,8 @@ index 932d209f..ea2de6f2 100644
|
|||||||
- [theEvent type], [theEvent clickCount]);
|
- [theEvent type], [theEvent clickCount]);
|
||||||
- return ns_click_through;
|
- return ns_click_through;
|
||||||
+ Lisp_Object plist = Ftext_properties_at (make_fixnum (pos), buf_obj);
|
+ Lisp_Object plist = Ftext_properties_at (make_fixnum (pos), buf_obj);
|
||||||
|
+ /* Third argument to Fplist_get is PREDICATE (Emacs 29+), not a
|
||||||
|
+ default value. Qnil selects the default `eq' comparison. */
|
||||||
+ return Fplist_get (plist, prop, Qnil);
|
+ return Fplist_get (plist, prop, Qnil);
|
||||||
}
|
}
|
||||||
-- (void)resetCursorRects
|
-- (void)resetCursorRects
|
||||||
@@ -970,7 +993,7 @@ index 932d209f..ea2de6f2 100644
|
|||||||
+ NSAccessibilityPostNotification (element, name);
|
+ NSAccessibilityPostNotification (element, name);
|
||||||
+ });
|
+ });
|
||||||
+}
|
+}
|
||||||
+
|
|
||||||
+static inline void
|
+static inline void
|
||||||
+ns_ax_post_notification_with_info (id element,
|
+ns_ax_post_notification_with_info (id element,
|
||||||
+ NSAccessibilityNotificationName name,
|
+ NSAccessibilityNotificationName name,
|
||||||
@@ -980,30 +1003,29 @@ index 932d209f..ea2de6f2 100644
|
|||||||
+ NSAccessibilityPostNotificationWithUserInfo (element, name, info);
|
+ NSAccessibilityPostNotificationWithUserInfo (element, name, info);
|
||||||
+ });
|
+ });
|
||||||
+}
|
+}
|
||||||
+
|
|
||||||
|
-/*****************************************************************************/
|
||||||
|
-/* Keyboard handling. */
|
||||||
|
-#define NS_KEYLOG 0
|
||||||
+/* Scan visible range of window W for interactive spans.
|
+/* Scan visible range of window W for interactive spans.
|
||||||
+ Returns NSArray<EmacsAccessibilityInteractiveSpan *>.
|
+ Returns NSArray<EmacsAccessibilityInteractiveSpan *>.
|
||||||
|
|
||||||
|
-- (void)keyDown: (NSEvent *)theEvent
|
||||||
+ Priority when properties overlap:
|
+ Priority when properties overlap:
|
||||||
+ widget > button > follow-link > org-link >
|
+ widget > button > follow-link > org-link >
|
||||||
+ completion-candidate > keymap-overlay. */
|
+ completion-candidate > keymap-overlay. */
|
||||||
+static NSArray *
|
+static NSArray *
|
||||||
+ns_ax_scan_interactive_spans (struct window *w,
|
+ns_ax_scan_interactive_spans (struct window *w,
|
||||||
+ EmacsAccessibilityBuffer *parent_buf)
|
+ EmacsAccessibilityBuffer *parent_buf)
|
||||||
+{
|
{
|
||||||
|
- Mouse_HLInfo *hlinfo = MOUSE_HL_INFO (emacsframe);
|
||||||
+ if (!w)
|
+ if (!w)
|
||||||
+ return @[];
|
+ return @[];
|
||||||
|
+
|
||||||
-/*****************************************************************************/
|
|
||||||
-/* Keyboard handling. */
|
|
||||||
-#define NS_KEYLOG 0
|
|
||||||
+ Lisp_Object buf_obj = ns_ax_window_buffer_object (w);
|
+ Lisp_Object buf_obj = ns_ax_window_buffer_object (w);
|
||||||
+ if (NILP (buf_obj))
|
+ if (NILP (buf_obj))
|
||||||
+ return @[];
|
+ return @[];
|
||||||
|
+
|
||||||
-- (void)keyDown: (NSEvent *)theEvent
|
|
||||||
-{
|
|
||||||
- Mouse_HLInfo *hlinfo = MOUSE_HL_INFO (emacsframe);
|
|
||||||
+ struct buffer *b = XBUFFER (buf_obj);
|
+ struct buffer *b = XBUFFER (buf_obj);
|
||||||
+ ptrdiff_t vis_start = marker_position (w->start);
|
+ ptrdiff_t vis_start = marker_position (w->start);
|
||||||
+ ptrdiff_t vis_end = ns_ax_window_end_charpos (w, b);
|
+ ptrdiff_t vis_end = ns_ax_window_end_charpos (w, b);
|
||||||
@@ -1278,6 +1300,11 @@ index 932d209f..ea2de6f2 100644
|
|||||||
+ NSString *text = nil;
|
+ NSString *text = nil;
|
||||||
+ specpdl_ref count = SPECPDL_INDEX ();
|
+ specpdl_ref count = SPECPDL_INDEX ();
|
||||||
+ record_unwind_current_buffer ();
|
+ record_unwind_current_buffer ();
|
||||||
|
+ /* Block input to prevent concurrent redisplay from modifying buffer
|
||||||
|
+ state while we read text properties. Unwind-protected so
|
||||||
|
+ block_input is always matched by unblock_input on signal. */
|
||||||
|
+ record_unwind_protect_void (unblock_input);
|
||||||
|
+ block_input ();
|
||||||
+ if (b != current_buffer)
|
+ if (b != current_buffer)
|
||||||
+ set_buffer_internal_1 (b);
|
+ set_buffer_internal_1 (b);
|
||||||
+
|
+
|
||||||
@@ -2214,26 +2241,11 @@ index 932d209f..ea2de6f2 100644
|
|||||||
+ height:text_h];
|
+ height:text_h];
|
||||||
+}
|
+}
|
||||||
+
|
+
|
||||||
+/* ---- Notification dispatch ---- */
|
+/* ---- Notification dispatch (helper methods) ---- */
|
||||||
+
|
+
|
||||||
+- (void)postAccessibilityNotificationsForFrame:(struct frame *)f
|
+/* Post NSAccessibilityValueChangedNotification for a text edit.
|
||||||
+{
|
+ Called when BUF_MODIFF changes between redisplay cycles. */
|
||||||
+ NSTRACE ("[EmacsView postAccessibilityNotificationsForFrame:]");
|
+- (void)postTextChangedNotification:(ptrdiff_t)point
|
||||||
+ struct window *w = [self validWindow];
|
|
||||||
+ if (!w || !WINDOW_LEAF_P (w))
|
|
||||||
+ return;
|
|
||||||
+
|
|
||||||
+ struct buffer *b = XBUFFER (w->contents);
|
|
||||||
+ if (!b)
|
|
||||||
+ return;
|
|
||||||
+
|
|
||||||
+ ptrdiff_t modiff = BUF_MODIFF (b);
|
|
||||||
+ ptrdiff_t point = BUF_PT (b);
|
|
||||||
+ BOOL markActive = !NILP (BVAR (b, mark_active));
|
|
||||||
+
|
|
||||||
+ /* --- Text changed → typing echo ---
|
|
||||||
+ WebKit AXObjectCacheMac fallback enum: Edit = 1, Typing = 3. */
|
|
||||||
+ if (modiff != self.cachedModiff)
|
|
||||||
+{
|
+{
|
||||||
+ /* Capture changed char before invalidating cache. */
|
+ /* Capture changed char before invalidating cache. */
|
||||||
+ NSString *changedChar = @"";
|
+ NSString *changedChar = @"";
|
||||||
@@ -2256,11 +2268,10 @@ index 932d209f..ea2de6f2 100644
|
|||||||
+ [self invalidateTextCache];
|
+ [self invalidateTextCache];
|
||||||
+ }
|
+ }
|
||||||
+
|
+
|
||||||
+ self.cachedModiff = modiff;
|
+ /* Update cachedPoint here so the selection-move branch does NOT
|
||||||
+ /* Update cachedPoint here so the selection-move branch below
|
+ fire for point changes caused by edits. WebKit and Chromium
|
||||||
+ does NOT fire for point changes caused by edits. WebKit and
|
+ never send both ValueChanged and SelectedTextChanged for the
|
||||||
+ Chromium never send both ValueChanged and SelectedTextChanged
|
+ same user action — they are mutually exclusive. */
|
||||||
+ for the same user action — they are mutually exclusive. */
|
|
||||||
+ self.cachedPoint = point;
|
+ self.cachedPoint = point;
|
||||||
+
|
+
|
||||||
+ NSDictionary *change = @{
|
+ NSDictionary *change = @{
|
||||||
@@ -2277,91 +2288,13 @@ index 932d209f..ea2de6f2 100644
|
|||||||
+ self, NSAccessibilityValueChangedNotification, userInfo);
|
+ self, NSAccessibilityValueChangedNotification, userInfo);
|
||||||
+}
|
+}
|
||||||
+
|
+
|
||||||
+ /* --- Cursor moved or selection changed → line reading ---
|
+/* Post SelectedTextChanged and AnnouncementRequested for the
|
||||||
+ WebKit AXObjectCacheMac fallback enum: SelectionMove = 2.
|
+ focused buffer element when point or mark changes. */
|
||||||
+ Use 'else if' — edits and selection moves are mutually exclusive
|
+- (void)postFocusedCursorNotification:(ptrdiff_t)point
|
||||||
+ per the WebKit/Chromium pattern. VoiceOver gets confused if
|
+ direction:(NSInteger)direction
|
||||||
+ both notifications arrive in the same runloop iteration. */
|
+ granularity:(NSInteger)granularity
|
||||||
+ else if (point != self.cachedPoint || markActive != self.cachedMarkActive)
|
+ markActive:(BOOL)markActive
|
||||||
+ {
|
+ oldMarkActive:(BOOL)oldMarkActive
|
||||||
+ ptrdiff_t oldPoint = self.cachedPoint;
|
|
||||||
+ BOOL oldMarkActive = self.cachedMarkActive;
|
|
||||||
+ self.cachedPoint = point;
|
|
||||||
+ self.cachedMarkActive = markActive;
|
|
||||||
+
|
|
||||||
+ /* Compute direction. */
|
|
||||||
+ NSInteger direction = ns_ax_text_selection_direction_discontiguous;
|
|
||||||
+ if (point > oldPoint)
|
|
||||||
+ direction = ns_ax_text_selection_direction_next;
|
|
||||||
+ else if (point < oldPoint)
|
|
||||||
+ direction = ns_ax_text_selection_direction_previous;
|
|
||||||
+
|
|
||||||
+ int ctrlNP = 0;
|
|
||||||
+ bool isCtrlNP = ns_ax_event_is_line_nav_key (&ctrlNP);
|
|
||||||
+
|
|
||||||
+ /* --- Granularity detection ---
|
|
||||||
+ Compare old and new cursor positions in cachedText to determine
|
|
||||||
+ what kind of move happened. Three levels:
|
|
||||||
+ - line: different line (lineRangeForRange)
|
|
||||||
+ - word: same line, distance > 1 UTF-16 unit
|
|
||||||
+ - character: same line, distance == 1 UTF-16 unit
|
|
||||||
+ C-n/C-p force line regardless of detected granularity. */
|
|
||||||
+ NSInteger granularity = ns_ax_text_selection_granularity_unknown;
|
|
||||||
+ [self ensureTextCache];
|
|
||||||
+ NSUInteger oldIdx = 0, newIdx = 0;
|
|
||||||
+ if (cachedText && oldPoint > 0)
|
|
||||||
+ {
|
|
||||||
+ NSUInteger tlen = [cachedText length];
|
|
||||||
+ oldIdx = [self accessibilityIndexForCharpos:oldPoint];
|
|
||||||
+ newIdx = [self accessibilityIndexForCharpos:point];
|
|
||||||
+ if (oldIdx > tlen) oldIdx = tlen;
|
|
||||||
+ if (newIdx > tlen) newIdx = tlen;
|
|
||||||
+
|
|
||||||
+ NSRange oldLine = [cachedText lineRangeForRange:
|
|
||||||
+ NSMakeRange (oldIdx, 0)];
|
|
||||||
+ NSRange newLine = [cachedText lineRangeForRange:
|
|
||||||
+ NSMakeRange (newIdx, 0)];
|
|
||||||
+ if (oldLine.location != newLine.location)
|
|
||||||
+ granularity = ns_ax_text_selection_granularity_line;
|
|
||||||
+ else
|
|
||||||
+ {
|
|
||||||
+ NSUInteger dist = (newIdx > oldIdx
|
|
||||||
+ ? newIdx - oldIdx
|
|
||||||
+ : oldIdx - newIdx);
|
|
||||||
+ if (dist > 1)
|
|
||||||
+ granularity = ns_ax_text_selection_granularity_word;
|
|
||||||
+ else if (dist == 1)
|
|
||||||
+ granularity = ns_ax_text_selection_granularity_character;
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ /* Force line semantics for explicit C-n/C-p / Tab / backtab. */
|
|
||||||
+ if (isCtrlNP)
|
|
||||||
+ {
|
|
||||||
+ direction = (ctrlNP > 0
|
|
||||||
+ ? ns_ax_text_selection_direction_next
|
|
||||||
+ : ns_ax_text_selection_direction_previous);
|
|
||||||
+ granularity = ns_ax_text_selection_granularity_line;
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ /* --- NOTIFICATION STRATEGY ---
|
|
||||||
+ SelectedTextChanged ALWAYS posted for focused element:
|
|
||||||
+ - Interrupts VoiceOver auto-read (buffer switch reading)
|
|
||||||
+ - Provides word/line/selection reading via VoiceOver defaults
|
|
||||||
+
|
|
||||||
+ For CHARACTER moves only: omit granularity from userInfo so
|
|
||||||
+ VoiceOver cannot derive speech from SelectedTextChanged, then
|
|
||||||
+ post AnnouncementRequested with char AT point. This avoids
|
|
||||||
+ double-speech while keeping the interrupt behaviour.
|
|
||||||
+
|
|
||||||
+ For WORD and LINE moves: include granularity in userInfo —
|
|
||||||
+ VoiceOver reads the word/line correctly on its own.
|
|
||||||
+
|
|
||||||
+ For SELECTION changes: include granularity — VoiceOver reads
|
|
||||||
+ selected/deselected text.
|
|
||||||
+
|
|
||||||
+ Non-focused buffers: AnnouncementRequested only (see below). */
|
|
||||||
+ if ([self isAccessibilityFocused])
|
|
||||||
+{
|
+{
|
||||||
+ BOOL isCharMove
|
+ BOOL isCharMove
|
||||||
+ = (!markActive && !oldMarkActive
|
+ = (!markActive && !oldMarkActive
|
||||||
@@ -2423,11 +2356,11 @@ index 932d209f..ea2de6f2 100644
|
|||||||
+ }
|
+ }
|
||||||
+
|
+
|
||||||
+ /* For focused line moves: always announce line text explicitly.
|
+ /* For focused line moves: always announce line text explicitly.
|
||||||
+ SelectedTextChanged with granularity=line works for arrow
|
+ SelectedTextChanged with granularity=line works for arrow keys,
|
||||||
+ keys, but C-n/C-p need the explicit announcement (VoiceOver
|
+ but C-n/C-p need the explicit announcement (VoiceOver processes
|
||||||
+ processes these keystrokes differently from arrows).
|
+ these keystrokes differently from arrows).
|
||||||
+ In completion-list-mode, read the completion candidate
|
+ In completion-list-mode, read the completion candidate instead
|
||||||
+ instead of the whole line. */
|
+ of the whole line. */
|
||||||
+ if (cachedText
|
+ if (cachedText
|
||||||
+ && granularity == ns_ax_text_selection_granularity_line)
|
+ && granularity == ns_ax_text_selection_granularity_line)
|
||||||
+ {
|
+ {
|
||||||
@@ -2479,16 +2412,12 @@ index 932d209f..ea2de6f2 100644
|
|||||||
+ }
|
+ }
|
||||||
+}
|
+}
|
||||||
+
|
+
|
||||||
+ /* --- Completions announcement ---
|
+/* Post AnnouncementRequested for non-focused buffers (typically
|
||||||
+ When point changes in a non-focused buffer (e.g. *Completions*
|
+ *Completions* while minibuffer has keyboard focus).
|
||||||
+ while the minibuffer has keyboard focus), VoiceOver won't read
|
+ VoiceOver does not automatically read changes in non-focused
|
||||||
+ the change because it's tracking the focused element. Post an
|
+ elements, so we announce the selected completion explicitly. */
|
||||||
+ announcement so the user hears the selected completion.
|
+- (void)postCompletionAnnouncementForBuffer:(struct buffer *)b
|
||||||
+
|
+ point:(ptrdiff_t)point
|
||||||
+ If there is a `completions-highlight` overlay at point (Emacs
|
|
||||||
+ highlights the selected completion candidate), read its full
|
|
||||||
+ text instead of just the current line. */
|
|
||||||
+ if (![self isAccessibilityFocused] && cachedText)
|
|
||||||
+{
|
+{
|
||||||
+ NSString *announceText = nil;
|
+ NSString *announceText = nil;
|
||||||
+ ptrdiff_t currentOverlayStart = 0;
|
+ ptrdiff_t currentOverlayStart = 0;
|
||||||
@@ -2499,17 +2428,13 @@ index 932d209f..ea2de6f2 100644
|
|||||||
+ if (b != current_buffer)
|
+ if (b != current_buffer)
|
||||||
+ set_buffer_internal_1 (b);
|
+ set_buffer_internal_1 (b);
|
||||||
+
|
+
|
||||||
+ /* 1) Prefer explicit completion candidate property when present.
|
+ /* 1) Prefer explicit completion candidate property. */
|
||||||
+ completion--string can be a plain string (simple completion)
|
|
||||||
+ or a list ("candidate" "annotation") for annotated completions.
|
|
||||||
+ In the list case, use car (the completion itself). */
|
|
||||||
+ Lisp_Object cstr = Fget_char_property (make_fixnum (point),
|
+ Lisp_Object cstr = Fget_char_property (make_fixnum (point),
|
||||||
+ Qns_ax_completion__string,
|
+ Qns_ax_completion__string,
|
||||||
+ Qnil);
|
+ Qnil);
|
||||||
+ announceText = ns_ax_completion_string_from_prop (cstr);
|
+ announceText = ns_ax_completion_string_from_prop (cstr);
|
||||||
+
|
+
|
||||||
+ /* 2) Fallback: announce the mouse-face span at point.
|
+ /* 2) Fallback: mouse-face span at point. */
|
||||||
+ completion-list-mode often marks the active candidate this way. */
|
|
||||||
+ if (!announceText)
|
+ if (!announceText)
|
||||||
+ {
|
+ {
|
||||||
+ Lisp_Object mf = Fget_char_property (make_fixnum (point),
|
+ Lisp_Object mf = Fget_char_property (make_fixnum (point),
|
||||||
@@ -2519,8 +2444,6 @@ index 932d209f..ea2de6f2 100644
|
|||||||
+ ptrdiff_t begv2 = BUF_BEGV (b);
|
+ ptrdiff_t begv2 = BUF_BEGV (b);
|
||||||
+ ptrdiff_t zv2 = BUF_ZV (b);
|
+ ptrdiff_t zv2 = BUF_ZV (b);
|
||||||
+
|
+
|
||||||
+ /* Find mouse-face span boundaries using property
|
|
||||||
+ change functions — O(log n) instead of O(n). */
|
|
||||||
+ Lisp_Object prev_change
|
+ Lisp_Object prev_change
|
||||||
+ = Fprevious_single_char_property_change (
|
+ = Fprevious_single_char_property_change (
|
||||||
+ make_fixnum (point + 1), Qmouse_face,
|
+ make_fixnum (point + 1), Qmouse_face,
|
||||||
@@ -2548,7 +2471,7 @@ index 932d209f..ea2de6f2 100644
|
|||||||
+ }
|
+ }
|
||||||
+ }
|
+ }
|
||||||
+
|
+
|
||||||
+ /* 3) Fallback: check completions-highlight overlay span at point. */
|
+ /* 3) Fallback: completions-highlight overlay at point. */
|
||||||
+ if (!announceText)
|
+ if (!announceText)
|
||||||
+ {
|
+ {
|
||||||
+ Lisp_Object faceSym = Qns_ax_completions_highlight;
|
+ Lisp_Object faceSym = Qns_ax_completions_highlight;
|
||||||
@@ -2578,17 +2501,16 @@ index 932d209f..ea2de6f2 100644
|
|||||||
+ }
|
+ }
|
||||||
+ }
|
+ }
|
||||||
+
|
+
|
||||||
+ /* 4) Fallback: select the best completions-highlight overlay.
|
+ /* 4) Fallback: nearest completions-highlight overlay. */
|
||||||
+ Prefer overlay nearest to point over first-found in buffer. */
|
|
||||||
+ if (!announceText)
|
+ if (!announceText)
|
||||||
+ {
|
+ {
|
||||||
+ ptrdiff_t ov_start = 0;
|
+ ptrdiff_t ov_start = 0;
|
||||||
+ ptrdiff_t ov_end = 0;
|
+ ptrdiff_t ov_end = 0;
|
||||||
+ if (ns_ax_find_completion_overlay_range (b, point, &ov_start, &ov_end))
|
+ if (ns_ax_find_completion_overlay_range (b, point,
|
||||||
|
+ &ov_start, &ov_end))
|
||||||
+ {
|
+ {
|
||||||
+ announceText = ns_ax_completion_text_for_span (self, b,
|
+ announceText = ns_ax_completion_text_for_span (self, b,
|
||||||
+ ov_start,
|
+ ov_start, ov_end,
|
||||||
+ ov_end,
|
|
||||||
+ cachedText);
|
+ cachedText);
|
||||||
+ currentOverlayStart = ov_start;
|
+ currentOverlayStart = ov_start;
|
||||||
+ currentOverlayEnd = ov_end;
|
+ currentOverlayEnd = ov_end;
|
||||||
@@ -2597,7 +2519,7 @@ index 932d209f..ea2de6f2 100644
|
|||||||
+
|
+
|
||||||
+ unbind_to (count2, Qnil);
|
+ unbind_to (count2, Qnil);
|
||||||
+
|
+
|
||||||
+ /* Final fallback: read the current line at point. */
|
+ /* Final fallback: read current line at point. */
|
||||||
+ if (!announceText)
|
+ if (!announceText)
|
||||||
+ {
|
+ {
|
||||||
+ NSUInteger point_idx = [self accessibilityIndexForCharpos:point];
|
+ NSUInteger point_idx = [self accessibilityIndexForCharpos:point];
|
||||||
@@ -2614,6 +2536,7 @@ index 932d209f..ea2de6f2 100644
|
|||||||
+ }
|
+ }
|
||||||
+ }
|
+ }
|
||||||
+
|
+
|
||||||
|
+ /* Deduplicate: post only when text, overlay, or point changed. */
|
||||||
+ if (announceText)
|
+ if (announceText)
|
||||||
+ {
|
+ {
|
||||||
+ announceText = [announceText stringByTrimmingCharactersInSet:
|
+ announceText = [announceText stringByTrimmingCharactersInSet:
|
||||||
@@ -2660,13 +2583,106 @@ index 932d209f..ea2de6f2 100644
|
|||||||
+ }
|
+ }
|
||||||
+}
|
+}
|
||||||
+
|
+
|
||||||
|
+/* ---- Notification dispatch (main entry point) ---- */
|
||||||
|
+
|
||||||
|
+/* Dispatch accessibility notifications after a redisplay cycle.
|
||||||
|
+ Detects three mutually exclusive events: text edit, cursor/mark
|
||||||
|
+ change, or no change. Delegates to helper methods above. */
|
||||||
|
+- (void)postAccessibilityNotificationsForFrame:(struct frame *)f
|
||||||
|
+{
|
||||||
|
+ NSTRACE ("[EmacsView postAccessibilityNotificationsForFrame:]");
|
||||||
|
+ struct window *w = [self validWindow];
|
||||||
|
+ if (!w || !WINDOW_LEAF_P (w))
|
||||||
|
+ return;
|
||||||
|
+
|
||||||
|
+ struct buffer *b = XBUFFER (w->contents);
|
||||||
|
+ if (!b)
|
||||||
|
+ return;
|
||||||
|
+
|
||||||
|
+ ptrdiff_t modiff = BUF_MODIFF (b);
|
||||||
|
+ ptrdiff_t point = BUF_PT (b);
|
||||||
|
+ BOOL markActive = !NILP (BVAR (b, mark_active));
|
||||||
|
+
|
||||||
|
+ /* --- Text changed (edit) --- */
|
||||||
|
+ if (modiff != self.cachedModiff)
|
||||||
|
+ {
|
||||||
|
+ self.cachedModiff = modiff;
|
||||||
|
+ [self postTextChangedNotification:point];
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ /* --- Cursor moved or selection changed ---
|
||||||
|
+ Use 'else if' — edits and selection moves are mutually exclusive
|
||||||
|
+ per the WebKit/Chromium pattern. */
|
||||||
|
+ else if (point != self.cachedPoint || markActive != self.cachedMarkActive)
|
||||||
|
+ {
|
||||||
|
+ ptrdiff_t oldPoint = self.cachedPoint;
|
||||||
|
+ BOOL oldMarkActive = self.cachedMarkActive;
|
||||||
|
+ self.cachedPoint = point;
|
||||||
|
+ self.cachedMarkActive = markActive;
|
||||||
|
+
|
||||||
|
+ /* Compute direction. */
|
||||||
|
+ NSInteger direction = ns_ax_text_selection_direction_discontiguous;
|
||||||
|
+ if (point > oldPoint)
|
||||||
|
+ direction = ns_ax_text_selection_direction_next;
|
||||||
|
+ else if (point < oldPoint)
|
||||||
|
+ direction = ns_ax_text_selection_direction_previous;
|
||||||
|
+
|
||||||
|
+ int ctrlNP = 0;
|
||||||
|
+ bool isCtrlNP = ns_ax_event_is_line_nav_key (&ctrlNP);
|
||||||
|
+
|
||||||
|
+ /* --- Granularity detection --- */
|
||||||
|
+ NSInteger granularity = ns_ax_text_selection_granularity_unknown;
|
||||||
|
+ [self ensureTextCache];
|
||||||
|
+ if (cachedText && oldPoint > 0)
|
||||||
|
+ {
|
||||||
|
+ NSUInteger tlen = [cachedText length];
|
||||||
|
+ NSUInteger oldIdx = [self accessibilityIndexForCharpos:oldPoint];
|
||||||
|
+ NSUInteger newIdx = [self accessibilityIndexForCharpos:point];
|
||||||
|
+ if (oldIdx > tlen) oldIdx = tlen;
|
||||||
|
+ if (newIdx > tlen) newIdx = tlen;
|
||||||
|
+
|
||||||
|
+ NSRange oldLine = [cachedText lineRangeForRange:
|
||||||
|
+ NSMakeRange (oldIdx, 0)];
|
||||||
|
+ NSRange newLine = [cachedText lineRangeForRange:
|
||||||
|
+ NSMakeRange (newIdx, 0)];
|
||||||
|
+ if (oldLine.location != newLine.location)
|
||||||
|
+ granularity = ns_ax_text_selection_granularity_line;
|
||||||
|
+ else
|
||||||
|
+ {
|
||||||
|
+ NSUInteger dist = (newIdx > oldIdx
|
||||||
|
+ ? newIdx - oldIdx
|
||||||
|
+ : oldIdx - newIdx);
|
||||||
|
+ if (dist > 1)
|
||||||
|
+ granularity = ns_ax_text_selection_granularity_word;
|
||||||
|
+ else if (dist == 1)
|
||||||
|
+ granularity = ns_ax_text_selection_granularity_character;
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ /* Force line semantics for explicit C-n/C-p / Tab / backtab. */
|
||||||
|
+ if (isCtrlNP)
|
||||||
|
+ {
|
||||||
|
+ direction = (ctrlNP > 0
|
||||||
|
+ ? ns_ax_text_selection_direction_next
|
||||||
|
+ : ns_ax_text_selection_direction_previous);
|
||||||
|
+ granularity = ns_ax_text_selection_granularity_line;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ /* Post notifications for focused and non-focused elements. */
|
||||||
|
+ if ([self isAccessibilityFocused])
|
||||||
|
+ [self postFocusedCursorNotification:point
|
||||||
|
+ direction:direction
|
||||||
|
+ granularity:granularity
|
||||||
|
+ markActive:markActive
|
||||||
|
+ oldMarkActive:oldMarkActive];
|
||||||
|
+
|
||||||
|
+ if (![self isAccessibilityFocused] && cachedText)
|
||||||
|
+ [self postCompletionAnnouncementForBuffer:b point:point];
|
||||||
+ }
|
+ }
|
||||||
+ else
|
+ else
|
||||||
+ {
|
+ {
|
||||||
+ /* Nothing changed (no text edit, no cursor move, no mark change).
|
+ /* Nothing changed. Reset completion cache for focused buffer
|
||||||
+ Overlay state cannot change without a modiff bump, so no scan
|
+ to avoid stale announcements. */
|
||||||
+ needed for non-focused buffers. Just reset completion cache
|
|
||||||
+ for focused buffer to avoid stale announcements. */
|
|
||||||
+ if ([self isAccessibilityFocused])
|
+ if ([self isAccessibilityFocused])
|
||||||
+ {
|
+ {
|
||||||
+ self.cachedCompletionAnnouncement = nil;
|
+ self.cachedCompletionAnnouncement = nil;
|
||||||
@@ -2984,7 +3000,7 @@ index 932d209f..ea2de6f2 100644
|
|||||||
int code;
|
int code;
|
||||||
unsigned fnKeysym = 0;
|
unsigned fnKeysym = 0;
|
||||||
static NSMutableArray *nsEvArray;
|
static NSMutableArray *nsEvArray;
|
||||||
@@ -8237,6 +10559,31 @@ - (void)windowDidBecomeKey /* for direct calls */
|
@@ -8237,6 +10584,32 @@ - (void)windowDidBecomeKey /* for direct calls */
|
||||||
XSETFRAME (event.frame_or_window, emacsframe);
|
XSETFRAME (event.frame_or_window, emacsframe);
|
||||||
kbd_buffer_store_event (&event);
|
kbd_buffer_store_event (&event);
|
||||||
ns_send_appdefined (-1); // Kick main loop
|
ns_send_appdefined (-1); // Kick main loop
|
||||||
@@ -2993,6 +3009,7 @@ index 932d209f..ea2de6f2 100644
|
|||||||
+ /* Notify VoiceOver that the focused accessibility element changed.
|
+ /* Notify VoiceOver that the focused accessibility element changed.
|
||||||
+ Post on the focused virtual element so VoiceOver starts tracking it.
|
+ Post on the focused virtual element so VoiceOver starts tracking it.
|
||||||
+ This is critical for initial focus and app-switch scenarios. */
|
+ This is critical for initial focus and app-switch scenarios. */
|
||||||
|
+ if (ns_accessibility_enabled)
|
||||||
+ {
|
+ {
|
||||||
+ id focused = [self accessibilityFocusedUIElement];
|
+ id focused = [self accessibilityFocusedUIElement];
|
||||||
+ if (focused
|
+ if (focused
|
||||||
@@ -3016,7 +3033,7 @@ index 932d209f..ea2de6f2 100644
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -9474,6 +11821,332 @@ - (int) fullscreenState
|
@@ -9474,6 +11847,332 @@ - (int) fullscreenState
|
||||||
return fs_state;
|
return fs_state;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3171,7 +3188,7 @@ index 932d209f..ea2de6f2 100644
|
|||||||
+ NSTRACE ("[EmacsView postAccessibilityUpdates]");
|
+ NSTRACE ("[EmacsView postAccessibilityUpdates]");
|
||||||
+ eassert ([NSThread isMainThread]);
|
+ eassert ([NSThread isMainThread]);
|
||||||
+
|
+
|
||||||
+ if (!emacsframe)
|
+ if (!emacsframe || !ns_accessibility_enabled)
|
||||||
+ return;
|
+ return;
|
||||||
+
|
+
|
||||||
+ /* Re-entrance guard: VoiceOver callbacks during notification posting
|
+ /* Re-entrance guard: VoiceOver callbacks during notification posting
|
||||||
@@ -3349,7 +3366,7 @@ index 932d209f..ea2de6f2 100644
|
|||||||
@end /* EmacsView */
|
@end /* EmacsView */
|
||||||
|
|
||||||
|
|
||||||
@@ -11303,7 +13976,29 @@ Convert an X font name (XLFD) to an NS font name.
|
@@ -11303,6 +14002,28 @@ Convert an X font name (XLFD) to an NS font name.
|
||||||
DEFSYM (Qns_drag_operation_generic, "ns-drag-operation-generic");
|
DEFSYM (Qns_drag_operation_generic, "ns-drag-operation-generic");
|
||||||
DEFSYM (Qns_handle_drag_motion, "ns-handle-drag-motion");
|
DEFSYM (Qns_handle_drag_motion, "ns-handle-drag-motion");
|
||||||
|
|
||||||
@@ -3375,10 +3392,25 @@ index 932d209f..ea2de6f2 100644
|
|||||||
+ DEFSYM (Qns_ax_completions_highlight, "completions-highlight");
|
+ DEFSYM (Qns_ax_completions_highlight, "completions-highlight");
|
||||||
+ DEFSYM (Qns_ax_backtab, "backtab");
|
+ DEFSYM (Qns_ax_backtab, "backtab");
|
||||||
+ /* Qmouse_face and Qkeymap are defined in textprop.c / keymap.c. */
|
+ /* Qmouse_face and Qkeymap are defined in textprop.c / keymap.c. */
|
||||||
+
|
|
||||||
Fput (Qalt, Qmodifier_value, make_fixnum (alt_modifier));
|
Fput (Qalt, Qmodifier_value, make_fixnum (alt_modifier));
|
||||||
Fput (Qhyper, Qmodifier_value, make_fixnum (hyper_modifier));
|
Fput (Qhyper, Qmodifier_value, make_fixnum (hyper_modifier));
|
||||||
Fput (Qmeta, Qmodifier_value, make_fixnum (meta_modifier));
|
Fput (Qmeta, Qmodifier_value, make_fixnum (meta_modifier));
|
||||||
|
@@ -11451,6 +14172,15 @@ 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;
|
||||||
|
|
||||||
|
+ DEFVAR_BOOL ("ns-accessibility-enabled", ns_accessibility_enabled,
|
||||||
|
+ doc: /* Non-nil means expose buffer content to the macOS accessibility
|
||||||
|
+subsystem (VoiceOver, Zoom, and other assistive technology).
|
||||||
|
+When nil, the accessibility virtual element tree is not built and no
|
||||||
|
+notifications are posted, eliminating the associated overhead.
|
||||||
|
+Requires the Cocoa (NS) build on macOS; ignored on GNUstep.
|
||||||
|
+Default is t. */);
|
||||||
|
+ ns_accessibility_enabled = YES;
|
||||||
|
+
|
||||||
|
DEFVAR_BOOL ("ns-use-mwheel-acceleration",
|
||||||
|
ns_use_mwheel_acceleration,
|
||||||
|
doc: /* Non-nil means use macOS's standard mouse wheel acceleration.
|
||||||
--
|
--
|
||||||
2.43.0
|
2.43.0
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
From ce3b2a8091c99f738ec59acd6f6ebf0d84826e34 Mon Sep 17 00:00:00 2001
|
||||||
From: Martin Sukany <martin@sukany.cz>
|
From: Martin Sukany <martin@sukany.cz>
|
||||||
Date: Fri, 27 Feb 2026 17:30:00 +0100
|
Date: Fri, 27 Feb 2026 17:49:51 +0100
|
||||||
Subject: [PATCH 2/2] doc: add VoiceOver accessibility section to macOS
|
Subject: [PATCH 2/2] doc: add VoiceOver accessibility section to macOS
|
||||||
appendix
|
appendix
|
||||||
|
|
||||||
Document the new VoiceOver accessibility support in the Emacs manual.
|
Document the new VoiceOver accessibility support in the Emacs manual.
|
||||||
Add a new section to the macOS appendix covering screen reader usage,
|
Add a new section to the macOS appendix covering screen reader usage,
|
||||||
keyboard navigation feedback, completion announcements, and Zoom
|
keyboard navigation feedback, completion announcements, Zoom cursor
|
||||||
cursor tracking.
|
tracking, and the ns-accessibility-enabled user option.
|
||||||
|
|
||||||
* doc/emacs/macos.texi (VoiceOver Accessibility): New section.
|
* doc/emacs/macos.texi (VoiceOver Accessibility): New section.
|
||||||
---
|
---
|
||||||
doc/emacs/macos.texi | 46 +++++++++++++++++++++++++++++++++++++++++++
|
doc/emacs/macos.texi | 53 ++++++++++++++++++++++++++++++++++++++++++++
|
||||||
1 file changed, 46 insertions(+)
|
1 file changed, 53 insertions(+)
|
||||||
|
|
||||||
diff --git a/doc/emacs/macos.texi b/doc/emacs/macos.texi
|
diff --git a/doc/emacs/macos.texi b/doc/emacs/macos.texi
|
||||||
index 1234567..abcdefg 100644
|
index 6bd334f..1d969f9 100644
|
||||||
--- a/doc/emacs/macos.texi
|
--- a/doc/emacs/macos.texi
|
||||||
+++ b/doc/emacs/macos.texi
|
+++ b/doc/emacs/macos.texi
|
||||||
@@ -31,6 +31,7 @@ Support}), but we hope to improve it in the future.
|
@@ -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.
|
||||||
@@ -26,10 +26,10 @@ index 1234567..abcdefg 100644
|
|||||||
* GNUstep Support:: Details on status of GNUstep support.
|
* GNUstep Support:: Details on status of GNUstep support.
|
||||||
@end menu
|
@end menu
|
||||||
|
|
||||||
@@ -272,6 +273,51 @@ services and receive the results back. Note that you may need to
|
@@ -272,6 +273,58 @@ 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.
|
restart Emacs to access newly-available services.
|
||||||
|
|
||||||
|
|
||||||
+@node VoiceOver Accessibility
|
+@node VoiceOver Accessibility
|
||||||
+@section VoiceOver Accessibility (macOS)
|
+@section VoiceOver Accessibility (macOS)
|
||||||
+@cindex VoiceOver
|
+@cindex VoiceOver
|
||||||
@@ -70,6 +70,12 @@ index 1234567..abcdefg 100644
|
|||||||
+position is communicated via @code{UAZoomChangeFocus} and the
|
+position is communicated via @code{UAZoomChangeFocus} and the
|
||||||
+@code{AXBoundsForRange} accessibility attribute.
|
+@code{AXBoundsForRange} accessibility attribute.
|
||||||
+
|
+
|
||||||
|
+@vindex ns-accessibility-enabled
|
||||||
|
+ To disable the accessibility interface entirely (for instance, to
|
||||||
|
+eliminate overhead on systems where assistive technology is not in
|
||||||
|
+use), set @code{ns-accessibility-enabled} to @code{nil}. The default
|
||||||
|
+is @code{t}.
|
||||||
|
+
|
||||||
+ This support is available only on the Cocoa build; GNUstep has a
|
+ This support is available only on the Cocoa build; GNUstep has a
|
||||||
+different accessibility model and is not yet supported
|
+different accessibility model and is not yet supported
|
||||||
+(@pxref{GNUstep Support}). Evil-mode block cursors are handled
|
+(@pxref{GNUstep Support}). Evil-mode block cursors are handled
|
||||||
@@ -81,3 +87,4 @@ index 1234567..abcdefg 100644
|
|||||||
|
|
||||||
--
|
--
|
||||||
2.43.0
|
2.43.0
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ EMACS NS VOICEOVER ACCESSIBILITY PATCH
|
|||||||
patch: 0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch
|
patch: 0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch
|
||||||
0002-doc-add-VoiceOver-accessibility-section-to-macOS-app.patch
|
0002-doc-add-VoiceOver-accessibility-section-to-macOS-app.patch
|
||||||
author: Martin Sukany <martin@sukany.cz>
|
author: Martin Sukany <martin@sukany.cz>
|
||||||
files: src/nsterm.h (+105 lines)
|
files: src/nsterm.h (+119 lines)
|
||||||
src/nsterm.m (+2846 ins, -151 del, +2695 net)
|
src/nsterm.m (+3024 ins, -147 del, +2877 net)
|
||||||
doc/emacs/macos.texi (+46 lines)
|
doc/emacs/macos.texi (+53 lines)
|
||||||
etc/NEWS (+11 lines)
|
etc/NEWS (+13 lines)
|
||||||
|
|
||||||
|
|
||||||
OVERVIEW
|
OVERVIEW
|
||||||
@@ -114,6 +114,18 @@ ARCHITECTURE
|
|||||||
accessibilityAttributeValue:forParameter: API.
|
accessibilityAttributeValue:forParameter: API.
|
||||||
|
|
||||||
|
|
||||||
|
USER OPTION
|
||||||
|
-----------
|
||||||
|
|
||||||
|
ns-accessibility-enabled (DEFVAR_BOOL, default t):
|
||||||
|
When nil, the accessibility virtual element tree is not built, no
|
||||||
|
notifications are posted, and ns_draw_phys_cursor skips the Zoom
|
||||||
|
update. This eliminates accessibility overhead entirely on systems
|
||||||
|
where assistive technology is not in use. Guarded at three entry
|
||||||
|
points: postAccessibilityUpdates, ns_draw_phys_cursor, and
|
||||||
|
windowDidBecomeKey.
|
||||||
|
|
||||||
|
|
||||||
THREADING MODEL
|
THREADING MODEL
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
@@ -615,4 +627,38 @@ TESTING CHECKLIST
|
|||||||
26. Open an org-mode file with many folded sections. Verify that
|
26. Open an org-mode file with many folded sections. Verify that
|
||||||
folded (invisible) text is not announced during navigation.
|
folded (invisible) text is not announced during navigation.
|
||||||
|
|
||||||
|
|
||||||
|
REVIEW CHANGES (post initial implementation)
|
||||||
|
---------------------------------------------
|
||||||
|
|
||||||
|
The following changes were made based on maintainer-style code review:
|
||||||
|
|
||||||
|
1. ns_ax_window_end_charpos: added window_end_valid guard. Falls
|
||||||
|
back to BUF_ZV when the window has not been fully redisplayed,
|
||||||
|
preventing stale data in AX getters called before next redisplay.
|
||||||
|
|
||||||
|
2. GC safety documentation: detailed comment on lispWindow ivar
|
||||||
|
explaining why staticpro is not needed (windows reachable from
|
||||||
|
frame tree, GC only on main thread, AX getters dispatch to main).
|
||||||
|
|
||||||
|
3. ns-accessibility-enabled (DEFVAR_BOOL): new user option to
|
||||||
|
disable accessibility entirely. Guards three entry points.
|
||||||
|
|
||||||
|
4. postAccessibilityNotificationsForFrame: extracted from one ~200
|
||||||
|
line method into four focused helpers:
|
||||||
|
- postTextChangedNotification: (typing echo)
|
||||||
|
- postFocusedCursorNotification:direction:granularity:markActive:
|
||||||
|
oldMarkActive: (focused cursor/selection)
|
||||||
|
- postCompletionAnnouncementForBuffer:point: (completions)
|
||||||
|
- postAccessibilityNotificationsForFrame: (orchestrator, ~60 lines)
|
||||||
|
|
||||||
|
5. ns_ax_completion_text_for_span: added block_input/unblock_input
|
||||||
|
with specpdl unwind protection for signal safety.
|
||||||
|
|
||||||
|
6. Fplist_get third-argument comment (PREDICATE, not default value).
|
||||||
|
|
||||||
|
7. Documentation: macos.texi section updated with
|
||||||
|
ns-accessibility-enabled variable reference. etc/NEWS updated.
|
||||||
|
|
||||||
|
|
||||||
-- end of README --
|
-- end of README --
|
||||||
|
|||||||
Reference in New Issue
Block a user