/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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 "mozilla/Logging.h"
#include "nsIContent.h"
#include "nsIRollupListener.h"
#include "nsIDOMEventListener.h"
#include "nsPoint.h"
#include "nsCOMPtr.h"
#include "nsTArray.h"
#include "nsIObserver.h"
#include "nsITimer.h"
#include "nsIReflowCallback.h"
#include "nsThreadUtils.h"
#include "nsStyleConsts.h"
#include "nsWidgetInitData.h"
#include "mozilla/Attributes.h"
#include "Units.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 for dismissable menus and panels or mNoHidePanels
 * for tooltips and panels with noautohide="true".
 * 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 nsMenuFrame;
class nsMenuPopupFrame;
class nsMenuBarFrame;
class nsMenuParent;
class nsIDOMKeyEvent;
class nsIDocShellTreeItem;
class nsPIDOMWindowOuter;

// 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_Handled,
};

#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(NS_STYLE_DIRECTION_LTR == 0 && NS_STYLE_DIRECTION_RTL == 1,
              "Left to Right should be 0 and Right to Left should be 1");

/**
 * 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[frame->StyleVisibility()->mDirection]  \
                            [keycode - nsIDOMKeyEvent::DOM_VK_END])

// 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
{
private:
  nsMenuPopupFrame* mFrame; // the popup frame
  nsPopupType mPopupType; // the popup type of the frame
  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

  nsMenuChainItem* mParent;
  nsMenuChainItem* mChild;

public:
  nsMenuChainItem(nsMenuPopupFrame* aFrame, bool aIsContext, nsPopupType aPopupType)
    : mFrame(aFrame),
      mPopupType(aPopupType),
      mIsContext(aIsContext),
      mOnMenuBar(false),
      mIgnoreKeys(eIgnoreKeys_False),
      mParent(nullptr),
      mChild(nullptr)
  {
    NS_ASSERTION(aFrame, "null frame passed to nsMenuChainItem constructor");
    MOZ_COUNT_CTOR(nsMenuChainItem);
  }

  ~nsMenuChainItem()
  {
    MOZ_COUNT_DTOR(nsMenuChainItem);
  }

  nsIContent* Content();
  nsMenuPopupFrame* Frame() { return mFrame; }
  nsPopupType PopupType() { return mPopupType; }
  bool IsMenu() { return mPopupType == ePopupTypeMenu; }
  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; }
  nsMenuChainItem* GetChild() { return mChild; }

  // set the parent of this item to aParent, also changing the parent
  // to have this as a child.
  void SetParent(nsMenuChainItem* aParent);

  // removes an item from the chain. The root pointer must be supplied in case
  // the item is the first item in the chain in which case the pointer will be
  // set to the next item, or null if there isn't another item. After detaching,
  // this item will not have a parent or a child.
  void Detach(nsMenuChainItem** aRoot);
};

// this class is used for dispatching popupshowing events asynchronously.
class nsXULPopupShowingEvent : public mozilla::Runnable
{
public:
  nsXULPopupShowingEvent(nsIContent *aPopup,
                         bool aIsContextMenu,
                         bool aSelectFirstItem)
    : mPopup(aPopup),
      mIsContextMenu(aIsContextMenu),
      mSelectFirstItem(aSelectFirstItem)
  {
    NS_ASSERTION(aPopup, "null popup supplied to nsXULPopupShowingEvent constructor");
  }

  NS_IMETHOD Run() override;

private:
  nsCOMPtr<nsIContent> mPopup;
  bool mIsContextMenu;
  bool mSelectFirstItem;
};

// this class is used for dispatching popuphiding events asynchronously.
class nsXULPopupHidingEvent : public mozilla::Runnable
{
public:
  nsXULPopupHidingEvent(nsIContent *aPopup,
                        nsIContent* aNextPopup,
                        nsIContent* aLastPopup,
                        nsPopupType aPopupType,
                        bool aDeselectMenu,
                        bool aIsCancel)
    : mPopup(aPopup),
      mNextPopup(aNextPopup),
      mLastPopup(aLastPopup),
      mPopupType(aPopupType),
      mDeselectMenu(aDeselectMenu),
      mIsRollup(aIsCancel)
  {
    NS_ASSERTION(aPopup, "null popup supplied to nsXULPopupHidingEvent constructor");
    // aNextPopup and aLastPopup may be null
  }

  NS_IMETHOD Run() override;

private:
  nsCOMPtr<nsIContent> mPopup;
  nsCOMPtr<nsIContent> mNextPopup;
  nsCOMPtr<nsIContent> mLastPopup;
  nsPopupType mPopupType;
  bool mDeselectMenu;
  bool mIsRollup;
};

// this class is used for dispatching menu command events asynchronously.
class nsXULMenuCommandEvent : public mozilla::Runnable
{
public:
  nsXULMenuCommandEvent(nsIContent *aMenu,
                        bool aIsTrusted,
                        bool aShift,
                        bool aControl,
                        bool aAlt,
                        bool aMeta,
                        bool aUserInput,
                        bool aFlipChecked)
    : mMenu(aMenu),
      mIsTrusted(aIsTrusted),
      mShift(aShift),
      mControl(aControl),
      mAlt(aAlt),
      mMeta(aMeta),
      mUserInput(aUserInput),
      mFlipChecked(aFlipChecked),
      mCloseMenuMode(CloseMenuMode_Auto)
  {
    NS_ASSERTION(aMenu, "null menu supplied to nsXULMenuCommandEvent constructor");
  }

  NS_IMETHOD Run() override;

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

private:
  nsCOMPtr<nsIContent> mMenu;
  bool mIsTrusted;
  bool mShift;
  bool mControl;
  bool mAlt;
  bool mMeta;
  bool mUserInput;
  bool mFlipChecked;
  CloseMenuMode mCloseMenuMode;
};

class nsXULPopupManager final : public nsIDOMEventListener,
                                public nsIRollupListener,
                                public nsITimerCallback,
                                public nsIObserver
{

public:
  friend class nsXULPopupShowingEvent;
  friend class nsXULPopupHidingEvent;
  friend class nsXULMenuCommandEvent;
  friend class TransitionEnder;

  NS_DECL_ISUPPORTS
  NS_DECL_NSIOBSERVER
  NS_DECL_NSITIMERCALLBACK
  NS_DECL_NSIDOMEVENTLISTENER

  // nsIRollupListener
  virtual bool Rollup(uint32_t aCount, bool aFlush,
                      const nsIntPoint* pos, nsIContent** aLastRolledUp) override;
  virtual bool ShouldRollupOnMouseWheelEvent() override;
  virtual bool ShouldConsumeOnMouseWheelEvent() override;
  virtual bool ShouldRollupOnMouseActivate() override;
  virtual uint32_t GetSubmenuWidgetChain(nsTArray<nsIWidget*> *aWidgetChain) override;
  virtual void NotifyGeometryChange() override {}
  virtual nsIWidget* GetRollupWidget() override;

  static nsXULPopupManager* sInstance;

  // initialize and shutdown methods called by nsLayoutStatics
  static nsresult 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(nsIPresShell* aPresShell);

  // given a menu frame, find the prevous or next menu frame. If aPopup is
  // true then navigate a menupopup, from one item on the menu to the previous
  // or next one. This is used for cursor navigation between items in a popup
  // menu. If aIsPopup is false, the navigation is on a menubar, so navigate
  // between menus on the menubar. This is used for left/right cursor navigation.
  //
  // Items that are not valid, such as non-menu or non-menuitem elements are
  // skipped, and the next or previous item after that is checked.
  //
  // If aStart is null, the first valid item is retrieved by GetNextMenuItem
  // and the last valid item is retrieved by GetPreviousMenuItem.
  //
  // Both methods will loop around the beginning or end if needed.
  //
  // aParent - the parent menubar or menupopup
  // aStart - the menu/menuitem to start navigation from. GetPreviousMenuItem
  //          returns the item before it, while GetNextMenuItem returns the
  //          item after it.
  // aIsPopup - true for menupopups, false for menubars
  static nsMenuFrame* GetPreviousMenuItem(nsContainerFrame* aParent,
                                          nsMenuFrame* aStart,
                                          bool aIsPopup);
  static nsMenuFrame* GetNextMenuItem(nsContainerFrame* aParent,
                                      nsMenuFrame* aStart,
                                      bool aIsPopup);

  // returns true if the menu item aContent is a valid menuitem which may
  // be navigated to. aIsPopup should be true for items on a popup, or false
  // for items on a menubar.
  static bool IsValidMenuItem(nsIContent* aContent, bool aOnPopup);

  // 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(nsMenuBarFrame* aMenuBar, bool aActivate);

  // retrieve the node and offset of the last mouse event used to open a
  // context menu. This information is determined from the rangeParent and
  // the rangeOffset of the event supplied to ShowPopup or ShowPopupAtScreen.
  // This is used by the implementation of nsIDOMXULDocument::GetPopupRangeParent
  // and nsIDOMXULDocument::GetPopupRangeOffset.
  void GetMouseLocation(nsIDOMNode** aNode, int32_t* aOffset);

  /**
   * Open a <menu> given its content node. If aSelectFirstItem is
   * set to true, the first item on the menu will automatically be
   * selected. If aAsynchronous is true, the event will be dispatched
   * asynchronously. This should be true when called from frame code.
   */
  void ShowMenu(nsIContent *aMenu, bool aSelectFirstItem, bool aAsynchronous);

  /**
   * 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 nsIPopupBoxObject::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(nsIContent* aPopup,
                 nsIContent* aAnchorContent,
                 const nsAString& aPosition,
                 int32_t aXPos, int32_t aYPos,
                 bool aIsContextMenu,
                 bool aAttributesOverride,
                 bool aSelectFirstItem,
                 nsIDOMEvent* 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(nsIContent* aPopup,
                         int32_t aXPos, int32_t aYPos,
                         bool aIsContextMenu,
                         nsIDOMEvent* aTriggerEvent);

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

  /**
   * Open a tooltip at a specific screen position specified by aXPos and aYPos,
   * measured in CSS pixels.
   *
   * This fires the popupshowing event synchronously.
   */
  void ShowTooltipAtScreen(nsIContent* aPopup,
                           nsIContent* aTriggerContent,
                           int32_t aXPos, int32_t aYPos);

  /**
   * This method is provided only for compatibility with an older popup API.
   * New code should not call this function and should call ShowPopup instead.
   *
   * This fires the popupshowing event synchronously.
   */
  void ShowPopupWithAnchorAlign(nsIContent* aPopup,
                                nsIContent* aAnchorContent,
                                nsAString& aAnchor,
                                nsAString& aAlign,
                                int32_t aXPos, int32_t aYPos,
                                bool aIsContextMenu);

  /*
   * Hide a popup aPopup. If the popup is in a <menu>, then also inform the
   * menu that the popup is being hidden.
   *
   * aHideChain - true if the entire chain of menus should be closed. If false,
   *              only this popup is closed.
   * aDeselectMenu - true if the parent <menu> of the popup should be deselected.
   *                 This will be false when the menu is closed by pressing the
   *                 Escape key.
   * aAsynchronous - true if the first popuphiding event should be sent
   *                 asynchrously. This should be true if HidePopup is called
   *                 from a frame.
   * aIsCancel - true if this popup is hiding due to being cancelled.
   * aLastPopup - optional popup to close last when hiding a chain of menus.
   *              If null, then all popups will be closed.
   */
  void HidePopup(nsIContent* aPopup,
                 bool aHideChain,
                 bool aDeselectMenu,
                 bool aAsynchronous,
                 bool aIsCancel,
                 nsIContent* aLastPopup = nullptr);

  /**
   * Hide the popup aFrame. This method is called by the view manager when the
   * close button is pressed.
   */
  void HidePopup(nsIFrame* aFrame);

  /**
   * 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);

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

  /**
   * Enable or disable the dynamic noautohide state of a panel.
   *
   * aPanel - the panel whose state is to change
   * aShouldRollup - whether the panel is no longer noautohide
   */
  void EnableRollup(nsIContent* aPopup, bool aShouldRollup);

  /**
   * 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
   */
  void ExecuteMenu(nsIContent* aMenu, nsXULMenuCommandEvent* aEvent);

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

  /**
   * Return true if the popup for the supplied menu parent is open.
   */
  bool IsPopupOpenForMenuParent(nsMenuParent* aMenuParent);

  /**
   * 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 ePopupTypeAny, a menu of any
   * type is returned, except for popups in the mNoHidePanels list.
   */
  nsIFrame* GetTopPopup(nsPopupType aType);

  /**
   * Return an array of all the open and visible popup frames for
   * menus, in order from top to bottom.
   */
  void GetVisiblePopups(nsTArray<nsIFrame *>& aPopups);

  /**
   * 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<nsIDOMNode> GetLastTriggerPopupNode(nsIDocument* aDocument)
  {
    return GetLastTriggerNode(aDocument, false);
  }

  already_AddRefed<nsIDOMNode> GetLastTriggerTooltipNode(nsIDocument* 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);

  /**
   * Indicate that the popup associated with aView has been moved to the
   * specified screen coordiates.
   */
  void PopupMoved(nsIFrame* aFrame, nsIntPoint aPoint);

  /**
   * Indicate that the popup associated with aView has been resized to the
   * given device pixel size aSize.
   */
  void PopupResized(nsIFrame* aFrame, mozilla::LayoutDeviceIntSize aSize);

  /**
   * 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.
   */
  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(nsIContent* 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(nsMenuParent* aMenuParent);

  /**
   * Handles navigation for menu accelkeys. Returns true if the key has
   * been handled. 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.
   */
  bool HandleShortcutNavigation(nsIDOMKeyEvent* aKeyEvent,
                                  nsMenuPopupFrame* aFrame);

  /**
   * Handles cursor navigation within a menu. Returns true if the key has
   * been handled.
   */
  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.
   */
  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.
   */
  bool HandleKeyboardEventWithKeyCode(nsIDOMKeyEvent* aKeyEvent,
                                      nsMenuChainItem* aTopVisibleMenuItem);

  nsresult KeyUp(nsIDOMKeyEvent* aKeyEvent);
  nsresult KeyDown(nsIDOMKeyEvent* aKeyEvent);
  nsresult KeyPress(nsIDOMKeyEvent* aKeyEvent);

protected:
  nsXULPopupManager();
  ~nsXULPopupManager();

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

  // return the topmost menu, skipping over invisible popups
  nsMenuChainItem* GetTopVisibleMenu();

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

  // set the event that was used to trigger the popup, or null to clear the
  // event details. aTriggerContent will be set to the target of the event.
  void InitTriggerEvent(nsIDOMEvent* aEvent, nsIContent* aPopup, nsIContent** aTriggerContent);

  // callbacks for ShowPopup and HidePopup as events may be done asynchronously
  void ShowPopupCallback(nsIContent* aPopup,
                         nsMenuPopupFrame* aPopupFrame,
                         bool aIsContextMenu,
                         bool aSelectFirstItem);
  void HidePopupCallback(nsIContent* aPopup,
                         nsMenuPopupFrame* aPopupFrame,
                         nsIContent* aNextPopup,
                         nsIContent* aLastPopup,
                         nsPopupType aPopupType,
                         bool aDeselectMenu);

  /**
   * Fire a popupshowing event on the popup and then open the popup.
   *
   * aPopup - the popup to open
   * aIsContextMenu - true for context menus
   * aSelectFirstItem - true to select the first item in the menu
   */
  void FirePopupShowingEvent(nsIContent* aPopup,
                             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. 
   * aDeselectMenu - true to unhighlight the menu when hiding it
   * aIsCancel - true if this popup is hiding due to being cancelled.
   */
  void FirePopupHidingEvent(nsIContent* aPopup,
                            nsIContent* aNextPopup,
                            nsIContent* aLastPopup,
                            nsPresContext *aPresContext,
                            nsPopupType aPopupType,
                            bool aDeselectMenu,
                            bool aIsCancel);

  /**
   * Handle keyboard navigation within a menu popup specified by aItem.
   */
  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.
   */
  bool HandleKeyboardNavigationInPopup(nsMenuChainItem* aItem,
                                         nsMenuPopupFrame* aFrame,
                                         nsNavigationDirection aDir);

protected:

  already_AddRefed<nsIDOMNode> GetLastTriggerNode(nsIDocument* aDocument, bool aIsTooltip);

  /**
   * 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.
   */
  void UpdateKeyboardListeners();

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

  // 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;

  // range parent and offset set in SetTriggerEvent
  nsCOMPtr<nsIDOMNode> mRangeParent;
  int32_t mRangeOffset;
  // Device pixels relative to the showing popup's presshell's
  // root prescontext's root frame.
  mozilla::LayoutDeviceIntPoint mCachedMousePoint;

  // cached modifiers
  mozilla::Modifiers mCachedModifiers;

  // set to the currently active menu bar, if any
  nsMenuBarFrame* mActiveMenuBar;

  // linked list of normal menus and panels.
  nsMenuChainItem* mPopups;

  // linked list of noautohide panels and tooltips.
  nsMenuChainItem* mNoHidePanels;

  // timer used for HidePopupAfterDelay
  nsCOMPtr<nsITimer> mCloseTimer;

  // a popup that is waiting on the timer
  nsMenuPopupFrame* mTimerMenu;

  // the popup that is currently being opened, stored only during the
  // popupshowing event
  nsCOMPtr<nsIContent> mOpeningPopup;

  // If true, all popups won't hide automatically on blur
  static bool sDevtoolsDisableAutoHide;
};

#endif
