The Battle for Wesnoth  1.17.0-dev
campaign_selection.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2009 - 2021
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 "font/text_formatting.hpp"
23 #include "gui/widgets/button.hpp"
24 #include "gui/widgets/image.hpp"
25 #include "gui/widgets/listbox.hpp"
30 #include "gui/widgets/settings.hpp"
31 #include "gui/widgets/text_box.hpp"
35 #include "gui/widgets/window.hpp"
36 #include "lexical_cast.hpp"
37 #include "preferences/game.hpp"
38 
39 #include <functional>
40 #include "utils/irdya_datetime.hpp"
41 
42 namespace gui2::dialogs
43 {
44 
45 REGISTER_DIALOG(campaign_selection)
46 
47 void campaign_selection::campaign_selected()
48 {
49  tree_view& tree = find_widget<tree_view>(get_window(), "campaign_tree", false);
50  if(tree.empty()) {
51  return;
52  }
53 
54  assert(tree.selected_item());
55 
56  if(!tree.selected_item()->id().empty()) {
57  auto iter = std::find(page_ids_.begin(), page_ids_.end(), tree.selected_item()->id());
58 
59  const int choice = std::distance(page_ids_.begin(), iter);
60  if(iter == page_ids_.end()) {
61  return;
62  }
63 
64  multi_page& pages = find_widget<multi_page>(get_window(), "campaign_details", false);
65  pages.select_page(choice);
66 
67  engine_.set_current_level(choice);
68 
69  styled_widget& background = find_widget<styled_widget>(get_window(), "campaign_background", false);
70  background.set_label(engine_.current_level().data()["background"].str());
71 
72  // Rebuild difficulty menu
73  difficulties_.clear();
74 
75  auto& diff_menu = find_widget<menu_button>(get_window(), "difficulty_menu", false);
76 
77  const auto& diff_config = generate_difficulty_config(engine_.current_level().data());
78  diff_menu.set_active(diff_config.child_count("difficulty") > 1);
79 
80  if(!diff_config.empty()) {
81  std::vector<config> entry_list;
82  unsigned n = 0, selection = 0, max_n = diff_config.child_count("difficulty");
83 
84  for(const auto& cfg : diff_config.child_range("difficulty")) {
85  config entry;
86 
87  // FIXME: description may have markup that will display weird on the menu_button proper
88  entry["label"] = cfg["label"].str() + " (" + cfg["description"].str() + ")";
89  entry["image"] = cfg["image"].str("misc/blank-hex.png");
90 
91  if(preferences::is_campaign_completed(tree.selected_item()->id(), cfg["define"])) {
92  std::string laurel;
93 
94  if(n + 1 >= max_n) {
96  } else if(n == 0) {
98  } else {
100  }
101 
102  entry["image"] = laurel + "~BLIT(" + entry["image"] + ")";
103  }
104 
105  if(!cfg["description"].empty()) {
106  std::string desc;
107  if(cfg["auto_markup"].to_bool(true) == false) {
108  desc = cfg["description"].str();
109  } else {
110  //desc = "<small>";
111  if(!cfg["old_markup"].to_bool()) {
112  desc += font::span_color(font::GRAY_COLOR) + "(" + cfg["description"].str() + ")</span>";
113  } else {
114  desc += font::span_color(font::GRAY_COLOR) + cfg["description"].str() + "</span>";
115  }
116  //desc += "</small>";
117  }
118 
119  // Icons get displayed instead of the labels on the dropdown menu itself,
120  // so we want to prepend each label to its description here
121  desc = cfg["label"].str() + "\n" + desc;
122 
123  entry["details"] = std::move(desc);
124  }
125 
126  entry_list.emplace_back(std::move(entry));
127  difficulties_.emplace_back(cfg["define"].str());
128 
129  if(cfg["default"].to_bool(false)) {
130  selection = n;
131  }
132 
133  ++n;
134  }
135 
136  diff_menu.set_values(entry_list);
137  diff_menu.set_selected(selection);
138  }
139  }
140 }
141 
143 {
144  const std::size_t selection = find_widget<menu_button>(get_window(), "difficulty_menu", false).get_value();
145  current_difficulty_ = difficulties_.at(std::min(difficulties_.size() - 1, selection));
146 }
147 
149 {
150  using level_ptr = ng::create_engine::level_ptr;
151 
152  auto levels = engine_.get_levels_by_type_unfiltered(ng::level::TYPE::SP_CAMPAIGN);
153 
154  switch(order) {
155  case RANK: // Already sorted by rank
156  // This'll actually never happen, but who knows if that'll ever change...
157  if(!ascending) {
158  std::reverse(levels.begin(), levels.end());
159  }
160 
161  break;
162 
163  case DATE:
164  std::sort(levels.begin(), levels.end(), [ascending](const level_ptr& a, const level_ptr& b) {
165  auto cpn_a = std::dynamic_pointer_cast<ng::campaign>(a);
166  auto cpn_b = std::dynamic_pointer_cast<ng::campaign>(b);
167 
168  if(cpn_b == nullptr) {
169  return cpn_a != nullptr;
170  }
171 
172  if(cpn_a == nullptr) {
173  return false;
174  }
175 
176  return ascending
177  ? cpn_a->dates().first < cpn_b->dates().first
178  : cpn_a->dates().first > cpn_b->dates().first;
179  });
180 
181  break;
182 
183  case NAME:
184  std::sort(levels.begin(), levels.end(), [ascending](const level_ptr& a, const level_ptr& b) {
185  const int cmp = translation::icompare(a->name(), b->name());
186  return ascending ? cmp < 0 : cmp > 0;
187  });
188 
189  break;
190  }
191 
192  tree_view& tree = find_widget<tree_view>(get_window(), "campaign_tree", false);
193 
194  // Remember which campaign was selected...
195  std::string was_selected;
196  if(!tree.empty()) {
197  was_selected = tree.selected_item()->id();
198  tree.clear();
199  }
200 
201  boost::dynamic_bitset<> show_items;
202  show_items.resize(levels.size(), true);
203 
204  if(!last_search_words_.empty()) {
205  for(unsigned i = 0; i < levels.size(); ++i) {
206  bool found = false;
207  for(const auto& word : last_search_words_) {
208  found = translation::ci_search(levels[i]->name(), word) ||
209  translation::ci_search(levels[i]->data()["name"].t_str().base_str(), word) ||
210  translation::ci_search(levels[i]->description(), word) ||
211  translation::ci_search(levels[i]->data()["description"].t_str().base_str(), word) ||
212  translation::ci_search(levels[i]->data()["abbrev"], word) ||
213  translation::ci_search(levels[i]->data()["abbrev"].t_str().base_str(), word);
214 
215  if(!found) {
216  break;
217  }
218  }
219 
220  show_items[i] = found;
221  }
222  }
223 
224  bool exists_in_filtered_result = false;
225  for(unsigned i = 0; i < levels.size(); ++i) {
226  if(show_items[i]) {
227  add_campaign_to_tree(levels[i]->data());
228 
229  if (!exists_in_filtered_result) {
230  exists_in_filtered_result = levels[i]->id() == was_selected;
231  }
232  }
233  }
234 
235  if(!was_selected.empty() && exists_in_filtered_result) {
236  find_widget<tree_view_node>(get_window(), was_selected, false).select_node();
237  } else {
239  }
240 }
241 
243 {
244  static bool force = false;
245  if(force) {
246  return;
247  }
248 
249  if(current_sorting_ == order) {
251  currently_sorted_asc_ = false;
252  } else {
253  currently_sorted_asc_ = true;
255  }
256  } else if(current_sorting_ == RANK) {
257  currently_sorted_asc_ = true;
258  current_sorting_ = order;
259  } else {
260  currently_sorted_asc_ = true;
261  current_sorting_ = order;
262 
263  force = true;
264 
265  if(order == NAME) {
266  find_widget<toggle_button>(get_window(), "sort_time", false).set_value(0);
267  } else if(order == DATE) {
268  find_widget<toggle_button>(get_window(), "sort_name", false).set_value(0);
269  }
270 
271  force = false;
272  }
273 
275 }
276 
277 void campaign_selection::filter_text_changed(const std::string& text)
278 {
279  const std::vector<std::string> words = utils::split(text, ' ');
280 
281  if(words == last_search_words_) {
282  return;
283  }
284 
285  last_search_words_ = words;
287 }
288 
290 {
291  text_box* filter = find_widget<text_box>(&window, "filter_box", false, true);
293  std::bind(&campaign_selection::filter_text_changed, this, std::placeholders::_2));
294 
295  /***** Setup campaign tree. *****/
296  tree_view& tree = find_widget<tree_view>(&window, "campaign_tree", false);
297 
299  std::bind(&campaign_selection::campaign_selected, this));
300 
301  toggle_button& sort_name = find_widget<toggle_button>(&window, "sort_name", false);
302  toggle_button& sort_time = find_widget<toggle_button>(&window, "sort_time", false);
303 
306 
309 
310  window.keyboard_capture(filter);
311  window.add_to_keyboard_chain(&tree);
312 
313  /***** Setup campaign details. *****/
314  multi_page& pages = find_widget<multi_page>(&window, "campaign_details", false);
315 
316  for(const auto& level : engine_.get_levels_by_type_unfiltered(ng::level::TYPE::SP_CAMPAIGN)) {
317  const config& campaign = level->data();
318 
319  /*** Add tree item ***/
320  add_campaign_to_tree(campaign);
321 
322  /*** Add detail item ***/
323  std::map<std::string, string_map> data;
325 
326  item["label"] = campaign["description"];
327  item["use_markup"] = "true";
328 
329  if(!campaign["description_alignment"].empty()) {
330  item["text_alignment"] = campaign["description_alignment"];
331  }
332 
333  data.emplace("description", item);
334 
335  item["label"] = campaign["image"];
336  data.emplace("image", item);
337 
338  pages.add_page(data);
339  page_ids_.push_back(campaign["id"]);
340  }
341 
342  //
343  // Set up Mods selection dropdown
344  //
345  multimenu_button& mods_menu = find_widget<multimenu_button>(&window, "mods_menu", false);
346 
348  std::vector<config> mod_menu_values;
349  std::vector<std::string> enabled = engine_.active_mods();
350 
352  const bool active = std::find(enabled.begin(), enabled.end(), mod->id) != enabled.end();
353 
354  mod_menu_values.emplace_back("label", mod->name, "checkbox", active);
355 
356  mod_states_.push_back(active);
357  }
358 
359  mods_menu.set_values(mod_menu_values);
360  mods_menu.select_options(mod_states_);
361 
363  } else {
364  mods_menu.set_active(false);
365  mods_menu.set_label(_("active_modifications^None"));
366  }
367 
368  //
369  // Set up Difficulty dropdown
370  //
371  menu_button& diff_menu = find_widget<menu_button>(get_window(), "difficulty_menu", false);
372 
373  diff_menu.set_use_markup(true);
375 
377 }
378 
380 {
381  tree_view& tree = find_widget<tree_view>(get_window(), "campaign_tree", false);
382  std::map<std::string, string_map> data;
384 
385  item["label"] = campaign["icon"];
386  data.emplace("icon", item);
387 
388  item["label"] = campaign["name"];
389  data.emplace("name", item);
390 
391  // We completed the campaign! Calculate the appropriate victory laurel.
392  if(campaign["completed"].to_bool()) {
393  config::const_child_itors difficulties = campaign.child_range("difficulty");
394 
395  auto did_complete_at = [](const config& c) { return c["completed_at"].to_bool(); };
396 
397  // Check for non-completion on every difficulty save the first.
398  const bool only_first_completed = difficulties.size() > 1 &&
399  std::none_of(difficulties.begin() + 1, difficulties.end(), did_complete_at);
400 
401  /*
402  * Criteria:
403  *
404  * - Use the gold laurel (hardest) for campaigns with only one difficulty OR
405  * if out of two or more difficulties, the last one has been completed.
406  *
407  * - Use the bronze laurel (easiest) only if the first difficulty out of two
408  * or more has been completed.
409  *
410  * - Use the silver laurel otherwise.
411  */
412  if(!difficulties.empty() && did_complete_at(difficulties.back())) {
414  } else if(only_first_completed && did_complete_at(difficulties.front())) {
416  } else {
417  item["label"] = game_config::images::victory_laurel;
418  }
419 
420  data.emplace("victory", item);
421  }
422 
423  tree.add_node("campaign", data).set_id(campaign["id"]);
424 }
425 
427 {
428  tree_view& tree = find_widget<tree_view>(&window, "campaign_tree", false);
429 
430  if(tree.empty()) {
431  return;
432  }
433 
434  assert(tree.selected_item());
435  if(!tree.selected_item()->id().empty()) {
436  auto iter = std::find(page_ids_.begin(), page_ids_.end(), tree.selected_item()->id());
437  if(iter != page_ids_.end()) {
438  choice_ = std::distance(page_ids_.begin(), iter);
439  }
440  }
441 
442 
443  rng_mode_ = RNG_MODE(std::clamp<unsigned>(find_widget<menu_button>(&window, "rng_menu", false).get_value(), RNG_DEFAULT, RNG_BIASED));
444 
446 }
447 
449 {
450  boost::dynamic_bitset<> new_mod_states =
451  find_widget<multimenu_button>(get_window(), "mods_menu", false).get_toggle_states();
452 
453  // Get a mask of any mods that were toggled, regardless of new state
454  mod_states_ = mod_states_ ^ new_mod_states;
455 
456  for(unsigned i = 0; i < mod_states_.size(); i++) {
457  if(mod_states_[i]) {
458  engine_.toggle_mod(i);
459  }
460  }
461 
462  // Save the full toggle states for next time
463  mod_states_ = new_mod_states;
464 }
465 
466 } // namespace dialogs
void set_text_changed_callback(std::function< void(text_box_base *textbox, const std::string text)> cb)
Set the text_changed callback.
std::string victory_laurel
void add_campaign_to_tree(const config &campaign) const
std::shared_ptr< level > level_ptr
A menu_button is a styled_widget to choose an element from a list of elements.
Definition: menu_button.hpp:61
This shows the dialog which allows the user to choose which campaign to play.
const color_t GRAY_COLOR
grid & add_page(const string_map &item)
Adds single page to the grid.
Definition: multi_page.cpp:44
New lexcical_cast header.
#define a
const std::string & id() const
Definition: widget.cpp:110
bool is_campaign_completed(const std::string &campaign_id)
Definition: game.cpp:307
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:344
tree_view_node * selected_item()
Definition: tree_view.hpp:117
window * get_window() const
Returns a pointer to the dialog&#39;s window.
std::string victory_laurel_easy
A multimenu_button is a styled_widget to choose an element from a list of elements.
std::pair< irdya_date, irdya_date > dates() const
static std::string _(const char *str)
Definition: gettext.hpp:93
void campaign_selected()
Called when another campaign is selected.
std::vector< std::pair< const std::string *, const stats * > > levels
Stats (and name) for each scenario.
Definition: statistics.hpp:136
Class for a single line text area.
Definition: text_box.hpp:141
#define b
void filter_text_changed(const std::string &text)
virtual void set_label(const t_string &label)
std::string span_color(const color_t &color)
Returns a Pango formatting string using the provided color_t object.
void difficulty_selected()
Called when the difficulty selection changes.
bool toggle_mod(int index, bool force=false)
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:187
std::string victory_laurel_hardest
RNG_MODE rng_mode_
whether the player checked the "Deterministic" checkbox.
This file contains the settings handling of the widget library.
std::vector< level_ptr > get_levels_by_type_unfiltered(level::TYPE type) const
virtual void post_show(window &window) override
Actions to be taken after the window has been shown.
virtual void set_use_markup(bool use_markup)
A tree view is a control that holds several items of the same or different types. ...
Definition: tree_view.hpp:60
std::vector< std::string > & active_mods()
void sort_campaigns(CAMPAIGN_ORDER order, bool ascending)
std::vector< std::string > page_ids_
boost::iterator_range< const_child_iterator > const_child_itors
Definition: config.hpp:206
bool ci_search(const std::string &s1, const std::string &s2)
Definition: gettext.cpp:559
virtual void set_active(const bool active) override
See styled_widget::set_active.
std::size_t i
Definition: function.cpp:967
void toggle_sorting_selection(CAMPAIGN_ORDER order)
config generate_difficulty_config(const config &source)
Helper function to convert old difficulty markup.
virtual void pre_show(window &window) override
Actions to be taken before showing the window.
void select_page(const unsigned page, const bool select=true)
Selects a page.
Definition: multi_page.cpp:107
std::vector< std::string > difficulties_
std::map< std::string, t_string > string_map
Definition: widget.hpp:26
Base class for all visible items.
static int sort(lua_State *L)
Definition: ltablib.cpp:397
RNG_MODE
RNG mode selection values.
A multi page is a control that contains several &#39;pages&#39; of which only one is visible.
Definition: multi_page.hpp:49
std::vector< std::string > split(const config_attribute_value &val)
int icompare(const std::string &s1, const std::string &s2)
Case-insensitive lexicographical comparison.
Definition: gettext.cpp:516
bool empty() const
Definition: tree_view.cpp:111
void set_modifications(const std::vector< std::string > &value, bool mp)
Definition: game.cpp:734
void set_id(const std::string &id)
Definition: widget.cpp:98
void select_options(boost::dynamic_bitset<> states)
Set the options selected in the menu.
const std::vector< extras_metadata_ptr > & get_const_extras_by_type(const MP_EXTRA extra_type) const
void set_values(const std::vector<::config > &values)
Set the available menu options.
static void reverse(lua_State *L, StkId from, StkId to)
Definition: lapi.cpp:203
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:61
mock_char c
static map_location::DIRECTION n
base class of top level items, the only item which needs to store the final canvases to draw on...
Definition: window.hpp:65
Class for a toggle button.
tree_view_node & add_node(const std::string &id, const std::map< std::string, string_map > &data, const int index=-1)
Definition: tree_view.cpp:57
std::pair< std::string, unsigned > item
Definition: help_impl.hpp:410
std::vector< std::string > last_search_words_