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"
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"
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() {
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() {
66 gMenuJustOpenedOrClosedTime
= TimeStamp::Now();
67 SetAttr(kNameSpaceID_None
, nsGkAtoms::open
, u
"true"_ns
, true);
70 void XULButtonElement::PopupClosed(bool aDeselectMenu
) {
74 nsContentUtils::AddScriptRunner(
75 new nsUnsetAttrRunnable(this, nsGkAtoms::open
));
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;
93 void XULButtonElement::HandleEnterKeyPress(WidgetEvent
& aEvent
) {
96 if (XULPopupElement
* popup
= GetContainingPopupElement()) {
97 if (nsXULPopupManager
* pm
= nsXULPopupManager::GetInstance()) {
99 popup
, {HidePopupOption::HideChain
, HidePopupOption::DeselectMenu
,
100 HidePopupOption::Async
});
106 if (IsMenuPopupOpen()) {
109 // The enter key press applies to us.
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
);
135 bool XULButtonElement::IsOnMenuBar() const {
136 if (XULMenuParentElement
* menu
= GetMenuParent()) {
137 return menu
->IsMenuBar();
142 nsMenuPopupFrame
* XULButtonElement::GetContainingPopupWithoutFlushing() const {
143 if (XULPopupElement
* popup
= GetContainingPopupElement()) {
144 return do_QueryFrame(popup
->GetPrimaryFrame());
149 XULPopupElement
* XULButtonElement::GetContainingPopupElement() const {
150 return XULPopupElement::FromNodeOrNull(GetMenuParent());
153 bool XULButtonElement::IsOnContextMenu() const {
154 if (nsMenuPopupFrame
* popup
= GetContainingPopupWithoutFlushing()) {
155 return popup
->IsContextMenu();
160 void XULButtonElement::ToggleMenuState() {
161 if (IsMenuPopupOpen()) {
162 CloseMenuPopup(false);
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();
182 if (!pm
->MayShowMenu(this)) {
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()) {
196 if (nsXULPopupManager
* pm
= nsXULPopupManager::GetInstance()) {
197 pm
->ShowMenu(self
, aSelectFirstItem
);
202 void XULButtonElement::CloseMenuPopup(bool aDeselectMenu
) {
203 gMenuJustOpenedOrClosedTime
= TimeStamp::Now();
204 // Close the menu asynchronously
205 nsXULPopupManager
* pm
= nsXULPopupManager::GetInstance();
209 if (auto* popup
= GetMenuPopupContent()) {
210 HidePopupOptions options
{HidePopupOption::Async
};
212 options
+= HidePopupOption::DeselectMenu
;
214 pm
->HidePopup(popup
, options
);
218 int32_t XULButtonElement::MenuOpenCloseDelay() const {
222 return LookAndFeel::GetInt(LookAndFeel::IntID::SubmenuDelay
, 300); // ms
225 void XULButtonElement::ExecuteMenu(Modifiers aModifiers
, int16_t aButton
,
227 MOZ_ASSERT(IsMenu());
231 auto menuType
= GetMenuType();
232 if (NS_WARN_IF(!menuType
)) {
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
);
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();
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
,
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
) {
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
;
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) {
352 if (auto* parent
= GetMenuParent()) {
353 if (NS_WARN_IF(parent
->IsLocked())) {
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
) {
369 if (event
->mMessage
== eKeyPress
&& !IsDisabled()) {
370 WidgetKeyboardEvent
* keyEvent
= event
->AsKeyboardEvent();
371 uint32_t keyCode
= keyEvent
->mKeyCode
;
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
379 if (keyEvent
->mCharCode
!= ' ' ||
380 !nsMenuPopupFrame::IsWithinIncrementalTime(keyEvent
->mTimeStamp
)) {
381 aVisitor
.mEventStatus
= nsEventStatus_eConsumeNoDefault
;
382 OpenMenuPopup(false);
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
;
393 } else if (event
->mMessage
== eMouseDown
&&
394 event
->AsMouseEvent()->mButton
== MouseButton::ePrimary
&&
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() &&
400 !IsDisabled() && !IsMenuItem()) {
401 // The menu item was selected. Bring up the menu.
403 // Don't prevent the default action here, since that will also cancel
404 // potential drag starts.
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
416 bool isMacCtrlClick
= false;
418 isMacCtrlClick
= event
->AsMouseEvent()->mButton
== MouseButton::ePrimary
&&
419 event
->AsMouseEvent()->IsControl();
421 bool clickMightOpenContextMenu
=
422 event
->AsMouseEvent()->mButton
== MouseButton::eSecondary
||
424 if (!clickMightOpenContextMenu
|| IsOnContextMenu()) {
425 aVisitor
.mEventStatus
= nsEventStatus_eConsumeNoDefault
;
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
) {
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.
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.
454 // De-select elsewhere.
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();
473 if (IsDisabled() && IsOnMenuList()) {
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)
491 if (IsDisabled() || IsMenuItem() || IsMenuPopupOpen() || mMenuOpenTimer
) {
492 // Disabled, or already opening or what not.
496 if (parent
->IsMenuBar() && !isOnOpenMenubar
) {
497 // We should only open on hover in the menubar iff the menubar is open
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()) {
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();
519 if (pm
->HasContextMenu(nullptr) && !self
->IsOnContextMenu()) {
522 if (!self
->IsMenuActive()) {
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
);
538 PostHandleEventForMenus(aVisitor
);
539 return nsXULElement::PostHandleEvent(aVisitor
);
542 auto* event
= aVisitor
.mEvent
;
543 switch (event
->mMessage
) {
549 WidgetKeyboardEvent
* keyEvent
= event
->AsKeyboardEvent();
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;
563 // On mac, Return fires the default button, not the focused one.
566 WidgetKeyboardEvent
* keyEvent
= event
->AsKeyboardEvent();
570 if (NS_VK_RETURN
== keyEvent
->mKeyCode
) {
571 if (RefPtr
<nsIDOMXULButtonElement
> button
= AsXULButton()) {
572 if (MouseClicked(*keyEvent
)) {
573 aVisitor
.mEventStatus
= nsEventStatus_eConsumeNoDefault
;
582 WidgetKeyboardEvent
* keyEvent
= event
->AsKeyboardEvent();
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
;
605 WidgetMouseEvent
* mouseEvent
= event
->AsMouseEvent();
606 if (mouseEvent
->IsLeftClickEvent()) {
607 if (MouseClicked(*mouseEvent
)) {
608 aVisitor
.mEventStatus
= nsEventStatus_eConsumeNoDefault
;
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()) {
641 // Have the content handle the event, propagating it according to normal DOM
643 RefPtr
<mozilla::PresShell
> presShell
= OwnerDoc()->GetPresShell();
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);
664 bool XULButtonElement::IsMenu() const {
668 return IsAnyOfXULElements(nsGkAtoms::button
, nsGkAtoms::toolbarbutton
) &&
669 AttrValueIs(kNameSpaceID_None
, nsGkAtoms::type
, nsGkAtoms::menu
,
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();
684 auto ShouldUncheck
= [&](const nsIContent
& aSibling
) {
685 const auto* button
= XULButtonElement::FromNode(aSibling
);
686 if (!button
|| button
->GetMenuType() != Some(MenuType::Radio
)) {
689 if (const auto* attr
= button
->GetParsedAttr(nsGkAtoms::name
)) {
690 if (!attr
->Equals(groupName
, eCaseMatters
)) {
693 } else if (!groupName
.IsEmpty()) {
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
)) {
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
,
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
;
731 if (shouldUncheckSiblings
) {
732 UncheckRadioSiblings();
737 auto XULButtonElement::GetMenuType() const -> Maybe
<MenuType
> {
738 if (!IsAlwaysMenu()) {
742 static Element::AttrValuesArray values
[] = {nsGkAtoms::checkbox
,
743 nsGkAtoms::radio
, nullptr};
744 switch (FindAttrValueIn(kNameSpaceID_None
, nsGkAtoms::type
, values
,
747 return Some(MenuType::Checkbox
);
749 return Some(MenuType::Radio
);
751 return Some(MenuType::Normal
);
755 XULMenuBarElement
* XULButtonElement::GetMenuBar() const {
759 return FirstAncestorOfType
<XULMenuBarElement
>();
762 XULMenuParentElement
* XULButtonElement::GetMenuParent() const {
763 if (IsXULElement(nsGkAtoms::menulist
)) {
766 return FirstAncestorOfType
<XULMenuParentElement
>();
769 XULPopupElement
* XULButtonElement::GetMenuPopupContent() const {
773 for (auto* child
= GetFirstChild(); child
; child
= child
->GetNextSibling()) {
774 if (auto* popup
= XULPopupElement::FromNode(child
)) {
781 nsMenuPopupFrame
* XULButtonElement::GetMenuPopupWithoutFlushing() const {
782 return const_cast<XULButtonElement
*>(this)->GetMenuPopup(FlushType::None
);
785 nsMenuPopupFrame
* XULButtonElement::GetMenuPopup(FlushType aFlushType
) {
786 RefPtr popup
= GetMenuPopupContent();
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