michael@0: /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #include michael@0: michael@0: #include "nsMenuX.h" michael@0: #include "nsMenuItemX.h" michael@0: #include "nsMenuUtilsX.h" michael@0: #include "nsMenuItemIconX.h" michael@0: #include "nsStandaloneNativeMenu.h" michael@0: michael@0: #include "nsObjCExceptions.h" michael@0: michael@0: #include "nsToolkit.h" michael@0: #include "nsCocoaFeatures.h" michael@0: #include "nsCocoaUtils.h" michael@0: #include "nsCOMPtr.h" michael@0: #include "prinrval.h" michael@0: #include "nsString.h" michael@0: #include "nsReadableUtils.h" michael@0: #include "nsUnicharUtils.h" michael@0: #include "plstr.h" michael@0: #include "nsGkAtoms.h" michael@0: #include "nsCRT.h" michael@0: #include "nsBaseWidget.h" michael@0: michael@0: #include "nsIDocument.h" michael@0: #include "nsIContent.h" michael@0: #include "nsIDOMDocument.h" michael@0: #include "nsIDocumentObserver.h" michael@0: #include "nsIComponentManager.h" michael@0: #include "nsIRollupListener.h" michael@0: #include "nsIDOMElement.h" michael@0: #include "nsBindingManager.h" michael@0: #include "nsIServiceManager.h" michael@0: #include "nsXULPopupManager.h" michael@0: #include "nsCxPusher.h" michael@0: michael@0: #include "jsapi.h" michael@0: #include "nsIScriptGlobalObject.h" michael@0: #include "nsIScriptContext.h" michael@0: #include "nsIXPConnect.h" michael@0: michael@0: #include "mozilla/MouseEvents.h" michael@0: michael@0: using namespace mozilla; michael@0: michael@0: static bool gConstructingMenu = false; michael@0: static bool gMenuMethodsSwizzled = false; michael@0: michael@0: int32_t nsMenuX::sIndexingMenuLevel = 0; michael@0: using mozilla::AutoPushJSContext; michael@0: michael@0: michael@0: // michael@0: // Objective-C class used for representedObject michael@0: // michael@0: michael@0: @implementation MenuItemInfo michael@0: michael@0: - (id) initWithMenuGroupOwner:(nsMenuGroupOwnerX *)aMenuGroupOwner michael@0: { michael@0: if ((self = [super init]) != nil) { michael@0: mMenuGroupOwner = nullptr; michael@0: [self setMenuGroupOwner:aMenuGroupOwner]; michael@0: } michael@0: return self; michael@0: } michael@0: michael@0: - (void) dealloc michael@0: { michael@0: [self setMenuGroupOwner:nullptr]; michael@0: [super dealloc]; michael@0: } michael@0: michael@0: - (nsMenuGroupOwnerX *) menuGroupOwner michael@0: { michael@0: return mMenuGroupOwner; michael@0: } michael@0: michael@0: - (void) setMenuGroupOwner:(nsMenuGroupOwnerX *)aMenuGroupOwner michael@0: { michael@0: // weak reference as the nsMenuGroupOwnerX owns all of its sub-objects michael@0: mMenuGroupOwner = aMenuGroupOwner; michael@0: } michael@0: michael@0: @end michael@0: michael@0: michael@0: // michael@0: // nsMenuX michael@0: // michael@0: michael@0: nsMenuX::nsMenuX() michael@0: : mVisibleItemsCount(0), mParent(nullptr), mMenuGroupOwner(nullptr), michael@0: mNativeMenu(nil), mNativeMenuItem(nil), mIsEnabled(true), michael@0: mDestroyHandlerCalled(false), mNeedsRebuild(true), michael@0: mConstructed(false), mVisible(true), mXBLAttached(false) michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK; michael@0: michael@0: if (!gMenuMethodsSwizzled) { michael@0: nsToolkit::SwizzleMethods([NSMenu class], @selector(_addItem:toTable:), michael@0: @selector(nsMenuX_NSMenu_addItem:toTable:), true); michael@0: nsToolkit::SwizzleMethods([NSMenu class], @selector(_removeItem:fromTable:), michael@0: @selector(nsMenuX_NSMenu_removeItem:fromTable:), true); michael@0: // On SnowLeopard the Shortcut framework (which contains the michael@0: // SCTGRLIndex class) is loaded on demand, whenever the user first opens michael@0: // a menu (which normally hasn't happened yet). So we need to load it michael@0: // here explicitly. michael@0: dlopen("/System/Library/PrivateFrameworks/Shortcut.framework/Shortcut", RTLD_LAZY); michael@0: Class SCTGRLIndexClass = ::NSClassFromString(@"SCTGRLIndex"); michael@0: nsToolkit::SwizzleMethods(SCTGRLIndexClass, @selector(indexMenuBarDynamically), michael@0: @selector(nsMenuX_SCTGRLIndex_indexMenuBarDynamically)); michael@0: michael@0: gMenuMethodsSwizzled = true; michael@0: } michael@0: michael@0: mMenuDelegate = [[MenuDelegate alloc] initWithGeckoMenu:this]; michael@0: michael@0: if (!nsMenuBarX::sNativeEventTarget) michael@0: nsMenuBarX::sNativeEventTarget = [[NativeMenuItemTarget alloc] init]; michael@0: michael@0: MOZ_COUNT_CTOR(nsMenuX); michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK; michael@0: } michael@0: michael@0: nsMenuX::~nsMenuX() michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK; michael@0: michael@0: // Prevent the icon object from outliving us. michael@0: if (mIcon) michael@0: mIcon->Destroy(); michael@0: michael@0: RemoveAll(); michael@0: michael@0: [mNativeMenu setDelegate:nil]; michael@0: [mNativeMenu release]; michael@0: [mMenuDelegate release]; michael@0: // autorelease the native menu item so that anything else happening to this michael@0: // object happens before the native menu item actually dies michael@0: [mNativeMenuItem autorelease]; michael@0: michael@0: // alert the change notifier we don't care no more michael@0: if (mContent) michael@0: mMenuGroupOwner->UnregisterForContentChanges(mContent); michael@0: michael@0: MOZ_COUNT_DTOR(nsMenuX); michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK; michael@0: } michael@0: michael@0: nsresult nsMenuX::Create(nsMenuObjectX* aParent, nsMenuGroupOwnerX* aMenuGroupOwner, nsIContent* aNode) michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; michael@0: michael@0: mContent = aNode; michael@0: mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::label, mLabel); michael@0: mNativeMenu = CreateMenuWithGeckoString(mLabel); michael@0: michael@0: // register this menu to be notified when changes are made to our content object michael@0: mMenuGroupOwner = aMenuGroupOwner; // weak ref michael@0: NS_ASSERTION(mMenuGroupOwner, "No menu owner given, must have one"); michael@0: mMenuGroupOwner->RegisterForContentChanges(mContent, this); michael@0: michael@0: mParent = aParent; michael@0: // our parent could be either a menu bar (if we're toplevel) or a menu (if we're a submenu) michael@0: michael@0: #ifdef DEBUG michael@0: nsMenuObjectTypeX parentType = michael@0: #endif michael@0: mParent->MenuObjectType(); michael@0: NS_ASSERTION((parentType == eMenuBarObjectType || parentType == eSubmenuObjectType || parentType == eStandaloneNativeMenuObjectType), michael@0: "Menu parent not a menu bar, menu, or native menu!"); michael@0: michael@0: if (nsMenuUtilsX::NodeIsHiddenOrCollapsed(mContent)) michael@0: mVisible = false; michael@0: if (mContent->GetChildCount() == 0) michael@0: mVisible = false; michael@0: michael@0: NSString *newCocoaLabelString = nsMenuUtilsX::GetTruncatedCocoaLabel(mLabel); michael@0: mNativeMenuItem = [[NSMenuItem alloc] initWithTitle:newCocoaLabelString action:nil keyEquivalent:@""]; michael@0: [mNativeMenuItem setSubmenu:mNativeMenu]; michael@0: michael@0: SetEnabled(!mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, michael@0: nsGkAtoms::_true, eCaseMatters)); michael@0: michael@0: // We call MenuConstruct here because keyboard commands are dependent upon michael@0: // native menu items being created. If we only call MenuConstruct when a menu michael@0: // is actually selected, then we can't access keyboard commands until the michael@0: // menu gets selected, which is bad. michael@0: MenuConstruct(); michael@0: michael@0: mIcon = new nsMenuItemIconX(this, mContent, mNativeMenuItem); michael@0: michael@0: return NS_OK; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; michael@0: } michael@0: michael@0: nsresult nsMenuX::AddMenuItem(nsMenuItemX* aMenuItem) michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; michael@0: michael@0: if (!aMenuItem) michael@0: return NS_ERROR_INVALID_ARG; michael@0: michael@0: mMenuObjectsArray.AppendElement(aMenuItem); michael@0: if (nsMenuUtilsX::NodeIsHiddenOrCollapsed(aMenuItem->Content())) michael@0: return NS_OK; michael@0: ++mVisibleItemsCount; michael@0: michael@0: NSMenuItem* newNativeMenuItem = (NSMenuItem*)aMenuItem->NativeData(); michael@0: michael@0: // add the menu item to this menu michael@0: [mNativeMenu addItem:newNativeMenuItem]; michael@0: michael@0: // set up target/action michael@0: [newNativeMenuItem setTarget:nsMenuBarX::sNativeEventTarget]; michael@0: [newNativeMenuItem setAction:@selector(menuItemHit:)]; michael@0: michael@0: // set its command. we get the unique command id from the menubar michael@0: [newNativeMenuItem setTag:mMenuGroupOwner->RegisterForCommand(aMenuItem)]; michael@0: MenuItemInfo * info = [[MenuItemInfo alloc] initWithMenuGroupOwner:mMenuGroupOwner]; michael@0: [newNativeMenuItem setRepresentedObject:info]; michael@0: [info release]; michael@0: michael@0: return NS_OK; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; michael@0: } michael@0: michael@0: nsresult nsMenuX::AddMenu(nsMenuX* aMenu) michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; michael@0: michael@0: // Add a submenu michael@0: if (!aMenu) michael@0: return NS_ERROR_NULL_POINTER; michael@0: michael@0: mMenuObjectsArray.AppendElement(aMenu); michael@0: if (nsMenuUtilsX::NodeIsHiddenOrCollapsed(aMenu->Content())) michael@0: return NS_OK; michael@0: ++mVisibleItemsCount; michael@0: michael@0: // We have to add a menu item and then associate the menu with it michael@0: NSMenuItem* newNativeMenuItem = aMenu->NativeMenuItem(); michael@0: if (!newNativeMenuItem) michael@0: return NS_ERROR_FAILURE; michael@0: [mNativeMenu addItem:newNativeMenuItem]; michael@0: michael@0: [newNativeMenuItem setSubmenu:(NSMenu*)aMenu->NativeData()]; michael@0: michael@0: return NS_OK; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; michael@0: } michael@0: michael@0: // Includes all items, including hidden/collapsed ones michael@0: uint32_t nsMenuX::GetItemCount() michael@0: { michael@0: return mMenuObjectsArray.Length(); michael@0: } michael@0: michael@0: // Includes all items, including hidden/collapsed ones michael@0: nsMenuObjectX* nsMenuX::GetItemAt(uint32_t aPos) michael@0: { michael@0: if (aPos >= (uint32_t)mMenuObjectsArray.Length()) michael@0: return NULL; michael@0: michael@0: return mMenuObjectsArray[aPos]; michael@0: } michael@0: michael@0: // Only includes visible items michael@0: nsresult nsMenuX::GetVisibleItemCount(uint32_t &aCount) michael@0: { michael@0: aCount = mVisibleItemsCount; michael@0: return NS_OK; michael@0: } michael@0: michael@0: // Only includes visible items. Note that this is provides O(N) access michael@0: // If you need to iterate or search, consider using GetItemAt and doing your own filtering michael@0: nsMenuObjectX* nsMenuX::GetVisibleItemAt(uint32_t aPos) michael@0: { michael@0: michael@0: uint32_t count = mMenuObjectsArray.Length(); michael@0: if (aPos >= mVisibleItemsCount || aPos >= count) michael@0: return NULL; michael@0: michael@0: // If there are no invisible items, can provide direct access michael@0: if (mVisibleItemsCount == count) michael@0: return mMenuObjectsArray[aPos]; michael@0: michael@0: // Otherwise, traverse the array until we find the the item we're looking for. michael@0: nsMenuObjectX* item; michael@0: uint32_t visibleNodeIndex = 0; michael@0: for (uint32_t i = 0; i < count; i++) { michael@0: item = mMenuObjectsArray[i]; michael@0: if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(item->Content())) { michael@0: if (aPos == visibleNodeIndex) { michael@0: // we found the visible node we're looking for, return it michael@0: return item; michael@0: } michael@0: visibleNodeIndex++; michael@0: } michael@0: } michael@0: michael@0: return NULL; michael@0: } michael@0: michael@0: nsresult nsMenuX::RemoveAll() michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; michael@0: michael@0: if (mNativeMenu) { michael@0: // clear command id's michael@0: int itemCount = [mNativeMenu numberOfItems]; michael@0: for (int i = 0; i < itemCount; i++) michael@0: mMenuGroupOwner->UnregisterCommand((uint32_t)[[mNativeMenu itemAtIndex:i] tag]); michael@0: // get rid of Cocoa menu items michael@0: for (int i = [mNativeMenu numberOfItems] - 1; i >= 0; i--) michael@0: [mNativeMenu removeItemAtIndex:i]; michael@0: } michael@0: michael@0: mMenuObjectsArray.Clear(); michael@0: mVisibleItemsCount = 0; michael@0: michael@0: return NS_OK; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; michael@0: } michael@0: michael@0: nsEventStatus nsMenuX::MenuOpened() michael@0: { michael@0: // Open the node. michael@0: mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::open, NS_LITERAL_STRING("true"), true); michael@0: michael@0: // Fire a handler. If we're told to stop, don't build the menu at all michael@0: bool keepProcessing = OnOpen(); michael@0: michael@0: if (!mNeedsRebuild || !keepProcessing) michael@0: return nsEventStatus_eConsumeNoDefault; michael@0: michael@0: if (!mConstructed || mNeedsRebuild) { michael@0: if (mNeedsRebuild) michael@0: RemoveAll(); michael@0: michael@0: MenuConstruct(); michael@0: mConstructed = true; michael@0: } michael@0: michael@0: nsEventStatus status = nsEventStatus_eIgnore; michael@0: WidgetMouseEvent event(true, NS_XUL_POPUP_SHOWN, nullptr, michael@0: WidgetMouseEvent::eReal); michael@0: michael@0: nsCOMPtr popupContent; michael@0: GetMenuPopupContent(getter_AddRefs(popupContent)); michael@0: nsIContent* dispatchTo = popupContent ? popupContent : mContent; michael@0: dispatchTo->DispatchDOMEvent(&event, nullptr, nullptr, &status); michael@0: michael@0: return nsEventStatus_eConsumeNoDefault; michael@0: } michael@0: michael@0: void nsMenuX::MenuClosed() michael@0: { michael@0: if (mConstructed) { michael@0: // Don't close if a handler tells us to stop. michael@0: if (!OnClose()) michael@0: return; michael@0: michael@0: if (mNeedsRebuild) michael@0: mConstructed = false; michael@0: michael@0: mContent->UnsetAttr(kNameSpaceID_None, nsGkAtoms::open, true); michael@0: michael@0: nsEventStatus status = nsEventStatus_eIgnore; michael@0: WidgetMouseEvent event(true, NS_XUL_POPUP_HIDDEN, nullptr, michael@0: WidgetMouseEvent::eReal); michael@0: michael@0: nsCOMPtr popupContent; michael@0: GetMenuPopupContent(getter_AddRefs(popupContent)); michael@0: nsIContent* dispatchTo = popupContent ? popupContent : mContent; michael@0: dispatchTo->DispatchDOMEvent(&event, nullptr, nullptr, &status); michael@0: michael@0: mDestroyHandlerCalled = true; michael@0: mConstructed = false; michael@0: } michael@0: } michael@0: michael@0: void nsMenuX::MenuConstruct() michael@0: { michael@0: mConstructed = false; michael@0: gConstructingMenu = true; michael@0: michael@0: // reset destroy handler flag so that we'll know to fire it next time this menu goes away. michael@0: mDestroyHandlerCalled = false; michael@0: michael@0: //printf("nsMenuX::MenuConstruct called for %s = %d \n", NS_LossyConvertUTF16toASCII(mLabel).get(), mNativeMenu); michael@0: michael@0: // Retrieve our menupopup. michael@0: nsCOMPtr menuPopup; michael@0: GetMenuPopupContent(getter_AddRefs(menuPopup)); michael@0: if (!menuPopup) { michael@0: gConstructingMenu = false; michael@0: return; michael@0: } michael@0: michael@0: // bug 365405: Manually wrap the menupopup node to make sure it's bounded michael@0: if (!mXBLAttached) { michael@0: nsresult rv; michael@0: nsCOMPtr xpconnect = michael@0: do_GetService(nsIXPConnect::GetCID(), &rv); michael@0: if (NS_SUCCEEDED(rv)) { michael@0: nsIDocument* ownerDoc = menuPopup->OwnerDoc(); michael@0: nsCOMPtr sgo; michael@0: if (ownerDoc && (sgo = do_QueryInterface(ownerDoc->GetWindow()))) { michael@0: nsCOMPtr scriptContext = sgo->GetContext(); michael@0: JSObject* global = sgo->GetGlobalJSObject(); michael@0: if (scriptContext && global) { michael@0: AutoPushJSContext cx(scriptContext->GetNativeContext()); michael@0: if (cx) { michael@0: nsCOMPtr wrapper; michael@0: xpconnect->WrapNative(cx, global, michael@0: menuPopup, NS_GET_IID(nsISupports), michael@0: getter_AddRefs(wrapper)); michael@0: mXBLAttached = true; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: // Iterate over the kids michael@0: uint32_t count = menuPopup->GetChildCount(); michael@0: for (uint32_t i = 0; i < count; i++) { michael@0: nsIContent *child = menuPopup->GetChildAt(i); michael@0: if (child) { michael@0: // depending on the type, create a menu item, separator, or submenu michael@0: nsIAtom *tag = child->Tag(); michael@0: if (tag == nsGkAtoms::menuitem || tag == nsGkAtoms::menuseparator) michael@0: LoadMenuItem(child); michael@0: else if (tag == nsGkAtoms::menu) michael@0: LoadSubMenu(child); michael@0: } michael@0: } // for each menu item michael@0: michael@0: gConstructingMenu = false; michael@0: mNeedsRebuild = false; michael@0: // printf("Done building, mMenuObjectsArray.Count() = %d \n", mMenuObjectsArray.Count()); michael@0: } michael@0: michael@0: void nsMenuX::SetRebuild(bool aNeedsRebuild) michael@0: { michael@0: if (!gConstructingMenu) michael@0: mNeedsRebuild = aNeedsRebuild; michael@0: } michael@0: michael@0: nsresult nsMenuX::SetEnabled(bool aIsEnabled) michael@0: { michael@0: if (aIsEnabled != mIsEnabled) { michael@0: // we always want to rebuild when this changes michael@0: mIsEnabled = aIsEnabled; michael@0: [mNativeMenuItem setEnabled:(BOOL)mIsEnabled]; michael@0: } michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult nsMenuX::GetEnabled(bool* aIsEnabled) michael@0: { michael@0: NS_ENSURE_ARG_POINTER(aIsEnabled); michael@0: *aIsEnabled = mIsEnabled; michael@0: return NS_OK; michael@0: } michael@0: michael@0: GeckoNSMenu* nsMenuX::CreateMenuWithGeckoString(nsString& menuTitle) michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; michael@0: michael@0: NSString* title = [NSString stringWithCharacters:(UniChar*)menuTitle.get() length:menuTitle.Length()]; michael@0: GeckoNSMenu* myMenu = [[GeckoNSMenu alloc] initWithTitle:title]; michael@0: [myMenu setDelegate:mMenuDelegate]; michael@0: michael@0: // We don't want this menu to auto-enable menu items because then Cocoa michael@0: // overrides our decisions and things get incorrectly enabled/disabled. michael@0: [myMenu setAutoenablesItems:NO]; michael@0: michael@0: // we used to install Carbon event handlers here, but since NSMenu* doesn't michael@0: // create its underlying MenuRef until just before display, we delay until michael@0: // that happens. Now we install the event handlers when Cocoa notifies michael@0: // us that a menu is about to display - see the Cocoa MenuDelegate class. michael@0: michael@0: return myMenu; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK_NIL; michael@0: } michael@0: michael@0: void nsMenuX::LoadMenuItem(nsIContent* inMenuItemContent) michael@0: { michael@0: if (!inMenuItemContent) michael@0: return; michael@0: michael@0: nsAutoString menuitemName; michael@0: inMenuItemContent->GetAttr(kNameSpaceID_None, nsGkAtoms::label, menuitemName); michael@0: michael@0: // printf("menuitem %s \n", NS_LossyConvertUTF16toASCII(menuitemName).get()); michael@0: michael@0: EMenuItemType itemType = eRegularMenuItemType; michael@0: if (inMenuItemContent->Tag() == nsGkAtoms::menuseparator) { michael@0: itemType = eSeparatorMenuItemType; michael@0: } michael@0: else { michael@0: static nsIContent::AttrValuesArray strings[] = michael@0: {&nsGkAtoms::checkbox, &nsGkAtoms::radio, nullptr}; michael@0: switch (inMenuItemContent->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::type, michael@0: strings, eCaseMatters)) { michael@0: case 0: itemType = eCheckboxMenuItemType; break; michael@0: case 1: itemType = eRadioMenuItemType; break; michael@0: } michael@0: } michael@0: michael@0: // Create the item. michael@0: nsMenuItemX* menuItem = new nsMenuItemX(); michael@0: if (!menuItem) michael@0: return; michael@0: michael@0: nsresult rv = menuItem->Create(this, menuitemName, itemType, mMenuGroupOwner, inMenuItemContent); michael@0: if (NS_FAILED(rv)) { michael@0: delete menuItem; michael@0: return; michael@0: } michael@0: michael@0: AddMenuItem(menuItem); michael@0: michael@0: // This needs to happen after the nsIMenuItem object is inserted into michael@0: // our item array in AddMenuItem() michael@0: menuItem->SetupIcon(); michael@0: } michael@0: michael@0: void nsMenuX::LoadSubMenu(nsIContent* inMenuContent) michael@0: { michael@0: nsAutoPtr menu(new nsMenuX()); michael@0: if (!menu) michael@0: return; michael@0: michael@0: nsresult rv = menu->Create(this, mMenuGroupOwner, inMenuContent); michael@0: if (NS_FAILED(rv)) michael@0: return; michael@0: michael@0: AddMenu(menu); michael@0: michael@0: // This needs to happen after the nsIMenu object is inserted into michael@0: // our item array in AddMenu() michael@0: menu->SetupIcon(); michael@0: michael@0: menu.forget(); michael@0: } michael@0: michael@0: // This menu is about to open. Returns TRUE if we should keep processing the event, michael@0: // FALSE if the handler wants to stop the opening of the menu. michael@0: bool nsMenuX::OnOpen() michael@0: { michael@0: nsEventStatus status = nsEventStatus_eIgnore; michael@0: WidgetMouseEvent event(true, NS_XUL_POPUP_SHOWING, nullptr, michael@0: WidgetMouseEvent::eReal); michael@0: michael@0: nsCOMPtr popupContent; michael@0: GetMenuPopupContent(getter_AddRefs(popupContent)); michael@0: michael@0: nsresult rv = NS_OK; michael@0: nsIContent* dispatchTo = popupContent ? popupContent : mContent; michael@0: rv = dispatchTo->DispatchDOMEvent(&event, nullptr, nullptr, &status); michael@0: if (NS_FAILED(rv) || status == nsEventStatus_eConsumeNoDefault) michael@0: return false; michael@0: michael@0: // If the open is going to succeed we need to walk our menu items, checking to michael@0: // see if any of them have a command attribute. If so, several attributes michael@0: // must potentially be updated. michael@0: michael@0: // Get new popup content first since it might have changed as a result of the michael@0: // NS_XUL_POPUP_SHOWING event above. michael@0: GetMenuPopupContent(getter_AddRefs(popupContent)); michael@0: if (!popupContent) michael@0: return true; michael@0: michael@0: nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); michael@0: if (pm) { michael@0: pm->UpdateMenuItems(popupContent); michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: // Returns TRUE if we should keep processing the event, FALSE if the handler michael@0: // wants to stop the closing of the menu. michael@0: bool nsMenuX::OnClose() michael@0: { michael@0: if (mDestroyHandlerCalled) michael@0: return true; michael@0: michael@0: nsEventStatus status = nsEventStatus_eIgnore; michael@0: WidgetMouseEvent event(true, NS_XUL_POPUP_HIDING, nullptr, michael@0: WidgetMouseEvent::eReal); michael@0: michael@0: nsCOMPtr popupContent; michael@0: GetMenuPopupContent(getter_AddRefs(popupContent)); michael@0: michael@0: nsresult rv = NS_OK; michael@0: nsIContent* dispatchTo = popupContent ? popupContent : mContent; michael@0: rv = dispatchTo->DispatchDOMEvent(&event, nullptr, nullptr, &status); michael@0: michael@0: mDestroyHandlerCalled = true; michael@0: michael@0: if (NS_FAILED(rv) || status == nsEventStatus_eConsumeNoDefault) michael@0: return false; michael@0: michael@0: return true; michael@0: } michael@0: michael@0: // Find the |menupopup| child in the |popup| representing this menu. It should be one michael@0: // of a very few children so we won't be iterating over a bazillion menu items to find michael@0: // it (so the strcmp won't kill us). michael@0: void nsMenuX::GetMenuPopupContent(nsIContent** aResult) michael@0: { michael@0: if (!aResult) michael@0: return; michael@0: *aResult = nullptr; michael@0: michael@0: // Check to see if we are a "menupopup" node (if we are a native menu). michael@0: { michael@0: int32_t dummy; michael@0: nsCOMPtr tag = mContent->OwnerDoc()->BindingManager()->ResolveTag(mContent, &dummy); michael@0: if (tag == nsGkAtoms::menupopup) { michael@0: *aResult = mContent; michael@0: NS_ADDREF(*aResult); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: // Otherwise check our child nodes. michael@0: michael@0: uint32_t count = mContent->GetChildCount(); michael@0: michael@0: for (uint32_t i = 0; i < count; i++) { michael@0: int32_t dummy; michael@0: nsIContent *child = mContent->GetChildAt(i); michael@0: nsCOMPtr tag = child->OwnerDoc()->BindingManager()->ResolveTag(child, &dummy); michael@0: if (tag == nsGkAtoms::menupopup) { michael@0: *aResult = child; michael@0: NS_ADDREF(*aResult); michael@0: return; michael@0: } michael@0: } michael@0: } michael@0: michael@0: NSMenuItem* nsMenuX::NativeMenuItem() michael@0: { michael@0: return mNativeMenuItem; michael@0: } michael@0: michael@0: bool nsMenuX::IsXULHelpMenu(nsIContent* aMenuContent) michael@0: { michael@0: bool retval = false; michael@0: if (aMenuContent) { michael@0: nsAutoString id; michael@0: aMenuContent->GetAttr(kNameSpaceID_None, nsGkAtoms::id, id); michael@0: if (id.Equals(NS_LITERAL_STRING("helpMenu"))) michael@0: retval = true; michael@0: } michael@0: return retval; michael@0: } michael@0: michael@0: // michael@0: // nsChangeObserver michael@0: // michael@0: michael@0: void nsMenuX::ObserveAttributeChanged(nsIDocument *aDocument, nsIContent *aContent, michael@0: nsIAtom *aAttribute) michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK; michael@0: michael@0: // ignore the |open| attribute, which is by far the most common michael@0: if (gConstructingMenu || (aAttribute == nsGkAtoms::open)) michael@0: return; michael@0: michael@0: nsMenuObjectTypeX parentType = mParent->MenuObjectType(); michael@0: michael@0: if (aAttribute == nsGkAtoms::disabled) { michael@0: SetEnabled(!mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, michael@0: nsGkAtoms::_true, eCaseMatters)); michael@0: } michael@0: else if (aAttribute == nsGkAtoms::label) { michael@0: mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::label, mLabel); michael@0: michael@0: // invalidate my parent. If we're a submenu parent, we have to rebuild michael@0: // the parent menu in order for the changes to be picked up. If we're michael@0: // a regular menu, just change the title and redraw the menubar. michael@0: if (parentType == eMenuBarObjectType) { michael@0: // reuse the existing menu, to avoid rebuilding the root menu bar. michael@0: NS_ASSERTION(mNativeMenu, "nsMenuX::AttributeChanged: invalid menu handle."); michael@0: NSString *newCocoaLabelString = nsMenuUtilsX::GetTruncatedCocoaLabel(mLabel); michael@0: [mNativeMenu setTitle:newCocoaLabelString]; michael@0: } michael@0: else if (parentType == eSubmenuObjectType) { michael@0: static_cast(mParent)->SetRebuild(true); michael@0: } michael@0: else if (parentType == eStandaloneNativeMenuObjectType) { michael@0: static_cast(mParent)->GetMenuXObject()->SetRebuild(true); michael@0: } michael@0: } michael@0: else if (aAttribute == nsGkAtoms::hidden || aAttribute == nsGkAtoms::collapsed) { michael@0: SetRebuild(true); michael@0: michael@0: bool contentIsHiddenOrCollapsed = nsMenuUtilsX::NodeIsHiddenOrCollapsed(mContent); michael@0: michael@0: // don't do anything if the state is correct already michael@0: if (contentIsHiddenOrCollapsed != mVisible) michael@0: return; michael@0: michael@0: if (contentIsHiddenOrCollapsed) { michael@0: if (parentType == eMenuBarObjectType || michael@0: parentType == eSubmenuObjectType || michael@0: parentType == eStandaloneNativeMenuObjectType) { michael@0: NSMenu* parentMenu = (NSMenu*)mParent->NativeData(); michael@0: // An exception will get thrown if we try to remove an item that isn't michael@0: // in the menu. michael@0: if ([parentMenu indexOfItem:mNativeMenuItem] != -1) michael@0: [parentMenu removeItem:mNativeMenuItem]; michael@0: mVisible = false; michael@0: } michael@0: } michael@0: else { michael@0: if (parentType == eMenuBarObjectType || michael@0: parentType == eSubmenuObjectType || michael@0: parentType == eStandaloneNativeMenuObjectType) { michael@0: int insertionIndex = nsMenuUtilsX::CalculateNativeInsertionPoint(mParent, this); michael@0: if (parentType == eMenuBarObjectType) { michael@0: // Before inserting we need to figure out if we should take the native michael@0: // application menu into account. michael@0: nsMenuBarX* mb = static_cast(mParent); michael@0: if (mb->MenuContainsAppMenu()) michael@0: insertionIndex++; michael@0: } michael@0: NSMenu* parentMenu = (NSMenu*)mParent->NativeData(); michael@0: [parentMenu insertItem:mNativeMenuItem atIndex:insertionIndex]; michael@0: [mNativeMenuItem setSubmenu:mNativeMenu]; michael@0: mVisible = true; michael@0: } michael@0: } michael@0: } michael@0: else if (aAttribute == nsGkAtoms::image) { michael@0: SetupIcon(); michael@0: } michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK; michael@0: } michael@0: michael@0: void nsMenuX::ObserveContentRemoved(nsIDocument *aDocument, nsIContent *aChild, michael@0: int32_t aIndexInContainer) michael@0: { michael@0: if (gConstructingMenu) michael@0: return; michael@0: michael@0: SetRebuild(true); michael@0: mMenuGroupOwner->UnregisterForContentChanges(aChild); michael@0: } michael@0: michael@0: void nsMenuX::ObserveContentInserted(nsIDocument *aDocument, nsIContent* aContainer, michael@0: nsIContent *aChild) michael@0: { michael@0: if (gConstructingMenu) michael@0: return; michael@0: michael@0: SetRebuild(true); michael@0: } michael@0: michael@0: nsresult nsMenuX::SetupIcon() michael@0: { michael@0: // In addition to out-of-memory, menus that are children of the menu bar michael@0: // will not have mIcon set. michael@0: if (!mIcon) michael@0: return NS_ERROR_OUT_OF_MEMORY; michael@0: michael@0: return mIcon->SetupIcon(); michael@0: } michael@0: michael@0: // michael@0: // MenuDelegate Objective-C class, used to set up Carbon events michael@0: // michael@0: michael@0: @implementation MenuDelegate michael@0: michael@0: - (id)initWithGeckoMenu:(nsMenuX*)geckoMenu michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; michael@0: michael@0: if ((self = [super init])) { michael@0: NS_ASSERTION(geckoMenu, "Cannot initialize native menu delegate with NULL gecko menu! Will crash!"); michael@0: mGeckoMenu = geckoMenu; michael@0: } michael@0: return self; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK_NIL; michael@0: } michael@0: michael@0: - (void)menu:(NSMenu *)menu willHighlightItem:(NSMenuItem *)item michael@0: { michael@0: if (!menu || !item || !mGeckoMenu) michael@0: return; michael@0: michael@0: nsMenuObjectX* target = mGeckoMenu->GetVisibleItemAt((uint32_t)[menu indexOfItem:item]); michael@0: if (target && (target->MenuObjectType() == eMenuItemObjectType)) { michael@0: nsMenuItemX* targetMenuItem = static_cast(target); michael@0: bool handlerCalledPreventDefault; // but we don't actually care michael@0: targetMenuItem->DispatchDOMEvent(NS_LITERAL_STRING("DOMMenuItemActive"), &handlerCalledPreventDefault); michael@0: } michael@0: } michael@0: michael@0: - (void)menuWillOpen:(NSMenu *)menu michael@0: { michael@0: if (!mGeckoMenu) michael@0: return; michael@0: michael@0: // Don't do anything while the OS is (re)indexing our menus (on Leopard and michael@0: // higher). This stops the Help menu from being able to search in our michael@0: // menus, but it also resolves many other problems. michael@0: if (nsMenuX::sIndexingMenuLevel > 0) michael@0: return; michael@0: michael@0: nsIRollupListener* rollupListener = nsBaseWidget::GetActiveRollupListener(); michael@0: if (rollupListener) { michael@0: nsCOMPtr rollupWidget = rollupListener->GetRollupWidget(); michael@0: if (rollupWidget) { michael@0: rollupListener->Rollup(0, nullptr, nullptr); michael@0: [menu cancelTracking]; michael@0: return; michael@0: } michael@0: } michael@0: mGeckoMenu->MenuOpened(); michael@0: } michael@0: michael@0: - (void)menuDidClose:(NSMenu *)menu michael@0: { michael@0: if (!mGeckoMenu) michael@0: return; michael@0: michael@0: // Don't do anything while the OS is (re)indexing our menus (on Leopard and michael@0: // higher). This stops the Help menu from being able to search in our michael@0: // menus, but it also resolves many other problems. michael@0: if (nsMenuX::sIndexingMenuLevel > 0) michael@0: return; michael@0: michael@0: mGeckoMenu->MenuClosed(); michael@0: } michael@0: michael@0: @end michael@0: michael@0: // OS X Leopard (at least as of 10.5.2) has an obscure bug triggered by some michael@0: // behavior that's present in Mozilla.org browsers but not (as best I can michael@0: // tell) in Apple products like Safari. (It's not yet clear exactly what this michael@0: // behavior is.) michael@0: // michael@0: // The bug is that sometimes you crash on quit in nsMenuX::RemoveAll(), on a michael@0: // call to [NSMenu removeItemAtIndex:]. The crash is caused by trying to michael@0: // access a deleted NSMenuItem object (sometimes (perhaps always?) by trying michael@0: // to send it a _setChangedFlags: message). Though this object was deleted michael@0: // some time ago, it remains registered as a potential target for a particular michael@0: // key equivalent. So when [NSMenu removeItemAtIndex:] removes the current michael@0: // target for that same key equivalent, the OS tries to "activate" the michael@0: // previous target. michael@0: // michael@0: // The underlying reason appears to be that NSMenu's _addItem:toTable: and michael@0: // _removeItem:fromTable: methods (which are used to keep a hashtable of michael@0: // registered key equivalents) don't properly "retain" and "release" michael@0: // NSMenuItem objects as they are added to and removed from the hashtable. michael@0: // michael@0: // Our (hackish) workaround is to shadow the OS's hashtable with another michael@0: // hastable of our own (gShadowKeyEquivDB), and use it to "retain" and michael@0: // "release" NSMenuItem objects as needed. This resolves bmo bugs 422287 and michael@0: // 423669. When (if) Apple fixes this bug, we can remove this workaround. michael@0: michael@0: static NSMutableDictionary *gShadowKeyEquivDB = nil; michael@0: michael@0: // Class for values in gShadowKeyEquivDB. michael@0: michael@0: @interface KeyEquivDBItem : NSObject michael@0: { michael@0: NSMenuItem *mItem; michael@0: NSMutableSet *mTables; michael@0: } michael@0: michael@0: - (id)initWithItem:(NSMenuItem *)aItem table:(NSMapTable *)aTable; michael@0: - (BOOL)hasTable:(NSMapTable *)aTable; michael@0: - (int)addTable:(NSMapTable *)aTable; michael@0: - (int)removeTable:(NSMapTable *)aTable; michael@0: michael@0: @end michael@0: michael@0: @implementation KeyEquivDBItem michael@0: michael@0: - (id)initWithItem:(NSMenuItem *)aItem table:(NSMapTable *)aTable michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; michael@0: michael@0: if (!gShadowKeyEquivDB) michael@0: gShadowKeyEquivDB = [[NSMutableDictionary alloc] init]; michael@0: self = [super init]; michael@0: if (aItem && aTable) { michael@0: mTables = [[NSMutableSet alloc] init]; michael@0: mItem = [aItem retain]; michael@0: [mTables addObject:[NSValue valueWithPointer:aTable]]; michael@0: } else { michael@0: mTables = nil; michael@0: mItem = nil; michael@0: } michael@0: return self; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK_NIL; michael@0: } michael@0: michael@0: - (void)dealloc michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK; michael@0: michael@0: if (mTables) michael@0: [mTables release]; michael@0: if (mItem) michael@0: [mItem release]; michael@0: [super dealloc]; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK; michael@0: } michael@0: michael@0: - (BOOL)hasTable:(NSMapTable *)aTable michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; michael@0: michael@0: return [mTables member:[NSValue valueWithPointer:aTable]] ? YES : NO; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NO); michael@0: } michael@0: michael@0: // Does nothing if aTable (its index value) is already present in mTables. michael@0: - (int)addTable:(NSMapTable *)aTable michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; michael@0: michael@0: if (aTable) michael@0: [mTables addObject:[NSValue valueWithPointer:aTable]]; michael@0: return [mTables count]; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(0); michael@0: } michael@0: michael@0: - (int)removeTable:(NSMapTable *)aTable michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; michael@0: michael@0: if (aTable) { michael@0: NSValue *objectToRemove = michael@0: [mTables member:[NSValue valueWithPointer:aTable]]; michael@0: if (objectToRemove) michael@0: [mTables removeObject:objectToRemove]; michael@0: } michael@0: return [mTables count]; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(0); michael@0: } michael@0: michael@0: @end michael@0: michael@0: @interface NSMenu (MethodSwizzling) michael@0: + (void)nsMenuX_NSMenu_addItem:(NSMenuItem *)aItem toTable:(NSMapTable *)aTable; michael@0: + (void)nsMenuX_NSMenu_removeItem:(NSMenuItem *)aItem fromTable:(NSMapTable *)aTable; michael@0: @end michael@0: michael@0: @implementation NSMenu (MethodSwizzling) michael@0: michael@0: + (void)nsMenuX_NSMenu_addItem:(NSMenuItem *)aItem toTable:(NSMapTable *)aTable michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK; michael@0: michael@0: if (aItem && aTable) { michael@0: NSValue *key = [NSValue valueWithPointer:aItem]; michael@0: KeyEquivDBItem *shadowItem = [gShadowKeyEquivDB objectForKey:key]; michael@0: if (shadowItem) { michael@0: [shadowItem addTable:aTable]; michael@0: } else { michael@0: shadowItem = [[KeyEquivDBItem alloc] initWithItem:aItem table:aTable]; michael@0: [gShadowKeyEquivDB setObject:shadowItem forKey:key]; michael@0: // Release after [NSMutableDictionary setObject:forKey:] retains it (so michael@0: // that it will get dealloced when removeObjectForKey: is called). michael@0: [shadowItem release]; michael@0: } michael@0: } michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK; michael@0: michael@0: [self nsMenuX_NSMenu_addItem:aItem toTable:aTable]; michael@0: } michael@0: michael@0: + (void)nsMenuX_NSMenu_removeItem:(NSMenuItem *)aItem fromTable:(NSMapTable *)aTable michael@0: { michael@0: [self nsMenuX_NSMenu_removeItem:aItem fromTable:aTable]; michael@0: michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK; michael@0: michael@0: if (aItem && aTable) { michael@0: NSValue *key = [NSValue valueWithPointer:aItem]; michael@0: KeyEquivDBItem *shadowItem = [gShadowKeyEquivDB objectForKey:key]; michael@0: if (shadowItem && [shadowItem hasTable:aTable]) { michael@0: if (![shadowItem removeTable:aTable]) michael@0: [gShadowKeyEquivDB removeObjectForKey:key]; michael@0: } michael@0: } michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK; michael@0: } michael@0: michael@0: @end michael@0: michael@0: // This class is needed to keep track of when the OS is (re)indexing all of michael@0: // our menus. This appears to only happen on Leopard and higher, and can michael@0: // be triggered by opening the Help menu. Some operations are unsafe while michael@0: // this is happening -- notably the calls to [[NSImage alloc] michael@0: // initWithSize:imageRect.size] and [newImage lockFocus] in nsMenuItemIconX:: michael@0: // OnStopFrame(). But we don't yet have a complete list, and Apple doesn't michael@0: // yet have any documentation on this subject. (Apple also doesn't yet have michael@0: // any documented way to find the information we seek here.) The "original" michael@0: // of this class (the one whose indexMenuBarDynamically method we hook) is michael@0: // defined in the Shortcut framework in /System/Library/PrivateFrameworks. michael@0: @interface NSObject (SCTGRLIndexMethodSwizzling) michael@0: - (void)nsMenuX_SCTGRLIndex_indexMenuBarDynamically; michael@0: @end michael@0: michael@0: @implementation NSObject (SCTGRLIndexMethodSwizzling) michael@0: michael@0: - (void)nsMenuX_SCTGRLIndex_indexMenuBarDynamically michael@0: { michael@0: // This method appears to be called (once) whenever the OS (re)indexes our michael@0: // menus. sIndexingMenuLevel is a int32_t just in case it might be michael@0: // reentered. As it's running, it spawns calls to two undocumented michael@0: // HIToolbox methods (_SimulateMenuOpening() and _SimulateMenuClosed()), michael@0: // which "simulate" the opening and closing of our menus without actually michael@0: // displaying them. michael@0: ++nsMenuX::sIndexingMenuLevel; michael@0: [self nsMenuX_SCTGRLIndex_indexMenuBarDynamically]; michael@0: --nsMenuX::sIndexingMenuLevel; michael@0: } michael@0: michael@0: @end