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