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