gecko-dev/layout/xul/nsXULPopupManager.cpp

3010 строки
105 KiB
C++

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "mozilla/Assertions.h"
#include "nsGkAtoms.h"
#include "nsXULPopupManager.h"
#include "nsMenuFrame.h"
#include "nsMenuPopupFrame.h"
#include "nsMenuBarFrame.h"
#include "nsMenuBarListener.h"
#include "nsContentUtils.h"
#include "nsXULElement.h"
#include "nsIDOMXULCommandDispatcher.h"
#include "nsCSSFrameConstructor.h"
#include "nsGlobalWindow.h"
#include "nsIContentInlines.h"
#include "nsLayoutUtils.h"
#include "nsViewManager.h"
#include "nsITimer.h"
#include "nsFocusManager.h"
#include "nsIDocShell.h"
#include "nsPIDOMWindow.h"
#include "nsIInterfaceRequestorUtils.h"
#include "nsIBaseWindow.h"
#include "nsCaret.h"
#include "mozilla/dom/Document.h"
#include "nsPIWindowRoot.h"
#include "nsFrameManager.h"
#include "nsPresContextInlines.h"
#include "nsIObserverService.h"
#include "mozilla/AnimationUtils.h"
#include "mozilla/dom/DocumentInlines.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/Event.h" // for Event
#include "mozilla/dom/HTMLSlotElement.h"
#include "mozilla/dom/KeyboardEvent.h"
#include "mozilla/dom/KeyboardEventBinding.h"
#include "mozilla/dom/MouseEvent.h"
#include "mozilla/dom/UIEvent.h"
#include "mozilla/dom/UserActivation.h"
#include "mozilla/dom/PopupPositionedEvent.h"
#include "mozilla/dom/PopupPositionedEventBinding.h"
#include "mozilla/dom/XULCommandEvent.h"
#include "mozilla/EventDispatcher.h"
#include "mozilla/EventStateManager.h"
#include "mozilla/LookAndFeel.h"
#include "mozilla/MouseEvents.h"
#include "mozilla/PresShell.h"
#include "mozilla/Services.h"
#include "mozilla/StaticPrefs_ui.h"
#include "mozilla/StaticPrefs_xul.h"
#include "mozilla/widget/nsAutoRollup.h"
#include "mozilla/widget/NativeMenuSupport.h"
using namespace mozilla;
using namespace mozilla::dom;
using mozilla::widget::NativeMenu;
static_assert(KeyboardEvent_Binding::DOM_VK_HOME ==
KeyboardEvent_Binding::DOM_VK_END + 1 &&
KeyboardEvent_Binding::DOM_VK_LEFT ==
KeyboardEvent_Binding::DOM_VK_END + 2 &&
KeyboardEvent_Binding::DOM_VK_UP ==
KeyboardEvent_Binding::DOM_VK_END + 3 &&
KeyboardEvent_Binding::DOM_VK_RIGHT ==
KeyboardEvent_Binding::DOM_VK_END + 4 &&
KeyboardEvent_Binding::DOM_VK_DOWN ==
KeyboardEvent_Binding::DOM_VK_END + 5,
"nsXULPopupManager assumes some keyCode values are consecutive");
const nsNavigationDirection DirectionFromKeyCodeTable[2][6] = {
{
eNavigationDirection_Last, // KeyboardEvent_Binding::DOM_VK_END
eNavigationDirection_First, // KeyboardEvent_Binding::DOM_VK_HOME
eNavigationDirection_Start, // KeyboardEvent_Binding::DOM_VK_LEFT
eNavigationDirection_Before, // KeyboardEvent_Binding::DOM_VK_UP
eNavigationDirection_End, // KeyboardEvent_Binding::DOM_VK_RIGHT
eNavigationDirection_After // KeyboardEvent_Binding::DOM_VK_DOWN
},
{
eNavigationDirection_Last, // KeyboardEvent_Binding::DOM_VK_END
eNavigationDirection_First, // KeyboardEvent_Binding::DOM_VK_HOME
eNavigationDirection_End, // KeyboardEvent_Binding::DOM_VK_LEFT
eNavigationDirection_Before, // KeyboardEvent_Binding::DOM_VK_UP
eNavigationDirection_Start, // KeyboardEvent_Binding::DOM_VK_RIGHT
eNavigationDirection_After // KeyboardEvent_Binding::DOM_VK_DOWN
}};
nsXULPopupManager* nsXULPopupManager::sInstance = nullptr;
PendingPopup::PendingPopup(nsIContent* aPopup, mozilla::dom::Event* aEvent)
: mPopup(aPopup), mEvent(aEvent), mModifiers(0) {
InitMousePoint();
}
void PendingPopup::InitMousePoint() {
// get the event coordinates relative to the root frame of the document
// containing the popup.
if (!mEvent) {
return;
}
WidgetEvent* event = mEvent->WidgetEventPtr();
WidgetInputEvent* inputEvent = event->AsInputEvent();
if (inputEvent) {
mModifiers = inputEvent->mModifiers;
}
Document* doc = mPopup->GetUncomposedDoc();
if (!doc) {
return;
}
PresShell* presShell = doc->GetPresShell();
nsPresContext* presContext;
if (presShell && (presContext = presShell->GetPresContext())) {
nsPresContext* rootDocPresContext = presContext->GetRootPresContext();
if (!rootDocPresContext) {
return;
}
nsIFrame* rootDocumentRootFrame =
rootDocPresContext->PresShell()->GetRootFrame();
if ((event->mClass == eMouseEventClass ||
event->mClass == eMouseScrollEventClass ||
event->mClass == eWheelEventClass) &&
!event->AsGUIEvent()->mWidget) {
// no widget, so just use the client point if available
MouseEvent* mouseEvent = mEvent->AsMouseEvent();
nsIntPoint clientPt(mouseEvent->ClientX(), mouseEvent->ClientY());
// XXX this doesn't handle IFRAMEs in transforms
nsPoint thisDocToRootDocOffset =
presShell->GetRootFrame()->GetOffsetToCrossDoc(rootDocumentRootFrame);
// convert to device pixels
mMousePoint.x = presContext->AppUnitsToDevPixels(
nsPresContext::CSSPixelsToAppUnits(clientPt.x) +
thisDocToRootDocOffset.x);
mMousePoint.y = presContext->AppUnitsToDevPixels(
nsPresContext::CSSPixelsToAppUnits(clientPt.y) +
thisDocToRootDocOffset.y);
} else if (rootDocumentRootFrame) {
nsPoint pnt = nsLayoutUtils::GetEventCoordinatesRelativeTo(
event, RelativeTo{rootDocumentRootFrame});
mMousePoint =
LayoutDeviceIntPoint(rootDocPresContext->AppUnitsToDevPixels(pnt.x),
rootDocPresContext->AppUnitsToDevPixels(pnt.y));
}
}
}
already_AddRefed<nsIContent> PendingPopup::GetTriggerContent() const {
nsCOMPtr<nsIContent> target =
do_QueryInterface(mEvent ? mEvent->GetTarget() : nullptr);
return target.forget();
}
uint16_t PendingPopup::MouseInputSource() const {
if (mEvent) {
mozilla::WidgetMouseEventBase* mouseEvent =
mEvent->WidgetEventPtr()->AsMouseEventBase();
if (mouseEvent) {
return mouseEvent->mInputSource;
}
RefPtr<XULCommandEvent> commandEvent = mEvent->AsXULCommandEvent();
if (commandEvent) {
return commandEvent->InputSource();
}
}
return MouseEvent_Binding::MOZ_SOURCE_UNKNOWN;
}
nsIContent* nsMenuChainItem::Content() { return mFrame->GetContent(); }
void nsMenuChainItem::SetParent(nsMenuChainItem* aParent) {
if (mParent) {
NS_ASSERTION(mParent->mChild == this,
"Unexpected - parent's child not set to this");
mParent->mChild = nullptr;
}
mParent = aParent;
if (mParent) {
if (mParent->mChild) mParent->mChild->mParent = nullptr;
mParent->mChild = this;
}
}
void nsMenuChainItem::Detach(nsMenuChainItem** aRoot) {
// If the item has a child, set the child's parent to this item's parent,
// effectively removing the item from the chain. If the item has no child,
// just set the parent to null.
if (mChild) {
NS_ASSERTION(this != *aRoot,
"Unexpected - popup with child at end of chain");
mChild->SetParent(mParent);
} else {
// An item without a child should be the first item in the chain, so set
// the first item pointer, pointed to by aRoot, to the parent.
NS_ASSERTION(this == *aRoot,
"Unexpected - popup with no child not at end of chain");
*aRoot = mParent;
SetParent(nullptr);
}
}
void nsMenuChainItem::UpdateFollowAnchor() {
mFollowAnchor = mFrame->ShouldFollowAnchor(mCurrentRect);
}
void nsMenuChainItem::CheckForAnchorChange() {
if (mFollowAnchor) {
mFrame->CheckForAnchorChange(mCurrentRect);
}
}
NS_IMPL_ISUPPORTS(nsXULPopupManager, nsIDOMEventListener, nsIObserver)
nsXULPopupManager::nsXULPopupManager()
: mActiveMenuBar(nullptr),
mPopups(nullptr),
mTimerMenu(nullptr),
mPendingPopup(nullptr) {
nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
if (obs) {
obs->AddObserver(this, "xpcom-shutdown", false);
}
}
nsXULPopupManager::~nsXULPopupManager() {
NS_ASSERTION(!mPopups, "XUL popups still open");
if (mNativeMenu) {
mNativeMenu->RemoveObserver(this);
}
}
nsresult nsXULPopupManager::Init() {
sInstance = new nsXULPopupManager();
NS_ENSURE_TRUE(sInstance, NS_ERROR_OUT_OF_MEMORY);
NS_ADDREF(sInstance);
return NS_OK;
}
void nsXULPopupManager::Shutdown() { NS_IF_RELEASE(sInstance); }
NS_IMETHODIMP
nsXULPopupManager::Observe(nsISupports* aSubject, const char* aTopic,
const char16_t* aData) {
if (!nsCRT::strcmp(aTopic, "xpcom-shutdown")) {
if (mKeyListener) {
mKeyListener->RemoveEventListener(u"keypress"_ns, this, true);
mKeyListener->RemoveEventListener(u"keydown"_ns, this, true);
mKeyListener->RemoveEventListener(u"keyup"_ns, this, true);
mKeyListener = nullptr;
}
nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
if (obs) {
obs->RemoveObserver(this, "xpcom-shutdown");
}
}
return NS_OK;
}
nsXULPopupManager* nsXULPopupManager::GetInstance() {
MOZ_ASSERT(sInstance);
return sInstance;
}
bool nsXULPopupManager::RollupNativeMenu() {
if (mNativeMenu) {
RefPtr<NativeMenu> menu = mNativeMenu;
return menu->Close();
}
return false;
}
bool nsXULPopupManager::Rollup(uint32_t aCount, bool aFlush,
const LayoutDeviceIntPoint* pos,
nsIContent** aLastRolledUp) {
if (aLastRolledUp) {
*aLastRolledUp = nullptr;
}
// We can disable the autohide behavior via a pref to ease debugging.
if (StaticPrefs::ui_popup_disable_autohide()) {
// Required on linux to allow events to work on other targets.
if (mWidget) {
mWidget->CaptureRollupEvents(nullptr, false);
}
return false;
}
bool consume = false;
nsMenuChainItem* item = GetTopVisibleMenu();
if (item) {
if (aLastRolledUp) {
// We need to get the popup that will be closed last, so that widget can
// keep track of it so it doesn't reopen if a mousedown event is going to
// processed. Keep going up the menu chain to get the first level menu of
// the same type. If a different type is encountered it means we have,
// for example, a menulist or context menu inside a panel, and we want to
// treat these as distinct. It's possible that this menu doesn't end up
// closing because the popuphiding event was cancelled, but in that case
// we don't need to deal with the menu reopening as it will already still
// be open.
nsMenuChainItem* first = item;
while (first->GetParent()) {
nsMenuChainItem* parent = first->GetParent();
if (first->Frame()->PopupType() != parent->Frame()->PopupType() ||
first->IsContextMenu() != parent->IsContextMenu()) {
break;
}
first = parent;
}
*aLastRolledUp = first->Content();
}
ConsumeOutsideClicksResult consumeResult =
item->Frame()->ConsumeOutsideClicks();
consume = (consumeResult == ConsumeOutsideClicks_True);
bool rollup = true;
// If norolluponanchor is true, then don't rollup when clicking the anchor.
// This would be used to allow adjusting the caret position in an
// autocomplete field without hiding the popup for example.
bool noRollupOnAnchor =
(!consume && pos &&
item->Frame()->GetContent()->AsElement()->AttrValueIs(
kNameSpaceID_None, nsGkAtoms::norolluponanchor, nsGkAtoms::_true,
eCaseMatters));
// When ConsumeOutsideClicks_ParentOnly is used, always consume the click
// when the click was over the anchor. This way, clicking on a menu doesn't
// reopen the menu.
if ((consumeResult == ConsumeOutsideClicks_ParentOnly ||
noRollupOnAnchor) &&
pos) {
nsMenuPopupFrame* popupFrame = item->Frame();
CSSIntRect anchorRect = [&] {
if (popupFrame->IsAnchored()) {
// Check if the popup has a screen anchor rectangle. If not, get the
// rectangle from the anchor element.
auto r = popupFrame->GetScreenAnchorRect();
if (r.x != -1 && r.y != -1) {
return r;
}
}
auto* anchor = Element::FromNodeOrNull(popupFrame->GetAnchor());
if (!anchor) {
return CSSIntRect();
}
// Check if the anchor has indicated another node to use for checking
// for roll-up. That way, we can anchor a popup on anonymous content
// or an individual icon, while clicking elsewhere within a button or
// other container doesn't result in us re-opening the popup.
nsAutoString consumeAnchor;
anchor->GetAttr(nsGkAtoms::consumeanchor, consumeAnchor);
if (!consumeAnchor.IsEmpty()) {
if (Element* newAnchor =
anchor->OwnerDoc()->GetElementById(consumeAnchor)) {
anchor = newAnchor;
}
}
nsIFrame* f = anchor->GetPrimaryFrame();
if (!f) {
return CSSIntRect();
}
return f->GetScreenRect();
}();
// It's possible that some other element is above the anchor at the same
// position, but the only thing that would happen is that the mouse
// event will get consumed, so here only a quick coordinates check is
// done rather than a slower complete check of what is at that location.
nsPresContext* presContext = item->Frame()->PresContext();
CSSIntPoint posCSSPixels = presContext->DevPixelsToIntCSSPixels(*pos);
if (anchorRect.Contains(posCSSPixels)) {
if (consumeResult == ConsumeOutsideClicks_ParentOnly) {
consume = true;
}
if (noRollupOnAnchor) {
rollup = false;
}
}
}
if (rollup) {
// if a number of popups to close has been specified, determine the last
// popup to close
nsIContent* lastPopup = nullptr;
if (aCount != UINT32_MAX) {
nsMenuChainItem* last = item;
while (--aCount && last->GetParent()) {
last = last->GetParent();
}
if (last) {
lastPopup = last->Content();
}
}
nsPresContext* presContext = item->Frame()->PresContext();
RefPtr<nsViewManager> viewManager =
presContext->PresShell()->GetViewManager();
HidePopup(item->Content(), true, true, false, true, lastPopup);
if (aFlush) {
// The popup's visibility doesn't update until the minimize animation
// has finished, so call UpdateWidgetGeometry to update it right away.
viewManager->UpdateWidgetGeometry();
}
}
}
return consume;
}
////////////////////////////////////////////////////////////////////////
bool nsXULPopupManager::ShouldRollupOnMouseWheelEvent() {
// should rollup only for autocomplete widgets
// XXXndeakin this should really be something the popup has more control over
nsMenuChainItem* item = GetTopVisibleMenu();
if (!item) return false;
nsIContent* content = item->Frame()->GetContent();
if (!content || !content->IsElement()) return false;
Element* element = content->AsElement();
if (element->AttrValueIs(kNameSpaceID_None, nsGkAtoms::rolluponmousewheel,
nsGkAtoms::_true, eCaseMatters))
return true;
if (element->AttrValueIs(kNameSpaceID_None, nsGkAtoms::rolluponmousewheel,
nsGkAtoms::_false, eCaseMatters))
return false;
nsAutoString value;
element->GetAttr(kNameSpaceID_None, nsGkAtoms::type, value);
return StringBeginsWith(value, u"autocomplete"_ns);
}
bool nsXULPopupManager::ShouldConsumeOnMouseWheelEvent() {
nsMenuChainItem* item = GetTopVisibleMenu();
if (!item) return false;
nsMenuPopupFrame* frame = item->Frame();
if (frame->PopupType() != ePopupTypePanel) return true;
return !frame->GetContent()->AsElement()->AttrValueIs(
kNameSpaceID_None, nsGkAtoms::type, nsGkAtoms::arrow, eCaseMatters);
}
// a menu should not roll up if activated by a mouse activate message (eg.
// X-mouse)
bool nsXULPopupManager::ShouldRollupOnMouseActivate() { return false; }
uint32_t nsXULPopupManager::GetSubmenuWidgetChain(
nsTArray<nsIWidget*>* aWidgetChain) {
// this method is used by the widget code to determine the list of popups
// that are open. If a mouse click occurs outside one of these popups, the
// panels will roll up. If the click is inside a popup, they will not roll up
uint32_t count = 0, sameTypeCount = 0;
NS_ASSERTION(aWidgetChain, "null parameter");
nsMenuChainItem* item = GetTopVisibleMenu();
while (item) {
nsMenuChainItem* parent = item->GetParent();
if (!item->IsNoAutoHide()) {
nsCOMPtr<nsIWidget> widget = item->Frame()->GetWidget();
NS_ASSERTION(widget, "open popup has no widget");
aWidgetChain->AppendElement(widget.get());
// In the case when a menulist inside a panel is open, clicking in the
// panel should still roll up the menu, so if a different type is found,
// stop scanning.
if (!sameTypeCount) {
count++;
if (!parent ||
item->Frame()->PopupType() != parent->Frame()->PopupType() ||
item->IsContextMenu() != parent->IsContextMenu()) {
sameTypeCount = count;
}
}
}
item = parent;
}
return sameTypeCount;
}
nsIWidget* nsXULPopupManager::GetRollupWidget() {
nsMenuChainItem* item = GetTopVisibleMenu();
return item ? item->Frame()->GetWidget() : nullptr;
}
void nsXULPopupManager::AdjustPopupsOnWindowChange(
nsPIDOMWindowOuter* aWindow) {
// When the parent window is moved, adjust any child popups. Dismissable
// menus and panels are expected to roll up when a window is moved, so there
// is no need to check these popups, only the noautohide popups.
// The items are added to a list so that they can be adjusted bottom to top.
nsTArray<nsMenuPopupFrame*> list;
nsMenuChainItem* item = mPopups;
while (item) {
// only move popups that are within the same window and where auto
// positioning has not been disabled
nsMenuPopupFrame* frame = item->Frame();
if (item->IsNoAutoHide()) {
nsIContent* popup = frame->GetContent();
if (popup) {
Document* document = popup->GetUncomposedDoc();
if (document) {
if (nsPIDOMWindowOuter* window = document->GetWindow()) {
window = window->GetPrivateRoot();
if (window == aWindow) {
list.AppendElement(frame);
}
}
}
}
}
item = item->GetParent();
}
for (int32_t l = list.Length() - 1; l >= 0; l--) {
list[l]->SetPopupPosition(nullptr, true, false);
}
}
void nsXULPopupManager::AdjustPopupsOnWindowChange(PresShell* aPresShell) {
if (aPresShell->GetDocument()) {
AdjustPopupsOnWindowChange(aPresShell->GetDocument()->GetWindow());
}
}
static nsMenuPopupFrame* GetPopupToMoveOrResize(nsIFrame* aFrame) {
nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(aFrame);
if (!menuPopupFrame) return nullptr;
// no point moving or resizing hidden popups
if (!menuPopupFrame->IsVisible()) return nullptr;
nsIWidget* widget = menuPopupFrame->GetWidget();
if (widget && !widget->IsVisible()) return nullptr;
return menuPopupFrame;
}
void nsXULPopupManager::PopupMoved(nsIFrame* aFrame, nsIntPoint aPnt,
bool aByMoveToRect) {
nsMenuPopupFrame* menuPopupFrame = GetPopupToMoveOrResize(aFrame);
if (!menuPopupFrame) {
return;
}
nsView* view = menuPopupFrame->GetView();
if (!view) {
return;
}
menuPopupFrame->WidgetPositionOrSizeDidChange();
// Don't do anything if the popup is already at the specified location. This
// prevents recursive calls when a popup is positioned.
LayoutDeviceIntRect curDevSize = view->CalcWidgetBounds(eWindowType_popup);
nsIWidget* widget = menuPopupFrame->GetWidget();
if (curDevSize.x == aPnt.x && curDevSize.y == aPnt.y &&
(!widget ||
widget->GetClientOffset() == menuPopupFrame->GetLastClientOffset())) {
return;
}
// Update the popup's position using SetPopupPosition if the popup is
// anchored and at the parent level as these maintain their position
// relative to the parent window. Otherwise, just update the popup to
// the specified screen coordinates.
if (menuPopupFrame->IsAnchored() &&
menuPopupFrame->PopupLevel() == ePopupLevelParent) {
menuPopupFrame->SetPopupPosition(nullptr, true, false);
} else {
CSSPoint cssPos = LayoutDeviceIntPoint::FromUnknownPoint(aPnt) /
menuPopupFrame->PresContext()->CSSToDevPixelScale();
menuPopupFrame->MoveTo(cssPos, false, aByMoveToRect);
}
}
void nsXULPopupManager::PopupResized(nsIFrame* aFrame,
LayoutDeviceIntSize aSize) {
nsMenuPopupFrame* menuPopupFrame = GetPopupToMoveOrResize(aFrame);
if (!menuPopupFrame) {
return;
}
menuPopupFrame->WidgetPositionOrSizeDidChange();
nsView* view = menuPopupFrame->GetView();
if (!view) {
return;
}
LayoutDeviceIntRect curDevSize = view->CalcWidgetBounds(eWindowType_popup);
// If the size is what we think it is, we have nothing to do.
if (curDevSize.width == aSize.width && curDevSize.height == aSize.height) {
return;
}
Element* popup = menuPopupFrame->GetContent()->AsElement();
// Only set the width and height if the popup already has these attributes.
if (!popup->HasAttr(kNameSpaceID_None, nsGkAtoms::width) ||
!popup->HasAttr(kNameSpaceID_None, nsGkAtoms::height)) {
return;
}
// The size is different. Convert the actual size to css pixels and store it
// as 'width' and 'height' attributes on the popup.
nsPresContext* presContext = menuPopupFrame->PresContext();
CSSIntSize newCSS(presContext->DevPixelsToIntCSSPixels(aSize.width),
presContext->DevPixelsToIntCSSPixels(aSize.height));
nsAutoString width, height;
width.AppendInt(newCSS.width);
height.AppendInt(newCSS.height);
popup->SetAttr(kNameSpaceID_None, nsGkAtoms::width, width, false);
popup->SetAttr(kNameSpaceID_None, nsGkAtoms::height, height, true);
}
nsMenuPopupFrame* nsXULPopupManager::GetPopupFrameForContent(
nsIContent* aContent, bool aShouldFlush) {
if (aShouldFlush) {
Document* document = aContent->GetUncomposedDoc();
if (document) {
if (RefPtr<PresShell> presShell = document->GetPresShell()) {
presShell->FlushPendingNotifications(FlushType::Layout);
}
}
}
return do_QueryFrame(aContent->GetPrimaryFrame());
}
nsMenuChainItem* nsXULPopupManager::GetTopVisibleMenu() {
nsMenuChainItem* item = mPopups;
while (item) {
if (!item->IsNoAutoHide() &&
item->Frame()->PopupState() != ePopupInvisible) {
return item;
}
item = item->GetParent();
}
return nullptr;
}
void nsXULPopupManager::SetActiveMenuBar(nsMenuBarFrame* aMenuBar,
bool aActivate) {
if (aActivate)
mActiveMenuBar = aMenuBar;
else if (mActiveMenuBar == aMenuBar)
mActiveMenuBar = nullptr;
UpdateKeyboardListeners();
}
static CloseMenuMode GetCloseMenuMode(nsIContent* aMenu) {
if (!aMenu->IsElement()) {
return CloseMenuMode_Auto;
}
static Element::AttrValuesArray strings[] = {nsGkAtoms::none,
nsGkAtoms::single, nullptr};
switch (aMenu->AsElement()->FindAttrValueIn(
kNameSpaceID_None, nsGkAtoms::closemenu, strings, eCaseMatters)) {
case 0:
return CloseMenuMode_None;
case 1:
return CloseMenuMode_Single;
default:
return CloseMenuMode_Auto;
}
}
auto nsXULPopupManager::MayShowMenu(nsIContent* aMenu) -> MayShowMenuResult {
if (mNativeMenu && aMenu->IsElement() &&
mNativeMenu->Element()->Contains(aMenu)) {
return {true};
}
nsMenuFrame* menuFrame = do_QueryFrame(aMenu->GetPrimaryFrame());
if (!menuFrame || !menuFrame->IsMenu()) {
return {};
}
nsMenuPopupFrame* popupFrame = menuFrame->GetPopup();
if (!popupFrame || !MayShowPopup(popupFrame)) {
return {};
}
return {false, menuFrame, popupFrame};
}
void nsXULPopupManager::ShowMenu(nsIContent* aMenu, bool aSelectFirstItem) {
auto mayShowResult = MayShowMenu(aMenu);
if (!mayShowResult) {
return;
}
if (mayShowResult.mIsNative) {
mNativeMenu->OpenSubmenu(aMenu->AsElement());
return;
}
nsMenuFrame* menuFrame = mayShowResult.mMenuFrame;
nsMenuPopupFrame* popupFrame = mayShowResult.mMenuPopupFrame;
// inherit whether or not we're a context menu from the parent
bool parentIsContextMenu = false;
bool onMenuBar = false;
bool onmenu = menuFrame->IsOnMenu();
nsMenuParent* parent = menuFrame->GetMenuParent();
if (parent && onmenu) {
parentIsContextMenu = parent->IsContextMenu();
onMenuBar = parent->IsMenuBar();
}
nsAutoString position;
#ifdef XP_MACOSX
if (aMenu->IsXULElement(nsGkAtoms::menulist)) {
position.AssignLiteral("selection");
} else
#endif
if (onMenuBar || !onmenu)
position.AssignLiteral("after_start");
else
position.AssignLiteral("end_before");
// there is no trigger event for menus
popupFrame->InitializePopup(aMenu, nullptr, position, 0, 0,
MenuPopupAnchorType_Node, true);
PendingPopup pendingPopup(popupFrame->GetContent(), nullptr);
BeginShowingPopup(pendingPopup, parentIsContextMenu, aSelectFirstItem);
}
void nsXULPopupManager::ShowPopup(nsIContent* aPopup,
nsIContent* aAnchorContent,
const nsAString& aPosition, int32_t aXPos,
int32_t aYPos, bool aIsContextMenu,
bool aAttributesOverride,
bool aSelectFirstItem, Event* aTriggerEvent) {
nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(aPopup, true);
if (!popupFrame || !MayShowPopup(popupFrame)) return;
PendingPopup pendingPopup(aPopup, aTriggerEvent);
nsCOMPtr<nsIContent> triggerContent = pendingPopup.GetTriggerContent();
popupFrame->InitializePopup(aAnchorContent, triggerContent, aPosition, aXPos,
aYPos, MenuPopupAnchorType_Node,
aAttributesOverride);
BeginShowingPopup(pendingPopup, aIsContextMenu, aSelectFirstItem);
}
static bool ShouldUseNativeContextMenus() {
#ifdef HAS_NATIVE_MENU_SUPPORT
return mozilla::widget::NativeMenuSupport::ShouldUseNativeContextMenus();
#else
return false;
#endif
}
void nsXULPopupManager::ShowPopupAtScreen(nsIContent* aPopup, int32_t aXPos,
int32_t aYPos, bool aIsContextMenu,
Event* aTriggerEvent) {
if (aIsContextMenu && ShouldUseNativeContextMenus() &&
ShowPopupAsNativeMenu(aPopup, aXPos, aYPos, aIsContextMenu,
aTriggerEvent)) {
return;
}
nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(aPopup, true);
if (!popupFrame || !MayShowPopup(popupFrame)) return;
PendingPopup pendingPopup(aPopup, aTriggerEvent);
nsCOMPtr<nsIContent> triggerContent = pendingPopup.GetTriggerContent();
popupFrame->InitializePopupAtScreen(triggerContent, aXPos, aYPos,
aIsContextMenu);
BeginShowingPopup(pendingPopup, aIsContextMenu, false);
}
bool nsXULPopupManager::ShowPopupAsNativeMenu(nsIContent* aPopup, int32_t aXPos,
int32_t aYPos,
bool aIsContextMenu,
Event* aTriggerEvent) {
if (mNativeMenu) {
NS_WARNING("Native menu still open when trying to open another");
RefPtr<NativeMenu> menu = mNativeMenu;
(void)menu->Close();
menu->RemoveObserver(this);
mNativeMenu = nullptr;
}
RefPtr<NativeMenu> menu;
#ifdef HAS_NATIVE_MENU_SUPPORT
if (aPopup->IsElement()) {
menu = mozilla::widget::NativeMenuSupport::CreateNativeContextMenu(
aPopup->AsElement());
}
#endif
if (!menu) {
return false;
}
nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(aPopup, true);
if (!popupFrame) {
return true;
}
// Hide the menu from our accessibility code so that we don't dispatch custom
// accessibility notifications which would conflict with the system ones.
aPopup->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::aria_hidden,
u"true"_ns, true);
PendingPopup pendingPopup(aPopup, aTriggerEvent);
nsCOMPtr<nsIContent> triggerContent = pendingPopup.GetTriggerContent();
popupFrame->InitializePopupAsNativeContextMenu(triggerContent, aXPos, aYPos);
RefPtr<nsPresContext> presContext = popupFrame->PresContext();
nsEventStatus status = FirePopupShowingEvent(pendingPopup, presContext);
// if the event was cancelled, don't open the popup, reset its state back
// to closed and clear its trigger content.
if (status == nsEventStatus_eConsumeNoDefault) {
if (nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(aPopup, true)) {
popupFrame->SetPopupState(ePopupClosed);
popupFrame->ClearTriggerContent();
}
return true;
}
mNativeMenu = menu;
mNativeMenu->AddObserver(this);
mNativeMenu->ShowAsContextMenu(presContext, CSSIntPoint(aXPos, aYPos));
// While the native menu is open, it consumes mouseup events.
// Clear any :active state, mouse capture state and drag tracking now.
EventStateManager* activeESM = static_cast<EventStateManager*>(
EventStateManager::GetActiveEventStateManager());
if (activeESM) {
EventStateManager::ClearGlobalActiveContent(activeESM);
activeESM->StopTrackingDragGesture(true);
}
PresShell::ReleaseCapturingContent();
return true;
}
void nsXULPopupManager::OnNativeMenuOpened() {
if (!mNativeMenu) {
return;
}
RefPtr<nsXULPopupManager> kungFuDeathGrip(this);
nsCOMPtr<nsIContent> popup = mNativeMenu->Element();
nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(popup, true);
if (popupFrame) {
popupFrame->SetPopupState(ePopupShown);
}
}
void nsXULPopupManager::OnNativeMenuClosed() {
if (!mNativeMenu) {
return;
}
RefPtr<nsXULPopupManager> kungFuDeathGrip(this);
bool shouldHideChain =
(mNativeMenuActivatedItemCloseMenuMode == Some(CloseMenuMode_Auto));
nsCOMPtr<nsIContent> popup = mNativeMenu->Element();
nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(popup, true);
if (popupFrame) {
popupFrame->ClearTriggerContentIncludingDocument();
popupFrame->SetPopupState(ePopupClosed);
}
mNativeMenu->RemoveObserver(this);
mNativeMenu = nullptr;
mNativeMenuActivatedItemCloseMenuMode = Nothing();
mNativeMenuSubmenuStates.Clear();
// Stop hiding the menu from accessibility code, in case it gets opened as a
// non-native menu in the future.
popup->AsElement()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::aria_hidden,
true);
if (shouldHideChain && mPopups && mPopups->PopupType() == ePopupTypeMenu) {
// A menu item was activated before this menu closed, and the item requested
// the entire popup chain to be closed, which includes any open non-native
// menus.
// Close the non-native menus now. This matches the HidePopup call in
// nsXULMenuCommandEvent::Run.
HidePopup(mPopups->Content(), true, false, false, false);
}
}
void nsXULPopupManager::OnNativeSubMenuWillOpen(
mozilla::dom::Element* aPopupElement) {
mNativeMenuSubmenuStates.InsertOrUpdate(aPopupElement, ePopupShowing);
}
void nsXULPopupManager::OnNativeSubMenuDidOpen(
mozilla::dom::Element* aPopupElement) {
mNativeMenuSubmenuStates.InsertOrUpdate(aPopupElement, ePopupShown);
}
void nsXULPopupManager::OnNativeSubMenuClosed(
mozilla::dom::Element* aPopupElement) {
mNativeMenuSubmenuStates.Remove(aPopupElement);
}
void nsXULPopupManager::OnNativeMenuWillActivateItem(
mozilla::dom::Element* aMenuItemElement) {
if (!mNativeMenu) {
return;
}
CloseMenuMode cmm = GetCloseMenuMode(aMenuItemElement);
mNativeMenuActivatedItemCloseMenuMode = Some(cmm);
if (cmm == CloseMenuMode_Auto) {
// If any non-native menus are visible (for example because the context menu
// was opened on a non-native menu item, e.g. in a bookmarks folder), hide
// the non-native menus before executing the item.
HideOpenMenusBeforeExecutingMenu(CloseMenuMode_Auto);
}
}
void nsXULPopupManager::ShowPopupAtScreenRect(
nsIContent* aPopup, const nsAString& aPosition, const nsIntRect& aRect,
bool aIsContextMenu, bool aAttributesOverride, Event* aTriggerEvent) {
nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(aPopup, true);
if (!popupFrame || !MayShowPopup(popupFrame)) return;
PendingPopup pendingPopup(aPopup, aTriggerEvent);
nsCOMPtr<nsIContent> triggerContent = pendingPopup.GetTriggerContent();
popupFrame->InitializePopupAtRect(triggerContent, aPosition, aRect,
aAttributesOverride);
BeginShowingPopup(pendingPopup, aIsContextMenu, false);
}
void nsXULPopupManager::ShowTooltipAtScreen(
nsIContent* aPopup, nsIContent* aTriggerContent,
const LayoutDeviceIntPoint& aScreenPoint) {
nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(aPopup, true);
if (!popupFrame || !MayShowPopup(popupFrame)) {
return;
}
PendingPopup pendingPopup(aPopup, nullptr);
nsPresContext* pc = popupFrame->PresContext();
pendingPopup.SetMousePoint([&] {
// Event coordinates are relative to the root widget
if (nsPresContext* rootPresContext = pc->GetRootPresContext()) {
if (nsCOMPtr<nsIWidget> rootWidget = rootPresContext->GetRootWidget()) {
return aScreenPoint - rootWidget->WidgetToScreenOffset();
}
}
return aScreenPoint;
}());
auto screenCSSPoint =
CSSIntPoint::Round(aScreenPoint / pc->CSSToDevPixelScale());
popupFrame->InitializePopupAtScreen(aTriggerContent, screenCSSPoint.x,
screenCSSPoint.y, false);
BeginShowingPopup(pendingPopup, false, false);
}
static void CheckCaretDrawingState() {
// There is 1 caret per document, we need to find the focused
// document and erase its caret.
nsFocusManager* fm = nsFocusManager::GetFocusManager();
if (fm) {
nsCOMPtr<mozIDOMWindowProxy> window;
fm->GetFocusedWindow(getter_AddRefs(window));
if (!window) return;
auto* piWindow = nsPIDOMWindowOuter::From(window);
MOZ_ASSERT(piWindow);
nsCOMPtr<Document> focusedDoc = piWindow->GetDoc();
if (!focusedDoc) return;
PresShell* presShell = focusedDoc->GetPresShell();
if (!presShell) {
return;
}
RefPtr<nsCaret> caret = presShell->GetCaret();
if (!caret) return;
caret->SchedulePaint();
}
}
void nsXULPopupManager::ShowPopupCallback(nsIContent* aPopup,
nsMenuPopupFrame* aPopupFrame,
bool aIsContextMenu,
bool aSelectFirstItem) {
nsPopupType popupType = aPopupFrame->PopupType();
bool ismenu = (popupType == ePopupTypeMenu);
// Popups normally hide when an outside click occurs. Panels may use
// the noautohide attribute to disable this behaviour. It is expected
// that the application will hide these popups manually. The tooltip
// listener will handle closing the tooltip also.
bool isNoAutoHide =
aPopupFrame->IsNoAutoHide() || popupType == ePopupTypeTooltip;
nsMenuChainItem* item =
new nsMenuChainItem(aPopupFrame, isNoAutoHide, aIsContextMenu, popupType);
if (!item) return;
// install keyboard event listeners for navigating menus. For panels, the
// escape key may be used to close the panel. However, the ignorekeys
// attribute may be used to disable adding these event listeners for popups
// that want to handle their own keyboard events.
nsAutoString ignorekeys;
if (aPopup->IsElement()) {
aPopup->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::ignorekeys,
ignorekeys);
}
if (ignorekeys.EqualsLiteral("true")) {
item->SetIgnoreKeys(eIgnoreKeys_True);
} else if (ignorekeys.EqualsLiteral("shortcuts")) {
item->SetIgnoreKeys(eIgnoreKeys_Shortcuts);
}
if (ismenu) {
// if the menu is on a menubar, use the menubar's listener instead
nsMenuFrame* menuFrame = do_QueryFrame(aPopupFrame->GetParent());
if (menuFrame) {
item->SetOnMenuBar(menuFrame->IsOnMenuBar());
}
}
// use a weak frame as the popup will set an open attribute if it is a menu
AutoWeakFrame weakFrame(aPopupFrame);
aPopupFrame->ShowPopup(aIsContextMenu);
NS_ENSURE_TRUE_VOID(weakFrame.IsAlive());
item->UpdateFollowAnchor();
// popups normally hide when an outside click occurs. Panels may use
// the noautohide attribute to disable this behaviour. It is expected
// that the application will hide these popups manually. The tooltip
// listener will handle closing the tooltip also.
nsIContent* oldmenu = nullptr;
if (mPopups) {
oldmenu = mPopups->Content();
}
item->SetParent(mPopups);
mPopups = item;
SetCaptureState(oldmenu);
NS_ENSURE_TRUE_VOID(weakFrame.IsAlive());
if (aSelectFirstItem) {
nsMenuFrame* next = GetNextMenuItem(aPopupFrame, nullptr, true, false);
aPopupFrame->SetCurrentMenuItem(next);
}
if (ismenu) UpdateMenuItems(aPopup);
// Caret visibility may have been affected, ensure that
// the caret isn't now drawn when it shouldn't be.
CheckCaretDrawingState();
}
void nsXULPopupManager::HidePopup(nsIContent* aPopup, bool aHideChain,
bool aDeselectMenu, bool aAsynchronous,
bool aIsCancel, nsIContent* aLastPopup) {
if (mNativeMenu && mNativeMenu->Element() == aPopup) {
RefPtr<NativeMenu> menu = mNativeMenu;
(void)menu->Close();
return;
}
nsMenuPopupFrame* popupFrame = do_QueryFrame(aPopup->GetPrimaryFrame());
if (!popupFrame) {
return;
}
nsMenuChainItem* foundPopup = mPopups;
while (foundPopup) {
if (foundPopup->Content() == aPopup) {
break;
}
foundPopup = foundPopup->GetParent();
}
bool deselectMenu = false;
nsCOMPtr<nsIContent> popupToHide, nextPopup, lastPopup;
if (foundPopup) {
if (foundPopup->IsNoAutoHide()) {
// If this is a noautohide panel, remove it but don't close any other
// panels.
popupToHide = aPopup;
} else {
// At this point, foundPopup will be set to the found item in the list. If
// foundPopup is the topmost menu, the one to remove, then there are no
// other popups to hide. If foundPopup is not the topmost menu, then there
// may be open submenus below it. In this case, we need to make sure that
// those submenus are closed up first. To do this, we scan up the menu
// list to find the topmost popup with only menus between it and
// foundPopup and close that menu first. In synchronous mode, the
// FirePopupHidingEvent method will be called which in turn calls
// HidePopupCallback to close up the next popup in the chain. These two
// methods will be called in sequence recursively to close up all the
// necessary popups. In asynchronous mode, a similar process occurs except
// that the FirePopupHidingEvent method is called asynchronously. In
// either case, nextPopup is set to the content node of the next popup to
// close, and lastPopup is set to the last popup in the chain to close,
// which will be aPopup, or null to close up all menus.
nsMenuChainItem* topMenu = foundPopup;
// Use IsMenu to ensure that foundPopup is a menu and scan down the child
// list until a non-menu is found. If foundPopup isn't a menu at all,
// don't scan and just close up this menu.
if (foundPopup->IsMenu()) {
nsMenuChainItem* child = foundPopup->GetChild();
while (child && child->IsMenu()) {
topMenu = child;
child = child->GetChild();
}
}
deselectMenu = aDeselectMenu;
popupToHide = topMenu->Content();
popupFrame = topMenu->Frame();
// Close up another popup if there is one, and we are either hiding the
// entire chain or the item to hide isn't the topmost popup.
nsMenuChainItem* parent = topMenu->GetParent();
if (parent && (aHideChain || topMenu != foundPopup)) {
while (parent && parent->IsNoAutoHide()) {
parent = parent->GetParent();
}
if (parent) {
nextPopup = parent->Content();
}
}
lastPopup = aLastPopup ? aLastPopup : (aHideChain ? nullptr : aPopup);
}
} else if (popupFrame->PopupState() == ePopupPositioning) {
// When the popup is in the popuppositioning state, it will not be in the
// mPopups list. We need another way to find it and make sure it does not
// continue the popup showing process.
deselectMenu = aDeselectMenu;
popupToHide = aPopup;
}
if (popupToHide) {
nsPopupState state = popupFrame->PopupState();
// If the popup is already being hidden, don't attempt to hide it again
if (state == ePopupHiding) {
return;
}
// Change the popup state to hiding. Don't set the hiding state if the
// popup is invisible, otherwise nsMenuPopupFrame::HidePopup will
// run again. In the invisible state, we just want the events to fire.
if (state != ePopupInvisible) {
popupFrame->SetPopupState(ePopupHiding);
}
// For menus, popupToHide is always the frontmost item in the list to hide.
if (aAsynchronous) {
nsCOMPtr<nsIRunnable> event = new nsXULPopupHidingEvent(
popupToHide, nextPopup, lastPopup, popupFrame->PopupType(),
deselectMenu, aIsCancel);
aPopup->OwnerDoc()->Dispatch(TaskCategory::Other, event.forget());
} else {
RefPtr<nsPresContext> presContext = popupFrame->PresContext();
FirePopupHidingEvent(popupToHide, nextPopup, lastPopup, presContext,
popupFrame->PopupType(), deselectMenu, aIsCancel);
}
}
}
void nsXULPopupManager::HideMenu(nsIContent* aMenu) {
if (mNativeMenu && aMenu->IsElement() &&
mNativeMenu->Element()->Contains(aMenu)) {
mNativeMenu->CloseSubmenu(aMenu->AsElement());
return;
}
nsMenuFrame* menu = do_QueryFrame(aMenu->GetPrimaryFrame(FlushType::Frames));
if (!menu) {
return;
}
nsMenuPopupFrame* popupFrame = menu->GetPopup();
if (!popupFrame) {
return;
}
HidePopup(popupFrame->GetContent(), false, true, false, false);
}
// This is used to hide the popup after a transition finishes.
class TransitionEnder final : public nsIDOMEventListener {
private:
// Effectively const but is cycle collected
MOZ_KNOWN_LIVE RefPtr<nsIContent> mContent;
protected:
virtual ~TransitionEnder() = default;
public:
bool mDeselectMenu;
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
NS_DECL_CYCLE_COLLECTION_CLASS(TransitionEnder)
TransitionEnder(nsIContent* aContent, bool aDeselectMenu)
: mContent(aContent), mDeselectMenu(aDeselectMenu) {}
MOZ_CAN_RUN_SCRIPT NS_IMETHOD HandleEvent(Event* aEvent) override {
mContent->RemoveSystemEventListener(u"transitionend"_ns, this, false);
nsMenuPopupFrame* popupFrame = do_QueryFrame(mContent->GetPrimaryFrame());
if (!popupFrame) {
return NS_OK;
}
// Now hide the popup. There could be other properties transitioning, but
// we'll assume they all end at the same time and just hide the popup upon
// the first one ending.
if (RefPtr<nsXULPopupManager> pm = nsXULPopupManager::GetInstance()) {
pm->HidePopupCallback(mContent, popupFrame, nullptr, nullptr,
popupFrame->PopupType(), mDeselectMenu);
}
return NS_OK;
}
};
NS_IMPL_CYCLE_COLLECTING_ADDREF(TransitionEnder)
NS_IMPL_CYCLE_COLLECTING_RELEASE(TransitionEnder)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TransitionEnder)
NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END
NS_IMPL_CYCLE_COLLECTION(TransitionEnder, mContent);
void nsXULPopupManager::HidePopupCallback(
nsIContent* aPopup, nsMenuPopupFrame* aPopupFrame, nsIContent* aNextPopup,
nsIContent* aLastPopup, nsPopupType aPopupType, bool aDeselectMenu) {
if (mCloseTimer && mTimerMenu == aPopupFrame) {
mCloseTimer->Cancel();
mCloseTimer = nullptr;
mTimerMenu = nullptr;
}
// The popup to hide is aPopup. Search the list again to find the item that
// corresponds to the popup to hide aPopup. This is done because it's
// possible someone added another item (attempted to open another popup)
// or removed a popup frame during the event processing so the item isn't at
// the front anymore.
nsMenuChainItem* item = mPopups;
while (item) {
if (item->Content() == aPopup) {
item->Detach(&mPopups);
SetCaptureState(aPopup);
break;
}
item = item->GetParent();
}
delete item;
AutoWeakFrame weakFrame(aPopupFrame);
aPopupFrame->HidePopup(aDeselectMenu, ePopupClosed);
NS_ENSURE_TRUE_VOID(weakFrame.IsAlive());
// send the popuphidden event synchronously. This event has no default
// behaviour.
nsEventStatus status = nsEventStatus_eIgnore;
WidgetMouseEvent event(true, eXULPopupHidden, nullptr,
WidgetMouseEvent::eReal);
RefPtr<nsPresContext> presContext = aPopupFrame->PresContext();
EventDispatcher::Dispatch(aPopup, presContext, &event, nullptr, &status);
NS_ENSURE_TRUE_VOID(weakFrame.IsAlive());
// Force any popups that might be anchored on elements within this popup to
// update.
UpdatePopupPositions(presContext->RefreshDriver());
// if there are more popups to close, look for the next one
if (aNextPopup && aPopup != aLastPopup) {
nsMenuChainItem* foundMenu = nullptr;
nsMenuChainItem* item = mPopups;
while (item) {
if (item->Content() == aNextPopup) {
foundMenu = item;
break;
}
item = item->GetParent();
}
// continue hiding the chain of popups until the last popup aLastPopup
// is reached, or until a popup of a different type is reached. This
// last check is needed so that a menulist inside a non-menu panel only
// closes the menu and not the panel as well.
if (foundMenu && (aLastPopup || aPopupType == foundMenu->PopupType())) {
nsCOMPtr<nsIContent> popupToHide = item->Content();
nsMenuChainItem* parent = item->GetParent();
nsCOMPtr<nsIContent> nextPopup;
if (parent && popupToHide != aLastPopup) nextPopup = parent->Content();
nsMenuPopupFrame* popupFrame = item->Frame();
nsPopupState state = popupFrame->PopupState();
if (state == ePopupHiding) return;
if (state != ePopupInvisible) popupFrame->SetPopupState(ePopupHiding);
RefPtr<nsPresContext> presContext = popupFrame->PresContext();
FirePopupHidingEvent(popupToHide, nextPopup, aLastPopup, presContext,
foundMenu->PopupType(), aDeselectMenu, false);
}
}
}
void nsXULPopupManager::HidePopupAfterDelay(nsMenuPopupFrame* aPopup) {
// Don't close up immediately.
// Kick off a close timer.
KillMenuTimer();
int32_t menuDelay =
LookAndFeel::GetInt(LookAndFeel::IntID::SubmenuDelay, 300); // ms
// Kick off the timer.
nsIEventTarget* target = nullptr;
if (nsIContent* content = aPopup->GetContent()) {
target = content->OwnerDoc()->EventTargetFor(TaskCategory::Other);
}
NS_NewTimerWithFuncCallback(
getter_AddRefs(mCloseTimer),
[](nsITimer* aTimer, void* aClosure) {
nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
if (pm) {
pm->KillMenuTimer();
}
},
nullptr, menuDelay, nsITimer::TYPE_ONE_SHOT, "KillMenuTimer", target);
// the popup will call PopupDestroyed if it is destroyed, which checks if it
// is set to mTimerMenu, so it should be safe to keep a reference to it
mTimerMenu = aPopup;
}
void nsXULPopupManager::HidePopupsInList(
const nsTArray<nsMenuPopupFrame*>& aFrames) {
// Create a weak frame list. This is done in a separate array with the
// right capacity predetermined to avoid multiple allocations.
nsTArray<WeakFrame> weakPopups(aFrames.Length());
uint32_t f;
for (f = 0; f < aFrames.Length(); f++) {
WeakFrame* wframe = weakPopups.AppendElement();
if (wframe) *wframe = aFrames[f];
}
for (f = 0; f < weakPopups.Length(); f++) {
// check to ensure that the frame is still alive before hiding it.
if (weakPopups[f].IsAlive()) {
nsMenuPopupFrame* frame =
static_cast<nsMenuPopupFrame*>(weakPopups[f].GetFrame());
frame->HidePopup(true, ePopupInvisible);
}
}
SetCaptureState(nullptr);
}
bool nsXULPopupManager::IsChildOfDocShell(Document* aDoc,
nsIDocShellTreeItem* aExpected) {
nsCOMPtr<nsIDocShellTreeItem> docShellItem(aDoc->GetDocShell());
while (docShellItem) {
if (docShellItem == aExpected) return true;
nsCOMPtr<nsIDocShellTreeItem> parent;
docShellItem->GetInProcessParent(getter_AddRefs(parent));
docShellItem = parent;
}
return false;
}
void nsXULPopupManager::HidePopupsInDocShell(
nsIDocShellTreeItem* aDocShellToHide) {
nsTArray<nsMenuPopupFrame*> popupsToHide;
// iterate to get the set of popup frames to hide
nsMenuChainItem* item = mPopups;
while (item) {
nsMenuChainItem* parent = item->GetParent();
if (item->Frame()->PopupState() != ePopupInvisible &&
IsChildOfDocShell(item->Content()->OwnerDoc(), aDocShellToHide)) {
nsMenuPopupFrame* frame = item->Frame();
item->Detach(&mPopups);
delete item;
popupsToHide.AppendElement(frame);
}
item = parent;
}
HidePopupsInList(popupsToHide);
}
void nsXULPopupManager::UpdatePopupPositions(nsRefreshDriver* aRefreshDriver) {
nsMenuChainItem* item = mPopups;
while (item) {
if (item->Frame()->PresContext()->RefreshDriver() == aRefreshDriver) {
item->CheckForAnchorChange();
}
item = item->GetParent();
}
}
void nsXULPopupManager::UpdateFollowAnchor(nsMenuPopupFrame* aPopup) {
nsMenuChainItem* item = mPopups;
while (item) {
if (item->Frame() == aPopup) {
item->UpdateFollowAnchor();
break;
}
item = item->GetParent();
}
}
void nsXULPopupManager::HideOpenMenusBeforeExecutingMenu(CloseMenuMode aMode) {
if (aMode == CloseMenuMode_None) {
return;
}
// When a menuitem is selected to be executed, first hide all the open
// popups, but don't remove them yet. This is needed when a menu command
// opens a modal dialog. The views associated with the popups needed to be
// hidden and the accesibility events fired before the command executes, but
// the popuphiding/popuphidden events are fired afterwards.
nsTArray<nsMenuPopupFrame*> popupsToHide;
nsMenuChainItem* item = GetTopVisibleMenu();
while (item) {
// if it isn't a <menupopup>, don't close it automatically
if (!item->IsMenu()) {
break;
}
nsMenuChainItem* next = item->GetParent();
popupsToHide.AppendElement(item->Frame());
if (aMode == CloseMenuMode_Single) {
// only close one level of menu
break;
}
item = next;
}
// Now hide the popups. If the closemenu mode is auto, deselect the menu,
// otherwise only one popup is closing, so keep the parent menu selected.
HidePopupsInList(popupsToHide);
}
void nsXULPopupManager::ExecuteMenu(nsIContent* aMenu,
nsXULMenuCommandEvent* aEvent) {
CloseMenuMode cmm = GetCloseMenuMode(aMenu);
HideOpenMenusBeforeExecutingMenu(cmm);
aEvent->SetCloseMenuMode(cmm);
nsCOMPtr<nsIRunnable> event = aEvent;
aMenu->OwnerDoc()->Dispatch(TaskCategory::Other, event.forget());
}
bool nsXULPopupManager::ActivateNativeMenuItem(nsIContent* aItem,
mozilla::Modifiers aModifiers,
int16_t aButton,
mozilla::ErrorResult& aRv) {
if (mNativeMenu && aItem->IsElement() &&
mNativeMenu->Element()->Contains(aItem)) {
mNativeMenu->ActivateItem(aItem->AsElement(), aModifiers, aButton, aRv);
return true;
}
return false;
}
nsEventStatus nsXULPopupManager::FirePopupShowingEvent(
const PendingPopup& aPendingPopup, nsPresContext* aPresContext) {
// Cache the pending popup so that the trigger node and other properties can
// be retrieved during the popupshowing event. It will be cleared below after
// the event has fired.
AutoRestore<const PendingPopup*> restorePendingPopup(mPendingPopup);
mPendingPopup = &aPendingPopup;
nsEventStatus status = nsEventStatus_eIgnore;
WidgetMouseEvent event(true, eXULPopupShowing, nullptr,
WidgetMouseEvent::eReal);
// coordinates are relative to the root widget
nsPresContext* rootPresContext = aPresContext->GetRootPresContext();
if (rootPresContext) {
event.mWidget =
rootPresContext->PresShell()->GetViewManager()->GetRootWidget();
} else {
event.mWidget = nullptr;
}
event.mInputSource = aPendingPopup.MouseInputSource();
event.mRefPoint = aPendingPopup.mMousePoint;
event.mModifiers = aPendingPopup.mModifiers;
RefPtr<nsIContent> popup = aPendingPopup.mPopup;
EventDispatcher::Dispatch(popup, aPresContext, &event, nullptr, &status);
return status;
}
void nsXULPopupManager::BeginShowingPopup(const PendingPopup& aPendingPopup,
bool aIsContextMenu,
bool aSelectFirstItem) {
RefPtr<nsIContent> popup = aPendingPopup.mPopup;
nsMenuPopupFrame* popupFrame = do_QueryFrame(popup->GetPrimaryFrame());
if (!popupFrame) {
return;
}
RefPtr<nsPresContext> presContext = popupFrame->PresContext();
RefPtr<PresShell> presShell = presContext->PresShell();
presShell->FrameNeedsReflow(popupFrame, IntrinsicDirty::TreeChange,
NS_FRAME_HAS_DIRTY_CHILDREN);
nsPopupType popupType = popupFrame->PopupType();
nsEventStatus status = FirePopupShowingEvent(aPendingPopup, presContext);
// if a panel, blur whatever has focus so that the panel can take the focus.
// This is done after the popupshowing event in case that event is cancelled.
// Using noautofocus="true" will disable this behaviour, which is needed for
// the autocomplete widget as it manages focus itself.
if (popupType == ePopupTypePanel &&
!popup->AsElement()->AttrValueIs(kNameSpaceID_None,
nsGkAtoms::noautofocus, nsGkAtoms::_true,
eCaseMatters)) {
if (RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager()) {
Document* doc = popup->GetUncomposedDoc();
// Only remove the focus if the currently focused item is ouside the
// popup. It isn't a big deal if the current focus is in a child popup
// inside the popup as that shouldn't be visible. This check ensures that
// a node inside the popup that is focused during a popupshowing event
// remains focused.
RefPtr<Element> currentFocus = fm->GetFocusedElement();
if (doc && currentFocus &&
!nsContentUtils::ContentIsCrossDocDescendantOf(currentFocus, popup)) {
nsCOMPtr<nsPIDOMWindowOuter> outerWindow = doc->GetWindow();
fm->ClearFocus(outerWindow);
}
}
}
popup->OwnerDoc()->FlushPendingNotifications(FlushType::Frames);
// get the frame again in case it went away
popupFrame = do_QueryFrame(popup->GetPrimaryFrame());
if (popupFrame) {
// if the event was cancelled or the popup was closed in the mean time,
// don't open the popup, reset its state back to closed and clear its
// trigger content.
if (popupFrame->PopupState() == ePopupClosed ||
status == nsEventStatus_eConsumeNoDefault) {
popupFrame->SetPopupState(ePopupClosed);
popupFrame->ClearTriggerContent();
} else {
// Now check if we need to fire the popuppositioned event. If not, call
// ShowPopupCallback directly.
// The popuppositioned event only fires on arrow panels for now.
if (popup->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
nsGkAtoms::arrow, eCaseMatters)) {
popupFrame->ShowWithPositionedEvent();
presShell->FrameNeedsReflow(popupFrame, IntrinsicDirty::TreeChange,
NS_FRAME_HAS_DIRTY_CHILDREN);
} else {
ShowPopupCallback(popup, popupFrame, aIsContextMenu, aSelectFirstItem);
}
}
}
}
void nsXULPopupManager::FirePopupHidingEvent(
nsIContent* aPopup, nsIContent* aNextPopup, nsIContent* aLastPopup,
nsPresContext* aPresContext, nsPopupType aPopupType, bool aDeselectMenu,
bool aIsCancel) {
nsCOMPtr<nsIContent> popup = aPopup;
RefPtr<PresShell> presShell = aPresContext->PresShell();
mozilla::Unused << presShell; // This presShell may be keeping things alive
// on non GTK platforms
nsEventStatus status = nsEventStatus_eIgnore;
WidgetMouseEvent event(true, eXULPopupHiding, nullptr,
WidgetMouseEvent::eReal);
EventDispatcher::Dispatch(aPopup, aPresContext, &event, nullptr, &status);
// when a panel is closed, blur whatever has focus inside the popup
if (aPopupType == ePopupTypePanel &&
(!aPopup->IsElement() || !aPopup->AsElement()->AttrValueIs(
kNameSpaceID_None, nsGkAtoms::noautofocus,
nsGkAtoms::_true, eCaseMatters))) {
if (RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager()) {
Document* doc = aPopup->GetUncomposedDoc();
// Remove the focus from the focused node only if it is inside the popup.
RefPtr<Element> currentFocus = fm->GetFocusedElement();
if (doc && currentFocus &&
nsContentUtils::ContentIsCrossDocDescendantOf(currentFocus, aPopup)) {
nsCOMPtr<nsPIDOMWindowOuter> outerWindow = doc->GetWindow();
fm->ClearFocus(outerWindow);
}
}
}
aPopup->OwnerDoc()->FlushPendingNotifications(FlushType::Frames);
// get frame again in case it went away
nsMenuPopupFrame* popupFrame = do_QueryFrame(aPopup->GetPrimaryFrame());
if (popupFrame) {
// if the event was cancelled, don't hide the popup, and reset its
// state back to open. Only popups in chrome shells can prevent a popup
// from hiding.
if (status == nsEventStatus_eConsumeNoDefault &&
!popupFrame->IsInContentShell()) {
// XXXndeakin
// If an attempt was made to hide this popup before the popupshown event
// fired, then ePopupShown is set here even though it should be
// ePopupVisible. This probably isn't worth the hassle of handling.
popupFrame->SetPopupState(ePopupShown);
} else {
// If the popup has an animate attribute and it is not set to false, check
// if it has a closing transition and wait for it to finish. The
// transition may still occur either way, but the view will be hidden and
// you won't be able to see it. If there is a next popup, indicating that
// mutliple popups are rolling up, don't wait and hide the popup right
// away since the effect would likely be undesirable.
if (StaticPrefs::xul_panel_animations_enabled() && !aNextPopup &&
aPopup->IsElement() &&
aPopup->AsElement()->HasAttr(kNameSpaceID_None, nsGkAtoms::animate)) {
// If animate="false" then don't transition at all. If animate="cancel",
// only show the transition if cancelling the popup or rolling up.
// Otherwise, always show the transition.
nsAutoString animate;
aPopup->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::animate,
animate);
if (!animate.EqualsLiteral("false") &&
(!animate.EqualsLiteral("cancel") || aIsCancel)) {
presShell->FlushPendingNotifications(FlushType::Layout);
// Get the frame again in case the flush caused it to go away
popupFrame = do_QueryFrame(aPopup->GetPrimaryFrame());
if (!popupFrame) return;
if (AnimationUtils::HasCurrentTransitions(
aPopup->AsElement(), PseudoStyleType::NotPseudo)) {
RefPtr<TransitionEnder> ender =
new TransitionEnder(aPopup, aDeselectMenu);
aPopup->AddSystemEventListener(u"transitionend"_ns, ender, false,
false);
return;
}
}
}
HidePopupCallback(aPopup, popupFrame, aNextPopup, aLastPopup, aPopupType,
aDeselectMenu);
}
}
}
bool nsXULPopupManager::IsPopupOpen(nsIContent* aPopup) {
if (mNativeMenu && mNativeMenu->Element() == aPopup) {
return true;
}
// a popup is open if it is in the open list. The assertions ensure that the
// frame is in the correct state. If the popup is in the hiding or invisible
// state, it will still be in the open popup list until it is closed.
nsMenuChainItem* item = mPopups;
while (item) {
if (item->Content() == aPopup) {
NS_ASSERTION(item->Frame()->IsOpen() ||
item->Frame()->PopupState() == ePopupHiding ||
item->Frame()->PopupState() == ePopupInvisible,
"popup in open list not actually open");
return true;
}
item = item->GetParent();
}
return false;
}
bool nsXULPopupManager::IsPopupOpenForMenuParent(nsMenuParent* aMenuParent) {
nsMenuChainItem* item = GetTopVisibleMenu();
while (item) {
nsMenuPopupFrame* popup = item->Frame();
if (popup && popup->IsOpen()) {
nsMenuFrame* menuFrame = do_QueryFrame(popup->GetParent());
if (menuFrame && menuFrame->GetMenuParent() == aMenuParent) {
return true;
}
}
item = item->GetParent();
}
return false;
}
nsIFrame* nsXULPopupManager::GetTopPopup(nsPopupType aType) {
nsMenuChainItem* item = mPopups;
while (item) {
if (item->Frame()->IsVisible() &&
(item->PopupType() == aType || aType == ePopupTypeAny)) {
return item->Frame();
}
item = item->GetParent();
}
return nullptr;
}
void nsXULPopupManager::GetVisiblePopups(nsTArray<nsIFrame*>& aPopups) {
aPopups.Clear();
nsMenuChainItem* item = mPopups;
while (item) {
// Skip panels which are not visible as well as popups that
// are transparent to mouse events.
if (item->Frame()->IsVisible() && !item->Frame()->IsMouseTransparent()) {
aPopups.AppendElement(item->Frame());
}
item = item->GetParent();
}
}
already_AddRefed<nsINode> nsXULPopupManager::GetLastTriggerNode(
Document* aDocument, bool aIsTooltip) {
if (!aDocument) return nullptr;
RefPtr<nsINode> node;
// If a pending popup is set, it means that a popupshowing event is being
// fired. In this case, just use the cached node, as the popup is not yet in
// the list of open popups.
RefPtr<nsIContent> openingPopup =
mPendingPopup ? mPendingPopup->mPopup : nullptr;
if (openingPopup && openingPopup->GetUncomposedDoc() == aDocument &&
aIsTooltip == openingPopup->IsXULElement(nsGkAtoms::tooltip)) {
node = nsMenuPopupFrame::GetTriggerContent(
GetPopupFrameForContent(openingPopup, false));
} else if (mNativeMenu && !aIsTooltip) {
RefPtr<dom::Element> popup = mNativeMenu->Element();
if (popup->GetUncomposedDoc() == aDocument) {
nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(popup, false);
node = nsMenuPopupFrame::GetTriggerContent(popupFrame);
}
} else {
nsMenuChainItem* item = mPopups;
while (item) {
// look for a popup of the same type and document.
if ((item->PopupType() == ePopupTypeTooltip) == aIsTooltip &&
item->Content()->GetUncomposedDoc() == aDocument) {
node = nsMenuPopupFrame::GetTriggerContent(item->Frame());
if (node) break;
}
item = item->GetParent();
}
}
return node.forget();
}
bool nsXULPopupManager::MayShowPopup(nsMenuPopupFrame* aPopup) {
// if a popup's IsOpen method returns true, then the popup must always be in
// the popup chain scanned in IsPopupOpen.
NS_ASSERTION(!aPopup->IsOpen() || IsPopupOpen(aPopup->GetContent()),
"popup frame state doesn't match XULPopupManager open state");
nsPopupState state = aPopup->PopupState();
// if the popup is not in the open popup chain, then it must have a state that
// is either closed, in the process of being shown, or invisible.
NS_ASSERTION(IsPopupOpen(aPopup->GetContent()) || state == ePopupClosed ||
state == ePopupShowing || state == ePopupPositioning ||
state == ePopupInvisible,
"popup not in XULPopupManager open list is open");
// don't show popups unless they are closed or invisible
if (state != ePopupClosed && state != ePopupInvisible) return false;
// Don't show popups that we already have in our popup chain
if (IsPopupOpen(aPopup->GetContent())) {
NS_WARNING("Refusing to show duplicate popup");
return false;
}
// if the popup was just rolled up, don't reopen it
if (mozilla::widget::nsAutoRollup::GetLastRollup() == aPopup->GetContent()) {
return false;
}
nsCOMPtr<nsIDocShell> docShell = aPopup->PresContext()->GetDocShell();
nsCOMPtr<nsIBaseWindow> baseWin = do_QueryInterface(docShell);
if (!baseWin) {
return false;
}
nsCOMPtr<nsIDocShellTreeItem> root;
docShell->GetInProcessRootTreeItem(getter_AddRefs(root));
if (!root) {
return false;
}
nsCOMPtr<nsPIDOMWindowOuter> rootWin = root->GetWindow();
MOZ_RELEASE_ASSERT(XRE_IsParentProcess(),
"Cannot have XUL in content process showing popups.");
// chrome shells can always open popups, but other types of shells can only
// open popups when they are focused and visible
if (docShell->ItemType() != nsIDocShellTreeItem::typeChrome) {
// only allow popups in active windows
nsFocusManager* fm = nsFocusManager::GetFocusManager();
if (!fm || !rootWin) {
return false;
}
nsCOMPtr<nsPIDOMWindowOuter> activeWindow = fm->GetActiveWindow();
if (activeWindow != rootWin) {
return false;
}
// only allow popups in visible frames
// TODO: This visibility check should be replaced with a check of
// bc->IsActive(). It is okay for now since this is only called
// in the parent process. Bug 1698533.
bool visible;
baseWin->GetVisibility(&visible);
if (!visible) {
return false;
}
}
// platforms respond differently when an popup is opened in a minimized
// window, so this is always disabled.
nsCOMPtr<nsIWidget> mainWidget;
baseWin->GetMainWidget(getter_AddRefs(mainWidget));
if (mainWidget && mainWidget->SizeMode() == nsSizeMode_Minimized) {
return false;
}
#ifdef XP_MACOSX
if (rootWin) {
auto globalWin = nsGlobalWindowOuter::Cast(rootWin.get());
if (globalWin->IsInModalState()) {
return false;
}
}
#endif
// cannot open a popup that is a submenu of a menupopup that isn't open.
nsMenuFrame* menuFrame = do_QueryFrame(aPopup->GetParent());
if (menuFrame) {
nsMenuParent* parentPopup = menuFrame->GetMenuParent();
if (parentPopup && !parentPopup->IsOpen()) return false;
}
return true;
}
void nsXULPopupManager::PopupDestroyed(nsMenuPopupFrame* aPopup) {
// when a popup frame is destroyed, just unhook it from the list of popups
if (mTimerMenu == aPopup) {
if (mCloseTimer) {
mCloseTimer->Cancel();
mCloseTimer = nullptr;
}
mTimerMenu = nullptr;
}
nsTArray<nsMenuPopupFrame*> popupsToHide;
nsMenuChainItem* item = mPopups;
while (item) {
nsMenuPopupFrame* frame = item->Frame();
if (frame == aPopup) {
// XXXndeakin shouldn't this only happen for menus?
if (!item->IsNoAutoHide() && frame->PopupState() != ePopupInvisible) {
// Iterate through any child menus and hide them as well, since the
// parent is going away. We won't remove them from the list yet, just
// hide them, as they will be removed from the list when this function
// gets called for that child frame.
nsMenuChainItem* child = item->GetChild();
while (child) {
// if the popup is a child frame of the menu that was destroyed, add
// it to the list of popups to hide. Don't bother with the events
// since the frames are going away. If the child menu is not a child
// frame, for example, a context menu, use HidePopup instead, but call
// it asynchronously since we are in the middle of frame destruction.
nsMenuPopupFrame* childframe = child->Frame();
if (nsLayoutUtils::IsProperAncestorFrame(frame, childframe)) {
popupsToHide.AppendElement(childframe);
} else {
// HidePopup will take care of hiding any of its children, so
// break out afterwards
HidePopup(child->Content(), false, false, true, false);
break;
}
child = child->GetChild();
}
}
item->Detach(&mPopups);
delete item;
break;
}
item = item->GetParent();
}
HidePopupsInList(popupsToHide);
}
bool nsXULPopupManager::HasContextMenu(nsMenuPopupFrame* aPopup) {
nsMenuChainItem* item = GetTopVisibleMenu();
while (item && item->Frame() != aPopup) {
if (item->IsContextMenu()) return true;
item = item->GetParent();
}
return false;
}
void nsXULPopupManager::SetCaptureState(nsIContent* aOldPopup) {
nsMenuChainItem* item = GetTopVisibleMenu();
if (item && aOldPopup == item->Content()) return;
if (mWidget) {
mWidget->CaptureRollupEvents(nullptr, false);
mWidget = nullptr;
}
if (item) {
nsMenuPopupFrame* popup = item->Frame();
mWidget = popup->GetWidget();
if (mWidget) {
mWidget->CaptureRollupEvents(nullptr, true);
}
}
UpdateKeyboardListeners();
}
void nsXULPopupManager::UpdateKeyboardListeners() {
nsCOMPtr<EventTarget> newTarget;
bool isForMenu = false;
nsMenuChainItem* item = GetTopVisibleMenu();
if (item) {
if (item->IgnoreKeys() != eIgnoreKeys_True) {
newTarget = item->Content()->GetComposedDoc();
}
isForMenu = item->PopupType() == ePopupTypeMenu;
} else if (mActiveMenuBar) {
newTarget = mActiveMenuBar->GetContent()->GetComposedDoc();
isForMenu = true;
}
if (mKeyListener != newTarget) {
OwningNonNull<nsXULPopupManager> kungFuDeathGrip(*this);
if (mKeyListener) {
mKeyListener->RemoveEventListener(u"keypress"_ns, this, true);
mKeyListener->RemoveEventListener(u"keydown"_ns, this, true);
mKeyListener->RemoveEventListener(u"keyup"_ns, this, true);
mKeyListener = nullptr;
nsContentUtils::NotifyInstalledMenuKeyboardListener(false);
}
if (newTarget) {
newTarget->AddEventListener(u"keypress"_ns, this, true);
newTarget->AddEventListener(u"keydown"_ns, this, true);
newTarget->AddEventListener(u"keyup"_ns, this, true);
nsContentUtils::NotifyInstalledMenuKeyboardListener(isForMenu);
mKeyListener = newTarget;
}
}
}
void nsXULPopupManager::UpdateMenuItems(nsIContent* aPopup) {
// Walk all of the menu's children, checking to see if any of them has a
// command attribute. If so, then several attributes must potentially be
// updated.
nsCOMPtr<Document> document = aPopup->GetUncomposedDoc();
if (!document) {
return;
}
// When a menu is opened, make sure that command updating is unlocked first.
nsCOMPtr<nsIDOMXULCommandDispatcher> commandDispatcher =
document->GetCommandDispatcher();
if (commandDispatcher) {
commandDispatcher->Unlock();
}
for (nsCOMPtr<nsIContent> grandChild = aPopup->GetFirstChild(); grandChild;
grandChild = grandChild->GetNextSibling()) {
if (grandChild->IsXULElement(nsGkAtoms::menugroup)) {
if (grandChild->GetChildCount() == 0) {
continue;
}
grandChild = grandChild->GetFirstChild();
}
if (grandChild->IsXULElement(nsGkAtoms::menuitem)) {
// See if we have a command attribute.
Element* grandChildElement = grandChild->AsElement();
nsAutoString command;
grandChildElement->GetAttr(kNameSpaceID_None, nsGkAtoms::command,
command);
if (!command.IsEmpty()) {
// We do! Look it up in our document
RefPtr<dom::Element> commandElement = document->GetElementById(command);
if (commandElement) {
nsAutoString commandValue;
// The menu's disabled state needs to be updated to match the command.
if (commandElement->GetAttr(kNameSpaceID_None, nsGkAtoms::disabled,
commandValue))
grandChildElement->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled,
commandValue, true);
else
grandChildElement->UnsetAttr(kNameSpaceID_None, nsGkAtoms::disabled,
true);
// The menu's label, accesskey checked and hidden states need to be
// updated to match the command. Note that unlike the disabled state
// if the command has *no* value, we assume the menu is supplying its
// own.
if (commandElement->GetAttr(kNameSpaceID_None, nsGkAtoms::label,
commandValue))
grandChildElement->SetAttr(kNameSpaceID_None, nsGkAtoms::label,
commandValue, true);
if (commandElement->GetAttr(kNameSpaceID_None, nsGkAtoms::accesskey,
commandValue))
grandChildElement->SetAttr(kNameSpaceID_None, nsGkAtoms::accesskey,
commandValue, true);
if (commandElement->GetAttr(kNameSpaceID_None, nsGkAtoms::checked,
commandValue))
grandChildElement->SetAttr(kNameSpaceID_None, nsGkAtoms::checked,
commandValue, true);
if (commandElement->GetAttr(kNameSpaceID_None, nsGkAtoms::hidden,
commandValue))
grandChildElement->SetAttr(kNameSpaceID_None, nsGkAtoms::hidden,
commandValue, true);
}
}
}
if (!grandChild->GetNextSibling() &&
grandChild->GetParent()->IsXULElement(nsGkAtoms::menugroup)) {
grandChild = grandChild->GetParent();
}
}
}
// Notify
//
// The item selection timer has fired, we might have to readjust the
// selected item. There are two cases here that we are trying to deal with:
// (1) diagonal movement from a parent menu to a submenu passing briefly over
// other items, and
// (2) moving out from a submenu to a parent or grandparent menu.
// In both cases, |mTimerMenu| is the menu item that might have an open submenu
// and the first item in |mPopups| is the item the mouse is currently over,
// which could be none of them.
//
// case (1):
// As the mouse moves from the parent item of a submenu (we'll call 'A')
// diagonally into the submenu, it probably passes through one or more
// sibilings (B). As the mouse passes through B, it becomes the current menu
// item and the timer is set and mTimerMenu is set to A. Before the timer
// fires, the mouse leaves the menu containing A and B and enters the submenus.
// Now when the timer fires, |mPopups| is null (!= |mTimerMenu|) so we have to
// see if anything in A's children is selected (recall that even disabled items
// are selected, the style just doesn't show it). If that is the case, we need
// to set the selected item back to A.
//
// case (2);
// Item A has an open submenu, and in it there is an item (B) which also has an
// open submenu (so there are 3 menus displayed right now). The mouse then
// leaves B's child submenu and selects an item that is a sibling of A, call it
// C. When the mouse enters C, the timer is set and |mTimerMenu| is A and
// |mPopups| is C. As the timer fires, the mouse is still within C. The correct
// behavior is to set the current item to C and close up the chain parented at
// A.
//
// This brings up the question of is the logic of case (1) enough? The answer
// is no, and is discussed in bugzilla bug 29400. Case (1) asks if A's submenu
// has a selected child, and if it does, set the selected item to A. Because B
// has a submenu open, it is selected and as a result, A is set to be the
// selected item even though the mouse rests in C -- very wrong.
//
// The solution is to use the same idea, but instead of only checking one
// level, drill all the way down to the deepest open submenu and check if it
// has something selected. Since the mouse is in a grandparent, it won't, and
// we know that we can safely close up A and all its children.
//
// The code below melds the two cases together.
//
void nsXULPopupManager::KillMenuTimer() {
if (mCloseTimer && mTimerMenu) {
mCloseTimer->Cancel();
mCloseTimer = nullptr;
if (mTimerMenu->IsOpen())
HidePopup(mTimerMenu->GetContent(), false, false, true, false);
}
mTimerMenu = nullptr;
}
void nsXULPopupManager::CancelMenuTimer(nsMenuParent* aMenuParent) {
if (mCloseTimer && mTimerMenu == aMenuParent) {
mCloseTimer->Cancel();
mCloseTimer = nullptr;
mTimerMenu = nullptr;
}
}
bool nsXULPopupManager::HandleShortcutNavigation(KeyboardEvent* aKeyEvent,
nsMenuPopupFrame* aFrame) {
// On Windows, don't check shortcuts when the accelerator key is down.
#ifdef XP_WIN
WidgetInputEvent* evt = aKeyEvent->WidgetEventPtr()->AsInputEvent();
if (evt && evt->IsAccel()) {
return false;
}
#endif
nsMenuChainItem* item = GetTopVisibleMenu();
if (!aFrame && item) aFrame = item->Frame();
if (aFrame) {
bool action;
nsMenuFrame* result = aFrame->FindMenuWithShortcut(aKeyEvent, action);
if (result) {
aFrame->ChangeMenuItem(result, false, true);
if (action) {
WidgetGUIEvent* evt = aKeyEvent->WidgetEventPtr()->AsGUIEvent();
nsMenuFrame* menuToOpen = result->Enter(evt);
if (menuToOpen) {
nsCOMPtr<nsIContent> content = menuToOpen->GetContent();
ShowMenu(content, true);
}
}
return true;
}
return false;
}
if (mActiveMenuBar) {
nsMenuFrame* result =
mActiveMenuBar->FindMenuWithShortcut(aKeyEvent, false);
if (result) {
mActiveMenuBar->SetActive(true);
result->OpenMenu(true);
return true;
}
}
return false;
}
bool nsXULPopupManager::HandleKeyboardNavigation(uint32_t aKeyCode) {
if (nsMenuChainItem* nextitem = GetTopVisibleMenu()) {
nextitem->Content()->OwnerDoc()->FlushPendingNotifications(
FlushType::Frames);
}
// navigate up through the open menus, looking for the topmost one
// in the same hierarchy
nsMenuChainItem* item = nullptr;
nsMenuChainItem* nextitem = GetTopVisibleMenu();
while (nextitem) {
item = nextitem;
nextitem = item->GetParent();
if (nextitem) {
// stop if the parent isn't a menu
if (!nextitem->IsMenu()) break;
// check to make sure that the parent is actually the parent menu. It
// won't be if the parent is in a different frame hierarchy, for example,
// for a context menu opened on another menu.
nsMenuParent* expectedParent =
static_cast<nsMenuParent*>(nextitem->Frame());
nsMenuFrame* menuFrame = do_QueryFrame(item->Frame()->GetParent());
if (!menuFrame || menuFrame->GetMenuParent() != expectedParent) {
break;
}
}
}
nsIFrame* itemFrame;
if (item)
itemFrame = item->Frame();
else if (mActiveMenuBar)
itemFrame = mActiveMenuBar;
else
return false;
nsNavigationDirection theDirection;
NS_ASSERTION(aKeyCode >= KeyboardEvent_Binding::DOM_VK_END &&
aKeyCode <= KeyboardEvent_Binding::DOM_VK_DOWN,
"Illegal key code");
theDirection = NS_DIRECTION_FROM_KEY_CODE(itemFrame, aKeyCode);
bool selectFirstItem = true;
#ifdef MOZ_WIDGET_GTK
nsMenuFrame* currentItem = nullptr;
if (item && mActiveMenuBar && NS_DIRECTION_IS_INLINE(theDirection)) {
currentItem = item->Frame()->GetCurrentMenuItem();
// If nothing is selected in the menu and we have a menubar, let it
// handle the movement not to steal focus from it.
if (!currentItem) {
item = nullptr;
}
}
// On menu change, only select first item if an item is already selected.
selectFirstItem = currentItem != nullptr;
#endif
// if a popup is open, first check for navigation within the popup
if (item && HandleKeyboardNavigationInPopup(item, theDirection)) return true;
// no popup handled the key, so check the active menubar, if any
if (mActiveMenuBar) {
nsMenuFrame* currentMenu = mActiveMenuBar->GetCurrentMenuItem();
if (NS_DIRECTION_IS_INLINE(theDirection)) {
nsMenuFrame* nextItem =
(theDirection == eNavigationDirection_End)
? GetNextMenuItem(mActiveMenuBar, currentMenu, false, true)
: GetPreviousMenuItem(mActiveMenuBar, currentMenu, false, true);
mActiveMenuBar->ChangeMenuItem(nextItem, selectFirstItem, true);
return true;
} else if (NS_DIRECTION_IS_BLOCK(theDirection)) {
// Open the menu and select its first item.
if (currentMenu) {
nsCOMPtr<nsIContent> content = currentMenu->GetContent();
ShowMenu(content, true);
}
return true;
}
}
return false;
}
bool nsXULPopupManager::HandleKeyboardNavigationInPopup(
nsMenuChainItem* item, nsMenuPopupFrame* aFrame,
nsNavigationDirection aDir) {
NS_ASSERTION(aFrame, "aFrame is null");
NS_ASSERTION(!item || item->Frame() == aFrame,
"aFrame is expected to be equal to item->Frame()");
nsMenuFrame* currentMenu = aFrame->GetCurrentMenuItem();
aFrame->ClearIncrementalString();
// This method only gets called if we're open.
if (!currentMenu && NS_DIRECTION_IS_INLINE(aDir)) {
// We've been opened, but we haven't had anything selected.
// We can handle End, but our parent handles Start.
if (aDir == eNavigationDirection_End) {
nsMenuFrame* nextItem = GetNextMenuItem(aFrame, nullptr, true, false);
if (nextItem) {
aFrame->ChangeMenuItem(nextItem, false, true);
return true;
}
}
return false;
}
bool isContainer = false;
bool isOpen = false;
if (currentMenu) {
isOpen = currentMenu->IsOpen();
isContainer = currentMenu->IsMenu();
if (isOpen) {
// for an open popup, have the child process the event
nsMenuChainItem* child = item ? item->GetChild() : nullptr;
if (child && HandleKeyboardNavigationInPopup(child, aDir)) return true;
} else if (aDir == eNavigationDirection_End && isContainer &&
!currentMenu->IsDisabled()) {
// The menu is not yet open. Open it and select the first item.
nsCOMPtr<nsIContent> content = currentMenu->GetContent();
ShowMenu(content, true);
return true;
}
}
// For block progression, we can move in either direction
if (NS_DIRECTION_IS_BLOCK(aDir) || NS_DIRECTION_IS_BLOCK_TO_EDGE(aDir)) {
nsMenuFrame* nextItem;
if (aDir == eNavigationDirection_Before ||
aDir == eNavigationDirection_After) {
// Cursor navigation does not wrap on Mac or for menulists on Windows.
bool wrap =
#ifdef XP_WIN
!aFrame->IsMenuList();
#elif defined XP_MACOSX
false;
#else
true;
#endif
if (aDir == eNavigationDirection_Before) {
nextItem = GetPreviousMenuItem(aFrame, currentMenu, true, wrap);
} else {
nextItem = GetNextMenuItem(aFrame, currentMenu, true, wrap);
}
} else if (aDir == eNavigationDirection_First) {
nextItem = GetNextMenuItem(aFrame, nullptr, true, false);
} else {
nextItem = GetPreviousMenuItem(aFrame, nullptr, true, false);
}
if (nextItem) {
aFrame->ChangeMenuItem(nextItem, false, true);
return true;
}
} else if (currentMenu && isContainer && isOpen) {
if (aDir == eNavigationDirection_Start) {
// close a submenu when Left is pressed
nsMenuPopupFrame* popupFrame = currentMenu->GetPopup();
if (popupFrame)
HidePopup(popupFrame->GetContent(), false, false, false, false);
return true;
}
}
return false;
}
bool nsXULPopupManager::HandleKeyboardEventWithKeyCode(
KeyboardEvent* aKeyEvent, nsMenuChainItem* aTopVisibleMenuItem) {
uint32_t keyCode = aKeyEvent->KeyCode();
// Escape should close panels, but the other keys should have no effect.
if (aTopVisibleMenuItem &&
aTopVisibleMenuItem->PopupType() != ePopupTypeMenu) {
if (keyCode == KeyboardEvent_Binding::DOM_VK_ESCAPE) {
HidePopup(aTopVisibleMenuItem->Content(), false, false, false, true);
aKeyEvent->StopPropagation();
aKeyEvent->StopCrossProcessForwarding();
aKeyEvent->PreventDefault();
}
return true;
}
bool consume = (aTopVisibleMenuItem || mActiveMenuBar);
switch (keyCode) {
case KeyboardEvent_Binding::DOM_VK_UP:
case KeyboardEvent_Binding::DOM_VK_DOWN:
#ifndef XP_MACOSX
// roll up the popup when alt+up/down are pressed within a menulist.
if (aKeyEvent->AltKey() && aTopVisibleMenuItem &&
aTopVisibleMenuItem->Frame()->IsMenuList()) {
Rollup(0, false, nullptr, nullptr);
break;
}
[[fallthrough]];
#endif
case KeyboardEvent_Binding::DOM_VK_LEFT:
case KeyboardEvent_Binding::DOM_VK_RIGHT:
case KeyboardEvent_Binding::DOM_VK_HOME:
case KeyboardEvent_Binding::DOM_VK_END:
HandleKeyboardNavigation(keyCode);
break;
case KeyboardEvent_Binding::DOM_VK_PAGE_DOWN:
case KeyboardEvent_Binding::DOM_VK_PAGE_UP:
if (aTopVisibleMenuItem) {
aTopVisibleMenuItem->Frame()->ChangeByPage(
keyCode == KeyboardEvent_Binding::DOM_VK_PAGE_UP);
}
break;
case KeyboardEvent_Binding::DOM_VK_ESCAPE:
// Pressing Escape hides one level of menus only. If no menu is open,
// check if a menubar is active and inform it that a menu closed. Even
// though in this latter case, a menu didn't actually close, the effect
// ends up being the same. Similar for the tab key below.
if (aTopVisibleMenuItem) {
HidePopup(aTopVisibleMenuItem->Content(), false, false, false, true);
} else if (mActiveMenuBar) {
mActiveMenuBar->MenuClosed();
}
break;
case KeyboardEvent_Binding::DOM_VK_TAB:
#ifndef XP_MACOSX
case KeyboardEvent_Binding::DOM_VK_F10:
#endif
if (aTopVisibleMenuItem &&
!aTopVisibleMenuItem->Frame()->GetContent()->AsElement()->AttrValueIs(
kNameSpaceID_None, nsGkAtoms::activateontab, nsGkAtoms::_true,
eCaseMatters)) {
// close popups or deactivate menubar when Tab or F10 are pressed
Rollup(0, false, nullptr, nullptr);
break;
} else if (mActiveMenuBar) {
mActiveMenuBar->MenuClosed();
break;
}
// Intentional fall-through to RETURN case
[[fallthrough]];
case KeyboardEvent_Binding::DOM_VK_RETURN: {
// If there is a popup open, check if the current item needs to be opened.
// Otherwise, tell the active menubar, if any, to activate the menu. The
// Enter method will return a menu if one needs to be opened as a result.
nsMenuFrame* menuToOpen = nullptr;
WidgetGUIEvent* GUIEvent = aKeyEvent->WidgetEventPtr()->AsGUIEvent();
if (aTopVisibleMenuItem) {
menuToOpen = aTopVisibleMenuItem->Frame()->Enter(GUIEvent);
} else if (mActiveMenuBar) {
menuToOpen = mActiveMenuBar->Enter(GUIEvent);
}
if (menuToOpen) {
nsCOMPtr<nsIContent> content = menuToOpen->GetContent();
ShowMenu(content, true);
}
break;
}
default:
return false;
}
if (consume) {
aKeyEvent->StopPropagation();
aKeyEvent->StopCrossProcessForwarding();
aKeyEvent->PreventDefault();
}
return true;
}
// TODO(emilio): This should probably just walk the DOM instead and call
// GetPrimaryFrame() on the items... Do we have anonymous / fallback menu items
// that could be selectable?
static nsIContent* FindDefaultInsertionPoint(nsIContent* aParent) {
if (ShadowRoot* shadow = aParent->GetShadowRoot()) {
if (HTMLSlotElement* slot = shadow->GetDefaultSlot()) {
return slot;
}
}
return aParent;
}
nsContainerFrame* nsXULPopupManager::ImmediateParentFrame(
nsContainerFrame* aFrame) {
MOZ_ASSERT(aFrame && aFrame->GetContent());
nsIContent* insertionPoint = FindDefaultInsertionPoint(aFrame->GetContent());
nsCSSFrameConstructor* fc = aFrame->PresContext()->FrameConstructor();
nsContainerFrame* insertionFrame =
insertionPoint ? fc->GetContentInsertionFrameFor(insertionPoint)
: nullptr;
return insertionFrame ? insertionFrame : aFrame;
}
nsMenuFrame* nsXULPopupManager::GetNextMenuItem(nsContainerFrame* aParent,
nsMenuFrame* aStart,
bool aIsPopup, bool aWrap) {
nsContainerFrame* immediateParent = ImmediateParentFrame(aParent);
nsIFrame* currFrame = nullptr;
if (aStart) {
if (aStart->GetNextSibling())
currFrame = aStart->GetNextSibling();
else if (aStart->GetParent()->GetContent()->IsXULElement(
nsGkAtoms::menugroup))
currFrame = aStart->GetParent()->GetNextSibling();
} else
currFrame = immediateParent->PrincipalChildList().FirstChild();
while (currFrame) {
// See if it's a menu item.
nsIContent* currFrameContent = currFrame->GetContent();
if (IsValidMenuItem(currFrameContent, aIsPopup)) {
return do_QueryFrame(currFrame);
}
if (currFrameContent->IsXULElement(nsGkAtoms::menugroup) &&
currFrameContent->GetChildCount() > 0)
currFrame = currFrame->PrincipalChildList().FirstChild();
else if (!currFrame->GetNextSibling() &&
currFrame->GetParent()->GetContent()->IsXULElement(
nsGkAtoms::menugroup))
currFrame = currFrame->GetParent()->GetNextSibling();
else
currFrame = currFrame->GetNextSibling();
}
if (!aWrap) {
return aStart;
}
currFrame = immediateParent->PrincipalChildList().FirstChild();
// Still don't have anything. Try cycling from the beginning.
while (currFrame && currFrame != aStart) {
// See if it's a menu item.
nsIContent* currFrameContent = currFrame->GetContent();
if (IsValidMenuItem(currFrameContent, aIsPopup)) {
return do_QueryFrame(currFrame);
}
if (currFrameContent->IsXULElement(nsGkAtoms::menugroup) &&
currFrameContent->GetChildCount() > 0)
currFrame = currFrame->PrincipalChildList().FirstChild();
else if (!currFrame->GetNextSibling() &&
currFrame->GetParent()->GetContent()->IsXULElement(
nsGkAtoms::menugroup))
currFrame = currFrame->GetParent()->GetNextSibling();
else
currFrame = currFrame->GetNextSibling();
}
// No luck. Just return our start value.
return aStart;
}
nsMenuFrame* nsXULPopupManager::GetPreviousMenuItem(nsContainerFrame* aParent,
nsMenuFrame* aStart,
bool aIsPopup, bool aWrap) {
nsContainerFrame* immediateParent = ImmediateParentFrame(aParent);
const nsFrameList& frames(immediateParent->PrincipalChildList());
nsIFrame* currFrame = nullptr;
if (aStart) {
if (aStart->GetPrevSibling())
currFrame = aStart->GetPrevSibling();
else if (aStart->GetParent()->GetContent()->IsXULElement(
nsGkAtoms::menugroup))
currFrame = aStart->GetParent()->GetPrevSibling();
} else
currFrame = frames.LastChild();
while (currFrame) {
// See if it's a menu item.
nsIContent* currFrameContent = currFrame->GetContent();
if (IsValidMenuItem(currFrameContent, aIsPopup)) {
return do_QueryFrame(currFrame);
}
if (currFrameContent->IsXULElement(nsGkAtoms::menugroup) &&
currFrameContent->GetChildCount() > 0) {
const nsFrameList& menugroupFrames(currFrame->PrincipalChildList());
currFrame = menugroupFrames.LastChild();
} else if (!currFrame->GetPrevSibling() &&
currFrame->GetParent()->GetContent()->IsXULElement(
nsGkAtoms::menugroup))
currFrame = currFrame->GetParent()->GetPrevSibling();
else
currFrame = currFrame->GetPrevSibling();
}
if (!aWrap) {
return aStart;
}
currFrame = frames.LastChild();
// Still don't have anything. Try cycling from the end.
while (currFrame && currFrame != aStart) {
// See if it's a menu item.
nsIContent* currFrameContent = currFrame->GetContent();
if (IsValidMenuItem(currFrameContent, aIsPopup)) {
return do_QueryFrame(currFrame);
}
if (currFrameContent->IsXULElement(nsGkAtoms::menugroup) &&
currFrameContent->GetChildCount() > 0) {
const nsFrameList& menugroupFrames(currFrame->PrincipalChildList());
currFrame = menugroupFrames.LastChild();
} else if (!currFrame->GetPrevSibling() &&
currFrame->GetParent()->GetContent()->IsXULElement(
nsGkAtoms::menugroup))
currFrame = currFrame->GetParent()->GetPrevSibling();
else
currFrame = currFrame->GetPrevSibling();
}
// No luck. Just return our start value.
return aStart;
}
bool nsXULPopupManager::IsValidMenuItem(nsIContent* aContent, bool aOnPopup) {
if (!aContent->IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menuitem)) {
return false;
}
nsMenuFrame* menuFrame = do_QueryFrame(aContent->GetPrimaryFrame());
bool skipNavigatingDisabledMenuItem = true;
if (aOnPopup && (!menuFrame || !menuFrame->IsParentMenuList())) {
skipNavigatingDisabledMenuItem =
LookAndFeel::GetInt(LookAndFeel::IntID::SkipNavigatingDisabledMenuItem,
0) != 0;
}
return !(skipNavigatingDisabledMenuItem && aContent->IsElement() &&
aContent->AsElement()->AttrValueIs(kNameSpaceID_None,
nsGkAtoms::disabled,
nsGkAtoms::_true, eCaseMatters));
}
nsresult nsXULPopupManager::HandleEvent(Event* aEvent) {
RefPtr<KeyboardEvent> keyEvent = aEvent->AsKeyboardEvent();
NS_ENSURE_TRUE(keyEvent, NS_ERROR_UNEXPECTED);
// handlers shouldn't be triggered by non-trusted events.
if (!keyEvent->IsTrusted()) {
return NS_OK;
}
nsAutoString eventType;
keyEvent->GetType(eventType);
if (eventType.EqualsLiteral("keyup")) {
return KeyUp(keyEvent);
}
if (eventType.EqualsLiteral("keydown")) {
return KeyDown(keyEvent);
}
if (eventType.EqualsLiteral("keypress")) {
return KeyPress(keyEvent);
}
MOZ_ASSERT_UNREACHABLE("Unexpected eventType");
return NS_OK;
}
nsresult nsXULPopupManager::UpdateIgnoreKeys(bool aIgnoreKeys) {
nsMenuChainItem* item = GetTopVisibleMenu();
if (item) {
item->SetIgnoreKeys(aIgnoreKeys ? eIgnoreKeys_True : eIgnoreKeys_Shortcuts);
}
UpdateKeyboardListeners();
return NS_OK;
}
nsPopupState nsXULPopupManager::GetPopupState(
mozilla::dom::Element* aPopupElement) {
if (mNativeMenu && mNativeMenu->Element()->Contains(aPopupElement)) {
if (aPopupElement != mNativeMenu->Element()) {
// Submenu state is stored in mNativeMenuSubmenuStates.
return mNativeMenuSubmenuStates.MaybeGet(aPopupElement)
.valueOr(ePopupClosed);
}
// mNativeMenu->Element()'s state is stored in its nsMenuPopupFrame.
}
nsMenuPopupFrame* menuPopupFrame =
do_QueryFrame(aPopupElement->GetPrimaryFrame());
if (menuPopupFrame) {
return menuPopupFrame->PopupState();
}
return ePopupClosed;
}
nsresult nsXULPopupManager::KeyUp(KeyboardEvent* aKeyEvent) {
// don't do anything if a menu isn't open or a menubar isn't active
if (!mActiveMenuBar) {
nsMenuChainItem* item = GetTopVisibleMenu();
if (!item || item->PopupType() != ePopupTypeMenu) return NS_OK;
if (item->IgnoreKeys() == eIgnoreKeys_Shortcuts) {
aKeyEvent->StopCrossProcessForwarding();
return NS_OK;
}
}
aKeyEvent->StopPropagation();
aKeyEvent->StopCrossProcessForwarding();
aKeyEvent->PreventDefault();
return NS_OK; // I am consuming event
}
nsresult nsXULPopupManager::KeyDown(KeyboardEvent* aKeyEvent) {
nsMenuChainItem* item = GetTopVisibleMenu();
if (item && item->Frame()->IsMenuLocked()) return NS_OK;
if (HandleKeyboardEventWithKeyCode(aKeyEvent, item)) {
return NS_OK;
}
// don't do anything if a menu isn't open or a menubar isn't active
if (!mActiveMenuBar && (!item || item->PopupType() != ePopupTypeMenu))
return NS_OK;
// Since a menu was open, stop propagation of the event to keep other event
// listeners from becoming confused.
if (!item || item->IgnoreKeys() != eIgnoreKeys_Shortcuts) {
aKeyEvent->StopPropagation();
}
int32_t menuAccessKey = -1;
// If the key just pressed is the access key (usually Alt),
// dismiss and unfocus the menu.
nsMenuBarListener::GetMenuAccessKey(&menuAccessKey);
if (menuAccessKey) {
uint32_t theChar = aKeyEvent->KeyCode();
if (theChar == (uint32_t)menuAccessKey) {
bool ctrl = (menuAccessKey != KeyboardEvent_Binding::DOM_VK_CONTROL &&
aKeyEvent->CtrlKey());
bool alt = (menuAccessKey != KeyboardEvent_Binding::DOM_VK_ALT &&
aKeyEvent->AltKey());
bool shift = (menuAccessKey != KeyboardEvent_Binding::DOM_VK_SHIFT &&
aKeyEvent->ShiftKey());
bool meta = (menuAccessKey != KeyboardEvent_Binding::DOM_VK_META &&
aKeyEvent->MetaKey());
if (!(ctrl || alt || shift || meta)) {
// The access key just went down and no other
// modifiers are already down.
nsMenuChainItem* item = GetTopVisibleMenu();
if (item && !item->Frame()->IsMenuList()) {
Rollup(0, false, nullptr, nullptr);
} else if (mActiveMenuBar) {
mActiveMenuBar->MenuClosed();
}
// Clear the item to avoid bugs as it may have been deleted during
// rollup.
item = nullptr;
}
aKeyEvent->StopPropagation();
aKeyEvent->PreventDefault();
}
}
aKeyEvent->StopCrossProcessForwarding();
return NS_OK;
}
nsresult nsXULPopupManager::KeyPress(KeyboardEvent* aKeyEvent) {
// Don't check prevent default flag -- menus always get first shot at key
// events.
nsMenuChainItem* item = GetTopVisibleMenu();
if (item &&
(item->Frame()->IsMenuLocked() || item->PopupType() != ePopupTypeMenu)) {
return NS_OK;
}
// if a menu is open or a menubar is active, it consumes the key event
bool consume = (item || mActiveMenuBar);
WidgetInputEvent* evt = aKeyEvent->WidgetEventPtr()->AsInputEvent();
bool isAccel = evt && evt->IsAccel();
// When ignorekeys="shortcuts" is used, we don't call preventDefault on the
// key event when the accelerator key is pressed. This allows another
// listener to handle keys. For instance, this allows global shortcuts to
// still apply while a menu is open.
if (item && item->IgnoreKeys() == eIgnoreKeys_Shortcuts && isAccel) {
consume = false;
}
HandleShortcutNavigation(aKeyEvent, nullptr);
aKeyEvent->StopCrossProcessForwarding();
if (consume) {
aKeyEvent->StopPropagation();
aKeyEvent->PreventDefault();
}
return NS_OK; // I am consuming event
}
NS_IMETHODIMP
nsXULPopupHidingEvent::Run() {
RefPtr<nsXULPopupManager> pm = nsXULPopupManager::GetInstance();
Document* document = mPopup->GetUncomposedDoc();
if (pm && document) {
if (RefPtr<nsPresContext> presContext = document->GetPresContext()) {
nsCOMPtr<nsIContent> popup = mPopup;
nsCOMPtr<nsIContent> nextPopup = mNextPopup;
nsCOMPtr<nsIContent> lastPopup = mLastPopup;
pm->FirePopupHidingEvent(popup, nextPopup, lastPopup, presContext,
mPopupType, mDeselectMenu, mIsRollup);
}
}
return NS_OK;
}
bool nsXULPopupPositionedEvent::DispatchIfNeeded(nsIContent* aPopup) {
// The popuppositioned event only fires on arrow panels for now.
if (aPopup->IsElement() &&
aPopup->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
nsGkAtoms::arrow, eCaseMatters)) {
nsCOMPtr<nsIRunnable> event = new nsXULPopupPositionedEvent(aPopup);
aPopup->OwnerDoc()->Dispatch(TaskCategory::Other, event.forget());
return true;
}
return false;
}
static void AlignmentPositionToString(nsMenuPopupFrame* aFrame,
nsAString& aString) {
aString.Truncate();
int8_t position = aFrame->GetAlignmentPosition();
switch (position) {
case POPUPPOSITION_AFTERSTART:
return aString.AssignLiteral("after_start");
case POPUPPOSITION_AFTEREND:
return aString.AssignLiteral("after_end");
case POPUPPOSITION_BEFORESTART:
return aString.AssignLiteral("before_start");
case POPUPPOSITION_BEFOREEND:
return aString.AssignLiteral("before_end");
case POPUPPOSITION_STARTBEFORE:
return aString.AssignLiteral("start_before");
case POPUPPOSITION_ENDBEFORE:
return aString.AssignLiteral("end_before");
case POPUPPOSITION_STARTAFTER:
return aString.AssignLiteral("start_after");
case POPUPPOSITION_ENDAFTER:
return aString.AssignLiteral("end_after");
case POPUPPOSITION_OVERLAP:
return aString.AssignLiteral("overlap");
case POPUPPOSITION_AFTERPOINTER:
return aString.AssignLiteral("after_pointer");
case POPUPPOSITION_SELECTION:
return aString.AssignLiteral("selection");
default:
// Leave as an empty string.
break;
}
}
NS_IMETHODIMP
nsXULPopupPositionedEvent::Run() {
nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
if (!pm) {
return NS_OK;
}
nsMenuPopupFrame* popupFrame = do_QueryFrame(mPopup->GetPrimaryFrame());
if (!popupFrame) {
return NS_OK;
}
popupFrame->WillDispatchPopupPositioned();
// At this point, hidePopup may have been called but it currently has no
// way to stop this event. However, if hidePopup was called, the popup
// will now be in the hiding or closed state. If we are in the shown or
// positioning state instead, we can assume that we are still clear to
// open/move the popup
nsPopupState state = popupFrame->PopupState();
if (state != ePopupPositioning && state != ePopupShown) {
return NS_OK;
}
// Note that the offset might be along either the X or Y axis, but for the
// sake of simplicity we use a point with only the X axis set so we can
// use ToNearestPixels().
int32_t popupOffset = nsPoint(popupFrame->GetAlignmentOffset(), 0)
.ToNearestPixels(AppUnitsPerCSSPixel())
.x;
PopupPositionedEventInit init;
init.mComposed = true;
init.mIsAnchored = popupFrame->IsAnchored();
init.mAlignmentOffset = popupOffset;
AlignmentPositionToString(popupFrame, init.mAlignmentPosition);
RefPtr<PopupPositionedEvent> event =
PopupPositionedEvent::Constructor(mPopup, u"popuppositioned"_ns, init);
event->SetTrusted(true);
mPopup->DispatchEvent(*event);
// Get the popup frame and make sure it is still in the positioning
// state. If it isn't, someone may have tried to reshow or hide it
// during the popuppositioned event.
// Alternately, this event may have been fired in reponse to moving the
// popup rather than opening it. In that case, we are done.
popupFrame = do_QueryFrame(mPopup->GetPrimaryFrame());
if (popupFrame && popupFrame->PopupState() == ePopupPositioning) {
pm->ShowPopupCallback(mPopup, popupFrame, false, false);
}
return NS_OK;
}
NS_IMETHODIMP
nsXULMenuCommandEvent::Run() {
nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
if (!pm) return NS_OK;
// The order of the nsViewManager and PresShell COM pointers is
// important below. We want the pres shell to get released before the
// associated view manager on exit from this function.
// See bug 54233.
// XXXndeakin is this still needed?
nsCOMPtr<nsIContent> popup;
nsMenuFrame* menuFrame = do_QueryFrame(mMenu->GetPrimaryFrame());
AutoWeakFrame weakFrame(menuFrame);
if (menuFrame && mFlipChecked) {
if (menuFrame->IsChecked()) {
mMenu->UnsetAttr(kNameSpaceID_None, nsGkAtoms::checked, true);
} else {
mMenu->SetAttr(kNameSpaceID_None, nsGkAtoms::checked, u"true"_ns, true);
}
}
if (menuFrame && weakFrame.IsAlive()) {
// Find the popup that the menu is inside. Below, this popup will
// need to be hidden.
nsIFrame* frame = menuFrame->GetParent();
while (frame) {
nsMenuPopupFrame* popupFrame = do_QueryFrame(frame);
if (popupFrame) {
popup = popupFrame->GetContent();
break;
}
frame = frame->GetParent();
}
nsPresContext* presContext = menuFrame->PresContext();
RefPtr<PresShell> presShell = presContext->PresShell();
RefPtr<nsViewManager> kungFuDeathGrip = presShell->GetViewManager();
mozilla::Unused
<< kungFuDeathGrip; // Not referred to directly within this function
// Deselect ourselves.
if (mCloseMenuMode != CloseMenuMode_None) menuFrame->SelectMenu(false);
AutoHandlingUserInputStatePusher userInpStatePusher(mUserInput);
RefPtr<Element> menu = mMenu;
nsContentUtils::DispatchXULCommand(
menu, mIsTrusted, nullptr, presShell, mModifiers & MODIFIER_CONTROL,
mModifiers & MODIFIER_ALT, mModifiers & MODIFIER_SHIFT,
mModifiers & MODIFIER_META, 0, mButton);
}
if (popup && mCloseMenuMode != CloseMenuMode_None)
pm->HidePopup(popup, mCloseMenuMode == CloseMenuMode_Auto, true, false,
false);
return NS_OK;
}