1149 lines
30 KiB
Diff
1149 lines
30 KiB
Diff
diff --git a/src/nsterm.h b/src/nsterm.h
|
|
index 7c1ee4c..8bf21f6 100644
|
|
--- a/src/nsterm.h
|
|
+++ b/src/nsterm.h
|
|
@@ -453,6 +453,40 @@ enum ns_return_frame_mode
|
|
@end
|
|
|
|
|
|
+/* ==========================================================================
|
|
+
|
|
+ Accessibility virtual elements (macOS / Cocoa only)
|
|
+
|
|
+ ========================================================================== */
|
|
+
|
|
+#ifdef NS_IMPL_COCOA
|
|
+@class 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). */
|
|
+@interface EmacsAccessibilityBuffer : EmacsAccessibilityElement
|
|
+@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) BOOL cachedMarkActive;
|
|
+- (void)invalidateTextCache;
|
|
+- (void)postAccessibilityNotificationsForFrame:(struct frame *)f;
|
|
+@end
|
|
+
|
|
+/* Virtual AXStaticText element — one per mode line. */
|
|
+@interface EmacsAccessibilityModeLine : EmacsAccessibilityElement
|
|
+@end
|
|
+#endif /* NS_IMPL_COCOA */
|
|
+
|
|
+
|
|
/* ==========================================================================
|
|
|
|
The main Emacs view
|
|
@@ -471,6 +505,12 @@ enum ns_return_frame_mode
|
|
#ifdef NS_IMPL_COCOA
|
|
char *old_title;
|
|
BOOL maximizing_resize;
|
|
+ NSMutableArray *accessibilityElements;
|
|
+ Lisp_Object lastSelectedWindow;
|
|
+ BOOL accessibilityTreeValid;
|
|
+ @public
|
|
+ NSRect lastAccessibilityCursorRect;
|
|
+ @protected
|
|
#endif
|
|
BOOL font_panel_active;
|
|
NSFont *font_panel_result;
|
|
@@ -528,6 +568,13 @@ enum ns_return_frame_mode
|
|
- (void)windowWillExitFullScreen;
|
|
- (void)windowDidExitFullScreen;
|
|
- (void)windowDidBecomeKey;
|
|
+
|
|
+#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 932d209..dd134dd 100644
|
|
--- a/src/nsterm.m
|
|
+++ b/src/nsterm.m
|
|
@@ -1104,6 +1104,11 @@ ns_update_end (struct frame *f)
|
|
|
|
unblock_input ();
|
|
ns_updating_frame = NULL;
|
|
+
|
|
+#ifdef NS_IMPL_COCOA
|
|
+ /* Post accessibility notifications after each redisplay cycle. */
|
|
+ [view postAccessibilityUpdates];
|
|
+#endif
|
|
}
|
|
|
|
static void
|
|
@@ -3232,6 +3237,37 @@ ns_draw_window_cursor (struct window *w, struct glyph_row *glyph_row,
|
|
/* 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];
|
|
@@ -6847,6 +6883,713 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action)
|
|
}
|
|
#endif
|
|
|
|
+/* ==========================================================================
|
|
+
|
|
+ Accessibility virtual elements (macOS / Cocoa only)
|
|
+
|
|
+ ========================================================================== */
|
|
+
|
|
+#ifdef NS_IMPL_COCOA
|
|
+
|
|
+/* ---- Helper: extract buffer text for accessibility ---- */
|
|
+
|
|
+/* Maximum characters exposed via accessibilityValue. */
|
|
+#define NS_AX_TEXT_CAP 100000
|
|
+
|
|
+static NSString *
|
|
+ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start)
|
|
+{
|
|
+ if (!w || !WINDOW_LEAF_P (w))
|
|
+ {
|
|
+ *out_start = 0;
|
|
+ return @"";
|
|
+ }
|
|
+
|
|
+ struct buffer *b = XBUFFER (w->contents);
|
|
+ if (!b)
|
|
+ {
|
|
+ *out_start = 0;
|
|
+ return @"";
|
|
+ }
|
|
+
|
|
+ ptrdiff_t begv = BUF_BEGV (b);
|
|
+ ptrdiff_t zv = BUF_ZV (b);
|
|
+ ptrdiff_t len = zv - begv;
|
|
+
|
|
+ /* Cap at NS_AX_TEXT_CAP characters, centered on point. */
|
|
+ if (len > NS_AX_TEXT_CAP)
|
|
+ {
|
|
+ ptrdiff_t pt = BUF_PT (b);
|
|
+ ptrdiff_t half = NS_AX_TEXT_CAP / 2;
|
|
+ ptrdiff_t start = MAX (begv, pt - half);
|
|
+ ptrdiff_t end = MIN (zv, start + NS_AX_TEXT_CAP);
|
|
+ start = MAX (begv, end - NS_AX_TEXT_CAP);
|
|
+ begv = start;
|
|
+ zv = end;
|
|
+ }
|
|
+
|
|
+ *out_start = begv;
|
|
+
|
|
+ if (zv <= begv)
|
|
+ return @"";
|
|
+
|
|
+ ptrdiff_t begv_byte = buf_charpos_to_bytepos (b, begv);
|
|
+ ptrdiff_t zv_byte = buf_charpos_to_bytepos (b, zv);
|
|
+ unsigned char *data = BUF_BYTE_ADDRESS (b, begv_byte);
|
|
+ ptrdiff_t nbytes = zv_byte - begv_byte;
|
|
+
|
|
+ Lisp_Object lstr = make_string_from_bytes ((char *) data,
|
|
+ zv - begv, nbytes);
|
|
+ return [NSString stringWithLispString:lstr];
|
|
+}
|
|
+
|
|
+
|
|
+/* ---- Helper: extract mode line text from glyph rows ---- */
|
|
+
|
|
+static NSString *
|
|
+ns_ax_mode_line_text (struct window *w)
|
|
+{
|
|
+ if (!w || !w->current_matrix)
|
|
+ return @"";
|
|
+
|
|
+ struct glyph_matrix *matrix = w->current_matrix;
|
|
+ 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;
|
|
+
|
|
+ struct glyph *g = row->glyphs[TEXT_AREA];
|
|
+ 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 text;
|
|
+}
|
|
+
|
|
+
|
|
+/* ---- Helper: screen rect for a character range via glyph matrix ---- */
|
|
+
|
|
+static NSRect
|
|
+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;
|
|
+ NSRect result = NSZeroRect;
|
|
+ BOOL found = NO;
|
|
+
|
|
+ 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;
|
|
+
|
|
+ ptrdiff_t row_start = MATRIX_ROW_START_CHARPOS (row);
|
|
+ ptrdiff_t row_end = MATRIX_ROW_END_CHARPOS (row);
|
|
+
|
|
+ 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->visible_height;
|
|
+
|
|
+ if (!found)
|
|
+ {
|
|
+ result = rowRect;
|
|
+ found = YES;
|
|
+ }
|
|
+ else
|
|
+ result = NSUnionRect (result, rowRect);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (!found)
|
|
+ return NSZeroRect;
|
|
+
|
|
+ /* Convert from EmacsView (flipped) coords to screen coords. */
|
|
+ NSRect winRect = [view convertRect:result toView:nil];
|
|
+ return [[view window] convertRectToScreen:winRect];
|
|
+}
|
|
+
|
|
+
|
|
+@implementation EmacsAccessibilityElement
|
|
+
|
|
+- (NSRect)screenRectFromEmacsX:(int)x y:(int)y width:(int)ew height:(int)eh
|
|
+{
|
|
+ EmacsView *view = self.emacsView;
|
|
+ if (!view || ![view window])
|
|
+ return NSZeroRect;
|
|
+
|
|
+ NSRect r = NSMakeRect (x, y, ew, eh);
|
|
+ NSRect winRect = [view convertRect:r toView:nil];
|
|
+ return [[view window] convertRectToScreen:winRect];
|
|
+}
|
|
+
|
|
+- (BOOL)isAccessibilityElement
|
|
+{
|
|
+ return YES;
|
|
+}
|
|
+
|
|
+/* ---- Hierarchy plumbing (required for VoiceOver to find us) ---- */
|
|
+
|
|
+- (id)accessibilityParent
|
|
+{
|
|
+ return NSAccessibilityUnignoredAncestor (self.emacsView);
|
|
+}
|
|
+
|
|
+- (id)accessibilityWindow
|
|
+{
|
|
+ return [self.emacsView window];
|
|
+}
|
|
+
|
|
+- (id)accessibilityTopLevelUIElement
|
|
+{
|
|
+ return [self.emacsView window];
|
|
+}
|
|
+
|
|
+@end
|
|
+
|
|
+
|
|
+@implementation EmacsAccessibilityBuffer
|
|
+@synthesize cachedText;
|
|
+@synthesize cachedTextModiff;
|
|
+@synthesize cachedTextStart;
|
|
+@synthesize cachedModiff;
|
|
+@synthesize cachedPoint;
|
|
+@synthesize cachedMarkActive;
|
|
+
|
|
+- (void)dealloc
|
|
+{
|
|
+ [cachedText release];
|
|
+ [super dealloc];
|
|
+}
|
|
+
|
|
+/* ---- Text cache ---- */
|
|
+
|
|
+- (void)invalidateTextCache
|
|
+{
|
|
+ [cachedText release];
|
|
+ cachedText = nil;
|
|
+}
|
|
+
|
|
+- (void)ensureTextCache
|
|
+{
|
|
+ struct window *w = self.emacsWindow;
|
|
+ if (!w || !WINDOW_LEAF_P (w))
|
|
+ return;
|
|
+
|
|
+ struct buffer *b = XBUFFER (w->contents);
|
|
+ if (!b)
|
|
+ return;
|
|
+
|
|
+ ptrdiff_t modiff = BUF_MODIFF (b);
|
|
+ if (cachedText && cachedTextModiff == modiff)
|
|
+ return;
|
|
+
|
|
+ ptrdiff_t start;
|
|
+ NSString *text = ns_ax_buffer_text (w, &start);
|
|
+
|
|
+ [cachedText release];
|
|
+ cachedText = [text retain];
|
|
+ cachedTextModiff = modiff;
|
|
+ cachedTextStart = start;
|
|
+}
|
|
+
|
|
+/* ---- 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);
|
|
+
|
|
+ struct buffer *b = XBUFFER (w->contents);
|
|
+ if (!b)
|
|
+ return NSMakeRange (0, 0);
|
|
+
|
|
+ [self ensureTextCache];
|
|
+ ptrdiff_t pt = BUF_PT (b);
|
|
+ NSUInteger point_idx = (NSUInteger) (pt - cachedTextStart);
|
|
+
|
|
+ if (NILP (BVAR (b, mark_active)))
|
|
+ return NSMakeRange (point_idx, 0);
|
|
+
|
|
+ ptrdiff_t mark_pos = marker_position (BVAR (b, mark));
|
|
+ NSUInteger mark_idx = (NSUInteger) (mark_pos - cachedTextStart);
|
|
+ NSUInteger start_idx = MIN (point_idx, mark_idx);
|
|
+ NSUInteger end_idx = MAX (point_idx, mark_idx);
|
|
+ return NSMakeRange (start_idx, end_idx - start_idx);
|
|
+}
|
|
+
|
|
+- (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 = (NSUInteger) (pt - cachedTextStart);
|
|
+ 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
|
|
+{
|
|
+ [self ensureTextCache];
|
|
+ if (!cachedText || range.location + range.length > [cachedText length])
|
|
+ return @"";
|
|
+ return [cachedText substringWithRange:range];
|
|
+}
|
|
+
|
|
+- (NSAttributedString *)accessibilityAttributedStringForRange:(NSRange)range
|
|
+{
|
|
+ NSString *str = [self accessibilityStringForRange:range];
|
|
+ return [[[NSAttributedString alloc] initWithString:str] autorelease];
|
|
+}
|
|
+
|
|
+- (NSInteger)accessibilityLineForIndex:(NSInteger)index
|
|
+{
|
|
+ [self ensureTextCache];
|
|
+ if (!cachedText || index < 0)
|
|
+ return 0;
|
|
+
|
|
+ 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
|
|
+{
|
|
+ [self ensureTextCache];
|
|
+ if (!cachedText || line < 0)
|
|
+ return NSMakeRange (NSNotFound, 0);
|
|
+
|
|
+ NSUInteger len = [cachedText length];
|
|
+ NSInteger cur_line = 0;
|
|
+ NSUInteger line_start = 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++;
|
|
+ return NSMakeRange (i, line_end - i);
|
|
+ }
|
|
+ if (i < len && [cachedText characterAtIndex:i] == '\n')
|
|
+ {
|
|
+ cur_line++;
|
|
+ line_start = i + 1;
|
|
+ }
|
|
+ }
|
|
+ return NSMakeRange (NSNotFound, 0);
|
|
+}
|
|
+
|
|
+- (NSRange)accessibilityRangeForIndex:(NSInteger)index
|
|
+{
|
|
+ [self ensureTextCache];
|
|
+ if (!cachedText || index < 0
|
|
+ || (NSUInteger) index >= [cachedText length])
|
|
+ return NSMakeRange (NSNotFound, 0);
|
|
+ return NSMakeRange ((NSUInteger) index, 1);
|
|
+}
|
|
+
|
|
+- (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
|
|
+{
|
|
+ struct window *w = self.emacsWindow;
|
|
+ EmacsView *view = self.emacsView;
|
|
+ if (!w || !view)
|
|
+ return NSZeroRect;
|
|
+ return ns_ax_frame_for_range (w, view, cachedTextStart, range);
|
|
+}
|
|
+
|
|
+- (NSRange)accessibilityRangeForPosition:(NSPoint)screenPoint
|
|
+{
|
|
+ /* Hit test: convert screen point to buffer character index. */
|
|
+ struct window *w = self.emacsWindow;
|
|
+ EmacsView *view = self.emacsView;
|
|
+ if (!w || !view || !w->current_matrix)
|
|
+ return NSMakeRange (0, 0);
|
|
+
|
|
+ /* Convert screen point to EmacsView coordinates. */
|
|
+ NSPoint windowPoint = [[view window] convertPointFromScreen:screenPoint];
|
|
+ NSPoint viewPoint = [view convertPoint:windowPoint fromView:nil];
|
|
+
|
|
+ /* Convert to window-relative pixel coordinates. */
|
|
+ int x = (int) viewPoint.x - w->pixel_left;
|
|
+ int y = (int) viewPoint.y - w->pixel_top;
|
|
+
|
|
+ if (x < 0 || y < 0 || x >= w->pixel_width || y >= w->pixel_height)
|
|
+ return NSMakeRange (0, 0);
|
|
+
|
|
+ /* Find the glyph row at this y coordinate. */
|
|
+ struct glyph_matrix *matrix = w->current_matrix;
|
|
+ struct glyph_row *hit_row = NULL;
|
|
+
|
|
+ for (int i = 0; i < matrix->nrows; i++)
|
|
+ {
|
|
+ struct glyph_row *row = matrix->rows + i;
|
|
+ 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)
|
|
+ return NSMakeRange (0, 0);
|
|
+
|
|
+ /* Find the glyph at this x coordinate within the row. */
|
|
+ struct glyph *glyph = hit_row->glyphs[TEXT_AREA];
|
|
+ struct glyph *end = glyph + hit_row->used[TEXT_AREA];
|
|
+ int glyph_x = 0;
|
|
+ ptrdiff_t best_charpos = MATRIX_ROW_START_CHARPOS (hit_row);
|
|
+
|
|
+ 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;
|
|
+ }
|
|
+ glyph_x += glyph->pixel_width;
|
|
+ }
|
|
+
|
|
+ /* Convert buffer charpos to accessibility index. */
|
|
+ [self ensureTextCache];
|
|
+ NSUInteger ax_idx = (NSUInteger) (best_charpos - cachedTextStart);
|
|
+ if (cachedText && ax_idx > [cachedText length])
|
|
+ ax_idx = [cachedText length];
|
|
+ return NSMakeRange (ax_idx, 1);
|
|
+}
|
|
+
|
|
+- (NSRange)accessibilityVisibleCharacterRange
|
|
+{
|
|
+ struct window *w = self.emacsWindow;
|
|
+ if (!w || !w->current_matrix)
|
|
+ {
|
|
+ [self ensureTextCache];
|
|
+ return NSMakeRange (0, cachedText ? [cachedText length] : 0);
|
|
+ }
|
|
+
|
|
+ [self ensureTextCache];
|
|
+ if (!cachedText)
|
|
+ return NSMakeRange (0, 0);
|
|
+
|
|
+ /* Compute visible range from window start to last visible row. */
|
|
+ ptrdiff_t vis_start = w->start_charpos;
|
|
+ ptrdiff_t vis_end = vis_start;
|
|
+
|
|
+ struct glyph_matrix *matrix = w->current_matrix;
|
|
+ for (int i = matrix->nrows - 1; i >= 0; 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))
|
|
+ {
|
|
+ vis_end = MATRIX_ROW_END_CHARPOS (row);
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ NSUInteger loc = (NSUInteger) (vis_start - cachedTextStart);
|
|
+ NSUInteger end = (NSUInteger) (vis_end - cachedTextStart);
|
|
+ NSUInteger text_len = [cachedText length];
|
|
+ if (loc > text_len) loc = text_len;
|
|
+ if (end > text_len) end = text_len;
|
|
+ if (end < loc) end = loc;
|
|
+
|
|
+ return NSMakeRange (loc, end - loc);
|
|
+}
|
|
+
|
|
+- (NSRect)accessibilityFrame
|
|
+{
|
|
+ struct window *w = self.emacsWindow;
|
|
+ if (!w)
|
|
+ return NSZeroRect;
|
|
+ return [self screenRectFromEmacsX:w->pixel_left
|
|
+ y:w->pixel_top
|
|
+ width:w->pixel_width
|
|
+ height:w->pixel_height];
|
|
+}
|
|
+
|
|
+/* ---- Notification dispatch ---- */
|
|
+
|
|
+- (void)postAccessibilityNotificationsForFrame:(struct frame *)f
|
|
+{
|
|
+ struct window *w = self.emacsWindow;
|
|
+ if (!w || !WINDOW_LEAF_P (w))
|
|
+ return;
|
|
+
|
|
+ struct buffer *b = XBUFFER (w->contents);
|
|
+ if (!b)
|
|
+ return;
|
|
+
|
|
+ ptrdiff_t modiff = BUF_MODIFF (b);
|
|
+ ptrdiff_t point = BUF_PT (b);
|
|
+ BOOL markActive = !NILP (BVAR (b, mark_active));
|
|
+
|
|
+ /* --- Text changed → typing echo ---
|
|
+ kAXTextStateChangeTypeEdit = 0, kAXTextEditTypeTyping = 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 = (NSUInteger) (point - 1 - cachedTextStart);
|
|
+ if (idx < [cachedText length])
|
|
+ changedChar = [cachedText substringWithRange:
|
|
+ NSMakeRange (idx, 1)];
|
|
+ }
|
|
+ }
|
|
+ else
|
|
+ {
|
|
+ [self invalidateTextCache];
|
|
+ }
|
|
+
|
|
+ self.cachedModiff = modiff;
|
|
+
|
|
+ NSDictionary *change = @{
|
|
+ @"AXTextEditType": @3,
|
|
+ @"AXTextChangeValue": changedChar,
|
|
+ @"AXTextChangeValueLength": @([changedChar length])
|
|
+ };
|
|
+ NSDictionary *userInfo = @{
|
|
+ @"AXTextStateChangeType": @0,
|
|
+ @"AXTextChangeValues": @[change]
|
|
+ };
|
|
+ NSAccessibilityPostNotificationWithUserInfo (
|
|
+ self, NSAccessibilityValueChangedNotification, userInfo);
|
|
+ }
|
|
+
|
|
+ /* --- Cursor moved or selection changed → line reading ---
|
|
+ kAXTextStateChangeTypeSelectionMove = 1. */
|
|
+ if (point != self.cachedPoint || markActive != self.cachedMarkActive)
|
|
+ {
|
|
+ self.cachedPoint = point;
|
|
+ self.cachedMarkActive = markActive;
|
|
+
|
|
+ NSDictionary *moveInfo = @{
|
|
+ @"AXTextStateChangeType": @1,
|
|
+ @"AXTextSelectionDirection": @4,
|
|
+ @"AXTextSelectionGranularity": @3
|
|
+ };
|
|
+ NSAccessibilityPostNotificationWithUserInfo (
|
|
+ self,
|
|
+ NSAccessibilitySelectedTextChangedNotification,
|
|
+ moveInfo);
|
|
+ }
|
|
+}
|
|
+
|
|
+@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
|
|
+
|
|
+#endif /* NS_IMPL_COCOA */
|
|
+
|
|
+
|
|
/* ==========================================================================
|
|
|
|
EmacsView implementation
|
|
@@ -6889,6 +7632,7 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action)
|
|
[layer release];
|
|
#endif
|
|
|
|
+ [accessibilityElements release];
|
|
[[self menu] release];
|
|
[super dealloc];
|
|
}
|
|
@@ -8237,6 +8981,18 @@ ns_in_echo_area (void)
|
|
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)
|
|
+ NSAccessibilityPostNotification (focused,
|
|
+ NSAccessibilityFocusedUIElementChangedNotification);
|
|
+ }
|
|
+#endif
|
|
}
|
|
|
|
|
|
@@ -9474,6 +10230,259 @@ ns_in_echo_area (void)
|
|
return fs_state;
|
|
}
|
|
|
|
+#ifdef NS_IMPL_COCOA
|
|
+
|
|
+/* ---- Accessibility: walk the Emacs window tree ---- */
|
|
+
|
|
+static void
|
|
+ns_ax_collect_windows (Lisp_Object window, EmacsView *view,
|
|
+ NSMutableArray *elements,
|
|
+ NSDictionary *existing)
|
|
+{
|
|
+ if (NILP (window))
|
|
+ return;
|
|
+
|
|
+ struct window *w = XWINDOW (window);
|
|
+
|
|
+ if (WINDOW_LEAF_P (w))
|
|
+ {
|
|
+ /* Buffer element — reuse existing if available. */
|
|
+ EmacsAccessibilityBuffer *elem
|
|
+ = [existing objectForKey:[NSValue valueWithPointer:w]];
|
|
+ if (!elem)
|
|
+ {
|
|
+ elem = [[EmacsAccessibilityBuffer alloc] init];
|
|
+ elem.emacsView = view;
|
|
+
|
|
+ /* 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);
|
|
+ elem.cachedMarkActive = !NILP (BVAR (b, mark_active));
|
|
+ }
|
|
+ }
|
|
+ else
|
|
+ {
|
|
+ [elem retain];
|
|
+ }
|
|
+ elem.emacsWindow = w;
|
|
+ [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, existing);
|
|
+ child = XWINDOW (child)->next;
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
+- (void)rebuildAccessibilityTree
|
|
+{
|
|
+ if (!emacsframe)
|
|
+ return;
|
|
+
|
|
+ /* 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, 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
|
|
+{
|
|
+ return NSAccessibilityGroupRole;
|
|
+}
|
|
+
|
|
+- (NSString *)accessibilityLabel
|
|
+{
|
|
+ return @"Emacs";
|
|
+}
|
|
+
|
|
+- (BOOL)isAccessibilityElement
|
|
+{
|
|
+ return YES;
|
|
+}
|
|
+
|
|
+- (NSArray *)accessibilityChildren
|
|
+{
|
|
+ if (!accessibilityElements || !accessibilityTreeValid)
|
|
+ [self rebuildAccessibilityTree];
|
|
+ return accessibilityElements;
|
|
+}
|
|
+
|
|
+- (id)accessibilityFocusedUIElement
|
|
+{
|
|
+ if (!emacsframe)
|
|
+ return self;
|
|
+
|
|
+ if (!accessibilityElements || !accessibilityTreeValid)
|
|
+ [self rebuildAccessibilityTree];
|
|
+
|
|
+ struct window *sel = XWINDOW (emacsframe->selected_window);
|
|
+ for (EmacsAccessibilityElement *elem in accessibilityElements)
|
|
+ {
|
|
+ if ([elem isKindOfClass:[EmacsAccessibilityBuffer class]]
|
|
+ && elem.emacsWindow == sel)
|
|
+ return elem;
|
|
+ }
|
|
+ return self;
|
|
+}
|
|
+
|
|
+/* Called from ns_update_end to post AX notifications.
|
|
+
|
|
+ Important: post notifications BEFORE rebuilding the tree.
|
|
+ The existing elements carry cached state (modiff, point) from the
|
|
+ previous redisplay cycle. Rebuilding first would create fresh
|
|
+ elements with current values, making change detection impossible. */
|
|
+- (void)postAccessibilityUpdates
|
|
+{
|
|
+ if (!emacsframe)
|
|
+ return;
|
|
+
|
|
+ /* Post per-buffer notifications using EXISTING elements that have
|
|
+ cached state from the previous cycle. */
|
|
+ for (EmacsAccessibilityElement *elem in accessibilityElements)
|
|
+ {
|
|
+ if ([elem isKindOfClass:[EmacsAccessibilityBuffer class]])
|
|
+ {
|
|
+ struct window *w = elem.emacsWindow;
|
|
+ if (w && WINDOW_LEAF_P (w))
|
|
+ [(EmacsAccessibilityBuffer *) elem
|
|
+ postAccessibilityNotificationsForFrame:emacsframe];
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /* Check for window switch (C-x o) before rebuild. */
|
|
+ Lisp_Object curSel = emacsframe->selected_window;
|
|
+ BOOL windowSwitched = !EQ (curSel, lastSelectedWindow);
|
|
+ if (windowSwitched)
|
|
+ lastSelectedWindow = curSel;
|
|
+
|
|
+ /* Rebuild tree only if window configuration changed. */
|
|
+ if (!accessibilityTreeValid)
|
|
+ [self rebuildAccessibilityTree];
|
|
+
|
|
+ /* Post focus change AFTER rebuild so the new element exists. */
|
|
+ if (windowSwitched)
|
|
+ {
|
|
+ id focused = [self accessibilityFocusedUIElement];
|
|
+ if (focused && focused != self)
|
|
+ NSAccessibilityPostNotification (focused,
|
|
+ NSAccessibilityFocusedUIElementChangedNotification);
|
|
+ }
|
|
+}
|
|
+
|
|
+/* ---- Cursor position for Zoom (via accessibilityBoundsForRange:) ----
|
|
+
|
|
+ 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])
|
|
+ {
|
|
+ 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 */
|
|
+
|
|
@end /* EmacsView */
|
|
|
|
|
|
@@ -9941,6 +10950,14 @@ nswindow_orderedIndex_sort (id w1, id w2, void *c)
|
|
|
|
return [super accessibilityAttributeValue:attribute];
|
|
}
|
|
+
|
|
+- (id)accessibilityFocusedUIElement
|
|
+{
|
|
+ EmacsView *view = (EmacsView *)[self delegate];
|
|
+ if (view && [view respondsToSelector:@selector(accessibilityFocusedUIElement)])
|
|
+ return [view accessibilityFocusedUIElement];
|
|
+ return self;
|
|
+}
|
|
#endif /* NS_IMPL_COCOA */
|
|
|
|
/* Constrain size and placement of a frame.
|