Bug 394600, address bz's additional popup reworking comments, add tests for removing popups, r+sr=bz,a=beltzner

This commit is contained in:
2007-11-17 07:47:38 -08:00
parent f9dea5b623
commit 16d7837d15
16 changed files with 560 additions and 161 deletions

View File

@@ -216,8 +216,9 @@ nsXULPopupListener::PreLaunchPopup(nsIDOMEvent* aMouseEvent)
// Turn the document into a XUL document so we can use SetPopupNode.
nsCOMPtr<nsIDOMXULDocument> xulDocument = do_QueryInterface(content->GetDocument());
if (!xulDocument)
if (!xulDocument) {
return NS_ERROR_FAILURE;
}
// Store clicked-on node in xul document for context menus and menu popups.
xulDocument->SetPopupNode(targetNode);
@@ -354,8 +355,8 @@ GetImmediateChild(nsIContent* aContent, nsIAtom *aTag, nsIContent** aResult)
// content.
//
// aTargetContent is the target of the mouse event aEvent that triggered the
// popup. mElement is the element that the popup menu is attached to. The
// former may be equal to mElement or it may be a descendant.
// popup. mElement is the element that the popup menu is attached to.
// aTargetContent may be equal to mElement or it may be a descendant.
//
// This looks for an attribute on |mElement| of the appropriate popup type
// (popup, context) and uses that attribute's value as an ID for
@@ -451,10 +452,6 @@ nsXULPopupListener::LaunchPopup(nsIDOMEvent* aEvent, nsIContent* aTargetContent)
if (!pm)
return NS_OK;
// XXXndeakin this is temporary. It is needed to grab the mouse location details
// used by the spellchecking popup. See bug 383930.
pm->SetMouseLocation(aEvent, popup);
// For left-clicks, if the popup has an position attribute, or both the
// popupanchor and popupalign attributes are used, anchor the popup to the
// element, otherwise just open it at the screen position where the mouse
@@ -465,7 +462,7 @@ nsXULPopupListener::LaunchPopup(nsIDOMEvent* aEvent, nsIContent* aTargetContent)
(mPopupContent->HasAttr(kNameSpaceID_None, nsGkAtoms::popupanchor) &&
mPopupContent->HasAttr(kNameSpaceID_None, nsGkAtoms::popupalign)))) {
pm->ShowPopup(mPopupContent, content, EmptyString(), 0, 0,
PR_FALSE, PR_TRUE, PR_FALSE);
PR_FALSE, PR_TRUE, PR_FALSE, aEvent);
}
else {
PRInt32 xPos = 0, yPos = 0;
@@ -480,7 +477,7 @@ nsXULPopupListener::LaunchPopup(nsIDOMEvent* aEvent, nsIContent* aTargetContent)
yPos += 2;
}
pm->ShowPopupAtScreen(mPopupContent, xPos, yPos, mIsContext);
pm->ShowPopupAtScreen(mPopupContent, xPos, yPos, mIsContext, aEvent);
}
return NS_OK;

View File

