/* 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/. */

/**
 * The XUL Popup Manager keeps track of all open popups.
 */

#ifndef nsXULPopupManager_h_
#define nsXULPopupManager_h_

#include "Units.h"
#include "mozilla/Attributes.h"
#include "mozilla/FunctionRef.h"
#include "mozilla/Logging.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/widget/InitData.h"
#include "mozilla/widget/NativeMenu.h"
#include "nsCOMPtr.h"
#include "nsHashtablesFwd.h"
#include "nsIContent.h"
#include "nsIDOMEventListener.h"
#include "nsIObserver.h"
#include "nsIRollupListener.h"
#include "nsPoint.h"
#include "nsTArray.h"
#include "nsThreadUtils.h"

// XXX Avoid including this here by moving function bodies to the cpp file.
#include "mozilla/dom/Element.h"

// X.h defines KeyPress
#ifdef KeyPress
#  undef KeyPress
#endif

/**
 * There are two types that are used:
 *   - dismissable popups such as menus, which should close up when there is a
 *     click outside the popup. In this situation, the entire chain of menus
 *     above should also be closed.
 *   - panels, which stay open until a request is made to close them. This
 *     type is used by tooltips.
 *
 * When a new popup is opened, it is appended to the popup chain, stored in a
 * linked list in mPopups.
 * Popups are stored in this list linked from newest to oldest. When a click
 * occurs outside one of the open dismissable popups, the chain is closed by
 * calling Rollup.
 */

class nsContainerFrame;
class nsITimer;
class nsIDocShellTreeItem;
class nsMenuPopupFrame;
class nsPIDOMWindowOuter;
class nsRefreshDriver;

namespace mozilla {
class PresShell;
namespace dom {
class Event;
class KeyboardEvent;
class UIEvent;
class XULButtonElement;
class XULMenuBarElement;
class XULPopupElement;
}  // namespace dom
}  // namespace mozilla

// XUL popups can be in several different states. When opening a popup, the
// state changes as follows:
//   ePopupClosed - initial state
//   ePopupShowing - during the period when the popupshowing event fires
//   ePopupOpening - between the popupshowing event and being visible. Creation
//                   of the child frames, layout and reflow occurs in this
//                   state. The popup is stored in the popup manager's list of
//                   open popups during this state.
//   ePopupVisible - layout is done and the popup's view and widget are made
//                   visible. The popup is visible on screen but may be
//                   transitioning. The popupshown event has not yet fired.
//   ePopupShown - the popup has been shown and is fully ready. This state is
//                 assigned just before the popupshown event fires.
// When closing a popup:
//   ePopupHidden - during the period when the popuphiding event fires and
//                  the popup is removed.
//   ePopupClosed - the popup's widget is made invisible.
enum nsPopupState {
  // state when a popup is not open
  ePopupClosed,
  // state from when a popup is requested to be shown to after the
  // popupshowing event has been fired.
  ePopupShowing,
  // state while a popup is waiting to be laid out and positioned
  ePopupPositioning,
  // state while a popup is open but the widget is not yet visible
  ePopupOpening,
  // state while a popup is visible and waiting for the popupshown event
  ePopupVisible,
  // state while a popup is open and visible on screen
  ePopupShown,
  // state from when a popup is requested to be hidden to when it is closed.
  ePopupHiding,
  // state which indicates that the popup was hidden without firing the
  // popuphiding or popuphidden events. It is used when executing a menu
  // command because the menu needs to be hidden before the command event
  // fires, yet the popuphiding and popuphidden events are fired after. This
  // state can also occur when the popup is removed because the document is
  // unloaded.
  ePopupInvisible
};

// when a menu command is executed, the closemenu attribute may be used
// to define how the menu should be closed up
enum CloseMenuMode {
  CloseMenuMode_Auto,   // close up the chain of menus, default value
  CloseMenuMode_None,   // don't close up any menus
  CloseMenuMode_Single  // close up only the menu the command is inside
};

/**
 * nsNavigationDirection: an enum expressing navigation through the menus in
 * terms which are independent of the directionality of the chrome. The
 * terminology, derived from XSL-FO and CSS3 (e.g.
 * http://www.w3.org/TR/css3-text/#TextLayout), is BASE (Before, After, Start,
 * End), with the addition of First and Last (mapped to Home and End
 * respectively).
 *
 * In languages such as English where the inline progression is left-to-right
 * and the block progression is top-to-bottom (lr-tb), these terms will map out
 * as in the following diagram
 *
 *  --- inline progression --->
 *
 *           First              |
 *           ...                |
 *           Before             |
 *         +--------+         block
 *   Start |        | End  progression
 *         +--------+           |
 *           After              |
 *           ...                |
 *           Last               V
 *
 */

