The Battle for Wesnoth  1.15.2+dev
game_load.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2008 - 2018 by Jörg Hinrichs <joerg.hinrichs@alice-dsl.de>
3  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
4 
5  This program is free software; you can redistribute it and/or modify
6  it under the terms of the GNU General Public License as published by
7  the Free Software Foundation; either version 2 of the License, or
8  (at your option) any later version.
9  This program is distributed in the hope that it will be useful,
10  but WITHOUT ANY WARRANTY.
11 
12  See the COPYING file for more details.
13 */
14 
15 #define GETTEXT_DOMAIN "wesnoth-lib"
16 
18 
19 #include "desktop/open.hpp"
20 #include "filesystem.hpp"
21 #include "formatter.hpp"
22 #include "formula/string_utils.hpp"
23 #include "gettext.hpp"
24 #include "game_config.hpp"
25 #include "preferences/game.hpp"
26 #include "game_classification.hpp"
27 #include "gui/auxiliary/field.hpp"
28 #include "gui/core/log.hpp"
30 #include "gui/widgets/button.hpp"
31 #include "gui/widgets/image.hpp"
32 #include "gui/widgets/label.hpp"
33 #include "gui/widgets/listbox.hpp"
34 #include "gui/widgets/minimap.hpp"
35 #include "gui/widgets/settings.hpp"
37 #include "gui/widgets/text_box.hpp"
39 #include "gui/widgets/window.hpp"
40 #include "picture.hpp"
41 #include "language.hpp"
43 #include "utils/general.hpp"
44 
45 #include <cctype>
46 #include "utils/functional.hpp"
47 
48 static lg::log_domain log_gameloaddlg{"gui/dialogs/game_load_dialog"};
49 #define ERR_GAMELOADDLG LOG_STREAM(err, log_gameloaddlg)
50 #define WRN_GAMELOADDLG LOG_STREAM(warn, log_gameloaddlg)
51 #define LOG_GAMELOADDLG LOG_STREAM(info, log_gameloaddlg)
52 #define DBG_GAMELOADDLG LOG_STREAM(debug, log_gameloaddlg)
53 
54 namespace gui2
55 {
56 namespace dialogs
57 {
58 
59 /*WIKI
60  * @page = GUIWindowDefinitionWML
61  * @order = 2_game_load
62  *
63  * == Load a game ==
64  *
65  * This shows the dialog to select and load a savegame file.
66  *
67  * @begin{table}{dialog_widgets}
68  *
69  * txtFilter & & text & m &
70  * The filter for the listbox items. $
71  *
72  * savegame_list & & listbox & m &
73  * List of savegames. $
74  *
75  * -filename & & styled_widget & m &
76  * Name of the savegame. $
77  *
78  * -date & & styled_widget & o &
79  * Date the savegame was created. $
80  *
81  * -minimap & & minimap & m &
82  * Minimap of the selected savegame. $
83  *
84  * -imgLeader & & image & m &
85  * The image of the leader in the selected savegame. $
86  *
87  * -lblScenario & & label & m &
88  * The name of the scenario of the selected savegame. $
89  *
90  * -lblSummary & & label & m &
91  * Summary of the selected savegame. $
92  *
93  * @end{table}
94  */
95 
96 REGISTER_DIALOG(game_load)
97 
98 game_load::game_load(const config& cache_config, savegame::load_game_metadata& data)
99  : filename_(data.filename)
100  , change_difficulty_(register_bool("change_difficulty", true, data.select_difficulty))
101  , show_replay_(register_bool("show_replay", true, data.show_replay))
102  , cancel_orders_(register_bool("cancel_orders", true, data.cancel_orders))
103  , summary_(data.summary)
104  , games_({savegame::get_saves_list()})
105  , cache_config_(cache_config)
106  , last_words_()
107 {
108 }
109 
111 {
112  // Allow deleting saves with the Delete key.
113  connect_signal_pre_key_press(window, std::bind(&game_load::key_press_callback, this, std::ref(window), _5));
114 
115  find_widget<minimap>(&window, "minimap", false).set_config(&cache_config_);
116 
117  text_box* filter = find_widget<text_box>(&window, "txtFilter", false, true);
118 
120  std::bind(&game_load::filter_text_changed, this, _1, _2));
121 
122  listbox& list = find_widget<listbox>(&window, "savegame_list", false);
123 
125  std::bind(&game_load::display_savegame, this, std::ref(window)));
126 
127  window.keyboard_capture(filter);
128  window.add_to_keyboard_chain(&list);
129 
130  list.clear();
131 
132  for(const auto& game : games_) {
133  std::map<std::string, string_map> data;
135 
136  std::string name = game.name();
137  utils::ellipsis_truncate(name, 40);
138  item["label"] = name;
139  data.emplace("filename", item);
140 
141  item["label"] = game.format_time_summary();
142  data.emplace("date", item);
143 
144  list.add_row(data);
145  }
146 
147  list.register_sorting_option(0, [this](const int i) { return games_[i].name(); });
148  list.register_sorting_option(1, [this](const int i) { return games_[i].modified(); });
149 
151  find_widget<button>(&window, "delete", false),
153  this, std::ref(window)));
154 
156  find_widget<button>(&window, "browse_saves_folder", false),
158 
159  display_savegame(window);
160 }
161 
163 {
164  const int selected_row =
165  find_widget<listbox>(&window, "savegame_list", false).get_selected_row();
166 
167  if(selected_row == -1) {
168  return;
169  }
170 
171  savegame::save_info& game = games_[selected_row];
172  filename_ = game.name();
173  summary_ = game.summary();
174 
175  find_widget<minimap>(&window, "minimap", false)
176  .set_map_data(summary_["map_data"]);
177 
178  find_widget<label>(&window, "lblScenario", false)
179  .set_label(summary_["label"]);
180 
181  listbox& leader_list = find_widget<listbox>(&window, "leader_list", false);
182 
183  leader_list.clear();
184 
185  const std::string sprite_scale_mod = (formatter() << "~SCALE_INTO(" << game_config::tile_size << ',' << game_config::tile_size << ')').str();
186 
187  for(const auto& leader : summary_.child_range("leader")) {
188  std::map<std::string, string_map> data;
190 
191  // First, we evaluate whether the leader image as provided exists.
192  // If not, we try getting a binary path-independent path. If that still doesn't
193  // work, we fallback on unknown-unit.png.
194  std::string leader_image = leader["leader_image"].str();
195  if(!::image::exists(leader_image)) {
196  leader_image = filesystem::get_independent_image_path(leader_image);
197 
198  // The leader TC modifier isn't appending if the independent image path can't
199  // be resolved during save_index entry creation, so we need to add it here.
200  if(!leader_image.empty()) {
201  leader_image += leader["leader_image_tc_modifier"].str();
202  }
203  }
204 
205  if(leader_image.empty()) {
206  leader_image = "units/unknown-unit.png" + leader["leader_image_tc_modifier"].str();
207  } else {
208  // Scale down any sprites larger than 72x72
209  leader_image += sprite_scale_mod;
210  }
211 
212  item["label"] = leader_image;
213  data.emplace("imgLeader", item);
214 
215  item["label"] = leader["leader_name"];
216  data.emplace("leader_name", item);
217 
218  item["label"] = leader["gold"];
219  data.emplace("leader_gold", item);
220 
221  item["label"] = leader["units"];
222  data.emplace("leader_troops", item);
223 
224  item["label"] = leader["recall_units"];
225  data.emplace("leader_reserves", item);
226 
227  leader_list.add_row(data);
228  }
229 
230  std::stringstream str;
231  str << game.format_time_local() << "\n";
233 
234  // The new label value may have more or less lines than the previous value, so invalidate the layout.
235  find_widget<scroll_label>(&window, "slblSummary", false).set_label(str.str());
236  window.invalidate_layout();
237 
238  toggle_button& replay_toggle = dynamic_cast<toggle_button&>(*show_replay_->get_widget());
239  toggle_button& cancel_orders_toggle = dynamic_cast<toggle_button&>(*cancel_orders_->get_widget());
240  toggle_button& change_difficulty_toggle = dynamic_cast<toggle_button&>(*change_difficulty_->get_widget());
241 
242  const bool is_replay = savegame::loadgame::is_replay_save(summary_);
243  const bool is_scenario_start = summary_["turn"].empty();
244 
245  // Always toggle show_replay on if the save is a replay
246  replay_toggle.set_value(is_replay);
247  replay_toggle.set_active(!is_replay && !is_scenario_start);
248 
249  // Cancel orders doesn't make sense on replay saves or start-of-scenario saves
250  cancel_orders_toggle.set_active(!is_replay && !is_scenario_start);
251 
252  // Changing difficulty doesn't make sense on non-start-of-scenario saves
253  change_difficulty_toggle.set_active(!is_replay && is_scenario_start);
254 }
255 
256 // This is a wrapper that prevents a corrupted save file (if it happens to be
257 // the first in the list) from making the dialog fail to open.
259 {
260  try {
262  } catch(const config::error& e) {
263  // Clear the UI widgets, show an error message.
264  const std::string preamble = _("The selected file is corrupt: ");
265  const std::string message = e.message.empty() ? "(no details)" : e.message;
266  ERR_GAMELOADDLG << preamble << message << "\n";
267  find_widget<minimap>(&window, "minimap", false).set_map_data("");
268  find_widget<label>(&window, "lblScenario", false)
269  .set_label(preamble);
270  find_widget<scroll_label>(&window, "slblSummary", false)
271  .set_label(message);
272 
273  listbox& leader_list = find_widget<listbox>(&window, "leader_list", false);
274  leader_list.clear();
275 
276  toggle_button& replay_toggle = dynamic_cast<toggle_button&>(*show_replay_->get_widget());
277  toggle_button& cancel_orders_toggle = dynamic_cast<toggle_button&>(*cancel_orders_->get_widget());
278  toggle_button& change_difficulty_toggle = dynamic_cast<toggle_button&>(*change_difficulty_->get_widget());
279 
280  replay_toggle.set_active(false);
281  cancel_orders_toggle.set_active(false);
282  change_difficulty_toggle.set_active(false);
283  }
284 }
285 
286 void game_load::filter_text_changed(text_box_base* textbox, const std::string& text)
287 {
288  window& window = *textbox->get_window();
289 
290  listbox& list = find_widget<listbox>(&window, "savegame_list", false);
291 
292  const std::vector<std::string> words = utils::split(text, ' ');
293 
294  if(words == last_words_)
295  return;
296  last_words_ = words;
297 
298  boost::dynamic_bitset<> show_items;
299  show_items.resize(list.get_item_count(), true);
300 
301  if(!text.empty()) {
302  for(unsigned int i = 0; i < list.get_item_count(); i++) {
303  grid* row = list.get_row_grid(i);
304 
305  grid::iterator it = row->begin();
306  label& filename_label = find_widget<label>(*it, "filename", false);
307 
308  bool found = false;
309  for(const auto & word : words)
310  {
311  found = std::search(filename_label.get_label().str().begin(),
312  filename_label.get_label().str().end(),
313  word.begin(),
314  word.end(),
316  != filename_label.get_label().str().end();
317 
318  if(!found) {
319  // one word doesn't match, we don't reach words.end()
320  break;
321  }
322  }
323 
324  show_items[i] = found;
325  }
326  }
327 
328  list.set_row_shown(show_items);
329 
330  const bool any_shown = list.any_rows_shown();
331 
332  // Disable Load button if no games are available
333  find_widget<button>(&window, "ok", false).set_active(any_shown);
334 
335  // Disable 'Enter' loading if no games are available
336  window.set_enter_disabled(!any_shown);
337 }
338 
339 void game_load::evaluate_summary_string(std::stringstream& str, const config& cfg_summary)
340 {
341  std::string difficulty_human_str = string_table[cfg_summary["difficulty"]];
342  if(cfg_summary["corrupt"].to_bool()) {
343  str << "\n<span color='#f00'>" << _("(Invalid)") << "</span>";
344 
345  return;
346  }
347 
348  const std::string& campaign_type = cfg_summary["campaign_type"];
349 
350  try {
351  switch(game_classification::CAMPAIGN_TYPE::string_to_enum(campaign_type).v) {
353  const std::string campaign_id = cfg_summary["campaign"];
354 
355  const config* campaign = nullptr;
356  if(!campaign_id.empty()) {
357  if(const config& c = cache_config_.find_child("campaign", "id", campaign_id)) {
358  campaign = &c;
359  }
360  }
361 
362  if (campaign != nullptr) {
363  try {
364  const config &difficulty = campaign->find_child("difficulty", "define", cfg_summary["difficulty"]);
365  std::ostringstream ss;
366  ss << difficulty["label"] << " (" << difficulty["description"] << ")";
367  difficulty_human_str = ss.str();
368  } catch(const config::error&) {
369  }
370  }
371 
372  utils::string_map symbols;
373  if(campaign != nullptr) {
374  symbols["campaign_name"] = (*campaign)["name"];
375  } else {
376  // Fallback to nontranslatable campaign id.
377  symbols["campaign_name"] = "(" + campaign_id + ")";
378  }
379 
380  str << VGETTEXT("Campaign: $campaign_name", symbols);
381 
382  // Display internal id for debug purposes if we didn't above
383  if(game_config::debug && (campaign != nullptr)) {
384  str << '\n' << "(" << campaign_id << ")";
385  }
386  break;
387  }
388  case game_classification::CAMPAIGN_TYPE::MULTIPLAYER:
389  str << _("Multiplayer");
390  break;
391  case game_classification::CAMPAIGN_TYPE::TUTORIAL:
392  str << _("Tutorial");
393  break;
394  case game_classification::CAMPAIGN_TYPE::TEST:
395  str << _("Test scenario");
396  break;
397  }
398  } catch(const bad_enum_cast&) {
399  str << campaign_type;
400  }
401 
402  str << "\n";
403 
404  if(savegame::loadgame::is_replay_save(cfg_summary)) {
405  str << _("Replay");
406  } else if(!cfg_summary["turn"].empty()) {
407  str << _("Turn") << " " << cfg_summary["turn"];
408  } else {
409  str << _("Scenario start");
410  }
411 
412  str << "\n" << _("Difficulty: ")
413  << difficulty_human_str;
414 
415  if(!cfg_summary["version"].empty()) {
416  str << "\n" << _("Version: ") << cfg_summary["version"];
417  }
418 
419  const std::vector<std::string>& active_mods = utils::split(cfg_summary["active_mods"]);
420  if(!active_mods.empty()) {
421  str << "\n" << _("Modifications: ");
422  for(const auto& mod_id : active_mods) {
423  std::string mod_name;
424  try {
425  mod_name = cache_config_.find_child("modification", "id", mod_id)["name"].str();
426  } catch(const config::error&) {
427  // Fallback to nontranslatable mod id.
428  mod_name = "(" + mod_id + ")";
429  }
430 
431  str << "\n" << font::unicode_bullet << " " << mod_name;
432  }
433  }
434 }
435 
437 {
438  listbox& list = find_widget<listbox>(&window, "savegame_list", false);
439 
440  const std::size_t index = std::size_t(list.get_selected_row());
441  if(index < games_.size()) {
442 
443  // See if we should ask the user for deletion confirmation
445  if(!gui2::dialogs::game_delete::execute()) {
446  return;
447  }
448  }
449 
450  // Delete the file
451  savegame::delete_game(games_[index].name());
452 
453  // Remove it from the list of saves
454  games_.erase(games_.begin() + index);
455 
456  list.remove_row(index);
457 
458  // Close the dialog if there are no more saves
459  if(list.get_item_count() == 0) {
460  window.set_retval(retval::CANCEL);
461  }
462 
463  display_savegame(window);
464  }
465 }
466 
467 void game_load::key_press_callback(window& window, const SDL_Keycode key)
468 {
469  //
470  // Don't delete games when we're typing in the textbox!
471  //
472  // I'm not sure if this check was necessary when I first added this feature
473  // (I didn't check at the time), but regardless, it's needed now. If it turns
474  // out I screwed something up in my refactoring, I'll remove this.
475  //
476  // - vultraz, 2017-08-28
477  //
478  if(find_widget<text_box>(&window, "txtFilter", false).get_state() == text_box_base::FOCUSED) {
479  return;
480  }
481 
482  if(key == SDLK_DELETE) {
483  delete_button_callback(window);
484  }
485 }
486 
487 } // namespace dialogs
488 } // namespace gui2
Define the common log macros for the gui toolkit.
Dialog was closed with the CANCEL button.
Definition: retval.hpp:37
void set_text_changed_callback(std::function< void(text_box_base *textbox, const std::string text)> cb)
Set the text_changed callback.
virtual void set_value(unsigned selected, bool fire_event=false) override
Inherited from selectable_item.
Abstract base class for text items.
std::map< std::string, t_string > string_map
iterator begin()
Definition: grid.hpp:479
field_bool * change_difficulty_
Definition: game_load.hpp:65
config & find_child(config_key_type key, const std::string &name, const std::string &value)
Returns the first child of tag key with a name attribute containing value.
Definition: config.cpp:836
Main class to show messages to the user.
Definition: message.hpp:34
void display_savegame(window &window)
Definition: game_load.cpp:258
#define ERR_GAMELOADDLG
Definition: game_load.cpp:49
This file contains the window object, this object is a top level container which has the event manage...
child_itors child_range(config_key_type key)
Definition: config.cpp:362
std::vector< save_info > get_saves_list(const std::string *dir, const std::string *filter)
Get a list of available saves.
Definition: save_index.cpp:190
void ellipsis_truncate(std::string &str, const std::size_t size)
Truncates a string to a given utf-8 character count and then appends an ellipsis. ...
bool chars_equal_insensitive(char a, char b)
Definition: general.hpp:21
Label showing a text.
Definition: label.hpp:32
int get_selected_row() const
Returns the first selected row.
Definition: listbox.cpp:272
void delete_button_callback(window &window)
Definition: game_load.cpp:436
Implements some helper classes to ease adding fields to a dialog and hide the synchronization needed...
std::string get_saves_dir()
std::vector< std::string > last_words_
Definition: game_load.hpp:74
std::vector< std::string > split(const std::string &val, const char c, const int flags)
Splits a (comma-)separated string into a vector of pieces.
const config & summary() const
Definition: save_index.cpp:209
Class for a single line text area.
Definition: text_box.hpp:121
std::string filename_
Definition: action_wml.cpp:555
Generic file dialog.
Definition: field-fwd.hpp:22
bool exists(const image::locator &i_locator)
returns true if the given image actually exists, without loading it.
Definition: picture.cpp:1070
The listbox class.
Definition: listbox.hpp:40
Base container class.
Definition: grid.hpp:30
std::string format_time_local() const
Definition: save_index.cpp:214
Desktop environment interaction functions.
static UNUSEDNOWARN std::string _(const char *str)
Definition: gettext.hpp:91
void connect_signal_notify_modified(dispatcher &dispatcher, const signal_notification_function &signal)
Connects a signal handler for getting a notification upon modification.
Definition: dispatcher.cpp:248
This file contains the settings handling of the widget library.
std::ostringstream wrapper.
Definition: formatter.hpp:38
void clear()
Removes all the rows in the listbox, clearing it.
Definition: listbox.cpp:125
void connect_signal_mouse_left_click(dispatcher &dispatcher, const signal_function &signal)
Connects a signal handler for a left mouse button click.
Definition: dispatcher.cpp:233
Iterator for the child items.
Definition: grid.hpp:440
void delete_game(const std::string &name)
Delete a savegame.
Definition: save_index.cpp:333
static bool is_replay_save(const config &cfg)
Definition: savegame.hpp:123
unsigned get_item_count() const
Returns the number of items in the listbox.
Definition: listbox.cpp:131
std::string get_independent_image_path(const std::string &filename)
Returns an image path to filename for binary path-independent use in saved games. ...
bool any_rows_shown() const
Definition: listbox.cpp:226
std::vector< savegame::save_info > games_
Definition: game_load.hpp:71
const std::string & name() const
Definition: save_index.hpp:36
Various uncategorised dialogs.
void key_press_callback(window &window, const SDL_Keycode key)
Definition: game_load.cpp:467
styled_widget * get_widget()
Definition: field.hpp:206
bool open_object(const std::string &path_or_url)
Opens the specified object with the default application configured for its type.
Definition: open.cpp:55
virtual void pre_show(window &window) override
Inherited from modal_dialog.
Definition: game_load.cpp:110
std::size_t i
Definition: function.cpp:933
const config & cache_config_
Definition: game_load.hpp:72
window * get_window()
Get the parent window.
Definition: widget.cpp:114
std::map< std::string, t_string > string_map
Definition: widget.hpp:24
grid & add_row(const string_map &item, const int index=-1)
When an item in the list is selected by the user we need to update the state.
Definition: listbox.cpp:66
field_bool * show_replay_
Definition: game_load.hpp:66
Declarations for File-IO.
const bool & debug
std::size_t index(const std::string &str, const std::size_t index)
Codepoint index corresponding to the nth character in a UTF-8 string.
Definition: unicode.cpp:71
const std::string unicode_bullet
Definition: constants.cpp:43
#define VGETTEXT(msgid,...)
Handy wrappers around interpolate_variables_into_string and gettext.
bool ask_delete_saves()
Definition: game.cpp:804
const grid * get_row_grid(const unsigned row) const
Returns the grid of the wanted row.
Definition: listbox.cpp:237
field_bool * cancel_orders_
Definition: game_load.hpp:67
void filter_text_changed(text_box_base *textbox, const std::string &text)
Definition: game_load.cpp:286
std::string & filename_
Definition: game_load.hpp:63
const t_string & get_label() const
void display_savegame_internal(window &window)
Definition: game_load.cpp:162
Filename and modification date for a file list.
Definition: save_index.hpp:24
void connect_signal_pre_key_press(dispatcher &dispatcher, const signal_keyboard_function &signal)
Connects the signal for &#39;snooping&#39; on the keypress.
Definition: dispatcher.cpp:228
unsigned int tile_size
Definition: game_config.cpp:68
symbol_table string_table
Definition: language.cpp:63
void remove_row(const unsigned row, unsigned count=1)
Removes a row in the listbox.
Definition: listbox.cpp:86
std::string message
Definition: exceptions.hpp:31
static lg::log_domain log_gameloaddlg
Definition: game_load.cpp:48
#define e
void register_sorting_option(const int col, const Func &f)
Definition: listbox.hpp:269
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:68
mock_char c
const std::string & str() const
Definition: tstring.hpp:186
base class of top level items, the only item which needs to store the final canvases to draw on ...
Definition: window.hpp:63
void set_row_shown(const unsigned row, const bool shown)
Makes a row visible or invisible.
Definition: listbox.cpp:143
void evaluate_summary_string(std::stringstream &str, const config &cfg_summary)
Definition: game_load.cpp:339
bool empty() const
Definition: config.cpp:884
Class for a toggle button.
std::pair< std::string, unsigned > item
Definition: help_impl.hpp:371
virtual void set_active(const bool active) override
See styled_widget::set_active.