summaryrefslogtreecommitdiffstats
path: root/layout/xul/nsMenuFrame.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--layout/xul/nsMenuFrame.cpp1167
1 files changed, 1167 insertions, 0 deletions
diff --git a/layout/xul/nsMenuFrame.cpp b/layout/xul/nsMenuFrame.cpp
new file mode 100644
index 0000000000..c9e955b098
--- /dev/null
+++ b/layout/xul/nsMenuFrame.cpp
@@ -0,0 +1,1167 @@
+/* -*- 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 "mozilla/ComputedStyle.h"
+#include "nsCSSRendering.h"
+#include "nsNameSpaceManager.h"
+#include "nsMenuPopupFrame.h"
+#include "nsMenuBarFrame.h"
+#include "mozilla/dom/Document.h"
+#include "nsBoxLayoutState.h"
+#include "nsIScrollableFrame.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 "mozilla/Attributes.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/Likely.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/Services.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/UserActivation.h"
+#include <algorithm>
+
+using namespace mozilla;
+using dom::Element;
+
+#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, u"true"_ns,
+ 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<dom::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::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(PresShell* aPresShell, ComputedStyle* aStyle) {
+ nsMenuFrame* it =
+ new (aPresShell) nsMenuFrame(aStyle, aPresShell->GetPresContext());
+ it->SetIsMenu(true);
+ return it;
+}
+
+nsIFrame* NS_NewMenuItemFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
+ nsMenuFrame* it =
+ new (aPresShell) nsMenuFrame(aStyle, aPresShell->GetPresContext());
+ 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(ComputedStyle* aStyle, nsPresContext* aPresContext)
+ : nsBoxFrame(aStyle, aPresContext, kClassID),
+ mIsMenu(false),
+ mChecked(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);
+
+ 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() const {
+ 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 = TakeProperty(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?");
+#ifdef DEBUG
+ for (nsIFrame* f : aChildList) {
+ MOZ_ASSERT(f->GetParent() == this, "Unexpected parent");
+ }
+#endif
+ 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()->mButton == MouseButton::ePrimary &&
+#ifdef XP_MACOSX
+ // On mac, ctrl-click will send a context menu event from the
+ // widget, so we don't want to bring up the menu.
+ !aEvent->AsMouseEvent()->IsControl() &&
+#endif
+ !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()->mButton == MouseButton::eSecondary
+# ifdef XP_MACOSX
+ // On Mac, we get the context menu event on left-click when ctrl key is
+ // pressed, listen it as well to dismiss the menu.
+ || (aEvent->AsMouseEvent()->mButton == MouseButton::ePrimary &&
+ aEvent->AsMouseEvent()->IsControl())
+# endif
+ )) &&
+#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()->mButton == MouseButton::ePrimary &&
+#ifdef XP_MACOSX
+ // On Mac, we get the context menu event on left-click when ctrl
+ // key is pressed, so we don't want to execute the event handler.
+ !aEvent->AsMouseEvent()->IsControl() &&
+#endif
+ !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
+ && !IsParentMenuList()
+#endif
+ ) {
+ menuParent->ChangeMenuItem(nullptr, false, false);
+ }
+ }
+ }
+ } else if (aEvent->mMessage == eMouseMove &&
+ (onmenu || (menuParent && menuParent->IsMenuBar()))) {
+ if (gMenuJustOpenedOrClosed) {
+ gMenuJustOpenedOrClosed = false;
+ return NS_OK;
+ }
+
+ if (IsDisabled() && IsParentMenuList()) {
+ 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::IntID::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, u"true"_ns,
+ 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::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;
+}
+
+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);
+ bool sizedToPopupSetToPref =
+ sizedToPopup.EqualsLiteral("pref") ||
+ (sizedToPopup.IsEmpty() && aContent->IsXULElement(nsGkAtoms::menulist));
+ return sizedToPopup.EqualsLiteral("always") ||
+ (!aRequireAlways && sizedToPopupSetToPref);
+}
+
+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, sizeToPopup);
+ }
+
+ return rv;
+}
+
+//
+// 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; }
+
+bool nsMenuFrame::IsParentMenuList() {
+ nsMenuParent* menuParent = GetMenuParent();
+ if (menuParent && menuParent->IsMenu()) {
+ nsMenuPopupFrame* popupFrame = static_cast<nsMenuPopupFrame*>(menuParent);
+ return popupFrame->IsMenuList();
+ }
+ return false;
+}
+
+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,
+ u"true"_ns, 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);
+ NS_ENSURE_TRUE_VOID(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::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::IntID::ChosenMenuItemsShouldBlink, 0);
+ if (!shouldBlink) 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 = dom::UserActivation::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, IntrinsicDirty::TreeChange,
+ NS_FRAME_HAS_DIRTY_CHILDREN);
+ return;
+ }
+ nsBoxFrame::RemoveFrame(aListID, aOldFrame);
+}
+
+void nsMenuFrame::InsertFrames(ChildListID aListID, nsIFrame* aPrevFrame,
+ const nsLineList::iterator* aPrevFrameLine,
+ nsFrameList& aFrameList) {
+ if (!HasPopup() && (aListID == kPrincipalList || aListID == kPopupList)) {
+ SetPopupFrame(aFrameList);
+ if (HasPopup()) {
+ PresShell()->FrameNeedsReflow(this, IntrinsicDirty::TreeChange,
+ NS_FRAME_HAS_DIRTY_CHILDREN);
+ }
+ }
+
+ if (aFrameList.IsEmpty()) return;
+
+ if (MOZ_UNLIKELY(aPrevFrame && aPrevFrame == GetPopup())) {
+ aPrevFrame = nullptr;
+ }
+
+ nsBoxFrame::InsertFrames(aListID, aPrevFrame, aPrevFrameLine, aFrameList);
+}
+
+void nsMenuFrame::AppendFrames(ChildListID aListID, nsFrameList& aFrameList) {
+ if (!HasPopup() && (aListID == kPrincipalList || aListID == kPopupList)) {
+ SetPopupFrame(aFrameList);
+ if (HasPopup()) {
+ PresShell()->FrameNeedsReflow(this, IntrinsicDirty::TreeChange,
+ 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 = popupFrame->GetScrollFrame(popupFrame);
+ 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 = XULBoundsCheck(minSize, size, maxSize);
+ }
+
+ return size;
+}
+
+NS_IMETHODIMP
+nsMenuFrame::GetActiveChild(dom::Element** aResult) {
+ nsMenuPopupFrame* popupFrame = GetPopup();
+ if (!popupFrame) return NS_ERROR_FAILURE;
+
+ nsMenuFrame* menuFrame = popupFrame->GetCurrentMenuItem();
+ if (!menuFrame) {
+ *aResult = nullptr;
+ } else {
+ RefPtr<dom::Element> elt = menuFrame->GetContent()->AsElement();
+ elt.forget(aResult);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMenuFrame::SetActiveChild(dom::Element* aChild) {
+ nsMenuPopupFrame* popupFrame = GetPopup();
+ if (!popupFrame) return NS_ERROR_FAILURE;
+
+ // Force the child frames within the popup to be generated.
+ AutoWeakFrame weakFrame(popupFrame);
+ popupFrame->GenerateFrames();
+ if (!weakFrame.IsAlive()) {
+ return NS_OK;
+ }
+
+ if (!aChild) {
+ // Remove the current selection
+ popupFrame->ChangeMenuItem(nullptr, false, false);
+ return NS_OK;
+ }
+
+ nsMenuFrame* menu = do_QueryFrame(aChild->GetPrimaryFrame());
+ if (menu) popupFrame->ChangeMenuItem(menu, false, false);
+ return NS_OK;
+}
+
+nsIScrollableFrame* nsMenuFrame::GetScrollTargetFrame() const {
+ 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() = default;
+
+/**
+ * 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;
+}