Blob Blame History Raw
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "nsGkAtoms.h"
#include "nsHTMLParts.h"
#include "nsMenuFrame.h"
#include "nsBoxFrame.h"
#include "nsIContent.h"
#include "nsAtom.h"
#include "nsPresContext.h"
#include "nsIPresShell.h"
#include "nsStyleContext.h"
#include "nsCSSRendering.h"
#include "nsNameSpaceManager.h"
#include "nsMenuPopupFrame.h"
#include "nsMenuBarFrame.h"
#include "nsIDocument.h"
#include "nsIDOMElement.h"
#include "nsIComponentManager.h"
#include "nsBoxLayoutState.h"
#include "nsIScrollableFrame.h"
#include "nsBindingManager.h"
#include "nsIServiceManager.h"
#include "nsCSSFrameConstructor.h"
#include "nsString.h"
#include "nsReadableUtils.h"
#include "nsUnicharUtils.h"
#include "nsIStringBundle.h"
#include "nsContentUtils.h"
#include "nsDisplayList.h"
#include "nsIReflowCallback.h"
#include "nsISound.h"
#include "nsIDOMXULMenuListElement.h"
#include "mozilla/Attributes.h"
#include "mozilla/EventDispatcher.h"
#include "mozilla/EventStateManager.h"
#include "mozilla/Likely.h"
#include "mozilla/LookAndFeel.h"
#include "mozilla/MouseEvents.h"
#include "mozilla/Preferences.h"
#include "mozilla/Services.h"
#include "mozilla/TextEvents.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/Event.h"
#include <algorithm>

using namespace mozilla;

#define NS_MENU_POPUP_LIST_INDEX 0

#if defined(XP_WIN)
#define NSCONTEXTMENUISMOUSEUP 1
#endif

NS_DECLARE_FRAME_PROPERTY_FRAMELIST(PopupListProperty)

// This global flag indicates that a menu just opened or closed and is used
// to ignore the mousemove and mouseup events that would fire on the menu after
// the mousedown occurred.
static int32_t gMenuJustOpenedOrClosed = false;

const int32_t kBlinkDelay = 67;  // milliseconds

// this class is used for dispatching menu activation events asynchronously.
class nsMenuActivateEvent : public Runnable {
 public:
  nsMenuActivateEvent(Element* aMenu, nsPresContext* aPresContext,
                      bool aIsActivate)
      : mozilla::Runnable("nsMenuActivateEvent"),
        mMenu(aMenu),
        mPresContext(aPresContext),
        mIsActivate(aIsActivate) {}

  NS_IMETHOD Run() override {
    nsAutoString domEventToFire;

    if (mIsActivate) {
      // Highlight the menu.
      mMenu->SetAttr(kNameSpaceID_None, nsGkAtoms::menuactive,
                     NS_LITERAL_STRING("true"), true);
      // The menuactivated event is used by accessibility to track the user's
      // movements through menus
      domEventToFire.AssignLiteral("DOMMenuItemActive");
    } else {
      // Unhighlight the menu.
      mMenu->UnsetAttr(kNameSpaceID_None, nsGkAtoms::menuactive, true);
      domEventToFire.AssignLiteral("DOMMenuItemInactive");
    }

    RefPtr<Event> event = NS_NewDOMEvent(mMenu, mPresContext, nullptr);
    event->InitEvent(domEventToFire, true, true);

    event->SetTrusted(true);

    EventDispatcher::DispatchDOMEvent(mMenu, nullptr, event, mPresContext,
                                      nullptr);

    return NS_OK;
  }

 private:
  RefPtr<Element> mMenu;
  RefPtr<nsPresContext> mPresContext;
  bool mIsActivate;
};

class nsMenuAttributeChangedEvent : public Runnable {
 public:
  nsMenuAttributeChangedEvent(nsIFrame* aFrame, nsAtom* aAttr)
      : mozilla::Runnable("nsMenuAttributeChangedEvent"),
        mFrame(aFrame),
        mAttr(aAttr) {}

  NS_IMETHOD Run() override {
    nsMenuFrame* frame = static_cast<nsMenuFrame*>(mFrame.GetFrame());
    NS_ENSURE_STATE(frame);
    if (mAttr == nsGkAtoms::checked) {
      frame->UpdateMenuSpecialState();
    } else if (mAttr == nsGkAtoms::acceltext) {
      // someone reset the accelText attribute,
      // so clear the bit that says *we* set it
      frame->RemoveStateBits(NS_STATE_ACCELTEXT_IS_DERIVED);
      frame->BuildAcceleratorText(true);
    } else if (mAttr == nsGkAtoms::key) {
      frame->BuildAcceleratorText(true);
    } else if (mAttr == nsGkAtoms::type || mAttr == nsGkAtoms::name) {
      frame->UpdateMenuType();
    }
    return NS_OK;
  }

 protected:
  WeakFrame mFrame;
  RefPtr<nsAtom> mAttr;
};

//
// NS_NewMenuFrame and NS_NewMenuItemFrame
//
// Wrappers for creating a new menu popup container
//
nsIFrame* NS_NewMenuFrame(nsIPresShell* aPresShell, nsStyleContext* aContext) {
  nsMenuFrame* it = new (aPresShell) nsMenuFrame(aContext);
  it->SetIsMenu(true);
  return it;
}

nsIFrame* NS_NewMenuItemFrame(nsIPresShell* aPresShell,
                              nsStyleContext* aContext) {
  nsMenuFrame* it = new (aPresShell) nsMenuFrame(aContext);
  it->SetIsMenu(false);
  return it;
}

NS_IMPL_FRAMEARENA_HELPERS(nsMenuFrame)

NS_QUERYFRAME_HEAD(nsMenuFrame)
NS_QUERYFRAME_ENTRY(nsMenuFrame)
NS_QUERYFRAME_TAIL_INHERITING(nsBoxFrame)

nsMenuFrame::nsMenuFrame(nsStyleContext* aContext)
    : nsBoxFrame(aContext, kClassID),
      mIsMenu(false),
      mChecked(false),
      mIgnoreAccelTextChange(false),
      mReflowCallbackPosted(false),
      mType(eMenuType_Normal),
      mBlinkState(0) {}

