From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- dom/xul/XULButtonElement.cpp | 802 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 802 insertions(+) create mode 100644 dom/xul/XULButtonElement.cpp (limited to 'dom/xul/XULButtonElement.cpp') diff --git a/dom/xul/XULButtonElement.cpp b/dom/xul/XULButtonElement.cpp new file mode 100644 index 0000000000..2fc0690945 --- /dev/null +++ b/dom/xul/XULButtonElement.cpp @@ -0,0 +1,802 @@ +/* -*- 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 -- cgit v1.2.3