1/*
2 * LegacyClonk
3 *
4 * Copyright (c) RedWolf Design
5 * Copyright (c) 2005, 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#include "C4StartupOptionsAdvancedConfigDialog.h"
19#include "C4Config.h"
20#include "C4Gui.h"
21#include "C4GuiEdit.h"
22#include "C4GuiListBox.h"
23#include "C4GuiSpinBox.h"
24#include "C4GuiResource.h"
25#include "C4GuiTabular.h"
26#include "StdCompiler.h"
27#include "C4ResStrTable.h"
28
29#include <algorithm>
30#include <format>
31#include <ranges>
32#include <stdexcept>
33
34using ConfigDialog = C4StartupOptionsAdvancedConfigDialog;
35
36namespace
37{
38template <typename Impl>
39class StdCompilerConfigGuiBase : public StdCompiler
40{
41public:
42 StdCompilerConfigGuiBase(ConfigDialog *dialog) : dialog{dialog} {}
43
44 bool hasNaming() override { return true; }
45
46 void QWord(int64_t &rInt) override { return HandleSettingInternal(rInt); }
47 void QWord(uint64_t &rInt) override { return HandleSettingInternal(rInt); }
48 void DWord(int32_t &rInt) override { return HandleSettingInternal(rInt); }
49 void DWord(uint32_t &rInt) override { return HandleSettingInternal(rInt); }
50 void Word(int16_t &rShort) override { return HandleSettingInternal(rShort); }
51 void Word(uint16_t &rShort) override { return HandleSettingInternal(rShort); }
52 void Byte(int8_t &rByte) override { return HandleSettingInternal(rByte); }
53 void Byte(uint8_t &rByte) override { return HandleSettingInternal(rByte); }
54 void Boolean(bool &rBool) override { return HandleSettingInternal(rBool); }
55 void Character(char &rChar) override { return HandleSettingInternal(rChar); }
56
57 void String(char *szString, size_t iMaxLength, RawCompileType type = RCT_Escaped) override { return HandleSettingInternal(szString, iMaxLength, type); }
58 void String(std::string &str, RawCompileType type = RCT_Escaped) override { return HandleSettingInternal(str, type); }
59 void Raw(void *pData, size_t iSize, RawCompileType type = RCT_Escaped) override { return HandleSettingInternal(pData, iSize, type); }
60
61 bool Default(const char *name) override { return false; }
62
63 NameGuard Name(const char *name) override
64 {
65 if (++level > 2)
66 {
67 ignore = true;
68 return {this, false};
69 }
70 else
71 {
72 lastName = name;
73 if (level == 1)
74 {
75 dialog->ChangeSection(name);
76 }
77 }
78 return StdCompiler::Name(szName: name);
79 }
80 void NameEnd(bool breaking = false) override
81 {
82 assert(level > 0);
83 --level;
84 ignore = false;
85 return StdCompiler::NameEnd(fBreak: breaking);
86 }
87protected:
88 ConfigDialog *dialog;
89
90private:
91 auto &self() { return static_cast<Impl&>(*this); }
92
93 template <typename T, typename... Args>
94 void HandleSettingInternal(T &setting, Args &&... args)
95 {
96 if (ignore) return;
97
98 try
99 {
100 self().HandleSetting(lastName, setting, std::forward<Args>(args)...);
101 }
102 catch (const std::out_of_range &e)
103 {
104 excNotFound(e.what());
105 }
106 }
107
108 std::string lastName;
109 std::size_t level{0};
110 bool ignore{false};
111};
112
113class StdCompilerConfigGuiRead : public StdCompilerConfigGuiBase<StdCompilerConfigGuiRead>
114{
115 using Base = StdCompilerConfigGuiBase<StdCompilerConfigGuiRead>;
116
117public:
118 using Base::Base;
119
120 bool isCompiler() override { return false; }
121
122 template <typename T>
123 void HandleSetting(std::string_view name, T &number)
124 {
125 dialog->AddSetting(name, number);
126 }
127
128 void HandleSetting(std::string_view name, std::string &str, RawCompileType type)
129 {
130 dialog->AddSetting(name, text&: str);
131 }
132
133 void HandleSetting(std::string_view name, char *str, std::size_t maxLength, RawCompileType type)
134 {
135 dialog->AddStringSetting(name, text: str, maxLength);
136 }
137
138 void HandleSetting(std::string_view name, void *data, std::size_t length, RawCompileType type)
139 {
140 dialog->AddText(name, text: "Raw data");
141 }
142};
143
144class StdCompilerConfigGuiWrite : public StdCompilerConfigGuiBase<StdCompilerConfigGuiWrite>
145{
146 using Base = StdCompilerConfigGuiBase<StdCompilerConfigGuiWrite>;
147
148public:
149 using Base::Base;
150
151 bool isCompiler() override { return true; }
152
153 template <typename T>
154 void HandleSetting(std::string_view name, T &number)
155 {
156 dialog->GetSetting(name, number);
157 }
158
159 void HandleSetting(std::string_view name, std::string &str, RawCompileType type)
160 {
161 std::string val;
162 dialog->GetSetting(name, text&: val);
163 if (type == RCT_Idtf || type == RCT_IdtfAllowEmpty || type == RCT_ID)
164 {
165 const auto wrongChar = std::ranges::find_if_not(val, IsIdentifierChar);
166 if (wrongChar != std::ranges::end(val))
167 {
168 val.erase(first: wrongChar, last: val.end());
169 }
170 }
171
172 if (type == RCT_Idtf || type == RCT_ID)
173 {
174 if (val.empty())
175 {
176 excNotFound(message: "String");
177 }
178 }
179
180 str = std::move(val);
181 }
182
183 void HandleSetting(std::string_view name, char *str, std::size_t maxLength, RawCompileType type)
184 {
185 std::string val;
186 HandleSetting(name, str&: val, type);
187
188 if (val.size() > maxLength)
189 {
190 std::strncpy(dest: str, src: val.c_str(), n: maxLength);
191 }
192 else
193 {
194 std::strcpy(dest: str, src: val.c_str());
195 }
196 }
197
198 void HandleSetting(std::string_view name, void *data, std::size_t length, RawCompileType type)
199 {
200
201 }
202};
203
204struct BlockedSetting {
205 std::string_view section;
206 std::string_view name;
207
208 std::strong_ordering operator<=>(const BlockedSetting &other) const = default;
209};
210
211constexpr BlockedSetting blockedSettings[]
212{
213 {.section: "General", .name: "Version"},
214 {.section: "General", .name: "ConfigResetSafety"},
215};
216
217class SectionSheet : public C4GUI::Tabular::Sheet
218{
219public:
220 SectionSheet(const char *title, const C4Rect &bounds, Element *mainContent) : Sheet{title, bounds}, mainContent{mainContent} {}
221
222 void RemoveElement(Element *element) override
223 {
224 if (element == mainContent)
225 {
226 mainContent = nullptr;
227 }
228 }
229
230 void UpdateSize() override
231 {
232 if (mainContent)
233 {
234 mainContent->SetBounds(GetContainedClientRect());
235 }
236 }
237
238private:
239 Element *mainContent;
240};
241}
242
243struct ConfigDialog::Section
244{
245 std::string name;
246 C4GUI::Tabular::Sheet *tab;
247 C4GUI::ListBox *list;
248 std::unordered_map<std::string, Setting, string_hash, std::equal_to<>> settings;
249};
250
251ConfigDialog::C4StartupOptionsAdvancedConfigDialog(std::int32_t width, std::int32_t height) : C4GUI::Dialog{width, height, LoadResStr(id: C4ResStrTableKey::IDS_DLG_ADVANCED_SETTINGS), false}
252{
253 StdCompilerConfigGuiRead comp{this};
254 saveButton = new C4GUI::CallbackButton<ConfigDialog>{LoadResStr(id: C4ResStrTableKey::IDS_BTN_SAVE), {0, 0, 1, 1}, &ConfigDialog::OnSave, this};
255 cancelButton = new C4GUI::CallbackButton<ConfigDialog>{LoadResStr(id: C4ResStrTableKey::IDS_BTN_CANCEL), {0, 0, 1, 1}, &ConfigDialog::OnAbort, this};
256 tabs = new C4GUI::Tabular{C4Rect{0, 0, 0, 0}, C4GUI::Tabular::tbLeft};
257 AddElement(pChild: tabs);
258 AddElement(pChild: saveButton);
259 AddElement(pChild: cancelButton);
260 UpdateSize();
261 comp.Decompile(rStruct: Config);
262}
263
264const char *ConfigDialog::GetID()
265{
266 return "OptionsAdvancedConfigDialog";
267}
268
269void ConfigDialog::UpdateSize()
270{
271 Dialog::UpdateSize();
272
273 static constexpr std::int32_t buttonMargin{10};
274 static constexpr std::int32_t buttonWidth{200};
275
276 C4GUI::ComponentAligner ca{GetContainedClientRect(), 0, 0};
277 C4GUI::ComponentAligner caBottom{ca.GetFromBottom(C4GUI_ButtonHgt + 2 * buttonMargin), 0, buttonMargin};
278 tabs->SetBounds(ca.GetAll());
279 if (caBottom.GetWidth() > 2 * buttonWidth + 4 * buttonMargin)
280 {
281 caBottom.GetFromLeft(iWdt: tabs->GetTabButtonWidth());
282 }
283 C4GUI::ComponentAligner caButtons{caBottom.GetCentered(iWdt: std::min(a: 600, b: caBottom.GetWidth()), iHgt: caBottom.GetHeight()), 0, buttonMargin};
284 const auto actualButtonWidth = std::min(a: buttonWidth, b: caButtons.GetInnerWidth() / 2);
285 saveButton->SetBounds(caButtons.GetFromLeft(iWdt: actualButtonWidth));
286 cancelButton->SetBounds(caButtons.GetFromRight(iWdt: actualButtonWidth));
287}
288
289void ConfigDialog::AddText(std::string_view name, std::string_view value) const
290{
291 const auto label = new C4GUI::Label{value, 0, 0};
292 AddSettingInternal(name, control: label);
293}
294
295void ConfigDialog::AddSettingInternal(std::string_view name, C4GUI::Element *control) const
296{
297 auto &setting = currentSection->settings.emplace(args: std::piecewise_construct, args: std::forward_as_tuple(args&: name), args: std::forward_as_tuple(args&: name, args&: control)).first->second;
298 setting.SetBounds({0, 0, currentSection->list->GetItemWidth(), setting.GetHeight()});
299 currentSection->list->AddElement(pChild: &setting);
300}
301
302void ConfigDialog::ChangeSection(const char *name)
303{
304 if (sections.contains(x: name))
305 {
306 currentSection = &sections.find(x: name)->second;
307 }
308 else
309 {
310 auto listBox = new C4GUI::ListBox{tabs->GetContainedClientRect()};
311 const auto tab = new SectionSheet{name, tabs->GetContainedClientRect(), listBox};
312 tabs->AddCustomSheet(pAddSheet: tab);
313 currentSection = &sections.emplace(args&: name, args: Section{.name: name, .tab: tab, .list: listBox}).first->second;
314 tab->AddElement(pChild: listBox);
315 UpdateSize();
316 }
317}
318
319bool ConfigDialog::ShowModal(C4GUI::Screen *screen)
320{
321 auto warningDialog = new C4GUI::MessageDialog{LoadResStr(id: C4ResStrTableKey::IDS_MSG_ADVANCED_SETTINGS_WARNING), LoadResStr(id: C4ResStrTableKey::IDS_DLG_WARNING), C4GUI::MessageDialog::btnOKAbort, C4GUI::Ico_None, C4GUI::MessageDialog::dsRegular, nullptr, true};
322 if (!screen->ShowModalDlg(pDlg: warningDialog))
323 {
324 return false;
325 }
326 return screen->ShowModalDlg(pDlg: new C4StartupOptionsAdvancedConfigDialog{screen->GetWidth() * 3 / 4, screen->GetHeight() * 3 / 4});
327}
328
329bool ConfigDialog::OnEnter()
330{
331 return false;
332}
333
334void ConfigDialog::UserClose(bool ok)
335{
336 // don’t close on enter
337 if (!ok)
338 {
339 Dialog::UserClose(fOK: ok);
340 }
341}
342
343ConfigDialog::Setting::Setting(std::string_view label, C4GUI::Element *control) : label{std::format(fmt: "{}: ", args&: label).c_str(), 0, 0}, control{control}
344{
345 AddElement(pChild: &this->label);
346 AddElement(pChild: control);
347 UpdateSize(adjustHeight: true);
348}
349
350void ConfigDialog::Setting::UpdateSize()
351{
352 Window::UpdateSize();
353
354 return UpdateSize(adjustHeight: false);
355}
356
357void ConfigDialog::Setting::UpdateSize(bool adjustHeight)
358{
359 if (adjustHeight)
360 {
361 SetBounds({rcBounds.x, rcBounds.y, rcBounds.Wdt, control->GetHeight() + GetMarginTop() + GetMarginBottom()});
362 }
363 else
364 {
365 C4GUI::ComponentAligner ca{GetContainedClientRect(), Margin, 0};
366 const auto labelWidth = C4GUI::GetRes()->TextFont.GetTextWidth(szText: label.GetText(), fCheckMarkup: false);
367 label.SetBounds(ca.GetFromLeft(iWdt: labelWidth));
368 control->SetBounds(ca.GetAll());
369 }
370}
371
372C4GUI::Element *ConfigDialog::Setting::GetControl() const { return control; }
373
374template <std::integral T>
375void ConfigDialog::AddSetting(std::string_view name, T &setting) const
376{
377 if (IsBlocked(name))
378 {
379 return AddText(name, value: std::format("{}", setting));
380 }
381
382 const auto edit = new C4GUI::SpinBox<T>{C4Rect{0, 0, 0, C4GUI::Edit::GetDefaultEditHeight()}};
383 edit->SetValue(setting, false);
384 return AddSettingInternal(name, control: edit);
385}
386
387void ConfigDialog::AddSetting(std::string_view name, bool &setting) const
388{
389 if (IsBlocked(name))
390 {
391 return AddText(name, value: std::format(fmt: "{}", args&: setting));
392 }
393
394 const auto edit = new C4GUI::CheckBox{C4Rect{0, 0, 0, C4GUI::Edit::GetDefaultEditHeight()}, "", setting};
395 return AddSettingInternal(name, control: edit);
396}
397
398void ConfigDialog::AddSetting(std::string_view name, char &setting) const
399{
400 if (IsBlocked(name))
401 {
402 return AddText(name, value: std::format(fmt: "{}", args&: setting));
403 }
404
405 char text[]{setting};
406 return AddStringSetting(name, text, maxLength: 1);
407}
408
409void ConfigDialog::AddStringSetting(std::string_view name, char *text, std::size_t maxLength) const
410{
411 if (IsBlocked(name))
412 {
413 return AddText(name, value: text);
414 }
415
416 std::string str{text};
417 return AddSetting(name, text&: str);
418}
419
420void ConfigDialog::AddSetting(std::string_view name, std::string &text, std::size_t maxLength) const
421{
422 if (IsBlocked(name))
423 {
424 AddText(name, value: text);
425 }
426
427 const auto edit = new C4GUI::Edit{C4Rect{0, 0, 0, C4GUI::Edit::GetDefaultEditHeight()}};
428 edit->SetText(text, fUser: false);
429 if (maxLength != std::string::npos)
430 {
431 edit->SetMaxText(maxLength);
432 }
433 return AddSettingInternal(name, control: edit);
434}
435
436template <std::integral T>
437void ConfigDialog::GetSetting(std::string_view name, T &setting) const
438{
439 if (IsBlocked(name))
440 {
441 return;
442 }
443
444 setting = GetSettingInternal<C4GUI::SpinBox<T>>(name)->GetValue();
445}
446
447void ConfigDialog::GetSetting(std::string_view name, bool &setting) const
448{
449 if (IsBlocked(name))
450 {
451 return;
452 }
453
454 setting = GetSettingInternal<C4GUI::CheckBox>(name)->GetChecked();
455}
456
457void ConfigDialog::GetSetting(std::string_view name, char &setting) const
458{
459 if (IsBlocked(name))
460 {
461 return;
462 }
463
464 const auto text = GetSettingInternal<C4GUI::Edit>(name)->GetText();
465 setting = text[0];
466}
467
468void ConfigDialog::GetSetting(std::string_view name, std::string &text) const
469{
470 if (IsBlocked(name))
471 {
472 return;
473 }
474
475 text = GetSettingInternal<C4GUI::Edit>(name)->GetText();
476}
477
478template <typename T>
479T *ConfigDialog::GetSettingInternal(std::string_view name) const
480{
481 const auto it = currentSection->settings.find(x: name);
482 if (it == currentSection->settings.end())
483 {
484 throw std::out_of_range{std::format(fmt: "Unknown setting: [{}]/{}", args&: currentSection->name, args&: name)};
485 }
486
487 const auto control = dynamic_cast<T *>(it->second.GetControl());
488 if (!control)
489 {
490 throw std::out_of_range{std::format(fmt: "Setting type mismatch: [{}]/{}", args&: currentSection->name, args&: name)};
491 }
492 return control;
493}
494
495void ConfigDialog::OnSave(C4GUI::Control *)
496{
497 StdCompilerConfigGuiWrite comp{this};
498 comp.Compile(rStruct&: Config);
499 Close(fOK: true);
500}
501
502void ConfigDialog::OnAbort(C4GUI::Control *)
503{
504 Close(fOK: false);
505}
506
507bool ConfigDialog::IsBlocked(std::string_view name) const
508{
509 return std::ranges::find(blockedSettings, BlockedSetting{.section: currentSection->name, .name: name}) != std::ranges::end(blockedSettings);
510}
511