patches: fix Tab navigation in completion buffer (probe order + Tab detection)

This commit is contained in:
2026-02-27 10:10:39 +01:00
parent 1245253e15
commit 7a0e7722f7

View File

@@ -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 <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
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 Subject: [PATCH] ns: implement VoiceOver accessibility (AXBoundsForRange, line
nav, completions) nav, completions)
--- ---
src/nsterm.h | 73 ++ src/nsterm.h | 73 ++
src/nsterm.m | 2249 +++++++++++++++++++++++++++++++++++++++++++++++--- src/nsterm.m | 2262 +++++++++++++++++++++++++++++++++++++++++++++++---
2 files changed, 2187 insertions(+), 135 deletions(-) 2 files changed, 2210 insertions(+), 125 deletions(-)
diff --git a/src/nsterm.h b/src/nsterm.h diff --git a/src/nsterm.h b/src/nsterm.h
index 7c1ee4c..717a838 100644 index 7c1ee4c..717a838 100644
@@ -108,7 +108,7 @@ index 7c1ee4c..717a838 100644
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index 932d209..220dccf 100644 index 932d209..0fa6b2d 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -46,6 +46,7 @@ Updated by Christian Limpach (chris@nice.ch) @@ -46,6 +46,7 @@ Updated by Christian Limpach (chris@nice.ch)
@@ -169,7 +169,7 @@ index 932d209..220dccf 100644
ns_focus (f, NULL, 0); ns_focus (f, NULL, 0);
NSGraphicsContext *ctx = [NSGraphicsContext currentContext]; 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_end = 0;
+ ptrdiff_t best_dist = PTRDIFF_MAX; + ptrdiff_t best_dist = PTRDIFF_MAX;
+ BOOL found = NO; + BOOL found = NO;
+
- if (!font_panel_result && FRAME_FONT (emacsframe)) + /* Fast path: look at point and immediate neighbors first.
+ /* Fast path: look at point and immediate neighbors first. */ + Prefer point+1 over point-1: when Tab moves to a new completion,
+ ptrdiff_t probes[3] = { point, point - 1, point + 1 }; + 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++) + for (int i = 0; i < 3 && !found; i++)
{ + {
- font_panel_result
- = macfont_get_nsctfont (FRAME_FONT (emacsframe));
+ ptrdiff_t p = probes[i]; + ptrdiff_t p = probes[i];
+ if (p < begv || p > zv) + if (p < begv || p > zv)
+ continue; + continue;
- if (font_panel_result) - if (!font_panel_result && FRAME_FONT (emacsframe))
- [font_panel_result retain]; - {
- font_panel_result
- = macfont_get_nsctfont (FRAME_FONT (emacsframe));
+ Lisp_Object overlays = Foverlays_at (make_fixnum (p), Qnil); + Lisp_Object overlays = Foverlays_at (make_fixnum (p), Qnil);
+ Lisp_Object tail; + Lisp_Object tail;
+ for (tail = overlays; CONSP (tail); tail = XCDR (tail)) + for (tail = overlays; CONSP (tail); tail = XCDR (tail))
@@ -543,9 +546,10 @@ index 932d209..220dccf 100644
+ found = YES; + found = YES;
+ break; + break;
+ } + }
} + }
- [NSApp stop: self]; - if (font_panel_result)
- [font_panel_result retain];
+ if (!found) + if (!found)
+ { + {
+ for (ptrdiff_t scan = begv; scan < zv; scan++) + for (ptrdiff_t scan = begv; scan < zv; scan++)
@@ -582,8 +586,9 @@ index 932d209..220dccf 100644
+ } + }
+ } + }
+ } + }
+ } }
+
- [NSApp stop: self];
+ if (!found) + if (!found)
+ return NO; + return NO;
+ +
@@ -605,7 +610,19 @@ index 932d209..220dccf 100644
- [font_panel_result release]; - [font_panel_result release];
- font_panel_result = nil; - font_panel_result = nil;
+ if (!FIXNUMP (ev)) + 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]; - [NSApp stop: self];
+ EMACS_INT c = XFIXNUM (ev); + EMACS_INT c = XFIXNUM (ev);
@@ -621,6 +638,12 @@ index 932d209..220dccf 100644
+ *which = -1; + *which = -1;
+ return true; + return true;
+ } + }
+ if (c == 9) /* Tab — next completion/link */
+ {
+ if (which)
+ *which = 1;
+ return true;
+ }
+ return false; + return false;
} }
-#endif -#endif
@@ -674,10 +697,10 @@ index 932d209..220dccf 100644
+ struct buffer *oldb = current_buffer; + struct buffer *oldb = current_buffer;
+ if (b != current_buffer) + if (b != current_buffer)
+ set_buffer_internal_1 (b); + set_buffer_internal_1 (b);
+
- font_panel_active = YES; + /* Prefer canonical completion candidate string from text property.
- timeout = make_timespec (0, 100000000); + Try both completion--string (new API, set by minibuffer.el) and
+ /* Prefer canonical completion candidate string from text property. */ + completion (older API used by some modes). */
+ ptrdiff_t probes[2] = { start, end - 1 }; + ptrdiff_t probes[2] = { start, end - 1 };
+ for (int i = 0; i < 2 && !text; i++) + for (int i = 0; i < 2 && !text; i++)
+ { + {
@@ -687,6 +710,25 @@ index 932d209..220dccf 100644
+ Qnil); + Qnil);
+ if (STRINGP (cstr)) + if (STRINGP (cstr))
+ text = [NSString stringWithLispString: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 (); - block_input ();
@@ -699,23 +741,11 @@ index 932d209..220dccf 100644
- ) - )
- ns_select_1 (0, NULL, NULL, NULL, &timeout, NULL, YES); - ns_select_1 (0, NULL, NULL, NULL, &timeout, NULL, YES);
- unblock_input (); - 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) + if (b != oldb)
+ set_buffer_internal_1 (oldb); + set_buffer_internal_1 (oldb);
-#ifdef NS_IMPL_COCOA - if (font_panel_result)
- if (!canceled) - [font_panel_result autorelease];
- font_panel_result = nil;
-#endif
+ if (text) + if (text)
+ { + {
+ text = [text stringByTrimmingCharactersInSet: + text = [text stringByTrimmingCharactersInSet:
@@ -724,48 +754,45 @@ index 932d209..220dccf 100644
+ text = nil; + text = nil;
+ } + }
- result = font_panel_result; -#ifdef NS_IMPL_COCOA
- font_panel_result = nil; - if (!canceled)
- font_panel_result = nil;
-#endif
+ return text; + return text;
+} +}
- result = font_panel_result;
- font_panel_result = nil;
- [[fm fontPanel: YES] setIsVisible: NO]; - [[fm fontPanel: YES] setIsVisible: NO];
- font_panel_active = NO; - font_panel_active = NO;
+@implementation EmacsAccessibilityElement
- if (result) - if (result)
- return ns_font_desc_to_font_spec ([result fontDescriptor], - return ns_font_desc_to_font_spec ([result fontDescriptor],
- result); - result);
+@implementation EmacsAccessibilityElement
- return Qnil;
+- (instancetype)init +- (instancetype)init
+{ +{
+ self = [super init]; + self = [super init];
+ if (self) + if (self)
+ self.lispWindow = Qnil; + self.lispWindow = Qnil;
+ return self; + return self;
} +}
-- (BOOL)acceptsFirstResponder - return Qnil;
+/* Return the associated Emacs window if it is still live, else NULL. +/* 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 + Use this instead of storing a raw struct window * which can become a
+ dangling pointer after delete-window or kill-buffer. */ + dangling pointer after delete-window or kill-buffer. */
+- (struct window *)validWindow +- (struct window *)validWindow
{ +{
- NSTRACE ("[EmacsView acceptsFirstResponder]");
- return YES;
+ if (NILP (self.lispWindow) || !WINDOW_LIVE_P (self.lispWindow)) + if (NILP (self.lispWindow) || !WINDOW_LIVE_P (self.lispWindow))
+ return NULL; + return NULL;
+ return XWINDOW (self.lispWindow); + return XWINDOW (self.lispWindow);
} }
-/* Tell NS we want to accept clicks that activate the window */ -- (BOOL)acceptsFirstResponder
-- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent
+- (NSRect)screenRectFromEmacsX:(int)x y:(int)y width:(int)ew height:(int)eh +- (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; + EmacsView *view = self.emacsView;
+ if (!view || ![view window]) + if (!view || ![view window])
+ return NSZeroRect; + return NSZeroRect;
@@ -773,36 +800,32 @@ index 932d209..220dccf 100644
+ NSRect r = NSMakeRect (x, y, ew, eh); + NSRect r = NSMakeRect (x, y, ew, eh);
+ NSRect winRect = [view convertRect:r toView:nil]; + NSRect winRect = [view convertRect:r toView:nil];
+ return [[view window] convertRectToScreen:winRect]; + 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 -- (void)resetCursorRects
+ +
+- (BOOL)isAccessibilityElement +- (id)accessibilityWindow
{ {
- NSRect visible = [self visibleRect]; - NSRect visible = [self visibleRect];
- NSCursor *currentCursor = FRAME_POINTER_TYPE (emacsframe); - NSCursor *currentCursor = FRAME_POINTER_TYPE (emacsframe);
- NSTRACE ("[EmacsView resetCursorRects]"); - 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]; + return [self.emacsView window];
+} +}
+ +
@@ -2118,23 +2141,10 @@ index 932d209..220dccf 100644
+ NSRect visible = [self visibleRect]; + NSRect visible = [self visibleRect];
+ NSCursor *currentCursor = FRAME_POINTER_TYPE (emacsframe); + NSCursor *currentCursor = FRAME_POINTER_TYPE (emacsframe);
+ NSTRACE ("[EmacsView resetCursorRects]"); + 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
}
if (currentCursor == nil)
@@ -8237,6 +9896,28 @@ - (void)windowDidBecomeKey /* for direct calls */ currentCursor = [NSCursor arrowCursor];
@@ -8237,6 +9929,28 @@ - (void)windowDidBecomeKey /* for direct calls */
XSETFRAME (event.frame_or_window, emacsframe); XSETFRAME (event.frame_or_window, emacsframe);
kbd_buffer_store_event (&event); kbd_buffer_store_event (&event);
ns_send_appdefined (-1); // Kick main loop 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; return fs_state;
} }