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