enum nsNavigationDirection {
  eNavigationDirection_Last,
  eNavigationDirection_First,
  eNavigationDirection_Start,
  eNavigationDirection_Before,
  eNavigationDirection_End,
  eNavigationDirection_After
};

enum nsIgnoreKeys {
  eIgnoreKeys_False,
  eIgnoreKeys_True,
  eIgnoreKeys_Shortcuts,
};

enum class HidePopupOption : uint8_t {
  // If the entire chain of menus should be closed.
  HideChain,
  // If the parent <menu> of the popup should not be deselected. This will not
  // be set when the menu is closed by pressing the Escape key.
  DeselectMenu,
  // If the first popuphiding event should be sent asynchrously. This should
  // be set if HidePopup is called from a frame.
  Async,
  // If this popup is hiding due to being cancelled.
  IsRollup,
  // Whether animations should be disabled for rolled-up popups.
  DisableAnimations,
};

using HidePopupOptions = mozilla::EnumSet<HidePopupOption>;

/**
 * DirectionFromKeyCodeTable: two arrays, the first for left-to-right and the
 * other for right-to-left, that map keycodes to values of
 * nsNavigationDirection.
 */
extern const nsNavigationDirection DirectionFromKeyCodeTable[2][6];

#define NS_DIRECTION_FROM_KEY_CODE(frame, keycode)                    \
  (DirectionFromKeyCodeTable                                          \
       [static_cast<uint8_t>((frame)->StyleVisibility()->mDirection)] \
       [(keycode) - mozilla::dom::KeyboardEvent_Binding::DOM_VK_END])

// Used to hold information about a popup that is about to be opened.
struct PendingPopup {
  using Element = mozilla::dom::Element;
  using Event = mozilla::dom::Event;

  PendingPopup(Element* aPopup, Event* aEvent);

  const RefPtr<Element> mPopup;
  const RefPtr<Event> mEvent;

  // Device pixels relative to the showing popup's presshell's
  // root prescontext's root frame.
  mozilla::LayoutDeviceIntPoint mMousePoint;

  // Cached modifiers used to trigger the popup.
  mozilla::Modifiers mModifiers;

  already_AddRefed<nsIContent> GetTriggerContent() const;

  void InitMousePoint();

  void SetMousePoint(mozilla::LayoutDeviceIntPoint aMousePoint) {
    mMousePoint = aMousePoint;
  }

  uint16_t MouseInputSource() const;
};

// nsMenuChainItem holds info about an open popup. Items are stored in a
// doubly linked list. Note that the linked list is stored beginning from
// the lowest child in a chain of menus, as this is the active submenu.
class nsMenuChainItem {
  using PopupType = mozilla::widget::PopupType;

  nsMenuPopupFrame* mFrame;  // the popup frame
  PopupType mPopupType;      // the popup type of the frame
  bool mNoAutoHide;          // true for noautohide panels
  bool mIsContext;           // true for context menus
  bool mOnMenuBar;           // true if the menu is on a menu bar
  nsIgnoreKeys mIgnoreKeys;  // indicates how keyboard listeners should be used

  // True if the popup should maintain its position relative to the anchor when
  // the anchor moves.
  bool mFollowAnchor;

  // The last seen position of the anchor, relative to the screen.
  nsRect mCurrentRect;

  mozilla::UniquePtr<nsMenuChainItem> mParent;
  // Back pointer, safe because mChild keeps us alive.
  nsMenuChainItem* mChild = nullptr;

 public:
  nsMenuChainItem(nsMenuPopupFrame* aFrame, bool aNoAutoHide, bool aIsContext,
                  PopupType aPopupType)
      : mFrame(aFrame),
        mPopupType(aPopupType),
        mNoAutoHide(aNoAutoHide),
        mIsContext(aIsContext),
        mOnMenuBar(false),
        mIgnoreKeys(eIgnoreKeys_False),
        mFollowAnchor(false) {
    NS_ASSERTION(aFrame, "null frame passed to nsMenuChainItem constructor");
    MOZ_COUNT_CTOR(nsMenuChainItem);
  }

  MOZ_COUNTED_DTOR(nsMenuChainItem)

