/* * OpenClonk, http://www.openclonk.org * * Copyright (c) 1998-2000, Matthes Bender * Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de/ * Copyright (c) 2009-2016, The OpenClonk Team and contributors * * Distributed under the terms of the ISC license; see accompanying file * "COPYING" for details. * * "Clonk" is a registered trademark of Matthes Bender, used with permission. * See accompanying file "TRADEMARK" for details. * * To redistribute this file separately, substitute the full license texts * for the above references. */ /* In-game menu as used by objects, players, and fullscreen options */ #include "C4Include.h" #include "gui/C4Menu.h" #include "control/C4GameControl.h" #include "game/C4Viewport.h" #include "graphics/C4Draw.h" #include "graphics/C4GraphicsResource.h" #include "gui/C4MouseControl.h" #include "lib/StdColors.h" #include "object/C4Def.h" #include "object/C4DefList.h" #include "object/C4Object.h" #include "player/C4Player.h" #include "player/C4PlayerList.h" const int32_t C4MN_DefInfoWdt = 270, // default width of info windows C4MN_DlgWdt = 270, // default width of dialog windows C4MN_DlgLines = 5, // default number of text lines visible in a dialog window C4MN_DlgLineMargin = 5, // px distance between text items C4MN_DlgOptionLineMargin = 3, // px distance between dialog option items C4MN_DlgPortraitWdt = 64, // size of portrait C4MN_DlgPortraitIndent = 5; // space between portrait and text const int32_t C4MN_InfoCaption_Delay = 90; // ----------------------------------------------------------- // C4MenuItem C4MenuItem::C4MenuItem(C4Menu *pMenu, int32_t iIndex, const char *szCaption, const char *szCommand, int32_t iCount, C4Object *pObject, const char *szInfoCaption, C4ID idID, const char *szCommand2, bool fOwnValue, int32_t iValue, int32_t iStyle, bool fIsSelectable) : C4GUI::Element(), Count(iCount), id(idID), Object(pObject), pSymbolObj(nullptr), pSymbolGraphics(nullptr), dwSymbolClr(0u), fOwnValue(fOwnValue), iValue(iValue), fSelected(false), iStyle(iStyle), pMenu(pMenu), iIndex(iIndex), IsSelectable(fIsSelectable), TextDisplayProgress(-1) { *Caption=*Command=*Command2=*InfoCaption=0; Symbol.Default(); SCopy(szCaption,Caption,C4MaxTitle); SCopy(szCommand,Command,_MAX_FNAME+30); SCopy(szCommand2,Command2,_MAX_FNAME+30); SCopy(szInfoCaption,InfoCaption,C4MaxTitle); // some info caption corrections SReplaceChar(InfoCaption, 10, ' '); SReplaceChar(InfoCaption, 13, '|'); SetToolTip(InfoCaption); } C4MenuItem::~C4MenuItem() { Symbol.Clear(); } void C4MenuItem::DoTextProgress(int32_t &riByVal) { // any progress to be done? if (TextDisplayProgress<0) return; // if this is an option or empty text, show it immediately if (IsSelectable || !*Caption) { TextDisplayProgress=-1; return; } // normal text: move forward in unbroken message, ignoring markup StdStrBuf sText(Caption); C4Markup MarkupChecker(false); const char *szPos = sText.getPtr(std::min(TextDisplayProgress, sText.getLength())); while (riByVal && *szPos) { MarkupChecker.SkipTags(&szPos); if (!*szPos) break; --riByVal; // Advance one UTF-8 character uint32_t c = GetNextCharacter(&szPos); // Treat embedded images {{XXX}} as one entity if(c == '{' && *szPos == '{') { int32_t end = SCharPos('}', szPos); if(end > 0 && szPos[end+1] == '}') szPos += end + 2; } } if (!*szPos) TextDisplayProgress=-1; else TextDisplayProgress = szPos - Caption; } bool C4MenuItem::IsDragElement() { // any constructibles can be dragged C4Def *pDef = C4Id2Def(id); return pDef && pDef->Constructable; } int32_t C4MenuItem::GetSymbolWidth(int32_t iForHeight) { // Context or dialog menus if (iStyle==C4MN_Style_Context || (iStyle==C4MN_Style_Dialog && Symbol.Surface)) return std::max(Symbol.Wdt * iForHeight / std::max(Symbol.Hgt, 1.0f), static_cast(iForHeight)); // Info menus if (iStyle==C4MN_Style_Info && Symbol.Surface && Symbol.Wdt) return Symbol.Wdt; // no symbol return 0; } void C4MenuItem::DrawElement(C4TargetFacet &cgo) { // get target pos C4Facet cgoOut(cgo.Surface, cgo.TargetX + rcBounds.x, cgo.TargetY + rcBounds.y, rcBounds.Wdt, rcBounds.Hgt); // Select mark if (iStyle!=C4MN_Style_Info) if (fSelected && TextDisplayProgress) pDraw->DrawBoxDw(cgo.Surface, cgoOut.X, cgoOut.Y, cgoOut.X + cgoOut.Wdt - 1, cgoOut.Y + cgoOut.Hgt - 1, C4RGB(0xca, 0, 0)); // Symbol/text areas C4Facet cgoItemSymbol,cgoItemText; cgoItemSymbol=cgoItemText=cgoOut; int32_t iSymWidth; if ((iSymWidth = GetSymbolWidth(cgoItemText.Hgt))) { // get symbol area cgoItemSymbol=cgoItemText.Truncate(C4FCT_Left, iSymWidth); } // cgoItemSymbol.Hgt is 0. This means rcBounds.Hgt is 0. That // makes no sense at this point, so let's just draw in a // square area at item y. C4Facet cgoSymbolOut(cgoItemSymbol.Surface, cgoItemSymbol.X, cgoItemSymbol.Y, cgoItemSymbol.Wdt, cgoItemSymbol.Wdt); // Draw item symbol: // Draw if there is no text progression at all (TextDisplayProgress==-1, or if it's progressed far enough already (TextDisplayProgress>0) if(pSymbolObj && TextDisplayProgress) { pSymbolObj->DrawPicture(cgoSymbolOut, false, nullptr); } else if (pSymbolGraphics && TextDisplayProgress) { pSymbolGraphics->Draw(cgoSymbolOut, dwSymbolClr ? dwSymbolClr : 0xffffffff, nullptr, 0, 0, nullptr); } else if (Symbol.Surface && TextDisplayProgress) Symbol.DrawClr(cgoItemSymbol, true, dwSymbolClr); // Draw item text pDraw->StorePrimaryClipper(); pDraw->SubPrimaryClipper(cgoItemText.X, cgoItemText.Y, cgoItemText.X+cgoItemText.Wdt-1, cgoItemText.Y+cgoItemText.Hgt-1); switch (iStyle) { case C4MN_Style_Context: pDraw->TextOut(Caption,::GraphicsResource.FontRegular, 1.0, cgoItemText.Surface,cgoItemText.X,cgoItemText.Y,C4Draw::DEFAULT_MESSAGE_COLOR,ALeft); break; case C4MN_Style_Info: { StdStrBuf sText; ::GraphicsResource.FontRegular.BreakMessage(InfoCaption, cgoItemText.Wdt, &sText, true); pDraw->TextOut(sText.getData(), ::GraphicsResource.FontRegular, 1.0, cgoItemText.Surface,cgoItemText.X,cgoItemText.Y); break; } case C4MN_Style_Dialog: { // cut buffer at text display pos char cXChg='\0'; int iStopPos = 0; if (TextDisplayProgress>-1) { iStopPos = std::min(TextDisplayProgress, strlen(Caption)); cXChg = Caption[iStopPos]; Caption[iStopPos] = '\0'; } // display broken text StdStrBuf sText; ::GraphicsResource.FontRegular.BreakMessage(Caption, cgoItemText.Wdt, &sText, true); pDraw->TextOut(sText.getData(),::GraphicsResource.FontRegular, 1.0, cgoItemText.Surface,cgoItemText.X,cgoItemText.Y); // restore complete text if (cXChg) Caption[iStopPos] = cXChg; break; } } pDraw->RestorePrimaryClipper(); // Draw count if (Count!=C4MN_Item_NoCount) { char szCount[10+1]; sprintf(szCount,"%ix",Count); pDraw->TextOut(szCount, ::GraphicsResource.FontRegular, 1.0, cgoItemText.Surface, cgoItemText.X+cgoItemText.Wdt-1, cgoItemText.Y+cgoItemText.Hgt-1-::GraphicsResource.FontRegular.GetLineHeight(), C4Draw::DEFAULT_MESSAGE_COLOR, ARight); } } void C4MenuItem::MouseInput(C4GUI::CMouse &rMouse, int32_t iButton, int32_t iX, int32_t iY, DWORD dwKeyParam) { // clicky clicky! if (iButton == C4MC_Button_LeftDown) { // button down: Init drag only; Enter selection only by button up if (IsDragElement()) StartDragging(rMouse, iX, iY, dwKeyParam); } else if (iButton == C4MC_Button_LeftUp) { // left-click performed pMenu->UserEnter(::MouseControl.GetPlayer(), this, false); return; } else if (iButton == C4MC_Button_RightUp) { // right-up: Alternative enter command pMenu->UserEnter(::MouseControl.GetPlayer(), this, true); return; } // inherited; this is just setting some vars typedef C4GUI::Element ParentClass; ParentClass::MouseInput(rMouse, iButton, iX, iY, dwKeyParam); } void C4MenuItem::MouseEnter(C4GUI::CMouse &rMouse) { // callback to menu: Select item pMenu->UserSelectItem(::MouseControl.GetPlayer(), this); typedef C4GUI::Element ParentClass; ParentClass::MouseEnter(rMouse); } // ----------------------------------------------------------- // C4Menu C4Menu::C4Menu() : C4GUI::Dialog(100, 100, nullptr, true) // will be re-adjusted later { Default(); AddElement(pClientWindow = new C4GUI::ScrollWindow(this)); // initially invisible: Will be made visible at first drawing by viewport // when the location will be inialized SetVisibility(false); } void C4Menu::Default() { Selection=-1; Style=C4MN_Style_Normal; ItemCount=0; ItemWidth=ItemHeight=C4SymbolSize; NeedRefill=false; Symbol.Default(); Caption[0]=0; Permanent=false; Extra=C4MN_Extra_None; ExtraData=0; TimeOnSelection=0; Identification=0; LocationSet=false; Columns=Lines=0; Alignment= C4MN_Align_Right | C4MN_Align_Bottom; VisibleCount=0; fHasPortrait = false; fTextProgressing = false; fEqualIconItemHeight = false; CloseCommand.Clear(); fActive = false; } void C4Menu::Clear() { Close(false); Symbol.Clear(); ClearItems(); ClearFrameDeco(); fActive = false; } bool C4Menu::TryClose(bool fOK, bool fControl) { // abort if menu is permanented by script if (!fOK) if (IsCloseDenied()) return false; // close the menu Close(fOK); Clear(); ::pGUI->RemoveElement(this); // invoke close command if (fControl && CloseCommand.getData()) { MenuCommand(CloseCommand.getData(), true); } // done return true; } bool C4Menu::DoInit(C4FacetSurface &fctSymbol, const char *szEmpty, int32_t iExtra, int32_t iExtraData, int32_t iId, int32_t iStyle) { Clear(); Default(); Symbol.GrabFrom(fctSymbol); return InitMenu(szEmpty, iExtra, iExtraData, iId, iStyle); } bool C4Menu::DoInitRefSym(const C4Facet &fctSymbol, const char *szEmpty, int32_t iExtra, int32_t iExtraData, int32_t iId, int32_t iStyle) { Clear(); Default(); Symbol.Set(fctSymbol); return InitMenu(szEmpty, iExtra, iExtraData, iId, iStyle); } bool C4Menu::InitMenu(const char *szEmpty, int32_t iExtra, int32_t iExtraData, int32_t iId, int32_t iStyle) { SCopy(szEmpty,Caption,C4MaxTitle); Extra=iExtra; ExtraData=iExtraData; Identification=iId; if (*Caption || iStyle == C4MN_Style_Dialog) SetTitle(Caption, HasMouse()); else SetTitle(" ", HasMouse()); if (pTitle) pTitle->SetIcon(Symbol); Style=iStyle & C4MN_Style_BaseMask; // Menus are synchronous to allow COM_MenuUp/Down to be converted to movements at the clients if (Style == C4MN_Style_Normal) Columns = 5; else // in reality, Dialog menus may have two coloumns (first for the portrait) // however, they are not uniformly spaced and stuff; so they are better just ignored and handled by the drawing routine Columns=1; if (iStyle & C4MN_Style_EqualItemHeight) SetEqualItemHeight(true); if (Style == C4MN_Style_Dialog) Alignment = C4MN_Align_Top; ::pGUI->ShowDialog(this, false); fTextProgressing = false; fActive = true; return true; } bool C4Menu::AddRefSym(const char *szCaption, const C4Facet &fctSymbol, const char *szCommand, int32_t iCount, C4Object *pObject, const char *szInfoCaption, C4ID idID, const char *szCommand2, bool fOwnValue, int32_t iValue, bool fIsSelectable) { if (!IsActive()) return false; // Create new menu item C4MenuItem *pNew = new C4MenuItem(this, ItemCount, szCaption,szCommand,iCount,pObject,szInfoCaption,idID,szCommand2,fOwnValue,iValue,Style,fIsSelectable); // Ref Symbol pNew->RefSymbol(fctSymbol); // Add return AddItem(pNew, szCaption, szCommand, iCount, pObject, szInfoCaption, idID, szCommand2, fOwnValue, iValue, fIsSelectable); } bool C4Menu::Add(const char *szCaption, C4FacetSurface &fctSymbol, const char *szCommand, int32_t iCount, C4Object *pObject, const char *szInfoCaption, C4ID idID, const char *szCommand2, bool fOwnValue, int32_t iValue, bool fIsSelectable) { if (!IsActive()) return false; // Create new menu item C4MenuItem *pNew = new C4MenuItem(this, ItemCount, szCaption,szCommand,iCount,pObject,szInfoCaption,idID,szCommand2,fOwnValue,iValue,Style,fIsSelectable); // Set Symbol pNew->GrabSymbol(fctSymbol); // Add return AddItem(pNew, szCaption, szCommand, iCount, pObject, szInfoCaption, idID, szCommand2, fOwnValue, iValue, fIsSelectable); } bool C4Menu::Add(const char *szCaption, C4Object* pGfxObj, const char *szCommand, int32_t iCount, C4Object *pObject, const char *szInfoCaption, C4ID idID, const char *szCommand2, bool fOwnValue, int32_t iValue, bool fIsSelectable) { if (!IsActive()) return false; // Create new menu item C4MenuItem *pNew = new C4MenuItem(this, ItemCount, szCaption,szCommand,iCount,pObject,szInfoCaption,idID,szCommand2,fOwnValue,iValue,Style,fIsSelectable); // Set Symbol pNew->SetGraphics(pGfxObj); // Add return AddItem(pNew, szCaption, szCommand, iCount, pObject, szInfoCaption, idID, szCommand2, fOwnValue, iValue, fIsSelectable); } bool C4Menu::Add(const char *szCaption, C4DefGraphics* pGfx, const char *szCommand, int32_t iCount, C4Object *pObject, const char *szInfoCaption, C4ID idID, const char *szCommand2, bool fOwnValue, int32_t iValue, bool fIsSelectable) { if (!IsActive()) return false; // Create new menu item C4MenuItem *pNew = new C4MenuItem(this, ItemCount, szCaption,szCommand,iCount,pObject,szInfoCaption,idID,szCommand2,fOwnValue,iValue,Style,fIsSelectable); // Set Symbol pNew->SetGraphics(pGfx); // Add return AddItem(pNew, szCaption, szCommand, iCount, pObject, szInfoCaption, idID, szCommand2, fOwnValue, iValue, fIsSelectable); } bool C4Menu::AddItem(C4MenuItem *pNew, const char *szCaption, const char *szCommand, int32_t iCount, C4Object *pObject, const char *szInfoCaption, C4ID idID, const char *szCommand2, bool fOwnValue, int32_t iValue, bool fIsSelectable) { if (DEBUGREC_MENU && Config.General.DebugRec) if (pObject) { C4RCMenuAdd rc = { pObject ? pObject->Number : -1, iCount, idID, fOwnValue, iValue, fIsSelectable }; AddDbgRec(RCT_MenuAdd, &rc, sizeof(C4RCMenuAdd)); if (szCommand) AddDbgRec(RCT_MenuAddC, szCommand, strlen(szCommand)+1); if (szCommand2) AddDbgRec(RCT_MenuAddC, szCommand2, strlen(szCommand2)+1); } // Add it to the list pClientWindow->AddElement(pNew); // first menuitem is portrait, if it does not have text but a facet if (!ItemCount && (!szCaption || !*szCaption)) fHasPortrait = true; // Item count ItemCount++; // set new item size if (!pClientWindow->IsFrozen()) UpdateElementPositions(); // Init selection if not frozen if (Selection==-1 && fIsSelectable && !pClientWindow->IsFrozen()) SetSelection(ItemCount-1, false, false); // initial progress if (fTextProgressing) pNew->TextDisplayProgress = 0; // adjust scrolling, etc. UpdateScrollBar(); // Success return true; } bool C4Menu::Control(BYTE byCom, int32_t iData) { if (!IsActive()) return false; switch (byCom) { case COM_MenuEnter: Enter(); break; case COM_MenuEnterAll: Enter(true); break; case COM_MenuClose: TryClose(false, true); break; // organize with nicer subfunction... case COM_MenuLeft: // Top wrap-around if (Selection-1 < 0) MoveSelection(ItemCount - 1 - Selection, true, true); else MoveSelection(-1, true, true); break; case COM_MenuRight: // Bottom wrap-around if (Selection+1 >= ItemCount) MoveSelection(-Selection, true, true); else MoveSelection(+1, true, true); break; case COM_MenuUp: iData = -Columns; // Top wrap-around if (Selection + iData < 0) while (Selection + iData + Columns < ItemCount) iData += Columns; MoveSelection(iData, true, true); break; case COM_MenuDown: iData = +Columns; // Bottom wrap-around if (Selection+iData >= ItemCount) while (Selection+iData-Columns >= 0) iData-=Columns; MoveSelection(iData, true, true); break; case COM_MenuSelect: if (ItemCount) SetSelection(iData & (~C4MN_AdjustPosition), !!(iData & C4MN_AdjustPosition), true); break; case COM_MenuShowText: SetTextProgress(-1, false); break; } return true; } bool C4Menu::KeyControl(BYTE byCom) { // direct keyboard callback if (!IsActive()) return false; return !!Control(byCom, 0); } bool C4Menu::IsActive() { return fActive; } bool C4Menu::Enter(bool fRight) { // Not active if (!IsActive()) return false; if (Style==C4MN_Style_Info) return false; // Get selected item C4MenuItem *pItem = GetSelectedItem(); if (!pItem) { // okay for dialogs: Just close them if (Style == C4MN_Style_Dialog) TryClose(false, true); return true; } // Copy command to buffer (menu might be cleared) char szCommand[_MAX_FNAME+30+1]; SCopy(pItem->Command,szCommand); if (fRight && pItem->Command2[0]) SCopy(pItem->Command2,szCommand); // Close if not permanent if (!Permanent) { Close(true); fActive = false; } // exec command (note that menu callback may delete this!) MenuCommand(szCommand, false); return true; } C4MenuItem* C4Menu::GetItem(int32_t iIndex) { return static_cast(pClientWindow->GetElementByIndex(iIndex)); } int32_t C4Menu::GetItemCount() { return ItemCount; } bool C4Menu::MoveSelection(int32_t iBy, bool fAdjustPosition, bool fDoCalls) { if (!iBy) return false; // find next item that can be selected by moving in iBy steps int32_t iNewSel = Selection; for (;;) { // determine new selection iNewSel += iBy; // selection is out of menu range if (!Inside(iNewSel, 0, ItemCount-1)) return false; // determine newly selected item C4MenuItem *pNewSel = GetItem(iNewSel); // nothing selectable if (!pNewSel || !pNewSel->IsSelectable) continue; // got something: go select it break; } // select it return !!SetSelection(iNewSel, fAdjustPosition, fDoCalls); } bool C4Menu::SetSelection(int32_t iSelection, bool fAdjustPosition, bool fDoCalls) { // Not active if (!IsActive()) return false; // Outside Limits / Selectable C4MenuItem *pNewSel = GetItem(iSelection); if ((iSelection==-1 && !ItemCount) || (pNewSel && pNewSel->IsSelectable)) { // Selection change if (iSelection!=Selection) { // calls C4MenuItem *pSel = GetSelectedItem(); if (pSel) pSel->SetSelected(false); // Set selection Selection=iSelection; // Reset time on selection TimeOnSelection=0; } // always recheck selection for internal refill C4MenuItem *pSel = GetSelectedItem(); if (pSel) pSel->SetSelected(true); // set main caption by selection if (Style == C4MN_Style_Normal) { if (pSel) SetTitle(*(pSel->Caption) ? pSel->Caption : " ", HasMouse()); else SetTitle(*Caption ? Caption : " ", HasMouse()); } } // adjust position, if desired if (fAdjustPosition) AdjustPosition(); // do selection callback if (fDoCalls) OnSelectionChanged(Selection); // Done return true; } C4MenuItem* C4Menu::GetSelectedItem() { return GetItem(Selection); } void C4Menu::AdjustPosition() { // Adjust position by selection (works only after InitLocation) if ((Lines > 1) && Columns) { C4MenuItem *pSel = GetSelectedItem(); if (pSel) pClientWindow->ScrollRangeInView(pSel->GetBounds().y, pSel->GetBounds().Hgt); } } int32_t C4Menu::GetSelection() { return Selection; } bool C4Menu::SetPosition(int32_t iPosition) { if (!IsActive()) return false; // update scroll pos, if location is initialized if (IsVisible() && pClientWindow) pClientWindow->SetScroll((iPosition/Columns) * ItemHeight); return true; } int32_t C4Menu::GetIdentification() { return Identification; } void C4Menu::SetSize(int32_t iToWdt, int32_t iToHgt) { if (iToWdt) Columns=iToWdt; if (iToHgt) Lines=iToHgt; InitSize(); } void C4Menu::InitLocation(C4Facet &cgoArea) { // Item size by style switch (Style) { case C4MN_Style_Normal: ItemWidth=ItemHeight=C4SymbolSize; break; case C4MN_Style_Context: { ItemHeight = std::max(C4MN_SymbolSize, ::GraphicsResource.FontRegular.GetLineHeight()); int32_t iWdt, iHgt; ::GraphicsResource.FontRegular.GetTextExtent(Caption, ItemWidth, iHgt, true); // FIXME: Blah. This stuff should be calculated correctly by pTitle. ItemWidth += ItemHeight + 16; C4MenuItem *pItem; for (int i = 0; (pItem = GetItem(i)); ++i) { ::GraphicsResource.FontRegular.GetTextExtent(pItem->Caption, iWdt, iHgt, true); ItemWidth = std::max(ItemWidth, iWdt + pItem->GetSymbolWidth(ItemHeight)); } ItemWidth += 3; // Add some extra space so text doesn't touch right border frame... break; } case C4MN_Style_Info: { // calculate size from a default size determined by a window width of C4MN_DefInfoWdt int32_t iWdt,iHgt,iLargestTextWdt; ::GraphicsResource.FontRegular.GetTextExtent(Caption,iWdt,iHgt, true); iLargestTextWdt = iWdt + 2 * C4MN_SymbolSize + C4MN_FrameWidth; ItemWidth=std::min(cgoArea.Wdt - 2*C4MN_FrameWidth, std::max(iLargestTextWdt, C4MN_DefInfoWdt)); ItemHeight=0; StdStrBuf sText; C4MenuItem *pItem; for (int32_t i=0; (pItem=GetItem(i)); ++i) { ::GraphicsResource.FontRegular.BreakMessage(pItem->InfoCaption, ItemWidth, &sText, true); ::GraphicsResource.FontRegular.GetTextExtent(sText.getData(),iWdt,iHgt, true); assert(iWdt <= ItemWidth); ItemWidth=std::max(ItemWidth,iWdt); ItemHeight=std::max(ItemHeight,iHgt); iLargestTextWdt = std::max(iLargestTextWdt, iWdt); } // although width calculation is done from C4MN_DefInfoWdt, this may be too large for some very tiny info windows // so make sure no space is wasted ItemWidth = std::min(ItemWidth, iLargestTextWdt); // Add some extra space so text doesn't touch right border frame... ItemWidth += 3; // Now add some space to show the picture on the left ItemWidth += C4PictureSize; // And set a minimum item height (again, for the picture) ItemHeight = std::max(ItemHeight, C4PictureSize); break; } case C4MN_Style_Dialog: { // dialog window: Item width is whole dialog, portrait subtracted if any // Item height varies int32_t iWdt,iHgt; ::GraphicsResource.FontRegular.GetTextExtent(Caption,iWdt,iHgt, true); ItemWidth=std::min(cgoArea.Wdt - 2*C4MN_FrameWidth, std::max(iWdt + 2 * C4MN_SymbolSize + C4MN_FrameWidth, C4MN_DlgWdt)); ItemHeight=iHgt; // Items may be multiline and higher if (HasPortrait()) { // subtract portrait only if this would not make the dialog too small if (ItemWidth > C4MN_DlgPortraitWdt*2 && cgoArea.Hgt > cgoArea.Wdt) ItemWidth = std::max(ItemWidth - C4MN_DlgPortraitWdt - C4MN_DlgPortraitIndent, 40); } } } int DisplayedItemCount = ItemCount - HasPortrait(); if (Style == C4MN_Style_Dialog) Lines = C4MN_DlgLines; else Lines = DisplayedItemCount/Columns+std::min(DisplayedItemCount%Columns,1); // adjust by max. height Lines=std::max(std::min((cgoArea.Hgt-100)/std::max(ItemHeight,1),Lines),1); InitSize(); int32_t X,Y; if (Alignment & C4MN_Align_Free) { X = rcBounds.x; Y = rcBounds.y; } else { X = (cgoArea.Wdt - rcBounds.Wdt)/2; Y = (cgoArea.Hgt - rcBounds.Hgt)/2; } // Alignment if (Alignment & C4MN_Align_Left) X=C4SymbolSize; if (Alignment & C4MN_Align_Right) X=cgoArea.Wdt-2*C4SymbolSize-rcBounds.Wdt; if (Alignment & C4MN_Align_Top) Y=C4SymbolSize; if (Alignment & C4MN_Align_Bottom) Y=cgoArea.Hgt-C4SymbolSize-rcBounds.Hgt; if (Alignment & C4MN_Align_Free) { X=Clamp(X,0,cgoArea.Wdt-rcBounds.Wdt); Y=Clamp(Y,0,cgoArea.Hgt-rcBounds.Hgt); } // Centered (due to small viewport size) if (rcBounds.Wdt>cgoArea.Wdt-2*C4SymbolSize) X=(cgoArea.Wdt-rcBounds.Wdt)/2; if (rcBounds.Hgt>cgoArea.Hgt-2*C4SymbolSize) Y=(cgoArea.Hgt-rcBounds.Hgt)/2; SetPos(X, Y); // Position initialized: Make it visible to be used! SetVisibility(true); // now align all menu items correctly UpdateElementPositions(); // and correct scroll pos UpdateScrollBar(); AdjustPosition(); } void C4Menu::InitSize() { C4GUI::Element *pLast = pClientWindow->GetLast(); // Size calculation int Width, Height; Width=Columns*ItemWidth; Height=Lines*ItemHeight; VisibleCount = Columns*Lines; bool fBarNeeded; if (HasPortrait()) Width += C4MN_DlgPortraitWdt + C4MN_DlgPortraitIndent; // dialogs have auto-enlarge vertically if (pLast && Style == C4MN_Style_Dialog) { Height = std::max(Height, pLast->GetBounds().y + pLast->GetBounds().Hgt + C4MN_DlgLineMargin); fBarNeeded = false; } else fBarNeeded = pLast && pLast->GetBounds().y + pLast->GetBounds().Hgt > pClientWindow->GetBounds().Hgt; // add dlg margins Width += GetMarginLeft() + GetMarginRight() + pClientWindow->GetMarginLeft() + pClientWindow->GetMarginRight(); Height += GetMarginTop() + GetMarginBottom() + pClientWindow->GetMarginTop() + pClientWindow->GetMarginBottom(); if (fBarNeeded) Width += C4GUI_ScrollBarWdt; SetBounds(C4Rect(rcBounds.x, rcBounds.y, Width, Height)); pClientWindow->SetScrollBarEnabled(fBarNeeded); UpdateOwnPos(); } void C4Menu::UpdateScrollBar() { C4GUI::Element *pLast = pClientWindow->GetLast(); bool fBarNeeded = pLast && pLast->GetBounds().y + pLast->GetBounds().Hgt > pClientWindow->GetBounds().Hgt; if (pClientWindow->IsScrollBarEnabled() == fBarNeeded) return; // resize for bar InitSize(); } void C4Menu::Draw(C4TargetFacet &cgo) { // Inactive if (!IsActive()) return; // Location if (!LocationSet) { InitLocation(cgo); LocationSet=true; } // If drawn by a viewport, then it's made visible SetVisibility(true); // do drawing typedef C4GUI::Dialog ParentClass; ParentClass::Draw(cgo); // draw tooltip if selection time has been long enough if (!fTextProgressing) ++TimeOnSelection; if (TimeOnSelection >= C4MN_InfoCaption_Delay) if (Style != C4MN_Style_Info) // No tooltips in info menus - doesn't make any sense... if (!::Control.isReplay()) if (!::pGUI->Mouse.IsActiveInput()) { C4MenuItem *pSel = GetSelectedItem(); if (pSel && *pSel->InfoCaption) { int32_t iX=0, iY=0; pSel->ClientPos2ScreenPos(iX, iY); C4GUI::Screen::DrawToolTip(pSel->InfoCaption, cgo, iX, iY); } } } void C4Menu::DrawElement(C4TargetFacet &cgo) { // inherited (background) typedef C4GUI::Dialog ParentClass; ParentClass::DrawElement(cgo); // Get selected item id C4ID idSelected; C4MenuItem *pItem; if ((pItem = GetSelectedItem())) idSelected = pItem->id; else idSelected = C4ID::None; C4Def *pDef = C4Id2Def(idSelected); // Get item value int32_t iValue = 0; if (pDef) { if (pItem && pItem->fOwnValue) iValue = pItem->iValue; else iValue = pDef->GetValue(nullptr, NO_OWNER); } // Store and clear global clipper // int32_t iX1,iY1,iX2,iY2; // pDraw->GetPrimaryClipper(iX1,iY1,iX2,iY2); // pDraw->SubPrimaryClipper(rcBounds.x, rcBounds.y, rcBounds.x+rcBounds.Wdt-1, rcBounds.y+rcBounds.Hgt-1); C4Facet cgoExtra(cgo.Surface, cgo.TargetX+rcBounds.x+1, cgo.TargetY+rcBounds.y+rcBounds.Hgt-C4MN_SymbolSize-1, rcBounds.Wdt-2, C4MN_SymbolSize); // Draw bar divider if (Extra) { DrawFrame(cgoExtra.Surface, cgoExtra.X-1, cgoExtra.Y-1, cgoExtra.Wdt+1, cgoExtra.Hgt+1); } // Draw specified extra switch (Extra) { case C4MN_Extra_Value: { if (pDef) ::GraphicsResource.fctWealth.DrawValue(cgoExtra,iValue,0,0,C4FCT_Right); } break; } } void C4Menu::DrawFrame(C4Surface * sfcSurface, int32_t iX, int32_t iY, int32_t iWdt, int32_t iHgt) { pDraw->DrawFrameDw(sfcSurface, iX+1, iY+1, iX+iWdt-1,iY+iHgt-1, C4RGB(0x44, 0, 0)); } void C4Menu::SetAlignment(int32_t iAlignment) { Alignment = iAlignment; } void C4Menu::SetPermanent(bool fPermanent) { Permanent = fPermanent; } bool C4Menu::RefillInternal() { // Reset flag NeedRefill=false; // do the refill in frozen window (no scrolling update) int32_t iLastItemCount=ItemCount; bool fRefilled = false; pClientWindow->Freeze(); bool fSuccess = DoRefillInternal(fRefilled); pClientWindow->UnFreeze(); UpdateElementPositions(); if (!fSuccess) return false; // menu contents may have changed: Adjust menu size and selection if (fRefilled) { // Adjust selection AdjustSelection(); // Item count increased: resize if (ItemCount > iLastItemCount) LocationSet = false; // Item count decreased: resize if we are a context menu if ((ItemCount < iLastItemCount) && IsContextMenu()) LocationSet = false; } // Success return true; } void C4Menu::ClearItems() { C4MenuItem *pItem; while ((pItem = GetItem(0))) delete pItem; ItemCount=0; UpdateScrollBar(); } void C4Menu::Execute() { if (!IsActive()) return; // Refill (timer or flag) if (!Game.iTick35 || NeedRefill) if (!RefillInternal()) Close(false); // text progress if (fTextProgressing) SetTextProgress(+1, true); } bool C4Menu::Refill() { if (!IsActive()) return false; // Refill (close if failure) if (!RefillInternal()) { Close(false); return false; } // Success return true; } void C4Menu::AdjustSelection() { // selection valid? C4MenuItem *pSelection = GetItem(Selection); int iSel = Selection; if (!pSelection || !pSelection->IsSelectable) { // set to new first valid selection: Downwards first iSel = Selection; while (--iSel>=0) if ((pSelection = GetItem(iSel))) if (pSelection->IsSelectable) break; // no success: upwards then if (iSel<0) for (iSel=Selection+1; (pSelection = GetItem(iSel)); ++iSel) if (pSelection->IsSelectable) break; } // set it then if (!pSelection) SetSelection(-1, Selection >= 0, false); else SetSelection(iSel, iSel != Selection, true); } bool C4Menu::SetLocation(int32_t iX, int32_t iY) { // just set position... SetPos(iX, iY); return true; } bool C4Menu::SetTextProgress(int32_t iToProgress, bool fAdd) { // menu active at all? if (!IsActive()) return false; // set: enable or disable progress? if (!fAdd) fTextProgressing = (iToProgress >= 0); else { // add: Does not enable progressing if (!fTextProgressing) return false; } // update menu items C4MenuItem *pItem; bool fAnyItemUnfinished = false; for (int32_t i=HasPortrait(); (pItem = GetItem(i)); ++i) { // disabled progress: set all progresses to shown if (!fTextProgressing) { pItem->TextDisplayProgress = -1; continue; } // do progress on item, if any is left // this call automatically reduces iToProgress as it's used up if (!fAdd) pItem->TextDisplayProgress = 0; if (iToProgress) pItem->DoTextProgress(iToProgress); if (pItem->TextDisplayProgress > -1) fAnyItemUnfinished = true; } // if that progress showed everything already, mark as not progressing fTextProgressing = fAnyItemUnfinished; // done, success return true; } C4Viewport *C4Menu::GetViewport() { // ask all viewports for (C4Viewport *pVP = ::Viewports.GetFirstViewport(); pVP; pVP = pVP->GetNext()) if (pVP->IsViewportMenu(this)) return pVP; // none matching return nullptr; } void C4Menu::UpdateElementPositions() { // only if already shown and made visible by first drawing // this will postpone the call of menu initialization until it's really needed if (!IsVisible() || !pClientWindow) return; // reposition client scrolling window pClientWindow->SetBounds(GetContainedClientRect()); // re-stack all list items int xOff, yOff = 0; C4MenuItem *pCurr = static_cast(pClientWindow->GetFirst()), *pPrev = nullptr; if (HasPortrait() && pCurr) { // recheck portrait xOff = C4MN_DlgPortraitWdt + C4MN_DlgPortraitIndent; C4Facet &fctPortrait = pCurr->Symbol; C4Rect rcPortraitBounds(0,0, C4MN_DlgPortraitWdt + C4MN_DlgPortraitIndent, fctPortrait.Hgt * C4MN_DlgPortraitWdt / std::max(fctPortrait.Wdt, 1)); if (pCurr->GetBounds() != rcPortraitBounds) { pCurr->GetBounds() = rcPortraitBounds; pCurr->UpdateOwnPos(); } pCurr = static_cast(pCurr->GetNext()); } else xOff = 0; // recheck list items int32_t iMaxDlgOptionHeight = -1; int32_t iIndex = 0; C4Rect rcNewBounds(0,0,ItemWidth,ItemHeight); C4MenuItem *pFirstStack = pCurr, *pNext = pFirstStack; while ((pCurr = pNext)) { pNext = static_cast(pCurr->GetNext()); if (Style == C4MN_Style_Dialog) { // y-margin always, except between options if (!pPrev || (!pPrev->IsSelectable || !pCurr->IsSelectable)) yOff += C4MN_DlgLineMargin; else yOff += C4MN_DlgOptionLineMargin; // determine item height. StdStrBuf sText; int32_t iAssumedItemHeight = ::GraphicsResource.FontRegular.GetLineHeight(); int32_t iWdt, iAvailWdt = ItemWidth, iSymWdt; for (;;) { iSymWdt = std::min(pCurr->GetSymbolWidth(iAssumedItemHeight), iAvailWdt/2); iAvailWdt = ItemWidth - iSymWdt; ::GraphicsResource.FontRegular.BreakMessage(pCurr->Caption, iAvailWdt, &sText, true); ::GraphicsResource.FontRegular.GetTextExtent(sText.getData(),iWdt,rcNewBounds.Hgt, true); if (!iSymWdt || rcNewBounds.Hgt <= iAssumedItemHeight) break; // If there is a symbol, the symbol grows as more lines become available // Thus, less space is available for the text, and it might become larger iAssumedItemHeight = rcNewBounds.Hgt; } if (fEqualIconItemHeight && iSymWdt) { // force equal height for all symbol items if (iMaxDlgOptionHeight < 0) { // first selectable item inits field iMaxDlgOptionHeight = rcNewBounds.Hgt; } else if (rcNewBounds.Hgt <= iMaxDlgOptionHeight) { // following item height smaller or equal: Force equal rcNewBounds.Hgt = iMaxDlgOptionHeight; } else { // following item larger height: Need to re-stack from beginning iMaxDlgOptionHeight = rcNewBounds.Hgt; pNext = pFirstStack; pPrev = nullptr; yOff = 0; iIndex = 0; continue; } } assert(iWdt <= iAvailWdt); rcNewBounds.x = 0; rcNewBounds.y = yOff; yOff += rcNewBounds.Hgt; } else { rcNewBounds.x = (iIndex % std::max(Columns, 1)) * ItemWidth; rcNewBounds.y = (iIndex / std::max(Columns, 1)) * ItemHeight; } rcNewBounds.x += xOff; if (pCurr->GetBounds() != rcNewBounds) { pCurr->GetBounds() = rcNewBounds; pCurr->UpdateOwnPos(); } ++iIndex; pPrev = pCurr; } // update scrolling pClientWindow->SetClientHeight(rcNewBounds.y + rcNewBounds.Hgt); // re-set caption C4MenuItem *pSel = GetSelectedItem(); const char *szCapt; if (pSel && Style == C4MN_Style_Normal) szCapt = pSel->Caption; else szCapt = Caption; SetTitle((*szCapt || Style == C4MN_Style_Dialog) ? szCapt : " ", HasMouse()); } void C4Menu::UpdateOwnPos() { // client rect and stuff typedef C4GUI::Dialog ParentClass; ParentClass::UpdateOwnPos(); UpdateElementPositions(); } void C4Menu::UserSelectItem(int32_t Player, C4MenuItem *pItem) { // not if user con't control anything if (IsReadOnly()) return; // the item must be selectable if (!pItem || !pItem->IsSelectable) return; // queue or direct selection OnUserSelectItem(Player, pItem->iIndex); } void C4Menu::UserEnter(int32_t Player, C4MenuItem *pItem, bool fRight) { // not if user con't control anything if (IsReadOnly()) return; // the item must be selectable if (!pItem || !pItem->IsSelectable) return; // queue or direct enter OnUserEnter(Player, pItem->iIndex, fRight); } void C4Menu::UserClose(bool fOK) { // not if user con't control anything if (IsReadOnly()) return; // queue or direct enter OnUserClose(); } void C4Menu::SetCloseCommand(const char *strCommand) { CloseCommand.Copy(strCommand); } bool C4Menu::HasMouse() { int32_t iPlayer = GetControllingPlayer(); if (iPlayer == NO_OWNER) return true; // free view dialog also has the mouse C4Player *pPlr = ::Players.Get(iPlayer); if (pPlr && pPlr->MouseControl) return true; return false; } void C4Menu::ClearPointers(C4Object *pObj) { C4MenuItem *pItem; for (int32_t i=0; (pItem = GetItem(i)); ++i) pItem->ClearPointers(pObj); } #ifdef _DEBUG void C4Menu::AssertSurfaceNotUsed(C4Surface *sfc) { C4MenuItem *pItem; if (!sfc) return; assert(sfc != Symbol.Surface); for (int32_t i=0; (pItem = GetItem(i)); ++i) assert(pItem->Symbol.Surface != sfc) ; } #endif