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