gecko-dev/accessible/mac/Platform.mm
Eitan Isaacson 949bd3acfe Bug 1915262 - Fire queued live region event from content in MacOS. r=morgan
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
2024-10-10 17:36:26 +00:00

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