/* -*- 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 "nsMenuBarFrame.h" #include "nsIContent.h" #include "nsAtom.h" #include "nsPresContext.h" #include "nsCSSRendering.h" #include "nsNameSpaceManager.h" #include "nsGkAtoms.h" #include "nsMenuFrame.h" #include "nsMenuPopupFrame.h" #include "nsUnicharUtils.h" #include "nsPIDOMWindow.h" #include "nsIInterfaceRequestorUtils.h" #include "nsCSSFrameConstructor.h" #ifdef XP_WIN # include "nsISound.h" # include "nsWidgetsCID.h" #endif #include "nsUTF8Utils.h" #include "mozilla/ComputedStyle.h" #include "mozilla/PresShell.h" #include "mozilla/TextEvents.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/Event.h" #include "mozilla/dom/KeyboardEvent.h" using namespace mozilla; using mozilla::dom::KeyboardEvent; // // NS_NewMenuBarFrame // // Wrapper for creating a new menu Bar container // nsIFrame* NS_NewMenuBarFrame(PresShell* aPresShell, ComputedStyle* aStyle) { return new (aPresShell) nsMenuBarFrame(aStyle, aPresShell->GetPresContext()); } NS_IMPL_FRAMEARENA_HELPERS(nsMenuBarFrame) NS_QUERYFRAME_HEAD(nsMenuBarFrame) NS_QUERYFRAME_ENTRY(nsMenuBarFrame) NS_QUERYFRAME_TAIL_INHERITING(nsBoxFrame) // // nsMenuBarFrame cntr // nsMenuBarFrame::nsMenuBarFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) : nsBoxFrame(aStyle, aPresContext, kClassID), mStayActive(false), mIsActive(false), mActiveByKeyboard(false), mCurrentMenu(nullptr) {} // cntr void nsMenuBarFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, nsIFrame* aPrevInFlow) { nsBoxFrame::Init(aContent, aParent, aPrevInFlow); // Create the menu bar listener. mMenuBarListener = new nsMenuBarListener(this, aContent); } NS_IMETHODIMP nsMenuBarFrame::SetActive(bool aActiveFlag) { // If the activity is not changed, there is nothing to do. if (mIsActive == aActiveFlag) return NS_OK; if (!aActiveFlag) { // Don't deactivate when switching between menus on the menubar. if (mStayActive) return NS_OK; // if there is a request to deactivate the menu bar, check to see whether // there is a menu popup open for the menu bar. In this case, don't // deactivate the menu bar. nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); if (pm && pm->IsPopupOpenForMenuParent(this)) return NS_OK; } mIsActive = aActiveFlag; if (mIsActive) { InstallKeyboardNavigator(); } else { mActiveByKeyboard = false; RemoveKeyboardNavigator(); } constexpr auto active = u"DOMMenuBarActive"_ns; constexpr auto inactive = u"DOMMenuBarInactive"_ns; FireDOMEvent(mIsActive ? active : inactive, mContent); return NS_OK; } nsMenuFrame* nsMenuBarFrame::ToggleMenuActiveState() { if (mIsActive) { // Deactivate the menu bar SetActive(false); if (mCurrentMenu) { nsMenuFrame* closeframe = mCurrentMenu; closeframe->SelectMenu(false); mCurrentMenu = nullptr; return closeframe; } } else { // if the menu bar is already selected (eg. mouseover), deselect it if (mCurrentMenu) mCurrentMenu->SelectMenu(false); // Set the active menu to be the top left item (e.g., the File menu). // We use an attribute called "menuactive" to track the current // active menu. nsMenuFrame* firstFrame = nsXULPopupManager::GetNextMenuItem(this, nullptr, false, false); if (firstFrame) { // Activate the menu bar SetActive(true); firstFrame->SelectMenu(true); // Track this item for keyboard navigation. mCurrentMenu = firstFrame; } } return nullptr; } nsMenuFrame* nsMenuBarFrame::FindMenuWithShortcut(KeyboardEvent* aKeyEvent, bool aPeek) { uint32_t charCode = aKeyEvent->CharCode(); AutoTArray accessKeys; WidgetKeyboardEvent* nativeKeyEvent = aKeyEvent->WidgetEventPtr()->AsKeyboardEvent(); if (nativeKeyEvent) { nativeKeyEvent->GetAccessKeyCandidates(accessKeys); } if (accessKeys.IsEmpty() && charCode) accessKeys.AppendElement(charCode); if (accessKeys.IsEmpty()) return nullptr; // no character was pressed so just return // Enumerate over our list of frames. nsContainerFrame* immediateParent = nsXULPopupManager::ImmediateParentFrame(this); // Find a most preferred accesskey which should be returned. nsIFrame* foundMenu = nullptr; size_t foundIndex = accessKeys.NoIndex; nsIFrame* currFrame = immediateParent->PrincipalChildList().FirstChild(); while (currFrame) { nsIContent* current = currFrame->GetContent(); // See if it's a menu item. if (nsXULPopupManager::IsValidMenuItem(current, false)) { // Get the shortcut attribute. nsAutoString shortcutKey; if (current->IsElement()) { current->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::accesskey, shortcutKey); } if (!shortcutKey.IsEmpty()) { ToLowerCase(shortcutKey); const char16_t* start = shortcutKey.BeginReading(); const char16_t* end = shortcutKey.EndReading(); uint32_t ch = UTF16CharEnumerator::NextChar(&start, end); size_t index = accessKeys.IndexOf(ch); if (index != accessKeys.NoIndex && (foundIndex == accessKeys.NoIndex || index < foundIndex)) { foundMenu = currFrame; foundIndex = index; } } } currFrame = currFrame->GetNextSibling(); } if (foundMenu) { return do_QueryFrame(foundMenu); } // didn't find a matching menu item #ifdef XP_WIN if (!aPeek) { // behavior on Windows - this item is on the menu bar, beep and deactivate // the menu bar if (mIsActive) { nsCOMPtr soundInterface = do_GetService("@mozilla.org/sound;1"); if (soundInterface) soundInterface->Beep(); } nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); if (pm) { nsIFrame* popup = pm->GetTopPopup(ePopupTypeMenu); if (popup) pm->HidePopup(popup->GetContent(), true, true, true, false); } SetCurrentMenuItem(nullptr); SetActive(false); } #endif // #ifdef XP_WIN return nullptr; } /* virtual */ nsMenuFrame* nsMenuBarFrame::GetCurrentMenuItem() { return mCurrentMenu; } NS_IMETHODIMP nsMenuBarFrame::SetCurrentMenuItem(nsMenuFrame* aMenuItem) { if (mCurrentMenu == aMenuItem) return NS_OK; if (mCurrentMenu) mCurrentMenu->SelectMenu(false); if (aMenuItem) aMenuItem->SelectMenu(true); mCurrentMenu = aMenuItem; return NS_OK; } void nsMenuBarFrame::CurrentMenuIsBeingDestroyed() { mCurrentMenu->SelectMenu(false); mCurrentMenu = nullptr; } class nsMenuBarSwitchMenu : public Runnable { public: nsMenuBarSwitchMenu(nsIContent* aMenuBar, nsIContent* aOldMenu, nsIContent* aNewMenu, bool aSelectFirstItem) : mozilla::Runnable("nsMenuBarSwitchMenu"), mMenuBar(aMenuBar), mOldMenu(aOldMenu), mNewMenu(aNewMenu), mSelectFirstItem(aSelectFirstItem) {} NS_IMETHOD Run() override { nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); if (!pm) return NS_ERROR_UNEXPECTED; // if switching from one menu to another, set a flag so that the call to // HidePopup doesn't deactivate the menubar when the first menu closes. nsMenuBarFrame* menubar = nullptr; if (mOldMenu && mNewMenu) { menubar = do_QueryFrame(mMenuBar->GetPrimaryFrame()); if (menubar) menubar->SetStayActive(true); } if (mOldMenu) { AutoWeakFrame weakMenuBar(menubar); pm->HidePopup(mOldMenu, false, false, false, false); // clear the flag again if (mNewMenu && weakMenuBar.IsAlive()) menubar->SetStayActive(false); } if (mNewMenu) { pm->ShowMenu(mNewMenu, mSelectFirstItem); } return NS_OK; } private: nsCOMPtr mMenuBar; nsCOMPtr mOldMenu; nsCOMPtr mNewMenu; bool mSelectFirstItem; }; NS_IMETHODIMP nsMenuBarFrame::ChangeMenuItem(nsMenuFrame* aMenuItem, bool aSelectFirstItem, bool aFromKey) { if (mCurrentMenu == aMenuItem) return NS_OK; // check if there's an open context menu, we ignore this nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); if (pm && pm->HasContextMenu(nullptr)) return NS_OK; nsIContent* aOldMenu = nullptr; nsIContent* aNewMenu = nullptr; // Unset the current child. bool wasOpen = false; if (mCurrentMenu) { wasOpen = mCurrentMenu->IsOpen(); mCurrentMenu->SelectMenu(false); if (wasOpen) { nsMenuPopupFrame* popupFrame = mCurrentMenu->GetPopup(); if (popupFrame) aOldMenu = popupFrame->GetContent(); } } // set to null first in case the IsAlive check below returns false mCurrentMenu = nullptr; // Set the new child. if (aMenuItem) { nsCOMPtr content = aMenuItem->GetContent(); aMenuItem->SelectMenu(true); mCurrentMenu = aMenuItem; if (wasOpen && !aMenuItem->IsDisabled()) aNewMenu = content; } // use an event so that hiding and showing can be done synchronously, which // avoids flickering nsCOMPtr event = new nsMenuBarSwitchMenu( GetContent(), aOldMenu, aNewMenu, aSelectFirstItem); return mContent->OwnerDoc()->Dispatch(TaskCategory::Other, event.forget()); } nsMenuFrame* nsMenuBarFrame::Enter(WidgetGUIEvent* aEvent) { if (!mCurrentMenu) return nullptr; if (mCurrentMenu->IsOpen()) return mCurrentMenu->Enter(aEvent); return mCurrentMenu; } bool nsMenuBarFrame::MenuClosed() { SetActive(false); if (!mIsActive && mCurrentMenu) { mCurrentMenu->SelectMenu(false); mCurrentMenu = nullptr; return true; } return false; } void nsMenuBarFrame::InstallKeyboardNavigator() { nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); if (pm) pm->SetActiveMenuBar(this, true); } void nsMenuBarFrame::RemoveKeyboardNavigator() { if (!mIsActive) { nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); if (pm) pm->SetActiveMenuBar(this, false); } } void nsMenuBarFrame::DestroyFrom(nsIFrame* aDestructRoot, PostDestroyData& aPostDestroyData) { nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); if (pm) pm->SetActiveMenuBar(this, false); mMenuBarListener->OnDestroyMenuBarFrame(); mMenuBarListener = nullptr; nsBoxFrame::DestroyFrom(aDestructRoot, aPostDestroyData); }