diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:13:33 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:13:33 +0000 |
commit | 086c044dc34dfc0f74fbe41f4ecb402b2cd34884 (patch) | |
tree | a4f824bd33cb075dd5aa3eb5a0a94af221bbe83a /accessible/ios/MUIAccessible.mm | |
parent | Adding debian version 124.0.1-1. (diff) | |
download | firefox-086c044dc34dfc0f74fbe41f4ecb402b2cd34884.tar.xz firefox-086c044dc34dfc0f74fbe41f4ecb402b2cd34884.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.mm | 497 |
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 |