1/*
2 * LegacyClonk
3 *
4 * Copyright (c) 1998-2000, Matthes Bender (RedWolf Design)
5 * Copyright (c) 2017-2022, The LegacyClonk Team and contributors
6 *
7 * Distributed under the terms of the ISC license; see accompanying file
8 * "COPYING" for details.
9 *
10 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
11 * See accompanying file "TRADEMARK" for details.
12 *
13 * To redistribute this file separately, substitute the full license texts
14 * for the above references.
15 */
16
17/* In-game menu as used by objects, players, and fullscreen options */
18
19#include <C4Menu.h>
20
21#include <C4Object.h>
22#include "StdMarkup.h"
23#include <C4FullScreen.h>
24#include <C4ObjectCom.h>
25#include <C4Viewport.h>
26#include <C4Wrappers.h>
27#include <C4Player.h>
28
29const int32_t C4MN_DefInfoWdt = 270, // default width of info windows
30 C4MN_DlgWdt = 270, // default width of dialog windows
31 C4MN_DlgLines = 5, // default number of text lines visible in a dialog window
32 C4MN_DlgLineMargin = 5, // px distance between text items
33 C4MN_DlgOptionLineMargin = 3, // px distance between dialog option items
34 C4MN_DlgPortraitWdt = 64, // size of portrait
35 C4MN_DlgPortraitIndent = 5; // space between portrait and text
36
37const int32_t C4MN_InfoCaption_Delay = 90;
38
39/* Obsolete helper function still used by CreateMenu(iSymbol) */
40
41#include <C4ObjectMenu.h> // only needed for menu symbol constants below
42
43void DrawMenuSymbol(int32_t iMenu, C4Facet &cgo, int32_t iOwner, C4Object *cObj)
44{
45 C4Facet ccgo;
46
47 uint32_t dwColor = 0;
48 if (ValidPlr(plr: iOwner)) dwColor = Game.Players.Get(iPlayer: iOwner)->ColorDw;
49
50 switch (iMenu)
51 {
52 case C4MN_Construction:
53 {
54 C4Def *pDef;
55 if (pDef = C4Id2Def(id: C4Id(str: "CXCN")))
56 pDef->Draw(cgo);
57 else if (pDef = C4Id2Def(id: C4Id(str: "WKS1")))
58 pDef->Draw(cgo);
59 }
60 break;
61 case C4MN_Buy:
62 Game.GraphicsResource.fctFlagClr.DrawClr(cgo&: ccgo = cgo.GetFraction(percentWdt: 75, percentHgt: 75), fAspect: true, dwClr: dwColor);
63 Game.GraphicsResource.fctWealth.Draw(cgo&: ccgo = cgo.GetFraction(percentWdt: 100, percentHgt: 50, alignX: C4FCT_Left, alignY: C4FCT_Bottom));
64 Game.GraphicsResource.fctArrow.Draw(cgo&: ccgo = cgo.GetFraction(percentWdt: 70, percentHgt: 70, alignX: C4FCT_Right, alignY: C4FCT_Center), fAspect: false, iPhaseX: 0);
65 break;
66 case C4MN_Sell:
67 Game.GraphicsResource.fctFlagClr.DrawClr(cgo&: ccgo = cgo.GetFraction(percentWdt: 75, percentHgt: 75), fAspect: true, dwClr: dwColor);
68 Game.GraphicsResource.fctWealth.Draw(cgo&: ccgo = cgo.GetFraction(percentWdt: 100, percentHgt: 50, alignX: C4FCT_Left, alignY: C4FCT_Bottom));
69 Game.GraphicsResource.fctArrow.Draw(cgo&: ccgo = cgo.GetFraction(percentWdt: 70, percentHgt: 70, alignX: C4FCT_Right, alignY: C4FCT_Center), fAspect: false, iPhaseX: 1);
70 break;
71 }
72}
73
74// C4MenuItem
75
76C4MenuItem::C4MenuItem(C4Menu *pMenu, int32_t iIndex, const char *szCaption,
77 const char *szCommand, int32_t iCount, C4Object *pObject, const char *szInfoCaption,
78 C4ID idID, const char *szCommand2, bool fOwnValue, int32_t iValue, int32_t iStyle, bool fIsSelectable)
79 : C4GUI::Element(), Count(iCount), id(idID), Object(pObject), fOwnValue(fOwnValue),
80 iValue(iValue), fSelected(false), iStyle(iStyle), pMenu(pMenu), iIndex(iIndex),
81 IsSelectable(fIsSelectable), TextDisplayProgress(-1), dwSymbolClr(0u)
82{
83 *Caption = *Command = *Command2 = *InfoCaption = 0;
84 Symbol.Default();
85 SCopy(szSource: szCaption, sTarget: Caption, iMaxL: C4MaxTitle);
86 SCopy(szSource: szCommand, sTarget: Command, _MAX_FNAME + 30);
87 SCopy(szSource: szCommand2, sTarget: Command2, _MAX_FNAME + 30);
88 SCopy(szSource: szInfoCaption, sTarget: InfoCaption, iMaxL: C4MaxTitle);
89 // some info caption corrections
90 SReplaceChar(str: InfoCaption, fc: 10, tc: ' '); SReplaceChar(str: InfoCaption, fc: 13, tc: '|');
91 SetToolTip(InfoCaption);
92 // components initialization
93 if (idID)
94 {
95 C4Def *pDef = C4Id2Def(id: idID);
96 if (pDef) pDef->GetComponents(pOutList: &Components, pObjInstance: nullptr, pBuilder: pMenu ? pMenu->GetParentObject() : nullptr);
97 }
98}
99
100C4MenuItem::~C4MenuItem()
101{
102 Symbol.Clear();
103}
104
105void C4MenuItem::DoTextProgress(int32_t &riByVal)
106{
107 // any progress to be done?
108 if (TextDisplayProgress < 0) return;
109 // if this is an option or empty text, show it immediately
110 if (IsSelectable || !*Caption) { TextDisplayProgress = -1; return; }
111 // normal text: move forward in unbroken message, ignoring markup
112 StdStrBuf sText(Caption, false);
113 CMarkup MarkupChecker(false);
114 const char *szPos = sText.getPtr(i: std::min<int>(a: TextDisplayProgress, b: sText.getLength()));
115 while (riByVal && *szPos)
116 {
117 MarkupChecker.SkipTags(ppText: &szPos);
118 if (!*szPos) break;
119 --riByVal;
120 ++szPos;
121 }
122 if (!*szPos)
123 TextDisplayProgress = -1;
124 else
125 TextDisplayProgress = szPos - Caption;
126}
127
128bool C4MenuItem::IsDragElement()
129{
130 // any constructibles can be dragged
131 C4Def *pDef = C4Id2Def(id);
132 return pDef && pDef->Constructable;
133}
134
135int32_t C4MenuItem::GetSymbolWidth(int32_t iForHeight)
136{
137 // Context or dialog menus
138 if (iStyle == C4MN_Style_Context || (iStyle == C4MN_Style_Dialog && Symbol.Surface))
139 return iForHeight;
140 // Info menus
141 if (iStyle == C4MN_Style_Info && Symbol.Surface && Symbol.Wdt)
142 return std::min(a: Symbol.Wdt, b: C4PictureSize);
143 // no symbol
144 return 0;
145}
146
147void C4MenuItem::DrawElement(C4FacetEx &cgo)
148{
149 // get target pos
150 C4Facet cgoOut(cgo.Surface, cgo.TargetX + rcBounds.x, cgo.TargetY + rcBounds.y, rcBounds.Wdt, rcBounds.Hgt);
151 // Select mark
152 if (iStyle != C4MN_Style_Info)
153 if (fSelected && TextDisplayProgress)
154 Application.DDraw->DrawBox(sfcDest: cgo.Surface, iX1: cgoOut.X, iY1: cgoOut.Y, iX2: cgoOut.X + cgoOut.Wdt - 1, iY2: cgoOut.Y + cgoOut.Hgt - 1, byCol: CRed);
155 // Symbol/text areas
156 C4Facet cgoItemSymbol, cgoItemText;
157 cgoItemSymbol = cgoItemText = cgoOut;
158 int32_t iSymWidth;
159 if (iSymWidth = GetSymbolWidth(iForHeight: cgoItemText.Hgt))
160 {
161 // get symbol area
162 cgoItemSymbol = cgoItemText.Truncate(iAlign: C4FCT_Left, iSize: iSymWidth);
163 }
164 // Draw item symbol:
165 // Draw if there is no text progression at all (TextDisplayProgress==-1, or if it's progressed far enough already (TextDisplayProgress>0)
166 if (Symbol.Surface && TextDisplayProgress) Symbol.DrawClr(cgo&: cgoItemSymbol, fAspect: true, dwClr: dwSymbolClr);
167 // Draw item text
168 Application.DDraw->StorePrimaryClipper(); Application.DDraw->SubPrimaryClipper(iX1: cgoItemText.X, iY1: cgoItemText.Y, iX2: cgoItemText.X + cgoItemText.Wdt - 1, iY2: cgoItemText.Y + cgoItemText.Hgt - 1);
169 switch (iStyle)
170 {
171 case C4MN_Style_Context:
172 Application.DDraw->TextOut(szText: Caption, rFont&: Game.GraphicsResource.FontRegular, fZoom: 1.0, sfcDest: cgoItemText.Surface, iTx: cgoItemText.X, iTy: cgoItemText.Y, dwFCol: CStdDDraw::DEFAULT_MESSAGE_COLOR, byForm: ALeft);
173 break;
174 case C4MN_Style_Info:
175 {
176 StdStrBuf sText;
177 Game.GraphicsResource.FontRegular.BreakMessage(szMsg: InfoCaption, iWdt: cgoItemText.Wdt, pOut: &sText, fCheckMarkup: true);
178 Application.DDraw->TextOut(szText: sText.getData(), rFont&: Game.GraphicsResource.FontRegular, fZoom: 1.0, sfcDest: cgoItemText.Surface, iTx: cgoItemText.X, iTy: cgoItemText.Y);
179 break;
180 }
181 case C4MN_Style_Dialog:
182 {
183 // cut buffer at text display pos
184 char cXChg = '\0'; int iStopPos;
185 if (TextDisplayProgress > -1)
186 {
187 iStopPos = std::min<int>(a: TextDisplayProgress, b: strlen(s: Caption));
188 cXChg = Caption[iStopPos];
189 Caption[iStopPos] = '\0';
190 }
191 // display broken text
192 StdStrBuf sText;
193 Game.GraphicsResource.FontRegular.BreakMessage(szMsg: Caption, iWdt: cgoItemText.Wdt, pOut: &sText, fCheckMarkup: true);
194 Application.DDraw->TextOut(szText: sText.getData(), rFont&: Game.GraphicsResource.FontRegular, fZoom: 1.0, sfcDest: cgoItemText.Surface, iTx: cgoItemText.X, iTy: cgoItemText.Y);
195 // restore complete text
196 if (cXChg) Caption[iStopPos] = cXChg;
197 break;
198 }
199 }
200 Application.DDraw->RestorePrimaryClipper();
201 // Draw count
202 if (Count != C4MN_Item_NoCount)
203 {
204 std::array<char, C4Strings::NumberOfCharactersForDigits<decltype(Count)> + 1 + 1> buf;
205 char *const ptr{std::to_chars(first: buf.data(), last: buf.data() + buf.size() - 2, value: Count).ptr};
206 ptr[0] = 'x';
207 ptr[1] = '\0';
208 std::to_chars(first: buf.data(), last: buf.data() + buf.size() - 2, value: Count);
209 Application.DDraw->TextOut(szText: buf.data(), rFont&: Game.GraphicsResource.FontRegular, fZoom: 1.0, sfcDest: cgoItemText.Surface, iTx: cgoItemText.X + cgoItemText.Wdt - 1, iTy: cgoItemText.Y + cgoItemText.Hgt - 1 - Game.GraphicsResource.FontRegular.GetLineHeight(), dwFCol: CStdDDraw::DEFAULT_MESSAGE_COLOR, byForm: ARight);
210 }
211}
212
213void C4MenuItem::MouseInput(C4GUI::CMouse &rMouse, int32_t iButton, int32_t iX, int32_t iY, uint32_t dwKeyParam)
214{
215 // clicky clicky!
216 if (iButton == C4MC_Button_LeftDown)
217 {
218 // button down: Init drag only; Enter selection only by button up
219 if (IsDragElement())
220 StartDragging(rMouse, iX, iY, dwKeyParam);
221 }
222 else if (iButton == C4MC_Button_LeftUp)
223 {
224 // left-click performed
225 pMenu->UserEnter(Player: Game.MouseControl.GetPlayer(), pItem: this, fRight: false);
226 return;
227 }
228 else if (iButton == C4MC_Button_RightUp)
229 {
230 // right-up: Alternative enter command
231 pMenu->UserEnter(Player: Game.MouseControl.GetPlayer(), pItem: this, fRight: true);
232 return;
233 }
234 // inherited; this is just setting some vars
235 typedef C4GUI::Element ParentClass;
236 ParentClass::MouseInput(rMouse, iButton, iX, iY, dwKeyParam);
237}
238
239void C4MenuItem::MouseEnter(C4GUI::CMouse &rMouse)
240{
241 // callback to menu: Select item
242 pMenu->UserSelectItem(Player: Game.MouseControl.GetPlayer(), pItem: this);
243 typedef C4GUI::Element ParentClass;
244 ParentClass::MouseEnter(rMouse);
245}
246
247void C4MenuItem::DoDragging(C4GUI::CMouse &rMouse, int32_t iX, int32_t iY, uint32_t dwKeyParam)
248{
249 // is this a drag element?
250 if (!IsDragElement()) { rMouse.pDragElement = nullptr; }
251 // check if outside drag range
252 if ((std::max)(a: Abs(val: iX - iDragX), b: Abs(val: iY - iDragY)) >= C4MC_DragSensitivity)
253 {
254 // then do a drag!
255 Game.MouseControl.StartConstructionDrag(id);
256 // this disables the window: Release mouse
257 rMouse.ReleaseButtons();
258 rMouse.pDragElement = nullptr;
259 rMouse.pMouseOverElement = nullptr;
260 }
261}
262
263void C4MenuItem::StopDragging(C4GUI::CMouse &rMouse, int32_t iX, int32_t iY, uint32_t dwKeyParam)
264{
265 // drag stop: Nothing to do, really
266 // Mouse up will be processed by regular procedure
267 rMouse.pDragElement = nullptr;
268}
269
270// C4Menu
271
272C4Menu::C4Menu() : C4GUI::Dialog(100, 100, nullptr, true) // will be re-adjusted later
273{
274 Default();
275 AddElement(pChild: pClientWindow = new C4GUI::ScrollWindow(this));
276 // initially invisible: Will be made visible at first drawing by viewport
277 // when the location will be inialized
278 SetVisibility(false);
279 LastSelection = -1;
280}
281
282void C4Menu::Default()
283{
284 Selection = -1;
285 Style = C4MN_Style_Normal;
286 ItemCount = 0;
287 ItemWidth = ItemHeight = C4SymbolSize;
288 NeedRefill = false;
289 Symbol.Default();
290 Caption[0] = 0;
291 Permanent = false;
292 Extra = C4MN_Extra_None;
293 ExtraData = 0;
294 DrawMenuControls = Config.Graphics.ShowCommands;
295 TimeOnSelection = 0;
296 Identification = 0;
297 LocationSet = false;
298 Columns = Lines = 0;
299 Alignment = C4MN_Align_Right | C4MN_Align_Bottom;
300 VisibleCount = 0;
301 fHasPortrait = false;
302 fTextProgressing = false;
303 fEqualIconItemHeight = false;
304 CloseCommand.Clear();
305 fActive = false;
306}
307
308void C4Menu::Clear()
309{
310 Close(fOK: false);
311 Symbol.Clear();
312 ClearItems();
313 ClearFrameDeco();
314 fActive = false;
315}
316
317bool C4Menu::TryClose(bool fOK, bool fControl)
318{
319 // abort if menu is permanented by script
320 if (!fOK) if (IsCloseDenied()) return false;
321
322 // close the menu
323 Close(fOK);
324 Clear();
325 if (Game.pGUI) Game.pGUI->RemoveElement(pChild: this);
326
327 // invoke close command
328 if (fControl && CloseCommand.getData())
329 {
330 MenuCommand(szCommand: CloseCommand.getData(), fIsCloseCommand: true);
331 }
332
333 // done
334 return true;
335}
336
337bool C4Menu::DoInit(C4FacetExSurface &fctSymbol, const char *szEmpty, int32_t iExtra, int32_t iExtraData, int32_t iId, int32_t iStyle)
338{
339 Clear(); Default();
340 Symbol.GrabFrom(rSource&: fctSymbol);
341 return InitMenu(szEmpty, iExtra, iExtraData, iId, iStyle);
342}
343
344bool C4Menu::DoInitRefSym(const C4FacetEx &fctSymbol, const char *szEmpty, int32_t iExtra, int32_t iExtraData, int32_t iId, int32_t iStyle)
345{
346 Clear(); Default();
347 Symbol.Set(fctSymbol);
348 return InitMenu(szEmpty, iExtra, iExtraData, iId, iStyle);
349}
350
351bool C4Menu::InitMenu(const char *szEmpty, int32_t iExtra, int32_t iExtraData, int32_t iId, int32_t iStyle)
352{
353 SCopy(szSource: szEmpty, sTarget: Caption, iMaxL: C4MaxTitle);
354 Extra = iExtra; ExtraData = iExtraData;
355 Identification = iId;
356 if (*Caption || iStyle == C4MN_Style_Dialog) SetTitle(szToTitle: Caption, fShowCloseButton: HasMouse()); else SetTitle(szToTitle: " ", fShowCloseButton: HasMouse());
357 if (pTitle) pTitle->SetIcon(Symbol);
358 Style = iStyle & C4MN_Style_BaseMask;
359 // Menus are synchronous to allow COM_MenuUp/Down to be converted to movements at the clients
360 if (Style == C4MN_Style_Normal)
361 Columns = 5;
362 else
363 // in reality, Dialog menus may have two coloumns (first for the portrait)
364 // however, they are not uniformly spaced and stuff; so they are better just ignored and handled by the drawing routine
365 Columns = 1;
366 if (iStyle & C4MN_Style_EqualItemHeight) SetEqualItemHeight(true);
367 if (Style == C4MN_Style_Dialog) Alignment = C4MN_Align_Top;
368 if (Style == C4MN_Style_Dialog) DrawMenuControls = 0;
369 if (Game.pGUI) Game.pGUI->ShowDialog(pDlg: this, fFade: false);
370 fTextProgressing = false;
371 fActive = true;
372 return true;
373}
374
375bool C4Menu::AddRefSym(const char *szCaption, const C4FacetEx &fctSymbol, const char *szCommand,
376 int32_t iCount, C4Object *pObject, const char *szInfoCaption,
377 C4ID idID, const char *szCommand2, bool fOwnValue, int32_t iValue, bool fIsSelectable)
378{
379 if (!IsActive()) return false;
380 // Create new menu item
381 C4MenuItem *pNew = new C4MenuItem(this, ItemCount, szCaption, szCommand, iCount, pObject, szInfoCaption, idID, szCommand2, fOwnValue, iValue, Style, fIsSelectable);
382 // Ref Symbol
383 pNew->RefSymbol(fctSymbol);
384 // Add
385 return AddItem(pNew, szCaption, szCommand, iCount, pObject, szInfoCaption, idID, szCommand2, fOwnValue, iValue, fIsSelectable);
386}
387
388bool C4Menu::Add(const char *szCaption, C4FacetExSurface &fctSymbol, const char *szCommand,
389 int32_t iCount, C4Object *pObject, const char *szInfoCaption,
390 C4ID idID, const char *szCommand2, bool fOwnValue, int32_t iValue, bool fIsSelectable)
391{
392 if (!IsActive()) return false;
393 // Create new menu item
394 C4MenuItem *pNew = new C4MenuItem(this, ItemCount, szCaption, szCommand, iCount, pObject, szInfoCaption, idID, szCommand2, fOwnValue, iValue, Style, fIsSelectable);
395 // Set Symbol
396 pNew->GrabSymbol(fctSymbol);
397 // Add
398 return AddItem(pNew, szCaption, szCommand, iCount, pObject, szInfoCaption, idID, szCommand2, fOwnValue, iValue, fIsSelectable);
399}
400
401bool C4Menu::AddItem(C4MenuItem *pNew, const char *szCaption, const char *szCommand,
402 int32_t iCount, C4Object *pObject, const char *szInfoCaption,
403 C4ID idID, const char *szCommand2, bool fOwnValue, int32_t iValue, bool fIsSelectable)
404{
405#ifdef DEBUGREC_MENU
406 if (pObject)
407 {
408 C4RCMenuAdd rc = { pObject ? pObject->Number : -1, iCount, idID, fOwnValue, iValue, fIsSelectable };
409 AddDbgRec(RCT_MenuAdd, &rc, sizeof(C4RCMenuAdd));
410 if (szCommand) AddDbgRec(RCT_MenuAddC, szCommand, strlen(szCommand) + 1);
411 if (szCommand2) AddDbgRec(RCT_MenuAddC, szCommand2, strlen(szCommand2) + 1);
412 }
413#endif
414 // Add it to the list
415 pClientWindow->AddElement(pChild: pNew);
416 // first menuitem is portrait, if it does not have text but a facet
417 if (!ItemCount && (!szCaption || !*szCaption))
418 fHasPortrait = true;
419 // Item count
420 ItemCount++;
421 // set new item size
422 if (!pClientWindow->IsFrozen()) UpdateElementPositions();
423 // Init selection if not frozen
424 if (Selection == -1 && fIsSelectable && !pClientWindow->IsFrozen()) SetSelection(iSelection: ItemCount - 1, fAdjustPosition: false, fDoCalls: false);
425 // initial progress
426 if (fTextProgressing) pNew->TextDisplayProgress = 0;
427 // adjust scrolling, etc.
428 UpdateScrollBar();
429 // Success
430 return true;
431}
432
433bool C4Menu::Control(uint8_t byCom, int32_t iData)
434{
435 if (!IsActive()) return false;
436
437 switch (byCom)
438 {
439 case COM_MenuEnter: Enter(); break;
440 case COM_MenuEnterAll: Enter(fRight: true); break;
441 case COM_MenuClose: TryClose(fOK: false, fControl: true); break;
442
443 // organize with nicer subfunction...
444 case COM_MenuLeft:
445 // Top wrap-around
446 if (Selection - 1 < 0)
447 MoveSelection(iBy: ItemCount - 1 - Selection, fAdjustPosition: true, fDoCalls: true);
448 else
449 MoveSelection(iBy: -1, fAdjustPosition: true, fDoCalls: true);
450 break;
451 case COM_MenuRight:
452 // Bottom wrap-around
453 if (Selection + 1 >= ItemCount)
454 MoveSelection(iBy: -Selection, fAdjustPosition: true, fDoCalls: true);
455 else
456 MoveSelection(iBy: +1, fAdjustPosition: true, fDoCalls: true);
457 break;
458 case COM_MenuUp:
459 iData = -Columns;
460 // Top wrap-around
461 if (Selection + iData < 0)
462 while (Selection + iData + Columns < ItemCount)
463 iData += Columns;
464 MoveSelection(iBy: iData, fAdjustPosition: true, fDoCalls: true);
465 break;
466 case COM_MenuDown:
467 iData = +Columns;
468 // Bottom wrap-around
469 if (Selection + iData >= ItemCount)
470 while (Selection + iData - Columns >= 0)
471 iData -= Columns;
472 MoveSelection(iBy: iData, fAdjustPosition: true, fDoCalls: true);
473 break;
474 case COM_MenuSelect:
475 if (ItemCount)
476 SetSelection(iSelection: iData & (~C4MN_AdjustPosition), fAdjustPosition: !!(iData & C4MN_AdjustPosition), fDoCalls: true);
477 break;
478 case COM_MenuShowText:
479 SetTextProgress(iToProgress: -1, fAdd: false);
480 break;
481 }
482
483 return true;
484}
485
486bool C4Menu::KeyControl(uint8_t byCom)
487{
488 // direct keyboard callback
489 if (!IsActive()) return false;
490 return !!Control(byCom, iData: 0);
491}
492
493bool C4Menu::IsActive()
494{
495 return fActive;
496}
497
498bool C4Menu::Enter(bool fRight)
499{
500 // Not active
501 if (!IsActive()) return false;
502 if (Style == C4MN_Style_Info) return false;
503 // Get selected item
504 C4MenuItem *pItem = GetSelectedItem();
505 if (!pItem)
506 {
507 // okay for dialogs: Just close them
508 if (Style == C4MN_Style_Dialog) TryClose(fOK: false, fControl: true);
509 return true;
510 }
511 // Copy command to buffer (menu might be cleared)
512 char szCommand[_MAX_FNAME + 30 + 1];
513 SCopy(szSource: pItem->Command, sTarget: szCommand);
514 if (fRight && pItem->Command2[0]) SCopy(szSource: pItem->Command2, sTarget: szCommand);
515
516 // Close if not permanent
517 if (!Permanent) { Close(fOK: true); fActive = false; }
518
519 // exec command (note that menu callback may delete this!)
520 MenuCommand(szCommand, fIsCloseCommand: false);
521
522 return true;
523}
524
525C4MenuItem *C4Menu::GetItem(int32_t iIndex)
526{
527 return static_cast<C4MenuItem *>(pClientWindow->GetElementByIndex(i: iIndex));
528}
529
530int32_t C4Menu::GetItemCount()
531{
532 return ItemCount;
533}
534
535bool C4Menu::MoveSelection(int32_t iBy, bool fAdjustPosition, bool fDoCalls)
536{
537 if (!iBy) return false;
538 // find next item that can be selected by moving in iBy steps
539 int32_t iNewSel = Selection;
540 for (;;)
541 {
542 // determine new selection
543 iNewSel += iBy;
544 // selection is out of menu range
545 if (!Inside<int32_t>(ival: iNewSel, lbound: 0, rbound: ItemCount - 1)) return false;
546 // determine newly selected item
547 C4MenuItem *pNewSel = GetItem(iIndex: iNewSel);
548 // nothing selectable
549 if (!pNewSel || !pNewSel->IsSelectable) continue;
550 // got something: go select it
551 break;
552 }
553 // select it
554 return !!SetSelection(iSelection: iNewSel, fAdjustPosition, fDoCalls);
555}
556
557bool C4Menu::SetSelection(int32_t iSelection, bool fAdjustPosition, bool fDoCalls)
558{
559 // Not active
560 if (!IsActive()) return false;
561 // Outside Limits / Selectable
562 C4MenuItem *pNewSel = GetItem(iIndex: iSelection);
563 if ((iSelection == -1 && !ItemCount) || (pNewSel && pNewSel->IsSelectable))
564 {
565 // Selection change
566 if (iSelection != Selection)
567 {
568 // calls
569 C4MenuItem *pSel = GetSelectedItem();
570 if (pSel) pSel->SetSelected(false);
571 // Set selection
572 Selection = iSelection;
573 // Reset time on selection
574 TimeOnSelection = 0;
575 }
576 // always recheck selection for internal refill
577 C4MenuItem *pSel = GetSelectedItem();
578 if (pSel) pSel->SetSelected(true);
579 // set main caption by selection
580 if (Style == C4MN_Style_Normal)
581 {
582 if (pSel)
583 SetTitle(szToTitle: *(pSel->Caption) ? pSel->Caption : " ", fShowCloseButton: HasMouse());
584 else
585 SetTitle(szToTitle: *Caption ? Caption : " ", fShowCloseButton: HasMouse());
586 }
587 }
588 // adjust position, if desired
589 if (fAdjustPosition) AdjustPosition();
590 // do selection callback
591 if (fDoCalls) OnSelectionChanged(iNewSelection: Selection);
592 // Done
593 return true;
594}
595
596C4MenuItem *C4Menu::GetSelectedItem()
597{
598 return GetItem(iIndex: Selection);
599}
600
601void C4Menu::AdjustPosition()
602{
603 // Adjust position by selection (works only after InitLocation)
604 if ((Lines > 1) && Columns)
605 {
606 C4MenuItem *pSel = GetSelectedItem();
607 if (pSel)
608 pClientWindow->ScrollRangeInView(iY: pSel->GetBounds().y, iHgt: pSel->GetBounds().Hgt);
609 }
610}
611
612int32_t C4Menu::GetSelection()
613{
614 return Selection;
615}
616
617int C4Menu::GetSymbolSize()
618{
619 return static_cast<float>((Style == C4MN_Style_Dialog) ? 64 : C4SymbolSize) * Application.GetScale();
620}
621
622bool C4Menu::SetPosition(int32_t iPosition)
623{
624 if (!IsActive()) return false;
625 // update scroll pos, if location is initialized
626 if (IsVisible() && pClientWindow) pClientWindow->SetScroll((iPosition / Columns) * ItemHeight);
627 return true;
628}
629
630int32_t C4Menu::GetIdentification()
631{
632 return Identification;
633}
634
635void C4Menu::SetSize(int32_t iToWdt, int32_t iToHgt)
636{
637 if (iToWdt) Columns = iToWdt;
638 if (iToHgt) Lines = iToHgt;
639 InitSize();
640}
641
642void C4Menu::InitLocation(C4Facet &cgoArea)
643{
644 // Item size by style
645 switch (Style)
646 {
647 case C4MN_Style_Normal:
648 ItemWidth = ItemHeight = C4SymbolSize;
649 break;
650 case C4MN_Style_Context:
651 {
652 ItemHeight = std::max<int32_t>(a: C4MN_SymbolSize, b: Game.GraphicsResource.FontRegular.GetLineHeight());
653 int32_t iWdt, iHgt;
654 Game.GraphicsResource.FontRegular.GetTextExtent(szText: Caption, rsx&: ItemWidth, rsy&: iHgt, fCheckMarkup: true);
655 // FIXME: Blah. This stuff should be calculated correctly by pTitle.
656 ItemWidth += ItemHeight + 16;
657 C4MenuItem *pItem;
658 for (int i = 0; pItem = GetItem(iIndex: i); ++i)
659 {
660 Game.GraphicsResource.FontRegular.GetTextExtent(szText: pItem->Caption, rsx&: iWdt, rsy&: iHgt, fCheckMarkup: true);
661 ItemWidth = (std::max)(a: ItemWidth, b: iWdt + pItem->GetSymbolWidth(iForHeight: ItemHeight));
662 }
663 ItemWidth += 3; // Add some extra space so text doesn't touch right border frame...
664 break;
665 }
666 case C4MN_Style_Info:
667 {
668 // calculate size from a default size determined by a window width of C4MN_DefInfoWdt
669 int32_t iWdt, iHgt, iLargestTextWdt;
670 Game.GraphicsResource.FontRegular.GetTextExtent(szText: Caption, rsx&: iWdt, rsy&: iHgt, fCheckMarkup: true);
671 iLargestTextWdt = iWdt + 2 * C4MN_SymbolSize + C4MN_FrameWidth;
672 ItemWidth = std::min<int>(a: cgoArea.Wdt - 2 * C4MN_FrameWidth, b: (std::max)(a: iLargestTextWdt, b: C4MN_DefInfoWdt));
673 ItemHeight = 0;
674 StdStrBuf sText;
675 C4MenuItem *pItem;
676 for (int32_t i = 0; pItem = GetItem(iIndex: i); ++i)
677 {
678 Game.GraphicsResource.FontRegular.BreakMessage(szMsg: pItem->InfoCaption, iWdt: ItemWidth, pOut: &sText, fCheckMarkup: true);
679 Game.GraphicsResource.FontRegular.GetTextExtent(szText: sText.getData(), rsx&: iWdt, rsy&: iHgt, fCheckMarkup: true);
680 assert(iWdt <= ItemWidth);
681 ItemWidth = (std::max)(a: ItemWidth, b: iWdt); ItemHeight = (std::max)(a: ItemHeight, b: iHgt);
682 iLargestTextWdt = (std::max)(a: iLargestTextWdt, b: iWdt);
683 }
684 // although width calculation is done from C4MN_DefInfoWdt, this may be too large for some very tiny info windows
685 // so make sure no space is wasted
686 ItemWidth = (std::min)(a: ItemWidth, b: iLargestTextWdt);
687 // Add some extra space so text doesn't touch right border frame...
688 ItemWidth += 3;
689 // Now add some space to show the picture on the left
690 ItemWidth += C4PictureSize;
691 // And set a minimum item height (again, for the picture)
692 ItemHeight = std::max<int>(a: ItemHeight, b: C4PictureSize);
693 break;
694 }
695
696 case C4MN_Style_Dialog:
697 {
698 // dialog window: Item width is whole dialog, portrait subtracted if any
699 // Item height varies
700 int32_t iWdt, iHgt;
701 Game.GraphicsResource.FontRegular.GetTextExtent(szText: Caption, rsx&: iWdt, rsy&: iHgt, fCheckMarkup: true);
702 ItemWidth = std::min<int>(a: cgoArea.Wdt - 2 * C4MN_FrameWidth, b: std::max<int>(a: iWdt + 2 * C4MN_SymbolSize + C4MN_FrameWidth, b: C4MN_DlgWdt));
703 ItemHeight = iHgt; // Items may be multiline and higher
704 if (HasPortrait())
705 {
706 // subtract portrait only if this would not make the dialog too small
707 if (ItemWidth > C4MN_DlgPortraitWdt * 2 && cgoArea.Hgt > cgoArea.Wdt)
708 ItemWidth = std::max<int>(a: ItemWidth - C4MN_DlgPortraitWdt - C4MN_DlgPortraitIndent, b: 40);
709 }
710 }
711 }
712
713 int DisplayedItemCount = ItemCount - HasPortrait();
714 if (Style == C4MN_Style_Dialog)
715 Lines = C4MN_DlgLines;
716 else
717 Lines = DisplayedItemCount / Columns + std::min<int32_t>(a: DisplayedItemCount % Columns, b: 1);
718 // adjust by max. height
719 Lines = std::max<int32_t>(a: std::min<int32_t>(a: (cgoArea.Hgt - 100) / std::max<int32_t>(a: ItemHeight, b: 1), b: Lines), b: 1);
720
721 InitSize();
722 int32_t X, Y;
723 if (Alignment & C4MN_Align_Free)
724 {
725 X = rcBounds.x;
726 Y = rcBounds.y;
727 }
728 else
729 {
730 X = (cgoArea.Wdt - rcBounds.Wdt) / 2;
731 Y = (cgoArea.Hgt - rcBounds.Hgt) / 2;
732 }
733 // Alignment
734 if (Alignment & C4MN_Align_Left) X = C4SymbolSize;
735 if (Alignment & C4MN_Align_Right) X = cgoArea.Wdt - 2 * C4SymbolSize - rcBounds.Wdt;
736 if (Alignment & C4MN_Align_Top) Y = C4SymbolSize;
737 if (Alignment & C4MN_Align_Bottom) Y = cgoArea.Hgt - C4SymbolSize - rcBounds.Hgt;
738 if (Alignment & C4MN_Align_Free) { X = BoundBy<int32_t>(bval: X, lbound: 0, rbound: cgoArea.Wdt - rcBounds.Wdt); Y = BoundBy<int32_t>(bval: Y, lbound: 0, rbound: cgoArea.Hgt - rcBounds.Hgt); }
739 // Centered (due to small viewport size)
740 if (rcBounds.Wdt > cgoArea.Wdt - 2 * C4SymbolSize) X = (cgoArea.Wdt - rcBounds.Wdt) / 2;
741 if (rcBounds.Hgt > cgoArea.Hgt - 2 * C4SymbolSize) Y = (cgoArea.Hgt - rcBounds.Hgt) / 2;
742 SetPos(iXPos: X, iYPos: Y);
743
744 // Position initialized: Make it visible to be used!
745 SetVisibility(true);
746
747 // now align all menu items correctly
748 UpdateElementPositions();
749
750 // and correct scroll pos
751 UpdateScrollBar();
752 AdjustPosition();
753}
754
755void C4Menu::InitSize()
756{
757 C4GUI::Element *pLast = pClientWindow->GetLast();
758 // Size calculation
759 int Width, Height;
760 Width = Columns * ItemWidth;
761 Height = Lines * ItemHeight;
762 VisibleCount = Columns * Lines;
763 bool fBarNeeded;
764 if (HasPortrait()) Width += C4MN_DlgPortraitWdt + C4MN_DlgPortraitIndent;
765 // dialogs have auto-enlarge vertically
766 if (pLast && Style == C4MN_Style_Dialog)
767 {
768 Height = std::max<int>(a: Height, b: pLast->GetBounds().y + pLast->GetBounds().Hgt + C4MN_DlgLineMargin);
769 fBarNeeded = false;
770 }
771 else
772 fBarNeeded = pLast && pLast->GetBounds().y + pLast->GetBounds().Hgt > pClientWindow->GetBounds().Hgt;
773 // add dlg margins
774 Width += GetMarginLeft() + GetMarginRight() + pClientWindow->GetMarginLeft() + pClientWindow->GetMarginRight();
775 Height += GetMarginTop() + GetMarginBottom() + pClientWindow->GetMarginTop() + pClientWindow->GetMarginBottom();
776 if (fBarNeeded) Width += C4GUI_ScrollBarWdt;
777 SetBounds(C4Rect(rcBounds.x, rcBounds.y, Width, Height));
778 pClientWindow->SetScrollBarEnabled(fBarNeeded);
779 UpdateOwnPos();
780}
781
782void C4Menu::UpdateScrollBar()
783{
784 C4GUI::Element *pLast = pClientWindow->GetLast();
785 bool fBarNeeded = pLast && pLast->GetBounds().y + pLast->GetBounds().Hgt > pClientWindow->GetBounds().Hgt;
786 if (pClientWindow->IsScrollBarEnabled() == fBarNeeded) return;
787 // resize for bar
788 InitSize();
789}
790
791void C4Menu::Draw(C4FacetEx &cgo)
792{
793 // Inactive
794 if (!IsActive()) return;
795
796 // Location
797 if (!LocationSet) { InitLocation(cgoArea&: cgo); LocationSet = true; }
798
799 // If drawn by a viewport, then it's made visible
800 SetVisibility(true);
801
802 // do drawing
803 typedef C4GUI::Dialog ParentClass;
804 ParentClass::Draw(cgo);
805
806 // draw tooltip if selection time has been long enough
807 if (!fTextProgressing) ++TimeOnSelection;
808 if (TimeOnSelection >= C4MN_InfoCaption_Delay)
809 if (Style != C4MN_Style_Info) // No tooltips in info menus - doesn't make any sense...
810 if (!Game.Control.isReplay() && Game.pGUI)
811 if (!Game.pGUI->Mouse.IsActiveInput())
812 {
813 C4MenuItem *pSel = GetSelectedItem();
814 if (pSel && *pSel->InfoCaption)
815 {
816 int32_t iX = 0, iY = 0;
817 pSel->ClientPos2ScreenPos(riX&: iX, riY&: iY);
818 C4GUI::Screen::DrawToolTip(szTip: pSel->InfoCaption, cgo, x: iX, y: iY);
819 }
820 }
821}
822
823void C4Menu::DrawElement(C4FacetEx &cgo)
824{
825 // inherited (background)
826 typedef C4GUI::Dialog ParentClass;
827 ParentClass::DrawElement(cgo);
828
829 // Get selected item id
830 C4ID idSelected; C4MenuItem *pItem;
831 if (pItem = GetSelectedItem()) idSelected = pItem->id; else idSelected = C4ID_None;
832 C4Def *pDef = C4Id2Def(id: idSelected);
833 // Get item value
834 int32_t iValue;
835 if (pDef)
836 {
837 if (pItem && pItem->fOwnValue)
838 iValue = pItem->iValue;
839 else
840 iValue = pDef->GetValue(pInBase: nullptr, iBuyPlayer: NO_OWNER);
841 }
842
843 C4Facet cgoExtra(cgo.Surface, cgo.TargetX + rcBounds.x + 1, cgo.TargetY + rcBounds.y + rcBounds.Hgt - C4MN_SymbolSize - 1, rcBounds.Wdt - 2, C4MN_SymbolSize);
844
845 // Draw bar divider
846 if (Extra || DrawMenuControls)
847 {
848 DrawFrame(sfcSurface: cgoExtra.Surface, iX: cgoExtra.X - 1, iY: cgoExtra.Y - 1, iWdt: cgoExtra.Wdt + 1, iHgt: cgoExtra.Hgt + 1);
849 }
850
851 // Draw menu controls
852 if (DrawMenuControls)
853 {
854 C4Facet cgoControl;
855 // Determine player
856 int32_t iPlayer = GetControllingPlayer();
857 // Draw menu 'enter' command (unless info dialog)
858 if (Style != C4MN_Style_Info)
859 {
860 // Normal enter
861 cgoControl = cgoExtra.TruncateSection(iAlign: C4FCT_Left);
862 DrawCommandKey(cgo&: cgoControl, iCom: COM_Throw, fPressed: false, szText: PlrControlKeyName(iPlayer, iControl: Com2Control(iCom: COM_Throw), fShort: true).c_str());
863 cgoControl = cgoExtra.TruncateSection(iAlign: C4FCT_Left);
864 GfxR->fctOKCancel.Draw(cgo&: cgoControl, fAspect: true, iPhaseX: 0, iPhaseY: 0);
865 // Enter-all on Special2
866 if (pItem && pItem->Command2[0])
867 {
868 cgoControl = cgoExtra.TruncateSection(iAlign: C4FCT_Left);
869 DrawCommandKey(cgo&: cgoControl, iCom: COM_Special2, fPressed: false, szText: PlrControlKeyName(iPlayer, iControl: Com2Control(iCom: COM_Special2), fShort: true).c_str());
870 cgoControl = cgoExtra.TruncateSection(iAlign: C4FCT_Left);
871 GfxR->fctOKCancel.Draw(cgo&: cgoControl, fAspect: true, iPhaseX: 2, iPhaseY: 1);
872 }
873 }
874 // Draw menu 'close' command
875 cgoControl = cgoExtra.TruncateSection(iAlign: C4FCT_Left);
876 DrawCommandKey(cgo&: cgoControl, iCom: COM_Dig, fPressed: false, szText: PlrControlKeyName(iPlayer, iControl: Com2Control(iCom: COM_Dig), fShort: true).c_str());
877 cgoControl = cgoExtra.TruncateSection(iAlign: C4FCT_Left);
878 // Close command contains "Exit"? Show an exit symbol in the status bar.
879 if (SSearch(szString: CloseCommand.getData(), szIndex: "\"Exit\"")) GfxR->fctExit.Draw(cgo&: cgoControl);
880 else GfxR->fctOKCancel.Draw(cgo&: cgoControl, fAspect: true, iPhaseX: 1, iPhaseY: 0);
881 }
882
883 // live max magic
884 int32_t iUseExtraData = 0;
885 if (Extra == C4MN_Extra_LiveMagicValue || Extra == C4MN_Extra_ComponentsLiveMagic)
886 {
887 C4Object *pMagicSourceObj = Game.Objects.SafeObjectPointer(iNumber: ExtraData);
888 if (pMagicSourceObj) iUseExtraData = pMagicSourceObj->MagicEnergy / MagicPhysicalFactor;
889 }
890 else
891 {
892 iUseExtraData = ExtraData;
893 }
894
895 // Draw specified extra
896 switch (Extra)
897 {
898 case C4MN_Extra_Components:
899 if (pItem) pItem->Components.Draw(cgo&: cgoExtra, iSelection: -1, rDefs&: Game.Defs, dwCategory: C4D_All, fCounts: true, iAlign: C4FCT_Right | C4FCT_Triple | C4FCT_Half);
900 break;
901 case C4MN_Extra_Value:
902 {
903 if (pDef) Game.GraphicsResource.fctWealth.DrawValue(cgo&: cgoExtra, iValue, iPhaseX: 0, iPhaseY: 0, iAlign: C4FCT_Right);
904 // Flag parent object's owner's wealth display
905 C4Player *pParentPlr = Game.Players.Get(iPlayer: GetControllingPlayer());
906 if (pParentPlr) pParentPlr->ViewWealth = C4ViewDelay;
907 }
908 break;
909 case C4MN_Extra_MagicValue:
910 case C4MN_Extra_LiveMagicValue:
911 if (pDef)
912 {
913 Game.GraphicsResource.fctMagic.DrawValue2(cgo&: cgoExtra, iValue1: iValue, iValue2: iUseExtraData, iPhaseX: 0, iPhaseY: 0, iAlign: C4FCT_Right);
914 }
915 break;
916 case C4MN_Extra_ComponentsMagic:
917 case C4MN_Extra_ComponentsLiveMagic:
918 // magic value and components
919 if (pItem)
920 {
921 // DrawValue2 kills the facet...
922 int32_t iOriginalX = cgoExtra.X;
923 Game.GraphicsResource.fctMagic.DrawValue2(cgo&: cgoExtra, iValue1: iValue, iValue2: iUseExtraData, iPhaseX: 0, iPhaseY: 0, iAlign: C4FCT_Right);
924 cgoExtra.Wdt = cgoExtra.X - iOriginalX - 5;
925 cgoExtra.X = iOriginalX;
926 pItem->Components.Draw(cgo&: cgoExtra, iSelection: -1, rDefs&: Game.Defs, dwCategory: C4D_All, fCounts: true, iAlign: C4FCT_Right | C4FCT_Triple | C4FCT_Half);
927 }
928 break;
929 }
930}
931
932void C4Menu::DrawFrame(C4Surface *sfcSurface, int32_t iX, int32_t iY, int32_t iWdt, int32_t iHgt)
933{
934 lpDDraw->DrawFrame(sfcDest: sfcSurface, x1: iX + 1, y1: iY + 1, x2: iX + iWdt - 1, y2: iY + iHgt - 1, col: 80);
935}
936
937void C4Menu::SetAlignment(int32_t iAlignment)
938{
939 Alignment = iAlignment;
940}
941
942void C4Menu::SetPermanent(bool fPermanent)
943{
944 Permanent = fPermanent;
945}
946
947bool C4Menu::RefillInternal()
948{
949 // Reset flag
950 NeedRefill = false;
951
952 // do the refill in frozen window (no scrolling update)
953 int32_t iLastItemCount = ItemCount;
954 bool fRefilled = false;
955 pClientWindow->Freeze();
956 bool fSuccess = DoRefillInternal(rfRefilled&: fRefilled);
957 pClientWindow->UnFreeze();
958 UpdateElementPositions();
959 if (!fSuccess) return false;
960
961 // menu contents may have changed: Adjust menu size and selection
962 if (fRefilled)
963 {
964 // Adjust selection
965 AdjustSelection();
966 // Item count increased: resize
967 if (ItemCount > iLastItemCount) LocationSet = false;
968 // Item count decreased: resize if we are a context menu
969 if ((ItemCount < iLastItemCount) && IsContextMenu()) LocationSet = false;
970 }
971 // Success
972 return true;
973}
974
975void C4Menu::ClearItems(bool fResetSelection)
976{
977 C4MenuItem *pItem;
978 while (pItem = GetItem(iIndex: 0)) delete pItem;
979 ItemCount = 0;
980 if (fResetSelection)
981 {
982 // Remember selection for nested menus
983 LastSelection = Selection;
984 SetSelection(iSelection: -1, fAdjustPosition: true, fDoCalls: false);
985 LocationSet = false;
986 }
987 UpdateScrollBar();
988}
989
990void C4Menu::Execute()
991{
992 if (!IsActive()) return;
993 // Refill (timer or flag)
994 if (!Game.iTick35 || NeedRefill)
995 if (!RefillInternal())
996 Close(fOK: false);
997 // text progress
998 if (fTextProgressing)
999 SetTextProgress(iToProgress: +1, fAdd: true);
1000}
1001
1002bool C4Menu::Refill()
1003{
1004 if (!IsActive()) return false;
1005 // Refill (close if failure)
1006 if (!RefillInternal())
1007 {
1008 Close(fOK: false); return false;
1009 }
1010 // Success
1011 return true;
1012}
1013
1014void C4Menu::AdjustSelection()
1015{
1016 // selection valid?
1017 C4MenuItem *pSelection = GetItem(iIndex: Selection);
1018 int iSel = Selection;
1019 if (!pSelection || !pSelection->IsSelectable)
1020 {
1021 // set to new first valid selection: Downwards first
1022 iSel = Selection;
1023 while (--iSel >= 0)
1024 if (pSelection = GetItem(iIndex: iSel))
1025 if (pSelection->IsSelectable)
1026 break;
1027 // no success: upwards then
1028 if (iSel < 0)
1029 for (iSel = Selection + 1; pSelection = GetItem(iIndex: iSel); ++iSel)
1030 if (pSelection->IsSelectable)
1031 break;
1032 }
1033 // set it then
1034 if (!pSelection)
1035 SetSelection(iSelection: -1, fAdjustPosition: Selection >= 0, fDoCalls: false);
1036 else
1037 SetSelection(iSelection: iSel, fAdjustPosition: iSel != Selection, fDoCalls: true);
1038}
1039
1040bool C4Menu::ConvertCom(int32_t &rCom, int32_t &rData, bool fAsyncConversion)
1041{
1042 // This function converts normal Coms to menu Coms before they are send to the queue
1043
1044 // Menu not active
1045 if (!IsActive()) return false;
1046
1047 // Convert plain com control to menu com
1048 switch (rCom)
1049 {
1050 // Convert recognized menu coms
1051 case COM_Throw: rCom = COM_MenuEnter; break;
1052 case COM_Dig: rCom = COM_MenuClose; break;
1053 case COM_Special2: rCom = COM_MenuEnterAll; break;
1054 case COM_Up: rCom = COM_MenuUp; break;
1055 case COM_Left: rCom = COM_MenuLeft; break;
1056 case COM_Down: rCom = COM_MenuDown; break;
1057 case COM_Right: rCom = COM_MenuRight; break;
1058 // Not a menu com: do nothing
1059 default: return true;
1060 }
1061
1062 // If text is still progressing, any menu com will complete it first
1063 // Note: conversion to COM_MenuShowText is not synchronized because text lengths may vary
1064 // between clients. The above switch is used to determine whether the com was a menu com
1065 if (fTextProgressing && fAsyncConversion)
1066 rCom = COM_MenuShowText;
1067
1068 // Done
1069 return true;
1070}
1071
1072bool C4Menu::SetLocation(int32_t iX, int32_t iY)
1073{
1074 // just set position...
1075 SetPos(iXPos: iX, iYPos: iY);
1076 return true;
1077}
1078
1079bool C4Menu::SetTextProgress(int32_t iToProgress, bool fAdd)
1080{
1081 // menu active at all?
1082 if (!IsActive()) return false;
1083 // set: enable or disable progress?
1084 if (!fAdd)
1085 fTextProgressing = (iToProgress >= 0);
1086 else
1087 {
1088 // add: Does not enable progressing
1089 if (!fTextProgressing) return false;
1090 }
1091 // update menu items
1092 C4MenuItem *pItem;
1093 bool fAnyItemUnfinished = false;
1094 for (int32_t i = HasPortrait(); pItem = GetItem(iIndex: i); ++i)
1095 {
1096 // disabled progress: set all progresses to shown
1097 if (!fTextProgressing)
1098 {
1099 pItem->TextDisplayProgress = -1;
1100 continue;
1101 }
1102 // do progress on item, if any is left
1103 // this call automatically reduces iToProgress as it's used up
1104 if (!fAdd) pItem->TextDisplayProgress = 0;
1105 if (iToProgress) pItem->DoTextProgress(riByVal&: iToProgress);
1106 if (pItem->TextDisplayProgress > -1) fAnyItemUnfinished = true;
1107 }
1108 // if that progress showed everything already, mark as not progressing
1109 fTextProgressing = fAnyItemUnfinished;
1110 // done, success
1111 return true;
1112}
1113
1114C4Viewport *C4Menu::GetViewport()
1115{
1116 // ask all viewports
1117 for (const auto &pVP : Game.GraphicsSystem.GetViewports())
1118 if (pVP->IsViewportMenu(pMenu: this))
1119 return pVP.get();
1120 // none matching
1121 return nullptr;
1122}
1123
1124void C4Menu::UpdateElementPositions()
1125{
1126 // only if already shown and made visible by first drawing
1127 // this will postpone the call of menu initialization until it's really needed
1128 if (!IsVisible() || !pClientWindow) return;
1129 // reposition client scrolling window
1130 pClientWindow->SetBounds(GetContainedClientRect());
1131 // re-stack all list items
1132 int xOff, yOff = 0;
1133 C4MenuItem *pCurr = static_cast<C4MenuItem *>(pClientWindow->GetFirst()), *pPrev = nullptr;
1134 if (HasPortrait() && pCurr)
1135 {
1136 // recheck portrait
1137 xOff = C4MN_DlgPortraitWdt + C4MN_DlgPortraitIndent;
1138 C4FacetEx &fctPortrait = pCurr->Symbol;
1139 C4Rect rcPortraitBounds(0, 0, C4MN_DlgPortraitWdt + C4MN_DlgPortraitIndent, fctPortrait.Hgt * C4MN_DlgPortraitWdt / std::max<int>(a: fctPortrait.Wdt, b: 1));
1140 if (pCurr->GetBounds() != rcPortraitBounds)
1141 {
1142 pCurr->GetBounds() = rcPortraitBounds;
1143 pCurr->UpdateOwnPos();
1144 }
1145 pCurr = static_cast<C4MenuItem *>(pCurr->GetNext());
1146 }
1147 else xOff = 0;
1148 // recheck list items
1149 int32_t iMaxDlgOptionHeight = -1;
1150 int32_t iIndex = 0; C4Rect rcNewBounds(0, 0, ItemWidth, ItemHeight);
1151 C4MenuItem *pFirstStack = pCurr, *pNext = pFirstStack;
1152 while (pCurr = pNext)
1153 {
1154 pNext = static_cast<C4MenuItem *>(pCurr->GetNext());
1155 if (Style == C4MN_Style_Dialog)
1156 {
1157 // y-margin always, except between options
1158 if (!pPrev || (!pPrev->IsSelectable || !pCurr->IsSelectable)) yOff += C4MN_DlgLineMargin; else yOff += C4MN_DlgOptionLineMargin;
1159 // determine item height.
1160 StdStrBuf sText;
1161 int32_t iAssumedItemHeight = Game.GraphicsResource.FontRegular.GetLineHeight();
1162 int32_t iWdt, iAvailWdt = ItemWidth, iSymWdt;
1163 for (;;)
1164 {
1165 iSymWdt = std::min<int32_t>(a: pCurr->GetSymbolWidth(iForHeight: iAssumedItemHeight), b: iAvailWdt / 2);
1166 iAvailWdt = ItemWidth - iSymWdt;
1167 Game.GraphicsResource.FontRegular.BreakMessage(szMsg: pCurr->Caption, iWdt: iAvailWdt, pOut: &sText, fCheckMarkup: true);
1168 Game.GraphicsResource.FontRegular.GetTextExtent(szText: sText.getData(), rsx&: iWdt, rsy&: rcNewBounds.Hgt, fCheckMarkup: true);
1169 if (!iSymWdt || rcNewBounds.Hgt <= iAssumedItemHeight) break;
1170 // If there is a symbol, the symbol grows as more lines become available
1171 // Thus, less space is available for the text, and it might become larger
1172 iAssumedItemHeight = rcNewBounds.Hgt;
1173 }
1174 if (fEqualIconItemHeight && iSymWdt)
1175 {
1176 // force equal height for all symbol items
1177 if (iMaxDlgOptionHeight < 0)
1178 {
1179 // first selectable item inits field
1180 iMaxDlgOptionHeight = rcNewBounds.Hgt;
1181 }
1182 else if (rcNewBounds.Hgt <= iMaxDlgOptionHeight)
1183 {
1184 // following item height smaller or equal: Force equal
1185 rcNewBounds.Hgt = iMaxDlgOptionHeight;
1186 }
1187 else
1188 {
1189 // following item larger height: Need to re-stack from beginning
1190 iMaxDlgOptionHeight = rcNewBounds.Hgt;
1191 pNext = pFirstStack;
1192 pPrev = nullptr;
1193 yOff = 0;
1194 iIndex = 0;
1195 continue;
1196 }
1197 }
1198 assert(iWdt <= iAvailWdt);
1199 rcNewBounds.x = 0;
1200 rcNewBounds.y = yOff;
1201 yOff += rcNewBounds.Hgt;
1202 }
1203 else
1204 {
1205 rcNewBounds.x = (iIndex % std::max<int32_t>(a: Columns, b: 1)) * ItemWidth;
1206 rcNewBounds.y = (iIndex / std::max<int32_t>(a: Columns, b: 1)) * ItemHeight;
1207 }
1208 rcNewBounds.x += xOff;
1209 if (pCurr->GetBounds() != rcNewBounds)
1210 {
1211 pCurr->GetBounds() = rcNewBounds;
1212 pCurr->UpdateOwnPos();
1213 }
1214 ++iIndex;
1215 pPrev = pCurr;
1216 }
1217 // update scrolling
1218 pClientWindow->SetClientHeight(rcNewBounds.y + rcNewBounds.Hgt);
1219 // re-set caption
1220 C4MenuItem *pSel = GetSelectedItem();
1221 const char *szCapt;
1222 if (pSel && Style == C4MN_Style_Normal)
1223 szCapt = pSel->Caption;
1224 else
1225 szCapt = Caption;
1226 SetTitle(szToTitle: (*szCapt || Style == C4MN_Style_Dialog) ? szCapt : " ", fShowCloseButton: HasMouse());
1227}
1228
1229void C4Menu::UpdateOwnPos()
1230{
1231 // client rect and stuff
1232 typedef C4GUI::Dialog ParentClass;
1233 ParentClass::UpdateOwnPos();
1234 UpdateElementPositions();
1235}
1236
1237void C4Menu::UserSelectItem(int32_t Player, C4MenuItem *pItem)
1238{
1239 // not if user con't control anything
1240 if (IsReadOnly()) return;
1241 // the item must be selectable
1242 if (!pItem || !pItem->IsSelectable) return;
1243 // queue or direct selection
1244 OnUserSelectItem(Player, iIndex: pItem->iIndex);
1245}
1246
1247void C4Menu::UserEnter(int32_t Player, C4MenuItem *pItem, bool fRight)
1248{
1249 // not if user con't control anything
1250 if (IsReadOnly()) return;
1251 // the item must be selectable
1252 if (!pItem || !pItem->IsSelectable) return;
1253 // queue or direct enter
1254 OnUserEnter(Player, iIndex: pItem->iIndex, fRight);
1255}
1256
1257void C4Menu::UserClose(bool fOK)
1258{
1259 // not if user con't control anything
1260 if (IsReadOnly()) return;
1261 // queue or direct enter
1262 OnUserClose();
1263}
1264
1265void C4Menu::SetCloseCommand(const char *strCommand)
1266{
1267 CloseCommand.Copy(pnData: strCommand);
1268}
1269
1270bool C4Menu::HasMouse()
1271{
1272 int32_t iPlayer = GetControllingPlayer();
1273 if (iPlayer == NO_OWNER) return true; // free view dialog also has the mouse
1274 C4Player *pPlr = Game.Players.Get(iPlayer);
1275 if (pPlr && pPlr->MouseControl) return true;
1276 return false;
1277}
1278
1279void C4Menu::ClearPointers(C4Object *pObj)
1280{
1281 C4MenuItem *pItem;
1282 for (int32_t i = 0; pItem = GetItem(iIndex: i); ++i)
1283 if (pItem->GetObject() == pObj)
1284 pItem->ClearObject();
1285}
1286
1287#ifndef NDEBUG
1288void C4Menu::AssertSurfaceNotUsed(C4Surface *sfc)
1289{
1290 C4MenuItem *pItem;
1291 if (!sfc) return;
1292 assert(sfc != Symbol.Surface);
1293 for (int32_t i = 0; pItem = GetItem(i); ++i)
1294 assert(pItem->Symbol.Surface != sfc);
1295}
1296#endif
1297