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 346273c..65f6799 100644 --- a/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch +++ b/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch @@ -1,19 +1,19 @@ -From 4626c5d0688db9d878120b120cb19e12fa2f3a94 Mon Sep 17 00:00:00 2001 +From 1caa0476b3109ad583715c2f8a90c943780ffcb9 Mon Sep 17 00:00:00 2001 From: Martin Sukany -Date: Fri, 27 Feb 2026 09:39:39 +0100 +Date: Fri, 27 Feb 2026 09:57:00 +0100 Subject: [PATCH] ns: implement VoiceOver accessibility (AXBoundsForRange, line nav, completions) --- - src/nsterm.h | 71 ++ - src/nsterm.m | 2195 ++++++++++++++++++++++++++++++++++++++++++++++---- - 2 files changed, 2126 insertions(+), 140 deletions(-) + src/nsterm.h | 73 ++ + src/nsterm.m | 2249 +++++++++++++++++++++++++++++++++++++++++++++++--- + 2 files changed, 2187 insertions(+), 135 deletions(-) diff --git a/src/nsterm.h b/src/nsterm.h -index 7c1ee4c..22828f2 100644 +index 7c1ee4c..717a838 100644 --- a/src/nsterm.h +++ b/src/nsterm.h -@@ -453,6 +453,62 @@ enum ns_return_frame_mode +@@ -453,6 +453,64 @@ enum ns_return_frame_mode @end @@ -29,7 +29,9 @@ index 7c1ee4c..22828f2 100644 +/* Base class for virtual accessibility elements attached to EmacsView. */ +@interface EmacsAccessibilityElement : NSAccessibilityElement +@property (nonatomic, unsafe_unretained) EmacsView *emacsView; -+@property (nonatomic, assign) struct window *emacsWindow; ++/* Lisp window object — safe across GC cycles. NULL_LISP when unset. */ ++@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 + @@ -76,7 +78,7 @@ index 7c1ee4c..22828f2 100644 /* ========================================================================== The main Emacs view -@@ -471,6 +527,14 @@ enum ns_return_frame_mode +@@ -471,6 +529,14 @@ enum ns_return_frame_mode #ifdef NS_IMPL_COCOA char *old_title; BOOL maximizing_resize; @@ -91,7 +93,7 @@ index 7c1ee4c..22828f2 100644 #endif BOOL font_panel_active; NSFont *font_panel_result; -@@ -528,6 +592,13 @@ enum ns_return_frame_mode +@@ -528,6 +594,13 @@ enum ns_return_frame_mode - (void)windowWillExitFullScreen; - (void)windowDidExitFullScreen; - (void)windowDidBecomeKey; @@ -106,10 +108,18 @@ index 7c1ee4c..22828f2 100644 diff --git a/src/nsterm.m b/src/nsterm.m -index 932d209..06ee636 100644 +index 932d209..220dccf 100644 --- a/src/nsterm.m +++ b/src/nsterm.m -@@ -1104,6 +1104,11 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen) +@@ -46,6 +46,7 @@ Updated by Christian Limpach (chris@nice.ch) + #include "blockinput.h" + #include "sysselect.h" + #include "nsterm.h" ++#include "intervals.h" + #include "systime.h" + #include "character.h" + #include "xwidget.h" +@@ -1104,6 +1105,11 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen) unblock_input (); ns_updating_frame = NULL; @@ -121,7 +131,7 @@ index 932d209..06ee636 100644 } static void -@@ -3232,6 +3237,37 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors. +@@ -3232,6 +3238,37 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors. /* Prevent the cursor from being drawn outside the text area. */ r = NSIntersectionRect (r, ns_row_rect (w, glyph_row, TEXT_AREA)); @@ -159,7 +169,7 @@ index 932d209..06ee636 100644 ns_focus (f, NULL, 0); NSGraphicsContext *ctx = [NSGraphicsContext currentContext]; -@@ -6849,216 +6885,1781 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg +@@ -6849,207 +6886,1829 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg /* ========================================================================== @@ -227,16 +237,14 @@ index 932d209..06ee636 100644 + + while (pos < zv) + { -+ /* Check invisible property (text properties + overlays). */ ++ /* Check invisible property (text properties + overlays). ++ Use TEXT_PROP_MEANS_INVISIBLE which respects buffer-invisibility-spec, ++ matching the logic in xdisp.c. This correctly handles org-mode, ++ outline-mode, hideshow and any mode using spec-controlled ++ invisibility (not just `invisible t'). */ + Lisp_Object invis = Fget_char_property (make_fixnum (pos), + Qinvisible, Qnil); -+ /* Check if invisible property means truly invisible. -+ TEXT_PROP_MEANS_INVISIBLE is defined only in xdisp.c, -+ so we replicate: EQ(invis, Qt), or invis is on the -+ buffer's invisibility-spec list. Simplified: any -+ non-nil invisible property hides the text. This matches -+ the common case (invisible t) and org-mode/dired usage. */ -+ if (!NILP (invis)) ++ if (TEXT_PROP_MEANS_INVISIBLE (invis)) + { + /* Skip to the next position where invisible changes. */ + Lisp_Object next = Fnext_single_char_property_change ( @@ -730,8 +738,34 @@ index 932d209..06ee636 100644 +@implementation EmacsAccessibilityElement - return Qnil; -+- (NSRect)screenRectFromEmacsX:(int)x y:(int)y width:(int)ew height:(int)eh ++- (instancetype)init +{ ++ self = [super init]; ++ if (self) ++ self.lispWindow = Qnil; ++ return self; + } + +-- (BOOL)acceptsFirstResponder ++/* Return the associated Emacs window if it is still live, else NULL. ++ Use this instead of storing a raw struct window * which can become a ++ dangling pointer after delete-window or kill-buffer. */ ++- (struct window *)validWindow + { +- NSTRACE ("[EmacsView acceptsFirstResponder]"); +- return YES; ++ if (NILP (self.lispWindow) || !WINDOW_LIVE_P (self.lispWindow)) ++ return NULL; ++ return XWINDOW (self.lispWindow); + } + +-/* Tell NS we want to accept clicks that activate the window */ +-- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent ++- (NSRect)screenRectFromEmacsX:(int)x y:(int)y width:(int)ew height:(int)eh + { +- NSTRACE_MSG ("First mouse event: type=%ld, clickCount=%ld", +- [theEvent type], [theEvent clickCount]); +- return ns_click_through; + EmacsView *view = self.emacsView; + if (!view || ![view window]) + return NSZeroRect; @@ -740,39 +774,25 @@ index 932d209..06ee636 100644 + NSRect winRect = [view convertRect:r toView:nil]; + return [[view window] convertRectToScreen:winRect]; } - --- (BOOL)acceptsFirstResponder +-- (void)resetCursorRects ++ +- (BOOL)isAccessibilityElement { -- NSTRACE ("[EmacsView acceptsFirstResponder]"); - return YES; - } - --/* Tell NS we want to accept clicks that activate the window */ --- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent -+/* ---- Hierarchy plumbing (required for VoiceOver to find us) ---- */ -+ -+- (id)accessibilityParent - { -- NSTRACE_MSG ("First mouse event: type=%ld, clickCount=%ld", -- [theEvent type], [theEvent clickCount]); -- return ns_click_through; -+ return NSAccessibilityUnignoredAncestor (self.emacsView); - } --- (void)resetCursorRects --{ - NSRect visible = [self visibleRect]; - NSCursor *currentCursor = FRAME_POINTER_TYPE (emacsframe); - NSTRACE ("[EmacsView resetCursorRects]"); -- ++ return YES; ++} + - if (currentCursor == nil) - currentCursor = [NSCursor arrowCursor]; ++/* ---- Hierarchy plumbing (required for VoiceOver to find us) ---- */ - if (!NSIsEmptyRect (visible)) - [self addCursorRect: visible cursor: currentCursor]; -+- (id)accessibilityWindow ++- (id)accessibilityParent +{ -+ return [self.emacsView window]; ++ return NSAccessibilityUnignoredAncestor (self.emacsView); +} -#if defined (NS_IMPL_GNUSTEP) || MAC_OS_X_VERSION_MIN_REQUIRED < 101300 @@ -781,17 +801,19 @@ index 932d209..06ee636 100644 -#endif - [currentCursor setOnMouseEntered: YES]; -#endif ++- (id)accessibilityWindow ++{ ++ return [self.emacsView window]; ++} ++ +- (id)accessibilityTopLevelUIElement +{ + return [self.emacsView window]; - } - ++} ++ +@end - - --/*****************************************************************************/ --/* Keyboard handling. */ --#define NS_KEYLOG 0 ++ ++ +@implementation EmacsAccessibilityBuffer +@synthesize cachedText; +@synthesize cachedTextModiff; @@ -803,8 +825,7 @@ index 932d209..06ee636 100644 +@synthesize cachedCompletionOverlayStart; +@synthesize cachedCompletionOverlayEnd; +@synthesize cachedCompletionPoint; - --- (void)keyDown: (NSEvent *)theEvent ++ +- (void)dealloc +{ + [cachedText release]; @@ -830,7 +851,7 @@ index 932d209..06ee636 100644 + +- (void)ensureTextCache +{ -+ struct window *w = self.emacsWindow; ++ struct window *w = [self validWindow]; + if (!w || !WINDOW_LEAF_P (w)) + return; + @@ -868,7 +889,7 @@ index 932d209..06ee636 100644 +/* Convert buffer charpos to accessibility string index. */ +- (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos +{ -+ struct window *w = self.emacsWindow; ++ struct window *w = [self validWindow]; + struct buffer *b = (w && WINDOW_LEAF_P (w)) ? XBUFFER (w->contents) : NULL; + + for (NSUInteger i = 0; i < visibleRunCount; i++) @@ -901,7 +922,7 @@ index 932d209..06ee636 100644 +/* Convert accessibility string index to buffer charpos. */ +- (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx +{ -+ struct window *w = self.emacsWindow; ++ struct window *w = [self validWindow]; + struct buffer *b = (w && WINDOW_LEAF_P (w)) ? XBUFFER (w->contents) : NULL; + + for (NSUInteger i = 0; i < visibleRunCount; i++) @@ -950,7 +971,7 @@ index 932d209..06ee636 100644 + +- (NSString *)accessibilityRoleDescription +{ -+ struct window *w = self.emacsWindow; ++ struct window *w = [self validWindow]; + if (w && MINI_WINDOW_P (w)) + return @"minibuffer"; + return @"editor"; @@ -958,7 +979,7 @@ index 932d209..06ee636 100644 + +- (NSString *)accessibilityLabel +{ -+ struct window *w = self.emacsWindow; ++ struct window *w = [self validWindow]; + if (w && WINDOW_LEAF_P (w)) + { + if (MINI_WINDOW_P (w)) @@ -977,7 +998,7 @@ index 932d209..06ee636 100644 + +- (BOOL)isAccessibilityFocused +{ -+ struct window *w = self.emacsWindow; ++ struct window *w = [self validWindow]; + if (!w) + return NO; + EmacsView *view = self.emacsView; @@ -989,6 +1010,14 @@ index 932d209..06ee636 100644 + +- (id)accessibilityValue +{ ++ /* AX getters can be called from any thread by the AT subsystem. ++ Dispatch to main thread where Emacs buffer state is consistent. */ ++ if (![NSThread isMainThread]) ++ { ++ __block id result; ++ dispatch_sync (dispatch_get_main_queue (), ^{ result = [self accessibilityValue]; }); ++ return result; ++ } + [self ensureTextCache]; + return cachedText ? cachedText : @""; +} @@ -1001,7 +1030,7 @@ index 932d209..06ee636 100644 + +- (NSString *)accessibilitySelectedText +{ -+ struct window *w = self.emacsWindow; ++ struct window *w = [self validWindow]; + if (!w || !WINDOW_LEAF_P (w)) + return @""; + @@ -1019,7 +1048,13 @@ index 932d209..06ee636 100644 + +- (NSRange)accessibilitySelectedTextRange +{ -+ struct window *w = self.emacsWindow; ++ if (![NSThread isMainThread]) ++ { ++ __block NSRange result; ++ dispatch_sync (dispatch_get_main_queue (), ^{ result = [self accessibilitySelectedTextRange]; }); ++ return result; ++ } ++ struct window *w = [self validWindow]; + if (!w || !WINDOW_LEAF_P (w)) + return NSMakeRange (0, 0); + @@ -1045,7 +1080,7 @@ index 932d209..06ee636 100644 + +- (void)setAccessibilitySelectedTextRange:(NSRange)range +{ -+ struct window *w = self.emacsWindow; ++ struct window *w = [self validWindow]; + if (!w || !WINDOW_LEAF_P (w)) + return; + @@ -1104,7 +1139,7 @@ index 932d209..06ee636 100644 + if (!flag) + return; + -+ struct window *w = self.emacsWindow; ++ struct window *w = [self validWindow]; + if (!w || !WINDOW_LEAF_P (w)) + return; + @@ -1132,7 +1167,13 @@ index 932d209..06ee636 100644 + +- (NSInteger)accessibilityInsertionPointLineNumber +{ -+ struct window *w = self.emacsWindow; ++ if (![NSThread isMainThread]) ++ { ++ __block NSInteger result; ++ dispatch_sync (dispatch_get_main_queue (), ^{ result = [self accessibilityInsertionPointLineNumber]; }); ++ return result; ++ } ++ struct window *w = [self validWindow]; + if (!w || !WINDOW_LEAF_P (w)) + return 0; + @@ -1246,7 +1287,13 @@ index 932d209..06ee636 100644 + +- (NSRect)accessibilityFrameForRange:(NSRange)range +{ -+ struct window *w = self.emacsWindow; ++ if (![NSThread isMainThread]) ++ { ++ __block NSRect result; ++ dispatch_sync (dispatch_get_main_queue (), ^{ result = [self accessibilityFrameForRange:range]; }); ++ return result; ++ } ++ struct window *w = [self validWindow]; + EmacsView *view = self.emacsView; + if (!w || !view) + return NSZeroRect; @@ -1261,8 +1308,14 @@ index 932d209..06ee636 100644 + +- (NSRange)accessibilityRangeForPosition:(NSPoint)screenPoint +{ ++ if (![NSThread isMainThread]) ++ { ++ __block NSRange result; ++ dispatch_sync (dispatch_get_main_queue (), ^{ result = [self accessibilityRangeForPosition:screenPoint]; }); ++ return result; ++ } + /* Hit test: convert screen point to buffer character index. */ -+ struct window *w = self.emacsWindow; ++ struct window *w = [self validWindow]; + EmacsView *view = self.emacsView; + if (!w || !view || !w->current_matrix) + return NSMakeRange (0, 0); @@ -1278,6 +1331,10 @@ index 932d209..06ee636 100644 + if (x < 0 || y < 0 || x >= w->pixel_width || y >= w->pixel_height) + return NSMakeRange (0, 0); + ++ /* Block input to prevent concurrent redisplay from modifying the ++ glyph matrix while we traverse it. */ ++ block_input (); ++ + /* Find the glyph row at this y coordinate. */ + struct glyph_matrix *matrix = w->current_matrix; + struct glyph_row *hit_row = NULL; @@ -1297,7 +1354,10 @@ index 932d209..06ee636 100644 + } + + if (!hit_row) -+ return NSMakeRange (0, 0); ++ { ++ unblock_input (); ++ return NSMakeRange (0, 0); ++ } + + /* Find the glyph at this x coordinate within the row. */ + struct glyph *glyph = hit_row->glyphs[TEXT_AREA]; @@ -1324,6 +1384,8 @@ index 932d209..06ee636 100644 + NSUInteger ax_idx = [self accessibilityIndexForCharpos:best_charpos]; + if (cachedText && ax_idx > [cachedText length]) + ax_idx = [cachedText length]; ++ ++ unblock_input (); + return NSMakeRange (ax_idx, 1); +} + @@ -1338,7 +1400,7 @@ index 932d209..06ee636 100644 + +- (NSRect)accessibilityFrame +{ -+ struct window *w = self.emacsWindow; ++ struct window *w = [self validWindow]; + if (!w) + return NSZeroRect; + @@ -1366,7 +1428,7 @@ index 932d209..06ee636 100644 + +- (void)postAccessibilityNotificationsForFrame:(struct frame *)f +{ -+ struct window *w = self.emacsWindow; ++ struct window *w = [self validWindow]; + if (!w || !WINDOW_LEAF_P (w)) + return; + @@ -1812,7 +1874,7 @@ index 932d209..06ee636 100644 + +- (NSString *)accessibilityLabel +{ -+ struct window *w = self.emacsWindow; ++ struct window *w = [self validWindow]; + if (w && WINDOW_LEAF_P (w)) + { + struct buffer *b = XBUFFER (w->contents); @@ -1831,7 +1893,7 @@ index 932d209..06ee636 100644 + +- (id)accessibilityValue +{ -+ struct window *w = self.emacsWindow; ++ struct window *w = [self validWindow]; + if (!w) + return @""; + return ns_ax_mode_line_text (w); @@ -1839,7 +1901,7 @@ index 932d209..06ee636 100644 + +- (NSRect)accessibilityFrame +{ -+ struct window *w = self.emacsWindow; ++ struct window *w = [self validWindow]; + if (!w || !w->current_matrix) + return NSZeroRect; + @@ -2069,19 +2131,10 @@ index 932d209..06ee636 100644 +#endif + [currentCursor setOnMouseEntered: YES]; +#endif -+} -+ -+ -+ -+/*****************************************************************************/ -+/* Keyboard handling. */ -+#define NS_KEYLOG 0 -+ -+- (void)keyDown: (NSEvent *)theEvent - { - Mouse_HLInfo *hlinfo = MOUSE_HL_INFO (emacsframe); - int code; -@@ -8237,6 +9838,28 @@ - (void)windowDidBecomeKey /* for direct calls */ + } + + +@@ -8237,6 +9896,28 @@ - (void)windowDidBecomeKey /* for direct calls */ XSETFRAME (event.frame_or_window, emacsframe); kbd_buffer_store_event (&event); ns_send_appdefined (-1); // Kick main loop @@ -2110,7 +2163,7 @@ index 932d209..06ee636 100644 } -@@ -9474,6 +11097,298 @@ - (int) fullscreenState +@@ -9474,6 +11155,304 @@ - (int) fullscreenState return fs_state; } @@ -2147,7 +2200,7 @@ index 932d209..06ee636 100644 + { + [elem retain]; + } -+ elem.emacsWindow = w; ++ elem.lispWindow = window; + [elements addObject:elem]; + [elem release]; + @@ -2157,7 +2210,7 @@ index 932d209..06ee636 100644 + EmacsAccessibilityModeLine *ml + = [[EmacsAccessibilityModeLine alloc] init]; + ml.emacsView = view; -+ ml.emacsWindow = w; ++ ml.lispWindow = window; + [elements addObject:ml]; + [ml release]; + } @@ -2186,10 +2239,10 @@ index 932d209..06ee636 100644 + for (EmacsAccessibilityElement *elem in accessibilityElements) + { + if ([elem isKindOfClass:[EmacsAccessibilityBuffer class]] -+ && elem.emacsWindow) ++ && !NILP (elem.lispWindow)) + [existing setObject:elem + forKey:[NSValue valueWithPointer: -+ elem.emacsWindow]]; ++ XWINDOW (elem.lispWindow)]]; + } + } + @@ -2248,7 +2301,7 @@ index 932d209..06ee636 100644 + for (EmacsAccessibilityElement *elem in accessibilityElements) + { + if ([elem isKindOfClass:[EmacsAccessibilityBuffer class]] -+ && elem.emacsWindow == sel) ++ && EQ (elem.lispWindow, emacsframe->selected_window)) + return elem; + } + return self; @@ -2262,6 +2315,9 @@ index 932d209..06ee636 100644 + elements with current values, making change detection impossible. */ +- (void)postAccessibilityUpdates +{ ++ NSCAssert ([NSThread isMainThread], ++ @"postAccessibilityUpdates must be called on the main thread"); ++ + if (!emacsframe) + return; + @@ -2271,6 +2327,7 @@ index 932d209..06ee636 100644 + if (accessibilityUpdating) + return; + accessibilityUpdating = YES; ++ @try { + + /* Detect window tree change (split, delete, new buffer). Compare + FRAME_ROOT_WINDOW — if it changed, the tree structure changed. */ @@ -2308,7 +2365,7 @@ index 932d209..06ee636 100644 + { + if ([elem isKindOfClass:[EmacsAccessibilityBuffer class]]) + { -+ struct window *w = elem.emacsWindow; ++ struct window *w = [elem validWindow]; + if (w && WINDOW_LEAF_P (w) + && BUFFERP (w->contents) && XBUFFER (w->contents)) + [(EmacsAccessibilityBuffer *) elem @@ -2338,7 +2395,9 @@ index 932d209..06ee636 100644 + NSAccessibilityFocusedUIElementChangedNotification); + } + -+ accessibilityUpdating = NO; ++ } @finally { ++ accessibilityUpdating = NO; ++ } +} + +/* ---- Cursor position for Zoom (via accessibilityBoundsForRange:) ----