no bug - Import translations from android-l10n r=release a=l10n CLOSED TREE
[gecko.git] / dom / xul / XULButtonElement.cpp
blob6c76488db4b94a09f9bdbf36df903f7e74f3d4ab
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "XULButtonElement.h"
8 #include "XULMenuParentElement.h"
9 #include "XULPopupElement.h"
10 #include "mozilla/Assertions.h"
11 #include "mozilla/Attributes.h"
12 #include "mozilla/EventDispatcher.h"
13 #include "mozilla/EventStateManager.h"
14 #include "mozilla/LookAndFeel.h"
15 #include "mozilla/TextEvents.h"
16 #include "mozilla/TimeStamp.h"
17 #include "mozilla/dom/DocumentInlines.h"
18 #include "mozilla/dom/MouseEventBinding.h"
19 #include "mozilla/dom/NameSpaceConstants.h"
20 #include "mozilla/dom/AncestorIterator.h"
21 #include "mozilla/dom/XULMenuBarElement.h"
22 #include "nsGkAtoms.h"
23 #include "nsITimer.h"
24 #include "nsLayoutUtils.h"
25 #include "nsCaseTreatment.h"
26 #include "nsChangeHint.h"
27 #include "nsMenuPopupFrame.h"
28 #include "nsPlaceholderFrame.h"
29 #include "nsPresContext.h"
30 #include "nsXULPopupManager.h"
31 #include "nsIDOMXULButtonElement.h"
32 #include "nsISound.h"
34 namespace mozilla::dom {
36 XULButtonElement::XULButtonElement(
37 already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
38 : nsXULElement(std::move(aNodeInfo)),
39 mIsAlwaysMenu(IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menulist,
40 nsGkAtoms::menuitem)) {}
42 XULButtonElement::~XULButtonElement() {
43 StopBlinking();
44 KillMenuOpenTimer();
47 nsChangeHint XULButtonElement::GetAttributeChangeHint(const nsAtom* aAttribute,
48 int32_t aModType) const {
49 if (aAttribute == nsGkAtoms::type &&
50 IsAnyOfXULElements(nsGkAtoms::button, nsGkAtoms::toolbarbutton)) {
51 // type=menu switches to a menu frame.
52 return nsChangeHint_ReconstructFrame;
54 return nsXULElement::GetAttributeChangeHint(aAttribute, aModType);
57 // This global flag is used to record the timestamp when a menu was opened or
58 // closed and is used to ignore the mousemove and mouseup events that would fire
59 // on the menu after the mousedown occurred.
60 static TimeStamp gMenuJustOpenedOrClosedTime = TimeStamp();
62 void XULButtonElement::PopupOpened() {
63 if (!IsMenu()) {
64 return;
66 gMenuJustOpenedOrClosedTime = TimeStamp::Now();
67 SetAttr(kNameSpaceID_None, nsGkAtoms::open, u"true"_ns, true);
70 void XULButtonElement::PopupClosed(bool aDeselectMenu) {
71 if (!IsMenu()) {
72 return;
74 nsContentUtils::AddScriptRunner(
75 new nsUnsetAttrRunnable(this, nsGkAtoms::open));
77 if (aDeselectMenu) {
78 if (RefPtr<XULMenuParentElement> parent = GetMenuParent()) {
79 if (parent->GetActiveMenuChild() == this) {
80 parent->SetActiveMenuChild(nullptr);
86 bool XULButtonElement::IsMenuActive() const {
87 if (XULMenuParentElement* menu = GetMenuParent()) {
88 return menu->GetActiveMenuChild() == this;
90 return false;
93 void XULButtonElement::HandleEnterKeyPress(WidgetEvent& aEvent) {
94 if (IsDisabled()) {
95 #ifdef XP_WIN
96 if (XULPopupElement* popup = GetContainingPopupElement()) {
97 if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) {
98 pm->HidePopup(
99 popup, {HidePopupOption::HideChain, HidePopupOption::DeselectMenu,
100 HidePopupOption::Async});
103 #endif
104 return;
106 if (IsMenuPopupOpen()) {
107 return;
109 // The enter key press applies to us.
110 if (IsMenuItem()) {
111 ExecuteMenu(aEvent);
112 } else {
113 OpenMenuPopup(true);
117 bool XULButtonElement::IsMenuPopupOpen() {
118 nsMenuPopupFrame* popupFrame = GetMenuPopup(FlushType::None);
119 return popupFrame && popupFrame->IsOpen();
122 bool XULButtonElement::IsOnMenu() const {
123 auto* popup = XULPopupElement::FromNodeOrNull(GetMenuParent());
124 return popup && popup->IsMenu();
127 bool XULButtonElement::IsOnMenuList() const {
128 if (XULMenuParentElement* menu = GetMenuParent()) {
129 return menu->GetParent() &&
130 menu->GetParent()->IsXULElement(nsGkAtoms::menulist);
132 return false;
135 bool XULButtonElement::IsOnMenuBar() const {
136 if (XULMenuParentElement* menu = GetMenuParent()) {
137 return menu->IsMenuBar();
139 return false;
142 nsMenuPopupFrame* XULButtonElement::GetContainingPopupWithoutFlushing() const {
143 if (XULPopupElement* popup = GetContainingPopupElement()) {
144 return do_QueryFrame(popup->GetPrimaryFrame());
146 return nullptr;
149 XULPopupElement* XULButtonElement::GetContainingPopupElement() const {
150 return XULPopupElement::FromNodeOrNull(GetMenuParent());
153 bool XULButtonElement::IsOnContextMenu() const {
154 if (nsMenuPopupFrame* popup = GetContainingPopupWithoutFlushing()) {
155 return popup->IsContextMenu();
157 return false;
160 void XULButtonElement::ToggleMenuState() {
161 if (IsMenuPopupOpen()) {
162 CloseMenuPopup(false);
163 } else {
164 OpenMenuPopup(false);
168 void XULButtonElement::KillMenuOpenTimer() {
169 if (mMenuOpenTimer) {
170 mMenuOpenTimer->Cancel();
171 mMenuOpenTimer = nullptr;
175 void XULButtonElement::OpenMenuPopup(bool aSelectFirstItem) {
176 nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
177 if (!pm) {
178 return;
181 pm->KillMenuTimer();
182 if (!pm->MayShowMenu(this)) {
183 return;
186 if (RefPtr<XULMenuParentElement> parent = GetMenuParent()) {
187 parent->SetActiveMenuChild(this);
190 // Open the menu asynchronously.
191 OwnerDoc()->Dispatch(NS_NewRunnableFunction(
192 "AsyncOpenMenu", [self = RefPtr{this}, aSelectFirstItem] {
193 if (self->GetMenuParent() && !self->IsMenuActive()) {
194 return;
196 if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) {
197 pm->ShowMenu(self, aSelectFirstItem);
199 }));
202 void XULButtonElement::CloseMenuPopup(bool aDeselectMenu) {
203 gMenuJustOpenedOrClosedTime = TimeStamp::Now();
204 // Close the menu asynchronously
205 nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
206 if (!pm) {
207 return;
209 if (auto* popup = GetMenuPopupContent()) {
210 HidePopupOptions options{HidePopupOption::Async};
211 if (aDeselectMenu) {
212 options += HidePopupOption::DeselectMenu;
214 pm->HidePopup(popup, options);
218 int32_t XULButtonElement::MenuOpenCloseDelay() const {
219 if (IsOnMenuBar()) {
220 return 0;
222 return LookAndFeel::GetInt(LookAndFeel::IntID::SubmenuDelay, 300); // ms
225 void XULButtonElement::ExecuteMenu(Modifiers aModifiers, int16_t aButton,
226 bool aIsTrusted) {
227 MOZ_ASSERT(IsMenu());
229 StopBlinking();
231 auto menuType = GetMenuType();
232 if (NS_WARN_IF(!menuType)) {
233 return;
236 // Because the command event is firing asynchronously, a flag is needed to
237 // indicate whether user input is being handled. This ensures that a popup
238 // window won't get blocked.
239 const bool userinput = dom::UserActivation::IsHandlingUserInput();
241 // Flip "checked" state if we're a checkbox menu, or an un-checked radio menu.
242 bool needToFlipChecked = false;
243 if (*menuType == MenuType::Checkbox ||
244 (*menuType == MenuType::Radio && !GetXULBoolAttr(nsGkAtoms::checked))) {
245 needToFlipChecked = !AttrValueIs(kNameSpaceID_None, nsGkAtoms::autocheck,
246 nsGkAtoms::_false, eCaseMatters);
249 mDelayedMenuCommandEvent = new nsXULMenuCommandEvent(
250 this, aIsTrusted, aModifiers, userinput, needToFlipChecked, aButton);
251 StartBlinking();
254 void XULButtonElement::StopBlinking() {
255 if (mMenuBlinkTimer) {
256 if (auto* parent = GetMenuParent()) {
257 parent->LockMenuUntilClosed(false);
259 mMenuBlinkTimer->Cancel();
260 mMenuBlinkTimer = nullptr;
262 mDelayedMenuCommandEvent = nullptr;
265 void XULButtonElement::PassMenuCommandEventToPopupManager() {
266 if (mDelayedMenuCommandEvent) {
267 if (RefPtr<nsXULPopupManager> pm = nsXULPopupManager::GetInstance()) {
268 RefPtr<nsXULMenuCommandEvent> event = std::move(mDelayedMenuCommandEvent);
269 nsCOMPtr<nsIContent> content = this;
270 pm->ExecuteMenu(content, event);
273 mDelayedMenuCommandEvent = nullptr;
276 static constexpr int32_t kBlinkDelay = 67; // milliseconds
278 void XULButtonElement::StartBlinking() {
279 if (!LookAndFeel::GetInt(LookAndFeel::IntID::ChosenMenuItemsShouldBlink)) {
280 PassMenuCommandEventToPopupManager();
281 return;
284 // Blink off.
285 UnsetAttr(kNameSpaceID_None, nsGkAtoms::menuactive, true);
286 if (auto* parent = GetMenuParent()) {
287 // Make this menu ignore events from now on.
288 parent->LockMenuUntilClosed(true);
291 // Set up a timer to blink back on.
292 NS_NewTimerWithFuncCallback(
293 getter_AddRefs(mMenuBlinkTimer),
294 [](nsITimer*, void* aClosure) MOZ_CAN_RUN_SCRIPT_BOUNDARY {
295 RefPtr self = static_cast<XULButtonElement*>(aClosure);
296 if (auto* parent = self->GetMenuParent()) {
297 if (parent->GetActiveMenuChild() == self) {
298 // Restore the highlighting if we're still the active item.
299 self->SetAttr(kNameSpaceID_None, nsGkAtoms::menuactive, u"true"_ns,
300 true);
303 // Reuse our timer to actually execute.
304 self->mMenuBlinkTimer->InitWithNamedFuncCallback(
305 [](nsITimer*, void* aClosure) MOZ_CAN_RUN_SCRIPT_BOUNDARY {
306 RefPtr self = static_cast<XULButtonElement*>(aClosure);
307 if (auto* parent = self->GetMenuParent()) {
308 parent->LockMenuUntilClosed(false);
310 self->PassMenuCommandEventToPopupManager();
311 self->StopBlinking();
313 aClosure, kBlinkDelay, nsITimer::TYPE_ONE_SHOT,
314 "XULButtonElement::ContinueBlinking");
316 this, kBlinkDelay, nsITimer::TYPE_ONE_SHOT,
317 "XULButtonElement::StartBlinking", GetMainThreadSerialEventTarget());
320 void XULButtonElement::UnbindFromTree(UnbindContext& aContext) {
321 StopBlinking();
322 nsXULElement::UnbindFromTree(aContext);
325 void XULButtonElement::ExecuteMenu(WidgetEvent& aEvent) {
326 MOZ_ASSERT(IsMenu());
327 if (nsCOMPtr<nsISound> sound = do_GetService("@mozilla.org/sound;1")) {
328 sound->PlayEventSound(nsISound::EVENT_MENU_EXECUTE);
331 Modifiers modifiers = 0;
332 if (WidgetInputEvent* inputEvent = aEvent.AsInputEvent()) {
333 modifiers = inputEvent->mModifiers;
336 int16_t button = 0;
337 if (WidgetMouseEventBase* mouseEvent = aEvent.AsMouseEventBase()) {
338 button = mouseEvent->mButton;
341 ExecuteMenu(modifiers, button, aEvent.IsTrusted());
344 void XULButtonElement::PostHandleEventForMenus(
345 EventChainPostVisitor& aVisitor) {
346 auto* event = aVisitor.mEvent;
348 if (event->mOriginalTarget != this) {
349 return;
352 if (auto* parent = GetMenuParent()) {
353 if (NS_WARN_IF(parent->IsLocked())) {
354 return;
358 // If a menu just opened, ignore the mouseup event that might occur after a
359 // the mousedown event that opened it. However, if a different mousedown event
360 // occurs, just clear this flag.
361 if (!gMenuJustOpenedOrClosedTime.IsNull()) {
362 if (event->mMessage == eMouseDown) {
363 gMenuJustOpenedOrClosedTime = TimeStamp();
364 } else if (event->mMessage == eMouseUp) {
365 return;
369 if (event->mMessage == eKeyPress && !IsDisabled()) {
370 WidgetKeyboardEvent* keyEvent = event->AsKeyboardEvent();
371 uint32_t keyCode = keyEvent->mKeyCode;
372 #ifdef XP_MACOSX
373 // On mac, open menulist on either up/down arrow or space (w/o Cmd pressed)
374 if (!IsMenuPopupOpen() &&
375 ((keyEvent->mCharCode == ' ' && !keyEvent->IsMeta()) ||
376 (keyCode == NS_VK_UP || keyCode == NS_VK_DOWN))) {
377 // When pressing space, don't open the menu if performing an incremental
378 // search.
379 if (keyEvent->mCharCode != ' ' ||
380 !nsMenuPopupFrame::IsWithinIncrementalTime(keyEvent->mTimeStamp)) {
381 aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
382 OpenMenuPopup(false);
385 #else
386 // On other platforms, toggle menulist on unmodified F4 or Alt arrow
387 if ((keyCode == NS_VK_F4 && !keyEvent->IsAlt()) ||
388 ((keyCode == NS_VK_UP || keyCode == NS_VK_DOWN) && keyEvent->IsAlt())) {
389 aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
390 ToggleMenuState();
392 #endif
393 } else if (event->mMessage == eMouseDown &&
394 event->AsMouseEvent()->mButton == MouseButton::ePrimary &&
395 #ifdef XP_MACOSX
396 // On mac, ctrl-click will send a context menu event from the
397 // widget, so we don't want to bring up the menu.
398 !event->AsMouseEvent()->IsControl() &&
399 #endif
400 !IsDisabled() && !IsMenuItem()) {
401 // The menu item was selected. Bring up the menu.
402 // We have children.
403 // Don't prevent the default action here, since that will also cancel
404 // potential drag starts.
405 if (!IsOnMenu()) {
406 ToggleMenuState();
407 } else if (!IsMenuPopupOpen()) {
408 OpenMenuPopup(false);
410 } else if (event->mMessage == eMouseUp && IsMenuItem() && !IsDisabled() &&
411 !event->mFlags.mMultipleActionsPrevented) {
412 // We accept left and middle clicks on all menu items to activate the item.
413 // On context menus we also accept right click to activate the item, because
414 // right-clicking on an item in a context menu cannot open another context
415 // menu.
416 bool isMacCtrlClick = false;
417 #ifdef XP_MACOSX
418 isMacCtrlClick = event->AsMouseEvent()->mButton == MouseButton::ePrimary &&
419 event->AsMouseEvent()->IsControl();
420 #endif
421 bool clickMightOpenContextMenu =
422 event->AsMouseEvent()->mButton == MouseButton::eSecondary ||
423 isMacCtrlClick;
424 if (!clickMightOpenContextMenu || IsOnContextMenu()) {
425 aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
426 ExecuteMenu(*event);
428 } else if (event->mMessage == eContextMenu && IsOnContextMenu() &&
429 !IsMenuItem() && !IsDisabled()) {
430 // Make sure we cancel default processing of the context menu event so
431 // that it doesn't bubble and get seen again by the popuplistener and show
432 // another context menu.
433 aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
434 } else if (event->mMessage == eMouseOut) {
435 KillMenuOpenTimer();
436 if (RefPtr<XULMenuParentElement> parent = GetMenuParent()) {
437 if (parent->GetActiveMenuChild() == this) {
438 // Deactivate the menu on mouse out in some cases...
439 const bool shouldDeactivate = [&] {
440 if (IsMenuPopupOpen()) {
441 // If we're open we never deselect. PopupClosed will do as needed.
442 return false;
444 if (auto* menubar = XULMenuBarElement::FromNode(*parent)) {
445 // De-select when exiting a menubar item, if the menubar wasn't
446 // activated by keyboard.
447 return !menubar->IsActiveByKeyboard();
449 if (IsOnMenuList()) {
450 // Don't de-select if on a menu-list. That matches Chromium and our
451 // historical Windows behavior, see bug 1197913.
452 return false;
454 // De-select elsewhere.
455 return true;
456 }();
458 if (shouldDeactivate) {
459 parent->SetActiveMenuChild(nullptr);
463 } else if (event->mMessage == eMouseMove && (IsOnMenu() || IsOnMenuBar())) {
464 // Use a tolerance to address situations where a user might perform a
465 // "wiggly" click that is accompanied by near-simultaneous mousemove events.
466 const TimeDuration kTolerance = TimeDuration::FromMilliseconds(200);
467 if (!gMenuJustOpenedOrClosedTime.IsNull() &&
468 gMenuJustOpenedOrClosedTime + kTolerance < TimeStamp::Now()) {
469 gMenuJustOpenedOrClosedTime = TimeStamp();
470 return;
473 if (IsDisabled() && IsOnMenuList()) {
474 return;
477 RefPtr<XULMenuParentElement> parent = GetMenuParent();
478 MOZ_ASSERT(parent, "How did IsOnMenu{,Bar} return true then?");
480 const bool isOnOpenMenubar =
481 parent->IsMenuBar() && parent->GetActiveMenuChild() &&
482 parent->GetActiveMenuChild()->IsMenuPopupOpen();
484 parent->SetActiveMenuChild(this);
486 // We need to check if we really became the current menu item or not.
487 if (!IsMenuActive()) {
488 // We didn't (presumably because a context menu was active)
489 return;
491 if (IsDisabled() || IsMenuItem() || IsMenuPopupOpen() || mMenuOpenTimer) {
492 // Disabled, or already opening or what not.
493 return;
496 if (parent->IsMenuBar() && !isOnOpenMenubar) {
497 // We should only open on hover in the menubar iff the menubar is open
498 // already.
499 return;
502 // A timer is used so that it doesn't open if the user moves the mouse
503 // quickly past the menu. The MenuOpenCloseDelay ensures that only menus
504 // have this behaviour.
505 NS_NewTimerWithFuncCallback(
506 getter_AddRefs(mMenuOpenTimer),
507 [](nsITimer*, void* aClosure) MOZ_CAN_RUN_SCRIPT_BOUNDARY {
508 RefPtr self = static_cast<XULButtonElement*>(aClosure);
509 self->mMenuOpenTimer = nullptr;
510 if (self->IsMenuPopupOpen()) {
511 return;
513 // make sure we didn't open a context menu in the meantime
514 // (i.e. the user right-clicked while hovering over a submenu).
515 nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
516 if (!pm) {
517 return;
519 if (pm->HasContextMenu(nullptr) && !self->IsOnContextMenu()) {
520 return;
522 if (!self->IsMenuActive()) {
523 return;
525 self->OpenMenuPopup(false);
527 this, MenuOpenCloseDelay(), nsITimer::TYPE_ONE_SHOT,
528 "XULButtonElement::OpenMenu", GetMainThreadSerialEventTarget());
532 nsresult XULButtonElement::PostHandleEvent(EventChainPostVisitor& aVisitor) {
533 if (aVisitor.mEventStatus == nsEventStatus_eConsumeNoDefault) {
534 return nsXULElement::PostHandleEvent(aVisitor);
537 if (IsMenu()) {
538 PostHandleEventForMenus(aVisitor);
539 return nsXULElement::PostHandleEvent(aVisitor);
542 auto* event = aVisitor.mEvent;
543 switch (event->mMessage) {
544 case eBlur: {
545 Blurred();
546 break;
548 case eKeyDown: {
549 WidgetKeyboardEvent* keyEvent = event->AsKeyboardEvent();
550 if (!keyEvent) {
551 break;
553 if (keyEvent->ShouldWorkAsSpaceKey() && aVisitor.mPresContext) {
554 EventStateManager* esm = aVisitor.mPresContext->EventStateManager();
555 // :hover:active state
556 esm->SetContentState(this, ElementState::HOVER);
557 esm->SetContentState(this, ElementState::ACTIVE);
558 mIsHandlingKeyEvent = true;
560 break;
563 // On mac, Return fires the default button, not the focused one.
564 #ifndef XP_MACOSX
565 case eKeyPress: {
566 WidgetKeyboardEvent* keyEvent = event->AsKeyboardEvent();
567 if (!keyEvent) {
568 break;
570 if (NS_VK_RETURN == keyEvent->mKeyCode) {
571 if (RefPtr<nsIDOMXULButtonElement> button = AsXULButton()) {
572 if (MouseClicked(*keyEvent)) {
573 aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
577 break;
579 #endif
581 case eKeyUp: {
582 WidgetKeyboardEvent* keyEvent = event->AsKeyboardEvent();
583 if (!keyEvent) {
584 break;
586 if (keyEvent->ShouldWorkAsSpaceKey()) {
587 mIsHandlingKeyEvent = false;
588 ElementState buttonState = State();
589 if (buttonState.HasAllStates(ElementState::ACTIVE |
590 ElementState::HOVER) &&
591 aVisitor.mPresContext) {
592 // return to normal state
593 EventStateManager* esm = aVisitor.mPresContext->EventStateManager();
594 esm->SetContentState(nullptr, ElementState::ACTIVE);
595 esm->SetContentState(nullptr, ElementState::HOVER);
596 if (MouseClicked(*keyEvent)) {
597 aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
601 break;
604 case eMouseClick: {
605 WidgetMouseEvent* mouseEvent = event->AsMouseEvent();
606 if (mouseEvent->IsLeftClickEvent()) {
607 if (MouseClicked(*mouseEvent)) {
608 aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
611 break;
614 default:
615 break;
618 return nsXULElement::PostHandleEvent(aVisitor);
621 void XULButtonElement::Blurred() {
622 ElementState buttonState = State();
623 if (mIsHandlingKeyEvent &&
624 buttonState.HasAllStates(ElementState::ACTIVE | ElementState::HOVER)) {
625 // Return to normal state
626 if (nsPresContext* pc = OwnerDoc()->GetPresContext()) {
627 EventStateManager* esm = pc->EventStateManager();
628 esm->SetContentState(nullptr, ElementState::ACTIVE);
629 esm->SetContentState(nullptr, ElementState::HOVER);
632 mIsHandlingKeyEvent = false;
635 bool XULButtonElement::MouseClicked(WidgetGUIEvent& aEvent) {
636 // Don't execute if we're disabled.
637 if (IsDisabled() || !IsInComposedDoc()) {
638 return false;
641 // Have the content handle the event, propagating it according to normal DOM
642 // rules.
643 RefPtr<mozilla::PresShell> presShell = OwnerDoc()->GetPresShell();
644 if (!presShell) {
645 return false;
648 // Execute the oncommand event handler.
649 WidgetInputEvent* inputEvent = aEvent.AsInputEvent();
650 WidgetMouseEventBase* mouseEvent = aEvent.AsMouseEventBase();
651 WidgetKeyboardEvent* keyEvent = aEvent.AsKeyboardEvent();
652 // TODO: Set aSourceEvent?
653 nsContentUtils::DispatchXULCommand(
654 this, aEvent.IsTrusted(), /* aSourceEvent = */ nullptr, presShell,
655 inputEvent->IsControl(), inputEvent->IsAlt(), inputEvent->IsShift(),
656 inputEvent->IsMeta(),
657 mouseEvent ? mouseEvent->mInputSource
658 : (keyEvent ? MouseEvent_Binding::MOZ_SOURCE_KEYBOARD
659 : MouseEvent_Binding::MOZ_SOURCE_UNKNOWN),
660 mouseEvent ? mouseEvent->mButton : 0);
661 return true;
664 bool XULButtonElement::IsMenu() const {
665 if (mIsAlwaysMenu) {
666 return true;
668 return IsAnyOfXULElements(nsGkAtoms::button, nsGkAtoms::toolbarbutton) &&
669 AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, nsGkAtoms::menu,
670 eCaseMatters);
673 void XULButtonElement::UncheckRadioSiblings() {
674 MOZ_ASSERT(!nsContentUtils::IsSafeToRunScript());
675 MOZ_ASSERT(GetMenuType() == Some(MenuType::Radio));
676 nsAutoString groupName;
677 GetAttr(nsGkAtoms::name, groupName);
679 nsIContent* parent = GetParent();
680 if (!parent) {
681 return;
684 auto ShouldUncheck = [&](const nsIContent& aSibling) {
685 const auto* button = XULButtonElement::FromNode(aSibling);
686 if (!button || button->GetMenuType() != Some(MenuType::Radio)) {
687 return false;
689 if (const auto* attr = button->GetParsedAttr(nsGkAtoms::name)) {
690 if (!attr->Equals(groupName, eCaseMatters)) {
691 return false;
693 } else if (!groupName.IsEmpty()) {
694 return false;
696 // we're in the same group, only uncheck if we're checked (for some reason,
697 // some tests rely on that specifically).
698 return button->GetXULBoolAttr(nsGkAtoms::checked);
701 for (nsIContent* child = parent->GetFirstChild(); child;
702 child = child->GetNextSibling()) {
703 if (child == this || !ShouldUncheck(*child)) {
704 continue;
706 child->AsElement()->UnsetAttr(nsGkAtoms::checked, IgnoreErrors());
710 void XULButtonElement::AfterSetAttr(int32_t aNamespaceID, nsAtom* aName,
711 const nsAttrValue* aValue,
712 const nsAttrValue* aOldValue,
713 nsIPrincipal* aSubjectPrincipal,
714 bool aNotify) {
715 nsXULElement::AfterSetAttr(aNamespaceID, aName, aValue, aOldValue,
716 aSubjectPrincipal, aNotify);
717 if (IsAlwaysMenu() && aNamespaceID == kNameSpaceID_None) {
718 // We need to uncheck radio siblings when we're a checked radio and switch
719 // groups, or become checked.
720 const bool shouldUncheckSiblings = [&] {
721 if (aName == nsGkAtoms::type || aName == nsGkAtoms::name) {
722 return *GetMenuType() == MenuType::Radio &&
723 GetXULBoolAttr(nsGkAtoms::checked);
725 if (aName == nsGkAtoms::checked && aValue &&
726 aValue->Equals(nsGkAtoms::_true, eCaseMatters)) {
727 return *GetMenuType() == MenuType::Radio;
729 return false;
730 }();
731 if (shouldUncheckSiblings) {
732 UncheckRadioSiblings();
737 auto XULButtonElement::GetMenuType() const -> Maybe<MenuType> {
738 if (!IsAlwaysMenu()) {
739 return Nothing();
742 static Element::AttrValuesArray values[] = {nsGkAtoms::checkbox,
743 nsGkAtoms::radio, nullptr};
744 switch (FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::type, values,
745 eCaseMatters)) {
746 case 0:
747 return Some(MenuType::Checkbox);
748 case 1:
749 return Some(MenuType::Radio);
750 default:
751 return Some(MenuType::Normal);
755 XULMenuBarElement* XULButtonElement::GetMenuBar() const {
756 if (!IsMenu()) {
757 return nullptr;
759 return FirstAncestorOfType<XULMenuBarElement>();
762 XULMenuParentElement* XULButtonElement::GetMenuParent() const {
763 if (IsXULElement(nsGkAtoms::menulist)) {
764 return nullptr;
766 return FirstAncestorOfType<XULMenuParentElement>();
769 XULPopupElement* XULButtonElement::GetMenuPopupContent() const {
770 if (!IsMenu()) {
771 return nullptr;
773 for (auto* child = GetFirstChild(); child; child = child->GetNextSibling()) {
774 if (auto* popup = XULPopupElement::FromNode(child)) {
775 return popup;
778 return nullptr;
781 nsMenuPopupFrame* XULButtonElement::GetMenuPopupWithoutFlushing() const {
782 return const_cast<XULButtonElement*>(this)->GetMenuPopup(FlushType::None);
785 nsMenuPopupFrame* XULButtonElement::GetMenuPopup(FlushType aFlushType) {
786 RefPtr popup = GetMenuPopupContent();
787 if (!popup) {
788 return nullptr;
790 return do_QueryFrame(popup->GetPrimaryFrame(aFlushType));
793 bool XULButtonElement::OpenedWithKey() const {
794 auto* menubar = GetMenuBar();
795 return menubar && menubar->IsActiveByKeyboard();
798 } // namespace mozilla::dom