From cb75d58ea57cb3316ad8d1a3aa55a17c1e78efc4 Mon Sep 17 00:00:00 2001 From: Josh Aas Date: Mon, 21 Jul 2008 00:33:38 -0400 Subject: [PATCH] improve support for DOM manipulations in native menu system, add tests. b=442972 r=kreeger sr=roc --- dom/public/idl/base/nsIDOMWindowUtils.idl | 10 ++ dom/src/base/nsDOMWindowUtils.cpp | 18 +++ widget/public/nsIWidget.h | 8 + widget/src/cocoa/nsChildView.h | 3 +- widget/src/cocoa/nsChildView.mm | 34 +++- widget/src/cocoa/nsMenuBarX.h | 13 +- widget/src/cocoa/nsMenuBarX.mm | 166 ++++++++++++------- widget/src/cocoa/nsMenuX.h | 9 +- widget/src/cocoa/nsMenuX.mm | 56 +++---- widget/src/xpwidgets/nsBaseWidget.h | 1 + widget/tests/native_menus_window.xul | 184 ++++++++++++++++++++-- 11 files changed, 385 insertions(+), 117 deletions(-) diff --git a/dom/public/idl/base/nsIDOMWindowUtils.idl b/dom/public/idl/base/nsIDOMWindowUtils.idl index b457a8f10ed..750014e9a0c 100644 --- a/dom/public/idl/base/nsIDOMWindowUtils.idl +++ b/dom/public/idl/base/nsIDOMWindowUtils.idl @@ -169,6 +169,16 @@ interface nsIDOMWindowUtils : nsISupports { */ void activateNativeMenuItemAt(in AString indexString); + /** + * See nsIWidget::ForceNativeMenuReload + * + * This is used for native menu system testing. Calling this forces a full + * reload of the menu system, reloading all native menus and their items. + * This is important for testing because changes to the DOM can affect the + * native menu system lazily. + */ + void forceNativeMenuReload(); + /** * Focus the element aElement. The element should be in the same document * that the window is displaying. Pass null to blur the element, if any, diff --git a/dom/src/base/nsDOMWindowUtils.cpp b/dom/src/base/nsDOMWindowUtils.cpp index f73b1c047cf..c1281cf342a 100644 --- a/dom/src/base/nsDOMWindowUtils.cpp +++ b/dom/src/base/nsDOMWindowUtils.cpp @@ -324,6 +324,24 @@ nsDOMWindowUtils::ActivateNativeMenuItemAt(const nsAString& indexString) return widget->ActivateNativeMenuItemAt(indexString); } + +NS_IMETHODIMP +nsDOMWindowUtils::ForceNativeMenuReload() +{ + PRBool hasCap = PR_FALSE; + if (NS_FAILED(nsContentUtils::GetSecurityManager()->IsCapabilityEnabled("UniversalXPConnect", &hasCap)) + || !hasCap) + return NS_ERROR_DOM_SECURITY_ERR; + + // get the widget to send the event to + nsCOMPtr widget = GetWidget(); + if (!widget) + return NS_ERROR_FAILURE; + + return widget->ForceNativeMenuReload(); +} + + nsIWidget* nsDOMWindowUtils::GetWidget() { diff --git a/widget/public/nsIWidget.h b/widget/public/nsIWidget.h index e72f474cd94..729b7e9a63e 100644 --- a/widget/public/nsIWidget.h +++ b/widget/public/nsIWidget.h @@ -1196,6 +1196,14 @@ class nsIWidget : public nsISupports { */ NS_IMETHOD GetToggledKeyState(PRUint32 aKeyCode, PRBool* aLEDState) = 0; + /** + * This is used for native menu system testing. Calling this forces a full + * reload of the menu system, reloading all native menus and their items. + * This is important for testing because changes to the DOM can affect the + * native menu system lazily. + */ + virtual nsresult ForceNativeMenuReload() = 0; + protected: // keep the list of children. We also keep track of our siblings. // The ownership model is as follows: parent holds a strong ref to diff --git a/widget/src/cocoa/nsChildView.h b/widget/src/cocoa/nsChildView.h index e9e28f0878d..fb7208118d2 100644 --- a/widget/src/cocoa/nsChildView.h +++ b/widget/src/cocoa/nsChildView.h @@ -331,7 +331,8 @@ public: NS_IMETHOD GetAttention(PRInt32 aCycleCount); - NS_IMETHOD ActivateNativeMenuItemAt(const nsAString& indexString); + NS_IMETHOD ActivateNativeMenuItemAt(const nsAString& indexString); + NS_IMETHOD ForceNativeMenuReload(); NS_IMETHOD ResetInputState(); NS_IMETHOD SetIMEOpenState(PRBool aState); diff --git a/widget/src/cocoa/nsChildView.mm b/widget/src/cocoa/nsChildView.mm index 20e51768004..69757f708f6 100644 --- a/widget/src/cocoa/nsChildView.mm +++ b/widget/src/cocoa/nsChildView.mm @@ -1380,7 +1380,9 @@ nsresult nsChildView::SynthesizeNativeKeyEvent(PRInt32 aNativeKeyboardLayout, // Used for testing native menu system structure and event handling. NS_IMETHODIMP nsChildView::ActivateNativeMenuItemAt(const nsAString& indexString) -{ +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + NSString* title = [NSString stringWithCharacters:indexString.BeginReading() length:indexString.Length()]; NSArray* indexes = [title componentsSeparatedByString:@"|"]; unsigned int indexCount = [indexes count]; @@ -1411,18 +1413,40 @@ NS_IMETHODIMP nsChildView::ActivateNativeMenuItemAt(const nsAString& indexString int itemCount = [currentSubmenu numberOfItems]; int targetIndex = [[indexes objectAtIndex:(indexCount - 1)] intValue]; - if (targetIndex < itemCount) { - // NSLog(@"Performing action for native menu item titled: %@\n", - // [[currentSubmenu itemAtIndex:targetIndex] title]); - [currentSubmenu performActionForItemAtIndex:targetIndex]; + // We can't perform an action on an item with a submenu, that will raise + // an obj-c exception. + if (targetIndex < itemCount && ![[currentSubmenu itemAtIndex:targetIndex] hasSubmenu]) { + // NSLog(@"Performing action for native menu item titled: %@\n", + // [[currentSubmenu itemAtIndex:targetIndex] title]); + [currentSubmenu performActionForItemAtIndex:targetIndex]; } else { return NS_ERROR_FAILURE; } return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; } + +NS_IMETHODIMP nsChildView::ForceNativeMenuReload() +{ + id windowDelegate = [[mView nativeWindow] delegate]; + if (windowDelegate && [windowDelegate isKindOfClass:[WindowDelegate class]]) { + nsCocoaWindow *widget = [(WindowDelegate *)windowDelegate geckoWidget]; + if (widget) { + nsMenuBarX* mb = widget->GetMenuBar(); + if (mb) { + mb->ForceNativeMenuReload(); + } + } + } + + return NS_OK; +} + + #pragma mark - diff --git a/widget/src/cocoa/nsMenuBarX.h b/widget/src/cocoa/nsMenuBarX.h index bc456e169d5..4774cde03e6 100644 --- a/widget/src/cocoa/nsMenuBarX.h +++ b/widget/src/cocoa/nsMenuBarX.h @@ -106,7 +106,7 @@ public: NS_DECL_NSIMUTATIONOBSERVER // nsMenuObjectX - void* NativeData() {return (void*)mRootMenu;} + void* NativeData() {return (void*)mNativeMenu;} nsMenuObjectTypeX MenuObjectType() {return eMenuBarObjectType;} // nsMenuBarX @@ -120,10 +120,13 @@ public: nsMenuX* GetMenuAt(PRUint32 aIndex); nsMenuItemX* GetMenuItemForCommandID(PRUint32 inCommandID); nsresult Paint(); + void ForceNativeMenuReload(); // used for testing protected: - nsresult AddMenu(nsMenuX* aMenu); - void RemoveMenu(PRUint32 aIndex); + void ConstructNativeMenus(); + bool MenuContainsAppMenu(); + nsresult InsertMenuAtIndex(nsMenuX* aMenu, PRUint32 aIndex); + void RemoveMenuAtIndex(PRUint32 aIndex); nsChangeObserver* LookupContentChangeObserver(nsIContent* aContent); void HideItem(nsIDOMDocument* inDoc, const nsAString & inID, nsIContent** outHiddenNode); void AquifyMenuBar(); @@ -132,10 +135,10 @@ protected: nsresult CreateApplicationMenu(nsMenuX* inMenu); nsTArray< nsAutoPtr > mMenuArray; - nsIWidget* mParent; // [weak] + nsIWidget* mParentWindow; // [weak] PRUint32 mCurrentCommandID; // unique command id (per menu-bar) to give to next item that asks nsIDocument* mDocument; // pointer to document - GeckoNSMenu* mRootMenu; // root menu, representing entire menu bar + GeckoNSMenu* mNativeMenu; // root menu, representing entire menu bar nsHashtable mObserverTable; // stores observers for content change notification }; diff --git a/widget/src/cocoa/nsMenuBarX.mm b/widget/src/cocoa/nsMenuBarX.mm index dc1764d3cbc..9f7847cd0ff 100644 --- a/widget/src/cocoa/nsMenuBarX.mm +++ b/widget/src/cocoa/nsMenuBarX.mm @@ -98,13 +98,13 @@ NS_IMETHODIMP nsNativeMenuServiceX::CreateNativeMenuBar(nsIWidget* aParent, nsIC nsMenuBarX::nsMenuBarX() -: mParent(nsnull), +: mParentWindow(nsnull), mCurrentCommandID(eCommand_ID_Last), mDocument(nsnull) { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; - mRootMenu = [[GeckoNSMenu alloc] initWithTitle:@"MainMenuBar"]; + mNativeMenu = [[GeckoNSMenu alloc] initWithTitle:@"MainMenuBar"]; NS_OBJC_END_TRY_ABORT_BLOCK; } @@ -136,7 +136,7 @@ nsMenuBarX::~nsMenuBarX() // before the registration hash table is destroyed. mMenuArray.Clear(); - [mRootMenu release]; + [mNativeMenu release]; NS_OBJC_END_TRY_ABORT_BLOCK; } @@ -147,7 +147,7 @@ nsresult nsMenuBarX::Create(nsIWidget* aParent, nsIContent* aContent) if (!aParent || !aContent) return NS_ERROR_INVALID_ARG; - mParent = aParent; + mParentWindow = aParent; mContent = aContent; AquifyMenuBar(); @@ -158,28 +158,31 @@ nsresult nsMenuBarX::Create(nsIWidget* aParent, nsIContent* aContent) doc->AddMutationObserver(this); mDocument = doc; + ConstructNativeMenus(); + + // Give this to the parent window. The parent takes ownership. + return mParentWindow->SetMenuBar(this); +} + + +void nsMenuBarX::ConstructNativeMenus() +{ PRUint32 count = mContent->GetChildCount(); for (PRUint32 i = 0; i < count; i++) { nsIContent *menuContent = mContent->GetChildAt(i); - if (menuContent) { - if (menuContent->Tag() == nsWidgetAtoms::menu && - menuContent->IsNodeOfType(nsINode::eXUL)) { - nsAutoString menuName; - menuContent->GetAttr(kNameSpaceID_None, nsWidgetAtoms::label, menuName); - nsMenuX* newMenu = new nsMenuX(); - if (newMenu) { - nsresult rv = newMenu->Create(this, menuName, this, menuContent); - if (NS_SUCCEEDED(rv)) - AddMenu(newMenu); - else - delete newMenu; - } + if (menuContent && + menuContent->Tag() == nsWidgetAtoms::menu && + menuContent->IsNodeOfType(nsINode::eXUL)) { + nsMenuX* newMenu = new nsMenuX(); + if (newMenu) { + nsresult rv = newMenu->Create(this, this, menuContent); + if (NS_SUCCEEDED(rv)) + InsertMenuAtIndex(newMenu, GetMenuCount()); + else + delete newMenu; } } - } - - // Give this to the parent window. The parent takes ownership. - return mParent->SetMenuBar(this); + } } @@ -189,7 +192,18 @@ PRUint32 nsMenuBarX::GetMenuCount() } -nsresult nsMenuBarX::AddMenu(nsMenuX* aMenu) +bool nsMenuBarX::MenuContainsAppMenu() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + return ([mNativeMenu numberOfItems] > 0 && + [[mNativeMenu itemAtIndex:0] submenu] == sApplicationMenu); + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(false); +} + + +nsresult nsMenuBarX::InsertMenuAtIndex(nsMenuX* aMenu, PRUint32 aIndex) { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; @@ -205,16 +219,19 @@ nsresult nsMenuBarX::AddMenu(nsMenuX* aMenu) [[mainMenu itemAtIndex:0] setSubmenu:sApplicationMenu]; } - // keep track of all added menus - mMenuArray.AppendElement(aMenu); // owner + // add menu to array that owns our menus + mMenuArray.InsertElementAt(aIndex, aMenu); - NSMenu* nativeMenu = (NSMenu*)aMenu->NativeData(); + // hook up submenus nsIContent* menuContent = aMenu->Content(); if (menuContent->GetChildCount() > 0 && !nsMenuUtilsX::NodeIsHiddenOrCollapsed(menuContent)) { - NSMenuItem* newMenuItem = [[[NSMenuItem alloc] initWithTitle:[nativeMenu title] action:NULL keyEquivalent:@""] autorelease]; - [mRootMenu addItem:newMenuItem]; - [newMenuItem setSubmenu:nativeMenu]; + NSMenuItem* newMenuItem = aMenu->NativeMenuItem(); + // If the application menu is in our menu bar we need to insert past it + PRUint32 targetIndex = aIndex; + if (MenuContainsAppMenu()) + targetIndex++; + [mNativeMenu insertItem:newMenuItem atIndex:targetIndex]; } return NS_OK; @@ -223,7 +240,7 @@ nsresult nsMenuBarX::AddMenu(nsMenuX* aMenu) } -void nsMenuBarX::RemoveMenu(PRUint32 aIndex) +void nsMenuBarX::RemoveMenuAtIndex(PRUint32 aIndex) { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; @@ -232,13 +249,32 @@ void nsMenuBarX::RemoveMenu(PRUint32 aIndex) // Our native menu and our internal menu object array might be out of sync. // This happens, for example, when a submenu is hidden. Because of this we // should not assume that a native submenu is hooked up. - [mRootMenu removeItem:(mMenuArray[aIndex]->NativeMenuItem())]; + NSMenuItem* nativeMenuItem = mMenuArray[aIndex]->NativeMenuItem(); + int nativeMenuItemIndex = [mNativeMenu indexOfItem:nativeMenuItem]; + if (nativeMenuItemIndex != -1) + [mNativeMenu removeItemAtIndex:nativeMenuItemIndex]; + mMenuArray.RemoveElementAt(aIndex); NS_OBJC_END_TRY_ABORT_BLOCK; } +// Calling this forces a full reload of the menu system, reloading all native +// menus and their items. +// Without this testing is hard because changes to the DOM affect the native +// menu system lazily. +void nsMenuBarX::ForceNativeMenuReload() +{ + // tear down everything + while (GetMenuCount() > 0) + RemoveMenuAtIndex(0); + + // construct everything + ConstructNativeMenus(); +} + + nsMenuX* nsMenuBarX::GetMenuAt(PRUint32 aIndex) { if (mMenuArray.Length() <= aIndex) { @@ -252,20 +288,22 @@ nsMenuX* nsMenuBarX::GetMenuAt(PRUint32 aIndex) nsresult nsMenuBarX::Paint() { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; - - NSMenu* mainMenu = [NSApp mainMenu]; - NS_ASSERTION([mainMenu numberOfItems] > 0, "Main menu does not have any items, something is terribly wrong!"); - // Swap out first item into incoming menu bar. We have to keep the same menu item for the - // Application menu and its submenu is global so we keep passing it along. - NSMenuItem* firstMenuItem = [[mainMenu itemAtIndex:0] retain]; - [mainMenu removeItemAtIndex:0]; - [mRootMenu insertItem:firstMenuItem atIndex:0]; - [firstMenuItem release]; + NSMenu* outgoingMenu = [NSApp mainMenu]; + NS_ASSERTION([outgoingMenu numberOfItems] > 0, "Main menu does not have any items, something is terribly wrong!"); - // Set menu bar and event target. - [NSApp setMainMenu:mRootMenu]; - nsMenuBarX::sLastGeckoMenuBarPainted = this; + // We have to keep the same menu item for the Application menu so we keep + // passing it along. + if (sLastGeckoMenuBarPainted != this) { + NSMenuItem* appMenuItem = [[outgoingMenu itemAtIndex:0] retain]; + [outgoingMenu removeItemAtIndex:0]; + [mNativeMenu insertItem:appMenuItem atIndex:0]; + [appMenuItem release]; + + // Set menu bar and event target. + [NSApp setMainMenu:mNativeMenu]; + nsMenuBarX::sLastGeckoMenuBarPainted = this; + } gSomeMenuBarPainted = YES; @@ -554,13 +592,15 @@ nsresult nsMenuBarX::CreateApplicationMenu(nsMenuX* inMenu) void nsMenuBarX::SetParent(nsIWidget* aParent) { - mParent = aParent; + mParentWindow = aParent; } + // // nsIMutationObserver // + void nsMenuBarX::CharacterDataWillChange(nsIDocument* aDocument, nsIContent* aContent, CharacterDataChangeInfo* aInfo) @@ -578,24 +618,18 @@ void nsMenuBarX::CharacterDataChanged(nsIDocument* aDocument, void nsMenuBarX::ContentAppended(nsIDocument* aDocument, nsIContent* aContainer, PRInt32 aNewIndexInContainer) { - if (aContainer != mContent) { - nsChangeObserver* obs = LookupContentChangeObserver(aContainer); - if (obs) - obs->ObserveContentInserted(aDocument, aContainer, aNewIndexInContainer); - else { - nsCOMPtr parent = aContainer->GetParent(); - if (parent) { - obs = LookupContentChangeObserver(parent); - if (obs) - obs->ObserveContentInserted(aDocument, aContainer, aNewIndexInContainer); - } - } + PRUint32 childCount = aContainer->GetChildCount(); + while ((PRUint32)aNewIndexInContainer < childCount) { + nsIContent *child = aContainer->GetChildAt(aNewIndexInContainer); + ContentInserted(aDocument, aContainer, child, aNewIndexInContainer); + aNewIndexInContainer++; } } void nsMenuBarX::NodeWillBeDestroyed(const nsINode * aNode) { + // our menu bar node is being destroyed mDocument = nsnull; } @@ -604,7 +638,6 @@ void nsMenuBarX::AttributeChanged(nsIDocument * aDocument, nsIContent * aContent PRInt32 aNameSpaceID, nsIAtom * aAttribute, PRInt32 aModType, PRUint32 aStateMask) { - // lookup and dispatch to registered thang nsChangeObserver* obs = LookupContentChangeObserver(aContent); if (obs) obs->ObserveAttributeChanged(aDocument, aContent, aAttribute); @@ -615,8 +648,7 @@ void nsMenuBarX::ContentRemoved(nsIDocument * aDocument, nsIContent * aContainer nsIContent * aChild, PRInt32 aIndexInContainer) { if (aContainer == mContent) { - UnregisterForContentChanges(aChild); - RemoveMenu(aIndexInContainer); + RemoveMenuAtIndex(aIndexInContainer); } else { nsChangeObserver* obs = LookupContentChangeObserver(aContainer); @@ -624,6 +656,9 @@ void nsMenuBarX::ContentRemoved(nsIDocument * aDocument, nsIContent * aContainer obs->ObserveContentRemoved(aDocument, aChild, aIndexInContainer); } else { + // We do a lookup on the parent container in case things were removed + // under a "menupopup" item. That is basically a wrapper for the contents + // of a "menu" node. nsCOMPtr parent = aContainer->GetParent(); if (parent) { obs = LookupContentChangeObserver(parent); @@ -638,11 +673,24 @@ void nsMenuBarX::ContentRemoved(nsIDocument * aDocument, nsIContent * aContainer void nsMenuBarX::ContentInserted(nsIDocument * aDocument, nsIContent * aContainer, nsIContent * aChild, PRInt32 aIndexInContainer) { - if (aContainer != mContent) { + if (aContainer == mContent) { + nsMenuX* newMenu = new nsMenuX(); + if (newMenu) { + nsresult rv = newMenu->Create(this, this, aChild); + if (NS_SUCCEEDED(rv)) + InsertMenuAtIndex(newMenu, aIndexInContainer); + else + delete newMenu; + } + } + else { nsChangeObserver* obs = LookupContentChangeObserver(aContainer); if (obs) obs->ObserveContentInserted(aDocument, aChild, aIndexInContainer); else { + // We do a lookup on the parent container in case things were removed + // under a "menupopup" item. That is basically a wrapper for the contents + // of a "menu" node. nsCOMPtr parent = aContainer->GetParent(); if (parent) { obs = LookupContentChangeObserver(parent); diff --git a/widget/src/cocoa/nsMenuX.h b/widget/src/cocoa/nsMenuX.h index a73c4b6c842..33440ab687f 100644 --- a/widget/src/cocoa/nsMenuX.h +++ b/widget/src/cocoa/nsMenuX.h @@ -83,12 +83,11 @@ public: NS_DECL_CHANGEOBSERVER // nsMenuObjectX - void* NativeData() {return (void*)mMacMenu;} + void* NativeData() {return (void*)mNativeMenu;} nsMenuObjectTypeX MenuObjectType() {return eSubmenuObjectType;} // nsMenuX - nsresult Create(nsMenuObjectX* aParent, const nsAString &aLabel, - nsMenuBarX* aMenuBar, nsIContent* aNode); + nsresult Create(nsMenuObjectX* aParent, nsMenuBarX* aMenuBar, nsIContent* aNode); PRUint32 GetItemCount(); nsMenuObjectX* GetItemAt(PRUint32 aPos); nsresult GetVisibleItemCount(PRUint32 &aCount); @@ -99,7 +98,7 @@ public: NSMenuItem* NativeMenuItem(); protected: - void MenuConstruct(nsIWidget* aParentWindow, void* aMenuNode); + void MenuConstruct(); nsresult RemoveAll(); nsresult SetEnabled(PRBool aIsEnabled); nsresult GetEnabled(PRBool* aIsEnabled); @@ -122,7 +121,7 @@ protected: nsMenuObjectX* mParent; // [weak] nsMenuBarX* mMenuBar; // [weak] nsRefPtr mIcon; - GeckoNSMenu* mMacMenu; // [strong] + GeckoNSMenu* mNativeMenu; // [strong] MenuDelegate* mMenuDelegate; // [strong] NSMenuItem* mNativeMenuItem; // [strong] PRPackedBool mIsEnabled; diff --git a/widget/src/cocoa/nsMenuX.mm b/widget/src/cocoa/nsMenuX.mm index a3b47fe8ac5..291e8028d1e 100644 --- a/widget/src/cocoa/nsMenuX.mm +++ b/widget/src/cocoa/nsMenuX.mm @@ -84,7 +84,7 @@ PRInt32 nsMenuX::sIndexingMenuLevel = 0; nsMenuX::nsMenuX() : mVisibleItemsCount(0), mParent(nsnull), mMenuBar(nsnull), - mMacMenu(nil), mNativeMenuItem(nil), mIsEnabled(PR_TRUE), + mNativeMenu(nil), mNativeMenuItem(nil), mIsEnabled(PR_TRUE), mDestroyHandlerCalled(PR_FALSE), mNeedsRebuild(PR_TRUE), mConstructed(PR_FALSE), mVisible(PR_TRUE), mXBLAttached(PR_FALSE) { @@ -118,8 +118,8 @@ nsMenuX::~nsMenuX() RemoveAll(); - [mMacMenu setDelegate:nil]; - [mMacMenu release]; + [mNativeMenu setDelegate:nil]; + [mNativeMenu release]; [mMenuDelegate release]; [mNativeMenuItem release]; @@ -133,14 +133,13 @@ nsMenuX::~nsMenuX() } -nsresult nsMenuX::Create(nsMenuObjectX* aParent, const nsAString& aLabel, - nsMenuBarX* aMenuBar, nsIContent* aNode) +nsresult nsMenuX::Create(nsMenuObjectX* aParent, nsMenuBarX* aMenuBar, nsIContent* aNode) { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; mContent = aNode; - mLabel = aLabel; - mMacMenu = CreateMenuWithGeckoString(mLabel); + mContent->GetAttr(kNameSpaceID_None, nsWidgetAtoms::label, mLabel); + mNativeMenu = CreateMenuWithGeckoString(mLabel); // register this menu to be notified when changes are made to our content object mMenuBar = aMenuBar; // weak ref @@ -164,6 +163,7 @@ nsresult nsMenuX::Create(nsMenuObjectX* aParent, const nsAString& aLabel, NSString *newCocoaLabelString = nsMenuUtilsX::CreateTruncatedCocoaLabel(mLabel); mNativeMenuItem = [[NSMenuItem alloc] initWithTitle:newCocoaLabelString action:nil keyEquivalent:@""]; [newCocoaLabelString release]; + [mNativeMenuItem setSubmenu:mNativeMenu]; [mNativeMenuItem setEnabled:(BOOL)mIsEnabled]; @@ -171,7 +171,7 @@ nsresult nsMenuX::Create(nsMenuObjectX* aParent, const nsAString& aLabel, // native menu items being created. If we only call MenuConstruct when a menu // is actually selected, then we can't access keyboard commands until the // menu gets selected, which is bad. - MenuConstruct(nsnull, nsnull); + MenuConstruct(); mIcon = new nsMenuItemIconX(this, mContent, mNativeMenuItem); @@ -196,7 +196,7 @@ nsresult nsMenuX::AddMenuItem(nsMenuItemX* aMenuItem) NSMenuItem* newNativeMenuItem = (NSMenuItem*)aMenuItem->NativeData(); // add the menu item to this menu - [mMacMenu addItem:newNativeMenuItem]; + [mNativeMenu addItem:newNativeMenuItem]; // set up target/action [newNativeMenuItem setTarget:nsMenuBarX::sNativeEventTarget]; @@ -225,10 +225,10 @@ nsresult nsMenuX::AddMenu(nsMenuX* aMenu) ++mVisibleItemsCount; // We have to add a menu item and then associate the menu with it - NSMenuItem* newNativeMenuItem = (static_cast(aMenu))->NativeMenuItem(); + NSMenuItem* newNativeMenuItem = aMenu->NativeMenuItem(); if (!newNativeMenuItem) return NS_ERROR_FAILURE; - [mMacMenu addItem:newNativeMenuItem]; + [mNativeMenu addItem:newNativeMenuItem]; [newNativeMenuItem setSubmenu:(NSMenu*)aMenu->NativeData()]; @@ -306,14 +306,14 @@ nsresult nsMenuX::RemoveAll() { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; - if (mMacMenu) { + if (mNativeMenu) { // clear command id's - int itemCount = [mMacMenu numberOfItems]; + int itemCount = [mNativeMenu numberOfItems]; for (int i = 0; i < itemCount; i++) - mMenuBar->UnregisterCommand((PRUint32)[[mMacMenu itemAtIndex:i] tag]); + mMenuBar->UnregisterCommand((PRUint32)[[mNativeMenu itemAtIndex:i] tag]); // get rid of Cocoa menu items - for (int i = [mMacMenu numberOfItems] - 1; i >= 0; i--) - [mMacMenu removeItemAtIndex:i]; + for (int i = [mNativeMenu numberOfItems] - 1; i >= 0; i--) + [mNativeMenu removeItemAtIndex:i]; } mMenuObjectsArray.Clear(); @@ -334,7 +334,7 @@ nsEventStatus nsMenuX::MenuOpened(const nsMenuEvent & aMenuEvent) // at this point, the carbon event handler was installed so there // must be a carbon MenuRef to be had - if (_NSGetCarbonMenu(mMacMenu) == selectedMenuHandle) { + if (_NSGetCarbonMenu(mNativeMenu) == selectedMenuHandle) { // Open the node. mContent->SetAttr(kNameSpaceID_None, nsWidgetAtoms::open, NS_LITERAL_STRING("true"), PR_TRUE); @@ -348,7 +348,7 @@ nsEventStatus nsMenuX::MenuOpened(const nsMenuEvent & aMenuEvent) if (mNeedsRebuild) RemoveAll(); - MenuConstruct(nsnull, nsnull); + MenuConstruct(); mConstructed = true; } @@ -394,16 +394,16 @@ void nsMenuX::MenuClosed(const nsMenuEvent & aMenuEvent) } -void nsMenuX::MenuConstruct(nsIWidget* aParentWindow, void* aMenuNode) +void nsMenuX::MenuConstruct() { mConstructed = false; gConstructingMenu = PR_TRUE; // reset destroy handler flag so that we'll know to fire it next time this menu goes away. mDestroyHandlerCalled = PR_FALSE; - - //printf("nsMenuX::MenuConstruct called for %s = %d \n", NS_LossyConvertUTF16toASCII(mLabel).get(), mMacMenu); - + + //printf("nsMenuX::MenuConstruct called for %s = %d \n", NS_LossyConvertUTF16toASCII(mLabel).get(), mNativeMenu); + // Retrieve our menupopup. nsCOMPtr menuPopup; GetMenuPopupContent(getter_AddRefs(menuPopup)); @@ -551,15 +551,11 @@ void nsMenuX::LoadMenuItem(nsIContent* inMenuItemContent) void nsMenuX::LoadSubMenu(nsIContent* inMenuContent) { - nsAutoString menuName; - inMenuContent->GetAttr(kNameSpaceID_None, nsWidgetAtoms::label, menuName); - //printf("Creating Menu [%s] \n", NS_LossyConvertUTF16toASCII(menuName).get()); - nsAutoPtr menu(new nsMenuX()); if (!menu) return; - nsresult rv = menu->Create(this, menuName, mMenuBar, inMenuContent); + nsresult rv = menu->Create(this, mMenuBar, inMenuContent); if (NS_FAILED(rv)) return; @@ -839,9 +835,9 @@ void nsMenuX::ObserveAttributeChanged(nsIDocument *aDocument, nsIContent *aConte // a regular menu, just change the title and redraw the menubar. if (parentType == eMenuBarObjectType) { // reuse the existing menu, to avoid rebuilding the root menu bar. - NS_ASSERTION(mMacMenu, "nsMenuX::AttributeChanged: invalid menu handle."); + NS_ASSERTION(mNativeMenu, "nsMenuX::AttributeChanged: invalid menu handle."); NSString *newCocoaLabelString = nsMenuUtilsX::CreateTruncatedCocoaLabel(mLabel); - [mMacMenu setTitle:newCocoaLabelString]; + [mNativeMenu setTitle:newCocoaLabelString]; [newCocoaLabelString release]; } else { @@ -873,7 +869,7 @@ void nsMenuX::ObserveAttributeChanged(nsIDocument *aDocument, nsIContent *aConte if (parentType == eMenuBarObjectType || parentType == eSubmenuObjectType) { NSMenu* parentMenu = (NSMenu*)mParent->NativeData(); [parentMenu insertItem:mNativeMenuItem atIndex:insertAfter]; - [mNativeMenuItem setSubmenu:mMacMenu]; + [mNativeMenuItem setSubmenu:mNativeMenu]; mVisible = PR_TRUE; } } diff --git a/widget/src/xpwidgets/nsBaseWidget.h b/widget/src/xpwidgets/nsBaseWidget.h index 0b5ff723e45..376c9f32104 100644 --- a/widget/src/xpwidgets/nsBaseWidget.h +++ b/widget/src/xpwidgets/nsBaseWidget.h @@ -137,6 +137,7 @@ public: virtual void FreeNativeData(void * data, PRUint32 aDataType) {} NS_IMETHOD BeginResizeDrag(nsGUIEvent* aEvent, PRInt32 aHorizontal, PRInt32 aVertical); virtual nsresult ActivateNativeMenuItemAt(const nsAString& indexString) { return NS_ERROR_NOT_IMPLEMENTED; } + virtual nsresult ForceNativeMenuReload() { return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHOD ResetInputState() { return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHOD SetIMEOpenState(PRBool aState) { return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHOD GetIMEOpenState(PRBool* aState) { return NS_ERROR_NOT_IMPLEMENTED; } diff --git a/widget/tests/native_menus_window.xul b/widget/tests/native_menus_window.xul index 1b68e3c4f7a..c1b0694d309 100644 --- a/widget/tests/native_menus_window.xul +++ b/widget/tests/native_menus_window.xul @@ -46,21 +46,31 @@ onload="onLoad();" title="Native Menu Test"> + - + - + + + + + + + + - + - @@ -78,13 +88,26 @@ window.opener.wrappedJSObject.SimpleTest.finish(); } + // We need to force a native menu reload before testing any dom changes + // because dom changes can affect the native menu system lazily. + function forceNativeMenuReload() { + netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect'); + var utils = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor). + getInterface(Components.interfaces.nsIDOMWindowUtils); + try { + utils.forceNativeMenuReload(); + } + catch (e) { + dump(e + "\n"); + } + } + var executedCommandID = ""; function testItem(location, targetID) { netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect'); var utils = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor). getInterface(Components.interfaces.nsIDOMWindowUtils); - var correctCommandHandler = false; try { utils.activateNativeMenuItemAt(location); @@ -94,22 +117,159 @@ dump(e + "\n"); } finally { - ok(correctCommandHandler, 'Command handler not run correctly'); executedCommandID = ""; + return correctCommandHandler; } } + function createXULMenuPopup() { + const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + var item = document.createElementNS(XUL_NS, "menupopup"); + return item; + } + + function createXULMenu(aLabel) { + const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + var item = document.createElementNS(XUL_NS, "menu"); + item.setAttribute("label", aLabel); + return item; + } + + function createXULMenuItem(aLabel, aCommandId) { + const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + var item = document.createElementNS(XUL_NS, "menuitem"); + item.setAttribute("label", aLabel); + item.setAttribute("command", aCommandId); + return item; + } + + function runBaseMenuTests() { + return testItem("0|0", "cmd_FooItem0") && + testItem("0|1", "cmd_FooItem1") && + testItem("0|3|0", "cmd_BarItem0") && + testItem("0|3|1", "cmd_BarItem1"); + } + function onLoad() { var _delayedOnLoad = function() { - testItem("0|0", "cmd_FooItem1"); - testItem("0|1", "cmd_FooItem2"); - testItem("0|3|0", "cmd_BarItem1"); - testItem("0|3|1", "cmd_BarItem2"); - onTestsFinished() + // First let's run the base menu tests. + ok(runBaseMenuTests()); + + // Set up some nodes that we'll use. + var menubarNode = document.getElementById("nativemenubar"); + var newMenu0 = createXULMenu("NewMenu0"); + var newMenu1 = createXULMenu("NewMenu1"); + var newMenuPopup0 = createXULMenuPopup(); + var newMenuPopup1 = createXULMenuPopup(); + var newMenuItem0 = createXULMenuItem("NewMenuItem0", "cmd_NewItem0"); + var newMenuItem1 = createXULMenuItem("NewMenuItem1", "cmd_NewItem1"); + var newMenuItem2 = createXULMenuItem("NewMenuItem2", "cmd_NewItem2"); + var newMenuItem3 = createXULMenuItem("NewMenuItem3", "cmd_NewItem3"); + var newMenuItem4 = createXULMenuItem("NewMenuItem4", "cmd_NewItem4"); + var newMenuItem5 = createXULMenuItem("NewMenuItem5", "cmd_NewItem5"); + + // Create another submenu with hierarchy via DOM manipulation. + // ****************** + // * Foo * NewMenu0 * <- Menu bar + // ****************** + // **************** + // * NewMenuItem0 * <- NewMenu0 submenu + // **************** + // * NewMenuItem1 * + // **************** + // * NewMenuItem2 * + // ******************************* + // * NewMenu1 > * NewMenuItem3 * <- NewMenu1 submenu + // ******************************* + // * NewMenuItem4 * + // **************** + // * NewMenuItem5 * + // **************** + menubarNode.appendChild(newMenu0); + newMenu0.appendChild(newMenuPopup0); + newMenuPopup0.appendChild(newMenuItem0); + newMenuPopup0.appendChild(newMenuItem1); + newMenuPopup0.appendChild(newMenuItem2); + newMenuPopup0.appendChild(newMenu1); + newMenu1.appendChild(newMenuPopup1); + newMenuPopup1.appendChild(newMenuItem3); + newMenuPopup1.appendChild(newMenuItem4); + newMenuPopup1.appendChild(newMenuItem5); + + // Error strings. + var sa = "Command handler(s) should have activated"; + var sna = "Command handler(s) should not have activated"; + + // Run basic tests again. + forceNativeMenuReload(); + ok(runBaseMenuTests()); + + // Test middle items. + ok(testItem("1|1", "cmd_NewItem1"), sa); + ok(testItem("1|3|1", "cmd_NewItem4"), sa); + + // Hide newMenu0. + newMenu0.setAttribute("hidden", "true"); + forceNativeMenuReload(); + ok(runBaseMenuTests(), sa); // the base menu should still be unhidden + ok(!testItem("1|0", ""), sna); + ok(!testItem("1|1", ""), sna); + ok(!testItem("1|2", ""), sna); + ok(!testItem("1|3|0", ""), sna); + ok(!testItem("1|3|1", ""), sna); + ok(!testItem("1|3|2", ""), sna); + + // Show newMenu0. + newMenu0.setAttribute("hidden", "false"); + forceNativeMenuReload(); + ok(runBaseMenuTests(), sa); + ok(testItem("1|0", "cmd_NewItem0"), sa); + ok(testItem("1|1", "cmd_NewItem1"), sa); + ok(testItem("1|2", "cmd_NewItem2"), sa); + ok(testItem("1|3|0", "cmd_NewItem3"), sa); + ok(testItem("1|3|1", "cmd_NewItem4"), sa); + ok(testItem("1|3|2", "cmd_NewItem5"), sa); + + // Hide items. + newMenuItem1.setAttribute("hidden", "true"); + newMenuItem4.setAttribute("hidden", "true"); + forceNativeMenuReload(); + ok(runBaseMenuTests(), sa); + ok(testItem("1|0", "cmd_NewItem0"), sa); + ok(testItem("1|1", "cmd_NewItem2"), sa); + ok(!testItem("1|2", ""), sna); + ok(testItem("1|2|0", "cmd_NewItem3"), sa); + ok(testItem("1|2|1", "cmd_NewItem5"), sa); + ok(!testItem("1|2|2", ""), sna); + + // Show items. + newMenuItem1.setAttribute("hidden", "false"); + newMenuItem4.setAttribute("hidden", "false"); + forceNativeMenuReload(); + ok(runBaseMenuTests(), sa); + ok(testItem("1|0", "cmd_NewItem0"), sa); + ok(testItem("1|1", "cmd_NewItem1"), sa); + ok(testItem("1|2", "cmd_NewItem2"), sa); + ok(testItem("1|3|0", "cmd_NewItem3"), sa); + ok(testItem("1|3|1", "cmd_NewItem4"), sa); + ok(testItem("1|3|2", "cmd_NewItem5"), sa); + + // Remove menu. + menubarNode.removeChild(newMenu0); + forceNativeMenuReload(); + ok(runBaseMenuTests(), sa); + ok(!testItem("1|0", ""), sna); + ok(!testItem("1|1", ""), sna); + ok(!testItem("1|2", ""), sna); + ok(!testItem("1|3|0", ""), sna); + ok(!testItem("1|3|1", ""), sna); + ok(!testItem("1|3|2", ""), sna); + + onTestsFinished(); } setTimeout(_delayedOnLoad, 1000); - } + } ]]>