summaryrefslogtreecommitdiffstats
path: root/accessible/mac
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--accessible/mac/.clang-format11
-rw-r--r--accessible/mac/AccessibleWrap.h95
-rw-r--r--accessible/mac/AccessibleWrap.mm281
-rw-r--r--accessible/mac/ApplicationAccessibleWrap.h21
-rw-r--r--accessible/mac/DocAccessibleWrap.h46
-rw-r--r--accessible/mac/DocAccessibleWrap.mm105
-rw-r--r--accessible/mac/GeckoTextMarker.h138
-rw-r--r--accessible/mac/GeckoTextMarker.mm514
-rw-r--r--accessible/mac/MOXAccessibleBase.h143
-rw-r--r--accessible/mac/MOXAccessibleBase.mm584
-rw-r--r--accessible/mac/MOXAccessibleProtocol.h538
-rw-r--r--accessible/mac/MOXLandmarkAccessibles.h15
-rw-r--r--accessible/mac/MOXLandmarkAccessibles.mm15
-rw-r--r--accessible/mac/MOXMathAccessibles.h64
-rw-r--r--accessible/mac/MOXMathAccessibles.mm117
-rw-r--r--accessible/mac/MOXSearchInfo.h43
-rw-r--r--accessible/mac/MOXSearchInfo.mm374
-rw-r--r--accessible/mac/MOXTextMarkerDelegate.h169
-rw-r--r--accessible/mac/MOXTextMarkerDelegate.mm527
-rw-r--r--accessible/mac/MOXWebAreaAccessible.h105
-rw-r--r--accessible/mac/MOXWebAreaAccessible.mm276
-rw-r--r--accessible/mac/MacUtils.h62
-rw-r--r--accessible/mac/MacUtils.mm169
-rw-r--r--accessible/mac/Platform.mm268
-rw-r--r--accessible/mac/PlatformExtTypes.h25
-rw-r--r--accessible/mac/RootAccessibleWrap.h40
-rw-r--r--accessible/mac/RootAccessibleWrap.mm51
-rw-r--r--accessible/mac/RotorRules.h144
-rw-r--r--accessible/mac/RotorRules.mm390
-rwxr-xr-xaccessible/mac/SelectorMapGen.py61
-rw-r--r--accessible/mac/moz.build70
-rw-r--r--accessible/mac/mozAccessible.h285
-rw-r--r--accessible/mac/mozAccessible.mm1003
-rw-r--r--accessible/mac/mozAccessibleProtocol.h65
-rw-r--r--accessible/mac/mozActionElements.h108
-rw-r--r--accessible/mac/mozActionElements.mm228
-rw-r--r--accessible/mac/mozHTMLAccessible.h44
-rw-r--r--accessible/mac/mozHTMLAccessible.mm83
-rw-r--r--accessible/mac/mozRootAccessible.h58
-rw-r--r--accessible/mac/mozRootAccessible.mm84
-rw-r--r--accessible/mac/mozSelectableElements.h128
-rw-r--r--accessible/mac/mozSelectableElements.mm330
-rw-r--r--accessible/mac/mozTableAccessible.h177
-rw-r--r--accessible/mac/mozTableAccessible.mm630
-rw-r--r--accessible/mac/mozTextAccessible.h114
-rw-r--r--accessible/mac/mozTextAccessible.mm423
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