mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-27 14:52:16 +00:00
949bd3acfe
Introduce a gecko live region changed event and fire it from within content. This way it gets coalesced in the case of many insertions/deletions. Also, rely on text insert/delete instead of reorder because there can be cases where the text in a leaf changes as opposed to a tree mutation. We get text insert/delete on mutations too, so that should cover it. Differential Revision: https://phabricator.services.mozilla.com/D224388
342 lines
12 KiB
Plaintext
342 lines
12 KiB
Plaintext
/* clang-format off */
|
|
/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* 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 "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/Telemetry.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 if (aProxy->IsTextField() && !aProxy->HasNumericValue()) {
|
|
type = [mozTextAccessible 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) {
|
|
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,
|
|
const LayoutDeviceIntRect& aCaretRect) {
|
|
if (mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget)) {
|
|
[wrapper handleAccessibleEvent:nsIAccessibleEvent::EVENT_FOCUS];
|
|
}
|
|
}
|
|
|
|
void PlatformCaretMoveEvent(Accessible* aTarget, int32_t aOffset,
|
|
bool aIsSelectionCollapsed, int32_t aGranularity,
|
|
const LayoutDeviceIntRect& aCaretRect,
|
|
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 (mozTextAccessible* textAcc =
|
|
static_cast<mozTextAccessible*>([wrapper moxEditableAncestor])) {
|
|
[textAcc
|
|
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) {
|
|
Accessible* acc = aTarget;
|
|
// If there is a text input ancestor, use it as the event source.
|
|
while (acc && GetTypeFromRole(acc->Role()) != [mozTextAccessible class]) {
|
|
acc = acc->Parent();
|
|
}
|
|
mozAccessible* wrapper = GetNativeFromGeckoAccessible(acc ? acc : aTarget);
|
|
[wrapper handleAccessibleTextChangeEvent:nsCocoaUtils::ToNSString(aStr)
|
|
inserted:aIsInsert
|
|
inContainer:aTarget
|
|
at:aStart];
|
|
}
|
|
|
|
void PlatformShowHideEvent(Accessible*, Accessible*, bool, bool) {}
|
|
|
|
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];
|
|
}
|
|
}
|
|
|
|
// 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,
|
|
FullKeyboardAccess,
|
|
VoiceControl
|
|
};
|
|
|
|
// 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.
|
|
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);
|
|
} else if ([[NSWorkspace sharedWorkspace]
|
|
respondsToSelector:@selector(isSwitchControlEnabled)] &&
|
|
[[NSWorkspace sharedWorkspace] isSwitchControlEnabled]) {
|
|
AddClient(Client::SwitchControl);
|
|
} else {
|
|
// 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;
|
|
int val = CFPreferencesGetAppIntegerValue(
|
|
CFSTR("FullKeyboardAccessEnabled"), CFSTR("com.apple.Accessibility"),
|
|
&exists);
|
|
if (exists && val == 1) {
|
|
AddClient(Client::FullKeyboardAccess);
|
|
} else {
|
|
val = CFPreferencesGetAppIntegerValue(CFSTR("CommandAndControlEnabled"),
|
|
CFSTR("com.apple.Accessibility"),
|
|
&exists);
|
|
if (exists && val == 1) {
|
|
AddClient(Client::VoiceControl);
|
|
} else {
|
|
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::FullKeyboardAccess:
|
|
return "FullKeyboardAccess";
|
|
case Client::VoiceControl:
|
|
return "VoiceControl";
|
|
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;
|
|
}
|
|
return aCacheDomains;
|
|
}
|
|
|
|
} // 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::Telemetry::ScalarSet(
|
|
mozilla::Telemetry::ScalarID::A11Y_INSTANTIATORS,
|
|
NS_ConvertASCIItoUTF16(client));
|
|
#endif // defined(MOZ_TELEMETRY_REPORTING)
|
|
CrashReporter::RecordAnnotationCString(
|
|
CrashReporter::Annotation::AccessibilityClient, client);
|
|
}
|
|
}
|
|
|
|
return [super accessibilitySetValue:value forAttribute:attribute];
|
|
}
|
|
|
|
@end
|