diff options
Diffstat (limited to '')
-rw-r--r-- | accessible/xul/XULMenuAccessible.cpp | 484 |
1 files changed, 484 insertions, 0 deletions
diff --git a/accessible/xul/XULMenuAccessible.cpp b/accessible/xul/XULMenuAccessible.cpp new file mode 100644 index 0000000000..dc39a6ee94 --- /dev/null +++ b/accessible/xul/XULMenuAccessible.cpp @@ -0,0 +1,484 @@ +/* -*- 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 "XULMenuAccessible.h" + +#include "LocalAccessible-inl.h" +#include "XULMenuBarElement.h" +#include "XULMenuParentElement.h" +#include "XULPopupElement.h" +#include "mozilla/Assertions.h" +#include "nsAccessibilityService.h" +#include "nsAccUtils.h" +#include "DocAccessible.h" +#include "Role.h" +#include "States.h" +#include "XULFormControlAccessible.h" + +#include "nsIContentInlines.h" +#include "nsIDOMXULContainerElement.h" +#include "nsIDOMXULSelectCntrlEl.h" +#include "nsIDOMXULSelectCntrlItemEl.h" +#include "nsIContent.h" +#include "nsMenuPopupFrame.h" + +#include "mozilla/Preferences.h" +#include "mozilla/LookAndFeel.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/XULButtonElement.h" +#include "mozilla/dom/KeyboardEventBinding.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// XULMenuitemAccessible +//////////////////////////////////////////////////////////////////////////////// + +XULMenuitemAccessible::XULMenuitemAccessible(nsIContent* aContent, + DocAccessible* aDoc) + : AccessibleWrap(aContent, aDoc) {} + +uint64_t XULMenuitemAccessible::NativeState() const { + uint64_t state = LocalAccessible::NativeState(); + + // Has Popup? + if (mContent->NodeInfo()->Equals(nsGkAtoms::menu, kNameSpaceID_XUL)) { + state |= states::HASPOPUP; + if (mContent->AsElement()->HasAttr(kNameSpaceID_None, nsGkAtoms::open)) { + state |= states::EXPANDED; + } else { + state |= states::COLLAPSED; + } + } + + // Checkable/checked? + static dom::Element::AttrValuesArray strings[] = { + nsGkAtoms::radio, nsGkAtoms::checkbox, nullptr}; + + if (mContent->AsElement()->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::type, + strings, eCaseMatters) >= 0) { + // Checkable? + state |= states::CHECKABLE; + + // Checked? + if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, + nsGkAtoms::checked, nsGkAtoms::_true, + eCaseMatters)) { + state |= states::CHECKED; + } + } + + // Combo box listitem + bool isComboboxOption = (Role() == roles::COMBOBOX_OPTION); + if (isComboboxOption) { + // Is selected? + bool isSelected = false; + nsCOMPtr<nsIDOMXULSelectControlItemElement> item = + Elm()->AsXULSelectControlItem(); + NS_ENSURE_TRUE(item, state); + item->GetSelected(&isSelected); + + // Is collapsed? + bool isCollapsed = false; + LocalAccessible* parent = LocalParent(); + if (parent && parent->State() & states::INVISIBLE) isCollapsed = true; + + if (isSelected) { + state |= states::SELECTED; + + // Selected and collapsed? + if (isCollapsed) { + // Set selected option offscreen/invisible according to combobox state + LocalAccessible* grandParent = parent->LocalParent(); + if (!grandParent) return state; + NS_ASSERTION(grandParent->IsCombobox(), + "grandparent of combobox listitem is not combobox"); + uint64_t grandParentState = grandParent->State(); + state &= ~(states::OFFSCREEN | states::INVISIBLE); + state |= (grandParentState & states::OFFSCREEN) | + (grandParentState & states::INVISIBLE) | + (grandParentState & states::OPAQUE1); + } // isCollapsed + } // isSelected + } // ROLE_COMBOBOX_OPTION + + return state; +} + +uint64_t XULMenuitemAccessible::NativeInteractiveState() const { + if (NativelyUnavailable()) { + // Note: keep in sinc with nsXULPopupManager::IsValidMenuItem() logic. + auto* button = dom::XULButtonElement::FromNode(GetContent()); + bool skipNavigatingDisabledMenuItem = true; + if (!button || !button->IsOnMenuBar()) { + skipNavigatingDisabledMenuItem = LookAndFeel::GetInt( + LookAndFeel::IntID::SkipNavigatingDisabledMenuItem); + } + + if (skipNavigatingDisabledMenuItem) return states::UNAVAILABLE; + + return states::UNAVAILABLE | states::FOCUSABLE | states::SELECTABLE; + } + + return states::FOCUSABLE | states::SELECTABLE; +} + +ENameValueFlag XULMenuitemAccessible::NativeName(nsString& aName) const { + mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::label, aName); + return eNameOK; +} + +void XULMenuitemAccessible::Description(nsString& aDescription) const { + mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::description, + aDescription); +} + +KeyBinding XULMenuitemAccessible::AccessKey() const { + // Return menu accesskey: N or Alt+F. + static int32_t gMenuAccesskeyModifier = + -1; // magic value of -1 indicates unitialized state + + // We do not use nsCoreUtils::GetAccesskeyFor() because accesskeys for + // menu are't registered by EventStateManager. + nsAutoString accesskey; + mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::accesskey, + accesskey); + if (accesskey.IsEmpty()) return KeyBinding(); + + uint32_t modifierKey = 0; + + LocalAccessible* parentAcc = LocalParent(); + if (parentAcc) { + if (parentAcc->NativeRole() == roles::MENUBAR) { + // If top level menu item, add Alt+ or whatever modifier text to string + // No need to cache pref service, this happens rarely + if (gMenuAccesskeyModifier == -1) { + // Need to initialize cached global accesskey pref + gMenuAccesskeyModifier = Preferences::GetInt("ui.key.menuAccessKey", 0); + } + + switch (gMenuAccesskeyModifier) { + case dom::KeyboardEvent_Binding::DOM_VK_CONTROL: + modifierKey = KeyBinding::kControl; + break; + case dom::KeyboardEvent_Binding::DOM_VK_ALT: + modifierKey = KeyBinding::kAlt; + break; + case dom::KeyboardEvent_Binding::DOM_VK_META: + modifierKey = KeyBinding::kMeta; + break; + case dom::KeyboardEvent_Binding::DOM_VK_WIN: + modifierKey = KeyBinding::kOS; + break; + } + } + } + + return KeyBinding(accesskey[0], modifierKey); +} + +KeyBinding XULMenuitemAccessible::KeyboardShortcut() const { + nsAutoString keyElmId; + mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::key, keyElmId); + if (keyElmId.IsEmpty()) return KeyBinding(); + + dom::Element* keyElm = mContent->OwnerDoc()->GetElementById(keyElmId); + if (!keyElm) return KeyBinding(); + + uint32_t key = 0; + + nsAutoString keyStr; + keyElm->GetAttr(kNameSpaceID_None, nsGkAtoms::key, keyStr); + if (keyStr.IsEmpty()) { + nsAutoString keyCodeStr; + keyElm->GetAttr(kNameSpaceID_None, nsGkAtoms::keycode, keyCodeStr); + nsresult errorCode; + key = keyStr.ToInteger(&errorCode, /* aRadix = */ 10); + if (NS_FAILED(errorCode)) { + key = keyStr.ToInteger(&errorCode, /* aRadix = */ 16); + } + } else { + key = keyStr[0]; + } + + nsAutoString modifiersStr; + keyElm->GetAttr(kNameSpaceID_None, nsGkAtoms::modifiers, modifiersStr); + + uint32_t modifierMask = 0; + if (modifiersStr.Find(u"shift") != -1) modifierMask |= KeyBinding::kShift; + if (modifiersStr.Find(u"alt") != -1) modifierMask |= KeyBinding::kAlt; + if (modifiersStr.Find(u"meta") != -1) modifierMask |= KeyBinding::kMeta; + if (modifiersStr.Find(u"os") != -1) modifierMask |= KeyBinding::kOS; + if (modifiersStr.Find(u"control") != -1) modifierMask |= KeyBinding::kControl; + if (modifiersStr.Find(u"accel") != -1) { + modifierMask |= KeyBinding::AccelModifier(); + } + + return KeyBinding(key, modifierMask); +} + +role XULMenuitemAccessible::NativeRole() const { + nsCOMPtr<nsIDOMXULContainerElement> xulContainer = Elm()->AsXULContainer(); + if (xulContainer) return roles::PARENT_MENUITEM; + + LocalAccessible* widget = ContainerWidget(); + if (widget && widget->Role() == roles::COMBOBOX_LIST) { + return roles::COMBOBOX_OPTION; + } + + if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, + nsGkAtoms::radio, eCaseMatters)) { + return roles::RADIO_MENU_ITEM; + } + + if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, + nsGkAtoms::checkbox, eCaseMatters)) { + return roles::CHECK_MENU_ITEM; + } + + return roles::MENUITEM; +} + +int32_t XULMenuitemAccessible::GetLevel(bool aFast) const { + return nsAccUtils::GetLevelForXULContainerItem(mContent); +} + +void XULMenuitemAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) { + if (aIndex == eAction_Click) aName.AssignLiteral("click"); +} + +bool XULMenuitemAccessible::HasPrimaryAction() const { return true; } + +//////////////////////////////////////////////////////////////////////////////// +// XULMenuitemAccessible: Widgets + +bool XULMenuitemAccessible::IsActiveWidget() const { + // Parent menu item is a widget, it's active when its popup is open. + // Typically the <menupopup> is included in the document markup, and + // <menu> prepends content in front of it. + nsIContent* menuPopupContent = mContent->GetLastChild(); + if (menuPopupContent) { + nsMenuPopupFrame* menuPopupFrame = + do_QueryFrame(menuPopupContent->GetPrimaryFrame()); + return menuPopupFrame && menuPopupFrame->IsOpen(); + } + return false; +} + +bool XULMenuitemAccessible::AreItemsOperable() const { + // Parent menu item is a widget, its items are operable when its popup is + // open. + nsIContent* menuPopupContent = mContent->GetLastChild(); + if (menuPopupContent) { + nsMenuPopupFrame* menuPopupFrame = + do_QueryFrame(menuPopupContent->GetPrimaryFrame()); + return menuPopupFrame && menuPopupFrame->IsOpen(); + } + return false; +} + +LocalAccessible* XULMenuitemAccessible::ContainerWidget() const { + if (auto* button = dom::XULButtonElement::FromNode(GetContent())) { + if (auto* popup = button->GetMenuParent()) { + // We use GetAccessibleOrContainer instead of just GetAccessible because + // we strip menupopups from the tree for ATK. + return mDoc->GetAccessibleOrContainer(popup); + } + } + return nullptr; +} + +//////////////////////////////////////////////////////////////////////////////// +// XULMenuSeparatorAccessible +//////////////////////////////////////////////////////////////////////////////// + +XULMenuSeparatorAccessible::XULMenuSeparatorAccessible(nsIContent* aContent, + DocAccessible* aDoc) + : XULMenuitemAccessible(aContent, aDoc) {} + +uint64_t XULMenuSeparatorAccessible::NativeState() const { + // Isn't focusable, but can be offscreen/invisible -- only copy those states + return XULMenuitemAccessible::NativeState() & + (states::OFFSCREEN | states::INVISIBLE); +} + +ENameValueFlag XULMenuSeparatorAccessible::NativeName(nsString& aName) const { + return eNameOK; +} + +role XULMenuSeparatorAccessible::NativeRole() const { return roles::SEPARATOR; } + +bool XULMenuSeparatorAccessible::HasPrimaryAction() const { return false; } + +//////////////////////////////////////////////////////////////////////////////// +// XULMenupopupAccessible +//////////////////////////////////////////////////////////////////////////////// + +XULMenupopupAccessible::XULMenupopupAccessible(nsIContent* aContent, + DocAccessible* aDoc) + : XULSelectControlAccessible(aContent, aDoc) { + if (nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(GetFrame())) { + if (menuPopupFrame->GetPopupType() == widget::PopupType::Menu) { + mType = eMenuPopupType; + } + } + + // May be the anonymous <menupopup> inside <menulist> (a combobox) + auto* parent = mContent->GetParentElement(); + nsCOMPtr<nsIDOMXULSelectControlElement> selectControl = + parent ? parent->AsXULSelectControl() : nullptr; + if (selectControl) { + mSelectControl = parent; + } else { + mSelectControl = nullptr; + mGenericTypes &= ~eSelect; + } +} + +uint64_t XULMenupopupAccessible::NativeState() const { + uint64_t state = LocalAccessible::NativeState(); + +#ifdef DEBUG + // We are onscreen if our parent is active + bool isActive = + mContent->AsElement()->HasAttr(kNameSpaceID_None, nsGkAtoms::menuactive); + if (!isActive) { + LocalAccessible* parent = LocalParent(); + if (parent) { + nsIContent* parentContent = parent->GetContent(); + if (parentContent && parentContent->IsElement()) + isActive = parentContent->AsElement()->HasAttr(kNameSpaceID_None, + nsGkAtoms::open); + } + } + + NS_ASSERTION(isActive || (state & states::INVISIBLE), + "XULMenupopup doesn't have INVISIBLE when it's inactive"); +#endif + + if (state & states::INVISIBLE) state |= states::OFFSCREEN | states::COLLAPSED; + + return state; +} + +ENameValueFlag XULMenupopupAccessible::NativeName(nsString& aName) const { + nsIContent* content = mContent; + while (content && aName.IsEmpty()) { + if (content->IsElement()) { + content->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::label, aName); + } + content = content->GetFlattenedTreeParent(); + } + + return eNameOK; +} + +role XULMenupopupAccessible::NativeRole() const { + nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(GetFrame()); + if (menuPopupFrame && menuPopupFrame->IsContextMenu()) { + return roles::MENUPOPUP; + } + + if (mParent) { + if (mParent->IsCombobox()) { + return roles::COMBOBOX_LIST; + } + } + + // If accessible is not bound to the tree (this happens while children are + // cached) return general role. + return roles::MENUPOPUP; +} + +//////////////////////////////////////////////////////////////////////////////// +// XULMenupopupAccessible: Widgets + +bool XULMenupopupAccessible::IsWidget() const { return true; } + +bool XULMenupopupAccessible::IsActiveWidget() const { + // If menupopup is a widget (the case of context menus) then active when open. + nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(GetFrame()); + return menuPopupFrame && menuPopupFrame->IsOpen(); +} + +bool XULMenupopupAccessible::AreItemsOperable() const { + nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(GetFrame()); + return menuPopupFrame && menuPopupFrame->IsOpen(); +} + +LocalAccessible* XULMenupopupAccessible::ContainerWidget() const { + DocAccessible* document = Document(); + + nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(GetFrame()); + MOZ_ASSERT(menuPopupFrame); + if (!menuPopupFrame) { + return nullptr; + } + + auto* cur = dom::XULPopupElement::FromNode(GetContent()); + while (cur) { + auto* menu = cur->GetContainingMenu(); + if (!menu) { + // <panel> / <tooltip> / etc. + return nullptr; + } + dom::XULMenuParentElement* parent = menu->GetMenuParent(); + if (!parent) { + LocalAccessible* menuPopup = document->GetAccessible(cur); + MOZ_ASSERT(menuPopup); + return menuPopup ? menuPopup->LocalParent() : nullptr; + } + + if (parent->IsMenuBar()) { + return document->GetAccessible(parent); + } + + cur = dom::XULPopupElement::FromNode(parent); + MOZ_ASSERT(cur, "Should be a popup"); + } + + MOZ_ASSERT_UNREACHABLE("How did we get out of the loop without returning?"); + return nullptr; +} + +//////////////////////////////////////////////////////////////////////////////// +// XULMenubarAccessible +//////////////////////////////////////////////////////////////////////////////// + +XULMenubarAccessible::XULMenubarAccessible(nsIContent* aContent, + DocAccessible* aDoc) + : AccessibleWrap(aContent, aDoc) {} + +ENameValueFlag XULMenubarAccessible::NativeName(nsString& aName) const { + aName.AssignLiteral("Application"); + return eNameOK; +} + +role XULMenubarAccessible::NativeRole() const { return roles::MENUBAR; } + +//////////////////////////////////////////////////////////////////////////////// +// XULMenubarAccessible: Widgets + +bool XULMenubarAccessible::IsActiveWidget() const { + auto* menuBar = dom::XULMenuBarElement::FromNode(GetContent()); + return menuBar && menuBar->IsActive(); +} + +bool XULMenubarAccessible::AreItemsOperable() const { return true; } + +LocalAccessible* XULMenubarAccessible::CurrentItem() const { + auto* content = dom::XULMenuParentElement::FromNode(GetContent()); + MOZ_ASSERT(content); + if (!content || !content->GetActiveMenuChild()) { + return nullptr; + } + return mDoc->GetAccessible(content->GetActiveMenuChild()); +} + +void XULMenubarAccessible::SetCurrentItem(const LocalAccessible* aItem) { + NS_ERROR("XULMenubarAccessible::SetCurrentItem not implemented"); +} |