gecko-dev/layout/xul/nsMenuBarFrame.cpp

357 строки
10 KiB
C++

/* -*- 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<uint32_t, 10> 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<nsISound> 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<nsIContent> mMenuBar;
nsCOMPtr<nsIContent> mOldMenu;
nsCOMPtr<nsIContent> 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<nsIContent> 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<nsIRunnable> 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);
}