nsMenuParent* nsMenuFrame::GetMenuParent() const {
  nsContainerFrame* parent = GetParent();
  for (; parent; parent = parent->GetParent()) {
    nsMenuPopupFrame* popup = do_QueryFrame(parent);
    if (popup) {
      return popup;
    }
    nsMenuBarFrame* menubar = do_QueryFrame(parent);
    if (menubar) {
      return menubar;
    }
  }
  return nullptr;
}

bool nsMenuFrame::ReflowFinished() {
  mReflowCallbackPosted = false;

  UpdateMenuType();
  return true;
}

void nsMenuFrame::ReflowCallbackCanceled() { mReflowCallbackPosted = false; }

void nsMenuFrame::Init(nsIContent* aContent, nsContainerFrame* aParent,
                       nsIFrame* aPrevInFlow) {
  nsBoxFrame::Init(aContent, aParent, aPrevInFlow);

  // Set up a mediator which can be used for callbacks on this frame.
  mTimerMediator = new nsMenuTimerMediator(this);

  BuildAcceleratorText(false);
  if (!mReflowCallbackPosted) {
    mReflowCallbackPosted = true;
    PresShell()->PostReflowCallback(this);
  }
}

const nsFrameList& nsMenuFrame::GetChildList(ChildListID aListID) const {
  if (kPopupList == aListID) {
    nsFrameList* list = GetPopupList();
    return list ? *list : nsFrameList::EmptyList();
  }
  return nsBoxFrame::GetChildList(aListID);
}

void nsMenuFrame::GetChildLists(nsTArray<ChildList>* aLists) const {
  nsBoxFrame::GetChildLists(aLists);
  nsFrameList* list = GetPopupList();
  if (list) {
    list->AppendIfNonempty(aLists, kPopupList);
  }
}

nsMenuPopupFrame* nsMenuFrame::GetPopup() {
  nsFrameList* popupList = GetPopupList();
  return popupList ? static_cast<nsMenuPopupFrame*>(popupList->FirstChild())
                   : nullptr;
}

nsFrameList* nsMenuFrame::GetPopupList() const {
  if (!HasPopup()) {
    return nullptr;
  }
  nsFrameList* prop = GetProperty(PopupListProperty());
  NS_ASSERTION(
      prop && prop->GetLength() == 1 && prop->FirstChild()->IsMenuPopupFrame(),
      "popup list should have exactly one nsMenuPopupFrame");
  return prop;
}

void nsMenuFrame::DestroyPopupList() {
  NS_ASSERTION(HasPopup(), "huh?");
  nsFrameList* prop = RemoveProperty(PopupListProperty());
  NS_ASSERTION(prop && prop->IsEmpty(),
               "popup list must exist and be empty when destroying");
  RemoveStateBits(NS_STATE_MENU_HAS_POPUP_LIST);
  prop->Delete(PresShell());
}

void nsMenuFrame::SetPopupFrame(nsFrameList& aFrameList) {
  for (nsFrameList::Enumerator e(aFrameList); !e.AtEnd(); e.Next()) {
    nsMenuPopupFrame* popupFrame = do_QueryFrame(e.get());
    if (popupFrame) {
      // Remove the frame from the list and store it in a nsFrameList* property.
      aFrameList.RemoveFrame(popupFrame);
      nsFrameList* popupList =
          new (PresShell()) nsFrameList(popupFrame, popupFrame);
      SetProperty(PopupListProperty(), popupList);
      AddStateBits(NS_STATE_MENU_HAS_POPUP_LIST);
      break;
    }
  }
}

void nsMenuFrame::SetInitialChildList(ChildListID aListID,
                                      nsFrameList& aChildList) {
  if (aListID == kPrincipalList || aListID == kPopupList) {
    NS_ASSERTION(!HasPopup(), "SetInitialChildList called twice?");
    SetPopupFrame(aChildList);
  }
  nsBoxFrame::SetInitialChildList(aListID, aChildList);
}

void nsMenuFrame::DestroyFrom(nsIFrame* aDestructRoot,
                              PostDestroyData& aPostDestroyData) {
  if (mReflowCallbackPosted) {
    PresShell()->CancelReflowCallback(this);
    mReflowCallbackPosted = false;
  }

  // Kill our timer if one is active. This is not strictly necessary as
  // the pointer to this frame will be cleared from the mediator, but
  // this is done for added safety.
  if (mOpenTimer) {
    mOpenTimer->Cancel();
  }

  StopBlinking();

  // Null out the pointer to this frame in the mediator wrapper so that it
  // doesn't try to interact with a deallocated frame.
  mTimerMediator->ClearFrame();

  // if the menu content is just being hidden, it may be made visible again
  // later, so make sure to clear the highlighting.
  mContent->AsElement()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::menuactive,
                                   false);

  // are we our menu parent's current menu item?
  nsMenuParent* menuParent = GetMenuParent();
  if (menuParent && menuParent->GetCurrentMenuItem() == this) {
    // yes; tell it that we're going away
    menuParent->CurrentMenuIsBeingDestroyed();
  }

  nsFrameList* popupList = GetPopupList();
  if (popupList) {
    popupList->DestroyFramesFrom(aDestructRoot, aPostDestroyData);
    DestroyPopupList();
  }

  nsBoxFrame::DestroyFrom(aDestructRoot, aPostDestroyData);
}

void nsMenuFrame::BuildDisplayListForChildren(nsDisplayListBuilder* aBuilder,
                                              const nsDisplayListSet& aLists) {
  if (!aBuilder->IsForEventDelivery()) {
    nsBoxFrame::BuildDisplayListForChildren(aBuilder, aLists);
    return;
  }

  nsDisplayListCollection set(aBuilder);
  nsBoxFrame::BuildDisplayListForChildren(aBuilder, set);

  WrapListsInRedirector(aBuilder, set, aLists);
}

