/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is mozilla.org code. * * The Initial Developer of the Original Code is * Netscape Communications Corporation. * Portions created by the Initial Developer are Copyright (C) 1998 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Original Author: David W. Hyatt (hyatt@netscape.com) * Michael Lowe * Pierre Phaneuf * Dean Tessman * * Alternatively, the contents of this file may be used under the terms of * either of the GNU General Public License Version 2 or later (the "GPL"), * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ #include "nsGkAtoms.h" #include "nsHTMLParts.h" #include "nsMenuFrame.h" #include "nsBoxFrame.h" #include "nsIContent.h" #include "prtypes.h" #include "nsIAtom.h" #include "nsPresContext.h" #include "nsIPresShell.h" #include "nsStyleContext.h" #include "nsCSSRendering.h" #include "nsINameSpaceManager.h" #include "nsMenuPopupFrame.h" #include "nsMenuBarFrame.h" #include "nsIView.h" #include "nsIWidget.h" #include "nsIDocument.h" #include "nsIDOMNSDocument.h" #include "nsIDOMDocument.h" #include "nsIDOMElement.h" #include "nsISupportsArray.h" #include "nsIDOMText.h" #include "nsILookAndFeel.h" #include "nsIComponentManager.h" #include "nsWidgetsCID.h" #include "nsBoxLayoutState.h" #include "nsIScrollableFrame.h" #include "nsIViewManager.h" #include "nsBindingManager.h" #include "nsIServiceManager.h" #include "nsCSSFrameConstructor.h" #include "nsIDOMKeyEvent.h" #include "nsEventDispatcher.h" #include "nsIPrivateDOMEvent.h" #include "nsIScrollableView.h" #include "nsXPIDLString.h" #include "nsReadableUtils.h" #include "nsUnicharUtils.h" #include "nsIStringBundle.h" #include "nsGUIEvent.h" #include "nsContentUtils.h" #include "nsDisplayList.h" #include "nsIReflowCallback.h" #define NS_MENU_POPUP_LIST_INDEX 0 #if defined(XP_WIN) || defined(XP_OS2) #define NSCONTEXTMENUISMOUSEUP 1 #endif static PRInt32 gEatMouseMove = PR_FALSE; static NS_DEFINE_IID(kLookAndFeelCID, NS_LOOKANDFEEL_CID); nsrefcnt nsMenuFrame::gRefCnt = 0; nsString *nsMenuFrame::gShiftText = nsnull; nsString *nsMenuFrame::gControlText = nsnull; nsString *nsMenuFrame::gMetaText = nsnull; nsString *nsMenuFrame::gAltText = nsnull; nsString *nsMenuFrame::gModifierSeparator = nsnull; // this class is used for dispatching menu activation events asynchronously. class nsMenuActivateEvent : public nsRunnable { public: nsMenuActivateEvent(nsIContent *aMenu, nsPresContext* aPresContext, PRBool aIsActivate) : mMenu(aMenu), mPresContext(aPresContext), mIsActivate(aIsActivate) { } NS_IMETHOD Run() { nsAutoString domEventToFire; if (mIsActivate) { // Highlight the menu. mMenu->SetAttr(kNameSpaceID_None, nsGkAtoms::menuactive, NS_LITERAL_STRING("true"), PR_TRUE); // The menuactivated event is used by accessibility to track the user's // movements through menus domEventToFire.AssignLiteral("DOMMenuItemActive"); } else { // Unhighlight the menu. mMenu->UnsetAttr(kNameSpaceID_None, nsGkAtoms::menuactive, PR_TRUE); domEventToFire.AssignLiteral("DOMMenuItemInactive"); } nsCOMPtr event; if (NS_SUCCEEDED(nsEventDispatcher::CreateEvent(mPresContext, nsnull, NS_LITERAL_STRING("Events"), getter_AddRefs(event)))) { event->InitEvent(domEventToFire, PR_TRUE, PR_TRUE); nsCOMPtr privateEvent(do_QueryInterface(event)); privateEvent->SetTrusted(PR_TRUE); nsEventDispatcher::DispatchDOMEvent(mMenu, nsnull, event, mPresContext, nsnull); } return NS_OK; } private: nsCOMPtr mMenu; nsCOMPtr mPresContext; PRBool mIsActivate; }; // // NS_NewMenuFrame // // Wrapper for creating a new menu popup container // nsIFrame* NS_NewMenuFrame(nsIPresShell* aPresShell, nsStyleContext* aContext, PRUint32 aFlags) { nsMenuFrame* it = new (aPresShell) nsMenuFrame (aPresShell, aContext); if ((it != nsnull) && aFlags) it->SetIsMenu(PR_TRUE); return it; } NS_IMETHODIMP_(nsrefcnt) nsMenuFrame::AddRef(void) { return NS_OK; } NS_IMETHODIMP_(nsrefcnt) nsMenuFrame::Release(void) { return NS_OK; } // // QueryInterface // NS_INTERFACE_MAP_BEGIN(nsMenuFrame) NS_INTERFACE_MAP_ENTRY(nsIMenuFrame) NS_INTERFACE_MAP_ENTRY(nsIScrollableViewProvider) NS_INTERFACE_MAP_END_INHERITING(nsBoxFrame) // // nsMenuFrame cntr // nsMenuFrame::nsMenuFrame(nsIPresShell* aShell, nsStyleContext* aContext): nsBoxFrame(aShell, aContext), mIsMenu(PR_FALSE), mChecked(PR_FALSE), mType(eMenuType_Normal), mMenuParent(nsnull), mPopupFrame(nsnull) { } // cntr NS_IMETHODIMP nsMenuFrame::SetParent(const nsIFrame* aParent) { nsBoxFrame::SetParent(aParent); InitMenuParent(const_cast(aParent)); return NS_OK; } void nsMenuFrame::InitMenuParent(nsIFrame* aParent) { while (aParent) { nsIAtom* type = aParent->GetType(); if (type == nsGkAtoms::menuPopupFrame) { mMenuParent = static_cast(aParent); break; } else if (type == nsGkAtoms::menuBarFrame) { mMenuParent = static_cast(aParent); break; } aParent = aParent->GetParent(); } } class nsASyncMenuInitialization : public nsIReflowCallback { public: nsASyncMenuInitialization(nsIFrame* aFrame) : mWeakFrame(aFrame) { } virtual PRBool ReflowFinished() { PRBool shouldFlush = PR_FALSE; if (mWeakFrame.IsAlive()) { if (mWeakFrame.GetFrame()->GetType() == nsGkAtoms::menuFrame) { nsMenuFrame* menu = static_cast(mWeakFrame.GetFrame()); menu->UpdateMenuType(menu->PresContext()); shouldFlush = PR_TRUE; } } delete this; return shouldFlush; } virtual void ReflowCallbackCanceled() { delete this; } nsWeakFrame mWeakFrame; }; NS_IMETHODIMP nsMenuFrame::Init(nsIContent* aContent, nsIFrame* aParent, nsIFrame* aPrevInFlow) { nsresult rv = nsBoxFrame::Init(aContent, aParent, aPrevInFlow); // Set up a mediator which can be used for callbacks on this frame. mTimerMediator = new nsMenuTimerMediator(this); if (NS_UNLIKELY(!mTimerMediator)) return NS_ERROR_OUT_OF_MEMORY; InitMenuParent(aParent); //load the display strings for the keyboard accelerators, but only once if (gRefCnt++ == 0) { nsCOMPtr bundleService(do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv)); nsCOMPtr bundle; if (NS_SUCCEEDED(rv) && bundleService) { rv = bundleService->CreateBundle( "chrome://global-platform/locale/platformKeys.properties", getter_AddRefs(bundle)); } NS_ASSERTION(NS_SUCCEEDED(rv) && bundle, "chrome://global/locale/platformKeys.properties could not be loaded"); nsXPIDLString shiftModifier; nsXPIDLString metaModifier; nsXPIDLString altModifier; nsXPIDLString controlModifier; nsXPIDLString modifierSeparator; if (NS_SUCCEEDED(rv) && bundle) { //macs use symbols for each modifier key, so fetch each from the bundle, which also covers i18n rv = bundle->GetStringFromName(NS_LITERAL_STRING("VK_SHIFT").get(), getter_Copies(shiftModifier)); rv = bundle->GetStringFromName(NS_LITERAL_STRING("VK_META").get(), getter_Copies(metaModifier)); rv = bundle->GetStringFromName(NS_LITERAL_STRING("VK_ALT").get(), getter_Copies(altModifier)); rv = bundle->GetStringFromName(NS_LITERAL_STRING("VK_CONTROL").get(), getter_Copies(controlModifier)); rv = bundle->GetStringFromName(NS_LITERAL_STRING("MODIFIER_SEPARATOR").get(), getter_Copies(modifierSeparator)); } else { rv = NS_ERROR_NOT_AVAILABLE; } //if any of these don't exist, we get an empty string gShiftText = new nsString(shiftModifier); gMetaText = new nsString(metaModifier); gAltText = new nsString(altModifier); gControlText = new nsString(controlModifier); gModifierSeparator = new nsString(modifierSeparator); } BuildAcceleratorText(); nsIReflowCallback* cb = new nsASyncMenuInitialization(this); NS_ENSURE_TRUE(cb, NS_ERROR_OUT_OF_MEMORY); PresContext()->PresShell()->PostReflowCallback(cb); return rv; } nsMenuFrame::~nsMenuFrame() { // Clean up shared statics if (--gRefCnt == 0) { delete gShiftText; gShiftText = nsnull; delete gControlText; gControlText = nsnull; delete gMetaText; gMetaText = nsnull; delete gAltText; gAltText = nsnull; delete gModifierSeparator; gModifierSeparator = nsnull; } } // The following methods are all overridden to ensure that the menupopup frame // is placed in the appropriate list. nsIFrame* nsMenuFrame::GetFirstChild(nsIAtom* aListName) const { if (nsGkAtoms::popupList == aListName) { return mPopupFrame; } return nsBoxFrame::GetFirstChild(aListName); } nsIFrame* nsMenuFrame::SetPopupFrame(nsIFrame* aChildList) { // Check for a menupopup and move it to mPopupFrame nsFrameList frames(aChildList); nsIFrame* frame = frames.FirstChild(); while (frame) { if (frame->GetType() == nsGkAtoms::menuPopupFrame) { // Remove this frame from the list and set it as mPopupFrame frames.RemoveFrame(frame); mPopupFrame = (nsMenuPopupFrame *)frame; aChildList = frames.FirstChild(); break; } frame = frame->GetNextSibling(); } 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); } nsIAtom* nsMenuFrame::GetAdditionalChildListName(PRInt32 aIndex) const { if (NS_MENU_POPUP_LIST_INDEX == aIndex) { return nsGkAtoms::popupList; } return nsnull; } void nsMenuFrame::Destroy() { // Kill our timer if one is active. This is not strictly necessary as // the pointer to this frame will be cleared from the mediator, but // this is done for added safety. if (mOpenTimer) { mOpenTimer->Cancel(); } // Null out the pointer to this frame in the mediator wrapper so that it // doesn't try to interact with a deallocated frame. mTimerMediator->ClearFrame(); // if the menu content is just being hidden, it may be made visible again // later, so make sure to clear the highlighting. mContent->UnsetAttr(kNameSpaceID_None, nsGkAtoms::menuactive, PR_FALSE); // are we our menu parent's current menu item? if (mMenuParent && mMenuParent->GetCurrentMenuItem() == this) { // yes; tell it that we're going away mMenuParent->CurrentMenuIsBeingDestroyed(); } if (mPopupFrame) mPopupFrame->Destroy(); nsBoxFrame::Destroy(); } NS_IMETHODIMP nsMenuFrame::BuildDisplayListForChildren(nsDisplayListBuilder* aBuilder, const nsRect& aDirtyRect, const nsDisplayListSet& aLists) { if (!aBuilder->IsForEventDelivery()) return nsBoxFrame::BuildDisplayListForChildren(aBuilder, aDirtyRect, aLists); nsDisplayListCollection set; nsresult rv = nsBoxFrame::BuildDisplayListForChildren(aBuilder, aDirtyRect, set); NS_ENSURE_SUCCESS(rv, rv); return WrapListsInRedirector(aBuilder, set, aLists); } NS_IMETHODIMP nsMenuFrame::HandleEvent(nsPresContext* aPresContext, nsGUIEvent* aEvent, nsEventStatus* aEventStatus) { NS_ENSURE_ARG_POINTER(aEventStatus); nsWeakFrame weakFrame(this); if (*aEventStatus == nsEventStatus_eIgnore) *aEventStatus = nsEventStatus_eConsumeDoDefault; PRBool onmenu = IsOnMenu(); if (aEvent->message == NS_KEY_PRESS && !IsDisabled()) { nsKeyEvent* keyEvent = (nsKeyEvent*)aEvent; PRUint32 keyCode = keyEvent->keyCode; #ifdef XP_MACOSX // On mac, open menulist on either up/down arrow or space (w/o Cmd pressed) if (!IsOpen() && ((keyEvent->charCode == NS_VK_SPACE && !keyEvent->isMeta) || (keyCode == NS_VK_UP || keyCode == NS_VK_DOWN))) OpenMenu(PR_FALSE); #else // On other platforms, toggle menulist on unmodified F4 or Alt arrow if ((keyCode == NS_VK_F4 && !keyEvent->isAlt) || ((keyCode == NS_VK_UP || keyCode == NS_VK_DOWN) && keyEvent->isAlt)) ToggleMenuState(); #endif } else if (aEvent->eventStructType == NS_MOUSE_EVENT && aEvent->message == NS_MOUSE_BUTTON_DOWN && static_cast(aEvent)->button == nsMouseEvent::eLeftButton && !IsDisabled() && IsMenu()) { // The menu item was selected. Bring up the menu. // We have children. if (!mMenuParent || mMenuParent->IsMenuBar()) { ToggleMenuState(); } else { if (!IsOpen()) OpenMenu(PR_FALSE); } } else if ( #ifndef NSCONTEXTMENUISMOUSEUP (aEvent->eventStructType == NS_MOUSE_EVENT && aEvent->message == NS_MOUSE_BUTTON_UP && static_cast(aEvent)->button == nsMouseEvent::eRightButton) && #else aEvent->message == NS_CONTEXTMENU && #endif onmenu && !IsMenu() && !IsDisabled()) { // if this menu is a context menu it accepts right-clicks...fire away! // Make sure we cancel default processing of the context menu event so // that it doesn't bubble and get seen again by the popuplistener and show // another context menu. // // Furthermore (there's always more, isn't there?), on some platforms (win32 // being one of them) we get the context menu event on a mouse up while // on others we get it on a mouse down. For the ones where we get it on a // mouse down, we must continue listening for the right button up event to // dismiss the menu. if (mMenuParent->IsContextMenu()) { *aEventStatus = nsEventStatus_eConsumeNoDefault; Execute(aEvent); } } else if (aEvent->eventStructType == NS_MOUSE_EVENT && aEvent->message == NS_MOUSE_BUTTON_UP && static_cast(aEvent)->button == nsMouseEvent::eLeftButton && !IsMenu() && !IsDisabled()) { // Execute the execute event handler. Execute(aEvent); } else if (aEvent->message == NS_MOUSE_EXIT_SYNTH) { // Kill our timer if one is active. if (mOpenTimer) { mOpenTimer->Cancel(); mOpenTimer = nsnull; } // Deactivate the menu. if (mMenuParent) { PRBool onmenubar = mMenuParent->IsMenuBar(); if (!(onmenubar && mMenuParent->IsActive())) { if (IsMenu() && !onmenubar && IsOpen()) { // Submenus don't get closed up immediately. } else if (this == mMenuParent->GetCurrentMenuItem()) { mMenuParent->ChangeMenuItem(nsnull, PR_FALSE); } } } } else if (aEvent->message == NS_MOUSE_MOVE && (onmenu || (mMenuParent && mMenuParent->IsMenuBar()))) { if (gEatMouseMove) { gEatMouseMove = PR_FALSE; return NS_OK; } // Let the menu parent know we're the new item. mMenuParent->ChangeMenuItem(this, PR_FALSE); NS_ENSURE_TRUE(weakFrame.IsAlive(), NS_OK); NS_ENSURE_TRUE(mMenuParent, NS_OK); // we need to check if we really became the current menu // item or not nsMenuFrame *realCurrentItem = mMenuParent->GetCurrentMenuItem(); if (realCurrentItem != this) { // we didn't (presumably because a context menu was active) return NS_OK; } // Hovering over a menu in a popup should open it without a need for a click. // A timer is used so that it doesn't open if the user moves the mouse quickly // past the menu. This conditional check ensures that only menus have this // behaviour if (!IsDisabled() && IsMenu() && !IsOpen() && !mOpenTimer && !mMenuParent->IsMenuBar()) { PRInt32 menuDelay = 300; // ms nsCOMPtr lookAndFeel(do_GetService(kLookAndFeelCID)); if (lookAndFeel) lookAndFeel->GetMetric(nsILookAndFeel::eMetric_SubmenuDelay, menuDelay); // We're a menu, we're built, we're closed, and no timer has been kicked off. mOpenTimer = do_CreateInstance("@mozilla.org/timer;1"); mOpenTimer->InitWithCallback(mTimerMediator, menuDelay, nsITimer::TYPE_ONE_SHOT); } } return NS_OK; } void nsMenuFrame::ToggleMenuState() { if (IsOpen()) CloseMenu(PR_FALSE); else OpenMenu(PR_FALSE); } void nsMenuFrame::PopupOpened() { nsWeakFrame weakFrame(this); mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::open, NS_LITERAL_STRING("true"), PR_TRUE); if (!weakFrame.IsAlive()) return; if (mMenuParent) { mMenuParent->SetActive(PR_TRUE); // Make sure the current menu which is being toggled on // the menubar is highlighted mMenuParent->SetCurrentMenuItem(this); } } void nsMenuFrame::PopupClosed(PRBool aDeselectMenu) { nsWeakFrame weakFrame(this); mContent->UnsetAttr(kNameSpaceID_None, nsGkAtoms::open, PR_TRUE); if (!weakFrame.IsAlive()) return; // if the popup is for a menu on a menubar, inform menubar to deactivate if (mMenuParent && mMenuParent->MenuClosed()) { if (aDeselectMenu) { SelectMenu(PR_FALSE); } else { // We are not deselecting the parent menu while closing the popup, so send // a DOMMenuItemActive event to the menu to indicate that the menu is // becoming active again. nsMenuFrame *current = mMenuParent->GetCurrentMenuItem(); if (current) { nsCOMPtr event = new nsMenuActivateEvent(current->GetContent(), PresContext(), PR_TRUE); NS_DispatchToCurrentThread(event); } } } } NS_IMETHODIMP nsMenuFrame::SelectMenu(PRBool aActivateFlag) { if (mContent) { // When a menu opens a submenu, the mouse will often be moved onto a // sibling before moving onto an item within the submenu, causing the // parent to become deselected. We need to ensure that the parent menu // is reselected when an item in the submenu is selected, so navigate up // from the item to its popup, and then to the popup above that. if (aActivateFlag) { nsIFrame* parent = GetParent(); while (parent) { if (parent->GetType() == nsGkAtoms::menuPopupFrame) { // a menu is always the direct parent of a menupopup parent = parent->GetParent(); if (parent && parent->GetType() == nsGkAtoms::menuFrame) { // a popup however is not necessarily the direct parent of a menu nsIFrame* popupParent = parent->GetParent(); while (popupParent) { if (popupParent->GetType() == nsGkAtoms::menuPopupFrame) { nsMenuPopupFrame* popup = static_cast(popupParent); popup->SetCurrentMenuItem(static_cast(parent)); break; } popupParent = popupParent->GetParent(); } } break; } parent = parent->GetParent(); } } // cancel the close timer if selecting a menu within the popup to be closed nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); if (pm) pm->CancelMenuTimer(mMenuParent); nsCOMPtr event = new nsMenuActivateEvent(mContent, PresContext(), aActivateFlag); NS_DispatchToCurrentThread(event); } return NS_OK; } NS_IMETHODIMP nsMenuFrame::AttributeChanged(PRInt32 aNameSpaceID, nsIAtom* aAttribute, PRInt32 aModType) { nsAutoString value; if (aAttribute == nsGkAtoms::checked) { if (mType != eMenuType_Normal) UpdateMenuSpecialState(PresContext()); } else if (aAttribute == nsGkAtoms::acceltext) { // someone reset the accelText attribute, so clear the bit that says *we* set it AddStateBits(NS_STATE_ACCELTEXT_IS_DERIVED); BuildAcceleratorText(); } else if (aAttribute == nsGkAtoms::key) { BuildAcceleratorText(); } else if (aAttribute == nsGkAtoms::type || aAttribute == nsGkAtoms::name) UpdateMenuType(PresContext()); return NS_OK; } void nsMenuFrame::OpenMenu(PRBool aSelectFirstItem) { if (!mContent) return; gEatMouseMove = PR_TRUE; nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); if (pm) { pm->KillMenuTimer(); // This opens the menu asynchronously pm->ShowMenu(mContent, aSelectFirstItem, PR_TRUE); } } void nsMenuFrame::CloseMenu(PRBool aDeselectMenu) { gEatMouseMove = PR_TRUE; // Close the menu asynchronously nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); if (pm && mPopupFrame) pm->HidePopup(mPopupFrame->GetContent(), PR_FALSE, aDeselectMenu, PR_TRUE); } PRBool nsMenuFrame::IsSizedToPopup(nsIContent* aContent, PRBool aRequireAlways) { PRBool sizeToPopup; if (aContent->Tag() == nsGkAtoms::select) sizeToPopup = PR_TRUE; else { nsAutoString sizedToPopup; aContent->GetAttr(kNameSpaceID_None, nsGkAtoms::sizetopopup, sizedToPopup); sizeToPopup = sizedToPopup.EqualsLiteral("always") || !aRequireAlways && sizedToPopup.EqualsLiteral("pref"); } return sizeToPopup; } nsSize nsMenuFrame::GetMinSize(nsBoxLayoutState& aBoxLayoutState) { nsSize size = nsBoxFrame::GetMinSize(aBoxLayoutState); DISPLAY_MIN_SIZE(this, size); if (IsSizedToPopup(mContent, PR_TRUE)) SizeToPopup(aBoxLayoutState, size); return size; } NS_IMETHODIMP nsMenuFrame::DoLayout(nsBoxLayoutState& aState) { // lay us out nsresult rv = nsBoxFrame::DoLayout(aState); // layout the popup. First we need to get it. if (mPopupFrame) { PRBool sizeToPopup = IsSizedToPopup(mContent, PR_FALSE); // then get its preferred size nsSize prefSize = mPopupFrame->GetPrefSize(aState); nsSize minSize = mPopupFrame->GetMinSize(aState); nsSize maxSize = mPopupFrame->GetMaxSize(aState); prefSize = BoundsCheck(minSize, prefSize, maxSize); if (sizeToPopup) prefSize.width = mRect.width; // if the pref size changed then set bounds to be the pref size PRBool sizeChanged = (mPopupFrame->PreferredSize() != prefSize); if (sizeChanged) { mPopupFrame->SetPreferredBounds(aState, nsRect(0,0,prefSize.width, prefSize.height)); } // if the menu has just been opened, or its size changed, position // the popup. The flag that the popup checks in the HasOpenChanged // method will get cleared in AdjustView which is called below. if (IsOpen() && (sizeChanged || mPopupFrame->HasOpenChanged())) mPopupFrame->SetPopupPosition(this); // is the new size too small? Make sure we handle scrollbars correctly nsIBox* child = mPopupFrame->GetChildBox(); nsRect bounds(mPopupFrame->GetRect()); nsCOMPtr scrollframe(do_QueryInterface(child)); if (scrollframe && scrollframe->GetScrollbarStyles().mVertical == NS_STYLE_OVERFLOW_AUTO) { if (bounds.height < prefSize.height) { // layout the child mPopupFrame->Layout(aState); nsMargin scrollbars = scrollframe->GetActualScrollbarSizes(); if (bounds.width < prefSize.width + scrollbars.left + scrollbars.right) { bounds.width += scrollbars.left + scrollbars.right; mPopupFrame->SetBounds(aState, bounds); } } } // layout the child mPopupFrame->Layout(aState); mPopupFrame->AdjustView(); } return rv; } #ifdef DEBUG_LAYOUT NS_IMETHODIMP nsMenuFrame::SetDebug(nsBoxLayoutState& aState, PRBool aDebug) { // see if our state matches the given debug state PRBool debugSet = mState & NS_STATE_CURRENTLY_IN_DEBUG; PRBool debugChanged = (!aDebug && debugSet) || (aDebug && !debugSet); // if it doesn't then tell each child below us the new debug state if (debugChanged) { nsBoxFrame::SetDebug(aState, aDebug); if (mPopupFrame) SetDebug(aState, mPopupFrame, aDebug); } return NS_OK; } nsresult nsMenuFrame::SetDebug(nsBoxLayoutState& aState, nsIFrame* aList, PRBool aDebug) { if (!aList) return NS_OK; while (aList) { if (aList->IsBoxFrame()) aList->SetDebug(aState, aDebug); aList = aList->GetNextSibling(); } return NS_OK; } #endif // // Enter // // Called when the user hits the / keys or presses the // shortcut key. If this is a leaf item, the item's action will be executed. // In either case, do nothing if the item is disabled. // nsMenuFrame* nsMenuFrame::Enter() { if (IsDisabled()) { #ifdef XP_WIN // behavior on Windows - close the popup chain if (mMenuParent) { nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); if (pm) { nsIFrame* popup = pm->GetTopPopup(ePopupTypeAny); if (popup) pm->HidePopup(popup->GetContent(), PR_TRUE, PR_TRUE, PR_TRUE); } } #endif // #ifdef XP_WIN // this menu item was disabled - exit return nsnull; } if (!IsOpen()) { // The enter key press applies to us. if (!IsMenu() && mMenuParent) Execute(0); // Execute our event handler else return this; } return nsnull; } PRBool nsMenuFrame::IsOpen() { return mPopupFrame && mPopupFrame->IsOpen(); } PRBool nsMenuFrame::IsMenu() { return mIsMenu; } nsresult nsMenuFrame::Notify(nsITimer* aTimer) { // Our timer has fired. if (aTimer == mOpenTimer.get()) { mOpenTimer = nsnull; if (!IsOpen() && mMenuParent) { // make sure we didn't open a context menu in the meantime // (i.e. the user right-clicked while hovering over a submenu). nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); if (pm) { if ((!pm->HasContextMenu(nsnull) || mMenuParent->IsContextMenu()) && mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::menuactive, nsGkAtoms::_true, eCaseMatters)) { OpenMenu(PR_FALSE); } } } } return NS_OK; } PRBool nsMenuFrame::IsDisabled() { return mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters); } void nsMenuFrame::UpdateMenuType(nsPresContext* aPresContext) { static nsIContent::AttrValuesArray strings[] = {&nsGkAtoms::checkbox, &nsGkAtoms::radio, nsnull}; switch (mContent->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::type, strings, eCaseMatters)) { case 0: mType = eMenuType_Checkbox; break; case 1: mType = eMenuType_Radio; mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::name, mGroupName); break; default: if (mType != eMenuType_Normal) { nsWeakFrame weakFrame(this); mContent->UnsetAttr(kNameSpaceID_None, nsGkAtoms::checked, PR_TRUE); ENSURE_TRUE(weakFrame.IsAlive()); } mType = eMenuType_Normal; break; } UpdateMenuSpecialState(aPresContext); } /* update checked-ness for type="checkbox" and type="radio" */ void nsMenuFrame::UpdateMenuSpecialState(nsPresContext* aPresContext) { PRBool newChecked = mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::checked, nsGkAtoms::_true, eCaseMatters); if (newChecked == mChecked) { /* checked state didn't change */ if (mType != eMenuType_Radio) return; // only Radio possibly cares about other kinds of change if (!mChecked || mGroupName.IsEmpty()) return; // no interesting change } else { mChecked = newChecked; if (mType != eMenuType_Radio || !mChecked) /* * Unchecking something requires no further changes, and only * menuRadio has to do additional work when checked. */ return; } /* * If we get this far, we're type=radio, and: * - our name= changed, or * - we went from checked="false" to checked="true" */ /* * Behavioural note: * If we're checked and renamed _into_ an existing radio group, we are * made the new checked item, and we unselect the previous one. * * The only other reasonable behaviour would be to check for another selected * item in that group. If found, unselect ourselves, otherwise we're the * selected item. That, however, would be a lot more work, and I don't think * it's better at all. */ /* walk siblings, looking for the other checked item with the same name */ // get the first sibling in this menu popup. This frame may be it, and if we're // being called at creation time, this frame isn't yet in the parent's child list. // All I'm saying is that this may fail, but it's most likely alright. nsIFrame* sib = GetParent()->GetFirstChild(nsnull); while (sib) { if (sib != this && sib->GetType() == nsGkAtoms::menuFrame) { nsMenuFrame* menu = static_cast(sib); if (menu->GetMenuType() == eMenuType_Radio && menu->IsChecked() && (menu->GetRadioGroupName() == mGroupName)) { /* uncheck the old item */ sib->GetContent()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::checked, PR_TRUE); /* XXX in DEBUG, check to make sure that there aren't two checked items */ return; } } sib = sib->GetNextSibling(); } } void nsMenuFrame::BuildAcceleratorText() { nsAutoString accelText; if ((GetStateBits() & NS_STATE_ACCELTEXT_IS_DERIVED) == 0) { mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::acceltext, accelText); if (!accelText.IsEmpty()) return; } // accelText is definitely empty here. // Now we're going to compute the accelerator text, so remember that we did. AddStateBits(NS_STATE_ACCELTEXT_IS_DERIVED); // If anything below fails, just leave the accelerator text blank. nsWeakFrame weakFrame(this); mContent->UnsetAttr(kNameSpaceID_None, nsGkAtoms::acceltext, PR_FALSE); ENSURE_TRUE(weakFrame.IsAlive()); // See if we have a key node and use that instead. nsAutoString keyValue; mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::key, keyValue); if (keyValue.IsEmpty()) return; // Turn the document into a DOM document so we can use getElementById nsCOMPtr domDocument(do_QueryInterface(mContent->GetDocument())); if (!domDocument) return; nsCOMPtr keyDOMElement; domDocument->GetElementById(keyValue, getter_AddRefs(keyDOMElement)); if (!keyDOMElement) { #ifdef DEBUG nsAutoString label; mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::label, label); nsAutoString msg = NS_LITERAL_STRING("Key '") + keyValue + NS_LITERAL_STRING("' of menu item '") + label + NS_LITERAL_STRING("' could not be found"); NS_WARNING(NS_ConvertUTF16toUTF8(msg).get()); #endif return; } nsCOMPtr keyElement(do_QueryInterface(keyDOMElement)); if (!keyElement) return; // get the string to display as accelerator text // check the key element's attributes in this order: // |keytext|, |key|, |keycode| nsAutoString accelString; keyElement->GetAttr(kNameSpaceID_None, nsGkAtoms::keytext, accelString); if (accelString.IsEmpty()) { keyElement->GetAttr(kNameSpaceID_None, nsGkAtoms::key, accelString); if (!accelString.IsEmpty()) { ToUpperCase(accelString); } else { nsAutoString keyCode; keyElement->GetAttr(kNameSpaceID_None, nsGkAtoms::keycode, keyCode); ToUpperCase(keyCode); nsresult rv; nsCOMPtr bundleService(do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv)); if (NS_SUCCEEDED(rv) && bundleService) { nsCOMPtr bundle; rv = bundleService->CreateBundle("chrome://global/locale/keys.properties", getter_AddRefs(bundle)); if (NS_SUCCEEDED(rv) && bundle) { nsXPIDLString keyName; rv = bundle->GetStringFromName(keyCode.get(), getter_Copies(keyName)); if (keyName) accelString = keyName; } } // nothing usable found, bail if (accelString.IsEmpty()) return; } } static PRInt32 accelKey = 0; if (!accelKey) { // Compiled-in defaults, in case we can't get LookAndFeel -- // command for mac, control for all other platforms. #ifdef XP_MACOSX accelKey = nsIDOMKeyEvent::DOM_VK_META; #else accelKey = nsIDOMKeyEvent::DOM_VK_CONTROL; #endif // Get the accelerator key value from prefs, overriding the default: accelKey = nsContentUtils::GetIntPref("ui.key.accelKey", accelKey); } nsAutoString modifiers; keyElement->GetAttr(kNameSpaceID_None, nsGkAtoms::modifiers, modifiers); char* str = ToNewCString(modifiers); char* newStr; char* token = nsCRT::strtok(str, ", \t", &newStr); while (token) { if (PL_strcmp(token, "shift") == 0) accelText += *gShiftText; else if (PL_strcmp(token, "alt") == 0) accelText += *gAltText; else if (PL_strcmp(token, "meta") == 0) accelText += *gMetaText; else if (PL_strcmp(token, "control") == 0) accelText += *gControlText; else if (PL_strcmp(token, "accel") == 0) { switch (accelKey) { case nsIDOMKeyEvent::DOM_VK_META: accelText += *gMetaText; break; case nsIDOMKeyEvent::DOM_VK_ALT: accelText += *gAltText; break; case nsIDOMKeyEvent::DOM_VK_CONTROL: default: accelText += *gControlText; break; } } accelText += *gModifierSeparator; token = nsCRT::strtok(newStr, ", \t", &newStr); } nsMemory::Free(str); accelText += accelString; mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::acceltext, accelText, PR_FALSE); } void nsMenuFrame::Execute(nsGUIEvent *aEvent) { nsWeakFrame weakFrame(this); // flip "checked" state if we're a checkbox menu, or an un-checked radio menu if (mType == eMenuType_Checkbox || (mType == eMenuType_Radio && !mChecked)) { if (!mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::autocheck, nsGkAtoms::_false, eCaseMatters)) { if (mChecked) { mContent->UnsetAttr(kNameSpaceID_None, nsGkAtoms::checked, PR_TRUE); ENSURE_TRUE(weakFrame.IsAlive()); } else { mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::checked, NS_LITERAL_STRING("true"), PR_TRUE); ENSURE_TRUE(weakFrame.IsAlive()); } } } nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); if (pm && mMenuParent) pm->ExecuteMenu(mContent, aEvent); } NS_IMETHODIMP nsMenuFrame::RemoveFrame(nsIAtom* aListName, nsIFrame* aOldFrame) { nsresult rv = NS_OK; if (mPopupFrame == aOldFrame) { // Go ahead and remove this frame. mPopupFrame->Destroy(); mPopupFrame = nsnull; PresContext()->PresShell()-> FrameNeedsReflow(this, nsIPresShell::eTreeChange, NS_FRAME_HAS_DIRTY_CHILDREN); rv = NS_OK; } else { rv = nsBoxFrame::RemoveFrame(aListName, aOldFrame); } return rv; } NS_IMETHODIMP nsMenuFrame::InsertFrames(nsIAtom* aListName, nsIFrame* aPrevFrame, nsIFrame* 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); return NS_OK; } } if (!aFrameList) return NS_OK; if (NS_UNLIKELY(aPrevFrame == mPopupFrame)) { aPrevFrame = nsnull; } return nsBoxFrame::InsertFrames(aListName, aPrevFrame, aFrameList); } NS_IMETHODIMP nsMenuFrame::AppendFrames(nsIAtom* aListName, nsIFrame* aFrameList) { if (!aFrameList) return NS_OK; 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); return NS_OK; } } if (!aFrameList) return NS_OK; return nsBoxFrame::AppendFrames(aListName, aFrameList); } PRBool nsMenuFrame::SizeToPopup(nsBoxLayoutState& aState, nsSize& aSize) { if (!IsCollapsed(aState)) { nsSize tmpSize(-1, 0); nsIBox::AddCSSPrefSize(aState, this, tmpSize); if (tmpSize.width == -1 && GetFlex(aState) == 0) { if (!mPopupFrame) return PR_FALSE; tmpSize = mPopupFrame->GetPrefSize(aState); aSize.width = tmpSize.width; return PR_TRUE; } } return PR_FALSE; } nsSize nsMenuFrame::GetPrefSize(nsBoxLayoutState& aState) { nsSize size = nsBoxFrame::GetPrefSize(aState); DISPLAY_PREF_SIZE(this, size); // If we are using sizetopopup="always" then // nsBoxFrame will already have enforced the minimum size if (!IsSizedToPopup(mContent, PR_TRUE) && IsSizedToPopup(mContent, PR_FALSE) && SizeToPopup(aState, size)) { // We now need to ensure that size is within the min - max range. nsSize minSize = nsBoxFrame::GetMinSize(aState); nsSize maxSize = GetMaxSize(aState); size = BoundsCheck(minSize, size, maxSize); } return size; } NS_IMETHODIMP nsMenuFrame::GetActiveChild(nsIDOMElement** aResult) { if (!mPopupFrame) return NS_ERROR_FAILURE; nsMenuFrame* menuFrame = mPopupFrame->GetCurrentMenuItem(); if (!menuFrame) { *aResult = nsnull; } else { nsCOMPtr elt(do_QueryInterface(menuFrame->GetContent())); *aResult = elt; NS_IF_ADDREF(*aResult); } return NS_OK; } NS_IMETHODIMP nsMenuFrame::SetActiveChild(nsIDOMElement* aChild) { if (!mPopupFrame) return NS_ERROR_FAILURE; if (!aChild) { // Remove the current selection mPopupFrame->ChangeMenuItem(nsnull, PR_FALSE); return NS_OK; } nsCOMPtr child(do_QueryInterface(aChild)); nsIFrame* kid = PresContext()->PresShell()->GetPrimaryFrameFor(child); if (kid && kid->GetType() == nsGkAtoms::menuFrame) mPopupFrame->ChangeMenuItem(static_cast(kid), PR_FALSE); return NS_OK; } nsIScrollableView* nsMenuFrame::GetScrollableView() { if (!mPopupFrame) return nsnull; nsIFrame* childFrame = mPopupFrame->GetFirstChild(nsnull); if (childFrame) return mPopupFrame->GetScrollableView(childFrame); return nsnull; } // nsMenuTimerMediator implementation. NS_IMPL_ISUPPORTS1(nsMenuTimerMediator, nsITimerCallback) /** * Constructs a wrapper around an nsMenuFrame. * @param aFrame nsMenuFrame to create a wrapper around. */ nsMenuTimerMediator::nsMenuTimerMediator(nsMenuFrame *aFrame) : mFrame(aFrame) { NS_ASSERTION(mFrame, "Must have frame"); } nsMenuTimerMediator::~nsMenuTimerMediator() { } /** * Delegates the notification to the contained frame if it has not been destroyed. * @param aTimer Timer which initiated the callback. * @return NS_ERROR_FAILURE if the frame has been destroyed. */ NS_IMETHODIMP nsMenuTimerMediator::Notify(nsITimer* aTimer) { if (!mFrame) return NS_ERROR_FAILURE; return mFrame->Notify(aTimer); } /** * Clear the pointer to the contained nsMenuFrame. This should be called * when the contained nsMenuFrame is destroyed. */ void nsMenuTimerMediator::ClearFrame() { mFrame = nsnull; }