The Battle for Wesnoth  1.17.17+dev
addon_list.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2016 - 2023
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 "addon/client.hpp"
20 #include "color.hpp"
21 #include "font/text_formatting.hpp"
22 #include "formatter.hpp"
23 #include "gettext.hpp"
27 #include "gui/widgets/button.hpp"
29 #include "gui/widgets/label.hpp"
30 #include "gui/widgets/listbox.hpp"
31 #include "gui/widgets/settings.hpp"
34 #include "gui/widgets/window.hpp"
35 #include "wml_exception.hpp"
36 
37 #include <algorithm>
38 
39 namespace gui2
40 {
41 namespace
42 {
43 const color_t color_outdated {255, 127, 0};
44 
45 const unsigned CONTROL_STACK_LAYER_INSTALL = 0;
46 const unsigned CONTROL_STACK_LAYER_UPDATE = 1;
47 const unsigned CONTROL_STACK_LAYER_PUBLISH = 2;
48 
49 } // end anon namespace
50 
51 REGISTER_WIDGET(addon_list)
52 
53 addon_list::addon_list(const implementation::builder_addon_list& builder)
54  : container_base(builder, type())
55  , addon_vector_()
56  , install_status_visibility_(visibility::visible)
57  , install_buttons_visibility_(visibility::invisible)
58  , install_function_()
59  , uninstall_function_()
60  , publish_function_()
61  , delete_function_()
62 {
63 }
64 
65 std::string addon_list::colorize_addon_state_string(const std::string& str, ADDON_STATUS state, bool verbose)
66 {
67  color_t colorname = font::NORMAL_COLOR;
68 
69  switch(state) {
70  case ADDON_NONE:
71  if(!verbose) {
72  return str;
73  }
74  colorname = font::weapon_details_color;
75  break;
76  case ADDON_INSTALLED:
78  case ADDON_NOT_TRACKED:
79  colorname = font::GOOD_COLOR;
80  break;
82  colorname = font::YELLOW_COLOR;
83  break;
85  colorname = color_outdated;
86  break;
88  colorname = font::BAD_COLOR;
89  break;
90  default:
91  colorname = font::GRAY_COLOR;
92  break;
93  }
94 
95  return font::span_color(colorname) + str + "</span>";
96 }
97 
99 {
100  std::string tx;
101 
102  switch(info.state) {
103  case ADDON_NONE:
104  tx = info.can_publish ? _("addon_state^Published, not installed") : _("addon_state^Not installed");
105  break;
106 
107  case ADDON_INSTALLED:
108  case ADDON_NOT_TRACKED:
109  // Consider add-ons without version information as installed
110  // for the main display. Their Description info should elaborate
111  // on their status.
112  tx = info.can_publish ? _("addon_state^Published") : _("addon_state^Installed");
113  break;
115  tx = info.can_publish ? _("addon_state^Ready to publish") : _("addon_state^Installed, not ready to publish");
116  break;
117 
119  tx = info.can_publish ? _("addon_state^Published, upgradable") : _("addon_state^Installed, upgradable");
120  break;
121 
123  tx = info.can_publish ? _("addon_state^Published, outdated on server") : _("addon_state^Installed, outdated on server");
124  break;
125 
127  tx = info.can_publish ? _("addon_state^Published, broken") : _("addon_state^Installed, broken");
128  break;
129 
130  default:
131  tx = _("addon_state^Unknown");
132  }
133 
134  return colorize_addon_state_string(tx, info.state, true);
135 }
136 
137 void addon_list::addon_action_wrapper(addon_op_func_t& func, const addon_info& addon, bool& handled, bool& halt)
138 {
139  try {
140  func(addon);
141 
142  handled = halt = true;
143  } catch(const addons_client::user_exit&) {
144  // User canceled the op.
145  }
146 }
147 
148 const std::string addon_list::display_title_full_shift(const addon_info& addon) const
149 {
150  const std::string& local_title = addon.display_title_translated();
151  const std::string& display_title = addon.display_title();
152  if(local_title.empty())
153  return display_title;
154  return local_title + "\n"
155  + "<small>(" + display_title + ")</small>";
156 }
157 
159 {
160  listbox& list = get_listbox();
161  list.clear();
162 
163  addon_vector_.clear();
164 
165  for(const auto& a : addons) {
166  const addon_info& addon = a.second;
167  addon_tracking_info tracking_info = get_addon_tracking_info(addon);
168 
169  addon_vector_.push_back(&addon);
170 
173 
174  if(!tracking_info.can_publish) {
175  item["label"] = addon.display_icon();
176  data.emplace("icon", item);
177 
178  item["label"] = display_title_full_shift(addon);
179  data.emplace("name", item);
180  } else {
181  item["label"] = addon.display_icon() + "~SCALE(72,72)~BLIT(icons/icon-addon-publish.png,8,8)";
182  data.emplace("icon", item);
183 
184  const std::string publish_name = formatter()
186  << display_title_full_shift(addon)
187  << "</span>";
188 
189  item["label"] = publish_name;
190  data.emplace("name", item);
191  }
192 
193  item["label"] = describe_status(tracking_info);
194  data.emplace("installation_status", item);
195 
196  // If the addon is upgradable or ourdated on server, we display the two relevant
197  // versions directly in the list for convenience.
198  const bool special_version_display =
199  tracking_info.state == ADDON_INSTALLED_UPGRADABLE ||
200  tracking_info.state == ADDON_INSTALLED_OUTDATED;
201 
202  std::ostringstream ss;
203  if(special_version_display) {
204  ss << tracking_info.installed_version.str() << "\n";
205  }
206 
207  ss << (*addon.versions.begin()).str();
208 
209  if(special_version_display) {
210  ss.str(colorize_addon_state_string(ss.str(), tracking_info.state, false));
211  }
212 
213  item["label"] = ss.str();
214  data.emplace("version", item);
215 
216  item["label"] = addon.author;
217  data.emplace("author", item);
218 
219  item["label"] = size_display_string(addon.size);
220  data.emplace("size", item);
221 
222  item["label"] = std::to_string(addon.downloads);
223  data.emplace("downloads", item);
224 
225  item["label"] = addon.display_type();
226  data.emplace("type", item);
227 
228  grid* row_grid = &list.add_row(data);
229 
230  // Set special retval for the toggle panels
231  find_widget<toggle_panel>(row_grid, "list_panel", false).set_retval(DEFAULT_ACTION_RETVAL);
232 
233  // The control button grid is excluded on lower resolutions.
234  grid* control_grid = find_widget<grid>(row_grid, "single_install_buttons", false, false);
235  if(!control_grid) {
236  continue;
237  }
238 
239  //
240  // Set up the inline control buttons.
241  //
242  stacked_widget& install_update_stack = find_widget<stacked_widget>(control_grid, "install_update_stack", false);
243 
244  // These three buttons are in the install_update_stack. Only one is shown depending on the addon's state.
245  button& install_button = find_widget<button>(control_grid, "single_install", false);
246  button& update_button = find_widget<button>(control_grid, "single_update", false);
247  button& publish_button = find_widget<button>(control_grid, "single_publish", false);
248 
249  // This button is always shown.
250  button& uninstall_button = find_widget<button>(control_grid, "single_uninstall", false);
251 
252  const bool is_installed = is_installed_addon_status(tracking_info.state);
253  const bool is_local = tracking_info.state == ADDON_INSTALLED_LOCAL_ONLY;
254 
255  // Select the right button layer and set its callback.
256  if(tracking_info.can_publish) {
257  install_update_stack.select_layer(CONTROL_STACK_LAYER_PUBLISH);
258 
259  publish_button.set_active(true);
260 
261  if(publish_function_ != nullptr) {
262  connect_signal_mouse_left_click(publish_button,
263  std::bind(&addon_list::addon_action_wrapper, this, publish_function_, std::ref(addon), std::placeholders::_3, std::placeholders::_4));
264 
265  install_button.set_tooltip(_("Publish add-on"));
266  }
267  } else if(tracking_info.state == ADDON_INSTALLED_UPGRADABLE) {
268  install_update_stack.select_layer(CONTROL_STACK_LAYER_UPDATE);
269 
270  update_button.set_active(true);
271 
272  if(update_function_ != nullptr) {
273  connect_signal_mouse_left_click(update_button,
274  std::bind(&addon_list::addon_action_wrapper, this, update_function_, std::ref(addon), std::placeholders::_3, std::placeholders::_4));
275  }
276  } else {
277  install_update_stack.select_layer(CONTROL_STACK_LAYER_INSTALL);
278 
279  install_button.set_active(!is_installed);
280 
281  if(install_function_ != nullptr) {
282  connect_signal_mouse_left_click(install_button,
283  std::bind(&addon_list::addon_action_wrapper, this, install_function_, std::ref(addon), std::placeholders::_3, std::placeholders::_4));
284  }
285  }
286 
287  // Set up the Uninstall button.
288  if(tracking_info.can_publish) {
289  // Use the uninstall button as a delete-from-server button if the addon's already been published...
290  uninstall_button.set_active(!is_local);
291 
292  if(!is_local && delete_function_ != nullptr) {
293  connect_signal_mouse_left_click(uninstall_button,
294  std::bind(&addon_list::addon_action_wrapper, this, delete_function_, std::ref(addon), std::placeholders::_3, std::placeholders::_4));
295 
296  uninstall_button.set_tooltip(_("Delete add-on from server"));
297  }
298  } else {
299  // ... else it functions as normal.
300  uninstall_button.set_active(is_installed);
301 
302  if(is_installed && uninstall_function_ != nullptr) {
303  connect_signal_mouse_left_click(uninstall_button,
304  std::bind(&addon_list::addon_action_wrapper, this, uninstall_function_, std::ref(addon), std::placeholders::_3, std::placeholders::_4));
305  }
306  }
307 
309  find_widget<label>(row_grid, "installation_status", false).set_visible(install_status_visibility_);
310  }
311 
313 }
314 
316 {
317  const listbox& list = find_widget<const listbox>(&get_grid(), "addons", false);
318 
319  try {
320  return addon_vector_.at(list.get_selected_row());
321  } catch(const std::out_of_range&) {
322  return nullptr;
323  }
324 }
325 
327 {
328  const addon_info* addon = get_selected_addon();
329  if(addon == nullptr || !get_addon_tracking_info(*addon).can_publish) {
330  return "";
331  } else {
332  return addon->id;
333  }
334 }
335 
336 void addon_list::select_addon(const std::string& id)
337 {
338  listbox& list = get_listbox();
339 
340  auto iter = std::find_if(addon_vector_.begin(), addon_vector_.end(),
341  [&id](const addon_info* a) { return a->id == id; }
342  );
343 
344  // Corner case: if you publish an addon with an out-of-folder .pbl file and
345  // delete it locally before deleting it from the server, the game will try
346  // to reselect an addon that now doesn't exist.
347  //
348  // I don't anticipate this check will match very often, but in the case that
349  // some other weird corner case crops up, it's probably better to just exit
350  // silently than asserting, as was the pervious behavior.
351  if(iter == addon_vector_.end()) {
352  return;
353  }
354 
355  const addon_info& info = **iter;
356 
357  for(unsigned int i = 0u; i < list.get_item_count(); ++i) {
358  grid* row = list.get_row_grid(i);
359 
360  const label& name_label = find_widget<label>(row, "name", false);
361  if(name_label.get_label().base_str() == display_title_full_shift(info)) {
362  list.select_row(i);
363  }
364  }
365 }
366 
368 {
369  return find_widget<listbox>(&get_grid(), "addons", false);
370 }
371 
373 {
374  if(window* window = get_window()) {
376  }
377 }
378 
380 {
381  listbox& list = get_listbox();
382 
383  list.register_translatable_sorting_option(0, [this](const int i) { return addon_vector_[i]->display_title_full(); });
384  list.register_sorting_option(1, [this](const int i) { return addon_vector_[i]->author; });
385  list.register_sorting_option(2, [this](const int i) { return addon_vector_[i]->size; });
386  list.register_sorting_option(3, [this](const int i) { return addon_vector_[i]->downloads; });
387  list.register_translatable_sorting_option(4, [this](const int i) { return addon_vector_[i]->display_type(); });
388 
389  auto order = std::pair(0, sort_order::type::ascending);
390  list.set_active_sorting_option(order);
391 }
392 
394 {
395  listbox& list = get_listbox();
396 
397  generator_base::order_func generator_func = [this, func](unsigned a, unsigned b)
398  {
399  return func(*addon_vector_[a], *addon_vector_[b]);
400  };
401 
402  list.mark_as_unsorted();
403  list.order_by(generator_func);
404 }
405 
407 {
408  if(addon_vector_.empty()) {
409  // Happens in the dialog unit test.
410  return;
411  }
413 }
414 
417 {
418  DBG_GUI_P << "Parsing add-on list " << id;
419 
420  load_resolutions<resolution>(cfg);
421 }
422 
424  : resolution_definition(cfg), grid(nullptr)
425 {
426  // Add a dummy state since every widget needs a state.
427  static config dummy("draw");
428  state.emplace_back(dummy);
429 
430  auto child = cfg.optional_child("grid");
431  VALIDATE(child, _("No grid defined."));
432 
433  grid = std::make_shared<builder_grid>(*child);
434 }
435 
436 namespace implementation
437 {
438 
439 static widget::visibility parse_visibility(const std::string& str)
440 {
441  if(str == "visible") {
443  } else if(str == "hidden") {
445  } else if(str == "invisible") {
447  } else {
448  FAIL("Invalid visibility value");
449  }
450 }
451 
452 builder_addon_list::builder_addon_list(const config& cfg)
453  : builder_styled_widget(cfg)
454  , install_status_visibility_(widget::visibility::visible)
455  , install_buttons_visibility_(widget::visibility::invisible)
456 {
457  if(cfg.has_attribute("install_status_visibility")) {
458  install_status_visibility_ = parse_visibility(cfg["install_status_visibility"]);
459  }
460 
461  if(cfg.has_attribute("install_buttons_visibility")) {
462  install_buttons_visibility_ = parse_visibility(cfg["install_buttons_visibility"]);
463  }
464 }
465 
466 std::unique_ptr<widget> builder_addon_list::build() const
467 {
468  auto widget = std::make_unique<addon_list>(*this);
469 
470  DBG_GUI_G << "Window builder: placed add-on list '" << id <<
471  "' with definition '" << definition << "'.";
472 
473  const auto conf = widget->cast_config_to<addon_list_definition>();
474  assert(conf != nullptr);
475 
476  widget->init_grid(*conf->grid);
477 
478  widget->set_install_status_visibility(install_status_visibility_);
479  widget->set_install_buttons_visibility(install_buttons_visibility_);
480 
481  widget->finalize_setup();
482 
483  return widget;
484 }
485 
486 } // end namespace implementation
487 
488 } // end namespace gui2
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:161
bool has_attribute(config_key_type key) const
Definition: config.cpp:159
optional_config_impl< config > optional_child(config_key_type key, int n=0)
Euivalent to mandatory_child, but returns an empty optional if the nth child was not found.
Definition: config.cpp:389
std::ostringstream wrapper.
Definition: formatter.hpp:40
addon_op_func_t uninstall_function_
Definition: addon_list.hpp:167
std::function< bool(const addon_info &, const addon_info &)> addon_sort_func
Definition: addon_list.hpp:42
void add_list_to_keyboard_chain()
Adds the internal listbox to the keyboard event chain.
Definition: addon_list.cpp:372
void addon_action_wrapper(addon_op_func_t &func, const addon_info &addon, bool &handled, bool &halt)
Helper to wrap the execution of any of the addon operation functions.
Definition: addon_list.cpp:137
const addon_info * get_selected_addon() const
Returns the selected add-on.
Definition: addon_list.cpp:315
visibility install_buttons_visibility_
Definition: addon_list.hpp:164
listbox & get_listbox()
Returns the underlying list box.
Definition: addon_list.cpp:367
std::string get_remote_addon_id()
Returns the selected add-on id, for use with remote publish/delete ops.
Definition: addon_list.cpp:326
void set_addon_order(addon_sort_func func)
Definition: addon_list.cpp:393
const std::string display_title_full_shift(const addon_info &addon) const
Definition: addon_list.cpp:148
static const int DEFAULT_ACTION_RETVAL
Special retval for the toggle panels in the addons list.
Definition: addon_list.hpp:47
static std::string colorize_addon_state_string(const std::string &str, ADDON_STATUS state, bool verbose=false)
Changes the color of an add-on state string (installed, outdated, etc.) according to the state itself...
Definition: addon_list.cpp:65
static std::string describe_status(const addon_tracking_info &info)
Definition: addon_list.cpp:98
std::vector< const addon_info * > addon_vector_
Definition: addon_list.hpp:161
addon_op_func_t update_function_
Definition: addon_list.hpp:168
void select_first_addon()
Choose the item at the top of the list (taking account of sort order).
Definition: addon_list.cpp:406
addon_op_func_t delete_function_
Definition: addon_list.hpp:171
addon_op_func_t publish_function_
Definition: addon_list.hpp:170
visibility install_status_visibility_
Definition: addon_list.hpp:163
void select_addon(const std::string &id)
Selects the add-on with the given ID.
Definition: addon_list.cpp:336
std::function< void(const addon_info &)> addon_op_func_t
Definition: addon_list.hpp:69
void set_addons(const addons_list &addons)
Sets the add-ons to show.
Definition: addon_list.cpp:158
addon_op_func_t install_function_
Definition: addon_list.hpp:166
Simple push button.
Definition: button.hpp:37
virtual void set_active(const bool active) override
See styled_widget::set_active.
Definition: button.cpp:65
A generic container base class.
const grid & get_grid() const
std::function< bool(unsigned, unsigned)> order_func
Definition: generator.hpp:249
Base container class.
Definition: grid.hpp:32
A label displays a text, the text can be wrapped but no scrollbars are provided.
Definition: label.hpp:58
The listbox class.
Definition: listbox.hpp:46
void mark_as_unsorted()
Deactivates all sorting toggle buttons at the top, making the list look like it's not sorted.
Definition: listbox.cpp:657
void set_active_sorting_option(const order_pair &sort_by, const bool select_first=false)
Sorts the listbox by a pre-set sorting option.
Definition: listbox.cpp:624
void order_by(const generator_base::order_func &func)
Definition: listbox.cpp:600
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:62
const grid * get_row_grid(const unsigned row) const
Returns the grid of the wanted row.
Definition: listbox.cpp:233
void register_translatable_sorting_option(const int col, translatable_sorter_func_t f)
Registers a special sorting function specifically for translatable values.
Definition: listbox.cpp:616
bool select_row(const unsigned row, const bool select=true)
Selects a row.
Definition: listbox.cpp:246
void register_sorting_option(const int col, const Func &f)
Definition: listbox.hpp:263
void clear()
Removes all the rows in the listbox, clearing it.
Definition: listbox.cpp:121
int get_selected_row() const
Returns the first selected row.
Definition: listbox.cpp:271
unsigned get_item_count() const
Returns the number of items in the listbox.
Definition: listbox.cpp:127
bool select_row_at(const unsigned row, const bool select=true)
Selects a row at the given position, regardless of sorting order.
Definition: listbox.cpp:259
A stacked widget holds several widgets on top of each other.
void select_layer(const int layer)
Selects and displays a particular layer.
const t_string & get_label() const
void set_tooltip(const t_string &tooltip)
Base class for all widgets.
Definition: widget.hpp:54
void set_visible(const visibility visible)
Definition: widget.cpp:456
window * get_window()
Get the parent window.
Definition: widget.cpp:118
visibility
Visibility settings done by the user.
Definition: widget.hpp:64
@ visible
The user sets the widget visible, that means:
@ invisible
The user set the widget invisible, that means:
@ hidden
The user sets the widget hidden, that means:
base class of top level items, the only item which needs to store the final canvases to draw on.
Definition: window.hpp:67
void add_to_keyboard_chain(widget *widget)
Adds the widget to the keyboard chain.
Definition: window.cpp:1136
std::string base_str() const
Definition: tstring.hpp:195
std::string str() const
Serializes the version number into string form.
Networked add-ons (campaignd) client interface.
std::size_t i
Definition: function.cpp:968
static std::string _(const char *str)
Definition: gettext.hpp:93
#define DBG_GUI_G
Definition: log.hpp:41
#define DBG_GUI_P
Definition: log.hpp:66
This file contains the window object, this object is a top level container which has the event manage...
std::string size_display_string(double size)
Get a human-readable representation of the specified byte count.
Definition: info.cpp:311
std::map< std::string, addon_info > addons_list
Definition: info.hpp:28
const color_t YELLOW_COLOR
const color_t GOOD_COLOR
const color_t BAD_COLOR
const color_t GRAY_COLOR
const color_t weapon_details_color
const color_t NORMAL_COLOR
std::string span_color(const color_t &color)
Returns a Pango formatting string using the provided color_t object.
void connect_signal_mouse_left_click(dispatcher &dispatcher, const signal &signal)
Connects a signal handler for a left mouse button click.
Definition: dispatcher.cpp:179
static widget::visibility parse_visibility(const std::string &str)
Definition: addon_list.cpp:439
Generic file dialog.
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
Contains the implementation details for lexical_cast and shouldn't be used directly.
logger & info()
Definition: log.cpp:232
std::string_view data
Definition: picture.cpp:199
#define REGISTER_WIDGET(id)
Wrapper for REGISTER_WIDGET3.
This file contains the settings handling of the widget library.
addon_tracking_info get_addon_tracking_info(const addon_info &addon)
Get information about an add-on comparing its local state with the add-ons server entry.
Definition: state.cpp:25
bool is_installed_addon_status(ADDON_STATUS s)
Definition: state.hpp:40
ADDON_STATUS
Defines various add-on installation statuses.
Definition: state.hpp:22
@ ADDON_NOT_TRACKED
No tracking information available.
Definition: state.hpp:37
@ ADDON_INSTALLED_OUTDATED
Version in the server is older than local installation.
Definition: state.hpp:30
@ ADDON_NONE
Add-on is not installed.
Definition: state.hpp:24
@ ADDON_INSTALLED_UPGRADABLE
Version in the server is newer than local installation.
Definition: state.hpp:28
@ ADDON_INSTALLED
Version in the server matches local installation.
Definition: state.hpp:26
@ ADDON_INSTALLED_LOCAL_ONLY
No version in the server.
Definition: state.hpp:32
@ ADDON_INSTALLED_BROKEN
Dependencies not satisfied.
Definition: state.hpp:35
std::string display_type() const
Get an add-on type identifier for display in the user's language.
Definition: info.cpp:249
int downloads
Definition: info.hpp:88
std::string display_title_translated() const
Definition: info.cpp:196
std::string display_icon() const
Get an icon path fixed for display (e.g.
Definition: info.cpp:232
std::string display_title() const
Get a title or automatic title for display.
Definition: info.cpp:155
std::string author
Definition: info.hpp:85
std::set< version_info, std::greater< version_info > > versions
Definition: info.hpp:83
int size
Definition: info.hpp:87
std::string id
Definition: info.hpp:76
Stores additional status information about add-ons.
Definition: state.hpp:47
ADDON_STATUS state
Definition: state.hpp:57
version_info installed_version
Definition: state.hpp:60
The basic class for representing 8-bit RGB or RGBA colour values.
Definition: color.hpp:59
addon_list_definition(const config &cfg)
Definition: addon_list.cpp:415
widget::visibility install_buttons_visibility_
Definition: addon_list.hpp:224
virtual std::unique_ptr< widget > build() const override
Definition: addon_list.cpp:466
std::string definition
Parameters for the styled_widget.
Base class of a resolution, contains the common keys for a resolution.
std::vector< state_definition > state
Add a special kind of assert to validate whether the input from WML doesn't contain any problems that...
#define FAIL(message)
#define VALIDATE(cond, message)
The macro to use for the validation of WML.
#define a
#define b