nsresult nsMenuFrame::HandleEvent(nsPresContext* aPresContext,
                                  WidgetGUIEvent* aEvent,
                                  nsEventStatus* aEventStatus) {
  NS_ENSURE_ARG_POINTER(aEventStatus);
  if (nsEventStatus_eConsumeNoDefault == *aEventStatus) {
    return NS_OK;
  }
  nsMenuParent* menuParent = GetMenuParent();
  if (menuParent && menuParent->IsMenuLocked()) {
    return NS_OK;
  }

  AutoWeakFrame weakFrame(this);
  if (*aEventStatus == nsEventStatus_eIgnore)
    *aEventStatus = nsEventStatus_eConsumeDoDefault;

  // If a menu just opened, ignore the mouseup event that might occur after a
  // the mousedown event that opened it. However, if a different mousedown
  // event occurs, just clear this flag.
  if (gMenuJustOpenedOrClosed) {
    if (aEvent->mMessage == eMouseDown) {
      gMenuJustOpenedOrClosed = false;
    } else if (aEvent->mMessage == eMouseUp) {
      return NS_OK;
    }
  }

  bool onmenu = IsOnMenu();

  if (aEvent->mMessage == eKeyPress && !IsDisabled()) {
    WidgetKeyboardEvent* keyEvent = aEvent->AsKeyboardEvent();
    uint32_t keyCode = keyEvent->mKeyCode;
#ifdef XP_MACOSX
    // On mac, open menulist on either up/down arrow or space (w/o Cmd pressed)
    if (!IsOpen() && ((keyEvent->mCharCode == ' ' && !keyEvent->IsMeta()) ||
                      (keyCode == NS_VK_UP || keyCode == NS_VK_DOWN))) {
      // When pressing space, don't open the menu if performing an incremental
      // search.
      if (keyEvent->mCharCode != ' ' ||
          !nsMenuPopupFrame::IsWithinIncrementalTime(keyEvent->mTime)) {
        *aEventStatus = nsEventStatus_eConsumeNoDefault;
        OpenMenu(false);
      }
    }
#else
    // On other platforms, toggle menulist on unmodified F4 or Alt arrow
    if ((keyCode == NS_VK_F4 && !keyEvent->IsAlt()) ||
        ((keyCode == NS_VK_UP || keyCode == NS_VK_DOWN) && keyEvent->IsAlt())) {
      *aEventStatus = nsEventStatus_eConsumeNoDefault;
      ToggleMenuState();
    }
#endif
  } else if (aEvent->mMessage == eMouseDown &&
             aEvent->AsMouseEvent()->button == WidgetMouseEvent::eLeftButton &&
             !IsDisabled() && IsMenu()) {
    // The menu item was selected. Bring up the menu.
    // We have children.
    // Don't prevent the default action here, since that will also cancel
    // potential drag starts.
    if (!menuParent || menuParent->IsMenuBar()) {
      ToggleMenuState();
    } else {
      if (!IsOpen()) {
        menuParent->ChangeMenuItem(this, false, false);
        OpenMenu(false);
      }
    }
  } else if (
#ifndef NSCONTEXTMENUISMOUSEUP
      (aEvent->mMessage == eMouseUp &&
       aEvent->AsMouseEvent()->button == WidgetMouseEvent::eRightButton) &&
#else
      aEvent->mMessage == eContextMenu &&
#endif
      onmenu && !IsMenu() && !IsDisabled()) {
    // if this menu is a context menu it accepts right-clicks...fire away!
    // Make sure we cancel default processing of the context menu event so
    // that it doesn't bubble and get seen again by the popuplistener and show
    // another context menu.
    //
    // Furthermore (there's always more, isn't there?), on some platforms (win32
    // being one of them) we get the context menu event on a mouse up while
    // on others we get it on a mouse down. For the ones where we get it on a
    // mouse down, we must continue listening for the right button up event to
    // dismiss the menu.
    if (menuParent->IsContextMenu()) {
      *aEventStatus = nsEventStatus_eConsumeNoDefault;
      Execute(aEvent);
    }
  } else if (aEvent->mMessage == eMouseUp &&
             aEvent->AsMouseEvent()->button == WidgetMouseEvent::eLeftButton &&
             !IsMenu() && !IsDisabled()) {
    // Execute the execute event handler.
    *aEventStatus = nsEventStatus_eConsumeNoDefault;
    Execute(aEvent);
  } else if (aEvent->mMessage == eMouseOut) {
    // Kill our timer if one is active.
    if (mOpenTimer) {
      mOpenTimer->Cancel();
      mOpenTimer = nullptr;
    }

    // Deactivate the menu.
    if (menuParent) {
      bool onmenubar = menuParent->IsMenuBar();
      if (!(onmenubar && menuParent->IsActive())) {
        if (IsMenu() && !onmenubar && IsOpen()) {
          // Submenus don't get closed up immediately.
        } else if (this == menuParent->GetCurrentMenuItem()
#ifdef XP_WIN
                   && GetParentMenuListType() == eNotMenuList
#endif
        ) {
          menuParent->ChangeMenuItem(nullptr, false, false);
        }
      }
    }
  } else if (aEvent->mMessage == eMouseMove &&
             (onmenu || (menuParent && menuParent->IsMenuBar()))) {
    if (gMenuJustOpenedOrClosed) {
      gMenuJustOpenedOrClosed = false;
      return NS_OK;
    }

    if (IsDisabled() && GetParentMenuListType() != eNotMenuList) {
      return NS_OK;
    }

    // Let the menu parent know we're the new item.
    menuParent->ChangeMenuItem(this, false, false);
    NS_ENSURE_TRUE(weakFrame.IsAlive(), NS_OK);
    NS_ENSURE_TRUE(menuParent, NS_OK);

    // we need to check if we really became the current menu
    // item or not
    nsMenuFrame* realCurrentItem = menuParent->GetCurrentMenuItem();
    if (realCurrentItem != this) {
      // we didn't (presumably because a context menu was active)
      return NS_OK;
    }

    // Hovering over a menu in a popup should open it without a need for a
    // click. A timer is used so that it doesn't open if the user moves the
    // mouse quickly past the menu. This conditional check ensures that only
    // menus have this behaviour
    if (!IsDisabled() && IsMenu() && !IsOpen() && !mOpenTimer &&
        !menuParent->IsMenuBar()) {
      int32_t menuDelay =
          LookAndFeel::GetInt(LookAndFeel::eIntID_SubmenuDelay, 300);  // ms

      // We're a menu, we're built, we're closed, and no timer has been kicked
      // off.
      NS_NewTimerWithCallback(
          getter_AddRefs(mOpenTimer), mTimerMediator, menuDelay,
          nsITimer::TYPE_ONE_SHOT,
          mContent->OwnerDoc()->EventTargetFor(TaskCategory::Other));
    }
  }

  return NS_OK;
}

void nsMenuFrame::ToggleMenuState() {
  if (IsOpen())
    CloseMenu(false);
  else
    OpenMenu(false);
}

