MenuBarListener.cpp (16530B)
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 "MenuBarListener.h" 8 9 #include "XULButtonElement.h" 10 #include "mozilla/Attributes.h" 11 #include "nsISound.h" 12 13 // Drag & Drop, Clipboard 14 #include "mozilla/BasicEvents.h" 15 #include "mozilla/LookAndFeel.h" 16 #include "mozilla/Preferences.h" 17 #include "mozilla/StaticPrefs_ui.h" 18 #include "mozilla/TextEvents.h" 19 #include "mozilla/dom/Document.h" 20 #include "mozilla/dom/Event.h" 21 #include "mozilla/dom/EventBinding.h" 22 #include "mozilla/dom/KeyboardEvent.h" 23 #include "mozilla/dom/KeyboardEventBinding.h" 24 #include "mozilla/dom/XULButtonElement.h" 25 #include "mozilla/dom/XULMenuBarElement.h" 26 #include "mozilla/dom/XULMenuParentElement.h" 27 #include "nsCOMPtr.h" 28 #include "nsContentUtils.h" 29 #include "nsIFrame.h" 30 #include "nsPIWindowRoot.h" 31 #include "nsWidgetsCID.h" 32 #include "nsXULPopupManager.h" 33 34 namespace mozilla::dom { 35 36 NS_IMPL_ISUPPORTS(MenuBarListener, nsIDOMEventListener) 37 38 MenuBarListener::MenuBarListener(XULMenuBarElement& aElement) 39 : mMenuBar(&aElement), 40 mEventTarget(aElement.GetComposedDoc()), 41 mAccessKeyDown(false), 42 mAccessKeyDownCanceled(false) { 43 MOZ_ASSERT(mEventTarget); 44 MOZ_ASSERT(mMenuBar); 45 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. 48 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. 51 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 58 // cancelled. 59 mEventTarget->AddEventListener(u"keydown"_ns, this, true); 60 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); 65 66 mEventTarget->AddEventListener(u"MozDOMFullscreen:Entered"_ns, this, false); 67 68 // Needs to listen to the deactivate event of the window. 69 if (RefPtr<EventTarget> top = nsContentUtils::GetWindowRoot(mEventTarget)) { 70 top->AddSystemEventListener(u"deactivate"_ns, this, true); 71 } 72 } 73 74 //////////////////////////////////////////////////////////////////////// 75 MenuBarListener::~MenuBarListener() { 76 MOZ_ASSERT(!mEventTarget, "Should've detached always"); 77 } 78 79 void MenuBarListener::Detach() { 80 if (!mMenuBar) { 81 MOZ_ASSERT(!mEventTarget); 82 return; 83 } 84 mEventTarget->RemoveSystemEventListener(u"keypress"_ns, this, false); 85 mEventTarget->RemoveSystemEventListener(u"keydown"_ns, this, false); 86 mEventTarget->RemoveSystemEventListener(u"keyup"_ns, this, false); 87 mEventTarget->RemoveSystemEventListener(u"mozaccesskeynotfound"_ns, this, 88 false); 89 mEventTarget->RemoveEventListener(u"keydown"_ns, this, true); 90 91 mEventTarget->RemoveEventListener(u"mousedown"_ns, this, true); 92 mEventTarget->RemoveEventListener(u"mousedown"_ns, this, false); 93 mEventTarget->RemoveEventListener(u"blur"_ns, this, true); 94 95 mEventTarget->RemoveEventListener(u"MozDOMFullscreen:Entered"_ns, this, 96 false); 97 if (RefPtr<EventTarget> top = nsContentUtils::GetWindowRoot(mEventTarget)) { 98 top->RemoveSystemEventListener(u"deactivate"_ns, this, true); 99 } 100 mMenuBar = nullptr; 101 mEventTarget = nullptr; 102 } 103 104 void MenuBarListener::ToggleMenuActiveState(ByKeyboard aByKeyboard) { 105 RefPtr menuBar = mMenuBar; 106 if (menuBar->IsActive()) { 107 menuBar->SetActive(false); 108 } else { 109 if (aByKeyboard == ByKeyboard::Yes) { 110 menuBar->SetActiveByKeyboard(); 111 } 112 // This will activate the menubar if needed. 113 menuBar->SelectFirstItem(); 114 } 115 } 116 117 //////////////////////////////////////////////////////////////////////// 118 nsresult MenuBarListener::KeyUp(Event* aKeyEvent) { 119 WidgetKeyboardEvent* nativeKeyEvent = 120 aKeyEvent->WidgetEventPtr()->AsKeyboardEvent(); 121 if (!nativeKeyEvent) { 122 return NS_OK; 123 } 124 125 // handlers shouldn't be triggered by non-trusted events. 126 if (!nativeKeyEvent->IsTrusted()) { 127 return NS_OK; 128 } 129 130 const auto accessKey = LookAndFeel::GetMenuAccessKey(); 131 if (!accessKey || !StaticPrefs::ui_key_menuAccessKeyFocuses()) { 132 return NS_OK; 133 } 134 135 // On a press of the ALT key by itself, we toggle the menu's 136 // active/inactive state. 137 if (!nativeKeyEvent->DefaultPrevented() && mAccessKeyDown && 138 !mAccessKeyDownCanceled && nativeKeyEvent->mKeyCode == accessKey) { 139 // The access key was down and is now up, and no other 140 // keys were pressed in between. 141 bool toggleMenuActiveState = true; 142 if (!mMenuBar->IsActive()) { 143 // If the focused content is in a remote process, we should allow the 144 // focused web app to prevent to activate the menubar. 145 if (nativeKeyEvent->WillBeSentToRemoteProcess()) { 146 nativeKeyEvent->StopImmediatePropagation(); 147 nativeKeyEvent->MarkAsWaitingReplyFromRemoteProcess(); 148 return NS_OK; 149 } 150 // First, close all existing popups because other popups shouldn't 151 // handle key events when menubar is active and IME should be 152 // disabled. 153 if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) { 154 pm->Rollup({}); 155 } 156 // If menubar active state is changed or the menubar is destroyed 157 // during closing the popups, we should do nothing anymore. 158 toggleMenuActiveState = !Destroyed() && !mMenuBar->IsActive(); 159 } 160 if (toggleMenuActiveState) { 161 ToggleMenuActiveState(ByKeyboard::Yes); 162 } 163 } 164 165 mAccessKeyDown = false; 166 mAccessKeyDownCanceled = false; 167 168 if (!Destroyed() && mMenuBar->IsActive()) { 169 nativeKeyEvent->StopPropagation(); 170 nativeKeyEvent->PreventDefault(); 171 } 172 173 return NS_OK; 174 } 175 176 //////////////////////////////////////////////////////////////////////// 177 nsresult MenuBarListener::KeyPress(Event* aKeyEvent) { 178 // if event has already been handled, bail 179 if (!aKeyEvent || aKeyEvent->DefaultPrevented()) { 180 return NS_OK; // don't consume event 181 } 182 183 // handlers shouldn't be triggered by non-trusted events. 184 if (!aKeyEvent->IsTrusted()) { 185 return NS_OK; 186 } 187 188 const auto accessKey = LookAndFeel::GetMenuAccessKey(); 189 if (!accessKey) { 190 return NS_OK; 191 } 192 // If accesskey handling was forwarded to a child process, wait for 193 // the mozaccesskeynotfound event before handling accesskeys. 194 WidgetKeyboardEvent* nativeKeyEvent = 195 aKeyEvent->WidgetEventPtr()->AsKeyboardEvent(); 196 if (!nativeKeyEvent) { 197 return NS_OK; 198 } 199 200 RefPtr<KeyboardEvent> keyEvent = aKeyEvent->AsKeyboardEvent(); 201 uint32_t keyCode = keyEvent->KeyCode(); 202 203 // Cancel the access key flag unless we are pressing the access key. 204 if (keyCode != accessKey) { 205 mAccessKeyDownCanceled = true; 206 } 207 208 #ifndef XP_MACOSX 209 // Need to handle F10 specially on Non-Mac platform. 210 if (nativeKeyEvent->mMessage == eKeyPress && keyCode == NS_VK_F10) { 211 if ((keyEvent->GetModifiersForMenuAccessKey() & ~MODIFIER_CONTROL) == 0) { 212 // If the keyboard event should activate the menubar and will be 213 // sent to a remote process, it should be executed with reply 214 // event from the focused remote process. Note that if the menubar 215 // is active, the event is already marked as "stop cross 216 // process dispatching". So, in that case, this won't wait 217 // reply from the remote content. 218 if (nativeKeyEvent->WillBeSentToRemoteProcess()) { 219 nativeKeyEvent->StopImmediatePropagation(); 220 nativeKeyEvent->MarkAsWaitingReplyFromRemoteProcess(); 221 return NS_OK; 222 } 223 // The F10 key just went down by itself or with ctrl pressed. 224 // In Windows, both of these activate the menu bar. 225 ToggleMenuActiveState(ByKeyboard::Yes); 226 227 if (mMenuBar && mMenuBar->IsActive()) { 228 # ifdef MOZ_WIDGET_GTK 229 if (RefPtr child = mMenuBar->GetActiveMenuChild()) { 230 // In GTK, this also opens the first menu. 231 child->OpenMenuPopup(false); 232 } 233 # endif 234 aKeyEvent->StopPropagation(); 235 aKeyEvent->PreventDefault(); 236 } 237 } 238 239 return NS_OK; 240 } 241 #endif // !XP_MACOSX 242 243 RefPtr menuForKey = GetMenuForKeyEvent(*keyEvent); 244 if (!menuForKey) { 245 #ifdef XP_WIN 246 // Behavior on Windows - this item is on the menu bar, beep and deactivate 247 // the menu bar. 248 // TODO(emilio): This is rather odd, and I cannot get the beep to work, 249 // but this matches what old code was doing... 250 if (mMenuBar && mMenuBar->IsActive() && mMenuBar->IsActiveByKeyboard()) { 251 if (nsCOMPtr<nsISound> sound = do_GetService("@mozilla.org/sound;1")) { 252 sound->Beep(); 253 } 254 ToggleMenuActiveState(ByKeyboard::Yes); 255 } 256 #endif 257 return NS_OK; 258 } 259 260 // If the keyboard event matches with a menu item's accesskey and 261 // will be sent to a remote process, it should be executed with 262 // reply event from the focused remote process. Note that if the 263 // menubar is active, the event is already marked as "stop cross 264 // process dispatching". So, in that case, this won't wait 265 // reply from the remote content. 266 if (nativeKeyEvent->WillBeSentToRemoteProcess()) { 267 nativeKeyEvent->StopImmediatePropagation(); 268 nativeKeyEvent->MarkAsWaitingReplyFromRemoteProcess(); 269 return NS_OK; 270 } 271 272 RefPtr menuBar = mMenuBar; 273 menuBar->SetActiveByKeyboard(); 274 // This will activate the menubar as needed. 275 menuForKey->OpenMenuPopup(true); 276 277 // The opened menu will listen next keyup event. 278 // Therefore, we should clear the keydown flags here. 279 mAccessKeyDown = mAccessKeyDownCanceled = false; 280 281 aKeyEvent->StopPropagation(); 282 aKeyEvent->PreventDefault(); 283 return NS_OK; 284 } 285 286 dom::XULButtonElement* MenuBarListener::GetMenuForKeyEvent( 287 KeyboardEvent& aKeyEvent) { 288 if (!aKeyEvent.IsMenuAccessKeyPressed()) { 289 return nullptr; 290 } 291 292 uint32_t charCode = aKeyEvent.CharCode(); 293 bool hasAccessKeyCandidates = charCode != 0; 294 if (!hasAccessKeyCandidates) { 295 WidgetKeyboardEvent* nativeKeyEvent = 296 aKeyEvent.WidgetEventPtr()->AsKeyboardEvent(); 297 AutoTArray<uint32_t, 10> keys; 298 nativeKeyEvent->GetAccessKeyCandidates(keys); 299 hasAccessKeyCandidates = !keys.IsEmpty(); 300 } 301 302 if (!hasAccessKeyCandidates) { 303 return nullptr; 304 } 305 // Do shortcut navigation. 306 // A letter was pressed. We want to see if a shortcut gets matched. If 307 // so, we'll know the menu got activated. 308 return mMenuBar->FindMenuWithShortcut(aKeyEvent); 309 } 310 311 void MenuBarListener::ReserveKeyIfNeeded(Event* aKeyEvent) { 312 WidgetKeyboardEvent* nativeKeyEvent = 313 aKeyEvent->WidgetEventPtr()->AsKeyboardEvent(); 314 if (nsContentUtils::ShouldBlockReservedKeys(nativeKeyEvent)) { 315 nativeKeyEvent->MarkAsReservedByChrome(); 316 } 317 } 318 319 //////////////////////////////////////////////////////////////////////// 320 nsresult MenuBarListener::KeyDown(Event* aKeyEvent) { 321 // handlers shouldn't be triggered by non-trusted events. 322 if (!aKeyEvent || !aKeyEvent->IsTrusted()) { 323 return NS_OK; 324 } 325 326 RefPtr<KeyboardEvent> keyEvent = aKeyEvent->AsKeyboardEvent(); 327 if (!keyEvent) { 328 return NS_OK; 329 } 330 331 uint32_t theChar = keyEvent->KeyCode(); 332 uint16_t eventPhase = keyEvent->EventPhase(); 333 bool capturing = (eventPhase == dom::Event_Binding::CAPTURING_PHASE); 334 335 #ifndef XP_MACOSX 336 if (capturing && !mAccessKeyDown && theChar == NS_VK_F10 && 337 (keyEvent->GetModifiersForMenuAccessKey() & ~MODIFIER_CONTROL) == 0) { 338 ReserveKeyIfNeeded(aKeyEvent); 339 } 340 #endif 341 342 const auto accessKey = LookAndFeel::GetMenuAccessKey(); 343 if (accessKey && StaticPrefs::ui_key_menuAccessKeyFocuses()) { 344 bool defaultPrevented = aKeyEvent->DefaultPrevented(); 345 346 // No other modifiers can be down. 347 // Especially CTRL. CTRL+ALT == AltGR, and we'll break on non-US 348 // enhanced 102-key keyboards if we don't check this. 349 bool isAccessKeyDownEvent = 350 (theChar == accessKey && 351 (keyEvent->GetModifiersForMenuAccessKey() & 352 ~LookAndFeel::GetMenuAccessKeyModifiers()) == 0); 353 354 if (!capturing && !mAccessKeyDown) { 355 // If accesskey isn't being pressed and the key isn't the accesskey, 356 // ignore the event. 357 if (!isAccessKeyDownEvent) { 358 return NS_OK; 359 } 360 361 // Otherwise, accept the accesskey state. 362 mAccessKeyDown = true; 363 // If default is prevented already, cancel the access key down. 364 mAccessKeyDownCanceled = defaultPrevented; 365 return NS_OK; 366 } 367 368 // If the pressed accesskey was canceled already or the event was 369 // consumed already, ignore the event. 370 if (mAccessKeyDownCanceled || defaultPrevented) { 371 return NS_OK; 372 } 373 374 // Some key other than the access key just went down, 375 // so we won't activate the menu bar when the access key is released. 376 mAccessKeyDownCanceled = !isAccessKeyDownEvent; 377 } 378 379 if (capturing && accessKey) { 380 if (GetMenuForKeyEvent(*keyEvent)) { 381 ReserveKeyIfNeeded(aKeyEvent); 382 } 383 } 384 385 return NS_OK; // means I am NOT consuming event 386 } 387 388 //////////////////////////////////////////////////////////////////////// 389 390 nsresult MenuBarListener::Blur(Event* aEvent) { 391 if (!IsMenuOpen() && mMenuBar->IsActive()) { 392 ToggleMenuActiveState(ByKeyboard::No); 393 mAccessKeyDown = false; 394 mAccessKeyDownCanceled = false; 395 } 396 return NS_OK; // means I am NOT consuming event 397 } 398 399 //////////////////////////////////////////////////////////////////////// 400 401 nsresult MenuBarListener::OnWindowDeactivated(Event* aEvent) { 402 // Reset the accesskey state because we cannot receive the keyup event for 403 // the pressing accesskey. 404 mAccessKeyDown = false; 405 mAccessKeyDownCanceled = false; 406 return NS_OK; // means I am NOT consuming event 407 } 408 409 bool MenuBarListener::IsMenuOpen() const { 410 auto* activeChild = mMenuBar->GetActiveMenuChild(); 411 return activeChild && activeChild->IsMenuPopupOpen(); 412 } 413 414 //////////////////////////////////////////////////////////////////////// 415 nsresult MenuBarListener::MouseDown(Event* aMouseEvent) { 416 // NOTE: MouseDown method listens all phases 417 418 // Even if the mousedown event is canceled, it means the user don't want 419 // to activate the menu. Therefore, we need to record it at capturing (or 420 // target) phase. 421 if (mAccessKeyDown) { 422 mAccessKeyDownCanceled = true; 423 } 424 425 // Don't do anything at capturing phase, any behavior should be cancelable. 426 if (aMouseEvent->EventPhase() == dom::Event_Binding::CAPTURING_PHASE) { 427 return NS_OK; 428 } 429 430 if (!IsMenuOpen() && mMenuBar->IsActive()) { 431 ToggleMenuActiveState(ByKeyboard::No); 432 } 433 434 return NS_OK; // means I am NOT consuming event 435 } 436 437 //////////////////////////////////////////////////////////////////////// 438 439 nsresult MenuBarListener::Fullscreen(Event* aEvent) { 440 if (mMenuBar->IsActive()) { 441 ToggleMenuActiveState(ByKeyboard::No); 442 } 443 return NS_OK; 444 } 445 446 //////////////////////////////////////////////////////////////////////// 447 MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult 448 MenuBarListener::HandleEvent(Event* aEvent) { 449 // If the menu bar is collapsed, don't do anything. 450 if (!mMenuBar || !mMenuBar->GetPrimaryFrame() || 451 !mMenuBar->GetPrimaryFrame()->StyleVisibility()->IsVisible()) { 452 return NS_OK; 453 } 454 455 nsAutoString eventType; 456 aEvent->GetType(eventType); 457 458 if (eventType.EqualsLiteral("keyup")) { 459 return KeyUp(aEvent); 460 } 461 if (eventType.EqualsLiteral("keydown")) { 462 return KeyDown(aEvent); 463 } 464 if (eventType.EqualsLiteral("keypress")) { 465 return KeyPress(aEvent); 466 } 467 if (eventType.EqualsLiteral("mozaccesskeynotfound")) { 468 return KeyPress(aEvent); 469 } 470 if (eventType.EqualsLiteral("blur")) { 471 return Blur(aEvent); 472 } 473 if (eventType.EqualsLiteral("deactivate")) { 474 return OnWindowDeactivated(aEvent); 475 } 476 if (eventType.EqualsLiteral("mousedown")) { 477 return MouseDown(aEvent); 478 } 479 if (eventType.EqualsLiteral("MozDOMFullscreen:Entered")) { 480 return Fullscreen(aEvent); 481 } 482 483 MOZ_ASSERT_UNREACHABLE("Unexpected eventType"); 484 return NS_OK; 485 } 486 487 } // namespace mozilla::dom