patches: fix Tab navigation in completion buffer (probe order + Tab detection)
This commit is contained in:
@@ -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>
|
||||
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))
|
||||
+ {
|
||||
+ /* 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;
|
||||
-#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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user