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 "nsGkAtoms.h"
8 #include "nsXULPopupManager.h"
9 #include "nsMenuFrame.h"
10 #include "nsMenuPopupFrame.h"
11 #include "nsMenuBarFrame.h"
12 #include "nsMenuBarListener.h"
13 #include "nsContentUtils.h"
14 #include "nsIDOMDocument.h"
15 #include "nsIDOMEvent.h"
16 #include "nsXULElement.h"
17 #include "nsIDOMXULMenuListElement.h"
18 #include "nsIDOMXULCommandDispatcher.h"
19 #include "nsCSSFrameConstructor.h"
20 #include "nsGlobalWindow.h"
21 #include "nsLayoutUtils.h"
22 #include "nsViewManager.h"
23 #include "nsIComponentManager.h"
24 #include "nsITimer.h"
25 #include "nsFocusManager.h"
26 #include "nsIDocShell.h"
27 #include "nsPIDOMWindow.h"
28 #include "nsIInterfaceRequestorUtils.h"
29 #include "nsIBaseWindow.h"
30 #include "nsIDOMMouseEvent.h"
31 #include "nsCaret.h"
32 #include "nsIDocument.h"
33 #include "nsPIWindowRoot.h"
34 #include "nsFrameManager.h"
35 #include "nsIObserverService.h"
36 #include "XULDocument.h"
37 #include "mozilla/dom/Element.h"
38 #include "mozilla/dom/Event.h"  // for nsIDOMEvent::InternalDOMEvent()
39 #include "mozilla/dom/KeyboardEvent.h"
40 #include "mozilla/dom/KeyboardEventBinding.h"
41 #include "mozilla/dom/UIEvent.h"
42 #include "mozilla/EventDispatcher.h"
43 #include "mozilla/EventStateManager.h"
44 #include "mozilla/LookAndFeel.h"
45 #include "mozilla/MouseEvents.h"
46 #include "mozilla/Services.h"
47 #include "mozilla/widget/nsAutoRollup.h"
48 
49 using namespace mozilla;
50 using namespace mozilla::dom;
51 
52 static_assert(KeyboardEventBinding::DOM_VK_HOME ==
53                       KeyboardEventBinding::DOM_VK_END + 1 &&
54                   KeyboardEventBinding::DOM_VK_LEFT ==
55                       KeyboardEventBinding::DOM_VK_END + 2 &&
56                   KeyboardEventBinding::DOM_VK_UP ==
57                       KeyboardEventBinding::DOM_VK_END + 3 &&
58                   KeyboardEventBinding::DOM_VK_RIGHT ==
59                       KeyboardEventBinding::DOM_VK_END + 4 &&
60                   KeyboardEventBinding::DOM_VK_DOWN ==
61                       KeyboardEventBinding::DOM_VK_END + 5,
62               "nsXULPopupManager assumes some keyCode values are consecutive");
63 
64 const nsNavigationDirection DirectionFromKeyCodeTable[2][6] = {
65     {
66         eNavigationDirection_Last,    // KeyboardEventBinding::DOM_VK_END
67         eNavigationDirection_First,   // KeyboardEventBinding::DOM_VK_HOME
68         eNavigationDirection_Start,   // KeyboardEventBinding::DOM_VK_LEFT
69         eNavigationDirection_Before,  // KeyboardEventBinding::DOM_VK_UP
70         eNavigationDirection_End,     // KeyboardEventBinding::DOM_VK_RIGHT
71         eNavigationDirection_After    // KeyboardEventBinding::DOM_VK_DOWN
72     },
73     {
74         eNavigationDirection_Last,    // KeyboardEventBinding::DOM_VK_END
75         eNavigationDirection_First,   // KeyboardEventBinding::DOM_VK_HOME
76         eNavigationDirection_End,     // KeyboardEventBinding::DOM_VK_LEFT
77         eNavigationDirection_Before,  // KeyboardEventBinding::DOM_VK_UP
78         eNavigationDirection_Start,   // KeyboardEventBinding::DOM_VK_RIGHT
79         eNavigationDirection_After    // KeyboardEventBinding::DOM_VK_DOWN
80     }};
81 
82 nsXULPopupManager* nsXULPopupManager::sInstance = nullptr;
83 
Content()84 nsIContent* nsMenuChainItem::Content() { return mFrame->GetContent(); }
85 
SetParent(nsMenuChainItem * aParent)86 void nsMenuChainItem::SetParent(nsMenuChainItem* aParent) {
87   if (mParent) {
88     NS_ASSERTION(mParent->mChild == this,
89                  "Unexpected - parent's child not set to this");
90     mParent->mChild = nullptr;
91   }
92   mParent = aParent;
93   if (mParent) {
94     if (mParent->mChild) mParent->mChild->mParent = nullptr;
95     mParent->mChild = this;
96   }
97 }
98 
Detach(nsMenuChainItem ** aRoot)99 void nsMenuChainItem::Detach(nsMenuChainItem** aRoot) {
100   // If the item has a child, set the child's parent to this item's parent,
101   // effectively removing the item from the chain. If the item has no child,
102   // just set the parent to null.
103   if (mChild) {
104     NS_ASSERTION(this != *aRoot,
105                  "Unexpected - popup with child at end of chain");
106     mChild->SetParent(mParent);
107   } else {
108     // An item without a child should be the first item in the chain, so set
109     // the first item pointer, pointed to by aRoot, to the parent.
110     NS_ASSERTION(this == *aRoot,
111                  "Unexpected - popup with no child not at end of chain");
112     *aRoot = mParent;
113     SetParent(nullptr);
114   }
115 }
116 
UpdateFollowAnchor()117 void nsMenuChainItem::UpdateFollowAnchor() {
118   mFollowAnchor = mFrame->ShouldFollowAnchor(mCurrentRect);
119 }
120 
CheckForAnchorChange()121 void nsMenuChainItem::CheckForAnchorChange() {
122   if (mFollowAnchor) {
123     mFrame->CheckForAnchorChange(mCurrentRect);
124   }
125 }
126 
127 bool nsXULPopupManager::sDevtoolsDisableAutoHide = false;
128 
129 const char* kPrefDevtoolsDisableAutoHide = "ui.popup.disable_autohide";
130 
NS_IMPL_ISUPPORTS(nsXULPopupManager,nsIDOMEventListener,nsIObserver)131 NS_IMPL_ISUPPORTS(nsXULPopupManager, nsIDOMEventListener, nsIObserver)
132 
133 nsXULPopupManager::nsXULPopupManager()
134     : mRangeOffset(0),
135       mCachedMousePoint(0, 0),
136       mCachedModifiers(0),
137       mActiveMenuBar(nullptr),
138       mPopups(nullptr),
139       mTimerMenu(nullptr) {
140   nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
141   if (obs) {
142     obs->AddObserver(this, "xpcom-shutdown", false);
143   }
144   Preferences::AddBoolVarCache(&sDevtoolsDisableAutoHide,
145                                kPrefDevtoolsDisableAutoHide, false);
146 }
147 
~nsXULPopupManager()148 nsXULPopupManager::~nsXULPopupManager() {
149   NS_ASSERTION(!mPopups, "XUL popups still open");
150 }
151 
Init()152 nsresult nsXULPopupManager::Init() {
153   sInstance = new nsXULPopupManager();
154   NS_ENSURE_TRUE(sInstance, NS_ERROR_OUT_OF_MEMORY);
155   NS_ADDREF(sInstance);
156   return NS_OK;
157 }
158 
Shutdown()159 void nsXULPopupManager::Shutdown() { NS_IF_RELEASE(sInstance); }
160 
161 NS_IMETHODIMP
Observe(nsISupports * aSubject,const char * aTopic,const char16_t * aData)162 nsXULPopupManager::Observe(nsISupports* aSubject, const char* aTopic,
163                            const char16_t* aData) {
164   if (!nsCRT::strcmp(aTopic, "xpcom-shutdown")) {
165     if (mKeyListener) {
166       mKeyListener->RemoveEventListener(NS_LITERAL_STRING("keypress"), this,
167                                         true);
168       mKeyListener->RemoveEventListener(NS_LITERAL_STRING("keydown"), this,
169                                         true);
170       mKeyListener->RemoveEventListener(NS_LITERAL_STRING("keyup"), this, true);
171       mKeyListener = nullptr;
172     }
173     mRangeParent = nullptr;
174     // mOpeningPopup is cleared explicitly soon after using it.
175     nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
176     if (obs) {
177       obs->RemoveObserver(this, "xpcom-shutdown");
178     }
179   }
180 
181   return NS_OK;
182 }
183 
GetInstance()184 nsXULPopupManager* nsXULPopupManager::GetInstance() {
185   MOZ_ASSERT(sInstance);
186   return sInstance;
187 }
188 
Rollup(uint32_t aCount,bool aFlush,const nsIntPoint * pos,nsIContent ** aLastRolledUp)189 bool nsXULPopupManager::Rollup(uint32_t aCount, bool aFlush,
190                                const nsIntPoint* pos,
191                                nsIContent** aLastRolledUp) {
192   if (aLastRolledUp) {
193     *aLastRolledUp = nullptr;
194   }
195 
196   // We can disable the autohide behavior via a pref to ease debugging.
197   if (nsXULPopupManager::sDevtoolsDisableAutoHide) {
198     // Required on linux to allow events to work on other targets.
199     if (mWidget) {
200       mWidget->CaptureRollupEvents(nullptr, false);
201     }
202     return false;
203   }
204 
205   bool consume = false;
206 
207   nsMenuChainItem* item = GetTopVisibleMenu();
208   if (item) {
209     if (aLastRolledUp) {
210       // We need to get the popup that will be closed last, so that widget can
211       // keep track of it so it doesn't reopen if a mousedown event is going to
212       // processed. Keep going up the menu chain to get the first level menu of
213       // the same type. If a different type is encountered it means we have,
214       // for example, a menulist or context menu inside a panel, and we want to
215       // treat these as distinct. It's possible that this menu doesn't end up
216       // closing because the popuphiding event was cancelled, but in that case
217       // we don't need to deal with the menu reopening as it will already still
218       // be open.
219       nsMenuChainItem* first = item;
220       while (first->GetParent()) {
221         nsMenuChainItem* parent = first->GetParent();
222         if (first->Frame()->PopupType() != parent->Frame()->PopupType() ||
223             first->IsContextMenu() != parent->IsContextMenu()) {
224           break;
225         }
226         first = parent;
227       }
228 
229       *aLastRolledUp = first->Content();
230     }
231 
232     ConsumeOutsideClicksResult consumeResult =
233         item->Frame()->ConsumeOutsideClicks();
234     consume = (consumeResult == ConsumeOutsideClicks_True);
235 
236     bool rollup = true;
237 
238     // If norolluponanchor is true, then don't rollup when clicking the anchor.
239     // This would be used to allow adjusting the caret position in an
240     // autocomplete field without hiding the popup for example.
241     bool noRollupOnAnchor =
242         (!consume && pos &&
243          item->Frame()->GetContent()->AsElement()->AttrValueIs(
244              kNameSpaceID_None, nsGkAtoms::norolluponanchor, nsGkAtoms::_true,
245              eCaseMatters));
246 
247     // When ConsumeOutsideClicks_ParentOnly is used, always consume the click
248     // when the click was over the anchor. This way, clicking on a menu doesn't
249     // reopen the menu.
250     if ((consumeResult == ConsumeOutsideClicks_ParentOnly ||
251          noRollupOnAnchor) &&
252         pos) {
253       nsMenuPopupFrame* popupFrame = item->Frame();
254       CSSIntRect anchorRect;
255       if (popupFrame->IsAnchored()) {
256         // Check if the popup has a screen anchor rectangle. If not, get the
257         // rectangle from the anchor element.
258         anchorRect =
259             CSSIntRect::FromUnknownRect(popupFrame->GetScreenAnchorRect());
260         if (anchorRect.x == -1 || anchorRect.y == -1) {
261           nsCOMPtr<nsIContent> anchor = popupFrame->GetAnchor();
262 
263           // Check if the anchor has indicated another node to use for checking
264           // for roll-up. That way, we can anchor a popup on anonymous content
265           // or an individual icon, while clicking elsewhere within a button or
266           // other container doesn't result in us re-opening the popup.
267           if (anchor && anchor->IsElement()) {
268             nsAutoString consumeAnchor;
269             anchor->AsElement()->GetAttr(
270                 kNameSpaceID_None, nsGkAtoms::consumeanchor, consumeAnchor);
271             if (!consumeAnchor.IsEmpty()) {
272               nsIDocument* doc = anchor->GetOwnerDocument();
273               nsIContent* newAnchor = doc->GetElementById(consumeAnchor);
274               if (newAnchor) {
275                 anchor = newAnchor;
276               }
277             }
278           }
279 
280           if (anchor && anchor->GetPrimaryFrame()) {
281             anchorRect = anchor->GetPrimaryFrame()->GetScreenRect();
282           }
283         }
284       }
285 
286       // It's possible that some other element is above the anchor at the same
287       // position, but the only thing that would happen is that the mouse
288       // event will get consumed, so here only a quick coordinates check is
289       // done rather than a slower complete check of what is at that location.
290       nsPresContext* presContext = item->Frame()->PresContext();
291       CSSIntPoint posCSSPixels(presContext->DevPixelsToIntCSSPixels(pos->x),
292                                presContext->DevPixelsToIntCSSPixels(pos->y));
293       if (anchorRect.Contains(posCSSPixels)) {
294         if (consumeResult == ConsumeOutsideClicks_ParentOnly) {
295           consume = true;
296         }
297 
298         if (noRollupOnAnchor) {
299           rollup = false;
300         }
301       }
302     }
303 
304     if (rollup) {
305       // if a number of popups to close has been specified, determine the last
306       // popup to close
307       nsIContent* lastPopup = nullptr;
308       if (aCount != UINT32_MAX) {
309         nsMenuChainItem* last = item;
310         while (--aCount && last->GetParent()) {
311           last = last->GetParent();
312         }
313         if (last) {
314           lastPopup = last->Content();
315         }
316       }
317 
318       nsPresContext* presContext = item->Frame()->PresContext();
319       RefPtr<nsViewManager> viewManager =
320           presContext->PresShell()->GetViewManager();
321 
322       HidePopup(item->Content(), true, true, false, true, lastPopup);
323 
324       if (aFlush) {
325         // The popup's visibility doesn't update until the minimize animation
326         // has finished, so call UpdateWidgetGeometry to update it right away.
327         viewManager->UpdateWidgetGeometry();
328       }
329     }
330   }
331 
332   return consume;
333 }
334 
335 ////////////////////////////////////////////////////////////////////////
ShouldRollupOnMouseWheelEvent()336 bool nsXULPopupManager::ShouldRollupOnMouseWheelEvent() {
337   // should rollup only for autocomplete widgets
338   // XXXndeakin this should really be something the popup has more control over
339 
340   nsMenuChainItem* item = GetTopVisibleMenu();
341   if (!item) return false;
342 
343   nsIContent* content = item->Frame()->GetContent();
344   if (!content || !content->IsElement()) return false;
345 
346   Element* element = content->AsElement();
347   if (element->AttrValueIs(kNameSpaceID_None, nsGkAtoms::rolluponmousewheel,
348                            nsGkAtoms::_true, eCaseMatters))
349     return true;
350 
351   if (element->AttrValueIs(kNameSpaceID_None, nsGkAtoms::rolluponmousewheel,
352                            nsGkAtoms::_false, eCaseMatters))
353     return false;
354 
355   nsAutoString value;
356   element->GetAttr(kNameSpaceID_None, nsGkAtoms::type, value);
357   return StringBeginsWith(value, NS_LITERAL_STRING("autocomplete"));
358 }
359 
ShouldConsumeOnMouseWheelEvent()360 bool nsXULPopupManager::ShouldConsumeOnMouseWheelEvent() {
361   nsMenuChainItem* item = GetTopVisibleMenu();
362   if (!item) return false;
363 
364   nsMenuPopupFrame* frame = item->Frame();
365   if (frame->PopupType() != ePopupTypePanel) return true;
366 
367   return !frame->GetContent()->AsElement()->AttrValueIs(
368       kNameSpaceID_None, nsGkAtoms::type, nsGkAtoms::arrow, eCaseMatters);
369 }
370 
371 // a menu should not roll up if activated by a mouse activate message (eg.
372 // X-mouse)
ShouldRollupOnMouseActivate()373 bool nsXULPopupManager::ShouldRollupOnMouseActivate() { return false; }
374 
GetSubmenuWidgetChain(nsTArray<nsIWidget * > * aWidgetChain)375 uint32_t nsXULPopupManager::GetSubmenuWidgetChain(
376     nsTArray<nsIWidget*>* aWidgetChain) {
377   // this method is used by the widget code to determine the list of popups
378   // that are open. If a mouse click occurs outside one of these popups, the
379   // panels will roll up. If the click is inside a popup, they will not roll up
380   uint32_t count = 0, sameTypeCount = 0;
381 
382   NS_ASSERTION(aWidgetChain, "null parameter");
383   nsMenuChainItem* item = GetTopVisibleMenu();
384   while (item) {
385     nsMenuChainItem* parent = item->GetParent();
386     if (!item->IsNoAutoHide()) {
387       nsCOMPtr<nsIWidget> widget = item->Frame()->GetWidget();
388       NS_ASSERTION(widget, "open popup has no widget");
389       aWidgetChain->AppendElement(widget.get());
390       // In the case when a menulist inside a panel is open, clicking in the
391       // panel should still roll up the menu, so if a different type is found,
392       // stop scanning.
393       if (!sameTypeCount) {
394         count++;
395         if (!parent ||
396             item->Frame()->PopupType() != parent->Frame()->PopupType() ||
397             item->IsContextMenu() != parent->IsContextMenu()) {
398           sameTypeCount = count;
399         }
400       }
401     }
402 
403     item = parent;
404   }
405 
406   return sameTypeCount;
407 }
408 
GetRollupWidget()409 nsIWidget* nsXULPopupManager::GetRollupWidget() {
410   nsMenuChainItem* item = GetTopVisibleMenu();
411   return item ? item->Frame()->GetWidget() : nullptr;
412 }
413 
AdjustPopupsOnWindowChange(nsPIDOMWindowOuter * aWindow)414 void nsXULPopupManager::AdjustPopupsOnWindowChange(
415     nsPIDOMWindowOuter* aWindow) {
416   // When the parent window is moved, adjust any child popups. Dismissable
417   // menus and panels are expected to roll up when a window is moved, so there
418   // is no need to check these popups, only the noautohide popups.
419 
420   // The items are added to a list so that they can be adjusted bottom to top.
421   nsTArray<nsMenuPopupFrame*> list;
422 
423   nsMenuChainItem* item = mPopups;
424   while (item) {
425     // only move popups that are within the same window and where auto
426     // positioning has not been disabled
427     nsMenuPopupFrame* frame = item->Frame();
428     if (item->IsNoAutoHide() && frame->GetAutoPosition()) {
429       nsIContent* popup = frame->GetContent();
430       if (popup) {
431         nsIDocument* document = popup->GetUncomposedDoc();
432         if (document) {
433           if (nsPIDOMWindowOuter* window = document->GetWindow()) {
434             window = window->GetPrivateRoot();
435             if (window == aWindow) {
436               list.AppendElement(frame);
437             }
438           }
439         }
440       }
441     }
442 
443     item = item->GetParent();
444   }
445 
446   for (int32_t l = list.Length() - 1; l >= 0; l--) {
447     list[l]->SetPopupPosition(nullptr, true, false, true);
448   }
449 }
450 
AdjustPopupsOnWindowChange(nsIPresShell * aPresShell)451 void nsXULPopupManager::AdjustPopupsOnWindowChange(nsIPresShell* aPresShell) {
452   if (aPresShell->GetDocument()) {
453     AdjustPopupsOnWindowChange(aPresShell->GetDocument()->GetWindow());
454   }
455 }
456 
GetPopupToMoveOrResize(nsIFrame * aFrame)457 static nsMenuPopupFrame* GetPopupToMoveOrResize(nsIFrame* aFrame) {
458   nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(aFrame);
459   if (!menuPopupFrame) return nullptr;
460 
461   // no point moving or resizing hidden popups
462   if (!menuPopupFrame->IsVisible()) return nullptr;
463 
464   nsIWidget* widget = menuPopupFrame->GetWidget();
465   if (widget && !widget->IsVisible()) return nullptr;
466 
467   return menuPopupFrame;
468 }
469 
PopupMoved(nsIFrame * aFrame,nsIntPoint aPnt)470 void nsXULPopupManager::PopupMoved(nsIFrame* aFrame, nsIntPoint aPnt) {
471   nsMenuPopupFrame* menuPopupFrame = GetPopupToMoveOrResize(aFrame);
472   if (!menuPopupFrame) return;
473 
474   nsView* view = menuPopupFrame->GetView();
475   if (!view) return;
476 
477   // Don't do anything if the popup is already at the specified location. This
478   // prevents recursive calls when a popup is positioned.
479   LayoutDeviceIntRect curDevSize = view->CalcWidgetBounds(eWindowType_popup);
480   nsIWidget* widget = menuPopupFrame->GetWidget();
481   if (curDevSize.x == aPnt.x && curDevSize.y == aPnt.y &&
482       (!widget ||
483        widget->GetClientOffset() == menuPopupFrame->GetLastClientOffset())) {
484     return;
485   }
486 
487   // Update the popup's position using SetPopupPosition if the popup is
488   // anchored and at the parent level as these maintain their position
489   // relative to the parent window. Otherwise, just update the popup to
490   // the specified screen coordinates.
491   if (menuPopupFrame->IsAnchored() &&
492       menuPopupFrame->PopupLevel() == ePopupLevelParent) {
493     menuPopupFrame->SetPopupPosition(nullptr, true, false, true);
494   } else {
495     CSSPoint cssPos = LayoutDeviceIntPoint::FromUnknownPoint(aPnt) /
496                       menuPopupFrame->PresContext()->CSSToDevPixelScale();
497     menuPopupFrame->MoveTo(RoundedToInt(cssPos), false);
498   }
499 }
500 
PopupResized(nsIFrame * aFrame,LayoutDeviceIntSize aSize)501 void nsXULPopupManager::PopupResized(nsIFrame* aFrame,
502                                      LayoutDeviceIntSize aSize) {
503   nsMenuPopupFrame* menuPopupFrame = GetPopupToMoveOrResize(aFrame);
504   if (!menuPopupFrame) return;
505 
506   nsView* view = menuPopupFrame->GetView();
507   if (!view) return;
508 
509   LayoutDeviceIntRect curDevSize = view->CalcWidgetBounds(eWindowType_popup);
510   // If the size is what we think it is, we have nothing to do.
511   if (curDevSize.width == aSize.width && curDevSize.height == aSize.height)
512     return;
513 
514   Element* popup = menuPopupFrame->GetContent()->AsElement();
515 
516   // Only set the width and height if the popup already has these attributes.
517   if (!popup->HasAttr(kNameSpaceID_None, nsGkAtoms::width) ||
518       !popup->HasAttr(kNameSpaceID_None, nsGkAtoms::height)) {
519     return;
520   }
521 
522   // The size is different. Convert the actual size to css pixels and store it
523   // as 'width' and 'height' attributes on the popup.
524   nsPresContext* presContext = menuPopupFrame->PresContext();
525 
526   CSSIntSize newCSS(presContext->DevPixelsToIntCSSPixels(aSize.width),
527                     presContext->DevPixelsToIntCSSPixels(aSize.height));
528 
529   nsAutoString width, height;
530   width.AppendInt(newCSS.width);
531   height.AppendInt(newCSS.height);
532   popup->SetAttr(kNameSpaceID_None, nsGkAtoms::width, width, false);
533   popup->SetAttr(kNameSpaceID_None, nsGkAtoms::height, height, true);
534 }
535 
GetPopupFrameForContent(nsIContent * aContent,bool aShouldFlush)536 nsMenuPopupFrame* nsXULPopupManager::GetPopupFrameForContent(
537     nsIContent* aContent, bool aShouldFlush) {
538   if (aShouldFlush) {
539     nsIDocument* document = aContent->GetUncomposedDoc();
540     if (document) {
541       nsCOMPtr<nsIPresShell> presShell = document->GetShell();
542       if (presShell) presShell->FlushPendingNotifications(FlushType::Layout);
543     }
544   }
545 
546   return do_QueryFrame(aContent->GetPrimaryFrame());
547 }
548 
GetTopVisibleMenu()549 nsMenuChainItem* nsXULPopupManager::GetTopVisibleMenu() {
550   nsMenuChainItem* item = mPopups;
551   while (item) {
552     if (!item->IsNoAutoHide() &&
553         item->Frame()->PopupState() != ePopupInvisible) {
554       return item;
555     }
556     item = item->GetParent();
557   }
558 
559   return nullptr;
560 }
561 
GetMouseLocationParent()562 nsINode* nsXULPopupManager::GetMouseLocationParent() { return mRangeParent; }
563 
MouseLocationOffset()564 int32_t nsXULPopupManager::MouseLocationOffset() { return mRangeOffset; }
565 
InitTriggerEvent(nsIDOMEvent * aEvent,nsIContent * aPopup,nsIContent ** aTriggerContent)566 void nsXULPopupManager::InitTriggerEvent(nsIDOMEvent* aEvent,
567                                          nsIContent* aPopup,
568                                          nsIContent** aTriggerContent) {
569   mCachedMousePoint = LayoutDeviceIntPoint(0, 0);
570 
571   if (aTriggerContent) {
572     *aTriggerContent = nullptr;
573     if (aEvent) {
574       // get the trigger content from the event
575       nsCOMPtr<nsIContent> target =
576           do_QueryInterface(aEvent->InternalDOMEvent()->GetTarget());
577       target.forget(aTriggerContent);
578     }
579   }
580 
581   mCachedModifiers = 0;
582 
583   nsCOMPtr<nsIDOMUIEvent> domUiEvent = do_QueryInterface(aEvent);
584   if (domUiEvent) {
585     auto uiEvent = static_cast<UIEvent*>(domUiEvent.get());
586     mRangeParent = uiEvent->GetRangeParent();
587     mRangeOffset = uiEvent->RangeOffset();
588 
589     // get the event coordinates relative to the root frame of the document
590     // containing the popup.
591     NS_ASSERTION(aPopup, "Expected a popup node");
592     WidgetEvent* event = aEvent->WidgetEventPtr();
593     if (event) {
594       WidgetInputEvent* inputEvent = event->AsInputEvent();
595       if (inputEvent) {
596         mCachedModifiers = inputEvent->mModifiers;
597       }
598       nsIDocument* doc = aPopup->GetUncomposedDoc();
599       if (doc) {
600         nsIPresShell* presShell = doc->GetShell();
601         nsPresContext* presContext;
602         if (presShell && (presContext = presShell->GetPresContext())) {
603           nsPresContext* rootDocPresContext = presContext->GetRootPresContext();
604           if (!rootDocPresContext) return;
605           nsIFrame* rootDocumentRootFrame =
606               rootDocPresContext->PresShell()->GetRootFrame();
607           if ((event->mClass == eMouseEventClass ||
608                event->mClass == eMouseScrollEventClass ||
609                event->mClass == eWheelEventClass) &&
610               !event->AsGUIEvent()->mWidget) {
611             // no widget, so just use the client point if available
612             nsCOMPtr<nsIDOMMouseEvent> mouseEvent = do_QueryInterface(aEvent);
613             nsIntPoint clientPt;
614             mouseEvent->GetClientX(&clientPt.x);
615             mouseEvent->GetClientY(&clientPt.y);
616 
617             // XXX this doesn't handle IFRAMEs in transforms
618             nsPoint thisDocToRootDocOffset =
619                 presShell->GetRootFrame()->GetOffsetToCrossDoc(
620                     rootDocumentRootFrame);
621             // convert to device pixels
622             mCachedMousePoint.x = presContext->AppUnitsToDevPixels(
623                 nsPresContext::CSSPixelsToAppUnits(clientPt.x) +
624                 thisDocToRootDocOffset.x);
625             mCachedMousePoint.y = presContext->AppUnitsToDevPixels(
626                 nsPresContext::CSSPixelsToAppUnits(clientPt.y) +
627                 thisDocToRootDocOffset.y);
628           } else if (rootDocumentRootFrame) {
629             nsPoint pnt = nsLayoutUtils::GetEventCoordinatesRelativeTo(
630                 event, rootDocumentRootFrame);
631             mCachedMousePoint = LayoutDeviceIntPoint(
632                 rootDocPresContext->AppUnitsToDevPixels(pnt.x),
633                 rootDocPresContext->AppUnitsToDevPixels(pnt.y));
634           }
635         }
636       }
637     }
638   } else {
639     mRangeParent = nullptr;
640     mRangeOffset = 0;
641   }
642 }
643 
SetActiveMenuBar(nsMenuBarFrame * aMenuBar,bool aActivate)644 void nsXULPopupManager::SetActiveMenuBar(nsMenuBarFrame* aMenuBar,
645                                          bool aActivate) {
646   if (aActivate)
647     mActiveMenuBar = aMenuBar;
648   else if (mActiveMenuBar == aMenuBar)
649     mActiveMenuBar = nullptr;
650 
651   UpdateKeyboardListeners();
652 }
653 
ShowMenu(nsIContent * aMenu,bool aSelectFirstItem,bool aAsynchronous)654 void nsXULPopupManager::ShowMenu(nsIContent* aMenu, bool aSelectFirstItem,
655                                  bool aAsynchronous) {
656   nsMenuFrame* menuFrame = do_QueryFrame(aMenu->GetPrimaryFrame());
657   if (!menuFrame || !menuFrame->IsMenu()) return;
658 
659   nsMenuPopupFrame* popupFrame = menuFrame->GetPopup();
660   if (!popupFrame || !MayShowPopup(popupFrame)) return;
661 
662   // inherit whether or not we're a context menu from the parent
663   bool parentIsContextMenu = false;
664   bool onMenuBar = false;
665   bool onmenu = menuFrame->IsOnMenu();
666 
667   nsMenuParent* parent = menuFrame->GetMenuParent();
668   if (parent && onmenu) {
669     parentIsContextMenu = parent->IsContextMenu();
670     onMenuBar = parent->IsMenuBar();
671   }
672 
673   nsAutoString position;
674 
675 #ifdef XP_MACOSX
676   nsCOMPtr<nsIDOMXULMenuListElement> menulist = do_QueryInterface(aMenu);
677   bool isNonEditableMenulist = false;
678   if (menulist) {
679     bool editable;
680     menulist->GetEditable(&editable);
681     isNonEditableMenulist = !editable;
682   }
683 
684   if (isNonEditableMenulist) {
685     position.AssignLiteral("selection");
686   } else
687 #endif
688 
689       if (onMenuBar || !onmenu)
690     position.AssignLiteral("after_start");
691   else
692     position.AssignLiteral("end_before");
693 
694   // there is no trigger event for menus
695   InitTriggerEvent(nullptr, nullptr, nullptr);
696   popupFrame->InitializePopup(menuFrame->GetAnchor(), nullptr, position, 0, 0,
697                               MenuPopupAnchorType_Node, true);
698 
699   if (aAsynchronous) {
700     nsCOMPtr<nsIRunnable> event = new nsXULPopupShowingEvent(
701         popupFrame->GetContent(), parentIsContextMenu, aSelectFirstItem);
702     aMenu->OwnerDoc()->Dispatch(TaskCategory::Other, event.forget());
703   } else {
704     nsCOMPtr<nsIContent> popupContent = popupFrame->GetContent();
705     FirePopupShowingEvent(popupContent, parentIsContextMenu, aSelectFirstItem,
706                           nullptr);
707   }
708 }
709 
ShowPopup(nsIContent * aPopup,nsIContent * aAnchorContent,const nsAString & aPosition,int32_t aXPos,int32_t aYPos,bool aIsContextMenu,bool aAttributesOverride,bool aSelectFirstItem,nsIDOMEvent * aTriggerEvent)710 void nsXULPopupManager::ShowPopup(
711     nsIContent* aPopup, nsIContent* aAnchorContent, const nsAString& aPosition,
712     int32_t aXPos, int32_t aYPos, bool aIsContextMenu, bool aAttributesOverride,
713     bool aSelectFirstItem, nsIDOMEvent* aTriggerEvent) {
714   nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(aPopup, true);
715   if (!popupFrame || !MayShowPopup(popupFrame)) return;
716 
717   nsCOMPtr<nsIContent> triggerContent;
718   InitTriggerEvent(aTriggerEvent, aPopup, getter_AddRefs(triggerContent));
719 
720   popupFrame->InitializePopup(aAnchorContent, triggerContent, aPosition, aXPos,
721                               aYPos, MenuPopupAnchorType_Node,
722                               aAttributesOverride);
723 
724   FirePopupShowingEvent(aPopup, aIsContextMenu, aSelectFirstItem,
725                         aTriggerEvent);
726 }
727 
ShowPopupAtScreen(nsIContent * aPopup,int32_t aXPos,int32_t aYPos,bool aIsContextMenu,nsIDOMEvent * aTriggerEvent)728 void nsXULPopupManager::ShowPopupAtScreen(nsIContent* aPopup, int32_t aXPos,
729                                           int32_t aYPos, bool aIsContextMenu,
730                                           nsIDOMEvent* aTriggerEvent) {
731   nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(aPopup, true);
732   if (!popupFrame || !MayShowPopup(popupFrame)) return;
733 
734   nsCOMPtr<nsIContent> triggerContent;
735   InitTriggerEvent(aTriggerEvent, aPopup, getter_AddRefs(triggerContent));
736 
737   popupFrame->InitializePopupAtScreen(triggerContent, aXPos, aYPos,
738                                       aIsContextMenu);
739   FirePopupShowingEvent(aPopup, aIsContextMenu, false, aTriggerEvent);
740 }
741 
ShowPopupAtScreenRect(nsIContent * aPopup,const nsAString & aPosition,const nsIntRect & aRect,bool aIsContextMenu,bool aAttributesOverride,nsIDOMEvent * aTriggerEvent)742 void nsXULPopupManager::ShowPopupAtScreenRect(
743     nsIContent* aPopup, const nsAString& aPosition, const nsIntRect& aRect,
744     bool aIsContextMenu, bool aAttributesOverride, nsIDOMEvent* aTriggerEvent) {
745   nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(aPopup, true);
746   if (!popupFrame || !MayShowPopup(popupFrame)) return;
747 
748   nsCOMPtr<nsIContent> triggerContent;
749   InitTriggerEvent(aTriggerEvent, aPopup, getter_AddRefs(triggerContent));
750 
751   popupFrame->InitializePopupAtRect(triggerContent, aPosition, aRect,
752                                     aAttributesOverride);
753 
754   FirePopupShowingEvent(aPopup, aIsContextMenu, false, aTriggerEvent);
755 }
756 
ShowTooltipAtScreen(nsIContent * aPopup,nsIContent * aTriggerContent,int32_t aXPos,int32_t aYPos)757 void nsXULPopupManager::ShowTooltipAtScreen(nsIContent* aPopup,
758                                             nsIContent* aTriggerContent,
759                                             int32_t aXPos, int32_t aYPos) {
760   nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(aPopup, true);
761   if (!popupFrame || !MayShowPopup(popupFrame)) return;
762 
763   InitTriggerEvent(nullptr, nullptr, nullptr);
764 
765   nsPresContext* pc = popupFrame->PresContext();
766   mCachedMousePoint = LayoutDeviceIntPoint(pc->CSSPixelsToDevPixels(aXPos),
767                                            pc->CSSPixelsToDevPixels(aYPos));
768 
769   // coordinates are relative to the root widget
770   nsPresContext* rootPresContext = pc->GetRootPresContext();
771   if (rootPresContext) {
772     nsIWidget* rootWidget = rootPresContext->GetRootWidget();
773     if (rootWidget) {
774       mCachedMousePoint -= rootWidget->WidgetToScreenOffset();
775     }
776   }
777 
778   popupFrame->InitializePopupAtScreen(aTriggerContent, aXPos, aYPos, false);
779 
780   FirePopupShowingEvent(aPopup, false, false, nullptr);
781 }
782 
ShowPopupWithAnchorAlign(nsIContent * aPopup,nsIContent * aAnchorContent,nsAString & aAnchor,nsAString & aAlign,int32_t aXPos,int32_t aYPos,bool aIsContextMenu)783 void nsXULPopupManager::ShowPopupWithAnchorAlign(
784     nsIContent* aPopup, nsIContent* aAnchorContent, nsAString& aAnchor,
785     nsAString& aAlign, int32_t aXPos, int32_t aYPos, bool aIsContextMenu) {
786   nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(aPopup, true);
787   if (!popupFrame || !MayShowPopup(popupFrame)) return;
788 
789   InitTriggerEvent(nullptr, nullptr, nullptr);
790 
791   popupFrame->InitializePopupWithAnchorAlign(aAnchorContent, aAnchor, aAlign,
792                                              aXPos, aYPos);
793   FirePopupShowingEvent(aPopup, aIsContextMenu, false, nullptr);
794 }
795 
CheckCaretDrawingState()796 static void CheckCaretDrawingState() {
797   // There is 1 caret per document, we need to find the focused
798   // document and erase its caret.
799   nsIFocusManager* fm = nsFocusManager::GetFocusManager();
800   if (fm) {
801     nsCOMPtr<mozIDOMWindowProxy> window;
802     fm->GetFocusedWindow(getter_AddRefs(window));
803     if (!window) return;
804 
805     auto* piWindow = nsPIDOMWindowOuter::From(window);
806     MOZ_ASSERT(piWindow);
807 
808     nsCOMPtr<nsIDocument> focusedDoc = piWindow->GetDoc();
809     if (!focusedDoc) return;
810 
811     nsIPresShell* presShell = focusedDoc->GetShell();
812     if (!presShell) return;
813 
814     RefPtr<nsCaret> caret = presShell->GetCaret();
815     if (!caret) return;
816     caret->SchedulePaint();
817   }
818 }
819 
ShowPopupCallback(nsIContent * aPopup,nsMenuPopupFrame * aPopupFrame,bool aIsContextMenu,bool aSelectFirstItem)820 void nsXULPopupManager::ShowPopupCallback(nsIContent* aPopup,
821                                           nsMenuPopupFrame* aPopupFrame,
822                                           bool aIsContextMenu,
823                                           bool aSelectFirstItem) {
824   nsPopupType popupType = aPopupFrame->PopupType();
825   bool ismenu = (popupType == ePopupTypeMenu);
826 
827   // Popups normally hide when an outside click occurs. Panels may use
828   // the noautohide attribute to disable this behaviour. It is expected
829   // that the application will hide these popups manually. The tooltip
830   // listener will handle closing the tooltip also.
831   bool isNoAutoHide =
832       aPopupFrame->IsNoAutoHide() || popupType == ePopupTypeTooltip;
833 
834   nsMenuChainItem* item =
835       new nsMenuChainItem(aPopupFrame, isNoAutoHide, aIsContextMenu, popupType);
836   if (!item) return;
837 
838   // install keyboard event listeners for navigating menus. For panels, the
839   // escape key may be used to close the panel. However, the ignorekeys
840   // attribute may be used to disable adding these event listeners for popups
841   // that want to handle their own keyboard events.
842   nsAutoString ignorekeys;
843   if (aPopup->IsElement()) {
844     aPopup->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::ignorekeys,
845                                  ignorekeys);
846   }
847   if (ignorekeys.EqualsLiteral("true")) {
848     item->SetIgnoreKeys(eIgnoreKeys_True);
849   } else if (ignorekeys.EqualsLiteral("shortcuts")) {
850     item->SetIgnoreKeys(eIgnoreKeys_Shortcuts);
851   }
852 
853   if (ismenu) {
854     // if the menu is on a menubar, use the menubar's listener instead
855     nsMenuFrame* menuFrame = do_QueryFrame(aPopupFrame->GetParent());
856     if (menuFrame) {
857       item->SetOnMenuBar(menuFrame->IsOnMenuBar());
858     }
859   }
860 
861   // use a weak frame as the popup will set an open attribute if it is a menu
862   AutoWeakFrame weakFrame(aPopupFrame);
863   aPopupFrame->ShowPopup(aIsContextMenu);
864   ENSURE_TRUE(weakFrame.IsAlive());
865 
866   // popups normally hide when an outside click occurs. Panels may use
867   // the noautohide attribute to disable this behaviour. It is expected
868   // that the application will hide these popups manually. The tooltip
869   // listener will handle closing the tooltip also.
870   nsIContent* oldmenu = nullptr;
871   if (mPopups) {
872     oldmenu = mPopups->Content();
873   }
874   item->SetParent(mPopups);
875   mPopups = item;
876   SetCaptureState(oldmenu);
877   NS_ENSURE_TRUE_VOID(weakFrame.IsAlive());
878 
879   item->UpdateFollowAnchor();
880 
881   if (aSelectFirstItem) {
882     nsMenuFrame* next = GetNextMenuItem(aPopupFrame, nullptr, true, false);
883     aPopupFrame->SetCurrentMenuItem(next);
884   }
885 
886   if (ismenu) UpdateMenuItems(aPopup);
887 
888   // Caret visibility may have been affected, ensure that
889   // the caret isn't now drawn when it shouldn't be.
890   CheckCaretDrawingState();
891 }
892 
HidePopup(nsIContent * aPopup,bool aHideChain,bool aDeselectMenu,bool aAsynchronous,bool aIsCancel,nsIContent * aLastPopup)893 void nsXULPopupManager::HidePopup(nsIContent* aPopup, bool aHideChain,
894                                   bool aDeselectMenu, bool aAsynchronous,
895                                   bool aIsCancel, nsIContent* aLastPopup) {
896   nsMenuPopupFrame* popupFrame = do_QueryFrame(aPopup->GetPrimaryFrame());
897   if (!popupFrame) {
898     return;
899   }
900 
901   nsMenuChainItem* foundPopup = mPopups;
902   while (foundPopup) {
903     if (foundPopup->Content() == aPopup) {
904       break;
905     }
906     foundPopup = foundPopup->GetParent();
907   }
908 
909   bool deselectMenu = false;
910   nsCOMPtr<nsIContent> popupToHide, nextPopup, lastPopup;
911 
912   if (foundPopup) {
913     if (foundPopup->IsNoAutoHide()) {
914       // If this is a noautohide panel, remove it but don't close any other
915       // panels.
916       popupToHide = aPopup;
917     } else {
918       // At this point, foundPopup will be set to the found item in the list. If
919       // foundPopup is the topmost menu, the one to remove, then there are no
920       // other popups to hide. If foundPopup is not the topmost menu, then there
921       // may be open submenus below it. In this case, we need to make sure that
922       // those submenus are closed up first. To do this, we scan up the menu
923       // list to find the topmost popup with only menus between it and
924       // foundPopup and close that menu first. In synchronous mode, the
925       // FirePopupHidingEvent method will be called which in turn calls
926       // HidePopupCallback to close up the next popup in the chain. These two
927       // methods will be called in sequence recursively to close up all the
928       // necessary popups. In asynchronous mode, a similar process occurs except
929       // that the FirePopupHidingEvent method is called asynchronously. In
930       // either case, nextPopup is set to the content node of the next popup to
931       // close, and lastPopup is set to the last popup in the chain to close,
932       // which will be aPopup, or null to close up all menus.
933 
934       nsMenuChainItem* topMenu = foundPopup;
935       // Use IsMenu to ensure that foundPopup is a menu and scan down the child
936       // list until a non-menu is found. If foundPopup isn't a menu at all,
937       // don't scan and just close up this menu.
938       if (foundPopup->IsMenu()) {
939         nsMenuChainItem* child = foundPopup->GetChild();
940         while (child && child->IsMenu()) {
941           topMenu = child;
942           child = child->GetChild();
943         }
944       }
945 
946       deselectMenu = aDeselectMenu;
947       popupToHide = topMenu->Content();
948       popupFrame = topMenu->Frame();
949 
950       // Close up another popup if there is one, and we are either hiding the
951       // entire chain or the item to hide isn't the topmost popup.
952       nsMenuChainItem* parent = topMenu->GetParent();
953       if (parent && (aHideChain || topMenu != foundPopup)) {
954         while (parent && parent->IsNoAutoHide()) {
955           parent = parent->GetParent();
956         }
957 
958         if (parent) {
959           nextPopup = parent->Content();
960         }
961       }
962 
963       lastPopup = aLastPopup ? aLastPopup : (aHideChain ? nullptr : aPopup);
964     }
965   } else if (popupFrame->PopupState() == ePopupPositioning) {
966     // When the popup is in the popuppositioning state, it will not be in the
967     // mPopups list. We need another way to find it and make sure it does not
968     // continue the popup showing process.
969     deselectMenu = aDeselectMenu;
970     popupToHide = aPopup;
971   }
972 
973   if (popupToHide) {
974     nsPopupState state = popupFrame->PopupState();
975     // If the popup is already being hidden, don't attempt to hide it again
976     if (state == ePopupHiding) {
977       return;
978     }
979 
980     // Change the popup state to hiding. Don't set the hiding state if the
981     // popup is invisible, otherwise nsMenuPopupFrame::HidePopup will
982     // run again. In the invisible state, we just want the events to fire.
983     if (state != ePopupInvisible) {
984       popupFrame->SetPopupState(ePopupHiding);
985     }
986 
987     // For menus, popupToHide is always the frontmost item in the list to hide.
988     if (aAsynchronous) {
989       nsCOMPtr<nsIRunnable> event = new nsXULPopupHidingEvent(
990           popupToHide, nextPopup, lastPopup, popupFrame->PopupType(),
991           deselectMenu, aIsCancel);
992       aPopup->OwnerDoc()->Dispatch(TaskCategory::Other, event.forget());
993     } else {
994       FirePopupHidingEvent(popupToHide, nextPopup, lastPopup,
995                            popupFrame->PresContext(), popupFrame->PopupType(),
996                            deselectMenu, aIsCancel);
997     }
998   }
999 }
1000 
1001 // This is used to hide the popup after a transition finishes.
1002 class TransitionEnder : public nsIDOMEventListener {
1003  protected:
~TransitionEnder()1004   virtual ~TransitionEnder() {}
1005 
1006  public:
1007   nsCOMPtr<nsIContent> mContent;
1008   bool mDeselectMenu;
1009 
1010   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
NS_DECL_CYCLE_COLLECTION_CLASS(TransitionEnder)1011   NS_DECL_CYCLE_COLLECTION_CLASS(TransitionEnder)
1012 
1013   TransitionEnder(nsIContent* aContent, bool aDeselectMenu)
1014       : mContent(aContent), mDeselectMenu(aDeselectMenu) {}
1015 
HandleEvent(nsIDOMEvent * aEvent)1016   NS_IMETHOD HandleEvent(nsIDOMEvent* aEvent) override {
1017     mContent->RemoveSystemEventListener(NS_LITERAL_STRING("transitionend"),
1018                                         this, false);
1019 
1020     nsMenuPopupFrame* popupFrame = do_QueryFrame(mContent->GetPrimaryFrame());
1021 
1022     // Now hide the popup. There could be other properties transitioning, but
1023     // we'll assume they all end at the same time and just hide the popup upon
1024     // the first one ending.
1025     nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
1026     if (pm && popupFrame) {
1027       pm->HidePopupCallback(mContent, popupFrame, nullptr, nullptr,
1028                             popupFrame->PopupType(), mDeselectMenu);
1029     }
1030 
1031     return NS_OK;
1032   }
1033 };
1034 
1035 NS_IMPL_CYCLE_COLLECTING_ADDREF(TransitionEnder)
1036 NS_IMPL_CYCLE_COLLECTING_RELEASE(TransitionEnder)
1037 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TransitionEnder)
1038   NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
1039   NS_INTERFACE_MAP_ENTRY(nsISupports)
1040 NS_INTERFACE_MAP_END
1041 
1042 NS_IMPL_CYCLE_COLLECTION(TransitionEnder, mContent);
1043 
HidePopupCallback(nsIContent * aPopup,nsMenuPopupFrame * aPopupFrame,nsIContent * aNextPopup,nsIContent * aLastPopup,nsPopupType aPopupType,bool aDeselectMenu)1044 void nsXULPopupManager::HidePopupCallback(
1045     nsIContent* aPopup, nsMenuPopupFrame* aPopupFrame, nsIContent* aNextPopup,
1046     nsIContent* aLastPopup, nsPopupType aPopupType, bool aDeselectMenu) {
1047   if (mCloseTimer && mTimerMenu == aPopupFrame) {
1048     mCloseTimer->Cancel();
1049     mCloseTimer = nullptr;
1050     mTimerMenu = nullptr;
1051   }
1052 
1053   // The popup to hide is aPopup. Search the list again to find the item that
1054   // corresponds to the popup to hide aPopup. This is done because it's
1055   // possible someone added another item (attempted to open another popup)
1056   // or removed a popup frame during the event processing so the item isn't at
1057   // the front anymore.
1058   nsMenuChainItem* item = mPopups;
1059   while (item) {
1060     if (item->Content() == aPopup) {
1061       item->Detach(&mPopups);
1062       SetCaptureState(aPopup);
1063       break;
1064     }
1065     item = item->GetParent();
1066   }
1067 
1068   delete item;
1069 
1070   AutoWeakFrame weakFrame(aPopupFrame);
1071   aPopupFrame->HidePopup(aDeselectMenu, ePopupClosed);
1072   ENSURE_TRUE(weakFrame.IsAlive());
1073 
1074   // send the popuphidden event synchronously. This event has no default
1075   // behaviour.
1076   nsEventStatus status = nsEventStatus_eIgnore;
1077   WidgetMouseEvent event(true, eXULPopupHidden, nullptr,
1078                          WidgetMouseEvent::eReal);
1079   EventDispatcher::Dispatch(aPopup, aPopupFrame->PresContext(), &event, nullptr,
1080                             &status);
1081   ENSURE_TRUE(weakFrame.IsAlive());
1082 
1083   // Force any popups that might be anchored on elements within this popup to
1084   // update.
1085   UpdatePopupPositions(aPopupFrame->PresContext()->RefreshDriver());
1086 
1087   // if there are more popups to close, look for the next one
1088   if (aNextPopup && aPopup != aLastPopup) {
1089     nsMenuChainItem* foundMenu = nullptr;
1090     nsMenuChainItem* item = mPopups;
1091     while (item) {
1092       if (item->Content() == aNextPopup) {
1093         foundMenu = item;
1094         break;
1095       }
1096       item = item->GetParent();
1097     }
1098 
1099     // continue hiding the chain of popups until the last popup aLastPopup
1100     // is reached, or until a popup of a different type is reached. This
1101     // last check is needed so that a menulist inside a non-menu panel only
1102     // closes the menu and not the panel as well.
1103     if (foundMenu && (aLastPopup || aPopupType == foundMenu->PopupType())) {
1104       nsCOMPtr<nsIContent> popupToHide = item->Content();
1105       nsMenuChainItem* parent = item->GetParent();
1106 
1107       nsCOMPtr<nsIContent> nextPopup;
1108       if (parent && popupToHide != aLastPopup) nextPopup = parent->Content();
1109 
1110       nsMenuPopupFrame* popupFrame = item->Frame();
1111       nsPopupState state = popupFrame->PopupState();
1112       if (state == ePopupHiding) return;
1113       if (state != ePopupInvisible) popupFrame->SetPopupState(ePopupHiding);
1114 
1115       FirePopupHidingEvent(popupToHide, nextPopup, aLastPopup,
1116                            popupFrame->PresContext(), foundMenu->PopupType(),
1117                            aDeselectMenu, false);
1118     }
1119   }
1120 }
1121 
HidePopupAfterDelay(nsMenuPopupFrame * aPopup)1122 void nsXULPopupManager::HidePopupAfterDelay(nsMenuPopupFrame* aPopup) {
1123   // Don't close up immediately.
1124   // Kick off a close timer.
1125   KillMenuTimer();
1126 
1127   int32_t menuDelay =
1128       LookAndFeel::GetInt(LookAndFeel::eIntID_SubmenuDelay, 300);  // ms
1129 
1130   // Kick off the timer.
1131   nsIEventTarget* target = nullptr;
1132   if (nsIContent* content = aPopup->GetContent()) {
1133     target = content->OwnerDoc()->EventTargetFor(TaskCategory::Other);
1134   }
1135   NS_NewTimerWithFuncCallback(
1136       getter_AddRefs(mCloseTimer),
1137       [](nsITimer* aTimer, void* aClosure) {
1138         nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
1139         if (pm) {
1140           pm->KillMenuTimer();
1141         }
1142       },
1143       nullptr, menuDelay, nsITimer::TYPE_ONE_SHOT, "KillMenuTimer", target);
1144 
1145   // the popup will call PopupDestroyed if it is destroyed, which checks if it
1146   // is set to mTimerMenu, so it should be safe to keep a reference to it
1147   mTimerMenu = aPopup;
1148 }
1149 
HidePopupsInList(const nsTArray<nsMenuPopupFrame * > & aFrames)1150 void nsXULPopupManager::HidePopupsInList(
1151     const nsTArray<nsMenuPopupFrame*>& aFrames) {
1152   // Create a weak frame list. This is done in a separate array with the
1153   // right capacity predetermined to avoid multiple allocations.
1154   nsTArray<WeakFrame> weakPopups(aFrames.Length());
1155   uint32_t f;
1156   for (f = 0; f < aFrames.Length(); f++) {
1157     WeakFrame* wframe = weakPopups.AppendElement();
1158     if (wframe) *wframe = aFrames[f];
1159   }
1160 
1161   for (f = 0; f < weakPopups.Length(); f++) {
1162     // check to ensure that the frame is still alive before hiding it.
1163     if (weakPopups[f].IsAlive()) {
1164       nsMenuPopupFrame* frame =
1165           static_cast<nsMenuPopupFrame*>(weakPopups[f].GetFrame());
1166       frame->HidePopup(true, ePopupInvisible);
1167     }
1168   }
1169 
1170   SetCaptureState(nullptr);
1171 }
1172 
EnableRollup(nsIContent * aPopup,bool aShouldRollup)1173 void nsXULPopupManager::EnableRollup(nsIContent* aPopup, bool aShouldRollup) {
1174 #ifndef MOZ_GTK
1175   nsMenuChainItem* item = mPopups;
1176   while (item) {
1177     if (item->Content() == aPopup) {
1178       nsIContent* oldmenu = nullptr;
1179       if (mPopups) {
1180         oldmenu = mPopups->Content();
1181       }
1182 
1183       item->SetNoAutoHide(!aShouldRollup);
1184       SetCaptureState(oldmenu);
1185       return;
1186     }
1187     item = item->GetParent();
1188   }
1189 #endif
1190 }
1191 
IsChildOfDocShell(nsIDocument * aDoc,nsIDocShellTreeItem * aExpected)1192 bool nsXULPopupManager::IsChildOfDocShell(nsIDocument* aDoc,
1193                                           nsIDocShellTreeItem* aExpected) {
1194   nsCOMPtr<nsIDocShellTreeItem> docShellItem(aDoc->GetDocShell());
1195   while (docShellItem) {
1196     if (docShellItem == aExpected) return true;
1197 
1198     nsCOMPtr<nsIDocShellTreeItem> parent;
1199     docShellItem->GetParent(getter_AddRefs(parent));
1200     docShellItem = parent;
1201   }
1202 
1203   return false;
1204 }
1205 
HidePopupsInDocShell(nsIDocShellTreeItem * aDocShellToHide)1206 void nsXULPopupManager::HidePopupsInDocShell(
1207     nsIDocShellTreeItem* aDocShellToHide) {
1208   nsTArray<nsMenuPopupFrame*> popupsToHide;
1209 
1210   // iterate to get the set of popup frames to hide
1211   nsMenuChainItem* item = mPopups;
1212   while (item) {
1213     nsMenuChainItem* parent = item->GetParent();
1214     if (item->Frame()->PopupState() != ePopupInvisible &&
1215         IsChildOfDocShell(item->Content()->OwnerDoc(), aDocShellToHide)) {
1216       nsMenuPopupFrame* frame = item->Frame();
1217       item->Detach(&mPopups);
1218       delete item;
1219       popupsToHide.AppendElement(frame);
1220     }
1221     item = parent;
1222   }
1223 
1224   HidePopupsInList(popupsToHide);
1225 }
1226 
UpdatePopupPositions(nsRefreshDriver * aRefreshDriver)1227 void nsXULPopupManager::UpdatePopupPositions(nsRefreshDriver* aRefreshDriver) {
1228   nsMenuChainItem* item = mPopups;
1229   while (item) {
1230     if (item->Frame()->PresContext()->RefreshDriver() == aRefreshDriver) {
1231       item->CheckForAnchorChange();
1232     }
1233 
1234     item = item->GetParent();
1235   }
1236 }
1237 
UpdateFollowAnchor(nsMenuPopupFrame * aPopup)1238 void nsXULPopupManager::UpdateFollowAnchor(nsMenuPopupFrame* aPopup) {
1239   nsMenuChainItem* item = mPopups;
1240   while (item) {
1241     if (item->Frame() == aPopup) {
1242       item->UpdateFollowAnchor();
1243       break;
1244     }
1245 
1246     item = item->GetParent();
1247   }
1248 }
1249 
ExecuteMenu(nsIContent * aMenu,nsXULMenuCommandEvent * aEvent)1250 void nsXULPopupManager::ExecuteMenu(nsIContent* aMenu,
1251                                     nsXULMenuCommandEvent* aEvent) {
1252   CloseMenuMode cmm = CloseMenuMode_Auto;
1253 
1254   static Element::AttrValuesArray strings[] = {&nsGkAtoms::none,
1255                                                &nsGkAtoms::single, nullptr};
1256 
1257   if (aMenu->IsElement()) {
1258     switch (aMenu->AsElement()->FindAttrValueIn(
1259         kNameSpaceID_None, nsGkAtoms::closemenu, strings, eCaseMatters)) {
1260       case 0:
1261         cmm = CloseMenuMode_None;
1262         break;
1263       case 1:
1264         cmm = CloseMenuMode_Single;
1265         break;
1266       default:
1267         break;
1268     }
1269   }
1270 
1271   // When a menuitem is selected to be executed, first hide all the open
1272   // popups, but don't remove them yet. This is needed when a menu command
1273   // opens a modal dialog. The views associated with the popups needed to be
1274   // hidden and the accesibility events fired before the command executes, but
1275   // the popuphiding/popuphidden events are fired afterwards.
1276   nsTArray<nsMenuPopupFrame*> popupsToHide;
1277   nsMenuChainItem* item = GetTopVisibleMenu();
1278   if (cmm != CloseMenuMode_None) {
1279     while (item) {
1280       // if it isn't a <menupopup>, don't close it automatically
1281       if (!item->IsMenu()) break;
1282       nsMenuChainItem* next = item->GetParent();
1283       popupsToHide.AppendElement(item->Frame());
1284       if (cmm == CloseMenuMode_Single)  // only close one level of menu
1285         break;
1286       item = next;
1287     }
1288 
1289     // Now hide the popups. If the closemenu mode is auto, deselect the menu,
1290     // otherwise only one popup is closing, so keep the parent menu selected.
1291     HidePopupsInList(popupsToHide);
1292   }
1293 
1294   aEvent->SetCloseMenuMode(cmm);
1295   nsCOMPtr<nsIRunnable> event = aEvent;
1296   aMenu->OwnerDoc()->Dispatch(TaskCategory::Other, event.forget());
1297 }
1298 
FirePopupShowingEvent(nsIContent * aPopup,bool aIsContextMenu,bool aSelectFirstItem,nsIDOMEvent * aTriggerEvent)1299 void nsXULPopupManager::FirePopupShowingEvent(nsIContent* aPopup,
1300                                               bool aIsContextMenu,
1301                                               bool aSelectFirstItem,
1302                                               nsIDOMEvent* aTriggerEvent) {
1303   nsCOMPtr<nsIContent> popup = aPopup;  // keep a strong reference to the popup
1304 
1305   nsMenuPopupFrame* popupFrame = do_QueryFrame(aPopup->GetPrimaryFrame());
1306   if (!popupFrame) return;
1307 
1308   nsPresContext* presContext = popupFrame->PresContext();
1309   nsCOMPtr<nsIPresShell> presShell = presContext->PresShell();
1310   nsPopupType popupType = popupFrame->PopupType();
1311 
1312   // generate the child frames if they have not already been generated
1313   if (!popupFrame->HasGeneratedChildren()) {
1314     popupFrame->SetGeneratedChildren();
1315     presShell->FrameConstructor()->GenerateChildFrames(popupFrame);
1316   }
1317 
1318   // get the frame again
1319   nsIFrame* frame = aPopup->GetPrimaryFrame();
1320   if (!frame) return;
1321 
1322   presShell->FrameNeedsReflow(frame, nsIPresShell::eTreeChange,
1323                               NS_FRAME_HAS_DIRTY_CHILDREN);
1324 
1325   // cache the popup so that document.popupNode can retrieve the trigger node
1326   // during the popupshowing event. It will be cleared below after the event
1327   // has fired.
1328   mOpeningPopup = aPopup;
1329 
1330   nsEventStatus status = nsEventStatus_eIgnore;
1331   WidgetMouseEvent event(true, eXULPopupShowing, nullptr,
1332                          WidgetMouseEvent::eReal);
1333 
1334   // coordinates are relative to the root widget
1335   nsPresContext* rootPresContext =
1336       presShell->GetPresContext()->GetRootPresContext();
1337   if (rootPresContext) {
1338     rootPresContext->PresShell()->GetViewManager()->GetRootWidget(
1339         getter_AddRefs(event.mWidget));
1340   } else {
1341     event.mWidget = nullptr;
1342   }
1343 
1344   if (aTriggerEvent) {
1345     WidgetMouseEventBase* mouseEvent =
1346         aTriggerEvent->WidgetEventPtr()->AsMouseEventBase();
1347     if (mouseEvent) {
1348       event.inputSource = mouseEvent->inputSource;
1349     }
1350   }
1351 
1352   event.mRefPoint = mCachedMousePoint;
1353   event.mModifiers = mCachedModifiers;
1354   EventDispatcher::Dispatch(popup, presContext, &event, nullptr, &status);
1355 
1356   mCachedMousePoint = LayoutDeviceIntPoint(0, 0);
1357   mOpeningPopup = nullptr;
1358 
1359   mCachedModifiers = 0;
1360 
1361   // if a panel, blur whatever has focus so that the panel can take the focus.
1362   // This is done after the popupshowing event in case that event is cancelled.
1363   // Using noautofocus="true" will disable this behaviour, which is needed for
1364   // the autocomplete widget as it manages focus itself.
1365   if (popupType == ePopupTypePanel &&
1366       !popup->AsElement()->AttrValueIs(kNameSpaceID_None,
1367                                        nsGkAtoms::noautofocus, nsGkAtoms::_true,
1368                                        eCaseMatters)) {
1369     nsIFocusManager* fm = nsFocusManager::GetFocusManager();
1370     if (fm) {
1371       nsIDocument* doc = popup->GetUncomposedDoc();
1372 
1373       // Only remove the focus if the currently focused item is ouside the
1374       // popup. It isn't a big deal if the current focus is in a child popup
1375       // inside the popup as that shouldn't be visible. This check ensures that
1376       // a node inside the popup that is focused during a popupshowing event
1377       // remains focused.
1378       nsCOMPtr<nsIDOMElement> currentFocusElement;
1379       fm->GetFocusedElement(getter_AddRefs(currentFocusElement));
1380       nsCOMPtr<nsIContent> currentFocus =
1381           do_QueryInterface(currentFocusElement);
1382       if (doc && currentFocus &&
1383           !nsContentUtils::ContentIsCrossDocDescendantOf(currentFocus, popup)) {
1384         fm->ClearFocus(doc->GetWindow());
1385       }
1386     }
1387   }
1388 
1389   // clear these as they are no longer valid
1390   mRangeParent = nullptr;
1391   mRangeOffset = 0;
1392 
1393   // get the frame again in case it went away
1394   popupFrame = do_QueryFrame(aPopup->GetPrimaryFrame());
1395   if (popupFrame) {
1396     // if the event was cancelled, don't open the popup, reset its state back
1397     // to closed and clear its trigger content.
1398     if (status == nsEventStatus_eConsumeNoDefault) {
1399       popupFrame->SetPopupState(ePopupClosed);
1400       popupFrame->ClearTriggerContent();
1401     } else {
1402       // Now check if we need to fire the popuppositioned event. If not, call
1403       // ShowPopupCallback directly.
1404 
1405       // The popuppositioned event only fires on arrow panels for now.
1406       if (popup->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
1407                                           nsGkAtoms::arrow, eCaseMatters)) {
1408         popupFrame->ShowWithPositionedEvent();
1409         presShell->FrameNeedsReflow(popupFrame, nsIPresShell::eTreeChange,
1410                                     NS_FRAME_HAS_DIRTY_CHILDREN);
1411       } else {
1412         ShowPopupCallback(popup, popupFrame, aIsContextMenu, aSelectFirstItem);
1413       }
1414     }
1415   }
1416 }
1417 
FirePopupHidingEvent(nsIContent * aPopup,nsIContent * aNextPopup,nsIContent * aLastPopup,nsPresContext * aPresContext,nsPopupType aPopupType,bool aDeselectMenu,bool aIsCancel)1418 void nsXULPopupManager::FirePopupHidingEvent(
1419     nsIContent* aPopup, nsIContent* aNextPopup, nsIContent* aLastPopup,
1420     nsPresContext* aPresContext, nsPopupType aPopupType, bool aDeselectMenu,
1421     bool aIsCancel) {
1422   nsCOMPtr<nsIPresShell> presShell = aPresContext->PresShell();
1423   mozilla::Unused << presShell;  // This presShell may be keeping things alive
1424                                  // on non GTK platforms
1425 
1426   nsEventStatus status = nsEventStatus_eIgnore;
1427   WidgetMouseEvent event(true, eXULPopupHiding, nullptr,
1428                          WidgetMouseEvent::eReal);
1429   EventDispatcher::Dispatch(aPopup, aPresContext, &event, nullptr, &status);
1430 
1431   // when a panel is closed, blur whatever has focus inside the popup
1432   if (aPopupType == ePopupTypePanel &&
1433       (!aPopup->IsElement() || !aPopup->AsElement()->AttrValueIs(
1434                                    kNameSpaceID_None, nsGkAtoms::noautofocus,
1435                                    nsGkAtoms::_true, eCaseMatters))) {
1436     nsIFocusManager* fm = nsFocusManager::GetFocusManager();
1437     if (fm) {
1438       nsIDocument* doc = aPopup->GetUncomposedDoc();
1439 
1440       // Remove the focus from the focused node only if it is inside the popup.
1441       nsCOMPtr<nsIDOMElement> currentFocusElement;
1442       fm->GetFocusedElement(getter_AddRefs(currentFocusElement));
1443       nsCOMPtr<nsIContent> currentFocus =
1444           do_QueryInterface(currentFocusElement);
1445       if (doc && currentFocus &&
1446           nsContentUtils::ContentIsCrossDocDescendantOf(currentFocus, aPopup)) {
1447         fm->ClearFocus(doc->GetWindow());
1448       }
1449     }
1450   }
1451 
1452   // get frame again in case it went away
1453   nsMenuPopupFrame* popupFrame = do_QueryFrame(aPopup->GetPrimaryFrame());
1454   if (popupFrame) {
1455     // if the event was cancelled, don't hide the popup, and reset its
1456     // state back to open. Only popups in chrome shells can prevent a popup
1457     // from hiding.
1458     if (status == nsEventStatus_eConsumeNoDefault &&
1459         !popupFrame->IsInContentShell()) {
1460       // XXXndeakin
1461       // If an attempt was made to hide this popup before the popupshown event
1462       // fired, then ePopupShown is set here even though it should be
1463       // ePopupVisible. This probably isn't worth the hassle of handling.
1464       popupFrame->SetPopupState(ePopupShown);
1465     } else {
1466     // If the popup has an animate attribute and it is not set to false, check
1467     // if it has a closing transition and wait for it to finish. The transition
1468     // may still occur either way, but the view will be hidden and you won't be
1469     // able to see it. If there is a next popup, indicating that mutliple popups
1470     // are rolling up, don't wait and hide the popup right away since the effect
1471     // would likely be undesirable. Transitions are currently disabled on Linux
1472     // due to rendering issues on certain configurations.
1473 #ifndef MOZ_WIDGET_GTK
1474       if (!aNextPopup && aPopup->IsElement() &&
1475           aPopup->AsElement()->HasAttr(kNameSpaceID_None, nsGkAtoms::animate)) {
1476         // If animate="false" then don't transition at all. If animate="cancel",
1477         // only show the transition if cancelling the popup or rolling up.
1478         // Otherwise, always show the transition.
1479         nsAutoString animate;
1480         aPopup->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::animate,
1481                                      animate);
1482 
1483         if (!animate.EqualsLiteral("false") &&
1484             (!animate.EqualsLiteral("cancel") || aIsCancel)) {
1485           presShell->FlushPendingNotifications(FlushType::Layout);
1486 
1487           // Get the frame again in case the flush caused it to go away
1488           popupFrame = do_QueryFrame(aPopup->GetPrimaryFrame());
1489           if (!popupFrame) return;
1490 
1491           if (nsLayoutUtils::HasCurrentTransitions(popupFrame)) {
1492             RefPtr<TransitionEnder> ender =
1493                 new TransitionEnder(aPopup, aDeselectMenu);
1494             aPopup->AddSystemEventListener(NS_LITERAL_STRING("transitionend"),
1495                                            ender, false, false);
1496             return;
1497           }
1498         }
1499       }
1500 #endif
1501 
1502       HidePopupCallback(aPopup, popupFrame, aNextPopup, aLastPopup, aPopupType,
1503                         aDeselectMenu);
1504     }
1505   }
1506 }
1507 
IsPopupOpen(nsIContent * aPopup)1508 bool nsXULPopupManager::IsPopupOpen(nsIContent* aPopup) {
1509   // a popup is open if it is in the open list. The assertions ensure that the
1510   // frame is in the correct state. If the popup is in the hiding or invisible
1511   // state, it will still be in the open popup list until it is closed.
1512   nsMenuChainItem* item = mPopups;
1513   while (item) {
1514     if (item->Content() == aPopup) {
1515       NS_ASSERTION(item->Frame()->IsOpen() ||
1516                        item->Frame()->PopupState() == ePopupHiding ||
1517                        item->Frame()->PopupState() == ePopupInvisible,
1518                    "popup in open list not actually open");
1519       return true;
1520     }
1521     item = item->GetParent();
1522   }
1523 
1524   return false;
1525 }
1526 
IsPopupOpenForMenuParent(nsMenuParent * aMenuParent)1527 bool nsXULPopupManager::IsPopupOpenForMenuParent(nsMenuParent* aMenuParent) {
1528   nsMenuChainItem* item = GetTopVisibleMenu();
1529   while (item) {
1530     nsMenuPopupFrame* popup = item->Frame();
1531     if (popup && popup->IsOpen()) {
1532       nsMenuFrame* menuFrame = do_QueryFrame(popup->GetParent());
1533       if (menuFrame && menuFrame->GetMenuParent() == aMenuParent) {
1534         return true;
1535       }
1536     }
1537     item = item->GetParent();
1538   }
1539 
1540   return false;
1541 }
1542 
GetTopPopup(nsPopupType aType)1543 nsIFrame* nsXULPopupManager::GetTopPopup(nsPopupType aType) {
1544   nsMenuChainItem* item = mPopups;
1545   while (item) {
1546     if (item->Frame()->IsVisible() &&
1547         (item->PopupType() == aType || aType == ePopupTypeAny)) {
1548       return item->Frame();
1549     }
1550 
1551     item = item->GetParent();
1552   }
1553 
1554   return nullptr;
1555 }
1556 
GetVisiblePopups(nsTArray<nsIFrame * > & aPopups)1557 void nsXULPopupManager::GetVisiblePopups(nsTArray<nsIFrame*>& aPopups) {
1558   aPopups.Clear();
1559 
1560   nsMenuChainItem* item = mPopups;
1561   while (item) {
1562     // Skip panels which are not visible as well as popups that
1563     // are transparent to mouse events.
1564     if (item->Frame()->IsVisible() && !item->Frame()->IsMouseTransparent()) {
1565       aPopups.AppendElement(item->Frame());
1566     }
1567 
1568     item = item->GetParent();
1569   }
1570 }
1571 
GetLastTriggerNode(nsIDocument * aDocument,bool aIsTooltip)1572 already_AddRefed<nsINode> nsXULPopupManager::GetLastTriggerNode(
1573     nsIDocument* aDocument, bool aIsTooltip) {
1574   if (!aDocument) return nullptr;
1575 
1576   nsCOMPtr<nsINode> node;
1577 
1578   // if mOpeningPopup is set, it means that a popupshowing event is being
1579   // fired. In this case, just use the cached node, as the popup is not yet in
1580   // the list of open popups.
1581   if (mOpeningPopup && mOpeningPopup->GetUncomposedDoc() == aDocument &&
1582       aIsTooltip == mOpeningPopup->IsXULElement(nsGkAtoms::tooltip)) {
1583     node = nsMenuPopupFrame::GetTriggerContent(
1584         GetPopupFrameForContent(mOpeningPopup, false));
1585   } else {
1586     nsMenuChainItem* item = mPopups;
1587     while (item) {
1588       // look for a popup of the same type and document.
1589       if ((item->PopupType() == ePopupTypeTooltip) == aIsTooltip &&
1590           item->Content()->GetUncomposedDoc() == aDocument) {
1591         node = nsMenuPopupFrame::GetTriggerContent(item->Frame());
1592         if (node) break;
1593       }
1594       item = item->GetParent();
1595     }
1596   }
1597 
1598   return node.forget();
1599 }
1600 
MayShowPopup(nsMenuPopupFrame * aPopup)1601 bool nsXULPopupManager::MayShowPopup(nsMenuPopupFrame* aPopup) {
1602   // if a popup's IsOpen method returns true, then the popup must always be in
1603   // the popup chain scanned in IsPopupOpen.
1604   NS_ASSERTION(!aPopup->IsOpen() || IsPopupOpen(aPopup->GetContent()),
1605                "popup frame state doesn't match XULPopupManager open state");
1606 
1607   nsPopupState state = aPopup->PopupState();
1608 
1609   // if the popup is not in the open popup chain, then it must have a state that
1610   // is either closed, in the process of being shown, or invisible.
1611   NS_ASSERTION(IsPopupOpen(aPopup->GetContent()) || state == ePopupClosed ||
1612                    state == ePopupShowing || state == ePopupPositioning ||
1613                    state == ePopupInvisible,
1614                "popup not in XULPopupManager open list is open");
1615 
1616   // don't show popups unless they are closed or invisible
1617   if (state != ePopupClosed && state != ePopupInvisible) return false;
1618 
1619   // Don't show popups that we already have in our popup chain
1620   if (IsPopupOpen(aPopup->GetContent())) {
1621     NS_WARNING("Refusing to show duplicate popup");
1622     return false;
1623   }
1624 
1625   // if the popup was just rolled up, don't reopen it
1626   if (mozilla::widget::nsAutoRollup::GetLastRollup() == aPopup->GetContent())
1627     return false;
1628 
1629   nsCOMPtr<nsIDocShellTreeItem> dsti = aPopup->PresContext()->GetDocShell();
1630   nsCOMPtr<nsIBaseWindow> baseWin = do_QueryInterface(dsti);
1631   if (!baseWin) return false;
1632 
1633   nsCOMPtr<nsIDocShellTreeItem> root;
1634   dsti->GetRootTreeItem(getter_AddRefs(root));
1635   if (!root) {
1636     return false;
1637   }
1638 
1639   nsCOMPtr<nsPIDOMWindowOuter> rootWin = root->GetWindow();
1640 
1641   // chrome shells can always open popups, but other types of shells can only
1642   // open popups when they are focused and visible
1643   if (dsti->ItemType() != nsIDocShellTreeItem::typeChrome) {
1644     // only allow popups in active windows
1645     nsIFocusManager* fm = nsFocusManager::GetFocusManager();
1646     if (!fm || !rootWin) return false;
1647 
1648     nsCOMPtr<mozIDOMWindowProxy> activeWindow;
1649     fm->GetActiveWindow(getter_AddRefs(activeWindow));
1650     if (activeWindow != rootWin) return false;
1651 
1652     // only allow popups in visible frames
1653     bool visible;
1654     baseWin->GetVisibility(&visible);
1655     if (!visible) return false;
1656   }
1657 
1658   // platforms respond differently when an popup is opened in a minimized
1659   // window, so this is always disabled.
1660   nsCOMPtr<nsIWidget> mainWidget;
1661   baseWin->GetMainWidget(getter_AddRefs(mainWidget));
1662   if (mainWidget && mainWidget->SizeMode() == nsSizeMode_Minimized) {
1663     return false;
1664   }
1665 
1666 #ifdef XP_MACOSX
1667   if (rootWin) {
1668     auto globalWin = nsGlobalWindowOuter::Cast(rootWin.get());
1669     if (globalWin->IsInModalState()) {
1670       return false;
1671     }
1672   }
1673 #endif
1674 
1675   // cannot open a popup that is a submenu of a menupopup that isn't open.
1676   nsMenuFrame* menuFrame = do_QueryFrame(aPopup->GetParent());
1677   if (menuFrame) {
1678     nsMenuParent* parentPopup = menuFrame->GetMenuParent();
1679     if (parentPopup && !parentPopup->IsOpen()) return false;
1680   }
1681 
1682   return true;
1683 }
1684 
PopupDestroyed(nsMenuPopupFrame * aPopup)1685 void nsXULPopupManager::PopupDestroyed(nsMenuPopupFrame* aPopup) {
1686   // when a popup frame is destroyed, just unhook it from the list of popups
1687   if (mTimerMenu == aPopup) {
1688     if (mCloseTimer) {
1689       mCloseTimer->Cancel();
1690       mCloseTimer = nullptr;
1691     }
1692     mTimerMenu = nullptr;
1693   }
1694 
1695   nsTArray<nsMenuPopupFrame*> popupsToHide;
1696 
1697   nsMenuChainItem* item = mPopups;
1698   while (item) {
1699     nsMenuPopupFrame* frame = item->Frame();
1700     if (frame == aPopup) {
1701       // XXXndeakin shouldn't this only happen for menus?
1702       if (!item->IsNoAutoHide() && frame->PopupState() != ePopupInvisible) {
1703         // Iterate through any child menus and hide them as well, since the
1704         // parent is going away. We won't remove them from the list yet, just
1705         // hide them, as they will be removed from the list when this function
1706         // gets called for that child frame.
1707         nsMenuChainItem* child = item->GetChild();
1708         while (child) {
1709           // if the popup is a child frame of the menu that was destroyed, add
1710           // it to the list of popups to hide. Don't bother with the events
1711           // since the frames are going away. If the child menu is not a child
1712           // frame, for example, a context menu, use HidePopup instead, but call
1713           // it asynchronously since we are in the middle of frame destruction.
1714           nsMenuPopupFrame* childframe = child->Frame();
1715           if (nsLayoutUtils::IsProperAncestorFrame(frame, childframe)) {
1716             popupsToHide.AppendElement(childframe);
1717           } else {
1718             // HidePopup will take care of hiding any of its children, so
1719             // break out afterwards
1720             HidePopup(child->Content(), false, false, true, false);
1721             break;
1722           }
1723 
1724           child = child->GetChild();
1725         }
1726       }
1727 
1728       item->Detach(&mPopups);
1729       delete item;
1730       break;
1731     }
1732 
1733     item = item->GetParent();
1734   }
1735 
1736   HidePopupsInList(popupsToHide);
1737 }
1738 
HasContextMenu(nsMenuPopupFrame * aPopup)1739 bool nsXULPopupManager::HasContextMenu(nsMenuPopupFrame* aPopup) {
1740   nsMenuChainItem* item = GetTopVisibleMenu();
1741   while (item && item->Frame() != aPopup) {
1742     if (item->IsContextMenu()) return true;
1743     item = item->GetParent();
1744   }
1745 
1746   return false;
1747 }
1748 
SetCaptureState(nsIContent * aOldPopup)1749 void nsXULPopupManager::SetCaptureState(nsIContent* aOldPopup) {
1750   nsMenuChainItem* item = GetTopVisibleMenu();
1751   if (item && aOldPopup == item->Content()) return;
1752 
1753   if (mWidget) {
1754     mWidget->CaptureRollupEvents(nullptr, false);
1755     mWidget = nullptr;
1756   }
1757 
1758   if (item) {
1759     nsMenuPopupFrame* popup = item->Frame();
1760     mWidget = popup->GetWidget();
1761     if (mWidget) {
1762       mWidget->CaptureRollupEvents(nullptr, true);
1763       popup->AttachedDismissalListener();
1764     }
1765   }
1766 
1767   UpdateKeyboardListeners();
1768 }
1769 
UpdateKeyboardListeners()1770 void nsXULPopupManager::UpdateKeyboardListeners() {
1771   nsCOMPtr<EventTarget> newTarget;
1772   bool isForMenu = false;
1773   nsMenuChainItem* item = GetTopVisibleMenu();
1774   if (item) {
1775     if (item->IgnoreKeys() != eIgnoreKeys_True) {
1776       newTarget = item->Content()->GetComposedDoc();
1777     }
1778     isForMenu = item->PopupType() == ePopupTypeMenu;
1779   } else if (mActiveMenuBar) {
1780     newTarget = mActiveMenuBar->GetContent()->GetComposedDoc();
1781     isForMenu = true;
1782   }
1783 
1784   if (mKeyListener != newTarget) {
1785     if (mKeyListener) {
1786       mKeyListener->RemoveEventListener(NS_LITERAL_STRING("keypress"), this,
1787                                         true);
1788       mKeyListener->RemoveEventListener(NS_LITERAL_STRING("keydown"), this,
1789                                         true);
1790       mKeyListener->RemoveEventListener(NS_LITERAL_STRING("keyup"), this, true);
1791       mKeyListener = nullptr;
1792       nsContentUtils::NotifyInstalledMenuKeyboardListener(false);
1793     }
1794 
1795     if (newTarget) {
1796       newTarget->AddEventListener(NS_LITERAL_STRING("keypress"), this, true);
1797       newTarget->AddEventListener(NS_LITERAL_STRING("keydown"), this, true);
1798       newTarget->AddEventListener(NS_LITERAL_STRING("keyup"), this, true);
1799       nsContentUtils::NotifyInstalledMenuKeyboardListener(isForMenu);
1800       mKeyListener = newTarget;
1801     }
1802   }
1803 }
1804 
UpdateMenuItems(nsIContent * aPopup)1805 void nsXULPopupManager::UpdateMenuItems(nsIContent* aPopup) {
1806   // Walk all of the menu's children, checking to see if any of them has a
1807   // command attribute. If so, then several attributes must potentially be
1808   // updated.
1809 
1810   nsCOMPtr<nsIDocument> document = aPopup->GetUncomposedDoc();
1811   if (!document) {
1812     return;
1813   }
1814 
1815   // When a menu is opened, make sure that command updating is unlocked first.
1816   XULDocument* xulDoc = document->AsXULDocument();
1817   if (xulDoc) {
1818     nsCOMPtr<nsIDOMXULCommandDispatcher> xulCommandDispatcher =
1819         xulDoc->GetCommandDispatcher();
1820     if (xulCommandDispatcher) {
1821       xulCommandDispatcher->Unlock();
1822     }
1823   }
1824 
1825   for (nsCOMPtr<nsIContent> grandChild = aPopup->GetFirstChild(); grandChild;
1826        grandChild = grandChild->GetNextSibling()) {
1827     if (grandChild->IsXULElement(nsGkAtoms::menugroup)) {
1828       if (grandChild->GetChildCount() == 0) {
1829         continue;
1830       }
1831       grandChild = grandChild->GetFirstChild();
1832     }
1833     if (grandChild->IsXULElement(nsGkAtoms::menuitem)) {
1834       // See if we have a command attribute.
1835       Element* grandChildElement = grandChild->AsElement();
1836       nsAutoString command;
1837       grandChildElement->GetAttr(kNameSpaceID_None, nsGkAtoms::command,
1838                                  command);
1839       if (!command.IsEmpty()) {
1840         // We do! Look it up in our document
1841         RefPtr<dom::Element> commandElement = document->GetElementById(command);
1842         if (commandElement) {
1843           nsAutoString commandValue;
1844           // The menu's disabled state needs to be updated to match the command.
1845           if (commandElement->GetAttr(kNameSpaceID_None, nsGkAtoms::disabled,
1846                                       commandValue))
1847             grandChildElement->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled,
1848                                        commandValue, true);
1849           else
1850             grandChildElement->UnsetAttr(kNameSpaceID_None, nsGkAtoms::disabled,
1851                                          true);
1852 
1853           // The menu's label, accesskey checked and hidden states need to be
1854           // updated to match the command. Note that unlike the disabled state
1855           // if the command has *no* value, we assume the menu is supplying its
1856           // own.
1857           if (commandElement->GetAttr(kNameSpaceID_None, nsGkAtoms::label,
1858                                       commandValue))
1859             grandChildElement->SetAttr(kNameSpaceID_None, nsGkAtoms::label,
1860                                        commandValue, true);
1861 
1862           if (commandElement->GetAttr(kNameSpaceID_None, nsGkAtoms::accesskey,
1863                                       commandValue))
1864             grandChildElement->SetAttr(kNameSpaceID_None, nsGkAtoms::accesskey,
1865                                        commandValue, true);
1866 
1867           if (commandElement->GetAttr(kNameSpaceID_None, nsGkAtoms::checked,
1868                                       commandValue))
1869             grandChildElement->SetAttr(kNameSpaceID_None, nsGkAtoms::checked,
1870                                        commandValue, true);
1871 
1872           if (commandElement->GetAttr(kNameSpaceID_None, nsGkAtoms::hidden,
1873                                       commandValue))
1874             grandChildElement->SetAttr(kNameSpaceID_None, nsGkAtoms::hidden,
1875                                        commandValue, true);
1876         }
1877       }
1878     }
1879     if (!grandChild->GetNextSibling() &&
1880         grandChild->GetParent()->IsXULElement(nsGkAtoms::menugroup)) {
1881       grandChild = grandChild->GetParent();
1882     }
1883   }
1884 }
1885 
1886 // Notify
1887 //
1888 // The item selection timer has fired, we might have to readjust the
1889 // selected item. There are two cases here that we are trying to deal with:
1890 //   (1) diagonal movement from a parent menu to a submenu passing briefly over
1891 //       other items, and
1892 //   (2) moving out from a submenu to a parent or grandparent menu.
1893 // In both cases, |mTimerMenu| is the menu item that might have an open submenu
1894 // and the first item in |mPopups| is the item the mouse is currently over,
1895 // which could be none of them.
1896 //
1897 // case (1):
1898 //  As the mouse moves from the parent item of a submenu (we'll call 'A')
1899 //  diagonally into the submenu, it probably passes through one or more
1900 //  sibilings (B). As the mouse passes through B, it becomes the current menu
1901 //  item and the timer is set and mTimerMenu is set to A. Before the timer
1902 //  fires, the mouse leaves the menu containing A and B and enters the submenus.
1903 //  Now when the timer fires, |mPopups| is null (!= |mTimerMenu|) so we have to
1904 //  see if anything in A's children is selected (recall that even disabled items
1905 //  are selected, the style just doesn't show it). If that is the case, we need
1906 //  to set the selected item back to A.
1907 //
1908 // case (2);
1909 //  Item A has an open submenu, and in it there is an item (B) which also has an
1910 //  open submenu (so there are 3 menus displayed right now). The mouse then
1911 //  leaves B's child submenu and selects an item that is a sibling of A, call it
1912 //  C. When the mouse enters C, the timer is set and |mTimerMenu| is A and
1913 //  |mPopups| is C. As the timer fires, the mouse is still within C. The correct
1914 //  behavior is to set the current item to C and close up the chain parented at
1915 //  A.
1916 //
1917 //  This brings up the question of is the logic of case (1) enough? The answer
1918 //  is no, and is discussed in bugzilla bug 29400. Case (1) asks if A's submenu
1919 //  has a selected child, and if it does, set the selected item to A. Because B
1920 //  has a submenu open, it is selected and as a result, A is set to be the
1921 //  selected item even though the mouse rests in C -- very wrong.
1922 //
1923 //  The solution is to use the same idea, but instead of only checking one
1924 //  level, drill all the way down to the deepest open submenu and check if it
1925 //  has something selected. Since the mouse is in a grandparent, it won't, and
1926 //  we know that we can safely close up A and all its children.
1927 //
1928 // The code below melds the two cases together.
1929 //
KillMenuTimer()1930 void nsXULPopupManager::KillMenuTimer() {
1931   if (mCloseTimer && mTimerMenu) {
1932     mCloseTimer->Cancel();
1933     mCloseTimer = nullptr;
1934 
1935     if (mTimerMenu->IsOpen())
1936       HidePopup(mTimerMenu->GetContent(), false, false, true, false);
1937   }
1938 
1939   mTimerMenu = nullptr;
1940 }
1941 
CancelMenuTimer(nsMenuParent * aMenuParent)1942 void nsXULPopupManager::CancelMenuTimer(nsMenuParent* aMenuParent) {
1943   if (mCloseTimer && mTimerMenu == aMenuParent) {
1944     mCloseTimer->Cancel();
1945     mCloseTimer = nullptr;
1946     mTimerMenu = nullptr;
1947   }
1948 }
1949 
HandleShortcutNavigation(KeyboardEvent * aKeyEvent,nsMenuPopupFrame * aFrame)1950 bool nsXULPopupManager::HandleShortcutNavigation(KeyboardEvent* aKeyEvent,
1951                                                  nsMenuPopupFrame* aFrame) {
1952 // On Windows, don't check shortcuts when the accelerator key is down.
1953 #ifdef XP_WIN
1954   WidgetInputEvent* evt = aKeyEvent->WidgetEventPtr()->AsInputEvent();
1955   if (evt && evt->IsAccel()) {
1956     return false;
1957   }
1958 #endif
1959 
1960   nsMenuChainItem* item = GetTopVisibleMenu();
1961   if (!aFrame && item) aFrame = item->Frame();
1962 
1963   if (aFrame) {
1964     bool action;
1965     nsMenuFrame* result = aFrame->FindMenuWithShortcut(aKeyEvent, action);
1966     if (result) {
1967       aFrame->ChangeMenuItem(result, false, true);
1968       if (action) {
1969         WidgetGUIEvent* evt = aKeyEvent->WidgetEventPtr()->AsGUIEvent();
1970         nsMenuFrame* menuToOpen = result->Enter(evt);
1971         if (menuToOpen) {
1972           nsCOMPtr<nsIContent> content = menuToOpen->GetContent();
1973           ShowMenu(content, true, false);
1974         }
1975       }
1976       return true;
1977     }
1978 
1979     return false;
1980   }
1981 
1982   if (mActiveMenuBar) {
1983     nsMenuFrame* result =
1984         mActiveMenuBar->FindMenuWithShortcut(aKeyEvent, false);
1985     if (result) {
1986       mActiveMenuBar->SetActive(true);
1987       result->OpenMenu(true);
1988       return true;
1989     }
1990   }
1991 
1992   return false;
1993 }
1994 
HandleKeyboardNavigation(uint32_t aKeyCode)1995 bool nsXULPopupManager::HandleKeyboardNavigation(uint32_t aKeyCode) {
1996   // navigate up through the open menus, looking for the topmost one
1997   // in the same hierarchy
1998   nsMenuChainItem* item = nullptr;
1999   nsMenuChainItem* nextitem = GetTopVisibleMenu();
2000 
2001   while (nextitem) {
2002     item = nextitem;
2003     nextitem = item->GetParent();
2004 
2005     if (nextitem) {
2006       // stop if the parent isn't a menu
2007       if (!nextitem->IsMenu()) break;
2008 
2009       // check to make sure that the parent is actually the parent menu. It
2010       // won't be if the parent is in a different frame hierarchy, for example,
2011       // for a context menu opened on another menu.
2012       nsMenuParent* expectedParent =
2013           static_cast<nsMenuParent*>(nextitem->Frame());
2014       nsMenuFrame* menuFrame = do_QueryFrame(item->Frame()->GetParent());
2015       if (!menuFrame || menuFrame->GetMenuParent() != expectedParent) {
2016         break;
2017       }
2018     }
2019   }
2020 
2021   nsIFrame* itemFrame;
2022   if (item)
2023     itemFrame = item->Frame();
2024   else if (mActiveMenuBar)
2025     itemFrame = mActiveMenuBar;
2026   else
2027     return false;
2028 
2029   nsNavigationDirection theDirection;
2030   NS_ASSERTION(aKeyCode >= KeyboardEventBinding::DOM_VK_END &&
2031                    aKeyCode <= KeyboardEventBinding::DOM_VK_DOWN,
2032                "Illegal key code");
2033   theDirection = NS_DIRECTION_FROM_KEY_CODE(itemFrame, aKeyCode);
2034 
2035   bool selectFirstItem = true;
2036 #ifdef MOZ_WIDGET_GTK
2037   nsMenuFrame* currentItem = nullptr;
2038   if (item && mActiveMenuBar && NS_DIRECTION_IS_INLINE(theDirection)) {
2039     currentItem = item->Frame()->GetCurrentMenuItem();
2040     // If nothing is selected in the menu and we have a menubar, let it
2041     // handle the movement not to steal focus from it.
2042     if (!currentItem) {
2043       item = nullptr;
2044     }
2045   }
2046   // On menu change, only select first item if an item is already selected.
2047   selectFirstItem = currentItem != nullptr;
2048 #endif
2049 
2050   // if a popup is open, first check for navigation within the popup
2051   if (item && HandleKeyboardNavigationInPopup(item, theDirection)) return true;
2052 
2053   // no popup handled the key, so check the active menubar, if any
2054   if (mActiveMenuBar) {
2055     nsMenuFrame* currentMenu = mActiveMenuBar->GetCurrentMenuItem();
2056 
2057     if (NS_DIRECTION_IS_INLINE(theDirection)) {
2058       nsMenuFrame* nextItem =
2059           (theDirection == eNavigationDirection_End)
2060               ? GetNextMenuItem(mActiveMenuBar, currentMenu, false, true)
2061               : GetPreviousMenuItem(mActiveMenuBar, currentMenu, false, true);
2062       mActiveMenuBar->ChangeMenuItem(nextItem, selectFirstItem, true);
2063       return true;
2064     } else if (NS_DIRECTION_IS_BLOCK(theDirection)) {
2065       // Open the menu and select its first item.
2066       if (currentMenu) {
2067         nsCOMPtr<nsIContent> content = currentMenu->GetContent();
2068         ShowMenu(content, true, false);
2069       }
2070       return true;
2071     }
2072   }
2073 
2074   return false;
2075 }
2076 
HandleKeyboardNavigationInPopup(nsMenuChainItem * item,nsMenuPopupFrame * aFrame,nsNavigationDirection aDir)2077 bool nsXULPopupManager::HandleKeyboardNavigationInPopup(
2078     nsMenuChainItem* item, nsMenuPopupFrame* aFrame,
2079     nsNavigationDirection aDir) {
2080   NS_ASSERTION(aFrame, "aFrame is null");
2081   NS_ASSERTION(!item || item->Frame() == aFrame,
2082                "aFrame is expected to be equal to item->Frame()");
2083 
2084   nsMenuFrame* currentMenu = aFrame->GetCurrentMenuItem();
2085 
2086   aFrame->ClearIncrementalString();
2087 
2088   // This method only gets called if we're open.
2089   if (!currentMenu && NS_DIRECTION_IS_INLINE(aDir)) {
2090     // We've been opened, but we haven't had anything selected.
2091     // We can handle End, but our parent handles Start.
2092     if (aDir == eNavigationDirection_End) {
2093       nsMenuFrame* nextItem = GetNextMenuItem(aFrame, nullptr, true, false);
2094       if (nextItem) {
2095         aFrame->ChangeMenuItem(nextItem, false, true);
2096         return true;
2097       }
2098     }
2099     return false;
2100   }
2101 
2102   bool isContainer = false;
2103   bool isOpen = false;
2104   if (currentMenu) {
2105     isOpen = currentMenu->IsOpen();
2106     isContainer = currentMenu->IsMenu();
2107     if (isOpen) {
2108       // for an open popup, have the child process the event
2109       nsMenuChainItem* child = item ? item->GetChild() : nullptr;
2110       if (child && HandleKeyboardNavigationInPopup(child, aDir)) return true;
2111     } else if (aDir == eNavigationDirection_End && isContainer &&
2112                !currentMenu->IsDisabled()) {
2113       // The menu is not yet open. Open it and select the first item.
2114       nsCOMPtr<nsIContent> content = currentMenu->GetContent();
2115       ShowMenu(content, true, false);
2116       return true;
2117     }
2118   }
2119 
2120   // For block progression, we can move in either direction
2121   if (NS_DIRECTION_IS_BLOCK(aDir) || NS_DIRECTION_IS_BLOCK_TO_EDGE(aDir)) {
2122     nsMenuFrame* nextItem;
2123 
2124     if (aDir == eNavigationDirection_Before ||
2125         aDir == eNavigationDirection_After) {
2126       // Cursor navigation does not wrap on Mac or for menulists on Windows.
2127       bool wrap =
2128 #ifdef XP_WIN
2129           aFrame->IsMenuList() ? false : true;
2130 #elif defined XP_MACOSX
2131           false;
2132 #else
2133           true;
2134 #endif
2135 
2136       if (aDir == eNavigationDirection_Before) {
2137         nextItem = GetPreviousMenuItem(aFrame, currentMenu, true, wrap);
2138       } else {
2139         nextItem = GetNextMenuItem(aFrame, currentMenu, true, wrap);
2140       }
2141     } else if (aDir == eNavigationDirection_First) {
2142       nextItem = GetNextMenuItem(aFrame, nullptr, true, false);
2143     } else {
2144       nextItem = GetPreviousMenuItem(aFrame, nullptr, true, false);
2145     }
2146 
2147     if (nextItem) {
2148       aFrame->ChangeMenuItem(nextItem, false, true);
2149       return true;
2150     }
2151   } else if (currentMenu && isContainer && isOpen) {
2152     if (aDir == eNavigationDirection_Start) {
2153       // close a submenu when Left is pressed
2154       nsMenuPopupFrame* popupFrame = currentMenu->GetPopup();
2155       if (popupFrame)
2156         HidePopup(popupFrame->GetContent(), false, false, false, false);
2157       return true;
2158     }
2159   }
2160 
2161   return false;
2162 }
2163 
HandleKeyboardEventWithKeyCode(KeyboardEvent * aKeyEvent,nsMenuChainItem * aTopVisibleMenuItem)2164 bool nsXULPopupManager::HandleKeyboardEventWithKeyCode(
2165     KeyboardEvent* aKeyEvent, nsMenuChainItem* aTopVisibleMenuItem) {
2166   uint32_t keyCode = aKeyEvent->KeyCode();
2167 
2168   // Escape should close panels, but the other keys should have no effect.
2169   if (aTopVisibleMenuItem &&
2170       aTopVisibleMenuItem->PopupType() != ePopupTypeMenu) {
2171     if (keyCode == KeyboardEventBinding::DOM_VK_ESCAPE) {
2172       HidePopup(aTopVisibleMenuItem->Content(), false, false, false, true);
2173       aKeyEvent->StopPropagation();
2174       aKeyEvent->StopCrossProcessForwarding();
2175       aKeyEvent->PreventDefault();
2176     }
2177     return true;
2178   }
2179 
2180   bool consume = (aTopVisibleMenuItem || mActiveMenuBar);
2181   switch (keyCode) {
2182     case KeyboardEventBinding::DOM_VK_UP:
2183     case KeyboardEventBinding::DOM_VK_DOWN:
2184 #ifndef XP_MACOSX
2185       // roll up the popup when alt+up/down are pressed within a menulist.
2186       if (aKeyEvent->AltKey() && aTopVisibleMenuItem &&
2187           aTopVisibleMenuItem->Frame()->IsMenuList()) {
2188         Rollup(0, false, nullptr, nullptr);
2189         break;
2190       }
2191       MOZ_FALLTHROUGH;
2192 #endif
2193 
2194     case KeyboardEventBinding::DOM_VK_LEFT:
2195     case KeyboardEventBinding::DOM_VK_RIGHT:
2196     case KeyboardEventBinding::DOM_VK_HOME:
2197     case KeyboardEventBinding::DOM_VK_END:
2198       HandleKeyboardNavigation(keyCode);
2199       break;
2200 
2201     case KeyboardEventBinding::DOM_VK_PAGE_DOWN:
2202     case KeyboardEventBinding::DOM_VK_PAGE_UP:
2203       if (aTopVisibleMenuItem) {
2204         aTopVisibleMenuItem->Frame()->ChangeByPage(
2205             keyCode == KeyboardEventBinding::DOM_VK_PAGE_UP);
2206       }
2207       break;
2208 
2209     case KeyboardEventBinding::DOM_VK_ESCAPE:
2210       // Pressing Escape hides one level of menus only. If no menu is open,
2211       // check if a menubar is active and inform it that a menu closed. Even
2212       // though in this latter case, a menu didn't actually close, the effect
2213       // ends up being the same. Similar for the tab key below.
2214       if (aTopVisibleMenuItem) {
2215         HidePopup(aTopVisibleMenuItem->Content(), false, false, false, true);
2216       } else if (mActiveMenuBar) {
2217         mActiveMenuBar->MenuClosed();
2218       }
2219       break;
2220 
2221     case KeyboardEventBinding::DOM_VK_TAB:
2222 #ifndef XP_MACOSX
2223     case KeyboardEventBinding::DOM_VK_F10:
2224 #endif
2225       if (aTopVisibleMenuItem &&
2226           !aTopVisibleMenuItem->Frame()->GetContent()->AsElement()->AttrValueIs(
2227               kNameSpaceID_None, nsGkAtoms::activateontab, nsGkAtoms::_true,
2228               eCaseMatters)) {
2229         // close popups or deactivate menubar when Tab or F10 are pressed
2230         Rollup(0, false, nullptr, nullptr);
2231         break;
2232       } else if (mActiveMenuBar) {
2233         mActiveMenuBar->MenuClosed();
2234         break;
2235       }
2236       // Intentional fall-through to RETURN case
2237       MOZ_FALLTHROUGH;
2238 
2239     case KeyboardEventBinding::DOM_VK_RETURN: {
2240       // If there is a popup open, check if the current item needs to be opened.
2241       // Otherwise, tell the active menubar, if any, to activate the menu. The
2242       // Enter method will return a menu if one needs to be opened as a result.
2243       nsMenuFrame* menuToOpen = nullptr;
2244       WidgetGUIEvent* GUIEvent = aKeyEvent->WidgetEventPtr()->AsGUIEvent();
2245 
2246       if (aTopVisibleMenuItem) {
2247         menuToOpen = aTopVisibleMenuItem->Frame()->Enter(GUIEvent);
2248       } else if (mActiveMenuBar) {
2249         menuToOpen = mActiveMenuBar->Enter(GUIEvent);
2250       }
2251       if (menuToOpen) {
2252         nsCOMPtr<nsIContent> content = menuToOpen->GetContent();
2253         ShowMenu(content, true, false);
2254       }
2255       break;
2256     }
2257 
2258     default:
2259       return false;
2260   }
2261 
2262   if (consume) {
2263     aKeyEvent->StopPropagation();
2264     aKeyEvent->StopCrossProcessForwarding();
2265     aKeyEvent->PreventDefault();
2266   }
2267   return true;
2268 }
2269 
ImmediateParentFrame(nsContainerFrame * aFrame)2270 nsContainerFrame* nsXULPopupManager::ImmediateParentFrame(
2271     nsContainerFrame* aFrame) {
2272   MOZ_ASSERT(aFrame && aFrame->GetContent());
2273 
2274   bool multiple = false;  // Unused
2275   nsIContent* insertionPoint =
2276       aFrame->GetContent()
2277           ->OwnerDoc()
2278           ->BindingManager()
2279           ->FindNestedSingleInsertionPoint(aFrame->GetContent(), &multiple);
2280 
2281   nsCSSFrameConstructor* fc = aFrame->PresContext()->FrameConstructor();
2282   nsContainerFrame* insertionFrame =
2283       insertionPoint ? fc->GetContentInsertionFrameFor(insertionPoint)
2284                      : nullptr;
2285 
2286   return insertionFrame ? insertionFrame : aFrame;
2287 }
2288 
GetNextMenuItem(nsContainerFrame * aParent,nsMenuFrame * aStart,bool aIsPopup,bool aWrap)2289 nsMenuFrame* nsXULPopupManager::GetNextMenuItem(nsContainerFrame* aParent,
2290                                                 nsMenuFrame* aStart,
2291                                                 bool aIsPopup, bool aWrap) {
2292   nsContainerFrame* immediateParent = ImmediateParentFrame(aParent);
2293   nsIFrame* currFrame = nullptr;
2294   if (aStart) {
2295     if (aStart->GetNextSibling())
2296       currFrame = aStart->GetNextSibling();
2297     else if (aStart->GetParent()->GetContent()->IsXULElement(
2298                  nsGkAtoms::menugroup))
2299       currFrame = aStart->GetParent()->GetNextSibling();
2300   } else
2301     currFrame = immediateParent->PrincipalChildList().FirstChild();
2302 
2303   while (currFrame) {
2304     // See if it's a menu item.
2305     nsIContent* currFrameContent = currFrame->GetContent();
2306     if (IsValidMenuItem(currFrameContent, aIsPopup)) {
2307       return do_QueryFrame(currFrame);
2308     }
2309     if (currFrameContent->IsXULElement(nsGkAtoms::menugroup) &&
2310         currFrameContent->GetChildCount() > 0)
2311       currFrame = currFrame->PrincipalChildList().FirstChild();
2312     else if (!currFrame->GetNextSibling() &&
2313              currFrame->GetParent()->GetContent()->IsXULElement(
2314                  nsGkAtoms::menugroup))
2315       currFrame = currFrame->GetParent()->GetNextSibling();
2316     else
2317       currFrame = currFrame->GetNextSibling();
2318   }
2319 
2320   if (!aWrap) {
2321     return aStart;
2322   }
2323 
2324   currFrame = immediateParent->PrincipalChildList().FirstChild();
2325 
2326   // Still don't have anything. Try cycling from the beginning.
2327   while (currFrame && currFrame != aStart) {
2328     // See if it's a menu item.
2329     nsIContent* currFrameContent = currFrame->GetContent();
2330     if (IsValidMenuItem(currFrameContent, aIsPopup)) {
2331       return do_QueryFrame(currFrame);
2332     }
2333     if (currFrameContent->IsXULElement(nsGkAtoms::menugroup) &&
2334         currFrameContent->GetChildCount() > 0)
2335       currFrame = currFrame->PrincipalChildList().FirstChild();
2336     else if (!currFrame->GetNextSibling() &&
2337              currFrame->GetParent()->GetContent()->IsXULElement(
2338                  nsGkAtoms::menugroup))
2339       currFrame = currFrame->GetParent()->GetNextSibling();
2340     else
2341       currFrame = currFrame->GetNextSibling();
2342   }
2343 
2344   // No luck. Just return our start value.
2345   return aStart;
2346 }
2347 
GetPreviousMenuItem(nsContainerFrame * aParent,nsMenuFrame * aStart,bool aIsPopup,bool aWrap)2348 nsMenuFrame* nsXULPopupManager::GetPreviousMenuItem(nsContainerFrame* aParent,
2349                                                     nsMenuFrame* aStart,
2350                                                     bool aIsPopup, bool aWrap) {
2351   nsContainerFrame* immediateParent = ImmediateParentFrame(aParent);
2352   const nsFrameList& frames(immediateParent->PrincipalChildList());
2353 
2354   nsIFrame* currFrame = nullptr;
2355   if (aStart) {
2356     if (aStart->GetPrevSibling())
2357       currFrame = aStart->GetPrevSibling();
2358     else if (aStart->GetParent()->GetContent()->IsXULElement(
2359                  nsGkAtoms::menugroup))
2360       currFrame = aStart->GetParent()->GetPrevSibling();
2361   } else
2362     currFrame = frames.LastChild();
2363 
2364   while (currFrame) {
2365     // See if it's a menu item.
2366     nsIContent* currFrameContent = currFrame->GetContent();
2367     if (IsValidMenuItem(currFrameContent, aIsPopup)) {
2368       return do_QueryFrame(currFrame);
2369     }
2370     if (currFrameContent->IsXULElement(nsGkAtoms::menugroup) &&
2371         currFrameContent->GetChildCount() > 0) {
2372       const nsFrameList& menugroupFrames(currFrame->PrincipalChildList());
2373       currFrame = menugroupFrames.LastChild();
2374     } else if (!currFrame->GetPrevSibling() &&
2375                currFrame->GetParent()->GetContent()->IsXULElement(
2376                    nsGkAtoms::menugroup))
2377       currFrame = currFrame->GetParent()->GetPrevSibling();
2378     else
2379       currFrame = currFrame->GetPrevSibling();
2380   }
2381 
2382   if (!aWrap) {
2383     return aStart;
2384   }
2385 
2386   currFrame = frames.LastChild();
2387 
2388   // Still don't have anything. Try cycling from the end.
2389   while (currFrame && currFrame != aStart) {
2390     // See if it's a menu item.
2391     nsIContent* currFrameContent = currFrame->GetContent();
2392     if (IsValidMenuItem(currFrameContent, aIsPopup)) {
2393       return do_QueryFrame(currFrame);
2394     }
2395     if (currFrameContent->IsXULElement(nsGkAtoms::menugroup) &&
2396         currFrameContent->GetChildCount() > 0) {
2397       const nsFrameList& menugroupFrames(currFrame->PrincipalChildList());
2398       currFrame = menugroupFrames.LastChild();
2399     } else if (!currFrame->GetPrevSibling() &&
2400                currFrame->GetParent()->GetContent()->IsXULElement(
2401                    nsGkAtoms::menugroup))
2402       currFrame = currFrame->GetParent()->GetPrevSibling();
2403     else
2404       currFrame = currFrame->GetPrevSibling();
2405   }
2406 
2407   // No luck. Just return our start value.
2408   return aStart;
2409 }
2410 
IsValidMenuItem(nsIContent * aContent,bool aOnPopup)2411 bool nsXULPopupManager::IsValidMenuItem(nsIContent* aContent, bool aOnPopup) {
2412   if (aContent->IsXULElement()) {
2413     if (!aContent->IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menuitem)) {
2414       return false;
2415     }
2416   } else if (!aOnPopup || !aContent->IsHTMLElement(nsGkAtoms::option)) {
2417     return false;
2418   }
2419 
2420   nsMenuFrame* menuFrame = do_QueryFrame(aContent->GetPrimaryFrame());
2421 
2422   bool skipNavigatingDisabledMenuItem = true;
2423   if (aOnPopup &&
2424       (!menuFrame || menuFrame->GetParentMenuListType() == eNotMenuList)) {
2425     skipNavigatingDisabledMenuItem =
2426         LookAndFeel::GetInt(LookAndFeel::eIntID_SkipNavigatingDisabledMenuItem,
2427                             0) != 0;
2428   }
2429 
2430   return !(skipNavigatingDisabledMenuItem && aContent->IsElement() &&
2431            aContent->AsElement()->AttrValueIs(kNameSpaceID_None,
2432                                               nsGkAtoms::disabled,
2433                                               nsGkAtoms::_true, eCaseMatters));
2434 }
2435 
HandleEvent(nsIDOMEvent * aEvent)2436 nsresult nsXULPopupManager::HandleEvent(nsIDOMEvent* aEvent) {
2437   RefPtr<KeyboardEvent> keyEvent =
2438       aEvent->InternalDOMEvent()->AsKeyboardEvent();
2439   NS_ENSURE_TRUE(keyEvent, NS_ERROR_UNEXPECTED);
2440 
2441   // handlers shouldn't be triggered by non-trusted events.
2442   if (!keyEvent->IsTrusted()) {
2443     return NS_OK;
2444   }
2445 
2446   nsAutoString eventType;
2447   keyEvent->GetType(eventType);
2448   if (eventType.EqualsLiteral("keyup")) {
2449     return KeyUp(keyEvent);
2450   }
2451   if (eventType.EqualsLiteral("keydown")) {
2452     return KeyDown(keyEvent);
2453   }
2454   if (eventType.EqualsLiteral("keypress")) {
2455     return KeyPress(keyEvent);
2456   }
2457 
2458   MOZ_ASSERT_UNREACHABLE("Unexpected eventType");
2459   return NS_OK;
2460 }
2461 
UpdateIgnoreKeys(bool aIgnoreKeys)2462 nsresult nsXULPopupManager::UpdateIgnoreKeys(bool aIgnoreKeys) {
2463   nsMenuChainItem* item = GetTopVisibleMenu();
2464   if (item) {
2465     item->SetIgnoreKeys(aIgnoreKeys ? eIgnoreKeys_True : eIgnoreKeys_Shortcuts);
2466   }
2467   UpdateKeyboardListeners();
2468   return NS_OK;
2469 }
2470 
KeyUp(KeyboardEvent * aKeyEvent)2471 nsresult nsXULPopupManager::KeyUp(KeyboardEvent* aKeyEvent) {
2472   // don't do anything if a menu isn't open or a menubar isn't active
2473   if (!mActiveMenuBar) {
2474     nsMenuChainItem* item = GetTopVisibleMenu();
2475     if (!item || item->PopupType() != ePopupTypeMenu) return NS_OK;
2476 
2477     if (item->IgnoreKeys() == eIgnoreKeys_Shortcuts) {
2478       aKeyEvent->StopCrossProcessForwarding();
2479       return NS_OK;
2480     }
2481   }
2482 
2483   aKeyEvent->StopPropagation();
2484   aKeyEvent->StopCrossProcessForwarding();
2485   aKeyEvent->PreventDefault();
2486 
2487   return NS_OK;  // I am consuming event
2488 }
2489 
KeyDown(KeyboardEvent * aKeyEvent)2490 nsresult nsXULPopupManager::KeyDown(KeyboardEvent* aKeyEvent) {
2491   nsMenuChainItem* item = GetTopVisibleMenu();
2492   if (item && item->Frame()->IsMenuLocked()) return NS_OK;
2493 
2494   if (HandleKeyboardEventWithKeyCode(aKeyEvent, item)) {
2495     return NS_OK;
2496   }
2497 
2498   // don't do anything if a menu isn't open or a menubar isn't active
2499   if (!mActiveMenuBar && (!item || item->PopupType() != ePopupTypeMenu))
2500     return NS_OK;
2501 
2502   // Since a menu was open, stop propagation of the event to keep other event
2503   // listeners from becoming confused.
2504   if (!item || item->IgnoreKeys() != eIgnoreKeys_Shortcuts) {
2505     aKeyEvent->StopPropagation();
2506   }
2507 
2508   int32_t menuAccessKey = -1;
2509 
2510   // If the key just pressed is the access key (usually Alt),
2511   // dismiss and unfocus the menu.
2512 
2513   nsMenuBarListener::GetMenuAccessKey(&menuAccessKey);
2514   if (menuAccessKey) {
2515     uint32_t theChar = aKeyEvent->KeyCode();
2516 
2517     if (theChar == (uint32_t)menuAccessKey) {
2518       bool ctrl = (menuAccessKey != KeyboardEventBinding::DOM_VK_CONTROL &&
2519                    aKeyEvent->CtrlKey());
2520       bool alt = (menuAccessKey != KeyboardEventBinding::DOM_VK_ALT &&
2521                   aKeyEvent->AltKey());
2522       bool shift = (menuAccessKey != KeyboardEventBinding::DOM_VK_SHIFT &&
2523                     aKeyEvent->ShiftKey());
2524       bool meta = (menuAccessKey != KeyboardEventBinding::DOM_VK_META &&
2525                    aKeyEvent->MetaKey());
2526       if (!(ctrl || alt || shift || meta)) {
2527         // The access key just went down and no other
2528         // modifiers are already down.
2529         nsMenuChainItem* item = GetTopVisibleMenu();
2530         if (item && !item->Frame()->IsMenuList()) {
2531           Rollup(0, false, nullptr, nullptr);
2532         } else if (mActiveMenuBar) {
2533           mActiveMenuBar->MenuClosed();
2534         }
2535 
2536         // Clear the item to avoid bugs as it may have been deleted during
2537         // rollup.
2538         item = nullptr;
2539       }
2540       aKeyEvent->StopPropagation();
2541       aKeyEvent->PreventDefault();
2542     }
2543   }
2544 
2545   aKeyEvent->AsEvent()->StopCrossProcessForwarding();
2546   return NS_OK;
2547 }
2548 
KeyPress(KeyboardEvent * aKeyEvent)2549 nsresult nsXULPopupManager::KeyPress(KeyboardEvent* aKeyEvent) {
2550   // Don't check prevent default flag -- menus always get first shot at key
2551   // events.
2552 
2553   nsMenuChainItem* item = GetTopVisibleMenu();
2554   if (item &&
2555       (item->Frame()->IsMenuLocked() || item->PopupType() != ePopupTypeMenu)) {
2556     return NS_OK;
2557   }
2558 
2559   // if a menu is open or a menubar is active, it consumes the key event
2560   bool consume = (item || mActiveMenuBar);
2561 
2562   WidgetInputEvent* evt = aKeyEvent->WidgetEventPtr()->AsInputEvent();
2563   bool isAccel = evt && evt->IsAccel();
2564 
2565   // When ignorekeys="shortcuts" is used, we don't call preventDefault on the
2566   // key event when the accelerator key is pressed. This allows another
2567   // listener to handle keys. For instance, this allows global shortcuts to
2568   // still apply while a menu is open.
2569   if (item && item->IgnoreKeys() == eIgnoreKeys_Shortcuts && isAccel) {
2570     consume = false;
2571   }
2572 
2573   HandleShortcutNavigation(aKeyEvent, nullptr);
2574 
2575   aKeyEvent->StopCrossProcessForwarding();
2576   if (consume) {
2577     aKeyEvent->StopPropagation();
2578     aKeyEvent->PreventDefault();
2579   }
2580 
2581   return NS_OK;  // I am consuming event
2582 }
2583 
2584 NS_IMETHODIMP
Run()2585 nsXULPopupShowingEvent::Run() {
2586   nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
2587   if (pm) {
2588     pm->FirePopupShowingEvent(mPopup, mIsContextMenu, mSelectFirstItem,
2589                               nullptr);
2590   }
2591 
2592   return NS_OK;
2593 }
2594 
2595 NS_IMETHODIMP
Run()2596 nsXULPopupHidingEvent::Run() {
2597   nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
2598 
2599   nsIDocument* document = mPopup->GetUncomposedDoc();
2600   if (pm && document) {
2601     nsPresContext* context = document->GetPresContext();
2602     if (context) {
2603       pm->FirePopupHidingEvent(mPopup, mNextPopup, mLastPopup, context,
2604                                mPopupType, mDeselectMenu, mIsRollup);
2605     }
2606   }
2607 
2608   return NS_OK;
2609 }
2610 
DispatchIfNeeded(nsIContent * aPopup,bool aIsContextMenu,bool aSelectFirstItem)2611 bool nsXULPopupPositionedEvent::DispatchIfNeeded(nsIContent* aPopup,
2612                                                  bool aIsContextMenu,
2613                                                  bool aSelectFirstItem) {
2614   // The popuppositioned event only fires on arrow panels for now.
2615   if (aPopup->IsElement() &&
2616       aPopup->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
2617                                        nsGkAtoms::arrow, eCaseMatters)) {
2618     nsCOMPtr<nsIRunnable> event =
2619         new nsXULPopupPositionedEvent(aPopup, aIsContextMenu, aSelectFirstItem);
2620     aPopup->OwnerDoc()->Dispatch(TaskCategory::Other, event.forget());
2621 
2622     return true;
2623   }
2624 
2625   return false;
2626 }
2627 
2628 NS_IMETHODIMP
Run()2629 nsXULPopupPositionedEvent::Run() {
2630   nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
2631   if (pm) {
2632     nsMenuPopupFrame* popupFrame = do_QueryFrame(mPopup->GetPrimaryFrame());
2633     if (popupFrame) {
2634       // At this point, hidePopup may have been called but it currently has no
2635       // way to stop this event. However, if hidePopup was called, the popup
2636       // will now be in the hiding or closed state. If we are in the shown or
2637       // positioning state instead, we can assume that we are still clear to
2638       // open/move the popup
2639       nsPopupState state = popupFrame->PopupState();
2640       if (state != ePopupPositioning && state != ePopupShown) {
2641         return NS_OK;
2642       }
2643       nsEventStatus status = nsEventStatus_eIgnore;
2644       WidgetMouseEvent event(true, eXULPopupPositioned, nullptr,
2645                              WidgetMouseEvent::eReal);
2646       EventDispatcher::Dispatch(mPopup, popupFrame->PresContext(), &event,
2647                                 nullptr, &status);
2648 
2649       // Get the popup frame and make sure it is still in the positioning
2650       // state. If it isn't, someone may have tried to reshow or hide it
2651       // during the popuppositioned event.
2652       // Alternately, this event may have been fired in reponse to moving the
2653       // popup rather than opening it. In that case, we are done.
2654       nsMenuPopupFrame* popupFrame = do_QueryFrame(mPopup->GetPrimaryFrame());
2655       if (popupFrame && popupFrame->PopupState() == ePopupPositioning) {
2656         pm->ShowPopupCallback(mPopup, popupFrame, mIsContextMenu,
2657                               mSelectFirstItem);
2658       }
2659     }
2660   }
2661 
2662   return NS_OK;
2663 }
2664 
2665 NS_IMETHODIMP
Run()2666 nsXULMenuCommandEvent::Run() {
2667   nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
2668   if (!pm) return NS_OK;
2669 
2670   // The order of the nsViewManager and nsIPresShell COM pointers is
2671   // important below.  We want the pres shell to get released before the
2672   // associated view manager on exit from this function.
2673   // See bug 54233.
2674   // XXXndeakin is this still needed?
2675 
2676   nsCOMPtr<nsIContent> popup;
2677   nsMenuFrame* menuFrame = do_QueryFrame(mMenu->GetPrimaryFrame());
2678   AutoWeakFrame weakFrame(menuFrame);
2679   if (menuFrame && mFlipChecked) {
2680     if (menuFrame->IsChecked()) {
2681       mMenu->UnsetAttr(kNameSpaceID_None, nsGkAtoms::checked, true);
2682     } else {
2683       mMenu->SetAttr(kNameSpaceID_None, nsGkAtoms::checked,
2684                      NS_LITERAL_STRING("true"), true);
2685     }
2686   }
2687 
2688   if (menuFrame && weakFrame.IsAlive()) {
2689     // Find the popup that the menu is inside. Below, this popup will
2690     // need to be hidden.
2691     nsIFrame* frame = menuFrame->GetParent();
2692     while (frame) {
2693       nsMenuPopupFrame* popupFrame = do_QueryFrame(frame);
2694       if (popupFrame) {
2695         popup = popupFrame->GetContent();
2696         break;
2697       }
2698       frame = frame->GetParent();
2699     }
2700 
2701     nsPresContext* presContext = menuFrame->PresContext();
2702     nsCOMPtr<nsIPresShell> shell = presContext->PresShell();
2703     RefPtr<nsViewManager> kungFuDeathGrip = shell->GetViewManager();
2704     mozilla::Unused
2705         << kungFuDeathGrip;  // Not referred to directly within this function
2706 
2707     // Deselect ourselves.
2708     if (mCloseMenuMode != CloseMenuMode_None) menuFrame->SelectMenu(false);
2709 
2710     AutoHandlingUserInputStatePusher userInpStatePusher(mUserInput, nullptr,
2711                                                         shell->GetDocument());
2712     nsContentUtils::DispatchXULCommand(mMenu, mIsTrusted, nullptr, shell,
2713                                        mControl, mAlt, mShift, mMeta);
2714   }
2715 
2716   if (popup && mCloseMenuMode != CloseMenuMode_None)
2717     pm->HidePopup(popup, mCloseMenuMode == CloseMenuMode_Auto, true, false,
2718                   false);
2719 
2720   return NS_OK;
2721 }
2722