void nsMenuFrame::PopupOpened() {
  gMenuJustOpenedOrClosed = true;

  AutoWeakFrame weakFrame(this);
  mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::open,
                                 NS_LITERAL_STRING("true"), true);
  if (!weakFrame.IsAlive()) return;

  nsMenuParent* menuParent = GetMenuParent();
  if (menuParent) {
    menuParent->SetActive(true);
    // Make sure the current menu which is being toggled on
    // the menubar is highlighted
    menuParent->SetCurrentMenuItem(this);
  }
}

void nsMenuFrame::PopupClosed(bool aDeselectMenu) {
  AutoWeakFrame weakFrame(this);
  nsContentUtils::AddScriptRunner(
      new nsUnsetAttrRunnable(mContent->AsElement(), nsGkAtoms::open));
  if (!weakFrame.IsAlive()) return;

  // if the popup is for a menu on a menubar, inform menubar to deactivate
  nsMenuParent* menuParent = GetMenuParent();
  if (menuParent && menuParent->MenuClosed()) {
    if (aDeselectMenu) {
      SelectMenu(false);
    } else {
      // We are not deselecting the parent menu while closing the popup, so send
      // a DOMMenuItemActive event to the menu to indicate that the menu is
      // becoming active again.
      nsMenuFrame* current = menuParent->GetCurrentMenuItem();
      if (current) {
        // However, if the menu is a descendant on a menubar, and the menubar
        // has the 'stay active' flag set, it means that the menubar is
        // switching to another toplevel menu entirely (for example from Edit to
        // View), so don't fire the DOMMenuItemActive event or else we'll send
        // extraneous events for submenus. nsMenuBarFrame::ChangeMenuItem has
        // already deselected the old menu, so it doesn't need to happen again
        // here, and the new menu can be selected right away.
        nsIFrame* parent = current;
        while (parent) {
          nsMenuBarFrame* menubar = do_QueryFrame(parent);
          if (menubar && menubar->GetStayActive()) return;

          parent = parent->GetParent();
        }

        nsCOMPtr<nsIRunnable> event = new nsMenuActivateEvent(
            current->GetContent()->AsElement(), PresContext(), true);
        mContent->OwnerDoc()->Dispatch(TaskCategory::Other, event.forget());
      }
    }
  }
}

NS_IMETHODIMP
nsMenuFrame::SelectMenu(bool aActivateFlag) {
  if (mContent) {
    // When a menu opens a submenu, the mouse will often be moved onto a
    // sibling before moving onto an item within the submenu, causing the
    // parent to become deselected. We need to ensure that the parent menu
    // is reselected when an item in the submenu is selected, so navigate up
    // from the item to its popup, and then to the popup above that.
    if (aActivateFlag) {
      nsIFrame* parent = GetParent();
      while (parent) {
        nsMenuPopupFrame* menupopup = do_QueryFrame(parent);
        if (menupopup) {
          // a menu is always the direct parent of a menupopup
          nsMenuFrame* menu = do_QueryFrame(menupopup->GetParent());
          if (menu) {
            // a popup however is not necessarily the direct parent of a menu
            nsIFrame* popupParent = menu->GetParent();
            while (popupParent) {
              menupopup = do_QueryFrame(popupParent);
              if (menupopup) {
                menupopup->SetCurrentMenuItem(menu);
                break;
              }
              popupParent = popupParent->GetParent();
            }
          }
          break;
        }
        parent = parent->GetParent();
      }
    }

    // cancel the close timer if selecting a menu within the popup to be closed
    nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
    if (pm) {
      nsMenuParent* menuParent = GetMenuParent();
      pm->CancelMenuTimer(menuParent);
    }

    nsCOMPtr<nsIRunnable> event = new nsMenuActivateEvent(
        mContent->AsElement(), PresContext(), aActivateFlag);
    mContent->OwnerDoc()->Dispatch(TaskCategory::Other, event.forget());
  }

  return NS_OK;
}

nsresult nsMenuFrame::AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
                                       int32_t aModType) {
  if (aAttribute == nsGkAtoms::acceltext && mIgnoreAccelTextChange) {
    // Reset the flag so that only one change is ignored.
    mIgnoreAccelTextChange = false;
    return NS_OK;
  }

  if (aAttribute == nsGkAtoms::checked || aAttribute == nsGkAtoms::acceltext ||
      aAttribute == nsGkAtoms::key || aAttribute == nsGkAtoms::type ||
      aAttribute == nsGkAtoms::name) {
    nsCOMPtr<nsIRunnable> event =
        new nsMenuAttributeChangedEvent(this, aAttribute);
    nsContentUtils::AddScriptRunner(event);
  }
  return NS_OK;
}

nsIContent* nsMenuFrame::GetAnchor() {
  mozilla::dom::Element* anchor = nullptr;

  nsAutoString id;
  mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::anchor, id);
  if (!id.IsEmpty()) {
    nsIDocument* doc = mContent->OwnerDoc();

    anchor =
        doc->GetAnonymousElementByAttribute(mContent, nsGkAtoms::anonid, id);
    if (!anchor) {
      anchor = doc->GetElementById(id);
    }
  }

  // Always return the menu's content if the anchor wasn't set or wasn't found.
  return anchor && anchor->GetPrimaryFrame() ? anchor : GetContent();
}

void nsMenuFrame::OpenMenu(bool aSelectFirstItem) {
  if (!mContent) return;

  nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
  if (pm) {
    pm->KillMenuTimer();
    // This opens the menu asynchronously
    pm->ShowMenu(mContent, aSelectFirstItem, true);
  }
}

void nsMenuFrame::CloseMenu(bool aDeselectMenu) {
  gMenuJustOpenedOrClosed = true;

  // Close the menu asynchronously
  nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
  if (pm && HasPopup())
    pm->HidePopup(GetPopup()->GetContent(), false, aDeselectMenu, true, false);
}

bool nsMenuFrame::IsSizedToPopup(nsIContent* aContent, bool aRequireAlways) {
  MOZ_ASSERT(aContent->IsElement());
  nsAutoString sizedToPopup;
  aContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::sizetopopup,
                                 sizedToPopup);
  return sizedToPopup.EqualsLiteral("always") ||
         (!aRequireAlways && sizedToPopup.EqualsLiteral("pref"));
}

nsSize nsMenuFrame::GetXULMinSize(nsBoxLayoutState& aBoxLayoutState) {
  nsSize size = nsBoxFrame::GetXULMinSize(aBoxLayoutState);
  DISPLAY_MIN_SIZE(this, size);

  if (IsSizedToPopup(mContent, true)) SizeToPopup(aBoxLayoutState, size);

  return size;
}