@@ -2192,6 +2192,8 @@ private:
class nsWeakFrame {
public:
nsWeakFrame() : mPrev(nsnull), mFrame(nsnull) { }
nsWeakFrame(nsIFrame* aFrame) : mPrev(nsnull), mFrame(nsnull)
{
Init(aFrame);

View File

@@ -121,7 +121,9 @@ interface nsIPopupBoxObject : nsISupports
* if position is empty.
*
* For an anchored popup, the x and y arguments may be used to offset the
* popup from its anchored position by some number, measured in CSS pixels.
* popup from its anchored position by some distance, measured in CSS pixels.
* x increases to the right and y increases down. Negative values may also
* be used to move to the left and upwards respectively.
*
* Unanchored popups may be created by supplying null as the anchor node.
* An unanchored popup appears at the position specified by x and y,

View File

@@ -349,16 +349,18 @@ public:
// menu. If aIsPopup is false, the navigation is on a menubar, so navigate
// between menus on the menubar. This is used for left/right cursor navigation.
//
// Items that not valid, such as non-menu or menuitem elements are skipped,
// and the next or previous item after that is checked.
// Items that are not valid, such as non-menu or non-menuitem elements are
// skipped, and the next or previous item after that is checked.
//
// If aStart is null, the first valid item is retrieved for GetNextMenuItem
// or the last valid item for GetPreviousMenuItem is used.
// If aStart is null, the first valid item is retrieved by GetNextMenuItem
// and the last valid item is retrieved by GetPreviousMenuItem.
//
// Both methods will loop around the beginning or end if needed.
//
// aParent - the parent menubar or menupopup
// aStart - the menu/menuitem to start navigation from. GetPreviousMenuItem
// returns the item before it, while GetNextMenuItem returns the
// next item.
// item after it.
// aIsPopup - true for menupopups, false for menubars
static nsMenuFrame* GetPreviousMenuItem(nsIFrame* aParent,
nsMenuFrame* aStart,
@@ -385,13 +387,10 @@ public:
// retrieve the node and offset of the last mouse event used to open a
// context menu. This information is determined from the rangeParent and
// the rangeOffset of the event supplied from the last call to SetMouseLocation.
// the rangeOffset of the event supplied to ShowPopup or ShowPopupAtScreen.
// This is used by the implementation of nsIDOMXULDocument::GetPopupRangeParent
// and nsIDOMXULDocument::GetPopupRangeOffset.
void GetMouseLocation(nsIDOMNode** aNode, PRInt32* aOffset);
// set the mouse event that was used to activate the next popup, specified by
// aPopup, to be opened.
void SetMouseLocation(nsIDOMEvent* aEvent, nsIContent* aPopup);
/**
* Open a <menu> given its content node. If aSelectFirstItem is
@@ -406,6 +405,11 @@ public:
* true, then the first item in the menu is selected. The arguments are
* similar to those for nsIPopupBoxObject::OpenPopup.
*
* aTriggerEvent should be the event that triggered the event. This is used
* to determine the coordinates for the popupshowing event. This may be null
* if the popup was not triggered by an event, or the coordinates are not
* important. Note that this may be reworked in bug 383930.
*
* This fires the popupshowing event synchronously.
*/
void ShowPopup(nsIContent* aPopup,
@@ -414,7 +418,8 @@ public:
PRInt32 aXPos, PRInt32 aYPos,
PRBool aIsContextMenu,
PRBool aAttributesOverride,
PRBool aSelectFirstItem);
PRBool aSelectFirstItem,
nsIDOMEvent* aTriggerEvent);
/**
* Open a popup at a specific screen position specified by aXPos and aYPos,
@@ -424,7 +429,8 @@ public:
*/
void ShowPopupAtScreen(nsIContent* aPopup,
PRInt32 aXPos, PRInt32 aYPos,
PRBool aIsContextMenu);
PRBool aIsContextMenu,
nsIDOMEvent* aTriggerEvent);
/**
* This method is provided only for compatibility with an older popup API.
@@ -565,7 +571,7 @@ public:
/**
* Handle keyboard navigation within a menu popup specified by aFrame.
* Returns true if the key was handled and that other default handling
* Returns true if the key was handled and other default handling
* should not occur.
*/
PRBool HandleKeyboardNavigationInPopup(nsMenuPopupFrame* aFrame,
@@ -593,6 +599,17 @@ protected:
// return the topmost menu, skipping over invisible popups
nsMenuChainItem* GetTopVisibleMenu();
// Hide all of the visible popups from the given list. aDeselectMenu
// indicates whether to deselect the menu of popups when hiding; this
// flag is passed as the first argument to HidePopup. This function
// can cause style changes and frame destruction.
void HidePopupsInList(const nsTArray<nsMenuPopupFrame *> &aFrames,
PRBool aDeselectMenu);
// set the event that was used to trigger the popup, or null to
// clear the event details.
void SetTriggerEvent(nsIDOMEvent* aEvent, nsIContent* aPopup);
// callbacks for ShowPopup and HidePopup as events may be done asynchronously
void ShowPopupCallback(nsIContent* aPopup,
nsMenuPopupFrame* aPopupFrame,
@@ -665,7 +682,7 @@ private:
* supplied, then it is expected to have a frame equal to aFrame.
* If aItem is non-null, then the navigation may be redirected to
* an open submenu if one exists. Returns true if the key was
* handled and that other default handling should not occur.
* handled and other default handling should not occur.
*/
PRBool HandleKeyboardNavigationInPopup(nsMenuChainItem* aItem,
nsMenuPopupFrame* aFrame,
@@ -699,7 +716,7 @@ protected:
// widget that is currently listening to rollup events
nsCOMPtr<nsIWidget> mWidget;
// range parent and offset set in SetMouseLocation
// range parent and offset set in SetTriggerEvent
nsCOMPtr<nsIDOMNode> mRangeParent;
PRInt32 mRangeOffset;
nsPoint mCachedMousePoint;

View File

@@ -62,7 +62,7 @@ public:
// deselects the current item and closes its popup if any, then selects the
// new item aMenuItem. For a menubar, if another menu is already open, the
// new menu aMenuItem is opened. In this case, if aSelectFirstItem is true,
// select the first item in it. For menupoups, the menu is not opened and
// select the first item in it. For menupopups, the menu is not opened and
// the aSelectFirstItem argument is not used.
NS_IMETHOD ChangeMenuItem(nsMenuFrame* aMenuItem, PRBool aSelectFirstItem) = 0;

View File

@@ -331,14 +331,12 @@ nsMenuBarFrame::SetCurrentMenuItem(nsMenuFrame* aMenuItem)
if (mCurrentMenu == aMenuItem)
return NS_OK;
nsWeakFrame weakFrame(this);
if (mCurrentMenu)
mCurrentMenu->SelectMenu(PR_FALSE);
if (aMenuItem)
aMenuItem->SelectMenu(PR_TRUE);
NS_ENSURE_TRUE(weakFrame.IsAlive(), NS_OK);
mCurrentMenu = aMenuItem;
mRecentlyClosedMenu = nsnull;
@@ -432,9 +430,7 @@ nsMenuBarFrame::ChangeMenuItem(nsMenuFrame* aMenuItem,
// Set the new child.
if (aMenuItem) {
nsCOMPtr<nsIContent> content = aMenuItem->GetContent();
nsWeakFrame weakNewMenu(aMenuItem);
aMenuItem->SelectMenu(PR_TRUE);
NS_ENSURE_TRUE(weakNewMenu.IsAlive(), NS_OK);
mCurrentMenu = aMenuItem;
if (wasOpen && !aMenuItem->IsDisabled())
aNewMenu = content;

View File

@@ -333,9 +333,8 @@ nsMenuFrame::GetFirstChild(nsIAtom* aListName) const
return nsBoxFrame::GetFirstChild(aListName);
}
NS_IMETHODIMP
nsMenuFrame::SetInitialChildList(nsIAtom* aListName,
nsIFrame* aChildList)
nsIFrame*
nsMenuFrame::SetPopupFrame(nsIFrame* aChildList)
{
// Check for a menupopup and move it to mPopupFrame
nsFrameList frames(aChildList);
@@ -351,7 +350,16 @@ nsMenuFrame::SetInitialChildList(nsIAtom* aListName,
frame = frame->GetNextSibling();
}
// Didn't find it.
return aChildList;
}
NS_IMETHODIMP
nsMenuFrame::SetInitialChildList(nsIAtom* aListName,
nsIFrame* aChildList)
{
NS_ASSERTION(!mPopupFrame, "already have a popup frame set");
if (!aListName || aListName == nsGkAtoms::popupList)
aChildList = SetPopupFrame(aChildList);
return nsBoxFrame::SetInitialChildList(aListName, aChildList);
}
@@ -1184,27 +1192,31 @@ nsMenuFrame::InsertFrames(nsIAtom* aListName,
nsIFrame* aPrevFrame,
nsIFrame* aFrameList)
{
nsresult rv;
if (!mPopupFrame && aFrameList->GetType() == nsGkAtoms::menuPopupFrame) {
mPopupFrame = static_cast<nsMenuPopupFrame *>(aFrameList);
if (!mPopupFrame && (!aListName || aListName == nsGkAtoms::popupList)) {
aFrameList = SetPopupFrame(aFrameList);
if (mPopupFrame) {
#ifdef DEBUG_LAYOUT
nsBoxLayoutState state(PresContext());
SetDebug(state, aFrameList, mState & NS_STATE_CURRENTLY_IN_DEBUG);
#endif
PresContext()->PresShell()->
FrameNeedsReflow(this, nsIPresShell::eTreeChange,
NS_FRAME_HAS_DIRTY_CHILDREN);
rv = NS_OK;
} else {
return NS_OK;
}
}
if (!aFrameList)
return NS_OK;
if (NS_UNLIKELY(aPrevFrame == mPopupFrame)) {
aPrevFrame = nsnull;
}
rv = nsBoxFrame::InsertFrames(aListName, aPrevFrame, aFrameList);
}
return rv;
return nsBoxFrame::InsertFrames(aListName, aPrevFrame, aFrameList);
}
NS_IMETHODIMP
@@ -1214,10 +1226,9 @@ nsMenuFrame::AppendFrames(nsIAtom* aListName,
if (!aFrameList)
return NS_OK;
nsresult rv;
if (!mPopupFrame && aFrameList->GetType() == nsGkAtoms::menuPopupFrame) {
mPopupFrame = static_cast<nsMenuPopupFrame *>(aFrameList);
if (!mPopupFrame && (!aListName || aListName == nsGkAtoms::popupList)) {
aFrameList = SetPopupFrame(aFrameList);
if (mPopupFrame) {
#ifdef DEBUG_LAYOUT
nsBoxLayoutState state(PresContext());
@@ -1226,12 +1237,15 @@ nsMenuFrame::AppendFrames(nsIAtom* aListName,
PresContext()->PresShell()->
FrameNeedsReflow(this, nsIPresShell::eTreeChange,
NS_FRAME_HAS_DIRTY_CHILDREN);
rv = NS_OK;
} else {
rv = nsBoxFrame::AppendFrames(aListName, aFrameList);
return NS_OK;
}
}
return rv;
if (!aFrameList)
return NS_OK;
return nsBoxFrame::AppendFrames(aListName, aFrameList);
}
PRBool

View File

@@ -103,12 +103,6 @@ private:
nsMenuFrame* mFrame;
};
/**
* @note *** Methods marked with '@see comment above ***' may cause the frame to be
* deleted during the method call. Be careful whenever using those
* methods.
*/
class nsMenuFrame : public nsBoxFrame,
public nsIMenuFrame,
public nsIScrollableViewProvider
@@ -140,16 +134,17 @@ public:
NS_IMETHOD SetInitialChildList(nsIAtom* aListName,
nsIFrame* aChildList);
virtual nsIAtom* GetAdditionalChildListName(PRInt32 aIndex) const;
virtual void Destroy(); // @see comment above ***
virtual void Destroy();
// Overridden to prevent events from going to children of the menu.
NS_IMETHOD BuildDisplayListForChildren(nsDisplayListBuilder* aBuilder,
const nsRect& aDirtyRect,
const nsDisplayListSet& aLists);
// this method can destroy the frame
NS_IMETHOD HandleEvent(nsPresContext* aPresContext,
nsGUIEvent* aEvent,
nsEventStatus* aEventStatus); // @see comment above ***
nsEventStatus* aEventStatus);
NS_IMETHOD AppendFrames(nsIAtom* aListName,
nsIFrame* aFrameList);
@@ -163,12 +158,10 @@ public:
virtual nsIAtom* GetType() const { return nsGkAtoms::menuFrame; }
NS_IMETHOD SelectMenu(PRBool aActivateFlag); // @see comment above ***
NS_IMETHOD SelectMenu(PRBool aActivateFlag);
/**
* NOTE: OpenMenu will open the menu synchronously. Don't call this if a frame
* is manipulated afterwards without checking to make sure it is still alive.
* All current calls to OpenMenu do not adjust the frame.
* NOTE: OpenMenu will open the menu asynchronously.
*/
void OpenMenu(PRBool aSelectFirstItem);
// CloseMenu closes the menu asynchronously
@@ -177,11 +170,12 @@ public:
PRBool IsChecked() { return mChecked; }
NS_IMETHOD GetActiveChild(nsIDOMElement** aResult);
NS_IMETHOD SetActiveChild(nsIDOMElement* aChild); // @see comment above ***
NS_IMETHOD SetActiveChild(nsIDOMElement* aChild);
// called when the Enter key is pressed while the menuitem is the current
// one in its parent popup. This will carry out the command attached to
// the menuitem.
// the menuitem. If the menu should be opened, this frame will be returned,
// otherwise null will be returned.
nsMenuFrame* Enter();
NS_IMETHOD SetParent(const nsIFrame* aParent);
@@ -209,11 +203,12 @@ public:
// indiciate that the menu's popup has just been opened, so that the menu
// can update its open state. This method modifies the open attribute on
// the menu, so the frames could be gone after this call
// the menu, so the frames could be gone after this call.
void PopupOpened();
// indiciate that the menu's popup has just been closed, so that the menu
// can update its open state. The menu should be unhighlighted if
// aDeselectedMenu is true.
// aDeselectedMenu is true. This method modifies the open attribute on
// the menu, so the frames could be gone after this call.
void PopupClosed(PRBool aDeselectMenu);
// returns true if this is a menu on another menu popup. A menu is a submenu
@@ -234,23 +229,32 @@ protected:
friend class nsMenuTimerMediator;
friend class nsASyncMenuInitialization;
// initialize mPopupFrame to the first popup frame within aChildList. Returns
// aChildList with the popup frame removed.
nsIFrame* SetPopupFrame(nsIFrame* aChildList);
// set mMenuParent to the nearest enclosing menu bar or menupopup frame of
// aParent (or aParent itself). This is called when initializing the frame,
// so aParent should be the expected parent of this frame.
void InitMenuParent(nsIFrame* aParent);
void UpdateMenuType(nsPresContext* aPresContext); // @see comment above ***
void UpdateMenuSpecialState(nsPresContext* aPresContext); // @see comment above ***
// Update the menu's type (normal, checkbox, radio).
// This method can destroy the frame.
void UpdateMenuType(nsPresContext* aPresContext);
// Update the checked state of the menu, and for radios, clear any other
// checked items. This method can destroy the frame.
void UpdateMenuSpecialState(nsPresContext* aPresContext);
// Examines the key node and builds the accelerator.
void BuildAcceleratorText();
// Called to execute our command handler.
void Execute(nsGUIEvent *aEvent); // @see comment above ***
// Called to execute our command handler. This method can destroy the frame.
void Execute(nsGUIEvent *aEvent);
// This method can destroy the frame
NS_IMETHOD AttributeChanged(PRInt32 aNameSpaceID,
nsIAtom* aAttribute,
PRInt32 aModType); // @see comment above ***
PRInt32 aModType);
virtual ~nsMenuFrame();
PRBool SizeToPopup(nsBoxLayoutState& aState, nsSize& aSize);

View File

@@ -203,9 +203,11 @@ public:
void SetGeneratedChildren() { mGeneratedChildren = PR_TRUE; }
// called when the Enter key is pressed while the popup is open. This will
// just pass the call down to the current menu, if any. Also, calling Enter
// will reset the current incremental search string, calculated in
// FindMenuWithShortcut
// just pass the call down to the current menu, if any. If a current menu
// should be opened as a result, this method should return the frame for
// that menu, or null if no menu should be opened. Also, calling Enter will
// reset the current incremental search string, calculated in
// FindMenuWithShortcut.
nsMenuFrame* Enter();
nsPopupType PopupType() const { return mPopupType; }

View File

@@ -130,7 +130,7 @@ nsPopupBoxObject::OpenPopup(nsIDOMElement* aAnchorElement,
if (pm) {
nsCOMPtr<nsIContent> anchorContent(do_QueryInterface(aAnchorElement));
pm->ShowPopup(mContent, anchorContent, aPosition, aXPos, aYPos,
aIsContextMenu, aAttributesOverride, PR_FALSE);
aIsContextMenu, aAttributesOverride, PR_FALSE, nsnull);
}
return NS_OK;
@@ -141,7 +141,7 @@ nsPopupBoxObject::OpenPopupAtScreen(PRInt32 aXPos, PRInt32 aYPos, PRBool aIsCont
{
nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
if (pm)
pm->ShowPopupAtScreen(mContent, aXPos, aYPos, aIsContextMenu);
pm->ShowPopupAtScreen(mContent, aXPos, aYPos, aIsContextMenu, nsnull);
return NS_OK;
}

View File

@@ -265,12 +265,11 @@ nsXULPopupManager::GetMouseLocation(nsIDOMNode** aNode, PRInt32* aOffset)
}
void
nsXULPopupManager::SetMouseLocation(nsIDOMEvent* aEvent, nsIContent* aPopup)
nsXULPopupManager::SetTriggerEvent(nsIDOMEvent* aEvent, nsIContent* aPopup)
{
mCachedMousePoint = nsPoint(0, 0);
nsCOMPtr<nsIDOMNSUIEvent> uiEvent = do_QueryInterface(aEvent);
NS_ASSERTION(!aEvent || uiEvent, "Expected an nsIDOMNSUIEvent");
if (uiEvent) {
uiEvent->GetRangeParent(getter_AddRefs(mRangeParent));
uiEvent->GetRangeOffset(&mRangeOffset);
@@ -349,7 +348,7 @@ nsXULPopupManager::ShowMenu(nsIContent *aMenu,
popupFrame->InitializePopup(aMenu, position, 0, 0, PR_TRUE);
if (aAsynchronous) {
SetMouseLocation(nsnull, nsnull);
SetTriggerEvent(nsnull, nsnull);
nsCOMPtr<nsIRunnable> event =
new nsXULPopupShowingEvent(popupFrame->GetContent(), aMenu,
parentIsContextMenu, aSelectFirstItem);
@@ -370,12 +369,15 @@ nsXULPopupManager::ShowPopup(nsIContent* aPopup,
PRInt32 aXPos, PRInt32 aYPos,
PRBool aIsContextMenu,
PRBool aAttributesOverride,
PRBool aSelectFirstItem)
PRBool aSelectFirstItem,
nsIDOMEvent* aTriggerEvent)
{
nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(aPopup);
if (!popupFrame || !MayShowPopup(popupFrame))
return;
SetTriggerEvent(aTriggerEvent, aPopup);
popupFrame->InitializePopup(aAnchorContent, aPosition, aXPos, aYPos,
aAttributesOverride);
@@ -386,12 +388,15 @@ nsXULPopupManager::ShowPopup(nsIContent* aPopup,
void
nsXULPopupManager::ShowPopupAtScreen(nsIContent* aPopup,
PRInt32 aXPos, PRInt32 aYPos,
PRBool aIsContextMenu)
PRBool aIsContextMenu,
nsIDOMEvent* aTriggerEvent)
{
nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(aPopup);
if (!popupFrame || !MayShowPopup(popupFrame))
return;
SetTriggerEvent(aTriggerEvent, aPopup);
popupFrame->InitializePopupAtScreen(aXPos, aYPos);
FirePopupShowingEvent(aPopup, nsnull, popupFrame->PresContext(),
@@ -410,6 +415,8 @@ nsXULPopupManager::ShowPopupWithAnchorAlign(nsIContent* aPopup,
if (!popupFrame || !MayShowPopup(popupFrame))
return;
SetTriggerEvent(nsnull, nsnull);
popupFrame->InitializePopupWithAnchorAlign(aAnchorContent, aAnchor,
aAlign, aXPos, aYPos);
@@ -494,9 +501,6 @@ nsXULPopupManager::HidePopup(nsIContent* aPopup,
PRBool aDeselectMenu,
PRBool aAsynchronous)
{
// remove the popup from the open lists. Just to be safe, check both the
// menu and panels lists.
// if the popup is on the panels list, remove it but don't close any other panels
nsMenuPopupFrame* popupFrame = nsnull;
PRBool foundPanel = PR_FALSE;
@@ -536,7 +540,7 @@ nsXULPopupManager::HidePopup(nsIContent* aPopup,
// the next popup in the chain. These two methods will be called in
// sequence recursively to close up all the necessary popups. In
// asynchronous mode, a similar process occurs except that the
// FirePopupHidingEvent method is called asynchrounsly. In either case,
// FirePopupHidingEvent method is called asynchronously. In either case,
// nextPopup is set to the content node of the next popup to close, and
// lastPopup is set to the last popup in the chain to close, which will be
// aPopup, or null to close up all menus.
@@ -708,33 +712,68 @@ nsXULPopupManager::HidePopupAfterDelay(nsMenuPopupFrame* aPopup)
}
void
nsXULPopupManager::HidePopupsInDocument(nsIDocument* aDocument)
nsXULPopupManager::HidePopupsInList(const nsTArray<nsMenuPopupFrame *> &aFrames,
PRBool aDeselectMenu)
{
nsMenuChainItem* item = GetTopVisibleMenu();
while (item) {
nsMenuChainItem* parent = item->GetParent();
if (item->Content()->GetOwnerDoc() == aDocument) {
item->Frame()->HidePopup(PR_TRUE, ePopupInvisible);
item->Detach(&mCurrentMenu);
delete item;
}
item = parent;
// Create a weak frame list. This is done in a separate array with the
// right capacity predetermined, otherwise the array would get resized and
// move the weak frame pointers around.
nsTArray<nsWeakFrame> weakPopups(aFrames.Length());
PRUint32 f;
for (f = 0; f < aFrames.Length(); f++) {
nsWeakFrame* wframe = weakPopups.AppendElement();
if (wframe)
*wframe = aFrames[f];
}
item = mPanels;
while (item) {
nsMenuChainItem* parent = item->GetParent();
if (item->Content()->GetOwnerDoc() == aDocument) {
item->Frame()->HidePopup(PR_TRUE, ePopupInvisible);
item->Detach(&mPanels);
delete item;
for (f = 0; f < weakPopups.Length(); f++) {
// check to ensure that the frame is still alive before hiding it.
if (weakPopups[f].IsAlive()) {
nsMenuPopupFrame* frame =
static_cast<nsMenuPopupFrame *>(weakPopups[f].GetFrame());
frame->HidePopup(PR_TRUE, ePopupInvisible);
}
item = parent;
}
SetCaptureState(nsnull);
}
void
nsXULPopupManager::HidePopupsInDocument(nsIDocument* aDocument)
{
nsTArray<nsMenuPopupFrame *> popupsToHide;
// iterate to get the set of popup frames to hide
nsMenuChainItem* item = mCurrentMenu;
while (item) {
nsMenuChainItem* parent = item->GetParent();
if (item->Frame()->PopupState() != ePopupInvisible &&
aDocument && item->Content()->GetOwnerDoc() == aDocument) {
nsMenuPopupFrame* frame = item->Frame();
item->Detach(&mCurrentMenu);
delete item;
popupsToHide.AppendElement(frame);
}
item = parent;
}
// now look for panels to hide
item = mPanels;
while (item) {
nsMenuChainItem* parent = item->GetParent();
if (item->Frame()->PopupState() != ePopupInvisible &&
aDocument && item->Content()->GetOwnerDoc() == aDocument) {
nsMenuPopupFrame* frame = item->Frame();
item->Detach(&mPanels);
delete item;
popupsToHide.AppendElement(frame);
}
item = parent;
}
HidePopupsInList(popupsToHide, PR_TRUE);
}
void
nsXULPopupManager::ExecuteMenu(nsIContent* aMenu, nsEvent* aEvent)
{
@@ -760,6 +799,7 @@ nsXULPopupManager::ExecuteMenu(nsIContent* aMenu, nsEvent* aEvent)
// opens a modal dialog. The views associated with the popups needed to be
// hidden and the accesibility events fired before the command executes, but
// the popuphiding/popuphidden events are fired afterwards.
nsTArray<nsMenuPopupFrame *> popupsToHide;
nsMenuChainItem* item = GetTopVisibleMenu();
if (cmm != CloseMenuMode_None) {
while (item) {
@@ -767,14 +807,16 @@ nsXULPopupManager::ExecuteMenu(nsIContent* aMenu, nsEvent* aEvent)
if (!item->IsMenu())
break;
nsMenuChainItem* next = item->GetParent();
item->Frame()->HidePopup(cmm == CloseMenuMode_Auto, ePopupInvisible);
popupsToHide.AppendElement(item->Frame());
if (cmm == CloseMenuMode_Single) // only close one level of menu
break;
item = next;
}
}
SetCaptureState(nsnull);
// Now hide the popups. If the closemenu mode is auto, deselect the menu,
// otherwise only one popup is closing, so keep the parent menu selected.
HidePopupsInList(popupsToHide, cmm == CloseMenuMode_Auto);
}
// Create a trusted event if the triggering event was trusted, or if
// we're called from chrome code (since at least one of our caller
@@ -1102,52 +1144,48 @@ nsXULPopupManager::PopupDestroyed(nsMenuPopupFrame* aPopup)
item = item->GetParent();
}
nsCOMPtr<nsIContent> oldMenu;
if (mCurrentMenu)
oldMenu = mCurrentMenu->Content();
nsTArray<nsMenuPopupFrame *> popupsToHide;
nsMenuChainItem* menuToDestroy = nsnull;
item = mCurrentMenu;
while (item) {
if (item->Frame() == aPopup) {
item->Detach(&mCurrentMenu);
menuToDestroy = item;
nsMenuPopupFrame* frame = item->Frame();
if (frame == aPopup) {
if (frame->PopupState() != ePopupInvisible) {
// Iterate through any child menus and hide them as well, since the
// parent is going away. We won't remove them from the list yet, just
// hide them, as they will be removed from the list when this function
// gets called for that child frame.
nsMenuChainItem* child = item->GetChild();
while (child) {
// if the popup is a child frame of the menu that was destroyed, add
// it to the list of popups to hide. Don't bother with the events
// since the frames are going away. If the child menu is not a child
// frame, for example, a context menu, use HidePopup instead, but call
// it asynchronously since we are in the middle of frame destruction.
nsMenuPopupFrame* childframe = child->Frame();
if (nsLayoutUtils::IsProperAncestorFrame(frame, childframe)) {
popupsToHide.AppendElement(childframe);
}
else {
// HidePopup will take care of hiding any of its children, so
// break out afterwards
HidePopup(child->Content(), PR_FALSE, PR_FALSE, PR_TRUE);
break;
}
child = child->GetChild();
}
}
item->Detach(&mCurrentMenu);
delete item;
break;
}
item = item->GetParent();
}
if (menuToDestroy) {
// menuToDestroy will be set to the item to delete. Iterate through any
// child menus and destroy them as well, since the parent is going away
nsIFrame* menuToDestroyFrame = menuToDestroy->Frame();
item = menuToDestroy->GetChild();
while (item) {
nsMenuChainItem* next = item->GetChild();
// if the popup is a child frame of the menu that was destroyed, unhook
// it from the list of open menus and inform the popup frame that it
// should be hidden. Don't bother with the events since the frames are
// going away. If the child menu is not a child frame, for example, a
// context menu, use HidePopup instead
if (nsLayoutUtils::IsProperAncestorFrame(menuToDestroyFrame, item->Frame())) {
item->Detach(&mCurrentMenu);
item->Frame()->HidePopup(PR_FALSE, ePopupInvisible);
}
else {
HidePopup(item->Content(), PR_FALSE, PR_FALSE, PR_TRUE);
break;
}
delete item;
item = next;
}
delete menuToDestroy;
}
if (oldMenu)
SetCaptureState(oldMenu);
HidePopupsInList(popupsToHide, PR_FALSE);
}
PRBool
@@ -1625,13 +1663,13 @@ nsXULPopupManager::IsValidMenuItem(nsPresContext* aPresContext,
{
PRInt32 ns = aContent->GetNameSpaceID();
nsIAtom *tag = aContent->Tag();
if (ns == kNameSpaceID_XUL &&
tag != nsGkAtoms::menu &&
tag != nsGkAtoms::menuitem)
if (ns == kNameSpaceID_XUL) {
if (tag != nsGkAtoms::menu && tag != nsGkAtoms::menuitem)
return PR_FALSE;
if (ns == kNameSpaceID_XHTML && (!aOnPopup || tag != nsGkAtoms::option))
}
else if (ns != kNameSpaceID_XHTML || !aOnPopup || tag != nsGkAtoms::option) {
return PR_FALSE;
}
PRBool skipNavigatingDisabledMenuItem = PR_TRUE;
if (aOnPopup) {

View File

@@ -512,7 +512,7 @@ nsXULTooltipListener::LaunchTooltip()
nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
if (pm) {
pm->ShowPopupAtScreen(mCurrentTooltip, mMouseClientX, mMouseClientY, PR_FALSE);
pm->ShowPopupAtScreen(mCurrentTooltip, mMouseClientX, mMouseClientY, PR_FALSE, nsnull);
// Clear the current tooltip if the popup was not opened successfully.
if (!pm->IsPopupOpen(mCurrentTooltip))
mCurrentTooltip = nsnull;

View File

@@ -87,6 +87,9 @@ _TEST_FILES = test_bug360220.xul \
test_popup_tree.xul \
test_popup_keys.xul \
test_popuphidden.xul \
test_popupremoving.xul \
test_popupremoving_frame.xul \
frame_popupremoving_frame.xul \
$(NULL)
ifeq (,$(filter mac cocoa,$(MOZ_WIDGET_TOOLKIT)))

View File

@@ -0,0 +1,75 @@
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
<window title="Popup Removing Frame Tests"
onload="setTimeout(init, 0)"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<hbox>
<menu id="separatemenu1" label="Menu">
<menupopup id="separatepopup1" onpopupshown="document.getElementById('separatemenu2').open = true">
<menuitem label="L1 One"/>
<menuitem label="L1 Two"/>
<menuitem label="L1 Three"/>
</menupopup>
</menu>
<menu id="separatemenu2" label="Menu">
<menupopup id="separatepopup2" onpopupshown="document.getElementById('separatemenu3').open = true">
<menuitem label="L2 One"/>
<menuitem label="L2 Two"/>
<menuitem label="L2 Three"/>
</menupopup>
</menu>
<menu id="separatemenu3" label="Menu" onpopupshown="document.getElementById('separatemenu4').open = true">
<menupopup id="separatepopup3">
<menuitem label="L3 One"/>
<menuitem label="L3 Two"/>
<menuitem label="L3 Three"/>
</menupopup>
</menu>
<menu id="separatemenu4" label="Menu" onpopupshown="document.getElementById('nestedmenu1').open = true">
<menupopup id="separatepopup3">
<menuitem label="L4 One"/>
<menuitem label="L4 Two"/>
<menuitem label="L4 Three"/>
</menupopup>
</menu>
</hbox>
<menu id="nestedmenu1" label="Menu">
<menupopup id="nestedpopup1" onpopupshown="if (event.target == this) this.firstChild.open = true">
<menu id="nestedmenu2" label="Menu">
<menupopup id="nestedpopup2" onpopupshown="if (event.target == this) this.firstChild.open = true">
<menu id="nestedmenu3" label="Menu">
<menupopup id="nestedpopup3" onpopupshown="if (event.target == this) this.firstChild.open = true">
<menu id="nestedmenu4" label="Menu" onpopupshown="parent.popupsOpened()">
<menupopup id="nestedpopup4">
<menuitem label="Nested One"/>
<menuitem label="Nested Two"/>
<menuitem label="Nested Three"/>
</menupopup>
</menu>
</menupopup>
</menu>
</menupopup>
</menu>
</menupopup>
</menu>
<script class="testbody" type="application/javascript">
<![CDATA[
function init()
{
document.getElementById("separatemenu1").open = true;
}
]]>
</script>
</window>

View File

@@ -0,0 +1,166 @@
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
<?xml-stylesheet href="/tests/SimpleTest/test.css" type="text/css"?>
<window title="Popup Removing Tests"
onload="setTimeout(nextTest, 0)"
onDOMAttrModified="modified(event)"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<!--
This test checks that popup elements can be removed in various ways without
crashing. It tests two situations, one with menus that are 'separate', and
one with menus that are 'nested'. In each case, there are four levels of menu.
The nextTest function starts the process by opening the first menu. A set of
popupshown event listeners are used to open the next menu until all four are
showing. This last one calls removePopup to remove the menu node from the
tree. This should hide the popups as they are no longer in a document.
A mutation listener is triggered when the fourth menu closes by having its
open attribute cleared. This listener hides the third popup which causes
its frame to be removed. Naturally, we want to ensure that this doesn't
crash when the third menu is removed.
-->
<script type="application/javascript" src="/MochiKit/packed.js"></script>
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
<hbox>
<menu id="nestedmenu1" label="1">
<menupopup id="nestedpopup1" onpopupshown="if (event.target == this) this.firstChild.open = true">
<menu id="nestedmenu2" label="2">
<menupopup id="nestedpopup2" onpopupshown="if (event.target == this) this.firstChild.open = true">
<menu id="nestedmenu3" label="3">
<menupopup id="nestedpopup3" onpopupshown="if (event.target == this) this.firstChild.open = true">
<menu id="nestedmenu4" label="4" onpopupshown="removePopups()">
<menupopup id="nestedpopup4">
<menuitem label="Nested 1"/>
<menuitem label="Nested 2"/>
<menuitem label="Nested 3"/>
</menupopup>
</menu>
</menupopup>
</menu>
</menupopup>
</menu>
</menupopup>
</menu>
<menu id="separatemenu1" label="1">
<menupopup id="separatepopup1" onpopupshown="$('separatemenu2').open = true">
<menuitem label="L1 One"/>
<menuitem label="L1 Two"/>
<menuitem label="L1 Three"/>
</menupopup>
</menu>
<menu id="separatemenu2" label="2">
<menupopup id="separatepopup2" onpopupshown="$('separatemenu3').open = true"
onpopuphidden="popup2Hidden()">
<menuitem label="L2 One"/>
<menuitem label="L2 Two"/>
<menuitem label="L2 Three"/>
</menupopup>
</menu>
<menu id="separatemenu3" label="3" onpopupshown="$('separatemenu4').open = true">
<menupopup id="separatepopup3">
<menuitem label="L3 One"/>
<menuitem label="L3 Two"/>
<menuitem label="L3 Three"/>
</menupopup>
</menu>
<menu id="separatemenu4" label="4" onpopupshown="removePopups()"
onpopuphidden="$('separatemenu2').open = false">
<menupopup id="separatepopup3">
<menuitem label="L4 One"/>
<menuitem label="L4 Two"/>
<menuitem label="L4 Three"/>
</menupopup>
</menu>
</hbox>
<script class="testbody" type="application/javascript">
<![CDATA[
SimpleTest.waitForExplicitFinish();
var gKey = "";
gTriggerMutation = null;
gChangeMutation = null;
function nextTest()
{
if (gKey == "") {
gKey = "separate";
}
else if (gKey == "separate") {
gKey = "nested";
}
else {
SimpleTest.finish();
return;
}
$(gKey + "menu1").open = true;
}
function modified(event)
{
// use this mutation listener to hide the third popup, destroying its frame.
// It gets triggered when the open attribute is cleared on the fourth menu.
if (event.target == gTriggerMutation &&
event.attrName == "open") {
gChangeMutation.hidden = true;
// force a layout flush
document.documentElement.boxObject.width;
gTriggerMutation = null;
gChangeMutation = null;
}
}
function removePopups()
{
var menu2 = $(gKey + "menu2");
var menu3 = $(gKey + "menu3");
is(menu2.getAttribute("open"), "true", gKey + " menu 2 open before");
is(menu3.getAttribute("open"), "true", gKey + " menu 3 open before");
gTriggerMutation = menu3;
gChangeMutation = $(gKey + "menu4");
var menu = $(gKey + "menu1");
menu.parentNode.removeChild(menu);
if (gKey == "nested") {
// the 'separate' test checks this during the popup2 hidden event handler
is(menu2.hasAttribute("open"), false, gKey + " menu 2 open after");
is(menu3.hasAttribute("open"), false, gKey + " menu 3 open after");
nextTest();
}
}
function popup2Hidden()
{
is($(gKey + "menu2").hasAttribute("open"), false, gKey + " menu 2 open after");
nextTest();
}
]]>
</script>
<body xmlns="http://www.w3.org/1999/xhtml">
<p id="display">
</p>
<div id="content" style="display: none">
</div>
<pre id="test">
</pre>
</body>
</window>

View File

@@ -0,0 +1,83 @@
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
<?xml-stylesheet href="/tests/SimpleTest/test.css" type="text/css"?>
<window title="Popup Unload Test"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<!--
This test checks that popup elements are removed when the document is changed.
-->
<script type="application/javascript" src="/MochiKit/packed.js"></script>
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
<iframe id="frame" width="300" height="150" src="frame_popupremoving_frame.xul"/>
<script class="testbody" type="application/javascript">
<![CDATA[
SimpleTest.waitForExplicitFinish();
var gMenus = [];
function popupsOpened()
{
netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
var framedoc = $("frame").contentDocument;
framedoc.addEventListener("DOMAttrModified", modified, false);
// this is the order in which the menus should be hidden (reverse of the
// order they were opened in). The frame for the second menu is removed
// during the mutation listener, so never gets an mutation event.
gMenus.push(framedoc.getElementById("nestedmenu4"));
gMenus.push(framedoc.getElementById("nestedmenu3"));
gMenus.push(framedoc.getElementById("nestedmenu1"));
gMenus.push(framedoc.getElementById("separatemenu4"));
gMenus.push(framedoc.getElementById("separatemenu3"));
gMenus.push(framedoc.getElementById("separatemenu1"));
framedoc.location = "about:blank";
}
function modified(event)
{
if (event.attrName != "open")
return;
netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
var framedoc = $("frame").contentDocument;
var tohide = null;
if (event.target.id == "separatemenu3")
tohide = framedoc.getElementById("separatemenu2");
else if (event.target.id == "nestedmenu3")
tohide = framedoc.getElementById("nestedmenu2");
if (tohide) {
tohide.hidden = true;
// force a layout flush
$("frame").contentDocument.documentElement.boxObject.width;
}
is(event.target, gMenus.shift(), event.target.id + " hidden");
if (gMenus.length == 0)
SimpleTest.finish();
}
]]>
</script>
<body xmlns="http://www.w3.org/1999/xhtml">
<p id="display">
</p>
<div id="content" style="display: none">
</div>
<pre id="test">
</pre>
</body>
</window>