1 /*
2 * Copyright (C) 2002-2020 by the Widelands Development Team
3 *
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; either version 2
7 * of the License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 *
18 */
19
20 #include "wui/game_main_menu_save_game.h"
21
22 #include <memory>
23
24 #include <boost/algorithm/string.hpp>
25
26 #include "base/i18n.h"
27 #include "game_io/game_loader.h"
28 #include "game_io/game_preload_packet.h"
29 #include "game_io/game_saver.h"
30 #include "io/filesystem/filesystem.h"
31 #include "io/filesystem/layered_filesystem.h"
32 #include "logic/filesystem_constants.h"
33 #include "logic/game.h"
34 #include "logic/game_controller.h"
35 #include "logic/generic_save_handler.h"
36 #include "logic/playersmanager.h"
37 #include "ui_basic/messagebox.h"
38 #include "wlapplication_options.h"
39 #include "wui/interactive_gamebase.h"
40
igbase()41 InteractiveGameBase& GameMainMenuSaveGame::igbase() {
42 return dynamic_cast<InteractiveGameBase&>(*get_parent());
43 }
44
GameMainMenuSaveGame(InteractiveGameBase & parent,UI::UniqueWindow::Registry & registry)45 GameMainMenuSaveGame::GameMainMenuSaveGame(InteractiveGameBase& parent,
46 UI::UniqueWindow::Registry& registry)
47 : UI::UniqueWindow(&parent,
48 "save_game",
49 ®istry,
50 parent.get_inner_w() - 40,
51 parent.get_inner_h() - 40,
52 _("Save Game")),
53 // Values for alignment and size
54 padding_(4),
55
56 main_box_(this, 0, 0, UI::Box::Vertical),
57 info_box_(&main_box_, 0, 0, UI::Box::Horizontal),
58
59 load_or_save_(&info_box_,
60 igbase().game(),
61 LoadOrSaveGame::FileType::kShowAll,
62 UI::PanelStyle::kWui,
63 false),
64
65 filename_box_(load_or_save_.table_box(), 0, 0, UI::Box::Horizontal),
66 filename_label_(&filename_box_, 0, 0, 0, 0, _("Filename:"), UI::Align::kLeft),
67 filename_editbox_(&filename_box_, 0, 0, 0, UI::PanelStyle::kWui),
68
69 buttons_box_(load_or_save_.game_details()->button_box(), 0, 0, UI::Box::Horizontal),
70 cancel_(&buttons_box_, "cancel", 0, 0, 0, 0, UI::ButtonStyle::kWuiSecondary, _("Cancel")),
71 ok_(&buttons_box_, "ok", 0, 0, 0, 0, UI::ButtonStyle::kWuiPrimary, _("OK")),
72
73 curdir_(kSaveDir),
74 illegal_filename_tooltip_(FileSystem::illegal_filename_tooltip()) {
75
76 layout();
77
78 main_box_.add_space(padding_);
79 main_box_.set_inner_spacing(padding_);
80 main_box_.add(&info_box_, UI::Box::Resizing::kExpandBoth);
81
82 info_box_.set_inner_spacing(padding_);
83 info_box_.add_space(padding_);
84 info_box_.add(load_or_save_.table_box(), UI::Box::Resizing::kFullSize);
85 info_box_.add(load_or_save_.game_details(), UI::Box::Resizing::kExpandBoth);
86
87 load_or_save_.table_box()->add_space(padding_);
88 load_or_save_.table_box()->add(&filename_box_, UI::Box::Resizing::kFullSize);
89
90 filename_box_.set_inner_spacing(padding_);
91 filename_box_.add(&filename_label_, UI::Box::Resizing::kAlign, UI::Align::kCenter);
92 filename_box_.add(&filename_editbox_, UI::Box::Resizing::kFillSpace);
93
94 load_or_save_.game_details()->button_box()->add_space(padding_);
95 load_or_save_.game_details()->button_box()->add(&buttons_box_, UI::Box::Resizing::kFullSize);
96 buttons_box_.set_inner_spacing(padding_);
97 buttons_box_.add(&cancel_, UI::Box::Resizing::kFillSpace);
98 buttons_box_.add(&ok_, UI::Box::Resizing::kFillSpace);
99
100 ok_.set_enabled(false);
101
102 filename_editbox_.changed.connect([this]() { edit_box_changed(); });
103 filename_editbox_.ok.connect([this]() { ok(); });
104 filename_editbox_.cancel.connect(
105 [this, &parent]() { reset_editbox_or_die(parent.game().save_handler().get_cur_filename()); });
106
107 ok_.sigclicked.connect([this]() { ok(); });
108 cancel_.sigclicked.connect([this]() { die(); });
109
110 load_or_save_.table().selected.connect([this](unsigned) { entry_selected(); });
111 load_or_save_.table().double_clicked.connect([this](unsigned) { ok(); });
112 load_or_save_.table().cancel.connect([this]() { die(); });
113
114 load_or_save_.fill_table();
115 load_or_save_.select_by_name(parent.game().save_handler().get_cur_filename());
116
117 center_to_parent();
118 move_to_top();
119
120 filename_editbox_.focus();
121 pause_game(true);
122 set_thinks(false);
123 layout();
124 }
125
layout()126 void GameMainMenuSaveGame::layout() {
127 main_box_.set_size(get_inner_w() - 2 * padding_, get_inner_h() - 2 * padding_);
128 load_or_save_.table().set_desired_size(get_inner_w() * 7 / 12, load_or_save_.table().get_h());
129 load_or_save_.delete_button()->set_desired_size(ok_.get_w(), ok_.get_h());
130 }
131
entry_selected()132 void GameMainMenuSaveGame::entry_selected() {
133 ok_.set_enabled(load_or_save_.table().selections().size() == 1);
134 load_or_save_.delete_button()->set_enabled(load_or_save_.has_selection());
135 if (load_or_save_.has_selection()) {
136 std::unique_ptr<SavegameData> gamedata = load_or_save_.entry_selected();
137 if (!gamedata->is_directory()) {
138 filename_editbox_.set_text(FileSystem::filename_without_ext(gamedata->filename.c_str()));
139 }
140 }
141 }
142
edit_box_changed()143 void GameMainMenuSaveGame::edit_box_changed() {
144 // Prevent the user from creating nonsense directory names, like e.g. ".." or "...".
145 const bool is_legal_filename = FileSystem::is_legal_filename(filename_editbox_.text());
146 ok_.set_enabled(is_legal_filename);
147 filename_editbox_.set_tooltip(is_legal_filename ? "" : illegal_filename_tooltip_);
148 load_or_save_.delete_button()->set_enabled(false);
149 load_or_save_.clear_selections();
150 }
151
reset_editbox_or_die(const std::string & current_filename)152 void GameMainMenuSaveGame::reset_editbox_or_die(const std::string& current_filename) {
153 if (filename_editbox_.text() == current_filename) {
154 die();
155 } else {
156 filename_editbox_.set_text(current_filename);
157 load_or_save_.select_by_name(current_filename);
158 }
159 }
160
ok()161 void GameMainMenuSaveGame::ok() {
162 if (!ok_.enabled()) {
163 return;
164 }
165 if (load_or_save_.has_selection() && load_or_save_.entry_selected()->is_directory()) {
166 std::unique_ptr<SavegameData> gamedata = load_or_save_.entry_selected();
167 load_or_save_.change_directory_to(gamedata->filename);
168 curdir_ = gamedata->filename;
169 filename_editbox_.focus();
170 } else {
171 std::string filename = filename_editbox_.text();
172 if (save_game(filename, !get_config_bool("nozip", false))) {
173 die();
174 } else {
175 load_or_save_.table().focus();
176 }
177 }
178 }
179
die()180 void GameMainMenuSaveGame::die() {
181 pause_game(false);
182 UI::UniqueWindow::die();
183 }
184
handle_key(bool down,SDL_Keysym code)185 bool GameMainMenuSaveGame::handle_key(bool down, SDL_Keysym code) {
186 if (down) {
187 switch (code.sym) {
188 case SDLK_KP_ENTER:
189 case SDLK_RETURN:
190 ok();
191 return true;
192 case SDLK_ESCAPE:
193 die();
194 return true;
195 case SDLK_DELETE:
196 load_or_save_.clicked_delete();
197 return true;
198 default:
199 break; // not handled
200 }
201 }
202 return UI::Panel::handle_key(down, code);
203 }
204
pause_game(bool paused)205 void GameMainMenuSaveGame::pause_game(bool paused) {
206 if (igbase().is_multiplayer()) {
207 return;
208 }
209 igbase().game().game_controller()->set_paused(paused);
210 }
211
212 /**
213 * Save the game in the Savegame directory with
214 * the given filename
215 *
216 * returns true if dialog should close, false if it
217 * should stay open
218 */
save_game(std::string filename,bool binary)219 bool GameMainMenuSaveGame::save_game(std::string filename, bool binary) {
220 // Trim it for preceding/trailing whitespaces in user input
221 boost::trim(filename);
222
223 // OK, first check if the extension matches (ignoring case).
224 if (!boost::iends_with(filename, kSavegameExtension)) {
225 filename += kSavegameExtension;
226 }
227
228 // Append directory name.
229 const std::string complete_filename = curdir_ + g_fs->file_separator() + filename;
230
231 // Check if file exists. If so, show a warning.
232 if (g_fs->file_exists(complete_filename)) {
233 const std::string s =
234 (boost::format(_("A file with the name ‘%s’ already exists. Overwrite?")) %
235 FileSystem::fs_filename(filename.c_str()))
236 .str();
237 UI::WLMessageBox mbox(
238 this, _("Error Saving Game!"), s, UI::WLMessageBox::MBoxType::kOkCancel);
239 if (mbox.run<UI::Panel::Returncodes>() == UI::Panel::Returncodes::kBack) {
240 return false;
241 }
242 }
243
244 // Try saving the game.
245 Widelands::Game& game = igbase().game();
246
247 game.create_loader_ui({"general_game"}, true);
248
249 GenericSaveHandler gsh(
250 [&game](FileSystem& fs) {
251 Widelands::GameSaver gs(fs, game);
252 gs.save();
253 },
254 complete_filename, binary ? FileSystem::ZIP : FileSystem::DIR);
255 GenericSaveHandler::Error error = gsh.save();
256
257 game.remove_loader_ui();
258
259 // If only the temporary backup couldn't be deleted, we still treat it as
260 // success. Automatic cleanup will deal with later. No need to bother the
261 // player with it.
262 if (error == GenericSaveHandler::Error::kSuccess ||
263 error == GenericSaveHandler::Error::kDeletingBackupFailed) {
264 game.save_handler().set_current_filename(complete_filename);
265 igbase().log_message(_("Game saved"));
266 return true;
267 }
268
269 // Show player an error message.
270 std::string msg = gsh.localized_formatted_result_message();
271 UI::WLMessageBox mbox(this, _("Error Saving Game!"), msg, UI::WLMessageBox::MBoxType::kOk);
272 mbox.run<UI::Panel::Returncodes>();
273
274 // If only the backup failed (likely just because of a file lock),
275 // then leave the dialog open for the player to try with a new filename.
276 if (error == GenericSaveHandler::Error::kBackupFailed) {
277 return false;
278 }
279
280 // In the other error cases close the dialog.
281 igbase().log_message(_("Saving failed!"));
282 return true;
283 }
284