This all is a bit hackish and should go away when native popovers are enabled by default. VoiceOver does not like having AXPopover windows appear as children of the root application node, so in this patch we ignore those windows and instead use the inline markup popovers. Differential Revision: https://phabricator.services.mozilla.com/D299894
444 lines
15 KiB
Plaintext
444 lines
15 KiB
Plaintext
/* clang-format off */
|
|
/* clang-format on */
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
#import <Cocoa/Cocoa.h>
|
|
#import <ApplicationServices/ApplicationServices.h>
|
|
|
|
#import "MOXTextMarkerDelegate.h"
|
|
|
|
#include "Platform.h"
|
|
#include "RemoteAccessible.h"
|
|
#include "DocAccessibleParent.h"
|
|
#include "mozTableAccessible.h"
|
|
#include "mozTextAccessible.h"
|
|
#include "MOXOuterDoc.h"
|
|
#include "MOXWebAreaAccessible.h"
|
|
#include "nsAccUtils.h"
|
|
#include "TextRange.h"
|
|
|
|
#include "nsAppShell.h"
|
|
#include "nsCocoaUtils.h"
|
|
#include "mozilla/EnumSet.h"
|
|
#include "mozilla/glean/AccessibleMetrics.h"
|
|
|
|
// Available from 10.13 onwards; test availability at runtime before using
|
|
@interface NSWorkspace (AvailableSinceHighSierra)
|
|
@property(readonly) BOOL isVoiceOverEnabled;
|
|
@property(readonly) BOOL isSwitchControlEnabled;
|
|
@end
|
|
|
|
namespace mozilla {
|
|
namespace a11y {
|
|
|
|
// Mac a11y whitelisting
|
|
static bool sA11yShouldBeEnabled = false;
|
|
|
|
bool ShouldA11yBeEnabled() {
|
|
EPlatformDisabledState disabledState = PlatformDisabledState();
|
|
return (disabledState == ePlatformIsForceEnabled) ||
|
|
((disabledState == ePlatformIsEnabled) && sA11yShouldBeEnabled);
|
|
}
|
|
|
|
void PlatformInit() {}
|
|
|
|
void PlatformShutdown() {}
|
|
|
|
void ProxyCreated(RemoteAccessible* aProxy) {
|
|
if (aProxy->Role() == roles::WHITESPACE) {
|
|
// We don't create a native object if we're child of a "flat" accessible;
|
|
// for example, on OS X buttons shouldn't have any children, because that
|
|
// makes the OS confused. We also don't create accessibles for <br>
|
|
// (whitespace) elements.
|
|
return;
|
|
}
|
|
|
|
// Pass in dummy state for now as retrieving proxy state requires IPC.
|
|
// Note that we can use RemoteAccessible::IsTable* functions here because they
|
|
// do not use IPC calls but that might change after bug 1210477.
|
|
Class type;
|
|
if (aProxy->IsTable()) {
|
|
type = [mozTableAccessible class];
|
|
} else if (aProxy->IsTableRow()) {
|
|
type = [mozTableRowAccessible class];
|
|
} else if (aProxy->IsTableCell()) {
|
|
type = [mozTableCellAccessible class];
|
|
} else if (aProxy->IsDoc()) {
|
|
type = [MOXWebAreaAccessible class];
|
|
} else if (aProxy->IsOuterDoc()) {
|
|
type = [MOXOuterDoc class];
|
|
} else {
|
|
type = GetTypeFromRole(aProxy->Role());
|
|
}
|
|
|
|
mozAccessible* mozWrapper = [[type alloc] initWithAccessible:aProxy];
|
|
aProxy->SetWrapper(reinterpret_cast<uintptr_t>(mozWrapper));
|
|
}
|
|
|
|
void ProxyDestroyed(RemoteAccessible* aProxy) {
|
|
mozAccessible* wrapper = GetNativeFromGeckoAccessible(aProxy);
|
|
[wrapper expire];
|
|
[wrapper release];
|
|
aProxy->SetWrapper(0);
|
|
|
|
if (aProxy->IsDoc()) {
|
|
[MOXTextMarkerDelegate destroyForDoc:aProxy];
|
|
}
|
|
}
|
|
|
|
void PlatformEvent(Accessible* aTarget, uint32_t aEventType) {
|
|
// Ignore event that we don't escape below, they aren't yet supported.
|
|
if (aEventType != nsIAccessibleEvent::EVENT_ALERT &&
|
|
aEventType != nsIAccessibleEvent::EVENT_VALUE_CHANGE &&
|
|
aEventType != nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE &&
|
|
aEventType != nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE &&
|
|
aEventType != nsIAccessibleEvent::EVENT_REORDER &&
|
|
aEventType != nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED &&
|
|
aEventType != nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED &&
|
|
aEventType != nsIAccessibleEvent::EVENT_LIVE_REGION_CHANGED &&
|
|
aEventType != nsIAccessibleEvent::EVENT_NAME_CHANGE &&
|
|
aEventType != nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED &&
|
|
aEventType != nsIAccessibleEvent::EVENT_ERRORMESSAGE_CHANGED) {
|
|
return;
|
|
}
|
|
|
|
mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget);
|
|
if (wrapper) {
|
|
[wrapper handleAccessibleEvent:aEventType];
|
|
}
|
|
}
|
|
|
|
void PlatformStateChangeEvent(Accessible* aTarget, uint64_t aState,
|
|
bool aEnabled) {
|
|
mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget);
|
|
if (wrapper) {
|
|
[wrapper stateChanged:aState isEnabled:aEnabled];
|
|
}
|
|
}
|
|
|
|
void PlatformFocusEvent(Accessible* aTarget) {
|
|
if (mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget)) {
|
|
[wrapper handleAccessibleEvent:nsIAccessibleEvent::EVENT_FOCUS];
|
|
}
|
|
}
|
|
|
|
void PlatformFocusedAccLocationChanged(Accessible* aFocusedAcc) {
|
|
// XXX: This function is called when CacheDomain::Viewport is updated on the
|
|
// document and the focused acc's bounds have changed. We redirect here
|
|
// instead of doing this work in maybeFireUAZoomChangeFocusEvent because
|
|
// other platforms will want this hook, too. See bug 1469779.
|
|
mozAccessible* nativeFocused = GetNativeFromGeckoAccessible(aFocusedAcc);
|
|
if (nativeFocused) {
|
|
int focusType = aFocusedAcc->IsEditableRoot()
|
|
? kUAZoomFocusTypeInsertionPoint
|
|
: kUAZoomFocusTypeOther;
|
|
[nativeFocused maybeFireUAZoomChangeFocusEvent:focusType];
|
|
}
|
|
}
|
|
|
|
bool PlatformShouldTrackFocusedAccLocation() {
|
|
// We want to track location changes for the focused acc if the
|
|
// macOS magnifier is enabled, or we're running tests that have
|
|
// the bounds domain enabled. We can't just check that we're in a
|
|
// testing environment, because this will break our caching granularity
|
|
// tests.
|
|
return UAZoomEnabled() ||
|
|
(xpc::IsInAutomation() && DomainsAreActive(CacheDomain::Bounds));
|
|
}
|
|
|
|
void PlatformCaretMoveEvent(Accessible* aTarget, int32_t aOffset,
|
|
bool aIsSelectionCollapsed, int32_t aGranularity,
|
|
bool aFromUser) {
|
|
mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget);
|
|
MOXTextMarkerDelegate* delegate = [MOXTextMarkerDelegate
|
|
getOrCreateForDoc:nsAccUtils::DocumentFor(aTarget)];
|
|
[delegate setCaretOffset:aTarget at:aOffset moveGranularity:aGranularity];
|
|
if (aIsSelectionCollapsed) {
|
|
// If selection is collapsed, invalidate selection.
|
|
[delegate setSelectionFrom:aTarget at:aOffset to:aTarget at:aOffset];
|
|
}
|
|
|
|
if (wrapper) {
|
|
if (mozAccessible* editable = [wrapper moxEditableAncestor]) {
|
|
[editable
|
|
handleAccessibleEvent:nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED];
|
|
} else {
|
|
[wrapper
|
|
handleAccessibleEvent:nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED];
|
|
}
|
|
}
|
|
}
|
|
|
|
void PlatformTextChangeEvent(Accessible* aTarget, const nsAString& aStr,
|
|
int32_t aStart, uint32_t aLen, bool aIsInsert,
|
|
bool aFromUser) {
|
|
mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget);
|
|
if (wrapper) {
|
|
if (mozAccessible* editable = [wrapper moxEditableAncestor]) {
|
|
[editable handleAccessibleTextChangeEvent:nsCocoaUtils::ToNSString(aStr)
|
|
inserted:aIsInsert
|
|
inContainer:aTarget
|
|
at:aStart];
|
|
} else {
|
|
[wrapper maybePostValidationErrorChanged];
|
|
}
|
|
}
|
|
}
|
|
|
|
void PlatformShowHideEvent(Accessible* aTarget, Accessible* aParent,
|
|
bool aIsInsert, bool aFromUser) {
|
|
mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget);
|
|
if (wrapper) {
|
|
[wrapper
|
|
handleAccessibleEvent:(aIsInsert ? nsIAccessibleEvent::EVENT_SHOW
|
|
: nsIAccessibleEvent::EVENT_HIDE)];
|
|
}
|
|
}
|
|
|
|
void PlatformSelectionEvent(Accessible* aTarget, Accessible* aWidget,
|
|
uint32_t aEventType) {
|
|
mozAccessible* wrapper = GetNativeFromGeckoAccessible(aWidget);
|
|
if (wrapper) {
|
|
[wrapper handleAccessibleEvent:aEventType];
|
|
}
|
|
}
|
|
|
|
void PlatformTextSelectionChangeEvent(Accessible* aTarget,
|
|
const nsTArray<TextRange>& aSelection) {
|
|
if (aSelection.Length()) {
|
|
MOXTextMarkerDelegate* delegate = [MOXTextMarkerDelegate
|
|
getOrCreateForDoc:nsAccUtils::DocumentFor(aTarget)];
|
|
// Cache the selection.
|
|
[delegate setSelectionFrom:aSelection[0].StartContainer()
|
|
at:aSelection[0].StartOffset()
|
|
to:aSelection[0].EndContainer()
|
|
at:aSelection[0].EndOffset()];
|
|
}
|
|
|
|
mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget);
|
|
if (wrapper) {
|
|
[wrapper
|
|
handleAccessibleEvent:nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED];
|
|
}
|
|
}
|
|
|
|
void PlatformRoleChangedEvent(Accessible* aTarget, const a11y::role& aRole,
|
|
uint8_t aRoleMapEntryIndex) {
|
|
if (mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget)) {
|
|
[wrapper handleRoleChanged:aRole];
|
|
}
|
|
}
|
|
|
|
void PlatformAnnouncementEvent(Accessible* aTarget,
|
|
const nsAString& aAnnouncement,
|
|
uint16_t aPriority) {
|
|
mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget);
|
|
if (wrapper) {
|
|
[wrapper handleAnnouncementEvent:nsCocoaUtils::ToNSString(aAnnouncement)
|
|
priority:aPriority];
|
|
}
|
|
}
|
|
|
|
// This enum lists possible assistive technology clients. It's intended for use
|
|
// in an EnumSet since there can be multiple ATs active at once.
|
|
enum class Client : uint64_t {
|
|
Unknown,
|
|
VoiceOver,
|
|
SwitchControl,
|
|
UAZoom,
|
|
FullKeyboardAccess,
|
|
VoiceControl,
|
|
SpeakSelection,
|
|
SpeakItemUnderMouse,
|
|
SpeakTypingFeedback,
|
|
HoverText
|
|
};
|
|
|
|
// Get the set of currently-active clients and the client to log.
|
|
// XXX: We should log all clients, but default to the first one encountered.
|
|
// (bug 2021479)
|
|
std::pair<EnumSet<Client>, Client> GetClients() {
|
|
EnumSet<Client> clients;
|
|
std::optional<Client> clientToLog;
|
|
auto AddClient = [&clients, &clientToLog](Client client) {
|
|
clients += client;
|
|
if (!clientToLog.has_value()) {
|
|
clientToLog = client;
|
|
}
|
|
};
|
|
if ([[NSWorkspace sharedWorkspace]
|
|
respondsToSelector:@selector(isVoiceOverEnabled)] &&
|
|
[[NSWorkspace sharedWorkspace] isVoiceOverEnabled]) {
|
|
AddClient(Client::VoiceOver);
|
|
}
|
|
|
|
if ([[NSWorkspace sharedWorkspace]
|
|
respondsToSelector:@selector(isSwitchControlEnabled)] &&
|
|
[[NSWorkspace sharedWorkspace] isSwitchControlEnabled]) {
|
|
AddClient(Client::SwitchControl);
|
|
}
|
|
|
|
if (UAZoomEnabled()) {
|
|
AddClient(Client::UAZoom);
|
|
}
|
|
|
|
// This is more complicated than the NSWorkspace queries above
|
|
// because (a) there is no "full keyboard access" query for NSWorkspace
|
|
// and (b) the [NSApplication fullKeyboardAccessEnabled] query checks
|
|
// the pre-Monterey version of full keyboard access, which is not what
|
|
// we're looking for here. For more info, see bug 1772375 comment 7.
|
|
Boolean exists;
|
|
long val = CFPreferencesGetAppIntegerValue(CFSTR("FullKeyboardAccessEnabled"),
|
|
CFSTR("com.apple.Accessibility"),
|
|
&exists);
|
|
if (exists && val == 1) {
|
|
AddClient(Client::FullKeyboardAccess);
|
|
}
|
|
|
|
val = CFPreferencesGetAppIntegerValue(CFSTR("CommandAndControlEnabled"),
|
|
CFSTR("com.apple.Accessibility"),
|
|
&exists);
|
|
if (exists && val == 1) {
|
|
AddClient(Client::VoiceControl);
|
|
}
|
|
|
|
val = CFPreferencesGetAppIntegerValue(
|
|
CFSTR("SpeakThisEnabled"), CFSTR("com.apple.universalaccess"), &exists);
|
|
if (exists && val == 1) {
|
|
AddClient(Client::SpeakSelection);
|
|
}
|
|
|
|
val = CFPreferencesGetAppIntegerValue(CFSTR("speakItemUnderMouseEnabled"),
|
|
CFSTR("com.apple.universalaccess"),
|
|
&exists);
|
|
if (exists && val == 1) {
|
|
AddClient(Client::SpeakItemUnderMouse);
|
|
}
|
|
|
|
val = CFPreferencesGetAppIntegerValue(
|
|
CFSTR("typingEchoEnabled"), CFSTR("com.apple.universalaccess"), &exists);
|
|
if (exists && val == 1) {
|
|
AddClient(Client::SpeakTypingFeedback);
|
|
}
|
|
|
|
val = CFPreferencesGetAppIntegerValue(
|
|
CFSTR("hoverTextEnabled"), CFSTR("com.apple.universalaccess"), &exists);
|
|
if (exists && val == 1) {
|
|
AddClient(Client::HoverText);
|
|
}
|
|
|
|
if (clients.isEmpty()) {
|
|
AddClient(Client::Unknown);
|
|
}
|
|
return std::make_pair(clients, clientToLog.value());
|
|
}
|
|
|
|
// Expects a single client, returns a string representation of that client.
|
|
constexpr const char* GetStringForClient(Client aClient) {
|
|
switch (aClient) {
|
|
case Client::Unknown:
|
|
return "Unknown";
|
|
case Client::VoiceOver:
|
|
return "VoiceOver";
|
|
case Client::SwitchControl:
|
|
return "SwitchControl";
|
|
case Client::UAZoom:
|
|
return "UAZoom";
|
|
case Client::FullKeyboardAccess:
|
|
return "FullKeyboardAccess";
|
|
case Client::VoiceControl:
|
|
return "VoiceControl";
|
|
case Client::SpeakSelection:
|
|
return "SpeakSelection";
|
|
case Client::SpeakItemUnderMouse:
|
|
return "SpeakItemUnderMouse";
|
|
case Client::SpeakTypingFeedback:
|
|
return "SpeakTypingFeedback";
|
|
case Client::HoverText:
|
|
return "HoverText";
|
|
default:
|
|
break;
|
|
}
|
|
MOZ_ASSERT_UNREACHABLE("Unknown Client enum value!");
|
|
return "";
|
|
}
|
|
|
|
uint64_t GetCacheDomainsForKnownClients(uint64_t aCacheDomains) {
|
|
auto [clients, _] = GetClients();
|
|
// We expect VoiceOver will require all information we have.
|
|
if (clients.contains(Client::VoiceOver)) {
|
|
return CacheDomain::All;
|
|
}
|
|
if (clients.contains(Client::FullKeyboardAccess)) {
|
|
aCacheDomains |= CacheDomain::Bounds;
|
|
}
|
|
if (clients.contains(Client::SwitchControl)) {
|
|
// XXX: Find minimum set of domains required for SwitchControl.
|
|
// SwitchControl can give up if we don't furnish it certain information.
|
|
return CacheDomain::All;
|
|
}
|
|
if (clients.contains(Client::VoiceControl)) {
|
|
// XXX: Find minimum set of domains required for VoiceControl.
|
|
return CacheDomain::All;
|
|
}
|
|
if (clients.contains(Client::UAZoom)) {
|
|
// XXX bug 2021474: Find minimum set of domains required for UAZoom.
|
|
return CacheDomain::All;
|
|
}
|
|
return aCacheDomains;
|
|
}
|
|
|
|
void GetHumanReadableInstantiatorStr(nsAString& aResult) {
|
|
auto [clients, _] = GetClients();
|
|
nsAutoString result;
|
|
for (Client client : clients) {
|
|
if (!result.IsEmpty()) {
|
|
result.AppendLiteral(",");
|
|
}
|
|
result.AppendASCII(GetStringForClient(client));
|
|
}
|
|
aResult = result;
|
|
}
|
|
|
|
} // namespace a11y
|
|
} // namespace mozilla
|
|
|
|
@interface GeckoNSApplication (a11y)
|
|
- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute;
|
|
@end
|
|
|
|
@implementation GeckoNSApplication (a11y)
|
|
|
|
- (NSAccessibilityRole)accessibilityRole {
|
|
// For ATs that don't request `AXEnhancedUserInterface` we need to enable
|
|
// accessibility when a role is fetched. Not ideal, but this is needed
|
|
// for such services as Voice Control.
|
|
if (!mozilla::a11y::sA11yShouldBeEnabled) {
|
|
[self accessibilitySetValue:@YES forAttribute:@"AXEnhancedUserInterface"];
|
|
}
|
|
return [super accessibilityRole];
|
|
}
|
|
|
|
- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute {
|
|
if ([attribute isEqualToString:@"AXEnhancedUserInterface"]) {
|
|
mozilla::a11y::sA11yShouldBeEnabled = ([value intValue] == 1);
|
|
if (sA11yShouldBeEnabled) {
|
|
// If accessibility should be enabled, log the appropriate client
|
|
auto [_, clientToLog] = GetClients();
|
|
const char* client = GetStringForClient(clientToLog);
|
|
|
|
#if defined(MOZ_TELEMETRY_REPORTING)
|
|
mozilla::glean::a11y::instantiators.Set(nsDependentCString(client));
|
|
#endif // defined(MOZ_TELEMETRY_REPORTING)
|
|
CrashReporter::RecordAnnotationCString(
|
|
CrashReporter::Annotation::AccessibilityClient, client);
|
|
}
|
|
}
|
|
|
|
return [super accessibilitySetValue:value forAttribute:attribute];
|
|
}
|
|
|
|
@end
|