NS_IMETHODIMP
nsMenuFrame::DoXULLayout(nsBoxLayoutState& aState) {
  // lay us out
  nsresult rv = nsBoxFrame::DoXULLayout(aState);

  nsMenuPopupFrame* popupFrame = GetPopup();
  if (popupFrame) {
    bool sizeToPopup = IsSizedToPopup(mContent, false);
    popupFrame->LayoutPopup(aState, this, GetAnchor()->GetPrimaryFrame(),
                            sizeToPopup);
  }

  return rv;
}

#ifdef DEBUG_LAYOUT
nsresult nsMenuFrame::SetXULDebug(nsBoxLayoutState& aState, bool aDebug) {
  // see if our state matches the given debug state
  bool debugSet = mState & NS_STATE_CURRENTLY_IN_DEBUG;
  bool debugChanged = (!aDebug && debugSet) || (aDebug && !debugSet);

  // if it doesn't then tell each child below us the new debug state
  if (debugChanged) {
    nsBoxFrame::SetXULDebug(aState, aDebug);
    nsMenuPopupFrame* popupFrame = GetPopup();
    if (popupFrame) SetXULDebug(aState, popupFrame, aDebug);
  }

  return NS_OK;
}

nsresult nsMenuFrame::SetXULDebug(nsBoxLayoutState& aState, nsIFrame* aList,
                                  bool aDebug) {
  if (!aList) return NS_OK;

  while (aList) {
    if (aList->IsXULBoxFrame()) aList->SetXULDebug(aState, aDebug);

    aList = aList->GetNextSibling();
  }

  return NS_OK;
}
#endif

//
// Enter
//
// Called when the user hits the <Enter>/<Return> keys or presses the
// shortcut key. If this is a leaf item, the item's action will be executed.
// In either case, do nothing if the item is disabled.
//
nsMenuFrame* nsMenuFrame::Enter(WidgetGUIEvent* aEvent) {
  if (IsDisabled()) {
#ifdef XP_WIN
    // behavior on Windows - close the popup chain
    nsMenuParent* menuParent = GetMenuParent();
    if (menuParent) {
      nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
      if (pm) {
        nsIFrame* popup = pm->GetTopPopup(ePopupTypeAny);
        if (popup) pm->HidePopup(popup->GetContent(), true, true, true, false);
      }
    }
#endif  // #ifdef XP_WIN
    // this menu item was disabled - exit
    return nullptr;
  }

  if (!IsOpen()) {
    // The enter key press applies to us.
    nsMenuParent* menuParent = GetMenuParent();
    if (!IsMenu() && menuParent)
      Execute(aEvent);  // Execute our event handler
    else
      return this;
  }

  return nullptr;
}

bool nsMenuFrame::IsOpen() {
  nsMenuPopupFrame* popupFrame = GetPopup();
  return popupFrame && popupFrame->IsOpen();
}

bool nsMenuFrame::IsMenu() { return mIsMenu; }

nsMenuListType nsMenuFrame::GetParentMenuListType() {
  nsMenuParent* menuParent = GetMenuParent();
  if (menuParent && menuParent->IsMenu()) {
    nsMenuPopupFrame* popupFrame = static_cast<nsMenuPopupFrame*>(menuParent);
    nsIFrame* parentMenu = popupFrame->GetParent();
    if (parentMenu) {
      nsCOMPtr<nsIDOMXULMenuListElement> menulist =
          do_QueryInterface(parentMenu->GetContent());
      if (menulist) {
        bool isEditable = false;
        menulist->GetEditable(&isEditable);
        return isEditable ? eEditableMenuList : eReadonlyMenuList;
      }
    }
  }
  return eNotMenuList;
}

nsresult nsMenuFrame::Notify(nsITimer* aTimer) {
  // Our timer has fired.
  if (aTimer == mOpenTimer.get()) {
    mOpenTimer = nullptr;

    nsMenuParent* menuParent = GetMenuParent();
    if (!IsOpen() && menuParent) {
      // make sure we didn't open a context menu in the meantime
      // (i.e. the user right-clicked while hovering over a submenu).
      nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
      if (pm) {
        if ((!pm->HasContextMenu(nullptr) || menuParent->IsContextMenu()) &&
            mContent->AsElement()->AttrValueIs(
                kNameSpaceID_None, nsGkAtoms::menuactive, nsGkAtoms::_true,
                eCaseMatters)) {
          OpenMenu(false);
        }
      }
    }
  } else if (aTimer == mBlinkTimer) {
    switch (mBlinkState++) {
      case 0:
        NS_ASSERTION(false, "Blink timer fired while not blinking");
        StopBlinking();
        break;
      case 1: {
        // Turn the highlight back on and wait for a while before closing the
        // menu.
        AutoWeakFrame weakFrame(this);
        mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::menuactive,
                                       NS_LITERAL_STRING("true"), true);
        if (weakFrame.IsAlive()) {
          aTimer->InitWithCallback(mTimerMediator, kBlinkDelay,
                                   nsITimer::TYPE_ONE_SHOT);
        }
      } break;
      default: {
        nsMenuParent* menuParent = GetMenuParent();
        if (menuParent) {
          menuParent->LockMenuUntilClosed(false);
        }
        PassMenuCommandEventToPopupManager();
        StopBlinking();
        break;
      }
    }
  }

  return NS_OK;
}

bool nsMenuFrame::IsDisabled() {
  return mContent->AsElement()->AttrValueIs(
      kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters);
}

void nsMenuFrame::UpdateMenuType() {
  static Element::AttrValuesArray strings[] = {&nsGkAtoms::checkbox,
                                               &nsGkAtoms::radio, nullptr};
  switch (mContent->AsElement()->FindAttrValueIn(
      kNameSpaceID_None, nsGkAtoms::type, strings, eCaseMatters)) {
    case 0:
      mType = eMenuType_Checkbox;
      break;
    case 1:
      mType = eMenuType_Radio;
      mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::name,
                                     mGroupName);
      break;

    default:
      if (mType != eMenuType_Normal) {
        AutoWeakFrame weakFrame(this);
        mContent->AsElement()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::checked,
                                         true);
        ENSURE_TRUE(weakFrame.IsAlive());
      }
      mType = eMenuType_Normal;
      break;
  }
  UpdateMenuSpecialState();
}

