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