/* -*- 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/. */ // // 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 MenuPopupAnchorType { MenuPopupAnchorType_Node = 0, // anchored to a node MenuPopupAnchorType_Point = 1, // unanchored and positioned at a screen point MenuPopupAnchorType_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 mPopup; const RefPtr 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 GetPrefISize(gfxContext*) final; nscoord GetMinISize(gfxContext*) final; 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; 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); nsIScrollableFrame* GetScrollFrame() 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 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 TweakMinPrefISize(nscoord&); 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 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 mTriggerContent; nsView* mView = nullptr; RefPtr 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}; // The position of the popup, in CSS pixels. // The screen coordinates, if set to values other than -1, // override mXPos and mYPos. int32_t mXPos = 0; int32_t mYPos = 0; 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