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
//
// nsMenuPopupFrame
//
#ifndef nsMenuPopupFrame_h__
#define nsMenuPopupFrame_h__
#include "mozilla/Attributes.h"
#include "mozilla/gfx/Types.h"
#include "mozilla/StaticPrefs_ui.h"
#include "mozilla/TimeStamp.h"
#include "nsAtom.h"
#include "nsCOMPtr.h"
#include "nsIDOMEventListener.h"
#include "nsXULPopupManager.h"
#include "nsBlockFrame.h"
#include "Units.h"
class nsIWidget;
namespace mozilla {
class PresShell;
enum class WindowShadow : uint8_t;
namespace dom {
class KeyboardEvent;
class XULButtonElement;
class XULPopupElement;
} // namespace dom
namespace widget {
enum class PopupLevel : uint8_t;
}
} // namespace mozilla
enum ConsumeOutsideClicksResult {
ConsumeOutsideClicks_ParentOnly =
0, // Only consume clicks on the parent anchor
ConsumeOutsideClicks_True = 1, // Always consume clicks
ConsumeOutsideClicks_Never = 2 // Never consume clicks
};
// How a popup may be flipped. Flipping to the outside edge is like how
// a submenu would work. The entire popup is flipped to the opposite side
// of the anchor.
enum FlipStyle {
FlipStyle_None = 0,
FlipStyle_Outside = 1,
FlipStyle_Inside = 2
};
// Values for the flip attribute
enum FlipType {
FlipType_Default = 0,
FlipType_None = 1, // don't try to flip or translate to stay onscreen
FlipType_Both = 2, // flip in both directions
FlipType_Slide = 3 // allow the arrow to "slide" instead of resizing
};
enum class MenuPopupAnchorType : uint8_t {
Node = 0, // anchored to a node
Point = 1, // unanchored, and positioned at a screen point
Rect = 2, // anchored at a screen rectangle
};
// values are selected so that the direction can be flipped just by
// changing the sign
#define POPUPALIGNMENT_NONE 0
#define POPUPALIGNMENT_TOPLEFT 1
#define POPUPALIGNMENT_TOPRIGHT -1
#define POPUPALIGNMENT_BOTTOMLEFT 2
#define POPUPALIGNMENT_BOTTOMRIGHT -2
#define POPUPALIGNMENT_LEFTCENTER 16
#define POPUPALIGNMENT_RIGHTCENTER -16
#define POPUPALIGNMENT_TOPCENTER 17
#define POPUPALIGNMENT_BOTTOMCENTER 18
// The constants here are selected so that horizontally and vertically flipping
// can be easily handled using the two flip macros below.
#define POPUPPOSITION_UNKNOWN -1
#define POPUPPOSITION_BEFORESTART 0
#define POPUPPOSITION_BEFOREEND 1
#define POPUPPOSITION_AFTERSTART 2
#define POPUPPOSITION_AFTEREND 3
#define POPUPPOSITION_STARTBEFORE 4
#define POPUPPOSITION_ENDBEFORE 5
#define POPUPPOSITION_STARTAFTER 6
#define POPUPPOSITION_ENDAFTER 7
#define POPUPPOSITION_OVERLAP 8
#define POPUPPOSITION_AFTERPOINTER 9
#define POPUPPOSITION_SELECTION 10
#define POPUPPOSITION_HFLIP(v) (v ^ 1)
#define POPUPPOSITION_VFLIP(v) (v ^ 2)
nsIFrame* NS_NewMenuPopupFrame(mozilla::PresShell* aPresShell,
mozilla::ComputedStyle* aStyle);
class nsView;
class nsMenuPopupFrame;
// this class is used for dispatching popupshown events asynchronously.
class nsXULPopupShownEvent final : public mozilla::Runnable,
public nsIDOMEventListener {
public:
nsXULPopupShownEvent(nsIContent* aPopup, nsPresContext* aPresContext)
: mozilla::Runnable("nsXULPopupShownEvent"),
mPopup(aPopup),
mPresContext(aPresContext) {}
NS_DECL_ISUPPORTS_INHERITED
NS_DECL_NSIRUNNABLE
NS_DECL_NSIDOMEVENTLISTENER
void CancelListener();
protected:
virtual ~nsXULPopupShownEvent() = default;
private:
const nsCOMPtr<nsIContent> mPopup;
const RefPtr<nsPresContext> mPresContext;
};
class nsMenuPopupFrame final : public nsBlockFrame {
using PopupLevel = mozilla::widget::PopupLevel;
using PopupType = mozilla::widget::PopupType;
public:
NS_DECL_QUERYFRAME
NS_DECL_FRAMEARENA_HELPERS(nsMenuPopupFrame)
explicit nsMenuPopupFrame(ComputedStyle* aStyle, nsPresContext* aPresContext);
~nsMenuPopupFrame();
// as popups are opened asynchronously, the popup pending state is used to
// prevent multiple requests from attempting to open the same popup twice
nsPopupState PopupState() const { return mPopupState; }
void SetPopupState(nsPopupState);
/*
* When this popup is open, should clicks outside of it be consumed?
* Return true if the popup should rollup on an outside click,
* but consume that click so it can't be used for anything else.
* Return false to allow clicks outside the popup to activate content
* even when the popup is open.
* ---------------------------------------------------------------------
*
* Should clicks outside of a popup be eaten?
*
* Menus Autocomplete Comboboxes
* Mac Eat No Eat
* Win No No Eat
* Unix Eat No Eat
*
*/
ConsumeOutsideClicksResult ConsumeOutsideClicks();
mozilla::dom::XULPopupElement& PopupElement() const;
nscoord IntrinsicISize(const mozilla::IntrinsicSizeInput& aInput,
mozilla::IntrinsicISizeType aType) override;
void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize,
const ReflowInput& aReflowInput,
nsReflowStatus& aStatus) override;
nsIWidget* GetWidget() const;
enum class WidgetStyle : uint8_t {
ColorScheme,
InputRegion,
Opacity,
Shadow,
Transform,
};
using WidgetStyleFlags = mozilla::EnumSet<WidgetStyle>;
static constexpr WidgetStyleFlags AllWidgetStyleFlags() {
return {WidgetStyle::ColorScheme, WidgetStyle::InputRegion,
WidgetStyle::Opacity, WidgetStyle::Shadow, WidgetStyle::Transform};
}
void PropagateStyleToWidget(WidgetStyleFlags = AllWidgetStyleFlags()) const;
// Overridden methods
void Init(nsIContent* aContent, nsContainerFrame* aParent,
nsIFrame* aPrevInFlow) override;
nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
int32_t aModType) override;
// FIXME: This shouldn't run script (this can end up calling HidePopup).
MOZ_CAN_RUN_SCRIPT_BOUNDARY void Destroy(DestroyContext&) override;
bool HasRemoteContent() const;
// Whether we should create a widget on Init().
bool ShouldCreateWidgetUpfront() const;
// Whether we should expand the menu to take the size of the parent menulist.
bool ShouldExpandToInflowParentOrAnchor() const;
// Returns true if the popup is a panel with the noautohide attribute set to
// true. These panels do not roll up automatically.
bool IsNoAutoHide() const;
PopupLevel GetPopupLevel() const { return GetPopupLevel(IsNoAutoHide()); }
// Ensure that a widget has already been created for this view, and create
// one if it hasn't. If aRecreate is true, destroys any existing widget and
// creates a new one, regardless of whether one has already been created.
void PrepareWidget(bool aRecreate = false);
MOZ_CAN_RUN_SCRIPT void EnsureActiveMenuListItemIsVisible();
nsresult CreateWidgetForView(nsView* aView);
mozilla::WindowShadow GetShadowStyle() const;
void DidSetComputedStyle(ComputedStyle* aOldStyle) override;
// layout, position and display the popup as needed
MOZ_CAN_RUN_SCRIPT_BOUNDARY
void LayoutPopup(nsPresContext*, ReflowOutput&, const ReflowInput&,
nsReflowStatus&);
// Set the position of the popup relative to the anchor content, anchored at a
// rectangle, or at a specific point if a screen position is set. The popup
// will be adjusted so that it is on screen. If aIsMove is true, then the
// popup is being moved, and should not be flipped.
void SetPopupPosition(bool aIsMove);
// Called when the Enter key is pressed while the popup is open. This will
// just pass the call down to the current menu, if any.
// Also, calling Enter will reset the current incremental search string,
// calculated in FindMenuWithShortcut.
MOZ_CAN_RUN_SCRIPT void HandleEnterKeyPress(mozilla::WidgetEvent&);
// Locate and return the menu frame that should be activated for the supplied
// key event. If aDoAction is set to true by this method, then the menu's
// action should be carried out, as if the user had pressed the Enter key. If
// aDoAction is false, the menu should just be highlighted.
// This method also handles incremental searching in menus so the user can
// type the first few letters of an item/s name to select it.
mozilla::dom::XULButtonElement* FindMenuWithShortcut(
mozilla::dom::KeyboardEvent& aKeyEvent, bool& aDoAction);
mozilla::dom::XULButtonElement* GetCurrentMenuItem() const;
nsIFrame* GetCurrentMenuItemFrame() const;
PopupType GetPopupType() const { return mPopupType; }
bool IsContextMenu() const { return mIsContextMenu; }
bool IsOpen() const {
return mPopupState == ePopupOpening || mPopupState == ePopupVisible ||
mPopupState == ePopupShown;
}
bool IsVisible() {
return mPopupState == ePopupVisible || mPopupState == ePopupShown;
}
bool IsVisibleOrShowing() {
return IsOpen() || mPopupState == ePopupPositioning ||
mPopupState == ePopupShowing;
}
bool IsNativeMenu() const { return mIsNativeMenu; }
bool IsMouseTransparent() const;
// Return true if the popup is for a menulist.
bool IsMenuList() const;
bool IsDragSource() const { return mIsDragSource; }
void SetIsDragSource(bool aIsDragSource) { mIsDragSource = aIsDragSource; }
static nsIContent* GetTriggerContent(nsMenuPopupFrame* aMenuPopupFrame);
void ClearTriggerContent() { mTriggerContent = nullptr; }
void ClearTriggerContentIncludingDocument();
// returns true if the popup is in a content shell, or false for a popup in
// a chrome shell
bool IsInContentShell() const { return mInContentShell; }
// the Initialize methods are used to set the anchor position for
// each way of opening a popup.
void InitializePopup(nsIContent* aAnchorContent, nsIContent* aTriggerContent,
const nsAString& aPosition, int32_t aXPos, int32_t aYPos,
MenuPopupAnchorType aAnchorType,
bool aAttributesOverride);
void InitializePopupAtRect(nsIContent* aTriggerContent,
const nsAString& aPosition, const nsIntRect& aRect,
bool aAttributesOverride);
/**
* @param aIsContextMenu if true, then the popup is
* positioned at a slight offset from aXPos/aYPos to ensure the
* (presumed) mouse position is not over the menu.
*/
void InitializePopupAtScreen(nsIContent* aTriggerContent, int32_t aXPos,
int32_t aYPos, bool aIsContextMenu);
// Called if this popup should be displayed as an OS-native context menu.
void InitializePopupAsNativeContextMenu(nsIContent* aTriggerContent,
int32_t aXPos, int32_t aYPos);
// indicate that the popup should be opened
void ShowPopup(bool aIsContextMenu);
// indicate that the popup should be hidden. The new state should either be
// ePopupClosed or ePopupInvisible.
MOZ_CAN_RUN_SCRIPT void HidePopup(bool aDeselectMenu, nsPopupState aNewState,
bool aFromFrameDestruction = false);
void ClearIncrementalString() { mIncrementalString.Truncate(); }
static bool IsWithinIncrementalTime(mozilla::TimeStamp time) {
return !sLastKeyTime.IsNull() &&
((time - sLastKeyTime).ToMilliseconds() <=
mozilla::StaticPrefs::ui_menu_incremental_search_timeout());
}
#ifdef DEBUG_FRAME_DUMP
virtual nsresult GetFrameName(nsAString& aResult) const override {
return MakeFrameName(u"MenuPopup"_ns, aResult);
}
#endif
MOZ_CAN_RUN_SCRIPT void ChangeByPage(bool aIsUp);
// Move the popup to the screen coordinate |aPos| in CSS pixels.
// If aUpdateAttrs is true, and the popup already has left or top attributes,
// then those attributes are updated to the new location.
// The frame may be destroyed by this method.
void MoveTo(const mozilla::CSSPoint& aPos, bool aUpdateAttrs,
bool aByMoveToRect = false);
void MoveToAnchor(nsIContent* aAnchorContent, const nsAString& aPosition,
int32_t aXPos, int32_t aYPos, bool aAttributesOverride);
mozilla::ScrollContainerFrame* GetScrollContainerFrame() const;
void SetOverrideConstraintRect(const mozilla::CSSIntRect& aRect) {
mOverrideConstraintRect = mozilla::CSSIntRect::ToAppUnits(aRect);
}
bool IsConstrainedByLayout() const { return mConstrainedByLayout; }
struct Rects {
// For anchored popups, the anchor rectangle. For non-anchored popups, the
// size will be 0.
nsRect mAnchorRect;
// mAnchorRect before accounting for flipping / resizing / intersecting with
// the screen. This is needed for Wayland, which flips / resizes at the
// widget level.
nsRect mUntransformedAnchorRect;
// The final used rect we want to occupy.
nsRect mUsedRect;
// The alignment offset for sliding the panel, see
// nsMenuPopupFrame::mAlignmentOffset.
nscoord mAlignmentOffset = 0;
bool mHFlip = false;
bool mVFlip = false;
bool mConstrainedByLayout = false;
// The client offset of our widget.
mozilla::LayoutDeviceIntPoint mClientOffset;
nsPoint mViewPoint;
};
// For a popup that should appear anchored at the given rect, gets the anchor
// and constraint rects for that popup.
// This will be the available area of the screen the popup should be displayed
// on. Content popups, however, will also be constrained by the content area.
//
// For non-toplevel popups (which will always be panels), we will also
// constrain them to the available screen rect, ie they will not fall
// underneath the taskbar, dock or other fixed OS elements.
Rects GetRects(const nsSize& aPrefSize) const;
Maybe<nsRect> GetConstraintRect(const nsRect& aAnchorRect,
const nsRect& aRootScreenRect,
PopupLevel) const;
void PerformMove(const Rects&);
// Return true if the popup is positioned relative to an anchor.
bool IsAnchored() const { return mAnchorType != MenuPopupAnchorType::Point; }
// Return the anchor if there is one.
nsIContent* GetAnchor() const { return mAnchorContent; }
// Return the screen coordinates in CSS pixels of the popup,
// or (-1, -1, 0, 0) if anchored.
mozilla::CSSIntRect GetScreenAnchorRect() const {
return mozilla::CSSRect::FromAppUnitsRounded(mScreenRect);
}
mozilla::LayoutDeviceIntPoint GetLastClientOffset() const {
return mLastClientOffset;
}
// Return the alignment of the popup
int8_t GetAlignmentPosition() const;
// Return the offset applied to the alignment of the popup
nscoord GetAlignmentOffset() const { return mAlignmentOffset; }
// Clear the mPopupShownDispatcher, remove the listener and return true if
// mPopupShownDispatcher was non-null.
bool ClearPopupShownDispatcher() {
if (mPopupShownDispatcher) {
mPopupShownDispatcher->CancelListener();
mPopupShownDispatcher = nullptr;
return true;
}
return false;
}
void ShowWithPositionedEvent() { mPopupState = ePopupPositioning; }
// Checks for the anchor to change and either moves or hides the popup
// accordingly. The original position of the anchor should be supplied as
// the argument. If the popup needs to be hidden, HidePopup will be called by
// CheckForAnchorChange. If the popup needs to be moved, aRect will be updated
// with the new rectangle.
void CheckForAnchorChange(nsRect& aRect);
void WillDispatchPopupPositioned() { mPendingPositionedEvent = false; }
protected:
// returns the popup's level.
PopupLevel GetPopupLevel(bool aIsNoAutoHide) const;
void InitPositionFromAnchorAlign(const nsAString& aAnchor,
const nsAString& aAlign);
// return the position where the popup should be, when it should be
// anchored at anchorRect. aHFlip and aVFlip will be set if the popup may be
// flipped in that direction if there is not enough space available.
nsPoint AdjustPositionForAnchorAlign(nsRect& aAnchorRect,
const nsSize& aPrefSize,
FlipStyle& aHFlip,
FlipStyle& aVFlip) const;
// For popups that are going to align to their selected item, get the frame of
// the selected item.
nsIFrame* GetSelectedItemForAlignment() const;
// check if the popup will fit into the available space and resize it. This
// method handles only one axis at a time so is called twice, once for
// horizontal and once for vertical. All arguments are specified for this
// one axis. All coordinates are in app units relative to the screen.
// aScreenPoint - the point where the popup should appear
// aSize - the size of the popup
// aScreenBegin - the left or top edge of the screen
// aScreenEnd - the right or bottom edge of the screen
// aAnchorBegin - the left or top edge of the anchor rectangle
// aAnchorEnd - the right or bottom edge of the anchor rectangle
// aMarginBegin - the left or top margin of the popup
// aMarginEnd - the right or bottom margin of the popup
// aFlip - how to flip or resize the popup when there isn't space
// aFlipSide - pointer to where current flip mode is stored
nscoord FlipOrResize(nscoord& aScreenPoint, nscoord aSize,
nscoord aScreenBegin, nscoord aScreenEnd,
nscoord aAnchorBegin, nscoord aAnchorEnd,
nscoord aMarginBegin, nscoord aMarginEnd,
FlipStyle aFlip, bool aIsOnEnd, bool* aFlipSide) const;
// check if the popup can fit into the available space by "sliding" (i.e.,
// by having the anchor arrow slide along one axis and only resizing if that
// can't provide the requested size). Only one axis can be slid - the other
// axis is "flipped" as normal. This method can handle either axis, but is
// only called for the sliding axis. All coordinates are in app units
// relative to the screen.
// aScreenPoint - the point where the popup should appear
// aSize - the size of the popup
// aScreenBegin - the left or top edge of the screen
// aScreenEnd - the right or bottom edge of the screen
// aOffset - the amount by which the arrow must be slid such that it is
// still aligned with the anchor.
// Result is the new size of the popup, which will typically be the same
// as aSize, unless aSize is greater than the screen width/height.
nscoord SlideOrResize(nscoord& aScreenPoint, nscoord aSize,
nscoord aScreenBegin, nscoord aScreenEnd,
nscoord* aOffset) const;
// Given an anchor frame, compute the anchor rectangle relative to the screen,
// using the popup frame's app units, and taking into account transforms.
nsRect ComputeAnchorRect(nsPresContext* aRootPresContext,
nsIFrame* aAnchorFrame) const;
// Move the popup to the position specified in its |left| and |top|
// attributes.
void MoveToAttributePosition();
// Create a popup view for this frame. The view is added a child of the root
// view, and is initially hidden.
void CreatePopupView();
nsView* GetViewInternal() const override { return mView; }
void SetViewInternal(nsView* aView) override { mView = aView; }
// Returns true if the popup should try to remain at the same relative
// location as the anchor while it is open. If the anchor becomes hidden
// either directly or indirectly because a parent popup or other element
// is no longer visible, or a parent deck page is changed, the popup hides
// as well. The second variation also sets the anchor rectangle, relative to
// the popup frame.
bool ShouldFollowAnchor() const;
nsIFrame* GetAnchorFrame() const;
public:
/**
* Return whether the popup direction should be RTL.
* If the popup has an anchor, its direction is the anchor direction.
* Otherwise, its the general direction of the UI.
*
* Return whether the popup direction should be RTL.
*/
bool IsDirectionRTL() const;
bool ShouldFollowAnchor(nsRect& aRect);
// Returns parent menu widget for submenus that are in the same
// frame hierarchy, it's needed for Linux/Wayland which demands
// strict popup windows hierarchy.
nsIWidget* GetParentMenuWidget();
// Returns the effective margin for this popup. This is the CSS margin plus
// the context-menu shift, if needed.
nsMargin GetMargin() const;
// These are used by Wayland backend.
const nsRect& GetUntransformedAnchorRect() const {
return mUntransformedAnchorRect;
}
int GetPopupAlignment() const { return mPopupAlignment; }
int GetPopupAnchor() const { return mPopupAnchor; }
FlipType GetFlipType() const { return mFlip; }
void WidgetPositionOrSizeDidChange();
protected:
nsString mIncrementalString; // for incremental typing navigation
// the content that the popup is anchored to, if any, which may be in a
// different document than the popup.
nsCOMPtr<nsIContent> mAnchorContent;
// the content that triggered the popup, typically the node where the mouse
// was clicked. It will be cleared when the popup is hidden.
nsCOMPtr<nsIContent> mTriggerContent;
nsView* mView = nullptr;
RefPtr<nsXULPopupShownEvent> mPopupShownDispatcher;
// The popup's screen rectangle in app units.
nsRect mUsedScreenRect;
// A popup's preferred size may be different than its actual size stored in
// mRect in the case where the popup was resized because it was too large
// for the screen. The preferred size mPrefSize holds the full size the popup
// would be before resizing. Computations are performed using this size.
nsSize mPrefSize{-1, -1};
// A point with extra offsets to apply in the horizontal and vertical axes. We
// don't use an nsMargin because the values would be the same for the same
// axis.
nsPoint mExtraMargin;
nsRect mScreenRect;
// Used for store rectangle which the popup is going to be anchored to, we
// need that for Wayland. It's important that this rect is unflipped, and
// without margins applied, as GTK is what takes care of determining how to
// flip etc. on Wayland.
nsRect mUntransformedAnchorRect;
// If the panel prefers to "slide" rather than resize, then the arrow gets
// positioned at this offset (along either the x or y axis, depending on
// mPosition)
nscoord mAlignmentOffset = 0;
// The value of the client offset of our widget the last time we positioned
// ourselves. We store this so that we can detect when it changes but the
// position of our widget didn't change.
mozilla::LayoutDeviceIntPoint mLastClientOffset;
PopupType mPopupType = PopupType::Panel; // type of popup
nsPopupState mPopupState = ePopupClosed; // open state of the popup
// popup alignment relative to the anchor node
int8_t mPopupAlignment = POPUPALIGNMENT_NONE;
int8_t mPopupAnchor = POPUPALIGNMENT_NONE;
int8_t mPosition = POPUPPOSITION_UNKNOWN;
FlipType mFlip = FlipType_Default; // Whether to flip
// Whether we were moved by the move-to-rect Wayland callback. In that case,
// we stop updating the anchor so that we can end up with a stable position.
bool mPositionedByMoveToRect = false;
// true if the open state changed since the last layout.
bool mIsOpenChanged = false;
// true for context menus and their submenus.
bool mIsContextMenu = false;
// true for the topmost context menu.
bool mIsTopLevelContextMenu = false;
// true if the popup is in a content shell.
bool mInContentShell = true;
// The flip modes that were used when the popup was opened
bool mHFlip = false;
bool mVFlip = false;
// Whether layout has constrained this popup in some way.
bool mConstrainedByLayout = false;
// Whether the most recent initialization of this menupopup happened via
// InitializePopupAsNativeContextMenu.
bool mIsNativeMenu = false;
// Whether we have a pending `popuppositioned` event.
bool mPendingPositionedEvent = false;
// Whether this popup is source of D&D operation. We can't close such
// popup on Wayland as it cancel whole D&D operation.
bool mIsDragSource = false;
// When POPUPPOSITION_SELECTION is used, this indicates the vertical offset
// that the original selected item was. This needs to be used in case the
// popup gets changed so that we can keep the popup at the same vertical
// offset.
// TODO(emilio): try to make this not mutable.
mutable nscoord mPositionedOffset = 0;
// How the popup is anchored.
MenuPopupAnchorType mAnchorType = MenuPopupAnchorType::Node;
nsRect mOverrideConstraintRect;
static int8_t sDefaultLevelIsTop;
static mozilla::TimeStamp sLastKeyTime;
}; // class nsMenuPopupFrame
#endif