1/*
2 * LegacyClonk
3 *
4 * Copyright (c) RedWolf Design
5 * Copyright (c) 2001, Sven2
6 * Copyright (c) 2017-2021, The LegacyClonk Team and contributors
7 *
8 * Distributed under the terms of the ISC license; see accompanying file
9 * "COPYING" for details.
10 *
11 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
12 * See accompanying file "TRADEMARK" for details.
13 *
14 * To redistribute this file separately, substitute the full license texts
15 * for the above references.
16 */
17
18// generic user interface
19// dialog base classes and some user dialogs
20
21#include "C4GuiDialogs.h"
22#include "C4GuiEdit.h"
23#include "C4GuiResource.h"
24#include <C4Gui.h>
25
26#include <C4FullScreen.h>
27#include <C4LoaderScreen.h>
28#include <C4Application.h>
29#include <C4Viewport.h>
30#include <C4Console.h>
31#include <C4Def.h>
32#include <C4Wrappers.h>
33
34#include <StdGL.h>
35
36#include <format>
37
38#ifdef _WIN32
39#include "StdRegistry.h"
40#include "res/engine_resource.h"
41#endif
42
43namespace C4GUI
44{
45namespace
46{
47 inline Button *newDlgCloseButton(const C4Rect &bounds) { return new CloseButton{LoadResStr(id: C4ResStrTableKey::IDS_DLG_CLOSE), bounds, true}; }
48 inline Button *newRetryButton(const C4Rect &bounds) { return new CloseButton{LoadResStr(id: C4ResStrTableKey::IDS_BTN_RETRY), bounds, true}; }
49}
50
51// EM window class
52class DialogWindow : public CStdWindow
53{
54#ifdef _WIN32
55private:
56 static constexpr auto WindowStyle = WS_VISIBLE | WS_POPUP | WS_SYSMENU | WS_CAPTION | WS_MINIMIZEBOX;
57#endif
58public:
59 DialogWindow(Dialog &dialog) : dialog{dialog} {}
60 virtual void Close() override;
61
62#ifdef _WIN32
63 bool Init(CStdApp *app, const char *title, const class C4Rect &bounds, CStdWindow *parent = nullptr) override;
64
65 std::pair<DWORD, DWORD> GetWindowStyle() const override { return {WindowStyle, 0}; }
66 WNDCLASSEX GetWindowClass(HINSTANCE instance) const override;
67 bool GetPositionData(std::string &id, std::string &subKey, bool &storeSize) const override;
68#endif
69
70private:
71 Dialog &dialog;
72
73#ifdef _WIN32
74 friend LRESULT APIENTRY DialogWinProc(HWND, UINT, WPARAM, LPARAM);
75#endif
76};
77
78// FrameDecoration
79
80void FrameDecoration::Clear()
81{
82 idSourceDef = 0;
83 dwBackClr = C4GUI_StandardBGColor;
84 iBorderTop = iBorderLeft = iBorderRight = iBorderBottom = 0;
85 fHasGfxOutsideClientArea = false;
86 fctTop.Default();
87 fctTopRight.Default();
88 fctRight.Default();
89 fctBottomRight.Default();
90 fctBottom.Default();
91 fctBottomLeft.Default();
92 fctLeft.Default();
93 fctTopLeft.Default();
94}
95
96bool FrameDecoration::SetFacetByAction(C4Def *pOfDef, C4FacetEx &rfctTarget, const char *szFacetName)
97{
98 // get action
99 const std::string actName{std::format(fmt: "FrameDeco{}", args&: szFacetName)};
100 int cnt; C4ActionDef *pAct = pOfDef->ActMap;
101 for (cnt = pOfDef->ActNum; cnt; --cnt, ++pAct)
102 if (actName == pAct->Name)
103 break;
104 if (!cnt) return false;
105 // set facet by it
106 rfctTarget.Set(nsfc: pOfDef->Graphics.GetBitmap(), nx: pAct->Facet.x, ny: pAct->Facet.y, nwdt: pAct->Facet.Wdt, nhgt: pAct->Facet.Hgt, ntx: pAct->Facet.tx, nty: pAct->Facet.ty);
107 return true;
108}
109
110bool FrameDecoration::SetByDef(C4ID idSourceDef)
111{
112 // get source def
113 C4Def *pSrcDef = C4Id2Def(id: idSourceDef);
114 if (!pSrcDef) return false;
115 // script compiled?
116 if (!pSrcDef->Script.IsReady()) return false;
117 // reset old
118 Clear();
119 this->idSourceDef = idSourceDef;
120 // query values
121 dwBackClr = pSrcDef->Script.Call(szFunction: std::format(PSF_FrameDecoration, args: "BackClr").c_str()).getInt();
122 iBorderTop = pSrcDef->Script.Call(szFunction: std::format(PSF_FrameDecoration, args: "BorderTop").c_str()).getInt();
123 iBorderLeft = pSrcDef->Script.Call(szFunction: std::format(PSF_FrameDecoration, args: "BorderLeft").c_str()).getInt();
124 iBorderRight = pSrcDef->Script.Call(szFunction: std::format(PSF_FrameDecoration, args: "BorderRight").c_str()).getInt();
125 iBorderBottom = pSrcDef->Script.Call(szFunction: std::format(PSF_FrameDecoration, args: "BorderBottom").c_str()).getInt();
126 // get gfx
127 SetFacetByAction(pOfDef: pSrcDef, rfctTarget&: fctTop, szFacetName: "Top");
128 SetFacetByAction(pOfDef: pSrcDef, rfctTarget&: fctTopRight, szFacetName: "TopRight");
129 SetFacetByAction(pOfDef: pSrcDef, rfctTarget&: fctRight, szFacetName: "Right");
130 SetFacetByAction(pOfDef: pSrcDef, rfctTarget&: fctBottomRight, szFacetName: "BottomRight");
131 SetFacetByAction(pOfDef: pSrcDef, rfctTarget&: fctBottom, szFacetName: "Bottom");
132 SetFacetByAction(pOfDef: pSrcDef, rfctTarget&: fctBottomLeft, szFacetName: "BottomLeft");
133 SetFacetByAction(pOfDef: pSrcDef, rfctTarget&: fctLeft, szFacetName: "Left");
134 SetFacetByAction(pOfDef: pSrcDef, rfctTarget&: fctTopLeft, szFacetName: "TopLeft");
135 // check for gfx outside main area
136 fHasGfxOutsideClientArea = (fctTopLeft.TargetY < 0) || (fctTop.TargetY < 0) || (fctTopRight.TargetY < 0)
137 || (fctTopLeft.TargetX < 0) || (fctLeft.TargetX < 0) || (fctBottomLeft.TargetX < 0)
138 || (fctTopRight.TargetX + fctTopRight.Wdt > iBorderRight) || (fctRight.TargetX + fctRight.Wdt > iBorderRight) || (fctBottomRight.TargetX + fctBottomRight.Wdt > iBorderRight)
139 || (fctBottomLeft.TargetY + fctBottomLeft.Hgt > iBorderBottom) || (fctBottom.TargetY + fctBottom.Hgt > iBorderBottom) || (fctBottomRight.TargetY + fctBottomRight.Hgt > iBorderBottom);
140 // k, done
141 return true;
142}
143
144bool FrameDecoration::UpdateGfx()
145{
146 // simply re-set by def
147 return SetByDef(idSourceDef);
148}
149
150void FrameDecoration::Draw(C4FacetEx &cgo, C4Rect &rcBounds)
151{
152 int ox = cgo.TargetX + rcBounds.x, oy = cgo.TargetY + rcBounds.y;
153
154 // draw BG
155 lpDDraw->DrawBoxDw(sfcDest: cgo.Surface, iX1: ox, iY1: oy, iX2: ox + rcBounds.Wdt - 1, iY2: oy + rcBounds.Hgt - 1, dwClr: dwBackClr);
156
157 const auto drawHorizontal = [this, &cgo, &rcBounds, ox](C4FacetEx facet, std::int32_t y)
158 {
159 if (facet.Wdt <= 0)
160 {
161 return;
162 }
163
164 for (std::int32_t x = iBorderLeft; x < rcBounds.Wdt - iBorderRight; x += facet.Wdt)
165 {
166 int w = std::min<int>(a: facet.Wdt, b: rcBounds.Wdt - iBorderRight - x);
167 facet.Wdt = w;
168 facet.Draw(sfcTarget: cgo.Surface, iX: ox + x, iY: y + facet.TargetY);
169 }
170 };
171 const auto drawVertical = [this, &cgo, &rcBounds, oy](C4FacetEx facet, std::int32_t x)
172 {
173 if (facet.Hgt <= 0)
174 {
175 return;
176 }
177
178 for (std::int32_t y = iBorderTop; y < rcBounds.Hgt - iBorderBottom; y += facet.Hgt)
179 {
180 int h = std::min<int>(a: facet.Hgt, b: rcBounds.Hgt - iBorderBottom - y);
181 facet.Hgt = h;
182 facet.Draw(sfcTarget: cgo.Surface, iX: x + facet.TargetX, iY: oy + y);
183 }
184 };
185
186 // draw borders
187 drawHorizontal(fctTop, oy);
188 drawVertical(fctLeft, ox);
189 drawVertical(fctRight, ox + rcBounds.Wdt - iBorderRight);
190 drawHorizontal(fctBottom, oy + rcBounds.Hgt - iBorderBottom);
191
192 // draw edges
193 fctTopLeft.Draw(sfcTarget: cgo.Surface, iX: ox + fctTopLeft.TargetX, iY: oy + fctTopLeft.TargetY);
194 fctTopRight.Draw(sfcTarget: cgo.Surface, iX: ox + rcBounds.Wdt - iBorderRight + fctTopRight.TargetX, iY: oy + fctTopRight.TargetY);
195 fctBottomLeft.Draw(sfcTarget: cgo.Surface, iX: ox + fctBottomLeft.TargetX, iY: oy + rcBounds.Hgt - iBorderBottom + fctBottomLeft.TargetY);
196 fctBottomRight.Draw(sfcTarget: cgo.Surface, iX: ox + rcBounds.Wdt - iBorderRight + fctBottomRight.TargetX, iY: oy + rcBounds.Hgt - iBorderBottom + fctBottomRight.TargetY);
197}
198
199// DialogWindow
200
201#ifdef _WIN32
202
203bool DialogWindow::Init(CStdApp *const app, const char *const title, const C4Rect &bounds, CStdWindow *const parent)
204{
205 // calculate required size
206 RECT size{0, 0, bounds.Wdt, bounds.Hgt};
207 if (!AdjustWindowRectEx(&size, WindowStyle, false, 0))
208 {
209 return false;
210 }
211
212 return CStdWindow::Init(app, title, {bounds.x, bounds.y, size.right - size.left, size.bottom - size.top}, parent);
213}
214
215// Dialog
216
217LRESULT APIENTRY DialogWinProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
218{
219 if (uMsg != WM_NCCREATE)
220 {
221 Dialog *const dialog{&reinterpret_cast<DialogWindow *>(GetWindowLongPtr(hwnd, GWLP_USERDATA))->dialog};
222 // Process message
223 switch (uMsg)
224 {
225 case WM_KEYDOWN:
226 if (Game.DoKeyboardInput(wParam, KEYEV_Down, !!(lParam & 0x20000000), Application.IsControlDown(), Application.IsShiftDown(), !!(lParam & 0x40000000), dialog)) return 0;
227 break;
228
229 case WM_KEYUP:
230 if (Game.DoKeyboardInput(wParam, KEYEV_Up, !!(lParam & 0x20000000), Application.IsControlDown(), Application.IsShiftDown(), false, dialog)) return 0;
231 break;
232
233 case WM_SYSKEYDOWN:
234 if (wParam == 18) break;
235 if (Game.DoKeyboardInput(wParam, KEYEV_Down, !!(lParam & 0x20000000), Application.IsControlDown(), Application.IsShiftDown(), !!(lParam & 0x40000000), dialog)) return 0;
236 break;
237
238 case WM_CLOSE:
239 dialog->Close(false);
240 break;
241
242 case WM_PAINT:
243 // 2do: only draw specific dlg?
244 break;
245 return 0;
246
247 case WM_LBUTTONDOWN: Game.pGUI->MouseInput(C4MC_Button_LeftDown, LOWORD(lParam), HIWORD(lParam), wParam, dialog, nullptr); break;
248 case WM_LBUTTONUP: Game.pGUI->MouseInput(C4MC_Button_LeftUp, LOWORD(lParam), HIWORD(lParam), wParam, dialog, nullptr); break;
249 case WM_RBUTTONDOWN: Game.pGUI->MouseInput(C4MC_Button_RightDown, LOWORD(lParam), HIWORD(lParam), wParam, dialog, nullptr); break;
250 case WM_RBUTTONUP: Game.pGUI->MouseInput(C4MC_Button_RightUp, LOWORD(lParam), HIWORD(lParam), wParam, dialog, nullptr); break;
251 case WM_LBUTTONDBLCLK: Game.pGUI->MouseInput(C4MC_Button_LeftDouble, LOWORD(lParam), HIWORD(lParam), wParam, dialog, nullptr); break;
252 case WM_RBUTTONDBLCLK: Game.pGUI->MouseInput(C4MC_Button_RightDouble, LOWORD(lParam), HIWORD(lParam), wParam, dialog, nullptr); break;
253
254 case WM_MOUSEMOVE:
255 Game.pGUI->MouseInput(C4MC_Button_None, LOWORD(lParam), HIWORD(lParam), wParam, dialog, nullptr);
256 break;
257
258 case WM_MOUSEWHEEL:
259 Game.pGUI->MouseInput(C4MC_Button_Wheel, LOWORD(lParam), HIWORD(lParam), wParam, dialog, nullptr);
260 break;
261 }
262 }
263
264 return CStdWindow::DefaultWindowProc(hwnd, uMsg, wParam, lParam);
265}
266
267WNDCLASSEX DialogWindow::GetWindowClass(const HINSTANCE instance) const
268{
269 return {
270 .cbSize = sizeof(WNDCLASSEX),
271 .style = CS_DBLCLKS | CS_BYTEALIGNCLIENT,
272 .lpfnWndProc = &DialogWinProc,
273 .cbClsExtra = 0,
274 .cbWndExtra = 0,
275 .hInstance = instance,
276 .hIcon = LoadIcon(instance, MAKEINTRESOURCE(IDI_00_C4X)),
277 .hCursor = LoadCursor(nullptr, IDC_ARROW),
278 .hbrBackground = reinterpret_cast<HBRUSH>(COLOR_BACKGROUND),
279 .lpszMenuName = nullptr,
280 .lpszClassName = L"C4GUIdlg", // keep for backwards compatibility
281 .hIconSm = LoadIcon(instance, MAKEINTRESOURCE(IDI_00_C4X))
282 };
283}
284
285bool DialogWindow::GetPositionData(std::string &id, std::string &subKey, bool &storeSize) const
286{
287 if (const char *const dialogID{dialog.GetID()}; dialogID && *dialogID)
288 {
289 id = std::string{"ConsoleGUI_"} + dialogID;
290 subKey = Config.GetSubkeyPath("Console");
291 storeSize = true;
292 return true;
293 }
294
295 return false;
296}
297
298#endif // _WIN32
299
300void DialogWindow::Close()
301{
302 // FIXME: Close the dialog of this window
303}
304
305bool Dialog::CreateConsoleWindow()
306{
307 // already created?
308 if (pWindow) return true;
309 // create it!
310 pWindow = new DialogWindow(*this);
311 if (!pWindow->Init(app: &Application, title: TitleString.isNull() ? "???" : TitleString.getData(), bounds: rcBounds, parent: &Console))
312 {
313 delete pWindow;
314 pWindow = nullptr;
315 return false;
316 }
317 // create rendering context
318 if (lpDDraw) pCtx = lpDDraw->CreateContext(pWindow, &Application);
319 return true;
320}
321
322void Dialog::DestroyConsoleWindow()
323{
324 if (pWindow) pWindow->Clear();
325 delete pWindow; pWindow = nullptr;
326 delete pCtx; pCtx = nullptr;
327}
328
329Dialog::Dialog(int32_t iWdt, int32_t iHgt, const char *szTitle, bool fViewportDlg) :
330 Window(), pTitle(nullptr), pCloseBtn(nullptr), fDelOnClose(false), fViewportDlg(fViewportDlg), pWindow(nullptr), pCtx(nullptr), pFrameDeco(nullptr)
331{
332 // zero fields
333 pActiveCtrl = nullptr;
334 fShow = fOK = false;
335 iFade = 100; eFade = eFadeNone;
336 // add title
337 rcBounds.Wdt = iWdt;
338 SetTitle(szToTitle: szTitle);
339 // set size - calcs client rect as well
340 SetBounds(C4Rect(0, 0, iWdt, iHgt));
341 // create key callbacks
342 C4CustomKey::CodeList Keys;
343 Keys.push_back(x: C4KeyCodeEx(K_TAB));
344 if (Config.Controls.GamepadGuiControl)
345 {
346 Keys.push_back(x: C4KeyCodeEx(KEY_Gamepad(idGamepad: 0, idButton: KEY_JOY_Right)));
347 }
348 pKeyAdvanceControl = new C4KeyBinding(Keys, "GUIAdvanceFocus", KEYSCOPE_Gui,
349 new DlgKeyCBEx<Dialog, bool>(*this, false, &Dialog::KeyAdvanceFocus), C4CustomKey::PRIO_Dlg);
350 Keys.clear();
351 Keys.push_back(x: C4KeyCodeEx(K_TAB, KEYS_Shift));
352 if (Config.Controls.GamepadGuiControl)
353 {
354 Keys.push_back(x: C4KeyCodeEx(KEY_Gamepad(idGamepad: 0, idButton: KEY_JOY_Left)));
355 }
356 pKeyAdvanceControlB = new C4KeyBinding(Keys, "GUIAdvanceFocusBack", KEYSCOPE_Gui,
357 new DlgKeyCBEx<Dialog, bool>(*this, true, &Dialog::KeyAdvanceFocus), C4CustomKey::PRIO_Dlg);
358 Keys.clear();
359 Keys.push_back(x: C4KeyCodeEx(KEY_Any, KEYS_Alt));
360 Keys.push_back(x: C4KeyCodeEx(KEY_Any, C4KeyShiftState(KEYS_Alt | KEYS_Shift)));
361 pKeyHotkey = new C4KeyBinding(Keys, "GUIHotkey", KEYSCOPE_Gui,
362 new DlgKeyCBPassKey<Dialog>(*this, &Dialog::KeyHotkey), C4CustomKey::PRIO_Ctrl);
363 Keys.clear();
364 Keys.push_back(x: C4KeyCodeEx(K_RETURN));
365 if (Config.Controls.GamepadGuiControl)
366 {
367 Keys.push_back(x: C4KeyCodeEx(KEY_Gamepad(idGamepad: 0, idButton: KEY_JOY_AnyLowButton)));
368 }
369 pKeyEnter = new C4KeyBinding(Keys, "GUIDialogOkay", KEYSCOPE_Gui,
370 new DlgKeyCB<Dialog>(*this, &Dialog::KeyEnter), C4CustomKey::PRIO_Dlg);
371 Keys.clear();
372 Keys.push_back(x: C4KeyCodeEx(K_ESCAPE));
373 if (Config.Controls.GamepadGuiControl)
374 {
375 Keys.push_back(x: C4KeyCodeEx(KEY_Gamepad(idGamepad: 0, idButton: KEY_JOY_AnyHighButton)));
376 }
377 pKeyEscape = new C4KeyBinding(Keys, "GUIDialogAbort", KEYSCOPE_Gui,
378 new DlgKeyCB<Dialog>(*this, &Dialog::KeyEscape), C4CustomKey::PRIO_Dlg);
379 Keys.clear();
380 Keys.push_back(x: C4KeyCodeEx(KEY_Any));
381 Keys.push_back(x: C4KeyCodeEx(KEY_Any, KEYS_Shift));
382 pKeyFocusDefControl = new C4KeyBinding(Keys, "GUIFocusDefault", KEYSCOPE_Gui,
383 new DlgKeyCB<Dialog>(*this, &Dialog::KeyFocusDefault), C4CustomKey::PRIO_Dlg);
384}
385
386void Dialog::SetTitle(const char *szTitle, bool fShowCloseButton)
387{
388 // always keep local copy of title
389 TitleString.Copy(pnData: szTitle);
390 // console mode dialogs: Use window bar
391 if (!Application.isFullScreen && !IsViewportDialog())
392 {
393 if (pWindow) pWindow->SetTitle(szTitle ? szTitle : "");
394 return;
395 }
396 // set new
397 if (szTitle && *szTitle)
398 {
399 int32_t iTextHgt = WoodenLabel::GetDefaultHeight(pUseFont: &GetRes()->TextFont);
400 if (pTitle)
401 {
402 pTitle->GetBounds() = C4Rect(-GetMarginLeft(), -iTextHgt, rcBounds.Wdt, iTextHgt);
403 // noupdate if title is same - this is necessary to prevent scrolling reset when refilling internal menus
404 if (SEqual(szStr1: pTitle->GetText(), szStr2: szTitle)) return;
405 pTitle->SetText(szText: szTitle);
406 }
407 else
408 AddElement(pChild: pTitle = new WoodenLabel(szTitle, C4Rect(-GetMarginLeft(), -iTextHgt, rcBounds.Wdt, iTextHgt), C4GUI_CaptionFontClr, &GetRes()->TextFont, ALeft, false));
409 pTitle->SetToolTip(szTitle);
410 pTitle->SetDragTarget(this);
411 pTitle->SetAutoScrollTime(C4GUI_TitleAutoScrollTime);
412 if (fShowCloseButton)
413 {
414 pTitle->SetRightIndent(20); // for close button
415 if (!pCloseBtn)
416 {
417 AddElement(pChild: pCloseBtn = new CallbackButton<Dialog, IconButton>(Ico_Close, pTitle->GetToprightCornerRect(iWidth: 16, iHeight: 16, iHIndent: 4, iVIndent: 4, iIndexX: 0), 0, &Dialog::OnUserClose));
418 pCloseBtn->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_MNU_CLOSE));
419 }
420 else
421 pCloseBtn->GetBounds() = pTitle->GetToprightCornerRect(iWidth: 16, iHeight: 16, iHIndent: 4, iVIndent: 4, iIndexX: 0);
422 }
423 }
424 else
425 {
426 delete pTitle; pTitle = nullptr;
427 delete pCloseBtn; pCloseBtn = nullptr;
428 }
429}
430
431Dialog::~Dialog()
432{
433 // kill key bindings
434 delete pKeyAdvanceControl;
435 delete pKeyAdvanceControlB;
436 delete pKeyHotkey;
437 delete pKeyEscape;
438 delete pKeyEnter;
439 delete pKeyFocusDefControl;
440 // clear window
441 DestroyConsoleWindow();
442 // avoid endless delete/close-recursion
443 fDelOnClose = false;
444 // free deco
445 if (pFrameDeco) pFrameDeco->Deref();
446}
447
448void Dialog::UpdateSize()
449{
450 // update title bar position
451 if (pTitle)
452 {
453 int32_t iTextHgt = WoodenLabel::GetDefaultHeight(pUseFont: &GetRes()->TextFont);
454 pTitle->SetBounds(C4Rect(-GetMarginLeft(), -iTextHgt, rcBounds.Wdt, iTextHgt));
455 if (pCloseBtn) pCloseBtn->SetBounds(pTitle->GetToprightCornerRect(iWidth: 16, iHeight: 16, iHIndent: 4, iVIndent: 4, iIndexX: 0));
456 }
457 // inherited
458 Window::UpdateSize();
459 // update assigned window
460 if (pWindow)
461 {
462 auto wdt = rcBounds.Wdt, hgt = rcBounds.Hgt;
463#ifdef _WIN32
464 RECT rect{0, 0, wdt, hgt};
465 if (::AdjustWindowRectEx(&rect, ConsoleDlgWindowStyle, false, 0))
466 {
467 wdt = rect.right - rect.left;
468 hgt = rect.bottom - rect.top;
469 }
470#endif
471 pWindow->SetSize(cx: wdt, cy: hgt);
472 }
473}
474
475void Dialog::RemoveElement(Element *pChild)
476{
477 // inherited
478 Window::RemoveElement(pChild);
479 // clear ptr
480 if (pChild == pActiveCtrl) pActiveCtrl = nullptr;
481}
482
483void Dialog::Draw(C4FacetEx &cgo)
484{
485#ifndef USE_CONSOLE
486 // select rendering context
487 if (pCtx) if (!pCtx->Select()) return;
488#endif
489 Screen *pScreen;
490 // evaluate fading
491 switch (eFade)
492 {
493 case eFadeNone: break; // no fading
494 case eFadeIn:
495 // fade in
496 if ((iFade += 10) >= 100)
497 {
498 if (pScreen = GetScreen())
499 {
500 if (pScreen->GetTopDialog() == this)
501 pScreen->ActivateDialog(pDlg: this);
502 }
503 eFade = eFadeNone;
504 }
505 break;
506 case eFadeOut:
507 // fade out
508 if ((iFade -= 10) <= 0)
509 {
510 fVisible = fShow = false;
511 if (pScreen = GetScreen())
512 pScreen->RecheckActiveDialog();
513 eFade = eFadeNone;
514 }
515 }
516 // set fade
517 if (iFade < 100)
518 {
519 if (iFade <= 0) return;
520 lpDDraw->ActivateBlitModulation(dwWithClr: ((100 - iFade) * 255 / 100) << 24 | 0xffffff);
521 }
522 // separate window: Clear background
523 if (pWindow)
524 lpDDraw->DrawBoxDw(sfcDest: cgo.Surface, iX1: rcBounds.x, iY1: rcBounds.y, iX2: rcBounds.Wdt - 1, iY2: rcBounds.Hgt - 1, C4GUI_StandardBGColor & 0xffffff);
525 // draw window + contents (evaluates IsVisible)
526 Window::Draw(cgo);
527 // reset blit modulation
528 if (iFade < 100) lpDDraw->DeactivateBlitModulation();
529 // blit output to own window
530 if (pWindow) Application.DDraw->PageFlip();
531#ifndef USE_CONSOLE
532 // switch back to original context
533 if (pCtx) pGL->GetMainCtx().Select();
534#endif
535}
536
537void Dialog::DrawElement(C4FacetEx &cgo)
538{
539 // custom border?
540 if (pFrameDeco)
541 pFrameDeco->Draw(cgo, rcBounds);
542 else
543 {
544 // standard border/bg then
545 // draw background
546 lpDDraw->DrawBoxDw(sfcDest: cgo.Surface, iX1: cgo.TargetX + rcBounds.x, iY1: cgo.TargetY + rcBounds.y, iX2: rcBounds.x + rcBounds.Wdt - 1 + cgo.TargetX, iY2: rcBounds.y + rcBounds.Hgt - 1 + cgo.TargetY, C4GUI_StandardBGColor);
547 // draw frame
548 Draw3DFrame(cgo);
549 }
550}
551
552bool Dialog::CharIn(const char *c)
553{
554 // reroute to active control
555 if (pActiveCtrl && pActiveCtrl->CharIn(c)) return true;
556 // unprocessed: Focus default control
557 // Except for space, which may have been processed as a key already
558 // (changing focus here would render buttons unusable, because they switch on KeyUp)
559 Control *pDefCtrl = GetDefaultControl();
560 if (pDefCtrl && pDefCtrl != pActiveCtrl && (!c || *c != 0x20))
561 {
562 SetFocus(pCtrl: pDefCtrl, fByMouse: false);
563 if (pActiveCtrl && pActiveCtrl->CharIn(c))
564 return true;
565 }
566 return false;
567}
568
569bool Dialog::KeyHotkey(C4KeyCodeEx key)
570{
571#ifdef USE_SDL_MAINLOOP
572 const auto wKey = SDL_GetKeyName(SDL_GetKeyFromScancode(static_cast<SDL_Scancode>(key.Key)))[0];
573#else
574 uint16_t wKey = uint16_t(key.Key);
575#endif
576
577 // do hotkey procs for standard alphanumerics only
578 if (Inside<uint16_t>(TOUPPERIFX11(wKey), lbound: 'A', rbound: 'Z')) if (OnHotkey(cHotkey: char(TOUPPERIFX11(wKey)))) return true;
579 if (Inside<uint16_t>(TOUPPERIFX11(wKey), lbound: '0', rbound: '9')) if (OnHotkey(cHotkey: char(TOUPPERIFX11(wKey)))) return true;
580 return false;
581}
582
583bool Dialog::KeyFocusDefault()
584{
585 // unprocessed key: Focus default control
586 Control *pDefCtrl = GetDefaultControl();
587 if (pDefCtrl && pDefCtrl != pActiveCtrl)
588 SetFocus(pCtrl: pDefCtrl, fByMouse: false);
589 // never mark this as processed, so a later char message to the control may be sent (for deselected chat)
590 return false;
591}
592
593void Dialog::MouseInput(CMouse &rMouse, int32_t iButton, int32_t iX, int32_t iY, uint32_t dwKeyParam)
594{
595 // inherited will do...
596 Window::MouseInput(rMouse, iButton, iX, iY, dwKeyParam);
597}
598
599void Dialog::SetFocus(Control *pCtrl, bool fByMouse)
600{
601 // no change?
602 if (pCtrl == pActiveCtrl) return;
603 // leave old focus
604 if (pActiveCtrl)
605 {
606 Control *pC = pActiveCtrl;
607 pActiveCtrl = nullptr;
608 pC->OnLooseFocus();
609 // if leaving the old focus set a new one, abort here because it looks like the control didn't want to lose focus
610 if (pActiveCtrl) return;
611 }
612 // set new
613 if (pActiveCtrl = pCtrl) pCtrl->OnGetFocus(fByMouse);
614}
615
616void Dialog::AdvanceFocus(bool fBackwards)
617{
618 // get element to start from
619 Element *pCurrElement = pActiveCtrl;
620 // find new control
621 for (;;)
622 {
623 // get next element
624 pCurrElement = GetNextNestedElement(pPrevElement: pCurrElement, fBackwards);
625 // end reached: start from beginning
626 if (!pCurrElement && pActiveCtrl) if (!(pCurrElement = GetNextNestedElement(pPrevElement: nullptr, fBackwards))) return;
627 // cycled?
628 if (pCurrElement == pActiveCtrl)
629 {
630 // but current is no longer a focus element? Then defocus it and return
631 if (pCurrElement && !pCurrElement->IsFocusElement())
632 SetFocus(pCtrl: nullptr, fByMouse: false);
633 return;
634 }
635 // for list elements, check whether the child can be selected
636 if (pCurrElement->GetParent() && !pCurrElement->GetParent()->IsSelectedChild(pChild: pCurrElement)) continue;
637 // check if this is a new control
638 Control *pFocusCtrl = pCurrElement->IsFocusElement();
639 if (pFocusCtrl && pFocusCtrl != pActiveCtrl && pFocusCtrl->IsVisible())
640 {
641 // set focus here...
642 SetFocus(pCtrl: pFocusCtrl, fByMouse: false);
643 // ...done!
644 return;
645 }
646 }
647 // never reached
648}
649
650bool Dialog::Show(Screen *pOnScreen, bool fCB)
651{
652 // already shown?
653 if (fShow) return false;
654 // default screen
655 if (!pOnScreen) if (!(pOnScreen = Screen::GetScreenS())) return false;
656 // show there
657 pOnScreen->ShowDialog(pDlg: this, fFade: false);
658 fVisible = true;
659 // developer mode: Create window
660 if (!Application.isFullScreen && !IsViewportDialog())
661 if (!CreateConsoleWindow()) return false;
662 // CB
663 if (fCB) OnShown();
664 return true;
665}
666
667void Dialog::Close(bool fOK)
668{
669 // already closed?
670 if (!fShow) return;
671 // set OK flag
672 this->fOK = fOK;
673 // get screen
674 Screen *pScreen = GetScreen();
675 if (pScreen) pScreen->CloseDialog(pDlg: this, fFade: false); else fShow = false;
676 // developer mode: Remove window
677 if (pWindow) DestroyConsoleWindow();
678 // do callback - last call, because it might do perilous things
679 OnClosed(fOK);
680}
681
682void Dialog::OnClosed(bool fOK)
683{
684 // developer mode: Remove window
685 if (pWindow) DestroyConsoleWindow();
686 // delete when closing?
687 if (fDelOnClose)
688 {
689 fDelOnClose = false;
690 delete this;
691 }
692}
693
694bool Dialog::DoModal()
695{
696 // main message loop
697 while (fShow)
698 {
699 int32_t iResult = 1;
700 while ((iResult != HR_Timer) && fShow)
701 {
702 // dialog idle proc
703 OnIdle();
704 // handle messages - this may block until the next timer
705 iResult = Application.HandleMessage();
706 // quit
707 if (iResult == HR_Failure || !IsGUIValid()) return false; // game GUI and lobby will deleted in Game::Clear()
708 }
709 // Idle proc may have done something nasty
710 if (!IsGUIValid()) return false;
711 }
712 // return whether dlg was OK
713 return fOK;
714}
715
716bool Dialog::Execute()
717{
718 // process messages
719 int32_t iResult;
720 while ((iResult = Application.HandleMessage()) == HR_Message)
721 // check status
722 if (!IsGUIValid() || !fShow) return false;
723 if (iResult == HR_Failure) return false;
724 // check status
725 if (!IsGUIValid() || !fShow) return false;
726 return true;
727}
728
729bool Dialog::IsActive(bool fForKeyboard)
730{
731 // must be fully visible
732 if (!IsShown() || IsFading()) return false;
733 // screen-less dialogs are always inactive (not yet added)
734 Screen *pScreen = GetScreen();
735 if (!pScreen) return false;
736 // no keyboard focus if screen is in context mode
737 if (fForKeyboard && pScreen->HasContext()) return false;
738 // always okay in shared mode: all dlgs accessible by mouse
739 if (!pScreen->IsExclusive() && !fForKeyboard) return true;
740 // exclusive mode or keyboard input: Only one dlg active
741 return pScreen->pActiveDlg == this;
742}
743
744bool Dialog::FadeIn(Screen *pOnScreen)
745{
746 // default screen
747 if (!pOnScreen) if (!(pOnScreen = Screen::GetScreenS())) return false;
748 // fade in there
749 pOnScreen->ShowDialog(pDlg: this, fFade: true);
750 iFade = 0;
751 eFade = eFadeIn;
752 fVisible = true;
753 OnShown();
754 // done, success
755 return true;
756}
757
758void Dialog::FadeOut(bool fCloseWithOK)
759{
760 // only if shown, or being faded in
761 if (!IsShown() && (!fVisible || eFade != eFadeIn)) return;
762 // set OK flag
763 this->fOK = fCloseWithOK;
764 // fade out
765 Screen *pOnScreen = GetScreen();
766 if (!pOnScreen) return;
767 pOnScreen->CloseDialog(pDlg: this, fFade: true);
768 eFade = eFadeOut;
769 // do callback - last call, because it might do perilous things
770 OnClosed(fOK: fCloseWithOK);
771}
772
773void Dialog::ApplyElementOffset(int32_t &riX, int32_t &riY)
774{
775 // inherited
776 Window::ApplyElementOffset(riX, riY);
777 // apply viewport offset, if a viewport is assigned
778 C4Viewport *pVP = GetViewport();
779 if (pVP)
780 {
781 C4Rect rcVP(pVP->GetOutputRect());
782 riX -= rcVP.x; riY -= rcVP.y;
783 }
784}
785
786void Dialog::ApplyInvElementOffset(int32_t &riX, int32_t &riY)
787{
788 // inherited
789 Window::ApplyInvElementOffset(riX, riY);
790 // apply viewport offset, if a viewport is assigned
791 C4Viewport *pVP = GetViewport();
792 if (pVP)
793 {
794 C4Rect rcVP(pVP->GetOutputRect());
795 riX += rcVP.x; riY += rcVP.y;
796 }
797}
798
799void Dialog::SetClientSize(int32_t iToWdt, int32_t iToHgt)
800{
801 // calc new bounds
802 iToWdt += GetMarginLeft() + GetMarginRight();
803 iToHgt += GetMarginTop() + GetMarginBottom();
804 rcBounds.x += (rcBounds.Wdt - iToWdt) / 2;
805 rcBounds.y += (rcBounds.Hgt - iToHgt) / 2;
806 rcBounds.Wdt = iToWdt; rcBounds.Hgt = iToHgt;
807 // reflect changes
808 UpdatePos();
809}
810
811// FullscreenDialog
812
813FullscreenDialog::FullscreenDialog(const char *szTitle, const char *szSubtitle)
814 : Dialog(Screen::GetScreenS()->GetClientRect().Wdt, Screen::GetScreenS()->GetClientRect().Hgt, nullptr /* create own title */, false), pFullscreenTitle(nullptr), pBtnHelp(nullptr)
815{
816 // set margins
817 int32_t iScreenX = Screen::GetScreenS()->GetClientRect().Wdt;
818 int32_t iScreenY = Screen::GetScreenS()->GetClientRect().Hgt;
819 if (iScreenX < 500) iDlgMarginX = 2; else iDlgMarginX = iScreenX / 50;
820 if (iScreenY < 320) iDlgMarginY = 2; else iDlgMarginY = iScreenY * 2 / 75;
821 // set size - calcs client rect as well
822 SetBounds(C4Rect(0, 0, iScreenX, iScreenY));
823 // create title
824 SetTitle(szTitle);
825 // create subtitle (only with upperboard)
826 if (szSubtitle && *szSubtitle && HasUpperBoard())
827 {
828 AddElement(pChild: pSubTitle = new Label(szSubtitle, rcClientRect.Wdt, C4UpperBoardHeight - GetRes()->CaptionFont.GetLineHeight() / 2 - 25 - GetMarginTop(), ARight, C4GUI_CaptionFontClr, &GetRes()->TextFont));
829 pSubTitle->SetToolTip(szTitle);
830 }
831 else pSubTitle = nullptr;
832}
833
834void FullscreenDialog::SetTitle(const char *szTitle)
835{
836 delete pFullscreenTitle; pFullscreenTitle = nullptr;
837 // change title text; creates or removes title bar if necessary
838 if (szTitle && *szTitle)
839 {
840 // not using dlg label, which is a wooden label
841 if (HasUpperBoard())
842 pFullscreenTitle = new Label(szTitle, 0, C4UpperBoardHeight / 2 - GetRes()->TitleFont.GetLineHeight() / 2 - GetMarginTop(), ALeft, C4GUI_CaptionFontClr, &GetRes()->TitleFont);
843 else
844 // non-woodbar: Title is centered and in big font
845 pFullscreenTitle = new Label(szTitle, GetClientRect().Wdt / 2, C4UpperBoardHeight / 2 - GetRes()->TitleFont.GetLineHeight() / 2 - GetMarginTop(), ACenter, C4GUI_FullscreenCaptionFontClr, &GetRes()->TitleFont);
846 AddElement(pChild: pFullscreenTitle);
847 pFullscreenTitle->SetToolTip(szTitle);
848 }
849}
850
851void FullscreenDialog::DrawElement(C4FacetEx &cgo)
852{
853 // draw upper board
854 if (HasUpperBoard())
855 lpDDraw->BlitSurfaceTile(sfcSurface: Game.GraphicsResource.fctUpperBoard.Surface, sfcTarget: cgo.Surface, iToX: 0, iToY: std::min<int32_t>(a: iFade - Game.GraphicsResource.fctUpperBoard.Hgt, b: 0), iToWdt: cgo.Wdt, iToHgt: Game.GraphicsResource.fctUpperBoard.Hgt);
856}
857
858int32_t FullscreenDialog::GetMarginTop()
859{
860 return (HasUpperBoard() ? C4UpperBoard::Height() : C4GUI_FullscreenDlg_TitleHeight)
861 + iDlgMarginY;
862}
863
864void FullscreenDialog::UpdateOwnPos()
865{
866 // inherited to update client rect
867 Dialog::UpdateOwnPos();
868 // reposition help button
869 UpdateHelpButtonPos();
870}
871
872void FullscreenDialog::UpdateHelpButtonPos()
873{
874 // reposition help button
875 if (pBtnHelp) pBtnHelp->SetBounds(C4Rect(GetBounds().Wdt - 4 - 32 - GetMarginLeft(), 4 - GetMarginTop(), 32, 32));
876}
877
878void FullscreenDialog::DrawBackground(C4FacetEx &cgo, C4Facet &rFromFct)
879{
880 // draw across fullscreen bounds - zoom 1px border to prevent flashing borders by blit offsets
881 Screen *pScr = GetScreen();
882 C4Facet cgoScreen = cgo;
883 C4Rect &rcScreenBounds = pScr ? pScr->GetBounds() : GetBounds();
884 cgoScreen.X = rcScreenBounds.x - 1; cgoScreen.Y = rcScreenBounds.y - 1;
885 cgoScreen.Wdt = rcScreenBounds.Wdt + 2; cgoScreen.Hgt = rcScreenBounds.Hgt + 2;
886 rFromFct.DrawFullScreen(cgo&: cgoScreen);
887}
888
889// MessageDialog
890
891MessageDialog::MessageDialog(const char *szMessage, const char *szCaption, uint32_t dwButtons, Icons icoIcon, DlgSize eSize, bool *piConfigDontShowAgainSetting, bool fDefaultNo, int32_t zOrdering)
892 : Dialog(eSize, 100 /* will be resized */, szCaption, false), piConfigDontShowAgainSetting(piConfigDontShowAgainSetting), zOrdering(zOrdering)
893{
894 CStdFont &rUseFont = GetRes()->TextFont;
895 // get positions
896 ComponentAligner caMain(GetClientRect(), C4GUI_DefDlgIndent, C4GUI_DefDlgIndent, true);
897 // place icon
898 C4Rect rcIcon = caMain.GetFromLeft(C4GUI_IconWdt); rcIcon.Hgt = C4GUI_IconHgt;
899 Icon *pIcon = new Icon(rcIcon, icoIcon); AddElement(pChild: pIcon);
900 // centered text for small dialogs and/or dialogs w/o much text (i.e.: no linebreaks)
901 bool fTextCentered;
902 if (eSize != dsRegular)
903 fTextCentered = true;
904 else
905 {
906 int32_t iMsgWdt = 0, iMsgHgt = 0;
907 rUseFont.GetTextExtent(szText: szMessage, rsx&: iMsgWdt, rsy&: iMsgHgt);
908 fTextCentered = ((iMsgWdt <= caMain.GetInnerWidth() - C4GUI_IconWdt - C4GUI_DefDlgIndent * 2) && iMsgHgt <= rUseFont.GetLineHeight());
909 }
910 // centered text dialog: waste some icon space on the right to balance dialog
911 if (fTextCentered) caMain.GetFromRight(C4GUI_IconWdt);
912 // place message label
913 // use text with line breaks
914 StdStrBuf sMsgBroken;
915 int iMsgHeight = rUseFont.BreakMessage(szMsg: szMessage, iWdt: caMain.GetInnerWidth(), pOut: &sMsgBroken, fCheckMarkup: true);
916 lblText = new Label("", caMain.GetFromTop(iHgt: iMsgHeight), fTextCentered ? ACenter : ALeft, C4GUI_MessageFontClr, &rUseFont, false);
917 lblText->SetText(szText: sMsgBroken.getData(), fAllowHotkey: false);
918 AddElement(pChild: lblText);
919 // place do-not-show-again-checkbox
920 if (piConfigDontShowAgainSetting)
921 {
922 int w = 100, h = 20;
923 const char *szCheckText = LoadResStr(id: C4ResStrTableKey::IDS_MSG_DONTSHOW);
924 CheckBox::GetStandardCheckBoxSize(piWdt: &w, piHgt: &h, szForCaptionText: szCheckText, pUseFont: nullptr);
925 CheckBox *pCheck = new C4GUI::CheckBox(caMain.GetFromTop(iHgt: h, iWdt: w), szCheckText, !!*piConfigDontShowAgainSetting);
926 pCheck->SetOnChecked(new C4GUI::CallbackHandler<MessageDialog>(this, &MessageDialog::OnDontShowAgainCheck));
927 AddElement(pChild: pCheck);
928 }
929 if (!fTextCentered) caMain.ExpandLeft(C4GUI_DefDlgIndent * 2 + C4GUI_IconWdt);
930 // place button(s)
931 ComponentAligner caButtonArea(caMain.GetFromTop(C4GUI_ButtonAreaHgt), 0, 0);
932 int32_t iButtonCount = 0;
933 int32_t i = 1; while (i) { if (dwButtons & i) ++iButtonCount; i = i << 1; }
934 fHasOK = !!(dwButtons & btnOK) || !!(dwButtons & btnYes);
935 Button *btnFocus = nullptr;
936 if (iButtonCount)
937 {
938 C4Rect rcBtn = caButtonArea.GetCentered(iWdt: iButtonCount * C4GUI_DefButton2Wdt + (iButtonCount - 1) * C4GUI_DefButton2HSpace, C4GUI_ButtonHgt);
939 rcBtn.Wdt = C4GUI_DefButton2Wdt;
940 // OK
941 if (dwButtons & btnOK)
942 {
943 Button *pBtnOK = newOKButton(bounds: rcBtn);
944 AddElement(pChild: pBtnOK);
945 rcBtn.x += C4GUI_DefButton2Wdt + C4GUI_DefButton2HSpace;
946 if (!fDefaultNo) btnFocus = pBtnOK;
947 }
948 // Retry
949 if (dwButtons & btnRetry)
950 {
951 Button *pBtnRetry = newRetryButton(bounds: rcBtn);
952 AddElement(pChild: pBtnRetry);
953 rcBtn.x += C4GUI_DefButton2Wdt + C4GUI_DefButton2HSpace;
954 if (!btnFocus) btnFocus = pBtnRetry;
955 }
956 // Cancel
957 if (dwButtons & btnAbort)
958 {
959 Button *pBtnAbort = newCancelButton(bounds: rcBtn);
960 AddElement(pChild: pBtnAbort);
961 rcBtn.x += C4GUI_DefButton2Wdt + C4GUI_DefButton2HSpace;
962 if (!btnFocus) btnFocus = pBtnAbort;
963 }
964 // Yes
965 if (dwButtons & btnYes)
966 {
967 Button *pBtnYes = newYesButton(bounds: rcBtn);
968 AddElement(pChild: pBtnYes);
969 rcBtn.x += C4GUI_DefButton2Wdt + C4GUI_DefButton2HSpace;
970 if (!btnFocus && !fDefaultNo) btnFocus = pBtnYes;
971 }
972 // No
973 if (dwButtons & btnNo)
974 {
975 Button *pBtnNo = newNoButton(bounds: rcBtn);
976 AddElement(pChild: pBtnNo);
977 if (!btnFocus) btnFocus = pBtnNo;
978 }
979 }
980 if (btnFocus) SetFocus(pCtrl: btnFocus, fByMouse: false);
981 // resize to actually needed size
982 SetClientSize(iToWdt: GetClientRect().Wdt, iToHgt: GetClientRect().Hgt - caMain.GetHeight());
983}
984
985// ConfirmationDialog
986
987ConfirmationDialog::ConfirmationDialog(const char *szMessage, const char *szCaption, BaseCallbackHandler *pCB, uint32_t dwButtons, bool fSmall, Icons icoIcon)
988 : MessageDialog(szMessage, szCaption, dwButtons, icoIcon, fSmall ? MessageDialog::dsSmall : MessageDialog::dsRegular)
989{
990 if (this->pCB = pCB) pCB->Ref();
991 // always log confirmation messages
992 spdlog::debug(fmt: "[Cnf] {}: {}", args&: szCaption, args&: szMessage);
993 // confirmations always get deleted on close
994 SetDelOnClose();
995}
996
997void ConfirmationDialog::OnClosed(bool fOK)
998{
999 // confirmed only on OK
1000 BaseCallbackHandler *pStackCB = fOK ? pCB : nullptr;
1001 if (pStackCB) pStackCB->Ref();
1002 // caution: this will usually delete the dlg (this)
1003 // so the CB-interface is backed up
1004 MessageDialog::OnClosed(fOK);
1005 if (pStackCB)
1006 {
1007 pStackCB->DoCall(pElement: nullptr);
1008 pStackCB->DeRef();
1009 }
1010}
1011
1012// ProgressDialog
1013
1014ProgressDialog::ProgressDialog(const char *szMessage, const char *szCaption, int32_t iMaxProgress, int32_t iInitialProgress, Icons icoIcon)
1015 : Dialog(C4GUI_ProgressDlgWdt, 100, szCaption, false)
1016{
1017 StdStrBuf broken;
1018 rcBounds.Hgt = (std::max)(a: GetRes()->TextFont.BreakMessage(szMsg: szMessage, C4GUI_ProgressDlgWdt - 3 * C4GUI_DefDlgIndent - C4GUI_IconWdt, pOut: &broken, fCheckMarkup: true), C4GUI_IconHgt) + C4GUI_ProgressDlgVRoom;
1019 SetBounds(rcBounds);
1020 // get positions
1021 ComponentAligner caMain(GetClientRect(), C4GUI_DefDlgIndent, C4GUI_DefDlgIndent, true);
1022 ComponentAligner caButtonArea(caMain.GetFromBottom(C4GUI_ButtonAreaHgt), 0, 0);
1023 C4Rect rtProgressBar = caMain.GetFromBottom(C4GUI_ProgressDlgPBHgt);
1024 // place icon
1025 C4Rect rcIcon = caMain.GetFromLeft(C4GUI_IconWdt); rcIcon.Hgt = C4GUI_IconHgt;
1026 Icon *pIcon = new Icon(rcIcon, icoIcon); AddElement(pChild: pIcon);
1027 // place message label
1028 // use text with line breaks
1029 Label *pLblMessage = new Label(broken.getData(), caMain.GetAll().GetMiddleX(), caMain.GetAll().y, ACenter, C4GUI_MessageFontClr, &GetRes()->TextFont);
1030 AddElement(pChild: pLblMessage);
1031 // place progress bar
1032 pBar = new ProgressBar(rtProgressBar, iMaxProgress);
1033 pBar->SetProgress(iInitialProgress);
1034 pBar->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_PROGRESS));
1035 AddElement(pChild: pBar);
1036 // place abort button
1037 Button *pBtnAbort = newCancelButton(bounds: caButtonArea.GetCentered(C4GUI_DefButtonWdt, C4GUI_ButtonHgt));
1038 AddElement(pChild: pBtnAbort);
1039}
1040
1041// Some dialog wrappers in Screen class
1042
1043bool Screen::ShowMessage(const char *szMessage, const char *szCaption, Icons icoIcon, bool *piConfigDontShowAgainSetting)
1044{
1045 // always log messages
1046 spdlog::debug(fmt: "[Msg] {}: {}", args&: szCaption, args&: szMessage);
1047 if (piConfigDontShowAgainSetting && *piConfigDontShowAgainSetting) return true;
1048#ifdef USE_CONSOLE
1049 // skip in console mode
1050 return true;
1051#endif
1052 return ShowRemoveDlg(pDlg: new MessageDialog(szMessage, szCaption, MessageDialog::btnOK, icoIcon, MessageDialog::dsRegular, piConfigDontShowAgainSetting));
1053}
1054
1055bool Screen::ShowErrorMessage(const char *szMessage)
1056{
1057 return ShowMessage(szMessage, szCaption: LoadResStr(id: C4ResStrTableKey::IDS_DLG_ERROR), icoIcon: Ico_Error);
1058}
1059
1060bool Screen::ShowMessageModal(const char *szMessage, const char *szCaption, uint32_t dwButtons, Icons icoIcon, bool *pbConfigDontShowAgainSetting)
1061{
1062 // always log messages
1063 spdlog::debug(fmt: "[Modal] {}: {}", args&: szCaption, args&: szMessage);
1064 // skip if user doesn't want to see it
1065 if (pbConfigDontShowAgainSetting && *pbConfigDontShowAgainSetting) return true;
1066 // create message dlg and show modal
1067 return ShowModalDlg(pDlg: new MessageDialog(szMessage, szCaption, dwButtons, icoIcon, MessageDialog::dsRegular, pbConfigDontShowAgainSetting));
1068}
1069
1070bool Screen::ShowModalDlg(Dialog *pDlg, bool fDestruct)
1071{
1072#ifdef USE_CONSOLE
1073 // no modal dialogs in console build
1074 // (there's most likely no way to close them!)
1075 if (fDestruct) delete pDlg;
1076 return true;
1077#endif
1078 // safety
1079 if (!pDlg) return false;
1080 // show it
1081 if (!pDlg->Show(pOnScreen: this, fCB: true)) { delete pDlg; return false; }
1082 // wait until it is closed
1083 bool fResult = pDlg->DoModal();
1084 // free dlg if this class is still valid (may have been deleted in game clear)
1085 if (!IsGUIValid()) return false;
1086 if (fDestruct) delete pDlg;
1087 // return result
1088 return fResult;
1089}
1090
1091bool Screen::ShowRemoveDlg(Dialog *pDlg)
1092{
1093 // safety
1094 if (!pDlg) return false;
1095 // mark removal when done
1096 pDlg->SetDelOnClose();
1097 // show it
1098 if (!pDlg->Show(pOnScreen: this, fCB: true)) { delete pDlg; return false; }
1099 // done, success
1100 return true;
1101}
1102
1103// InputDialog
1104
1105InputDialog::InputDialog(const char *szMessage, const char *szCaption, Icons icoIcon, BaseInputCallback *pCB, bool fChatLayout)
1106 : Dialog(fChatLayout ? Config.Graphics.ResX * 4 / 5 : C4GUI_InputDlgWdt, fChatLayout ? C4GUI::Edit::GetDefaultEditHeight() + 2 : 100, szCaption, false), pEdit(nullptr), pChatLbl(nullptr), pCB(pCB), fChatLayout(fChatLayout)
1107{
1108 if (fChatLayout)
1109 {
1110 // chat input layout
1111 C4GUI::ComponentAligner caChat(GetContainedClientRect(), 1, 1);
1112 // normal chatbox layout: Left chat label
1113 int32_t w = 40, h;
1114 C4GUI::GetRes()->TextFont.GetTextExtent(szText: szMessage, rsx&: w, rsy&: h, fCheckMarkup: true);
1115 pChatLbl = new C4GUI::WoodenLabel(szMessage, caChat.GetFromLeft(iWdt: w + 4), C4GUI_CaptionFontClr, &C4GUI::GetRes()->TextFont);
1116 caChat.ExpandLeft(iByWdt: 2); // undo margin
1117 rcEditBounds = caChat.GetAll();
1118 SetCustomEdit(new Edit(rcEditBounds));
1119 pChatLbl->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_CHAT));
1120 AddElement(pChild: pChatLbl);
1121 }
1122 else
1123 {
1124 StdStrBuf broken;
1125 rcBounds.Hgt = (std::max)(a: GetRes()->TextFont.BreakMessage(szMsg: szMessage, C4GUI_InputDlgWdt - 3 * C4GUI_DefDlgIndent - C4GUI_IconWdt, pOut: &broken, fCheckMarkup: true), C4GUI_IconHgt) + C4GUI_InputDlgVRoom;
1126 SetBounds(rcBounds);
1127 // regular input dialog layout
1128 // get positions
1129 ComponentAligner caMain(GetClientRect(), C4GUI_DefDlgIndent, C4GUI_DefDlgIndent, true);
1130 ComponentAligner caButtonArea(caMain.GetFromBottom(C4GUI_ButtonAreaHgt), 0, 0);
1131 rcEditBounds = caMain.GetFromBottom(iHgt: Edit::GetDefaultEditHeight());
1132 // place icon
1133 C4Rect rcIcon = caMain.GetFromLeft(C4GUI_IconWdt); rcIcon.Hgt = C4GUI_IconHgt;
1134 Icon *pIcon = new Icon(rcIcon, icoIcon); AddElement(pChild: pIcon);
1135 // place message label
1136 // use text with line breaks
1137 Label *pLblMessage = new Label(broken.getData(), caMain.GetAll().GetMiddleX(), caMain.GetAll().y, ACenter, C4GUI_MessageFontClr, &GetRes()->TextFont);
1138 AddElement(pChild: pLblMessage);
1139 // place input edit
1140 SetCustomEdit(new Edit(rcEditBounds));
1141 // place buttons
1142 C4Rect rcBtn = caButtonArea.GetCentered(iWdt: 2 * C4GUI_DefButton2Wdt + C4GUI_DefButton2HSpace, C4GUI_ButtonHgt);
1143 rcBtn.Wdt = C4GUI_DefButton2Wdt;
1144 // OK
1145 Button *pBtnOK = newOKButton(bounds: rcBtn);
1146 AddElement(pChild: pBtnOK);
1147 rcBtn.x += rcBtn.Wdt + C4GUI_DefButton2HSpace;
1148 // Cancel
1149 Button *pBtnAbort = newCancelButton(bounds: rcBtn);
1150 AddElement(pChild: pBtnAbort);
1151 rcBtn.x += rcBtn.Wdt + C4GUI_DefButton2HSpace;
1152 }
1153 // input dlg always closed in the end
1154 SetDelOnClose();
1155}
1156
1157void InputDialog::SetMaxText(int32_t iMaxLen)
1158{
1159 pEdit->SetMaxText(iMaxLen);
1160}
1161
1162void InputDialog::SetInputText(const char *szToText)
1163{
1164 pEdit->SelectAll(); pEdit->DeleteSelection();
1165 if (szToText)
1166 {
1167 pEdit->InsertText(text: szToText, fUser: false);
1168 pEdit->SelectAll();
1169 }
1170}
1171
1172void InputDialog::SetCustomEdit(Edit *pCustomEdit)
1173{
1174 // del old
1175 delete pEdit;
1176 // add new
1177 pEdit = pCustomEdit;
1178 pEdit->SetBounds(rcEditBounds);
1179 if (fChatLayout)
1180 {
1181 pEdit->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_CHAT));
1182 pChatLbl->SetClickFocusControl(pEdit); // 2do: to all, to allies, etc.
1183 }
1184 AddElement(pChild: pEdit);
1185 SetFocus(pCtrl: pEdit, fByMouse: false);
1186}
1187
1188void InputDialog::OnClosed(bool fOK)
1189{
1190 if (pCB && fOK)
1191 {
1192 pCB->OnOK(sText: StdStrBuf::MakeRef(str: pEdit->GetText()));
1193 }
1194 Dialog::OnClosed(fOK);
1195}
1196
1197const char *InputDialog::GetInputText()
1198{
1199 return pEdit->GetText();
1200}
1201
1202// InfoDialog
1203
1204InfoDialog::InfoDialog(const char *szCaption, int32_t iLineCount)
1205 : Dialog(C4GUI_InfoDlgWdt, GetRes()->TextFont.GetLineHeight() * iLineCount + C4GUI_InfoDlgVRoom, szCaption, false), iScroll(0), pSec1Timer(nullptr)
1206{
1207 // timer
1208 pSec1Timer = new C4Sec1TimerCallback<InfoDialog>(this);
1209 CreateSubComponents();
1210}
1211
1212InfoDialog::InfoDialog(const char *szCaption, int iLineCount, const StdStrBuf &sText)
1213 : Dialog(C4GUI_InfoDlgWdt, GetRes()->TextFont.GetLineHeight() * iLineCount + C4GUI_InfoDlgVRoom, szCaption, false), iScroll(0), pSec1Timer(nullptr)
1214{
1215 // ctor - init w/o timer
1216 CreateSubComponents();
1217 // fill in initial text
1218 for (size_t i = 0; i < sText.getLength(); ++i)
1219 {
1220 size_t i0 = i;
1221 while (sText[i] != '|' && sText[i]) ++i;
1222 StdStrBuf sLine = sText.copyPart(iStart: i0, inSize: i - i0);
1223 pTextWin->AddTextLine(szText: sLine.getData(), pFont: &GetRes()->TextFont, C4GUI_MessageFontClr, fDoUpdate: false, fMakeReadableOnBlack: true);
1224 }
1225 pTextWin->UpdateHeight();
1226}
1227
1228InfoDialog::~InfoDialog()
1229{
1230 if (pSec1Timer) pSec1Timer->Release();
1231}
1232
1233void InfoDialog::CreateSubComponents()
1234{
1235 // get positions
1236 ComponentAligner caMain(GetClientRect(), C4GUI_DefDlgIndent, C4GUI_DefDlgIndent, true);
1237 ComponentAligner caButtonArea(caMain.GetFromBottom(C4GUI_ButtonAreaHgt), 0, 0);
1238 // place info box
1239 pTextWin = new TextWindow(caMain.GetAll(), 0, 0, 0, 100, 4096, " ", true, nullptr, 0);
1240 AddElement(pChild: pTextWin);
1241 // place close button
1242 Button *pBtnClose = newDlgCloseButton(bounds: caButtonArea.GetCentered(C4GUI_DefButtonWdt, C4GUI_ButtonHgt));
1243 AddElement(pChild: pBtnClose); pBtnClose->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_MNU_CLOSE));
1244}
1245
1246void InfoDialog::AddLine(const char *szText)
1247{
1248 // add line to text window
1249 if (!pTextWin) return;
1250 pTextWin->AddTextLine(szText, pFont: &GetRes()->TextFont, C4GUI_MessageFontClr, fDoUpdate: false, fMakeReadableOnBlack: true);
1251}
1252
1253void InfoDialog::BeginUpdateText()
1254{
1255 // safety
1256 if (!pTextWin) return;
1257 // backup scrolling
1258 iScroll = pTextWin->GetScrollPos();
1259 // clear text window, so new text can be added
1260 pTextWin->ClearText(fDoUpdate: false);
1261}
1262
1263void InfoDialog::EndUpdateText()
1264{
1265 // safety
1266 if (!pTextWin) return;
1267 // update text height
1268 pTextWin->UpdateHeight();
1269 // restore scrolling
1270 pTextWin->SetScrollPos(iScroll);
1271}
1272
1273void InfoDialog::OnSec1Timer()
1274{
1275 // always update
1276 UpdateText();
1277}
1278
1279TimedDialog::TimedDialog(uint32_t time, const char *message, const char *caption, uint32_t buttons, Icons icon, DlgSize size, bool *configDontShowAgainSetting, bool defaultNo, int32_t zOrdering)
1280 : MessageDialog{message, caption, buttons, icon, size, configDontShowAgainSetting, defaultNo, zOrdering}, time{time}
1281{
1282 sec1Timer = new C4Sec1TimerCallback<TimedDialog>{this};
1283}
1284
1285TimedDialog::~TimedDialog()
1286{
1287 sec1Timer->Release();
1288}
1289
1290void TimedDialog::OnSec1Timer()
1291{
1292 if (--time == 0)
1293 {
1294 Close(fOK: false);
1295 return;
1296 }
1297
1298 UpdateText();
1299}
1300
1301void TimedDialog::SetText(const char *message)
1302{
1303 lblText->SetText(szText: message);
1304
1305 ComponentAligner caMain{GetClientRect(), C4GUI_DefDlgIndent, C4GUI_DefDlgIndent, true};
1306 caMain.ExpandTop(iByHgt: -lblText->GetHeight());
1307 ComponentAligner caButtonArea{caMain.GetFromTop(C4GUI_ButtonAreaHgt), 0, 0};
1308 const C4Rect &bounds{caButtonArea.GetCentered(C4GUI_DefButtonWdt, C4GUI_DefButton2HSpace)};
1309
1310 int32_t oldButtonY{-1};
1311 int32_t oldButtonHeight{0};
1312
1313 for (Element *element{GetFirst()}; element; element = element->GetNext())
1314 {
1315 if (element != pCloseBtn)
1316 {
1317 if (auto *btn = dynamic_cast<Button *>(element); btn)
1318 {
1319 int32_t &y{btn->GetBounds().y};
1320 if (oldButtonY == -1)
1321 {
1322 oldButtonY = y;
1323 oldButtonHeight = btn->GetHeight();
1324 }
1325
1326 y = bounds.y;
1327 }
1328 }
1329 }
1330
1331 GetClientRect().Hgt = bounds.y + oldButtonHeight + C4GUI_DefButton2HSpace;
1332 if (pTitle)
1333 {
1334 GetBounds().Hgt = GetClientRect().Hgt + pTitle->GetHeight();
1335 }
1336 UpdateSize();
1337}
1338
1339void CloseButton::OnPress()
1340{
1341 Dialog *pDlg;
1342 if ((pDlg = GetDlg()))
1343 {
1344 pDlg->UserClose(fOK: fCloseResult);
1345 }
1346}
1347
1348void CloseIconButton::OnPress()
1349{
1350 Dialog *pDlg;
1351 if ((pDlg = GetDlg()))
1352 {
1353 pDlg->UserClose(fOK: fCloseResult);
1354 }
1355}
1356} // end of namespace
1357