tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

XULButtonElement.cpp (26941B)


      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/. */
      6 
      7 #include "XULButtonElement.h"
      8 
      9 #include "XULMenuParentElement.h"
     10 #include "XULPopupElement.h"
     11 #include "mozilla/Assertions.h"
     12 #include "mozilla/Attributes.h"
     13 #include "mozilla/EventDispatcher.h"
     14 #include "mozilla/EventStateManager.h"
     15 #include "mozilla/LookAndFeel.h"
     16 #include "mozilla/TextEvents.h"
     17 #include "mozilla/TimeStamp.h"
     18 #include "mozilla/dom/AncestorIterator.h"
     19 #include "mozilla/dom/DocumentInlines.h"
     20 #include "mozilla/dom/MouseEventBinding.h"
     21 #include "mozilla/dom/NameSpaceConstants.h"
     22 #include "mozilla/dom/XULMenuBarElement.h"
     23 #include "mozilla/glue/Debug.h"
     24 #include "nsCaseTreatment.h"
     25 #include "nsChangeHint.h"
     26 #include "nsGkAtoms.h"
     27 #include "nsIDOMXULButtonElement.h"
     28 #include "nsISound.h"
     29 #include "nsITimer.h"
     30 #include "nsLayoutUtils.h"
     31 #include "nsMenuPopupFrame.h"
     32 #include "nsPlaceholderFrame.h"
     33 #include "nsPresContext.h"
     34 #include "nsXULPopupManager.h"
     35 
     36 namespace mozilla::dom {
     37 
     38 XULButtonElement::XULButtonElement(
     39    already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
     40    : nsXULElement(std::move(aNodeInfo)),
     41      mIsAlwaysMenu(IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menulist,
     42                                       nsGkAtoms::menuitem)),
     43      mCheckable(IsAnyOfXULElements(nsGkAtoms::menuitem, nsGkAtoms::radio,
     44                                    nsGkAtoms::checkbox)) {}
     45 
     46 XULButtonElement::~XULButtonElement() {
     47  StopBlinking();
     48  KillMenuOpenTimer();
     49 }
     50 
     51 nsChangeHint XULButtonElement::GetAttributeChangeHint(
     52    const nsAtom* aAttribute, AttrModType aModType) const {
     53  if (aAttribute == nsGkAtoms::type &&
     54      IsAnyOfXULElements(nsGkAtoms::button, nsGkAtoms::toolbarbutton)) {
     55    // type=menu switches to a menu frame.
     56    return nsChangeHint_ReconstructFrame;
     57  }
     58  return nsXULElement::GetAttributeChangeHint(aAttribute, aModType);
     59 }
     60 
     61 // This global flag is used to record the timestamp when a menu was opened or
     62 // closed and is used to ignore the mousemove and mouseup events that would fire
     63 // on the menu after the mousedown occurred.
     64 static TimeStamp gMenuJustOpenedOrClosedTime = TimeStamp();
     65 
     66 void XULButtonElement::PopupOpened() {
     67  if (!IsMenu()) {
     68    return;
     69  }
     70  gMenuJustOpenedOrClosedTime = TimeStamp::Now();
     71  SetAttr(kNameSpaceID_None, nsGkAtoms::open, u"true"_ns, true);
     72 }
     73 
     74 void XULButtonElement::PopupClosed(bool aDeselectMenu) {
     75  if (!IsMenu()) {
     76    return;
     77  }
     78  nsContentUtils::AddScriptRunner(
     79      new nsUnsetAttrRunnable(this, nsGkAtoms::open));
     80 
     81  if (aDeselectMenu) {
     82    if (RefPtr<XULMenuParentElement> parent = GetMenuParent()) {
     83      if (parent->GetActiveMenuChild() == this) {
     84        parent->SetActiveMenuChild(nullptr);
     85      }
     86    }
     87  }
     88 }
     89 
     90 bool XULButtonElement::IsMenuActive() const {
     91  if (XULMenuParentElement* menu = GetMenuParent()) {
     92    return menu->GetActiveMenuChild() == this;
     93  }
     94  return false;
     95 }
     96 
     97 void XULButtonElement::HandleEnterKeyPress(WidgetEvent& aEvent) {
     98  if (IsDisabled()) {
     99 #ifdef XP_WIN
    100    if (XULPopupElement* popup = GetContainingPopupElement()) {
    101      if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) {
    102        pm->HidePopup(
    103            popup, {HidePopupOption::HideChain, HidePopupOption::DeselectMenu,
    104                    HidePopupOption::Async});
    105      }
    106    }
    107 #endif
    108    return;
    109  }
    110  if (IsMenuPopupOpen()) {
    111    return;
    112  }
    113  // The enter key press applies to us.
    114  if (IsMenuItem()) {
    115    ExecuteMenu(aEvent);
    116  } else {
    117    OpenMenuPopup(true);
    118  }
    119 }
    120 
    121 bool XULButtonElement::IsMenuPopupOpen() {
    122  nsMenuPopupFrame* popupFrame = GetMenuPopup(FlushType::None);
    123  return popupFrame && popupFrame->IsOpen();
    124 }
    125 
    126 bool XULButtonElement::IsOnMenu() const {
    127  auto* popup = XULPopupElement::FromNodeOrNull(GetMenuParent());
    128  return popup && popup->IsMenu();
    129 }
    130 
    131 bool XULButtonElement::IsOnMenuList() const {
    132  if (XULMenuParentElement* menu = GetMenuParent()) {
    133    return menu->GetParent() &&
    134           menu->GetParent()->IsXULElement(nsGkAtoms::menulist);
    135  }
    136  return false;
    137 }
    138 
    139 bool XULButtonElement::IsOnMenuBar() const {
    140  if (XULMenuParentElement* menu = GetMenuParent()) {
    141    return menu->IsMenuBar();
    142  }
    143  return false;
    144 }
    145 
    146 nsMenuPopupFrame* XULButtonElement::GetContainingPopupWithoutFlushing() const {
    147  if (XULPopupElement* popup = GetContainingPopupElement()) {
    148    return do_QueryFrame(popup->GetPrimaryFrame());
    149  }
    150  return nullptr;
    151 }
    152 
    153 XULPopupElement* XULButtonElement::GetContainingPopupElement() const {
    154  return XULPopupElement::FromNodeOrNull(GetMenuParent());
    155 }
    156 
    157 bool XULButtonElement::IsOnContextMenu() const {
    158  if (nsMenuPopupFrame* popup = GetContainingPopupWithoutFlushing()) {
    159    return popup->IsContextMenu();
    160  }
    161  return false;
    162 }
    163 
    164 void XULButtonElement::ToggleMenuState() {
    165  if (IsMenuPopupOpen()) {
    166    CloseMenuPopup(false);
    167  } else {
    168    OpenMenuPopup(false);
    169  }
    170 }
    171 
    172 void XULButtonElement::KillMenuOpenTimer() {
    173  if (mMenuOpenTimer) {
    174    mMenuOpenTimer->Cancel();
    175    mMenuOpenTimer = nullptr;
    176  }
    177 }
    178 
    179 void XULButtonElement::OpenMenuPopup(bool aSelectFirstItem) {
    180  nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
    181  if (!pm) {
    182    return;
    183  }
    184 
    185  pm->KillMenuTimer();
    186  if (!pm->MayShowMenu(this)) {
    187    return;
    188  }
    189 
    190  if (RefPtr<XULMenuParentElement> parent = GetMenuParent()) {
    191    parent->SetActiveMenuChild(this);
    192  }
    193 
    194  // Open the menu asynchronously.
    195  OwnerDoc()->Dispatch(NS_NewRunnableFunction(
    196      "AsyncOpenMenu", [self = RefPtr{this}, aSelectFirstItem] {
    197        if (self->GetMenuParent() && !self->IsMenuActive()) {
    198          return;
    199        }
    200        if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) {
    201          pm->ShowMenu(self, aSelectFirstItem);
    202        }
    203      }));
    204 }
    205 
    206 void XULButtonElement::CloseMenuPopup(bool aDeselectMenu) {
    207  gMenuJustOpenedOrClosedTime = TimeStamp::Now();
    208  // Close the menu asynchronously
    209  nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
    210  if (!pm) {
    211    return;
    212  }
    213  if (auto* popup = GetMenuPopupContent()) {
    214    HidePopupOptions options{HidePopupOption::Async};
    215    if (aDeselectMenu) {
    216      options += HidePopupOption::DeselectMenu;
    217    }
    218    pm->HidePopup(popup, options);
    219  }
    220 }
    221 
    222 int32_t XULButtonElement::MenuOpenCloseDelay() const {
    223  if (IsOnMenuBar()) {
    224    return 0;
    225  }
    226  return LookAndFeel::GetInt(LookAndFeel::IntID::SubmenuDelay, 300);  // ms
    227 }
    228 
    229 void XULButtonElement::ExecuteMenu(Modifiers aModifiers, int16_t aButton,
    230                                   bool aIsTrusted) {
    231  MOZ_ASSERT(IsMenu());
    232 
    233  StopBlinking();
    234 
    235  auto menuType = GetMenuType();
    236  if (NS_WARN_IF(!menuType)) {
    237    return;
    238  }
    239 
    240  // Because the command event is firing asynchronously, a flag is needed to
    241  // indicate whether user input is being handled. This ensures that a popup
    242  // window won't get blocked.
    243  const bool userinput = dom::UserActivation::IsHandlingUserInput();
    244 
    245  // Flip "checked" state if we're a checkbox menu, or an un-checked radio menu.
    246  bool needToFlipChecked = false;
    247  if (*menuType == MenuType::Checkbox ||
    248      (*menuType == MenuType::Radio && !GetBoolAttr(nsGkAtoms::checked))) {
    249    needToFlipChecked = !AttrValueIs(kNameSpaceID_None, nsGkAtoms::autocheck,
    250                                     nsGkAtoms::_false, eCaseMatters);
    251  }
    252 
    253  mDelayedMenuCommandEvent = new nsXULMenuCommandEvent(
    254      this, aIsTrusted, aModifiers, userinput, needToFlipChecked, aButton);
    255  StartBlinking();
    256 }
    257 
    258 void XULButtonElement::StopBlinking() {
    259  if (mMenuBlinkTimer) {
    260    if (auto* parent = GetMenuParent()) {
    261      parent->LockMenuUntilClosed(false);
    262    }
    263    mMenuBlinkTimer->Cancel();
    264    mMenuBlinkTimer = nullptr;
    265  }
    266  mDelayedMenuCommandEvent = nullptr;
    267 }
    268 
    269 void XULButtonElement::PassMenuCommandEventToPopupManager() {
    270  if (mDelayedMenuCommandEvent) {
    271    if (RefPtr<nsXULPopupManager> pm = nsXULPopupManager::GetInstance()) {
    272      RefPtr<nsXULMenuCommandEvent> event = std::move(mDelayedMenuCommandEvent);
    273      nsCOMPtr<nsIContent> content = this;
    274      pm->ExecuteMenu(content, event);
    275    }
    276  }
    277  mDelayedMenuCommandEvent = nullptr;
    278 }
    279 
    280 static constexpr int32_t kBlinkDelay = 67;  // milliseconds
    281 
    282 void XULButtonElement::StartBlinking() {
    283  if (!LookAndFeel::GetInt(LookAndFeel::IntID::ChosenMenuItemsShouldBlink)) {
    284    PassMenuCommandEventToPopupManager();
    285    return;
    286  }
    287 
    288  // Blink off.
    289  UnsetAttr(kNameSpaceID_None, nsGkAtoms::menuactive, true);
    290  if (auto* parent = GetMenuParent()) {
    291    // Make this menu ignore events from now on.
    292    parent->LockMenuUntilClosed(true);
    293  }
    294 
    295  // Set up a timer to blink back on.
    296  NS_NewTimerWithFuncCallback(
    297      getter_AddRefs(mMenuBlinkTimer),
    298      [](nsITimer*, void* aClosure) MOZ_CAN_RUN_SCRIPT_BOUNDARY {
    299        RefPtr self = static_cast<XULButtonElement*>(aClosure);
    300        if (auto* parent = self->GetMenuParent()) {
    301          if (parent->GetActiveMenuChild() == self) {
    302            // Restore the highlighting if we're still the active item.
    303            self->SetAttr(kNameSpaceID_None, nsGkAtoms::menuactive, u"true"_ns,
    304                          true);
    305          }
    306        }
    307        // Reuse our timer to actually execute.
    308        self->mMenuBlinkTimer->InitWithNamedFuncCallback(
    309            [](nsITimer*, void* aClosure) MOZ_CAN_RUN_SCRIPT_BOUNDARY {
    310              RefPtr self = static_cast<XULButtonElement*>(aClosure);
    311              if (auto* parent = self->GetMenuParent()) {
    312                parent->LockMenuUntilClosed(false);
    313              }
    314              self->PassMenuCommandEventToPopupManager();
    315              self->StopBlinking();
    316            },
    317            aClosure, kBlinkDelay, nsITimer::TYPE_ONE_SHOT,
    318            "XULButtonElement::ContinueBlinking"_ns);
    319      },
    320      this, kBlinkDelay, nsITimer::TYPE_ONE_SHOT,
    321      "XULButtonElement::StartBlinking"_ns, GetMainThreadSerialEventTarget());
    322 }
    323 
    324 void XULButtonElement::UnbindFromTree(UnbindContext& aContext) {
    325  StopBlinking();
    326  nsXULElement::UnbindFromTree(aContext);
    327 }
    328 
    329 void XULButtonElement::ExecuteMenu(WidgetEvent& aEvent) {
    330  MOZ_ASSERT(IsMenu());
    331  if (nsCOMPtr<nsISound> sound = do_GetService("@mozilla.org/sound;1")) {
    332    sound->PlayEventSound(nsISound::EVENT_MENU_EXECUTE);
    333  }
    334 
    335  Modifiers modifiers = 0;
    336  if (WidgetInputEvent* inputEvent = aEvent.AsInputEvent()) {
    337    modifiers = inputEvent->mModifiers;
    338  }
    339 
    340  int16_t button = 0;
    341  if (WidgetMouseEventBase* mouseEvent = aEvent.AsMouseEventBase()) {
    342    button = mouseEvent->mButton;
    343  }
    344 
    345  ExecuteMenu(modifiers, button, aEvent.IsTrusted());
    346 }
    347 
    348 void XULButtonElement::PostHandleEventForMenus(
    349    EventChainPostVisitor& aVisitor) {
    350  auto* event = aVisitor.mEvent;
    351 
    352  if (event->mOriginalTarget != this) {
    353    return;
    354  }
    355 
    356  if (auto* parent = GetMenuParent()) {
    357    if (NS_WARN_IF(parent->IsLocked())) {
    358      return;
    359    }
    360  }
    361 
    362  // If a menu just opened, ignore the mouseup event that might occur after a
    363  // the mousedown event that opened it. However, if a different mousedown event
    364  // occurs, just clear this flag.
    365  if (!gMenuJustOpenedOrClosedTime.IsNull()) {
    366    if (event->mMessage == eMouseDown) {
    367      gMenuJustOpenedOrClosedTime = TimeStamp();
    368    } else if (event->mMessage == eMouseUp) {
    369      return;
    370    }
    371  }
    372 
    373  if (event->mMessage == eKeyPress && !IsDisabled()) {
    374    WidgetKeyboardEvent* keyEvent = event->AsKeyboardEvent();
    375    uint32_t keyCode = keyEvent->mKeyCode;
    376 #ifdef XP_MACOSX
    377    // On mac, open menulist on either up/down arrow or space (w/o Cmd pressed)
    378    if (!IsMenuPopupOpen() &&
    379        ((keyEvent->mCharCode == ' ' && !keyEvent->IsMeta()) ||
    380         (keyCode == NS_VK_UP || keyCode == NS_VK_DOWN))) {
    381      // When pressing space, don't open the menu if performing an incremental
    382      // search.
    383      if (keyEvent->mCharCode != ' ' ||
    384          !nsMenuPopupFrame::IsWithinIncrementalTime(keyEvent->mTimeStamp)) {
    385        aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
    386        OpenMenuPopup(false);
    387      }
    388    }
    389 #else
    390    // On other platforms, toggle menulist on unmodified F4 or Alt arrow
    391    if ((keyCode == NS_VK_F4 && !keyEvent->IsAlt()) ||
    392        ((keyCode == NS_VK_UP || keyCode == NS_VK_DOWN) && keyEvent->IsAlt())) {
    393      aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
    394      ToggleMenuState();
    395    }
    396 #endif
    397  } else if (event->mMessage == eMouseDown &&
    398             event->AsMouseEvent()->mButton == MouseButton::ePrimary &&
    399 #ifdef XP_MACOSX
    400             // On mac, ctrl-click will send a context menu event from the
    401             // widget, so we don't want to bring up the menu.
    402             !event->AsMouseEvent()->IsControl() &&
    403 #endif
    404             !IsDisabled() && !IsMenuItem()) {
    405    // The menu item was selected. Bring up the menu.
    406    // We have children.
    407    // Don't prevent the default action here, since that will also cancel
    408    // potential drag starts.
    409    if (!IsOnMenu()) {
    410      ToggleMenuState();
    411    } else if (!IsMenuPopupOpen()) {
    412      OpenMenuPopup(false);
    413    }
    414  } else if (event->mMessage == eMouseUp && IsMenuItem() && !IsDisabled() &&
    415             !event->mFlags.mMultipleActionsPrevented) {
    416    // We accept left and middle clicks on all menu items to activate the item.
    417    // On context menus we also accept right click to activate the item, because
    418    // right-clicking on an item in a context menu cannot open another context
    419    // menu.
    420    bool isMacCtrlClick = false;
    421 #ifdef XP_MACOSX
    422    isMacCtrlClick = event->AsMouseEvent()->mButton == MouseButton::ePrimary &&
    423                     event->AsMouseEvent()->IsControl();
    424 #endif
    425    bool clickMightOpenContextMenu =
    426        event->AsMouseEvent()->mButton == MouseButton::eSecondary ||
    427        isMacCtrlClick;
    428    if (!clickMightOpenContextMenu || IsOnContextMenu()) {
    429      aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
    430      ExecuteMenu(*event);
    431    }
    432  } else if (event->mMessage == eContextMenu && IsOnContextMenu() &&
    433             !IsMenuItem() && !IsDisabled()) {
    434    // Make sure we cancel default processing of the context menu event so
    435    // that it doesn't bubble and get seen again by the popuplistener and show
    436    // another context menu.
    437    aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
    438  } else if (event->mMessage == eMouseOut) {
    439    KillMenuOpenTimer();
    440    if (RefPtr<XULMenuParentElement> parent = GetMenuParent()) {
    441      if (parent->GetActiveMenuChild() == this) {
    442        // Deactivate the menu on mouse out in some cases...
    443        const bool shouldDeactivate = [&] {
    444          if (IsMenuPopupOpen()) {
    445            // If we're open we never deselect. PopupClosed will do as needed.
    446            return false;
    447          }
    448          if (auto* menubar = XULMenuBarElement::FromNode(*parent)) {
    449            // De-select when exiting a menubar item, if the menubar wasn't
    450            // activated by keyboard.
    451            return !menubar->IsActiveByKeyboard();
    452          }
    453          if (IsOnMenuList()) {
    454            // Don't de-select if on a menu-list. That matches Chromium and our
    455            // historical Windows behavior, see bug 1197913.
    456            return false;
    457          }
    458          // De-select elsewhere.
    459          return true;
    460        }();
    461 
    462        if (shouldDeactivate) {
    463          parent->SetActiveMenuChild(nullptr);
    464        }
    465      }
    466    }
    467  } else if (event->mMessage == eMouseMove && (IsOnMenu() || IsOnMenuBar())) {
    468    // Use a tolerance to address situations where a user might perform a
    469    // "wiggly" click that is accompanied by near-simultaneous mousemove events.
    470    const TimeDuration kTolerance = TimeDuration::FromMilliseconds(200);
    471    if (!gMenuJustOpenedOrClosedTime.IsNull() &&
    472        gMenuJustOpenedOrClosedTime + kTolerance < TimeStamp::Now()) {
    473      gMenuJustOpenedOrClosedTime = TimeStamp();
    474      return;
    475    }
    476 
    477    if (IsDisabled() && IsOnMenuList()) {
    478      return;
    479    }
    480 
    481    RefPtr<XULMenuParentElement> parent = GetMenuParent();
    482    MOZ_ASSERT(parent, "How did IsOnMenu{,Bar} return true then?");
    483 
    484    const bool isOnOpenMenubar =
    485        parent->IsMenuBar() && parent->GetActiveMenuChild() &&
    486        parent->GetActiveMenuChild()->IsMenuPopupOpen();
    487 
    488    parent->SetActiveMenuChild(this);
    489 
    490    // We need to check if we really became the current menu item or not.
    491    if (!IsMenuActive()) {
    492      // We didn't (presumably because a context menu was active)
    493      return;
    494    }
    495    if (IsDisabled() || IsMenuItem() || IsMenuPopupOpen() || mMenuOpenTimer) {
    496      // Disabled, or already opening or what not.
    497      return;
    498    }
    499 
    500    if (parent->IsMenuBar() && !isOnOpenMenubar) {
    501      // We should only open on hover in the menubar iff the menubar is open
    502      // already.
    503      return;
    504    }
    505 
    506    // A timer is used so that it doesn't open if the user moves the mouse
    507    // quickly past the menu. The MenuOpenCloseDelay ensures that only menus
    508    // have this behaviour.
    509    NS_NewTimerWithFuncCallback(
    510        getter_AddRefs(mMenuOpenTimer),
    511        [](nsITimer*, void* aClosure) MOZ_CAN_RUN_SCRIPT_BOUNDARY {
    512          RefPtr self = static_cast<XULButtonElement*>(aClosure);
    513          self->mMenuOpenTimer = nullptr;
    514          if (self->IsMenuPopupOpen()) {
    515            return;
    516          }
    517          // make sure we didn't open a context menu in the meantime
    518          // (i.e. the user right-clicked while hovering over a submenu).
    519          nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
    520          if (!pm) {
    521            return;
    522          }
    523          if (pm->HasContextMenu(nullptr) && !self->IsOnContextMenu()) {
    524            return;
    525          }
    526          if (!self->IsMenuActive()) {
    527            return;
    528          }
    529          self->OpenMenuPopup(false);
    530        },
    531        this, MenuOpenCloseDelay(), nsITimer::TYPE_ONE_SHOT,
    532        "XULButtonElement::OpenMenu"_ns, GetMainThreadSerialEventTarget());
    533  }
    534 }
    535 
    536 nsresult XULButtonElement::PostHandleEvent(EventChainPostVisitor& aVisitor) {
    537  if (aVisitor.mEventStatus == nsEventStatus_eConsumeNoDefault) {
    538    return nsXULElement::PostHandleEvent(aVisitor);
    539  }
    540 
    541  if (IsMenu()) {
    542    PostHandleEventForMenus(aVisitor);
    543    return nsXULElement::PostHandleEvent(aVisitor);
    544  }
    545 
    546  auto* event = aVisitor.mEvent;
    547  switch (event->mMessage) {
    548    case eBlur: {
    549      Blurred();
    550      break;
    551    }
    552    case eKeyDown: {
    553      WidgetKeyboardEvent* keyEvent = event->AsKeyboardEvent();
    554      if (!keyEvent) {
    555        break;
    556      }
    557      if (keyEvent->ShouldWorkAsSpaceKey() && aVisitor.mPresContext) {
    558        EventStateManager* esm = aVisitor.mPresContext->EventStateManager();
    559        // :hover:active state
    560        esm->SetContentState(this, ElementState::HOVER);
    561        esm->SetContentState(this, ElementState::ACTIVE);
    562        mIsHandlingKeyEvent = true;
    563      }
    564      break;
    565    }
    566 
    567 // On mac, Return fires the default button, not the focused one.
    568 #ifndef XP_MACOSX
    569    case eKeyPress: {
    570      WidgetKeyboardEvent* keyEvent = event->AsKeyboardEvent();
    571      if (!keyEvent) {
    572        break;
    573      }
    574      if (NS_VK_RETURN == keyEvent->mKeyCode) {
    575        if (RefPtr<nsIDOMXULButtonElement> button = AsXULButton()) {
    576          if (OnPointerClicked(*keyEvent)) {
    577            aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
    578          }
    579        }
    580      }
    581      break;
    582    }
    583 #endif
    584 
    585    case eKeyUp: {
    586      WidgetKeyboardEvent* keyEvent = event->AsKeyboardEvent();
    587      if (!keyEvent) {
    588        break;
    589      }
    590      if (keyEvent->ShouldWorkAsSpaceKey()) {
    591        mIsHandlingKeyEvent = false;
    592        ElementState buttonState = State();
    593        if (buttonState.HasAllStates(ElementState::ACTIVE |
    594                                     ElementState::HOVER) &&
    595            aVisitor.mPresContext) {
    596          // return to normal state
    597          EventStateManager* esm = aVisitor.mPresContext->EventStateManager();
    598          esm->SetContentState(nullptr, ElementState::ACTIVE);
    599          esm->SetContentState(nullptr, ElementState::HOVER);
    600          if (OnPointerClicked(*keyEvent)) {
    601            aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
    602          }
    603        }
    604      }
    605      break;
    606    }
    607 
    608    case ePointerClick: {
    609      WidgetMouseEvent* mouseEvent = event->AsMouseEvent();
    610      if (mouseEvent->IsLeftClickEvent()) {
    611        if (OnPointerClicked(*mouseEvent)) {
    612          aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
    613        }
    614      }
    615      break;
    616    }
    617 
    618    default:
    619      break;
    620  }
    621 
    622  return nsXULElement::PostHandleEvent(aVisitor);
    623 }
    624 
    625 void XULButtonElement::Blurred() {
    626  ElementState buttonState = State();
    627  if (mIsHandlingKeyEvent &&
    628      buttonState.HasAllStates(ElementState::ACTIVE | ElementState::HOVER)) {
    629    // Return to normal state
    630    if (nsPresContext* pc = OwnerDoc()->GetPresContext()) {
    631      EventStateManager* esm = pc->EventStateManager();
    632      esm->SetContentState(nullptr, ElementState::ACTIVE);
    633      esm->SetContentState(nullptr, ElementState::HOVER);
    634    }
    635  }
    636  mIsHandlingKeyEvent = false;
    637 }
    638 
    639 bool XULButtonElement::OnPointerClicked(WidgetGUIEvent& aEvent) {
    640  // Don't execute if we're disabled.
    641  if (IsDisabled() || !IsInComposedDoc()) {
    642    return false;
    643  }
    644 
    645  // Have the content handle the event, propagating it according to normal DOM
    646  // rules.
    647  RefPtr<mozilla::PresShell> presShell = OwnerDoc()->GetPresShell();
    648  if (!presShell) {
    649    return false;
    650  }
    651 
    652  // Execute the oncommand event handler.
    653  WidgetInputEvent* inputEvent = aEvent.AsInputEvent();
    654  WidgetMouseEventBase* mouseEvent = aEvent.AsMouseEventBase();
    655  WidgetKeyboardEvent* keyEvent = aEvent.AsKeyboardEvent();
    656  // TODO: Set aSourceEvent?
    657  nsContentUtils::DispatchXULCommand(
    658      this, aEvent.IsTrusted(), /* aSourceEvent = */ nullptr, presShell,
    659      inputEvent->IsControl(), inputEvent->IsAlt(), inputEvent->IsShift(),
    660      inputEvent->IsMeta(),
    661      mouseEvent ? mouseEvent->mInputSource
    662                 : (keyEvent ? MouseEvent_Binding::MOZ_SOURCE_KEYBOARD
    663                             : MouseEvent_Binding::MOZ_SOURCE_UNKNOWN),
    664      mouseEvent ? mouseEvent->mButton : 0);
    665  return true;
    666 }
    667 
    668 bool XULButtonElement::IsMenu() const {
    669  if (mIsAlwaysMenu) {
    670    return true;
    671  }
    672  return IsAnyOfXULElements(nsGkAtoms::button, nsGkAtoms::toolbarbutton) &&
    673         AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, nsGkAtoms::menu,
    674                     eCaseMatters);
    675 }
    676 
    677 void XULButtonElement::UncheckRadioSiblings() {
    678  MOZ_ASSERT(!nsContentUtils::IsSafeToRunScript());
    679  MOZ_ASSERT(GetMenuType() == Some(MenuType::Radio));
    680  nsAutoString groupName;
    681  GetAttr(nsGkAtoms::name, groupName);
    682 
    683  nsIContent* parent = GetParent();
    684  if (!parent) {
    685    return;
    686  }
    687 
    688  auto ShouldUncheck = [&](const nsIContent& aSibling) {
    689    const auto* button = XULButtonElement::FromNode(aSibling);
    690    if (!button || button->GetMenuType() != Some(MenuType::Radio)) {
    691      return false;
    692    }
    693    if (const auto* attr = button->GetParsedAttr(nsGkAtoms::name)) {
    694      if (!attr->Equals(groupName, eCaseMatters)) {
    695        return false;
    696      }
    697    } else if (!groupName.IsEmpty()) {
    698      return false;
    699    }
    700    // we're in the same group, only uncheck if we're checked (for some reason,
    701    // some tests rely on that specifically).
    702    return button->GetBoolAttr(nsGkAtoms::checked);
    703  };
    704 
    705  for (nsIContent* child = parent->GetFirstChild(); child;
    706       child = child->GetNextSibling()) {
    707    if (child == this || !ShouldUncheck(*child)) {
    708      continue;
    709    }
    710    child->AsElement()->UnsetAttr(nsGkAtoms::checked, IgnoreErrors());
    711  }
    712 }
    713 
    714 void XULButtonElement::AfterSetAttr(int32_t aNamespaceID, nsAtom* aName,
    715                                    const nsAttrValue* aValue,
    716                                    const nsAttrValue* aOldValue,
    717                                    nsIPrincipal* aSubjectPrincipal,
    718                                    bool aNotify) {
    719  nsXULElement::AfterSetAttr(aNamespaceID, aName, aValue, aOldValue,
    720                             aSubjectPrincipal, aNotify);
    721  if (aNamespaceID != kNameSpaceID_None) {
    722    return;
    723  }
    724  if (mCheckable &&
    725      (aName == nsGkAtoms::checked || aName == nsGkAtoms::selected)) {
    726    // <menuitem> uses checked for type=radio / type=checkbox and selected for
    727    // menulists. <radio> uses selected, but <checkbox> uses checked. We just
    728    // make both work for simplicity (also matches historical behavior).
    729    const bool checked =
    730        aValue || GetBoolAttr(aName == nsGkAtoms::checked ? nsGkAtoms::selected
    731                                                          : nsGkAtoms::checked);
    732    SetStates(ElementState::CHECKED, checked, aNotify);
    733  }
    734  if (aName == nsGkAtoms::disabled) {
    735    SetStates(ElementState::DISABLED, !!aValue, aNotify);
    736  }
    737  if (IsAlwaysMenu()) {
    738    // We need to uncheck radio siblings when we're a checked radio and switch
    739    // groups, or become checked.
    740    const bool shouldUncheckSiblings = [&] {
    741      if (aName == nsGkAtoms::type || aName == nsGkAtoms::name) {
    742        return *GetMenuType() == MenuType::Radio &&
    743               GetBoolAttr(nsGkAtoms::checked);
    744      }
    745      if (aName == nsGkAtoms::checked && aValue) {
    746        return *GetMenuType() == MenuType::Radio;
    747      }
    748      return false;
    749    }();
    750    if (shouldUncheckSiblings) {
    751      UncheckRadioSiblings();
    752    }
    753  }
    754 }
    755 
    756 auto XULButtonElement::GetMenuType() const -> Maybe<MenuType> {
    757  if (!IsAlwaysMenu()) {
    758    return Nothing();
    759  }
    760 
    761  static Element::AttrValuesArray values[] = {nsGkAtoms::checkbox,
    762                                              nsGkAtoms::radio, nullptr};
    763  switch (FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::type, values,
    764                          eCaseMatters)) {
    765    case 0:
    766      return Some(MenuType::Checkbox);
    767    case 1:
    768      return Some(MenuType::Radio);
    769    default:
    770      return Some(MenuType::Normal);
    771  }
    772 }
    773 
    774 XULMenuBarElement* XULButtonElement::GetMenuBar() const {
    775  if (!IsMenu()) {
    776    return nullptr;
    777  }
    778  return FirstAncestorOfType<XULMenuBarElement>();
    779 }
    780 
    781 XULMenuParentElement* XULButtonElement::GetMenuParent() const {
    782  if (IsXULElement(nsGkAtoms::menulist)) {
    783    return nullptr;
    784  }
    785  return FirstAncestorOfType<XULMenuParentElement>();
    786 }
    787 
    788 XULPopupElement* XULButtonElement::GetMenuPopupContent() const {
    789  if (!IsMenu()) {
    790    return nullptr;
    791  }
    792  for (auto* child = GetFirstChild(); child; child = child->GetNextSibling()) {
    793    if (auto* popup = XULPopupElement::FromNode(child)) {
    794      return popup;
    795    }
    796  }
    797  return nullptr;
    798 }
    799 
    800 nsMenuPopupFrame* XULButtonElement::GetMenuPopupWithoutFlushing() const {
    801  return const_cast<XULButtonElement*>(this)->GetMenuPopup(FlushType::None);
    802 }
    803 
    804 nsMenuPopupFrame* XULButtonElement::GetMenuPopup(FlushType aFlushType) {
    805  RefPtr popup = GetMenuPopupContent();
    806  if (!popup) {
    807    return nullptr;
    808  }
    809  return do_QueryFrame(popup->GetPrimaryFrame(aFlushType));
    810 }
    811 
    812 bool XULButtonElement::OpenedWithKey() const {
    813  auto* menubar = GetMenuBar();
    814  return menubar && menubar->IsActiveByKeyboard();
    815 }
    816 
    817 }  // namespace mozilla::dom