The Battle for Wesnoth  1.19.5+dev
game_load.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2008 - 2024
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 "game_config_view.hpp"
43 
44 #include <functional>
45 
46 
47 static lg::log_domain log_gameloaddlg{"gui/dialogs/game_load_dialog"};
48 #define ERR_GAMELOADDLG LOG_STREAM(err, log_gameloaddlg)
49 #define WRN_GAMELOADDLG LOG_STREAM(warn, log_gameloaddlg)
50 #define LOG_GAMELOADDLG LOG_STREAM(info, log_gameloaddlg)
51 #define DBG_GAMELOADDLG LOG_STREAM(debug, log_gameloaddlg)
52 
53 namespace gui2::dialogs
54 {
55 
56 REGISTER_DIALOG(game_load)
57 
58 bool game_load::execute(const game_config_view& cache_config, savegame::load_game_metadata& data)
59 {
60  if(savegame::save_index_class::default_saves_dir()->get_saves_list().empty()) {
61  bool found_files = false;
62  for(const auto& dir : filesystem::find_other_version_saves_dirs()) {
63  if(!found_files) {
64  // this needs to be a shared_ptr because get_saves_list() uses shared_from_this
65  auto index = std::make_shared<savegame::save_index_class>(dir.path);
66  found_files = !index->get_saves_list().empty();
67  }
68  }
69 
70  if(!found_files) {
71  gui2::show_transient_message(_("No Saved Games"), _("There are no saved games to load."));
72  return false;
73  }
74  }
75 
76  return game_load(cache_config, data).show();
77 }
78 
80  : modal_dialog(window_id())
82  , save_index_manager_(data.manager)
83  , change_difficulty_(register_bool("change_difficulty", true, data.select_difficulty))
84  , show_replay_(register_bool("show_replay", true, data.show_replay))
85  , cancel_orders_(register_bool("cancel_orders", true, data.cancel_orders))
86  , summary_(data.summary)
87  , games_()
88  , cache_config_(cache_config)
89  , last_words_()
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->set_text_changed_callback(std::bind(&game_load::filter_text_changed, this, std::placeholders::_2));
101 
102  listbox& list = find_widget<listbox>("savegame_list");
103 
105 
106  keyboard_capture(filter);
107  add_to_keyboard_chain(&list);
108 
109  list.register_sorting_option(0, [this](const int i) { return games_[i].name(); });
110  list.register_sorting_option(1, [this](const int i) { return games_[i].modified(); });
111 
113 
114  connect_signal_mouse_left_click(find_widget<button>("delete"),
115  std::bind(&game_load::delete_button_callback, this));
116 
117  connect_signal_mouse_left_click(find_widget<button>("browse_saves_folder"),
118  std::bind(&game_load::browse_button_callback, this));
119 
120  menu_button& dir_list = find_widget<menu_button>("dirList");
121 
122  dir_list.set_use_markup(true);
123  set_save_dir_list(dir_list);
124 
126 
128 }
129 
131 {
132  const auto other_dirs = filesystem::find_other_version_saves_dirs();
133  if(other_dirs.empty()) {
135  return;
136  }
137 
138  std::vector<config> options;
139 
140  // The first option in the list is the current version's save dir
141  options.emplace_back("label", _("game_version^Current Version"), "path", "");
142 
143  for(const auto& known_dir : other_dirs) {
144  options.emplace_back(
145  "label", VGETTEXT("game_version^Wesnoth $version", utils::string_map{{"version", known_dir.version}}),
146  "path", known_dir.path
147  );
148  }
149 
150  dir_list.set_values(options);
151 }
152 
154 {
155  listbox& list = find_widget<listbox>("savegame_list");
156 
157  list.clear();
158 
159  games_ = save_index_manager_->get_saves_list();
160 
161  for(const auto& game : games_) {
163  widget_item item;
164 
165  std::string name = game.name();
166  utils::ellipsis_truncate(name, 40);
167  item["label"] = name;
168  data.emplace("filename", item);
169 
170  item["label"] = game.format_time_summary();
171  data.emplace("date", item);
172 
173  list.add_row(data);
174  }
175 
176  find_widget<button>("delete").set_active(!save_index_manager_->read_only());
177 }
178 
180 {
181  filename_ = game.name();
182  summary_ = game.summary();
183 
184  find_widget<minimap>("minimap")
185  .set_map_data(summary_["map_data"]);
186 
187  find_widget<label>("lblScenario")
188  .set_label(summary_["label"]);
189 
190  listbox& leader_list = find_widget<listbox>("leader_list");
191 
192  leader_list.clear();
193 
194  const std::string sprite_scale_mod = (formatter() << "~SCALE_INTO(" << game_config::tile_size << ',' << game_config::tile_size << ')').str();
195 
196  unsigned li = 0;
197  for(const auto& leader : summary_.child_range("leader")) {
199  widget_item item;
200 
201  // First, we evaluate whether the leader image as provided exists.
202  // If not, we try getting a binary path-independent path. If that still doesn't
203  // work, we fallback on unknown-unit.png.
204  std::string leader_image = leader["leader_image"].str();
205  if(!::image::exists(leader_image)) {
206  auto indep_path = filesystem::get_independent_binary_file_path("images", leader_image);
207 
208  // The leader TC modifier isn't appending if the independent image path can't
209  // be resolved during save_index entry creation, so we need to add it here.
210  if(indep_path) {
211  leader_image = indep_path.value() + leader["leader_image_tc_modifier"].str();
212  }
213  }
214 
215  if(leader_image.empty()) {
216  leader_image = "units/unknown-unit.png" + leader["leader_image_tc_modifier"].str();
217  } else {
218  // Scale down any sprites larger than 72x72
219  leader_image += sprite_scale_mod + "~FL(horiz)";
220  }
221 
222  item["label"] = leader_image;
223  data.emplace("imgLeader", item);
224 
225  item["label"] = leader["leader_name"];
226  data.emplace("leader_name", item);
227 
228  item["label"] = leader["gold"];
229  data.emplace("leader_gold", item);
230 
231  // TRANSLATORS: "reserve" refers to units on the recall list
232  item["label"] = VGETTEXT("$active active, $reserve reserve", {{"active", leader["units"]}, {"reserve", leader["recall_units"]}});
233  data.emplace("leader_troops", item);
234 
235  leader_list.add_row(data);
236 
237  // FIXME: hack. In order to use the listbox in view-only mode, you also need to
238  // disable the max number of "selected items", since in this mode, "selected" is
239  // synonymous with "visible". This basically just flags all rows as visible. Need
240  // a better solution at some point
241  leader_list.select_row(li++, true);
242  }
243 
244  std::stringstream str;
245  str << game.format_time_local() << "\n";
247 
248  // The new label value may have more or less lines than the previous value, so invalidate the layout.
249  find_widget<styled_widget>("slblSummary").set_label(str.str());
250  //get_window()->invalidate_layout();
251 
252  toggle_button& replay_toggle = dynamic_cast<toggle_button&>(*show_replay_->get_widget());
253  toggle_button& cancel_orders_toggle = dynamic_cast<toggle_button&>(*cancel_orders_->get_widget());
254  toggle_button& change_difficulty_toggle = dynamic_cast<toggle_button&>(*change_difficulty_->get_widget());
255 
256  const bool is_replay = savegame::loadgame::is_replay_save(summary_);
257  const bool is_scenario_start = summary_["turn"].empty();
258 
259  // Always toggle show_replay on if the save is a replay
260  replay_toggle.set_value(is_replay);
261  replay_toggle.set_active(!is_replay && !is_scenario_start);
262 
263  // Cancel orders doesn't make sense on replay saves or start-of-scenario saves
264  cancel_orders_toggle.set_active(!is_replay && !is_scenario_start);
265 
266  // Changing difficulty doesn't make sense on non-start-of-scenario saves
267  change_difficulty_toggle.set_active(!is_replay && is_scenario_start);
268 }
269 
270 // This is a wrapper that prevents a corrupted save file (if it happens to be
271 // the first in the list) from making the dialog fail to open.
273 {
274  bool successfully_displayed_a_game = false;
275 
276  try {
277  const int selected_row = find_widget<listbox>("savegame_list").get_selected_row();
278  if(selected_row < 0) {
279  find_widget<button>("delete").set_active(false);
280  } else {
281  find_widget<button>("delete").set_active(!save_index_manager_->read_only());
283  successfully_displayed_a_game = true;
284  }
285  } catch(const config::error& e) {
286  // Clear the UI widgets, show an error message.
287  const std::string preamble = _("The selected file is corrupt: ");
288  const std::string message = e.message.empty() ? "(no details)" : e.message;
289  ERR_GAMELOADDLG << preamble << message;
290  }
291 
292  if(!successfully_displayed_a_game) {
293  find_widget<minimap>("minimap").set_map_data("");
294  find_widget<label>("lblScenario")
295  .set_label("");
296  find_widget<styled_widget>("slblSummary")
297  .set_label("");
298 
299  listbox& leader_list = find_widget<listbox>("leader_list");
300  leader_list.clear();
301 
302  toggle_button& replay_toggle = dynamic_cast<toggle_button&>(*show_replay_->get_widget());
303  toggle_button& cancel_orders_toggle = dynamic_cast<toggle_button&>(*cancel_orders_->get_widget());
304  toggle_button& change_difficulty_toggle = dynamic_cast<toggle_button&>(*change_difficulty_->get_widget());
305 
306  replay_toggle.set_active(false);
307  cancel_orders_toggle.set_active(false);
308  change_difficulty_toggle.set_active(false);
309  }
310 
311  // Disable Load button if nothing is selected or if the currently selected file can't be loaded
312  find_widget<button>("ok").set_active(successfully_displayed_a_game);
313 
314  // Disable 'Enter' loading in the same circumstance
315  get_window()->set_enter_disabled(!successfully_displayed_a_game);
316 }
317 
318 void game_load::filter_text_changed(const std::string& text)
319 {
320  apply_filter_text(text, false);
321 }
322 
323 void game_load::apply_filter_text(const std::string& text, bool force)
324 {
325  listbox& list = find_widget<listbox>("savegame_list");
326 
327  const std::vector<std::string> words = utils::split(text, ' ');
328 
329  if(words == last_words_ && !force)
330  return;
331  last_words_ = words;
332 
333  boost::dynamic_bitset<> show_items;
334  show_items.resize(list.get_item_count(), true);
335 
336  if(!text.empty()) {
337  for(unsigned int i = 0; i < list.get_item_count() && i < games_.size(); i++) {
338  bool found = false;
339  for(const auto & word : words)
340  {
341  found = translation::ci_search(games_[i].name(), word);
342  if(!found) {
343  // one word doesn't match, we don't reach words.end()
344  break;
345  }
346  }
347 
348  show_items[i] = found;
349  }
350  }
351 
352  list.set_row_shown(show_items);
353 }
354 
355 void game_load::evaluate_summary_string(std::stringstream& str, const config& cfg_summary)
356 {
357  if(cfg_summary["corrupt"].to_bool()) {
358  str << "\n" << markup::span_color("#f00", _("(Invalid)"));
359  // \todo: this skips the catch() statement in display_savegame. Low priority, as the
360  // dialog's state is reasonable; the "load" button is inactive, the "delete" button is
361  // active, and (cosmetic bug) it leaves the "change difficulty" toggle active. Can be
362  // triggered by creating an empty file in the save directory.
363  return;
364  }
365 
366  const std::string& campaign_type = cfg_summary["campaign_type"];
367  const std::string campaign_id = cfg_summary["campaign"];
368  auto campaign_type_enum = campaign_type::get_enum(campaign_type);
369 
370  if(campaign_type_enum) {
371  switch(*campaign_type_enum) {
372  case campaign_type::type::scenario: {
373  const config* campaign = nullptr;
374  if(!campaign_id.empty()) {
375  if(auto c = cache_config_.find_child("campaign", "id", campaign_id)) {
376  campaign = c.ptr();
377  }
378  }
379 
380  utils::string_map symbols;
381  if(campaign != nullptr) {
382  symbols["campaign_name"] = (*campaign)["name"];
383  } else {
384  // Fallback to nontranslatable campaign id.
385  symbols["campaign_name"] = "(" + campaign_id + ")";
386  }
387 
388  str << VGETTEXT("Campaign: $campaign_name", symbols);
389 
390  // Display internal id for debug purposes if we didn't above
391  if(game_config::debug && (campaign != nullptr)) {
392  str << '\n' << "(" << campaign_id << ")";
393  }
394  break;
395  }
396  case campaign_type::type::multiplayer:
397  str << _("Multiplayer");
398  break;
399  case campaign_type::type::tutorial:
400  str << _("Tutorial");
401  break;
402  case campaign_type::type::test:
403  str << _("Test scenario");
404  break;
405  }
406  } else {
407  str << campaign_type;
408  }
409 
410  str << "\n";
411 
412  if(savegame::loadgame::is_replay_save(cfg_summary)) {
413  str << _("Replay");
414  } else if(!cfg_summary["turn"].empty()) {
415  str << _("Turn") << " " << cfg_summary["turn"];
416  } else {
417  str << _("Scenario start");
418  }
419 
420  if(campaign_type_enum) {
421  switch (*campaign_type_enum) {
422  case campaign_type::type::scenario:
423  case campaign_type::type::multiplayer: {
424  const config* campaign = nullptr;
425  if (!campaign_id.empty()) {
426  if (auto c = cache_config_.find_child("campaign", "id", campaign_id)) {
427  campaign = c.ptr();
428  }
429  }
430 
431  // 'SCENARIO' or SP should only ever be campaigns
432  // 'MULTIPLAYER' may be a campaign with difficulty or single scenario without difficulty
433  // For the latter do not show the difficulty - even though it will be listed as
434  // NORMAL -> Medium in the save file it should not be considered valid (GitHub Issue #5321)
435  if (campaign != nullptr) {
436  str << "\n" << _("Difficulty: ");
437  try {
438  const config& difficulty = campaign->find_mandatory_child("difficulty", "define", cfg_summary["difficulty"]);
439  std::ostringstream ss;
440  ss << difficulty["label"] << " (" << difficulty["description"] << ")";
441  str << ss.str();
442  }
443  catch (const config::error&) {
444  // fall back to standard difficulty string in case of exception
445  str << string_table[cfg_summary["difficulty"]];
446  }
447  }
448 
449  break;
450  }
451  case campaign_type::type::tutorial:
452  case campaign_type::type::test:
453  break;
454  }
455  } else {
456  }
457 
458  if(!cfg_summary["version"].empty()) {
459  str << "\n" << _("Version: ") << cfg_summary["version"];
460  }
461 
462  const std::vector<std::string>& active_mods = utils::split(cfg_summary["active_mods"]);
463  if(!active_mods.empty()) {
464  str << "\n" << _("Modifications: ");
465  for(const auto& mod_id : active_mods) {
466  std::string mod_name;
467  try {
468  mod_name = cache_config_.find_mandatory_child("modification", "id", mod_id)["name"].str();
469  } catch(const config::error&) {
470  // Fallback to nontranslatable mod id.
471  mod_name = "(" + mod_id + ")";
472  }
473 
474  str << "\n" << font::unicode_bullet << " " << mod_name;
475  }
476  }
477 }
479 {
481 }
482 
484 {
485  listbox& list = find_widget<listbox>("savegame_list");
486 
487  const std::size_t index = std::size_t(list.get_selected_row());
488  if(index < games_.size()) {
489 
490  // See if we should ask the user for deletion confirmation
491  if(prefs::get().ask_delete()) {
492  if(!gui2::dialogs::game_delete::execute()) {
493  return;
494  }
495  }
496 
497  // Delete the file
498  save_index_manager_->delete_game(games_[index].name());
499 
500  // Remove it from the list of saves
501  games_.erase(games_.begin() + index);
502 
503  list.remove_row(index);
504 
506  }
507 }
508 
509 void game_load::key_press_callback(const SDL_Keycode key)
510 {
511  //
512  // Don't delete games when we're typing in the textbox!
513  //
514  // I'm not sure if this check was necessary when I first added this feature
515  // (I didn't check at the time), but regardless, it's needed now. If it turns
516  // out I screwed something up in my refactoring, I'll remove
517  //
518  // - vultraz, 2017-08-28
519  //
520  if(find_widget<text_box>("txtFilter").get_state() == text_box_base::FOCUSED) {
521  return;
522  }
523 
524  if(key == SDLK_DELETE) {
526  }
527 }
528 
530 {
531  menu_button& dir_list = find_widget<menu_button>("dirList");
532 
533  const auto& path = dir_list.get_value_config()["path"].str();
534  if(path.empty()) {
536  } else {
537  save_index_manager_ = std::make_shared<savegame::save_index_class>(path);
538  }
539 
541  if(auto* filter = find_widget<text_box>("txtFilter", false, true)) {
542  apply_filter_text(filter->get_value(), true);
543  }
545 }
546 
547 } // namespace dialogs
std::string filename_
Definition: action_wml.cpp:538
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:172
config & find_mandatory_child(config_key_type key, const std::string &name, const std::string &value)
Definition: config.cpp:810
child_itors child_range(config_key_type key)
Definition: config.cpp:272
bool empty() const
Definition: config.cpp:849
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 apply_filter_text(const std::string &text, bool force)
Implementation detail of filter_text_changed and handle_dir_select.
Definition: game_load.cpp:323
std::vector< std::string > last_words_
Definition: game_load.hpp:81
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:179
field_bool * show_replay_
Definition: game_load.hpp:73
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:79
void key_press_callback(const SDL_Keycode key)
Definition: game_load.cpp:509
void evaluate_summary_string(std::stringstream &str, const config &cfg_summary)
Definition: game_load.cpp:355
void set_save_dir_list(menu_button &dir_list)
Definition: game_load.cpp:130
field_bool * cancel_orders_
Definition: game_load.hpp:74
const game_config_view & cache_config_
Definition: game_load.hpp:79
std::shared_ptr< savegame::save_index_class > & save_index_manager_
Definition: game_load.hpp:70
std::string & filename_
Definition: game_load.hpp:69
std::vector< savegame::save_info > games_
Definition: game_load.hpp:78
void populate_game_list()
Update (both internally and visually) the list of games.
Definition: game_load.cpp:153
void filter_text_changed(const std::string &text)
Definition: game_load.cpp:318
field_bool * change_difficulty_
Definition: game_load.hpp:72
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:43
void set_row_shown(const unsigned row, const bool shown)
Makes a row visible or invisible.
Definition: listbox.cpp:135
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:58
bool select_row(const unsigned row, const bool select=true)
Selects a row.
Definition: listbox.cpp:242
void register_sorting_option(const int col, const Func &f)
Definition: listbox.hpp:260
void remove_row(const unsigned row, unsigned count=1)
Removes a row in the listbox.
Definition: listbox.cpp:78
void clear()
Removes all the rows in the listbox, clearing it.
Definition: listbox.cpp:117
int get_selected_row() const
Returns the first selected row.
Definition: listbox.cpp:267
unsigned get_item_count() const
Returns the number of items in the listbox.
Definition: listbox.cpp:123
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)
void set_text_changed_callback(std::function< void(text_box_base *textbox, const std::string text)> cb)
Set the text_changed callback.
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
window * get_window()
Get the parent window.
Definition: widget.cpp:117
@ invisible
The user set the widget invisible, that means:
void set_enter_disabled(const bool enter_disabled)
Disable the enter key.
Definition: window.hpp:323
void keyboard_capture(widget *widget)
Definition: window.cpp:1207
void add_to_keyboard_chain(widget *widget)
Adds the widget to the keyboard chain.
Definition: window.cpp:1213
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:209
Filename and modification date for a file list.
Definition: save_index.hpp:26
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:1023
#define ERR_GAMELOADDLG
Definition: game_load.cpp:48
static lg::log_domain log_gameloaddlg
Definition: game_load.cpp:47
static std::string _(const char *str)
Definition: gettext.hpp:93
This file contains the window object, this object is a top level container which has the event manage...
symbol_table string_table
Definition: language.cpp:64
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:866
const std::string unicode_bullet
Definition: constants.cpp:47
std::string path
Definition: filesystem.cpp:90
const bool & debug
Definition: game_config.cpp:92
unsigned int tile_size
Definition: game_config.cpp:52
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:172
void connect_signal_notify_modified(dispatcher &dispatcher, const signal_notification &signal)
Connects a signal handler for getting a notification upon modification.
Definition: dispatcher.cpp:203
void connect_signal_mouse_left_click(dispatcher &dispatcher, const signal &signal)
Connects a signal handler for a left mouse button click.
Definition: dispatcher.cpp:177
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:818
std::string span_color(const color_t &color, Args &&... data)
Returns a Pango formatting string using the provided color_t object.
Definition: markup.hpp:68
bool ci_search(const std::string &s1, const std::string &s2)
Definition: gettext.cpp:565
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:70
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