diff options
Diffstat (limited to 'accessible/generic/Accessible.cpp')
-rw-r--r-- | accessible/generic/Accessible.cpp | 2856 |
1 files changed, 2856 insertions, 0 deletions
diff --git a/accessible/generic/Accessible.cpp b/accessible/generic/Accessible.cpp new file mode 100644 index 0000000000..7ff2f82836 --- /dev/null +++ b/accessible/generic/Accessible.cpp @@ -0,0 +1,2856 @@ +/* -*- 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/. */ + +#include "Accessible-inl.h" + +#include "nsIXBLAccessible.h" + +#include "EmbeddedObjCollector.h" +#include "AccGroupInfo.h" +#include "AccIterator.h" +#include "nsAccUtils.h" +#include "nsAccessibilityService.h" +#include "ApplicationAccessible.h" +#include "NotificationController.h" +#include "nsEventShell.h" +#include "nsTextEquivUtils.h" +#include "DocAccessibleChild.h" +#include "EventTree.h" +#include "Relation.h" +#include "Role.h" +#include "RootAccessible.h" +#include "States.h" +#include "StyleInfo.h" +#include "TableAccessible.h" +#include "TableCellAccessible.h" +#include "TreeWalker.h" + +#include "nsIDOMElement.h" +#include "nsIDOMNodeFilter.h" +#include "nsIDOMHTMLElement.h" +#include "nsIDOMKeyEvent.h" +#include "nsIDOMTreeWalker.h" +#include "nsIDOMXULButtonElement.h" +#include "nsIDOMXULDocument.h" +#include "nsIDOMXULElement.h" +#include "nsIDOMXULLabelElement.h" +#include "nsIDOMXULSelectCntrlEl.h" +#include "nsIDOMXULSelectCntrlItemEl.h" +#include "nsPIDOMWindow.h" + +#include "nsIDocument.h" +#include "nsIContent.h" +#include "nsIForm.h" +#include "nsIFormControl.h" + +#include "nsDeckFrame.h" +#include "nsLayoutUtils.h" +#include "nsIPresShell.h" +#include "nsIStringBundle.h" +#include "nsPresContext.h" +#include "nsIFrame.h" +#include "nsView.h" +#include "nsIDocShellTreeItem.h" +#include "nsIScrollableFrame.h" +#include "nsFocusManager.h" + +#include "nsXPIDLString.h" +#include "nsUnicharUtils.h" +#include "nsReadableUtils.h" +#include "prdtoa.h" +#include "nsIAtom.h" +#include "nsIURI.h" +#include "nsArrayUtils.h" +#include "nsIMutableArray.h" +#include "nsIObserverService.h" +#include "nsIServiceManager.h" +#include "nsWhitespaceTokenizer.h" +#include "nsAttrName.h" + +#ifdef DEBUG +#include "nsIDOMCharacterData.h" +#endif + +#include "mozilla/Assertions.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/EventStates.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/Unused.h" +#include "mozilla/Preferences.h" +#include "mozilla/dom/CanvasRenderingContext2D.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/HTMLCanvasElement.h" +#include "mozilla/dom/HTMLBodyElement.h" +#include "mozilla/dom/TreeWalker.h" + +using namespace mozilla; +using namespace mozilla::a11y; + + +//////////////////////////////////////////////////////////////////////////////// +// Accessible: nsISupports and cycle collection + +NS_IMPL_CYCLE_COLLECTION_CLASS(Accessible) +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Accessible) + tmp->Shutdown(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Accessible) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mContent, mDoc) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Accessible) + if (aIID.Equals(NS_GET_IID(Accessible))) + foundInterface = this; + else + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, Accessible) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(Accessible) +NS_IMPL_CYCLE_COLLECTING_RELEASE_WITH_DESTROY(Accessible, LastRelease()) + +Accessible::Accessible(nsIContent* aContent, DocAccessible* aDoc) : + mContent(aContent), mDoc(aDoc), + mParent(nullptr), mIndexInParent(-1), + mRoleMapEntryIndex(aria::NO_ROLE_MAP_ENTRY_INDEX), + mStateFlags(0), mContextFlags(0), mType(0), mGenericTypes(0), + mReorderEventTarget(false), mShowEventTarget(false), mHideEventTarget(false) +{ + mBits.groupInfo = nullptr; + mInt.mIndexOfEmbeddedChild = -1; +} + +Accessible::~Accessible() +{ + NS_ASSERTION(!mDoc, "LastRelease was never called!?!"); +} + +ENameValueFlag +Accessible::Name(nsString& aName) +{ + aName.Truncate(); + + if (!HasOwnContent()) + return eNameOK; + + ARIAName(aName); + if (!aName.IsEmpty()) + return eNameOK; + + nsCOMPtr<nsIXBLAccessible> xblAccessible(do_QueryInterface(mContent)); + if (xblAccessible) { + xblAccessible->GetAccessibleName(aName); + if (!aName.IsEmpty()) + return eNameOK; + } + + ENameValueFlag nameFlag = NativeName(aName); + if (!aName.IsEmpty()) + return nameFlag; + + // In the end get the name from tooltip. + if (mContent->IsHTMLElement()) { + if (mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::title, aName)) { + aName.CompressWhitespace(); + return eNameFromTooltip; + } + } else if (mContent->IsXULElement()) { + if (mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::tooltiptext, aName)) { + aName.CompressWhitespace(); + return eNameFromTooltip; + } + } else if (mContent->IsSVGElement()) { + // If user agents need to choose among multiple ‘desc’ or ‘title’ elements + // for processing, the user agent shall choose the first one. + for (nsIContent* childElm = mContent->GetFirstChild(); childElm; + childElm = childElm->GetNextSibling()) { + if (childElm->IsSVGElement(nsGkAtoms::desc)) { + nsTextEquivUtils::AppendTextEquivFromContent(this, childElm, &aName); + return eNameFromTooltip; + } + } + } + + if (nameFlag != eNoNameOnPurpose) + aName.SetIsVoid(true); + + return nameFlag; +} + +void +Accessible::Description(nsString& aDescription) +{ + // There are 4 conditions that make an accessible have no accDescription: + // 1. it's a text node; or + // 2. It has no DHTML describedby property + // 3. it doesn't have an accName; or + // 4. its title attribute already equals to its accName nsAutoString name; + + if (!HasOwnContent() || mContent->IsNodeOfType(nsINode::eTEXT)) + return; + + nsTextEquivUtils:: + GetTextEquivFromIDRefs(this, nsGkAtoms::aria_describedby, + aDescription); + + if (aDescription.IsEmpty()) { + NativeDescription(aDescription); + + if (aDescription.IsEmpty()) { + // Keep the Name() method logic. + if (mContent->IsHTMLElement()) { + mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::title, aDescription); + } else if (mContent->IsXULElement()) { + mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::tooltiptext, aDescription); + } else if (mContent->IsSVGElement()) { + for (nsIContent* childElm = mContent->GetFirstChild(); childElm; + childElm = childElm->GetNextSibling()) { + if (childElm->IsSVGElement(nsGkAtoms::desc)) { + nsTextEquivUtils::AppendTextEquivFromContent(this, childElm, + &aDescription); + break; + } + } + } + } + } + + if (!aDescription.IsEmpty()) { + aDescription.CompressWhitespace(); + nsAutoString name; + Name(name); + // Don't expose a description if it is the same as the name. + if (aDescription.Equals(name)) + aDescription.Truncate(); + } +} + +KeyBinding +Accessible::AccessKey() const +{ + if (!HasOwnContent()) + return KeyBinding(); + + uint32_t key = nsCoreUtils::GetAccessKeyFor(mContent); + if (!key && mContent->IsElement()) { + Accessible* label = nullptr; + + // Copy access key from label node. + if (mContent->IsHTMLElement()) { + // Unless it is labeled via an ancestor <label>, in which case that would + // be redundant. + HTMLLabelIterator iter(Document(), this, + HTMLLabelIterator::eSkipAncestorLabel); + label = iter.Next(); + + } else if (mContent->IsXULElement()) { + XULLabelIterator iter(Document(), mContent); + label = iter.Next(); + } + + if (label) + key = nsCoreUtils::GetAccessKeyFor(label->GetContent()); + } + + if (!key) + return KeyBinding(); + + // Get modifier mask. Use ui.key.generalAccessKey (unless it is -1). + switch (Preferences::GetInt("ui.key.generalAccessKey", -1)) { + case -1: + break; + case nsIDOMKeyEvent::DOM_VK_SHIFT: + return KeyBinding(key, KeyBinding::kShift); + case nsIDOMKeyEvent::DOM_VK_CONTROL: + return KeyBinding(key, KeyBinding::kControl); + case nsIDOMKeyEvent::DOM_VK_ALT: + return KeyBinding(key, KeyBinding::kAlt); + case nsIDOMKeyEvent::DOM_VK_META: + return KeyBinding(key, KeyBinding::kMeta); + default: + return KeyBinding(); + } + + // Determine the access modifier used in this context. + nsIDocument* document = mContent->GetUncomposedDoc(); + if (!document) + return KeyBinding(); + + nsCOMPtr<nsIDocShellTreeItem> treeItem(document->GetDocShell()); + if (!treeItem) + return KeyBinding(); + + nsresult rv = NS_ERROR_FAILURE; + int32_t modifierMask = 0; + switch (treeItem->ItemType()) { + case nsIDocShellTreeItem::typeChrome: + rv = Preferences::GetInt("ui.key.chromeAccess", &modifierMask); + break; + case nsIDocShellTreeItem::typeContent: + rv = Preferences::GetInt("ui.key.contentAccess", &modifierMask); + break; + } + + return NS_SUCCEEDED(rv) ? KeyBinding(key, modifierMask) : KeyBinding(); +} + +KeyBinding +Accessible::KeyboardShortcut() const +{ + return KeyBinding(); +} + +void +Accessible::TranslateString(const nsString& aKey, nsAString& aStringOut) +{ + nsCOMPtr<nsIStringBundleService> stringBundleService = + services::GetStringBundleService(); + if (!stringBundleService) + return; + + nsCOMPtr<nsIStringBundle> stringBundle; + stringBundleService->CreateBundle( + "chrome://global-platform/locale/accessible.properties", + getter_AddRefs(stringBundle)); + if (!stringBundle) + return; + + nsXPIDLString xsValue; + nsresult rv = stringBundle->GetStringFromName(aKey.get(), getter_Copies(xsValue)); + if (NS_SUCCEEDED(rv)) + aStringOut.Assign(xsValue); +} + +uint64_t +Accessible::VisibilityState() +{ + nsIFrame* frame = GetFrame(); + if (!frame) + return states::INVISIBLE; + + // Walk the parent frame chain to see if there's invisible parent or the frame + // is in background tab. + if (!frame->StyleVisibility()->IsVisible()) + return states::INVISIBLE; + + nsIFrame* curFrame = frame; + do { + nsView* view = curFrame->GetView(); + if (view && view->GetVisibility() == nsViewVisibility_kHide) + return states::INVISIBLE; + + if (nsLayoutUtils::IsPopup(curFrame)) + return 0; + + // Offscreen state for background tab content and invisible for not selected + // deck panel. + nsIFrame* parentFrame = curFrame->GetParent(); + nsDeckFrame* deckFrame = do_QueryFrame(parentFrame); + if (deckFrame && deckFrame->GetSelectedBox() != curFrame) { + if (deckFrame->GetContent()->IsXULElement(nsGkAtoms::tabpanels)) + return states::OFFSCREEN; + + NS_NOTREACHED("Children of not selected deck panel are not accessible."); + return states::INVISIBLE; + } + + // If contained by scrollable frame then check that at least 12 pixels + // around the object is visible, otherwise the object is offscreen. + nsIScrollableFrame* scrollableFrame = do_QueryFrame(parentFrame); + if (scrollableFrame) { + nsRect scrollPortRect = scrollableFrame->GetScrollPortRect(); + nsRect frameRect = nsLayoutUtils::TransformFrameRectToAncestor( + frame, frame->GetRectRelativeToSelf(), parentFrame); + if (!scrollPortRect.Contains(frameRect)) { + const nscoord kMinPixels = nsPresContext::CSSPixelsToAppUnits(12); + scrollPortRect.Deflate(kMinPixels, kMinPixels); + if (!scrollPortRect.Intersects(frameRect)) + return states::OFFSCREEN; + } + } + + if (!parentFrame) { + parentFrame = nsLayoutUtils::GetCrossDocParentFrame(curFrame); + if (parentFrame && !parentFrame->StyleVisibility()->IsVisible()) + return states::INVISIBLE; + } + + curFrame = parentFrame; + } while (curFrame); + + // Zero area rects can occur in the first frame of a multi-frame text flow, + // in which case the rendered text is not empty and the frame should not be + // marked invisible. + // XXX Can we just remove this check? Why do we need to mark empty + // text invisible? + if (frame->GetType() == nsGkAtoms::textFrame && + !(frame->GetStateBits() & NS_FRAME_OUT_OF_FLOW) && + frame->GetRect().IsEmpty()) { + nsIFrame::RenderedText text = frame->GetRenderedText(0, + UINT32_MAX, nsIFrame::TextOffsetType::OFFSETS_IN_CONTENT_TEXT, + nsIFrame::TrailingWhitespace::DONT_TRIM_TRAILING_WHITESPACE); + if (text.mString.IsEmpty()) { + return states::INVISIBLE; + } + } + + return 0; +} + +uint64_t +Accessible::NativeState() +{ + uint64_t state = 0; + + if (!IsInDocument()) + state |= states::STALE; + + if (HasOwnContent() && mContent->IsElement()) { + EventStates elementState = mContent->AsElement()->State(); + + if (elementState.HasState(NS_EVENT_STATE_INVALID)) + state |= states::INVALID; + + if (elementState.HasState(NS_EVENT_STATE_REQUIRED)) + state |= states::REQUIRED; + + state |= NativeInteractiveState(); + if (FocusMgr()->IsFocused(this)) + state |= states::FOCUSED; + } + + // Gather states::INVISIBLE and states::OFFSCREEN flags for this object. + state |= VisibilityState(); + + nsIFrame *frame = GetFrame(); + if (frame) { + if (frame->GetStateBits() & NS_FRAME_OUT_OF_FLOW) + state |= states::FLOATING; + + // XXX we should look at layout for non XUL box frames, but need to decide + // how that interacts with ARIA. + if (HasOwnContent() && mContent->IsXULElement() && frame->IsXULBoxFrame()) { + const nsStyleXUL* xulStyle = frame->StyleXUL(); + if (xulStyle && frame->IsXULBoxFrame()) { + // In XUL all boxes are either vertical or horizontal + if (xulStyle->mBoxOrient == StyleBoxOrient::Vertical) + state |= states::VERTICAL; + else + state |= states::HORIZONTAL; + } + } + } + + // Check if a XUL element has the popup attribute (an attached popup menu). + if (HasOwnContent() && mContent->IsXULElement() && + mContent->HasAttr(kNameSpaceID_None, nsGkAtoms::popup)) + state |= states::HASPOPUP; + + // Bypass the link states specialization for non links. + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + if (!roleMapEntry || roleMapEntry->roleRule == kUseNativeRole || + roleMapEntry->role == roles::LINK) + state |= NativeLinkState(); + + return state; +} + +uint64_t +Accessible::NativeInteractiveState() const +{ + if (!mContent->IsElement()) + return 0; + + if (NativelyUnavailable()) + return states::UNAVAILABLE; + + nsIFrame* frame = GetFrame(); + if (frame && frame->IsFocusable()) + return states::FOCUSABLE; + + return 0; +} + +uint64_t +Accessible::NativeLinkState() const +{ + return 0; +} + +bool +Accessible::NativelyUnavailable() const +{ + if (mContent->IsHTMLElement()) + return mContent->AsElement()->State().HasState(NS_EVENT_STATE_DISABLED); + + return mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, + nsGkAtoms::_true, eCaseMatters); +} + +Accessible* +Accessible::FocusedChild() +{ + Accessible* focus = FocusMgr()->FocusedAccessible(); + if (focus && (focus == this || focus->Parent() == this)) + return focus; + + return nullptr; +} + +Accessible* +Accessible::ChildAtPoint(int32_t aX, int32_t aY, + EWhichChildAtPoint aWhichChild) +{ + // If we can't find the point in a child, we will return the fallback answer: + // we return |this| if the point is within it, otherwise nullptr. + Accessible* fallbackAnswer = nullptr; + nsIntRect rect = Bounds(); + if (aX >= rect.x && aX < rect.x + rect.width && + aY >= rect.y && aY < rect.y + rect.height) + fallbackAnswer = this; + + if (nsAccUtils::MustPrune(this)) // Do not dig any further + return fallbackAnswer; + + // Search an accessible at the given point starting from accessible document + // because containing block (see CSS2) for out of flow element (for example, + // absolutely positioned element) may be different from its DOM parent and + // therefore accessible for containing block may be different from accessible + // for DOM parent but GetFrameForPoint() should be called for containing block + // to get an out of flow element. + DocAccessible* accDocument = Document(); + NS_ENSURE_TRUE(accDocument, nullptr); + + nsIFrame* rootFrame = accDocument->GetFrame(); + NS_ENSURE_TRUE(rootFrame, nullptr); + + nsIFrame* startFrame = rootFrame; + + // Check whether the point is at popup content. + nsIWidget* rootWidget = rootFrame->GetView()->GetNearestWidget(nullptr); + NS_ENSURE_TRUE(rootWidget, nullptr); + + LayoutDeviceIntRect rootRect = rootWidget->GetScreenBounds(); + + WidgetMouseEvent dummyEvent(true, eMouseMove, rootWidget, + WidgetMouseEvent::eSynthesized); + dummyEvent.mRefPoint = LayoutDeviceIntPoint(aX - rootRect.x, aY - rootRect.y); + + nsIFrame* popupFrame = nsLayoutUtils:: + GetPopupFrameForEventCoordinates(accDocument->PresContext()->GetRootPresContext(), + &dummyEvent); + if (popupFrame) { + // If 'this' accessible is not inside the popup then ignore the popup when + // searching an accessible at point. + DocAccessible* popupDoc = + GetAccService()->GetDocAccessible(popupFrame->GetContent()->OwnerDoc()); + Accessible* popupAcc = + popupDoc->GetAccessibleOrContainer(popupFrame->GetContent()); + Accessible* popupChild = this; + while (popupChild && !popupChild->IsDoc() && popupChild != popupAcc) + popupChild = popupChild->Parent(); + + if (popupChild == popupAcc) + startFrame = popupFrame; + } + + nsPresContext* presContext = startFrame->PresContext(); + nsRect screenRect = startFrame->GetScreenRectInAppUnits(); + nsPoint offset(presContext->DevPixelsToAppUnits(aX) - screenRect.x, + presContext->DevPixelsToAppUnits(aY) - screenRect.y); + nsIFrame* foundFrame = nsLayoutUtils::GetFrameForPoint(startFrame, offset); + + nsIContent* content = nullptr; + if (!foundFrame || !(content = foundFrame->GetContent())) + return fallbackAnswer; + + // Get accessible for the node with the point or the first accessible in + // the DOM parent chain. + DocAccessible* contentDocAcc = GetAccService()-> + GetDocAccessible(content->OwnerDoc()); + + // contentDocAcc in some circumstances can be nullptr. See bug 729861 + NS_ASSERTION(contentDocAcc, "could not get the document accessible"); + if (!contentDocAcc) + return fallbackAnswer; + + Accessible* accessible = contentDocAcc->GetAccessibleOrContainer(content); + if (!accessible) + return fallbackAnswer; + + // Hurray! We have an accessible for the frame that layout gave us. + // Since DOM node of obtained accessible may be out of flow then we should + // ensure obtained accessible is a child of this accessible. + Accessible* child = accessible; + while (child != this) { + Accessible* parent = child->Parent(); + if (!parent) { + // Reached the top of the hierarchy. These bounds were inside an + // accessible that is not a descendant of this one. + return fallbackAnswer; + } + + // If we landed on a legitimate child of |this|, and we want the direct + // child, return it here. + if (parent == this && aWhichChild == eDirectChild) + return child; + + child = parent; + } + + // Manually walk through accessible children and see if the are within this + // point. Skip offscreen or invisible accessibles. This takes care of cases + // where layout won't walk into things for us, such as image map areas and + // sub documents (XXX: subdocuments should be handled by methods of + // OuterDocAccessibles). + uint32_t childCount = accessible->ChildCount(); + for (uint32_t childIdx = 0; childIdx < childCount; childIdx++) { + Accessible* child = accessible->GetChildAt(childIdx); + + nsIntRect childRect = child->Bounds(); + if (aX >= childRect.x && aX < childRect.x + childRect.width && + aY >= childRect.y && aY < childRect.y + childRect.height && + (child->State() & states::INVISIBLE) == 0) { + + if (aWhichChild == eDeepestChild) + return child->ChildAtPoint(aX, aY, eDeepestChild); + + return child; + } + } + + return accessible; +} + +nsRect +Accessible::RelativeBounds(nsIFrame** aBoundingFrame) const +{ + nsIFrame* frame = GetFrame(); + if (frame && mContent) { + bool* hasHitRegionRect = static_cast<bool*>(mContent->GetProperty(nsGkAtoms::hitregion)); + + if (hasHitRegionRect && mContent->IsElement()) { + // This is for canvas fallback content + // Find a canvas frame the found hit region is relative to. + nsIFrame* canvasFrame = frame->GetParent(); + if (canvasFrame) { + canvasFrame = nsLayoutUtils::GetClosestFrameOfType(canvasFrame, nsGkAtoms::HTMLCanvasFrame); + } + + // make the canvas the bounding frame + if (canvasFrame) { + *aBoundingFrame = canvasFrame; + dom::HTMLCanvasElement *canvas = + dom::HTMLCanvasElement::FromContent(canvasFrame->GetContent()); + + // get the bounding rect of the hit region + nsRect bounds; + if (canvas && canvas->CountContexts() && + canvas->GetContextAtIndex(0)->GetHitRegionRect(mContent->AsElement(), bounds)) { + return bounds; + } + } + } + + *aBoundingFrame = nsLayoutUtils::GetContainingBlockForClientRect(frame); + return nsLayoutUtils:: + GetAllInFlowRectsUnion(frame, *aBoundingFrame, + nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS); + } + + return nsRect(); +} + +nsIntRect +Accessible::Bounds() const +{ + nsIFrame* boundingFrame = nullptr; + nsRect unionRectTwips = RelativeBounds(&boundingFrame); + if (!boundingFrame) + return nsIntRect(); + + nsIntRect screenRect; + nsPresContext* presContext = mDoc->PresContext(); + screenRect.x = presContext->AppUnitsToDevPixels(unionRectTwips.x); + screenRect.y = presContext->AppUnitsToDevPixels(unionRectTwips.y); + screenRect.width = presContext->AppUnitsToDevPixels(unionRectTwips.width); + screenRect.height = presContext->AppUnitsToDevPixels(unionRectTwips.height); + + // We have the union of the rectangle, now we need to put it in absolute + // screen coords. + nsIntRect orgRectPixels = boundingFrame->GetScreenRectInAppUnits(). + ToNearestPixels(presContext->AppUnitsPerDevPixel()); + screenRect.x += orgRectPixels.x; + screenRect.y += orgRectPixels.y; + + return screenRect; +} + +void +Accessible::SetSelected(bool aSelect) +{ + if (!HasOwnContent()) + return; + + Accessible* select = nsAccUtils::GetSelectableContainer(this, State()); + if (select) { + if (select->State() & states::MULTISELECTABLE) { + if (ARIARoleMap()) { + if (aSelect) { + mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::aria_selected, + NS_LITERAL_STRING("true"), true); + } else { + mContent->UnsetAttr(kNameSpaceID_None, nsGkAtoms::aria_selected, true); + } + } + return; + } + + if (aSelect) + TakeFocus(); + } +} + +void +Accessible::TakeSelection() +{ + Accessible* select = nsAccUtils::GetSelectableContainer(this, State()); + if (select) { + if (select->State() & states::MULTISELECTABLE) + select->UnselectAll(); + SetSelected(true); + } +} + +void +Accessible::TakeFocus() +{ + nsIFrame* frame = GetFrame(); + if (!frame) + return; + + nsIContent* focusContent = mContent; + + // If the accessible focus is managed by container widget then focus the + // widget and set the accessible as its current item. + if (!frame->IsFocusable()) { + Accessible* widget = ContainerWidget(); + if (widget && widget->AreItemsOperable()) { + nsIContent* widgetElm = widget->GetContent(); + nsIFrame* widgetFrame = widgetElm->GetPrimaryFrame(); + if (widgetFrame && widgetFrame->IsFocusable()) { + focusContent = widgetElm; + widget->SetCurrentItem(this); + } + } + } + + nsCOMPtr<nsIDOMElement> element(do_QueryInterface(focusContent)); + nsFocusManager* fm = nsFocusManager::GetFocusManager(); + if (fm) + fm->SetFocus(element, 0); +} + +void +Accessible::XULElmName(DocAccessible* aDocument, + nsIContent* aElm, nsString& aName) +{ + /** + * 3 main cases for XUL Controls to be labeled + * 1 - control contains label="foo" + * 2 - control has, as a child, a label element + * - label has either value="foo" or children + * 3 - non-child label contains control="controlID" + * - label has either value="foo" or children + * Once a label is found, the search is discontinued, so a control + * that has a label child as well as having a label external to + * the control that uses the control="controlID" syntax will use + * the child label for its Name. + */ + + // CASE #1 (via label attribute) -- great majority of the cases + nsCOMPtr<nsIDOMXULLabeledControlElement> labeledEl = do_QueryInterface(aElm); + if (labeledEl) { + labeledEl->GetLabel(aName); + } else { + nsCOMPtr<nsIDOMXULSelectControlItemElement> itemEl = do_QueryInterface(aElm); + if (itemEl) { + itemEl->GetLabel(aName); + } else { + nsCOMPtr<nsIDOMXULSelectControlElement> select = do_QueryInterface(aElm); + // Use label if this is not a select control element which + // uses label attribute to indicate which option is selected + if (!select) { + nsCOMPtr<nsIDOMXULElement> xulEl(do_QueryInterface(aElm)); + if (xulEl) + xulEl->GetAttribute(NS_LITERAL_STRING("label"), aName); + } + } + } + + // CASES #2 and #3 ------ label as a child or <label control="id" ... > </label> + if (aName.IsEmpty()) { + Accessible* labelAcc = nullptr; + XULLabelIterator iter(aDocument, aElm); + while ((labelAcc = iter.Next())) { + nsCOMPtr<nsIDOMXULLabelElement> xulLabel = + do_QueryInterface(labelAcc->GetContent()); + // Check if label's value attribute is used + if (xulLabel && NS_SUCCEEDED(xulLabel->GetValue(aName)) && aName.IsEmpty()) { + // If no value attribute, a non-empty label must contain + // children that define its text -- possibly using HTML + nsTextEquivUtils:: + AppendTextEquivFromContent(labelAcc, labelAcc->GetContent(), &aName); + } + } + } + + aName.CompressWhitespace(); + if (!aName.IsEmpty()) + return; + + // Can get text from title of <toolbaritem> if we're a child of a <toolbaritem> + nsIContent *bindingParent = aElm->GetBindingParent(); + nsIContent* parent = + bindingParent? bindingParent->GetParent() : aElm->GetParent(); + nsAutoString ancestorTitle; + while (parent) { + if (parent->IsXULElement(nsGkAtoms::toolbaritem) && + parent->GetAttr(kNameSpaceID_None, nsGkAtoms::title, ancestorTitle)) { + // Before returning this, check if the element itself has a tooltip: + if (aElm->GetAttr(kNameSpaceID_None, nsGkAtoms::tooltiptext, aName)) { + aName.CompressWhitespace(); + return; + } + + aName.Assign(ancestorTitle); + aName.CompressWhitespace(); + return; + } + parent = parent->GetParent(); + } +} + +nsresult +Accessible::HandleAccEvent(AccEvent* aEvent) +{ + NS_ENSURE_ARG_POINTER(aEvent); + + if (IPCAccessibilityActive() && Document()) { + DocAccessibleChild* ipcDoc = mDoc->IPCDoc(); + MOZ_ASSERT(ipcDoc); + if (ipcDoc) { + uint64_t id = aEvent->GetAccessible()->IsDoc() ? 0 : + reinterpret_cast<uintptr_t>(aEvent->GetAccessible()); + + switch(aEvent->GetEventType()) { + case nsIAccessibleEvent::EVENT_SHOW: + ipcDoc->ShowEvent(downcast_accEvent(aEvent)); + break; + + case nsIAccessibleEvent::EVENT_HIDE: + ipcDoc->SendHideEvent(id, aEvent->IsFromUserInput()); + break; + + case nsIAccessibleEvent::EVENT_REORDER: + // reorder events on the application acc aren't necessary to tell the parent + // about new top level documents. + if (!aEvent->GetAccessible()->IsApplication()) + ipcDoc->SendEvent(id, aEvent->GetEventType()); + break; + case nsIAccessibleEvent::EVENT_STATE_CHANGE: { + AccStateChangeEvent* event = downcast_accEvent(aEvent); + ipcDoc->SendStateChangeEvent(id, event->GetState(), + event->IsStateEnabled()); + break; + } + case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: { + AccCaretMoveEvent* event = downcast_accEvent(aEvent); + ipcDoc->SendCaretMoveEvent(id, event->GetCaretOffset()); + break; + } + case nsIAccessibleEvent::EVENT_TEXT_INSERTED: + case nsIAccessibleEvent::EVENT_TEXT_REMOVED: { + AccTextChangeEvent* event = downcast_accEvent(aEvent); + ipcDoc->SendTextChangeEvent(id, event->ModifiedText(), + event->GetStartOffset(), + event->GetLength(), + event->IsTextInserted(), + event->IsFromUserInput()); + break; + } + case nsIAccessibleEvent::EVENT_SELECTION: + case nsIAccessibleEvent::EVENT_SELECTION_ADD: + case nsIAccessibleEvent::EVENT_SELECTION_REMOVE: { + AccSelChangeEvent* selEvent = downcast_accEvent(aEvent); + uint64_t widgetID = selEvent->Widget()->IsDoc() ? 0 : + reinterpret_cast<uintptr_t>(selEvent->Widget()); + ipcDoc->SendSelectionEvent(id, widgetID, aEvent->GetEventType()); + break; + } + default: + ipcDoc->SendEvent(id, aEvent->GetEventType()); + } + } + } + + if (nsCoreUtils::AccEventObserversExist()) { + nsCoreUtils::DispatchAccEvent(MakeXPCEvent(aEvent)); + } + + return NS_OK; +} + +already_AddRefed<nsIPersistentProperties> +Accessible::Attributes() +{ + nsCOMPtr<nsIPersistentProperties> attributes = NativeAttributes(); + if (!HasOwnContent() || !mContent->IsElement()) + return attributes.forget(); + + // 'xml-roles' attribute for landmark. + nsIAtom* landmark = LandmarkRole(); + if (landmark) { + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::xmlroles, landmark); + + } else { + // 'xml-roles' attribute coming from ARIA. + nsAutoString xmlRoles; + if (mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::role, xmlRoles)) + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::xmlroles, xmlRoles); + } + + // Expose object attributes from ARIA attributes. + nsAutoString unused; + aria::AttrIterator attribIter(mContent); + nsAutoString name, value; + while(attribIter.Next(name, value)) + attributes->SetStringProperty(NS_ConvertUTF16toUTF8(name), value, unused); + + if (IsARIAHidden()) { + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::hidden, + NS_LITERAL_STRING("true")); + } + + // If there is no aria-live attribute then expose default value of 'live' + // object attribute used for ARIA role of this accessible. + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + if (roleMapEntry) { + if (roleMapEntry->Is(nsGkAtoms::searchbox)) { + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::textInputType, + NS_LITERAL_STRING("search")); + } + + nsAutoString live; + nsAccUtils::GetAccAttr(attributes, nsGkAtoms::live, live); + if (live.IsEmpty()) { + if (nsAccUtils::GetLiveAttrValue(roleMapEntry->liveAttRule, live)) + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::live, live); + } + } + + return attributes.forget(); +} + +already_AddRefed<nsIPersistentProperties> +Accessible::NativeAttributes() +{ + nsCOMPtr<nsIPersistentProperties> attributes = + do_CreateInstance(NS_PERSISTENTPROPERTIES_CONTRACTID); + + nsAutoString unused; + + // We support values, so expose the string value as well, via the valuetext + // object attribute. We test for the value interface because we don't want + // to expose traditional Value() information such as URL's on links and + // documents, or text in an input. + if (HasNumericValue()) { + nsAutoString valuetext; + Value(valuetext); + attributes->SetStringProperty(NS_LITERAL_CSTRING("valuetext"), valuetext, + unused); + } + + // Expose checkable object attribute if the accessible has checkable state + if (State() & states::CHECKABLE) { + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::checkable, + NS_LITERAL_STRING("true")); + } + + // Expose 'explicit-name' attribute. + nsAutoString name; + if (Name(name) != eNameFromSubtree && !name.IsVoid()) { + attributes->SetStringProperty(NS_LITERAL_CSTRING("explicit-name"), + NS_LITERAL_STRING("true"), unused); + } + + // Group attributes (level/setsize/posinset) + GroupPos groupPos = GroupPosition(); + nsAccUtils::SetAccGroupAttrs(attributes, groupPos.level, + groupPos.setSize, groupPos.posInSet); + + // If the accessible doesn't have own content (such as list item bullet or + // xul tree item) then don't calculate content based attributes. + if (!HasOwnContent()) + return attributes.forget(); + + nsEventShell::GetEventAttributes(GetNode(), attributes); + + // Get container-foo computed live region properties based on the closest + // container with the live region attribute. Inner nodes override outer nodes + // within the same document. The inner nodes can be used to override live + // region behavior on more general outer nodes. However, nodes in outer + // documents override nodes in inner documents: outer doc author may want to + // override properties on a widget they used in an iframe. + nsIContent* startContent = mContent; + while (startContent) { + nsIDocument* doc = startContent->GetComposedDoc(); + if (!doc) + break; + + nsAccUtils::SetLiveContainerAttributes(attributes, startContent, + doc->GetRootElement()); + + // Allow ARIA live region markup from outer documents to override + nsCOMPtr<nsIDocShellTreeItem> docShellTreeItem = doc->GetDocShell(); + if (!docShellTreeItem) + break; + + nsCOMPtr<nsIDocShellTreeItem> sameTypeParent; + docShellTreeItem->GetSameTypeParent(getter_AddRefs(sameTypeParent)); + if (!sameTypeParent || sameTypeParent == docShellTreeItem) + break; + + nsIDocument* parentDoc = doc->GetParentDocument(); + if (!parentDoc) + break; + + startContent = parentDoc->FindContentForSubDocument(doc); + } + + if (!mContent->IsElement()) + return attributes.forget(); + + nsAutoString id; + if (nsCoreUtils::GetID(mContent, id)) + attributes->SetStringProperty(NS_LITERAL_CSTRING("id"), id, unused); + + // Expose class because it may have useful microformat information. + nsAutoString _class; + if (mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::_class, _class)) + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::_class, _class); + + // Expose tag. + nsAutoString tagName; + mContent->NodeInfo()->GetName(tagName); + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::tag, tagName); + + // Expose draggable object attribute. + nsCOMPtr<nsIDOMHTMLElement> htmlElement = do_QueryInterface(mContent); + if (htmlElement) { + bool draggable = false; + htmlElement->GetDraggable(&draggable); + if (draggable) { + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::draggable, + NS_LITERAL_STRING("true")); + } + } + + // Don't calculate CSS-based object attributes when no frame (i.e. + // the accessible is unattached from the tree). + if (!mContent->GetPrimaryFrame()) + return attributes.forget(); + + // CSS style based object attributes. + nsAutoString value; + StyleInfo styleInfo(mContent->AsElement(), mDoc->PresShell()); + + // Expose 'display' attribute. + styleInfo.Display(value); + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::display, value); + + // Expose 'text-align' attribute. + styleInfo.TextAlign(value); + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::textAlign, value); + + // Expose 'text-indent' attribute. + styleInfo.TextIndent(value); + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::textIndent, value); + + // Expose 'margin-left' attribute. + styleInfo.MarginLeft(value); + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::marginLeft, value); + + // Expose 'margin-right' attribute. + styleInfo.MarginRight(value); + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::marginRight, value); + + // Expose 'margin-top' attribute. + styleInfo.MarginTop(value); + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::marginTop, value); + + // Expose 'margin-bottom' attribute. + styleInfo.MarginBottom(value); + nsAccUtils::SetAccAttr(attributes, nsGkAtoms::marginBottom, value); + + return attributes.forget(); +} + +GroupPos +Accessible::GroupPosition() +{ + GroupPos groupPos; + if (!HasOwnContent()) + return groupPos; + + // Get group position from ARIA attributes. + nsCoreUtils::GetUIntAttr(mContent, nsGkAtoms::aria_level, &groupPos.level); + nsCoreUtils::GetUIntAttr(mContent, nsGkAtoms::aria_setsize, &groupPos.setSize); + nsCoreUtils::GetUIntAttr(mContent, nsGkAtoms::aria_posinset, &groupPos.posInSet); + + // If ARIA is missed and the accessible is visible then calculate group + // position from hierarchy. + if (State() & states::INVISIBLE) + return groupPos; + + // Calculate group level if ARIA is missed. + if (groupPos.level == 0) { + int32_t level = GetLevelInternal(); + if (level != 0) + groupPos.level = level; + } + + // Calculate position in group and group size if ARIA is missed. + if (groupPos.posInSet == 0 || groupPos.setSize == 0) { + int32_t posInSet = 0, setSize = 0; + GetPositionAndSizeInternal(&posInSet, &setSize); + if (posInSet != 0 && setSize != 0) { + if (groupPos.posInSet == 0) + groupPos.posInSet = posInSet; + + if (groupPos.setSize == 0) + groupPos.setSize = setSize; + } + } + + return groupPos; +} + +uint64_t +Accessible::State() +{ + if (IsDefunct()) + return states::DEFUNCT; + + uint64_t state = NativeState(); + // Apply ARIA states to be sure accessible states will be overridden. + ApplyARIAState(&state); + + // If this is an ARIA item of the selectable widget and if it's focused and + // not marked unselected explicitly (i.e. aria-selected="false") then expose + // it as selected to make ARIA widget authors life easier. + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + if (roleMapEntry && !(state & states::SELECTED) && + !mContent->AttrValueIs(kNameSpaceID_None, + nsGkAtoms::aria_selected, + nsGkAtoms::_false, eCaseMatters)) { + // Special case for tabs: focused tab or focus inside related tab panel + // implies selected state. + if (roleMapEntry->role == roles::PAGETAB) { + if (state & states::FOCUSED) { + state |= states::SELECTED; + } else { + // If focus is in a child of the tab panel surely the tab is selected! + Relation rel = RelationByType(RelationType::LABEL_FOR); + Accessible* relTarget = nullptr; + while ((relTarget = rel.Next())) { + if (relTarget->Role() == roles::PROPERTYPAGE && + FocusMgr()->IsFocusWithin(relTarget)) + state |= states::SELECTED; + } + } + } else if (state & states::FOCUSED) { + Accessible* container = nsAccUtils::GetSelectableContainer(this, state); + if (container && + !nsAccUtils::HasDefinedARIAToken(container->GetContent(), + nsGkAtoms::aria_multiselectable)) { + state |= states::SELECTED; + } + } + } + + const uint32_t kExpandCollapseStates = states::COLLAPSED | states::EXPANDED; + if ((state & kExpandCollapseStates) == kExpandCollapseStates) { + // Cannot be both expanded and collapsed -- this happens in ARIA expanded + // combobox because of limitation of ARIAMap. + // XXX: Perhaps we will be able to make this less hacky if we support + // extended states in ARIAMap, e.g. derive COLLAPSED from + // EXPANDABLE && !EXPANDED. + state &= ~states::COLLAPSED; + } + + if (!(state & states::UNAVAILABLE)) { + state |= states::ENABLED | states::SENSITIVE; + + // If the object is a current item of container widget then mark it as + // ACTIVE. This allows screen reader virtual buffer modes to know which + // descendant is the current one that would get focus if the user navigates + // to the container widget. + Accessible* widget = ContainerWidget(); + if (widget && widget->CurrentItem() == this) + state |= states::ACTIVE; + } + + if ((state & states::COLLAPSED) || (state & states::EXPANDED)) + state |= states::EXPANDABLE; + + // For some reasons DOM node may have not a frame. We tract such accessibles + // as invisible. + nsIFrame *frame = GetFrame(); + if (!frame) + return state; + + if (frame->StyleEffects()->mOpacity == 1.0f && + !(state & states::INVISIBLE)) { + state |= states::OPAQUE1; + } + + return state; +} + +void +Accessible::ApplyARIAState(uint64_t* aState) const +{ + if (!mContent->IsElement()) + return; + + dom::Element* element = mContent->AsElement(); + + // Test for universal states first + *aState |= aria::UniversalStatesFor(element); + + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + if (roleMapEntry) { + + // We only force the readonly bit off if we have a real mapping for the aria + // role. This preserves the ability for screen readers to use readonly + // (primarily on the document) as the hint for creating a virtual buffer. + if (roleMapEntry->role != roles::NOTHING) + *aState &= ~states::READONLY; + + if (mContent->HasID()) { + // If has a role & ID and aria-activedescendant on the container, assume + // focusable. + const Accessible* ancestor = this; + while ((ancestor = ancestor->Parent()) && !ancestor->IsDoc()) { + dom::Element* el = ancestor->Elm(); + if (el && + el->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_activedescendant)) { + *aState |= states::FOCUSABLE; + break; + } + } + } + } + + if (*aState & states::FOCUSABLE) { + // Propogate aria-disabled from ancestors down to any focusable descendant. + const Accessible* ancestor = this; + while ((ancestor = ancestor->Parent()) && !ancestor->IsDoc()) { + dom::Element* el = ancestor->Elm(); + if (el && el->AttrValueIs(kNameSpaceID_None, nsGkAtoms::aria_disabled, + nsGkAtoms::_true, eCaseMatters)) { + *aState |= states::UNAVAILABLE; + break; + } + } + } + + // special case: A native button element whose role got transformed by ARIA to a toggle button + // Also applies to togglable button menus, like in the Dev Tools Web Console. + if (IsButton() || IsMenuButton()) + aria::MapToState(aria::eARIAPressed, element, aState); + + if (!roleMapEntry) + return; + + *aState |= roleMapEntry->state; + + if (aria::MapToState(roleMapEntry->attributeMap1, element, aState) && + aria::MapToState(roleMapEntry->attributeMap2, element, aState) && + aria::MapToState(roleMapEntry->attributeMap3, element, aState)) + aria::MapToState(roleMapEntry->attributeMap4, element, aState); + + // ARIA gridcell inherits editable/readonly states from the grid until it's + // overridden. + if ((roleMapEntry->Is(nsGkAtoms::gridcell) || + roleMapEntry->Is(nsGkAtoms::columnheader) || + roleMapEntry->Is(nsGkAtoms::rowheader)) && + !(*aState & (states::READONLY | states::EDITABLE))) { + const TableCellAccessible* cell = AsTableCell(); + if (cell) { + TableAccessible* table = cell->Table(); + if (table) { + Accessible* grid = table->AsAccessible(); + uint64_t gridState = 0; + grid->ApplyARIAState(&gridState); + *aState |= (gridState & (states::READONLY | states::EDITABLE)); + } + } + } +} + +void +Accessible::Value(nsString& aValue) +{ + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + if (!roleMapEntry) + return; + + if (roleMapEntry->valueRule != eNoValue) { + // aria-valuenow is a number, and aria-valuetext is the optional text + // equivalent. For the string value, we will try the optional text + // equivalent first. + if (!mContent->GetAttr(kNameSpaceID_None, + nsGkAtoms::aria_valuetext, aValue)) { + mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::aria_valuenow, + aValue); + } + return; + } + + // Value of textbox is a textified subtree. + if (roleMapEntry->Is(nsGkAtoms::textbox)) { + nsTextEquivUtils::GetTextEquivFromSubtree(this, aValue); + return; + } + + // Value of combobox is a text of current or selected item. + if (roleMapEntry->Is(nsGkAtoms::combobox)) { + Accessible* option = CurrentItem(); + if (!option) { + uint32_t childCount = ChildCount(); + for (uint32_t idx = 0; idx < childCount; idx++) { + Accessible* child = mChildren.ElementAt(idx); + if (child->IsListControl()) { + option = child->GetSelectedItem(0); + break; + } + } + } + + if (option) + nsTextEquivUtils::GetTextEquivFromSubtree(option, aValue); + } +} + +double +Accessible::MaxValue() const +{ + return AttrNumericValue(nsGkAtoms::aria_valuemax); +} + +double +Accessible::MinValue() const +{ + return AttrNumericValue(nsGkAtoms::aria_valuemin); +} + +double +Accessible::Step() const +{ + return UnspecifiedNaN<double>(); // no mimimum increment (step) in ARIA. +} + +double +Accessible::CurValue() const +{ + return AttrNumericValue(nsGkAtoms::aria_valuenow); +} + +bool +Accessible::SetCurValue(double aValue) +{ + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + if (!roleMapEntry || roleMapEntry->valueRule == eNoValue) + return false; + + const uint32_t kValueCannotChange = states::READONLY | states::UNAVAILABLE; + if (State() & kValueCannotChange) + return false; + + double checkValue = MinValue(); + if (!IsNaN(checkValue) && aValue < checkValue) + return false; + + checkValue = MaxValue(); + if (!IsNaN(checkValue) && aValue > checkValue) + return false; + + nsAutoString strValue; + strValue.AppendFloat(aValue); + + return NS_SUCCEEDED( + mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::aria_valuenow, strValue, true)); +} + +role +Accessible::ARIATransformRole(role aRole) +{ + // XXX: these unfortunate exceptions don't fit into the ARIA table. This is + // where the accessible role depends on both the role and ARIA state. + if (aRole == roles::PUSHBUTTON) { + if (nsAccUtils::HasDefinedARIAToken(mContent, nsGkAtoms::aria_pressed)) { + // For simplicity, any existing pressed attribute except "" or "undefined" + // indicates a toggle. + return roles::TOGGLE_BUTTON; + } + + if (mContent->AttrValueIs(kNameSpaceID_None, + nsGkAtoms::aria_haspopup, + nsGkAtoms::_true, + eCaseMatters)) { + // For button with aria-haspopup="true". + return roles::BUTTONMENU; + } + + } else if (aRole == roles::LISTBOX) { + // A listbox inside of a combobox needs a special role because of ATK + // mapping to menu. + if (mParent && mParent->Role() == roles::COMBOBOX) { + return roles::COMBOBOX_LIST; + } else { + // Listbox is owned by a combobox + Relation rel = RelationByType(RelationType::NODE_CHILD_OF); + Accessible* targetAcc = nullptr; + while ((targetAcc = rel.Next())) + if (targetAcc->Role() == roles::COMBOBOX) + return roles::COMBOBOX_LIST; + } + + } else if (aRole == roles::OPTION) { + if (mParent && mParent->Role() == roles::COMBOBOX_LIST) + return roles::COMBOBOX_OPTION; + + } else if (aRole == roles::MENUITEM) { + // Menuitem has a submenu. + if (mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::aria_haspopup, + nsGkAtoms::_true, eCaseMatters)) { + return roles::PARENT_MENUITEM; + } + } + + return aRole; +} + +nsIAtom* +Accessible::LandmarkRole() const +{ + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + return roleMapEntry && roleMapEntry->IsOfType(eLandmark) ? + *(roleMapEntry->roleAtom) : nullptr; +} + +role +Accessible::NativeRole() +{ + return roles::NOTHING; +} + +uint8_t +Accessible::ActionCount() +{ + return GetActionRule() == eNoAction ? 0 : 1; +} + +void +Accessible::ActionNameAt(uint8_t aIndex, nsAString& aName) +{ + aName.Truncate(); + + if (aIndex != 0) + return; + + uint32_t actionRule = GetActionRule(); + + switch (actionRule) { + case eActivateAction: + aName.AssignLiteral("activate"); + return; + + case eClickAction: + aName.AssignLiteral("click"); + return; + + case ePressAction: + aName.AssignLiteral("press"); + return; + + case eCheckUncheckAction: + { + uint64_t state = State(); + if (state & states::CHECKED) + aName.AssignLiteral("uncheck"); + else if (state & states::MIXED) + aName.AssignLiteral("cycle"); + else + aName.AssignLiteral("check"); + return; + } + + case eJumpAction: + aName.AssignLiteral("jump"); + return; + + case eOpenCloseAction: + if (State() & states::COLLAPSED) + aName.AssignLiteral("open"); + else + aName.AssignLiteral("close"); + return; + + case eSelectAction: + aName.AssignLiteral("select"); + return; + + case eSwitchAction: + aName.AssignLiteral("switch"); + return; + + case eSortAction: + aName.AssignLiteral("sort"); + return; + + case eExpandAction: + if (State() & states::COLLAPSED) + aName.AssignLiteral("expand"); + else + aName.AssignLiteral("collapse"); + return; + } +} + +bool +Accessible::DoAction(uint8_t aIndex) +{ + if (aIndex != 0) + return false; + + if (GetActionRule() != eNoAction) { + DoCommand(); + return true; + } + + return false; +} + +nsIContent* +Accessible::GetAtomicRegion() const +{ + nsIContent *loopContent = mContent; + nsAutoString atomic; + while (loopContent && !loopContent->GetAttr(kNameSpaceID_None, nsGkAtoms::aria_atomic, atomic)) + loopContent = loopContent->GetParent(); + + return atomic.EqualsLiteral("true") ? loopContent : nullptr; +} + +Relation +Accessible::RelationByType(RelationType aType) +{ + if (!HasOwnContent()) + return Relation(); + + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + + // Relationships are defined on the same content node that the role would be + // defined on. + switch (aType) { + case RelationType::LABELLED_BY: { + Relation rel(new IDRefsIterator(mDoc, mContent, + nsGkAtoms::aria_labelledby)); + if (mContent->IsHTMLElement()) { + rel.AppendIter(new HTMLLabelIterator(Document(), this)); + } else if (mContent->IsXULElement()) { + rel.AppendIter(new XULLabelIterator(Document(), mContent)); + } + + return rel; + } + + case RelationType::LABEL_FOR: { + Relation rel(new RelatedAccIterator(Document(), mContent, + nsGkAtoms::aria_labelledby)); + if (mContent->IsXULElement(nsGkAtoms::label)) + rel.AppendIter(new IDRefsIterator(mDoc, mContent, nsGkAtoms::control)); + + return rel; + } + + case RelationType::DESCRIBED_BY: { + Relation rel(new IDRefsIterator(mDoc, mContent, + nsGkAtoms::aria_describedby)); + if (mContent->IsXULElement()) + rel.AppendIter(new XULDescriptionIterator(Document(), mContent)); + + return rel; + } + + case RelationType::DESCRIPTION_FOR: { + Relation rel(new RelatedAccIterator(Document(), mContent, + nsGkAtoms::aria_describedby)); + + // This affectively adds an optional control attribute to xul:description, + // which only affects accessibility, by allowing the description to be + // tied to a control. + if (mContent->IsXULElement(nsGkAtoms::description)) + rel.AppendIter(new IDRefsIterator(mDoc, mContent, + nsGkAtoms::control)); + + return rel; + } + + case RelationType::NODE_CHILD_OF: { + Relation rel; + // This is an ARIA tree or treegrid that doesn't use owns, so we need to + // get the parent the hard way. + if (roleMapEntry && (roleMapEntry->role == roles::OUTLINEITEM || + roleMapEntry->role == roles::LISTITEM || + roleMapEntry->role == roles::ROW)) { + rel.AppendTarget(GetGroupInfo()->ConceptualParent()); + } + + // If accessible is in its own Window, or is the root of a document, + // then we should provide NODE_CHILD_OF relation so that MSAA clients + // can easily get to true parent instead of getting to oleacc's + // ROLE_WINDOW accessible which will prevent us from going up further + // (because it is system generated and has no idea about the hierarchy + // above it). + nsIFrame *frame = GetFrame(); + if (frame) { + nsView *view = frame->GetView(); + if (view) { + nsIScrollableFrame *scrollFrame = do_QueryFrame(frame); + if (scrollFrame || view->GetWidget() || !frame->GetParent()) + rel.AppendTarget(Parent()); + } + } + + return rel; + } + + case RelationType::NODE_PARENT_OF: { + // ARIA tree or treegrid can do the hierarchy by @aria-level, ARIA trees + // also can be organized by groups. + if (roleMapEntry && + (roleMapEntry->role == roles::OUTLINEITEM || + roleMapEntry->role == roles::LISTITEM || + roleMapEntry->role == roles::ROW || + roleMapEntry->role == roles::OUTLINE || + roleMapEntry->role == roles::LIST || + roleMapEntry->role == roles::TREE_TABLE)) { + return Relation(new ItemIterator(this)); + } + + return Relation(); + } + + case RelationType::CONTROLLED_BY: + return Relation(new RelatedAccIterator(Document(), mContent, + nsGkAtoms::aria_controls)); + + case RelationType::CONTROLLER_FOR: { + Relation rel(new IDRefsIterator(mDoc, mContent, + nsGkAtoms::aria_controls)); + rel.AppendIter(new HTMLOutputIterator(Document(), mContent)); + return rel; + } + + case RelationType::FLOWS_TO: + return Relation(new IDRefsIterator(mDoc, mContent, + nsGkAtoms::aria_flowto)); + + case RelationType::FLOWS_FROM: + return Relation(new RelatedAccIterator(Document(), mContent, + nsGkAtoms::aria_flowto)); + + case RelationType::MEMBER_OF: + return Relation(mDoc, GetAtomicRegion()); + + case RelationType::SUBWINDOW_OF: + case RelationType::EMBEDS: + case RelationType::EMBEDDED_BY: + case RelationType::POPUP_FOR: + case RelationType::PARENT_WINDOW_OF: + return Relation(); + + case RelationType::DEFAULT_BUTTON: { + if (mContent->IsHTMLElement()) { + // HTML form controls implements nsIFormControl interface. + nsCOMPtr<nsIFormControl> control(do_QueryInterface(mContent)); + if (control) { + nsCOMPtr<nsIForm> form(do_QueryInterface(control->GetFormElement())); + if (form) { + nsCOMPtr<nsIContent> formContent = + do_QueryInterface(form->GetDefaultSubmitElement()); + return Relation(mDoc, formContent); + } + } + } else { + // In XUL, use first <button default="true" .../> in the document + nsCOMPtr<nsIDOMXULDocument> xulDoc = + do_QueryInterface(mContent->OwnerDoc()); + nsCOMPtr<nsIDOMXULButtonElement> buttonEl; + if (xulDoc) { + nsCOMPtr<nsIDOMNodeList> possibleDefaultButtons; + xulDoc->GetElementsByAttribute(NS_LITERAL_STRING("default"), + NS_LITERAL_STRING("true"), + getter_AddRefs(possibleDefaultButtons)); + if (possibleDefaultButtons) { + uint32_t length; + possibleDefaultButtons->GetLength(&length); + nsCOMPtr<nsIDOMNode> possibleButton; + // Check for button in list of default="true" elements + for (uint32_t count = 0; count < length && !buttonEl; count ++) { + possibleDefaultButtons->Item(count, getter_AddRefs(possibleButton)); + buttonEl = do_QueryInterface(possibleButton); + } + } + if (!buttonEl) { // Check for anonymous accept button in <dialog> + dom::Element* rootElm = mContent->OwnerDoc()->GetRootElement(); + if (rootElm) { + nsIContent* possibleButtonEl = rootElm->OwnerDoc()-> + GetAnonymousElementByAttribute(rootElm, nsGkAtoms::_default, + NS_LITERAL_STRING("true")); + buttonEl = do_QueryInterface(possibleButtonEl); + } + } + nsCOMPtr<nsIContent> relatedContent(do_QueryInterface(buttonEl)); + return Relation(mDoc, relatedContent); + } + } + return Relation(); + } + + case RelationType::CONTAINING_DOCUMENT: + return Relation(mDoc); + + case RelationType::CONTAINING_TAB_PANE: { + nsCOMPtr<nsIDocShell> docShell = + nsCoreUtils::GetDocShellFor(GetNode()); + if (docShell) { + // Walk up the parent chain without crossing the boundary at which item + // types change, preventing us from walking up out of tab content. + nsCOMPtr<nsIDocShellTreeItem> root; + docShell->GetSameTypeRootTreeItem(getter_AddRefs(root)); + if (root) { + // If the item type is typeContent, we assume we are in browser tab + // content. Note, this includes content such as about:addons, + // for consistency. + if (root->ItemType() == nsIDocShellTreeItem::typeContent) { + return Relation(nsAccUtils::GetDocAccessibleFor(root)); + } + } + } + return Relation(); + } + + case RelationType::CONTAINING_APPLICATION: + return Relation(ApplicationAcc()); + + case RelationType::DETAILS: + return Relation(new IDRefsIterator(mDoc, mContent, nsGkAtoms::aria_details)); + + case RelationType::DETAILS_FOR: + return Relation(new RelatedAccIterator(mDoc, mContent, nsGkAtoms::aria_details)); + + case RelationType::ERRORMSG: + return Relation(new IDRefsIterator(mDoc, mContent, nsGkAtoms::aria_errormessage)); + + case RelationType::ERRORMSG_FOR: + return Relation(new RelatedAccIterator(mDoc, mContent, nsGkAtoms::aria_errormessage)); + + default: + return Relation(); + } +} + +void +Accessible::GetNativeInterface(void** aNativeAccessible) +{ +} + +void +Accessible::DoCommand(nsIContent *aContent, uint32_t aActionIndex) +{ + class Runnable final : public mozilla::Runnable + { + public: + Runnable(Accessible* aAcc, nsIContent* aContent, uint32_t aIdx) : + mAcc(aAcc), mContent(aContent), mIdx(aIdx) { } + + NS_IMETHOD Run() override + { + if (mAcc) + mAcc->DispatchClickEvent(mContent, mIdx); + + return NS_OK; + } + + void Revoke() + { + mAcc = nullptr; + mContent = nullptr; + } + + private: + RefPtr<Accessible> mAcc; + nsCOMPtr<nsIContent> mContent; + uint32_t mIdx; + }; + + nsIContent* content = aContent ? aContent : mContent.get(); + nsCOMPtr<nsIRunnable> runnable = new Runnable(this, content, aActionIndex); + NS_DispatchToMainThread(runnable); +} + +void +Accessible::DispatchClickEvent(nsIContent *aContent, uint32_t aActionIndex) +{ + if (IsDefunct()) + return; + + nsCOMPtr<nsIPresShell> presShell = mDoc->PresShell(); + + // Scroll into view. + presShell->ScrollContentIntoView(aContent, + nsIPresShell::ScrollAxis(), + nsIPresShell::ScrollAxis(), + nsIPresShell::SCROLL_OVERFLOW_HIDDEN); + + nsWeakFrame frame = aContent->GetPrimaryFrame(); + if (!frame) + return; + + // Compute x and y coordinates. + nsPoint point; + nsCOMPtr<nsIWidget> widget = frame->GetNearestWidget(point); + if (!widget) + return; + + nsSize size = frame->GetSize(); + + RefPtr<nsPresContext> presContext = presShell->GetPresContext(); + int32_t x = presContext->AppUnitsToDevPixels(point.x + size.width / 2); + int32_t y = presContext->AppUnitsToDevPixels(point.y + size.height / 2); + + // Simulate a touch interaction by dispatching touch events with mouse events. + nsCoreUtils::DispatchTouchEvent(eTouchStart, x, y, aContent, frame, + presShell, widget); + nsCoreUtils::DispatchMouseEvent(eMouseDown, x, y, aContent, frame, + presShell, widget); + nsCoreUtils::DispatchTouchEvent(eTouchEnd, x, y, aContent, frame, + presShell, widget); + nsCoreUtils::DispatchMouseEvent(eMouseUp, x, y, aContent, frame, + presShell, widget); +} + +void +Accessible::ScrollToPoint(uint32_t aCoordinateType, int32_t aX, int32_t aY) +{ + nsIFrame* frame = GetFrame(); + if (!frame) + return; + + nsIntPoint coords = + nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordinateType, this); + + nsIFrame* parentFrame = frame; + while ((parentFrame = parentFrame->GetParent())) + nsCoreUtils::ScrollFrameToPoint(parentFrame, frame, coords); +} + +void +Accessible::AppendTextTo(nsAString& aText, uint32_t aStartOffset, + uint32_t aLength) +{ + // Return text representation of non-text accessible within hypertext + // accessible. Text accessible overrides this method to return enclosed text. + if (aStartOffset != 0 || aLength == 0) + return; + + nsIFrame *frame = GetFrame(); + if (!frame) + return; + + NS_ASSERTION(mParent, + "Called on accessible unbound from tree. Result can be wrong."); + + if (frame->GetType() == nsGkAtoms::brFrame) { + aText += kForcedNewLineChar; + } else if (mParent && nsAccUtils::MustPrune(mParent)) { + // Expose the embedded object accessible as imaginary embedded object + // character if its parent hypertext accessible doesn't expose children to + // AT. + aText += kImaginaryEmbeddedObjectChar; + } else { + aText += kEmbeddedObjectChar; + } +} + +void +Accessible::Shutdown() +{ + // Mark the accessible as defunct, invalidate the child count and pointers to + // other accessibles, also make sure none of its children point to this parent + mStateFlags |= eIsDefunct; + + int32_t childCount = mChildren.Length(); + for (int32_t childIdx = 0; childIdx < childCount; childIdx++) { + mChildren.ElementAt(childIdx)->UnbindFromParent(); + } + mChildren.Clear(); + + mEmbeddedObjCollector = nullptr; + + if (mParent) + mParent->RemoveChild(this); + + mContent = nullptr; + mDoc = nullptr; + if (SelectionMgr() && SelectionMgr()->AccessibleWithCaret(nullptr) == this) + SelectionMgr()->ResetCaretOffset(); +} + +// Accessible protected +void +Accessible::ARIAName(nsString& aName) +{ + // aria-labelledby now takes precedence over aria-label + nsresult rv = nsTextEquivUtils:: + GetTextEquivFromIDRefs(this, nsGkAtoms::aria_labelledby, aName); + if (NS_SUCCEEDED(rv)) { + aName.CompressWhitespace(); + } + + if (aName.IsEmpty() && + mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::aria_label, aName)) { + aName.CompressWhitespace(); + } +} + +// Accessible protected +ENameValueFlag +Accessible::NativeName(nsString& aName) +{ + if (mContent->IsHTMLElement()) { + Accessible* label = nullptr; + HTMLLabelIterator iter(Document(), this); + while ((label = iter.Next())) { + nsTextEquivUtils::AppendTextEquivFromContent(this, label->GetContent(), + &aName); + aName.CompressWhitespace(); + } + + if (!aName.IsEmpty()) + return eNameOK; + + nsTextEquivUtils::GetNameFromSubtree(this, aName); + return aName.IsEmpty() ? eNameOK : eNameFromSubtree; + } + + if (mContent->IsXULElement()) { + XULElmName(mDoc, mContent, aName); + if (!aName.IsEmpty()) + return eNameOK; + + nsTextEquivUtils::GetNameFromSubtree(this, aName); + return aName.IsEmpty() ? eNameOK : eNameFromSubtree; + } + + if (mContent->IsSVGElement()) { + // If user agents need to choose among multiple ‘desc’ or ‘title’ elements + // for processing, the user agent shall choose the first one. + for (nsIContent* childElm = mContent->GetFirstChild(); childElm; + childElm = childElm->GetNextSibling()) { + if (childElm->IsSVGElement(nsGkAtoms::title)) { + nsTextEquivUtils::AppendTextEquivFromContent(this, childElm, &aName); + return eNameOK; + } + } + } + + return eNameOK; +} + +// Accessible protected +void +Accessible::NativeDescription(nsString& aDescription) +{ + bool isXUL = mContent->IsXULElement(); + if (isXUL) { + // Try XUL <description control="[id]">description text</description> + XULDescriptionIterator iter(Document(), mContent); + Accessible* descr = nullptr; + while ((descr = iter.Next())) { + nsTextEquivUtils::AppendTextEquivFromContent(this, descr->GetContent(), + &aDescription); + } + } +} + +// Accessible protected +void +Accessible::BindToParent(Accessible* aParent, uint32_t aIndexInParent) +{ + MOZ_ASSERT(aParent, "This method isn't used to set null parent"); + MOZ_ASSERT(!mParent, "The child was expected to be moved"); + +#ifdef A11Y_LOG + if (mParent) { + logging::TreeInfo("BindToParent: stealing accessible", 0, + "old parent", mParent, + "new parent", aParent, + "child", this, nullptr); + } +#endif + + mParent = aParent; + mIndexInParent = aIndexInParent; + + // Note: this is currently only used for richlistitems and their children. + if (mParent->HasNameDependentParent() || mParent->IsXULListItem()) + mContextFlags |= eHasNameDependentParent; + else + mContextFlags &= ~eHasNameDependentParent; + + if (mParent->IsARIAHidden() || aria::HasDefinedARIAHidden(mContent)) + SetARIAHidden(true); + + mContextFlags |= + static_cast<uint32_t>((mParent->IsAlert() || + mParent->IsInsideAlert())) & eInsideAlert; +} + +// Accessible protected +void +Accessible::UnbindFromParent() +{ + mParent = nullptr; + mIndexInParent = -1; + mInt.mIndexOfEmbeddedChild = -1; + if (IsProxy()) + MOZ_CRASH("this should never be called on proxy wrappers"); + + delete mBits.groupInfo; + mBits.groupInfo = nullptr; + mContextFlags &= ~eHasNameDependentParent & ~eInsideAlert; +} + +//////////////////////////////////////////////////////////////////////////////// +// Accessible public methods + +RootAccessible* +Accessible::RootAccessible() const +{ + nsCOMPtr<nsIDocShell> docShell = nsCoreUtils::GetDocShellFor(GetNode()); + NS_ASSERTION(docShell, "No docshell for mContent"); + if (!docShell) { + return nullptr; + } + + nsCOMPtr<nsIDocShellTreeItem> root; + docShell->GetRootTreeItem(getter_AddRefs(root)); + NS_ASSERTION(root, "No root content tree item"); + if (!root) { + return nullptr; + } + + DocAccessible* docAcc = nsAccUtils::GetDocAccessibleFor(root); + return docAcc ? docAcc->AsRoot() : nullptr; +} + +nsIFrame* +Accessible::GetFrame() const +{ + return mContent ? mContent->GetPrimaryFrame() : nullptr; +} + +nsINode* +Accessible::GetNode() const +{ + return mContent; +} + +void +Accessible::Language(nsAString& aLanguage) +{ + aLanguage.Truncate(); + + if (!mDoc) + return; + + nsCoreUtils::GetLanguageFor(mContent, nullptr, aLanguage); + if (aLanguage.IsEmpty()) { // Nothing found, so use document's language + mDoc->DocumentNode()->GetHeaderData(nsGkAtoms::headerContentLanguage, + aLanguage); + } +} + +bool +Accessible::InsertChildAt(uint32_t aIndex, Accessible* aChild) +{ + if (!aChild) + return false; + + if (aIndex == mChildren.Length()) { + if (!mChildren.AppendElement(aChild)) + return false; + + } else { + if (!mChildren.InsertElementAt(aIndex, aChild)) + return false; + + MOZ_ASSERT(mStateFlags & eKidsMutating, "Illicit children change"); + + for (uint32_t idx = aIndex + 1; idx < mChildren.Length(); idx++) { + mChildren[idx]->mIndexInParent = idx; + } + } + + if (aChild->IsText()) { + mStateFlags |= eHasTextKids; + } + + aChild->BindToParent(this, aIndex); + return true; +} + +bool +Accessible::RemoveChild(Accessible* aChild) +{ + if (!aChild) + return false; + + if (aChild->mParent != this || aChild->mIndexInParent == -1) + return false; + + MOZ_ASSERT((mStateFlags & eKidsMutating) || aChild->IsDefunct() || aChild->IsDoc(), + "Illicit children change"); + + int32_t index = static_cast<uint32_t>(aChild->mIndexInParent); + if (mChildren.SafeElementAt(index) != aChild) { + MOZ_ASSERT_UNREACHABLE("A wrong child index"); + index = mChildren.IndexOf(aChild); + if (index == -1) { + MOZ_ASSERT_UNREACHABLE("No child was found"); + return false; + } + } + + aChild->UnbindFromParent(); + mChildren.RemoveElementAt(index); + + for (uint32_t idx = index; idx < mChildren.Length(); idx++) { + mChildren[idx]->mIndexInParent = idx; + } + + return true; +} + +void +Accessible::MoveChild(uint32_t aNewIndex, Accessible* aChild) +{ + MOZ_ASSERT(aChild, "No child was given"); + MOZ_ASSERT(aChild->mParent == this, "A child from different subtree was given"); + MOZ_ASSERT(aChild->mIndexInParent != -1, "Unbound child was given"); + MOZ_ASSERT(static_cast<uint32_t>(aChild->mIndexInParent) != aNewIndex, + "No move, same index"); + MOZ_ASSERT(aNewIndex <= mChildren.Length(), "Wrong new index was given"); + + RefPtr<AccHideEvent> hideEvent = new AccHideEvent(aChild, false); + if (mDoc->Controller()->QueueMutationEvent(hideEvent)) { + aChild->SetHideEventTarget(true); + } + + mEmbeddedObjCollector = nullptr; + mChildren.RemoveElementAt(aChild->mIndexInParent); + + uint32_t startIdx = aNewIndex, endIdx = aChild->mIndexInParent; + + // If the child is moved after its current position. + if (static_cast<uint32_t>(aChild->mIndexInParent) < aNewIndex) { + startIdx = aChild->mIndexInParent; + if (aNewIndex == mChildren.Length() + 1) { + // The child is moved to the end. + mChildren.AppendElement(aChild); + endIdx = mChildren.Length() - 1; + } + else { + mChildren.InsertElementAt(aNewIndex - 1, aChild); + endIdx = aNewIndex; + } + } + else { + // The child is moved prior its current position. + mChildren.InsertElementAt(aNewIndex, aChild); + } + + for (uint32_t idx = startIdx; idx <= endIdx; idx++) { + mChildren[idx]->mIndexInParent = idx; + mChildren[idx]->mStateFlags |= eGroupInfoDirty; + mChildren[idx]->mInt.mIndexOfEmbeddedChild = -1; + } + + RefPtr<AccShowEvent> showEvent = new AccShowEvent(aChild); + DebugOnly<bool> added = mDoc->Controller()->QueueMutationEvent(showEvent); + MOZ_ASSERT(added); + aChild->SetShowEventTarget(true); +} + +Accessible* +Accessible::GetChildAt(uint32_t aIndex) const +{ + Accessible* child = mChildren.SafeElementAt(aIndex, nullptr); + if (!child) + return nullptr; + +#ifdef DEBUG + Accessible* realParent = child->mParent; + NS_ASSERTION(!realParent || realParent == this, + "Two accessibles have the same first child accessible!"); +#endif + + return child; +} + +uint32_t +Accessible::ChildCount() const +{ + return mChildren.Length(); +} + +int32_t +Accessible::IndexInParent() const +{ + return mIndexInParent; +} + +uint32_t +Accessible::EmbeddedChildCount() +{ + if (mStateFlags & eHasTextKids) { + if (!mEmbeddedObjCollector) + mEmbeddedObjCollector.reset(new EmbeddedObjCollector(this)); + return mEmbeddedObjCollector->Count(); + } + + return ChildCount(); +} + +Accessible* +Accessible::GetEmbeddedChildAt(uint32_t aIndex) +{ + if (mStateFlags & eHasTextKids) { + if (!mEmbeddedObjCollector) + mEmbeddedObjCollector.reset(new EmbeddedObjCollector(this)); + return mEmbeddedObjCollector.get() ? + mEmbeddedObjCollector->GetAccessibleAt(aIndex) : nullptr; + } + + return GetChildAt(aIndex); +} + +int32_t +Accessible::GetIndexOfEmbeddedChild(Accessible* aChild) +{ + if (mStateFlags & eHasTextKids) { + if (!mEmbeddedObjCollector) + mEmbeddedObjCollector.reset(new EmbeddedObjCollector(this)); + return mEmbeddedObjCollector.get() ? + mEmbeddedObjCollector->GetIndexAt(aChild) : -1; + } + + return GetIndexOf(aChild); +} + +//////////////////////////////////////////////////////////////////////////////// +// HyperLinkAccessible methods + +bool +Accessible::IsLink() +{ + // Every embedded accessible within hypertext accessible implements + // hyperlink interface. + return mParent && mParent->IsHyperText() && !IsText(); +} + +uint32_t +Accessible::StartOffset() +{ + NS_PRECONDITION(IsLink(), "StartOffset is called not on hyper link!"); + + HyperTextAccessible* hyperText = mParent ? mParent->AsHyperText() : nullptr; + return hyperText ? hyperText->GetChildOffset(this) : 0; +} + +uint32_t +Accessible::EndOffset() +{ + NS_PRECONDITION(IsLink(), "EndOffset is called on not hyper link!"); + + HyperTextAccessible* hyperText = mParent ? mParent->AsHyperText() : nullptr; + return hyperText ? (hyperText->GetChildOffset(this) + 1) : 0; +} + +uint32_t +Accessible::AnchorCount() +{ + NS_PRECONDITION(IsLink(), "AnchorCount is called on not hyper link!"); + return 1; +} + +Accessible* +Accessible::AnchorAt(uint32_t aAnchorIndex) +{ + NS_PRECONDITION(IsLink(), "GetAnchor is called on not hyper link!"); + return aAnchorIndex == 0 ? this : nullptr; +} + +already_AddRefed<nsIURI> +Accessible::AnchorURIAt(uint32_t aAnchorIndex) +{ + NS_PRECONDITION(IsLink(), "AnchorURIAt is called on not hyper link!"); + return nullptr; +} + +void +Accessible::ToTextPoint(HyperTextAccessible** aContainer, int32_t* aOffset, + bool aIsBefore) const +{ + if (IsHyperText()) { + *aContainer = const_cast<Accessible*>(this)->AsHyperText(); + *aOffset = aIsBefore ? 0 : (*aContainer)->CharacterCount(); + return; + } + + const Accessible* child = nullptr; + const Accessible* parent = this; + do { + child = parent; + parent = parent->Parent(); + } while (parent && !parent->IsHyperText()); + + if (parent) { + *aContainer = const_cast<Accessible*>(parent)->AsHyperText(); + *aOffset = (*aContainer)->GetChildOffset( + child->IndexInParent() + static_cast<int32_t>(!aIsBefore)); + } +} + + +//////////////////////////////////////////////////////////////////////////////// +// SelectAccessible + +void +Accessible::SelectedItems(nsTArray<Accessible*>* aItems) +{ + AccIterator iter(this, filters::GetSelected); + Accessible* selected = nullptr; + while ((selected = iter.Next())) + aItems->AppendElement(selected); +} + +uint32_t +Accessible::SelectedItemCount() +{ + uint32_t count = 0; + AccIterator iter(this, filters::GetSelected); + Accessible* selected = nullptr; + while ((selected = iter.Next())) + ++count; + + return count; +} + +Accessible* +Accessible::GetSelectedItem(uint32_t aIndex) +{ + AccIterator iter(this, filters::GetSelected); + Accessible* selected = nullptr; + + uint32_t index = 0; + while ((selected = iter.Next()) && index < aIndex) + index++; + + return selected; +} + +bool +Accessible::IsItemSelected(uint32_t aIndex) +{ + uint32_t index = 0; + AccIterator iter(this, filters::GetSelectable); + Accessible* selected = nullptr; + while ((selected = iter.Next()) && index < aIndex) + index++; + + return selected && + selected->State() & states::SELECTED; +} + +bool +Accessible::AddItemToSelection(uint32_t aIndex) +{ + uint32_t index = 0; + AccIterator iter(this, filters::GetSelectable); + Accessible* selected = nullptr; + while ((selected = iter.Next()) && index < aIndex) + index++; + + if (selected) + selected->SetSelected(true); + + return static_cast<bool>(selected); +} + +bool +Accessible::RemoveItemFromSelection(uint32_t aIndex) +{ + uint32_t index = 0; + AccIterator iter(this, filters::GetSelectable); + Accessible* selected = nullptr; + while ((selected = iter.Next()) && index < aIndex) + index++; + + if (selected) + selected->SetSelected(false); + + return static_cast<bool>(selected); +} + +bool +Accessible::SelectAll() +{ + bool success = false; + Accessible* selectable = nullptr; + + AccIterator iter(this, filters::GetSelectable); + while((selectable = iter.Next())) { + success = true; + selectable->SetSelected(true); + } + return success; +} + +bool +Accessible::UnselectAll() +{ + bool success = false; + Accessible* selected = nullptr; + + AccIterator iter(this, filters::GetSelected); + while ((selected = iter.Next())) { + success = true; + selected->SetSelected(false); + } + return success; +} + +//////////////////////////////////////////////////////////////////////////////// +// Widgets + +bool +Accessible::IsWidget() const +{ + return false; +} + +bool +Accessible::IsActiveWidget() const +{ + if (FocusMgr()->HasDOMFocus(mContent)) + return true; + + // If text entry of combobox widget has a focus then the combobox widget is + // active. + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + if (roleMapEntry && roleMapEntry->Is(nsGkAtoms::combobox)) { + uint32_t childCount = ChildCount(); + for (uint32_t idx = 0; idx < childCount; idx++) { + Accessible* child = mChildren.ElementAt(idx); + if (child->Role() == roles::ENTRY) + return FocusMgr()->HasDOMFocus(child->GetContent()); + } + } + + return false; +} + +bool +Accessible::AreItemsOperable() const +{ + return HasOwnContent() && + mContent->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_activedescendant); +} + +Accessible* +Accessible::CurrentItem() +{ + // Check for aria-activedescendant, which changes which element has focus. + // For activedescendant, the ARIA spec does not require that the user agent + // checks whether pointed node is actually a DOM descendant of the element + // with the aria-activedescendant attribute. + nsAutoString id; + if (HasOwnContent() && + mContent->GetAttr(kNameSpaceID_None, + nsGkAtoms::aria_activedescendant, id)) { + nsIDocument* DOMDoc = mContent->OwnerDoc(); + dom::Element* activeDescendantElm = DOMDoc->GetElementById(id); + if (activeDescendantElm) { + DocAccessible* document = Document(); + if (document) + return document->GetAccessible(activeDescendantElm); + } + } + return nullptr; +} + +void +Accessible::SetCurrentItem(Accessible* aItem) +{ + nsIAtom* id = aItem->GetContent()->GetID(); + if (id) { + nsAutoString idStr; + id->ToString(idStr); + mContent->SetAttr(kNameSpaceID_None, + nsGkAtoms::aria_activedescendant, idStr, true); + } +} + +Accessible* +Accessible::ContainerWidget() const +{ + if (HasARIARole() && mContent->HasID()) { + for (Accessible* parent = Parent(); parent; parent = parent->Parent()) { + nsIContent* parentContent = parent->GetContent(); + if (parentContent && + parentContent->HasAttr(kNameSpaceID_None, + nsGkAtoms::aria_activedescendant)) { + return parent; + } + + // Don't cross DOM document boundaries. + if (parent->IsDoc()) + break; + } + } + return nullptr; +} + +void +Accessible::SetARIAHidden(bool aIsDefined) +{ + if (aIsDefined) + mContextFlags |= eARIAHidden; + else + mContextFlags &= ~eARIAHidden; + + uint32_t length = mChildren.Length(); + for (uint32_t i = 0; i < length; i++) { + mChildren[i]->SetARIAHidden(aIsDefined); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Accessible protected methods + +void +Accessible::LastRelease() +{ + // First cleanup if needed... + if (mDoc) { + Shutdown(); + NS_ASSERTION(!mDoc, + "A Shutdown() impl forgot to call its parent's Shutdown?"); + } + // ... then die. + delete this; +} + +Accessible* +Accessible::GetSiblingAtOffset(int32_t aOffset, nsresult* aError) const +{ + if (!mParent || mIndexInParent == -1) { + if (aError) + *aError = NS_ERROR_UNEXPECTED; + + return nullptr; + } + + if (aError && + mIndexInParent + aOffset >= static_cast<int32_t>(mParent->ChildCount())) { + *aError = NS_OK; // fail peacefully + return nullptr; + } + + Accessible* child = mParent->GetChildAt(mIndexInParent + aOffset); + if (aError && !child) + *aError = NS_ERROR_UNEXPECTED; + + return child; +} + +double +Accessible::AttrNumericValue(nsIAtom* aAttr) const +{ + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + if (!roleMapEntry || roleMapEntry->valueRule == eNoValue) + return UnspecifiedNaN<double>(); + + nsAutoString attrValue; + if (!mContent->GetAttr(kNameSpaceID_None, aAttr, attrValue)) + return UnspecifiedNaN<double>(); + + nsresult error = NS_OK; + double value = attrValue.ToDouble(&error); + return NS_FAILED(error) ? UnspecifiedNaN<double>() : value; +} + +uint32_t +Accessible::GetActionRule() const +{ + if (!HasOwnContent() || (InteractiveState() & states::UNAVAILABLE)) + return eNoAction; + + // Return "click" action on elements that have an attached popup menu. + if (mContent->IsXULElement()) + if (mContent->HasAttr(kNameSpaceID_None, nsGkAtoms::popup)) + return eClickAction; + + // Has registered 'click' event handler. + bool isOnclick = nsCoreUtils::HasClickListener(mContent); + + if (isOnclick) + return eClickAction; + + // Get an action based on ARIA role. + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + if (roleMapEntry && + roleMapEntry->actionRule != eNoAction) + return roleMapEntry->actionRule; + + // Get an action based on ARIA attribute. + if (nsAccUtils::HasDefinedARIAToken(mContent, + nsGkAtoms::aria_expanded)) + return eExpandAction; + + return eNoAction; +} + +AccGroupInfo* +Accessible::GetGroupInfo() +{ + if (IsProxy()) + MOZ_CRASH("This should never be called on proxy wrappers"); + + if (mBits.groupInfo){ + if (HasDirtyGroupInfo()) { + mBits.groupInfo->Update(); + mStateFlags &= ~eGroupInfoDirty; + } + + return mBits.groupInfo; + } + + mBits.groupInfo = AccGroupInfo::CreateGroupInfo(this); + return mBits.groupInfo; +} + +void +Accessible::GetPositionAndSizeInternal(int32_t *aPosInSet, int32_t *aSetSize) +{ + AccGroupInfo* groupInfo = GetGroupInfo(); + if (groupInfo) { + *aPosInSet = groupInfo->PosInSet(); + *aSetSize = groupInfo->SetSize(); + } +} + +int32_t +Accessible::GetLevelInternal() +{ + int32_t level = nsAccUtils::GetDefaultLevel(this); + + if (!IsBoundToParent()) + return level; + + roles::Role role = Role(); + if (role == roles::OUTLINEITEM) { + // Always expose 'level' attribute for 'outlineitem' accessible. The number + // of nested 'grouping' accessibles containing 'outlineitem' accessible is + // its level. + level = 1; + + Accessible* parent = this; + while ((parent = parent->Parent())) { + roles::Role parentRole = parent->Role(); + + if (parentRole == roles::OUTLINE) + break; + if (parentRole == roles::GROUPING) + ++ level; + + } + + } else if (role == roles::LISTITEM) { + // Expose 'level' attribute on nested lists. We support two hierarchies: + // a) list -> listitem -> list -> listitem (nested list is a last child + // of listitem of the parent list); + // b) list -> listitem -> group -> listitem (nested listitems are contained + // by group that is a last child of the parent listitem). + + // Calculate 'level' attribute based on number of parent listitems. + level = 0; + Accessible* parent = this; + while ((parent = parent->Parent())) { + roles::Role parentRole = parent->Role(); + + if (parentRole == roles::LISTITEM) + ++ level; + else if (parentRole != roles::LIST && parentRole != roles::GROUPING) + break; + } + + if (level == 0) { + // If this listitem is on top of nested lists then expose 'level' + // attribute. + parent = Parent(); + uint32_t siblingCount = parent->ChildCount(); + for (uint32_t siblingIdx = 0; siblingIdx < siblingCount; siblingIdx++) { + Accessible* sibling = parent->GetChildAt(siblingIdx); + + Accessible* siblingChild = sibling->LastChild(); + if (siblingChild) { + roles::Role lastChildRole = siblingChild->Role(); + if (lastChildRole == roles::LIST || lastChildRole == roles::GROUPING) + return 1; + } + } + } else { + ++ level; // level is 1-index based + } + } + + return level; +} + +void +Accessible::StaticAsserts() const +{ + static_assert(eLastStateFlag <= (1 << kStateFlagsBits) - 1, + "Accessible::mStateFlags was oversized by eLastStateFlag!"); + static_assert(eLastAccType <= (1 << kTypeBits) - 1, + "Accessible::mType was oversized by eLastAccType!"); + static_assert(eLastContextFlag <= (1 << kContextFlagsBits) - 1, + "Accessible::mContextFlags was oversized by eLastContextFlag!"); + static_assert(eLastAccGenericType <= (1 << kGenericTypesBits) - 1, + "Accessible::mGenericType was oversized by eLastAccGenericType!"); +} + +//////////////////////////////////////////////////////////////////////////////// +// KeyBinding class + +// static +uint32_t +KeyBinding::AccelModifier() +{ + switch (WidgetInputEvent::AccelModifier()) { + case MODIFIER_ALT: + return kAlt; + case MODIFIER_CONTROL: + return kControl; + case MODIFIER_META: + return kMeta; + case MODIFIER_OS: + return kOS; + default: + MOZ_CRASH("Handle the new result of WidgetInputEvent::AccelModifier()"); + return 0; + } +} + +void +KeyBinding::ToPlatformFormat(nsAString& aValue) const +{ + nsCOMPtr<nsIStringBundle> keyStringBundle; + nsCOMPtr<nsIStringBundleService> stringBundleService = + mozilla::services::GetStringBundleService(); + if (stringBundleService) + stringBundleService->CreateBundle( + "chrome://global-platform/locale/platformKeys.properties", + getter_AddRefs(keyStringBundle)); + + if (!keyStringBundle) + return; + + nsAutoString separator; + keyStringBundle->GetStringFromName(u"MODIFIER_SEPARATOR", + getter_Copies(separator)); + + nsAutoString modifierName; + if (mModifierMask & kControl) { + keyStringBundle->GetStringFromName(u"VK_CONTROL", + getter_Copies(modifierName)); + + aValue.Append(modifierName); + aValue.Append(separator); + } + + if (mModifierMask & kAlt) { + keyStringBundle->GetStringFromName(u"VK_ALT", + getter_Copies(modifierName)); + + aValue.Append(modifierName); + aValue.Append(separator); + } + + if (mModifierMask & kShift) { + keyStringBundle->GetStringFromName(u"VK_SHIFT", + getter_Copies(modifierName)); + + aValue.Append(modifierName); + aValue.Append(separator); + } + + if (mModifierMask & kMeta) { + keyStringBundle->GetStringFromName(u"VK_META", + getter_Copies(modifierName)); + + aValue.Append(modifierName); + aValue.Append(separator); + } + + aValue.Append(mKey); +} + +void +KeyBinding::ToAtkFormat(nsAString& aValue) const +{ + nsAutoString modifierName; + if (mModifierMask & kControl) + aValue.AppendLiteral("<Control>"); + + if (mModifierMask & kAlt) + aValue.AppendLiteral("<Alt>"); + + if (mModifierMask & kShift) + aValue.AppendLiteral("<Shift>"); + + if (mModifierMask & kMeta) + aValue.AppendLiteral("<Meta>"); + + aValue.Append(mKey); +} |