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/. */
6
7 #include "nsMenuBarListener.h"
8 #include "nsMenuBarFrame.h"
9 #include "nsMenuPopupFrame.h"
10 #include "nsPIWindowRoot.h"
11
12 // Drag & Drop, Clipboard
13 #include "nsWidgetsCID.h"
14 #include "nsCOMPtr.h"
15 #include "nsIContent.h"
16
17 #include "nsContentUtils.h"
18 #include "mozilla/BasicEvents.h"
19 #include "mozilla/Preferences.h"
20 #include "mozilla/StaticPrefs_ui.h"
21 #include "mozilla/TextEvents.h"
22 #include "mozilla/dom/Event.h"
23 #include "mozilla/dom/EventBinding.h"
24 #include "mozilla/dom/KeyboardEvent.h"
25 #include "mozilla/dom/KeyboardEventBinding.h"
26
27 using namespace mozilla;
28 using mozilla::dom::Event;
29 using mozilla::dom::KeyboardEvent;
30
31 /*
32 * nsMenuBarListener implementation
33 */
34
35 NS_IMPL_ISUPPORTS(nsMenuBarListener, nsIDOMEventListener)
36
37 ////////////////////////////////////////////////////////////////////////
38
39 int32_t nsMenuBarListener::mAccessKey = -1;
40 Modifiers nsMenuBarListener::mAccessKeyMask = 0;
41
nsMenuBarListener(nsMenuBarFrame * aMenuBarFrame,nsIContent * aMenuBarContent)42 nsMenuBarListener::nsMenuBarListener(nsMenuBarFrame* aMenuBarFrame,
43 nsIContent* aMenuBarContent)
44 : mMenuBarFrame(aMenuBarFrame),
45 mEventTarget(aMenuBarContent ? aMenuBarContent->GetComposedDoc()
46 : nullptr),
47 mTopWindowEventTarget(nullptr),
48 mAccessKeyDown(false),
49 mAccessKeyDownCanceled(false) {
50 MOZ_ASSERT(mEventTarget);
51
52 // Hook up the menubar as a key listener on the whole document. This will
53 // see every keypress that occurs, but after everyone else does.
54
55 // Also hook up the listener to the window listening for focus events. This
56 // is so we can keep proper state as the user alt-tabs through processes.
57
58 mEventTarget->AddSystemEventListener(u"keypress"_ns, this, false);
59 mEventTarget->AddSystemEventListener(u"keydown"_ns, this, false);
60 mEventTarget->AddSystemEventListener(u"keyup"_ns, this, false);
61 mEventTarget->AddSystemEventListener(u"mozaccesskeynotfound"_ns, this, false);
62 // Need a capturing event listener if the user has blocked pages from
63 // overriding system keys so that we can prevent menu accesskeys from being
64 // cancelled.
65 mEventTarget->AddEventListener(u"keydown"_ns, this, true);
66
67 // mousedown event should be handled in all phase
68 mEventTarget->AddEventListener(u"mousedown"_ns, this, true);
69 mEventTarget->AddEventListener(u"mousedown"_ns, this, false);
70 mEventTarget->AddEventListener(u"blur"_ns, this, true);
71
72 mEventTarget->AddEventListener(u"MozDOMFullscreen:Entered"_ns, this, false);
73
74 // Needs to listen to the deactivate event of the window.
75 RefPtr<dom::EventTarget> topWindowEventTarget =
76 nsContentUtils::GetWindowRoot(aMenuBarContent->GetComposedDoc());
77 mTopWindowEventTarget = topWindowEventTarget.get();
78
79 mTopWindowEventTarget->AddSystemEventListener(u"deactivate"_ns, this, true);
80 }
81
82 ////////////////////////////////////////////////////////////////////////
~nsMenuBarListener()83 nsMenuBarListener::~nsMenuBarListener() {
84 MOZ_ASSERT(!mEventTarget,
85 "OnDestroyMenuBarFrame() should've alreay been called");
86 }
87
OnDestroyMenuBarFrame()88 void nsMenuBarListener::OnDestroyMenuBarFrame() {
89 mEventTarget->RemoveSystemEventListener(u"keypress"_ns, this, false);
90 mEventTarget->RemoveSystemEventListener(u"keydown"_ns, this, false);
91 mEventTarget->RemoveSystemEventListener(u"keyup"_ns, this, false);
92 mEventTarget->RemoveSystemEventListener(u"mozaccesskeynotfound"_ns, this,
93 false);
94 mEventTarget->RemoveEventListener(u"keydown"_ns, this, true);
95
96 mEventTarget->RemoveEventListener(u"mousedown"_ns, this, true);
97 mEventTarget->RemoveEventListener(u"mousedown"_ns, this, false);
98 mEventTarget->RemoveEventListener(u"blur"_ns, this, true);
99
100 mEventTarget->RemoveEventListener(u"MozDOMFullscreen:Entered"_ns, this,
101 false);
102
103 mTopWindowEventTarget->RemoveSystemEventListener(u"deactivate"_ns, this,
104 true);
105
106 mMenuBarFrame = nullptr;
107 mEventTarget = nullptr;
108 mTopWindowEventTarget = nullptr;
109 }
110
GetMenuAccessKey(int32_t * aAccessKey)111 nsresult nsMenuBarListener::GetMenuAccessKey(int32_t* aAccessKey) {
112 if (!aAccessKey) return NS_ERROR_INVALID_POINTER;
113 InitAccessKey();
114 *aAccessKey = mAccessKey;
115 return NS_OK;
116 }
117
InitAccessKey()118 void nsMenuBarListener::InitAccessKey() {
119 if (mAccessKey >= 0) return;
120
121 // Compiled-in defaults, in case we can't get LookAndFeel --
122 // mac doesn't have menu shortcuts, other platforms use alt.
123 #ifdef XP_MACOSX
124 mAccessKey = 0;
125 mAccessKeyMask = 0;
126 #else
127 mAccessKey = dom::KeyboardEvent_Binding::DOM_VK_ALT;
128 mAccessKeyMask = MODIFIER_ALT;
129 #endif
130
131 // Get the menu access key value from prefs, overriding the default:
132 mAccessKey = Preferences::GetInt("ui.key.menuAccessKey", mAccessKey);
133 switch (mAccessKey) {
134 case dom::KeyboardEvent_Binding::DOM_VK_SHIFT:
135 mAccessKeyMask = MODIFIER_SHIFT;
136 break;
137 case dom::KeyboardEvent_Binding::DOM_VK_CONTROL:
138 mAccessKeyMask = MODIFIER_CONTROL;
139 break;
140 case dom::KeyboardEvent_Binding::DOM_VK_ALT:
141 mAccessKeyMask = MODIFIER_ALT;
142 break;
143 case dom::KeyboardEvent_Binding::DOM_VK_META:
144 mAccessKeyMask = MODIFIER_META;
145 break;
146 case dom::KeyboardEvent_Binding::DOM_VK_WIN:
147 mAccessKeyMask = MODIFIER_OS;
148 break;
149 default:
150 // Don't touch mAccessKeyMask.
151 break;
152 }
153 }
154
ToggleMenuActiveState()155 void nsMenuBarListener::ToggleMenuActiveState() {
156 nsMenuFrame* closemenu = mMenuBarFrame->ToggleMenuActiveState();
157 nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
158 if (pm && closemenu) {
159 nsMenuPopupFrame* popupFrame = closemenu->GetPopup();
160 if (popupFrame)
161 pm->HidePopup(popupFrame->GetContent(), false, false, true, false);
162 }
163 }
164
165 ////////////////////////////////////////////////////////////////////////
KeyUp(Event * aKeyEvent)166 nsresult nsMenuBarListener::KeyUp(Event* aKeyEvent) {
167 WidgetKeyboardEvent* nativeKeyEvent =
168 aKeyEvent->WidgetEventPtr()->AsKeyboardEvent();
169 if (!nativeKeyEvent) {
170 return NS_OK;
171 }
172
173 InitAccessKey();
174
175 // handlers shouldn't be triggered by non-trusted events.
176 if (!nativeKeyEvent->IsTrusted()) {
177 return NS_OK;
178 }
179
180 if (!mAccessKey || !StaticPrefs::ui_key_menuAccessKeyFocuses()) {
181 return NS_OK;
182 }
183
184 // On a press of the ALT key by itself, we toggle the menu's
185 // active/inactive state.
186 if (!nativeKeyEvent->DefaultPrevented() && mAccessKeyDown &&
187 !mAccessKeyDownCanceled &&
188 static_cast<int32_t>(nativeKeyEvent->mKeyCode) == mAccessKey) {
189 // The access key was down and is now up, and no other
190 // keys were pressed in between.
191 bool toggleMenuActiveState = true;
192 if (!mMenuBarFrame->IsActive()) {
193 // If the focused content is in a remote process, we should allow the
194 // focused web app to prevent to activate the menubar.
195 if (nativeKeyEvent->WillBeSentToRemoteProcess()) {
196 nativeKeyEvent->StopImmediatePropagation();
197 nativeKeyEvent->MarkAsWaitingReplyFromRemoteProcess();
198 return NS_OK;
199 }
200 // First, close all existing popups because other popups shouldn't
201 // handle key events when menubar is active and IME should be
202 // disabled.
203 nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
204 if (pm) {
205 pm->Rollup(0, false, nullptr, nullptr);
206 }
207 // If menubar active state is changed or the menubar is destroyed
208 // during closing the popups, we should do nothing anymore.
209 toggleMenuActiveState = !Destroyed() && !mMenuBarFrame->IsActive();
210 }
211 if (toggleMenuActiveState) {
212 if (!mMenuBarFrame->IsActive()) {
213 mMenuBarFrame->SetActiveByKeyboard();
214 }
215 ToggleMenuActiveState();
216 }
217 }
218
219 mAccessKeyDown = false;
220 mAccessKeyDownCanceled = false;
221
222 if (!Destroyed() && mMenuBarFrame->IsActive()) {
223 nativeKeyEvent->StopPropagation();
224 nativeKeyEvent->PreventDefault();
225 }
226
227 return NS_OK;
228 }
229
230 ////////////////////////////////////////////////////////////////////////
KeyPress(Event * aKeyEvent)231 nsresult nsMenuBarListener::KeyPress(Event* aKeyEvent) {
232 // if event has already been handled, bail
233 if (!aKeyEvent || aKeyEvent->DefaultPrevented()) {
234 return NS_OK; // don't consume event
235 }
236
237 // handlers shouldn't be triggered by non-trusted events.
238 if (!aKeyEvent->IsTrusted()) {
239 return NS_OK;
240 }
241
242 InitAccessKey();
243
244 if (mAccessKey) {
245 // If accesskey handling was forwarded to a child process, wait for
246 // the mozaccesskeynotfound event before handling accesskeys.
247 WidgetKeyboardEvent* nativeKeyEvent =
248 aKeyEvent->WidgetEventPtr()->AsKeyboardEvent();
249 if (!nativeKeyEvent) {
250 return NS_OK;
251 }
252
253 RefPtr<KeyboardEvent> keyEvent = aKeyEvent->AsKeyboardEvent();
254 uint32_t keyCode = keyEvent->KeyCode();
255
256 // Cancel the access key flag unless we are pressing the access key.
257 if (keyCode != (uint32_t)mAccessKey) {
258 mAccessKeyDownCanceled = true;
259 }
260
261 #ifndef XP_MACOSX
262 // Need to handle F10 specially on Non-Mac platform.
263 if (nativeKeyEvent->mMessage == eKeyPress && keyCode == NS_VK_F10) {
264 if ((GetModifiersForAccessKey(keyEvent) & ~MODIFIER_CONTROL) == 0) {
265 // If the keyboard event should activate the menubar and will be
266 // sent to a remote process, it should be executed with reply
267 // event from the focused remote process. Note that if the menubar
268 // is active, the event is already marked as "stop cross
269 // process dispatching". So, in that case, this won't wait
270 // reply from the remote content.
271 if (nativeKeyEvent->WillBeSentToRemoteProcess()) {
272 nativeKeyEvent->StopImmediatePropagation();
273 nativeKeyEvent->MarkAsWaitingReplyFromRemoteProcess();
274 return NS_OK;
275 }
276 // The F10 key just went down by itself or with ctrl pressed.
277 // In Windows, both of these activate the menu bar.
278 mMenuBarFrame->SetActiveByKeyboard();
279 ToggleMenuActiveState();
280
281 if (mMenuBarFrame->IsActive()) {
282 # ifdef MOZ_WIDGET_GTK
283 // In GTK, this also opens the first menu.
284 mMenuBarFrame->GetCurrentMenuItem()->OpenMenu(false);
285 # endif
286 aKeyEvent->StopPropagation();
287 aKeyEvent->PreventDefault();
288 }
289 }
290
291 return NS_OK;
292 }
293 #endif // !XP_MACOSX
294
295 nsMenuFrame* menuFrameForKey = GetMenuForKeyEvent(keyEvent, false);
296 if (!menuFrameForKey) {
297 return NS_OK;
298 }
299
300 // If the keyboard event matches with a menu item's accesskey and
301 // will be sent to a remote process, it should be executed with
302 // reply event from the focused remote process. Note that if the
303 // menubar is active, the event is already marked as "stop cross
304 // process dispatching". So, in that case, this won't wait
305 // reply from the remote content.
306 if (nativeKeyEvent->WillBeSentToRemoteProcess()) {
307 nativeKeyEvent->StopImmediatePropagation();
308 nativeKeyEvent->MarkAsWaitingReplyFromRemoteProcess();
309 return NS_OK;
310 }
311
312 mMenuBarFrame->SetActiveByKeyboard();
313 mMenuBarFrame->SetActive(true);
314 menuFrameForKey->OpenMenu(true);
315
316 // The opened menu will listen next keyup event.
317 // Therefore, we should clear the keydown flags here.
318 mAccessKeyDown = mAccessKeyDownCanceled = false;
319
320 aKeyEvent->StopPropagation();
321 aKeyEvent->PreventDefault();
322 }
323
324 return NS_OK;
325 }
326
IsAccessKeyPressed(KeyboardEvent * aKeyEvent)327 bool nsMenuBarListener::IsAccessKeyPressed(KeyboardEvent* aKeyEvent) {
328 InitAccessKey();
329 // No other modifiers are allowed to be down except for Shift.
330 uint32_t modifiers = GetModifiersForAccessKey(aKeyEvent);
331
332 return (mAccessKeyMask != MODIFIER_SHIFT && (modifiers & mAccessKeyMask) &&
333 (modifiers & ~(mAccessKeyMask | MODIFIER_SHIFT)) == 0);
334 }
335
GetModifiersForAccessKey(KeyboardEvent * aKeyEvent)336 Modifiers nsMenuBarListener::GetModifiersForAccessKey(
337 KeyboardEvent* aKeyEvent) {
338 WidgetInputEvent* inputEvent = aKeyEvent->WidgetEventPtr()->AsInputEvent();
339 MOZ_ASSERT(inputEvent);
340
341 static const Modifiers kPossibleModifiersForAccessKey =
342 (MODIFIER_SHIFT | MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META |
343 MODIFIER_OS);
344 return (inputEvent->mModifiers & kPossibleModifiersForAccessKey);
345 }
346
GetMenuForKeyEvent(KeyboardEvent * aKeyEvent,bool aPeek)347 nsMenuFrame* nsMenuBarListener::GetMenuForKeyEvent(KeyboardEvent* aKeyEvent,
348 bool aPeek) {
349 if (!IsAccessKeyPressed(aKeyEvent)) {
350 return nullptr;
351 }
352
353 uint32_t charCode = aKeyEvent->CharCode();
354 bool hasAccessKeyCandidates = charCode != 0;
355 if (!hasAccessKeyCandidates) {
356 WidgetKeyboardEvent* nativeKeyEvent =
357 aKeyEvent->WidgetEventPtr()->AsKeyboardEvent();
358
359 AutoTArray<uint32_t, 10> keys;
360 nativeKeyEvent->GetAccessKeyCandidates(keys);
361 hasAccessKeyCandidates = !keys.IsEmpty();
362 }
363
364 if (hasAccessKeyCandidates) {
365 // Do shortcut navigation.
366 // A letter was pressed. We want to see if a shortcut gets matched. If
367 // so, we'll know the menu got activated.
368 return mMenuBarFrame->FindMenuWithShortcut(aKeyEvent, aPeek);
369 }
370
371 return nullptr;
372 }
373
ReserveKeyIfNeeded(Event * aKeyEvent)374 void nsMenuBarListener::ReserveKeyIfNeeded(Event* aKeyEvent) {
375 WidgetKeyboardEvent* nativeKeyEvent =
376 aKeyEvent->WidgetEventPtr()->AsKeyboardEvent();
377 if (nsContentUtils::ShouldBlockReservedKeys(nativeKeyEvent)) {
378 nativeKeyEvent->MarkAsReservedByChrome();
379 }
380 }
381
382 ////////////////////////////////////////////////////////////////////////
KeyDown(Event * aKeyEvent)383 nsresult nsMenuBarListener::KeyDown(Event* aKeyEvent) {
384 InitAccessKey();
385
386 // handlers shouldn't be triggered by non-trusted events.
387 if (!aKeyEvent || !aKeyEvent->IsTrusted()) {
388 return NS_OK;
389 }
390
391 RefPtr<KeyboardEvent> keyEvent = aKeyEvent->AsKeyboardEvent();
392 if (!keyEvent) {
393 return NS_OK;
394 }
395
396 uint32_t theChar = keyEvent->KeyCode();
397
398 uint16_t eventPhase = keyEvent->EventPhase();
399 bool capturing = (eventPhase == dom::Event_Binding::CAPTURING_PHASE);
400
401 #ifndef XP_MACOSX
402 if (capturing && !mAccessKeyDown && theChar == NS_VK_F10 &&
403 (GetModifiersForAccessKey(keyEvent) & ~MODIFIER_CONTROL) == 0) {
404 ReserveKeyIfNeeded(aKeyEvent);
405 }
406 #endif
407
408 if (mAccessKey && StaticPrefs::ui_key_menuAccessKeyFocuses()) {
409 bool defaultPrevented = aKeyEvent->DefaultPrevented();
410
411 // No other modifiers can be down.
412 // Especially CTRL. CTRL+ALT == AltGR, and we'll break on non-US
413 // enhanced 102-key keyboards if we don't check this.
414 bool isAccessKeyDownEvent =
415 ((theChar == (uint32_t)mAccessKey) &&
416 (GetModifiersForAccessKey(keyEvent) & ~mAccessKeyMask) == 0);
417
418 if (!capturing && !mAccessKeyDown) {
419 // If accesskey isn't being pressed and the key isn't the accesskey,
420 // ignore the event.
421 if (!isAccessKeyDownEvent) {
422 return NS_OK;
423 }
424
425 // Otherwise, accept the accesskey state.
426 mAccessKeyDown = true;
427 // If default is prevented already, cancel the access key down.
428 mAccessKeyDownCanceled = defaultPrevented;
429 return NS_OK;
430 }
431
432 // If the pressed accesskey was canceled already or the event was
433 // consumed already, ignore the event.
434 if (mAccessKeyDownCanceled || defaultPrevented) {
435 return NS_OK;
436 }
437
438 // Some key other than the access key just went down,
439 // so we won't activate the menu bar when the access key is released.
440 mAccessKeyDownCanceled = !isAccessKeyDownEvent;
441 }
442
443 if (capturing && mAccessKey) {
444 nsMenuFrame* menuFrameForKey = GetMenuForKeyEvent(keyEvent, true);
445 if (menuFrameForKey) {
446 ReserveKeyIfNeeded(aKeyEvent);
447 }
448 }
449
450 return NS_OK; // means I am NOT consuming event
451 }
452
453 ////////////////////////////////////////////////////////////////////////
454
Blur(Event * aEvent)455 nsresult nsMenuBarListener::Blur(Event* aEvent) {
456 if (!mMenuBarFrame->IsMenuOpen() && mMenuBarFrame->IsActive()) {
457 ToggleMenuActiveState();
458 mAccessKeyDown = false;
459 mAccessKeyDownCanceled = false;
460 }
461 return NS_OK; // means I am NOT consuming event
462 }
463
464 ////////////////////////////////////////////////////////////////////////
465
OnWindowDeactivated(Event * aEvent)466 nsresult nsMenuBarListener::OnWindowDeactivated(Event* aEvent) {
467 // Reset the accesskey state because we cannot receive the keyup event for
468 // the pressing accesskey.
469 mAccessKeyDown = false;
470 mAccessKeyDownCanceled = false;
471 return NS_OK; // means I am NOT consuming event
472 }
473
474 ////////////////////////////////////////////////////////////////////////
MouseDown(Event * aMouseEvent)475 nsresult nsMenuBarListener::MouseDown(Event* aMouseEvent) {
476 // NOTE: MouseDown method listens all phases
477
478 // Even if the mousedown event is canceled, it means the user don't want
479 // to activate the menu. Therefore, we need to record it at capturing (or
480 // target) phase.
481 if (mAccessKeyDown) {
482 mAccessKeyDownCanceled = true;
483 }
484
485 // Don't do anything at capturing phase, any behavior should be cancelable.
486 if (aMouseEvent->EventPhase() == dom::Event_Binding::CAPTURING_PHASE) {
487 return NS_OK;
488 }
489
490 if (!mMenuBarFrame->IsMenuOpen() && mMenuBarFrame->IsActive())
491 ToggleMenuActiveState();
492
493 return NS_OK; // means I am NOT consuming event
494 }
495
496 ////////////////////////////////////////////////////////////////////////
497
Fullscreen(Event * aEvent)498 nsresult nsMenuBarListener::Fullscreen(Event* aEvent) {
499 if (mMenuBarFrame->IsActive()) {
500 ToggleMenuActiveState();
501 }
502 return NS_OK;
503 }
504
505 ////////////////////////////////////////////////////////////////////////
HandleEvent(Event * aEvent)506 nsresult nsMenuBarListener::HandleEvent(Event* aEvent) {
507 // If the menu bar is collapsed, don't do anything.
508 if (!mMenuBarFrame->StyleVisibility()->IsVisible()) {
509 return NS_OK;
510 }
511
512 nsAutoString eventType;
513 aEvent->GetType(eventType);
514
515 if (eventType.EqualsLiteral("keyup")) {
516 return KeyUp(aEvent);
517 }
518 if (eventType.EqualsLiteral("keydown")) {
519 return KeyDown(aEvent);
520 }
521 if (eventType.EqualsLiteral("keypress")) {
522 return KeyPress(aEvent);
523 }
524 if (eventType.EqualsLiteral("mozaccesskeynotfound")) {
525 return KeyPress(aEvent);
526 }
527 if (eventType.EqualsLiteral("blur")) {
528 return Blur(aEvent);
529 }
530 if (eventType.EqualsLiteral("deactivate")) {
531 return OnWindowDeactivated(aEvent);
532 }
533 if (eventType.EqualsLiteral("mousedown")) {
534 return MouseDown(aEvent);
535 }
536 if (eventType.EqualsLiteral("MozDOMFullscreen:Entered")) {
537 return Fullscreen(aEvent);
538 }
539
540 MOZ_ASSERT_UNREACHABLE("Unexpected eventType");
541 return NS_OK;
542 }
543