1 /*****************************************************************************
2  * Copyright (c) 2014-2020 OpenRCT2 developers
3  *
4  * For a complete list of all authors, please refer to contributors.md
5  * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
6  *
7  * OpenRCT2 is licensed under the GNU General Public License version 3.
8  *****************************************************************************/
9 
10 #include <algorithm>
11 #include <ctime>
12 #include <iterator>
13 #include <memory>
14 #include <openrct2-ui/interface/Widget.h>
15 #include <openrct2-ui/windows/Window.h>
16 #include <openrct2/Context.h>
17 #include <openrct2/Editor.h>
18 #include <openrct2/FileClassifier.h>
19 #include <openrct2/Game.h>
20 #include <openrct2/GameState.h>
21 #include <openrct2/config/Config.h>
22 #include <openrct2/core/FileScanner.h>
23 #include <openrct2/core/Guard.hpp>
24 #include <openrct2/core/Path.hpp>
25 #include <openrct2/core/String.hpp>
26 #include <openrct2/localisation/Localisation.h>
27 #include <openrct2/platform/Platform2.h>
28 #include <openrct2/platform/platform.h>
29 #include <openrct2/rct2/T6Exporter.h>
30 #include <openrct2/ride/TrackDesign.h>
31 #include <openrct2/scenario/Scenario.h>
32 #include <openrct2/title/TitleScreen.h>
33 #include <openrct2/ui/UiContext.h>
34 #include <openrct2/util/Util.h>
35 #include <openrct2/windows/Intent.h>
36 #include <openrct2/world/Park.h>
37 #include <string>
38 #include <vector>
39 
40 #pragma region Widgets
41 
42 static constexpr const rct_string_id WINDOW_TITLE = STR_NONE;
43 static constexpr const int32_t WW = 350;
44 static constexpr const int32_t WH = 400;
45 
46 // clang-format off
47 enum
48 {
49     WIDX_BACKGROUND,
50     WIDX_TITLE,
51     WIDX_CLOSE,
52     WIDX_RESIZE,
53     WIDX_DEFAULT,
54     WIDX_UP,
55     WIDX_NEW_FOLDER,
56     WIDX_NEW_FILE,
57     WIDX_SORT_NAME,
58     WIDX_SORT_DATE,
59     WIDX_SCROLL,
60     WIDX_BROWSE,
61 };
62 
63 // 0x9DE48C
64 static rct_widget window_loadsave_widgets[] =
65 {
66     WINDOW_SHIM(WINDOW_TITLE, WW, WH),
67     MakeWidget({               0,  WH - 1}, { WW,   1}, WindowWidgetType::Resize,       WindowColour::Secondary                                                             ), // tab content panel
68     MakeWidget({               4,      36}, { 84,  14}, WindowWidgetType::Button,       WindowColour::Primary  , STR_LOADSAVE_DEFAULT,              STR_LOADSAVE_DEFAULT_TIP), // Go to default directory
69     MakeWidget({              88,      36}, { 84,  14}, WindowWidgetType::Button,       WindowColour::Primary  , STR_FILEBROWSER_ACTION_UP                                  ), // Up
70     MakeWidget({             172,      36}, { 87,  14}, WindowWidgetType::Button,       WindowColour::Primary  , STR_FILEBROWSER_ACTION_NEW_FOLDER                          ), // New
71     MakeWidget({             259,      36}, { 87,  14}, WindowWidgetType::Button,       WindowColour::Primary  , STR_FILEBROWSER_ACTION_NEW_FILE                            ), // New
72     MakeWidget({               4,      55}, {170,  14}, WindowWidgetType::TableHeader, WindowColour::Primary                                                               ), // Name
73     MakeWidget({(WW - 5) / 2 + 1,      55}, {170,  14}, WindowWidgetType::TableHeader, WindowColour::Primary                                                               ), // Date
74     MakeWidget({               4,      68}, {342, 303}, WindowWidgetType::Scroll,       WindowColour::Primary  , SCROLL_VERTICAL                                            ), // File list
75     MakeWidget({               4, WH - 24}, {197,  19}, WindowWidgetType::Button,       WindowColour::Primary  , STR_FILEBROWSER_USE_SYSTEM_WINDOW                          ), // Use native browser
76     WIDGETS_END,
77 };
78 
79 #pragma endregion
80 
81 #pragma region Events
82 
83 static void window_loadsave_close(rct_window *w);
84 static void window_loadsave_mouseup(rct_window *w, rct_widgetindex widgetIndex);
85 static void window_loadsave_resize(rct_window *w);
86 static void window_loadsave_scrollgetsize(rct_window *w, int32_t scrollIndex, int32_t *width, int32_t *height);
87 static void window_loadsave_scrollmousedown(rct_window *w, int32_t scrollIndex, const ScreenCoordsXY& screenCoords);
88 static void window_loadsave_scrollmouseover(rct_window *w, int32_t scrollIndex, const ScreenCoordsXY& screenCoords);
89 static void window_loadsave_textinput(rct_window *w, rct_widgetindex widgetIndex, char *text);
90 static void window_loadsave_compute_max_date_width();
91 static void window_loadsave_invalidate(rct_window *w);
92 static void window_loadsave_paint(rct_window *w, rct_drawpixelinfo *dpi);
93 static void window_loadsave_scrollpaint(rct_window *w, rct_drawpixelinfo *dpi, int32_t scrollIndex);
94 
95 static rct_window_event_list window_loadsave_events([](auto& events)
__anon53cf0eb60202(auto& events) 96 {
97     events.close = &window_loadsave_close;
98     events.mouse_up = &window_loadsave_mouseup;
99     events.resize = &window_loadsave_resize;
100     events.get_scroll_size = &window_loadsave_scrollgetsize;
101     events.scroll_mousedown = &window_loadsave_scrollmousedown;
102     events.scroll_mouseover = &window_loadsave_scrollmouseover;
103     events.text_input = &window_loadsave_textinput;
104     events.invalidate = &window_loadsave_invalidate;
105     events.paint = &window_loadsave_paint;
106     events.scroll_paint = &window_loadsave_scrollpaint;
107 });
108 // clang-format on
109 
110 #pragma endregion
111 
112 enum
113 {
114     TYPE_DIRECTORY,
115     TYPE_FILE,
116 };
117 
118 struct LoadSaveListItem
119 {
120     std::string name{};
121     std::string path{};
122     time_t date_modified{ 0 };
123     std::string date_formatted{};
124     std::string time_formatted{};
125     uint8_t type{ 0 };
126     bool loaded{ false };
127 };
128 
129 static std::function<void(int32_t result, std::string_view)> _loadSaveCallback;
130 static TrackDesign* _trackDesign;
131 
132 static std::vector<LoadSaveListItem> _listItems;
133 static char _directory[MAX_PATH];
134 static char _shortenedDirectory[MAX_PATH];
135 static char _parentDirectory[MAX_PATH];
136 static char _extension[256];
137 static std::string _defaultPath;
138 static int32_t _type;
139 
140 static int32_t maxDateWidth = 0;
141 static int32_t maxTimeWidth = 0;
142 
143 static void window_loadsave_populate_list(rct_window* w, int32_t includeNewItem, const char* directory, const char* extension);
144 static void window_loadsave_select(rct_window* w, const char* path);
145 static void window_loadsave_sort_list();
146 
147 static rct_window* window_overwrite_prompt_open(const char* name, const char* path);
148 
getLastDirectoryByType(int32_t type)149 static utf8* getLastDirectoryByType(int32_t type)
150 {
151     switch (type & 0x0E)
152     {
153         case LOADSAVETYPE_GAME:
154             return gConfigGeneral.last_save_game_directory;
155 
156         case LOADSAVETYPE_LANDSCAPE:
157             return gConfigGeneral.last_save_landscape_directory;
158 
159         case LOADSAVETYPE_SCENARIO:
160             return gConfigGeneral.last_save_scenario_directory;
161 
162         case LOADSAVETYPE_TRACK:
163             return gConfigGeneral.last_save_track_directory;
164 
165         default:
166             return nullptr;
167     }
168 }
169 
getInitialDirectoryByType(const int32_t type,char * path,size_t pathSize)170 static void getInitialDirectoryByType(const int32_t type, char* path, size_t pathSize)
171 {
172     const char* subdir = nullptr;
173     switch (type & 0x0E)
174     {
175         case LOADSAVETYPE_GAME:
176             subdir = "save";
177             break;
178 
179         case LOADSAVETYPE_LANDSCAPE:
180             subdir = "landscape";
181             break;
182 
183         case LOADSAVETYPE_SCENARIO:
184             subdir = "scenario";
185             break;
186 
187         case LOADSAVETYPE_TRACK:
188             subdir = "track";
189             break;
190 
191         case LOADSAVETYPE_HEIGHTMAP:
192             subdir = "heightmap";
193             break;
194     }
195 
196     platform_get_user_directory(path, subdir, pathSize);
197 }
198 
getFilterPatternByType(const int32_t type,const bool isSave)199 static const char* getFilterPatternByType(const int32_t type, const bool isSave)
200 {
201     switch (type & 0x0E)
202     {
203         case LOADSAVETYPE_GAME:
204             return isSave ? "*.sv6" : "*.sv6;*.sc6;*.sc4;*.sv4;*.sv7;*.sea;";
205 
206         case LOADSAVETYPE_LANDSCAPE:
207             return isSave ? "*.sc6" : "*.sc6;*.sv6;*.sc4;*.sv4;*.sv7;*.sea;";
208 
209         case LOADSAVETYPE_SCENARIO:
210             return "*.sc6";
211 
212         case LOADSAVETYPE_TRACK:
213             return isSave ? "*.td6" : "*.td6;*.td4";
214 
215         case LOADSAVETYPE_HEIGHTMAP:
216             return "*.bmp;*.png";
217 
218         default:
219             openrct2_assert(true, "Unsupported load/save directory type.");
220     }
221 
222     return "";
223 }
224 
window_loadsave_get_dir(const int32_t type,char * path,size_t pathSize)225 static int32_t window_loadsave_get_dir(const int32_t type, char* path, size_t pathSize)
226 {
227     const char* last_save = getLastDirectoryByType(type);
228     if (last_save != nullptr && platform_directory_exists(last_save))
229         safe_strcpy(path, last_save, pathSize);
230     else
231         getInitialDirectoryByType(type, path, pathSize);
232     return 1;
233 }
234 
235 static bool browse(bool isSave, char* path, size_t pathSize);
236 
window_loadsave_open(int32_t type,std::string_view defaultPath,std::function<void (int32_t result,std::string_view)> callback,TrackDesign * trackDesign)237 rct_window* window_loadsave_open(
238     int32_t type, std::string_view defaultPath, std::function<void(int32_t result, std::string_view)> callback,
239     TrackDesign* trackDesign)
240 {
241     _loadSaveCallback = callback;
242     _trackDesign = trackDesign;
243     _type = type;
244     _defaultPath = defaultPath;
245 
246     bool isSave = (type & 0x01) == LOADSAVETYPE_SAVE;
247     char path[MAX_PATH];
248     bool success = window_loadsave_get_dir(type, path, sizeof(path));
249     if (!success)
250         return nullptr;
251 
252     // Bypass the lot?
253     auto hasFilePicker = OpenRCT2::GetContext()->GetUiContext()->HasFilePicker();
254     if (gConfigGeneral.use_native_browse_dialog && hasFilePicker)
255     {
256         if (browse(isSave, path, sizeof(path)))
257         {
258             window_loadsave_select(nullptr, path);
259         }
260         return nullptr;
261     }
262 
263     rct_window* w = window_bring_to_front_by_class(WC_LOADSAVE);
264     if (w == nullptr)
265     {
266         w = WindowCreateCentred(WW, WH, &window_loadsave_events, WC_LOADSAVE, WF_STICK_TO_FRONT | WF_RESIZABLE);
267         w->widgets = window_loadsave_widgets;
268         w->enabled_widgets = (1ULL << WIDX_CLOSE) | (1ULL << WIDX_UP) | (1ULL << WIDX_NEW_FOLDER) | (1ULL << WIDX_NEW_FILE)
269             | (1ULL << WIDX_SORT_NAME) | (1ULL << WIDX_SORT_DATE) | (1ULL << WIDX_BROWSE) | (1ULL << WIDX_DEFAULT);
270 
271         w->min_width = WW;
272         w->min_height = WH / 2;
273         w->max_width = WW * 2;
274         w->max_height = WH * 2;
275 
276         if (!hasFilePicker)
277         {
278             w->enabled_widgets &= ~(1ULL << WIDX_BROWSE);
279             w->disabled_widgets |= (1ULL << WIDX_BROWSE);
280             window_loadsave_widgets[WIDX_BROWSE].type = WindowWidgetType::Empty;
281         }
282     }
283 
284     const char* pattern = getFilterPatternByType(type, isSave);
285     window_loadsave_populate_list(w, isSave, path, pattern);
286     w->no_list_items = static_cast<uint16_t>(_listItems.size());
287     w->selected_list_item = -1;
288 
289     switch (type & 0x0E)
290     {
291         case LOADSAVETYPE_GAME:
292             w->widgets[WIDX_TITLE].text = isSave ? STR_FILE_DIALOG_TITLE_SAVE_GAME : STR_FILE_DIALOG_TITLE_LOAD_GAME;
293             break;
294 
295         case LOADSAVETYPE_LANDSCAPE:
296             w->widgets[WIDX_TITLE].text = isSave ? STR_FILE_DIALOG_TITLE_SAVE_LANDSCAPE : STR_FILE_DIALOG_TITLE_LOAD_LANDSCAPE;
297             break;
298 
299         case LOADSAVETYPE_SCENARIO:
300             w->widgets[WIDX_TITLE].text = STR_FILE_DIALOG_TITLE_SAVE_SCENARIO;
301             break;
302 
303         case LOADSAVETYPE_TRACK:
304             w->widgets[WIDX_TITLE].text = isSave ? STR_FILE_DIALOG_TITLE_SAVE_TRACK
305                                                  : STR_FILE_DIALOG_TITLE_INSTALL_NEW_TRACK_DESIGN;
306             break;
307 
308         case LOADSAVETYPE_HEIGHTMAP:
309             openrct2_assert(!isSave, "Cannot save images through loadsave window");
310             w->widgets[WIDX_TITLE].text = STR_FILE_DIALOG_TITLE_LOAD_HEIGHTMAP;
311             break;
312 
313         default:
314             openrct2_assert(true, "Unsupported load/save type: %d", type & 0x0F);
315     }
316 
317     WindowInitScrollWidgets(w);
318     window_loadsave_compute_max_date_width();
319 
320     return w;
321 }
322 
window_loadsave_close(rct_window * w)323 static void window_loadsave_close(rct_window* w)
324 {
325     _listItems.clear();
326     window_close_by_class(WC_LOADSAVE_OVERWRITE_PROMPT);
327 }
328 
window_loadsave_resize(rct_window * w)329 static void window_loadsave_resize(rct_window* w)
330 {
331     if (w->width < w->min_width)
332     {
333         w->Invalidate();
334         w->width = w->min_width;
335     }
336     if (w->height < w->min_height)
337     {
338         w->Invalidate();
339         w->height = w->min_height;
340     }
341 }
342 
browse(bool isSave,char * path,size_t pathSize)343 static bool browse(bool isSave, char* path, size_t pathSize)
344 {
345     file_dialog_desc desc = {};
346     const utf8* extension = "";
347     uint32_t fileType = FILE_EXTENSION_UNKNOWN;
348     rct_string_id title = STR_NONE;
349     switch (_type & 0x0E)
350     {
351         case LOADSAVETYPE_GAME:
352             extension = ".sv6";
353             fileType = FILE_EXTENSION_SV6;
354             title = isSave ? STR_FILE_DIALOG_TITLE_SAVE_GAME : STR_FILE_DIALOG_TITLE_LOAD_GAME;
355             desc.filters[0].name = language_get_string(STR_OPENRCT2_SAVED_GAME);
356             desc.filters[0].pattern = getFilterPatternByType(_type, isSave);
357             break;
358 
359         case LOADSAVETYPE_LANDSCAPE:
360             extension = ".sc6";
361             fileType = FILE_EXTENSION_SC6;
362             title = isSave ? STR_FILE_DIALOG_TITLE_SAVE_LANDSCAPE : STR_FILE_DIALOG_TITLE_LOAD_LANDSCAPE;
363             desc.filters[0].name = language_get_string(STR_OPENRCT2_LANDSCAPE_FILE);
364             desc.filters[0].pattern = getFilterPatternByType(_type, isSave);
365             break;
366 
367         case LOADSAVETYPE_SCENARIO:
368             extension = ".sc6";
369             fileType = FILE_EXTENSION_SC6;
370             title = STR_FILE_DIALOG_TITLE_SAVE_SCENARIO;
371             desc.filters[0].name = language_get_string(STR_OPENRCT2_SCENARIO_FILE);
372             desc.filters[0].pattern = getFilterPatternByType(_type, isSave);
373             break;
374 
375         case LOADSAVETYPE_TRACK:
376             extension = ".td6";
377             fileType = FILE_EXTENSION_TD6;
378             title = isSave ? STR_FILE_DIALOG_TITLE_SAVE_TRACK : STR_FILE_DIALOG_TITLE_INSTALL_NEW_TRACK_DESIGN;
379             desc.filters[0].name = language_get_string(STR_OPENRCT2_TRACK_DESIGN_FILE);
380             desc.filters[0].pattern = getFilterPatternByType(_type, isSave);
381             break;
382 
383         case LOADSAVETYPE_HEIGHTMAP:
384             title = STR_FILE_DIALOG_TITLE_LOAD_HEIGHTMAP;
385             desc.filters[0].name = language_get_string(STR_OPENRCT2_HEIGHTMAP_FILE);
386             desc.filters[0].pattern = getFilterPatternByType(_type, isSave);
387             break;
388     }
389 
390     safe_strcpy(path, _directory, pathSize);
391     if (isSave)
392     {
393         // The file browser requires a file path instead of just a directory
394         if (!_defaultPath.empty())
395         {
396             safe_strcat_path(path, _defaultPath.c_str(), pathSize);
397         }
398         else
399         {
400             auto& park = OpenRCT2::GetContext()->GetGameState()->GetPark();
401             auto buffer = park.Name;
402             if (buffer.empty())
403             {
404                 // Use localised "Unnamed Park" if park name was empty.
405                 buffer = format_string(STR_UNNAMED_PARK, nullptr);
406             }
407             safe_strcat_path(path, buffer.c_str(), pathSize);
408         }
409     }
410 
411     desc.initial_directory = _directory;
412     desc.type = isSave ? FileDialogType::Save : FileDialogType::Open;
413     desc.default_filename = isSave ? path : nullptr;
414 
415     // Add 'all files' filter. If the number of filters is increased, this code will need to be adjusted.
416     desc.filters[1].name = language_get_string(STR_ALL_FILES);
417     desc.filters[1].pattern = "*";
418 
419     desc.title = language_get_string(title);
420     if (platform_open_common_file_dialog(path, &desc, pathSize))
421     {
422         // When the given save type was given, Windows still interprets a filename with a dot in its name as a custom extension,
423         // meaning files like "My Coaster v1.2" will not get the .td6 extension by default.
424         if (isSave && get_file_extension_type(path) != fileType)
425             path_append_extension(path, extension, pathSize);
426 
427         return true;
428     }
429 
430     return false;
431 }
432 
window_loadsave_mouseup(rct_window * w,rct_widgetindex widgetIndex)433 static void window_loadsave_mouseup(rct_window* w, rct_widgetindex widgetIndex)
434 {
435     char path[MAX_PATH];
436 
437     bool isSave = (_type & 0x01) == LOADSAVETYPE_SAVE;
438     switch (widgetIndex)
439     {
440         case WIDX_CLOSE:
441             window_close(w);
442             break;
443 
444         case WIDX_UP:
445             safe_strcpy(path, _parentDirectory, sizeof(path));
446             window_loadsave_populate_list(w, isSave, path, _extension);
447             WindowInitScrollWidgets(w);
448             w->no_list_items = static_cast<uint16_t>(_listItems.size());
449             break;
450 
451         case WIDX_NEW_FILE:
452             window_text_input_open(
453                 w, WIDX_NEW_FILE, STR_NONE, STR_FILEBROWSER_FILE_NAME_PROMPT, {}, STR_STRING,
454                 reinterpret_cast<uintptr_t>(_defaultPath.c_str()), 64);
455             break;
456 
457         case WIDX_NEW_FOLDER:
458             window_text_input_raw_open(w, WIDX_NEW_FOLDER, STR_NONE, STR_FILEBROWSER_FOLDER_NAME_PROMPT, {}, "", 64);
459             break;
460 
461         case WIDX_BROWSE:
462             if (browse(isSave, path, sizeof(path)))
463             {
464                 window_loadsave_select(w, path);
465             }
466             else
467             {
468                 // If user cancels file dialog, refresh list
469                 safe_strcpy(path, _directory, sizeof(path));
470                 window_loadsave_populate_list(w, isSave, path, _extension);
471                 WindowInitScrollWidgets(w);
472                 w->no_list_items = static_cast<uint16_t>(_listItems.size());
473             }
474             break;
475 
476         case WIDX_SORT_NAME:
477             if (gConfigGeneral.load_save_sort == Sort::NameAscending)
478             {
479                 gConfigGeneral.load_save_sort = Sort::NameDescending;
480             }
481             else
482             {
483                 gConfigGeneral.load_save_sort = Sort::NameAscending;
484             }
485             config_save_default();
486             window_loadsave_sort_list();
487             w->Invalidate();
488             break;
489 
490         case WIDX_SORT_DATE:
491             if (gConfigGeneral.load_save_sort == Sort::DateDescending)
492             {
493                 gConfigGeneral.load_save_sort = Sort::DateAscending;
494             }
495             else
496             {
497                 gConfigGeneral.load_save_sort = Sort::DateDescending;
498             }
499             config_save_default();
500             window_loadsave_sort_list();
501             w->Invalidate();
502             break;
503 
504         case WIDX_DEFAULT:
505             getInitialDirectoryByType(_type, path, sizeof(path));
506             window_loadsave_populate_list(w, isSave, path, _extension);
507             WindowInitScrollWidgets(w);
508             w->no_list_items = static_cast<uint16_t>(_listItems.size());
509             break;
510     }
511 }
512 
window_loadsave_scrollgetsize(rct_window * w,int32_t scrollIndex,int32_t * width,int32_t * height)513 static void window_loadsave_scrollgetsize(rct_window* w, int32_t scrollIndex, int32_t* width, int32_t* height)
514 {
515     *height = w->no_list_items * SCROLLABLE_ROW_HEIGHT;
516 }
517 
window_loadsave_scrollmousedown(rct_window * w,int32_t scrollIndex,const ScreenCoordsXY & screenCoords)518 static void window_loadsave_scrollmousedown(rct_window* w, int32_t scrollIndex, const ScreenCoordsXY& screenCoords)
519 {
520     int32_t selectedItem;
521 
522     selectedItem = screenCoords.y / SCROLLABLE_ROW_HEIGHT;
523     if (selectedItem >= w->no_list_items)
524         return;
525 
526     if (_listItems[selectedItem].type == TYPE_DIRECTORY)
527     {
528         // The selected item is a folder
529         int32_t includeNewItem;
530 
531         w->no_list_items = 0;
532         w->selected_list_item = -1;
533         includeNewItem = (_type & 1) == LOADSAVETYPE_SAVE;
534 
535         char directory[MAX_PATH];
536         safe_strcpy(directory, _listItems[selectedItem].path.c_str(), sizeof(directory));
537 
538         window_loadsave_populate_list(w, includeNewItem, directory, _extension);
539         WindowInitScrollWidgets(w);
540 
541         w->no_list_items = static_cast<uint16_t>(_listItems.size());
542     }
543     else
544     {
545         // TYPE_FILE
546         // Load or overwrite
547         if ((_type & 0x01) == LOADSAVETYPE_SAVE)
548             window_overwrite_prompt_open(_listItems[selectedItem].name.c_str(), _listItems[selectedItem].path.c_str());
549         else
550             window_loadsave_select(w, _listItems[selectedItem].path.c_str());
551     }
552 }
553 
window_loadsave_scrollmouseover(rct_window * w,int32_t scrollIndex,const ScreenCoordsXY & screenCoords)554 static void window_loadsave_scrollmouseover(rct_window* w, int32_t scrollIndex, const ScreenCoordsXY& screenCoords)
555 {
556     int32_t selectedItem;
557 
558     selectedItem = screenCoords.y / SCROLLABLE_ROW_HEIGHT;
559     if (selectedItem >= w->no_list_items)
560         return;
561 
562     w->selected_list_item = selectedItem;
563 
564     w->Invalidate();
565 }
566 
window_loadsave_textinput(rct_window * w,rct_widgetindex widgetIndex,char * text)567 static void window_loadsave_textinput(rct_window* w, rct_widgetindex widgetIndex, char* text)
568 {
569     char path[MAX_PATH];
570     bool overwrite;
571 
572     if (text == nullptr || text[0] == 0)
573         return;
574 
575     switch (widgetIndex)
576     {
577         case WIDX_NEW_FOLDER:
578             if (!filename_valid_characters(text))
579             {
580                 context_show_error(STR_ERROR_INVALID_CHARACTERS, STR_NONE, {});
581                 return;
582             }
583 
584             safe_strcpy(path, _directory, sizeof(path));
585             safe_strcat_path(path, text, sizeof(path));
586 
587             if (!platform_ensure_directory_exists(path))
588             {
589                 context_show_error(STR_UNABLE_TO_CREATE_FOLDER, STR_NONE, {});
590                 return;
591             }
592 
593             w->no_list_items = 0;
594             w->selected_list_item = -1;
595 
596             window_loadsave_populate_list(w, (_type & 1) == LOADSAVETYPE_SAVE, path, _extension);
597             WindowInitScrollWidgets(w);
598 
599             w->no_list_items = static_cast<uint16_t>(_listItems.size());
600             w->Invalidate();
601             break;
602 
603         case WIDX_NEW_FILE:
604             safe_strcpy(path, _directory, sizeof(path));
605             safe_strcat_path(path, text, sizeof(path));
606             path_append_extension(path, _extension, sizeof(path));
607 
608             overwrite = false;
609             for (auto& item : _listItems)
610             {
611                 if (_stricmp(item.path.c_str(), path) == 0)
612                 {
613                     overwrite = true;
614                     break;
615                 }
616             }
617 
618             if (overwrite)
619                 window_overwrite_prompt_open(text, path);
620             else
621                 window_loadsave_select(w, path);
622             break;
623     }
624 }
625 
626 constexpr uint16_t DATE_TIME_GAP = 2;
627 
window_loadsave_compute_max_date_width()628 static void window_loadsave_compute_max_date_width()
629 {
630     // Generate a time object for a relatively wide time: 2000-02-20 00:00:00
631     std::tm tm;
632     tm.tm_sec = 0;
633     tm.tm_min = 0;
634     tm.tm_hour = 0;
635     tm.tm_mday = 20;
636     tm.tm_mon = 2;
637     tm.tm_year = 100;
638     tm.tm_wday = 5;
639     tm.tm_yday = 51;
640     tm.tm_isdst = -1;
641 
642     std::time_t long_time = mktime(&tm);
643 
644     // Check how this date is represented (e.g. 2000-02-20, or 00/02/20)
645     std::string date = Platform::FormatShortDate(long_time);
646     maxDateWidth = gfx_get_string_width(date.c_str(), FontSpriteBase::MEDIUM) + DATE_TIME_GAP;
647 
648     // Some locales do not use leading zeros for months and days, so let's try October, too.
649     tm.tm_mon = 10;
650     tm.tm_yday = 294;
651     long_time = mktime(&tm);
652 
653     // Again, check how this date is represented (e.g. 2000-10-20, or 00/10/20)
654     date = Platform::FormatShortDate(long_time);
655     maxDateWidth = std::max(maxDateWidth, gfx_get_string_width(date.c_str(), FontSpriteBase::MEDIUM) + DATE_TIME_GAP);
656 
657     // Time appears to be universally represented with two digits for minutes, so 12:00 or 00:00 should be representable.
658     std::string time = Platform::FormatTime(long_time);
659     maxTimeWidth = gfx_get_string_width(time.c_str(), FontSpriteBase::MEDIUM) + DATE_TIME_GAP;
660 }
661 
window_loadsave_invalidate(rct_window * w)662 static void window_loadsave_invalidate(rct_window* w)
663 {
664     window_loadsave_widgets[WIDX_TITLE].right = w->width - 2;
665     // close button has to move if it's on the right side
666     window_loadsave_widgets[WIDX_CLOSE].left = w->width - 13;
667     window_loadsave_widgets[WIDX_CLOSE].right = w->width - 3;
668 
669     window_loadsave_widgets[WIDX_BACKGROUND].right = w->width - 1;
670     window_loadsave_widgets[WIDX_BACKGROUND].bottom = w->height - 1;
671     window_loadsave_widgets[WIDX_RESIZE].top = w->height - 1;
672     window_loadsave_widgets[WIDX_RESIZE].right = w->width - 1;
673     window_loadsave_widgets[WIDX_RESIZE].bottom = w->height - 1;
674 
675     rct_widget* date_widget = &window_loadsave_widgets[WIDX_SORT_DATE];
676     date_widget->right = w->width - 5;
677     date_widget->left = date_widget->right - (maxDateWidth + maxTimeWidth + (4 * DATE_TIME_GAP) + (SCROLLBAR_WIDTH + 1));
678 
679     window_loadsave_widgets[WIDX_SORT_NAME].left = 4;
680     window_loadsave_widgets[WIDX_SORT_NAME].right = window_loadsave_widgets[WIDX_SORT_DATE].left - 1;
681 
682     window_loadsave_widgets[WIDX_SCROLL].right = w->width - 4;
683     window_loadsave_widgets[WIDX_SCROLL].bottom = w->height - 30;
684 
685     window_loadsave_widgets[WIDX_BROWSE].top = w->height - 24;
686     window_loadsave_widgets[WIDX_BROWSE].bottom = w->height - 6;
687 }
688 
window_loadsave_paint(rct_window * w,rct_drawpixelinfo * dpi)689 static void window_loadsave_paint(rct_window* w, rct_drawpixelinfo* dpi)
690 {
691     WindowDrawWidgets(w, dpi);
692 
693     if (_shortenedDirectory[0] == '\0')
694     {
695         shorten_path(_shortenedDirectory, sizeof(_shortenedDirectory), _directory, w->width - 8, FontSpriteBase::MEDIUM);
696     }
697 
698     // Format text
699     thread_local std::string buffer;
700     buffer.assign("{BLACK}");
701     buffer += _shortenedDirectory;
702 
703     // Draw path text
704     auto ft = Formatter();
705     ft.Add<const char*>(Platform::StrDecompToPrecomp(buffer.data()));
706     DrawTextEllipsised(dpi, { w->windowPos.x + 4, w->windowPos.y + 20 }, w->width - 8, STR_STRING, ft);
707 
708     // Name button text
709     rct_string_id id = STR_NONE;
710     if (gConfigGeneral.load_save_sort == Sort::NameAscending)
711         id = STR_UP;
712     else if (gConfigGeneral.load_save_sort == Sort::NameDescending)
713         id = STR_DOWN;
714 
715     // Draw name button indicator.
716     rct_widget sort_name_widget = window_loadsave_widgets[WIDX_SORT_NAME];
717     ft = Formatter();
718     ft.Add<rct_string_id>(id);
719     DrawTextBasic(
720         dpi, w->windowPos + ScreenCoordsXY{ sort_name_widget.left + 11, sort_name_widget.top + 1 }, STR_NAME, ft,
721         { COLOUR_GREY });
722 
723     // Date button text
724     if (gConfigGeneral.load_save_sort == Sort::DateAscending)
725         id = STR_UP;
726     else if (gConfigGeneral.load_save_sort == Sort::DateDescending)
727         id = STR_DOWN;
728     else
729         id = STR_NONE;
730 
731     rct_widget sort_date_widget = window_loadsave_widgets[WIDX_SORT_DATE];
732     ft = Formatter();
733     ft.Add<rct_string_id>(id);
734     DrawTextBasic(
735         dpi, w->windowPos + ScreenCoordsXY{ sort_date_widget.left + 5, sort_date_widget.top + 1 }, STR_DATE, ft,
736         { COLOUR_GREY });
737 }
738 
window_loadsave_scrollpaint(rct_window * w,rct_drawpixelinfo * dpi,int32_t scrollIndex)739 static void window_loadsave_scrollpaint(rct_window* w, rct_drawpixelinfo* dpi, int32_t scrollIndex)
740 {
741     gfx_fill_rect(
742         dpi, { { dpi->x, dpi->y }, { dpi->x + dpi->width - 1, dpi->y + dpi->height - 1 } },
743         ColourMapA[w->colours[1]].mid_light);
744     const int32_t listWidth = w->widgets[WIDX_SCROLL].width();
745     const int32_t dateAnchor = w->widgets[WIDX_SORT_DATE].left + maxDateWidth + DATE_TIME_GAP;
746 
747     for (int32_t i = 0; i < w->no_list_items; i++)
748     {
749         int32_t y = i * SCROLLABLE_ROW_HEIGHT;
750         if (y > dpi->y + dpi->height)
751             break;
752 
753         if (y + SCROLLABLE_ROW_HEIGHT < dpi->y)
754             continue;
755 
756         rct_string_id stringId = STR_BLACK_STRING;
757 
758         // If hovering over item, change the color and fill the backdrop.
759         if (i == w->selected_list_item)
760         {
761             stringId = STR_WINDOW_COLOUR_2_STRINGID;
762             gfx_filter_rect(dpi, { 0, y, listWidth, y + SCROLLABLE_ROW_HEIGHT }, FilterPaletteID::PaletteDarken1);
763         }
764         // display a marker next to the currently loaded game file
765         if (_listItems[i].loaded)
766         {
767             auto ft = Formatter();
768             ft.Add<rct_string_id>(STR_RIGHTGUILLEMET);
769             DrawTextBasic(dpi, { 0, y }, stringId, ft);
770         }
771 
772         // Print filename
773         auto ft = Formatter();
774         ft.Add<rct_string_id>(STR_STRING);
775         ft.Add<char*>(_listItems[i].name.c_str());
776         int32_t max_file_width = w->widgets[WIDX_SORT_NAME].width() - 10;
777         DrawTextEllipsised(dpi, { 10, y }, max_file_width, stringId, ft);
778 
779         // Print formatted modified date, if this is a file
780         if (_listItems[i].type == TYPE_FILE)
781         {
782             ft = Formatter();
783             ft.Add<rct_string_id>(STR_STRING);
784             ft.Add<char*>(_listItems[i].date_formatted.c_str());
785             DrawTextEllipsised(dpi, { dateAnchor - DATE_TIME_GAP, y }, maxDateWidth, stringId, ft, { TextAlignment::RIGHT });
786 
787             ft = Formatter();
788             ft.Add<rct_string_id>(STR_STRING);
789             ft.Add<char*>(_listItems[i].time_formatted.c_str());
790             DrawTextEllipsised(dpi, { dateAnchor + DATE_TIME_GAP, y }, maxTimeWidth, stringId, ft);
791         }
792     }
793 }
794 
list_item_sort(LoadSaveListItem & a,LoadSaveListItem & b)795 static bool list_item_sort(LoadSaveListItem& a, LoadSaveListItem& b)
796 {
797     if (a.type != b.type)
798         return a.type - b.type < 0;
799 
800     switch (gConfigGeneral.load_save_sort)
801     {
802         case Sort::NameAscending:
803             return strlogicalcmp(a.name.c_str(), b.name.c_str()) < 0;
804         case Sort::NameDescending:
805             return -strlogicalcmp(a.name.c_str(), b.name.c_str()) < 0;
806         case Sort::DateDescending:
807             return -difftime(a.date_modified, b.date_modified) < 0;
808         case Sort::DateAscending:
809             return difftime(a.date_modified, b.date_modified) < 0;
810     }
811     return strlogicalcmp(a.name.c_str(), b.name.c_str()) < 0;
812 }
813 
window_loadsave_sort_list()814 static void window_loadsave_sort_list()
815 {
816     std::sort(_listItems.begin(), _listItems.end(), list_item_sort);
817 }
818 
window_loadsave_populate_list(rct_window * w,int32_t includeNewItem,const char * directory,const char * extension)819 static void window_loadsave_populate_list(rct_window* w, int32_t includeNewItem, const char* directory, const char* extension)
820 {
821     utf8 absoluteDirectory[MAX_PATH];
822     Path::GetAbsolute(absoluteDirectory, std::size(absoluteDirectory), directory);
823     safe_strcpy(_directory, absoluteDirectory, std::size(_directory));
824     // Note: This compares the pointers, not values
825     if (_extension != extension)
826     {
827         safe_strcpy(_extension, extension, std::size(_extension));
828     }
829     _shortenedDirectory[0] = '\0';
830 
831     _listItems.clear();
832 
833     // Show "new" buttons when saving
834     window_loadsave_widgets[WIDX_NEW_FILE].type = includeNewItem ? WindowWidgetType::Button : WindowWidgetType::Empty;
835     window_loadsave_widgets[WIDX_NEW_FOLDER].type = includeNewItem ? WindowWidgetType::Button : WindowWidgetType::Empty;
836 
837     int32_t drives = platform_get_drives();
838     if (str_is_null_or_empty(directory) && drives)
839     {
840         // List Windows drives
841         w->disabled_widgets |= (1ULL << WIDX_NEW_FILE) | (1ULL << WIDX_NEW_FOLDER) | (1ULL << WIDX_UP);
842         for (int32_t x = 0; x < 26; x++)
843         {
844             if (drives & (1 << x))
845             {
846                 // If the drive exists, list it
847                 LoadSaveListItem newListItem;
848                 newListItem.path = std::string(1, 'A' + x) + ":" PATH_SEPARATOR;
849                 newListItem.name = newListItem.path;
850                 newListItem.type = TYPE_DIRECTORY;
851 
852                 _listItems.push_back(std::move(newListItem));
853             }
854         }
855     }
856     else
857     {
858         // Remove the separator at the end of the path, if present
859         safe_strcpy(_parentDirectory, absoluteDirectory, std::size(_parentDirectory));
860         if (_parentDirectory[strlen(_parentDirectory) - 1] == *PATH_SEPARATOR
861             || _parentDirectory[strlen(_parentDirectory) - 1] == '/')
862             _parentDirectory[strlen(_parentDirectory) - 1] = '\0';
863 
864         // Remove everything past the now last separator
865         char* ch = strrchr(_parentDirectory, *PATH_SEPARATOR);
866         char* posix_ch = strrchr(_parentDirectory, '/');
867         ch = ch < posix_ch ? posix_ch : ch;
868         if (ch != nullptr)
869         {
870             *(ch + 1) = '\0';
871         }
872         else if (drives)
873         {
874             // If on Windows, clear the entire path to show the drives
875             _parentDirectory[0] = '\0';
876         }
877         else
878         {
879             // Else, go to the root directory
880             snprintf(_parentDirectory, MAX_PATH, "%c", *PATH_SEPARATOR);
881         }
882 
883         // Disable the Up button if the current directory is the root directory
884         if (str_is_null_or_empty(_parentDirectory) && !drives)
885             w->disabled_widgets |= (1ULL << WIDX_UP);
886         else
887             w->disabled_widgets &= ~(1ULL << WIDX_UP);
888 
889         // Re-enable the "new" buttons if these were disabled
890         w->disabled_widgets &= ~(1ULL << WIDX_NEW_FILE);
891         w->disabled_widgets &= ~(1ULL << WIDX_NEW_FOLDER);
892 
893         // List all directories
894         auto subDirectories = Path::GetDirectories(absoluteDirectory);
895         for (const auto& sdName : subDirectories)
896         {
897             auto subDir = sdName + PATH_SEPARATOR;
898 
899             LoadSaveListItem newListItem;
900             newListItem.path = Path::Combine(absoluteDirectory, subDir);
901             newListItem.name = subDir;
902             newListItem.type = TYPE_DIRECTORY;
903             newListItem.loaded = false;
904 
905             _listItems.push_back(std::move(newListItem));
906         }
907 
908         // List all files with the wanted extensions
909         char filter[MAX_PATH];
910         char extCopy[64];
911         safe_strcpy(extCopy, extension, std::size(extCopy));
912         bool showExtension = false;
913         char* extToken = strtok(extCopy, ";");
914         while (extToken != nullptr)
915         {
916             safe_strcpy(filter, directory, std::size(filter));
917             safe_strcat_path(filter, "*", std::size(filter));
918             path_append_extension(filter, extToken, std::size(filter));
919 
920             auto scanner = Path::ScanDirectory(filter, false);
921             while (scanner->Next())
922             {
923                 LoadSaveListItem newListItem;
924                 newListItem.path = scanner->GetPath();
925                 newListItem.type = TYPE_FILE;
926                 newListItem.date_modified = platform_file_get_modified_time(newListItem.path.c_str());
927 
928                 // Cache a human-readable version of the modified date.
929                 newListItem.date_formatted = Platform::FormatShortDate(newListItem.date_modified);
930                 newListItem.time_formatted = Platform::FormatTime(newListItem.date_modified);
931 
932                 // Mark if file is the currently loaded game
933                 newListItem.loaded = newListItem.path.compare(gCurrentLoadedPath.c_str()) == 0;
934 
935                 // Remove the extension (but only the first extension token)
936                 if (!showExtension)
937                 {
938                     newListItem.name = Path::GetFileNameWithoutExtension(newListItem.path);
939                 }
940                 else
941                 {
942                     newListItem.name = Path::GetFileName(newListItem.path);
943                 }
944 
945                 _listItems.push_back(std::move(newListItem));
946             }
947 
948             extToken = strtok(nullptr, ";");
949             showExtension = true; // Show any extension after the first iteration
950         }
951 
952         window_loadsave_sort_list();
953     }
954 
955     w->Invalidate();
956 }
957 
window_loadsave_invoke_callback(int32_t result,const utf8 * path)958 static void window_loadsave_invoke_callback(int32_t result, const utf8* path)
959 {
960     if (_loadSaveCallback != nullptr)
961     {
962         _loadSaveCallback(result, path);
963     }
964 }
965 
save_path(utf8 ** config_str,const char * path)966 static void save_path(utf8** config_str, const char* path)
967 {
968     free(*config_str);
969     *config_str = path_get_directory(path);
970     config_save_default();
971 }
972 
is_valid_path(const char * path)973 static bool is_valid_path(const char* path)
974 {
975     char filename[MAX_PATH];
976     safe_strcpy(filename, path_get_filename(path), sizeof(filename));
977 
978     // HACK This is needed because tracks get passed through with td?
979     //      I am sure this will change eventually to use the new FileScanner
980     //      which handles multiple patterns
981     path_remove_extension(filename);
982 
983     return filename_valid_characters(filename);
984 }
985 
window_loadsave_select(rct_window * w,const char * path)986 static void window_loadsave_select(rct_window* w, const char* path)
987 {
988     if (!is_valid_path(path))
989     {
990         context_show_error(STR_ERROR_INVALID_CHARACTERS, STR_NONE, {});
991         return;
992     }
993 
994     char pathBuffer[MAX_PATH];
995     safe_strcpy(pathBuffer, path, sizeof(pathBuffer));
996 
997     switch (_type & 0x0F)
998     {
999         case (LOADSAVETYPE_LOAD | LOADSAVETYPE_GAME):
1000             save_path(&gConfigGeneral.last_save_game_directory, pathBuffer);
1001             window_loadsave_invoke_callback(MODAL_RESULT_OK, pathBuffer);
1002             window_close_by_class(WC_LOADSAVE);
1003             gfx_invalidate_screen();
1004             break;
1005 
1006         case (LOADSAVETYPE_SAVE | LOADSAVETYPE_GAME):
1007             save_path(&gConfigGeneral.last_save_game_directory, pathBuffer);
1008             if (scenario_save(pathBuffer, gConfigGeneral.save_plugin_data ? 1 : 0))
1009             {
1010                 gScenarioSavePath = pathBuffer;
1011                 gCurrentLoadedPath = pathBuffer;
1012                 gFirstTimeSaving = false;
1013 
1014                 window_close_by_class(WC_LOADSAVE);
1015                 gfx_invalidate_screen();
1016 
1017                 window_loadsave_invoke_callback(MODAL_RESULT_OK, pathBuffer);
1018             }
1019             else
1020             {
1021                 context_show_error(STR_SAVE_GAME, STR_GAME_SAVE_FAILED, {});
1022                 window_loadsave_invoke_callback(MODAL_RESULT_FAIL, pathBuffer);
1023             }
1024             break;
1025 
1026         case (LOADSAVETYPE_LOAD | LOADSAVETYPE_LANDSCAPE):
1027             save_path(&gConfigGeneral.last_save_landscape_directory, pathBuffer);
1028             if (Editor::LoadLandscape(pathBuffer))
1029             {
1030                 gCurrentLoadedPath = pathBuffer;
1031                 gfx_invalidate_screen();
1032                 window_loadsave_invoke_callback(MODAL_RESULT_OK, pathBuffer);
1033             }
1034             else
1035             {
1036                 // Not the best message...
1037                 context_show_error(STR_LOAD_LANDSCAPE, STR_FAILED_TO_LOAD_FILE_CONTAINS_INVALID_DATA, {});
1038                 window_loadsave_invoke_callback(MODAL_RESULT_FAIL, pathBuffer);
1039             }
1040             break;
1041 
1042         case (LOADSAVETYPE_SAVE | LOADSAVETYPE_LANDSCAPE):
1043             save_path(&gConfigGeneral.last_save_landscape_directory, pathBuffer);
1044             safe_strcpy(gScenarioFileName, pathBuffer, sizeof(gScenarioFileName));
1045             if (scenario_save(pathBuffer, gConfigGeneral.save_plugin_data ? 3 : 2))
1046             {
1047                 gCurrentLoadedPath = pathBuffer;
1048                 window_close_by_class(WC_LOADSAVE);
1049                 gfx_invalidate_screen();
1050                 window_loadsave_invoke_callback(MODAL_RESULT_OK, pathBuffer);
1051             }
1052             else
1053             {
1054                 context_show_error(STR_SAVE_LANDSCAPE, STR_LANDSCAPE_SAVE_FAILED, {});
1055                 window_loadsave_invoke_callback(MODAL_RESULT_FAIL, pathBuffer);
1056             }
1057             break;
1058 
1059         case (LOADSAVETYPE_SAVE | LOADSAVETYPE_SCENARIO):
1060         {
1061             save_path(&gConfigGeneral.last_save_scenario_directory, pathBuffer);
1062             int32_t parkFlagsBackup = gParkFlags;
1063             gParkFlags &= ~PARK_FLAGS_SPRITES_INITIALISED;
1064             gEditorStep = EditorStep::Invalid;
1065             safe_strcpy(gScenarioFileName, pathBuffer, sizeof(gScenarioFileName));
1066             int32_t success = scenario_save(pathBuffer, gConfigGeneral.save_plugin_data ? 3 : 2);
1067             gParkFlags = parkFlagsBackup;
1068 
1069             if (success)
1070             {
1071                 window_close_by_class(WC_LOADSAVE);
1072                 window_loadsave_invoke_callback(MODAL_RESULT_OK, pathBuffer);
1073                 title_load();
1074             }
1075             else
1076             {
1077                 context_show_error(STR_FILE_DIALOG_TITLE_SAVE_SCENARIO, STR_SCENARIO_SAVE_FAILED, {});
1078                 gEditorStep = EditorStep::ObjectiveSelection;
1079                 window_loadsave_invoke_callback(MODAL_RESULT_FAIL, pathBuffer);
1080             }
1081             break;
1082         }
1083 
1084         case (LOADSAVETYPE_LOAD | LOADSAVETYPE_TRACK):
1085         {
1086             save_path(&gConfigGeneral.last_save_track_directory, pathBuffer);
1087             auto intent = Intent(WC_INSTALL_TRACK);
1088             intent.putExtra(INTENT_EXTRA_PATH, std::string{ pathBuffer });
1089             context_open_intent(&intent);
1090             window_close_by_class(WC_LOADSAVE);
1091             window_loadsave_invoke_callback(MODAL_RESULT_OK, pathBuffer);
1092             break;
1093         }
1094 
1095         case (LOADSAVETYPE_SAVE | LOADSAVETYPE_TRACK):
1096         {
1097             save_path(&gConfigGeneral.last_save_track_directory, pathBuffer);
1098 
1099             path_set_extension(pathBuffer, "td6", sizeof(pathBuffer));
1100 
1101             T6Exporter t6Export{ _trackDesign };
1102 
1103             auto success = t6Export.SaveTrack(pathBuffer);
1104 
1105             if (success)
1106             {
1107                 window_close_by_class(WC_LOADSAVE);
1108                 window_ride_measurements_design_cancel();
1109                 window_loadsave_invoke_callback(MODAL_RESULT_OK, path);
1110             }
1111             else
1112             {
1113                 context_show_error(STR_FILE_DIALOG_TITLE_SAVE_TRACK, STR_TRACK_SAVE_FAILED, {});
1114                 window_loadsave_invoke_callback(MODAL_RESULT_FAIL, path);
1115             }
1116             break;
1117         }
1118 
1119         case (LOADSAVETYPE_LOAD | LOADSAVETYPE_HEIGHTMAP):
1120             window_close_by_class(WC_LOADSAVE);
1121             window_loadsave_invoke_callback(MODAL_RESULT_OK, pathBuffer);
1122             break;
1123     }
1124 }
1125 
1126 #pragma region Overwrite prompt
1127 
1128 constexpr int32_t OVERWRITE_WW = 200;
1129 constexpr int32_t OVERWRITE_WH = 100;
1130 
1131 enum
1132 {
1133     WIDX_OVERWRITE_BACKGROUND,
1134     WIDX_OVERWRITE_TITLE,
1135     WIDX_OVERWRITE_CLOSE,
1136     WIDX_OVERWRITE_OVERWRITE,
1137     WIDX_OVERWRITE_CANCEL
1138 };
1139 
1140 static rct_widget window_overwrite_prompt_widgets[] = {
1141     WINDOW_SHIM_WHITE(STR_FILEBROWSER_OVERWRITE_TITLE, OVERWRITE_WW, OVERWRITE_WH),
1142     { WindowWidgetType::Button, 0, 10, 94, OVERWRITE_WH - 20, OVERWRITE_WH - 9, STR_FILEBROWSER_OVERWRITE_TITLE, STR_NONE },
1143     { WindowWidgetType::Button, 0, OVERWRITE_WW - 95, OVERWRITE_WW - 11, OVERWRITE_WH - 20, OVERWRITE_WH - 9,
1144       STR_SAVE_PROMPT_CANCEL, STR_NONE },
1145     WIDGETS_END,
1146 };
1147 
1148 static void window_overwrite_prompt_mouseup(rct_window* w, rct_widgetindex widgetIndex);
1149 static void window_overwrite_prompt_paint(rct_window* w, rct_drawpixelinfo* dpi);
1150 
__anon53cf0eb60502(auto& events) 1151 static rct_window_event_list window_overwrite_prompt_events([](auto& events) {
1152     events.mouse_up = &window_overwrite_prompt_mouseup;
1153     events.paint = &window_overwrite_prompt_paint;
1154 });
1155 
1156 static char _window_overwrite_prompt_name[256];
1157 static char _window_overwrite_prompt_path[MAX_PATH];
1158 
window_overwrite_prompt_open(const char * name,const char * path)1159 static rct_window* window_overwrite_prompt_open(const char* name, const char* path)
1160 {
1161     rct_window* w;
1162 
1163     window_close_by_class(WC_LOADSAVE_OVERWRITE_PROMPT);
1164 
1165     w = WindowCreateCentred(
1166         OVERWRITE_WW, OVERWRITE_WH, &window_overwrite_prompt_events, WC_LOADSAVE_OVERWRITE_PROMPT, WF_STICK_TO_FRONT);
1167     w->widgets = window_overwrite_prompt_widgets;
1168     w->enabled_widgets = (1ULL << WIDX_CLOSE) | (1ULL << WIDX_OVERWRITE_CANCEL) | (1ULL << WIDX_OVERWRITE_OVERWRITE);
1169 
1170     WindowInitScrollWidgets(w);
1171 
1172     w->flags |= WF_TRANSPARENT;
1173     w->colours[0] = TRANSLUCENT(COLOUR_BORDEAUX_RED);
1174 
1175     safe_strcpy(_window_overwrite_prompt_name, name, sizeof(_window_overwrite_prompt_name));
1176     safe_strcpy(_window_overwrite_prompt_path, path, sizeof(_window_overwrite_prompt_path));
1177 
1178     return w;
1179 }
1180 
window_overwrite_prompt_mouseup(rct_window * w,rct_widgetindex widgetIndex)1181 static void window_overwrite_prompt_mouseup(rct_window* w, rct_widgetindex widgetIndex)
1182 {
1183     rct_window* loadsaveWindow;
1184 
1185     switch (widgetIndex)
1186     {
1187         case WIDX_OVERWRITE_OVERWRITE:
1188             loadsaveWindow = window_find_by_class(WC_LOADSAVE);
1189             if (loadsaveWindow != nullptr)
1190                 window_loadsave_select(loadsaveWindow, _window_overwrite_prompt_path);
1191             // As the window_loadsave_select function can change the order of the
1192             // windows we can't use window_close(w).
1193             window_close_by_class(WC_LOADSAVE_OVERWRITE_PROMPT);
1194             break;
1195 
1196         case WIDX_OVERWRITE_CANCEL:
1197         case WIDX_OVERWRITE_CLOSE:
1198             window_close(w);
1199             break;
1200     }
1201 }
1202 
window_overwrite_prompt_paint(rct_window * w,rct_drawpixelinfo * dpi)1203 static void window_overwrite_prompt_paint(rct_window* w, rct_drawpixelinfo* dpi)
1204 {
1205     WindowDrawWidgets(w, dpi);
1206 
1207     auto ft = Formatter();
1208     ft.Add<rct_string_id>(STR_STRING);
1209     ft.Add<char*>(_window_overwrite_prompt_name);
1210 
1211     ScreenCoordsXY stringCoords(w->windowPos.x + w->width / 2, w->windowPos.y + (w->height / 2) - 3);
1212     DrawTextWrapped(dpi, stringCoords, w->width - 4, STR_FILEBROWSER_OVERWRITE_PROMPT, ft, { TextAlignment::CENTRE });
1213 }
1214 
1215 #pragma endregion
1216