nsMenuPopupFrame.cpp (96394B)
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 "nsMenuPopupFrame.h" 8 9 #include <algorithm> 10 11 #include "LayoutConstants.h" 12 #include "WindowRenderer.h" 13 #include "X11UndefineNone.h" 14 #include "XULButtonElement.h" 15 #include "XULPopupElement.h" 16 #include "mozilla/AnimationUtils.h" 17 #include "mozilla/BasePrincipal.h" 18 #include "mozilla/ClearOnShutdown.h" 19 #include "mozilla/ComputedStyle.h" 20 #include "mozilla/EventDispatcher.h" 21 #include "mozilla/EventStateManager.h" 22 #include "mozilla/LookAndFeel.h" 23 #include "mozilla/MouseEvents.h" 24 #include "mozilla/Preferences.h" 25 #include "mozilla/PresShell.h" 26 #include "mozilla/ScrollContainerFrame.h" 27 #include "mozilla/Services.h" 28 #include "mozilla/dom/BrowserParent.h" 29 #include "mozilla/dom/Document.h" 30 #include "mozilla/dom/Element.h" 31 #include "mozilla/dom/Event.h" 32 #include "mozilla/dom/KeyboardEvent.h" 33 #include "mozilla/dom/KeyboardEventBinding.h" 34 #include "mozilla/dom/XULPopupElement.h" 35 #include "mozilla/widget/ScreenManager.h" 36 #include "nsAtom.h" 37 #include "nsCSSFrameConstructor.h" 38 #include "nsCSSRendering.h" 39 #include "nsContentUtils.h" 40 #include "nsDisplayList.h" 41 #include "nsExpirationTracker.h" 42 #include "nsFrameManager.h" 43 #include "nsGkAtoms.h" 44 #include "nsIBaseWindow.h" 45 #include "nsIContent.h" 46 #include "nsIDOMXULSelectCntrlEl.h" 47 #include "nsIDocShell.h" 48 #include "nsIDocShellTreeOwner.h" 49 #include "nsIFrameInlines.h" 50 #include "nsIPopupContainer.h" 51 #include "nsIReflowCallback.h" 52 #include "nsIScreenManager.h" 53 #include "nsISound.h" 54 #include "nsLayoutUtils.h" 55 #include "nsNameSpaceManager.h" 56 #include "nsPIDOMWindow.h" 57 #include "nsPIWindowRoot.h" 58 #include "nsPresContext.h" 59 #include "nsReadableUtils.h" 60 #include "nsRect.h" 61 #include "nsServiceManagerUtils.h" 62 #include "nsStyleConsts.h" 63 #include "nsStyleStructInlines.h" 64 #include "nsTransitionManager.h" 65 #include "nsUnicharUtils.h" 66 #include "nsWidgetsCID.h" 67 #include "nsXULPopupManager.h" 68 69 using namespace mozilla; 70 using namespace mozilla::widget; 71 using mozilla::dom::Document; 72 using mozilla::dom::Element; 73 using mozilla::dom::Event; 74 using mozilla::dom::XULButtonElement; 75 76 TimeStamp nsMenuPopupFrame::sLastKeyTime; 77 78 #ifdef MOZ_WAYLAND 79 # include "mozilla/WidgetUtilsGtk.h" 80 # define IS_WAYLAND_DISPLAY() mozilla::widget::GdkIsWaylandDisplay() 81 extern mozilla::LazyLogModule gWidgetPopupLog; 82 # define LOG_WAYLAND(...) \ 83 MOZ_LOG(gWidgetPopupLog, mozilla::LogLevel::Debug, (__VA_ARGS__)) 84 # define LOG_WAYLAND_VERBOSE(...) \ 85 MOZ_LOG(gWidgetPopupLog, mozilla::LogLevel::Verbose, (__VA_ARGS__)) 86 #else 87 # define IS_WAYLAND_DISPLAY() false 88 # define LOG_WAYLAND(...) 89 # define LOG_WAYLAND_VERBOSE(...) 90 #endif 91 92 nsIFrame* NS_NewMenuPopupFrame(PresShell* aPresShell, ComputedStyle* aStyle) { 93 return new (aPresShell) 94 nsMenuPopupFrame(aStyle, aPresShell->GetPresContext()); 95 } 96 97 NS_IMPL_FRAMEARENA_HELPERS(nsMenuPopupFrame) 98 99 NS_QUERYFRAME_HEAD(nsMenuPopupFrame) 100 NS_QUERYFRAME_ENTRY(nsMenuPopupFrame) 101 NS_QUERYFRAME_TAIL_INHERITING(nsBlockFrame) 102 103 // Three generations of 5000ms (so 15s to get rid of all the closed popups). 104 class PopupExpirationTracker final 105 : public nsExpirationTracker<nsMenuPopupFrame, 3> { 106 static StaticAutoPtr<PopupExpirationTracker> sInstance; 107 108 void NotifyExpired(nsMenuPopupFrame* aPopup) override { 109 // printf_stderr("PopupExpirationTracker::NotifyExpired(%s)\n", 110 // aPopup->ListTag().get()); 111 RemoveObject(aPopup); 112 aPopup->DestroyWidgetIfNeeded(); 113 } 114 115 public: 116 PopupExpirationTracker() 117 : nsExpirationTracker(5000 /* ms */, "PopupExpirationTracker"_ns) {} 118 static PopupExpirationTracker* Get() { return sInstance.get(); } 119 static PopupExpirationTracker& GetOrCreate() { 120 if (!sInstance) { 121 sInstance = new PopupExpirationTracker(); 122 ClearOnShutdown(&sInstance); 123 } 124 return *sInstance; 125 } 126 }; 127 StaticAutoPtr<PopupExpirationTracker> PopupExpirationTracker::sInstance; 128 129 nsMenuPopupFrame::nsMenuPopupFrame(ComputedStyle* aStyle, 130 nsPresContext* aPresContext) 131 : nsBlockFrame(aStyle, aPresContext, kClassID) {} 132 133 nsMenuPopupFrame::~nsMenuPopupFrame() = default; 134 135 static bool IsMouseTransparent(const ComputedStyle& aStyle) { 136 // If pointer-events: none; is set on the popup, then the widget should 137 // ignore mouse events, passing them through to the content behind. 138 return aStyle.PointerEvents() == StylePointerEvents::None; 139 } 140 141 static nsIWidget::InputRegion ComputeInputRegion(const ComputedStyle& aStyle, 142 const nsPresContext& aPc) { 143 return {IsMouseTransparent(aStyle), 144 (aStyle.StyleUIReset()->mMozWindowInputRegionMargin.ToCSSPixels() * 145 aPc.CSSToDevPixelScale()) 146 .Truncated()}; 147 } 148 149 bool nsMenuPopupFrame::IsDragPopup() const { 150 return !mInContentShell && mPopupType == PopupType::Panel && 151 mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, 152 nsGkAtoms::drag, eIgnoreCase); 153 } 154 155 bool nsMenuPopupFrame::ShouldHaveWidgetWhenHidden() const { 156 if (mContent->AsElement()->HasAttr(nsGkAtoms::neverhidden)) { 157 // Create a widget upfront for panels that never hide frames for their 158 // contents (like web extension popups). These, for now, need to create the 159 // widgets upfront, so that the frames inside the popup don't get 160 // "reparented" in the widget tree. 161 // 162 // TODO(emilio, bug 1976324): Try to somehow remove this special-case, 163 // web-ext panel needs it to compute the "natural" bounds of their contents 164 // before showing the popup, but that seems like it could be tweaked. 165 return true; 166 } 167 if (IsDragPopup()) { 168 // Create widgets upfront for the drag popup for now, see bug 1976623. 169 return true; 170 } 171 return false; 172 } 173 174 void nsMenuPopupFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, 175 nsIFrame* aPrevInFlow) { 176 nsBlockFrame::Init(aContent, aParent, aPrevInFlow); 177 178 const auto& el = PopupElement(); 179 mPopupType = PopupType::Panel; 180 if (el.IsMenu()) { 181 mPopupType = PopupType::Menu; 182 } else if (el.IsXULElement(nsGkAtoms::tooltip)) { 183 mPopupType = PopupType::Tooltip; 184 } 185 186 if (PresContext()->IsChrome()) { 187 mInContentShell = false; 188 } 189 190 // Support incontentshell=false attribute to allow popups to be displayed 191 // outside of the content shell. Chrome only. 192 if (el.NodePrincipal()->IsSystemPrincipal()) { 193 if (el.GetXULBoolAttr(nsGkAtoms::incontentshell)) { 194 mInContentShell = true; 195 } else if (el.AttrValueIs(kNameSpaceID_None, nsGkAtoms::incontentshell, 196 nsGkAtoms::_false, eCaseMatters)) { 197 mInContentShell = false; 198 } 199 } 200 201 // To improve performance, create the widget for the popup if needed. Popups 202 // such as menus will create their widgets later when the popup opens. 203 if (!mWidget && ShouldHaveWidgetWhenHidden()) { 204 CreateWidget(); 205 } 206 207 AddStateBits(NS_FRAME_IN_POPUP); 208 } 209 210 bool nsMenuPopupFrame::HasRemoteContent() const { 211 return !mInContentShell && mPopupType == PopupType::Panel && 212 mContent->AsElement()->AttrValueIs(kNameSpaceID_None, 213 nsGkAtoms::remote, nsGkAtoms::_true, 214 eIgnoreCase); 215 } 216 217 bool nsMenuPopupFrame::IsNoAutoHide() const { 218 // Panels with noautohide="true" don't hide when the mouse is clicked 219 // outside of them, or when another application is made active. Non-autohide 220 // panels cannot be used in content windows. 221 return !mInContentShell && mPopupType == PopupType::Panel && 222 mContent->AsElement()->AttrValueIs(kNameSpaceID_None, 223 nsGkAtoms::noautohide, 224 nsGkAtoms::_true, eIgnoreCase); 225 } 226 227 widget::PopupLevel nsMenuPopupFrame::GetPopupLevel(bool aIsNoAutoHide) const { 228 // The popup level is determined as follows, in this order: 229 // 1. non-panels (menus and tooltips) are always topmost 230 // 2. any specified level attribute 231 // 3. if a titlebar attribute is set, use the 'floating' level 232 // 4. if this is a noautohide panel, use the 'parent' level 233 // 5. use the platform-specific default level 234 235 // If this is not a panel, this is always a top-most popup. 236 if (mPopupType != PopupType::Panel) { 237 return PopupLevel::Top; 238 } 239 240 // If the level attribute has been set, use that. 241 static Element::AttrValuesArray strings[] = {nsGkAtoms::top, 242 nsGkAtoms::parent, nullptr}; 243 switch (mContent->AsElement()->FindAttrValueIn( 244 kNameSpaceID_None, nsGkAtoms::level, strings, eCaseMatters)) { 245 case 0: 246 return PopupLevel::Top; 247 case 1: 248 return PopupLevel::Parent; 249 default: 250 break; 251 } 252 253 // If this panel is a noautohide panel, the default is the parent level. 254 if (aIsNoAutoHide) { 255 return PopupLevel::Parent; 256 } 257 258 // Otherwise, the result depends on the platform. 259 return StaticPrefs::ui_panel_default_level_parent() ? PopupLevel::Parent 260 : PopupLevel::Top; 261 } 262 263 void nsMenuPopupFrame::PrepareWidget(bool aForceRecreate) { 264 if (mExpirationState.IsTracked()) { 265 PopupExpirationTracker::Get()->RemoveObject(this); 266 } 267 if (auto* widget = GetWidget()) { 268 nsCOMPtr<nsIWidget> parent = ComputeParentWidget(); 269 if (aForceRecreate || widget->GetParent() != parent || 270 widget->NeedsRecreateToReshow()) { 271 DestroyWidget(); 272 } 273 } 274 if (!mWidget) { 275 CreateWidget(); 276 } else { 277 PropagateStyleToWidget(); 278 } 279 } 280 281 already_AddRefed<nsIWidget> nsMenuPopupFrame::ComputeParentWidget() const { 282 auto popupLevel = GetPopupLevel(IsNoAutoHide()); 283 // Panels which have a parent level need a parent widget. This allows them to 284 // always appear in front of the parent window but behind other windows that 285 // should be in front of it. 286 nsCOMPtr<nsIWidget> parentWidget; 287 if (popupLevel != PopupLevel::Top) { 288 nsCOMPtr<nsIDocShellTreeItem> dsti = PresContext()->GetDocShell(); 289 if (!dsti) { 290 return nullptr; 291 } 292 293 nsCOMPtr<nsIDocShellTreeOwner> treeOwner; 294 dsti->GetTreeOwner(getter_AddRefs(treeOwner)); 295 if (!treeOwner) { 296 return nullptr; 297 } 298 299 if (nsCOMPtr<nsIBaseWindow> baseWindow = do_QueryInterface(treeOwner)) { 300 parentWidget = baseWindow->GetMainWidget(); 301 } 302 } 303 if (!parentWidget) { 304 parentWidget = GetParent()->GetNearestWidget(); 305 } 306 return parentWidget.forget(); 307 } 308 309 void nsMenuPopupFrame::CreateWidget() { 310 // Create a widget for ourselves. 311 widget::InitData widgetData; 312 widgetData.mWindowType = widget::WindowType::Popup; 313 widgetData.mBorderStyle = widget::BorderStyle::Default; 314 widgetData.mClipSiblings = true; 315 widgetData.mPopupHint = mPopupType; 316 widgetData.mIsDragPopup = IsDragPopup(); 317 318 const bool remote = HasRemoteContent(); 319 320 const auto mode = nsLayoutUtils::GetFrameTransparency(this, this); 321 widgetData.mHasRemoteContent = remote; 322 widgetData.mTransparencyMode = mode; 323 widgetData.mPopupLevel = GetPopupLevel(IsNoAutoHide()); 324 325 nsCOMPtr<nsIWidget> parentWidget = ComputeParentWidget(); 326 if (NS_WARN_IF(!parentWidget)) { 327 return; 328 } 329 330 mWidget = parentWidget->CreateChild(CalcWidgetBounds(), widgetData); 331 if (NS_WARN_IF(!mWidget)) { 332 return; 333 } 334 mWidget->SetWidgetListener(this); 335 mWidget->EnableDragDrop(true); 336 // TODO(emilio): Make all widgets look at widgetData.mTransparencyMode 337 // (maybe in BaseCreate?) then remove this call. 338 mWidget->SetTransparencyMode(mode); 339 PropagateStyleToWidget(); 340 } 341 342 LayoutDeviceIntRect nsMenuPopupFrame::CalcWidgetBounds() const { 343 auto a2d = PresContext()->AppUnitsPerDevPixel(); 344 nsPoint offset; 345 nsIWidget* parentWidget = 346 PresShell()->GetRootFrame()->GetNearestWidget(offset); 347 // We want the bounds be relative to the parent widget, in appunits 348 if (parentWidget) { 349 // put offset into screen coordinates. (based on client area origin) 350 offset += LayoutDeviceIntPoint::ToAppUnits( 351 parentWidget->WidgetToScreenOffset(), a2d); 352 } 353 int32_t roundTo = 354 parentWidget ? parentWidget->RoundsWidgetCoordinatesTo() : 1; 355 auto bounds = GetRect() + offset; 356 // We use outside pixels for transparent windows if possible, so that we 357 // don't truncate the contents. For opaque popups, we use nearest pixels 358 // which prevents having pixels not drawn by the frame. 359 const auto transparency = nsLayoutUtils::GetFrameTransparency(this, this); 360 const bool opaque = transparency == TransparencyMode::Opaque; 361 const auto idealBounds = LayoutDeviceIntRect::FromUnknownRect( 362 opaque ? bounds.ToNearestPixels(a2d) : bounds.ToOutsidePixels(a2d)); 363 return nsIWidget::MaybeRoundToDisplayPixels(idealBounds, transparency, 364 roundTo); 365 } 366 367 void nsMenuPopupFrame::DestroyWidget() { 368 RefPtr widget = mWidget.forget(); 369 if (!widget) { 370 return; 371 } 372 // Widget's WebRender resources needs to be cleared before creating new 373 // widget. 374 widget->ClearCachedWebrenderResources(); 375 widget->SetWidgetListener(nullptr); 376 NS_DispatchToMainThread( 377 NewRunnableMethod("DestroyWidget", widget, &nsIWidget::Destroy)); 378 } 379 380 void nsMenuPopupFrame::PropagateStyleToWidget(WidgetStyleFlags aFlags) const { 381 if (aFlags.isEmpty()) { 382 return; 383 } 384 385 nsIWidget* widget = GetWidget(); 386 if (!widget) { 387 return; 388 } 389 390 if (aFlags.contains(WidgetStyle::ColorScheme)) { 391 widget->SetColorScheme(Some(LookAndFeel::ColorSchemeForFrame(this))); 392 } 393 if (aFlags.contains(WidgetStyle::InputRegion)) { 394 widget->SetInputRegion(ComputeInputRegion(*Style(), *PresContext())); 395 } 396 if (aFlags.contains(WidgetStyle::Opacity)) { 397 widget->SetWindowOpacity(StyleUIReset()->mWindowOpacity); 398 } 399 if (aFlags.contains(WidgetStyle::Shadow)) { 400 widget->SetWindowShadowStyle(GetShadowStyle()); 401 } 402 if (aFlags.contains(WidgetStyle::Transform)) { 403 widget->SetWindowTransform(ComputeWidgetTransform()); 404 } 405 if (aFlags.contains(WidgetStyle::MicaBackdrop)) { 406 widget->SetMicaBackdrop(StyleDisplay()->EffectiveAppearance() == 407 StyleAppearance::Menupopup); 408 } 409 } 410 411 bool nsMenuPopupFrame::IsMouseTransparent() const { 412 return ::IsMouseTransparent(*Style()); 413 } 414 415 WindowShadow nsMenuPopupFrame::GetShadowStyle() const { 416 StyleWindowShadow shadow = StyleUIReset()->mWindowShadow; 417 if (shadow != StyleWindowShadow::Auto) { 418 MOZ_ASSERT(shadow == StyleWindowShadow::None); 419 return WindowShadow::None; 420 } 421 422 switch (StyleDisplay()->EffectiveAppearance()) { 423 case StyleAppearance::Tooltip: 424 return WindowShadow::Tooltip; 425 case StyleAppearance::Menupopup: 426 return WindowShadow::Menu; 427 default: 428 return WindowShadow::Panel; 429 } 430 } 431 432 void nsMenuPopupFrame::SetPopupState(nsPopupState aState) { 433 mPopupState = aState; 434 435 // Work around https://gitlab.gnome.org/GNOME/gtk/-/issues/4166 436 if (aState == ePopupShown && IS_WAYLAND_DISPLAY()) { 437 if (nsIWidget* widget = GetWidget()) { 438 widget->SetInputRegion(ComputeInputRegion(*Style(), *PresContext())); 439 } 440 } 441 } 442 443 // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398) 444 MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP nsXULPopupShownEvent::Run() { 445 nsMenuPopupFrame* popup = do_QueryFrame(mPopup->GetPrimaryFrame()); 446 // Set the state to visible if the popup is still open. 447 if (popup && popup->IsOpen()) { 448 popup->SetPopupState(ePopupShown); 449 } 450 451 if (!mPopup->IsXULElement(nsGkAtoms::tooltip)) { 452 nsCOMPtr<nsIObserverService> obsService = 453 mozilla::services::GetObserverService(); 454 if (obsService) { 455 obsService->NotifyObservers(mPopup, "popup-shown", nullptr); 456 } 457 } 458 WidgetMouseEvent event(true, eXULPopupShown, nullptr, 459 WidgetMouseEvent::eReal); 460 return EventDispatcher::Dispatch(mPopup, mPresContext, &event); 461 } 462 463 NS_IMETHODIMP nsXULPopupShownEvent::HandleEvent(Event* aEvent) { 464 nsMenuPopupFrame* popup = do_QueryFrame(mPopup->GetPrimaryFrame()); 465 // Ignore events not targeted at the popup itself (ie targeted at 466 // descendants): 467 if (mPopup != aEvent->GetTarget()) { 468 return NS_OK; 469 } 470 if (popup) { 471 // ResetPopupShownDispatcher will delete the reference to this, so keep 472 // another one until Run is finished. 473 RefPtr<nsXULPopupShownEvent> event = this; 474 // Only call Run if it the dispatcher was assigned. This avoids calling the 475 // Run method if the transitionend event fires multiple times. 476 if (popup->ClearPopupShownDispatcher()) { 477 return Run(); 478 } 479 } 480 481 CancelListener(); 482 return NS_OK; 483 } 484 485 void nsXULPopupShownEvent::CancelListener() { 486 mPopup->RemoveSystemEventListener(u"transitionend"_ns, this, false); 487 } 488 489 NS_IMPL_ISUPPORTS_INHERITED(nsXULPopupShownEvent, Runnable, 490 nsIDOMEventListener); 491 492 void nsMenuPopupFrame::DidSetComputedStyle(ComputedStyle* aOldStyle) { 493 nsBlockFrame::DidSetComputedStyle(aOldStyle); 494 495 if (!aOldStyle) { 496 return; 497 } 498 499 WidgetStyleFlags flags; 500 501 if (aOldStyle->StyleUI()->mColorScheme != StyleUI()->mColorScheme) { 502 flags += WidgetStyle::ColorScheme; 503 } 504 505 auto& newUI = *StyleUIReset(); 506 auto& oldUI = *aOldStyle->StyleUIReset(); 507 if (newUI.mWindowOpacity != oldUI.mWindowOpacity) { 508 flags += WidgetStyle::Opacity; 509 } 510 511 if (newUI.mMozWindowTransform != oldUI.mMozWindowTransform) { 512 flags += WidgetStyle::Transform; 513 } 514 515 if (newUI.mWindowShadow != oldUI.mWindowShadow) { 516 flags += WidgetStyle::Shadow; 517 } 518 519 if (aOldStyle->StyleDisplay()->EffectiveAppearance() != 520 StyleDisplay()->EffectiveAppearance()) { 521 flags += WidgetStyle::MicaBackdrop; 522 } 523 524 const auto& pc = *PresContext(); 525 auto oldRegion = ComputeInputRegion(*aOldStyle, pc); 526 auto newRegion = ComputeInputRegion(*Style(), pc); 527 if (oldRegion.mFullyTransparent != newRegion.mFullyTransparent || 528 oldRegion.mMargin != newRegion.mMargin) { 529 flags += WidgetStyle::InputRegion; 530 } 531 532 PropagateStyleToWidget(flags); 533 } 534 535 nscoord nsMenuPopupFrame::IntrinsicISize(const IntrinsicSizeInput& aInput, 536 IntrinsicISizeType aType) { 537 if (CanSkipLayout()) { 538 return 0; 539 } 540 nscoord iSize = nsBlockFrame::IntrinsicISize(aInput, aType); 541 if (!ShouldExpandToInflowParentOrAnchor()) { 542 return iSize; 543 } 544 // Make sure to accommodate for our scrollbar if needed. Do it only for 545 // menulists to match previous behavior. 546 // 547 // NOTE(emilio): This is somewhat hacky. The "right" fix (which would be 548 // using scrollbar-gutter: stable on the scroller) isn't great, because even 549 // though we want a stable gutter, we want to draw on top of the gutter when 550 // there's no scrollbar, otherwise it looks rather weird. 551 // 552 // Automatically accommodating for the scrollbar otherwise would be bug 553 // 764076, but that has its own set of problems. 554 if (ScrollContainerFrame* sf = GetScrollContainerFrame()) { 555 iSize += sf->GetDesiredScrollbarSizes().LeftRight(); 556 } 557 558 nscoord menuListOrAnchorWidth = 0; 559 if (nsIFrame* menuList = GetInFlowParent()) { 560 menuListOrAnchorWidth = menuList->GetRect().width; 561 } 562 if (mAnchorType == MenuPopupAnchorType::Rect) { 563 menuListOrAnchorWidth = std::max(menuListOrAnchorWidth, mScreenRect.width); 564 } 565 // Input margin doesn't have contents, so account for it for popup sizing 566 // purposes. 567 menuListOrAnchorWidth += 568 2 * StyleUIReset()->mMozWindowInputRegionMargin.ToAppUnits(); 569 570 return std::max(iSize, menuListOrAnchorWidth); 571 } 572 573 void nsMenuPopupFrame::Reflow(nsPresContext* aPresContext, 574 ReflowOutput& aDesiredSize, 575 const ReflowInput& aReflowInput, 576 nsReflowStatus& aStatus) { 577 MarkInReflow(); 578 DO_GLOBAL_REFLOW_COUNT("nsMenuPopupFrame"); 579 MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); 580 581 const auto wm = GetWritingMode(); 582 // Default to preserving our bounds. 583 aDesiredSize.SetSize(wm, GetLogicalSize(wm)); 584 585 LayoutPopup(aPresContext, aDesiredSize, aReflowInput, aStatus); 586 587 aDesiredSize.SetBlockStartAscent(aDesiredSize.BSize(wm)); 588 aDesiredSize.SetOverflowAreasToDesiredBounds(); 589 FinishAndStoreOverflow(&aDesiredSize, aReflowInput.mStyleDisplay); 590 } 591 592 void nsMenuPopupFrame::EnsureActiveMenuListItemIsVisible() { 593 if (!IsMenuList() || !IsOpen()) { 594 return; 595 } 596 nsIFrame* frame = GetCurrentMenuItemFrame(); 597 if (!frame) { 598 return; 599 } 600 RefPtr<mozilla::PresShell> presShell = PresShell(); 601 presShell->ScrollFrameIntoView( 602 frame, Nothing(), ScrollAxis(), ScrollAxis(), 603 ScrollFlags::ScrollOverflowHidden | ScrollFlags::ScrollFirstAncestorOnly); 604 } 605 606 bool nsMenuPopupFrame::CanSkipLayout() const { 607 // If the popup is not open, only do layout while showing or if we're a 608 // menulist. 609 // 610 // The later is needed because the SelectParent code wants to limit the height 611 // of the popup before opening it. 612 // 613 // TODO(emilio): We should consider adding a way to do that more reliably 614 // instead, but this preserves existing behavior. 615 return !IsVisibleOrShowing() && !IsMenuList(); 616 } 617 618 void nsMenuPopupFrame::LayoutPopup(nsPresContext* aPresContext, 619 ReflowOutput& aDesiredSize, 620 const ReflowInput& aReflowInput, 621 nsReflowStatus& aStatus) { 622 if (IsNativeMenu()) { 623 return; 624 } 625 626 SchedulePaint(); 627 628 const bool isOpen = IsOpen(); 629 if (CanSkipLayout()) { 630 RemoveStateBits(NS_FRAME_FIRST_REFLOW); 631 return; 632 } 633 634 // Do a first reflow, with all our content, in order to find our preferred 635 // size. Then, we do a second reflow with the updated dimensions. 636 const bool needsPrefSize = mPrefSize == nsSize(-1, -1) || IsSubtreeDirty(); 637 if (needsPrefSize) { 638 // Get the preferred, minimum and maximum size. If the menu is sized to the 639 // popup, then the popup's width is the menu's width. 640 ReflowOutput preferredSize(aReflowInput); 641 nsBlockFrame::Reflow(aPresContext, preferredSize, aReflowInput, aStatus); 642 mPrefSize = preferredSize.PhysicalSize(); 643 } 644 645 // Get our desired position and final size, now that we have a preferred size. 646 auto constraints = GetRects(mPrefSize); 647 const auto finalSize = constraints.mUsedRect.Size(); 648 649 // We need to do an extra reflow if we haven't reflowed, our size doesn't 650 // match with our final intended size, or our bsize is unconstrained (in which 651 // case we need to specify the final size so that percentages work). 652 const bool needDefiniteReflow = 653 aReflowInput.ComputedBSize() == NS_UNCONSTRAINEDSIZE || !needsPrefSize || 654 finalSize != mPrefSize; 655 656 if (needDefiniteReflow) { 657 ReflowInput constrainedReflowInput(aReflowInput); 658 const auto& bp = aReflowInput.ComputedPhysicalBorderPadding(); 659 // TODO: writing-mode handling not terribly correct, but it doesn't matter. 660 const nsSize finalContentSize(finalSize.width - bp.LeftRight(), 661 finalSize.height - bp.TopBottom()); 662 constrainedReflowInput.SetComputedISize(finalContentSize.width); 663 constrainedReflowInput.SetComputedBSize(finalContentSize.height); 664 constrainedReflowInput.SetIResize(finalSize.width != mPrefSize.width); 665 constrainedReflowInput.SetBResize([&] { 666 if (finalSize.height != mPrefSize.height) { 667 return true; 668 } 669 if (needsPrefSize && 670 aReflowInput.ComputedBSize() == NS_UNCONSTRAINEDSIZE && 671 aReflowInput.ComputedMaxBSize() == finalContentSize.height) { 672 // If we have measured, and maybe clamped our children via max-height, 673 // they might need to get percentages in the block axis re-resolved. 674 return true; 675 } 676 return false; 677 }()); 678 679 aStatus.Reset(); 680 nsBlockFrame::Reflow(aPresContext, aDesiredSize, constrainedReflowInput, 681 aStatus); 682 } 683 684 if (mIsOpenChanged || !mRect.IsEqualEdges(constraints.mUsedRect)) { 685 SchedulePendingWidgetMoveResize(); 686 } 687 688 // Set our size, since AbsoluteContainingBlock won't. 689 SetRect(constraints.mUsedRect); 690 691 if (isOpen) { 692 if (mPopupState == ePopupOpening) { 693 mPopupState = ePopupVisible; 694 } 695 } 696 697 // Perform our move now. That will position the view and so on. 698 PerformMove(constraints); 699 700 // finally, if the popup just opened, send a popupshown event 701 bool openChanged = mIsOpenChanged; 702 if (openChanged) { 703 mIsOpenChanged = false; 704 705 // Make sure the current selection in a menulist is visible. 706 EnsureActiveMenuListItemIsVisible(); 707 708 // If the animate attribute is set to open, check for a transition and wait 709 // for it to finish before firing the popupshown event. 710 if (LookAndFeel::GetInt(LookAndFeel::IntID::PanelAnimations) && 711 mContent->AsElement()->AttrValueIs(kNameSpaceID_None, 712 nsGkAtoms::animate, nsGkAtoms::open, 713 eCaseMatters) && 714 AnimationUtils::HasCurrentTransitions(mContent->AsElement())) { 715 mPopupShownDispatcher = new nsXULPopupShownEvent(mContent, aPresContext); 716 mContent->AddSystemEventListener(u"transitionend"_ns, 717 mPopupShownDispatcher, false, false); 718 return; 719 } 720 721 // If there are no transitions, fire the popupshown event right away. 722 nsCOMPtr<nsIRunnable> event = 723 new nsXULPopupShownEvent(GetContent(), aPresContext); 724 mContent->OwnerDoc()->Dispatch(event.forget()); 725 } 726 } 727 728 bool nsMenuPopupFrame::IsMenuList() const { 729 return PopupElement().IsInMenuList(); 730 } 731 732 bool nsMenuPopupFrame::ShouldExpandToInflowParentOrAnchor() const { 733 return IsMenuList() && !mContent->GetParent()->AsElement()->AttrValueIs( 734 kNameSpaceID_None, nsGkAtoms::sizetopopup, 735 nsGkAtoms::none, eCaseMatters); 736 } 737 738 nsIContent* nsMenuPopupFrame::GetTriggerContent( 739 nsMenuPopupFrame* aMenuPopupFrame) { 740 while (aMenuPopupFrame) { 741 if (aMenuPopupFrame->mTriggerContent) { 742 return aMenuPopupFrame->mTriggerContent; 743 } 744 745 auto* button = XULButtonElement::FromNodeOrNull( 746 aMenuPopupFrame->GetContent()->GetParent()); 747 if (!button || !button->IsMenu()) { 748 break; 749 } 750 751 auto* popup = button->GetContainingPopupElement(); 752 if (!popup) { 753 break; 754 } 755 756 // check up the menu hierarchy until a popup with a trigger node is found 757 aMenuPopupFrame = do_QueryFrame(popup->GetPrimaryFrame()); 758 } 759 760 return nullptr; 761 } 762 763 void nsMenuPopupFrame::InitPositionFromAnchorAlign(const nsAString& aAnchor, 764 const nsAString& aAlign) { 765 mTriggerContent = nullptr; 766 767 if (aAnchor.EqualsLiteral("topleft")) { 768 mPopupAnchor = POPUPALIGNMENT_TOPLEFT; 769 } else if (aAnchor.EqualsLiteral("topright")) { 770 mPopupAnchor = POPUPALIGNMENT_TOPRIGHT; 771 } else if (aAnchor.EqualsLiteral("bottomleft")) { 772 mPopupAnchor = POPUPALIGNMENT_BOTTOMLEFT; 773 } else if (aAnchor.EqualsLiteral("bottomright")) { 774 mPopupAnchor = POPUPALIGNMENT_BOTTOMRIGHT; 775 } else if (aAnchor.EqualsLiteral("leftcenter")) { 776 mPopupAnchor = POPUPALIGNMENT_LEFTCENTER; 777 } else if (aAnchor.EqualsLiteral("rightcenter")) { 778 mPopupAnchor = POPUPALIGNMENT_RIGHTCENTER; 779 } else if (aAnchor.EqualsLiteral("topcenter")) { 780 mPopupAnchor = POPUPALIGNMENT_TOPCENTER; 781 } else if (aAnchor.EqualsLiteral("bottomcenter")) { 782 mPopupAnchor = POPUPALIGNMENT_BOTTOMCENTER; 783 } else { 784 mPopupAnchor = POPUPALIGNMENT_NONE; 785 } 786 787 if (aAlign.EqualsLiteral("topleft")) { 788 mPopupAlignment = POPUPALIGNMENT_TOPLEFT; 789 } else if (aAlign.EqualsLiteral("topright")) { 790 mPopupAlignment = POPUPALIGNMENT_TOPRIGHT; 791 } else if (aAlign.EqualsLiteral("bottomleft")) { 792 mPopupAlignment = POPUPALIGNMENT_BOTTOMLEFT; 793 } else if (aAlign.EqualsLiteral("bottomright")) { 794 mPopupAlignment = POPUPALIGNMENT_BOTTOMRIGHT; 795 } else if (aAlign.EqualsLiteral("leftcenter")) { 796 mPopupAlignment = POPUPALIGNMENT_LEFTCENTER; 797 } else if (aAlign.EqualsLiteral("rightcenter")) { 798 mPopupAlignment = POPUPALIGNMENT_RIGHTCENTER; 799 } else if (aAlign.EqualsLiteral("topcenter")) { 800 mPopupAlignment = POPUPALIGNMENT_TOPCENTER; 801 } else if (aAlign.EqualsLiteral("bottomcenter")) { 802 mPopupAlignment = POPUPALIGNMENT_BOTTOMCENTER; 803 } else { 804 mPopupAlignment = POPUPALIGNMENT_NONE; 805 } 806 807 mPosition = POPUPPOSITION_UNKNOWN; 808 } 809 810 static FlipType FlipFromAttribute(nsMenuPopupFrame* aFrame) { 811 nsAutoString flip; 812 aFrame->PopupElement().GetAttr(nsGkAtoms::flip, flip); 813 if (flip.EqualsLiteral("none")) { 814 return FlipType::None; 815 } 816 if (flip.EqualsLiteral("both")) { 817 return FlipType::Both; 818 } 819 if (flip.EqualsLiteral("slide")) { 820 return FlipType::Slide; 821 } 822 return FlipType::Default; 823 } 824 825 void nsMenuPopupFrame::InitializePopup(nsIContent* aAnchorContent, 826 nsIContent* aTriggerContent, 827 const nsAString& aPosition, 828 int32_t aXPos, int32_t aYPos, 829 MenuPopupAnchorType aAnchorType, 830 bool aAttributesOverride) { 831 PrepareWidget(); 832 833 mPopupState = ePopupShowing; 834 mAnchorContent = aAnchorContent; 835 mAnchorType = aAnchorType; 836 const nscoord auPerCssPx = AppUnitsPerCSSPixel(); 837 const nsPoint pos = CSSPixel::ToAppUnits(CSSIntPoint(aXPos, aYPos)); 838 // When converted back to CSSIntRect it is (-1, -1, 0, 0) - as expected in 839 // nsXULPopupManager::Rollup 840 mScreenRect = nsRect(-auPerCssPx, -auPerCssPx, 0, 0); 841 mExtraMargin = pos; 842 // If we have no anchor node, anchor to the given position instead. 843 if (mAnchorType == MenuPopupAnchorType::Node && !aAnchorContent) { 844 mAnchorType = MenuPopupAnchorType::Point; 845 mScreenRect = nsRect( 846 pos + PresShell()->GetRootFrame()->GetScreenRectInAppUnits().TopLeft(), 847 nsSize()); 848 mExtraMargin = {}; 849 } 850 mTriggerContent = aTriggerContent; 851 mIsNativeMenu = false; 852 mIsTopLevelContextMenu = false; 853 mVFlip = false; 854 mHFlip = false; 855 mConstrainedByLayout = false; 856 mAlignmentOffset = 0; 857 mPositionedOffset = 0; 858 mPositionedByMoveToRect = false; 859 860 // if aAttributesOverride is true, then the popupanchor, popupalign and 861 // position attributes on the <menupopup> override those values passed in. 862 // If false, those attributes are only used if the values passed in are empty 863 if (aAnchorContent || aAnchorType == MenuPopupAnchorType::Rect) { 864 nsAutoString anchor, align, position; 865 mContent->AsElement()->GetAttr(nsGkAtoms::popupanchor, anchor); 866 mContent->AsElement()->GetAttr(nsGkAtoms::popupalign, align); 867 mContent->AsElement()->GetAttr(nsGkAtoms::position, position); 868 869 if (aAttributesOverride) { 870 // if the attributes are set, clear the offset position. Otherwise, 871 // the offset is used to adjust the position from the anchor point 872 if (anchor.IsEmpty() && align.IsEmpty() && position.IsEmpty()) { 873 position.Assign(aPosition); 874 } 875 } else if (!aPosition.IsEmpty()) { 876 position.Assign(aPosition); 877 } 878 879 mFlip = FlipFromAttribute(this); 880 881 position.CompressWhitespace(); 882 int32_t spaceIdx = position.FindChar(' '); 883 // if there is a space in the position, assume it is the anchor and 884 // alignment as two separate tokens. 885 if (spaceIdx >= 0) { 886 InitPositionFromAnchorAlign(Substring(position, 0, spaceIdx), 887 Substring(position, spaceIdx + 1)); 888 } else if (position.EqualsLiteral("before_start")) { 889 mPopupAnchor = POPUPALIGNMENT_TOPLEFT; 890 mPopupAlignment = POPUPALIGNMENT_BOTTOMLEFT; 891 mPosition = POPUPPOSITION_BEFORESTART; 892 } else if (position.EqualsLiteral("before_end")) { 893 mPopupAnchor = POPUPALIGNMENT_TOPRIGHT; 894 mPopupAlignment = POPUPALIGNMENT_BOTTOMRIGHT; 895 mPosition = POPUPPOSITION_BEFOREEND; 896 } else if (position.EqualsLiteral("after_start")) { 897 mPopupAnchor = POPUPALIGNMENT_BOTTOMLEFT; 898 mPopupAlignment = POPUPALIGNMENT_TOPLEFT; 899 mPosition = POPUPPOSITION_AFTERSTART; 900 } else if (position.EqualsLiteral("after_end")) { 901 mPopupAnchor = POPUPALIGNMENT_BOTTOMRIGHT; 902 mPopupAlignment = POPUPALIGNMENT_TOPRIGHT; 903 mPosition = POPUPPOSITION_AFTEREND; 904 } else if (position.EqualsLiteral("start_before")) { 905 mPopupAnchor = POPUPALIGNMENT_TOPLEFT; 906 mPopupAlignment = POPUPALIGNMENT_TOPRIGHT; 907 mPosition = POPUPPOSITION_STARTBEFORE; 908 } else if (position.EqualsLiteral("start_after")) { 909 mPopupAnchor = POPUPALIGNMENT_BOTTOMLEFT; 910 mPopupAlignment = POPUPALIGNMENT_BOTTOMRIGHT; 911 mPosition = POPUPPOSITION_STARTAFTER; 912 } else if (position.EqualsLiteral("end_before")) { 913 mPopupAnchor = POPUPALIGNMENT_TOPRIGHT; 914 mPopupAlignment = POPUPALIGNMENT_TOPLEFT; 915 mPosition = POPUPPOSITION_ENDBEFORE; 916 } else if (position.EqualsLiteral("end_after")) { 917 mPopupAnchor = POPUPALIGNMENT_BOTTOMRIGHT; 918 mPopupAlignment = POPUPALIGNMENT_BOTTOMLEFT; 919 mPosition = POPUPPOSITION_ENDAFTER; 920 } else if (position.EqualsLiteral("overlap")) { 921 mPopupAnchor = POPUPALIGNMENT_TOPLEFT; 922 mPopupAlignment = POPUPALIGNMENT_TOPLEFT; 923 mPosition = POPUPPOSITION_OVERLAP; 924 } else if (position.EqualsLiteral("selection")) { 925 mPopupAnchor = POPUPALIGNMENT_BOTTOMLEFT; 926 mPopupAlignment = POPUPALIGNMENT_TOPLEFT; 927 mPosition = POPUPPOSITION_SELECTION; 928 } else { 929 InitPositionFromAnchorAlign(anchor, align); 930 } 931 } 932 mUntransformedPopupAnchor = mPopupAnchor; 933 mUntransformedPopupAlignment = mPopupAlignment; 934 935 if (aAttributesOverride) { 936 // Use |left| and |top| dimension attributes to position the popup if 937 // present, as they may have been persisted. 938 nsAutoString left, top; 939 mContent->AsElement()->GetAttr(nsGkAtoms::left, left); 940 mContent->AsElement()->GetAttr(nsGkAtoms::top, top); 941 942 nsresult err; 943 if (!left.IsEmpty()) { 944 int32_t x = left.ToInteger(&err); 945 if (NS_SUCCEEDED(err)) { 946 mScreenRect.x = CSSPixel::ToAppUnits(x); 947 } 948 } 949 if (!top.IsEmpty()) { 950 int32_t y = top.ToInteger(&err); 951 if (NS_SUCCEEDED(err)) { 952 mScreenRect.y = CSSPixel::ToAppUnits(y); 953 } 954 } 955 } 956 } 957 958 void nsMenuPopupFrame::InitializePopupAtScreen(nsIContent* aTriggerContent, 959 int32_t aXPos, int32_t aYPos, 960 bool aIsContextMenu) { 961 PrepareWidget(); 962 963 mPopupState = ePopupShowing; 964 mAnchorContent = nullptr; 965 mTriggerContent = aTriggerContent; 966 mScreenRect = 967 nsRect(CSSPixel::ToAppUnits(CSSIntPoint(aXPos, aYPos)), nsSize()); 968 mExtraMargin = {}; 969 mFlip = FlipFromAttribute(this); 970 mPopupAnchor = POPUPALIGNMENT_NONE; 971 mPopupAlignment = POPUPALIGNMENT_NONE; 972 mPosition = POPUPPOSITION_UNKNOWN; 973 mIsContextMenu = aIsContextMenu; 974 mIsTopLevelContextMenu = aIsContextMenu; 975 mIsNativeMenu = false; 976 mAnchorType = MenuPopupAnchorType::Point; 977 mPositionedOffset = 0; 978 mPositionedByMoveToRect = false; 979 } 980 981 void nsMenuPopupFrame::InitializePopupAsNativeContextMenu( 982 nsIContent* aTriggerContent, int32_t aXPos, int32_t aYPos) { 983 mTriggerContent = aTriggerContent; 984 mPopupState = ePopupShowing; 985 mAnchorContent = nullptr; 986 mScreenRect = 987 nsRect(CSSPixel::ToAppUnits(CSSIntPoint(aXPos, aYPos)), nsSize()); 988 mExtraMargin = {}; 989 mFlip = FlipType::Default; 990 mPopupAnchor = POPUPALIGNMENT_NONE; 991 mPopupAlignment = POPUPALIGNMENT_NONE; 992 mPosition = POPUPPOSITION_UNKNOWN; 993 mIsContextMenu = true; 994 mIsTopLevelContextMenu = true; 995 mIsNativeMenu = true; 996 mAnchorType = MenuPopupAnchorType::Point; 997 mPositionedOffset = 0; 998 mPositionedByMoveToRect = false; 999 // Native context menus don't call PrepareWidget(), so if we have a widget 1000 // already (which generally should only be possible on tests, since 1001 // otherwise we shouldn't ever mix native / non-native for the same popup) we 1002 // should destroy it now. 1003 if (mExpirationState.IsTracked()) { 1004 PopupExpirationTracker::Get()->RemoveObject(this); 1005 } 1006 DestroyWidget(); 1007 } 1008 1009 void nsMenuPopupFrame::InitializePopupAtRect(nsIContent* aTriggerContent, 1010 const nsAString& aPosition, 1011 const nsIntRect& aRect, 1012 bool aAttributesOverride) { 1013 InitializePopup(nullptr, aTriggerContent, aPosition, 0, 0, 1014 MenuPopupAnchorType::Rect, aAttributesOverride); 1015 mScreenRect = ToAppUnits(aRect, AppUnitsPerCSSPixel()); 1016 } 1017 1018 void nsMenuPopupFrame::ShowPopup(bool aIsContextMenu) { 1019 mIsContextMenu = aIsContextMenu; 1020 1021 InvalidateFrameSubtree(); 1022 SchedulePendingWidgetMoveResize(); 1023 1024 if (mPopupState == ePopupShowing || mPopupState == ePopupPositioning) { 1025 mPopupState = ePopupOpening; 1026 mIsOpenChanged = true; 1027 1028 // Clear mouse capture when a popup is opened. 1029 if (mPopupType == PopupType::Menu) { 1030 if (auto* activeESM = EventStateManager::GetActiveEventStateManager()) { 1031 EventStateManager::ClearGlobalActiveContent(activeESM); 1032 } 1033 1034 PresShell::ReleaseCapturingContent(); 1035 } 1036 1037 if (RefPtr menu = PopupElement().GetContainingMenu()) { 1038 menu->PopupOpened(); 1039 } 1040 1041 // We skip laying out children if we're closed, so make sure that we do a 1042 // full dirty reflow when opening to pick up any potential change. 1043 PresShell()->FrameNeedsReflow( 1044 this, IntrinsicDirty::FrameAncestorsAndDescendants, NS_FRAME_IS_DIRTY); 1045 1046 if (mPopupType == PopupType::Menu) { 1047 nsCOMPtr<nsISound> sound(do_GetService("@mozilla.org/sound;1")); 1048 if (sound) { 1049 sound->PlayEventSound(nsISound::EVENT_MENU_POPUP); 1050 } 1051 } 1052 } 1053 } 1054 1055 void nsMenuPopupFrame::ClearTriggerContentIncludingDocument() { 1056 // clear the trigger content if the popup is being closed. But don't clear 1057 // it if the popup is just being made invisible as a popuphiding or command 1058 if (mTriggerContent) { 1059 // if the popup had a trigger node set, clear the global window popup node 1060 // as well 1061 Document* doc = mContent->GetUncomposedDoc(); 1062 if (doc) { 1063 if (nsPIDOMWindowOuter* win = doc->GetWindow()) { 1064 nsCOMPtr<nsPIWindowRoot> root = win->GetTopWindowRoot(); 1065 if (root) { 1066 root->SetPopupNode(nullptr); 1067 } 1068 } 1069 } 1070 } 1071 mTriggerContent = nullptr; 1072 } 1073 1074 void nsMenuPopupFrame::HidePopup(bool aDeselectMenu, nsPopupState aNewState, 1075 bool aFromFrameDestruction) { 1076 NS_ASSERTION(aNewState == ePopupClosed || aNewState == ePopupInvisible, 1077 "popup being set to unexpected state"); 1078 1079 ClearPopupShownDispatcher(); 1080 1081 // don't hide the popup when it isn't open 1082 if (mPopupState == ePopupClosed || mPopupState == ePopupShowing || 1083 mPopupState == ePopupPositioning) { 1084 return; 1085 } 1086 1087 if (aNewState == ePopupClosed) { 1088 // clear the trigger content if the popup is being closed. But don't clear 1089 // it if the popup is just being made invisible as a popuphiding or command 1090 // event may want to retrieve it. 1091 ClearTriggerContentIncludingDocument(); 1092 mAnchorContent = nullptr; 1093 } 1094 1095 // when invisible and about to be closed, HidePopup has already been called, 1096 // so just set the new state to closed and return 1097 if (mPopupState == ePopupInvisible) { 1098 if (aNewState == ePopupClosed) { 1099 mPopupState = ePopupClosed; 1100 } 1101 return; 1102 } 1103 1104 mPopupState = aNewState; 1105 1106 mIncrementalString.Truncate(); 1107 1108 mIsOpenChanged = false; 1109 mHFlip = mVFlip = false; 1110 mConstrainedByLayout = false; 1111 1112 RefPtr widget = GetWidget(); 1113 if (widget) { 1114 widget->ClearCachedWebrenderResources(); 1115 if (!aFromFrameDestruction && !ShouldHaveWidgetWhenHidden()) { 1116 PopupExpirationTracker::GetOrCreate().AddObject(this); 1117 } 1118 } 1119 1120 ClearPendingWidgetMoveResize(); 1121 RefPtr popup = &PopupElement(); 1122 // XXX, bug 137033, In Windows, if mouse is outside the window when the 1123 // menupopup closes, no mouse_enter/mouse_exit event will be fired to clear 1124 // current hover state, we should clear it manually. This code may not the 1125 // best solution, but we can leave it here until we find the better approach. 1126 if (!aFromFrameDestruction && 1127 popup->State().HasState(dom::ElementState::HOVER)) { 1128 EventStateManager* esm = PresContext()->EventStateManager(); 1129 esm->SetContentState(nullptr, dom::ElementState::HOVER); 1130 } 1131 popup->PopupClosed(aDeselectMenu); 1132 1133 if (widget) { 1134 nsContentUtils::AddScriptRunner( 1135 NS_NewRunnableFunction("HideWidget", [widget = std::move(widget)] { 1136 auto* frame = widget->GetPopupFrame(); 1137 if (!frame || !frame->IsVisibleOrShowing()) { 1138 widget->Show(false); 1139 } 1140 })); 1141 } 1142 } 1143 1144 void nsMenuPopupFrame::SchedulePendingWidgetMoveResize() { 1145 if (mPendingWidgetMoveResize) { 1146 return; 1147 } 1148 mPendingWidgetMoveResize = true; 1149 SchedulePaint(); 1150 } 1151 1152 nsPoint nsMenuPopupFrame::AdjustPositionForAnchorAlign( 1153 nsRect& anchorRect, const nsSize& aPrefSize, FlipStyle& aHFlip, 1154 FlipStyle& aVFlip) const { 1155 // flip the anchor and alignment for right-to-left 1156 int8_t popupAnchor(mPopupAnchor); 1157 int8_t popupAlign(mPopupAlignment); 1158 if (IsDirectionRTL()) { 1159 // no need to flip the centered anchor types vertically 1160 if (popupAnchor <= POPUPALIGNMENT_LEFTCENTER) { 1161 popupAnchor = -popupAnchor; 1162 } 1163 popupAlign = -popupAlign; 1164 } 1165 1166 nsRect originalAnchorRect(anchorRect); 1167 1168 // first, determine at which corner of the anchor the popup should appear 1169 nsPoint pnt; 1170 switch (popupAnchor) { 1171 case POPUPALIGNMENT_LEFTCENTER: 1172 pnt = nsPoint(anchorRect.x, anchorRect.y + anchorRect.height / 2); 1173 anchorRect.y = pnt.y; 1174 anchorRect.height = 0; 1175 break; 1176 case POPUPALIGNMENT_RIGHTCENTER: 1177 pnt = nsPoint(anchorRect.XMost(), anchorRect.y + anchorRect.height / 2); 1178 anchorRect.y = pnt.y; 1179 anchorRect.height = 0; 1180 break; 1181 case POPUPALIGNMENT_TOPCENTER: 1182 pnt = nsPoint(anchorRect.x + anchorRect.width / 2, anchorRect.y); 1183 anchorRect.x = pnt.x; 1184 anchorRect.width = 0; 1185 break; 1186 case POPUPALIGNMENT_BOTTOMCENTER: 1187 pnt = nsPoint(anchorRect.x + anchorRect.width / 2, anchorRect.YMost()); 1188 anchorRect.x = pnt.x; 1189 anchorRect.width = 0; 1190 break; 1191 case POPUPALIGNMENT_TOPRIGHT: 1192 pnt = anchorRect.TopRight(); 1193 break; 1194 case POPUPALIGNMENT_BOTTOMLEFT: 1195 pnt = anchorRect.BottomLeft(); 1196 break; 1197 case POPUPALIGNMENT_BOTTOMRIGHT: 1198 pnt = anchorRect.BottomRight(); 1199 break; 1200 case POPUPALIGNMENT_TOPLEFT: 1201 default: 1202 pnt = anchorRect.TopLeft(); 1203 break; 1204 } 1205 1206 // If the alignment is on the right edge of the popup, move the popup left 1207 // by the width. Similarly, if the alignment is on the bottom edge of the 1208 // popup, move the popup up by the height. In addition, account for the 1209 // margins of the popup on the edge on which it is aligned. 1210 nsMargin margin = GetMargin(); 1211 switch (popupAlign) { 1212 case POPUPALIGNMENT_LEFTCENTER: 1213 pnt.MoveBy(margin.left, -aPrefSize.height / 2); 1214 break; 1215 case POPUPALIGNMENT_RIGHTCENTER: 1216 pnt.MoveBy(-aPrefSize.width - margin.right, -aPrefSize.height / 2); 1217 break; 1218 case POPUPALIGNMENT_TOPCENTER: 1219 pnt.MoveBy(-aPrefSize.width / 2, margin.top); 1220 break; 1221 case POPUPALIGNMENT_BOTTOMCENTER: 1222 pnt.MoveBy(-aPrefSize.width / 2, -aPrefSize.height - margin.bottom); 1223 break; 1224 case POPUPALIGNMENT_TOPRIGHT: 1225 pnt.MoveBy(-aPrefSize.width - margin.right, margin.top); 1226 break; 1227 case POPUPALIGNMENT_BOTTOMLEFT: 1228 pnt.MoveBy(margin.left, -aPrefSize.height - margin.bottom); 1229 break; 1230 case POPUPALIGNMENT_BOTTOMRIGHT: 1231 pnt.MoveBy(-aPrefSize.width - margin.right, 1232 -aPrefSize.height - margin.bottom); 1233 break; 1234 case POPUPALIGNMENT_TOPLEFT: 1235 default: 1236 pnt.MoveBy(margin.left, margin.top); 1237 break; 1238 } 1239 1240 // If we aligning to the selected item in the popup, adjust the vertical 1241 // position by the height of the menulist label and the selected item's 1242 // position. 1243 if (mPosition == POPUPPOSITION_SELECTION) { 1244 MOZ_ASSERT(popupAnchor == POPUPALIGNMENT_BOTTOMLEFT || 1245 popupAnchor == POPUPALIGNMENT_BOTTOMRIGHT); 1246 MOZ_ASSERT(popupAlign == POPUPALIGNMENT_TOPLEFT || 1247 popupAlign == POPUPALIGNMENT_TOPRIGHT); 1248 1249 // Only adjust the popup if it just opened, otherwise the popup will move 1250 // around if its gets resized or the selection changed. Cache the value in 1251 // mPositionedOffset and use that instead for any future calculations. 1252 if (mIsOpenChanged) { 1253 if (nsIFrame* selectedItemFrame = GetSelectedItemForAlignment()) { 1254 const nscoord itemHeight = selectedItemFrame->GetRect().height; 1255 const nscoord itemOffset = 1256 selectedItemFrame->GetOffsetToIgnoringScrolling(this).y; 1257 // We want to line-up the anchor rect with the selected item, but if the 1258 // selected item is outside of our bounds, we don't want to shift the 1259 // popup up in a way that our box would no longer intersect with the 1260 // anchor. 1261 nscoord maxOffset = aPrefSize.height - itemHeight; 1262 if (const ScrollContainerFrame* sf = GetScrollContainerFrame()) { 1263 // HACK: We ideally would want to use the offset from the bottom 1264 // bottom of our scroll-frame to the bottom of our frame (so as to 1265 // ensure that the bottom of the scrollport is inside the anchor 1266 // rect). 1267 // 1268 // But at this point of the code, the scroll frame may not be laid out 1269 // with a definite size (might be overflowing us). 1270 // 1271 // So, we assume the offset from the bottom is symmetric to the offset 1272 // from the top. This holds for all the popups where this matters 1273 // (menulists on macOS, effectively), and seems better than somehow 1274 // moving the popup after the fact as we used to do. 1275 maxOffset -= sf->GetOffsetTo(this).y; 1276 } 1277 mPositionedOffset = 1278 originalAnchorRect.height + std::min(itemOffset, maxOffset); 1279 } 1280 } 1281 1282 pnt.y -= mPositionedOffset; 1283 } 1284 1285 // Flipping horizontally is allowed as long as the popup is above or below 1286 // the anchor. This will happen if both the anchor and alignment are top or 1287 // both are bottom, but different values. Similarly, flipping vertically is 1288 // allowed if the popup is to the left or right of the anchor. In this case, 1289 // the values of the constants are such that both must be positive or both 1290 // must be negative. A special case, used for overlap, allows flipping 1291 // vertically as well. 1292 // If we are flipping in both directions, we want to set a flip style both 1293 // horizontally and vertically. However, we want to flip on the inside edge 1294 // of the anchor. Consider the example of a typical dropdown menu. 1295 // Vertically, we flip the popup on the outside edges of the anchor menu, 1296 // however horizontally, we want to to use the inside edges so the popup 1297 // still appears underneath the anchor menu instead of floating off the 1298 // side of the menu. 1299 switch (popupAnchor) { 1300 case POPUPALIGNMENT_LEFTCENTER: 1301 case POPUPALIGNMENT_RIGHTCENTER: 1302 aHFlip = FlipStyle::Outside; 1303 aVFlip = FlipStyle::Inside; 1304 break; 1305 case POPUPALIGNMENT_TOPCENTER: 1306 case POPUPALIGNMENT_BOTTOMCENTER: 1307 aHFlip = FlipStyle::Inside; 1308 aVFlip = FlipStyle::Outside; 1309 break; 1310 default: { 1311 FlipStyle anchorEdge = 1312 mFlip == FlipType::Both ? FlipStyle::Inside : FlipStyle::None; 1313 aHFlip = (popupAnchor == -popupAlign) ? FlipStyle::Outside : anchorEdge; 1314 if (((popupAnchor > 0) == (popupAlign > 0)) || 1315 (popupAnchor == POPUPALIGNMENT_TOPLEFT && 1316 popupAlign == POPUPALIGNMENT_TOPLEFT)) { 1317 aVFlip = FlipStyle::Outside; 1318 } else { 1319 aVFlip = anchorEdge; 1320 } 1321 break; 1322 } 1323 } 1324 1325 return pnt; 1326 } 1327 1328 nsIFrame* nsMenuPopupFrame::GetSelectedItemForAlignment() const { 1329 // This method adjusts a menulist's popup such that the selected item is under 1330 // the cursor, aligned with the menulist label. 1331 nsCOMPtr<nsIDOMXULSelectControlElement> select; 1332 if (mAnchorContent) { 1333 select = mAnchorContent->AsElement()->AsXULSelectControl(); 1334 } 1335 1336 if (!select) { 1337 // If there isn't an anchor, then try just getting the parent of the popup. 1338 select = mContent->GetParent()->AsElement()->AsXULSelectControl(); 1339 if (!select) { 1340 return nullptr; 1341 } 1342 } 1343 1344 nsCOMPtr<Element> selectedElement; 1345 select->GetSelectedItem(getter_AddRefs(selectedElement)); 1346 return selectedElement ? selectedElement->GetPrimaryFrame() : nullptr; 1347 } 1348 1349 nscoord nsMenuPopupFrame::SlideOrResize(nscoord& aScreenPoint, nscoord aSize, 1350 nscoord aScreenBegin, 1351 nscoord aScreenEnd, 1352 nscoord* aOffset) const { 1353 // The popup may be positioned such that either the left/top or bottom/right 1354 // is outside the screen - but never both. 1355 nscoord newPos = 1356 std::max(aScreenBegin, std::min(aScreenEnd - aSize, aScreenPoint)); 1357 *aOffset = newPos - aScreenPoint; 1358 aScreenPoint = newPos; 1359 return std::min(aSize, aScreenEnd - aScreenPoint); 1360 } 1361 1362 nscoord nsMenuPopupFrame::FlipOrResize(nscoord& aScreenPoint, nscoord aSize, 1363 nscoord aScreenBegin, nscoord aScreenEnd, 1364 nscoord aAnchorBegin, nscoord aAnchorEnd, 1365 nscoord aMarginBegin, nscoord aMarginEnd, 1366 FlipStyle aFlip, bool aEndAligned, 1367 bool* aFlipSide) const { 1368 // The flip side argument will be set to true if there wasn't room and we 1369 // flipped to the opposite side. 1370 *aFlipSide = false; 1371 1372 // all of the coordinates used here are in app units relative to the screen 1373 nscoord popupSize = aSize; 1374 if (aScreenPoint < aScreenBegin) { 1375 // at its current position, the popup would extend past the left or top 1376 // edge of the screen, so it will have to be moved or resized. 1377 if (aFlip != FlipStyle::None) { 1378 // for inside flips, we flip on the opposite side of the anchor 1379 nscoord startpos = 1380 aFlip == FlipStyle::Outside ? aAnchorBegin : aAnchorEnd; 1381 nscoord endpos = aFlip == FlipStyle::Outside ? aAnchorEnd : aAnchorBegin; 1382 1383 // check whether there is more room to the left and right (or top and 1384 // bottom) of the anchor and put the popup on the side with more room. 1385 if (startpos - aScreenBegin >= aScreenEnd - endpos) { 1386 aScreenPoint = aScreenBegin; 1387 popupSize = startpos - aScreenPoint - aMarginEnd; 1388 *aFlipSide = !aEndAligned; 1389 } else { 1390 // If the newly calculated position is different than the existing 1391 // position, flip such that the popup is to the right or bottom of the 1392 // anchor point instead . However, when flipping use the same margin 1393 // size. 1394 nscoord newScreenPoint = endpos + aMarginEnd; 1395 if (newScreenPoint != aScreenPoint) { 1396 *aFlipSide = aEndAligned; 1397 aScreenPoint = newScreenPoint; 1398 // check if the new position is still off the right or bottom edge of 1399 // the screen. If so, resize the popup. 1400 if (aScreenPoint + aSize > aScreenEnd) { 1401 popupSize = aScreenEnd - aScreenPoint; 1402 } 1403 } 1404 } 1405 } else { 1406 aScreenPoint = aScreenBegin; 1407 } 1408 } else if (aScreenPoint + aSize > aScreenEnd) { 1409 // at its current position, the popup would extend past the right or 1410 // bottom edge of the screen, so it will have to be moved or resized. 1411 if (aFlip != FlipStyle::None) { 1412 // for inside flips, we flip on the opposite side of the anchor 1413 nscoord startpos = 1414 aFlip == FlipStyle::Outside ? aAnchorBegin : aAnchorEnd; 1415 nscoord endpos = aFlip == FlipStyle::Outside ? aAnchorEnd : aAnchorBegin; 1416 1417 // check whether there is more room to the left and right (or top and 1418 // bottom) of the anchor and put the popup on the side with more room. 1419 if (aScreenEnd - endpos >= startpos - aScreenBegin) { 1420 *aFlipSide = aEndAligned; 1421 if (mIsContextMenu) { 1422 aScreenPoint = aScreenEnd - aSize; 1423 } else { 1424 aScreenPoint = endpos + aMarginBegin; 1425 popupSize = aScreenEnd - aScreenPoint; 1426 } 1427 } else { 1428 // if the newly calculated position is different than the existing 1429 // position, we flip such that the popup is to the left or top of the 1430 // anchor point instead. 1431 nscoord newScreenPoint = startpos - aSize - aMarginBegin; 1432 if (newScreenPoint != aScreenPoint) { 1433 *aFlipSide = !aEndAligned; 1434 aScreenPoint = newScreenPoint; 1435 1436 // check if the new position is still off the left or top edge of the 1437 // screen. If so, resize the popup. 1438 if (aScreenPoint < aScreenBegin) { 1439 aScreenPoint = aScreenBegin; 1440 if (!mIsContextMenu) { 1441 popupSize = startpos - aScreenPoint - aMarginBegin; 1442 } 1443 } 1444 } 1445 } 1446 } else { 1447 aScreenPoint = aScreenEnd - aSize; 1448 } 1449 } 1450 1451 // Make sure that the point is within the screen boundaries and that the 1452 // size isn't off the edge of the screen. This can happen when a large 1453 // positive or negative margin is used. 1454 if (aScreenPoint < aScreenBegin) { 1455 aScreenPoint = aScreenBegin; 1456 } 1457 if (aScreenPoint > aScreenEnd) { 1458 aScreenPoint = aScreenEnd - aSize; 1459 } 1460 1461 // If popupSize ended up being negative, or the original size was actually 1462 // smaller than the calculated popup size, just use the original size instead. 1463 if (popupSize <= 0 || aSize < popupSize) { 1464 popupSize = aSize; 1465 } 1466 1467 return std::min(popupSize, aScreenEnd - aScreenPoint); 1468 } 1469 1470 nsRect nsMenuPopupFrame::ComputeAnchorRect(nsPresContext* aRootPresContext, 1471 nsIFrame* aAnchorFrame) const { 1472 // Get the root frame for a reference 1473 nsIFrame* rootFrame = aRootPresContext->PresShell()->GetRootFrame(); 1474 1475 // The dimensions of the anchor 1476 nsRect anchorRect = aAnchorFrame->GetRectRelativeToSelf(); 1477 1478 // Relative to the root 1479 anchorRect = nsLayoutUtils::TransformFrameRectToAncestor( 1480 aAnchorFrame, anchorRect, rootFrame); 1481 // Relative to the screen 1482 anchorRect.MoveBy(rootFrame->GetScreenRectInAppUnits().TopLeft()); 1483 1484 // In its own app units 1485 return anchorRect.ScaleToOtherAppUnitsRoundOut( 1486 aRootPresContext->AppUnitsPerDevPixel(), 1487 PresContext()->AppUnitsPerDevPixel()); 1488 } 1489 1490 static nsIFrame* MaybeDelegatedAnchorFrame(nsIFrame* aFrame) { 1491 if (!aFrame) { 1492 return nullptr; 1493 } 1494 if (auto* element = Element::FromNodeOrNull(aFrame->GetContent())) { 1495 if (element->HasAttr(nsGkAtoms::delegatesanchor)) { 1496 for (nsIFrame* f : aFrame->PrincipalChildList()) { 1497 if (!f->IsPlaceholderFrame()) { 1498 return f; 1499 } 1500 } 1501 } 1502 } 1503 return aFrame; 1504 } 1505 1506 auto nsMenuPopupFrame::GetRects(const nsSize& aPrefSize) const -> Rects { 1507 if (NS_WARN_IF(aPrefSize == nsSize(-1, -1))) { 1508 // Return early if the popup hasn't been laid out yet. On Windows, this can 1509 // happen when using a drag popup before it opens. 1510 return {}; 1511 } 1512 1513 nsPresContext* pc = PresContext(); 1514 nsIFrame* rootFrame = pc->PresShell()->GetRootFrame(); 1515 1516 // Indicators of whether the popup should be flipped or resized. 1517 FlipStyle hFlip = FlipStyle::None; 1518 FlipStyle vFlip = FlipStyle::None; 1519 1520 const nsMargin margin = GetMargin(); 1521 1522 // the screen rectangle of the root frame, in dev pixels. 1523 const nsRect rootScreenRect = rootFrame->GetScreenRectInAppUnits(); 1524 1525 const bool isNoAutoHide = IsNoAutoHide(); 1526 const PopupLevel popupLevel = GetPopupLevel(isNoAutoHide); 1527 1528 Rects result; 1529 1530 // Set the popup's size to the preferred size. Below, this size will be 1531 // adjusted to fit on the screen or within the content area. If the anchor is 1532 // sized to the popup, use the anchor's width instead of the preferred width. 1533 result.mUsedRect = nsRect(nsPoint(), aPrefSize); 1534 1535 const bool anchored = IsAnchored(); 1536 if (anchored) { 1537 // In order to deal with transforms, we need the root prescontext: 1538 nsPresContext* rootPc = pc->GetRootPresContext(); 1539 if (NS_WARN_IF(!rootPc)) { 1540 // If we can't reach a root pres context, don't bother continuing. 1541 return result; 1542 } 1543 1544 result.mAnchorRect = result.mUntransformedAnchorRect = [&] { 1545 // If anchored to a rectangle, use that rectangle. Otherwise, determine 1546 // the rectangle from the anchor. 1547 if (mAnchorType == MenuPopupAnchorType::Rect) { 1548 return mScreenRect; 1549 } 1550 // if the frame is not specified, use the anchor node passed to OpenPopup. 1551 // If that wasn't specified either, use the root frame. Note that 1552 // mAnchorContent might be a different document so its presshell must be 1553 // used. 1554 nsIFrame* anchorFrame = GetAnchorFrame(); 1555 if (!anchorFrame) { 1556 return rootScreenRect; 1557 } 1558 return ComputeAnchorRect(rootPc, anchorFrame); 1559 }(); 1560 1561 // if we are anchored, there are certain things we don't want to do when 1562 // repositioning the popup to fit on the screen, such as end up positioned 1563 // over the anchor, for instance a popup appearing over the menu label. 1564 // When doing this reposition, we want to move the popup to the side with 1565 // the most room. The combination of anchor and alignment dictate if we 1566 // readjust above/below or to the left/right. 1567 if (mAnchorContent || mAnchorType == MenuPopupAnchorType::Rect) { 1568 // move the popup according to the anchor and alignment. This will also 1569 // tell us which axis the popup is flush against in case we have to move 1570 // it around later. The AdjustPositionForAnchorAlign method accounts for 1571 // the popup's margin. 1572 result.mUsedRect.MoveTo(AdjustPositionForAnchorAlign( 1573 result.mAnchorRect, aPrefSize, hFlip, vFlip)); 1574 } else { 1575 // With no anchor, the popup is positioned relative to the root frame. 1576 result.mUsedRect.MoveTo(result.mAnchorRect.TopLeft() + 1577 nsPoint(margin.left, margin.top)); 1578 } 1579 } else { 1580 // Not anchored, use mScreenRect 1581 result.mUsedRect.MoveTo(mScreenRect.TopLeft()); 1582 result.mAnchorRect = result.mUntransformedAnchorRect = 1583 nsRect(mScreenRect.TopLeft(), nsSize()); 1584 1585 // Right-align RTL context menus, and apply margin and offsets as per the 1586 // platform conventions. 1587 if (mIsContextMenu && IsDirectionRTL()) { 1588 result.mUsedRect.x -= aPrefSize.Width(); 1589 result.mUsedRect.MoveBy(-margin.right, margin.top); 1590 } else { 1591 result.mUsedRect.MoveBy(margin.left, margin.top); 1592 } 1593 #ifdef XP_MACOSX 1594 // On macOS, tooltips follow standard flip rule but other popups like 1595 // context menus flip horizontally, not vertically. 1596 if (mPopupType == PopupType::Tooltip) { 1597 vFlip = FlipStyle::Outside; 1598 } else { 1599 hFlip = FlipStyle::Outside; 1600 } 1601 #else 1602 // On Windows and Linux, other OS screen positioned popups can be flipped 1603 // vertically. Only the context menu can be flipped horizontally as well, in 1604 // order to avoid showing the context menu accidentally under the mouse. 1605 vFlip = FlipStyle::Outside; 1606 if (mIsContextMenu) { 1607 hFlip = FlipStyle::Outside; 1608 } 1609 #endif // #ifdef XP_MACOSX 1610 } 1611 1612 const int32_t a2d = pc->AppUnitsPerDevPixel(); 1613 1614 nsIWidget* widget = mWidget; 1615 1616 // If a panel has flip="none", don't constrain or flip it. 1617 // Also, always do this for content shells, so that the popup doesn't extend 1618 // outside the containing frame. 1619 if (mInContentShell || mFlip != FlipType::None) { 1620 const Maybe<nsRect> constraintRect = 1621 GetConstraintRect(result.mAnchorRect, rootScreenRect, popupLevel); 1622 1623 if (constraintRect) { 1624 // Ensure that anchorRect is on the constraint rect. 1625 result.mAnchorRect = result.mAnchorRect.Intersect(*constraintRect); 1626 // Shrink the popup down if it is larger than the constraint size 1627 if (result.mUsedRect.width > constraintRect->width) { 1628 result.mUsedRect.width = constraintRect->width; 1629 } 1630 if (result.mUsedRect.height > constraintRect->height) { 1631 result.mUsedRect.height = constraintRect->height; 1632 } 1633 result.mConstrainedByLayout = true; 1634 } 1635 1636 if (IS_WAYLAND_DISPLAY() && widget) { 1637 // Shrink the popup down if it's larger than popup size received from 1638 // Wayland compositor. We don't know screen size on Wayland so this is the 1639 // only info we have there. 1640 const nsSize waylandSize = LayoutDeviceIntRect::ToAppUnits( 1641 widget->GetMoveToRectPopupSize(), a2d); 1642 1643 LOG_WAYLAND_VERBOSE( 1644 "[%p] Wayland popup size from layout [%d x %d] a2d %d", widget, 1645 result.mUsedRect.width / a2d, result.mUsedRect.height / a2d, a2d); 1646 LOG_WAYLAND_VERBOSE( 1647 "[%p] Wayland popup size from last move-to-rect [%d x %d] a2d %d", 1648 widget, widget->GetMoveToRectPopupSize().width, 1649 widget->GetMoveToRectPopupSize().height, a2d); 1650 1651 if (waylandSize.width > 0 && result.mUsedRect.width > waylandSize.width) { 1652 LOG_WAYLAND("[%p] Wayland constraint width %d to %d", widget, 1653 result.mUsedRect.width, waylandSize.width); 1654 result.mUsedRect.width = waylandSize.width; 1655 } 1656 if (waylandSize.height > 0 && 1657 result.mUsedRect.height > waylandSize.height) { 1658 LOG_WAYLAND("[%p] Wayland constraint height %d to %d", widget, 1659 result.mUsedRect.height, waylandSize.height); 1660 result.mUsedRect.height = waylandSize.height; 1661 } 1662 if (RefPtr<widget::Screen> s = widget->GetWidgetScreen()) { 1663 const nsSize screenSize = 1664 LayoutDeviceIntSize::ToAppUnits(s->GetAvailRect().Size(), a2d); 1665 LOG_WAYLAND_VERBOSE("[%p] Wayland screen size [%d x %d] a2d %d", widget, 1666 s->GetAvailRect().Size().width, 1667 s->GetAvailRect().Size().height, a2d); 1668 1669 if (result.mUsedRect.height > screenSize.height) { 1670 LOG_WAYLAND("[%p] Wayland constraint height to screen %d to %d", 1671 widget, result.mUsedRect.height / a2d, 1672 screenSize.height / a2d); 1673 result.mUsedRect.height = screenSize.height; 1674 } 1675 if (result.mUsedRect.width > screenSize.width) { 1676 LOG_WAYLAND("[%p] Wayland constraint widthto screen %d to %d", widget, 1677 result.mUsedRect.width / a2d, screenSize.width / a2d); 1678 result.mUsedRect.width = screenSize.width; 1679 } 1680 } 1681 } 1682 1683 // At this point the anchor (anchorRect) is within the available screen 1684 // area (constraintRect) and the popup is known to be no larger than the 1685 // screen. 1686 if (constraintRect) { 1687 // We might want to "slide" an arrow if the panel is of the correct type - 1688 // but we can only slide on one axis - the other axis must be "flipped or 1689 // resized" as normal. 1690 bool slideHorizontal = false, slideVertical = false; 1691 if (mFlip == FlipType::Slide) { 1692 int8_t position = GetAlignmentPosition(); 1693 slideHorizontal = position >= POPUPPOSITION_BEFORESTART && 1694 position <= POPUPPOSITION_AFTEREND; 1695 slideVertical = position >= POPUPPOSITION_STARTBEFORE && 1696 position <= POPUPPOSITION_ENDAFTER; 1697 } 1698 1699 // Next, check if there is enough space to show the popup at full size 1700 // when positioned at screenPoint. If not, flip the popups to the opposite 1701 // side of their anchor point, or resize them as necessary. 1702 if (slideHorizontal) { 1703 result.mUsedRect.width = SlideOrResize( 1704 result.mUsedRect.x, result.mUsedRect.width, constraintRect->x, 1705 constraintRect->XMost(), &result.mAlignmentOffset); 1706 } else { 1707 const bool endAligned = 1708 IsDirectionRTL() 1709 ? mPopupAlignment == POPUPALIGNMENT_TOPLEFT || 1710 mPopupAlignment == POPUPALIGNMENT_BOTTOMLEFT || 1711 mPopupAlignment == POPUPALIGNMENT_LEFTCENTER 1712 : mPopupAlignment == POPUPALIGNMENT_TOPRIGHT || 1713 mPopupAlignment == POPUPALIGNMENT_BOTTOMRIGHT || 1714 mPopupAlignment == POPUPALIGNMENT_RIGHTCENTER; 1715 result.mUsedRect.width = FlipOrResize( 1716 result.mUsedRect.x, result.mUsedRect.width, constraintRect->x, 1717 constraintRect->XMost(), result.mAnchorRect.x, 1718 result.mAnchorRect.XMost(), margin.left, margin.right, hFlip, 1719 endAligned, &result.mHFlip); 1720 } 1721 if (slideVertical) { 1722 result.mUsedRect.height = SlideOrResize( 1723 result.mUsedRect.y, result.mUsedRect.height, constraintRect->y, 1724 constraintRect->YMost(), &result.mAlignmentOffset); 1725 } else { 1726 bool endAligned = mPopupAlignment == POPUPALIGNMENT_BOTTOMLEFT || 1727 mPopupAlignment == POPUPALIGNMENT_BOTTOMRIGHT || 1728 mPopupAlignment == POPUPALIGNMENT_BOTTOMCENTER; 1729 result.mUsedRect.height = FlipOrResize( 1730 result.mUsedRect.y, result.mUsedRect.height, constraintRect->y, 1731 constraintRect->YMost(), result.mAnchorRect.y, 1732 result.mAnchorRect.YMost(), margin.top, margin.bottom, vFlip, 1733 endAligned, &result.mVFlip); 1734 } 1735 1736 #ifdef DEBUG 1737 NS_ASSERTION(constraintRect->Contains(result.mUsedRect), 1738 "Popup is offscreen"); 1739 if (!constraintRect->Contains(result.mUsedRect)) { 1740 NS_WARNING(nsPrintfCString("Popup is offscreen (%s vs. %s)", 1741 ToString(constraintRect).c_str(), 1742 ToString(result.mUsedRect).c_str()) 1743 .get()); 1744 } 1745 #endif 1746 } 1747 } 1748 // snap the popup's position in screen coordinates to device pixels, see 1749 // bug 622507, bug 961431 1750 result.mUsedRect.x = pc->RoundAppUnitsToNearestDevPixels(result.mUsedRect.x); 1751 result.mUsedRect.y = pc->RoundAppUnitsToNearestDevPixels(result.mUsedRect.y); 1752 1753 // determine the x and y position of the view by subtracting the desired 1754 // screen position from the screen position of the root frame. 1755 result.mViewPoint = result.mUsedRect.TopLeft() - rootScreenRect.TopLeft(); 1756 return result; 1757 } 1758 1759 void nsMenuPopupFrame::SetPopupPosition(bool aIsMove) { 1760 if (aIsMove && (mPrefSize.width == -1 || mPrefSize.height == -1)) { 1761 return; 1762 } 1763 1764 auto rects = GetRects(mPrefSize); 1765 if (rects.mUsedRect.Size() != mRect.Size()) { 1766 MOZ_ASSERT(!HasAnyStateBits(NS_FRAME_IN_REFLOW)); 1767 // We need to resize on top of moving, trigger an actual reflow. 1768 PresShell()->FrameNeedsReflow(this, IntrinsicDirty::FrameAndAncestors, 1769 NS_FRAME_IS_DIRTY); 1770 return; 1771 } 1772 PerformMove(rects); 1773 } 1774 1775 void nsMenuPopupFrame::PerformMove(const Rects& aRects) { 1776 auto* ps = PresShell(); 1777 1778 // Now that we've positioned the view, sync up the frame's origin. 1779 const nsPoint oldPos = mRect.TopLeft(); 1780 const nsPoint newPos = 1781 aRects.mViewPoint - GetParent()->GetOffsetTo(ps->GetRootFrame()); 1782 nsBlockFrame::SetPosition(newPos); 1783 if (oldPos != newPos) { 1784 SchedulePendingWidgetMoveResize(); 1785 } 1786 1787 // If the popup is in the positioned state or if it is shown and the position 1788 // or size changed, dispatch a popuppositioned event if the popup wants it. 1789 if (mPopupState == ePopupPositioning || 1790 (mPopupState == ePopupShown && 1791 !aRects.mUsedRect.IsEqualEdges(mUsedScreenRect)) || 1792 (mPopupState == ePopupShown && 1793 aRects.mAlignmentOffset != mAlignmentOffset)) { 1794 mUsedScreenRect = aRects.mUsedRect; 1795 if (!HasAnyStateBits(NS_FRAME_FIRST_REFLOW) && !mPendingPositionedEvent) { 1796 mPendingPositionedEvent = 1797 nsXULPopupPositionedEvent::DispatchIfNeeded(mContent->AsElement()); 1798 } 1799 } 1800 1801 if (!mPositionedByMoveToRect) { 1802 mUntransformedAnchorRect = aRects.mUntransformedAnchorRect; 1803 } 1804 1805 mAlignmentOffset = aRects.mAlignmentOffset; 1806 mHFlip = aRects.mHFlip; 1807 mVFlip = aRects.mVFlip; 1808 mConstrainedByLayout = aRects.mConstrainedByLayout; 1809 1810 // If this is a noautohide popup, set the screen coordinates of the popup. 1811 // This way, the popup stays at the location where it was opened even when the 1812 // window is moved. Popups at the parent level follow the parent window as it 1813 // is moved and remained anchored, so we want to maintain the anchoring 1814 // instead. 1815 // 1816 // FIXME: This suffers from issues like bug 1823552, where constraints imposed 1817 // by the anchor are lost, but this is super-old behavior. 1818 const bool fixPositionToPoint = 1819 IsNoAutoHide() && (GetPopupLevel() != PopupLevel::Parent || 1820 mAnchorType == MenuPopupAnchorType::Rect); 1821 if (fixPositionToPoint) { 1822 // Account for the margin that will end up being added to the screen 1823 // coordinate the next time SetPopupPosition is called. 1824 const auto& margin = GetMargin(); 1825 mAnchorType = MenuPopupAnchorType::Point; 1826 mScreenRect.x = aRects.mUsedRect.x - margin.left; 1827 mScreenRect.y = aRects.mUsedRect.y - margin.top; 1828 } 1829 1830 // For anchored popups that shouldn't follow the anchor, fix the original 1831 // anchor rect. 1832 if (IsAnchored() && !ShouldFollowAnchor() && !mUsedScreenRect.IsEmpty() && 1833 mAnchorType != MenuPopupAnchorType::Rect) { 1834 mAnchorType = MenuPopupAnchorType::Rect; 1835 mScreenRect = aRects.mUntransformedAnchorRect; 1836 } 1837 } 1838 1839 Maybe<nsRect> nsMenuPopupFrame::GetConstraintRect( 1840 const nsRect& aAnchorRect, const nsRect& aRootScreenRect, 1841 PopupLevel aPopupLevel) const { 1842 const nsPresContext* pc = PresContext(); 1843 const int32_t a2d = PresContext()->AppUnitsPerDevPixel(); 1844 Maybe<nsRect> result; 1845 1846 auto AddConstraint = [&result](const nsRect& aConstraint) { 1847 if (result) { 1848 *result = result->Intersect(aConstraint); 1849 } else { 1850 result.emplace(aConstraint); 1851 } 1852 }; 1853 1854 // Determine the available screen space. It will be reduced by the OS chrome 1855 // such as menubars. It addition, for content shells, it will be the area of 1856 // the content rather than the screen. 1857 if (IS_WAYLAND_DISPLAY()) { 1858 // In Wayland we can't use the screen rect, because we can't know the 1859 // absolute window position. MoveToRect usually deals with this, but we 1860 // can't use it unconditionally. Tooltips are presumed small enough, and 1861 // they can open basically at any time with any other open menu, so we 1862 // constrain them to the window area, see bug 1941237. 1863 // TODO(emilio, stransky): Do we want to constrain other popups to the 1864 // window area, if the window is big enough or maximized? 1865 if (mPopupType == PopupType::Tooltip) { 1866 AddConstraint(aRootScreenRect); 1867 } 1868 } else { 1869 const DesktopToLayoutDeviceScale scale = 1870 pc->DeviceContext()->GetDesktopToDeviceScale(); 1871 // For content shells, get the screen where the root frame is located. This 1872 // is because we need to constrain the content to this content area, so we 1873 // should use the same screen. Otherwise, use the screen where the anchor is 1874 // located. 1875 const nsRect& rect = mInContentShell ? aRootScreenRect : aAnchorRect; 1876 auto desktopRect = DesktopIntRect::RoundOut( 1877 LayoutDeviceRect::FromAppUnits(rect, a2d) / scale); 1878 desktopRect.width = std::max(1, desktopRect.width); 1879 desktopRect.height = std::max(1, desktopRect.height); 1880 1881 RefPtr<nsIScreen> screen = 1882 widget::ScreenManager::GetSingleton().ScreenForRect(desktopRect); 1883 MOZ_ASSERT(screen, "We always fall back to the primary screen"); 1884 // Non-top-level popups (which will always be panels) should never overlap 1885 // the OS bar. 1886 const bool canOverlapOSBar = 1887 aPopupLevel == PopupLevel::Top && 1888 LookAndFeel::GetInt(LookAndFeel::IntID::MenusCanOverlapOSBar) && 1889 !mInContentShell; 1890 // Get the total screen area if the popup is allowed to overlap it. 1891 const auto screenRect = 1892 canOverlapOSBar ? screen->GetRect() : screen->GetAvailRect(); 1893 AddConstraint(LayoutDeviceRect::ToAppUnits(screenRect, a2d)); 1894 } 1895 1896 if (mInContentShell) { 1897 // For content shells, clip to the client area rather than the screen area 1898 AddConstraint(aRootScreenRect); 1899 } else if (!mOverrideConstraintRect.IsEmpty()) { 1900 AddConstraint(mOverrideConstraintRect); 1901 // This is currently only used for <select> elements where we want to 1902 // constrain vertically to the screen but not horizontally, so do the 1903 // intersection and then reset the horizontal values. 1904 // 1905 // FIXME(emilio): This doesn't make any sense to me... 1906 result->x = mOverrideConstraintRect.x; 1907 result->width = mOverrideConstraintRect.width; 1908 } 1909 1910 // Expand the allowable screen rect by the input margin (which can't be 1911 // interacted with). 1912 if (result) { 1913 const nscoord inputMargin = 1914 StyleUIReset()->mMozWindowInputRegionMargin.ToAppUnits(); 1915 result->Inflate(inputMargin); 1916 } 1917 return result; 1918 } 1919 1920 ConsumeOutsideClicksResult nsMenuPopupFrame::ConsumeOutsideClicks() { 1921 if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, 1922 nsGkAtoms::consumeoutsideclicks, 1923 nsGkAtoms::_true, eCaseMatters)) { 1924 return ConsumeOutsideClicks_True; 1925 } 1926 if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, 1927 nsGkAtoms::consumeoutsideclicks, 1928 nsGkAtoms::_false, eCaseMatters)) { 1929 return ConsumeOutsideClicks_ParentOnly; 1930 } 1931 if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, 1932 nsGkAtoms::consumeoutsideclicks, 1933 nsGkAtoms::never, eCaseMatters)) { 1934 return ConsumeOutsideClicks_Never; 1935 } 1936 1937 nsCOMPtr<nsIContent> parentContent = mContent->GetParent(); 1938 if (parentContent) { 1939 dom::NodeInfo* ni = parentContent->NodeInfo(); 1940 if (ni->Equals(nsGkAtoms::menulist, kNameSpaceID_XUL)) { 1941 return ConsumeOutsideClicks_True; // Consume outside clicks for combo 1942 // boxes on all platforms 1943 } 1944 #if defined(XP_WIN) 1945 // Don't consume outside clicks for menus in Windows 1946 if (ni->Equals(nsGkAtoms::menu, kNameSpaceID_XUL) || 1947 ni->Equals(nsGkAtoms::popupset, kNameSpaceID_XUL) || 1948 ((ni->Equals(nsGkAtoms::button, kNameSpaceID_XUL) || 1949 ni->Equals(nsGkAtoms::toolbarbutton, kNameSpaceID_XUL)) && 1950 parentContent->AsElement()->AttrValueIs( 1951 kNameSpaceID_None, nsGkAtoms::type, nsGkAtoms::menu, 1952 eCaseMatters))) { 1953 return ConsumeOutsideClicks_Never; 1954 } 1955 #endif 1956 } 1957 1958 return ConsumeOutsideClicks_True; 1959 } 1960 1961 static ScrollContainerFrame* DoGetScrollContainerFrame(const nsIFrame* aFrame) { 1962 if (const ScrollContainerFrame* sf = do_QueryFrame(aFrame)) { 1963 return const_cast<ScrollContainerFrame*>(sf); 1964 } 1965 for (nsIFrame* childFrame : aFrame->PrincipalChildList()) { 1966 if (auto* sf = DoGetScrollContainerFrame(childFrame)) { 1967 return sf; 1968 } 1969 } 1970 return nullptr; 1971 } 1972 1973 // XXXroc this is megalame. Fossicking around for a frame of the right 1974 // type is a recipe for disaster in the long term. 1975 ScrollContainerFrame* nsMenuPopupFrame::GetScrollContainerFrame() const { 1976 return DoGetScrollContainerFrame(this); 1977 } 1978 1979 void nsMenuPopupFrame::ChangeByPage(bool aIsUp) { 1980 // Only scroll by page within menulists. 1981 if (!IsMenuList()) { 1982 return; 1983 } 1984 1985 ScrollContainerFrame* scrollContainerFrame = GetScrollContainerFrame(); 1986 1987 RefPtr popup = &PopupElement(); 1988 XULButtonElement* currentMenu = popup->GetActiveMenuChild(); 1989 XULButtonElement* newMenu = nullptr; 1990 if (!currentMenu) { 1991 // If there is no current menu item, get the first item. When moving up, 1992 // just use this as the newMenu and leave currentMenu null so that no check 1993 // for a later element is performed. When moving down, set currentMenu so 1994 // that we look for one page down from the first item. 1995 newMenu = popup->GetFirstMenuItem(); 1996 if (!aIsUp) { 1997 currentMenu = newMenu; 1998 } 1999 } 2000 2001 if (currentMenu && currentMenu->GetPrimaryFrame()) { 2002 const nscoord scrollHeight = 2003 scrollContainerFrame ? scrollContainerFrame->GetScrollPortRect().height 2004 : mRect.height; 2005 const nsRect currentRect = currentMenu->GetPrimaryFrame()->GetRect(); 2006 const XULButtonElement* startMenu = currentMenu; 2007 2008 // Get the position of the current item and add or subtract one popup's 2009 // height to or from it. 2010 const nscoord targetPos = aIsUp ? currentRect.YMost() - scrollHeight 2011 : currentRect.y + scrollHeight; 2012 // Look for the next child which is just past the target position. This 2013 // child will need to be selected. 2014 for (; currentMenu; 2015 currentMenu = aIsUp ? popup->GetPrevMenuItemFrom(*currentMenu) 2016 : popup->GetNextMenuItemFrom(*currentMenu)) { 2017 if (!currentMenu->GetPrimaryFrame()) { 2018 continue; 2019 } 2020 const nsRect curRect = currentMenu->GetPrimaryFrame()->GetRect(); 2021 const nscoord curPos = aIsUp ? curRect.y : curRect.YMost(); 2022 // If the right position was found, break out. Otherwise, look for another 2023 // item. 2024 if (aIsUp ? (curPos < targetPos) : (curPos > targetPos)) { 2025 if (!newMenu || newMenu == startMenu) { 2026 newMenu = currentMenu; 2027 } 2028 break; 2029 } 2030 2031 // Assign this item to newMenu. This item will be selected in case we 2032 // don't find any more. 2033 newMenu = currentMenu; 2034 } 2035 } 2036 2037 // Select the new menuitem. 2038 if (RefPtr newMenuRef = newMenu) { 2039 popup->SetActiveMenuChild(newMenuRef); 2040 } 2041 } 2042 2043 dom::XULPopupElement& nsMenuPopupFrame::PopupElement() const { 2044 auto* popup = dom::XULPopupElement::FromNode(GetContent()); 2045 MOZ_DIAGNOSTIC_ASSERT(popup); 2046 return *popup; 2047 } 2048 2049 XULButtonElement* nsMenuPopupFrame::GetCurrentMenuItem() const { 2050 return PopupElement().GetActiveMenuChild(); 2051 } 2052 2053 nsIFrame* nsMenuPopupFrame::GetCurrentMenuItemFrame() const { 2054 auto* child = GetCurrentMenuItem(); 2055 return child ? child->GetPrimaryFrame() : nullptr; 2056 } 2057 2058 void nsMenuPopupFrame::HandleEnterKeyPress(WidgetEvent& aEvent) { 2059 mIncrementalString.Truncate(); 2060 RefPtr popup = &PopupElement(); 2061 popup->HandleEnterKeyPress(aEvent); 2062 } 2063 2064 XULButtonElement* nsMenuPopupFrame::FindMenuWithShortcut( 2065 mozilla::dom::KeyboardEvent& aKeyEvent, bool& aDoAction) { 2066 uint32_t charCode = aKeyEvent.CharCode(); 2067 uint32_t keyCode = aKeyEvent.KeyCode(); 2068 2069 aDoAction = false; 2070 2071 // Enumerate over our list of frames. 2072 const bool isMenu = !IsMenuList(); 2073 TimeStamp keyTime = aKeyEvent.WidgetEventPtr()->mTimeStamp; 2074 if (charCode == 0) { 2075 if (keyCode == dom::KeyboardEvent_Binding::DOM_VK_BACK_SPACE) { 2076 if (!isMenu && !mIncrementalString.IsEmpty()) { 2077 mIncrementalString.SetLength(mIncrementalString.Length() - 1); 2078 return nullptr; 2079 } 2080 #ifdef XP_WIN 2081 if (nsCOMPtr<nsISound> sound = do_GetService("@mozilla.org/sound;1")) { 2082 sound->Beep(); 2083 } 2084 #endif // #ifdef XP_WIN 2085 } 2086 return nullptr; 2087 } 2088 char16_t uniChar = ToLowerCase(static_cast<char16_t>(charCode)); 2089 if (isMenu) { 2090 // Menu supports only first-letter navigation 2091 mIncrementalString = uniChar; 2092 } else if (IsWithinIncrementalTime(keyTime)) { 2093 mIncrementalString.Append(uniChar); 2094 } else { 2095 // Interval too long, treat as new typing 2096 mIncrementalString = uniChar; 2097 } 2098 2099 // See bug 188199 & 192346, if all letters in incremental string are same, 2100 // just try to match the first one 2101 nsAutoString incrementalString(mIncrementalString); 2102 uint32_t charIndex = 1, stringLength = incrementalString.Length(); 2103 while (charIndex < stringLength && 2104 incrementalString[charIndex] == incrementalString[charIndex - 1]) { 2105 charIndex++; 2106 } 2107 if (charIndex == stringLength) { 2108 incrementalString.Truncate(1); 2109 stringLength = 1; 2110 } 2111 2112 sLastKeyTime = keyTime; 2113 2114 auto* item = 2115 PopupElement().FindMenuWithShortcut(incrementalString, aDoAction); 2116 if (item) { 2117 return item; 2118 } 2119 2120 // If we don't match anything, rollback the last typing 2121 mIncrementalString.SetLength(mIncrementalString.Length() - 1); 2122 2123 // didn't find a matching menu item 2124 #ifdef XP_WIN 2125 // behavior on Windows - this item is in a menu popup off of the 2126 // menu bar, so beep and do nothing else 2127 if (isMenu) { 2128 if (nsCOMPtr<nsISound> sound = do_GetService("@mozilla.org/sound;1")) { 2129 sound->Beep(); 2130 } 2131 } 2132 #endif // #ifdef XP_WIN 2133 2134 return nullptr; 2135 } 2136 2137 nsIWidget* nsMenuPopupFrame::GetWidget() const { return mWidget.get(); } 2138 2139 // helpers ///////////////////////////////////////////////////////////// 2140 2141 nsresult nsMenuPopupFrame::AttributeChanged(int32_t aNameSpaceID, 2142 nsAtom* aAttribute, 2143 AttrModType aModType) 2144 2145 { 2146 nsresult rv = 2147 nsBlockFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType); 2148 2149 if (aAttribute == nsGkAtoms::left || aAttribute == nsGkAtoms::top) { 2150 MoveToAttributePosition(); 2151 } 2152 2153 if (aAttribute == nsGkAtoms::remote && GetWidget()) { 2154 // When the remote attribute changes, we need to create a new widget to 2155 // ensure that it has the correct compositor and transparency settings to 2156 // match the new value. Do that only if we already have a widget. 2157 // TODO(emilio): We should consider doing it only when we get re-shown or 2158 // so. 2159 PrepareWidget(true); 2160 } 2161 2162 if (aAttribute == nsGkAtoms::followanchor) { 2163 if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) { 2164 pm->UpdateFollowAnchor(this); 2165 } 2166 } 2167 2168 if (aAttribute == nsGkAtoms::label) { 2169 // set the label for the titlebar 2170 if (nsIWidget* widget = GetWidget()) { 2171 nsAutoString title; 2172 mContent->AsElement()->GetAttr(nsGkAtoms::label, title); 2173 if (!title.IsEmpty()) { 2174 widget->SetTitle(title); 2175 } 2176 } 2177 } else if (aAttribute == nsGkAtoms::ignorekeys) { 2178 nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); 2179 if (pm) { 2180 nsAutoString ignorekeys; 2181 mContent->AsElement()->GetAttr(nsGkAtoms::ignorekeys, ignorekeys); 2182 pm->UpdateIgnoreKeys(ignorekeys.EqualsLiteral("true")); 2183 } 2184 } 2185 2186 return rv; 2187 } 2188 2189 void nsMenuPopupFrame::MoveToAttributePosition() { 2190 // Move the widget around when the user sets the |left| and |top| attributes. 2191 // Note that this is not the best way to move the widget, as it results in 2192 // lots of FE notifications and is likely to be slow as molasses. Use |moveTo| 2193 // on the element if possible. 2194 nsAutoString left, top; 2195 mContent->AsElement()->GetAttr(nsGkAtoms::left, left); 2196 mContent->AsElement()->GetAttr(nsGkAtoms::top, top); 2197 nsresult err1, err2; 2198 const CSSIntPoint pos(left.ToInteger(&err1), top.ToInteger(&err2)); 2199 if (NS_SUCCEEDED(err1) && NS_SUCCEEDED(err2)) { 2200 MoveTo(pos, false); 2201 } 2202 2203 PresShell()->FrameNeedsReflow( 2204 this, IntrinsicDirty::FrameAncestorsAndDescendants, NS_FRAME_IS_DIRTY); 2205 } 2206 2207 void nsMenuPopupFrame::Destroy(DestroyContext& aContext) { 2208 // XXX: Currently we don't fire popuphidden for these popups, that seems wrong 2209 // but alas, also pre-existing. 2210 HidePopup(/* aDeselectMenu = */ false, ePopupClosed, 2211 /* aFromFrameDestruction = */ true); 2212 if (mExpirationState.IsTracked()) { 2213 PopupExpirationTracker::Get()->RemoveObject(this); 2214 } 2215 2216 if (RefPtr<nsXULPopupManager> pm = nsXULPopupManager::GetInstance()) { 2217 pm->PopupDestroyed(this); 2218 } 2219 2220 DestroyWidget(); 2221 nsBlockFrame::Destroy(aContext); 2222 } 2223 2224 nsMargin nsMenuPopupFrame::GetMargin() const { 2225 nsMargin margin; 2226 StyleMargin()->GetMargin(margin); 2227 if (mIsTopLevelContextMenu) { 2228 const CSSIntPoint offset( 2229 LookAndFeel::GetInt(LookAndFeel::IntID::ContextMenuOffsetHorizontal), 2230 LookAndFeel::GetInt(LookAndFeel::IntID::ContextMenuOffsetVertical)); 2231 auto auOffset = CSSIntPoint::ToAppUnits(offset); 2232 margin.top += auOffset.y; 2233 margin.bottom += auOffset.y; 2234 margin.left += auOffset.x; 2235 margin.right += auOffset.x; 2236 } 2237 if (mPopupType == PopupType::Tooltip && !IsAnchored()) { 2238 const auto auOffset = 2239 CSSPixel::ToAppUnits(LookAndFeel::TooltipOffsetVertical()); 2240 margin.top += auOffset; 2241 margin.bottom += auOffset; 2242 } 2243 // TODO(emilio): We should consider make these properly mirrored (that is, 2244 // changing -= to += here, and removing the rtl special case), but some tests 2245 // rely on the old behavior of the anchor moving physically regardless of 2246 // alignment... 2247 margin.top += mExtraMargin.y; 2248 margin.bottom -= mExtraMargin.y; 2249 if (IsDirectionRTL()) { 2250 margin.left -= mExtraMargin.x; 2251 margin.right += mExtraMargin.x; 2252 } else { 2253 margin.left += mExtraMargin.x; 2254 margin.right -= mExtraMargin.x; 2255 } 2256 return margin; 2257 } 2258 2259 void nsMenuPopupFrame::DestroyWidgetIfNeeded() { 2260 if (IsVisibleOrShowing()) { 2261 MOZ_ASSERT_UNREACHABLE("Shouldn't be tracked while visible"); 2262 return; 2263 } 2264 DestroyWidget(); 2265 } 2266 2267 void nsMenuPopupFrame::MoveTo(const CSSPoint& aPos, bool aUpdateAttrs, 2268 bool aByMoveToRect) { 2269 nsPoint appUnitsPos = CSSPixel::ToAppUnits(aPos); 2270 2271 const bool rtl = IsDirectionRTL(); 2272 2273 // reposition the popup at the specified coordinates. Don't clear the anchor 2274 // and position, because the popup can be reset to its anchor position by 2275 // using (-1, -1) as coordinates. 2276 // 2277 // Subtract off the margin as it will be added to the position when 2278 // SetPopupPosition is called. 2279 { 2280 nsMargin margin = GetMargin(); 2281 if (rtl && mIsContextMenu) { 2282 appUnitsPos.x += margin.right + mRect.Width(); 2283 } else { 2284 appUnitsPos.x -= margin.left; 2285 } 2286 appUnitsPos.y -= margin.top; 2287 } 2288 2289 if (mScreenRect.TopLeft() == appUnitsPos) { 2290 return; 2291 } 2292 2293 mPositionedByMoveToRect = aByMoveToRect; 2294 mScreenRect.MoveTo(appUnitsPos); 2295 if (mAnchorType == MenuPopupAnchorType::Rect) { 2296 // This ensures that the anchor width is still honored, to prevent it from 2297 // changing spuriously. 2298 mScreenRect.height = 0; 2299 // But we still need to make sure that our top left position ends up in 2300 // appUnitsPos. 2301 mPopupAlignment = rtl ? POPUPALIGNMENT_TOPRIGHT : POPUPALIGNMENT_TOPLEFT; 2302 mPopupAnchor = rtl ? POPUPALIGNMENT_BOTTOMRIGHT : POPUPALIGNMENT_BOTTOMLEFT; 2303 } else { 2304 mAnchorType = MenuPopupAnchorType::Point; 2305 } 2306 2307 SetPopupPosition(true); 2308 2309 RefPtr<Element> popup = mContent->AsElement(); 2310 if (aUpdateAttrs && 2311 (popup->HasAttr(nsGkAtoms::left) || popup->HasAttr(nsGkAtoms::top))) { 2312 nsAutoString left, top; 2313 left.AppendInt(RoundedToInt(aPos).x); 2314 top.AppendInt(RoundedToInt(aPos).y); 2315 popup->SetAttr(kNameSpaceID_None, nsGkAtoms::left, left, false); 2316 popup->SetAttr(kNameSpaceID_None, nsGkAtoms::top, top, false); 2317 } 2318 } 2319 2320 void nsMenuPopupFrame::MoveToAnchor(nsIContent* aAnchorContent, 2321 const nsAString& aPosition, int32_t aXPos, 2322 int32_t aYPos, bool aAttributesOverride) { 2323 NS_ASSERTION(IsVisibleOrShowing(), 2324 "popup must be visible or showing to move it"); 2325 2326 nsPopupState oldstate = mPopupState; 2327 InitializePopup(aAnchorContent, mTriggerContent, aPosition, aXPos, aYPos, 2328 MenuPopupAnchorType::Node, aAttributesOverride); 2329 // InitializePopup changed the state so reset it. 2330 mPopupState = oldstate; 2331 2332 // Pass false here so that flipping and adjusting to fit on the screen happen. 2333 SetPopupPosition(false); 2334 } 2335 2336 int8_t nsMenuPopupFrame::GetAlignmentPosition() const { 2337 // The code below handles most cases of alignment, anchor and position values. 2338 // Those that are not handled just return POPUPPOSITION_UNKNOWN. 2339 2340 if (mPosition == POPUPPOSITION_OVERLAP || 2341 mPosition == POPUPPOSITION_AFTERPOINTER || 2342 mPosition == POPUPPOSITION_SELECTION) { 2343 return mPosition; 2344 } 2345 2346 int8_t position = mPosition; 2347 2348 if (position == POPUPPOSITION_UNKNOWN) { 2349 switch (mPopupAnchor) { 2350 case POPUPALIGNMENT_BOTTOMRIGHT: 2351 case POPUPALIGNMENT_BOTTOMLEFT: 2352 case POPUPALIGNMENT_BOTTOMCENTER: 2353 position = mPopupAlignment == POPUPALIGNMENT_TOPRIGHT 2354 ? POPUPPOSITION_AFTEREND 2355 : POPUPPOSITION_AFTERSTART; 2356 break; 2357 case POPUPALIGNMENT_TOPRIGHT: 2358 case POPUPALIGNMENT_TOPLEFT: 2359 case POPUPALIGNMENT_TOPCENTER: 2360 position = mPopupAlignment == POPUPALIGNMENT_BOTTOMRIGHT 2361 ? POPUPPOSITION_BEFOREEND 2362 : POPUPPOSITION_BEFORESTART; 2363 break; 2364 case POPUPALIGNMENT_LEFTCENTER: 2365 position = mPopupAlignment == POPUPALIGNMENT_BOTTOMRIGHT 2366 ? POPUPPOSITION_STARTAFTER 2367 : POPUPPOSITION_STARTBEFORE; 2368 break; 2369 case POPUPALIGNMENT_RIGHTCENTER: 2370 position = mPopupAlignment == POPUPALIGNMENT_BOTTOMLEFT 2371 ? POPUPPOSITION_ENDAFTER 2372 : POPUPPOSITION_ENDBEFORE; 2373 break; 2374 default: 2375 break; 2376 } 2377 } 2378 2379 if (mHFlip) { 2380 position = POPUPPOSITION_HFLIP(position); 2381 } 2382 2383 if (mVFlip) { 2384 position = POPUPPOSITION_VFLIP(position); 2385 } 2386 2387 return position; 2388 } 2389 2390 bool nsMenuPopupFrame::ShouldFollowAnchor() const { 2391 if (mAnchorType != MenuPopupAnchorType::Node || !mAnchorContent) { 2392 return false; 2393 } 2394 2395 // Follow anchor mode is used when followanchor="true" is set or for arrow 2396 // panels. 2397 if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, 2398 nsGkAtoms::followanchor, 2399 nsGkAtoms::_true, eCaseMatters)) { 2400 return true; 2401 } 2402 2403 if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, 2404 nsGkAtoms::followanchor, 2405 nsGkAtoms::_false, eCaseMatters)) { 2406 return false; 2407 } 2408 2409 return mPopupType == PopupType::Panel && 2410 mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, 2411 nsGkAtoms::arrow, eCaseMatters); 2412 } 2413 2414 bool nsMenuPopupFrame::ShouldFollowAnchor(nsRect& aRect) { 2415 if (!ShouldFollowAnchor()) { 2416 return false; 2417 } 2418 2419 if (nsIFrame* anchorFrame = GetAnchorFrame()) { 2420 if (nsPresContext* rootPresContext = PresContext()->GetRootPresContext()) { 2421 aRect = ComputeAnchorRect(rootPresContext, anchorFrame); 2422 } 2423 } 2424 2425 return true; 2426 } 2427 2428 bool nsMenuPopupFrame::IsDirectionRTL() const { 2429 const nsIFrame* anchor = GetAnchorFrame(); 2430 const nsIFrame* f = anchor ? anchor : this; 2431 return f->StyleVisibility()->mDirection == StyleDirection::Rtl; 2432 } 2433 2434 nsIFrame* nsMenuPopupFrame::GetAnchorFrame() const { 2435 nsIContent* anchor = mAnchorContent; 2436 if (!anchor) { 2437 return nullptr; 2438 } 2439 return MaybeDelegatedAnchorFrame(anchor->GetPrimaryFrame()); 2440 } 2441 2442 void nsMenuPopupFrame::CheckForAnchorChange(nsRect& aRect) { 2443 // Don't update if the popup isn't visible or we shouldn't be following the 2444 // anchor. 2445 if (!IsVisible() || !ShouldFollowAnchor()) { 2446 return; 2447 } 2448 2449 bool shouldHide = false; 2450 2451 nsPresContext* rootPresContext = PresContext()->GetRootPresContext(); 2452 2453 // If the frame for the anchor has gone away, hide the popup. 2454 nsIFrame* anchor = GetAnchorFrame(); 2455 if (!anchor || !rootPresContext) { 2456 shouldHide = true; 2457 } else if (!anchor->IsVisibleConsideringAncestors( 2458 VISIBILITY_CROSS_CHROME_CONTENT_BOUNDARY)) { 2459 // If the anchor is now inside something that is invisible, hide the popup. 2460 shouldHide = true; 2461 } else { 2462 // If the anchor is now inside a hidden parent popup, hide the popup. 2463 nsIFrame* frame = anchor; 2464 while (frame) { 2465 nsMenuPopupFrame* popup = do_QueryFrame(frame); 2466 if (popup && popup->PopupState() != ePopupShown) { 2467 shouldHide = true; 2468 break; 2469 } 2470 2471 frame = frame->GetParent(); 2472 } 2473 } 2474 2475 if (shouldHide) { 2476 if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) { 2477 // As the caller will be iterating over the open popups, hide 2478 // asyncronously. 2479 pm->HidePopup(mContent->AsElement(), 2480 {HidePopupOption::DeselectMenu, HidePopupOption::Async}); 2481 } 2482 2483 return; 2484 } 2485 2486 nsRect anchorRect = ComputeAnchorRect(rootPresContext, anchor); 2487 2488 // If the rectangles are different, move the popup. 2489 if (!anchorRect.IsEqualEdges(aRect)) { 2490 aRect = anchorRect; 2491 SetPopupPosition(true); 2492 } 2493 } 2494 2495 void nsMenuPopupFrame::WindowMoved(nsIWidget* aWidget, 2496 const LayoutDeviceIntPoint& aPoint, 2497 ByMoveToRect aByMoveToRect) { 2498 MOZ_ASSERT(aWidget == mWidget); 2499 2500 if (!IsVisibleOrShowing()) { 2501 return; 2502 } 2503 2504 // Don't do anything if the popup is already at the specified location. This 2505 // prevents recursive calls when a popup is positioned. 2506 LayoutDeviceIntRect curDevBounds = CalcWidgetBounds(); 2507 if (curDevBounds.TopLeft() == aPoint) { 2508 return; 2509 } 2510 2511 // Update the popup's position using SetPopupPosition if the popup is 2512 // anchored and at the parent level as these maintain their position 2513 // relative to the parent window (except if positioned by move to rect, in 2514 // which case we better make sure that layout matches that). Otherwise, just 2515 // update the popup to the specified screen coordinates. 2516 if (IsAnchored() && GetPopupLevel() == widget::PopupLevel::Parent && 2517 aByMoveToRect == ByMoveToRect::No) { 2518 SetPopupPosition(true); 2519 } else { 2520 CSSPoint cssPos = aPoint / PresContext()->CSSToDevPixelScale(); 2521 MoveTo(cssPos, false, aByMoveToRect == ByMoveToRect::Yes); 2522 } 2523 } 2524 2525 void nsMenuPopupFrame::WindowResized(nsIWidget* aWidget, 2526 const LayoutDeviceIntSize& aSize) { 2527 MOZ_ASSERT(aWidget == mWidget); 2528 if (!IsVisibleOrShowing()) { 2529 return; 2530 } 2531 2532 const LayoutDeviceIntRect curDevBounds = CalcWidgetBounds(); 2533 // If the size is what we think it is, we have nothing to do. 2534 if (curDevBounds.Size() == aSize) { 2535 return; 2536 } 2537 2538 RefPtr<Element> popup = &PopupElement(); 2539 2540 // Only set the width and height if the popup already has these attributes. 2541 if (!popup->HasAttr(nsGkAtoms::width) || !popup->HasAttr(nsGkAtoms::height)) { 2542 return; 2543 } 2544 2545 // The size is different. Convert the actual size to css pixels and store it 2546 // as 'width' and 'height' attributes on the popup. 2547 nsPresContext* presContext = PresContext(); 2548 2549 CSSIntSize newCSS(presContext->DevPixelsToIntCSSPixels(aSize.width), 2550 presContext->DevPixelsToIntCSSPixels(aSize.height)); 2551 2552 nsAutoString width, height; 2553 width.AppendInt(newCSS.width); 2554 height.AppendInt(newCSS.height); 2555 popup->SetAttr(kNameSpaceID_None, nsGkAtoms::width, width, true); 2556 popup->SetAttr(kNameSpaceID_None, nsGkAtoms::height, height, true); 2557 } 2558 2559 bool nsMenuPopupFrame::RequestWindowClose(nsIWidget* aWidget) { 2560 MOZ_ASSERT(aWidget == mWidget); 2561 if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) { 2562 pm->HidePopup(&PopupElement(), {HidePopupOption::DeselectMenu}); 2563 return true; 2564 } 2565 return false; 2566 } 2567 2568 nsEventStatus nsMenuPopupFrame::HandleEvent(mozilla::WidgetGUIEvent* aEvent) { 2569 MOZ_ASSERT(aEvent->mWidget); 2570 MOZ_ASSERT(aEvent->mWidget == mWidget); 2571 nsEventStatus status = nsEventStatus_eIgnore; 2572 RefPtr ps = PresShell(); 2573 ps->HandleEvent(this, aEvent, false, &status); 2574 return status; 2575 } 2576 2577 void nsMenuPopupFrame::PaintWindow(nsIWidget* aWidget) { 2578 MOZ_ASSERT(aWidget == mWidget); 2579 nsAutoScriptBlocker scriptBlocker; 2580 RefPtr ps = PresShell(); 2581 RefPtr<WindowRenderer> renderer = aWidget->GetWindowRenderer(); 2582 if (!renderer->NeedsWidgetInvalidation()) { 2583 renderer->FlushRendering(wr::RenderReasons::WIDGET); 2584 } else { 2585 ps->SyncPaintFallback(this, renderer); 2586 } 2587 } 2588 2589 void nsMenuPopupFrame::DidCompositeWindow( 2590 mozilla::layers::TransactionId aTransactionId, 2591 const TimeStamp& aCompositeStart, const TimeStamp& aCompositeEnd) { 2592 RefPtr rootPc = PresContext()->GetRootPresContext(); 2593 if (!rootPc) { 2594 return; 2595 } 2596 nsAutoScriptBlocker scriptBlocker; 2597 rootPc->NotifyDidPaintForSubtree(aTransactionId, aCompositeEnd); 2598 }