| 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 | |
| 29 | const 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 | |
| 37 | const 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 | |
| 43 | void (int32_t , 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 | |
| 76 | C4MenuItem::(C4Menu *, 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 | |
| 100 | C4MenuItem::() |
| 101 | { |
| 102 | Symbol.Clear(); |
| 103 | } |
| 104 | |
| 105 | void C4MenuItem::(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 | |
| 128 | bool C4MenuItem::() |
| 129 | { |
| 130 | // any constructibles can be dragged |
| 131 | C4Def *pDef = C4Id2Def(id); |
| 132 | return pDef && pDef->Constructable; |
| 133 | } |
| 134 | |
| 135 | int32_t C4MenuItem::(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 | |
| 147 | void C4MenuItem::(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 | |
| 213 | void C4MenuItem::(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 | |
| 239 | void C4MenuItem::(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 | |
| 247 | void C4MenuItem::(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 | |
| 263 | void C4MenuItem::(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 | |
| 272 | 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 | |
| 282 | void C4Menu::() |
| 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 | |
| 308 | void C4Menu::() |
| 309 | { |
| 310 | Close(fOK: false); |
| 311 | Symbol.Clear(); |
| 312 | ClearItems(); |
| 313 | ClearFrameDeco(); |
| 314 | fActive = false; |
| 315 | } |
| 316 | |
| 317 | bool C4Menu::(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 | |
| 337 | bool C4Menu::(C4FacetExSurface &fctSymbol, const char *szEmpty, int32_t , int32_t , 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 | |
| 344 | bool C4Menu::(const C4FacetEx &fctSymbol, const char *szEmpty, int32_t , int32_t , int32_t iId, int32_t iStyle) |
| 345 | { |
| 346 | Clear(); Default(); |
| 347 | Symbol.Set(fctSymbol); |
| 348 | return InitMenu(szEmpty, iExtra, iExtraData, iId, iStyle); |
| 349 | } |
| 350 | |
| 351 | bool C4Menu::(const char *szEmpty, int32_t , int32_t , 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 | |
| 375 | bool C4Menu::(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 | |
| 388 | bool C4Menu::(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 | |
| 401 | bool C4Menu::(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 | |
| 433 | bool C4Menu::(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 | |
| 486 | bool C4Menu::(uint8_t byCom) |
| 487 | { |
| 488 | // direct keyboard callback |
| 489 | if (!IsActive()) return false; |
| 490 | return !!Control(byCom, iData: 0); |
| 491 | } |
| 492 | |
| 493 | bool C4Menu::() |
| 494 | { |
| 495 | return fActive; |
| 496 | } |
| 497 | |
| 498 | bool C4Menu::(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 | |
| 525 | C4MenuItem *C4Menu::(int32_t iIndex) |
| 526 | { |
| 527 | return static_cast<C4MenuItem *>(pClientWindow->GetElementByIndex(i: iIndex)); |
| 528 | } |
| 529 | |
| 530 | int32_t C4Menu::() |
| 531 | { |
| 532 | return ItemCount; |
| 533 | } |
| 534 | |
| 535 | bool C4Menu::(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 | |
| 557 | bool C4Menu::(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 | |
| 596 | C4MenuItem *C4Menu::() |
| 597 | { |
| 598 | return GetItem(iIndex: Selection); |
| 599 | } |
| 600 | |
| 601 | void C4Menu::() |
| 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 | |
| 612 | int32_t C4Menu::() |
| 613 | { |
| 614 | return Selection; |
| 615 | } |
| 616 | |
| 617 | int C4Menu::() |
| 618 | { |
| 619 | return static_cast<float>((Style == C4MN_Style_Dialog) ? 64 : C4SymbolSize) * Application.GetScale(); |
| 620 | } |
| 621 | |
| 622 | bool C4Menu::(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 | |
| 630 | int32_t C4Menu::() |
| 631 | { |
| 632 | return Identification; |
| 633 | } |
| 634 | |
| 635 | void C4Menu::(int32_t iToWdt, int32_t iToHgt) |
| 636 | { |
| 637 | if (iToWdt) Columns = iToWdt; |
| 638 | if (iToHgt) Lines = iToHgt; |
| 639 | InitSize(); |
| 640 | } |
| 641 | |
| 642 | void C4Menu::(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 | |
| 755 | void C4Menu::() |
| 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 | |
| 782 | void C4Menu::() |
| 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 | |
| 791 | void C4Menu::(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 | |
| 823 | void C4Menu::(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 (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 = 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 | |
| 932 | void C4Menu::(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 | |
| 937 | void C4Menu::(int32_t iAlignment) |
| 938 | { |
| 939 | Alignment = iAlignment; |
| 940 | } |
| 941 | |
| 942 | void C4Menu::(bool fPermanent) |
| 943 | { |
| 944 | Permanent = fPermanent; |
| 945 | } |
| 946 | |
| 947 | bool C4Menu::() |
| 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 | |
| 975 | void C4Menu::(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 | |
| 990 | void C4Menu::() |
| 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 | |
| 1002 | bool C4Menu::() |
| 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 | |
| 1014 | void C4Menu::() |
| 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 | |
| 1040 | bool C4Menu::(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 | |
| 1072 | bool C4Menu::(int32_t iX, int32_t iY) |
| 1073 | { |
| 1074 | // just set position... |
| 1075 | SetPos(iXPos: iX, iYPos: iY); |
| 1076 | return true; |
| 1077 | } |
| 1078 | |
| 1079 | bool C4Menu::(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 | |
| 1114 | C4Viewport *C4Menu::() |
| 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 | |
| 1124 | void C4Menu::() |
| 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 | |
| 1229 | void C4Menu::() |
| 1230 | { |
| 1231 | // client rect and stuff |
| 1232 | typedef C4GUI::Dialog ParentClass; |
| 1233 | ParentClass::UpdateOwnPos(); |
| 1234 | UpdateElementPositions(); |
| 1235 | } |
| 1236 | |
| 1237 | void C4Menu::(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 | |
| 1247 | void C4Menu::(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 | |
| 1257 | void C4Menu::(bool fOK) |
| 1258 | { |
| 1259 | // not if user con't control anything |
| 1260 | if (IsReadOnly()) return; |
| 1261 | // queue or direct enter |
| 1262 | OnUserClose(); |
| 1263 | } |
| 1264 | |
| 1265 | void C4Menu::SetCloseCommand(const char *strCommand) |
| 1266 | { |
| 1267 | CloseCommand.Copy(pnData: strCommand); |
| 1268 | } |
| 1269 | |
| 1270 | bool C4Menu::() |
| 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 | |
| 1279 | void C4Menu::(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 |
| 1288 | void 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 | |