openclonk/src/gui/C4Menu.cpp

1175 lines
35 KiB
C++

/*
* 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<int>(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<float>(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<int>(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<C4MenuItem *>(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<int32_t>(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<int32_t>(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<int>(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<int>(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<int>(cgoArea.Wdt - 2*C4MN_FrameWidth, std::max<int>(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<int>(ItemWidth - C4MN_DlgPortraitWdt - C4MN_DlgPortraitIndent, 40);
}
}
}
int DisplayedItemCount = ItemCount - HasPortrait();
if (Style == C4MN_Style_Dialog)
Lines = C4MN_DlgLines;
else
Lines = DisplayedItemCount/Columns+std::min<int32_t>(DisplayedItemCount%Columns,1);
// adjust by max. height
Lines=std::max<int32_t>(std::min<int32_t>((cgoArea.Hgt-100)/std::max<int32_t>(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<int32_t>(X,0,cgoArea.Wdt-rcBounds.Wdt); Y=Clamp<int32_t>(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<int>(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<C4MenuItem *>(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<int>(fctPortrait.Wdt, 1));
if (pCurr->GetBounds() != rcPortraitBounds)
{
pCurr->GetBounds() = rcPortraitBounds;
pCurr->UpdateOwnPos();
}
pCurr = static_cast<C4MenuItem *>(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<C4MenuItem *>(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<int32_t>(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<int32_t>(Columns, 1)) * ItemWidth;
rcNewBounds.y = (iIndex / std::max<int32_t>(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