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