diff options
Diffstat (limited to '')
55 files changed, 9958 insertions, 0 deletions
diff --git a/accessible/mac/.clang-format b/accessible/mac/.clang-format new file mode 100644 index 0000000000..269bce4d0f --- /dev/null +++ b/accessible/mac/.clang-format @@ -0,0 +1,11 @@ +--- +# Objective C formatting rules. +# Since this doesn't derive from the Cpp section, we need to redifine the root rules here. +Language: ObjC +BasedOnStyle: Google + +DerivePointerAlignment: false +PointerAlignment: Left +SortIncludes: false +ColumnLimit: 80 +IndentPPDirectives: AfterHash diff --git a/accessible/mac/ARIAGridAccessibleWrap.h b/accessible/mac/ARIAGridAccessibleWrap.h new file mode 100644 index 0000000000..f6380f2b2e --- /dev/null +++ b/accessible/mac/ARIAGridAccessibleWrap.h @@ -0,0 +1,22 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=2: + */ +/* 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/. */ + +#ifndef MOZILLA_A11Y_ARIAGRIDACCESSIBLEWRAP_H +#define MOZILLA_A11Y_ARIAGRIDACCESSIBLEWRAP_H + +#include "ARIAGridAccessible.h" + +namespace mozilla { +namespace a11y { + +typedef class ARIAGridAccessible ARIAGridAccessibleWrap; +typedef class ARIAGridCellAccessible ARIAGridCellAccessibleWrap; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/mac/AccessibleWrap.h b/accessible/mac/AccessibleWrap.h new file mode 100644 index 0000000000..ecbead67dd --- /dev/null +++ b/accessible/mac/AccessibleWrap.h @@ -0,0 +1,94 @@ +/* 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/. */ + +/* For documentation of the accessibility architecture, + * see http://lxr.mozilla.org/seamonkey/source/accessible/accessible-docs.html + */ + +#ifndef _AccessibleWrap_H_ +#define _AccessibleWrap_H_ + +#include <objc/objc.h> + +#include "Accessible.h" +#include "PlatformExtTypes.h" +#include "States.h" + +#include "nsCOMPtr.h" + +#include "nsTArray.h" + +#if defined(__OBJC__) +@class mozAccessible; +#endif + +namespace mozilla { +namespace a11y { + +class AccessibleWrap : public Accessible { + public: // construction, destruction + AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc); + virtual ~AccessibleWrap(); + + /** + * Get the native Obj-C object (mozAccessible). + */ + virtual void GetNativeInterface(void** aOutAccessible) override; + + /** + * The objective-c |Class| type that this accessible's native object + * should be instantied with. used on runtime to determine the + * right type for this accessible's associated native object. + */ + virtual Class GetNativeType(); + + virtual void Shutdown() override; + + virtual nsresult HandleAccEvent(AccEvent* aEvent) override; + + bool ApplyPostFilter(const EWhichPostFilter& aSearchKey, + const nsString& aSearchText); + + protected: + friend class xpcAccessibleMacInterface; + + /** + * Get the native object. Create it if needed. + */ +#if defined(__OBJC__) + mozAccessible* GetNativeObject(); +#else + id GetNativeObject(); +#endif + + private: + /** + * Our native object. Private because its creation is done lazily. + * Don't access it directly. Ever. Unless you are GetNativeObject() or + * Shutdown() + */ +#if defined(__OBJC__) + // if we are in Objective-C, we use the actual Obj-C class. + mozAccessible* mNativeObject; +#else + id mNativeObject; +#endif + + /** + * We have created our native. This does not mean there is one. + * This can never go back to false. + * We need it because checking whether we need a native object cost time. + */ + bool mNativeInited; +}; + +Class GetTypeFromRole(roles::Role aRole); + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/mac/AccessibleWrap.mm b/accessible/mac/AccessibleWrap.mm new file mode 100644 index 0000000000..9774fbc3cc --- /dev/null +++ b/accessible/mac/AccessibleWrap.mm @@ -0,0 +1,400 @@ +/* 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/. */ + +#include "DocAccessibleWrap.h" +#include "nsObjCExceptions.h" +#include "nsCocoaUtils.h" + +#include "Accessible-inl.h" +#include "nsAccUtils.h" +#include "Role.h" +#include "TextRange.h" +#include "gfxPlatform.h" + +#import "MOXLandmarkAccessibles.h" +#import "MOXMathAccessibles.h" +#import "MOXTextMarkerDelegate.h" +#import "MOXWebAreaAccessible.h" +#import "mozAccessible.h" +#import "mozActionElements.h" +#import "mozHTMLAccessible.h" +#import "mozSelectableElements.h" +#import "mozTableAccessible.h" +#import "mozTextAccessible.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +AccessibleWrap::AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc) + : Accessible(aContent, aDoc), mNativeObject(nil), mNativeInited(false) { + if (aContent && aContent->IsElement() && aDoc) { + // Check if this accessible is a live region and queue it + // it for dispatching an event after it has been inserted. + DocAccessibleWrap* doc = static_cast<DocAccessibleWrap*>(aDoc); + static const dom::Element::AttrValuesArray sLiveRegionValues[] = { + nsGkAtoms::OFF, nsGkAtoms::polite, nsGkAtoms::assertive, nullptr}; + int32_t attrValue = aContent->AsElement()->FindAttrValueIn( + kNameSpaceID_None, nsGkAtoms::aria_live, sLiveRegionValues, + eIgnoreCase); + if (attrValue == 0) { + // aria-live is "off", do nothing. + } else if (attrValue > 0) { + // aria-live attribute is polite or assertive. It's live! + doc->QueueNewLiveRegion(this); + } else if (const nsRoleMapEntry* roleMap = + aria::GetRoleMap(aContent->AsElement())) { + // aria role defines it as a live region. It's live! + if (roleMap->liveAttRule == ePoliteLiveAttr || + roleMap->liveAttRule == eAssertiveLiveAttr) { + doc->QueueNewLiveRegion(this); + } + } else if (nsStaticAtom* value = GetAccService()->MarkupAttribute( + aContent, nsGkAtoms::live)) { + // HTML element defines it as a live region. It's live! + if (value == nsGkAtoms::polite || value == nsGkAtoms::assertive) { + doc->QueueNewLiveRegion(this); + } + } + } +} + +AccessibleWrap::~AccessibleWrap() {} + +mozAccessible* AccessibleWrap::GetNativeObject() { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + if (!mNativeInited && !mNativeObject) { + // We don't creat OSX accessibles for xul tooltips, defunct accessibles, + // <br> (whitespace) elements, or pruned children. + // + // To maintain a scripting environment where the XPCOM accessible hierarchy + // look the same on all platforms, we still let the C++ objects be created + // though. + if (!IsXULTooltip() && !IsDefunct() && Role() != roles::WHITESPACE) { + mNativeObject = [[GetNativeType() alloc] initWithAccessible:this]; + } + } + + mNativeInited = true; + + return mNativeObject; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +void AccessibleWrap::GetNativeInterface(void** aOutInterface) { + *aOutInterface = static_cast<void*>(GetNativeObject()); +} + +// overridden in subclasses to create the right kind of object. by default we +// create a generic 'mozAccessible' node. +Class AccessibleWrap::GetNativeType() { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + if (IsXULTabpanels()) { + return [mozPaneAccessible class]; + } + + if (IsTable()) { + return [mozTableAccessible class]; + } + + if (IsTableRow()) { + return [mozTableRowAccessible class]; + } + + if (IsTableCell()) { + return [mozTableCellAccessible class]; + } + + if (IsDoc()) { + return [MOXWebAreaAccessible class]; + } + + return GetTypeFromRole(Role()); + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +// this method is very important. it is fired when an accessible object "dies". +// after this point the object might still be around (because some 3rd party +// still has a ref to it), but it is in fact 'dead'. +void AccessibleWrap::Shutdown() { + // this ensure we will not try to re-create the native object. + mNativeInited = true; + + // we really intend to access the member directly. + if (mNativeObject) { + [mNativeObject expire]; + [mNativeObject release]; + mNativeObject = nil; + } + + Accessible::Shutdown(); +} + +nsresult AccessibleWrap::HandleAccEvent(AccEvent* aEvent) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + nsresult rv = Accessible::HandleAccEvent(aEvent); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t eventType = aEvent->GetEventType(); + + if (eventType == nsIAccessibleEvent::EVENT_SHOW) { + DocAccessibleWrap* doc = static_cast<DocAccessibleWrap*>(Document()); + doc->ProcessNewLiveRegions(); + } + + if (IPCAccessibilityActive()) { + return NS_OK; + } + + Accessible* eventTarget = nullptr; + + switch (eventType) { + case nsIAccessibleEvent::EVENT_SELECTION: + case nsIAccessibleEvent::EVENT_SELECTION_ADD: + case nsIAccessibleEvent::EVENT_SELECTION_REMOVE: { + AccSelChangeEvent* selEvent = downcast_accEvent(aEvent); + // The "widget" is the selected widget's container. In OSX + // it is the target of the selection changed event. + eventTarget = selEvent->Widget(); + break; + } + case nsIAccessibleEvent::EVENT_TEXT_INSERTED: + case nsIAccessibleEvent::EVENT_TEXT_REMOVED: { + Accessible* acc = aEvent->GetAccessible(); + // If there is a text input ancestor, use it as the event source. + while (acc && GetTypeFromRole(acc->Role()) != [mozTextAccessible class]) { + acc = acc->Parent(); + } + eventTarget = acc ? acc : aEvent->GetAccessible(); + break; + } + default: + eventTarget = aEvent->GetAccessible(); + break; + } + + mozAccessible* nativeAcc = nil; + eventTarget->GetNativeInterface((void**)&nativeAcc); + if (!nativeAcc) { + return NS_ERROR_FAILURE; + } + + switch (eventType) { + case nsIAccessibleEvent::EVENT_STATE_CHANGE: { + AccStateChangeEvent* event = downcast_accEvent(aEvent); + [nativeAcc stateChanged:event->GetState() + isEnabled:event->IsStateEnabled()]; + break; + } + + case nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED: { + MOXTextMarkerDelegate* delegate = + [MOXTextMarkerDelegate getOrCreateForDoc:aEvent->Document()]; + AccTextSelChangeEvent* event = downcast_accEvent(aEvent); + AutoTArray<TextRange, 1> ranges; + event->SelectionRanges(&ranges); + + if (ranges.Length()) { + // Cache selection in delegate. + [delegate setSelectionFrom:ranges[0].StartContainer() + at:ranges[0].StartOffset() + to:ranges[0].EndContainer() + at:ranges[0].EndOffset()]; + } + + [nativeAcc handleAccessibleEvent:eventType]; + break; + } + + case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: { + AccCaretMoveEvent* event = downcast_accEvent(aEvent); + int32_t caretOffset = event->GetCaretOffset(); + MOXTextMarkerDelegate* delegate = + [MOXTextMarkerDelegate getOrCreateForDoc:aEvent->Document()]; + [delegate setCaretOffset:eventTarget at:caretOffset]; + if (event->IsSelectionCollapsed()) { + // If the selection is collapsed, invalidate our text selection cache. + [delegate setSelectionFrom:eventTarget + at:caretOffset + to:eventTarget + at:caretOffset]; + } + + if (mozTextAccessible* textAcc = static_cast<mozTextAccessible*>( + [nativeAcc moxEditableAncestor])) { + [textAcc + handleAccessibleEvent:nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED]; + } else { + [nativeAcc + handleAccessibleEvent:nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED]; + } + break; + } + + case nsIAccessibleEvent::EVENT_TEXT_INSERTED: + case nsIAccessibleEvent::EVENT_TEXT_REMOVED: { + AccTextChangeEvent* tcEvent = downcast_accEvent(aEvent); + [nativeAcc handleAccessibleTextChangeEvent:nsCocoaUtils::ToNSString( + tcEvent->ModifiedText()) + inserted:tcEvent->IsTextInserted() + inContainer:aEvent->GetAccessible() + at:tcEvent->GetStartOffset()]; + break; + } + + case nsIAccessibleEvent::EVENT_FOCUS: + case nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE: + case nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE: + case nsIAccessibleEvent::EVENT_MENUPOPUP_START: + case nsIAccessibleEvent::EVENT_MENUPOPUP_END: + case nsIAccessibleEvent::EVENT_REORDER: + case nsIAccessibleEvent::EVENT_SELECTION: + case nsIAccessibleEvent::EVENT_SELECTION_ADD: + case nsIAccessibleEvent::EVENT_SELECTION_REMOVE: + case nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED: + case nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED: + case nsIAccessibleEvent::EVENT_NAME_CHANGE: + [nativeAcc handleAccessibleEvent:eventType]; + break; + + default: + break; + } + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +bool AccessibleWrap::ApplyPostFilter(const EWhichPostFilter& aSearchKey, + const nsString& aSearchText) { + // We currently only support the eContainsText post filter. + MOZ_ASSERT(aSearchKey == EWhichPostFilter::eContainsText, + "Only search text supported"); + nsAutoString name; + Name(name); + return name.Find(aSearchText, true) != kNotFound; +} + +//////////////////////////////////////////////////////////////////////////////// +// AccessibleWrap protected + +Class a11y::GetTypeFromRole(roles::Role aRole) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + switch (aRole) { + case roles::COMBOBOX: + return [mozPopupButtonAccessible class]; + + case roles::PUSHBUTTON: + return [mozButtonAccessible class]; + + case roles::PAGETAB: + return [mozTabAccessible class]; + + case roles::CHECKBUTTON: + case roles::TOGGLE_BUTTON: + return [mozCheckboxAccessible class]; + + case roles::RADIOBUTTON: + return [mozRadioButtonAccessible class]; + + case roles::SPINBUTTON: + case roles::SLIDER: + return [mozIncrementableAccessible class]; + + case roles::HEADING: + return [mozHeadingAccessible class]; + + case roles::PAGETABLIST: + return [mozTabGroupAccessible class]; + + case roles::ENTRY: + case roles::CAPTION: + case roles::ACCEL_LABEL: + case roles::EDITCOMBOBOX: + case roles::PASSWORD_TEXT: + // normal textfield (static or editable) + return [mozTextAccessible class]; + + case roles::TEXT_LEAF: + case roles::STATICTEXT: + return [mozTextLeafAccessible class]; + + case roles::LANDMARK: + return [MOXLandmarkAccessible class]; + + case roles::LINK: + return [mozLinkAccessible class]; + + case roles::LISTBOX: + return [mozListboxAccessible class]; + + case roles::LISTITEM: + return [MOXListItemAccessible class]; + + case roles::OPTION: { + return [mozOptionAccessible class]; + } + + case roles::RICH_OPTION: { + return [mozSelectableChildAccessible class]; + } + + case roles::COMBOBOX_LIST: + case roles::MENUBAR: + case roles::MENUPOPUP: { + return [mozMenuAccessible class]; + } + + case roles::COMBOBOX_OPTION: + case roles::PARENT_MENUITEM: + case roles::MENUITEM: { + return [mozMenuItemAccessible class]; + } + + case roles::MATHML_ROOT: + return [MOXMathRootAccessible class]; + + case roles::MATHML_SQUARE_ROOT: + return [MOXMathSquareRootAccessible class]; + + case roles::MATHML_FRACTION: + return [MOXMathFractionAccessible class]; + + case roles::MATHML_SUB: + case roles::MATHML_SUP: + case roles::MATHML_SUB_SUP: + return [MOXMathSubSupAccessible class]; + + case roles::MATHML_UNDER: + case roles::MATHML_OVER: + case roles::MATHML_UNDER_OVER: + return [MOXMathUnderOverAccessible class]; + + case roles::SUMMARY: + return [MOXSummaryAccessible class]; + + case roles::OUTLINE: + case roles::TREE_TABLE: + return [mozOutlineAccessible class]; + + case roles::OUTLINEITEM: + return [mozOutlineRowAccessible class]; + + default: + return [mozAccessible class]; + } + + return nil; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} diff --git a/accessible/mac/ApplicationAccessibleWrap.h b/accessible/mac/ApplicationAccessibleWrap.h new file mode 100644 index 0000000000..a4b2fd70c7 --- /dev/null +++ b/accessible/mac/ApplicationAccessibleWrap.h @@ -0,0 +1,21 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=4:tabstop=4: + */ +/* 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/. */ + +#ifndef mozilla_a11y_ApplicationAccessibleWrap_h__ +#define mozilla_a11y_ApplicationAccessibleWrap_h__ + +#include "ApplicationAccessible.h" + +namespace mozilla { +namespace a11y { + +typedef ApplicationAccessible ApplicationAccessibleWrap; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/mac/DocAccessibleWrap.h b/accessible/mac/DocAccessibleWrap.h new file mode 100644 index 0000000000..b46fa9189a --- /dev/null +++ b/accessible/mac/DocAccessibleWrap.h @@ -0,0 +1,45 @@ +/* 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/. */ + +#ifndef mozilla_a11y_DocAccessibleWrap_h__ +#define mozilla_a11y_DocAccessibleWrap_h__ + +#include "DocAccessible.h" + +namespace mozilla { + +class PresShell; + +namespace a11y { + +class DocAccessibleWrap : public DocAccessible { + public: + DocAccessibleWrap(dom::Document* aDocument, PresShell* aPresShell); + + virtual ~DocAccessibleWrap(); + + virtual void Shutdown() override; + + virtual void AttributeChanged(dom::Element* aElement, int32_t aNameSpaceID, + nsAtom* aAttribute, int32_t aModType, + const nsAttrValue* aOldValue) override; + + void QueueNewLiveRegion(Accessible* aAccessible); + + void ProcessNewLiveRegions(); + + protected: + virtual void DoInitialUpdate() override; + + private: + nsTHashtable<nsVoidPtrHashKey> mNewLiveRegions; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/mac/DocAccessibleWrap.mm b/accessible/mac/DocAccessibleWrap.mm new file mode 100644 index 0000000000..b00f48a284 --- /dev/null +++ b/accessible/mac/DocAccessibleWrap.mm @@ -0,0 +1,104 @@ +/* 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/. */ + +#include "DocAccessibleWrap.h" +#include "DocAccessible-inl.h" + +#import "mozAccessible.h" +#import "MOXTextMarkerDelegate.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +DocAccessibleWrap::DocAccessibleWrap(dom::Document* aDocument, + PresShell* aPresShell) + : DocAccessible(aDocument, aPresShell) {} + +void DocAccessibleWrap::Shutdown() { + [MOXTextMarkerDelegate destroyForDoc:this]; + DocAccessible::Shutdown(); +} + +DocAccessibleWrap::~DocAccessibleWrap() {} + +void DocAccessibleWrap::AttributeChanged(dom::Element* aElement, + int32_t aNameSpaceID, + nsAtom* aAttribute, int32_t aModType, + const nsAttrValue* aOldValue) { + DocAccessible::AttributeChanged(aElement, aNameSpaceID, aAttribute, aModType, + aOldValue); + if (aAttribute == nsGkAtoms::aria_live) { + Accessible* accessible = + mContent != aElement ? GetAccessible(aElement) : this; + if (!accessible) { + return; + } + + static const dom::Element::AttrValuesArray sLiveRegionValues[] = { + nsGkAtoms::OFF, nsGkAtoms::polite, nsGkAtoms::assertive, nullptr}; + int32_t attrValue = + aElement->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::aria_live, + sLiveRegionValues, eIgnoreCase); + if (attrValue > 0) { + if (!aOldValue || aOldValue->IsEmptyString() || + aOldValue->Equals(nsGkAtoms::OFF, eIgnoreCase)) { + // This element just got an active aria-live attribute value + FireDelayedEvent(nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED, + accessible); + } + } else { + if (aOldValue && (aOldValue->Equals(nsGkAtoms::polite, eIgnoreCase) || + aOldValue->Equals(nsGkAtoms::assertive, eIgnoreCase))) { + // This element lost an active live region + FireDelayedEvent(nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED, + accessible); + } else if (attrValue == 0) { + // aria-live="off", check if its a role-based live region that + // needs to be removed. + if (const nsRoleMapEntry* roleMap = accessible->ARIARoleMap()) { + // aria role defines it as a live region. It's live! + if (roleMap->liveAttRule == ePoliteLiveAttr || + roleMap->liveAttRule == eAssertiveLiveAttr) { + FireDelayedEvent(nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED, + accessible); + } + } else if (nsStaticAtom* value = GetAccService()->MarkupAttribute( + aElement, nsGkAtoms::live)) { + // HTML element defines it as a live region. It's live! + if (value == nsGkAtoms::polite || value == nsGkAtoms::assertive) { + FireDelayedEvent(nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED, + accessible); + } + } + } + } + } +} + +void DocAccessibleWrap::QueueNewLiveRegion(Accessible* aAccessible) { + if (!aAccessible) { + return; + } + + mNewLiveRegions.PutEntry(aAccessible->UniqueID()); +} + +void DocAccessibleWrap::ProcessNewLiveRegions() { + for (auto iter = mNewLiveRegions.Iter(); !iter.Done(); iter.Next()) { + if (Accessible* liveRegion = + GetAccessibleByUniqueID(const_cast<void*>(iter.Get()->GetKey()))) { + FireDelayedEvent(nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED, liveRegion); + } + } + + mNewLiveRegions.Clear(); +} + +void DocAccessibleWrap::DoInitialUpdate() { + DocAccessible::DoInitialUpdate(); + ProcessNewLiveRegions(); +} diff --git a/accessible/mac/GeckoTextMarker.h b/accessible/mac/GeckoTextMarker.h new file mode 100644 index 0000000000..4818e98138 --- /dev/null +++ b/accessible/mac/GeckoTextMarker.h @@ -0,0 +1,122 @@ +/* clang-format off */ +/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* clang-format on */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef _GeckoTextMarker_H_ +#define _GeckoTextMarker_H_ + +typedef CFTypeRef AXTextMarkerRef; +typedef CFTypeRef AXTextMarkerRangeRef; + +namespace mozilla { +namespace a11y { + +class AccessibleOrProxy; +class GeckoTextMarkerRange; + +class GeckoTextMarker final { + public: + GeckoTextMarker(const AccessibleOrProxy& aContainer, int32_t aOffset) + : mContainer(aContainer), mOffset(aOffset) {} + + GeckoTextMarker(const GeckoTextMarker& aPoint) + : mContainer(aPoint.mContainer), mOffset(aPoint.mOffset) {} + + GeckoTextMarker(AccessibleOrProxy aDoc, AXTextMarkerRef aTextMarker); + + GeckoTextMarker() : mContainer(nullptr), mOffset(0) {} + + static GeckoTextMarker MarkerFromIndex(const AccessibleOrProxy& aRoot, + int32_t aIndex); + + id CreateAXTextMarker(); + + bool Next(); + + bool Previous(); + + // Return a range with the given type relative to this marker. + GeckoTextMarkerRange Range(EWhichRange aRangeType); + + AccessibleOrProxy Leaf(); + + bool IsValid() const { return !mContainer.IsNull(); }; + + bool operator<(const GeckoTextMarker& aPoint) const; + + bool operator==(const GeckoTextMarker& aPoint) const { + return mContainer == aPoint.mContainer && mOffset == aPoint.mOffset; + } + + AccessibleOrProxy mContainer; + int32_t mOffset; + + HyperTextAccessibleWrap* ContainerAsHyperTextWrap() const { + return mContainer.IsAccessible() + ? static_cast<HyperTextAccessibleWrap*>( + mContainer.AsAccessible()->AsHyperText()) + : nullptr; + } + + private: + bool IsEditableRoot(); +}; + +class GeckoTextMarkerRange final { + public: + GeckoTextMarkerRange(const GeckoTextMarker& aStart, + const GeckoTextMarker& aEnd) + : mStart(aStart), mEnd(aEnd) {} + + GeckoTextMarkerRange() {} + + GeckoTextMarkerRange(AccessibleOrProxy aDoc, + AXTextMarkerRangeRef aTextMarkerRange); + + explicit GeckoTextMarkerRange(const AccessibleOrProxy& aAccessible); + + id CreateAXTextMarkerRange(); + + bool IsValid() const { + return !mStart.mContainer.IsNull() && !mEnd.mContainer.IsNull(); + }; + + /** + * Return text enclosed by the range. + */ + NSString* Text() const; + + /** + * Return length of characters enclosed by the range. + */ + int32_t Length() const; + + /** + * Return screen bounds of range. + */ + NSValue* Bounds() const; + + /** + * Set the current range as the DOM selection. + */ + MOZ_CAN_RUN_SCRIPT_BOUNDARY void Select() const; + + /** + * Crops the range if it overlaps the given accessible element boundaries. + * Return true if successfully cropped. false if the range does not intersect + * with the container. + */ + bool Crop(const AccessibleOrProxy& aContainer); + + GeckoTextMarker mStart; + GeckoTextMarker mEnd; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/mac/GeckoTextMarker.mm b/accessible/mac/GeckoTextMarker.mm new file mode 100644 index 0000000000..0481eae83b --- /dev/null +++ b/accessible/mac/GeckoTextMarker.mm @@ -0,0 +1,481 @@ +/* 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/. */ + +#include "DocAccessibleParent.h" +#include "AccessibleOrProxy.h" +#include "nsCocoaUtils.h" + +#include "mozilla/a11y/DocAccessiblePlatformExtParent.h" + +#import "GeckoTextMarker.h" + +extern "C" { + +CFTypeID AXTextMarkerGetTypeID(); + +AXTextMarkerRef AXTextMarkerCreate(CFAllocatorRef allocator, const UInt8* bytes, + CFIndex length); + +const UInt8* AXTextMarkerGetBytePtr(AXTextMarkerRef text_marker); + +size_t AXTextMarkerGetLength(AXTextMarkerRef text_marker); + +CFTypeID AXTextMarkerRangeGetTypeID(); + +AXTextMarkerRangeRef AXTextMarkerRangeCreate(CFAllocatorRef allocator, + AXTextMarkerRef start_marker, + AXTextMarkerRef end_marker); + +AXTextMarkerRef AXTextMarkerRangeCopyStartMarker( + AXTextMarkerRangeRef text_marker_range); + +AXTextMarkerRef AXTextMarkerRangeCopyEndMarker( + AXTextMarkerRangeRef text_marker_range); +} + +namespace mozilla { +namespace a11y { + +struct OpaqueGeckoTextMarker { + OpaqueGeckoTextMarker(uintptr_t aDoc, uintptr_t aID, int32_t aOffset) + : mDoc(aDoc), mID(aID), mOffset(aOffset) {} + OpaqueGeckoTextMarker() {} + uintptr_t mDoc; + uintptr_t mID; + int32_t mOffset; +}; + +static bool DocumentExists(AccessibleOrProxy aDoc, uintptr_t aDocPtr) { + if (aDoc.Bits() == aDocPtr) { + return true; + } + + if (aDoc.IsAccessible()) { + DocAccessible* docAcc = aDoc.AsAccessible()->AsDoc(); + uint32_t docCount = docAcc->ChildDocumentCount(); + for (uint32_t i = 0; i < docCount; i++) { + if (DocumentExists(docAcc->GetChildDocumentAt(i), aDocPtr)) { + return true; + } + } + } else { + DocAccessibleParent* docProxy = aDoc.AsProxy()->AsDoc(); + size_t docCount = docProxy->ChildDocCount(); + for (uint32_t i = 0; i < docCount; i++) { + if (DocumentExists(docProxy->ChildDocAt(i), aDocPtr)) { + return true; + } + } + } + + return false; +} + +// GeckoTextMarker + +GeckoTextMarker::GeckoTextMarker(AccessibleOrProxy aDoc, + AXTextMarkerRef aTextMarker) { + MOZ_ASSERT(!aDoc.IsNull()); + OpaqueGeckoTextMarker opaqueMarker; + if (aTextMarker && + AXTextMarkerGetLength(aTextMarker) == sizeof(OpaqueGeckoTextMarker)) { + memcpy(&opaqueMarker, AXTextMarkerGetBytePtr(aTextMarker), + sizeof(OpaqueGeckoTextMarker)); + if (DocumentExists(aDoc, opaqueMarker.mDoc)) { + AccessibleOrProxy doc; + doc.SetBits(opaqueMarker.mDoc); + if (doc.IsProxy()) { + mContainer = doc.AsProxy()->AsDoc()->GetAccessible(opaqueMarker.mID); + } else { + mContainer = doc.AsAccessible()->AsDoc()->GetAccessibleByUniqueID( + reinterpret_cast<void*>(opaqueMarker.mID)); + } + } + + mOffset = opaqueMarker.mOffset; + } +} + +GeckoTextMarker GeckoTextMarker::MarkerFromIndex(const AccessibleOrProxy& aRoot, + int32_t aIndex) { + if (aRoot.IsProxy()) { + int32_t offset = 0; + uint64_t containerID = 0; + DocAccessibleParent* ipcDoc = aRoot.AsProxy()->Document(); + Unused << ipcDoc->GetPlatformExtension()->SendOffsetAtIndex( + aRoot.AsProxy()->ID(), aIndex, &containerID, &offset); + ProxyAccessible* container = ipcDoc->GetAccessible(containerID); + return GeckoTextMarker(container, offset); + } else if (auto htWrap = static_cast<HyperTextAccessibleWrap*>( + aRoot.AsAccessible()->AsHyperText())) { + int32_t offset = 0; + HyperTextAccessible* container = nullptr; + htWrap->OffsetAtIndex(aIndex, &container, &offset); + return GeckoTextMarker(container, offset); + } + + return GeckoTextMarker(); +} + +id GeckoTextMarker::CreateAXTextMarker() { + if (!IsValid()) { + return nil; + } + + AccessibleOrProxy doc; + if (mContainer.IsProxy()) { + doc = mContainer.AsProxy()->Document(); + } else { + doc = mContainer.AsAccessible()->Document(); + } + + uintptr_t identifier = + mContainer.IsProxy() + ? mContainer.AsProxy()->ID() + : reinterpret_cast<uintptr_t>(mContainer.AsAccessible()->UniqueID()); + + OpaqueGeckoTextMarker opaqueMarker(doc.Bits(), identifier, mOffset); + AXTextMarkerRef cf_text_marker = AXTextMarkerCreate( + kCFAllocatorDefault, reinterpret_cast<const UInt8*>(&opaqueMarker), + sizeof(OpaqueGeckoTextMarker)); + + return [static_cast<id>(cf_text_marker) autorelease]; +} + +bool GeckoTextMarker::operator<(const GeckoTextMarker& aPoint) const { + if (mContainer == aPoint.mContainer) return mOffset < aPoint.mOffset; + + // Build the chain of parents + AutoTArray<AccessibleOrProxy, 30> parents1, parents2; + AccessibleOrProxy p1 = mContainer; + while (!p1.IsNull()) { + parents1.AppendElement(p1); + p1 = p1.Parent(); + } + + AccessibleOrProxy p2 = aPoint.mContainer; + while (!p2.IsNull()) { + parents2.AppendElement(p2); + p2 = p2.Parent(); + } + + // An empty chain of parents means one of the containers was null. + MOZ_ASSERT(parents1.Length() != 0 && parents2.Length() != 0, + "have empty chain of parents!"); + + // Find where the parent chain differs + uint32_t pos1 = parents1.Length(), pos2 = parents2.Length(); + for (uint32_t len = std::min(pos1, pos2); len > 0; --len) { + AccessibleOrProxy child1 = parents1.ElementAt(--pos1); + AccessibleOrProxy child2 = parents2.ElementAt(--pos2); + if (child1 != child2) { + return child1.IndexInParent() < child2.IndexInParent(); + } + } + + if (pos1 != 0) { + // If parents1 is a superset of parents2 then mContainer is a + // descendant of aPoint.mContainer. The next element down in parents1 + // is mContainer's ancestor that is the child of aPoint.mContainer. + // We compare its end offset in aPoint.mContainer with aPoint.mOffset. + AccessibleOrProxy child = parents1.ElementAt(pos1 - 1); + MOZ_ASSERT(child.Parent() == aPoint.mContainer); + bool unused; + uint32_t endOffset = child.IsProxy() ? child.AsProxy()->EndOffset(&unused) + : child.AsAccessible()->EndOffset(); + return endOffset < static_cast<uint32_t>(aPoint.mOffset); + } + + if (pos2 != 0) { + // If parents2 is a superset of parents1 then aPoint.mContainer is a + // descendant of mContainer. The next element down in parents2 + // is aPoint.mContainer's ancestor that is the child of mContainer. + // We compare its start offset in mContainer with mOffset. + AccessibleOrProxy child = parents2.ElementAt(pos2 - 1); + MOZ_ASSERT(child.Parent() == mContainer); + bool unused; + uint32_t startOffset = child.IsProxy() + ? child.AsProxy()->StartOffset(&unused) + : child.AsAccessible()->StartOffset(); + return static_cast<uint32_t>(mOffset) <= startOffset; + } + + MOZ_ASSERT_UNREACHABLE("Broken tree?!"); + return false; +} + +bool GeckoTextMarker::IsEditableRoot() { + uint64_t state = mContainer.IsProxy() ? mContainer.AsProxy()->State() + : mContainer.AsAccessible()->State(); + if ((state & states::EDITABLE) == 0) { + return false; + } + + AccessibleOrProxy parent = mContainer.Parent(); + if (parent.IsNull()) { + // Not sure when this can happen, but it would technically be an editable + // root. + return true; + } + + state = parent.IsProxy() ? parent.AsProxy()->State() + : parent.AsAccessible()->State(); + + return (state & states::EDITABLE) == 0; +} + +bool GeckoTextMarker::Next() { + if (mContainer.IsProxy()) { + int32_t nextOffset = 0; + uint64_t nextContainerID = 0; + DocAccessibleParent* ipcDoc = mContainer.AsProxy()->Document(); + Unused << ipcDoc->GetPlatformExtension()->SendNextClusterAt( + mContainer.AsProxy()->ID(), mOffset, &nextContainerID, &nextOffset); + ProxyAccessible* nextContainer = ipcDoc->GetAccessible(nextContainerID); + bool moved = nextContainer != mContainer.AsProxy() || nextOffset != mOffset; + mContainer = nextContainer; + mOffset = nextOffset; + return moved; + } else if (auto htWrap = ContainerAsHyperTextWrap()) { + HyperTextAccessible* nextContainer = nullptr; + int32_t nextOffset = 0; + htWrap->NextClusterAt(mOffset, &nextContainer, &nextOffset); + bool moved = nextContainer != htWrap || nextOffset != mOffset; + mContainer = nextContainer; + mOffset = nextOffset; + return moved; + } + + return false; +} + +bool GeckoTextMarker::Previous() { + if (mContainer.IsProxy()) { + int32_t prevOffset = 0; + uint64_t prevContainerID = 0; + DocAccessibleParent* ipcDoc = mContainer.AsProxy()->Document(); + Unused << ipcDoc->GetPlatformExtension()->SendPreviousClusterAt( + mContainer.AsProxy()->ID(), mOffset, &prevContainerID, &prevOffset); + ProxyAccessible* prevContainer = ipcDoc->GetAccessible(prevContainerID); + bool moved = prevContainer != mContainer.AsProxy() || prevOffset != mOffset; + mContainer = prevContainer; + mOffset = prevOffset; + return moved; + } else if (auto htWrap = ContainerAsHyperTextWrap()) { + HyperTextAccessible* prevContainer = nullptr; + int32_t prevOffset = 0; + htWrap->PreviousClusterAt(mOffset, &prevContainer, &prevOffset); + bool moved = prevContainer != htWrap || prevOffset != mOffset; + mContainer = prevContainer; + mOffset = prevOffset; + return moved; + } + + return false; +} + +static uint32_t CharacterCount(const AccessibleOrProxy& aContainer) { + if (aContainer.IsProxy()) { + return aContainer.AsProxy()->CharacterCount(); + } + + if (aContainer.AsAccessible()->IsHyperText()) { + return aContainer.AsAccessible()->AsHyperText()->CharacterCount(); + } + + return 0; +} + +GeckoTextMarkerRange GeckoTextMarker::Range(EWhichRange aRangeType) { + MOZ_ASSERT(!mContainer.IsNull()); + if (mContainer.IsProxy()) { + int32_t startOffset = 0, endOffset = 0; + uint64_t startContainerID = 0, endContainerID = 0; + DocAccessibleParent* ipcDoc = mContainer.AsProxy()->Document(); + bool success = ipcDoc->GetPlatformExtension()->SendRangeAt( + mContainer.AsProxy()->ID(), mOffset, aRangeType, &startContainerID, + &startOffset, &endContainerID, &endOffset); + if (success) { + return GeckoTextMarkerRange( + GeckoTextMarker(ipcDoc->GetAccessible(startContainerID), startOffset), + GeckoTextMarker(ipcDoc->GetAccessible(endContainerID), endOffset)); + } + } else if (auto htWrap = ContainerAsHyperTextWrap()) { + int32_t startOffset = 0, endOffset = 0; + HyperTextAccessible* startContainer = nullptr; + HyperTextAccessible* endContainer = nullptr; + htWrap->RangeAt(mOffset, aRangeType, &startContainer, &startOffset, + &endContainer, &endOffset); + return GeckoTextMarkerRange(GeckoTextMarker(startContainer, startOffset), + GeckoTextMarker(endContainer, endOffset)); + } + + return GeckoTextMarkerRange(GeckoTextMarker(), GeckoTextMarker()); +} + +AccessibleOrProxy GeckoTextMarker::Leaf() { + MOZ_ASSERT(!mContainer.IsNull()); + if (mContainer.IsProxy()) { + uint64_t leafID = 0; + DocAccessibleParent* ipcDoc = mContainer.AsProxy()->Document(); + Unused << ipcDoc->GetPlatformExtension()->SendLeafAtOffset( + mContainer.AsProxy()->ID(), mOffset, &leafID); + return ipcDoc->GetAccessible(leafID); + } else if (auto htWrap = ContainerAsHyperTextWrap()) { + return htWrap->LeafAtOffset(mOffset); + } + + return mContainer; +} + +// GeckoTextMarkerRange + +GeckoTextMarkerRange::GeckoTextMarkerRange( + AccessibleOrProxy aDoc, AXTextMarkerRangeRef aTextMarkerRange) { + if (!aTextMarkerRange || + CFGetTypeID(aTextMarkerRange) != AXTextMarkerRangeGetTypeID()) { + return; + } + + AXTextMarkerRef start_marker( + AXTextMarkerRangeCopyStartMarker(aTextMarkerRange)); + AXTextMarkerRef end_marker(AXTextMarkerRangeCopyEndMarker(aTextMarkerRange)); + + mStart = GeckoTextMarker(aDoc, start_marker); + mEnd = GeckoTextMarker(aDoc, end_marker); + + CFRelease(start_marker); + CFRelease(end_marker); +} + +GeckoTextMarkerRange::GeckoTextMarkerRange( + const AccessibleOrProxy& aAccessible) { + if ((aAccessible.IsAccessible() && + aAccessible.AsAccessible()->IsHyperText()) || + (aAccessible.IsProxy() && aAccessible.AsProxy()->mIsHyperText)) { + // The accessible is a hypertext. Initialize range to its inner text range. + mStart = GeckoTextMarker(aAccessible, 0); + mEnd = GeckoTextMarker(aAccessible, (CharacterCount(aAccessible))); + } else { + // The accessible is not a hypertext (maybe a text leaf?). Initialize range + // to its offsets in its container. + mStart = GeckoTextMarker(aAccessible.Parent(), 0); + mEnd = GeckoTextMarker(aAccessible.Parent(), 0); + if (mStart.mContainer.IsProxy()) { + DocAccessibleParent* ipcDoc = mStart.mContainer.AsProxy()->Document(); + Unused << ipcDoc->GetPlatformExtension()->SendRangeOfChild( + mStart.mContainer.AsProxy()->ID(), aAccessible.AsProxy()->ID(), + &mStart.mOffset, &mEnd.mOffset); + } else if (auto htWrap = mStart.ContainerAsHyperTextWrap()) { + htWrap->RangeOfChild(aAccessible.AsAccessible(), &mStart.mOffset, + &mEnd.mOffset); + } + } +} + +id GeckoTextMarkerRange::CreateAXTextMarkerRange() { + if (!IsValid()) { + return nil; + } + + AXTextMarkerRangeRef cf_text_marker_range = + AXTextMarkerRangeCreate(kCFAllocatorDefault, mStart.CreateAXTextMarker(), + mEnd.CreateAXTextMarker()); + return [static_cast<id>(cf_text_marker_range) autorelease]; +} + +NSString* GeckoTextMarkerRange::Text() const { + nsAutoString text; + if (mStart.mContainer.IsProxy() && mEnd.mContainer.IsProxy()) { + DocAccessibleParent* ipcDoc = mStart.mContainer.AsProxy()->Document(); + Unused << ipcDoc->GetPlatformExtension()->SendTextForRange( + mStart.mContainer.AsProxy()->ID(), mStart.mOffset, + mEnd.mContainer.AsProxy()->ID(), mEnd.mOffset, &text); + } else if (auto htWrap = mStart.ContainerAsHyperTextWrap()) { + htWrap->TextForRange(text, mStart.mOffset, mEnd.ContainerAsHyperTextWrap(), + mEnd.mOffset); + } + return nsCocoaUtils::ToNSString(text); +} + +int32_t GeckoTextMarkerRange::Length() const { + int32_t length = 0; + if (mStart.mContainer.IsProxy() && mEnd.mContainer.IsProxy()) { + DocAccessibleParent* ipcDoc = mStart.mContainer.AsProxy()->Document(); + Unused << ipcDoc->GetPlatformExtension()->SendLengthForRange( + mStart.mContainer.AsProxy()->ID(), mStart.mOffset, + mEnd.mContainer.AsProxy()->ID(), mEnd.mOffset, &length); + } else if (auto htWrap = mStart.ContainerAsHyperTextWrap()) { + length = htWrap->LengthForRange( + mStart.mOffset, mEnd.ContainerAsHyperTextWrap(), mEnd.mOffset); + } + + return length; +} + +NSValue* GeckoTextMarkerRange::Bounds() const { + nsIntRect rect; + if (mStart.mContainer.IsProxy() && mEnd.mContainer.IsProxy()) { + DocAccessibleParent* ipcDoc = mStart.mContainer.AsProxy()->Document(); + Unused << ipcDoc->GetPlatformExtension()->SendBoundsForRange( + mStart.mContainer.AsProxy()->ID(), mStart.mOffset, + mEnd.mContainer.AsProxy()->ID(), mEnd.mOffset, &rect); + } else if (auto htWrap = mStart.ContainerAsHyperTextWrap()) { + rect = htWrap->BoundsForRange( + mStart.mOffset, mEnd.ContainerAsHyperTextWrap(), mEnd.mOffset); + } + + NSScreen* mainView = [[NSScreen screens] objectAtIndex:0]; + CGFloat scaleFactor = nsCocoaUtils::GetBackingScaleFactor(mainView); + NSRect r = + NSMakeRect(static_cast<CGFloat>(rect.x) / scaleFactor, + [mainView frame].size.height - + static_cast<CGFloat>(rect.y + rect.height) / scaleFactor, + static_cast<CGFloat>(rect.width) / scaleFactor, + static_cast<CGFloat>(rect.height) / scaleFactor); + + return [NSValue valueWithRect:r]; +} + +void GeckoTextMarkerRange::Select() const { + if (mStart.mContainer.IsProxy() && mEnd.mContainer.IsProxy()) { + DocAccessibleParent* ipcDoc = mStart.mContainer.AsProxy()->Document(); + Unused << ipcDoc->GetPlatformExtension()->SendSelectRange( + mStart.mContainer.AsProxy()->ID(), mStart.mOffset, + mEnd.mContainer.AsProxy()->ID(), mEnd.mOffset); + } else if (RefPtr<HyperTextAccessibleWrap> htWrap = + mStart.ContainerAsHyperTextWrap()) { + RefPtr<HyperTextAccessibleWrap> end = mEnd.ContainerAsHyperTextWrap(); + htWrap->SelectRange(mStart.mOffset, end, mEnd.mOffset); + } +} + +bool GeckoTextMarkerRange::Crop(const AccessibleOrProxy& aContainer) { + GeckoTextMarker containerStart(aContainer, 0); + GeckoTextMarker containerEnd(aContainer, CharacterCount(aContainer)); + + if (mEnd < containerStart || containerEnd < mStart) { + // The range ends before the container, or starts after it. + return false; + } + + if (mStart < containerStart) { + // If range start is before container start, adjust range start to + // start of container. + mStart = containerStart; + } + + if (containerEnd < mEnd) { + // If range end is after container end, adjust range end to end of + // container. + mEnd = containerEnd; + } + + return true; +} +} +} diff --git a/accessible/mac/HTMLTableAccessibleWrap.h b/accessible/mac/HTMLTableAccessibleWrap.h new file mode 100644 index 0000000000..f62e626109 --- /dev/null +++ b/accessible/mac/HTMLTableAccessibleWrap.h @@ -0,0 +1,23 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=2: + */ +/* 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/. */ + +#ifndef mozilla_a11y_HTMLTableAccessibleWrap_h__ +#define mozilla_a11y_HTMLTableAccessibleWrap_h__ + +#include "HTMLTableAccessible.h" + +namespace mozilla { +namespace a11y { + +typedef class HTMLTableAccessible HTMLTableAccessibleWrap; +typedef class HTMLTableCellAccessible HTMLTableCellAccessibleWrap; +typedef class HTMLTableHeaderCellAccessible HTMLTableHeaderCellAccessibleWrap; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/mac/HyperTextAccessibleWrap.h b/accessible/mac/HyperTextAccessibleWrap.h new file mode 100644 index 0000000000..1c25e8a4e6 --- /dev/null +++ b/accessible/mac/HyperTextAccessibleWrap.h @@ -0,0 +1,89 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozilla_a11y_HyperTextAccessibleWrap_h__ +#define mozilla_a11y_HyperTextAccessibleWrap_h__ + +#include "HyperTextAccessible.h" +#include "PlatformExtTypes.h" + +namespace mozilla { +namespace a11y { + +struct TextPoint; + +class HyperTextAccessibleWrap : public HyperTextAccessible { + public: + HyperTextAccessibleWrap(nsIContent* aContent, DocAccessible* aDoc) + : HyperTextAccessible(aContent, aDoc) {} + + void TextForRange(nsAString& aText, int32_t aStartOffset, + HyperTextAccessible* aEndContainer, int32_t aEndOffset); + + nsIntRect BoundsForRange(int32_t aStartOffset, + HyperTextAccessible* aEndContainer, + int32_t aEndOffset); + + int32_t LengthForRange(int32_t aStartOffset, + HyperTextAccessible* aEndContainer, + int32_t aEndOffset); + + void OffsetAtIndex(int32_t aIndex, HyperTextAccessible** aContainer, + int32_t* aOffset); + + void RangeAt(int32_t aOffset, EWhichRange aRangeType, + HyperTextAccessible** aStartContainer, int32_t* aStartOffset, + HyperTextAccessible** aEndContainer, int32_t* aEndOffset); + + void NextClusterAt(int32_t aOffset, HyperTextAccessible** aNextContainer, + int32_t* aNextOffset); + + void PreviousClusterAt(int32_t aOffset, HyperTextAccessible** aPrevContainer, + int32_t* aPrevOffset); + + void RangeOfChild(Accessible* aChild, int32_t* aStartOffset, + int32_t* aEndOffset); + + Accessible* LeafAtOffset(int32_t aOffset); + + MOZ_CAN_RUN_SCRIPT void SelectRange(int32_t aStartOffset, + HyperTextAccessible* aEndContainer, + int32_t aEndOffset); + + protected: + ~HyperTextAccessibleWrap() {} + + private: + TextPoint FindTextPoint(int32_t aOffset, nsDirection aDirection, + nsSelectionAmount aAmount, + EWordMovementType aWordMovementType); + + HyperTextAccessibleWrap* EditableRoot(); + + void LeftWordAt(int32_t aOffset, HyperTextAccessible** aStartContainer, + int32_t* aStartOffset, HyperTextAccessible** aEndContainer, + int32_t* aEndOffset); + + void RightWordAt(int32_t aOffset, HyperTextAccessible** aStartContainer, + int32_t* aStartOffset, HyperTextAccessible** aEndContainer, + int32_t* aEndOffset); + + void LineAt(int32_t aOffset, bool aNextLine, + HyperTextAccessible** aStartContainer, int32_t* aStartOffset, + HyperTextAccessible** aEndContainer, int32_t* aEndOffset); + + void ParagraphAt(int32_t aOffset, HyperTextAccessible** aStartContainer, + int32_t* aStartOffset, HyperTextAccessible** aEndContainer, + int32_t* aEndOffset); + + void StyleAt(int32_t aOffset, HyperTextAccessible** aStartContainer, + int32_t* aStartOffset, HyperTextAccessible** aEndContainer, + int32_t* aEndOffset); +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/mac/HyperTextAccessibleWrap.mm b/accessible/mac/HyperTextAccessibleWrap.mm new file mode 100644 index 0000000000..0fff4621ea --- /dev/null +++ b/accessible/mac/HyperTextAccessibleWrap.mm @@ -0,0 +1,704 @@ +/* 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/. */ + +#include "HyperTextAccessibleWrap.h" + +#include "Accessible-inl.h" +#include "HTMLListAccessible.h" +#include "nsAccUtils.h" +#include "nsFrameSelection.h" +#include "TextRange.h" +#include "TreeWalker.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +// HyperTextIterator + +class HyperTextIterator { + public: + HyperTextIterator(HyperTextAccessible* aStartContainer, int32_t aStartOffset, + HyperTextAccessible* aEndContainer, int32_t aEndOffset) + : mCurrentContainer(aStartContainer), + mCurrentStartOffset(aStartOffset), + mCurrentEndOffset(aStartOffset), + mEndContainer(aEndContainer), + mEndOffset(aEndOffset) {} + + bool Next(); + + int32_t SegmentLength(); + + // If offset is set to a child hyperlink, adjust it so it set on the first + // offset in the deepest link. Or, if the offset to the last character, set it + // to the outermost end offset in an ancestor. Returns true if iterator was + // mutated. + bool NormalizeForward(); + + // If offset is set right after child hyperlink, adjust it so it set on the + // last offset in the deepest link. Or, if the offset is on the first + // character of a link, set it to the outermost start offset in an ancestor. + // Returns true if iterator was mutated. + bool NormalizeBackward(); + + HyperTextAccessible* mCurrentContainer; + int32_t mCurrentStartOffset; + int32_t mCurrentEndOffset; + + private: + int32_t NextLinkOffset(); + + HyperTextAccessible* mEndContainer; + int32_t mEndOffset; +}; + +bool HyperTextIterator::NormalizeForward() { + if (mCurrentStartOffset == nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT || + mCurrentStartOffset >= + static_cast<int32_t>(mCurrentContainer->CharacterCount())) { + // If this is the end of the current container, mutate to its parent's + // end offset. + if (!mCurrentContainer->IsLink()) { + // If we are not a link, it is a root hypertext accessible. + return false; + } + if (!mCurrentContainer->Parent() || + !mCurrentContainer->Parent()->IsHyperText()) { + // If we are a link, but our parent is not a hypertext accessible + // treat the current container as the root hypertext accessible. + // This can be the case with some XUL containers that are not + // hypertext accessibles. + return false; + } + uint32_t endOffset = mCurrentContainer->EndOffset(); + if (endOffset != 0) { + mCurrentContainer = mCurrentContainer->Parent()->AsHyperText(); + mCurrentStartOffset = endOffset; + + if (mCurrentContainer == mEndContainer && + mCurrentStartOffset >= mEndOffset) { + // Reached end boundary. + return false; + } + + // Call NormalizeForward recursively to get top-most link if at the end of + // one, or innermost link if at the beginning. + NormalizeForward(); + return true; + } + } else { + Accessible* link = mCurrentContainer->LinkAt( + mCurrentContainer->LinkIndexAtOffset(mCurrentStartOffset)); + + // If there is a link at this offset, mutate into it. + if (link && link->IsHyperText()) { + if (mCurrentStartOffset > 0 && + mCurrentContainer->LinkIndexAtOffset(mCurrentStartOffset) == + mCurrentContainer->LinkIndexAtOffset(mCurrentStartOffset - 1)) { + MOZ_ASSERT_UNREACHABLE("Same link for previous offset"); + return false; + } + + mCurrentContainer = link->AsHyperText(); + if (link->IsHTMLListItem()) { + Accessible* bullet = link->AsHTMLListItem()->Bullet(); + mCurrentStartOffset = bullet ? nsAccUtils::TextLength(bullet) : 0; + } else { + mCurrentStartOffset = 0; + } + + if (mCurrentContainer == mEndContainer && + mCurrentStartOffset >= mEndOffset) { + // Reached end boundary. + return false; + } + + // Call NormalizeForward recursively to get top-most embedding ancestor + // if at the end of one, or innermost link if at the beginning. + NormalizeForward(); + return true; + } + } + + return false; +} + +bool HyperTextIterator::NormalizeBackward() { + if (mCurrentStartOffset == 0) { + // If this is the start of the current container, mutate to its parent's + // start offset. + if (!mCurrentContainer->IsLink()) { + // If we are not a link, it is a root hypertext accessible. + return false; + } + if (!mCurrentContainer->Parent() || + !mCurrentContainer->Parent()->IsHyperText()) { + // If we are a link, but our parent is not a hypertext accessible + // treat the current container as the root hypertext accessible. + // This can be the case with some XUL containers that are not + // hypertext accessibles. + return false; + } + + uint32_t startOffset = mCurrentContainer->StartOffset(); + mCurrentContainer = mCurrentContainer->Parent()->AsHyperText(); + mCurrentStartOffset = startOffset; + + // Call NormalizeBackward recursively to get top-most link if at the + // beginning of one, or innermost link if at the end. + NormalizeBackward(); + return true; + } else { + Accessible* link = + mCurrentContainer->GetChildAtOffset(mCurrentStartOffset - 1); + + // If there is a link before this offset, mutate into it, + // and set the offset to its last character. + if (link && link->IsHyperText()) { + mCurrentContainer = link->AsHyperText(); + mCurrentStartOffset = mCurrentContainer->CharacterCount(); + + // Call NormalizeBackward recursively to get top-most top-most embedding + // ancestor if at the beginning of one, or innermost link if at the end. + NormalizeBackward(); + return true; + } + + if (mCurrentContainer->IsHTMLListItem() && + mCurrentContainer->AsHTMLListItem()->Bullet() == link) { + mCurrentStartOffset = 0; + NormalizeBackward(); + return true; + } + } + + return false; +} + +int32_t HyperTextIterator::SegmentLength() { + int32_t endOffset = mCurrentEndOffset < 0 + ? mCurrentContainer->CharacterCount() + : mCurrentEndOffset; + + return endOffset - mCurrentStartOffset; +} + +int32_t HyperTextIterator::NextLinkOffset() { + int32_t linkCount = mCurrentContainer->LinkCount(); + for (int32_t i = 0; i < linkCount; i++) { + Accessible* link = mCurrentContainer->LinkAt(i); + MOZ_ASSERT(link); + int32_t linkStartOffset = link->StartOffset(); + if (mCurrentStartOffset < linkStartOffset) { + return linkStartOffset; + } + } + + return -1; +} + +bool HyperTextIterator::Next() { + if (!mCurrentContainer->Document()->HasLoadState( + DocAccessible::eTreeConstructed)) { + // If the accessible tree is still being constructed the text tree + // is not in a traversable state yet. + return false; + } + + if (mCurrentContainer == mEndContainer && + (mCurrentEndOffset == -1 || mEndOffset <= mCurrentEndOffset)) { + return false; + } else { + mCurrentStartOffset = mCurrentEndOffset; + NormalizeForward(); + } + + int32_t nextLinkOffset = NextLinkOffset(); + if (mCurrentContainer == mEndContainer && + (nextLinkOffset == -1 || nextLinkOffset > mEndOffset)) { + mCurrentEndOffset = + mEndOffset < 0 ? mEndContainer->CharacterCount() : mEndOffset; + } else { + mCurrentEndOffset = nextLinkOffset < 0 ? mCurrentContainer->CharacterCount() + : nextLinkOffset; + } + + return mCurrentStartOffset != mCurrentEndOffset; +} + +void HyperTextAccessibleWrap::TextForRange(nsAString& aText, + int32_t aStartOffset, + HyperTextAccessible* aEndContainer, + int32_t aEndOffset) { + if (IsHTMLListItem()) { + Accessible* maybeBullet = GetChildAtOffset(aStartOffset - 1); + if (maybeBullet) { + Accessible* bullet = AsHTMLListItem()->Bullet(); + if (maybeBullet == bullet) { + TextSubstring(0, nsAccUtils::TextLength(bullet), aText); + } + } + } + + HyperTextIterator iter(this, aStartOffset, aEndContainer, aEndOffset); + while (iter.Next()) { + nsAutoString text; + iter.mCurrentContainer->TextSubstring(iter.mCurrentStartOffset, + iter.mCurrentEndOffset, text); + aText.Append(text); + } +} + +nsIntRect HyperTextAccessibleWrap::BoundsForRange( + int32_t aStartOffset, HyperTextAccessible* aEndContainer, + int32_t aEndOffset) { + nsIntRect rect; + HyperTextIterator iter(this, aStartOffset, aEndContainer, aEndOffset); + while (iter.Next()) { + nsIntRect stringRect = iter.mCurrentContainer->TextBounds( + iter.mCurrentStartOffset, iter.mCurrentEndOffset); + rect.UnionRect(rect, stringRect); + } + + return rect; +} + +int32_t HyperTextAccessibleWrap::LengthForRange( + int32_t aStartOffset, HyperTextAccessible* aEndContainer, + int32_t aEndOffset) { + int32_t length = 0; + HyperTextIterator iter(this, aStartOffset, aEndContainer, aEndOffset); + while (iter.Next()) { + length += iter.SegmentLength(); + } + + return length; +} + +void HyperTextAccessibleWrap::OffsetAtIndex(int32_t aIndex, + HyperTextAccessible** aContainer, + int32_t* aOffset) { + int32_t index = aIndex; + HyperTextIterator iter(this, 0, this, CharacterCount()); + while (iter.Next()) { + int32_t segmentLength = iter.SegmentLength(); + if (index <= segmentLength) { + *aContainer = iter.mCurrentContainer; + *aOffset = iter.mCurrentStartOffset + index; + break; + } + index -= segmentLength; + } +} + +void HyperTextAccessibleWrap::RangeAt(int32_t aOffset, EWhichRange aRangeType, + HyperTextAccessible** aStartContainer, + int32_t* aStartOffset, + HyperTextAccessible** aEndContainer, + int32_t* aEndOffset) { + switch (aRangeType) { + case EWhichRange::eLeftWord: + LeftWordAt(aOffset, aStartContainer, aStartOffset, aEndContainer, + aEndOffset); + break; + case EWhichRange::eRightWord: + RightWordAt(aOffset, aStartContainer, aStartOffset, aEndContainer, + aEndOffset); + break; + case EWhichRange::eLine: + case EWhichRange::eLeftLine: + LineAt(aOffset, false, aStartContainer, aStartOffset, aEndContainer, + aEndOffset); + break; + case EWhichRange::eRightLine: + LineAt(aOffset, true, aStartContainer, aStartOffset, aEndContainer, + aEndOffset); + break; + case EWhichRange::eParagraph: + ParagraphAt(aOffset, aStartContainer, aStartOffset, aEndContainer, + aEndOffset); + break; + case EWhichRange::eStyle: + StyleAt(aOffset, aStartContainer, aStartOffset, aEndContainer, + aEndOffset); + break; + default: + break; + } +} + +void HyperTextAccessibleWrap::LeftWordAt(int32_t aOffset, + HyperTextAccessible** aStartContainer, + int32_t* aStartOffset, + HyperTextAccessible** aEndContainer, + int32_t* aEndOffset) { + TextPoint here(this, aOffset); + TextPoint start = + FindTextPoint(aOffset, eDirPrevious, eSelectWord, eStartWord); + if (!start.mContainer) { + return; + } + + if ((NativeState() & states::EDITABLE) && + !(start.mContainer->NativeState() & states::EDITABLE)) { + // The word search crossed an editable boundary. Return the first word of + // the editable root. + return EditableRoot()->RightWordAt(0, aStartContainer, aStartOffset, + aEndContainer, aEndOffset); + } + + TextPoint end = + static_cast<HyperTextAccessibleWrap*>(start.mContainer) + ->FindTextPoint(start.mOffset, eDirNext, eSelectWord, eEndWord); + if (end < here) { + *aStartContainer = end.mContainer; + *aEndContainer = here.mContainer; + *aStartOffset = end.mOffset; + *aEndOffset = here.mOffset; + } else { + *aStartContainer = start.mContainer; + *aEndContainer = end.mContainer; + *aStartOffset = start.mOffset; + *aEndOffset = end.mOffset; + } +} + +void HyperTextAccessibleWrap::RightWordAt(int32_t aOffset, + HyperTextAccessible** aStartContainer, + int32_t* aStartOffset, + HyperTextAccessible** aEndContainer, + int32_t* aEndOffset) { + TextPoint here(this, aOffset); + TextPoint end = FindTextPoint(aOffset, eDirNext, eSelectWord, eEndWord); + if (!end.mContainer || end < here || here == end) { + // If we didn't find a word end, or if we wrapped around (bug 1652833), + // return with no result. + return; + } + + if ((NativeState() & states::EDITABLE) && + !(end.mContainer->NativeState() & states::EDITABLE)) { + // The word search crossed an editable boundary. Return with no result. + return; + } + + TextPoint start = + static_cast<HyperTextAccessibleWrap*>(end.mContainer) + ->FindTextPoint(end.mOffset, eDirPrevious, eSelectWord, eStartWord); + + if (here < start) { + *aStartContainer = here.mContainer; + *aEndContainer = start.mContainer; + *aStartOffset = here.mOffset; + *aEndOffset = start.mOffset; + } else { + *aStartContainer = start.mContainer; + *aEndContainer = end.mContainer; + *aStartOffset = start.mOffset; + *aEndOffset = end.mOffset; + } +} + +void HyperTextAccessibleWrap::LineAt(int32_t aOffset, bool aNextLine, + HyperTextAccessible** aStartContainer, + int32_t* aStartOffset, + HyperTextAccessible** aEndContainer, + int32_t* aEndOffset) { + TextPoint here(this, aOffset); + TextPoint end = + FindTextPoint(aOffset, eDirNext, eSelectEndLine, eDefaultBehavior); + if (!end.mContainer || end < here) { + // If we didn't find a word end, or if we wrapped around (bug 1652833), + // return with no result. + return; + } + + TextPoint start = static_cast<HyperTextAccessibleWrap*>(end.mContainer) + ->FindTextPoint(end.mOffset, eDirPrevious, + eSelectBeginLine, eDefaultBehavior); + + if (!aNextLine && here < start) { + start = FindTextPoint(aOffset, eDirPrevious, eSelectBeginLine, + eDefaultBehavior); + if (!start.mContainer) { + return; + } + + end = static_cast<HyperTextAccessibleWrap*>(start.mContainer) + ->FindTextPoint(start.mOffset, eDirNext, eSelectEndLine, + eDefaultBehavior); + } + + *aStartContainer = start.mContainer; + *aEndContainer = end.mContainer; + *aStartOffset = start.mOffset; + *aEndOffset = end.mOffset; +} + +void HyperTextAccessibleWrap::ParagraphAt(int32_t aOffset, + HyperTextAccessible** aStartContainer, + int32_t* aStartOffset, + HyperTextAccessible** aEndContainer, + int32_t* aEndOffset) { + TextPoint here(this, aOffset); + TextPoint end = + FindTextPoint(aOffset, eDirNext, eSelectParagraph, eDefaultBehavior); + + if (!end.mContainer || end < here) { + // If we didn't find a word end, or if we wrapped around (bug 1652833), + // return with no result. + return; + } + + if (end.mOffset == -1 && Parent() && Parent()->IsHyperText()) { + // If end offset is -1 we didn't find a paragraph boundary. + // This must be an inline container, go to its parent to + // retrieve paragraph boundaries. + static_cast<HyperTextAccessibleWrap*>(Parent()->AsHyperText()) + ->ParagraphAt(StartOffset(), aStartContainer, aStartOffset, + aEndContainer, aEndOffset); + return; + } + + TextPoint start = static_cast<HyperTextAccessibleWrap*>(end.mContainer) + ->FindTextPoint(end.mOffset, eDirPrevious, + eSelectParagraph, eDefaultBehavior); + + *aStartContainer = start.mContainer; + *aEndContainer = end.mContainer; + *aStartOffset = start.mOffset; + *aEndOffset = end.mOffset; +} + +void HyperTextAccessibleWrap::StyleAt(int32_t aOffset, + HyperTextAccessible** aStartContainer, + int32_t* aStartOffset, + HyperTextAccessible** aEndContainer, + int32_t* aEndOffset) { + // Get the range of the text leaf at this offset. + // A text leaf represents a stretch of like-styled text. + auto leaf = LeafAtOffset(aOffset); + if (!leaf) { + return; + } + + MOZ_ASSERT(leaf->Parent()->IsHyperText()); + HyperTextAccessibleWrap* container = + static_cast<HyperTextAccessibleWrap*>(leaf->Parent()->AsHyperText()); + if (!container) { + return; + } + + *aStartContainer = *aEndContainer = container; + container->RangeOfChild(leaf, aStartOffset, aEndOffset); +} + +void HyperTextAccessibleWrap::NextClusterAt( + int32_t aOffset, HyperTextAccessible** aNextContainer, + int32_t* aNextOffset) { + TextPoint here(this, aOffset); + TextPoint next = + FindTextPoint(aOffset, eDirNext, eSelectCluster, eDefaultBehavior); + + if ((next.mOffset == nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT && + next.mContainer == Document()) || + (next < here)) { + // If we reached the end of the doc, or if we wrapped to the start of the + // doc return given offset as-is. + *aNextContainer = this; + *aNextOffset = aOffset; + } else { + *aNextContainer = next.mContainer; + *aNextOffset = next.mOffset; + } +} + +void HyperTextAccessibleWrap::PreviousClusterAt( + int32_t aOffset, HyperTextAccessible** aPrevContainer, + int32_t* aPrevOffset) { + TextPoint prev = + FindTextPoint(aOffset, eDirPrevious, eSelectCluster, eDefaultBehavior); + *aPrevContainer = prev.mContainer; + *aPrevOffset = prev.mOffset; +} + +void HyperTextAccessibleWrap::RangeOfChild(Accessible* aChild, + int32_t* aStartOffset, + int32_t* aEndOffset) { + MOZ_ASSERT(aChild->Parent() == this); + *aStartOffset = *aEndOffset = -1; + int32_t index = GetIndexOf(aChild); + if (index != -1) { + *aStartOffset = GetChildOffset(index); + // If this is the last child index + 1 will return the total + // chracter count. + *aEndOffset = GetChildOffset(index + 1); + } +} + +Accessible* HyperTextAccessibleWrap::LeafAtOffset(int32_t aOffset) { + HyperTextAccessible* text = this; + Accessible* child = nullptr; + // The offset needed should "attach" the previous accessible if + // in between two accessibles. + int32_t innerOffset = aOffset > 0 ? aOffset - 1 : aOffset; + do { + int32_t childIdx = text->GetChildIndexAtOffset(innerOffset); + if (childIdx == -1) { + return text; + } + + child = text->GetChildAt(childIdx); + if (!child || nsAccUtils::MustPrune(text)) { + return text; + } + + innerOffset -= text->GetChildOffset(childIdx); + + text = child->AsHyperText(); + } while (text); + + return child; +} + +void HyperTextAccessibleWrap::SelectRange(int32_t aStartOffset, + HyperTextAccessible* aEndContainer, + int32_t aEndOffset) { + TextRange range(this, this, aStartOffset, aEndContainer, aEndOffset); + range.SetSelectionAt(0); +} + +TextPoint HyperTextAccessibleWrap::FindTextPoint( + int32_t aOffset, nsDirection aDirection, nsSelectionAmount aAmount, + EWordMovementType aWordMovementType) { + // Layout can remain trapped in an editable. We normalize out of + // it if we are in its last offset. + HyperTextIterator iter(this, aOffset, this, CharacterCount()); + if (aDirection == eDirNext) { + iter.NormalizeForward(); + } else { + iter.NormalizeBackward(); + } + + // Find a leaf accessible frame to start with. PeekOffset wants this. + HyperTextAccessible* text = iter.mCurrentContainer; + Accessible* child = nullptr; + int32_t innerOffset = iter.mCurrentStartOffset; + + do { + int32_t childIdx = text->GetChildIndexAtOffset(innerOffset); + + // We can have an empty text leaf as our only child. Since empty text + // leaves are not accessible we then have no children, but 0 is a valid + // innerOffset. + if (childIdx == -1) { + NS_ASSERTION(innerOffset == 0 && !text->ChildCount(), "No childIdx?"); + return TextPoint(text, 0); + } + + child = text->GetChildAt(childIdx); + if (child->IsHyperText() && !child->ChildCount()) { + // If this is a childless hypertext, jump to its + // previous or next sibling, depending on + // direction. + if (aDirection == eDirPrevious && childIdx > 0) { + child = text->GetChildAt(--childIdx); + } else if (aDirection == eDirNext && + childIdx + 1 < static_cast<int32_t>(text->ChildCount())) { + child = text->GetChildAt(++childIdx); + } + } + + int32_t childOffset = text->GetChildOffset(childIdx); + + if (child->IsHyperText() && aDirection == eDirPrevious && childIdx > 0 && + innerOffset - childOffset == 0) { + // If we are searching backwards, and this is the begining of a + // segment, get the previous sibling so that layout will start + // its search there. + childIdx--; + innerOffset -= text->GetChildOffset(childIdx); + child = text->GetChildAt(childIdx); + } else { + innerOffset -= childOffset; + } + + text = child->AsHyperText(); + } while (text); + + nsIFrame* childFrame = child->GetFrame(); + if (!childFrame) { + NS_ERROR("No child frame"); + return TextPoint(this, aOffset); + } + + int32_t innerContentOffset = innerOffset; + if (child->IsTextLeaf()) { + NS_ASSERTION(childFrame->IsTextFrame(), "Wrong frame!"); + RenderedToContentOffset(childFrame, innerOffset, &innerContentOffset); + } + + nsIFrame* frameAtOffset = childFrame; + int32_t offsetInFrame = 0; + childFrame->GetChildFrameContainingOffset(innerContentOffset, true, + &offsetInFrame, &frameAtOffset); + if (aDirection == eDirPrevious && offsetInFrame == 0) { + // If we are searching backwards, and we are at the start of a frame, + // get the previous continuation frame. + if (nsIFrame* prevInContinuation = frameAtOffset->GetPrevContinuation()) { + frameAtOffset = prevInContinuation; + } + } + + const bool kIsJumpLinesOk = true; // okay to jump lines + const bool kIsScrollViewAStop = false; // do not stop at scroll views + const bool kIsKeyboardSelect = true; // is keyboard selection + const bool kIsVisualBidi = false; // use visual order for bidi text + nsPeekOffsetStruct pos( + aAmount, aDirection, innerContentOffset, nsPoint(0, 0), kIsJumpLinesOk, + kIsScrollViewAStop, kIsKeyboardSelect, kIsVisualBidi, false, + nsPeekOffsetStruct::ForceEditableRegion::No, aWordMovementType, false); + nsresult rv = frameAtOffset->PeekOffset(&pos); + + // PeekOffset fails on last/first lines of the text in certain cases. + if (NS_FAILED(rv) && aAmount == eSelectLine) { + pos.mAmount = aDirection == eDirNext ? eSelectEndLine : eSelectBeginLine; + frameAtOffset->PeekOffset(&pos); + } + if (!pos.mResultContent) { + NS_ERROR("No result content!"); + return TextPoint(this, aOffset); + } + + if (aDirection == eDirNext && + nsContentUtils::PositionIsBefore(pos.mResultContent, mContent, nullptr, + nullptr)) { + // Bug 1652833 makes us sometimes return the first element on the doc. + return TextPoint(Document(), nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT); + } + + HyperTextAccessible* container = + nsAccUtils::GetTextContainer(pos.mResultContent); + int32_t offset = container ? container->DOMPointToOffset( + pos.mResultContent, pos.mContentOffset, + aDirection == eDirNext) + : 0; + return TextPoint(container, offset); +} + +HyperTextAccessibleWrap* HyperTextAccessibleWrap::EditableRoot() { + Accessible* editable = nullptr; + for (Accessible* acc = this; acc && acc != Document(); acc = acc->Parent()) { + if (acc->NativeState() & states::EDITABLE) { + editable = acc; + } else { + break; + } + } + + return static_cast<HyperTextAccessibleWrap*>(editable->AsHyperText()); +} diff --git a/accessible/mac/ImageAccessibleWrap.h b/accessible/mac/ImageAccessibleWrap.h new file mode 100644 index 0000000000..a5621c7313 --- /dev/null +++ b/accessible/mac/ImageAccessibleWrap.h @@ -0,0 +1,21 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=2: + */ +/* 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/. */ + +#ifndef mozilla_a11y_ImageAccessibleWrap_h__ +#define mozilla_a11y_ImageAccessibleWrap_h__ + +#include "ImageAccessible.h" + +namespace mozilla { +namespace a11y { + +typedef class ImageAccessible ImageAccessibleWrap; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/mac/MOXAccessibleBase.h b/accessible/mac/MOXAccessibleBase.h new file mode 100644 index 0000000000..b88b8f2857 --- /dev/null +++ b/accessible/mac/MOXAccessibleBase.h @@ -0,0 +1,138 @@ +/* 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 "mozAccessibleProtocol.h" +#import "MOXAccessibleProtocol.h" + +#include "Platform.h" + +inline id<mozAccessible> GetObjectOrRepresentedView(id<mozAccessible> aObject) { + if (!mozilla::a11y::ShouldA11yBeEnabled()) { + // If platform a11y is not enabled, don't return represented view. + // This is mostly for our mochitest environment because the represented + // ChildView checks `ShouldA11yBeEnabled` before proxying accessibility + // methods to mozAccessibles. + return aObject; + } + + return [aObject hasRepresentedView] ? [aObject representedView] : aObject; +} + +@interface MOXAccessibleBase : NSObject <mozAccessible, MOXAccessible> { + BOOL mIsExpired; +} + +#pragma mark - mozAccessible/widget + +// override +- (BOOL)hasRepresentedView; + +// override +- (id)representedView; + +// override +- (BOOL)isRoot; + +#pragma mark - mozAccessible/NSAccessibility + +// The methods below interface with the platform through NSAccessibility. +// They should not be called directly or overridden in subclasses. + +// override, final +- (NSArray*)accessibilityAttributeNames; + +// override, final +- (id)accessibilityAttributeValue:(NSString*)attribute; + +// override, final +- (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute; + +// override, final +- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute; + +// override, final +- (NSArray*)accessibilityActionNames; + +// override, final +- (void)accessibilityPerformAction:(NSString*)action; + +// override, final +- (NSString*)accessibilityActionDescription:(NSString*)action; + +// override, final +- (NSArray*)accessibilityParameterizedAttributeNames; + +// override, final +- (id)accessibilityAttributeValue:(NSString*)attribute + forParameter:(id)parameter; + +// override, final +- (id)accessibilityHitTest:(NSPoint)point; + +// override, final +- (id)accessibilityFocusedUIElement; + +// override, final +- (BOOL)isAccessibilityElement; + +// final +- (BOOL)accessibilityNotifiesWhenDestroyed; + +#pragma mark - MOXAccessible protocol + +// override +- (id)moxHitTest:(NSPoint)point; + +// override +- (id)moxFocusedUIElement; + +// override +- (void)moxPostNotification:(NSString*)notification; + +// override +- (void)moxPostNotification:(NSString*)notification + withUserInfo:(NSDictionary*)userInfo; + +// override +- (BOOL)moxBlockSelector:(SEL)selector; + +// override +- (NSArray*)moxUnignoredChildren; + +// override +- (NSArray*)moxChildren; + +// override +- (id<mozAccessible>)moxUnignoredParent; + +// override +- (id<mozAccessible>)moxParent; + +// override +- (NSNumber*)moxIndexForChildUIElement:(id)child; + +// override +- (id)moxTopLevelUIElement; + +// override +- (id<MOXTextMarkerSupport>)moxTextMarkerDelegate; + +- (BOOL)moxIsLiveRegion; + +#pragma mark - + +- (NSString*)description; + +- (BOOL)isExpired; + +// makes ourselves "expired". after this point, we might be around if someone +// has retained us (e.g., a third-party), but we really contain no information. +- (void)expire; + +@end diff --git a/accessible/mac/MOXAccessibleBase.mm b/accessible/mac/MOXAccessibleBase.mm new file mode 100644 index 0000000000..0e671659a2 --- /dev/null +++ b/accessible/mac/MOXAccessibleBase.mm @@ -0,0 +1,553 @@ +/* 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 "MOXAccessibleBase.h" + +#import "MacSelectorMap.h" + +#include "nsObjCExceptions.h" +#include "xpcAccessibleMacInterface.h" +#include "mozilla/Logging.h" + +using namespace mozilla::a11y; + +#undef LOG +mozilla::LogModule* GetMacAccessibilityLog() { + static mozilla::LazyLogModule sLog("MacAccessibility"); + + return sLog; +} +#define LOG(type, format, ...) \ + do { \ + if (MOZ_LOG_TEST(GetMacAccessibilityLog(), type)) { \ + NSString* msg = [NSString stringWithFormat:(format), ##__VA_ARGS__]; \ + MOZ_LOG(GetMacAccessibilityLog(), type, ("%s", [msg UTF8String])); \ + } \ + } while (0) + +@interface NSObject (MOXAccessible) + +// This NSObject conforms to MOXAccessible. +// This is needed to we know to mutate the value +// (get represented view, check isAccessibilityElement) +// before forwarding it to NSAccessibility. +- (BOOL)isMOXAccessible; + +// Same as above, but this checks if the NSObject is an array with +// mozAccessible conforming objects. +- (BOOL)hasMOXAccessibles; + +@end + +@implementation NSObject (MOXAccessible) + +- (BOOL)isMOXAccessible { + return [self conformsToProtocol:@protocol(MOXAccessible)]; +} + +- (BOOL)hasMOXAccessibles { + return [self isKindOfClass:[NSArray class]] && + [[(NSArray*)self firstObject] isMOXAccessible]; +} + +@end + +// Private methods +@interface MOXAccessibleBase () + +- (BOOL)isSelectorSupported:(SEL)selector; + +@end + +@implementation MOXAccessibleBase + +#pragma mark - mozAccessible/widget + +- (BOOL)hasRepresentedView { + return NO; +} + +- (id)representedView { + return nil; +} + +- (BOOL)isRoot { + return NO; +} + +#pragma mark - mozAccessible/NSAccessibility + +- (NSArray*)accessibilityAttributeNames { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + if ([self isExpired]) { + return nil; + } + + static NSMutableDictionary* attributesForEachClass = nil; + + if (!attributesForEachClass) { + attributesForEachClass = [[NSMutableDictionary alloc] init]; + } + + NSMutableArray* attributes = + attributesForEachClass [[self class]] ?: [[NSMutableArray alloc] init]; + + NSDictionary* getters = mac::AttributeGetters(); + if (![attributes count]) { + // Go through all our attribute getters, if they are supported by this class + // advertise the attribute name. + for (NSString* attribute in getters) { + SEL selector = NSSelectorFromString(getters[attribute]); + if ([self isSelectorSupported:selector]) { + [attributes addObject:attribute]; + } + } + + // If we have a delegate add all the text marker attributes. + if ([self moxTextMarkerDelegate]) { + [attributes addObjectsFromArray:[mac::TextAttributeGetters() allKeys]]; + } + + // We store a hash table with types as keys, and atttribute lists as values. + // This lets us cache the atttribute list of each subclass so we only + // need to gather its MOXAccessible methods once. + // XXX: Uncomment when accessibilityAttributeNames is removed from all + // subclasses. attributesForEachClass[[self class]] = attributes; + } + + return attributes; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (id)accessibilityAttributeValue:(NSString*)attribute { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + if ([self isExpired]) { + return nil; + } + + id value = nil; + NSDictionary* getters = mac::AttributeGetters(); + if (getters[attribute]) { + SEL selector = NSSelectorFromString(getters[attribute]); + if ([self isSelectorSupported:selector]) { + value = [self performSelector:selector]; + } + } else if (id textMarkerDelegate = [self moxTextMarkerDelegate]) { + // If we have a delegate, check if attribute is a text marker + // attribute and call the associated selector on the delegate + // if so. + NSDictionary* textMarkerGetters = mac::TextAttributeGetters(); + if (textMarkerGetters[attribute]) { + SEL selector = NSSelectorFromString(textMarkerGetters[attribute]); + if ([textMarkerDelegate respondsToSelector:selector]) { + value = [textMarkerDelegate performSelector:selector]; + } + } + } + + if ([value isMOXAccessible]) { + // If this is a MOXAccessible, get its represented view or filter it if + // it should be ignored. + value = [value isAccessibilityElement] ? GetObjectOrRepresentedView(value) + : nil; + } + + if ([value hasMOXAccessibles]) { + // If this is an array of mozAccessibles, get each element's represented + // view and remove it from the returned array if it should be ignored. + NSUInteger arrSize = [value count]; + NSMutableArray* arr = [[NSMutableArray alloc] initWithCapacity:arrSize]; + for (NSUInteger i = 0; i < arrSize; i++) { + id<mozAccessible> mozAcc = GetObjectOrRepresentedView(value[i]); + if ([mozAcc isAccessibilityElement]) { + [arr addObject:mozAcc]; + } + } + + value = arr; + } + + if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Debug)) { + if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Verbose)) { + LOG(LogLevel::Verbose, @"%@ attributeValue %@ => %@", self, attribute, + value); + } else if (![attribute isEqualToString:@"AXParent"] && + ![attribute isEqualToString:@"AXRole"] && + ![attribute isEqualToString:@"AXSubrole"] && + ![attribute isEqualToString:@"AXSize"] && + ![attribute isEqualToString:@"AXPosition"] && + ![attribute isEqualToString:@"AXRole"] && + ![attribute isEqualToString:@"AXChildren"]) { + LOG(LogLevel::Debug, @"%@ attributeValue %@", self, attribute); + } + } + + return value; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + if ([self isExpired]) { + return NO; + } + + NSDictionary* setters = mac::AttributeSetters(); + if (setters[attribute]) { + SEL selector = NSSelectorFromString(setters[attribute]); + if ([self isSelectorSupported:selector]) { + return YES; + } + } else if (id textMarkerDelegate = [self moxTextMarkerDelegate]) { + // If we have a delegate, check text setters on delegate + NSDictionary* textMarkerSetters = mac::TextAttributeSetters(); + if (textMarkerSetters[attribute]) { + SEL selector = NSSelectorFromString(textMarkerSetters[attribute]); + if ([textMarkerDelegate respondsToSelector:selector]) { + return YES; + } + } + } + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NO); +} + +- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if ([self isExpired]) { + return; + } + + LOG(LogLevel::Debug, @"%@ setValueForattribute %@ = %@", self, attribute, + value); + + NSDictionary* setters = mac::AttributeSetters(); + if (setters[attribute]) { + SEL selector = NSSelectorFromString(setters[attribute]); + if ([self isSelectorSupported:selector]) { + [self performSelector:selector withObject:value]; + } + } else if (id textMarkerDelegate = [self moxTextMarkerDelegate]) { + // If we have a delegate, check if attribute is a text marker + // attribute and call the associated selector on the delegate + // if so. + NSDictionary* textMarkerSetters = mac::TextAttributeSetters(); + if (textMarkerSetters[attribute]) { + SEL selector = NSSelectorFromString(textMarkerSetters[attribute]); + if ([textMarkerDelegate respondsToSelector:selector]) { + [textMarkerDelegate performSelector:selector withObject:value]; + } + } + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (NSArray*)accessibilityActionNames { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + if ([self isExpired]) { + return nil; + } + + NSMutableArray* actionNames = [[NSMutableArray alloc] init]; + + NSDictionary* actions = mac::Actions(); + for (NSString* action in actions) { + SEL selector = NSSelectorFromString(actions[action]); + if ([self isSelectorSupported:selector]) { + [actionNames addObject:action]; + } + } + + return actionNames; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (void)accessibilityPerformAction:(NSString*)action { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if ([self isExpired]) { + return; + } + + LOG(LogLevel::Debug, @"%@ performAction %@ ", self, action); + + NSDictionary* actions = mac::Actions(); + if (actions[action]) { + SEL selector = NSSelectorFromString(actions[action]); + if ([self isSelectorSupported:selector]) { + [self performSelector:selector]; + } + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (NSString*)accessibilityActionDescription:(NSString*)action { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + // by default we return whatever the MacOS API know about. + // if you have custom actions, override. + return NSAccessibilityActionDescription(action); + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (NSArray*)accessibilityParameterizedAttributeNames { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + if ([self isExpired]) { + return nil; + } + + NSMutableArray* attributeNames = [[NSMutableArray alloc] init]; + + NSDictionary* attributes = mac::ParameterizedAttributeGetters(); + for (NSString* attribute in attributes) { + SEL selector = NSSelectorFromString(attributes[attribute]); + if ([self isSelectorSupported:selector]) { + [attributeNames addObject:attribute]; + } + } + + // If we have a delegate add all the text marker attributes. + if ([self moxTextMarkerDelegate]) { + [attributeNames + addObjectsFromArray:[mac::ParameterizedTextAttributeGetters() allKeys]]; + } + + return attributeNames; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (id)accessibilityAttributeValue:(NSString*)attribute + forParameter:(id)parameter { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + if ([self isExpired]) { + return nil; + } + + id value = nil; + + NSDictionary* getters = mac::ParameterizedAttributeGetters(); + if (getters[attribute]) { + SEL selector = NSSelectorFromString(getters[attribute]); + if ([self isSelectorSupported:selector]) { + value = [self performSelector:selector withObject:parameter]; + } + } else if (id textMarkerDelegate = [self moxTextMarkerDelegate]) { + // If we have a delegate, check if attribute is a text marker + // attribute and call the associated selector on the delegate + // if so. + NSDictionary* textMarkerGetters = mac::ParameterizedTextAttributeGetters(); + if (textMarkerGetters[attribute]) { + SEL selector = NSSelectorFromString(textMarkerGetters[attribute]); + if ([textMarkerDelegate respondsToSelector:selector]) { + value = [textMarkerDelegate performSelector:selector + withObject:parameter]; + } + } + } + + if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Verbose)) { + LOG(LogLevel::Verbose, @"%@ attributeValueForParam %@(%@) => %@", self, + attribute, parameter, value); + } else { + LOG(LogLevel::Debug, @"%@ attributeValueForParam %@", self, attribute); + } + + return value; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (id)accessibilityHitTest:(NSPoint)point { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + return [self moxHitTest:point]; + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (id)accessibilityFocusedUIElement { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + return [self moxFocusedUIElement]; + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (BOOL)isAccessibilityElement { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + if ([self isExpired]) { + return YES; + } + + id parent = [self moxParent]; + if (![parent isMOXAccessible]) { + return YES; + } + + return ![self moxIgnoreWithParent:parent]; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NO); +} + +- (BOOL)accessibilityNotifiesWhenDestroyed { + return YES; +} + +#pragma mark - MOXAccessible protocol + +- (NSNumber*)moxIndexForChildUIElement:(id)child { + return @([[self moxUnignoredChildren] indexOfObject:child]); +} + +- (id)moxTopLevelUIElement { + return [self moxWindow]; +} + +- (id)moxHitTest:(NSPoint)point { + return GetObjectOrRepresentedView(self); +} + +- (id)moxFocusedUIElement { + return GetObjectOrRepresentedView(self); +} + +- (void)moxPostNotification:(NSString*)notification { + [self moxPostNotification:notification withUserInfo:nil]; +} + +- (void)moxPostNotification:(NSString*)notification + withUserInfo:(NSDictionary*)userInfo { + if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Verbose)) { + LOG(LogLevel::Verbose, @"%@ notify %@ %@", self, notification, userInfo); + } else { + LOG(LogLevel::Debug, @"%@ notify %@", self, notification); + } + + // This sends events via nsIObserverService to be consumed by our mochitests. + xpcAccessibleMacEvent::FireEvent(self, notification, userInfo); + + if (gfxPlatform::IsHeadless()) { + // Using a headless toolkit for tests and whatnot, posting accessibility + // notification won't work. + return; + } + + if (![self isAccessibilityElement]) { + // If this is an ignored object, don't expose it to system. + return; + } + + if (userInfo) { + NSAccessibilityPostNotificationWithUserInfo( + GetObjectOrRepresentedView(self), notification, userInfo); + } else { + NSAccessibilityPostNotification(GetObjectOrRepresentedView(self), + notification); + } +} + +- (BOOL)moxBlockSelector:(SEL)selector { + return NO; +} + +- (NSArray*)moxChildren { + return @[]; +} + +- (NSArray*)moxUnignoredChildren { + NSMutableArray* unignoredChildren = [[NSMutableArray alloc] init]; + NSArray* allChildren = [self moxChildren]; + + for (MOXAccessibleBase* nativeChild in allChildren) { + if ([nativeChild moxIgnoreWithParent:self]) { + // If this child should be ignored get its unignored children. + // This will in turn recurse to any unignored descendants if the + // child is ignored. + [unignoredChildren + addObjectsFromArray:[nativeChild moxUnignoredChildren]]; + } else { + [unignoredChildren addObject:nativeChild]; + } + } + + return unignoredChildren; +} + +- (id<mozAccessible>)moxParent { + return nil; +} + +- (id<mozAccessible>)moxUnignoredParent { + id nativeParent = [self moxParent]; + + if (![nativeParent isAccessibilityElement]) { + return [nativeParent moxUnignoredParent]; + } + + return GetObjectOrRepresentedView(nativeParent); +} + +- (BOOL)moxIgnoreWithParent:(MOXAccessibleBase*)parent { + return [parent moxIgnoreChild:self]; +} + +- (BOOL)moxIgnoreChild:(MOXAccessibleBase*)child { + return NO; +} + +- (id<MOXTextMarkerSupport>)moxTextMarkerDelegate { + return nil; +} + +- (BOOL)moxIsLiveRegion { + return NO; +} + +#pragma mark - + +// objc-style description (from NSObject); not to be confused with the +// accessible description above. +- (NSString*)description { + if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Debug)) { + if ([self isSelectorSupported:@selector(moxMozDebugDescription)]) { + return [self moxMozDebugDescription]; + } + } + + return [NSString stringWithFormat:@"<%@: %p %@>", + NSStringFromClass([self class]), self, + [self moxRole]]; +} + +- (BOOL)isExpired { + return mIsExpired; +} + +- (void)expire { + MOZ_ASSERT(!mIsExpired, "expire called an expired mozAccessible!"); + + mIsExpired = YES; + + [self moxPostNotification:NSAccessibilityUIElementDestroyedNotification]; +} + +#pragma mark - Private + +- (BOOL)isSelectorSupported:(SEL)selector { + return + [self respondsToSelector:selector] && ![self moxBlockSelector:selector]; +} + +@end diff --git a/accessible/mac/MOXAccessibleProtocol.h b/accessible/mac/MOXAccessibleProtocol.h new file mode 100644 index 0000000000..8b9a55b07a --- /dev/null +++ b/accessible/mac/MOXAccessibleProtocol.h @@ -0,0 +1,502 @@ +/* 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/. */ + +@protocol MOXTextMarkerSupport; + +// This protocol's primary use is for abstracting the NSAccessibility informal +// protocol into a formal internal API. Conforming classes get to choose a +// subset of the optional methods to implement. Those methods will be mapped to +// NSAccessibility attributes or actions. A conforming class can implement +// moxBlockSelector to control which of its implemented methods should be +// exposed to NSAccessibility. + +@protocol MOXAccessible + +// The deepest descendant of the accessible subtree that contains the specified +// point. Forwarded from accessibilityHitTest. +- (id _Nullable)moxHitTest:(NSPoint)point; + +// The deepest descendant of the accessible subtree that has the focus. +// Forwarded from accessibilityFocusedUIElement. +- (id _Nullable)moxFocusedUIElement; + +// Sends a notification to any observing assistive applications. +- (void)moxPostNotification:(NSString* _Nonnull)notification; + +- (void)moxPostNotification:(NSString* _Nonnull)notification + withUserInfo:(NSDictionary* _Nullable)userInfo; + +// Return YES if selector should be considered not supported. +// Used in implementations such as: +// - accessibilityAttributeNames +// - accessibilityActionNames +// - accessibilityIsAttributeSettable +- (BOOL)moxBlockSelector:(SEL _Nonnull)selector; + +// Returns a list of all children, doesn't do ignore filtering. +- (NSArray* _Nullable)moxChildren; + +// Returns our parent, doesn't do ignore filtering. +- (id<mozAccessible> _Nullable)moxParent; + +// This is called by isAccessibilityElement. If a subclass wants +// to alter the isAccessibilityElement return value, it must +// override this and not isAccessibilityElement directly. +- (BOOL)moxIgnoreWithParent:(id<MOXAccessible> _Nullable)parent; + +// Should the child be ignored. This allows subclasses to determine +// what kinds of accessibles are valid children. This causes the child +// to be skipped, but the unignored descendants will be added to the +// container in the default children getter. +- (BOOL)moxIgnoreChild:(id<MOXAccessible> _Nullable)child; + +// Return text delegate if it exists. +- (id<MOXTextMarkerSupport> _Nullable)moxTextMarkerDelegate; + +// Return true if this accessible is a live region +- (BOOL)moxIsLiveRegion; + +@optional + +#pragma mark - AttributeGetters + +// AXChildren +- (NSArray* _Nullable)moxUnignoredChildren; + +// AXParent +- (id _Nullable)moxUnignoredParent; + +// AXRole +- (NSString* _Nullable)moxRole; + +// AXRoleDescription +- (NSString* _Nullable)moxRoleDescription; + +// AXSubrole +- (NSString* _Nullable)moxSubrole; + +// AXTitle +- (NSString* _Nullable)moxTitle; + +// AXDescription +- (NSString* _Nullable)moxLabel; + +// AXHelp +- (NSString* _Nullable)moxHelp; + +// AXValue +- (id _Nullable)moxValue; + +// AXSize +- (NSValue* _Nullable)moxSize; + +// AXPosition +- (NSValue* _Nullable)moxPosition; + +// AXEnabled +- (NSNumber* _Nullable)moxEnabled; + +// AXFocused +- (NSNumber* _Nullable)moxFocused; + +// AXWindow +- (id _Nullable)moxWindow; + +// AXTitleUIElement +- (id _Nullable)moxTitleUIElement; + +// AXTopLevelUIElement +- (id _Nullable)moxTopLevelUIElement; + +// AXHasPopup +- (NSNumber* _Nullable)moxHasPopup; + +// AXARIACurrent +- (NSString* _Nullable)moxARIACurrent; + +// AXSelected +- (NSNumber* _Nullable)moxSelected; + +// AXRequired +- (NSNumber* _Nullable)moxRequired; + +// AXElementBusy +- (NSNumber* _Nullable)moxElementBusy; + +// AXLinkedUIElements +- (NSArray* _Nullable)moxLinkedUIElements; + +// AXARIAControls +- (NSArray* _Nullable)moxARIAControls; + +// AXDOMIdentifier +- (NSString* _Nullable)moxDOMIdentifier; + +// AXURL +- (NSURL* _Nullable)moxURL; + +// AXLinkUIElements +- (NSArray* _Nullable)moxLinkUIElements; + +// AXPopupValue +- (NSString* _Nullable)moxPopupValue; + +// AXVisited +- (NSNumber* _Nullable)moxVisited; + +// AXExpanded +- (NSNumber* _Nullable)moxExpanded; + +// AXMain +- (NSNumber* _Nullable)moxMain; + +// AXMinimized +- (NSNumber* _Nullable)moxMinimized; + +// AXSelectedChildren +- (NSArray* _Nullable)moxSelectedChildren; + +// AXTabs +- (NSArray* _Nullable)moxTabs; + +// AXContents +- (NSArray* _Nullable)moxContents; + +// AXOrientation +- (NSString* _Nullable)moxOrientation; + +// AXMenuItemMarkChar +- (NSString* _Nullable)moxMenuItemMarkChar; + +// AXLoaded +- (NSNumber* _Nullable)moxLoaded; + +// AXLoadingProgress +- (NSNumber* _Nullable)moxLoadingProgress; + +// Webkit also implements the following: +// // AXCaretBrowsingEnabled +// - (NSString* _Nullable)moxCaretBrowsingEnabled; + +// // AXLayoutCount +// - (NSString* _Nullable)moxLayoutCount; + +// // AXWebSessionID +// - (NSString* _Nullable)moxWebSessionID; + +// // AXPreventKeyboardDOMEventDispatch +// - (NSString* _Nullable)moxPreventKeyboardDOMEventDispatch; + +// Table Attributes + +// AXRowCount +- (NSNumber* _Nullable)moxRowCount; + +// AXColumnCount +- (NSNumber* _Nullable)moxColumnCount; + +// AXRows +- (NSArray* _Nullable)moxRows; + +// AXColumns +- (NSArray* _Nullable)moxColumns; + +// AXIndex +- (NSNumber* _Nullable)moxIndex; + +// AXRowIndexRange +- (NSValue* _Nullable)moxRowIndexRange; + +// AXColumnIndexRange +- (NSValue* _Nullable)moxColumnIndexRange; + +// AXRowHeaderUIElements +- (NSArray* _Nullable)moxRowHeaderUIElements; + +// AXColumnHeaderUIElements +- (NSArray* _Nullable)moxColumnHeaderUIElements; + +// AXIdentifier +- (NSString* _Nullable)moxIdentifier; + +// AXVisibleChildren +- (NSArray* _Nullable)moxVisibleChildren; + +// Outline Attributes + +// AXDisclosing +- (NSNumber* _Nullable)moxDisclosing; + +// AXDisclosedByRow +- (id _Nullable)moxDisclosedByRow; + +// AXDisclosureLevel +- (NSNumber* _Nullable)moxDisclosureLevel; + +// AXDisclosedRows +- (NSArray* _Nullable)moxDisclosedRows; + +// AXSelectedRows +- (NSArray* _Nullable)moxSelectedRows; + +// Math Attributes + +// AXMathRootRadicand +- (id _Nullable)moxMathRootRadicand; + +// AXMathRootIndex +- (id _Nullable)moxMathRootIndex; + +// AXMathFractionNumerator +- (id _Nullable)moxMathFractionNumerator; + +// AXMathFractionDenominator +- (id _Nullable)moxMathFractionDenominator; + +// AXMathLineThickness +- (NSNumber* _Nullable)moxMathLineThickness; + +// AXMathBase +- (id _Nullable)moxMathBase; + +// AXMathSubscript +- (id _Nullable)moxMathSubscript; + +// AXMathSuperscript +- (id _Nullable)moxMathSuperscript; + +// AXMathUnder +- (id _Nullable)moxMathUnder; + +// AXMathOver +- (id _Nullable)moxMathOver; + +// AXInvalid +- (NSString* _Nullable)moxInvalid; + +// AXSelectedText +- (NSString* _Nullable)moxSelectedText; + +// AXSelectedTextRange +- (NSValue* _Nullable)moxSelectedTextRange; + +// AXNumberOfCharacters +- (NSNumber* _Nullable)moxNumberOfCharacters; + +// AXVisibleCharacterRange +- (NSValue* _Nullable)moxVisibleCharacterRange; + +// AXInsertionPointLineNumber +- (NSNumber* _Nullable)moxInsertionPointLineNumber; + +// AXEditableAncestor +- (id _Nullable)moxEditableAncestor; + +// AXHighestEditableAncestor +- (id _Nullable)moxHighestEditableAncestor; + +// AXFocusableAncestor +- (id _Nullable)moxFocusableAncestor; + +// AXARIAAtomic +- (NSNumber* _Nullable)moxARIAAtomic; + +// AXARIALive +- (NSString* _Nullable)moxARIALive; + +// AXARIARelevant +- (NSString* _Nullable)moxARIARelevant; + +// AXMozDebugDescription +- (NSString* _Nullable)moxMozDebugDescription; + +#pragma mark - AttributeSetters + +// AXDisclosing +- (void)moxSetDisclosing:(NSNumber* _Nullable)disclosing; + +// AXValue +- (void)moxSetValue:(id _Nullable)value; + +// AXFocused +- (void)moxSetFocused:(NSNumber* _Nullable)focused; + +// AXSelected +- (void)moxSetSelected:(NSNumber* _Nullable)selected; + +// AXSelectedChildren +- (void)moxSetSelectedChildren:(NSArray* _Nullable)selectedChildren; + +// AXSelectedText +- (void)moxSetSelectedText:(NSString* _Nullable)selectedText; + +// AXSelectedTextRange +- (void)moxSetSelectedTextRange:(NSValue* _Nullable)selectedTextRange; + +// AXVisibleCharacterRange +- (void)moxSetVisibleCharacterRange:(NSValue* _Nullable)visibleCharacterRange; + +#pragma mark - Actions + +// AXPress +- (void)moxPerformPress; + +// AXShowMenu +- (void)moxPerformShowMenu; + +// AXScrollToVisible +- (void)moxPerformScrollToVisible; + +// AXIncrement +- (void)moxPerformIncrement; + +// AXDecrement +- (void)moxPerformDecrement; + +#pragma mark - ParameterizedAttributeGetters + +// AXLineForIndex +- (NSNumber* _Nullable)moxLineForIndex:(NSNumber* _Nonnull)index; + +// AXRangeForLine +- (NSValue* _Nullable)moxRangeForLine:(NSNumber* _Nonnull)line; + +// AXStringForRange +- (NSString* _Nullable)moxStringForRange:(NSValue* _Nonnull)range; + +// AXRangeForPosition +- (NSValue* _Nullable)moxRangeForPosition:(NSValue* _Nonnull)position; + +// AXRangeForIndex +- (NSValue* _Nullable)moxRangeForIndex:(NSNumber* _Nonnull)index; + +// AXBoundsForRange +- (NSValue* _Nullable)moxBoundsForRange:(NSValue* _Nonnull)range; + +// AXRTFForRange +- (NSData* _Nullable)moxRTFForRange:(NSValue* _Nonnull)range; + +// AXStyleRangeForIndex +- (NSValue* _Nullable)moxStyleRangeForIndex:(NSNumber* _Nonnull)index; + +// AttributedStringForRange +- (NSAttributedString* _Nullable)moxAttributedStringForRange: + (NSValue* _Nonnull)range; + +// AXUIElementsForSearchPredicate +- (NSArray* _Nullable)moxUIElementsForSearchPredicate: + (NSDictionary* _Nonnull)searchPredicate; + +// AXUIElementCountForSearchPredicate +- (NSNumber* _Nullable)moxUIElementCountForSearchPredicate: + (NSDictionary* _Nonnull)searchPredicate; + +// AXCellForColumnAndRow +- (id _Nullable)moxCellForColumnAndRow:(NSArray* _Nonnull)columnAndRow; + +// AXIndexForChildUIElement +- (NSNumber* _Nullable)moxIndexForChildUIElement:(id _Nonnull)child; + +@end + +// This protocol maps text marker and text marker range parameters to +// methods. It is implemented by a delegate of a MOXAccessible. +@protocol MOXTextMarkerSupport + +#pragma mark - TextAttributeGetters + +// AXStartTextMarker +- (id _Nullable)moxStartTextMarker; + +// AXEndTextMarker +- (id _Nullable)moxEndTextMarker; + +// AXSelectedTextMarkerRange +- (id _Nullable)moxSelectedTextMarkerRange; + +#pragma mark - ParameterizedTextAttributeGetters + +// AXLengthForTextMarkerRange +- (NSNumber* _Nullable)moxLengthForTextMarkerRange:(id _Nonnull)textMarkerRange; + +// AXStringForTextMarkerRange +- (NSString* _Nullable)moxStringForTextMarkerRange:(id _Nonnull)textMarkerRange; + +// AXTextMarkerRangeForUnorderedTextMarkers +- (id _Nullable)moxTextMarkerRangeForUnorderedTextMarkers: + (NSArray* _Nonnull)textMarkers; + +// AXLeftWordTextMarkerRangeForTextMarker +- (id _Nullable)moxLeftWordTextMarkerRangeForTextMarker:(id _Nonnull)textMarker; + +// AXRightWordTextMarkerRangeForTextMarker +- (id _Nullable)moxRightWordTextMarkerRangeForTextMarker: + (id _Nonnull)textMarker; + +// AXStartTextMarkerForTextMarkerRange +- (id _Nullable)moxStartTextMarkerForTextMarkerRange: + (id _Nonnull)textMarkerRange; + +// AXEndTextMarkerForTextMarkerRange +- (id _Nullable)moxEndTextMarkerForTextMarkerRange:(id _Nonnull)textMarkerRange; + +// AXNextTextMarkerForTextMarker +- (id _Nullable)moxNextTextMarkerForTextMarker:(id _Nonnull)textMarker; + +// AXPreviousTextMarkerForTextMarker +- (id _Nullable)moxPreviousTextMarkerForTextMarker:(id _Nonnull)textMarker; + +// AXAttributedStringForTextMarkerRange +- (NSAttributedString* _Nullable)moxAttributedStringForTextMarkerRange: + (id _Nonnull)textMarkerRange; + +// AXBoundsForTextMarkerRange +- (NSValue* _Nullable)moxBoundsForTextMarkerRange:(id _Nonnull)textMarkerRange; + +// AXIndexForTextMarker +- (NSNumber* _Nullable)moxIndexForTextMarker:(id _Nonnull)textMarker; + +// AXTextMarkerForIndex +- (id _Nullable)moxTextMarkerForIndex:(NSNumber* _Nonnull)index; + +// AXUIElementForTextMarker +- (id _Nullable)moxUIElementForTextMarker:(id _Nonnull)textMarker; + +// AXTextMarkerRangeForUIElement +- (id _Nullable)moxTextMarkerRangeForUIElement:(id _Nonnull)element; + +// AXLineTextMarkerRangeForTextMarker +- (id _Nullable)moxLineTextMarkerRangeForTextMarker:(id _Nonnull)textMarker; + +// AXLeftLineTextMarkerRangeForTextMarker +- (id _Nullable)moxLeftLineTextMarkerRangeForTextMarker:(id _Nonnull)textMarker; + +// AXRightLineTextMarkerRangeForTextMarker +- (id _Nullable)moxRightLineTextMarkerRangeForTextMarker: + (id _Nonnull)textMarker; + +// AXParagraphTextMarkerRangeForTextMarker +- (id _Nullable)moxParagraphTextMarkerRangeForTextMarker: + (id _Nonnull)textMarker; + +// AXStyleTextMarkerRangeForTextMarker +- (id _Nullable)moxStyleTextMarkerRangeForTextMarker:(id _Nonnull)textMarker; + +// AXMozDebugDescriptionForTextMarker +- (NSString* _Nullable)moxMozDebugDescriptionForTextMarker: + (id _Nonnull)textMarker; + +// AXMozDebugDescriptionForTextMarkerRange +- (NSString* _Nullable)moxMozDebugDescriptionForTextMarkerRange: + (id _Nonnull)textMarkerRange; + +#pragma mark - TextAttributeSetters + +// AXSelectedTextMarkerRange +- (void)moxSetSelectedTextMarkerRange:(id _Nullable)textMarkerRange; + +@end diff --git a/accessible/mac/MOXLandmarkAccessibles.h b/accessible/mac/MOXLandmarkAccessibles.h new file mode 100644 index 0000000000..bea44e7a8f --- /dev/null +++ b/accessible/mac/MOXLandmarkAccessibles.h @@ -0,0 +1,15 @@ +/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: + * 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=2: + */ +/* 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 "mozAccessible.h" + +@interface MOXLandmarkAccessible : mozAccessible +// overrides +- (NSString*)moxTitle; + +@end diff --git a/accessible/mac/MOXLandmarkAccessibles.mm b/accessible/mac/MOXLandmarkAccessibles.mm new file mode 100644 index 0000000000..4a3aa8f597 --- /dev/null +++ b/accessible/mac/MOXLandmarkAccessibles.mm @@ -0,0 +1,15 @@ +/* -*- (Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; + * c-basic-offset:) 2 -*- */ +/* 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 "MOXLandmarkAccessibles.h" + +@implementation MOXLandmarkAccessible + +- (NSString*)moxTitle { + return @""; +} + +@end diff --git a/accessible/mac/MOXMathAccessibles.h b/accessible/mac/MOXMathAccessibles.h new file mode 100644 index 0000000000..7661ad5c6a --- /dev/null +++ b/accessible/mac/MOXMathAccessibles.h @@ -0,0 +1,64 @@ +/* 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 "mozAccessible.h" + +@interface MOXMathRootAccessible : mozAccessible + +// overrides +- (id)moxMathRootRadicand; + +// overrides +- (id)moxMathRootIndex; + +@end + +@interface MOXMathSquareRootAccessible : mozAccessible + +// overrides +- (id)moxMathRootRadicand; + +@end + +@interface MOXMathFractionAccessible : mozAccessible + +// overrides +- (id)moxMathFractionNumerator; + +// overrides +- (id)moxMathFractionDenominator; + +// overrides +- (NSNumber*)moxMathLineThickness; + +@end + +@interface MOXMathSubSupAccessible : mozAccessible + +// overrides +- (id)moxMathBase; + +// overrides +- (id)moxMathSubscript; + +// overrides +- (id)moxMathSuperscript; + +@end + +@interface MOXMathUnderOverAccessible : mozAccessible + +// overrides +- (id)moxMathBase; + +// overrides +- (id)moxMathUnder; + +// overrides +- (id)moxMathOver; + +@end diff --git a/accessible/mac/MOXMathAccessibles.mm b/accessible/mac/MOXMathAccessibles.mm new file mode 100644 index 0000000000..1296becd93 --- /dev/null +++ b/accessible/mac/MOXMathAccessibles.mm @@ -0,0 +1,116 @@ +/* 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 "MOXMathAccessibles.h" + +#import "MacUtils.h" + +using namespace mozilla::a11y; + +// XXX WebKit also defines the following attributes. +// See bugs 1176970 and 1176983. +// - NSAccessibilityMathFencedOpenAttribute @"AXMathFencedOpen" +// - NSAccessibilityMathFencedCloseAttribute @"AXMathFencedClose" +// - NSAccessibilityMathPrescriptsAttribute @"AXMathPrescripts" +// - NSAccessibilityMathPostscriptsAttribute @"AXMathPostscripts" + +@implementation MOXMathRootAccessible + +- (id)moxMathRootRadicand { + return [self childAt:0]; +} + +- (id)moxMathRootIndex { + return [self childAt:1]; +} + +@end + +@implementation MOXMathSquareRootAccessible + +- (id)moxMathRootRadicand { + return [self childAt:0]; +} + +@end + +@implementation MOXMathFractionAccessible + +- (id)moxMathFractionNumerator { + return [self childAt:0]; +} + +- (id)moxMathFractionDenominator { + return [self childAt:1]; +} + +// Bug 1639745: This doesn't actually work. +- (NSNumber*)moxMathLineThickness { + // WebKit sets line thickness to some logical value parsed in the + // renderer object of the <mfrac> element. It's not clear whether the + // exact value is relevant to assistive technologies. From a semantic + // point of view, the only important point is to distinguish between + // <mfrac> elements that have a fraction bar and those that do not. + // Per the MathML 3 spec, the latter happens iff the linethickness + // attribute is of the form [zero-float][optional-unit]. In that case we + // set line thickness to zero and in the other cases we set it to one. + if (NSString* thickness = utils::GetAccAttr(self, "thickness")) { + NSNumberFormatter* formatter = + [[[NSNumberFormatter alloc] init] autorelease]; + NSNumber* value = [formatter numberFromString:thickness]; + return [NSNumber numberWithBool:[value boolValue]]; + } else { + return [NSNumber numberWithInteger:0]; + } +} + +@end + +@implementation MOXMathSubSupAccessible +- (id)moxMathBase { + return [self childAt:0]; +} + +- (id)moxMathSubscript { + if (mRole == roles::MATHML_SUP) { + return nil; + } + + return [self childAt:1]; +} + +- (id)moxMathSuperscript { + if (mRole == roles::MATHML_SUB) { + return nil; + } + + return [self childAt:mRole == roles::MATHML_SUP ? 1 : 2]; +} + +@end + +@implementation MOXMathUnderOverAccessible +- (id)moxMathBase { + return [self childAt:0]; +} + +- (id)moxMathUnder { + if (mRole == roles::MATHML_OVER) { + return nil; + } + + return [self childAt:1]; +} + +- (id)moxMathOver { + if (mRole == roles::MATHML_UNDER) { + return nil; + } + + return [self childAt:mRole == roles::MATHML_OVER ? 1 : 2]; +} +@end diff --git a/accessible/mac/MOXSearchInfo.h b/accessible/mac/MOXSearchInfo.h new file mode 100644 index 0000000000..8f5e6f414d --- /dev/null +++ b/accessible/mac/MOXSearchInfo.h @@ -0,0 +1,43 @@ +/* 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 "mozAccessible.h" +#include "Pivot.h" + +using namespace mozilla::a11y; + +@interface MOXSearchInfo : NSObject { + // The MOX accessible of the web area, we need a reference + // to set the pivot's root. This is a weak ref. + MOXAccessibleBase* mRoot; + + // The MOX accessible we should start searching from. + // This is a weak ref. + MOXAccessibleBase* mStartElem; + + // The amount of matches we should return + int mResultLimit; + + // The array of search keys to use during this search + NSMutableArray* mSearchKeys; + + // Set to YES if we should search forward, NO if backward + BOOL mSearchForward; + + // Set to YES if we should match on immediate descendants only, NO otherwise + BOOL mImmediateDescendantsOnly; + + NSString* mSearchText; +} + +- (id)initWithParameters:(NSDictionary*)params andRoot:(MOXAccessibleBase*)root; + +- (NSArray*)performSearch; + +- (void)dealloc; + +@end diff --git a/accessible/mac/MOXSearchInfo.mm b/accessible/mac/MOXSearchInfo.mm new file mode 100644 index 0000000000..c2cb07fced --- /dev/null +++ b/accessible/mac/MOXSearchInfo.mm @@ -0,0 +1,451 @@ +/* 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 "MOXSearchInfo.h" +#import "MOXWebAreaAccessible.h" +#import "RotorRules.h" + +#include "nsCocoaUtils.h" +#include "DocAccessibleParent.h" + +using namespace mozilla::a11y; + +@interface MOXSearchInfo () +- (NSArray*)getMatchesForRule:(PivotRule&)rule; + +- (NSArray<mozAccessible*>*)applyPostFilter:(NSArray<mozAccessible*>*)matches; + +- (AccessibleOrProxy)rootGeckoAccessible; + +- (AccessibleOrProxy)startGeckoAccessible; + +- (BOOL)shouldApplyPostFilter; +@end + +@implementation MOXSearchInfo + +- (id)initWithParameters:(NSDictionary*)params + andRoot:(MOXAccessibleBase*)root { + if (id searchKeyParam = [params objectForKey:@"AXSearchKey"]) { + mSearchKeys = [searchKeyParam isKindOfClass:[NSString class]] + ? @[ searchKeyParam ] + : searchKeyParam; + } + + if (id startElemParam = [params objectForKey:@"AXStartElement"]) { + mStartElem = startElemParam; + } else { + mStartElem = root; + } + + mRoot = root; + + mResultLimit = [[params objectForKey:@"AXResultsLimit"] intValue]; + + mSearchForward = + [[params objectForKey:@"AXDirection"] isEqualToString:@"AXDirectionNext"]; + + mImmediateDescendantsOnly = + [[params objectForKey:@"AXImmediateDescendantsOnly"] boolValue]; + + mSearchText = [params objectForKey:@"AXSearchText"]; + + return [super init]; +} + +- (AccessibleOrProxy)rootGeckoAccessible { + id root = + [mRoot isKindOfClass:[mozAccessible class]] ? mRoot : [mRoot moxParent]; + + return [static_cast<mozAccessible*>(root) geckoAccessible]; +} + +- (AccessibleOrProxy)startGeckoAccessible { + if ([mStartElem isKindOfClass:[mozAccessible class]]) { + return [static_cast<mozAccessible*>(mStartElem) geckoAccessible]; + } + + // If it isn't a mozAccessible, it doesn't have a gecko accessible + // this is most likely the root group. Use the gecko doc as the start + // accessible. + return [self rootGeckoAccessible]; +} + +- (NSArray*)getMatchesForRule:(PivotRule&)rule { + // If we will apply a post-filter, don't limit search so we + // don't come up short on the final result count. + int resultLimit = [self shouldApplyPostFilter] ? -1 : mResultLimit; + + NSMutableArray<mozAccessible*>* matches = [[NSMutableArray alloc] init]; + AccessibleOrProxy geckoRootAcc = [self rootGeckoAccessible]; + AccessibleOrProxy geckoStartAcc = [self startGeckoAccessible]; + Pivot p = Pivot(geckoRootAcc); + AccessibleOrProxy match; + if (mSearchForward) { + match = p.Next(geckoStartAcc, rule); + } else { + // Search backwards + if (geckoRootAcc == geckoStartAcc) { + // If we have no explicit start accessible, start from the last match. + match = p.Last(rule); + } else { + match = p.Prev(geckoStartAcc, rule); + } + } + + while (!match.IsNull() && resultLimit != 0) { + if (!mSearchForward && match == geckoRootAcc) { + // If searching backwards, don't include root. + break; + } + + // we use mResultLimit != 0 to capture the case where mResultLimit is -1 + // when it is set from the params dictionary. If that's true, we want + // to return all matches (ie. have no limit) + mozAccessible* nativeMatch = GetNativeFromGeckoAccessible(match); + if (nativeMatch) { + // only add/count results for which there is a matching + // native accessible + [matches addObject:nativeMatch]; + resultLimit -= 1; + } + + match = mSearchForward ? p.Next(match, rule) : p.Prev(match, rule); + } + + return [self applyPostFilter:matches]; +} + +- (BOOL)shouldApplyPostFilter { + // We currently only support AXSearchText as a post-search filter. + return !!mSearchText; +} + +- (NSArray<mozAccessible*>*)applyPostFilter:(NSArray<mozAccessible*>*)matches { + if (![self shouldApplyPostFilter]) { + return matches; + } + + NSMutableArray<mozAccessible*>* postMatches = [[NSMutableArray alloc] init]; + + nsString searchText; + nsCocoaUtils::GetStringForNSString(mSearchText, searchText); + + __block DocAccessibleParent* ipcDoc = nullptr; + __block nsTArray<uint64_t> accIds; + + [matches enumerateObjectsUsingBlock:^(mozAccessible* match, NSUInteger idx, + BOOL* stop) { + AccessibleOrProxy geckoAcc = [match geckoAccessible]; + if (geckoAcc.IsNull()) { + return; + } + + switch (geckoAcc.Role()) { + case roles::LANDMARK: + case roles::COMBOBOX: + case roles::LISTITEM: + case roles::COMBOBOX_LIST: + case roles::MENUBAR: + case roles::MENUPOPUP: + case roles::DOCUMENT: + case roles::APPLICATION: + // XXX: These roles either have AXTitle/AXDescription overridden as + // empty, or should never be returned in search text results. This + // should be integrated into a pivot rule in the future, and possibly + // better mapped somewhere. + return; + default: + break; + } + + if (geckoAcc.IsAccessible()) { + AccessibleWrap* acc = + static_cast<AccessibleWrap*>(geckoAcc.AsAccessible()); + if (acc->ApplyPostFilter(EWhichPostFilter::eContainsText, searchText)) { + if (mozAccessible* nativePostMatch = + GetNativeFromGeckoAccessible(acc)) { + [postMatches addObject:nativePostMatch]; + if (mResultLimit > 0 && + [postMatches count] >= static_cast<NSUInteger>(mResultLimit)) { + // If we reached the result limit, alter the `stop` pointer to YES + // to stop iteration. + *stop = YES; + } + } + } + + return; + } + + ProxyAccessible* proxy = geckoAcc.AsProxy(); + if (ipcDoc && + ((ipcDoc != proxy->Document()) || (idx + 1 == [matches count]))) { + // If the ipcDoc doesn't match the current proxy's doc, we crossed into a + // new document. ..or this is the last match. Apply the filter on the list + // of the current ipcDoc. + nsTArray<uint64_t> matchIds; + Unused << ipcDoc->GetPlatformExtension()->SendApplyPostSearchFilter( + accIds, mResultLimit, EWhichPostFilter::eContainsText, searchText, + &matchIds); + for (size_t i = 0; i < matchIds.Length(); i++) { + if (ProxyAccessible* postMatch = + ipcDoc->GetAccessible(matchIds.ElementAt(i))) { + if (mozAccessible* nativePostMatch = + GetNativeFromGeckoAccessible(postMatch)) { + [postMatches addObject:nativePostMatch]; + if (mResultLimit > 0 && + [postMatches count] >= static_cast<NSUInteger>(mResultLimit)) { + // If we reached the result limit, alter the `stop` pointer to YES + // to stop iteration. + *stop = YES; + return; + } + } + } + } + + ipcDoc = nullptr; + accIds.Clear(); + } + + if (!ipcDoc) { + ipcDoc = proxy->Document(); + } + accIds.AppendElement(proxy->ID()); + }]; + + return postMatches; +} + +- (NSArray*)performSearch { + AccessibleOrProxy geckoRootAcc = [self rootGeckoAccessible]; + AccessibleOrProxy geckoStartAcc = [self startGeckoAccessible]; + NSMutableArray* matches = [[NSMutableArray alloc] init]; + for (id key in mSearchKeys) { + if ([key isEqualToString:@"AXAnyTypeSearchKey"]) { + RotorRule rule = + mImmediateDescendantsOnly ? RotorRule(geckoRootAcc) : RotorRule(); + + if ([mStartElem isKindOfClass:[MOXWebAreaAccessible class]]) { + if (id rootGroup = + [static_cast<MOXWebAreaAccessible*>(mStartElem) rootGroup]) { + // Moving forward from web area, rootgroup; if it exists, is next. + [matches addObject:rootGroup]; + if (mResultLimit == 1) { + // Found one match, continue in search keys for block. + continue; + } + } + } + + if (mImmediateDescendantsOnly && mStartElem != mRoot && + [mStartElem isKindOfClass:[MOXRootGroup class]]) { + // Moving forward from root group. If we don't match descendants, + // there is no match. Continue. + continue; + } + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXHeadingSearchKey"]) { + RotorRoleRule rule = mImmediateDescendantsOnly + ? RotorRoleRule(roles::HEADING, geckoRootAcc) + : RotorRoleRule(roles::HEADING); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXArticleSearchKey"]) { + RotorRoleRule rule = mImmediateDescendantsOnly + ? RotorRoleRule(roles::ARTICLE, geckoRootAcc) + : RotorRoleRule(roles::ARTICLE); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXTableSearchKey"]) { + RotorRoleRule rule = mImmediateDescendantsOnly + ? RotorRoleRule(roles::TABLE, geckoRootAcc) + : RotorRoleRule(roles::TABLE); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXLandmarkSearchKey"]) { + RotorRoleRule rule = mImmediateDescendantsOnly + ? RotorRoleRule(roles::LANDMARK, geckoRootAcc) + : RotorRoleRule(roles::LANDMARK); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXListSearchKey"]) { + RotorRoleRule rule = mImmediateDescendantsOnly + ? RotorRoleRule(roles::LIST, geckoRootAcc) + : RotorRoleRule(roles::LIST); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXLinkSearchKey"]) { + RotorLinkRule rule = mImmediateDescendantsOnly + ? RotorLinkRule(geckoRootAcc) + : RotorLinkRule(); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXVisitedLinkSearchKey"]) { + RotorVisitedLinkRule rule = mImmediateDescendantsOnly + ? RotorVisitedLinkRule(geckoRootAcc) + : RotorVisitedLinkRule(); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXUnvisitedLinkSearchKey"]) { + RotorUnvisitedLinkRule rule = mImmediateDescendantsOnly + ? RotorUnvisitedLinkRule(geckoRootAcc) + : RotorUnvisitedLinkRule(); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXButtonSearchKey"]) { + RotorRoleRule rule = mImmediateDescendantsOnly + ? RotorRoleRule(roles::PUSHBUTTON, geckoRootAcc) + : RotorRoleRule(roles::PUSHBUTTON); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXControlSearchKey"]) { + RotorControlRule rule = mImmediateDescendantsOnly + ? RotorControlRule(geckoRootAcc) + : RotorControlRule(); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXSameTypeSearchKey"]) { + mozAccessible* native = GetNativeFromGeckoAccessible(geckoStartAcc); + NSString* macRole = [native moxRole]; + RotorMacRoleRule rule = mImmediateDescendantsOnly + ? RotorMacRoleRule(macRole, geckoRootAcc) + : RotorMacRoleRule(macRole); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXDifferentTypeSearchKey"]) { + mozAccessible* native = GetNativeFromGeckoAccessible(geckoStartAcc); + NSString* macRole = [native moxRole]; + RotorNotMacRoleRule rule = + mImmediateDescendantsOnly ? RotorNotMacRoleRule(macRole, geckoRootAcc) + : RotorNotMacRoleRule(macRole); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXRadioGroupSearchKey"]) { + RotorRoleRule rule = mImmediateDescendantsOnly + ? RotorRoleRule(roles::RADIO_GROUP, geckoRootAcc) + : RotorRoleRule(roles::RADIO_GROUP); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXFrameSearchKey"]) { + RotorRoleRule rule = mImmediateDescendantsOnly + ? RotorRoleRule(roles::DOCUMENT, geckoRootAcc) + : RotorRoleRule(roles::DOCUMENT); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXImageSearchKey"] || + [key isEqualToString:@"AXGraphicSearchKey"]) { + RotorRoleRule rule = mImmediateDescendantsOnly + ? RotorRoleRule(roles::GRAPHIC, geckoRootAcc) + : RotorRoleRule(roles::GRAPHIC); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXCheckboxSearchKey"]) { + RotorRoleRule rule = mImmediateDescendantsOnly + ? RotorRoleRule(roles::CHECKBUTTON, geckoRootAcc) + : RotorRoleRule(roles::CHECKBUTTON); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXStaticTextSearchKey"]) { + RotorStaticTextRule rule = mImmediateDescendantsOnly + ? RotorStaticTextRule(geckoRootAcc) + : RotorStaticTextRule(); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXHeadingLevel1SearchKey"]) { + RotorHeadingLevelRule rule = mImmediateDescendantsOnly + ? RotorHeadingLevelRule(1, geckoRootAcc) + : RotorHeadingLevelRule(1); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXHeadingLevel2SearchKey"]) { + RotorHeadingLevelRule rule = mImmediateDescendantsOnly + ? RotorHeadingLevelRule(2, geckoRootAcc) + : RotorHeadingLevelRule(2); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXHeadingLevel3SearchKey"]) { + RotorHeadingLevelRule rule = mImmediateDescendantsOnly + ? RotorHeadingLevelRule(3, geckoRootAcc) + : RotorHeadingLevelRule(3); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXHeadingLevel4SearchKey"]) { + RotorHeadingLevelRule rule = mImmediateDescendantsOnly + ? RotorHeadingLevelRule(4, geckoRootAcc) + : RotorHeadingLevelRule(4); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXHeadingLevel5SearchKey"]) { + RotorHeadingLevelRule rule = mImmediateDescendantsOnly + ? RotorHeadingLevelRule(5, geckoRootAcc) + : RotorHeadingLevelRule(5); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXHeadingLevel6SearchKey"]) { + RotorHeadingLevelRule rule = mImmediateDescendantsOnly + ? RotorHeadingLevelRule(6, geckoRootAcc) + : RotorHeadingLevelRule(6); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXBlockquoteSearchKey"]) { + RotorRoleRule rule = mImmediateDescendantsOnly + ? RotorRoleRule(roles::BLOCKQUOTE, geckoRootAcc) + : RotorRoleRule(roles::BLOCKQUOTE); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXTextFieldSearchKey"]) { + RotorTextEntryRule rule = mImmediateDescendantsOnly + ? RotorTextEntryRule(geckoRootAcc) + : RotorTextEntryRule(); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXLiveRegionSearchKey"]) { + RotorLiveRegionRule rule = mImmediateDescendantsOnly + ? RotorLiveRegionRule(geckoRootAcc) + : RotorLiveRegionRule(); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + } + + return matches; +} + +- (void)dealloc { + [mSearchKeys release]; + [super dealloc]; +} + +@end diff --git a/accessible/mac/MOXTextMarkerDelegate.h b/accessible/mac/MOXTextMarkerDelegate.h new file mode 100644 index 0000000000..cd5c78cb9a --- /dev/null +++ b/accessible/mac/MOXTextMarkerDelegate.h @@ -0,0 +1,158 @@ +/* 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 "MOXAccessibleProtocol.h" +#import "GeckoTextMarker.h" + +#include "AccessibleOrProxy.h" + +@interface MOXTextMarkerDelegate : NSObject <MOXTextMarkerSupport> { + mozilla::a11y::AccessibleOrProxy mGeckoDocAccessible; + id mSelection; + id mCaret; + id mPrevCaret; +} + ++ (id)getOrCreateForDoc:(mozilla::a11y::AccessibleOrProxy)aDoc; + ++ (void)destroyForDoc:(mozilla::a11y::AccessibleOrProxy)aDoc; + +- (id)initWithDoc:(mozilla::a11y::AccessibleOrProxy)aDoc; + +- (void)dealloc; + +- (void)setSelectionFrom:(mozilla::a11y::AccessibleOrProxy)startContainer + at:(int32_t)startOffset + to:(mozilla::a11y::AccessibleOrProxy)endContainer + at:(int32_t)endOffset; + +- (void)setCaretOffset:(mozilla::a11y::AccessibleOrProxy)container + at:(int32_t)offset; + +- (NSDictionary*)selectionChangeInfo; + +- (void)invalidateSelection; + +- (mozilla::a11y::GeckoTextMarkerRange)selection; + +// override +- (id)moxStartTextMarker; + +// override +- (id)moxEndTextMarker; + +// override +- (id)moxSelectedTextMarkerRange; + +// override +- (NSNumber*)moxLengthForTextMarkerRange:(id)textMarkerRange; + +// override +- (NSString*)moxStringForTextMarkerRange:(id)textMarkerRange; + +// override +- (id)moxTextMarkerRangeForUnorderedTextMarkers:(NSArray*)textMarkers; + +// override +- (id)moxStartTextMarkerForTextMarkerRange:(id)textMarkerRange; + +// override +- (id)moxEndTextMarkerForTextMarkerRange:(id)textMarkerRange; + +// override +- (id)moxLeftWordTextMarkerRangeForTextMarker:(id)textMarker; + +// override +- (id)moxRightWordTextMarkerRangeForTextMarker:(id)textMarker; + +// override +- (id)moxLineTextMarkerRangeForTextMarker:(id)textMarker; + +// override +- (id)moxLeftLineTextMarkerRangeForTextMarker:(id)textMarker; + +// override +- (id)moxRightLineTextMarkerRangeForTextMarker:(id)textMarker; + +// override +- (id)moxParagraphTextMarkerRangeForTextMarker:(id)textMarker; + +// override +- (id)moxStyleTextMarkerRangeForTextMarker:(id)textMarker; + +// override +- (id)moxNextTextMarkerForTextMarker:(id)textMarker; + +// override +- (id)moxPreviousTextMarkerForTextMarker:(id)textMarker; + +// override +- (NSAttributedString*)moxAttributedStringForTextMarkerRange: + (id)textMarkerRange; + +// override +- (NSValue*)moxBoundsForTextMarkerRange:(id)textMarkerRange; + +// override +- (id)moxUIElementForTextMarker:(id)textMarker; + +// override +- (id)moxTextMarkerRangeForUIElement:(id)element; + +// override +- (NSString*)moxMozDebugDescriptionForTextMarker:(id)textMarker; + +// override +- (void)moxSetSelectedTextMarkerRange:(id)textMarkerRange; + +@end + +namespace mozilla { +namespace a11y { + +enum AXTextEditType { + AXTextEditTypeUnknown, + AXTextEditTypeDelete, + AXTextEditTypeInsert, + AXTextEditTypeTyping, + AXTextEditTypeDictation, + AXTextEditTypeCut, + AXTextEditTypePaste, + AXTextEditTypeAttributesChange +}; + +enum AXTextStateChangeType { + AXTextStateChangeTypeUnknown, + AXTextStateChangeTypeEdit, + AXTextStateChangeTypeSelectionMove, + AXTextStateChangeTypeSelectionExtend +}; + +enum AXTextSelectionDirection { + AXTextSelectionDirectionUnknown, + AXTextSelectionDirectionBeginning, + AXTextSelectionDirectionEnd, + AXTextSelectionDirectionPrevious, + AXTextSelectionDirectionNext, + AXTextSelectionDirectionDiscontiguous +}; + +enum AXTextSelectionGranularity { + AXTextSelectionGranularityUnknown, + AXTextSelectionGranularityCharacter, + AXTextSelectionGranularityWord, + AXTextSelectionGranularityLine, + AXTextSelectionGranularitySentence, + AXTextSelectionGranularityParagraph, + AXTextSelectionGranularityPage, + AXTextSelectionGranularityDocument, + AXTextSelectionGranularityAll +}; +} +} diff --git a/accessible/mac/MOXTextMarkerDelegate.mm b/accessible/mac/MOXTextMarkerDelegate.mm new file mode 100644 index 0000000000..2aea7232b2 --- /dev/null +++ b/accessible/mac/MOXTextMarkerDelegate.mm @@ -0,0 +1,440 @@ +/* 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> + +#include "mozilla/Preferences.h" + +#import "MOXTextMarkerDelegate.h" + +using namespace mozilla::a11y; + +#define PREF_ACCESSIBILITY_MAC_DEBUG "accessibility.mac.debug" + +static nsDataHashtable<nsUint64HashKey, MOXTextMarkerDelegate*> sDelegates; + +@implementation MOXTextMarkerDelegate + ++ (id)getOrCreateForDoc:(mozilla::a11y::AccessibleOrProxy)aDoc { + MOZ_ASSERT(!aDoc.IsNull()); + + MOXTextMarkerDelegate* delegate = sDelegates.Get(aDoc.Bits()); + if (!delegate) { + delegate = [[MOXTextMarkerDelegate alloc] initWithDoc:aDoc]; + sDelegates.Put(aDoc.Bits(), delegate); + [delegate retain]; + } + + return delegate; +} + ++ (void)destroyForDoc:(mozilla::a11y::AccessibleOrProxy)aDoc { + MOZ_ASSERT(!aDoc.IsNull()); + + MOXTextMarkerDelegate* delegate = sDelegates.Get(aDoc.Bits()); + if (delegate) { + sDelegates.Remove(aDoc.Bits()); + [delegate release]; + } +} + +- (id)initWithDoc:(AccessibleOrProxy)aDoc { + MOZ_ASSERT(!aDoc.IsNull(), "Cannot init MOXTextDelegate with null"); + if ((self = [super init])) { + mGeckoDocAccessible = aDoc; + } + + return self; +} + +- (void)dealloc { + [self invalidateSelection]; + [super dealloc]; +} + +- (void)setSelectionFrom:(AccessibleOrProxy)startContainer + at:(int32_t)startOffset + to:(AccessibleOrProxy)endContainer + at:(int32_t)endOffset { + GeckoTextMarkerRange selection(GeckoTextMarker(startContainer, startOffset), + GeckoTextMarker(endContainer, endOffset)); + + // We store it as an AXTextMarkerRange because it is a safe + // way to keep a weak reference - when we need to use the + // range we can convert it back to a GeckoTextMarkerRange + // and check that it's valid. + mSelection = [selection.CreateAXTextMarkerRange() retain]; +} + +- (void)setCaretOffset:(mozilla::a11y::AccessibleOrProxy)container + at:(int32_t)offset { + GeckoTextMarker caretMarker(container, offset); + + mPrevCaret = mCaret; + mCaret = [caretMarker.CreateAXTextMarker() retain]; +} + +// This returns an info object to pass with AX SelectedTextChanged events. +// It uses the current and previous caret position to make decisions +// regarding which attributes to add to the info object. +- (NSDictionary*)selectionChangeInfo { + GeckoTextMarkerRange selectedGeckoRange = + GeckoTextMarkerRange(mGeckoDocAccessible, mSelection); + int32_t stateChangeType = selectedGeckoRange.mStart == selectedGeckoRange.mEnd + ? AXTextStateChangeTypeSelectionMove + : AXTextStateChangeTypeSelectionExtend; + + // This is the base info object, includes the selected marker range and + // the change type depending on the collapsed state of the selection. + NSMutableDictionary* info = [@{ + @"AXSelectedTextMarkerRange" : selectedGeckoRange.IsValid() ? mSelection + : [NSNull null], + @"AXTextStateChangeType" : @(stateChangeType), + } mutableCopy]; + + GeckoTextMarker caretMarker(mGeckoDocAccessible, mCaret); + GeckoTextMarker prevCaretMarker(mGeckoDocAccessible, mPrevCaret); + if (!caretMarker.IsValid()) { + // If the current caret is invalid, stop here and return base info. + return info; + } + + mozAccessible* caretEditable = + [GetNativeFromGeckoAccessible(caretMarker.mContainer) + moxEditableAncestor]; + + if (!caretEditable && stateChangeType == AXTextStateChangeTypeSelectionMove) { + // If we are not in an editable, VO expects AXTextStateSync to be present + // and true. + info[@"AXTextStateSync"] = @YES; + } + + if (!prevCaretMarker.IsValid() || caretMarker == prevCaretMarker) { + // If we have no stored previous marker, stop here. + return info; + } + + mozAccessible* prevCaretEditable = + [GetNativeFromGeckoAccessible(prevCaretMarker.mContainer) + moxEditableAncestor]; + + if (prevCaretEditable != caretEditable) { + // If the caret goes in or out of an editable, consider the + // move direction "discontiguous". + info[@"AXTextSelectionDirection"] = + @(AXTextSelectionDirectionDiscontiguous); + if ([[caretEditable moxFocused] boolValue]) { + // If the caret is in a new focused editable, VO expects this attribute to + // be present and to be true. + info[@"AXTextSelectionChangedFocus"] = @YES; + } + + return info; + } + + bool isForward = prevCaretMarker < caretMarker; + uint32_t deltaLength = + GeckoTextMarkerRange(isForward ? prevCaretMarker : caretMarker, + isForward ? caretMarker : prevCaretMarker) + .Length(); + + // Determine selection direction with marker comparison. + // If the delta between the two markers is more than one, consider it + // a word. Not accurate, but good enough for VO. + [info addEntriesFromDictionary:@{ + @"AXTextSelectionDirection" : isForward + ? @(AXTextSelectionDirectionNext) + : @(AXTextSelectionDirectionPrevious), + @"AXTextSelectionGranularity" : deltaLength == 1 + ? @(AXTextSelectionGranularityCharacter) + : @(AXTextSelectionGranularityWord) + }]; + + return info; +} + +- (void)invalidateSelection { + [mSelection release]; + [mCaret release]; + [mPrevCaret release]; + mSelection = nil; +} + +- (mozilla::a11y::GeckoTextMarkerRange)selection { + return mozilla::a11y::GeckoTextMarkerRange(mGeckoDocAccessible, mSelection); +} + +- (id)moxStartTextMarker { + GeckoTextMarker geckoTextPoint(mGeckoDocAccessible, 0); + return geckoTextPoint.CreateAXTextMarker(); +} + +- (id)moxEndTextMarker { + uint32_t characterCount = + mGeckoDocAccessible.IsProxy() + ? mGeckoDocAccessible.AsProxy()->CharacterCount() + : mGeckoDocAccessible.AsAccessible() + ->Document() + ->AsHyperText() + ->CharacterCount(); + GeckoTextMarker geckoTextPoint(mGeckoDocAccessible, characterCount); + return geckoTextPoint.CreateAXTextMarker(); +} + +- (id)moxSelectedTextMarkerRange { + return mSelection && + GeckoTextMarkerRange(mGeckoDocAccessible, mSelection).IsValid() + ? mSelection + : nil; +} + +- (NSString*)moxStringForTextMarkerRange:(id)textMarkerRange { + mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible, + textMarkerRange); + if (!range.IsValid()) { + return @""; + } + + return range.Text(); +} + +- (NSNumber*)moxLengthForTextMarkerRange:(id)textMarkerRange { + return @([[self moxStringForTextMarkerRange:textMarkerRange] length]); +} + +- (id)moxTextMarkerRangeForUnorderedTextMarkers:(NSArray*)textMarkers { + if ([textMarkers count] != 2) { + // Don't allow anything but a two member array. + return nil; + } + + GeckoTextMarker p1(mGeckoDocAccessible, textMarkers[0]); + GeckoTextMarker p2(mGeckoDocAccessible, textMarkers[1]); + + if (!p1.IsValid() || !p2.IsValid()) { + // If either marker is invalid, return nil. + return nil; + } + + bool ordered = p1 < p2; + GeckoTextMarkerRange range(ordered ? p1 : p2, ordered ? p2 : p1); + + return range.CreateAXTextMarkerRange(); +} + +- (id)moxStartTextMarkerForTextMarkerRange:(id)textMarkerRange { + mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible, + textMarkerRange); + + return range.IsValid() ? range.mStart.CreateAXTextMarker() : nil; +} + +- (id)moxEndTextMarkerForTextMarkerRange:(id)textMarkerRange { + mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible, + textMarkerRange); + + return range.IsValid() ? range.mEnd.CreateAXTextMarker() : nil; +} + +- (id)moxLeftWordTextMarkerRangeForTextMarker:(id)textMarker { + GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); + if (!geckoTextMarker.IsValid()) { + return nil; + } + + return geckoTextMarker.Range(EWhichRange::eLeftWord) + .CreateAXTextMarkerRange(); +} + +- (id)moxRightWordTextMarkerRangeForTextMarker:(id)textMarker { + GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); + if (!geckoTextMarker.IsValid()) { + return nil; + } + + return geckoTextMarker.Range(EWhichRange::eRightWord) + .CreateAXTextMarkerRange(); +} + +- (id)moxLineTextMarkerRangeForTextMarker:(id)textMarker { + GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); + if (!geckoTextMarker.IsValid()) { + return nil; + } + + return geckoTextMarker.Range(EWhichRange::eLine).CreateAXTextMarkerRange(); +} + +- (id)moxLeftLineTextMarkerRangeForTextMarker:(id)textMarker { + GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); + if (!geckoTextMarker.IsValid()) { + return nil; + } + + return geckoTextMarker.Range(EWhichRange::eLeftLine) + .CreateAXTextMarkerRange(); +} + +- (id)moxRightLineTextMarkerRangeForTextMarker:(id)textMarker { + GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); + if (!geckoTextMarker.IsValid()) { + return nil; + } + + return geckoTextMarker.Range(EWhichRange::eRightLine) + .CreateAXTextMarkerRange(); +} + +- (id)moxParagraphTextMarkerRangeForTextMarker:(id)textMarker { + GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); + if (!geckoTextMarker.IsValid()) { + return nil; + } + + return geckoTextMarker.Range(EWhichRange::eParagraph) + .CreateAXTextMarkerRange(); +} + +// override +- (id)moxStyleTextMarkerRangeForTextMarker:(id)textMarker { + GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); + if (!geckoTextMarker.IsValid()) { + return nil; + } + + return geckoTextMarker.Range(EWhichRange::eStyle).CreateAXTextMarkerRange(); +} + +- (id)moxNextTextMarkerForTextMarker:(id)textMarker { + GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); + if (!geckoTextMarker.IsValid()) { + return nil; + } + + if (!geckoTextMarker.Next()) { + return nil; + } + + return geckoTextMarker.CreateAXTextMarker(); +} + +- (id)moxPreviousTextMarkerForTextMarker:(id)textMarker { + GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); + if (!geckoTextMarker.IsValid()) { + return nil; + } + + if (!geckoTextMarker.Previous()) { + return nil; + } + + return geckoTextMarker.CreateAXTextMarker(); +} + +- (NSAttributedString*)moxAttributedStringForTextMarkerRange: + (id)textMarkerRange { + return [[[NSAttributedString alloc] + initWithString:[self moxStringForTextMarkerRange:textMarkerRange]] + autorelease]; +} + +- (NSValue*)moxBoundsForTextMarkerRange:(id)textMarkerRange { + mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible, + textMarkerRange); + if (!range.IsValid()) { + return nil; + } + + return range.Bounds(); +} + +- (NSNumber*)moxIndexForTextMarker:(id)textMarker { + GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); + if (!geckoTextMarker.IsValid()) { + return nil; + } + + GeckoTextMarkerRange range(GeckoTextMarker(mGeckoDocAccessible, 0), + geckoTextMarker); + + return @(range.Length()); +} + +- (id)moxTextMarkerForIndex:(NSNumber*)index { + GeckoTextMarker geckoTextMarker = GeckoTextMarker::MarkerFromIndex( + mGeckoDocAccessible, [index integerValue]); + if (!geckoTextMarker.IsValid()) { + return nil; + } + + return geckoTextMarker.CreateAXTextMarker(); +} + +- (id)moxUIElementForTextMarker:(id)textMarker { + GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); + if (!geckoTextMarker.IsValid()) { + return nil; + } + + AccessibleOrProxy leaf = geckoTextMarker.Leaf(); + if (leaf.IsNull()) { + return nil; + } + + return GetNativeFromGeckoAccessible(leaf); +} + +- (id)moxTextMarkerRangeForUIElement:(id)element { + if (![element isKindOfClass:[mozAccessible class]]) { + return nil; + } + + GeckoTextMarkerRange range([element geckoAccessible]); + return range.CreateAXTextMarkerRange(); +} + +- (NSString*)moxMozDebugDescriptionForTextMarker:(id)textMarker { + if (!Preferences::GetBool(PREF_ACCESSIBILITY_MAC_DEBUG)) { + return nil; + } + + GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); + if (!geckoTextMarker.IsValid()) { + return @"<GeckoTextMarker 0x0 [0]>"; + } + + return [NSString stringWithFormat:@"<GeckoTextMarker 0x%lx [%d]>", + geckoTextMarker.mContainer.Bits(), + geckoTextMarker.mOffset]; +} + +- (NSString*)moxMozDebugDescriptionForTextMarkerRange:(id)textMarkerRange { + if (!Preferences::GetBool(PREF_ACCESSIBILITY_MAC_DEBUG)) { + return nil; + } + + mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible, + textMarkerRange); + if (!range.IsValid()) { + return @"<GeckoTextMarkerRange 0x0 [0] - 0x0 [0]>"; + } + + return [NSString + stringWithFormat:@"<GeckoTextMarkerRange 0x%lx [%d] - 0x%lx [%d]>", + range.mStart.mContainer.Bits(), range.mStart.mOffset, + range.mEnd.mContainer.Bits(), range.mEnd.mOffset]; +} + +- (void)moxSetSelectedTextMarkerRange:(id)textMarkerRange { + mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible, + textMarkerRange); + if (range.IsValid()) { + range.Select(); + } +} + +@end diff --git a/accessible/mac/MOXWebAreaAccessible.h b/accessible/mac/MOXWebAreaAccessible.h new file mode 100644 index 0000000000..1ef11af50c --- /dev/null +++ b/accessible/mac/MOXWebAreaAccessible.h @@ -0,0 +1,105 @@ +/* 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 "mozAccessible.h" + +using namespace mozilla::a11y; + +@class MOXRootGroup; + +@interface MOXWebAreaAccessible : mozAccessible { + MOXRootGroup* mRootGroup; +} +// overrides +- (NSString*)moxRole; + +// overrides +- (NSString*)moxRoleDescription; + +// overrides +- (NSURL*)moxURL; + +// override +- (NSNumber*)moxLoaded; + +// override +- (NSNumber*)moxLoadingProgress; + +// override +- (NSArray*)moxLinkUIElements; + +// override +- (NSArray*)moxUnignoredChildren; + +// override +- (BOOL)moxBlockSelector:(SEL)selector; + +// override +- (void)moxPostNotification:(NSString*)notification; + +// override +- (void)handleAccessibleEvent:(uint32_t)eventType; + +// override +- (void)dealloc; + +- (NSArray*)rootGroupChildren; + +- (id)rootGroup; + +@end + +@interface MOXRootGroup : MOXAccessibleBase { + MOXWebAreaAccessible* mParent; +} + +// override +- (id)initWithParent:(MOXWebAreaAccessible*)parent; + +// override +- (NSString*)moxRole; + +// override +- (NSString*)moxRoleDescription; + +// override +- (id<mozAccessible>)moxParent; + +// override +- (NSArray*)moxChildren; + +// override +- (NSString*)moxIdentifier; + +// override +- (NSString*)moxSubrole; + +// override +- (id)moxHitTest:(NSPoint)point; + +// override +- (NSValue*)moxPosition; + +// override +- (NSValue*)moxSize; + +// override +- (NSArray*)moxUIElementsForSearchPredicate:(NSDictionary*)searchPredicate; + +// override +- (NSNumber*)moxUIElementCountForSearchPredicate:(NSDictionary*)searchPredicate; + +// override +- (BOOL)disableChild:(id)child; + +// override +- (void)expire; + +// override +- (BOOL)isExpired; + +@end diff --git a/accessible/mac/MOXWebAreaAccessible.mm b/accessible/mac/MOXWebAreaAccessible.mm new file mode 100644 index 0000000000..b91732325c --- /dev/null +++ b/accessible/mac/MOXWebAreaAccessible.mm @@ -0,0 +1,275 @@ +/* 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 "MOXWebAreaAccessible.h" + +#import "MOXSearchInfo.h" + +#include "nsCocoaUtils.h" +#include "DocAccessibleParent.h" + +using namespace mozilla::a11y; + +@implementation MOXRootGroup + +- (id)initWithParent:(MOXWebAreaAccessible*)parent { + // The parent is always a MOXWebAreaAccessible + mParent = parent; + return [super init]; +} + +- (NSString*)moxRole { + return NSAccessibilityGroupRole; +} + +- (NSString*)moxRoleDescription { + if ([[self moxSubrole] isEqualToString:@"AXLandmarkApplication"]) { + return utils::LocalizedString(u"application"_ns); + } + + return NSAccessibilityRoleDescription(NSAccessibilityGroupRole, nil); +} + +- (id<mozAccessible>)moxParent { + return mParent; +} + +- (NSArray*)moxChildren { + // Reparent the children of the web area here. + return [mParent rootGroupChildren]; +} + +- (NSString*)moxIdentifier { + // This is mostly for testing purposes to assert that this is the generated + // root group. + return @"root-group"; +} + +- (NSString*)moxSubrole { + // Steal the subrole internally mapped to the web area. + return [mParent moxSubrole]; +} + +- (id)moxHitTest:(NSPoint)point { + return [mParent moxHitTest:point]; +} + +- (NSValue*)moxPosition { + return [mParent moxPosition]; +} + +- (NSValue*)moxSize { + return [mParent moxSize]; +} + +- (NSArray*)moxUIElementsForSearchPredicate:(NSDictionary*)searchPredicate { + MOXSearchInfo* search = + [[MOXSearchInfo alloc] initWithParameters:searchPredicate andRoot:self]; + + return [search performSearch]; +} + +- (NSNumber*)moxUIElementCountForSearchPredicate: + (NSDictionary*)searchPredicate { + return [NSNumber + numberWithDouble:[[self moxUIElementsForSearchPredicate:searchPredicate] + count]]; +} + +- (BOOL)disableChild:(id)child { + return NO; +} + +- (void)expire { + mParent = nil; + [super expire]; +} + +- (BOOL)isExpired { + MOZ_ASSERT((mParent == nil) == mIsExpired); + + return [super isExpired]; +} + +@end + +@implementation MOXWebAreaAccessible + +- (NSString*)moxRole { + // The OS role is AXWebArea regardless of the gecko role + // (APPLICATION or DOCUMENT). + // If the web area has a role of APPLICATION, its root group will + // reflect that in a subrole/description. + return @"AXWebArea"; +} + +- (NSString*)moxRoleDescription { + // The role description is "HTML Content" regardless of the gecko role + // (APPLICATION or DOCUMENT) + return utils::LocalizedString(u"htmlContent"_ns); +} + +- (NSURL*)moxURL { + if ([self isExpired]) { + return nil; + } + + nsAutoString url; + if (mGeckoAccessible.IsAccessible()) { + MOZ_ASSERT(mGeckoAccessible.AsAccessible()->IsDoc()); + DocAccessible* acc = mGeckoAccessible.AsAccessible()->AsDoc(); + acc->URL(url); + } else { + ProxyAccessible* proxy = mGeckoAccessible.AsProxy(); + proxy->URL(url); + } + + if (url.IsEmpty()) { + return nil; + } + + return [NSURL URLWithString:nsCocoaUtils::ToNSString(url)]; +} + +- (NSNumber*)moxLoaded { + if ([self isExpired]) { + return nil; + } + // We are loaded if we aren't busy or stale + return @([self stateWithMask:(states::BUSY & states::STALE)] == 0); +} + +// overrides +- (NSNumber*)moxLoadingProgress { + if ([self isExpired]) { + return nil; + } + + if ([self stateWithMask:states::STALE] != 0) { + // We expose stale state until the document is ready (DOM is loaded and tree + // is constructed) so we indicate load hasn't started while this state is + // present. + return @0.0; + } + + if ([self stateWithMask:states::BUSY] != 0) { + // We expose state busy until the document and all its subdocuments are + // completely loaded, so we indicate partial loading here + return @0.5; + } + + // if we are not busy and not stale, we are loaded + return @1.0; +} + +- (NSArray*)moxLinkUIElements { + NSDictionary* searchPredicate = @{ + @"AXSearchKey" : @"AXLinkSearchKey", + @"AXImmediateDescendantsOnly" : @NO, + @"AXResultsLimit" : @(-1), + @"AXDirection" : @"AXDirectionNext", + }; + + return [self moxUIElementsForSearchPredicate:searchPredicate]; +} + +- (void)handleAccessibleEvent:(uint32_t)eventType { + switch (eventType) { + case nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE: + [self moxPostNotification: + NSAccessibilityFocusedUIElementChangedNotification]; + if ((mGeckoAccessible.IsProxy() && mGeckoAccessible.AsProxy()->IsDoc() && + mGeckoAccessible.AsProxy()->AsDoc()->IsTopLevel()) || + (mGeckoAccessible.IsAccessible() && + !mGeckoAccessible.AsAccessible()->IsRoot() && + mGeckoAccessible.AsAccessible() + ->AsDoc() + ->ParentDocument() + ->IsRoot())) { + // we fire an AXLoadComplete event on top-level documents only + [self moxPostNotification:@"AXLoadComplete"]; + } else { + // otherwise the doc belongs to an iframe (IsTopLevelInContentProcess) + // and we fire AXLayoutComplete instead + [self moxPostNotification:@"AXLayoutComplete"]; + } + break; + } + + [super handleAccessibleEvent:eventType]; +} + +- (NSArray*)rootGroupChildren { + // This method is meant to expose the doc's children to the root group. + return [super moxChildren]; +} + +- (NSArray*)moxUnignoredChildren { + if (id rootGroup = [self rootGroup]) { + return @[ [self rootGroup] ]; + } + + // There is no root group, expose the children here directly. + return [super moxUnignoredChildren]; +} + +- (BOOL)moxBlockSelector:(SEL)selector { + if (selector == @selector(moxSubrole)) { + // Never expose a subrole for a web area. + return YES; + } + + if (selector == @selector(moxElementBusy)) { + // Don't confuse aria-busy with a document's busy state. + return YES; + } + + return [super moxBlockSelector:selector]; +} + +- (void)moxPostNotification:(NSString*)notification { + if (![notification isEqualToString:@"AXElementBusyChanged"]) { + // Suppress AXElementBusyChanged since it uses gecko's BUSY state + // to tell VoiceOver about aria-busy changes. We use that state + // differently in documents. + [super moxPostNotification:notification]; + } +} + +- (id)rootGroup { + NSArray* children = [super moxUnignoredChildren]; + if (mRole != roles::APPLICATION && [children count] == 1 && + [[[children firstObject] moxUnignoredChildren] count] != 0) { + // We only need a root group if our document: + // (1) has multiple children, or + // (2) a one child that is a leaf, or + // (3) has a role of APPLICATION + return nil; + } + + if (!mRootGroup) { + mRootGroup = [[MOXRootGroup alloc] initWithParent:self]; + } + + return mRootGroup; +} + +- (void)expire { + [mRootGroup expire]; + [super expire]; +} + +- (void)dealloc { + // This object can only be dealoced after the gecko accessible wrapper + // reference is released, and that happens after expire is called. + MOZ_ASSERT([self isExpired]); + [mRootGroup release]; + + [super dealloc]; +} + +@end diff --git a/accessible/mac/MacUtils.h b/accessible/mac/MacUtils.h new file mode 100644 index 0000000000..fdb146df25 --- /dev/null +++ b/accessible/mac/MacUtils.h @@ -0,0 +1,43 @@ +/* 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/. */ + +#ifndef _MacUtils_H_ +#define _MacUtils_H_ + +#include "nsStringFwd.h" + +@class NSString; +@class mozAccessible; + +namespace mozilla { +namespace a11y { +namespace utils { + +// convert an array of Gecko accessibles to an NSArray of native accessibles +NSArray<mozAccessible*>* ConvertToNSArray(nsTArray<Accessible*>& aArray); + +// convert an array of Gecko proxy accessibles to an NSArray of native +// accessibles +NSArray<mozAccessible*>* ConvertToNSArray(nsTArray<ProxyAccessible*>& aArray); + +/** + * Get a localized string from the string bundle. + * Return nil if not found. + */ +NSString* LocalizedString(const nsString& aString); + +/** + * Gets an accessible atttribute from the mozAccessible's associated + * accessible wrapper or proxy, and returns the value as an NSString. + * nil if no attribute is found. + */ +NSString* GetAccAttr(mozAccessible* aNativeAccessible, const char* aAttrName); +} +} +} + +#endif diff --git a/accessible/mac/MacUtils.mm b/accessible/mac/MacUtils.mm new file mode 100644 index 0000000000..a6c906413c --- /dev/null +++ b/accessible/mac/MacUtils.mm @@ -0,0 +1,89 @@ +/* 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 "MacUtils.h" + +#include "Accessible.h" +#include "nsCocoaUtils.h" +#include "mozilla/a11y/PDocAccessible.h" +#include "nsIPersistentProperties2.h" + +namespace mozilla { +namespace a11y { +namespace utils { + +// convert an array of Gecko accessibles to an NSArray of native accessibles +NSArray<mozAccessible*>* ConvertToNSArray(nsTArray<Accessible*>& aArray) { + NSMutableArray* nativeArray = [[NSMutableArray alloc] init]; + + // iterate through the list, and get each native accessible. + size_t totalCount = aArray.Length(); + for (size_t i = 0; i < totalCount; i++) { + Accessible* curAccessible = aArray.ElementAt(i); + mozAccessible* curNative = GetNativeFromGeckoAccessible(curAccessible); + if (curNative) + [nativeArray addObject:GetObjectOrRepresentedView(curNative)]; + } + + return nativeArray; +} + +// convert an array of Gecko proxy accessibles to an NSArray of native +// accessibles +NSArray<mozAccessible*>* ConvertToNSArray(nsTArray<ProxyAccessible*>& aArray) { + NSMutableArray* nativeArray = [[NSMutableArray alloc] init]; + + // iterate through the list, and get each native accessible. + size_t totalCount = aArray.Length(); + for (size_t i = 0; i < totalCount; i++) { + ProxyAccessible* curAccessible = aArray.ElementAt(i); + mozAccessible* curNative = GetNativeFromGeckoAccessible(curAccessible); + if (curNative) + [nativeArray addObject:GetObjectOrRepresentedView(curNative)]; + } + + return nativeArray; +} + +/** + * Get a localized string from the a11y string bundle. + * Return nil if not found. + */ +NSString* LocalizedString(const nsString& aString) { + nsString text; + + Accessible::TranslateString(aString, text); + + return text.IsEmpty() ? nil : nsCocoaUtils::ToNSString(text); +} + +NSString* GetAccAttr(mozAccessible* aNativeAccessible, const char* aAttrName) { + nsAutoString result; + if (Accessible* acc = [aNativeAccessible geckoAccessible].AsAccessible()) { + nsCOMPtr<nsIPersistentProperties> attributes = acc->Attributes(); + attributes->GetStringProperty(nsCString(aAttrName), result); + } else if (ProxyAccessible* proxy = + [aNativeAccessible geckoAccessible].AsProxy()) { + AutoTArray<Attribute, 10> attrs; + proxy->Attributes(&attrs); + for (size_t i = 0; i < attrs.Length(); i++) { + if (attrs.ElementAt(i).Name() == aAttrName) { + result = attrs.ElementAt(i).Value(); + break; + } + } + } + + if (!result.IsEmpty()) { + return nsCocoaUtils::ToNSString(result); + } + + return nil; +} +} +} +} diff --git a/accessible/mac/Platform.mm b/accessible/mac/Platform.mm new file mode 100644 index 0000000000..47ef41fe20 --- /dev/null +++ b/accessible/mac/Platform.mm @@ -0,0 +1,212 @@ +/* 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 "ProxyAccessible.h" +#include "AccessibleOrProxy.h" +#include "DocAccessibleParent.h" +#include "mozTableAccessible.h" +#include "MOXWebAreaAccessible.h" + +#include "nsAppShell.h" +#include "mozilla/Telemetry.h" + +// Available from 10.13 onwards; test availability at runtime before using +@interface NSWorkspace (AvailableSinceHighSierra) +@property(readonly) BOOL isVoiceOverEnabled; +@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(ProxyAccessible* aProxy, uint32_t) { + 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 ProxyAccessible::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 { + type = GetTypeFromRole(aProxy->Role()); + } + + mozAccessible* mozWrapper = [[type alloc] initWithAccessible:aProxy]; + aProxy->SetWrapper(reinterpret_cast<uintptr_t>(mozWrapper)); +} + +void ProxyDestroyed(ProxyAccessible* aProxy) { + mozAccessible* wrapper = GetNativeFromGeckoAccessible(aProxy); + [wrapper expire]; + [wrapper release]; + aProxy->SetWrapper(0); + + if (aProxy->IsDoc()) { + [MOXTextMarkerDelegate destroyForDoc:aProxy]; + } +} + +void ProxyEvent(ProxyAccessible* aProxy, uint32_t aEventType) { + // Ignore event that we don't escape below, they aren't yet supported. + if (aEventType != nsIAccessibleEvent::EVENT_FOCUS && + aEventType != nsIAccessibleEvent::EVENT_VALUE_CHANGE && + aEventType != nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE && + aEventType != nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED && + 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_NAME_CHANGE) + return; + + mozAccessible* wrapper = GetNativeFromGeckoAccessible(aProxy); + if (wrapper) { + [wrapper handleAccessibleEvent:aEventType]; + } +} + +void ProxyStateChangeEvent(ProxyAccessible* aProxy, uint64_t aState, + bool aEnabled) { + mozAccessible* wrapper = GetNativeFromGeckoAccessible(aProxy); + if (wrapper) { + [wrapper stateChanged:aState isEnabled:aEnabled]; + } +} + +void ProxyCaretMoveEvent(ProxyAccessible* aTarget, int32_t aOffset, + bool aIsSelectionCollapsed) { + mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget); + MOXTextMarkerDelegate* delegate = + [MOXTextMarkerDelegate getOrCreateForDoc:aTarget->Document()]; + [delegate setCaretOffset:aTarget at:aOffset]; + 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 ProxyTextChangeEvent(ProxyAccessible* aTarget, const nsString& aStr, + int32_t aStart, uint32_t aLen, bool aIsInsert, + bool aFromUser) { + ProxyAccessible* 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 ProxyShowHideEvent(ProxyAccessible*, ProxyAccessible*, bool, bool) {} + +void ProxySelectionEvent(ProxyAccessible* aTarget, ProxyAccessible* aWidget, + uint32_t aEventType) { + mozAccessible* wrapper = GetNativeFromGeckoAccessible(aWidget); + if (wrapper) { + [wrapper handleAccessibleEvent:aEventType]; + } +} + +void ProxyTextSelectionChangeEvent(ProxyAccessible* aTarget, + const nsTArray<TextRangeData>& aSelection) { + if (aSelection.Length()) { + MOXTextMarkerDelegate* delegate = + [MOXTextMarkerDelegate getOrCreateForDoc:aTarget->Document()]; + DocAccessibleParent* doc = aTarget->Document(); + ProxyAccessible* startContainer = + doc->GetAccessible(aSelection[0].StartID()); + ProxyAccessible* endContainer = doc->GetAccessible(aSelection[0].EndID()); + // Cache the selection. + [delegate setSelectionFrom:startContainer + at:aSelection[0].StartOffset() + to:endContainer + at:aSelection[0].EndOffset()]; + } + + mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget); + if (wrapper) { + [wrapper + handleAccessibleEvent:nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED]; + } +} + +void ProxyRoleChangedEvent(ProxyAccessible* aTarget, const a11y::role& aRole) { + if (mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget)) { + [wrapper handleRoleChanged:aRole]; + } +} + +} // namespace a11y +} // namespace mozilla + +@interface GeckoNSApplication (a11y) +- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute; +@end + +@implementation GeckoNSApplication (a11y) + +- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute { + if ([attribute isEqualToString:@"AXEnhancedUserInterface"]) { + mozilla::a11y::sA11yShouldBeEnabled = ([value intValue] == 1); +#if defined(MOZ_TELEMETRY_REPORTING) + if ([[NSWorkspace sharedWorkspace] + respondsToSelector:@selector(isVoiceOverEnabled)] && + [[NSWorkspace sharedWorkspace] isVoiceOverEnabled]) { + Telemetry::ScalarSet(Telemetry::ScalarID::A11Y_INSTANTIATORS, + u"VoiceOver"_ns); + } +#endif // defined(MOZ_TELEMETRY_REPORTING) + } + + return [super accessibilitySetValue:value forAttribute:attribute]; +} + +@end diff --git a/accessible/mac/PlatformExtTypes.h b/accessible/mac/PlatformExtTypes.h new file mode 100644 index 0000000000..fdd33a4341 --- /dev/null +++ b/accessible/mac/PlatformExtTypes.h @@ -0,0 +1,27 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozilla_a11y_PlatformExtTypes_h__ +#define mozilla_a11y_PlatformExtTypes_h__ + +namespace mozilla { +namespace a11y { + +enum class EWhichRange { + eLeftWord, + eRightWord, + eLine, + eLeftLine, + eRightLine, + eParagraph, + eStyle +}; + +enum class EWhichPostFilter { eContainsText }; + +} // namespace a11y +} // namespace mozilla + +#endif // mozilla_a11y_PlatformExtTypes_h__ diff --git a/accessible/mac/RootAccessibleWrap.h b/accessible/mac/RootAccessibleWrap.h new file mode 100644 index 0000000000..0d165e3236 --- /dev/null +++ b/accessible/mac/RootAccessibleWrap.h @@ -0,0 +1,36 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +/* For documentation of the accessibility architecture, + * see http://lxr.mozilla.org/seamonkey/source/accessible/accessible-docs.html + */ + +#ifndef mozilla_a11y_RootAccessibleWrap_h__ +#define mozilla_a11y_RootAccessibleWrap_h__ + +#include "RootAccessible.h" + +namespace mozilla { + +class PresShell; + +namespace a11y { + +class RootAccessibleWrap : public RootAccessible { + public: + RootAccessibleWrap(dom::Document* aDocument, PresShell* aPresShell); + virtual ~RootAccessibleWrap(); + + Class GetNativeType(); + + // let's our native accessible get in touch with the + // native cocoa view that is our accessible parent. + void GetNativeWidget(void** aOutView); +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/mac/RootAccessibleWrap.mm b/accessible/mac/RootAccessibleWrap.mm new file mode 100644 index 0000000000..4086b3bf30 --- /dev/null +++ b/accessible/mac/RootAccessibleWrap.mm @@ -0,0 +1,51 @@ +/* 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/. */ + +#include "RootAccessibleWrap.h" + +#include "mozRootAccessible.h" + +#include "gfxPlatform.h" +#include "nsCOMPtr.h" +#include "nsObjCExceptions.h" +#include "nsIFrame.h" +#include "nsView.h" +#include "nsIWidget.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +RootAccessibleWrap::RootAccessibleWrap(dom::Document* aDocument, + PresShell* aPresShell) + : RootAccessible(aDocument, aPresShell) {} + +RootAccessibleWrap::~RootAccessibleWrap() {} + +Class RootAccessibleWrap::GetNativeType() { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + return [mozRootAccessible class]; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +void RootAccessibleWrap::GetNativeWidget(void** aOutView) { + nsIFrame* frame = GetFrame(); + if (frame) { + nsView* view = frame->GetView(); + if (view) { + nsIWidget* widget = view->GetWidget(); + if (widget) { + *aOutView = (void**)widget->GetNativeData(NS_NATIVE_WIDGET); + MOZ_ASSERT( + *aOutView || gfxPlatform::IsHeadless(), + "Couldn't get the native NSView parent we need to connect the " + "accessibility hierarchy!"); + } + } + } +} diff --git a/accessible/mac/RotorRules.h b/accessible/mac/RotorRules.h new file mode 100644 index 0000000000..e158d6e764 --- /dev/null +++ b/accessible/mac/RotorRules.h @@ -0,0 +1,142 @@ +/* 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 "mozAccessible.h" +#include "Pivot.h" + +using namespace mozilla::a11y; + +/** + * This rule matches all accessibles that satisfy the "boilerplate" + * pivot conditions and have a corresponding native accessible. + */ +class RotorRule : public PivotRule { + public: + explicit RotorRule(AccessibleOrProxy& aDirectDescendantsFrom); + explicit RotorRule(); + uint16_t Match(const AccessibleOrProxy& aAccOrProxy) override; + + private: + AccessibleOrProxy mDirectDescendantsFrom; +}; + +/** + * This rule matches all accessibles of a given role. + */ +class RotorRoleRule : public RotorRule { + public: + explicit RotorRoleRule(role aRole, AccessibleOrProxy& aDirectDescendantsFrom); + explicit RotorRoleRule(role aRole); + uint16_t Match(const AccessibleOrProxy& aAccOrProxy) override; + + private: + role mRole; +}; + +class RotorMacRoleRule : public RotorRule { + public: + explicit RotorMacRoleRule(NSString* aRole); + explicit RotorMacRoleRule(NSString* aRole, + AccessibleOrProxy& aDirectDescendantsFrom); + ~RotorMacRoleRule(); + virtual uint16_t Match(const AccessibleOrProxy& aAccOrProxy) override; + + protected: + NSString* mMacRole; +}; + +class RotorControlRule final : public RotorRule { + public: + explicit RotorControlRule(AccessibleOrProxy& aDirectDescendantsFrom); + explicit RotorControlRule(); + + virtual uint16_t Match(const AccessibleOrProxy& aAccOrProxy) override; +}; + +class RotorTextEntryRule final : public RotorRule { + public: + explicit RotorTextEntryRule(AccessibleOrProxy& aDirectDescendantsFrom); + explicit RotorTextEntryRule(); + + virtual uint16_t Match(const AccessibleOrProxy& aAccOrProxy) override; +}; + +class RotorLinkRule : public RotorRule { + public: + explicit RotorLinkRule(); + explicit RotorLinkRule(AccessibleOrProxy& aDirectDescendantsFrom); + + virtual uint16_t Match(const AccessibleOrProxy& aAccOrProxy) override; +}; + +class RotorVisitedLinkRule final : public RotorLinkRule { + public: + explicit RotorVisitedLinkRule(); + explicit RotorVisitedLinkRule(AccessibleOrProxy& aDirectDescendantsFrom); + + virtual uint16_t Match(const AccessibleOrProxy& aAccOrProxy) override; +}; + +class RotorUnvisitedLinkRule final : public RotorLinkRule { + public: + explicit RotorUnvisitedLinkRule(); + explicit RotorUnvisitedLinkRule(AccessibleOrProxy& aDirectDescendantsFrom); + + virtual uint16_t Match(const AccessibleOrProxy& aAccOrProxy) override; +}; + +/** + * This rule matches all accessibles that satisfy the "boilerplate" + * pivot conditions and have a corresponding native accessible. + */ +class RotorNotMacRoleRule : public RotorMacRoleRule { + public: + explicit RotorNotMacRoleRule(NSString* aMacRole, + AccessibleOrProxy& aDirectDescendantsFrom); + explicit RotorNotMacRoleRule(NSString* aMacRole); + uint16_t Match(const AccessibleOrProxy& aAccOrProxy) override; +}; + +class RotorStaticTextRule : public RotorRule { + public: + explicit RotorStaticTextRule(); + explicit RotorStaticTextRule(AccessibleOrProxy& aDirectDescendantsFrom); + + virtual uint16_t Match(const AccessibleOrProxy& aAccOrProxy) override; +}; + +class RotorHeadingLevelRule : public RotorRoleRule { + public: + explicit RotorHeadingLevelRule(int32_t aLevel); + explicit RotorHeadingLevelRule(int32_t aLevel, + AccessibleOrProxy& aDirectDescendantsFrom); + + virtual uint16_t Match(const AccessibleOrProxy& aAccOrProxy) override; + + private: + int32_t mLevel; +}; + +class RotorLiveRegionRule : public RotorRule { + public: + explicit RotorLiveRegionRule(AccessibleOrProxy& aDirectDescendantsFrom) + : RotorRule(aDirectDescendantsFrom) {} + explicit RotorLiveRegionRule() : RotorRule() {} + + uint16_t Match(const AccessibleOrProxy& aAccOrProxy) override; +}; + +/** + * This rule matches all accessibles with roles::OUTLINEITEM. If + * outlines are nested, it ignores the nested subtree and returns + * only items which are descendants of the primary outline. + */ +class OutlineRule : public RotorRule { + public: + explicit OutlineRule(); + uint16_t Match(const AccessibleOrProxy& aAccOrProxy) override; +}; diff --git a/accessible/mac/RotorRules.mm b/accessible/mac/RotorRules.mm new file mode 100644 index 0000000000..e42d768608 --- /dev/null +++ b/accessible/mac/RotorRules.mm @@ -0,0 +1,370 @@ +/* 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 "RotorRules.h" + +#include "nsCocoaUtils.h" +#include "DocAccessibleParent.h" + +using namespace mozilla::a11y; + +// Generic Rotor Rule + +RotorRule::RotorRule(AccessibleOrProxy& aDirectDescendantsFrom) + : mDirectDescendantsFrom(aDirectDescendantsFrom) {} + +RotorRule::RotorRule() : mDirectDescendantsFrom(nullptr) {} + +uint16_t RotorRule::Match(const AccessibleOrProxy& aAccOrProxy) { + uint16_t result = nsIAccessibleTraversalRule::FILTER_IGNORE; + + if (nsAccUtils::MustPrune(aAccOrProxy)) { + result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + + if (!mDirectDescendantsFrom.IsNull() && + (aAccOrProxy != mDirectDescendantsFrom)) { + result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + + if ([GetNativeFromGeckoAccessible(aAccOrProxy) isAccessibilityElement]) { + result |= nsIAccessibleTraversalRule::FILTER_MATCH; + } + + return result; +} + +// Rotor Role Rule + +RotorRoleRule::RotorRoleRule(role aRole, + AccessibleOrProxy& aDirectDescendantsFrom) + : RotorRule(aDirectDescendantsFrom), mRole(aRole){}; + +RotorRoleRule::RotorRoleRule(role aRole) : RotorRule(), mRole(aRole){}; + +uint16_t RotorRoleRule::Match(const AccessibleOrProxy& aAccOrProxy) { + uint16_t result = RotorRule::Match(aAccOrProxy); + + // if a match was found in the base-class's Match function, + // it is valid to consider that match again here. if it is + // not of the desired role, we flip the match bit to "unmatch" + // otherwise, the match persists. + if ((result & nsIAccessibleTraversalRule::FILTER_MATCH) && + aAccOrProxy.Role() != mRole) { + result &= ~nsIAccessibleTraversalRule::FILTER_MATCH; + } + + return result; +} + +// Rotor Mac Role Rule + +RotorMacRoleRule::RotorMacRoleRule(NSString* aMacRole, + AccessibleOrProxy& aDirectDescendantsFrom) + : RotorRule(aDirectDescendantsFrom), mMacRole(aMacRole) { + [mMacRole retain]; +}; + +RotorMacRoleRule::RotorMacRoleRule(NSString* aMacRole) + : RotorRule(), mMacRole(aMacRole) { + [mMacRole retain]; +}; + +RotorMacRoleRule::~RotorMacRoleRule() { [mMacRole release]; } + +uint16_t RotorMacRoleRule::Match(const AccessibleOrProxy& aAccOrProxy) { + uint16_t result = RotorRule::Match(aAccOrProxy); + + // if a match was found in the base-class's Match function, + // it is valid to consider that match again here. if it is + // not of the desired role, we flip the match bit to "unmatch" + // otherwise, the match persists. + if ((result & nsIAccessibleTraversalRule::FILTER_MATCH)) { + mozAccessible* nativeMatch = GetNativeFromGeckoAccessible(aAccOrProxy); + if (![[nativeMatch moxRole] isEqualToString:mMacRole]) { + result &= ~nsIAccessibleTraversalRule::FILTER_MATCH; + } + } + + return result; +} + +// Rotor Control Rule + +RotorControlRule::RotorControlRule(AccessibleOrProxy& aDirectDescendantsFrom) + : RotorRule(aDirectDescendantsFrom){}; + +RotorControlRule::RotorControlRule() : RotorRule(){}; + +uint16_t RotorControlRule::Match(const AccessibleOrProxy& aAccOrProxy) { + uint16_t result = RotorRule::Match(aAccOrProxy); + + // if a match was found in the base-class's Match function, + // it is valid to consider that match again here. if it is + // not of the desired role, we flip the match bit to "unmatch" + // otherwise, the match persists. + if ((result & nsIAccessibleTraversalRule::FILTER_MATCH)) { + switch (aAccOrProxy.Role()) { + case roles::PUSHBUTTON: + case roles::SPINBUTTON: + case roles::DETAILS: + case roles::CHECKBUTTON: + case roles::COLOR_CHOOSER: + case roles::BUTTONDROPDOWNGRID: // xul colorpicker + case roles::LISTBOX: + case roles::COMBOBOX: + case roles::EDITCOMBOBOX: + case roles::RADIOBUTTON: + case roles::RADIO_GROUP: + case roles::PAGETAB: + case roles::SLIDER: + case roles::SWITCH: + case roles::ENTRY: + case roles::OUTLINE: + case roles::PASSWORD_TEXT: + return result; + + case roles::DATE_EDITOR: + case roles::TIME_EDITOR: + result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + return result; + + case roles::GROUPING: { + // Groupings are sometimes used (like radio groups) to denote + // sets of controls. If that's the case, we want to surface + // them. We also want to surface grouped time and date controls. + for (unsigned int i = 0; i < aAccOrProxy.ChildCount(); i++) { + AccessibleOrProxy currChild = aAccOrProxy.ChildAt(i); + if (currChild.Role() == roles::CHECKBUTTON || + currChild.Role() == roles::SWITCH || + currChild.Role() == roles::SPINBUTTON || + currChild.Role() == roles::RADIOBUTTON) { + return result; + } + } + + // if we iterated through the groups children and didn't + // find a control with one of the roles above, we should + // ignore this grouping + result &= ~nsIAccessibleTraversalRule::FILTER_MATCH; + return result; + } + + default: + // if we did not match on any above role, we should + // ignore this accessible. + result &= ~nsIAccessibleTraversalRule::FILTER_MATCH; + } + } + + return result; +} + +// Rotor TextEntry Rule + +RotorTextEntryRule::RotorTextEntryRule( + AccessibleOrProxy& aDirectDescendantsFrom) + : RotorRule(aDirectDescendantsFrom){}; + +RotorTextEntryRule::RotorTextEntryRule() : RotorRule(){}; + +uint16_t RotorTextEntryRule::Match(const AccessibleOrProxy& aAccOrProxy) { + uint16_t result = RotorRule::Match(aAccOrProxy); + + // if a match was found in the base-class's Match function, + // it is valid to consider that match again here. if it is + // not of the desired role, we flip the match bit to "unmatch" + // otherwise, the match persists. + if ((result & nsIAccessibleTraversalRule::FILTER_MATCH)) { + if (aAccOrProxy.Role() != roles::PASSWORD_TEXT && + aAccOrProxy.Role() != roles::ENTRY) { + result &= ~nsIAccessibleTraversalRule::FILTER_MATCH; + } + } + + return result; +} + +// Rotor Link Rule + +RotorLinkRule::RotorLinkRule(AccessibleOrProxy& aDirectDescendantsFrom) + : RotorRule(aDirectDescendantsFrom){}; + +RotorLinkRule::RotorLinkRule() : RotorRule(){}; + +uint16_t RotorLinkRule::Match(const AccessibleOrProxy& aAccOrProxy) { + uint16_t result = RotorRule::Match(aAccOrProxy); + + // if a match was found in the base-class's Match function, + // it is valid to consider that match again here. if it is + // not of the desired role, we flip the match bit to "unmatch" + // otherwise, the match persists. + if ((result & nsIAccessibleTraversalRule::FILTER_MATCH)) { + mozAccessible* nativeMatch = GetNativeFromGeckoAccessible(aAccOrProxy); + if (![[nativeMatch moxRole] isEqualToString:@"AXLink"]) { + result &= ~nsIAccessibleTraversalRule::FILTER_MATCH; + } + } + + return result; +} + +RotorVisitedLinkRule::RotorVisitedLinkRule() : RotorLinkRule() {} + +RotorVisitedLinkRule::RotorVisitedLinkRule( + AccessibleOrProxy& aDirectDescendantsFrom) + : RotorLinkRule(aDirectDescendantsFrom) {} + +uint16_t RotorVisitedLinkRule::Match(const AccessibleOrProxy& aAccOrProxy) { + uint16_t result = RotorLinkRule::Match(aAccOrProxy); + + if (result & nsIAccessibleTraversalRule::FILTER_MATCH) { + mozAccessible* nativeMatch = GetNativeFromGeckoAccessible(aAccOrProxy); + if (![[nativeMatch moxVisited] boolValue]) { + result &= ~nsIAccessibleTraversalRule::FILTER_MATCH; + } + } + + return result; +} + +RotorUnvisitedLinkRule::RotorUnvisitedLinkRule() : RotorLinkRule() {} + +RotorUnvisitedLinkRule::RotorUnvisitedLinkRule( + AccessibleOrProxy& aDirectDescendantsFrom) + : RotorLinkRule(aDirectDescendantsFrom) {} + +uint16_t RotorUnvisitedLinkRule::Match(const AccessibleOrProxy& aAccOrProxy) { + uint16_t result = RotorLinkRule::Match(aAccOrProxy); + + if (result & nsIAccessibleTraversalRule::FILTER_MATCH) { + mozAccessible* nativeMatch = GetNativeFromGeckoAccessible(aAccOrProxy); + if ([[nativeMatch moxVisited] boolValue]) { + result &= ~nsIAccessibleTraversalRule::FILTER_MATCH; + } + } + + return result; +} + +// Match Not Rule + +RotorNotMacRoleRule::RotorNotMacRoleRule( + NSString* aMacRole, AccessibleOrProxy& aDirectDescendantsFrom) + : RotorMacRoleRule(aMacRole, aDirectDescendantsFrom) {} + +RotorNotMacRoleRule::RotorNotMacRoleRule(NSString* aMacRole) + : RotorMacRoleRule(aMacRole) {} + +uint16_t RotorNotMacRoleRule::Match(const AccessibleOrProxy& aAccOrProxy) { + uint16_t result = RotorRule::Match(aAccOrProxy); + + // if a match was found in the base-class's Match function, + // it is valid to consider that match again here. if it is + // not different from the desired role, we flip the + // match bit to "unmatch" otherwise, the match persists. + if ((result & nsIAccessibleTraversalRule::FILTER_MATCH)) { + mozAccessible* nativeMatch = GetNativeFromGeckoAccessible(aAccOrProxy); + if ([[nativeMatch moxRole] isEqualToString:mMacRole]) { + result &= ~nsIAccessibleTraversalRule::FILTER_MATCH; + } + } + return result; +} + +// Rotor Static Text Rule + +RotorStaticTextRule::RotorStaticTextRule( + AccessibleOrProxy& aDirectDescendantsFrom) + : RotorRule(aDirectDescendantsFrom){}; + +RotorStaticTextRule::RotorStaticTextRule() : RotorRule(){}; + +uint16_t RotorStaticTextRule::Match(const AccessibleOrProxy& aAccOrProxy) { + uint16_t result = RotorRule::Match(aAccOrProxy); + + // if a match was found in the base-class's Match function, + // it is valid to consider that match again here. if it is + // not of the desired role, we flip the match bit to "unmatch" + // otherwise, the match persists. + if ((result & nsIAccessibleTraversalRule::FILTER_MATCH)) { + mozAccessible* nativeMatch = GetNativeFromGeckoAccessible(aAccOrProxy); + if (![[nativeMatch moxRole] isEqualToString:@"AXStaticText"]) { + result &= ~nsIAccessibleTraversalRule::FILTER_MATCH; + } + } + + return result; +} + +// Rotor Heading Level Rule + +RotorHeadingLevelRule::RotorHeadingLevelRule( + int32_t aLevel, AccessibleOrProxy& aDirectDescendantsFrom) + : RotorRoleRule(roles::HEADING, aDirectDescendantsFrom), mLevel(aLevel){}; + +RotorHeadingLevelRule::RotorHeadingLevelRule(int32_t aLevel) + : RotorRoleRule(roles::HEADING), mLevel(aLevel){}; + +uint16_t RotorHeadingLevelRule::Match(const AccessibleOrProxy& aAccOrProxy) { + uint16_t result = RotorRoleRule::Match(aAccOrProxy); + + // if a match was found in the base-class's Match function, + // it is valid to consider that match again here. if it is + // not of the desired heading level, we flip the match bit to + // "unmatch" otherwise, the match persists. + if ((result & nsIAccessibleTraversalRule::FILTER_MATCH)) { + int32_t currLevel = 0; + if (Accessible* acc = aAccOrProxy.AsAccessible()) { + currLevel = acc->GroupPosition().level; + } else if (ProxyAccessible* proxy = aAccOrProxy.AsProxy()) { + currLevel = proxy->GroupPosition().level; + } + + if (currLevel != mLevel) { + result &= ~nsIAccessibleTraversalRule::FILTER_MATCH; + } + } + + return result; +} + +uint16_t RotorLiveRegionRule::Match(const AccessibleOrProxy& aAccOrProxy) { + uint16_t result = RotorRule::Match(aAccOrProxy); + + if ((result & nsIAccessibleTraversalRule::FILTER_MATCH)) { + mozAccessible* nativeMatch = GetNativeFromGeckoAccessible(aAccOrProxy); + if (![nativeMatch moxIsLiveRegion]) { + result &= ~nsIAccessibleTraversalRule::FILTER_MATCH; + } + } + return result; +} + +// Outline Rule + +OutlineRule::OutlineRule() : RotorRule(){}; + +uint16_t OutlineRule::Match(const AccessibleOrProxy& aAccOrProxy) { + uint16_t result = RotorRule::Match(aAccOrProxy); + + // if a match was found in the base-class's Match function, + // it is valid to consider that match again here. + if (result & nsIAccessibleTraversalRule::FILTER_MATCH) { + if (aAccOrProxy.Role() == roles::OUTLINE) { + // if the match is an outline, we ignore all children here + // and unmatch the outline itself + result &= ~nsIAccessibleTraversalRule::FILTER_MATCH; + result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } else if (aAccOrProxy.Role() != roles::OUTLINEITEM) { + // if the match is not an outline item, we unmatch here + result &= ~nsIAccessibleTraversalRule::FILTER_MATCH; + } + } + + return result; +} diff --git a/accessible/mac/SelectorMapGen.py b/accessible/mac/SelectorMapGen.py new file mode 100755 index 0000000000..c3504a1751 --- /dev/null +++ b/accessible/mac/SelectorMapGen.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +# +# 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/. + +from __future__ import absolute_import + +import re + + +def write_map(fd, name, text): + matches = re.findall(r"^//\s(AX\w+)\n-\s?\(.*?\)([\w:]+)", text, re.MULTILINE) + entries = [' @"%s" : @"%s"' % (a, s) for [a, s] in matches] + + fd.write("NSDictionary* %s() {\n" % name) + fd.write(" // Create an autoreleased NSDictionary object once, and leak it.\n") + fd.write(" static NSDictionary* s%s = [@{\n" % name) + fd.write(",\n".join(entries)) + fd.write("\n } retain];\n\n") + fd.write(" return s%s;\n" % name) + fd.write("}\n\n") + + +def gen_mm(fd, protocol_file): + protocol = open(protocol_file).read() + fd.write("/* THIS FILE IS AUTOGENERATED - DO NOT EDIT */\n\n") + fd.write("#import <Foundation/Foundation.h>\n\n") + fd.write("namespace mozilla {\nnamespace a11y {\nnamespace mac {\n\n") + + sections = re.findall( + r"#pragma mark - (\w+)\n(.*?)(?=(?:#pragma mark|@end))", protocol, re.DOTALL + ) + for name, text in sections: + write_map(fd, name, text) + + fd.write("}\n}\n}\n") + + +def gen_h(fd, protocol_file): + protocol = open(protocol_file).read() + sections = re.findall( + r"#pragma mark - (\w+)\n(.*?)(?=(?:#pragma mark|@end))", protocol, re.DOTALL + ) + + fd.write("/* THIS FILE IS AUTOGENERATED - DO NOT EDIT */\n\n") + fd.write("#ifndef _MacSelectorMap_H_\n") + fd.write("#define _MacSelectorMap_H_\n") + fd.write("\n@class NSDictionary;\n") + fd.write("\nnamespace mozilla {\nnamespace a11y {\nnamespace mac {\n\n") + for name, text in sections: + fd.write("NSDictionary* %s();\n\n" % name) + fd.write("}\n}\n}\n") + fd.write("\n#endif\n") + + +# For debugging +if __name__ == "__main__": + import sys + + gen_mm(sys.stdout, "accessible/mac/MOXAccessibleProtocol.h") + + gen_h(sys.stdout, "accessible/mac/MOXAccessibleProtocol.h") diff --git a/accessible/mac/TextLeafAccessibleWrap.h b/accessible/mac/TextLeafAccessibleWrap.h new file mode 100644 index 0000000000..27de110160 --- /dev/null +++ b/accessible/mac/TextLeafAccessibleWrap.h @@ -0,0 +1,19 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozilla_a11y_TextLeafAccessibleWrap_h__ +#define mozilla_a11y_TextLeafAccessibleWrap_h__ + +#include "TextLeafAccessible.h" + +namespace mozilla { +namespace a11y { + +typedef class TextLeafAccessible TextLeafAccessibleWrap; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/mac/XULListboxAccessibleWrap.h b/accessible/mac/XULListboxAccessibleWrap.h new file mode 100644 index 0000000000..da6dd13ede --- /dev/null +++ b/accessible/mac/XULListboxAccessibleWrap.h @@ -0,0 +1,19 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozilla_a11y_XULListboxAccessibleWrap_h__ +#define mozilla_a11y_XULListboxAccessibleWrap_h__ + +#include "XULListboxAccessible.h" + +namespace mozilla { +namespace a11y { + +typedef class XULListboxAccessible XULListboxAccessibleWrap; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/mac/XULMenuAccessibleWrap.h b/accessible/mac/XULMenuAccessibleWrap.h new file mode 100644 index 0000000000..81cdaf94ad --- /dev/null +++ b/accessible/mac/XULMenuAccessibleWrap.h @@ -0,0 +1,19 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozilla_a11y_XULMenuAccessibleWrap_h__ +#define mozilla_a11y_XULMenuAccessibleWrap_h__ + +#include "XULMenuAccessible.h" + +namespace mozilla { +namespace a11y { + +typedef class XULMenuitemAccessible XULMenuitemAccessibleWrap; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/mac/XULTreeGridAccessibleWrap.h b/accessible/mac/XULTreeGridAccessibleWrap.h new file mode 100644 index 0000000000..e991d8f4f4 --- /dev/null +++ b/accessible/mac/XULTreeGridAccessibleWrap.h @@ -0,0 +1,20 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozilla_a11y_XULTreeGridAccessibleWrap_h__ +#define mozilla_a11y_XULTreeGridAccessibleWrap_h__ + +#include "XULTreeGridAccessible.h" + +namespace mozilla { +namespace a11y { + +typedef class XULTreeGridAccessible XULTreeGridAccessibleWrap; +typedef class XULTreeGridCellAccessible XULTreeGridCellAccessibleWrap; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/mac/moz.build b/accessible/mac/moz.build new file mode 100644 index 0000000000..c28c89826b --- /dev/null +++ b/accessible/mac/moz.build @@ -0,0 +1,73 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +EXPORTS += [ + "mozAccessibleProtocol.h", +] + +EXPORTS.mozilla.a11y += [ + "AccessibleWrap.h", + "HyperTextAccessibleWrap.h", + "PlatformExtTypes.h", +] + +UNIFIED_SOURCES += [ + "AccessibleWrap.mm", + "DocAccessibleWrap.mm", + "GeckoTextMarker.mm", + "HyperTextAccessibleWrap.mm", + "MacUtils.mm", + "MOXAccessibleBase.mm", + "MOXLandmarkAccessibles.mm", + "MOXMathAccessibles.mm", + "MOXSearchInfo.mm", + "MOXTextMarkerDelegate.mm", + "MOXWebAreaAccessible.mm", + "mozAccessible.mm", + "mozActionElements.mm", + "mozHTMLAccessible.mm", + "mozRootAccessible.mm", + "mozSelectableElements.mm", + "mozTableAccessible.mm", + "mozTextAccessible.mm", + "Platform.mm", + "RootAccessibleWrap.mm", + "RotorRules.mm", +] + +SOURCES += [ + "!MacSelectorMap.mm", +] + +LOCAL_INCLUDES += [ + "/accessible/base", + "/accessible/generic", + "/accessible/html", + "/accessible/ipc", + "/accessible/ipc/other", + "/accessible/xul", + "/layout/generic", + "/layout/xul", + "/widget", + "/widget/cocoa", +] + +GeneratedFile( + "MacSelectorMap.h", + script="/accessible/mac/SelectorMapGen.py", + entry_point="gen_h", + inputs=["MOXAccessibleProtocol.h"], +) +GeneratedFile( + "MacSelectorMap.mm", + script="/accessible/mac/SelectorMapGen.py", + entry_point="gen_mm", + inputs=["MOXAccessibleProtocol.h"], +) + +FINAL_LIBRARY = "xul" + +include("/ipc/chromium/chromium-config.mozbuild") diff --git a/accessible/mac/mozAccessible.h b/accessible/mac/mozAccessible.h new file mode 100644 index 0000000000..ee8cd65e77 --- /dev/null +++ b/accessible/mac/mozAccessible.h @@ -0,0 +1,273 @@ +/* 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/. */ + +#include "AccessibleWrap.h" +#include "ProxyAccessible.h" +#include "AccessibleOrProxy.h" + +#import <Cocoa/Cocoa.h> + +#import "MOXAccessibleBase.h" + +@class mozRootAccessible; + +/** + * All mozAccessibles are either abstract objects (that correspond to XUL + * widgets, HTML frames, etc) or are attached to a certain view; for example + * a document view. When we hand an object off to an AT, we always want + * to give it the represented view, in the latter case. + */ + +namespace mozilla { +namespace a11y { + +inline mozAccessible* GetNativeFromGeckoAccessible( + mozilla::a11y::AccessibleOrProxy aAccOrProxy) { + if (aAccOrProxy.IsNull()) { + return nil; + } + if (Accessible* acc = aAccOrProxy.AsAccessible()) { + mozAccessible* native = nil; + acc->GetNativeInterface((void**)&native); + return native; + } + + ProxyAccessible* proxy = aAccOrProxy.AsProxy(); + return reinterpret_cast<mozAccessible*>(proxy->GetWrapper()); +} + +} // a11y +} // mozilla + +@interface mozAccessible : MOXAccessibleBase { + /** + * Reference to the accessible we were created with; + * either a proxy accessible or an accessible wrap. + */ + mozilla::a11y::AccessibleOrProxy mGeckoAccessible; + + /** + * The role of our gecko accessible. + */ + mozilla::a11y::role mRole; + + /** + * A cache of a subset of our states. + */ + uint64_t mCachedState; + + nsStaticAtom* mARIARole; + + bool mIsLiveRegion; +} + +// inits with the given wrap or proxy accessible +- (id)initWithAccessible:(mozilla::a11y::AccessibleOrProxy)aAccOrProxy; + +// allows for gecko accessible access outside of the class +- (mozilla::a11y::AccessibleOrProxy)geckoAccessible; + +- (mozilla::a11y::AccessibleOrProxy)geckoDocument; + +// override +- (void)dealloc; + +// should a child be disabled +- (BOOL)disableChild:(mozAccessible*)child; + +// Given a gecko accessibility event type, post the relevant +// system accessibility notification. +// Note: when overriding or adding new events, make sure your events aren't +// filtered out in Platform::ProxyEvent or AccessibleWrap::HandleAccEvent! +- (void)handleAccessibleEvent:(uint32_t)eventType; + +- (void)handleAccessibleTextChangeEvent:(NSString*)change + inserted:(BOOL)isInserted + inContainer: + (const mozilla::a11y::AccessibleOrProxy&) + container + at:(int32_t)start; + +// internal method to retrieve a child at a given index. +- (id)childAt:(uint32_t)i; + +// Get gecko accessible's state. +- (uint64_t)state; + +// Get gecko accessible's state filtered through given mask. +- (uint64_t)stateWithMask:(uint64_t)mask; + +// Notify of a state change, so the cache can be altered. +- (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled; + +// Invalidate cached state. +- (void)invalidateState; + +// Get top level (tab) web area. +- (mozAccessible*)topWebArea; + +// Handle a role change +- (void)handleRoleChanged:(mozilla::a11y::role)newRole; + +// Get ARIA role +- (nsStaticAtom*)ARIARole; + +// Get array of related mozAccessibles +- (NSArray<mozAccessible*>*)getRelationsByType: + (mozilla::a11y::RelationType)relationType; + +#pragma mark - mozAccessible protocol / widget + +// override +- (BOOL)hasRepresentedView; + +// override +- (id)representedView; + +// override +- (BOOL)isRoot; + +#pragma mark - MOXAccessible protocol + +// override +- (BOOL)moxBlockSelector:(SEL)selector; + +// override +- (id)moxHitTest:(NSPoint)point; + +// override +- (id)moxFocusedUIElement; + +- (id<MOXTextMarkerSupport>)moxTextMarkerDelegate; + +- (BOOL)moxIsLiveRegion; + +// Attribute getters + +// override +- (id<mozAccessible>)moxParent; + +// override +- (NSArray*)moxChildren; + +// override +- (NSValue*)moxSize; + +// override +- (NSValue*)moxPosition; + +// override +- (NSString*)moxRole; + +// override +- (NSString*)moxSubrole; + +// override +- (NSString*)moxRoleDescription; + +// override +- (NSWindow*)moxWindow; + +// override +- (id)moxValue; + +// override +- (NSString*)moxTitle; + +// override +- (NSString*)moxLabel; + +// override +- (NSString*)moxHelp; + +// override +- (NSNumber*)moxEnabled; + +// override +- (NSNumber*)moxFocused; + +// override +- (NSNumber*)moxSelected; + +// override +- (NSString*)moxARIACurrent; + +// override +- (NSNumber*)moxARIAAtomic; + +// override +- (NSString*)moxARIALive; + +// override +- (NSString*)moxARIARelevant; + +// override +- (id)moxTitleUIElement; + +// override +- (NSString*)moxDOMIdentifier; + +// override +- (NSNumber*)moxRequired; + +// override +- (NSNumber*)moxElementBusy; + +// override +- (NSArray*)moxLinkedUIElements; + +// override +- (NSArray*)moxARIAControls; + +// override +- (id)moxEditableAncestor; + +// override +- (id)moxHighestEditableAncestor; + +// override +- (id)moxFocusableAncestor; + +#ifndef RELEASE_OR_BETA +// override +- (NSString*)moxMozDebugDescription; +#endif + +// override +- (NSArray*)moxUIElementsForSearchPredicate:(NSDictionary*)searchPredicate; + +// override +- (NSNumber*)moxUIElementCountForSearchPredicate:(NSDictionary*)searchPredicate; + +// override +- (void)moxSetFocused:(NSNumber*)focused; + +// override +- (void)moxPerformScrollToVisible; + +// override +- (void)moxPerformShowMenu; + +// override +- (void)moxPerformPress; + +// override +- (BOOL)moxIgnoreWithParent:(mozAccessible*)parent; + +// override +- (BOOL)moxIgnoreChild:(mozAccessible*)child; + +#pragma mark - + +// makes ourselves "expired". after this point, we might be around if someone +// has retained us (e.g., a third-party), but we really contain no information. +// override +- (void)expire; +// override +- (BOOL)isExpired; + +@end diff --git a/accessible/mac/mozAccessible.mm b/accessible/mac/mozAccessible.mm new file mode 100644 index 0000000000..717513dce6 --- /dev/null +++ b/accessible/mac/mozAccessible.mm @@ -0,0 +1,1074 @@ +/* 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 "mozAccessible.h" +#include "MOXAccessibleBase.h" + +#import "MacUtils.h" +#import "mozView.h" +#import "MOXSearchInfo.h" +#import "mozTextAccessible.h" + +#include "Accessible-inl.h" +#include "nsAccUtils.h" +#include "nsIPersistentProperties2.h" +#include "DocAccessibleParent.h" +#include "Relation.h" +#include "Role.h" +#include "RootAccessible.h" +#include "TableAccessible.h" +#include "mozilla/a11y/PDocAccessible.h" +#include "mozilla/dom/BrowserParent.h" +#include "OuterDocAccessible.h" +#include "nsChildView.h" + +#include "nsRect.h" +#include "nsCocoaUtils.h" +#include "nsCoord.h" +#include "nsObjCExceptions.h" +#include "nsWhitespaceTokenizer.h" +#include <prdtoa.h> + +using namespace mozilla; +using namespace mozilla::a11y; + +#pragma mark - + +@interface mozAccessible () +- (BOOL)providesLabelNotTitle; + +- (void)maybePostLiveRegionChanged; +@end + +@implementation mozAccessible + +- (id)initWithAccessible:(AccessibleOrProxy)aAccOrProxy { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + MOZ_ASSERT(!aAccOrProxy.IsNull(), "Cannot init mozAccessible with null"); + if ((self = [super init])) { + mGeckoAccessible = aAccOrProxy; + mRole = aAccOrProxy.Role(); + } + + return self; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (void)dealloc { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + [super dealloc]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +#pragma mark - mozAccessible widget + +- (BOOL)hasRepresentedView { + return NO; +} + +- (id)representedView { + return nil; +} + +- (BOOL)isRoot { + return NO; +} + +#pragma mark - + +- (BOOL)moxIgnoreWithParent:(mozAccessible*)parent { + if (Accessible* acc = mGeckoAccessible.AsAccessible()) { + if (acc->IsContent() && acc->GetContent()->IsXULElement()) { + if (acc->VisibilityState() & states::INVISIBLE) { + return YES; + } + } + } + + return [parent moxIgnoreChild:self]; +} + +- (BOOL)moxIgnoreChild:(mozAccessible*)child { + return nsAccUtils::MustPrune(mGeckoAccessible); +} + +- (id)childAt:(uint32_t)i { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + AccessibleOrProxy child = mGeckoAccessible.ChildAt(i); + return !child.IsNull() ? GetNativeFromGeckoAccessible(child) : nil; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +static const uint64_t kCachedStates = + states::CHECKED | states::PRESSED | states::MIXED | states::EXPANDED | + states::CURRENT | states::SELECTED | states::TRAVERSED | states::LINKED | + states::HASPOPUP | states::BUSY; +static const uint64_t kCacheInitialized = ((uint64_t)0x1) << 63; + +- (uint64_t)state { + uint64_t state = 0; + + if (Accessible* acc = mGeckoAccessible.AsAccessible()) { + state = acc->State(); + } + + if (ProxyAccessible* proxy = mGeckoAccessible.AsProxy()) { + state = proxy->State(); + } + + if (!(mCachedState & kCacheInitialized)) { + mCachedState = state & kCachedStates; + mCachedState |= kCacheInitialized; + } + + return state; +} + +- (uint64_t)stateWithMask:(uint64_t)mask { + if ((mask & kCachedStates) == mask && + (mCachedState & kCacheInitialized) != 0) { + return mCachedState & mask; + } + + return [self state] & mask; +} + +- (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled { + if ((state & kCachedStates) != 0) { + if (!(mCachedState & kCacheInitialized)) { + [self state]; + } else { + if (enabled) { + mCachedState |= state; + } else { + mCachedState &= ~state; + } + } + } + + if (state == states::BUSY) { + [self moxPostNotification:@"AXElementBusyChanged"]; + } +} + +- (void)invalidateState { + mCachedState = 0; +} + +- (BOOL)providesLabelNotTitle { + // These accessible types are the exception to the rule of label vs. title: + // They may be named explicitly, but they still provide a label not a title. + return mRole == roles::GROUPING || mRole == roles::RADIO_GROUP || + mRole == roles::FIGURE || mRole == roles::GRAPHIC || + mRole == roles::DOCUMENT || mRole == roles::OUTLINE; +} + +- (mozilla::a11y::AccessibleOrProxy)geckoAccessible { + return mGeckoAccessible; +} + +- (mozilla::a11y::AccessibleOrProxy)geckoDocument { + MOZ_ASSERT(!mGeckoAccessible.IsNull()); + + if (mGeckoAccessible.IsAccessible()) { + if (mGeckoAccessible.AsAccessible()->IsDoc()) { + return mGeckoAccessible; + } + return mGeckoAccessible.AsAccessible()->Document(); + } + + if (mGeckoAccessible.AsProxy()->IsDoc()) { + return mGeckoAccessible; + } + + return mGeckoAccessible.AsProxy()->Document(); +} + +#pragma mark - MOXAccessible protocol + +- (BOOL)moxBlockSelector:(SEL)selector { + if (selector == @selector(moxPerformPress)) { + uint8_t actionCount = mGeckoAccessible.IsAccessible() + ? mGeckoAccessible.AsAccessible()->ActionCount() + : mGeckoAccessible.AsProxy()->ActionCount(); + + // If we have no action, we don't support press, so return YES. + return actionCount == 0; + } + + if (selector == @selector(moxSetFocused:)) { + return [self stateWithMask:states::FOCUSABLE] == 0; + } + + if (selector == @selector(moxARIALive) || + selector == @selector(moxARIAAtomic) || + selector == @selector(moxARIARelevant)) { + return ![self moxIsLiveRegion]; + } + + return [super moxBlockSelector:selector]; +} + +- (id)moxFocusedUIElement { + MOZ_ASSERT(!mGeckoAccessible.IsNull()); + + Accessible* acc = mGeckoAccessible.AsAccessible(); + ProxyAccessible* proxy = mGeckoAccessible.AsProxy(); + + mozAccessible* focusedChild = nil; + if (acc) { + Accessible* focusedGeckoChild = acc->FocusedChild(); + if (focusedGeckoChild) { + focusedChild = GetNativeFromGeckoAccessible(focusedGeckoChild); + } else { + dom::BrowserParent* browser = dom::BrowserParent::GetFocused(); + if (browser) { + a11y::DocAccessibleParent* proxyDoc = + browser->GetTopLevelDocAccessible(); + if (proxyDoc) { + mozAccessible* nativeRemoteChild = + GetNativeFromGeckoAccessible(proxyDoc); + return [nativeRemoteChild accessibilityFocusedUIElement]; + } + } + } + } else if (proxy) { + ProxyAccessible* focusedGeckoChild = proxy->FocusedChild(); + if (focusedGeckoChild) { + focusedChild = GetNativeFromGeckoAccessible(focusedGeckoChild); + } + } + + if ([focusedChild isAccessibilityElement]) { + return focusedChild; + } + + // return ourself if we can't get a native focused child. + return self; +} + +- (id<MOXTextMarkerSupport>)moxTextMarkerDelegate { + MOZ_ASSERT(!mGeckoAccessible.IsNull()); + + if (mGeckoAccessible.IsAccessible()) { + return [MOXTextMarkerDelegate + getOrCreateForDoc:mGeckoAccessible.AsAccessible()->Document()]; + } + + return [MOXTextMarkerDelegate + getOrCreateForDoc:mGeckoAccessible.AsProxy()->Document()]; +} + +- (BOOL)moxIsLiveRegion { + return mIsLiveRegion; +} + +- (id)moxHitTest:(NSPoint)point { + MOZ_ASSERT(!mGeckoAccessible.IsNull()); + + // Convert the given screen-global point in the cocoa coordinate system (with + // origin in the bottom-left corner of the screen) into point in the Gecko + // coordinate system (with origin in a top-left screen point). + NSScreen* mainView = [[NSScreen screens] objectAtIndex:0]; + NSPoint tmpPoint = + NSMakePoint(point.x, [mainView frame].size.height - point.y); + LayoutDeviceIntPoint geckoPoint = nsCocoaUtils::CocoaPointsToDevPixels( + tmpPoint, nsCocoaUtils::GetBackingScaleFactor(mainView)); + + AccessibleOrProxy child = mGeckoAccessible.ChildAtPoint( + geckoPoint.x, geckoPoint.y, Accessible::eDeepestChild); + + if (!child.IsNull()) { + mozAccessible* nativeChild = GetNativeFromGeckoAccessible(child); + return [nativeChild isAccessibilityElement] + ? nativeChild + : [nativeChild moxUnignoredParent]; + } + + // if we didn't find anything, return ourself or child view. + return self; +} + +- (id<mozAccessible>)moxParent { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + if ([self isExpired]) { + return nil; + } + + AccessibleOrProxy parent = mGeckoAccessible.Parent(); + + if (parent.IsNull()) { + return nil; + } + + id nativeParent = GetNativeFromGeckoAccessible(parent); + if (parent.Role() == roles::DOCUMENT && + [nativeParent respondsToSelector:@selector(rootGroup)]) { + // Before returning a WebArea as parent, check to see if + // there is a generated root group that is an intermediate container. + if (id<mozAccessible> rootGroup = [nativeParent rootGroup]) { + nativeParent = rootGroup; + } + } + + if (!nativeParent && mGeckoAccessible.IsAccessible()) { + // Return native of root accessible if we have no direct parent. + // XXX: need to return a sensible fallback in proxy case as well + nativeParent = GetNativeFromGeckoAccessible( + mGeckoAccessible.AsAccessible()->RootAccessible()); + } + + return GetObjectOrRepresentedView(nativeParent); + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +// gets all our native children lazily, including those that are ignored. +- (NSArray*)moxChildren { + MOZ_ASSERT(!mGeckoAccessible.IsNull()); + + NSMutableArray* children = + [[NSMutableArray alloc] initWithCapacity:mGeckoAccessible.ChildCount()]; + + for (uint32_t childIdx = 0; childIdx < mGeckoAccessible.ChildCount(); + childIdx++) { + AccessibleOrProxy child = mGeckoAccessible.ChildAt(childIdx); + mozAccessible* nativeChild = GetNativeFromGeckoAccessible(child); + if (!nativeChild) { + continue; + } + + [children addObject:nativeChild]; + } + + return children; +} + +- (NSValue*)moxPosition { + MOZ_ASSERT(!mGeckoAccessible.IsNull()); + + nsIntRect rect = mGeckoAccessible.IsAccessible() + ? mGeckoAccessible.AsAccessible()->Bounds() + : mGeckoAccessible.AsProxy()->Bounds(); + + NSScreen* mainView = [[NSScreen screens] objectAtIndex:0]; + CGFloat scaleFactor = nsCocoaUtils::GetBackingScaleFactor(mainView); + NSPoint p = + NSMakePoint(static_cast<CGFloat>(rect.x) / scaleFactor, + [mainView frame].size.height - + static_cast<CGFloat>(rect.y + rect.height) / scaleFactor); + + return [NSValue valueWithPoint:p]; +} + +- (NSValue*)moxSize { + MOZ_ASSERT(!mGeckoAccessible.IsNull()); + + nsIntRect rect = mGeckoAccessible.IsAccessible() + ? mGeckoAccessible.AsAccessible()->Bounds() + : mGeckoAccessible.AsProxy()->Bounds(); + + CGFloat scaleFactor = + nsCocoaUtils::GetBackingScaleFactor([[NSScreen screens] objectAtIndex:0]); + return [NSValue + valueWithSize:NSMakeSize( + static_cast<CGFloat>(rect.width) / scaleFactor, + static_cast<CGFloat>(rect.height) / scaleFactor)]; +} + +- (NSString*)moxRole { +#define ROLE(geckoRole, stringRole, atkRole, macRole, macSubrole, msaaRole, \ + ia2Role, androidClass, nameRule) \ + case roles::geckoRole: \ + return macRole; + + switch (mRole) { +#include "RoleMap.h" + default: + MOZ_ASSERT_UNREACHABLE("Unknown role."); + return NSAccessibilityUnknownRole; + } + +#undef ROLE +} + +- (nsStaticAtom*)ARIARole { + MOZ_ASSERT(!mGeckoAccessible.IsNull()); + + if (Accessible* acc = mGeckoAccessible.AsAccessible()) { + if (acc->HasARIARole()) { + const nsRoleMapEntry* roleMap = acc->ARIARoleMap(); + return roleMap->roleAtom; + } + + return nsGkAtoms::_empty; + } + + if (!mARIARole) { + mARIARole = mGeckoAccessible.AsProxy()->ARIARoleAtom(); + if (!mARIARole) { + mARIARole = nsGkAtoms::_empty; + } + } + + return mARIARole; +} + +- (NSString*)moxSubrole { + MOZ_ASSERT(!mGeckoAccessible.IsNull()); + + Accessible* acc = mGeckoAccessible.AsAccessible(); + ProxyAccessible* proxy = mGeckoAccessible.AsProxy(); + + // Deal with landmarks first + // macOS groups the specific landmark types of DPub ARIA into two broad + // categories with corresponding subroles: Navigation and region/container. + if (mRole == roles::LANDMARK) { + nsAtom* landmark = acc ? acc->LandmarkRole() : proxy->LandmarkRole(); + // HTML Elements treated as landmarks, and ARIA landmarks. + if (landmark) { + if (landmark == nsGkAtoms::banner) return @"AXLandmarkBanner"; + if (landmark == nsGkAtoms::complementary) + return @"AXLandmarkComplementary"; + if (landmark == nsGkAtoms::contentinfo) return @"AXLandmarkContentInfo"; + if (landmark == nsGkAtoms::main) return @"AXLandmarkMain"; + if (landmark == nsGkAtoms::navigation) return @"AXLandmarkNavigation"; + if (landmark == nsGkAtoms::search) return @"AXLandmarkSearch"; + } + + // None of the above, so assume DPub ARIA. + return @"AXLandmarkRegion"; + } + + // Now, deal with widget roles + nsStaticAtom* roleAtom = nullptr; + + if (mRole == roles::DIALOG) { + roleAtom = [self ARIARole]; + + if (roleAtom == nsGkAtoms::alertdialog) { + return @"AXApplicationAlertDialog"; + } + if (roleAtom == nsGkAtoms::dialog) { + return @"AXApplicationDialog"; + } + } + + if (mRole == roles::FORM) { + roleAtom = [self ARIARole]; + + if (roleAtom == nsGkAtoms::form) { + return @"AXLandmarkForm"; + } + } + +#define ROLE(geckoRole, stringRole, atkRole, macRole, macSubrole, msaaRole, \ + ia2Role, androidClass, nameRule) \ + case roles::geckoRole: \ + if (![macSubrole isEqualToString:NSAccessibilityUnknownSubrole]) { \ + return macSubrole; \ + } else { \ + break; \ + } + + switch (mRole) { +#include "RoleMap.h" + } + + // These are special. They map to roles::NOTHING + // and are instructed by the ARIA map to use the native host role. + roleAtom = [self ARIARole]; + + if (roleAtom == nsGkAtoms::log_) { + return @"AXApplicationLog"; + } + + if (roleAtom == nsGkAtoms::timer) { + return @"AXApplicationTimer"; + } + // macOS added an AXSubrole value to distinguish generic AXGroup objects + // from those which are AXGroups as a result of an explicit ARIA role, + // such as the non-landmark, non-listitem text containers in DPub ARIA. + if (mRole == roles::FOOTNOTE || mRole == roles::SECTION) { + return @"AXApplicationGroup"; + } + + return NSAccessibilityUnknownSubrole; + +#undef ROLE +} + +struct RoleDescrMap { + NSString* role; + const nsString description; +}; + +static const RoleDescrMap sRoleDescrMap[] = { + {@"AXApplicationAlert", u"alert"_ns}, + {@"AXApplicationAlertDialog", u"alertDialog"_ns}, + {@"AXApplicationDialog", u"dialog"_ns}, + {@"AXApplicationLog", u"log"_ns}, + {@"AXApplicationMarquee", u"marquee"_ns}, + {@"AXApplicationStatus", u"status"_ns}, + {@"AXApplicationTimer", u"timer"_ns}, + {@"AXContentSeparator", u"separator"_ns}, + {@"AXDefinition", u"definition"_ns}, + {@"AXDetails", u"details"_ns}, + {@"AXDocument", u"document"_ns}, + {@"AXDocumentArticle", u"article"_ns}, + {@"AXDocumentMath", u"math"_ns}, + {@"AXDocumentNote", u"note"_ns}, + {@"AXLandmarkApplication", u"application"_ns}, + {@"AXLandmarkBanner", u"banner"_ns}, + {@"AXLandmarkComplementary", u"complementary"_ns}, + {@"AXLandmarkContentInfo", u"content"_ns}, + {@"AXLandmarkMain", u"main"_ns}, + {@"AXLandmarkNavigation", u"navigation"_ns}, + {@"AXLandmarkRegion", u"region"_ns}, + {@"AXLandmarkSearch", u"search"_ns}, + {@"AXSearchField", u"searchTextField"_ns}, + {@"AXSummary", u"summary"_ns}, + {@"AXTabPanel", u"tabPanel"_ns}, + {@"AXTerm", u"term"_ns}, + {@"AXUserInterfaceTooltip", u"tooltip"_ns}}; + +struct RoleDescrComparator { + const NSString* mRole; + explicit RoleDescrComparator(const NSString* aRole) : mRole(aRole) {} + int operator()(const RoleDescrMap& aEntry) const { + return [mRole compare:aEntry.role]; + } +}; + +- (NSString*)moxRoleDescription { + if (NSString* ariaRoleDescription = + utils::GetAccAttr(self, "roledescription")) { + if ([ariaRoleDescription length]) { + return ariaRoleDescription; + } + } + + if (mRole == roles::FIGURE) return utils::LocalizedString(u"figure"_ns); + + if (mRole == roles::HEADING) return utils::LocalizedString(u"heading"_ns); + + if (mRole == roles::MARK) { + return utils::LocalizedString(u"highlight"_ns); + } + + NSString* subrole = [self moxSubrole]; + + if (subrole) { + size_t idx = 0; + if (BinarySearchIf(sRoleDescrMap, 0, ArrayLength(sRoleDescrMap), + RoleDescrComparator(subrole), &idx)) { + return utils::LocalizedString(sRoleDescrMap[idx].description); + } + } + + return NSAccessibilityRoleDescription([self moxRole], subrole); +} + +- (NSString*)moxLabel { + if ([self isExpired]) { + return nil; + } + + Accessible* acc = mGeckoAccessible.AsAccessible(); + ProxyAccessible* proxy = mGeckoAccessible.AsProxy(); + nsAutoString name; + + /* If our accessible is: + * 1. Named by invisible text, or + * 2. Has more than one labeling relation, or + * 3. Is a special role defined in providesLabelNotTitle + * ... return its name as a label (AXDescription). + */ + if (acc) { + ENameValueFlag flag = acc->Name(name); + if (flag == eNameFromSubtree) { + return nil; + } + } else if (proxy) { + uint32_t flag = proxy->Name(name); + if (flag == eNameFromSubtree) { + return nil; + } + } + + if (![self providesLabelNotTitle]) { + NSArray* relations = [self getRelationsByType:RelationType::LABELLED_BY]; + if ([relations count] == 1) { + return nil; + } + } + + return nsCocoaUtils::ToNSString(name); +} + +- (NSString*)moxTitle { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + // In some special cases we provide the name in the label (AXDescription). + if ([self providesLabelNotTitle]) { + return nil; + } + + nsAutoString title; + if (Accessible* acc = mGeckoAccessible.AsAccessible()) { + acc->Name(title); + } else { + mGeckoAccessible.AsProxy()->Name(title); + } + + return nsCocoaUtils::ToNSString(title); + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (id)moxValue { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + nsAutoString value; + if (Accessible* acc = mGeckoAccessible.AsAccessible()) { + acc->Value(value); + } else { + mGeckoAccessible.AsProxy()->Value(value); + } + + return nsCocoaUtils::ToNSString(value); + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (NSString*)moxHelp { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + // What needs to go here is actually the accDescription of an item. + // The MSAA acc_help method has nothing to do with this one. + nsAutoString helpText; + if (Accessible* acc = mGeckoAccessible.AsAccessible()) { + acc->Description(helpText); + } else { + mGeckoAccessible.AsProxy()->Description(helpText); + } + + return nsCocoaUtils::ToNSString(helpText); + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (NSWindow*)moxWindow { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + // Get a pointer to the native window (NSWindow) we reside in. + NSWindow* nativeWindow = nil; + DocAccessible* docAcc = nullptr; + if (Accessible* acc = mGeckoAccessible.AsAccessible()) { + docAcc = acc->Document(); + } else { + ProxyAccessible* proxy = mGeckoAccessible.AsProxy(); + Accessible* outerDoc = proxy->OuterDocOfRemoteBrowser(); + if (outerDoc) docAcc = outerDoc->Document(); + } + + if (docAcc) nativeWindow = static_cast<NSWindow*>(docAcc->GetNativeWindow()); + + MOZ_ASSERT(nativeWindow, "Couldn't get native window"); + return nativeWindow; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (NSNumber*)moxEnabled { + if ([self stateWithMask:states::UNAVAILABLE]) { + return @NO; + } + + if (![self isRoot]) { + mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent]; + if (![parent isRoot]) { + return @(![parent disableChild:self]); + } + } + + return @YES; +} + +- (NSNumber*)moxFocused { + return @([self stateWithMask:states::FOCUSED] != 0); +} + +- (NSNumber*)moxSelected { + return @NO; +} + +- (NSString*)moxARIACurrent { + if (![self stateWithMask:states::CURRENT]) { + return nil; + } + + return utils::GetAccAttr(self, "current"); +} + +- (NSNumber*)moxARIAAtomic { + return @(utils::GetAccAttr(self, "atomic") != nil); +} + +- (NSString*)moxARIALive { + return utils::GetAccAttr(self, "live"); +} + +- (NSString*)moxARIARelevant { + if (NSString* relevant = utils::GetAccAttr(self, "container-relevant")) { + return relevant; + } + + // Default aria-relevant value + return @"additions text"; +} + +- (id)moxTitleUIElement { + MOZ_ASSERT(!mGeckoAccessible.IsNull()); + + NSArray* relations = [self getRelationsByType:RelationType::LABELLED_BY]; + if ([relations count] == 1) { + return [relations firstObject]; + } + + return nil; +} + +- (NSString*)moxDOMIdentifier { + MOZ_ASSERT(!mGeckoAccessible.IsNull()); + + nsAutoString id; + if (Accessible* acc = mGeckoAccessible.AsAccessible()) { + if (acc->GetContent()) { + nsCoreUtils::GetID(acc->GetContent(), id); + } + } else { + mGeckoAccessible.AsProxy()->DOMNodeID(id); + } + + return nsCocoaUtils::ToNSString(id); +} + +- (NSNumber*)moxRequired { + return @([self stateWithMask:states::REQUIRED] != 0); +} + +- (NSNumber*)moxElementBusy { + return @([self stateWithMask:states::BUSY] != 0); +} + +- (NSArray*)moxLinkedUIElements { + return [self getRelationsByType:RelationType::FLOWS_TO]; +} + +- (NSArray*)moxARIAControls { + return [self getRelationsByType:RelationType::CONTROLLER_FOR]; +} + +- (mozAccessible*)topWebArea { + AccessibleOrProxy doc = [self geckoDocument]; + while (!doc.IsNull()) { + if (doc.IsAccessible()) { + DocAccessible* docAcc = doc.AsAccessible()->AsDoc(); + if (docAcc->DocumentNode()->GetBrowsingContext()->IsTopContent()) { + return GetNativeFromGeckoAccessible(docAcc); + } + + doc = docAcc->ParentDocument(); + } else { + DocAccessibleParent* docProxy = doc.AsProxy()->AsDoc(); + if (docProxy->IsTopLevel()) { + return GetNativeFromGeckoAccessible(docProxy); + } + doc = docProxy->ParentDoc(); + } + } + + return nil; +} + +- (void)handleRoleChanged:(mozilla::a11y::role)newRole { + mRole = newRole; + mARIARole = nullptr; + + // For testing purposes + [self moxPostNotification:@"AXMozRoleChanged"]; +} + +- (id)moxEditableAncestor { + for (id element = self; [element conformsToProtocol:@protocol(MOXAccessible)]; + element = [element moxUnignoredParent]) { + if ([element isKindOfClass:[mozTextAccessible class]]) { + return element; + } + } + + return nil; +} + +- (id)moxHighestEditableAncestor { + id highestAncestor = [self moxEditableAncestor]; + while ([highestAncestor conformsToProtocol:@protocol(MOXAccessible)]) { + id ancestorParent = [highestAncestor moxUnignoredParent]; + if (![ancestorParent conformsToProtocol:@protocol(MOXAccessible)]) { + break; + } + + id higherAncestor = [ancestorParent moxEditableAncestor]; + + if (!higherAncestor) { + break; + } + + highestAncestor = higherAncestor; + } + + return highestAncestor; +} + +- (id)moxFocusableAncestor { + // XXX: Checking focusable state up the chain can be expensive. For now, + // we can just return AXEditableAncestor since the main use case for this + // is rich text editing with links. + return [self moxEditableAncestor]; +} + +#ifndef RELEASE_OR_BETA +- (NSString*)moxMozDebugDescription { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + NSMutableString* domInfo = [NSMutableString string]; + if (NSString* tagName = utils::GetAccAttr(self, "tag")) { + [domInfo appendFormat:@" %@", tagName]; + NSString* domID = [self moxDOMIdentifier]; + if ([domID length]) { + [domInfo appendFormat:@"#%@", domID]; + } + if (NSString* className = utils::GetAccAttr(self, "class")) { + [domInfo + appendFormat:@".%@", + [className stringByReplacingOccurrencesOfString:@" " + withString:@"."]]; + } + } + + return [NSString stringWithFormat:@"<%@: %p %@%@>", + NSStringFromClass([self class]), self, + [self moxRole], domInfo]; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} +#endif + +- (NSArray*)moxUIElementsForSearchPredicate:(NSDictionary*)searchPredicate { + // Create our search object and set it up with the searchPredicate + // params. The init function does additional parsing. We pass a + // reference to the web area to use as a start element if one is not + // specified. + MOXSearchInfo* search = + [[MOXSearchInfo alloc] initWithParameters:searchPredicate andRoot:self]; + + return [search performSearch]; +} + +- (NSNumber*)moxUIElementCountForSearchPredicate: + (NSDictionary*)searchPredicate { + return [NSNumber + numberWithDouble:[[self moxUIElementsForSearchPredicate:searchPredicate] + count]]; +} + +- (void)moxSetFocused:(NSNumber*)focused { + MOZ_ASSERT(!mGeckoAccessible.IsNull()); + + if ([focused boolValue]) { + if (mGeckoAccessible.IsAccessible()) { + mGeckoAccessible.AsAccessible()->TakeFocus(); + } else { + mGeckoAccessible.AsProxy()->TakeFocus(); + } + } +} + +- (void)moxPerformScrollToVisible { + MOZ_ASSERT(!mGeckoAccessible.IsNull()); + + if (mGeckoAccessible.IsAccessible()) { + // Need strong ref because of MOZ_CAN_RUN_SCRIPT + RefPtr<Accessible> acc = mGeckoAccessible.AsAccessible(); + acc->ScrollTo(nsIAccessibleScrollType::SCROLL_TYPE_ANYWHERE); + } else { + mGeckoAccessible.AsProxy()->ScrollTo( + nsIAccessibleScrollType::SCROLL_TYPE_ANYWHERE); + } +} + +- (void)moxPerformShowMenu { + MOZ_ASSERT(!mGeckoAccessible.IsNull()); + + nsIntRect bounds = mGeckoAccessible.IsAccessible() + ? mGeckoAccessible.AsAccessible()->Bounds() + : mGeckoAccessible.AsProxy()->Bounds(); + // We don't need to convert this rect into mac coordinates because the + // mouse event synthesizer expects layout (gecko) coordinates. + LayoutDeviceIntRect geckoRect = LayoutDeviceIntRect::FromUnknownRect(bounds); + + Accessible* rootAcc = mGeckoAccessible.IsAccessible() + ? mGeckoAccessible.AsAccessible()->RootAccessible() + : mGeckoAccessible.AsProxy() + ->OuterDocOfRemoteBrowser() + ->RootAccessible(); + id objOrView = + GetObjectOrRepresentedView(GetNativeFromGeckoAccessible(rootAcc)); + + LayoutDeviceIntPoint p = + LayoutDeviceIntPoint(geckoRect.X() + (geckoRect.Width() / 2), + geckoRect.Y() + (geckoRect.Height() / 2)); + nsIWidget* widget = [objOrView widget]; + widget->SynthesizeNativeMouseEvent(p, NSEventTypeRightMouseDown, 0, nullptr); +} + +- (void)moxPerformPress { + MOZ_ASSERT(!mGeckoAccessible.IsNull()); + + if (mGeckoAccessible.IsAccessible()) { + mGeckoAccessible.AsAccessible()->DoAction(0); + } else { + mGeckoAccessible.AsProxy()->DoAction(0); + } + + // Activating accessible may alter its state. + [self invalidateState]; +} + +#pragma mark - + +- (BOOL)disableChild:(mozAccessible*)child { + return NO; +} + +- (void)maybePostLiveRegionChanged { + for (id element = self; [element conformsToProtocol:@protocol(MOXAccessible)]; + element = [element moxUnignoredParent]) { + if ([element moxIsLiveRegion]) { + [element moxPostNotification:@"AXLiveRegionChanged"]; + return; + } + } +} + +- (NSArray<mozAccessible*>*)getRelationsByType:(RelationType)relationType { + if (Accessible* acc = mGeckoAccessible.AsAccessible()) { + NSMutableArray<mozAccessible*>* relations = [[NSMutableArray alloc] init]; + Relation rel = acc->RelationByType(relationType); + while (Accessible* relAcc = rel.Next()) { + if (mozAccessible* relNative = GetNativeFromGeckoAccessible(relAcc)) { + [relations addObject:relNative]; + } + } + + return relations; + } + + ProxyAccessible* proxy = mGeckoAccessible.AsProxy(); + nsTArray<ProxyAccessible*> rel = proxy->RelationByType(relationType); + return utils::ConvertToNSArray(rel); +} + +- (void)handleAccessibleTextChangeEvent:(NSString*)change + inserted:(BOOL)isInserted + inContainer:(const AccessibleOrProxy&)container + at:(int32_t)start { +} + +- (void)handleAccessibleEvent:(uint32_t)eventType { + switch (eventType) { + case nsIAccessibleEvent::EVENT_FOCUS: + [self moxPostNotification: + NSAccessibilityFocusedUIElementChangedNotification]; + break; + case nsIAccessibleEvent::EVENT_MENUPOPUP_START: + [self moxPostNotification:@"AXMenuOpened"]; + break; + case nsIAccessibleEvent::EVENT_MENUPOPUP_END: + [self moxPostNotification:@"AXMenuClosed"]; + break; + case nsIAccessibleEvent::EVENT_SELECTION: + case nsIAccessibleEvent::EVENT_SELECTION_ADD: + case nsIAccessibleEvent::EVENT_SELECTION_REMOVE: + case nsIAccessibleEvent::EVENT_SELECTION_WITHIN: + [self moxPostNotification: + NSAccessibilitySelectedChildrenChangedNotification]; + break; + case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: { + if (![self stateWithMask:states::SELECTABLE_TEXT]) { + break; + } + // We consider any caret move event to be a selected text change event. + // So dispatching an event for EVENT_TEXT_SELECTION_CHANGED would be + // reduntant. + MOXTextMarkerDelegate* delegate = + static_cast<MOXTextMarkerDelegate*>([self moxTextMarkerDelegate]); + NSMutableDictionary* userInfo = + [[delegate selectionChangeInfo] mutableCopy]; + userInfo[@"AXTextChangeElement"] = self; + + mozAccessible* webArea = [self topWebArea]; + [webArea + moxPostNotification:NSAccessibilitySelectedTextChangedNotification + withUserInfo:userInfo]; + [self moxPostNotification:NSAccessibilitySelectedTextChangedNotification + withUserInfo:userInfo]; + break; + } + case nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED: + mIsLiveRegion = true; + [self moxPostNotification:@"AXLiveRegionCreated"]; + break; + case nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED: + mIsLiveRegion = false; + break; + case nsIAccessibleEvent::EVENT_REORDER: + [self maybePostLiveRegionChanged]; + break; + case nsIAccessibleEvent::EVENT_NAME_CHANGE: { + if (![self providesLabelNotTitle]) { + [self moxPostNotification:NSAccessibilityTitleChangedNotification]; + } + [self maybePostLiveRegionChanged]; + break; + } + } +} + +- (void)expire { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + [self invalidateState]; + + mGeckoAccessible.SetBits(0); + + [self moxPostNotification:NSAccessibilityUIElementDestroyedNotification]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (BOOL)isExpired { + return !mGeckoAccessible.AsAccessible() && !mGeckoAccessible.AsProxy(); +} + +@end diff --git a/accessible/mac/mozAccessibleProtocol.h b/accessible/mac/mozAccessibleProtocol.h new file mode 100644 index 0000000000..3fc46fb7b6 --- /dev/null +++ b/accessible/mac/mozAccessibleProtocol.h @@ -0,0 +1,65 @@ +/* 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 "mozView.h" + +/* This protocol's primary use is so widget/cocoa can talk back to us + properly. + + ChildView owns the topmost mozRootAccessible, and needs to take care of + setting up that parent/child relationship. + + This protocol is thus used to make sure it knows it's talking to us, and not + just some random |id|. +*/ + +@protocol mozAccessible + +// returns whether this accessible is the root accessible. there is one +// root accessible per window. +- (BOOL)isRoot; + +// some mozAccessibles implement accessibility support in place of another +// object. for example, ChildView gets its support from us. +// +// instead of returning a mozAccessible to the OS when it wants an object, we +// need to pass the view we represent, so the OS doesn't get confused and think +// we return some random object. +- (BOOL)hasRepresentedView; +- (id)representedView; + +/*** general ***/ + +// returns the accessible at the specified point. +- (id)accessibilityHitTest:(NSPoint)point; + +// whether this element should be exposed to platform. +- (BOOL)isAccessibilityElement; + +// currently focused UI element (possibly a child accessible) +- (id)accessibilityFocusedUIElement; + +/*** attributes ***/ + +// all supported attributes +- (NSArray*)accessibilityAttributeNames; + +// value for given attribute. +- (id)accessibilityAttributeValue:(NSString*)attribute; + +// whether a particular attribute can be modified +- (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute; + +/*** actions ***/ + +- (NSArray*)accessibilityActionNames; +- (NSString*)accessibilityActionDescription:(NSString*)action; +- (void)accessibilityPerformAction:(NSString*)action; + +@end diff --git a/accessible/mac/mozActionElements.h b/accessible/mac/mozActionElements.h new file mode 100644 index 0000000000..fce2778e87 --- /dev/null +++ b/accessible/mac/mozActionElements.h @@ -0,0 +1,83 @@ +/* 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 "mozAccessible.h" + +/* Simple subclasses for things like checkboxes, buttons, etc. */ + +@interface mozButtonAccessible : mozAccessible + +// override +- (NSNumber*)moxHasPopup; + +// override +- (NSString*)moxPopupValue; + +@end + +@interface mozPopupButtonAccessible : mozButtonAccessible + +// override +- (NSString*)moxTitle; + +// override +- (BOOL)moxBlockSelector:(SEL)selector; + +// override +- (NSArray*)moxChildren; + +// override +- (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled; + +@end + +@interface mozCheckboxAccessible : mozButtonAccessible + +// override +- (id)moxValue; + +// override +- (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled; + +@end + +// Accessible for a radio button +@interface mozRadioButtonAccessible : mozCheckboxAccessible + +// override +- (NSArray*)moxLinkedUIElements; + +@end + +/** + * Accessible for a PANE + */ +@interface mozPaneAccessible : mozAccessible + +// override +- (NSArray*)moxChildren; + +@end + +/** + * Base accessible for an incrementable + */ +@interface mozIncrementableAccessible : mozAccessible + +// override +- (void)moxPerformIncrement; + +// override +- (void)moxPerformDecrement; + +// override +- (void)handleAccessibleEvent:(uint32_t)eventType; + +- (void)changeValueBySteps:(int)factor; + +@end diff --git a/accessible/mac/mozActionElements.mm b/accessible/mac/mozActionElements.mm new file mode 100644 index 0000000000..ee62b29fcb --- /dev/null +++ b/accessible/mac/mozActionElements.mm @@ -0,0 +1,208 @@ +/* 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 "mozActionElements.h" + +#import "MacUtils.h" +#include "Accessible-inl.h" +#include "DocAccessible.h" +#include "XULTabAccessible.h" +#include "HTMLFormControlAccessible.h" + +#include "nsDeckFrame.h" +#include "nsObjCExceptions.h" + +using namespace mozilla::a11y; + +enum CheckboxValue { + // these constants correspond to the values in the OS + kUnchecked = 0, + kChecked = 1, + kMixed = 2 +}; + +@implementation mozButtonAccessible + +- (NSNumber*)moxHasPopup { + return @([self stateWithMask:states::HASPOPUP] != 0); +} + +- (NSString*)moxPopupValue { + if ([self stateWithMask:states::HASPOPUP] != 0) { + return utils::GetAccAttr(self, "haspopup"); + } + + return nil; +} + +@end + +@implementation mozPopupButtonAccessible + +- (NSString*)moxTitle { + // Popup buttons don't have titles. + return @""; +} + +- (BOOL)moxBlockSelector:(SEL)selector { + if (selector == @selector(moxHasPopup)) { + return YES; + } + + return [super moxBlockSelector:selector]; +} + +- (NSArray*)moxChildren { + if ([self stateWithMask:states::EXPANDED] == 0) { + // If the popup button is collapsed don't return its children. + return @[]; + } + + return [super moxChildren]; +} + +- (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled { + [super stateChanged:state isEnabled:enabled]; + + if (state == states::EXPANDED) { + // If the EXPANDED state is updated, fire AXMenu events on the + // popups child which is the actual menu. + if (mozAccessible* popup = (mozAccessible*)[self childAt:0]) { + [popup moxPostNotification:(enabled ? @"AXMenuOpened" : @"AXMenuClosed")]; + } + } +} + +@end + +@implementation mozRadioButtonAccessible + +- (NSArray*)moxLinkedUIElements { + return [[self getRelationsByType:RelationType::MEMBER_OF] + arrayByAddingObjectsFromArray:[super moxLinkedUIElements]]; +} + +@end + +@implementation mozCheckboxAccessible + +- (int)isChecked { + // check if we're checked or in a mixed state + uint64_t state = + [self stateWithMask:(states::CHECKED | states::PRESSED | states::MIXED)]; + if (state & (states::CHECKED | states::PRESSED)) { + return kChecked; + } + + if (state & states::MIXED) { + return kMixed; + } + + return kUnchecked; +} + +- (id)moxValue { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + return [NSNumber numberWithInt:[self isChecked]]; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled { + [super stateChanged:state isEnabled:enabled]; + + if (state & (states::CHECKED | states::PRESSED | states::MIXED)) { + [self moxPostNotification:NSAccessibilityValueChangedNotification]; + } +} + +@end + +@implementation mozPaneAccessible + +- (NSArray*)moxChildren { + if (!mGeckoAccessible.AsAccessible()) return nil; + + nsDeckFrame* deckFrame = + do_QueryFrame(mGeckoAccessible.AsAccessible()->GetFrame()); + nsIFrame* selectedFrame = deckFrame ? deckFrame->GetSelectedBox() : nullptr; + + Accessible* selectedAcc = nullptr; + if (selectedFrame) { + nsINode* node = selectedFrame->GetContent(); + selectedAcc = + mGeckoAccessible.AsAccessible()->Document()->GetAccessible(node); + } + + if (selectedAcc) { + mozAccessible* curNative = GetNativeFromGeckoAccessible(selectedAcc); + if (curNative) + return + [NSArray arrayWithObjects:GetObjectOrRepresentedView(curNative), nil]; + } + + return nil; +} + +@end + +@implementation mozIncrementableAccessible + +- (void)moxPerformIncrement { + [self changeValueBySteps:1]; +} + +- (void)moxPerformDecrement { + [self changeValueBySteps:-1]; +} + +- (void)handleAccessibleEvent:(uint32_t)eventType { + switch (eventType) { + case nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE: + case nsIAccessibleEvent::EVENT_VALUE_CHANGE: + [self moxPostNotification:NSAccessibilityValueChangedNotification]; + break; + default: + [super handleAccessibleEvent:eventType]; + break; + } +} + +/* + * Updates the accessible's current value by (factor * step). + * If incrementing factor should be positive, if decrementing + * factor should be negative. + */ + +- (void)changeValueBySteps:(int)factor { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (Accessible* acc = mGeckoAccessible.AsAccessible()) { + double newVal = acc->CurValue() + (acc->Step() * factor); + double min = acc->MinValue(); + double max = acc->MaxValue(); + if ((IsNaN(min) || newVal >= min) && (IsNaN(max) || newVal <= max)) { + acc->SetCurValue(newVal); + } + } else if (ProxyAccessible* proxy = mGeckoAccessible.AsProxy()) { + double newVal = proxy->CurValue() + (proxy->Step() * factor); + double min = proxy->MinValue(); + double max = proxy->MaxValue(); + // Because min and max are not required attributes, we first check + // if the value is undefined. If this check fails, + // the value is defined, and we we verify our new value falls + // within the bound (inclusive). + if ((IsNaN(min) || newVal >= min) && (IsNaN(max) || newVal <= max)) { + proxy->SetCurValue(newVal); + } + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +@end diff --git a/accessible/mac/mozHTMLAccessible.h b/accessible/mac/mozHTMLAccessible.h new file mode 100644 index 0000000000..7288f8a108 --- /dev/null +++ b/accessible/mac/mozHTMLAccessible.h @@ -0,0 +1,48 @@ +/* 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 "mozAccessible.h" + +@interface mozHeadingAccessible : mozAccessible + +// override +- (NSString*)moxTitle; + +// override +- (id)moxValue; + +@end + +@interface mozLinkAccessible : mozAccessible + +// override +- (id)moxValue; + +// override +- (NSString*)moxRole; + +// override +- (NSURL*)moxURL; + +// override +- (NSNumber*)moxVisited; + +@end + +@interface MOXSummaryAccessible : mozAccessible + +// override +- (NSNumber*)moxExpanded; + +@end + +@interface MOXListItemAccessible : mozAccessible + +// override +- (NSString*)moxTitle; + +@end diff --git a/accessible/mac/mozHTMLAccessible.mm b/accessible/mac/mozHTMLAccessible.mm new file mode 100644 index 0000000000..aee3886ec6 --- /dev/null +++ b/accessible/mac/mozHTMLAccessible.mm @@ -0,0 +1,97 @@ +/* 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 "mozHTMLAccessible.h" + +#import "Accessible-inl.h" +#import "HyperTextAccessible.h" + +#import "nsCocoaUtils.h" + +using namespace mozilla::a11y; + +@implementation mozHeadingAccessible + +- (NSString*)moxTitle { + nsAutoString title; + if (Accessible* acc = mGeckoAccessible.AsAccessible()) { + mozilla::ErrorResult rv; + // XXX use the flattening API when there are available + // see bug 768298 + acc->GetContent()->GetTextContent(title, rv); + } else if (ProxyAccessible* proxy = mGeckoAccessible.AsProxy()) { + proxy->Title(title); + } + + return nsCocoaUtils::ToNSString(title); +} + +- (id)moxValue { + GroupPos groupPos; + if (Accessible* acc = mGeckoAccessible.AsAccessible()) { + groupPos = acc->GroupPosition(); + } else if (ProxyAccessible* proxy = mGeckoAccessible.AsProxy()) { + groupPos = proxy->GroupPosition(); + } + + return [NSNumber numberWithInt:groupPos.level]; +} + +@end + +@implementation mozLinkAccessible + +- (NSString*)moxValue { + return @""; +} + +- (NSURL*)moxURL { + nsAutoString value; + if (Accessible* acc = mGeckoAccessible.AsAccessible()) { + acc->Value(value); + } else if (ProxyAccessible* proxy = mGeckoAccessible.AsProxy()) { + proxy->Value(value); + } + + NSString* urlString = value.IsEmpty() ? nil : nsCocoaUtils::ToNSString(value); + if (!urlString) return nil; + + return [NSURL URLWithString:urlString]; +} + +- (NSNumber*)moxVisited { + return @([self stateWithMask:states::TRAVERSED] != 0); +} + +- (NSString*)moxRole { + // If this is not LINKED, just expose this as a generic group accessible. + // Chrome and Safari expose this as a childless AXStaticText, but + // the HTML Accessibility API Mappings spec says this should be an AXGroup. + if (![self stateWithMask:states::LINKED]) { + return NSAccessibilityGroupRole; + } + + return [super moxRole]; +} + +@end + +@implementation MOXSummaryAccessible + +- (NSNumber*)moxExpanded { + return @([self stateWithMask:states::EXPANDED] != 0); +} + +@end + +@implementation MOXListItemAccessible + +- (NSString*)moxTitle { + return @""; +} + +@end diff --git a/accessible/mac/mozRootAccessible.h b/accessible/mac/mozRootAccessible.h new file mode 100644 index 0000000000..3b0f4fc25e --- /dev/null +++ b/accessible/mac/mozRootAccessible.h @@ -0,0 +1,58 @@ +/* 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 "mozAccessible.h" + +// our protocol that we implement (so cocoa widgets can talk to us) +#import "mozAccessibleProtocol.h" + +/* + The root accessible. There is one per window. + Created by the RootAccessibleWrap. +*/ +@interface mozRootAccessible : mozAccessible { + // the mozView that we're representing. + // all outside communication goes through the mozView. + // in reality, it's just piping all calls to us, and we're + // doing its dirty work! + // + // whenever someone asks who we are (e.g., a child asking + // for its parent, or our parent asking for its child), we'll + // respond the mozView. it is absolutely necessary for third- + // party tools that we do this! + // + // /hwaara + id<mozView, mozAccessible> mParallelView; // weak ref +} + +// override +- (id)initWithAccessible:(mozilla::a11y::AccessibleOrProxy)aAccOrProxy; + +#pragma mark - MOXAccessible + +// override +- (NSNumber*)moxMain; + +// override +- (NSNumber*)moxMinimized; + +// override +- (id)moxUnignoredParent; + +#pragma mark - mozAccessible/widget + +// override +- (BOOL)hasRepresentedView; + +// override +- (id)representedView; + +// override +- (BOOL)isRoot; + +@end diff --git a/accessible/mac/mozRootAccessible.mm b/accessible/mac/mozRootAccessible.mm new file mode 100644 index 0000000000..910509f09d --- /dev/null +++ b/accessible/mac/mozRootAccessible.mm @@ -0,0 +1,83 @@ +/* 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/. */ + +#include "RootAccessibleWrap.h" + +#import "mozRootAccessible.h" + +#import "mozView.h" + +// This must be included last: +#include "nsObjCExceptions.h" + +using namespace mozilla::a11y; + +static id<mozAccessible, mozView> getNativeViewFromRootAccessible( + Accessible* aAccessible) { + RootAccessibleWrap* root = + static_cast<RootAccessibleWrap*>(aAccessible->AsRoot()); + id<mozAccessible, mozView> nativeView = nil; + root->GetNativeWidget((void**)&nativeView); + return nativeView; +} + +#pragma mark - + +@implementation mozRootAccessible + +- (id)initWithAccessible:(mozilla::a11y::AccessibleOrProxy)aAccOrProxy { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + MOZ_ASSERT(!aAccOrProxy.IsProxy(), "mozRootAccessible is never a proxy"); + + mParallelView = getNativeViewFromRootAccessible(aAccOrProxy.AsAccessible()); + + return [super initWithAccessible:aAccOrProxy]; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (NSNumber*)moxMain { + return @([[self moxWindow] isMainWindow]); +} + +- (NSNumber*)moxMinimized { + return @([[self moxWindow] isMiniaturized]); +} + +// return the AXParent that our parallell NSView tells us about. +- (id)moxUnignoredParent { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + // If there is no represented view (eg. headless), this will return nil. + return [[self representedView] + accessibilityAttributeValue:NSAccessibilityParentAttribute]; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (BOOL)hasRepresentedView { + return YES; +} + +// this will return our parallell NSView. see mozDocAccessible.h +- (id)representedView { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + MOZ_ASSERT(mParallelView, + "root accessible does not have a native parallel view."); + + return mParallelView; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (BOOL)isRoot { + return YES; +} + +@end diff --git a/accessible/mac/mozSelectableElements.h b/accessible/mac/mozSelectableElements.h new file mode 100644 index 0000000000..2baf07c942 --- /dev/null +++ b/accessible/mac/mozSelectableElements.h @@ -0,0 +1,125 @@ +/* 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 "mozAccessible.h" + +@interface mozSelectableAccessible : mozAccessible + +- (NSArray*)selectableChildren; + +// override +- (void)moxSetSelectedChildren:(NSArray*)selectedChildren; + +// override +- (NSArray*)moxSelectedChildren; + +@end + +@interface mozSelectableChildAccessible : mozAccessible + +// override +- (NSNumber*)moxSelected; + +// override +- (void)moxSetSelected:(NSNumber*)selected; + +@end + +@interface mozTabGroupAccessible : mozSelectableAccessible + +// override +- (NSArray*)moxTabs; + +// override +- (NSArray*)moxContents; + +// override +- (id)moxValue; + +@end + +@interface mozTabAccessible : mozSelectableChildAccessible + +// override +- (NSString*)moxRoleDescription; + +// override +- (id)moxValue; + +@end + +@interface mozListboxAccessible : mozSelectableAccessible + +// override +- (BOOL)moxIgnoreChild:(mozAccessible*)child; + +// override +- (BOOL)disableChild:(mozAccessible*)child; + +// override +- (NSString*)moxOrientation; + +@end + +@interface mozOptionAccessible : mozSelectableChildAccessible + +// override +- (NSString*)moxTitle; + +// override +- (id)moxValue; + +@end + +@interface mozMenuAccessible : mozSelectableAccessible { + BOOL mIsOpened; +} + +// override +- (NSString*)moxTitle; + +// override +- (NSString*)moxLabel; + +// override +- (NSArray*)moxVisibleChildren; + +// override +- (BOOL)moxIgnoreWithParent:(mozAccessible*)parent; + +// override +- (id)moxTitleUIElement; + +// override +- (void)moxPostNotification:(NSString*)notification; + +// override +- (void)expire; + +- (BOOL)isOpened; + +@end + +@interface mozMenuItemAccessible : mozSelectableChildAccessible + +// override +- (NSString*)moxLabel; + +// override +- (BOOL)moxIgnoreWithParent:(mozAccessible*)parent; + +// override +- (NSString*)moxMenuItemMarkChar; + +// override +- (NSNumber*)moxSelected; + +// override +- (void)handleAccessibleEvent:(uint32_t)eventType; + +@end diff --git a/accessible/mac/mozSelectableElements.mm b/accessible/mac/mozSelectableElements.mm new file mode 100644 index 0000000000..fdfeda198e --- /dev/null +++ b/accessible/mac/mozSelectableElements.mm @@ -0,0 +1,332 @@ +/* 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 "mozSelectableElements.h" +#import "MOXWebAreaAccessible.h" +#import "MacUtils.h" +#include "Accessible-inl.h" +#include "nsCocoaUtils.h" + +using namespace mozilla::a11y; + +@implementation mozSelectableAccessible + +/** + * Return the mozAccessibles that are selectable. + */ +- (NSArray*)selectableChildren { + NSArray* toFilter; + if ([self isKindOfClass:[mozMenuAccessible class]]) { + // If we are a menu, our children are only selectable if they are visible + // so we filter this array instead of our unignored children list, which may + // contain invisible items. + toFilter = [static_cast<mozMenuAccessible*>(self) moxVisibleChildren]; + } else { + toFilter = [self moxUnignoredChildren]; + } + return [toFilter + filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL( + mozAccessible* child, + NSDictionary* bindings) { + return [child isKindOfClass:[mozSelectableChildAccessible class]]; + }]]; +} + +- (void)moxSetSelectedChildren:(NSArray*)selectedChildren { + for (id child in [self selectableChildren]) { + BOOL selected = + [selectedChildren indexOfObjectIdenticalTo:child] != NSNotFound; + [child moxSetSelected:@(selected)]; + } +} + +/** + * Return the mozAccessibles that are actually selected. + */ +- (NSArray*)moxSelectedChildren { + return [[self selectableChildren] + filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL( + mozAccessible* child, + NSDictionary* bindings) { + // Return mozSelectableChildAccessibles that have are selected (truthy + // value). + return [[(mozSelectableChildAccessible*)child moxSelected] boolValue]; + }]]; +} + +@end + +@implementation mozSelectableChildAccessible + +- (NSNumber*)moxSelected { + return @([self stateWithMask:states::SELECTED] != 0); +} + +- (void)moxSetSelected:(NSNumber*)selected { + // Get SELECTABLE and UNAVAILABLE state. + uint64_t state = + [self stateWithMask:(states::SELECTABLE | states::UNAVAILABLE)]; + if ((state & states::SELECTABLE) == 0 || (state & states::UNAVAILABLE) != 0) { + // The object is either not selectable or is unavailable. Don't do anything. + return; + } + + if (Accessible* acc = mGeckoAccessible.AsAccessible()) { + acc->SetSelected([selected boolValue]); + } else { + mGeckoAccessible.AsProxy()->SetSelected([selected boolValue]); + } + + // We need to invalidate the state because the accessibility service + // may check the selected attribute synchornously and not wait for + // selection events. + [self invalidateState]; +} + +@end + +@implementation mozTabGroupAccessible + +- (NSArray*)moxTabs { + return [self selectableChildren]; +} + +- (NSArray*)moxContents { + return [self moxUnignoredChildren]; +} + +- (id)moxValue { + // The value of a tab group is its selected child. In the case + // of multiple selections this will return the first one. + return [[self moxSelectedChildren] firstObject]; +} + +@end + +@implementation mozTabAccessible + +- (NSString*)moxRoleDescription { + return utils::LocalizedString(u"tab"_ns); +} + +- (id)moxValue { + // Retuens 1 if item is selected, 0 if not. + return [self moxSelected]; +} + +@end + +@implementation mozListboxAccessible + +- (BOOL)moxIgnoreChild:(mozAccessible*)child { + if (!child || child->mRole == roles::GROUPING) { + return YES; + } + + return [super moxIgnoreChild:child]; +} + +- (BOOL)disableChild:(mozAccessible*)child { + return ![child isKindOfClass:[mozSelectableChildAccessible class]]; +} + +- (NSString*)moxOrientation { + return NSAccessibilityUnknownOrientationValue; +} + +@end + +@implementation mozOptionAccessible + +- (NSString*)moxTitle { + return @""; +} + +- (id)moxValue { + // Swap title and value of option so it behaves more like a AXStaticText. + return [super moxTitle]; +} + +@end + +@implementation mozMenuAccessible + +- (NSString*)moxTitle { + return @""; +} + +- (NSString*)moxLabel { + return @""; +} + +- (BOOL)moxIgnoreWithParent:(mozAccessible*)parent { + // This helps us generate the correct moxChildren array for + // a sub menu -- that returned array should contain all + // menu items, regardless of if they are visible or not. + // Because moxChildren does ignore filtering, and because + // our base ignore method filters out invisible accessibles, + // we override this method. + if ([parent isKindOfClass:[MOXWebAreaAccessible class]] || + [parent isKindOfClass:[MOXRootGroup class]]) { + // We are a top level menu. Check our visibility the normal way + return [super moxIgnoreWithParent:parent]; + } + + if ([parent isKindOfClass:[mozMenuItemAccessible class]] && + [parent geckoAccessible].Role() == roles::PARENT_MENUITEM) { + // We are a submenu. If our parent menu item is in an open menu + // we should not be ignored + id grandparent = [parent moxParent]; + if ([grandparent isKindOfClass:[mozMenuAccessible class]]) { + mozMenuAccessible* parentMenu = + static_cast<mozMenuAccessible*>(grandparent); + return ![parentMenu isOpened]; + } + } + + // Otherwise, we call into our superclass's ignore method + // to handle menus that are not submenus + return [super moxIgnoreWithParent:parent]; +} + +- (NSArray*)moxVisibleChildren { + // VO expects us to expose two lists of children on menus: all children + // (done in moxUnignoredChildren), and children which are visible (here). + // We implement ignoreWithParent for both menus and menu items + // to ensure moxUnignoredChildren returns a complete list of children + // regardless of visibility, see comments in those methods for additional + // info. + return [[self moxChildren] + filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL( + mozAccessible* child, + NSDictionary* bindings) { + if (Accessible* acc = [child geckoAccessible].AsAccessible()) { + if (acc->IsContent() && acc->GetContent()->IsXULElement()) { + return ((acc->VisibilityState() & states::INVISIBLE) == 0); + } + } + return true; + }]]; +} + +- (id)moxTitleUIElement { + id parent = [self moxUnignoredParent]; + if (parent && [parent isKindOfClass:[mozAccessible class]]) { + return parent; + } + + return nil; +} + +- (void)moxPostNotification:(NSString*)notification { + if ([notification isEqualToString:@"AXMenuOpened"]) { + mIsOpened = YES; + } else if ([notification isEqualToString:@"AXMenuClosed"]) { + mIsOpened = NO; + } + + [super moxPostNotification:notification]; +} + +- (void)expire { + if (mIsOpened) { + // VO needs to receive a menu closed event when the menu goes away. + // If the menu is being destroyed, send a menu closed event first. + [self moxPostNotification:@"AXMenuClosed"]; + } + + [super expire]; +} + +- (BOOL)isOpened { + return mIsOpened; +} + +@end + +@implementation mozMenuItemAccessible + +- (NSString*)moxLabel { + return @""; +} + +- (BOOL)moxIgnoreWithParent:(mozAccessible*)parent { + // This helps us generate the correct moxChildren array for + // a mozMenuAccessible; the returned array should contain all + // menu items, regardless of if they are visible or not. + // Because moxChildren does ignore filtering, and because + // our base ignore method filters out invisible accessibles, + // we override this method. + AccessibleOrProxy parentAcc = [parent geckoAccessible]; + if (!parentAcc.IsNull()) { + AccessibleOrProxy grandparentAcc = parentAcc.Parent(); + if (mozAccessible* directGrandparent = + GetNativeFromGeckoAccessible(grandparentAcc)) { + if ([directGrandparent isKindOfClass:[MOXWebAreaAccessible class]]) { + return [parent moxIgnoreWithParent:directGrandparent]; + } + } + } + + id grandparent = [parent moxParent]; + if ([grandparent isKindOfClass:[mozMenuItemAccessible class]]) { + mozMenuItemAccessible* acc = + static_cast<mozMenuItemAccessible*>(grandparent); + if ([acc geckoAccessible].Role() == roles::PARENT_MENUITEM) { + mozMenuAccessible* parentMenu = static_cast<mozMenuAccessible*>(parent); + // if we are a menu item in a submenu, display only when + // parent menu item is open + return ![parentMenu isOpened]; + } + } + + // Otherwise, we call into our superclass's method to handle + // menuitems that are not within submenus + return [super moxIgnoreWithParent:parent]; +} + +- (NSString*)moxMenuItemMarkChar { + Accessible* acc = mGeckoAccessible.AsAccessible(); + if (acc && acc->IsContent() && + acc->GetContent()->IsXULElement(nsGkAtoms::menuitem)) { + // We need to provide a marker character. This is the visible "√" you see + // on dropdown menus. In our a11y tree this is a single child text node + // of the menu item. + // We do this only with XUL menuitems that conform to the native theme, and + // not with aria menu items that might have a pseudo element or something. + if (acc->ChildCount() == 1 && + acc->FirstChild()->Role() == roles::STATICTEXT) { + nsAutoString marker; + acc->FirstChild()->Name(marker); + if (marker.Length() == 1) { + return nsCocoaUtils::ToNSString(marker); + } + } + } + + return nil; +} + +- (NSNumber*)moxSelected { + // Our focused state is equivelent to native selected states for menus. + return @([self stateWithMask:states::FOCUSED] != 0); +} + +- (void)handleAccessibleEvent:(uint32_t)eventType { + switch (eventType) { + case nsIAccessibleEvent::EVENT_FOCUS: + // Our focused state is equivelent to native selected states for menus. + mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent]; + [parent moxPostNotification: + NSAccessibilitySelectedChildrenChangedNotification]; + break; + } + + [super handleAccessibleEvent:eventType]; +} + +@end diff --git a/accessible/mac/mozTableAccessible.h b/accessible/mac/mozTableAccessible.h new file mode 100644 index 0000000000..756359e5cf --- /dev/null +++ b/accessible/mac/mozTableAccessible.h @@ -0,0 +1,165 @@ +/* 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 "mozAccessible.h" + +@interface mozColumnContainer : MOXAccessibleBase { + uint32_t mIndex; + mozAccessible* mParent; + NSMutableArray* mChildren; +} + +// override +- (id)initWithIndex:(uint32_t)aIndex andParent:(mozAccessible*)aParent; + +// override +- (NSString*)moxRole; + +// override +- (NSString*)moxRoleDescription; + +// override +- (mozAccessible*)moxParent; + +// override +- (NSArray*)moxUnignoredChildren; + +// override +- (void)dealloc; + +// override +- (void)expire; + +// override +- (BOOL)isExpired; + +- (void)invalidateChildren; + +@end + +@interface mozTablePartAccessible : mozAccessible + +// override +- (NSString*)moxTitle; + +// override +- (NSString*)moxRole; + +- (BOOL)isLayoutTablePart; + +@end + +@interface mozTableAccessible : mozTablePartAccessible { + NSMutableArray* mColContainers; +} + +- (void)invalidateColumns; + +// override +- (void)handleAccessibleEvent:(uint32_t)eventType; + +// override +- (void)dealloc; + +// override +- (NSNumber*)moxRowCount; + +// override +- (NSNumber*)moxColumnCount; + +// override +- (NSArray*)moxRows; + +// override +- (NSArray*)moxColumns; + +// override +- (NSArray*)moxUnignoredChildren; + +// override +- (NSArray*)moxColumnHeaderUIElements; + +// override +- (id)moxCellForColumnAndRow:(NSArray*)columnAndRow; + +@end + +@interface mozTableRowAccessible : mozTablePartAccessible + +// override +- (void)handleAccessibleEvent:(uint32_t)eventType; + +// override +- (NSNumber*)moxIndex; + +@end + +@interface mozTableCellAccessible : mozTablePartAccessible + +// override +- (NSValue*)moxRowIndexRange; + +// override +- (NSValue*)moxColumnIndexRange; + +// override +- (NSArray*)moxRowHeaderUIElements; + +// override +- (NSArray*)moxColumnHeaderUIElements; + +@end + +@interface mozOutlineAccessible : mozAccessible + +// override +- (NSArray*)moxRows; + +// override +- (NSArray*)moxColumns; + +// override +- (NSArray*)moxSelectedRows; + +// override +- (NSString*)moxOrientation; + +@end + +@interface mozOutlineRowAccessible : mozTableRowAccessible + +// override +- (BOOL)isLayoutTablePart; + +// override +- (NSNumber*)moxDisclosing; + +// override +- (void)moxSetDisclosing:(NSNumber*)disclosing; + +// override +- (NSNumber*)moxExpanded; + +// override +- (id)moxDisclosedByRow; + +// override +- (NSNumber*)moxDisclosureLevel; + +// override +- (NSArray*)moxDisclosedRows; + +// override +- (NSNumber*)moxIndex; + +// override +- (NSString*)moxLabel; + +// override +- (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled; + +@end diff --git a/accessible/mac/mozTableAccessible.mm b/accessible/mac/mozTableAccessible.mm new file mode 100644 index 0000000000..292970323a --- /dev/null +++ b/accessible/mac/mozTableAccessible.mm @@ -0,0 +1,591 @@ +/* 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 "mozTableAccessible.h" +#import "nsCocoaUtils.h" +#import "MacUtils.h" +#import "RotorRules.h" + +#include "AccIterator.h" +#include "Accessible.h" +#include "TableAccessible.h" +#include "TableCellAccessible.h" +#include "XULTreeAccessible.h" +#include "Pivot.h" +#include "Relation.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +@implementation mozColumnContainer + +- (id)initWithIndex:(uint32_t)aIndex andParent:(mozAccessible*)aParent { + self = [super init]; + mIndex = aIndex; + mParent = aParent; + return self; +} + +- (NSString*)moxRole { + return NSAccessibilityColumnRole; +} + +- (NSString*)moxRoleDescription { + return NSAccessibilityRoleDescription(NSAccessibilityColumnRole, nil); +} + +- (mozAccessible*)moxParent { + return mParent; +} + +- (NSArray*)moxUnignoredChildren { + if (mChildren) return mChildren; + + mChildren = [[NSMutableArray alloc] init]; + + if (Accessible* acc = [mParent geckoAccessible].AsAccessible()) { + TableAccessible* table = acc->AsTable(); + MOZ_ASSERT(table, "Got null table when fetching column children!"); + uint32_t numRows = table->RowCount(); + + for (uint32_t j = 0; j < numRows; j++) { + Accessible* cell = table->CellAt(j, mIndex); + mozAccessible* nativeCell = + cell ? GetNativeFromGeckoAccessible(cell) : nil; + if ([nativeCell isAccessibilityElement]) { + [mChildren addObject:nativeCell]; + } + } + + } else if (ProxyAccessible* proxy = [mParent geckoAccessible].AsProxy()) { + uint32_t numRows = proxy->TableRowCount(); + + for (uint32_t j = 0; j < numRows; j++) { + ProxyAccessible* cell = proxy->TableCellAt(j, mIndex); + mozAccessible* nativeCell = + cell ? GetNativeFromGeckoAccessible(cell) : nil; + if ([nativeCell isAccessibilityElement]) { + [mChildren addObject:nativeCell]; + } + } + } + + return mChildren; +} + +- (void)dealloc { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + [self invalidateChildren]; + [super dealloc]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (void)expire { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + [self invalidateChildren]; + + mParent = nil; + + [super expire]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (BOOL)isExpired { + MOZ_ASSERT((mChildren == nil && mParent == nil) == mIsExpired); + + return [super isExpired]; +} + +- (void)invalidateChildren { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + // make room for new children + if (mChildren) { + [mChildren release]; + mChildren = nil; + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +@end + +@implementation mozTablePartAccessible + +- (NSString*)moxTitle { + return @""; +} + +- (NSString*)moxRole { + return [self isLayoutTablePart] ? NSAccessibilityGroupRole : [super moxRole]; +} + +- (BOOL)isLayoutTablePart { + if (Accessible* acc = mGeckoAccessible.AsAccessible()) { + while (acc) { + if (acc->Role() == roles::TREE_TABLE) { + return false; + } + if (acc->IsTable()) { + return acc->AsTable()->IsProbablyLayoutTable(); + } + acc = acc->Parent(); + } + return false; + } + + if (ProxyAccessible* proxy = mGeckoAccessible.AsProxy()) { + while (proxy) { + if (proxy->Role() == roles::TREE_TABLE) { + return false; + } + if (proxy->IsTable()) { + return proxy->TableIsProbablyForLayout(); + } + proxy = proxy->Parent(); + } + } + + return false; +} + +@end + +@implementation mozTableAccessible + +- (void)handleAccessibleEvent:(uint32_t)eventType { + if (eventType == nsIAccessibleEvent::EVENT_REORDER) { + [self invalidateColumns]; + } + + [super handleAccessibleEvent:eventType]; +} + +- (void)dealloc { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + [self invalidateColumns]; + [super dealloc]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (NSNumber*)moxRowCount { + MOZ_ASSERT(!mGeckoAccessible.IsNull()); + + return mGeckoAccessible.IsAccessible() + ? @(mGeckoAccessible.AsAccessible()->AsTable()->RowCount()) + : @(mGeckoAccessible.AsProxy()->TableRowCount()); +} + +- (NSNumber*)moxColumnCount { + MOZ_ASSERT(!mGeckoAccessible.IsNull()); + + return mGeckoAccessible.IsAccessible() + ? @(mGeckoAccessible.AsAccessible()->AsTable()->ColCount()) + : @(mGeckoAccessible.AsProxy()->TableColumnCount()); +} + +- (NSArray*)moxRows { + // Create a new array with the list of table rows. + return [[self moxChildren] + filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL( + mozAccessible* child, + NSDictionary* bindings) { + return [child isKindOfClass:[mozTableRowAccessible class]]; + }]]; +} + +- (NSArray*)moxColumns { + MOZ_ASSERT(!mGeckoAccessible.IsNull()); + + if (mColContainers) { + return mColContainers; + } + + mColContainers = [[NSMutableArray alloc] init]; + uint32_t numCols = 0; + + if (Accessible* acc = mGeckoAccessible.AsAccessible()) { + numCols = acc->AsTable()->ColCount(); + } else { + numCols = mGeckoAccessible.AsProxy()->TableColumnCount(); + } + + for (uint32_t i = 0; i < numCols; i++) { + mozColumnContainer* container = + [[mozColumnContainer alloc] initWithIndex:i andParent:self]; + [mColContainers addObject:container]; + } + + return mColContainers; +} + +- (NSArray*)moxUnignoredChildren { + if (![self isLayoutTablePart]) { + return [[super moxUnignoredChildren] + arrayByAddingObjectsFromArray:[self moxColumns]]; + } + + return [super moxUnignoredChildren]; +} + +- (NSArray*)moxColumnHeaderUIElements { + MOZ_ASSERT(!mGeckoAccessible.IsNull()); + + uint32_t numCols = 0; + TableAccessible* table = nullptr; + + if (Accessible* acc = mGeckoAccessible.AsAccessible()) { + table = mGeckoAccessible.AsAccessible()->AsTable(); + numCols = table->ColCount(); + } else { + numCols = mGeckoAccessible.AsProxy()->TableColumnCount(); + } + + NSMutableArray* colHeaders = + [[NSMutableArray alloc] initWithCapacity:numCols]; + + for (uint32_t i = 0; i < numCols; i++) { + AccessibleOrProxy cell; + if (table) { + cell = table->CellAt(0, i); + } else { + cell = mGeckoAccessible.AsProxy()->TableCellAt(0, i); + } + + if (!cell.IsNull() && cell.Role() == roles::COLUMNHEADER) { + mozAccessible* colHeader = GetNativeFromGeckoAccessible(cell); + [colHeaders addObject:colHeader]; + } + } + + return colHeaders; +} + +- (id)moxCellForColumnAndRow:(NSArray*)columnAndRow { + if (columnAndRow == nil || [columnAndRow count] != 2) { + return nil; + } + + uint32_t col = [[columnAndRow objectAtIndex:0] unsignedIntValue]; + uint32_t row = [[columnAndRow objectAtIndex:1] unsignedIntValue]; + + MOZ_ASSERT(!mGeckoAccessible.IsNull()); + + AccessibleOrProxy cell; + if (mGeckoAccessible.IsAccessible()) { + cell = mGeckoAccessible.AsAccessible()->AsTable()->CellAt(row, col); + } else { + cell = mGeckoAccessible.AsProxy()->TableCellAt(row, col); + } + + if (cell.IsNull()) { + return nil; + } + + return GetNativeFromGeckoAccessible(cell); +} + +- (void)invalidateColumns { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + if (mColContainers) { + [mColContainers release]; + mColContainers = nil; + } + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +@end + +@implementation mozTableRowAccessible + +- (void)handleAccessibleEvent:(uint32_t)eventType { + if (eventType == nsIAccessibleEvent::EVENT_REORDER) { + id parent = [self moxParent]; + if ([parent isKindOfClass:[mozTableAccessible class]]) { + [parent invalidateColumns]; + } + } + + [super handleAccessibleEvent:eventType]; +} + +- (NSNumber*)moxIndex { + mozTableAccessible* parent = (mozTableAccessible*)[self moxParent]; + return @([[parent moxRows] indexOfObjectIdenticalTo:self]); +} + +@end + +@implementation mozTableCellAccessible + +- (NSValue*)moxRowIndexRange { + MOZ_ASSERT(!mGeckoAccessible.IsNull()); + + if (mGeckoAccessible.IsAccessible()) { + TableCellAccessible* cell = mGeckoAccessible.AsAccessible()->AsTableCell(); + return + [NSValue valueWithRange:NSMakeRange(cell->RowIdx(), cell->RowExtent())]; + } else { + ProxyAccessible* proxy = mGeckoAccessible.AsProxy(); + return [NSValue + valueWithRange:NSMakeRange(proxy->RowIdx(), proxy->RowExtent())]; + } +} + +- (NSValue*)moxColumnIndexRange { + MOZ_ASSERT(!mGeckoAccessible.IsNull()); + + if (mGeckoAccessible.IsAccessible()) { + TableCellAccessible* cell = mGeckoAccessible.AsAccessible()->AsTableCell(); + return + [NSValue valueWithRange:NSMakeRange(cell->ColIdx(), cell->ColExtent())]; + } else { + ProxyAccessible* proxy = mGeckoAccessible.AsProxy(); + return [NSValue + valueWithRange:NSMakeRange(proxy->ColIdx(), proxy->ColExtent())]; + } +} + +- (NSArray*)moxRowHeaderUIElements { + MOZ_ASSERT(!mGeckoAccessible.IsNull()); + + if (mGeckoAccessible.IsAccessible()) { + TableCellAccessible* cell = mGeckoAccessible.AsAccessible()->AsTableCell(); + AutoTArray<Accessible*, 10> headerCells; + cell->RowHeaderCells(&headerCells); + return utils::ConvertToNSArray(headerCells); + } else { + ProxyAccessible* proxy = mGeckoAccessible.AsProxy(); + nsTArray<ProxyAccessible*> headerCells; + proxy->RowHeaderCells(&headerCells); + return utils::ConvertToNSArray(headerCells); + } +} + +- (NSArray*)moxColumnHeaderUIElements { + MOZ_ASSERT(!mGeckoAccessible.IsNull()); + + if (mGeckoAccessible.IsAccessible()) { + TableCellAccessible* cell = mGeckoAccessible.AsAccessible()->AsTableCell(); + AutoTArray<Accessible*, 10> headerCells; + cell->ColHeaderCells(&headerCells); + return utils::ConvertToNSArray(headerCells); + } else { + ProxyAccessible* proxy = mGeckoAccessible.AsProxy(); + nsTArray<ProxyAccessible*> headerCells; + proxy->ColHeaderCells(&headerCells); + return utils::ConvertToNSArray(headerCells); + } +} + +@end + +@implementation mozOutlineAccessible + +- (NSArray*)moxRows { + // Create a new array with the list of outline rows. We + // use pivot here to do a deep traversal of all rows nested + // in this outline, not just those which are direct + // children, since that's what VO expects. + NSMutableArray* allRows = [[NSMutableArray alloc] init]; + Pivot p = Pivot(mGeckoAccessible); + OutlineRule rule = OutlineRule(); + AccessibleOrProxy firstChild = mGeckoAccessible.FirstChild(); + AccessibleOrProxy match = p.Next(firstChild, rule, true); + while (!match.IsNull()) { + [allRows addObject:GetNativeFromGeckoAccessible(match)]; + match = p.Next(match, rule); + } + return allRows; +} + +- (NSArray*)moxColumns { + if (Accessible* acc = mGeckoAccessible.AsAccessible()) { + if (acc->IsContent() && acc->GetContent()->IsXULElement(nsGkAtoms::tree)) { + XULTreeAccessible* treeAcc = (XULTreeAccessible*)acc; + NSMutableArray* cols = [[NSMutableArray alloc] init]; + // XUL trees store their columns in a group at the tree's first + // child. Here, we iterate over that group to get each column's + // native accessible and add it to our col array. + Accessible* treeColumns = treeAcc->GetChildAt(0); + if (treeColumns) { + uint32_t colCount = treeColumns->ChildCount(); + for (uint32_t i = 0; i < colCount; i++) { + Accessible* treeColumnItem = treeColumns->GetChildAt(i); + [cols addObject:GetNativeFromGeckoAccessible(treeColumnItem)]; + } + return cols; + } + } + } + // Webkit says we shouldn't expose any cols for aria-tree + // so we return an empty array here + return @[]; +} + +- (NSArray*)moxSelectedRows { + NSMutableArray* selectedRows = [[NSMutableArray alloc] init]; + NSArray* allRows = [self moxRows]; + for (mozAccessible* row in allRows) { + if ([row stateWithMask:states::SELECTED] != 0) { + [selectedRows addObject:row]; + } + } + + return selectedRows; +} + +- (NSString*)moxOrientation { + return NSAccessibilityVerticalOrientationValue; +} + +@end + +@implementation mozOutlineRowAccessible + +- (BOOL)isLayoutTablePart { + return NO; +} + +- (NSNumber*)moxDisclosing { + return @([self stateWithMask:states::EXPANDED] != 0); +} + +- (void)moxSetDisclosing:(NSNumber*)disclosing { + // VoiceOver requires this to be settable, but doesn't + // require it actually affect our disclosing state. + // We expose the attr as settable with this method + // but do nothing to actually set it. + return; +} + +- (NSNumber*)moxExpanded { + return @([self stateWithMask:states::EXPANDED] != 0); +} + +- (id)moxDisclosedByRow { + // According to webkit: this attr corresponds to the row + // that contains this row. It should be the same as the + // first parent that is a treeitem. If the parent is the tree + // itself, this should be nil. This is tricky for xul trees because + // all rows are direct children of the outline; they use + // relations to expose their heirarchy structure. + + // first we check the relations to see if we're in a xul tree + // with weird row semantics + NSArray<mozAccessible*>* disclosingRows = + [self getRelationsByType:RelationType::NODE_CHILD_OF]; + mozAccessible* disclosingRow = [disclosingRows firstObject]; + + if (disclosingRow) { + // if we find a row from our relation check, + // verify it isn't the outline itself and return + // appropriately + if ([[disclosingRow moxRole] isEqualToString:@"AXOutline"]) { + return nil; + } + + return disclosingRow; + } + + mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent]; + // otherwise, its likely we're in an aria tree, so we can use + // these role and subrole checks + if ([[parent moxRole] isEqualToString:@"AXOutline"]) { + return nil; + } + + if ([[parent moxSubrole] isEqualToString:@"AXOutlineRow"]) { + disclosingRow = parent; + } + + return nil; +} + +- (NSNumber*)moxDisclosureLevel { + GroupPos groupPos; + if (Accessible* acc = mGeckoAccessible.AsAccessible()) { + groupPos = acc->GroupPosition(); + } else if (ProxyAccessible* proxy = mGeckoAccessible.AsProxy()) { + groupPos = proxy->GroupPosition(); + } + // mac expects 0-indexed levels, but groupPos.level is 1-indexed + // so we subtract 1 here for levels above 0 + return groupPos.level > 0 ? @(groupPos.level - 1) : @(groupPos.level); +} + +- (NSArray*)moxDisclosedRows { + // According to webkit: this attr corresponds to the rows + // that are considered inside this row. Again, this is weird for + // xul trees so we have to use relations first and then fall-back + // to the children filter for non-xul outlines. + + // first we check the relations to see if we're in a xul tree + // with weird row semantics + if (NSArray* disclosedRows = + [self getRelationsByType:RelationType::NODE_PARENT_OF]) { + // if we find rows from our relation check, return them here + return disclosedRows; + } + + // otherwise, filter our children for outline rows + return [[self moxChildren] + filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL( + mozAccessible* child, + NSDictionary* bindings) { + return [child isKindOfClass:[mozOutlineRowAccessible class]]; + }]]; +} + +- (NSNumber*)moxIndex { + mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent]; + while (parent) { + if ([[parent moxRole] isEqualToString:@"AXOutline"]) { + break; + } + parent = (mozAccessible*)[parent moxUnignoredParent]; + } + + NSUInteger index = + [[(mozOutlineAccessible*)parent moxRows] indexOfObjectIdenticalTo:self]; + return index == NSNotFound ? nil : @(index); +} + +- (NSString*)moxLabel { + nsAutoString title; + if (Accessible* acc = mGeckoAccessible.AsAccessible()) { + acc->Name(title); + } else { + mGeckoAccessible.AsProxy()->Name(title); + } + // XXX: When parsing outlines built with ul/lu's, we + // include the bullet in this description even + // though webkit doesn't. Not all outlines are built with + // ul/lu's so we can't strip the first character here. + + return nsCocoaUtils::ToNSString(title); +} + +- (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled { + [super stateChanged:state isEnabled:enabled]; + + if (state == states::EXPANDED) { + // If the EXPANDED state is updated, fire appropriate events on the + // outline row. + [self moxPostNotification:(enabled + ? NSAccessibilityRowExpandedNotification + : NSAccessibilityRowCollapsedNotification)]; + } +} + +@end diff --git a/accessible/mac/mozTextAccessible.h b/accessible/mac/mozTextAccessible.h new file mode 100644 index 0000000000..55d0ac7a06 --- /dev/null +++ b/accessible/mac/mozTextAccessible.h @@ -0,0 +1,113 @@ +/* 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 "mozAccessible.h" + +@interface mozTextAccessible : mozAccessible + +// override +- (NSString*)moxTitle; + +// override +- (id)moxValue; + +// override +- (id)moxRequired; + +// override +- (NSNumber*)moxInvalid; + +// override +- (NSNumber*)moxInsertionPointLineNumber; + +// override +- (NSString*)moxRole; + +// override +- (NSString*)moxSubrole; + +// override +- (NSNumber*)moxNumberOfCharacters; + +// override +- (NSString*)moxSelectedText; + +// override +- (NSValue*)moxSelectedTextRange; + +// override +- (NSValue*)moxVisibleCharacterRange; + +// override +- (BOOL)moxBlockSelector:(SEL)selector; + +// override +- (void)moxSetValue:(id)value; + +// override +- (void)moxSetSelectedText:(NSString*)text; + +// override +- (void)moxSetSelectedTextRange:(NSValue*)range; + +// override +- (void)moxSetVisibleCharacterRange:(NSValue*)range; + +// override +- (NSString*)moxStringForRange:(NSValue*)range; + +// override +- (NSAttributedString*)moxAttributedStringForRange:(NSValue*)range; + +// override +- (NSValue*)moxRangeForLine:(NSNumber*)line; + +// override +- (NSNumber*)moxLineForIndex:(NSNumber*)index; + +// override +- (NSValue*)moxBoundsForRange:(NSValue*)range; + +#pragma mark - mozAccessible + +// override +- (void)handleAccessibleTextChangeEvent:(NSString*)change + inserted:(BOOL)isInserted + inContainer: + (const mozilla::a11y::AccessibleOrProxy&) + container + at:(int32_t)start; + +// override +- (void)handleAccessibleEvent:(uint32_t)eventType; + +@end + +@interface mozTextLeafAccessible : mozAccessible + +// override +- (BOOL)moxBlockSelector:(SEL)selector; + +// override +- (NSString*)moxValue; + +// override +- (NSString*)moxTitle; + +// override +- (NSString*)moxLabel; + +// override +- (NSString*)moxStringForRange:(NSValue*)range; + +// override +- (NSAttributedString*)moxAttributedStringForRange:(NSValue*)range; + +// override +- (NSValue*)moxBoundsForRange:(NSValue*)range; + +@end diff --git a/accessible/mac/mozTextAccessible.mm b/accessible/mac/mozTextAccessible.mm new file mode 100644 index 0000000000..12d654f67a --- /dev/null +++ b/accessible/mac/mozTextAccessible.mm @@ -0,0 +1,448 @@ +/* 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/. */ + +#include "Accessible-inl.h" +#include "HyperTextAccessible-inl.h" +#include "mozilla/a11y/PDocAccessible.h" +#include "nsCocoaUtils.h" +#include "nsIPersistentProperties2.h" +#include "nsObjCExceptions.h" +#include "TextLeafAccessible.h" + +#import "mozTextAccessible.h" +#import "GeckoTextMarker.h" +#import "MOXTextMarkerDelegate.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +inline bool ToNSRange(id aValue, NSRange* aRange) { + MOZ_ASSERT(aRange, "aRange is nil"); + + if ([aValue isKindOfClass:[NSValue class]] && + strcmp([(NSValue*)aValue objCType], @encode(NSRange)) == 0) { + *aRange = [aValue rangeValue]; + return true; + } + + return false; +} + +inline NSString* ToNSString(id aValue) { + if ([aValue isKindOfClass:[NSString class]]) { + return aValue; + } + + return nil; +} + +@interface mozTextAccessible () +- (long)textLength; +- (BOOL)isReadOnly; +- (NSString*)text; +- (GeckoTextMarkerRange)selection; +- (GeckoTextMarkerRange)textMarkerRangeFromRange:(NSValue*)range; +@end + +@implementation mozTextAccessible + +- (NSString*)moxTitle { + return @""; +} + +- (id)moxValue { + // Apple's SpeechSynthesisServer expects AXValue to return an AXStaticText + // object's AXSelectedText attribute. See bug 674612 for details. + // Also if there is no selected text, we return the full text. + // See bug 369710 for details. + if ([[self moxRole] isEqualToString:NSAccessibilityStaticTextRole]) { + NSString* selectedText = [self moxSelectedText]; + return (selectedText && [selectedText length]) ? selectedText : [self text]; + } + + return [self text]; +} + +- (id)moxRequired { + return @([self stateWithMask:states::REQUIRED] != 0); +} + +- (NSString*)moxInvalid { + if ([self stateWithMask:states::INVALID] != 0) { + // If the attribute exists, it has one of four values: true, false, + // grammar, or spelling. We query the attribute value here in order + // to find the correct string to return. + if (Accessible* acc = mGeckoAccessible.AsAccessible()) { + HyperTextAccessible* text = acc->AsHyperText(); + if (!text || !text->IsTextRole()) { + // we can't get the attribute, but we should still respect the + // invalid state flag + return @"true"; + } + nsAutoString invalidStr; + nsCOMPtr<nsIPersistentProperties> attributes = + text->DefaultTextAttributes(); + nsAccUtils::GetAccAttr(attributes, nsGkAtoms::invalid, invalidStr); + if (invalidStr.IsEmpty()) { + // if the attribute had no value, we should still respect the + // invalid state flag. + return @"true"; + } + return nsCocoaUtils::ToNSString(invalidStr); + } else { + ProxyAccessible* proxy = mGeckoAccessible.AsProxy(); + // Similar to the acc case above, we iterate through our attributes + // to find the value for `invalid`. + AutoTArray<Attribute, 10> attrs; + proxy->DefaultTextAttributes(&attrs); + for (size_t i = 0; i < attrs.Length(); i++) { + if (attrs.ElementAt(i).Name() == "invalid") { + nsString invalidStr = attrs.ElementAt(i).Value(); + if (invalidStr.IsEmpty()) { + break; + } + return nsCocoaUtils::ToNSString(invalidStr); + } + } + // if we iterated through our attributes and didn't find `invalid`, + // or if the invalid attribute had no value, we should still respect + // the invalid flag and return true. + return @"true"; + } + } + // If the flag is not set, we return false. + return @"false"; +} + +- (NSNumber*)moxInsertionPointLineNumber { + MOZ_ASSERT(!mGeckoAccessible.IsNull()); + + int32_t lineNumber = -1; + if (mGeckoAccessible.IsAccessible()) { + if (HyperTextAccessible* textAcc = + mGeckoAccessible.AsAccessible()->AsHyperText()) { + lineNumber = textAcc->CaretLineNumber() - 1; + } + } else { + lineNumber = mGeckoAccessible.AsProxy()->CaretLineNumber() - 1; + } + + return (lineNumber >= 0) ? [NSNumber numberWithInt:lineNumber] : nil; +} + +- (NSString*)moxRole { + if ([self ARIARole] == nsGkAtoms::textbox || + [self stateWithMask:states::MULTI_LINE]) { + return NSAccessibilityTextAreaRole; + } + + return [super moxRole]; +} + +- (NSString*)moxSubrole { + MOZ_ASSERT(!mGeckoAccessible.IsNull()); + + if (mRole == roles::PASSWORD_TEXT) { + return NSAccessibilitySecureTextFieldSubrole; + } + + if (mRole == roles::ENTRY) { + Accessible* acc = mGeckoAccessible.AsAccessible(); + ProxyAccessible* proxy = mGeckoAccessible.AsProxy(); + if ((acc && acc->IsSearchbox()) || (proxy && proxy->IsSearchbox())) { + return @"AXSearchField"; + } + } + + return nil; +} + +- (NSNumber*)moxNumberOfCharacters { + return @([self textLength]); +} + +- (NSString*)moxSelectedText { + GeckoTextMarkerRange selection = [self selection]; + if (!selection.IsValid()) { + return nil; + } + + return selection.Text(); +} + +- (NSValue*)moxSelectedTextRange { + GeckoTextMarkerRange selection = [self selection]; + if (!selection.IsValid()) { + return nil; + } + + GeckoTextMarkerRange fromStartToSelection( + GeckoTextMarker(mGeckoAccessible, 0), selection.mStart); + + return [NSValue valueWithRange:NSMakeRange(fromStartToSelection.Length(), + selection.Length())]; +} + +- (NSValue*)moxVisibleCharacterRange { + // XXX this won't work with Textarea and such as we actually don't give + // the visible character range. + return [NSValue valueWithRange:NSMakeRange(0, [self textLength])]; +} + +- (BOOL)moxBlockSelector:(SEL)selector { + if (selector == @selector(moxSetValue:) && [self isReadOnly]) { + return YES; + } + + return [super moxBlockSelector:selector]; +} + +- (void)moxSetValue:(id)value { + MOZ_ASSERT(!mGeckoAccessible.IsNull()); + + nsString text; + nsCocoaUtils::GetStringForNSString(value, text); + if (mGeckoAccessible.IsAccessible()) { + if (HyperTextAccessible* textAcc = + mGeckoAccessible.AsAccessible()->AsHyperText()) { + textAcc->ReplaceText(text); + } + } else { + mGeckoAccessible.AsProxy()->ReplaceText(text); + } +} + +- (void)moxSetSelectedText:(NSString*)selectedText { + MOZ_ASSERT(!mGeckoAccessible.IsNull()); + + NSString* stringValue = ToNSString(selectedText); + if (!stringValue) { + return; + } + + int32_t start = 0, end = 0; + nsString text; + if (mGeckoAccessible.IsAccessible()) { + if (HyperTextAccessible* textAcc = + mGeckoAccessible.AsAccessible()->AsHyperText()) { + textAcc->SelectionBoundsAt(0, &start, &end); + textAcc->DeleteText(start, end - start); + nsCocoaUtils::GetStringForNSString(stringValue, text); + textAcc->InsertText(text, start); + } + } else { + ProxyAccessible* proxy = mGeckoAccessible.AsProxy(); + nsString data; + proxy->SelectionBoundsAt(0, data, &start, &end); + proxy->DeleteText(start, end - start); + nsCocoaUtils::GetStringForNSString(stringValue, text); + proxy->InsertText(text, start); + } +} + +- (void)moxSetSelectedTextRange:(NSValue*)selectedTextRange { + GeckoTextMarkerRange markerRange = + [self textMarkerRangeFromRange:selectedTextRange]; + + markerRange.Select(); +} + +- (void)moxSetVisibleCharacterRange:(NSValue*)visibleCharacterRange { + MOZ_ASSERT(!mGeckoAccessible.IsNull()); + + NSRange range; + if (!ToNSRange(visibleCharacterRange, &range)) { + return; + } + + if (mGeckoAccessible.IsAccessible()) { + if (HyperTextAccessible* textAcc = + mGeckoAccessible.AsAccessible()->AsHyperText()) { + textAcc->ScrollSubstringTo(range.location, range.location + range.length, + nsIAccessibleScrollType::SCROLL_TYPE_TOP_EDGE); + } + } else { + mGeckoAccessible.AsProxy()->ScrollSubstringTo( + range.location, range.location + range.length, + nsIAccessibleScrollType::SCROLL_TYPE_TOP_EDGE); + } +} + +- (NSString*)moxStringForRange:(NSValue*)range { + GeckoTextMarkerRange markerRange = [self textMarkerRangeFromRange:range]; + + if (!markerRange.IsValid()) { + return nil; + } + + return markerRange.Text(); +} + +- (NSAttributedString*)moxAttributedStringForRange:(NSValue*)range { + return [[[NSAttributedString alloc] + initWithString:[self moxStringForRange:range]] autorelease]; +} + +- (NSValue*)moxRangeForLine:(NSNumber*)line { + // XXX: actually get the integer value for the line # + return [NSValue valueWithRange:NSMakeRange(0, [self textLength])]; +} + +- (NSNumber*)moxLineForIndex:(NSNumber*)index { + // XXX: actually return the line # + return @0; +} + +- (NSValue*)moxBoundsForRange:(NSValue*)range { + GeckoTextMarkerRange markerRange = [self textMarkerRangeFromRange:range]; + + if (!markerRange.IsValid()) { + return nil; + } + + return markerRange.Bounds(); +} + +#pragma mark - mozAccessible + +- (void)handleAccessibleTextChangeEvent:(NSString*)change + inserted:(BOOL)isInserted + inContainer:(const AccessibleOrProxy&)container + at:(int32_t)start { + GeckoTextMarker startMarker(container, start); + NSDictionary* userInfo = @{ + @"AXTextChangeElement" : self, + @"AXTextStateChangeType" : @(AXTextStateChangeTypeEdit), + @"AXTextChangeValues" : @[ @{ + @"AXTextChangeValue" : (change ? change : @""), + @"AXTextChangeValueStartMarker" : startMarker.CreateAXTextMarker(), + @"AXTextEditType" : isInserted ? @(AXTextEditTypeTyping) + : @(AXTextEditTypeDelete) + } ] + }; + + mozAccessible* webArea = [self topWebArea]; + [webArea moxPostNotification:NSAccessibilityValueChangedNotification + withUserInfo:userInfo]; + [self moxPostNotification:NSAccessibilityValueChangedNotification + withUserInfo:userInfo]; + + [self moxPostNotification:NSAccessibilityValueChangedNotification]; +} + +- (void)handleAccessibleEvent:(uint32_t)eventType { + switch (eventType) { + default: + [super handleAccessibleEvent:eventType]; + break; + } +} + +#pragma mark - + +- (long)textLength { + return [[self text] length]; +} + +- (BOOL)isReadOnly { + return [self stateWithMask:states::EDITABLE] == 0; +} + +- (NSString*)text { + // A password text field returns an empty value + if (mRole == roles::PASSWORD_TEXT) { + return @""; + } + + id<MOXTextMarkerSupport> delegate = [self moxTextMarkerDelegate]; + return [delegate + moxStringForTextMarkerRange:[delegate + moxTextMarkerRangeForUIElement:self]]; +} + +- (GeckoTextMarkerRange)selection { + MOZ_ASSERT(!mGeckoAccessible.IsNull()); + + id<MOXTextMarkerSupport> delegate = [self moxTextMarkerDelegate]; + GeckoTextMarkerRange selection = + [static_cast<MOXTextMarkerDelegate*>(delegate) selection]; + + if (!selection.IsValid() || !selection.Crop(mGeckoAccessible)) { + // The selection is not in this accessible. Return invalid range. + return GeckoTextMarkerRange(); + } + + return selection; +} + +- (GeckoTextMarkerRange)textMarkerRangeFromRange:(NSValue*)range { + NSRange r = [range rangeValue]; + + GeckoTextMarker startMarker = + GeckoTextMarker::MarkerFromIndex(mGeckoAccessible, r.location); + + GeckoTextMarker endMarker = + GeckoTextMarker::MarkerFromIndex(mGeckoAccessible, r.location + r.length); + + return GeckoTextMarkerRange(startMarker, endMarker); +} + +@end + +@implementation mozTextLeafAccessible + +- (BOOL)moxBlockSelector:(SEL)selector { + if (selector == @selector(moxChildren) || selector == @selector + (moxTitleUIElement)) { + return YES; + } + + return [super moxBlockSelector:selector]; +} + +- (NSString*)moxValue { + return [super moxTitle]; +} + +- (NSString*)moxTitle { + return nil; +} + +- (NSString*)moxLabel { + return nil; +} + +- (NSString*)moxStringForRange:(NSValue*)range { + MOZ_ASSERT(!mGeckoAccessible.IsNull()); + + NSRange r = [range rangeValue]; + GeckoTextMarkerRange textMarkerRange(mGeckoAccessible); + textMarkerRange.mStart.mOffset += r.location; + textMarkerRange.mEnd.mOffset = + textMarkerRange.mStart.mOffset + r.location + r.length; + + return textMarkerRange.Text(); +} + +- (NSAttributedString*)moxAttributedStringForRange:(NSValue*)range { + return [[[NSAttributedString alloc] + initWithString:[self moxStringForRange:range]] autorelease]; +} + +- (NSValue*)moxBoundsForRange:(NSValue*)range { + MOZ_ASSERT(!mGeckoAccessible.IsNull()); + + NSRange r = [range rangeValue]; + GeckoTextMarkerRange textMarkerRange(mGeckoAccessible); + + textMarkerRange.mStart.mOffset += r.location; + textMarkerRange.mEnd.mOffset = textMarkerRange.mStart.mOffset + r.length; + + return textMarkerRange.Bounds(); +} + +@end |