  mozilla::dom::XULPopupElement* Element();
  nsMenuPopupFrame* Frame() { return mFrame; }
  PopupType GetPopupType() { return mPopupType; }
  bool IsNoAutoHide() { return mNoAutoHide; }
  void SetNoAutoHide(bool aNoAutoHide) { mNoAutoHide = aNoAutoHide; }
  bool IsMenu() { return mPopupType == PopupType::Menu; }
  bool IsContextMenu() { return mIsContext; }
  nsIgnoreKeys IgnoreKeys() { return mIgnoreKeys; }
  void SetIgnoreKeys(nsIgnoreKeys aIgnoreKeys) { mIgnoreKeys = aIgnoreKeys; }
  bool IsOnMenuBar() { return mOnMenuBar; }
  void SetOnMenuBar(bool aOnMenuBar) { mOnMenuBar = aOnMenuBar; }
  nsMenuChainItem* GetParent() { return mParent.get(); }
  nsMenuChainItem* GetChild() { return mChild; }
  bool FollowsAnchor() { return mFollowAnchor; }
  void UpdateFollowAnchor();
  void CheckForAnchorChange();

  // set the parent of this item to aParent, also changing the parent
  // to have this as a child.
  void SetParent(mozilla::UniquePtr<nsMenuChainItem> aParent);
  // Removes the parent pointer and returns it.
  mozilla::UniquePtr<nsMenuChainItem> Detach();
};

// this class is used for dispatching popuphiding events asynchronously.
class nsXULPopupHidingEvent : public mozilla::Runnable {
  using PopupType = mozilla::widget::PopupType;
  using Element = mozilla::dom::Element;

 public:
  nsXULPopupHidingEvent(Element* aPopup, Element* aNextPopup,
                        Element* aLastPopup, PopupType aPopupType,
                        HidePopupOptions aOptions)
      : mozilla::Runnable("nsXULPopupHidingEvent"),
        mPopup(aPopup),
        mNextPopup(aNextPopup),
        mLastPopup(aLastPopup),
        mPopupType(aPopupType),
        mOptions(aOptions) {
    NS_ASSERTION(aPopup,
                 "null popup supplied to nsXULPopupHidingEvent constructor");
    // aNextPopup and aLastPopup may be null
  }

  NS_IMETHOD Run() override;

 private:
  nsCOMPtr<Element> mPopup;
  nsCOMPtr<Element> mNextPopup;
  nsCOMPtr<Element> mLastPopup;
  PopupType mPopupType;
  HidePopupOptions mOptions;
};

// this class is used for dispatching popuppositioned events asynchronously.
class nsXULPopupPositionedEvent : public mozilla::Runnable {
  using Element = mozilla::dom::Element;

 public:
  explicit nsXULPopupPositionedEvent(Element* aPopup)
      : mozilla::Runnable("nsXULPopupPositionedEvent"), mPopup(aPopup) {
    MOZ_ASSERT(aPopup);
  }

  NS_IMETHOD Run() override;

  // Asynchronously dispatch a popuppositioned event at aPopup if this is a
  // panel that should receieve such events. Return true if the event was sent.
  static bool DispatchIfNeeded(Element* aPopup);

 private:
  const nsCOMPtr<Element> mPopup;
};

// this class is used for dispatching menu command events asynchronously.
class nsXULMenuCommandEvent : public mozilla::Runnable {
  using Element = mozilla::dom::Element;

 public:
  nsXULMenuCommandEvent(Element* aMenu, bool aIsTrusted,
                        mozilla::Modifiers aModifiers, bool aUserInput,
                        bool aFlipChecked, int16_t aButton)
      : mozilla::Runnable("nsXULMenuCommandEvent"),
        mMenu(aMenu),
        mModifiers(aModifiers),
        mButton(aButton),
        mIsTrusted(aIsTrusted),
        mUserInput(aUserInput),
        mFlipChecked(aFlipChecked),
        mCloseMenuMode(CloseMenuMode_Auto) {
    NS_ASSERTION(aMenu,
                 "null menu supplied to nsXULMenuCommandEvent constructor");
  }

  MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override;

  void SetCloseMenuMode(CloseMenuMode aCloseMenuMode) {
    mCloseMenuMode = aCloseMenuMode;
  }

 private:
  RefPtr<Element> mMenu;

  mozilla::Modifiers mModifiers;
  int16_t mButton;
  bool mIsTrusted;
  bool mUserInput;
  bool mFlipChecked;
  CloseMenuMode mCloseMenuMode;
};

