diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /dom/xul/XULMenuParentElement.cpp | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/xul/XULMenuParentElement.cpp')
-rw-r--r-- | dom/xul/XULMenuParentElement.cpp | 398 |
1 files changed, 398 insertions, 0 deletions
diff --git a/dom/xul/XULMenuParentElement.cpp b/dom/xul/XULMenuParentElement.cpp new file mode 100644 index 0000000000..c457187025 --- /dev/null +++ b/dom/xul/XULMenuParentElement.cpp @@ -0,0 +1,398 @@ +/* -*- 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(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 |