Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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 "XULButtonElement.h"
#include "mozilla/Assertions.h"
#include "mozilla/Attributes.h"
#include "mozilla/FlushType.h"
#include "mozilla/UniquePtr.h"
#include "nsGkAtoms.h"
#include "nsISound.h"
#include "nsXULPopupManager.h"
#include "nsMenuPopupFrame.h"
#include "nsContentUtils.h"
#include "nsXULElement.h"
#include "nsIDOMXULCommandDispatcher.h"
#include "nsCSSFrameConstructor.h"
#include "nsGlobalWindowOuter.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/dom/XULMenuElement.h"
#include "mozilla/dom/XULMenuBarElement.h"
#include "mozilla/dom/XULPopupElement.h"
#include "mozilla/AutoRestore.h"
#include "mozilla/EventDispatcher.h"
#include "mozilla/EventStateManager.h"
#include "mozilla/LookAndFeel.h"
#include "mozilla/MouseEvents.h"
#include "mozilla/PointerLockManager.h"
#include "mozilla/PresShell.h"
#include "mozilla/Services.h"
#include "mozilla/StaticPrefs_ui.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");
#define NS_DIRECTION_IS_INLINE(dir) \
(dir == eNavigationDirection_Start || dir == eNavigationDirection_End)
#define NS_DIRECTION_IS_BLOCK(dir) \
(dir == eNavigationDirection_Before || dir == eNavigationDirection_After)
#define NS_DIRECTION_IS_BLOCK_TO_EDGE(dir) \
(dir == eNavigationDirection_First || dir == eNavigationDirection_Last)
static_assert(static_cast<uint8_t>(mozilla::StyleDirection::Ltr) == 0 &&
static_cast<uint8_t>(mozilla::StyleDirection::Rtl) == 1,
"Left to Right should be 0 and Right to Left should be 1");
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(Element* 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->IsMouseEventClassOrHasClickRelatedPointerEvent() ||
event->mClass == eMouseScrollEventClass ||
event->mClass == eWheelEventClass) &&
!event->AsGUIEvent()->mWidget) {
// no widget, so just use the client point if available
MouseEvent* mouseEvent = mEvent->AsMouseEvent();
const CSSIntPoint clientPt(RoundedToInt(mouseEvent->ClientPoint()));
// 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;
}
XULPopupElement* nsMenuChainItem::Element() { return &mFrame->PopupElement(); }
void nsMenuChainItem::SetParent(UniquePtr<nsMenuChainItem> aParent) {
MOZ_ASSERT_IF(aParent, !aParent->mChild);
auto oldParent = Detach();
mParent = std::move(aParent);
if (mParent) {
mParent->mChild = this;
}
}
UniquePtr<nsMenuChainItem> nsMenuChainItem::Detach() {
if (mParent) {
MOZ_ASSERT(mParent->mChild == this,
"Unexpected - parent's child not set to this");
mParent->mChild = nullptr;
}
return std::move(mParent);
}
void nsXULPopupManager::AddMenuChainItem(UniquePtr<nsMenuChainItem> aItem) {
PopupType popupType = aItem->Frame()->GetPopupType();
if (StaticPrefs::layout_cursor_disable_for_popups() &&
popupType != PopupType::Tooltip) {
if (nsPresContext* rootPC =
aItem->Frame()->PresContext()->GetRootPresContext()) {
if (nsCOMPtr<nsIWidget> rootWidget = rootPC->GetRootWidget()) {
rootWidget->SetCustomCursorAllowed(false);
}
}
}
// 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->Element();
}
aItem->SetParent(std::move(mPopups));
mPopups = std::move(aItem);
SetCaptureState(oldmenu);
}
void nsXULPopupManager::RemoveMenuChainItem(nsMenuChainItem* aItem) {
nsPresContext* rootPC = aItem->Frame()->PresContext()->GetRootPresContext();
auto matcher = [&](nsMenuChainItem* aChainItem) -> bool {
return aChainItem != aItem &&
rootPC == aChainItem->Frame()->PresContext()->GetRootPresContext();
};
if (rootPC && !FirstMatchingPopup(matcher)) {
if (nsCOMPtr<nsIWidget> rootWidget = rootPC->GetRootWidget()) {
rootWidget->SetCustomCursorAllowed(true);
}
}
auto parent = aItem->Detach();
if (auto* child = aItem->GetChild()) {
MOZ_ASSERT(aItem != mPopups,
"Unexpected - popup with child at end of chain");
// This will kill aItem by changing child's mParent pointer.
child->SetParent(std::move(parent));
} 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.
MOZ_ASSERT(aItem == mPopups,
"Unexpected - popup with no child not at end of chain");
mPopups = std::move(parent);
}
}
nsMenuChainItem* nsXULPopupManager::FirstMatchingPopup(
mozilla::FunctionRef<bool(nsMenuChainItem*)> aMatcher) const {
for (nsMenuChainItem* popup = mPopups.get(); popup;
popup = popup->GetParent()) {
if (aMatcher(popup)) {
return popup;
}
}
return 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), 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::RollupTooltips() {
const RollupOptions options{0, FlushViews::Yes, nullptr, AllowAnimations::No};
return RollupInternal(RollupKind::Tooltip, options, nullptr);
}
bool nsXULPopupManager::Rollup(const RollupOptions& aOptions,
nsIContent** aLastRolledUp) {
return RollupInternal(RollupKind::Menu, aOptions, aLastRolledUp);
}
bool nsXULPopupManager::RollupNativeMenu() {
if (mNativeMenu) {
RefPtr<NativeMenu> menu = mNativeMenu;
return menu->Close();
}
return false;
}
bool nsXULPopupManager::RollupInternal(RollupKind aKind,
const RollupOptions& aOptions,
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(false);
}
return false;
}
nsMenuChainItem* item = GetRollupItem(aKind);
if (!item) {
return false;
}
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()->GetPopupType() != parent->Frame()->GetPopupType() ||
first->IsContextMenu() != parent->IsContextMenu()) {
break;
}
first = parent;
}
*aLastRolledUp = first->Element();
}
ConsumeOutsideClicksResult consumeResult =
item->Frame()->ConsumeOutsideClicks();
bool 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 && aOptions.mPoint &&
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) &&
aOptions.mPoint) {
nsMenuPopupFrame* popupFrame = item->Frame();
CSSIntRect anchorRect = [&] {
if (popupFrame->IsAnchored()) {
// Check if the popup has an anchor rectangle set. If not, get the
// rectangle from the anchor element.
auto r = popupFrame->GetScreenAnchorRect();
if (r.x != -1 && r.y != -1) {
// Prefer the untransformed anchor rect, so as to account for Wayland
// properly. Note we still need to check GetScreenAnchorRect() tho, so
// as to detect whether the anchor came from the popup opening call,
// or from an element (in which case we want to take the code-path
// below)..
auto untransformed = popupFrame->GetUntransformedAnchorRect();
if (!untransformed.IsEmpty()) {
return CSSIntRect::FromAppUnitsRounded(untransformed);
}
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(*aOptions.mPoint);
if (anchorRect.Contains(posCSSPixels)) {
if (consumeResult == ConsumeOutsideClicks_ParentOnly) {
consume = true;
}
if (noRollupOnAnchor) {
rollup = false;
}
}
}
if (!rollup) {
return false;
}
// If a number of popups to close has been specified, determine the last
// popup to close.
Element* lastPopup = nullptr;
uint32_t count = aOptions.mCount;
if (count && count != UINT32_MAX) {
nsMenuChainItem* last = item;
while (--count && last->GetParent()) {
last = last->GetParent();
}
if (last) {
lastPopup = last->Element();
}
}
nsPresContext* presContext = item->Frame()->PresContext();
RefPtr<nsViewManager> viewManager =
presContext->PresShell()->GetViewManager();
HidePopupOptions options{HidePopupOption::HideChain,
HidePopupOption::DeselectMenu,
HidePopupOption::IsRollup};
if (aOptions.mAllowAnimations == AllowAnimations::No) {
options += HidePopupOption::DisableAnimations;
}
HidePopup(item->Element(), options, lastPopup);
if (aOptions.mFlush == FlushViews::Yes) {
// 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(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->GetPopupType() != PopupType::Panel) {
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");
if (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()->GetPopupType() !=
parent->Frame()->GetPopupType() ||
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;
for (nsMenuChainItem* item = mPopups.get(); item; item = item->GetParent()) {
// only move popups that are within the same window and where auto
// positioning has not been disabled
if (!item->IsNoAutoHide()) {
continue;
}
nsMenuPopupFrame* frame = item->Frame();
nsIContent* popup = frame->GetContent();
if (!popup) {
continue;
}
Document* document = popup->GetUncomposedDoc();
if (!document) {
continue;
}
nsPIDOMWindowOuter* window = document->GetWindow();
if (!window) {
continue;
}
window = window->GetPrivateRoot();
if (window == aWindow) {
list.AppendElement(frame);
}
}
for (int32_t l = list.Length() - 1; l >= 0; l--) {
list[l]->SetPopupPosition(true);
}
}
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,
const LayoutDeviceIntPoint& aPoint,
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 curDevBounds = view->RecalcWidgetBounds();
nsIWidget* widget = menuPopupFrame->GetWidget();
if (curDevBounds.TopLeft() == aPoint &&
(!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 (except if positioned by move to rect, in
// which case we better make sure that layout matches that). Otherwise, just
// update the popup to the specified screen coordinates.
if (menuPopupFrame->IsAnchored() &&
menuPopupFrame->GetPopupLevel() == widget::PopupLevel::Parent &&
!aByMoveToRect) {
menuPopupFrame->SetPopupPosition(true);
} else {
CSSPoint cssPos =
aPoint / menuPopupFrame->PresContext()->CSSToDevPixelScale();
menuPopupFrame->MoveTo(cssPos, false, aByMoveToRect);
}
}
void nsXULPopupManager::PopupResized(nsIFrame* aFrame,
const LayoutDeviceIntSize& aSize) {
nsMenuPopupFrame* menuPopupFrame = GetPopupToMoveOrResize(aFrame);
if (!menuPopupFrame) {
return;
}
menuPopupFrame->WidgetPositionOrSizeDidChange();
nsView* view = menuPopupFrame->GetView();
if (!view) {
return;
}
const LayoutDeviceIntRect curDevBounds = view->RecalcWidgetBounds();
// If the size is what we think it is, we have nothing to do.
if (curDevBounds.Size() == aSize) {
return;
}
Element* popup = menuPopupFrame->GetContent()->AsElement();
// Only set the width and height if the popup already has these attributes.
if (!popup->HasAttr(nsGkAtoms::width) || !popup->HasAttr(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);
// FIXME(emilio): aNotify should be consistent (probably true in the two calls
// below?).
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::GetRollupItem(RollupKind aKind) {
for (nsMenuChainItem* item = mPopups.get(); item; item = item->GetParent()) {
if (item->Frame()->PopupState() == ePopupInvisible) {
continue;
}
MOZ_ASSERT_IF(item->Frame()->GetPopupType() == PopupType::Tooltip,
item->IsNoAutoHide());
const bool valid = aKind == RollupKind::Tooltip
? item->Frame()->GetPopupType() == PopupType::Tooltip
: !item->IsNoAutoHide();
if (valid) {
return item;
}
}
return nullptr;
}
void nsXULPopupManager::SetActiveMenuBar(XULMenuBarElement* 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};
}
auto* menu = XULButtonElement::FromNode(aMenu);
if (!menu) {
return {};
}
nsMenuPopupFrame* popupFrame = menu->GetMenuPopup(FlushType::None);
if (!popupFrame || !MayShowPopup(popupFrame)) {
return {};
}
return {false, menu, popupFrame};
}
void nsXULPopupManager::ShowMenu(nsIContent* aMenu, bool aSelectFirstItem) {
auto mayShowResult = MayShowMenu(aMenu);
if (NS_WARN_IF(!mayShowResult)) {
return;
}
if (mayShowResult.mIsNative) {
mNativeMenu->OpenSubmenu(aMenu->AsElement());
return;
}
nsMenuPopupFrame* popupFrame = mayShowResult.mMenuPopupFrame;
// inherit whether or not we're a context menu from the parent
const bool onMenuBar = mayShowResult.mMenuButton->IsOnMenuBar();
const bool onmenu = mayShowResult.mMenuButton->IsOnMenu();
const bool parentIsContextMenu = mayShowResult.mMenuButton->IsOnContextMenu();
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->PopupElement(), nullptr);
BeginShowingPopup(pendingPopup, parentIsContextMenu, aSelectFirstItem);
}
static bool ShouldUseNativeContextMenus() {
#ifdef HAS_NATIVE_MENU_SUPPORT
return mozilla::widget::NativeMenuSupport::ShouldUseNativeContextMenus();
#else
return false;
#endif
}
void nsXULPopupManager::ShowPopup(Element* aPopup, nsIContent* aAnchorContent,
const nsAString& aPosition, int32_t aXPos,
int32_t aYPos, bool aIsContextMenu,
bool aAttributesOverride,
bool aSelectFirstItem, Event* aTriggerEvent) {
#ifdef XP_MACOSX
// On Mac, use a native menu if possible since the non-native menu looks out
// of place. Native menus for anchored popups are not currently implemented,
// so fall back to the non-native path below if `aAnchorContent` is given. We
// also fall back if the position string is not empty so we don't break tests
// that either themselves call or test app features that call
// `openPopup(null, "position")`.
if (!aAnchorContent && aPosition.IsEmpty() && ShouldUseNativeContextMenus() &&
aPopup->IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menupopup) &&
ShowPopupAsNativeMenu(aPopup, aXPos, aYPos, aIsContextMenu,
aTriggerEvent)) {
return;
}
#endif
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);
}
void nsXULPopupManager::ShowPopupAtScreen(Element* 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(Element* 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
menu = mozilla::widget::NativeMenuSupport::CreateNativeContextMenu(aPopup);
#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->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 ((popupFrame = GetPopupFrameForContent(aPopup, true))) {
popupFrame->SetPopupState(ePopupClosed);
popupFrame->ClearTriggerContent();
}
return true;
}
mNativeMenu = menu;
mNativeMenu->AddObserver(this);
nsIFrame* frame = presContext->PresShell()->GetCurrentEventFrame();
if (!frame) {
frame = presContext->PresShell()->GetRootFrame();
}
mNativeMenu->ShowAsContextMenu(frame, CSSIntPoint(aXPos, aYPos),
aIsContextMenu);
// 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);
}
PointerLockManager::Unlock("ShowPopupAsNativeMenu");
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->GetPopupType() == PopupType::Menu) {
// 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->Element(), {HidePopupOption::HideChain});
}
}
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(
Element* 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(
Element* 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(Element* aPopup,
nsMenuPopupFrame* aPopupFrame,
bool aIsContextMenu,
bool aSelectFirstItem) {
PopupType popupType = aPopupFrame->GetPopupType();
const bool isMenu = popupType == PopupType::Menu;
// 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 == PopupType::Tooltip;
auto item = MakeUnique<nsMenuChainItem>(aPopupFrame, isNoAutoHide,
aIsContextMenu, popupType);
// 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;
aPopup->GetAttr(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
if (auto* menu = aPopupFrame->PopupElement().GetContainingMenu()) {
item->SetOnMenuBar(menu->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();
AddMenuChainItem(std::move(item));
NS_ENSURE_TRUE_VOID(weakFrame.IsAlive());
RefPtr popup = &aPopupFrame->PopupElement();
popup->PopupOpened(aSelectFirstItem);
if (isMenu) {
UpdateMenuItems(aPopup);
}
// Caret visibility may have been affected, ensure that
// the caret isn't now drawn when it shouldn't be.
CheckCaretDrawingState();
if (popupType != PopupType::Tooltip) {
PointerLockManager::Unlock("ShowPopupCallback");
}
}
nsMenuChainItem* nsXULPopupManager::FindPopup(Element* aPopup) const {
auto matcher = [&](nsMenuChainItem* aItem) -> bool {
return aItem->Frame()->GetContent() == aPopup;
};
return FirstMatchingPopup(matcher);
}
void nsXULPopupManager::HidePopup(Element* aPopup, HidePopupOptions aOptions,
Element* 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 = FindPopup(aPopup);
RefPtr<Element> 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;
// XXX This preserves behavior but why is it the right thing to do?
aOptions -= HidePopupOption::DeselectMenu;
} 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();
}
}
popupToHide = topMenu->Element();
popupFrame = topMenu->Frame();
const bool hideChain = aOptions.contains(HidePopupOption::HideChain);
// 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 && (hideChain || topMenu != foundPopup)) {
while (parent && parent->IsNoAutoHide()) {
parent = parent->GetParent();
}
if (parent) {
nextPopup = parent->Element();
}
}
lastPopup = aLastPopup ? aLastPopup : (hideChain ? 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.
popupToHide = aPopup;
}
if (!popupToHide) {
return;
}
nsPopupState state = popupFrame->PopupState();
if (state == ePopupHiding) {
// If the popup is already being hidden, don't fire another popuphiding
// event. But finish hiding it sync if we need to.
if (aOptions.contains(HidePopupOption::DisableAnimations) &&
!aOptions.contains(HidePopupOption::Async)) {
HidePopupCallback(popupToHide, popupFrame, nullptr, nullptr,
popupFrame->GetPopupType(), aOptions);
}
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);
}