The Battle for Wesnoth  1.17.17+dev
campaign_selection.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2009 - 2023
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>(this, "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>(this, "campaign_details", false);
65  pages.select_page(choice);
66 
67  engine_.set_current_level(choice);
68 
69  styled_widget& background = find_widget<styled_widget>(this, "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>(this, "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>(this, "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(level_type::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>(this, "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>(this, was_selected, false).select_node();
237  } else {
238  campaign_selected();
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>(this, "sort_time", false).set_value(0);
267  } else if(order == DATE) {
268  find_widget<toggle_button>(this, "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);
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(level_type::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 ***/
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>(this, "difficulty_menu", false);
372 
373  diff_menu.set_use_markup(true);
375 
377 }
378 
380 {
381  tree_view& tree = find_widget<tree_view>(this, "campaign_tree", false);
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 {
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>(this, "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]) {
459  }
460  }
461 
462  // Save the full toggle states for next time
463  mod_states_ = new_mod_states;
464 }
465 
466 } // namespace dialogs
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:161
child_itors child_range(config_key_type key)
Definition: config.cpp:277
boost::iterator_range< const_child_iterator > const_child_itors
Definition: config.hpp:285
This shows the dialog which allows the user to choose which campaign to play.
void toggle_sorting_selection(CAMPAIGN_ORDER order)
RNG_MODE rng_mode_
whether the player checked the "Deterministic" checkbox.
virtual void pre_show(window &window) override
Actions to be taken before showing the window.
void difficulty_selected()
Called when the difficulty selection changes.
std::vector< std::string > difficulties_
RNG_MODE
RNG mode selection values.
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 another campaign is selected.
virtual void post_show(window &window) override
Actions to be taken after the window has been shown.
std::vector< std::string > last_search_words_
std::vector< std::string > page_ids_
A menu_button is a styled_widget to choose an element from a list of elements.
Definition: menu_button.hpp:62
A multi page is a control that contains several 'pages' of which only one is visible.
Definition: multi_page.hpp:50
grid & add_page(const widget_item &item)
Adds single page to the grid.
Definition: multi_page.cpp:44
void select_page(const unsigned page, const bool select=true)
Selects a page.
Definition: multi_page.cpp:105
A multimenu_button is a styled_widget to choose an element from a list of elements.
void select_options(boost::dynamic_bitset<> states)
Set the options selected in the menu.
void set_values(const std::vector<::config > &values)
Set the available menu options.
virtual void set_active(const bool active) override
See styled_widget::set_active.
Base class for all visible items.
virtual void set_label(const t_string &label)
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.
Class for a single line text area.
Definition: text_box.hpp:142
Class for a toggle button.
A tree view is a control that holds several items of the same or different types.
Definition: tree_view.hpp:61
bool empty() const
Definition: tree_view.cpp:100
tree_view_node & add_node(const std::string &id, const widget_data &data, const int index=-1)
Definition: tree_view.cpp:57
tree_view_node * selected_item()
Definition: tree_view.hpp:110
void set_id(const std::string &id)
Definition: widget.cpp:99
const std::string & id() const
Definition: widget.cpp:111
base class of top level items, the only item which needs to store the final canvases to draw on.
Definition: window.hpp:67
void keyboard_capture(widget *widget)
Definition: window.cpp:1130
void add_to_keyboard_chain(widget *widget)
Adds the widget to the keyboard chain.
Definition: window.cpp:1136
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::vector< level_ptr > get_levels_by_type_unfiltered(level_type::type type) const
bool toggle_mod(int index, bool force=false)
std::size_t i
Definition: function.cpp:968
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...
New lexcical_cast header.
#define REGISTER_DIALOG(window_id)
Wrapper for REGISTER_DIALOG2.
const color_t GRAY_COLOR
std::string span_color(const color_t &color)
Returns a Pango formatting string using the provided color_t object.
std::string victory_laurel_hardest
std::string victory_laurel
std::string victory_laurel_easy
config generate_difficulty_config(const config &source)
Helper function to convert old difficulty markup.
void connect_signal_notify_modified(dispatcher &dispatcher, const signal_notification &signal)
Connects a signal handler for getting a notification upon modification.
Definition: dispatcher.cpp:205
std::map< std::string, widget_item > widget_data
Definition: widget.hpp:35
std::map< std::string, t_string > widget_item
Definition: widget.hpp:32
std::pair< std::string, unsigned > item
Definition: help_impl.hpp:414
void set_modifications(const std::vector< std::string > &value, bool mp)
Definition: game.cpp:720
bool is_campaign_completed(const std::string &campaign_id)
Definition: game.cpp:293
bool ci_search(const std::string &s1, const std::string &s2)
Definition: gettext.cpp:563
std::vector< std::string > split(const config_attribute_value &val)
std::string_view data
Definition: picture.cpp:199
This file contains the settings handling of the widget library.
mock_char c
static map_location::DIRECTION n
#define a
#define b