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