1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "MenuBarListener.h"
8 #include "XULButtonElement.h"
9 #include "mozilla/Attributes.h"
12 // Drag & Drop, Clipboard
13 #include "nsWidgetsCID.h"
16 #include "nsContentUtils.h"
17 #include "nsPIWindowRoot.h"
19 #include "mozilla/BasicEvents.h"
20 #include "mozilla/Preferences.h"
21 #include "mozilla/LookAndFeel.h"
22 #include "mozilla/StaticPrefs_ui.h"
23 #include "mozilla/TextEvents.h"
24 #include "mozilla/dom/Document.h"
25 #include "mozilla/dom/Event.h"
26 #include "mozilla/dom/EventBinding.h"
27 #include "mozilla/dom/KeyboardEvent.h"
28 #include "mozilla/dom/KeyboardEventBinding.h"
29 #include "mozilla/dom/XULButtonElement.h"
30 #include "mozilla/dom/XULMenuBarElement.h"
31 #include "mozilla/dom/XULMenuParentElement.h"
32 #include "nsXULPopupManager.h"
34 namespace mozilla::dom
{
36 NS_IMPL_ISUPPORTS(MenuBarListener
, nsIDOMEventListener
)
38 MenuBarListener::MenuBarListener(XULMenuBarElement
& aElement
)
39 : mMenuBar(&aElement
),
40 mEventTarget(aElement
.GetComposedDoc()),
41 mAccessKeyDown(false),
42 mAccessKeyDownCanceled(false) {
43 MOZ_ASSERT(mEventTarget
);
46 // Hook up the menubar as a key listener on the whole document. This will
47 // see every keypress that occurs, but after everyone else does.
49 // Also hook up the listener to the window listening for focus events. This
50 // is so we can keep proper state as the user alt-tabs through processes.
52 mEventTarget
->AddSystemEventListener(u
"keypress"_ns
, this, false);
53 mEventTarget
->AddSystemEventListener(u
"keydown"_ns
, this, false);
54 mEventTarget
->AddSystemEventListener(u
"keyup"_ns
, this, false);
55 mEventTarget
->AddSystemEventListener(u
"mozaccesskeynotfound"_ns
, this, false);
56 // Need a capturing event listener if the user has blocked pages from
57 // overriding system keys so that we can prevent menu accesskeys from being
59 mEventTarget
->AddEventListener(u
"keydown"_ns
, this, true);
61 // mousedown event should be handled in all phase
62 mEventTarget
->AddEventListener(u
"mousedown"_ns
, this, true);
63 mEventTarget
->AddEventListener(u
"mousedown"_ns
, this, false);
64 mEventTarget
->AddEventListener(u
"blur"_ns
, this, true);
66 mEventTarget
->AddEventListener(u
"MozDOMFullscreen:Entered"_ns
, this, false);
68 // Needs to listen to the deactivate event of the window.
69 RefPtr
<EventTarget
> top
= nsContentUtils::GetWindowRoot(mEventTarget
);
70 if (!NS_WARN_IF(!top
)) {
71 top
->AddSystemEventListener(u
"deactivate"_ns
, this, true);
75 ////////////////////////////////////////////////////////////////////////
76 MenuBarListener::~MenuBarListener() {
77 MOZ_ASSERT(!mEventTarget
, "Should've detached always");
80 void MenuBarListener::Detach() {
82 MOZ_ASSERT(!mEventTarget
);
85 mEventTarget
->RemoveSystemEventListener(u
"keypress"_ns
, this, false);
86 mEventTarget
->RemoveSystemEventListener(u
"keydown"_ns
, this, false);
87 mEventTarget
->RemoveSystemEventListener(u
"keyup"_ns
, this, false);
88 mEventTarget
->RemoveSystemEventListener(u
"mozaccesskeynotfound"_ns
, this,
90 mEventTarget
->RemoveEventListener(u
"keydown"_ns
, this, true);
92 mEventTarget
->RemoveEventListener(u
"mousedown"_ns
, this, true);
93 mEventTarget
->RemoveEventListener(u
"mousedown"_ns
, this, false);
94 mEventTarget
->RemoveEventListener(u
"blur"_ns
, this, true);
96 mEventTarget
->RemoveEventListener(u
"MozDOMFullscreen:Entered"_ns
, this,
98 RefPtr
<EventTarget
> top
= nsContentUtils::GetWindowRoot(mEventTarget
);
99 if (!NS_WARN_IF(!top
)) {
100 top
->RemoveSystemEventListener(u
"deactivate"_ns
, this, true);
103 mEventTarget
= nullptr;
106 void MenuBarListener::ToggleMenuActiveState(ByKeyboard aByKeyboard
) {
107 RefPtr menuBar
= mMenuBar
;
108 if (menuBar
->IsActive()) {
109 menuBar
->SetActive(false);
111 if (aByKeyboard
== ByKeyboard::Yes
) {
112 menuBar
->SetActiveByKeyboard();
114 // This will activate the menubar if needed.
115 menuBar
->SelectFirstItem();
119 ////////////////////////////////////////////////////////////////////////
120 nsresult
MenuBarListener::KeyUp(Event
* aKeyEvent
) {
121 WidgetKeyboardEvent
* nativeKeyEvent
=
122 aKeyEvent
->WidgetEventPtr()->AsKeyboardEvent();
123 if (!nativeKeyEvent
) {
127 // handlers shouldn't be triggered by non-trusted events.
128 if (!nativeKeyEvent
->IsTrusted()) {
132 const auto accessKey
= LookAndFeel::GetMenuAccessKey();
133 if (!accessKey
|| !StaticPrefs::ui_key_menuAccessKeyFocuses()) {
137 // On a press of the ALT key by itself, we toggle the menu's
138 // active/inactive state.
139 if (!nativeKeyEvent
->DefaultPrevented() && mAccessKeyDown
&&
140 !mAccessKeyDownCanceled
&& nativeKeyEvent
->mKeyCode
== accessKey
) {
141 // The access key was down and is now up, and no other
142 // keys were pressed in between.
143 bool toggleMenuActiveState
= true;
144 if (!mMenuBar
->IsActive()) {
145 // If the focused content is in a remote process, we should allow the
146 // focused web app to prevent to activate the menubar.
147 if (nativeKeyEvent
->WillBeSentToRemoteProcess()) {
148 nativeKeyEvent
->StopImmediatePropagation();
149 nativeKeyEvent
->MarkAsWaitingReplyFromRemoteProcess();
152 // First, close all existing popups because other popups shouldn't
153 // handle key events when menubar is active and IME should be
155 if (nsXULPopupManager
* pm
= nsXULPopupManager::GetInstance()) {
158 // If menubar active state is changed or the menubar is destroyed
159 // during closing the popups, we should do nothing anymore.
160 toggleMenuActiveState
= !Destroyed() && !mMenuBar
->IsActive();
162 if (toggleMenuActiveState
) {
163 ToggleMenuActiveState(ByKeyboard::Yes
);
167 mAccessKeyDown
= false;
168 mAccessKeyDownCanceled
= false;
170 if (!Destroyed() && mMenuBar
->IsActive()) {
171 nativeKeyEvent
->StopPropagation();
172 nativeKeyEvent
->PreventDefault();
178 ////////////////////////////////////////////////////////////////////////
179 nsresult
MenuBarListener::KeyPress(Event
* aKeyEvent
) {
180 // if event has already been handled, bail
181 if (!aKeyEvent
|| aKeyEvent
->DefaultPrevented()) {
182 return NS_OK
; // don't consume event
185 // handlers shouldn't be triggered by non-trusted events.
186 if (!aKeyEvent
->IsTrusted()) {
190 const auto accessKey
= LookAndFeel::GetMenuAccessKey();
194 // If accesskey handling was forwarded to a child process, wait for
195 // the mozaccesskeynotfound event before handling accesskeys.
196 WidgetKeyboardEvent
* nativeKeyEvent
=
197 aKeyEvent
->WidgetEventPtr()->AsKeyboardEvent();
198 if (!nativeKeyEvent
) {
202 RefPtr
<KeyboardEvent
> keyEvent
= aKeyEvent
->AsKeyboardEvent();
203 uint32_t keyCode
= keyEvent
->KeyCode();
205 // Cancel the access key flag unless we are pressing the access key.
206 if (keyCode
!= accessKey
) {
207 mAccessKeyDownCanceled
= true;
211 // Need to handle F10 specially on Non-Mac platform.
212 if (nativeKeyEvent
->mMessage
== eKeyPress
&& keyCode
== NS_VK_F10
) {
213 if ((keyEvent
->GetModifiersForMenuAccessKey() & ~MODIFIER_CONTROL
) == 0) {
214 // If the keyboard event should activate the menubar and will be
215 // sent to a remote process, it should be executed with reply
216 // event from the focused remote process. Note that if the menubar
217 // is active, the event is already marked as "stop cross
218 // process dispatching". So, in that case, this won't wait
219 // reply from the remote content.
220 if (nativeKeyEvent
->WillBeSentToRemoteProcess()) {
221 nativeKeyEvent
->StopImmediatePropagation();
222 nativeKeyEvent
->MarkAsWaitingReplyFromRemoteProcess();
225 // The F10 key just went down by itself or with ctrl pressed.
226 // In Windows, both of these activate the menu bar.
227 ToggleMenuActiveState(ByKeyboard::Yes
);
229 if (mMenuBar
&& mMenuBar
->IsActive()) {
230 # ifdef MOZ_WIDGET_GTK
231 if (RefPtr child
= mMenuBar
->GetActiveMenuChild()) {
232 // In GTK, this also opens the first menu.
233 child
->OpenMenuPopup(false);
236 aKeyEvent
->StopPropagation();
237 aKeyEvent
->PreventDefault();
245 RefPtr menuForKey
= GetMenuForKeyEvent(*keyEvent
);
248 // Behavior on Windows - this item is on the menu bar, beep and deactivate
250 // TODO(emilio): This is rather odd, and I cannot get the beep to work,
251 // but this matches what old code was doing...
252 if (mMenuBar
&& mMenuBar
->IsActive() && mMenuBar
->IsActiveByKeyboard()) {
253 if (nsCOMPtr
<nsISound
> sound
= do_GetService("@mozilla.org/sound;1")) {
256 ToggleMenuActiveState(ByKeyboard::Yes
);
262 // If the keyboard event matches with a menu item's accesskey and
263 // will be sent to a remote process, it should be executed with
264 // reply event from the focused remote process. Note that if the
265 // menubar is active, the event is already marked as "stop cross
266 // process dispatching". So, in that case, this won't wait
267 // reply from the remote content.
268 if (nativeKeyEvent
->WillBeSentToRemoteProcess()) {
269 nativeKeyEvent
->StopImmediatePropagation();
270 nativeKeyEvent
->MarkAsWaitingReplyFromRemoteProcess();
274 RefPtr menuBar
= mMenuBar
;
275 menuBar
->SetActiveByKeyboard();
276 // This will activate the menubar as needed.
277 menuForKey
->OpenMenuPopup(true);
279 // The opened menu will listen next keyup event.
280 // Therefore, we should clear the keydown flags here.
281 mAccessKeyDown
= mAccessKeyDownCanceled
= false;
283 aKeyEvent
->StopPropagation();
284 aKeyEvent
->PreventDefault();
288 dom::XULButtonElement
* MenuBarListener::GetMenuForKeyEvent(
289 KeyboardEvent
& aKeyEvent
) {
290 if (!aKeyEvent
.IsMenuAccessKeyPressed()) {
294 uint32_t charCode
= aKeyEvent
.CharCode();
295 bool hasAccessKeyCandidates
= charCode
!= 0;
296 if (!hasAccessKeyCandidates
) {
297 WidgetKeyboardEvent
* nativeKeyEvent
=
298 aKeyEvent
.WidgetEventPtr()->AsKeyboardEvent();
299 AutoTArray
<uint32_t, 10> keys
;
300 nativeKeyEvent
->GetAccessKeyCandidates(keys
);
301 hasAccessKeyCandidates
= !keys
.IsEmpty();
304 if (!hasAccessKeyCandidates
) {
307 // Do shortcut navigation.
308 // A letter was pressed. We want to see if a shortcut gets matched. If
309 // so, we'll know the menu got activated.
310 return mMenuBar
->FindMenuWithShortcut(aKeyEvent
);
313 void MenuBarListener::ReserveKeyIfNeeded(Event
* aKeyEvent
) {
314 WidgetKeyboardEvent
* nativeKeyEvent
=
315 aKeyEvent
->WidgetEventPtr()->AsKeyboardEvent();
316 if (nsContentUtils::ShouldBlockReservedKeys(nativeKeyEvent
)) {
317 nativeKeyEvent
->MarkAsReservedByChrome();
321 ////////////////////////////////////////////////////////////////////////
322 nsresult
MenuBarListener::KeyDown(Event
* aKeyEvent
) {
323 // handlers shouldn't be triggered by non-trusted events.
324 if (!aKeyEvent
|| !aKeyEvent
->IsTrusted()) {
328 RefPtr
<KeyboardEvent
> keyEvent
= aKeyEvent
->AsKeyboardEvent();
333 uint32_t theChar
= keyEvent
->KeyCode();
334 uint16_t eventPhase
= keyEvent
->EventPhase();
335 bool capturing
= (eventPhase
== dom::Event_Binding::CAPTURING_PHASE
);
338 if (capturing
&& !mAccessKeyDown
&& theChar
== NS_VK_F10
&&
339 (keyEvent
->GetModifiersForMenuAccessKey() & ~MODIFIER_CONTROL
) == 0) {
340 ReserveKeyIfNeeded(aKeyEvent
);
344 const auto accessKey
= LookAndFeel::GetMenuAccessKey();
345 if (accessKey
&& StaticPrefs::ui_key_menuAccessKeyFocuses()) {
346 bool defaultPrevented
= aKeyEvent
->DefaultPrevented();
348 // No other modifiers can be down.
349 // Especially CTRL. CTRL+ALT == AltGR, and we'll break on non-US
350 // enhanced 102-key keyboards if we don't check this.
351 bool isAccessKeyDownEvent
=
352 (theChar
== accessKey
&&
353 (keyEvent
->GetModifiersForMenuAccessKey() &
354 ~LookAndFeel::GetMenuAccessKeyModifiers()) == 0);
356 if (!capturing
&& !mAccessKeyDown
) {
357 // If accesskey isn't being pressed and the key isn't the accesskey,
359 if (!isAccessKeyDownEvent
) {
363 // Otherwise, accept the accesskey state.
364 mAccessKeyDown
= true;
365 // If default is prevented already, cancel the access key down.
366 mAccessKeyDownCanceled
= defaultPrevented
;
370 // If the pressed accesskey was canceled already or the event was
371 // consumed already, ignore the event.
372 if (mAccessKeyDownCanceled
|| defaultPrevented
) {
376 // Some key other than the access key just went down,
377 // so we won't activate the menu bar when the access key is released.
378 mAccessKeyDownCanceled
= !isAccessKeyDownEvent
;
381 if (capturing
&& accessKey
) {
382 if (GetMenuForKeyEvent(*keyEvent
)) {
383 ReserveKeyIfNeeded(aKeyEvent
);
387 return NS_OK
; // means I am NOT consuming event
390 ////////////////////////////////////////////////////////////////////////
392 nsresult
MenuBarListener::Blur(Event
* aEvent
) {
393 if (!IsMenuOpen() && mMenuBar
->IsActive()) {
394 ToggleMenuActiveState(ByKeyboard::No
);
395 mAccessKeyDown
= false;
396 mAccessKeyDownCanceled
= false;
398 return NS_OK
; // means I am NOT consuming event
401 ////////////////////////////////////////////////////////////////////////
403 nsresult
MenuBarListener::OnWindowDeactivated(Event
* aEvent
) {
404 // Reset the accesskey state because we cannot receive the keyup event for
405 // the pressing accesskey.
406 mAccessKeyDown
= false;
407 mAccessKeyDownCanceled
= false;
408 return NS_OK
; // means I am NOT consuming event
411 bool MenuBarListener::IsMenuOpen() const {
412 auto* activeChild
= mMenuBar
->GetActiveMenuChild();
413 return activeChild
&& activeChild
->IsMenuPopupOpen();
416 ////////////////////////////////////////////////////////////////////////
417 nsresult
MenuBarListener::MouseDown(Event
* aMouseEvent
) {
418 // NOTE: MouseDown method listens all phases
420 // Even if the mousedown event is canceled, it means the user don't want
421 // to activate the menu. Therefore, we need to record it at capturing (or
423 if (mAccessKeyDown
) {
424 mAccessKeyDownCanceled
= true;
427 // Don't do anything at capturing phase, any behavior should be cancelable.
428 if (aMouseEvent
->EventPhase() == dom::Event_Binding::CAPTURING_PHASE
) {
432 if (!IsMenuOpen() && mMenuBar
->IsActive()) {
433 ToggleMenuActiveState(ByKeyboard::No
);
436 return NS_OK
; // means I am NOT consuming event
439 ////////////////////////////////////////////////////////////////////////
441 nsresult
MenuBarListener::Fullscreen(Event
* aEvent
) {
442 if (mMenuBar
->IsActive()) {
443 ToggleMenuActiveState(ByKeyboard::No
);
448 ////////////////////////////////////////////////////////////////////////
449 MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult
450 MenuBarListener::HandleEvent(Event
* aEvent
) {
451 // If the menu bar is collapsed, don't do anything.
452 if (!mMenuBar
|| !mMenuBar
->GetPrimaryFrame() ||
453 !mMenuBar
->GetPrimaryFrame()->StyleVisibility()->IsVisible()) {
457 nsAutoString eventType
;
458 aEvent
->GetType(eventType
);
460 if (eventType
.EqualsLiteral("keyup")) {
461 return KeyUp(aEvent
);
463 if (eventType
.EqualsLiteral("keydown")) {
464 return KeyDown(aEvent
);
466 if (eventType
.EqualsLiteral("keypress")) {
467 return KeyPress(aEvent
);
469 if (eventType
.EqualsLiteral("mozaccesskeynotfound")) {
470 return KeyPress(aEvent
);
472 if (eventType
.EqualsLiteral("blur")) {
475 if (eventType
.EqualsLiteral("deactivate")) {
476 return OnWindowDeactivated(aEvent
);
478 if (eventType
.EqualsLiteral("mousedown")) {
479 return MouseDown(aEvent
);
481 if (eventType
.EqualsLiteral("MozDOMFullscreen:Entered")) {
482 return Fullscreen(aEvent
);
485 MOZ_ASSERT_UNREACHABLE("Unexpected eventType");
489 } // namespace mozilla::dom