summaryrefslogtreecommitdiffstats
path: root/accessible/mac
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /accessible/mac
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--accessible/mac/.clang-format11
-rw-r--r--accessible/mac/AccessibleWrap.h98
-rw-r--r--accessible/mac/AccessibleWrap.mm413
-rw-r--r--accessible/mac/ApplicationAccessibleWrap.h21
-rw-r--r--accessible/mac/DocAccessibleWrap.h46
-rw-r--r--accessible/mac/DocAccessibleWrap.mm103
-rw-r--r--accessible/mac/GeckoTextMarker.h127
-rw-r--r--accessible/mac/GeckoTextMarker.mm590
-rw-r--r--accessible/mac/HyperTextAccessibleWrap.h96
-rw-r--r--accessible/mac/HyperTextAccessibleWrap.mm771
-rw-r--r--accessible/mac/MOXAccessibleBase.h143
-rw-r--r--accessible/mac/MOXAccessibleBase.mm573
-rw-r--r--accessible/mac/MOXAccessibleProtocol.h534
-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.mm454
-rw-r--r--accessible/mac/MOXTextMarkerDelegate.h169
-rw-r--r--accessible/mac/MOXTextMarkerDelegate.mm495
-rw-r--r--accessible/mac/MOXWebAreaAccessible.h105
-rw-r--r--accessible/mac/MOXWebAreaAccessible.mm274
-rw-r--r--accessible/mac/MacUtils.h51
-rw-r--r--accessible/mac/MacUtils.mm49
-rw-r--r--accessible/mac/Platform.mm243
-rw-r--r--accessible/mac/PlatformExtTypes.h27
-rw-r--r--accessible/mac/RootAccessibleWrap.h40
-rw-r--r--accessible/mac/RootAccessibleWrap.mm51
-rw-r--r--accessible/mac/RotorRules.h142
-rw-r--r--accessible/mac/RotorRules.mm360
-rwxr-xr-xaccessible/mac/SelectorMapGen.py61
-rw-r--r--accessible/mac/moz.build75
-rw-r--r--accessible/mac/mozAccessible.h276
-rw-r--r--accessible/mac/mozAccessible.mm1062
-rw-r--r--accessible/mac/mozAccessibleProtocol.h65
-rw-r--r--accessible/mac/mozActionElements.h101
-rw-r--r--accessible/mac/mozActionElements.mm237
-rw-r--r--accessible/mac/mozHTMLAccessible.h44
-rw-r--r--accessible/mac/mozHTMLAccessible.mm86
-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.mm336
-rw-r--r--accessible/mac/mozTableAccessible.h186
-rw-r--r--accessible/mac/mozTableAccessible.mm739
-rw-r--r--accessible/mac/mozTextAccessible.h114
-rw-r--r--accessible/mac/mozTextAccessible.mm446
48 files changed, 10338 insertions, 0 deletions
diff --git a/accessible/mac/.clang-format b/accessible/mac/.clang-format
new file mode 100644
index 0000000000..269bce4d0f
--- /dev/null
+++ b/accessible/mac/.clang-format
@@ -0,0 +1,11 @@
+---
+# Objective C formatting rules.
+# Since this doesn't derive from the Cpp section, we need to redifine the root rules here.
+Language: ObjC
+BasedOnStyle: Google
+
+DerivePointerAlignment: false
+PointerAlignment: Left
+SortIncludes: false
+ColumnLimit: 80
+IndentPPDirectives: AfterHash
diff --git a/accessible/mac/AccessibleWrap.h b/accessible/mac/AccessibleWrap.h
new file mode 100644
index 0000000000..4ece00cbd8
--- /dev/null
+++ b/accessible/mac/AccessibleWrap.h
@@ -0,0 +1,98 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* For documentation of the accessibility architecture,
+ * see http://lxr.mozilla.org/seamonkey/source/accessible/accessible-docs.html
+ */
+
+#ifndef _AccessibleWrap_H_
+#define _AccessibleWrap_H_
+
+#include <objc/objc.h>
+
+#include "LocalAccessible.h"
+#include "PlatformExtTypes.h"
+#include "States.h"
+
+#include "nsCOMPtr.h"
+
+#include "nsTArray.h"
+
+#if defined(__OBJC__)
+@class mozAccessible;
+#endif
+
+namespace mozilla {
+namespace a11y {
+
+/**
+ * Mac specific functionality for an accessibility tree node that originated in
+ * mDoc's content process.
+ */
+class AccessibleWrap : public LocalAccessible {
+ public: // construction, destruction
+ AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc);
+ virtual ~AccessibleWrap();
+
+ /**
+ * Get the native Obj-C object (mozAccessible).
+ */
+ virtual void GetNativeInterface(void** aOutAccessible) override;
+
+ /**
+ * The objective-c |Class| type that this accessible's native object
+ * should be instantied with. used on runtime to determine the
+ * right type for this accessible's associated native object.
+ */
+ virtual Class GetNativeType();
+
+ virtual void Shutdown() override;
+
+ virtual nsresult HandleAccEvent(AccEvent* aEvent) override;
+
+ bool ApplyPostFilter(const EWhichPostFilter& aSearchKey,
+ const nsString& aSearchText);
+
+ protected:
+ friend class xpcAccessibleMacInterface;
+
+ /**
+ * Get the native object. Create it if needed.
+ */
+#if defined(__OBJC__)
+ mozAccessible* GetNativeObject();
+#else
+ id GetNativeObject();
+#endif
+
+ private:
+ /**
+ * Our native object. Private because its creation is done lazily.
+ * Don't access it directly. Ever. Unless you are GetNativeObject() or
+ * Shutdown()
+ */
+#if defined(__OBJC__)
+ // if we are in Objective-C, we use the actual Obj-C class.
+ mozAccessible* mNativeObject;
+#else
+ id mNativeObject;
+#endif
+
+ /**
+ * We have created our native. This does not mean there is one.
+ * This can never go back to false.
+ * We need it because checking whether we need a native object cost time.
+ */
+ bool mNativeInited;
+};
+
+Class GetTypeFromRole(roles::Role aRole);
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/mac/AccessibleWrap.mm b/accessible/mac/AccessibleWrap.mm
new file mode 100644
index 0000000000..944e8bd059
--- /dev/null
+++ b/accessible/mac/AccessibleWrap.mm
@@ -0,0 +1,413 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DocAccessibleWrap.h"
+#include "nsObjCExceptions.h"
+#include "nsCocoaUtils.h"
+#include "nsUnicharUtils.h"
+
+#include "LocalAccessible-inl.h"
+#include "nsAccUtils.h"
+#include "Role.h"
+#include "TextRange.h"
+#include "gfxPlatform.h"
+
+#import "MOXLandmarkAccessibles.h"
+#import "MOXMathAccessibles.h"
+#import "MOXTextMarkerDelegate.h"
+#import "MOXWebAreaAccessible.h"
+#import "mozAccessible.h"
+#import "mozActionElements.h"
+#import "mozHTMLAccessible.h"
+#import "mozSelectableElements.h"
+#import "mozTableAccessible.h"
+#import "mozTextAccessible.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+AccessibleWrap::AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc)
+ : LocalAccessible(aContent, aDoc),
+ mNativeObject(nil),
+ mNativeInited(false) {
+ if (aContent && aContent->IsElement() && aDoc) {
+ // Check if this accessible is a live region and queue it
+ // it for dispatching an event after it has been inserted.
+ DocAccessibleWrap* doc = static_cast<DocAccessibleWrap*>(aDoc);
+ static const dom::Element::AttrValuesArray sLiveRegionValues[] = {
+ nsGkAtoms::OFF, nsGkAtoms::polite, nsGkAtoms::assertive, nullptr};
+ int32_t attrValue = nsAccUtils::FindARIAAttrValueIn(
+ aContent->AsElement(), nsGkAtoms::aria_live, sLiveRegionValues,
+ eIgnoreCase);
+ if (attrValue == 0) {
+ // aria-live is "off", do nothing.
+ } else if (attrValue > 0) {
+ // aria-live attribute is polite or assertive. It's live!
+ doc->QueueNewLiveRegion(this);
+ } else if (const nsRoleMapEntry* roleMap =
+ aria::GetRoleMap(aContent->AsElement())) {
+ // aria role defines it as a live region. It's live!
+ if (roleMap->liveAttRule == ePoliteLiveAttr ||
+ roleMap->liveAttRule == eAssertiveLiveAttr) {
+ doc->QueueNewLiveRegion(this);
+ }
+ } else if (nsStaticAtom* value = GetAccService()->MarkupAttribute(
+ aContent, nsGkAtoms::aria_live)) {
+ // HTML element defines it as a live region. It's live!
+ if (value == nsGkAtoms::polite || value == nsGkAtoms::assertive) {
+ doc->QueueNewLiveRegion(this);
+ }
+ }
+ }
+}
+
+AccessibleWrap::~AccessibleWrap() {}
+
+mozAccessible* AccessibleWrap::GetNativeObject() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (!mNativeInited && !mNativeObject) {
+ // We don't creat OSX accessibles for xul tooltips, defunct accessibles,
+ // <br> (whitespace) elements, or pruned children.
+ //
+ // To maintain a scripting environment where the XPCOM accessible hierarchy
+ // look the same on all platforms, we still let the C++ objects be created
+ // though.
+ if (!IsXULTooltip() && !IsDefunct() && Role() != roles::WHITESPACE) {
+ mNativeObject = [[GetNativeType() alloc] initWithAccessible:this];
+ }
+ }
+
+ mNativeInited = true;
+
+ return mNativeObject;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+void AccessibleWrap::GetNativeInterface(void** aOutInterface) {
+ *aOutInterface = static_cast<void*>(GetNativeObject());
+}
+
+// overridden in subclasses to create the right kind of object. by default we
+// create a generic 'mozAccessible' node.
+Class AccessibleWrap::GetNativeType() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (IsXULTabpanels()) {
+ return [mozPaneAccessible class];
+ }
+
+ if (IsTable()) {
+ return [mozTableAccessible class];
+ }
+
+ if (IsTableRow()) {
+ return [mozTableRowAccessible class];
+ }
+
+ if (IsTableCell()) {
+ return [mozTableCellAccessible class];
+ }
+
+ if (IsDoc()) {
+ return [MOXWebAreaAccessible class];
+ }
+
+ return GetTypeFromRole(Role());
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+// this method is very important. it is fired when an accessible object "dies".
+// after this point the object might still be around (because some 3rd party
+// still has a ref to it), but it is in fact 'dead'.
+void AccessibleWrap::Shutdown() {
+ // this ensure we will not try to re-create the native object.
+ mNativeInited = true;
+
+ // we really intend to access the member directly.
+ if (mNativeObject) {
+ [mNativeObject expire];
+ [mNativeObject release];
+ mNativeObject = nil;
+ }
+
+ LocalAccessible::Shutdown();
+}
+
+nsresult AccessibleWrap::HandleAccEvent(AccEvent* aEvent) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsresult rv = LocalAccessible::HandleAccEvent(aEvent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (IsDefunct()) {
+ // The accessible can become defunct after their events are handled.
+ return NS_OK;
+ }
+
+ uint32_t eventType = aEvent->GetEventType();
+
+ if (eventType == nsIAccessibleEvent::EVENT_SHOW) {
+ DocAccessibleWrap* doc = static_cast<DocAccessibleWrap*>(Document());
+ doc->ProcessNewLiveRegions();
+ }
+
+ if (IPCAccessibilityActive()) {
+ return NS_OK;
+ }
+
+ LocalAccessible* eventTarget = nullptr;
+
+ switch (eventType) {
+ case nsIAccessibleEvent::EVENT_SELECTION:
+ case nsIAccessibleEvent::EVENT_SELECTION_ADD:
+ case nsIAccessibleEvent::EVENT_SELECTION_REMOVE: {
+ AccSelChangeEvent* selEvent = downcast_accEvent(aEvent);
+ // The "widget" is the selected widget's container. In OSX
+ // it is the target of the selection changed event.
+ eventTarget = selEvent->Widget();
+ break;
+ }
+ case nsIAccessibleEvent::EVENT_TEXT_INSERTED:
+ case nsIAccessibleEvent::EVENT_TEXT_REMOVED: {
+ LocalAccessible* acc = aEvent->GetAccessible();
+ // If there is a text input ancestor, use it as the event source.
+ while (acc && GetTypeFromRole(acc->Role()) != [mozTextAccessible class]) {
+ acc = acc->LocalParent();
+ }
+ eventTarget = acc ? acc : aEvent->GetAccessible();
+ break;
+ }
+ default:
+ eventTarget = aEvent->GetAccessible();
+ break;
+ }
+
+ mozAccessible* nativeAcc = nil;
+ eventTarget->GetNativeInterface((void**)&nativeAcc);
+ if (!nativeAcc) {
+ return NS_ERROR_FAILURE;
+ }
+
+ switch (eventType) {
+ case nsIAccessibleEvent::EVENT_STATE_CHANGE: {
+ AccStateChangeEvent* event = downcast_accEvent(aEvent);
+ [nativeAcc stateChanged:event->GetState()
+ isEnabled:event->IsStateEnabled()];
+ break;
+ }
+
+ case nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED: {
+ MOXTextMarkerDelegate* delegate =
+ [MOXTextMarkerDelegate getOrCreateForDoc:aEvent->Document()];
+ AccTextSelChangeEvent* event = downcast_accEvent(aEvent);
+ AutoTArray<TextRange, 1> ranges;
+ event->SelectionRanges(&ranges);
+
+ if (ranges.Length()) {
+ // Cache selection in delegate.
+ [delegate setSelectionFrom:ranges[0].StartContainer()
+ at:ranges[0].StartOffset()
+ to:ranges[0].EndContainer()
+ at:ranges[0].EndOffset()];
+ }
+
+ [nativeAcc handleAccessibleEvent:eventType];
+ break;
+ }
+
+ case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: {
+ AccCaretMoveEvent* event = downcast_accEvent(aEvent);
+ int32_t caretOffset = event->GetCaretOffset();
+ MOXTextMarkerDelegate* delegate =
+ [MOXTextMarkerDelegate getOrCreateForDoc:aEvent->Document()];
+ [delegate setCaretOffset:eventTarget
+ at:caretOffset
+ moveGranularity:event->GetGranularity()];
+ if (event->IsSelectionCollapsed()) {
+ // If the selection is collapsed, invalidate our text selection cache.
+ [delegate setSelectionFrom:eventTarget
+ at:caretOffset
+ to:eventTarget
+ at:caretOffset];
+ }
+
+ if (mozTextAccessible* textAcc = static_cast<mozTextAccessible*>(
+ [nativeAcc moxEditableAncestor])) {
+ [textAcc
+ handleAccessibleEvent:nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED];
+ } else {
+ [nativeAcc
+ handleAccessibleEvent:nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED];
+ }
+ break;
+ }
+
+ case nsIAccessibleEvent::EVENT_TEXT_INSERTED:
+ case nsIAccessibleEvent::EVENT_TEXT_REMOVED: {
+ AccTextChangeEvent* tcEvent = downcast_accEvent(aEvent);
+ [nativeAcc handleAccessibleTextChangeEvent:nsCocoaUtils::ToNSString(
+ tcEvent->ModifiedText())
+ inserted:tcEvent->IsTextInserted()
+ inContainer:aEvent->GetAccessible()
+ at:tcEvent->GetStartOffset()];
+ break;
+ }
+
+ case nsIAccessibleEvent::EVENT_ALERT:
+ case nsIAccessibleEvent::EVENT_FOCUS:
+ case nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE:
+ case nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE:
+ case nsIAccessibleEvent::EVENT_MENUPOPUP_START:
+ case nsIAccessibleEvent::EVENT_MENUPOPUP_END:
+ case nsIAccessibleEvent::EVENT_REORDER:
+ case nsIAccessibleEvent::EVENT_SELECTION:
+ case nsIAccessibleEvent::EVENT_SELECTION_ADD:
+ case nsIAccessibleEvent::EVENT_SELECTION_REMOVE:
+ case nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED:
+ case nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED:
+ case nsIAccessibleEvent::EVENT_NAME_CHANGE:
+ case nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED:
+ case nsIAccessibleEvent::EVENT_TABLE_STYLING_CHANGED:
+ [nativeAcc handleAccessibleEvent:eventType];
+ break;
+
+ default:
+ break;
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+bool AccessibleWrap::ApplyPostFilter(const EWhichPostFilter& aSearchKey,
+ const nsString& aSearchText) {
+ // We currently only support the eContainsText post filter.
+ MOZ_ASSERT(aSearchKey == EWhichPostFilter::eContainsText,
+ "Only search text supported");
+ nsAutoString name;
+ Name(name);
+ return CaseInsensitiveFindInReadable(aSearchText, name);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// AccessibleWrap protected
+
+Class a11y::GetTypeFromRole(roles::Role aRole) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ switch (aRole) {
+ case roles::COMBOBOX:
+ return [mozPopupButtonAccessible class];
+
+ case roles::PUSHBUTTON:
+ return [mozButtonAccessible class];
+
+ case roles::PAGETAB:
+ return [mozTabAccessible class];
+
+ case roles::CHECKBUTTON:
+ case roles::TOGGLE_BUTTON:
+ case roles::SWITCH:
+ case roles::CHECK_MENU_ITEM:
+ return [mozCheckboxAccessible class];
+
+ case roles::RADIOBUTTON:
+ case roles::RADIO_MENU_ITEM:
+ return [mozRadioButtonAccessible class];
+
+ case roles::SPINBUTTON:
+ case roles::SLIDER:
+ return [mozIncrementableAccessible class];
+
+ case roles::HEADING:
+ return [mozHeadingAccessible class];
+
+ case roles::PAGETABLIST:
+ return [mozTabGroupAccessible class];
+
+ case roles::ENTRY:
+ case roles::CAPTION:
+ case roles::ACCEL_LABEL:
+ case roles::EDITCOMBOBOX:
+ case roles::PASSWORD_TEXT:
+ // normal textfield (static or editable)
+ return [mozTextAccessible class];
+
+ case roles::TEXT_LEAF:
+ case roles::STATICTEXT:
+ return [mozTextLeafAccessible class];
+
+ case roles::LANDMARK:
+ return [MOXLandmarkAccessible class];
+
+ case roles::LINK:
+ return [mozLinkAccessible class];
+
+ case roles::LISTBOX:
+ return [mozListboxAccessible class];
+
+ case roles::LISTITEM:
+ return [MOXListItemAccessible class];
+
+ case roles::OPTION: {
+ return [mozOptionAccessible class];
+ }
+
+ case roles::RICH_OPTION: {
+ return [mozSelectableChildAccessible class];
+ }
+
+ case roles::COMBOBOX_LIST:
+ case roles::MENUBAR:
+ case roles::MENUPOPUP: {
+ return [mozMenuAccessible class];
+ }
+
+ case roles::COMBOBOX_OPTION:
+ case roles::PARENT_MENUITEM:
+ case roles::MENUITEM: {
+ return [mozMenuItemAccessible class];
+ }
+
+ case roles::MATHML_ROOT:
+ return [MOXMathRootAccessible class];
+
+ case roles::MATHML_SQUARE_ROOT:
+ return [MOXMathSquareRootAccessible class];
+
+ case roles::MATHML_FRACTION:
+ return [MOXMathFractionAccessible class];
+
+ case roles::MATHML_SUB:
+ case roles::MATHML_SUP:
+ case roles::MATHML_SUB_SUP:
+ return [MOXMathSubSupAccessible class];
+
+ case roles::MATHML_UNDER:
+ case roles::MATHML_OVER:
+ case roles::MATHML_UNDER_OVER:
+ return [MOXMathUnderOverAccessible class];
+
+ case roles::OUTLINE:
+ case roles::TREE_TABLE:
+ return [mozOutlineAccessible class];
+
+ case roles::OUTLINEITEM:
+ return [mozOutlineRowAccessible class];
+
+ default:
+ return [mozAccessible class];
+ }
+
+ return nil;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
diff --git a/accessible/mac/ApplicationAccessibleWrap.h b/accessible/mac/ApplicationAccessibleWrap.h
new file mode 100644
index 0000000000..a4b2fd70c7
--- /dev/null
+++ b/accessible/mac/ApplicationAccessibleWrap.h
@@ -0,0 +1,21 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_a11y_ApplicationAccessibleWrap_h__
+#define mozilla_a11y_ApplicationAccessibleWrap_h__
+
+#include "ApplicationAccessible.h"
+
+namespace mozilla {
+namespace a11y {
+
+typedef ApplicationAccessible ApplicationAccessibleWrap;
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/mac/DocAccessibleWrap.h b/accessible/mac/DocAccessibleWrap.h
new file mode 100644
index 0000000000..4526fb2b80
--- /dev/null
+++ b/accessible/mac/DocAccessibleWrap.h
@@ -0,0 +1,46 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_a11y_DocAccessibleWrap_h__
+#define mozilla_a11y_DocAccessibleWrap_h__
+
+#include "DocAccessible.h"
+#include "nsTHashSet.h"
+
+namespace mozilla {
+
+class PresShell;
+
+namespace a11y {
+
+class DocAccessibleWrap : public DocAccessible {
+ public:
+ DocAccessibleWrap(dom::Document* aDocument, PresShell* aPresShell);
+
+ virtual ~DocAccessibleWrap();
+
+ virtual void Shutdown() override;
+
+ virtual void AttributeChanged(dom::Element* aElement, int32_t aNameSpaceID,
+ nsAtom* aAttribute, int32_t aModType,
+ const nsAttrValue* aOldValue) override;
+
+ void QueueNewLiveRegion(LocalAccessible* aAccessible);
+
+ void ProcessNewLiveRegions();
+
+ protected:
+ virtual void DoInitialUpdate() override;
+
+ private:
+ nsTHashSet<void*> mNewLiveRegions;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/mac/DocAccessibleWrap.mm b/accessible/mac/DocAccessibleWrap.mm
new file mode 100644
index 0000000000..d83c8d5056
--- /dev/null
+++ b/accessible/mac/DocAccessibleWrap.mm
@@ -0,0 +1,103 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DocAccessibleWrap.h"
+#include "DocAccessible-inl.h"
+
+#import "mozAccessible.h"
+#import "MOXTextMarkerDelegate.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+DocAccessibleWrap::DocAccessibleWrap(dom::Document* aDocument,
+ PresShell* aPresShell)
+ : DocAccessible(aDocument, aPresShell) {}
+
+void DocAccessibleWrap::Shutdown() {
+ [MOXTextMarkerDelegate destroyForDoc:this];
+ DocAccessible::Shutdown();
+}
+
+DocAccessibleWrap::~DocAccessibleWrap() {}
+
+void DocAccessibleWrap::AttributeChanged(dom::Element* aElement,
+ int32_t aNameSpaceID,
+ nsAtom* aAttribute, int32_t aModType,
+ const nsAttrValue* aOldValue) {
+ DocAccessible::AttributeChanged(aElement, aNameSpaceID, aAttribute, aModType,
+ aOldValue);
+ if (aAttribute == nsGkAtoms::aria_live) {
+ LocalAccessible* accessible =
+ mContent != aElement ? GetAccessible(aElement) : this;
+ if (!accessible) {
+ return;
+ }
+
+ static const dom::Element::AttrValuesArray sLiveRegionValues[] = {
+ nsGkAtoms::OFF, nsGkAtoms::polite, nsGkAtoms::assertive, nullptr};
+ int32_t attrValue = nsAccUtils::FindARIAAttrValueIn(
+ aElement, nsGkAtoms::aria_live, sLiveRegionValues, eIgnoreCase);
+ if (attrValue > 0) {
+ if (!aOldValue || aOldValue->IsEmptyString() ||
+ aOldValue->Equals(nsGkAtoms::OFF, eIgnoreCase)) {
+ // This element just got an active aria-live attribute value
+ FireDelayedEvent(nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED,
+ accessible);
+ }
+ } else {
+ if (aOldValue && (aOldValue->Equals(nsGkAtoms::polite, eIgnoreCase) ||
+ aOldValue->Equals(nsGkAtoms::assertive, eIgnoreCase))) {
+ // This element lost an active live region
+ FireDelayedEvent(nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED,
+ accessible);
+ } else if (attrValue == 0) {
+ // aria-live="off", check if its a role-based live region that
+ // needs to be removed.
+ if (const nsRoleMapEntry* roleMap = accessible->ARIARoleMap()) {
+ // aria role defines it as a live region. It's live!
+ if (roleMap->liveAttRule == ePoliteLiveAttr ||
+ roleMap->liveAttRule == eAssertiveLiveAttr) {
+ FireDelayedEvent(nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED,
+ accessible);
+ }
+ } else if (nsStaticAtom* value = GetAccService()->MarkupAttribute(
+ aElement, nsGkAtoms::aria_live)) {
+ // HTML element defines it as a live region. It's live!
+ if (value == nsGkAtoms::polite || value == nsGkAtoms::assertive) {
+ FireDelayedEvent(nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED,
+ accessible);
+ }
+ }
+ }
+ }
+ }
+}
+
+void DocAccessibleWrap::QueueNewLiveRegion(LocalAccessible* aAccessible) {
+ if (!aAccessible) {
+ return;
+ }
+
+ mNewLiveRegions.Insert(aAccessible->UniqueID());
+}
+
+void DocAccessibleWrap::ProcessNewLiveRegions() {
+ for (const auto& uniqueID : mNewLiveRegions) {
+ if (LocalAccessible* liveRegion =
+ GetAccessibleByUniqueID(const_cast<void*>(uniqueID))) {
+ FireDelayedEvent(nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED, liveRegion);
+ }
+ }
+
+ mNewLiveRegions.Clear();
+}
+
+void DocAccessibleWrap::DoInitialUpdate() {
+ DocAccessible::DoInitialUpdate();
+ ProcessNewLiveRegions();
+}
diff --git a/accessible/mac/GeckoTextMarker.h b/accessible/mac/GeckoTextMarker.h
new file mode 100644
index 0000000000..6c16b7f6d6
--- /dev/null
+++ b/accessible/mac/GeckoTextMarker.h
@@ -0,0 +1,127 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _GeckoTextMarker_H_
+#define _GeckoTextMarker_H_
+
+#include <ApplicationServices/ApplicationServices.h>
+#include <Foundation/Foundation.h>
+
+#include "HyperTextAccessibleWrap.h"
+#include "PlatformExtTypes.h"
+#include "SDKDeclarations.h"
+
+namespace mozilla {
+namespace a11y {
+
+class Accessible;
+class GeckoTextMarkerRange;
+
+class GeckoTextMarker final {
+ public:
+ GeckoTextMarker(Accessible* aContainer, int32_t aOffset)
+ : mContainer(aContainer), mOffset(aOffset) {}
+
+ GeckoTextMarker(const GeckoTextMarker& aPoint)
+ : mContainer(aPoint.mContainer), mOffset(aPoint.mOffset) {}
+
+ GeckoTextMarker(Accessible* aDoc, AXTextMarkerRef aTextMarker);
+
+ GeckoTextMarker() : mContainer(nullptr), mOffset(0) {}
+
+ static GeckoTextMarker MarkerFromIndex(Accessible* aRoot, int32_t aIndex);
+
+ AXTextMarkerRef CreateAXTextMarker();
+
+ bool Next();
+
+ bool Previous();
+
+ // Return a range with the given type relative to this marker.
+ GeckoTextMarkerRange Range(EWhichRange aRangeType);
+
+ Accessible* Leaf();
+
+ bool IsValid() const { return !!mContainer; };
+
+ bool operator<(const GeckoTextMarker& aPoint) const;
+
+ bool operator==(const GeckoTextMarker& aPoint) const {
+ return mContainer == aPoint.mContainer && mOffset == aPoint.mOffset;
+ }
+
+ Accessible* mContainer;
+ int32_t mOffset;
+
+ HyperTextAccessibleWrap* ContainerAsHyperTextWrap() const {
+ return (mContainer && mContainer->IsLocal())
+ ? static_cast<HyperTextAccessibleWrap*>(
+ mContainer->AsLocal()->AsHyperText())
+ : nullptr;
+ }
+
+ private:
+ bool IsEditableRoot();
+};
+
+class GeckoTextMarkerRange final {
+ public:
+ GeckoTextMarkerRange(const GeckoTextMarker& aStart,
+ const GeckoTextMarker& aEnd)
+ : mStart(aStart), mEnd(aEnd) {}
+
+ GeckoTextMarkerRange() {}
+
+ GeckoTextMarkerRange(Accessible* aDoc, AXTextMarkerRangeRef aTextMarkerRange);
+
+ explicit GeckoTextMarkerRange(Accessible* aAccessible);
+
+ AXTextMarkerRangeRef CreateAXTextMarkerRange();
+
+ bool IsValid() const { return !!mStart.mContainer && !!mEnd.mContainer; };
+
+ /**
+ * Return text enclosed by the range.
+ */
+ NSString* Text() const;
+
+ /**
+ * Return the attributed text enclosed by the range.
+ */
+ NSAttributedString* AttributedText() const;
+
+ /**
+ * Return length of characters enclosed by the range.
+ */
+ int32_t Length() const;
+
+ /**
+ * Return screen bounds of range.
+ */
+ NSValue* Bounds() const;
+
+ /**
+ * Set the current range as the DOM selection.
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void Select() const;
+
+ /**
+ * Crops the range if it overlaps the given accessible element boundaries.
+ * Return true if successfully cropped. false if the range does not intersect
+ * with the container.
+ */
+ bool Crop(Accessible* aContainer);
+
+ GeckoTextMarker mStart;
+ GeckoTextMarker mEnd;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/mac/GeckoTextMarker.mm b/accessible/mac/GeckoTextMarker.mm
new file mode 100644
index 0000000000..d7233964f0
--- /dev/null
+++ b/accessible/mac/GeckoTextMarker.mm
@@ -0,0 +1,590 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "GeckoTextMarker.h"
+
+#include "DocAccessible.h"
+#include "DocAccessibleParent.h"
+#include "AccAttributes.h"
+#include "nsCocoaUtils.h"
+#include "MOXAccessibleBase.h"
+#include "mozAccessible.h"
+
+#include "mozilla/a11y/DocAccessiblePlatformExtParent.h"
+
+namespace mozilla {
+namespace a11y {
+
+struct OpaqueGeckoTextMarker {
+ OpaqueGeckoTextMarker(uintptr_t aDoc, uintptr_t aID, int32_t aOffset)
+ : mDoc(aDoc), mID(aID), mOffset(aOffset) {}
+ OpaqueGeckoTextMarker() {}
+ uintptr_t mDoc;
+ uintptr_t mID;
+ int32_t mOffset;
+};
+
+static bool DocumentExists(Accessible* aDoc, uintptr_t aDocPtr) {
+ if (reinterpret_cast<uintptr_t>(aDoc) == aDocPtr) {
+ return true;
+ }
+
+ if (aDoc->IsLocal()) {
+ DocAccessible* docAcc = aDoc->AsLocal()->AsDoc();
+ uint32_t docCount = docAcc->ChildDocumentCount();
+ for (uint32_t i = 0; i < docCount; i++) {
+ if (DocumentExists(docAcc->GetChildDocumentAt(i), aDocPtr)) {
+ return true;
+ }
+ }
+ } else {
+ DocAccessibleParent* docProxy = aDoc->AsRemote()->AsDoc();
+ size_t docCount = docProxy->ChildDocCount();
+ for (uint32_t i = 0; i < docCount; i++) {
+ if (DocumentExists(docProxy->ChildDocAt(i), aDocPtr)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+// GeckoTextMarker
+
+GeckoTextMarker::GeckoTextMarker(Accessible* aDoc,
+ AXTextMarkerRef aTextMarker) {
+ MOZ_ASSERT(aDoc);
+ OpaqueGeckoTextMarker opaqueMarker;
+ if (aTextMarker &&
+ AXTextMarkerGetLength(aTextMarker) == sizeof(OpaqueGeckoTextMarker)) {
+ memcpy(&opaqueMarker, AXTextMarkerGetBytePtr(aTextMarker),
+ sizeof(OpaqueGeckoTextMarker));
+ if (DocumentExists(aDoc, opaqueMarker.mDoc)) {
+ Accessible* doc = reinterpret_cast<Accessible*>(opaqueMarker.mDoc);
+ if (doc->IsRemote()) {
+ mContainer = doc->AsRemote()->AsDoc()->GetAccessible(opaqueMarker.mID);
+ } else {
+ mContainer = doc->AsLocal()->AsDoc()->GetAccessibleByUniqueID(
+ reinterpret_cast<void*>(opaqueMarker.mID));
+ }
+ }
+
+ mOffset = opaqueMarker.mOffset;
+ } else {
+ mContainer = nullptr;
+ mOffset = 0;
+ }
+}
+
+GeckoTextMarker GeckoTextMarker::MarkerFromIndex(Accessible* aRoot,
+ int32_t aIndex) {
+ if (aRoot->IsRemote()) {
+ int32_t offset = 0;
+ uint64_t containerID = 0;
+ DocAccessibleParent* ipcDoc = aRoot->AsRemote()->Document();
+ Unused << ipcDoc->GetPlatformExtension()->SendOffsetAtIndex(
+ aRoot->AsRemote()->ID(), aIndex, &containerID, &offset);
+ RemoteAccessible* container = ipcDoc->GetAccessible(containerID);
+ return GeckoTextMarker(container, offset);
+ } else if (auto htWrap = static_cast<HyperTextAccessibleWrap*>(
+ aRoot->AsLocal()->AsHyperText())) {
+ int32_t offset = 0;
+ HyperTextAccessible* container = nullptr;
+ htWrap->OffsetAtIndex(aIndex, &container, &offset);
+ return GeckoTextMarker(container, offset);
+ }
+
+ return GeckoTextMarker();
+}
+
+AXTextMarkerRef GeckoTextMarker::CreateAXTextMarker() {
+ if (!IsValid()) {
+ return nil;
+ }
+
+ Accessible* doc;
+ if (mContainer->IsRemote()) {
+ doc = mContainer->AsRemote()->Document();
+ } else {
+ doc = mContainer->AsLocal()->Document();
+ }
+
+ uintptr_t identifier =
+ mContainer->IsRemote()
+ ? mContainer->AsRemote()->ID()
+ : reinterpret_cast<uintptr_t>(mContainer->AsLocal()->UniqueID());
+
+ OpaqueGeckoTextMarker opaqueMarker(reinterpret_cast<uintptr_t>(doc),
+ identifier, mOffset);
+ AXTextMarkerRef cf_text_marker = AXTextMarkerCreate(
+ kCFAllocatorDefault, reinterpret_cast<const UInt8*>(&opaqueMarker),
+ sizeof(OpaqueGeckoTextMarker));
+
+ return (__bridge AXTextMarkerRef)[(__bridge id)(cf_text_marker)autorelease];
+}
+
+bool GeckoTextMarker::operator<(const GeckoTextMarker& aPoint) const {
+ if (mContainer == aPoint.mContainer) return mOffset < aPoint.mOffset;
+
+ // Build the chain of parents
+ AutoTArray<Accessible*, 30> parents1, parents2;
+ Accessible* p1 = mContainer;
+ while (p1) {
+ parents1.AppendElement(p1);
+ p1 = p1->Parent();
+ }
+
+ Accessible* p2 = aPoint.mContainer;
+ while (p2) {
+ parents2.AppendElement(p2);
+ p2 = p2->Parent();
+ }
+
+ // An empty chain of parents means one of the containers was null.
+ MOZ_ASSERT(parents1.Length() != 0 && parents2.Length() != 0,
+ "have empty chain of parents!");
+
+ // Find where the parent chain differs
+ uint32_t pos1 = parents1.Length(), pos2 = parents2.Length();
+ for (uint32_t len = std::min(pos1, pos2); len > 0; --len) {
+ Accessible* child1 = parents1.ElementAt(--pos1);
+ Accessible* child2 = parents2.ElementAt(--pos2);
+ if (child1 != child2) {
+ return child1->IndexInParent() < child2->IndexInParent();
+ }
+ }
+
+ if (pos1 != 0) {
+ // If parents1 is a superset of parents2 then mContainer is a
+ // descendant of aPoint.mContainer. The next element down in parents1
+ // is mContainer's ancestor that is the child of aPoint.mContainer.
+ // We compare its end offset in aPoint.mContainer with aPoint.mOffset.
+ Accessible* child = parents1.ElementAt(pos1 - 1);
+ MOZ_ASSERT(child->Parent() == aPoint.mContainer);
+ uint32_t endOffset = child->EndOffset();
+ return endOffset < static_cast<uint32_t>(aPoint.mOffset);
+ }
+
+ if (pos2 != 0) {
+ // If parents2 is a superset of parents1 then aPoint.mContainer is a
+ // descendant of mContainer. The next element down in parents2
+ // is aPoint.mContainer's ancestor that is the child of mContainer.
+ // We compare its start offset in mContainer with mOffset.
+ Accessible* child = parents2.ElementAt(pos2 - 1);
+ MOZ_ASSERT(child->Parent() == mContainer);
+ uint32_t startOffset = child->StartOffset();
+ return static_cast<uint32_t>(mOffset) <= startOffset;
+ }
+
+ MOZ_ASSERT_UNREACHABLE("Broken tree?!");
+ return false;
+}
+
+bool GeckoTextMarker::IsEditableRoot() {
+ uint64_t state = mContainer->IsRemote() ? mContainer->AsRemote()->State()
+ : mContainer->AsLocal()->State();
+ if ((state & states::EDITABLE) == 0) {
+ return false;
+ }
+
+ Accessible* parent = mContainer->Parent();
+ if (!parent) {
+ // Not sure when this can happen, but it would technically be an editable
+ // root.
+ return true;
+ }
+
+ state = parent->IsRemote() ? parent->AsRemote()->State()
+ : parent->AsLocal()->State();
+
+ return (state & states::EDITABLE) == 0;
+}
+
+bool GeckoTextMarker::Next() {
+ if (mContainer->IsRemote()) {
+ int32_t nextOffset = 0;
+ uint64_t nextContainerID = 0;
+ DocAccessibleParent* ipcDoc = mContainer->AsRemote()->Document();
+ Unused << ipcDoc->GetPlatformExtension()->SendNextClusterAt(
+ mContainer->AsRemote()->ID(), mOffset, &nextContainerID, &nextOffset);
+ RemoteAccessible* nextContainer = ipcDoc->GetAccessible(nextContainerID);
+ bool moved =
+ nextContainer != mContainer->AsRemote() || nextOffset != mOffset;
+ mContainer = nextContainer;
+ mOffset = nextOffset;
+ return moved;
+ } else if (auto htWrap = ContainerAsHyperTextWrap()) {
+ HyperTextAccessible* nextContainer = nullptr;
+ int32_t nextOffset = 0;
+ htWrap->NextClusterAt(mOffset, &nextContainer, &nextOffset);
+ bool moved = nextContainer != htWrap || nextOffset != mOffset;
+ mContainer = nextContainer;
+ mOffset = nextOffset;
+ return moved;
+ }
+
+ return false;
+}
+
+bool GeckoTextMarker::Previous() {
+ if (mContainer->IsRemote()) {
+ int32_t prevOffset = 0;
+ uint64_t prevContainerID = 0;
+ DocAccessibleParent* ipcDoc = mContainer->AsRemote()->Document();
+ Unused << ipcDoc->GetPlatformExtension()->SendPreviousClusterAt(
+ mContainer->AsRemote()->ID(), mOffset, &prevContainerID, &prevOffset);
+ RemoteAccessible* prevContainer = ipcDoc->GetAccessible(prevContainerID);
+ bool moved =
+ prevContainer != mContainer->AsRemote() || prevOffset != mOffset;
+ mContainer = prevContainer;
+ mOffset = prevOffset;
+ return moved;
+ } else if (auto htWrap = ContainerAsHyperTextWrap()) {
+ HyperTextAccessible* prevContainer = nullptr;
+ int32_t prevOffset = 0;
+ htWrap->PreviousClusterAt(mOffset, &prevContainer, &prevOffset);
+ bool moved = prevContainer != htWrap || prevOffset != mOffset;
+ mContainer = prevContainer;
+ mOffset = prevOffset;
+ return moved;
+ }
+
+ return false;
+}
+
+static uint32_t CharacterCount(Accessible* aContainer) {
+ if (aContainer->IsRemote()) {
+ return aContainer->AsRemote()->CharacterCount();
+ }
+
+ if (aContainer->AsLocal()->IsHyperText()) {
+ return aContainer->AsLocal()->AsHyperText()->CharacterCount();
+ }
+
+ return 0;
+}
+
+GeckoTextMarkerRange GeckoTextMarker::Range(EWhichRange aRangeType) {
+ MOZ_ASSERT(mContainer);
+ if (mContainer->IsRemote()) {
+ int32_t startOffset = 0, endOffset = 0;
+ uint64_t startContainerID = 0, endContainerID = 0;
+ DocAccessibleParent* ipcDoc = mContainer->AsRemote()->Document();
+ bool success = ipcDoc->GetPlatformExtension()->SendRangeAt(
+ mContainer->AsRemote()->ID(), mOffset, aRangeType, &startContainerID,
+ &startOffset, &endContainerID, &endOffset);
+ if (success) {
+ return GeckoTextMarkerRange(
+ GeckoTextMarker(ipcDoc->GetAccessible(startContainerID), startOffset),
+ GeckoTextMarker(ipcDoc->GetAccessible(endContainerID), endOffset));
+ }
+ } else if (auto htWrap = ContainerAsHyperTextWrap()) {
+ int32_t startOffset = 0, endOffset = 0;
+ HyperTextAccessible* startContainer = nullptr;
+ HyperTextAccessible* endContainer = nullptr;
+ htWrap->RangeAt(mOffset, aRangeType, &startContainer, &startOffset,
+ &endContainer, &endOffset);
+ return GeckoTextMarkerRange(GeckoTextMarker(startContainer, startOffset),
+ GeckoTextMarker(endContainer, endOffset));
+ }
+
+ return GeckoTextMarkerRange(GeckoTextMarker(), GeckoTextMarker());
+}
+
+Accessible* GeckoTextMarker::Leaf() {
+ MOZ_ASSERT(mContainer);
+ if (mContainer->IsRemote()) {
+ uint64_t leafID = 0;
+ DocAccessibleParent* ipcDoc = mContainer->AsRemote()->Document();
+ Unused << ipcDoc->GetPlatformExtension()->SendLeafAtOffset(
+ mContainer->AsRemote()->ID(), mOffset, &leafID);
+ return ipcDoc->GetAccessible(leafID);
+ } else if (auto htWrap = ContainerAsHyperTextWrap()) {
+ return htWrap->LeafAtOffset(mOffset);
+ }
+
+ return mContainer;
+}
+
+// GeckoTextMarkerRange
+
+GeckoTextMarkerRange::GeckoTextMarkerRange(
+ Accessible* aDoc, AXTextMarkerRangeRef aTextMarkerRange) {
+ if (!aTextMarkerRange ||
+ CFGetTypeID(aTextMarkerRange) != AXTextMarkerRangeGetTypeID()) {
+ return;
+ }
+
+ AXTextMarkerRef start_marker(
+ AXTextMarkerRangeCopyStartMarker(aTextMarkerRange));
+ AXTextMarkerRef end_marker(AXTextMarkerRangeCopyEndMarker(aTextMarkerRange));
+
+ mStart = GeckoTextMarker(aDoc, start_marker);
+ mEnd = GeckoTextMarker(aDoc, end_marker);
+
+ CFRelease(start_marker);
+ CFRelease(end_marker);
+}
+
+GeckoTextMarkerRange::GeckoTextMarkerRange(Accessible* aAccessible) {
+ if (aAccessible->IsHyperText()) {
+ // The accessible is a hypertext. Initialize range to its inner text range.
+ mStart = GeckoTextMarker(aAccessible, 0);
+ mEnd = GeckoTextMarker(aAccessible, (CharacterCount(aAccessible)));
+ } else {
+ // The accessible is not a hypertext (maybe a text leaf?). Initialize range
+ // to its offsets in its container.
+ mStart = GeckoTextMarker(aAccessible->Parent(), 0);
+ mEnd = GeckoTextMarker(aAccessible->Parent(), 0);
+ if (mStart.mContainer->IsRemote()) {
+ DocAccessibleParent* ipcDoc = mStart.mContainer->AsRemote()->Document();
+ Unused << ipcDoc->GetPlatformExtension()->SendRangeOfChild(
+ mStart.mContainer->AsRemote()->ID(), aAccessible->AsRemote()->ID(),
+ &mStart.mOffset, &mEnd.mOffset);
+ } else if (auto htWrap = mStart.ContainerAsHyperTextWrap()) {
+ htWrap->RangeOfChild(aAccessible->AsLocal(), &mStart.mOffset,
+ &mEnd.mOffset);
+ }
+ }
+}
+
+AXTextMarkerRangeRef GeckoTextMarkerRange::CreateAXTextMarkerRange() {
+ if (!IsValid()) {
+ return nil;
+ }
+
+ AXTextMarkerRangeRef cf_text_marker_range =
+ AXTextMarkerRangeCreate(kCFAllocatorDefault, mStart.CreateAXTextMarker(),
+ mEnd.CreateAXTextMarker());
+
+ return (__bridge AXTextMarkerRangeRef)[(__bridge id)(
+ cf_text_marker_range)autorelease];
+}
+
+NSString* GeckoTextMarkerRange::Text() const {
+ nsAutoString text;
+ if (mStart.mContainer->IsRemote() && mEnd.mContainer->IsRemote()) {
+ DocAccessibleParent* ipcDoc = mStart.mContainer->AsRemote()->Document();
+ Unused << ipcDoc->GetPlatformExtension()->SendTextForRange(
+ mStart.mContainer->AsRemote()->ID(), mStart.mOffset,
+ mEnd.mContainer->AsRemote()->ID(), mEnd.mOffset, &text);
+ } else if (auto htWrap = mStart.ContainerAsHyperTextWrap()) {
+ htWrap->TextForRange(text, mStart.mOffset, mEnd.ContainerAsHyperTextWrap(),
+ mEnd.mOffset);
+ }
+ return nsCocoaUtils::ToNSString(text);
+}
+
+static NSColor* ColorFromColor(const Color& aColor) {
+ return [NSColor colorWithCalibratedRed:NS_GET_R(aColor.mValue) / 255.0
+ green:NS_GET_G(aColor.mValue) / 255.0
+ blue:NS_GET_B(aColor.mValue) / 255.0
+ alpha:1.0];
+}
+
+static NSDictionary* StringAttributesFromAttributes(AccAttributes* aAttributes,
+ Accessible* aContainer) {
+ NSMutableDictionary* attrDict =
+ [NSMutableDictionary dictionaryWithCapacity:aAttributes->Count()];
+ NSMutableDictionary* fontAttrDict = [[NSMutableDictionary alloc] init];
+ [attrDict setObject:fontAttrDict forKey:@"AXFont"];
+ for (auto iter : *aAttributes) {
+ if (iter.Name() == nsGkAtoms::backgroundColor) {
+ if (Maybe<Color> value = iter.Value<Color>()) {
+ NSColor* color = ColorFromColor(*value);
+ [attrDict setObject:(__bridge id)color.CGColor
+ forKey:@"AXBackgroundColor"];
+ }
+ } else if (iter.Name() == nsGkAtoms::color) {
+ if (Maybe<Color> value = iter.Value<Color>()) {
+ NSColor* color = ColorFromColor(*value);
+ [attrDict setObject:(__bridge id)color.CGColor
+ forKey:@"AXForegroundColor"];
+ }
+ } else if (iter.Name() == nsGkAtoms::font_size) {
+ if (Maybe<FontSize> pointSize = iter.Value<FontSize>()) {
+ int32_t fontPixelSize = static_cast<int32_t>(pointSize->mValue * 4 / 3);
+ [fontAttrDict setObject:@(fontPixelSize) forKey:@"AXFontSize"];
+ }
+ } else if (iter.Name() == nsGkAtoms::font_family) {
+ nsAutoString fontFamily;
+ iter.ValueAsString(fontFamily);
+ [fontAttrDict setObject:nsCocoaUtils::ToNSString(fontFamily)
+ forKey:@"AXFontFamily"];
+ } else if (iter.Name() == nsGkAtoms::textUnderlineColor) {
+ [attrDict setObject:@1 forKey:@"AXUnderline"];
+ if (Maybe<Color> value = iter.Value<Color>()) {
+ NSColor* color = ColorFromColor(*value);
+ [attrDict setObject:(__bridge id)color.CGColor
+ forKey:@"AXUnderlineColor"];
+ }
+ } else if (iter.Name() == nsGkAtoms::invalid) {
+ // XXX: There is currently no attribute for grammar
+ if (auto value = iter.Value<RefPtr<nsAtom>>()) {
+ if (*value == nsGkAtoms::spelling) {
+ [attrDict setObject:@YES
+ forKey:NSAccessibilityMarkedMisspelledTextAttribute];
+ }
+ }
+ } else {
+ nsAutoString valueStr;
+ iter.ValueAsString(valueStr);
+ nsAutoString keyStr;
+ iter.NameAsString(keyStr);
+ [attrDict setObject:nsCocoaUtils::ToNSString(valueStr)
+ forKey:nsCocoaUtils::ToNSString(keyStr)];
+ }
+ }
+
+ mozAccessible* container = GetNativeFromGeckoAccessible(aContainer);
+ id<MOXAccessible> link =
+ [container moxFindAncestor:^BOOL(id<MOXAccessible> moxAcc, BOOL* stop) {
+ return [[moxAcc moxRole] isEqualToString:NSAccessibilityLinkRole];
+ }];
+ if (link) {
+ [attrDict setObject:link forKey:@"AXLink"];
+ }
+
+ id<MOXAccessible> heading =
+ [container moxFindAncestor:^BOOL(id<MOXAccessible> moxAcc, BOOL* stop) {
+ return [[moxAcc moxRole] isEqualToString:@"AXHeading"];
+ }];
+ if (heading) {
+ [attrDict setObject:[heading moxValue] forKey:@"AXHeadingLevel"];
+ }
+
+ return attrDict;
+}
+
+NSAttributedString* GeckoTextMarkerRange::AttributedText() const {
+ NSMutableAttributedString* str =
+ [[[NSMutableAttributedString alloc] init] autorelease];
+
+ if (mStart.mContainer->IsRemote() && mEnd.mContainer->IsRemote()) {
+ nsTArray<TextAttributesRun> textAttributesRuns;
+ DocAccessibleParent* ipcDoc = mStart.mContainer->AsRemote()->Document();
+ Unused << ipcDoc->GetPlatformExtension()->SendAttributedTextForRange(
+ mStart.mContainer->AsRemote()->ID(), mStart.mOffset,
+ mEnd.mContainer->AsRemote()->ID(), mEnd.mOffset, &textAttributesRuns);
+
+ for (size_t i = 0; i < textAttributesRuns.Length(); i++) {
+ AccAttributes* attributes =
+ textAttributesRuns.ElementAt(i).TextAttributes();
+ RemoteAccessible* container =
+ ipcDoc->GetAccessible(textAttributesRuns.ElementAt(i).ContainerID());
+
+ NSAttributedString* substr = [[[NSAttributedString alloc]
+ initWithString:nsCocoaUtils::ToNSString(
+ textAttributesRuns.ElementAt(i).Text())
+ attributes:StringAttributesFromAttributes(attributes, container)]
+ autorelease];
+
+ [str appendAttributedString:substr];
+ }
+ } else if (auto htWrap = mStart.ContainerAsHyperTextWrap()) {
+ nsTArray<nsString> texts;
+ nsTArray<LocalAccessible*> containers;
+ nsTArray<RefPtr<AccAttributes>> props;
+
+ htWrap->AttributedTextForRange(texts, props, containers, mStart.mOffset,
+ mEnd.ContainerAsHyperTextWrap(),
+ mEnd.mOffset);
+
+ MOZ_ASSERT(texts.Length() == props.Length() &&
+ texts.Length() == containers.Length());
+
+ for (size_t i = 0; i < texts.Length(); i++) {
+ NSAttributedString* substr = [[[NSAttributedString alloc]
+ initWithString:nsCocoaUtils::ToNSString(texts.ElementAt(i))
+ attributes:StringAttributesFromAttributes(
+ props.ElementAt(i), containers.ElementAt(i))]
+ autorelease];
+ [str appendAttributedString:substr];
+ }
+ }
+
+ return str;
+}
+
+int32_t GeckoTextMarkerRange::Length() const {
+ int32_t length = 0;
+ if (mStart.mContainer->IsRemote() && mEnd.mContainer->IsRemote()) {
+ DocAccessibleParent* ipcDoc = mStart.mContainer->AsRemote()->Document();
+ Unused << ipcDoc->GetPlatformExtension()->SendLengthForRange(
+ mStart.mContainer->AsRemote()->ID(), mStart.mOffset,
+ mEnd.mContainer->AsRemote()->ID(), mEnd.mOffset, &length);
+ } else if (auto htWrap = mStart.ContainerAsHyperTextWrap()) {
+ length = htWrap->LengthForRange(
+ mStart.mOffset, mEnd.ContainerAsHyperTextWrap(), mEnd.mOffset);
+ }
+
+ return length;
+}
+
+NSValue* GeckoTextMarkerRange::Bounds() const {
+ LayoutDeviceIntRect rect;
+ if (mStart.mContainer->IsRemote() && mEnd.mContainer->IsRemote()) {
+ DocAccessibleParent* ipcDoc = mStart.mContainer->AsRemote()->Document();
+ Unused << ipcDoc->GetPlatformExtension()->SendBoundsForRange(
+ mStart.mContainer->AsRemote()->ID(), mStart.mOffset,
+ mEnd.mContainer->AsRemote()->ID(), mEnd.mOffset, &rect);
+ } else if (auto htWrap = mStart.ContainerAsHyperTextWrap()) {
+ rect = htWrap->BoundsForRange(
+ mStart.mOffset, mEnd.ContainerAsHyperTextWrap(), mEnd.mOffset);
+ }
+
+ NSScreen* mainView = [[NSScreen screens] objectAtIndex:0];
+ CGFloat scaleFactor = nsCocoaUtils::GetBackingScaleFactor(mainView);
+ NSRect r =
+ NSMakeRect(static_cast<CGFloat>(rect.x) / scaleFactor,
+ [mainView frame].size.height -
+ static_cast<CGFloat>(rect.y + rect.height) / scaleFactor,
+ static_cast<CGFloat>(rect.width) / scaleFactor,
+ static_cast<CGFloat>(rect.height) / scaleFactor);
+
+ return [NSValue valueWithRect:r];
+}
+
+void GeckoTextMarkerRange::Select() const {
+ if (mStart.mContainer->IsRemote() && mEnd.mContainer->IsRemote()) {
+ DocAccessibleParent* ipcDoc = mStart.mContainer->AsRemote()->Document();
+ Unused << ipcDoc->GetPlatformExtension()->SendSelectRange(
+ mStart.mContainer->AsRemote()->ID(), mStart.mOffset,
+ mEnd.mContainer->AsRemote()->ID(), mEnd.mOffset);
+ } else if (RefPtr<HyperTextAccessibleWrap> htWrap =
+ mStart.ContainerAsHyperTextWrap()) {
+ RefPtr<HyperTextAccessibleWrap> end = mEnd.ContainerAsHyperTextWrap();
+ htWrap->SelectRange(mStart.mOffset, end, mEnd.mOffset);
+ }
+}
+
+bool GeckoTextMarkerRange::Crop(Accessible* aContainer) {
+ GeckoTextMarker containerStart(aContainer, 0);
+ GeckoTextMarker containerEnd(aContainer, CharacterCount(aContainer));
+
+ if (mEnd < containerStart || containerEnd < mStart) {
+ // The range ends before the container, or starts after it.
+ return false;
+ }
+
+ if (mStart < containerStart) {
+ // If range start is before container start, adjust range start to
+ // start of container.
+ mStart = containerStart;
+ }
+
+ if (containerEnd < mEnd) {
+ // If range end is after container end, adjust range end to end of
+ // container.
+ mEnd = containerEnd;
+ }
+
+ return true;
+}
+}
+}
diff --git a/accessible/mac/HyperTextAccessibleWrap.h b/accessible/mac/HyperTextAccessibleWrap.h
new file mode 100644
index 0000000000..bb418ed179
--- /dev/null
+++ b/accessible/mac/HyperTextAccessibleWrap.h
@@ -0,0 +1,96 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_a11y_HyperTextAccessibleWrap_h__
+#define mozilla_a11y_HyperTextAccessibleWrap_h__
+
+#include "HyperTextAccessible.h"
+#include "PlatformExtTypes.h"
+
+namespace mozilla {
+namespace a11y {
+
+struct TextPoint;
+
+class HyperTextAccessibleWrap : public HyperTextAccessible {
+ public:
+ HyperTextAccessibleWrap(nsIContent* aContent, DocAccessible* aDoc)
+ : HyperTextAccessible(aContent, aDoc) {}
+
+ void TextForRange(nsAString& aText, int32_t aStartOffset,
+ HyperTextAccessible* aEndContainer, int32_t aEndOffset);
+
+ void AttributedTextForRange(nsTArray<nsString>& aStrings,
+ nsTArray<RefPtr<AccAttributes>>& aProperties,
+ nsTArray<LocalAccessible*>& aContainers,
+ int32_t aStartOffset,
+ HyperTextAccessible* aEndContainer,
+ int32_t aEndOffset);
+
+ LayoutDeviceIntRect BoundsForRange(int32_t aStartOffset,
+ HyperTextAccessible* aEndContainer,
+ int32_t aEndOffset);
+
+ int32_t LengthForRange(int32_t aStartOffset,
+ HyperTextAccessible* aEndContainer,
+ int32_t aEndOffset);
+
+ void OffsetAtIndex(int32_t aIndex, HyperTextAccessible** aContainer,
+ int32_t* aOffset);
+
+ void RangeAt(int32_t aOffset, EWhichRange aRangeType,
+ HyperTextAccessible** aStartContainer, int32_t* aStartOffset,
+ HyperTextAccessible** aEndContainer, int32_t* aEndOffset);
+
+ void NextClusterAt(int32_t aOffset, HyperTextAccessible** aNextContainer,
+ int32_t* aNextOffset);
+
+ void PreviousClusterAt(int32_t aOffset, HyperTextAccessible** aPrevContainer,
+ int32_t* aPrevOffset);
+
+ void RangeOfChild(LocalAccessible* aChild, int32_t* aStartOffset,
+ int32_t* aEndOffset);
+
+ LocalAccessible* LeafAtOffset(int32_t aOffset);
+
+ MOZ_CAN_RUN_SCRIPT void SelectRange(int32_t aStartOffset,
+ HyperTextAccessible* aEndContainer,
+ int32_t aEndOffset);
+
+ protected:
+ ~HyperTextAccessibleWrap() {}
+
+ private:
+ TextPoint FindTextPoint(int32_t aOffset, nsDirection aDirection,
+ nsSelectionAmount aAmount,
+ EWordMovementType aWordMovementType);
+
+ HyperTextAccessibleWrap* EditableRoot();
+
+ void LeftWordAt(int32_t aOffset, HyperTextAccessible** aStartContainer,
+ int32_t* aStartOffset, HyperTextAccessible** aEndContainer,
+ int32_t* aEndOffset);
+
+ void RightWordAt(int32_t aOffset, HyperTextAccessible** aStartContainer,
+ int32_t* aStartOffset, HyperTextAccessible** aEndContainer,
+ int32_t* aEndOffset);
+
+ void LineAt(int32_t aOffset, bool aNextLine,
+ HyperTextAccessible** aStartContainer, int32_t* aStartOffset,
+ HyperTextAccessible** aEndContainer, int32_t* aEndOffset);
+
+ void ParagraphAt(int32_t aOffset, HyperTextAccessible** aStartContainer,
+ int32_t* aStartOffset, HyperTextAccessible** aEndContainer,
+ int32_t* aEndOffset);
+
+ void StyleAt(int32_t aOffset, HyperTextAccessible** aStartContainer,
+ int32_t* aStartOffset, HyperTextAccessible** aEndContainer,
+ int32_t* aEndOffset);
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/mac/HyperTextAccessibleWrap.mm b/accessible/mac/HyperTextAccessibleWrap.mm
new file mode 100644
index 0000000000..361eb9ca22
--- /dev/null
+++ b/accessible/mac/HyperTextAccessibleWrap.mm
@@ -0,0 +1,771 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HyperTextAccessibleWrap.h"
+
+#include "LocalAccessible-inl.h"
+#include "HTMLListAccessible.h"
+#include "nsAccUtils.h"
+#include "nsFrameSelection.h"
+#include "TextRange.h"
+#include "TreeWalker.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+// HyperTextIterator
+
+class HyperTextIterator {
+ public:
+ HyperTextIterator(HyperTextAccessible* aStartContainer, int32_t aStartOffset,
+ HyperTextAccessible* aEndContainer, int32_t aEndOffset)
+ : mCurrentContainer(aStartContainer),
+ mCurrentStartOffset(0),
+ mCurrentEndOffset(0),
+ mEndContainer(aEndContainer),
+ mEndOffset(0) {
+ mCurrentStartOffset =
+ std::min(aStartOffset,
+ static_cast<int32_t>(mCurrentContainer->CharacterCount()));
+ mCurrentEndOffset = mCurrentStartOffset;
+ mEndOffset = std::min(
+ aEndOffset, static_cast<int32_t>(mEndContainer->CharacterCount()));
+ }
+
+ bool Next();
+
+ int32_t SegmentLength();
+
+ // If offset is set to a child hyperlink, adjust it so it set on the first
+ // offset in the deepest link. Or, if the offset to the last character, set it
+ // to the outermost end offset in an ancestor. Returns true if iterator was
+ // mutated.
+ bool NormalizeForward();
+
+ // If offset is set right after child hyperlink, adjust it so it set on the
+ // last offset in the deepest link. Or, if the offset is on the first
+ // character of a link, set it to the outermost start offset in an ancestor.
+ // Returns true if iterator was mutated.
+ bool NormalizeBackward();
+
+ HyperTextAccessible* mCurrentContainer;
+ int32_t mCurrentStartOffset;
+ int32_t mCurrentEndOffset;
+
+ private:
+ int32_t NextLinkOffset();
+
+ HyperTextAccessible* mEndContainer;
+ int32_t mEndOffset;
+};
+
+bool HyperTextIterator::NormalizeForward() {
+ if (mCurrentStartOffset == nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT ||
+ mCurrentStartOffset >=
+ static_cast<int32_t>(mCurrentContainer->CharacterCount())) {
+ // If this is the end of the current container, mutate to its parent's
+ // end offset.
+ if (!mCurrentContainer->IsLink()) {
+ // If we are not a link, it is a root hypertext accessible.
+ return false;
+ }
+ if (!mCurrentContainer->LocalParent() ||
+ !mCurrentContainer->LocalParent()->IsHyperText()) {
+ // If we are a link, but our parent is not a hypertext accessible
+ // treat the current container as the root hypertext accessible.
+ // This can be the case with some XUL containers that are not
+ // hypertext accessibles.
+ return false;
+ }
+ uint32_t endOffset = mCurrentContainer->EndOffset();
+ if (endOffset != 0) {
+ mCurrentContainer = mCurrentContainer->LocalParent()->AsHyperText();
+ mCurrentStartOffset = endOffset;
+
+ if (mCurrentContainer == mEndContainer &&
+ mCurrentStartOffset >= mEndOffset) {
+ // Reached end boundary.
+ return false;
+ }
+
+ // Call NormalizeForward recursively to get top-most link if at the end of
+ // one, or innermost link if at the beginning.
+ NormalizeForward();
+ return true;
+ }
+ } else {
+ LocalAccessible* link = mCurrentContainer->LinkAt(
+ mCurrentContainer->LinkIndexAtOffset(mCurrentStartOffset));
+
+ // If there is a link at this offset, mutate into it.
+ if (link && link->IsHyperText()) {
+ if (mCurrentStartOffset > 0 &&
+ mCurrentContainer->LinkIndexAtOffset(mCurrentStartOffset) ==
+ mCurrentContainer->LinkIndexAtOffset(mCurrentStartOffset - 1)) {
+ MOZ_ASSERT_UNREACHABLE("Same link for previous offset");
+ return false;
+ }
+
+ mCurrentContainer = link->AsHyperText();
+ if (link->IsHTMLListItem()) {
+ LocalAccessible* bullet = link->AsHTMLListItem()->Bullet();
+ mCurrentStartOffset = bullet ? nsAccUtils::TextLength(bullet) : 0;
+ } else {
+ mCurrentStartOffset = 0;
+ }
+
+ if (mCurrentContainer == mEndContainer &&
+ mCurrentStartOffset >= mEndOffset) {
+ // Reached end boundary.
+ return false;
+ }
+
+ // Call NormalizeForward recursively to get top-most embedding ancestor
+ // if at the end of one, or innermost link if at the beginning.
+ NormalizeForward();
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool HyperTextIterator::NormalizeBackward() {
+ if (mCurrentStartOffset == 0) {
+ // If this is the start of the current container, mutate to its parent's
+ // start offset.
+ if (!mCurrentContainer->IsLink()) {
+ // If we are not a link, it is a root hypertext accessible.
+ return false;
+ }
+ if (!mCurrentContainer->LocalParent() ||
+ !mCurrentContainer->LocalParent()->IsHyperText()) {
+ // If we are a link, but our parent is not a hypertext accessible
+ // treat the current container as the root hypertext accessible.
+ // This can be the case with some XUL containers that are not
+ // hypertext accessibles.
+ return false;
+ }
+
+ uint32_t startOffset = mCurrentContainer->StartOffset();
+ mCurrentContainer = mCurrentContainer->LocalParent()->AsHyperText();
+ mCurrentStartOffset = startOffset;
+
+ // Call NormalizeBackward recursively to get top-most link if at the
+ // beginning of one, or innermost link if at the end.
+ NormalizeBackward();
+ return true;
+ } else {
+ LocalAccessible* link =
+ mCurrentContainer->GetChildAtOffset(mCurrentStartOffset - 1);
+
+ // If there is a link before this offset, mutate into it,
+ // and set the offset to its last character.
+ if (link && link->IsHyperText()) {
+ mCurrentContainer = link->AsHyperText();
+ mCurrentStartOffset = mCurrentContainer->CharacterCount();
+
+ // Call NormalizeBackward recursively to get top-most top-most embedding
+ // ancestor if at the beginning of one, or innermost link if at the end.
+ NormalizeBackward();
+ return true;
+ }
+
+ if (mCurrentContainer->IsHTMLListItem() &&
+ mCurrentContainer->AsHTMLListItem()->Bullet() == link) {
+ mCurrentStartOffset = 0;
+ NormalizeBackward();
+ return true;
+ }
+ }
+
+ return false;
+}
+
+int32_t HyperTextIterator::SegmentLength() {
+ int32_t endOffset = mCurrentEndOffset < 0
+ ? mCurrentContainer->CharacterCount()
+ : mCurrentEndOffset;
+
+ return endOffset - mCurrentStartOffset;
+}
+
+int32_t HyperTextIterator::NextLinkOffset() {
+ int32_t linkCount = mCurrentContainer->LinkCount();
+ for (int32_t i = 0; i < linkCount; i++) {
+ LocalAccessible* link = mCurrentContainer->LinkAt(i);
+ MOZ_ASSERT(link);
+ int32_t linkStartOffset = link->StartOffset();
+ if (mCurrentStartOffset < linkStartOffset) {
+ return linkStartOffset;
+ }
+ }
+
+ return -1;
+}
+
+bool HyperTextIterator::Next() {
+ if (!mCurrentContainer->Document()->HasLoadState(
+ DocAccessible::eTreeConstructed)) {
+ // If the accessible tree is still being constructed the text tree
+ // is not in a traversable state yet.
+ return false;
+ }
+
+ if (mCurrentContainer == mEndContainer &&
+ (mCurrentEndOffset == -1 || mEndOffset <= mCurrentEndOffset)) {
+ return false;
+ } else {
+ mCurrentStartOffset = mCurrentEndOffset;
+ NormalizeForward();
+ }
+
+ int32_t nextLinkOffset = NextLinkOffset();
+ if (mCurrentContainer == mEndContainer &&
+ (nextLinkOffset == -1 || nextLinkOffset > mEndOffset)) {
+ mCurrentEndOffset =
+ mEndOffset < 0 ? mEndContainer->CharacterCount() : mEndOffset;
+ } else {
+ mCurrentEndOffset = nextLinkOffset < 0 ? mCurrentContainer->CharacterCount()
+ : nextLinkOffset;
+ }
+
+ return mCurrentStartOffset != mCurrentEndOffset;
+}
+
+void HyperTextAccessibleWrap::TextForRange(nsAString& aText,
+ int32_t aStartOffset,
+ HyperTextAccessible* aEndContainer,
+ int32_t aEndOffset) {
+ if (IsHTMLListItem()) {
+ LocalAccessible* maybeBullet = GetChildAtOffset(aStartOffset - 1);
+ if (maybeBullet) {
+ LocalAccessible* bullet = AsHTMLListItem()->Bullet();
+ if (maybeBullet == bullet) {
+ TextSubstring(0, nsAccUtils::TextLength(bullet), aText);
+ }
+ }
+ }
+
+ HyperTextIterator iter(this, aStartOffset, aEndContainer, aEndOffset);
+ while (iter.Next()) {
+ nsAutoString text;
+ iter.mCurrentContainer->TextSubstring(iter.mCurrentStartOffset,
+ iter.mCurrentEndOffset, text);
+ aText.Append(text);
+ }
+}
+
+void HyperTextAccessibleWrap::AttributedTextForRange(
+ nsTArray<nsString>& aStrings, nsTArray<RefPtr<AccAttributes>>& aProperties,
+ nsTArray<LocalAccessible*>& aContainers, int32_t aStartOffset,
+ HyperTextAccessible* aEndContainer, int32_t aEndOffset) {
+ if (IsHTMLListItem()) {
+ LocalAccessible* maybeBullet = GetChildAtOffset(aStartOffset - 1);
+ if (maybeBullet) {
+ LocalAccessible* bullet = AsHTMLListItem()->Bullet();
+ if (maybeBullet == bullet) {
+ nsAutoString text;
+ TextSubstring(0, nsAccUtils::TextLength(bullet), text);
+
+ int32_t unusedAttrStartOffset, unusedAttrEndOffset;
+ RefPtr<AccAttributes> props =
+ TextAttributes(true, aStartOffset - 1, &unusedAttrStartOffset,
+ &unusedAttrEndOffset);
+
+ aStrings.AppendElement(text);
+ aProperties.AppendElement(props);
+ aContainers.AppendElement(this);
+ }
+ }
+ }
+
+ HyperTextIterator iter(this, aStartOffset, aEndContainer, aEndOffset);
+ while (iter.Next()) {
+ int32_t attrStartOffset = 0;
+ int32_t attrEndOffset = iter.mCurrentStartOffset;
+ do {
+ int32_t oldEndOffset = attrEndOffset;
+ RefPtr<AccAttributes> props = iter.mCurrentContainer->TextAttributes(
+ true, attrEndOffset, &attrStartOffset, &attrEndOffset);
+
+ if (oldEndOffset == attrEndOffset) {
+ MOZ_ASSERT_UNREACHABLE("new attribute end offset should be different");
+ break;
+ }
+
+ nsAutoString text;
+ iter.mCurrentContainer->TextSubstring(
+ attrStartOffset < iter.mCurrentStartOffset ? iter.mCurrentStartOffset
+ : attrStartOffset,
+ attrEndOffset < iter.mCurrentEndOffset ? attrEndOffset
+ : iter.mCurrentEndOffset,
+ text);
+
+ aStrings.AppendElement(text);
+ aProperties.AppendElement(props);
+ aContainers.AppendElement(iter.mCurrentContainer);
+ } while (attrEndOffset < iter.mCurrentEndOffset);
+ }
+}
+
+LayoutDeviceIntRect HyperTextAccessibleWrap::BoundsForRange(
+ int32_t aStartOffset, HyperTextAccessible* aEndContainer,
+ int32_t aEndOffset) {
+ LayoutDeviceIntRect rect;
+ HyperTextIterator iter(this, aStartOffset, aEndContainer, aEndOffset);
+ while (iter.Next()) {
+ LayoutDeviceIntRect stringRect = iter.mCurrentContainer->TextBounds(
+ iter.mCurrentStartOffset, iter.mCurrentEndOffset);
+ rect.UnionRect(rect, stringRect);
+ }
+
+ return rect;
+}
+
+int32_t HyperTextAccessibleWrap::LengthForRange(
+ int32_t aStartOffset, HyperTextAccessible* aEndContainer,
+ int32_t aEndOffset) {
+ int32_t length = 0;
+ HyperTextIterator iter(this, aStartOffset, aEndContainer, aEndOffset);
+ while (iter.Next()) {
+ length += iter.SegmentLength();
+ }
+
+ return length;
+}
+
+void HyperTextAccessibleWrap::OffsetAtIndex(int32_t aIndex,
+ HyperTextAccessible** aContainer,
+ int32_t* aOffset) {
+ int32_t index = aIndex;
+ HyperTextIterator iter(this, 0, this, CharacterCount());
+ while (iter.Next()) {
+ int32_t segmentLength = iter.SegmentLength();
+ if (index <= segmentLength) {
+ *aContainer = iter.mCurrentContainer;
+ *aOffset = iter.mCurrentStartOffset + index;
+ break;
+ }
+ index -= segmentLength;
+ }
+}
+
+void HyperTextAccessibleWrap::RangeAt(int32_t aOffset, EWhichRange aRangeType,
+ HyperTextAccessible** aStartContainer,
+ int32_t* aStartOffset,
+ HyperTextAccessible** aEndContainer,
+ int32_t* aEndOffset) {
+ switch (aRangeType) {
+ case EWhichRange::eLeftWord:
+ LeftWordAt(aOffset, aStartContainer, aStartOffset, aEndContainer,
+ aEndOffset);
+ break;
+ case EWhichRange::eRightWord:
+ RightWordAt(aOffset, aStartContainer, aStartOffset, aEndContainer,
+ aEndOffset);
+ break;
+ case EWhichRange::eLine:
+ case EWhichRange::eLeftLine:
+ LineAt(aOffset, false, aStartContainer, aStartOffset, aEndContainer,
+ aEndOffset);
+ break;
+ case EWhichRange::eRightLine:
+ LineAt(aOffset, true, aStartContainer, aStartOffset, aEndContainer,
+ aEndOffset);
+ break;
+ case EWhichRange::eParagraph:
+ ParagraphAt(aOffset, aStartContainer, aStartOffset, aEndContainer,
+ aEndOffset);
+ break;
+ case EWhichRange::eStyle:
+ StyleAt(aOffset, aStartContainer, aStartOffset, aEndContainer,
+ aEndOffset);
+ break;
+ default:
+ break;
+ }
+}
+
+void HyperTextAccessibleWrap::LeftWordAt(int32_t aOffset,
+ HyperTextAccessible** aStartContainer,
+ int32_t* aStartOffset,
+ HyperTextAccessible** aEndContainer,
+ int32_t* aEndOffset) {
+ TextPoint here(this, aOffset);
+ TextPoint start =
+ FindTextPoint(aOffset, eDirPrevious, eSelectWord, eStartWord);
+ if (!start.mContainer) {
+ return;
+ }
+
+ auto* startContainer = static_cast<HyperTextAccessibleWrap*>(
+ start.mContainer->AsLocal()->AsHyperText());
+ if ((NativeState() & states::EDITABLE) &&
+ !(startContainer->NativeState() & states::EDITABLE)) {
+ // The word search crossed an editable boundary. Return the first word of
+ // the editable root.
+ return EditableRoot()->RightWordAt(0, aStartContainer, aStartOffset,
+ aEndContainer, aEndOffset);
+ }
+
+ TextPoint end = startContainer->FindTextPoint(start.mOffset, eDirNext,
+ eSelectWord, eEndWord);
+ if (end < here) {
+ *aStartContainer = end.mContainer->AsLocal()->AsHyperText();
+ *aEndContainer = here.mContainer->AsLocal()->AsHyperText();
+ *aStartOffset = end.mOffset;
+ *aEndOffset = here.mOffset;
+ } else {
+ *aStartContainer = startContainer;
+ *aEndContainer = end.mContainer->AsLocal()->AsHyperText();
+ *aStartOffset = start.mOffset;
+ *aEndOffset = end.mOffset;
+ }
+}
+
+void HyperTextAccessibleWrap::RightWordAt(int32_t aOffset,
+ HyperTextAccessible** aStartContainer,
+ int32_t* aStartOffset,
+ HyperTextAccessible** aEndContainer,
+ int32_t* aEndOffset) {
+ TextPoint here(this, aOffset);
+ TextPoint end = FindTextPoint(aOffset, eDirNext, eSelectWord, eEndWord);
+ if (!end.mContainer || end < here || here == end) {
+ // If we didn't find a word end, or if we wrapped around (bug 1652833),
+ // return with no result.
+ return;
+ }
+
+ auto* endContainer = static_cast<HyperTextAccessibleWrap*>(
+ end.mContainer->AsLocal()->AsHyperText());
+ if ((NativeState() & states::EDITABLE) &&
+ !(endContainer->NativeState() & states::EDITABLE)) {
+ // The word search crossed an editable boundary. Return with no result.
+ return;
+ }
+
+ TextPoint start = endContainer->FindTextPoint(end.mOffset, eDirPrevious,
+ eSelectWord, eStartWord);
+
+ if (here < start) {
+ *aStartContainer = here.mContainer->AsLocal()->AsHyperText();
+ *aEndContainer = start.mContainer->AsLocal()->AsHyperText();
+ *aStartOffset = here.mOffset;
+ *aEndOffset = start.mOffset;
+ } else {
+ *aStartContainer = start.mContainer->AsLocal()->AsHyperText();
+ *aEndContainer = endContainer;
+ *aStartOffset = start.mOffset;
+ *aEndOffset = end.mOffset;
+ }
+}
+
+void HyperTextAccessibleWrap::LineAt(int32_t aOffset, bool aNextLine,
+ HyperTextAccessible** aStartContainer,
+ int32_t* aStartOffset,
+ HyperTextAccessible** aEndContainer,
+ int32_t* aEndOffset) {
+ TextPoint here(this, aOffset);
+ TextPoint end =
+ FindTextPoint(aOffset, eDirNext, eSelectEndLine, eDefaultBehavior);
+ if (!end.mContainer || end < here) {
+ // If we didn't find a word end, or if we wrapped around (bug 1652833),
+ // return with no result.
+ return;
+ }
+
+ auto* endContainer = static_cast<HyperTextAccessibleWrap*>(
+ end.mContainer->AsLocal()->AsHyperText());
+ TextPoint start = endContainer->FindTextPoint(
+ end.mOffset, eDirPrevious, eSelectBeginLine, eDefaultBehavior);
+
+ if (!aNextLine && here < start) {
+ start = FindTextPoint(aOffset, eDirPrevious, eSelectBeginLine,
+ eDefaultBehavior);
+ if (!start.mContainer) {
+ return;
+ }
+
+ auto* startContainer = static_cast<HyperTextAccessibleWrap*>(
+ start.mContainer->AsLocal()->AsHyperText());
+ end = startContainer->FindTextPoint(start.mOffset, eDirNext, eSelectEndLine,
+ eDefaultBehavior);
+ }
+
+ *aStartContainer = start.mContainer->AsLocal()->AsHyperText();
+ *aEndContainer = end.mContainer->AsLocal()->AsHyperText();
+ *aStartOffset = start.mOffset;
+ *aEndOffset = end.mOffset;
+}
+
+void HyperTextAccessibleWrap::ParagraphAt(int32_t aOffset,
+ HyperTextAccessible** aStartContainer,
+ int32_t* aStartOffset,
+ HyperTextAccessible** aEndContainer,
+ int32_t* aEndOffset) {
+ TextPoint here(this, aOffset);
+ TextPoint end =
+ FindTextPoint(aOffset, eDirNext, eSelectParagraph, eDefaultBehavior);
+
+ if (!end.mContainer || end < here) {
+ // If we didn't find a word end, or if we wrapped around (bug 1652833),
+ // return with no result.
+ return;
+ }
+
+ if (end.mOffset == -1 && LocalParent() && LocalParent()->IsHyperText()) {
+ // If end offset is -1 we didn't find a paragraph boundary.
+ // This must be an inline container, go to its parent to
+ // retrieve paragraph boundaries.
+ static_cast<HyperTextAccessibleWrap*>(LocalParent()->AsHyperText())
+ ->ParagraphAt(StartOffset(), aStartContainer, aStartOffset,
+ aEndContainer, aEndOffset);
+ return;
+ }
+
+ auto* endContainer = static_cast<HyperTextAccessibleWrap*>(
+ end.mContainer->AsLocal()->AsHyperText());
+ TextPoint start = static_cast<HyperTextAccessibleWrap*>(endContainer)
+ ->FindTextPoint(end.mOffset, eDirPrevious,
+ eSelectParagraph, eDefaultBehavior);
+
+ *aStartContainer = start.mContainer->AsLocal()->AsHyperText();
+ *aEndContainer = endContainer;
+ *aStartOffset = start.mOffset;
+ *aEndOffset = end.mOffset;
+}
+
+void HyperTextAccessibleWrap::StyleAt(int32_t aOffset,
+ HyperTextAccessible** aStartContainer,
+ int32_t* aStartOffset,
+ HyperTextAccessible** aEndContainer,
+ int32_t* aEndOffset) {
+ // Get the range of the text leaf at this offset.
+ // A text leaf represents a stretch of like-styled text.
+ auto leaf = LeafAtOffset(aOffset);
+ if (!leaf) {
+ return;
+ }
+
+ MOZ_ASSERT(leaf->LocalParent()->IsHyperText());
+ HyperTextAccessibleWrap* container =
+ static_cast<HyperTextAccessibleWrap*>(leaf->LocalParent()->AsHyperText());
+ if (!container) {
+ return;
+ }
+
+ *aStartContainer = *aEndContainer = container;
+ container->RangeOfChild(leaf, aStartOffset, aEndOffset);
+}
+
+void HyperTextAccessibleWrap::NextClusterAt(
+ int32_t aOffset, HyperTextAccessible** aNextContainer,
+ int32_t* aNextOffset) {
+ TextPoint here(this, aOffset);
+ TextPoint next =
+ FindTextPoint(aOffset, eDirNext, eSelectCluster, eDefaultBehavior);
+
+ if ((next.mOffset == nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT &&
+ next.mContainer == Document()) ||
+ (next < here)) {
+ // If we reached the end of the doc, or if we wrapped to the start of the
+ // doc return given offset as-is.
+ *aNextContainer = this;
+ *aNextOffset = aOffset;
+ } else {
+ *aNextContainer = next.mContainer->AsLocal()->AsHyperText();
+ *aNextOffset = next.mOffset;
+ }
+}
+
+void HyperTextAccessibleWrap::PreviousClusterAt(
+ int32_t aOffset, HyperTextAccessible** aPrevContainer,
+ int32_t* aPrevOffset) {
+ TextPoint prev =
+ FindTextPoint(aOffset, eDirPrevious, eSelectCluster, eDefaultBehavior);
+ *aPrevContainer = prev.mContainer->AsLocal()->AsHyperText();
+ *aPrevOffset = prev.mOffset;
+}
+
+void HyperTextAccessibleWrap::RangeOfChild(LocalAccessible* aChild,
+ int32_t* aStartOffset,
+ int32_t* aEndOffset) {
+ MOZ_ASSERT(aChild->LocalParent() == this);
+ *aStartOffset = *aEndOffset = -1;
+ int32_t index = GetIndexOf(aChild);
+ if (index != -1) {
+ *aStartOffset = GetChildOffset(index);
+ // If this is the last child index + 1 will return the total
+ // chracter count.
+ *aEndOffset = GetChildOffset(index + 1);
+ }
+}
+
+LocalAccessible* HyperTextAccessibleWrap::LeafAtOffset(int32_t aOffset) {
+ HyperTextAccessible* text = this;
+ LocalAccessible* child = nullptr;
+ // The offset needed should "attach" the previous accessible if
+ // in between two accessibles.
+ int32_t innerOffset = aOffset > 0 ? aOffset - 1 : aOffset;
+ do {
+ int32_t childIdx = text->GetChildIndexAtOffset(innerOffset);
+ if (childIdx == -1) {
+ return text;
+ }
+
+ child = text->LocalChildAt(childIdx);
+ if (!child || nsAccUtils::MustPrune(text)) {
+ return text;
+ }
+
+ innerOffset -= text->GetChildOffset(childIdx);
+
+ text = child->AsHyperText();
+ } while (text);
+
+ return child;
+}
+
+void HyperTextAccessibleWrap::SelectRange(int32_t aStartOffset,
+ HyperTextAccessible* aEndContainer,
+ int32_t aEndOffset) {
+ TextRange range(this, this, aStartOffset, aEndContainer, aEndOffset);
+ range.SetSelectionAt(0);
+}
+
+TextPoint HyperTextAccessibleWrap::FindTextPoint(
+ int32_t aOffset, nsDirection aDirection, nsSelectionAmount aAmount,
+ EWordMovementType aWordMovementType) {
+ // Layout can remain trapped in an editable. We normalize out of
+ // it if we are in its last offset.
+ HyperTextIterator iter(this, aOffset, this, CharacterCount());
+ if (aDirection == eDirNext) {
+ iter.NormalizeForward();
+ } else {
+ iter.NormalizeBackward();
+ }
+
+ // Find a leaf accessible frame to start with. PeekOffset wants this.
+ HyperTextAccessible* text = iter.mCurrentContainer;
+ LocalAccessible* child = nullptr;
+ int32_t innerOffset = iter.mCurrentStartOffset;
+
+ do {
+ int32_t childIdx = text->GetChildIndexAtOffset(innerOffset);
+
+ // We can have an empty text leaf as our only child. Since empty text
+ // leaves are not accessible we then have no children, but 0 is a valid
+ // innerOffset.
+ if (childIdx == -1) {
+ NS_ASSERTION(innerOffset == 0 && !text->ChildCount(), "No childIdx?");
+ return TextPoint(text, 0);
+ }
+
+ child = text->LocalChildAt(childIdx);
+ if (child->IsHyperText() && !child->ChildCount()) {
+ // If this is a childless hypertext, jump to its
+ // previous or next sibling, depending on
+ // direction.
+ if (aDirection == eDirPrevious && childIdx > 0) {
+ child = text->LocalChildAt(--childIdx);
+ } else if (aDirection == eDirNext &&
+ childIdx + 1 < static_cast<int32_t>(text->ChildCount())) {
+ child = text->LocalChildAt(++childIdx);
+ }
+ }
+
+ int32_t childOffset = text->GetChildOffset(childIdx);
+
+ if (child->IsHyperText() && aDirection == eDirPrevious && childIdx > 0 &&
+ innerOffset - childOffset == 0) {
+ // If we are searching backwards, and this is the begining of a
+ // segment, get the previous sibling so that layout will start
+ // its search there.
+ childIdx--;
+ innerOffset -= text->GetChildOffset(childIdx);
+ child = text->LocalChildAt(childIdx);
+ } else {
+ innerOffset -= childOffset;
+ }
+
+ text = child->AsHyperText();
+ } while (text);
+
+ nsIFrame* childFrame = child->GetFrame();
+ if (!childFrame) {
+ NS_ERROR("No child frame");
+ return TextPoint(this, aOffset);
+ }
+
+ int32_t innerContentOffset = innerOffset;
+ if (child->IsTextLeaf()) {
+ NS_ASSERTION(childFrame->IsTextFrame(), "Wrong frame!");
+ RenderedToContentOffset(childFrame, innerOffset, &innerContentOffset);
+ }
+
+ nsIFrame* frameAtOffset = childFrame;
+ int32_t offsetInFrame = 0;
+ childFrame->GetChildFrameContainingOffset(innerContentOffset, true,
+ &offsetInFrame, &frameAtOffset);
+ if (aDirection == eDirPrevious && offsetInFrame == 0) {
+ // If we are searching backwards, and we are at the start of a frame,
+ // get the previous continuation frame.
+ if (nsIFrame* prevInContinuation = frameAtOffset->GetPrevContinuation()) {
+ frameAtOffset = prevInContinuation;
+ }
+ }
+
+ const bool kIsJumpLinesOk = true; // okay to jump lines
+ const bool kIsScrollViewAStop = false; // do not stop at scroll views
+ const bool kIsKeyboardSelect = true; // is keyboard selection
+ const bool kIsVisualBidi = false; // use visual order for bidi text
+ nsPeekOffsetStruct pos(
+ aAmount, aDirection, innerContentOffset, nsPoint(0, 0), kIsJumpLinesOk,
+ kIsScrollViewAStop, kIsKeyboardSelect, kIsVisualBidi, false,
+ nsPeekOffsetStruct::ForceEditableRegion::No, aWordMovementType, false);
+ nsresult rv = frameAtOffset->PeekOffset(&pos);
+
+ // PeekOffset fails on last/first lines of the text in certain cases.
+ if (NS_FAILED(rv) && aAmount == eSelectLine) {
+ pos.mAmount = aDirection == eDirNext ? eSelectEndLine : eSelectBeginLine;
+ frameAtOffset->PeekOffset(&pos);
+ }
+ if (!pos.mResultContent) {
+ NS_ERROR("No result content!");
+ return TextPoint(this, aOffset);
+ }
+
+ if (aDirection == eDirNext &&
+ nsContentUtils::PositionIsBefore(pos.mResultContent, mContent, nullptr,
+ nullptr)) {
+ // Bug 1652833 makes us sometimes return the first element on the doc.
+ return TextPoint(Document(), nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT);
+ }
+
+ HyperTextAccessible* container =
+ nsAccUtils::GetTextContainer(pos.mResultContent);
+ int32_t offset = container ? container->DOMPointToOffset(
+ pos.mResultContent, pos.mContentOffset,
+ aDirection == eDirNext)
+ : 0;
+ return TextPoint(container, offset);
+}
+
+HyperTextAccessibleWrap* HyperTextAccessibleWrap::EditableRoot() {
+ LocalAccessible* editable = nullptr;
+ for (LocalAccessible* acc = this; acc && acc != Document();
+ acc = acc->LocalParent()) {
+ if (acc->NativeState() & states::EDITABLE) {
+ editable = acc;
+ } else {
+ break;
+ }
+ }
+
+ return static_cast<HyperTextAccessibleWrap*>(editable->AsHyperText());
+}
diff --git a/accessible/mac/MOXAccessibleBase.h b/accessible/mac/MOXAccessibleBase.h
new file mode 100644
index 0000000000..751fa5f28d
--- /dev/null
+++ b/accessible/mac/MOXAccessibleBase.h
@@ -0,0 +1,143 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import <Cocoa/Cocoa.h>
+
+#import "mozAccessibleProtocol.h"
+#import "MOXAccessibleProtocol.h"
+
+#include "Platform.h"
+
+inline id<mozAccessible> GetObjectOrRepresentedView(id<mozAccessible> aObject) {
+ if (!mozilla::a11y::ShouldA11yBeEnabled()) {
+ // If platform a11y is not enabled, don't return represented view.
+ // This is mostly for our mochitest environment because the represented
+ // ChildView checks `ShouldA11yBeEnabled` before proxying accessibility
+ // methods to mozAccessibles.
+ return aObject;
+ }
+
+ return [aObject hasRepresentedView] ? [aObject representedView] : aObject;
+}
+
+@interface MOXAccessibleBase : NSObject <mozAccessible, MOXAccessible> {
+ BOOL mIsExpired;
+}
+
+#pragma mark - mozAccessible/widget
+
+// override
+- (BOOL)hasRepresentedView;
+
+// override
+- (id)representedView;
+
+// override
+- (BOOL)isRoot;
+
+#pragma mark - mozAccessible/NSAccessibility
+
+// The methods below interface with the platform through NSAccessibility.
+// They should not be called directly or overridden in subclasses.
+
+// override, final
+- (NSArray*)accessibilityAttributeNames;
+
+// override, final
+- (id)accessibilityAttributeValue:(NSString*)attribute;
+
+// override, final
+- (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute;
+
+// override, final
+- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute;
+
+// override, final
+- (NSArray*)accessibilityActionNames;
+
+// override, final
+- (void)accessibilityPerformAction:(NSString*)action;
+
+// override, final
+- (NSString*)accessibilityActionDescription:(NSString*)action;
+
+// override, final
+- (NSArray*)accessibilityParameterizedAttributeNames;
+
+// override, final
+- (id)accessibilityAttributeValue:(NSString*)attribute
+ forParameter:(id)parameter;
+
+// override, final
+- (id)accessibilityHitTest:(NSPoint)point;
+
+// override, final
+- (id)accessibilityFocusedUIElement;
+
+// override, final
+- (BOOL)isAccessibilityElement;
+
+// final
+- (BOOL)accessibilityNotifiesWhenDestroyed;
+
+#pragma mark - MOXAccessible protocol
+
+// override
+- (id)moxHitTest:(NSPoint)point;
+
+// override
+- (id)moxFocusedUIElement;
+
+// override
+- (void)moxPostNotification:(NSString*)notification;
+
+// override
+- (void)moxPostNotification:(NSString*)notification
+ withUserInfo:(NSDictionary*)userInfo;
+
+// override
+- (BOOL)moxBlockSelector:(SEL)selector;
+
+// override
+- (NSArray*)moxUnignoredChildren;
+
+// override
+- (NSArray*)moxChildren;
+
+// override
+- (id<mozAccessible>)moxUnignoredParent;
+
+// override
+- (id<mozAccessible>)moxParent;
+
+// override
+- (NSNumber*)moxIndexForChildUIElement:(id)child;
+
+// override
+- (id)moxTopLevelUIElement;
+
+// override
+- (id<MOXTextMarkerSupport>)moxTextMarkerDelegate;
+
+// override
+- (BOOL)moxIsLiveRegion;
+
+// override
+- (id<MOXAccessible>)moxFindAncestor:(BOOL (^)(id<MOXAccessible> moxAcc,
+ BOOL* stop))findBlock;
+
+#pragma mark -
+
+- (NSString*)description;
+
+- (BOOL)isExpired;
+
+// makes ourselves "expired". after this point, we might be around if someone
+// has retained us (e.g., a third-party), but we really contain no information.
+- (void)expire;
+
+@end
diff --git a/accessible/mac/MOXAccessibleBase.mm b/accessible/mac/MOXAccessibleBase.mm
new file mode 100644
index 0000000000..098a248a03
--- /dev/null
+++ b/accessible/mac/MOXAccessibleBase.mm
@@ -0,0 +1,573 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "MOXAccessibleBase.h"
+
+#import "MacSelectorMap.h"
+
+#include "nsObjCExceptions.h"
+#include "xpcAccessibleMacInterface.h"
+#include "mozilla/Logging.h"
+
+using namespace mozilla::a11y;
+
+#undef LOG
+mozilla::LogModule* GetMacAccessibilityLog() {
+ static mozilla::LazyLogModule sLog("MacAccessibility");
+
+ return sLog;
+}
+#define LOG(type, format, ...) \
+ do { \
+ if (MOZ_LOG_TEST(GetMacAccessibilityLog(), type)) { \
+ NSString* msg = [NSString stringWithFormat:(format), ##__VA_ARGS__]; \
+ MOZ_LOG(GetMacAccessibilityLog(), type, ("%s", [msg UTF8String])); \
+ } \
+ } while (0)
+
+@interface NSObject (MOXAccessible)
+
+// This NSObject conforms to MOXAccessible.
+// This is needed to we know to mutate the value
+// (get represented view, check isAccessibilityElement)
+// before forwarding it to NSAccessibility.
+- (BOOL)isMOXAccessible;
+
+// Same as above, but this checks if the NSObject is an array with
+// mozAccessible conforming objects.
+- (BOOL)hasMOXAccessibles;
+
+@end
+
+@implementation NSObject (MOXAccessible)
+
+- (BOOL)isMOXAccessible {
+ return [self conformsToProtocol:@protocol(MOXAccessible)];
+}
+
+- (BOOL)hasMOXAccessibles {
+ return [self isKindOfClass:[NSArray class]] &&
+ [[(NSArray*)self firstObject] isMOXAccessible];
+}
+
+@end
+
+// Private methods
+@interface MOXAccessibleBase ()
+
+- (BOOL)isSelectorSupported:(SEL)selector;
+
+@end
+
+@implementation MOXAccessibleBase
+
+#pragma mark - mozAccessible/widget
+
+- (BOOL)hasRepresentedView {
+ return NO;
+}
+
+- (id)representedView {
+ return nil;
+}
+
+- (BOOL)isRoot {
+ return NO;
+}
+
+#pragma mark - mozAccessible/NSAccessibility
+
+- (NSArray*)accessibilityAttributeNames {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if ([self isExpired]) {
+ return nil;
+ }
+
+ static NSMutableDictionary* attributesForEachClass = nil;
+
+ if (!attributesForEachClass) {
+ attributesForEachClass = [[NSMutableDictionary alloc] init];
+ }
+
+ NSMutableArray* attributes =
+ attributesForEachClass [[self class]]
+ ?: [[[NSMutableArray alloc] init] autorelease];
+
+ NSDictionary* getters = mac::AttributeGetters();
+ if (![attributes count]) {
+ // Go through all our attribute getters, if they are supported by this class
+ // advertise the attribute name.
+ for (NSString* attribute in getters) {
+ SEL selector = NSSelectorFromString(getters[attribute]);
+ if ([self isSelectorSupported:selector]) {
+ [attributes addObject:attribute];
+ }
+ }
+
+ // If we have a delegate add all the text marker attributes.
+ if ([self moxTextMarkerDelegate]) {
+ [attributes addObjectsFromArray:[mac::TextAttributeGetters() allKeys]];
+ }
+
+ // We store a hash table with types as keys, and atttribute lists as values.
+ // This lets us cache the atttribute list of each subclass so we only
+ // need to gather its MOXAccessible methods once.
+ // XXX: Uncomment when accessibilityAttributeNames is removed from all
+ // subclasses. attributesForEachClass[[self class]] = attributes;
+ }
+
+ return attributes;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (id)accessibilityAttributeValue:(NSString*)attribute {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+ if ([self isExpired]) {
+ return nil;
+ }
+
+ id value = nil;
+ NSDictionary* getters = mac::AttributeGetters();
+ if (getters[attribute]) {
+ SEL selector = NSSelectorFromString(getters[attribute]);
+ if ([self isSelectorSupported:selector]) {
+ value = [self performSelector:selector];
+ }
+ } else if (id textMarkerDelegate = [self moxTextMarkerDelegate]) {
+ // If we have a delegate, check if attribute is a text marker
+ // attribute and call the associated selector on the delegate
+ // if so.
+ NSDictionary* textMarkerGetters = mac::TextAttributeGetters();
+ if (textMarkerGetters[attribute]) {
+ SEL selector = NSSelectorFromString(textMarkerGetters[attribute]);
+ if ([textMarkerDelegate respondsToSelector:selector]) {
+ value = [textMarkerDelegate performSelector:selector];
+ }
+ }
+ }
+
+ if ([value isMOXAccessible]) {
+ // If this is a MOXAccessible, get its represented view or filter it if
+ // it should be ignored.
+ value = [value isAccessibilityElement] ? GetObjectOrRepresentedView(value)
+ : nil;
+ }
+
+ if ([value hasMOXAccessibles]) {
+ // If this is an array of mozAccessibles, get each element's represented
+ // view and remove it from the returned array if it should be ignored.
+ NSUInteger arrSize = [value count];
+ NSMutableArray* arr =
+ [[[NSMutableArray alloc] initWithCapacity:arrSize] autorelease];
+ for (NSUInteger i = 0; i < arrSize; i++) {
+ id<mozAccessible> mozAcc = GetObjectOrRepresentedView(value[i]);
+ if ([mozAcc isAccessibilityElement]) {
+ [arr addObject:mozAcc];
+ }
+ }
+
+ value = arr;
+ }
+
+ if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Debug)) {
+ if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Verbose)) {
+ LOG(LogLevel::Verbose, @"%@ attributeValue %@ => %@", self, attribute,
+ value);
+ } else if (![attribute isEqualToString:@"AXParent"] &&
+ ![attribute isEqualToString:@"AXRole"] &&
+ ![attribute isEqualToString:@"AXSubrole"] &&
+ ![attribute isEqualToString:@"AXSize"] &&
+ ![attribute isEqualToString:@"AXPosition"] &&
+ ![attribute isEqualToString:@"AXRole"] &&
+ ![attribute isEqualToString:@"AXChildren"]) {
+ LOG(LogLevel::Debug, @"%@ attributeValue %@", self, attribute);
+ }
+ }
+
+ return value;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if ([self isExpired]) {
+ return NO;
+ }
+
+ NSDictionary* setters = mac::AttributeSetters();
+ if (setters[attribute]) {
+ SEL selector = NSSelectorFromString(setters[attribute]);
+ if ([self isSelectorSupported:selector]) {
+ return YES;
+ }
+ } else if (id textMarkerDelegate = [self moxTextMarkerDelegate]) {
+ // If we have a delegate, check text setters on delegate
+ NSDictionary* textMarkerSetters = mac::TextAttributeSetters();
+ if (textMarkerSetters[attribute]) {
+ SEL selector = NSSelectorFromString(textMarkerSetters[attribute]);
+ if ([textMarkerDelegate respondsToSelector:selector]) {
+ return YES;
+ }
+ }
+ }
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NO);
+}
+
+- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if ([self isExpired]) {
+ return;
+ }
+
+ LOG(LogLevel::Debug, @"%@ setValueForattribute %@ = %@", self, attribute,
+ value);
+
+ NSDictionary* setters = mac::AttributeSetters();
+ if (setters[attribute]) {
+ SEL selector = NSSelectorFromString(setters[attribute]);
+ if ([self isSelectorSupported:selector]) {
+ [self performSelector:selector withObject:value];
+ }
+ } else if (id textMarkerDelegate = [self moxTextMarkerDelegate]) {
+ // If we have a delegate, check if attribute is a text marker
+ // attribute and call the associated selector on the delegate
+ // if so.
+ NSDictionary* textMarkerSetters = mac::TextAttributeSetters();
+ if (textMarkerSetters[attribute]) {
+ SEL selector = NSSelectorFromString(textMarkerSetters[attribute]);
+ if ([textMarkerDelegate respondsToSelector:selector]) {
+ [textMarkerDelegate performSelector:selector withObject:value];
+ }
+ }
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (NSArray*)accessibilityActionNames {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if ([self isExpired]) {
+ return nil;
+ }
+
+ NSMutableArray* actionNames = [[[NSMutableArray alloc] init] autorelease];
+
+ NSDictionary* actions = mac::Actions();
+ for (NSString* action in actions) {
+ SEL selector = NSSelectorFromString(actions[action]);
+ if ([self isSelectorSupported:selector]) {
+ [actionNames addObject:action];
+ }
+ }
+
+ return actionNames;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (void)accessibilityPerformAction:(NSString*)action {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if ([self isExpired]) {
+ return;
+ }
+
+ LOG(LogLevel::Debug, @"%@ performAction %@ ", self, action);
+
+ NSDictionary* actions = mac::Actions();
+ if (actions[action]) {
+ SEL selector = NSSelectorFromString(actions[action]);
+ if ([self isSelectorSupported:selector]) {
+ [self performSelector:selector];
+ }
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (NSString*)accessibilityActionDescription:(NSString*)action {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+ // by default we return whatever the MacOS API know about.
+ // if you have custom actions, override.
+ return NSAccessibilityActionDescription(action);
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (NSArray*)accessibilityParameterizedAttributeNames {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if ([self isExpired]) {
+ return nil;
+ }
+
+ NSMutableArray* attributeNames = [[[NSMutableArray alloc] init] autorelease];
+
+ NSDictionary* attributes = mac::ParameterizedAttributeGetters();
+ for (NSString* attribute in attributes) {
+ SEL selector = NSSelectorFromString(attributes[attribute]);
+ if ([self isSelectorSupported:selector]) {
+ [attributeNames addObject:attribute];
+ }
+ }
+
+ // If we have a delegate add all the text marker attributes.
+ if ([self moxTextMarkerDelegate]) {
+ [attributeNames
+ addObjectsFromArray:[mac::ParameterizedTextAttributeGetters() allKeys]];
+ }
+
+ return attributeNames;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (id)accessibilityAttributeValue:(NSString*)attribute
+ forParameter:(id)parameter {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if ([self isExpired]) {
+ return nil;
+ }
+
+ id value = nil;
+
+ NSDictionary* getters = mac::ParameterizedAttributeGetters();
+ if (getters[attribute]) {
+ SEL selector = NSSelectorFromString(getters[attribute]);
+ if ([self isSelectorSupported:selector]) {
+ value = [self performSelector:selector withObject:parameter];
+ }
+ } else if (id textMarkerDelegate = [self moxTextMarkerDelegate]) {
+ // If we have a delegate, check if attribute is a text marker
+ // attribute and call the associated selector on the delegate
+ // if so.
+ NSDictionary* textMarkerGetters = mac::ParameterizedTextAttributeGetters();
+ if (textMarkerGetters[attribute]) {
+ SEL selector = NSSelectorFromString(textMarkerGetters[attribute]);
+ if ([textMarkerDelegate respondsToSelector:selector]) {
+ value = [textMarkerDelegate performSelector:selector
+ withObject:parameter];
+ }
+ }
+ }
+
+ if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Verbose)) {
+ LOG(LogLevel::Verbose, @"%@ attributeValueForParam %@(%@) => %@", self,
+ attribute, parameter, value);
+ } else {
+ LOG(LogLevel::Debug, @"%@ attributeValueForParam %@", self, attribute);
+ }
+
+ return value;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (id)accessibilityHitTest:(NSPoint)point {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+ return GetObjectOrRepresentedView([self moxHitTest:point]);
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (id)accessibilityFocusedUIElement {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+ return GetObjectOrRepresentedView([self moxFocusedUIElement]);
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (BOOL)isAccessibilityElement {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if ([self isExpired]) {
+ return YES;
+ }
+
+ id parent = [self moxParent];
+ if (![parent isMOXAccessible]) {
+ return YES;
+ }
+
+ return ![self moxIgnoreWithParent:parent];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NO);
+}
+
+- (BOOL)accessibilityNotifiesWhenDestroyed {
+ return YES;
+}
+
+#pragma mark - MOXAccessible protocol
+
+- (NSNumber*)moxIndexForChildUIElement:(id)child {
+ return @([[self moxUnignoredChildren] indexOfObject:child]);
+}
+
+- (id)moxTopLevelUIElement {
+ return [self moxWindow];
+}
+
+- (id)moxHitTest:(NSPoint)point {
+ return self;
+}
+
+- (id)moxFocusedUIElement {
+ return self;
+}
+
+- (void)moxPostNotification:(NSString*)notification {
+ [self moxPostNotification:notification withUserInfo:nil];
+}
+
+- (void)moxPostNotification:(NSString*)notification
+ withUserInfo:(NSDictionary*)userInfo {
+ if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Verbose)) {
+ LOG(LogLevel::Verbose, @"%@ notify %@ %@", self, notification, userInfo);
+ } else {
+ LOG(LogLevel::Debug, @"%@ notify %@", self, notification);
+ }
+
+ // This sends events via nsIObserverService to be consumed by our mochitests.
+ xpcAccessibleMacEvent::FireEvent(self, notification, userInfo);
+
+ if (gfxPlatform::IsHeadless()) {
+ // Using a headless toolkit for tests and whatnot, posting accessibility
+ // notification won't work.
+ return;
+ }
+
+ if (![self isAccessibilityElement]) {
+ // If this is an ignored object, don't expose it to system.
+ return;
+ }
+
+ if (userInfo) {
+ NSAccessibilityPostNotificationWithUserInfo(
+ GetObjectOrRepresentedView(self), notification, userInfo);
+ } else {
+ NSAccessibilityPostNotification(GetObjectOrRepresentedView(self),
+ notification);
+ }
+}
+
+- (BOOL)moxBlockSelector:(SEL)selector {
+ return NO;
+}
+
+- (NSArray*)moxChildren {
+ return @[];
+}
+
+- (NSArray*)moxUnignoredChildren {
+ NSMutableArray* unignoredChildren =
+ [[[NSMutableArray alloc] init] autorelease];
+ NSArray* allChildren = [self moxChildren];
+
+ for (MOXAccessibleBase* nativeChild in allChildren) {
+ if ([nativeChild moxIgnoreWithParent:self]) {
+ // If this child should be ignored get its unignored children.
+ // This will in turn recurse to any unignored descendants if the
+ // child is ignored.
+ [unignoredChildren
+ addObjectsFromArray:[nativeChild moxUnignoredChildren]];
+ } else {
+ [unignoredChildren addObject:nativeChild];
+ }
+ }
+
+ return unignoredChildren;
+}
+
+- (id<mozAccessible>)moxParent {
+ return nil;
+}
+
+- (id<mozAccessible>)moxUnignoredParent {
+ id nativeParent = [self moxParent];
+
+ if (![nativeParent isAccessibilityElement]) {
+ return [nativeParent moxUnignoredParent];
+ }
+
+ return GetObjectOrRepresentedView(nativeParent);
+}
+
+- (BOOL)moxIgnoreWithParent:(MOXAccessibleBase*)parent {
+ return [parent moxIgnoreChild:self];
+}
+
+- (BOOL)moxIgnoreChild:(MOXAccessibleBase*)child {
+ return NO;
+}
+
+- (id<MOXTextMarkerSupport>)moxTextMarkerDelegate {
+ return nil;
+}
+
+- (BOOL)moxIsLiveRegion {
+ return NO;
+}
+
+#pragma mark -
+
+// objc-style description (from NSObject); not to be confused with the
+// accessible description above.
+- (NSString*)description {
+ if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Debug)) {
+ if ([self isSelectorSupported:@selector(moxMozDebugDescription)]) {
+ return [self moxMozDebugDescription];
+ }
+ }
+
+ return [NSString stringWithFormat:@"<%@: %p %@>",
+ NSStringFromClass([self class]), self,
+ [self moxRole]];
+}
+
+- (BOOL)isExpired {
+ return mIsExpired;
+}
+
+- (void)expire {
+ MOZ_ASSERT(!mIsExpired, "expire called an expired mozAccessible!");
+
+ mIsExpired = YES;
+
+ [self moxPostNotification:NSAccessibilityUIElementDestroyedNotification];
+}
+
+- (id<MOXAccessible>)moxFindAncestor:(BOOL (^)(id<MOXAccessible> moxAcc,
+ BOOL* stop))findBlock {
+ for (id element = self; [element conformsToProtocol:@protocol(MOXAccessible)];
+ element = [element moxUnignoredParent]) {
+ BOOL stop = NO;
+ if (findBlock(element, &stop)) {
+ return element;
+ }
+
+ if (stop) {
+ break;
+ }
+ }
+
+ return nil;
+}
+
+#pragma mark - Private
+
+- (BOOL)isSelectorSupported:(SEL)selector {
+ return
+ [self respondsToSelector:selector] && ![self moxBlockSelector:selector];
+}
+
+@end
diff --git a/accessible/mac/MOXAccessibleProtocol.h b/accessible/mac/MOXAccessibleProtocol.h
new file mode 100644
index 0000000000..117bce79e7
--- /dev/null
+++ b/accessible/mac/MOXAccessibleProtocol.h
@@ -0,0 +1,534 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SDKDeclarations.h"
+
+@protocol MOXTextMarkerSupport;
+@protocol mozAccessible;
+
+// This protocol's primary use is for abstracting the NSAccessibility informal
+// protocol into a formal internal API. Conforming classes get to choose a
+// subset of the optional methods to implement. Those methods will be mapped to
+// NSAccessibility attributes or actions. A conforming class can implement
+// moxBlockSelector to control which of its implemented methods should be
+// exposed to NSAccessibility.
+
+@protocol MOXAccessible
+
+// The deepest descendant of the accessible subtree that contains the specified
+// point. Forwarded from accessibilityHitTest.
+- (id _Nullable)moxHitTest:(NSPoint)point;
+
+// The deepest descendant of the accessible subtree that has the focus.
+// Forwarded from accessibilityFocusedUIElement.
+- (id _Nullable)moxFocusedUIElement;
+
+// Sends a notification to any observing assistive applications.
+- (void)moxPostNotification:(NSString* _Nonnull)notification;
+
+- (void)moxPostNotification:(NSString* _Nonnull)notification
+ withUserInfo:(NSDictionary* _Nullable)userInfo;
+
+// Return YES if selector should be considered not supported.
+// Used in implementations such as:
+// - accessibilityAttributeNames
+// - accessibilityActionNames
+// - accessibilityIsAttributeSettable
+- (BOOL)moxBlockSelector:(SEL _Nonnull)selector;
+
+// Returns a list of all children, doesn't do ignore filtering.
+- (NSArray* _Nullable)moxChildren;
+
+// Returns our parent, doesn't do ignore filtering.
+- (id<mozAccessible> _Nullable)moxParent;
+
+// This is called by isAccessibilityElement. If a subclass wants
+// to alter the isAccessibilityElement return value, it must
+// override this and not isAccessibilityElement directly.
+- (BOOL)moxIgnoreWithParent:(id<MOXAccessible> _Nullable)parent;
+
+// Should the child be ignored. This allows subclasses to determine
+// what kinds of accessibles are valid children. This causes the child
+// to be skipped, but the unignored descendants will be added to the
+// container in the default children getter.
+- (BOOL)moxIgnoreChild:(id<MOXAccessible> _Nullable)child;
+
+// Return text delegate if it exists.
+- (id<MOXTextMarkerSupport> _Nullable)moxTextMarkerDelegate;
+
+// Return true if this accessible is a live region
+- (BOOL)moxIsLiveRegion;
+
+// Find the nearest ancestor that returns true with the given block function
+- (id<MOXAccessible> _Nullable)moxFindAncestor:
+ (BOOL (^_Nonnull)(id<MOXAccessible> _Nonnull moxAcc,
+ BOOL* _Nonnull stop))findBlock;
+
+@optional
+
+#pragma mark - AttributeGetters
+
+// AXChildren
+- (NSArray* _Nullable)moxUnignoredChildren;
+
+// AXParent
+- (id _Nullable)moxUnignoredParent;
+
+// AXRole
+- (NSString* _Nullable)moxRole;
+
+// AXRoleDescription
+- (NSString* _Nullable)moxRoleDescription;
+
+// AXSubrole
+- (NSString* _Nullable)moxSubrole;
+
+// AXTitle
+- (NSString* _Nullable)moxTitle;
+
+// AXDescription
+- (NSString* _Nullable)moxLabel;
+
+// AXHelp
+- (NSString* _Nullable)moxHelp;
+
+// AXValue
+- (id _Nullable)moxValue;
+
+// AXValueDescription
+- (NSString* _Nullable)moxValueDescription;
+
+// AXSize
+- (NSValue* _Nullable)moxSize;
+
+// AXPosition
+- (NSValue* _Nullable)moxPosition;
+
+// AXEnabled
+- (NSNumber* _Nullable)moxEnabled;
+
+// AXFocused
+- (NSNumber* _Nullable)moxFocused;
+
+// AXWindow
+- (id _Nullable)moxWindow;
+
+// AXFrame
+- (NSValue* _Nullable)moxFrame;
+
+// AXTitleUIElement
+- (id _Nullable)moxTitleUIElement;
+
+// AXTopLevelUIElement
+- (id _Nullable)moxTopLevelUIElement;
+
+// AXHasPopup
+- (NSNumber* _Nullable)moxHasPopup;
+
+// AXARIACurrent
+- (NSString* _Nullable)moxARIACurrent;
+
+// AXSelected
+- (NSNumber* _Nullable)moxSelected;
+
+// AXRequired
+- (NSNumber* _Nullable)moxRequired;
+
+// AXElementBusy
+- (NSNumber* _Nullable)moxElementBusy;
+
+// AXLinkedUIElements
+- (NSArray* _Nullable)moxLinkedUIElements;
+
+// AXARIAControls
+- (NSArray* _Nullable)moxARIAControls;
+
+// AXDOMIdentifier
+- (NSString* _Nullable)moxDOMIdentifier;
+
+// AXURL
+- (NSURL* _Nullable)moxURL;
+
+// AXLinkUIElements
+- (NSArray* _Nullable)moxLinkUIElements;
+
+// AXPopupValue
+- (NSString* _Nullable)moxPopupValue;
+
+// AXVisited
+- (NSNumber* _Nullable)moxVisited;
+
+// AXExpanded
+- (NSNumber* _Nullable)moxExpanded;
+
+// AXMain
+- (NSNumber* _Nullable)moxMain;
+
+// AXMinimized
+- (NSNumber* _Nullable)moxMinimized;
+
+// AXSelectedChildren
+- (NSArray* _Nullable)moxSelectedChildren;
+
+// AXTabs
+- (NSArray* _Nullable)moxTabs;
+
+// AXContents
+- (NSArray* _Nullable)moxContents;
+
+// AXOrientation
+- (NSString* _Nullable)moxOrientation;
+
+// AXMenuItemMarkChar
+- (NSString* _Nullable)moxMenuItemMarkChar;
+
+// AXLoaded
+- (NSNumber* _Nullable)moxLoaded;
+
+// AXLoadingProgress
+- (NSNumber* _Nullable)moxLoadingProgress;
+
+// AXMinValue
+- (id _Nullable)moxMinValue;
+
+// AXMaxValue
+- (id _Nullable)moxMaxValue;
+
+// Webkit also implements the following:
+// // AXCaretBrowsingEnabled
+// - (NSString* _Nullable)moxCaretBrowsingEnabled;
+
+// // AXLayoutCount
+// - (NSString* _Nullable)moxLayoutCount;
+
+// // AXWebSessionID
+// - (NSString* _Nullable)moxWebSessionID;
+
+// // AXPreventKeyboardDOMEventDispatch
+// - (NSString* _Nullable)moxPreventKeyboardDOMEventDispatch;
+
+// Table Attributes
+
+// AXRowCount
+- (NSNumber* _Nullable)moxRowCount;
+
+// AXColumnCount
+- (NSNumber* _Nullable)moxColumnCount;
+
+// AXRows
+- (NSArray* _Nullable)moxRows;
+
+// AXColumns
+- (NSArray* _Nullable)moxColumns;
+
+// AXIndex
+- (NSNumber* _Nullable)moxIndex;
+
+// AXRowIndexRange
+- (NSValue* _Nullable)moxRowIndexRange;
+
+// AXColumnIndexRange
+- (NSValue* _Nullable)moxColumnIndexRange;
+
+// AXRowHeaderUIElements
+- (NSArray* _Nullable)moxRowHeaderUIElements;
+
+// AXColumnHeaderUIElements
+- (NSArray* _Nullable)moxColumnHeaderUIElements;
+
+// AXIdentifier
+- (NSString* _Nullable)moxIdentifier;
+
+// AXVisibleChildren
+- (NSArray* _Nullable)moxVisibleChildren;
+
+// Outline Attributes
+
+// AXDisclosing
+- (NSNumber* _Nullable)moxDisclosing;
+
+// AXDisclosedByRow
+- (id _Nullable)moxDisclosedByRow;
+
+// AXDisclosureLevel
+- (NSNumber* _Nullable)moxDisclosureLevel;
+
+// AXDisclosedRows
+- (NSArray* _Nullable)moxDisclosedRows;
+
+// AXSelectedRows
+- (NSArray* _Nullable)moxSelectedRows;
+
+// Math Attributes
+
+// AXMathRootRadicand
+- (id _Nullable)moxMathRootRadicand;
+
+// AXMathRootIndex
+- (id _Nullable)moxMathRootIndex;
+
+// AXMathFractionNumerator
+- (id _Nullable)moxMathFractionNumerator;
+
+// AXMathFractionDenominator
+- (id _Nullable)moxMathFractionDenominator;
+
+// AXMathLineThickness
+- (NSNumber* _Nullable)moxMathLineThickness;
+
+// AXMathBase
+- (id _Nullable)moxMathBase;
+
+// AXMathSubscript
+- (id _Nullable)moxMathSubscript;
+
+// AXMathSuperscript
+- (id _Nullable)moxMathSuperscript;
+
+// AXMathUnder
+- (id _Nullable)moxMathUnder;
+
+// AXMathOver
+- (id _Nullable)moxMathOver;
+
+// AXInvalid
+- (NSString* _Nullable)moxInvalid;
+
+// AXSelectedText
+- (NSString* _Nullable)moxSelectedText;
+
+// AXSelectedTextRange
+- (NSValue* _Nullable)moxSelectedTextRange;
+
+// AXNumberOfCharacters
+- (NSNumber* _Nullable)moxNumberOfCharacters;
+
+// AXVisibleCharacterRange
+- (NSValue* _Nullable)moxVisibleCharacterRange;
+
+// AXInsertionPointLineNumber
+- (NSNumber* _Nullable)moxInsertionPointLineNumber;
+
+// AXEditableAncestor
+- (id _Nullable)moxEditableAncestor;
+
+// AXHighestEditableAncestor
+- (id _Nullable)moxHighestEditableAncestor;
+
+// AXFocusableAncestor
+- (id _Nullable)moxFocusableAncestor;
+
+// AXARIAAtomic
+- (NSNumber* _Nullable)moxARIAAtomic;
+
+// AXARIALive
+- (NSString* _Nullable)moxARIALive;
+
+// AXARIARelevant
+- (NSString* _Nullable)moxARIARelevant;
+
+// AXMozDebugDescription
+- (NSString* _Nullable)moxMozDebugDescription;
+
+#pragma mark - AttributeSetters
+
+// AXDisclosing
+- (void)moxSetDisclosing:(NSNumber* _Nullable)disclosing;
+
+// AXValue
+- (void)moxSetValue:(id _Nullable)value;
+
+// AXFocused
+- (void)moxSetFocused:(NSNumber* _Nullable)focused;
+
+// AXSelected
+- (void)moxSetSelected:(NSNumber* _Nullable)selected;
+
+// AXSelectedChildren
+- (void)moxSetSelectedChildren:(NSArray* _Nullable)selectedChildren;
+
+// AXSelectedText
+- (void)moxSetSelectedText:(NSString* _Nullable)selectedText;
+
+// AXSelectedTextRange
+- (void)moxSetSelectedTextRange:(NSValue* _Nullable)selectedTextRange;
+
+// AXVisibleCharacterRange
+- (void)moxSetVisibleCharacterRange:(NSValue* _Nullable)visibleCharacterRange;
+
+#pragma mark - Actions
+
+// AXPress
+- (void)moxPerformPress;
+
+// AXShowMenu
+- (void)moxPerformShowMenu;
+
+// AXScrollToVisible
+- (void)moxPerformScrollToVisible;
+
+// AXIncrement
+- (void)moxPerformIncrement;
+
+// AXDecrement
+- (void)moxPerformDecrement;
+
+#pragma mark - ParameterizedAttributeGetters
+
+// AXLineForIndex
+- (NSNumber* _Nullable)moxLineForIndex:(NSNumber* _Nonnull)index;
+
+// AXRangeForLine
+- (NSValue* _Nullable)moxRangeForLine:(NSNumber* _Nonnull)line;
+
+// AXStringForRange
+- (NSString* _Nullable)moxStringForRange:(NSValue* _Nonnull)range;
+
+// AXRangeForPosition
+- (NSValue* _Nullable)moxRangeForPosition:(NSValue* _Nonnull)position;
+
+// AXRangeForIndex
+- (NSValue* _Nullable)moxRangeForIndex:(NSNumber* _Nonnull)index;
+
+// AXBoundsForRange
+- (NSValue* _Nullable)moxBoundsForRange:(NSValue* _Nonnull)range;
+
+// AXRTFForRange
+- (NSData* _Nullable)moxRTFForRange:(NSValue* _Nonnull)range;
+
+// AXStyleRangeForIndex
+- (NSValue* _Nullable)moxStyleRangeForIndex:(NSNumber* _Nonnull)index;
+
+// AXAttributedStringForRange
+- (NSAttributedString* _Nullable)moxAttributedStringForRange:
+ (NSValue* _Nonnull)range;
+
+// AXUIElementsForSearchPredicate
+- (NSArray* _Nullable)moxUIElementsForSearchPredicate:
+ (NSDictionary* _Nonnull)searchPredicate;
+
+// AXUIElementCountForSearchPredicate
+- (NSNumber* _Nullable)moxUIElementCountForSearchPredicate:
+ (NSDictionary* _Nonnull)searchPredicate;
+
+// AXCellForColumnAndRow
+- (id _Nullable)moxCellForColumnAndRow:(NSArray* _Nonnull)columnAndRow;
+
+// AXIndexForChildUIElement
+- (NSNumber* _Nullable)moxIndexForChildUIElement:(id _Nonnull)child;
+
+@end
+
+// This protocol maps text marker and text marker range parameters to
+// methods. It is implemented by a delegate of a MOXAccessible.
+@protocol MOXTextMarkerSupport
+
+#pragma mark - TextAttributeGetters
+
+// AXStartTextMarker
+- (AXTextMarkerRef _Nullable)moxStartTextMarker;
+
+// AXEndTextMarker
+- (AXTextMarkerRef _Nullable)moxEndTextMarker;
+
+// AXSelectedTextMarkerRange
+- (AXTextMarkerRangeRef _Nullable)moxSelectedTextMarkerRange;
+
+#pragma mark - ParameterizedTextAttributeGetters
+
+// AXLengthForTextMarkerRange
+- (NSNumber* _Nullable)moxLengthForTextMarkerRange:
+ (AXTextMarkerRangeRef _Nonnull)textMarkerRange;
+
+// AXStringForTextMarkerRange
+- (NSString* _Nullable)moxStringForTextMarkerRange:
+ (AXTextMarkerRangeRef _Nonnull)textMarkerRange;
+
+// AXTextMarkerRangeForUnorderedTextMarkers
+- (AXTextMarkerRangeRef _Nullable)moxTextMarkerRangeForUnorderedTextMarkers:
+ (NSArray* _Nonnull)textMarkers;
+
+// AXLeftWordTextMarkerRangeForTextMarker
+- (AXTextMarkerRangeRef _Nullable)moxLeftWordTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef _Nonnull)textMarker;
+
+// AXRightWordTextMarkerRangeForTextMarker
+- (AXTextMarkerRangeRef _Nullable)moxRightWordTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef _Nonnull)textMarker;
+
+// AXStartTextMarkerForTextMarkerRange
+- (AXTextMarkerRef _Nullable)moxStartTextMarkerForTextMarkerRange:
+ (AXTextMarkerRangeRef _Nonnull)textMarkerRange;
+
+// AXEndTextMarkerForTextMarkerRange
+- (AXTextMarkerRef _Nullable)moxEndTextMarkerForTextMarkerRange:
+ (AXTextMarkerRangeRef _Nonnull)textMarkerRange;
+
+// AXNextTextMarkerForTextMarker
+- (AXTextMarkerRef _Nullable)moxNextTextMarkerForTextMarker:
+ (AXTextMarkerRef _Nonnull)textMarker;
+
+// AXPreviousTextMarkerForTextMarker
+- (AXTextMarkerRef _Nullable)moxPreviousTextMarkerForTextMarker:
+ (AXTextMarkerRef _Nonnull)textMarker;
+
+// AXAttributedStringForTextMarkerRange
+- (NSAttributedString* _Nullable)moxAttributedStringForTextMarkerRange:
+ (AXTextMarkerRangeRef _Nonnull)textMarkerRange;
+
+// AXBoundsForTextMarkerRange
+- (NSValue* _Nullable)moxBoundsForTextMarkerRange:
+ (AXTextMarkerRangeRef _Nonnull)textMarkerRange;
+
+// AXIndexForTextMarker
+- (NSNumber* _Nullable)moxIndexForTextMarker:
+ (AXTextMarkerRef _Nonnull)textMarker;
+
+// AXTextMarkerForIndex
+- (AXTextMarkerRef _Nullable)moxTextMarkerForIndex:(NSNumber* _Nonnull)index;
+
+// AXUIElementForTextMarker
+- (id _Nullable)moxUIElementForTextMarker:(AXTextMarkerRef _Nonnull)textMarker;
+
+// AXTextMarkerRangeForUIElement
+- (AXTextMarkerRangeRef _Nullable)moxTextMarkerRangeForUIElement:
+ (id _Nonnull)element;
+
+// AXLineTextMarkerRangeForTextMarker
+- (AXTextMarkerRangeRef _Nullable)moxLineTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef _Nonnull)textMarker;
+
+// AXLeftLineTextMarkerRangeForTextMarker
+- (AXTextMarkerRangeRef _Nullable)moxLeftLineTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef _Nonnull)textMarker;
+
+// AXRightLineTextMarkerRangeForTextMarker
+- (AXTextMarkerRangeRef _Nullable)moxRightLineTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef _Nonnull)textMarker;
+
+// AXParagraphTextMarkerRangeForTextMarker
+- (AXTextMarkerRangeRef _Nullable)moxParagraphTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef _Nonnull)textMarker;
+
+// AXStyleTextMarkerRangeForTextMarker
+- (AXTextMarkerRangeRef _Nullable)moxStyleTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef _Nonnull)textMarker;
+
+// AXMozDebugDescriptionForTextMarker
+- (NSString* _Nullable)moxMozDebugDescriptionForTextMarker:
+ (AXTextMarkerRef _Nonnull)textMarker;
+
+// AXMozDebugDescriptionForTextMarkerRange
+- (NSString* _Nullable)moxMozDebugDescriptionForTextMarkerRange:
+ (AXTextMarkerRangeRef _Nonnull)textMarkerRange;
+
+#pragma mark - TextAttributeSetters
+
+// AXSelectedTextMarkerRange
+- (void)moxSetSelectedTextMarkerRange:(id _Nullable)textMarkerRange;
+
+@end
diff --git a/accessible/mac/MOXLandmarkAccessibles.h b/accessible/mac/MOXLandmarkAccessibles.h
new file mode 100644
index 0000000000..bea44e7a8f
--- /dev/null
+++ b/accessible/mac/MOXLandmarkAccessibles.h
@@ -0,0 +1,15 @@
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset:
+ * 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=2:
+ */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "mozAccessible.h"
+
+@interface MOXLandmarkAccessible : mozAccessible
+// overrides
+- (NSString*)moxTitle;
+
+@end
diff --git a/accessible/mac/MOXLandmarkAccessibles.mm b/accessible/mac/MOXLandmarkAccessibles.mm
new file mode 100644
index 0000000000..4a3aa8f597
--- /dev/null
+++ b/accessible/mac/MOXLandmarkAccessibles.mm
@@ -0,0 +1,15 @@
+/* -*- (Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil;
+ * c-basic-offset:) 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "MOXLandmarkAccessibles.h"
+
+@implementation MOXLandmarkAccessible
+
+- (NSString*)moxTitle {
+ return @"";
+}
+
+@end
diff --git a/accessible/mac/MOXMathAccessibles.h b/accessible/mac/MOXMathAccessibles.h
new file mode 100644
index 0000000000..7661ad5c6a
--- /dev/null
+++ b/accessible/mac/MOXMathAccessibles.h
@@ -0,0 +1,64 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "mozAccessible.h"
+
+@interface MOXMathRootAccessible : mozAccessible
+
+// overrides
+- (id)moxMathRootRadicand;
+
+// overrides
+- (id)moxMathRootIndex;
+
+@end
+
+@interface MOXMathSquareRootAccessible : mozAccessible
+
+// overrides
+- (id)moxMathRootRadicand;
+
+@end
+
+@interface MOXMathFractionAccessible : mozAccessible
+
+// overrides
+- (id)moxMathFractionNumerator;
+
+// overrides
+- (id)moxMathFractionDenominator;
+
+// overrides
+- (NSNumber*)moxMathLineThickness;
+
+@end
+
+@interface MOXMathSubSupAccessible : mozAccessible
+
+// overrides
+- (id)moxMathBase;
+
+// overrides
+- (id)moxMathSubscript;
+
+// overrides
+- (id)moxMathSuperscript;
+
+@end
+
+@interface MOXMathUnderOverAccessible : mozAccessible
+
+// overrides
+- (id)moxMathBase;
+
+// overrides
+- (id)moxMathUnder;
+
+// overrides
+- (id)moxMathOver;
+
+@end
diff --git a/accessible/mac/MOXMathAccessibles.mm b/accessible/mac/MOXMathAccessibles.mm
new file mode 100644
index 0000000000..7bfe2e3e05
--- /dev/null
+++ b/accessible/mac/MOXMathAccessibles.mm
@@ -0,0 +1,117 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "MOXMathAccessibles.h"
+
+#import "MacUtils.h"
+
+using namespace mozilla::a11y;
+
+// XXX WebKit also defines the following attributes.
+// See bugs 1176970 and 1176983.
+// - NSAccessibilityMathFencedOpenAttribute @"AXMathFencedOpen"
+// - NSAccessibilityMathFencedCloseAttribute @"AXMathFencedClose"
+// - NSAccessibilityMathPrescriptsAttribute @"AXMathPrescripts"
+// - NSAccessibilityMathPostscriptsAttribute @"AXMathPostscripts"
+
+@implementation MOXMathRootAccessible
+
+- (id)moxMathRootRadicand {
+ return [self childAt:0];
+}
+
+- (id)moxMathRootIndex {
+ return [self childAt:1];
+}
+
+@end
+
+@implementation MOXMathSquareRootAccessible
+
+- (id)moxMathRootRadicand {
+ return [self childAt:0];
+}
+
+@end
+
+@implementation MOXMathFractionAccessible
+
+- (id)moxMathFractionNumerator {
+ return [self childAt:0];
+}
+
+- (id)moxMathFractionDenominator {
+ return [self childAt:1];
+}
+
+// Bug 1639745: This doesn't actually work.
+- (NSNumber*)moxMathLineThickness {
+ // WebKit sets line thickness to some logical value parsed in the
+ // renderer object of the <mfrac> element. It's not clear whether the
+ // exact value is relevant to assistive technologies. From a semantic
+ // point of view, the only important point is to distinguish between
+ // <mfrac> elements that have a fraction bar and those that do not.
+ // Per the MathML 3 spec, the latter happens iff the linethickness
+ // attribute is of the form [zero-float][optional-unit]. In that case we
+ // set line thickness to zero and in the other cases we set it to one.
+ if (NSString* thickness =
+ utils::GetAccAttr(self, nsGkAtoms::linethickness_)) {
+ NSNumberFormatter* formatter =
+ [[[NSNumberFormatter alloc] init] autorelease];
+ NSNumber* value = [formatter numberFromString:thickness];
+ return [NSNumber numberWithBool:[value boolValue]];
+ } else {
+ return [NSNumber numberWithInteger:0];
+ }
+}
+
+@end
+
+@implementation MOXMathSubSupAccessible
+- (id)moxMathBase {
+ return [self childAt:0];
+}
+
+- (id)moxMathSubscript {
+ if (mRole == roles::MATHML_SUP) {
+ return nil;
+ }
+
+ return [self childAt:1];
+}
+
+- (id)moxMathSuperscript {
+ if (mRole == roles::MATHML_SUB) {
+ return nil;
+ }
+
+ return [self childAt:mRole == roles::MATHML_SUP ? 1 : 2];
+}
+
+@end
+
+@implementation MOXMathUnderOverAccessible
+- (id)moxMathBase {
+ return [self childAt:0];
+}
+
+- (id)moxMathUnder {
+ if (mRole == roles::MATHML_OVER) {
+ return nil;
+ }
+
+ return [self childAt:1];
+}
+
+- (id)moxMathOver {
+ if (mRole == roles::MATHML_UNDER) {
+ return nil;
+ }
+
+ return [self childAt:mRole == roles::MATHML_OVER ? 1 : 2];
+}
+@end
diff --git a/accessible/mac/MOXSearchInfo.h b/accessible/mac/MOXSearchInfo.h
new file mode 100644
index 0000000000..8f5e6f414d
--- /dev/null
+++ b/accessible/mac/MOXSearchInfo.h
@@ -0,0 +1,43 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "mozAccessible.h"
+#include "Pivot.h"
+
+using namespace mozilla::a11y;
+
+@interface MOXSearchInfo : NSObject {
+ // The MOX accessible of the web area, we need a reference
+ // to set the pivot's root. This is a weak ref.
+ MOXAccessibleBase* mRoot;
+
+ // The MOX accessible we should start searching from.
+ // This is a weak ref.
+ MOXAccessibleBase* mStartElem;
+
+ // The amount of matches we should return
+ int mResultLimit;
+
+ // The array of search keys to use during this search
+ NSMutableArray* mSearchKeys;
+
+ // Set to YES if we should search forward, NO if backward
+ BOOL mSearchForward;
+
+ // Set to YES if we should match on immediate descendants only, NO otherwise
+ BOOL mImmediateDescendantsOnly;
+
+ NSString* mSearchText;
+}
+
+- (id)initWithParameters:(NSDictionary*)params andRoot:(MOXAccessibleBase*)root;
+
+- (NSArray*)performSearch;
+
+- (void)dealloc;
+
+@end
diff --git a/accessible/mac/MOXSearchInfo.mm b/accessible/mac/MOXSearchInfo.mm
new file mode 100644
index 0000000000..17138d93d3
--- /dev/null
+++ b/accessible/mac/MOXSearchInfo.mm
@@ -0,0 +1,454 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "MOXSearchInfo.h"
+#import "MOXWebAreaAccessible.h"
+#import "RotorRules.h"
+
+#include "nsCocoaUtils.h"
+#include "DocAccessibleParent.h"
+
+using namespace mozilla::a11y;
+
+@interface MOXSearchInfo ()
+- (NSArray*)getMatchesForRule:(PivotRule&)rule;
+
+- (NSArray<mozAccessible*>*)applyPostFilter:(NSArray<mozAccessible*>*)matches;
+
+- (Accessible*)rootGeckoAccessible;
+
+- (Accessible*)startGeckoAccessible;
+
+- (BOOL)shouldApplyPostFilter;
+@end
+
+@implementation MOXSearchInfo
+
+- (id)initWithParameters:(NSDictionary*)params
+ andRoot:(MOXAccessibleBase*)root {
+ if (id searchKeyParam = [params objectForKey:@"AXSearchKey"]) {
+ mSearchKeys = [searchKeyParam isKindOfClass:[NSString class]]
+ ? [[NSArray alloc] initWithObjects:searchKeyParam, nil]
+ : [searchKeyParam retain];
+ }
+
+ if (id startElemParam = [params objectForKey:@"AXStartElement"]) {
+ mStartElem = startElemParam;
+ } else {
+ mStartElem = root;
+ }
+
+ mRoot = root;
+
+ mResultLimit = [[params objectForKey:@"AXResultsLimit"] intValue];
+
+ mSearchForward =
+ [[params objectForKey:@"AXDirection"] isEqualToString:@"AXDirectionNext"];
+
+ mImmediateDescendantsOnly =
+ [[params objectForKey:@"AXImmediateDescendantsOnly"] boolValue];
+
+ mSearchText = [params objectForKey:@"AXSearchText"];
+
+ return [super init];
+}
+
+- (Accessible*)rootGeckoAccessible {
+ id root =
+ [mRoot isKindOfClass:[mozAccessible class]] ? mRoot : [mRoot moxParent];
+
+ return [static_cast<mozAccessible*>(root) geckoAccessible];
+}
+
+- (Accessible*)startGeckoAccessible {
+ if ([mStartElem isKindOfClass:[mozAccessible class]]) {
+ return [static_cast<mozAccessible*>(mStartElem) geckoAccessible];
+ }
+
+ // If it isn't a mozAccessible, it doesn't have a gecko accessible
+ // this is most likely the root group. Use the gecko doc as the start
+ // accessible.
+ return [self rootGeckoAccessible];
+}
+
+- (NSArray*)getMatchesForRule:(PivotRule&)rule {
+ // If we will apply a post-filter, don't limit search so we
+ // don't come up short on the final result count.
+ int resultLimit = [self shouldApplyPostFilter] ? -1 : mResultLimit;
+
+ NSMutableArray<mozAccessible*>* matches =
+ [[[NSMutableArray alloc] init] autorelease];
+ Accessible* geckoRootAcc = [self rootGeckoAccessible];
+ Accessible* geckoStartAcc = [self startGeckoAccessible];
+ Pivot p = Pivot(geckoRootAcc);
+ Accessible* match;
+ if (mSearchForward) {
+ match = p.Next(geckoStartAcc, rule);
+ } else {
+ // Search backwards
+ if (geckoRootAcc == geckoStartAcc) {
+ // If we have no explicit start accessible, start from the last match.
+ match = p.Last(rule);
+ } else {
+ match = p.Prev(geckoStartAcc, rule);
+ }
+ }
+
+ while (match && resultLimit != 0) {
+ if (!mSearchForward && match == geckoRootAcc) {
+ // If searching backwards, don't include root.
+ break;
+ }
+
+ // we use mResultLimit != 0 to capture the case where mResultLimit is -1
+ // when it is set from the params dictionary. If that's true, we want
+ // to return all matches (ie. have no limit)
+ mozAccessible* nativeMatch = GetNativeFromGeckoAccessible(match);
+ if (nativeMatch) {
+ // only add/count results for which there is a matching
+ // native accessible
+ [matches addObject:nativeMatch];
+ resultLimit -= 1;
+ }
+
+ match = mSearchForward ? p.Next(match, rule) : p.Prev(match, rule);
+ }
+
+ return [self applyPostFilter:matches];
+}
+
+- (BOOL)shouldApplyPostFilter {
+ // We currently only support AXSearchText as a post-search filter.
+ // In some cases, VO passes a non-null, empty string for AXSearchText.
+ // In that case, we should act as if no AXSearchText was given.
+ return !!mSearchText && [mSearchText length] > 0;
+}
+
+- (NSArray<mozAccessible*>*)applyPostFilter:(NSArray<mozAccessible*>*)matches {
+ if (![self shouldApplyPostFilter]) {
+ return matches;
+ }
+
+ NSMutableArray<mozAccessible*>* postMatches =
+ [[[NSMutableArray alloc] init] autorelease];
+
+ nsString searchText;
+ nsCocoaUtils::GetStringForNSString(mSearchText, searchText);
+
+ __block DocAccessibleParent* ipcDoc = nullptr;
+ __block nsTArray<uint64_t> accIds;
+
+ [matches enumerateObjectsUsingBlock:^(mozAccessible* match, NSUInteger idx,
+ BOOL* stop) {
+ Accessible* geckoAcc = [match geckoAccessible];
+ if (!geckoAcc) {
+ return;
+ }
+
+ switch (geckoAcc->Role()) {
+ case roles::LANDMARK:
+ case roles::COMBOBOX:
+ case roles::LISTITEM:
+ case roles::COMBOBOX_LIST:
+ case roles::MENUBAR:
+ case roles::MENUPOPUP:
+ case roles::DOCUMENT:
+ case roles::APPLICATION:
+ // XXX: These roles either have AXTitle/AXDescription overridden as
+ // empty, or should never be returned in search text results. This
+ // should be integrated into a pivot rule in the future, and possibly
+ // better mapped somewhere.
+ return;
+ default:
+ break;
+ }
+
+ if (geckoAcc->IsLocal()) {
+ AccessibleWrap* acc = static_cast<AccessibleWrap*>(geckoAcc->AsLocal());
+ if (acc->ApplyPostFilter(EWhichPostFilter::eContainsText, searchText)) {
+ if (mozAccessible* nativePostMatch =
+ GetNativeFromGeckoAccessible(acc)) {
+ [postMatches addObject:nativePostMatch];
+ if (mResultLimit > 0 &&
+ [postMatches count] >= static_cast<NSUInteger>(mResultLimit)) {
+ // If we reached the result limit, alter the `stop` pointer to YES
+ // to stop iteration.
+ *stop = YES;
+ }
+ }
+ }
+
+ return;
+ }
+
+ RemoteAccessible* proxy = geckoAcc->AsRemote();
+ if (ipcDoc &&
+ ((ipcDoc != proxy->Document()) || (idx + 1 == [matches count]))) {
+ // If the ipcDoc doesn't match the current proxy's doc, we crossed into a
+ // new document. ..or this is the last match. Apply the filter on the list
+ // of the current ipcDoc.
+ nsTArray<uint64_t> matchIds;
+ Unused << ipcDoc->GetPlatformExtension()->SendApplyPostSearchFilter(
+ accIds, mResultLimit, EWhichPostFilter::eContainsText, searchText,
+ &matchIds);
+ for (size_t i = 0; i < matchIds.Length(); i++) {
+ if (RemoteAccessible* postMatch =
+ ipcDoc->GetAccessible(matchIds.ElementAt(i))) {
+ if (mozAccessible* nativePostMatch =
+ GetNativeFromGeckoAccessible(postMatch)) {
+ [postMatches addObject:nativePostMatch];
+ if (mResultLimit > 0 &&
+ [postMatches count] >= static_cast<NSUInteger>(mResultLimit)) {
+ // If we reached the result limit, alter the `stop` pointer to YES
+ // to stop iteration.
+ *stop = YES;
+ return;
+ }
+ }
+ }
+ }
+
+ ipcDoc = nullptr;
+ accIds.Clear();
+ }
+
+ if (!ipcDoc) {
+ ipcDoc = proxy->Document();
+ }
+ accIds.AppendElement(proxy->ID());
+ }];
+
+ return postMatches;
+}
+
+- (NSArray*)performSearch {
+ Accessible* geckoRootAcc = [self rootGeckoAccessible];
+ Accessible* geckoStartAcc = [self startGeckoAccessible];
+ NSMutableArray* matches = [[[NSMutableArray alloc] init] autorelease];
+ for (id key in mSearchKeys) {
+ if ([key isEqualToString:@"AXAnyTypeSearchKey"]) {
+ RotorRule rule =
+ mImmediateDescendantsOnly ? RotorRule(geckoRootAcc) : RotorRule();
+
+ if ([mStartElem isKindOfClass:[MOXWebAreaAccessible class]]) {
+ if (id rootGroup =
+ [static_cast<MOXWebAreaAccessible*>(mStartElem) rootGroup]) {
+ // Moving forward from web area, rootgroup; if it exists, is next.
+ [matches addObject:rootGroup];
+ if (mResultLimit == 1) {
+ // Found one match, continue in search keys for block.
+ continue;
+ }
+ }
+ }
+
+ if (mImmediateDescendantsOnly && mStartElem != mRoot &&
+ [mStartElem isKindOfClass:[MOXRootGroup class]]) {
+ // Moving forward from root group. If we don't match descendants,
+ // there is no match. Continue.
+ continue;
+ }
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXHeadingSearchKey"]) {
+ RotorRoleRule rule = mImmediateDescendantsOnly
+ ? RotorRoleRule(roles::HEADING, geckoRootAcc)
+ : RotorRoleRule(roles::HEADING);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXArticleSearchKey"]) {
+ RotorRoleRule rule = mImmediateDescendantsOnly
+ ? RotorRoleRule(roles::ARTICLE, geckoRootAcc)
+ : RotorRoleRule(roles::ARTICLE);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXTableSearchKey"]) {
+ RotorRoleRule rule = mImmediateDescendantsOnly
+ ? RotorRoleRule(roles::TABLE, geckoRootAcc)
+ : RotorRoleRule(roles::TABLE);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXLandmarkSearchKey"]) {
+ RotorRoleRule rule = mImmediateDescendantsOnly
+ ? RotorRoleRule(roles::LANDMARK, geckoRootAcc)
+ : RotorRoleRule(roles::LANDMARK);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXListSearchKey"]) {
+ RotorRoleRule rule = mImmediateDescendantsOnly
+ ? RotorRoleRule(roles::LIST, geckoRootAcc)
+ : RotorRoleRule(roles::LIST);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXLinkSearchKey"]) {
+ RotorLinkRule rule = mImmediateDescendantsOnly
+ ? RotorLinkRule(geckoRootAcc)
+ : RotorLinkRule();
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXVisitedLinkSearchKey"]) {
+ RotorVisitedLinkRule rule = mImmediateDescendantsOnly
+ ? RotorVisitedLinkRule(geckoRootAcc)
+ : RotorVisitedLinkRule();
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXUnvisitedLinkSearchKey"]) {
+ RotorUnvisitedLinkRule rule = mImmediateDescendantsOnly
+ ? RotorUnvisitedLinkRule(geckoRootAcc)
+ : RotorUnvisitedLinkRule();
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXButtonSearchKey"]) {
+ RotorRoleRule rule = mImmediateDescendantsOnly
+ ? RotorRoleRule(roles::PUSHBUTTON, geckoRootAcc)
+ : RotorRoleRule(roles::PUSHBUTTON);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXControlSearchKey"]) {
+ RotorControlRule rule = mImmediateDescendantsOnly
+ ? RotorControlRule(geckoRootAcc)
+ : RotorControlRule();
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXSameTypeSearchKey"]) {
+ mozAccessible* native = GetNativeFromGeckoAccessible(geckoStartAcc);
+ NSString* macRole = [native moxRole];
+ RotorMacRoleRule rule = mImmediateDescendantsOnly
+ ? RotorMacRoleRule(macRole, geckoRootAcc)
+ : RotorMacRoleRule(macRole);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXDifferentTypeSearchKey"]) {
+ mozAccessible* native = GetNativeFromGeckoAccessible(geckoStartAcc);
+ NSString* macRole = [native moxRole];
+ RotorNotMacRoleRule rule =
+ mImmediateDescendantsOnly ? RotorNotMacRoleRule(macRole, geckoRootAcc)
+ : RotorNotMacRoleRule(macRole);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXRadioGroupSearchKey"]) {
+ RotorRoleRule rule = mImmediateDescendantsOnly
+ ? RotorRoleRule(roles::RADIO_GROUP, geckoRootAcc)
+ : RotorRoleRule(roles::RADIO_GROUP);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXFrameSearchKey"]) {
+ RotorRoleRule rule = mImmediateDescendantsOnly
+ ? RotorRoleRule(roles::DOCUMENT, geckoRootAcc)
+ : RotorRoleRule(roles::DOCUMENT);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXImageSearchKey"] ||
+ [key isEqualToString:@"AXGraphicSearchKey"]) {
+ RotorRoleRule rule = mImmediateDescendantsOnly
+ ? RotorRoleRule(roles::GRAPHIC, geckoRootAcc)
+ : RotorRoleRule(roles::GRAPHIC);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXCheckBoxSearchKey"]) {
+ RotorRoleRule rule = mImmediateDescendantsOnly
+ ? RotorRoleRule(roles::CHECKBUTTON, geckoRootAcc)
+ : RotorRoleRule(roles::CHECKBUTTON);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXStaticTextSearchKey"]) {
+ RotorStaticTextRule rule = mImmediateDescendantsOnly
+ ? RotorStaticTextRule(geckoRootAcc)
+ : RotorStaticTextRule();
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXHeadingLevel1SearchKey"]) {
+ RotorHeadingLevelRule rule = mImmediateDescendantsOnly
+ ? RotorHeadingLevelRule(1, geckoRootAcc)
+ : RotorHeadingLevelRule(1);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXHeadingLevel2SearchKey"]) {
+ RotorHeadingLevelRule rule = mImmediateDescendantsOnly
+ ? RotorHeadingLevelRule(2, geckoRootAcc)
+ : RotorHeadingLevelRule(2);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXHeadingLevel3SearchKey"]) {
+ RotorHeadingLevelRule rule = mImmediateDescendantsOnly
+ ? RotorHeadingLevelRule(3, geckoRootAcc)
+ : RotorHeadingLevelRule(3);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXHeadingLevel4SearchKey"]) {
+ RotorHeadingLevelRule rule = mImmediateDescendantsOnly
+ ? RotorHeadingLevelRule(4, geckoRootAcc)
+ : RotorHeadingLevelRule(4);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXHeadingLevel5SearchKey"]) {
+ RotorHeadingLevelRule rule = mImmediateDescendantsOnly
+ ? RotorHeadingLevelRule(5, geckoRootAcc)
+ : RotorHeadingLevelRule(5);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXHeadingLevel6SearchKey"]) {
+ RotorHeadingLevelRule rule = mImmediateDescendantsOnly
+ ? RotorHeadingLevelRule(6, geckoRootAcc)
+ : RotorHeadingLevelRule(6);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXBlockquoteSearchKey"]) {
+ RotorRoleRule rule = mImmediateDescendantsOnly
+ ? RotorRoleRule(roles::BLOCKQUOTE, geckoRootAcc)
+ : RotorRoleRule(roles::BLOCKQUOTE);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXTextFieldSearchKey"]) {
+ RotorTextEntryRule rule = mImmediateDescendantsOnly
+ ? RotorTextEntryRule(geckoRootAcc)
+ : RotorTextEntryRule();
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXLiveRegionSearchKey"]) {
+ RotorLiveRegionRule rule = mImmediateDescendantsOnly
+ ? RotorLiveRegionRule(geckoRootAcc)
+ : RotorLiveRegionRule();
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+ }
+
+ return matches;
+}
+
+- (void)dealloc {
+ [mSearchKeys release];
+ [super dealloc];
+}
+
+@end
diff --git a/accessible/mac/MOXTextMarkerDelegate.h b/accessible/mac/MOXTextMarkerDelegate.h
new file mode 100644
index 0000000000..7a0161a6b8
--- /dev/null
+++ b/accessible/mac/MOXTextMarkerDelegate.h
@@ -0,0 +1,169 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import <Cocoa/Cocoa.h>
+
+#import "MOXAccessibleProtocol.h"
+#import "GeckoTextMarker.h"
+
+@interface MOXTextMarkerDelegate : NSObject <MOXTextMarkerSupport> {
+ mozilla::a11y::Accessible* mGeckoDocAccessible;
+ AXTextMarkerRangeRef mSelection;
+ AXTextMarkerRef mCaret;
+ AXTextMarkerRef mPrevCaret;
+ int32_t mCaretMoveGranularity;
+}
+
++ (id)getOrCreateForDoc:(mozilla::a11y::Accessible*)aDoc;
+
++ (void)destroyForDoc:(mozilla::a11y::Accessible*)aDoc;
+
+- (id)initWithDoc:(mozilla::a11y::Accessible*)aDoc;
+
+- (void)dealloc;
+
+- (void)setSelectionFrom:(mozilla::a11y::Accessible*)startContainer
+ at:(int32_t)startOffset
+ to:(mozilla::a11y::Accessible*)endContainer
+ at:(int32_t)endOffset;
+
+- (void)setCaretOffset:(mozilla::a11y::Accessible*)container
+ at:(int32_t)offset
+ moveGranularity:(int32_t)granularity;
+
+- (NSDictionary*)selectionChangeInfo;
+
+- (void)invalidateSelection;
+
+- (mozilla::a11y::GeckoTextMarkerRange)selection;
+
+// override
+- (AXTextMarkerRef)moxStartTextMarker;
+
+// override
+- (AXTextMarkerRef)moxEndTextMarker;
+
+// override
+- (AXTextMarkerRangeRef)moxSelectedTextMarkerRange;
+
+// override
+- (NSNumber*)moxLengthForTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange;
+
+// override
+- (NSString*)moxStringForTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange;
+
+// override
+- (AXTextMarkerRangeRef)moxTextMarkerRangeForUnorderedTextMarkers:
+ (NSArray*)textMarkers;
+
+// override
+- (AXTextMarkerRef)moxStartTextMarkerForTextMarkerRange:
+ (AXTextMarkerRangeRef)textMarkerRange;
+
+// override
+- (AXTextMarkerRef)moxEndTextMarkerForTextMarkerRange:
+ (AXTextMarkerRangeRef)textMarkerRange;
+
+// override
+- (AXTextMarkerRangeRef)moxLeftWordTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef)textMarker;
+
+// override
+- (AXTextMarkerRangeRef)moxRightWordTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef)textMarker;
+
+// override
+- (AXTextMarkerRangeRef)moxLineTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef)textMarker;
+
+// override
+- (AXTextMarkerRangeRef)moxLeftLineTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef)textMarker;
+
+// override
+- (AXTextMarkerRangeRef)moxRightLineTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef)textMarker;
+
+// override
+- (AXTextMarkerRangeRef)moxParagraphTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef)textMarker;
+
+// override
+- (AXTextMarkerRangeRef)moxStyleTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef)textMarker;
+
+// override
+- (AXTextMarkerRef)moxNextTextMarkerForTextMarker:(AXTextMarkerRef)textMarker;
+
+// override
+- (AXTextMarkerRef)moxPreviousTextMarkerForTextMarker:
+ (AXTextMarkerRef)textMarker;
+
+// override
+- (NSAttributedString*)moxAttributedStringForTextMarkerRange:
+ (AXTextMarkerRangeRef)textMarkerRange;
+
+// override
+- (NSValue*)moxBoundsForTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange;
+
+// override
+- (id)moxUIElementForTextMarker:(AXTextMarkerRef)textMarker;
+
+// override
+- (AXTextMarkerRangeRef)moxTextMarkerRangeForUIElement:(id)element;
+
+// override
+- (NSString*)moxMozDebugDescriptionForTextMarker:(AXTextMarkerRef)textMarker;
+
+// override
+- (void)moxSetSelectedTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange;
+
+@end
+
+namespace mozilla {
+namespace a11y {
+
+enum AXTextEditType {
+ AXTextEditTypeUnknown,
+ AXTextEditTypeDelete,
+ AXTextEditTypeInsert,
+ AXTextEditTypeTyping,
+ AXTextEditTypeDictation,
+ AXTextEditTypeCut,
+ AXTextEditTypePaste,
+ AXTextEditTypeAttributesChange
+};
+
+enum AXTextStateChangeType {
+ AXTextStateChangeTypeUnknown,
+ AXTextStateChangeTypeEdit,
+ AXTextStateChangeTypeSelectionMove,
+ AXTextStateChangeTypeSelectionExtend
+};
+
+enum AXTextSelectionDirection {
+ AXTextSelectionDirectionUnknown,
+ AXTextSelectionDirectionBeginning,
+ AXTextSelectionDirectionEnd,
+ AXTextSelectionDirectionPrevious,
+ AXTextSelectionDirectionNext,
+ AXTextSelectionDirectionDiscontiguous
+};
+
+enum AXTextSelectionGranularity {
+ AXTextSelectionGranularityUnknown,
+ AXTextSelectionGranularityCharacter,
+ AXTextSelectionGranularityWord,
+ AXTextSelectionGranularityLine,
+ AXTextSelectionGranularitySentence,
+ AXTextSelectionGranularityParagraph,
+ AXTextSelectionGranularityPage,
+ AXTextSelectionGranularityDocument,
+ AXTextSelectionGranularityAll
+};
+}
+}
diff --git a/accessible/mac/MOXTextMarkerDelegate.mm b/accessible/mac/MOXTextMarkerDelegate.mm
new file mode 100644
index 0000000000..e232dc0b63
--- /dev/null
+++ b/accessible/mac/MOXTextMarkerDelegate.mm
@@ -0,0 +1,495 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import <Cocoa/Cocoa.h>
+#include "DocAccessible.h"
+
+#import "MOXTextMarkerDelegate.h"
+
+#include "mozAccessible.h"
+#include "mozilla/Preferences.h"
+#include "nsISelectionListener.h"
+
+using namespace mozilla::a11y;
+
+#define PREF_ACCESSIBILITY_MAC_DEBUG "accessibility.mac.debug"
+
+static nsTHashMap<nsPtrHashKey<mozilla::a11y::Accessible>,
+ MOXTextMarkerDelegate*>
+ sDelegates;
+
+@implementation MOXTextMarkerDelegate
+
++ (id)getOrCreateForDoc:(mozilla::a11y::Accessible*)aDoc {
+ MOZ_ASSERT(aDoc);
+
+ MOXTextMarkerDelegate* delegate = sDelegates.Get(aDoc);
+ if (!delegate) {
+ delegate = [[MOXTextMarkerDelegate alloc] initWithDoc:aDoc];
+ sDelegates.InsertOrUpdate(aDoc, delegate);
+ [delegate retain];
+ }
+
+ return delegate;
+}
+
++ (void)destroyForDoc:(mozilla::a11y::Accessible*)aDoc {
+ MOZ_ASSERT(aDoc);
+
+ MOXTextMarkerDelegate* delegate = sDelegates.Get(aDoc);
+ if (delegate) {
+ sDelegates.Remove(aDoc);
+ [delegate release];
+ }
+}
+
+- (id)initWithDoc:(Accessible*)aDoc {
+ MOZ_ASSERT(aDoc, "Cannot init MOXTextDelegate with null");
+ if ((self = [super init])) {
+ mGeckoDocAccessible = aDoc;
+ }
+
+ mCaretMoveGranularity = nsISelectionListener::NO_AMOUNT;
+
+ return self;
+}
+
+- (void)dealloc {
+ [self invalidateSelection];
+ [super dealloc];
+}
+
+- (void)setSelectionFrom:(Accessible*)startContainer
+ at:(int32_t)startOffset
+ to:(Accessible*)endContainer
+ at:(int32_t)endOffset {
+ GeckoTextMarkerRange selection(GeckoTextMarker(startContainer, startOffset),
+ GeckoTextMarker(endContainer, endOffset));
+
+ // We store it as an AXTextMarkerRange because it is a safe
+ // way to keep a weak reference - when we need to use the
+ // range we can convert it back to a GeckoTextMarkerRange
+ // and check that it's valid.
+ mSelection = selection.CreateAXTextMarkerRange();
+ CFRetain(mSelection);
+}
+
+- (void)setCaretOffset:(mozilla::a11y::Accessible*)container
+ at:(int32_t)offset
+ moveGranularity:(int32_t)granularity {
+ GeckoTextMarker caretMarker(container, offset);
+
+ mPrevCaret = mCaret;
+ mCaret = caretMarker.CreateAXTextMarker();
+ mCaretMoveGranularity = granularity;
+
+ CFRetain(mCaret);
+}
+
+// This returns an info object to pass with AX SelectedTextChanged events.
+// It uses the current and previous caret position to make decisions
+// regarding which attributes to add to the info object.
+- (NSDictionary*)selectionChangeInfo {
+ GeckoTextMarkerRange selectedGeckoRange =
+ GeckoTextMarkerRange(mGeckoDocAccessible, mSelection);
+
+ int32_t stateChangeType = selectedGeckoRange.mStart == selectedGeckoRange.mEnd
+ ? AXTextStateChangeTypeSelectionMove
+ : AXTextStateChangeTypeSelectionExtend;
+
+ // This is the base info object, includes the selected marker range and
+ // the change type depending on the collapsed state of the selection.
+ NSMutableDictionary* info = [[@{
+ @"AXSelectedTextMarkerRange" : selectedGeckoRange.IsValid()
+ ? (__bridge id)mSelection
+ : [NSNull null],
+ @"AXTextStateChangeType" : @(stateChangeType),
+ } mutableCopy] autorelease];
+
+ GeckoTextMarker caretMarker(mGeckoDocAccessible, mCaret);
+ GeckoTextMarker prevCaretMarker(mGeckoDocAccessible, mPrevCaret);
+
+ if (!caretMarker.IsValid()) {
+ // If the current caret is invalid, stop here and return base info.
+ return info;
+ }
+
+ mozAccessible* caretEditable =
+ [GetNativeFromGeckoAccessible(caretMarker.mContainer)
+ moxEditableAncestor];
+
+ if (!caretEditable && stateChangeType == AXTextStateChangeTypeSelectionMove) {
+ // If we are not in an editable, VO expects AXTextStateSync to be present
+ // and true.
+ info[@"AXTextStateSync"] = @YES;
+ }
+
+ if (!prevCaretMarker.IsValid() || caretMarker == prevCaretMarker) {
+ // If we have no stored previous marker, stop here.
+ return info;
+ }
+
+ mozAccessible* prevCaretEditable =
+ [GetNativeFromGeckoAccessible(prevCaretMarker.mContainer)
+ moxEditableAncestor];
+
+ if (prevCaretEditable != caretEditable) {
+ // If the caret goes in or out of an editable, consider the
+ // move direction "discontiguous".
+ info[@"AXTextSelectionDirection"] =
+ @(AXTextSelectionDirectionDiscontiguous);
+ if ([[caretEditable moxFocused] boolValue]) {
+ // If the caret is in a new focused editable, VO expects this attribute to
+ // be present and to be true.
+ info[@"AXTextSelectionChangedFocus"] = @YES;
+ }
+
+ return info;
+ }
+
+ bool isForward = prevCaretMarker < caretMarker;
+ int direction = isForward ? AXTextSelectionDirectionNext
+ : AXTextSelectionDirectionPrevious;
+
+ int32_t granularity = AXTextSelectionGranularityUnknown;
+ switch (mCaretMoveGranularity) {
+ case nsISelectionListener::CHARACTER_AMOUNT:
+ case nsISelectionListener::CLUSTER_AMOUNT:
+ granularity = AXTextSelectionGranularityCharacter;
+ break;
+ case nsISelectionListener::WORD_AMOUNT:
+ case nsISelectionListener::WORDNOSPACE_AMOUNT:
+ granularity = AXTextSelectionGranularityWord;
+ break;
+ case nsISelectionListener::LINE_AMOUNT:
+ granularity = AXTextSelectionGranularityLine;
+ break;
+ case nsISelectionListener::BEGINLINE_AMOUNT:
+ direction = AXTextSelectionDirectionBeginning;
+ granularity = AXTextSelectionGranularityLine;
+ break;
+ case nsISelectionListener::ENDLINE_AMOUNT:
+ direction = AXTextSelectionDirectionEnd;
+ granularity = AXTextSelectionGranularityLine;
+ break;
+ case nsISelectionListener::PARAGRAPH_AMOUNT:
+ granularity = AXTextSelectionGranularityParagraph;
+ break;
+ default:
+ break;
+ }
+
+ // Determine selection direction with marker comparison.
+ // If the delta between the two markers is more than one, consider it
+ // a word. Not accurate, but good enough for VO.
+ [info addEntriesFromDictionary:@{
+ @"AXTextSelectionDirection" : @(direction),
+ @"AXTextSelectionGranularity" : @(granularity)
+ }];
+
+ return info;
+}
+
+- (void)invalidateSelection {
+ CFRelease(mSelection);
+ CFRelease(mCaret);
+ CFRelease(mPrevCaret);
+ mSelection = nil;
+}
+
+- (mozilla::a11y::GeckoTextMarkerRange)selection {
+ return mozilla::a11y::GeckoTextMarkerRange(mGeckoDocAccessible, mSelection);
+}
+
+- (AXTextMarkerRef)moxStartTextMarker {
+ GeckoTextMarker geckoTextPoint(mGeckoDocAccessible, 0);
+ return geckoTextPoint.CreateAXTextMarker();
+}
+
+- (AXTextMarkerRef)moxEndTextMarker {
+ uint32_t characterCount =
+ mGeckoDocAccessible->IsRemote()
+ ? mGeckoDocAccessible->AsRemote()->CharacterCount()
+ : mGeckoDocAccessible->AsLocal()
+ ->Document()
+ ->AsHyperText()
+ ->CharacterCount();
+ GeckoTextMarker geckoTextPoint(mGeckoDocAccessible, characterCount);
+ return geckoTextPoint.CreateAXTextMarker();
+}
+
+- (AXTextMarkerRangeRef)moxSelectedTextMarkerRange {
+ return mSelection &&
+ GeckoTextMarkerRange(mGeckoDocAccessible, mSelection).IsValid()
+ ? mSelection
+ : nil;
+}
+
+- (NSString*)moxStringForTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange {
+ mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible,
+ textMarkerRange);
+ if (!range.IsValid()) {
+ return @"";
+ }
+
+ return range.Text();
+}
+
+- (NSNumber*)moxLengthForTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange {
+ return @([[self moxStringForTextMarkerRange:textMarkerRange] length]);
+}
+
+- (AXTextMarkerRangeRef)moxTextMarkerRangeForUnorderedTextMarkers:
+ (NSArray*)textMarkers {
+ if ([textMarkers count] != 2) {
+ // Don't allow anything but a two member array.
+ return nil;
+ }
+
+ GeckoTextMarker p1(mGeckoDocAccessible,
+ (__bridge AXTextMarkerRef)textMarkers[0]);
+ GeckoTextMarker p2(mGeckoDocAccessible,
+ (__bridge AXTextMarkerRef)textMarkers[1]);
+
+ if (!p1.IsValid() || !p2.IsValid()) {
+ // If either marker is invalid, return nil.
+ return nil;
+ }
+
+ bool ordered = p1 < p2;
+ GeckoTextMarkerRange range(ordered ? p1 : p2, ordered ? p2 : p1);
+
+ return range.CreateAXTextMarkerRange();
+}
+
+- (AXTextMarkerRef)moxStartTextMarkerForTextMarkerRange:
+ (AXTextMarkerRangeRef)textMarkerRange {
+ mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible,
+ textMarkerRange);
+
+ return range.IsValid() ? range.mStart.CreateAXTextMarker() : nil;
+}
+
+- (AXTextMarkerRef)moxEndTextMarkerForTextMarkerRange:
+ (AXTextMarkerRangeRef)textMarkerRange {
+ mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible,
+ textMarkerRange);
+
+ return range.IsValid() ? range.mEnd.CreateAXTextMarker() : nil;
+}
+
+- (AXTextMarkerRangeRef)moxLeftWordTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef)textMarker {
+ GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker);
+ if (!geckoTextMarker.IsValid()) {
+ return nil;
+ }
+
+ return geckoTextMarker.Range(EWhichRange::eLeftWord)
+ .CreateAXTextMarkerRange();
+}
+
+- (AXTextMarkerRangeRef)moxRightWordTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef)textMarker {
+ GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker);
+ if (!geckoTextMarker.IsValid()) {
+ return nil;
+ }
+
+ return geckoTextMarker.Range(EWhichRange::eRightWord)
+ .CreateAXTextMarkerRange();
+}
+
+- (AXTextMarkerRangeRef)moxLineTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef)textMarker {
+ GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker);
+ if (!geckoTextMarker.IsValid()) {
+ return nil;
+ }
+
+ return geckoTextMarker.Range(EWhichRange::eLine).CreateAXTextMarkerRange();
+}
+
+- (AXTextMarkerRangeRef)moxLeftLineTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef)textMarker {
+ GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker);
+ if (!geckoTextMarker.IsValid()) {
+ return nil;
+ }
+
+ return geckoTextMarker.Range(EWhichRange::eLeftLine)
+ .CreateAXTextMarkerRange();
+}
+
+- (AXTextMarkerRangeRef)moxRightLineTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef)textMarker {
+ GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker);
+ if (!geckoTextMarker.IsValid()) {
+ return nil;
+ }
+
+ return geckoTextMarker.Range(EWhichRange::eRightLine)
+ .CreateAXTextMarkerRange();
+}
+
+- (AXTextMarkerRangeRef)moxParagraphTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef)textMarker {
+ GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker);
+ if (!geckoTextMarker.IsValid()) {
+ return nil;
+ }
+
+ return geckoTextMarker.Range(EWhichRange::eParagraph)
+ .CreateAXTextMarkerRange();
+}
+
+// override
+- (AXTextMarkerRangeRef)moxStyleTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef)textMarker {
+ GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker);
+ if (!geckoTextMarker.IsValid()) {
+ return nil;
+ }
+
+ return geckoTextMarker.Range(EWhichRange::eStyle).CreateAXTextMarkerRange();
+}
+
+- (AXTextMarkerRef)moxNextTextMarkerForTextMarker:(AXTextMarkerRef)textMarker {
+ GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker);
+ if (!geckoTextMarker.IsValid()) {
+ return nil;
+ }
+
+ if (!geckoTextMarker.Next()) {
+ return nil;
+ }
+
+ return geckoTextMarker.CreateAXTextMarker();
+}
+
+- (AXTextMarkerRef)moxPreviousTextMarkerForTextMarker:
+ (AXTextMarkerRef)textMarker {
+ GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker);
+ if (!geckoTextMarker.IsValid()) {
+ return nil;
+ }
+
+ if (!geckoTextMarker.Previous()) {
+ return nil;
+ }
+
+ return geckoTextMarker.CreateAXTextMarker();
+}
+
+- (NSAttributedString*)moxAttributedStringForTextMarkerRange:
+ (AXTextMarkerRangeRef)textMarkerRange {
+ mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible,
+ textMarkerRange);
+ if (!range.IsValid()) {
+ return nil;
+ }
+
+ return range.AttributedText();
+}
+
+- (NSValue*)moxBoundsForTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange {
+ mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible,
+ textMarkerRange);
+ if (!range.IsValid()) {
+ return nil;
+ }
+
+ return range.Bounds();
+}
+
+- (NSNumber*)moxIndexForTextMarker:(AXTextMarkerRef)textMarker {
+ GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker);
+ if (!geckoTextMarker.IsValid()) {
+ return nil;
+ }
+
+ GeckoTextMarkerRange range(GeckoTextMarker(mGeckoDocAccessible, 0),
+ geckoTextMarker);
+
+ return @(range.Length());
+}
+
+- (AXTextMarkerRef)moxTextMarkerForIndex:(NSNumber*)index {
+ GeckoTextMarker geckoTextMarker = GeckoTextMarker::MarkerFromIndex(
+ mGeckoDocAccessible, [index integerValue]);
+ if (!geckoTextMarker.IsValid()) {
+ return nil;
+ }
+
+ return geckoTextMarker.CreateAXTextMarker();
+}
+
+- (id)moxUIElementForTextMarker:(AXTextMarkerRef)textMarker {
+ GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker);
+ if (!geckoTextMarker.IsValid()) {
+ return nil;
+ }
+
+ Accessible* leaf = geckoTextMarker.Leaf();
+ if (!leaf) {
+ return nil;
+ }
+
+ return GetNativeFromGeckoAccessible(leaf);
+}
+
+- (AXTextMarkerRangeRef)moxTextMarkerRangeForUIElement:(id)element {
+ if (![element isKindOfClass:[mozAccessible class]]) {
+ return nil;
+ }
+
+ GeckoTextMarkerRange range((Accessible*)[element geckoAccessible]);
+ return range.CreateAXTextMarkerRange();
+}
+
+- (NSString*)moxMozDebugDescriptionForTextMarker:(AXTextMarkerRef)textMarker {
+ if (!mozilla::Preferences::GetBool(PREF_ACCESSIBILITY_MAC_DEBUG)) {
+ return nil;
+ }
+
+ GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker);
+ if (!geckoTextMarker.IsValid()) {
+ return @"<GeckoTextMarker 0x0 [0]>";
+ }
+
+ return [NSString stringWithFormat:@"<GeckoTextMarker %p [%d]>",
+ geckoTextMarker.mContainer,
+ geckoTextMarker.mOffset];
+}
+
+- (NSString*)moxMozDebugDescriptionForTextMarkerRange:
+ (AXTextMarkerRangeRef)textMarkerRange {
+ if (!mozilla::Preferences::GetBool(PREF_ACCESSIBILITY_MAC_DEBUG)) {
+ return nil;
+ }
+
+ mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible,
+ textMarkerRange);
+ if (!range.IsValid()) {
+ return @"<GeckoTextMarkerRange 0x0 [0] - 0x0 [0]>";
+ }
+
+ return
+ [NSString stringWithFormat:@"<GeckoTextMarkerRange %p [%d] - %p [%d]>",
+ range.mStart.mContainer, range.mStart.mOffset,
+ range.mEnd.mContainer, range.mEnd.mOffset];
+}
+
+- (void)moxSetSelectedTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange {
+ mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible,
+ textMarkerRange);
+ if (range.IsValid()) {
+ range.Select();
+ }
+}
+
+@end
diff --git a/accessible/mac/MOXWebAreaAccessible.h b/accessible/mac/MOXWebAreaAccessible.h
new file mode 100644
index 0000000000..1ef11af50c
--- /dev/null
+++ b/accessible/mac/MOXWebAreaAccessible.h
@@ -0,0 +1,105 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "mozAccessible.h"
+
+using namespace mozilla::a11y;
+
+@class MOXRootGroup;
+
+@interface MOXWebAreaAccessible : mozAccessible {
+ MOXRootGroup* mRootGroup;
+}
+// overrides
+- (NSString*)moxRole;
+
+// overrides
+- (NSString*)moxRoleDescription;
+
+// overrides
+- (NSURL*)moxURL;
+
+// override
+- (NSNumber*)moxLoaded;
+
+// override
+- (NSNumber*)moxLoadingProgress;
+
+// override
+- (NSArray*)moxLinkUIElements;
+
+// override
+- (NSArray*)moxUnignoredChildren;
+
+// override
+- (BOOL)moxBlockSelector:(SEL)selector;
+
+// override
+- (void)moxPostNotification:(NSString*)notification;
+
+// override
+- (void)handleAccessibleEvent:(uint32_t)eventType;
+
+// override
+- (void)dealloc;
+
+- (NSArray*)rootGroupChildren;
+
+- (id)rootGroup;
+
+@end
+
+@interface MOXRootGroup : MOXAccessibleBase {
+ MOXWebAreaAccessible* mParent;
+}
+
+// override
+- (id)initWithParent:(MOXWebAreaAccessible*)parent;
+
+// override
+- (NSString*)moxRole;
+
+// override
+- (NSString*)moxRoleDescription;
+
+// override
+- (id<mozAccessible>)moxParent;
+
+// override
+- (NSArray*)moxChildren;
+
+// override
+- (NSString*)moxIdentifier;
+
+// override
+- (NSString*)moxSubrole;
+
+// override
+- (id)moxHitTest:(NSPoint)point;
+
+// override
+- (NSValue*)moxPosition;
+
+// override
+- (NSValue*)moxSize;
+
+// override
+- (NSArray*)moxUIElementsForSearchPredicate:(NSDictionary*)searchPredicate;
+
+// override
+- (NSNumber*)moxUIElementCountForSearchPredicate:(NSDictionary*)searchPredicate;
+
+// override
+- (BOOL)disableChild:(id)child;
+
+// override
+- (void)expire;
+
+// override
+- (BOOL)isExpired;
+
+@end
diff --git a/accessible/mac/MOXWebAreaAccessible.mm b/accessible/mac/MOXWebAreaAccessible.mm
new file mode 100644
index 0000000000..148d391542
--- /dev/null
+++ b/accessible/mac/MOXWebAreaAccessible.mm
@@ -0,0 +1,274 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "MOXWebAreaAccessible.h"
+
+#import "MOXSearchInfo.h"
+
+#include "nsAccUtils.h"
+#include "nsCocoaUtils.h"
+#include "DocAccessibleParent.h"
+
+using namespace mozilla::a11y;
+
+@implementation MOXRootGroup
+
+- (id)initWithParent:(MOXWebAreaAccessible*)parent {
+ // The parent is always a MOXWebAreaAccessible
+ mParent = parent;
+ return [super init];
+}
+
+- (NSString*)moxRole {
+ return NSAccessibilityGroupRole;
+}
+
+- (NSString*)moxRoleDescription {
+ if ([[self moxSubrole] isEqualToString:@"AXLandmarkApplication"]) {
+ return utils::LocalizedString(u"application"_ns);
+ }
+
+ return NSAccessibilityRoleDescription(NSAccessibilityGroupRole, nil);
+}
+
+- (id<mozAccessible>)moxParent {
+ return mParent;
+}
+
+- (NSArray*)moxChildren {
+ // Reparent the children of the web area here.
+ return [mParent rootGroupChildren];
+}
+
+- (NSString*)moxIdentifier {
+ // This is mostly for testing purposes to assert that this is the generated
+ // root group.
+ return @"root-group";
+}
+
+- (NSString*)moxSubrole {
+ // Steal the subrole internally mapped to the web area.
+ return [mParent moxSubrole];
+}
+
+- (id)moxHitTest:(NSPoint)point {
+ return [mParent moxHitTest:point];
+}
+
+- (NSValue*)moxPosition {
+ return [mParent moxPosition];
+}
+
+- (NSValue*)moxSize {
+ return [mParent moxSize];
+}
+
+- (NSArray*)moxUIElementsForSearchPredicate:(NSDictionary*)searchPredicate {
+ MOXSearchInfo* search =
+ [[[MOXSearchInfo alloc] initWithParameters:searchPredicate
+ andRoot:self] autorelease];
+
+ return [search performSearch];
+}
+
+- (NSNumber*)moxUIElementCountForSearchPredicate:
+ (NSDictionary*)searchPredicate {
+ return [NSNumber
+ numberWithDouble:[[self moxUIElementsForSearchPredicate:searchPredicate]
+ count]];
+}
+
+- (BOOL)disableChild:(id)child {
+ return NO;
+}
+
+- (void)expire {
+ mParent = nil;
+ [super expire];
+}
+
+- (BOOL)isExpired {
+ MOZ_ASSERT((mParent == nil) == mIsExpired);
+
+ return [super isExpired];
+}
+
+@end
+
+@implementation MOXWebAreaAccessible
+
+- (NSString*)moxRole {
+ // The OS role is AXWebArea regardless of the gecko role
+ // (APPLICATION or DOCUMENT).
+ // If the web area has a role of APPLICATION, its root group will
+ // reflect that in a subrole/description.
+ return @"AXWebArea";
+}
+
+- (NSString*)moxRoleDescription {
+ // The role description is "HTML Content" regardless of the gecko role
+ // (APPLICATION or DOCUMENT)
+ return utils::LocalizedString(u"htmlContent"_ns);
+}
+
+- (NSURL*)moxURL {
+ if ([self isExpired]) {
+ return nil;
+ }
+
+ nsAutoString url;
+ MOZ_ASSERT(mGeckoAccessible->IsDoc());
+ nsAccUtils::DocumentURL(mGeckoAccessible, url);
+
+ if (url.IsEmpty()) {
+ return nil;
+ }
+
+ return [NSURL URLWithString:nsCocoaUtils::ToNSString(url)];
+}
+
+- (NSNumber*)moxLoaded {
+ if ([self isExpired]) {
+ return nil;
+ }
+ // We are loaded if we aren't busy or stale
+ return @([self stateWithMask:(states::BUSY & states::STALE)] == 0);
+}
+
+// overrides
+- (NSNumber*)moxLoadingProgress {
+ if ([self isExpired]) {
+ return nil;
+ }
+
+ if ([self stateWithMask:states::STALE] != 0) {
+ // We expose stale state until the document is ready (DOM is loaded and tree
+ // is constructed) so we indicate load hasn't started while this state is
+ // present.
+ return @0.0;
+ }
+
+ if ([self stateWithMask:states::BUSY] != 0) {
+ // We expose state busy until the document and all its subdocuments are
+ // completely loaded, so we indicate partial loading here
+ return @0.5;
+ }
+
+ // if we are not busy and not stale, we are loaded
+ return @1.0;
+}
+
+- (NSArray*)moxLinkUIElements {
+ NSDictionary* searchPredicate = @{
+ @"AXSearchKey" : @"AXLinkSearchKey",
+ @"AXImmediateDescendantsOnly" : @NO,
+ @"AXResultsLimit" : @(-1),
+ @"AXDirection" : @"AXDirectionNext",
+ };
+
+ return [self moxUIElementsForSearchPredicate:searchPredicate];
+}
+
+- (void)handleAccessibleEvent:(uint32_t)eventType {
+ switch (eventType) {
+ case nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE:
+ [self moxPostNotification:
+ NSAccessibilityFocusedUIElementChangedNotification];
+ MOZ_ASSERT(mGeckoAccessible->IsRemote() ||
+ mGeckoAccessible->AsLocal()->IsRoot() ||
+ mGeckoAccessible->AsLocal()->AsDoc()->ParentDocument(),
+ "Non-root doc without a parent!");
+ if ((mGeckoAccessible->IsRemote() &&
+ mGeckoAccessible->AsRemote()->IsDoc() &&
+ mGeckoAccessible->AsRemote()->AsDoc()->IsTopLevel()) ||
+ (mGeckoAccessible->IsLocal() &&
+ !mGeckoAccessible->AsLocal()->IsRoot() &&
+ mGeckoAccessible->AsLocal()->AsDoc()->ParentDocument() &&
+ mGeckoAccessible->AsLocal()->AsDoc()->ParentDocument()->IsRoot())) {
+ // we fire an AXLoadComplete event on top-level documents only
+ [self moxPostNotification:@"AXLoadComplete"];
+ } else {
+ // otherwise the doc belongs to an iframe (IsTopLevelInContentProcess)
+ // and we fire AXLayoutComplete instead
+ [self moxPostNotification:@"AXLayoutComplete"];
+ }
+ break;
+ }
+
+ [super handleAccessibleEvent:eventType];
+}
+
+- (NSArray*)rootGroupChildren {
+ // This method is meant to expose the doc's children to the root group.
+ return [super moxChildren];
+}
+
+- (NSArray*)moxUnignoredChildren {
+ if (id rootGroup = [self rootGroup]) {
+ return @[ [self rootGroup] ];
+ }
+
+ // There is no root group, expose the children here directly.
+ return [super moxUnignoredChildren];
+}
+
+- (BOOL)moxBlockSelector:(SEL)selector {
+ if (selector == @selector(moxSubrole)) {
+ // Never expose a subrole for a web area.
+ return YES;
+ }
+
+ if (selector == @selector(moxElementBusy)) {
+ // Don't confuse aria-busy with a document's busy state.
+ return YES;
+ }
+
+ return [super moxBlockSelector:selector];
+}
+
+- (void)moxPostNotification:(NSString*)notification {
+ if (![notification isEqualToString:@"AXElementBusyChanged"]) {
+ // Suppress AXElementBusyChanged since it uses gecko's BUSY state
+ // to tell VoiceOver about aria-busy changes. We use that state
+ // differently in documents.
+ [super moxPostNotification:notification];
+ }
+}
+
+- (id)rootGroup {
+ NSArray* children = [super moxUnignoredChildren];
+ if (mRole == roles::DOCUMENT && [children count] == 1 &&
+ [[[children firstObject] moxUnignoredChildren] count] != 0) {
+ // We only need a root group if our document:
+ // (1) has multiple children, or
+ // (2) a one child that is a leaf, or
+ // (3) has a role other than the default document role
+ return nil;
+ }
+
+ if (!mRootGroup) {
+ mRootGroup = [[MOXRootGroup alloc] initWithParent:self];
+ }
+
+ return mRootGroup;
+}
+
+- (void)expire {
+ [mRootGroup expire];
+ [super expire];
+}
+
+- (void)dealloc {
+ // This object can only be dealoced after the gecko accessible wrapper
+ // reference is released, and that happens after expire is called.
+ MOZ_ASSERT([self isExpired]);
+ [mRootGroup release];
+
+ [super dealloc];
+}
+
+@end
diff --git a/accessible/mac/MacUtils.h b/accessible/mac/MacUtils.h
new file mode 100644
index 0000000000..3f095a7996
--- /dev/null
+++ b/accessible/mac/MacUtils.h
@@ -0,0 +1,51 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _MacUtils_H_
+#define _MacUtils_H_
+
+#include "nsStringFwd.h"
+
+@class NSString;
+@class mozAccessible;
+
+namespace mozilla {
+namespace a11y {
+namespace utils {
+
+// convert an array of Gecko accessibles to an NSArray of native accessibles
+template <typename AccArray>
+NSArray<mozAccessible*>* ConvertToNSArray(AccArray& aArray) {
+ NSMutableArray* nativeArray = [[[NSMutableArray alloc] init] autorelease];
+
+ // iterate through the list, and get each native accessible.
+ for (Accessible* curAccessible : aArray) {
+ mozAccessible* curNative = GetNativeFromGeckoAccessible(curAccessible);
+ if (curNative)
+ [nativeArray addObject:GetObjectOrRepresentedView(curNative)];
+ }
+
+ return nativeArray;
+}
+
+/**
+ * Get a localized string from the string bundle.
+ * Return nil if not found.
+ */
+NSString* LocalizedString(const nsString& aString);
+
+/**
+ * Gets an accessible atttribute from the mozAccessible's associated
+ * accessible wrapper or proxy, and returns the value as an NSString.
+ * nil if no attribute is found.
+ */
+NSString* GetAccAttr(mozAccessible* aNativeAccessible, nsAtom* aAttrName);
+}
+}
+}
+
+#endif
diff --git a/accessible/mac/MacUtils.mm b/accessible/mac/MacUtils.mm
new file mode 100644
index 0000000000..7fbb0e9ba4
--- /dev/null
+++ b/accessible/mac/MacUtils.mm
@@ -0,0 +1,49 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "MacUtils.h"
+
+#include "LocalAccessible.h"
+#include "nsCocoaUtils.h"
+#include "mozilla/a11y/PDocAccessible.h"
+
+namespace mozilla {
+namespace a11y {
+namespace utils {
+
+/**
+ * Get a localized string from the a11y string bundle.
+ * Return nil if not found.
+ */
+NSString* LocalizedString(const nsString& aString) {
+ nsString text;
+
+ Accessible::TranslateString(aString, text);
+
+ return text.IsEmpty() ? nil : nsCocoaUtils::ToNSString(text);
+}
+
+NSString* GetAccAttr(mozAccessible* aNativeAccessible, nsAtom* aAttrName) {
+ nsAutoString result;
+ Accessible* acc = [aNativeAccessible geckoAccessible];
+ RefPtr<AccAttributes> attributes = acc->Attributes();
+
+ if (!attributes) {
+ return nil;
+ }
+
+ attributes->GetAttribute(aAttrName, result);
+
+ if (!result.IsEmpty()) {
+ return nsCocoaUtils::ToNSString(result);
+ }
+
+ return nil;
+}
+}
+}
+}
diff --git a/accessible/mac/Platform.mm b/accessible/mac/Platform.mm
new file mode 100644
index 0000000000..c0d8273af0
--- /dev/null
+++ b/accessible/mac/Platform.mm
@@ -0,0 +1,243 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import <Cocoa/Cocoa.h>
+
+#import "MOXTextMarkerDelegate.h"
+
+#include "Platform.h"
+#include "RemoteAccessible.h"
+#include "DocAccessibleParent.h"
+#include "mozTableAccessible.h"
+#include "MOXWebAreaAccessible.h"
+
+#include "nsAppShell.h"
+#include "mozilla/Telemetry.h"
+
+// Available from 10.13 onwards; test availability at runtime before using
+@interface NSWorkspace (AvailableSinceHighSierra)
+@property(readonly) BOOL isVoiceOverEnabled;
+@property(readonly) BOOL isSwitchControlEnabled;
+@end
+
+namespace mozilla {
+namespace a11y {
+
+// Mac a11y whitelisting
+static bool sA11yShouldBeEnabled = false;
+
+bool ShouldA11yBeEnabled() {
+ EPlatformDisabledState disabledState = PlatformDisabledState();
+ return (disabledState == ePlatformIsForceEnabled) ||
+ ((disabledState == ePlatformIsEnabled) && sA11yShouldBeEnabled);
+}
+
+void PlatformInit() {}
+
+void PlatformShutdown() {}
+
+void ProxyCreated(RemoteAccessible* aProxy) {
+ if (aProxy->Role() == roles::WHITESPACE) {
+ // We don't create a native object if we're child of a "flat" accessible;
+ // for example, on OS X buttons shouldn't have any children, because that
+ // makes the OS confused. We also don't create accessibles for <br>
+ // (whitespace) elements.
+ return;
+ }
+
+ // Pass in dummy state for now as retrieving proxy state requires IPC.
+ // Note that we can use RemoteAccessible::IsTable* functions here because they
+ // do not use IPC calls but that might change after bug 1210477.
+ Class type;
+ if (aProxy->IsTable()) {
+ type = [mozTableAccessible class];
+ } else if (aProxy->IsTableRow()) {
+ type = [mozTableRowAccessible class];
+ } else if (aProxy->IsTableCell()) {
+ type = [mozTableCellAccessible class];
+ } else if (aProxy->IsDoc()) {
+ type = [MOXWebAreaAccessible class];
+ } else {
+ type = GetTypeFromRole(aProxy->Role());
+ }
+
+ mozAccessible* mozWrapper = [[type alloc] initWithAccessible:aProxy];
+ aProxy->SetWrapper(reinterpret_cast<uintptr_t>(mozWrapper));
+}
+
+void ProxyDestroyed(RemoteAccessible* aProxy) {
+ mozAccessible* wrapper = GetNativeFromGeckoAccessible(aProxy);
+ [wrapper expire];
+ [wrapper release];
+ aProxy->SetWrapper(0);
+
+ if (aProxy->IsDoc()) {
+ [MOXTextMarkerDelegate destroyForDoc:aProxy];
+ }
+}
+
+void ProxyEvent(RemoteAccessible* aProxy, uint32_t aEventType) {
+ // Ignore event that we don't escape below, they aren't yet supported.
+ if (aEventType != nsIAccessibleEvent::EVENT_ALERT &&
+ aEventType != nsIAccessibleEvent::EVENT_FOCUS &&
+ aEventType != nsIAccessibleEvent::EVENT_VALUE_CHANGE &&
+ aEventType != nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE &&
+ aEventType != nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED &&
+ aEventType != nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE &&
+ aEventType != nsIAccessibleEvent::EVENT_REORDER &&
+ aEventType != nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED &&
+ aEventType != nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED &&
+ aEventType != nsIAccessibleEvent::EVENT_NAME_CHANGE &&
+ aEventType != nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED &&
+ aEventType != nsIAccessibleEvent::EVENT_TABLE_STYLING_CHANGED)
+ return;
+
+ mozAccessible* wrapper = GetNativeFromGeckoAccessible(aProxy);
+ if (wrapper) {
+ [wrapper handleAccessibleEvent:aEventType];
+ }
+}
+
+void ProxyStateChangeEvent(RemoteAccessible* aProxy, uint64_t aState,
+ bool aEnabled) {
+ mozAccessible* wrapper = GetNativeFromGeckoAccessible(aProxy);
+ if (wrapper) {
+ [wrapper stateChanged:aState isEnabled:aEnabled];
+ }
+}
+
+void ProxyCaretMoveEvent(RemoteAccessible* aTarget, int32_t aOffset,
+ bool aIsSelectionCollapsed, int32_t aGranularity) {
+ mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget);
+ MOXTextMarkerDelegate* delegate =
+ [MOXTextMarkerDelegate getOrCreateForDoc:aTarget->Document()];
+ [delegate setCaretOffset:aTarget at:aOffset moveGranularity:aGranularity];
+ if (aIsSelectionCollapsed) {
+ // If selection is collapsed, invalidate selection.
+ [delegate setSelectionFrom:aTarget at:aOffset to:aTarget at:aOffset];
+ }
+
+ if (wrapper) {
+ if (mozTextAccessible* textAcc =
+ static_cast<mozTextAccessible*>([wrapper moxEditableAncestor])) {
+ [textAcc
+ handleAccessibleEvent:nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED];
+ } else {
+ [wrapper
+ handleAccessibleEvent:nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED];
+ }
+ }
+}
+
+void ProxyTextChangeEvent(RemoteAccessible* aTarget, const nsAString& aStr,
+ int32_t aStart, uint32_t aLen, bool aIsInsert,
+ bool aFromUser) {
+ RemoteAccessible* acc = aTarget;
+ // If there is a text input ancestor, use it as the event source.
+ while (acc && GetTypeFromRole(acc->Role()) != [mozTextAccessible class]) {
+ acc = acc->RemoteParent();
+ }
+ mozAccessible* wrapper = GetNativeFromGeckoAccessible(acc ? acc : aTarget);
+ [wrapper handleAccessibleTextChangeEvent:nsCocoaUtils::ToNSString(aStr)
+ inserted:aIsInsert
+ inContainer:aTarget
+ at:aStart];
+}
+
+void ProxyShowHideEvent(RemoteAccessible*, RemoteAccessible*, bool, bool) {}
+
+void ProxySelectionEvent(RemoteAccessible* aTarget, RemoteAccessible* aWidget,
+ uint32_t aEventType) {
+ mozAccessible* wrapper = GetNativeFromGeckoAccessible(aWidget);
+ if (wrapper) {
+ [wrapper handleAccessibleEvent:aEventType];
+ }
+}
+
+void ProxyTextSelectionChangeEvent(RemoteAccessible* aTarget,
+ const nsTArray<TextRangeData>& aSelection) {
+ if (aSelection.Length()) {
+ MOXTextMarkerDelegate* delegate =
+ [MOXTextMarkerDelegate getOrCreateForDoc:aTarget->Document()];
+ DocAccessibleParent* doc = aTarget->Document();
+ RemoteAccessible* startContainer =
+ doc->GetAccessible(aSelection[0].StartID());
+ RemoteAccessible* endContainer = doc->GetAccessible(aSelection[0].EndID());
+ // Cache the selection.
+ [delegate setSelectionFrom:startContainer
+ at:aSelection[0].StartOffset()
+ to:endContainer
+ at:aSelection[0].EndOffset()];
+ }
+
+ mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget);
+ if (wrapper) {
+ [wrapper
+ handleAccessibleEvent:nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED];
+ }
+}
+
+void ProxyRoleChangedEvent(RemoteAccessible* aTarget, const a11y::role& aRole,
+ uint8_t aRoleMapEntryIndex) {
+ if (mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget)) {
+ [wrapper handleRoleChanged:aRole];
+ }
+}
+
+} // namespace a11y
+} // namespace mozilla
+
+@interface GeckoNSApplication (a11y)
+- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute;
+@end
+
+@implementation GeckoNSApplication (a11y)
+
+- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute {
+ if ([attribute isEqualToString:@"AXEnhancedUserInterface"]) {
+ mozilla::a11y::sA11yShouldBeEnabled = ([value intValue] == 1);
+ if (sA11yShouldBeEnabled) {
+ // If accessibility should be enabled, log the appropriate client
+ nsAutoString client;
+ if ([[NSWorkspace sharedWorkspace]
+ respondsToSelector:@selector(isVoiceOverEnabled)] &&
+ [[NSWorkspace sharedWorkspace] isVoiceOverEnabled]) {
+ client.Assign(u"VoiceOver"_ns);
+ } else if ([[NSWorkspace sharedWorkspace]
+ respondsToSelector:@selector(isSwitchControlEnabled)] &&
+ [[NSWorkspace sharedWorkspace] isSwitchControlEnabled]) {
+ client.Assign(u"SwitchControl"_ns);
+ } else {
+ // This is more complicated than the NSWorkspace queries above
+ // because (a) there is no "full keyboard access" query for NSWorkspace
+ // and (b) the [NSApplication fullKeyboardAccessEnabled] query checks
+ // the pre-Monterey version of full keyboard access, which is not what
+ // we're looking for here. For more info, see bug 1772375 comment 7.
+ Boolean exists;
+ int val = CFPreferencesGetAppIntegerValue(
+ CFSTR("FullKeyboardAccessEnabled"),
+ CFSTR("com.apple.Accessibility"), &exists);
+ if (exists && val == 1) {
+ client.Assign(u"FullKeyboardAccess"_ns);
+ } else {
+ client.Assign(u"Unknown"_ns);
+ }
+ }
+
+#if defined(MOZ_TELEMETRY_REPORTING)
+ Telemetry::ScalarSet(Telemetry::ScalarID::A11Y_INSTANTIATORS, client);
+#endif // defined(MOZ_TELEMETRY_REPORTING)
+ CrashReporter::AnnotateCrashReport(
+ CrashReporter::Annotation::AccessibilityClient,
+ NS_ConvertUTF16toUTF8(client));
+ }
+ }
+
+ return [super accessibilitySetValue:value forAttribute:attribute];
+}
+
+@end
diff --git a/accessible/mac/PlatformExtTypes.h b/accessible/mac/PlatformExtTypes.h
new file mode 100644
index 0000000000..fdd33a4341
--- /dev/null
+++ b/accessible/mac/PlatformExtTypes.h
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_a11y_PlatformExtTypes_h__
+#define mozilla_a11y_PlatformExtTypes_h__
+
+namespace mozilla {
+namespace a11y {
+
+enum class EWhichRange {
+ eLeftWord,
+ eRightWord,
+ eLine,
+ eLeftLine,
+ eRightLine,
+ eParagraph,
+ eStyle
+};
+
+enum class EWhichPostFilter { eContainsText };
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif // mozilla_a11y_PlatformExtTypes_h__
diff --git a/accessible/mac/RootAccessibleWrap.h b/accessible/mac/RootAccessibleWrap.h
new file mode 100644
index 0000000000..632233cfcf
--- /dev/null
+++ b/accessible/mac/RootAccessibleWrap.h
@@ -0,0 +1,40 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* For documentation of the accessibility architecture,
+ * see http://lxr.mozilla.org/seamonkey/source/accessible/accessible-docs.html
+ */
+
+#ifndef mozilla_a11y_RootAccessibleWrap_h__
+#define mozilla_a11y_RootAccessibleWrap_h__
+
+#include "RootAccessible.h"
+
+namespace mozilla {
+
+class PresShell;
+
+namespace a11y {
+
+/**
+ * Mac specific functionality for the node at a root of the accessibility
+ * tree: see the RootAccessible superclass for further details.
+ */
+class RootAccessibleWrap : public RootAccessible {
+ public:
+ RootAccessibleWrap(dom::Document* aDocument, PresShell* aPresShell);
+ virtual ~RootAccessibleWrap();
+
+ Class GetNativeType();
+
+ // let's our native accessible get in touch with the
+ // native cocoa view that is our accessible parent.
+ void GetNativeWidget(void** aOutView);
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/mac/RootAccessibleWrap.mm b/accessible/mac/RootAccessibleWrap.mm
new file mode 100644
index 0000000000..e3d3da9224
--- /dev/null
+++ b/accessible/mac/RootAccessibleWrap.mm
@@ -0,0 +1,51 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "RootAccessibleWrap.h"
+
+#include "mozRootAccessible.h"
+
+#include "gfxPlatform.h"
+#include "nsCOMPtr.h"
+#include "nsObjCExceptions.h"
+#include "nsIFrame.h"
+#include "nsView.h"
+#include "nsIWidget.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+RootAccessibleWrap::RootAccessibleWrap(dom::Document* aDocument,
+ PresShell* aPresShell)
+ : RootAccessible(aDocument, aPresShell) {}
+
+RootAccessibleWrap::~RootAccessibleWrap() {}
+
+Class RootAccessibleWrap::GetNativeType() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return [mozRootAccessible class];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+void RootAccessibleWrap::GetNativeWidget(void** aOutView) {
+ nsIFrame* frame = GetFrame();
+ if (frame) {
+ nsView* view = frame->GetView();
+ if (view) {
+ nsIWidget* widget = view->GetWidget();
+ if (widget) {
+ *aOutView = (void**)widget->GetNativeData(NS_NATIVE_WIDGET);
+ MOZ_ASSERT(
+ *aOutView || gfxPlatform::IsHeadless(),
+ "Couldn't get the native NSView parent we need to connect the "
+ "accessibility hierarchy!");
+ }
+ }
+ }
+}
diff --git a/accessible/mac/RotorRules.h b/accessible/mac/RotorRules.h
new file mode 100644
index 0000000000..e7d44adf4d
--- /dev/null
+++ b/accessible/mac/RotorRules.h
@@ -0,0 +1,142 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "mozAccessible.h"
+#include "Pivot.h"
+
+using namespace mozilla::a11y;
+
+/**
+ * This rule matches all accessibles that satisfy the "boilerplate"
+ * pivot conditions and have a corresponding native accessible.
+ */
+class RotorRule : public PivotRule {
+ public:
+ explicit RotorRule(Accessible* aDirectDescendantsFrom);
+ explicit RotorRule();
+ uint16_t Match(Accessible* aAcc) override;
+
+ private:
+ Accessible* mDirectDescendantsFrom;
+};
+
+/**
+ * This rule matches all accessibles of a given role.
+ */
+class RotorRoleRule : public RotorRule {
+ public:
+ explicit RotorRoleRule(role aRole, Accessible* aDirectDescendantsFrom);
+ explicit RotorRoleRule(role aRole);
+ uint16_t Match(Accessible* aAcc) override;
+
+ private:
+ role mRole;
+};
+
+class RotorMacRoleRule : public RotorRule {
+ public:
+ explicit RotorMacRoleRule(NSString* aRole);
+ explicit RotorMacRoleRule(NSString* aRole,
+ Accessible* aDirectDescendantsFrom);
+ ~RotorMacRoleRule();
+ virtual uint16_t Match(Accessible* aAcc) override;
+
+ protected:
+ NSString* mMacRole;
+};
+
+class RotorControlRule final : public RotorRule {
+ public:
+ explicit RotorControlRule(Accessible* aDirectDescendantsFrom);
+ explicit RotorControlRule();
+
+ virtual uint16_t Match(Accessible* aAcc) override;
+};
+
+class RotorTextEntryRule final : public RotorRule {
+ public:
+ explicit RotorTextEntryRule(Accessible* aDirectDescendantsFrom);
+ explicit RotorTextEntryRule();
+
+ virtual uint16_t Match(Accessible* aAcc) override;
+};
+
+class RotorLinkRule : public RotorRule {
+ public:
+ explicit RotorLinkRule();
+ explicit RotorLinkRule(Accessible* aDirectDescendantsFrom);
+
+ virtual uint16_t Match(Accessible* aAcc) override;
+};
+
+class RotorVisitedLinkRule final : public RotorLinkRule {
+ public:
+ explicit RotorVisitedLinkRule();
+ explicit RotorVisitedLinkRule(Accessible* aDirectDescendantsFrom);
+
+ virtual uint16_t Match(Accessible* aAcc) override;
+};
+
+class RotorUnvisitedLinkRule final : public RotorLinkRule {
+ public:
+ explicit RotorUnvisitedLinkRule();
+ explicit RotorUnvisitedLinkRule(Accessible* aDirectDescendantsFrom);
+
+ virtual uint16_t Match(Accessible* aAcc) override;
+};
+
+/**
+ * This rule matches all accessibles that satisfy the "boilerplate"
+ * pivot conditions and have a corresponding native accessible.
+ */
+class RotorNotMacRoleRule : public RotorMacRoleRule {
+ public:
+ explicit RotorNotMacRoleRule(NSString* aMacRole,
+ Accessible* aDirectDescendantsFrom);
+ explicit RotorNotMacRoleRule(NSString* aMacRole);
+ uint16_t Match(Accessible* aAcc) override;
+};
+
+class RotorStaticTextRule : public RotorRule {
+ public:
+ explicit RotorStaticTextRule();
+ explicit RotorStaticTextRule(Accessible* aDirectDescendantsFrom);
+
+ virtual uint16_t Match(Accessible* aAcc) override;
+};
+
+class RotorHeadingLevelRule : public RotorRoleRule {
+ public:
+ explicit RotorHeadingLevelRule(int32_t aLevel);
+ explicit RotorHeadingLevelRule(int32_t aLevel,
+ Accessible* aDirectDescendantsFrom);
+
+ virtual uint16_t Match(Accessible* aAcc) override;
+
+ private:
+ int32_t mLevel;
+};
+
+class RotorLiveRegionRule : public RotorRule {
+ public:
+ explicit RotorLiveRegionRule(Accessible* aDirectDescendantsFrom)
+ : RotorRule(aDirectDescendantsFrom) {}
+ explicit RotorLiveRegionRule() : RotorRule() {}
+
+ uint16_t Match(Accessible* aAcc) override;
+};
+
+/**
+ * This rule matches all accessibles with roles::OUTLINEITEM. If
+ * outlines are nested, it ignores the nested subtree and returns
+ * only items which are descendants of the primary outline.
+ */
+class OutlineRule : public RotorRule {
+ public:
+ explicit OutlineRule();
+ uint16_t Match(Accessible* aAcc) override;
+};
diff --git a/accessible/mac/RotorRules.mm b/accessible/mac/RotorRules.mm
new file mode 100644
index 0000000000..02593b91f5
--- /dev/null
+++ b/accessible/mac/RotorRules.mm
@@ -0,0 +1,360 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "RotorRules.h"
+
+#include "nsCocoaUtils.h"
+#include "DocAccessibleParent.h"
+
+using namespace mozilla::a11y;
+
+// Generic Rotor Rule
+
+RotorRule::RotorRule(Accessible* aDirectDescendantsFrom)
+ : mDirectDescendantsFrom(aDirectDescendantsFrom) {}
+
+RotorRule::RotorRule() : mDirectDescendantsFrom(nullptr) {}
+
+uint16_t RotorRule::Match(Accessible* aAcc) {
+ uint16_t result = nsIAccessibleTraversalRule::FILTER_IGNORE;
+
+ if (nsAccUtils::MustPrune(aAcc)) {
+ result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
+ }
+
+ if (mDirectDescendantsFrom && (aAcc != mDirectDescendantsFrom)) {
+ result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
+ }
+
+ if ([GetNativeFromGeckoAccessible(aAcc) isAccessibilityElement]) {
+ result |= nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+
+ return result;
+}
+
+// Rotor Role Rule
+
+RotorRoleRule::RotorRoleRule(role aRole, Accessible* aDirectDescendantsFrom)
+ : RotorRule(aDirectDescendantsFrom), mRole(aRole){};
+
+RotorRoleRule::RotorRoleRule(role aRole) : RotorRule(), mRole(aRole){};
+
+uint16_t RotorRoleRule::Match(Accessible* aAcc) {
+ uint16_t result = RotorRule::Match(aAcc);
+
+ // if a match was found in the base-class's Match function,
+ // it is valid to consider that match again here. if it is
+ // not of the desired role, we flip the match bit to "unmatch"
+ // otherwise, the match persists.
+ if ((result & nsIAccessibleTraversalRule::FILTER_MATCH) &&
+ aAcc->Role() != mRole) {
+ result &= ~nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+
+ return result;
+}
+
+// Rotor Mac Role Rule
+
+RotorMacRoleRule::RotorMacRoleRule(NSString* aMacRole,
+ Accessible* aDirectDescendantsFrom)
+ : RotorRule(aDirectDescendantsFrom), mMacRole(aMacRole) {
+ [mMacRole retain];
+};
+
+RotorMacRoleRule::RotorMacRoleRule(NSString* aMacRole)
+ : RotorRule(), mMacRole(aMacRole) {
+ [mMacRole retain];
+};
+
+RotorMacRoleRule::~RotorMacRoleRule() { [mMacRole release]; }
+
+uint16_t RotorMacRoleRule::Match(Accessible* aAcc) {
+ uint16_t result = RotorRule::Match(aAcc);
+
+ // if a match was found in the base-class's Match function,
+ // it is valid to consider that match again here. if it is
+ // not of the desired role, we flip the match bit to "unmatch"
+ // otherwise, the match persists.
+ if ((result & nsIAccessibleTraversalRule::FILTER_MATCH)) {
+ mozAccessible* nativeMatch = GetNativeFromGeckoAccessible(aAcc);
+ if (![[nativeMatch moxRole] isEqualToString:mMacRole]) {
+ result &= ~nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+ }
+
+ return result;
+}
+
+// Rotor Control Rule
+
+RotorControlRule::RotorControlRule(Accessible* aDirectDescendantsFrom)
+ : RotorRule(aDirectDescendantsFrom){};
+
+RotorControlRule::RotorControlRule() : RotorRule(){};
+
+uint16_t RotorControlRule::Match(Accessible* aAcc) {
+ uint16_t result = RotorRule::Match(aAcc);
+
+ // if a match was found in the base-class's Match function,
+ // it is valid to consider that match again here. if it is
+ // not of the desired role, we flip the match bit to "unmatch"
+ // otherwise, the match persists.
+ if ((result & nsIAccessibleTraversalRule::FILTER_MATCH)) {
+ switch (aAcc->Role()) {
+ case roles::PUSHBUTTON:
+ case roles::SPINBUTTON:
+ case roles::DETAILS:
+ case roles::CHECKBUTTON:
+ case roles::COLOR_CHOOSER:
+ case roles::BUTTONDROPDOWNGRID: // xul colorpicker
+ case roles::LISTBOX:
+ case roles::COMBOBOX:
+ case roles::EDITCOMBOBOX:
+ case roles::RADIOBUTTON:
+ case roles::RADIO_GROUP:
+ case roles::PAGETAB:
+ case roles::SLIDER:
+ case roles::SWITCH:
+ case roles::ENTRY:
+ case roles::OUTLINE:
+ case roles::PASSWORD_TEXT:
+ case roles::BUTTONMENU:
+ return result;
+
+ case roles::DATE_EDITOR:
+ case roles::TIME_EDITOR:
+ result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
+ return result;
+
+ case roles::GROUPING: {
+ // Groupings are sometimes used (like radio groups) to denote
+ // sets of controls. If that's the case, we want to surface
+ // them. We also want to surface grouped time and date controls.
+ for (unsigned int i = 0; i < aAcc->ChildCount(); i++) {
+ Accessible* currChild = aAcc->ChildAt(i);
+ if (currChild->Role() == roles::CHECKBUTTON ||
+ currChild->Role() == roles::SWITCH ||
+ currChild->Role() == roles::SPINBUTTON ||
+ currChild->Role() == roles::RADIOBUTTON) {
+ return result;
+ }
+ }
+
+ // if we iterated through the groups children and didn't
+ // find a control with one of the roles above, we should
+ // ignore this grouping
+ result &= ~nsIAccessibleTraversalRule::FILTER_MATCH;
+ return result;
+ }
+
+ default:
+ // if we did not match on any above role, we should
+ // ignore this accessible.
+ result &= ~nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+ }
+
+ return result;
+}
+
+// Rotor TextEntry Rule
+
+RotorTextEntryRule::RotorTextEntryRule(Accessible* aDirectDescendantsFrom)
+ : RotorRule(aDirectDescendantsFrom){};
+
+RotorTextEntryRule::RotorTextEntryRule() : RotorRule(){};
+
+uint16_t RotorTextEntryRule::Match(Accessible* aAcc) {
+ uint16_t result = RotorRule::Match(aAcc);
+
+ // if a match was found in the base-class's Match function,
+ // it is valid to consider that match again here. if it is
+ // not of the desired role, we flip the match bit to "unmatch"
+ // otherwise, the match persists.
+ if ((result & nsIAccessibleTraversalRule::FILTER_MATCH)) {
+ if (aAcc->Role() != roles::PASSWORD_TEXT && aAcc->Role() != roles::ENTRY) {
+ result &= ~nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+ }
+
+ return result;
+}
+
+// Rotor Link Rule
+
+RotorLinkRule::RotorLinkRule(Accessible* aDirectDescendantsFrom)
+ : RotorRule(aDirectDescendantsFrom){};
+
+RotorLinkRule::RotorLinkRule() : RotorRule(){};
+
+uint16_t RotorLinkRule::Match(Accessible* aAcc) {
+ uint16_t result = RotorRule::Match(aAcc);
+
+ // if a match was found in the base-class's Match function,
+ // it is valid to consider that match again here. if it is
+ // not of the desired role, we flip the match bit to "unmatch"
+ // otherwise, the match persists.
+ if ((result & nsIAccessibleTraversalRule::FILTER_MATCH)) {
+ mozAccessible* nativeMatch = GetNativeFromGeckoAccessible(aAcc);
+ if (![[nativeMatch moxRole] isEqualToString:@"AXLink"]) {
+ result &= ~nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+ }
+
+ return result;
+}
+
+RotorVisitedLinkRule::RotorVisitedLinkRule() : RotorLinkRule() {}
+
+RotorVisitedLinkRule::RotorVisitedLinkRule(Accessible* aDirectDescendantsFrom)
+ : RotorLinkRule(aDirectDescendantsFrom) {}
+
+uint16_t RotorVisitedLinkRule::Match(Accessible* aAcc) {
+ uint16_t result = RotorLinkRule::Match(aAcc);
+
+ if (result & nsIAccessibleTraversalRule::FILTER_MATCH) {
+ mozAccessible* nativeMatch = GetNativeFromGeckoAccessible(aAcc);
+ if (![[nativeMatch moxVisited] boolValue]) {
+ result &= ~nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+ }
+
+ return result;
+}
+
+RotorUnvisitedLinkRule::RotorUnvisitedLinkRule() : RotorLinkRule() {}
+
+RotorUnvisitedLinkRule::RotorUnvisitedLinkRule(
+ Accessible* aDirectDescendantsFrom)
+ : RotorLinkRule(aDirectDescendantsFrom) {}
+
+uint16_t RotorUnvisitedLinkRule::Match(Accessible* aAcc) {
+ uint16_t result = RotorLinkRule::Match(aAcc);
+
+ if (result & nsIAccessibleTraversalRule::FILTER_MATCH) {
+ mozAccessible* nativeMatch = GetNativeFromGeckoAccessible(aAcc);
+ if ([[nativeMatch moxVisited] boolValue]) {
+ result &= ~nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+ }
+
+ return result;
+}
+
+// Match Not Rule
+
+RotorNotMacRoleRule::RotorNotMacRoleRule(NSString* aMacRole,
+ Accessible* aDirectDescendantsFrom)
+ : RotorMacRoleRule(aMacRole, aDirectDescendantsFrom) {}
+
+RotorNotMacRoleRule::RotorNotMacRoleRule(NSString* aMacRole)
+ : RotorMacRoleRule(aMacRole) {}
+
+uint16_t RotorNotMacRoleRule::Match(Accessible* aAcc) {
+ uint16_t result = RotorRule::Match(aAcc);
+
+ // if a match was found in the base-class's Match function,
+ // it is valid to consider that match again here. if it is
+ // not different from the desired role, we flip the
+ // match bit to "unmatch" otherwise, the match persists.
+ if ((result & nsIAccessibleTraversalRule::FILTER_MATCH)) {
+ mozAccessible* nativeMatch = GetNativeFromGeckoAccessible(aAcc);
+ if ([[nativeMatch moxRole] isEqualToString:mMacRole]) {
+ result &= ~nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+ }
+ return result;
+}
+
+// Rotor Static Text Rule
+
+RotorStaticTextRule::RotorStaticTextRule(Accessible* aDirectDescendantsFrom)
+ : RotorRule(aDirectDescendantsFrom){};
+
+RotorStaticTextRule::RotorStaticTextRule() : RotorRule(){};
+
+uint16_t RotorStaticTextRule::Match(Accessible* aAcc) {
+ uint16_t result = RotorRule::Match(aAcc);
+
+ // if a match was found in the base-class's Match function,
+ // it is valid to consider that match again here. if it is
+ // not of the desired role, we flip the match bit to "unmatch"
+ // otherwise, the match persists.
+ if ((result & nsIAccessibleTraversalRule::FILTER_MATCH)) {
+ mozAccessible* nativeMatch = GetNativeFromGeckoAccessible(aAcc);
+ if (![[nativeMatch moxRole] isEqualToString:@"AXStaticText"]) {
+ result &= ~nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+ }
+
+ return result;
+}
+
+// Rotor Heading Level Rule
+
+RotorHeadingLevelRule::RotorHeadingLevelRule(int32_t aLevel,
+ Accessible* aDirectDescendantsFrom)
+ : RotorRoleRule(roles::HEADING, aDirectDescendantsFrom), mLevel(aLevel){};
+
+RotorHeadingLevelRule::RotorHeadingLevelRule(int32_t aLevel)
+ : RotorRoleRule(roles::HEADING), mLevel(aLevel){};
+
+uint16_t RotorHeadingLevelRule::Match(Accessible* aAcc) {
+ uint16_t result = RotorRoleRule::Match(aAcc);
+
+ // if a match was found in the base-class's Match function,
+ // it is valid to consider that match again here. if it is
+ // not of the desired heading level, we flip the match bit to
+ // "unmatch" otherwise, the match persists.
+ if ((result & nsIAccessibleTraversalRule::FILTER_MATCH)) {
+ int32_t currLevel = aAcc->GroupPosition().level;
+
+ if (currLevel != mLevel) {
+ result &= ~nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+ }
+
+ return result;
+}
+
+uint16_t RotorLiveRegionRule::Match(Accessible* aAcc) {
+ uint16_t result = RotorRule::Match(aAcc);
+
+ if ((result & nsIAccessibleTraversalRule::FILTER_MATCH)) {
+ mozAccessible* nativeMatch = GetNativeFromGeckoAccessible(aAcc);
+ if (![nativeMatch moxIsLiveRegion]) {
+ result &= ~nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+ }
+ return result;
+}
+
+// Outline Rule
+
+OutlineRule::OutlineRule() : RotorRule(){};
+
+uint16_t OutlineRule::Match(Accessible* aAcc) {
+ uint16_t result = RotorRule::Match(aAcc);
+
+ // if a match was found in the base-class's Match function,
+ // it is valid to consider that match again here.
+ if (result & nsIAccessibleTraversalRule::FILTER_MATCH) {
+ if (aAcc->Role() == roles::OUTLINE) {
+ // if the match is an outline, we ignore all children here
+ // and unmatch the outline itself
+ result &= ~nsIAccessibleTraversalRule::FILTER_MATCH;
+ result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
+ } else if (aAcc->Role() != roles::OUTLINEITEM) {
+ // if the match is not an outline item, we unmatch here
+ result &= ~nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+ }
+
+ return result;
+}
diff --git a/accessible/mac/SelectorMapGen.py b/accessible/mac/SelectorMapGen.py
new file mode 100755
index 0000000000..1e406ade1f
--- /dev/null
+++ b/accessible/mac/SelectorMapGen.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import re
+
+
+def write_map(fd, name, text):
+ matches = re.findall(r"^//\s(AX\w+)\n-\s?\(.*?\)([\w:]+)", text, re.MULTILINE)
+ entries = [' @"%s" : @"%s"' % (a, s) for [a, s] in matches]
+
+ fd.write("NSDictionary* %s() {\n" % name)
+ fd.write(" // Create an autoreleased NSDictionary object once, and leak it.\n")
+ fd.write(" static NSDictionary* s%s = [@{\n" % name)
+ fd.write(",\n".join(entries))
+ fd.write("\n } retain];\n\n")
+ fd.write(" return s%s;\n" % name)
+ fd.write("}\n\n")
+
+
+def gen_mm(fd, protocol_file):
+ protocol = open(protocol_file).read()
+ fd.write("/* THIS FILE IS AUTOGENERATED - DO NOT EDIT */\n\n")
+ fd.write("#import <Foundation/Foundation.h>\n\n")
+ fd.write("namespace mozilla {\nnamespace a11y {\nnamespace mac {\n\n")
+
+ sections = re.findall(
+ r"#pragma mark - (\w+)\n(.*?)(?=(?:#pragma mark|@end))", protocol, re.DOTALL
+ )
+ for name, text in sections:
+ write_map(fd, name, text)
+
+ fd.write("}\n}\n}\n")
+
+
+def gen_h(fd, protocol_file):
+ protocol = open(protocol_file).read()
+ sections = re.findall(
+ r"#pragma mark - (\w+)\n(.*?)(?=(?:#pragma mark|@end))", protocol, re.DOTALL
+ )
+
+ fd.write("/* THIS FILE IS AUTOGENERATED - DO NOT EDIT */\n\n")
+ fd.write("#ifndef _MacSelectorMap_H_\n")
+ fd.write("#define _MacSelectorMap_H_\n")
+ fd.write("\n@class NSDictionary;\n")
+ fd.write("\nnamespace mozilla {\nnamespace a11y {\nnamespace mac {\n\n")
+ for name, _ in sections:
+ fd.write("NSDictionary* %s();\n\n" % name)
+ fd.write("}\n}\n}\n")
+ fd.write("\n#endif\n")
+
+
+# For debugging
+if __name__ == "__main__":
+ import sys
+
+ gen_mm(sys.stdout, "accessible/mac/MOXAccessibleProtocol.h")
+
+ gen_h(sys.stdout, "accessible/mac/MOXAccessibleProtocol.h")
diff --git a/accessible/mac/moz.build b/accessible/mac/moz.build
new file mode 100644
index 0000000000..1216c9f151
--- /dev/null
+++ b/accessible/mac/moz.build
@@ -0,0 +1,75 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXPORTS += [
+ "mozAccessibleProtocol.h",
+]
+
+EXPORTS.mozilla.a11y += [
+ "AccessibleWrap.h",
+ "HyperTextAccessibleWrap.h",
+ "PlatformExtTypes.h",
+]
+
+UNIFIED_SOURCES += [
+ "AccessibleWrap.mm",
+ "DocAccessibleWrap.mm",
+ "GeckoTextMarker.mm",
+ "HyperTextAccessibleWrap.mm",
+ "MacUtils.mm",
+ "MOXAccessibleBase.mm",
+ "MOXLandmarkAccessibles.mm",
+ "MOXMathAccessibles.mm",
+ "MOXSearchInfo.mm",
+ "MOXTextMarkerDelegate.mm",
+ "MOXWebAreaAccessible.mm",
+ "mozAccessible.mm",
+ "mozActionElements.mm",
+ "mozHTMLAccessible.mm",
+ "mozRootAccessible.mm",
+ "mozSelectableElements.mm",
+ "mozTableAccessible.mm",
+ "mozTextAccessible.mm",
+ "Platform.mm",
+ "RootAccessibleWrap.mm",
+ "RotorRules.mm",
+]
+
+SOURCES += [
+ "!MacSelectorMap.mm",
+]
+
+LOCAL_INCLUDES += [
+ "/accessible/base",
+ "/accessible/generic",
+ "/accessible/html",
+ "/accessible/ipc",
+ "/accessible/ipc/other",
+ "/accessible/xul",
+ "/layout/generic",
+ "/layout/xul",
+ "/widget",
+ "/widget/cocoa",
+]
+
+GeneratedFile(
+ "MacSelectorMap.h",
+ script="/accessible/mac/SelectorMapGen.py",
+ entry_point="gen_h",
+ inputs=["MOXAccessibleProtocol.h"],
+)
+GeneratedFile(
+ "MacSelectorMap.mm",
+ script="/accessible/mac/SelectorMapGen.py",
+ entry_point="gen_mm",
+ inputs=["MOXAccessibleProtocol.h"],
+)
+
+FINAL_LIBRARY = "xul"
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+REQUIRES_UNIFIED_BUILD = True
diff --git a/accessible/mac/mozAccessible.h b/accessible/mac/mozAccessible.h
new file mode 100644
index 0000000000..3288b0cd97
--- /dev/null
+++ b/accessible/mac/mozAccessible.h
@@ -0,0 +1,276 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AccessibleWrap.h"
+#include "RemoteAccessible.h"
+
+#import <Cocoa/Cocoa.h>
+
+#import "MOXAccessibleBase.h"
+
+@class mozRootAccessible;
+
+/**
+ * All mozAccessibles are either abstract objects (that correspond to XUL
+ * widgets, HTML frames, etc) or are attached to a certain view; for example
+ * a document view. When we hand an object off to an AT, we always want
+ * to give it the represented view, in the latter case.
+ */
+
+namespace mozilla {
+namespace a11y {
+
+inline mozAccessible* GetNativeFromGeckoAccessible(
+ mozilla::a11y::Accessible* aAcc) {
+ if (!aAcc) {
+ return nil;
+ }
+ if (LocalAccessible* acc = aAcc->AsLocal()) {
+ mozAccessible* native = nil;
+ acc->GetNativeInterface((void**)&native);
+ return native;
+ }
+
+ RemoteAccessible* proxy = aAcc->AsRemote();
+ return reinterpret_cast<mozAccessible*>(proxy->GetWrapper());
+}
+
+} // a11y
+} // mozilla
+
+@interface mozAccessible : MOXAccessibleBase {
+ /**
+ * Reference to the accessible we were created with;
+ * either a proxy accessible or an accessible wrap.
+ */
+ mozilla::a11y::Accessible* mGeckoAccessible;
+
+ /**
+ * The role of our gecko accessible.
+ */
+ mozilla::a11y::role mRole;
+
+ /**
+ * A cache of a subset of our states.
+ */
+ uint64_t mCachedState;
+
+ nsStaticAtom* mARIARole;
+
+ bool mIsLiveRegion;
+}
+
+// inits with the given wrap or proxy accessible
+- (id)initWithAccessible:(mozilla::a11y::Accessible*)aAcc;
+
+// allows for gecko accessible access outside of the class
+- (mozilla::a11y::Accessible*)geckoAccessible;
+
+- (mozilla::a11y::Accessible*)geckoDocument;
+
+// override
+- (void)dealloc;
+
+// should a child be disabled
+- (BOOL)disableChild:(mozAccessible*)child;
+
+// Given a gecko accessibility event type, post the relevant
+// system accessibility notification.
+// Note: when overriding or adding new events, make sure your events aren't
+// filtered out in Platform::ProxyEvent or AccessibleWrap::HandleAccEvent!
+- (void)handleAccessibleEvent:(uint32_t)eventType;
+
+- (void)handleAccessibleTextChangeEvent:(NSString*)change
+ inserted:(BOOL)isInserted
+ inContainer:(mozilla::a11y::Accessible*)container
+ at:(int32_t)start;
+
+// internal method to retrieve a child at a given index.
+- (id)childAt:(uint32_t)i;
+
+// Get gecko accessible's state.
+- (uint64_t)state;
+
+// Get gecko accessible's state filtered through given mask.
+- (uint64_t)stateWithMask:(uint64_t)mask;
+
+// Notify of a state change, so the cache can be altered.
+- (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled;
+
+// Invalidate cached state.
+- (void)invalidateState;
+
+// Get top level (tab) web area.
+- (mozAccessible*)topWebArea;
+
+// Handle a role change
+- (void)handleRoleChanged:(mozilla::a11y::role)newRole;
+
+// Get ARIA role
+- (nsStaticAtom*)ARIARole;
+
+// Get array of related mozAccessibles
+- (NSArray<mozAccessible*>*)getRelationsByType:
+ (mozilla::a11y::RelationType)relationType;
+
+#pragma mark - mozAccessible protocol / widget
+
+// override
+- (BOOL)hasRepresentedView;
+
+// override
+- (id)representedView;
+
+// override
+- (BOOL)isRoot;
+
+#pragma mark - MOXAccessible protocol
+
+// override
+- (BOOL)moxBlockSelector:(SEL)selector;
+
+// override
+- (id)moxHitTest:(NSPoint)point;
+
+// override
+- (id)moxFocusedUIElement;
+
+- (id<MOXTextMarkerSupport>)moxTextMarkerDelegate;
+
+- (BOOL)moxIsLiveRegion;
+
+// Attribute getters
+
+// override
+- (id<mozAccessible>)moxParent;
+
+// override
+- (NSArray*)moxChildren;
+
+// override
+- (NSValue*)moxSize;
+
+// override
+- (NSValue*)moxPosition;
+
+// override
+- (NSString*)moxRole;
+
+// override
+- (NSString*)moxSubrole;
+
+// override
+- (NSString*)moxRoleDescription;
+
+// override
+- (NSWindow*)moxWindow;
+
+// override
+- (id)moxValue;
+
+// override
+- (NSString*)moxTitle;
+
+// override
+- (NSString*)moxLabel;
+
+// override
+- (NSString*)moxHelp;
+
+// override
+- (NSNumber*)moxEnabled;
+
+// override
+- (NSNumber*)moxFocused;
+
+// override
+- (NSNumber*)moxSelected;
+
+// override
+- (NSNumber*)moxExpanded;
+
+// override
+- (NSValue*)moxFrame;
+
+// override
+- (NSString*)moxARIACurrent;
+
+// override
+- (NSNumber*)moxARIAAtomic;
+
+// override
+- (NSString*)moxARIALive;
+
+// override
+- (NSString*)moxARIARelevant;
+
+// override
+- (id)moxTitleUIElement;
+
+// override
+- (NSString*)moxDOMIdentifier;
+
+// override
+- (NSNumber*)moxRequired;
+
+// override
+- (NSNumber*)moxElementBusy;
+
+// override
+- (NSArray*)moxLinkedUIElements;
+
+// override
+- (NSArray*)moxARIAControls;
+
+// override
+- (id)moxEditableAncestor;
+
+// override
+- (id)moxHighestEditableAncestor;
+
+// override
+- (id)moxFocusableAncestor;
+
+#ifndef RELEASE_OR_BETA
+// override
+- (NSString*)moxMozDebugDescription;
+#endif
+
+// override
+- (NSArray*)moxUIElementsForSearchPredicate:(NSDictionary*)searchPredicate;
+
+// override
+- (NSNumber*)moxUIElementCountForSearchPredicate:(NSDictionary*)searchPredicate;
+
+// override
+- (void)moxSetFocused:(NSNumber*)focused;
+
+// override
+- (void)moxPerformScrollToVisible;
+
+// override
+- (void)moxPerformShowMenu;
+
+// override
+- (void)moxPerformPress;
+
+// override
+- (BOOL)moxIgnoreWithParent:(mozAccessible*)parent;
+
+// override
+- (BOOL)moxIgnoreChild:(mozAccessible*)child;
+
+#pragma mark -
+
+// makes ourselves "expired". after this point, we might be around if someone
+// has retained us (e.g., a third-party), but we really contain no information.
+// override
+- (void)expire;
+// override
+- (BOOL)isExpired;
+
+@end
diff --git a/accessible/mac/mozAccessible.mm b/accessible/mac/mozAccessible.mm
new file mode 100644
index 0000000000..f8def7a384
--- /dev/null
+++ b/accessible/mac/mozAccessible.mm
@@ -0,0 +1,1062 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "mozAccessible.h"
+#include "MOXAccessibleBase.h"
+
+#import "MacUtils.h"
+#import "mozView.h"
+#import "MOXSearchInfo.h"
+#import "mozTextAccessible.h"
+
+#include "LocalAccessible-inl.h"
+#include "nsAccUtils.h"
+#include "DocAccessibleParent.h"
+#include "Relation.h"
+#include "Role.h"
+#include "RootAccessible.h"
+#include "TableAccessible.h"
+#include "mozilla/a11y/PDocAccessible.h"
+#include "mozilla/dom/BrowserParent.h"
+#include "OuterDocAccessible.h"
+#include "nsChildView.h"
+
+#include "nsRect.h"
+#include "nsCocoaUtils.h"
+#include "nsCoord.h"
+#include "nsObjCExceptions.h"
+#include "nsWhitespaceTokenizer.h"
+#include <prdtoa.h>
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+#pragma mark -
+
+@interface mozAccessible ()
+- (BOOL)providesLabelNotTitle;
+
+- (void)maybePostLiveRegionChanged;
+- (void)maybePostA11yUtilNotification;
+@end
+
+@implementation mozAccessible
+
+- (id)initWithAccessible:(Accessible*)aAcc {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+ MOZ_ASSERT(aAcc, "Cannot init mozAccessible with null");
+ if ((self = [super init])) {
+ mGeckoAccessible = aAcc;
+ mRole = aAcc->Role();
+ }
+
+ return self;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (void)dealloc {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [super dealloc];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+#pragma mark - mozAccessible widget
+
+- (BOOL)hasRepresentedView {
+ return NO;
+}
+
+- (id)representedView {
+ return nil;
+}
+
+- (BOOL)isRoot {
+ return NO;
+}
+
+#pragma mark -
+
+- (BOOL)moxIgnoreWithParent:(mozAccessible*)parent {
+ if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) {
+ if (acc->IsContent() && acc->GetContent()->IsXULElement()) {
+ if (acc->VisibilityState() & states::INVISIBLE) {
+ return YES;
+ }
+ }
+ }
+
+ return [parent moxIgnoreChild:self];
+}
+
+- (BOOL)moxIgnoreChild:(mozAccessible*)child {
+ return nsAccUtils::MustPrune(mGeckoAccessible);
+}
+
+- (id)childAt:(uint32_t)i {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ Accessible* child = mGeckoAccessible->ChildAt(i);
+ return child ? GetNativeFromGeckoAccessible(child) : nil;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+static const uint64_t kCachedStates =
+ states::CHECKED | states::PRESSED | states::MIXED | states::EXPANDED |
+ states::EXPANDABLE | states::CURRENT | states::SELECTED |
+ states::TRAVERSED | states::LINKED | states::HASPOPUP | states::BUSY |
+ states::MULTI_LINE | states::CHECKABLE;
+static const uint64_t kCacheInitialized = ((uint64_t)0x1) << 63;
+
+- (uint64_t)state {
+ uint64_t state = 0;
+
+ if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) {
+ state = acc->State();
+ }
+
+ if (RemoteAccessible* proxy = mGeckoAccessible->AsRemote()) {
+ state = proxy->State();
+ }
+
+ if (!(mCachedState & kCacheInitialized)) {
+ mCachedState = state & kCachedStates;
+ mCachedState |= kCacheInitialized;
+ }
+
+ return state;
+}
+
+- (uint64_t)stateWithMask:(uint64_t)mask {
+ if ((mask & kCachedStates) == mask &&
+ (mCachedState & kCacheInitialized) != 0) {
+ return mCachedState & mask;
+ }
+
+ return [self state] & mask;
+}
+
+- (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled {
+ if ((state & kCachedStates) != 0) {
+ if (!(mCachedState & kCacheInitialized)) {
+ [self state];
+ } else {
+ if (enabled) {
+ mCachedState |= state;
+ } else {
+ mCachedState &= ~state;
+ }
+ }
+ }
+
+ if (state == states::BUSY) {
+ [self moxPostNotification:@"AXElementBusyChanged"];
+ }
+}
+
+- (void)invalidateState {
+ mCachedState = 0;
+}
+
+- (BOOL)providesLabelNotTitle {
+ // These accessible types are the exception to the rule of label vs. title:
+ // They may be named explicitly, but they still provide a label not a title.
+ return mRole == roles::GROUPING || mRole == roles::RADIO_GROUP ||
+ mRole == roles::FIGURE || mRole == roles::GRAPHIC ||
+ mRole == roles::DOCUMENT || mRole == roles::OUTLINE ||
+ mRole == roles::ARTICLE || mRole == roles::ENTRY ||
+ mRole == roles::SPINBUTTON;
+}
+
+- (mozilla::a11y::Accessible*)geckoAccessible {
+ return mGeckoAccessible;
+}
+
+- (mozilla::a11y::Accessible*)geckoDocument {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ if (mGeckoAccessible->IsLocal()) {
+ if (mGeckoAccessible->AsLocal()->IsDoc()) {
+ return mGeckoAccessible;
+ }
+ return mGeckoAccessible->AsLocal()->Document();
+ }
+
+ if (mGeckoAccessible->AsRemote()->IsDoc()) {
+ return mGeckoAccessible;
+ }
+
+ return mGeckoAccessible->AsRemote()->Document();
+}
+
+#pragma mark - MOXAccessible protocol
+
+- (BOOL)moxBlockSelector:(SEL)selector {
+ if (selector == @selector(moxPerformPress)) {
+ uint8_t actionCount = mGeckoAccessible->IsLocal()
+ ? mGeckoAccessible->AsLocal()->ActionCount()
+ : mGeckoAccessible->AsRemote()->ActionCount();
+
+ // If we have no action, we don't support press, so return YES.
+ return actionCount == 0;
+ }
+
+ if (selector == @selector(moxSetFocused:)) {
+ return [self stateWithMask:states::FOCUSABLE] == 0;
+ }
+
+ if (selector == @selector(moxARIALive) ||
+ selector == @selector(moxARIAAtomic) ||
+ selector == @selector(moxARIARelevant)) {
+ return ![self moxIsLiveRegion];
+ }
+
+ if (selector == @selector(moxExpanded)) {
+ return [self stateWithMask:states::EXPANDABLE] == 0;
+ }
+
+ return [super moxBlockSelector:selector];
+}
+
+- (id)moxFocusedUIElement {
+ MOZ_ASSERT(mGeckoAccessible);
+ // This only gets queried on the web area or the root group
+ // so just use the doc's focused child instead of trying to get
+ // the focused child of mGeckoAccessible.
+ Accessible* doc = nsAccUtils::DocumentFor(mGeckoAccessible);
+ mozAccessible* focusedChild =
+ GetNativeFromGeckoAccessible(doc->FocusedChild());
+
+ if ([focusedChild isAccessibilityElement]) {
+ return focusedChild;
+ }
+
+ // return ourself if we can't get a native focused child.
+ return self;
+}
+
+- (id<MOXTextMarkerSupport>)moxTextMarkerDelegate {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ if (mGeckoAccessible->IsLocal()) {
+ return [MOXTextMarkerDelegate
+ getOrCreateForDoc:mGeckoAccessible->AsLocal()->Document()];
+ }
+
+ return [MOXTextMarkerDelegate
+ getOrCreateForDoc:mGeckoAccessible->AsRemote()->Document()];
+}
+
+- (BOOL)moxIsLiveRegion {
+ return mIsLiveRegion;
+}
+
+- (id)moxHitTest:(NSPoint)point {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ // Convert the given screen-global point in the cocoa coordinate system (with
+ // origin in the bottom-left corner of the screen) into point in the Gecko
+ // coordinate system (with origin in a top-left screen point).
+ NSScreen* mainView = [[NSScreen screens] objectAtIndex:0];
+ NSPoint tmpPoint =
+ NSMakePoint(point.x, [mainView frame].size.height - point.y);
+ LayoutDeviceIntPoint geckoPoint = nsCocoaUtils::CocoaPointsToDevPixels(
+ tmpPoint, nsCocoaUtils::GetBackingScaleFactor(mainView));
+
+ Accessible* child = mGeckoAccessible->ChildAtPoint(
+ geckoPoint.x, geckoPoint.y, Accessible::EWhichChildAtPoint::DeepestChild);
+
+ if (child) {
+ mozAccessible* nativeChild = GetNativeFromGeckoAccessible(child);
+ return [nativeChild isAccessibilityElement]
+ ? nativeChild
+ : [nativeChild moxUnignoredParent];
+ }
+
+ // if we didn't find anything, return ourself or child view.
+ return self;
+}
+
+- (id<mozAccessible>)moxParent {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+ if ([self isExpired]) {
+ return nil;
+ }
+
+ Accessible* parent = mGeckoAccessible->Parent();
+
+ if (!parent) {
+ return nil;
+ }
+
+ id nativeParent = GetNativeFromGeckoAccessible(parent);
+ if ([nativeParent respondsToSelector:@selector(rootGroup)]) {
+ // Before returning a WebArea as parent, check to see if
+ // there is a generated root group that is an intermediate container.
+ if (id<mozAccessible> rootGroup = [nativeParent rootGroup]) {
+ nativeParent = rootGroup;
+ }
+ }
+
+ if (!nativeParent && mGeckoAccessible->IsLocal()) {
+ // Return native of root accessible if we have no direct parent.
+ // XXX: need to return a sensible fallback in proxy case as well
+ nativeParent = GetNativeFromGeckoAccessible(
+ mGeckoAccessible->AsLocal()->RootAccessible());
+ }
+
+ return GetObjectOrRepresentedView(nativeParent);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+// gets all our native children lazily, including those that are ignored.
+- (NSArray*)moxChildren {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ NSMutableArray* children = [[[NSMutableArray alloc]
+ initWithCapacity:mGeckoAccessible->ChildCount()] autorelease];
+
+ for (uint32_t childIdx = 0; childIdx < mGeckoAccessible->ChildCount();
+ childIdx++) {
+ Accessible* child = mGeckoAccessible->ChildAt(childIdx);
+ mozAccessible* nativeChild = GetNativeFromGeckoAccessible(child);
+ if (!nativeChild) {
+ continue;
+ }
+
+ [children addObject:nativeChild];
+ }
+
+ return children;
+}
+
+- (NSValue*)moxPosition {
+ CGRect frame = [[self moxFrame] rectValue];
+
+ return [NSValue valueWithPoint:NSMakePoint(frame.origin.x, frame.origin.y)];
+}
+
+- (NSValue*)moxSize {
+ CGRect frame = [[self moxFrame] rectValue];
+
+ return
+ [NSValue valueWithSize:NSMakeSize(frame.size.width, frame.size.height)];
+}
+
+- (NSString*)moxRole {
+#define ROLE(geckoRole, stringRole, atkRole, macRole, macSubrole, msaaRole, \
+ ia2Role, androidClass, nameRule) \
+ case roles::geckoRole: \
+ return macRole;
+
+ switch (mRole) {
+#include "RoleMap.h"
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unknown role.");
+ return NSAccessibilityUnknownRole;
+ }
+
+#undef ROLE
+}
+
+- (nsStaticAtom*)ARIARole {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ if (mGeckoAccessible->HasARIARole()) {
+ const nsRoleMapEntry* roleMap = mGeckoAccessible->ARIARoleMap();
+ return roleMap->roleAtom;
+ }
+
+ return nsGkAtoms::_empty;
+}
+
+- (NSString*)moxSubrole {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ LocalAccessible* acc = mGeckoAccessible->AsLocal();
+ RemoteAccessible* proxy = mGeckoAccessible->AsRemote();
+
+ // Deal with landmarks first
+ // macOS groups the specific landmark types of DPub ARIA into two broad
+ // categories with corresponding subroles: Navigation and region/container.
+ if (mRole == roles::LANDMARK) {
+ nsAtom* landmark = acc ? acc->LandmarkRole() : proxy->LandmarkRole();
+ // HTML Elements treated as landmarks, and ARIA landmarks.
+ if (landmark) {
+ if (landmark == nsGkAtoms::banner) return @"AXLandmarkBanner";
+ if (landmark == nsGkAtoms::complementary)
+ return @"AXLandmarkComplementary";
+ if (landmark == nsGkAtoms::contentinfo) return @"AXLandmarkContentInfo";
+ if (landmark == nsGkAtoms::main) return @"AXLandmarkMain";
+ if (landmark == nsGkAtoms::navigation) return @"AXLandmarkNavigation";
+ if (landmark == nsGkAtoms::search) return @"AXLandmarkSearch";
+ }
+
+ // None of the above, so assume DPub ARIA.
+ return @"AXLandmarkRegion";
+ }
+
+ // Now, deal with widget roles
+ nsStaticAtom* roleAtom = nullptr;
+
+ if (mRole == roles::DIALOG) {
+ roleAtom = [self ARIARole];
+
+ if (roleAtom == nsGkAtoms::alertdialog) {
+ return @"AXApplicationAlertDialog";
+ }
+ if (roleAtom == nsGkAtoms::dialog) {
+ return @"AXApplicationDialog";
+ }
+ }
+
+ if (mRole == roles::FORM) {
+ roleAtom = [self ARIARole];
+
+ if (roleAtom == nsGkAtoms::form) {
+ return @"AXLandmarkForm";
+ }
+ }
+
+#define ROLE(geckoRole, stringRole, atkRole, macRole, macSubrole, msaaRole, \
+ ia2Role, androidClass, nameRule) \
+ case roles::geckoRole: \
+ if (![macSubrole isEqualToString:NSAccessibilityUnknownSubrole]) { \
+ return macSubrole; \
+ } else { \
+ break; \
+ }
+
+ switch (mRole) {
+#include "RoleMap.h"
+ }
+
+ // These are special. They map to roles::NOTHING
+ // and are instructed by the ARIA map to use the native host role.
+ roleAtom = [self ARIARole];
+
+ if (roleAtom == nsGkAtoms::log_) {
+ return @"AXApplicationLog";
+ }
+
+ if (roleAtom == nsGkAtoms::timer) {
+ return @"AXApplicationTimer";
+ }
+ // macOS added an AXSubrole value to distinguish generic AXGroup objects
+ // from those which are AXGroups as a result of an explicit ARIA role,
+ // such as the non-landmark, non-listitem text containers in DPub ARIA.
+ if (mRole == roles::FOOTNOTE || mRole == roles::SECTION) {
+ return @"AXApplicationGroup";
+ }
+
+ return NSAccessibilityUnknownSubrole;
+
+#undef ROLE
+}
+
+struct RoleDescrMap {
+ NSString* role;
+ const nsString description;
+};
+
+static const RoleDescrMap sRoleDescrMap[] = {
+ {@"AXApplicationAlert", u"alert"_ns},
+ {@"AXApplicationAlertDialog", u"alertDialog"_ns},
+ {@"AXApplicationDialog", u"dialog"_ns},
+ {@"AXApplicationLog", u"log"_ns},
+ {@"AXApplicationMarquee", u"marquee"_ns},
+ {@"AXApplicationStatus", u"status"_ns},
+ {@"AXApplicationTimer", u"timer"_ns},
+ {@"AXContentSeparator", u"separator"_ns},
+ {@"AXDefinition", u"definition"_ns},
+ {@"AXDetails", u"details"_ns},
+ {@"AXDocument", u"document"_ns},
+ {@"AXDocumentArticle", u"article"_ns},
+ {@"AXDocumentMath", u"math"_ns},
+ {@"AXDocumentNote", u"note"_ns},
+ {@"AXLandmarkApplication", u"application"_ns},
+ {@"AXLandmarkBanner", u"banner"_ns},
+ {@"AXLandmarkComplementary", u"complementary"_ns},
+ {@"AXLandmarkContentInfo", u"content"_ns},
+ {@"AXLandmarkMain", u"main"_ns},
+ {@"AXLandmarkNavigation", u"navigation"_ns},
+ {@"AXLandmarkRegion", u"region"_ns},
+ {@"AXLandmarkSearch", u"search"_ns},
+ {@"AXSearchField", u"searchTextField"_ns},
+ {@"AXSummary", u"summary"_ns},
+ {@"AXTabPanel", u"tabPanel"_ns},
+ {@"AXTerm", u"term"_ns},
+ {@"AXUserInterfaceTooltip", u"tooltip"_ns}};
+
+struct RoleDescrComparator {
+ const NSString* mRole;
+ explicit RoleDescrComparator(const NSString* aRole) : mRole(aRole) {}
+ int operator()(const RoleDescrMap& aEntry) const {
+ return [mRole compare:aEntry.role];
+ }
+};
+
+- (NSString*)moxRoleDescription {
+ if (NSString* ariaRoleDescription =
+ utils::GetAccAttr(self, nsGkAtoms::aria_roledescription)) {
+ if ([ariaRoleDescription length]) {
+ return ariaRoleDescription;
+ }
+ }
+
+ if (mRole == roles::FIGURE) return utils::LocalizedString(u"figure"_ns);
+
+ if (mRole == roles::HEADING) return utils::LocalizedString(u"heading"_ns);
+
+ if (mRole == roles::MARK) {
+ return utils::LocalizedString(u"highlight"_ns);
+ }
+
+ NSString* subrole = [self moxSubrole];
+
+ if (subrole) {
+ size_t idx = 0;
+ if (BinarySearchIf(sRoleDescrMap, 0, ArrayLength(sRoleDescrMap),
+ RoleDescrComparator(subrole), &idx)) {
+ return utils::LocalizedString(sRoleDescrMap[idx].description);
+ }
+ }
+
+ return NSAccessibilityRoleDescription([self moxRole], subrole);
+}
+
+- (NSString*)moxLabel {
+ if ([self isExpired]) {
+ return nil;
+ }
+
+ nsAutoString name;
+
+ /* If our accessible is:
+ * 1. Named by invisible text, or
+ * 2. Has more than one labeling relation, or
+ * 3. Is a special role defined in providesLabelNotTitle
+ * ... return its name as a label (AXDescription).
+ */
+ ENameValueFlag flag = mGeckoAccessible->Name(name);
+ if (flag == eNameFromSubtree) {
+ return nil;
+ }
+
+ if (![self providesLabelNotTitle]) {
+ NSArray* relations = [self getRelationsByType:RelationType::LABELLED_BY];
+ if ([relations count] == 1) {
+ return nil;
+ }
+ }
+
+ return nsCocoaUtils::ToNSString(name);
+}
+
+- (NSString*)moxTitle {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // In some special cases we provide the name in the label (AXDescription).
+ if ([self providesLabelNotTitle]) {
+ return nil;
+ }
+
+ nsAutoString title;
+ mGeckoAccessible->Name(title);
+ if (nsCoreUtils::IsWhitespaceString(title)) {
+ return @"";
+ }
+
+ return nsCocoaUtils::ToNSString(title);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (id)moxValue {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsAutoString value;
+ mGeckoAccessible->Value(value);
+
+ return nsCocoaUtils::ToNSString(value);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (NSString*)moxHelp {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // What needs to go here is actually the accDescription of an item.
+ // The MSAA acc_help method has nothing to do with this one.
+ nsAutoString helpText;
+ mGeckoAccessible->Description(helpText);
+
+ return nsCocoaUtils::ToNSString(helpText);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (NSWindow*)moxWindow {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // Get a pointer to the native window (NSWindow) we reside in.
+ NSWindow* nativeWindow = nil;
+ DocAccessible* docAcc = nullptr;
+ if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) {
+ docAcc = acc->Document();
+ } else {
+ RemoteAccessible* proxy = mGeckoAccessible->AsRemote();
+ LocalAccessible* outerDoc = proxy->OuterDocOfRemoteBrowser();
+ if (outerDoc) docAcc = outerDoc->Document();
+ }
+
+ if (docAcc) nativeWindow = static_cast<NSWindow*>(docAcc->GetNativeWindow());
+
+ MOZ_ASSERT(nativeWindow || gfxPlatform::IsHeadless(),
+ "Couldn't get native window");
+ return nativeWindow;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (NSNumber*)moxEnabled {
+ if ([self stateWithMask:states::UNAVAILABLE]) {
+ return @NO;
+ }
+
+ if (![self isRoot]) {
+ mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent];
+ if (![parent isRoot]) {
+ return @(![parent disableChild:self]);
+ }
+ }
+
+ return @YES;
+}
+
+- (NSNumber*)moxFocused {
+ return @([self stateWithMask:states::FOCUSED] != 0);
+}
+
+- (NSNumber*)moxSelected {
+ return @NO;
+}
+
+- (NSNumber*)moxExpanded {
+ return @([self stateWithMask:states::EXPANDED] != 0);
+}
+
+- (NSValue*)moxFrame {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ LayoutDeviceIntRect rect = mGeckoAccessible->IsLocal()
+ ? mGeckoAccessible->AsLocal()->Bounds()
+ : mGeckoAccessible->AsRemote()->Bounds();
+ NSScreen* mainView = [[NSScreen screens] objectAtIndex:0];
+ CGFloat scaleFactor = nsCocoaUtils::GetBackingScaleFactor(mainView);
+
+ return [NSValue
+ valueWithRect:NSMakeRect(
+ static_cast<CGFloat>(rect.x) / scaleFactor,
+ [mainView frame].size.height -
+ static_cast<CGFloat>(rect.y + rect.height) /
+ scaleFactor,
+ static_cast<CGFloat>(rect.width) / scaleFactor,
+ static_cast<CGFloat>(rect.height) / scaleFactor)];
+}
+
+- (NSString*)moxARIACurrent {
+ if (![self stateWithMask:states::CURRENT]) {
+ return nil;
+ }
+
+ return utils::GetAccAttr(self, nsGkAtoms::aria_current);
+}
+
+- (NSNumber*)moxARIAAtomic {
+ return @(utils::GetAccAttr(self, nsGkAtoms::aria_atomic) != nil);
+}
+
+- (NSString*)moxARIALive {
+ return utils::GetAccAttr(self, nsGkAtoms::aria_live);
+}
+
+- (NSString*)moxARIARelevant {
+ if (NSString* relevant =
+ utils::GetAccAttr(self, nsGkAtoms::containerRelevant)) {
+ return relevant;
+ }
+
+ // Default aria-relevant value
+ return @"additions text";
+}
+
+- (id)moxTitleUIElement {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ NSArray* relations = [self getRelationsByType:RelationType::LABELLED_BY];
+ if ([relations count] == 1) {
+ return [relations firstObject];
+ }
+
+ return nil;
+}
+
+- (NSString*)moxDOMIdentifier {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ nsAutoString id;
+ mGeckoAccessible->DOMNodeID(id);
+
+ return nsCocoaUtils::ToNSString(id);
+}
+
+- (NSNumber*)moxRequired {
+ return @([self stateWithMask:states::REQUIRED] != 0);
+}
+
+- (NSNumber*)moxElementBusy {
+ return @([self stateWithMask:states::BUSY] != 0);
+}
+
+- (NSArray*)moxLinkedUIElements {
+ return [self getRelationsByType:RelationType::FLOWS_TO];
+}
+
+- (NSArray*)moxARIAControls {
+ return [self getRelationsByType:RelationType::CONTROLLER_FOR];
+}
+
+- (mozAccessible*)topWebArea {
+ Accessible* doc = [self geckoDocument];
+ while (doc) {
+ if (doc->IsLocal()) {
+ DocAccessible* docAcc = doc->AsLocal()->AsDoc();
+ if (docAcc->DocumentNode()->GetBrowsingContext()->IsTopContent()) {
+ return GetNativeFromGeckoAccessible(docAcc);
+ }
+
+ doc = docAcc->ParentDocument();
+ } else {
+ DocAccessibleParent* docProxy = doc->AsRemote()->AsDoc();
+ if (docProxy->IsTopLevel()) {
+ return GetNativeFromGeckoAccessible(docProxy);
+ }
+ doc = docProxy->ParentDoc();
+ }
+ }
+
+ return nil;
+}
+
+- (void)handleRoleChanged:(mozilla::a11y::role)newRole {
+ mRole = newRole;
+ mARIARole = nullptr;
+
+ // For testing purposes
+ [self moxPostNotification:@"AXMozRoleChanged"];
+}
+
+- (id)moxEditableAncestor {
+ return [self moxFindAncestor:^BOOL(id moxAcc, BOOL* stop) {
+ return [moxAcc isKindOfClass:[mozTextAccessible class]];
+ }];
+}
+
+- (id)moxHighestEditableAncestor {
+ id highestAncestor = [self moxEditableAncestor];
+ while ([highestAncestor conformsToProtocol:@protocol(MOXAccessible)]) {
+ id ancestorParent = [highestAncestor moxUnignoredParent];
+ if (![ancestorParent conformsToProtocol:@protocol(MOXAccessible)]) {
+ break;
+ }
+
+ id higherAncestor = [ancestorParent moxEditableAncestor];
+
+ if (!higherAncestor) {
+ break;
+ }
+
+ highestAncestor = higherAncestor;
+ }
+
+ return highestAncestor;
+}
+
+- (id)moxFocusableAncestor {
+ // XXX: Checking focusable state up the chain can be expensive. For now,
+ // we can just return AXEditableAncestor since the main use case for this
+ // is rich text editing with links.
+ return [self moxEditableAncestor];
+}
+
+#ifndef RELEASE_OR_BETA
+- (NSString*)moxMozDebugDescription {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSMutableString* domInfo = [NSMutableString string];
+ if (NSString* tagName = utils::GetAccAttr(self, nsGkAtoms::tag)) {
+ [domInfo appendFormat:@" %@", tagName];
+ NSString* domID = [self moxDOMIdentifier];
+ if ([domID length]) {
+ [domInfo appendFormat:@"#%@", domID];
+ }
+ if (NSString* className = utils::GetAccAttr(self, nsGkAtoms::_class)) {
+ [domInfo
+ appendFormat:@".%@",
+ [className stringByReplacingOccurrencesOfString:@" "
+ withString:@"."]];
+ }
+ }
+
+ return [NSString stringWithFormat:@"<%@: %p %@%@>",
+ NSStringFromClass([self class]), self,
+ [self moxRole], domInfo];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+#endif
+
+- (NSArray*)moxUIElementsForSearchPredicate:(NSDictionary*)searchPredicate {
+ // Create our search object and set it up with the searchPredicate
+ // params. The init function does additional parsing. We pass a
+ // reference to the web area to use as a start element if one is not
+ // specified.
+ MOXSearchInfo* search =
+ [[[MOXSearchInfo alloc] initWithParameters:searchPredicate
+ andRoot:self] autorelease];
+
+ return [search performSearch];
+}
+
+- (NSNumber*)moxUIElementCountForSearchPredicate:
+ (NSDictionary*)searchPredicate {
+ return [NSNumber
+ numberWithDouble:[[self moxUIElementsForSearchPredicate:searchPredicate]
+ count]];
+}
+
+- (void)moxSetFocused:(NSNumber*)focused {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ if ([focused boolValue]) {
+ mGeckoAccessible->TakeFocus();
+ }
+}
+
+- (void)moxPerformScrollToVisible {
+ MOZ_ASSERT(mGeckoAccessible);
+ mGeckoAccessible->ScrollTo(nsIAccessibleScrollType::SCROLL_TYPE_ANYWHERE);
+}
+
+- (void)moxPerformShowMenu {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ // We don't need to convert this rect into mac coordinates because the
+ // mouse event synthesizer expects layout (gecko) coordinates.
+ LayoutDeviceIntRect bounds = mGeckoAccessible->IsLocal()
+ ? mGeckoAccessible->AsLocal()->Bounds()
+ : mGeckoAccessible->AsRemote()->Bounds();
+
+ LocalAccessible* rootAcc = mGeckoAccessible->IsLocal()
+ ? mGeckoAccessible->AsLocal()->RootAccessible()
+ : mGeckoAccessible->AsRemote()
+ ->OuterDocOfRemoteBrowser()
+ ->RootAccessible();
+ id objOrView =
+ GetObjectOrRepresentedView(GetNativeFromGeckoAccessible(rootAcc));
+
+ LayoutDeviceIntPoint p = LayoutDeviceIntPoint(
+ bounds.X() + (bounds.Width() / 2), bounds.Y() + (bounds.Height() / 2));
+ nsIWidget* widget = [objOrView widget];
+ widget->SynthesizeNativeMouseEvent(
+ p, nsIWidget::NativeMouseMessage::ButtonDown, MouseButton::eSecondary,
+ nsIWidget::Modifiers::NO_MODIFIERS, nullptr);
+}
+
+- (void)moxPerformPress {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ if (mGeckoAccessible->IsLocal()) {
+ mGeckoAccessible->AsLocal()->DoAction(0);
+ } else {
+ mGeckoAccessible->AsRemote()->DoAction(0);
+ }
+
+ // Activating accessible may alter its state.
+ [self invalidateState];
+}
+
+#pragma mark -
+
+- (BOOL)disableChild:(mozAccessible*)child {
+ return NO;
+}
+
+- (void)maybePostLiveRegionChanged {
+ id<MOXAccessible> liveRegion =
+ [self moxFindAncestor:^BOOL(id<MOXAccessible> moxAcc, BOOL* stop) {
+ return [moxAcc moxIsLiveRegion];
+ }];
+
+ if (liveRegion) {
+ [liveRegion moxPostNotification:@"AXLiveRegionChanged"];
+ }
+}
+
+- (void)maybePostA11yUtilNotification {
+ MOZ_ASSERT(mGeckoAccessible);
+ // Sometimes we use a special live region to make announcements to the user.
+ // This region is a child of the root document, but doesn't contain any
+ // content. If we try to fire regular AXLiveRegion changed events through it,
+ // VoiceOver clips the notifications because it (rightfully) doesn't detect
+ // focus within the region. We get around this by firing an
+ // AXAnnouncementRequested notification here instead.
+ // Verify we're trying to send a notification for the a11yUtils alert (and not
+ // a random acc with the same ID) by checking:
+ // - The gecko acc is local, our a11y-announcement lives in browser.xhtml
+ // - The ID of the gecko acc is "a11y-announcement"
+ // - The native acc is a direct descendent of the root
+ if (mGeckoAccessible->IsLocal() &&
+ [[self moxDOMIdentifier] isEqualToString:@"a11y-announcement"] &&
+ [[self moxParent] isKindOfClass:[mozRootAccessible class]]) {
+ // Our actual announcement should be stored as a child of the alert,
+ // so we verify a child exists, and then query that child below.
+ NSArray* children = [self moxChildren];
+ MOZ_ASSERT([children count] == 1 && children[0],
+ "A11yUtil event recieved, but no announcement found?");
+
+ mozAccessible* announcement = children[0];
+ NSString* key;
+ if ([announcement providesLabelNotTitle]) {
+ key = [announcement moxLabel];
+ } else {
+ key = [announcement moxTitle];
+ }
+
+ NSDictionary* info = @{
+ NSAccessibilityAnnouncementKey : key ? key : @(""),
+ NSAccessibilityPriorityKey : @(NSAccessibilityPriorityMedium)
+ };
+
+ id window = [self moxWindow];
+
+ // This sends events via nsIObserverService to be consumed by our
+ // mochitests. Normally we'd fire these events through moxPostNotification
+ // which takes care of this, but because the window we fetch above isn't
+ // derrived from MOXAccessibleBase, we do this (and post the notification)
+ // manually.
+ xpcAccessibleMacEvent::FireEvent(
+ window, NSAccessibilityAnnouncementRequestedNotification, info);
+ NSAccessibilityPostNotificationWithUserInfo(
+ window, NSAccessibilityAnnouncementRequestedNotification, info);
+ }
+}
+
+- (NSArray<mozAccessible*>*)getRelationsByType:(RelationType)relationType {
+ NSMutableArray<mozAccessible*>* relations =
+ [[[NSMutableArray alloc] init] autorelease];
+ Relation rel = mGeckoAccessible->RelationByType(relationType);
+ while (Accessible* relAcc = rel.Next()) {
+ if (mozAccessible* relNative = GetNativeFromGeckoAccessible(relAcc)) {
+ [relations addObject:relNative];
+ }
+ }
+
+ return relations;
+}
+
+- (void)handleAccessibleTextChangeEvent:(NSString*)change
+ inserted:(BOOL)isInserted
+ inContainer:(Accessible*)container
+ at:(int32_t)start {
+}
+
+- (void)handleAccessibleEvent:(uint32_t)eventType {
+ switch (eventType) {
+ case nsIAccessibleEvent::EVENT_ALERT:
+ [self maybePostA11yUtilNotification];
+ break;
+ case nsIAccessibleEvent::EVENT_FOCUS:
+ [self moxPostNotification:
+ NSAccessibilityFocusedUIElementChangedNotification];
+ break;
+ case nsIAccessibleEvent::EVENT_MENUPOPUP_START:
+ [self moxPostNotification:@"AXMenuOpened"];
+ break;
+ case nsIAccessibleEvent::EVENT_MENUPOPUP_END:
+ [self moxPostNotification:@"AXMenuClosed"];
+ break;
+ case nsIAccessibleEvent::EVENT_SELECTION:
+ case nsIAccessibleEvent::EVENT_SELECTION_ADD:
+ case nsIAccessibleEvent::EVENT_SELECTION_REMOVE:
+ case nsIAccessibleEvent::EVENT_SELECTION_WITHIN:
+ [self moxPostNotification:
+ NSAccessibilitySelectedChildrenChangedNotification];
+ break;
+ case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: {
+ if (![self stateWithMask:states::SELECTABLE_TEXT]) {
+ break;
+ }
+ // We consider any caret move event to be a selected text change event.
+ // So dispatching an event for EVENT_TEXT_SELECTION_CHANGED would be
+ // reduntant.
+ MOXTextMarkerDelegate* delegate =
+ static_cast<MOXTextMarkerDelegate*>([self moxTextMarkerDelegate]);
+ NSMutableDictionary* userInfo =
+ [[[delegate selectionChangeInfo] mutableCopy] autorelease];
+ userInfo[@"AXTextChangeElement"] = self;
+
+ mozAccessible* webArea = [self topWebArea];
+ [webArea
+ moxPostNotification:NSAccessibilitySelectedTextChangedNotification
+ withUserInfo:userInfo];
+ [self moxPostNotification:NSAccessibilitySelectedTextChangedNotification
+ withUserInfo:userInfo];
+ break;
+ }
+ case nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED:
+ mIsLiveRegion = true;
+ [self moxPostNotification:@"AXLiveRegionCreated"];
+ break;
+ case nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED:
+ mIsLiveRegion = false;
+ break;
+ case nsIAccessibleEvent::EVENT_REORDER:
+ [self maybePostLiveRegionChanged];
+ break;
+ case nsIAccessibleEvent::EVENT_NAME_CHANGE: {
+ if (![self providesLabelNotTitle]) {
+ [self moxPostNotification:NSAccessibilityTitleChangedNotification];
+ }
+ [self maybePostLiveRegionChanged];
+ break;
+ }
+ }
+}
+
+- (void)expire {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [self invalidateState];
+
+ mGeckoAccessible = nullptr;
+
+ [self moxPostNotification:NSAccessibilityUIElementDestroyedNotification];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (BOOL)isExpired {
+ return !mGeckoAccessible;
+}
+
+@end
diff --git a/accessible/mac/mozAccessibleProtocol.h b/accessible/mac/mozAccessibleProtocol.h
new file mode 100644
index 0000000000..bc418fa4f5
--- /dev/null
+++ b/accessible/mac/mozAccessibleProtocol.h
@@ -0,0 +1,65 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import <Cocoa/Cocoa.h>
+
+#import "mozView.h"
+
+/* This protocol's primary use is so widget/cocoa can talk back to us
+ properly.
+
+ ChildView owns the topmost mozRootAccessible, and needs to take care of
+ setting up that parent/child relationship.
+
+ This protocol is thus used to make sure it knows it's talking to us, and not
+ just some random |id|.
+*/
+
+@protocol mozAccessible <NSObject>
+
+// returns whether this accessible is the root accessible. there is one
+// root accessible per window.
+- (BOOL)isRoot;
+
+// some mozAccessibles implement accessibility support in place of another
+// object. for example, ChildView gets its support from us.
+//
+// instead of returning a mozAccessible to the OS when it wants an object, we
+// need to pass the view we represent, so the OS doesn't get confused and think
+// we return some random object.
+- (BOOL)hasRepresentedView;
+- (id)representedView;
+
+/*** general ***/
+
+// returns the accessible at the specified point.
+- (id)accessibilityHitTest:(NSPoint)point;
+
+// whether this element should be exposed to platform.
+- (BOOL)isAccessibilityElement;
+
+// currently focused UI element (possibly a child accessible)
+- (id)accessibilityFocusedUIElement;
+
+/*** attributes ***/
+
+// all supported attributes
+- (NSArray*)accessibilityAttributeNames;
+
+// value for given attribute.
+- (id)accessibilityAttributeValue:(NSString*)attribute;
+
+// whether a particular attribute can be modified
+- (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute;
+
+/*** actions ***/
+
+- (NSArray*)accessibilityActionNames;
+- (NSString*)accessibilityActionDescription:(NSString*)action;
+- (void)accessibilityPerformAction:(NSString*)action;
+
+@end
diff --git a/accessible/mac/mozActionElements.h b/accessible/mac/mozActionElements.h
new file mode 100644
index 0000000000..4c116b1bd0
--- /dev/null
+++ b/accessible/mac/mozActionElements.h
@@ -0,0 +1,101 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import <Cocoa/Cocoa.h>
+#import "mozAccessible.h"
+
+/* Simple subclasses for things like checkboxes, buttons, etc. */
+
+@interface mozButtonAccessible : mozAccessible
+
+// override
+- (NSNumber*)moxHasPopup;
+
+// override
+- (NSString*)moxPopupValue;
+
+@end
+
+@interface mozPopupButtonAccessible : mozButtonAccessible
+
+// override
+- (NSString*)moxTitle;
+
+// override
+- (BOOL)moxBlockSelector:(SEL)selector;
+
+// override
+- (NSArray*)moxChildren;
+
+// override
+- (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled;
+
+@end
+
+@interface mozCheckboxAccessible : mozButtonAccessible
+
+// override
+- (id)moxValue;
+
+// override
+- (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled;
+
+@end
+
+// LocalAccessible for a radio button
+@interface mozRadioButtonAccessible : mozCheckboxAccessible
+
+// override
+- (NSArray*)moxLinkedUIElements;
+
+@end
+
+/**
+ * Accessible for a PANE
+ */
+@interface mozPaneAccessible : mozAccessible
+
+// override
+- (NSArray*)moxChildren;
+
+@end
+
+/**
+ * Base accessible for an incrementable
+ */
+@interface mozIncrementableAccessible : mozAccessible
+
+// override
+- (id)moxValue;
+
+// override
+- (NSString*)moxValueDescription;
+
+// override
+- (id)moxMinValue;
+
+// override
+- (id)moxMaxValue;
+
+// override
+- (void)moxSetValue:(id)value;
+
+// override
+- (void)moxPerformIncrement;
+
+// override
+- (void)moxPerformDecrement;
+
+// override
+- (NSString*)moxOrientation;
+
+// override
+- (void)handleAccessibleEvent:(uint32_t)eventType;
+
+- (void)changeValueBySteps:(int)factor;
+
+@end
diff --git a/accessible/mac/mozActionElements.mm b/accessible/mac/mozActionElements.mm
new file mode 100644
index 0000000000..46ca8dab81
--- /dev/null
+++ b/accessible/mac/mozActionElements.mm
@@ -0,0 +1,237 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "mozActionElements.h"
+
+#import "MacUtils.h"
+#include "LocalAccessible-inl.h"
+#include "DocAccessible.h"
+#include "XULTabAccessible.h"
+#include "HTMLFormControlAccessible.h"
+
+#include "nsObjCExceptions.h"
+
+using namespace mozilla::a11y;
+
+enum CheckboxValue {
+ // these constants correspond to the values in the OS
+ kUnchecked = 0,
+ kChecked = 1,
+ kMixed = 2
+};
+
+@implementation mozButtonAccessible
+
+- (NSNumber*)moxHasPopup {
+ return @([self stateWithMask:states::HASPOPUP] != 0);
+}
+
+- (NSString*)moxPopupValue {
+ if ([self stateWithMask:states::HASPOPUP] != 0) {
+ return utils::GetAccAttr(self, nsGkAtoms::aria_haspopup);
+ }
+
+ return nil;
+}
+
+@end
+
+@implementation mozPopupButtonAccessible
+
+- (NSString*)moxTitle {
+ // Popup buttons don't have titles.
+ return @"";
+}
+
+- (BOOL)moxBlockSelector:(SEL)selector {
+ if (selector == @selector(moxHasPopup)) {
+ return YES;
+ }
+
+ return [super moxBlockSelector:selector];
+}
+
+- (NSArray*)moxChildren {
+ if ([self stateWithMask:states::EXPANDED] == 0) {
+ // If the popup button is collapsed don't return its children.
+ return @[];
+ }
+
+ return [super moxChildren];
+}
+
+- (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled {
+ [super stateChanged:state isEnabled:enabled];
+
+ if (state == states::EXPANDED) {
+ // If the EXPANDED state is updated, fire AXMenu events on the
+ // popups child which is the actual menu.
+ if (mozAccessible* popup = (mozAccessible*)[self childAt:0]) {
+ [popup moxPostNotification:(enabled ? @"AXMenuOpened" : @"AXMenuClosed")];
+ }
+ }
+}
+
+@end
+
+@implementation mozRadioButtonAccessible
+
+- (NSArray*)moxLinkedUIElements {
+ return [[self getRelationsByType:RelationType::MEMBER_OF]
+ arrayByAddingObjectsFromArray:[super moxLinkedUIElements]];
+}
+
+@end
+
+@implementation mozCheckboxAccessible
+
+- (int)isChecked {
+ // check if we're checked or in a mixed state
+ uint64_t state =
+ [self stateWithMask:(states::CHECKED | states::PRESSED | states::MIXED)];
+ if (state & (states::CHECKED | states::PRESSED)) {
+ return kChecked;
+ }
+
+ if (state & states::MIXED) {
+ return kMixed;
+ }
+
+ return kUnchecked;
+}
+
+- (id)moxValue {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return [NSNumber numberWithInt:[self isChecked]];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled {
+ [super stateChanged:state isEnabled:enabled];
+
+ if (state & (states::CHECKED | states::PRESSED | states::MIXED)) {
+ [self moxPostNotification:NSAccessibilityValueChangedNotification];
+ }
+}
+
+@end
+
+@implementation mozPaneAccessible
+
+- (NSArray*)moxChildren {
+ // By default, all tab panels are exposed in the a11y tree
+ // even if the tab they represent isn't the active tab. To
+ // prevent VoiceOver from navigating background tab content,
+ // only expose the tab panel that is currently on screen.
+ for (mozAccessible* child in [super moxChildren]) {
+ if (!([child state] & states::OFFSCREEN)) {
+ return [NSArray arrayWithObject:GetObjectOrRepresentedView(child)];
+ }
+ }
+ MOZ_ASSERT_UNREACHABLE("We have no on screen tab content?");
+ return @[];
+}
+
+@end
+
+@implementation mozIncrementableAccessible
+
+- (id)moxValue {
+ return [NSNumber numberWithDouble:mGeckoAccessible->CurValue()];
+}
+
+- (NSString*)moxValueDescription {
+ nsAutoString valueDesc;
+ mGeckoAccessible->Value(valueDesc);
+ return nsCocoaUtils::ToNSString(valueDesc);
+}
+- (id)moxMinValue {
+ return [NSNumber numberWithDouble:mGeckoAccessible->MinValue()];
+}
+
+- (id)moxMaxValue {
+ return [NSNumber numberWithDouble:mGeckoAccessible->MaxValue()];
+}
+
+- (void)moxSetValue:(id)value {
+ [self setValue:([value doubleValue])];
+}
+
+- (void)moxPerformIncrement {
+ [self changeValueBySteps:1];
+}
+
+- (void)moxPerformDecrement {
+ [self changeValueBySteps:-1];
+}
+
+- (NSString*)moxOrientation {
+ RefPtr<AccAttributes> attributes = mGeckoAccessible->Attributes();
+ if (attributes) {
+ nsAutoString result;
+ attributes->GetAttribute(nsGkAtoms::aria_orientation, result);
+ if (result.Equals(u"horizontal"_ns)) {
+ return NSAccessibilityHorizontalOrientationValue;
+ } else if (result.Equals(u"vertical"_ns)) {
+ return NSAccessibilityVerticalOrientationValue;
+ }
+ }
+
+ return NSAccessibilityUnknownOrientationValue;
+}
+
+- (void)handleAccessibleEvent:(uint32_t)eventType {
+ switch (eventType) {
+ case nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE:
+ case nsIAccessibleEvent::EVENT_VALUE_CHANGE:
+ [self moxPostNotification:NSAccessibilityValueChangedNotification];
+ break;
+ default:
+ [super handleAccessibleEvent:eventType];
+ break;
+ }
+}
+
+/*
+ * Updates the accessible's current value by factor and step.
+ *
+ * factor: A signed integer representing the number of times to
+ * apply step to the current value. A positive value will increment,
+ * while a negative one will decrement.
+ * step: An unsigned integer specified by the webauthor and indicating the
+ * amount by which to increment/decrement the current value.
+ */
+- (void)changeValueBySteps:(int)factor {
+ MOZ_ASSERT(mGeckoAccessible, "mGeckoAccessible is null");
+
+ double newValue =
+ mGeckoAccessible->CurValue() + (mGeckoAccessible->Step() * factor);
+ [self setValue:(newValue)];
+}
+
+/*
+ * Updates the accessible's current value to the specified value
+ */
+- (void)setValue:(double)value {
+ MOZ_ASSERT(mGeckoAccessible, "mGeckoAccessible is null");
+
+ double min = mGeckoAccessible->MinValue();
+ double max = mGeckoAccessible->MaxValue();
+
+ if ((IsNaN(min) || value >= min) && (IsNaN(max) || value <= max)) {
+ if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) {
+ acc->SetCurValue(value);
+ } else {
+ RemoteAccessible* proxy = mGeckoAccessible->AsRemote();
+ proxy->SetCurValue(value);
+ }
+ }
+}
+
+@end
diff --git a/accessible/mac/mozHTMLAccessible.h b/accessible/mac/mozHTMLAccessible.h
new file mode 100644
index 0000000000..48fd4b0bdc
--- /dev/null
+++ b/accessible/mac/mozHTMLAccessible.h
@@ -0,0 +1,44 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "mozAccessible.h"
+
+@interface mozHeadingAccessible : mozAccessible
+
+// override
+- (NSString*)moxTitle;
+
+// override
+- (id)moxValue;
+
+@end
+
+@interface mozLinkAccessible : mozAccessible
+
+// override
+- (id)moxValue;
+
+// override
+- (NSString*)moxRole;
+
+// override
+- (NSURL*)moxURL;
+
+// override
+- (NSNumber*)moxVisited;
+
+// override
+- (NSArray*)moxLinkedUIElements;
+
+@end
+
+@interface MOXListItemAccessible : mozAccessible
+
+// override
+- (NSString*)moxTitle;
+
+@end
diff --git a/accessible/mac/mozHTMLAccessible.mm b/accessible/mac/mozHTMLAccessible.mm
new file mode 100644
index 0000000000..5465f578a8
--- /dev/null
+++ b/accessible/mac/mozHTMLAccessible.mm
@@ -0,0 +1,86 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "mozHTMLAccessible.h"
+
+#import "LocalAccessible-inl.h"
+#import "HyperTextAccessible.h"
+
+#import "nsCocoaUtils.h"
+
+using namespace mozilla::a11y;
+
+@implementation mozHeadingAccessible
+
+- (NSString*)moxTitle {
+ nsAutoString title;
+ if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) {
+ mozilla::ErrorResult rv;
+ // XXX use the flattening API when there are available
+ // see bug 768298
+ acc->GetContent()->GetTextContent(title, rv);
+ } else if (RemoteAccessible* proxy = mGeckoAccessible->AsRemote()) {
+ proxy->Title(title);
+ }
+
+ title.CompressWhitespace();
+
+ return nsCocoaUtils::ToNSString(title);
+}
+
+- (id)moxValue {
+ GroupPos groupPos = mGeckoAccessible->GroupPosition();
+
+ return [NSNumber numberWithInt:groupPos.level];
+}
+
+@end
+
+@implementation mozLinkAccessible
+
+- (NSString*)moxValue {
+ return @"";
+}
+
+- (NSURL*)moxURL {
+ nsAutoString value;
+ mGeckoAccessible->Value(value);
+
+ NSString* urlString = value.IsEmpty() ? nil : nsCocoaUtils::ToNSString(value);
+ if (!urlString) return nil;
+
+ return [NSURL URLWithString:urlString];
+}
+
+- (NSNumber*)moxVisited {
+ return @([self stateWithMask:states::TRAVERSED] != 0);
+}
+
+- (NSString*)moxRole {
+ // If this is not LINKED, just expose this as a generic group accessible.
+ // Chrome and Safari expose this as a childless AXStaticText, but
+ // the HTML Accessibility API Mappings spec says this should be an AXGroup.
+ if (![self stateWithMask:states::LINKED]) {
+ return NSAccessibilityGroupRole;
+ }
+
+ return [super moxRole];
+}
+
+- (NSArray*)moxLinkedUIElements {
+ return [self getRelationsByType:RelationType::LINKS_TO];
+}
+
+@end
+
+@implementation MOXListItemAccessible
+
+- (NSString*)moxTitle {
+ return @"";
+}
+
+@end
diff --git a/accessible/mac/mozRootAccessible.h b/accessible/mac/mozRootAccessible.h
new file mode 100644
index 0000000000..929eca01dd
--- /dev/null
+++ b/accessible/mac/mozRootAccessible.h
@@ -0,0 +1,58 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import <Cocoa/Cocoa.h>
+#import "mozAccessible.h"
+
+// our protocol that we implement (so cocoa widgets can talk to us)
+#import "mozAccessibleProtocol.h"
+
+/*
+ The root accessible. There is one per window.
+ Created by the RootAccessibleWrap.
+*/
+@interface mozRootAccessible : mozAccessible {
+ // the mozView that we're representing.
+ // all outside communication goes through the mozView.
+ // in reality, it's just piping all calls to us, and we're
+ // doing its dirty work!
+ //
+ // whenever someone asks who we are (e.g., a child asking
+ // for its parent, or our parent asking for its child), we'll
+ // respond the mozView. it is absolutely necessary for third-
+ // party tools that we do this!
+ //
+ // /hwaara
+ id<mozView, mozAccessible> mParallelView; // weak ref
+}
+
+// override
+- (id)initWithAccessible:(mozilla::a11y::Accessible*)aAcc;
+
+#pragma mark - MOXAccessible
+
+// override
+- (NSNumber*)moxMain;
+
+// override
+- (NSNumber*)moxMinimized;
+
+// override
+- (id)moxUnignoredParent;
+
+#pragma mark - mozAccessible/widget
+
+// override
+- (BOOL)hasRepresentedView;
+
+// override
+- (id)representedView;
+
+// override
+- (BOOL)isRoot;
+
+@end
diff --git a/accessible/mac/mozRootAccessible.mm b/accessible/mac/mozRootAccessible.mm
new file mode 100644
index 0000000000..3f171ada8c
--- /dev/null
+++ b/accessible/mac/mozRootAccessible.mm
@@ -0,0 +1,84 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "RootAccessibleWrap.h"
+
+#import "mozRootAccessible.h"
+
+#import "mozView.h"
+
+#include "gfxPlatform.h"
+// This must be included last:
+#include "nsObjCExceptions.h"
+
+using namespace mozilla::a11y;
+
+static id<mozAccessible, mozView> getNativeViewFromRootAccessible(
+ LocalAccessible* aAccessible) {
+ RootAccessibleWrap* root =
+ static_cast<RootAccessibleWrap*>(aAccessible->AsRoot());
+ id<mozAccessible, mozView> nativeView = nil;
+ root->GetNativeWidget((void**)&nativeView);
+ return nativeView;
+}
+
+#pragma mark -
+
+@implementation mozRootAccessible
+
+- (id)initWithAccessible:(mozilla::a11y::Accessible*)aAcc {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_ASSERT(!aAcc->IsRemote(), "mozRootAccessible is never a proxy");
+
+ mParallelView = getNativeViewFromRootAccessible(aAcc->AsLocal());
+
+ return [super initWithAccessible:aAcc];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (NSNumber*)moxMain {
+ return @([[self moxWindow] isMainWindow]);
+}
+
+- (NSNumber*)moxMinimized {
+ return @([[self moxWindow] isMiniaturized]);
+}
+
+// return the AXParent that our parallell NSView tells us about.
+- (id)moxUnignoredParent {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // If there is no represented view (eg. headless), this will return nil.
+ return [[self representedView]
+ accessibilityAttributeValue:NSAccessibilityParentAttribute];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (BOOL)hasRepresentedView {
+ return YES;
+}
+
+// this will return our parallell NSView. see mozDocAccessible.h
+- (id)representedView {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_ASSERT(mParallelView || gfxPlatform::IsHeadless(),
+ "root accessible does not have a native parallel view.");
+
+ return mParallelView;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (BOOL)isRoot {
+ return YES;
+}
+
+@end
diff --git a/accessible/mac/mozSelectableElements.h b/accessible/mac/mozSelectableElements.h
new file mode 100644
index 0000000000..77c8c30aed
--- /dev/null
+++ b/accessible/mac/mozSelectableElements.h
@@ -0,0 +1,128 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import <Cocoa/Cocoa.h>
+#import "mozAccessible.h"
+
+@interface mozSelectableAccessible : mozAccessible
+
+- (NSArray*)selectableChildren;
+
+// override
+- (void)moxSetSelectedChildren:(NSArray*)selectedChildren;
+
+// override
+- (NSArray*)moxSelectedChildren;
+
+@end
+
+@interface mozSelectableChildAccessible : mozAccessible
+
+// override
+- (NSNumber*)moxSelected;
+
+// override
+- (void)moxSetSelected:(NSNumber*)selected;
+
+@end
+
+@interface mozTabGroupAccessible : mozSelectableAccessible
+
+// override
+- (NSArray*)moxTabs;
+
+// override
+- (NSArray*)moxContents;
+
+// override
+- (id)moxValue;
+
+@end
+
+@interface mozTabAccessible : mozSelectableChildAccessible
+
+// override
+- (NSString*)moxRoleDescription;
+
+// override
+- (id)moxValue;
+
+@end
+
+@interface mozListboxAccessible : mozSelectableAccessible
+
+// override
+- (BOOL)moxIgnoreChild:(mozAccessible*)child;
+
+// override
+- (BOOL)disableChild:(mozAccessible*)child;
+
+// override
+- (NSString*)moxOrientation;
+
+@end
+
+@interface mozOptionAccessible : mozSelectableChildAccessible
+
+// override
+- (NSString*)moxTitle;
+
+// override
+- (id)moxValue;
+
+@end
+
+@interface mozMenuAccessible : mozSelectableAccessible {
+ BOOL mIsOpened;
+}
+
+// override
+- (NSString*)moxTitle;
+
+// override
+- (NSString*)moxLabel;
+
+// override
+- (NSArray*)moxVisibleChildren;
+
+// override
+- (BOOL)moxIgnoreWithParent:(mozAccessible*)parent;
+
+// override
+- (id)moxTitleUIElement;
+
+// override
+- (void)moxPostNotification:(NSString*)notification;
+
+// override
+- (void)expire;
+
+- (BOOL)isOpened;
+
+@end
+
+@interface mozMenuItemAccessible : mozSelectableChildAccessible
+
+// override
+- (NSString*)moxLabel;
+
+// override
+- (BOOL)moxIgnoreWithParent:(mozAccessible*)parent;
+
+// override
+- (NSString*)moxMenuItemMarkChar;
+
+// override
+- (NSNumber*)moxSelected;
+
+// override
+- (void)handleAccessibleEvent:(uint32_t)eventType;
+
+// override
+- (void)moxPerformPress;
+
+@end
diff --git a/accessible/mac/mozSelectableElements.mm b/accessible/mac/mozSelectableElements.mm
new file mode 100644
index 0000000000..693514e405
--- /dev/null
+++ b/accessible/mac/mozSelectableElements.mm
@@ -0,0 +1,336 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "mozSelectableElements.h"
+#import "MOXWebAreaAccessible.h"
+#import "MacUtils.h"
+#include "LocalAccessible-inl.h"
+#include "nsCocoaUtils.h"
+
+using namespace mozilla::a11y;
+
+@implementation mozSelectableAccessible
+
+/**
+ * Return the mozAccessibles that are selectable.
+ */
+- (NSArray*)selectableChildren {
+ NSArray* toFilter;
+ if ([self isKindOfClass:[mozMenuAccessible class]]) {
+ // If we are a menu, our children are only selectable if they are visible
+ // so we filter this array instead of our unignored children list, which may
+ // contain invisible items.
+ toFilter = [static_cast<mozMenuAccessible*>(self) moxVisibleChildren];
+ } else {
+ toFilter = [self moxUnignoredChildren];
+ }
+ return [toFilter
+ filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(
+ mozAccessible* child,
+ NSDictionary* bindings) {
+ return [child isKindOfClass:[mozSelectableChildAccessible class]];
+ }]];
+}
+
+- (void)moxSetSelectedChildren:(NSArray*)selectedChildren {
+ for (id child in [self selectableChildren]) {
+ BOOL selected =
+ [selectedChildren indexOfObjectIdenticalTo:child] != NSNotFound;
+ [child moxSetSelected:@(selected)];
+ }
+}
+
+/**
+ * Return the mozAccessibles that are actually selected.
+ */
+- (NSArray*)moxSelectedChildren {
+ return [[self selectableChildren]
+ filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(
+ mozAccessible* child,
+ NSDictionary* bindings) {
+ // Return mozSelectableChildAccessibles that have are selected (truthy
+ // value).
+ return [[(mozSelectableChildAccessible*)child moxSelected] boolValue];
+ }]];
+}
+
+@end
+
+@implementation mozSelectableChildAccessible
+
+- (NSNumber*)moxSelected {
+ return @([self stateWithMask:states::SELECTED] != 0);
+}
+
+- (void)moxSetSelected:(NSNumber*)selected {
+ // Get SELECTABLE and UNAVAILABLE state.
+ uint64_t state =
+ [self stateWithMask:(states::SELECTABLE | states::UNAVAILABLE)];
+ if ((state & states::SELECTABLE) == 0 || (state & states::UNAVAILABLE) != 0) {
+ // The object is either not selectable or is unavailable. Don't do anything.
+ return;
+ }
+
+ mGeckoAccessible->SetSelected([selected boolValue]);
+
+ // We need to invalidate the state because the accessibility service
+ // may check the selected attribute synchornously and not wait for
+ // selection events.
+ [self invalidateState];
+}
+
+@end
+
+@implementation mozTabGroupAccessible
+
+- (NSArray*)moxTabs {
+ return [self selectableChildren];
+}
+
+- (NSArray*)moxContents {
+ return [self moxUnignoredChildren];
+}
+
+- (id)moxValue {
+ // The value of a tab group is its selected child. In the case
+ // of multiple selections this will return the first one.
+ return [[self moxSelectedChildren] firstObject];
+}
+
+@end
+
+@implementation mozTabAccessible
+
+- (NSString*)moxRoleDescription {
+ return utils::LocalizedString(u"tab"_ns);
+}
+
+- (id)moxValue {
+ // Retuens 1 if item is selected, 0 if not.
+ return [self moxSelected];
+}
+
+@end
+
+@implementation mozListboxAccessible
+
+- (BOOL)moxIgnoreChild:(mozAccessible*)child {
+ if (!child || child->mRole == roles::GROUPING) {
+ return YES;
+ }
+
+ return [super moxIgnoreChild:child];
+}
+
+- (BOOL)disableChild:(mozAccessible*)child {
+ return ![child isKindOfClass:[mozSelectableChildAccessible class]];
+}
+
+- (NSString*)moxOrientation {
+ return NSAccessibilityUnknownOrientationValue;
+}
+
+@end
+
+@implementation mozOptionAccessible
+
+- (NSString*)moxTitle {
+ return @"";
+}
+
+- (id)moxValue {
+ // Swap title and value of option so it behaves more like a AXStaticText.
+ return [super moxTitle];
+}
+
+@end
+
+@implementation mozMenuAccessible
+
+- (NSString*)moxTitle {
+ return @"";
+}
+
+- (NSString*)moxLabel {
+ return @"";
+}
+
+- (BOOL)moxIgnoreWithParent:(mozAccessible*)parent {
+ // This helps us generate the correct moxChildren array for
+ // a sub menu -- that returned array should contain all
+ // menu items, regardless of if they are visible or not.
+ // Because moxChildren does ignore filtering, and because
+ // our base ignore method filters out invisible accessibles,
+ // we override this method.
+ if ([parent isKindOfClass:[MOXWebAreaAccessible class]] ||
+ [parent isKindOfClass:[MOXRootGroup class]]) {
+ // We are a top level menu. Check our visibility the normal way
+ return [super moxIgnoreWithParent:parent];
+ }
+
+ if ([parent isKindOfClass:[mozMenuItemAccessible class]] &&
+ [parent geckoAccessible]->Role() == roles::PARENT_MENUITEM) {
+ // We are a submenu. If our parent menu item is in an open menu
+ // we should not be ignored
+ id grandparent = [parent moxParent];
+ if ([grandparent isKindOfClass:[mozMenuAccessible class]]) {
+ mozMenuAccessible* parentMenu =
+ static_cast<mozMenuAccessible*>(grandparent);
+ return ![parentMenu isOpened];
+ }
+ }
+
+ // Otherwise, we call into our superclass's ignore method
+ // to handle menus that are not submenus
+ return [super moxIgnoreWithParent:parent];
+}
+
+- (NSArray*)moxVisibleChildren {
+ // VO expects us to expose two lists of children on menus: all children
+ // (done in moxUnignoredChildren), and children which are visible (here).
+ // We implement ignoreWithParent for both menus and menu items
+ // to ensure moxUnignoredChildren returns a complete list of children
+ // regardless of visibility, see comments in those methods for additional
+ // info.
+ return [[self moxChildren]
+ filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(
+ mozAccessible* child,
+ NSDictionary* bindings) {
+ if (LocalAccessible* acc = [child geckoAccessible]->AsLocal()) {
+ if (acc->IsContent() && acc->GetContent()->IsXULElement()) {
+ return ((acc->VisibilityState() & states::INVISIBLE) == 0);
+ }
+ }
+ return true;
+ }]];
+}
+
+- (id)moxTitleUIElement {
+ id parent = [self moxUnignoredParent];
+ if (parent && [parent isKindOfClass:[mozAccessible class]]) {
+ return parent;
+ }
+
+ return nil;
+}
+
+- (void)moxPostNotification:(NSString*)notification {
+ if ([notification isEqualToString:@"AXMenuOpened"]) {
+ mIsOpened = YES;
+ } else if ([notification isEqualToString:@"AXMenuClosed"]) {
+ mIsOpened = NO;
+ }
+
+ [super moxPostNotification:notification];
+}
+
+- (void)expire {
+ if (mIsOpened) {
+ // VO needs to receive a menu closed event when the menu goes away.
+ // If the menu is being destroyed, send a menu closed event first.
+ [self moxPostNotification:@"AXMenuClosed"];
+ }
+
+ [super expire];
+}
+
+- (BOOL)isOpened {
+ return mIsOpened;
+}
+
+@end
+
+@implementation mozMenuItemAccessible
+
+- (NSString*)moxLabel {
+ return @"";
+}
+
+- (BOOL)moxIgnoreWithParent:(mozAccessible*)parent {
+ // This helps us generate the correct moxChildren array for
+ // a mozMenuAccessible; the returned array should contain all
+ // menu items, regardless of if they are visible or not.
+ // Because moxChildren does ignore filtering, and because
+ // our base ignore method filters out invisible accessibles,
+ // we override this method.
+ Accessible* parentAcc = [parent geckoAccessible];
+ if (parentAcc) {
+ Accessible* grandparentAcc = parentAcc->Parent();
+ if (mozAccessible* directGrandparent =
+ GetNativeFromGeckoAccessible(grandparentAcc)) {
+ if ([directGrandparent isKindOfClass:[MOXWebAreaAccessible class]]) {
+ return [parent moxIgnoreWithParent:directGrandparent];
+ }
+ }
+ }
+
+ id grandparent = [parent moxParent];
+ if ([grandparent isKindOfClass:[mozMenuItemAccessible class]]) {
+ mozMenuItemAccessible* acc =
+ static_cast<mozMenuItemAccessible*>(grandparent);
+ if ([acc geckoAccessible]->Role() == roles::PARENT_MENUITEM) {
+ mozMenuAccessible* parentMenu = static_cast<mozMenuAccessible*>(parent);
+ // if we are a menu item in a submenu, display only when
+ // parent menu item is open
+ return ![parentMenu isOpened];
+ }
+ }
+
+ // Otherwise, we call into our superclass's method to handle
+ // menuitems that are not within submenus
+ return [super moxIgnoreWithParent:parent];
+}
+
+- (NSString*)moxMenuItemMarkChar {
+ LocalAccessible* acc = mGeckoAccessible->AsLocal();
+ if (acc && acc->IsContent() &&
+ acc->GetContent()->IsXULElement(nsGkAtoms::menuitem)) {
+ // We need to provide a marker character. This is the visible "√" you see
+ // on dropdown menus. In our a11y tree this is a single child text node
+ // of the menu item.
+ // We do this only with XUL menuitems that conform to the native theme, and
+ // not with aria menu items that might have a pseudo element or something.
+ if (acc->ChildCount() == 1 &&
+ acc->LocalFirstChild()->Role() == roles::STATICTEXT) {
+ nsAutoString marker;
+ acc->LocalFirstChild()->Name(marker);
+ if (marker.Length() == 1) {
+ return nsCocoaUtils::ToNSString(marker);
+ }
+ }
+ }
+
+ return nil;
+}
+
+- (NSNumber*)moxSelected {
+ // Our focused state is equivelent to native selected states for menus.
+ return @([self stateWithMask:states::FOCUSED] != 0);
+}
+
+- (void)handleAccessibleEvent:(uint32_t)eventType {
+ switch (eventType) {
+ case nsIAccessibleEvent::EVENT_FOCUS:
+ [self invalidateState];
+ // Our focused state is equivelent to native selected states for menus.
+ mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent];
+ [parent moxPostNotification:
+ NSAccessibilitySelectedChildrenChangedNotification];
+ break;
+ }
+
+ [super handleAccessibleEvent:eventType];
+}
+
+- (void)moxPerformPress {
+ [super moxPerformPress];
+ // when a menu item is pressed (chosen), we need to tell
+ // VoiceOver about it, so we send this notification
+ [self moxPostNotification:@"AXMenuItemSelected"];
+}
+
+@end
diff --git a/accessible/mac/mozTableAccessible.h b/accessible/mac/mozTableAccessible.h
new file mode 100644
index 0000000000..a54614eaca
--- /dev/null
+++ b/accessible/mac/mozTableAccessible.h
@@ -0,0 +1,186 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "mozAccessible.h"
+
+@interface mozColumnContainer : MOXAccessibleBase {
+ uint32_t mIndex;
+ mozAccessible* mParent;
+ NSMutableArray* mChildren;
+}
+
+// override
+- (id)initWithIndex:(uint32_t)aIndex andParent:(mozAccessible*)aParent;
+
+// override
+- (NSString*)moxRole;
+
+// override
+- (NSString*)moxRoleDescription;
+
+// override
+- (mozAccessible*)moxParent;
+
+// override
+- (NSArray*)moxUnignoredChildren;
+
+// override
+- (void)dealloc;
+
+// override
+- (void)expire;
+
+// override
+- (BOOL)isExpired;
+
+- (void)invalidateChildren;
+
+@end
+
+@interface mozTablePartAccessible : mozAccessible
+
+// override
+- (NSString*)moxTitle;
+
+// override
+- (NSString*)moxRole;
+
+// override
+- (void)handleAccessibleEvent:(uint32_t)eventType;
+
+- (BOOL)isLayoutTablePart;
+
+- (void)invalidateLayoutTableCache;
+
+@end
+
+@interface mozTableAccessible : mozTablePartAccessible {
+ NSMutableArray* mColContainers;
+ uint32_t mIsLayoutTable;
+}
+
+// local override
+- (BOOL)isLayoutTablePart;
+
+// local override
+- (void)invalidateLayoutTableCache;
+
+- (void)invalidateColumns;
+
+// override
+- (void)handleAccessibleEvent:(uint32_t)eventType;
+
+// override
+- (void)dealloc;
+
+// override
+- (void)expire;
+
+// override
+- (NSNumber*)moxRowCount;
+
+// override
+- (NSNumber*)moxColumnCount;
+
+// override
+- (NSArray*)moxRows;
+
+// override
+- (NSArray*)moxColumns;
+
+// override
+- (NSArray*)moxUnignoredChildren;
+
+// override
+- (NSArray*)moxColumnHeaderUIElements;
+
+// override
+- (id)moxCellForColumnAndRow:(NSArray*)columnAndRow;
+
+@end
+
+@interface mozTableRowAccessible : mozTablePartAccessible
+
+// override
+- (void)handleAccessibleEvent:(uint32_t)eventType;
+
+// override
+- (NSNumber*)moxIndex;
+
+@end
+
+@interface mozTableCellAccessible : mozTablePartAccessible
+
+// override
+- (NSValue*)moxRowIndexRange;
+
+// override
+- (NSValue*)moxColumnIndexRange;
+
+// override
+- (NSArray*)moxRowHeaderUIElements;
+
+// override
+- (NSArray*)moxColumnHeaderUIElements;
+
+@end
+
+@interface mozOutlineAccessible : mozAccessible
+
+// local override
+- (BOOL)isLayoutTablePart;
+
+// override
+- (NSArray*)moxRows;
+
+// override
+- (NSArray*)moxColumns;
+
+// override
+- (NSArray*)moxSelectedRows;
+
+// override
+- (NSString*)moxOrientation;
+
+@end
+
+@interface mozOutlineRowAccessible : mozTableRowAccessible
+
+// override
+- (BOOL)isLayoutTablePart;
+
+// override
+- (NSNumber*)moxDisclosing;
+
+// override
+- (void)moxSetDisclosing:(NSNumber*)disclosing;
+
+// override
+- (NSNumber*)moxExpanded;
+
+// override
+- (id)moxDisclosedByRow;
+
+// override
+- (NSNumber*)moxDisclosureLevel;
+
+// override
+- (NSArray*)moxDisclosedRows;
+
+// override
+- (NSNumber*)moxIndex;
+
+// override
+- (NSString*)moxLabel;
+
+// override
+- (id)moxValue;
+
+// override
+- (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled;
+
+@end
diff --git a/accessible/mac/mozTableAccessible.mm b/accessible/mac/mozTableAccessible.mm
new file mode 100644
index 0000000000..48cd8ba11f
--- /dev/null
+++ b/accessible/mac/mozTableAccessible.mm
@@ -0,0 +1,739 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "mozTableAccessible.h"
+#import "nsCocoaUtils.h"
+#import "MacUtils.h"
+#import "RotorRules.h"
+
+#include "AccIterator.h"
+#include "LocalAccessible.h"
+#include "mozilla/a11y/TableAccessibleBase.h"
+#include "mozilla/a11y/TableCellAccessibleBase.h"
+#include "mozilla/StaticPrefs_accessibility.h"
+#include "XULTreeAccessible.h"
+#include "Pivot.h"
+#include "Relation.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+enum CachedBool { eCachedBoolMiss, eCachedTrue, eCachedFalse };
+
+@implementation mozColumnContainer
+
+- (id)initWithIndex:(uint32_t)aIndex andParent:(mozAccessible*)aParent {
+ self = [super init];
+ mIndex = aIndex;
+ mParent = aParent;
+ return self;
+}
+
+- (NSString*)moxRole {
+ return NSAccessibilityColumnRole;
+}
+
+- (NSString*)moxRoleDescription {
+ return NSAccessibilityRoleDescription(NSAccessibilityColumnRole, nil);
+}
+
+- (mozAccessible*)moxParent {
+ return mParent;
+}
+
+- (NSArray*)moxUnignoredChildren {
+ if (mChildren) return mChildren;
+
+ mChildren = [[NSMutableArray alloc] init];
+
+ if (StaticPrefs::accessibility_cache_enabled_AtStartup() ||
+ [mParent geckoAccessible]->IsLocal()) {
+ TableAccessibleBase* table = [mParent geckoAccessible]->AsTableBase();
+ MOZ_ASSERT(table, "Got null table when fetching column children!");
+ uint32_t numRows = table->RowCount();
+
+ for (uint32_t j = 0; j < numRows; j++) {
+ Accessible* cell = table->CellAt(j, mIndex);
+ mozAccessible* nativeCell =
+ cell ? GetNativeFromGeckoAccessible(cell) : nil;
+ if ([nativeCell isAccessibilityElement]) {
+ [mChildren addObject:nativeCell];
+ }
+ }
+
+ } else if (RemoteAccessible* proxy = [mParent geckoAccessible]->AsRemote()) {
+ uint32_t numRows = proxy->TableRowCount();
+
+ for (uint32_t j = 0; j < numRows; j++) {
+ RemoteAccessible* cell = proxy->TableCellAt(j, mIndex);
+ mozAccessible* nativeCell =
+ cell ? GetNativeFromGeckoAccessible(cell) : nil;
+ if ([nativeCell isAccessibilityElement]) {
+ [mChildren addObject:nativeCell];
+ }
+ }
+ }
+
+ return mChildren;
+}
+
+- (void)dealloc {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [self invalidateChildren];
+ [super dealloc];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)expire {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [self invalidateChildren];
+
+ mParent = nil;
+
+ [super expire];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (BOOL)isExpired {
+ MOZ_ASSERT((mChildren == nil && mParent == nil) == mIsExpired);
+
+ return [super isExpired];
+}
+
+- (void)invalidateChildren {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // make room for new children
+ if (mChildren) {
+ [mChildren release];
+ mChildren = nil;
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+@end
+
+@implementation mozTablePartAccessible
+
+- (NSString*)moxTitle {
+ return @"";
+}
+
+- (NSString*)moxRole {
+ return [self isLayoutTablePart] ? NSAccessibilityGroupRole : [super moxRole];
+}
+
+- (void)handleAccessibleEvent:(uint32_t)eventType {
+ if (![self isKindOfClass:[mozTableAccessible class]] &&
+ !StaticPrefs::accessibility_cache_enabled_AtStartup()) {
+ // If we are not a table, we are a cell or a row.
+ // Check to see if the event we're handling should
+ // invalidate the mIsLayoutTable cache on our parent
+ // table. Only do this when the core cache is off, because
+ // we don't use the platform cache when its on.
+ if (eventType == nsIAccessibleEvent::EVENT_REORDER ||
+ eventType == nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED) {
+ // Invalidate the cache on our parent table
+ [self invalidateLayoutTableCache];
+ }
+ }
+
+ [super handleAccessibleEvent:eventType];
+}
+
+- (BOOL)isLayoutTablePart {
+ // mIsLayoutTable is a cache on each mozTableAccessible that stores
+ // the previous result of calling IsProbablyLayoutTable in core. To see
+ // how/when the cache is invalidated, view handleAccessibleEvent.
+ // The cache contains one of three values from the CachedBool enum
+ // defined in mozTableAccessible.h
+ mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent];
+ if ([parent isKindOfClass:[mozTablePartAccessible class]]) {
+ return [(mozTablePartAccessible*)parent isLayoutTablePart];
+ } else if ([parent isKindOfClass:[mozOutlineAccessible class]]) {
+ return [(mozOutlineAccessible*)parent isLayoutTablePart];
+ }
+
+ return NO;
+}
+
+- (void)invalidateLayoutTableCache {
+ mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent];
+ if ([parent isKindOfClass:[mozTablePartAccessible class]]) {
+ // We do this to prevent dispatching invalidateLayoutTableCache
+ // on outlines or outline parts. This is possible here because
+ // outline rows subclass table rows, which are a table part.
+ // This means `parent` could be an outline, and there is no
+ // cache on outlines to invalidate.
+ [(mozTablePartAccessible*)parent invalidateLayoutTableCache];
+ }
+}
+@end
+
+@implementation mozTableAccessible
+
+- (void)invalidateLayoutTableCache {
+ MOZ_ASSERT(!StaticPrefs::accessibility_cache_enabled_AtStartup(),
+ "If the core cache is enabled we shouldn't be maintaining the "
+ "platform table cache!");
+ mIsLayoutTable = eCachedBoolMiss;
+}
+
+- (BOOL)isLayoutTablePart {
+ if (mIsLayoutTable != eCachedBoolMiss &&
+ !StaticPrefs::accessibility_cache_enabled_AtStartup()) {
+ // Only use the platform cache if the core cache is not on
+ return mIsLayoutTable == eCachedTrue;
+ }
+
+ if (mGeckoAccessible->Role() == roles::TREE_TABLE) {
+ // tree tables are never layout tables, and we shouldn't
+ // query IsProbablyLayoutTable() on them, so we short
+ // circuit here
+ mIsLayoutTable = eCachedFalse;
+ return false;
+ }
+
+ bool tableGuess;
+ // For LocalAccessible and cached RemoteAccessible, we could use
+ // AsTableBase()->IsProbablyLayoutTable(). However, if the cache is enabled,
+ // that would build the table cache, which is pointless for layout tables on
+ // Mac because layout tables are AXGroups and do not expose table properties
+ // like AXRows, AXColumns, etc.
+ if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) {
+ tableGuess = acc->AsTable()->IsProbablyLayoutTable();
+ } else {
+ RemoteAccessible* proxy = mGeckoAccessible->AsRemote();
+ tableGuess = proxy->TableIsProbablyForLayout();
+ }
+
+ mIsLayoutTable = tableGuess ? eCachedTrue : eCachedFalse;
+ return tableGuess;
+}
+
+- (void)handleAccessibleEvent:(uint32_t)eventType {
+ if (eventType == nsIAccessibleEvent::EVENT_REORDER ||
+ eventType == nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED ||
+ eventType == nsIAccessibleEvent::EVENT_TABLE_STYLING_CHANGED) {
+ if (!StaticPrefs::accessibility_cache_enabled_AtStartup()) {
+ [self invalidateLayoutTableCache];
+ }
+ [self invalidateColumns];
+ }
+
+ [super handleAccessibleEvent:eventType];
+}
+
+- (void)dealloc {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [self invalidateColumns];
+ [super dealloc];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)expire {
+ [self invalidateColumns];
+ [super expire];
+}
+
+- (NSNumber*)moxRowCount {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ return (StaticPrefs::accessibility_cache_enabled_AtStartup() ||
+ mGeckoAccessible->IsLocal())
+ ? @(mGeckoAccessible->AsTableBase()->RowCount())
+ : @(mGeckoAccessible->AsRemote()->TableRowCount());
+}
+
+- (NSNumber*)moxColumnCount {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ return (StaticPrefs::accessibility_cache_enabled_AtStartup() ||
+ mGeckoAccessible->IsLocal())
+ ? @(mGeckoAccessible->AsTableBase()->ColCount())
+ : @(mGeckoAccessible->AsRemote()->TableColumnCount());
+}
+
+- (NSArray*)moxRows {
+ // Create a new array with the list of table rows.
+ NSArray* children = [self moxChildren];
+ NSMutableArray* rows = [[[NSMutableArray alloc] init] autorelease];
+ for (mozAccessible* curr : children) {
+ if ([curr isKindOfClass:[mozTableRowAccessible class]]) {
+ [rows addObject:curr];
+ } else if ([[curr moxRole] isEqualToString:@"AXGroup"]) {
+ // Plain thead/tbody elements are removed from the core a11y tree and
+ // replaced with their subtree, but thead/tbody elements with click
+ // handlers are not -- they remain as groups. We need to expose any
+ // rows they contain as rows of the parent table.
+ [rows
+ addObjectsFromArray:[[curr moxChildren]
+ filteredArrayUsingPredicate:
+ [NSPredicate predicateWithBlock:^BOOL(
+ mozAccessible* child,
+ NSDictionary* bindings) {
+ return [child
+ isKindOfClass:[mozTableRowAccessible
+ class]];
+ }]]];
+ }
+ }
+
+ return rows;
+}
+
+- (NSArray*)moxColumns {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ if (mColContainers) {
+ return mColContainers;
+ }
+
+ mColContainers = [[NSMutableArray alloc] init];
+ uint32_t numCols = 0;
+
+ if (StaticPrefs::accessibility_cache_enabled_AtStartup() ||
+ mGeckoAccessible->IsLocal()) {
+ numCols = mGeckoAccessible->AsTableBase()->ColCount();
+ } else {
+ numCols = mGeckoAccessible->AsRemote()->TableColumnCount();
+ }
+
+ for (uint32_t i = 0; i < numCols; i++) {
+ mozColumnContainer* container =
+ [[mozColumnContainer alloc] initWithIndex:i andParent:self];
+ [mColContainers addObject:container];
+ }
+
+ return mColContainers;
+}
+
+- (NSArray*)moxUnignoredChildren {
+ if (![self isLayoutTablePart]) {
+ return [[super moxUnignoredChildren]
+ arrayByAddingObjectsFromArray:[self moxColumns]];
+ }
+
+ return [super moxUnignoredChildren];
+}
+
+- (NSArray*)moxColumnHeaderUIElements {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ uint32_t numCols = 0;
+ TableAccessibleBase* table = nullptr;
+
+ if (StaticPrefs::accessibility_cache_enabled_AtStartup() ||
+ mGeckoAccessible->IsLocal()) {
+ table = mGeckoAccessible->AsTableBase();
+ numCols = table->ColCount();
+ } else {
+ numCols = mGeckoAccessible->AsRemote()->TableColumnCount();
+ }
+
+ NSMutableArray* colHeaders =
+ [[[NSMutableArray alloc] initWithCapacity:numCols] autorelease];
+
+ for (uint32_t i = 0; i < numCols; i++) {
+ Accessible* cell;
+ if (table) {
+ cell = table->CellAt(0, i);
+ } else {
+ cell = mGeckoAccessible->AsRemote()->TableCellAt(0, i);
+ }
+
+ if (cell && cell->Role() == roles::COLUMNHEADER) {
+ mozAccessible* colHeader = GetNativeFromGeckoAccessible(cell);
+ [colHeaders addObject:colHeader];
+ }
+ }
+
+ return colHeaders;
+}
+
+- (id)moxCellForColumnAndRow:(NSArray*)columnAndRow {
+ if (columnAndRow == nil || [columnAndRow count] != 2) {
+ return nil;
+ }
+
+ uint32_t col = [[columnAndRow objectAtIndex:0] unsignedIntValue];
+ uint32_t row = [[columnAndRow objectAtIndex:1] unsignedIntValue];
+
+ MOZ_ASSERT(mGeckoAccessible);
+
+ Accessible* cell;
+ if (StaticPrefs::accessibility_cache_enabled_AtStartup() ||
+ mGeckoAccessible->IsLocal()) {
+ cell = mGeckoAccessible->AsTableBase()->CellAt(row, col);
+ } else {
+ cell = mGeckoAccessible->AsRemote()->TableCellAt(row, col);
+ }
+
+ if (!cell) {
+ return nil;
+ }
+
+ return GetNativeFromGeckoAccessible(cell);
+}
+
+- (void)invalidateColumns {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+ if (mColContainers) {
+ for (mozColumnContainer* col in mColContainers) {
+ [col expire];
+ }
+ [mColContainers release];
+ mColContainers = nil;
+ }
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+@end
+
+@interface mozTableRowAccessible ()
+- (mozTableAccessible*)getTableParent;
+@end
+
+@implementation mozTableRowAccessible
+
+- (mozTableAccessible*)getTableParent {
+ id tableParent = static_cast<mozTableAccessible*>(
+ [self moxFindAncestor:^BOOL(id curr, BOOL* stop) {
+ if ([curr isKindOfClass:[mozOutlineAccessible class]]) {
+ // Outline rows are a kind of table row, so it's possible
+ // we're trying to call getTableParent on an outline row here.
+ // Stop searching.
+ *stop = YES;
+ }
+ return [curr isKindOfClass:[mozTableAccessible class]];
+ }]);
+
+ return [tableParent isKindOfClass:[mozTableAccessible class]] ? tableParent
+ : nil;
+}
+
+- (void)handleAccessibleEvent:(uint32_t)eventType {
+ if (eventType == nsIAccessibleEvent::EVENT_REORDER) {
+ // It is possible for getTableParent to return nil if we're
+ // handling a reorder on an outilne row. Outlines don't have
+ // columns, so there's nothing to do here and this will no-op.
+ [[self getTableParent] invalidateColumns];
+ }
+
+ [super handleAccessibleEvent:eventType];
+}
+
+- (NSNumber*)moxIndex {
+ return @([[[self getTableParent] moxRows] indexOfObjectIdenticalTo:self]);
+}
+
+@end
+
+@implementation mozTableCellAccessible
+
+- (NSValue*)moxRowIndexRange {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ if (StaticPrefs::accessibility_cache_enabled_AtStartup() ||
+ mGeckoAccessible->IsLocal()) {
+ TableCellAccessibleBase* cell = mGeckoAccessible->AsTableCellBase();
+ return
+ [NSValue valueWithRange:NSMakeRange(cell->RowIdx(), cell->RowExtent())];
+ } else {
+ RemoteAccessible* proxy = mGeckoAccessible->AsRemote();
+ return [NSValue
+ valueWithRange:NSMakeRange(proxy->RowIdx(), proxy->RowExtent())];
+ }
+}
+
+- (NSValue*)moxColumnIndexRange {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ if (StaticPrefs::accessibility_cache_enabled_AtStartup() ||
+ mGeckoAccessible->IsLocal()) {
+ TableCellAccessibleBase* cell = mGeckoAccessible->AsTableCellBase();
+ return
+ [NSValue valueWithRange:NSMakeRange(cell->ColIdx(), cell->ColExtent())];
+ } else {
+ RemoteAccessible* proxy = mGeckoAccessible->AsRemote();
+ return [NSValue
+ valueWithRange:NSMakeRange(proxy->ColIdx(), proxy->ColExtent())];
+ }
+}
+
+- (NSArray*)moxRowHeaderUIElements {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ if (StaticPrefs::accessibility_cache_enabled_AtStartup() ||
+ mGeckoAccessible->IsLocal()) {
+ TableCellAccessibleBase* cell = mGeckoAccessible->AsTableCellBase();
+ AutoTArray<Accessible*, 10> headerCells;
+ cell->RowHeaderCells(&headerCells);
+ return utils::ConvertToNSArray(headerCells);
+ } else {
+ RemoteAccessible* proxy = mGeckoAccessible->AsRemote();
+ nsTArray<RemoteAccessible*> headerCells;
+ proxy->RowHeaderCells(&headerCells);
+ return utils::ConvertToNSArray(headerCells);
+ }
+}
+
+- (NSArray*)moxColumnHeaderUIElements {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ if (StaticPrefs::accessibility_cache_enabled_AtStartup() ||
+ mGeckoAccessible->IsLocal()) {
+ TableCellAccessibleBase* cell = mGeckoAccessible->AsTableCellBase();
+ AutoTArray<Accessible*, 10> headerCells;
+ cell->ColHeaderCells(&headerCells);
+ return utils::ConvertToNSArray(headerCells);
+ } else {
+ RemoteAccessible* proxy = mGeckoAccessible->AsRemote();
+ nsTArray<RemoteAccessible*> headerCells;
+ proxy->ColHeaderCells(&headerCells);
+ return utils::ConvertToNSArray(headerCells);
+ }
+}
+
+@end
+
+@implementation mozOutlineAccessible
+
+- (BOOL)isLayoutTablePart {
+ return NO;
+}
+
+- (NSArray*)moxRows {
+ // Create a new array with the list of outline rows. We
+ // use pivot here to do a deep traversal of all rows nested
+ // in this outline, not just those which are direct
+ // children, since that's what VO expects.
+ NSMutableArray* allRows = [[[NSMutableArray alloc] init] autorelease];
+ Pivot p = Pivot(mGeckoAccessible);
+ OutlineRule rule = OutlineRule();
+ Accessible* firstChild = mGeckoAccessible->FirstChild();
+ Accessible* match = p.Next(firstChild, rule, true);
+ while (match) {
+ [allRows addObject:GetNativeFromGeckoAccessible(match)];
+ match = p.Next(match, rule);
+ }
+ return allRows;
+}
+
+- (NSArray*)moxColumns {
+ if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) {
+ if (acc->IsContent() && acc->GetContent()->IsXULElement(nsGkAtoms::tree)) {
+ XULTreeAccessible* treeAcc = (XULTreeAccessible*)acc;
+ NSMutableArray* cols = [[[NSMutableArray alloc] init] autorelease];
+ // XUL trees store their columns in a group at the tree's first
+ // child. Here, we iterate over that group to get each column's
+ // native accessible and add it to our col array.
+ LocalAccessible* treeColumns = treeAcc->LocalChildAt(0);
+ if (treeColumns) {
+ uint32_t colCount = treeColumns->ChildCount();
+ for (uint32_t i = 0; i < colCount; i++) {
+ LocalAccessible* treeColumnItem = treeColumns->LocalChildAt(i);
+ [cols addObject:GetNativeFromGeckoAccessible(treeColumnItem)];
+ }
+ return cols;
+ }
+ }
+ }
+ // Webkit says we shouldn't expose any cols for aria-tree
+ // so we return an empty array here
+ return @[];
+}
+
+- (NSArray*)moxSelectedRows {
+ NSMutableArray* selectedRows = [[[NSMutableArray alloc] init] autorelease];
+ NSArray* allRows = [self moxRows];
+ for (mozAccessible* row in allRows) {
+ if ([row stateWithMask:states::SELECTED] != 0) {
+ [selectedRows addObject:row];
+ }
+ }
+
+ return selectedRows;
+}
+
+- (NSString*)moxOrientation {
+ return NSAccessibilityVerticalOrientationValue;
+}
+
+@end
+
+@implementation mozOutlineRowAccessible
+
+- (BOOL)isLayoutTablePart {
+ return NO;
+}
+
+- (NSNumber*)moxDisclosing {
+ return @([self stateWithMask:states::EXPANDED] != 0);
+}
+
+- (void)moxSetDisclosing:(NSNumber*)disclosing {
+ // VoiceOver requires this to be settable, but doesn't
+ // require it actually affect our disclosing state.
+ // We expose the attr as settable with this method
+ // but do nothing to actually set it.
+ return;
+}
+
+- (NSNumber*)moxExpanded {
+ return @([self stateWithMask:states::EXPANDED] != 0);
+}
+
+- (id)moxDisclosedByRow {
+ // According to webkit: this attr corresponds to the row
+ // that contains this row. It should be the same as the
+ // first parent that is a treeitem. If the parent is the tree
+ // itself, this should be nil. This is tricky for xul trees because
+ // all rows are direct children of the outline; they use
+ // relations to expose their heirarchy structure.
+
+ // first we check the relations to see if we're in a xul tree
+ // with weird row semantics
+ NSArray<mozAccessible*>* disclosingRows =
+ [self getRelationsByType:RelationType::NODE_CHILD_OF];
+ mozAccessible* disclosingRow = [disclosingRows firstObject];
+
+ if (disclosingRow) {
+ // if we find a row from our relation check,
+ // verify it isn't the outline itself and return
+ // appropriately
+ if ([[disclosingRow moxRole] isEqualToString:@"AXOutline"]) {
+ return nil;
+ }
+
+ return disclosingRow;
+ }
+
+ mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent];
+ // otherwise, its likely we're in an aria tree, so we can use
+ // these role and subrole checks
+ if ([[parent moxRole] isEqualToString:@"AXOutline"]) {
+ return nil;
+ }
+
+ if ([[parent moxSubrole] isEqualToString:@"AXOutlineRow"]) {
+ disclosingRow = parent;
+ }
+
+ return nil;
+}
+
+- (NSNumber*)moxDisclosureLevel {
+ GroupPos groupPos = mGeckoAccessible->GroupPosition();
+
+ // mac expects 0-indexed levels, but groupPos.level is 1-indexed
+ // so we subtract 1 here for levels above 0
+ return groupPos.level > 0 ? @(groupPos.level - 1) : @(groupPos.level);
+}
+
+- (NSArray*)moxDisclosedRows {
+ // According to webkit: this attr corresponds to the rows
+ // that are considered inside this row. Again, this is weird for
+ // xul trees so we have to use relations first and then fall-back
+ // to the children filter for non-xul outlines.
+
+ // first we check the relations to see if we're in a xul tree
+ // with weird row semantics
+ if (NSArray* disclosedRows =
+ [self getRelationsByType:RelationType::NODE_PARENT_OF]) {
+ // if we find rows from our relation check, return them here
+ return disclosedRows;
+ }
+
+ // otherwise, filter our children for outline rows
+ return [[self moxChildren]
+ filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(
+ mozAccessible* child,
+ NSDictionary* bindings) {
+ return [child isKindOfClass:[mozOutlineRowAccessible class]];
+ }]];
+}
+
+- (NSNumber*)moxIndex {
+ id<MOXAccessible> outline =
+ [self moxFindAncestor:^BOOL(id<MOXAccessible> moxAcc, BOOL* stop) {
+ return [[moxAcc moxRole] isEqualToString:@"AXOutline"];
+ }];
+
+ NSUInteger index = [[outline moxRows] indexOfObjectIdenticalTo:self];
+ return index == NSNotFound ? nil : @(index);
+}
+
+- (NSString*)moxLabel {
+ nsAutoString title;
+ mGeckoAccessible->Name(title);
+
+ // XXX: When parsing outlines built with ul/lu's, we
+ // include the bullet in this description even
+ // though webkit doesn't. Not all outlines are built with
+ // ul/lu's so we can't strip the first character here.
+
+ return nsCocoaUtils::ToNSString(title);
+}
+
+enum CheckedState {
+ kUncheckable = -1,
+ kUnchecked = 0,
+ kChecked = 1,
+ kMixed = 2
+};
+
+- (int)checkedValue {
+ uint64_t state = [self
+ stateWithMask:(states::CHECKABLE | states::CHECKED | states::MIXED)];
+
+ if (state & states::CHECKABLE) {
+ if (state & states::CHECKED) {
+ return kChecked;
+ }
+
+ if (state & states::MIXED) {
+ return kMixed;
+ }
+
+ return kUnchecked;
+ }
+
+ return kUncheckable;
+}
+
+- (id)moxValue {
+ int checkedValue = [self checkedValue];
+ return checkedValue >= 0 ? @(checkedValue) : nil;
+}
+
+- (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled {
+ [super stateChanged:state isEnabled:enabled];
+
+ if (state & states::EXPANDED) {
+ // If the EXPANDED state is updated, fire appropriate events on the
+ // outline row.
+ [self moxPostNotification:(enabled
+ ? NSAccessibilityRowExpandedNotification
+ : NSAccessibilityRowCollapsedNotification)];
+ }
+
+ if (state & (states::CHECKED | states::CHECKABLE | states::MIXED)) {
+ // If the MIXED, CHECKED or CHECKABLE state changes, update the value we
+ // expose for the row, which communicates checked status.
+ [self moxPostNotification:NSAccessibilityValueChangedNotification];
+ }
+}
+
+@end
diff --git a/accessible/mac/mozTextAccessible.h b/accessible/mac/mozTextAccessible.h
new file mode 100644
index 0000000000..b242a2da32
--- /dev/null
+++ b/accessible/mac/mozTextAccessible.h
@@ -0,0 +1,114 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "mozAccessible.h"
+
+@interface mozTextAccessible : mozAccessible
+
+// override
+- (NSString*)moxTitle;
+
+// override
+- (id)moxValue;
+
+// override
+- (id)moxRequired;
+
+// override
+- (NSNumber*)moxInvalid;
+
+// override
+- (NSNumber*)moxInsertionPointLineNumber;
+
+// override
+- (NSString*)moxRole;
+
+// override
+- (NSString*)moxSubrole;
+
+// override
+- (NSNumber*)moxNumberOfCharacters;
+
+// override
+- (NSString*)moxSelectedText;
+
+// override
+- (NSValue*)moxSelectedTextRange;
+
+// override
+- (NSValue*)moxVisibleCharacterRange;
+
+// override
+- (BOOL)moxBlockSelector:(SEL)selector;
+
+// override
+- (void)moxSetValue:(id)value;
+
+// override
+- (void)moxSetSelectedText:(NSString*)text;
+
+// override
+- (void)moxSetSelectedTextRange:(NSValue*)range;
+
+// override
+- (void)moxSetVisibleCharacterRange:(NSValue*)range;
+
+// override
+- (NSString*)moxStringForRange:(NSValue*)range;
+
+// override
+- (NSAttributedString*)moxAttributedStringForRange:(NSValue*)range;
+
+// override
+- (NSValue*)moxRangeForLine:(NSNumber*)line;
+
+// override
+- (NSNumber*)moxLineForIndex:(NSNumber*)index;
+
+// override
+- (NSValue*)moxBoundsForRange:(NSValue*)range;
+
+#pragma mark - mozAccessible
+
+// override
+- (void)handleAccessibleTextChangeEvent:(NSString*)change
+ inserted:(BOOL)isInserted
+ inContainer:(mozilla::a11y::Accessible*)container
+ at:(int32_t)start;
+
+// override
+- (void)handleAccessibleEvent:(uint32_t)eventType;
+
+@end
+
+@interface mozTextLeafAccessible : mozAccessible
+
+// override
+- (BOOL)moxBlockSelector:(SEL)selector;
+
+// override
+- (NSString*)moxValue;
+
+// override
+- (NSString*)moxTitle;
+
+// override
+- (NSString*)moxLabel;
+
+// override
+- (BOOL)moxIgnoreWithParent:(mozAccessible*)parent;
+
+// override
+- (NSString*)moxStringForRange:(NSValue*)range;
+
+// override
+- (NSAttributedString*)moxAttributedStringForRange:(NSValue*)range;
+
+// override
+- (NSValue*)moxBoundsForRange:(NSValue*)range;
+
+@end
diff --git a/accessible/mac/mozTextAccessible.mm b/accessible/mac/mozTextAccessible.mm
new file mode 100644
index 0000000000..c3c2f9bcd8
--- /dev/null
+++ b/accessible/mac/mozTextAccessible.mm
@@ -0,0 +1,446 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AccAttributes.h"
+#include "HyperTextAccessible-inl.h"
+#include "LocalAccessible-inl.h"
+#include "mozilla/a11y/PDocAccessible.h"
+#include "nsCocoaUtils.h"
+#include "nsObjCExceptions.h"
+#include "TextLeafAccessible.h"
+
+#import "mozTextAccessible.h"
+#import "GeckoTextMarker.h"
+#import "MOXTextMarkerDelegate.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+inline bool ToNSRange(id aValue, NSRange* aRange) {
+ MOZ_ASSERT(aRange, "aRange is nil");
+
+ if ([aValue isKindOfClass:[NSValue class]] &&
+ strcmp([(NSValue*)aValue objCType], @encode(NSRange)) == 0) {
+ *aRange = [aValue rangeValue];
+ return true;
+ }
+
+ return false;
+}
+
+inline NSString* ToNSString(id aValue) {
+ if ([aValue isKindOfClass:[NSString class]]) {
+ return aValue;
+ }
+
+ return nil;
+}
+
+@interface mozTextAccessible ()
+- (long)textLength;
+- (BOOL)isReadOnly;
+- (NSString*)text;
+- (GeckoTextMarkerRange)selection;
+- (GeckoTextMarkerRange)textMarkerRangeFromRange:(NSValue*)range;
+@end
+
+@implementation mozTextAccessible
+
+- (NSString*)moxTitle {
+ return @"";
+}
+
+- (id)moxValue {
+ // Apple's SpeechSynthesisServer expects AXValue to return an AXStaticText
+ // object's AXSelectedText attribute. See bug 674612 for details.
+ // Also if there is no selected text, we return the full text.
+ // See bug 369710 for details.
+ if ([[self moxRole] isEqualToString:NSAccessibilityStaticTextRole]) {
+ NSString* selectedText = [self moxSelectedText];
+ return (selectedText && [selectedText length]) ? selectedText : [self text];
+ }
+
+ return [self text];
+}
+
+- (id)moxRequired {
+ return @([self stateWithMask:states::REQUIRED] != 0);
+}
+
+- (NSString*)moxInvalid {
+ if ([self stateWithMask:states::INVALID] != 0) {
+ // If the attribute exists, it has one of four values: true, false,
+ // grammar, or spelling. We query the attribute value here in order
+ // to find the correct string to return.
+ RefPtr<AccAttributes> attributes;
+ HyperTextAccessibleBase* text = mGeckoAccessible->AsHyperTextBase();
+ if (text && mGeckoAccessible->IsTextRole()) {
+ attributes = text->DefaultTextAttributes();
+ }
+
+ nsAutoString invalidStr;
+ if (!attributes ||
+ !attributes->GetAttribute(nsGkAtoms::invalid, invalidStr)) {
+ return @"true";
+ }
+ return nsCocoaUtils::ToNSString(invalidStr);
+ }
+
+ // If the flag is not set, we return false.
+ return @"false";
+}
+
+- (NSNumber*)moxInsertionPointLineNumber {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ int32_t lineNumber = -1;
+ if (mGeckoAccessible->IsLocal()) {
+ if (HyperTextAccessible* textAcc =
+ mGeckoAccessible->AsLocal()->AsHyperText()) {
+ lineNumber = textAcc->CaretLineNumber() - 1;
+ }
+ } else {
+ lineNumber = mGeckoAccessible->AsRemote()->CaretLineNumber() - 1;
+ }
+
+ return (lineNumber >= 0) ? [NSNumber numberWithInt:lineNumber] : nil;
+}
+
+- (NSString*)moxRole {
+ if ([self stateWithMask:states::MULTI_LINE]) {
+ return NSAccessibilityTextAreaRole;
+ }
+
+ return [super moxRole];
+}
+
+- (NSString*)moxSubrole {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ if (mRole == roles::PASSWORD_TEXT) {
+ return NSAccessibilitySecureTextFieldSubrole;
+ }
+
+ if (mRole == roles::ENTRY) {
+ LocalAccessible* acc = mGeckoAccessible->AsLocal();
+ RemoteAccessible* proxy = mGeckoAccessible->AsRemote();
+ if ((acc && acc->IsSearchbox()) || (proxy && proxy->IsSearchbox())) {
+ return @"AXSearchField";
+ }
+ }
+
+ return nil;
+}
+
+- (NSNumber*)moxNumberOfCharacters {
+ return @([self textLength]);
+}
+
+- (NSString*)moxSelectedText {
+ GeckoTextMarkerRange selection = [self selection];
+ if (!selection.IsValid()) {
+ return nil;
+ }
+
+ return selection.Text();
+}
+
+- (NSValue*)moxSelectedTextRange {
+ GeckoTextMarkerRange selection = [self selection];
+ if (!selection.IsValid()) {
+ return nil;
+ }
+
+ GeckoTextMarkerRange fromStartToSelection(
+ GeckoTextMarker(mGeckoAccessible, 0), selection.mStart);
+
+ return [NSValue valueWithRange:NSMakeRange(fromStartToSelection.Length(),
+ selection.Length())];
+}
+
+- (NSValue*)moxVisibleCharacterRange {
+ // XXX this won't work with Textarea and such as we actually don't give
+ // the visible character range.
+ return [NSValue valueWithRange:NSMakeRange(0, [self textLength])];
+}
+
+- (BOOL)moxBlockSelector:(SEL)selector {
+ if (selector == @selector(moxSetValue:) && [self isReadOnly]) {
+ return YES;
+ }
+
+ return [super moxBlockSelector:selector];
+}
+
+- (void)moxSetValue:(id)value {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ nsString text;
+ nsCocoaUtils::GetStringForNSString(value, text);
+ if (mGeckoAccessible->IsLocal()) {
+ if (HyperTextAccessible* textAcc =
+ mGeckoAccessible->AsLocal()->AsHyperText()) {
+ textAcc->ReplaceText(text);
+ }
+ } else {
+ mGeckoAccessible->AsRemote()->ReplaceText(text);
+ }
+}
+
+- (void)moxSetSelectedText:(NSString*)selectedText {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ NSString* stringValue = ToNSString(selectedText);
+ if (!stringValue) {
+ return;
+ }
+
+ int32_t start = 0, end = 0;
+ nsString text;
+ if (mGeckoAccessible->IsLocal()) {
+ if (HyperTextAccessible* textAcc =
+ mGeckoAccessible->AsLocal()->AsHyperText()) {
+ textAcc->SelectionBoundsAt(0, &start, &end);
+ textAcc->DeleteText(start, end - start);
+ nsCocoaUtils::GetStringForNSString(stringValue, text);
+ textAcc->InsertText(text, start);
+ }
+ } else {
+ RemoteAccessible* proxy = mGeckoAccessible->AsRemote();
+ nsString data;
+ proxy->SelectionBoundsAt(0, data, &start, &end);
+ proxy->DeleteText(start, end - start);
+ nsCocoaUtils::GetStringForNSString(stringValue, text);
+ proxy->InsertText(text, start);
+ }
+}
+
+- (void)moxSetSelectedTextRange:(NSValue*)selectedTextRange {
+ GeckoTextMarkerRange markerRange =
+ [self textMarkerRangeFromRange:selectedTextRange];
+
+ if (markerRange.IsValid()) {
+ markerRange.Select();
+ }
+}
+
+- (void)moxSetVisibleCharacterRange:(NSValue*)visibleCharacterRange {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ NSRange range;
+ if (!ToNSRange(visibleCharacterRange, &range)) {
+ return;
+ }
+
+ if (mGeckoAccessible->IsLocal()) {
+ if (HyperTextAccessible* textAcc =
+ mGeckoAccessible->AsLocal()->AsHyperText()) {
+ textAcc->ScrollSubstringTo(range.location, range.location + range.length,
+ nsIAccessibleScrollType::SCROLL_TYPE_TOP_EDGE);
+ }
+ } else {
+ mGeckoAccessible->AsRemote()->ScrollSubstringTo(
+ range.location, range.location + range.length,
+ nsIAccessibleScrollType::SCROLL_TYPE_TOP_EDGE);
+ }
+}
+
+- (NSString*)moxStringForRange:(NSValue*)range {
+ GeckoTextMarkerRange markerRange = [self textMarkerRangeFromRange:range];
+
+ if (!markerRange.IsValid()) {
+ return nil;
+ }
+
+ return markerRange.Text();
+}
+
+- (NSAttributedString*)moxAttributedStringForRange:(NSValue*)range {
+ GeckoTextMarkerRange markerRange = [self textMarkerRangeFromRange:range];
+
+ if (!markerRange.IsValid()) {
+ return nil;
+ }
+
+ return markerRange.AttributedText();
+}
+
+- (NSValue*)moxRangeForLine:(NSNumber*)line {
+ // XXX: actually get the integer value for the line #
+ return [NSValue valueWithRange:NSMakeRange(0, [self textLength])];
+}
+
+- (NSNumber*)moxLineForIndex:(NSNumber*)index {
+ // XXX: actually return the line #
+ return @0;
+}
+
+- (NSValue*)moxBoundsForRange:(NSValue*)range {
+ GeckoTextMarkerRange markerRange = [self textMarkerRangeFromRange:range];
+
+ if (!markerRange.IsValid()) {
+ return nil;
+ }
+
+ return markerRange.Bounds();
+}
+
+#pragma mark - mozAccessible
+
+- (void)handleAccessibleTextChangeEvent:(NSString*)change
+ inserted:(BOOL)isInserted
+ inContainer:(Accessible*)container
+ at:(int32_t)start {
+ GeckoTextMarker startMarker(container, start);
+ NSDictionary* userInfo = @{
+ @"AXTextChangeElement" : self,
+ @"AXTextStateChangeType" : @(AXTextStateChangeTypeEdit),
+ @"AXTextChangeValues" : @[ @{
+ @"AXTextChangeValue" : (change ? change : @""),
+ @"AXTextChangeValueStartMarker" :
+ (__bridge id)startMarker.CreateAXTextMarker(),
+ @"AXTextEditType" : isInserted ? @(AXTextEditTypeTyping)
+ : @(AXTextEditTypeDelete)
+ } ]
+ };
+
+ mozAccessible* webArea = [self topWebArea];
+ [webArea moxPostNotification:NSAccessibilityValueChangedNotification
+ withUserInfo:userInfo];
+ [self moxPostNotification:NSAccessibilityValueChangedNotification
+ withUserInfo:userInfo];
+
+ [self moxPostNotification:NSAccessibilityValueChangedNotification];
+}
+
+- (void)handleAccessibleEvent:(uint32_t)eventType {
+ switch (eventType) {
+ default:
+ [super handleAccessibleEvent:eventType];
+ break;
+ }
+}
+
+#pragma mark -
+
+- (long)textLength {
+ return [[self text] length];
+}
+
+- (BOOL)isReadOnly {
+ return [self stateWithMask:states::EDITABLE] == 0;
+}
+
+- (NSString*)text {
+ // A password text field returns an empty value
+ if (mRole == roles::PASSWORD_TEXT) {
+ return @"";
+ }
+
+ id<MOXTextMarkerSupport> delegate = [self moxTextMarkerDelegate];
+ return [delegate
+ moxStringForTextMarkerRange:[delegate
+ moxTextMarkerRangeForUIElement:self]];
+}
+
+- (GeckoTextMarkerRange)selection {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ id<MOXTextMarkerSupport> delegate = [self moxTextMarkerDelegate];
+ GeckoTextMarkerRange selection =
+ [static_cast<MOXTextMarkerDelegate*>(delegate) selection];
+
+ if (!selection.IsValid() || !selection.Crop(mGeckoAccessible)) {
+ // The selection is not in this accessible. Return invalid range.
+ return GeckoTextMarkerRange();
+ }
+
+ return selection;
+}
+
+- (GeckoTextMarkerRange)textMarkerRangeFromRange:(NSValue*)range {
+ NSRange r = [range rangeValue];
+
+ GeckoTextMarker startMarker =
+ GeckoTextMarker::MarkerFromIndex(mGeckoAccessible, r.location);
+
+ GeckoTextMarker endMarker =
+ GeckoTextMarker::MarkerFromIndex(mGeckoAccessible, r.location + r.length);
+
+ return GeckoTextMarkerRange(startMarker, endMarker);
+}
+
+@end
+
+@implementation mozTextLeafAccessible
+
+- (BOOL)moxBlockSelector:(SEL)selector {
+ if (selector == @selector(moxChildren) || selector == @selector
+ (moxTitleUIElement)) {
+ return YES;
+ }
+
+ return [super moxBlockSelector:selector];
+}
+
+- (NSString*)moxValue {
+ NSString* val = [super moxTitle];
+ return [val length] ? val : nil;
+}
+
+- (NSString*)moxTitle {
+ return nil;
+}
+
+- (NSString*)moxLabel {
+ return nil;
+}
+
+- (BOOL)moxIgnoreWithParent:(mozAccessible*)parent {
+ // Don't render text nodes that are completely empty
+ // or those that should be ignored based on our
+ // standard ignore rules
+ return [self moxValue] == nil || [super moxIgnoreWithParent:parent];
+}
+
+- (NSString*)moxStringForRange:(NSValue*)range {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ NSRange r = [range rangeValue];
+ GeckoTextMarkerRange textMarkerRange(mGeckoAccessible);
+ textMarkerRange.mStart.mOffset += r.location;
+ textMarkerRange.mEnd.mOffset =
+ textMarkerRange.mStart.mOffset + r.location + r.length;
+
+ return textMarkerRange.Text();
+}
+
+- (NSAttributedString*)moxAttributedStringForRange:(NSValue*)range {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ NSRange r = [range rangeValue];
+ GeckoTextMarkerRange textMarkerRange(mGeckoAccessible);
+ textMarkerRange.mStart.mOffset += r.location;
+ textMarkerRange.mEnd.mOffset =
+ textMarkerRange.mStart.mOffset + r.location + r.length;
+
+ return textMarkerRange.AttributedText();
+}
+
+- (NSValue*)moxBoundsForRange:(NSValue*)range {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ NSRange r = [range rangeValue];
+ GeckoTextMarkerRange textMarkerRange(mGeckoAccessible);
+
+ textMarkerRange.mStart.mOffset += r.location;
+ textMarkerRange.mEnd.mOffset = textMarkerRange.mStart.mOffset + r.length;
+
+ return textMarkerRange.Bounds();
+}
+
+@end