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