The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
edit_pbl.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2023 - 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/validation.hpp"
21#include "filesystem.hpp"
22#include "gettext.hpp"
28#include "gui/widgets/label.hpp"
39
40
41namespace gui2::dialogs
42{
43
44REGISTER_DIALOG(editor_edit_pbl)
45
46const std::array type_values = {
47 "",
48 "core",
49 "campaign",
50 "campaign_sp_mp",
51 "campaign_mp",
52 "scenario",
53 "scenario_mp",
54 "faction",
55 "era",
56 "map_pack",
57 "mod_mp",
58 "media",
59 "theme",
60 "other",
61};
62
63const std::array tag_values = {
64 "cooperative",
65 "cosmetic",
66 "difficulty",
67 "rng",
68 "survival",
69 "terraforming",
70};
71
72editor_edit_pbl::editor_edit_pbl(const std::string& pbl, const std::string& current_addon)
73 : modal_dialog(window_id())
74 , pbl_(pbl)
75 , current_addon_(current_addon)
76 , dirs_()
77{
78 connect_signal_mouse_left_click(
79 find_widget<toggle_button>("forum_auth"), std::bind(&editor_edit_pbl::toggle_auth, this));
80 connect_signal_mouse_left_click(find_widget<button>("translations_add"),
81 std::bind(&editor_edit_pbl::add_translation, this));
82 connect_signal_mouse_left_click(find_widget<button>("translations_delete"),
83 std::bind(&editor_edit_pbl::delete_translation, this));
84 connect_signal_mouse_left_click(
85 find_widget<button>("validate"), std::bind(&editor_edit_pbl::validate, this));
86 connect_signal_mouse_left_click(
87 find_widget<button>("select_icon"), std::bind(&editor_edit_pbl::select_icon_file, this));
88 connect_signal_notify_modified(
89 find_widget<text_box>("icon"), std::bind(&editor_edit_pbl::update_icon_preview, this));
90 connect_signal_notify_modified(find_widget<text_box>("forum_thread"),
91 std::bind(&editor_edit_pbl::update_url_preview, this));
92 label& url = find_widget<label>("forum_url");
93 url.set_link_aware(true);
94 url.set_use_markup(true);
95 // not setting this to some value causes the modified signal to not update the label text
96 url.set_label("https://r.wesnoth.org/t");
97}
98
100{
101 config pbl;
103 try {
105 } catch(const config::error& e) {
106 ERR_ED << "Caught a config error while parsing file " << pbl_ << "\n" << e.message;
107 }
108 }
109
110 text_box* name = find_widget<text_box>("name", false, true);
111 name->set_value(pbl["title"]);
112 keyboard_capture(name);
113
114 find_widget<scroll_text>("description").set_value(pbl["description"].t_str());
115 find_widget<text_box>("icon").set_value(pbl["icon"]);
116 if(!pbl["icon"].empty()) {
117 drawing& img = find_widget<drawing>("preview");
118 img.set_label(pbl["icon"].str());
119 }
120 find_widget<text_box>("author").set_value(pbl["author"]);
121 find_widget<text_box>("version").set_value(pbl["version"]);
122
123 multimenu_button& dependencies = find_widget<multimenu_button>("dependencies");
124 std::vector<config> addons_list;
126 if(dirs_.size() > 0 && utils::contains(dirs_, current_addon_)) {
128 }
129
130 for(const std::string& dir : dirs_) {
131 addons_list.emplace_back("label", dir, "checkbox", false);
132 }
133 dependencies.set_values(addons_list);
134
135 std::vector<std::string> existing_dependencies = utils::split(pbl["dependencies"].str(), ',');
136 for(unsigned i = 0; i < dirs_.size(); i++) {
137 if(utils::contains(existing_dependencies, dirs_[i])) {
138 dependencies.select_option(i);
139 }
140 }
141
142 if(pbl["forum_auth"].to_bool()) {
143 find_widget<toggle_button>("forum_auth").set_value(true);
144 find_widget<text_box>("primary_authors").set_value(pbl["primary_authors"]);
145 find_widget<text_box>("secondary_authors").set_value(pbl["secondary_authors"]);
146 find_widget<text_box>("email").set_visible(gui2::widget::visibility::invisible);
147 find_widget<label>("email_label").set_visible(gui2::widget::visibility::invisible);
148 find_widget<text_box>("password").set_visible(gui2::widget::visibility::invisible);
149 find_widget<label>("password_label").set_visible(gui2::widget::visibility::invisible);
150 find_widget<text_box>("primary_authors").set_visible(gui2::widget::visibility::visible);
151 find_widget<label>("primary_authors_label").set_visible(gui2::widget::visibility::visible);
152 find_widget<text_box>("secondary_authors").set_visible(gui2::widget::visibility::visible);
153 find_widget<label>("secondary_authors_label").set_visible(gui2::widget::visibility::visible);
154 } else {
155 find_widget<text_box>("email").set_value(pbl["email"]);
156 find_widget<text_box>("password").set_value(pbl["passphrase"]);
157 find_widget<text_box>("primary_authors").set_visible(gui2::widget::visibility::invisible);
158 find_widget<label>("primary_authors_label").set_visible(gui2::widget::visibility::invisible);
159 find_widget<text_box>("secondary_authors").set_visible(gui2::widget::visibility::invisible);
160 find_widget<label>("secondary_authors_label").set_visible(gui2::widget::visibility::invisible);
161 }
162
163 if(pbl.has_child("feedback")) {
164 find_widget<text_box>("forum_thread").set_value(pbl.mandatory_child("feedback")["topic_id"]);
165 }
166
167 unsigned selected = 0;
168 for(unsigned i = 0; i < type_values.size(); i++) {
169 if(pbl["type"] == type_values[i]) {
170 selected = i;
171 break;
172 }
173 }
174
175 menu_button& types = find_widget<menu_button>("type");
176 std::vector<config> type_list;
177 type_list.emplace_back("label", "");
178 type_list.emplace_back("label", _("Core"));
179 type_list.emplace_back("label", _("Campaign"));
180 type_list.emplace_back("label", _("Hybrid Campaign"));
181 type_list.emplace_back("label", _("Multiplayer Campaign"));
182 type_list.emplace_back("label", _("Scenario"));
183 type_list.emplace_back("label", _("Multiplayer Scenario"));
184 type_list.emplace_back("label", _("Faction"));
185 type_list.emplace_back("label", _("Era"));
186 type_list.emplace_back("label", _("Map Pack"));
187 type_list.emplace_back("label", _("Modification"));
188 type_list.emplace_back("label", _("Media"));
189 type_list.emplace_back("label", _("Theme"));
190 type_list.emplace_back("label", _("Other"));
191 types.set_values(type_list);
192 types.set_selected(selected);
193
194 multimenu_button& tags = find_widget<multimenu_button>("tags");
195 std::vector<config> tags_list;
196 tags_list.emplace_back("label", _("Cooperative"), "checkbox", false);
197 tags_list.emplace_back("label", _("Cosmetic"), "checkbox", false);
198 tags_list.emplace_back("label", _("Difficulty"), "checkbox", false);
199 tags_list.emplace_back("label", _("RNG"), "checkbox", false);
200 tags_list.emplace_back("label", _("Survival"), "checkbox", false);
201 tags_list.emplace_back("label", _("Terraforming"), "checkbox", false);
202 tags.set_values(tags_list);
203
204 std::vector<std::string> chosen_tags = utils::split(pbl["tags"].str(), ',');
205 for(unsigned i = 0; i < tag_values.size(); i++) {
206 if(utils::contains(chosen_tags, tag_values[i])) {
207 tags.select_option(i);
208 }
209 }
210
211 listbox& translations = find_widget<listbox>("translations");
212 button& translations_delete = find_widget<button>("translations_delete");
213
214 for(const config& child : pbl.child_range("translation")) {
215 translations.add_row(widget_data{
216 { "translations_language", {
217 { "label", child["language"].str() }
218 }},
219 { "translations_title", {
220 { "label", child["title"].str() }
221 }},
222 { "translations_description", {
223 { "label", child["description"].str() }
224 }},
225 });
226 }
227
228 if(translations.get_item_count() == 0) {
229 translations_delete.set_active(false);
230 }
231}
232
234{
235 if(get_retval() != retval::OK) {
236 return;
237 }
238
239 std::stringstream wml_stream;
240 config_writer out(wml_stream, false);
241 out.write(create_cfg());
242 filesystem::write_file(pbl_, wml_stream.str());
243}
244
246{
247 config cfg;
248
249 if(const std::string& name = find_widget<text_box>("name").get_value(); !name.empty()) {
250 cfg["title"] = name;
251 }
252 if(const std::string& description = find_widget<scroll_text>("description").get_value(); !description.empty()) {
253 cfg["description"] = description;
254 }
255 if(const std::string& icon = find_widget<text_box>("icon").get_value(); !icon.empty()) {
256 cfg["icon"] = icon;
257 }
258 if(const std::string& author = find_widget<text_box>("author").get_value(); !author.empty()) {
259 cfg["author"] = author;
260 }
261 if(const std::string& version = find_widget<text_box>("version").get_value(); !version.empty()) {
262 cfg["version"] = version;
263 }
264
265 multimenu_button& dependencies = find_widget<multimenu_button>("dependencies");
266 boost::dynamic_bitset<> dep_states = dependencies.get_toggle_states();
267 std::vector<std::string> chosen_deps;
268 for(unsigned i = 0; i < dep_states.size(); i++) {
269 if(dep_states[i] == 1) {
270 chosen_deps.emplace_back(dirs_[i]);
271 }
272 }
273 if(chosen_deps.size() > 0) {
274 cfg["dependencies"] = utils::join(chosen_deps, ",");
275 }
276
277 if(find_widget<toggle_button>("forum_auth").get_value_bool()) {
278 cfg["forum_auth"] = true;
279
280 if(const std::string& primary_authors = find_widget<text_box>("primary_authors").get_value(); !primary_authors.empty()) {
281 cfg["primary_authors"] = primary_authors;
282 }
283
284 if(const std::string& secondary_authors = find_widget<text_box>("secondary_authors").get_value(); !secondary_authors.empty()) {
285 cfg["secondary_authors"] = secondary_authors;
286 }
287 } else {
288 if(const std::string& email = find_widget<text_box>("email").get_value(); !email.empty()) {
289 cfg["email"] = email;
290 }
291 if(const std::string& passphrase = find_widget<text_box>("password").get_value(); !passphrase.empty()) {
292 cfg["passphrase"] = passphrase;
293 }
294 }
295
296 if(const std::string& topic_id = find_widget<text_box>("forum_thread").get_value(); !topic_id.empty()) {
297 config& feedback = cfg.add_child("feedback");
298 feedback["topic_id"] = topic_id;
299 }
300
301 if(unsigned value = find_widget<menu_button>("type").get_value(); value != 0) {
302 cfg["type"] = type_values[value];
303 }
304
305 multimenu_button& tags = find_widget<multimenu_button>("tags");
306 boost::dynamic_bitset<> tag_states = tags.get_toggle_states();
307 std::vector<std::string> chosen_tags;
308 for(unsigned i = 0; i < tag_states.size(); i++) {
309 if(tag_states[i] == 1) {
310 chosen_tags.emplace_back(tag_values[i]);
311 }
312 }
313 if(chosen_tags.size() > 0) {
314 cfg["tags"] = utils::join(chosen_tags, ",");
315 }
316
317 listbox& translations = find_widget<listbox>("translations");
318 for(unsigned i = 0; i < translations.get_item_count(); i++) {
319 grid* row = translations.get_row_grid(i);
320 config& translation = cfg.add_child("translation");
321
322 translation["language"] = dynamic_cast<label*>(row->find("translations_language", false))->get_label();
323 translation["title"] = dynamic_cast<label*>(row->find("translations_title", false))->get_label();
324 translation["description"] = dynamic_cast<label*>(row->find("translations_description", false))->get_label();
325 }
326
327 return cfg;
328}
329
331{
332 toggle_button& forum_auth = find_widget<toggle_button>("forum_auth");
333 if(forum_auth.get_value_bool()) {
334 find_widget<text_box>("email").set_visible(gui2::widget::visibility::invisible);
335 find_widget<text_box>("password").set_visible(gui2::widget::visibility::invisible);
336 find_widget<label>("email_label").set_visible(gui2::widget::visibility::invisible);
337 find_widget<label>("password_label").set_visible(gui2::widget::visibility::invisible);
338 find_widget<text_box>("primary_authors").set_visible(gui2::widget::visibility::visible);
339 find_widget<label>("primary_authors_label").set_visible(gui2::widget::visibility::visible);
340 find_widget<text_box>("secondary_authors").set_visible(gui2::widget::visibility::visible);
341 find_widget<label>("secondary_authors_label").set_visible(gui2::widget::visibility::visible);
342 } else {
343 find_widget<text_box>("email").set_visible(gui2::widget::visibility::visible);
344 find_widget<text_box>("password").set_visible(gui2::widget::visibility::visible);
345 find_widget<label>("email_label").set_visible(gui2::widget::visibility::visible);
346 find_widget<label>("password_label").set_visible(gui2::widget::visibility::visible);
347 find_widget<text_box>("primary_authors").set_visible(gui2::widget::visibility::invisible);
348 find_widget<label>("primary_authors_label").set_visible(gui2::widget::visibility::invisible);
349 find_widget<text_box>("secondary_authors").set_visible(gui2::widget::visibility::invisible);
350 find_widget<label>("secondary_authors_label").set_visible(gui2::widget::visibility::invisible);
351 }
352}
353
355{
356 std::string language;
357 std::string title;
358 std::string description;
359 editor_edit_pbl_translation::execute(language, title, description);
360
361 if(!language.empty() && !title.empty()) {
362 find_widget<listbox>("translations").add_row(widget_data{
363 { "translations_language", {
364 { "label", language }
365 }},
366 { "translations_title", {
367 { "label", title }
368 }},
369 { "translations_description", {
370 { "label", description }
371 }},
372 });
373
374 find_widget<button>("translations_delete").set_active(true);
375 }
376}
377
379{
380 listbox& translations = find_widget<listbox>("translations");
381 translations.remove_row(translations.get_selected_row());
382
383 button& translations_delete = find_widget<button>("translations_delete");
384 if(translations.get_item_count() == 0) {
385 translations_delete.set_active(false);
386 }
387}
388
390{
391 std::unique_ptr<schema_validation::schema_validator> validator;
393 validator->set_create_exceptions(false);
394
395 std::stringstream ss;
396 ss << create_cfg();
397 config temp = io::read(ss, validator.get());
398
399 if(!validator->get_errors().empty()) {
400 gui2::show_error_message(utils::join(validator->get_errors(), "\n"));
401 } else if(addon_icon_too_large(temp["icon"].str())) {
402 gui2::show_error_message(_("The icon’s file size is too large"));
403 } else {
404 gui2::show_message(_("Success"), _("No validation errors"), gui2::dialogs::message::button_style::auto_close);
405 }
406}
407
409{
410 std::string icon = find_widget<text_box>("icon").get_value();
411 if(icon.find(".png") != std::string::npos || icon.find(".jpg") != std::string::npos || icon.find(".webp") != std::string::npos) {
412 std::string path = filesystem::get_core_images_dir() + icon;
413 drawing& img = find_widget<drawing>("preview");
414
415 if(filesystem::file_exists(path) || icon.find("data:image") != std::string::npos) {
416 img.set_label(icon);
417 } else {
418 img.set_label("");
419 ERR_ED << "Failed to find icon file: " << path;
420 }
421 }
422}
423
425{
426 std::string topic = find_widget<text_box>("forum_thread").get_value();
427 find_widget<label>("forum_url").set_label("https://r.wesnoth.org/t" + topic);
428}
429
431{
433
434 dlg.set_title(_("Choose an icon")).set_path(filesystem::get_core_images_dir() + "/icons/");
435
436 if(dlg.show()) {
437 std::string path = dlg.path();
438 if(path.find(filesystem::get_core_images_dir()) == 0) {
439 std::string icon = path.substr(filesystem::get_core_images_dir().length() + 1);
440 // setting this programmatically doesn't seem to trigger connect_signal_notify_modified()
441 find_widget<text_box>("icon").set_value(icon);
442 find_widget<drawing>("preview").set_label(icon);
443 } else {
444 std::string uri = filesystem::read_file_as_data_uri(path);
445
446 if(!uri.empty()) {
447 find_widget<text_box>("icon").set_value(uri);
448 find_widget<drawing>("preview").set_label(uri);
449 }
450 }
451 }
452}
453
454} // namespace gui2::dialogs
Class for writing a config out to a file in pieces.
void write(const config &cfg, bool strong_quotes=false)
A config object defines a single node in a WML file, with access to child nodes.
Definition config.hpp:157
config & add_child(std::string_view key)
Definition config.cpp:436
child_itors child_range(std::string_view key)
Definition config.cpp:268
bool has_child(std::string_view key) const
Determine whether a config has a child or not.
Definition config.cpp:312
config & mandatory_child(std::string_view key, int n=0)
Returns the nth child with the given key, or throws an error if there is none.
Definition config.cpp:362
Simple push button.
Definition button.hpp:36
virtual void set_active(const bool active) override
See styled_widget::set_active.
Definition button.cpp:64
editor_edit_pbl(const std::string &pbl, const std::string &current_addon)
Definition edit_pbl.cpp:72
virtual void pre_show() override
The execute function.
Definition edit_pbl.cpp:99
virtual void post_show() override
Actions to be taken after the window has been shown.
Definition edit_pbl.cpp:233
std::vector< std::string > dirs_
Definition edit_pbl.hpp:46
file_dialog & set_path(const std::string &value)
Sets the initial file selection.
std::string path() const
Gets the current file selection.
file_dialog & set_title(const std::string &value)
Sets the current dialog title text.
@ auto_close
Enables auto close.
Definition message.hpp:71
Abstract base class for all modal dialogs.
bool show(const unsigned auto_close_time=0)
Shows the window.
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
void set_link_aware(bool l)
Definition label.cpp:89
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:256
void remove_row(const unsigned row, unsigned count=1)
Removes a row in the listbox.
Definition listbox.cpp:108
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
void set_selected(unsigned selected, bool fire_event=true)
void set_values(const std::vector<::config > &values, unsigned selected=0)
void set_values(const std::vector<::config > &values)
Set the available menu options.
void select_option(const unsigned option, const bool selected=true)
Select an option in the menu.
boost::dynamic_bitset get_toggle_states() const
Get the current state of the menu options.
virtual void set_label(const t_string &text)
virtual void set_use_markup(bool use_markup)
const t_string & get_label() const
virtual void set_value(const std::string &text)
The set_value is virtual for the password_box class.
A widget that allows the user to input text in single line.
Definition text_box.hpp:125
@ visible
The user sets the widget visible, that means:
@ invisible
The user set the widget invisible, that means:
void keyboard_capture(widget *widget)
Definition window.cpp:1197
int get_retval()
Definition window.hpp:394
Realization of serialization/validator.hpp abstract validator.
Main (common) editor header.
#define ERR_ED
const config * cfg
Declarations for File-IO.
static std::string _(const char *str)
Definition gettext.hpp:100
std::map< std::string, addon_info > addons_list
Definition info.hpp:28
#define REGISTER_DIALOG(window_id)
Wrapper for REGISTER_DIALOG2.
bool file_exists(const std::string &name)
Returns true if a file or directory with such name already exists.
void get_files_in_dir(const std::string &dir, std::vector< std::string > *files, std::vector< std::string > *dirs=nullptr, name_mode mode=name_mode::FILE_NAME_ONLY, filter_mode filter=filter_mode::NO_FILTER, reorder_mode reorder=reorder_mode::DONT_REORDER, file_tree_checksum *checksum=nullptr)
Get a list of all files and/or directories in a given directory.
utils::optional< std::string > get_wml_location(const std::string &path, const utils::optional< std::string > &current_dir=utils::nullopt)
Returns a translated path to the actual file or directory, if it exists.
std::string read_file_as_data_uri(const std::string &fname)
void write_file(const std::string &fname, const std::string &data, std::ios_base::openmode mode=std::ios_base::binary)
Throws io_exception if an error occurs.
std::string get_addons_dir()
std::string get_core_images_dir()
const std::array type_values
Definition edit_pbl.cpp:46
const std::array tag_values
Definition edit_pbl.cpp:63
std::map< std::string, widget_item > widget_data
Definition widget.hpp:36
void show_error_message(const std::string &msg, bool message_use_markup)
Shows an error message to the user.
Definition message.cpp:201
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
config read(std::istream &in, abstract_validator *validator)
Definition parser.cpp:610
STL namespace.
std::size_t erase(Container &container, const Value &value)
Convenience wrapper for using std::remove on a container.
Definition general.hpp:119
bool contains(const Container &container, const Value &value)
Returns true iff value is found in container.
Definition general.hpp:88
std::string join(const Range &v, const std::string &s=",")
Generates a new string joining container items in a list.
std::vector< std::string > split(const config_attribute_value &val)
filesystem::scoped_istream preprocess_file(const std::string &fname, preproc_map &defines)
Function to use the WML preprocessor on a file.
One of the realizations of serialization/validator.hpp abstract validator.
std::unique_ptr< MIX_Audio, decltype(&MIX_DestroyAudio)> value
Definition sound.cpp:139
bool addon_icon_too_large(const std::string &icon)
Checks whether an add-on icon is too large.
#define i
#define e