forked from mirrors/gecko-dev
		
	 25c0d10932
			
		
	
	
		25c0d10932
		
	
	
	
	
		
			
			Sorry this is not a particularly easy patch to review. But it should be mostly straight-forward. I kept Document::Dispatch mostly for convenience, but could be cleaned-up too / changed by SchedulerGroup::Dispatch. Similarly maybe that can just be NS_DispatchToMainThread if we add an NS_IsMainThread check there or something (to preserve shutdown semantics). Differential Revision: https://phabricator.services.mozilla.com/D190450
		
			
				
	
	
		
			398 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			398 lines
		
	
	
	
		
			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(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
 |