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 65f6799..65e3be3 100644 --- a/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch +++ b/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch @@ -1,13 +1,13 @@ -From 1caa0476b3109ad583715c2f8a90c943780ffcb9 Mon Sep 17 00:00:00 2001 +From 03ad0337a0f4cf8b01261eb34068fb17cc925e96 Mon Sep 17 00:00:00 2001 From: Martin Sukany -Date: Fri, 27 Feb 2026 09:57:00 +0100 +Date: Fri, 27 Feb 2026 10:10:36 +0100 Subject: [PATCH] ns: implement VoiceOver accessibility (AXBoundsForRange, line nav, completions) --- src/nsterm.h | 73 ++ - src/nsterm.m | 2249 +++++++++++++++++++++++++++++++++++++++++++++++--- - 2 files changed, 2187 insertions(+), 135 deletions(-) + src/nsterm.m | 2262 +++++++++++++++++++++++++++++++++++++++++++++++--- + 2 files changed, 2210 insertions(+), 125 deletions(-) diff --git a/src/nsterm.h b/src/nsterm.h index 7c1ee4c..717a838 100644 @@ -108,7 +108,7 @@ index 7c1ee4c..717a838 100644 diff --git a/src/nsterm.m b/src/nsterm.m -index 932d209..220dccf 100644 +index 932d209..0fa6b2d 100644 --- a/src/nsterm.m +++ b/src/nsterm.m @@ -46,6 +46,7 @@ Updated by Christian Limpach (chris@nice.ch) @@ -169,7 +169,7 @@ index 932d209..220dccf 100644 ns_focus (f, NULL, 0); NSGraphicsContext *ctx = [NSGraphicsContext currentContext]; -@@ -6849,207 +6886,1829 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg +@@ -6849,194 +6886,1849 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg /* ========================================================================== @@ -508,20 +508,23 @@ index 932d209..220dccf 100644 + ptrdiff_t best_end = 0; + ptrdiff_t best_dist = PTRDIFF_MAX; + BOOL found = NO; - -- if (!font_panel_result && FRAME_FONT (emacsframe)) -+ /* Fast path: look at point and immediate neighbors first. */ -+ ptrdiff_t probes[3] = { point, point - 1, point + 1 }; ++ ++ /* Fast path: look at point and immediate neighbors first. ++ Prefer point+1 over point-1: when Tab moves to a new completion, ++ point is at the START of the new entry while point-1 is still ++ inside the previous entry's overlay. Forward probe finds the ++ correct new entry; backward probe finds the wrong old one. */ ++ ptrdiff_t probes[3] = { point, point + 1, point - 1 }; + for (int i = 0; i < 3 && !found; i++) - { -- font_panel_result -- = macfont_get_nsctfont (FRAME_FONT (emacsframe)); ++ { + ptrdiff_t p = probes[i]; + if (p < begv || p > zv) + continue; -- if (font_panel_result) -- [font_panel_result retain]; +- if (!font_panel_result && FRAME_FONT (emacsframe)) +- { +- font_panel_result +- = macfont_get_nsctfont (FRAME_FONT (emacsframe)); + Lisp_Object overlays = Foverlays_at (make_fixnum (p), Qnil); + Lisp_Object tail; + for (tail = overlays; CONSP (tail); tail = XCDR (tail)) @@ -543,9 +546,10 @@ index 932d209..220dccf 100644 + found = YES; + break; + } - } ++ } -- [NSApp stop: self]; +- if (font_panel_result) +- [font_panel_result retain]; + if (!found) + { + for (ptrdiff_t scan = begv; scan < zv; scan++) @@ -582,8 +586,9 @@ index 932d209..220dccf 100644 + } + } + } -+ } -+ + } + +- [NSApp stop: self]; + if (!found) + return NO; + @@ -605,7 +610,19 @@ index 932d209..220dccf 100644 - [font_panel_result release]; - font_panel_result = nil; + if (!FIXNUMP (ev)) -+ return false; ++ { ++ /* Handle symbol events: backtab (S-Tab = previous completion). */ ++ if (SYMBOLP (ev) && !NILP (ev)) ++ { ++ if (EQ (ev, intern ("backtab"))) ++ { ++ if (which) ++ *which = -1; ++ return true; ++ } ++ } ++ return false; ++ } - [NSApp stop: self]; + EMACS_INT c = XFIXNUM (ev); @@ -621,6 +638,12 @@ index 932d209..220dccf 100644 + *which = -1; + return true; + } ++ if (c == 9) /* Tab — next completion/link */ ++ { ++ if (which) ++ *which = 1; ++ return true; ++ } + return false; } -#endif @@ -674,10 +697,10 @@ index 932d209..220dccf 100644 + struct buffer *oldb = current_buffer; + if (b != current_buffer) + set_buffer_internal_1 (b); - -- font_panel_active = YES; -- timeout = make_timespec (0, 100000000); -+ /* Prefer canonical completion candidate string from text property. */ ++ ++ /* Prefer canonical completion candidate string from text property. ++ Try both completion--string (new API, set by minibuffer.el) and ++ completion (older API used by some modes). */ + ptrdiff_t probes[2] = { start, end - 1 }; + for (int i = 0; i < 2 && !text; i++) + { @@ -687,6 +710,25 @@ index 932d209..220dccf 100644 + Qnil); + if (STRINGP (cstr)) + text = [NSString stringWithLispString:cstr]; ++ if (!text) ++ { ++ /* Fallback: 'completion property used by display-completion-list. */ ++ cstr = Fget_char_property (make_fixnum (p), ++ intern ("completion"), ++ Qnil); ++ if (STRINGP (cstr)) ++ text = [NSString stringWithLispString:cstr]; ++ } ++ } + +- font_panel_active = YES; +- timeout = make_timespec (0, 100000000); ++ if (!text) ++ { ++ NSUInteger ax_s = [elem accessibilityIndexForCharpos:start]; ++ NSUInteger ax_e = [elem accessibilityIndexForCharpos:end]; ++ if (ax_e > ax_s && ax_e <= [cachedText length]) ++ text = [cachedText substringWithRange:NSMakeRange (ax_s, ax_e - ax_s)]; + } - block_input (); @@ -699,23 +741,11 @@ index 932d209..220dccf 100644 - ) - ns_select_1 (0, NULL, NULL, NULL, &timeout, NULL, YES); - unblock_input (); -+ if (!text) -+ { -+ NSUInteger ax_s = [elem accessibilityIndexForCharpos:start]; -+ NSUInteger ax_e = [elem accessibilityIndexForCharpos:end]; -+ if (ax_e > ax_s && ax_e <= [cachedText length]) -+ text = [cachedText substringWithRange:NSMakeRange (ax_s, ax_e - ax_s)]; -+ } - -- if (font_panel_result) -- [font_panel_result autorelease]; + if (b != oldb) + set_buffer_internal_1 (oldb); --#ifdef NS_IMPL_COCOA -- if (!canceled) -- font_panel_result = nil; --#endif +- if (font_panel_result) +- [font_panel_result autorelease]; + if (text) + { + text = [text stringByTrimmingCharactersInSet: @@ -724,48 +754,45 @@ index 932d209..220dccf 100644 + text = nil; + } -- result = font_panel_result; -- font_panel_result = nil; +-#ifdef NS_IMPL_COCOA +- if (!canceled) +- font_panel_result = nil; +-#endif + return text; +} +- result = font_panel_result; +- font_panel_result = nil; + - [[fm fontPanel: YES] setIsVisible: NO]; - font_panel_active = NO; ++@implementation EmacsAccessibilityElement - if (result) - return ns_font_desc_to_font_spec ([result fontDescriptor], - result); -+@implementation EmacsAccessibilityElement - -- return Qnil; +- (instancetype)init +{ + self = [super init]; + if (self) + self.lispWindow = Qnil; + return self; - } ++} --- (BOOL)acceptsFirstResponder +- return Qnil; +/* 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 +-- (BOOL)acceptsFirstResponder +- (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; @@ -773,36 +800,32 @@ index 932d209..220dccf 100644 + NSRect r = NSMakeRect (x, y, ew, eh); + NSRect winRect = [view convertRect:r toView:nil]; + return [[view window] convertRectToScreen:winRect]; ++} ++ ++- (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 + -+- (BOOL)isAccessibilityElement ++- (id)accessibilityWindow { - 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)accessibilityParent -+{ -+ return NSAccessibilityUnignoredAncestor (self.emacsView); -+} - --#if defined (NS_IMPL_GNUSTEP) || MAC_OS_X_VERSION_MIN_REQUIRED < 101300 --#if defined (NS_IMPL_COCOA) && MAC_OS_X_VERSION_MAX_ALLOWED >= 101300 -- if ([currentCursor respondsToSelector: @selector(setOnMouseEntered:)]) --#endif -- [currentCursor setOnMouseEntered: YES]; --#endif -+- (id)accessibilityWindow -+{ + return [self.emacsView window]; +} + @@ -2118,23 +2141,10 @@ index 932d209..220dccf 100644 + NSRect visible = [self visibleRect]; + NSCursor *currentCursor = FRAME_POINTER_TYPE (emacsframe); + NSTRACE ("[EmacsView resetCursorRects]"); -+ -+ if (currentCursor == nil) -+ currentCursor = [NSCursor arrowCursor]; -+ -+ if (!NSIsEmptyRect (visible)) -+ [self addCursorRect: visible cursor: currentCursor]; -+ -+#if defined (NS_IMPL_GNUSTEP) || MAC_OS_X_VERSION_MIN_REQUIRED < 101300 -+#if defined (NS_IMPL_COCOA) && MAC_OS_X_VERSION_MAX_ALLOWED >= 101300 -+ if ([currentCursor respondsToSelector: @selector(setOnMouseEntered:)]) -+#endif -+ [currentCursor setOnMouseEntered: YES]; -+#endif - } - -@@ -8237,6 +9896,28 @@ - (void)windowDidBecomeKey /* for direct calls */ + if (currentCursor == nil) + currentCursor = [NSCursor arrowCursor]; +@@ -8237,6 +9929,28 @@ - (void)windowDidBecomeKey /* for direct calls */ XSETFRAME (event.frame_or_window, emacsframe); kbd_buffer_store_event (&event); ns_send_appdefined (-1); // Kick main loop @@ -2163,7 +2173,7 @@ index 932d209..220dccf 100644 } -@@ -9474,6 +11155,304 @@ - (int) fullscreenState +@@ -9474,6 +11188,304 @@ - (int) fullscreenState return fs_state; }