The Battle for Wesnoth  1.19.10+dev
game_load.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2008 - 2025
3  by Jörg Hinrichs <joerg.hinrichs@alice-dsl.de>
4  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5 
6  This program is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY.
12 
13  See the COPYING file for more details.
14 */
15 
16 #define GETTEXT_DOMAIN "wesnoth-lib"
17 
19 
20 #include "desktop/open.hpp"
21 #include "filesystem.hpp"
22 #include "formatter.hpp"
23 #include "formula/string_utils.hpp"
24 #include "game_config.hpp"
25 #include "gettext.hpp"
26 #include "gui/auxiliary/field.hpp"
28 #include "gui/widgets/button.hpp"
29 #include "gui/widgets/label.hpp"
30 #include "gui/widgets/listbox.hpp"
31 #include "gui/widgets/minimap.hpp"
33 #include "gui/widgets/text_box.hpp"
35 #include "gui/widgets/window.hpp"
36 #include "language.hpp"
37 #include "picture.hpp"
40 #include "serialization/markup.hpp"
41 #include "utils/general.hpp"
42 #include "utils/ci_searcher.hpp"
43 #include "game_config_view.hpp"
44 
45 #include <functional>
46 
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::dialogs
55 {
56 
57 REGISTER_DIALOG(game_load)
58 
59 bool game_load::execute(const game_config_view& cache_config, savegame::load_game_metadata& data)
60 {
61  if(savegame::save_index_class::default_saves_dir()->get_saves_list().empty()) {
62  bool found_files = false;
63  for(const auto& dir : filesystem::find_other_version_saves_dirs()) {
64  if(!found_files) {
65  // this needs to be a shared_ptr because get_saves_list() uses shared_from_this
66  auto index = std::make_shared<savegame::save_index_class>(dir.path);
67  found_files = !index->get_saves_list().empty();
68  }
69  }
70 
71  if(!found_files) {
72  gui2::show_transient_message(_("No Saved Games"), _("There are no saved games to load."));
73  return false;
74  }
75  }
76 
77  return game_load(cache_config, data).show();
78 }
79 
81  : modal_dialog(window_id())
83  , save_index_manager_(data.manager)
84  , change_difficulty_(register_bool("change_difficulty", true, data.select_difficulty))
85  , show_replay_(register_bool("show_replay", true, data.show_replay))
86  , cancel_orders_(register_bool("cancel_orders", true, data.cancel_orders))
87  , summary_(data.summary)
88  , games_()
89  , cache_config_(cache_config)
90 {
91 }
92 
94 {
95  // Allow deleting saves with the Delete key.
96  connect_signal_pre_key_press(*this, std::bind(&game_load::key_press_callback, this, std::placeholders::_5));
97 
98  text_box* filter = find_widget<text_box>("txtFilter", false, true);
99 
100  filter->on_modified([this](const auto& box) { apply_filter_text(box.text()); });
101 
102  listbox& list = find_widget<listbox>("savegame_list");
103 
105 
107  add_to_keyboard_chain(&list);
108 
109  list.set_sorters(
110  [this](const std::size_t i) { return games_[i].name(); },
111  [this](const std::size_t i) { return games_[i].modified(); }
112  );
113 
115 
116  connect_signal_mouse_left_click(find_widget<button>("delete"),
117  std::bind(&game_load::delete_button_callback, this));
118 
119  connect_signal_mouse_left_click(find_widget<button>("browse_saves_folder"),
120  std::bind(&game_load::browse_button_callback, this));
121 
122  menu_button& dir_list = find_widget<menu_button>("dirList");
123 
124  dir_list.set_use_markup(true);
125  set_save_dir_list(dir_list);
126 
128 
130 }
131 
133 {
134  const auto other_dirs = filesystem::find_other_version_saves_dirs();
135  if(other_dirs.empty()) {
137  return;
138  }
139 
140  std::vector<config> options;
141 
142  // The first option in the list is the current version's save dir
143  options.emplace_back("label", _("game_version^Current Version"), "path", "");
144 
145  for(const auto& known_dir : other_dirs) {
146  options.emplace_back(
147  "label", VGETTEXT("game_version^Wesnoth $version", utils::string_map{{"version", known_dir.version}}),
148  "path", known_dir.path
149  );
150  }
151 
152  dir_list.set_values(options);
153 }
154 
156 {
157  listbox& list = find_widget<listbox>("savegame_list");
158 
159  list.clear();
160 
161  games_ = save_index_manager_->get_saves_list();
162 
163  for(const auto& game : games_) {
164  std::string name = game.name();
165  utils::ellipsis_truncate(name, 40);
166 
167  list.add_row(widget_data{
168  { "filename", {
169  { "label", std::move(name) }
170  }},
171  { "date", {
172  { "label", game.format_time_summary() }
173  }},
174  });
175  }
176 
177  find_widget<button>("delete").set_active(!save_index_manager_->read_only());
178 }
179 
181 {
182  filename_ = game.name();
183  summary_ = game.summary();
184 
185  find_widget<minimap>("minimap")
186  .set_map_data(summary_["map_data"]);
187 
188  find_widget<label>("lblScenario")
189  .set_label(summary_["label"]);
190 
191  listbox& leader_list = find_widget<listbox>("leader_list");
192 
193  leader_list.clear();
194 
195  const std::string sprite_scale_mod = (formatter() << "~SCALE_INTO(" << game_config::tile_size << ',' << game_config::tile_size << ')').str();
196 
197  unsigned li = 0;
198  for(const auto& leader : summary_.child_range("leader")) {
200  widget_item item;
201 
202  // First, we evaluate whether the leader image as provided exists.
203  // If not, we try getting a binary path-independent path. If that still doesn't
204  // work, we fallback on unknown-unit.png.
205  std::string leader_image = leader["leader_image"].str();
206  if(!::image::exists(leader_image)) {
207  auto indep_path = filesystem::get_independent_binary_file_path("images", leader_image);
208 
209  // The leader TC modifier isn't appending if the independent image path can't
210  // be resolved during save_index entry creation, so we need to add it here.
211  if(indep_path) {
212  leader_image = indep_path.value() + leader["leader_image_tc_modifier"].str();
213  }
214  }
215 
216  if(leader_image.empty()) {
217  leader_image = "units/unknown-unit.png" + leader["leader_image_tc_modifier"].str();
218  } else {
219  // Scale down any sprites larger than 72x72
220  leader_image += sprite_scale_mod + "~FL(horiz)";
221  }
222 
223  item["label"] = leader_image;
224  data.emplace("imgLeader", item);
225 
226  item["label"] = leader["leader_name"];
227  data.emplace("leader_name", item);
228 
229  item["label"] = leader["gold"];
230  data.emplace("leader_gold", item);
231 
232  // TRANSLATORS: "reserve" refers to units on the recall list
233  item["label"] = VGETTEXT("$active active, $reserve reserve", {{"active", leader["units"]}, {"reserve", leader["recall_units"]}});
234  data.emplace("leader_troops", item);
235 
236  leader_list.add_row(data);
237 
238  // FIXME: hack. In order to use the listbox in view-only mode, you also need to
239  // disable the max number of "selected items", since in this mode, "selected" is
240  // synonymous with "visible". This basically just flags all rows as visible. Need
241  // a better solution at some point
242  leader_list.select_row(li++, true);
243  }
244 
245  std::stringstream str;
246  str << game.format_time_local() << "\n";
248 
249  // The new label value may have more or less lines than the previous value, so invalidate the layout.
250  find_widget<styled_widget>("slblSummary").set_label(str.str());
251  //invalidate_layout();
252 
253  toggle_button& replay_toggle = dynamic_cast<toggle_button&>(*show_replay_->get_widget());
254  toggle_button& cancel_orders_toggle = dynamic_cast<toggle_button&>(*cancel_orders_->get_widget());
255  toggle_button& change_difficulty_toggle = dynamic_cast<toggle_button&>(*change_difficulty_->get_widget());
256 
257  const bool is_replay = savegame::loadgame::is_replay_save(summary_);
258  const bool is_scenario_start = summary_["turn"].empty();
259 
260  // Always toggle show_replay on if the save is a replay
261  replay_toggle.set_value(is_replay);
262  replay_toggle.set_active(!is_replay && !is_scenario_start);
263 
264  // Cancel orders doesn't make sense on replay saves or start-of-scenario saves
265  cancel_orders_toggle.set_active(!is_replay && !is_scenario_start);
266 
267  // Changing difficulty doesn't make sense on non-start-of-scenario saves
268  change_difficulty_toggle.set_active(!is_replay && is_scenario_start);
269 }
270 
271 // This is a wrapper that prevents a corrupted save file (if it happens to be
272 // the first in the list) from making the dialog fail to open.
274 {
275  bool successfully_displayed_a_game = false;
276 
277  try {
278  const int selected_row = find_widget<listbox>("savegame_list").get_selected_row();
279  if(selected_row < 0) {
280  find_widget<button>("delete").set_active(false);
281  } else {
282  find_widget<button>("delete").set_active(!save_index_manager_->read_only());
284  successfully_displayed_a_game = true;
285  }
286  } catch(const config::error& e) {
287  // Clear the UI widgets, show an error message.
288  const std::string preamble = _("The selected file is corrupt: ");
289  const std::string message = e.message.empty() ? "(no details)" : e.message;
290  ERR_GAMELOADDLG << preamble << message;
291  }
292 
293  if(!successfully_displayed_a_game) {
294  find_widget<minimap>("minimap").set_map_data("");
295  find_widget<label>("lblScenario")
296  .set_label("");
297  find_widget<styled_widget>("slblSummary")
298  .set_label("");
299 
300  listbox& leader_list = find_widget<listbox>("leader_list");
301  leader_list.clear();
302 
303  toggle_button& replay_toggle = dynamic_cast<toggle_button&>(*show_replay_->get_widget());
304  toggle_button& cancel_orders_toggle = dynamic_cast<toggle_button&>(*cancel_orders_->get_widget());
305  toggle_button& change_difficulty_toggle = dynamic_cast<toggle_button&>(*change_difficulty_->get_widget());
306 
307  replay_toggle.set_active(false);
308  cancel_orders_toggle.set_active(false);
309  change_difficulty_toggle.set_active(false);
310  }
311 
312  // Disable Load button if nothing is selected or if the currently selected file can't be loaded
313  find_widget<button>("ok").set_active(successfully_displayed_a_game);
314 
315  // Disable 'Enter' loading in the same circumstance
316  set_enter_disabled(!successfully_displayed_a_game);
317 }
318 
319 void game_load::apply_filter_text(const std::string& text)
320 {
321  find_widget<listbox>("savegame_list").filter_rows_by(
322  [this, match = translation::make_ci_matcher(text)](std::size_t row) { return match(games_[row].name()); });
323 }
324 
325 void game_load::evaluate_summary_string(std::stringstream& str, const config& cfg_summary)
326 {
327  if(cfg_summary["corrupt"].to_bool()) {
328  str << "\n" << markup::span_color("#f00", _("(Invalid)"));
329  // \todo: this skips the catch() statement in display_savegame. Low priority, as the
330  // dialog's state is reasonable; the "load" button is inactive, the "delete" button is
331  // active, and (cosmetic bug) it leaves the "change difficulty" toggle active. Can be
332  // triggered by creating an empty file in the save directory.
333  return;
334  }
335 
336  const std::string& campaign_type = cfg_summary["campaign_type"];
337  const std::string campaign_id = cfg_summary["campaign"];
338  auto campaign_type_enum = campaign_type::get_enum(campaign_type);
339 
340  if(campaign_type_enum) {
341  switch(*campaign_type_enum) {
342  case campaign_type::type::scenario: {
343  const config* campaign = nullptr;
344  if(!campaign_id.empty()) {
345  if(auto c = cache_config_.find_child("campaign", "id", campaign_id)) {
346  campaign = c.ptr();
347  }
348  }
349 
350  utils::string_map symbols;
351  if(campaign != nullptr) {
352  symbols["campaign_name"] = (*campaign)["name"];
353  } else {
354  // Fallback to nontranslatable campaign id.
355  symbols["campaign_name"] = "(" + campaign_id + ")";
356  }
357 
358  str << VGETTEXT("Campaign: $campaign_name", symbols);
359 
360  // Display internal id for debug purposes if we didn't above
361  if(game_config::debug && (campaign != nullptr)) {
362  str << '\n' << "(" << campaign_id << ")";
363  }
364  break;
365  }
366  case campaign_type::type::multiplayer:
367  str << _("Multiplayer");
368  break;
369  case campaign_type::type::tutorial:
370  str << _("Tutorial");
371  break;
372  case campaign_type::type::test:
373  str << _("Test scenario");
374  break;
375  }
376  } else {
377  str << campaign_type;
378  }
379 
380  str << "\n";
381 
382  if(savegame::loadgame::is_replay_save(cfg_summary)) {
383  str << _("Replay");
384  } else if(!cfg_summary["turn"].empty()) {
385  str << _("Turn") << " " << cfg_summary["turn"];
386  } else {
387  str << _("Scenario start");
388  }
389 
390  if(campaign_type_enum) {
391  switch (*campaign_type_enum) {
392  case campaign_type::type::scenario:
393  case campaign_type::type::multiplayer: {
394  const config* campaign = nullptr;
395  if (!campaign_id.empty()) {
396  if (auto c = cache_config_.find_child("campaign", "id", campaign_id)) {
397  campaign = c.ptr();
398  }
399  }
400 
401  // 'SCENARIO' or SP should only ever be campaigns
402  // 'MULTIPLAYER' may be a campaign with difficulty or single scenario without difficulty
403  // For the latter do not show the difficulty - even though it will be listed as
404  // NORMAL -> Medium in the save file it should not be considered valid (GitHub Issue #5321)
405  if (campaign != nullptr) {
406  str << "\n" << _("Difficulty: ");
407  try {
408  const config& difficulty = campaign->find_mandatory_child("difficulty", "define", cfg_summary["difficulty"]);
409  std::ostringstream ss;
410  ss << difficulty["label"] << " (" << difficulty["description"] << ")";
411  str << ss.str();
412  }
413  catch (const config::error&) {
414  // fall back to standard difficulty string in case of exception
415  str << string_table[cfg_summary["difficulty"]];
416  }
417  }
418 
419  break;
420  }
421  case campaign_type::type::tutorial:
422  case campaign_type::type::test:
423  break;
424  }
425  } else {
426  }
427 
428  if(!cfg_summary["version"].empty()) {
429  str << "\n" << _("Version: ") << cfg_summary["version"];
430  }
431 
432  const std::vector<std::string>& active_mods = utils::split(cfg_summary["active_mods"]);
433  if(!active_mods.empty()) {
434  str << "\n" << _("Modifications: ");
435  for(const auto& mod_id : active_mods) {
436  std::string mod_name;
437  try {
438  mod_name = cache_config_.find_mandatory_child("modification", "id", mod_id)["name"].str();
439  } catch(const config::error&) {
440  // Fallback to nontranslatable mod id.
441  mod_name = "(" + mod_id + ")";
442  }
443 
444  str << "\n" << font::unicode_bullet << " " << mod_name;
445  }
446  }
447 }
449 {
451 }
452 
454 {
455  listbox& list = find_widget<listbox>("savegame_list");
456 
457  const std::size_t index = std::size_t(list.get_selected_row());
458  if(index < games_.size()) {
459 
460  // See if we should ask the user for deletion confirmation
461  if(prefs::get().ask_delete()) {
462  if(!gui2::dialogs::game_delete::execute()) {
463  return;
464  }
465  }
466 
467  // Delete the file
468  save_index_manager_->delete_game(games_[index].name());
469 
470  // Remove it from the list of saves
471  games_.erase(games_.begin() + index);
472 
473  list.remove_row(index);
474 
476  }
477 }
478 
479 void game_load::key_press_callback(const SDL_Keycode key)
480 {
481  //
482  // Don't delete games when we're typing in the textbox!
483  //
484  // I'm not sure if this check was necessary when I first added this feature
485  // (I didn't check at the time), but regardless, it's needed now. If it turns
486  // out I screwed something up in my refactoring, I'll remove
487  //
488  // - vultraz, 2017-08-28
489  //
490  if(find_widget<text_box>("txtFilter").get_state() == text_box_base::FOCUSED) {
491  return;
492  }
493 
494  if(key == SDLK_DELETE) {
496  }
497 }
498 
500 {
501  menu_button& dir_list = find_widget<menu_button>("dirList");
502 
503  const auto& path = dir_list.get_value_config()["path"].str();
504  if(path.empty()) {
506  } else {
507  save_index_manager_ = std::make_shared<savegame::save_index_class>(path);
508  }
509 
511  if(auto* filter = find_widget<text_box>("txtFilter", false, true)) {
512  apply_filter_text(filter->get_value());
513  }
515 }
516 
517 } // namespace dialogs
std::string filename_
Definition: action_wml.cpp:534
string_enums::enum_base< campaign_type_defines > campaign_type
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:158
config & find_mandatory_child(config_key_type key, const std::string &name, const std::string &value)
Definition: config.cpp:806
child_itors child_range(config_key_type key)
Definition: config.cpp:268
bool empty() const
Definition: config.cpp:845
std::ostringstream wrapper.
Definition: formatter.hpp:40
A class grating read only view to a vector of config objects, viewed as one config with all children ...
optional_const_config find_child(config_key_type key, const std::string &name, const std::string &value) const
const config & find_mandatory_child(config_key_type key, const std::string &name, const std::string &value) const
void display_savegame_internal(const savegame::save_info &game)
Part of display_savegame that might throw a config::error if the savegame data is corrupt.
Definition: game_load.cpp:180
void apply_filter_text(const std::string &text)
Hides saves not matching the given filter.
Definition: game_load.cpp:319
field_bool * show_replay_
Definition: game_load.hpp:67
virtual void pre_show() override
Actions to be taken before showing the window.
Definition: game_load.cpp:93
game_load(const game_config_view &cache_config, savegame::load_game_metadata &data)
Definition: game_load.cpp:80
void key_press_callback(const SDL_Keycode key)
Definition: game_load.cpp:479
void evaluate_summary_string(std::stringstream &str, const config &cfg_summary)
Definition: game_load.cpp:325
void set_save_dir_list(menu_button &dir_list)
Definition: game_load.cpp:132
field_bool * cancel_orders_
Definition: game_load.hpp:68
const game_config_view & cache_config_
Definition: game_load.hpp:73
std::shared_ptr< savegame::save_index_class > & save_index_manager_
Definition: game_load.hpp:64
std::string & filename_
Definition: game_load.hpp:63
std::vector< savegame::save_info > games_
Definition: game_load.hpp:72
void populate_game_list()
Update (both internally and visually) the list of games.
Definition: game_load.cpp:155
field_bool * change_difficulty_
Definition: game_load.hpp:66
Main class to show messages to the user.
Definition: message.hpp:36
Abstract base class for all modal dialogs.
bool show(const unsigned auto_close_time=0)
Shows the window.
styled_widget * get_widget()
Definition: field.hpp:192
void set_active(const bool active)
Activates all children.
Definition: grid.cpp:167
The listbox class.
Definition: listbox.hpp:41
grid & add_row(const widget_item &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:92
void set_sorters(Args &&... functors)
Registers sorting controls using magic index IDs.
Definition: listbox.hpp:306
bool select_row(const unsigned row, const bool select=true)
Selects a row.
Definition: listbox.cpp:280
void remove_row(const unsigned row, unsigned count=1)
Removes a row in the listbox.
Definition: listbox.cpp:112
void clear()
Removes all the rows in the listbox, clearing it.
Definition: listbox.cpp:153
int get_selected_row() const
Returns the first selected row.
Definition: listbox.cpp:305
const ::config & get_value_config() const
Returns the entire config object for the selected row.
Definition: menu_button.hpp:70
void set_values(const std::vector<::config > &values, unsigned selected=0)
virtual unsigned get_state() const override
See styled_widget::get_state.
Definition: panel.cpp:61
virtual void set_label(const t_string &text)
virtual void set_use_markup(bool use_markup)
A widget that allows the user to input text in single line.
Definition: text_box.hpp:125
virtual void set_value(unsigned selected, bool fire_event=false) override
Inherited from selectable_item.
virtual void set_active(const bool active) override
See styled_widget::set_active.
void set_visible(const visibility visible)
Definition: widget.cpp:479
@ invisible
The user set the widget invisible, that means:
void set_enter_disabled(const bool enter_disabled)
Disable the enter key.
Definition: window.hpp:321
void keyboard_capture(widget *widget)
Definition: window.cpp:1201
void add_to_keyboard_chain(widget *widget)
Adds the widget to the keyboard chain.
Definition: window.cpp:1207
static prefs & get()
static bool is_replay_save(const config &cfg)
Definition: savegame.hpp:127
static std::shared_ptr< save_index_class > default_saves_dir()
Returns an instance for managing saves in filesystem::get_saves_dir()
Definition: save_index.cpp:210
Filename and modification date for a file list.
Definition: save_index.hpp:28
Implements some helper classes to ease adding fields to a dialog and hide the synchronization needed.
Declarations for File-IO.
#define VGETTEXT(msgid,...)
Handy wrappers around interpolate_variables_into_string and gettext.
std::size_t i
Definition: function.cpp:1030
#define ERR_GAMELOADDLG
Definition: game_load.cpp:49
static lg::log_domain log_gameloaddlg
Definition: game_load.cpp:48
static std::string _(const char *str)
Definition: gettext.hpp:97
This file contains the window object, this object is a top level container which has the event manage...
auto string_table
Definition: language.hpp:68
bool open_object([[maybe_unused]] const std::string &path_or_url)
Definition: open.cpp:46
utils::optional< std::string > get_independent_binary_file_path(const std::string &type, const std::string &filename)
Returns an asset path to filename for binary path-independent use in saved games.
std::vector< other_version_dir > find_other_version_saves_dirs()
Searches for directories containing saves created by other versions of Wesnoth.
Definition: filesystem.cpp:895
const std::string unicode_bullet
Definition: constants.cpp:47
std::string path
Definition: filesystem.cpp:93
const bool & debug
Definition: game_config.cpp:95
unsigned int tile_size
Definition: game_config.cpp:55
REGISTER_DIALOG(editor_edit_unit)
void connect_signal_pre_key_press(dispatcher &dispatcher, const signal_keyboard &signal)
Connects the signal for 'snooping' on the keypress.
Definition: dispatcher.cpp:158
void connect_signal_notify_modified(dispatcher &dispatcher, const signal_notification &signal)
Connects a signal handler for getting a notification upon modification.
Definition: dispatcher.cpp:189
void connect_signal_mouse_left_click(dispatcher &dispatcher, const signal &signal)
Connects a signal handler for a left mouse button click.
Definition: dispatcher.cpp:163
std::map< std::string, widget_item > widget_data
Definition: widget.hpp:36
std::map< std::string, t_string > widget_item
Definition: widget.hpp:33
void show_transient_message(const std::string &title, const std::string &message, const std::string &image, const bool message_use_markup, const bool title_use_markup)
Shows a transient message to the user.
bool exists(const image::locator &i_locator)
Returns true if the given image actually exists, without loading it.
Definition: picture.cpp:835
std::string span_color(const color_t &color, Args &&... data)
Applies Pango markup to the input specifying its display color.
Definition: markup.hpp:110
auto make_ci_matcher(std::string_view filter_text)
Returns a function which performs locale-aware case-insensitive search.
Definition: ci_searcher.hpp:24
std::size_t index(std::string_view str, const std::size_t index)
Codepoint index corresponding to the nth character in a UTF-8 string.
Definition: unicode.cpp:70
constexpr auto filter
Definition: ranges.hpp:38
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.
std::map< std::string, t_string > string_map
std::vector< std::string > split(const config_attribute_value &val)
Desktop environment interaction functions.
std::string_view data
Definition: picture.cpp:178
std::string filename
Filename.
The base template for associating string values with enum values.
Definition: enum_base.hpp:33
static constexpr utils::optional< enum_type > get_enum(const std::string_view value)
Converts a string into its enum equivalent.
Definition: enum_base.hpp:57
mock_char c
#define e