1/* -*- Mode: C++; tab-width: 2; 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/. */ 5 6#include "nsMenuX.h" 7 8#include <_types/_uint32_t.h> 9#include <dlfcn.h> 10 11#include "mozilla/dom/Document.h" 12#include "mozilla/dom/ScriptSettings.h" 13#include "mozilla/EventDispatcher.h" 14#include "mozilla/MouseEvents.h" 15 16#include "MOZMenuOpeningCoordinator.h" 17#include "nsMenuItemX.h" 18#include "nsMenuUtilsX.h" 19#include "nsMenuItemIconX.h" 20 21#include "nsObjCExceptions.h" 22 23#include "nsComputedDOMStyle.h" 24#include "nsThreadUtils.h" 25#include "nsToolkit.h" 26#include "nsCocoaUtils.h" 27#include "nsCOMPtr.h" 28#include "prinrval.h" 29#include "nsString.h" 30#include "nsReadableUtils.h" 31#include "nsUnicharUtils.h" 32#include "plstr.h" 33#include "nsGkAtoms.h" 34#include "nsCRT.h" 35#include "nsBaseWidget.h" 36 37#include "nsIContent.h" 38#include "nsIDocumentObserver.h" 39#include "nsIComponentManager.h" 40#include "nsIRollupListener.h" 41#include "nsIServiceManager.h" 42#include "nsXULPopupManager.h" 43 44using namespace mozilla; 45using namespace mozilla::dom; 46 47static bool gConstructingMenu = false; 48static bool gMenuMethodsSwizzled = false; 49 50int32_t nsMenuX::sIndexingMenuLevel = 0; 51 52// TODO: It is unclear whether this is still needed. 53static void SwizzleDynamicIndexingMethods() { 54 if (gMenuMethodsSwizzled) { 55 return; 56 } 57 58 nsToolkit::SwizzleMethods([NSMenu class], @selector(_addItem:toTable:), 59 @selector(nsMenuX_NSMenu_addItem:toTable:), true); 60 nsToolkit::SwizzleMethods([NSMenu class], @selector(_removeItem:fromTable:), 61 @selector(nsMenuX_NSMenu_removeItem:fromTable:), true); 62 // On SnowLeopard the Shortcut framework (which contains the 63 // SCTGRLIndex class) is loaded on demand, whenever the user first opens 64 // a menu (which normally hasn't happened yet). So we need to load it 65 // here explicitly. 66 dlopen("/System/Library/PrivateFrameworks/Shortcut.framework/Shortcut", RTLD_LAZY); 67 Class SCTGRLIndexClass = ::NSClassFromString(@"SCTGRLIndex"); 68 nsToolkit::SwizzleMethods(SCTGRLIndexClass, @selector(indexMenuBarDynamically), 69 @selector(nsMenuX_SCTGRLIndex_indexMenuBarDynamically)); 70 71 gMenuMethodsSwizzled = true; 72} 73 74// 75// nsMenuX 76// 77 78nsMenuX::nsMenuX(nsMenuParentX* aParent, nsMenuGroupOwnerX* aMenuGroupOwner, nsIContent* aContent) 79 : mContent(aContent), mParent(aParent), mMenuGroupOwner(aMenuGroupOwner) { 80 NS_OBJC_BEGIN_TRY_ABORT_BLOCK; 81 82 MOZ_COUNT_CTOR(nsMenuX); 83 84 SwizzleDynamicIndexingMethods(); 85 86 mMenuDelegate = [[MenuDelegate alloc] initWithGeckoMenu:this]; 87 mMenuDelegate.menuIsInMenubar = mMenuGroupOwner->GetMenuBar() != nullptr; 88 89 if (!nsMenuBarX::sNativeEventTarget) { 90 nsMenuBarX::sNativeEventTarget = [[NativeMenuItemTarget alloc] init]; 91 } 92 93 if (mContent->IsElement()) { 94 mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::label, mLabel); 95 } 96 mNativeMenu = CreateMenuWithGeckoString(mLabel); 97 98 // register this menu to be notified when changes are made to our content object 99 NS_ASSERTION(mMenuGroupOwner, "No menu owner given, must have one"); 100 mMenuGroupOwner->RegisterForContentChanges(mContent, this); 101 102 mVisible = !nsMenuUtilsX::NodeIsHiddenOrCollapsed(mContent); 103 104 NSString* newCocoaLabelString = nsMenuUtilsX::GetTruncatedCocoaLabel(mLabel); 105 mNativeMenuItem = [[NSMenuItem alloc] initWithTitle:newCocoaLabelString 106 action:nil 107 keyEquivalent:@""]; 108 mNativeMenuItem.submenu = mNativeMenu; 109 110 SetEnabled(!mContent->IsElement() || 111 !mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, 112 nsGkAtoms::_true, eCaseMatters)); 113 114 // We call RebuildMenu here because keyboard commands are dependent upon 115 // native menu items being created. If we only call RebuildMenu when a menu 116 // is actually selected, then we can't access keyboard commands until the 117 // menu gets selected, which is bad. 118 RebuildMenu(); 119 120 mIcon = MakeUnique<nsMenuItemIconX>(this); 121 122 if (mVisible) { 123 SetupIcon(); 124 } 125 126 NS_OBJC_END_TRY_ABORT_BLOCK; 127} 128 129nsMenuX::~nsMenuX() { 130 NS_OBJC_BEGIN_TRY_ABORT_BLOCK; 131 132 // Make sure a pending popupshown event isn't dropped. 133 FlushMenuOpenedRunnable(); 134 135 if (mIsOpen) { 136 [mNativeMenu cancelTracking]; 137 MOZMenuOpeningCoordinator.needToUnwindForMenuClosing = YES; 138 } 139 140 // Make sure pending popuphiding/popuphidden events aren't dropped. 141 FlushMenuClosedRunnable(); 142 143 OnHighlightedItemChanged(Nothing()); 144 RemoveAll(); 145 146 mNativeMenu.delegate = nil; 147 [mNativeMenu release]; 148 [mMenuDelegate release]; 149 // autorelease the native menu item so that anything else happening to this 150 // object happens before the native menu item actually dies 151 [mNativeMenuItem autorelease]; 152 153 DetachFromGroupOwnerRecursive(); 154 155 MOZ_COUNT_DTOR(nsMenuX); 156 157 NS_OBJC_END_TRY_ABORT_BLOCK; 158} 159 160void nsMenuX::DetachFromGroupOwnerRecursive() { 161 if (!mMenuGroupOwner) { 162 // Don't recurse if this subtree is already detached. 163 // This avoids repeated recursion during the destruction of nested nsMenuX structures. 164 // Our invariant is: If we are detached, all of our contents are also detached. 165 return; 166 } 167 168 if (mMenuGroupOwner && mContent) { 169 mMenuGroupOwner->UnregisterForContentChanges(mContent); 170 } 171 mMenuGroupOwner = nullptr; 172 173 // Also detach all our children. 174 for (auto& child : mMenuChildren) { 175 child.match([](const RefPtr<nsMenuX>& aMenu) { aMenu->DetachFromGroupOwnerRecursive(); }, 176 [](const RefPtr<nsMenuItemX>& aMenuItem) { aMenuItem->DetachFromGroupOwner(); }); 177 } 178} 179 180void nsMenuX::OnMenuWillOpen(dom::Element* aPopupElement) { 181 RefPtr<nsMenuX> kungFuDeathGrip(this); 182 if (mObserver) { 183 mObserver->OnMenuWillOpen(aPopupElement); 184 } 185} 186 187void nsMenuX::OnMenuDidOpen(dom::Element* aPopupElement) { 188 RefPtr<nsMenuX> kungFuDeathGrip(this); 189 if (mObserver) { 190 mObserver->OnMenuDidOpen(aPopupElement); 191 } 192} 193 194void nsMenuX::OnMenuWillActivateItem(dom::Element* aPopupElement, dom::Element* aMenuItemElement) { 195 RefPtr<nsMenuX> kungFuDeathGrip(this); 196 if (mObserver) { 197 mObserver->OnMenuWillActivateItem(aPopupElement, aMenuItemElement); 198 } 199} 200 201void nsMenuX::OnMenuClosed(dom::Element* aPopupElement) { 202 RefPtr<nsMenuX> kungFuDeathGrip(this); 203 if (mObserver) { 204 mObserver->OnMenuClosed(aPopupElement); 205 } 206} 207 208void nsMenuX::AddMenuChild(MenuChild&& aChild) { 209 NS_OBJC_BEGIN_TRY_ABORT_BLOCK; 210 211 WillInsertChild(aChild); 212 mMenuChildren.AppendElement(aChild); 213 214 bool isVisible = 215 aChild.match([](const RefPtr<nsMenuX>& aMenu) { return aMenu->IsVisible(); }, 216 [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->IsVisible(); }); 217 NSMenuItem* nativeItem = aChild.match( 218 [](const RefPtr<nsMenuX>& aMenu) { return aMenu->NativeNSMenuItem(); }, 219 [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->NativeNSMenuItem(); }); 220 221 if (isVisible) { 222 RemovePlaceholderIfPresent(); 223 [mNativeMenu addItem:nativeItem]; 224 ++mVisibleItemsCount; 225 } 226 227 NS_OBJC_END_TRY_ABORT_BLOCK; 228} 229 230void nsMenuX::InsertMenuChild(MenuChild&& aChild) { 231 NS_OBJC_BEGIN_TRY_ABORT_BLOCK; 232 233 WillInsertChild(aChild); 234 size_t insertionIndex = FindInsertionIndex(aChild); 235 mMenuChildren.InsertElementAt(insertionIndex, aChild); 236 237 bool isVisible = 238 aChild.match([](const RefPtr<nsMenuX>& aMenu) { return aMenu->IsVisible(); }, 239 [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->IsVisible(); }); 240 if (isVisible) { 241 MenuChildChangedVisibility(aChild, true); 242 } 243 244 NS_OBJC_END_TRY_ABORT_BLOCK; 245} 246 247void nsMenuX::RemoveMenuChild(const MenuChild& aChild) { 248 bool isVisible = 249 aChild.match([](const RefPtr<nsMenuX>& aMenu) { return aMenu->IsVisible(); }, 250 [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->IsVisible(); }); 251 if (isVisible) { 252 MenuChildChangedVisibility(aChild, false); 253 } 254 255 WillRemoveChild(aChild); 256 mMenuChildren.RemoveElement(aChild); 257} 258 259size_t nsMenuX::FindInsertionIndex(const MenuChild& aChild) { 260 nsCOMPtr<nsIContent> menuPopup = GetMenuPopupContent(); 261 MOZ_RELEASE_ASSERT(menuPopup); 262 263 RefPtr<nsIContent> insertedContent = 264 aChild.match([](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); }, 265 [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->Content(); }); 266 267 MOZ_RELEASE_ASSERT(insertedContent->GetParent() == menuPopup); 268 269 // Iterate over menuPopup's children (insertedContent's siblings) until we encounter 270 // insertedContent. At the same time, keep track of the index in mMenuChildren. 271 size_t index = 0; 272 for (nsIContent* child = menuPopup->GetFirstChild(); child && index < mMenuChildren.Length(); 273 child = child->GetNextSibling()) { 274 if (child == insertedContent) { 275 break; 276 } 277 278 RefPtr<nsIContent> contentAtIndex = mMenuChildren[index].match( 279 [](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); }, 280 [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->Content(); }); 281 if (child == contentAtIndex) { 282 index++; 283 } 284 } 285 286 return index; 287} 288 289// Includes all items, including hidden/collapsed ones 290uint32_t nsMenuX::GetItemCount() { return mMenuChildren.Length(); } 291 292// Includes all items, including hidden/collapsed ones 293mozilla::Maybe<nsMenuX::MenuChild> nsMenuX::GetItemAt(uint32_t aPos) { 294 if (aPos >= (uint32_t)mMenuChildren.Length()) { 295 return {}; 296 } 297 298 return Some(mMenuChildren[aPos]); 299} 300 301// Only includes visible items 302nsresult nsMenuX::GetVisibleItemCount(uint32_t& aCount) { 303 aCount = mVisibleItemsCount; 304 return NS_OK; 305} 306 307// Only includes visible items. Note that this is provides O(N) access 308// If you need to iterate or search, consider using GetItemAt and doing your own filtering 309Maybe<nsMenuX::MenuChild> nsMenuX::GetVisibleItemAt(uint32_t aPos) { 310 uint32_t count = mMenuChildren.Length(); 311 if (aPos >= mVisibleItemsCount || aPos >= count) { 312 return {}; 313 } 314 315 // If there are no invisible items, can provide direct access 316 if (mVisibleItemsCount == count) { 317 return GetItemAt(aPos); 318 } 319 320 // Otherwise, traverse the array until we find the the item we're looking for. 321 uint32_t visibleNodeIndex = 0; 322 for (uint32_t i = 0; i < count; i++) { 323 MenuChild item = *GetItemAt(i); 324 RefPtr<nsIContent> content = 325 item.match([](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); }, 326 [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->Content(); }); 327 if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(content)) { 328 if (aPos == visibleNodeIndex) { 329 // we found the visible node we're looking for, return it 330 return Some(item); 331 } 332 visibleNodeIndex++; 333 } 334 } 335 336 return {}; 337} 338 339Maybe<nsMenuX::MenuChild> nsMenuX::GetItemForElement(Element* aMenuChildElement) { 340 for (auto& child : mMenuChildren) { 341 RefPtr<nsIContent> content = 342 child.match([](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); }, 343 [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->Content(); }); 344 if (content == aMenuChildElement) { 345 return Some(child); 346 } 347 } 348 return {}; 349} 350 351nsresult nsMenuX::RemoveAll() { 352 NS_OBJC_BEGIN_TRY_ABORT_BLOCK; 353 354 [mNativeMenu removeAllItems]; 355 356 for (auto& child : mMenuChildren) { 357 WillRemoveChild(child); 358 } 359 360 mMenuChildren.Clear(); 361 mVisibleItemsCount = 0; 362 363 return NS_OK; 364 365 NS_OBJC_END_TRY_ABORT_BLOCK; 366} 367 368void nsMenuX::WillInsertChild(const MenuChild& aChild) { 369 if (aChild.is<RefPtr<nsMenuX>>()) { 370 aChild.as<RefPtr<nsMenuX>>()->SetObserver(this); 371 } 372} 373 374void nsMenuX::WillRemoveChild(const MenuChild& aChild) { 375 aChild.match( 376 [](const RefPtr<nsMenuX>& aMenu) { 377 aMenu->DetachFromGroupOwnerRecursive(); 378 aMenu->DetachFromParent(); 379 aMenu->SetObserver(nullptr); 380 }, 381 [](const RefPtr<nsMenuItemX>& aMenuItem) { 382 aMenuItem->DetachFromGroupOwner(); 383 aMenuItem->DetachFromParent(); 384 }); 385} 386 387void nsMenuX::MenuOpened() { 388 if (mIsOpen) { 389 return; 390 } 391 392 // Make sure we fire any pending popupshown / popuphiding / popuphidden events first. 393 FlushMenuOpenedRunnable(); 394 FlushMenuClosedRunnable(); 395 396 if (!mDidFirePopupshowingAndIsApprovedToOpen) { 397 // Fire popupshowing now. 398 bool approvedToOpen = OnOpen(); 399 if (!approvedToOpen) { 400 // We can only stop menus from opening which we open ourselves. We cannot stop menubar root 401 // menus or menu submenus from opening. 402 // For context menus, we can call OnOpen() before we ask the system to open the menu. 403 NS_WARNING("The popupshowing event had preventDefault() called on it, but in MenuOpened() it " 404 "is too late to stop the menu from opening."); 405 } 406 } 407 408 mIsOpen = true; 409 410 // Reset mDidFirePopupshowingAndIsApprovedToOpen for the next menu opening. 411 mDidFirePopupshowingAndIsApprovedToOpen = false; 412 413 if (mNeedsRebuild) { 414 OnHighlightedItemChanged(Nothing()); 415 RemoveAll(); 416 RebuildMenu(); 417 } 418 419 // Fire the popupshown event in MenuOpenedAsync. 420 // MenuOpened() is called during menuWillOpen, and if cancelTracking is called now, menuDidClose 421 // will not be called. 422 // The runnable object must not hold a strong reference to the nsMenuX, so that there is no 423 // reference cycle. 424 class MenuOpenedAsyncRunnable final : public mozilla::CancelableRunnable { 425 public: 426 explicit MenuOpenedAsyncRunnable(nsMenuX* aMenu) 427 : CancelableRunnable("MenuOpenedAsyncRunnable"), mMenu(aMenu) {} 428 429 nsresult Run() override { 430 if (mMenu) { 431 RefPtr<nsMenuX> menu = mMenu; 432 menu->MenuOpenedAsync(); 433 mMenu = nullptr; 434 } 435 return NS_OK; 436 } 437 nsresult Cancel() override { 438 mMenu = nullptr; 439 return NS_OK; 440 } 441 442 private: 443 nsMenuX* mMenu; // weak, cleared by Cancel() and Run() 444 }; 445 mPendingAsyncMenuOpenRunnable = new MenuOpenedAsyncRunnable(this); 446 NS_DispatchToCurrentThread(mPendingAsyncMenuOpenRunnable); 447} 448 449void nsMenuX::FlushMenuOpenedRunnable() { 450 if (mPendingAsyncMenuOpenRunnable) { 451 MenuOpenedAsync(); 452 } 453} 454 455void nsMenuX::MenuOpenedAsync() { 456 if (mPendingAsyncMenuOpenRunnable) { 457 mPendingAsyncMenuOpenRunnable->Cancel(); 458 mPendingAsyncMenuOpenRunnable = nullptr; 459 } 460 461 mIsOpenForGecko = true; 462 463 // Open the node. 464 if (mContent->IsElement()) { 465 mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::open, u"true"_ns, true); 466 } 467 468 nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent(); 469 470 // Notify our observer. 471 if (mObserver && popupContent) { 472 mObserver->OnMenuDidOpen(popupContent->AsElement()); 473 } 474 475 // Fire popupshown. 476 nsEventStatus status = nsEventStatus_eIgnore; 477 WidgetMouseEvent event(true, eXULPopupShown, nullptr, WidgetMouseEvent::eReal); 478 nsIContent* dispatchTo = popupContent ? popupContent : mContent; 479 EventDispatcher::Dispatch(dispatchTo, nullptr, &event, nullptr, &status); 480} 481 482void nsMenuX::MenuClosed(bool aEntireMenuClosingDueToActivateItem) { 483 if (!mIsOpen) { 484 return; 485 } 486 487 // Make sure we fire any pending popupshown events first. 488 FlushMenuOpenedRunnable(); 489 490 // If any of our submenus were opened programmatically, make sure they get closed first. 491 for (auto& child : mMenuChildren) { 492 if (child.is<RefPtr<nsMenuX>>()) { 493 child.as<RefPtr<nsMenuX>>()->MenuClosed(aEntireMenuClosingDueToActivateItem); 494 } 495 } 496 497 mIsOpen = false; 498 499 // Do the rest of the MenuClosed work in MenuClosedAsync. 500 // MenuClosed() is called from -[NSMenuDelegate menuDidClose:]. If a menuitem was clicked, 501 // menuDidClose is called *before* menuItemHit for the clicked menu item is called. 502 // This runnable will be canceled if ~nsMenuX runs before the runnable. 503 // The runnable object must not hold a strong reference to the nsMenuX, so that there is no 504 // reference cycle. 505 class MenuClosedAsyncRunnable final : public mozilla::CancelableRunnable { 506 public: 507 explicit MenuClosedAsyncRunnable(nsMenuX* aMenu) 508 : CancelableRunnable("MenuClosedAsyncRunnable"), mMenu(aMenu) {} 509 510 nsresult Run() override { 511 if (mMenu) { 512 RefPtr<nsMenuX> menu = mMenu; 513 menu->MenuClosedAsync(); 514 mMenu = nullptr; 515 } 516 return NS_OK; 517 } 518 nsresult Cancel() override { 519 mMenu = nullptr; 520 return NS_OK; 521 } 522 523 private: 524 nsMenuX* mMenu; // weak, cleared by Cancel() and Run() 525 }; 526 527 mPendingAsyncMenuCloseRunnable = new MenuClosedAsyncRunnable(this); 528 529 if (aEntireMenuClosingDueToActivateItem) { 530 // Delay the call to MenuClosedAsync until after the menu's event loop has been exited, by using 531 // -[MOZMenuOpeningCoordinator runAfterMenuClosed:]. Otherwise, the runnable might potentially 532 // run before the event loop has been exited, and MenuClosedAsync() would flush the pending 533 // command runnable for the menu activation, and then the command event would run inside the 534 // menu's event loop which is what we're trying to avoid. 535 [MOZMenuOpeningCoordinator.sharedInstance runAfterMenuClosed:mPendingAsyncMenuCloseRunnable]; 536 } else { 537 // Just dispatch to the Gecko event queue. 538 // One way to get here is if a submenu is closed but the rest of the menu stays open; in that 539 // case, we really can't use runAfterMenuClosed because the submenu's MenuClosedAsync method 540 // would run way too late. 541 NS_DispatchToCurrentThread(mPendingAsyncMenuCloseRunnable); 542 } 543} 544 545void nsMenuX::FlushMenuClosedRunnable() { 546 // If any of our submenus have a pending menu closed runnable, make sure those run first. 547 for (auto& child : mMenuChildren) { 548 if (child.is<RefPtr<nsMenuX>>()) { 549 child.as<RefPtr<nsMenuX>>()->FlushMenuClosedRunnable(); 550 } 551 } 552 553 if (mPendingAsyncMenuCloseRunnable) { 554 MenuClosedAsync(); 555 } 556} 557 558void nsMenuX::MenuClosedAsync() { 559 if (mPendingAsyncMenuCloseRunnable) { 560 mPendingAsyncMenuCloseRunnable->Cancel(); 561 mPendingAsyncMenuCloseRunnable = nullptr; 562 } 563 564 // If we have pending command events, run those first. 565 nsTArray<RefPtr<Runnable>> runnables = std::move(mPendingCommandRunnables); 566 for (auto& runnable : runnables) { 567 runnable->Run(); 568 } 569 570 // Make sure no item is highlighted. 571 OnHighlightedItemChanged(Nothing()); 572 573 nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent(); 574 nsCOMPtr<nsIContent> dispatchTo = popupContent ? popupContent : mContent; 575 576 nsEventStatus status = nsEventStatus_eIgnore; 577 WidgetMouseEvent popupHiding(true, eXULPopupHiding, nullptr, WidgetMouseEvent::eReal); 578 EventDispatcher::Dispatch(dispatchTo, nullptr, &popupHiding, nullptr, &status); 579 580 mIsOpenForGecko = false; 581 582 if (mContent->IsElement()) { 583 mContent->AsElement()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::open, true); 584 } 585 586 WidgetMouseEvent popupHidden(true, eXULPopupHidden, nullptr, WidgetMouseEvent::eReal); 587 EventDispatcher::Dispatch(dispatchTo, nullptr, &popupHidden, nullptr, &status); 588 589 // Notify our observer. 590 if (mObserver && popupContent) { 591 mObserver->OnMenuClosed(popupContent->AsElement()); 592 } 593} 594 595void nsMenuX::ActivateItemAfterClosing(RefPtr<nsMenuItemX>&& aItem, NSEventModifierFlags aModifiers, 596 int16_t aButton) { 597 NS_OBJC_BEGIN_TRY_ABORT_BLOCK; 598 599 class DoCommandRunnable final : public mozilla::Runnable { 600 public: 601 explicit DoCommandRunnable(RefPtr<nsMenuItemX>&& aItem, NSEventModifierFlags aModifiers, 602 int16_t aButton) 603 : Runnable("DoCommandRunnable"), 604 mMenuItem(aItem), 605 mModifiers(aModifiers), 606 mButton(aButton) {} 607 608 nsresult Run() override { 609 if (mMenuItem) { 610 RefPtr<nsMenuItemX> menuItem = std::move(mMenuItem); 611 menuItem->DoCommand(mModifiers, mButton); 612 } 613 return NS_OK; 614 } 615 616 private: 617 RefPtr<nsMenuItemX> mMenuItem; // cleared by Run() 618 NSEventModifierFlags mModifiers; 619 int16_t mButton; 620 }; 621 RefPtr<Runnable> doCommandAsync = new DoCommandRunnable(std::move(aItem), aModifiers, aButton); 622 mPendingCommandRunnables.AppendElement(doCommandAsync); 623 624 // Delay the command event until after the menu's event loop has been exited, by using 625 // -[MOZMenuOpeningCoordinator runAfterMenuClosed:]. Otherwise, the runnable might potentially 626 // run inside the menu's nested event loop, and command event handlers can do arbitrary things 627 // like opening modal windows which spawn more nested event loops. This repeated nesting of event 628 // loops is something we'd like to avoid. 629 [MOZMenuOpeningCoordinator.sharedInstance runAfterMenuClosed:std::move(doCommandAsync)]; 630 631 NS_OBJC_END_TRY_ABORT_BLOCK; 632} 633 634bool nsMenuX::Close() { 635 NS_OBJC_BEGIN_TRY_ABORT_BLOCK; 636 637 if (mDidFirePopupshowingAndIsApprovedToOpen && !mIsOpen) { 638 // Close is being called right after this menu was opened, but before MenuOpened() had a chance 639 // to run. Call it here so that we can go through the entire popupshown -> popuphiding -> 640 // popuphidden sequence. Some callers expect to get a popuphidden event even if they close the 641 // popup before it was fully open. 642 MenuOpened(); 643 } 644 645 FlushMenuOpenedRunnable(); 646 647 bool wasOpen = mIsOpenForGecko; 648 649 if (mIsOpen) { 650 // Close the menu. 651 // We usually don't get here during normal Firefox usage: If the user closes the menu by 652 // clicking an item, or by clicking outside the menu, or by pressing escape, then the menu gets 653 // closed by macOS, and not by a call to nsMenuX::Close(). 654 // If we do get here, it's usually because we're running an automated test. Close the menu 655 // without the fade-out animation so that we don't unnecessarily slow down the automated tests. 656 [mNativeMenu cancelTrackingWithoutAnimation]; 657 MOZMenuOpeningCoordinator.needToUnwindForMenuClosing = YES; 658 659 // Handle closing synchronously. 660 MenuClosed(); 661 } 662 663 FlushMenuClosedRunnable(); 664 665 return wasOpen; 666 667 NS_OBJC_END_TRY_ABORT_BLOCK; 668} 669 670void nsMenuX::OnHighlightedItemChanged(const Maybe<uint32_t>& aNewHighlightedIndex) { 671 if (mHighlightedItemIndex == aNewHighlightedIndex) { 672 return; 673 } 674 675 if (mHighlightedItemIndex) { 676 Maybe<nsMenuX::MenuChild> target = GetVisibleItemAt(*mHighlightedItemIndex); 677 if (target && target->is<RefPtr<nsMenuItemX>>()) { 678 bool handlerCalledPreventDefault; // but we don't actually care 679 target->as<RefPtr<nsMenuItemX>>()->DispatchDOMEvent(u"DOMMenuItemInactive"_ns, 680 &handlerCalledPreventDefault); 681 } 682 } 683 if (aNewHighlightedIndex) { 684 Maybe<nsMenuX::MenuChild> target = GetVisibleItemAt(*aNewHighlightedIndex); 685 if (target && target->is<RefPtr<nsMenuItemX>>()) { 686 bool handlerCalledPreventDefault; // but we don't actually care 687 target->as<RefPtr<nsMenuItemX>>()->DispatchDOMEvent(u"DOMMenuItemActive"_ns, 688 &handlerCalledPreventDefault); 689 } 690 } 691 mHighlightedItemIndex = aNewHighlightedIndex; 692} 693 694void nsMenuX::OnWillActivateItem(NSMenuItem* aItem) { 695 if (!mIsOpenForGecko) { 696 return; 697 } 698 699 if (mMenuGroupOwner && mObserver) { 700 nsMenuItemX* item = mMenuGroupOwner->GetMenuItemForCommandID(uint32_t(aItem.tag)); 701 if (item && item->Content()->IsElement()) { 702 RefPtr<dom::Element> itemElement = item->Content()->AsElement(); 703 if (nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent()) { 704 mObserver->OnMenuWillActivateItem(popupContent->AsElement(), itemElement); 705 } 706 } 707 } 708} 709 710// Flushes style. 711static NSUserInterfaceLayoutDirection DirectionForElement(dom::Element* aElement) { 712 // Get the direction from the computed style so that inheritance into submenus is respected. 713 // aElement may not have a frame. 714 RefPtr<ComputedStyle> sc = nsComputedDOMStyle::GetComputedStyle(aElement, nullptr); 715 if (!sc) { 716 return NSApp.userInterfaceLayoutDirection; 717 } 718 719 switch (sc->StyleVisibility()->mDirection) { 720 case StyleDirection::Ltr: 721 return NSUserInterfaceLayoutDirectionLeftToRight; 722 case StyleDirection::Rtl: 723 return NSUserInterfaceLayoutDirectionRightToLeft; 724 } 725} 726 727void nsMenuX::RebuildMenu() { 728 MOZ_RELEASE_ASSERT(mNeedsRebuild); 729 gConstructingMenu = true; 730 731 // Retrieve our menupopup. 732 nsCOMPtr<nsIContent> menuPopup = GetMenuPopupContent(); 733 if (!menuPopup) { 734 gConstructingMenu = false; 735 return; 736 } 737 738 if (menuPopup->IsElement()) { 739 mNativeMenu.userInterfaceLayoutDirection = DirectionForElement(menuPopup->AsElement()); 740 } 741 742 // Iterate over the kids 743 for (nsIContent* child = menuPopup->GetFirstChild(); child; child = child->GetNextSibling()) { 744 if (Maybe<MenuChild> menuChild = CreateMenuChild(child)) { 745 AddMenuChild(std::move(*menuChild)); 746 } 747 } // for each menu item 748 749 InsertPlaceholderIfNeeded(); 750 751 gConstructingMenu = false; 752 mNeedsRebuild = false; 753} 754 755void nsMenuX::InsertPlaceholderIfNeeded() { 756 NS_OBJC_BEGIN_TRY_ABORT_BLOCK; 757 758 if ([mNativeMenu numberOfItems] == 0) { 759 MOZ_RELEASE_ASSERT(mVisibleItemsCount == 0); 760 NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""]; 761 item.enabled = NO; 762 item.view = [[[NSView alloc] initWithFrame:NSMakeRect(0, 0, 150, 1)] autorelease]; 763 [mNativeMenu addItem:item]; 764 [item release]; 765 } 766 767 NS_OBJC_END_TRY_ABORT_BLOCK; 768} 769 770void nsMenuX::RemovePlaceholderIfPresent() { 771 NS_OBJC_BEGIN_TRY_ABORT_BLOCK; 772 773 if (mVisibleItemsCount == 0 && [mNativeMenu numberOfItems] == 1) { 774 // Remove the placeholder. 775 [mNativeMenu removeItemAtIndex:0]; 776 } 777 778 NS_OBJC_END_TRY_ABORT_BLOCK; 779} 780 781void nsMenuX::SetRebuild(bool aNeedsRebuild) { 782 if (!gConstructingMenu) { 783 mNeedsRebuild = aNeedsRebuild; 784 if (mParent && mParent->AsMenuBar()) { 785 mParent->AsMenuBar()->SetNeedsRebuild(); 786 } 787 } 788} 789 790nsresult nsMenuX::SetEnabled(bool aIsEnabled) { 791 if (aIsEnabled != mIsEnabled) { 792 // we always want to rebuild when this changes 793 mIsEnabled = aIsEnabled; 794 mNativeMenuItem.enabled = mIsEnabled; 795 } 796 return NS_OK; 797} 798 799nsresult nsMenuX::GetEnabled(bool* aIsEnabled) { 800 NS_ENSURE_ARG_POINTER(aIsEnabled); 801 *aIsEnabled = mIsEnabled; 802 return NS_OK; 803} 804 805GeckoNSMenu* nsMenuX::CreateMenuWithGeckoString(nsString& aMenuTitle) { 806 NS_OBJC_BEGIN_TRY_ABORT_BLOCK; 807 808 NSString* title = [NSString stringWithCharacters:(UniChar*)aMenuTitle.get() 809 length:aMenuTitle.Length()]; 810 GeckoNSMenu* myMenu = [[GeckoNSMenu alloc] initWithTitle:title]; 811 myMenu.delegate = mMenuDelegate; 812 813 // We don't want this menu to auto-enable menu items because then Cocoa 814 // overrides our decisions and things get incorrectly enabled/disabled. 815 myMenu.autoenablesItems = NO; 816 817 // Disable the Services item for now. Bug 660452 tracks turning this on for the appropriate menus. 818 myMenu.allowsContextMenuPlugIns = NO; 819 820 // we used to install Carbon event handlers here, but since NSMenu* doesn't 821 // create its underlying MenuRef until just before display, we delay until 822 // that happens. Now we install the event handlers when Cocoa notifies 823 // us that a menu is about to display - see the Cocoa MenuDelegate class. 824 825 return myMenu; 826 827 NS_OBJC_END_TRY_ABORT_BLOCK; 828} 829 830Maybe<nsMenuX::MenuChild> nsMenuX::CreateMenuChild(nsIContent* aContent) { 831 if (aContent->IsAnyOfXULElements(nsGkAtoms::menuitem, nsGkAtoms::menuseparator)) { 832 return Some(MenuChild(CreateMenuItem(aContent))); 833 } 834 if (aContent->IsXULElement(nsGkAtoms::menu)) { 835 return Some(MenuChild(MakeRefPtr<nsMenuX>(this, mMenuGroupOwner, aContent))); 836 } 837 return {}; 838} 839 840RefPtr<nsMenuItemX> nsMenuX::CreateMenuItem(nsIContent* aMenuItemContent) { 841 MOZ_RELEASE_ASSERT(aMenuItemContent); 842 843 nsAutoString menuitemName; 844 if (aMenuItemContent->IsElement()) { 845 aMenuItemContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::label, menuitemName); 846 } 847 848 EMenuItemType itemType = eRegularMenuItemType; 849 if (aMenuItemContent->IsXULElement(nsGkAtoms::menuseparator)) { 850 itemType = eSeparatorMenuItemType; 851 } else if (aMenuItemContent->IsElement()) { 852 static Element::AttrValuesArray strings[] = {nsGkAtoms::checkbox, nsGkAtoms::radio, nullptr}; 853 switch (aMenuItemContent->AsElement()->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::type, 854 strings, eCaseMatters)) { 855 case 0: 856 itemType = eCheckboxMenuItemType; 857 break; 858 case 1: 859 itemType = eRadioMenuItemType; 860 break; 861 } 862 } 863 864 return MakeRefPtr<nsMenuItemX>(this, menuitemName, itemType, mMenuGroupOwner, aMenuItemContent); 865} 866 867// This menu is about to open. Returns false if the handler wants to stop the opening of the menu. 868bool nsMenuX::OnOpen() { 869 if (mDidFirePopupshowingAndIsApprovedToOpen) { 870 return true; 871 } 872 873 if (mIsOpen) { 874 NS_WARNING("nsMenuX::OnOpen() called while the menu is already considered to be open. This " 875 "seems odd."); 876 } 877 878 nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent(); 879 880 if (mObserver && popupContent) { 881 mObserver->OnMenuWillOpen(popupContent->AsElement()); 882 } 883 884 nsEventStatus status = nsEventStatus_eIgnore; 885 WidgetMouseEvent event(true, eXULPopupShowing, nullptr, WidgetMouseEvent::eReal); 886 887 nsresult rv = NS_OK; 888 nsIContent* dispatchTo = popupContent ? popupContent : mContent; 889 rv = EventDispatcher::Dispatch(dispatchTo, nullptr, &event, nullptr, &status); 890 if (NS_FAILED(rv) || status == nsEventStatus_eConsumeNoDefault) { 891 return false; 892 } 893 894 DidFirePopupShowing(); 895 896 return true; 897} 898 899void nsMenuX::DidFirePopupShowing() { 900 mDidFirePopupshowingAndIsApprovedToOpen = true; 901 902 // If the open is going to succeed we need to walk our menu items, checking to 903 // see if any of them have a command attribute. If so, several attributes 904 // must potentially be updated. 905 906 nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent(); 907 if (!popupContent) { 908 return; 909 } 910 911 nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); 912 if (pm) { 913 pm->UpdateMenuItems(popupContent); 914 } 915} 916 917// Find the |menupopup| child in the |popup| representing this menu. It should be one 918// of a very few children so we won't be iterating over a bazillion menu items to find 919// it (so the strcmp won't kill us). 920already_AddRefed<nsIContent> nsMenuX::GetMenuPopupContent() { 921 // Check to see if we are a "menupopup" node (if we are a native menu). 922 if (mContent->IsXULElement(nsGkAtoms::menupopup)) { 923 return do_AddRef(mContent); 924 } 925 926 // Otherwise check our child nodes. 927 928 for (RefPtr<nsIContent> child = mContent->GetFirstChild(); child; 929 child = child->GetNextSibling()) { 930 if (child->IsXULElement(nsGkAtoms::menupopup)) { 931 return child.forget(); 932 } 933 } 934 935 return nullptr; 936} 937 938bool nsMenuX::IsXULHelpMenu(nsIContent* aMenuContent) { 939 bool retval = false; 940 if (aMenuContent && aMenuContent->IsElement()) { 941 nsAutoString id; 942 aMenuContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::id, id); 943 if (id.Equals(u"helpMenu"_ns)) { 944 retval = true; 945 } 946 } 947 return retval; 948} 949 950// 951// nsChangeObserver 952// 953 954void nsMenuX::ObserveAttributeChanged(dom::Document* aDocument, nsIContent* aContent, 955 nsAtom* aAttribute) { 956 NS_OBJC_BEGIN_TRY_ABORT_BLOCK; 957 958 // ignore the |open| attribute, which is by far the most common 959 if (gConstructingMenu || (aAttribute == nsGkAtoms::open)) { 960 return; 961 } 962 963 if (aAttribute == nsGkAtoms::disabled) { 964 SetEnabled(!mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, 965 nsGkAtoms::_true, eCaseMatters)); 966 } else if (aAttribute == nsGkAtoms::label) { 967 mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::label, mLabel); 968 NSString* newCocoaLabelString = nsMenuUtilsX::GetTruncatedCocoaLabel(mLabel); 969 mNativeMenu.title = newCocoaLabelString; 970 mNativeMenuItem.title = newCocoaLabelString; 971 } else if (aAttribute == nsGkAtoms::hidden || aAttribute == nsGkAtoms::collapsed) { 972 SetRebuild(true); 973 974 bool newVisible = !nsMenuUtilsX::NodeIsHiddenOrCollapsed(mContent); 975 976 // don't do anything if the state is correct already 977 if (newVisible == mVisible) { 978 return; 979 } 980 981 mVisible = newVisible; 982 if (mParent) { 983 RefPtr<nsMenuX> self = this; 984 mParent->MenuChildChangedVisibility(MenuChild(self), newVisible); 985 } 986 if (mVisible) { 987 SetupIcon(); 988 } 989 } else if (aAttribute == nsGkAtoms::image) { 990 SetupIcon(); 991 } 992 993 NS_OBJC_END_TRY_ABORT_BLOCK; 994} 995 996void nsMenuX::ObserveContentRemoved(dom::Document* aDocument, nsIContent* aContainer, 997 nsIContent* aChild, nsIContent* aPreviousSibling) { 998 if (gConstructingMenu) { 999 return; 1000 } 1001 1002 SetRebuild(true); 1003 mMenuGroupOwner->UnregisterForContentChanges(aChild); 1004 1005 if (!mIsOpen) { 1006 // We will update the menu contents the next time the menu is opened. 1007 return; 1008 } 1009 1010 // The menu is currently open. Remove the child from mMenuChildren and from our NSMenu. 1011 nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent(); 1012 if (popupContent && aContainer == popupContent && aChild->IsElement()) { 1013 if (Maybe<MenuChild> child = GetItemForElement(aChild->AsElement())) { 1014 RemoveMenuChild(*child); 1015 } 1016 } 1017} 1018 1019void nsMenuX::ObserveContentInserted(dom::Document* aDocument, nsIContent* aContainer, 1020 nsIContent* aChild) { 1021 if (gConstructingMenu) { 1022 return; 1023 } 1024 1025 SetRebuild(true); 1026 1027 if (!mIsOpen) { 1028 // We will update the menu contents the next time the menu is opened. 1029 return; 1030 } 1031 1032 // The menu is currently open. Insert the child into mMenuChildren and into our NSMenu. 1033 nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent(); 1034 if (popupContent && aContainer == popupContent) { 1035 if (Maybe<MenuChild> child = CreateMenuChild(aChild)) { 1036 InsertMenuChild(std::move(*child)); 1037 } 1038 } 1039} 1040 1041void nsMenuX::SetupIcon() { 1042 mIcon->SetupIcon(mContent); 1043 mNativeMenuItem.image = mIcon->GetIconImage(); 1044} 1045 1046void nsMenuX::IconUpdated() { 1047 mNativeMenuItem.image = mIcon->GetIconImage(); 1048 if (mIconListener) { 1049 mIconListener->IconUpdated(); 1050 } 1051} 1052 1053void nsMenuX::MenuChildChangedVisibility(const MenuChild& aChild, bool aIsVisible) { 1054 NS_OBJC_BEGIN_TRY_ABORT_BLOCK; 1055 1056 NSMenuItem* nativeItem = aChild.match( 1057 [](const RefPtr<nsMenuX>& aMenu) { return aMenu->NativeNSMenuItem(); }, 1058 [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->NativeNSMenuItem(); }); 1059 if (aIsVisible) { 1060 MOZ_RELEASE_ASSERT(!nativeItem.menu, 1061 "The native item should not be in a menu while it is hidden"); 1062 RemovePlaceholderIfPresent(); 1063 NSInteger insertionPoint = CalculateNativeInsertionPoint(aChild); 1064 [mNativeMenu insertItem:nativeItem atIndex:insertionPoint]; 1065 mVisibleItemsCount++; 1066 } else { 1067 MOZ_RELEASE_ASSERT([mNativeMenu indexOfItem:nativeItem] != -1, 1068 "The native item should be in this menu while it is visible"); 1069 [mNativeMenu removeItem:nativeItem]; 1070 mVisibleItemsCount--; 1071 InsertPlaceholderIfNeeded(); 1072 } 1073 1074 NS_OBJC_END_TRY_ABORT_BLOCK; 1075} 1076 1077NSInteger nsMenuX::CalculateNativeInsertionPoint(const MenuChild& aChild) { 1078 NSInteger insertionPoint = 0; 1079 for (auto& currItem : mMenuChildren) { 1080 // Using GetItemAt instead of GetVisibleItemAt to avoid O(N^2) 1081 if (currItem == aChild) { 1082 return insertionPoint; 1083 } 1084 NSMenuItem* nativeItem = currItem.match( 1085 [](const RefPtr<nsMenuX>& aMenu) { return aMenu->NativeNSMenuItem(); }, 1086 [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->NativeNSMenuItem(); }); 1087 // Only count visible items. 1088 if (nativeItem.menu) { 1089 insertionPoint++; 1090 } 1091 } 1092 return insertionPoint; 1093} 1094 1095void nsMenuX::Dump(uint32_t aIndent) const { 1096 printf("%*s - menu [%p] %-16s <%s>", aIndent * 2, "", this, 1097 mLabel.IsEmpty() ? "(empty label)" : NS_ConvertUTF16toUTF8(mLabel).get(), 1098 NS_ConvertUTF16toUTF8(mContent->NodeName()).get()); 1099 if (mNeedsRebuild) { 1100 printf(" [NeedsRebuild]"); 1101 } 1102 if (mIsOpen) { 1103 printf(" [Open]"); 1104 } 1105 if (mVisible) { 1106 printf(" [Visible]"); 1107 } 1108 if (mIsEnabled) { 1109 printf(" [IsEnabled]"); 1110 } 1111 printf(" (%d visible items)", int(mVisibleItemsCount)); 1112 printf("\n"); 1113 for (const auto& subitem : mMenuChildren) { 1114 subitem.match([=](const RefPtr<nsMenuX>& aMenu) { aMenu->Dump(aIndent + 1); }, 1115 [=](const RefPtr<nsMenuItemX>& aMenuItem) { aMenuItem->Dump(aIndent + 1); }); 1116 } 1117} 1118 1119// 1120// MenuDelegate Objective-C class, used to set up Carbon events 1121// 1122 1123@implementation MenuDelegate 1124 1125- (id)initWithGeckoMenu:(nsMenuX*)geckoMenu { 1126 if ((self = [super init])) { 1127 NS_ASSERTION(geckoMenu, 1128 "Cannot initialize native menu delegate with NULL gecko menu! Will crash!"); 1129 mGeckoMenu = geckoMenu; 1130 mBlocksToRunWhenOpen = [[NSMutableArray alloc] init]; 1131 } 1132 return self; 1133} 1134 1135- (void)dealloc { 1136 [mBlocksToRunWhenOpen release]; 1137 [super dealloc]; 1138} 1139 1140- (void)runBlockWhenOpen:(void (^)())block { 1141 [mBlocksToRunWhenOpen addObject:[[block copy] autorelease]]; 1142} 1143 1144- (void)menu:(NSMenu*)aMenu willHighlightItem:(NSMenuItem*)aItem { 1145 if (!aMenu || !mGeckoMenu) { 1146 return; 1147 } 1148 1149 Maybe<uint32_t> index = 1150 aItem ? Some(static_cast<uint32_t>([aMenu indexOfItem:aItem])) : Nothing(); 1151 mGeckoMenu->OnHighlightedItemChanged(index); 1152} 1153 1154- (void)menuWillOpen:(NSMenu*)menu { 1155 for (void (^block)() in mBlocksToRunWhenOpen) { 1156 block(); 1157 } 1158 [mBlocksToRunWhenOpen removeAllObjects]; 1159 1160 if (!mGeckoMenu) { 1161 return; 1162 } 1163 1164 // Don't do anything while the OS is (re)indexing our menus (on Leopard and 1165 // higher). This stops the Help menu from being able to search in our 1166 // menus, but it also resolves many other problems. 1167 if (nsMenuX::sIndexingMenuLevel > 0) { 1168 return; 1169 } 1170 1171 if (self.menuIsInMenubar) { 1172 // If a menu in the menubar is trying open while a non-native menu is open, roll up the 1173 // non-native menu and reject the menubar opening attempt, effectively consuming the event. 1174 nsIRollupListener* rollupListener = nsBaseWidget::GetActiveRollupListener(); 1175 if (rollupListener) { 1176 nsCOMPtr<nsIWidget> rollupWidget = rollupListener->GetRollupWidget(); 1177 if (rollupWidget) { 1178 rollupListener->Rollup(0, true, nullptr, nullptr); 1179 [menu cancelTracking]; 1180 return; 1181 } 1182 } 1183 } 1184 1185 // Hold a strong reference to mGeckoMenu while calling its methods. 1186 RefPtr<nsMenuX> geckoMenu = mGeckoMenu; 1187 geckoMenu->MenuOpened(); 1188} 1189 1190- (void)menuDidClose:(NSMenu*)menu { 1191 if (!mGeckoMenu) { 1192 return; 1193 } 1194 1195 // Don't do anything while the OS is (re)indexing our menus (on Leopard and 1196 // higher). This stops the Help menu from being able to search in our 1197 // menus, but it also resolves many other problems. 1198 if (nsMenuX::sIndexingMenuLevel > 0) { 1199 return; 1200 } 1201 1202 // Hold a strong reference to mGeckoMenu while calling its methods. 1203 RefPtr<nsMenuX> geckoMenu = mGeckoMenu; 1204 geckoMenu->MenuClosed(); 1205} 1206 1207// This is called after menuDidClose:. 1208- (void)menu:(NSMenu*)aMenu willActivateItem:(NSMenuItem*)aItem { 1209 if (!mGeckoMenu) { 1210 return; 1211 } 1212 1213 // Hold a strong reference to mGeckoMenu while calling its methods. 1214 RefPtr<nsMenuX> geckoMenu = mGeckoMenu; 1215 geckoMenu->OnWillActivateItem(aItem); 1216} 1217 1218@end 1219 1220// OS X Leopard (at least as of 10.5.2) has an obscure bug triggered by some 1221// behavior that's present in Mozilla.org browsers but not (as best I can 1222// tell) in Apple products like Safari. (It's not yet clear exactly what this 1223// behavior is.) 1224// 1225// The bug is that sometimes you crash on quit in nsMenuX::RemoveAll(), on a 1226// call to [NSMenu removeItemAtIndex:]. The crash is caused by trying to 1227// access a deleted NSMenuItem object (sometimes (perhaps always?) by trying 1228// to send it a _setChangedFlags: message). Though this object was deleted 1229// some time ago, it remains registered as a potential target for a particular 1230// key equivalent. So when [NSMenu removeItemAtIndex:] removes the current 1231// target for that same key equivalent, the OS tries to "activate" the 1232// previous target. 1233// 1234// The underlying reason appears to be that NSMenu's _addItem:toTable: and 1235// _removeItem:fromTable: methods (which are used to keep a hashtable of 1236// registered key equivalents) don't properly "retain" and "release" 1237// NSMenuItem objects as they are added to and removed from the hashtable. 1238// 1239// Our (hackish) workaround is to shadow the OS's hashtable with another 1240// hastable of our own (gShadowKeyEquivDB), and use it to "retain" and 1241// "release" NSMenuItem objects as needed. This resolves bmo bugs 422287 and 1242// 423669. When (if) Apple fixes this bug, we can remove this workaround. 1243 1244static NSMutableDictionary* gShadowKeyEquivDB = nil; 1245 1246// Class for values in gShadowKeyEquivDB. 1247 1248@interface KeyEquivDBItem : NSObject { 1249 NSMenuItem* mItem; 1250 NSMutableSet* mTables; 1251} 1252 1253- (id)initWithItem:(NSMenuItem*)aItem table:(NSMapTable*)aTable; 1254- (BOOL)hasTable:(NSMapTable*)aTable; 1255- (int)addTable:(NSMapTable*)aTable; 1256- (int)removeTable:(NSMapTable*)aTable; 1257 1258@end 1259 1260@implementation KeyEquivDBItem 1261 1262- (id)initWithItem:(NSMenuItem*)aItem table:(NSMapTable*)aTable { 1263 if (!gShadowKeyEquivDB) { 1264 gShadowKeyEquivDB = [[NSMutableDictionary alloc] init]; 1265 } 1266 self = [super init]; 1267 if (aItem && aTable) { 1268 mTables = [[NSMutableSet alloc] init]; 1269 mItem = [aItem retain]; 1270 [mTables addObject:[NSValue valueWithPointer:aTable]]; 1271 } else { 1272 mTables = nil; 1273 mItem = nil; 1274 } 1275 return self; 1276} 1277 1278- (void)dealloc { 1279 if (mTables) { 1280 [mTables release]; 1281 } 1282 if (mItem) { 1283 [mItem release]; 1284 } 1285 [super dealloc]; 1286} 1287 1288- (BOOL)hasTable:(NSMapTable*)aTable { 1289 return [mTables member:[NSValue valueWithPointer:aTable]] ? YES : NO; 1290} 1291 1292// Does nothing if aTable (its index value) is already present in mTables. 1293- (int)addTable:(NSMapTable*)aTable { 1294 if (aTable) { 1295 [mTables addObject:[NSValue valueWithPointer:aTable]]; 1296 } 1297 return [mTables count]; 1298} 1299 1300- (int)removeTable:(NSMapTable*)aTable { 1301 if (aTable) { 1302 NSValue* objectToRemove = [mTables member:[NSValue valueWithPointer:aTable]]; 1303 if (objectToRemove) { 1304 [mTables removeObject:objectToRemove]; 1305 } 1306 } 1307 return [mTables count]; 1308} 1309 1310@end 1311 1312@interface NSMenu (MethodSwizzling) 1313+ (void)nsMenuX_NSMenu_addItem:(NSMenuItem*)aItem toTable:(NSMapTable*)aTable; 1314+ (void)nsMenuX_NSMenu_removeItem:(NSMenuItem*)aItem fromTable:(NSMapTable*)aTable; 1315@end 1316 1317@implementation NSMenu (MethodSwizzling) 1318 1319+ (void)nsMenuX_NSMenu_addItem:(NSMenuItem*)aItem toTable:(NSMapTable*)aTable { 1320 if (aItem && aTable) { 1321 NSValue* key = [NSValue valueWithPointer:aItem]; 1322 KeyEquivDBItem* shadowItem = [gShadowKeyEquivDB objectForKey:key]; 1323 if (shadowItem) { 1324 [shadowItem addTable:aTable]; 1325 } else { 1326 shadowItem = [[KeyEquivDBItem alloc] initWithItem:aItem table:aTable]; 1327 [gShadowKeyEquivDB setObject:shadowItem forKey:key]; 1328 // Release after [NSMutableDictionary setObject:forKey:] retains it (so 1329 // that it will get dealloced when removeObjectForKey: is called). 1330 [shadowItem release]; 1331 } 1332 } 1333 1334 [self nsMenuX_NSMenu_addItem:aItem toTable:aTable]; 1335} 1336 1337+ (void)nsMenuX_NSMenu_removeItem:(NSMenuItem*)aItem fromTable:(NSMapTable*)aTable { 1338 [self nsMenuX_NSMenu_removeItem:aItem fromTable:aTable]; 1339 1340 if (aItem && aTable) { 1341 NSValue* key = [NSValue valueWithPointer:aItem]; 1342 KeyEquivDBItem* shadowItem = [gShadowKeyEquivDB objectForKey:key]; 1343 if (shadowItem && [shadowItem hasTable:aTable]) { 1344 if (![shadowItem removeTable:aTable]) { 1345 [gShadowKeyEquivDB removeObjectForKey:key]; 1346 } 1347 } 1348 } 1349} 1350 1351@end 1352 1353// This class is needed to keep track of when the OS is (re)indexing all of 1354// our menus. This appears to only happen on Leopard and higher, and can 1355// be triggered by opening the Help menu. Some operations are unsafe while 1356// this is happening -- notably the calls to [[NSImage alloc] 1357// initWithSize:imageRect.size] and [newImage lockFocus] in nsMenuItemIconX:: 1358// OnStopFrame(). But we don't yet have a complete list, and Apple doesn't 1359// yet have any documentation on this subject. (Apple also doesn't yet have 1360// any documented way to find the information we seek here.) The "original" 1361// of this class (the one whose indexMenuBarDynamically method we hook) is 1362// defined in the Shortcut framework in /System/Library/PrivateFrameworks. 1363@interface NSObject (SCTGRLIndexMethodSwizzling) 1364- (void)nsMenuX_SCTGRLIndex_indexMenuBarDynamically; 1365@end 1366 1367@implementation NSObject (SCTGRLIndexMethodSwizzling) 1368 1369- (void)nsMenuX_SCTGRLIndex_indexMenuBarDynamically { 1370 // This method appears to be called (once) whenever the OS (re)indexes our 1371 // menus. sIndexingMenuLevel is a int32_t just in case it might be 1372 // reentered. As it's running, it spawns calls to two undocumented 1373 // HIToolbox methods (_SimulateMenuOpening() and _SimulateMenuClosed()), 1374 // which "simulate" the opening and closing of our menus without actually 1375 // displaying them. 1376 ++nsMenuX::sIndexingMenuLevel; 1377 [self nsMenuX_SCTGRLIndex_indexMenuBarDynamically]; 1378 --nsMenuX::sIndexingMenuLevel; 1379} 1380 1381@end 1382