v13.2 patch: restore Zoom cursor tracking + fix MRC + fix typing echo
- Restore UAZoomChangeFocus() in ns_draw_window_cursor (was missing in v13) - Restore accessibilityBoundsForRange: on EmacsView (Zoom queries the view) - Restore legacy parameterized attribute APIs (Zoom uses these) - Add lastAccessibilityCursorRect ivar for cursor position tracking - Fix typing echo: AXTextEditType=3 (not 0), add AXTextChangeValues array - Keep virtual element tree (EmacsAccessibilityBuffer) for VoiceOver - MRC fixes: retain/release for accessibilityElements array
This commit is contained in:
@@ -1,21 +1,24 @@
|
||||
From: Martin Sukany <martin@sukany.cz>
|
||||
Date: Wed, 26 Feb 2026 00:00:00 +0100
|
||||
Subject: [PATCH] ns: add VoiceOver accessibility support (virtual element tree)
|
||||
Subject: [PATCH] ns: add macOS Zoom cursor tracking and VoiceOver accessibility
|
||||
|
||||
Implement an accessibility tree using virtual NSAccessibilityElement
|
||||
subclasses, enabling VoiceOver to read buffer contents, track cursor
|
||||
movement, and announce window switches on macOS.
|
||||
Implement dual accessibility support for macOS:
|
||||
|
||||
New classes:
|
||||
- EmacsAccessibilityElement: base class with coordinate conversion
|
||||
- EmacsAccessibilityBuffer: AXTextArea virtual element per Emacs window
|
||||
1. UAZoomChangeFocus() in ns_draw_window_cursor: directly tells macOS
|
||||
Zoom where the cursor is, using CG coordinates.
|
||||
|
||||
EmacsView becomes an AXGroup containing EmacsAccessibilityBuffer children.
|
||||
Notification hooks fire on cursor movement, text edits, and window changes.
|
||||
Uses unsafe_unretained references (MRC compatible) and proper retain/release.
|
||||
2. Virtual accessibility tree with EmacsAccessibilityElement base class
|
||||
and EmacsAccessibilityBuffer (AXTextArea per Emacs window) for
|
||||
VoiceOver: text content, cursor tracking, window switch notifications.
|
||||
|
||||
3. EmacsView provides accessibilityBoundsForRange: for Zoom queries and
|
||||
legacy parameterized attribute APIs. EmacsView acts as AXGroup
|
||||
containing EmacsAccessibilityBuffer children.
|
||||
|
||||
Uses unsafe_unretained references and proper retain/release (MRC compatible).
|
||||
---
|
||||
--- a/src/nsterm.h 2026-02-26 08:46:18.118172281 +0100
|
||||
+++ b/src/nsterm.h 2026-02-26 08:46:06.891980688 +0100
|
||||
+++ b/src/nsterm.h 2026-02-26 09:01:18.404357802 +0100
|
||||
@@ -455,6 +455,34 @@
|
||||
|
||||
/* ==========================================================================
|
||||
@@ -51,16 +54,18 @@ Uses unsafe_unretained references (MRC compatible) and proper retain/release.
|
||||
The main Emacs view
|
||||
|
||||
========================================================================== */
|
||||
@@ -471,6 +499,8 @@
|
||||
@@ -471,6 +499,10 @@
|
||||
#ifdef NS_IMPL_COCOA
|
||||
char *old_title;
|
||||
BOOL maximizing_resize;
|
||||
+ NSMutableArray *accessibilityElements;
|
||||
+ Lisp_Object lastSelectedWindow;
|
||||
+ NSRect lastAccessibilityCursorRect;
|
||||
+ ptrdiff_t lastAccessibilityModiff;
|
||||
#endif
|
||||
BOOL font_panel_active;
|
||||
NSFont *font_panel_result;
|
||||
@@ -528,6 +558,12 @@
|
||||
@@ -528,6 +560,12 @@
|
||||
- (void)windowWillExitFullScreen;
|
||||
- (void)windowDidExitFullScreen;
|
||||
- (void)windowDidBecomeKey;
|
||||
@@ -74,7 +79,7 @@ Uses unsafe_unretained references (MRC compatible) and proper retain/release.
|
||||
|
||||
|
||||
--- a/src/nsterm.m 2026-02-26 08:46:18.124172384 +0100
|
||||
+++ b/src/nsterm.m 2026-02-26 08:52:18.397374436 +0100
|
||||
+++ b/src/nsterm.m 2026-02-26 09:02:44.734005575 +0100
|
||||
@@ -1104,6 +1104,11 @@
|
||||
|
||||
unblock_input ();
|
||||
@@ -87,7 +92,46 @@ Uses unsafe_unretained references (MRC compatible) and proper retain/release.
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -6849,6 +6854,610 @@
|
||||
@@ -3232,6 +3237,38 @@
|
||||
/* Prevent the cursor from being drawn outside the text area. */
|
||||
r = NSIntersectionRect (r, ns_row_rect (w, glyph_row, TEXT_AREA));
|
||||
|
||||
+#ifdef NS_IMPL_COCOA
|
||||
+ /* Accessibility cursor tracking for macOS Zoom and VoiceOver.
|
||||
+ Only notify AT when drawing the cursor in the active (selected)
|
||||
+ window. Without this guard, C-x o triggers UAZoomChangeFocus
|
||||
+ for the old window last, snapping Zoom back. */
|
||||
+ {
|
||||
+ EmacsView *view = FRAME_NS_VIEW (f);
|
||||
+ if (view && on_p && active_p)
|
||||
+ {
|
||||
+ /* Store cursor rect for accessibilityBoundsForRange: queries. */
|
||||
+ view->lastAccessibilityCursorRect = r;
|
||||
+
|
||||
+ /* Tell macOS Zoom where the cursor is. UAZoomChangeFocus()
|
||||
+ expects top-left origin (CG coordinate space). */
|
||||
+ if (UAZoomEnabled ())
|
||||
+ {
|
||||
+ NSRect windowRect = [view convertRect:r toView:nil];
|
||||
+ NSRect screenRect = [[view window] convertRectToScreen:windowRect];
|
||||
+ CGRect cgRect = NSRectToCGRect (screenRect);
|
||||
+
|
||||
+ CGFloat primaryH
|
||||
+ = [[[NSScreen screens] firstObject] frame].size.height;
|
||||
+ cgRect.origin.y
|
||||
+ = primaryH - cgRect.origin.y - cgRect.size.height;
|
||||
+
|
||||
+ UAZoomChangeFocus (&cgRect, &cgRect,
|
||||
+ kUAZoomFocusTypeInsertionPoint);
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+#endif
|
||||
+
|
||||
ns_focus (f, NULL, 0);
|
||||
|
||||
NSGraphicsContext *ctx = [NSGraphicsContext currentContext];
|
||||
@@ -6849,6 +6886,631 @@
|
||||
|
||||
/* ==========================================================================
|
||||
|
||||
@@ -670,10 +714,31 @@ Uses unsafe_unretained references (MRC compatible) and proper retain/release.
|
||||
+ NSAccessibilityPostNotification(self,
|
||||
+ NSAccessibilityValueChangedNotification);
|
||||
+
|
||||
+ /* Rich typing echo for VoiceOver. */
|
||||
+ /* Rich typing echo for VoiceOver.
|
||||
+ kAXTextStateChangeTypeEdit = 1, kAXTextEditTypeTyping = 3.
|
||||
+ Must include AXTextChangeValues array for VoiceOver to speak. */
|
||||
+ NSString *changedText = @"";
|
||||
+ ptrdiff_t pt = BUF_PT (b);
|
||||
+ if (pt > BUF_BEGV (b))
|
||||
+ {
|
||||
+ EmacsView *view = self.emacsView;
|
||||
+ if (view)
|
||||
+ {
|
||||
+ NSRange charRange = NSMakeRange (
|
||||
+ (NSUInteger)(pt - BUF_BEGV (b) - 1), 1);
|
||||
+ changedText = [view accessibilityStringForRange:charRange];
|
||||
+ if (!changedText)
|
||||
+ changedText = @"";
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ NSDictionary *change = @{
|
||||
+ @"AXTextEditType": @3,
|
||||
+ @"AXTextChangeValue": changedText
|
||||
+ };
|
||||
+ NSDictionary *userInfo = @{
|
||||
+ @"AXTextStateChangeType" : @1, /* AXTextStateChangeTypeEdit */
|
||||
+ @"AXTextEditType" : @0 /* kAXTextEditTypeTyping */
|
||||
+ @"AXTextStateChangeType": @1,
|
||||
+ @"AXTextChangeValues": @[change]
|
||||
+ };
|
||||
+ NSAccessibilityPostNotificationWithUserInfo(
|
||||
+ self, NSAccessibilityValueChangedNotification, userInfo);
|
||||
@@ -698,7 +763,7 @@ Uses unsafe_unretained references (MRC compatible) and proper retain/release.
|
||||
EmacsView implementation
|
||||
|
||||
========================================================================== */
|
||||
@@ -6889,6 +7498,7 @@
|
||||
@@ -6889,6 +7551,7 @@
|
||||
[layer release];
|
||||
#endif
|
||||
|
||||
@@ -706,7 +771,7 @@ Uses unsafe_unretained references (MRC compatible) and proper retain/release.
|
||||
[[self menu] release];
|
||||
[super dealloc];
|
||||
}
|
||||
@@ -9474,6 +10084,144 @@
|
||||
@@ -9474,6 +10137,293 @@
|
||||
return fs_state;
|
||||
}
|
||||
|
||||
@@ -846,12 +911,161 @@ Uses unsafe_unretained references (MRC compatible) and proper retain/release.
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+/* ---- Cursor position for Zoom (via accessibilityBoundsForRange:) ----
|
||||
+
|
||||
+ accessibilityFrame returns the VIEW's frame (standard behavior).
|
||||
+ The cursor location is exposed through accessibilityBoundsForRange:
|
||||
+ which AT tools query using the selectedTextRange. */
|
||||
+
|
||||
+- (NSRect)accessibilityFrame
|
||||
+{
|
||||
+ return [super accessibilityFrame];
|
||||
+}
|
||||
+
|
||||
+- (NSRect)accessibilityBoundsForRange:(NSRange)range
|
||||
+{
|
||||
+ /* Return cursor screen rect. AT tools call this with the
|
||||
+ selectedTextRange to locate the insertion point. */
|
||||
+ NSRect viewRect = lastAccessibilityCursorRect;
|
||||
+
|
||||
+ if (viewRect.size.width < 1)
|
||||
+ viewRect.size.width = 1;
|
||||
+ if (viewRect.size.height < 1)
|
||||
+ viewRect.size.height = 8;
|
||||
+
|
||||
+ NSWindow *win = [self window];
|
||||
+ if (win == nil)
|
||||
+ return NSZeroRect;
|
||||
+
|
||||
+ NSRect windowRect = [self convertRect:viewRect toView:nil];
|
||||
+ return [win convertRectToScreen:windowRect];
|
||||
+}
|
||||
+
|
||||
+- (NSRect)accessibilityFrameForRange:(NSRange)range
|
||||
+{
|
||||
+ return [self accessibilityBoundsForRange:range];
|
||||
+}
|
||||
+
|
||||
+/* ---- Text content methods (for Zoom and legacy AT) ---- */
|
||||
+
|
||||
+- (id)accessibilityValue
|
||||
+{
|
||||
+ if (!emacsframe)
|
||||
+ return @"";
|
||||
+
|
||||
+ struct buffer *curbuf
|
||||
+ = XBUFFER (XWINDOW (emacsframe->selected_window)->contents);
|
||||
+ if (!curbuf)
|
||||
+ return @"";
|
||||
+
|
||||
+ ptrdiff_t start_byte = BUF_BEGV_BYTE (curbuf);
|
||||
+ ptrdiff_t byte_range = BUF_ZV_BYTE (curbuf) - start_byte;
|
||||
+ ptrdiff_t range = BUF_ZV (curbuf) - BUF_BEGV (curbuf);
|
||||
+
|
||||
+ if (range > 10000)
|
||||
+ {
|
||||
+ range = 10000;
|
||||
+ ptrdiff_t end_byte = buf_charpos_to_bytepos (curbuf,
|
||||
+ BUF_BEGV (curbuf) + range);
|
||||
+ byte_range = end_byte - start_byte;
|
||||
+ }
|
||||
+
|
||||
+ Lisp_Object str;
|
||||
+ if (! NILP (BVAR (curbuf, enable_multibyte_characters)))
|
||||
+ str = make_uninit_multibyte_string (range, byte_range);
|
||||
+ else
|
||||
+ str = make_uninit_string (range);
|
||||
+ memcpy (SDATA (str), BYTE_POS_ADDR (start_byte), byte_range);
|
||||
+
|
||||
+ return [NSString stringWithLispString:str];
|
||||
+}
|
||||
+
|
||||
+- (NSRange)accessibilitySelectedTextRange
|
||||
+{
|
||||
+ if (!emacsframe)
|
||||
+ return NSMakeRange (0, 0);
|
||||
+
|
||||
+ struct buffer *curbuf
|
||||
+ = XBUFFER (XWINDOW (emacsframe->selected_window)->contents);
|
||||
+ if (!curbuf)
|
||||
+ return NSMakeRange (0, 0);
|
||||
+
|
||||
+ ptrdiff_t pt = BUF_PT (curbuf) - BUF_BEGV (curbuf);
|
||||
+ return NSMakeRange ((NSUInteger) pt, 0);
|
||||
+}
|
||||
+
|
||||
+- (NSString *)accessibilityStringForRange:(NSRange)nsrange
|
||||
+{
|
||||
+ if (!emacsframe)
|
||||
+ return @"";
|
||||
+
|
||||
+ struct buffer *curbuf
|
||||
+ = XBUFFER (XWINDOW (emacsframe->selected_window)->contents);
|
||||
+ if (!curbuf)
|
||||
+ return @"";
|
||||
+
|
||||
+ ptrdiff_t start = BUF_BEGV (curbuf) + (ptrdiff_t) nsrange.location;
|
||||
+ ptrdiff_t end = start + (ptrdiff_t) nsrange.length;
|
||||
+ ptrdiff_t buf_end = BUF_ZV (curbuf);
|
||||
+
|
||||
+ if (start < BUF_BEGV (curbuf)) start = BUF_BEGV (curbuf);
|
||||
+ if (end > buf_end) end = buf_end;
|
||||
+ if (start >= end) return @"";
|
||||
+
|
||||
+ ptrdiff_t start_byte = buf_charpos_to_bytepos (curbuf, start);
|
||||
+ ptrdiff_t end_byte = buf_charpos_to_bytepos (curbuf, end);
|
||||
+ ptrdiff_t char_range = end - start;
|
||||
+ ptrdiff_t brange = end_byte - start_byte;
|
||||
+
|
||||
+ Lisp_Object str;
|
||||
+ if (! NILP (BVAR (curbuf, enable_multibyte_characters)))
|
||||
+ str = make_uninit_multibyte_string (char_range, brange);
|
||||
+ else
|
||||
+ str = make_uninit_string (char_range);
|
||||
+ memcpy (SDATA (str), BUF_BYTE_ADDRESS (curbuf, start_byte), brange);
|
||||
+
|
||||
+ return [NSString stringWithLispString:str];
|
||||
+}
|
||||
+
|
||||
+/* ---- Legacy parameterized attribute APIs (Zoom uses these) ---- */
|
||||
+
|
||||
+- (NSArray *)accessibilityParameterizedAttributeNames
|
||||
+{
|
||||
+ NSArray *superAttrs = [super accessibilityParameterizedAttributeNames];
|
||||
+ if (superAttrs == nil)
|
||||
+ superAttrs = @[];
|
||||
+ return [superAttrs arrayByAddingObjectsFromArray:
|
||||
+ @[NSAccessibilityBoundsForRangeParameterizedAttribute,
|
||||
+ NSAccessibilityStringForRangeParameterizedAttribute]];
|
||||
+}
|
||||
+
|
||||
+- (id)accessibilityAttributeValue:(NSString *)attribute
|
||||
+ forParameter:(id)parameter
|
||||
+{
|
||||
+ if ([attribute isEqualToString:
|
||||
+ NSAccessibilityBoundsForRangeParameterizedAttribute])
|
||||
+ {
|
||||
+ NSRange range = [(NSValue *) parameter rangeValue];
|
||||
+ return [NSValue valueWithRect:
|
||||
+ [self accessibilityBoundsForRange:range]];
|
||||
+ }
|
||||
+
|
||||
+ if ([attribute isEqualToString:
|
||||
+ NSAccessibilityStringForRangeParameterizedAttribute])
|
||||
+ {
|
||||
+ NSRange range = [(NSValue *) parameter rangeValue];
|
||||
+ return [self accessibilityStringForRange:range];
|
||||
+ }
|
||||
+
|
||||
+ return [super accessibilityAttributeValue:attribute forParameter:parameter];
|
||||
+}
|
||||
+
|
||||
+#endif /* NS_IMPL_COCOA */
|
||||
+
|
||||
@end /* EmacsView */
|
||||
|
||||
|
||||
@@ -9941,6 +10689,14 @@
|
||||
@@ -9941,6 +10891,14 @@
|
||||
|
||||
return [super accessibilityAttributeValue:attribute];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user