зеркало из https://github.com/mozilla/gecko-dev.git
399 строки
13 KiB
C++
399 строки
13 KiB
C++
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
#include "XULMenuParentElement.h"
|
|
#include "XULButtonElement.h"
|
|
#include "XULMenuBarElement.h"
|
|
#include "XULPopupElement.h"
|
|
#include "mozilla/LookAndFeel.h"
|
|
#include "mozilla/StaticAnalysisFunctions.h"
|
|
#include "mozilla/TextEvents.h"
|
|
#include "mozilla/dom/DocumentInlines.h"
|
|
#include "mozilla/dom/KeyboardEvent.h"
|
|
#include "mozilla/EventDispatcher.h"
|
|
#include "nsDebug.h"
|
|
#include "nsMenuPopupFrame.h"
|
|
#include "nsString.h"
|
|
#include "nsStringFwd.h"
|
|
#include "nsUTF8Utils.h"
|
|
#include "nsXULElement.h"
|
|
#include "nsXULPopupManager.h"
|
|
|
|
namespace mozilla::dom {
|
|
|
|
NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(XULMenuParentElement,
|
|
nsXULElement)
|
|
NS_IMPL_CYCLE_COLLECTION_INHERITED(XULMenuParentElement, nsXULElement,
|
|
mActiveItem)
|
|
|
|
XULMenuParentElement::XULMenuParentElement(
|
|
already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
|
|
: nsXULElement(std::move(aNodeInfo)) {}
|
|
|
|
XULMenuParentElement::~XULMenuParentElement() = default;
|
|
|
|
class MenuActivateEvent final : public Runnable {
|
|
public:
|
|
MenuActivateEvent(Element* aMenu, bool aIsActivate)
|
|
: Runnable("MenuActivateEvent"), mMenu(aMenu), mIsActivate(aIsActivate) {}
|
|
|
|
// TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398)
|
|
MOZ_CAN_RUN_SCRIPT_BOUNDARY 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<nsPresContext> pc = mMenu->OwnerDoc()->GetPresContext();
|
|
RefPtr<dom::Event> event = NS_NewDOMEvent(mMenu, pc, nullptr);
|
|
event->InitEvent(domEventToFire, true, true);
|
|
|
|
event->SetTrusted(true);
|
|
|
|
EventDispatcher::DispatchDOMEvent(mMenu, nullptr, event, pc, nullptr);
|
|
return NS_OK;
|
|
}
|
|
|
|
private:
|
|
const RefPtr<Element> mMenu;
|
|
bool mIsActivate;
|
|
};
|
|
|
|
static void ActivateOrDeactivate(XULButtonElement& aButton, bool aActivate) {
|
|
if (!aButton.IsMenu()) {
|
|
return;
|
|
}
|
|
|
|
if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) {
|
|
if (aActivate) {
|
|
// Cancel the close timer if selecting a menu within the popup to be
|
|
// closed.
|
|
pm->CancelMenuTimer(aButton.GetContainingPopupWithoutFlushing());
|
|
} else if (auto* popup = aButton.GetMenuPopupWithoutFlushing()) {
|
|
if (popup->IsOpen()) {
|
|
// Set up the close timer if deselecting an open sub-menu.
|
|
pm->HidePopupAfterDelay(popup, aButton.MenuOpenCloseDelay());
|
|
}
|
|
}
|
|
}
|
|
|
|
nsCOMPtr<nsIRunnable> event = new MenuActivateEvent(&aButton, aActivate);
|
|
aButton.OwnerDoc()->Dispatch(TaskCategory::Other, event.forget());
|
|
}
|
|
|
|
XULButtonElement* XULMenuParentElement::GetContainingMenu() const {
|
|
if (IsMenuBar()) {
|
|
return nullptr;
|
|
}
|
|
auto* button = XULButtonElement::FromNodeOrNull(GetParent());
|
|
if (!button || !button->IsMenu()) {
|
|
return nullptr;
|
|
}
|
|
return button;
|
|
}
|
|
|
|
void XULMenuParentElement::LockMenuUntilClosed(bool aLock) {
|
|
if (IsMenuBar()) {
|
|
return;
|
|
}
|
|
mLocked = aLock;
|
|
// Lock/Unlock the parent menu too.
|
|
if (XULButtonElement* menu = GetContainingMenu()) {
|
|
if (XULMenuParentElement* parent = menu->GetMenuParent()) {
|
|
parent->LockMenuUntilClosed(aLock);
|
|
}
|
|
}
|
|
}
|
|
|
|
void XULMenuParentElement::SetActiveMenuChild(XULButtonElement* aChild,
|
|
ByKey aByKey) {
|
|
if (aChild == mActiveItem) {
|
|
return;
|
|
}
|
|
|
|
if (mActiveItem) {
|
|
ActivateOrDeactivate(*mActiveItem, false);
|
|
}
|
|
mActiveItem = nullptr;
|
|
|
|
if (auto* menuBar = XULMenuBarElement::FromNode(*this)) {
|
|
// KnownLive because `this` is known-live by definition.
|
|
MOZ_KnownLive(menuBar)->SetActive(!!aChild);
|
|
}
|
|
|
|
if (!aChild) {
|
|
return;
|
|
}
|
|
|
|
// 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.
|
|
if (RefPtr menu = GetContainingMenu()) {
|
|
if (RefPtr parent = menu->GetMenuParent()) {
|
|
parent->SetActiveMenuChild(menu, aByKey);
|
|
}
|
|
}
|
|
|
|
mActiveItem = aChild;
|
|
ActivateOrDeactivate(*mActiveItem, true);
|
|
|
|
if (IsInMenuList()) {
|
|
if (nsMenuPopupFrame* f = do_QueryFrame(GetPrimaryFrame())) {
|
|
f->EnsureActiveMenuListItemIsVisible();
|
|
#ifdef XP_WIN
|
|
// On Windows, a menulist should update its value whenever navigation was
|
|
// done by the keyboard.
|
|
//
|
|
// NOTE(emilio): This is a rather odd per-platform behavior difference,
|
|
// but other browsers also do this.
|
|
if (aByKey == ByKey::Yes && f->IsOpen()) {
|
|
// Fire a command event as the new item, but we don't want to close the
|
|
// menu, blink it, or update any other state of the menuitem. The
|
|
// command event will cause the item to be selected.
|
|
RefPtr<mozilla::PresShell> presShell = OwnerDoc()->GetPresShell();
|
|
nsContentUtils::DispatchXULCommand(aChild, /* aTrusted = */ true,
|
|
nullptr, presShell, false, false,
|
|
false, false);
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool IsValidMenuItem(const XULMenuParentElement& aMenuParent,
|
|
const nsIContent& aContent) {
|
|
const auto* button = XULButtonElement::FromNode(aContent);
|
|
if (!button || !button->IsMenu()) {
|
|
return false;
|
|
}
|
|
if (!button->GetPrimaryFrame()) {
|
|
// Hidden buttons are not focusable/activatable.
|
|
return false;
|
|
}
|
|
if (!button->IsDisabled()) {
|
|
return true;
|
|
}
|
|
// In the menubar or a menulist disabled items are always skipped.
|
|
const bool skipDisabled =
|
|
LookAndFeel::GetInt(LookAndFeel::IntID::SkipNavigatingDisabledMenuItem) ||
|
|
aMenuParent.IsMenuBar() || aMenuParent.IsInMenuList();
|
|
return !skipDisabled;
|
|
}
|
|
|
|
enum class StartKind { Parent, Item };
|
|
|
|
template <bool aForward>
|
|
static XULButtonElement* DoGetNextMenuItem(
|
|
const XULMenuParentElement& aMenuParent, const nsIContent& aStart,
|
|
StartKind aStartKind) {
|
|
nsIContent* start =
|
|
aStartKind == StartKind::Item
|
|
? (aForward ? aStart.GetNextSibling() : aStart.GetPreviousSibling())
|
|
: (aForward ? aStart.GetFirstChild() : aStart.GetLastChild());
|
|
for (nsIContent* node = start; node;
|
|
node = aForward ? node->GetNextSibling() : node->GetPreviousSibling()) {
|
|
if (IsValidMenuItem(aMenuParent, *node)) {
|
|
return static_cast<XULButtonElement*>(node);
|
|
}
|
|
if (node->IsXULElement(nsGkAtoms::menugroup)) {
|
|
if (XULButtonElement* child = DoGetNextMenuItem<aForward>(
|
|
aMenuParent, *node, StartKind::Parent)) {
|
|
return child;
|
|
}
|
|
}
|
|
}
|
|
if (aStartKind == StartKind::Item && aStart.GetParent() &&
|
|
aStart.GetParent()->IsXULElement(nsGkAtoms::menugroup)) {
|
|
// We haven't found anything in aStart's sibling list, but if we're in a
|
|
// group we need to keep looking.
|
|
return DoGetNextMenuItem<aForward>(aMenuParent, *aStart.GetParent(),
|
|
StartKind::Item);
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
XULButtonElement* XULMenuParentElement::GetFirstMenuItem() const {
|
|
return DoGetNextMenuItem<true>(*this, *this, StartKind::Parent);
|
|
}
|
|
|
|
XULButtonElement* XULMenuParentElement::GetLastMenuItem() const {
|
|
return DoGetNextMenuItem<false>(*this, *this, StartKind::Parent);
|
|
}
|
|
|
|
XULButtonElement* XULMenuParentElement::GetNextMenuItemFrom(
|
|
const XULButtonElement& aStartingItem) const {
|
|
return DoGetNextMenuItem<true>(*this, aStartingItem, StartKind::Item);
|
|
}
|
|
|
|
XULButtonElement* XULMenuParentElement::GetPrevMenuItemFrom(
|
|
const XULButtonElement& aStartingItem) const {
|
|
return DoGetNextMenuItem<false>(*this, aStartingItem, StartKind::Item);
|
|
}
|
|
|
|
XULButtonElement* XULMenuParentElement::GetNextMenuItem(Wrap aWrap) const {
|
|
if (mActiveItem) {
|
|
if (auto* next = GetNextMenuItemFrom(*mActiveItem)) {
|
|
return next;
|
|
}
|
|
if (aWrap == Wrap::No) {
|
|
return nullptr;
|
|
}
|
|
}
|
|
return GetFirstMenuItem();
|
|
}
|
|
|
|
XULButtonElement* XULMenuParentElement::GetPrevMenuItem(Wrap aWrap) const {
|
|
if (mActiveItem) {
|
|
if (auto* prev = GetPrevMenuItemFrom(*mActiveItem)) {
|
|
return prev;
|
|
}
|
|
if (aWrap == Wrap::No) {
|
|
return nullptr;
|
|
}
|
|
}
|
|
return GetLastMenuItem();
|
|
}
|
|
|
|
void XULMenuParentElement::SelectFirstItem() {
|
|
if (RefPtr firstItem = GetFirstMenuItem()) {
|
|
SetActiveMenuChild(firstItem);
|
|
}
|
|
}
|
|
|
|
XULButtonElement* XULMenuParentElement::FindMenuWithShortcut(
|
|
KeyboardEvent& aKeyEvent) const {
|
|
using AccessKeyArray = AutoTArray<uint32_t, 10>;
|
|
AccessKeyArray accessKeys;
|
|
WidgetKeyboardEvent* nativeKeyEvent =
|
|
aKeyEvent.WidgetEventPtr()->AsKeyboardEvent();
|
|
if (nativeKeyEvent) {
|
|
nativeKeyEvent->GetAccessKeyCandidates(accessKeys);
|
|
}
|
|
const uint32_t charCode = aKeyEvent.CharCode();
|
|
if (accessKeys.IsEmpty() && charCode) {
|
|
accessKeys.AppendElement(charCode);
|
|
}
|
|
if (accessKeys.IsEmpty()) {
|
|
return nullptr; // no character was pressed so just return
|
|
}
|
|
XULButtonElement* foundMenu = nullptr;
|
|
size_t foundIndex = AccessKeyArray::NoIndex;
|
|
for (auto* item = GetFirstMenuItem(); item;
|
|
item = GetNextMenuItemFrom(*item)) {
|
|
nsAutoString shortcutKey;
|
|
item->GetAttr(nsGkAtoms::accesskey, shortcutKey);
|
|
if (shortcutKey.IsEmpty()) {
|
|
continue;
|
|
}
|
|
|
|
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 == AccessKeyArray::NoIndex) {
|
|
continue;
|
|
}
|
|
if (foundIndex == AccessKeyArray::NoIndex || index < foundIndex) {
|
|
foundMenu = item;
|
|
foundIndex = index;
|
|
}
|
|
}
|
|
return foundMenu;
|
|
}
|
|
|
|
XULButtonElement* XULMenuParentElement::FindMenuWithShortcut(
|
|
const nsAString& aString, bool& aDoAction) const {
|
|
aDoAction = false;
|
|
uint32_t accessKeyMatchCount = 0;
|
|
uint32_t matchCount = 0;
|
|
|
|
XULButtonElement* foundAccessKeyMenu = nullptr;
|
|
XULButtonElement* foundMenuBeforeCurrent = nullptr;
|
|
XULButtonElement* foundMenuAfterCurrent = nullptr;
|
|
|
|
bool foundActive = false;
|
|
for (auto* item = GetFirstMenuItem(); item;
|
|
item = GetNextMenuItemFrom(*item)) {
|
|
nsAutoString textKey;
|
|
// Get the shortcut attribute.
|
|
item->GetAttr(nsGkAtoms::accesskey, textKey);
|
|
const bool isAccessKey = !textKey.IsEmpty();
|
|
if (textKey.IsEmpty()) { // No shortcut, try first letter
|
|
item->GetAttr(nsGkAtoms::label, textKey);
|
|
if (textKey.IsEmpty()) { // No label, try another attribute (value)
|
|
item->GetAttr(nsGkAtoms::value, textKey);
|
|
}
|
|
}
|
|
|
|
const bool isActive = item == GetActiveMenuChild();
|
|
foundActive |= isActive;
|
|
|
|
if (!StringBeginsWith(
|
|
nsContentUtils::TrimWhitespace<
|
|
nsContentUtils::IsHTMLWhitespaceOrNBSP>(textKey, false),
|
|
aString, nsCaseInsensitiveStringComparator)) {
|
|
continue;
|
|
}
|
|
// There is one match
|
|
matchCount++;
|
|
if (isAccessKey) {
|
|
// There is one shortcut-key match
|
|
accessKeyMatchCount++;
|
|
// Record the matched item. If there is only one matched shortcut
|
|
// item, do it
|
|
foundAccessKeyMenu = item;
|
|
}
|
|
// Get the active status
|
|
if (isActive && aString.Length() > 1 && !foundMenuBeforeCurrent) {
|
|
// If there is more than one char typed and the current item matches, the
|
|
// current item has highest priority, otherwise the item next to current
|
|
// has highest priority.
|
|
return item;
|
|
}
|
|
if (!foundActive || isActive) {
|
|
// It's a first candidate item located before/on the current item
|
|
if (!foundMenuBeforeCurrent) {
|
|
foundMenuBeforeCurrent = item;
|
|
}
|
|
} else {
|
|
if (!foundMenuAfterCurrent) {
|
|
foundMenuAfterCurrent = item;
|
|
}
|
|
}
|
|
}
|
|
|
|
aDoAction = !IsInMenuList() && (matchCount == 1 || accessKeyMatchCount == 1);
|
|
|
|
if (accessKeyMatchCount == 1) {
|
|
// We have one matched accesskey item
|
|
return foundAccessKeyMenu;
|
|
}
|
|
// If we have matched an item after the current, use it.
|
|
if (foundMenuAfterCurrent) {
|
|
return foundMenuAfterCurrent;
|
|
}
|
|
// If we haven't, use the item before the current, if any.
|
|
return foundMenuBeforeCurrent;
|
|
}
|
|
|
|
void XULMenuParentElement::HandleEnterKeyPress(WidgetEvent& aEvent) {
|
|
if (RefPtr child = GetActiveMenuChild()) {
|
|
child->HandleEnterKeyPress(aEvent);
|
|
}
|
|
}
|
|
|
|
} // namespace mozilla::dom
|