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