summaryrefslogtreecommitdiffstats
path: root/dom/xul/XULButtonElement.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/xul/XULButtonElement.cpp')
-rw-r--r--dom/xul/XULButtonElement.cpp802
1 files changed, 802 insertions, 0 deletions
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<mozilla::dom::NodeInfo>&& 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<XULMenuParentElement> 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<XULMenuParentElement> 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<nsXULPopupManager> pm = nsXULPopupManager::GetInstance()) {
+ RefPtr<nsXULMenuCommandEvent> event = std::move(mDelayedMenuCommandEvent);
+ nsCOMPtr<nsIContent> 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<XULButtonElement*>(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<XULButtonElement*>(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<nsISound> 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<XULMenuParentElement> 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<XULMenuParentElement> 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<XULButtonElement*>(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<nsIDOMXULButtonElement> 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<mozilla::PresShell> 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<MenuType> {
+ 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<XULMenuBarElement>();
+}
+
+XULMenuParentElement* XULButtonElement::GetMenuParent() const {
+ if (IsXULElement(nsGkAtoms::menulist)) {
+ return nullptr;
+ }
+ return FirstAncestorOfType<XULMenuParentElement>();
+}
+
+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<XULButtonElement*>(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