Bug 1675375 Part 7: Update expectations in helper_hittest_clippath.html. r=botond
[gecko.git] / layout / xul / nsXULPopupManager.cpp
blobdd7d6a6c250c3d958116872ea065920c7932a7ea
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "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_browser.h"
52 #include "mozilla/StaticPrefs_ui.h"
53 #include "mozilla/StaticPrefs_widget.h"
54 #include "mozilla/StaticPrefs_xul.h"
55 #include "mozilla/widget/nsAutoRollup.h"
56 #ifdef XP_MACOSX
57 # include "mozilla/widget/NativeMenuSupport.h"
58 #endif
60 using namespace mozilla;
61 using namespace mozilla::dom;
62 using mozilla::widget::NativeMenu;
64 static_assert(KeyboardEvent_Binding::DOM_VK_HOME ==
65 KeyboardEvent_Binding::DOM_VK_END + 1 &&
66 KeyboardEvent_Binding::DOM_VK_LEFT ==
67 KeyboardEvent_Binding::DOM_VK_END + 2 &&
68 KeyboardEvent_Binding::DOM_VK_UP ==
69 KeyboardEvent_Binding::DOM_VK_END + 3 &&
70 KeyboardEvent_Binding::DOM_VK_RIGHT ==
71 KeyboardEvent_Binding::DOM_VK_END + 4 &&
72 KeyboardEvent_Binding::DOM_VK_DOWN ==
73 KeyboardEvent_Binding::DOM_VK_END + 5,
74 "nsXULPopupManager assumes some keyCode values are consecutive");
76 const nsNavigationDirection DirectionFromKeyCodeTable[2][6] = {
78 eNavigationDirection_Last, // KeyboardEvent_Binding::DOM_VK_END
79 eNavigationDirection_First, // KeyboardEvent_Binding::DOM_VK_HOME
80 eNavigationDirection_Start, // KeyboardEvent_Binding::DOM_VK_LEFT
81 eNavigationDirection_Before, // KeyboardEvent_Binding::DOM_VK_UP
82 eNavigationDirection_End, // KeyboardEvent_Binding::DOM_VK_RIGHT
83 eNavigationDirection_After // KeyboardEvent_Binding::DOM_VK_DOWN
86 eNavigationDirection_Last, // KeyboardEvent_Binding::DOM_VK_END
87 eNavigationDirection_First, // KeyboardEvent_Binding::DOM_VK_HOME
88 eNavigationDirection_End, // KeyboardEvent_Binding::DOM_VK_LEFT
89 eNavigationDirection_Before, // KeyboardEvent_Binding::DOM_VK_UP
90 eNavigationDirection_Start, // KeyboardEvent_Binding::DOM_VK_RIGHT
91 eNavigationDirection_After // KeyboardEvent_Binding::DOM_VK_DOWN
92 }};
94 nsXULPopupManager* nsXULPopupManager::sInstance = nullptr;
96 nsIContent* nsMenuChainItem::Content() { return mFrame->GetContent(); }
98 void nsMenuChainItem::SetParent(nsMenuChainItem* aParent) {
99 if (mParent) {
100 NS_ASSERTION(mParent->mChild == this,
101 "Unexpected - parent's child not set to this");
102 mParent->mChild = nullptr;
104 mParent = aParent;
105 if (mParent) {
106 if (mParent->mChild) mParent->mChild->mParent = nullptr;
107 mParent->mChild = this;
111 void nsMenuChainItem::Detach(nsMenuChainItem** aRoot) {
112 // If the item has a child, set the child's parent to this item's parent,
113 // effectively removing the item from the chain. If the item has no child,
114 // just set the parent to null.
115 if (mChild) {
116 NS_ASSERTION(this != *aRoot,
117 "Unexpected - popup with child at end of chain");
118 mChild->SetParent(mParent);
119 } else {
120 // An item without a child should be the first item in the chain, so set
121 // the first item pointer, pointed to by aRoot, to the parent.
122 NS_ASSERTION(this == *aRoot,
123 "Unexpected - popup with no child not at end of chain");
124 *aRoot = mParent;
125 SetParent(nullptr);
129 void nsMenuChainItem::UpdateFollowAnchor() {
130 mFollowAnchor = mFrame->ShouldFollowAnchor(mCurrentRect);
133 void nsMenuChainItem::CheckForAnchorChange() {
134 if (mFollowAnchor) {
135 mFrame->CheckForAnchorChange(mCurrentRect);
139 NS_IMPL_ISUPPORTS(nsXULPopupManager, nsIDOMEventListener, nsIObserver)
141 nsXULPopupManager::nsXULPopupManager()
142 : mRangeOffset(0),
143 mCachedMousePoint(0, 0),
144 mCachedModifiers(0),
145 mActiveMenuBar(nullptr),
146 mPopups(nullptr),
147 mTimerMenu(nullptr) {
148 nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
149 if (obs) {
150 obs->AddObserver(this, "xpcom-shutdown", false);
154 nsXULPopupManager::~nsXULPopupManager() {
155 NS_ASSERTION(!mPopups, "XUL popups still open");
157 if (mNativeMenu) {
158 mNativeMenu->RemoveObserver(this);
162 nsresult nsXULPopupManager::Init() {
163 sInstance = new nsXULPopupManager();
164 NS_ENSURE_TRUE(sInstance, NS_ERROR_OUT_OF_MEMORY);
165 NS_ADDREF(sInstance);
166 return NS_OK;
169 void nsXULPopupManager::Shutdown() { NS_IF_RELEASE(sInstance); }
171 NS_IMETHODIMP
172 nsXULPopupManager::Observe(nsISupports* aSubject, const char* aTopic,
173 const char16_t* aData) {
174 if (!nsCRT::strcmp(aTopic, "xpcom-shutdown")) {
175 if (mKeyListener) {
176 mKeyListener->RemoveEventListener(u"keypress"_ns, this, true);
177 mKeyListener->RemoveEventListener(u"keydown"_ns, this, true);
178 mKeyListener->RemoveEventListener(u"keyup"_ns, this, true);
179 mKeyListener = nullptr;
181 mRangeParentContent = nullptr;
182 // mOpeningPopup is cleared explicitly soon after using it.
183 nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
184 if (obs) {
185 obs->RemoveObserver(this, "xpcom-shutdown");
189 return NS_OK;
192 nsXULPopupManager* nsXULPopupManager::GetInstance() {
193 MOZ_ASSERT(sInstance);
194 return sInstance;
197 bool nsXULPopupManager::RollupNativeMenu() {
198 if (mNativeMenu) {
199 RefPtr<NativeMenu> menu = mNativeMenu;
200 return menu->Close();
202 return false;
205 bool nsXULPopupManager::Rollup(uint32_t aCount, bool aFlush,
206 const nsIntPoint* pos,
207 nsIContent** aLastRolledUp) {
208 if (aLastRolledUp) {
209 *aLastRolledUp = nullptr;
212 // We can disable the autohide behavior via a pref to ease debugging.
213 if (StaticPrefs::ui_popup_disable_autohide()) {
214 // Required on linux to allow events to work on other targets.
215 if (mWidget) {
216 mWidget->CaptureRollupEvents(nullptr, false);
218 return false;
221 bool consume = false;
223 nsMenuChainItem* item = GetTopVisibleMenu();
224 if (item) {
225 if (aLastRolledUp) {
226 // We need to get the popup that will be closed last, so that widget can
227 // keep track of it so it doesn't reopen if a mousedown event is going to
228 // processed. Keep going up the menu chain to get the first level menu of
229 // the same type. If a different type is encountered it means we have,
230 // for example, a menulist or context menu inside a panel, and we want to
231 // treat these as distinct. It's possible that this menu doesn't end up
232 // closing because the popuphiding event was cancelled, but in that case
233 // we don't need to deal with the menu reopening as it will already still
234 // be open.
235 nsMenuChainItem* first = item;
236 while (first->GetParent()) {
237 nsMenuChainItem* parent = first->GetParent();
238 if (first->Frame()->PopupType() != parent->Frame()->PopupType() ||
239 first->IsContextMenu() != parent->IsContextMenu()) {
240 break;
242 first = parent;
245 *aLastRolledUp = first->Content();
248 ConsumeOutsideClicksResult consumeResult =
249 item->Frame()->ConsumeOutsideClicks();
250 consume = (consumeResult == ConsumeOutsideClicks_True);
252 bool rollup = true;
254 // If norolluponanchor is true, then don't rollup when clicking the anchor.
255 // This would be used to allow adjusting the caret position in an
256 // autocomplete field without hiding the popup for example.
257 bool noRollupOnAnchor =
258 (!consume && pos &&
259 item->Frame()->GetContent()->AsElement()->AttrValueIs(
260 kNameSpaceID_None, nsGkAtoms::norolluponanchor, nsGkAtoms::_true,
261 eCaseMatters));
263 // When ConsumeOutsideClicks_ParentOnly is used, always consume the click
264 // when the click was over the anchor. This way, clicking on a menu doesn't
265 // reopen the menu.
266 if ((consumeResult == ConsumeOutsideClicks_ParentOnly ||
267 noRollupOnAnchor) &&
268 pos) {
269 nsMenuPopupFrame* popupFrame = item->Frame();
270 CSSIntRect anchorRect;
271 if (popupFrame->IsAnchored()) {
272 // Check if the popup has a screen anchor rectangle. If not, get the
273 // rectangle from the anchor element.
274 anchorRect =
275 CSSIntRect::FromUnknownRect(popupFrame->GetScreenAnchorRect());
276 if (anchorRect.x == -1 || anchorRect.y == -1) {
277 nsCOMPtr<nsIContent> anchor = popupFrame->GetAnchor();
279 // Check if the anchor has indicated another node to use for checking
280 // for roll-up. That way, we can anchor a popup on anonymous content
281 // or an individual icon, while clicking elsewhere within a button or
282 // other container doesn't result in us re-opening the popup.
283 if (anchor && anchor->IsElement()) {
284 nsAutoString consumeAnchor;
285 anchor->AsElement()->GetAttr(
286 kNameSpaceID_None, nsGkAtoms::consumeanchor, consumeAnchor);
287 if (!consumeAnchor.IsEmpty()) {
288 Document* doc = anchor->GetOwnerDocument();
289 nsIContent* newAnchor = doc->GetElementById(consumeAnchor);
290 if (newAnchor) {
291 anchor = newAnchor;
296 if (anchor && anchor->GetPrimaryFrame()) {
297 anchorRect = anchor->GetPrimaryFrame()->GetScreenRect();
302 // It's possible that some other element is above the anchor at the same
303 // position, but the only thing that would happen is that the mouse
304 // event will get consumed, so here only a quick coordinates check is
305 // done rather than a slower complete check of what is at that location.
306 nsPresContext* presContext = item->Frame()->PresContext();
307 CSSIntPoint posCSSPixels(presContext->DevPixelsToIntCSSPixels(pos->x),
308 presContext->DevPixelsToIntCSSPixels(pos->y));
309 if (anchorRect.Contains(posCSSPixels)) {
310 if (consumeResult == ConsumeOutsideClicks_ParentOnly) {
311 consume = true;
314 if (noRollupOnAnchor) {
315 rollup = false;
320 if (rollup) {
321 // if a number of popups to close has been specified, determine the last
322 // popup to close
323 nsIContent* lastPopup = nullptr;
324 if (aCount != UINT32_MAX) {
325 nsMenuChainItem* last = item;
326 while (--aCount && last->GetParent()) {
327 last = last->GetParent();
329 if (last) {
330 lastPopup = last->Content();
334 nsPresContext* presContext = item->Frame()->PresContext();
335 RefPtr<nsViewManager> viewManager =
336 presContext->PresShell()->GetViewManager();
338 HidePopup(item->Content(), true, true, false, true, lastPopup);
340 if (aFlush) {
341 // The popup's visibility doesn't update until the minimize animation
342 // has finished, so call UpdateWidgetGeometry to update it right away.
343 viewManager->UpdateWidgetGeometry();
348 return consume;
351 ////////////////////////////////////////////////////////////////////////
352 bool nsXULPopupManager::ShouldRollupOnMouseWheelEvent() {
353 // should rollup only for autocomplete widgets
354 // XXXndeakin this should really be something the popup has more control over
356 nsMenuChainItem* item = GetTopVisibleMenu();
357 if (!item) return false;
359 nsIContent* content = item->Frame()->GetContent();
360 if (!content || !content->IsElement()) return false;
362 Element* element = content->AsElement();
363 if (element->AttrValueIs(kNameSpaceID_None, nsGkAtoms::rolluponmousewheel,
364 nsGkAtoms::_true, eCaseMatters))
365 return true;
367 if (element->AttrValueIs(kNameSpaceID_None, nsGkAtoms::rolluponmousewheel,
368 nsGkAtoms::_false, eCaseMatters))
369 return false;
371 nsAutoString value;
372 element->GetAttr(kNameSpaceID_None, nsGkAtoms::type, value);
373 return StringBeginsWith(value, u"autocomplete"_ns);
376 bool nsXULPopupManager::ShouldConsumeOnMouseWheelEvent() {
377 nsMenuChainItem* item = GetTopVisibleMenu();
378 if (!item) return false;
380 nsMenuPopupFrame* frame = item->Frame();
381 if (frame->PopupType() != ePopupTypePanel) return true;
383 return !frame->GetContent()->AsElement()->AttrValueIs(
384 kNameSpaceID_None, nsGkAtoms::type, nsGkAtoms::arrow, eCaseMatters);
387 // a menu should not roll up if activated by a mouse activate message (eg.
388 // X-mouse)
389 bool nsXULPopupManager::ShouldRollupOnMouseActivate() { return false; }
391 uint32_t nsXULPopupManager::GetSubmenuWidgetChain(
392 nsTArray<nsIWidget*>* aWidgetChain) {
393 // this method is used by the widget code to determine the list of popups
394 // that are open. If a mouse click occurs outside one of these popups, the
395 // panels will roll up. If the click is inside a popup, they will not roll up
396 uint32_t count = 0, sameTypeCount = 0;
398 NS_ASSERTION(aWidgetChain, "null parameter");
399 nsMenuChainItem* item = GetTopVisibleMenu();
400 while (item) {
401 nsMenuChainItem* parent = item->GetParent();
402 if (!item->IsNoAutoHide()) {
403 nsCOMPtr<nsIWidget> widget = item->Frame()->GetWidget();
404 NS_ASSERTION(widget, "open popup has no widget");
405 aWidgetChain->AppendElement(widget.get());
406 // In the case when a menulist inside a panel is open, clicking in the
407 // panel should still roll up the menu, so if a different type is found,
408 // stop scanning.
409 if (!sameTypeCount) {
410 count++;
411 if (!parent ||
412 item->Frame()->PopupType() != parent->Frame()->PopupType() ||
413 item->IsContextMenu() != parent->IsContextMenu()) {
414 sameTypeCount = count;
419 item = parent;
422 return sameTypeCount;
425 nsIWidget* nsXULPopupManager::GetRollupWidget() {
426 nsMenuChainItem* item = GetTopVisibleMenu();
427 return item ? item->Frame()->GetWidget() : nullptr;
430 void nsXULPopupManager::AdjustPopupsOnWindowChange(
431 nsPIDOMWindowOuter* aWindow) {
432 // When the parent window is moved, adjust any child popups. Dismissable
433 // menus and panels are expected to roll up when a window is moved, so there
434 // is no need to check these popups, only the noautohide popups.
436 // The items are added to a list so that they can be adjusted bottom to top.
437 nsTArray<nsMenuPopupFrame*> list;
439 nsMenuChainItem* item = mPopups;
440 while (item) {
441 // only move popups that are within the same window and where auto
442 // positioning has not been disabled
443 nsMenuPopupFrame* frame = item->Frame();
444 if (item->IsNoAutoHide() && frame->GetAutoPosition()) {
445 nsIContent* popup = frame->GetContent();
446 if (popup) {
447 Document* document = popup->GetUncomposedDoc();
448 if (document) {
449 if (nsPIDOMWindowOuter* window = document->GetWindow()) {
450 window = window->GetPrivateRoot();
451 if (window == aWindow) {
452 list.AppendElement(frame);
459 item = item->GetParent();
462 for (int32_t l = list.Length() - 1; l >= 0; l--) {
463 list[l]->SetPopupPosition(nullptr, true, false);
467 void nsXULPopupManager::AdjustPopupsOnWindowChange(PresShell* aPresShell) {
468 if (aPresShell->GetDocument()) {
469 AdjustPopupsOnWindowChange(aPresShell->GetDocument()->GetWindow());
473 static nsMenuPopupFrame* GetPopupToMoveOrResize(nsIFrame* aFrame) {
474 nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(aFrame);
475 if (!menuPopupFrame) return nullptr;
477 // no point moving or resizing hidden popups
478 if (!menuPopupFrame->IsVisible()) return nullptr;
480 nsIWidget* widget = menuPopupFrame->GetWidget();
481 if (widget && !widget->IsVisible()) return nullptr;
483 return menuPopupFrame;
486 void nsXULPopupManager::PopupMoved(nsIFrame* aFrame, nsIntPoint aPnt) {
487 nsMenuPopupFrame* menuPopupFrame = GetPopupToMoveOrResize(aFrame);
488 if (!menuPopupFrame) return;
490 nsView* view = menuPopupFrame->GetView();
491 if (!view) return;
493 // Don't do anything if the popup is already at the specified location. This
494 // prevents recursive calls when a popup is positioned.
495 LayoutDeviceIntRect curDevSize = view->CalcWidgetBounds(eWindowType_popup);
496 nsIWidget* widget = menuPopupFrame->GetWidget();
497 if (curDevSize.x == aPnt.x && curDevSize.y == aPnt.y &&
498 (!widget ||
499 widget->GetClientOffset() == menuPopupFrame->GetLastClientOffset())) {
500 return;
503 // Update the popup's position using SetPopupPosition if the popup is
504 // anchored and at the parent level as these maintain their position
505 // relative to the parent window. Otherwise, just update the popup to
506 // the specified screen coordinates.
507 if (menuPopupFrame->IsAnchored() &&
508 menuPopupFrame->PopupLevel() == ePopupLevelParent) {
509 menuPopupFrame->SetPopupPosition(nullptr, true, false);
510 } else {
511 CSSPoint cssPos = LayoutDeviceIntPoint::FromUnknownPoint(aPnt) /
512 menuPopupFrame->PresContext()->CSSToDevPixelScale();
513 menuPopupFrame->MoveTo(RoundedToInt(cssPos), false);
517 void nsXULPopupManager::PopupResized(nsIFrame* aFrame,
518 LayoutDeviceIntSize aSize) {
519 nsMenuPopupFrame* menuPopupFrame = GetPopupToMoveOrResize(aFrame);
520 if (!menuPopupFrame) return;
522 nsView* view = menuPopupFrame->GetView();
523 if (!view) return;
525 LayoutDeviceIntRect curDevSize = view->CalcWidgetBounds(eWindowType_popup);
526 // If the size is what we think it is, we have nothing to do.
527 if (curDevSize.width == aSize.width && curDevSize.height == aSize.height)
528 return;
530 Element* popup = menuPopupFrame->GetContent()->AsElement();
532 // Only set the width and height if the popup already has these attributes.
533 if (!popup->HasAttr(kNameSpaceID_None, nsGkAtoms::width) ||
534 !popup->HasAttr(kNameSpaceID_None, nsGkAtoms::height)) {
535 return;
538 // The size is different. Convert the actual size to css pixels and store it
539 // as 'width' and 'height' attributes on the popup.
540 nsPresContext* presContext = menuPopupFrame->PresContext();
542 CSSIntSize newCSS(presContext->DevPixelsToIntCSSPixels(aSize.width),
543 presContext->DevPixelsToIntCSSPixels(aSize.height));
545 nsAutoString width, height;
546 width.AppendInt(newCSS.width);
547 height.AppendInt(newCSS.height);
548 popup->SetAttr(kNameSpaceID_None, nsGkAtoms::width, width, false);
549 popup->SetAttr(kNameSpaceID_None, nsGkAtoms::height, height, true);
552 nsMenuPopupFrame* nsXULPopupManager::GetPopupFrameForContent(
553 nsIContent* aContent, bool aShouldFlush) {
554 if (aShouldFlush) {
555 Document* document = aContent->GetUncomposedDoc();
556 if (document) {
557 if (RefPtr<PresShell> presShell = document->GetPresShell()) {
558 presShell->FlushPendingNotifications(FlushType::Layout);
563 return do_QueryFrame(aContent->GetPrimaryFrame());
566 nsMenuChainItem* nsXULPopupManager::GetTopVisibleMenu() {
567 nsMenuChainItem* item = mPopups;
568 while (item) {
569 if (!item->IsNoAutoHide() &&
570 item->Frame()->PopupState() != ePopupInvisible) {
571 return item;
573 item = item->GetParent();
576 return nullptr;
579 void nsXULPopupManager::InitTriggerEvent(Event* aEvent, nsIContent* aPopup,
580 nsIContent** aTriggerContent) {
581 mCachedMousePoint = LayoutDeviceIntPoint(0, 0);
583 if (aTriggerContent) {
584 *aTriggerContent = nullptr;
585 if (aEvent) {
586 // get the trigger content from the event
587 nsCOMPtr<nsIContent> target = do_QueryInterface(aEvent->GetTarget());
588 target.forget(aTriggerContent);
592 mCachedModifiers = 0;
594 RefPtr<UIEvent> uiEvent = aEvent ? aEvent->AsUIEvent() : nullptr;
595 if (uiEvent) {
596 mRangeOffset = -1;
597 mRangeParentContent =
598 uiEvent->GetRangeParentContentAndOffset(&mRangeOffset);
600 // get the event coordinates relative to the root frame of the document
601 // containing the popup.
602 NS_ASSERTION(aPopup, "Expected a popup node");
603 WidgetEvent* event = aEvent->WidgetEventPtr();
604 if (event) {
605 WidgetInputEvent* inputEvent = event->AsInputEvent();
606 if (inputEvent) {
607 mCachedModifiers = inputEvent->mModifiers;
609 Document* doc = aPopup->GetUncomposedDoc();
610 if (doc) {
611 PresShell* presShell = doc->GetPresShell();
612 nsPresContext* presContext;
613 if (presShell && (presContext = presShell->GetPresContext())) {
614 nsPresContext* rootDocPresContext = presContext->GetRootPresContext();
615 if (!rootDocPresContext) return;
616 nsIFrame* rootDocumentRootFrame =
617 rootDocPresContext->PresShell()->GetRootFrame();
618 if ((event->mClass == eMouseEventClass ||
619 event->mClass == eMouseScrollEventClass ||
620 event->mClass == eWheelEventClass) &&
621 !event->AsGUIEvent()->mWidget) {
622 // no widget, so just use the client point if available
623 MouseEvent* mouseEvent = aEvent->AsMouseEvent();
624 nsIntPoint clientPt(mouseEvent->ClientX(), mouseEvent->ClientY());
626 // XXX this doesn't handle IFRAMEs in transforms
627 nsPoint thisDocToRootDocOffset =
628 presShell->GetRootFrame()->GetOffsetToCrossDoc(
629 rootDocumentRootFrame);
630 // convert to device pixels
631 mCachedMousePoint.x = presContext->AppUnitsToDevPixels(
632 nsPresContext::CSSPixelsToAppUnits(clientPt.x) +
633 thisDocToRootDocOffset.x);
634 mCachedMousePoint.y = presContext->AppUnitsToDevPixels(
635 nsPresContext::CSSPixelsToAppUnits(clientPt.y) +
636 thisDocToRootDocOffset.y);
637 } else if (rootDocumentRootFrame) {
638 nsPoint pnt = nsLayoutUtils::GetEventCoordinatesRelativeTo(
639 event, RelativeTo{rootDocumentRootFrame});
640 mCachedMousePoint = LayoutDeviceIntPoint(
641 rootDocPresContext->AppUnitsToDevPixels(pnt.x),
642 rootDocPresContext->AppUnitsToDevPixels(pnt.y));
647 } else {
648 mRangeParentContent = nullptr;
649 mRangeOffset = 0;
653 void nsXULPopupManager::SetActiveMenuBar(nsMenuBarFrame* aMenuBar,
654 bool aActivate) {
655 if (aActivate)
656 mActiveMenuBar = aMenuBar;
657 else if (mActiveMenuBar == aMenuBar)
658 mActiveMenuBar = nullptr;
660 UpdateKeyboardListeners();
663 void nsXULPopupManager::ShowMenu(nsIContent* aMenu, bool aSelectFirstItem,
664 bool aAsynchronous) {
665 if (mNativeMenu && aMenu->IsElement() &&
666 mNativeMenu->Element()->Contains(aMenu)) {
667 mNativeMenu->OpenSubmenu(aMenu->AsElement());
668 return;
671 nsMenuFrame* menuFrame = do_QueryFrame(aMenu->GetPrimaryFrame());
672 if (!menuFrame || !menuFrame->IsMenu()) return;
674 nsMenuPopupFrame* popupFrame = menuFrame->GetPopup();
675 if (!popupFrame || !MayShowPopup(popupFrame)) return;
677 // inherit whether or not we're a context menu from the parent
678 bool parentIsContextMenu = false;
679 bool onMenuBar = false;
680 bool onmenu = menuFrame->IsOnMenu();
682 nsMenuParent* parent = menuFrame->GetMenuParent();
683 if (parent && onmenu) {
684 parentIsContextMenu = parent->IsContextMenu();
685 onMenuBar = parent->IsMenuBar();
688 nsAutoString position;
690 #ifdef XP_MACOSX
691 if (aMenu->IsXULElement(nsGkAtoms::menulist)) {
692 position.AssignLiteral("selection");
693 } else
694 #endif
696 if (onMenuBar || !onmenu)
697 position.AssignLiteral("after_start");
698 else
699 position.AssignLiteral("end_before");
701 // there is no trigger event for menus
702 InitTriggerEvent(nullptr, nullptr, nullptr);
703 popupFrame->InitializePopup(aMenu, nullptr, position, 0, 0,
704 MenuPopupAnchorType_Node, true);
706 nsCOMPtr<nsIContent> popupContent = popupFrame->GetContent();
707 if (aAsynchronous) {
708 nsCOMPtr<nsIRunnable> event =
709 NS_NewRunnableFunction("BeginShowingPopup", [=]() {
710 nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
711 if (pm) {
712 pm->BeginShowingPopup(popupContent, parentIsContextMenu,
713 aSelectFirstItem, nullptr);
716 aMenu->OwnerDoc()->Dispatch(TaskCategory::Other, event.forget());
717 } else {
718 BeginShowingPopup(popupContent, parentIsContextMenu, aSelectFirstItem,
719 nullptr);
723 void nsXULPopupManager::ShowPopup(nsIContent* aPopup,
724 nsIContent* aAnchorContent,
725 const nsAString& aPosition, int32_t aXPos,
726 int32_t aYPos, bool aIsContextMenu,
727 bool aAttributesOverride,
728 bool aSelectFirstItem, Event* aTriggerEvent) {
729 nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(aPopup, true);
730 if (!popupFrame || !MayShowPopup(popupFrame)) return;
732 nsCOMPtr<nsIContent> triggerContent;
733 InitTriggerEvent(aTriggerEvent, aPopup, getter_AddRefs(triggerContent));
735 popupFrame->InitializePopup(aAnchorContent, triggerContent, aPosition, aXPos,
736 aYPos, MenuPopupAnchorType_Node,
737 aAttributesOverride);
739 BeginShowingPopup(aPopup, aIsContextMenu, aSelectFirstItem, aTriggerEvent);
742 static bool ShouldUseNativeContextMenus() {
743 #ifdef XP_MACOSX
744 return StaticPrefs::widget_macos_native_context_menus() &&
745 StaticPrefs::browser_proton_enabled();
746 #else
747 return false;
748 #endif
751 void nsXULPopupManager::ShowPopupAtScreen(nsIContent* aPopup, int32_t aXPos,
752 int32_t aYPos, bool aIsContextMenu,
753 Event* aTriggerEvent) {
754 if (aIsContextMenu && ShouldUseNativeContextMenus() &&
755 ShowPopupAsNativeMenu(aPopup, aXPos, aYPos, aIsContextMenu,
756 aTriggerEvent)) {
757 return;
760 nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(aPopup, true);
761 if (!popupFrame || !MayShowPopup(popupFrame)) return;
763 nsCOMPtr<nsIContent> triggerContent;
764 InitTriggerEvent(aTriggerEvent, aPopup, getter_AddRefs(triggerContent));
766 popupFrame->InitializePopupAtScreen(triggerContent, aXPos, aYPos,
767 aIsContextMenu);
768 BeginShowingPopup(aPopup, aIsContextMenu, false, aTriggerEvent);
771 bool nsXULPopupManager::ShowPopupAsNativeMenu(nsIContent* aPopup, int32_t aXPos,
772 int32_t aYPos,
773 bool aIsContextMenu,
774 Event* aTriggerEvent) {
775 if (mNativeMenu) {
776 NS_WARNING("Native menu still open when trying to open another");
777 mNativeMenu->RemoveObserver(this);
778 mNativeMenu = nullptr;
781 RefPtr<NativeMenu> menu;
782 #ifdef XP_MACOSX
783 if (aPopup->IsElement()) {
784 menu = mozilla::widget::NativeMenuSupport::CreateNativeContextMenu(
785 aPopup->AsElement());
787 #endif
789 if (!menu) {
790 return false;
793 nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(aPopup, true);
794 if (!popupFrame) {
795 return true;
798 nsCOMPtr<nsIContent> triggerContent;
799 InitTriggerEvent(aTriggerEvent, aPopup, getter_AddRefs(triggerContent));
801 popupFrame->InitializePopupAsNativeContextMenu(triggerContent, aXPos, aYPos);
803 nsEventStatus status =
804 FirePopupShowingEvent(aPopup, popupFrame->PresContext(), aTriggerEvent);
806 // if the event was cancelled, don't open the popup, reset its state back
807 // to closed and clear its trigger content.
808 if (status == nsEventStatus_eConsumeNoDefault) {
809 if (nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(aPopup, true)) {
810 popupFrame->SetPopupState(ePopupClosed);
811 popupFrame->ClearTriggerContent();
813 return true;
816 mNativeMenu = menu;
817 mNativeMenu->AddObserver(this);
818 mNativeMenu->ShowAsContextMenu(DesktopPoint(aXPos, aYPos));
820 // While the native menu is open, it consumes mouseup events.
821 // Clear any :active state and mouse capture now so that we don't get stuck in
822 // that state.
823 EventStateManager* activeESM = static_cast<EventStateManager*>(
824 EventStateManager::GetActiveEventStateManager());
825 if (activeESM) {
826 EventStateManager::ClearGlobalActiveContent(activeESM);
828 PresShell::ReleaseCapturingContent();
830 return true;
833 void nsXULPopupManager::OnNativeMenuOpened() {
834 if (!mNativeMenu) {
835 return;
838 RefPtr<nsXULPopupManager> kungFuDeathGrip(this);
840 nsCOMPtr<nsIContent> popup = mNativeMenu->Element();
841 nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(popup, true);
842 if (popupFrame) {
843 popupFrame->SetPopupState(ePopupShown);
847 void nsXULPopupManager::OnNativeMenuClosed() {
848 if (!mNativeMenu) {
849 return;
852 RefPtr<nsXULPopupManager> kungFuDeathGrip(this);
854 nsCOMPtr<nsIContent> popup = mNativeMenu->Element();
855 nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(popup, true);
856 if (popupFrame) {
857 popupFrame->ClearTriggerContentIncludingDocument();
858 popupFrame->SetPopupState(ePopupClosed);
860 mNativeMenu->RemoveObserver(this);
861 mNativeMenu = nullptr;
864 void nsXULPopupManager::ShowPopupAtScreenRect(
865 nsIContent* aPopup, const nsAString& aPosition, const nsIntRect& aRect,
866 bool aIsContextMenu, bool aAttributesOverride, Event* aTriggerEvent) {
867 nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(aPopup, true);
868 if (!popupFrame || !MayShowPopup(popupFrame)) return;
870 nsCOMPtr<nsIContent> triggerContent;
871 InitTriggerEvent(aTriggerEvent, aPopup, getter_AddRefs(triggerContent));
873 popupFrame->InitializePopupAtRect(triggerContent, aPosition, aRect,
874 aAttributesOverride);
876 BeginShowingPopup(aPopup, aIsContextMenu, false, aTriggerEvent);
879 void nsXULPopupManager::ShowTooltipAtPosition(nsIContent* aPopup,
880 nsIContent* aTriggerContent,
881 const nsAString& aPosition) {
882 nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(aPopup, true);
883 if (!popupFrame || !MayShowPopup(popupFrame)) {
884 return;
887 InitTriggerEvent(nullptr, nullptr, nullptr);
889 popupFrame->InitializePopup(aTriggerContent, aTriggerContent, aPosition, 0, 0,
890 MenuPopupAnchorType_Node, false);
892 BeginShowingPopup(aPopup, false, false, nullptr);
895 void nsXULPopupManager::ShowTooltipAtScreen(nsIContent* aPopup,
896 nsIContent* aTriggerContent,
897 int32_t aXPos, int32_t aYPos) {
898 nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(aPopup, true);
899 if (!popupFrame || !MayShowPopup(popupFrame)) return;
901 InitTriggerEvent(nullptr, nullptr, nullptr);
903 nsPresContext* pc = popupFrame->PresContext();
904 mCachedMousePoint = LayoutDeviceIntPoint(pc->CSSPixelsToDevPixels(aXPos),
905 pc->CSSPixelsToDevPixels(aYPos));
907 // coordinates are relative to the root widget
908 nsPresContext* rootPresContext = pc->GetRootPresContext();
909 if (rootPresContext) {
910 nsIWidget* rootWidget = rootPresContext->GetRootWidget();
911 if (rootWidget) {
912 mCachedMousePoint -= rootWidget->WidgetToScreenOffset();
916 popupFrame->InitializePopupAtScreen(aTriggerContent, aXPos, aYPos, false);
918 BeginShowingPopup(aPopup, false, false, nullptr);
921 static void CheckCaretDrawingState() {
922 // There is 1 caret per document, we need to find the focused
923 // document and erase its caret.
924 nsFocusManager* fm = nsFocusManager::GetFocusManager();
925 if (fm) {
926 nsCOMPtr<mozIDOMWindowProxy> window;
927 fm->GetFocusedWindow(getter_AddRefs(window));
928 if (!window) return;
930 auto* piWindow = nsPIDOMWindowOuter::From(window);
931 MOZ_ASSERT(piWindow);
933 nsCOMPtr<Document> focusedDoc = piWindow->GetDoc();
934 if (!focusedDoc) return;
936 PresShell* presShell = focusedDoc->GetPresShell();
937 if (!presShell) {
938 return;
941 RefPtr<nsCaret> caret = presShell->GetCaret();
942 if (!caret) return;
943 caret->SchedulePaint();
947 void nsXULPopupManager::ShowPopupCallback(nsIContent* aPopup,
948 nsMenuPopupFrame* aPopupFrame,
949 bool aIsContextMenu,
950 bool aSelectFirstItem) {
951 nsPopupType popupType = aPopupFrame->PopupType();
952 bool ismenu = (popupType == ePopupTypeMenu);
954 // Popups normally hide when an outside click occurs. Panels may use
955 // the noautohide attribute to disable this behaviour. It is expected
956 // that the application will hide these popups manually. The tooltip
957 // listener will handle closing the tooltip also.
958 bool isNoAutoHide =
959 aPopupFrame->IsNoAutoHide() || popupType == ePopupTypeTooltip;
961 nsMenuChainItem* item =
962 new nsMenuChainItem(aPopupFrame, isNoAutoHide, aIsContextMenu, popupType);
963 if (!item) return;
965 // install keyboard event listeners for navigating menus. For panels, the
966 // escape key may be used to close the panel. However, the ignorekeys
967 // attribute may be used to disable adding these event listeners for popups
968 // that want to handle their own keyboard events.
969 nsAutoString ignorekeys;
970 if (aPopup->IsElement()) {
971 aPopup->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::ignorekeys,
972 ignorekeys);
974 if (ignorekeys.EqualsLiteral("true")) {
975 item->SetIgnoreKeys(eIgnoreKeys_True);
976 } else if (ignorekeys.EqualsLiteral("shortcuts")) {
977 item->SetIgnoreKeys(eIgnoreKeys_Shortcuts);
980 if (ismenu) {
981 // if the menu is on a menubar, use the menubar's listener instead
982 nsMenuFrame* menuFrame = do_QueryFrame(aPopupFrame->GetParent());
983 if (menuFrame) {
984 item->SetOnMenuBar(menuFrame->IsOnMenuBar());
988 // use a weak frame as the popup will set an open attribute if it is a menu
989 AutoWeakFrame weakFrame(aPopupFrame);
990 aPopupFrame->ShowPopup(aIsContextMenu);
991 NS_ENSURE_TRUE_VOID(weakFrame.IsAlive());
993 item->UpdateFollowAnchor();
995 // popups normally hide when an outside click occurs. Panels may use
996 // the noautohide attribute to disable this behaviour. It is expected
997 // that the application will hide these popups manually. The tooltip
998 // listener will handle closing the tooltip also.
999 nsIContent* oldmenu = nullptr;
1000 if (mPopups) {
1001 oldmenu = mPopups->Content();
1003 item->SetParent(mPopups);
1004 mPopups = item;
1005 SetCaptureState(oldmenu);
1006 NS_ENSURE_TRUE_VOID(weakFrame.IsAlive());
1008 if (aSelectFirstItem) {
1009 nsMenuFrame* next = GetNextMenuItem(aPopupFrame, nullptr, true, false);
1010 aPopupFrame->SetCurrentMenuItem(next);
1013 if (ismenu) UpdateMenuItems(aPopup);
1015 // Caret visibility may have been affected, ensure that
1016 // the caret isn't now drawn when it shouldn't be.
1017 CheckCaretDrawingState();
1020 void nsXULPopupManager::HidePopup(nsIContent* aPopup, bool aHideChain,
1021 bool aDeselectMenu, bool aAsynchronous,
1022 bool aIsCancel, nsIContent* aLastPopup) {
1023 if (mNativeMenu && mNativeMenu->Element() == aPopup) {
1024 RefPtr<NativeMenu> menu = mNativeMenu;
1025 (void)menu->Close();
1026 return;
1029 nsMenuPopupFrame* popupFrame = do_QueryFrame(aPopup->GetPrimaryFrame());
1030 if (!popupFrame) {
1031 return;
1034 nsMenuChainItem* foundPopup = mPopups;
1035 while (foundPopup) {
1036 if (foundPopup->Content() == aPopup) {
1037 break;
1039 foundPopup = foundPopup->GetParent();
1042 bool deselectMenu = false;
1043 nsCOMPtr<nsIContent> popupToHide, nextPopup, lastPopup;
1045 if (foundPopup) {
1046 if (foundPopup->IsNoAutoHide()) {
1047 // If this is a noautohide panel, remove it but don't close any other
1048 // panels.
1049 popupToHide = aPopup;
1050 } else {
1051 // At this point, foundPopup will be set to the found item in the list. If
1052 // foundPopup is the topmost menu, the one to remove, then there are no
1053 // other popups to hide. If foundPopup is not the topmost menu, then there
1054 // may be open submenus below it. In this case, we need to make sure that
1055 // those submenus are closed up first. To do this, we scan up the menu
1056 // list to find the topmost popup with only menus between it and
1057 // foundPopup and close that menu first. In synchronous mode, the
1058 // FirePopupHidingEvent method will be called which in turn calls
1059 // HidePopupCallback to close up the next popup in the chain. These two
1060 // methods will be called in sequence recursively to close up all the
1061 // necessary popups. In asynchronous mode, a similar process occurs except
1062 // that the FirePopupHidingEvent method is called asynchronously. In
1063 // either case, nextPopup is set to the content node of the next popup to
1064 // close, and lastPopup is set to the last popup in the chain to close,
1065 // which will be aPopup, or null to close up all menus.
1067 nsMenuChainItem* topMenu = foundPopup;
1068 // Use IsMenu to ensure that foundPopup is a menu and scan down the child
1069 // list until a non-menu is found. If foundPopup isn't a menu at all,
1070 // don't scan and just close up this menu.
1071 if (foundPopup->IsMenu()) {
1072 nsMenuChainItem* child = foundPopup->GetChild();
1073 while (child && child->IsMenu()) {
1074 topMenu = child;
1075 child = child->GetChild();
1079 deselectMenu = aDeselectMenu;
1080 popupToHide = topMenu->Content();
1081 popupFrame = topMenu->Frame();
1083 // Close up another popup if there is one, and we are either hiding the
1084 // entire chain or the item to hide isn't the topmost popup.
1085 nsMenuChainItem* parent = topMenu->GetParent();
1086 if (parent && (aHideChain || topMenu != foundPopup)) {
1087 while (parent && parent->IsNoAutoHide()) {
1088 parent = parent->GetParent();
1091 if (parent) {
1092 nextPopup = parent->Content();
1096 lastPopup = aLastPopup ? aLastPopup : (aHideChain ? nullptr : aPopup);
1098 } else if (popupFrame->PopupState() == ePopupPositioning) {
1099 // When the popup is in the popuppositioning state, it will not be in the
1100 // mPopups list. We need another way to find it and make sure it does not
1101 // continue the popup showing process.
1102 deselectMenu = aDeselectMenu;
1103 popupToHide = aPopup;
1106 if (popupToHide) {
1107 nsPopupState state = popupFrame->PopupState();
1108 // If the popup is already being hidden, don't attempt to hide it again
1109 if (state == ePopupHiding) {
1110 return;
1113 // Change the popup state to hiding. Don't set the hiding state if the
1114 // popup is invisible, otherwise nsMenuPopupFrame::HidePopup will
1115 // run again. In the invisible state, we just want the events to fire.
1116 if (state != ePopupInvisible) {
1117 popupFrame->SetPopupState(ePopupHiding);
1120 // For menus, popupToHide is always the frontmost item in the list to hide.
1121 if (aAsynchronous) {
1122 nsCOMPtr<nsIRunnable> event = new nsXULPopupHidingEvent(
1123 popupToHide, nextPopup, lastPopup, popupFrame->PopupType(),
1124 deselectMenu, aIsCancel);
1125 aPopup->OwnerDoc()->Dispatch(TaskCategory::Other, event.forget());
1126 } else {
1127 RefPtr<nsPresContext> presContext = popupFrame->PresContext();
1128 FirePopupHidingEvent(popupToHide, nextPopup, lastPopup, presContext,
1129 popupFrame->PopupType(), deselectMenu, aIsCancel);
1134 void nsXULPopupManager::HideMenu(nsIContent* aMenu) {
1135 if (mNativeMenu && aMenu->IsElement() &&
1136 mNativeMenu->Element()->Contains(aMenu)) {
1137 mNativeMenu->CloseSubmenu(aMenu->AsElement());
1138 return;
1141 nsMenuFrame* menu = do_QueryFrame(aMenu->GetPrimaryFrame(FlushType::Frames));
1142 if (!menu) {
1143 return;
1146 nsMenuPopupFrame* popupFrame = menu->GetPopup();
1147 if (!popupFrame) {
1148 return;
1151 HidePopup(popupFrame->GetContent(), false, true, false, false);
1154 // This is used to hide the popup after a transition finishes.
1155 class TransitionEnder final : public nsIDOMEventListener {
1156 protected:
1157 virtual ~TransitionEnder() = default;
1159 public:
1160 nsCOMPtr<nsIContent> mContent;
1161 bool mDeselectMenu;
1163 NS_DECL_CYCLE_COLLECTING_ISUPPORTS
1164 NS_DECL_CYCLE_COLLECTION_CLASS(TransitionEnder)
1166 TransitionEnder(nsIContent* aContent, bool aDeselectMenu)
1167 : mContent(aContent), mDeselectMenu(aDeselectMenu) {}
1169 NS_IMETHOD HandleEvent(Event* aEvent) override {
1170 mContent->RemoveSystemEventListener(u"transitionend"_ns, this, false);
1172 nsMenuPopupFrame* popupFrame = do_QueryFrame(mContent->GetPrimaryFrame());
1174 // Now hide the popup. There could be other properties transitioning, but
1175 // we'll assume they all end at the same time and just hide the popup upon
1176 // the first one ending.
1177 nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
1178 if (pm && popupFrame) {
1179 pm->HidePopupCallback(mContent, popupFrame, nullptr, nullptr,
1180 popupFrame->PopupType(), mDeselectMenu);
1183 return NS_OK;
1187 NS_IMPL_CYCLE_COLLECTING_ADDREF(TransitionEnder)
1188 NS_IMPL_CYCLE_COLLECTING_RELEASE(TransitionEnder)
1189 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TransitionEnder)
1190 NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
1191 NS_INTERFACE_MAP_ENTRY(nsISupports)
1192 NS_INTERFACE_MAP_END
1194 NS_IMPL_CYCLE_COLLECTION(TransitionEnder, mContent);
1196 void nsXULPopupManager::HidePopupCallback(
1197 nsIContent* aPopup, nsMenuPopupFrame* aPopupFrame, nsIContent* aNextPopup,
1198 nsIContent* aLastPopup, nsPopupType aPopupType, bool aDeselectMenu) {
1199 if (mCloseTimer && mTimerMenu == aPopupFrame) {
1200 mCloseTimer->Cancel();
1201 mCloseTimer = nullptr;
1202 mTimerMenu = nullptr;
1205 // The popup to hide is aPopup. Search the list again to find the item that
1206 // corresponds to the popup to hide aPopup. This is done because it's
1207 // possible someone added another item (attempted to open another popup)
1208 // or removed a popup frame during the event processing so the item isn't at
1209 // the front anymore.
1210 nsMenuChainItem* item = mPopups;
1211 while (item) {
1212 if (item->Content() == aPopup) {
1213 item->Detach(&mPopups);
1214 SetCaptureState(aPopup);
1215 break;
1217 item = item->GetParent();
1220 delete item;
1222 AutoWeakFrame weakFrame(aPopupFrame);
1223 aPopupFrame->HidePopup(aDeselectMenu, ePopupClosed);
1224 NS_ENSURE_TRUE_VOID(weakFrame.IsAlive());
1226 // send the popuphidden event synchronously. This event has no default
1227 // behaviour.
1228 nsEventStatus status = nsEventStatus_eIgnore;
1229 WidgetMouseEvent event(true, eXULPopupHidden, nullptr,
1230 WidgetMouseEvent::eReal);
1231 EventDispatcher::Dispatch(aPopup, aPopupFrame->PresContext(), &event, nullptr,
1232 &status);
1233 NS_ENSURE_TRUE_VOID(weakFrame.IsAlive());
1235 // Force any popups that might be anchored on elements within this popup to
1236 // update.
1237 UpdatePopupPositions(aPopupFrame->PresContext()->RefreshDriver());
1239 // if there are more popups to close, look for the next one
1240 if (aNextPopup && aPopup != aLastPopup) {
1241 nsMenuChainItem* foundMenu = nullptr;
1242 nsMenuChainItem* item = mPopups;
1243 while (item) {
1244 if (item->Content() == aNextPopup) {
1245 foundMenu = item;
1246 break;
1248 item = item->GetParent();
1251 // continue hiding the chain of popups until the last popup aLastPopup
1252 // is reached, or until a popup of a different type is reached. This
1253 // last check is needed so that a menulist inside a non-menu panel only
1254 // closes the menu and not the panel as well.
1255 if (foundMenu && (aLastPopup || aPopupType == foundMenu->PopupType())) {
1256 nsCOMPtr<nsIContent> popupToHide = item->Content();
1257 nsMenuChainItem* parent = item->GetParent();
1259 nsCOMPtr<nsIContent> nextPopup;
1260 if (parent && popupToHide != aLastPopup) nextPopup = parent->Content();
1262 nsMenuPopupFrame* popupFrame = item->Frame();
1263 nsPopupState state = popupFrame->PopupState();
1264 if (state == ePopupHiding) return;
1265 if (state != ePopupInvisible) popupFrame->SetPopupState(ePopupHiding);
1267 RefPtr<nsPresContext> presContext = popupFrame->PresContext();
1268 FirePopupHidingEvent(popupToHide, nextPopup, aLastPopup, presContext,
1269 foundMenu->PopupType(), aDeselectMenu, false);
1274 void nsXULPopupManager::HidePopupAfterDelay(nsMenuPopupFrame* aPopup) {
1275 // Don't close up immediately.
1276 // Kick off a close timer.
1277 KillMenuTimer();
1279 int32_t menuDelay =
1280 LookAndFeel::GetInt(LookAndFeel::IntID::SubmenuDelay, 300); // ms
1282 // Kick off the timer.
1283 nsIEventTarget* target = nullptr;
1284 if (nsIContent* content = aPopup->GetContent()) {
1285 target = content->OwnerDoc()->EventTargetFor(TaskCategory::Other);
1287 NS_NewTimerWithFuncCallback(
1288 getter_AddRefs(mCloseTimer),
1289 [](nsITimer* aTimer, void* aClosure) {
1290 nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
1291 if (pm) {
1292 pm->KillMenuTimer();
1295 nullptr, menuDelay, nsITimer::TYPE_ONE_SHOT, "KillMenuTimer", target);
1297 // the popup will call PopupDestroyed if it is destroyed, which checks if it
1298 // is set to mTimerMenu, so it should be safe to keep a reference to it
1299 mTimerMenu = aPopup;
1302 void nsXULPopupManager::HidePopupsInList(
1303 const nsTArray<nsMenuPopupFrame*>& aFrames) {
1304 // Create a weak frame list. This is done in a separate array with the
1305 // right capacity predetermined to avoid multiple allocations.
1306 nsTArray<WeakFrame> weakPopups(aFrames.Length());
1307 uint32_t f;
1308 for (f = 0; f < aFrames.Length(); f++) {
1309 WeakFrame* wframe = weakPopups.AppendElement();
1310 if (wframe) *wframe = aFrames[f];
1313 for (f = 0; f < weakPopups.Length(); f++) {
1314 // check to ensure that the frame is still alive before hiding it.
1315 if (weakPopups[f].IsAlive()) {
1316 nsMenuPopupFrame* frame =
1317 static_cast<nsMenuPopupFrame*>(weakPopups[f].GetFrame());
1318 frame->HidePopup(true, ePopupInvisible);
1322 SetCaptureState(nullptr);
1325 void nsXULPopupManager::EnableRollup(nsIContent* aPopup, bool aShouldRollup) {
1326 #ifndef MOZ_GTK
1327 nsMenuChainItem* item = mPopups;
1328 while (item) {
1329 if (item->Content() == aPopup) {
1330 nsIContent* oldmenu = nullptr;
1331 if (mPopups) {
1332 oldmenu = mPopups->Content();
1335 item->SetNoAutoHide(!aShouldRollup);
1336 SetCaptureState(oldmenu);
1337 return;
1339 item = item->GetParent();
1341 #endif
1344 bool nsXULPopupManager::IsChildOfDocShell(Document* aDoc,
1345 nsIDocShellTreeItem* aExpected) {
1346 nsCOMPtr<nsIDocShellTreeItem> docShellItem(aDoc->GetDocShell());
1347 while (docShellItem) {
1348 if (docShellItem == aExpected) return true;
1350 nsCOMPtr<nsIDocShellTreeItem> parent;
1351 docShellItem->GetInProcessParent(getter_AddRefs(parent));
1352 docShellItem = parent;
1355 return false;
1358 void nsXULPopupManager::HidePopupsInDocShell(
1359 nsIDocShellTreeItem* aDocShellToHide) {
1360 nsTArray<nsMenuPopupFrame*> popupsToHide;
1362 // iterate to get the set of popup frames to hide
1363 nsMenuChainItem* item = mPopups;
1364 while (item) {
1365 nsMenuChainItem* parent = item->GetParent();
1366 if (item->Frame()->PopupState() != ePopupInvisible &&
1367 IsChildOfDocShell(item->Content()->OwnerDoc(), aDocShellToHide)) {
1368 nsMenuPopupFrame* frame = item->Frame();
1369 item->Detach(&mPopups);
1370 delete item;
1371 popupsToHide.AppendElement(frame);
1373 item = parent;
1376 HidePopupsInList(popupsToHide);
1379 void nsXULPopupManager::UpdatePopupPositions(nsRefreshDriver* aRefreshDriver) {
1380 nsMenuChainItem* item = mPopups;
1381 while (item) {
1382 if (item->Frame()->PresContext()->RefreshDriver() == aRefreshDriver) {
1383 item->CheckForAnchorChange();
1386 item = item->GetParent();
1390 void nsXULPopupManager::UpdateFollowAnchor(nsMenuPopupFrame* aPopup) {
1391 nsMenuChainItem* item = mPopups;
1392 while (item) {
1393 if (item->Frame() == aPopup) {
1394 item->UpdateFollowAnchor();
1395 break;
1398 item = item->GetParent();
1402 void nsXULPopupManager::ExecuteMenu(nsIContent* aMenu,
1403 nsXULMenuCommandEvent* aEvent) {
1404 CloseMenuMode cmm = CloseMenuMode_Auto;
1406 static Element::AttrValuesArray strings[] = {nsGkAtoms::none,
1407 nsGkAtoms::single, nullptr};
1409 if (aMenu->IsElement()) {
1410 switch (aMenu->AsElement()->FindAttrValueIn(
1411 kNameSpaceID_None, nsGkAtoms::closemenu, strings, eCaseMatters)) {
1412 case 0:
1413 cmm = CloseMenuMode_None;
1414 break;
1415 case 1:
1416 cmm = CloseMenuMode_Single;
1417 break;
1418 default:
1419 break;
1423 // When a menuitem is selected to be executed, first hide all the open
1424 // popups, but don't remove them yet. This is needed when a menu command
1425 // opens a modal dialog. The views associated with the popups needed to be
1426 // hidden and the accesibility events fired before the command executes, but
1427 // the popuphiding/popuphidden events are fired afterwards.
1428 nsTArray<nsMenuPopupFrame*> popupsToHide;
1429 nsMenuChainItem* item = GetTopVisibleMenu();
1430 if (cmm != CloseMenuMode_None) {
1431 while (item) {
1432 // if it isn't a <menupopup>, don't close it automatically
1433 if (!item->IsMenu()) break;
1434 nsMenuChainItem* next = item->GetParent();
1435 popupsToHide.AppendElement(item->Frame());
1436 if (cmm == CloseMenuMode_Single) // only close one level of menu
1437 break;
1438 item = next;
1441 // Now hide the popups. If the closemenu mode is auto, deselect the menu,
1442 // otherwise only one popup is closing, so keep the parent menu selected.
1443 HidePopupsInList(popupsToHide);
1446 aEvent->SetCloseMenuMode(cmm);
1447 nsCOMPtr<nsIRunnable> event = aEvent;
1448 aMenu->OwnerDoc()->Dispatch(TaskCategory::Other, event.forget());
1451 bool nsXULPopupManager::ActivateNativeMenuItem(nsIContent* aItem,
1452 mozilla::Modifiers aModifiers,
1453 mozilla::ErrorResult& aRv) {
1454 if (mNativeMenu && aItem->IsElement() &&
1455 mNativeMenu->Element()->Contains(aItem)) {
1456 mNativeMenu->ActivateItem(aItem->AsElement(), aModifiers, aRv);
1457 return true;
1459 return false;
1462 nsEventStatus nsXULPopupManager::FirePopupShowingEvent(
1463 nsIContent* aPopup, nsPresContext* aPresContext, Event* aTriggerEvent) {
1464 nsCOMPtr<nsIContent> popup = aPopup; // keep a strong reference to the popup
1466 // cache the popup so that document.popupNode can retrieve the trigger node
1467 // during the popupshowing event. It will be cleared below after the event
1468 // has fired.
1469 mOpeningPopup = aPopup;
1471 nsEventStatus status = nsEventStatus_eIgnore;
1472 WidgetMouseEvent event(true, eXULPopupShowing, nullptr,
1473 WidgetMouseEvent::eReal);
1475 // coordinates are relative to the root widget
1476 nsPresContext* rootPresContext = aPresContext->GetRootPresContext();
1477 if (rootPresContext) {
1478 rootPresContext->PresShell()->GetViewManager()->GetRootWidget(
1479 getter_AddRefs(event.mWidget));
1480 } else {
1481 event.mWidget = nullptr;
1484 if (aTriggerEvent) {
1485 WidgetMouseEventBase* mouseEvent =
1486 aTriggerEvent->WidgetEventPtr()->AsMouseEventBase();
1487 if (mouseEvent) {
1488 event.mInputSource = mouseEvent->mInputSource;
1492 event.mRefPoint = mCachedMousePoint;
1493 event.mModifiers = mCachedModifiers;
1494 EventDispatcher::Dispatch(popup, aPresContext, &event, nullptr, &status);
1496 mCachedMousePoint = LayoutDeviceIntPoint(0, 0);
1497 mOpeningPopup = nullptr;
1499 mCachedModifiers = 0;
1501 // clear these as they are no longer valid
1502 mRangeParentContent = nullptr;
1503 mRangeOffset = 0;
1505 return status;
1508 void nsXULPopupManager::BeginShowingPopup(nsIContent* aPopup,
1509 bool aIsContextMenu,
1510 bool aSelectFirstItem,
1511 Event* aTriggerEvent) {
1512 nsCOMPtr<nsIContent> popup = aPopup; // keep a strong reference to the popup
1514 nsMenuPopupFrame* popupFrame = do_QueryFrame(aPopup->GetPrimaryFrame());
1515 if (!popupFrame) return;
1517 popupFrame->GenerateFrames();
1519 // get the frame again
1520 popupFrame = do_QueryFrame(aPopup->GetPrimaryFrame());
1521 if (!popupFrame) return;
1523 nsPresContext* presContext = popupFrame->PresContext();
1524 RefPtr<PresShell> presShell = presContext->PresShell();
1525 presShell->FrameNeedsReflow(popupFrame, IntrinsicDirty::TreeChange,
1526 NS_FRAME_HAS_DIRTY_CHILDREN);
1528 nsPopupType popupType = popupFrame->PopupType();
1530 nsEventStatus status =
1531 FirePopupShowingEvent(aPopup, presContext, aTriggerEvent);
1533 // if a panel, blur whatever has focus so that the panel can take the focus.
1534 // This is done after the popupshowing event in case that event is cancelled.
1535 // Using noautofocus="true" will disable this behaviour, which is needed for
1536 // the autocomplete widget as it manages focus itself.
1537 if (popupType == ePopupTypePanel &&
1538 !popup->AsElement()->AttrValueIs(kNameSpaceID_None,
1539 nsGkAtoms::noautofocus, nsGkAtoms::_true,
1540 eCaseMatters)) {
1541 nsFocusManager* fm = nsFocusManager::GetFocusManager();
1542 if (fm) {
1543 Document* doc = popup->GetUncomposedDoc();
1545 // Only remove the focus if the currently focused item is ouside the
1546 // popup. It isn't a big deal if the current focus is in a child popup
1547 // inside the popup as that shouldn't be visible. This check ensures that
1548 // a node inside the popup that is focused during a popupshowing event
1549 // remains focused.
1550 RefPtr<Element> currentFocus = fm->GetFocusedElement();
1551 if (doc && currentFocus &&
1552 !nsContentUtils::ContentIsCrossDocDescendantOf(currentFocus, popup)) {
1553 fm->ClearFocus(doc->GetWindow());
1558 aPopup->OwnerDoc()->FlushPendingNotifications(FlushType::Frames);
1560 // get the frame again in case it went away
1561 popupFrame = do_QueryFrame(aPopup->GetPrimaryFrame());
1562 if (popupFrame) {
1563 // if the event was cancelled, don't open the popup, reset its state back
1564 // to closed and clear its trigger content.
1565 if (status == nsEventStatus_eConsumeNoDefault) {
1566 popupFrame->SetPopupState(ePopupClosed);
1567 popupFrame->ClearTriggerContent();
1568 } else {
1569 // Now check if we need to fire the popuppositioned event. If not, call
1570 // ShowPopupCallback directly.
1572 // The popuppositioned event only fires on arrow panels for now.
1573 if (popup->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
1574 nsGkAtoms::arrow, eCaseMatters)) {
1575 popupFrame->ShowWithPositionedEvent();
1576 presShell->FrameNeedsReflow(popupFrame, IntrinsicDirty::TreeChange,
1577 NS_FRAME_HAS_DIRTY_CHILDREN);
1578 } else {
1579 ShowPopupCallback(popup, popupFrame, aIsContextMenu, aSelectFirstItem);
1585 void nsXULPopupManager::FirePopupHidingEvent(
1586 nsIContent* aPopup, nsIContent* aNextPopup, nsIContent* aLastPopup,
1587 nsPresContext* aPresContext, nsPopupType aPopupType, bool aDeselectMenu,
1588 bool aIsCancel) {
1589 nsCOMPtr<nsIContent> popup = aPopup;
1590 RefPtr<PresShell> presShell = aPresContext->PresShell();
1591 mozilla::Unused << presShell; // This presShell may be keeping things alive
1592 // on non GTK platforms
1594 nsEventStatus status = nsEventStatus_eIgnore;
1595 WidgetMouseEvent event(true, eXULPopupHiding, nullptr,
1596 WidgetMouseEvent::eReal);
1597 EventDispatcher::Dispatch(aPopup, aPresContext, &event, nullptr, &status);
1599 // when a panel is closed, blur whatever has focus inside the popup
1600 if (aPopupType == ePopupTypePanel &&
1601 (!aPopup->IsElement() || !aPopup->AsElement()->AttrValueIs(
1602 kNameSpaceID_None, nsGkAtoms::noautofocus,
1603 nsGkAtoms::_true, eCaseMatters))) {
1604 nsFocusManager* fm = nsFocusManager::GetFocusManager();
1605 if (fm) {
1606 Document* doc = aPopup->GetUncomposedDoc();
1608 // Remove the focus from the focused node only if it is inside the popup.
1609 RefPtr<Element> currentFocus = fm->GetFocusedElement();
1610 if (doc && currentFocus &&
1611 nsContentUtils::ContentIsCrossDocDescendantOf(currentFocus, aPopup)) {
1612 fm->ClearFocus(doc->GetWindow());
1617 aPopup->OwnerDoc()->FlushPendingNotifications(FlushType::Frames);
1619 // get frame again in case it went away
1620 nsMenuPopupFrame* popupFrame = do_QueryFrame(aPopup->GetPrimaryFrame());
1621 if (popupFrame) {
1622 // if the event was cancelled, don't hide the popup, and reset its
1623 // state back to open. Only popups in chrome shells can prevent a popup
1624 // from hiding.
1625 if (status == nsEventStatus_eConsumeNoDefault &&
1626 !popupFrame->IsInContentShell()) {
1627 // XXXndeakin
1628 // If an attempt was made to hide this popup before the popupshown event
1629 // fired, then ePopupShown is set here even though it should be
1630 // ePopupVisible. This probably isn't worth the hassle of handling.
1631 popupFrame->SetPopupState(ePopupShown);
1632 } else {
1633 // If the popup has an animate attribute and it is not set to false, check
1634 // if it has a closing transition and wait for it to finish. The
1635 // transition may still occur either way, but the view will be hidden and
1636 // you won't be able to see it. If there is a next popup, indicating that
1637 // mutliple popups are rolling up, don't wait and hide the popup right
1638 // away since the effect would likely be undesirable.
1639 if (StaticPrefs::xul_panel_animations_enabled() && !aNextPopup &&
1640 aPopup->IsElement() &&
1641 aPopup->AsElement()->HasAttr(kNameSpaceID_None, nsGkAtoms::animate)) {
1642 // If animate="false" then don't transition at all. If animate="cancel",
1643 // only show the transition if cancelling the popup or rolling up.
1644 // Otherwise, always show the transition.
1645 nsAutoString animate;
1646 aPopup->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::animate,
1647 animate);
1649 if (!animate.EqualsLiteral("false") &&
1650 (!animate.EqualsLiteral("cancel") || aIsCancel)) {
1651 presShell->FlushPendingNotifications(FlushType::Layout);
1653 // Get the frame again in case the flush caused it to go away
1654 popupFrame = do_QueryFrame(aPopup->GetPrimaryFrame());
1655 if (!popupFrame) return;
1657 if (AnimationUtils::HasCurrentTransitions(
1658 aPopup->AsElement(), PseudoStyleType::NotPseudo)) {
1659 RefPtr<TransitionEnder> ender =
1660 new TransitionEnder(aPopup, aDeselectMenu);
1661 aPopup->AddSystemEventListener(u"transitionend"_ns, ender, false,
1662 false);
1663 return;
1668 HidePopupCallback(aPopup, popupFrame, aNextPopup, aLastPopup, aPopupType,
1669 aDeselectMenu);
1674 bool nsXULPopupManager::IsPopupOpen(nsIContent* aPopup) {
1675 if (mNativeMenu && mNativeMenu->Element() == aPopup) {
1676 return true;
1679 // a popup is open if it is in the open list. The assertions ensure that the
1680 // frame is in the correct state. If the popup is in the hiding or invisible
1681 // state, it will still be in the open popup list until it is closed.
1682 nsMenuChainItem* item = mPopups;
1683 while (item) {
1684 if (item->Content() == aPopup) {
1685 NS_ASSERTION(item->Frame()->IsOpen() ||
1686 item->Frame()->PopupState() == ePopupHiding ||
1687 item->Frame()->PopupState() == ePopupInvisible,
1688 "popup in open list not actually open");
1689 return true;
1691 item = item->GetParent();
1694 return false;
1697 bool nsXULPopupManager::IsPopupOpenForMenuParent(nsMenuParent* aMenuParent) {
1698 nsMenuChainItem* item = GetTopVisibleMenu();
1699 while (item) {
1700 nsMenuPopupFrame* popup = item->Frame();
1701 if (popup && popup->IsOpen()) {
1702 nsMenuFrame* menuFrame = do_QueryFrame(popup->GetParent());
1703 if (menuFrame && menuFrame->GetMenuParent() == aMenuParent) {
1704 return true;
1707 item = item->GetParent();
1710 return false;
1713 nsIFrame* nsXULPopupManager::GetTopPopup(nsPopupType aType) {
1714 nsMenuChainItem* item = mPopups;
1715 while (item) {
1716 if (item->Frame()->IsVisible() &&
1717 (item->PopupType() == aType || aType == ePopupTypeAny)) {
1718 return item->Frame();
1721 item = item->GetParent();
1724 return nullptr;
1727 void nsXULPopupManager::GetVisiblePopups(nsTArray<nsIFrame*>& aPopups) {
1728 aPopups.Clear();
1730 nsMenuChainItem* item = mPopups;
1731 while (item) {
1732 // Skip panels which are not visible as well as popups that
1733 // are transparent to mouse events.
1734 if (item->Frame()->IsVisible() && !item->Frame()->IsMouseTransparent()) {
1735 aPopups.AppendElement(item->Frame());
1738 item = item->GetParent();
1742 already_AddRefed<nsINode> nsXULPopupManager::GetLastTriggerNode(
1743 Document* aDocument, bool aIsTooltip) {
1744 if (!aDocument) return nullptr;
1746 nsCOMPtr<nsINode> node;
1748 // if mOpeningPopup is set, it means that a popupshowing event is being
1749 // fired. In this case, just use the cached node, as the popup is not yet in
1750 // the list of open popups.
1751 if (mOpeningPopup && mOpeningPopup->GetUncomposedDoc() == aDocument &&
1752 aIsTooltip == mOpeningPopup->IsXULElement(nsGkAtoms::tooltip)) {
1753 nsCOMPtr<nsIContent> openingPopup = mOpeningPopup;
1754 node = nsMenuPopupFrame::GetTriggerContent(
1755 GetPopupFrameForContent(openingPopup, false));
1756 } else if (mNativeMenu && !aIsTooltip) {
1757 RefPtr<dom::Element> popup = mNativeMenu->Element();
1758 if (popup->GetUncomposedDoc() == aDocument) {
1759 nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(popup, false);
1760 node = nsMenuPopupFrame::GetTriggerContent(popupFrame);
1762 } else {
1763 nsMenuChainItem* item = mPopups;
1764 while (item) {
1765 // look for a popup of the same type and document.
1766 if ((item->PopupType() == ePopupTypeTooltip) == aIsTooltip &&
1767 item->Content()->GetUncomposedDoc() == aDocument) {
1768 node = nsMenuPopupFrame::GetTriggerContent(item->Frame());
1769 if (node) break;
1771 item = item->GetParent();
1775 return node.forget();
1778 bool nsXULPopupManager::MayShowPopup(nsMenuPopupFrame* aPopup) {
1779 // if a popup's IsOpen method returns true, then the popup must always be in
1780 // the popup chain scanned in IsPopupOpen.
1781 NS_ASSERTION(!aPopup->IsOpen() || IsPopupOpen(aPopup->GetContent()),
1782 "popup frame state doesn't match XULPopupManager open state");
1784 nsPopupState state = aPopup->PopupState();
1786 // if the popup is not in the open popup chain, then it must have a state that
1787 // is either closed, in the process of being shown, or invisible.
1788 NS_ASSERTION(IsPopupOpen(aPopup->GetContent()) || state == ePopupClosed ||
1789 state == ePopupShowing || state == ePopupPositioning ||
1790 state == ePopupInvisible,
1791 "popup not in XULPopupManager open list is open");
1793 // don't show popups unless they are closed or invisible
1794 if (state != ePopupClosed && state != ePopupInvisible) return false;
1796 // Don't show popups that we already have in our popup chain
1797 if (IsPopupOpen(aPopup->GetContent())) {
1798 NS_WARNING("Refusing to show duplicate popup");
1799 return false;
1802 // if the popup was just rolled up, don't reopen it
1803 if (mozilla::widget::nsAutoRollup::GetLastRollup() == aPopup->GetContent()) {
1804 return false;
1807 nsCOMPtr<nsIDocShell> docShell = aPopup->PresContext()->GetDocShell();
1809 nsCOMPtr<nsIBaseWindow> baseWin = do_QueryInterface(docShell);
1810 if (!baseWin) {
1811 return false;
1814 nsCOMPtr<nsIDocShellTreeItem> root;
1815 docShell->GetInProcessRootTreeItem(getter_AddRefs(root));
1816 if (!root) {
1817 return false;
1820 nsCOMPtr<nsPIDOMWindowOuter> rootWin = root->GetWindow();
1822 if (XRE_IsParentProcess()) {
1823 // chrome shells can always open popups, but other types of shells can only
1824 // open popups when they are focused and visible
1825 if (docShell->ItemType() != nsIDocShellTreeItem::typeChrome) {
1826 // only allow popups in active windows
1827 nsFocusManager* fm = nsFocusManager::GetFocusManager();
1828 if (!fm || !rootWin) {
1829 return false;
1832 nsCOMPtr<nsPIDOMWindowOuter> activeWindow = fm->GetActiveWindow();
1833 if (activeWindow != rootWin) {
1834 return false;
1837 // only allow popups in visible frames
1838 // TODO: This visibility check should be replaced with a check of
1839 // bc->IsActive(). It is okay for now since this is only called
1840 // in the parent process. Bug 1698533.
1841 bool visible;
1842 baseWin->GetVisibility(&visible);
1843 if (!visible) {
1844 return false;
1847 } else {
1848 nsFocusManager* fm = nsFocusManager::GetFocusManager();
1849 BrowsingContext* bc = docShell->GetBrowsingContext();
1850 if (!fm || !bc || fm->GetActiveBrowsingContext() != bc->Top()) {
1851 // fm->GetActiveBrowsingContext() == bc->Top() would imply bc->IsActive(),
1852 // so we don't bother checking/early returning for !bc->IsActive().
1853 return false;
1857 // platforms respond differently when an popup is opened in a minimized
1858 // window, so this is always disabled.
1859 nsCOMPtr<nsIWidget> mainWidget;
1860 baseWin->GetMainWidget(getter_AddRefs(mainWidget));
1861 if (mainWidget && mainWidget->SizeMode() == nsSizeMode_Minimized) {
1862 return false;
1865 #ifdef XP_MACOSX
1866 if (rootWin) {
1867 auto globalWin = nsGlobalWindowOuter::Cast(rootWin.get());
1868 if (globalWin->IsInModalState()) {
1869 return false;
1872 #endif
1874 // cannot open a popup that is a submenu of a menupopup that isn't open.
1875 nsMenuFrame* menuFrame = do_QueryFrame(aPopup->GetParent());
1876 if (menuFrame) {
1877 nsMenuParent* parentPopup = menuFrame->GetMenuParent();
1878 if (parentPopup && !parentPopup->IsOpen()) return false;
1881 return true;
1884 void nsXULPopupManager::PopupDestroyed(nsMenuPopupFrame* aPopup) {
1885 // when a popup frame is destroyed, just unhook it from the list of popups
1886 if (mTimerMenu == aPopup) {
1887 if (mCloseTimer) {
1888 mCloseTimer->Cancel();
1889 mCloseTimer = nullptr;
1891 mTimerMenu = nullptr;
1894 nsTArray<nsMenuPopupFrame*> popupsToHide;
1896 nsMenuChainItem* item = mPopups;
1897 while (item) {
1898 nsMenuPopupFrame* frame = item->Frame();
1899 if (frame == aPopup) {
1900 // XXXndeakin shouldn't this only happen for menus?
1901 if (!item->IsNoAutoHide() && frame->PopupState() != ePopupInvisible) {
1902 // Iterate through any child menus and hide them as well, since the
1903 // parent is going away. We won't remove them from the list yet, just
1904 // hide them, as they will be removed from the list when this function
1905 // gets called for that child frame.
1906 nsMenuChainItem* child = item->GetChild();
1907 while (child) {
1908 // if the popup is a child frame of the menu that was destroyed, add
1909 // it to the list of popups to hide. Don't bother with the events
1910 // since the frames are going away. If the child menu is not a child
1911 // frame, for example, a context menu, use HidePopup instead, but call
1912 // it asynchronously since we are in the middle of frame destruction.
1913 nsMenuPopupFrame* childframe = child->Frame();
1914 if (nsLayoutUtils::IsProperAncestorFrame(frame, childframe)) {
1915 popupsToHide.AppendElement(childframe);
1916 } else {
1917 // HidePopup will take care of hiding any of its children, so
1918 // break out afterwards
1919 HidePopup(child->Content(), false, false, true, false);
1920 break;
1923 child = child->GetChild();
1927 item->Detach(&mPopups);
1928 delete item;
1929 break;
1932 item = item->GetParent();
1935 HidePopupsInList(popupsToHide);
1938 bool nsXULPopupManager::HasContextMenu(nsMenuPopupFrame* aPopup) {
1939 nsMenuChainItem* item = GetTopVisibleMenu();
1940 while (item && item->Frame() != aPopup) {
1941 if (item->IsContextMenu()) return true;
1942 item = item->GetParent();
1945 return false;
1948 void nsXULPopupManager::SetCaptureState(nsIContent* aOldPopup) {
1949 nsMenuChainItem* item = GetTopVisibleMenu();
1950 if (item && aOldPopup == item->Content()) return;
1952 if (mWidget) {
1953 mWidget->CaptureRollupEvents(nullptr, false);
1954 mWidget = nullptr;
1957 if (item) {
1958 nsMenuPopupFrame* popup = item->Frame();
1959 mWidget = popup->GetWidget();
1960 if (mWidget) {
1961 mWidget->CaptureRollupEvents(nullptr, true);
1965 UpdateKeyboardListeners();
1968 void nsXULPopupManager::UpdateKeyboardListeners() {
1969 nsCOMPtr<EventTarget> newTarget;
1970 bool isForMenu = false;
1971 nsMenuChainItem* item = GetTopVisibleMenu();
1972 if (item) {
1973 if (item->IgnoreKeys() != eIgnoreKeys_True) {
1974 newTarget = item->Content()->GetComposedDoc();
1976 isForMenu = item->PopupType() == ePopupTypeMenu;
1977 } else if (mActiveMenuBar) {
1978 newTarget = mActiveMenuBar->GetContent()->GetComposedDoc();
1979 isForMenu = true;
1982 if (mKeyListener != newTarget) {
1983 if (mKeyListener) {
1984 mKeyListener->RemoveEventListener(u"keypress"_ns, this, true);
1985 mKeyListener->RemoveEventListener(u"keydown"_ns, this, true);
1986 mKeyListener->RemoveEventListener(u"keyup"_ns, this, true);
1987 mKeyListener = nullptr;
1988 nsContentUtils::NotifyInstalledMenuKeyboardListener(false);
1991 if (newTarget) {
1992 newTarget->AddEventListener(u"keypress"_ns, this, true);
1993 newTarget->AddEventListener(u"keydown"_ns, this, true);
1994 newTarget->AddEventListener(u"keyup"_ns, this, true);
1995 nsContentUtils::NotifyInstalledMenuKeyboardListener(isForMenu);
1996 mKeyListener = newTarget;
2001 void nsXULPopupManager::UpdateMenuItems(nsIContent* aPopup) {
2002 // Walk all of the menu's children, checking to see if any of them has a
2003 // command attribute. If so, then several attributes must potentially be
2004 // updated.
2006 nsCOMPtr<Document> document = aPopup->GetUncomposedDoc();
2007 if (!document) {
2008 return;
2011 // When a menu is opened, make sure that command updating is unlocked first.
2012 nsCOMPtr<nsIDOMXULCommandDispatcher> commandDispatcher =
2013 document->GetCommandDispatcher();
2014 if (commandDispatcher) {
2015 commandDispatcher->Unlock();
2018 for (nsCOMPtr<nsIContent> grandChild = aPopup->GetFirstChild(); grandChild;
2019 grandChild = grandChild->GetNextSibling()) {
2020 if (grandChild->IsXULElement(nsGkAtoms::menugroup)) {
2021 if (grandChild->GetChildCount() == 0) {
2022 continue;
2024 grandChild = grandChild->GetFirstChild();
2026 if (grandChild->IsXULElement(nsGkAtoms::menuitem)) {
2027 // See if we have a command attribute.
2028 Element* grandChildElement = grandChild->AsElement();
2029 nsAutoString command;
2030 grandChildElement->GetAttr(kNameSpaceID_None, nsGkAtoms::command,
2031 command);
2032 if (!command.IsEmpty()) {
2033 // We do! Look it up in our document
2034 RefPtr<dom::Element> commandElement = document->GetElementById(command);
2035 if (commandElement) {
2036 nsAutoString commandValue;
2037 // The menu's disabled state needs to be updated to match the command.
2038 if (commandElement->GetAttr(kNameSpaceID_None, nsGkAtoms::disabled,
2039 commandValue))
2040 grandChildElement->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled,
2041 commandValue, true);
2042 else
2043 grandChildElement->UnsetAttr(kNameSpaceID_None, nsGkAtoms::disabled,
2044 true);
2046 // The menu's label, accesskey checked and hidden states need to be
2047 // updated to match the command. Note that unlike the disabled state
2048 // if the command has *no* value, we assume the menu is supplying its
2049 // own.
2050 if (commandElement->GetAttr(kNameSpaceID_None, nsGkAtoms::label,
2051 commandValue))
2052 grandChildElement->SetAttr(kNameSpaceID_None, nsGkAtoms::label,
2053 commandValue, true);
2055 if (commandElement->GetAttr(kNameSpaceID_None, nsGkAtoms::accesskey,
2056 commandValue))
2057 grandChildElement->SetAttr(kNameSpaceID_None, nsGkAtoms::accesskey,
2058 commandValue, true);
2060 if (commandElement->GetAttr(kNameSpaceID_None, nsGkAtoms::checked,
2061 commandValue))
2062 grandChildElement->SetAttr(kNameSpaceID_None, nsGkAtoms::checked,
2063 commandValue, true);
2065 if (commandElement->GetAttr(kNameSpaceID_None, nsGkAtoms::hidden,
2066 commandValue))
2067 grandChildElement->SetAttr(kNameSpaceID_None, nsGkAtoms::hidden,
2068 commandValue, true);
2072 if (!grandChild->GetNextSibling() &&
2073 grandChild->GetParent()->IsXULElement(nsGkAtoms::menugroup)) {
2074 grandChild = grandChild->GetParent();
2079 // Notify
2081 // The item selection timer has fired, we might have to readjust the
2082 // selected item. There are two cases here that we are trying to deal with:
2083 // (1) diagonal movement from a parent menu to a submenu passing briefly over
2084 // other items, and
2085 // (2) moving out from a submenu to a parent or grandparent menu.
2086 // In both cases, |mTimerMenu| is the menu item that might have an open submenu
2087 // and the first item in |mPopups| is the item the mouse is currently over,
2088 // which could be none of them.
2090 // case (1):
2091 // As the mouse moves from the parent item of a submenu (we'll call 'A')
2092 // diagonally into the submenu, it probably passes through one or more
2093 // sibilings (B). As the mouse passes through B, it becomes the current menu
2094 // item and the timer is set and mTimerMenu is set to A. Before the timer
2095 // fires, the mouse leaves the menu containing A and B and enters the submenus.
2096 // Now when the timer fires, |mPopups| is null (!= |mTimerMenu|) so we have to
2097 // see if anything in A's children is selected (recall that even disabled items
2098 // are selected, the style just doesn't show it). If that is the case, we need
2099 // to set the selected item back to A.
2101 // case (2);
2102 // Item A has an open submenu, and in it there is an item (B) which also has an
2103 // open submenu (so there are 3 menus displayed right now). The mouse then
2104 // leaves B's child submenu and selects an item that is a sibling of A, call it
2105 // C. When the mouse enters C, the timer is set and |mTimerMenu| is A and
2106 // |mPopups| is C. As the timer fires, the mouse is still within C. The correct
2107 // behavior is to set the current item to C and close up the chain parented at
2108 // A.
2110 // This brings up the question of is the logic of case (1) enough? The answer
2111 // is no, and is discussed in bugzilla bug 29400. Case (1) asks if A's submenu
2112 // has a selected child, and if it does, set the selected item to A. Because B
2113 // has a submenu open, it is selected and as a result, A is set to be the
2114 // selected item even though the mouse rests in C -- very wrong.
2116 // The solution is to use the same idea, but instead of only checking one
2117 // level, drill all the way down to the deepest open submenu and check if it
2118 // has something selected. Since the mouse is in a grandparent, it won't, and
2119 // we know that we can safely close up A and all its children.
2121 // The code below melds the two cases together.
2123 void nsXULPopupManager::KillMenuTimer() {
2124 if (mCloseTimer && mTimerMenu) {
2125 mCloseTimer->Cancel();
2126 mCloseTimer = nullptr;
2128 if (mTimerMenu->IsOpen())
2129 HidePopup(mTimerMenu->GetContent(), false, false, true, false);
2132 mTimerMenu = nullptr;
2135 void nsXULPopupManager::CancelMenuTimer(nsMenuParent* aMenuParent) {
2136 if (mCloseTimer && mTimerMenu == aMenuParent) {
2137 mCloseTimer->Cancel();
2138 mCloseTimer = nullptr;
2139 mTimerMenu = nullptr;
2143 bool nsXULPopupManager::HandleShortcutNavigation(KeyboardEvent* aKeyEvent,
2144 nsMenuPopupFrame* aFrame) {
2145 // On Windows, don't check shortcuts when the accelerator key is down.
2146 #ifdef XP_WIN
2147 WidgetInputEvent* evt = aKeyEvent->WidgetEventPtr()->AsInputEvent();
2148 if (evt && evt->IsAccel()) {
2149 return false;
2151 #endif
2153 nsMenuChainItem* item = GetTopVisibleMenu();
2154 if (!aFrame && item) aFrame = item->Frame();
2156 if (aFrame) {
2157 bool action;
2158 nsMenuFrame* result = aFrame->FindMenuWithShortcut(aKeyEvent, action);
2159 if (result) {
2160 aFrame->ChangeMenuItem(result, false, true);
2161 if (action) {
2162 WidgetGUIEvent* evt = aKeyEvent->WidgetEventPtr()->AsGUIEvent();
2163 nsMenuFrame* menuToOpen = result->Enter(evt);
2164 if (menuToOpen) {
2165 nsCOMPtr<nsIContent> content = menuToOpen->GetContent();
2166 ShowMenu(content, true, false);
2169 return true;
2172 return false;
2175 if (mActiveMenuBar) {
2176 nsMenuFrame* result =
2177 mActiveMenuBar->FindMenuWithShortcut(aKeyEvent, false);
2178 if (result) {
2179 mActiveMenuBar->SetActive(true);
2180 result->OpenMenu(true);
2181 return true;
2185 return false;
2188 bool nsXULPopupManager::HandleKeyboardNavigation(uint32_t aKeyCode) {
2189 if (nsMenuChainItem* nextitem = GetTopVisibleMenu()) {
2190 nextitem->Content()->OwnerDoc()->FlushPendingNotifications(
2191 FlushType::Frames);
2194 // navigate up through the open menus, looking for the topmost one
2195 // in the same hierarchy
2196 nsMenuChainItem* item = nullptr;
2197 nsMenuChainItem* nextitem = GetTopVisibleMenu();
2198 while (nextitem) {
2199 item = nextitem;
2200 nextitem = item->GetParent();
2202 if (nextitem) {
2203 // stop if the parent isn't a menu
2204 if (!nextitem->IsMenu()) break;
2206 // check to make sure that the parent is actually the parent menu. It
2207 // won't be if the parent is in a different frame hierarchy, for example,
2208 // for a context menu opened on another menu.
2209 nsMenuParent* expectedParent =
2210 static_cast<nsMenuParent*>(nextitem->Frame());
2211 nsMenuFrame* menuFrame = do_QueryFrame(item->Frame()->GetParent());
2212 if (!menuFrame || menuFrame->GetMenuParent() != expectedParent) {
2213 break;
2218 nsIFrame* itemFrame;
2219 if (item)
2220 itemFrame = item->Frame();
2221 else if (mActiveMenuBar)
2222 itemFrame = mActiveMenuBar;
2223 else
2224 return false;
2226 nsNavigationDirection theDirection;
2227 NS_ASSERTION(aKeyCode >= KeyboardEvent_Binding::DOM_VK_END &&
2228 aKeyCode <= KeyboardEvent_Binding::DOM_VK_DOWN,
2229 "Illegal key code");
2230 theDirection = NS_DIRECTION_FROM_KEY_CODE(itemFrame, aKeyCode);
2232 bool selectFirstItem = true;
2233 #ifdef MOZ_WIDGET_GTK
2234 nsMenuFrame* currentItem = nullptr;
2235 if (item && mActiveMenuBar && NS_DIRECTION_IS_INLINE(theDirection)) {
2236 currentItem = item->Frame()->GetCurrentMenuItem();
2237 // If nothing is selected in the menu and we have a menubar, let it
2238 // handle the movement not to steal focus from it.
2239 if (!currentItem) {
2240 item = nullptr;
2243 // On menu change, only select first item if an item is already selected.
2244 selectFirstItem = currentItem != nullptr;
2245 #endif
2247 // if a popup is open, first check for navigation within the popup
2248 if (item && HandleKeyboardNavigationInPopup(item, theDirection)) return true;
2250 // no popup handled the key, so check the active menubar, if any
2251 if (mActiveMenuBar) {
2252 nsMenuFrame* currentMenu = mActiveMenuBar->GetCurrentMenuItem();
2254 if (NS_DIRECTION_IS_INLINE(theDirection)) {
2255 nsMenuFrame* nextItem =
2256 (theDirection == eNavigationDirection_End)
2257 ? GetNextMenuItem(mActiveMenuBar, currentMenu, false, true)
2258 : GetPreviousMenuItem(mActiveMenuBar, currentMenu, false, true);
2259 mActiveMenuBar->ChangeMenuItem(nextItem, selectFirstItem, true);
2260 return true;
2261 } else if (NS_DIRECTION_IS_BLOCK(theDirection)) {
2262 // Open the menu and select its first item.
2263 if (currentMenu) {
2264 nsCOMPtr<nsIContent> content = currentMenu->GetContent();
2265 ShowMenu(content, true, false);
2267 return true;
2271 return false;
2274 bool nsXULPopupManager::HandleKeyboardNavigationInPopup(
2275 nsMenuChainItem* item, nsMenuPopupFrame* aFrame,
2276 nsNavigationDirection aDir) {
2277 NS_ASSERTION(aFrame, "aFrame is null");
2278 NS_ASSERTION(!item || item->Frame() == aFrame,
2279 "aFrame is expected to be equal to item->Frame()");
2281 nsMenuFrame* currentMenu = aFrame->GetCurrentMenuItem();
2283 aFrame->ClearIncrementalString();
2285 // This method only gets called if we're open.
2286 if (!currentMenu && NS_DIRECTION_IS_INLINE(aDir)) {
2287 // We've been opened, but we haven't had anything selected.
2288 // We can handle End, but our parent handles Start.
2289 if (aDir == eNavigationDirection_End) {
2290 nsMenuFrame* nextItem = GetNextMenuItem(aFrame, nullptr, true, false);
2291 if (nextItem) {
2292 aFrame->ChangeMenuItem(nextItem, false, true);
2293 return true;
2296 return false;
2299 bool isContainer = false;
2300 bool isOpen = false;
2301 if (currentMenu) {
2302 isOpen = currentMenu->IsOpen();
2303 isContainer = currentMenu->IsMenu();
2304 if (isOpen) {
2305 // for an open popup, have the child process the event
2306 nsMenuChainItem* child = item ? item->GetChild() : nullptr;
2307 if (child && HandleKeyboardNavigationInPopup(child, aDir)) return true;
2308 } else if (aDir == eNavigationDirection_End && isContainer &&
2309 !currentMenu->IsDisabled()) {
2310 // The menu is not yet open. Open it and select the first item.
2311 nsCOMPtr<nsIContent> content = currentMenu->GetContent();
2312 ShowMenu(content, true, false);
2313 return true;
2317 // For block progression, we can move in either direction
2318 if (NS_DIRECTION_IS_BLOCK(aDir) || NS_DIRECTION_IS_BLOCK_TO_EDGE(aDir)) {
2319 nsMenuFrame* nextItem;
2321 if (aDir == eNavigationDirection_Before ||
2322 aDir == eNavigationDirection_After) {
2323 // Cursor navigation does not wrap on Mac or for menulists on Windows.
2324 bool wrap =
2325 #ifdef XP_WIN
2326 !aFrame->IsMenuList();
2327 #elif defined XP_MACOSX
2328 false;
2329 #else
2330 true;
2331 #endif
2333 if (aDir == eNavigationDirection_Before) {
2334 nextItem = GetPreviousMenuItem(aFrame, currentMenu, true, wrap);
2335 } else {
2336 nextItem = GetNextMenuItem(aFrame, currentMenu, true, wrap);
2338 } else if (aDir == eNavigationDirection_First) {
2339 nextItem = GetNextMenuItem(aFrame, nullptr, true, false);
2340 } else {
2341 nextItem = GetPreviousMenuItem(aFrame, nullptr, true, false);
2344 if (nextItem) {
2345 aFrame->ChangeMenuItem(nextItem, false, true);
2346 return true;
2348 } else if (currentMenu && isContainer && isOpen) {
2349 if (aDir == eNavigationDirection_Start) {
2350 // close a submenu when Left is pressed
2351 nsMenuPopupFrame* popupFrame = currentMenu->GetPopup();
2352 if (popupFrame)
2353 HidePopup(popupFrame->GetContent(), false, false, false, false);
2354 return true;
2358 return false;
2361 bool nsXULPopupManager::HandleKeyboardEventWithKeyCode(
2362 KeyboardEvent* aKeyEvent, nsMenuChainItem* aTopVisibleMenuItem) {
2363 uint32_t keyCode = aKeyEvent->KeyCode();
2365 // Escape should close panels, but the other keys should have no effect.
2366 if (aTopVisibleMenuItem &&
2367 aTopVisibleMenuItem->PopupType() != ePopupTypeMenu) {
2368 if (keyCode == KeyboardEvent_Binding::DOM_VK_ESCAPE) {
2369 HidePopup(aTopVisibleMenuItem->Content(), false, false, false, true);
2370 aKeyEvent->StopPropagation();
2371 aKeyEvent->StopCrossProcessForwarding();
2372 aKeyEvent->PreventDefault();
2374 return true;
2377 bool consume = (aTopVisibleMenuItem || mActiveMenuBar);
2378 switch (keyCode) {
2379 case KeyboardEvent_Binding::DOM_VK_UP:
2380 case KeyboardEvent_Binding::DOM_VK_DOWN:
2381 #ifndef XP_MACOSX
2382 // roll up the popup when alt+up/down are pressed within a menulist.
2383 if (aKeyEvent->AltKey() && aTopVisibleMenuItem &&
2384 aTopVisibleMenuItem->Frame()->IsMenuList()) {
2385 Rollup(0, false, nullptr, nullptr);
2386 break;
2388 [[fallthrough]];
2389 #endif
2391 case KeyboardEvent_Binding::DOM_VK_LEFT:
2392 case KeyboardEvent_Binding::DOM_VK_RIGHT:
2393 case KeyboardEvent_Binding::DOM_VK_HOME:
2394 case KeyboardEvent_Binding::DOM_VK_END:
2395 HandleKeyboardNavigation(keyCode);
2396 break;
2398 case KeyboardEvent_Binding::DOM_VK_PAGE_DOWN:
2399 case KeyboardEvent_Binding::DOM_VK_PAGE_UP:
2400 if (aTopVisibleMenuItem) {
2401 aTopVisibleMenuItem->Frame()->ChangeByPage(
2402 keyCode == KeyboardEvent_Binding::DOM_VK_PAGE_UP);
2404 break;
2406 case KeyboardEvent_Binding::DOM_VK_ESCAPE:
2407 // Pressing Escape hides one level of menus only. If no menu is open,
2408 // check if a menubar is active and inform it that a menu closed. Even
2409 // though in this latter case, a menu didn't actually close, the effect
2410 // ends up being the same. Similar for the tab key below.
2411 if (aTopVisibleMenuItem) {
2412 HidePopup(aTopVisibleMenuItem->Content(), false, false, false, true);
2413 } else if (mActiveMenuBar) {
2414 mActiveMenuBar->MenuClosed();
2416 break;
2418 case KeyboardEvent_Binding::DOM_VK_TAB:
2419 #ifndef XP_MACOSX
2420 case KeyboardEvent_Binding::DOM_VK_F10:
2421 #endif
2422 if (aTopVisibleMenuItem &&
2423 !aTopVisibleMenuItem->Frame()->GetContent()->AsElement()->AttrValueIs(
2424 kNameSpaceID_None, nsGkAtoms::activateontab, nsGkAtoms::_true,
2425 eCaseMatters)) {
2426 // close popups or deactivate menubar when Tab or F10 are pressed
2427 Rollup(0, false, nullptr, nullptr);
2428 break;
2429 } else if (mActiveMenuBar) {
2430 mActiveMenuBar->MenuClosed();
2431 break;
2433 // Intentional fall-through to RETURN case
2434 [[fallthrough]];
2436 case KeyboardEvent_Binding::DOM_VK_RETURN: {
2437 // If there is a popup open, check if the current item needs to be opened.
2438 // Otherwise, tell the active menubar, if any, to activate the menu. The
2439 // Enter method will return a menu if one needs to be opened as a result.
2440 nsMenuFrame* menuToOpen = nullptr;
2441 WidgetGUIEvent* GUIEvent = aKeyEvent->WidgetEventPtr()->AsGUIEvent();
2443 if (aTopVisibleMenuItem) {
2444 menuToOpen = aTopVisibleMenuItem->Frame()->Enter(GUIEvent);
2445 } else if (mActiveMenuBar) {
2446 menuToOpen = mActiveMenuBar->Enter(GUIEvent);
2448 if (menuToOpen) {
2449 nsCOMPtr<nsIContent> content = menuToOpen->GetContent();
2450 ShowMenu(content, true, false);
2452 break;
2455 default:
2456 return false;
2459 if (consume) {
2460 aKeyEvent->StopPropagation();
2461 aKeyEvent->StopCrossProcessForwarding();
2462 aKeyEvent->PreventDefault();
2464 return true;
2467 // TODO(emilio): This should probably just walk the DOM instead and call
2468 // GetPrimaryFrame() on the items... Do we have anonymous / fallback menu items
2469 // that could be selectable?
2470 static nsIContent* FindDefaultInsertionPoint(nsIContent* aParent) {
2471 if (ShadowRoot* shadow = aParent->GetShadowRoot()) {
2472 if (HTMLSlotElement* slot = shadow->GetDefaultSlot()) {
2473 return slot;
2476 return aParent;
2479 nsContainerFrame* nsXULPopupManager::ImmediateParentFrame(
2480 nsContainerFrame* aFrame) {
2481 MOZ_ASSERT(aFrame && aFrame->GetContent());
2482 nsIContent* insertionPoint = FindDefaultInsertionPoint(aFrame->GetContent());
2484 nsCSSFrameConstructor* fc = aFrame->PresContext()->FrameConstructor();
2485 nsContainerFrame* insertionFrame =
2486 insertionPoint ? fc->GetContentInsertionFrameFor(insertionPoint)
2487 : nullptr;
2489 return insertionFrame ? insertionFrame : aFrame;
2492 nsMenuFrame* nsXULPopupManager::GetNextMenuItem(nsContainerFrame* aParent,
2493 nsMenuFrame* aStart,
2494 bool aIsPopup, bool aWrap) {
2495 nsContainerFrame* immediateParent = ImmediateParentFrame(aParent);
2496 nsIFrame* currFrame = nullptr;
2497 if (aStart) {
2498 if (aStart->GetNextSibling())
2499 currFrame = aStart->GetNextSibling();
2500 else if (aStart->GetParent()->GetContent()->IsXULElement(
2501 nsGkAtoms::menugroup))
2502 currFrame = aStart->GetParent()->GetNextSibling();
2503 } else
2504 currFrame = immediateParent->PrincipalChildList().FirstChild();
2506 while (currFrame) {
2507 // See if it's a menu item.
2508 nsIContent* currFrameContent = currFrame->GetContent();
2509 if (IsValidMenuItem(currFrameContent, aIsPopup)) {
2510 return do_QueryFrame(currFrame);
2512 if (currFrameContent->IsXULElement(nsGkAtoms::menugroup) &&
2513 currFrameContent->GetChildCount() > 0)
2514 currFrame = currFrame->PrincipalChildList().FirstChild();
2515 else if (!currFrame->GetNextSibling() &&
2516 currFrame->GetParent()->GetContent()->IsXULElement(
2517 nsGkAtoms::menugroup))
2518 currFrame = currFrame->GetParent()->GetNextSibling();
2519 else
2520 currFrame = currFrame->GetNextSibling();
2523 if (!aWrap) {
2524 return aStart;
2527 currFrame = immediateParent->PrincipalChildList().FirstChild();
2529 // Still don't have anything. Try cycling from the beginning.
2530 while (currFrame && currFrame != aStart) {
2531 // See if it's a menu item.
2532 nsIContent* currFrameContent = currFrame->GetContent();
2533 if (IsValidMenuItem(currFrameContent, aIsPopup)) {
2534 return do_QueryFrame(currFrame);
2536 if (currFrameContent->IsXULElement(nsGkAtoms::menugroup) &&
2537 currFrameContent->GetChildCount() > 0)
2538 currFrame = currFrame->PrincipalChildList().FirstChild();
2539 else if (!currFrame->GetNextSibling() &&
2540 currFrame->GetParent()->GetContent()->IsXULElement(
2541 nsGkAtoms::menugroup))
2542 currFrame = currFrame->GetParent()->GetNextSibling();
2543 else
2544 currFrame = currFrame->GetNextSibling();
2547 // No luck. Just return our start value.
2548 return aStart;
2551 nsMenuFrame* nsXULPopupManager::GetPreviousMenuItem(nsContainerFrame* aParent,
2552 nsMenuFrame* aStart,
2553 bool aIsPopup, bool aWrap) {
2554 nsContainerFrame* immediateParent = ImmediateParentFrame(aParent);
2555 const nsFrameList& frames(immediateParent->PrincipalChildList());
2557 nsIFrame* currFrame = nullptr;
2558 if (aStart) {
2559 if (aStart->GetPrevSibling())
2560 currFrame = aStart->GetPrevSibling();
2561 else if (aStart->GetParent()->GetContent()->IsXULElement(
2562 nsGkAtoms::menugroup))
2563 currFrame = aStart->GetParent()->GetPrevSibling();
2564 } else
2565 currFrame = frames.LastChild();
2567 while (currFrame) {
2568 // See if it's a menu item.
2569 nsIContent* currFrameContent = currFrame->GetContent();
2570 if (IsValidMenuItem(currFrameContent, aIsPopup)) {
2571 return do_QueryFrame(currFrame);
2573 if (currFrameContent->IsXULElement(nsGkAtoms::menugroup) &&
2574 currFrameContent->GetChildCount() > 0) {
2575 const nsFrameList& menugroupFrames(currFrame->PrincipalChildList());
2576 currFrame = menugroupFrames.LastChild();
2577 } else if (!currFrame->GetPrevSibling() &&
2578 currFrame->GetParent()->GetContent()->IsXULElement(
2579 nsGkAtoms::menugroup))
2580 currFrame = currFrame->GetParent()->GetPrevSibling();
2581 else
2582 currFrame = currFrame->GetPrevSibling();
2585 if (!aWrap) {
2586 return aStart;
2589 currFrame = frames.LastChild();
2591 // Still don't have anything. Try cycling from the end.
2592 while (currFrame && currFrame != aStart) {
2593 // See if it's a menu item.
2594 nsIContent* currFrameContent = currFrame->GetContent();
2595 if (IsValidMenuItem(currFrameContent, aIsPopup)) {
2596 return do_QueryFrame(currFrame);
2598 if (currFrameContent->IsXULElement(nsGkAtoms::menugroup) &&
2599 currFrameContent->GetChildCount() > 0) {
2600 const nsFrameList& menugroupFrames(currFrame->PrincipalChildList());
2601 currFrame = menugroupFrames.LastChild();
2602 } else if (!currFrame->GetPrevSibling() &&
2603 currFrame->GetParent()->GetContent()->IsXULElement(
2604 nsGkAtoms::menugroup))
2605 currFrame = currFrame->GetParent()->GetPrevSibling();
2606 else
2607 currFrame = currFrame->GetPrevSibling();
2610 // No luck. Just return our start value.
2611 return aStart;
2614 bool nsXULPopupManager::IsValidMenuItem(nsIContent* aContent, bool aOnPopup) {
2615 if (!aContent->IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menuitem)) {
2616 return false;
2619 nsMenuFrame* menuFrame = do_QueryFrame(aContent->GetPrimaryFrame());
2621 bool skipNavigatingDisabledMenuItem = true;
2622 if (aOnPopup && (!menuFrame || !menuFrame->IsParentMenuList())) {
2623 skipNavigatingDisabledMenuItem =
2624 LookAndFeel::GetInt(LookAndFeel::IntID::SkipNavigatingDisabledMenuItem,
2625 0) != 0;
2628 return !(skipNavigatingDisabledMenuItem && aContent->IsElement() &&
2629 aContent->AsElement()->AttrValueIs(kNameSpaceID_None,
2630 nsGkAtoms::disabled,
2631 nsGkAtoms::_true, eCaseMatters));
2634 nsresult nsXULPopupManager::HandleEvent(Event* aEvent) {
2635 RefPtr<KeyboardEvent> keyEvent = aEvent->AsKeyboardEvent();
2636 NS_ENSURE_TRUE(keyEvent, NS_ERROR_UNEXPECTED);
2638 // handlers shouldn't be triggered by non-trusted events.
2639 if (!keyEvent->IsTrusted()) {
2640 return NS_OK;
2643 nsAutoString eventType;
2644 keyEvent->GetType(eventType);
2645 if (eventType.EqualsLiteral("keyup")) {
2646 return KeyUp(keyEvent);
2648 if (eventType.EqualsLiteral("keydown")) {
2649 return KeyDown(keyEvent);
2651 if (eventType.EqualsLiteral("keypress")) {
2652 return KeyPress(keyEvent);
2655 MOZ_ASSERT_UNREACHABLE("Unexpected eventType");
2656 return NS_OK;
2659 nsresult nsXULPopupManager::UpdateIgnoreKeys(bool aIgnoreKeys) {
2660 nsMenuChainItem* item = GetTopVisibleMenu();
2661 if (item) {
2662 item->SetIgnoreKeys(aIgnoreKeys ? eIgnoreKeys_True : eIgnoreKeys_Shortcuts);
2664 UpdateKeyboardListeners();
2665 return NS_OK;
2668 nsresult nsXULPopupManager::KeyUp(KeyboardEvent* aKeyEvent) {
2669 // don't do anything if a menu isn't open or a menubar isn't active
2670 if (!mActiveMenuBar) {
2671 nsMenuChainItem* item = GetTopVisibleMenu();
2672 if (!item || item->PopupType() != ePopupTypeMenu) return NS_OK;
2674 if (item->IgnoreKeys() == eIgnoreKeys_Shortcuts) {
2675 aKeyEvent->StopCrossProcessForwarding();
2676 return NS_OK;
2680 aKeyEvent->StopPropagation();
2681 aKeyEvent->StopCrossProcessForwarding();
2682 aKeyEvent->PreventDefault();
2684 return NS_OK; // I am consuming event
2687 nsresult nsXULPopupManager::KeyDown(KeyboardEvent* aKeyEvent) {
2688 nsMenuChainItem* item = GetTopVisibleMenu();
2689 if (item && item->Frame()->IsMenuLocked()) return NS_OK;
2691 if (HandleKeyboardEventWithKeyCode(aKeyEvent, item)) {
2692 return NS_OK;
2695 // don't do anything if a menu isn't open or a menubar isn't active
2696 if (!mActiveMenuBar && (!item || item->PopupType() != ePopupTypeMenu))
2697 return NS_OK;
2699 // Since a menu was open, stop propagation of the event to keep other event
2700 // listeners from becoming confused.
2701 if (!item || item->IgnoreKeys() != eIgnoreKeys_Shortcuts) {
2702 aKeyEvent->StopPropagation();
2705 int32_t menuAccessKey = -1;
2707 // If the key just pressed is the access key (usually Alt),
2708 // dismiss and unfocus the menu.
2710 nsMenuBarListener::GetMenuAccessKey(&menuAccessKey);
2711 if (menuAccessKey) {
2712 uint32_t theChar = aKeyEvent->KeyCode();
2714 if (theChar == (uint32_t)menuAccessKey) {
2715 bool ctrl = (menuAccessKey != KeyboardEvent_Binding::DOM_VK_CONTROL &&
2716 aKeyEvent->CtrlKey());
2717 bool alt = (menuAccessKey != KeyboardEvent_Binding::DOM_VK_ALT &&
2718 aKeyEvent->AltKey());
2719 bool shift = (menuAccessKey != KeyboardEvent_Binding::DOM_VK_SHIFT &&
2720 aKeyEvent->ShiftKey());
2721 bool meta = (menuAccessKey != KeyboardEvent_Binding::DOM_VK_META &&
2722 aKeyEvent->MetaKey());
2723 if (!(ctrl || alt || shift || meta)) {
2724 // The access key just went down and no other
2725 // modifiers are already down.
2726 nsMenuChainItem* item = GetTopVisibleMenu();
2727 if (item && !item->Frame()->IsMenuList()) {
2728 Rollup(0, false, nullptr, nullptr);
2729 } else if (mActiveMenuBar) {
2730 mActiveMenuBar->MenuClosed();
2733 // Clear the item to avoid bugs as it may have been deleted during
2734 // rollup.
2735 item = nullptr;
2737 aKeyEvent->StopPropagation();
2738 aKeyEvent->PreventDefault();
2742 aKeyEvent->StopCrossProcessForwarding();
2743 return NS_OK;
2746 nsresult nsXULPopupManager::KeyPress(KeyboardEvent* aKeyEvent) {
2747 // Don't check prevent default flag -- menus always get first shot at key
2748 // events.
2750 nsMenuChainItem* item = GetTopVisibleMenu();
2751 if (item &&
2752 (item->Frame()->IsMenuLocked() || item->PopupType() != ePopupTypeMenu)) {
2753 return NS_OK;
2756 // if a menu is open or a menubar is active, it consumes the key event
2757 bool consume = (item || mActiveMenuBar);
2759 WidgetInputEvent* evt = aKeyEvent->WidgetEventPtr()->AsInputEvent();
2760 bool isAccel = evt && evt->IsAccel();
2762 // When ignorekeys="shortcuts" is used, we don't call preventDefault on the
2763 // key event when the accelerator key is pressed. This allows another
2764 // listener to handle keys. For instance, this allows global shortcuts to
2765 // still apply while a menu is open.
2766 if (item && item->IgnoreKeys() == eIgnoreKeys_Shortcuts && isAccel) {
2767 consume = false;
2770 HandleShortcutNavigation(aKeyEvent, nullptr);
2772 aKeyEvent->StopCrossProcessForwarding();
2773 if (consume) {
2774 aKeyEvent->StopPropagation();
2775 aKeyEvent->PreventDefault();
2778 return NS_OK; // I am consuming event
2781 NS_IMETHODIMP
2782 nsXULPopupHidingEvent::Run() {
2783 RefPtr<nsXULPopupManager> pm = nsXULPopupManager::GetInstance();
2784 Document* document = mPopup->GetUncomposedDoc();
2785 if (pm && document) {
2786 if (RefPtr<nsPresContext> presContext = document->GetPresContext()) {
2787 nsCOMPtr<nsIContent> popup = mPopup;
2788 nsCOMPtr<nsIContent> nextPopup = mNextPopup;
2789 nsCOMPtr<nsIContent> lastPopup = mLastPopup;
2790 pm->FirePopupHidingEvent(popup, nextPopup, lastPopup, presContext,
2791 mPopupType, mDeselectMenu, mIsRollup);
2794 return NS_OK;
2797 bool nsXULPopupPositionedEvent::DispatchIfNeeded(nsIContent* aPopup) {
2798 // The popuppositioned event only fires on arrow panels for now.
2799 if (aPopup->IsElement() &&
2800 aPopup->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
2801 nsGkAtoms::arrow, eCaseMatters)) {
2802 nsCOMPtr<nsIRunnable> event = new nsXULPopupPositionedEvent(aPopup);
2803 aPopup->OwnerDoc()->Dispatch(TaskCategory::Other, event.forget());
2804 return true;
2807 return false;
2810 static void AlignmentPositionToString(nsMenuPopupFrame* aFrame,
2811 nsAString& aString) {
2812 aString.Truncate();
2813 int8_t position = aFrame->GetAlignmentPosition();
2814 switch (position) {
2815 case POPUPPOSITION_AFTERSTART:
2816 return aString.AssignLiteral("after_start");
2817 case POPUPPOSITION_AFTEREND:
2818 return aString.AssignLiteral("after_end");
2819 case POPUPPOSITION_BEFORESTART:
2820 return aString.AssignLiteral("before_start");
2821 case POPUPPOSITION_BEFOREEND:
2822 return aString.AssignLiteral("before_end");
2823 case POPUPPOSITION_STARTBEFORE:
2824 return aString.AssignLiteral("start_before");
2825 case POPUPPOSITION_ENDBEFORE:
2826 return aString.AssignLiteral("end_before");
2827 case POPUPPOSITION_STARTAFTER:
2828 return aString.AssignLiteral("start_after");
2829 case POPUPPOSITION_ENDAFTER:
2830 return aString.AssignLiteral("end_after");
2831 case POPUPPOSITION_OVERLAP:
2832 return aString.AssignLiteral("overlap");
2833 case POPUPPOSITION_AFTERPOINTER:
2834 return aString.AssignLiteral("after_pointer");
2835 case POPUPPOSITION_SELECTION:
2836 return aString.AssignLiteral("selection");
2837 default:
2838 // Leave as an empty string.
2839 break;
2843 NS_IMETHODIMP
2844 nsXULPopupPositionedEvent::Run() {
2845 nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
2846 if (!pm) {
2847 return NS_OK;
2849 nsMenuPopupFrame* popupFrame = do_QueryFrame(mPopup->GetPrimaryFrame());
2850 if (!popupFrame) {
2851 return NS_OK;
2854 popupFrame->WillDispatchPopupPositioned();
2856 // At this point, hidePopup may have been called but it currently has no
2857 // way to stop this event. However, if hidePopup was called, the popup
2858 // will now be in the hiding or closed state. If we are in the shown or
2859 // positioning state instead, we can assume that we are still clear to
2860 // open/move the popup
2861 nsPopupState state = popupFrame->PopupState();
2862 if (state != ePopupPositioning && state != ePopupShown) {
2863 return NS_OK;
2866 // Note that the offset might be along either the X or Y axis, but for the
2867 // sake of simplicity we use a point with only the X axis set so we can
2868 // use ToNearestPixels().
2869 int32_t popupOffset = nsPoint(popupFrame->GetAlignmentOffset(), 0)
2870 .ToNearestPixels(AppUnitsPerCSSPixel())
2873 PopupPositionedEventInit init;
2874 init.mComposed = true;
2875 init.mAlignmentOffset = popupOffset;
2876 AlignmentPositionToString(popupFrame, init.mAlignmentPosition);
2877 RefPtr<PopupPositionedEvent> event =
2878 PopupPositionedEvent::Constructor(mPopup, u"popuppositioned"_ns, init);
2879 event->SetTrusted(true);
2881 mPopup->DispatchEvent(*event);
2883 // Get the popup frame and make sure it is still in the positioning
2884 // state. If it isn't, someone may have tried to reshow or hide it
2885 // during the popuppositioned event.
2886 // Alternately, this event may have been fired in reponse to moving the
2887 // popup rather than opening it. In that case, we are done.
2888 popupFrame = do_QueryFrame(mPopup->GetPrimaryFrame());
2889 if (popupFrame && popupFrame->PopupState() == ePopupPositioning) {
2890 pm->ShowPopupCallback(mPopup, popupFrame, false, false);
2893 return NS_OK;
2896 NS_IMETHODIMP
2897 nsXULMenuCommandEvent::Run() {
2898 nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
2899 if (!pm) return NS_OK;
2901 // The order of the nsViewManager and PresShell COM pointers is
2902 // important below. We want the pres shell to get released before the
2903 // associated view manager on exit from this function.
2904 // See bug 54233.
2905 // XXXndeakin is this still needed?
2907 nsCOMPtr<nsIContent> popup;
2908 nsMenuFrame* menuFrame = do_QueryFrame(mMenu->GetPrimaryFrame());
2909 AutoWeakFrame weakFrame(menuFrame);
2910 if (menuFrame && mFlipChecked) {
2911 if (menuFrame->IsChecked()) {
2912 mMenu->UnsetAttr(kNameSpaceID_None, nsGkAtoms::checked, true);
2913 } else {
2914 mMenu->SetAttr(kNameSpaceID_None, nsGkAtoms::checked, u"true"_ns, true);
2918 if (menuFrame && weakFrame.IsAlive()) {
2919 // Find the popup that the menu is inside. Below, this popup will
2920 // need to be hidden.
2921 nsIFrame* frame = menuFrame->GetParent();
2922 while (frame) {
2923 nsMenuPopupFrame* popupFrame = do_QueryFrame(frame);
2924 if (popupFrame) {
2925 popup = popupFrame->GetContent();
2926 break;
2928 frame = frame->GetParent();
2931 nsPresContext* presContext = menuFrame->PresContext();
2932 RefPtr<PresShell> presShell = presContext->PresShell();
2933 RefPtr<nsViewManager> kungFuDeathGrip = presShell->GetViewManager();
2934 mozilla::Unused
2935 << kungFuDeathGrip; // Not referred to directly within this function
2937 // Deselect ourselves.
2938 if (mCloseMenuMode != CloseMenuMode_None) menuFrame->SelectMenu(false);
2940 AutoHandlingUserInputStatePusher userInpStatePusher(mUserInput);
2941 RefPtr<Element> menu = mMenu;
2942 nsContentUtils::DispatchXULCommand(
2943 menu, mIsTrusted, nullptr, presShell, mModifiers & MODIFIER_CONTROL,
2944 mModifiers & MODIFIER_ALT, mModifiers & MODIFIER_SHIFT,
2945 mModifiers & MODIFIER_META);
2948 if (popup && mCloseMenuMode != CloseMenuMode_None)
2949 pm->HidePopup(popup, mCloseMenuMode == CloseMenuMode_Auto, true, false,
2950 false);
2952 return NS_OK;