1 #include "SaveFileDialog.h"
2
3 #include "ClientUI.h"
4 #include "CUIControls.h"
5 #include "OptionsWnd.h"
6 #include "../client/human/HumanClientApp.h"
7 #include "../network/Networking.h"
8 #include "../util/i18n.h"
9 #include "../util/OptionsDB.h"
10 #include "../util/Directories.h"
11 #include "../util/Logger.h"
12 #include "../util/SaveGamePreviewUtils.h"
13 #include "../util/VarText.h"
14
15 #include <GG/Button.h>
16 #include <GG/Clr.h>
17 #include <GG/dialogs/ThreeButtonDlg.h>
18 #include <GG/utf8/checked.h>
19
20 #include <boost/filesystem/path.hpp>
21 #include <boost/filesystem/operations.hpp>
22 #include <boost/cast.hpp>
23 #include <boost/format.hpp>
24
25 #include <memory>
26 #include <string>
27
28 namespace fs = boost::filesystem;
29
30 using std::vector;
31 using std::string;
32
33 namespace {
34 const GG::X SAVE_FILE_DIALOG_WIDTH(600);
35 const GG::Y SAVE_FILE_DIALOG_HEIGHT(400);
36 const GG::X SAVE_FILE_DIALOG_MIN_WIDTH(160);
37 const GG::Y SAVE_FILE_DIALOG_MIN_HEIGHT(100);
38
39 const std::string SAVE_FILE_WND_NAME = "dialog.save";
40
41 const GG::X PROMT_WIDTH(200);
42 const GG::Y PROMPT_HEIGHT(75);
43
44 const double DEFAULT_STRETCH = 1.0;
45
46 const GG::X SAVE_FILE_BUTTON_MARGIN ( 10 );
47 const unsigned int SAVE_FILE_CELL_MARGIN = 2;
48 const unsigned int ROW_MARGIN = 2;
49
50 const std::string PATH_DELIM_BEGIN = "[";
51 const std::string PATH_DELIM_END = "]";
52
53 const std::string WIDE_AS = "wide-as";
54 const std::string STRETCH = "stretch";
55
56 const std::string SERVER_LABEL = "SERVER";
57
58 const std::string VALID_PREVIEW_COLUMNS[] = {
59 "player", "empire", "turn", "time", "file", "seed", "galaxy_age", "galaxy_size", "galaxy_shape",
60 "monster_freq", "native_freq", "planet_freq", "specials_freq", "starlane_freq", "ai_aggression",
61 "number_of_empires", "number_of_humans"
62 };
63
64 const unsigned int VALID_PREVIEW_COLUMN_COUNT = sizeof(VALID_PREVIEW_COLUMNS) / sizeof(std::string);
65
66 const int WHEEL_INCREMENT = 80;
67
68 // command-line options
AddOptions(OptionsDB & db)69 void AddOptions(OptionsDB& db) {
70 // List the columns to show, separated by colons.
71 // Valid: time, turn, player, empire, systems, seed, galaxy_age, galaxy_shape, planet_freq, native_freq, specials_freq, starlane_freq
72 // These settings are not visible in the options panel; the defaults should be good for regular users.
73 db.Add<std::vector<std::string>>("ui.dialog.save.columns", UserStringNop("OPTIONS_DB_UI_SAVE_DIALOG_COLUMNS"),
74 StringToList("time,turn,player,empire,file"));
75 db.Add<std::string>("ui.dialog.save.columns.time." + WIDE_AS,
76 UserStringNop("OPTIONS_DB_UI_SAVE_DIALOG_COLUMN_WIDE_AS"),
77 "YYYY-MM-DD");
78 db.Add<std::string>("ui.dialog.save.columns.turn." + WIDE_AS,
79 UserStringNop("OPTIONS_DB_UI_SAVE_DIALOG_COLUMN_WIDE_AS"),
80 "9999");
81 db.Add("ui.dialog.save.columns.player." + STRETCH, UserStringNop("OPTIONS_DB_UI_SAVE_DIALOG_COLUMN_STRETCH"), 1.0);
82 db.Add("ui.dialog.save.columns.empire." + STRETCH, UserStringNop("OPTIONS_DB_UI_SAVE_DIALOG_COLUMN_STRETCH"), 1.0);
83 db.Add("ui.dialog.save.columns.file." + STRETCH, UserStringNop("OPTIONS_DB_UI_SAVE_DIALOG_COLUMN_STRETCH"), 2.0);
84 db.Add<std::string>("ui.save.columns.galaxy_size." + WIDE_AS, UserStringNop("OPTIONS_DB_UI_SAVE_DIALOG_COLUMN_WIDE_AS"), "9999");
85 db.Add("ui.dialog.save.columns.seed." + STRETCH, UserStringNop("OPTIONS_DB_UI_SAVE_DIALOG_COLUMN_STRETCH"), 0.75);
86
87 db.Add("ui.dialog.save.columns.default." + STRETCH, UserStringNop("OPTIONS_DB_UI_SAVE_DIALOG_COLUMN_STRETCH"), 1.0);
88
89 // We give them a custom delay since the general one is a bit quick
90 db.Add("ui.dialog.save.tooltip.delay", UserStringNop("OPTIONS_DB_UI_SAVE_DIALOG_TOOLTIP_DELAY"), 800);
91 }
92 bool temp_bool = RegisterOptions(&AddOptions);
93
94 /// Creates a text control that support resizing and word wrap.
CreateResizingText(const std::string & string,GG::X width)95 std::shared_ptr<GG::Label> CreateResizingText(const std::string& string, GG::X width) {
96 // Calculate the extent manually to ensure the control stretches to full
97 // width when possible. Otherwise it would always word break.
98 GG::Flags<GG::TextFormat> fmt = GG::FORMAT_NONE;
99 std::vector<std::shared_ptr<GG::Font::TextElement>> text_elements =
100 ClientUI::GetFont()->ExpensiveParseFromTextToTextElements(string, fmt);
101 std::vector<GG::Font::LineData> lines = ClientUI::GetFont()->DetermineLines(string, fmt, width, text_elements);
102 GG::Pt extent = ClientUI::GetFont()->TextExtent(lines);
103 auto text = GG::Wnd::Create<CUILabel>(string, text_elements,
104 GG::FORMAT_WORDBREAK | GG::FORMAT_LEFT, GG::NO_WND_FLAGS,
105 GG::X0, GG::Y0, extent.x, extent.y);
106 text->ClipText(true);
107 text->SetChildClippingMode(GG::Wnd::ClipToClient);
108 return text;
109 }
110
Prompt(const std::string & question)111 bool Prompt(const std::string& question) {
112 std::shared_ptr<GG::Font> font = ClientUI::GetFont();
113 auto prompt = GG::GUI::GetGUI()->GetStyleFactory()->NewThreeButtonDlg(
114 PROMT_WIDTH, PROMPT_HEIGHT, question, font,
115 ClientUI::CtrlColor(), ClientUI::CtrlBorderColor(), ClientUI::CtrlColor(), ClientUI::TextColor(),
116 2, UserString("YES"), UserString("CANCEL"));
117 prompt->Run();
118 return prompt->Result() == 0;
119 }
120 }
121
122 /** Describes how a column should be set up in the dialog */
123 class SaveFileColumn {
124 public:
GetColumns(GG::X max_width)125 static std::shared_ptr<std::vector<SaveFileColumn>> GetColumns(GG::X max_width) {
126 auto columns = std::make_shared<std::vector<SaveFileColumn>>();
127 for (unsigned int i = 0; i < VALID_PREVIEW_COLUMN_COUNT; ++i)
128 columns->push_back(GetColumn(VALID_PREVIEW_COLUMNS[i], max_width));
129 return columns;
130 }
131
TitleForColumn(const SaveFileColumn & column)132 static std::shared_ptr<GG::Control> TitleForColumn(const SaveFileColumn& column) {
133 auto retval = GG::Wnd::Create<CUILabel>(column.Title(), GG::FORMAT_LEFT);
134 retval->Resize(GG::Pt(GG::X1, ClientUI::GetFont()->Height()));
135 return retval;
136 }
137
CellForColumn(const SaveFileColumn & column,const FullPreview & full,GG::X max_width)138 static std::shared_ptr<GG::Control> CellForColumn(const SaveFileColumn& column,
139 const FullPreview& full, GG::X max_width)
140 {
141 GG::Clr color = ClientUI::TextColor();
142 std::string value = ColumnInPreview(full, column.m_name);
143 if (column.m_name == "empire")
144 color = full.preview.main_player_empire_colour;
145
146 GG::Flags<GG::TextFormat> format_flags = GG::FORMAT_LEFT;
147 if (column.m_name == "turn")
148 format_flags = GG::FORMAT_CENTER;
149
150 std::shared_ptr<GG::Label> retval;
151
152 if (column.m_fixed) {
153 retval = GG::Wnd::Create<CUILabel>(value, format_flags, GG::NO_WND_FLAGS,
154 GG::X0, GG::Y0,
155 column.FixedWidth(), ClientUI::GetFont()->Height());
156 } else {
157 retval = CreateResizingText(value, max_width);
158 }
159
160 retval->SetTextColor(color);
161 return retval;
162 }
163
Fixed() const164 bool Fixed() const
165 { return m_fixed; }
166
FixedWidth() const167 GG::X FixedWidth() const
168 { return m_fixed_width; }
169
Stretch() const170 double Stretch() const
171 { return m_stretch; }
172
Name() const173 const std::string& Name() const
174 { return m_name; }
175
176 private:
GetColumn(const std::string & name,GG::X max_width)177 static SaveFileColumn GetColumn(const std::string& name, GG::X max_width) {
178 const std::string prefix = "ui.dialog.save.columns.";
179 std::string option = prefix + name + ".";
180 OptionsDB& db = GetOptionsDB();
181
182 if (db.OptionExists(option + WIDE_AS)) {
183 return SaveFileColumn(name, db.Get<std::string>(option + WIDE_AS), max_width);
184 } else if (db.OptionExists(option + STRETCH)) {
185 return SaveFileColumn(name, db.Get<double>(option + STRETCH), max_width);
186 } else if (db.OptionExists(prefix + "default." + STRETCH)) {
187 return SaveFileColumn(name, db.Get<double>(prefix + "default." + STRETCH), max_width);
188 } else {
189 return SaveFileColumn(name, DEFAULT_STRETCH, max_width);
190 }
191 }
192
193 /// Creates a fixed width column
SaveFileColumn(const std::string & name,const std::string & wide_as,GG::X max_width)194 SaveFileColumn(const std::string& name, const std::string& wide_as, GG::X max_width) :
195 m_name(name),
196 m_fixed(true),
197 m_fixed_width(GG::X0),
198 m_wide_as(wide_as),
199 m_stretch(0.0)
200 { m_fixed_width = ComputeFixedWidth(Title(), m_wide_as, max_width);}
201
202 /// Creates a stretchy column
SaveFileColumn(const std::string & name,double stretch,GG::X max_width)203 SaveFileColumn(const std::string& name, double stretch, GG::X max_width) :
204 m_name(name),
205 m_fixed(false),
206 m_fixed_width(GG::X0),
207 m_wide_as(),
208 m_stretch(stretch)
209 { m_fixed_width = ComputeFixedWidth(Title(), m_wide_as, max_width);}
210
Title() const211 std::string Title() const {
212 if (m_name == "player") {
213 return UserString("SAVE_PLAYER_TITLE");
214 } else if( m_name == "empire") {
215 return UserString("SAVE_EMPIRE_TITLE");
216 } else if( m_name == "turn") {
217 return UserString("SAVE_TURN_TITLE");
218 } else if( m_name == "time") {
219 return UserString("SAVE_TIME_TITLE");
220 } else if( m_name == "file") {
221 return UserString("SAVE_FILE_TITLE");
222 } else if( m_name == "galaxy_size") {
223 return UserString("SAVE_GALAXY_SIZE_TITLE");
224 } else if( m_name == "seed") {
225 return UserString("SAVE_SEED_TITLE");
226 } else if( m_name == "galaxy_age") {
227 return UserString("SAVE_GALAXY_AGE_TITLE");
228 } else if( m_name == "monster_freq") {
229 return UserString("SAVE_MONSTER_FREQ_TITLE");
230 } else if( m_name == "native_freq") {
231 return UserString("SAVE_NATIVE_FREQ_TITLE");
232 } else if( m_name == "planet_freq") {
233 return UserString("SAVE_PLANET_FREQ_TITLE");
234 } else if( m_name == "specials_freq") {
235 return UserString("SAVE_SPECIALS_FREQ_TITLE");
236 } else if( m_name == "starlane_freq") {
237 return UserString("SAVE_STARLANE_FREQ_TITLE");
238 } else if( m_name == "galaxy_shape") {
239 return UserString("SAVE_GALAXY_SHAPE_TITLE");
240 } else if( m_name == "ai_aggression") {
241 return UserString("SAVE_AI_AGGRESSION_TITLE");
242 } else if( m_name == "number_of_empires") {
243 return UserString("SAVE_NUMBER_EMPIRES_TITLE");
244 } else if( m_name == "number_of_humans" ){
245 return UserString("SAVE_NUMBER_HUMANS_TITLE");
246 } else {
247 ErrorLogger() << "SaveFileColumn::Title Error: no such preview field: " << m_name;
248 return "???";
249 }
250 }
251
ComputeFixedWidth(const std::string & title,const std::string & wide_as,GG::X max_width)252 static GG::X ComputeFixedWidth(const std::string& title, const std::string& wide_as, GG::X max_width) {
253 std::shared_ptr<GG::Font> font = ClientUI::GetFont();
254 // We need to maintain the fixed sizes since the base list box messes them
255 std::vector<GG::Font::LineData> lines;
256 GG::Flags<GG::TextFormat> fmt = GG::FORMAT_NONE;
257
258 //TODO cache this resulting extent
259 std::vector<std::shared_ptr<GG::Font::TextElement>> text_elements =
260 font->ExpensiveParseFromTextToTextElements(wide_as, fmt);
261 lines = font->DetermineLines(wide_as, fmt, max_width, text_elements);
262 GG::Pt extent1 = font->TextExtent(lines);
263
264 text_elements = font->ExpensiveParseFromTextToTextElements(title, fmt);
265 lines = font->DetermineLines(title, fmt, max_width, text_elements);
266 GG::Pt extent2 = font->TextExtent(lines);
267
268 return std::max(extent1.x, extent2.x) + GG::X(SAVE_FILE_CELL_MARGIN);
269 }
270
271 /// The identifier of what data to show. Must be valid.
272 std::string m_name;
273 /// If true, column width is fixed to be the width of m_wide_as under the current font.
274 /// If false, column stretches with factor m_stretch
275 bool m_fixed;
276 /// The wideset fixed width from wide_as or the title
277 GG::X m_fixed_width;
278 /// The string to be used in determining the width of the column
279 std::string m_wide_as;
280 /// The stretch of the column.
281 double m_stretch;
282 };
283
284 /** A Specialized row for the save dialog list box. */
285 class SaveFileRow: public GG::ListBox::Row {
286 public:
SaveFileRow()287 SaveFileRow() {}
288
SaveFileRow(const std::shared_ptr<std::vector<SaveFileColumn>> & columns,const std::string & filename)289 SaveFileRow(const std::shared_ptr<std::vector<SaveFileColumn>>& columns, const std::string& filename) :
290 m_filename(filename),
291 m_columns(columns),
292 m_initialized(false)
293 {
294 SetName("SaveFileRow for \""+filename+"\"");
295 SetChildClippingMode(ClipToClient);
296 RequirePreRender();
297 }
298
Init()299 virtual void Init()
300 { m_initialized = true;}
301
Filename() const302 const std::string& Filename() const
303 { return m_filename; }
304
PreRender()305 void PreRender() override {
306 if (!m_initialized)
307 Init();
308 GG::ListBox::Row::PreRender();
309 }
310
Render()311 void Render() override {
312 GG::FlatRectangle(ClientUpperLeft(),
313 ClientLowerRight() - GG::Pt(GG::X(SAVE_FILE_CELL_MARGIN), GG::Y0),
314 GG::CLR_ZERO, ClientUI::WndOuterBorderColor(), 1u);
315 }
316
317 /** Excludes border from the client area. */
ClientUpperLeft() const318 GG::Pt ClientUpperLeft() const override {
319 return UpperLeft() + GG::Pt(GG::X(SAVE_FILE_CELL_MARGIN),
320 GG::Y(SAVE_FILE_CELL_MARGIN));
321 }
322
323 /** Excludes border from the client area. */
ClientLowerRight() const324 GG::Pt ClientLowerRight() const override {
325 return LowerRight() - GG::Pt(GG::X(SAVE_FILE_CELL_MARGIN * 2),
326 GG::Y(SAVE_FILE_CELL_MARGIN));
327 }
328
329 /** Forces the columns to column widths not defined by ListBox. Needs to be called after any
330 interaction with the ListBox base class that sets the column widths back to those defined
331 by SetColWidths().*/
AdjustColumns()332 virtual void AdjustColumns() {
333 auto&& layout = GetLayout();
334 if (!layout)
335 return;
336 for (unsigned int i = 0; i < m_columns->size(); ++i) {
337 const SaveFileColumn& column = (*m_columns)[i];
338 layout->SetColumnStretch ( i, column.Stretch() );
339 layout->SetMinimumColumnWidth ( i, column.FixedWidth() ); // Considers header
340 }
341 }
342
343 protected:
344 std::string m_filename;
345 std::shared_ptr<std::vector<SaveFileColumn>> m_columns;
346 bool m_initialized;
347 };
348
349 class SaveFileHeaderRow: public SaveFileRow {
350 public:
SaveFileHeaderRow(const std::shared_ptr<std::vector<SaveFileColumn>> & columns)351 SaveFileHeaderRow(const std::shared_ptr<std::vector<SaveFileColumn>>& columns) :
352 SaveFileRow(columns, "")
353 {}
354
CompleteConstruction()355 void CompleteConstruction() override {
356 SaveFileRow::CompleteConstruction();
357
358 SetMargin(ROW_MARGIN);
359
360 for (const auto& column : *m_columns)
361 { push_back(SaveFileColumn::TitleForColumn(column)); }
362 AdjustColumns();
363 }
364
Render()365 void Render() override
366 {}
367 };
368
369 class SaveFileDirectoryRow: public SaveFileRow {
370 public:
SaveFileDirectoryRow(const std::shared_ptr<std::vector<SaveFileColumn>> & columns,const std::string & directory)371 SaveFileDirectoryRow(const std::shared_ptr<std::vector<SaveFileColumn>>& columns, const std::string& directory) :
372 SaveFileRow(columns, directory) {}
373
CompleteConstruction()374 void CompleteConstruction() override {
375 SaveFileRow::CompleteConstruction();
376 SetMargin(ROW_MARGIN);
377 }
378
Init()379 void Init() override {
380 SaveFileRow::Init();
381 for (unsigned int i = 0; i < m_columns->size(); ++i) {
382 if (i == 0) {
383 auto label = GG::Wnd::Create<CUILabel>(PATH_DELIM_BEGIN + m_filename + PATH_DELIM_END,
384 GG::FORMAT_NOWRAP | GG::FORMAT_LEFT);
385 label->Resize(GG::Pt(DirectoryNameSize(), ClientUI::GetFont()->Height()));
386 push_back(label);
387 } else {
388 // Dummy columns so that all rows have the same number of cols
389 auto label = GG::Wnd::Create<CUILabel>("", GG::FORMAT_NOWRAP);
390 label->Resize(GG::Pt(GG::X0, ClientUI::GetFont()->Height()));
391 push_back(label);
392 }
393 }
394
395 AdjustColumns();
396 GetLayout()->PreRender();
397 }
398
SortKey(std::size_t column) const399 SortKeyType SortKey(std::size_t column) const override
400 { return m_filename; }
401
DirectoryNameSize()402 GG::X DirectoryNameSize() {
403 auto&& layout = GetLayout();
404 if (!layout)
405 return ClientUI::GetFont()->SpaceWidth() * 10;
406
407 // Give the directory label at least all the room that the other columns demand anyway
408 GG::X sum(0);
409 for (const auto& column : *m_columns) {
410 sum += column.FixedWidth();
411 }
412 return sum;
413 }
414 };
415
416 class SaveFileFileRow: public SaveFileRow {
417 public:
418 /// Creates a row for the given savefile
SaveFileFileRow(const FullPreview & full,const std::shared_ptr<std::vector<SaveFileColumn>> & visible_columns,const std::shared_ptr<std::vector<SaveFileColumn>> & columns,int tooltip_delay)419 SaveFileFileRow(const FullPreview& full,
420 const std::shared_ptr<std::vector<SaveFileColumn>>& visible_columns,
421 const std::shared_ptr<std::vector<SaveFileColumn>>& columns,
422 int tooltip_delay) :
423 SaveFileRow(visible_columns, full.filename),
424 m_all_columns(columns),
425 m_full_preview(full)
426 {
427 SetBrowseModeTime(tooltip_delay);
428 }
429
CompleteConstruction()430 void CompleteConstruction() override {
431 SaveFileRow::CompleteConstruction();
432
433 SetMargin (ROW_MARGIN);
434 }
435
Init()436 void Init() override {
437 SaveFileRow::Init();
438 VarText browse_text(UserStringNop("SAVE_DIALOG_ROW_BROWSE_TEMPLATE"));
439
440 for (const auto& column : *m_columns)
441 push_back(SaveFileColumn::CellForColumn(column, m_full_preview, ClientWidth()));
442 for (const auto& column : *m_all_columns)
443 browse_text.AddVariable(column.Name(), ColumnInPreview(m_full_preview, column.Name(), false));
444
445 AdjustColumns();
446 SetBrowseText(browse_text.GetText());
447 GetLayout()->PreRender();
448 }
449
SortKey(std::size_t column) const450 SortKeyType SortKey(std::size_t column) const override
451 { return m_full_preview.preview.save_time; }
452
453 private:
454 /** All possible columns. */
455 std::shared_ptr<std::vector<SaveFileColumn>> m_all_columns;
456 const FullPreview m_full_preview;
457 };
458
459 class SaveFileListBox : public CUIListBox {
460 public:
SaveFileListBox()461 SaveFileListBox() :
462 CUIListBox ()
463 { }
464
Init()465 void Init() {
466 m_columns = SaveFileColumn::GetColumns(ClientWidth());
467 m_visible_columns = FilterColumns(m_columns);
468 ManuallyManageColProps();
469 NormalizeRowsOnInsert(false);
470 SetNumCols(m_visible_columns->size());
471
472 auto header_row = GG::Wnd::Create<SaveFileHeaderRow>(m_visible_columns);
473 SetColHeaders(header_row);
474 for (unsigned int i = 0; i < m_visible_columns->size(); ++i) {
475 const SaveFileColumn& column = (*m_visible_columns)[i];
476 SetColStretch(i, column.Stretch());
477 SetColWidth(i, column.FixedWidth());
478 }
479
480 SetSortCmp(&SaveFileListBox::DirectoryAwareCmp);
481 SetVScrollWheelIncrement(WHEEL_INCREMENT);
482 }
483
ResetColHeaders()484 void ResetColHeaders() {
485 RemoveColHeaders();
486 SetColHeaders(GG::Wnd::Create<SaveFileHeaderRow>(m_visible_columns));
487 }
488
ListRowSize() const489 GG::Pt ListRowSize() const
490 { return GG::Pt(Width() - RightMargin(), ListRowHeight()); }
491
ListRowHeight()492 static GG::Y ListRowHeight()
493 { return GG::Y(ClientUI::Pts() * 2); }
494
495 /// Loads preview data on all save files in a directory specidifed by path.
496 /// @param[in] path The path of the directory
497 /// @param[in] extension File name extension to filter by
LoadLocalSaveGamePreviews(const fs::path & path,const std::string & extension)498 void LoadLocalSaveGamePreviews(const fs::path& path, const std::string& extension) {
499 LoadDirectories(FindLocalRelativeDirs(path));
500
501 auto abs_path = path;
502 if (abs_path.is_relative())
503 abs_path = GetSaveDir() / path;
504
505 vector<FullPreview> previews;
506 ::LoadSaveGamePreviews(abs_path, extension, previews);
507 LoadSaveGamePreviews(previews);
508 }
509
510 /// Loads preview data on all save files in a directory specidifed by path.
511 /// @param [in] previews The preview data
LoadSaveGamePreviews(const std::vector<FullPreview> & previews)512 void LoadSaveGamePreviews(const std::vector<FullPreview>& previews) {
513 int tooltip_delay = GetOptionsDB().Get<int>("ui.dialog.save.tooltip.delay");
514
515 std::vector<std::shared_ptr<Row>> rows;
516 for (const FullPreview& preview : previews) {
517 rows.push_back(GG::Wnd::Create<SaveFileFileRow>(preview, m_visible_columns, m_columns, tooltip_delay));
518 }
519
520 // Insert rows enmasse to avoid per insertion vector sort costs.
521 Insert(rows);
522 }
523
524 /** Find local sub directories of \p path. */
FindLocalRelativeDirs(const fs::path & path)525 std::vector<std::string> FindLocalRelativeDirs(const fs::path& path) {
526 std::vector<std::string> dirs;
527 if (path.has_parent_path() && path.parent_path() != path)
528 dirs.push_back("..");
529
530 fs::directory_iterator end_it;
531 for (fs::directory_iterator it(path); it != end_it; ++it) {
532 if (fs::is_directory(it->path())) {
533 fs::path last_bit_of_path = it->path().filename();
534 std::string utf8_dir_name = PathToString(last_bit_of_path);
535 DebugLogger() << "SaveFileDialog::FindLocalRelativeDirs name: " << utf8_dir_name
536 << " valid UTF-8: " << IsValidUTF8(utf8_dir_name);
537 dirs.push_back(utf8_dir_name);
538 }
539 }
540
541 return dirs;
542 }
543
LoadDirectories(const std::vector<std::string> & dirs)544 void LoadDirectories(const std::vector<std::string>& dirs) {
545 std::vector<std::shared_ptr<Row>> rows;
546 for (const auto& dir : dirs)
547 rows.push_back(GG::Wnd::Create<SaveFileDirectoryRow>(m_visible_columns, dir));
548
549 // Insert rows enmasse to avoid per insertion vector sort costs.
550 Insert(rows);
551 }
552
HasFile(const std::string & filename)553 bool HasFile(const std::string& filename) {
554 for (auto& row : *this) {
555 SaveFileRow* srow = dynamic_cast<SaveFileRow*>(row.get());
556 if (srow && srow->Filename() == filename)
557 return true;
558 }
559 return false;
560 }
561
562 private:
563 std::shared_ptr<std::vector<SaveFileColumn>> m_columns;
564 std::shared_ptr<std::vector<SaveFileColumn>> m_visible_columns;
565
FilterColumns(const std::shared_ptr<std::vector<SaveFileColumn>> & all_cols)566 static std::shared_ptr<std::vector<SaveFileColumn>> FilterColumns(
567 const std::shared_ptr<std::vector<SaveFileColumn>>& all_cols)
568 {
569 std::vector<std::string> names = GetOptionsDB().Get<std::vector<std::string>>("ui.dialog.save.columns");
570 auto columns = std::make_shared<std::vector<SaveFileColumn>>();
571 for (const std::string& column_name : names) {
572 bool found_col = false;
573 for (const auto& column : *all_cols) {
574 if (column.Name() == column_name) {
575 columns->push_back(column);
576 found_col = true;
577 break;
578 }
579 }
580 if (!found_col)
581 ErrorLogger() << "SaveFileListBox::FilterColumns: Column not found: " << column_name;
582 }
583 DebugLogger() << "SaveFileDialog::FilterColumns: Visible columns: " << columns->size();
584 return columns;
585 }
586
587 /// We want the timestamps to be sorted in ascending order to get the latest save
588 /// first, but we want the directories to
589 /// a) always be first
590 /// b) be sorted alphabetically
591 /// This custom comparer achieves these goals.
DirectoryAwareCmp(const Row & row1,const Row & row2,int column_int)592 static bool DirectoryAwareCmp(const Row& row1, const Row& row2, int column_int) {
593 std::string key1(row1.SortKey(0));
594 std::string key2(row2.SortKey(0));
595
596 const bool row1_is_directory = dynamic_cast<const SaveFileDirectoryRow*>(&row1);
597 const bool row2_is_directory = dynamic_cast<const SaveFileDirectoryRow*>(&row2);
598 if (!row1_is_directory && !row2_is_directory) {
599 return key1.compare(key2) <= 0;
600 } else if ( row1_is_directory && row2_is_directory ) {
601 // Directories always return directory name as sort key
602 return key1.compare(key2) >= 0;
603 } else {
604 return ( !row1_is_directory && row2_is_directory );
605 }
606 }
607 };
608
SaveFileDialog(const Purpose purpose,const SaveType type)609 SaveFileDialog::SaveFileDialog(const Purpose purpose /* =Purpose::Load*/,
610 const SaveType type /*= SaveType::SinglePlayer*/) :
611 CUIWnd(UserString("GAME_MENU_SAVE_FILES"),
612 GG::INTERACTIVE | GG::DRAGABLE | GG::MODAL | GG::RESIZABLE,
613 SAVE_FILE_WND_NAME),
614 m_extension((type == SaveType::SinglePlayer) ? SP_SAVE_FILE_EXTENSION : MP_SAVE_FILE_EXTENSION),
615 m_load_only(purpose == Purpose::Load),
616 m_server_previews((type == SaveType::SinglePlayer) ? false : true)
617 {}
618
CompleteConstruction()619 void SaveFileDialog::CompleteConstruction() {
620 CUIWnd::CompleteConstruction();
621 Init();
622 }
623
Init()624 void SaveFileDialog::Init() {
625 ResetDefaultPosition();
626 SetMinSize(GG::Pt(2*SAVE_FILE_DIALOG_MIN_WIDTH, 2*SAVE_FILE_DIALOG_MIN_HEIGHT));
627
628 m_layout = GG::Wnd::Create<GG::Layout>(GG::X0, GG::Y0,
629 SAVE_FILE_DIALOG_WIDTH - LeftBorder() - RightBorder(),
630 SAVE_FILE_DIALOG_HEIGHT - TopBorder() - BottomBorder(), 4, 4);
631 m_layout->SetCellMargin(SAVE_FILE_CELL_MARGIN);
632 m_layout->SetBorderMargin(SAVE_FILE_CELL_MARGIN*2);
633
634 m_file_list = GG::Wnd::Create<SaveFileListBox>();
635 m_file_list->SetStyle(GG::LIST_SINGLESEL | GG::LIST_SORTDESCENDING);
636
637 m_confirm_btn = Wnd::Create<CUIButton>(UserString("OK"));
638 auto cancel_btn = Wnd::Create<CUIButton>(UserString("CANCEL"));
639
640 m_name_edit = GG::Wnd::Create<CUIEdit>("");
641 if (m_extension != MP_SAVE_FILE_EXTENSION && m_extension != SP_SAVE_FILE_EXTENSION) {
642 std::string savefile_ext = HumanClientApp::GetApp()->SinglePlayerGame() ? SP_SAVE_FILE_EXTENSION : MP_SAVE_FILE_EXTENSION;
643 DebugLogger() << "SaveFileDialog passed invalid extension " << m_extension << ", changing to " << savefile_ext;
644 m_extension = savefile_ext;
645 }
646
647 auto filename_label = GG::Wnd::Create<CUILabel>(UserString("SAVE_FILENAME"), GG::FORMAT_NOWRAP);
648 auto directory_label = GG::Wnd::Create<CUILabel>(UserString("SAVE_DIRECTORY"), GG::FORMAT_NOWRAP);
649
650 m_layout->Add(directory_label, 0, 0);
651
652 std::shared_ptr<GG::Font> font = ClientUI::GetFont();
653 if (!m_server_previews) {
654 m_current_dir_edit = GG::Wnd::Create<CUIEdit>(PathToString(GetSaveDir()));
655 m_layout->Add(m_current_dir_edit, 0, 1, 1, 3);
656
657 auto delete_btn = Wnd::Create<CUIButton>(UserString("DELETE"));
658 m_layout->Add(delete_btn, 2, 3);
659 delete_btn->LeftClickedSignal.connect(boost::bind(&SaveFileDialog::AskDelete, this));
660
661 m_layout->SetMinimumRowHeight(2, delete_btn->MinUsableSize().y + GG::Y(Value(SAVE_FILE_BUTTON_MARGIN)));
662 m_layout->SetMinimumColumnWidth(2, m_confirm_btn->MinUsableSize().x + 2*SAVE_FILE_BUTTON_MARGIN);
663 m_layout->SetMinimumColumnWidth(3, std::max( cancel_btn->MinUsableSize().x,
664 delete_btn->MinUsableSize().x) + SAVE_FILE_BUTTON_MARGIN);
665 } else {
666 m_current_dir_edit = GG::Wnd::Create<CUIEdit>(SERVER_LABEL + "/.");
667 m_layout->Add(m_current_dir_edit, 0, 1, 1, 3);
668 GG::Flags<GG::TextFormat> fmt = GG::FORMAT_NONE;
669 std::string server_label(SERVER_LABEL+SERVER_LABEL+SERVER_LABEL);
670 std::vector<std::shared_ptr<GG::Font::TextElement>> text_elements =
671 font->ExpensiveParseFromTextToTextElements(server_label, fmt);
672 std::vector<GG::Font::LineData> lines =
673 font->DetermineLines(server_label, fmt, ClientWidth(), text_elements);
674 GG::X drop_width = font->TextExtent(lines).x;
675 m_layout->SetMinimumColumnWidth(2, std::max(m_confirm_btn->MinUsableSize().x + 2*SAVE_FILE_BUTTON_MARGIN, drop_width/2));
676 m_layout->SetMinimumColumnWidth(3, std::max(cancel_btn->MinUsableSize().x + SAVE_FILE_BUTTON_MARGIN, drop_width / 2));
677 }
678
679 m_layout->Add(m_file_list, 1, 0, 1, 4);
680 m_layout->Add(filename_label, 2, 0);
681 m_layout->Add(m_name_edit, 3, 0, 1, 2);
682 m_layout->Add(m_confirm_btn, 3, 2);
683 m_layout->Add(cancel_btn, 3, 3);
684
685 m_layout->SetMinimumRowHeight(0, m_current_dir_edit->MinUsableSize().y);
686 m_layout->SetRowStretch (1, 1.0 );
687 GG::Flags<GG::TextFormat> fmt = GG::FORMAT_NONE;
688 std::string cancel_text(cancel_btn->Text());
689 std::vector<std::shared_ptr<GG::Font::TextElement>> text_elements =
690 font->ExpensiveParseFromTextToTextElements(cancel_text, fmt);
691 std::vector<GG::Font::LineData> lines = ClientUI::GetFont()->DetermineLines(
692 cancel_text, fmt, GG::X(1 << 15), text_elements);
693 GG::Pt extent = ClientUI::GetFont()->TextExtent(lines);
694 m_layout->SetMinimumRowHeight(3, extent.y);
695
696 std::string filename_label_text(filename_label->Text());
697 text_elements = font->ExpensiveParseFromTextToTextElements(filename_label_text, fmt);
698 lines = font->DetermineLines(filename_label_text, fmt, ClientWidth(), text_elements);
699 GG::Pt extent1 = font->TextExtent(lines);
700
701 std::string dir_label_text(directory_label->Text());
702 text_elements = font->ExpensiveParseFromTextToTextElements(dir_label_text, fmt);
703 lines = font->DetermineLines(dir_label_text, fmt, ClientWidth(), text_elements);
704 GG::Pt extent2 = font->TextExtent(lines);
705
706 m_layout->SetMinimumColumnWidth(0, std::max(extent1.x, extent2.x));
707 m_layout->SetColumnStretch(1, 1.0);
708
709 SetLayout(m_layout);
710
711 #if BOOST_VERSION >= 106000
712 using boost::placeholders::_1;
713 using boost::placeholders::_2;
714 using boost::placeholders::_3;
715 #endif
716
717 m_confirm_btn->LeftClickedSignal.connect(boost::bind(&SaveFileDialog::Confirm, this));
718 cancel_btn->LeftClickedSignal.connect(boost::bind(&SaveFileDialog::Cancel, this));
719 m_file_list->SelRowsChangedSignal.connect(boost::bind(&SaveFileDialog::SelectionChanged, this, _1));
720 m_file_list->DoubleClickedRowSignal.connect(boost::bind(&SaveFileDialog::DoubleClickRow, this, _1, _2, _3));
721 m_name_edit->EditedSignal.connect(boost::bind(&SaveFileDialog::FileNameEdited, this, _1));
722 m_current_dir_edit->EditedSignal.connect(boost::bind(&SaveFileDialog::DirectoryEdited, this, _1));
723
724 if (!m_load_only) {
725 m_name_edit->SetText(std::string("save-") + FilenameTimestamp() + m_extension);
726 m_name_edit->SelectAll();
727 }
728
729 if (m_server_previews) {
730 // Indicate to the user that they are browsing server saves
731 SetDirPath("./");
732 }
733
734 UpdatePreviewList();
735 SaveDefaultedOptions();
736 SaveOptions();
737 }
738
~SaveFileDialog()739 SaveFileDialog::~SaveFileDialog()
740 {}
741
CalculatePosition() const742 GG::Rect SaveFileDialog::CalculatePosition() const {
743 GG::Pt ul((GG::GUI::GetGUI()->AppWidth() - SAVE_FILE_DIALOG_WIDTH) / 2,
744 (GG::GUI::GetGUI()->AppHeight() - SAVE_FILE_DIALOG_HEIGHT) / 2);
745 GG::Pt wh(SAVE_FILE_DIALOG_WIDTH, SAVE_FILE_DIALOG_HEIGHT);
746 return GG::Rect(ul, ul + wh);
747 }
748
ModalInit()749 void SaveFileDialog::ModalInit() {
750 GG::Wnd::ModalInit();
751 GG::GUI::GetGUI()->SetFocusWnd(m_name_edit);
752 }
753
KeyPress(GG::Key key,std::uint32_t key_code_point,GG::Flags<GG::ModKey> mod_keys)754 void SaveFileDialog::KeyPress(GG::Key key, std::uint32_t key_code_point, GG::Flags<GG::ModKey> mod_keys ) {
755 // Return without filename
756 if (key == GG::GGK_ESCAPE) {
757 Cancel();
758 return;
759 }
760
761 // Update list on enter if directory changed by hand
762 if (key == GG::GGK_RETURN || key == GG::GGK_KP_ENTER) {
763 if (m_loaded_dir != GetDirPath()) {
764 UpdatePreviewList();
765 } else {
766 if (GG::GUI::GetGUI()->FocusWnd() == m_name_edit) {
767 Confirm();
768 }
769 }
770
771 } else if (key == GG::GGK_DELETE) { // Delete would be better, but gets eaten by someone
772 // Ask to delete selection on Delete, if valid and not editing text
773 if (CheckChoiceValidity() &&
774 GG::GUI::GetGUI()->FocusWnd() != m_name_edit &&
775 GG::GUI::GetGUI()->FocusWnd() != m_current_dir_edit)
776 {
777 AskDelete();
778 }
779
780 } else {
781 // The keypress may have changed our choice
782 CheckChoiceValidity();
783 }
784 }
785
Confirm()786 void SaveFileDialog::Confirm() {
787 DebugLogger() << "SaveFileDialog::Confirm: Confirming";
788
789 if (!CheckChoiceValidity()) {
790 WarnLogger() << "SaveFileDialog::Confirm: Invalid choice. abort.";
791 return;
792 }
793
794 /// Check if we chose a directory
795 std::string choice = m_name_edit->Text();
796 if (choice.empty()) {
797 WarnLogger() << "SaveFileDialog::Confirm: Returning no file.";
798 CloseClicked();
799 return;
800 }
801
802 fs::path choice_path = FilenameToPath(choice);
803 DebugLogger() << "choice: " << choice << " valid utf-8: " << IsValidUTF8(choice);
804
805 fs::path current_dir = FilenameToPath(GetDirPath());
806 DebugLogger() << "current dir PathString: " << PathToString(current_dir) << " valid utf-8: " << IsValidUTF8(PathToString(current_dir));
807
808 fs::path chosen_full_path = current_dir / choice_path;
809 DebugLogger() << "chosen_full_path PathString: " << PathToString(chosen_full_path) << " valid utf-8: " << IsValidUTF8(PathToString(chosen_full_path));
810 DebugLogger() << "chosen_full_path is directory? : " << fs::is_directory(chosen_full_path);
811
812 if (fs::is_directory(chosen_full_path)) {
813 DebugLogger() << "SaveFileDialog::Confirm: " << PathToString(chosen_full_path) << " is a directory. Listing content.";
814 UpdateDirectory(PathToString(chosen_full_path));
815 return;
816 }
817
818 if (m_server_previews && choice_path.generic_string().find(SERVER_LABEL) == 0) {
819 UpdateDirectory(choice);
820 return;
821 }
822
823 if (!m_load_only) {
824 // append appropriate extension if invalid
825 std::string chosen_ext = fs::path(chosen_full_path).extension().string();
826 if (chosen_ext != m_extension) {
827 choice += m_extension;
828 chosen_full_path += m_extension;
829 m_name_edit->SetText(m_name_edit->Text() + m_extension);
830 }
831 DebugLogger() << "SaveFileDialog::Confirm: File " << PathToString(chosen_full_path) << " chosen.";
832 // If not loading and file exists(and is regular file), ask to confirm override
833 if (fs::is_regular_file(chosen_full_path)) {
834 std::string question = str((FlexibleFormat(UserString("SAVE_REALLY_OVERRIDE")) % choice));
835 if (!Prompt(question))
836 return;
837 } else if (fs::exists(chosen_full_path)) {
838 ErrorLogger() << "SaveFileDialog::Confirm: Invalid status for file: " << Result();
839 return;
840 }
841 }
842
843 CloseClicked();
844 }
845
AskDelete()846 void SaveFileDialog::AskDelete() {
847 if (m_server_previews)
848 return;
849
850 fs::path chosen(Result());
851 if (fs::exists (chosen) && fs::is_regular_file (chosen)) {
852 std::string filename = m_name_edit->Text();
853
854 boost::format templ(UserString("SAVE_REALLY_DELETE"));
855
856 std::string question = str(templ % filename);
857 if (Prompt (question)) {
858 fs::remove(chosen);
859 // Move selection to next if any or previous, if any
860 auto it = m_file_list->Selections().begin();
861 if (it != m_file_list->Selections().end()) {
862 auto row_it = *it;
863 auto next_it(row_it);
864 ++next_it;
865 if (next_it != m_file_list->end()) {
866 m_file_list->SelectRow(next_it, true);
867 } else if (row_it != m_file_list->begin()) {
868 auto prev_it(row_it);
869 --prev_it;
870 m_file_list->SelectRow(next_it, true);
871 }
872 m_file_list->Erase(row_it);
873 }
874 }
875 }
876 }
877
DoubleClickRow(GG::ListBox::iterator row,const GG::Pt & pt,const GG::Flags<GG::ModKey> & modkeys)878 void SaveFileDialog::DoubleClickRow(GG::ListBox::iterator row, const GG::Pt& pt, const GG::Flags<GG::ModKey>& modkeys) {
879 m_file_list->SelectRow(row);
880 Confirm();
881 }
882
Cancel()883 void SaveFileDialog::Cancel() {
884 DebugLogger() << "SaveFileDialog::Cancel: Dialog Canceled";
885 m_name_edit->SetText("");
886 CloseClicked();
887 }
888
SelectionChanged(const GG::ListBox::SelectionSet & selections)889 void SaveFileDialog::SelectionChanged(const GG::ListBox::SelectionSet& selections) {
890 if ( selections.size() == 1 ) {
891 auto& row = **selections.begin();
892 SaveFileRow* save_row = boost::polymorphic_downcast<SaveFileRow*> (row.get());
893 m_name_edit -> SetText ( save_row->Filename() );
894 } else {
895 DebugLogger() << "SaveFileDialog::SelectionChanged: Unexpected selection size: " << selections.size();
896 }
897 CheckChoiceValidity();
898 }
899
UpdateDirectory(const std::string & newdir)900 void SaveFileDialog::UpdateDirectory(const std::string& newdir) {
901 SetDirPath(newdir);
902 UpdatePreviewList();
903 }
904
UpdatePreviewList()905 void SaveFileDialog::UpdatePreviewList() {
906 // If no browsing, no reloading
907 if (!m_server_previews) {
908 SetPreviewList(FilenameToPath(GetDirPath()));
909 } else {
910 HumanClientApp::GetApp()->RequestSavePreviews(GetDirPath());
911 }
912 }
913
SetPreviewList(const fs::path & path)914 void SaveFileDialog::SetPreviewList(const fs::path& path) {
915 auto setup_func = [this, &path]() { m_file_list->LoadLocalSaveGamePreviews(path, m_extension); };
916
917 CheckChoiceValidity();
918 SetPreviewListCore(setup_func);
919 }
920
SetPreviewList(const PreviewInformation & preview_info)921 void SaveFileDialog::SetPreviewList(const PreviewInformation& preview_info) {
922 auto setup_func = [this, &preview_info]() {
923
924 // Prefix directories with the server label;
925 std::vector<std::string> prefixed_subdirs;
926 for (const std::string& subdir : preview_info.subdirectories) {
927 if (subdir.find("/") == 0 || subdir.find("\\") == 0) {
928 prefixed_subdirs.push_back(SERVER_LABEL + subdir);
929 } else if(subdir.find("./") == 0) {
930 prefixed_subdirs.push_back(SERVER_LABEL + subdir.substr(1));
931 } else {
932 prefixed_subdirs.push_back(SERVER_LABEL + "/" + subdir);
933 }
934 }
935
936 m_file_list->LoadDirectories(prefixed_subdirs);
937 m_file_list->LoadSaveGamePreviews(preview_info.previews);
938 SetDirPath(preview_info.folder);
939 };
940
941 SetPreviewListCore(setup_func);
942 }
943
SetPreviewListCore(const std::function<void ()> & setup_preview_info)944 void SaveFileDialog::SetPreviewListCore(const std::function<void ()>& setup_preview_info) {
945 m_file_list->Clear();
946 m_file_list->Init();
947
948 setup_preview_info();
949
950 // HACK: Sometimes the first row is not drawn without this
951 m_file_list->BringRowIntoView(m_file_list->begin());
952
953 // Remember which directory we are showing
954 //m_loaded_dir = GetDirPath();
955
956 CheckChoiceValidity();
957 }
958
CheckChoiceValidity()959 bool SaveFileDialog::CheckChoiceValidity() {
960 // Check folder validity
961 if (!m_server_previews) {
962 fs::path dir(FilenameToPath(GetDirPath()));
963 if (fs::exists(dir) && fs::is_directory(dir)) {
964 m_current_dir_edit->SetColor(ClientUI::TextColor());
965 } else {
966 m_current_dir_edit->SetColor(GG::CLR_RED);
967 }
968 }
969
970 // Check file name validity
971 if (m_load_only) {
972 if (!m_file_list->HasFile(m_name_edit->Text())) {
973 m_confirm_btn->Disable();
974 return false;
975 } else {
976 m_confirm_btn->Disable(false);
977 return true;
978 }
979 }
980
981 return true;
982 }
983
FileNameEdited(const std::string & filename)984 void SaveFileDialog::FileNameEdited(const std::string& filename)
985 { CheckChoiceValidity(); }
986
DirectoryEdited(const string & filename)987 void SaveFileDialog::DirectoryEdited(const string& filename)
988 { CheckChoiceValidity(); }
989
GetDirPath() const990 std::string SaveFileDialog::GetDirPath() const {
991 const std::string& path_edit_text = m_current_dir_edit->Text();
992
993 //DebugLogger() << "SaveFileDialog::GetDirPath text: " << path_edit_text << " valid UTF-8: " << utf8::is_valid(path_edit_text.begin(), path_edit_text.end());
994 if (!m_server_previews)
995 return path_edit_text;
996
997 std::string dir = path_edit_text;
998
999 // We want to indicate at all times that the saves are on the server.
1000 if (dir.find(SERVER_LABEL) != 0) {
1001 if (dir.find("/") != 0)
1002 dir = "/" + dir;
1003
1004 dir = SERVER_LABEL + dir;
1005 }
1006
1007 // We should now be sure that the path is SERVER/whatever
1008 if (dir.length() < SERVER_LABEL.size()) {
1009 ErrorLogger() << "SaveFileDialog::GetDirPath: Error decorating directory for server: not long enough";
1010 return ".";
1011 }
1012
1013 auto retval = "." + dir.substr(SERVER_LABEL.size());
1014 DebugLogger() << "SaveFileDialog::GetDirPath retval: " << retval << " valid UTF-8: " << utf8::is_valid(retval.begin(), retval.end());
1015 return retval;
1016 }
1017
SetDirPath(const string & dir_path)1018 void SaveFileDialog::SetDirPath(const string& dir_path) {
1019 std::string dirname = dir_path;
1020 if (m_server_previews) {
1021 // Indicate that the path is on the server
1022 if (dir_path.find("./") == 0) {
1023 dirname = SERVER_LABEL + dirname.substr(1);
1024 } else if (dir_path.find(SERVER_LABEL) == 0) {
1025 // Already has label. No need to change
1026 } else {
1027 dirname = SERVER_LABEL + "/" + dirname;
1028 }
1029 } else {
1030 // Normalize path
1031 fs::path path(dirname);
1032 if (fs::is_directory(path)) {
1033 path = fs::canonical(path);
1034 dirname = PathToString(path);
1035 }
1036 }
1037 m_current_dir_edit->SetText(dirname);
1038 }
1039
Result() const1040 std::string SaveFileDialog::Result() const {
1041 std::string choice = m_name_edit->Text();
1042 if (choice.empty())
1043 return "";
1044
1045 fs::path choice_path = FilenameToPath(choice);
1046 fs::path current_dir = FilenameToPath(GetDirPath());
1047 fs::path chosen_full_path = current_dir / choice_path;
1048
1049 return PathToString(chosen_full_path);
1050 }
1051