/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "XULButtonElement.h" #include "XULMenuParentElement.h" #include "XULPopupElement.h" #include "mozilla/Assertions.h" #include "mozilla/Attributes.h" #include "mozilla/EventDispatcher.h" #include "mozilla/EventStateManager.h" #include "mozilla/LookAndFeel.h" #include "mozilla/TextEvents.h" #include "mozilla/TimeStamp.h" #include "mozilla/dom/DocumentInlines.h" #include "mozilla/dom/MouseEventBinding.h" #include "mozilla/dom/NameSpaceConstants.h" #include "mozilla/dom/AncestorIterator.h" #include "mozilla/dom/XULMenuBarElement.h" #include "nsGkAtoms.h" #include "nsITimer.h" #include "nsLayoutUtils.h" #include "nsCaseTreatment.h" #include "nsChangeHint.h" #include "nsMenuPopupFrame.h" #include "nsPlaceholderFrame.h" #include "nsPresContext.h" #include "nsXULPopupManager.h" #include "nsIDOMXULButtonElement.h" #include "nsISound.h" namespace mozilla::dom { XULButtonElement::XULButtonElement( already_AddRefed&& aNodeInfo) : nsXULElement(std::move(aNodeInfo)), mIsAlwaysMenu(IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menulist, nsGkAtoms::menuitem)) {} XULButtonElement::~XULButtonElement() { StopBlinking(); KillMenuOpenTimer(); } nsChangeHint XULButtonElement::GetAttributeChangeHint(const nsAtom* aAttribute, int32_t aModType) const { if (aAttribute == nsGkAtoms::type && IsAnyOfXULElements(nsGkAtoms::button, nsGkAtoms::toolbarbutton)) { // type=menu switches to a menu frame. return nsChangeHint_ReconstructFrame; } return nsXULElement::GetAttributeChangeHint(aAttribute, aModType); } // This global flag is used to record the timestamp when a menu was opened or // closed and is used to ignore the mousemove and mouseup events that would fire // on the menu after the mousedown occurred. static TimeStamp gMenuJustOpenedOrClosedTime = TimeStamp(); void XULButtonElement::PopupOpened() { if (!IsMenu()) { return; } gMenuJustOpenedOrClosedTime = TimeStamp::Now(); SetAttr(kNameSpaceID_None, nsGkAtoms::open, u"true"_ns, true); } void XULButtonElement::PopupClosed(bool aDeselectMenu) { if (!IsMenu()) { return; } nsContentUtils::AddScriptRunner( new nsUnsetAttrRunnable(this, nsGkAtoms::open)); if (aDeselectMenu) { if (RefPtr parent = GetMenuParent()) { if (parent->GetActiveMenuChild() == this) { parent->SetActiveMenuChild(nullptr); } } } } bool XULButtonElement::IsMenuActive() const { if (XULMenuParentElement* menu = GetMenuParent()) { return menu->GetActiveMenuChild() == this; } return false; } void XULButtonElement::HandleEnterKeyPress(WidgetEvent& aEvent) { if (IsDisabled()) { #ifdef XP_WIN if (XULPopupElement* popup = GetContainingPopupElement()) { if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) { pm->HidePopup( popup, {HidePopupOption::HideChain, HidePopupOption::DeselectMenu, HidePopupOption::Async}); } } #endif return; } if (IsMenuPopupOpen()) { return; } // The enter key press applies to us. if (IsMenuItem()) { ExecuteMenu(aEvent); } else { OpenMenuPopup(true); } } bool XULButtonElement::IsMenuPopupOpen() { nsMenuPopupFrame* popupFrame = GetMenuPopup(FlushType::None); return popupFrame && popupFrame->IsOpen(); } bool XULButtonElement::IsOnMenu() const { auto* popup = XULPopupElement::FromNodeOrNull(GetMenuParent()); return popup && popup->IsMenu(); } bool XULButtonElement::IsOnMenuList() const { if (XULMenuParentElement* menu = GetMenuParent()) { return menu->GetParent() && menu->GetParent()->IsXULElement(nsGkAtoms::menulist); } return false; } bool XULButtonElement::IsOnMenuBar() const { if (XULMenuParentElement* menu = GetMenuParent()) { return menu->IsMenuBar(); } return false; } nsMenuPopupFrame* XULButtonElement::GetContainingPopupWithoutFlushing() const { if (XULPopupElement* popup = GetContainingPopupElement()) { return do_QueryFrame(popup->GetPrimaryFrame()); } return nullptr; } XULPopupElement* XULButtonElement::GetContainingPopupElement() const { return XULPopupElement::FromNodeOrNull(GetMenuParent()); } bool XULButtonElement::IsOnContextMenu() const { if (nsMenuPopupFrame* popup = GetContainingPopupWithoutFlushing()) { return popup->IsContextMenu(); } return false; } void XULButtonElement::ToggleMenuState() { if (IsMenuPopupOpen()) { CloseMenuPopup(false); } else { OpenMenuPopup(false); } } void XULButtonElement::KillMenuOpenTimer() { if (mMenuOpenTimer) { mMenuOpenTimer->Cancel(); mMenuOpenTimer = nullptr; } } void XULButtonElement::OpenMenuPopup(bool aSelectFirstItem) { nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); if (!pm) { return; } pm->KillMenuTimer(); if (!pm->MayShowMenu(this)) { return; } if (RefPtr parent = GetMenuParent()) { parent->SetActiveMenuChild(this); } // Open the menu asynchronously. OwnerDoc()->Dispatch( TaskCategory::Other, NS_NewRunnableFunction( "AsyncOpenMenu", [self = RefPtr{this}, aSelectFirstItem] { if (self->GetMenuParent() && !self->IsMenuActive()) { return; } if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) { pm->ShowMenu(self, aSelectFirstItem); } })); } void XULButtonElement::CloseMenuPopup(bool aDeselectMenu) { gMenuJustOpenedOrClosedTime = TimeStamp::Now(); // Close the menu asynchronously nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); if (!pm) { return; } if (auto* popup = GetMenuPopupContent()) { HidePopupOptions options{HidePopupOption::Async}; if (aDeselectMenu) { options += HidePopupOption::DeselectMenu; } pm->HidePopup(popup, options); } } int32_t XULButtonElement::MenuOpenCloseDelay() const { if (IsOnMenuBar()) { return 0; } return LookAndFeel::GetInt(LookAndFeel::IntID::SubmenuDelay, 300); // ms } void XULButtonElement::ExecuteMenu(Modifiers aModifiers, int16_t aButton, bool aIsTrusted) { MOZ_ASSERT(IsMenu()); StopBlinking(); auto menuType = GetMenuType(); if (NS_WARN_IF(!menuType)) { return; } // Because the command event is firing asynchronously, a flag is needed to // indicate whether user input is being handled. This ensures that a popup // window won't get blocked. const bool userinput = dom::UserActivation::IsHandlingUserInput(); // Flip "checked" state if we're a checkbox menu, or an un-checked radio menu. bool needToFlipChecked = false; if (*menuType == MenuType::Checkbox || (*menuType == MenuType::Radio && !GetXULBoolAttr(nsGkAtoms::checked))) { needToFlipChecked = !AttrValueIs(kNameSpaceID_None, nsGkAtoms::autocheck, nsGkAtoms::_false, eCaseMatters); } mDelayedMenuCommandEvent = new nsXULMenuCommandEvent( this, aIsTrusted, aModifiers, userinput, needToFlipChecked, aButton); StartBlinking(); } void XULButtonElement::StopBlinking() { if (mMenuBlinkTimer) { if (auto* parent = GetMenuParent()) { parent->LockMenuUntilClosed(false); } mMenuBlinkTimer->Cancel(); mMenuBlinkTimer = nullptr; } mDelayedMenuCommandEvent = nullptr; } void XULButtonElement::PassMenuCommandEventToPopupManager() { if (mDelayedMenuCommandEvent) { if (RefPtr pm = nsXULPopupManager::GetInstance()) { RefPtr event = std::move(mDelayedMenuCommandEvent); nsCOMPtr content = this; pm->ExecuteMenu(content, event); } } mDelayedMenuCommandEvent = nullptr; } static constexpr int32_t kBlinkDelay = 67; // milliseconds void XULButtonElement::StartBlinking() { if (!LookAndFeel::GetInt(LookAndFeel::IntID::ChosenMenuItemsShouldBlink)) { PassMenuCommandEventToPopupManager(); return; } // Blink off. UnsetAttr(kNameSpaceID_None, nsGkAtoms::menuactive, true); if (auto* parent = GetMenuParent()) { // Make this menu ignore events from now on. parent->LockMenuUntilClosed(true); } // Set up a timer to blink back on. NS_NewTimerWithFuncCallback( getter_AddRefs(mMenuBlinkTimer), [](nsITimer*, void* aClosure) MOZ_CAN_RUN_SCRIPT_BOUNDARY { RefPtr self = static_cast(aClosure); if (auto* parent = self->GetMenuParent()) { if (parent->GetActiveMenuChild() == self) { // Restore the highlighting if we're still the active item. self->SetAttr(kNameSpaceID_None, nsGkAtoms::menuactive, u"true"_ns, true); } } // Reuse our timer to actually execute. self->mMenuBlinkTimer->InitWithNamedFuncCallback( [](nsITimer*, void* aClosure) MOZ_CAN_RUN_SCRIPT_BOUNDARY { RefPtr self = static_cast(aClosure); if (auto* parent = self->GetMenuParent()) { parent->LockMenuUntilClosed(false); } self->PassMenuCommandEventToPopupManager(); self->StopBlinking(); }, aClosure, kBlinkDelay, nsITimer::TYPE_ONE_SHOT, "XULButtonElement::ContinueBlinking"); }, this, kBlinkDelay, nsITimer::TYPE_ONE_SHOT, "XULButtonElement::StartBlinking", OwnerDoc()->EventTargetFor(TaskCategory::Other)); } void XULButtonElement::UnbindFromTree(bool aNullParent) { StopBlinking(); nsXULElement::UnbindFromTree(aNullParent); } void XULButtonElement::ExecuteMenu(WidgetEvent& aEvent) { MOZ_ASSERT(IsMenu()); if (nsCOMPtr sound = do_GetService("@mozilla.org/sound;1")) { sound->PlayEventSound(nsISound::EVENT_MENU_EXECUTE); } Modifiers modifiers = 0; if (WidgetInputEvent* inputEvent = aEvent.AsInputEvent()) { modifiers = inputEvent->mModifiers; } int16_t button = 0; if (WidgetMouseEventBase* mouseEvent = aEvent.AsMouseEventBase()) { button = mouseEvent->mButton; } ExecuteMenu(modifiers, button, aEvent.IsTrusted()); } void XULButtonElement::PostHandleEventForMenus( EventChainPostVisitor& aVisitor) { auto* event = aVisitor.mEvent; if (event->mOriginalTarget != this) { return; } if (auto* parent = GetMenuParent()) { if (NS_WARN_IF(parent->IsLocked())) { return; } } // If a menu just opened, ignore the mouseup event that might occur after a // the mousedown event that opened it. However, if a different mousedown event // occurs, just clear this flag. if (!gMenuJustOpenedOrClosedTime.IsNull()) { if (event->mMessage == eMouseDown) { gMenuJustOpenedOrClosedTime = TimeStamp(); } else if (event->mMessage == eMouseUp) { return; } } if (event->mMessage == eKeyPress && !IsDisabled()) { WidgetKeyboardEvent* keyEvent = event->AsKeyboardEvent(); uint32_t keyCode = keyEvent->mKeyCode; #ifdef XP_MACOSX // On mac, open menulist on either up/down arrow or space (w/o Cmd pressed) if (!IsMenuPopupOpen() && ((keyEvent->mCharCode == ' ' && !keyEvent->IsMeta()) || (keyCode == NS_VK_UP || keyCode == NS_VK_DOWN))) { // When pressing space, don't open the menu if performing an incremental // search. if (keyEvent->mCharCode != ' ' || !nsMenuPopupFrame::IsWithinIncrementalTime(keyEvent->mTimeStamp)) { aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; OpenMenuPopup(false); } } #else // On other platforms, toggle menulist on unmodified F4 or Alt arrow if ((keyCode == NS_VK_F4 && !keyEvent->IsAlt()) || ((keyCode == NS_VK_UP || keyCode == NS_VK_DOWN) && keyEvent->IsAlt())) { aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; ToggleMenuState(); } #endif } else if (event->mMessage == eMouseDown && event->AsMouseEvent()->mButton == MouseButton::ePrimary && #ifdef XP_MACOSX // On mac, ctrl-click will send a context menu event from the // widget, so we don't want to bring up the menu. !event->AsMouseEvent()->IsControl() && #endif !IsDisabled() && !IsMenuItem()) { // The menu item was selected. Bring up the menu. // We have children. // Don't prevent the default action here, since that will also cancel // potential drag starts. if (!IsOnMenu()) { ToggleMenuState(); } else if (!IsMenuPopupOpen()) { OpenMenuPopup(false); } } else if (event->mMessage == eMouseUp && IsMenuItem() && !IsDisabled() && !event->mFlags.mMultipleActionsPrevented) { // We accept left and middle clicks on all menu items to activate the item. // On context menus we also accept right click to activate the item, because // right-clicking on an item in a context menu cannot open another context // menu. bool isMacCtrlClick = false; #ifdef XP_MACOSX isMacCtrlClick = event->AsMouseEvent()->mButton == MouseButton::ePrimary && event->AsMouseEvent()->IsControl(); #endif bool clickMightOpenContextMenu = event->AsMouseEvent()->mButton == MouseButton::eSecondary || isMacCtrlClick; if (!clickMightOpenContextMenu || IsOnContextMenu()) { aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; ExecuteMenu(*event); } } else if (event->mMessage == eContextMenu && IsOnContextMenu() && !IsMenuItem() && !IsDisabled()) { // Make sure we cancel default processing of the context menu event so // that it doesn't bubble and get seen again by the popuplistener and show // another context menu. aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; } else if (event->mMessage == eMouseOut) { KillMenuOpenTimer(); if (RefPtr parent = GetMenuParent()) { if (parent->GetActiveMenuChild() == this) { // Deactivate the menu on mouse out in some cases... const bool shouldDeactivate = [&] { if (IsMenuPopupOpen()) { // If we're open we never deselect. PopupClosed will do as needed. return false; } if (auto* menubar = XULMenuBarElement::FromNode(*parent)) { // De-select when exiting a menubar item, if the menubar wasn't // activated by keyboard. return !menubar->IsActiveByKeyboard(); } if (IsOnMenuList()) { // Don't de-select if on a menu-list. That matches Chromium and our // historical Windows behavior, see bug 1197913. return false; } // De-select elsewhere. return true; }(); if (shouldDeactivate) { parent->SetActiveMenuChild(nullptr); } } } } else if (event->mMessage == eMouseMove && (IsOnMenu() || IsOnMenuBar())) { // Use a tolerance to address situations where a user might perform a // "wiggly" click that is accompanied by near-simultaneous mousemove events. const TimeDuration kTolerance = TimeDuration::FromMilliseconds(200); if (!gMenuJustOpenedOrClosedTime.IsNull() && gMenuJustOpenedOrClosedTime + kTolerance < TimeStamp::Now()) { gMenuJustOpenedOrClosedTime = TimeStamp(); return; } if (IsDisabled() && IsOnMenuList()) { return; } RefPtr parent = GetMenuParent(); MOZ_ASSERT(parent, "How did IsOnMenu{,Bar} return true then?"); const bool isOnOpenMenubar = parent->IsMenuBar() && parent->GetActiveMenuChild() && parent->GetActiveMenuChild()->IsMenuPopupOpen(); parent->SetActiveMenuChild(this); // We need to check if we really became the current menu item or not. if (!IsMenuActive()) { // We didn't (presumably because a context menu was active) return; } if (IsDisabled() || IsMenuItem() || IsMenuPopupOpen() || mMenuOpenTimer) { // Disabled, or already opening or what not. return; } if (parent->IsMenuBar() && !isOnOpenMenubar) { // We should only open on hover in the menubar iff the menubar is open // already. return; } // A timer is used so that it doesn't open if the user moves the mouse // quickly past the menu. The MenuOpenCloseDelay ensures that only menus // have this behaviour. NS_NewTimerWithFuncCallback( getter_AddRefs(mMenuOpenTimer), [](nsITimer*, void* aClosure) MOZ_CAN_RUN_SCRIPT_BOUNDARY { RefPtr self = static_cast(aClosure); self->mMenuOpenTimer = nullptr; if (self->IsMenuPopupOpen()) { return; } // make sure we didn't open a context menu in the meantime // (i.e. the user right-clicked while hovering over a submenu). nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); if (!pm) { return; } if (pm->HasContextMenu(nullptr) && !self->IsOnContextMenu()) { return; } if (!self->IsMenuActive()) { return; } self->OpenMenuPopup(false); }, this, MenuOpenCloseDelay(), nsITimer::TYPE_ONE_SHOT, "XULButtonElement::OpenMenu", OwnerDoc()->EventTargetFor(TaskCategory::Other)); } } nsresult XULButtonElement::PostHandleEvent(EventChainPostVisitor& aVisitor) { if (aVisitor.mEventStatus == nsEventStatus_eConsumeNoDefault) { return nsXULElement::PostHandleEvent(aVisitor); } if (IsMenu()) { PostHandleEventForMenus(aVisitor); return nsXULElement::PostHandleEvent(aVisitor); } auto* event = aVisitor.mEvent; switch (event->mMessage) { case eBlur: { Blurred(); break; } case eKeyDown: { WidgetKeyboardEvent* keyEvent = event->AsKeyboardEvent(); if (!keyEvent) { break; } if (keyEvent->ShouldWorkAsSpaceKey() && aVisitor.mPresContext) { EventStateManager* esm = aVisitor.mPresContext->EventStateManager(); // :hover:active state esm->SetContentState(this, ElementState::HOVER); esm->SetContentState(this, ElementState::ACTIVE); mIsHandlingKeyEvent = true; } break; } // On mac, Return fires the default button, not the focused one. #ifndef XP_MACOSX case eKeyPress: { WidgetKeyboardEvent* keyEvent = event->AsKeyboardEvent(); if (!keyEvent) { break; } if (NS_VK_RETURN == keyEvent->mKeyCode) { if (RefPtr button = AsXULButton()) { if (MouseClicked(*keyEvent)) { aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; } } } break; } #endif case eKeyUp: { WidgetKeyboardEvent* keyEvent = event->AsKeyboardEvent(); if (!keyEvent) { break; } if (keyEvent->ShouldWorkAsSpaceKey()) { mIsHandlingKeyEvent = false; ElementState buttonState = State(); if (buttonState.HasAllStates(ElementState::ACTIVE | ElementState::HOVER) && aVisitor.mPresContext) { // return to normal state EventStateManager* esm = aVisitor.mPresContext->EventStateManager(); esm->SetContentState(nullptr, ElementState::ACTIVE); esm->SetContentState(nullptr, ElementState::HOVER); if (MouseClicked(*keyEvent)) { aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; } } } break; } case eMouseClick: { WidgetMouseEvent* mouseEvent = event->AsMouseEvent(); if (mouseEvent->IsLeftClickEvent()) { if (MouseClicked(*mouseEvent)) { aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; } } break; } default: break; } return nsXULElement::PostHandleEvent(aVisitor); } void XULButtonElement::Blurred() { ElementState buttonState = State(); if (mIsHandlingKeyEvent && buttonState.HasAllStates(ElementState::ACTIVE | ElementState::HOVER)) { // Return to normal state if (nsPresContext* pc = OwnerDoc()->GetPresContext()) { EventStateManager* esm = pc->EventStateManager(); esm->SetContentState(nullptr, ElementState::ACTIVE); esm->SetContentState(nullptr, ElementState::HOVER); } } mIsHandlingKeyEvent = false; } bool XULButtonElement::MouseClicked(WidgetGUIEvent& aEvent) { // Don't execute if we're disabled. if (IsDisabled() || !IsInComposedDoc()) { return false; } // Have the content handle the event, propagating it according to normal DOM // rules. RefPtr presShell = OwnerDoc()->GetPresShell(); if (!presShell) { return false; } // Execute the oncommand event handler. WidgetInputEvent* inputEvent = aEvent.AsInputEvent(); WidgetMouseEventBase* mouseEvent = aEvent.AsMouseEventBase(); WidgetKeyboardEvent* keyEvent = aEvent.AsKeyboardEvent(); // TODO: Set aSourceEvent? nsContentUtils::DispatchXULCommand( this, aEvent.IsTrusted(), /* aSourceEvent = */ nullptr, presShell, inputEvent->IsControl(), inputEvent->IsAlt(), inputEvent->IsShift(), inputEvent->IsMeta(), mouseEvent ? mouseEvent->mInputSource : (keyEvent ? MouseEvent_Binding::MOZ_SOURCE_KEYBOARD : MouseEvent_Binding::MOZ_SOURCE_UNKNOWN), mouseEvent ? mouseEvent->mButton : 0); return true; } bool XULButtonElement::IsMenu() const { if (mIsAlwaysMenu) { return true; } return IsAnyOfXULElements(nsGkAtoms::button, nsGkAtoms::toolbarbutton) && AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, nsGkAtoms::menu, eCaseMatters); } void XULButtonElement::UncheckRadioSiblings() { MOZ_ASSERT(!nsContentUtils::IsSafeToRunScript()); MOZ_ASSERT(GetMenuType() == Some(MenuType::Radio)); nsAutoString groupName; GetAttr(nsGkAtoms::name, groupName); nsIContent* parent = GetParent(); if (!parent) { return; } auto ShouldUncheck = [&](const nsIContent& aSibling) { const auto* button = XULButtonElement::FromNode(aSibling); if (!button || button->GetMenuType() != Some(MenuType::Radio)) { return false; } if (const auto* attr = button->GetParsedAttr(nsGkAtoms::name)) { if (!attr->Equals(groupName, eCaseMatters)) { return false; } } else if (!groupName.IsEmpty()) { return false; } // we're in the same group, only uncheck if we're checked (for some reason, // some tests rely on that specifically). return button->GetXULBoolAttr(nsGkAtoms::checked); }; for (nsIContent* child = parent->GetFirstChild(); child; child = child->GetNextSibling()) { if (child == this || !ShouldUncheck(*child)) { continue; } child->AsElement()->UnsetAttr(nsGkAtoms::checked, IgnoreErrors()); } } void XULButtonElement::AfterSetAttr(int32_t aNamespaceID, nsAtom* aName, const nsAttrValue* aValue, const nsAttrValue* aOldValue, nsIPrincipal* aSubjectPrincipal, bool aNotify) { nsXULElement::AfterSetAttr(aNamespaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify); if (IsAlwaysMenu() && aNamespaceID == kNameSpaceID_None) { // We need to uncheck radio siblings when we're a checked radio and switch // groups, or become checked. const bool shouldUncheckSiblings = [&] { if (aName == nsGkAtoms::type || aName == nsGkAtoms::name) { return *GetMenuType() == MenuType::Radio && GetXULBoolAttr(nsGkAtoms::checked); } if (aName == nsGkAtoms::checked && aValue && aValue->Equals(nsGkAtoms::_true, eCaseMatters)) { return *GetMenuType() == MenuType::Radio; } return false; }(); if (shouldUncheckSiblings) { UncheckRadioSiblings(); } } } auto XULButtonElement::GetMenuType() const -> Maybe { if (!IsAlwaysMenu()) { return Nothing(); } static Element::AttrValuesArray values[] = {nsGkAtoms::checkbox, nsGkAtoms::radio, nullptr}; switch (FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::type, values, eCaseMatters)) { case 0: return Some(MenuType::Checkbox); case 1: return Some(MenuType::Radio); default: return Some(MenuType::Normal); } } XULMenuBarElement* XULButtonElement::GetMenuBar() const { if (!IsMenu()) { return nullptr; } return FirstAncestorOfType(); } XULMenuParentElement* XULButtonElement::GetMenuParent() const { if (IsXULElement(nsGkAtoms::menulist)) { return nullptr; } return FirstAncestorOfType(); } XULPopupElement* XULButtonElement::GetMenuPopupContent() const { if (!IsMenu()) { return nullptr; } for (auto* child = GetFirstChild(); child; child = child->GetNextSibling()) { if (auto* popup = XULPopupElement::FromNode(child)) { return popup; } } return nullptr; } nsMenuPopupFrame* XULButtonElement::GetMenuPopupWithoutFlushing() const { return const_cast(this)->GetMenuPopup(FlushType::None); } nsMenuPopupFrame* XULButtonElement::GetMenuPopup(FlushType aFlushType) { RefPtr popup = GetMenuPopupContent(); if (!popup) { return nullptr; } return do_QueryFrame(popup->GetPrimaryFrame(aFlushType)); } bool XULButtonElement::OpenedWithKey() const { auto* menubar = GetMenuBar(); return menubar && menubar->IsActiveByKeyboard(); } } // namespace mozilla::dom