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>
|
From: Martin Sukany <martin@sukany.cz>
|
||||||
Date: Wed, 26 Feb 2026 00:00:00 +0100
|
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
|
Implement dual accessibility support for macOS:
|
||||||
subclasses, enabling VoiceOver to read buffer contents, track cursor
|
|
||||||
movement, and announce window switches on macOS.
|
|
||||||
|
|
||||||
New classes:
|
1. UAZoomChangeFocus() in ns_draw_window_cursor: directly tells macOS
|
||||||
- EmacsAccessibilityElement: base class with coordinate conversion
|
Zoom where the cursor is, using CG coordinates.
|
||||||
- EmacsAccessibilityBuffer: AXTextArea virtual element per Emacs window
|
|
||||||
|
|
||||||
EmacsView becomes an AXGroup containing EmacsAccessibilityBuffer children.
|
2. Virtual accessibility tree with EmacsAccessibilityElement base class
|
||||||
Notification hooks fire on cursor movement, text edits, and window changes.
|
and EmacsAccessibilityBuffer (AXTextArea per Emacs window) for
|
||||||
Uses unsafe_unretained references (MRC compatible) and proper retain/release.
|
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
|
--- 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 @@
|
@@ -455,6 +455,34 @@
|
||||||
|
|
||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
@@ -51,16 +54,18 @@ Uses unsafe_unretained references (MRC compatible) and proper retain/release.
|
|||||||
The main Emacs view
|
The main Emacs view
|
||||||
|
|
||||||
========================================================================== */
|
========================================================================== */
|
||||||
@@ -471,6 +499,8 @@
|
@@ -471,6 +499,10 @@
|
||||||
#ifdef NS_IMPL_COCOA
|
#ifdef NS_IMPL_COCOA
|
||||||
char *old_title;
|
char *old_title;
|
||||||
BOOL maximizing_resize;
|
BOOL maximizing_resize;
|
||||||
+ NSMutableArray *accessibilityElements;
|
+ NSMutableArray *accessibilityElements;
|
||||||
+ Lisp_Object lastSelectedWindow;
|
+ Lisp_Object lastSelectedWindow;
|
||||||
|
+ NSRect lastAccessibilityCursorRect;
|
||||||
|
+ ptrdiff_t lastAccessibilityModiff;
|
||||||
#endif
|
#endif
|
||||||
BOOL font_panel_active;
|
BOOL font_panel_active;
|
||||||
NSFont *font_panel_result;
|
NSFont *font_panel_result;
|
||||||
@@ -528,6 +558,12 @@
|
@@ -528,6 +560,12 @@
|
||||||
- (void)windowWillExitFullScreen;
|
- (void)windowWillExitFullScreen;
|
||||||
- (void)windowDidExitFullScreen;
|
- (void)windowDidExitFullScreen;
|
||||||
- (void)windowDidBecomeKey;
|
- (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
|
--- 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 @@
|
@@ -1104,6 +1104,11 @@
|
||||||
|
|
||||||
unblock_input ();
|
unblock_input ();
|
||||||
@@ -87,7 +92,46 @@ Uses unsafe_unretained references (MRC compatible) and proper retain/release.
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void
|
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,
|
+ NSAccessibilityPostNotification(self,
|
||||||
+ NSAccessibilityValueChangedNotification);
|
+ 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 = @{
|
+ NSDictionary *userInfo = @{
|
||||||
+ @"AXTextStateChangeType" : @1, /* AXTextStateChangeTypeEdit */
|
+ @"AXTextStateChangeType": @1,
|
||||||
+ @"AXTextEditType" : @0 /* kAXTextEditTypeTyping */
|
+ @"AXTextChangeValues": @[change]
|
||||||
+ };
|
+ };
|
||||||
+ NSAccessibilityPostNotificationWithUserInfo(
|
+ NSAccessibilityPostNotificationWithUserInfo(
|
||||||
+ self, NSAccessibilityValueChangedNotification, userInfo);
|
+ self, NSAccessibilityValueChangedNotification, userInfo);
|
||||||
@@ -698,7 +763,7 @@ Uses unsafe_unretained references (MRC compatible) and proper retain/release.
|
|||||||
EmacsView implementation
|
EmacsView implementation
|
||||||
|
|
||||||
========================================================================== */
|
========================================================================== */
|
||||||
@@ -6889,6 +7498,7 @@
|
@@ -6889,6 +7551,7 @@
|
||||||
[layer release];
|
[layer release];
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -706,7 +771,7 @@ Uses unsafe_unretained references (MRC compatible) and proper retain/release.
|
|||||||
[[self menu] release];
|
[[self menu] release];
|
||||||
[super dealloc];
|
[super dealloc];
|
||||||
}
|
}
|
||||||
@@ -9474,6 +10084,144 @@
|
@@ -9474,6 +10137,293 @@
|
||||||
return fs_state;
|
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 */
|
+#endif /* NS_IMPL_COCOA */
|
||||||
+
|
+
|
||||||
@end /* EmacsView */
|
@end /* EmacsView */
|
||||||
|
|
||||||
|
|
||||||
@@ -9941,6 +10689,14 @@
|
@@ -9941,6 +10891,14 @@
|
||||||
|
|
||||||
return [super accessibilityAttributeValue:attribute];
|
return [super accessibilityAttributeValue:attribute];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user