/* update checked-ness for type="checkbox" and type="radio" */
void nsMenuFrame::UpdateMenuSpecialState() {
  bool newChecked = mContent->AsElement()->AttrValueIs(
      kNameSpaceID_None, nsGkAtoms::checked, nsGkAtoms::_true, eCaseMatters);
  if (newChecked == mChecked) {
    /* checked state didn't change */

    if (mType != eMenuType_Radio)
      return;  // only Radio possibly cares about other kinds of change

    if (!mChecked || mGroupName.IsEmpty()) return;  // no interesting change
  } else {
    mChecked = newChecked;
    if (mType != eMenuType_Radio || !mChecked)
      /*
       * Unchecking something requires no further changes, and only
       * menuRadio has to do additional work when checked.
       */
      return;
  }

  /*
   * If we get this far, we're type=radio, and:
   * - our name= changed, or
   * - we went from checked="false" to checked="true"
   */

  /*
   * Behavioural note:
   * If we're checked and renamed _into_ an existing radio group, we are
   * made the new checked item, and we unselect the previous one.
   *
   * The only other reasonable behaviour would be to check for another selected
   * item in that group.  If found, unselect ourselves, otherwise we're the
   * selected item.  That, however, would be a lot more work, and I don't think
   * it's better at all.
   */

  /* walk siblings, looking for the other checked item with the same name */
  // get the first sibling in this menu popup. This frame may be it, and if
  // we're being called at creation time, this frame isn't yet in the parent's
  // child list. All I'm saying is that this may fail, but it's most likely
  // alright.
  nsIFrame* firstMenuItem =
      nsXULPopupManager::GetNextMenuItem(GetParent(), nullptr, true, false);
  nsIFrame* sib = firstMenuItem;
  while (sib) {
    nsMenuFrame* menu = do_QueryFrame(sib);
    if (sib != this) {
      if (menu && menu->GetMenuType() == eMenuType_Radio && menu->IsChecked() &&
          menu->GetRadioGroupName() == mGroupName) {
        /* uncheck the old item */
        sib->GetContent()->AsElement()->UnsetAttr(kNameSpaceID_None,
                                                  nsGkAtoms::checked, true);
        /* XXX in DEBUG, check to make sure that there aren't two checked items
         */
        return;
      }
    }
    sib = nsXULPopupManager::GetNextMenuItem(GetParent(), menu, true, true);
    if (sib == firstMenuItem) {
      break;
    }
  }
}

void nsMenuFrame::BuildAcceleratorText(bool aNotify) {
  nsAutoString accelText;

  if ((GetStateBits() & NS_STATE_ACCELTEXT_IS_DERIVED) == 0) {
    mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::acceltext,
                                   accelText);
    if (!accelText.IsEmpty()) return;
  }
  // accelText is definitely empty here.

  // Now we're going to compute the accelerator text, so remember that we did.
  AddStateBits(NS_STATE_ACCELTEXT_IS_DERIVED);

  // If anything below fails, just leave the accelerator text blank.
  AutoWeakFrame weakFrame(this);
  mContent->AsElement()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::acceltext,
                                   aNotify);
  ENSURE_TRUE(weakFrame.IsAlive());

  // See if we have a key node and use that instead.
  nsAutoString keyValue;
  mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::key, keyValue);
  if (keyValue.IsEmpty()) return;

  // Turn the document into a DOM document so we can use getElementById
  nsIDocument* document = mContent->GetUncomposedDoc();
  if (!document) return;

  // XXXsmaug If mContent is in shadow dom, should we use
  //         ShadowRoot::GetElementById()?
  Element* keyElement = document->GetElementById(keyValue);
  if (!keyElement) {
#ifdef DEBUG
    nsAutoString label;
    mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::label, label);
    nsAutoString msg = NS_LITERAL_STRING("Key '") + keyValue +
                       NS_LITERAL_STRING("' of menu item '") + label +
                       NS_LITERAL_STRING("' could not be found");
    NS_WARNING(NS_ConvertUTF16toUTF8(msg).get());
#endif
    return;
  }

  // get the string to display as accelerator text
  // check the key element's attributes in this order:
  // |keytext|, |key|, |keycode|
  nsAutoString accelString;
  keyElement->GetAttr(kNameSpaceID_None, nsGkAtoms::keytext, accelString);

  if (accelString.IsEmpty()) {
    keyElement->GetAttr(kNameSpaceID_None, nsGkAtoms::key, accelString);

    if (!accelString.IsEmpty()) {
      ToUpperCase(accelString);
    } else {
      nsAutoString keyCode;
      keyElement->GetAttr(kNameSpaceID_None, nsGkAtoms::keycode, keyCode);
      ToUpperCase(keyCode);

      nsresult rv;
      nsCOMPtr<nsIStringBundleService> bundleService =
          mozilla::services::GetStringBundleService();
      if (bundleService) {
        nsCOMPtr<nsIStringBundle> bundle;
        rv = bundleService->CreateBundle(
            keyCode.EqualsLiteral("VK_RETURN")
                ? "chrome://global-platform/locale/platformKeys.properties"
                : "chrome://global/locale/keys.properties",
            getter_AddRefs(bundle));
        if (NS_SUCCEEDED(rv) && bundle) {
          nsAutoString keyName;
          rv = bundle->GetStringFromName(NS_ConvertUTF16toUTF8(keyCode).get(),
                                         keyName);
          if (NS_SUCCEEDED(rv)) {
            accelString = keyName;
          }
        }
      }

      // nothing usable found, bail
      if (accelString.IsEmpty()) return;
    }
  }

  nsAutoString modifiers;
  keyElement->GetAttr(kNameSpaceID_None, nsGkAtoms::modifiers, modifiers);

  char* str = ToNewCString(modifiers);
  char* newStr;
  char* token = nsCRT::strtok(str, ", \t", &newStr);

  nsAutoString shiftText;
  nsAutoString altText;
  nsAutoString metaText;
  nsAutoString controlText;
  nsAutoString osText;
  nsAutoString modifierSeparator;

  nsContentUtils::GetShiftText(shiftText);
  nsContentUtils::GetAltText(altText);
  nsContentUtils::GetMetaText(metaText);
  nsContentUtils::GetControlText(controlText);
  nsContentUtils::GetOSText(osText);
  nsContentUtils::GetModifierSeparatorText(modifierSeparator);

  while (token) {
    if (PL_strcmp(token, "shift") == 0)
      accelText += shiftText;
    else if (PL_strcmp(token, "alt") == 0)
      accelText += altText;
    else if (PL_strcmp(token, "meta") == 0)
      accelText += metaText;
    else if (PL_strcmp(token, "os") == 0)
      accelText += osText;
    else if (PL_strcmp(token, "control") == 0)
      accelText += controlText;
    else if (PL_strcmp(token, "accel") == 0) {
      switch (WidgetInputEvent::AccelModifier()) {
        case MODIFIER_META:
          accelText += metaText;
          break;
        case MODIFIER_OS:
          accelText += osText;
          break;
        case MODIFIER_ALT:
          accelText += altText;
          break;
        case MODIFIER_CONTROL:
          accelText += controlText;
          break;
        default:
          MOZ_CRASH(
              "Handle the new result of WidgetInputEvent::AccelModifier()");
          break;
      }
    }

    accelText += modifierSeparator;

    token = nsCRT::strtok(newStr, ", \t", &newStr);
  }

  free(str);

  accelText += accelString;

  mIgnoreAccelTextChange = true;
  mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::acceltext,
                                 accelText, aNotify);
  ENSURE_TRUE(weakFrame.IsAlive());

  mIgnoreAccelTextChange = false;
}

