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