The Battle for Wesnoth  1.19.25+dev
campaign_selection.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2009 - 2025
3  by Mark de Wever <koraq@xs4all.nl>
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 "filesystem.hpp"
21 #include "gui/widgets/button.hpp"
25 #include "gui/widgets/text_box.hpp"
30 #include "gui/widgets/window.hpp"
32 #include "serialization/markup.hpp"
33 #include "utils/irdya_datetime.hpp"
34 
35 #include <functional>
36 
37 namespace gui2::dialogs
38 {
39 
40 namespace
41 {
42  // These arbitary strings are used instead of a campaign id for the special pages
43  // which can appear in the right-hand pane. The data for those pages is loaded
44  // from the tree_view's WML via ids hardcoded in pre_show().
45  const std::string PAGE_ID_GET_ADDONS = "////addons////";
46  const std::string PAGE_ID_LANDING_PAGE = "////landing-page////";
47  const std::string PAGE_ID_MISSING_CAMPAIGNS = "////missing-campaign////";
48 
49  // The campaign_rng_mode preference stores a string mirroring the savefile's random_mode RNG.
50  const std::vector<std::string> rng_mode_prefs{"default", "deterministic", "biased"};
51 };
52 
53 REGISTER_DIALOG(campaign_selection)
54 
56  : modal_dialog(window_id())
57  , engine_(eng)
58  , choice_(-1)
59  , rng_mode_(RNG_DEFAULT)
60  , mod_states_()
61  , page_ids_()
62  , difficulties_()
63  , current_difficulty_()
64  , current_sorting_(RANK)
65  , currently_sorted_asc_(true)
66  , mod_ids_()
67 {
68  set_show_even_without_video(true);
69  set_allow_plugin_skip(false);
70 }
71 
73 {
74  tree_view& tree = find_widget<tree_view>("campaign_tree");
75  const std::string& campaign_id = tree.selected_item() ? tree.selected_item()->id() : PAGE_ID_LANDING_PAGE;
76  if(campaign_id.empty()) {
77  return;
78  }
79 
80  const bool can_proceed = campaign_id != PAGE_ID_MISSING_CAMPAIGNS && campaign_id != PAGE_ID_LANDING_PAGE;
81  const bool actual_campaign = can_proceed && campaign_id != PAGE_ID_GET_ADDONS;
82 
83  button& ok_button = find_widget<button>("proceed");
84  ok_button.set_active(can_proceed);
85  ok_button.set_label((campaign_id == PAGE_ID_GET_ADDONS) ? _("game^Get Add-ons") : _("game^Play"));
86 
87  auto iter = std::find(page_ids_.begin(), page_ids_.end(), campaign_id);
88  if(iter == page_ids_.end()) {
89  return;
90  }
91  const int choice = std::distance(page_ids_.begin(), iter);
92 
93  multi_page& pages = find_widget<multi_page>("campaign_details");
94  pages.select_page(choice);
95 
96  // The engine bounds-checks this and silently selects the first campaign if choice points to one
97  // of the non-campaign pages.
98  engine_.set_current_level(choice);
99 
100  styled_widget& background = find_widget<styled_widget>("campaign_background");
101  background.set_label(engine_.current_level().data()["background"].str());
102 
103  // Rebuild difficulty menu
104  difficulties_.clear();
105 
106  auto& diff_menu = find_widget<menu_button>("difficulty_menu");
107 
108  auto diff_config_range = engine_.current_level().data().child_range("difficulty");
109  const std::size_t difficulty_count = diff_config_range.size();
110 
111  if(diff_config_range.empty() || !actual_campaign) {
112  // Cosmetic bug: this leaves the difficulties of a previously-selected campaign visible
113  diff_menu.set_active(false);
114  return;
115  }
116 
117  std::vector<config> entry_list;
118  std::size_t n = 0, selection = 0;
119 
120  for(const auto& cfg : diff_config_range) {
121  config entry;
122 
123  // FIXME: description may have markup that will display weird on the menu_button proper
124  entry["label"] = cfg["label"].str() + " (" + cfg["description"].str() + ")";
125  entry["image"] = cfg["image"].str("misc/blank-hex.png");
126 
127  if(prefs::get().is_campaign_completed(campaign_id, cfg["define"])) {
128  std::string laurel;
129 
130  if(n + 1 >= difficulty_count) {
132  } else if(n == 0) {
134  } else {
136  }
137 
138  entry["image"] = laurel + "~BLIT(" + entry["image"].str() + ")";
139  }
140 
141  if(!cfg["description"].empty()) {
142  std::string desc;
143  if(cfg["auto_markup"].to_bool(true) == false) {
144  desc = cfg["description"].str();
145  } else {
146  desc = markup::span_color(font::GRAY_COLOR, "(", cfg["description"].str(), ")");
147  }
148 
149  // Icons get displayed instead of the labels on the dropdown menu itself,
150  // so we want to prepend each label to its description here
151  desc = cfg["label"].str() + "\n" + desc;
152 
153  entry["details"] = std::move(desc);
154  }
155 
156  entry_list.emplace_back(std::move(entry));
157  difficulties_.emplace_back(cfg["define"].str());
158 
159  if(cfg["default"].to_bool(false)) {
160  selection = n;
161  }
162 
163  ++n;
164  }
165 
166  diff_menu.set_active(true);
167  diff_menu.set_values(entry_list);
168  diff_menu.set_selected(selection);
169 }
170 
172 {
173  const std::size_t selection = find_widget<menu_button>("difficulty_menu").get_value();
174  current_difficulty_ = difficulties_.at(std::min(difficulties_.size() - 1, selection));
175 }
176 
178 {
179  using level_ptr = ng::create_engine::level_ptr;
180 
181  auto levels = engine_.get_levels_by_type_unfiltered(level_type::type::sp_campaign);
182 
183  switch(order) {
184  case RANK: // Already sorted by rank
185  // This'll actually never happen, but who knows if that'll ever change...
186  if(!ascending) {
187  std::reverse(levels.begin(), levels.end());
188  }
189 
190  break;
191 
192  case DATE:
193  std::sort(levels.begin(), levels.end(), [ascending](const level_ptr& a, const level_ptr& b) {
194  auto cpn_a = std::dynamic_pointer_cast<ng::campaign>(a);
195  auto cpn_b = std::dynamic_pointer_cast<ng::campaign>(b);
196 
197  if(cpn_b == nullptr) {
198  return cpn_a != nullptr;
199  }
200 
201  if(cpn_a == nullptr) {
202  return false;
203  }
204 
205  return ascending
206  ? cpn_a->dates().first < cpn_b->dates().first
207  : cpn_a->dates().first > cpn_b->dates().first;
208  });
209 
210  break;
211 
212  case NAME:
213  std::sort(levels.begin(), levels.end(), [ascending](const level_ptr& a, const level_ptr& b) {
214  const int cmp = translation::icompare(a->name(), b->name());
215  return ascending ? cmp < 0 : cmp > 0;
216  });
217 
218  break;
219  }
220 
221  tree_view& tree = find_widget<tree_view>("campaign_tree");
222 
223  // Remember which campaign was selected...
224  std::string was_selected;
225  if(tree.selected_item()) {
226  was_selected = tree.selected_item()->id();
227  }
228  tree.clear();
229 
230  boost::dynamic_bitset<> show_items;
231  show_items.resize(levels.size(), true);
232 
233  if(!last_search_words_.empty()) {
234  for(unsigned i = 0; i < levels.size(); ++i) {
235  bool found = false;
236  for(const auto& word : last_search_words_) {
237  found = translation::ci_search(levels[i]->name(), word) ||
238  translation::ci_search(levels[i]->data()["name"].t_str().base_str(), word) ||
239  translation::ci_search(levels[i]->description(), word) ||
240  translation::ci_search(levels[i]->data()["description"].t_str().base_str(), word) ||
241  translation::ci_search(levels[i]->data()["abbrev"], word) ||
242  translation::ci_search(levels[i]->data()["abbrev"].t_str().base_str(), word);
243 
244  if(!found) {
245  break;
246  }
247  }
248 
249  show_items[i] = found;
250  }
251  }
252 
253  // List of which options has been selected in the completion filter multimenu_button
254  boost::dynamic_bitset<> filter_comp_options = find_widget<multimenu_button>("filter_completion").get_toggle_states();
255 
256  bool exists_in_filtered_result = false;
257  for(unsigned i = 0; i < levels.size(); ++i) {
258  bool completed = prefs::get().is_campaign_completed(levels[i]->data()["id"]);
259  config::const_child_itors difficulties = levels[i]->data().child_range("difficulty");
260  auto did_complete_at = [](const config& c) { return c["completed_at"].to_bool(); };
261 
262  // Check for non-completion on every difficulty save the first.
263  const bool only_first_completed = difficulties.size() > 1 &&
264  std::none_of(difficulties.begin() + 1, difficulties.end(), did_complete_at);
265  const bool completed_easy = only_first_completed && did_complete_at(difficulties.front());
266  const bool completed_hardest = !difficulties.empty() && did_complete_at(difficulties.back());
267  const bool completed_mid = completed && !completed_hardest && !completed_easy;
268 
269  if( show_items[i] && (
270  ( (!completed) && filter_comp_options[0] ) // Selects all campaigns not finished by player
271  || ( completed && filter_comp_options[4] ) // Selects all campaigns finished by player
272  || ( completed_hardest && filter_comp_options[3] ) // Selects campaigns completed in hardest difficulty
273  || ( completed_easy && filter_comp_options[1] ) // Selects campaigns completed in easiest difficulty
274  || ( completed_mid && filter_comp_options[2]) // Selects campaigns completed in any other difficulty
275  )) {
276  add_campaign_to_tree(levels[i]->data());
277  if (!exists_in_filtered_result) {
278  exists_in_filtered_result = levels[i]->id() == was_selected;
279  }
280  }
281  }
282 
283  if(!was_selected.empty() && exists_in_filtered_result) {
284  find_widget<tree_view_node>(was_selected).select_node();
285  } else {
286  campaign_selected();
287  }
288 }
289 
291 {
292  static bool force = false;
293  if(force) {
294  return;
295  }
296 
297  if(current_sorting_ == order) {
299  currently_sorted_asc_ = false;
300  } else {
301  currently_sorted_asc_ = true;
303  }
304  } else if(current_sorting_ == RANK) {
305  currently_sorted_asc_ = true;
306  current_sorting_ = order;
307  } else {
308  currently_sorted_asc_ = true;
309  current_sorting_ = order;
310 
311  force = true;
312 
313  if(order == NAME) {
314  find_widget<toggle_button>("sort_time").set_value(0);
315  } else if(order == DATE) {
316  find_widget<toggle_button>("sort_name").set_value(0);
317  }
318 
319  force = false;
320  }
321 
323 }
324 
325 void campaign_selection::filter_text_changed(const std::string& text)
326 {
327  const std::vector<std::string> words = utils::split(text, ' ');
328 
329  if(words == last_search_words_) {
330  return;
331  }
332 
333  last_search_words_ = words;
335 }
336 
338 {
339  text_box* filter = find_widget<text_box>("filter_box", false, true);
340  filter->on_modified([this](const auto& box) { filter_text_changed(box.text()); });
341 
342  /***** Setup campaign tree. *****/
343  tree_view& tree = find_widget<tree_view>("campaign_tree");
344 
346  std::bind(&campaign_selection::campaign_selected, this));
347 
348  toggle_button& sort_name = find_widget<toggle_button>("sort_name");
349  toggle_button& sort_time = find_widget<toggle_button>("sort_time");
350 
353 
356 
357  connect_signal_mouse_left_click(find_widget<button>("proceed"),
358  std::bind(&campaign_selection::proceed, this));
359 
360 #ifdef __IPHONEOS__
361  // On iOS, opening the campaign browser should not immediately summon the
362  // software keyboard just because the optional filter field exists.
363  keyboard_capture(&tree);
364 #else
366 #endif
367  add_to_keyboard_chain(&tree);
368 
369  /***** Setup campaign details. *****/
370  multi_page& pages = find_widget<multi_page>("campaign_details");
371 
372  // Setup completion filter
373  multimenu_button& filter_comp = find_widget<multimenu_button>("filter_completion");
374  connect_signal_notify_modified(filter_comp,
375  std::bind(&campaign_selection::sort_campaigns, this, RANK, 1));
376  for (unsigned j = 0; j < filter_comp.num_options(); j++) {
377  filter_comp.select_option(j);
378  }
379 
380  // Add campaigns to the list
381  for(const auto& level : engine_.get_levels_by_type_unfiltered(level_type::type::sp_campaign)) {
382  const config& campaign = level->data();
383 
384  /*** Add tree item ***/
385  add_campaign_to_tree(campaign);
386 
387  /*** Add detail item ***/
388  pages.add_page({
389  {"description", {
390  {"label", campaign["description"].t_str()},
391  {"text_alignment", campaign["description_alignment"].str("left")},
392  {"use_markup", "true"}
393  }},
394  {"image", {
395  {"label", campaign["image"].str()}
396  }}
397  });
398  page_ids_.push_back(campaign["id"]);
399  }
400 
401  //
402  // Special-purpose pages that appear as if they were campaigns.
403  //
404 
405  // The intro page is shown if nothing is selected in the tree_view, it isn't in the tree itself
406  pages.add_page("no_campaign_selected", -1, widget_data{});
407  page_ids_.push_back(PAGE_ID_LANDING_PAGE);
408 
409  // Addon Manager link
410  config addons;
411  addons["icon"] = "icons/icon-game.png~BLIT(icons/icon-addon-publish.png)";
412  addons["name"] = _("More campaigns...");
413  addons["completed"] = false;
414  addons["id"] = PAGE_ID_GET_ADDONS;
415 
416  add_campaign_to_tree(addons);
417 
418  pages.add_page("go_download_more_stuff", -1, widget_data{});
419  page_ids_.push_back(PAGE_ID_GET_ADDONS);
420 
421  // The data directory has few (or none) of the mainline campaigns
422  std::vector<std::string> dirs;
423  filesystem::get_files_in_dir(game_config::path + "/data/campaigns", nullptr, &dirs);
424  if(dirs.size() <= 15) {
425  config missing;
426  missing["icon"] = "units/unknown-unit.png";
427  missing["name"] = _("Missing Campaigns");
428  missing["completed"] = false;
429  missing["id"] = PAGE_ID_MISSING_CAMPAIGNS;
430 
432 
433  pages.add_page("missing_campaign_warning", -1, widget_data{});
434  page_ids_.push_back(PAGE_ID_MISSING_CAMPAIGNS);
435  }
436 
437  //
438  // Set up Mods selection dropdown
439  //
440  multimenu_button& mods_menu = find_widget<multimenu_button>("mods_menu");
441 
443  std::vector<config> mod_menu_values;
444  std::vector<std::string> enabled = engine_.active_mods();
445 
447  const bool active = std::find(enabled.begin(), enabled.end(), mod->id) != enabled.end();
448 
449  mod_menu_values.emplace_back("label", mod->name, "tooltip", mod->description, "checkbox", active);
450 
451  mod_states_.push_back(active);
452  mod_ids_.emplace_back(mod->id);
453  }
454 
455  mods_menu.set_values(mod_menu_values);
456  mods_menu.select_options(mod_states_);
457 
459  } else {
460  mods_menu.set_active(false);
461  mods_menu.set_label(_("active_modifications^None"));
462  }
463 
464  //
465  // Set up RNG mode dropdown
466  //
467  // New installs default to "Reduced RNG"; existing installs restore the last selection.
468  menu_button& rng_menu = find_widget<menu_button>("rng_menu");
469  auto rng_it = std::find(rng_mode_prefs.begin(), rng_mode_prefs.end(), prefs::get().campaign_rng_mode());
470  rng_menu.set_selected(rng_it != rng_mode_prefs.end()
471  ? static_cast<unsigned>(std::distance(rng_mode_prefs.begin(), rng_it))
472  : static_cast<unsigned>(RNG_DEFAULT));
473 
474  //
475  // Set up Difficulty dropdown
476  //
477  menu_button& diff_menu = find_widget<menu_button>("difficulty_menu");
478 
479  diff_menu.set_use_markup(true);
481 
483 
484  plugins_context_.reset(new plugins_context("Campaign Selection"));
485  plugins_context_->set_callback("create", [this](const config&) { set_retval(retval::OK); }, false);
486  plugins_context_->set_callback("quit", [this](const config&) { set_retval(retval::CANCEL); }, false);
487 
488  plugins_context_->set_accessor("find_level", [this](const config& cfg) {
489  const std::string id = cfg["id"].str();
490  auto result = engine_.find_level_by_id(id);
491  return config {
492  "index", result.second,
493  "type", level_type::get_string(result.first),
494  };
495  });
496 
497  plugins_context_->set_accessor_int("find_mod", [this](const config& cfg) {
499  });
500 
501  plugins_context_->set_callback("select_level", [this](const config& cfg) {
502  choice_ = cfg["index"].to_int();
504  }, true);
505 }
506 
508 {
509  // We completed the campaign! Calculate the appropriate victory laurel.
510  const auto get_laurel = [&campaign] {
511  if(!campaign["completed"].to_bool()) {
512  return std::string{};
513  }
514 
515  config::const_child_itors difficulties = campaign.child_range("difficulty");
516  auto did_complete_at = [](const config& c) { return c["completed_at"].to_bool(); };
517 
518  // Check for non-completion on every difficulty save the first.
519  const bool only_first_completed = difficulties.size() > 1 &&
520  std::none_of(difficulties.begin() + 1, difficulties.end(), did_complete_at);
521 
522  /*
523  * Criteria:
524  *
525  * - Use the gold laurel (hardest) for campaigns with only one difficulty OR
526  * if out of two or more difficulties, the last one has been completed.
527  *
528  * - Use the bronze laurel (easiest) only if the first difficulty out of two
529  * or more has been completed.
530  *
531  * - Use the silver laurel otherwise.
532  */
533  if(!difficulties.empty() && did_complete_at(difficulties.back())) {
535  } else if(only_first_completed && did_complete_at(difficulties.front())) {
537  } else {
539  }
540  };
541 
542  auto& node = find_widget<tree_view>("campaign_tree")
543  .add_node("campaign", {
544  {"icon", {
545  {"label", campaign["icon"].str()}
546  }},
547  {"name", {
548  {"label", campaign["name"].t_str()}
549  }},
550  {"victory", {
551  {"label", get_laurel()}
552  }}
553  });
554 
555  node.set_id(campaign["id"]);
557  node.find_widget<toggle_panel>("tree_view_node_label"),
558  std::bind(&campaign_selection::proceed, this)
559  );
560 }
561 
563 {
564  tree_view& tree = find_widget<tree_view>("campaign_tree");
565 
566  if(tree.empty() || !tree.selected_item()) {
567  return;
568  }
569 
570  const std::string& campaign_id = tree.selected_item()->id();
571  if(!campaign_id.empty()) {
572  if (campaign_id == PAGE_ID_GET_ADDONS) {
574  } else {
575  auto iter = std::find(page_ids_.begin(), page_ids_.end(), campaign_id);
576  if(iter != page_ids_.end()) {
577  choice_ = std::distance(page_ids_.begin(), iter);
578  }
580  }
581  }
582 
583 
584  rng_mode_ = RNG_MODE(std::clamp<unsigned>(find_widget<menu_button>("rng_menu").get_value(), RNG_DEFAULT, RNG_BIASED));
585 
586  prefs::get().set_campaign_rng_mode(rng_mode_prefs[rng_mode_]);
587 
589 }
590 
592 {
593  boost::dynamic_bitset<> new_mod_states =
594  find_widget<multimenu_button>("mods_menu").get_toggle_states();
595 
596  // Get a mask of any mods that were toggled, regardless of new state
597  mod_states_ = mod_states_ ^ new_mod_states;
598 
599  for(unsigned i = 0; i < mod_states_.size(); i++) {
600  if(mod_states_[i]) {
602  }
603  }
604 
605  // Save the full toggle states for next time
606  mod_states_ = new_mod_states;
607 }
608 
609 } // namespace dialogs
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:157
child_itors child_range(std::string_view key)
Definition: config.cpp:268
boost::iterator_range< const_child_iterator > const_child_itors
Definition: config.hpp:281
Simple push button.
Definition: button.hpp:36
virtual void set_active(const bool active) override
See styled_widget::set_active.
Definition: button.cpp:64
void toggle_sorting_selection(CAMPAIGN_ORDER order)
RNG_MODE rng_mode_
whether the player checked the "Deterministic" checkbox.
void difficulty_selected()
Called when the difficulty selection changes.
std::vector< std::string > mod_ids_
std::vector< std::string > difficulties_
RNG_MODE
RNG mode selection values.
virtual void pre_show() override
Actions to be taken before showing the window.
void sort_campaigns(CAMPAIGN_ORDER order, bool ascending)
void add_campaign_to_tree(const config &campaign)
void filter_text_changed(const std::string &text)
void campaign_selected()
Called when an item in the left-hand pane (the tree view) is selected.
std::vector< std::string > last_search_words_
std::vector< std::string > page_ids_
Abstract base class for all modal dialogs.
std::unique_ptr< plugins_context > plugins_context_
void set_selected(unsigned selected, bool fire_event=true)
grid & add_page(const widget_item &item)
Adds single page to the grid.
Definition: multi_page.cpp:58
void select_page(const unsigned page, const bool select=true)
Selects a page.
Definition: multi_page.cpp:119
void select_options(const boost::dynamic_bitset<> &states)
Set the options selected in the menu.
void set_values(const std::vector<::config > &values)
Set the available menu options.
unsigned num_options()
Get the number of options available in the menu.
virtual void set_active(const bool active) override
See styled_widget::set_active.
void select_option(const unsigned option, const bool selected=true)
Select an option in the menu.
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
bool empty() const
Definition: tree_view.cpp:99
tree_view_node * selected_item()
Definition: tree_view.hpp:108
const std::string & id() const
Definition: widget.cpp:110
void set_retval(const int retval, const bool close_window=true)
Sets there return value of the window.
Definition: window.hpp:387
void keyboard_capture(widget *widget)
Definition: window.cpp:1197
void add_to_keyboard_chain(widget *widget)
Adds the widget to the keyboard chain.
Definition: window.cpp:1211
int find_extra_by_id(const MP_EXTRA extra_type, const std::string &id) const
bool toggle_mod(const std::string &id, bool force=false)
std::vector< std::string > & active_mods()
const std::vector< extras_metadata_ptr > & get_const_extras_by_type(const MP_EXTRA extra_type) const
std::shared_ptr< level > level_ptr
std::pair< level_type::type, int > find_level_by_id(const std::string &id) const
std::vector< level_ptr > get_levels_by_type_unfiltered(level_type::type type) const
void set_current_level(const std::size_t index)
level & current_level() const
const config & data() const
static prefs & get()
bool is_campaign_completed(const std::string &campaign_id)
void set_modifications(const std::vector< std::string > &value, bool mp=true)
const config * cfg
Declarations for File-IO.
std::size_t i
Definition: function.cpp:1031
static std::string _(const char *str)
Definition: gettext.hpp:100
This file contains the window object, this object is a top level container which has the event manage...
void get_files_in_dir(const std::string &dir, std::vector< std::string > *files, std::vector< std::string > *dirs, name_mode mode, filter_mode filter, reorder_mode reorder, file_tree_checksum *checksum)
Get a list of all files and/or directories in a given directory.
Definition: filesystem.cpp:466
const color_t GRAY_COLOR
std::string victory_laurel_hardest
std::string victory_laurel
std::string victory_laurel_easy
std::string path
Definition: filesystem.cpp:106
REGISTER_DIALOG(editor_edit_unit)
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
void connect_signal_mouse_left_double_click(dispatcher &dispatcher, const signal &signal)
Connects a signal handler for a left mouse button double click.
Definition: dispatcher.cpp:184
std::map< std::string, widget_item > widget_data
Definition: widget.hpp:36
@ OK
Dialog was closed with the OK button.
Definition: retval.hpp:35
@ CANCEL
Dialog was closed with the CANCEL button.
Definition: retval.hpp:38
std::string span_color(const color_t &color, Args &&... data)
Applies Pango markup to the input specifying its display color.
Definition: markup.hpp:110
bool ci_search(const std::string &s1, const std::string &s2)
Case-insensitive search.
Definition: gettext.cpp:559
constexpr auto reverse
Definition: ranges.hpp:44
constexpr auto filter
Definition: ranges.hpp:42
std::vector< std::string > split(const config_attribute_value &val)
auto * find(Container &container, const Value &value)
Convenience wrapper for using find on a container without needing to comare to end()
Definition: general.hpp:141
std::string_view data
Definition: picture.cpp:188
static std::string get_string(enum_type key)
Converts a enum to its string equivalent.
Definition: enum_base.hpp:46
mock_char c
static map_location::direction n
#define b