tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

XULMenuParentElement.cpp (13156B)


      1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
      2 /* This Source Code Form is subject to the terms of the Mozilla Public
      3 * License, v. 2.0. If a copy of the MPL was not distributed with this
      4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      5 
      6 #include "XULMenuParentElement.h"
      7 
      8 #include "XULButtonElement.h"
      9 #include "XULMenuBarElement.h"
     10 #include "XULPopupElement.h"
     11 #include "mozilla/EventDispatcher.h"
     12 #include "mozilla/LookAndFeel.h"
     13 #include "mozilla/StaticAnalysisFunctions.h"
     14 #include "mozilla/TextEvents.h"
     15 #include "mozilla/dom/DocumentInlines.h"
     16 #include "mozilla/dom/KeyboardEvent.h"
     17 #include "nsDebug.h"
     18 #include "nsMenuPopupFrame.h"
     19 #include "nsString.h"
     20 #include "nsStringFwd.h"
     21 #include "nsUTF8Utils.h"
     22 #include "nsXULElement.h"
     23 #include "nsXULPopupManager.h"
     24 
     25 namespace mozilla::dom {
     26 
     27 NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(XULMenuParentElement,
     28                                               nsXULElement)
     29 NS_IMPL_CYCLE_COLLECTION_INHERITED(XULMenuParentElement, nsXULElement,
     30                                   mActiveItem)
     31 
     32 XULMenuParentElement::XULMenuParentElement(
     33    already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
     34    : nsXULElement(std::move(aNodeInfo)) {}
     35 
     36 XULMenuParentElement::~XULMenuParentElement() = default;
     37 
     38 class MenuActivateEvent final : public Runnable {
     39 public:
     40  MenuActivateEvent(Element* aMenu, bool aIsActivate)
     41      : Runnable("MenuActivateEvent"), mMenu(aMenu), mIsActivate(aIsActivate) {}
     42 
     43  // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398)
     44  MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override {
     45    nsAutoString domEventToFire;
     46    if (mIsActivate) {
     47      // Highlight the menu.
     48      mMenu->SetAttr(kNameSpaceID_None, nsGkAtoms::menuactive, u"true"_ns,
     49                     true);
     50      // The menuactivated event is used by accessibility to track the user's
     51      // movements through menus
     52      domEventToFire.AssignLiteral("DOMMenuItemActive");
     53    } else {
     54      // Unhighlight the menu.
     55      mMenu->UnsetAttr(kNameSpaceID_None, nsGkAtoms::menuactive, true);
     56      domEventToFire.AssignLiteral("DOMMenuItemInactive");
     57    }
     58 
     59    RefPtr<nsPresContext> pc = mMenu->OwnerDoc()->GetPresContext();
     60    RefPtr<dom::Event> event = NS_NewDOMEvent(mMenu, pc, nullptr);
     61    event->InitEvent(domEventToFire, true, true);
     62 
     63    event->SetTrusted(true);
     64 
     65    EventDispatcher::DispatchDOMEvent(mMenu, nullptr, event, pc, nullptr);
     66    return NS_OK;
     67  }
     68 
     69 private:
     70  const RefPtr<Element> mMenu;
     71  bool mIsActivate;
     72 };
     73 
     74 static void ActivateOrDeactivate(XULButtonElement& aButton, bool aActivate) {
     75  if (!aButton.IsMenu()) {
     76    return;
     77  }
     78 
     79  if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) {
     80    if (aActivate) {
     81      // Cancel the close timer if selecting a menu within the popup to be
     82      // closed.
     83      pm->CancelMenuTimer(aButton.GetContainingPopupWithoutFlushing());
     84    } else if (auto* popup = aButton.GetMenuPopupWithoutFlushing()) {
     85      if (popup->IsOpen()) {
     86        // Set up the close timer if deselecting an open sub-menu.
     87        pm->HidePopupAfterDelay(popup, aButton.MenuOpenCloseDelay());
     88      }
     89    }
     90  }
     91 
     92  nsCOMPtr<nsIRunnable> event = new MenuActivateEvent(&aButton, aActivate);
     93  aButton.OwnerDoc()->Dispatch(event.forget());
     94 }
     95 
     96 XULButtonElement* XULMenuParentElement::GetContainingMenu() const {
     97  if (IsMenuBar()) {
     98    return nullptr;
     99  }
    100  auto* button = XULButtonElement::FromNodeOrNull(GetParent());
    101  if (!button || !button->IsMenu()) {
    102    return nullptr;
    103  }
    104  return button;
    105 }
    106 
    107 void XULMenuParentElement::LockMenuUntilClosed(bool aLock) {
    108  if (IsMenuBar()) {
    109    return;
    110  }
    111  mLocked = aLock;
    112  // Lock/Unlock the parent menu too.
    113  if (XULButtonElement* menu = GetContainingMenu()) {
    114    if (XULMenuParentElement* parent = menu->GetMenuParent()) {
    115      parent->LockMenuUntilClosed(aLock);
    116    }
    117  }
    118 }
    119 
    120 void XULMenuParentElement::SetActiveMenuChild(XULButtonElement* aChild,
    121                                              ByKey aByKey) {
    122  if (aChild == mActiveItem) {
    123    return;
    124  }
    125 
    126  if (mActiveItem) {
    127    ActivateOrDeactivate(*mActiveItem, false);
    128  }
    129  mActiveItem = nullptr;
    130 
    131  if (auto* menuBar = XULMenuBarElement::FromNode(*this)) {
    132    // KnownLive because `this` is known-live by definition.
    133    MOZ_KnownLive(menuBar)->SetActive(!!aChild);
    134  }
    135 
    136  if (!aChild) {
    137    return;
    138  }
    139 
    140  // When a menu opens a submenu, the mouse will often be moved onto a sibling
    141  // before moving onto an item within the submenu, causing the parent to become
    142  // deselected. We need to ensure that the parent menu is reselected when an
    143  // item in the submenu is selected.
    144  if (RefPtr menu = GetContainingMenu()) {
    145    if (RefPtr parent = menu->GetMenuParent()) {
    146      parent->SetActiveMenuChild(menu, aByKey);
    147    }
    148  }
    149 
    150  mActiveItem = aChild;
    151  ActivateOrDeactivate(*mActiveItem, true);
    152 
    153  if (IsInMenuList()) {
    154    if (nsMenuPopupFrame* f = do_QueryFrame(GetPrimaryFrame())) {
    155      f->EnsureActiveMenuListItemIsVisible();
    156 #ifdef XP_WIN
    157      // On Windows, a menulist should update its value whenever navigation was
    158      // done by the keyboard.
    159      //
    160      // NOTE(emilio): This is a rather odd per-platform behavior difference,
    161      // but other browsers also do this.
    162      if (aByKey == ByKey::Yes && f->IsOpen()) {
    163        // Fire a command event as the new item, but we don't want to close the
    164        // menu, blink it, or update any other state of the menuitem. The
    165        // command event will cause the item to be selected.
    166        RefPtr<mozilla::PresShell> presShell = OwnerDoc()->GetPresShell();
    167        nsContentUtils::DispatchXULCommand(aChild, /* aTrusted = */ true,
    168                                           nullptr, presShell, false, false,
    169                                           false, false);
    170      }
    171 #endif
    172    }
    173  }
    174 }
    175 
    176 static bool IsValidMenuItem(const XULMenuParentElement& aMenuParent,
    177                            const nsIContent& aContent) {
    178  const auto* button = XULButtonElement::FromNode(aContent);
    179  if (!button || !button->IsMenu()) {
    180    return false;
    181  }
    182  if (!button->GetPrimaryFrame()) {
    183    // Hidden buttons are not focusable/activatable.
    184    return false;
    185  }
    186  if (!button->IsDisabled()) {
    187    return true;
    188  }
    189  // In the menubar or a menulist disabled items are always skipped.
    190  const bool skipDisabled =
    191      LookAndFeel::GetInt(LookAndFeel::IntID::SkipNavigatingDisabledMenuItem) ||
    192      aMenuParent.IsMenuBar() || aMenuParent.IsInMenuList();
    193  return !skipDisabled;
    194 }
    195 
    196 enum class StartKind { Parent, Item };
    197 
    198 template <bool aForward>
    199 static XULButtonElement* DoGetNextMenuItem(
    200    const XULMenuParentElement& aMenuParent, const nsIContent& aStart,
    201    StartKind aStartKind) {
    202  nsIContent* start =
    203      aStartKind == StartKind::Item
    204          ? (aForward ? aStart.GetNextSibling() : aStart.GetPreviousSibling())
    205          : (aForward ? aStart.GetFirstChild() : aStart.GetLastChild());
    206  for (nsIContent* node = start; node;
    207       node = aForward ? node->GetNextSibling() : node->GetPreviousSibling()) {
    208    if (IsValidMenuItem(aMenuParent, *node)) {
    209      return static_cast<XULButtonElement*>(node);
    210    }
    211    if (node->IsXULElement(nsGkAtoms::menugroup)) {
    212      if (XULButtonElement* child = DoGetNextMenuItem<aForward>(
    213              aMenuParent, *node, StartKind::Parent)) {
    214        return child;
    215      }
    216    }
    217  }
    218  if (aStartKind == StartKind::Item && aStart.GetParent() &&
    219      aStart.GetParent()->IsXULElement(nsGkAtoms::menugroup)) {
    220    // We haven't found anything in aStart's sibling list, but if we're in a
    221    // group we need to keep looking.
    222    return DoGetNextMenuItem<aForward>(aMenuParent, *aStart.GetParent(),
    223                                       StartKind::Item);
    224  }
    225  return nullptr;
    226 }
    227 
    228 XULButtonElement* XULMenuParentElement::GetFirstMenuItem() const {
    229  return DoGetNextMenuItem<true>(*this, *this, StartKind::Parent);
    230 }
    231 
    232 XULButtonElement* XULMenuParentElement::GetLastMenuItem() const {
    233  return DoGetNextMenuItem<false>(*this, *this, StartKind::Parent);
    234 }
    235 
    236 XULButtonElement* XULMenuParentElement::GetNextMenuItemFrom(
    237    const XULButtonElement& aStartingItem) const {
    238  return DoGetNextMenuItem<true>(*this, aStartingItem, StartKind::Item);
    239 }
    240 
    241 XULButtonElement* XULMenuParentElement::GetPrevMenuItemFrom(
    242    const XULButtonElement& aStartingItem) const {
    243  return DoGetNextMenuItem<false>(*this, aStartingItem, StartKind::Item);
    244 }
    245 
    246 XULButtonElement* XULMenuParentElement::GetNextMenuItem(Wrap aWrap) const {
    247  if (mActiveItem) {
    248    if (auto* next = GetNextMenuItemFrom(*mActiveItem)) {
    249      return next;
    250    }
    251    if (aWrap == Wrap::No) {
    252      return nullptr;
    253    }
    254  }
    255  return GetFirstMenuItem();
    256 }
    257 
    258 XULButtonElement* XULMenuParentElement::GetPrevMenuItem(Wrap aWrap) const {
    259  if (mActiveItem) {
    260    if (auto* prev = GetPrevMenuItemFrom(*mActiveItem)) {
    261      return prev;
    262    }
    263    if (aWrap == Wrap::No) {
    264      return nullptr;
    265    }
    266  }
    267  return GetLastMenuItem();
    268 }
    269 
    270 void XULMenuParentElement::SelectFirstItem() {
    271  if (RefPtr firstItem = GetFirstMenuItem()) {
    272    SetActiveMenuChild(firstItem);
    273  }
    274 }
    275 
    276 XULButtonElement* XULMenuParentElement::FindMenuWithShortcut(
    277    KeyboardEvent& aKeyEvent) const {
    278  using AccessKeyArray = AutoTArray<uint32_t, 10>;
    279  AccessKeyArray accessKeys;
    280  WidgetKeyboardEvent* nativeKeyEvent =
    281      aKeyEvent.WidgetEventPtr()->AsKeyboardEvent();
    282  if (nativeKeyEvent) {
    283    nativeKeyEvent->GetAccessKeyCandidates(accessKeys);
    284  }
    285  const uint32_t charCode = aKeyEvent.CharCode();
    286  if (accessKeys.IsEmpty() && charCode) {
    287    accessKeys.AppendElement(charCode);
    288  }
    289  if (accessKeys.IsEmpty()) {
    290    return nullptr;  // no character was pressed so just return
    291  }
    292  XULButtonElement* foundMenu = nullptr;
    293  size_t foundIndex = AccessKeyArray::NoIndex;
    294  for (auto* item = GetFirstMenuItem(); item;
    295       item = GetNextMenuItemFrom(*item)) {
    296    nsAutoString shortcutKey;
    297    item->GetAttr(nsGkAtoms::accesskey, shortcutKey);
    298    if (shortcutKey.IsEmpty()) {
    299      continue;
    300    }
    301 
    302    ToLowerCase(shortcutKey);
    303    const char16_t* start = shortcutKey.BeginReading();
    304    const char16_t* end = shortcutKey.EndReading();
    305    uint32_t ch = UTF16CharEnumerator::NextChar(&start, end);
    306    size_t index = accessKeys.IndexOf(ch);
    307    if (index == AccessKeyArray::NoIndex) {
    308      continue;
    309    }
    310    if (foundIndex == AccessKeyArray::NoIndex || index < foundIndex) {
    311      foundMenu = item;
    312      foundIndex = index;
    313    }
    314  }
    315  return foundMenu;
    316 }
    317 
    318 XULButtonElement* XULMenuParentElement::FindMenuWithShortcut(
    319    const nsAString& aString, bool& aDoAction) const {
    320  aDoAction = false;
    321  uint32_t accessKeyMatchCount = 0;
    322  uint32_t matchCount = 0;
    323 
    324  XULButtonElement* foundAccessKeyMenu = nullptr;
    325  XULButtonElement* foundMenuBeforeCurrent = nullptr;
    326  XULButtonElement* foundMenuAfterCurrent = nullptr;
    327 
    328  bool foundActive = false;
    329  for (auto* item = GetFirstMenuItem(); item;
    330       item = GetNextMenuItemFrom(*item)) {
    331    nsAutoString textKey;
    332    // Get the shortcut attribute.
    333    item->GetAttr(nsGkAtoms::accesskey, textKey);
    334    const bool isAccessKey = !textKey.IsEmpty();
    335    if (textKey.IsEmpty()) {  // No shortcut, try first letter
    336      item->GetAttr(nsGkAtoms::label, textKey);
    337      if (textKey.IsEmpty()) {  // No label, try another attribute (value)
    338        item->GetAttr(nsGkAtoms::value, textKey);
    339      }
    340    }
    341 
    342    const bool isActive = item == GetActiveMenuChild();
    343    foundActive |= isActive;
    344 
    345    if (!StringBeginsWith(
    346            nsContentUtils::TrimWhitespace<
    347                nsContentUtils::IsHTMLWhitespaceOrNBSP>(textKey, false),
    348            aString, nsCaseInsensitiveStringComparator)) {
    349      continue;
    350    }
    351    // There is one match
    352    matchCount++;
    353    if (isAccessKey) {
    354      // There is one shortcut-key match
    355      accessKeyMatchCount++;
    356      // Record the matched item. If there is only one matched shortcut
    357      // item, do it
    358      foundAccessKeyMenu = item;
    359    }
    360    // Get the active status
    361    if (isActive && aString.Length() > 1 && !foundMenuBeforeCurrent) {
    362      // If there is more than one char typed and the current item matches, the
    363      // current item has highest priority, otherwise the item next to current
    364      // has highest priority.
    365      return item;
    366    }
    367    if (!foundActive || isActive) {
    368      // It's a first candidate item located before/on the current item
    369      if (!foundMenuBeforeCurrent) {
    370        foundMenuBeforeCurrent = item;
    371      }
    372    } else {
    373      if (!foundMenuAfterCurrent) {
    374        foundMenuAfterCurrent = item;
    375      }
    376    }
    377  }
    378 
    379  aDoAction = !IsInMenuList() && (matchCount == 1 || accessKeyMatchCount == 1);
    380 
    381  if (accessKeyMatchCount == 1) {
    382    // We have one matched accesskey item
    383    return foundAccessKeyMenu;
    384  }
    385  // If we have matched an item after the current, use it.
    386  if (foundMenuAfterCurrent) {
    387    return foundMenuAfterCurrent;
    388  }
    389  // If we haven't, use the item before the current, if any.
    390  return foundMenuBeforeCurrent;
    391 }
    392 
    393 void XULMenuParentElement::HandleEnterKeyPress(WidgetEvent& aEvent) {
    394  if (RefPtr child = GetActiveMenuChild()) {
    395    child->HandleEnterKeyPress(aEvent);
    396  }
    397 }
    398 
    399 }  // namespace mozilla::dom