class nsXULPopupManager final : public nsIDOMEventListener,
                                public nsIRollupListener,
                                public nsIObserver,
                                public mozilla::widget::NativeMenu::Observer {
 public:
  friend class nsXULPopupHidingEvent;
  friend class nsXULPopupPositionedEvent;
  friend class nsXULMenuCommandEvent;
  friend class TransitionEnder;

  using PopupType = mozilla::widget::PopupType;
  using Element = mozilla::dom::Element;

  NS_DECL_ISUPPORTS
  NS_DECL_NSIOBSERVER
  NS_DECL_NSIDOMEVENTLISTENER

  // nsIRollupListener
  MOZ_CAN_RUN_SCRIPT_BOUNDARY
  bool Rollup(const RollupOptions&,
              nsIContent** aLastRolledUp = nullptr) override;
  bool ShouldRollupOnMouseWheelEvent() override;
  bool ShouldConsumeOnMouseWheelEvent() override;
  bool ShouldRollupOnMouseActivate() override;
  uint32_t GetSubmenuWidgetChain(nsTArray<nsIWidget*>* aWidgetChain) override;
  nsIWidget* GetRollupWidget() override;
  bool RollupNativeMenu() override;

  MOZ_CAN_RUN_SCRIPT_BOUNDARY bool RollupTooltips();

  enum class RollupKind { Tooltip, Menu };
  MOZ_CAN_RUN_SCRIPT
  bool RollupInternal(RollupKind, const RollupOptions&,
                      nsIContent** aLastRolledUp);

  // NativeMenu::Observer
  void OnNativeMenuOpened() override;
  MOZ_CAN_RUN_SCRIPT_BOUNDARY void OnNativeMenuClosed() override;
  void OnNativeSubMenuWillOpen(mozilla::dom::Element* aPopupElement) override;
  void OnNativeSubMenuDidOpen(mozilla::dom::Element* aPopupElement) override;
  void OnNativeSubMenuClosed(mozilla::dom::Element* aPopupElement) override;
  MOZ_CAN_RUN_SCRIPT_BOUNDARY void OnNativeMenuWillActivateItem(
      mozilla::dom::Element* aMenuItemElement) override;

  static mozilla::StaticRefPtr<nsXULPopupManager> sInstance;

  // initialize and shutdown methods called by nsLayoutStatics
  static void Init();
  static void Shutdown();

  // returns a weak reference to the popup manager instance, could return null
  // if a popup manager could not be allocated
  static nsXULPopupManager* GetInstance();

  // This should be called when a window is moved or resized to adjust the
  // popups accordingly.
  void AdjustPopupsOnWindowChange(nsPIDOMWindowOuter* aWindow);
  void AdjustPopupsOnWindowChange(mozilla::PresShell* aPresShell);

  // inform the popup manager that a menu bar has been activated or deactivated,
  // either because one of its menus has opened or closed, or that the menubar
  // has been focused such that its menus may be navigated with the keyboard.
  // aActivate should be true when the menubar should be focused, and false
  // when the active menu bar should be defocused. In the latter case, if
  // aMenuBar isn't currently active, yet another menu bar is, that menu bar
  // will remain active.
  void SetActiveMenuBar(mozilla::dom::XULMenuBarElement* aMenuBar,
                        bool aActivate);

  struct MayShowMenuResult {
    const bool mIsNative = false;
    mozilla::dom::XULButtonElement* const mMenuButton = nullptr;
    nsMenuPopupFrame* const mMenuPopupFrame = nullptr;

    explicit operator bool() const {
      MOZ_ASSERT(!!mMenuButton == !!mMenuPopupFrame);
      return mIsNative || mMenuButton;
    }
  };

  MayShowMenuResult MayShowMenu(nsIContent* aMenu);

  /**
   * Open a <menu> given its content node. If aSelectFirstItem is
   * set to true, the first item on the menu will automatically be
   * selected.
   */
  void ShowMenu(nsIContent* aMenu, bool aSelectFirstItem);

  /**
   * Open a popup, either anchored or unanchored. If aSelectFirstItem is
   * true, then the first item in the menu is selected. The arguments are
   * similar to those for XULPopupElement::OpenPopup.
   *
   * aTriggerEvent should be the event that triggered the event. This is used
   * to determine the coordinates and trigger node for the popup. This may be
   * null if the popup was not triggered by an event.
   *
   * This fires the popupshowing event synchronously.
   */
  void ShowPopup(Element* aPopup, nsIContent* aAnchorContent,
                 const nsAString& aPosition, int32_t aXPos, int32_t aYPos,
                 bool aIsContextMenu, bool aAttributesOverride,
                 bool aSelectFirstItem, mozilla::dom::Event* aTriggerEvent);

  /**
   * Open a popup at a specific screen position specified by aXPos and aYPos,
   * measured in CSS pixels.
   *
   * This fires the popupshowing event synchronously.
   *
   * If aIsContextMenu is true, the popup is positioned at a slight
   * offset from aXPos/aYPos to ensure that it is not under the mouse
   * cursor.
   */
  void ShowPopupAtScreen(Element* aPopup, int32_t aXPos, int32_t aYPos,
                         bool aIsContextMenu,
                         mozilla::dom::Event* aTriggerEvent);

  /* Open a popup anchored at a screen rectangle specified by aRect.
   * The remaining arguments are similar to ShowPopup.
   */
  void ShowPopupAtScreenRect(Element* aPopup, const nsAString& aPosition,
                             const nsIntRect& aRect, bool aIsContextMenu,
                             bool aAttributesOverride,
                             mozilla::dom::Event* aTriggerEvent);

  /**
   * Open a popup as a native menu, anchored to content specified by
   * aAnchorContent, aligned as specified by aPosition.
   *
   * This fires the popupshowing event synchronously.
   *
   * Returns whether native menus are supported for aPopup on this platform.
   * TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230)
   */
  MOZ_CAN_RUN_SCRIPT_BOUNDARY bool ShowPopupAtAnchorAsNativeMenu(
      nsIContent* aAnchorContent, Element* aPopup, const nsAString& aPosition,
      bool aAttributesOverride, mozilla::dom::Event* aTriggerEvent);

  /**
   * Open a popup as a native menu, at a specific screen position specified by
   * aXPos and aYPos, measured in CSS pixels.
   *
   * This fires the popupshowing event synchronously.
   *
   * Returns whether native menus are supported for aPopup on this platform.
   * TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230)
   */
  MOZ_CAN_RUN_SCRIPT_BOUNDARY bool ShowPopupAtScreenAsNativeMenu(
      Element* aPopup, mozilla::CSSIntPoint aScreenPoint, bool aIsContextMenu,
      mozilla::dom::Event* aTriggerEvent);

  /**
   * Open a popup as a native menu, anchored to a specific screen rect specified
   * by aRect, measured in CSS pixels, aligned as specified by aPosition.
   *
   * This fires the popupshowing event synchronously.
   *
   * Returns whether native menus are supported for aPopup on this platform.
   * TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230)
   */
  MOZ_CAN_RUN_SCRIPT_BOUNDARY bool ShowPopupAtScreenRectAsNativeMenu(
      Element* aPopup, const nsAString& aPosition,
      const mozilla::CSSIntRect& aRect, bool aAttributesOverride,
      mozilla::dom::Event* aTriggerEvent);

  /**
   * Open a tooltip at a specific screen position specified by aXPos and aYPos,
   * measured in device pixels. This fires the popupshowing event synchronously.
   */
  void ShowTooltipAtScreen(Element* aPopup, nsIContent* aTriggerContent,
                           const mozilla::LayoutDeviceIntPoint&);

  /*
   * Hide a popup aPopup. If the popup is in a <menu>, then also inform the
   * menu that the popup is being hidden.
   * aLastPopup - optional popup to close last when hiding a chain of menus.
   *              If null, then all popups will be closed.
   */
  void HidePopup(Element* aPopup, HidePopupOptions,
                 Element* aLastPopup = nullptr);

  /*
   * Hide the popup of a <menu>.
   */
  void HideMenu(nsIContent* aMenu);

  /**
   * Hide a popup after a short delay. This is used when rolling over menu
   * items. This timer is stored in mCloseTimer. The timer may be cancelled and
   * the popup closed by calling KillMenuTimer.
   */
  void HidePopupAfterDelay(nsMenuPopupFrame* aPopup, int32_t aDelay);

  /**
   * Hide all of the popups from a given docshell. This should be called when
   * the document is hidden.
   */
  MOZ_CAN_RUN_SCRIPT_BOUNDARY
  void HidePopupsInDocShell(nsIDocShellTreeItem* aDocShellToHide);

  /**
   * Check if any popups need to be repositioned or hidden after a style or
   * layout change. This will update, for example, any arrow type panels when
   * the anchor that is is pointing to has moved, resized or gone away.
   * Only those popups that pertain to the supplied aRefreshDriver are updated.
   */
  void UpdatePopupPositions(nsRefreshDriver* aRefreshDriver);
  MOZ_CAN_RUN_SCRIPT_BOUNDARY
  void PaintPopups(nsRefreshDriver* aRefreshDriver);

  /**
   * Get the first nsMenuChainItem that is matched by the matching callback
   * function provided.
   */
  nsMenuChainItem* FirstMatchingPopup(
      mozilla::FunctionRef<bool(nsMenuChainItem*)> aMatcher) const;

  /**
   * Enable or disable anchor following on the popup if needed.
   */
  void UpdateFollowAnchor(nsMenuPopupFrame* aPopup);

  /**
   * Execute a menu command from the triggering event aEvent.
   *
   * aMenu - a menuitem to execute
   * aEvent - an nsXULMenuCommandEvent that contains all the info from the mouse
   *          event which triggered the menu to be executed, may not be null
   */
  MOZ_CAN_RUN_SCRIPT void ExecuteMenu(nsIContent* aMenu,
                                      nsXULMenuCommandEvent* aEvent);

  /**
   * If a native menu is open, and aItem is an item in the menu's subtree,
   * execute the item with the help of the native menu and close the menu.
   * Returns true if a native menu was open.
   */
  bool ActivateNativeMenuItem(nsIContent* aItem, mozilla::Modifiers aModifiers,
                              int16_t aButton, mozilla::ErrorResult& aRv);

  /**
   * Return true if the popup for the supplied content node is open.
   */
  bool IsPopupOpen(Element* aPopup);

  /**
   * Return the frame for the topmost open popup of a given type, or null if
   * no popup of that type is open. If aType is PopupType::Any, a menu of any
   * type is returned.
   */
  nsIFrame* GetTopPopup(PopupType aType);

  /**
   * Returns the topmost active menuitem that's currently visible, if any.
   */
  nsIContent* GetTopActiveMenuItemContent();

  /**
   * Return an array of all the open and visible popup frames for
   * menus, in order from top to bottom.
   * XXX should we always include native menu?
   */
  void GetVisiblePopups(nsTArray<nsMenuPopupFrame*>& aPopups,
                        bool aIncludeNativeMenu = false);

  /**
   * Get the node that last triggered a popup or tooltip in the document
   * aDocument. aDocument must be non-null and be a document contained within
   * the same window hierarchy as the popup to retrieve.
   */
  already_AddRefed<nsINode> GetLastTriggerPopupNode(
      mozilla::dom::Document* aDocument) {
    return GetLastTriggerNode(aDocument, false);
  }

  already_AddRefed<nsINode> GetLastTriggerTooltipNode(
      mozilla::dom::Document* aDocument) {
    return GetLastTriggerNode(aDocument, true);
  }

  /**
   * Return false if a popup may not be opened. This will return false if the
   * popup is already open, if the popup is in a content shell that is not
   * focused, or if it is a submenu of another menu that isn't open.
   */
  bool MayShowPopup(nsMenuPopupFrame* aFrame);

  /**
   * Called when a popup frame is destroyed. In this case, just remove the
   * item and later popups from the list. No point going through HidePopup as
   * the frames have gone away.
   */
  MOZ_CAN_RUN_SCRIPT void PopupDestroyed(nsMenuPopupFrame* aFrame);

  /**
   * Returns true if there is a context menu open. If aPopup is specified,
   * then the context menu must be later in the chain than aPopup. If aPopup
   * is null, returns true if any context menu at all is open.
   */
  bool HasContextMenu(nsMenuPopupFrame* aPopup);

  /**
   * Update the commands for the menus within the menu popup for a given
   * content node. aPopup should be a XUL menupopup element. This method
   * changes attributes on the children of aPopup, and deals only with the
   * content of the popup, not the frames.
   */
  void UpdateMenuItems(Element* aPopup);

  /**
   * Stop the timer which hides a popup after a delay, started by a previous
   * call to HidePopupAfterDelay. In addition, the popup awaiting to be hidden
   * is closed asynchronously.
   */
  void KillMenuTimer();

  /**
   * Cancel the timer which closes menus after delay, but only if the menu to
   * close is aMenuParent. When a submenu is opened, the user might move the
   * mouse over a sibling menuitem which would normally close the menu. This
   * menu is closed via a timer. However, if the user moves the mouse over the
   * submenu before the timer fires, we should instead cancel the timer. This
   * ensures that the user can move the mouse diagonally over a menu.
   */
  void CancelMenuTimer(nsMenuPopupFrame*);

  /**
   * Handles navigation for menu accelkeys. If aFrame is specified, then the
   * key is handled by that popup, otherwise if aFrame is null, the key is
   * handled by the active popup or menubar.
   */
  MOZ_CAN_RUN_SCRIPT bool HandleShortcutNavigation(
      mozilla::dom::KeyboardEvent& aKeyEvent, nsMenuPopupFrame* aFrame);

  /**
   * Handles cursor navigation within a menu. Returns true if the key has
   * been handled.
   */
  MOZ_CAN_RUN_SCRIPT bool HandleKeyboardNavigation(uint32_t aKeyCode);

  /**
   * Handle keyboard navigation within a menu popup specified by aFrame.
   * Returns true if the key was handled and other default handling
   * should not occur.
   */
  MOZ_CAN_RUN_SCRIPT bool HandleKeyboardNavigationInPopup(
      nsMenuPopupFrame* aFrame, nsNavigationDirection aDir) {
    return HandleKeyboardNavigationInPopup(nullptr, aFrame, aDir);
  }

  /**
   * Handles the keyboard event with keyCode value. Returns true if the event
   * has been handled.
   */
  MOZ_CAN_RUN_SCRIPT bool HandleKeyboardEventWithKeyCode(
      mozilla::dom::KeyboardEvent* aKeyEvent,
      nsMenuChainItem* aTopVisibleMenuItem);

  // Sets mIgnoreKeys of the Top Visible Menu Item
  nsresult UpdateIgnoreKeys(bool aIgnoreKeys);

  nsPopupState GetPopupState(mozilla::dom::Element* aPopupElement);

  mozilla::dom::Event* GetOpeningPopupEvent() const {
    return mPendingPopup->mEvent.get();
  }

  MOZ_CAN_RUN_SCRIPT nsresult KeyUp(mozilla::dom::KeyboardEvent* aKeyEvent);
  MOZ_CAN_RUN_SCRIPT nsresult KeyDown(mozilla::dom::KeyboardEvent* aKeyEvent);
  MOZ_CAN_RUN_SCRIPT nsresult KeyPress(mozilla::dom::KeyboardEvent* aKeyEvent);

 protected:
  nsXULPopupManager();
  ~nsXULPopupManager();

  // get the nsMenuPopupFrame, if any, for the given content node
  MOZ_CAN_RUN_SCRIPT_BOUNDARY
  nsMenuPopupFrame* GetPopupFrameForContent(nsIContent* aContent,
                                            bool aShouldFlush);

  // Get the menu to start rolling up.
  nsMenuChainItem* GetRollupItem(RollupKind);

  // Return the topmost menu, skipping over invisible popups
  nsMenuChainItem* GetTopVisibleMenu() {
    return GetRollupItem(RollupKind::Menu);
  }

  // Add the chain item to the chain and update mPopups to point to it.
  void AddMenuChainItem(mozilla::UniquePtr<nsMenuChainItem>);

  // Removes the chain item from the chain and deletes it.
  void RemoveMenuChainItem(nsMenuChainItem*);

  // Hide all of the visible popups from the given list. This function can
  // cause style changes and frame destruction.
  MOZ_CAN_RUN_SCRIPT void HidePopupsInList(
      const nsTArray<nsMenuPopupFrame*>& aFrames);

  // Hide, but don't close, visible menus. Called before executing a menu item.
  // The caller promises to close the menus properly (with a call to HidePopup)
  // once the item has been executed.
  MOZ_CAN_RUN_SCRIPT void HideOpenMenusBeforeExecutingMenu(CloseMenuMode aMode);

  // callbacks for ShowPopup and HidePopup as events may be done asynchronously
  MOZ_CAN_RUN_SCRIPT void ShowPopupCallback(Element* aPopup,
                                            nsMenuPopupFrame* aPopupFrame,
                                            bool aIsContextMenu,
                                            bool aSelectFirstItem);
  MOZ_CAN_RUN_SCRIPT_BOUNDARY void HidePopupCallback(
      Element* aPopup, nsMenuPopupFrame* aPopupFrame, Element* aNextPopup,
      Element* aLastPopup, PopupType aPopupType, HidePopupOptions);

  /**
   * Trigger frame construction and reflow in the popup, fire a popupshowing
   * event on the popup and then open the popup.
   *
   * aPendingPopup - information about the popup to open
   * aIsContextMenu - true for context menus
   * aSelectFirstItem - true to select the first item in the menu
   * TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230)
   */
  MOZ_CAN_RUN_SCRIPT_BOUNDARY void BeginShowingPopup(
      const PendingPopup& aPendingPopup, bool aIsContextMenu,
      bool aSelectFirstItem);

  /**
   * Fire a popuphiding event and then hide the popup. This will be called
   * recursively if aNextPopup and aLastPopup are set in order to hide a chain
   * of open menus. If these are not set, only one popup is closed. However,
   * if the popup type indicates a menu, yet the next popup is not a menu,
   * then this ends the closing of popups. This allows a menulist inside a
   * non-menu to close up the menu but not close up the panel it is contained
   * within.
   *
   * The caller must keep a strong reference to aPopup, aNextPopup and
   * aLastPopup.
   *
   * aPopup - the popup to hide
   * aNextPopup - the next popup to hide
   * aLastPopup - the last popup in the chain to hide
   * aPresContext - nsPresContext for the popup's frame
   * aPopupType - the PopupType of the frame.
   * aOptions - the relevant options to hide the popup. Only a subset is looked
   * at.
   */
  MOZ_CAN_RUN_SCRIPT_BOUNDARY
  void FirePopupHidingEvent(Element* aPopup, Element* aNextPopup,
                            Element* aLastPopup, nsPresContext* aPresContext,
                            PopupType aPopupType, HidePopupOptions aOptions);

  /**
   * Handle keyboard navigation within a menu popup specified by aItem.
   */
  MOZ_CAN_RUN_SCRIPT
  bool HandleKeyboardNavigationInPopup(nsMenuChainItem* aItem,
                                       nsNavigationDirection aDir) {
    return HandleKeyboardNavigationInPopup(aItem, aItem->Frame(), aDir);
  }

 private:
  /**
   * Handle keyboard navigation within a menu popup aFrame. If aItem is
   * supplied, then it is expected to have a frame equal to aFrame.
   * If aItem is non-null, then the navigation may be redirected to
   * an open submenu if one exists. Returns true if the key was
   * handled and other default handling should not occur.
   */
  MOZ_CAN_RUN_SCRIPT bool HandleKeyboardNavigationInPopup(
      nsMenuChainItem* aItem, nsMenuPopupFrame* aFrame,
      nsNavigationDirection aDir);

  MOZ_CAN_RUN_SCRIPT_BOUNDARY bool ShowNativeMenuInternal(
      Element* aPopup, nsIFrame* aClickedFrame,
      mozilla::dom::Event* aTriggerEvent,
      mozilla::FunctionRef<void(nsMenuPopupFrame*, nsIContent*)> aInitFn,
      mozilla::FunctionRef<void(mozilla::widget::NativeMenu*, nsMenuPopupFrame*,
                                nsIFrame*)>
          aShowFn);

 protected:
  already_AddRefed<nsINode> GetLastTriggerNode(
      mozilla::dom::Document* aDocument, bool aIsTooltip);

  /**
   * Fire a popupshowing event for aPopup.
   */
  MOZ_CAN_RUN_SCRIPT nsEventStatus FirePopupShowingEvent(
      const PendingPopup& aPendingPopup, nsPresContext* aPresContext);

  /**
   * Set mouse capturing for the current popup. This traps mouse clicks that
   * occur outside the popup so that it can be closed up. aOldPopup should be
   * set to the popup that was previously the current popup.
   */
  void SetCaptureState(nsIContent* aOldPopup);

  /**
   * Key event listeners are attached to the document containing the current
   * menu for menu and shortcut navigation. Only one listener is needed at a
   * time, stored in mKeyListener, so switch it only if the document changes.
   * Having menus in different documents is very rare, so the listeners will
   * usually only be attached when the first menu opens and removed when all
   * menus have closed.
   *
   * This is also used when only a menubar is active without any open menus,
   * so that keyboard navigation between menus on the menubar may be done.
   */
  // TODO: Convert UpdateKeyboardListeners() to MOZ_CAN_RUN_SCRIPT and get rid
  //       of the kungFuDeathGrip in it.
  MOZ_CAN_RUN_SCRIPT_BOUNDARY void UpdateKeyboardListeners();

  /*
   * Returns true if the docshell for aDoc is aExpected or a child of aExpected.
   */
  bool IsChildOfDocShell(mozilla::dom::Document* aDoc,
                         nsIDocShellTreeItem* aExpected);

  // Finds a chain item in mPopups.
  nsMenuChainItem* FindPopup(Element* aPopup) const;

  // the document the key event listener is attached to
  nsCOMPtr<mozilla::dom::EventTarget> mKeyListener;

  // widget that is currently listening to rollup events
  nsCOMPtr<nsIWidget> mWidget;

  // set to the currently active menu bar, if any
  mozilla::dom::XULMenuBarElement* mActiveMenuBar;

  // linked list of normal menus and panels. mPopups points to the innermost
  // popup, which keeps alive all their parents.
  mozilla::UniquePtr<nsMenuChainItem> mPopups;

  // timer used for HidePopupAfterDelay
  nsCOMPtr<nsITimer> mCloseTimer;
  nsMenuPopupFrame* mTimerMenu = nullptr;

  // Information about the popup that is currently firing a popupshowing event.
  const PendingPopup* mPendingPopup;

  // If a popup is displayed as a native menu, this is non-null while the
  // native menu is open.
  // mNativeMenu has a strong reference to the menupopup nsIContent.
  RefPtr<mozilla::widget::NativeMenu> mNativeMenu;

  // If the currently open native menu activated an item, this is the item's
  // close menu mode. Nothing() if mNativeMenu is null or if no item was
  // activated.
  mozilla::Maybe<CloseMenuMode> mNativeMenuActivatedItemCloseMenuMode;

  // If a popup is displayed as a native menu, this map contains the popup state
  // for any of its non-closed submenus. This state cannot be stored on the
  // submenus' nsMenuPopupFrames, because we usually don't generate frames for
  // the contents of native menus.
  // If a submenu is not present in this map, it means it's closed.
  // This map is empty if mNativeMenu is null.
  nsTHashMap<RefPtr<mozilla::dom::Element>, nsPopupState>
      mNativeMenuSubmenuStates;
};

#endif
