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                       &registry,
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