2396 lines
73 KiB
Diff
2396 lines
73 KiB
Diff
From 7970024f17d83610a4fd58d7ab135b2c71783049 Mon Sep 17 00:00:00 2001
|
|
From: Martin Sukany <martin@sukany.cz>
|
|
Date: Fri, 27 Feb 2026 07:31:34 +0100
|
|
Subject: [PATCH] ns: implement VoiceOver accessibility (AXBoundsForRange, line
|
|
nav, completions)
|
|
|
|
---
|
|
src/nsterm.h | 47 +-
|
|
src/nsterm.m | 2005 +++++++++++++++++++++++++++++++++++++++-----------
|
|
2 files changed, 1628 insertions(+), 424 deletions(-)
|
|
|
|
diff --git a/src/nsterm.h b/src/nsterm.h
|
|
index 4f9a1b0..22828f2 100644
|
|
--- a/src/nsterm.h
|
|
+++ b/src/nsterm.h
|
|
@@ -462,21 +462,49 @@ enum ns_return_frame_mode
|
|
#ifdef NS_IMPL_COCOA
|
|
@class EmacsView;
|
|
|
|
-/* Base class for virtual accessibility elements attached to EmacsView. */
|
|
+/* Base class for virtual accessibility elements attached to EmacsView. */
|
|
@interface EmacsAccessibilityElement : NSAccessibilityElement
|
|
@property (nonatomic, unsafe_unretained) EmacsView *emacsView;
|
|
@property (nonatomic, assign) struct window *emacsWindow;
|
|
- (NSRect)screenRectFromEmacsX:(int)x y:(int)y width:(int)w height:(int)h;
|
|
@end
|
|
|
|
-/* Virtual AXTextArea element — one per visible Emacs window (buffer). */
|
|
+/* A visible run: maps a contiguous range of accessibility indices
|
|
+ to a contiguous range of buffer character positions. Invisible
|
|
+ text is skipped, so ax_start values are consecutive across runs
|
|
+ while charpos values may have gaps. */
|
|
+typedef struct ns_ax_visible_run
|
|
+{
|
|
+ ptrdiff_t charpos; /* Buffer charpos where this visible run starts. */
|
|
+ ptrdiff_t length; /* Number of visible Emacs characters in this run. */
|
|
+ NSUInteger ax_start; /* Starting index in the accessibility string. */
|
|
+ NSUInteger ax_length; /* Length in accessibility string (UTF-16 units). */
|
|
+} ns_ax_visible_run;
|
|
+
|
|
+/* Virtual AXTextArea element — one per visible Emacs window (buffer). */
|
|
@interface EmacsAccessibilityBuffer : EmacsAccessibilityElement
|
|
+{
|
|
+ ns_ax_visible_run *visibleRuns;
|
|
+ NSUInteger visibleRunCount;
|
|
+}
|
|
+@property (nonatomic, retain) NSString *cachedText;
|
|
+@property (nonatomic, assign) ptrdiff_t cachedTextModiff;
|
|
+@property (nonatomic, assign) ptrdiff_t cachedTextStart;
|
|
@property (nonatomic, assign) ptrdiff_t cachedModiff;
|
|
@property (nonatomic, assign) ptrdiff_t cachedPoint;
|
|
-@property (nonatomic, assign) Lisp_Object cachedSelectedWindow;
|
|
-- (void)
|
|
- postAccessibilityUpdatesForWindow:(struct window *)w
|
|
- frame:(struct frame *)f;
|
|
+@property (nonatomic, assign) BOOL cachedMarkActive;
|
|
+@property (nonatomic, copy) NSString *cachedCompletionAnnouncement;
|
|
+@property (nonatomic, assign) ptrdiff_t cachedCompletionOverlayStart;
|
|
+@property (nonatomic, assign) ptrdiff_t cachedCompletionOverlayEnd;
|
|
+@property (nonatomic, assign) ptrdiff_t cachedCompletionPoint;
|
|
+- (void)invalidateTextCache;
|
|
+- (void)postAccessibilityNotificationsForFrame:(struct frame *)f;
|
|
+- (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx;
|
|
+- (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos;
|
|
+@end
|
|
+
|
|
+/* Virtual AXStaticText element — one per mode line. */
|
|
+@interface EmacsAccessibilityModeLine : EmacsAccessibilityElement
|
|
@end
|
|
#endif /* NS_IMPL_COCOA */
|
|
|
|
@@ -501,6 +529,12 @@ enum ns_return_frame_mode
|
|
BOOL maximizing_resize;
|
|
NSMutableArray *accessibilityElements;
|
|
Lisp_Object lastSelectedWindow;
|
|
+ Lisp_Object lastRootWindow;
|
|
+ BOOL accessibilityTreeValid;
|
|
+ BOOL accessibilityUpdating;
|
|
+ @public
|
|
+ NSRect lastAccessibilityCursorRect;
|
|
+ @protected
|
|
#endif
|
|
BOOL font_panel_active;
|
|
NSFont *font_panel_result;
|
|
@@ -562,6 +596,7 @@ enum ns_return_frame_mode
|
|
#ifdef NS_IMPL_COCOA
|
|
/* Accessibility support. */
|
|
- (void)rebuildAccessibilityTree;
|
|
+- (void)invalidateAccessibilityTree;
|
|
- (void)postAccessibilityUpdates;
|
|
#endif
|
|
@end
|
|
diff --git a/src/nsterm.m b/src/nsterm.m
|
|
index e67edbe..cfc5b4c 100644
|
|
--- a/src/nsterm.m
|
|
+++ b/src/nsterm.m
|
|
@@ -3237,6 +3237,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));
|
|
|
|
+#ifdef NS_IMPL_COCOA
|
|
+ /* Accessibility: store cursor rect for Zoom and bounds queries.
|
|
+ VoiceOver notifications are handled solely by
|
|
+ postAccessibilityUpdates (called from ns_update_end)
|
|
+ to avoid duplicate notifications and mid-redisplay fragility. */
|
|
+ {
|
|
+ EmacsView *view = FRAME_NS_VIEW (f);
|
|
+ if (view && on_p && active_p)
|
|
+ {
|
|
+ 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];
|
|
@@ -6860,172 +6891,174 @@ Accessibility virtual elements (macOS / Cocoa only)
|
|
|
|
#ifdef NS_IMPL_COCOA
|
|
|
|
-/* ---- Helper: extract visible text from glyph rows of a window ---- */
|
|
+/* ---- Helper: extract buffer text for accessibility ---- */
|
|
+
|
|
+/* Maximum characters exposed via accessibilityValue. */
|
|
+#define NS_AX_TEXT_CAP 100000
|
|
+
|
|
+/* Build accessibility text for window W, skipping invisible text.
|
|
+ Populates *OUT_START with the buffer start charpos.
|
|
+ Populates *OUT_RUNS with an array of visible runs and *OUT_NRUNS
|
|
+ with the count. Caller must free *OUT_RUNS with xfree(). */
|
|
+
|
|
static NSString *
|
|
-ns_ax_text_from_glyph_rows (struct window *w)
|
|
+ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start,
|
|
+ ns_ax_visible_run **out_runs, NSUInteger *out_nruns)
|
|
{
|
|
- if (!w || !w->current_matrix)
|
|
- return @"";
|
|
-
|
|
- struct glyph_matrix *matrix = w->current_matrix;
|
|
- NSMutableString *text = [NSMutableString stringWithCapacity:4096];
|
|
- int nrows = matrix->nrows;
|
|
+ *out_runs = NULL;
|
|
+ *out_nruns = 0;
|
|
|
|
- for (int i = 0; i < nrows; i++)
|
|
+ if (!w || !WINDOW_LEAF_P (w))
|
|
{
|
|
- struct glyph_row *row = matrix->rows + i;
|
|
- if (!row->enabled_p || row->mode_line_p)
|
|
- continue;
|
|
- if (!row->displays_text_p && !row->ends_at_zv_p)
|
|
- continue;
|
|
+ *out_start = 0;
|
|
+ return @"";
|
|
+ }
|
|
|
|
- struct glyph *glyph = row->glyphs[TEXT_AREA];
|
|
- struct glyph *end = glyph + row->used[TEXT_AREA];
|
|
+ struct buffer *b = XBUFFER (w->contents);
|
|
+ if (!b)
|
|
+ {
|
|
+ *out_start = 0;
|
|
+ return @"";
|
|
+ }
|
|
|
|
- for (; glyph < end; glyph++)
|
|
- {
|
|
- if (glyph->type == CHAR_GLYPH && !glyph->padding_p)
|
|
- {
|
|
- unsigned ch = glyph->u.ch;
|
|
- if (ch == '\n' || ch == '\r')
|
|
- continue; /* row boundary handles newlines */
|
|
- if (ch >= 32)
|
|
- {
|
|
- unichar uch = (unichar)ch;
|
|
- [text appendString:[NSString stringWithCharacters:&uch
|
|
- length:1]];
|
|
- }
|
|
- }
|
|
- }
|
|
+ ptrdiff_t begv = BUF_BEGV (b);
|
|
+ ptrdiff_t zv = BUF_ZV (b);
|
|
|
|
- /* Add newline between rows unless this is the last displayed row. */
|
|
- if (i + 1 < nrows)
|
|
- {
|
|
- struct glyph_row *next = matrix->rows + i + 1;
|
|
- if (next->enabled_p && (next->displays_text_p || next->ends_at_zv_p)
|
|
- && !next->mode_line_p)
|
|
- [text appendString:@"\n"];
|
|
- }
|
|
- }
|
|
+ *out_start = begv;
|
|
|
|
- /* Cap at 32KB */
|
|
- if ([text length] > 32768)
|
|
- return [text substringToIndex:32768];
|
|
+ if (zv <= begv)
|
|
+ return @"";
|
|
|
|
- return text;
|
|
-}
|
|
+ struct buffer *oldb = current_buffer;
|
|
+ if (b != current_buffer)
|
|
+ set_buffer_internal_1 (b);
|
|
+
|
|
+ /* First pass: count visible runs to allocate the mapping array. */
|
|
+ NSUInteger run_capacity = 64;
|
|
+ ns_ax_visible_run *runs = xmalloc (run_capacity
|
|
+ * sizeof (ns_ax_visible_run));
|
|
+ NSUInteger nruns = 0;
|
|
+ NSUInteger ax_offset = 0;
|
|
+
|
|
+ NSMutableString *result = [NSMutableString string];
|
|
+ ptrdiff_t pos = begv;
|
|
+
|
|
+ while (pos < zv)
|
|
+ {
|
|
+ /* Check invisible property (text properties + overlays). */
|
|
+ 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))
|
|
+ {
|
|
+ /* Skip to the next position where invisible changes. */
|
|
+ Lisp_Object next = Fnext_single_char_property_change (
|
|
+ make_fixnum (pos), Qinvisible, Qnil, make_fixnum (zv));
|
|
+ pos = FIXNUMP (next) ? XFIXNUM (next) : zv;
|
|
+ continue;
|
|
+ }
|
|
|
|
+ /* Find end of this visible run: where invisible property changes. */
|
|
+ Lisp_Object next = Fnext_single_char_property_change (
|
|
+ make_fixnum (pos), Qinvisible, Qnil, make_fixnum (zv));
|
|
+ ptrdiff_t run_end = FIXNUMP (next) ? XFIXNUM (next) : zv;
|
|
|
|
-/* ---- Row geometry helpers ---- */
|
|
+ /* Cap total text at NS_AX_TEXT_CAP. */
|
|
+ ptrdiff_t run_len = run_end - pos;
|
|
+ if (ax_offset + (NSUInteger) run_len > NS_AX_TEXT_CAP)
|
|
+ run_len = (ptrdiff_t) (NS_AX_TEXT_CAP - ax_offset);
|
|
+ if (run_len <= 0)
|
|
+ break;
|
|
+ run_end = pos + run_len;
|
|
+
|
|
+ /* Extract this visible run's text. Use
|
|
+ Fbuffer_substring_no_properties which correctly handles the
|
|
+ buffer gap — raw BUF_BYTE_ADDRESS reads across the gap would
|
|
+ include garbage bytes when the run spans the gap position. */
|
|
+ Lisp_Object lstr = Fbuffer_substring_no_properties (
|
|
+ make_fixnum (pos), make_fixnum (run_end));
|
|
+ NSString *nsstr = [NSString stringWithLispString:lstr];
|
|
+ NSUInteger ns_len = [nsstr length];
|
|
+ [result appendString:nsstr];
|
|
+
|
|
+ /* Record this visible run in the mapping. */
|
|
+ if (nruns >= run_capacity)
|
|
+ {
|
|
+ run_capacity *= 2;
|
|
+ runs = xrealloc (runs, run_capacity
|
|
+ * sizeof (ns_ax_visible_run));
|
|
+ }
|
|
+ runs[nruns].charpos = pos;
|
|
+ runs[nruns].length = run_len;
|
|
+ runs[nruns].ax_start = ax_offset;
|
|
+ runs[nruns].ax_length = ns_len;
|
|
+ nruns++;
|
|
|
|
-/* Count the number of visible text rows (excluding mode line). */
|
|
-static int
|
|
-ns_ax_visible_row_count (struct window *w)
|
|
-{
|
|
- if (!w || !w->current_matrix)
|
|
- return 0;
|
|
- struct glyph_matrix *matrix = w->current_matrix;
|
|
- int count = 0;
|
|
- for (int i = 0; i < matrix->nrows; i++)
|
|
- {
|
|
- struct glyph_row *row = matrix->rows + i;
|
|
- if (row->enabled_p && !row->mode_line_p
|
|
- && (row->displays_text_p || row->ends_at_zv_p))
|
|
- count++;
|
|
+ ax_offset += ns_len;
|
|
+ pos = run_end;
|
|
}
|
|
- return count;
|
|
-}
|
|
|
|
-/* Map a character index (within the glyph-extracted text) to a visual
|
|
- row number (0-based, text rows only). */
|
|
-static int
|
|
-ns_ax_line_for_index (struct window *w, NSUInteger idx)
|
|
-{
|
|
- if (!w || !w->current_matrix)
|
|
- return 0;
|
|
- struct glyph_matrix *matrix = w->current_matrix;
|
|
- NSUInteger pos = 0;
|
|
- int line = 0;
|
|
+ if (b != oldb)
|
|
+ set_buffer_internal_1 (oldb);
|
|
|
|
- for (int i = 0; i < matrix->nrows; i++)
|
|
- {
|
|
- struct glyph_row *row = matrix->rows + i;
|
|
- if (!row->enabled_p || row->mode_line_p)
|
|
- continue;
|
|
- if (!row->displays_text_p && !row->ends_at_zv_p)
|
|
- continue;
|
|
+ *out_runs = runs;
|
|
+ *out_nruns = nruns;
|
|
+ return result;
|
|
+}
|
|
|
|
- /* Count characters in this row. */
|
|
- int row_chars = 0;
|
|
- struct glyph *g = row->glyphs[TEXT_AREA];
|
|
- struct glyph *gend = g + row->used[TEXT_AREA];
|
|
- for (; g < gend; g++)
|
|
- {
|
|
- if (g->type == CHAR_GLYPH && !g->padding_p)
|
|
- {
|
|
- unsigned ch = g->u.ch;
|
|
- if (ch != '\n' && ch != '\r' && (ch >= 32))
|
|
- row_chars++;
|
|
- }
|
|
- }
|
|
|
|
- NSUInteger row_end = pos + row_chars + 1; /* +1 for newline */
|
|
- if (idx < row_end)
|
|
- return line;
|
|
- pos = row_end;
|
|
- line++;
|
|
- }
|
|
- return MAX(0, line - 1);
|
|
-}
|
|
+/* ---- Helper: extract mode line text from glyph rows ---- */
|
|
|
|
-/* Return character range for a given visual line number. */
|
|
-static NSRange
|
|
-ns_ax_range_for_line (struct window *w, int target_line)
|
|
+static NSString *
|
|
+ns_ax_mode_line_text (struct window *w)
|
|
{
|
|
if (!w || !w->current_matrix)
|
|
- return NSMakeRange(0, 0);
|
|
+ return @"";
|
|
+
|
|
struct glyph_matrix *matrix = w->current_matrix;
|
|
- NSUInteger pos = 0;
|
|
- int line = 0;
|
|
+ NSMutableString *text = [NSMutableString string];
|
|
|
|
for (int i = 0; i < matrix->nrows; i++)
|
|
{
|
|
struct glyph_row *row = matrix->rows + i;
|
|
- if (!row->enabled_p || row->mode_line_p)
|
|
- continue;
|
|
- if (!row->displays_text_p && !row->ends_at_zv_p)
|
|
- continue;
|
|
+ if (!row->enabled_p || !row->mode_line_p)
|
|
+ continue;
|
|
|
|
- int row_chars = 0;
|
|
struct glyph *g = row->glyphs[TEXT_AREA];
|
|
- struct glyph *gend = g + row->used[TEXT_AREA];
|
|
- for (; g < gend; g++)
|
|
- {
|
|
- if (g->type == CHAR_GLYPH && !g->padding_p)
|
|
- {
|
|
- unsigned ch = g->u.ch;
|
|
- if (ch != '\n' && ch != '\r' && (ch >= 32))
|
|
- row_chars++;
|
|
- }
|
|
- }
|
|
-
|
|
- if (line == target_line)
|
|
- return NSMakeRange(pos, row_chars);
|
|
-
|
|
- pos += row_chars + 1; /* +1 for newline */
|
|
- line++;
|
|
+ struct glyph *end = g + row->used[TEXT_AREA];
|
|
+ for (; g < end; g++)
|
|
+ {
|
|
+ if (g->type == CHAR_GLYPH && g->u.ch >= 32)
|
|
+ {
|
|
+ unichar uch = (unichar) g->u.ch;
|
|
+ [text appendString:[NSString stringWithCharacters:&uch
|
|
+ length:1]];
|
|
+ }
|
|
+ }
|
|
}
|
|
- return NSMakeRange(NSNotFound, 0);
|
|
+ return text;
|
|
}
|
|
|
|
-/* Compute screen rect for a character range by unioning glyph row rects. */
|
|
+
|
|
+/* ---- Helper: screen rect for a character range via glyph matrix ---- */
|
|
+
|
|
static NSRect
|
|
-ns_ax_frame_for_range (struct window *w, EmacsView *view, NSRange range)
|
|
+ns_ax_frame_for_range (struct window *w, EmacsView *view,
|
|
+ ptrdiff_t text_start, NSRange range)
|
|
{
|
|
if (!w || !w->current_matrix || !view)
|
|
return NSZeroRect;
|
|
+
|
|
+ /* Convert range indices back to buffer charpos. */
|
|
+ ptrdiff_t cp_start = text_start + (ptrdiff_t) range.location;
|
|
+ ptrdiff_t cp_end = cp_start + (ptrdiff_t) range.length;
|
|
+
|
|
struct glyph_matrix *matrix = w->current_matrix;
|
|
- NSUInteger pos = 0;
|
|
NSRect result = NSZeroRect;
|
|
BOOL found = NO;
|
|
|
|
@@ -7033,121 +7066,274 @@ row number (0-based, text rows only). */
|
|
{
|
|
struct glyph_row *row = matrix->rows + i;
|
|
if (!row->enabled_p || row->mode_line_p)
|
|
- continue;
|
|
+ continue;
|
|
if (!row->displays_text_p && !row->ends_at_zv_p)
|
|
- continue;
|
|
+ continue;
|
|
|
|
- int row_chars = 0;
|
|
- struct glyph *g = row->glyphs[TEXT_AREA];
|
|
- struct glyph *gend = g + row->used[TEXT_AREA];
|
|
- for (; g < gend; g++)
|
|
- {
|
|
- if (g->type == CHAR_GLYPH && !g->padding_p)
|
|
- {
|
|
- unsigned ch = g->u.ch;
|
|
- if (ch != '\n' && ch != '\r' && (ch >= 32))
|
|
- row_chars++;
|
|
- }
|
|
- }
|
|
+ ptrdiff_t row_start = MATRIX_ROW_START_CHARPOS (row);
|
|
+ ptrdiff_t row_end = MATRIX_ROW_END_CHARPOS (row);
|
|
|
|
- NSUInteger row_end = pos + row_chars + 1;
|
|
- if (pos < range.location + range.length && row_end > range.location)
|
|
- {
|
|
- /* This row overlaps the requested range. */
|
|
- int window_x, window_y, window_width;
|
|
- window_box (w, TEXT_AREA, &window_x, &window_y, &window_width, 0);
|
|
-
|
|
- NSRect rowRect;
|
|
- rowRect.origin.x = window_x;
|
|
- rowRect.origin.y = WINDOW_TO_FRAME_PIXEL_Y (w, MAX(0, row->y));
|
|
- rowRect.origin.y = MAX(rowRect.origin.y, window_y);
|
|
- rowRect.size.width = window_width;
|
|
- rowRect.size.height = row->visible_height;
|
|
-
|
|
- if (!found)
|
|
- {
|
|
- result = rowRect;
|
|
- found = YES;
|
|
- }
|
|
- else
|
|
- result = NSUnionRect(result, rowRect);
|
|
- }
|
|
- pos = row_end;
|
|
+ if (row_start < cp_end && row_end > cp_start)
|
|
+ {
|
|
+ int window_x, window_y, window_width;
|
|
+ window_box (w, TEXT_AREA, &window_x, &window_y,
|
|
+ &window_width, 0);
|
|
+
|
|
+ NSRect rowRect;
|
|
+ rowRect.origin.x = window_x;
|
|
+ rowRect.origin.y = WINDOW_TO_FRAME_PIXEL_Y (w, MAX (0, row->y));
|
|
+ rowRect.origin.y = MAX (rowRect.origin.y, window_y);
|
|
+ rowRect.size.width = window_width;
|
|
+ rowRect.size.height = row->height;
|
|
+
|
|
+ if (!found)
|
|
+ {
|
|
+ result = rowRect;
|
|
+ found = YES;
|
|
+ }
|
|
+ else
|
|
+ result = NSUnionRect (result, rowRect);
|
|
+ }
|
|
}
|
|
|
|
if (!found)
|
|
return NSZeroRect;
|
|
|
|
+ /* Clip result to text area bounds. */
|
|
+ {
|
|
+ int text_area_x, text_area_y, text_area_w, text_area_h;
|
|
+ window_box (w, TEXT_AREA, &text_area_x, &text_area_y,
|
|
+ &text_area_w, &text_area_h);
|
|
+ CGFloat max_y = WINDOW_TO_FRAME_PIXEL_Y (w, text_area_y + text_area_h);
|
|
+ if (NSMaxY (result) > max_y)
|
|
+ result.size.height = max_y - result.origin.y;
|
|
+ }
|
|
+
|
|
/* Convert from EmacsView (flipped) coords to screen coords. */
|
|
NSRect winRect = [view convertRect:result toView:nil];
|
|
return [[view window] convertRectToScreen:winRect];
|
|
}
|
|
|
|
+/* AX enum numeric compatibility for NSAccessibility notifications.
|
|
+ Values match WebKit AXObjectCacheMac fallback enums
|
|
+ (AXTextStateChangeType / AXTextEditType / AXTextSelectionDirection /
|
|
+ AXTextSelectionGranularity). */
|
|
+enum {
|
|
+ ns_ax_text_state_change_unknown = 0,
|
|
+ ns_ax_text_state_change_edit = 1,
|
|
+ ns_ax_text_state_change_selection_move = 2,
|
|
+
|
|
+ ns_ax_text_edit_type_typing = 3,
|
|
+
|
|
+ ns_ax_text_selection_direction_unknown = 0,
|
|
+ ns_ax_text_selection_direction_previous = 3,
|
|
+ ns_ax_text_selection_direction_next = 4,
|
|
+ ns_ax_text_selection_direction_discontiguous = 5,
|
|
+
|
|
+ ns_ax_text_selection_granularity_unknown = 0,
|
|
+ ns_ax_text_selection_granularity_character = 1,
|
|
+ ns_ax_text_selection_granularity_line = 3,
|
|
+};
|
|
|
|
-/* Compute the character index within glyph-extracted text that
|
|
- corresponds to the buffer point position. */
|
|
static NSUInteger
|
|
-ns_ax_index_for_point (struct window *w)
|
|
+ns_ax_utf16_length_for_buffer_range (struct buffer *b, ptrdiff_t start,
|
|
+ ptrdiff_t end)
|
|
{
|
|
- if (!w || !w->current_matrix || !WINDOW_LEAF_P(w))
|
|
+ if (!b || end <= start)
|
|
return 0;
|
|
|
|
- struct buffer *b = XBUFFER(w->contents);
|
|
- if (!b)
|
|
- return 0;
|
|
+ struct buffer *oldb = current_buffer;
|
|
+ if (b != current_buffer)
|
|
+ set_buffer_internal_1 (b);
|
|
|
|
- ptrdiff_t point = BUF_PT(b);
|
|
- struct glyph_matrix *matrix = w->current_matrix;
|
|
- NSUInteger pos = 0;
|
|
+ Lisp_Object lstr = Fbuffer_substring_no_properties (make_fixnum (start),
|
|
+ make_fixnum (end));
|
|
+ NSString *nsstr = [NSString stringWithLispString:lstr];
|
|
+ NSUInteger len = [nsstr length];
|
|
|
|
- for (int i = 0; i < matrix->nrows; i++)
|
|
+ if (b != oldb)
|
|
+ set_buffer_internal_1 (oldb);
|
|
+
|
|
+ return len;
|
|
+}
|
|
+
|
|
+static BOOL
|
|
+ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point,
|
|
+ ptrdiff_t *out_start,
|
|
+ ptrdiff_t *out_end)
|
|
+{
|
|
+ if (!b || !out_start || !out_end)
|
|
+ return NO;
|
|
+
|
|
+ Lisp_Object faceSym = intern ("completions-highlight");
|
|
+ ptrdiff_t begv = BUF_BEGV (b);
|
|
+ ptrdiff_t zv = BUF_ZV (b);
|
|
+ ptrdiff_t best_start = 0;
|
|
+ ptrdiff_t best_end = 0;
|
|
+ ptrdiff_t best_dist = PTRDIFF_MAX;
|
|
+ BOOL found = NO;
|
|
+
|
|
+ /* Fast path: look at point and immediate neighbors first. */
|
|
+ ptrdiff_t probes[3] = { point, point - 1, point + 1 };
|
|
+ for (int i = 0; i < 3 && !found; i++)
|
|
{
|
|
- struct glyph_row *row = matrix->rows + i;
|
|
- if (!row->enabled_p || row->mode_line_p)
|
|
- continue;
|
|
- if (!row->displays_text_p && !row->ends_at_zv_p)
|
|
- continue;
|
|
+ ptrdiff_t p = probes[i];
|
|
+ if (p < begv || p > zv)
|
|
+ continue;
|
|
|
|
- ptrdiff_t row_start = MATRIX_ROW_START_CHARPOS (row);
|
|
- ptrdiff_t row_end = MATRIX_ROW_END_CHARPOS (row);
|
|
+ Lisp_Object overlays = Foverlays_at (make_fixnum (p), Qnil);
|
|
+ Lisp_Object tail;
|
|
+ for (tail = overlays; CONSP (tail); tail = XCDR (tail))
|
|
+ {
|
|
+ Lisp_Object ov = XCAR (tail);
|
|
+ Lisp_Object face = Foverlay_get (ov, Qface);
|
|
+ if (!(EQ (face, faceSym)
|
|
+ || (CONSP (face) && !NILP (Fmemq (faceSym, face)))))
|
|
+ continue;
|
|
+
|
|
+ ptrdiff_t ov_start = OVERLAY_START (ov);
|
|
+ ptrdiff_t ov_end = OVERLAY_END (ov);
|
|
+ if (ov_end <= ov_start)
|
|
+ continue;
|
|
+
|
|
+ best_start = ov_start;
|
|
+ best_end = ov_end;
|
|
+ best_dist = 0;
|
|
+ found = YES;
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
|
|
- if (point >= row_start && point < row_end)
|
|
- {
|
|
- /* Point is within this row. Count visible glyphs whose
|
|
- buffer charpos is before point. */
|
|
- int chars_before = 0;
|
|
- struct glyph *g = row->glyphs[TEXT_AREA];
|
|
- struct glyph *gend = g + row->used[TEXT_AREA];
|
|
- for (; g < gend; g++)
|
|
- {
|
|
- if (g->type == CHAR_GLYPH && !g->padding_p
|
|
- && g->charpos >= row_start
|
|
- && g->charpos < point)
|
|
- {
|
|
- unsigned ch = g->u.ch;
|
|
- if (ch != '\n' && ch != '\r' && ch >= 32)
|
|
- chars_before++;
|
|
- }
|
|
- }
|
|
- return pos + chars_before;
|
|
- }
|
|
+ if (!found)
|
|
+ {
|
|
+ for (ptrdiff_t scan = begv; scan < zv; scan++)
|
|
+ {
|
|
+ Lisp_Object overlays = Foverlays_at (make_fixnum (scan), Qnil);
|
|
+ Lisp_Object tail;
|
|
+ for (tail = overlays; CONSP (tail); tail = XCDR (tail))
|
|
+ {
|
|
+ Lisp_Object ov = XCAR (tail);
|
|
+ Lisp_Object face = Foverlay_get (ov, Qface);
|
|
+ if (!(EQ (face, faceSym)
|
|
+ || (CONSP (face) && !NILP (Fmemq (faceSym, face)))))
|
|
+ continue;
|
|
+
|
|
+ ptrdiff_t ov_start = OVERLAY_START (ov);
|
|
+ ptrdiff_t ov_end = OVERLAY_END (ov);
|
|
+ if (ov_end <= ov_start)
|
|
+ continue;
|
|
+
|
|
+ ptrdiff_t dist = 0;
|
|
+ if (point < ov_start)
|
|
+ dist = ov_start - point;
|
|
+ else if (point > ov_end)
|
|
+ dist = point - ov_end;
|
|
+
|
|
+ if (!found || dist < best_dist
|
|
+ || (dist == best_dist
|
|
+ && (ov_start < point && best_start >= point)))
|
|
+ {
|
|
+ best_start = ov_start;
|
|
+ best_end = ov_end;
|
|
+ best_dist = dist;
|
|
+ found = YES;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
|
|
- /* Count visible chars in this row + newline. */
|
|
- int row_chars = 0;
|
|
- struct glyph *g = row->glyphs[TEXT_AREA];
|
|
- struct glyph *gend = g + row->used[TEXT_AREA];
|
|
- for (; g < gend; g++)
|
|
- {
|
|
- if (g->type == CHAR_GLYPH && !g->padding_p)
|
|
- {
|
|
- unsigned ch = g->u.ch;
|
|
- if (ch != '\n' && ch != '\r' && (ch >= 32))
|
|
- row_chars++;
|
|
- }
|
|
- }
|
|
- pos += row_chars + 1;
|
|
+ if (!found)
|
|
+ return NO;
|
|
+
|
|
+ *out_start = best_start;
|
|
+ *out_end = best_end;
|
|
+ return YES;
|
|
+}
|
|
+
|
|
+static bool
|
|
+ns_ax_event_is_ctrl_n_or_p (int *which)
|
|
+{
|
|
+ Lisp_Object ev = last_command_event;
|
|
+ if (CONSP (ev))
|
|
+ ev = EVENT_HEAD (ev);
|
|
+
|
|
+ if (!FIXNUMP (ev))
|
|
+ return false;
|
|
+
|
|
+ EMACS_INT c = XFIXNUM (ev);
|
|
+ if (c == 14) /* C-n */
|
|
+ {
|
|
+ if (which)
|
|
+ *which = 1;
|
|
+ return true;
|
|
+ }
|
|
+ if (c == 16) /* C-p */
|
|
+ {
|
|
+ if (which)
|
|
+ *which = -1;
|
|
+ return true;
|
|
+ }
|
|
+ return false;
|
|
+}
|
|
+
|
|
+static bool
|
|
+ns_ax_command_is_basic_line_move (void)
|
|
+{
|
|
+ if (!SYMBOLP (real_this_command))
|
|
+ return false;
|
|
+
|
|
+ Lisp_Object next = intern ("next-line");
|
|
+ Lisp_Object prev = intern ("previous-line");
|
|
+ return EQ (real_this_command, next) || EQ (real_this_command, prev);
|
|
+}
|
|
+
|
|
+static NSString *
|
|
+ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
|
|
+ struct buffer *b,
|
|
+ ptrdiff_t start,
|
|
+ ptrdiff_t end,
|
|
+ NSString *cachedText)
|
|
+{
|
|
+ if (!elem || !b || !cachedText || end <= start)
|
|
+ return nil;
|
|
+
|
|
+ NSString *text = nil;
|
|
+ struct buffer *oldb = current_buffer;
|
|
+ if (b != current_buffer)
|
|
+ set_buffer_internal_1 (b);
|
|
+
|
|
+ /* Prefer canonical completion candidate string from text property. */
|
|
+ ptrdiff_t probes[2] = { start, end - 1 };
|
|
+ for (int i = 0; i < 2 && !text; i++)
|
|
+ {
|
|
+ ptrdiff_t p = probes[i];
|
|
+ Lisp_Object cstr = Fget_char_property (make_fixnum (p),
|
|
+ intern ("completion--string"),
|
|
+ Qnil);
|
|
+ if (STRINGP (cstr))
|
|
+ text = [NSString stringWithLispString:cstr];
|
|
+ }
|
|
+
|
|
+ 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 (b != oldb)
|
|
+ set_buffer_internal_1 (oldb);
|
|
+
|
|
+ if (text)
|
|
+ {
|
|
+ text = [text stringByTrimmingCharactersInSet:
|
|
+ [NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
+ if ([text length] == 0)
|
|
+ text = nil;
|
|
}
|
|
- return pos > 0 ? pos - 1 : 0;
|
|
+
|
|
+ return text;
|
|
}
|
|
|
|
|
|
@@ -7159,7 +7345,7 @@ - (NSRect)screenRectFromEmacsX:(int)x y:(int)y width:(int)ew height:(int)eh
|
|
if (!view || ![view window])
|
|
return NSZeroRect;
|
|
|
|
- NSRect r = NSMakeRect(x, y, ew, eh);
|
|
+ NSRect r = NSMakeRect (x, y, ew, eh);
|
|
NSRect winRect = [view convertRect:r toView:nil];
|
|
return [[view window] convertRectToScreen:winRect];
|
|
}
|
|
@@ -7169,130 +7355,475 @@ - (BOOL)isAccessibilityElement
|
|
return YES;
|
|
}
|
|
|
|
-@end
|
|
-
|
|
-
|
|
-@implementation EmacsAccessibilityBuffer
|
|
+/* ---- Hierarchy plumbing (required for VoiceOver to find us) ---- */
|
|
|
|
-/* ---- NSAccessibility protocol ---- */
|
|
-
|
|
-- (NSAccessibilityRole)accessibilityRole
|
|
+- (id)accessibilityParent
|
|
{
|
|
- return NSAccessibilityTextAreaRole;
|
|
+ return NSAccessibilityUnignoredAncestor (self.emacsView);
|
|
}
|
|
|
|
-- (NSString *)accessibilityRoleDescription
|
|
+- (id)accessibilityWindow
|
|
{
|
|
- return @"editor";
|
|
+ return [self.emacsView window];
|
|
}
|
|
|
|
-- (NSString *)accessibilityLabel
|
|
+- (id)accessibilityTopLevelUIElement
|
|
{
|
|
- struct window *w = self.emacsWindow;
|
|
- if (w && WINDOW_LEAF_P(w))
|
|
- {
|
|
- struct buffer *b = XBUFFER(w->contents);
|
|
- if (b)
|
|
- {
|
|
- Lisp_Object name = BVAR(b, name);
|
|
- if (STRINGP(name))
|
|
- return [NSString stringWithLispString:name];
|
|
- }
|
|
- }
|
|
- return @"buffer";
|
|
+ return [self.emacsView window];
|
|
}
|
|
|
|
-- (id)accessibilityValue
|
|
-{
|
|
- struct window *w = self.emacsWindow;
|
|
- if (!w)
|
|
- return @"";
|
|
- return ns_ax_text_from_glyph_rows(w);
|
|
-}
|
|
+@end
|
|
|
|
-- (NSInteger)accessibilityNumberOfCharacters
|
|
-{
|
|
- NSString *text = [self accessibilityValue];
|
|
- return [text length];
|
|
-}
|
|
|
|
-- (NSString *)accessibilitySelectedText
|
|
+@implementation EmacsAccessibilityBuffer
|
|
+@synthesize cachedText;
|
|
+@synthesize cachedTextModiff;
|
|
+@synthesize cachedTextStart;
|
|
+@synthesize cachedModiff;
|
|
+@synthesize cachedPoint;
|
|
+@synthesize cachedMarkActive;
|
|
+@synthesize cachedCompletionAnnouncement;
|
|
+@synthesize cachedCompletionOverlayStart;
|
|
+@synthesize cachedCompletionOverlayEnd;
|
|
+@synthesize cachedCompletionPoint;
|
|
+
|
|
+- (void)dealloc
|
|
{
|
|
- struct window *w = self.emacsWindow;
|
|
- if (!w || !WINDOW_LEAF_P(w))
|
|
- return @"";
|
|
+ [cachedText release];
|
|
+ [cachedCompletionAnnouncement release];
|
|
+ if (visibleRuns)
|
|
+ xfree (visibleRuns);
|
|
+ [super dealloc];
|
|
+}
|
|
|
|
- struct buffer *b = XBUFFER(w->contents);
|
|
- if (!b || NILP(BVAR(b, mark_active)))
|
|
- return @"";
|
|
+/* ---- Text cache ---- */
|
|
|
|
- /* Return the selected region text. */
|
|
- NSString *text = [self accessibilityValue];
|
|
- NSRange sel = [self accessibilitySelectedTextRange];
|
|
- if (sel.location == NSNotFound || sel.location + sel.length > [text length])
|
|
- return @"";
|
|
- return [text substringWithRange:sel];
|
|
+- (void)invalidateTextCache
|
|
+{
|
|
+ [cachedText release];
|
|
+ cachedText = nil;
|
|
+ if (visibleRuns)
|
|
+ {
|
|
+ xfree (visibleRuns);
|
|
+ visibleRuns = NULL;
|
|
+ }
|
|
+ visibleRunCount = 0;
|
|
}
|
|
|
|
-- (NSRange)accessibilitySelectedTextRange
|
|
+- (void)ensureTextCache
|
|
{
|
|
struct window *w = self.emacsWindow;
|
|
- if (!w || !WINDOW_LEAF_P(w))
|
|
- return NSMakeRange(0, 0);
|
|
+ if (!w || !WINDOW_LEAF_P (w))
|
|
+ return;
|
|
|
|
- struct buffer *b = XBUFFER(w->contents);
|
|
+ struct buffer *b = XBUFFER (w->contents);
|
|
if (!b)
|
|
- return NSMakeRange(0, 0);
|
|
+ return;
|
|
|
|
- NSUInteger point_idx = ns_ax_index_for_point(w);
|
|
+ ptrdiff_t modiff = BUF_MODIFF (b);
|
|
+ ptrdiff_t pt = BUF_PT (b);
|
|
+ NSUInteger textLen = cachedText ? [cachedText length] : 0;
|
|
+ if (cachedText && cachedTextModiff == modiff
|
|
+ && pt >= cachedTextStart
|
|
+ && (textLen == 0
|
|
+ || [self accessibilityIndexForCharpos:pt] <= textLen))
|
|
+ return;
|
|
|
|
- if (NILP(BVAR(b, mark_active)))
|
|
- return NSMakeRange(point_idx, 0);
|
|
+ ptrdiff_t start;
|
|
+ ns_ax_visible_run *runs = NULL;
|
|
+ NSUInteger nruns = 0;
|
|
+ NSString *text = ns_ax_buffer_text (w, &start, &runs, &nruns);
|
|
|
|
- /* With active mark, report the selection range. Map mark
|
|
- position to accessibility index using the same glyph-based
|
|
- mapping as point. */
|
|
- ptrdiff_t mark_pos = marker_position (BVAR (b, mark));
|
|
- ptrdiff_t pt_pos = BUF_PT (b);
|
|
- ptrdiff_t begv = BUF_BEGV (b);
|
|
- ptrdiff_t sel_start = (mark_pos < pt_pos) ? mark_pos : pt_pos;
|
|
- ptrdiff_t sel_end = (mark_pos < pt_pos) ? pt_pos : mark_pos;
|
|
- NSUInteger start_idx = (NSUInteger) (sel_start - begv);
|
|
- NSUInteger len = (NSUInteger) (sel_end - sel_start);
|
|
- return NSMakeRange(start_idx, len);
|
|
+ [cachedText release];
|
|
+ cachedText = [text retain];
|
|
+ cachedTextModiff = modiff;
|
|
+ cachedTextStart = start;
|
|
+
|
|
+ if (visibleRuns)
|
|
+ xfree (visibleRuns);
|
|
+ visibleRuns = runs;
|
|
+ visibleRunCount = nruns;
|
|
}
|
|
|
|
-- (NSInteger)accessibilityInsertionPointLineNumber
|
|
+/* ---- Index mapping ---- */
|
|
+
|
|
+/* Convert buffer charpos to accessibility string index. */
|
|
+- (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos
|
|
{
|
|
struct window *w = self.emacsWindow;
|
|
- if (!w)
|
|
- return 0;
|
|
- NSUInteger idx = ns_ax_index_for_point(w);
|
|
- return ns_ax_line_for_index(w, idx);
|
|
+ struct buffer *b = (w && WINDOW_LEAF_P (w)) ? XBUFFER (w->contents) : NULL;
|
|
+
|
|
+ for (NSUInteger i = 0; i < visibleRunCount; i++)
|
|
+ {
|
|
+ ns_ax_visible_run *r = &visibleRuns[i];
|
|
+ if (charpos >= r->charpos && charpos < r->charpos + r->length)
|
|
+ {
|
|
+ if (!b)
|
|
+ return r->ax_start;
|
|
+ NSUInteger delta = ns_ax_utf16_length_for_buffer_range (b, r->charpos,
|
|
+ charpos);
|
|
+ if (delta > r->ax_length)
|
|
+ delta = r->ax_length;
|
|
+ return r->ax_start + delta;
|
|
+ }
|
|
+ /* If charpos falls in an invisible gap before the next run,
|
|
+ map it to the start of the next visible run. */
|
|
+ if (charpos < r->charpos)
|
|
+ return r->ax_start;
|
|
+ }
|
|
+ /* Past end — return total length. */
|
|
+ if (visibleRunCount > 0)
|
|
+ {
|
|
+ ns_ax_visible_run *last = &visibleRuns[visibleRunCount - 1];
|
|
+ return last->ax_start + last->ax_length;
|
|
+ }
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+/* Convert accessibility string index to buffer charpos. */
|
|
+- (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx
|
|
+{
|
|
+ struct window *w = self.emacsWindow;
|
|
+ struct buffer *b = (w && WINDOW_LEAF_P (w)) ? XBUFFER (w->contents) : NULL;
|
|
+
|
|
+ for (NSUInteger i = 0; i < visibleRunCount; i++)
|
|
+ {
|
|
+ ns_ax_visible_run *r = &visibleRuns[i];
|
|
+ if (ax_idx >= r->ax_start
|
|
+ && ax_idx < r->ax_start + r->ax_length)
|
|
+ {
|
|
+ if (!b)
|
|
+ return r->charpos;
|
|
+
|
|
+ NSUInteger target = ax_idx - r->ax_start;
|
|
+ ptrdiff_t lo = r->charpos;
|
|
+ ptrdiff_t hi = r->charpos + r->length;
|
|
+
|
|
+ while (lo < hi)
|
|
+ {
|
|
+ ptrdiff_t mid = lo + (hi - lo) / 2;
|
|
+ NSUInteger mid_len = ns_ax_utf16_length_for_buffer_range (b,
|
|
+ r->charpos,
|
|
+ mid);
|
|
+ if (mid_len < target)
|
|
+ lo = mid + 1;
|
|
+ else
|
|
+ hi = mid;
|
|
+ }
|
|
+
|
|
+ return lo;
|
|
+ }
|
|
+ }
|
|
+ /* Past end — return last charpos. */
|
|
+ if (visibleRunCount > 0)
|
|
+ {
|
|
+ ns_ax_visible_run *last = &visibleRuns[visibleRunCount - 1];
|
|
+ return last->charpos + last->length;
|
|
+ }
|
|
+ return cachedTextStart;
|
|
+}
|
|
+
|
|
+/* ---- NSAccessibility protocol ---- */
|
|
+
|
|
+- (NSAccessibilityRole)accessibilityRole
|
|
+{
|
|
+ return NSAccessibilityTextAreaRole;
|
|
+}
|
|
+
|
|
+- (NSString *)accessibilityRoleDescription
|
|
+{
|
|
+ struct window *w = self.emacsWindow;
|
|
+ if (w && MINI_WINDOW_P (w))
|
|
+ return @"minibuffer";
|
|
+ return @"editor";
|
|
+}
|
|
+
|
|
+- (NSString *)accessibilityLabel
|
|
+{
|
|
+ struct window *w = self.emacsWindow;
|
|
+ if (w && WINDOW_LEAF_P (w))
|
|
+ {
|
|
+ if (MINI_WINDOW_P (w))
|
|
+ return @"Minibuffer";
|
|
+
|
|
+ struct buffer *b = XBUFFER (w->contents);
|
|
+ if (b)
|
|
+ {
|
|
+ Lisp_Object name = BVAR (b, name);
|
|
+ if (STRINGP (name))
|
|
+ return [NSString stringWithLispString:name];
|
|
+ }
|
|
+ }
|
|
+ return @"buffer";
|
|
+}
|
|
+
|
|
+- (BOOL)isAccessibilityFocused
|
|
+{
|
|
+ struct window *w = self.emacsWindow;
|
|
+ if (!w)
|
|
+ return NO;
|
|
+ EmacsView *view = self.emacsView;
|
|
+ if (!view || !view->emacsframe)
|
|
+ return NO;
|
|
+ struct frame *f = view->emacsframe;
|
|
+ return (w == XWINDOW (f->selected_window));
|
|
+}
|
|
+
|
|
+- (id)accessibilityValue
|
|
+{
|
|
+ [self ensureTextCache];
|
|
+ return cachedText ? cachedText : @"";
|
|
+}
|
|
+
|
|
+- (NSInteger)accessibilityNumberOfCharacters
|
|
+{
|
|
+ [self ensureTextCache];
|
|
+ return cachedText ? [cachedText length] : 0;
|
|
+}
|
|
+
|
|
+- (NSString *)accessibilitySelectedText
|
|
+{
|
|
+ struct window *w = self.emacsWindow;
|
|
+ if (!w || !WINDOW_LEAF_P (w))
|
|
+ return @"";
|
|
+
|
|
+ struct buffer *b = XBUFFER (w->contents);
|
|
+ if (!b || NILP (BVAR (b, mark_active)))
|
|
+ return @"";
|
|
+
|
|
+ NSRange sel = [self accessibilitySelectedTextRange];
|
|
+ [self ensureTextCache];
|
|
+ if (!cachedText || sel.location == NSNotFound
|
|
+ || sel.location + sel.length > [cachedText length])
|
|
+ return @"";
|
|
+ return [cachedText substringWithRange:sel];
|
|
+}
|
|
+
|
|
+- (NSRange)accessibilitySelectedTextRange
|
|
+{
|
|
+ struct window *w = self.emacsWindow;
|
|
+ if (!w || !WINDOW_LEAF_P (w))
|
|
+ return NSMakeRange (0, 0);
|
|
+
|
|
+ if (!BUFFERP (w->contents))
|
|
+ return NSMakeRange (0, 0);
|
|
+ struct buffer *b = XBUFFER (w->contents);
|
|
+ if (!b)
|
|
+ return NSMakeRange (0, 0);
|
|
+
|
|
+ [self ensureTextCache];
|
|
+ ptrdiff_t pt = BUF_PT (b);
|
|
+ NSUInteger point_idx = [self accessibilityIndexForCharpos:pt];
|
|
+
|
|
+ if (NILP (BVAR (b, mark_active)))
|
|
+ return NSMakeRange (point_idx, 0);
|
|
+
|
|
+ ptrdiff_t mark_pos = marker_position (BVAR (b, mark));
|
|
+ NSUInteger mark_idx = [self accessibilityIndexForCharpos:mark_pos];
|
|
+ NSUInteger start_idx = MIN (point_idx, mark_idx);
|
|
+ NSUInteger end_idx = MAX (point_idx, mark_idx);
|
|
+ return NSMakeRange (start_idx, end_idx - start_idx);
|
|
+}
|
|
+
|
|
+- (void)setAccessibilitySelectedTextRange:(NSRange)range
|
|
+{
|
|
+ struct window *w = self.emacsWindow;
|
|
+ if (!w || !WINDOW_LEAF_P (w))
|
|
+ return;
|
|
+
|
|
+ struct buffer *b = XBUFFER (w->contents);
|
|
+ if (!b)
|
|
+ return;
|
|
+
|
|
+ [self ensureTextCache];
|
|
+
|
|
+ /* Convert accessibility index to buffer charpos via mapping. */
|
|
+ ptrdiff_t charpos = [self charposForAccessibilityIndex:range.location];
|
|
+
|
|
+ /* Clamp to buffer bounds. */
|
|
+ if (charpos < BUF_BEGV (b))
|
|
+ charpos = BUF_BEGV (b);
|
|
+ if (charpos > BUF_ZV (b))
|
|
+ charpos = BUF_ZV (b);
|
|
+
|
|
+ block_input ();
|
|
+
|
|
+ /* Move point directly in the buffer. Use set_point_both which
|
|
+ operates on the current buffer — temporarily switch if needed. */
|
|
+ struct buffer *oldb = current_buffer;
|
|
+ if (b != current_buffer)
|
|
+ set_buffer_internal_1 (b);
|
|
+
|
|
+ SET_PT_BOTH (charpos, CHAR_TO_BYTE (charpos));
|
|
+
|
|
+ /* Keep mark state aligned with requested selection range. */
|
|
+ if (range.length > 0)
|
|
+ {
|
|
+ ptrdiff_t mark_charpos = [self charposForAccessibilityIndex:
|
|
+ range.location + range.length];
|
|
+ if (mark_charpos > BUF_ZV (b))
|
|
+ mark_charpos = BUF_ZV (b);
|
|
+ Fset_marker (BVAR (b, mark), make_fixnum (mark_charpos),
|
|
+ Fcurrent_buffer ());
|
|
+ bset_mark_active (b, Qt);
|
|
+ }
|
|
+ else
|
|
+ bset_mark_active (b, Qnil);
|
|
+
|
|
+ if (b != oldb)
|
|
+ set_buffer_internal_1 (oldb);
|
|
+
|
|
+ unblock_input ();
|
|
+
|
|
+ /* Update cached state so the next notification cycle doesn't
|
|
+ re-announce this movement. */
|
|
+ self.cachedPoint = charpos;
|
|
+ self.cachedMarkActive = (range.length > 0);
|
|
+}
|
|
+
|
|
+- (void)setAccessibilityFocused:(BOOL)flag
|
|
+{
|
|
+ if (!flag)
|
|
+ return;
|
|
+
|
|
+ struct window *w = self.emacsWindow;
|
|
+ if (!w || !WINDOW_LEAF_P (w))
|
|
+ return;
|
|
+
|
|
+ EmacsView *view = self.emacsView;
|
|
+ if (!view || !view->emacsframe)
|
|
+ return;
|
|
+
|
|
+ block_input ();
|
|
+
|
|
+ /* Raise the frame's NS window to ensure keyboard focus. */
|
|
+ NSWindow *nswin = [view window];
|
|
+ if (nswin && ![nswin isKeyWindow])
|
|
+ [nswin makeKeyAndOrderFront:nil];
|
|
+
|
|
+ unblock_input ();
|
|
+
|
|
+ /* Post SelectedTextChanged so VoiceOver reads the current line
|
|
+ upon entering text interaction mode.
|
|
+ WebKit AXObjectCacheMac fallback enum: SelectionMove = 2. */
|
|
+ NSDictionary *info = @{@"AXTextStateChangeType": @(ns_ax_text_state_change_selection_move),
|
|
+ @"AXTextChangeElement": self};
|
|
+ NSAccessibilityPostNotificationWithUserInfo (
|
|
+ self, NSAccessibilitySelectedTextChangedNotification, info);
|
|
+}
|
|
+
|
|
+- (NSInteger)accessibilityInsertionPointLineNumber
|
|
+{
|
|
+ struct window *w = self.emacsWindow;
|
|
+ if (!w || !WINDOW_LEAF_P (w))
|
|
+ return 0;
|
|
+
|
|
+ struct buffer *b = XBUFFER (w->contents);
|
|
+ if (!b)
|
|
+ return 0;
|
|
+
|
|
+ [self ensureTextCache];
|
|
+ if (!cachedText)
|
|
+ return 0;
|
|
+
|
|
+ ptrdiff_t pt = BUF_PT (b);
|
|
+ NSUInteger point_idx = [self accessibilityIndexForCharpos:pt];
|
|
+ if (point_idx > [cachedText length])
|
|
+ point_idx = [cachedText length];
|
|
+
|
|
+ /* Count newlines from start to point_idx. */
|
|
+ NSInteger line = 0;
|
|
+ for (NSUInteger i = 0; i < point_idx; i++)
|
|
+ {
|
|
+ if ([cachedText characterAtIndex:i] == '\n')
|
|
+ line++;
|
|
+ }
|
|
+ return line;
|
|
}
|
|
|
|
- (NSString *)accessibilityStringForRange:(NSRange)range
|
|
{
|
|
- NSString *text = [self accessibilityValue];
|
|
- if (range.location + range.length > [text length])
|
|
+ [self ensureTextCache];
|
|
+ if (!cachedText || range.location + range.length > [cachedText length])
|
|
return @"";
|
|
- return [text substringWithRange:range];
|
|
+ return [cachedText substringWithRange:range];
|
|
+}
|
|
+
|
|
+- (NSAttributedString *)accessibilityAttributedStringForRange:(NSRange)range
|
|
+{
|
|
+ NSString *str = [self accessibilityStringForRange:range];
|
|
+ return [[[NSAttributedString alloc] initWithString:str] autorelease];
|
|
}
|
|
|
|
- (NSInteger)accessibilityLineForIndex:(NSInteger)index
|
|
{
|
|
- struct window *w = self.emacsWindow;
|
|
- if (!w)
|
|
+ [self ensureTextCache];
|
|
+ if (!cachedText || index < 0)
|
|
return 0;
|
|
- return ns_ax_line_for_index(w, (NSUInteger)index);
|
|
+
|
|
+ NSUInteger idx = (NSUInteger) index;
|
|
+ if (idx > [cachedText length])
|
|
+ idx = [cachedText length];
|
|
+
|
|
+ /* Count newlines from start of cachedText to idx. */
|
|
+ NSInteger line = 0;
|
|
+ for (NSUInteger i = 0; i < idx; i++)
|
|
+ {
|
|
+ if ([cachedText characterAtIndex:i] == '\n')
|
|
+ line++;
|
|
+ }
|
|
+ return line;
|
|
}
|
|
|
|
- (NSRange)accessibilityRangeForLine:(NSInteger)line
|
|
{
|
|
- struct window *w = self.emacsWindow;
|
|
- if (!w)
|
|
- return NSMakeRange(0, 0);
|
|
- return ns_ax_range_for_line(w, (int)line);
|
|
+ [self ensureTextCache];
|
|
+ if (!cachedText || line < 0)
|
|
+ return NSMakeRange (NSNotFound, 0);
|
|
+
|
|
+ NSUInteger len = [cachedText length];
|
|
+ NSInteger cur_line = 0;
|
|
+
|
|
+ for (NSUInteger i = 0; i <= len; i++)
|
|
+ {
|
|
+ if (cur_line == line)
|
|
+ {
|
|
+ /* Find end of this line. */
|
|
+ NSUInteger line_end = i;
|
|
+ while (line_end < len
|
|
+ && [cachedText characterAtIndex:line_end] != '\n')
|
|
+ line_end++;
|
|
+ /* Include the trailing newline so empty lines have length 1. */
|
|
+ if (line_end < len
|
|
+ && [cachedText characterAtIndex:line_end] == '\n')
|
|
+ line_end++;
|
|
+ return NSMakeRange (i, line_end - i);
|
|
+ }
|
|
+ if (i < len && [cachedText characterAtIndex:i] == '\n')
|
|
+ {
|
|
+ cur_line++;
|
|
+ }
|
|
+ }
|
|
+ /* Phantom final line after the last newline. */
|
|
+ if (cur_line == line)
|
|
+ return NSMakeRange (len, 0);
|
|
+ return NSMakeRange (NSNotFound, 0);
|
|
+}
|
|
+
|
|
+- (NSRange)accessibilityRangeForIndex:(NSInteger)index
|
|
+{
|
|
+ [self ensureTextCache];
|
|
+ if (!cachedText || index < 0
|
|
+ || (NSUInteger) index >= [cachedText length])
|
|
+ return NSMakeRange (NSNotFound, 0);
|
|
+ return [cachedText rangeOfComposedCharacterSequenceAtIndex:(NSUInteger)index];
|
|
+}
|
|
+
|
|
+- (NSRange)accessibilityStyleRangeForIndex:(NSInteger)index
|
|
+{
|
|
+ /* Return the range of the current line — simple approach. */
|
|
+ NSInteger line = [self accessibilityLineForIndex:index];
|
|
+ return [self accessibilityRangeForLine:line];
|
|
}
|
|
|
|
- (NSRect)accessibilityFrameForRange:(NSRange)range
|
|
@@ -7301,14 +7832,18 @@ - (NSRect)accessibilityFrameForRange:(NSRange)range
|
|
EmacsView *view = self.emacsView;
|
|
if (!w || !view)
|
|
return NSZeroRect;
|
|
- return ns_ax_frame_for_range(w, view, range);
|
|
+ /* Convert ax-index range to charpos range for glyph lookup. */
|
|
+ [self ensureTextCache];
|
|
+ ptrdiff_t cp_start = [self charposForAccessibilityIndex:range.location];
|
|
+ ptrdiff_t cp_end = [self charposForAccessibilityIndex:
|
|
+ range.location + range.length];
|
|
+ NSRange charRange = NSMakeRange (0, (NSUInteger) (cp_end - cp_start));
|
|
+ return ns_ax_frame_for_range (w, view, cp_start, charRange);
|
|
}
|
|
|
|
-
|
|
- (NSRange)accessibilityRangeForPosition:(NSPoint)screenPoint
|
|
{
|
|
- /* Hit test: convert screen point to buffer character index.
|
|
- Used by VoiceOver for mouse/trackpad exploration. */
|
|
+ /* Hit test: convert screen point to buffer character index. */
|
|
struct window *w = self.emacsWindow;
|
|
EmacsView *view = self.emacsView;
|
|
if (!w || !view || !w->current_matrix)
|
|
@@ -7318,7 +7853,7 @@ - (NSRange)accessibilityRangeForPosition:(NSPoint)screenPoint
|
|
NSPoint windowPoint = [[view window] convertPointFromScreen:screenPoint];
|
|
NSPoint viewPoint = [view convertPoint:windowPoint fromView:nil];
|
|
|
|
- /* Convert to Emacs pixel coordinates (EmacsView is flipped). */
|
|
+ /* Convert to window-relative pixel coordinates. */
|
|
int x = (int) viewPoint.x - w->pixel_left;
|
|
int y = (int) viewPoint.y - w->pixel_top;
|
|
|
|
@@ -7328,19 +7863,19 @@ - (NSRange)accessibilityRangeForPosition:(NSPoint)screenPoint
|
|
/* Find the glyph row at this y coordinate. */
|
|
struct glyph_matrix *matrix = w->current_matrix;
|
|
struct glyph_row *hit_row = NULL;
|
|
- int row_y = 0;
|
|
|
|
for (int i = 0; i < matrix->nrows; i++)
|
|
{
|
|
struct glyph_row *row = matrix->rows + i;
|
|
- if (!row->enabled_p || !row->displays_text_p)
|
|
- continue;
|
|
- if (y >= row_y && y < row_y + row->visible_height)
|
|
- {
|
|
- hit_row = row;
|
|
- break;
|
|
- }
|
|
- row_y += row->visible_height;
|
|
+ if (!row->enabled_p || !row->displays_text_p || row->mode_line_p)
|
|
+ continue;
|
|
+ int row_top = WINDOW_TO_FRAME_PIXEL_Y (w, MAX (0, row->y));
|
|
+ if ((int) viewPoint.y >= row_top
|
|
+ && (int) viewPoint.y < row_top + row->visible_height)
|
|
+ {
|
|
+ hit_row = row;
|
|
+ break;
|
|
+ }
|
|
}
|
|
|
|
if (!hit_row)
|
|
@@ -7355,49 +7890,32 @@ - (NSRange)accessibilityRangeForPosition:(NSPoint)screenPoint
|
|
for (; glyph < end; glyph++)
|
|
{
|
|
if (glyph->type == CHAR_GLYPH && glyph->charpos > 0)
|
|
- {
|
|
- if (x >= glyph_x && x < glyph_x + glyph->pixel_width)
|
|
- {
|
|
- best_charpos = glyph->charpos;
|
|
- break;
|
|
- }
|
|
- best_charpos = glyph->charpos;
|
|
- }
|
|
+ {
|
|
+ if (x >= glyph_x && x < glyph_x + glyph->pixel_width)
|
|
+ {
|
|
+ best_charpos = glyph->charpos;
|
|
+ break;
|
|
+ }
|
|
+ best_charpos = glyph->charpos;
|
|
+ }
|
|
glyph_x += glyph->pixel_width;
|
|
}
|
|
|
|
- /* Convert buffer charpos to accessibility index. */
|
|
- struct buffer *b = XBUFFER (w->contents);
|
|
- if (!b)
|
|
- return NSMakeRange (0, 0);
|
|
-
|
|
- ptrdiff_t idx = best_charpos - BUF_BEGV (b);
|
|
- if (idx < 0) idx = 0;
|
|
-
|
|
- return NSMakeRange ((NSUInteger) idx, 1);
|
|
+ /* Convert buffer charpos to accessibility index via mapping. */
|
|
+ [self ensureTextCache];
|
|
+ NSUInteger ax_idx = [self accessibilityIndexForCharpos:best_charpos];
|
|
+ if (cachedText && ax_idx > [cachedText length])
|
|
+ ax_idx = [cachedText length];
|
|
+ return NSMakeRange (ax_idx, 1);
|
|
}
|
|
|
|
- (NSRange)accessibilityVisibleCharacterRange
|
|
{
|
|
- NSString *text = [self accessibilityValue];
|
|
- return NSMakeRange(0, [text length]);
|
|
-}
|
|
-
|
|
-- (BOOL)isAccessibilityFocused
|
|
-{
|
|
- struct window *w = self.emacsWindow;
|
|
- if (!w)
|
|
- return NO;
|
|
- EmacsView *view = self.emacsView;
|
|
- if (!view || !view->emacsframe)
|
|
- return NO;
|
|
- struct frame *f = view->emacsframe;
|
|
- return (w == XWINDOW(f->selected_window));
|
|
-}
|
|
-
|
|
-- (id)accessibilityParent
|
|
-{
|
|
- return NSAccessibilityUnignoredAncestor (self.emacsView);
|
|
+ /* Return the full cached text range. VoiceOver interprets the
|
|
+ visible range boundary as end-of-text, so we must expose the
|
|
+ entire buffer to avoid premature "end of text" announcements. */
|
|
+ [self ensureTextCache];
|
|
+ return NSMakeRange (0, cachedText ? [cachedText length] : 0);
|
|
}
|
|
|
|
- (NSRect)accessibilityFrame
|
|
@@ -7405,50 +7923,523 @@ - (NSRect)accessibilityFrame
|
|
struct window *w = self.emacsWindow;
|
|
if (!w)
|
|
return NSZeroRect;
|
|
+
|
|
+ /* Subtract mode line height so the buffer element does not overlap it. */
|
|
+ int text_h = w->pixel_height;
|
|
+ if (w->current_matrix)
|
|
+ {
|
|
+ for (int i = w->current_matrix->nrows - 1; i >= 0; i--)
|
|
+ {
|
|
+ struct glyph_row *row = w->current_matrix->rows + i;
|
|
+ if (row->enabled_p && row->mode_line_p)
|
|
+ {
|
|
+ text_h -= row->visible_height;
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
return [self screenRectFromEmacsX:w->pixel_left
|
|
- y:w->pixel_top
|
|
- width:w->pixel_width
|
|
- height:w->pixel_height];
|
|
+ y:w->pixel_top
|
|
+ width:w->pixel_width
|
|
+ height:text_h];
|
|
}
|
|
|
|
/* ---- Notification dispatch ---- */
|
|
|
|
-- (void)postAccessibilityUpdatesForWindow:(struct window *)w
|
|
- frame:(struct frame *)f
|
|
+- (void)postAccessibilityNotificationsForFrame:(struct frame *)f
|
|
{
|
|
- if (!w || !WINDOW_LEAF_P(w))
|
|
+ struct window *w = self.emacsWindow;
|
|
+ if (!w || !WINDOW_LEAF_P (w))
|
|
return;
|
|
|
|
- struct buffer *b = XBUFFER(w->contents);
|
|
+ struct buffer *b = XBUFFER (w->contents);
|
|
if (!b)
|
|
return;
|
|
|
|
- ptrdiff_t modiff = BUF_MODIFF(b);
|
|
- ptrdiff_t point = BUF_PT(b);
|
|
+ ptrdiff_t modiff = BUF_MODIFF (b);
|
|
+ ptrdiff_t point = BUF_PT (b);
|
|
+ BOOL markActive = !NILP (BVAR (b, mark_active));
|
|
|
|
- /* Text content changed? */
|
|
+ /* --- Text changed → typing echo ---
|
|
+ WebKit AXObjectCacheMac fallback enum: Edit = 1, Typing = 3. */
|
|
if (modiff != self.cachedModiff)
|
|
{
|
|
+ /* Capture changed char before invalidating cache. */
|
|
+ NSString *changedChar = @"";
|
|
+ if (point > self.cachedPoint
|
|
+ && point - self.cachedPoint == 1)
|
|
+ {
|
|
+ /* Single char inserted — refresh cache and grab it. */
|
|
+ [self invalidateTextCache];
|
|
+ [self ensureTextCache];
|
|
+ if (cachedText)
|
|
+ {
|
|
+ NSUInteger idx = [self accessibilityIndexForCharpos:point - 1];
|
|
+ if (idx < [cachedText length])
|
|
+ changedChar = [cachedText substringWithRange:
|
|
+ NSMakeRange (idx, 1)];
|
|
+ }
|
|
+ }
|
|
+ else
|
|
+ {
|
|
+ [self invalidateTextCache];
|
|
+ }
|
|
+
|
|
self.cachedModiff = modiff;
|
|
- NSAccessibilityPostNotification(self,
|
|
- NSAccessibilityValueChangedNotification);
|
|
+ /* Update cachedPoint here so the selection-move branch below
|
|
+ does NOT fire for point changes caused by edits. WebKit and
|
|
+ Chromium never send both ValueChanged and SelectedTextChanged
|
|
+ for the same user action — they are mutually exclusive. */
|
|
+ self.cachedPoint = point;
|
|
|
|
- /* Rich typing echo for VoiceOver. */
|
|
+ NSDictionary *change = @{
|
|
+ @"AXTextEditType": @(ns_ax_text_edit_type_typing),
|
|
+ @"AXTextChangeValue": changedChar,
|
|
+ @"AXTextChangeValueLength": @([changedChar length])
|
|
+ };
|
|
NSDictionary *userInfo = @{
|
|
- @"AXTextStateChangeType" : @1, /* AXTextStateChangeTypeEdit */
|
|
- @"AXTextEditType" : @0 /* kAXTextEditTypeTyping */
|
|
+ @"AXTextStateChangeType": @(ns_ax_text_state_change_edit),
|
|
+ @"AXTextChangeValues": @[change],
|
|
+ @"AXTextChangeElement": self
|
|
};
|
|
- NSAccessibilityPostNotificationWithUserInfo(
|
|
- self, NSAccessibilityValueChangedNotification, userInfo);
|
|
+ NSAccessibilityPostNotificationWithUserInfo (
|
|
+ self, NSAccessibilityValueChangedNotification, userInfo);
|
|
}
|
|
|
|
- /* Cursor moved? */
|
|
- if (point != self.cachedPoint)
|
|
+ /* --- Cursor moved or selection changed → line reading ---
|
|
+ WebKit AXObjectCacheMac fallback enum: SelectionMove = 2.
|
|
+ Use 'else if' — edits and selection moves are mutually exclusive
|
|
+ per the WebKit/Chromium pattern. VoiceOver gets confused if
|
|
+ both notifications arrive in the same runloop iteration. */
|
|
+ else if (point != self.cachedPoint || markActive != self.cachedMarkActive)
|
|
{
|
|
+ ptrdiff_t oldPoint = self.cachedPoint;
|
|
self.cachedPoint = point;
|
|
- NSAccessibilityPostNotification(self,
|
|
- NSAccessibilitySelectedTextChangedNotification);
|
|
+ self.cachedMarkActive = markActive;
|
|
+
|
|
+ /* Compute direction. */
|
|
+ NSInteger direction = ns_ax_text_selection_direction_discontiguous;
|
|
+ if (point > oldPoint)
|
|
+ direction = ns_ax_text_selection_direction_next;
|
|
+ else if (point < oldPoint)
|
|
+ direction = ns_ax_text_selection_direction_previous;
|
|
+
|
|
+ int ctrlNP = 0;
|
|
+ bool isCtrlNP = ns_ax_event_is_ctrl_n_or_p (&ctrlNP);
|
|
+
|
|
+ /* Compute granularity from movement distance.
|
|
+ Prefer robust line-range comparison for vertical movement,
|
|
+ otherwise single char (1) or unknown (0). */
|
|
+ NSInteger granularity = ns_ax_text_selection_granularity_unknown;
|
|
+ [self ensureTextCache];
|
|
+ if (cachedText && oldPoint > 0)
|
|
+ {
|
|
+ ptrdiff_t delta = point - oldPoint;
|
|
+ if (delta == 1 || delta == -1)
|
|
+ granularity = ns_ax_text_selection_granularity_character; /* Character. */
|
|
+ else
|
|
+ {
|
|
+ NSUInteger tlen = [cachedText length];
|
|
+ NSUInteger oldIdx = [self accessibilityIndexForCharpos:oldPoint];
|
|
+ NSUInteger newIdx = [self accessibilityIndexForCharpos:point];
|
|
+ if (oldIdx > tlen)
|
|
+ oldIdx = tlen;
|
|
+ if (newIdx > tlen)
|
|
+ newIdx = tlen;
|
|
+
|
|
+ NSRange oldLine = [cachedText lineRangeForRange:
|
|
+ NSMakeRange (oldIdx, 0)];
|
|
+ NSRange newLine = [cachedText lineRangeForRange:
|
|
+ NSMakeRange (newIdx, 0)];
|
|
+ if (oldLine.location != newLine.location)
|
|
+ granularity = ns_ax_text_selection_granularity_line; /* Line. */
|
|
+
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /* Force line semantics for explicit C-n/C-p keystrokes.
|
|
+ This isolates the key-path difference from arrow-down/up. */
|
|
+ if (isCtrlNP)
|
|
+ {
|
|
+ direction = (ctrlNP > 0
|
|
+ ? ns_ax_text_selection_direction_next
|
|
+ : ns_ax_text_selection_direction_previous);
|
|
+ granularity = ns_ax_text_selection_granularity_line;
|
|
+ }
|
|
+
|
|
+ NSDictionary *moveInfo = @{
|
|
+ @"AXTextStateChangeType": @(ns_ax_text_state_change_selection_move),
|
|
+ @"AXTextSelectionDirection": @(direction),
|
|
+ @"AXTextSelectionGranularity": @(granularity),
|
|
+ @"AXTextChangeElement": self
|
|
+ };
|
|
+ NSAccessibilityPostNotificationWithUserInfo (
|
|
+ self,
|
|
+ NSAccessibilitySelectedTextChangedNotification,
|
|
+ moveInfo);
|
|
+
|
|
+ /* C-n/C-p (`next-line' / `previous-line') can diverge from
|
|
+ arrow-down/up command paths in some major modes (completion list,
|
|
+ Dired, etc.). Emit an explicit line announcement for this basic
|
|
+ line-motion path so VoiceOver tracks the Emacs point reliably. */
|
|
+ if ([self isAccessibilityFocused]
|
|
+ && cachedText
|
|
+ && (isCtrlNP || ns_ax_command_is_basic_line_move ())
|
|
+ && (direction == ns_ax_text_selection_direction_next
|
|
+ || direction == ns_ax_text_selection_direction_previous))
|
|
+ {
|
|
+ NSUInteger point_idx = [self accessibilityIndexForCharpos:point];
|
|
+ if (point_idx <= [cachedText length])
|
|
+ {
|
|
+ NSInteger lineNum = [self accessibilityLineForIndex:point_idx];
|
|
+ NSRange lineRange = [self accessibilityRangeForLine:lineNum];
|
|
+ if (lineRange.location != NSNotFound
|
|
+ && lineRange.length > 0
|
|
+ && lineRange.location + lineRange.length <= [cachedText length])
|
|
+ {
|
|
+ NSString *lineText = [cachedText substringWithRange:lineRange];
|
|
+ lineText = [lineText stringByTrimmingCharactersInSet:
|
|
+ [NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
+ if ([lineText length] > 0)
|
|
+ {
|
|
+ NSDictionary *annInfo = @{
|
|
+ NSAccessibilityAnnouncementKey: lineText,
|
|
+ NSAccessibilityPriorityKey: @(NSAccessibilityPriorityHigh)
|
|
+ };
|
|
+ NSAccessibilityPostNotificationWithUserInfo (
|
|
+ NSApp,
|
|
+ NSAccessibilityAnnouncementRequestedNotification,
|
|
+ annInfo);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /* --- Completions announcement ---
|
|
+ When point changes in a non-focused buffer (e.g. *Completions*
|
|
+ while the minibuffer has keyboard focus), VoiceOver won't read
|
|
+ the change because it's tracking the focused element. Post an
|
|
+ announcement so the user hears the selected completion.
|
|
+
|
|
+ If there is a `completions-highlight` overlay at point (Emacs
|
|
+ highlights the selected completion candidate), read its full
|
|
+ text instead of just the current line. */
|
|
+ if (![self isAccessibilityFocused] && cachedText)
|
|
+ {
|
|
+ NSString *announceText = nil;
|
|
+ ptrdiff_t currentOverlayStart = 0;
|
|
+ ptrdiff_t currentOverlayEnd = 0;
|
|
+
|
|
+ struct buffer *oldb2 = current_buffer;
|
|
+ if (b != current_buffer)
|
|
+ set_buffer_internal_1 (b);
|
|
+
|
|
+ /* 1) Prefer explicit completion candidate property when present. */
|
|
+ Lisp_Object cstr = Fget_char_property (make_fixnum (point),
|
|
+ intern ("completion--string"),
|
|
+ Qnil);
|
|
+ if (STRINGP (cstr))
|
|
+ announceText = [NSString stringWithLispString:cstr];
|
|
+
|
|
+ /* 2) Fallback: announce the mouse-face span at point.
|
|
+ completion-list-mode often marks the active candidate this way. */
|
|
+ if (!announceText)
|
|
+ {
|
|
+ Lisp_Object mf = Fget_char_property (make_fixnum (point),
|
|
+ Qmouse_face, Qnil);
|
|
+ if (!NILP (mf))
|
|
+ {
|
|
+ ptrdiff_t begv2 = BUF_BEGV (b);
|
|
+ ptrdiff_t zv2 = BUF_ZV (b);
|
|
+ ptrdiff_t s2 = point;
|
|
+ ptrdiff_t e2 = point;
|
|
+
|
|
+ while (s2 > begv2)
|
|
+ {
|
|
+ Lisp_Object prev = Fget_char_property (
|
|
+ make_fixnum (s2 - 1), Qmouse_face, Qnil);
|
|
+ if (!NILP (Fequal (prev, mf)))
|
|
+ s2--;
|
|
+ else
|
|
+ break;
|
|
+ }
|
|
+
|
|
+ while (e2 < zv2)
|
|
+ {
|
|
+ Lisp_Object cur = Fget_char_property (
|
|
+ make_fixnum (e2), Qmouse_face, Qnil);
|
|
+ if (!NILP (Fequal (cur, mf)))
|
|
+ e2++;
|
|
+ else
|
|
+ break;
|
|
+ }
|
|
+
|
|
+ if (e2 > s2)
|
|
+ {
|
|
+ NSUInteger ax_s = [self accessibilityIndexForCharpos:s2];
|
|
+ NSUInteger ax_e = [self accessibilityIndexForCharpos:e2];
|
|
+ if (ax_e > ax_s && ax_e <= [cachedText length])
|
|
+ announceText = [cachedText substringWithRange:
|
|
+ NSMakeRange (ax_s, ax_e - ax_s)];
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /* 3) Fallback: check completions-highlight overlay span at point. */
|
|
+ if (!announceText)
|
|
+ {
|
|
+ Lisp_Object faceSym = intern ("completions-highlight");
|
|
+ Lisp_Object overlays = Foverlays_at (make_fixnum (point), Qnil);
|
|
+ Lisp_Object tail;
|
|
+ for (tail = overlays; CONSP (tail); tail = XCDR (tail))
|
|
+ {
|
|
+ Lisp_Object ov = XCAR (tail);
|
|
+ Lisp_Object face = Foverlay_get (ov, Qface);
|
|
+ if (EQ (face, faceSym)
|
|
+ || (CONSP (face)
|
|
+ && !NILP (Fmemq (faceSym, face))))
|
|
+ {
|
|
+ ptrdiff_t ov_start = OVERLAY_START (ov);
|
|
+ ptrdiff_t ov_end = OVERLAY_END (ov);
|
|
+ if (ov_end > ov_start)
|
|
+ {
|
|
+ announceText = ns_ax_completion_text_for_span (self, b,
|
|
+ ov_start,
|
|
+ ov_end,
|
|
+ cachedText);
|
|
+ currentOverlayStart = ov_start;
|
|
+ currentOverlayEnd = ov_end;
|
|
+ }
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /* 4) Fallback: select the best completions-highlight overlay.
|
|
+ Prefer overlay nearest to point over first-found in buffer. */
|
|
+ if (!announceText)
|
|
+ {
|
|
+ ptrdiff_t ov_start = 0;
|
|
+ ptrdiff_t ov_end = 0;
|
|
+ if (ns_ax_find_completion_overlay_range (b, point, &ov_start, &ov_end))
|
|
+ {
|
|
+ announceText = ns_ax_completion_text_for_span (self, b,
|
|
+ ov_start,
|
|
+ ov_end,
|
|
+ cachedText);
|
|
+ currentOverlayStart = ov_start;
|
|
+ currentOverlayEnd = ov_end;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (b != oldb2)
|
|
+ set_buffer_internal_1 (oldb2);
|
|
+
|
|
+ /* Final fallback: read the current line at point. */
|
|
+ if (!announceText)
|
|
+ {
|
|
+ NSUInteger point_idx = [self accessibilityIndexForCharpos:point];
|
|
+ if (point_idx <= [cachedText length])
|
|
+ {
|
|
+ NSInteger lineNum = [self accessibilityLineForIndex:
|
|
+ point_idx];
|
|
+ NSRange lineRange = [self accessibilityRangeForLine:lineNum];
|
|
+ if (lineRange.location != NSNotFound
|
|
+ && lineRange.length > 0
|
|
+ && lineRange.location + lineRange.length
|
|
+ <= [cachedText length])
|
|
+ announceText = [cachedText substringWithRange:lineRange];
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (announceText)
|
|
+ {
|
|
+ announceText = [announceText stringByTrimmingCharactersInSet:
|
|
+ [NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
+ if ([announceText length] > 0)
|
|
+ {
|
|
+ BOOL textChanged = ![announceText isEqualToString:
|
|
+ self.cachedCompletionAnnouncement];
|
|
+ BOOL overlayChanged =
|
|
+ (currentOverlayStart != self.cachedCompletionOverlayStart
|
|
+ || currentOverlayEnd != self.cachedCompletionOverlayEnd);
|
|
+ BOOL pointChanged = (point != self.cachedCompletionPoint);
|
|
+ if (textChanged || overlayChanged || pointChanged)
|
|
+ {
|
|
+ NSDictionary *annInfo = @{
|
|
+ NSAccessibilityAnnouncementKey: announceText,
|
|
+ NSAccessibilityPriorityKey:
|
|
+ @(NSAccessibilityPriorityHigh)
|
|
+ };
|
|
+ NSAccessibilityPostNotificationWithUserInfo (
|
|
+ NSApp,
|
|
+ NSAccessibilityAnnouncementRequestedNotification,
|
|
+ annInfo);
|
|
+ }
|
|
+ self.cachedCompletionAnnouncement = announceText;
|
|
+ self.cachedCompletionOverlayStart = currentOverlayStart;
|
|
+ self.cachedCompletionOverlayEnd = currentOverlayEnd;
|
|
+ self.cachedCompletionPoint = point;
|
|
+ }
|
|
+ else
|
|
+ {
|
|
+ self.cachedCompletionAnnouncement = nil;
|
|
+ self.cachedCompletionOverlayStart = 0;
|
|
+ self.cachedCompletionOverlayEnd = 0;
|
|
+ self.cachedCompletionPoint = 0;
|
|
+ }
|
|
+ }
|
|
+ else
|
|
+ {
|
|
+ self.cachedCompletionAnnouncement = nil;
|
|
+ self.cachedCompletionOverlayStart = 0;
|
|
+ self.cachedCompletionOverlayEnd = 0;
|
|
+ self.cachedCompletionPoint = 0;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ }
|
|
+ else
|
|
+ {
|
|
+ if ([self isAccessibilityFocused])
|
|
+ {
|
|
+ self.cachedCompletionAnnouncement = nil;
|
|
+ self.cachedCompletionOverlayStart = 0;
|
|
+ self.cachedCompletionOverlayEnd = 0;
|
|
+ self.cachedCompletionPoint = 0;
|
|
+ }
|
|
+ else
|
|
+ {
|
|
+ [self ensureTextCache];
|
|
+ if (cachedText)
|
|
+ {
|
|
+ NSString *announceText = nil;
|
|
+ ptrdiff_t currentOverlayStart = 0;
|
|
+ ptrdiff_t currentOverlayEnd = 0;
|
|
+ struct buffer *oldb2 = current_buffer;
|
|
+ if (b != current_buffer)
|
|
+ set_buffer_internal_1 (b);
|
|
+
|
|
+ if (ns_ax_find_completion_overlay_range (b, point,
|
|
+ ¤tOverlayStart,
|
|
+ ¤tOverlayEnd))
|
|
+ {
|
|
+ announceText = ns_ax_completion_text_for_span (self, b,
|
|
+ currentOverlayStart,
|
|
+ currentOverlayEnd,
|
|
+ cachedText);
|
|
+ }
|
|
+
|
|
+ if (b != oldb2)
|
|
+ set_buffer_internal_1 (oldb2);
|
|
+
|
|
+ if (announceText)
|
|
+ {
|
|
+ announceText = [announceText stringByTrimmingCharactersInSet:
|
|
+ [NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
+ if ([announceText length] > 0)
|
|
+ {
|
|
+ BOOL textChanged = ![announceText isEqualToString:
|
|
+ self.cachedCompletionAnnouncement];
|
|
+ BOOL overlayChanged =
|
|
+ (currentOverlayStart != self.cachedCompletionOverlayStart
|
|
+ || currentOverlayEnd != self.cachedCompletionOverlayEnd);
|
|
+ BOOL pointChanged = (point != self.cachedCompletionPoint);
|
|
+ if (textChanged || overlayChanged || pointChanged)
|
|
+ {
|
|
+ NSDictionary *annInfo = @{
|
|
+ NSAccessibilityAnnouncementKey: announceText,
|
|
+ NSAccessibilityPriorityKey:
|
|
+ @(NSAccessibilityPriorityHigh)
|
|
+ };
|
|
+ NSAccessibilityPostNotificationWithUserInfo (
|
|
+ NSApp,
|
|
+ NSAccessibilityAnnouncementRequestedNotification,
|
|
+ annInfo);
|
|
+ }
|
|
+ self.cachedCompletionAnnouncement = announceText;
|
|
+ self.cachedCompletionOverlayStart = currentOverlayStart;
|
|
+ self.cachedCompletionOverlayEnd = currentOverlayEnd;
|
|
+ self.cachedCompletionPoint = point;
|
|
+ }
|
|
+ else
|
|
+ {
|
|
+ self.cachedCompletionAnnouncement = nil;
|
|
+ self.cachedCompletionOverlayStart = 0;
|
|
+ self.cachedCompletionOverlayEnd = 0;
|
|
+ self.cachedCompletionPoint = 0;
|
|
+ }
|
|
+ }
|
|
+ else
|
|
+ {
|
|
+ self.cachedCompletionAnnouncement = nil;
|
|
+ self.cachedCompletionOverlayStart = 0;
|
|
+ self.cachedCompletionOverlayEnd = 0;
|
|
+ self.cachedCompletionPoint = 0;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
+@end
|
|
+
|
|
+
|
|
+@implementation EmacsAccessibilityModeLine
|
|
+
|
|
+- (NSAccessibilityRole)accessibilityRole
|
|
+{
|
|
+ return NSAccessibilityStaticTextRole;
|
|
+}
|
|
+
|
|
+- (NSString *)accessibilityLabel
|
|
+{
|
|
+ struct window *w = self.emacsWindow;
|
|
+ if (w && WINDOW_LEAF_P (w))
|
|
+ {
|
|
+ struct buffer *b = XBUFFER (w->contents);
|
|
+ if (b)
|
|
+ {
|
|
+ Lisp_Object name = BVAR (b, name);
|
|
+ if (STRINGP (name))
|
|
+ {
|
|
+ NSString *bufName = [NSString stringWithLispString:name];
|
|
+ return [NSString stringWithFormat:@"Mode Line - %@", bufName];
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ return @"Mode Line";
|
|
+}
|
|
+
|
|
+- (id)accessibilityValue
|
|
+{
|
|
+ struct window *w = self.emacsWindow;
|
|
+ if (!w)
|
|
+ return @"";
|
|
+ return ns_ax_mode_line_text (w);
|
|
+}
|
|
+
|
|
+- (NSRect)accessibilityFrame
|
|
+{
|
|
+ struct window *w = self.emacsWindow;
|
|
+ if (!w || !w->current_matrix)
|
|
+ return NSZeroRect;
|
|
+
|
|
+ /* Find the mode line row and return its screen rect. */
|
|
+ struct glyph_matrix *matrix = w->current_matrix;
|
|
+ for (int i = 0; i < matrix->nrows; i++)
|
|
+ {
|
|
+ struct glyph_row *row = matrix->rows + i;
|
|
+ if (row->enabled_p && row->mode_line_p)
|
|
+ {
|
|
+ return [self screenRectFromEmacsX:w->pixel_left
|
|
+ y:WINDOW_TO_FRAME_PIXEL_Y (w,
|
|
+ MAX (0, row->y))
|
|
+ width:w->pixel_width
|
|
+ height:row->visible_height];
|
|
+ }
|
|
}
|
|
+ return NSZeroRect;
|
|
}
|
|
|
|
@end
|
|
@@ -7498,6 +8489,7 @@ - (void)dealloc
|
|
[layer release];
|
|
#endif
|
|
|
|
+ [accessibilityElements release];
|
|
[[self menu] release];
|
|
[super dealloc];
|
|
}
|
|
@@ -8846,6 +9838,28 @@ - (void)windowDidBecomeKey /* for direct calls */
|
|
XSETFRAME (event.frame_or_window, emacsframe);
|
|
kbd_buffer_store_event (&event);
|
|
ns_send_appdefined (-1); // Kick main loop
|
|
+
|
|
+#ifdef NS_IMPL_COCOA
|
|
+ /* Notify VoiceOver that the focused accessibility element changed.
|
|
+ Post on the focused virtual element so VoiceOver starts tracking it.
|
|
+ This is critical for initial focus and app-switch scenarios. */
|
|
+ {
|
|
+ id focused = [self accessibilityFocusedUIElement];
|
|
+ if (focused
|
|
+ && [focused isKindOfClass:[EmacsAccessibilityBuffer class]])
|
|
+ {
|
|
+ NSAccessibilityPostNotification (focused,
|
|
+ NSAccessibilityFocusedUIElementChangedNotification);
|
|
+ NSDictionary *info = @{@"AXTextStateChangeType": @(ns_ax_text_state_change_selection_move),
|
|
+ @"AXTextChangeElement": focused};
|
|
+ NSAccessibilityPostNotificationWithUserInfo (focused,
|
|
+ NSAccessibilitySelectedTextChangedNotification, info);
|
|
+ }
|
|
+ else if (focused)
|
|
+ NSAccessibilityPostNotification (focused,
|
|
+ NSAccessibilityFocusedUIElementChangedNotification);
|
|
+ }
|
|
+#endif
|
|
}
|
|
|
|
|
|
@@ -10089,7 +11103,8 @@ - (int) fullscreenState
|
|
|
|
static void
|
|
ns_ax_collect_windows (Lisp_Object window, EmacsView *view,
|
|
- NSMutableArray *elements)
|
|
+ NSMutableArray *elements,
|
|
+ NSDictionary *existing)
|
|
{
|
|
if (NILP (window))
|
|
return;
|
|
@@ -10098,32 +11113,47 @@ - (int) fullscreenState
|
|
|
|
if (WINDOW_LEAF_P (w))
|
|
{
|
|
- if (MINI_WINDOW_P (w))
|
|
- return; /* Skip minibuffer for MVP. */
|
|
+ /* Buffer element — reuse existing if available. */
|
|
+ EmacsAccessibilityBuffer *elem
|
|
+ = [existing objectForKey:[NSValue valueWithPointer:w]];
|
|
+ if (!elem)
|
|
+ {
|
|
+ elem = [[EmacsAccessibilityBuffer alloc] init];
|
|
+ elem.emacsView = view;
|
|
|
|
- EmacsAccessibilityBuffer *elem = [[EmacsAccessibilityBuffer alloc] init];
|
|
- elem.emacsView = view;
|
|
+ /* Initialize cached state to -1 to force first notification. */
|
|
+ elem.cachedModiff = -1;
|
|
+ elem.cachedPoint = -1;
|
|
+ elem.cachedMarkActive = NO;
|
|
+ }
|
|
+ else
|
|
+ {
|
|
+ [elem retain];
|
|
+ }
|
|
elem.emacsWindow = w;
|
|
-
|
|
- /* Initialize cached state to trigger first notification. */
|
|
- struct buffer *b = XBUFFER (w->contents);
|
|
- if (b)
|
|
- {
|
|
- elem.cachedModiff = BUF_MODIFF (b);
|
|
- elem.cachedPoint = BUF_PT (b);
|
|
- }
|
|
-
|
|
[elements addObject:elem];
|
|
+ [elem release];
|
|
+
|
|
+ /* Mode line element (skip for minibuffer). */
|
|
+ if (!MINI_WINDOW_P (w))
|
|
+ {
|
|
+ EmacsAccessibilityModeLine *ml
|
|
+ = [[EmacsAccessibilityModeLine alloc] init];
|
|
+ ml.emacsView = view;
|
|
+ ml.emacsWindow = w;
|
|
+ [elements addObject:ml];
|
|
+ [ml release];
|
|
+ }
|
|
}
|
|
else
|
|
{
|
|
/* Internal (combination) window — recurse into children. */
|
|
Lisp_Object child = w->contents;
|
|
while (!NILP (child))
|
|
- {
|
|
- ns_ax_collect_windows (child, view, elements);
|
|
- child = XWINDOW (child)->next;
|
|
- }
|
|
+ {
|
|
+ ns_ax_collect_windows (child, view, elements, existing);
|
|
+ child = XWINDOW (child)->next;
|
|
+ }
|
|
}
|
|
}
|
|
|
|
@@ -10132,10 +11162,39 @@ - (void)rebuildAccessibilityTree
|
|
if (!emacsframe)
|
|
return;
|
|
|
|
- NSMutableArray *newElements = [NSMutableArray arrayWithCapacity:4];
|
|
+ /* Build map of existing elements by window pointer for reuse. */
|
|
+ NSMutableDictionary *existing = [NSMutableDictionary dictionary];
|
|
+ if (accessibilityElements)
|
|
+ {
|
|
+ for (EmacsAccessibilityElement *elem in accessibilityElements)
|
|
+ {
|
|
+ if ([elem isKindOfClass:[EmacsAccessibilityBuffer class]]
|
|
+ && elem.emacsWindow)
|
|
+ [existing setObject:elem
|
|
+ forKey:[NSValue valueWithPointer:
|
|
+ elem.emacsWindow]];
|
|
+ }
|
|
+ }
|
|
+
|
|
+ NSMutableArray *newElements = [NSMutableArray arrayWithCapacity:8];
|
|
+
|
|
+ /* Collect from main window tree. */
|
|
Lisp_Object root = FRAME_ROOT_WINDOW (emacsframe);
|
|
- ns_ax_collect_windows (root, self, newElements);
|
|
- accessibilityElements = newElements;
|
|
+ ns_ax_collect_windows (root, self, newElements, existing);
|
|
+
|
|
+ /* Include minibuffer. */
|
|
+ Lisp_Object mini = emacsframe->minibuffer_window;
|
|
+ if (!NILP (mini))
|
|
+ ns_ax_collect_windows (mini, self, newElements, existing);
|
|
+
|
|
+ [accessibilityElements release];
|
|
+ accessibilityElements = [newElements retain];
|
|
+ accessibilityTreeValid = YES;
|
|
+}
|
|
+
|
|
+- (void)invalidateAccessibilityTree
|
|
+{
|
|
+ accessibilityTreeValid = NO;
|
|
}
|
|
|
|
- (NSAccessibilityRole)accessibilityRole
|
|
@@ -10155,7 +11214,7 @@ - (BOOL)isAccessibilityElement
|
|
|
|
- (NSArray *)accessibilityChildren
|
|
{
|
|
- if (!accessibilityElements || [accessibilityElements count] == 0)
|
|
+ if (!accessibilityElements || !accessibilityTreeValid)
|
|
[self rebuildAccessibilityTree];
|
|
return accessibilityElements;
|
|
}
|
|
@@ -10165,16 +11224,15 @@ - (id)accessibilityFocusedUIElement
|
|
if (!emacsframe)
|
|
return self;
|
|
|
|
- /* Ensure tree exists (lazy init); avoid redundant rebuild since
|
|
- postAccessibilityUpdates already rebuilds each cycle. */
|
|
- if (!accessibilityElements || [accessibilityElements count] == 0)
|
|
+ if (!accessibilityElements || !accessibilityTreeValid)
|
|
[self rebuildAccessibilityTree];
|
|
|
|
struct window *sel = XWINDOW (emacsframe->selected_window);
|
|
- for (EmacsAccessibilityBuffer *elem in accessibilityElements)
|
|
+ for (EmacsAccessibilityElement *elem in accessibilityElements)
|
|
{
|
|
- if (elem.emacsWindow == sel)
|
|
- return elem;
|
|
+ if ([elem isKindOfClass:[EmacsAccessibilityBuffer class]]
|
|
+ && elem.emacsWindow == sel)
|
|
+ return elem;
|
|
}
|
|
return self;
|
|
}
|
|
@@ -10190,32 +11248,143 @@ - (void)postAccessibilityUpdates
|
|
if (!emacsframe)
|
|
return;
|
|
|
|
+ /* Re-entrance guard: VoiceOver callbacks during notification posting
|
|
+ can trigger redisplay, which calls ns_update_end, which calls us
|
|
+ again. Prevent infinite recursion. */
|
|
+ if (accessibilityUpdating)
|
|
+ return;
|
|
+ accessibilityUpdating = YES;
|
|
+
|
|
+ /* Detect window tree change (split, delete, new buffer). Compare
|
|
+ FRAME_ROOT_WINDOW — if it changed, the tree structure changed. */
|
|
+ Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe);
|
|
+ if (!EQ (curRoot, lastRootWindow))
|
|
+ {
|
|
+ lastRootWindow = curRoot;
|
|
+ accessibilityTreeValid = NO;
|
|
+ }
|
|
+
|
|
+ /* If tree is stale, rebuild FIRST so we don't iterate freed
|
|
+ window pointers. Skip notifications for this cycle — the
|
|
+ freshly-built elements have no previous state to diff against. */
|
|
+ if (!accessibilityTreeValid)
|
|
+ {
|
|
+ [self rebuildAccessibilityTree];
|
|
+ NSAccessibilityPostNotification (self,
|
|
+ NSAccessibilityLayoutChangedNotification);
|
|
+
|
|
+ /* Post focus change so VoiceOver picks up the new tree. */
|
|
+ id focused = [self accessibilityFocusedUIElement];
|
|
+ if (focused && focused != self)
|
|
+ NSAccessibilityPostNotification (focused,
|
|
+ NSAccessibilityFocusedUIElementChangedNotification);
|
|
+
|
|
+ lastSelectedWindow = emacsframe->selected_window;
|
|
+ accessibilityUpdating = NO;
|
|
+ return;
|
|
+ }
|
|
+
|
|
/* Post per-buffer notifications using EXISTING elements that have
|
|
- cached state from the previous cycle. */
|
|
- for (EmacsAccessibilityBuffer *elem in accessibilityElements)
|
|
+ cached state from the previous cycle. Validate each window
|
|
+ pointer before use. */
|
|
+ for (EmacsAccessibilityElement *elem in accessibilityElements)
|
|
{
|
|
- struct window *w = elem.emacsWindow;
|
|
- if (w && WINDOW_LEAF_P (w))
|
|
- [elem postAccessibilityUpdatesForWindow:w frame:emacsframe];
|
|
+ if ([elem isKindOfClass:[EmacsAccessibilityBuffer class]])
|
|
+ {
|
|
+ struct window *w = elem.emacsWindow;
|
|
+ if (w && WINDOW_LEAF_P (w)
|
|
+ && BUFFERP (w->contents) && XBUFFER (w->contents))
|
|
+ [(EmacsAccessibilityBuffer *) elem
|
|
+ postAccessibilityNotificationsForFrame:emacsframe];
|
|
+ }
|
|
}
|
|
|
|
- /* Check for window switch (C-x o) before rebuild. */
|
|
+ /* Check for window switch (C-x o). */
|
|
Lisp_Object curSel = emacsframe->selected_window;
|
|
BOOL windowSwitched = !EQ (curSel, lastSelectedWindow);
|
|
if (windowSwitched)
|
|
- lastSelectedWindow = curSel;
|
|
+ {
|
|
+ lastSelectedWindow = curSel;
|
|
+ id focused = [self accessibilityFocusedUIElement];
|
|
+ if (focused && focused != self
|
|
+ && [focused isKindOfClass:[EmacsAccessibilityBuffer class]])
|
|
+ {
|
|
+ NSAccessibilityPostNotification (focused,
|
|
+ NSAccessibilityFocusedUIElementChangedNotification);
|
|
+ NSDictionary *info = @{@"AXTextStateChangeType": @(ns_ax_text_state_change_selection_move),
|
|
+ @"AXTextChangeElement": focused};
|
|
+ NSAccessibilityPostNotificationWithUserInfo (focused,
|
|
+ NSAccessibilitySelectedTextChangedNotification, info);
|
|
+ }
|
|
+ else if (focused && focused != self)
|
|
+ NSAccessibilityPostNotification (focused,
|
|
+ NSAccessibilityFocusedUIElementChangedNotification);
|
|
+ }
|
|
+
|
|
+ accessibilityUpdating = NO;
|
|
+}
|
|
|
|
- /* Now rebuild tree to pick up window configuration changes. */
|
|
- [self rebuildAccessibilityTree];
|
|
+/* ---- Cursor position for Zoom (via accessibilityBoundsForRange:) ----
|
|
|
|
- /* Post focus change AFTER rebuild so the new element exists. */
|
|
- if (windowSwitched)
|
|
+ accessibilityFrame returns the VIEW's frame (standard behavior).
|
|
+ The cursor location is exposed through accessibilityBoundsForRange:
|
|
+ which AT tools query using the selectedTextRange. */
|
|
+
|
|
+- (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];
|
|
+}
|
|
+
|
|
+/* ---- 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])
|
|
{
|
|
- id focused = [self accessibilityFocusedUIElement];
|
|
- if (focused && focused != self)
|
|
- NSAccessibilityPostNotification (focused,
|
|
- NSAccessibilityFocusedUIElementChangedNotification);
|
|
+ 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 */
|
|
--
|
|
2.43.0
|
|
|