void nsMenuFrame::Execute(WidgetGUIEvent* aEvent) {
  // flip "checked" state if we're a checkbox menu, or an un-checked radio menu
  bool needToFlipChecked = false;
  if (mType == eMenuType_Checkbox || (mType == eMenuType_Radio && !mChecked)) {
    needToFlipChecked = !mContent->AsElement()->AttrValueIs(
        kNameSpaceID_None, nsGkAtoms::autocheck, nsGkAtoms::_false,
        eCaseMatters);
  }

  nsCOMPtr<nsISound> sound(do_CreateInstance("@mozilla.org/sound;1"));
  if (sound) sound->PlayEventSound(nsISound::EVENT_MENU_EXECUTE);

  StartBlinking(aEvent, needToFlipChecked);
}

bool nsMenuFrame::ShouldBlink() {
  int32_t shouldBlink =
      LookAndFeel::GetInt(LookAndFeel::eIntID_ChosenMenuItemsShouldBlink, 0);
  if (!shouldBlink) return false;

  // Don't blink in editable menulists.
  if (GetParentMenuListType() == eEditableMenuList) return false;

  return true;
}

void nsMenuFrame::StartBlinking(WidgetGUIEvent* aEvent, bool aFlipChecked) {
  StopBlinking();
  CreateMenuCommandEvent(aEvent, aFlipChecked);

  if (!ShouldBlink()) {
    PassMenuCommandEventToPopupManager();
    return;
  }

  // Blink off.
  AutoWeakFrame weakFrame(this);
  mContent->AsElement()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::menuactive,
                                   true);
  if (!weakFrame.IsAlive()) return;

  nsMenuParent* menuParent = GetMenuParent();
  if (menuParent) {
    // Make this menu ignore events from now on.
    menuParent->LockMenuUntilClosed(true);
  }

  // Set up a timer to blink back on.
  NS_NewTimerWithCallback(
      getter_AddRefs(mBlinkTimer), mTimerMediator, kBlinkDelay,
      nsITimer::TYPE_ONE_SHOT,
      mContent->OwnerDoc()->EventTargetFor(TaskCategory::Other));
  mBlinkState = 1;
}

void nsMenuFrame::StopBlinking() {
  mBlinkState = 0;
  if (mBlinkTimer) {
    mBlinkTimer->Cancel();
    mBlinkTimer = nullptr;
  }
  mDelayedMenuCommandEvent = nullptr;
}

void nsMenuFrame::CreateMenuCommandEvent(WidgetGUIEvent* aEvent,
                                         bool aFlipChecked) {
  // Create a trusted event if the triggering event was trusted, or if
  // we're called from chrome code (since at least one of our caller
  // passes in a null event).
  bool isTrusted =
      aEvent ? aEvent->IsTrusted() : nsContentUtils::IsCallerChrome();

  bool shift = false, control = false, alt = false, meta = false;
  WidgetInputEvent* inputEvent = aEvent ? aEvent->AsInputEvent() : nullptr;
  if (inputEvent) {
    shift = inputEvent->IsShift();
    control = inputEvent->IsControl();
    alt = inputEvent->IsAlt();
    meta = inputEvent->IsMeta();
  }

  // Because the command event is firing asynchronously, a flag is needed to
  // indicate whether user input is being handled. This ensures that a popup
  // window won't get blocked.
  bool userinput = EventStateManager::IsHandlingUserInput();

  mDelayedMenuCommandEvent =
      new nsXULMenuCommandEvent(mContent->AsElement(), isTrusted, shift,
                                control, alt, meta, userinput, aFlipChecked);
}

void nsMenuFrame::PassMenuCommandEventToPopupManager() {
  nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
  nsMenuParent* menuParent = GetMenuParent();
  if (pm && menuParent && mDelayedMenuCommandEvent) {
    pm->ExecuteMenu(mContent, mDelayedMenuCommandEvent);
  }
  mDelayedMenuCommandEvent = nullptr;
}

void nsMenuFrame::RemoveFrame(ChildListID aListID, nsIFrame* aOldFrame) {
  nsFrameList* popupList = GetPopupList();
  if (popupList && popupList->FirstChild() == aOldFrame) {
    popupList->RemoveFirstChild();
    aOldFrame->Destroy();
    DestroyPopupList();
    PresShell()->FrameNeedsReflow(this, nsIPresShell::eTreeChange,
                                  NS_FRAME_HAS_DIRTY_CHILDREN);
    return;
  }
  nsBoxFrame::RemoveFrame(aListID, aOldFrame);
}

void nsMenuFrame::InsertFrames(ChildListID aListID, nsIFrame* aPrevFrame,
                               nsFrameList& aFrameList) {
  if (!HasPopup() && (aListID == kPrincipalList || aListID == kPopupList)) {
    SetPopupFrame(aFrameList);
    if (HasPopup()) {
#ifdef DEBUG_LAYOUT
      nsBoxLayoutState state(PresContext());
      SetXULDebug(state, aFrameList, mState & NS_STATE_CURRENTLY_IN_DEBUG);
#endif

      PresShell()->FrameNeedsReflow(this, nsIPresShell::eTreeChange,
                                    NS_FRAME_HAS_DIRTY_CHILDREN);
    }
  }

  if (aFrameList.IsEmpty()) return;

  if (MOZ_UNLIKELY(aPrevFrame && aPrevFrame == GetPopup())) {
    aPrevFrame = nullptr;
  }

  nsBoxFrame::InsertFrames(aListID, aPrevFrame, aFrameList);
}

