Bug 1793629 - Implement attention indicator for the unified extensions button, r...
[gecko.git] / layout / xul / nsMenuPopupFrame.cpp
blobeb8acc5f3949c07ffd319a55b18e16074aa0510f
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "nsMenuPopupFrame.h"
8 #include "nsGkAtoms.h"
9 #include "nsIContent.h"
10 #include "nsAtom.h"
11 #include "nsPresContext.h"
12 #include "mozilla/ComputedStyle.h"
13 #include "nsCSSRendering.h"
14 #include "nsNameSpaceManager.h"
15 #include "nsViewManager.h"
16 #include "nsWidgetsCID.h"
17 #include "nsMenuFrame.h"
18 #include "nsMenuBarFrame.h"
19 #include "nsPopupSetFrame.h"
20 #include "nsPIDOMWindow.h"
21 #include "nsFrameManager.h"
22 #include "mozilla/dom/Document.h"
23 #include "nsRect.h"
24 #include "nsBoxLayoutState.h"
25 #include "nsIScrollableFrame.h"
26 #include "nsIPopupContainer.h"
27 #include "nsIDocShell.h"
28 #include "nsReadableUtils.h"
29 #include "nsUnicharUtils.h"
30 #include "nsLayoutUtils.h"
31 #include "nsContentUtils.h"
32 #include "nsCSSFrameConstructor.h"
33 #include "nsPIWindowRoot.h"
34 #include "nsIReflowCallback.h"
35 #include "nsIDocShellTreeOwner.h"
36 #include "nsIBaseWindow.h"
37 #include "nsISound.h"
38 #include "nsIScreenManager.h"
39 #include "nsServiceManagerUtils.h"
40 #include "nsStyleConsts.h"
41 #include "nsStyleStructInlines.h"
42 #include "nsTransitionManager.h"
43 #include "nsDisplayList.h"
44 #include "nsIDOMXULSelectCntrlEl.h"
45 #include "mozilla/AnimationUtils.h"
46 #include "mozilla/BasePrincipal.h"
47 #include "mozilla/EventDispatcher.h"
48 #include "mozilla/EventStateManager.h"
49 #include "mozilla/Preferences.h"
50 #include "mozilla/LookAndFeel.h"
51 #include "mozilla/MouseEvents.h"
52 #include "mozilla/PresShell.h"
53 #include "mozilla/Services.h"
54 #include "mozilla/StaticPrefs_xul.h"
55 #include "mozilla/dom/BrowserParent.h"
56 #include "mozilla/dom/Element.h"
57 #include "mozilla/dom/Event.h"
58 #include "mozilla/dom/KeyboardEvent.h"
59 #include "mozilla/dom/KeyboardEventBinding.h"
60 #include <algorithm>
62 #include "X11UndefineNone.h"
64 using namespace mozilla;
65 using mozilla::dom::Document;
66 using mozilla::dom::Element;
67 using mozilla::dom::Event;
68 using mozilla::dom::KeyboardEvent;
70 int8_t nsMenuPopupFrame::sDefaultLevelIsTop = -1;
72 DOMTimeStamp nsMenuPopupFrame::sLastKeyTime = 0;
74 #ifdef MOZ_WAYLAND
75 # include "mozilla/WidgetUtilsGtk.h"
76 # define IS_WAYLAND_DISPLAY() mozilla::widget::GdkIsWaylandDisplay()
77 extern mozilla::LazyLogModule gWidgetPopupLog;
78 # define LOG_WAYLAND(...) \
79 MOZ_LOG(gWidgetPopupLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
80 #else
81 # define IS_WAYLAND_DISPLAY() false
82 # define LOG_WAYLAND (...)
83 #endif
85 // NS_NewMenuPopupFrame
87 // Wrapper for creating a new menu popup container
89 nsIFrame* NS_NewMenuPopupFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
90 return new (aPresShell)
91 nsMenuPopupFrame(aStyle, aPresShell->GetPresContext());
94 NS_IMPL_FRAMEARENA_HELPERS(nsMenuPopupFrame)
96 NS_QUERYFRAME_HEAD(nsMenuPopupFrame)
97 NS_QUERYFRAME_ENTRY(nsMenuPopupFrame)
98 NS_QUERYFRAME_TAIL_INHERITING(nsBoxFrame)
101 // nsMenuPopupFrame ctor
103 nsMenuPopupFrame::nsMenuPopupFrame(ComputedStyle* aStyle,
104 nsPresContext* aPresContext)
105 : nsBoxFrame(aStyle, aPresContext, kClassID),
106 mCurrentMenu(nullptr),
107 mView(nullptr),
108 mPrefSize(-1, -1),
109 mXPos(0),
110 mYPos(0),
111 mAlignmentOffset(0),
112 mLastClientOffset(0, 0),
113 mPopupType(ePopupTypePanel),
114 mPopupState(ePopupClosed),
115 mPopupAlignment(POPUPALIGNMENT_NONE),
116 mPopupAnchor(POPUPALIGNMENT_NONE),
117 mPosition(POPUPPOSITION_UNKNOWN),
118 mFlip(FlipType_Default),
119 mIsOpenChanged(false),
120 mMenuCanOverlapOSBar(false),
121 mShouldAutoPosition(true),
122 mInContentShell(true),
123 mIsMenuLocked(false),
124 mIsOffset(false),
125 mHFlip(false),
126 mVFlip(false),
127 mPositionedOffset(0),
128 mAnchorType(MenuPopupAnchorType_Node) {
129 // the preference name is backwards here. True means that the 'top' level is
130 // the default, and false means that the 'parent' level is the default.
131 if (sDefaultLevelIsTop >= 0) return;
132 sDefaultLevelIsTop =
133 Preferences::GetBool("ui.panel.default_level_parent", false);
134 } // ctor
136 nsMenuPopupFrame::~nsMenuPopupFrame() = default;
138 static bool IsMouseTransparent(const ComputedStyle& aStyle) {
139 // If pointer-events: none; is set on the popup, then the widget should
140 // ignore mouse events, passing them through to the content behind.
141 return aStyle.PointerEvents() == StylePointerEvents::None;
144 static nsIWidget::InputRegion ComputeInputRegion(const ComputedStyle& aStyle,
145 const nsPresContext& aPc) {
146 return {IsMouseTransparent(aStyle),
147 (aStyle.StyleUIReset()->mMozWindowInputRegionMargin.ToCSSPixels() *
148 aPc.CSSToDevPixelScale())
149 .Truncated()};
152 bool nsMenuPopupFrame::ShouldCreateWidgetUpfront() const {
153 if (mPopupType != ePopupTypeMenu) {
154 // Any panel with a type attribute, such as the autocomplete popup, is
155 // always generated right away.
156 return mContent->AsElement()->HasAttr(nsGkAtoms::type);
158 // Generate the widget up-front if the following is true:
160 // - If the parent menu is a <menulist> unless its sizetopopup is set to
161 // "none".
162 // - For other elements, if the parent menu has a sizetopopup attribute.
164 // In these cases the size of the parent menu is dependent on the size of the
165 // popup, so the widget needs to exist in order to calculate this size.
166 nsIContent* parentContent = mContent->GetParent();
167 if (!parentContent) {
168 return true;
171 if (parentContent->IsXULElement(nsGkAtoms::menulist)) {
172 Element* parent = parentContent->AsElement();
173 nsAutoString sizedToPopup;
174 if (!parent->GetAttr(nsGkAtoms::sizetopopup, sizedToPopup)) {
175 // No prop set, generate child frames normally for the default value
176 // ("pref").
177 return true;
179 // Don't generate child frame only if the property is set to none.
180 return !sizedToPopup.EqualsLiteral("none");
183 return parentContent->IsElement() &&
184 parentContent->AsElement()->HasAttr(nsGkAtoms::sizetopopup);
187 void nsMenuPopupFrame::Init(nsIContent* aContent, nsContainerFrame* aParent,
188 nsIFrame* aPrevInFlow) {
189 nsBoxFrame::Init(aContent, aParent, aPrevInFlow);
191 // lookup if we're allowed to overlap the OS bar (menubar/taskbar) from the
192 // look&feel object
193 mMenuCanOverlapOSBar =
194 LookAndFeel::GetInt(LookAndFeel::IntID::MenusCanOverlapOSBar) != 0;
196 CreatePopupView();
198 // XXX Hack. The popup's view should float above all other views,
199 // so we use the nsView::SetFloating() to tell the view manager
200 // about that constraint.
201 nsView* ourView = GetView();
202 nsViewManager* viewManager = ourView->GetViewManager();
203 viewManager->SetViewFloating(ourView, true);
205 mPopupType = ePopupTypePanel;
206 if (aContent->IsAnyOfXULElements(nsGkAtoms::menupopup, nsGkAtoms::popup)) {
207 mPopupType = ePopupTypeMenu;
208 } else if (aContent->IsXULElement(nsGkAtoms::tooltip)) {
209 mPopupType = ePopupTypeTooltip;
212 if (PresContext()->IsChrome()) {
213 mInContentShell = false;
216 // Support incontentshell=false attribute to allow popups to be displayed
217 // outside of the content shell. Chrome only.
218 if (aContent->NodePrincipal()->IsSystemPrincipal()) {
219 if (aContent->AsElement()->AttrValueIs(kNameSpaceID_None,
220 nsGkAtoms::incontentshell,
221 nsGkAtoms::_true, eCaseMatters)) {
222 mInContentShell = true;
223 } else if (aContent->AsElement()->AttrValueIs(
224 kNameSpaceID_None, nsGkAtoms::incontentshell,
225 nsGkAtoms::_false, eCaseMatters)) {
226 mInContentShell = false;
230 // To improve performance, create the widget for the popup if needed. Popups
231 // such as menus will create their widgets later when the popup opens.
233 // FIXME(emilio): Doing this up-front for all menupopups causes a bunch of
234 // assertions, while it's supposed to be just an optimization.
235 if (!ourView->HasWidget() && ShouldCreateWidgetUpfront()) {
236 CreateWidgetForView(ourView);
239 if (aContent->NodeInfo()->Equals(nsGkAtoms::tooltip, kNameSpaceID_XUL) &&
240 aContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::_default,
241 nsGkAtoms::_true, eIgnoreCase)) {
242 nsIPopupContainer* popupContainer =
243 nsIPopupContainer::GetPopupContainer(PresShell());
244 if (popupContainer) {
245 popupContainer->SetDefaultTooltip(aContent->AsElement());
249 AddStateBits(NS_FRAME_IN_POPUP);
252 bool nsMenuPopupFrame::HasRemoteContent() const {
253 return (!mInContentShell && mPopupType == ePopupTypePanel &&
254 mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
255 nsGkAtoms::remote,
256 nsGkAtoms::_true, eIgnoreCase));
259 bool nsMenuPopupFrame::IsNoAutoHide() const {
260 // Panels with noautohide="true" don't hide when the mouse is clicked
261 // outside of them, or when another application is made active. Non-autohide
262 // panels cannot be used in content windows.
263 return (!mInContentShell && mPopupType == ePopupTypePanel &&
264 mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
265 nsGkAtoms::noautohide,
266 nsGkAtoms::_true, eIgnoreCase));
269 nsPopupLevel nsMenuPopupFrame::PopupLevel(bool aIsNoAutoHide) const {
270 // The popup level is determined as follows, in this order:
271 // 1. non-panels (menus and tooltips) are always topmost
272 // 2. any specified level attribute
273 // 3. if a titlebar attribute is set, use the 'floating' level
274 // 4. if this is a noautohide panel, use the 'parent' level
275 // 5. use the platform-specific default level
277 // If this is not a panel, this is always a top-most popup.
278 if (mPopupType != ePopupTypePanel) return ePopupLevelTop;
280 // If the level attribute has been set, use that.
281 static Element::AttrValuesArray strings[] = {
282 nsGkAtoms::top, nsGkAtoms::parent, nsGkAtoms::floating, nullptr};
283 switch (mContent->AsElement()->FindAttrValueIn(
284 kNameSpaceID_None, nsGkAtoms::level, strings, eCaseMatters)) {
285 case 0:
286 return ePopupLevelTop;
287 case 1:
288 return ePopupLevelParent;
289 case 2:
290 return ePopupLevelFloating;
293 // Panels with titlebars most likely want to be floating popups.
294 if (mContent->AsElement()->HasAttr(kNameSpaceID_None, nsGkAtoms::titlebar))
295 return ePopupLevelFloating;
297 // If this panel is a noautohide panel, the default is the parent level.
298 if (aIsNoAutoHide) return ePopupLevelParent;
300 // Otherwise, the result depends on the platform.
301 return sDefaultLevelIsTop ? ePopupLevelTop : ePopupLevelParent;
304 void nsMenuPopupFrame::PrepareWidget(bool aRecreate) {
305 nsView* ourView = GetView();
306 if (aRecreate) {
307 if (auto* widget = GetWidget()) {
308 // Widget's WebRender resources needs to be cleared before creating new
309 // widget.
310 widget->ClearCachedWebrenderResources();
312 ourView->DestroyWidget();
314 if (!ourView->HasWidget()) {
315 CreateWidgetForView(ourView);
317 if (nsIWidget* widget = GetWidget()) {
318 // This won't dynamically update the color scheme changes while the widget
319 // is shown, but it's good enough.
320 widget->SetColorScheme(Some(LookAndFeel::ColorSchemeForFrame(this)));
324 nsresult nsMenuPopupFrame::CreateWidgetForView(nsView* aView) {
325 // Create a widget for ourselves.
326 nsWidgetInitData widgetData;
327 widgetData.mWindowType = eWindowType_popup;
328 widgetData.mBorderStyle = eBorderStyle_default;
329 widgetData.mForMenupopupFrame = true;
330 widgetData.mClipSiblings = true;
331 widgetData.mPopupHint = mPopupType;
332 widgetData.mNoAutoHide = IsNoAutoHide();
334 if (!mInContentShell) {
335 // A drag popup may be used for non-static translucent drag feedback
336 if (mPopupType == ePopupTypePanel &&
337 mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
338 nsGkAtoms::drag, eIgnoreCase)) {
339 widgetData.mIsDragPopup = true;
343 nsAutoString title;
344 if (widgetData.mNoAutoHide) {
345 if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
346 nsGkAtoms::titlebar,
347 nsGkAtoms::normal, eCaseMatters)) {
348 widgetData.mBorderStyle = eBorderStyle_title;
350 mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::label,
351 title);
353 if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
354 nsGkAtoms::close, nsGkAtoms::_true,
355 eCaseMatters)) {
356 widgetData.mBorderStyle = static_cast<enum nsBorderStyle>(
357 widgetData.mBorderStyle | eBorderStyle_close);
362 bool remote = HasRemoteContent();
364 nsTransparencyMode mode = nsLayoutUtils::GetFrameTransparency(this, this);
365 widgetData.mHasRemoteContent = remote;
366 widgetData.mSupportTranslucency = mode == eTransparencyTransparent;
367 widgetData.mPopupLevel = PopupLevel(widgetData.mNoAutoHide);
369 // Panels which have a parent level need a parent widget. This allows them to
370 // always appear in front of the parent window but behind other windows that
371 // should be in front of it.
372 nsCOMPtr<nsIWidget> parentWidget;
373 if (widgetData.mPopupLevel != ePopupLevelTop) {
374 nsCOMPtr<nsIDocShellTreeItem> dsti = PresContext()->GetDocShell();
375 if (!dsti) return NS_ERROR_FAILURE;
377 nsCOMPtr<nsIDocShellTreeOwner> treeOwner;
378 dsti->GetTreeOwner(getter_AddRefs(treeOwner));
379 if (!treeOwner) return NS_ERROR_FAILURE;
381 nsCOMPtr<nsIBaseWindow> baseWindow(do_QueryInterface(treeOwner));
382 if (baseWindow) baseWindow->GetMainWidget(getter_AddRefs(parentWidget));
385 nsresult rv =
386 aView->CreateWidgetForPopup(&widgetData, parentWidget, true, true);
387 if (NS_FAILED(rv)) {
388 return rv;
391 nsIWidget* widget = aView->GetWidget();
392 widget->SetTransparencyMode(mode);
393 widget->SetInputRegion(ComputeInputRegion(*Style(), *PresContext()));
394 widget->SetWindowShadowStyle(GetShadowStyle());
395 widget->SetWindowOpacity(StyleUIReset()->mWindowOpacity);
396 widget->SetWindowTransform(ComputeWidgetTransform());
398 // most popups don't have a title so avoid setting the title if there isn't
399 // one
400 if (!title.IsEmpty()) {
401 widget->SetTitle(title);
404 return NS_OK;
407 bool nsMenuPopupFrame::IsMouseTransparent() const {
408 return ::IsMouseTransparent(*Style());
411 StyleWindowShadow nsMenuPopupFrame::GetShadowStyle() {
412 StyleWindowShadow shadow = StyleUIReset()->mWindowShadow;
413 if (shadow != StyleWindowShadow::Default) return shadow;
415 switch (StyleDisplay()->EffectiveAppearance()) {
416 case StyleAppearance::Tooltip:
417 return StyleWindowShadow::Tooltip;
418 case StyleAppearance::Menupopup:
419 return StyleWindowShadow::Menu;
420 default:
421 return StyleWindowShadow::Default;
425 void nsMenuPopupFrame::SetPopupState(nsPopupState aState) {
426 mPopupState = aState;
428 // Work around https://gitlab.gnome.org/GNOME/gtk/-/issues/4166
429 if (aState == ePopupShown && IS_WAYLAND_DISPLAY()) {
430 if (nsIWidget* widget = GetWidget()) {
431 widget->SetInputRegion(ComputeInputRegion(*Style(), *PresContext()));
436 // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398)
437 MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP nsXULPopupShownEvent::Run() {
438 nsMenuPopupFrame* popup = do_QueryFrame(mPopup->GetPrimaryFrame());
439 // Set the state to visible if the popup is still open.
440 if (popup && popup->IsOpen()) {
441 popup->SetPopupState(ePopupShown);
444 if (!mPopup->IsXULElement(nsGkAtoms::tooltip)) {
445 nsCOMPtr<nsIObserverService> obsService =
446 mozilla::services::GetObserverService();
447 if (obsService) {
448 obsService->NotifyObservers(mPopup, "popup-shown", nullptr);
451 WidgetMouseEvent event(true, eXULPopupShown, nullptr,
452 WidgetMouseEvent::eReal);
453 return EventDispatcher::Dispatch(mPopup, mPresContext, &event);
456 NS_IMETHODIMP nsXULPopupShownEvent::HandleEvent(Event* aEvent) {
457 nsMenuPopupFrame* popup = do_QueryFrame(mPopup->GetPrimaryFrame());
458 // Ignore events not targeted at the popup itself (ie targeted at
459 // descendants):
460 if (mPopup != aEvent->GetTarget()) {
461 return NS_OK;
463 if (popup) {
464 // ResetPopupShownDispatcher will delete the reference to this, so keep
465 // another one until Run is finished.
466 RefPtr<nsXULPopupShownEvent> event = this;
467 // Only call Run if it the dispatcher was assigned. This avoids calling the
468 // Run method if the transitionend event fires multiple times.
469 if (popup->ClearPopupShownDispatcher()) {
470 return Run();
474 CancelListener();
475 return NS_OK;
478 void nsXULPopupShownEvent::CancelListener() {
479 mPopup->RemoveSystemEventListener(u"transitionend"_ns, this, false);
482 NS_IMPL_ISUPPORTS_INHERITED(nsXULPopupShownEvent, Runnable,
483 nsIDOMEventListener);
485 void nsMenuPopupFrame::DidSetComputedStyle(ComputedStyle* aOldStyle) {
486 nsBoxFrame::DidSetComputedStyle(aOldStyle);
488 if (!aOldStyle) {
489 return;
492 auto& newUI = *StyleUIReset();
493 auto& oldUI = *aOldStyle->StyleUIReset();
494 if (newUI.mWindowOpacity != oldUI.mWindowOpacity) {
495 if (nsIWidget* widget = GetWidget()) {
496 widget->SetWindowOpacity(newUI.mWindowOpacity);
500 if (newUI.mMozWindowTransform != oldUI.mMozWindowTransform) {
501 if (nsIWidget* widget = GetWidget()) {
502 widget->SetWindowTransform(ComputeWidgetTransform());
506 auto oldRegion = ComputeInputRegion(*aOldStyle, *PresContext());
507 auto newRegion = ComputeInputRegion(*Style(), *PresContext());
508 if (oldRegion.mFullyTransparent != newRegion.mFullyTransparent ||
509 oldRegion.mMargin != newRegion.mMargin) {
510 if (nsIWidget* widget = GetWidget()) {
511 widget->SetInputRegion(newRegion);
516 void nsMenuPopupFrame::ConstrainSizeForWayland(nsSize& aSize) const {
517 #ifdef MOZ_WAYLAND
518 if (!IS_WAYLAND_DISPLAY()) {
519 return;
522 // If the size is not a whole number in CSS pixels we need round it up to
523 // avoid reflow of the tooltips/popups and putting the text on two lines
524 // (usually happens with 200% scale factor and font scale factor <> 1) because
525 // GTK throws away the decimals.
526 int32_t appPerCSS = AppUnitsPerCSSPixel();
527 if (aSize.width % appPerCSS > 0) {
528 aSize.width += appPerCSS;
530 if (aSize.height % appPerCSS > 0) {
531 aSize.height += appPerCSS;
534 nsIWidget* widget = GetWidget();
535 if (!widget) {
536 return;
539 // Shrink the popup down if it's larger than popup size received from Wayland
540 // compositor. We don't know screen size on Wayland so this is the only info
541 // we have there.
542 const nsSize waylandSize = LayoutDeviceIntRect::ToAppUnits(
543 widget->GetMoveToRectPopupSize(), PresContext()->AppUnitsPerDevPixel());
544 if (waylandSize.width > 0 && aSize.width > waylandSize.width) {
545 LOG_WAYLAND("Wayland constraint width [%p]: %d to %d", widget, aSize.width,
546 waylandSize.width);
547 aSize.width = waylandSize.width;
549 if (waylandSize.height > 0 && aSize.height > waylandSize.height) {
550 LOG_WAYLAND("Wayland constraint height [%p]: %d to %d", widget,
551 aSize.height, waylandSize.height);
552 aSize.height = waylandSize.height;
554 #endif
557 void nsMenuPopupFrame::LayoutPopup(nsBoxLayoutState& aState,
558 nsIFrame* aParentMenu, bool aSizedToPopup) {
559 if (IsNativeMenu()) {
560 return;
563 mSizedToPopup = aSizedToPopup;
565 SchedulePaint();
567 bool shouldPosition = [&] {
568 if (!IsAnchored()) {
569 return true;
571 if (ShouldFollowAnchor()) {
572 return true;
574 // Don't reposition anchored popups that shouldn't follow the anchor and
575 // have already been positioned.
576 return mPopupState != ePopupShown || mUsedScreenRect.IsEmpty();
577 }();
579 bool isOpen = IsOpen();
580 if (!isOpen) {
581 // if the popup is not open, only do layout while showing or if the menu
582 // is sized to the popup
583 shouldPosition =
584 (mPopupState == ePopupShowing || mPopupState == ePopupPositioning);
585 if (!shouldPosition && !aSizedToPopup) {
586 RemoveStateBits(NS_FRAME_FIRST_REFLOW);
587 return;
591 // if the popup has just been opened, make sure the scrolled window is at 0,0
592 // Don't scroll menulists as they will scroll to their selected item on their
593 // own.
594 if (mIsOpenChanged && !IsMenuList()) {
595 nsIScrollableFrame* scrollframe =
596 do_QueryFrame(nsIFrame::GetChildXULBox(this));
597 if (scrollframe) {
598 AutoWeakFrame weakFrame(this);
599 scrollframe->ScrollTo(nsPoint(0, 0), ScrollMode::Instant);
600 if (!weakFrame.IsAlive()) {
601 return;
606 // get the preferred, minimum and maximum size. If the menu is sized to the
607 // popup, then the popup's width is the menu's width.
608 nsSize prefSize = GetXULPrefSize(aState);
609 nsSize minSize = GetXULMinSize(aState);
610 nsSize maxSize = GetXULMaxSize(aState);
612 if (aSizedToPopup) {
613 prefSize.width = aParentMenu->GetRect().width;
616 prefSize = XULBoundsCheck(minSize, prefSize, maxSize);
618 ConstrainSizeForWayland(prefSize);
620 const bool sizeChanged = mPrefSize != prefSize;
621 // if the size changed then set the bounds to be the preferred size, and make
622 // sure we re-position the popup too (as that can shrink or resize us again).
623 if (sizeChanged) {
624 shouldPosition = true;
625 SetXULBounds(aState, nsRect(0, 0, prefSize.width, prefSize.height), false);
626 mPrefSize = prefSize;
629 bool needCallback = false;
630 if (shouldPosition) {
631 SetPopupPosition(aParentMenu, false, aSizedToPopup);
632 needCallback = true;
635 nsRect bounds(GetRect());
636 XULLayout(aState);
638 // if the width or height changed, readjust the popup position. This is a
639 // special case for tooltips where the preferred height doesn't include the
640 // real height for its inline element, but does once it is laid out.
641 // This is bug 228673 which doesn't have a simple fix.
642 bool rePosition = shouldPosition && (mPosition == POPUPPOSITION_SELECTION);
643 if (!aParentMenu) {
644 nsSize newsize = GetSize();
645 if (newsize.width > bounds.width || newsize.height > bounds.height) {
646 // the size after layout was larger than the preferred size,
647 // so set the preferred size accordingly
648 mPrefSize = newsize;
649 if (isOpen) {
650 rePosition = true;
651 needCallback = true;
656 if (rePosition) {
657 SetPopupPosition(aParentMenu, false, aSizedToPopup);
660 nsPresContext* pc = PresContext();
661 nsView* view = GetView();
663 if (sizeChanged) {
664 // If the size of the popup changed, apply any size constraints.
665 nsIWidget* widget = view->GetWidget();
666 if (widget) {
667 SetSizeConstraints(pc, widget, minSize, maxSize);
671 if (isOpen) {
672 nsViewManager* viewManager = view->GetViewManager();
673 nsRect rect = GetRect();
674 rect.x = rect.y = 0;
675 rect.SizeTo(XULBoundsCheck(minSize, rect.Size(), maxSize));
676 viewManager->ResizeView(view, rect);
678 if (mPopupState == ePopupOpening) {
679 mPopupState = ePopupVisible;
682 viewManager->SetViewVisibility(view, nsViewVisibility_kShow);
683 SyncFrameViewProperties(view);
686 // finally, if the popup just opened, send a popupshown event
687 bool openChanged = mIsOpenChanged;
688 if (openChanged) {
689 mIsOpenChanged = false;
691 // Make sure the current selection in a menulist is visible.
692 if (IsMenuList() && mCurrentMenu) {
693 EnsureMenuItemIsVisible(mCurrentMenu);
696 // If the animate attribute is set to open, check for a transition and wait
697 // for it to finish before firing the popupshown event.
698 if (StaticPrefs::xul_panel_animations_enabled() &&
699 mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
700 nsGkAtoms::animate, nsGkAtoms::open,
701 eCaseMatters) &&
702 AnimationUtils::HasCurrentTransitions(mContent->AsElement(),
703 PseudoStyleType::NotPseudo)) {
704 mPopupShownDispatcher = new nsXULPopupShownEvent(mContent, pc);
705 mContent->AddSystemEventListener(u"transitionend"_ns,
706 mPopupShownDispatcher, false, false);
707 return;
710 // If there are no transitions, fire the popupshown event right away.
711 nsCOMPtr<nsIRunnable> event = new nsXULPopupShownEvent(GetContent(), pc);
712 mContent->OwnerDoc()->Dispatch(TaskCategory::Other, event.forget());
715 if (needCallback && !mReflowCallbackData.mPosted) {
716 pc->PresShell()->PostReflowCallback(this);
717 mReflowCallbackData.MarkPosted(aParentMenu, openChanged);
721 bool nsMenuPopupFrame::ReflowFinished() {
722 SetPopupPosition(mReflowCallbackData.mAnchor, false, mSizedToPopup);
723 mReflowCallbackData.Clear();
724 return false;
727 void nsMenuPopupFrame::ReflowCallbackCanceled() { mReflowCallbackData.Clear(); }
729 bool nsMenuPopupFrame::IsMenuList() {
730 nsIFrame* parentMenu = GetParent();
731 return (parentMenu && parentMenu->GetContent() &&
732 parentMenu->GetContent()->IsXULElement(nsGkAtoms::menulist));
735 nsIContent* nsMenuPopupFrame::GetTriggerContent(
736 nsMenuPopupFrame* aMenuPopupFrame) {
737 while (aMenuPopupFrame) {
738 if (aMenuPopupFrame->mTriggerContent)
739 return aMenuPopupFrame->mTriggerContent;
741 // check up the menu hierarchy until a popup with a trigger node is found
742 nsMenuFrame* menuFrame = do_QueryFrame(aMenuPopupFrame->GetParent());
743 if (!menuFrame) break;
745 nsMenuParent* parentPopup = menuFrame->GetMenuParent();
746 if (!parentPopup || !parentPopup->IsMenu()) break;
748 aMenuPopupFrame = static_cast<nsMenuPopupFrame*>(parentPopup);
751 return nullptr;
754 void nsMenuPopupFrame::InitPositionFromAnchorAlign(const nsAString& aAnchor,
755 const nsAString& aAlign) {
756 mTriggerContent = nullptr;
758 if (aAnchor.EqualsLiteral("topleft"))
759 mPopupAnchor = POPUPALIGNMENT_TOPLEFT;
760 else if (aAnchor.EqualsLiteral("topright"))
761 mPopupAnchor = POPUPALIGNMENT_TOPRIGHT;
762 else if (aAnchor.EqualsLiteral("bottomleft"))
763 mPopupAnchor = POPUPALIGNMENT_BOTTOMLEFT;
764 else if (aAnchor.EqualsLiteral("bottomright"))
765 mPopupAnchor = POPUPALIGNMENT_BOTTOMRIGHT;
766 else if (aAnchor.EqualsLiteral("leftcenter"))
767 mPopupAnchor = POPUPALIGNMENT_LEFTCENTER;
768 else if (aAnchor.EqualsLiteral("rightcenter"))
769 mPopupAnchor = POPUPALIGNMENT_RIGHTCENTER;
770 else if (aAnchor.EqualsLiteral("topcenter"))
771 mPopupAnchor = POPUPALIGNMENT_TOPCENTER;
772 else if (aAnchor.EqualsLiteral("bottomcenter"))
773 mPopupAnchor = POPUPALIGNMENT_BOTTOMCENTER;
774 else
775 mPopupAnchor = POPUPALIGNMENT_NONE;
777 if (aAlign.EqualsLiteral("topleft"))
778 mPopupAlignment = POPUPALIGNMENT_TOPLEFT;
779 else if (aAlign.EqualsLiteral("topright"))
780 mPopupAlignment = POPUPALIGNMENT_TOPRIGHT;
781 else if (aAlign.EqualsLiteral("bottomleft"))
782 mPopupAlignment = POPUPALIGNMENT_BOTTOMLEFT;
783 else if (aAlign.EqualsLiteral("bottomright"))
784 mPopupAlignment = POPUPALIGNMENT_BOTTOMRIGHT;
785 else
786 mPopupAlignment = POPUPALIGNMENT_NONE;
788 mPosition = POPUPPOSITION_UNKNOWN;
791 void nsMenuPopupFrame::InitializePopup(nsIContent* aAnchorContent,
792 nsIContent* aTriggerContent,
793 const nsAString& aPosition,
794 int32_t aXPos, int32_t aYPos,
795 MenuPopupAnchorType aAnchorType,
796 bool aAttributesOverride) {
797 auto* widget = GetWidget();
798 bool recreateWidget = widget && widget->NeedsRecreateToReshow();
799 PrepareWidget(recreateWidget);
801 mPopupState = ePopupShowing;
802 mAnchorContent = aAnchorContent;
803 mTriggerContent = aTriggerContent;
804 mXPos = aXPos;
805 mYPos = aYPos;
806 mIsNativeMenu = false;
807 mIsTopLevelContextMenu = false;
808 mVFlip = false;
809 mHFlip = false;
810 mAlignmentOffset = 0;
811 mPositionedOffset = 0;
812 mPositionedByMoveToRect = false;
814 mAnchorType = aAnchorType;
816 // if aAttributesOverride is true, then the popupanchor, popupalign and
817 // position attributes on the <menupopup> override those values passed in.
818 // If false, those attributes are only used if the values passed in are empty
819 if (aAnchorContent || aAnchorType == MenuPopupAnchorType_Rect) {
820 nsAutoString anchor, align, position, flip;
821 mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::popupanchor,
822 anchor);
823 mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::popupalign,
824 align);
825 mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::position,
826 position);
827 mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::flip, flip);
829 if (aAttributesOverride) {
830 // if the attributes are set, clear the offset position. Otherwise,
831 // the offset is used to adjust the position from the anchor point
832 if (anchor.IsEmpty() && align.IsEmpty() && position.IsEmpty())
833 position.Assign(aPosition);
834 else
835 mXPos = mYPos = 0;
836 } else if (!aPosition.IsEmpty()) {
837 position.Assign(aPosition);
840 if (flip.EqualsLiteral("none")) {
841 mFlip = FlipType_None;
842 } else if (flip.EqualsLiteral("both")) {
843 mFlip = FlipType_Both;
844 } else if (flip.EqualsLiteral("slide")) {
845 mFlip = FlipType_Slide;
848 position.CompressWhitespace();
849 int32_t spaceIdx = position.FindChar(' ');
850 // if there is a space in the position, assume it is the anchor and
851 // alignment as two separate tokens.
852 if (spaceIdx >= 0) {
853 InitPositionFromAnchorAlign(Substring(position, 0, spaceIdx),
854 Substring(position, spaceIdx + 1));
855 } else if (position.EqualsLiteral("before_start")) {
856 mPopupAnchor = POPUPALIGNMENT_TOPLEFT;
857 mPopupAlignment = POPUPALIGNMENT_BOTTOMLEFT;
858 mPosition = POPUPPOSITION_BEFORESTART;
859 } else if (position.EqualsLiteral("before_end")) {
860 mPopupAnchor = POPUPALIGNMENT_TOPRIGHT;
861 mPopupAlignment = POPUPALIGNMENT_BOTTOMRIGHT;
862 mPosition = POPUPPOSITION_BEFOREEND;
863 } else if (position.EqualsLiteral("after_start")) {
864 mPopupAnchor = POPUPALIGNMENT_BOTTOMLEFT;
865 mPopupAlignment = POPUPALIGNMENT_TOPLEFT;
866 mPosition = POPUPPOSITION_AFTERSTART;
867 } else if (position.EqualsLiteral("after_end")) {
868 mPopupAnchor = POPUPALIGNMENT_BOTTOMRIGHT;
869 mPopupAlignment = POPUPALIGNMENT_TOPRIGHT;
870 mPosition = POPUPPOSITION_AFTEREND;
871 } else if (position.EqualsLiteral("start_before")) {
872 mPopupAnchor = POPUPALIGNMENT_TOPLEFT;
873 mPopupAlignment = POPUPALIGNMENT_TOPRIGHT;
874 mPosition = POPUPPOSITION_STARTBEFORE;
875 } else if (position.EqualsLiteral("start_after")) {
876 mPopupAnchor = POPUPALIGNMENT_BOTTOMLEFT;
877 mPopupAlignment = POPUPALIGNMENT_BOTTOMRIGHT;
878 mPosition = POPUPPOSITION_STARTAFTER;
879 } else if (position.EqualsLiteral("end_before")) {
880 mPopupAnchor = POPUPALIGNMENT_TOPRIGHT;
881 mPopupAlignment = POPUPALIGNMENT_TOPLEFT;
882 mPosition = POPUPPOSITION_ENDBEFORE;
883 } else if (position.EqualsLiteral("end_after")) {
884 mPopupAnchor = POPUPALIGNMENT_BOTTOMRIGHT;
885 mPopupAlignment = POPUPALIGNMENT_BOTTOMLEFT;
886 mPosition = POPUPPOSITION_ENDAFTER;
887 } else if (position.EqualsLiteral("overlap")) {
888 mPopupAnchor = POPUPALIGNMENT_TOPLEFT;
889 mPopupAlignment = POPUPALIGNMENT_TOPLEFT;
890 mPosition = POPUPPOSITION_OVERLAP;
891 } else if (position.EqualsLiteral("after_pointer")) {
892 mPopupAnchor = POPUPALIGNMENT_TOPLEFT;
893 mPopupAlignment = POPUPALIGNMENT_TOPLEFT;
894 mPosition = POPUPPOSITION_AFTERPOINTER;
895 // XXXndeakin this is supposed to anchor vertically after, but with the
896 // horizontal position as the mouse pointer.
897 mYPos += 21;
898 } else if (position.EqualsLiteral("selection")) {
899 mPopupAnchor = POPUPALIGNMENT_BOTTOMLEFT;
900 mPopupAlignment = POPUPALIGNMENT_TOPLEFT;
901 mPosition = POPUPPOSITION_SELECTION;
902 } else {
903 InitPositionFromAnchorAlign(anchor, align);
906 // When converted back to CSSIntRect it is (-1, -1, 0, 0) - as expected in
907 // nsXULPopupManager::Rollup
908 mScreenRect = nsRect(-AppUnitsPerCSSPixel(), -AppUnitsPerCSSPixel(), 0, 0);
910 if (aAttributesOverride) {
911 // Use |left| and |top| dimension attributes to position the popup if
912 // present, as they may have been persisted.
913 nsAutoString left, top;
914 mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::left, left);
915 mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::top, top);
917 nsresult err;
918 if (!left.IsEmpty()) {
919 int32_t x = left.ToInteger(&err);
920 if (NS_SUCCEEDED(err)) {
921 mScreenRect.x = CSSPixel::ToAppUnits(x);
924 if (!top.IsEmpty()) {
925 int32_t y = top.ToInteger(&err);
926 if (NS_SUCCEEDED(err)) {
927 mScreenRect.y = CSSPixel::ToAppUnits(y);
933 void nsMenuPopupFrame::InitializePopupAtScreen(nsIContent* aTriggerContent,
934 int32_t aXPos, int32_t aYPos,
935 bool aIsContextMenu) {
936 auto* widget = GetWidget();
937 bool recreateWidget = widget && widget->NeedsRecreateToReshow();
938 PrepareWidget(recreateWidget);
940 mPopupState = ePopupShowing;
941 mAnchorContent = nullptr;
942 mTriggerContent = aTriggerContent;
943 mScreenRect =
944 nsRect(CSSPixel::ToAppUnits(aXPos), CSSPixel::ToAppUnits(aYPos), 0, 0);
945 mXPos = 0;
946 mYPos = 0;
947 mFlip = FlipType_Default;
948 mPopupAnchor = POPUPALIGNMENT_NONE;
949 mPopupAlignment = POPUPALIGNMENT_NONE;
950 mPosition = POPUPPOSITION_UNKNOWN;
951 mIsContextMenu = aIsContextMenu;
952 mIsTopLevelContextMenu = aIsContextMenu;
953 mIsNativeMenu = false;
954 mAnchorType = MenuPopupAnchorType_Point;
955 mPositionedOffset = 0;
956 mPositionedByMoveToRect = false;
959 void nsMenuPopupFrame::InitializePopupAsNativeContextMenu(
960 nsIContent* aTriggerContent, int32_t aXPos, int32_t aYPos) {
961 mTriggerContent = aTriggerContent;
962 mPopupState = ePopupShowing;
963 mAnchorContent = nullptr;
964 mScreenRect =
965 nsRect(CSSPixel::ToAppUnits(aXPos), CSSPixel::ToAppUnits(aYPos), 0, 0);
966 mXPos = 0;
967 mYPos = 0;
968 mFlip = FlipType_Default;
969 mPopupAnchor = POPUPALIGNMENT_NONE;
970 mPopupAlignment = POPUPALIGNMENT_NONE;
971 mPosition = POPUPPOSITION_UNKNOWN;
972 mIsContextMenu = true;
973 mIsTopLevelContextMenu = true;
974 mIsNativeMenu = true;
975 mAnchorType = MenuPopupAnchorType_Point;
976 mPositionedOffset = 0;
977 mPositionedByMoveToRect = false;
980 void nsMenuPopupFrame::InitializePopupAtRect(nsIContent* aTriggerContent,
981 const nsAString& aPosition,
982 const nsIntRect& aRect,
983 bool aAttributesOverride) {
984 InitializePopup(nullptr, aTriggerContent, aPosition, 0, 0,
985 MenuPopupAnchorType_Rect, aAttributesOverride);
986 mScreenRect = ToAppUnits(aRect, AppUnitsPerCSSPixel());
989 void nsMenuPopupFrame::ShowPopup(bool aIsContextMenu) {
990 mIsContextMenu = aIsContextMenu;
992 InvalidateFrameSubtree();
994 if (mPopupState == ePopupShowing || mPopupState == ePopupPositioning) {
995 mPopupState = ePopupOpening;
996 mIsOpenChanged = true;
998 // Clear mouse capture when a popup is opened.
999 if (mPopupType == ePopupTypeMenu) {
1000 EventStateManager* activeESM = static_cast<EventStateManager*>(
1001 EventStateManager::GetActiveEventStateManager());
1002 if (activeESM) {
1003 EventStateManager::ClearGlobalActiveContent(activeESM);
1006 PresShell::ReleaseCapturingContent();
1009 nsMenuFrame* menuFrame = do_QueryFrame(GetParent());
1010 if (menuFrame) {
1011 AutoWeakFrame weakFrame(this);
1012 menuFrame->PopupOpened();
1013 if (!weakFrame.IsAlive()) return;
1016 // do we need an actual reflow here?
1017 // is SetPopupPosition all that is needed?
1018 PresShell()->FrameNeedsReflow(this, IntrinsicDirty::TreeChange,
1019 NS_FRAME_HAS_DIRTY_CHILDREN);
1021 if (mPopupType == ePopupTypeMenu) {
1022 nsCOMPtr<nsISound> sound(do_GetService("@mozilla.org/sound;1"));
1023 if (sound) sound->PlayEventSound(nsISound::EVENT_MENU_POPUP);
1027 mShouldAutoPosition = true;
1030 void nsMenuPopupFrame::ClearTriggerContentIncludingDocument() {
1031 // clear the trigger content if the popup is being closed. But don't clear
1032 // it if the popup is just being made invisible as a popuphiding or command
1033 if (mTriggerContent) {
1034 // if the popup had a trigger node set, clear the global window popup node
1035 // as well
1036 Document* doc = mContent->GetUncomposedDoc();
1037 if (doc) {
1038 if (nsPIDOMWindowOuter* win = doc->GetWindow()) {
1039 nsCOMPtr<nsPIWindowRoot> root = win->GetTopWindowRoot();
1040 if (root) {
1041 root->SetPopupNode(nullptr);
1046 mTriggerContent = nullptr;
1049 void nsMenuPopupFrame::HidePopup(bool aDeselectMenu, nsPopupState aNewState) {
1050 NS_ASSERTION(aNewState == ePopupClosed || aNewState == ePopupInvisible,
1051 "popup being set to unexpected state");
1053 ClearPopupShownDispatcher();
1055 // don't hide the popup when it isn't open
1056 if (mPopupState == ePopupClosed || mPopupState == ePopupShowing ||
1057 mPopupState == ePopupPositioning)
1058 return;
1060 if (aNewState == ePopupClosed) {
1061 // clear the trigger content if the popup is being closed. But don't clear
1062 // it if the popup is just being made invisible as a popuphiding or command
1063 // event may want to retrieve it.
1064 ClearTriggerContentIncludingDocument();
1065 mAnchorContent = nullptr;
1068 // when invisible and about to be closed, HidePopup has already been called,
1069 // so just set the new state to closed and return
1070 if (mPopupState == ePopupInvisible) {
1071 if (aNewState == ePopupClosed) mPopupState = ePopupClosed;
1072 return;
1075 mPopupState = aNewState;
1077 if (IsMenu()) SetCurrentMenuItem(nullptr);
1079 mIncrementalString.Truncate();
1081 LockMenuUntilClosed(false);
1083 mIsOpenChanged = false;
1084 mCurrentMenu = nullptr; // make sure no current menu is set
1085 mHFlip = mVFlip = false;
1087 if (auto* widget = GetWidget()) {
1088 // Ideally we should call ClearCachedWebrenderResources but there are
1089 // intermittent failures (see bug 1748788), so we currently call
1090 // ClearWebrenderAnimationResources instead.
1091 widget->ClearWebrenderAnimationResources();
1094 nsView* view = GetView();
1095 nsViewManager* viewManager = view->GetViewManager();
1096 viewManager->SetViewVisibility(view, nsViewVisibility_kHide);
1098 FireDOMEvent(u"DOMMenuInactive"_ns, mContent);
1100 // XXX, bug 137033, In Windows, if mouse is outside the window when the
1101 // menupopup closes, no mouse_enter/mouse_exit event will be fired to clear
1102 // current hover state, we should clear it manually. This code may not the
1103 // best solution, but we can leave it here until we find the better approach.
1104 NS_ASSERTION(mContent->IsElement(), "How do we have a non-element?");
1105 if (mContent->AsElement()->State().HasState(dom::ElementState::HOVER)) {
1106 EventStateManager* esm = PresContext()->EventStateManager();
1107 esm->SetContentState(nullptr, dom::ElementState::HOVER);
1110 nsMenuFrame* menuFrame = do_QueryFrame(GetParent());
1111 if (menuFrame) {
1112 menuFrame->PopupClosed(aDeselectMenu);
1116 nsIFrame::ReflowChildFlags nsMenuPopupFrame::GetXULLayoutFlags() {
1117 return ReflowChildFlags::NoSizeView | ReflowChildFlags::NoMoveView;
1120 nsPoint nsMenuPopupFrame::AdjustPositionForAnchorAlign(nsRect& anchorRect,
1121 FlipStyle& aHFlip,
1122 FlipStyle& aVFlip) {
1123 // flip the anchor and alignment for right-to-left
1124 int8_t popupAnchor(mPopupAnchor);
1125 int8_t popupAlign(mPopupAlignment);
1126 if (IsDirectionRTL()) {
1127 // no need to flip the centered anchor types vertically
1128 if (popupAnchor <= POPUPALIGNMENT_LEFTCENTER) {
1129 popupAnchor = -popupAnchor;
1131 popupAlign = -popupAlign;
1134 nsRect originalAnchorRect(anchorRect);
1136 // first, determine at which corner of the anchor the popup should appear
1137 nsPoint pnt;
1138 switch (popupAnchor) {
1139 case POPUPALIGNMENT_LEFTCENTER:
1140 pnt = nsPoint(anchorRect.x, anchorRect.y + anchorRect.height / 2);
1141 anchorRect.y = pnt.y;
1142 anchorRect.height = 0;
1143 break;
1144 case POPUPALIGNMENT_RIGHTCENTER:
1145 pnt = nsPoint(anchorRect.XMost(), anchorRect.y + anchorRect.height / 2);
1146 anchorRect.y = pnt.y;
1147 anchorRect.height = 0;
1148 break;
1149 case POPUPALIGNMENT_TOPCENTER:
1150 pnt = nsPoint(anchorRect.x + anchorRect.width / 2, anchorRect.y);
1151 anchorRect.x = pnt.x;
1152 anchorRect.width = 0;
1153 break;
1154 case POPUPALIGNMENT_BOTTOMCENTER:
1155 pnt = nsPoint(anchorRect.x + anchorRect.width / 2, anchorRect.YMost());
1156 anchorRect.x = pnt.x;
1157 anchorRect.width = 0;
1158 break;
1159 case POPUPALIGNMENT_TOPRIGHT:
1160 pnt = anchorRect.TopRight();
1161 break;
1162 case POPUPALIGNMENT_BOTTOMLEFT:
1163 pnt = anchorRect.BottomLeft();
1164 break;
1165 case POPUPALIGNMENT_BOTTOMRIGHT:
1166 pnt = anchorRect.BottomRight();
1167 break;
1168 case POPUPALIGNMENT_TOPLEFT:
1169 default:
1170 pnt = anchorRect.TopLeft();
1171 break;
1174 // If the alignment is on the right edge of the popup, move the popup left
1175 // by the width. Similarly, if the alignment is on the bottom edge of the
1176 // popup, move the popup up by the height. In addition, account for the
1177 // margins of the popup on the edge on which it is aligned.
1178 nsMargin margin = GetMargin();
1179 switch (popupAlign) {
1180 case POPUPALIGNMENT_TOPRIGHT:
1181 pnt.MoveBy(-mRect.width - margin.right, margin.top);
1182 break;
1183 case POPUPALIGNMENT_BOTTOMLEFT:
1184 pnt.MoveBy(margin.left, -mRect.height - margin.bottom);
1185 break;
1186 case POPUPALIGNMENT_BOTTOMRIGHT:
1187 pnt.MoveBy(-mRect.width - margin.right, -mRect.height - margin.bottom);
1188 break;
1189 case POPUPALIGNMENT_TOPLEFT:
1190 default:
1191 pnt.MoveBy(margin.left, margin.top);
1192 break;
1195 // If we aligning to the selected item in the popup, adjust the vertical
1196 // position by the height of the menulist label and the selected item's
1197 // position.
1198 if (mPosition == POPUPPOSITION_SELECTION) {
1199 MOZ_ASSERT(popupAnchor == POPUPALIGNMENT_BOTTOMLEFT ||
1200 popupAnchor == POPUPALIGNMENT_BOTTOMRIGHT);
1201 MOZ_ASSERT(popupAlign == POPUPALIGNMENT_TOPLEFT ||
1202 popupAlign == POPUPALIGNMENT_TOPRIGHT);
1204 // Only adjust the popup if it just opened, otherwise the popup will move
1205 // around if its gets resized or the selection changed. Cache the value in
1206 // mPositionedOffset and use that instead for any future calculations.
1207 if (mIsOpenChanged || mReflowCallbackData.mIsOpenChanged) {
1208 nsIFrame* selectedItemFrame = GetSelectedItemForAlignment();
1209 if (selectedItemFrame) {
1210 int32_t scrolly = 0;
1211 nsIScrollableFrame* scrollframe = GetScrollFrame(this);
1212 if (scrollframe) {
1213 scrolly = scrollframe->GetScrollPosition().y;
1216 mPositionedOffset = originalAnchorRect.height +
1217 selectedItemFrame->GetRect().y - scrolly;
1221 pnt.y -= mPositionedOffset;
1224 // Flipping horizontally is allowed as long as the popup is above or below
1225 // the anchor. This will happen if both the anchor and alignment are top or
1226 // both are bottom, but different values. Similarly, flipping vertically is
1227 // allowed if the popup is to the left or right of the anchor. In this case,
1228 // the values of the constants are such that both must be positive or both
1229 // must be negative. A special case, used for overlap, allows flipping
1230 // vertically as well.
1231 // If we are flipping in both directions, we want to set a flip style both
1232 // horizontally and vertically. However, we want to flip on the inside edge
1233 // of the anchor. Consider the example of a typical dropdown menu.
1234 // Vertically, we flip the popup on the outside edges of the anchor menu,
1235 // however horizontally, we want to to use the inside edges so the popup
1236 // still appears underneath the anchor menu instead of floating off the
1237 // side of the menu.
1238 switch (popupAnchor) {
1239 case POPUPALIGNMENT_LEFTCENTER:
1240 case POPUPALIGNMENT_RIGHTCENTER:
1241 aHFlip = FlipStyle_Outside;
1242 aVFlip = FlipStyle_Inside;
1243 break;
1244 case POPUPALIGNMENT_TOPCENTER:
1245 case POPUPALIGNMENT_BOTTOMCENTER:
1246 aHFlip = FlipStyle_Inside;
1247 aVFlip = FlipStyle_Outside;
1248 break;
1249 default: {
1250 FlipStyle anchorEdge =
1251 mFlip == FlipType_Both ? FlipStyle_Inside : FlipStyle_None;
1252 aHFlip = (popupAnchor == -popupAlign) ? FlipStyle_Outside : anchorEdge;
1253 if (((popupAnchor > 0) == (popupAlign > 0)) ||
1254 (popupAnchor == POPUPALIGNMENT_TOPLEFT &&
1255 popupAlign == POPUPALIGNMENT_TOPLEFT))
1256 aVFlip = FlipStyle_Outside;
1257 else
1258 aVFlip = anchorEdge;
1259 break;
1263 return pnt;
1266 nsIFrame* nsMenuPopupFrame::GetSelectedItemForAlignment() {
1267 // This method adjusts a menulist's popup such that the selected item is under
1268 // the cursor, aligned with the menulist label.
1269 nsCOMPtr<nsIDOMXULSelectControlElement> select;
1270 if (mAnchorContent) {
1271 select = mAnchorContent->AsElement()->AsXULSelectControl();
1274 if (!select) {
1275 // If there isn't an anchor, then try just getting the parent of the popup.
1276 select = mContent->GetParent()->AsElement()->AsXULSelectControl();
1277 if (!select) {
1278 return nullptr;
1282 nsCOMPtr<Element> selectedElement;
1283 select->GetSelectedItem(getter_AddRefs(selectedElement));
1284 return selectedElement ? selectedElement->GetPrimaryFrame() : nullptr;
1287 nscoord nsMenuPopupFrame::SlideOrResize(nscoord& aScreenPoint, nscoord aSize,
1288 nscoord aScreenBegin,
1289 nscoord aScreenEnd, nscoord* aOffset) {
1290 // The popup may be positioned such that either the left/top or bottom/right
1291 // is outside the screen - but never both.
1292 nscoord newPos =
1293 std::max(aScreenBegin, std::min(aScreenEnd - aSize, aScreenPoint));
1294 *aOffset = newPos - aScreenPoint;
1295 aScreenPoint = newPos;
1296 return std::min(aSize, aScreenEnd - aScreenPoint);
1299 nscoord nsMenuPopupFrame::FlipOrResize(nscoord& aScreenPoint, nscoord aSize,
1300 nscoord aScreenBegin, nscoord aScreenEnd,
1301 nscoord aAnchorBegin, nscoord aAnchorEnd,
1302 nscoord aMarginBegin, nscoord aMarginEnd,
1303 FlipStyle aFlip, bool aEndAligned,
1304 bool* aFlipSide) {
1305 // The flip side argument will be set to true if there wasn't room and we
1306 // flipped to the opposite side.
1307 *aFlipSide = false;
1309 // all of the coordinates used here are in app units relative to the screen
1310 nscoord popupSize = aSize;
1311 if (aScreenPoint < aScreenBegin) {
1312 // at its current position, the popup would extend past the left or top
1313 // edge of the screen, so it will have to be moved or resized.
1314 if (aFlip) {
1315 // for inside flips, we flip on the opposite side of the anchor
1316 nscoord startpos = aFlip == FlipStyle_Outside ? aAnchorBegin : aAnchorEnd;
1317 nscoord endpos = aFlip == FlipStyle_Outside ? aAnchorEnd : aAnchorBegin;
1319 // check whether there is more room to the left and right (or top and
1320 // bottom) of the anchor and put the popup on the side with more room.
1321 if (startpos - aScreenBegin >= aScreenEnd - endpos) {
1322 aScreenPoint = aScreenBegin;
1323 popupSize = startpos - aScreenPoint - aMarginEnd;
1324 *aFlipSide = !aEndAligned;
1325 } else {
1326 // If the newly calculated position is different than the existing
1327 // position, flip such that the popup is to the right or bottom of the
1328 // anchor point instead . However, when flipping use the same margin
1329 // size.
1330 nscoord newScreenPoint = endpos + aMarginEnd;
1331 if (newScreenPoint != aScreenPoint) {
1332 *aFlipSide = aEndAligned;
1333 aScreenPoint = newScreenPoint;
1334 // check if the new position is still off the right or bottom edge of
1335 // the screen. If so, resize the popup.
1336 if (aScreenPoint + aSize > aScreenEnd) {
1337 popupSize = aScreenEnd - aScreenPoint;
1341 } else {
1342 aScreenPoint = aScreenBegin;
1344 } else if (aScreenPoint + aSize > aScreenEnd) {
1345 // at its current position, the popup would extend past the right or
1346 // bottom edge of the screen, so it will have to be moved or resized.
1347 if (aFlip) {
1348 // for inside flips, we flip on the opposite side of the anchor
1349 nscoord startpos = aFlip == FlipStyle_Outside ? aAnchorBegin : aAnchorEnd;
1350 nscoord endpos = aFlip == FlipStyle_Outside ? aAnchorEnd : aAnchorBegin;
1352 // check whether there is more room to the left and right (or top and
1353 // bottom) of the anchor and put the popup on the side with more room.
1354 if (aScreenEnd - endpos >= startpos - aScreenBegin) {
1355 *aFlipSide = aEndAligned;
1356 if (mIsContextMenu) {
1357 aScreenPoint = aScreenEnd - aSize;
1358 } else {
1359 aScreenPoint = endpos + aMarginBegin;
1360 popupSize = aScreenEnd - aScreenPoint;
1362 } else {
1363 // if the newly calculated position is different than the existing
1364 // position, we flip such that the popup is to the left or top of the
1365 // anchor point instead.
1366 nscoord newScreenPoint = startpos - aSize - aMarginBegin;
1367 if (newScreenPoint != aScreenPoint) {
1368 *aFlipSide = !aEndAligned;
1369 aScreenPoint = newScreenPoint;
1371 // check if the new position is still off the left or top edge of the
1372 // screen. If so, resize the popup.
1373 if (aScreenPoint < aScreenBegin) {
1374 aScreenPoint = aScreenBegin;
1375 if (!mIsContextMenu) {
1376 popupSize = startpos - aScreenPoint - aMarginBegin;
1381 } else {
1382 aScreenPoint = aScreenEnd - aSize;
1386 // Make sure that the point is within the screen boundaries and that the
1387 // size isn't off the edge of the screen. This can happen when a large
1388 // positive or negative margin is used.
1389 if (aScreenPoint < aScreenBegin) {
1390 aScreenPoint = aScreenBegin;
1392 if (aScreenPoint > aScreenEnd) {
1393 aScreenPoint = aScreenEnd - aSize;
1396 // If popupSize ended up being negative, or the original size was actually
1397 // smaller than the calculated popup size, just use the original size instead.
1398 if (popupSize <= 0 || aSize < popupSize) {
1399 popupSize = aSize;
1402 return std::min(popupSize, aScreenEnd - aScreenPoint);
1405 nsRect nsMenuPopupFrame::ComputeAnchorRect(nsPresContext* aRootPresContext,
1406 nsIFrame* aAnchorFrame) {
1407 // Get the root frame for a reference
1408 nsIFrame* rootFrame = aRootPresContext->PresShell()->GetRootFrame();
1410 // The dimensions of the anchor
1411 nsRect anchorRect = aAnchorFrame->GetRectRelativeToSelf();
1413 // Relative to the root
1414 anchorRect = nsLayoutUtils::TransformFrameRectToAncestor(
1415 aAnchorFrame, anchorRect, rootFrame);
1416 // Relative to the screen
1417 anchorRect.MoveBy(rootFrame->GetScreenRectInAppUnits().TopLeft());
1419 // In its own app units
1420 return anchorRect.ScaleToOtherAppUnitsRoundOut(
1421 aRootPresContext->AppUnitsPerDevPixel(),
1422 PresContext()->AppUnitsPerDevPixel());
1425 nsresult nsMenuPopupFrame::SetPopupPosition(nsIFrame* aAnchorFrame,
1426 bool aIsMove, bool aSizedToPopup) {
1427 if (!mShouldAutoPosition) {
1428 return NS_OK;
1431 // If this is due to a move, return early if the popup hasn't been laid out
1432 // yet. On Windows, this can happen when using a drag popup before it opens.
1433 if (aIsMove && (mPrefSize.width == -1 || mPrefSize.height == -1)) {
1434 return NS_OK;
1437 nsPresContext* presContext = PresContext();
1438 nsIFrame* rootFrame = presContext->PresShell()->GetRootFrame();
1439 NS_ASSERTION(rootFrame->GetView() && GetView() &&
1440 rootFrame->GetView() == GetView()->GetParent(),
1441 "rootFrame's view is not our view's parent???");
1443 // For anchored popups, the anchor rectangle. For non-anchored popups, the
1444 // size will be 0.
1445 nsRect anchorRect;
1447 bool anchored = IsAnchored();
1448 if (anchored || aSizedToPopup) {
1449 // In order to deal with transforms, we need the root prescontext:
1450 nsPresContext* rootPresContext = presContext->GetRootPresContext();
1452 // If we can't reach a root pres context, don't bother continuing:
1453 if (!rootPresContext) {
1454 return NS_OK;
1457 // If anchored to a rectangle, use that rectangle. Otherwise, determine the
1458 // rectangle from the anchor.
1459 if (mAnchorType == MenuPopupAnchorType_Rect) {
1460 anchorRect = mScreenRect;
1461 } else {
1462 // if the frame is not specified, use the anchor node passed to OpenPopup.
1463 // If that wasn't specified either, use the root frame. Note that
1464 // mAnchorContent might be a different document so its presshell must be
1465 // used.
1466 if (!aAnchorFrame) {
1467 if (mAnchorContent) {
1468 aAnchorFrame = mAnchorContent->GetPrimaryFrame();
1471 if (!aAnchorFrame) {
1472 aAnchorFrame = rootFrame;
1473 if (!aAnchorFrame) return NS_OK;
1477 anchorRect = ComputeAnchorRect(rootPresContext, aAnchorFrame);
1481 // Set the popup's size to the preferred size. Below, this size will be
1482 // adjusted to fit on the screen or within the content area. If the anchor
1483 // is sized to the popup, use the anchor's width instead of the preferred
1484 // width. The preferred size should already be set by the parent frame.
1486 NS_ASSERTION(mPrefSize.width >= 0 || mPrefSize.height >= 0,
1487 "preferred size of popup not set");
1488 nsSize newSize = mPrefSize;
1489 if (aSizedToPopup) {
1490 // Input margin doesn't have contents, so account for it for popup sizing
1491 // purposes.
1492 const nscoord inputMargin =
1493 StyleUIReset()->mMozWindowInputRegionMargin.ToAppUnits();
1494 newSize.width = anchorRect.width + 2 * inputMargin;
1495 // If we're anchoring to a rect, and the rect is smaller than the
1496 // preferred size of the popup, change its width accordingly.
1497 if (mAnchorType == MenuPopupAnchorType_Rect) {
1498 newSize.width = std::max(newSize.width, mPrefSize.width);
1501 // Pref size is already constrained by LayoutPopup().
1502 ConstrainSizeForWayland(newSize);
1504 mRect.SizeTo(newSize);
1507 // the screen position in app units where the popup should appear
1508 nsPoint screenPoint;
1510 // indicators of whether the popup should be flipped or resized.
1511 FlipStyle hFlip = FlipStyle_None, vFlip = FlipStyle_None;
1513 const nsMargin margin = GetMargin();
1515 // the screen rectangle of the root frame, in dev pixels.
1516 nsRect rootScreenRect = rootFrame->GetScreenRectInAppUnits();
1518 bool isNoAutoHide = IsNoAutoHide();
1519 nsPopupLevel popupLevel = PopupLevel(isNoAutoHide);
1521 if (anchored) {
1522 // if we are anchored, there are certain things we don't want to do when
1523 // repositioning the popup to fit on the screen, such as end up positioned
1524 // over the anchor, for instance a popup appearing over the menu label.
1525 // When doing this reposition, we want to move the popup to the side with
1526 // the most room. The combination of anchor and alignment dictate if we
1527 // readjust above/below or to the left/right.
1528 if (mAnchorContent || mAnchorType == MenuPopupAnchorType_Rect) {
1529 // move the popup according to the anchor and alignment. This will also
1530 // tell us which axis the popup is flush against in case we have to move
1531 // it around later. The AdjustPositionForAnchorAlign method accounts for
1532 // the popup's margin.
1533 if (!mPositionedByMoveToRect) {
1534 mUntransformedAnchorRect = anchorRect;
1536 screenPoint = AdjustPositionForAnchorAlign(anchorRect, hFlip, vFlip);
1537 } else {
1538 // with no anchor, the popup is positioned relative to the root frame
1539 anchorRect = rootScreenRect;
1540 if (!mPositionedByMoveToRect) {
1541 mUntransformedAnchorRect = anchorRect;
1543 screenPoint = anchorRect.TopLeft() + nsPoint(margin.left, margin.top);
1546 // mXPos and mYPos specify an additional offset passed to OpenPopup that
1547 // should be added to the position. We also add the offset to the anchor
1548 // pos so a later flip/resize takes the offset into account.
1549 // FIXME(emilio): Wayland doesn't seem to be accounting for this offset
1550 // anywhere, and it probably should.
1551 nscoord anchorXOffset = CSSPixel::ToAppUnits(mXPos);
1552 if (IsDirectionRTL()) {
1553 screenPoint.x -= anchorXOffset;
1554 anchorRect.x -= anchorXOffset;
1555 } else {
1556 screenPoint.x += anchorXOffset;
1557 anchorRect.x += anchorXOffset;
1559 nscoord anchorYOffset = CSSPixel::ToAppUnits(mYPos);
1560 screenPoint.y += anchorYOffset;
1561 anchorRect.y += anchorYOffset;
1563 // If this is a noautohide popup, set the screen coordinates of the popup.
1564 // This way, the popup stays at the location where it was opened even when
1565 // the window is moved. Popups at the parent level follow the parent
1566 // window as it is moved and remained anchored, so we want to maintain the
1567 // anchoring instead.
1568 if (isNoAutoHide && (popupLevel != ePopupLevelParent ||
1569 mAnchorType == MenuPopupAnchorType_Rect)) {
1570 // Account for the margin that will end up being added to the screen
1571 // coordinate the next time SetPopupPosition is called.
1572 mAnchorType = MenuPopupAnchorType_Point;
1573 mScreenRect.x = screenPoint.x - margin.left;
1574 mScreenRect.y = screenPoint.y - margin.top;
1576 } else {
1577 screenPoint = mScreenRect.TopLeft();
1578 anchorRect = nsRect(screenPoint, nsSize());
1579 if (!mPositionedByMoveToRect) {
1580 mUntransformedAnchorRect = anchorRect;
1583 // Right-align RTL context menus, and apply margin and offsets as per the
1584 // platform conventions.
1585 if (mIsContextMenu && IsDirectionRTL()) {
1586 screenPoint.x -= mRect.Width();
1587 screenPoint.MoveBy(-margin.right, margin.top);
1588 } else {
1589 screenPoint.MoveBy(margin.left, margin.top);
1592 #ifdef XP_MACOSX
1593 // OSX tooltips follow standard flip rule but other popups flip horizontally
1594 // not vertically
1595 if (mPopupType == ePopupTypeTooltip) {
1596 vFlip = FlipStyle_Outside;
1597 } else {
1598 hFlip = FlipStyle_Outside;
1600 #else
1601 // Other OS screen positioned popups can be flipped vertically but never
1602 // horizontally
1603 vFlip = FlipStyle_Outside;
1604 #endif // #ifdef XP_MACOSX
1607 nscoord oldAlignmentOffset = mAlignmentOffset;
1609 // If a panel is being moved or has flip="none", don't constrain or flip it,
1610 // in order to avoid visual noise when moving windows between screens.
1611 // However, if a panel is already constrained or flipped (mIsOffset), then we
1612 // want to continue to calculate this. Also, always do this for content
1613 // shells, so that the popup doesn't extend outside the containing frame.
1614 if (!IS_WAYLAND_DISPLAY() &&
1615 (mInContentShell ||
1616 (mFlip != FlipType_None &&
1617 (!aIsMove || mIsOffset || mPopupType != ePopupTypePanel)))) {
1618 const nsRect screenRect = [&] {
1619 int32_t appPerDev = presContext->AppUnitsPerDevPixel();
1620 auto anchorRectDevPix =
1621 LayoutDeviceIntRect::FromAppUnitsToNearest(anchorRect, appPerDev);
1622 auto rootScreenRectDevPix =
1623 LayoutDeviceIntRect::FromAppUnitsToNearest(rootScreenRect, appPerDev);
1624 auto screenRectDevPix =
1625 GetConstraintRect(anchorRectDevPix, rootScreenRectDevPix, popupLevel);
1626 nsRect sr = LayoutDeviceIntRect::ToAppUnits(screenRectDevPix, appPerDev);
1628 // Expand the allowable screen rect by the input margin (which can't be
1629 // interacted with).
1630 const nscoord inputMargin =
1631 StyleUIReset()->mMozWindowInputRegionMargin.ToAppUnits();
1632 sr.Inflate(inputMargin);
1633 return sr;
1634 }();
1636 // Ensure that anchorRect is on screen.
1637 anchorRect = anchorRect.Intersect(screenRect);
1639 // Shrink the the popup down if it is larger than the screen size
1640 if (mRect.width > screenRect.width) {
1641 mRect.width = screenRect.width;
1643 if (mRect.height > screenRect.height) {
1644 mRect.height = screenRect.height;
1647 // At this point the anchor (anchorRect) is within the available screen
1648 // area (screenRect) and the popup is known to be no larger than the
1649 // screen.
1651 // We might want to "slide" an arrow if the panel is of the correct type -
1652 // but we can only slide on one axis - the other axis must be "flipped or
1653 // resized" as normal.
1654 bool slideHorizontal = false, slideVertical = false;
1655 if (mFlip == FlipType_Slide) {
1656 int8_t position = GetAlignmentPosition();
1657 slideHorizontal = position >= POPUPPOSITION_BEFORESTART &&
1658 position <= POPUPPOSITION_AFTEREND;
1659 slideVertical = position >= POPUPPOSITION_STARTBEFORE &&
1660 position <= POPUPPOSITION_ENDAFTER;
1663 // Next, check if there is enough space to show the popup at full size
1664 // when positioned at screenPoint. If not, flip the popups to the opposite
1665 // side of their anchor point, or resize them as necessary.
1666 const nsPoint preOffsetScreenPoint = screenPoint;
1667 if (slideHorizontal) {
1668 mRect.width = SlideOrResize(screenPoint.x, mRect.width, screenRect.x,
1669 screenRect.XMost(), &mAlignmentOffset);
1670 } else {
1671 bool endAligned = IsDirectionRTL()
1672 ? mPopupAlignment == POPUPALIGNMENT_TOPLEFT ||
1673 mPopupAlignment == POPUPALIGNMENT_BOTTOMLEFT
1674 : mPopupAlignment == POPUPALIGNMENT_TOPRIGHT ||
1675 mPopupAlignment == POPUPALIGNMENT_BOTTOMRIGHT;
1676 mRect.width =
1677 FlipOrResize(screenPoint.x, mRect.width, screenRect.x,
1678 screenRect.XMost(), anchorRect.x, anchorRect.XMost(),
1679 margin.left, margin.right, hFlip, endAligned, &mHFlip);
1681 if (slideVertical) {
1682 mRect.height = SlideOrResize(screenPoint.y, mRect.height, screenRect.y,
1683 screenRect.YMost(), &mAlignmentOffset);
1684 } else {
1685 bool endAligned = mPopupAlignment == POPUPALIGNMENT_BOTTOMLEFT ||
1686 mPopupAlignment == POPUPALIGNMENT_BOTTOMRIGHT;
1687 mRect.height =
1688 FlipOrResize(screenPoint.y, mRect.height, screenRect.y,
1689 screenRect.YMost(), anchorRect.y, anchorRect.YMost(),
1690 margin.top, margin.bottom, vFlip, endAligned, &mVFlip);
1692 mIsOffset = preOffsetScreenPoint != screenPoint;
1694 NS_ASSERTION(screenPoint.x >= screenRect.x, "Popup is offscreen (x start)");
1695 NS_ASSERTION(screenPoint.y >= screenRect.y, "Popup is offscreen (y start)");
1696 NS_ASSERTION(screenPoint.x + mRect.width <= screenRect.XMost(),
1697 "Popup is offscreen (x end)");
1698 NS_ASSERTION(screenPoint.y + mRect.height <= screenRect.YMost(),
1699 "Popup is offscreen (y end)");
1702 // snap the popup's position in screen coordinates to device pixels,
1703 // see bug 622507, bug 961431
1704 screenPoint.x = presContext->RoundAppUnitsToNearestDevPixels(screenPoint.x);
1705 screenPoint.y = presContext->RoundAppUnitsToNearestDevPixels(screenPoint.y);
1707 // determine the x and y position of the view by subtracting the desired
1708 // screen position from the screen position of the root frame.
1709 nsPoint viewPoint = screenPoint - rootScreenRect.TopLeft();
1711 nsView* view = GetView();
1712 NS_ASSERTION(view, "popup with no view");
1714 // Offset the position by the width and height of the borders and titlebar.
1715 // Even though GetClientOffset should return (0, 0) when there is no
1716 // titlebar or borders, we skip these calculations anyway for non-panels
1717 // to save time since they will never have a titlebar.
1718 nsIWidget* widget = view->GetWidget();
1719 if (mPopupType == ePopupTypePanel && widget) {
1720 mLastClientOffset = widget->GetClientOffset();
1721 viewPoint.x += presContext->DevPixelsToAppUnits(mLastClientOffset.x);
1722 viewPoint.y += presContext->DevPixelsToAppUnits(mLastClientOffset.y);
1725 presContext->GetPresShell()->GetViewManager()->MoveViewTo(view, viewPoint.x,
1726 viewPoint.y);
1728 // Now that we've positioned the view, sync up the frame's origin.
1729 nsBoxFrame::SetPosition(viewPoint - GetParent()->GetOffsetTo(rootFrame));
1731 if (aSizedToPopup) {
1732 nsBoxLayoutState state(PresContext());
1733 // XXXndeakin can parentSize.width still extend outside?
1734 SetXULBounds(state, mRect);
1737 // If the popup is in the positioned state or if it is shown and the position
1738 // or size changed, dispatch a popuppositioned event if the popup wants it.
1739 nsIntRect newRect(screenPoint.x, screenPoint.y, mRect.width, mRect.height);
1740 if (mPopupState == ePopupPositioning ||
1741 (mPopupState == ePopupShown && !newRect.IsEqualEdges(mUsedScreenRect)) ||
1742 (mPopupState == ePopupShown && oldAlignmentOffset != mAlignmentOffset)) {
1743 mUsedScreenRect = newRect;
1744 if (!HasAnyStateBits(NS_FRAME_FIRST_REFLOW) && !mPendingPositionedEvent) {
1745 mPendingPositionedEvent =
1746 nsXULPopupPositionedEvent::DispatchIfNeeded(mContent);
1750 // NOTE(emilio): This call below is kind of a workaround, but we need to do
1751 // this here because some position changes don't go through the
1752 // view system -> popup manager, like:
1754 // https://searchfox.org/mozilla-central/rev/477950cf9ca9c9bb5ff6f34e0d0f6ca4718ea798/widget/gtk/nsWindow.cpp#3847
1756 // So this might be the last chance we have to set the remote browser's
1757 // position.
1759 // Ultimately this probably wants to get fixed in the widget size of things,
1760 // but given this is worst-case a redundant DOM traversal, and that popups
1761 // usually don't have all that much content, this is probably an ok
1762 // workaround.
1763 WidgetPositionOrSizeDidChange();
1765 return NS_OK;
1768 void nsMenuPopupFrame::WidgetPositionOrSizeDidChange() {
1769 // In the case this popup has remote contents having OOP iframes, it's
1770 // possible that OOP iframe's nsSubDocumentFrame has been already reflowed
1771 // thus, we will never have a chance to tell this parent browser's position
1772 // update to the OOP documents without notifying it explicitly.
1773 if (!HasRemoteContent()) {
1774 return;
1776 for (nsIContent* content = mContent->GetFirstChild(); content;
1777 content = content->GetNextNode(mContent)) {
1778 if (content->IsXULElement(nsGkAtoms::browser) &&
1779 content->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::remote,
1780 nsGkAtoms::_true, eIgnoreCase)) {
1781 if (auto* browserParent = dom::BrowserParent::GetFrom(content)) {
1782 browserParent->NotifyPositionUpdatedForContentsInPopup();
1788 /* virtual */
1789 nsMenuFrame* nsMenuPopupFrame::GetCurrentMenuItem() { return mCurrentMenu; }
1791 LayoutDeviceIntRect nsMenuPopupFrame::GetConstraintRect(
1792 const LayoutDeviceIntRect& aAnchorRect,
1793 const LayoutDeviceIntRect& aRootScreenRect, nsPopupLevel aPopupLevel) {
1794 LayoutDeviceIntRect screenRectPixels;
1796 // GetConstraintRect() does not work on Wayland as we can't get absolute
1797 // window position there.
1798 MOZ_ASSERT(!IS_WAYLAND_DISPLAY(),
1799 "GetConstraintRect does not work on Wayland");
1801 // determine the available screen space. It will be reduced by the OS chrome
1802 // such as menubars. It addition, for content shells, it will be the area of
1803 // the content rather than the screen.
1804 nsCOMPtr<nsIScreen> screen;
1805 nsCOMPtr<nsIScreenManager> sm(
1806 do_GetService("@mozilla.org/gfx/screenmanager;1"));
1807 if (sm) {
1808 // for content shells, get the screen where the root frame is located.
1809 // This is because we need to constrain the content to this content area,
1810 // so we should use the same screen. Otherwise, use the screen where the
1811 // anchor is located.
1812 DesktopToLayoutDeviceScale scale =
1813 PresContext()->DeviceContext()->GetDesktopToDeviceScale();
1814 DesktopRect rect =
1815 (mInContentShell ? aRootScreenRect : aAnchorRect) / scale;
1816 int32_t width = std::max(1, NSToIntRound(rect.width));
1817 int32_t height = std::max(1, NSToIntRound(rect.height));
1818 sm->ScreenForRect(rect.x, rect.y, width, height, getter_AddRefs(screen));
1819 if (screen) {
1820 // Non-top-level popups (which will always be panels)
1821 // should never overlap the OS bar:
1822 bool dontOverlapOSBar = aPopupLevel != ePopupLevelTop;
1823 // get the total screen area if the popup is allowed to overlap it.
1824 if (!dontOverlapOSBar && mMenuCanOverlapOSBar && !mInContentShell)
1825 screen->GetRect(&screenRectPixels.x, &screenRectPixels.y,
1826 &screenRectPixels.width, &screenRectPixels.height);
1827 else
1828 screen->GetAvailRect(&screenRectPixels.x, &screenRectPixels.y,
1829 &screenRectPixels.width, &screenRectPixels.height);
1833 if (mInContentShell) {
1834 // for content shells, clip to the client area rather than the screen area
1835 screenRectPixels.IntersectRect(screenRectPixels, aRootScreenRect);
1836 } else if (!mOverrideConstraintRect.IsEmpty()) {
1837 LayoutDeviceIntRect overrideConstrainRect =
1838 LayoutDeviceIntRect::FromAppUnitsToNearest(
1839 mOverrideConstraintRect, PresContext()->AppUnitsPerDevPixel());
1840 // This is currently only used for <select> elements where we want to
1841 // constrain vertically to the screen but not horizontally, so do the
1842 // intersection and then reset the horizontal values.
1843 screenRectPixels.IntersectRect(screenRectPixels, overrideConstrainRect);
1844 screenRectPixels.x = overrideConstrainRect.x;
1845 screenRectPixels.width = overrideConstrainRect.width;
1848 return screenRectPixels;
1851 void nsMenuPopupFrame::CanAdjustEdges(Side aHorizontalSide, Side aVerticalSide,
1852 LayoutDeviceIntPoint& aChange) {
1853 int8_t popupAlign(mPopupAlignment);
1854 if (IsDirectionRTL()) {
1855 popupAlign = -popupAlign;
1858 if (aHorizontalSide == (mHFlip ? eSideRight : eSideLeft)) {
1859 if (popupAlign == POPUPALIGNMENT_TOPLEFT ||
1860 popupAlign == POPUPALIGNMENT_BOTTOMLEFT) {
1861 aChange.x = 0;
1863 } else if (aHorizontalSide == (mHFlip ? eSideLeft : eSideRight)) {
1864 if (popupAlign == POPUPALIGNMENT_TOPRIGHT ||
1865 popupAlign == POPUPALIGNMENT_BOTTOMRIGHT) {
1866 aChange.x = 0;
1870 if (aVerticalSide == (mVFlip ? eSideBottom : eSideTop)) {
1871 if (popupAlign == POPUPALIGNMENT_TOPLEFT ||
1872 popupAlign == POPUPALIGNMENT_TOPRIGHT) {
1873 aChange.y = 0;
1875 } else if (aVerticalSide == (mVFlip ? eSideTop : eSideBottom)) {
1876 if (popupAlign == POPUPALIGNMENT_BOTTOMLEFT ||
1877 popupAlign == POPUPALIGNMENT_BOTTOMRIGHT) {
1878 aChange.y = 0;
1883 ConsumeOutsideClicksResult nsMenuPopupFrame::ConsumeOutsideClicks() {
1884 if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
1885 nsGkAtoms::consumeoutsideclicks,
1886 nsGkAtoms::_true, eCaseMatters)) {
1887 return ConsumeOutsideClicks_True;
1889 if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
1890 nsGkAtoms::consumeoutsideclicks,
1891 nsGkAtoms::_false, eCaseMatters)) {
1892 return ConsumeOutsideClicks_ParentOnly;
1894 if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
1895 nsGkAtoms::consumeoutsideclicks,
1896 nsGkAtoms::never, eCaseMatters)) {
1897 return ConsumeOutsideClicks_Never;
1900 nsCOMPtr<nsIContent> parentContent = mContent->GetParent();
1901 if (parentContent) {
1902 dom::NodeInfo* ni = parentContent->NodeInfo();
1903 if (ni->Equals(nsGkAtoms::menulist, kNameSpaceID_XUL)) {
1904 return ConsumeOutsideClicks_True; // Consume outside clicks for combo
1905 // boxes on all platforms
1907 #if defined(XP_WIN)
1908 // Don't consume outside clicks for menus in Windows
1909 if (ni->Equals(nsGkAtoms::menu, kNameSpaceID_XUL) ||
1910 ni->Equals(nsGkAtoms::popupset, kNameSpaceID_XUL) ||
1911 ((ni->Equals(nsGkAtoms::button, kNameSpaceID_XUL) ||
1912 ni->Equals(nsGkAtoms::toolbarbutton, kNameSpaceID_XUL)) &&
1913 parentContent->AsElement()->AttrValueIs(
1914 kNameSpaceID_None, nsGkAtoms::type, nsGkAtoms::menu,
1915 eCaseMatters))) {
1916 return ConsumeOutsideClicks_Never;
1918 #endif
1921 return ConsumeOutsideClicks_True;
1924 // XXXroc this is megalame. Fossicking around for a frame of the right
1925 // type is a recipe for disaster in the long term.
1926 nsIScrollableFrame* nsMenuPopupFrame::GetScrollFrame(nsIFrame* aStart) {
1927 if (!aStart) return nullptr;
1929 // try start frame and siblings
1930 nsIFrame* currFrame = aStart;
1931 do {
1932 nsIScrollableFrame* sf = do_QueryFrame(currFrame);
1933 if (sf) return sf;
1934 currFrame = currFrame->GetNextSibling();
1935 } while (currFrame);
1937 // try children
1938 currFrame = aStart;
1939 do {
1940 nsIFrame* childFrame = currFrame->PrincipalChildList().FirstChild();
1941 nsIScrollableFrame* sf = GetScrollFrame(childFrame);
1942 if (sf) return sf;
1943 currFrame = currFrame->GetNextSibling();
1944 } while (currFrame);
1946 return nullptr;
1949 void nsMenuPopupFrame::EnsureMenuItemIsVisible(nsMenuFrame* aMenuItem) {
1950 if (aMenuItem) {
1951 RefPtr<mozilla::PresShell> presShell = aMenuItem->PresShell();
1952 presShell->ScrollFrameRectIntoView(
1953 aMenuItem, nsRect(nsPoint(0, 0), aMenuItem->GetRect().Size()),
1954 nsMargin(), ScrollAxis(), ScrollAxis(),
1955 ScrollFlags::ScrollOverflowHidden |
1956 ScrollFlags::ScrollFirstAncestorOnly);
1960 void nsMenuPopupFrame::ChangeByPage(bool aIsUp) {
1961 // Only scroll by page within menulists.
1962 if (!IsMenuList()) {
1963 return;
1966 nsMenuFrame* newMenu = nullptr;
1967 nsIFrame* currentMenu = mCurrentMenu;
1968 if (!currentMenu) {
1969 // If there is no current menu item, get the first item. When moving up,
1970 // just use this as the newMenu and leave currentMenu null so that no
1971 // check for a later element is performed. When moving down, set currentMenu
1972 // so that we look for one page down from the first item.
1973 newMenu = nsXULPopupManager::GetNextMenuItem(this, nullptr, true, false);
1974 if (!aIsUp) {
1975 currentMenu = newMenu;
1979 if (currentMenu) {
1980 nscoord scrollHeight = mRect.height;
1981 nsIScrollableFrame* scrollframe = GetScrollFrame(this);
1982 if (scrollframe) {
1983 scrollHeight = scrollframe->GetScrollPortRect().height;
1986 // Get the position of the current item and add or subtract one popup's
1987 // height to or from it.
1988 nscoord targetPosition = aIsUp
1989 ? currentMenu->GetRect().YMost() - scrollHeight
1990 : currentMenu->GetRect().y + scrollHeight;
1992 // Indicates that the last visible child was a valid menuitem.
1993 bool lastWasValid = false;
1995 // Look for the next child which is just past the target position. This
1996 // child will need to be selected.
1997 while (currentMenu) {
1998 // Only consider menu frames.
1999 nsMenuFrame* menuFrame = do_QueryFrame(currentMenu);
2000 if (menuFrame &&
2001 nsXULPopupManager::IsValidMenuItem(menuFrame->GetContent(), true)) {
2002 // If the right position was found, break out. Otherwise, look for
2003 // another item.
2004 if ((!aIsUp && currentMenu->GetRect().YMost() > targetPosition) ||
2005 (aIsUp && currentMenu->GetRect().y < targetPosition)) {
2006 // If the last visible child was not a valid menuitem or was disabled,
2007 // use this as the menu to select, skipping over any non-valid items
2008 // at the edge of the page.
2009 if (!lastWasValid) {
2010 newMenu = menuFrame;
2013 break;
2016 // Assign this item to newMenu. This item will be selected in case we
2017 // don't find any more.
2018 lastWasValid = true;
2019 newMenu = menuFrame;
2020 } else {
2021 lastWasValid = false;
2024 currentMenu =
2025 aIsUp ? currentMenu->GetPrevSibling() : currentMenu->GetNextSibling();
2029 // Select the new menuitem.
2030 if (newMenu) {
2031 ChangeMenuItem(newMenu, false, true);
2035 NS_IMETHODIMP nsMenuPopupFrame::SetCurrentMenuItem(nsMenuFrame* aMenuItem) {
2036 if (mCurrentMenu == aMenuItem) return NS_OK;
2038 if (mCurrentMenu) {
2039 mCurrentMenu->SelectMenu(false);
2042 if (aMenuItem) {
2043 EnsureMenuItemIsVisible(aMenuItem);
2044 aMenuItem->SelectMenu(true);
2047 mCurrentMenu = aMenuItem;
2049 return NS_OK;
2052 void nsMenuPopupFrame::CurrentMenuIsBeingDestroyed() { mCurrentMenu = nullptr; }
2054 NS_IMETHODIMP
2055 nsMenuPopupFrame::ChangeMenuItem(nsMenuFrame* aMenuItem, bool aSelectFirstItem,
2056 bool aFromKey) {
2057 if (mCurrentMenu == aMenuItem) return NS_OK;
2059 // When a context menu is open, the current menu is locked, and no change
2060 // to the menu is allowed.
2061 nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
2062 if (!mIsContextMenu && pm && pm->HasContextMenu(this)) return NS_OK;
2064 // Unset the current child.
2065 if (mCurrentMenu) {
2066 mCurrentMenu->SelectMenu(false);
2067 nsMenuPopupFrame* popup = mCurrentMenu->GetPopup();
2068 if (popup) {
2069 if (mCurrentMenu->IsOpen()) {
2070 if (pm) pm->HidePopupAfterDelay(popup);
2075 // Set the new child.
2076 if (aMenuItem) {
2077 EnsureMenuItemIsVisible(aMenuItem);
2078 aMenuItem->SelectMenu(true);
2080 // On Windows, a menulist should update its value whenever navigation was
2081 // done by the keyboard.
2082 #ifdef XP_WIN
2083 if (aFromKey && IsOpen() && IsMenuList()) {
2084 // Fire a command event as the new item, but we don't want to close
2085 // the menu, blink it, or update any other state of the menuitem. The
2086 // command event will cause the item to be selected.
2087 nsCOMPtr<nsIContent> menuItemContent = aMenuItem->GetContent();
2088 RefPtr<mozilla::PresShell> presShell = PresShell();
2089 nsContentUtils::DispatchXULCommand(menuItemContent, /* aTrusted = */ true,
2090 nullptr, presShell, false, false,
2091 false, false);
2093 #endif
2096 mCurrentMenu = aMenuItem;
2098 return NS_OK;
2101 nsMenuFrame* nsMenuPopupFrame::Enter(WidgetGUIEvent* aEvent) {
2102 mIncrementalString.Truncate();
2104 // Give it to the child.
2105 if (mCurrentMenu) return mCurrentMenu->Enter(aEvent);
2107 return nullptr;
2110 nsMenuFrame* nsMenuPopupFrame::FindMenuWithShortcut(KeyboardEvent* aKeyEvent,
2111 bool& doAction) {
2112 uint32_t charCode = aKeyEvent->CharCode();
2113 uint32_t keyCode = aKeyEvent->KeyCode();
2115 doAction = false;
2117 // Enumerate over our list of frames.
2118 nsContainerFrame* immediateParent =
2119 nsXULPopupManager::ImmediateParentFrame(this);
2120 uint32_t matchCount = 0, matchShortcutCount = 0;
2121 bool foundActive = false;
2122 nsMenuFrame* frameBefore = nullptr;
2123 nsMenuFrame* frameAfter = nullptr;
2124 nsMenuFrame* frameShortcut = nullptr;
2126 nsIContent* parentContent = mContent->GetParent();
2128 bool isMenu = parentContent && !parentContent->NodeInfo()->Equals(
2129 nsGkAtoms::menulist, kNameSpaceID_XUL);
2131 DOMTimeStamp keyTime = aKeyEvent->TimeStamp();
2133 if (charCode == 0) {
2134 if (keyCode == dom::KeyboardEvent_Binding::DOM_VK_BACK_SPACE) {
2135 if (!isMenu && !mIncrementalString.IsEmpty()) {
2136 mIncrementalString.SetLength(mIncrementalString.Length() - 1);
2137 return nullptr;
2139 #ifdef XP_WIN
2140 nsCOMPtr<nsISound> soundInterface = do_GetService("@mozilla.org/sound;1");
2141 if (soundInterface) soundInterface->Beep();
2142 #endif // #ifdef XP_WIN
2144 return nullptr;
2146 char16_t uniChar = ToLowerCase(static_cast<char16_t>(charCode));
2147 if (isMenu) {
2148 // Menu supports only first-letter navigation
2149 mIncrementalString = uniChar;
2150 } else if (IsWithinIncrementalTime(keyTime)) {
2151 mIncrementalString.Append(uniChar);
2152 } else {
2153 // Interval too long, treat as new typing
2154 mIncrementalString = uniChar;
2157 // See bug 188199 & 192346, if all letters in incremental string are same,
2158 // just try to match the first one
2159 nsAutoString incrementalString(mIncrementalString);
2160 uint32_t charIndex = 1, stringLength = incrementalString.Length();
2161 while (charIndex < stringLength &&
2162 incrementalString[charIndex] == incrementalString[charIndex - 1]) {
2163 charIndex++;
2165 if (charIndex == stringLength) {
2166 incrementalString.Truncate(1);
2167 stringLength = 1;
2170 sLastKeyTime = keyTime;
2172 // NOTE: If you crashed here due to a bogus |immediateParent| it is
2173 // possible that the menu whose shortcut is being looked up has
2174 // been destroyed already. One strategy would be to
2175 // setTimeout(<func>,0) as detailed in:
2176 // <http://bugzilla.mozilla.org/show_bug.cgi?id=126675#c32>
2177 nsIFrame* firstMenuItem =
2178 nsXULPopupManager::GetNextMenuItem(immediateParent, nullptr, true, false);
2179 nsIFrame* currFrame = firstMenuItem;
2181 int32_t menuAccessKey = -1;
2182 nsMenuBarListener::GetMenuAccessKey(&menuAccessKey);
2184 // We start searching from first child. This process is divided into two parts
2185 // -- before current and after current -- by the current item
2186 while (currFrame) {
2187 nsIContent* current = currFrame->GetContent();
2188 nsAutoString textKey;
2189 bool isShortcut = false;
2190 if (current->IsElement()) {
2191 if (menuAccessKey >= 0) {
2192 // Get the shortcut attribute.
2193 current->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::accesskey,
2194 textKey);
2196 isShortcut = !textKey.IsEmpty();
2197 if (textKey.IsEmpty()) { // No shortcut, try first letter
2198 current->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::label,
2199 textKey);
2200 if (textKey.IsEmpty()) // No label, try another attribute (value)
2201 current->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::value,
2202 textKey);
2206 if (StringBeginsWith(
2207 nsContentUtils::TrimWhitespace<
2208 nsContentUtils::IsHTMLWhitespaceOrNBSP>(textKey, false),
2209 incrementalString, nsCaseInsensitiveStringComparator)) {
2210 // mIncrementalString is a prefix of textKey
2211 nsMenuFrame* menu = do_QueryFrame(currFrame);
2212 if (menu) {
2213 // There is one match
2214 matchCount++;
2215 if (isShortcut) {
2216 // There is one shortcut-key match
2217 matchShortcutCount++;
2218 // Record the matched item. If there is only one matched shortcut
2219 // item, do it
2220 frameShortcut = menu;
2222 if (!foundActive) {
2223 // It's a first candidate item located before/on the current item
2224 if (!frameBefore) frameBefore = menu;
2225 } else {
2226 // It's a first candidate item located after the current item
2227 if (!frameAfter) frameAfter = menu;
2229 } else
2230 return nullptr;
2233 // Get the active status
2234 if (current->IsElement() && current->AsElement()->AttrValueIs(
2235 kNameSpaceID_None, nsGkAtoms::menuactive,
2236 nsGkAtoms::_true, eCaseMatters)) {
2237 foundActive = true;
2238 if (stringLength > 1) {
2239 // If there is more than one char typed, the current item has highest
2240 // priority,
2241 // otherwise the item next to current has highest priority
2242 if (currFrame == frameBefore) return frameBefore;
2246 nsMenuFrame* menu = do_QueryFrame(currFrame);
2247 currFrame =
2248 nsXULPopupManager::GetNextMenuItem(immediateParent, menu, true, true);
2249 if (currFrame == firstMenuItem) break;
2252 doAction = (isMenu && (matchCount == 1 || matchShortcutCount == 1));
2254 if (matchShortcutCount == 1) // We have one matched shortcut item
2255 return frameShortcut;
2256 if (frameAfter) // If we have matched item after the current, use it
2257 return frameAfter;
2258 else if (frameBefore) // If we haven't, use the item before the current
2259 return frameBefore;
2261 // If we don't match anything, rollback the last typing
2262 mIncrementalString.SetLength(mIncrementalString.Length() - 1);
2264 // didn't find a matching menu item
2265 #ifdef XP_WIN
2266 // behavior on Windows - this item is in a menu popup off of the
2267 // menu bar, so beep and do nothing else
2268 if (isMenu) {
2269 nsCOMPtr<nsISound> soundInterface = do_GetService("@mozilla.org/sound;1");
2270 if (soundInterface) soundInterface->Beep();
2272 #endif // #ifdef XP_WIN
2274 return nullptr;
2277 void nsMenuPopupFrame::LockMenuUntilClosed(bool aLock) {
2278 mIsMenuLocked = aLock;
2280 // Lock / unlock the parent, too.
2281 nsMenuFrame* menu = do_QueryFrame(GetParent());
2282 if (menu) {
2283 nsMenuParent* parentParent = menu->GetMenuParent();
2284 if (parentParent) {
2285 parentParent->LockMenuUntilClosed(aLock);
2290 nsIWidget* nsMenuPopupFrame::GetWidget() const {
2291 return mView ? mView->GetWidget() : nullptr;
2294 // helpers /////////////////////////////////////////////////////////////
2296 nsresult nsMenuPopupFrame::AttributeChanged(int32_t aNameSpaceID,
2297 nsAtom* aAttribute,
2298 int32_t aModType)
2301 nsresult rv =
2302 nsBoxFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType);
2304 if (aAttribute == nsGkAtoms::left || aAttribute == nsGkAtoms::top) {
2305 MoveToAttributePosition();
2308 if (aAttribute == nsGkAtoms::remote) {
2309 // When the remote attribute changes, we need to create a new widget to
2310 // ensure that it has the correct compositor and transparency settings to
2311 // match the new value.
2312 PrepareWidget(true);
2315 if (aAttribute == nsGkAtoms::followanchor) {
2316 nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
2317 if (pm) {
2318 pm->UpdateFollowAnchor(this);
2322 if (aAttribute == nsGkAtoms::label) {
2323 // set the label for the titlebar
2324 nsView* view = GetView();
2325 if (view) {
2326 nsIWidget* widget = view->GetWidget();
2327 if (widget) {
2328 nsAutoString title;
2329 mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::label,
2330 title);
2331 if (!title.IsEmpty()) {
2332 widget->SetTitle(title);
2336 } else if (aAttribute == nsGkAtoms::ignorekeys) {
2337 nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
2338 if (pm) {
2339 nsAutoString ignorekeys;
2340 mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::ignorekeys,
2341 ignorekeys);
2342 pm->UpdateIgnoreKeys(ignorekeys.EqualsLiteral("true"));
2346 return rv;
2349 void nsMenuPopupFrame::MoveToAttributePosition() {
2350 // Move the widget around when the user sets the |left| and |top| attributes.
2351 // Note that this is not the best way to move the widget, as it results in
2352 // lots of FE notifications and is likely to be slow as molasses. Use |moveTo|
2353 // on the element if possible.
2354 nsAutoString left, top;
2355 mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::left, left);
2356 mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::top, top);
2357 nsresult err1, err2;
2358 mozilla::CSSIntPoint pos(left.ToInteger(&err1), top.ToInteger(&err2));
2360 if (NS_SUCCEEDED(err1) && NS_SUCCEEDED(err2)) MoveTo(pos, false);
2362 PresShell()->FrameNeedsReflow(this, IntrinsicDirty::StyleChange,
2363 NS_FRAME_IS_DIRTY);
2366 void nsMenuPopupFrame::DestroyFrom(nsIFrame* aDestructRoot,
2367 PostDestroyData& aPostDestroyData) {
2368 if (mReflowCallbackData.mPosted) {
2369 PresShell()->CancelReflowCallback(this);
2370 mReflowCallbackData.Clear();
2373 nsMenuFrame* menu = do_QueryFrame(GetParent());
2374 if (menu) {
2375 // clear the open attribute on the parent menu
2376 nsContentUtils::AddScriptRunner(new nsUnsetAttrRunnable(
2377 menu->GetContent()->AsElement(), nsGkAtoms::open));
2380 ClearPopupShownDispatcher();
2382 nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
2383 if (pm) pm->PopupDestroyed(this);
2385 nsIPopupContainer* popupContainer =
2386 nsIPopupContainer::GetPopupContainer(PresShell());
2387 if (popupContainer && popupContainer->GetDefaultTooltip() == mContent) {
2388 popupContainer->SetDefaultTooltip(nullptr);
2391 nsBoxFrame::DestroyFrom(aDestructRoot, aPostDestroyData);
2394 nsMargin nsMenuPopupFrame::GetMargin() const {
2395 nsMargin margin;
2396 StyleMargin()->GetMargin(margin);
2397 if (mIsTopLevelContextMenu) {
2398 const CSSIntPoint offset(
2399 LookAndFeel::GetInt(LookAndFeel::IntID::ContextMenuOffsetHorizontal),
2400 LookAndFeel::GetInt(LookAndFeel::IntID::ContextMenuOffsetVertical));
2401 auto auOffset = CSSIntPoint::ToAppUnits(offset);
2402 margin.top += auOffset.y;
2403 margin.bottom += auOffset.y;
2404 margin.left += auOffset.x;
2405 margin.right += auOffset.x;
2407 return margin;
2410 void nsMenuPopupFrame::MoveTo(const CSSPoint& aPos, bool aUpdateAttrs,
2411 bool aByMoveToRect) {
2412 nsIWidget* widget = GetWidget();
2413 nsPoint appUnitsPos = CSSPixel::ToAppUnits(aPos);
2415 // reposition the popup at the specified coordinates. Don't clear the anchor
2416 // and position, because the popup can be reset to its anchor position by
2417 // using (-1, -1) as coordinates.
2419 // Subtract off the margin as it will be added to the position when
2420 // SetPopupPosition is called.
2422 nsMargin margin = GetMargin();
2423 if (mIsContextMenu && IsDirectionRTL()) {
2424 appUnitsPos.x += margin.right + mRect.Width();
2425 } else {
2426 appUnitsPos.x -= margin.left;
2428 appUnitsPos.y -= margin.top;
2431 if ((mScreenRect.x == appUnitsPos.x && mScreenRect.y == appUnitsPos.y) &&
2432 (!widget || widget->GetClientOffset() == mLastClientOffset)) {
2433 return;
2436 mPositionedByMoveToRect = aByMoveToRect;
2437 mScreenRect.MoveTo(appUnitsPos);
2438 if (mAnchorType == MenuPopupAnchorType_Rect) {
2439 // This ensures that the anchor width is still honored, to prevent it from
2440 // changing spuriously.
2441 mScreenRect.height = 0;
2442 } else {
2443 mAnchorType = MenuPopupAnchorType_Point;
2446 SetPopupPosition(nullptr, true, mSizedToPopup);
2448 RefPtr<Element> popup = mContent->AsElement();
2449 if (aUpdateAttrs && (popup->HasAttr(kNameSpaceID_None, nsGkAtoms::left) ||
2450 popup->HasAttr(kNameSpaceID_None, nsGkAtoms::top))) {
2451 nsAutoString left, top;
2452 left.AppendInt(RoundedToInt(aPos).x);
2453 top.AppendInt(RoundedToInt(aPos).y);
2454 popup->SetAttr(kNameSpaceID_None, nsGkAtoms::left, left, false);
2455 popup->SetAttr(kNameSpaceID_None, nsGkAtoms::top, top, false);
2459 void nsMenuPopupFrame::MoveToAnchor(nsIContent* aAnchorContent,
2460 const nsAString& aPosition, int32_t aXPos,
2461 int32_t aYPos, bool aAttributesOverride) {
2462 NS_ASSERTION(IsVisible(), "popup must be visible to move it");
2464 nsPopupState oldstate = mPopupState;
2465 InitializePopup(aAnchorContent, mTriggerContent, aPosition, aXPos, aYPos,
2466 MenuPopupAnchorType_Node, aAttributesOverride);
2467 // InitializePopup changed the state so reset it.
2468 mPopupState = oldstate;
2470 // Pass false here so that flipping and adjusting to fit on the screen happen.
2471 SetPopupPosition(nullptr, false, false);
2474 bool nsMenuPopupFrame::GetAutoPosition() { return mShouldAutoPosition; }
2476 void nsMenuPopupFrame::SetAutoPosition(bool aShouldAutoPosition) {
2477 mShouldAutoPosition = aShouldAutoPosition;
2478 nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
2479 if (pm) {
2480 pm->UpdateFollowAnchor(this);
2484 int8_t nsMenuPopupFrame::GetAlignmentPosition() const {
2485 // The code below handles most cases of alignment, anchor and position values.
2486 // Those that are not handled just return POPUPPOSITION_UNKNOWN.
2488 if (mPosition == POPUPPOSITION_OVERLAP ||
2489 mPosition == POPUPPOSITION_AFTERPOINTER ||
2490 mPosition == POPUPPOSITION_SELECTION) {
2491 return mPosition;
2494 int8_t position = mPosition;
2496 if (position == POPUPPOSITION_UNKNOWN) {
2497 switch (mPopupAnchor) {
2498 case POPUPALIGNMENT_BOTTOMRIGHT:
2499 case POPUPALIGNMENT_BOTTOMLEFT:
2500 case POPUPALIGNMENT_BOTTOMCENTER:
2501 position = mPopupAlignment == POPUPALIGNMENT_TOPRIGHT
2502 ? POPUPPOSITION_AFTEREND
2503 : POPUPPOSITION_AFTERSTART;
2504 break;
2505 case POPUPALIGNMENT_TOPRIGHT:
2506 case POPUPALIGNMENT_TOPLEFT:
2507 case POPUPALIGNMENT_TOPCENTER:
2508 position = mPopupAlignment == POPUPALIGNMENT_BOTTOMRIGHT
2509 ? POPUPPOSITION_BEFOREEND
2510 : POPUPPOSITION_BEFORESTART;
2511 break;
2512 case POPUPALIGNMENT_LEFTCENTER:
2513 position = mPopupAlignment == POPUPALIGNMENT_BOTTOMRIGHT
2514 ? POPUPPOSITION_STARTAFTER
2515 : POPUPPOSITION_STARTBEFORE;
2516 break;
2517 case POPUPALIGNMENT_RIGHTCENTER:
2518 position = mPopupAlignment == POPUPALIGNMENT_BOTTOMLEFT
2519 ? POPUPPOSITION_ENDAFTER
2520 : POPUPPOSITION_ENDBEFORE;
2521 break;
2522 default:
2523 break;
2527 if (mHFlip) {
2528 position = POPUPPOSITION_HFLIP(position);
2531 if (mVFlip) {
2532 position = POPUPPOSITION_VFLIP(position);
2535 return position;
2539 * KEEP THIS IN SYNC WITH nsIFrame::CreateView
2540 * as much as possible. Until we get rid of views finally...
2542 void nsMenuPopupFrame::CreatePopupView() {
2543 if (HasView()) {
2544 return;
2547 nsViewManager* viewManager = PresContext()->GetPresShell()->GetViewManager();
2548 NS_ASSERTION(nullptr != viewManager, "null view manager");
2550 // Create a view
2551 nsView* parentView = viewManager->GetRootView();
2552 nsViewVisibility visibility = nsViewVisibility_kHide;
2554 NS_ASSERTION(parentView, "no parent view");
2556 // Create a view
2557 nsView* view = viewManager->CreateView(GetRect(), parentView, visibility);
2558 auto zIndex = ZIndex();
2559 viewManager->SetViewZIndex(view, zIndex.isNothing(), zIndex.valueOr(0));
2560 // XXX put view last in document order until we can do better
2561 viewManager->InsertChild(parentView, view, nullptr, true);
2563 // Remember our view
2564 SetView(view);
2566 NS_FRAME_LOG(
2567 NS_FRAME_TRACE_CALLS,
2568 ("nsMenuPopupFrame::CreatePopupView: frame=%p view=%p", this, view));
2571 bool nsMenuPopupFrame::ShouldFollowAnchor() {
2572 if (!mShouldAutoPosition || mAnchorType != MenuPopupAnchorType_Node ||
2573 !mAnchorContent) {
2574 return false;
2577 // Follow anchor mode is used when followanchor="true" is set or for arrow
2578 // panels.
2579 if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
2580 nsGkAtoms::followanchor,
2581 nsGkAtoms::_true, eCaseMatters)) {
2582 return true;
2585 if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
2586 nsGkAtoms::followanchor,
2587 nsGkAtoms::_false, eCaseMatters)) {
2588 return false;
2591 return (mPopupType == ePopupTypePanel &&
2592 mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
2593 nsGkAtoms::arrow, eCaseMatters));
2596 bool nsMenuPopupFrame::ShouldFollowAnchor(nsRect& aRect) {
2597 if (!ShouldFollowAnchor()) {
2598 return false;
2601 nsIFrame* anchorFrame = mAnchorContent->GetPrimaryFrame();
2602 if (anchorFrame) {
2603 nsPresContext* rootPresContext = PresContext()->GetRootPresContext();
2604 if (rootPresContext) {
2605 aRect = ComputeAnchorRect(rootPresContext, anchorFrame);
2609 return true;
2612 void nsMenuPopupFrame::CheckForAnchorChange(nsRect& aRect) {
2613 // Don't update if the popup isn't visible or we shouldn't be following the
2614 // anchor.
2615 if (!IsVisible() || !ShouldFollowAnchor()) {
2616 return;
2619 bool shouldHide = false;
2621 nsPresContext* rootPresContext = PresContext()->GetRootPresContext();
2623 // If the frame for the anchor has gone away, hide the popup.
2624 nsIFrame* anchor = mAnchorContent->GetPrimaryFrame();
2625 if (!anchor || !rootPresContext) {
2626 shouldHide = true;
2627 } else if (!anchor->IsVisibleConsideringAncestors(
2628 VISIBILITY_CROSS_CHROME_CONTENT_BOUNDARY)) {
2629 // If the anchor is now inside something that is invisible, hide the popup.
2630 shouldHide = true;
2631 } else {
2632 // If the anchor is now inside a hidden parent popup, hide the popup.
2633 nsIFrame* frame = anchor;
2634 while (frame) {
2635 nsMenuPopupFrame* popup = do_QueryFrame(frame);
2636 if (popup && popup->PopupState() != ePopupShown) {
2637 shouldHide = true;
2638 break;
2641 frame = frame->GetParent();
2645 if (shouldHide) {
2646 nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
2647 if (pm) {
2648 // As the caller will be iterating over the open popups, hide
2649 // asyncronously.
2650 pm->HidePopup(mContent, false, true, true, false);
2653 return;
2656 nsRect anchorRect = ComputeAnchorRect(rootPresContext, anchor);
2658 // If the rectangles are different, move the popup.
2659 if (!anchorRect.IsEqualEdges(aRect)) {
2660 aRect = anchorRect;
2661 SetPopupPosition(nullptr, true, false);
2665 nsIWidget* nsMenuPopupFrame::GetParentMenuWidget() {
2666 nsMenuFrame* menuFrame = do_QueryFrame(GetParent());
2667 if (menuFrame) {
2668 nsMenuParent* parentPopup = menuFrame->GetMenuParent();
2669 if (parentPopup && (parentPopup->IsMenu() || parentPopup->IsMenuBar())) {
2670 return static_cast<nsMenuPopupFrame*>(parentPopup)->GetWidget();
2673 return nullptr;