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"
17 #include "nsMenuPopupFrame.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
,
28 NS_IMPL_CYCLE_COLLECTION_INHERITED(XULMenuParentElement
, nsXULElement
,
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
{
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
;
46 // Highlight the menu.
47 mMenu
->SetAttr(kNameSpaceID_None
, nsGkAtoms::menuactive
, u
"true"_ns
,
49 // The menuactivated event is used by accessibility to track the user's
50 // movements through menus
51 domEventToFire
.AssignLiteral("DOMMenuItemActive");
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);
69 const RefPtr
<Element
> mMenu
;
73 static void ActivateOrDeactivate(XULButtonElement
& aButton
, bool aActivate
) {
74 if (!aButton
.IsMenu()) {
78 if (nsXULPopupManager
* pm
= nsXULPopupManager::GetInstance()) {
80 // Cancel the close timer if selecting a menu within the popup to be
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 {
99 auto* button
= XULButtonElement::FromNodeOrNull(GetParent());
100 if (!button
|| !button
->IsMenu()) {
106 void XULMenuParentElement::LockMenuUntilClosed(bool 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
,
121 if (aChild
== 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
);
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();
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,
175 static bool IsValidMenuItem(const XULMenuParentElement
& aMenuParent
,
176 const nsIContent
& aContent
) {
177 const auto* button
= XULButtonElement::FromNode(aContent
);
178 if (!button
|| !button
->IsMenu()) {
181 if (!button
->GetPrimaryFrame()) {
182 // Hidden buttons are not focusable/activatable.
185 if (!button
->IsDisabled()) {
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
) {
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
)) {
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(),
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 {
247 if (auto* next
= GetNextMenuItemFrom(*mActiveItem
)) {
250 if (aWrap
== Wrap::No
) {
254 return GetFirstMenuItem();
257 XULButtonElement
* XULMenuParentElement::GetPrevMenuItem(Wrap aWrap
) const {
259 if (auto* prev
= GetPrevMenuItemFrom(*mActiveItem
)) {
262 if (aWrap
== Wrap::No
) {
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()) {
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
) {
309 if (foundIndex
== AccessKeyArray::NoIndex
|| index
< foundIndex
) {
317 XULButtonElement
* XULMenuParentElement::FindMenuWithShortcut(
318 const nsAString
& aString
, bool& aDoAction
) const {
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
)) {
350 // There is one match
353 // There is one shortcut-key match
354 accessKeyMatchCount
++;
355 // Record the matched item. If there is only one matched shortcut
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.
366 if (!foundActive
|| isActive
) {
367 // It's a first candidate item located before/on the current item
368 if (!foundMenuBeforeCurrent
) {
369 foundMenuBeforeCurrent
= item
;
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