mirror of
git://git.sv.gnu.org/emacs.git
synced 2026-06-14 04:21:24 +00:00
Inform macOS Accessibility Zoom of cursor position (bug#80624)
Enable cursor focus tracking for visually-impaired users that rely on macOS Zoom screen magnification. * src/nsterm.m: Import ApplicationServices.h. (ns_ua_zoom_enabled_p, ns_cg_rect_flip_y, ns_UAZoomChangeFocus): New static function. Advise UAZoomChangeFocus of potentially new cursor position. (ns_update_end): Call ns_UAZoomChangeFocus. (ns_draw_window_cursor): Cache the cursor position. (applicationDidFinishLaunching): NSLog Accessibility API permissions AXIsProcessTrusted. (windowDidBecomeKey): Schedule a call to ns_UAZoomChangeFocus. (deferred_UAZoomChangeFocus_handler): New view method to call ns_UAZoomChangeFocus. (accessibilityFrame): New view method to help UAZoomChangeFocus. (initFrameFromEmacs): Initialize ns_UAZoom_cursor_rect_new and ns_UAZoom_cursor_rect_old. * etc/NEWS: Announce the change.
This commit is contained in:
parent
e0ca1d8822
commit
52205e38d3
2 changed files with 204 additions and 0 deletions
7
etc/NEWS
7
etc/NEWS
|
|
@ -4497,6 +4497,13 @@ singleton list.
|
|||
|
||||
* Changes in Emacs 31.1 on Non-Free Operating Systems
|
||||
|
||||
---
|
||||
** Support macOS Accessibility Zoom focus tracking.
|
||||
This is an important change for visually-impaired users. If macOS
|
||||
Accessibility Zoom is enabled (System Settings, Accessibility, Zoom)
|
||||
with keyboard focus tracking (Advanced...), Zoom is informed of updated
|
||||
cursor positions during each redisplay cycle.
|
||||
|
||||
---
|
||||
** Process execution has been optimized on Android.
|
||||
The run-time performance of subprocesses on recent Android releases,
|
||||
|
|
|
|||
197
src/nsterm.m
197
src/nsterm.m
|
|
@ -71,6 +71,12 @@ Updated by Christian Limpach (chris@nice.ch)
|
|||
#include "macfont.h"
|
||||
#include <Carbon/Carbon.h>
|
||||
#include <IOSurface/IOSurface.h>
|
||||
/* ApplicationServices provides the macOS accessibility Zoom API
|
||||
UAZoomEnabled and UAZoomChangeFocus (UniversalAccess framework).
|
||||
Carbon.h already pulls in ApplicationServices on most SDK versions,
|
||||
but the explicit import makes the dependency visible and guards
|
||||
against SDK changes. */
|
||||
#import <ApplicationServices/ApplicationServices.h>
|
||||
#endif
|
||||
|
||||
static EmacsMenu *dockMenu;
|
||||
|
|
@ -1080,6 +1086,126 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen)
|
|||
[view lockFocus];
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------------
|
||||
macOS Accessibility Zoom Support
|
||||
-------------------------------------------------------------------------- */
|
||||
#ifdef NS_IMPL_COCOA
|
||||
|
||||
static BOOL ns_is_UAZoomEnabled = NO;
|
||||
static unsigned long ns_UAZoomEnabled_last_called_time_ns = 0;
|
||||
static const unsigned long NS_UAZOOMENABLED_CACHE_INTERVAL_NS =
|
||||
(unsigned long)(500 * NSEC_PER_MSEC); /* 500ms. */
|
||||
static NSTimeInterval NS_UAZOOMENABLED_DEFER_INTERVAL_SECS = 0.2; /* 200ms. */
|
||||
static NSTimer *ns_deferred_UAZoomChangeFocus_timer = nil;
|
||||
|
||||
static BOOL
|
||||
ns_ua_zoom_enabled_p (void)
|
||||
/* --------------------------------------------------------------------------
|
||||
Return the cached result of UAZoomEnabled. Refresh the cache every
|
||||
NS_UAZOOMENABLED_CACHE_INTERVAL_NS nanoseconds.
|
||||
|
||||
We cache the result to avoid the macOS Mach IPC Accessibility Server
|
||||
round trip cost on every Emacs cursor update. Since enabling Zoom
|
||||
requires an explicit user UI action that takes real user time, the
|
||||
cache TTL should be invisible to the user.
|
||||
|
||||
Use clock_gettime_nsec_np not CFAbsoluteTimeGetCurrent which depends
|
||||
on the wall clock which can be reset by the user or by NTP.
|
||||
|
||||
Main-thread-only and called from ns_update_end, below.
|
||||
-------------------------------------------------------------------------- */
|
||||
{
|
||||
/* User-space equivalent to mach_absolute_time. */
|
||||
unsigned long now_ns = clock_gettime_nsec_np (CLOCK_UPTIME_RAW);
|
||||
if (now_ns - ns_UAZoomEnabled_last_called_time_ns
|
||||
> NS_UAZOOMENABLED_CACHE_INTERVAL_NS)
|
||||
{
|
||||
ns_is_UAZoomEnabled = UAZoomEnabled ();
|
||||
ns_UAZoomEnabled_last_called_time_ns = now_ns;
|
||||
}
|
||||
return ns_is_UAZoomEnabled;
|
||||
}
|
||||
|
||||
static inline CGRect
|
||||
ns_cg_rect_flip_y (CGRect r)
|
||||
/* --------------------------------------------------------------------------
|
||||
Convert a CGRect from Cocoa screen coordinates (origin at bottom-left
|
||||
of the primary display) to CoreGraphics coordinates (origin at
|
||||
top-left of the primary display). CoreGraphics defines its
|
||||
coordinate origin at the top-left corner of the primary display and
|
||||
all screens share this global coordinate space, so the flip always
|
||||
uses the primary display height regardless of which screen R is on.
|
||||
-------------------------------------------------------------------------- */
|
||||
{
|
||||
CGDirectDisplayID mainID = CGMainDisplayID ();
|
||||
if (mainID == kCGNullDirectDisplay)
|
||||
return r;
|
||||
CGFloat primaryH = CGDisplayBounds (mainID).size.height;
|
||||
if (primaryH <= 0)
|
||||
return r;
|
||||
r.origin.y = primaryH - r.origin.y - r.size.height;
|
||||
return r;
|
||||
}
|
||||
|
||||
/* Cache cursor rects to call UAZoomChangeFocus only when the cursor
|
||||
position has changed, not merely when the cursor is blinking.
|
||||
See ns_draw_window_cursor and ns_update_end. */
|
||||
static NSRect ns_UAZoom_cursor_rect_new;
|
||||
static NSRect ns_UAZoom_cursor_rect_old;
|
||||
|
||||
/* Track Zoom state per display cycle. Update the macOS Zoom cursor
|
||||
position when Zoom transitions to enabled. */
|
||||
static BOOL ns_update_was_UAZoomEnabled = NO;
|
||||
|
||||
static void
|
||||
ns_UAZoomChangeFocus (EmacsView *view, BOOL force)
|
||||
/* --------------------------------------------------------------------------
|
||||
Advise macOS Accessibility Zoom UAZoomChangeFocus of a potentially
|
||||
new cursor position. Force an updated position when Zoom transitions
|
||||
to enabled, or when the frame gets focus.
|
||||
-------------------------------------------------------------------------- */
|
||||
{
|
||||
if (ns_ua_zoom_enabled_p ())
|
||||
{
|
||||
force = force || !ns_update_was_UAZoomEnabled;
|
||||
ns_update_was_UAZoomEnabled = YES;
|
||||
if (NSIsEmptyRect (ns_UAZoom_cursor_rect_new))
|
||||
return;
|
||||
if (force || !NSEqualRects (ns_UAZoom_cursor_rect_new,
|
||||
ns_UAZoom_cursor_rect_old))
|
||||
{
|
||||
ns_UAZoom_cursor_rect_old = ns_UAZoom_cursor_rect_new;
|
||||
NSRect windowRect = [view convertRect:ns_UAZoom_cursor_rect_new
|
||||
toView:nil];
|
||||
NSRect screenRect = [[view window] convertRectToScreen:windowRect];
|
||||
CGRect cgRect = ns_cg_rect_flip_y (NSRectToCGRect (screenRect));
|
||||
/* Some versions of macOS can ignore tiny rects, so we
|
||||
slightly expand a tiny one. Since we care mostly about its
|
||||
origin, this should be innocuous. */
|
||||
cgRect.size.width = MAX (cgRect.size.width, 6);
|
||||
cgRect.size.height = MAX (cgRect.size.height, 10);
|
||||
if (force)
|
||||
{
|
||||
/* UAZoomChangeFocus needs old and new cursor positions to
|
||||
be different, and also it sometimes needs a kick. In
|
||||
both cases, we fake a cursor move followed by the real
|
||||
cursor move. */
|
||||
CGRect cgRectJiggle = CGRectOffset (cgRect, 1.0, 1.0);
|
||||
if (UAZoomChangeFocus (&cgRectJiggle, NULL,
|
||||
kUAZoomFocusTypeInsertionPoint))
|
||||
NSLog (@"UAZoomChangeFocus jiggle failed");
|
||||
}
|
||||
if (UAZoomChangeFocus (&cgRect, NULL,
|
||||
kUAZoomFocusTypeInsertionPoint))
|
||||
NSLog (@"UAZoomChangeFocus failed");
|
||||
NSAccessibilityPostNotification
|
||||
(view, NSAccessibilityFocusedUIElementChangedNotification);
|
||||
}
|
||||
}
|
||||
else
|
||||
ns_update_was_UAZoomEnabled = NO;
|
||||
}
|
||||
#endif /* NS_IMPL_COCOA */
|
||||
|
||||
static void
|
||||
ns_update_end (struct frame *f)
|
||||
|
|
@ -1102,6 +1228,10 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen)
|
|||
[[view window] flushWindow];
|
||||
#endif
|
||||
|
||||
#ifdef NS_IMPL_COCOA
|
||||
ns_UAZoomChangeFocus (view, false);
|
||||
#endif
|
||||
|
||||
unblock_input ();
|
||||
ns_updating_frame = NULL;
|
||||
}
|
||||
|
|
@ -3232,6 +3362,16 @@ 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
|
||||
/* Cache the cursor rect for macOS Accessibility Zoom integration (see
|
||||
ns_update_end). Only store the rect for the active cursor ---
|
||||
inactive windows must not overwrite the value because redisplay may
|
||||
draw multiple windows per frame and the drawing order is not
|
||||
guaranteed. */
|
||||
if (active_p)
|
||||
ns_UAZoom_cursor_rect_new = r;
|
||||
#endif
|
||||
|
||||
ns_focus (f, NULL, 0);
|
||||
|
||||
NSGraphicsContext *ctx = [NSGraphicsContext currentContext];
|
||||
|
|
@ -6377,6 +6517,14 @@ - (void)applicationDidFinishLaunching: (NSNotification *)notification
|
|||
}
|
||||
#endif
|
||||
|
||||
#ifdef NS_IMPL_COCOA
|
||||
/* Is accessibility enabled for this process/bundle? */
|
||||
if (AXIsProcessTrusted())
|
||||
NSLog (@"Emacs is macOS AXIsProcessTrusted");
|
||||
else
|
||||
NSLog (@"Emacs is not macOS AXIsProcessTrusted");
|
||||
#endif
|
||||
|
||||
ns_send_appdefined (-2);
|
||||
}
|
||||
|
||||
|
|
@ -7288,6 +7436,12 @@ - (NSRect) firstRectForCharacterRange: (NSRange) range
|
|||
return [self firstRectForCharacterRange: range];
|
||||
}
|
||||
|
||||
- (NSRect)accessibilityFrame
|
||||
{
|
||||
EmacsView *view = FRAME_NS_VIEW (emacsframe);
|
||||
return [[view window] convertRectToScreen: ns_UAZoom_cursor_rect_new];
|
||||
}
|
||||
|
||||
#endif /* NS_IMPL_COCOA */
|
||||
|
||||
/***********************************************************************
|
||||
|
|
@ -8242,12 +8396,48 @@ - (void)windowDidBecomeKey /* for direct calls */
|
|||
ns_frame_rehighlight (emacsframe);
|
||||
[self adjustEmacsFrameRect];
|
||||
|
||||
#ifdef NS_IMPL_COCOA
|
||||
EmacsView *view = FRAME_NS_VIEW (emacsframe);
|
||||
/* Make sure we have focus and the timer isn't already scheduled. */
|
||||
if (self.window.firstResponder == view
|
||||
&& !ns_deferred_UAZoomChangeFocus_timer)
|
||||
{
|
||||
/* Calls to ns_UAZoomChangeFocus are synchronous. We defer the
|
||||
call to give macOS time to finish window compositing or the
|
||||
calls can be silently ignored by the Zoom daemon and with no
|
||||
errors reported. This also helps ensure ns_draw_window_cursor
|
||||
has populated ns_UAZoom_cursor_rect_new. The 200 ms delay was
|
||||
chosen as a balance between macOS headroom and user
|
||||
perception. */
|
||||
ns_deferred_UAZoomChangeFocus_timer
|
||||
= [[NSTimer
|
||||
scheduledTimerWithTimeInterval:
|
||||
NS_UAZOOMENABLED_DEFER_INTERVAL_SECS
|
||||
target: self
|
||||
selector:
|
||||
@selector (deferred_UAZoomChangeFocus_handler:)
|
||||
userInfo: 0
|
||||
repeats: NO]
|
||||
retain];
|
||||
}
|
||||
#endif
|
||||
|
||||
event.kind = FOCUS_IN_EVENT;
|
||||
XSETFRAME (event.frame_or_window, emacsframe);
|
||||
kbd_buffer_store_event (&event);
|
||||
ns_send_appdefined (-1); // Kick main loop
|
||||
}
|
||||
|
||||
#ifdef NS_IMPL_COCOA
|
||||
- (void)deferred_UAZoomChangeFocus_handler: (NSTimer *)timer
|
||||
{
|
||||
EmacsView *view = FRAME_NS_VIEW (emacsframe);
|
||||
ns_UAZoomChangeFocus (view, true);
|
||||
[ns_deferred_UAZoomChangeFocus_timer invalidate];
|
||||
[ns_deferred_UAZoomChangeFocus_timer release];
|
||||
ns_deferred_UAZoomChangeFocus_timer = nil;
|
||||
}
|
||||
#endif
|
||||
|
||||
- (void)windowDidResignKey: (NSNotification *)notification
|
||||
/* cf. x_detect_focus_change(), x_focus_changed(), x_new_focus_frame() */
|
||||
|
|
@ -8344,6 +8534,13 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f
|
|||
|
||||
FRAME_NS_VIEW (f) = self;
|
||||
emacsframe = f;
|
||||
|
||||
#ifdef NS_IMPL_COCOA
|
||||
/* macOS Accessibility Zoom Support. */
|
||||
ns_UAZoom_cursor_rect_new = NSZeroRect;
|
||||
ns_UAZoom_cursor_rect_old = NSZeroRect;
|
||||
#endif
|
||||
|
||||
#ifdef NS_IMPL_COCOA
|
||||
old_title = 0;
|
||||
maximizing_resize = NO;
|
||||
|
|
|
|||
Loading…
Reference in a new issue