summaryrefslogtreecommitdiffstats
path: root/accessible/ios/MUIAccessible.mm
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:14:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:14:29 +0000
commitfbaf0bb26397aa498eb9156f06d5a6fe34dd7dd8 (patch)
tree4c1ccaf5486d4f2009f9a338a98a83e886e29c97 /accessible/ios/MUIAccessible.mm
parentReleasing progress-linux version 124.0.1-1~progress7.99u1. (diff)
downloadfirefox-fbaf0bb26397aa498eb9156f06d5a6fe34dd7dd8.tar.xz
firefox-fbaf0bb26397aa498eb9156f06d5a6fe34dd7dd8.zip
Merging upstream version 125.0.1.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'accessible/ios/MUIAccessible.mm')
-rw-r--r--accessible/ios/MUIAccessible.mm497
1 files changed, 497 insertions, 0 deletions
diff --git a/accessible/ios/MUIAccessible.mm b/accessible/ios/MUIAccessible.mm
new file mode 100644
index 0000000000..46c4712e2e
--- /dev/null
+++ b/accessible/ios/MUIAccessible.mm
@@ -0,0 +1,497 @@
+/* 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 "MUIAccessible.h"
+
+#include "nsString.h"
+#include "RootAccessibleWrap.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+#ifdef A11Y_LOG
+# define DEBUG_HINTS
+#endif
+
+#ifdef DEBUG_HINTS
+static NSString* ToNSString(const nsACString& aCString) {
+ if (aCString.IsEmpty()) {
+ return [NSString string];
+ }
+ return [[[NSString alloc] initWithBytes:aCString.BeginReading()
+ length:aCString.Length()
+ encoding:NSUTF8StringEncoding] autorelease];
+}
+#endif
+
+static NSString* ToNSString(const nsAString& aString) {
+ if (aString.IsEmpty()) {
+ return [NSString string];
+ }
+ return [NSString stringWithCharacters:reinterpret_cast<const unichar*>(
+ aString.BeginReading())
+ length:aString.Length()];
+}
+
+// These rules offer conditions for whether a gecko accessible
+// should be considered a UIKit accessibility element. Each role is mapped to a
+// rule.
+enum class IsAccessibilityElementRule {
+ // Always yes
+ Yes,
+ // Always no
+ No,
+ // If the accessible has no children. For example an empty header
+ // which is labeled.
+ IfChildless,
+ // If the accessible has no children and it is named and focusable.
+ IfChildlessWithNameAndFocusable,
+ // If this accessible isn't a child of an accessibility element. For example,
+ // a text leaf child of a button.
+ IfParentIsntElementWithName,
+ // If this accessible has multiple leafs that should functionally be
+ // united, for example a link with span elements.
+ IfBrokenUp,
+};
+
+class Trait {
+ public:
+ static const uint64_t None = 0;
+ static const uint64_t Button = ((uint64_t)0x1) << 0;
+ static const uint64_t Link = ((uint64_t)0x1) << 1;
+ static const uint64_t Image = ((uint64_t)0x1) << 2;
+ static const uint64_t Selected = ((uint64_t)0x1) << 3;
+ static const uint64_t PlaysSound = ((uint64_t)0x1) << 4;
+ static const uint64_t KeyboardKey = ((uint64_t)0x1) << 5;
+ static const uint64_t StaticText = ((uint64_t)0x1) << 6;
+ static const uint64_t SummaryElement = ((uint64_t)0x1) << 7;
+ static const uint64_t NotEnabled = ((uint64_t)0x1) << 8;
+ static const uint64_t UpdatesFrequently = ((uint64_t)0x1) << 9;
+ static const uint64_t SearchField = ((uint64_t)0x1) << 10;
+ static const uint64_t StartsMediaSession = ((uint64_t)0x1) << 11;
+ static const uint64_t Adjustable = ((uint64_t)0x1) << 12;
+ static const uint64_t AllowsDirectInteraction = ((uint64_t)0x1) << 13;
+ static const uint64_t CausesPageTurn = ((uint64_t)0x1) << 14;
+ static const uint64_t TabBar = ((uint64_t)0x1) << 15;
+ static const uint64_t Header = ((uint64_t)0x1) << 16;
+ static const uint64_t WebContent = ((uint64_t)0x1) << 17;
+ static const uint64_t TextEntry = ((uint64_t)0x1) << 18;
+ static const uint64_t PickerElement = ((uint64_t)0x1) << 19;
+ static const uint64_t RadioButton = ((uint64_t)0x1) << 20;
+ static const uint64_t IsEditing = ((uint64_t)0x1) << 21;
+ static const uint64_t LaunchIcon = ((uint64_t)0x1) << 22;
+ static const uint64_t StatusBarElement = ((uint64_t)0x1) << 23;
+ static const uint64_t SecureTextField = ((uint64_t)0x1) << 24;
+ static const uint64_t Inactive = ((uint64_t)0x1) << 25;
+ static const uint64_t Footer = ((uint64_t)0x1) << 26;
+ static const uint64_t BackButton = ((uint64_t)0x1) << 27;
+ static const uint64_t TabButton = ((uint64_t)0x1) << 28;
+ static const uint64_t AutoCorrectCandidate = ((uint64_t)0x1) << 29;
+ static const uint64_t DeleteKey = ((uint64_t)0x1) << 30;
+ static const uint64_t SelectionDismissesItem = ((uint64_t)0x1) << 31;
+ static const uint64_t Visited = ((uint64_t)0x1) << 32;
+ static const uint64_t Scrollable = ((uint64_t)0x1) << 33;
+ static const uint64_t Spacer = ((uint64_t)0x1) << 34;
+ static const uint64_t TableIndex = ((uint64_t)0x1) << 35;
+ static const uint64_t Map = ((uint64_t)0x1) << 36;
+ static const uint64_t TextOperationsAvailable = ((uint64_t)0x1) << 37;
+ static const uint64_t Draggable = ((uint64_t)0x1) << 38;
+ static const uint64_t GesturePracticeRegion = ((uint64_t)0x1) << 39;
+ static const uint64_t PopupButton = ((uint64_t)0x1) << 40;
+ static const uint64_t AllowsNativeSliding = ((uint64_t)0x1) << 41;
+ static const uint64_t MathEquation = ((uint64_t)0x1) << 42;
+ static const uint64_t ContainedByTable = ((uint64_t)0x1) << 43;
+ static const uint64_t ContainedByList = ((uint64_t)0x1) << 44;
+ static const uint64_t TouchContainer = ((uint64_t)0x1) << 45;
+ static const uint64_t SupportsZoom = ((uint64_t)0x1) << 46;
+ static const uint64_t TextArea = ((uint64_t)0x1) << 47;
+ static const uint64_t BookContent = ((uint64_t)0x1) << 48;
+ static const uint64_t ContainedByLandmark = ((uint64_t)0x1) << 49;
+ static const uint64_t FolderIcon = ((uint64_t)0x1) << 50;
+ static const uint64_t ReadOnly = ((uint64_t)0x1) << 51;
+ static const uint64_t MenuItem = ((uint64_t)0x1) << 52;
+ static const uint64_t Toggle = ((uint64_t)0x1) << 53;
+ static const uint64_t IgnoreItemChooser = ((uint64_t)0x1) << 54;
+ static const uint64_t SupportsTrackingDetail = ((uint64_t)0x1) << 55;
+ static const uint64_t Alert = ((uint64_t)0x1) << 56;
+ static const uint64_t ContainedByFieldset = ((uint64_t)0x1) << 57;
+ static const uint64_t AllowsLayoutChangeInStatusBar = ((uint64_t)0x1) << 58;
+};
+
+#pragma mark -
+
+@interface NSObject (AccessibilityPrivate)
+- (void)_accessibilityUnregister;
+@end
+
+@implementation MUIAccessible
+
+- (id)initWithAccessible:(Accessible*)aAcc {
+ MOZ_ASSERT(aAcc, "Cannot init MUIAccessible with null");
+ if ((self = [super init])) {
+ mGeckoAccessible = aAcc;
+ }
+
+ return self;
+}
+
+- (mozilla::a11y::Accessible*)geckoAccessible {
+ return mGeckoAccessible;
+}
+
+- (void)expire {
+ mGeckoAccessible = nullptr;
+ if ([self respondsToSelector:@selector(_accessibilityUnregister)]) {
+ [self _accessibilityUnregister];
+ }
+}
+
+- (void)dealloc {
+ [super dealloc];
+}
+
+static bool isAccessibilityElementInternal(Accessible* aAccessible) {
+ MOZ_ASSERT(aAccessible);
+ IsAccessibilityElementRule rule = IsAccessibilityElementRule::No;
+
+#define ROLE(_geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \
+ msaaRole, ia2Role, androidClass, iosIsElement, nameRule) \
+ case roles::_geckoRole: \
+ rule = iosIsElement; \
+ break;
+ switch (aAccessible->Role()) {
+#include "RoleMap.h"
+ }
+
+ switch (rule) {
+ case IsAccessibilityElementRule::Yes:
+ return true;
+ case IsAccessibilityElementRule::No:
+ return false;
+ case IsAccessibilityElementRule::IfChildless:
+ return aAccessible->ChildCount() == 0;
+ case IsAccessibilityElementRule::IfParentIsntElementWithName: {
+ nsAutoString name;
+ aAccessible->Name(name);
+ name.CompressWhitespace();
+ if (name.IsEmpty()) {
+ return false;
+ }
+
+ if (isAccessibilityElementInternal(aAccessible->Parent())) {
+ // This is a text leaf that needs to be pruned from a button or the
+ // likes. It should also be ignored in the event of its parent being a
+ // pruned link.
+ return false;
+ }
+
+ return true;
+ }
+ case IsAccessibilityElementRule::IfChildlessWithNameAndFocusable:
+ if (aAccessible->ChildCount() == 0 &&
+ (aAccessible->State() & states::FOCUSABLE)) {
+ nsAutoString name;
+ aAccessible->Name(name);
+ name.CompressWhitespace();
+ return !name.IsEmpty();
+ }
+ return false;
+ case IsAccessibilityElementRule::IfBrokenUp: {
+ uint32_t childCount = aAccessible->ChildCount();
+ if (childCount == 1) {
+ // If this is a single child container just use the text leaf and its
+ // traits will be inherited.
+ return false;
+ }
+
+ for (uint32_t idx = 0; idx < childCount; idx++) {
+ Accessible* child = aAccessible->ChildAt(idx);
+ role accRole = child->Role();
+ if (accRole != roles::STATICTEXT && accRole != roles::TEXT_LEAF &&
+ accRole != roles::GRAPHIC) {
+ // If this container contains anything but text leafs and images
+ // ignore this accessible. Its descendants will inherit the
+ // container's traits.
+ return false;
+ }
+ }
+
+ return true;
+ }
+ default:
+ break;
+ }
+
+ MOZ_ASSERT_UNREACHABLE("Unhandled IsAccessibilityElementRule");
+
+ return false;
+}
+
+- (BOOL)isAccessibilityElement {
+ if (!mGeckoAccessible) {
+ return NO;
+ }
+
+ return isAccessibilityElementInternal(mGeckoAccessible) ? YES : NO;
+}
+
+- (NSString*)accessibilityLabel {
+ if (!mGeckoAccessible) {
+ return @"";
+ }
+
+ nsAutoString name;
+ mGeckoAccessible->Name(name);
+
+ return ToNSString(name);
+}
+
+- (NSString*)accessibilityHint {
+ if (!mGeckoAccessible) {
+ return @"";
+ }
+
+#ifdef DEBUG_HINTS
+ // Just put in a debug description as the label so we get a clue about which
+ // accessible ends up where.
+ nsAutoCString desc;
+ mGeckoAccessible->DebugDescription(desc);
+ return ToNSString(desc);
+#else
+ return @"";
+#endif
+}
+
+- (CGRect)accessibilityFrame {
+ RootAccessibleWrap* rootAcc = static_cast<RootAccessibleWrap*>(
+ mGeckoAccessible->IsLocal()
+ ? mGeckoAccessible->AsLocal()->RootAccessible()
+ : mGeckoAccessible->AsRemote()
+ ->OuterDocOfRemoteBrowser()
+ ->RootAccessible());
+
+ if (!rootAcc) {
+ return CGRectMake(0, 0, 0, 0);
+ }
+
+ LayoutDeviceIntRect rect = mGeckoAccessible->Bounds();
+ return rootAcc->DevPixelsRectToUIKit(rect);
+}
+
+- (NSString*)accessibilityValue {
+ if (!mGeckoAccessible) {
+ return nil;
+ }
+
+ uint64_t state = mGeckoAccessible->State();
+ if (state & states::LINKED) {
+ // Value returns the URL. We don't want to expose that as the value on iOS.
+ return nil;
+ }
+
+ if (state & states::CHECKABLE) {
+ if (state & states::CHECKED) {
+ return @"1";
+ }
+ if (state & states::MIXED) {
+ return @"2";
+ }
+ return @"0";
+ }
+
+ if (mGeckoAccessible->IsPassword()) {
+ // Accessible::Value returns an empty string. On iOS, we need to return the
+ // masked password so that AT knows how many characters are in the password.
+ Accessible* leaf = mGeckoAccessible->FirstChild();
+ if (!leaf) {
+ return nil;
+ }
+ nsAutoString masked;
+ leaf->AppendTextTo(masked);
+ return ToNSString(masked);
+ }
+
+ // If there is a heading ancestor, self has the header trait, so value should
+ // be the heading level.
+ for (Accessible* acc = mGeckoAccessible; acc; acc = acc->Parent()) {
+ if (acc->Role() == roles::HEADING) {
+ return [NSString stringWithFormat:@"%d", acc->GroupPosition().level];
+ }
+ }
+
+ nsAutoString value;
+ mGeckoAccessible->Value(value);
+ return ToNSString(value);
+}
+
+static uint64_t GetAccessibilityTraits(Accessible* aAccessible) {
+ uint64_t state = aAccessible->State();
+ uint64_t traits = Trait::WebContent;
+ switch (aAccessible->Role()) {
+ case roles::LINK:
+ traits |= Trait::Link;
+ break;
+ case roles::GRAPHIC:
+ traits |= Trait::Image;
+ break;
+ case roles::PAGETAB:
+ traits |= Trait::TabButton;
+ break;
+ case roles::PUSHBUTTON:
+ case roles::SUMMARY:
+ case roles::COMBOBOX:
+ case roles::BUTTONMENU:
+ case roles::TOGGLE_BUTTON:
+ case roles::CHECKBUTTON:
+ case roles::SWITCH:
+ traits |= Trait::Button;
+ break;
+ case roles::RADIOBUTTON:
+ traits |= Trait::RadioButton;
+ break;
+ case roles::HEADING:
+ traits |= Trait::Header;
+ break;
+ case roles::STATICTEXT:
+ case roles::TEXT_LEAF:
+ traits |= Trait::StaticText;
+ break;
+ case roles::SLIDER:
+ case roles::SPINBUTTON:
+ traits |= Trait::Adjustable;
+ break;
+ case roles::MENUITEM:
+ case roles::PARENT_MENUITEM:
+ case roles::CHECK_MENU_ITEM:
+ case roles::RADIO_MENU_ITEM:
+ traits |= Trait::MenuItem;
+ break;
+ case roles::PASSWORD_TEXT:
+ traits |= Trait::SecureTextField;
+ break;
+ default:
+ break;
+ }
+
+ if ((traits & Trait::Link) && (state & states::TRAVERSED)) {
+ traits |= Trait::Visited;
+ }
+
+ if ((traits & Trait::Button) && (state & states::HASPOPUP)) {
+ traits |= Trait::PopupButton;
+ }
+
+ if (state & states::SELECTED) {
+ traits |= Trait::Selected;
+ }
+
+ if (state & states::CHECKABLE) {
+ traits |= Trait::Toggle;
+ }
+
+ if (!(state & states::ENABLED)) {
+ traits |= Trait::NotEnabled;
+ }
+
+ if (state & states::EDITABLE) {
+ traits |= Trait::TextEntry;
+ if (state & states::FOCUSED) {
+ // XXX: Also add "has text cursor" trait
+ traits |= Trait::IsEditing | Trait::TextOperationsAvailable;
+ }
+
+ if (aAccessible->IsSearchbox()) {
+ traits |= Trait::SearchField;
+ }
+
+ if (state & states::MULTI_LINE) {
+ traits |= Trait::TextArea;
+ }
+ }
+
+ return traits;
+}
+
+- (uint64_t)accessibilityTraits {
+ if (!mGeckoAccessible) {
+ return Trait::None;
+ }
+
+ uint64_t traits = GetAccessibilityTraits(mGeckoAccessible);
+
+ for (Accessible* parent = mGeckoAccessible->Parent(); parent;
+ parent = parent->Parent()) {
+ traits |= GetAccessibilityTraits(parent);
+ }
+
+ return traits;
+}
+
+- (NSInteger)accessibilityElementCount {
+ return mGeckoAccessible ? mGeckoAccessible->ChildCount() : 0;
+}
+
+- (nullable id)accessibilityElementAtIndex:(NSInteger)index {
+ if (!mGeckoAccessible) {
+ return nil;
+ }
+
+ Accessible* child = mGeckoAccessible->ChildAt(index);
+ return GetNativeFromGeckoAccessible(child);
+}
+
+- (NSInteger)indexOfAccessibilityElement:(id)element {
+ Accessible* acc = [(MUIAccessible*)element geckoAccessible];
+ if (!acc || mGeckoAccessible != acc->Parent()) {
+ return -1;
+ }
+
+ return acc->IndexInParent();
+}
+
+- (NSArray* _Nullable)accessibilityElements {
+ NSMutableArray* children = [[[NSMutableArray alloc] init] autorelease];
+ uint32_t childCount = mGeckoAccessible->ChildCount();
+ for (uint32_t i = 0; i < childCount; i++) {
+ if (MUIAccessible* child =
+ GetNativeFromGeckoAccessible(mGeckoAccessible->ChildAt(i))) {
+ [children addObject:child];
+ }
+ }
+
+ return children;
+}
+
+- (UIAccessibilityContainerType)accessibilityContainerType {
+ return UIAccessibilityContainerTypeNone;
+}
+
+- (NSRange)_accessibilitySelectedTextRange {
+ if (!mGeckoAccessible || !mGeckoAccessible->IsHyperText()) {
+ return NSMakeRange(NSNotFound, 0);
+ }
+ // XXX This will only work in simple plain text boxes. It will break horribly
+ // if there are any embedded objects. Also, it only supports caret, not
+ // selection.
+ int32_t caret = mGeckoAccessible->AsHyperTextBase()->CaretOffset();
+ if (caret != -1) {
+ return NSMakeRange(caret, 0);
+ }
+ return NSMakeRange(NSNotFound, 0);
+}
+
+- (void)_accessibilitySetSelectedTextRange:(NSRange)range {
+ if (!mGeckoAccessible || !mGeckoAccessible->IsHyperText()) {
+ return;
+ }
+ // XXX This will only work in simple plain text boxes. It will break horribly
+ // if there are any embedded objects. Also, it only supports caret, not
+ // selection.
+ mGeckoAccessible->AsHyperTextBase()->SetCaretOffset(range.location);
+}
+
+@end