1/*
2 * LegacyClonk
3 *
4 * Copyright (c) RedWolf Design
5 * Copyright (c) 2008, 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// file selection dialogs
19
20#include "C4GuiComboBox.h"
21#include "C4GuiListBox.h"
22#include "C4GuiResource.h"
23#include <C4Include.h>
24#include <C4FileSelDlg.h>
25
26#include <C4Game.h> // only for single use of Game.GraphicsResource.fctOKCancel below...
27
28#ifdef _WIN32
29#include "C4Windows.h"
30#include <shlobj.h>
31#endif
32
33#include <format>
34
35// C4FileSelDlg::ListItem
36
37C4FileSelDlg::ListItem::ListItem(const char *szFilename) : C4GUI::Control(C4Rect(0, 0, 0, 0))
38{
39 if (szFilename) sFilename.Copy(pnData: szFilename);
40}
41
42C4FileSelDlg::ListItem::~ListItem() {}
43
44// C4FileSelDlg::DefaultListItem
45
46C4FileSelDlg::DefaultListItem::DefaultListItem(const char *szFilename, bool fTruncateExtension, bool fCheckbox, bool fGrayed, C4GUI::Icons eIcon)
47 : C4FileSelDlg::ListItem(szFilename), pLbl(nullptr), pCheck(nullptr), pKeyCheck(nullptr), fGrayed(fGrayed)
48{
49 StdStrBuf sLabel; if (szFilename) sLabel.Ref(pnData: ::GetFilename(path: szFilename)); else sLabel.Ref(pnData: LoadResStr(id: C4ResStrTableKey::IDS_CTL_NONE));
50 if (szFilename && fTruncateExtension)
51 {
52 sLabel.Copy();
53 char *szFilename = sLabel.GrabPointer();
54 RemoveExtension(szFileName: szFilename);
55 sLabel.Take(pnData: szFilename);
56 }
57 rcBounds.Hgt = C4GUI::GetRes()->TextFont.GetLineHeight();
58 UpdateSize();
59 C4GUI::ComponentAligner caMain(GetContainedClientRect(), 0, 0);
60 int32_t iHeight = caMain.GetInnerHeight();
61 if (fCheckbox)
62 {
63 pCheck = new C4GUI::CheckBox(caMain.GetFromLeft(iWdt: iHeight), "", false);
64 if (fGrayed) pCheck->SetEnabled(false);
65 AddElement(pChild: pCheck);
66 pKeyCheck = new C4KeyBinding(C4KeyCodeEx(K_SPACE), "FileSelToggleFileActive", KEYSCOPE_Gui,
67 new C4GUI::ControlKeyCB<ListItem>(*this, &ListItem::UserToggleCheck), C4CustomKey::PRIO_Ctrl);
68 }
69 C4GUI::Icon *pIco = new C4GUI::Icon(caMain.GetFromLeft(iWdt: iHeight), eIcon);
70 AddElement(pChild: pIco);
71 pLbl = new C4GUI::Label(sLabel.getData(), caMain.GetAll(), ALeft, fGrayed ? C4GUI_CheckboxDisabledFontClr : C4GUI_CheckboxFontClr);
72 AddElement(pChild: pLbl);
73}
74
75C4FileSelDlg::DefaultListItem::~DefaultListItem()
76{
77 delete pKeyCheck;
78}
79
80void C4FileSelDlg::DefaultListItem::UpdateOwnPos()
81{
82 BaseClass::UpdateOwnPos();
83 if (!pLbl) return;
84 C4GUI::ComponentAligner caMain(GetContainedClientRect(), 0, 0);
85 caMain.GetFromLeft(iWdt: caMain.GetInnerHeight() * (1 + !!pCheck));
86 pLbl->SetBounds(caMain.GetAll());
87}
88
89bool C4FileSelDlg::DefaultListItem::IsChecked() const
90{
91 return pCheck ? pCheck->GetChecked() : false;
92}
93
94void C4FileSelDlg::DefaultListItem::SetChecked(bool fChecked)
95{
96 // store new state in checkbox
97 if (pCheck) pCheck->SetChecked(fChecked);
98}
99
100bool C4FileSelDlg::DefaultListItem::UserToggleCheck()
101{
102 // toggle if possible
103 if (pCheck && !IsGrayed())
104 {
105 pCheck->ToggleCheck(fByUser: true);
106 return true;
107 }
108 return false;
109}
110
111// C4FileSelDlg
112
113C4FileSelDlg::C4FileSelDlg(const char *szRootPath, const char *szTitle, C4FileSel_BaseCB *pSelCallback, bool fInitElements)
114 : C4GUI::Dialog(BoundBy(bval: C4GUI::GetScreenWdt() * 2 / 3 + 10, lbound: 300, rbound: 600), BoundBy(bval: C4GUI::GetScreenHgt() * 2 / 3 + 10, lbound: 220, rbound: 500), szTitle, false),
115 pFileListBox(nullptr), pSelectionInfoBox(nullptr), btnOK(nullptr), pSelection(nullptr), pSelCallback(pSelCallback), pLocations(nullptr), iLocationCount(0), pLocationComboBox(nullptr)
116{
117 sTitle.Copy(pnData: szTitle);
118 // key bindings
119 pKeyRefresh = new C4KeyBinding(C4KeyCodeEx(K_F5), "FileSelReload", KEYSCOPE_Gui,
120 new C4GUI::DlgKeyCB<C4FileSelDlg>(*this, &C4FileSelDlg::KeyRefresh), C4CustomKey::PRIO_CtrlOverride);
121 pKeyEnterOverride = new C4KeyBinding(C4KeyCodeEx(K_RETURN), "FileSelConfirm", KEYSCOPE_Gui,
122 new C4GUI::DlgKeyCB<C4FileSelDlg>(*this, &C4FileSelDlg::KeyEnter), C4CustomKey::PRIO_CtrlOverride);
123 if (fInitElements) InitElements();
124 sPath.Copy(pnData: szRootPath);
125}
126
127void C4FileSelDlg::InitElements()
128{
129 UpdateSize();
130 CStdFont *pUseFont = &(C4GUI::GetRes()->TextFont);
131 // main calcs
132 bool fHasOptions = HasExtraOptions();
133 C4GUI::ComponentAligner caMain(GetClientRect(), 0, 0, true);
134 C4Rect rcOptions;
135 C4GUI::ComponentAligner caButtonArea(caMain.GetFromBottom(C4GUI_ButtonAreaHgt, iWdt: 2 * C4GUI_DefButton2Wdt + 4 * C4GUI_DefButton2HSpace), C4GUI_DefButton2HSpace, (C4GUI_ButtonAreaHgt - C4GUI_ButtonHgt) / 2);
136 if (fHasOptions) rcOptions = caMain.GetFromBottom(iHgt: pUseFont->GetLineHeight() + 2 * C4GUI_DefDlgSmallIndent);
137 C4GUI::ComponentAligner caUpperArea(caMain.GetAll(), C4GUI_DefDlgIndent, C4GUI_DefDlgIndent, true);
138 // create file selection area
139 if (iLocationCount)
140 {
141 C4GUI::ComponentAligner caLocations(caUpperArea.GetFromTop(iHgt: C4GUI::ComboBox::GetDefaultHeight() + 2 * C4GUI_DefDlgSmallIndent), C4GUI_DefDlgIndent, C4GUI_DefDlgSmallIndent, false);
142 StdStrBuf sText(LoadResStr(id: C4ResStrTableKey::IDS_TEXT_LOCATION), false);
143 AddElement(pChild: new C4GUI::Label(sText.getData(), caLocations.GetFromLeft(iWdt: pUseFont->GetTextWidth(szText: sText.getData())), ALeft));
144 pLocationComboBox = new C4GUI::ComboBox(caLocations.GetAll());
145 pLocationComboBox->SetComboCB(new C4GUI::ComboBox_FillCallback<C4FileSelDlg>(this, &C4FileSelDlg::OnLocationComboFill, &C4FileSelDlg::OnLocationComboSelChange));
146 pLocationComboBox->SetText(pLocations[0].sName.getData());
147 }
148 // create file selection area
149 bool fHasPreview = HasPreviewArea();
150 pFileListBox = new C4GUI::ListBox(fHasPreview ? caUpperArea.GetFromLeft(iWdt: caUpperArea.GetWidth() / 2) : caUpperArea.GetAll(), GetFileSelColWidth());
151 pFileListBox->SetSelectionChangeCallbackFn(new C4GUI::CallbackHandler<C4FileSelDlg>(this, &C4FileSelDlg::OnSelChange));
152 pFileListBox->SetSelectionDblClickFn(new C4GUI::CallbackHandler<C4FileSelDlg>(this, &C4FileSelDlg::OnSelDblClick));
153 if (fHasPreview)
154 {
155 caUpperArea.ExpandLeft(C4GUI_DefDlgIndent);
156 pSelectionInfoBox = new C4GUI::TextWindow(caUpperArea.GetAll());
157 pSelectionInfoBox->SetDecoration(fDrawBG: true, fDrawFrame: true, pToGfx: nullptr, fAutoScroll: true);
158 }
159 // create button area
160 C4GUI::Button *btnAbort = C4GUI::newCancelButton(bounds: caButtonArea.GetFromRight(C4GUI_DefButton2Wdt));
161 btnOK = C4GUI::newOKButton(bounds: caButtonArea.GetFromRight(C4GUI_DefButton2Wdt));
162 // add components in tab order
163 if (pLocationComboBox) AddElement(pChild: pLocationComboBox);
164 AddElement(pChild: pFileListBox);
165 if (pSelectionInfoBox) AddElement(pChild: pSelectionInfoBox);
166 if (fHasOptions) AddExtraOptions(rcOptionsRect: rcOptions);
167 AddElement(pChild: btnOK);
168 AddElement(pChild: btnAbort);
169 SetFocus(pCtrl: pFileListBox, fByMouse: false);
170 // no selection yet
171 UpdateSelection();
172}
173
174C4FileSelDlg::~C4FileSelDlg()
175{
176 delete[] pLocations;
177 delete pSelCallback;
178 delete pKeyEnterOverride;
179 delete pKeyRefresh;
180}
181
182void C4FileSelDlg::OnLocationComboFill(C4GUI::ComboBox_FillCB *pFiller)
183{
184 // Add all locations
185 for (int32_t i = 0; i < iLocationCount; ++i)
186 pFiller->AddEntry(szText: pLocations[i].sName.getData(), id: i);
187}
188
189bool C4FileSelDlg::OnLocationComboSelChange(C4GUI::ComboBox *pForCombo, int32_t idNewSelection)
190{
191 SetCurrentLocation(idx: idNewSelection, fRefresh: true);
192 // No text change by caller; text alread changed by SetCurrentLocation
193 return true;
194}
195
196void C4FileSelDlg::SetPath(const char *szNewPath, bool fRefresh)
197{
198 sPath.Copy(pnData: szNewPath);
199 if (fRefresh && IsShown()) UpdateFileList();
200}
201
202void C4FileSelDlg::OnShown()
203{
204 BaseClass::OnShown();
205 // load files
206 UpdateFileList();
207}
208
209void C4FileSelDlg::UserClose(bool fOK)
210{
211 if (!fOK || pSelection)
212 {
213 // allow OK only if something is sth is selected
214 Close(fOK);
215 }
216 else
217 {
218 GetScreen()->ShowErrorMessage(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_ERR_PLEASESELECTAFILEFIRST));
219 }
220}
221
222void C4FileSelDlg::OnClosed(bool fOK)
223{
224 if (fOK && pSelection && pSelCallback)
225 pSelCallback->OnFileSelected(szFilename: pSelection->GetFilename());
226 // base call: Might delete dlg
227 BaseClass::OnClosed(fOK);
228}
229
230void C4FileSelDlg::OnSelDblClick(class C4GUI::Element *pEl)
231{
232 // item double-click: confirms with this file in single mode; toggles selection in multi mode
233 if (IsMultiSelection())
234 {
235 ListItem *pItem = static_cast<ListItem *>(pEl);
236 pItem->UserToggleCheck();
237 }
238 else
239 UserClose(fOK: true);
240}
241
242C4FileSelDlg::ListItem *C4FileSelDlg::CreateListItem(const char *szFilename)
243{
244 // Default list item
245 if (szFilename)
246 return new DefaultListItem(szFilename, !!GetFileMask(), IsMultiSelection(), IsItemGrayed(szFilename), GetFileItemIcon());
247 else
248 return new DefaultListItem(nullptr, false, IsMultiSelection(), false, GetFileItemIcon());
249}
250
251void C4FileSelDlg::UpdateFileList()
252{
253 BeginFileListUpdate();
254 // reload files
255 C4GUI::Element *pEl;
256 while (pEl = pFileListBox->GetFirst()) delete pEl;
257 // file items
258 const char *szFileMask = GetFileMask();
259 for (DirectoryIterator iter(sPath.getData()); *iter; ++iter)
260 if (!szFileMask || WildcardListMatch(szWildcardList: szFileMask, szString: *iter))
261 pFileListBox->AddElement(pChild: CreateListItem(szFilename: *iter));
262 // none-item?
263 if (HasNoneItem())
264 {
265 pFileListBox->AddElement(pChild: CreateListItem(szFilename: nullptr));
266 }
267 // list now done
268 EndFileListUpdate();
269 // path into title
270 const char *szPath = sPath.getData();
271 SetTitle(szToTitle: *szPath ? std::format(fmt: "{} [{}]", args: sTitle.getData(), args&: szPath).c_str() : sTitle.getData());
272 // initial no-selection
273 UpdateSelection();
274}
275
276void C4FileSelDlg::UpdateSelection()
277{
278 // update selection from list
279 pSelection = static_cast<ListItem *>(pFileListBox->GetSelectedItem());
280 // OK button only available if selection
281 // btnOK->SetEnabled would look a lot better here, but it doesn't exist yet :(
282 // selection preview, if enabled
283 if (pSelectionInfoBox)
284 {
285 // default empty
286 pSelectionInfoBox->ClearText(fDoUpdate: false);
287 if (!pSelection) { pSelectionInfoBox->UpdateHeight(); return; }
288 // add selection description
289 if (pSelection->GetFilename())
290 pSelectionInfoBox->AddTextLine(szText: pSelection->GetFilename(), pFont: &C4GUI::GetRes()->TextFont, C4GUI_MessageFontClr, fDoUpdate: true, fMakeReadableOnBlack: false);
291 }
292}
293
294void C4FileSelDlg::SetSelection(const std::vector<std::string> &newSelection, bool fFilenameOnly)
295{
296 // check all selected definitions
297 for (ListItem *pFileItem = static_cast<ListItem *>(pFileListBox->GetFirst()); pFileItem; pFileItem = static_cast<ListItem *>(pFileItem->GetNext()))
298 {
299 const char *szFileItemFilename = pFileItem->GetFilename();
300 if (fFilenameOnly) szFileItemFilename = GetFilename(path: szFileItemFilename);
301 pFileItem->SetChecked(std::find(first: newSelection.begin(), last: newSelection.end(), val: szFileItemFilename) != newSelection.end());
302 }
303}
304
305std::vector<std::string> C4FileSelDlg::GetSelection(const std::vector<std::string> &fixedSelection, bool filenameOnly) const
306{
307 if (!IsMultiSelection())
308 {
309 // get single selected file for single selection dlg
310 if (const char *str{filenameOnly ? GetFilename(path: pSelection->GetFilename()) : pSelection->GetFilename()}; str)
311 {
312 return {str};
313 }
314
315 return {};
316 }
317 else
318 {
319 std::vector<std::string> selection{fixedSelection};
320
321 // get ';'-separated list for multi selection dlg
322 for (ListItem *pFileItem = static_cast<ListItem *>(pFileListBox->GetFirst()); pFileItem; pFileItem = static_cast<ListItem *>(pFileItem->GetNext()))
323 {
324 if (pFileItem->IsChecked())
325 {
326 const char *szAppendFilename = pFileItem->GetFilename();
327 if (filenameOnly) szAppendFilename = GetFilename(path: szAppendFilename);
328
329 // prevent adding entries twice (especially those from the fixed selection list)
330 if (std::find(first: selection.cbegin(), last: selection.cend(), val: szAppendFilename) == selection.cend())
331 {
332 selection.push_back(x: szAppendFilename);
333 }
334 }
335 }
336
337 return selection;
338 }
339}
340
341void C4FileSelDlg::AddLocation(const char *szName, const char *szPath)
342{
343 // add to list
344 int32_t iNewLocCount = iLocationCount + 1;
345 Location *pNewLocations = new Location[iNewLocCount];
346 for (int32_t i = 0; i < iLocationCount; ++i) pNewLocations[i] = pLocations[i];
347 pNewLocations[iLocationCount].sName.Copy(pnData: szName);
348 pNewLocations[iLocationCount].sPath.Copy(pnData: szPath);
349 delete[] pLocations; pLocations = pNewLocations; iLocationCount = iNewLocCount;
350 // first location? Then set path to this
351 if (iLocationCount == 1) SetPath(szNewPath: szPath, fRefresh: false);
352}
353
354void C4FileSelDlg::AddCheckedLocation(const char *szName, const char *szPath)
355{
356 // check location
357 // path must exit
358 if (!szPath || !*szPath) return;
359 if (!DirectoryExists(szFileName: szPath)) return;
360 // path must not be in list yet
361 for (int32_t i = 0; i < iLocationCount; ++i)
362 if (ItemIdentical(szFilename1: szPath, szFilename2: pLocations[i].sPath.getData()))
363 return;
364 // OK; add it!
365 AddLocation(szName, szPath);
366}
367
368int32_t C4FileSelDlg::GetCurrentLocationIndex() const
369{
370 return iCurrentLocationIndex;
371}
372
373void C4FileSelDlg::SetCurrentLocation(int32_t idx, bool fRefresh)
374{
375 // safety
376 if (!Inside<int32_t>(ival: idx, lbound: 0, rbound: iLocationCount)) return;
377 // update ComboBox-text
378 iCurrentLocationIndex = idx;
379 if (pLocationComboBox) pLocationComboBox->SetText(pLocations[idx].sName.getData());
380 // set new path
381 SetPath(szNewPath: pLocations[idx].sPath.getData(), fRefresh);
382}
383
384// C4PlayerSelDlg
385
386C4PlayerSelDlg::C4PlayerSelDlg(C4FileSel_BaseCB *pSelCallback)
387 : C4FileSelDlg(Config.AtExePath(szFilename: Config.General.PlayerPath), LoadResStr(id: C4ResStrTableKey::IDS_MSG_SELECTPLR), pSelCallback) {}
388
389// C4DefinitionSelDlg
390
391C4DefinitionSelDlg::C4DefinitionSelDlg(C4FileSel_BaseCB *pSelCallback, const std::vector<std::string> &fixedSelection)
392 : C4FileSelDlg(Config.AtExePath(szFilename: Config.General.DefinitionPath), LoadResStr(id: C4ResStrTableKey::IDS_MSG_SELECT, args: LoadResStr(id: C4ResStrTableKey::IDS_DLG_DEFINITIONS)).c_str(), pSelCallback), fixedSelection(fixedSelection)
393{
394}
395
396void C4DefinitionSelDlg::OnShown()
397{
398 // base call: load file list
399 C4FileSelDlg::OnShown();
400 // initial selection
401 if (fixedSelection.size()) SetSelection(newSelection: fixedSelection, fFilenameOnly: true);
402}
403
404bool C4DefinitionSelDlg::IsItemGrayed(const char *szFilename) const
405{
406 // cannot change initial selection
407 if (fixedSelection.empty()) return false;
408 return std::find(first: fixedSelection.begin(), last: fixedSelection.end(), val: GetFilename(path: szFilename)) != fixedSelection.end();
409}
410
411bool C4DefinitionSelDlg::SelectDefinitions(C4GUI::Screen *pOnScreen, std::vector<std::string> &selection)
412{
413 // let the user select definitions by showing a modal selection dialog
414 C4DefinitionSelDlg *pDlg = new C4DefinitionSelDlg(nullptr, selection);
415 bool fResult;
416 if (fResult = pOnScreen->ShowModalDlg(pDlg, fDestruct: false))
417 {
418 selection = pDlg->GetSelection(fixedSelection: selection, filenameOnly: true);
419 }
420 if (C4GUI::IsGUIValid()) delete pDlg;
421 return fResult;
422}
423
424// C4PortraitSelDlg::ListItem
425
426C4PortraitSelDlg::ListItem::ListItem(const char *szFilename) : C4FileSelDlg::ListItem(szFilename)
427, fError(false), fLoaded(false)
428{
429 CStdFont *pUseFont = &(C4GUI::GetRes()->MiniFont);
430 // determine label text
431 StdStrBuf sDisplayLabel;
432 if (szFilename)
433 {
434 sDisplayLabel.Copy(pnData: ::GetFilename(path: szFilename));
435 ::RemoveExtension(psFileName: &sDisplayLabel);
436 }
437 else
438 {
439 sDisplayLabel.Ref(pnData: LoadResStr(id: C4ResStrTableKey::IDS_MSG_NOPORTRAIT));
440 }
441 // insert linebreaks into label text
442 int32_t iLineHgt = std::max<int32_t>(a: pUseFont->BreakMessage(szMsg: sDisplayLabel.getData(), iWdt: ImagePreviewSize - 6, pOut: &sFilenameLabelText, fCheckMarkup: false), b: 1);
443 // set size
444 SetBounds(C4Rect(0, 0, ImagePreviewSize, ImagePreviewSize + iLineHgt));
445}
446
447void C4PortraitSelDlg::ListItem::Load()
448{
449 if (sFilename)
450 {
451 // safety
452 fLoaded = false;
453 // load image file
454 C4Group SrcGrp;
455 StdStrBuf sParentPath;
456 GetParentPath(szFilename: sFilename.getData(), outBuf: &sParentPath);
457 bool fLoadError = true;
458 if (SrcGrp.Open(szGroupName: sParentPath.getData()))
459 if (fctLoadedImage.Load(hGroup&: SrcGrp, szName: ::GetFilename(path: sFilename.getData())))
460 {
461 // image loaded. Can only be put into facet by main thread, because those operations aren't thread safe
462 fLoaded = true;
463 fLoadError = false;
464 }
465 SrcGrp.Close();
466 fError = fLoadError;
467 }
468}
469
470void C4PortraitSelDlg::ListItem::DrawElement(C4FacetEx &cgo)
471{
472 // Scale down newly loaded image?
473 if (fLoaded)
474 {
475 fLoaded = false;
476 if (!fctImage.CopyFromSfcMaxSize(srcSfc&: fctLoadedImage.GetFace(), iMaxSize: ImagePreviewSize))
477 fError = true;
478 fctLoadedImage.GetFace().Clear();
479 fctLoadedImage.Clear();
480 }
481 // Draw picture
482 CStdFont *pUseFont = &(C4GUI::GetRes()->MiniFont);
483 C4Facet cgoPicture(cgo.Surface, cgo.TargetX + rcBounds.x, cgo.TargetY + rcBounds.y, ImagePreviewSize, ImagePreviewSize);
484 if (fError || !sFilename)
485 {
486 C4Facet &fctNoneImg = Game.GraphicsResource.fctOKCancel;
487 fctNoneImg.Draw(sfcTarget: cgoPicture.Surface, iX: cgoPicture.X + (cgoPicture.Wdt - fctNoneImg.Wdt) / 2, iY: cgoPicture.Y + (cgoPicture.Hgt - fctNoneImg.Hgt) / 2, iPhaseX: 1, iPhaseY: 0);
488 }
489 else
490 {
491 if (!fctImage.Surface)
492 {
493 // not loaded yet
494 lpDDraw->TextOut(szText: LoadResStr(id: C4ResStrTableKey::IDS_PRC_INITIALIZE), rFont&: C4GUI::GetRes()->MiniFont, fZoom: 1.0f, sfcDest: cgo.Surface, iTx: cgoPicture.X + cgoPicture.Wdt / 2, iTy: cgoPicture.Y + (cgoPicture.Hgt - C4GUI::GetRes()->MiniFont.GetLineHeight()) / 2, C4GUI_StatusFontClr, byForm: ACenter, fDoMarkup: false);
495 }
496 else
497 {
498 fctImage.Draw(cgo&: cgoPicture);
499 }
500 }
501 // draw filename
502 lpDDraw->TextOut(szText: sFilenameLabelText.getData(), rFont&: *pUseFont, fZoom: 1.0f, sfcDest: cgo.Surface, iTx: cgoPicture.X + rcBounds.Wdt / 2, iTy: cgoPicture.Y + cgoPicture.Hgt, C4GUI_MessageFontClr, byForm: ACenter, fDoMarkup: false);
503}
504
505// C4PortraitSelDlg::ImageLoader
506
507void C4PortraitSelDlg::ImageLoader::ClearLoadItems()
508{
509 // clear list
510 LoadItems.clear();
511}
512
513void C4PortraitSelDlg::ImageLoader::AddLoadItem(ListItem *pItem)
514{
515 LoadItems.push_back(x: pItem);
516}
517
518void C4PortraitSelDlg::ImageLoader::Execute()
519{
520 // list empty?
521 if (!LoadItems.size())
522 {
523 // then we're done!
524 return;
525 }
526 // load one item at the time
527 ListItem *pLoadItem = LoadItems.front();
528 pLoadItem->Load();
529 LoadItems.pop_front();
530}
531
532// C4PortraitSelDlg
533
534C4PortraitSelDlg::C4PortraitSelDlg(C4FileSel_BaseCB *pSelCallback, bool fSetPicture, bool fSetBigIcon)
535 : C4FileSelDlg(Config.General.ExePath, LoadResStr(id: C4ResStrTableKey::IDS_MSG_SELECT, args: LoadResStr(id: C4ResStrTableKey::IDS_TYPE_PORTRAIT)).c_str(), pSelCallback, false)
536 , pCheckSetPicture(nullptr), pCheckSetBigIcon(nullptr), fDefSetPicture(fSetPicture), fDefSetBigIcon(fSetBigIcon)
537{
538 char path[_MAX_PATH + 1];
539 // add common picture locations
540 StdStrBuf strLocation;
541 SCopy(szSource: Config.AtUserPath(szFilename: ""), sTarget: path, _MAX_PATH); TruncateBackslash(szFilename: path);
542 AddLocation(szName: std::format(C4ENGINECAPTION " {}", args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_USERPATH)).c_str(), szPath: path);
543 AddCheckedLocation(szName: std::format(C4ENGINECAPTION " {}", args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_PROGRAMDIRECTORY)).c_str(), szPath: Config.General.ExePath);
544#ifdef _WIN32
545 if (SHGetSpecialFolderPathA(nullptr, path, CSIDL_PERSONAL, FALSE)) AddCheckedLocation(LoadResStr(C4ResStrTableKey::IDS_TEXT_MYDOCUMENTS), path);
546 if (SHGetSpecialFolderPathA(nullptr, path, CSIDL_MYPICTURES, FALSE)) AddCheckedLocation(LoadResStr(C4ResStrTableKey::IDS_TEXT_MYPICTURES), path);
547 if (SHGetSpecialFolderPathA(nullptr, path, CSIDL_DESKTOPDIRECTORY, FALSE)) AddCheckedLocation(LoadResStr(C4ResStrTableKey::IDS_TEXT_DESKTOP), path);
548#endif
549#ifdef __APPLE__
550 AddCheckedLocation(LoadResStr(C4ResStrTableKey::IDS_TEXT_HOME), getenv("HOME"));
551#else
552 AddCheckedLocation(szName: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_HOMEFOLDER), szPath: getenv(name: "HOME"));
553#endif
554#ifndef _WIN32
555 FormatWithNull(buf&: path, fmt: "{}" DirSep "Desktop", args: getenv(name: "HOME"));
556 AddCheckedLocation(szName: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_DESKTOP), szPath: path);
557#endif
558 // build dialog
559 InitElements();
560 // select last location
561 SetCurrentLocation(idx: Config.Startup.LastPortraitFolderIdx, fRefresh: false);
562}
563
564void C4PortraitSelDlg::AddExtraOptions(const C4Rect &rcOptionsRect)
565{
566 C4GUI::ComponentAligner caOptions(rcOptionsRect, C4GUI_DefDlgIndent, C4GUI_DefDlgSmallIndent, false);
567 CStdFont *pUseFont = &(C4GUI::GetRes()->TextFont);
568 AddElement(pChild: new C4GUI::Label(LoadResStr(id: C4ResStrTableKey::IDS_CTL_IMPORTIMAGEAS), caOptions.GetGridCell(iSectX: 0, iSectXMax: 3, iSectY: 0, iSectYMax: 1, iSectSizeX: -1, iSectSizeY: pUseFont->GetLineHeight(), fCenterPos: true), ALeft));
569 AddElement(pChild: pCheckSetPicture = new C4GUI::CheckBox(caOptions.GetGridCell(iSectX: 1, iSectXMax: 3, iSectY: 0, iSectYMax: 1, iSectSizeX: -1, iSectSizeY: pUseFont->GetLineHeight(), fCenterPos: true), LoadResStr(id: C4ResStrTableKey::IDS_TEXT_PLAYERIMAGE), fDefSetPicture));
570 pCheckSetPicture->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DESC_CHANGESTHEIMAGEYOUSEEINTH));
571 AddElement(pChild: pCheckSetBigIcon = new C4GUI::CheckBox(caOptions.GetGridCell(iSectX: 2, iSectXMax: 3, iSectY: 0, iSectYMax: 1, iSectSizeX: -1, iSectSizeY: pUseFont->GetLineHeight(), fCenterPos: true), LoadResStr(id: C4ResStrTableKey::IDS_TEXT_LOBBYICON), fDefSetPicture));
572 pCheckSetBigIcon->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DESC_CHANGESTHEIMAGEYOUSEEINTH2));
573}
574
575void C4PortraitSelDlg::OnClosed(bool fOK)
576{
577 // remember location
578 Config.Startup.LastPortraitFolderIdx = GetCurrentLocationIndex();
579 // inherited
580 C4FileSelDlg::OnClosed(fOK);
581}
582
583C4FileSelDlg::ListItem *C4PortraitSelDlg::CreateListItem(const char *szFilename)
584{
585 // use own list item type
586 ListItem *pNew = new ListItem(szFilename);
587 // schedule image loading
588 ImageLoader.AddLoadItem(pItem: pNew);
589 return pNew;
590}
591
592void C4PortraitSelDlg::BeginFileListUpdate()
593{
594 // new file list. Stop loading current
595 ImageLoader.ClearLoadItems();
596}
597
598void C4PortraitSelDlg::OnIdle()
599{
600 // no multithreading. Workaround for image loading then...
601 static int32_t i = 0;
602 if (!(i++ % 10)) ImageLoader.Execute();
603}
604
605bool C4PortraitSelDlg::SelectPortrait(C4GUI::Screen *pOnScreen, std::string &selection, bool *pfSetPicture, bool *pfSetBigIcon)
606{
607 // copy some default potraits to UserPath (but only try this once, no real error handling)
608 if (!Config.General.UserPortraitsWritten)
609 {
610 LogNTr(level: spdlog::level::trace, message: "Copying default portraits to user path...");
611 C4Group hGroup;
612 if (hGroup.Open(szGroupName: Config.AtExePath(C4CFN_Graphics)))
613 {
614 hGroup.Extract(szFiles: "Portrait1.png", szExtractTo: Config.AtUserPath(szFilename: "Clonk.png"));
615 hGroup.Extract(szFiles: "PortraitBandit.png", szExtractTo: Config.AtUserPath(szFilename: "Bandit.png"));
616 hGroup.Extract(szFiles: "PortraitIndianChief.png", szExtractTo: Config.AtUserPath(szFilename: "IndianChief.png"));
617 hGroup.Extract(szFiles: "PortraitKing.png", szExtractTo: Config.AtUserPath(szFilename: "King.png"));
618 hGroup.Extract(szFiles: "PortraitKnight.png", szExtractTo: Config.AtUserPath(szFilename: "Knight.png"));
619 hGroup.Extract(szFiles: "PortraitMage.png", szExtractTo: Config.AtUserPath(szFilename: "Mage.png"));
620 hGroup.Extract(szFiles: "PortraitPiranha.png", szExtractTo: Config.AtUserPath(szFilename: "Piranha.png"));
621 hGroup.Extract(szFiles: "PortraitSheriff.png", szExtractTo: Config.AtUserPath(szFilename: "Sheriff.png"));
622 hGroup.Extract(szFiles: "PortraitWipf.png", szExtractTo: Config.AtUserPath(szFilename: "Wipf.png"));
623 hGroup.Close();
624 }
625 Config.General.UserPortraitsWritten = true;
626 }
627 // let the user select a portrait by showing a modal selection dialog
628 C4PortraitSelDlg *pDlg = new C4PortraitSelDlg(nullptr, *pfSetPicture, *pfSetBigIcon);
629 bool fResult = pOnScreen->ShowModalDlg(pDlg, fDestruct: false);
630 if (fResult)
631 {
632 std::vector<std::string> s{pDlg->GetSelection(fixedSelection: {}, filenameOnly: false)};
633 fResult = !s.empty();
634 if (fResult)
635 {
636 selection = *s.begin();
637 }
638 *pfSetPicture = pDlg->IsSetPicture();
639 *pfSetBigIcon = pDlg->IsSetBigIcon();
640 }
641 if (C4GUI::IsGUIValid()) delete pDlg;
642 return fResult;
643}
644