diff options
Diffstat (limited to '')
48 files changed, 10338 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/AccessibleWrap.h b/accessible/mac/AccessibleWrap.h new file mode 100644 index 0000000000..4ece00cbd8 --- /dev/null +++ b/accessible/mac/AccessibleWrap.h @@ -0,0 +1,98 @@ +/* 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 "LocalAccessible.h" +#include "PlatformExtTypes.h" +#include "States.h" + +#include "nsCOMPtr.h" + +#include "nsTArray.h" + +#if defined(__OBJC__) +@class mozAccessible; +#endif + +namespace mozilla { +namespace a11y { + +/** + * Mac specific functionality for an accessibility tree node that originated in + * mDoc's content process. + */ +class AccessibleWrap : public LocalAccessible { + 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..944e8bd059 --- /dev/null +++ b/accessible/mac/AccessibleWrap.mm @@ -0,0 +1,413 @@ +/* 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 "nsUnicharUtils.h" + +#include "LocalAccessible-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) + : LocalAccessible(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 = nsAccUtils::FindARIAAttrValueIn( + aContent->AsElement(), 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::aria_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_BLOCK_RETURN; + + 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_BLOCK_RETURN(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_BLOCK_RETURN; + + 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_BLOCK_RETURN(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; + } + + LocalAccessible::Shutdown(); +} + +nsresult AccessibleWrap::HandleAccEvent(AccEvent* aEvent) { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + nsresult rv = LocalAccessible::HandleAccEvent(aEvent); + NS_ENSURE_SUCCESS(rv, rv); + + if (IsDefunct()) { + // The accessible can become defunct after their events are handled. + return NS_OK; + } + + uint32_t eventType = aEvent->GetEventType(); + + if (eventType == nsIAccessibleEvent::EVENT_SHOW) { + DocAccessibleWrap* doc = static_cast<DocAccessibleWrap*>(Document()); + doc->ProcessNewLiveRegions(); + } + + if (IPCAccessibilityActive()) { + return NS_OK; + } + + LocalAccessible* 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: { + LocalAccessible* 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->LocalParent(); + } + 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 + moveGranularity:event->GetGranularity()]; + 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_ALERT: + 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: + case nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED: + case nsIAccessibleEvent::EVENT_TABLE_STYLING_CHANGED: + [nativeAcc handleAccessibleEvent:eventType]; + break; + + default: + break; + } + + return NS_OK; + + NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE); +} + +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 CaseInsensitiveFindInReadable(aSearchText, name); +} + +//////////////////////////////////////////////////////////////////////////////// +// AccessibleWrap protected + +Class a11y::GetTypeFromRole(roles::Role aRole) { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + 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: + case roles::SWITCH: + case roles::CHECK_MENU_ITEM: + return [mozCheckboxAccessible class]; + + case roles::RADIOBUTTON: + case roles::RADIO_MENU_ITEM: + 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::OUTLINE: + case roles::TREE_TABLE: + return [mozOutlineAccessible class]; + + case roles::OUTLINEITEM: + return [mozOutlineRowAccessible class]; + + default: + return [mozAccessible class]; + } + + return nil; + + NS_OBJC_END_TRY_BLOCK_RETURN(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..4526fb2b80 --- /dev/null +++ b/accessible/mac/DocAccessibleWrap.h @@ -0,0 +1,46 @@ +/* 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" +#include "nsTHashSet.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(LocalAccessible* aAccessible); + + void ProcessNewLiveRegions(); + + protected: + virtual void DoInitialUpdate() override; + + private: + nsTHashSet<void*> mNewLiveRegions; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/mac/DocAccessibleWrap.mm b/accessible/mac/DocAccessibleWrap.mm new file mode 100644 index 0000000000..d83c8d5056 --- /dev/null +++ b/accessible/mac/DocAccessibleWrap.mm @@ -0,0 +1,103 @@ +/* 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) { + LocalAccessible* 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 = nsAccUtils::FindARIAAttrValueIn( + aElement, 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::aria_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(LocalAccessible* aAccessible) { + if (!aAccessible) { + return; + } + + mNewLiveRegions.Insert(aAccessible->UniqueID()); +} + +void DocAccessibleWrap::ProcessNewLiveRegions() { + for (const auto& uniqueID : mNewLiveRegions) { + if (LocalAccessible* liveRegion = + GetAccessibleByUniqueID(const_cast<void*>(uniqueID))) { + 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..6c16b7f6d6 --- /dev/null +++ b/accessible/mac/GeckoTextMarker.h @@ -0,0 +1,127 @@ +/* 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_ + +#include <ApplicationServices/ApplicationServices.h> +#include <Foundation/Foundation.h> + +#include "HyperTextAccessibleWrap.h" +#include "PlatformExtTypes.h" +#include "SDKDeclarations.h" + +namespace mozilla { +namespace a11y { + +class Accessible; +class GeckoTextMarkerRange; + +class GeckoTextMarker final { + public: + GeckoTextMarker(Accessible* aContainer, int32_t aOffset) + : mContainer(aContainer), mOffset(aOffset) {} + + GeckoTextMarker(const GeckoTextMarker& aPoint) + : mContainer(aPoint.mContainer), mOffset(aPoint.mOffset) {} + + GeckoTextMarker(Accessible* aDoc, AXTextMarkerRef aTextMarker); + + GeckoTextMarker() : mContainer(nullptr), mOffset(0) {} + + static GeckoTextMarker MarkerFromIndex(Accessible* aRoot, int32_t aIndex); + + AXTextMarkerRef CreateAXTextMarker(); + + bool Next(); + + bool Previous(); + + // Return a range with the given type relative to this marker. + GeckoTextMarkerRange Range(EWhichRange aRangeType); + + Accessible* Leaf(); + + bool IsValid() const { return !!mContainer; }; + + bool operator<(const GeckoTextMarker& aPoint) const; + + bool operator==(const GeckoTextMarker& aPoint) const { + return mContainer == aPoint.mContainer && mOffset == aPoint.mOffset; + } + + Accessible* mContainer; + int32_t mOffset; + + HyperTextAccessibleWrap* ContainerAsHyperTextWrap() const { + return (mContainer && mContainer->IsLocal()) + ? static_cast<HyperTextAccessibleWrap*>( + mContainer->AsLocal()->AsHyperText()) + : nullptr; + } + + private: + bool IsEditableRoot(); +}; + +class GeckoTextMarkerRange final { + public: + GeckoTextMarkerRange(const GeckoTextMarker& aStart, + const GeckoTextMarker& aEnd) + : mStart(aStart), mEnd(aEnd) {} + + GeckoTextMarkerRange() {} + + GeckoTextMarkerRange(Accessible* aDoc, AXTextMarkerRangeRef aTextMarkerRange); + + explicit GeckoTextMarkerRange(Accessible* aAccessible); + + AXTextMarkerRangeRef CreateAXTextMarkerRange(); + + bool IsValid() const { return !!mStart.mContainer && !!mEnd.mContainer; }; + + /** + * Return text enclosed by the range. + */ + NSString* Text() const; + + /** + * Return the attributed text enclosed by the range. + */ + NSAttributedString* AttributedText() 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(Accessible* 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..d7233964f0 --- /dev/null +++ b/accessible/mac/GeckoTextMarker.mm @@ -0,0 +1,590 @@ +/* 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 "GeckoTextMarker.h" + +#include "DocAccessible.h" +#include "DocAccessibleParent.h" +#include "AccAttributes.h" +#include "nsCocoaUtils.h" +#include "MOXAccessibleBase.h" +#include "mozAccessible.h" + +#include "mozilla/a11y/DocAccessiblePlatformExtParent.h" + +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(Accessible* aDoc, uintptr_t aDocPtr) { + if (reinterpret_cast<uintptr_t>(aDoc) == aDocPtr) { + return true; + } + + if (aDoc->IsLocal()) { + DocAccessible* docAcc = aDoc->AsLocal()->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->AsRemote()->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(Accessible* aDoc, + AXTextMarkerRef aTextMarker) { + MOZ_ASSERT(aDoc); + OpaqueGeckoTextMarker opaqueMarker; + if (aTextMarker && + AXTextMarkerGetLength(aTextMarker) == sizeof(OpaqueGeckoTextMarker)) { + memcpy(&opaqueMarker, AXTextMarkerGetBytePtr(aTextMarker), + sizeof(OpaqueGeckoTextMarker)); + if (DocumentExists(aDoc, opaqueMarker.mDoc)) { + Accessible* doc = reinterpret_cast<Accessible*>(opaqueMarker.mDoc); + if (doc->IsRemote()) { + mContainer = doc->AsRemote()->AsDoc()->GetAccessible(opaqueMarker.mID); + } else { + mContainer = doc->AsLocal()->AsDoc()->GetAccessibleByUniqueID( + reinterpret_cast<void*>(opaqueMarker.mID)); + } + } + + mOffset = opaqueMarker.mOffset; + } else { + mContainer = nullptr; + mOffset = 0; + } +} + +GeckoTextMarker GeckoTextMarker::MarkerFromIndex(Accessible* aRoot, + int32_t aIndex) { + if (aRoot->IsRemote()) { + int32_t offset = 0; + uint64_t containerID = 0; + DocAccessibleParent* ipcDoc = aRoot->AsRemote()->Document(); + Unused << ipcDoc->GetPlatformExtension()->SendOffsetAtIndex( + aRoot->AsRemote()->ID(), aIndex, &containerID, &offset); + RemoteAccessible* container = ipcDoc->GetAccessible(containerID); + return GeckoTextMarker(container, offset); + } else if (auto htWrap = static_cast<HyperTextAccessibleWrap*>( + aRoot->AsLocal()->AsHyperText())) { + int32_t offset = 0; + HyperTextAccessible* container = nullptr; + htWrap->OffsetAtIndex(aIndex, &container, &offset); + return GeckoTextMarker(container, offset); + } + + return GeckoTextMarker(); +} + +AXTextMarkerRef GeckoTextMarker::CreateAXTextMarker() { + if (!IsValid()) { + return nil; + } + + Accessible* doc; + if (mContainer->IsRemote()) { + doc = mContainer->AsRemote()->Document(); + } else { + doc = mContainer->AsLocal()->Document(); + } + + uintptr_t identifier = + mContainer->IsRemote() + ? mContainer->AsRemote()->ID() + : reinterpret_cast<uintptr_t>(mContainer->AsLocal()->UniqueID()); + + OpaqueGeckoTextMarker opaqueMarker(reinterpret_cast<uintptr_t>(doc), + identifier, mOffset); + AXTextMarkerRef cf_text_marker = AXTextMarkerCreate( + kCFAllocatorDefault, reinterpret_cast<const UInt8*>(&opaqueMarker), + sizeof(OpaqueGeckoTextMarker)); + + return (__bridge AXTextMarkerRef)[(__bridge 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<Accessible*, 30> parents1, parents2; + Accessible* p1 = mContainer; + while (p1) { + parents1.AppendElement(p1); + p1 = p1->Parent(); + } + + Accessible* p2 = aPoint.mContainer; + while (p2) { + 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) { + Accessible* child1 = parents1.ElementAt(--pos1); + Accessible* 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. + Accessible* child = parents1.ElementAt(pos1 - 1); + MOZ_ASSERT(child->Parent() == aPoint.mContainer); + uint32_t endOffset = child->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. + Accessible* child = parents2.ElementAt(pos2 - 1); + MOZ_ASSERT(child->Parent() == mContainer); + uint32_t startOffset = child->StartOffset(); + return static_cast<uint32_t>(mOffset) <= startOffset; + } + + MOZ_ASSERT_UNREACHABLE("Broken tree?!"); + return false; +} + +bool GeckoTextMarker::IsEditableRoot() { + uint64_t state = mContainer->IsRemote() ? mContainer->AsRemote()->State() + : mContainer->AsLocal()->State(); + if ((state & states::EDITABLE) == 0) { + return false; + } + + Accessible* parent = mContainer->Parent(); + if (!parent) { + // Not sure when this can happen, but it would technically be an editable + // root. + return true; + } + + state = parent->IsRemote() ? parent->AsRemote()->State() + : parent->AsLocal()->State(); + + return (state & states::EDITABLE) == 0; +} + +bool GeckoTextMarker::Next() { + if (mContainer->IsRemote()) { + int32_t nextOffset = 0; + uint64_t nextContainerID = 0; + DocAccessibleParent* ipcDoc = mContainer->AsRemote()->Document(); + Unused << ipcDoc->GetPlatformExtension()->SendNextClusterAt( + mContainer->AsRemote()->ID(), mOffset, &nextContainerID, &nextOffset); + RemoteAccessible* nextContainer = ipcDoc->GetAccessible(nextContainerID); + bool moved = + nextContainer != mContainer->AsRemote() || 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->IsRemote()) { + int32_t prevOffset = 0; + uint64_t prevContainerID = 0; + DocAccessibleParent* ipcDoc = mContainer->AsRemote()->Document(); + Unused << ipcDoc->GetPlatformExtension()->SendPreviousClusterAt( + mContainer->AsRemote()->ID(), mOffset, &prevContainerID, &prevOffset); + RemoteAccessible* prevContainer = ipcDoc->GetAccessible(prevContainerID); + bool moved = + prevContainer != mContainer->AsRemote() || 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(Accessible* aContainer) { + if (aContainer->IsRemote()) { + return aContainer->AsRemote()->CharacterCount(); + } + + if (aContainer->AsLocal()->IsHyperText()) { + return aContainer->AsLocal()->AsHyperText()->CharacterCount(); + } + + return 0; +} + +GeckoTextMarkerRange GeckoTextMarker::Range(EWhichRange aRangeType) { + MOZ_ASSERT(mContainer); + if (mContainer->IsRemote()) { + int32_t startOffset = 0, endOffset = 0; + uint64_t startContainerID = 0, endContainerID = 0; + DocAccessibleParent* ipcDoc = mContainer->AsRemote()->Document(); + bool success = ipcDoc->GetPlatformExtension()->SendRangeAt( + mContainer->AsRemote()->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()); +} + +Accessible* GeckoTextMarker::Leaf() { + MOZ_ASSERT(mContainer); + if (mContainer->IsRemote()) { + uint64_t leafID = 0; + DocAccessibleParent* ipcDoc = mContainer->AsRemote()->Document(); + Unused << ipcDoc->GetPlatformExtension()->SendLeafAtOffset( + mContainer->AsRemote()->ID(), mOffset, &leafID); + return ipcDoc->GetAccessible(leafID); + } else if (auto htWrap = ContainerAsHyperTextWrap()) { + return htWrap->LeafAtOffset(mOffset); + } + + return mContainer; +} + +// GeckoTextMarkerRange + +GeckoTextMarkerRange::GeckoTextMarkerRange( + Accessible* 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(Accessible* aAccessible) { + if (aAccessible->IsHyperText()) { + // 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->IsRemote()) { + DocAccessibleParent* ipcDoc = mStart.mContainer->AsRemote()->Document(); + Unused << ipcDoc->GetPlatformExtension()->SendRangeOfChild( + mStart.mContainer->AsRemote()->ID(), aAccessible->AsRemote()->ID(), + &mStart.mOffset, &mEnd.mOffset); + } else if (auto htWrap = mStart.ContainerAsHyperTextWrap()) { + htWrap->RangeOfChild(aAccessible->AsLocal(), &mStart.mOffset, + &mEnd.mOffset); + } + } +} + +AXTextMarkerRangeRef GeckoTextMarkerRange::CreateAXTextMarkerRange() { + if (!IsValid()) { + return nil; + } + + AXTextMarkerRangeRef cf_text_marker_range = + AXTextMarkerRangeCreate(kCFAllocatorDefault, mStart.CreateAXTextMarker(), + mEnd.CreateAXTextMarker()); + + return (__bridge AXTextMarkerRangeRef)[(__bridge id)( + cf_text_marker_range)autorelease]; +} + +NSString* GeckoTextMarkerRange::Text() const { + nsAutoString text; + if (mStart.mContainer->IsRemote() && mEnd.mContainer->IsRemote()) { + DocAccessibleParent* ipcDoc = mStart.mContainer->AsRemote()->Document(); + Unused << ipcDoc->GetPlatformExtension()->SendTextForRange( + mStart.mContainer->AsRemote()->ID(), mStart.mOffset, + mEnd.mContainer->AsRemote()->ID(), mEnd.mOffset, &text); + } else if (auto htWrap = mStart.ContainerAsHyperTextWrap()) { + htWrap->TextForRange(text, mStart.mOffset, mEnd.ContainerAsHyperTextWrap(), + mEnd.mOffset); + } + return nsCocoaUtils::ToNSString(text); +} + +static NSColor* ColorFromColor(const Color& aColor) { + return [NSColor colorWithCalibratedRed:NS_GET_R(aColor.mValue) / 255.0 + green:NS_GET_G(aColor.mValue) / 255.0 + blue:NS_GET_B(aColor.mValue) / 255.0 + alpha:1.0]; +} + +static NSDictionary* StringAttributesFromAttributes(AccAttributes* aAttributes, + Accessible* aContainer) { + NSMutableDictionary* attrDict = + [NSMutableDictionary dictionaryWithCapacity:aAttributes->Count()]; + NSMutableDictionary* fontAttrDict = [[NSMutableDictionary alloc] init]; + [attrDict setObject:fontAttrDict forKey:@"AXFont"]; + for (auto iter : *aAttributes) { + if (iter.Name() == nsGkAtoms::backgroundColor) { + if (Maybe<Color> value = iter.Value<Color>()) { + NSColor* color = ColorFromColor(*value); + [attrDict setObject:(__bridge id)color.CGColor + forKey:@"AXBackgroundColor"]; + } + } else if (iter.Name() == nsGkAtoms::color) { + if (Maybe<Color> value = iter.Value<Color>()) { + NSColor* color = ColorFromColor(*value); + [attrDict setObject:(__bridge id)color.CGColor + forKey:@"AXForegroundColor"]; + } + } else if (iter.Name() == nsGkAtoms::font_size) { + if (Maybe<FontSize> pointSize = iter.Value<FontSize>()) { + int32_t fontPixelSize = static_cast<int32_t>(pointSize->mValue * 4 / 3); + [fontAttrDict setObject:@(fontPixelSize) forKey:@"AXFontSize"]; + } + } else if (iter.Name() == nsGkAtoms::font_family) { + nsAutoString fontFamily; + iter.ValueAsString(fontFamily); + [fontAttrDict setObject:nsCocoaUtils::ToNSString(fontFamily) + forKey:@"AXFontFamily"]; + } else if (iter.Name() == nsGkAtoms::textUnderlineColor) { + [attrDict setObject:@1 forKey:@"AXUnderline"]; + if (Maybe<Color> value = iter.Value<Color>()) { + NSColor* color = ColorFromColor(*value); + [attrDict setObject:(__bridge id)color.CGColor + forKey:@"AXUnderlineColor"]; + } + } else if (iter.Name() == nsGkAtoms::invalid) { + // XXX: There is currently no attribute for grammar + if (auto value = iter.Value<RefPtr<nsAtom>>()) { + if (*value == nsGkAtoms::spelling) { + [attrDict setObject:@YES + forKey:NSAccessibilityMarkedMisspelledTextAttribute]; + } + } + } else { + nsAutoString valueStr; + iter.ValueAsString(valueStr); + nsAutoString keyStr; + iter.NameAsString(keyStr); + [attrDict setObject:nsCocoaUtils::ToNSString(valueStr) + forKey:nsCocoaUtils::ToNSString(keyStr)]; + } + } + + mozAccessible* container = GetNativeFromGeckoAccessible(aContainer); + id<MOXAccessible> link = + [container moxFindAncestor:^BOOL(id<MOXAccessible> moxAcc, BOOL* stop) { + return [[moxAcc moxRole] isEqualToString:NSAccessibilityLinkRole]; + }]; + if (link) { + [attrDict setObject:link forKey:@"AXLink"]; + } + + id<MOXAccessible> heading = + [container moxFindAncestor:^BOOL(id<MOXAccessible> moxAcc, BOOL* stop) { + return [[moxAcc moxRole] isEqualToString:@"AXHeading"]; + }]; + if (heading) { + [attrDict setObject:[heading moxValue] forKey:@"AXHeadingLevel"]; + } + + return attrDict; +} + +NSAttributedString* GeckoTextMarkerRange::AttributedText() const { + NSMutableAttributedString* str = + [[[NSMutableAttributedString alloc] init] autorelease]; + + if (mStart.mContainer->IsRemote() && mEnd.mContainer->IsRemote()) { + nsTArray<TextAttributesRun> textAttributesRuns; + DocAccessibleParent* ipcDoc = mStart.mContainer->AsRemote()->Document(); + Unused << ipcDoc->GetPlatformExtension()->SendAttributedTextForRange( + mStart.mContainer->AsRemote()->ID(), mStart.mOffset, + mEnd.mContainer->AsRemote()->ID(), mEnd.mOffset, &textAttributesRuns); + + for (size_t i = 0; i < textAttributesRuns.Length(); i++) { + AccAttributes* attributes = + textAttributesRuns.ElementAt(i).TextAttributes(); + RemoteAccessible* container = + ipcDoc->GetAccessible(textAttributesRuns.ElementAt(i).ContainerID()); + + NSAttributedString* substr = [[[NSAttributedString alloc] + initWithString:nsCocoaUtils::ToNSString( + textAttributesRuns.ElementAt(i).Text()) + attributes:StringAttributesFromAttributes(attributes, container)] + autorelease]; + + [str appendAttributedString:substr]; + } + } else if (auto htWrap = mStart.ContainerAsHyperTextWrap()) { + nsTArray<nsString> texts; + nsTArray<LocalAccessible*> containers; + nsTArray<RefPtr<AccAttributes>> props; + + htWrap->AttributedTextForRange(texts, props, containers, mStart.mOffset, + mEnd.ContainerAsHyperTextWrap(), + mEnd.mOffset); + + MOZ_ASSERT(texts.Length() == props.Length() && + texts.Length() == containers.Length()); + + for (size_t i = 0; i < texts.Length(); i++) { + NSAttributedString* substr = [[[NSAttributedString alloc] + initWithString:nsCocoaUtils::ToNSString(texts.ElementAt(i)) + attributes:StringAttributesFromAttributes( + props.ElementAt(i), containers.ElementAt(i))] + autorelease]; + [str appendAttributedString:substr]; + } + } + + return str; +} + +int32_t GeckoTextMarkerRange::Length() const { + int32_t length = 0; + if (mStart.mContainer->IsRemote() && mEnd.mContainer->IsRemote()) { + DocAccessibleParent* ipcDoc = mStart.mContainer->AsRemote()->Document(); + Unused << ipcDoc->GetPlatformExtension()->SendLengthForRange( + mStart.mContainer->AsRemote()->ID(), mStart.mOffset, + mEnd.mContainer->AsRemote()->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 { + LayoutDeviceIntRect rect; + if (mStart.mContainer->IsRemote() && mEnd.mContainer->IsRemote()) { + DocAccessibleParent* ipcDoc = mStart.mContainer->AsRemote()->Document(); + Unused << ipcDoc->GetPlatformExtension()->SendBoundsForRange( + mStart.mContainer->AsRemote()->ID(), mStart.mOffset, + mEnd.mContainer->AsRemote()->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->IsRemote() && mEnd.mContainer->IsRemote()) { + DocAccessibleParent* ipcDoc = mStart.mContainer->AsRemote()->Document(); + Unused << ipcDoc->GetPlatformExtension()->SendSelectRange( + mStart.mContainer->AsRemote()->ID(), mStart.mOffset, + mEnd.mContainer->AsRemote()->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(Accessible* 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/HyperTextAccessibleWrap.h b/accessible/mac/HyperTextAccessibleWrap.h new file mode 100644 index 0000000000..bb418ed179 --- /dev/null +++ b/accessible/mac/HyperTextAccessibleWrap.h @@ -0,0 +1,96 @@ +/* -*- 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); + + void AttributedTextForRange(nsTArray<nsString>& aStrings, + nsTArray<RefPtr<AccAttributes>>& aProperties, + nsTArray<LocalAccessible*>& aContainers, + int32_t aStartOffset, + HyperTextAccessible* aEndContainer, + int32_t aEndOffset); + + LayoutDeviceIntRect 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(LocalAccessible* aChild, int32_t* aStartOffset, + int32_t* aEndOffset); + + LocalAccessible* 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..361eb9ca22 --- /dev/null +++ b/accessible/mac/HyperTextAccessibleWrap.mm @@ -0,0 +1,771 @@ +/* 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 "LocalAccessible-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(0), + mCurrentEndOffset(0), + mEndContainer(aEndContainer), + mEndOffset(0) { + mCurrentStartOffset = + std::min(aStartOffset, + static_cast<int32_t>(mCurrentContainer->CharacterCount())); + mCurrentEndOffset = mCurrentStartOffset; + mEndOffset = std::min( + aEndOffset, static_cast<int32_t>(mEndContainer->CharacterCount())); + } + + 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->LocalParent() || + !mCurrentContainer->LocalParent()->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->LocalParent()->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 { + LocalAccessible* 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()) { + LocalAccessible* 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->LocalParent() || + !mCurrentContainer->LocalParent()->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->LocalParent()->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 { + LocalAccessible* 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++) { + LocalAccessible* 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()) { + LocalAccessible* maybeBullet = GetChildAtOffset(aStartOffset - 1); + if (maybeBullet) { + LocalAccessible* 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); + } +} + +void HyperTextAccessibleWrap::AttributedTextForRange( + nsTArray<nsString>& aStrings, nsTArray<RefPtr<AccAttributes>>& aProperties, + nsTArray<LocalAccessible*>& aContainers, int32_t aStartOffset, + HyperTextAccessible* aEndContainer, int32_t aEndOffset) { + if (IsHTMLListItem()) { + LocalAccessible* maybeBullet = GetChildAtOffset(aStartOffset - 1); + if (maybeBullet) { + LocalAccessible* bullet = AsHTMLListItem()->Bullet(); + if (maybeBullet == bullet) { + nsAutoString text; + TextSubstring(0, nsAccUtils::TextLength(bullet), text); + + int32_t unusedAttrStartOffset, unusedAttrEndOffset; + RefPtr<AccAttributes> props = + TextAttributes(true, aStartOffset - 1, &unusedAttrStartOffset, + &unusedAttrEndOffset); + + aStrings.AppendElement(text); + aProperties.AppendElement(props); + aContainers.AppendElement(this); + } + } + } + + HyperTextIterator iter(this, aStartOffset, aEndContainer, aEndOffset); + while (iter.Next()) { + int32_t attrStartOffset = 0; + int32_t attrEndOffset = iter.mCurrentStartOffset; + do { + int32_t oldEndOffset = attrEndOffset; + RefPtr<AccAttributes> props = iter.mCurrentContainer->TextAttributes( + true, attrEndOffset, &attrStartOffset, &attrEndOffset); + + if (oldEndOffset == attrEndOffset) { + MOZ_ASSERT_UNREACHABLE("new attribute end offset should be different"); + break; + } + + nsAutoString text; + iter.mCurrentContainer->TextSubstring( + attrStartOffset < iter.mCurrentStartOffset ? iter.mCurrentStartOffset + : attrStartOffset, + attrEndOffset < iter.mCurrentEndOffset ? attrEndOffset + : iter.mCurrentEndOffset, + text); + + aStrings.AppendElement(text); + aProperties.AppendElement(props); + aContainers.AppendElement(iter.mCurrentContainer); + } while (attrEndOffset < iter.mCurrentEndOffset); + } +} + +LayoutDeviceIntRect HyperTextAccessibleWrap::BoundsForRange( + int32_t aStartOffset, HyperTextAccessible* aEndContainer, + int32_t aEndOffset) { + LayoutDeviceIntRect rect; + HyperTextIterator iter(this, aStartOffset, aEndContainer, aEndOffset); + while (iter.Next()) { + LayoutDeviceIntRect 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; + } + + auto* startContainer = static_cast<HyperTextAccessibleWrap*>( + start.mContainer->AsLocal()->AsHyperText()); + if ((NativeState() & states::EDITABLE) && + !(startContainer->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 = startContainer->FindTextPoint(start.mOffset, eDirNext, + eSelectWord, eEndWord); + if (end < here) { + *aStartContainer = end.mContainer->AsLocal()->AsHyperText(); + *aEndContainer = here.mContainer->AsLocal()->AsHyperText(); + *aStartOffset = end.mOffset; + *aEndOffset = here.mOffset; + } else { + *aStartContainer = startContainer; + *aEndContainer = end.mContainer->AsLocal()->AsHyperText(); + *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; + } + + auto* endContainer = static_cast<HyperTextAccessibleWrap*>( + end.mContainer->AsLocal()->AsHyperText()); + if ((NativeState() & states::EDITABLE) && + !(endContainer->NativeState() & states::EDITABLE)) { + // The word search crossed an editable boundary. Return with no result. + return; + } + + TextPoint start = endContainer->FindTextPoint(end.mOffset, eDirPrevious, + eSelectWord, eStartWord); + + if (here < start) { + *aStartContainer = here.mContainer->AsLocal()->AsHyperText(); + *aEndContainer = start.mContainer->AsLocal()->AsHyperText(); + *aStartOffset = here.mOffset; + *aEndOffset = start.mOffset; + } else { + *aStartContainer = start.mContainer->AsLocal()->AsHyperText(); + *aEndContainer = endContainer; + *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; + } + + auto* endContainer = static_cast<HyperTextAccessibleWrap*>( + end.mContainer->AsLocal()->AsHyperText()); + TextPoint start = endContainer->FindTextPoint( + end.mOffset, eDirPrevious, eSelectBeginLine, eDefaultBehavior); + + if (!aNextLine && here < start) { + start = FindTextPoint(aOffset, eDirPrevious, eSelectBeginLine, + eDefaultBehavior); + if (!start.mContainer) { + return; + } + + auto* startContainer = static_cast<HyperTextAccessibleWrap*>( + start.mContainer->AsLocal()->AsHyperText()); + end = startContainer->FindTextPoint(start.mOffset, eDirNext, eSelectEndLine, + eDefaultBehavior); + } + + *aStartContainer = start.mContainer->AsLocal()->AsHyperText(); + *aEndContainer = end.mContainer->AsLocal()->AsHyperText(); + *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 && LocalParent() && LocalParent()->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*>(LocalParent()->AsHyperText()) + ->ParagraphAt(StartOffset(), aStartContainer, aStartOffset, + aEndContainer, aEndOffset); + return; + } + + auto* endContainer = static_cast<HyperTextAccessibleWrap*>( + end.mContainer->AsLocal()->AsHyperText()); + TextPoint start = static_cast<HyperTextAccessibleWrap*>(endContainer) + ->FindTextPoint(end.mOffset, eDirPrevious, + eSelectParagraph, eDefaultBehavior); + + *aStartContainer = start.mContainer->AsLocal()->AsHyperText(); + *aEndContainer = endContainer; + *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->LocalParent()->IsHyperText()); + HyperTextAccessibleWrap* container = + static_cast<HyperTextAccessibleWrap*>(leaf->LocalParent()->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->AsLocal()->AsHyperText(); + *aNextOffset = next.mOffset; + } +} + +void HyperTextAccessibleWrap::PreviousClusterAt( + int32_t aOffset, HyperTextAccessible** aPrevContainer, + int32_t* aPrevOffset) { + TextPoint prev = + FindTextPoint(aOffset, eDirPrevious, eSelectCluster, eDefaultBehavior); + *aPrevContainer = prev.mContainer->AsLocal()->AsHyperText(); + *aPrevOffset = prev.mOffset; +} + +void HyperTextAccessibleWrap::RangeOfChild(LocalAccessible* aChild, + int32_t* aStartOffset, + int32_t* aEndOffset) { + MOZ_ASSERT(aChild->LocalParent() == 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); + } +} + +LocalAccessible* HyperTextAccessibleWrap::LeafAtOffset(int32_t aOffset) { + HyperTextAccessible* text = this; + LocalAccessible* 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->LocalChildAt(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; + LocalAccessible* 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->LocalChildAt(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->LocalChildAt(--childIdx); + } else if (aDirection == eDirNext && + childIdx + 1 < static_cast<int32_t>(text->ChildCount())) { + child = text->LocalChildAt(++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->LocalChildAt(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() { + LocalAccessible* editable = nullptr; + for (LocalAccessible* acc = this; acc && acc != Document(); + acc = acc->LocalParent()) { + if (acc->NativeState() & states::EDITABLE) { + editable = acc; + } else { + break; + } + } + + return static_cast<HyperTextAccessibleWrap*>(editable->AsHyperText()); +} diff --git a/accessible/mac/MOXAccessibleBase.h b/accessible/mac/MOXAccessibleBase.h new file mode 100644 index 0000000000..751fa5f28d --- /dev/null +++ b/accessible/mac/MOXAccessibleBase.h @@ -0,0 +1,143 @@ +/* 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; + +// override +- (BOOL)moxIsLiveRegion; + +// override +- (id<MOXAccessible>)moxFindAncestor:(BOOL (^)(id<MOXAccessible> moxAcc, + BOOL* stop))findBlock; + +#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..098a248a03 --- /dev/null +++ b/accessible/mac/MOXAccessibleBase.mm @@ -0,0 +1,573 @@ +/* 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_BLOCK_RETURN; + + if ([self isExpired]) { + return nil; + } + + static NSMutableDictionary* attributesForEachClass = nil; + + if (!attributesForEachClass) { + attributesForEachClass = [[NSMutableDictionary alloc] init]; + } + + NSMutableArray* attributes = + attributesForEachClass [[self class]] + ?: [[[NSMutableArray alloc] init] autorelease]; + + 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_BLOCK_RETURN(nil); +} + +- (id)accessibilityAttributeValue:(NSString*)attribute { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + 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] autorelease]; + 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_BLOCK_RETURN(nil); +} + +- (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute { + NS_OBJC_BEGIN_TRY_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_BLOCK_RETURN(NO); +} + +- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute { + NS_OBJC_BEGIN_TRY_IGNORE_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_IGNORE_BLOCK; +} + +- (NSArray*)accessibilityActionNames { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + if ([self isExpired]) { + return nil; + } + + NSMutableArray* actionNames = [[[NSMutableArray alloc] init] autorelease]; + + 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_BLOCK_RETURN(nil); +} + +- (void)accessibilityPerformAction:(NSString*)action { + NS_OBJC_BEGIN_TRY_IGNORE_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_IGNORE_BLOCK; +} + +- (NSString*)accessibilityActionDescription:(NSString*)action { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + // by default we return whatever the MacOS API know about. + // if you have custom actions, override. + return NSAccessibilityActionDescription(action); + NS_OBJC_END_TRY_BLOCK_RETURN(nil); +} + +- (NSArray*)accessibilityParameterizedAttributeNames { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + if ([self isExpired]) { + return nil; + } + + NSMutableArray* attributeNames = [[[NSMutableArray alloc] init] autorelease]; + + 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_BLOCK_RETURN(nil); +} + +- (id)accessibilityAttributeValue:(NSString*)attribute + forParameter:(id)parameter { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + 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_BLOCK_RETURN(nil); +} + +- (id)accessibilityHitTest:(NSPoint)point { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + return GetObjectOrRepresentedView([self moxHitTest:point]); + NS_OBJC_END_TRY_BLOCK_RETURN(nil); +} + +- (id)accessibilityFocusedUIElement { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + return GetObjectOrRepresentedView([self moxFocusedUIElement]); + NS_OBJC_END_TRY_BLOCK_RETURN(nil); +} + +- (BOOL)isAccessibilityElement { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + if ([self isExpired]) { + return YES; + } + + id parent = [self moxParent]; + if (![parent isMOXAccessible]) { + return YES; + } + + return ![self moxIgnoreWithParent:parent]; + + NS_OBJC_END_TRY_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 self; +} + +- (id)moxFocusedUIElement { + return 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] autorelease]; + 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]; +} + +- (id<MOXAccessible>)moxFindAncestor:(BOOL (^)(id<MOXAccessible> moxAcc, + BOOL* stop))findBlock { + for (id element = self; [element conformsToProtocol:@protocol(MOXAccessible)]; + element = [element moxUnignoredParent]) { + BOOL stop = NO; + if (findBlock(element, &stop)) { + return element; + } + + if (stop) { + break; + } + } + + return nil; +} + +#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..117bce79e7 --- /dev/null +++ b/accessible/mac/MOXAccessibleProtocol.h @@ -0,0 +1,534 @@ +/* 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 "SDKDeclarations.h" + +@protocol MOXTextMarkerSupport; +@protocol mozAccessible; + +// 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; + +// Find the nearest ancestor that returns true with the given block function +- (id<MOXAccessible> _Nullable)moxFindAncestor: + (BOOL (^_Nonnull)(id<MOXAccessible> _Nonnull moxAcc, + BOOL* _Nonnull stop))findBlock; + +@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; + +// AXValueDescription +- (NSString* _Nullable)moxValueDescription; + +// AXSize +- (NSValue* _Nullable)moxSize; + +// AXPosition +- (NSValue* _Nullable)moxPosition; + +// AXEnabled +- (NSNumber* _Nullable)moxEnabled; + +// AXFocused +- (NSNumber* _Nullable)moxFocused; + +// AXWindow +- (id _Nullable)moxWindow; + +// AXFrame +- (NSValue* _Nullable)moxFrame; + +// 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; + +// AXMinValue +- (id _Nullable)moxMinValue; + +// AXMaxValue +- (id _Nullable)moxMaxValue; + +// 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; + +// AXAttributedStringForRange +- (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 +- (AXTextMarkerRef _Nullable)moxStartTextMarker; + +// AXEndTextMarker +- (AXTextMarkerRef _Nullable)moxEndTextMarker; + +// AXSelectedTextMarkerRange +- (AXTextMarkerRangeRef _Nullable)moxSelectedTextMarkerRange; + +#pragma mark - ParameterizedTextAttributeGetters + +// AXLengthForTextMarkerRange +- (NSNumber* _Nullable)moxLengthForTextMarkerRange: + (AXTextMarkerRangeRef _Nonnull)textMarkerRange; + +// AXStringForTextMarkerRange +- (NSString* _Nullable)moxStringForTextMarkerRange: + (AXTextMarkerRangeRef _Nonnull)textMarkerRange; + +// AXTextMarkerRangeForUnorderedTextMarkers +- (AXTextMarkerRangeRef _Nullable)moxTextMarkerRangeForUnorderedTextMarkers: + (NSArray* _Nonnull)textMarkers; + +// AXLeftWordTextMarkerRangeForTextMarker +- (AXTextMarkerRangeRef _Nullable)moxLeftWordTextMarkerRangeForTextMarker: + (AXTextMarkerRef _Nonnull)textMarker; + +// AXRightWordTextMarkerRangeForTextMarker +- (AXTextMarkerRangeRef _Nullable)moxRightWordTextMarkerRangeForTextMarker: + (AXTextMarkerRef _Nonnull)textMarker; + +// AXStartTextMarkerForTextMarkerRange +- (AXTextMarkerRef _Nullable)moxStartTextMarkerForTextMarkerRange: + (AXTextMarkerRangeRef _Nonnull)textMarkerRange; + +// AXEndTextMarkerForTextMarkerRange +- (AXTextMarkerRef _Nullable)moxEndTextMarkerForTextMarkerRange: + (AXTextMarkerRangeRef _Nonnull)textMarkerRange; + +// AXNextTextMarkerForTextMarker +- (AXTextMarkerRef _Nullable)moxNextTextMarkerForTextMarker: + (AXTextMarkerRef _Nonnull)textMarker; + +// AXPreviousTextMarkerForTextMarker +- (AXTextMarkerRef _Nullable)moxPreviousTextMarkerForTextMarker: + (AXTextMarkerRef _Nonnull)textMarker; + +// AXAttributedStringForTextMarkerRange +- (NSAttributedString* _Nullable)moxAttributedStringForTextMarkerRange: + (AXTextMarkerRangeRef _Nonnull)textMarkerRange; + +// AXBoundsForTextMarkerRange +- (NSValue* _Nullable)moxBoundsForTextMarkerRange: + (AXTextMarkerRangeRef _Nonnull)textMarkerRange; + +// AXIndexForTextMarker +- (NSNumber* _Nullable)moxIndexForTextMarker: + (AXTextMarkerRef _Nonnull)textMarker; + +// AXTextMarkerForIndex +- (AXTextMarkerRef _Nullable)moxTextMarkerForIndex:(NSNumber* _Nonnull)index; + +// AXUIElementForTextMarker +- (id _Nullable)moxUIElementForTextMarker:(AXTextMarkerRef _Nonnull)textMarker; + +// AXTextMarkerRangeForUIElement +- (AXTextMarkerRangeRef _Nullable)moxTextMarkerRangeForUIElement: + (id _Nonnull)element; + +// AXLineTextMarkerRangeForTextMarker +- (AXTextMarkerRangeRef _Nullable)moxLineTextMarkerRangeForTextMarker: + (AXTextMarkerRef _Nonnull)textMarker; + +// AXLeftLineTextMarkerRangeForTextMarker +- (AXTextMarkerRangeRef _Nullable)moxLeftLineTextMarkerRangeForTextMarker: + (AXTextMarkerRef _Nonnull)textMarker; + +// AXRightLineTextMarkerRangeForTextMarker +- (AXTextMarkerRangeRef _Nullable)moxRightLineTextMarkerRangeForTextMarker: + (AXTextMarkerRef _Nonnull)textMarker; + +// AXParagraphTextMarkerRangeForTextMarker +- (AXTextMarkerRangeRef _Nullable)moxParagraphTextMarkerRangeForTextMarker: + (AXTextMarkerRef _Nonnull)textMarker; + +// AXStyleTextMarkerRangeForTextMarker +- (AXTextMarkerRangeRef _Nullable)moxStyleTextMarkerRangeForTextMarker: + (AXTextMarkerRef _Nonnull)textMarker; + +// AXMozDebugDescriptionForTextMarker +- (NSString* _Nullable)moxMozDebugDescriptionForTextMarker: + (AXTextMarkerRef _Nonnull)textMarker; + +// AXMozDebugDescriptionForTextMarkerRange +- (NSString* _Nullable)moxMozDebugDescriptionForTextMarkerRange: + (AXTextMarkerRangeRef _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..7bfe2e3e05 --- /dev/null +++ b/accessible/mac/MOXMathAccessibles.mm @@ -0,0 +1,117 @@ +/* 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, nsGkAtoms::linethickness_)) { + 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..17138d93d3 --- /dev/null +++ b/accessible/mac/MOXSearchInfo.mm @@ -0,0 +1,454 @@ +/* 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; + +- (Accessible*)rootGeckoAccessible; + +- (Accessible*)startGeckoAccessible; + +- (BOOL)shouldApplyPostFilter; +@end + +@implementation MOXSearchInfo + +- (id)initWithParameters:(NSDictionary*)params + andRoot:(MOXAccessibleBase*)root { + if (id searchKeyParam = [params objectForKey:@"AXSearchKey"]) { + mSearchKeys = [searchKeyParam isKindOfClass:[NSString class]] + ? [[NSArray alloc] initWithObjects:searchKeyParam, nil] + : [searchKeyParam retain]; + } + + 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]; +} + +- (Accessible*)rootGeckoAccessible { + id root = + [mRoot isKindOfClass:[mozAccessible class]] ? mRoot : [mRoot moxParent]; + + return [static_cast<mozAccessible*>(root) geckoAccessible]; +} + +- (Accessible*)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] autorelease]; + Accessible* geckoRootAcc = [self rootGeckoAccessible]; + Accessible* geckoStartAcc = [self startGeckoAccessible]; + Pivot p = Pivot(geckoRootAcc); + Accessible* 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 && 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. + // In some cases, VO passes a non-null, empty string for AXSearchText. + // In that case, we should act as if no AXSearchText was given. + return !!mSearchText && [mSearchText length] > 0; +} + +- (NSArray<mozAccessible*>*)applyPostFilter:(NSArray<mozAccessible*>*)matches { + if (![self shouldApplyPostFilter]) { + return matches; + } + + NSMutableArray<mozAccessible*>* postMatches = + [[[NSMutableArray alloc] init] autorelease]; + + nsString searchText; + nsCocoaUtils::GetStringForNSString(mSearchText, searchText); + + __block DocAccessibleParent* ipcDoc = nullptr; + __block nsTArray<uint64_t> accIds; + + [matches enumerateObjectsUsingBlock:^(mozAccessible* match, NSUInteger idx, + BOOL* stop) { + Accessible* geckoAcc = [match geckoAccessible]; + if (!geckoAcc) { + 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->IsLocal()) { + AccessibleWrap* acc = static_cast<AccessibleWrap*>(geckoAcc->AsLocal()); + 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; + } + + RemoteAccessible* proxy = geckoAcc->AsRemote(); + 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 (RemoteAccessible* 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 { + Accessible* geckoRootAcc = [self rootGeckoAccessible]; + Accessible* geckoStartAcc = [self startGeckoAccessible]; + NSMutableArray* matches = [[[NSMutableArray alloc] init] autorelease]; + 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..7a0161a6b8 --- /dev/null +++ b/accessible/mac/MOXTextMarkerDelegate.h @@ -0,0 +1,169 @@ +/* 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" + +@interface MOXTextMarkerDelegate : NSObject <MOXTextMarkerSupport> { + mozilla::a11y::Accessible* mGeckoDocAccessible; + AXTextMarkerRangeRef mSelection; + AXTextMarkerRef mCaret; + AXTextMarkerRef mPrevCaret; + int32_t mCaretMoveGranularity; +} + ++ (id)getOrCreateForDoc:(mozilla::a11y::Accessible*)aDoc; + ++ (void)destroyForDoc:(mozilla::a11y::Accessible*)aDoc; + +- (id)initWithDoc:(mozilla::a11y::Accessible*)aDoc; + +- (void)dealloc; + +- (void)setSelectionFrom:(mozilla::a11y::Accessible*)startContainer + at:(int32_t)startOffset + to:(mozilla::a11y::Accessible*)endContainer + at:(int32_t)endOffset; + +- (void)setCaretOffset:(mozilla::a11y::Accessible*)container + at:(int32_t)offset + moveGranularity:(int32_t)granularity; + +- (NSDictionary*)selectionChangeInfo; + +- (void)invalidateSelection; + +- (mozilla::a11y::GeckoTextMarkerRange)selection; + +// override +- (AXTextMarkerRef)moxStartTextMarker; + +// override +- (AXTextMarkerRef)moxEndTextMarker; + +// override +- (AXTextMarkerRangeRef)moxSelectedTextMarkerRange; + +// override +- (NSNumber*)moxLengthForTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange; + +// override +- (NSString*)moxStringForTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange; + +// override +- (AXTextMarkerRangeRef)moxTextMarkerRangeForUnorderedTextMarkers: + (NSArray*)textMarkers; + +// override +- (AXTextMarkerRef)moxStartTextMarkerForTextMarkerRange: + (AXTextMarkerRangeRef)textMarkerRange; + +// override +- (AXTextMarkerRef)moxEndTextMarkerForTextMarkerRange: + (AXTextMarkerRangeRef)textMarkerRange; + +// override +- (AXTextMarkerRangeRef)moxLeftWordTextMarkerRangeForTextMarker: + (AXTextMarkerRef)textMarker; + +// override +- (AXTextMarkerRangeRef)moxRightWordTextMarkerRangeForTextMarker: + (AXTextMarkerRef)textMarker; + +// override +- (AXTextMarkerRangeRef)moxLineTextMarkerRangeForTextMarker: + (AXTextMarkerRef)textMarker; + +// override +- (AXTextMarkerRangeRef)moxLeftLineTextMarkerRangeForTextMarker: + (AXTextMarkerRef)textMarker; + +// override +- (AXTextMarkerRangeRef)moxRightLineTextMarkerRangeForTextMarker: + (AXTextMarkerRef)textMarker; + +// override +- (AXTextMarkerRangeRef)moxParagraphTextMarkerRangeForTextMarker: + (AXTextMarkerRef)textMarker; + +// override +- (AXTextMarkerRangeRef)moxStyleTextMarkerRangeForTextMarker: + (AXTextMarkerRef)textMarker; + +// override +- (AXTextMarkerRef)moxNextTextMarkerForTextMarker:(AXTextMarkerRef)textMarker; + +// override +- (AXTextMarkerRef)moxPreviousTextMarkerForTextMarker: + (AXTextMarkerRef)textMarker; + +// override +- (NSAttributedString*)moxAttributedStringForTextMarkerRange: + (AXTextMarkerRangeRef)textMarkerRange; + +// override +- (NSValue*)moxBoundsForTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange; + +// override +- (id)moxUIElementForTextMarker:(AXTextMarkerRef)textMarker; + +// override +- (AXTextMarkerRangeRef)moxTextMarkerRangeForUIElement:(id)element; + +// override +- (NSString*)moxMozDebugDescriptionForTextMarker:(AXTextMarkerRef)textMarker; + +// override +- (void)moxSetSelectedTextMarkerRange:(AXTextMarkerRangeRef)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..e232dc0b63 --- /dev/null +++ b/accessible/mac/MOXTextMarkerDelegate.mm @@ -0,0 +1,495 @@ +/* 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 "DocAccessible.h" + +#import "MOXTextMarkerDelegate.h" + +#include "mozAccessible.h" +#include "mozilla/Preferences.h" +#include "nsISelectionListener.h" + +using namespace mozilla::a11y; + +#define PREF_ACCESSIBILITY_MAC_DEBUG "accessibility.mac.debug" + +static nsTHashMap<nsPtrHashKey<mozilla::a11y::Accessible>, + MOXTextMarkerDelegate*> + sDelegates; + +@implementation MOXTextMarkerDelegate + ++ (id)getOrCreateForDoc:(mozilla::a11y::Accessible*)aDoc { + MOZ_ASSERT(aDoc); + + MOXTextMarkerDelegate* delegate = sDelegates.Get(aDoc); + if (!delegate) { + delegate = [[MOXTextMarkerDelegate alloc] initWithDoc:aDoc]; + sDelegates.InsertOrUpdate(aDoc, delegate); + [delegate retain]; + } + + return delegate; +} + ++ (void)destroyForDoc:(mozilla::a11y::Accessible*)aDoc { + MOZ_ASSERT(aDoc); + + MOXTextMarkerDelegate* delegate = sDelegates.Get(aDoc); + if (delegate) { + sDelegates.Remove(aDoc); + [delegate release]; + } +} + +- (id)initWithDoc:(Accessible*)aDoc { + MOZ_ASSERT(aDoc, "Cannot init MOXTextDelegate with null"); + if ((self = [super init])) { + mGeckoDocAccessible = aDoc; + } + + mCaretMoveGranularity = nsISelectionListener::NO_AMOUNT; + + return self; +} + +- (void)dealloc { + [self invalidateSelection]; + [super dealloc]; +} + +- (void)setSelectionFrom:(Accessible*)startContainer + at:(int32_t)startOffset + to:(Accessible*)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(); + CFRetain(mSelection); +} + +- (void)setCaretOffset:(mozilla::a11y::Accessible*)container + at:(int32_t)offset + moveGranularity:(int32_t)granularity { + GeckoTextMarker caretMarker(container, offset); + + mPrevCaret = mCaret; + mCaret = caretMarker.CreateAXTextMarker(); + mCaretMoveGranularity = granularity; + + CFRetain(mCaret); +} + +// 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() + ? (__bridge id)mSelection + : [NSNull null], + @"AXTextStateChangeType" : @(stateChangeType), + } mutableCopy] autorelease]; + + 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; + int direction = isForward ? AXTextSelectionDirectionNext + : AXTextSelectionDirectionPrevious; + + int32_t granularity = AXTextSelectionGranularityUnknown; + switch (mCaretMoveGranularity) { + case nsISelectionListener::CHARACTER_AMOUNT: + case nsISelectionListener::CLUSTER_AMOUNT: + granularity = AXTextSelectionGranularityCharacter; + break; + case nsISelectionListener::WORD_AMOUNT: + case nsISelectionListener::WORDNOSPACE_AMOUNT: + granularity = AXTextSelectionGranularityWord; + break; + case nsISelectionListener::LINE_AMOUNT: + granularity = AXTextSelectionGranularityLine; + break; + case nsISelectionListener::BEGINLINE_AMOUNT: + direction = AXTextSelectionDirectionBeginning; + granularity = AXTextSelectionGranularityLine; + break; + case nsISelectionListener::ENDLINE_AMOUNT: + direction = AXTextSelectionDirectionEnd; + granularity = AXTextSelectionGranularityLine; + break; + case nsISelectionListener::PARAGRAPH_AMOUNT: + granularity = AXTextSelectionGranularityParagraph; + break; + default: + break; + } + + // 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" : @(direction), + @"AXTextSelectionGranularity" : @(granularity) + }]; + + return info; +} + +- (void)invalidateSelection { + CFRelease(mSelection); + CFRelease(mCaret); + CFRelease(mPrevCaret); + mSelection = nil; +} + +- (mozilla::a11y::GeckoTextMarkerRange)selection { + return mozilla::a11y::GeckoTextMarkerRange(mGeckoDocAccessible, mSelection); +} + +- (AXTextMarkerRef)moxStartTextMarker { + GeckoTextMarker geckoTextPoint(mGeckoDocAccessible, 0); + return geckoTextPoint.CreateAXTextMarker(); +} + +- (AXTextMarkerRef)moxEndTextMarker { + uint32_t characterCount = + mGeckoDocAccessible->IsRemote() + ? mGeckoDocAccessible->AsRemote()->CharacterCount() + : mGeckoDocAccessible->AsLocal() + ->Document() + ->AsHyperText() + ->CharacterCount(); + GeckoTextMarker geckoTextPoint(mGeckoDocAccessible, characterCount); + return geckoTextPoint.CreateAXTextMarker(); +} + +- (AXTextMarkerRangeRef)moxSelectedTextMarkerRange { + return mSelection && + GeckoTextMarkerRange(mGeckoDocAccessible, mSelection).IsValid() + ? mSelection + : nil; +} + +- (NSString*)moxStringForTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange { + mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible, + textMarkerRange); + if (!range.IsValid()) { + return @""; + } + + return range.Text(); +} + +- (NSNumber*)moxLengthForTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange { + return @([[self moxStringForTextMarkerRange:textMarkerRange] length]); +} + +- (AXTextMarkerRangeRef)moxTextMarkerRangeForUnorderedTextMarkers: + (NSArray*)textMarkers { + if ([textMarkers count] != 2) { + // Don't allow anything but a two member array. + return nil; + } + + GeckoTextMarker p1(mGeckoDocAccessible, + (__bridge AXTextMarkerRef)textMarkers[0]); + GeckoTextMarker p2(mGeckoDocAccessible, + (__bridge AXTextMarkerRef)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(); +} + +- (AXTextMarkerRef)moxStartTextMarkerForTextMarkerRange: + (AXTextMarkerRangeRef)textMarkerRange { + mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible, + textMarkerRange); + + return range.IsValid() ? range.mStart.CreateAXTextMarker() : nil; +} + +- (AXTextMarkerRef)moxEndTextMarkerForTextMarkerRange: + (AXTextMarkerRangeRef)textMarkerRange { + mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible, + textMarkerRange); + + return range.IsValid() ? range.mEnd.CreateAXTextMarker() : nil; +} + +- (AXTextMarkerRangeRef)moxLeftWordTextMarkerRangeForTextMarker: + (AXTextMarkerRef)textMarker { + GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); + if (!geckoTextMarker.IsValid()) { + return nil; + } + + return geckoTextMarker.Range(EWhichRange::eLeftWord) + .CreateAXTextMarkerRange(); +} + +- (AXTextMarkerRangeRef)moxRightWordTextMarkerRangeForTextMarker: + (AXTextMarkerRef)textMarker { + GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); + if (!geckoTextMarker.IsValid()) { + return nil; + } + + return geckoTextMarker.Range(EWhichRange::eRightWord) + .CreateAXTextMarkerRange(); +} + +- (AXTextMarkerRangeRef)moxLineTextMarkerRangeForTextMarker: + (AXTextMarkerRef)textMarker { + GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); + if (!geckoTextMarker.IsValid()) { + return nil; + } + + return geckoTextMarker.Range(EWhichRange::eLine).CreateAXTextMarkerRange(); +} + +- (AXTextMarkerRangeRef)moxLeftLineTextMarkerRangeForTextMarker: + (AXTextMarkerRef)textMarker { + GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); + if (!geckoTextMarker.IsValid()) { + return nil; + } + + return geckoTextMarker.Range(EWhichRange::eLeftLine) + .CreateAXTextMarkerRange(); +} + +- (AXTextMarkerRangeRef)moxRightLineTextMarkerRangeForTextMarker: + (AXTextMarkerRef)textMarker { + GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); + if (!geckoTextMarker.IsValid()) { + return nil; + } + + return geckoTextMarker.Range(EWhichRange::eRightLine) + .CreateAXTextMarkerRange(); +} + +- (AXTextMarkerRangeRef)moxParagraphTextMarkerRangeForTextMarker: + (AXTextMarkerRef)textMarker { + GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); + if (!geckoTextMarker.IsValid()) { + return nil; + } + + return geckoTextMarker.Range(EWhichRange::eParagraph) + .CreateAXTextMarkerRange(); +} + +// override +- (AXTextMarkerRangeRef)moxStyleTextMarkerRangeForTextMarker: + (AXTextMarkerRef)textMarker { + GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); + if (!geckoTextMarker.IsValid()) { + return nil; + } + + return geckoTextMarker.Range(EWhichRange::eStyle).CreateAXTextMarkerRange(); +} + +- (AXTextMarkerRef)moxNextTextMarkerForTextMarker:(AXTextMarkerRef)textMarker { + GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); + if (!geckoTextMarker.IsValid()) { + return nil; + } + + if (!geckoTextMarker.Next()) { + return nil; + } + + return geckoTextMarker.CreateAXTextMarker(); +} + +- (AXTextMarkerRef)moxPreviousTextMarkerForTextMarker: + (AXTextMarkerRef)textMarker { + GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); + if (!geckoTextMarker.IsValid()) { + return nil; + } + + if (!geckoTextMarker.Previous()) { + return nil; + } + + return geckoTextMarker.CreateAXTextMarker(); +} + +- (NSAttributedString*)moxAttributedStringForTextMarkerRange: + (AXTextMarkerRangeRef)textMarkerRange { + mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible, + textMarkerRange); + if (!range.IsValid()) { + return nil; + } + + return range.AttributedText(); +} + +- (NSValue*)moxBoundsForTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange { + mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible, + textMarkerRange); + if (!range.IsValid()) { + return nil; + } + + return range.Bounds(); +} + +- (NSNumber*)moxIndexForTextMarker:(AXTextMarkerRef)textMarker { + GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); + if (!geckoTextMarker.IsValid()) { + return nil; + } + + GeckoTextMarkerRange range(GeckoTextMarker(mGeckoDocAccessible, 0), + geckoTextMarker); + + return @(range.Length()); +} + +- (AXTextMarkerRef)moxTextMarkerForIndex:(NSNumber*)index { + GeckoTextMarker geckoTextMarker = GeckoTextMarker::MarkerFromIndex( + mGeckoDocAccessible, [index integerValue]); + if (!geckoTextMarker.IsValid()) { + return nil; + } + + return geckoTextMarker.CreateAXTextMarker(); +} + +- (id)moxUIElementForTextMarker:(AXTextMarkerRef)textMarker { + GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); + if (!geckoTextMarker.IsValid()) { + return nil; + } + + Accessible* leaf = geckoTextMarker.Leaf(); + if (!leaf) { + return nil; + } + + return GetNativeFromGeckoAccessible(leaf); +} + +- (AXTextMarkerRangeRef)moxTextMarkerRangeForUIElement:(id)element { + if (![element isKindOfClass:[mozAccessible class]]) { + return nil; + } + + GeckoTextMarkerRange range((Accessible*)[element geckoAccessible]); + return range.CreateAXTextMarkerRange(); +} + +- (NSString*)moxMozDebugDescriptionForTextMarker:(AXTextMarkerRef)textMarker { + if (!mozilla::Preferences::GetBool(PREF_ACCESSIBILITY_MAC_DEBUG)) { + return nil; + } + + GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); + if (!geckoTextMarker.IsValid()) { + return @"<GeckoTextMarker 0x0 [0]>"; + } + + return [NSString stringWithFormat:@"<GeckoTextMarker %p [%d]>", + geckoTextMarker.mContainer, + geckoTextMarker.mOffset]; +} + +- (NSString*)moxMozDebugDescriptionForTextMarkerRange: + (AXTextMarkerRangeRef)textMarkerRange { + if (!mozilla::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 %p [%d] - %p [%d]>", + range.mStart.mContainer, range.mStart.mOffset, + range.mEnd.mContainer, range.mEnd.mOffset]; +} + +- (void)moxSetSelectedTextMarkerRange:(AXTextMarkerRangeRef)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..148d391542 --- /dev/null +++ b/accessible/mac/MOXWebAreaAccessible.mm @@ -0,0 +1,274 @@ +/* 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 "nsAccUtils.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] autorelease]; + + 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; + MOZ_ASSERT(mGeckoAccessible->IsDoc()); + nsAccUtils::DocumentURL(mGeckoAccessible, 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]; + MOZ_ASSERT(mGeckoAccessible->IsRemote() || + mGeckoAccessible->AsLocal()->IsRoot() || + mGeckoAccessible->AsLocal()->AsDoc()->ParentDocument(), + "Non-root doc without a parent!"); + if ((mGeckoAccessible->IsRemote() && + mGeckoAccessible->AsRemote()->IsDoc() && + mGeckoAccessible->AsRemote()->AsDoc()->IsTopLevel()) || + (mGeckoAccessible->IsLocal() && + !mGeckoAccessible->AsLocal()->IsRoot() && + mGeckoAccessible->AsLocal()->AsDoc()->ParentDocument() && + mGeckoAccessible->AsLocal()->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::DOCUMENT && [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 other than the default document role + 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..3f095a7996 --- /dev/null +++ b/accessible/mac/MacUtils.h @@ -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/. */ + +#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 +template <typename AccArray> +NSArray<mozAccessible*>* ConvertToNSArray(AccArray& aArray) { + NSMutableArray* nativeArray = [[[NSMutableArray alloc] init] autorelease]; + + // iterate through the list, and get each native accessible. + for (Accessible* curAccessible : aArray) { + mozAccessible* curNative = GetNativeFromGeckoAccessible(curAccessible); + if (curNative) + [nativeArray addObject:GetObjectOrRepresentedView(curNative)]; + } + + return nativeArray; +} + +/** + * 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, nsAtom* aAttrName); +} +} +} + +#endif diff --git a/accessible/mac/MacUtils.mm b/accessible/mac/MacUtils.mm new file mode 100644 index 0000000000..7fbb0e9ba4 --- /dev/null +++ b/accessible/mac/MacUtils.mm @@ -0,0 +1,49 @@ +/* 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 "LocalAccessible.h" +#include "nsCocoaUtils.h" +#include "mozilla/a11y/PDocAccessible.h" + +namespace mozilla { +namespace a11y { +namespace utils { + +/** + * 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, nsAtom* aAttrName) { + nsAutoString result; + Accessible* acc = [aNativeAccessible geckoAccessible]; + RefPtr<AccAttributes> attributes = acc->Attributes(); + + if (!attributes) { + return nil; + } + + attributes->GetAttribute(aAttrName, result); + + 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..c0d8273af0 --- /dev/null +++ b/accessible/mac/Platform.mm @@ -0,0 +1,243 @@ +/* clang-format off */ +/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* clang-format on */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#import <Cocoa/Cocoa.h> + +#import "MOXTextMarkerDelegate.h" + +#include "Platform.h" +#include "RemoteAccessible.h" +#include "DocAccessibleParent.h" +#include "mozTableAccessible.h" +#include "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; +@property(readonly) BOOL isSwitchControlEnabled; +@end + +namespace mozilla { +namespace a11y { + +// Mac a11y whitelisting +static bool sA11yShouldBeEnabled = false; + +bool ShouldA11yBeEnabled() { + EPlatformDisabledState disabledState = PlatformDisabledState(); + return (disabledState == ePlatformIsForceEnabled) || + ((disabledState == ePlatformIsEnabled) && sA11yShouldBeEnabled); +} + +void PlatformInit() {} + +void PlatformShutdown() {} + +void ProxyCreated(RemoteAccessible* aProxy) { + if (aProxy->Role() == roles::WHITESPACE) { + // We don't create a native object if we're child of a "flat" accessible; + // for example, on OS X buttons shouldn't have any children, because that + // makes the OS confused. We also don't create accessibles for <br> + // (whitespace) elements. + return; + } + + // Pass in dummy state for now as retrieving proxy state requires IPC. + // Note that we can use RemoteAccessible::IsTable* functions here because they + // do not use IPC calls but that might change after bug 1210477. + Class type; + if (aProxy->IsTable()) { + type = [mozTableAccessible class]; + } else if (aProxy->IsTableRow()) { + type = [mozTableRowAccessible class]; + } else if (aProxy->IsTableCell()) { + type = [mozTableCellAccessible class]; + } else if (aProxy->IsDoc()) { + type = [MOXWebAreaAccessible class]; + } else { + type = GetTypeFromRole(aProxy->Role()); + } + + mozAccessible* mozWrapper = [[type alloc] initWithAccessible:aProxy]; + aProxy->SetWrapper(reinterpret_cast<uintptr_t>(mozWrapper)); +} + +void ProxyDestroyed(RemoteAccessible* aProxy) { + mozAccessible* wrapper = GetNativeFromGeckoAccessible(aProxy); + [wrapper expire]; + [wrapper release]; + aProxy->SetWrapper(0); + + if (aProxy->IsDoc()) { + [MOXTextMarkerDelegate destroyForDoc:aProxy]; + } +} + +void ProxyEvent(RemoteAccessible* aProxy, uint32_t aEventType) { + // Ignore event that we don't escape below, they aren't yet supported. + if (aEventType != nsIAccessibleEvent::EVENT_ALERT && + 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 && + aEventType != nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED && + aEventType != nsIAccessibleEvent::EVENT_TABLE_STYLING_CHANGED) + return; + + mozAccessible* wrapper = GetNativeFromGeckoAccessible(aProxy); + if (wrapper) { + [wrapper handleAccessibleEvent:aEventType]; + } +} + +void ProxyStateChangeEvent(RemoteAccessible* aProxy, uint64_t aState, + bool aEnabled) { + mozAccessible* wrapper = GetNativeFromGeckoAccessible(aProxy); + if (wrapper) { + [wrapper stateChanged:aState isEnabled:aEnabled]; + } +} + +void ProxyCaretMoveEvent(RemoteAccessible* aTarget, int32_t aOffset, + bool aIsSelectionCollapsed, int32_t aGranularity) { + mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget); + MOXTextMarkerDelegate* delegate = + [MOXTextMarkerDelegate getOrCreateForDoc:aTarget->Document()]; + [delegate setCaretOffset:aTarget at:aOffset moveGranularity:aGranularity]; + if (aIsSelectionCollapsed) { + // If selection is collapsed, invalidate selection. + [delegate setSelectionFrom:aTarget at:aOffset to:aTarget at:aOffset]; + } + + if (wrapper) { + if (mozTextAccessible* textAcc = + static_cast<mozTextAccessible*>([wrapper moxEditableAncestor])) { + [textAcc + handleAccessibleEvent:nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED]; + } else { + [wrapper + handleAccessibleEvent:nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED]; + } + } +} + +void ProxyTextChangeEvent(RemoteAccessible* aTarget, const nsAString& aStr, + int32_t aStart, uint32_t aLen, bool aIsInsert, + bool aFromUser) { + RemoteAccessible* acc = aTarget; + // If there is a text input ancestor, use it as the event source. + while (acc && GetTypeFromRole(acc->Role()) != [mozTextAccessible class]) { + acc = acc->RemoteParent(); + } + mozAccessible* wrapper = GetNativeFromGeckoAccessible(acc ? acc : aTarget); + [wrapper handleAccessibleTextChangeEvent:nsCocoaUtils::ToNSString(aStr) + inserted:aIsInsert + inContainer:aTarget + at:aStart]; +} + +void ProxyShowHideEvent(RemoteAccessible*, RemoteAccessible*, bool, bool) {} + +void ProxySelectionEvent(RemoteAccessible* aTarget, RemoteAccessible* aWidget, + uint32_t aEventType) { + mozAccessible* wrapper = GetNativeFromGeckoAccessible(aWidget); + if (wrapper) { + [wrapper handleAccessibleEvent:aEventType]; + } +} + +void ProxyTextSelectionChangeEvent(RemoteAccessible* aTarget, + const nsTArray<TextRangeData>& aSelection) { + if (aSelection.Length()) { + MOXTextMarkerDelegate* delegate = + [MOXTextMarkerDelegate getOrCreateForDoc:aTarget->Document()]; + DocAccessibleParent* doc = aTarget->Document(); + RemoteAccessible* startContainer = + doc->GetAccessible(aSelection[0].StartID()); + RemoteAccessible* 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(RemoteAccessible* aTarget, const a11y::role& aRole, + uint8_t aRoleMapEntryIndex) { + 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 (sA11yShouldBeEnabled) { + // If accessibility should be enabled, log the appropriate client + nsAutoString client; + if ([[NSWorkspace sharedWorkspace] + respondsToSelector:@selector(isVoiceOverEnabled)] && + [[NSWorkspace sharedWorkspace] isVoiceOverEnabled]) { + client.Assign(u"VoiceOver"_ns); + } else if ([[NSWorkspace sharedWorkspace] + respondsToSelector:@selector(isSwitchControlEnabled)] && + [[NSWorkspace sharedWorkspace] isSwitchControlEnabled]) { + client.Assign(u"SwitchControl"_ns); + } else { + // This is more complicated than the NSWorkspace queries above + // because (a) there is no "full keyboard access" query for NSWorkspace + // and (b) the [NSApplication fullKeyboardAccessEnabled] query checks + // the pre-Monterey version of full keyboard access, which is not what + // we're looking for here. For more info, see bug 1772375 comment 7. + Boolean exists; + int val = CFPreferencesGetAppIntegerValue( + CFSTR("FullKeyboardAccessEnabled"), + CFSTR("com.apple.Accessibility"), &exists); + if (exists && val == 1) { + client.Assign(u"FullKeyboardAccess"_ns); + } else { + client.Assign(u"Unknown"_ns); + } + } + +#if defined(MOZ_TELEMETRY_REPORTING) + Telemetry::ScalarSet(Telemetry::ScalarID::A11Y_INSTANTIATORS, client); +#endif // defined(MOZ_TELEMETRY_REPORTING) + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::AccessibilityClient, + NS_ConvertUTF16toUTF8(client)); + } + } + + 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..632233cfcf --- /dev/null +++ b/accessible/mac/RootAccessibleWrap.h @@ -0,0 +1,40 @@ +/* -*- 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 { + +/** + * Mac specific functionality for the node at a root of the accessibility + * tree: see the RootAccessible superclass for further details. + */ +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..e3d3da9224 --- /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_BLOCK_RETURN; + + return [mozRootAccessible class]; + + NS_OBJC_END_TRY_BLOCK_RETURN(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..e7d44adf4d --- /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(Accessible* aDirectDescendantsFrom); + explicit RotorRule(); + uint16_t Match(Accessible* aAcc) override; + + private: + Accessible* mDirectDescendantsFrom; +}; + +/** + * This rule matches all accessibles of a given role. + */ +class RotorRoleRule : public RotorRule { + public: + explicit RotorRoleRule(role aRole, Accessible* aDirectDescendantsFrom); + explicit RotorRoleRule(role aRole); + uint16_t Match(Accessible* aAcc) override; + + private: + role mRole; +}; + +class RotorMacRoleRule : public RotorRule { + public: + explicit RotorMacRoleRule(NSString* aRole); + explicit RotorMacRoleRule(NSString* aRole, + Accessible* aDirectDescendantsFrom); + ~RotorMacRoleRule(); + virtual uint16_t Match(Accessible* aAcc) override; + + protected: + NSString* mMacRole; +}; + +class RotorControlRule final : public RotorRule { + public: + explicit RotorControlRule(Accessible* aDirectDescendantsFrom); + explicit RotorControlRule(); + + virtual uint16_t Match(Accessible* aAcc) override; +}; + +class RotorTextEntryRule final : public RotorRule { + public: + explicit RotorTextEntryRule(Accessible* aDirectDescendantsFrom); + explicit RotorTextEntryRule(); + + virtual uint16_t Match(Accessible* aAcc) override; +}; + +class RotorLinkRule : public RotorRule { + public: + explicit RotorLinkRule(); + explicit RotorLinkRule(Accessible* aDirectDescendantsFrom); + + virtual uint16_t Match(Accessible* aAcc) override; +}; + +class RotorVisitedLinkRule final : public RotorLinkRule { + public: + explicit RotorVisitedLinkRule(); + explicit RotorVisitedLinkRule(Accessible* aDirectDescendantsFrom); + + virtual uint16_t Match(Accessible* aAcc) override; +}; + +class RotorUnvisitedLinkRule final : public RotorLinkRule { + public: + explicit RotorUnvisitedLinkRule(); + explicit RotorUnvisitedLinkRule(Accessible* aDirectDescendantsFrom); + + virtual uint16_t Match(Accessible* aAcc) 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, + Accessible* aDirectDescendantsFrom); + explicit RotorNotMacRoleRule(NSString* aMacRole); + uint16_t Match(Accessible* aAcc) override; +}; + +class RotorStaticTextRule : public RotorRule { + public: + explicit RotorStaticTextRule(); + explicit RotorStaticTextRule(Accessible* aDirectDescendantsFrom); + + virtual uint16_t Match(Accessible* aAcc) override; +}; + +class RotorHeadingLevelRule : public RotorRoleRule { + public: + explicit RotorHeadingLevelRule(int32_t aLevel); + explicit RotorHeadingLevelRule(int32_t aLevel, + Accessible* aDirectDescendantsFrom); + + virtual uint16_t Match(Accessible* aAcc) override; + + private: + int32_t mLevel; +}; + +class RotorLiveRegionRule : public RotorRule { + public: + explicit RotorLiveRegionRule(Accessible* aDirectDescendantsFrom) + : RotorRule(aDirectDescendantsFrom) {} + explicit RotorLiveRegionRule() : RotorRule() {} + + uint16_t Match(Accessible* aAcc) 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(Accessible* aAcc) override; +}; diff --git a/accessible/mac/RotorRules.mm b/accessible/mac/RotorRules.mm new file mode 100644 index 0000000000..02593b91f5 --- /dev/null +++ b/accessible/mac/RotorRules.mm @@ -0,0 +1,360 @@ +/* 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(Accessible* aDirectDescendantsFrom) + : mDirectDescendantsFrom(aDirectDescendantsFrom) {} + +RotorRule::RotorRule() : mDirectDescendantsFrom(nullptr) {} + +uint16_t RotorRule::Match(Accessible* aAcc) { + uint16_t result = nsIAccessibleTraversalRule::FILTER_IGNORE; + + if (nsAccUtils::MustPrune(aAcc)) { + result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + + if (mDirectDescendantsFrom && (aAcc != mDirectDescendantsFrom)) { + result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + + if ([GetNativeFromGeckoAccessible(aAcc) isAccessibilityElement]) { + result |= nsIAccessibleTraversalRule::FILTER_MATCH; + } + + return result; +} + +// Rotor Role Rule + +RotorRoleRule::RotorRoleRule(role aRole, Accessible* aDirectDescendantsFrom) + : RotorRule(aDirectDescendantsFrom), mRole(aRole){}; + +RotorRoleRule::RotorRoleRule(role aRole) : RotorRule(), mRole(aRole){}; + +uint16_t RotorRoleRule::Match(Accessible* aAcc) { + uint16_t result = RotorRule::Match(aAcc); + + // 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) && + aAcc->Role() != mRole) { + result &= ~nsIAccessibleTraversalRule::FILTER_MATCH; + } + + return result; +} + +// Rotor Mac Role Rule + +RotorMacRoleRule::RotorMacRoleRule(NSString* aMacRole, + Accessible* aDirectDescendantsFrom) + : RotorRule(aDirectDescendantsFrom), mMacRole(aMacRole) { + [mMacRole retain]; +}; + +RotorMacRoleRule::RotorMacRoleRule(NSString* aMacRole) + : RotorRule(), mMacRole(aMacRole) { + [mMacRole retain]; +}; + +RotorMacRoleRule::~RotorMacRoleRule() { [mMacRole release]; } + +uint16_t RotorMacRoleRule::Match(Accessible* aAcc) { + uint16_t result = RotorRule::Match(aAcc); + + // 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(aAcc); + if (![[nativeMatch moxRole] isEqualToString:mMacRole]) { + result &= ~nsIAccessibleTraversalRule::FILTER_MATCH; + } + } + + return result; +} + +// Rotor Control Rule + +RotorControlRule::RotorControlRule(Accessible* aDirectDescendantsFrom) + : RotorRule(aDirectDescendantsFrom){}; + +RotorControlRule::RotorControlRule() : RotorRule(){}; + +uint16_t RotorControlRule::Match(Accessible* aAcc) { + uint16_t result = RotorRule::Match(aAcc); + + // 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 (aAcc->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: + case roles::BUTTONMENU: + 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 < aAcc->ChildCount(); i++) { + Accessible* currChild = aAcc->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(Accessible* aDirectDescendantsFrom) + : RotorRule(aDirectDescendantsFrom){}; + +RotorTextEntryRule::RotorTextEntryRule() : RotorRule(){}; + +uint16_t RotorTextEntryRule::Match(Accessible* aAcc) { + uint16_t result = RotorRule::Match(aAcc); + + // 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 (aAcc->Role() != roles::PASSWORD_TEXT && aAcc->Role() != roles::ENTRY) { + result &= ~nsIAccessibleTraversalRule::FILTER_MATCH; + } + } + + return result; +} + +// Rotor Link Rule + +RotorLinkRule::RotorLinkRule(Accessible* aDirectDescendantsFrom) + : RotorRule(aDirectDescendantsFrom){}; + +RotorLinkRule::RotorLinkRule() : RotorRule(){}; + +uint16_t RotorLinkRule::Match(Accessible* aAcc) { + uint16_t result = RotorRule::Match(aAcc); + + // 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(aAcc); + if (![[nativeMatch moxRole] isEqualToString:@"AXLink"]) { + result &= ~nsIAccessibleTraversalRule::FILTER_MATCH; + } + } + + return result; +} + +RotorVisitedLinkRule::RotorVisitedLinkRule() : RotorLinkRule() {} + +RotorVisitedLinkRule::RotorVisitedLinkRule(Accessible* aDirectDescendantsFrom) + : RotorLinkRule(aDirectDescendantsFrom) {} + +uint16_t RotorVisitedLinkRule::Match(Accessible* aAcc) { + uint16_t result = RotorLinkRule::Match(aAcc); + + if (result & nsIAccessibleTraversalRule::FILTER_MATCH) { + mozAccessible* nativeMatch = GetNativeFromGeckoAccessible(aAcc); + if (![[nativeMatch moxVisited] boolValue]) { + result &= ~nsIAccessibleTraversalRule::FILTER_MATCH; + } + } + + return result; +} + +RotorUnvisitedLinkRule::RotorUnvisitedLinkRule() : RotorLinkRule() {} + +RotorUnvisitedLinkRule::RotorUnvisitedLinkRule( + Accessible* aDirectDescendantsFrom) + : RotorLinkRule(aDirectDescendantsFrom) {} + +uint16_t RotorUnvisitedLinkRule::Match(Accessible* aAcc) { + uint16_t result = RotorLinkRule::Match(aAcc); + + if (result & nsIAccessibleTraversalRule::FILTER_MATCH) { + mozAccessible* nativeMatch = GetNativeFromGeckoAccessible(aAcc); + if ([[nativeMatch moxVisited] boolValue]) { + result &= ~nsIAccessibleTraversalRule::FILTER_MATCH; + } + } + + return result; +} + +// Match Not Rule + +RotorNotMacRoleRule::RotorNotMacRoleRule(NSString* aMacRole, + Accessible* aDirectDescendantsFrom) + : RotorMacRoleRule(aMacRole, aDirectDescendantsFrom) {} + +RotorNotMacRoleRule::RotorNotMacRoleRule(NSString* aMacRole) + : RotorMacRoleRule(aMacRole) {} + +uint16_t RotorNotMacRoleRule::Match(Accessible* aAcc) { + uint16_t result = RotorRule::Match(aAcc); + + // 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(aAcc); + if ([[nativeMatch moxRole] isEqualToString:mMacRole]) { + result &= ~nsIAccessibleTraversalRule::FILTER_MATCH; + } + } + return result; +} + +// Rotor Static Text Rule + +RotorStaticTextRule::RotorStaticTextRule(Accessible* aDirectDescendantsFrom) + : RotorRule(aDirectDescendantsFrom){}; + +RotorStaticTextRule::RotorStaticTextRule() : RotorRule(){}; + +uint16_t RotorStaticTextRule::Match(Accessible* aAcc) { + uint16_t result = RotorRule::Match(aAcc); + + // 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(aAcc); + if (![[nativeMatch moxRole] isEqualToString:@"AXStaticText"]) { + result &= ~nsIAccessibleTraversalRule::FILTER_MATCH; + } + } + + return result; +} + +// Rotor Heading Level Rule + +RotorHeadingLevelRule::RotorHeadingLevelRule(int32_t aLevel, + Accessible* aDirectDescendantsFrom) + : RotorRoleRule(roles::HEADING, aDirectDescendantsFrom), mLevel(aLevel){}; + +RotorHeadingLevelRule::RotorHeadingLevelRule(int32_t aLevel) + : RotorRoleRule(roles::HEADING), mLevel(aLevel){}; + +uint16_t RotorHeadingLevelRule::Match(Accessible* aAcc) { + uint16_t result = RotorRoleRule::Match(aAcc); + + // 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 = aAcc->GroupPosition().level; + + if (currLevel != mLevel) { + result &= ~nsIAccessibleTraversalRule::FILTER_MATCH; + } + } + + return result; +} + +uint16_t RotorLiveRegionRule::Match(Accessible* aAcc) { + uint16_t result = RotorRule::Match(aAcc); + + if ((result & nsIAccessibleTraversalRule::FILTER_MATCH)) { + mozAccessible* nativeMatch = GetNativeFromGeckoAccessible(aAcc); + if (![nativeMatch moxIsLiveRegion]) { + result &= ~nsIAccessibleTraversalRule::FILTER_MATCH; + } + } + return result; +} + +// Outline Rule + +OutlineRule::OutlineRule() : RotorRule(){}; + +uint16_t OutlineRule::Match(Accessible* aAcc) { + uint16_t result = RotorRule::Match(aAcc); + + // 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 (aAcc->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 (aAcc->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..1e406ade1f --- /dev/null +++ b/accessible/mac/SelectorMapGen.py @@ -0,0 +1,61 @@ +#!/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/. + +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, _ 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/moz.build b/accessible/mac/moz.build new file mode 100644 index 0000000000..1216c9f151 --- /dev/null +++ b/accessible/mac/moz.build @@ -0,0 +1,75 @@ +# -*- 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") + +REQUIRES_UNIFIED_BUILD = True diff --git a/accessible/mac/mozAccessible.h b/accessible/mac/mozAccessible.h new file mode 100644 index 0000000000..3288b0cd97 --- /dev/null +++ b/accessible/mac/mozAccessible.h @@ -0,0 +1,276 @@ +/* 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 "RemoteAccessible.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::Accessible* aAcc) { + if (!aAcc) { + return nil; + } + if (LocalAccessible* acc = aAcc->AsLocal()) { + mozAccessible* native = nil; + acc->GetNativeInterface((void**)&native); + return native; + } + + RemoteAccessible* proxy = aAcc->AsRemote(); + 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::Accessible* 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::Accessible*)aAcc; + +// allows for gecko accessible access outside of the class +- (mozilla::a11y::Accessible*)geckoAccessible; + +- (mozilla::a11y::Accessible*)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:(mozilla::a11y::Accessible*)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 +- (NSNumber*)moxExpanded; + +// override +- (NSValue*)moxFrame; + +// 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..f8def7a384 --- /dev/null +++ b/accessible/mac/mozAccessible.mm @@ -0,0 +1,1062 @@ +/* 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 "LocalAccessible-inl.h" +#include "nsAccUtils.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; +- (void)maybePostA11yUtilNotification; +@end + +@implementation mozAccessible + +- (id)initWithAccessible:(Accessible*)aAcc { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + MOZ_ASSERT(aAcc, "Cannot init mozAccessible with null"); + if ((self = [super init])) { + mGeckoAccessible = aAcc; + mRole = aAcc->Role(); + } + + return self; + + NS_OBJC_END_TRY_BLOCK_RETURN(nil); +} + +- (void)dealloc { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + [super dealloc]; + + NS_OBJC_END_TRY_IGNORE_BLOCK; +} + +#pragma mark - mozAccessible widget + +- (BOOL)hasRepresentedView { + return NO; +} + +- (id)representedView { + return nil; +} + +- (BOOL)isRoot { + return NO; +} + +#pragma mark - + +- (BOOL)moxIgnoreWithParent:(mozAccessible*)parent { + if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) { + 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_BLOCK_RETURN; + + Accessible* child = mGeckoAccessible->ChildAt(i); + return child ? GetNativeFromGeckoAccessible(child) : nil; + + NS_OBJC_END_TRY_BLOCK_RETURN(nil); +} + +static const uint64_t kCachedStates = + states::CHECKED | states::PRESSED | states::MIXED | states::EXPANDED | + states::EXPANDABLE | states::CURRENT | states::SELECTED | + states::TRAVERSED | states::LINKED | states::HASPOPUP | states::BUSY | + states::MULTI_LINE | states::CHECKABLE; +static const uint64_t kCacheInitialized = ((uint64_t)0x1) << 63; + +- (uint64_t)state { + uint64_t state = 0; + + if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) { + state = acc->State(); + } + + if (RemoteAccessible* proxy = mGeckoAccessible->AsRemote()) { + 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 || + mRole == roles::ARTICLE || mRole == roles::ENTRY || + mRole == roles::SPINBUTTON; +} + +- (mozilla::a11y::Accessible*)geckoAccessible { + return mGeckoAccessible; +} + +- (mozilla::a11y::Accessible*)geckoDocument { + MOZ_ASSERT(mGeckoAccessible); + + if (mGeckoAccessible->IsLocal()) { + if (mGeckoAccessible->AsLocal()->IsDoc()) { + return mGeckoAccessible; + } + return mGeckoAccessible->AsLocal()->Document(); + } + + if (mGeckoAccessible->AsRemote()->IsDoc()) { + return mGeckoAccessible; + } + + return mGeckoAccessible->AsRemote()->Document(); +} + +#pragma mark - MOXAccessible protocol + +- (BOOL)moxBlockSelector:(SEL)selector { + if (selector == @selector(moxPerformPress)) { + uint8_t actionCount = mGeckoAccessible->IsLocal() + ? mGeckoAccessible->AsLocal()->ActionCount() + : mGeckoAccessible->AsRemote()->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]; + } + + if (selector == @selector(moxExpanded)) { + return [self stateWithMask:states::EXPANDABLE] == 0; + } + + return [super moxBlockSelector:selector]; +} + +- (id)moxFocusedUIElement { + MOZ_ASSERT(mGeckoAccessible); + // This only gets queried on the web area or the root group + // so just use the doc's focused child instead of trying to get + // the focused child of mGeckoAccessible. + Accessible* doc = nsAccUtils::DocumentFor(mGeckoAccessible); + mozAccessible* focusedChild = + GetNativeFromGeckoAccessible(doc->FocusedChild()); + + if ([focusedChild isAccessibilityElement]) { + return focusedChild; + } + + // return ourself if we can't get a native focused child. + return self; +} + +- (id<MOXTextMarkerSupport>)moxTextMarkerDelegate { + MOZ_ASSERT(mGeckoAccessible); + + if (mGeckoAccessible->IsLocal()) { + return [MOXTextMarkerDelegate + getOrCreateForDoc:mGeckoAccessible->AsLocal()->Document()]; + } + + return [MOXTextMarkerDelegate + getOrCreateForDoc:mGeckoAccessible->AsRemote()->Document()]; +} + +- (BOOL)moxIsLiveRegion { + return mIsLiveRegion; +} + +- (id)moxHitTest:(NSPoint)point { + MOZ_ASSERT(mGeckoAccessible); + + // 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)); + + Accessible* child = mGeckoAccessible->ChildAtPoint( + geckoPoint.x, geckoPoint.y, Accessible::EWhichChildAtPoint::DeepestChild); + + if (child) { + 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_BLOCK_RETURN; + if ([self isExpired]) { + return nil; + } + + Accessible* parent = mGeckoAccessible->Parent(); + + if (!parent) { + return nil; + } + + id nativeParent = GetNativeFromGeckoAccessible(parent); + if ([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->IsLocal()) { + // 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->AsLocal()->RootAccessible()); + } + + return GetObjectOrRepresentedView(nativeParent); + + NS_OBJC_END_TRY_BLOCK_RETURN(nil); +} + +// gets all our native children lazily, including those that are ignored. +- (NSArray*)moxChildren { + MOZ_ASSERT(mGeckoAccessible); + + NSMutableArray* children = [[[NSMutableArray alloc] + initWithCapacity:mGeckoAccessible->ChildCount()] autorelease]; + + for (uint32_t childIdx = 0; childIdx < mGeckoAccessible->ChildCount(); + childIdx++) { + Accessible* child = mGeckoAccessible->ChildAt(childIdx); + mozAccessible* nativeChild = GetNativeFromGeckoAccessible(child); + if (!nativeChild) { + continue; + } + + [children addObject:nativeChild]; + } + + return children; +} + +- (NSValue*)moxPosition { + CGRect frame = [[self moxFrame] rectValue]; + + return [NSValue valueWithPoint:NSMakePoint(frame.origin.x, frame.origin.y)]; +} + +- (NSValue*)moxSize { + CGRect frame = [[self moxFrame] rectValue]; + + return + [NSValue valueWithSize:NSMakeSize(frame.size.width, frame.size.height)]; +} + +- (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); + + if (mGeckoAccessible->HasARIARole()) { + const nsRoleMapEntry* roleMap = mGeckoAccessible->ARIARoleMap(); + return roleMap->roleAtom; + } + + return nsGkAtoms::_empty; +} + +- (NSString*)moxSubrole { + MOZ_ASSERT(mGeckoAccessible); + + LocalAccessible* acc = mGeckoAccessible->AsLocal(); + RemoteAccessible* proxy = mGeckoAccessible->AsRemote(); + + // 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, nsGkAtoms::aria_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; + } + + 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). + */ + ENameValueFlag flag = mGeckoAccessible->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_BLOCK_RETURN; + + // In some special cases we provide the name in the label (AXDescription). + if ([self providesLabelNotTitle]) { + return nil; + } + + nsAutoString title; + mGeckoAccessible->Name(title); + if (nsCoreUtils::IsWhitespaceString(title)) { + return @""; + } + + return nsCocoaUtils::ToNSString(title); + + NS_OBJC_END_TRY_BLOCK_RETURN(nil); +} + +- (id)moxValue { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + nsAutoString value; + mGeckoAccessible->Value(value); + + return nsCocoaUtils::ToNSString(value); + + NS_OBJC_END_TRY_BLOCK_RETURN(nil); +} + +- (NSString*)moxHelp { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + // 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; + mGeckoAccessible->Description(helpText); + + return nsCocoaUtils::ToNSString(helpText); + + NS_OBJC_END_TRY_BLOCK_RETURN(nil); +} + +- (NSWindow*)moxWindow { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + // Get a pointer to the native window (NSWindow) we reside in. + NSWindow* nativeWindow = nil; + DocAccessible* docAcc = nullptr; + if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) { + docAcc = acc->Document(); + } else { + RemoteAccessible* proxy = mGeckoAccessible->AsRemote(); + LocalAccessible* outerDoc = proxy->OuterDocOfRemoteBrowser(); + if (outerDoc) docAcc = outerDoc->Document(); + } + + if (docAcc) nativeWindow = static_cast<NSWindow*>(docAcc->GetNativeWindow()); + + MOZ_ASSERT(nativeWindow || gfxPlatform::IsHeadless(), + "Couldn't get native window"); + return nativeWindow; + + NS_OBJC_END_TRY_BLOCK_RETURN(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; +} + +- (NSNumber*)moxExpanded { + return @([self stateWithMask:states::EXPANDED] != 0); +} + +- (NSValue*)moxFrame { + MOZ_ASSERT(mGeckoAccessible); + + LayoutDeviceIntRect rect = mGeckoAccessible->IsLocal() + ? mGeckoAccessible->AsLocal()->Bounds() + : mGeckoAccessible->AsRemote()->Bounds(); + NSScreen* mainView = [[NSScreen screens] objectAtIndex:0]; + CGFloat scaleFactor = nsCocoaUtils::GetBackingScaleFactor(mainView); + + return [NSValue + valueWithRect: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)]; +} + +- (NSString*)moxARIACurrent { + if (![self stateWithMask:states::CURRENT]) { + return nil; + } + + return utils::GetAccAttr(self, nsGkAtoms::aria_current); +} + +- (NSNumber*)moxARIAAtomic { + return @(utils::GetAccAttr(self, nsGkAtoms::aria_atomic) != nil); +} + +- (NSString*)moxARIALive { + return utils::GetAccAttr(self, nsGkAtoms::aria_live); +} + +- (NSString*)moxARIARelevant { + if (NSString* relevant = + utils::GetAccAttr(self, nsGkAtoms::containerRelevant)) { + return relevant; + } + + // Default aria-relevant value + return @"additions text"; +} + +- (id)moxTitleUIElement { + MOZ_ASSERT(mGeckoAccessible); + + NSArray* relations = [self getRelationsByType:RelationType::LABELLED_BY]; + if ([relations count] == 1) { + return [relations firstObject]; + } + + return nil; +} + +- (NSString*)moxDOMIdentifier { + MOZ_ASSERT(mGeckoAccessible); + + nsAutoString id; + mGeckoAccessible->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 { + Accessible* doc = [self geckoDocument]; + while (doc) { + if (doc->IsLocal()) { + DocAccessible* docAcc = doc->AsLocal()->AsDoc(); + if (docAcc->DocumentNode()->GetBrowsingContext()->IsTopContent()) { + return GetNativeFromGeckoAccessible(docAcc); + } + + doc = docAcc->ParentDocument(); + } else { + DocAccessibleParent* docProxy = doc->AsRemote()->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 { + return [self moxFindAncestor:^BOOL(id moxAcc, BOOL* stop) { + return [moxAcc isKindOfClass:[mozTextAccessible class]]; + }]; +} + +- (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_BLOCK_RETURN; + + NSMutableString* domInfo = [NSMutableString string]; + if (NSString* tagName = utils::GetAccAttr(self, nsGkAtoms::tag)) { + [domInfo appendFormat:@" %@", tagName]; + NSString* domID = [self moxDOMIdentifier]; + if ([domID length]) { + [domInfo appendFormat:@"#%@", domID]; + } + if (NSString* className = utils::GetAccAttr(self, nsGkAtoms::_class)) { + [domInfo + appendFormat:@".%@", + [className stringByReplacingOccurrencesOfString:@" " + withString:@"."]]; + } + } + + return [NSString stringWithFormat:@"<%@: %p %@%@>", + NSStringFromClass([self class]), self, + [self moxRole], domInfo]; + + NS_OBJC_END_TRY_BLOCK_RETURN(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] autorelease]; + + return [search performSearch]; +} + +- (NSNumber*)moxUIElementCountForSearchPredicate: + (NSDictionary*)searchPredicate { + return [NSNumber + numberWithDouble:[[self moxUIElementsForSearchPredicate:searchPredicate] + count]]; +} + +- (void)moxSetFocused:(NSNumber*)focused { + MOZ_ASSERT(mGeckoAccessible); + + if ([focused boolValue]) { + mGeckoAccessible->TakeFocus(); + } +} + +- (void)moxPerformScrollToVisible { + MOZ_ASSERT(mGeckoAccessible); + mGeckoAccessible->ScrollTo(nsIAccessibleScrollType::SCROLL_TYPE_ANYWHERE); +} + +- (void)moxPerformShowMenu { + MOZ_ASSERT(mGeckoAccessible); + + // We don't need to convert this rect into mac coordinates because the + // mouse event synthesizer expects layout (gecko) coordinates. + LayoutDeviceIntRect bounds = mGeckoAccessible->IsLocal() + ? mGeckoAccessible->AsLocal()->Bounds() + : mGeckoAccessible->AsRemote()->Bounds(); + + LocalAccessible* rootAcc = mGeckoAccessible->IsLocal() + ? mGeckoAccessible->AsLocal()->RootAccessible() + : mGeckoAccessible->AsRemote() + ->OuterDocOfRemoteBrowser() + ->RootAccessible(); + id objOrView = + GetObjectOrRepresentedView(GetNativeFromGeckoAccessible(rootAcc)); + + LayoutDeviceIntPoint p = LayoutDeviceIntPoint( + bounds.X() + (bounds.Width() / 2), bounds.Y() + (bounds.Height() / 2)); + nsIWidget* widget = [objOrView widget]; + widget->SynthesizeNativeMouseEvent( + p, nsIWidget::NativeMouseMessage::ButtonDown, MouseButton::eSecondary, + nsIWidget::Modifiers::NO_MODIFIERS, nullptr); +} + +- (void)moxPerformPress { + MOZ_ASSERT(mGeckoAccessible); + + if (mGeckoAccessible->IsLocal()) { + mGeckoAccessible->AsLocal()->DoAction(0); + } else { + mGeckoAccessible->AsRemote()->DoAction(0); + } + + // Activating accessible may alter its state. + [self invalidateState]; +} + +#pragma mark - + +- (BOOL)disableChild:(mozAccessible*)child { + return NO; +} + +- (void)maybePostLiveRegionChanged { + id<MOXAccessible> liveRegion = + [self moxFindAncestor:^BOOL(id<MOXAccessible> moxAcc, BOOL* stop) { + return [moxAcc moxIsLiveRegion]; + }]; + + if (liveRegion) { + [liveRegion moxPostNotification:@"AXLiveRegionChanged"]; + } +} + +- (void)maybePostA11yUtilNotification { + MOZ_ASSERT(mGeckoAccessible); + // Sometimes we use a special live region to make announcements to the user. + // This region is a child of the root document, but doesn't contain any + // content. If we try to fire regular AXLiveRegion changed events through it, + // VoiceOver clips the notifications because it (rightfully) doesn't detect + // focus within the region. We get around this by firing an + // AXAnnouncementRequested notification here instead. + // Verify we're trying to send a notification for the a11yUtils alert (and not + // a random acc with the same ID) by checking: + // - The gecko acc is local, our a11y-announcement lives in browser.xhtml + // - The ID of the gecko acc is "a11y-announcement" + // - The native acc is a direct descendent of the root + if (mGeckoAccessible->IsLocal() && + [[self moxDOMIdentifier] isEqualToString:@"a11y-announcement"] && + [[self moxParent] isKindOfClass:[mozRootAccessible class]]) { + // Our actual announcement should be stored as a child of the alert, + // so we verify a child exists, and then query that child below. + NSArray* children = [self moxChildren]; + MOZ_ASSERT([children count] == 1 && children[0], + "A11yUtil event recieved, but no announcement found?"); + + mozAccessible* announcement = children[0]; + NSString* key; + if ([announcement providesLabelNotTitle]) { + key = [announcement moxLabel]; + } else { + key = [announcement moxTitle]; + } + + NSDictionary* info = @{ + NSAccessibilityAnnouncementKey : key ? key : @(""), + NSAccessibilityPriorityKey : @(NSAccessibilityPriorityMedium) + }; + + id window = [self moxWindow]; + + // This sends events via nsIObserverService to be consumed by our + // mochitests. Normally we'd fire these events through moxPostNotification + // which takes care of this, but because the window we fetch above isn't + // derrived from MOXAccessibleBase, we do this (and post the notification) + // manually. + xpcAccessibleMacEvent::FireEvent( + window, NSAccessibilityAnnouncementRequestedNotification, info); + NSAccessibilityPostNotificationWithUserInfo( + window, NSAccessibilityAnnouncementRequestedNotification, info); + } +} + +- (NSArray<mozAccessible*>*)getRelationsByType:(RelationType)relationType { + NSMutableArray<mozAccessible*>* relations = + [[[NSMutableArray alloc] init] autorelease]; + Relation rel = mGeckoAccessible->RelationByType(relationType); + while (Accessible* relAcc = rel.Next()) { + if (mozAccessible* relNative = GetNativeFromGeckoAccessible(relAcc)) { + [relations addObject:relNative]; + } + } + + return relations; +} + +- (void)handleAccessibleTextChangeEvent:(NSString*)change + inserted:(BOOL)isInserted + inContainer:(Accessible*)container + at:(int32_t)start { +} + +- (void)handleAccessibleEvent:(uint32_t)eventType { + switch (eventType) { + case nsIAccessibleEvent::EVENT_ALERT: + [self maybePostA11yUtilNotification]; + break; + 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] autorelease]; + 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_IGNORE_BLOCK; + + [self invalidateState]; + + mGeckoAccessible = nullptr; + + [self moxPostNotification:NSAccessibilityUIElementDestroyedNotification]; + + NS_OBJC_END_TRY_IGNORE_BLOCK; +} + +- (BOOL)isExpired { + return !mGeckoAccessible; +} + +@end diff --git a/accessible/mac/mozAccessibleProtocol.h b/accessible/mac/mozAccessibleProtocol.h new file mode 100644 index 0000000000..bc418fa4f5 --- /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 <NSObject> + +// 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..4c116b1bd0 --- /dev/null +++ b/accessible/mac/mozActionElements.h @@ -0,0 +1,101 @@ +/* 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 + +// LocalAccessible 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 +- (id)moxValue; + +// override +- (NSString*)moxValueDescription; + +// override +- (id)moxMinValue; + +// override +- (id)moxMaxValue; + +// override +- (void)moxSetValue:(id)value; + +// override +- (void)moxPerformIncrement; + +// override +- (void)moxPerformDecrement; + +// override +- (NSString*)moxOrientation; + +// 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..46ca8dab81 --- /dev/null +++ b/accessible/mac/mozActionElements.mm @@ -0,0 +1,237 @@ +/* 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 "LocalAccessible-inl.h" +#include "DocAccessible.h" +#include "XULTabAccessible.h" +#include "HTMLFormControlAccessible.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, nsGkAtoms::aria_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_BLOCK_RETURN; + + return [NSNumber numberWithInt:[self isChecked]]; + + NS_OBJC_END_TRY_BLOCK_RETURN(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 { + // By default, all tab panels are exposed in the a11y tree + // even if the tab they represent isn't the active tab. To + // prevent VoiceOver from navigating background tab content, + // only expose the tab panel that is currently on screen. + for (mozAccessible* child in [super moxChildren]) { + if (!([child state] & states::OFFSCREEN)) { + return [NSArray arrayWithObject:GetObjectOrRepresentedView(child)]; + } + } + MOZ_ASSERT_UNREACHABLE("We have no on screen tab content?"); + return @[]; +} + +@end + +@implementation mozIncrementableAccessible + +- (id)moxValue { + return [NSNumber numberWithDouble:mGeckoAccessible->CurValue()]; +} + +- (NSString*)moxValueDescription { + nsAutoString valueDesc; + mGeckoAccessible->Value(valueDesc); + return nsCocoaUtils::ToNSString(valueDesc); +} +- (id)moxMinValue { + return [NSNumber numberWithDouble:mGeckoAccessible->MinValue()]; +} + +- (id)moxMaxValue { + return [NSNumber numberWithDouble:mGeckoAccessible->MaxValue()]; +} + +- (void)moxSetValue:(id)value { + [self setValue:([value doubleValue])]; +} + +- (void)moxPerformIncrement { + [self changeValueBySteps:1]; +} + +- (void)moxPerformDecrement { + [self changeValueBySteps:-1]; +} + +- (NSString*)moxOrientation { + RefPtr<AccAttributes> attributes = mGeckoAccessible->Attributes(); + if (attributes) { + nsAutoString result; + attributes->GetAttribute(nsGkAtoms::aria_orientation, result); + if (result.Equals(u"horizontal"_ns)) { + return NSAccessibilityHorizontalOrientationValue; + } else if (result.Equals(u"vertical"_ns)) { + return NSAccessibilityVerticalOrientationValue; + } + } + + return NSAccessibilityUnknownOrientationValue; +} + +- (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 and step. + * + * factor: A signed integer representing the number of times to + * apply step to the current value. A positive value will increment, + * while a negative one will decrement. + * step: An unsigned integer specified by the webauthor and indicating the + * amount by which to increment/decrement the current value. + */ +- (void)changeValueBySteps:(int)factor { + MOZ_ASSERT(mGeckoAccessible, "mGeckoAccessible is null"); + + double newValue = + mGeckoAccessible->CurValue() + (mGeckoAccessible->Step() * factor); + [self setValue:(newValue)]; +} + +/* + * Updates the accessible's current value to the specified value + */ +- (void)setValue:(double)value { + MOZ_ASSERT(mGeckoAccessible, "mGeckoAccessible is null"); + + double min = mGeckoAccessible->MinValue(); + double max = mGeckoAccessible->MaxValue(); + + if ((IsNaN(min) || value >= min) && (IsNaN(max) || value <= max)) { + if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) { + acc->SetCurValue(value); + } else { + RemoteAccessible* proxy = mGeckoAccessible->AsRemote(); + proxy->SetCurValue(value); + } + } +} + +@end diff --git a/accessible/mac/mozHTMLAccessible.h b/accessible/mac/mozHTMLAccessible.h new file mode 100644 index 0000000000..48fd4b0bdc --- /dev/null +++ b/accessible/mac/mozHTMLAccessible.h @@ -0,0 +1,44 @@ +/* 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; + +// override +- (NSArray*)moxLinkedUIElements; + +@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..5465f578a8 --- /dev/null +++ b/accessible/mac/mozHTMLAccessible.mm @@ -0,0 +1,86 @@ +/* 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 "LocalAccessible-inl.h" +#import "HyperTextAccessible.h" + +#import "nsCocoaUtils.h" + +using namespace mozilla::a11y; + +@implementation mozHeadingAccessible + +- (NSString*)moxTitle { + nsAutoString title; + if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) { + mozilla::ErrorResult rv; + // XXX use the flattening API when there are available + // see bug 768298 + acc->GetContent()->GetTextContent(title, rv); + } else if (RemoteAccessible* proxy = mGeckoAccessible->AsRemote()) { + proxy->Title(title); + } + + title.CompressWhitespace(); + + return nsCocoaUtils::ToNSString(title); +} + +- (id)moxValue { + GroupPos groupPos = mGeckoAccessible->GroupPosition(); + + return [NSNumber numberWithInt:groupPos.level]; +} + +@end + +@implementation mozLinkAccessible + +- (NSString*)moxValue { + return @""; +} + +- (NSURL*)moxURL { + nsAutoString value; + mGeckoAccessible->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]; +} + +- (NSArray*)moxLinkedUIElements { + return [self getRelationsByType:RelationType::LINKS_TO]; +} + +@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..929eca01dd --- /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::Accessible*)aAcc; + +#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..3f171ada8c --- /dev/null +++ b/accessible/mac/mozRootAccessible.mm @@ -0,0 +1,84 @@ +/* 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" + +#include "gfxPlatform.h" +// This must be included last: +#include "nsObjCExceptions.h" + +using namespace mozilla::a11y; + +static id<mozAccessible, mozView> getNativeViewFromRootAccessible( + LocalAccessible* 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::Accessible*)aAcc { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + MOZ_ASSERT(!aAcc->IsRemote(), "mozRootAccessible is never a proxy"); + + mParallelView = getNativeViewFromRootAccessible(aAcc->AsLocal()); + + return [super initWithAccessible:aAcc]; + + NS_OBJC_END_TRY_BLOCK_RETURN(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_BLOCK_RETURN; + + // If there is no represented view (eg. headless), this will return nil. + return [[self representedView] + accessibilityAttributeValue:NSAccessibilityParentAttribute]; + + NS_OBJC_END_TRY_BLOCK_RETURN(nil); +} + +- (BOOL)hasRepresentedView { + return YES; +} + +// this will return our parallell NSView. see mozDocAccessible.h +- (id)representedView { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + MOZ_ASSERT(mParallelView || gfxPlatform::IsHeadless(), + "root accessible does not have a native parallel view."); + + return mParallelView; + + NS_OBJC_END_TRY_BLOCK_RETURN(nil); +} + +- (BOOL)isRoot { + return YES; +} + +@end diff --git a/accessible/mac/mozSelectableElements.h b/accessible/mac/mozSelectableElements.h new file mode 100644 index 0000000000..77c8c30aed --- /dev/null +++ b/accessible/mac/mozSelectableElements.h @@ -0,0 +1,128 @@ +/* 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; + +// override +- (void)moxPerformPress; + +@end diff --git a/accessible/mac/mozSelectableElements.mm b/accessible/mac/mozSelectableElements.mm new file mode 100644 index 0000000000..693514e405 --- /dev/null +++ b/accessible/mac/mozSelectableElements.mm @@ -0,0 +1,336 @@ +/* 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 "LocalAccessible-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; + } + + mGeckoAccessible->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 (LocalAccessible* acc = [child geckoAccessible]->AsLocal()) { + 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. + Accessible* parentAcc = [parent geckoAccessible]; + if (parentAcc) { + Accessible* 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 { + LocalAccessible* acc = mGeckoAccessible->AsLocal(); + 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->LocalFirstChild()->Role() == roles::STATICTEXT) { + nsAutoString marker; + acc->LocalFirstChild()->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: + [self invalidateState]; + // Our focused state is equivelent to native selected states for menus. + mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent]; + [parent moxPostNotification: + NSAccessibilitySelectedChildrenChangedNotification]; + break; + } + + [super handleAccessibleEvent:eventType]; +} + +- (void)moxPerformPress { + [super moxPerformPress]; + // when a menu item is pressed (chosen), we need to tell + // VoiceOver about it, so we send this notification + [self moxPostNotification:@"AXMenuItemSelected"]; +} + +@end diff --git a/accessible/mac/mozTableAccessible.h b/accessible/mac/mozTableAccessible.h new file mode 100644 index 0000000000..a54614eaca --- /dev/null +++ b/accessible/mac/mozTableAccessible.h @@ -0,0 +1,186 @@ +/* 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; + +// override +- (void)handleAccessibleEvent:(uint32_t)eventType; + +- (BOOL)isLayoutTablePart; + +- (void)invalidateLayoutTableCache; + +@end + +@interface mozTableAccessible : mozTablePartAccessible { + NSMutableArray* mColContainers; + uint32_t mIsLayoutTable; +} + +// local override +- (BOOL)isLayoutTablePart; + +// local override +- (void)invalidateLayoutTableCache; + +- (void)invalidateColumns; + +// override +- (void)handleAccessibleEvent:(uint32_t)eventType; + +// override +- (void)dealloc; + +// override +- (void)expire; + +// 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 + +// local override +- (BOOL)isLayoutTablePart; + +// 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 +- (id)moxValue; + +// 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..48cd8ba11f --- /dev/null +++ b/accessible/mac/mozTableAccessible.mm @@ -0,0 +1,739 @@ +/* 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 "LocalAccessible.h" +#include "mozilla/a11y/TableAccessibleBase.h" +#include "mozilla/a11y/TableCellAccessibleBase.h" +#include "mozilla/StaticPrefs_accessibility.h" +#include "XULTreeAccessible.h" +#include "Pivot.h" +#include "Relation.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +enum CachedBool { eCachedBoolMiss, eCachedTrue, eCachedFalse }; + +@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 (StaticPrefs::accessibility_cache_enabled_AtStartup() || + [mParent geckoAccessible]->IsLocal()) { + TableAccessibleBase* table = [mParent geckoAccessible]->AsTableBase(); + 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 (RemoteAccessible* proxy = [mParent geckoAccessible]->AsRemote()) { + uint32_t numRows = proxy->TableRowCount(); + + for (uint32_t j = 0; j < numRows; j++) { + RemoteAccessible* 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_IGNORE_BLOCK; + + [self invalidateChildren]; + [super dealloc]; + + NS_OBJC_END_TRY_IGNORE_BLOCK; +} + +- (void)expire { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + [self invalidateChildren]; + + mParent = nil; + + [super expire]; + + NS_OBJC_END_TRY_IGNORE_BLOCK; +} + +- (BOOL)isExpired { + MOZ_ASSERT((mChildren == nil && mParent == nil) == mIsExpired); + + return [super isExpired]; +} + +- (void)invalidateChildren { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + // make room for new children + if (mChildren) { + [mChildren release]; + mChildren = nil; + } + + NS_OBJC_END_TRY_IGNORE_BLOCK; +} + +@end + +@implementation mozTablePartAccessible + +- (NSString*)moxTitle { + return @""; +} + +- (NSString*)moxRole { + return [self isLayoutTablePart] ? NSAccessibilityGroupRole : [super moxRole]; +} + +- (void)handleAccessibleEvent:(uint32_t)eventType { + if (![self isKindOfClass:[mozTableAccessible class]] && + !StaticPrefs::accessibility_cache_enabled_AtStartup()) { + // If we are not a table, we are a cell or a row. + // Check to see if the event we're handling should + // invalidate the mIsLayoutTable cache on our parent + // table. Only do this when the core cache is off, because + // we don't use the platform cache when its on. + if (eventType == nsIAccessibleEvent::EVENT_REORDER || + eventType == nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED) { + // Invalidate the cache on our parent table + [self invalidateLayoutTableCache]; + } + } + + [super handleAccessibleEvent:eventType]; +} + +- (BOOL)isLayoutTablePart { + // mIsLayoutTable is a cache on each mozTableAccessible that stores + // the previous result of calling IsProbablyLayoutTable in core. To see + // how/when the cache is invalidated, view handleAccessibleEvent. + // The cache contains one of three values from the CachedBool enum + // defined in mozTableAccessible.h + mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent]; + if ([parent isKindOfClass:[mozTablePartAccessible class]]) { + return [(mozTablePartAccessible*)parent isLayoutTablePart]; + } else if ([parent isKindOfClass:[mozOutlineAccessible class]]) { + return [(mozOutlineAccessible*)parent isLayoutTablePart]; + } + + return NO; +} + +- (void)invalidateLayoutTableCache { + mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent]; + if ([parent isKindOfClass:[mozTablePartAccessible class]]) { + // We do this to prevent dispatching invalidateLayoutTableCache + // on outlines or outline parts. This is possible here because + // outline rows subclass table rows, which are a table part. + // This means `parent` could be an outline, and there is no + // cache on outlines to invalidate. + [(mozTablePartAccessible*)parent invalidateLayoutTableCache]; + } +} +@end + +@implementation mozTableAccessible + +- (void)invalidateLayoutTableCache { + MOZ_ASSERT(!StaticPrefs::accessibility_cache_enabled_AtStartup(), + "If the core cache is enabled we shouldn't be maintaining the " + "platform table cache!"); + mIsLayoutTable = eCachedBoolMiss; +} + +- (BOOL)isLayoutTablePart { + if (mIsLayoutTable != eCachedBoolMiss && + !StaticPrefs::accessibility_cache_enabled_AtStartup()) { + // Only use the platform cache if the core cache is not on + return mIsLayoutTable == eCachedTrue; + } + + if (mGeckoAccessible->Role() == roles::TREE_TABLE) { + // tree tables are never layout tables, and we shouldn't + // query IsProbablyLayoutTable() on them, so we short + // circuit here + mIsLayoutTable = eCachedFalse; + return false; + } + + bool tableGuess; + // For LocalAccessible and cached RemoteAccessible, we could use + // AsTableBase()->IsProbablyLayoutTable(). However, if the cache is enabled, + // that would build the table cache, which is pointless for layout tables on + // Mac because layout tables are AXGroups and do not expose table properties + // like AXRows, AXColumns, etc. + if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) { + tableGuess = acc->AsTable()->IsProbablyLayoutTable(); + } else { + RemoteAccessible* proxy = mGeckoAccessible->AsRemote(); + tableGuess = proxy->TableIsProbablyForLayout(); + } + + mIsLayoutTable = tableGuess ? eCachedTrue : eCachedFalse; + return tableGuess; +} + +- (void)handleAccessibleEvent:(uint32_t)eventType { + if (eventType == nsIAccessibleEvent::EVENT_REORDER || + eventType == nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED || + eventType == nsIAccessibleEvent::EVENT_TABLE_STYLING_CHANGED) { + if (!StaticPrefs::accessibility_cache_enabled_AtStartup()) { + [self invalidateLayoutTableCache]; + } + [self invalidateColumns]; + } + + [super handleAccessibleEvent:eventType]; +} + +- (void)dealloc { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + [self invalidateColumns]; + [super dealloc]; + + NS_OBJC_END_TRY_IGNORE_BLOCK; +} + +- (void)expire { + [self invalidateColumns]; + [super expire]; +} + +- (NSNumber*)moxRowCount { + MOZ_ASSERT(mGeckoAccessible); + + return (StaticPrefs::accessibility_cache_enabled_AtStartup() || + mGeckoAccessible->IsLocal()) + ? @(mGeckoAccessible->AsTableBase()->RowCount()) + : @(mGeckoAccessible->AsRemote()->TableRowCount()); +} + +- (NSNumber*)moxColumnCount { + MOZ_ASSERT(mGeckoAccessible); + + return (StaticPrefs::accessibility_cache_enabled_AtStartup() || + mGeckoAccessible->IsLocal()) + ? @(mGeckoAccessible->AsTableBase()->ColCount()) + : @(mGeckoAccessible->AsRemote()->TableColumnCount()); +} + +- (NSArray*)moxRows { + // Create a new array with the list of table rows. + NSArray* children = [self moxChildren]; + NSMutableArray* rows = [[[NSMutableArray alloc] init] autorelease]; + for (mozAccessible* curr : children) { + if ([curr isKindOfClass:[mozTableRowAccessible class]]) { + [rows addObject:curr]; + } else if ([[curr moxRole] isEqualToString:@"AXGroup"]) { + // Plain thead/tbody elements are removed from the core a11y tree and + // replaced with their subtree, but thead/tbody elements with click + // handlers are not -- they remain as groups. We need to expose any + // rows they contain as rows of the parent table. + [rows + addObjectsFromArray:[[curr moxChildren] + filteredArrayUsingPredicate: + [NSPredicate predicateWithBlock:^BOOL( + mozAccessible* child, + NSDictionary* bindings) { + return [child + isKindOfClass:[mozTableRowAccessible + class]]; + }]]]; + } + } + + return rows; +} + +- (NSArray*)moxColumns { + MOZ_ASSERT(mGeckoAccessible); + + if (mColContainers) { + return mColContainers; + } + + mColContainers = [[NSMutableArray alloc] init]; + uint32_t numCols = 0; + + if (StaticPrefs::accessibility_cache_enabled_AtStartup() || + mGeckoAccessible->IsLocal()) { + numCols = mGeckoAccessible->AsTableBase()->ColCount(); + } else { + numCols = mGeckoAccessible->AsRemote()->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); + + uint32_t numCols = 0; + TableAccessibleBase* table = nullptr; + + if (StaticPrefs::accessibility_cache_enabled_AtStartup() || + mGeckoAccessible->IsLocal()) { + table = mGeckoAccessible->AsTableBase(); + numCols = table->ColCount(); + } else { + numCols = mGeckoAccessible->AsRemote()->TableColumnCount(); + } + + NSMutableArray* colHeaders = + [[[NSMutableArray alloc] initWithCapacity:numCols] autorelease]; + + for (uint32_t i = 0; i < numCols; i++) { + Accessible* cell; + if (table) { + cell = table->CellAt(0, i); + } else { + cell = mGeckoAccessible->AsRemote()->TableCellAt(0, i); + } + + if (cell && 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); + + Accessible* cell; + if (StaticPrefs::accessibility_cache_enabled_AtStartup() || + mGeckoAccessible->IsLocal()) { + cell = mGeckoAccessible->AsTableBase()->CellAt(row, col); + } else { + cell = mGeckoAccessible->AsRemote()->TableCellAt(row, col); + } + + if (!cell) { + return nil; + } + + return GetNativeFromGeckoAccessible(cell); +} + +- (void)invalidateColumns { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + if (mColContainers) { + for (mozColumnContainer* col in mColContainers) { + [col expire]; + } + [mColContainers release]; + mColContainers = nil; + } + NS_OBJC_END_TRY_IGNORE_BLOCK; +} + +@end + +@interface mozTableRowAccessible () +- (mozTableAccessible*)getTableParent; +@end + +@implementation mozTableRowAccessible + +- (mozTableAccessible*)getTableParent { + id tableParent = static_cast<mozTableAccessible*>( + [self moxFindAncestor:^BOOL(id curr, BOOL* stop) { + if ([curr isKindOfClass:[mozOutlineAccessible class]]) { + // Outline rows are a kind of table row, so it's possible + // we're trying to call getTableParent on an outline row here. + // Stop searching. + *stop = YES; + } + return [curr isKindOfClass:[mozTableAccessible class]]; + }]); + + return [tableParent isKindOfClass:[mozTableAccessible class]] ? tableParent + : nil; +} + +- (void)handleAccessibleEvent:(uint32_t)eventType { + if (eventType == nsIAccessibleEvent::EVENT_REORDER) { + // It is possible for getTableParent to return nil if we're + // handling a reorder on an outilne row. Outlines don't have + // columns, so there's nothing to do here and this will no-op. + [[self getTableParent] invalidateColumns]; + } + + [super handleAccessibleEvent:eventType]; +} + +- (NSNumber*)moxIndex { + return @([[[self getTableParent] moxRows] indexOfObjectIdenticalTo:self]); +} + +@end + +@implementation mozTableCellAccessible + +- (NSValue*)moxRowIndexRange { + MOZ_ASSERT(mGeckoAccessible); + + if (StaticPrefs::accessibility_cache_enabled_AtStartup() || + mGeckoAccessible->IsLocal()) { + TableCellAccessibleBase* cell = mGeckoAccessible->AsTableCellBase(); + return + [NSValue valueWithRange:NSMakeRange(cell->RowIdx(), cell->RowExtent())]; + } else { + RemoteAccessible* proxy = mGeckoAccessible->AsRemote(); + return [NSValue + valueWithRange:NSMakeRange(proxy->RowIdx(), proxy->RowExtent())]; + } +} + +- (NSValue*)moxColumnIndexRange { + MOZ_ASSERT(mGeckoAccessible); + + if (StaticPrefs::accessibility_cache_enabled_AtStartup() || + mGeckoAccessible->IsLocal()) { + TableCellAccessibleBase* cell = mGeckoAccessible->AsTableCellBase(); + return + [NSValue valueWithRange:NSMakeRange(cell->ColIdx(), cell->ColExtent())]; + } else { + RemoteAccessible* proxy = mGeckoAccessible->AsRemote(); + return [NSValue + valueWithRange:NSMakeRange(proxy->ColIdx(), proxy->ColExtent())]; + } +} + +- (NSArray*)moxRowHeaderUIElements { + MOZ_ASSERT(mGeckoAccessible); + + if (StaticPrefs::accessibility_cache_enabled_AtStartup() || + mGeckoAccessible->IsLocal()) { + TableCellAccessibleBase* cell = mGeckoAccessible->AsTableCellBase(); + AutoTArray<Accessible*, 10> headerCells; + cell->RowHeaderCells(&headerCells); + return utils::ConvertToNSArray(headerCells); + } else { + RemoteAccessible* proxy = mGeckoAccessible->AsRemote(); + nsTArray<RemoteAccessible*> headerCells; + proxy->RowHeaderCells(&headerCells); + return utils::ConvertToNSArray(headerCells); + } +} + +- (NSArray*)moxColumnHeaderUIElements { + MOZ_ASSERT(mGeckoAccessible); + + if (StaticPrefs::accessibility_cache_enabled_AtStartup() || + mGeckoAccessible->IsLocal()) { + TableCellAccessibleBase* cell = mGeckoAccessible->AsTableCellBase(); + AutoTArray<Accessible*, 10> headerCells; + cell->ColHeaderCells(&headerCells); + return utils::ConvertToNSArray(headerCells); + } else { + RemoteAccessible* proxy = mGeckoAccessible->AsRemote(); + nsTArray<RemoteAccessible*> headerCells; + proxy->ColHeaderCells(&headerCells); + return utils::ConvertToNSArray(headerCells); + } +} + +@end + +@implementation mozOutlineAccessible + +- (BOOL)isLayoutTablePart { + return NO; +} + +- (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] autorelease]; + Pivot p = Pivot(mGeckoAccessible); + OutlineRule rule = OutlineRule(); + Accessible* firstChild = mGeckoAccessible->FirstChild(); + Accessible* match = p.Next(firstChild, rule, true); + while (match) { + [allRows addObject:GetNativeFromGeckoAccessible(match)]; + match = p.Next(match, rule); + } + return allRows; +} + +- (NSArray*)moxColumns { + if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) { + if (acc->IsContent() && acc->GetContent()->IsXULElement(nsGkAtoms::tree)) { + XULTreeAccessible* treeAcc = (XULTreeAccessible*)acc; + NSMutableArray* cols = [[[NSMutableArray alloc] init] autorelease]; + // 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. + LocalAccessible* treeColumns = treeAcc->LocalChildAt(0); + if (treeColumns) { + uint32_t colCount = treeColumns->ChildCount(); + for (uint32_t i = 0; i < colCount; i++) { + LocalAccessible* treeColumnItem = treeColumns->LocalChildAt(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] autorelease]; + 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 = mGeckoAccessible->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 { + id<MOXAccessible> outline = + [self moxFindAncestor:^BOOL(id<MOXAccessible> moxAcc, BOOL* stop) { + return [[moxAcc moxRole] isEqualToString:@"AXOutline"]; + }]; + + NSUInteger index = [[outline moxRows] indexOfObjectIdenticalTo:self]; + return index == NSNotFound ? nil : @(index); +} + +- (NSString*)moxLabel { + nsAutoString title; + mGeckoAccessible->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); +} + +enum CheckedState { + kUncheckable = -1, + kUnchecked = 0, + kChecked = 1, + kMixed = 2 +}; + +- (int)checkedValue { + uint64_t state = [self + stateWithMask:(states::CHECKABLE | states::CHECKED | states::MIXED)]; + + if (state & states::CHECKABLE) { + if (state & states::CHECKED) { + return kChecked; + } + + if (state & states::MIXED) { + return kMixed; + } + + return kUnchecked; + } + + return kUncheckable; +} + +- (id)moxValue { + int checkedValue = [self checkedValue]; + return checkedValue >= 0 ? @(checkedValue) : nil; +} + +- (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)]; + } + + if (state & (states::CHECKED | states::CHECKABLE | states::MIXED)) { + // If the MIXED, CHECKED or CHECKABLE state changes, update the value we + // expose for the row, which communicates checked status. + [self moxPostNotification:NSAccessibilityValueChangedNotification]; + } +} + +@end diff --git a/accessible/mac/mozTextAccessible.h b/accessible/mac/mozTextAccessible.h new file mode 100644 index 0000000000..b242a2da32 --- /dev/null +++ b/accessible/mac/mozTextAccessible.h @@ -0,0 +1,114 @@ +/* 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:(mozilla::a11y::Accessible*)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 +- (BOOL)moxIgnoreWithParent:(mozAccessible*)parent; + +// 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..c3c2f9bcd8 --- /dev/null +++ b/accessible/mac/mozTextAccessible.mm @@ -0,0 +1,446 @@ +/* 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 "AccAttributes.h" +#include "HyperTextAccessible-inl.h" +#include "LocalAccessible-inl.h" +#include "mozilla/a11y/PDocAccessible.h" +#include "nsCocoaUtils.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. + RefPtr<AccAttributes> attributes; + HyperTextAccessibleBase* text = mGeckoAccessible->AsHyperTextBase(); + if (text && mGeckoAccessible->IsTextRole()) { + attributes = text->DefaultTextAttributes(); + } + + nsAutoString invalidStr; + if (!attributes || + !attributes->GetAttribute(nsGkAtoms::invalid, invalidStr)) { + return @"true"; + } + return nsCocoaUtils::ToNSString(invalidStr); + } + + // If the flag is not set, we return false. + return @"false"; +} + +- (NSNumber*)moxInsertionPointLineNumber { + MOZ_ASSERT(mGeckoAccessible); + + int32_t lineNumber = -1; + if (mGeckoAccessible->IsLocal()) { + if (HyperTextAccessible* textAcc = + mGeckoAccessible->AsLocal()->AsHyperText()) { + lineNumber = textAcc->CaretLineNumber() - 1; + } + } else { + lineNumber = mGeckoAccessible->AsRemote()->CaretLineNumber() - 1; + } + + return (lineNumber >= 0) ? [NSNumber numberWithInt:lineNumber] : nil; +} + +- (NSString*)moxRole { + if ([self stateWithMask:states::MULTI_LINE]) { + return NSAccessibilityTextAreaRole; + } + + return [super moxRole]; +} + +- (NSString*)moxSubrole { + MOZ_ASSERT(mGeckoAccessible); + + if (mRole == roles::PASSWORD_TEXT) { + return NSAccessibilitySecureTextFieldSubrole; + } + + if (mRole == roles::ENTRY) { + LocalAccessible* acc = mGeckoAccessible->AsLocal(); + RemoteAccessible* proxy = mGeckoAccessible->AsRemote(); + 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); + + nsString text; + nsCocoaUtils::GetStringForNSString(value, text); + if (mGeckoAccessible->IsLocal()) { + if (HyperTextAccessible* textAcc = + mGeckoAccessible->AsLocal()->AsHyperText()) { + textAcc->ReplaceText(text); + } + } else { + mGeckoAccessible->AsRemote()->ReplaceText(text); + } +} + +- (void)moxSetSelectedText:(NSString*)selectedText { + MOZ_ASSERT(mGeckoAccessible); + + NSString* stringValue = ToNSString(selectedText); + if (!stringValue) { + return; + } + + int32_t start = 0, end = 0; + nsString text; + if (mGeckoAccessible->IsLocal()) { + if (HyperTextAccessible* textAcc = + mGeckoAccessible->AsLocal()->AsHyperText()) { + textAcc->SelectionBoundsAt(0, &start, &end); + textAcc->DeleteText(start, end - start); + nsCocoaUtils::GetStringForNSString(stringValue, text); + textAcc->InsertText(text, start); + } + } else { + RemoteAccessible* proxy = mGeckoAccessible->AsRemote(); + 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]; + + if (markerRange.IsValid()) { + markerRange.Select(); + } +} + +- (void)moxSetVisibleCharacterRange:(NSValue*)visibleCharacterRange { + MOZ_ASSERT(mGeckoAccessible); + + NSRange range; + if (!ToNSRange(visibleCharacterRange, &range)) { + return; + } + + if (mGeckoAccessible->IsLocal()) { + if (HyperTextAccessible* textAcc = + mGeckoAccessible->AsLocal()->AsHyperText()) { + textAcc->ScrollSubstringTo(range.location, range.location + range.length, + nsIAccessibleScrollType::SCROLL_TYPE_TOP_EDGE); + } + } else { + mGeckoAccessible->AsRemote()->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 { + GeckoTextMarkerRange markerRange = [self textMarkerRangeFromRange:range]; + + if (!markerRange.IsValid()) { + return nil; + } + + return markerRange.AttributedText(); +} + +- (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:(Accessible*)container + at:(int32_t)start { + GeckoTextMarker startMarker(container, start); + NSDictionary* userInfo = @{ + @"AXTextChangeElement" : self, + @"AXTextStateChangeType" : @(AXTextStateChangeTypeEdit), + @"AXTextChangeValues" : @[ @{ + @"AXTextChangeValue" : (change ? change : @""), + @"AXTextChangeValueStartMarker" : + (__bridge id)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); + + 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 { + NSString* val = [super moxTitle]; + return [val length] ? val : nil; +} + +- (NSString*)moxTitle { + return nil; +} + +- (NSString*)moxLabel { + return nil; +} + +- (BOOL)moxIgnoreWithParent:(mozAccessible*)parent { + // Don't render text nodes that are completely empty + // or those that should be ignored based on our + // standard ignore rules + return [self moxValue] == nil || [super moxIgnoreWithParent:parent]; +} + +- (NSString*)moxStringForRange:(NSValue*)range { + MOZ_ASSERT(mGeckoAccessible); + + 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 { + MOZ_ASSERT(mGeckoAccessible); + + NSRange r = [range rangeValue]; + GeckoTextMarkerRange textMarkerRange(mGeckoAccessible); + textMarkerRange.mStart.mOffset += r.location; + textMarkerRange.mEnd.mOffset = + textMarkerRange.mStart.mOffset + r.location + r.length; + + return textMarkerRange.AttributedText(); +} + +- (NSValue*)moxBoundsForRange:(NSValue*)range { + MOZ_ASSERT(mGeckoAccessible); + + NSRange r = [range rangeValue]; + GeckoTextMarkerRange textMarkerRange(mGeckoAccessible); + + textMarkerRange.mStart.mOffset += r.location; + textMarkerRange.mEnd.mOffset = textMarkerRange.mStart.mOffset + r.length; + + return textMarkerRange.Bounds(); +} + +@end |