//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: // // $NoKeywords: $ //=============================================================================// #include "vgui_controls/pch_vgui_controls.h" // memdbgon must be the last include file in a .cpp file #include "tier0/memdbgon.h" #define MENU_SEPARATOR_HEIGHT 3 using namespace vgui; //----------------------------------------------------------------------------- // Purpose: divider line in a menu //----------------------------------------------------------------------------- class vgui::MenuSeparator : public Panel { public: DECLARE_CLASS_SIMPLE( MenuSeparator, Panel ); MenuSeparator( Panel *parent, char const *panelName ) : BaseClass( parent, panelName ) { SetPaintEnabled( true ); SetPaintBackgroundEnabled( true ); SetPaintBorderEnabled( false ); } virtual void Paint() { int w, h; GetSize( w, h ); surface()->DrawSetColor( GetFgColor() ); surface()->DrawFilledRect( 4, 1, w-1, 2 ); } virtual void ApplySchemeSettings( IScheme *pScheme ) { BaseClass::ApplySchemeSettings( pScheme ); SetFgColor( pScheme->GetColor( "Menu.SeparatorColor", Color( 142, 142, 142, 255 ) ) ); SetBgColor( pScheme->GetColor( "Menu.BgColor", Color( 0, 0, 0, 255 ) ) ); } }; DECLARE_BUILD_FACTORY( Menu ); //----------------------------------------------------------------------------- // Purpose: Constructor //----------------------------------------------------------------------------- Menu::Menu(Panel *parent, const char *panelName) : Panel(parent, panelName) { m_Alignment = Label::a_west; m_iFixedWidth = 0; m_iMinimumWidth = 0; m_iNumVisibleLines = -1; // No limit m_iCurrentlySelectedItemID = m_MenuItems.InvalidIndex(); m_pScroller = new ScrollBar(this, "MenuScrollBar", true); m_pScroller->SetVisible(false); m_pScroller->AddActionSignalTarget(this); _sizedForScrollBar = false; SetZPos(1); SetVisible(false); MakePopup(false); SetParent(parent); _recalculateWidth = true; m_bUseMenuManager = true; m_iInputMode = MOUSE; m_iCheckImageWidth = 0; m_iActivatedItem = 0; m_bUseFallbackFont = false; m_hFallbackItemFont = INVALID_FONT; if (IsProportional()) { m_iMenuItemHeight = scheme()->GetProportionalScaledValueEx( GetScheme(), DEFAULT_MENU_ITEM_HEIGHT ); } else { m_iMenuItemHeight = DEFAULT_MENU_ITEM_HEIGHT; } m_hItemFont = INVALID_FONT; m_eTypeAheadMode = COMPAT_MODE; m_szTypeAheadBuf[0] = '\0'; m_iNumTypeAheadChars = 0; m_fLastTypeAheadTime = 0.0f; } //----------------------------------------------------------------------------- // Purpose: Destructor //----------------------------------------------------------------------------- Menu::~Menu() { delete m_pScroller; } //----------------------------------------------------------------------------- // Purpose: Remove all menu items from the menu. //----------------------------------------------------------------------------- void Menu::DeleteAllItems() { FOR_EACH_LL( m_MenuItems, i ) { m_MenuItems[i]->MarkForDeletion(); } m_MenuItems.RemoveAll(); m_SortedItems.RemoveAll(); m_VisibleSortedItems.RemoveAll(); m_Separators.RemoveAll(); int c = m_SeparatorPanels.Count(); for ( int i = 0 ; i < c; ++i ) { m_SeparatorPanels[ i ]->MarkForDeletion(); } m_SeparatorPanels.RemoveAll(); InvalidateLayout(); } //----------------------------------------------------------------------------- // Purpose: Add a menu item to the menu. //----------------------------------------------------------------------------- int Menu::AddMenuItem( MenuItem *panel ) { panel->SetParent( this ); MEM_ALLOC_CREDIT(); int itemID = m_MenuItems.AddToTail( panel ); m_SortedItems.AddToTail(itemID); InvalidateLayout(false); _recalculateWidth = true; panel->SetContentAlignment( m_Alignment ); if ( INVALID_FONT != m_hItemFont ) { panel->SetFont( m_hItemFont ); } if ( m_bUseFallbackFont && INVALID_FONT != m_hFallbackItemFont ) { Label *l = panel; TextImage *ti = l->GetTextImage(); if ( ti ) { ti->SetUseFallbackFont( m_bUseFallbackFont, m_hFallbackItemFont ); } } if ( panel->GetHotKey() ) { SetTypeAheadMode( HOT_KEY_MODE ); } return itemID; } //----------------------------------------------------------------------------- // Remove a single item //----------------------------------------------------------------------------- void Menu::DeleteItem( int itemID ) { // FIXME: This doesn't work with separator panels yet Assert( m_SeparatorPanels.Count() == 0 ); m_MenuItems[itemID]->MarkForDeletion(); m_MenuItems.Remove( itemID ); m_SortedItems.FindAndRemove( itemID ); m_VisibleSortedItems.FindAndRemove( itemID ); InvalidateLayout(false); _recalculateWidth = true; } //----------------------------------------------------------------------------- // Purpose: Add a menu item to the menu. // Input : *item - MenuItem // *command - Command text to be sent when menu item is selected // *target - Target panel of the command // *userData - any user data associated with this menu item // Output: itemID - ID of this item //----------------------------------------------------------------------------- int Menu::AddMenuItemCharCommand(MenuItem *item, const char *command, Panel *target, const KeyValues *userData) { item->SetCommand(command); item->AddActionSignalTarget( target ); item->SetUserData(userData); return AddMenuItem( item ); } //----------------------------------------------------------------------------- // Purpose: Add a menu item to the menu. // Input : *itemName - Name of item // *itemText - Name of item text that will appear in the manu. // *message - pointer to the message to send when the item is selected // *target - Target panel of the command // *cascadeMenu - if the menu item opens a cascading menu, this is a // ptr to the menu that opens on selecting the item // Output: itemID - ID of this item //----------------------------------------------------------------------------- int Menu::AddMenuItemKeyValuesCommand( MenuItem *item, KeyValues *message, Panel *target, const KeyValues *userData ) { item->SetCommand(message); item->AddActionSignalTarget(target); item->SetUserData(userData); return AddMenuItem(item); } //----------------------------------------------------------------------------- // Purpose: Add a menu item to the menu. // Input : *itemName - Name of item // *itemText - Name of item text that will appear in the manu. // *command - Command text to be sent when menu item is selected // *target - Target panel of the command // Output: itemID - ID of this item //----------------------------------------------------------------------------- int Menu::AddMenuItem( const char *itemName, const char *itemText, const char *command, Panel *target, const KeyValues *userData ) { MenuItem *item = new MenuItem(this, itemName, itemText ); return AddMenuItemCharCommand(item, command, target, userData); } int Menu::AddMenuItem( const char *itemName, const wchar_t *wszItemText, const char *command, Panel *target, const KeyValues *userData ) { MenuItem *item = new MenuItem(this, itemName, wszItemText ); return AddMenuItemCharCommand(item, command, target, userData); } //----------------------------------------------------------------------------- // Purpose: Add a menu item to the menu. // Input : *itemText - Name of item text that will appear in the manu. // This will also be used as the name of the menu item panel. // *command - Command text to be sent when menu item is selected // *target - Target panel of the command // Output: itemID - ID of this item //----------------------------------------------------------------------------- int Menu::AddMenuItem( const char *itemText, const char *command, Panel *target, const KeyValues *userData ) { return AddMenuItem(itemText, itemText, command, target, userData ) ; } //----------------------------------------------------------------------------- // Purpose: Add a menu item to the menu. // Input : *itemName - Name of item // *itemText - Name of item text that will appear in the manu. // *message - pointer to the message to send when the item is selected // *target - Target panel of the command // *cascadeMenu - if the menu item opens a cascading menu, this is a // ptr to the menu that opens on selecting the item //----------------------------------------------------------------------------- int Menu::AddMenuItem( const char *itemName, const char *itemText, KeyValues *message, Panel *target, const KeyValues *userData ) { MenuItem *item = new MenuItem(this, itemName, itemText ); return AddMenuItemKeyValuesCommand(item, message, target, userData); } int Menu::AddMenuItem( const char *itemName, const wchar_t *wszItemText, KeyValues *message, Panel *target, const KeyValues *userData ) { MenuItem *item = new MenuItem(this, itemName, wszItemText ); return AddMenuItemKeyValuesCommand(item, message, target, userData); } //----------------------------------------------------------------------------- // Purpose: Add a menu item to the menu. // Input : *itemText - Name of item text that will appear in the manu. // This will also be used as the name of the menu item panel. // *message - pointer to the message to send when the item is selected // *target - Target panel of the command // *cascadeMenu - if the menu item opens a cascading menu, this is a // ptr to the menu that opens on selecting the item //----------------------------------------------------------------------------- int Menu::AddMenuItem( const char *itemText, KeyValues *message, Panel *target, const KeyValues *userData ) { return AddMenuItem(itemText, itemText, message, target, userData ); } //----------------------------------------------------------------------------- // Purpose: Add a menu item to the menu. // Input : *itemText - Name of item text that will appear in the manu. // This will also be the text of the command sent when the // item is selected. // *target - Target panel of the command // *cascadeMenu - if the menu item opens a cascading menu, this is a // ptr to the menu that opens on selecting the item //----------------------------------------------------------------------------- int Menu::AddMenuItem( const char *itemText, Panel *target , const KeyValues *userData ) { return AddMenuItem(itemText, itemText, target, userData ); } //----------------------------------------------------------------------------- // Purpose: Add a checkable menu item to the menu. // Input : *itemName - Name of item // *itemText - Name of item text that will appear in the manu. // *command - Command text to be sent when menu item is selected // *target - Target panel of the command //----------------------------------------------------------------------------- int Menu::AddCheckableMenuItem( const char *itemName, const char *itemText, const char *command, Panel *target, const KeyValues *userData ) { MenuItem *item = new MenuItem(this, itemName, itemText, NULL, true); return AddMenuItemCharCommand(item, command, target, userData); } int Menu::AddCheckableMenuItem( const char *itemName, const wchar_t *wszItemText, const char *command, Panel *target, const KeyValues *userData ) { MenuItem *item = new MenuItem(this, itemName, wszItemText, NULL, true); return AddMenuItemCharCommand(item, command, target, userData); } //----------------------------------------------------------------------------- // Purpose: Add a checkable menu item to the menu. // Input : *itemText - Name of item text that will appear in the manu. // This will also be used as the name of the menu item panel. // *command - Command text to be sent when menu item is selected // *target - Target panel of the command // *cascadeMenu - if the menu item opens a cascading menu, this is a // ptr to the menu that opens on selecting the item //----------------------------------------------------------------------------- int Menu::AddCheckableMenuItem( const char *itemText, const char *command, Panel *target, const KeyValues *userData ) { return AddCheckableMenuItem(itemText, itemText, command, target, userData ); } //----------------------------------------------------------------------------- // Purpose: Add a checkable menu item to the menu. // Input : *itemName - Name of item // *itemText - Name of item text that will appear in the manu. // *message - pointer to the message to send when the item is selected // *target - Target panel of the command // *cascadeMenu - if the menu item opens a cascading menu, this is a // ptr to the menu that opens on selecting the item //----------------------------------------------------------------------------- int Menu::AddCheckableMenuItem( const char *itemName, const char *itemText, KeyValues *message, Panel *target, const KeyValues *userData ) { MenuItem *item = new MenuItem(this, itemName, itemText, NULL, true); return AddMenuItemKeyValuesCommand(item, message, target, userData); } int Menu::AddCheckableMenuItem( const char *itemName, const wchar_t *wszItemText, KeyValues *message, Panel *target, const KeyValues *userData ) { MenuItem *item = new MenuItem(this, itemName, wszItemText, NULL, true); return AddMenuItemKeyValuesCommand(item, message, target, userData); } //----------------------------------------------------------------------------- // Purpose: Add a checkable menu item to the menu. // Input : *itemText - Name of item text that will appear in the manu. // This will also be used as the name of the menu item panel. // *message - pointer to the message to send when the item is selected // *target - Target panel of the command // *cascadeMenu - if the menu item opens a cascading menu, this is a // ptr to the menu that opens on selecting the item //----------------------------------------------------------------------------- int Menu::AddCheckableMenuItem( const char *itemText, KeyValues *message, Panel *target, const KeyValues *userData ) { return AddCheckableMenuItem(itemText, itemText, message, target, userData ); } //----------------------------------------------------------------------------- // Purpose: Add a checkable menu item to the menu. // Input : *itemText - Name of item text that will appear in the manu. // This will also be the text of the command sent when the // item is selected. // *target - Target panel of the command // *cascadeMenu - if the menu item opens a cascading menu, this is a // ptr to the menu that opens on selecting the item //----------------------------------------------------------------------------- int Menu::AddCheckableMenuItem( const char *itemText, Panel *target, const KeyValues *userData ) { return AddCheckableMenuItem(itemText, itemText, target, userData ); } //----------------------------------------------------------------------------- // Purpose: Add a Cascading menu item to the menu. // Input : *itemName - Name of item // *itemText - Name of item text that will appear in the manu. // *command - Command text to be sent when menu item is selected // *target - Target panel of the command // *cascadeMenu - if the menu item opens a cascading menu, this is a // ptr to the menu that opens on selecting the item //----------------------------------------------------------------------------- int Menu::AddCascadingMenuItem( const char *itemName, const char *itemText, const char *command, Panel *target, Menu *cascadeMenu , const KeyValues *userData ) { MenuItem *item = new MenuItem(this, itemName, itemText, cascadeMenu ); return AddMenuItemCharCommand(item, command, target, userData); } int Menu::AddCascadingMenuItem( const char *itemName, const wchar_t *wszItemText, const char *command, Panel *target, Menu *cascadeMenu , const KeyValues *userData ) { MenuItem *item = new MenuItem(this, itemName, wszItemText, cascadeMenu ); return AddMenuItemCharCommand(item, command, target, userData); } //----------------------------------------------------------------------------- // Purpose: Add a Cascading menu item to the menu. // Input : *itemText - Name of item text that will appear in the manu. // This will also be used as the name of the menu item panel. // *command - Command text to be sent when menu item is selected // *target - Target panel of the command // *cascadeMenu - if the menu item opens a cascading menu, this is a // ptr to the menu that opens on selecting the item //----------------------------------------------------------------------------- int Menu::AddCascadingMenuItem( const char *itemText, const char *command, Panel *target, Menu *cascadeMenu , const KeyValues *userData ) { return AddCascadingMenuItem( itemText, itemText, command, target, cascadeMenu, userData ); } //----------------------------------------------------------------------------- // Purpose: Add a Cascading menu item to the menu. // Input : *itemName - Name of item // *itemText - Name of item text that will appear in the manu. // *message - pointer to the message to send when the item is selected // *target - Target panel of the command // *cascadeMenu - if the menu item opens a cascading menu, this is a // ptr to the menu that opens on selecting the item //----------------------------------------------------------------------------- int Menu::AddCascadingMenuItem( const char *itemName, const char *itemText, KeyValues *message, Panel *target, Menu *cascadeMenu, const KeyValues *userData ) { MenuItem *item = new MenuItem( this, itemName, itemText, cascadeMenu); return AddMenuItemKeyValuesCommand(item, message, target, userData); } int Menu::AddCascadingMenuItem( const char *itemName, const wchar_t *wszItemText, KeyValues *message, Panel *target, Menu *cascadeMenu, const KeyValues *userData ) { MenuItem *item = new MenuItem( this, itemName, wszItemText, cascadeMenu); return AddMenuItemKeyValuesCommand(item, message, target, userData); } //----------------------------------------------------------------------------- // Purpose: Add a Cascading menu item to the menu. // Input : *itemText - Name of item text that will appear in the manu. // This will also be used as the name of the menu item panel. // *message - pointer to the message to send when the item is selected // *target - Target panel of the command // *cascadeMenu - if the menu item opens a cascading menu, this is a // ptr to the menu that opens on selecting the item //----------------------------------------------------------------------------- int Menu::AddCascadingMenuItem( const char *itemText, KeyValues *message, Panel *target, Menu *cascadeMenu, const KeyValues *userData ) { return AddCascadingMenuItem(itemText, itemText, message, target, cascadeMenu, userData ); } //----------------------------------------------------------------------------- // Purpose: Add a Cascading menu item to the menu. // Input : *itemText - Name of item text that will appear in the manu. // This will also be the text of the command sent when the // item is selected. // *target - Target panel of the command // *cascadeMenu - if the menu item opens a cascading menu, this is a // ptr to the menu that opens on selecting the item //----------------------------------------------------------------------------- int Menu::AddCascadingMenuItem( const char *itemText, Panel *target, Menu *cascadeMenu, const KeyValues *userData ) { return AddCascadingMenuItem(itemText, itemText, target, cascadeMenu, userData); } //----------------------------------------------------------------------------- // Purpose: Sets the values of a menu item at the specified index // Input : index - the index of this item entry // *message - pointer to the message to send when the item is selected //----------------------------------------------------------------------------- void Menu::UpdateMenuItem(int itemID, const char *itemText, KeyValues *message, const KeyValues *userData) { Assert( m_MenuItems.IsValidIndex(itemID) ); if ( m_MenuItems.IsValidIndex(itemID) ) { MenuItem *menuItem = dynamic_cast(m_MenuItems[itemID]); // make sure its enabled since disabled items get highlighted. if (menuItem) { menuItem->SetText(itemText); menuItem->SetCommand(message); if(userData) { menuItem->SetUserData(userData); } } } _recalculateWidth = true; } //----------------------------------------------------------------------------- // Purpose: Sets the values of a menu item at the specified index //----------------------------------------------------------------------------- void Menu::UpdateMenuItem(int itemID, const wchar_t *wszItemText, KeyValues *message, const KeyValues *userData) { Assert( m_MenuItems.IsValidIndex(itemID) ); if ( m_MenuItems.IsValidIndex(itemID) ) { MenuItem *menuItem = dynamic_cast(m_MenuItems[itemID]); // make sure its enabled since disabled items get highlighted. if (menuItem) { menuItem->SetText(wszItemText); menuItem->SetCommand(message); if(userData) { menuItem->SetUserData(userData); } } } _recalculateWidth = true; } //----------------------------------------------------------------------------- // Sets the content alignment of all items in the menu //----------------------------------------------------------------------------- void Menu::SetContentAlignment( Label::Alignment alignment ) { if ( m_Alignment != alignment ) { m_Alignment = alignment; // Change the alignment of existing menu items int nCount = m_MenuItems.Count(); for ( int i = 0; i < nCount; ++i ) { m_MenuItems[i]->SetContentAlignment( alignment ); } } } //----------------------------------------------------------------------------- // Purpose: Locks down a specific width //----------------------------------------------------------------------------- void Menu::SetFixedWidth(int width) { // the padding makes it so the menu has the label padding on each side of the menu. // makes the menu items look centered. m_iFixedWidth = width; InvalidateLayout(false); } //----------------------------------------------------------------------------- // Purpose: sets the height of each menu item //----------------------------------------------------------------------------- void Menu::SetMenuItemHeight(int itemHeight) { m_iMenuItemHeight = itemHeight; } int Menu::GetMenuItemHeight() const { return m_iMenuItemHeight; } int Menu::CountVisibleItems() { int count = 0; int c = m_SortedItems.Count(); for ( int i = 0 ; i < c; ++i ) { if ( m_MenuItems[ m_SortedItems[ i ] ]->IsVisible() ) ++count; } return count; } void Menu::ComputeWorkspaceSize( int& workWide, int& workTall ) { // make sure we factor in insets int ileft, iright, itop, ibottom; GetInset(ileft, iright, itop, ibottom); int workX, workY; surface()->GetWorkspaceBounds(workX, workY, workWide, workTall); workTall -= 20; workTall -= itop; workTall -= ibottom; } // Assumes relative coords in screenspace void Menu::PositionRelativeToPanel( Panel *relative, MenuDirection_e direction, int nAdditionalYOffset /*=0*/, bool showMenu /*=false*/ ) { Assert( relative ); int rx, ry, rw, rh; relative->GetBounds( rx, ry, rw, rh ); relative->LocalToScreen( rx, ry ); if ( direction == CURSOR ) { // force the menu to appear where the mouse button was pressed input()->GetCursorPos(rx, ry); rw = rh = 0; } else if ( direction == ALIGN_WITH_PARENT && relative->GetVParent() ) { rx = 0, ry = 0; relative->ParentLocalToScreen(rx, ry); rx -= 1; // take border into account ry += rh + nAdditionalYOffset; rw = rh = 0; } else { rx = 0, ry = 0; relative->LocalToScreen(rx, ry); } int workWide, workTall; ComputeWorkspaceSize( workWide, workTall ); // Final pos int x = 0, y = 0; int mWide, mTall; GetSize( mWide, mTall ); switch( direction ) { case Menu::UP: // Menu prefers to open upward { x = rx; int topOfReference = ry; y = topOfReference - mTall; if ( y < 0 ) { int bottomOfReference = ry + rh + 1; int remainingPixels = workTall - bottomOfReference; // Can't fit on bottom, either, move to side if ( mTall >= remainingPixels ) { y = workTall - mTall; x = rx + rw; // Try and place it to the left of the button if ( x + mWide > workWide ) { x = rx - mWide; } } else { // Room at bottom y = bottomOfReference; } } } break; // Everyone else aligns downward... default: case Menu::LEFT: case Menu::RIGHT: case Menu::DOWN: { x = rx; int bottomOfReference = ry + rh + 1; y = bottomOfReference; if ( bottomOfReference + mTall >= workTall ) { // See if there's run straight above if ( mTall >= ry ) // No room, try and push menu to right or left { y = workTall - mTall; x = rx + rw; // Try and place it to the left of the button if ( x + mWide > workWide ) { x = rx - mWide; } } else { // Room at top y = ry - mTall; } } } break; } // Check left rightness if ( x + mWide > workWide ) { x = workWide - mWide; Assert( x >= 0 ); // yikes!!! } else if ( x < 0 ) { x = 0; } SetPos( x, y ); if ( showMenu ) { SetVisible( true ); } } int Menu::ComputeFullMenuHeightWithInsets() { // make sure we factor in insets int ileft, iright, itop, ibottom; GetInset(ileft, iright, itop, ibottom); int separatorHeight = 3; // add up the size of all the child panels // move the child panels to the correct place in the menu int totalTall = itop + ibottom; int i; for ( i = 0 ; i < m_SortedItems.Count() ; i++ ) // use sortedItems instead of MenuItems due to SetPos() { int itemId = m_SortedItems[i]; MenuItem *child = m_MenuItems[ itemId ]; Assert( child ); if ( !child ) continue; // These should all be visible at this point if ( !child->IsVisible() ) continue; totalTall += m_iMenuItemHeight; // Add a separator if needed... int sepIndex = m_Separators.Find( itemId ); if ( sepIndex != m_Separators.InvalidIndex() ) { totalTall += separatorHeight; } } return totalTall; } //----------------------------------------------------------------------------- // Purpose: Reformat according to the new layout //----------------------------------------------------------------------------- void Menu::PerformLayout() { MenuItem *parent = GetParentMenuItem(); bool cascading = parent != NULL ? true : false; // make sure we factor in insets int ileft, iright, itop, ibottom; GetInset(ileft, iright, itop, ibottom); int workWide, workTall; ComputeWorkspaceSize( workWide, workTall ); int fullHeightWouldRequire = ComputeFullMenuHeightWithInsets(); bool bNeedScrollbar = fullHeightWouldRequire >= workTall; int maxVisibleItems = CountVisibleItems(); if ( m_iNumVisibleLines > 0 && maxVisibleItems > m_iNumVisibleLines ) { bNeedScrollbar = true; maxVisibleItems = m_iNumVisibleLines; } // if we have a scroll bar if ( bNeedScrollbar ) { // add it to the display AddScrollBar(); // This fills in m_VisibleSortedItems as needed MakeItemsVisibleInScrollRange( m_iNumVisibleLines, min( fullHeightWouldRequire, workTall ) ); } else { RemoveScrollBar(); // Make everything visible m_VisibleSortedItems.RemoveAll(); int i; int c = m_SortedItems.Count(); for ( i = 0; i < c; ++i ) { int itemID = m_SortedItems[ i ]; MenuItem *child = m_MenuItems[ itemID ]; if ( !child || !child->IsVisible() ) continue; m_VisibleSortedItems.AddToTail( itemID ); } // Hide the separators, the needed ones will be readded below c = m_SeparatorPanels.Count(); for ( i = 0; i < c; ++i ) { if ( m_SeparatorPanels[ i ] ) { m_SeparatorPanels[ i ]->SetVisible( false ); } } } // get the appropriate menu border LayoutMenuBorder(); int trueW = GetWide(); if ( bNeedScrollbar ) { trueW -= m_pScroller->GetWide(); } int separatorHeight = MENU_SEPARATOR_HEIGHT; // add up the size of all the child panels // move the child panels to the correct place in the menu int menuTall = 0; int totalTall = itop + ibottom; int i; for ( i = 0 ; i < m_VisibleSortedItems.Count() ; i++ ) // use sortedItems instead of MenuItems due to SetPos() { int itemId = m_VisibleSortedItems[i]; MenuItem *child = m_MenuItems[ itemId ]; Assert( child ); if ( !child ) continue; // These should all be visible at this point if ( !child->IsVisible() ) continue; if ( totalTall >= workTall ) break; if ( INVALID_FONT != m_hItemFont ) { child->SetFont( m_hItemFont ); } // take into account inset child->SetPos (0, menuTall); child->SetTall( m_iMenuItemHeight ); // Width is set in a second pass menuTall += m_iMenuItemHeight; totalTall += m_iMenuItemHeight; // this will make all the menuitems line up in a column with space for the checks to the left. if ( ( !child->IsCheckable() ) && ( m_iCheckImageWidth > 0 ) ) { // Non checkable items have to move over child->SetTextInset( m_iCheckImageWidth, 0 ); } else if ( child->IsCheckable() ) { child->SetTextInset(0, 0); //TODO: for some reason I can't comment this out. } // Add a separator if needed... int sepIndex = m_Separators.Find( itemId ); if ( sepIndex != m_Separators.InvalidIndex() ) { MenuSeparator *sep = m_SeparatorPanels[ sepIndex ]; Assert( sep ); sep->SetVisible( true ); sep->SetBounds( 0, menuTall, trueW, separatorHeight ); menuTall += separatorHeight; totalTall += separatorHeight; } } if (!m_iFixedWidth) { _recalculateWidth = true; CalculateWidth(); } else if (m_iFixedWidth) { _menuWide = m_iFixedWidth; // fixed width menus include the scroll bar in their width. if (_sizedForScrollBar) { _menuWide -= m_pScroller->GetWide(); } } SizeMenuItems(); int extraWidth = 0; if (_sizedForScrollBar) { extraWidth = m_pScroller->GetWide(); } int mwide = _menuWide + extraWidth; if ( mwide > workWide ) { mwide = workWide; } int mtall = menuTall + itop + ibottom; if ( mtall > workTall ) { // Shouldn't happen mtall = workTall; } // set the new size of the menu SetSize( mwide, mtall ); // move the menu to the correct position if it is a cascading menu. if ( cascading ) { // move the menu to the correct position if it is a cascading menu. PositionCascadingMenu(); } // set up scroll bar as appropriate if ( m_pScroller->IsVisible() ) { LayoutScrollBar(); } FOR_EACH_LL( m_MenuItems, j ) { m_MenuItems[j]->InvalidateLayout(); // cause each menu item to redo its apply settings now we have sized ourselves } Repaint(); } //----------------------------------------------------------------------------- // Purpose: Force the menu to work out how wide it should be //----------------------------------------------------------------------------- void Menu::ForceCalculateWidth() { _recalculateWidth = true; CalculateWidth(); PerformLayout(); } //----------------------------------------------------------------------------- // Purpose: Figure out how wide the menu should be if the menu is not fixed width //----------------------------------------------------------------------------- void Menu::CalculateWidth() { if (!_recalculateWidth) return; _menuWide = 0; if (!m_iFixedWidth) { // find the biggest menu item FOR_EACH_LL( m_MenuItems, i ) { int wide, tall; m_MenuItems[i]->GetContentSize(wide, tall); if (wide > _menuWide - Label::Content) { _menuWide = wide + Label::Content; } } } // enfoce a minimumWidth if (_menuWide < m_iMinimumWidth) { _menuWide = m_iMinimumWidth; } _recalculateWidth = false; } //----------------------------------------------------------------------------- // Purpose: Set up the scroll bar attributes,size and location. //----------------------------------------------------------------------------- void Menu::LayoutScrollBar() { //!! need to make it recalculate scroll positions m_pScroller->SetEnabled(false); m_pScroller->SetRangeWindow( m_VisibleSortedItems.Count() ); m_pScroller->SetRange( 0, CountVisibleItems() ); m_pScroller->SetButtonPressedScrollValue( 1 ); int wide, tall; GetSize (wide, tall); // make sure we factor in insets int ileft, iright, itop, ibottom; GetInset(ileft, iright, itop, ibottom); // with a scroll bar we take off the inset wide -= iright; m_pScroller->SetPos(wide - m_pScroller->GetWide(), 1); // scrollbar is inside the menu's borders. m_pScroller->SetSize(m_pScroller->GetWide(), tall - ibottom - itop); } //----------------------------------------------------------------------------- // Purpose: Figure out where to open menu if it is a cascading menu //----------------------------------------------------------------------------- void Menu::PositionCascadingMenu() { Assert(GetVParent()); int parentX, parentY, parentWide, parentTall; // move the menu to the correct place below the menuItem ipanel()->GetSize(GetVParent(), parentWide, parentTall); ipanel()->GetPos(GetVParent(), parentX, parentY); parentX += parentWide, parentY = 0; ParentLocalToScreen(parentX, parentY); SetPos(parentX, parentY); // for cascading menus, // make sure we're on the screen int workX, workY, workWide, workTall, x, y, wide, tall; GetBounds(x, y, wide, tall); surface()->GetWorkspaceBounds(workX, workY, workWide, workTall); if (x + wide > workX + workWide) { // we're off the right, move the menu to the left side // orignalX - width of the parentmenuitem - width of this menu. // add 2 pixels to offset one pixel onto the parent menu. x -= (parentWide + wide); x -= 2; } else { // alignment move it in the amount of the insets. x += 1; } if ( y + tall > workY + workTall ) { int lastWorkY = workY + workTall; int pixelsOffBottom = ( y + tall ) - lastWorkY; y -= pixelsOffBottom; y -= 2; } else { y -= 1; } SetPos(x, y); MoveToFront(); } //----------------------------------------------------------------------------- // Purpose: Size the menu items so they are the width of the menu. // Also size the menu items with cascading menus so the arrow fits in there. //----------------------------------------------------------------------------- void Menu::SizeMenuItems() { int ileft, iright, itop, ibottom; GetInset(ileft, iright, itop, ibottom); // assign the sizes of all the menu item panels FOR_EACH_LL( m_MenuItems, i ) { MenuItem *child = m_MenuItems[i]; if (child ) { // labels do thier own sizing. this will size the label to the width of the menu, // this will put the cascading menu arrow on the right side automatically. child->SetWide(_menuWide - ileft - iright); } } } //----------------------------------------------------------------------------- // Purpose: Makes menu items visible in relation to where the scroll bar is //----------------------------------------------------------------------------- void Menu::MakeItemsVisibleInScrollRange( int maxVisibleItems, int nNumPixelsAvailable ) { // Detach all items from tree int i; FOR_EACH_LL( m_MenuItems, item ) { m_MenuItems[ item ]->SetBounds( 0, 0, 0, 0 ); } for ( i = 0; i < m_SeparatorPanels.Count(); ++i ) { m_SeparatorPanels[ i ]->SetVisible( false ); } m_VisibleSortedItems.RemoveAll(); int tall = 0; int startItem = m_pScroller->GetValue(); Assert( startItem >= 0 ); do { if ( startItem >= m_SortedItems.Count() ) break; int itemId = m_SortedItems[ startItem ]; if ( !m_MenuItems[ itemId ]->IsVisible() ) { ++startItem; continue; } int itemHeight = m_iMenuItemHeight; int sepIndex = m_Separators.Find( itemId ); if ( sepIndex != m_Separators.InvalidIndex() ) { itemHeight += MENU_SEPARATOR_HEIGHT; } if ( tall + itemHeight > nNumPixelsAvailable ) break; // Too many items if ( maxVisibleItems > 0 ) { if ( m_VisibleSortedItems.Count() >= maxVisibleItems ) break; } tall += itemHeight; // Re-attach this one m_VisibleSortedItems.AddToTail( itemId ); ++startItem; } while ( true ); } //----------------------------------------------------------------------------- // Purpose: Get the approproate menu border //----------------------------------------------------------------------------- void Menu::LayoutMenuBorder() { IBorder *menuBorder; IScheme *pScheme = scheme()->GetIScheme( GetScheme() ); menuBorder = pScheme->GetBorder("MenuBorder"); if ( menuBorder ) { SetBorder(menuBorder); } } //----------------------------------------------------------------------------- // Purpose: Draw a black border on the right side of the menu items //----------------------------------------------------------------------------- void Menu::Paint() { if ( m_pScroller->IsVisible() ) { // draw black bar int wide, tall; GetSize (wide, tall); surface()->DrawSetColor(_borderDark); if( IsProportional() ) { surface()->DrawFilledRect(wide - m_pScroller->GetWide(), -1, wide - m_pScroller->GetWide() + 1, tall); } else { surface()->DrawFilledRect(wide - m_pScroller->GetWide(), -1, wide - m_pScroller->GetWide() + 1, tall); } } } //----------------------------------------------------------------------------- // Purpose: sets the max number of items visible (scrollbar appears with more) // Input : numItems - //----------------------------------------------------------------------------- void Menu::SetNumberOfVisibleItems( int numItems ) { m_iNumVisibleLines = numItems; InvalidateLayout(false); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void Menu::EnableUseMenuManager( bool bUseMenuManager ) { m_bUseMenuManager = bUseMenuManager; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- MenuItem *Menu::GetMenuItem(int itemID) { if ( !m_MenuItems.IsValidIndex(itemID) ) return NULL; return m_MenuItems[itemID]; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool Menu::IsValidMenuID(int itemID) { return m_MenuItems.IsValidIndex(itemID); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int Menu::GetInvalidMenuID() { return m_MenuItems.InvalidIndex(); } //----------------------------------------------------------------------------- // Purpose: When a menuItem is selected, close cascading menus // if the menuItem selected has a cascading menu attached, we // want to keep that one open so skip it. // Passing NULL will close all cascading menus. //----------------------------------------------------------------------------- void Menu::CloseOtherMenus(MenuItem *item) { FOR_EACH_LL( m_MenuItems, i ) { if (m_MenuItems[i] == item) continue; m_MenuItems[i]->CloseCascadeMenu(); } } //----------------------------------------------------------------------------- // Purpose: Respond to string commands. //----------------------------------------------------------------------------- void Menu::OnCommand( const char *command ) { // forward on the message PostActionSignal(new KeyValues("Command", "command", command)); Panel::OnCommand(command); } //----------------------------------------------------------------------------- // Purpose: Handle key presses, Activate shortcuts //----------------------------------------------------------------------------- void Menu::OnKeyCodeTyped(KeyCode keycode) { vgui::KeyCode code = GetBaseButtonCode( keycode ); // Don't allow key inputs when disabled! if ( !IsEnabled() ) return; bool alt = (input()->IsKeyDown(KEY_LALT) || input()->IsKeyDown(KEY_RALT)); if (alt) { BaseClass::OnKeyCodeTyped( keycode ); // Ignore alt when in combobox mode if (m_eTypeAheadMode != TYPE_AHEAD_MODE) { PostActionSignal(new KeyValues("MenuClose")); } } switch (code) { case KEY_ESCAPE: case KEY_XBUTTON_B: case STEAMCONTROLLER_B: { // hide the menu on ESC SetVisible(false); break; } // arrow keys scroll through items on the list. // they should also scroll the scroll bar if needed case KEY_UP: case KEY_XBUTTON_UP: case KEY_XSTICK1_UP: case STEAMCONTROLLER_DPAD_UP: { MoveAlongMenuItemList(MENU_UP, 0); if ( m_MenuItems.IsValidIndex( m_iCurrentlySelectedItemID ) ) { m_MenuItems[m_iCurrentlySelectedItemID]->ArmItem(); } else { BaseClass::OnKeyCodeTyped( keycode ); // chain up } break; } case KEY_DOWN: case KEY_XBUTTON_DOWN: case KEY_XSTICK1_DOWN: case STEAMCONTROLLER_DPAD_DOWN: { MoveAlongMenuItemList(MENU_DOWN, 0); if ( m_MenuItems.IsValidIndex( m_iCurrentlySelectedItemID ) ) { m_MenuItems[m_iCurrentlySelectedItemID]->ArmItem(); } else { BaseClass::OnKeyCodeTyped( keycode ); // chain up } break; } // for now left and right arrows just open or close submenus if they are there. case KEY_RIGHT: case KEY_XBUTTON_RIGHT: case KEY_XSTICK1_RIGHT: case STEAMCONTROLLER_DPAD_RIGHT: { // make sure a menuItem is currently selected if ( m_MenuItems.IsValidIndex(m_iCurrentlySelectedItemID) ) { if (m_MenuItems[m_iCurrentlySelectedItemID]->HasMenu()) { ActivateItem(m_iCurrentlySelectedItemID); } else { BaseClass::OnKeyCodeTyped( keycode ); } } else { BaseClass::OnKeyCodeTyped( keycode ); } break; } case KEY_LEFT: case KEY_XBUTTON_LEFT: case KEY_XSTICK1_LEFT: case STEAMCONTROLLER_DPAD_LEFT: { // if our parent is a menu item then we are a submenu so close us. if (GetParentMenuItem()) { SetVisible(false); } else { BaseClass::OnKeyCodeTyped( keycode ); } break; } case KEY_ENTER: case KEY_XBUTTON_A: case STEAMCONTROLLER_A: { // make sure a menuItem is currently selected if ( m_MenuItems.IsValidIndex(m_iCurrentlySelectedItemID) ) { ActivateItem(m_iCurrentlySelectedItemID); } else { BaseClass::OnKeyCodeTyped( keycode ); // chain up } break; } case KEY_PAGEUP: { if ( m_iNumVisibleLines > 1 ) { if ( m_iCurrentlySelectedItemID < m_iNumVisibleLines ) { MoveAlongMenuItemList( MENU_UP * m_iCurrentlySelectedItemID, 0 ); } else { MoveAlongMenuItemList(MENU_UP * m_iNumVisibleLines - 1, 0); } } else { MoveAlongMenuItemList(MENU_UP, 0); } if ( m_MenuItems.IsValidIndex( m_iCurrentlySelectedItemID ) ) { m_MenuItems[m_iCurrentlySelectedItemID]->ArmItem(); } break; } case KEY_PAGEDOWN: { if ( m_iNumVisibleLines > 1 ) { if ( m_iCurrentlySelectedItemID + m_iNumVisibleLines >= GetItemCount() ) { MoveAlongMenuItemList(MENU_DOWN * ( GetItemCount() - m_iCurrentlySelectedItemID - 1), 0); } else { MoveAlongMenuItemList(MENU_DOWN * m_iNumVisibleLines - 1, 0); } } else { MoveAlongMenuItemList(MENU_DOWN, 0); } if ( m_MenuItems.IsValidIndex( m_iCurrentlySelectedItemID ) ) { m_MenuItems[m_iCurrentlySelectedItemID]->ArmItem(); } break; } case KEY_HOME: { MoveAlongMenuItemList( MENU_UP * m_iCurrentlySelectedItemID, 0 ); if ( m_MenuItems.IsValidIndex( m_iCurrentlySelectedItemID ) ) { m_MenuItems[m_iCurrentlySelectedItemID]->ArmItem(); } break; } case KEY_END: { MoveAlongMenuItemList(MENU_DOWN * ( GetItemCount() - m_iCurrentlySelectedItemID - 1), 0); if ( m_MenuItems.IsValidIndex( m_iCurrentlySelectedItemID ) ) { m_MenuItems[m_iCurrentlySelectedItemID]->ArmItem(); } break; } } // don't chain back } void Menu::OnHotKey(wchar_t unichar) { // iterate the menu items looking for one with the matching hotkey FOR_EACH_LL( m_MenuItems, i ) { MenuItem *panel = m_MenuItems[i]; if (panel->IsVisible()) { Panel *hot = panel->HasHotkey(unichar); if (hot) { // post a message to the menuitem telling it it's hotkey was pressed PostMessage(hot, new KeyValues("Hotkey")); return; } // if the menuitem is a cascading menuitem and it is open, check its hotkeys too Menu *cascadingMenu = panel->GetMenu(); if (cascadingMenu && cascadingMenu->IsVisible()) { cascadingMenu->OnKeyTyped(unichar); } } } } void Menu::OnTypeAhead(wchar_t unichar) { // Don't do anything if the menu is empty since there cannot be a selected item. if ( m_MenuItems.Count() <= 0) return; // expire the type ahead buffer after 0.5 seconds double tCurrentTime = Sys_FloatTime(); if ( (tCurrentTime - m_fLastTypeAheadTime) > 0.5f ) { m_iNumTypeAheadChars = 0; m_szTypeAheadBuf[0] = '\0'; } m_fLastTypeAheadTime = tCurrentTime; // add current character to the type ahead buffer if ( m_iNumTypeAheadChars+1 < TYPEAHEAD_BUFSIZE ) { m_szTypeAheadBuf[m_iNumTypeAheadChars++] = unichar; } int itemToSelect = m_iCurrentlySelectedItemID; if ( itemToSelect < 0 || itemToSelect >= m_MenuItems.Count()) { itemToSelect = 0; } int i = itemToSelect; do { wchar_t menuItemName[255]; m_MenuItems[i]->GetText(menuItemName, 254); // This is supposed to be case insensitive but we don't have a portable case // insensitive wide-character routine. if ( wcsncmp( m_szTypeAheadBuf, menuItemName, m_iNumTypeAheadChars) == 0 ) { itemToSelect = i; break; } i = (i+1) % m_MenuItems.Count(); } while ( i != itemToSelect ); if ( itemToSelect >= 0 ) { SetCurrentlyHighlightedItem( itemToSelect ); InvalidateLayout(); } } //----------------------------------------------------------------------------- // Purpose: Handle key presses, Activate shortcuts // Input : code - //----------------------------------------------------------------------------- void Menu::OnKeyTyped(wchar_t unichar) { if (! unichar) { return; } switch( m_eTypeAheadMode ) { case HOT_KEY_MODE: OnHotKey(unichar); return; case TYPE_AHEAD_MODE: OnTypeAhead(unichar); return; case COMPAT_MODE: default: break; } int itemToSelect = m_iCurrentlySelectedItemID; if ( itemToSelect < 0 ) { itemToSelect = 0; } int i; wchar_t menuItemName[255]; i = itemToSelect + 1; if ( i >= m_MenuItems.Count() ) { i = 0; } while ( i != itemToSelect ) { m_MenuItems[i]->GetText(menuItemName, 254); if ( tolower( unichar ) == tolower( menuItemName[0] ) ) { itemToSelect = i; break; } i++; if ( i >= m_MenuItems.Count() ) { i = 0; } } if ( itemToSelect >= 0 ) { SetCurrentlyHighlightedItem( itemToSelect ); InvalidateLayout(); } // don't chain back } void Menu::SetTypeAheadMode(MenuTypeAheadMode mode) { m_eTypeAheadMode = mode; } int Menu::GetTypeAheadMode() { return m_eTypeAheadMode; } //----------------------------------------------------------------------------- // Purpose: Handle the mouse wheel event, scroll the selection //----------------------------------------------------------------------------- void Menu::OnMouseWheeled(int delta) { if (!m_pScroller->IsVisible()) return; int val = m_pScroller->GetValue(); val -= delta; m_pScroller->SetValue(val); // moving the slider redraws the scrollbar, // and so we should redraw the menu since the // menu draws the black border to the right of the scrollbar. InvalidateLayout(); // don't chain back } //----------------------------------------------------------------------------- // Purpose: Lose focus, hide menu //----------------------------------------------------------------------------- void Menu::OnKillFocus() { // check to see if it's a child taking it if (!input()->GetFocus() || !ipanel()->HasParent(input()->GetFocus(), GetVPanel())) { // if we don't accept keyboard input, then we have to ignore the killfocus if it's not actually being stolen if (!IsKeyBoardInputEnabled() && !input()->GetFocus()) return; // get the parent of this menu. MenuItem *item = GetParentMenuItem(); // if the parent is a menu item, this menu is a cascading menu // if the panel that is getting focus is the parent menu, don't close this menu. if ( (item) && (input()->GetFocus() == item->GetVParent()) ) { // if we are in mouse mode and we clicked on the menuitem that // triggers the cascading menu, leave it open. if (m_iInputMode == MOUSE) { // return the focus to the cascading menu. MoveToFront(); return; } } // forward the message to the parent. PostActionSignal(new KeyValues("MenuClose")); // hide this menu SetVisible(false); } } namespace vgui { class CMenuManager { public: void AddMenu( Menu *m ) { if ( !m ) return; int c = m_Menus.Count(); for ( int i = 0 ; i < c; ++i ) { if ( m_Menus[ i ].Get() == m ) return; } DHANDLE< Menu > h; h = m; m_Menus.AddToTail( h ); } void RemoveMenu( Menu *m ) { if ( !m ) return; int c = m_Menus.Count(); for ( int i = c - 1 ; i >= 0; --i ) { if ( m_Menus[ i ].Get() == m ) { m_Menus.Remove( i ); return; } } } void OnInternalMousePressed( Panel *other, MouseCode code ) { int c = m_Menus.Count(); if ( !c ) return; int x, y; input()->GetCursorPos( x, y ); bool mouseInsideMenuRelatedPanel = false; for ( int i = c - 1; i >= 0 ; --i ) { Menu *m = m_Menus[ i ].Get(); if ( !m ) { m_Menus.Remove( i ); continue; } // See if the mouse is within a menu if ( IsWithinMenuOrRelative( m, x, y ) ) { mouseInsideMenuRelatedPanel = true; } } if ( mouseInsideMenuRelatedPanel ) { return; } AbortMenus(); } void AbortMenus() { // Close all of the menus int c = m_Menus.Count(); for ( int i = c - 1; i >= 0 ; --i ) { Menu *m = m_Menus[ i ].Get(); if ( !m ) { continue; } m_Menus.Remove( i ); // Force it to close m->SetVisible( false ); } m_Menus.RemoveAll(); } bool IsWithinMenuOrRelative( Panel *panel, int x, int y ) { VPANEL topMost = panel->IsWithinTraverse( x, y, true ); if ( topMost ) { // It's over the menu if ( topMost == panel->GetVPanel() ) { return true; } // It's over something which is parented to the menu (i.e., a menu item) if ( ipanel()->HasParent( topMost, panel->GetVPanel() ) ) { return true; } } if ( panel->GetParent() ) { Panel *parent = panel->GetParent(); topMost = parent->IsWithinTraverse( x, y, true ); if ( topMost ) { if ( topMost == parent->GetVPanel() ) { return true; } /* // NOTE: this check used to not cast to MenuButton, but it seems wrong to me // since if the mouse is over another child of the parent panel to the menu then // the menu stays visible. I think this is bogus. Panel *pTopMost = ipanel()->GetPanel(topMost, GetControlsModuleName()); if ( pTopMost && ipanel()->HasParent( topMost, parent->GetVPanel() ) && dynamic_cast< MenuButton * >( pTopMost ) ) { Msg( "topMost %s has parent %s\n", ipanel()->GetName( topMost ), parent->GetName() ); return true; } */ } } return false; } #ifdef DBGFLAG_VALIDATE void Validate( CValidator &validator, char *pchName ) { validator.Push( "CMenuManager", this, pchName ); m_Menus.Validate( validator, "m_Menus" ); validator.Pop(); } #endif private: // List of visible menus CUtlVector< DHANDLE< Menu > > m_Menus; }; // Singleton helper class static CMenuManager g_MenuMgr; void ValidateMenuGlobals( CValidator &validator ) { #ifdef DBGFLAG_VALIDATE g_MenuMgr.Validate( validator, "g_MenuMgr" ); #endif } } // end namespace vgui //----------------------------------------------------------------------------- // Purpose: Static method called on mouse released to see if Menu objects should be aborted // Input : *other - // code - //----------------------------------------------------------------------------- void Menu::OnInternalMousePressed( Panel *other, MouseCode code ) { g_MenuMgr.OnInternalMousePressed( other, code ); } //----------------------------------------------------------------------------- // Purpose: Set visibility of menu and its children as appropriate. //----------------------------------------------------------------------------- void Menu::SetVisible(bool state) { if (state == IsVisible()) return; if ( state == false ) { PostActionSignal(new KeyValues("MenuClose")); CloseOtherMenus(NULL); SetCurrentlySelectedItem(-1); g_MenuMgr.RemoveMenu( this ); } else if ( state == true ) { MoveToFront(); RequestFocus(); // Add to menu manager? if ( m_bUseMenuManager ) { g_MenuMgr.AddMenu( this ); } } // must be after movetofront() BaseClass::SetVisible(state); _sizedForScrollBar = false; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void Menu::ApplySchemeSettings(IScheme *pScheme) { BaseClass::ApplySchemeSettings(pScheme); SetFgColor(GetSchemeColor("Menu.TextColor", pScheme)); SetBgColor(GetSchemeColor("Menu.BgColor", pScheme)); _borderDark = pScheme->GetColor("BorderDark", Color(255, 255, 255, 0)); FOR_EACH_LL( m_MenuItems, i ) { if( m_MenuItems[i]->IsCheckable() ) { int wide, tall; m_MenuItems[i]->GetCheckImageSize( wide, tall ); m_iCheckImageWidth = max ( m_iCheckImageWidth, wide ); } } _recalculateWidth = true; CalculateWidth(); InvalidateLayout(); } void Menu::SetBgColor( Color newColor ) { BaseClass::SetBgColor( newColor ); FOR_EACH_LL( m_MenuItems, i ) { if( m_MenuItems[i]->HasMenu() ) { m_MenuItems[i]->GetMenu()->SetBgColor( newColor ); } } } void Menu::SetFgColor( Color newColor ) { BaseClass::SetFgColor( newColor ); FOR_EACH_LL( m_MenuItems, i ) { if( m_MenuItems[i]->HasMenu() ) { m_MenuItems[i]->GetMenu()->SetFgColor( newColor ); } } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void Menu::SetBorder(class IBorder *border) { Panel::SetBorder(border); } //----------------------------------------------------------------------------- // Purpose: returns a pointer to a MenuItem that is this menus parent, if it has one //----------------------------------------------------------------------------- MenuItem *Menu::GetParentMenuItem() { return dynamic_cast(GetParent()); } //----------------------------------------------------------------------------- // Purpose: Hide the menu when an item has been selected //----------------------------------------------------------------------------- void Menu::OnMenuItemSelected(Panel *panel) { SetVisible(false); m_pScroller->SetVisible(false); // chain this message up through the hierarchy so // all the parent menus will close // get the parent of this menu. MenuItem *item = GetParentMenuItem(); // if the parent is a menu item, this menu is a cascading menu if (item) { // get the parent of the menuitem. it should be a menu. Menu *parentMenu = item->GetParentMenu(); if (parentMenu) { // send the message to this parent menu KeyValues *kv = new KeyValues("MenuItemSelected"); kv->SetPtr("panel", panel); ivgui()->PostMessage(parentMenu->GetVPanel(), kv, GetVPanel()); } } bool activeItemSet = false; FOR_EACH_LL( m_MenuItems, i ) { if( m_MenuItems[i] == panel ) { activeItemSet = true; m_iActivatedItem = i; break; } } if( !activeItemSet ) { FOR_EACH_LL( m_MenuItems, i ) { if(m_MenuItems[i]->HasMenu() ) { /* // GetActiveItem needs to return -1 or similar if it hasn't been set... if( m_MenuItems[i]->GetActiveItem() ) { m_iActivatedItem = m_MenuItems[i]->GetActiveItem(); }*/ } } } // also pass it to the parent so they can respond if they like if (GetVParent()) { KeyValues *kv = new KeyValues("MenuItemSelected"); kv->SetPtr("panel", panel); ivgui()->PostMessage(GetVParent(), kv, GetVPanel()); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int Menu::GetActiveItem() { return m_iActivatedItem; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- KeyValues *Menu::GetItemUserData(int itemID) { if ( m_MenuItems.IsValidIndex( itemID ) ) { MenuItem *menuItem = dynamic_cast(m_MenuItems[itemID]); // make sure its enabled since disabled items get highlighted. if (menuItem && menuItem->IsEnabled()) { return menuItem->GetUserData(); } } return NULL; } //----------------------------------------------------------------------------- // Purpose: data accessor //----------------------------------------------------------------------------- void Menu::GetItemText(int itemID, wchar_t *text, int bufLenInBytes) { if ( m_MenuItems.IsValidIndex( itemID ) ) { MenuItem *menuItem = dynamic_cast(m_MenuItems[itemID]); if (menuItem) { menuItem->GetText(text, bufLenInBytes); return; } } text[0] = 0; } void Menu::GetItemText(int itemID, char *text, int bufLenInBytes) { if ( m_MenuItems.IsValidIndex( itemID ) ) { MenuItem *menuItem = dynamic_cast(m_MenuItems[itemID]); if (menuItem) { menuItem->GetText( text, bufLenInBytes ); return; } } text[0] = 0; } //----------------------------------------------------------------------------- // Purpose: Activate the n'th item in the menu list, as if that menu item had been selected by the user //----------------------------------------------------------------------------- void Menu::ActivateItem(int itemID) { if ( m_MenuItems.IsValidIndex( itemID ) ) { MenuItem *menuItem = dynamic_cast(m_MenuItems[itemID]); // make sure its enabled since disabled items get highlighted. if (menuItem && menuItem->IsEnabled()) { menuItem->FireActionSignal(); m_iActivatedItem = itemID; } } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void Menu::SilentActivateItem(int itemID) { if ( m_MenuItems.IsValidIndex( itemID ) ) { MenuItem *menuItem = dynamic_cast(m_MenuItems[itemID]); // make sure its enabled since disabled items get highlighted. if (menuItem && menuItem->IsEnabled()) { m_iActivatedItem = itemID; } } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void Menu::ActivateItemByRow(int row) { if (m_SortedItems.IsValidIndex(row)) { ActivateItem(m_SortedItems[row]); } } //----------------------------------------------------------------------------- // Purpose: Return the number of items currently in the menu list //----------------------------------------------------------------------------- int Menu::GetItemCount() const { return m_MenuItems.Count(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int Menu::GetMenuID(int index) { if ( !m_SortedItems.IsValidIndex(index) ) return m_MenuItems.InvalidIndex(); return m_SortedItems[index]; } //----------------------------------------------------------------------------- // Purpose: Return the number of items currently visible in the menu list //----------------------------------------------------------------------------- int Menu::GetCurrentlyVisibleItemsCount() { if (m_MenuItems.Count() < m_iNumVisibleLines) { int cMenuItems = 0; FOR_EACH_LL(m_MenuItems, i) { if (m_MenuItems[i]->IsVisible()) { ++cMenuItems; } } return cMenuItems; } return m_iNumVisibleLines; } //----------------------------------------------------------------------------- // Purpose: Enables/disables choices in the list // itemText - string name of item in the list // state - true enables, false disables //----------------------------------------------------------------------------- void Menu::SetItemEnabled(const char *itemName, bool state) { FOR_EACH_LL( m_MenuItems, i ) { if ((Q_stricmp(itemName, m_MenuItems[i]->GetName())) == 0) { m_MenuItems[i]->SetEnabled(state); } } } //----------------------------------------------------------------------------- // Purpose: Enables/disables choices in the list //----------------------------------------------------------------------------- void Menu::SetItemEnabled(int itemID, bool state) { if ( !m_MenuItems.IsValidIndex(itemID) ) return; m_MenuItems[itemID]->SetEnabled(state); } //----------------------------------------------------------------------------- // Purpose: shows/hides choices in the list //----------------------------------------------------------------------------- void Menu::SetItemVisible(const char *itemName, bool state) { FOR_EACH_LL( m_MenuItems, i ) { if ((Q_stricmp(itemName, m_MenuItems[i]->GetName())) == 0) { m_MenuItems[i]->SetVisible(state); InvalidateLayout(); } } } //----------------------------------------------------------------------------- // Purpose: shows/hides choices in the list //----------------------------------------------------------------------------- void Menu::SetItemVisible(int itemID, bool state) { if ( !m_MenuItems.IsValidIndex(itemID) ) return; m_MenuItems[itemID]->SetVisible(state); } //----------------------------------------------------------------------------- // Purpose: Make the scroll bar visible and narrow the menu // also make items visible or invisible in the list as appropriate //----------------------------------------------------------------------------- void Menu::AddScrollBar() { m_pScroller->SetVisible(true); _sizedForScrollBar = true; } //----------------------------------------------------------------------------- // Purpose: Make the scroll bar invisible and widen the menu //----------------------------------------------------------------------------- void Menu::RemoveScrollBar() { m_pScroller->SetVisible(false); _sizedForScrollBar = false; } //----------------------------------------------------------------------------- // Purpose: Invalidate layout if the slider is moved so items scroll //----------------------------------------------------------------------------- void Menu::OnSliderMoved() { CloseOtherMenus(NULL); // close any cascading menus // Invalidate so we redraw the menu! InvalidateLayout(); Repaint(); } //----------------------------------------------------------------------------- // Purpose: Toggle into mouse mode. //----------------------------------------------------------------------------- void Menu::OnCursorMoved(int x, int y) { m_iInputMode = MOUSE; // chain up CallParentFunction(new KeyValues("OnCursorMoved", "x", x, "y", y)); //RequestFocus(); //InvalidateLayout(); } //----------------------------------------------------------------------------- // Purpose: Toggle into keyboard mode. //----------------------------------------------------------------------------- void Menu::OnKeyCodePressed(KeyCode code) { m_iInputMode = KEYBOARD; // send the message to this parent in case this is a cascading menu if (GetVParent()) { ivgui()->PostMessage(GetVParent(), new KeyValues("KeyModeSet"), GetVPanel()); } BaseClass::OnKeyCodePressed( code ); } //----------------------------------------------------------------------------- // Purpose: Sets the item currently highlighted in the menu by ptr //----------------------------------------------------------------------------- void Menu::SetCurrentlySelectedItem(MenuItem *item) { int itemNum = -1; // find it in our list of menuitems FOR_EACH_LL( m_MenuItems, i ) { MenuItem *child = m_MenuItems[i]; if (child == item) { itemNum = i; break; } } Assert( itemNum >= 0 ); SetCurrentlySelectedItem(itemNum); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void Menu::ClearCurrentlyHighlightedItem() { if ( m_MenuItems.IsValidIndex(m_iCurrentlySelectedItemID) ) { m_MenuItems[m_iCurrentlySelectedItemID]->DisarmItem(); } m_iCurrentlySelectedItemID = m_MenuItems.InvalidIndex(); } //----------------------------------------------------------------------------- // Purpose: Sets the item currently highlighted in the menu by index //----------------------------------------------------------------------------- void Menu::SetCurrentlySelectedItem(int itemID) { // dont deselect if its the same item if (itemID == m_iCurrentlySelectedItemID) return; if ( m_MenuItems.IsValidIndex(m_iCurrentlySelectedItemID) ) { m_MenuItems[m_iCurrentlySelectedItemID]->DisarmItem(); } PostActionSignal(new KeyValues("MenuItemHighlight", "itemID", itemID)); m_iCurrentlySelectedItemID = itemID; } //----------------------------------------------------------------------------- // This will set the item to be currenly selected and highlight it // will not open cascading menu. This was added for comboboxes // to have the combobox item highlighted in the menu when they open the // dropdown. //----------------------------------------------------------------------------- void Menu::SetCurrentlyHighlightedItem(int itemID) { SetCurrentlySelectedItem(itemID); int row = m_SortedItems.Find(itemID); // If we have no items, then row will be -1. The dev console, for example... Assert( ( m_SortedItems.Count() == 0 ) || ( row != -1 ) ); if ( row == -1 ) return; // if there is a scroll bar, and we scroll off lets move it. if ( m_pScroller->IsVisible() ) { // now if we are off the scroll bar, it means we moved the scroll bar // by hand or set the item off the list // so just snap the scroll bar straight to the item. if ( ( row > m_pScroller->GetValue() + m_iNumVisibleLines - 1 ) || ( row < m_pScroller->GetValue() ) ) { if ( !m_pScroller->IsVisible() ) return; m_pScroller->SetValue(row); } } if ( m_MenuItems.IsValidIndex(m_iCurrentlySelectedItemID) ) { if ( !m_MenuItems[m_iCurrentlySelectedItemID]->IsArmed() ) { m_MenuItems[m_iCurrentlySelectedItemID]->ArmItem(); } } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int Menu::GetCurrentlyHighlightedItem() { return m_iCurrentlySelectedItemID; } //----------------------------------------------------------------------------- // Purpose: Respond to cursor entering a menuItem. //----------------------------------------------------------------------------- void Menu::OnCursorEnteredMenuItem(int VPanel) { VPANEL menuItem = (VPANEL)VPanel; // if we are in mouse mode if (m_iInputMode == MOUSE) { MenuItem *item = static_cast(ipanel()->GetPanel(menuItem, GetModuleName())); // arm the menu item->ArmItem(); SetCurrentlySelectedItem(item); // open the cascading menu if there is one. if ( item->HasMenu() ) { // open the cascading menu if there is one. item->OpenCascadeMenu(); ActivateItem( m_iCurrentlySelectedItemID ); } } } //----------------------------------------------------------------------------- // Purpose: Respond to cursor exiting a menuItem //----------------------------------------------------------------------------- void Menu::OnCursorExitedMenuItem(int VPanel) { VPANEL menuItem = (VPANEL)VPanel; // only care if we are in mouse mode if (m_iInputMode == MOUSE) { MenuItem *item = static_cast(ipanel()->GetPanel(menuItem, GetModuleName())); // unhighlight the item. // note menuItems with cascading menus will stay lit. item->DisarmItem(); } } //----------------------------------------------------------------------------- // Purpose: Move up or down one in the list of items in the menu // Direction is MENU_UP or MENU_DOWN //----------------------------------------------------------------------------- void Menu::MoveAlongMenuItemList(int direction, int loopCount) { // Early out if no menu items to scroll through if (m_MenuItems.Count() <= 0) return; int itemID = m_iCurrentlySelectedItemID; int row = m_SortedItems.Find(itemID); row += direction; if ( row > m_SortedItems.Count() - 1 ) { if ( m_pScroller->IsVisible() ) { // stop at bottom of scrolled list row = m_SortedItems.Count() - 1; } else { // if no scroll bar we circle around row = 0; } } else if (row < 0) { if ( m_pScroller->IsVisible() ) { // stop at top of scrolled list row = m_pScroller->GetValue(); } else { // if no scroll bar circle around row = m_SortedItems.Count()-1; } } // if there is a scroll bar, and we scroll off lets move it. if ( m_pScroller->IsVisible() ) { if ( row > m_pScroller->GetValue() + m_iNumVisibleLines - 1) { int val = m_pScroller->GetValue(); val -= -direction; m_pScroller->SetValue(val); // moving the slider redraws the scrollbar, // and so we should redraw the menu since the // menu draws the black border to the right of the scrollbar. InvalidateLayout(); } else if ( row < m_pScroller->GetValue() ) { int val = m_pScroller->GetValue(); val -= -direction; m_pScroller->SetValue(val); // moving the slider redraws the scrollbar, // and so we should redraw the menu since the // menu draws the black border to the right of the scrollbar. InvalidateLayout(); } // now if we are still off the scroll bar, it means we moved the scroll bar // by hand and created a situation in which we moved an item down, but the // scroll bar is already too far down and should scroll up or vice versa // so just snap the scroll bar straight to the item. if ( ( row > m_pScroller->GetValue() + m_iNumVisibleLines - 1) || ( row < m_pScroller->GetValue() ) ) { m_pScroller->SetValue(row); } } // switch it back to an itemID from row if ( m_SortedItems.IsValidIndex( row ) ) { SetCurrentlySelectedItem( m_SortedItems[row] ); } // don't allow us to loop around more than once if (loopCount < m_MenuItems.Count()) { // see if the text is empty, if so skip wchar_t text[256]; m_MenuItems[m_iCurrentlySelectedItemID]->GetText(text, 255); if (text[0] == 0 || !m_MenuItems[m_iCurrentlySelectedItemID]->IsVisible()) { // menu item is empty, keep moving along MoveAlongMenuItemList(direction, loopCount + 1); } } } //----------------------------------------------------------------------------- // Purpose: Return which type of events the menu is currently interested in // MenuItems need to know because behaviour is different depending on mode. //----------------------------------------------------------------------------- int Menu::GetMenuMode() { return m_iInputMode; } //----------------------------------------------------------------------------- // Purpose: Set the menu to key mode if a child menu goes into keymode // This mode change has to be chained up through the menu heirarchy // so cascading menus will work when you do a bunch of stuff in keymode // in high level menus and then switch to keymode in lower level menus. //----------------------------------------------------------------------------- void Menu::OnKeyModeSet() { m_iInputMode = KEYBOARD; } //----------------------------------------------------------------------------- // Purpose: Set the checked state of a menuItem //----------------------------------------------------------------------------- void Menu::SetMenuItemChecked(int itemID, bool state) { m_MenuItems[itemID]->SetChecked(state); } //----------------------------------------------------------------------------- // Purpose: Check if item is checked. //----------------------------------------------------------------------------- bool Menu::IsChecked(int itemID) { return m_MenuItems[itemID]->IsChecked(); } //----------------------------------------------------------------------------- // Purpose: Set the minmum width the menu has to be. This // is useful if you have a menu that is sized to the largest item in it // but you don't want the menu to be thinner than the menu button //----------------------------------------------------------------------------- void Menu::SetMinimumWidth(int width) { m_iMinimumWidth = width; } //----------------------------------------------------------------------------- // Purpose: Get the minmum width the menu //----------------------------------------------------------------------------- int Menu::GetMinimumWidth() { return m_iMinimumWidth; } //----------------------------------------------------------------------------- // Purpose: // Input : - //----------------------------------------------------------------------------- void Menu::AddSeparator() { int lastID = m_MenuItems.Count() - 1; m_Separators.AddToTail( lastID ); m_SeparatorPanels.AddToTail( new MenuSeparator( this, "MenuSeparator" ) ); } void Menu::AddSeparatorAfterItem( int itemID ) { Assert( m_MenuItems.IsValidIndex( itemID ) ); m_Separators.AddToTail( itemID ); m_SeparatorPanels.AddToTail( new MenuSeparator( this, "MenuSeparator" ) ); } void Menu::MoveMenuItem( int itemID, int moveBeforeThisItemID ) { int c = m_SortedItems.Count(); int i; for ( i = 0; i < c; ++i ) { if ( m_SortedItems[i] == itemID ) { m_SortedItems.Remove( i ); break; } } // Didn't find it if ( i >= c ) { return; } // Now find insert pos c = m_SortedItems.Count(); for ( i = 0; i < c; ++i ) { if ( m_SortedItems[i] == moveBeforeThisItemID ) { m_SortedItems.InsertBefore( i, itemID ); break; } } } void Menu::SetFont( HFont font ) { m_hItemFont = font; if ( font ) { m_iMenuItemHeight = surface()->GetFontTall( font ) + 2; } InvalidateLayout(); } void Menu::SetCurrentKeyBinding( int itemID, char const *hotkey ) { if ( m_MenuItems.IsValidIndex( itemID ) ) { MenuItem *menuItem = dynamic_cast(m_MenuItems[itemID]); menuItem->SetCurrentKeyBinding( hotkey ); } } //----------------------------------------------------------------------------- // Purpose: Static method to display a context menu // Input : *parent - // *menu - //----------------------------------------------------------------------------- void Menu::PlaceContextMenu( Panel *parent, Menu *menu ) { Assert( parent ); Assert( menu ); if ( !menu || !parent ) return; menu->SetVisible(false); menu->SetParent( parent ); menu->AddActionSignalTarget( parent ); // get cursor position, this is local to this text edit window int cursorX, cursorY; input()->GetCursorPos(cursorX, cursorY); menu->SetVisible(true); // relayout the menu immediately so that we know it's size menu->InvalidateLayout(true); int menuWide, menuTall; menu->GetSize(menuWide, menuTall); // work out where the cursor is and therefore the best place to put the menu int wide, tall; surface()->GetScreenSize(wide, tall); if (wide - menuWide > cursorX) { // menu hanging right if (tall - menuTall > cursorY) { // menu hanging down menu->SetPos(cursorX, cursorY); } else { // menu hanging up menu->SetPos(cursorX, cursorY - menuTall); } } else { // menu hanging left if (tall - menuTall > cursorY) { // menu hanging down menu->SetPos(cursorX - menuWide, cursorY); } else { // menu hanging up menu->SetPos(cursorX - menuWide, cursorY - menuTall); } } menu->RequestFocus(); } void Menu::SetUseFallbackFont( bool bState, HFont hFallback ) { m_hFallbackItemFont = hFallback; m_bUseFallbackFont = bState; } #ifdef DBGFLAG_VALIDATE //----------------------------------------------------------------------------- // Purpose: Run a global validation pass on all of our data structures and memory // allocations. // Input: validator - Our global validator object // pchName - Our name (typically a member var in our container) //----------------------------------------------------------------------------- void Menu::Validate( CValidator &validator, char *pchName ) { validator.Push( "vgui::Menu", this, pchName ); m_MenuItems.Validate( validator, "m_MenuItems" ); m_SortedItems.Validate( validator, "m_SortedItems" ); BaseClass::Validate( validator, "vgui::Menu" ); validator.Pop(); } #endif // DBGFLAG_VALIDATE MenuBuilder::MenuBuilder( Menu *pMenu, Panel *pActionTarget ) : m_pMenu( pMenu ) , m_pActionTarget( pActionTarget ) , m_pszLastCategory( NULL ) {} MenuItem* MenuBuilder::AddMenuItem( const char *pszButtonText, const char *pszCommand, const char *pszCategoryName ) { AddSepratorIfNeeded( pszCategoryName ); return m_pMenu->GetMenuItem( m_pMenu->AddMenuItem( pszButtonText, pszCommand, m_pActionTarget ) ); } MenuItem* MenuBuilder::AddMenuItem( const char *pszButtonText, KeyValues *kvUserData, const char *pszCategoryName ) { AddSepratorIfNeeded( pszCategoryName ); return m_pMenu->GetMenuItem( m_pMenu->AddMenuItem( pszButtonText, kvUserData, m_pActionTarget ) ); } MenuItem* MenuBuilder::AddCascadingMenuItem( const char *pszButtonText, Menu *pSubMenu, const char *pszCategoryName ) { AddSepratorIfNeeded( pszCategoryName ); return m_pMenu->GetMenuItem( m_pMenu->AddCascadingMenuItem( pszButtonText, m_pActionTarget, pSubMenu ) ); } void MenuBuilder::AddSepratorIfNeeded( const char *pszCategoryName ) { // Add a separator if the categories are different if ( m_pszLastCategory && V_stricmp( pszCategoryName, m_pszLastCategory ) != 0 ) { m_pMenu->AddSeparator(); } m_pszLastCategory = pszCategoryName; }