From 46265bdaa6d425fddca015da6be4b309d98f0759 Mon Sep 17 00:00:00 2001 From: Daneel Date: Thu, 26 Feb 2026 09:03:10 +0100 Subject: [PATCH] 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 --- ...oundsForRange-for-macOS-Zoom-cursor-.patch | 256 ++++++++++++++++-- 1 file changed, 235 insertions(+), 21 deletions(-) diff --git a/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch b/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch index c1e37af..901c44d 100644 --- a/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch +++ b/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch @@ -1,21 +1,24 @@ From: Martin Sukany 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]; }