The Battle for Wesnoth  1.19.7+dev
unit_recall.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2016 - 2024
3  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
4 
5  This program is free software; you can redistribute it and/or modify
6  it under the terms of the GNU General Public License as published by
7  the Free Software Foundation; either version 2 of the License, or
8  (at your option) any later version.
9  This program is distributed in the hope that it will be useful,
10  but WITHOUT ANY WARRANTY.
11 
12  See the COPYING file for more details.
13 */
14 
15 #define GETTEXT_DOMAIN "wesnoth-lib"
16 
18 
19 #include "serialization/markup.hpp"
21 #include "gui/dialogs/message.hpp"
22 #include "gui/widgets/listbox.hpp"
23 #include "gui/widgets/button.hpp"
24 #include "gui/widgets/image.hpp"
25 #include "gui/widgets/label.hpp"
26 #include "gui/widgets/text_box.hpp"
28 #include "gui/widgets/window.hpp"
29 #include "help/help.hpp"
30 #include "game_board.hpp"
31 #include "gettext.hpp"
32 #include "replay_helper.hpp"
33 #include "play_controller.hpp"
34 #include "resources.hpp"
35 #include "synced_context.hpp"
36 #include "team.hpp"
37 #include "units/unit.hpp"
38 #include "units/ptr.hpp"
39 #include "units/types.hpp"
40 #include "utils/ci_searcher.hpp"
41 #include <functional>
42 #include "whiteboard/manager.hpp"
43 
44 
45 static lg::log_domain log_display("display");
46 #define LOG_DP LOG_STREAM(info, log_display)
47 
48 namespace gui2::dialogs
49 {
50 
51 static std::pair sort_default{ std::string{"sort_2"}, sort_order::type::descending };
52 static utils::optional<decltype(sort_default)> sort_last;
53 
55 
56 unit_recall::unit_recall(std::vector<unit_const_ptr>& recall_list, team& team)
57  : modal_dialog(window_id())
58  , recall_list_(recall_list)
59  , team_(team)
60  , selected_index_()
61  , filter_options_()
62  , last_words_()
63 {
64 }
65 
66 template<typename T>
67 static void dump_recall_list_to_console(const T& units)
68 {
69  log_scope2(log_display, "dump_recall_list_to_console()")
70 
71  LOG_DP << "size: " << units.size();
72 
73  std::size_t idx = 0;
74  for(const auto& u_ptr : units) {
75  LOG_DP << "\tunit[" << (idx++) << "]: " << u_ptr->id() << " name = '" << u_ptr->name() << "'";
76  }
77 }
78 
79 static const color_t inactive_row_color(0x96, 0x96, 0x96);
80 
81 static const inline std::string maybe_inactive(const std::string& str, bool active)
82 {
83  if(active)
84  return str;
85  else
87 }
88 
89 static std::string format_level_string(const int level, bool recallable)
90 {
91  if(!recallable) {
92  // Same logic as when recallable, but always in inactive_row_color.
94  (level < 2 ? std::to_string(level) : markup::bold(level)));
95  } else if(level < 1) {
97  } else if(level == 1) {
98  return std::to_string(level);
99  } else if(level == 2) {
100  return markup::bold(level);
101  } else {
102  return markup::span_color("#ffffff", markup::bold(level));
103  }
104 }
105 
106 static std::string format_cost_string(int unit_recall_cost, const int team_recall_cost)
107 {
108  std::stringstream str;
109 
110  if(unit_recall_cost < 0) {
111  unit_recall_cost = team_recall_cost;
112  }
113 
114  if(unit_recall_cost > team_recall_cost) {
115  str << markup::span_color("#ff0000", unit_recall_cost);
116  } else if(unit_recall_cost == team_recall_cost) {
117  str << unit_recall_cost;
118  } else if(unit_recall_cost < team_recall_cost) {
119  str << markup::span_color("#00ff00", unit_recall_cost);
120  }
121 
122  return str.str();
123 }
124 
125 static std::string get_title_suffix(int side_num)
126 {
127  if(!resources::gameboard) {
128  return "";
129  }
130 
131  unit_map& units = resources::gameboard->units();
132 
133  int controlled_recruiters = 0;
134  for(const auto& team : resources::gameboard->teams()) {
135  if(team.is_local_human() && !team.recruits().empty() && units.find_leader(team.side()) !=units.end()) {
136  ++controlled_recruiters;
137  }
138  }
139 
140  std::stringstream msg;
141  if(controlled_recruiters >= 2) {
143  if(leader != resources::gameboard->units().end() && !leader->name().empty()) {
144  msg << " (" << leader->name(); msg << ")";
145  }
146  }
147 
148  return msg.str();
149 }
150 
152 {
153  label& title = find_widget<label>("title", true);
154  title.set_label(title.get_label() + get_title_suffix(team_.side()));
155 
157  = find_widget<text_box>("filter_box", false, true);
158 
159  filter->on_modified([this](const auto& box) { filter_text_changed(box.text()); });
160 
161  listbox& list = find_widget<listbox>("recall_list");
162 
164 
165  list.clear();
166 
168  add_to_keyboard_chain(&list);
169 
171  find_widget<button>("rename"),
172  std::bind(&unit_recall::rename_unit, this));
173 
175  find_widget<button>("dismiss"),
176  std::bind(&unit_recall::dismiss_unit, this));
177 
179  find_widget<button>("show_help"),
180  std::bind(&unit_recall::show_help, this));
181 
182  for(const unit_const_ptr& unit : recall_list_) {
183  widget_data row_data;
184  widget_item column;
185 
186  std::string mods = unit->image_mods();
187 
188  int wb_gold = 0;
190  if(const std::shared_ptr<wb::manager>& whiteb = resources::controller->get_whiteboard()) {
191  wb::future_map future; // So gold takes into account planned spending
192  wb_gold = whiteb->get_spent_gold_for(team_.side());
193  }
194  }
195 
196  // Note: Our callers apply [filter_recall], but leave it to us
197  // to apply cost-based filtering.
198  const int recall_cost = (unit->recall_cost() > -1 ? unit->recall_cost() : team_.recall_cost());
199  const bool recallable = (recall_cost <= team_.gold() - wb_gold);
200 
201  if(unit->can_recruit()) {
202  mods += "~BLIT(" + unit::leader_crown() + ")";
203  }
204 
205  for(const std::string& overlay : unit->overlays()) {
206  mods += "~BLIT(" + overlay + ")";
207  }
208 
209  if(!recallable) {
210  mods += "~GS()";
211 
212  // Just set the tooltip on every single element in this row.
213  if(wb_gold > 0)
214  column["tooltip"] = _("This unit cannot be recalled because you will not have enough gold at this point in your plan.");
215  else
216  column["tooltip"] = _("This unit cannot be recalled because you do not have enough gold.");
217  }
218 
219  column["use_markup"] = "true";
220 
221  column["label"] = unit->absolute_image() + mods;
222  row_data.emplace("unit_image", column);
223 
224  column["label"] = maybe_inactive(unit->type_name(), recallable);
225  row_data.emplace("unit_type", column);
226 
227  // gold_icon is handled below
228 
229  column["label"] =
230  recallable
232  : maybe_inactive(std::to_string(recall_cost), recallable);
233  row_data.emplace("unit_recall_cost", column);
234 
235  const std::string& name = !unit->name().empty() ? unit->name().str() : font::unicode_en_dash;
236  column["label"] = maybe_inactive(name, recallable);
237  row_data.emplace("unit_name", column);
238 
239  column["label"] = format_level_string(unit->level(), recallable);
240  row_data.emplace("unit_level", column);
241 
242  std::stringstream exp_str;
243  if(unit->can_advance()) {
244  exp_str << unit->experience() << "/" << unit->max_experience();
245  } else {
246  exp_str << font::unicode_en_dash;
247  }
248 
249  column["label"] = markup::span_color(recallable ? unit->xp_color() : inactive_row_color, exp_str.str());
250  row_data.emplace("unit_experience", column);
251 
252  // Since the table widgets use heavy formatting, we save a bare copy
253  // of certain options to filter on.
254  std::string filter_text = unit->type_name() + " " + name + " " + std::to_string(unit->level())
256  if(const auto* race = unit->race()) {
257  filter_text += " " + race->name(unit->gender()) + " " + race->plural_name();
258  }
259 
260  if(recallable) {
261  // This is to allow filtering for recallable units by typing "vvv" in the search box.
262  // That's intended to be easy to type and unlikely to match unit or type names.
263  //
264  // TODO: document this. (Also, implement a "Hide non-recallable units" checkbox.)
265  filter_text += " " + std::string("vvv");
266  }
267 
268  std::string traits;
269  for(const std::string& trait : unit->trait_names()) {
270  traits += (traits.empty() ? "" : "\n") + trait;
271  filter_text += " " + trait;
272  }
273 
274  column["label"] = maybe_inactive(
275  !traits.empty() ? traits : font::unicode_en_dash,
276  recallable);
277  row_data.emplace("unit_traits", column);
278 
279  filter_options_.push_back(filter_text);
280  grid& grid = list.add_row(row_data);
281  if(!recallable) {
282  image *gold_icon = dynamic_cast<image*>(grid.find("gold_icon", false));
283  assert(gold_icon);
284  gold_icon->set_image(gold_icon->get_image() + "~GS()");
285  }
286  }
287 
288  list.set_sorters(
289  [this](const std::size_t i) { return recall_list_[i]->type_name(); },
290  [this](const std::size_t i) { return recall_list_[i]->name(); },
291  [this](const std::size_t i) {
292  const unit& u = *recall_list_[i];
293  return std::tuple(u.level(), -static_cast<int>(u.experience_to_advance()));
294  },
295  [this](const std::size_t i) { return recall_list_[i]->experience(); },
296  [this](const std::size_t i) {
297  return !recall_list_[i]->trait_names().empty() ? recall_list_[i]->trait_names().front() : t_string();
298  }
299  );
300 
301  const auto [sorter_id, order] = sort_last.value_or(sort_default);
302  list.set_active_sorter(sorter_id, order, true);
303 
305 }
306 
308 {
309  listbox& list = find_widget<listbox>("recall_list");
310 
311  const int index = list.get_selected_row();
312  if (index == -1) {
313  return;
314  }
315 
316  unit& selected_unit = const_cast<unit&>(*recall_list_[index].get());
317 
318  std::string name = selected_unit.name();
319  const std::string dialog_title(_("Rename Unit"));
320  const std::string dialog_label(_("Name:"));
321 
322  if(gui2::dialogs::edit_text::execute(dialog_title, dialog_label, name)) {
323  selected_unit.rename(name);
324 
325  list.get_row_grid(index)->find_widget<label>("unit_name").set_label(name);
326 
327  filter_options_.erase(filter_options_.begin() + index);
328  std::ostringstream filter_text;
329  filter_text << selected_unit.type_name() << " " << name << " " << std::to_string(selected_unit.level());
330  for(const std::string& trait : selected_unit.trait_names()) {
331  filter_text << " " << trait;
332  }
333  filter_options_.insert(filter_options_.begin() + index, filter_text.str());
334 
337  }
338 }
339 
341 {
342  LOG_DP << "Recall list units:"; dump_recall_list_to_console(recall_list_);
343 
344  listbox& list = find_widget<listbox>("recall_list");
345  const int index = list.get_selected_row();
346  if (index == -1) {
347  return;
348  }
349 
350  const unit& u = *recall_list_[index].get();
351 
352  // If the unit is of level > 1, or is close to advancing, we warn the player about it
353  std::stringstream message;
354  if(u.loyal()) {
355  message << _("This unit is loyal and requires no upkeep.") << " " << (u.gender() == unit_race::MALE
356  ? _("Do you really want to dismiss him?")
357  : _("Do you really want to dismiss her?"));
358 
359  } else if(u.level() > 1) {
360  message << _("This unit is an experienced one, having advanced levels.") << " " << (u.gender() == unit_race::MALE
361  ? _("Do you really want to dismiss him?")
362  : _("Do you really want to dismiss her?"));
363 
364  } else if(u.experience() > u.max_experience()/2) {
365  message << _("This unit is close to advancing a level.") << " " << (u.gender() == unit_race::MALE
366  ? _("Do you really want to dismiss him?")
367  : _("Do you really want to dismiss her?"));
368  }
369 
370  if(!message.str().empty()) {
371  const int res = gui2::show_message(_("Dismiss Unit"), message.str(), message::yes_no_buttons);
372 
373  if(res != gui2::retval::OK) {
374  return;
375  }
376  }
377 
378  recall_list_.erase(recall_list_.begin() + index);
379 
380  // Remove the entry from the dialog list
381  list.remove_row(index);
383 
384  // Remove the entry from the filter list
385  filter_options_.erase(filter_options_.begin() + index);
386  assert(filter_options_.size() == list.get_item_count());
387 
388  LOG_DP << "Dismissing a unit, side = " << u.side() << ", id = '" << u.id() << "'";
389  LOG_DP << "That side's recall list:";
391 
392  // Find the unit in the recall list.
393  unit_ptr dismissed_unit = team_.recall_list().find_if_matches_id(u.id());
394  assert(dismissed_unit);
395 
396  // Record the dismissal, then delete the unit.
397  synced_context::run_and_throw("disband", replay_helper::get_disband(dismissed_unit->id()));
398 
399  // Close the dialog if all units are dismissed
400  if(list.get_item_count() == 0) {
402  }
403 }
404 
406 {
407  help::show_help("recruit_and_recall");
408 }
409 
411 {
412  const int selected_row
413  = find_widget<listbox>("recall_list").get_selected_row();
414 
415  if(selected_row == -1) {
416  return;
417  }
418 
419  const unit& selected_unit = *recall_list_[selected_row].get();
420 
421  find_widget<unit_preview_pane>("unit_details")
422  .set_display_data(selected_unit);
423 
424  find_widget<button>("rename").set_active(!selected_unit.unrenamable());
425 }
426 
428 {
429  listbox& list = find_widget<listbox>("recall_list");
430 
431  if(const auto [sorter, order] = list.get_active_sorter(); sorter) {
432  sort_last.emplace(sorter->id(), order);
433  } else {
434  sort_last.reset();
435  }
436 
437  if(get_retval() == retval::OK) {
439  }
440 }
441 
442 void unit_recall::filter_text_changed(const std::string& text)
443 {
444  const std::size_t shown = find_widget<listbox>("recall_list")
445  .filter_rows_by([this, match = translation::make_ci_matcher(text)](std::size_t row) {
446  return match(filter_options_[row]);
447  });
448 
449  // Disable rename and dismiss buttons if no units are shown
450  find_widget<button>("rename").set_active(shown > 0);
451  find_widget<button>("dismiss").set_active(shown > 0);
452 }
453 
454 } // namespace dialogs
virtual const unit_map & units() const override
Definition: game_board.hpp:107
Main class to show messages to the user.
Definition: message.hpp:36
@ yes_no_buttons
Shows a yes and no button.
Definition: message.hpp:81
Abstract base class for all modal dialogs.
int get_retval() const
Returns the cached window exit code.
std::vector< std::string > filter_options_
Definition: unit_recall.hpp:49
virtual void pre_show() override
Actions to be taken before showing the window.
void list_item_clicked()
Callbacks.
void filter_text_changed(const std::string &text)
std::vector< unit_const_ptr > & recall_list_
Definition: unit_recall.hpp:43
virtual void post_show() override
Actions to be taken after the window has been shown.
Base container class.
Definition: grid.hpp:32
widget * find(const std::string_view id, const bool must_be_active) override
See widget::find.
Definition: grid.cpp:645
t_string get_image() const
Wrapper for label.
Definition: image.hpp:58
void set_image(const t_string &label)
Wrapper for set_label.
Definition: image.hpp:45
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
const grid * get_row_grid(const unsigned row) const
Returns the grid of the wanted row.
Definition: listbox.cpp:267
void set_active_sorter(std::string_view id, sort_order::type order, bool select_first=false)
Sorts the listbox by a pre-set sorting option.
Definition: listbox.cpp:621
void set_sorters(Args &&... functors)
Registers sorting controls using magic index IDs.
Definition: listbox.hpp:306
std::pair< widget *, sort_order::type > get_active_sorter() const
Returns a widget pointer to the active sorter, along with its corresponding order.
Definition: listbox.cpp:635
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
unsigned get_item_count() const
Returns the number of items in the listbox.
Definition: listbox.cpp:159
const t_string & get_label() const
virtual void set_label(const t_string &text)
A widget that allows the user to input text in single line.
Definition: text_box.hpp:125
window * get_window()
Get the parent window.
Definition: widget.cpp:117
T * find_widget(const std::string_view id, const bool must_be_active, const bool must_exist)
Gets a widget with the wanted id.
Definition: widget.hpp:753
void set_retval(const int retval, const bool close_window=true)
Sets there return value of the window.
Definition: window.hpp:394
void keyboard_capture(widget *widget)
Definition: window.cpp:1207
void invalidate_layout()
Updates the size of the window.
Definition: window.cpp:761
void add_to_keyboard_chain(widget *widget)
Adds the widget to the keyboard chain.
Definition: window.cpp:1213
unit_ptr find_if_matches_id(const std::string &unit_id)
Find a unit by id.
static config get_disband(const std::string &unit_id)
static bool run_and_throw(const std::string &commandname, const config &data, action_spectator &spectator=get_default_spectator())
bool empty() const
Definition: tstring.hpp:194
const std::string & str() const
Definition: tstring.hpp:198
This class stores all the data for a single 'side' (in game nomenclature).
Definition: team.hpp:75
int side() const
Definition: team.hpp:180
int recall_cost() const
Definition: team.hpp:185
bool is_local_human() const
Definition: team.hpp:258
int gold() const
Definition: team.hpp:181
recall_list_manager & recall_list()
Definition: team.hpp:206
const std::set< std::string > & recruits() const
Definition: team.hpp:214
Container associating units to locations.
Definition: map.hpp:98
unit_iterator end()
Definition: map.hpp:428
unit_iterator find_leader(int side)
Definition: map.cpp:320
@ MALE
Definition: race.hpp:28
static std::string alignment_description(unit_alignments::type align, unit_race::GENDER gender=unit_race::MALE)
Implementation detail of unit_type::alignment_description.
Definition: types.cpp:841
This class represents a single unit of a specific type.
Definition: unit.hpp:133
static const std::string & leader_crown()
The path to the leader crown overlay.
Definition: unit.cpp:1141
std::size_t i
Definition: function.cpp:1029
static std::string _(const char *str)
Definition: gettext.hpp:93
unit_alignments::type alignment() const
The alignment of this unit.
Definition: unit.hpp:475
int level() const
The current level of this unit.
Definition: unit.hpp:559
const t_string & type_name() const
Gets the translatable name of this unit's type.
Definition: unit.hpp:369
bool unrenamable() const
Whether this unit can be renamed.
Definition: unit.hpp:436
int recall_cost() const
How much gold it costs to recall this unit, or -1 if the side's default recall cost is used.
Definition: unit.hpp:640
void rename(const std::string &name)
Attempts to rename this unit's translatable display name, taking the 'unrenamable' flag into account.
Definition: unit.hpp:424
const unit_race * race() const
Gets this unit's race.
Definition: unit.hpp:493
int experience() const
The current number of experience points this unit has.
Definition: unit.hpp:523
bool can_recruit() const
Whether this unit can recruit other units - ie, are they a leader unit.
Definition: unit.hpp:612
const std::string & id() const
Gets this unit's id.
Definition: unit.hpp:380
int side() const
The side this unit belongs to.
Definition: unit.hpp:343
unsigned int experience_to_advance() const
The number of experience points this unit needs to level up, or 0 if current XP > max XP.
Definition: unit.hpp:541
int max_experience() const
The max number of experience points this unit can have.
Definition: unit.hpp:529
unit_race::GENDER gender() const
The gender of this unit.
Definition: unit.hpp:465
const t_string & name() const
Gets this unit's translatable display name.
Definition: unit.hpp:403
bool can_advance() const
Checks whether this unit has any options to advance to.
Definition: unit.hpp:272
color_t xp_color() const
Color for this unit's XP.
Definition: unit.cpp:1216
std::string image_mods() const
Gets an IPF string containing all IPF image mods.
Definition: unit.cpp:2738
const std::vector< std::string > & overlays() const
Get the unit's overlay images.
Definition: unit.hpp:1677
std::string absolute_image() const
The name of the file to game_display (used in menus).
Definition: unit.cpp:2557
const std::vector< t_string > & trait_names() const
Gets the names of the currently registered traits.
Definition: unit.hpp:1098
bool loyal() const
Gets whether this unit is loyal - ie, it costs no upkeep.
Definition: unit.cpp:1701
This file contains the window object, this object is a top level container which has the event manage...
#define log_scope2(domain, description)
Definition: log.hpp:277
const std::string unicode_en_dash
Definition: constants.cpp:43
static std::string get_title_suffix(int side_num)
static const std::string maybe_inactive(const std::string &str, bool active)
Definition: unit_recall.cpp:81
static std::string format_level_string(const int level)
Definition: unit_list.cpp:47
static void dump_recall_list_to_console(const T &units)
Definition: unit_recall.cpp:67
static const color_t inactive_row_color(0x96, 0x96, 0x96)
static utils::optional< decltype(sort_default)> sort_last
Definition: unit_recall.cpp:52
REGISTER_DIALOG(editor_edit_unit)
static std::string format_cost_string(int unit_recall_cost, const int team_recall_cost)
static std::pair sort_default
Definition: unit_recall.cpp:51
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_message(const std::string &title, const std::string &msg, const std::string &button_caption, const bool auto_close, const bool message_use_markup, const bool title_use_markup)
Shows a message to the user.
Definition: message.cpp:148
@ OK
Dialog was closed with the OK button.
Definition: retval.hpp:35
@ CANCEL
Dialog was closed with the CANCEL button.
Definition: retval.hpp:38
void show_help(const std::string &show_topic)
Open the help browser, show topic with id show_topic.
Definition: help.cpp:140
Functions to load and save images from/to disk.
std::string bold(Args &&... data)
Definition: markup.hpp:128
std::string span_color(const color_t &color, Args &&... data)
Definition: markup.hpp:68
game_board * gameboard
Definition: resources.cpp:20
play_controller * controller
Definition: resources.cpp:21
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
static void msg(const char *act, debug_info &i, const char *to="", const char *result="")
Definition: debugger.cpp:109
std::shared_ptr< const unit > unit_const_ptr
Definition: ptr.hpp:27
std::shared_ptr< unit > unit_ptr
Definition: ptr.hpp:26
The basic class for representing 8-bit RGB or RGBA colour values.
Definition: color.hpp:59
Applies the planned unit map for the duration of the struct's life.
Definition: manager.hpp:253
#define LOG_DP
Definition: unit_recall.cpp:46
static lg::log_domain log_display("display")