diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /accessible/mac | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'accessible/mac')
46 files changed, 9221 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..eb78b9417f --- /dev/null +++ b/accessible/mac/AccessibleWrap.h @@ -0,0 +1,95 @@ +/* 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; + + 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..ef2f4ba779 --- /dev/null +++ b/accessible/mac/AccessibleWrap.mm @@ -0,0 +1,281 @@ +/* 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 "mozilla/a11y/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(); + } + + return NS_OK; + + NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE); +} + +//////////////////////////////////////////////////////////////////////////////// +// 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::DATE_EDITOR: + return [mozDatePickerAccessible 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::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..9b21251e1e --- /dev/null +++ b/accessible/mac/DocAccessibleWrap.mm @@ -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/. */ + +#include "DocAccessibleWrap.h" +#include "ARIAMap.h" +#include "DocAccessible-inl.h" +#include "nsAccUtils.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..0e8f660694 --- /dev/null +++ b/accessible/mac/GeckoTextMarker.h @@ -0,0 +1,138 @@ +/* clang-format off */ +/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* clang-format on */ +/* 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 "TextLeafRange.h" + +namespace mozilla { +namespace a11y { + +class Accessible; +class GeckoTextMarkerRange; + +class GeckoTextMarker final { + public: + GeckoTextMarker(Accessible* aAcc, int32_t aOffset); + + explicit GeckoTextMarker(const TextLeafPoint& aTextLeafPoint) + : mPoint(aTextLeafPoint) {} + + GeckoTextMarker() : mPoint() {} + + static GeckoTextMarker MarkerFromAXTextMarker(Accessible* aDoc, + AXTextMarkerRef aTextMarker); + + static GeckoTextMarker MarkerFromIndex(Accessible* aRoot, int32_t aIndex); + + AXTextMarkerRef CreateAXTextMarker(); + + bool Next(); + + bool Previous(); + + GeckoTextMarkerRange LeftWordRange() const; + + GeckoTextMarkerRange RightWordRange() const; + + GeckoTextMarkerRange LineRange() const; + + GeckoTextMarkerRange LeftLineRange() const; + + GeckoTextMarkerRange RightLineRange() const; + + GeckoTextMarkerRange ParagraphRange() const; + + GeckoTextMarkerRange StyleRange() const; + + int32_t& Offset() { return mPoint.mOffset; } + + Accessible* Leaf(); + + Accessible* Acc() const { return mPoint.mAcc; } + + bool IsValid() const { return !!mPoint; }; + + bool operator<(const GeckoTextMarker& aOther) const { + return mPoint < aOther.mPoint; + } + + bool operator==(const GeckoTextMarker& aOther) const { + return mPoint == aOther.mPoint; + } + + TextLeafPoint mPoint; +}; + +class GeckoTextMarkerRange final { + public: + GeckoTextMarkerRange(const GeckoTextMarker& aStart, + const GeckoTextMarker& aEnd) + : mRange(aStart.mPoint, aEnd.mPoint) {} + + GeckoTextMarkerRange(const TextLeafPoint& aStart, const TextLeafPoint& aEnd) + : mRange(aStart, aEnd) {} + + GeckoTextMarkerRange() {} + + explicit GeckoTextMarkerRange(Accessible* aAccessible); + + static GeckoTextMarkerRange MarkerRangeFromAXTextMarkerRange( + Accessible* aDoc, AXTextMarkerRangeRef aTextMarkerRange); + + AXTextMarkerRangeRef CreateAXTextMarkerRange(); + + bool IsValid() const { return !!mRange.Start() && !!mRange.End(); }; + + GeckoTextMarker Start() { return GeckoTextMarker(mRange.Start()); } + + GeckoTextMarker End() { return GeckoTextMarker(mRange.End()); } + + /** + * 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) { return mRange.Crop(aContainer); } + + TextLeafRange mRange; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/mac/GeckoTextMarker.mm b/accessible/mac/GeckoTextMarker.mm new file mode 100644 index 0000000000..ba3a6e6231 --- /dev/null +++ b/accessible/mac/GeckoTextMarker.mm @@ -0,0 +1,514 @@ +/* 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" + +#import "MacUtils.h" + +#include "AccAttributes.h" +#include "DocAccessible.h" +#include "DocAccessibleParent.h" +#include "nsCocoaUtils.h" +#include "HyperTextAccessible.h" +#include "States.h" +#include "nsAccUtils.h" + +namespace mozilla { +namespace a11y { + +struct TextMarkerData { + TextMarkerData(uintptr_t aDoc, uintptr_t aID, int32_t aOffset) + : mDoc(aDoc), mID(aID), mOffset(aOffset) {} + TextMarkerData() {} + uintptr_t mDoc; + uintptr_t mID; + int32_t mOffset; +}; + +// GeckoTextMarker + +GeckoTextMarker::GeckoTextMarker(Accessible* aAcc, int32_t aOffset) { + HyperTextAccessibleBase* ht = aAcc->AsHyperTextBase(); + if (ht && aOffset != nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT && + aOffset <= static_cast<int32_t>(ht->CharacterCount())) { + mPoint = aAcc->AsHyperTextBase()->ToTextLeafPoint(aOffset); + } else { + mPoint = TextLeafPoint(aAcc, aOffset); + } +} + +GeckoTextMarker GeckoTextMarker::MarkerFromAXTextMarker( + Accessible* aDoc, AXTextMarkerRef aTextMarker) { + MOZ_ASSERT(aDoc); + if (!aTextMarker) { + return GeckoTextMarker(); + } + + if (AXTextMarkerGetLength(aTextMarker) != sizeof(TextMarkerData)) { + MOZ_ASSERT_UNREACHABLE("Malformed AXTextMarkerRef"); + return GeckoTextMarker(); + } + + TextMarkerData markerData; + memcpy(&markerData, AXTextMarkerGetBytePtr(aTextMarker), + sizeof(TextMarkerData)); + + if (!utils::DocumentExists(aDoc, markerData.mDoc)) { + return GeckoTextMarker(); + } + + Accessible* doc = reinterpret_cast<Accessible*>(markerData.mDoc); + MOZ_ASSERT(doc->IsDoc()); + int32_t offset = markerData.mOffset; + Accessible* acc = nullptr; + if (doc->IsRemote()) { + acc = doc->AsRemote()->AsDoc()->GetAccessible(markerData.mID); + } else { + acc = doc->AsLocal()->AsDoc()->GetAccessibleByUniqueID( + reinterpret_cast<void*>(markerData.mID)); + } + + if (!acc) { + return GeckoTextMarker(); + } + + return GeckoTextMarker(acc, offset); +} + +GeckoTextMarker GeckoTextMarker::MarkerFromIndex(Accessible* aRoot, + int32_t aIndex) { + TextLeafRange range( + TextLeafPoint(aRoot, 0), + TextLeafPoint(aRoot, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT)); + int32_t index = aIndex; + // Iterate through all segments until we exhausted the index sum + // so we can find the segment the index lives in. + for (TextLeafRange segment : range) { + if (segment.End().mAcc->Role() == roles::LISTITEM_MARKER) { + // XXX: MacOS expects bullets to be in the range's text, but not in + // the calculated length! + continue; + } + + index -= segment.End().mOffset - segment.Start().mOffset; + if (index <= 0) { + // The index is in the current segment. + return GeckoTextMarker(segment.Start().mAcc, + segment.End().mOffset + index); + } + } + + return GeckoTextMarker(); +} + +AXTextMarkerRef GeckoTextMarker::CreateAXTextMarker() { + if (!IsValid()) { + return nil; + } + + Accessible* doc = nsAccUtils::DocumentFor(mPoint.mAcc); + TextMarkerData markerData(reinterpret_cast<uintptr_t>(doc), mPoint.mAcc->ID(), + mPoint.mOffset); + AXTextMarkerRef cf_text_marker = AXTextMarkerCreate( + kCFAllocatorDefault, reinterpret_cast<const UInt8*>(&markerData), + sizeof(TextMarkerData)); + + return (__bridge AXTextMarkerRef)[(__bridge id)(cf_text_marker)autorelease]; +} + +bool GeckoTextMarker::Next() { + TextLeafPoint next = + mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirNext, + TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker); + + if (next && next != mPoint) { + mPoint = next; + return true; + } + + return false; +} + +bool GeckoTextMarker::Previous() { + TextLeafPoint prev = + mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious, + TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker); + if (prev && mPoint != prev) { + mPoint = prev; + return true; + } + + return false; +} + +/** + * Return true if the given point is inside editable content. + */ +static bool IsPointInEditable(const TextLeafPoint& aPoint) { + if (aPoint.mAcc) { + if (aPoint.mAcc->State() & states::EDITABLE) { + return true; + } + + Accessible* parent = aPoint.mAcc->Parent(); + if (parent && (parent->State() & states::EDITABLE)) { + return true; + } + } + + return false; +} + +GeckoTextMarkerRange GeckoTextMarker::LeftWordRange() const { + bool includeCurrentInStart = !mPoint.IsParagraphStart(true); + if (includeCurrentInStart) { + TextLeafPoint prevChar = + mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious); + if (!prevChar.IsSpace()) { + includeCurrentInStart = false; + } + } + + TextLeafPoint start = mPoint.FindBoundary( + nsIAccessibleText::BOUNDARY_WORD_START, eDirPrevious, + includeCurrentInStart + ? (TextLeafPoint::BoundaryFlags::eIncludeOrigin | + TextLeafPoint::BoundaryFlags::eStopInEditable | + TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker) + : (TextLeafPoint::BoundaryFlags::eStopInEditable | + TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker)); + + TextLeafPoint end; + if (start == mPoint) { + end = start.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_END, eDirPrevious, + TextLeafPoint::BoundaryFlags::eStopInEditable); + } + + if (start != mPoint || end == start) { + end = start.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_END, eDirNext, + TextLeafPoint::BoundaryFlags::eStopInEditable); + if (end < mPoint && IsPointInEditable(end) && !IsPointInEditable(mPoint)) { + start = end; + end = mPoint; + } + } + + return GeckoTextMarkerRange(start < end ? start : end, + start < end ? end : start); +} + +GeckoTextMarkerRange GeckoTextMarker::RightWordRange() const { + TextLeafPoint prevChar = + mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious, + TextLeafPoint::BoundaryFlags::eStopInEditable); + + if (prevChar != mPoint && mPoint.IsParagraphStart(true)) { + return GeckoTextMarkerRange(mPoint, mPoint); + } + + TextLeafPoint end = + mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_END, eDirNext, + TextLeafPoint::BoundaryFlags::eStopInEditable); + + if (end == mPoint) { + // No word to the right of this point. + return GeckoTextMarkerRange(mPoint, mPoint); + } + + TextLeafPoint start = + end.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_START, eDirPrevious, + TextLeafPoint::BoundaryFlags::eStopInEditable); + + if (start.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_END, eDirNext, + TextLeafPoint::BoundaryFlags::eStopInEditable) < + mPoint) { + // Word end is inside of an input to the left of this. + return GeckoTextMarkerRange(mPoint, mPoint); + } + + if (mPoint < start) { + end = start; + start = mPoint; + } + + return GeckoTextMarkerRange(start < end ? start : end, + start < end ? end : start); +} + +GeckoTextMarkerRange GeckoTextMarker::LineRange() const { + TextLeafPoint start = mPoint.FindBoundary( + nsIAccessibleText::BOUNDARY_LINE_START, eDirPrevious, + TextLeafPoint::BoundaryFlags::eStopInEditable | + TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker | + TextLeafPoint::BoundaryFlags::eIncludeOrigin); + // If this is a blank line containing only a line feed, the start boundary + // is the same as the end boundary. We do not want to walk to the end of the + // next line. + TextLeafPoint end = + start.IsLineFeedChar() + ? start + : start.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_END, eDirNext, + TextLeafPoint::BoundaryFlags::eStopInEditable); + + return GeckoTextMarkerRange(start, end); +} + +GeckoTextMarkerRange GeckoTextMarker::LeftLineRange() const { + TextLeafPoint start = mPoint.FindBoundary( + nsIAccessibleText::BOUNDARY_LINE_START, eDirPrevious, + TextLeafPoint::BoundaryFlags::eStopInEditable | + TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker); + TextLeafPoint end = + start.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_END, eDirNext, + TextLeafPoint::BoundaryFlags::eStopInEditable); + + return GeckoTextMarkerRange(start, end); +} + +GeckoTextMarkerRange GeckoTextMarker::RightLineRange() const { + TextLeafPoint end = + mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_END, eDirNext, + TextLeafPoint::BoundaryFlags::eStopInEditable); + TextLeafPoint start = + end.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_START, eDirPrevious, + TextLeafPoint::BoundaryFlags::eStopInEditable); + + return GeckoTextMarkerRange(start, end); +} + +GeckoTextMarkerRange GeckoTextMarker::ParagraphRange() const { + // XXX: WebKit gets trapped in inputs. Maybe we shouldn't? + TextLeafPoint end = + mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_PARAGRAPH, eDirNext, + TextLeafPoint::BoundaryFlags::eStopInEditable); + TextLeafPoint start = + end.FindBoundary(nsIAccessibleText::BOUNDARY_PARAGRAPH, eDirPrevious, + TextLeafPoint::BoundaryFlags::eStopInEditable); + + return GeckoTextMarkerRange(start, end); +} + +GeckoTextMarkerRange GeckoTextMarker::StyleRange() const { + if (mPoint.mOffset == 0) { + // If the marker is on the boundary between two leafs, MacOS expects the + // previous leaf. + TextLeafPoint prev = mPoint.FindBoundary( + nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious, + TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker); + if (prev != mPoint) { + return GeckoTextMarker(prev).StyleRange(); + } + } + + TextLeafPoint start(mPoint.mAcc, 0); + TextLeafPoint end(mPoint.mAcc, nsAccUtils::TextLength(mPoint.mAcc)); + return GeckoTextMarkerRange(start, end); +} + +Accessible* GeckoTextMarker::Leaf() { + MOZ_ASSERT(mPoint.mAcc); + Accessible* acc = mPoint.mAcc; + if (mPoint.mOffset == 0) { + // If the marker is on the boundary between two leafs, MacOS expects the + // previous leaf. + TextLeafPoint prev = mPoint.FindBoundary( + nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious, + TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker); + acc = prev.mAcc; + } + + Accessible* parent = acc->Parent(); + return parent && nsAccUtils::MustPrune(parent) ? parent : acc; +} + +// GeckoTextMarkerRange + +GeckoTextMarkerRange::GeckoTextMarkerRange(Accessible* aAccessible) { + mRange = TextLeafRange( + TextLeafPoint(aAccessible, 0), + TextLeafPoint(aAccessible, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT)); +} + +GeckoTextMarkerRange GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange( + Accessible* aDoc, AXTextMarkerRangeRef aTextMarkerRange) { + if (!aTextMarkerRange || + CFGetTypeID(aTextMarkerRange) != AXTextMarkerRangeGetTypeID()) { + return GeckoTextMarkerRange(); + } + + AXTextMarkerRef start_marker( + AXTextMarkerRangeCopyStartMarker(aTextMarkerRange)); + AXTextMarkerRef end_marker(AXTextMarkerRangeCopyEndMarker(aTextMarkerRange)); + + GeckoTextMarker start = + GeckoTextMarker::MarkerFromAXTextMarker(aDoc, start_marker); + GeckoTextMarker end = + GeckoTextMarker::MarkerFromAXTextMarker(aDoc, end_marker); + + CFRelease(start_marker); + CFRelease(end_marker); + + return GeckoTextMarkerRange(start, end); +} + +AXTextMarkerRangeRef GeckoTextMarkerRange::CreateAXTextMarkerRange() { + if (!IsValid()) { + return nil; + } + + GeckoTextMarker start = GeckoTextMarker(mRange.Start()); + GeckoTextMarker end = GeckoTextMarker(mRange.End()); + + AXTextMarkerRangeRef cf_text_marker_range = + AXTextMarkerRangeCreate(kCFAllocatorDefault, start.CreateAXTextMarker(), + end.CreateAXTextMarker()); + + return (__bridge AXTextMarkerRangeRef)[(__bridge id)( + cf_text_marker_range)autorelease]; +} + +NSString* GeckoTextMarkerRange::Text() const { + if (mRange.Start() == mRange.End()) { + return @""; + } + + if ((mRange.Start().mAcc == mRange.End().mAcc) && + (mRange.Start().mAcc->ChildCount() == 0) && + (mRange.Start().mAcc->State() & states::EDITABLE)) { + return @""; + } + + nsAutoString text; + TextLeafPoint prev = mRange.Start().FindBoundary( + nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious); + TextLeafRange range = + prev != mRange.Start() && prev.mAcc->Role() == roles::LISTITEM_MARKER + ? TextLeafRange(TextLeafPoint(prev.mAcc, 0), mRange.End()) + : mRange; + + for (TextLeafRange segment : range) { + TextLeafPoint start = segment.Start(); + if (start.mAcc->IsTextField() && start.mAcc->ChildCount() == 0) { + continue; + } + + start.mAcc->AppendTextTo(text, start.mOffset, + segment.End().mOffset - start.mOffset); + } + + return nsCocoaUtils::ToNSString(text); +} + +static void AppendTextToAttributedString( + NSMutableAttributedString* aAttributedString, Accessible* aAccessible, + const nsString& aString, AccAttributes* aAttributes) { + NSAttributedString* substr = [[[NSAttributedString alloc] + initWithString:nsCocoaUtils::ToNSString(aString) + attributes:utils::StringAttributesFromAccAttributes( + aAttributes, aAccessible)] autorelease]; + + [aAttributedString appendAttributedString:substr]; +} + +NSAttributedString* GeckoTextMarkerRange::AttributedText() const { + NSMutableAttributedString* str = + [[[NSMutableAttributedString alloc] init] autorelease]; + + if (mRange.Start() == mRange.End()) { + return str; + } + + if ((mRange.Start().mAcc == mRange.End().mAcc) && + (mRange.Start().mAcc->ChildCount() == 0) && + (mRange.Start().mAcc->IsTextField())) { + return str; + } + + TextLeafPoint prev = mRange.Start().FindBoundary( + nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious); + TextLeafRange range = + prev != mRange.Start() && prev.mAcc->Role() == roles::LISTITEM_MARKER + ? TextLeafRange(TextLeafPoint(prev.mAcc, 0), mRange.End()) + : mRange; + + nsAutoString text; + RefPtr<AccAttributes> currentRun = range.Start().GetTextAttributes(); + Accessible* runAcc = range.Start().mAcc; + for (TextLeafRange segment : range) { + TextLeafPoint start = segment.Start(); + TextLeafPoint attributesNext; + do { + if (start.mAcc->IsText()) { + attributesNext = start.FindTextAttrsStart(eDirNext, false); + } else { + // If this segment isn't a text leaf, but another kind of inline element + // like a control, just consider this full segment one "attributes run". + attributesNext = segment.End(); + } + if (attributesNext == start) { + // XXX: FindTextAttrsStart should not return the same point. + break; + } + RefPtr<AccAttributes> attributes = start.GetTextAttributes(); + if (!currentRun || !attributes || !attributes->Equal(currentRun)) { + // If currentRun is null this is a non-text control and we will + // append a run with no text or attributes, just an AXAttachment + // referencing this accessible. + AppendTextToAttributedString(str, runAcc, text, currentRun); + text.Truncate(); + currentRun = attributes; + runAcc = start.mAcc; + } + TextLeafPoint end = + attributesNext < segment.End() ? attributesNext : segment.End(); + start.mAcc->AppendTextTo(text, start.mOffset, + end.mOffset - start.mOffset); + start = attributesNext; + + } while (attributesNext < segment.End()); + } + + if (!text.IsEmpty()) { + AppendTextToAttributedString(str, runAcc, text, currentRun); + } + + return str; +} + +int32_t GeckoTextMarkerRange::Length() const { + int32_t length = 0; + for (TextLeafRange segment : mRange) { + if (segment.End().mAcc->Role() == roles::LISTITEM_MARKER) { + // XXX: MacOS expects bullets to be in the range's text, but not in + // the calculated length! + continue; + } + length += segment.End().mOffset - segment.Start().mOffset; + } + + return length; +} + +NSValue* GeckoTextMarkerRange::Bounds() const { + LayoutDeviceIntRect rect = mRange ? mRange.Bounds() : LayoutDeviceIntRect(); + + 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 { mRange.SetSelection(0); } + +} // namespace a11y +} // namespace mozilla 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..fe7c367f8c --- /dev/null +++ b/accessible/mac/MOXAccessibleBase.mm @@ -0,0 +1,584 @@ +/* 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" +#include "gfxPlatform.h" + +using namespace mozilla; +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<mozAccessible> nativeParent = [self moxParent]; + if (!nativeParent) { + return nil; + } + + if (![nativeParent isAccessibilityElement]) { + if ([nativeParent conformsToProtocol:@protocol(MOXAccessible)] && + [nativeParent respondsToSelector:@selector(moxUnignoredParent)]) { + // Cast away the protocol so we can cast to another protocol. + id bareNativeParent = nativeParent; + id<MOXAccessible> moxNativeParent = bareNativeParent; + return [moxNativeParent 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 || ![element respondsToSelector:@selector(moxUnignoredParent)]) { + 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..e3535c1628 --- /dev/null +++ b/accessible/mac/MOXAccessibleProtocol.h @@ -0,0 +1,538 @@ +/* clang-format off */ +/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* clang-format on */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +@protocol MOXTextMarkerSupport; +@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; + +// AXARIAPosInSet +- (NSNumber* _Nullable)moxARIAPosInSet; + +// AXARIASetSize +- (NSNumber* _Nullable)moxARIASetSize; + +// 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..ce7a7dedf3 --- /dev/null +++ b/accessible/mac/MOXSearchInfo.mm @@ -0,0 +1,374 @@ +/* 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" +#include "nsAccessibilityService.h" + +using namespace mozilla::a11y; + +@interface MOXSearchInfo () +- (NSArray*)getMatchesForRule:(PivotRule&)rule; + +- (Accessible*)rootGeckoAccessible; + +- (Accessible*)startGeckoAccessible; +@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 { + int resultLimit = 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 matches; +} + +- (NSArray*)performSearch { + Accessible* geckoRootAcc = [self rootGeckoAccessible]; + Accessible* geckoStartAcc = [self startGeckoAccessible]; + NSMutableArray* matches = [[[NSMutableArray alloc] init] autorelease]; + nsString searchText; + nsCocoaUtils::GetStringForNSString(mSearchText, searchText); + for (id key in mSearchKeys) { + if ([key isEqualToString:@"AXAnyTypeSearchKey"]) { + RotorRule rule = mImmediateDescendantsOnly + ? RotorRule(geckoRootAcc, searchText) + : RotorRule(searchText); + + if (searchText.IsEmpty() && + [mStartElem isKindOfClass:[MOXWebAreaAccessible class]]) { + // Don't include the root group when a search text is defined. + 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, searchText) + : RotorRoleRule(roles::HEADING, searchText); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXArticleSearchKey"]) { + RotorRoleRule rule = + mImmediateDescendantsOnly + ? RotorRoleRule(roles::ARTICLE, geckoRootAcc, searchText) + : RotorRoleRule(roles::ARTICLE, searchText); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXTableSearchKey"]) { + RotorRoleRule rule = + mImmediateDescendantsOnly + ? RotorRoleRule(roles::TABLE, geckoRootAcc, searchText) + : RotorRoleRule(roles::TABLE, searchText); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXLandmarkSearchKey"]) { + RotorRoleRule rule = + mImmediateDescendantsOnly + ? RotorRoleRule(roles::LANDMARK, geckoRootAcc, searchText) + : RotorRoleRule(roles::LANDMARK, searchText); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXListSearchKey"]) { + RotorRoleRule rule = + mImmediateDescendantsOnly + ? RotorRoleRule(roles::LIST, geckoRootAcc, searchText) + : RotorRoleRule(roles::LIST, searchText); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXLinkSearchKey"]) { + RotorLinkRule rule = mImmediateDescendantsOnly + ? RotorLinkRule(geckoRootAcc, searchText) + : RotorLinkRule(searchText); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXVisitedLinkSearchKey"]) { + RotorVisitedLinkRule rule = + mImmediateDescendantsOnly + ? RotorVisitedLinkRule(geckoRootAcc, searchText) + : RotorVisitedLinkRule(searchText); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXUnvisitedLinkSearchKey"]) { + RotorUnvisitedLinkRule rule = + mImmediateDescendantsOnly + ? RotorUnvisitedLinkRule(geckoRootAcc, searchText) + : RotorUnvisitedLinkRule(searchText); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXButtonSearchKey"]) { + RotorRoleRule rule = + mImmediateDescendantsOnly + ? RotorRoleRule(roles::PUSHBUTTON, geckoRootAcc, searchText) + : RotorRoleRule(roles::PUSHBUTTON, searchText); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXControlSearchKey"]) { + RotorControlRule rule = mImmediateDescendantsOnly + ? RotorControlRule(geckoRootAcc, searchText) + : RotorControlRule(searchText); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXSameTypeSearchKey"]) { + mozAccessible* native = GetNativeFromGeckoAccessible(geckoStartAcc); + NSString* macRole = [native moxRole]; + RotorMacRoleRule rule = + mImmediateDescendantsOnly + ? RotorMacRoleRule(macRole, geckoRootAcc, searchText) + : RotorMacRoleRule(macRole, searchText); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXDifferentTypeSearchKey"]) { + mozAccessible* native = GetNativeFromGeckoAccessible(geckoStartAcc); + NSString* macRole = [native moxRole]; + RotorNotMacRoleRule rule = + mImmediateDescendantsOnly + ? RotorNotMacRoleRule(macRole, geckoRootAcc, searchText) + : RotorNotMacRoleRule(macRole, searchText); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXRadioGroupSearchKey"]) { + RotorRoleRule rule = + mImmediateDescendantsOnly + ? RotorRoleRule(roles::RADIO_GROUP, geckoRootAcc, searchText) + : RotorRoleRule(roles::RADIO_GROUP, searchText); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXFrameSearchKey"]) { + RotorRoleRule rule = + mImmediateDescendantsOnly + ? RotorRoleRule(roles::DOCUMENT, geckoRootAcc, searchText) + : RotorRoleRule(roles::DOCUMENT, searchText); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXImageSearchKey"] || + [key isEqualToString:@"AXGraphicSearchKey"]) { + RotorRoleRule rule = + mImmediateDescendantsOnly + ? RotorRoleRule(roles::GRAPHIC, geckoRootAcc, searchText) + : RotorRoleRule(roles::GRAPHIC, searchText); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXCheckBoxSearchKey"]) { + RotorRoleRule rule = + mImmediateDescendantsOnly + ? RotorRoleRule(roles::CHECKBUTTON, geckoRootAcc, searchText) + : RotorRoleRule(roles::CHECKBUTTON, searchText); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXStaticTextSearchKey"]) { + RotorStaticTextRule rule = + mImmediateDescendantsOnly + ? RotorStaticTextRule(geckoRootAcc, searchText) + : RotorStaticTextRule(searchText); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXHeadingLevel1SearchKey"]) { + RotorHeadingLevelRule rule = + mImmediateDescendantsOnly + ? RotorHeadingLevelRule(1, geckoRootAcc, searchText) + : RotorHeadingLevelRule(1, searchText); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXHeadingLevel2SearchKey"]) { + RotorHeadingLevelRule rule = + mImmediateDescendantsOnly + ? RotorHeadingLevelRule(2, geckoRootAcc, searchText) + : RotorHeadingLevelRule(2, searchText); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXHeadingLevel3SearchKey"]) { + RotorHeadingLevelRule rule = + mImmediateDescendantsOnly + ? RotorHeadingLevelRule(3, geckoRootAcc, searchText) + : RotorHeadingLevelRule(3, searchText); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXHeadingLevel4SearchKey"]) { + RotorHeadingLevelRule rule = + mImmediateDescendantsOnly + ? RotorHeadingLevelRule(4, geckoRootAcc, searchText) + : RotorHeadingLevelRule(4, searchText); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXHeadingLevel5SearchKey"]) { + RotorHeadingLevelRule rule = + mImmediateDescendantsOnly + ? RotorHeadingLevelRule(5, geckoRootAcc, searchText) + : RotorHeadingLevelRule(5, searchText); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXHeadingLevel6SearchKey"]) { + RotorHeadingLevelRule rule = + mImmediateDescendantsOnly + ? RotorHeadingLevelRule(6, geckoRootAcc, searchText) + : RotorHeadingLevelRule(6, searchText); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXBlockquoteSearchKey"]) { + RotorRoleRule rule = + mImmediateDescendantsOnly + ? RotorRoleRule(roles::BLOCKQUOTE, geckoRootAcc, searchText) + : RotorRoleRule(roles::BLOCKQUOTE, searchText); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXTextFieldSearchKey"]) { + RotorTextEntryRule rule = + mImmediateDescendantsOnly + ? RotorTextEntryRule(geckoRootAcc, searchText) + : RotorTextEntryRule(searchText); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } + + if ([key isEqualToString:@"AXLiveRegionSearchKey"]) { + RotorLiveRegionRule rule = + mImmediateDescendantsOnly + ? RotorLiveRegionRule(geckoRootAcc, searchText) + : RotorLiveRegionRule(searchText); + [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..f1d40f6ffa --- /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 +}; +} // namespace a11y +} // namespace mozilla diff --git a/accessible/mac/MOXTextMarkerDelegate.mm b/accessible/mac/MOXTextMarkerDelegate.mm new file mode 100644 index 0000000000..3e1e451ddd --- /dev/null +++ b/accessible/mac/MOXTextMarkerDelegate.mm @@ -0,0 +1,527 @@ +/* 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); +} + +mozAccessible* GetEditableNativeFromGeckoAccessible(Accessible* aAcc) { + // The gecko accessible may not have a native accessible so we need + // to walk up the parent chain to find the nearest one. + // This happens when caching is enabled and the text marker's accessible + // may be a text leaf that is pruned from the platform. + for (Accessible* acc = aAcc; acc; acc = acc->Parent()) { + if (mozAccessible* mozAcc = GetNativeFromGeckoAccessible(acc)) { + return [mozAcc moxEditableAncestor]; + } + } + + return nil; +} + +// 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::MarkerRangeFromAXTextMarkerRange( + mGeckoDocAccessible, mSelection); + + int32_t stateChangeType = + selectedGeckoRange.Start() == selectedGeckoRange.End() + ? 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 = + GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, mCaret); + GeckoTextMarker prevCaretMarker = + GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, mPrevCaret); + + if (!caretMarker.IsValid()) { + // If the current caret is invalid, stop here and return base info. + return info; + } + + mozAccessible* caretEditable = + GetEditableNativeFromGeckoAccessible(caretMarker.Acc()); + + 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 = + GetEditableNativeFromGeckoAccessible(prevCaretMarker.Acc()); + + 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::MarkerRangeFromAXTextMarkerRange( + mGeckoDocAccessible, mSelection); +} + +- (AXTextMarkerRef)moxStartTextMarker { + GeckoTextMarker geckoTextPoint(mGeckoDocAccessible, 0); + return geckoTextPoint.CreateAXTextMarker(); +} + +- (AXTextMarkerRef)moxEndTextMarker { + GeckoTextMarker geckoTextPoint(mGeckoDocAccessible, + nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT); + return geckoTextPoint.CreateAXTextMarker(); +} + +- (AXTextMarkerRangeRef)moxSelectedTextMarkerRange { + return mSelection && GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange( + mGeckoDocAccessible, mSelection) + .IsValid() + ? mSelection + : nil; +} + +- (NSString*)moxStringForTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange { + mozilla::a11y::GeckoTextMarkerRange range = + GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange( + mGeckoDocAccessible, textMarkerRange); + if (!range.IsValid()) { + return @""; + } + + return range.Text(); +} + +- (NSNumber*)moxLengthForTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange { + mozilla::a11y::GeckoTextMarkerRange range = + GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange( + mGeckoDocAccessible, textMarkerRange); + if (!range.IsValid()) { + return @0; + } + + return @(range.Length()); +} + +- (AXTextMarkerRangeRef)moxTextMarkerRangeForUnorderedTextMarkers: + (NSArray*)textMarkers { + if ([textMarkers count] != 2) { + // Don't allow anything but a two member array. + return nil; + } + + GeckoTextMarker p1 = GeckoTextMarker::MarkerFromAXTextMarker( + mGeckoDocAccessible, (__bridge AXTextMarkerRef)textMarkers[0]); + GeckoTextMarker p2 = GeckoTextMarker::MarkerFromAXTextMarker( + 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 = + GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange( + mGeckoDocAccessible, textMarkerRange); + + return range.IsValid() ? range.Start().CreateAXTextMarker() : nil; +} + +- (AXTextMarkerRef)moxEndTextMarkerForTextMarkerRange: + (AXTextMarkerRangeRef)textMarkerRange { + mozilla::a11y::GeckoTextMarkerRange range = + GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange( + mGeckoDocAccessible, textMarkerRange); + + return range.IsValid() ? range.End().CreateAXTextMarker() : nil; +} + +- (AXTextMarkerRangeRef)moxLeftWordTextMarkerRangeForTextMarker: + (AXTextMarkerRef)textMarker { + GeckoTextMarker geckoTextMarker = + GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker); + if (!geckoTextMarker.IsValid()) { + return nil; + } + + return geckoTextMarker.LeftWordRange().CreateAXTextMarkerRange(); +} + +- (AXTextMarkerRangeRef)moxRightWordTextMarkerRangeForTextMarker: + (AXTextMarkerRef)textMarker { + GeckoTextMarker geckoTextMarker = + GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker); + if (!geckoTextMarker.IsValid()) { + return nil; + } + + return geckoTextMarker.RightWordRange().CreateAXTextMarkerRange(); +} + +- (AXTextMarkerRangeRef)moxLineTextMarkerRangeForTextMarker: + (AXTextMarkerRef)textMarker { + GeckoTextMarker geckoTextMarker = + GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker); + if (!geckoTextMarker.IsValid()) { + return nil; + } + + return geckoTextMarker.LineRange().CreateAXTextMarkerRange(); +} + +- (AXTextMarkerRangeRef)moxLeftLineTextMarkerRangeForTextMarker: + (AXTextMarkerRef)textMarker { + GeckoTextMarker geckoTextMarker = + GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker); + if (!geckoTextMarker.IsValid()) { + return nil; + } + + return geckoTextMarker.LeftLineRange().CreateAXTextMarkerRange(); +} + +- (AXTextMarkerRangeRef)moxRightLineTextMarkerRangeForTextMarker: + (AXTextMarkerRef)textMarker { + GeckoTextMarker geckoTextMarker = + GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker); + if (!geckoTextMarker.IsValid()) { + return nil; + } + + return geckoTextMarker.RightLineRange().CreateAXTextMarkerRange(); +} + +- (AXTextMarkerRangeRef)moxParagraphTextMarkerRangeForTextMarker: + (AXTextMarkerRef)textMarker { + GeckoTextMarker geckoTextMarker = + GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker); + if (!geckoTextMarker.IsValid()) { + return nil; + } + + return geckoTextMarker.ParagraphRange().CreateAXTextMarkerRange(); +} + +// override +- (AXTextMarkerRangeRef)moxStyleTextMarkerRangeForTextMarker: + (AXTextMarkerRef)textMarker { + GeckoTextMarker geckoTextMarker = + GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker); + if (!geckoTextMarker.IsValid()) { + return nil; + } + + return geckoTextMarker.StyleRange().CreateAXTextMarkerRange(); +} + +- (AXTextMarkerRef)moxNextTextMarkerForTextMarker:(AXTextMarkerRef)textMarker { + GeckoTextMarker geckoTextMarker = + GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker); + if (!geckoTextMarker.IsValid()) { + return nil; + } + + if (!geckoTextMarker.Next()) { + return nil; + } + + return geckoTextMarker.CreateAXTextMarker(); +} + +- (AXTextMarkerRef)moxPreviousTextMarkerForTextMarker: + (AXTextMarkerRef)textMarker { + GeckoTextMarker geckoTextMarker = + GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker); + if (!geckoTextMarker.IsValid()) { + return nil; + } + + if (!geckoTextMarker.Previous()) { + return nil; + } + + return geckoTextMarker.CreateAXTextMarker(); +} + +- (NSAttributedString*)moxAttributedStringForTextMarkerRange: + (AXTextMarkerRangeRef)textMarkerRange { + mozilla::a11y::GeckoTextMarkerRange range = + GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange( + mGeckoDocAccessible, textMarkerRange); + if (!range.IsValid()) { + return nil; + } + + return range.AttributedText(); +} + +- (NSValue*)moxBoundsForTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange { + mozilla::a11y::GeckoTextMarkerRange range = + GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange( + mGeckoDocAccessible, textMarkerRange); + if (!range.IsValid()) { + return nil; + } + + return range.Bounds(); +} + +- (NSNumber*)moxIndexForTextMarker:(AXTextMarkerRef)textMarker { + GeckoTextMarker geckoTextMarker = + GeckoTextMarker::MarkerFromAXTextMarker(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 = + GeckoTextMarker::MarkerFromAXTextMarker(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 = + GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker); + if (!geckoTextMarker.IsValid()) { + return @"<GeckoTextMarker 0x0 [0]>"; + } + + return [NSString stringWithFormat:@"<GeckoTextMarker %p [%d]>", + geckoTextMarker.Acc(), + geckoTextMarker.Offset()]; +} + +- (NSString*)moxMozDebugDescriptionForTextMarkerRange: + (AXTextMarkerRangeRef)textMarkerRange { + if (!mozilla::Preferences::GetBool(PREF_ACCESSIBILITY_MAC_DEBUG)) { + return nil; + } + + mozilla::a11y::GeckoTextMarkerRange range = + GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange( + mGeckoDocAccessible, textMarkerRange); + if (!range.IsValid()) { + return @"<GeckoTextMarkerRange 0x0 [0] - 0x0 [0]>"; + } + + return [NSString stringWithFormat:@"<GeckoTextMarkerRange %p [%d] - %p [%d]>", + range.Start().Acc(), range.Start().Offset(), + range.End().Acc(), range.End().Offset()]; +} + +- (void)moxSetSelectedTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange { + mozilla::a11y::GeckoTextMarkerRange range = + GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange( + 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..c1ae585fa1 --- /dev/null +++ b/accessible/mac/MOXWebAreaAccessible.mm @@ -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/. */ + +#import "MOXWebAreaAccessible.h" + +#import "MOXSearchInfo.h" +#import "MacUtils.h" + +#include "nsAccUtils.h" +#include "nsCocoaUtils.h" +#include "DocAccessible.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..33ee5d0a19 --- /dev/null +++ b/accessible/mac/MacUtils.h @@ -0,0 +1,62 @@ +/* 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" +#include "mozAccessible.h" +#include "MOXAccessibleBase.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); + +/** + * Return true if the passed raw pointer is a live document accessible. Uses + * the provided root doc accessible to check for current documents. + */ +bool DocumentExists(Accessible* aDoc, uintptr_t aDocPtr); + +NSDictionary* StringAttributesFromAccAttributes(AccAttributes* aAttributes, + Accessible* aContainer); +} // namespace utils +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/mac/MacUtils.mm b/accessible/mac/MacUtils.mm new file mode 100644 index 0000000000..5534e8fcc8 --- /dev/null +++ b/accessible/mac/MacUtils.mm @@ -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 "MacUtils.h" +#include "mozAccessible.h" + +#include "LocalAccessible.h" +#include "DocAccessible.h" +#include "DocAccessibleParent.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; +} + +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; +} + +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]; +} + +NSDictionary* StringAttributesFromAccAttributes(AccAttributes* aAttributes, + Accessible* aContainer) { + if (!aAttributes) { + if (mozAccessible* mozAcc = GetNativeFromGeckoAccessible(aContainer)) { + // If we don't have attributes provided this is probably a control like + // a button or empty entry. Just provide the accessible as an + // AXAttachment. + return @{@"AXAttachment" : mozAcc}; + } + return @{}; + } + + 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; +} +} // namespace utils +} // namespace a11y +} // namespace mozilla diff --git a/accessible/mac/Platform.mm b/accessible/mac/Platform.mm new file mode 100644 index 0000000000..eb507adefb --- /dev/null +++ b/accessible/mac/Platform.mm @@ -0,0 +1,268 @@ +/* clang-format off */ +/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* clang-format on */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#import <Cocoa/Cocoa.h> + +#import "MOXTextMarkerDelegate.h" + +#include "Platform.h" +#include "RemoteAccessible.h" +#include "DocAccessibleParent.h" +#include "mozTableAccessible.h" +#include "mozTextAccessible.h" +#include "MOXWebAreaAccessible.h" +#include "nsAccUtils.h" +#include "TextRange.h" + +#include "nsAppShell.h" +#include "nsCocoaUtils.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 PlatformEvent(Accessible* aTarget, uint32_t aEventType) { + // Ignore event that we don't escape below, they aren't yet supported. + if (aEventType != nsIAccessibleEvent::EVENT_ALERT && + aEventType != nsIAccessibleEvent::EVENT_VALUE_CHANGE && + aEventType != nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE && + aEventType != nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE && + aEventType != nsIAccessibleEvent::EVENT_REORDER && + aEventType != nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED && + aEventType != nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED && + aEventType != nsIAccessibleEvent::EVENT_NAME_CHANGE && + aEventType != nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED) { + return; + } + + mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget); + if (wrapper) { + [wrapper handleAccessibleEvent:aEventType]; + } +} + +void PlatformStateChangeEvent(Accessible* aTarget, uint64_t aState, + bool aEnabled) { + mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget); + if (wrapper) { + [wrapper stateChanged:aState isEnabled:aEnabled]; + } +} + +void PlatformFocusEvent(Accessible* aTarget, + const LayoutDeviceIntRect& aCaretRect) { + if (mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget)) { + [wrapper handleAccessibleEvent:nsIAccessibleEvent::EVENT_FOCUS]; + } +} + +void PlatformCaretMoveEvent(Accessible* aTarget, int32_t aOffset, + bool aIsSelectionCollapsed, int32_t aGranularity, + const LayoutDeviceIntRect& aCaretRect, + bool aFromUser) { + mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget); + MOXTextMarkerDelegate* delegate = [MOXTextMarkerDelegate + getOrCreateForDoc:nsAccUtils::DocumentFor(aTarget)]; + [delegate setCaretOffset:aTarget at:aOffset moveGranularity:aGranularity]; + if (aIsSelectionCollapsed) { + // If selection is collapsed, invalidate selection. + [delegate setSelectionFrom:aTarget at:aOffset to:aTarget at:aOffset]; + } + + if (wrapper) { + if (mozTextAccessible* textAcc = + static_cast<mozTextAccessible*>([wrapper moxEditableAncestor])) { + [textAcc + handleAccessibleEvent:nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED]; + } else { + [wrapper + handleAccessibleEvent:nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED]; + } + } +} + +void PlatformTextChangeEvent(Accessible* aTarget, const nsAString& aStr, + int32_t aStart, uint32_t aLen, bool aIsInsert, + bool aFromUser) { + Accessible* acc = aTarget; + // If there is a text input ancestor, use it as the event source. + while (acc && GetTypeFromRole(acc->Role()) != [mozTextAccessible class]) { + acc = acc->Parent(); + } + mozAccessible* wrapper = GetNativeFromGeckoAccessible(acc ? acc : aTarget); + [wrapper handleAccessibleTextChangeEvent:nsCocoaUtils::ToNSString(aStr) + inserted:aIsInsert + inContainer:aTarget + at:aStart]; +} + +void PlatformShowHideEvent(Accessible*, Accessible*, bool, bool) {} + +void PlatformSelectionEvent(Accessible* aTarget, Accessible* aWidget, + uint32_t aEventType) { + mozAccessible* wrapper = GetNativeFromGeckoAccessible(aWidget); + if (wrapper) { + [wrapper handleAccessibleEvent:aEventType]; + } +} + +void PlatformTextSelectionChangeEvent(Accessible* aTarget, + const nsTArray<TextRange>& aSelection) { + if (aSelection.Length()) { + MOXTextMarkerDelegate* delegate = [MOXTextMarkerDelegate + getOrCreateForDoc:nsAccUtils::DocumentFor(aTarget)]; + // Cache the selection. + [delegate setSelectionFrom:aSelection[0].StartContainer() + at:aSelection[0].StartOffset() + to:aSelection[0].EndContainer() + at:aSelection[0].EndOffset()]; + } + + mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget); + if (wrapper) { + [wrapper + handleAccessibleEvent:nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED]; + } +} + +void PlatformRoleChangedEvent(Accessible* aTarget, const a11y::role& aRole, + uint8_t aRoleMapEntryIndex) { + if (mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget)) { + [wrapper handleRoleChanged:aRole]; + } +} + +} // namespace a11y +} // namespace mozilla + +@interface GeckoNSApplication (a11y) +- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute; +@end + +@implementation GeckoNSApplication (a11y) + +- (NSAccessibilityRole)accessibilityRole { + // For ATs that don't request `AXEnhancedUserInterface` we need to enable + // accessibility when a role is fetched. Not ideal, but this is needed + // for such services as Voice Control. + if (!mozilla::a11y::sA11yShouldBeEnabled) { + [self accessibilitySetValue:@YES forAttribute:@"AXEnhancedUserInterface"]; + } + return [super accessibilityRole]; +} + +- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute { + if ([attribute isEqualToString:@"AXEnhancedUserInterface"]) { + mozilla::a11y::sA11yShouldBeEnabled = ([value intValue] == 1); + if (sA11yShouldBeEnabled) { + // If accessibility should be enabled, log the appropriate client + 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 { + val = CFPreferencesGetAppIntegerValue( + CFSTR("CommandAndControlEnabled"), + CFSTR("com.apple.Accessibility"), &exists); + if (exists && val == 1) { + client.Assign(u"VoiceControl"_ns); + } else { + client.Assign(u"Unknown"_ns); + } + } + } + +#if defined(MOZ_TELEMETRY_REPORTING) + mozilla::Telemetry::ScalarSet( + mozilla::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..8d861ed12e --- /dev/null +++ b/accessible/mac/PlatformExtTypes.h @@ -0,0 +1,25 @@ +/* -*- 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 +}; + +} // 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..7ccb191cb1 --- /dev/null +++ b/accessible/mac/RotorRules.h @@ -0,0 +1,144 @@ +/* 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, + const nsString& aSearchText); + explicit RotorRule(const nsString& aSearchText); + uint16_t Match(Accessible* aAcc) override; + + private: + Accessible* mDirectDescendantsFrom; + const nsString& mSearchText; +}; + +/** + * This rule matches all accessibles of a given role. + */ +class RotorRoleRule : public RotorRule { + public: + explicit RotorRoleRule(role aRole, Accessible* aDirectDescendantsFrom, + const nsString& aSearchText); + explicit RotorRoleRule(role aRole, const nsString& aSearchText); + uint16_t Match(Accessible* aAcc) override; + + private: + role mRole; +}; + +class RotorMacRoleRule : public RotorRule { + public: + explicit RotorMacRoleRule(NSString* aRole, const nsString& aSearchText); + explicit RotorMacRoleRule(NSString* aRole, Accessible* aDirectDescendantsFrom, + const nsString& aSearchText); + ~RotorMacRoleRule(); + virtual uint16_t Match(Accessible* aAcc) override; + + protected: + NSString* mMacRole; +}; + +class RotorControlRule final : public RotorRule { + public: + explicit RotorControlRule(Accessible* aDirectDescendantsFrom, + const nsString& aSearchText); + explicit RotorControlRule(const nsString& aSearchText); + + virtual uint16_t Match(Accessible* aAcc) override; +}; + +class RotorTextEntryRule final : public RotorRule { + public: + explicit RotorTextEntryRule(Accessible* aDirectDescendantsFrom, + const nsString& aSearchText); + explicit RotorTextEntryRule(const nsString& aSearchText); + + virtual uint16_t Match(Accessible* aAcc) override; +}; + +class RotorLinkRule : public RotorRule { + public: + explicit RotorLinkRule(const nsString& aSearchText); + explicit RotorLinkRule(Accessible* aDirectDescendantsFrom, + const nsString& aSearchText); + + virtual uint16_t Match(Accessible* aAcc) override; +}; + +class RotorVisitedLinkRule final : public RotorLinkRule { + public: + explicit RotorVisitedLinkRule(const nsString& aSearchText); + explicit RotorVisitedLinkRule(Accessible* aDirectDescendantsFrom, + const nsString& aSearchText); + + virtual uint16_t Match(Accessible* aAcc) override; +}; + +class RotorUnvisitedLinkRule final : public RotorLinkRule { + public: + explicit RotorUnvisitedLinkRule(const nsString& aSearchText); + explicit RotorUnvisitedLinkRule(Accessible* aDirectDescendantsFrom, + const nsString& aSearchText); + + 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, + const nsString& aSearchText); + explicit RotorNotMacRoleRule(NSString* aMacRole, const nsString& aSearchText); + uint16_t Match(Accessible* aAcc) override; +}; + +class RotorStaticTextRule : public RotorRule { + public: + explicit RotorStaticTextRule(const nsString& aSearchText); + explicit RotorStaticTextRule(Accessible* aDirectDescendantsFrom, + const nsString& aSearchText); + + virtual uint16_t Match(Accessible* aAcc) override; +}; + +class RotorHeadingLevelRule : public RotorRoleRule { + public: + explicit RotorHeadingLevelRule(int32_t aLevel, const nsString& aSearchText); + explicit RotorHeadingLevelRule(int32_t aLevel, + Accessible* aDirectDescendantsFrom, + const nsString& aSearchText); + + virtual uint16_t Match(Accessible* aAcc) override; + + private: + int32_t mLevel; +}; + +class RotorLiveRegionRule : public RotorRule { + public: + explicit RotorLiveRegionRule(Accessible* aDirectDescendantsFrom, + const nsString& aSearchText) + : RotorRule(aDirectDescendantsFrom, aSearchText) {} + explicit RotorLiveRegionRule(const nsString& aSearchText) + : RotorRule(aSearchText) {} + + uint16_t Match(Accessible* aAcc) override; +}; diff --git a/accessible/mac/RotorRules.mm b/accessible/mac/RotorRules.mm new file mode 100644 index 0000000000..07f8479161 --- /dev/null +++ b/accessible/mac/RotorRules.mm @@ -0,0 +1,390 @@ +/* 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" +#include "nsIAccessiblePivot.h" +#include "nsAccUtils.h" + +#include "nsAccessibilityService.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +// Generic Rotor Rule + +RotorRule::RotorRule(Accessible* aDirectDescendantsFrom, + const nsString& aSearchText) + : mDirectDescendantsFrom(aDirectDescendantsFrom), + mSearchText(aSearchText) {} + +RotorRule::RotorRule(const nsString& aSearchText) + : mDirectDescendantsFrom(nullptr), mSearchText(aSearchText) {} + +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; + } + + if ((result & nsIAccessibleTraversalRule::FILTER_MATCH) && + !mSearchText.IsEmpty()) { + // If we have a non-empty search text, there are some roles + // we can safely ignore. + switch (aAcc->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 better mapped somewhere. + result &= ~nsIAccessibleTraversalRule::FILTER_MATCH; + break; + default: + nsAutoString name; + aAcc->Name(name); + if (!CaseInsensitiveFindInReadable(mSearchText, name)) { + result &= ~nsIAccessibleTraversalRule::FILTER_MATCH; + } + break; + } + } + + return result; +} + +// Rotor Role Rule + +RotorRoleRule::RotorRoleRule(role aRole, Accessible* aDirectDescendantsFrom, + const nsString& aSearchText) + : RotorRule(aDirectDescendantsFrom, aSearchText), mRole(aRole){}; + +RotorRoleRule::RotorRoleRule(role aRole, const nsString& aSearchText) + : RotorRule(aSearchText), 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, + const nsString& aSearchText) + : RotorRule(aDirectDescendantsFrom, aSearchText), mMacRole(aMacRole) { + [mMacRole retain]; +}; + +RotorMacRoleRule::RotorMacRoleRule(NSString* aMacRole, + const nsString& aSearchText) + : RotorRule(aSearchText), 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, + const nsString& aSearchText) + : RotorRule(aDirectDescendantsFrom, aSearchText){}; + +RotorControlRule::RotorControlRule(const nsString& aSearchText) + : RotorRule(aSearchText){}; + +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::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, + const nsString& aSearchText) + : RotorRule(aDirectDescendantsFrom, aSearchText){}; + +RotorTextEntryRule::RotorTextEntryRule(const nsString& aSearchText) + : RotorRule(aSearchText){}; + +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, + const nsString& aSearchText) + : RotorRule(aDirectDescendantsFrom, aSearchText){}; + +RotorLinkRule::RotorLinkRule(const nsString& aSearchText) + : RotorRule(aSearchText){}; + +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(const nsString& aSearchText) + : RotorLinkRule(aSearchText) {} + +RotorVisitedLinkRule::RotorVisitedLinkRule(Accessible* aDirectDescendantsFrom, + const nsString& aSearchText) + : RotorLinkRule(aDirectDescendantsFrom, aSearchText) {} + +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(const nsString& aSearchText) + : RotorLinkRule(aSearchText) {} + +RotorUnvisitedLinkRule::RotorUnvisitedLinkRule( + Accessible* aDirectDescendantsFrom, const nsString& aSearchText) + : RotorLinkRule(aDirectDescendantsFrom, aSearchText) {} + +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, + const nsString& aSearchText) + : RotorMacRoleRule(aMacRole, aDirectDescendantsFrom, aSearchText) {} + +RotorNotMacRoleRule::RotorNotMacRoleRule(NSString* aMacRole, + const nsString& aSearchText) + : RotorMacRoleRule(aMacRole, aSearchText) {} + +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, + const nsString& aSearchText) + : RotorRule(aDirectDescendantsFrom, aSearchText){}; + +RotorStaticTextRule::RotorStaticTextRule(const nsString& aSearchText) + : RotorRule(aSearchText){}; + +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, + const nsString& aSearchText) + : RotorRoleRule(roles::HEADING, aDirectDescendantsFrom, aSearchText), + mLevel(aLevel){}; + +RotorHeadingLevelRule::RotorHeadingLevelRule(int32_t aLevel, + const nsString& aSearchText) + : RotorRoleRule(roles::HEADING, aSearchText), 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; +} 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..acf6c4443b --- /dev/null +++ b/accessible/mac/moz.build @@ -0,0 +1,70 @@ +# -*- 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", + "PlatformExtTypes.h", +] + +UNIFIED_SOURCES += [ + "AccessibleWrap.mm", + "DocAccessibleWrap.mm", + "GeckoTextMarker.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/xul", + "/layout/generic", + "/layout/xul", + "/widget", + "/widget/cocoa", +] + +GeneratedFile( + "MacSelectorMap.h", + script="/accessible/mac/SelectorMapGen.py", + entry_point="gen_h", + inputs=["MOXAccessibleProtocol.h"], +) +GeneratedFile( + "MacSelectorMap.mm", + script="/accessible/mac/SelectorMapGen.py", + entry_point="gen_mm", + inputs=["MOXAccessibleProtocol.h"], +) + +FINAL_LIBRARY = "xul" + +include("/ipc/chromium/chromium-config.mozbuild") diff --git a/accessible/mac/mozAccessible.h b/accessible/mac/mozAccessible.h new file mode 100644 index 0000000000..175e25d508 --- /dev/null +++ b/accessible/mac/mozAccessible.h @@ -0,0 +1,285 @@ +/* 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 _MozAccessible_H_ +#define _MozAccessible_H_ + +#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()); +} + +// Checked state values some accessibles return as AXValue. +enum CheckedState { + kUncheckable = -1, + kUnchecked = 0, + kChecked = 1, + kMixed = 2 +}; + +} // namespace a11y +} // namespace 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; + + 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; + +// 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::PlatformEvent 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 notifications can be fired. +- (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled; + +// 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 +- (NSNumber*)moxARIAPosInSet; + +// override +- (NSNumber*)moxARIASetSize; + +// 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 + +#endif diff --git a/accessible/mac/mozAccessible.mm b/accessible/mac/mozAccessible.mm new file mode 100644 index 0000000000..2d8e140343 --- /dev/null +++ b/accessible/mac/mozAccessible.mm @@ -0,0 +1,1003 @@ +/* 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 "MOXTextMarkerDelegate.h" +#import "MOXWebAreaAccessible.h" +#import "mozTextAccessible.h" +#import "mozRootAccessible.h" + +#include "LocalAccessible-inl.h" +#include "nsAccUtils.h" +#include "DocAccessibleParent.h" +#include "Relation.h" +#include "mozilla/a11y/Role.h" +#include "RootAccessible.h" +#include "mozilla/a11y/PDocAccessible.h" +#include "mozilla/dom/BrowserParent.h" +#include "OuterDocAccessible.h" +#include "nsChildView.h" +#include "xpcAccessibleMacInterface.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); +} + +- (uint64_t)state { + return mGeckoAccessible->State(); +} + +- (uint64_t)stateWithMask:(uint64_t)mask { + return [self state] & mask; +} + +- (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled { + if (state == states::BUSY) { + [self moxPostNotification:@"AXElementBusyChanged"]; + } +} + +- (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; +} + +#pragma mark - MOXAccessible protocol + +- (BOOL)moxBlockSelector:(SEL)selector { + if (selector == @selector(moxPerformPress)) { + uint8_t actionCount = mGeckoAccessible->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(moxARIAPosInSet) || selector == @selector + (moxARIASetSize)) { + GroupPos groupPos = mGeckoAccessible->GroupPosition(); + return groupPos.setSize == 0; + } + + 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); + + return [MOXTextMarkerDelegate + getOrCreateForDoc:nsAccUtils::DocumentFor(mGeckoAccessible)]; +} + +- (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 isKindOfClass:[MOXWebAreaAccessible class]]) { + // 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, ariaRole, 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); + + // 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 = mGeckoAccessible->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, ariaRole, 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->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); +} + +- (NSNumber*)moxARIAPosInSet { + GroupPos groupPos = mGeckoAccessible->GroupPosition(); + return @(groupPos.posInSet); +} + +- (NSNumber*)moxARIASetSize { + GroupPos groupPos = mGeckoAccessible->GroupPosition(); + return @(groupPos.setSize); +} + +- (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 = nsAccUtils::DocumentFor(mGeckoAccessible); + 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; + + if (!mGeckoAccessible) { + return [NSString stringWithFormat:@"<%@: %p mGeckoAccessible=null>", + NSStringFromClass([self class]), self]; + } + + 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->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); + + mGeckoAccessible->DoAction(0); +} + +#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 received, 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; + + 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..f9940c793a --- /dev/null +++ b/accessible/mac/mozActionElements.h @@ -0,0 +1,108 @@ +/* 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 + +@interface mozDatePickerAccessible : mozAccessible + +// override +- (NSString*)moxTitle; + +@end diff --git a/accessible/mac/mozActionElements.mm b/accessible/mac/mozActionElements.mm new file mode 100644 index 0000000000..f39f2c8ad5 --- /dev/null +++ b/accessible/mac/mozActionElements.mm @@ -0,0 +1,228 @@ +/* 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 "nsCocoaUtils.h" +#include "mozilla/FloatingPoint.h" + +using namespace mozilla::a11y; + +@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"); + mGeckoAccessible->SetCurValue(value); +} + +@end + +@implementation mozDatePickerAccessible + +- (NSString*)moxTitle { + return utils::LocalizedString(u"dateField"_ns); +} + +@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..0968003341 --- /dev/null +++ b/accessible/mac/mozHTMLAccessible.mm @@ -0,0 +1,83 @@ +/* clang-format off */ +/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* clang-format on */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#import "mozHTMLAccessible.h" + +#import "LocalAccessible-inl.h" +#import "HyperTextAccessible.h" + +#import "nsCocoaUtils.h" + +using namespace mozilla::a11y; + +@implementation mozHeadingAccessible + +- (NSString*)moxTitle { + nsAutoString title; + + ENameValueFlag flag = mGeckoAccessible->Name(title); + if (flag != eNameFromSubtree) { + // If this is a name via relation or attribute (eg. aria-label) + // it will be provided via AXDescription. + return nil; + } + + 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..348221ef1d --- /dev/null +++ b/accessible/mac/mozSelectableElements.mm @@ -0,0 +1,330 @@ +/* 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]); +} + +@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: + // 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..09a0c1d5ea --- /dev/null +++ b/accessible/mac/mozTableAccessible.h @@ -0,0 +1,177 @@ +/* clang-format off */ +/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* clang-format on */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#import "mozAccessible.h" + +@interface mozColumnContainer : MOXAccessibleBase { + uint32_t mIndex; + mozAccessible* mParent; + NSMutableArray* mChildren; +} + +// override +- (id)initWithIndex:(uint32_t)aIndex andParent:(mozAccessible*)aParent; + +// override +- (NSString*)moxRole; + +// override +- (NSString*)moxRoleDescription; + +// override +- (mozAccessible*)moxParent; + +// override +- (NSArray*)moxUnignoredChildren; + +// override +- (void)dealloc; + +// override +- (void)expire; + +// override +- (BOOL)isExpired; + +- (void)invalidateChildren; + +@end + +@interface mozTablePartAccessible : mozAccessible + +// override +- (NSString*)moxTitle; + +// override +- (NSString*)moxRole; + +- (BOOL)isLayoutTablePart; + +@end + +@interface mozTableAccessible : mozTablePartAccessible { + NSMutableArray* mColContainers; +} + +// local override +- (BOOL)isLayoutTablePart; + +- (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..a179780a81 --- /dev/null +++ b/accessible/mac/mozTableAccessible.mm @@ -0,0 +1,630 @@ +/* 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" + +#include "AccIterator.h" +#include "LocalAccessible.h" +#include "mozilla/a11y/TableAccessible.h" +#include "mozilla/a11y/TableCellAccessible.h" +#include "nsAccessibilityService.h" +#include "nsIAccessiblePivot.h" +#include "XULTreeAccessible.h" +#include "Pivot.h" +#include "nsAccUtils.h" +#include "Relation.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +@implementation mozColumnContainer + +- (id)initWithIndex:(uint32_t)aIndex andParent:(mozAccessible*)aParent { + self = [super init]; + mIndex = aIndex; + mParent = aParent; + return self; +} + +- (NSString*)moxRole { + return NSAccessibilityColumnRole; +} + +- (NSString*)moxRoleDescription { + return NSAccessibilityRoleDescription(NSAccessibilityColumnRole, nil); +} + +- (mozAccessible*)moxParent { + return mParent; +} + +- (NSArray*)moxUnignoredChildren { + if (mChildren) return mChildren; + + mChildren = [[NSMutableArray alloc] init]; + + TableAccessible* table = [mParent geckoAccessible]->AsTable(); + MOZ_ASSERT(table, "Got null table when fetching column children!"); + uint32_t numRows = table->RowCount(); + + for (uint32_t j = 0; j < numRows; j++) { + Accessible* cell = table->CellAt(j, mIndex); + mozAccessible* nativeCell = cell ? GetNativeFromGeckoAccessible(cell) : nil; + if ([nativeCell isAccessibilityElement]) { + [mChildren addObject:nativeCell]; + } + } + + 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]; +} + +- (BOOL)isLayoutTablePart { + 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; +} +@end + +@implementation mozTableAccessible + +- (BOOL)isLayoutTablePart { + 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 + return false; + } + + // For LocalAccessible and cached RemoteAccessible, we could use + // AsTable()->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()) { + return acc->AsTable()->IsProbablyLayoutTable(); + } + RemoteAccessible* proxy = mGeckoAccessible->AsRemote(); + return proxy->TableIsProbablyForLayout(); +} + +- (void)handleAccessibleEvent:(uint32_t)eventType { + if (eventType == nsIAccessibleEvent::EVENT_REORDER || + eventType == nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED) { + [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 @(mGeckoAccessible->AsTable()->RowCount()); +} + +- (NSNumber*)moxColumnCount { + MOZ_ASSERT(mGeckoAccessible); + + return @(mGeckoAccessible->AsTable()->ColCount()); +} + +- (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; + + numCols = mGeckoAccessible->AsTable()->ColCount(); + 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; + TableAccessible* table = nullptr; + + table = mGeckoAccessible->AsTable(); + numCols = table->ColCount(); + NSMutableArray* colHeaders = + [[[NSMutableArray alloc] initWithCapacity:numCols] autorelease]; + + for (uint32_t i = 0; i < numCols; i++) { + Accessible* cell = table->CellAt(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 = mGeckoAccessible->AsTable()->CellAt(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); + + TableCellAccessible* cell = mGeckoAccessible->AsTableCell(); + return + [NSValue valueWithRange:NSMakeRange(cell->RowIdx(), cell->RowExtent())]; +} + +- (NSValue*)moxColumnIndexRange { + MOZ_ASSERT(mGeckoAccessible); + + TableCellAccessible* cell = mGeckoAccessible->AsTableCell(); + return + [NSValue valueWithRange:NSMakeRange(cell->ColIdx(), cell->ColExtent())]; +} + +- (NSArray*)moxRowHeaderUIElements { + MOZ_ASSERT(mGeckoAccessible); + + TableCellAccessible* cell = mGeckoAccessible->AsTableCell(); + AutoTArray<Accessible*, 10> headerCells; + if (cell) { + cell->RowHeaderCells(&headerCells); + } + return utils::ConvertToNSArray(headerCells); +} + +- (NSArray*)moxColumnHeaderUIElements { + MOZ_ASSERT(mGeckoAccessible); + + TableCellAccessible* cell = mGeckoAccessible->AsTableCell(); + AutoTArray<Accessible*, 10> headerCells; + if (cell) { + cell->ColHeaderCells(&headerCells); + } + return utils::ConvertToNSArray(headerCells); +} + +@end + +/** + * 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 PivotRule { + public: + uint16_t Match(Accessible* aAcc) override { + uint16_t result = nsIAccessibleTraversalRule::FILTER_IGNORE; + + if (nsAccUtils::MustPrune(aAcc)) { + result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + + if (![GetNativeFromGeckoAccessible(aAcc) isAccessibilityElement]) { + return result; + } + + if (aAcc->Role() == roles::OUTLINE) { + // if the accessible is an outline, we ignore all children + result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } else if (aAcc->Role() == roles::OUTLINEITEM) { + // if the accessible is not an outline item, we match here + result |= nsIAccessibleTraversalRule::FILTER_MATCH; + } + + return result; + } +}; + +@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); +} + +- (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..4993e220d2 --- /dev/null +++ b/accessible/mac/mozTextAccessible.mm @@ -0,0 +1,423 @@ +/* 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 (HyperTextAccessibleBase* textAcc = mGeckoAccessible->AsHyperTextBase()) { + lineNumber = textAcc->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 && mGeckoAccessible->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.Start()); + + 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 (HyperTextAccessibleBase* textAcc = mGeckoAccessible->AsHyperTextBase()) { + textAcc->ReplaceText(text); + } +} + +- (void)moxSetSelectedText:(NSString*)selectedText { + MOZ_ASSERT(mGeckoAccessible); + + NSString* stringValue = ToNSString(selectedText); + if (!stringValue) { + return; + } + + HyperTextAccessibleBase* textAcc = mGeckoAccessible->AsHyperTextBase(); + if (!textAcc) { + return; + } + int32_t start = 0, end = 0; + textAcc->SelectionBoundsAt(0, &start, &end); + nsString text; + nsCocoaUtils::GetStringForNSString(stringValue, text); + textAcc->SelectionBoundsAt(0, &start, &end); + textAcc->DeleteText(start, end - start); + textAcc->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 (HyperTextAccessibleBase* textAcc = mGeckoAccessible->AsHyperTextBase()) { + textAcc->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]; +} + +static GeckoTextMarkerRange TextMarkerSubrange(Accessible* aAccessible, + NSValue* aRange) { + GeckoTextMarkerRange textMarkerRange(aAccessible); + GeckoTextMarker start = textMarkerRange.Start(); + GeckoTextMarker end = textMarkerRange.End(); + + NSRange r = [aRange rangeValue]; + start.Offset() += r.location; + end.Offset() = start.Offset() + r.length; + + textMarkerRange = GeckoTextMarkerRange(start, end); + // Crop range to accessible + textMarkerRange.Crop(aAccessible); + + return textMarkerRange; +} + +- (NSString*)moxStringForRange:(NSValue*)range { + MOZ_ASSERT(mGeckoAccessible); + GeckoTextMarkerRange textMarkerRange = + TextMarkerSubrange(mGeckoAccessible, range); + + return textMarkerRange.Text(); +} + +- (NSAttributedString*)moxAttributedStringForRange:(NSValue*)range { + MOZ_ASSERT(mGeckoAccessible); + GeckoTextMarkerRange textMarkerRange = + TextMarkerSubrange(mGeckoAccessible, range); + + return textMarkerRange.AttributedText(); +} + +- (NSValue*)moxBoundsForRange:(NSValue*)range { + MOZ_ASSERT(mGeckoAccessible); + GeckoTextMarkerRange textMarkerRange = + TextMarkerSubrange(mGeckoAccessible, range); + + return textMarkerRange.Bounds(); +} + +@end |