void nsMenuFrame::AppendFrames(ChildListID aListID, nsFrameList& aFrameList) {
  if (!HasPopup() && (aListID == kPrincipalList || aListID == kPopupList)) {
    SetPopupFrame(aFrameList);
    if (HasPopup()) {
#ifdef DEBUG_LAYOUT
      nsBoxLayoutState state(PresContext());
      SetXULDebug(state, aFrameList, mState & NS_STATE_CURRENTLY_IN_DEBUG);
#endif
      PresShell()->FrameNeedsReflow(this, nsIPresShell::eTreeChange,
                                    NS_FRAME_HAS_DIRTY_CHILDREN);
    }
  }

  if (aFrameList.IsEmpty()) return;

  nsBoxFrame::AppendFrames(aListID, aFrameList);
}

bool nsMenuFrame::SizeToPopup(nsBoxLayoutState& aState, nsSize& aSize) {
  if (!IsXULCollapsed()) {
    bool widthSet, heightSet;
    nsSize tmpSize(-1, 0);
    nsIFrame::AddXULPrefSize(this, tmpSize, widthSet, heightSet);
    if (!widthSet && GetXULFlex() == 0) {
      nsMenuPopupFrame* popupFrame = GetPopup();
      if (!popupFrame) return false;
      tmpSize = popupFrame->GetXULPrefSize(aState);

      // Produce a size such that:
      //  (1) the menu and its popup can be the same width
      //  (2) there's enough room in the menu for the content and its
      //      border-padding
      //  (3) there's enough room in the popup for the content and its
      //      scrollbar
      nsMargin borderPadding;
      GetXULBorderAndPadding(borderPadding);

      // if there is a scroll frame, add the desired width of the scrollbar as
      // well
      nsIScrollableFrame* scrollFrame =
          do_QueryFrame(popupFrame->PrincipalChildList().FirstChild());
      nscoord scrollbarWidth = 0;
      if (scrollFrame) {
        scrollbarWidth =
            scrollFrame->GetDesiredScrollbarSizes(&aState).LeftRight();
      }

      aSize.width =
          tmpSize.width + std::max(borderPadding.LeftRight(), scrollbarWidth);

      return true;
    }
  }

  return false;
}

nsSize nsMenuFrame::GetXULPrefSize(nsBoxLayoutState& aState) {
  nsSize size = nsBoxFrame::GetXULPrefSize(aState);
  DISPLAY_PREF_SIZE(this, size);

  // If we are using sizetopopup="always" then
  // nsBoxFrame will already have enforced the minimum size
  if (!IsSizedToPopup(mContent, true) && IsSizedToPopup(mContent, false) &&
      SizeToPopup(aState, size)) {
    // We now need to ensure that size is within the min - max range.
    nsSize minSize = nsBoxFrame::GetXULMinSize(aState);
    nsSize maxSize = GetXULMaxSize(aState);
    size = BoundsCheck(minSize, size, maxSize);
  }

  return size;
}

NS_IMETHODIMP
nsMenuFrame::GetActiveChild(nsIDOMElement** aResult) {
  nsMenuPopupFrame* popupFrame = GetPopup();
  if (!popupFrame) return NS_ERROR_FAILURE;

  nsMenuFrame* menuFrame = popupFrame->GetCurrentMenuItem();
  if (!menuFrame) {
    *aResult = nullptr;
  } else {
    nsCOMPtr<nsIDOMElement> elt(do_QueryInterface(menuFrame->GetContent()));
    *aResult = elt;
    NS_IF_ADDREF(*aResult);
  }

  return NS_OK;
}

NS_IMETHODIMP
nsMenuFrame::SetActiveChild(nsIDOMElement* aChild) {
  nsMenuPopupFrame* popupFrame = GetPopup();
  if (!popupFrame) return NS_ERROR_FAILURE;

  if (!aChild) {
    // Remove the current selection
    popupFrame->ChangeMenuItem(nullptr, false, false);
    return NS_OK;
  }

  nsCOMPtr<nsIContent> child(do_QueryInterface(aChild));

  nsMenuFrame* menu = do_QueryFrame(child->GetPrimaryFrame());
  if (menu) popupFrame->ChangeMenuItem(menu, false, false);
  return NS_OK;
}

nsIScrollableFrame* nsMenuFrame::GetScrollTargetFrame() {
  nsMenuPopupFrame* popupFrame = GetPopup();
  if (!popupFrame) return nullptr;
  nsIFrame* childFrame = popupFrame->PrincipalChildList().FirstChild();
  if (childFrame) return popupFrame->GetScrollFrame(childFrame);
  return nullptr;
}

// nsMenuTimerMediator implementation.
NS_IMPL_ISUPPORTS(nsMenuTimerMediator, nsITimerCallback)

/**
 * Constructs a wrapper around an nsMenuFrame.
 * @param aFrame nsMenuFrame to create a wrapper around.
 */
nsMenuTimerMediator::nsMenuTimerMediator(nsMenuFrame* aFrame) : mFrame(aFrame) {
  NS_ASSERTION(mFrame, "Must have frame");
}

nsMenuTimerMediator::~nsMenuTimerMediator() {}

/**
 * Delegates the notification to the contained frame if it has not been
 * destroyed.
 * @param aTimer Timer which initiated the callback.
 * @return NS_ERROR_FAILURE if the frame has been destroyed.
 */
NS_IMETHODIMP nsMenuTimerMediator::Notify(nsITimer* aTimer) {
  if (!mFrame) return NS_ERROR_FAILURE;

  return mFrame->Notify(aTimer);
}

/**
 * Clear the pointer to the contained nsMenuFrame. This should be called
 * when the contained nsMenuFrame is destroyed.
 */
void nsMenuTimerMediator::ClearFrame() { mFrame = nullptr; }

/**
 * Get the name of this timer callback.
 * @param aName the name to return
 */
NS_IMETHODIMP
nsMenuTimerMediator::GetName(nsACString& aName) {
  aName.AssignLiteral("nsMenuTimerMediator");
  return NS_OK;
}