The Battle for Wesnoth  1.19.0-dev
edit_pbl.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2023 - 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 "editor/editor_common.hpp"
20 #include "filesystem.hpp"
21 #include "gettext.hpp"
25 #include "gui/dialogs/message.hpp"
26 #include "gui/widgets/button.hpp"
27 #include "gui/widgets/drawing.hpp"
28 #include "gui/widgets/label.hpp"
29 #include "gui/widgets/listbox.hpp"
32 #include "gui/widgets/text_box.hpp"
36 #include "serialization/parser.hpp"
39 
40 
41 namespace gui2::dialogs
42 {
43 
44 REGISTER_DIALOG(editor_edit_pbl)
45 
46 const 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  "other",
60 };
61 
62 const std::array tag_values = {
63  "cooperative",
64  "cosmetic",
65  "difficulty",
66  "rng",
67  "survival",
68  "terraforming",
69 };
70 
71 editor_edit_pbl::editor_edit_pbl(const std::string& pbl, const std::string& current_addon)
72  : modal_dialog(window_id())
73  , pbl_(pbl)
74  , current_addon_(current_addon)
75  , dirs_()
76 {
78  find_widget<toggle_button>(get_window(), "forum_auth", false), std::bind(&editor_edit_pbl::toggle_auth, this));
79  connect_signal_mouse_left_click(find_widget<button>(get_window(), "translations_add", false),
80  std::bind(&editor_edit_pbl::add_translation, this));
81  connect_signal_mouse_left_click(find_widget<button>(get_window(), "translations_delete", false),
82  std::bind(&editor_edit_pbl::delete_translation, this));
84  find_widget<button>(get_window(), "validate", false), std::bind(&editor_edit_pbl::validate, this));
86  find_widget<button>(get_window(), "select_icon", false), std::bind(&editor_edit_pbl::select_icon_file, this));
88  find_widget<text_box>(get_window(), "icon", false), std::bind(&editor_edit_pbl::update_icon_preview, this));
89  connect_signal_notify_modified(find_widget<text_box>(get_window(), "forum_thread", false),
90  std::bind(&editor_edit_pbl::update_url_preview, this));
91  label& url = find_widget<label>(get_window(), "forum_url", false);
92  url.set_link_aware(true);
93  url.set_use_markup(true);
94  // not setting this to some value causes the modified signal to not update the label text
95  url.set_label("https://r.wesnoth.org/t");
96 }
97 
99 {
100  config pbl;
102  try {
103  read(pbl, *preprocess_file(pbl_));
104  } catch(const config::error& e) {
105  ERR_ED << "Caught a config error while parsing file " << pbl_ << "\n" << e.message;
106  }
107  }
108 
109  text_box* name = find_widget<text_box>(&win, "name", false, true);
110  name->set_value(pbl["title"]);
111  win.keyboard_capture(name);
112 
113  find_widget<scroll_text>(&win, "description", false).set_value(pbl["description"]);
114  find_widget<text_box>(&win, "icon", false).set_value(pbl["icon"]);
115  if(!pbl["icon"].empty()) {
116  drawing& img = find_widget<drawing>(&win, "preview", false);
117  img.set_label(pbl["icon"]);
118  }
119  find_widget<text_box>(&win, "author", false).set_value(pbl["author"]);
120  find_widget<text_box>(&win, "version", false).set_value(pbl["version"]);
121 
122  multimenu_button& dependencies = find_widget<multimenu_button>(&win, "dependencies", false);
123  std::vector<config> addons_list;
125  if(dirs_.size() > 0 && std::find(dirs_.begin(), dirs_.end(), current_addon_) != dirs_.end()) {
126  dirs_.erase(std::remove(dirs_.begin(), dirs_.end(), current_addon_));
127  }
128 
129  for(const std::string& dir : dirs_) {
130  addons_list.emplace_back("label", dir, "checkbox", false);
131  }
132  dependencies.set_values(addons_list);
133 
134  std::vector<std::string> existing_dependencies = utils::split(pbl["dependencies"].str(), ',');
135  for(unsigned i = 0; i < dirs_.size(); i++) {
136  if(std::find(existing_dependencies.begin(), existing_dependencies.end(), dirs_[i]) != existing_dependencies.end()) {
137  dependencies.select_option(i);
138  }
139  }
140 
141  if(pbl["forum_auth"].to_bool()) {
142  find_widget<toggle_button>(&win, "forum_auth", false).set_value(true);
143  find_widget<text_box>(&win, "email", false).set_visible(gui2::widget::visibility::invisible);
144  find_widget<label>(&win, "email_label", false).set_visible(gui2::widget::visibility::invisible);
145  find_widget<text_box>(&win, "password", false).set_visible(gui2::widget::visibility::invisible);
146  find_widget<label>(&win, "password_label", false).set_visible(gui2::widget::visibility::invisible);
147  find_widget<text_box>(&win, "secondary_authors", false).set_visible(gui2::widget::visibility::visible);
148  find_widget<label>(&win, "secondary_authors_label", false).set_visible(gui2::widget::visibility::visible);
149  } else {
150  find_widget<text_box>(&win, "email", false).set_value(pbl["email"]);
151  find_widget<text_box>(&win, "password", false).set_value(pbl["passphrase"]);
152  find_widget<text_box>(&win, "secondary_authors", false).set_visible(gui2::widget::visibility::invisible);
153  find_widget<label>(&win, "secondary_authors_label", false).set_visible(gui2::widget::visibility::invisible);
154  }
155 
156  if(pbl.has_child("feedback")) {
157  find_widget<text_box>(&win, "forum_thread", false).set_value(pbl.mandatory_child("feedback")["topic_id"]);
158  }
159 
160  unsigned selected = 0;
161  for(unsigned i = 0; i < type_values.size(); i++) {
162  if(type_values[i] == pbl["type"]) {
163  selected = i;
164  break;
165  }
166  }
167 
168  menu_button& types = find_widget<menu_button>(&win, "type", false);
169  std::vector<config> type_list;
170  type_list.emplace_back("label", "");
171  type_list.emplace_back("label", _("Core"));
172  type_list.emplace_back("label", _("Campaign"));
173  type_list.emplace_back("label", _("Hybrid Campaign"));
174  type_list.emplace_back("label", _("Multiplayer Campaign"));
175  type_list.emplace_back("label", _("Scenario"));
176  type_list.emplace_back("label", _("Multiplayer Scenario"));
177  type_list.emplace_back("label", _("Faction"));
178  type_list.emplace_back("label", _("Era"));
179  type_list.emplace_back("label", _("Map Pack"));
180  type_list.emplace_back("label", _("Modification"));
181  type_list.emplace_back("label", _("Media"));
182  type_list.emplace_back("label", _("Other"));
183  types.set_values(type_list);
184  types.set_selected(selected);
185 
186  multimenu_button& tags = find_widget<multimenu_button>(&win, "tags", false);
187  std::vector<config> tags_list;
188  tags_list.emplace_back("label", _("Cooperative"), "checkbox", false);
189  tags_list.emplace_back("label", _("Cosmetic"), "checkbox", false);
190  tags_list.emplace_back("label", _("Difficulty"), "checkbox", false);
191  tags_list.emplace_back("label", _("RNG"), "checkbox", false);
192  tags_list.emplace_back("label", _("Survival"), "checkbox", false);
193  tags_list.emplace_back("label", _("Terraforming"), "checkbox", false);
194  tags.set_values(tags_list);
195 
196  std::vector<std::string> chosen_tags = utils::split(pbl["tags"].str(), ',');
197  for(unsigned i = 0; i < tag_values.size(); i++) {
198  if(std::find(chosen_tags.begin(), chosen_tags.end(), tag_values[i]) != chosen_tags.end()) {
199  tags.select_option(i);
200  }
201  }
202 
203  listbox& translations = find_widget<listbox>(&win, "translations", false);
204  button& translations_delete = find_widget<button>(&win, "translations_delete", false);
205 
206  for(const config& child : pbl.child_range("translation")) {
207  const widget_data& entry{
208  {"translations_language", widget_item{{"label", child["language"].str()}}},
209  {"translations_title", widget_item{{"label", child["title"].str()}}},
210  {"translations_description", widget_item{{"label", child["description"].str()}}},
211  };
212  translations.add_row(entry);
213  }
214 
215  if(translations.get_item_count() == 0) {
216  translations_delete.set_active(false);
217  }
218 }
219 
221 {
222  if(get_retval() != retval::OK) {
223  return;
224  }
225 
226  std::stringstream wml_stream;
227  config_writer out(wml_stream, false);
228  out.write(create_cfg());
229  filesystem::write_file(pbl_, wml_stream.str());
230 }
231 
233 {
234  config cfg;
235 
236  if(const std::string& name = find_widget<text_box>(get_window(), "name", false).get_value(); !name.empty()) {
237  cfg["title"] = name;
238  }
239  if(const std::string& description = find_widget<scroll_text>(get_window(), "description", false).get_value(); !description.empty()) {
240  cfg["description"] = description;
241  }
242  if(const std::string& icon = find_widget<text_box>(get_window(), "icon", false).get_value(); !icon.empty()) {
243  cfg["icon"] = icon;
244  }
245  if(const std::string& author = find_widget<text_box>(get_window(), "author", false).get_value(); !author.empty()) {
246  cfg["author"] = author;
247  }
248  if(const std::string& version = find_widget<text_box>(get_window(), "version", false).get_value(); !version.empty()) {
249  cfg["version"] = version;
250  }
251 
252  multimenu_button& dependencies = find_widget<multimenu_button>(get_window(), "dependencies", false);
253  boost::dynamic_bitset<> dep_states = dependencies.get_toggle_states();
254  std::vector<std::string> chosen_deps;
255  for(unsigned i = 0; i < dep_states.size(); i++) {
256  if(dep_states[i] == 1) {
257  chosen_deps.emplace_back(dirs_[i]);
258  }
259  }
260  if(chosen_deps.size() > 0) {
261  cfg["dependencies"] = utils::join(chosen_deps, ",");
262  }
263 
264  if(find_widget<toggle_button>(get_window(), "forum_auth", false).get_value_bool()) {
265  cfg["forum_auth"] = true;
266 
267  if(const std::string& secondary_authors = find_widget<text_box>(get_window(), "secondary_authors", false).get_value(); !secondary_authors.empty()) {
268  cfg["secondary_authors"] = secondary_authors;
269  }
270  } else {
271  if(const std::string& email = find_widget<text_box>(get_window(), "email", false).get_value(); !email.empty()) {
272  cfg["email"] = email;
273  }
274  if(const std::string& passphrase = find_widget<text_box>(get_window(), "password", false).get_value(); !passphrase.empty()) {
275  cfg["passphrase"] = passphrase;
276  }
277  }
278 
279  if(const std::string& topic_id = find_widget<text_box>(get_window(), "forum_thread", false).get_value(); !topic_id.empty()) {
280  config& feedback = cfg.add_child("feedback");
281  feedback["topic_id"] = topic_id;
282  }
283 
284  if(unsigned value = find_widget<menu_button>(get_window(), "type", false).get_value(); value != 0) {
285  cfg["type"] = type_values[value];
286  }
287 
288  multimenu_button& tags = find_widget<multimenu_button>(get_window(), "tags", false);
289  boost::dynamic_bitset<> tag_states = tags.get_toggle_states();
290  std::vector<std::string> chosen_tags;
291  for(unsigned i = 0; i < tag_states.size(); i++) {
292  if(tag_states[i] == 1) {
293  chosen_tags.emplace_back(tag_values[i]);
294  }
295  }
296  if(chosen_tags.size() > 0) {
297  cfg["tags"] = utils::join(chosen_tags, ",");
298  }
299 
300  listbox& translations = find_widget<listbox>(get_window(), "translations", false);
301  for(unsigned i = 0; i < translations.get_item_count(); i++) {
302  grid* row = translations.get_row_grid(i);
303  config& translation = cfg.add_child("translation");
304 
305  translation["language"] = dynamic_cast<label*>(row->find("translations_language", false))->get_label();
306  translation["title"] = dynamic_cast<label*>(row->find("translations_title", false))->get_label();
307  translation["description"] = dynamic_cast<label*>(row->find("translations_description", false))->get_label();
308  }
309 
310  return cfg;
311 }
312 
314 {
315  toggle_button& forum_auth = find_widget<toggle_button>(get_window(), "forum_auth", false);
316  if(forum_auth.get_value_bool()) {
317  find_widget<text_box>(get_window(), "email", false).set_visible(gui2::widget::visibility::invisible);
318  find_widget<text_box>(get_window(), "password", false).set_visible(gui2::widget::visibility::invisible);
319  find_widget<label>(get_window(), "email_label", false).set_visible(gui2::widget::visibility::invisible);
320  find_widget<label>(get_window(), "password_label", false).set_visible(gui2::widget::visibility::invisible);
321  find_widget<text_box>(get_window(), "secondary_authors", false).set_visible(gui2::widget::visibility::visible);
322  find_widget<label>(get_window(), "secondary_authors_label", false).set_visible(gui2::widget::visibility::visible);
323  } else {
324  find_widget<text_box>(get_window(), "email", false).set_visible(gui2::widget::visibility::visible);
325  find_widget<text_box>(get_window(), "password", false).set_visible(gui2::widget::visibility::visible);
326  find_widget<label>(get_window(), "email_label", false).set_visible(gui2::widget::visibility::visible);
327  find_widget<label>(get_window(), "password_label", false).set_visible(gui2::widget::visibility::visible);
328  find_widget<text_box>(get_window(), "secondary_authors", false).set_visible(gui2::widget::visibility::invisible);
329  find_widget<label>(get_window(), "secondary_authors_label", false).set_visible(gui2::widget::visibility::invisible);
330  }
331 }
332 
334 {
335  std::string language;
336  std::string title;
337  std::string description;
338  editor_edit_pbl_translation::execute(language, title, description);
339 
340  if(!language.empty() && !title.empty()) {
341  listbox& translations = find_widget<listbox>(get_window(), "translations", false);
342  const widget_data& entry{
343  {"translations_language", widget_item{{"label", language}}},
344  {"translations_title", widget_item{{"label", title}}},
345  {"translations_description", widget_item{{"label", description}}},
346  };
347  translations.add_row(entry);
348  find_widget<button>(get_window(), "translations_delete", false).set_active(true);
349  }
350 }
351 
353 {
354  listbox& translations = find_widget<listbox>(get_window(), "translations", false);
355  translations.remove_row(translations.get_selected_row());
356 
357  button& translations_delete = find_widget<button>(get_window(), "translations_delete", false);
358  if(translations.get_item_count() == 0) {
359  translations_delete.set_active(false);
360  }
361 }
362 
364 {
365  std::unique_ptr<schema_validation::schema_validator> validator;
366  validator.reset(new schema_validation::schema_validator(filesystem::get_wml_location("schema/pbl.cfg")));
367  validator->set_create_exceptions(false);
368 
369  config temp;
370  std::stringstream ss;
371  ss << create_cfg();
372  read(temp, ss.str(), validator.get());
373  if(!validator->get_errors().empty()) {
374  gui2::show_error_message(utils::join(validator->get_errors(), "\n"));
375  } else {
376  gui2::show_message(_("Success"), _("No validation errors"), gui2::dialogs::message::button_style::auto_close);
377  }
378 }
379 
381 {
382  std::string icon = find_widget<text_box>(get_window(), "icon", false).get_value();
383  if(icon.find(".png") != std::string::npos || icon.find(".jpg") != std::string::npos || icon.find(".webp") != std::string::npos) {
384  std::string path = filesystem::get_core_images_dir() + icon;
385  drawing& img = find_widget<drawing>(get_window(), "preview", false);
386 
387  if(filesystem::file_exists(path) || icon.find("data:image") != std::string::npos) {
388  img.set_label(icon);
389  } else {
390  img.set_label("");
391  ERR_ED << "Failed to find icon file: " << path;
392  }
393  }
394 }
395 
397 {
398  std::string topic = find_widget<text_box>(get_window(), "forum_thread", false).get_value();
399  find_widget<label>(get_window(), "forum_url", false).set_label("https://r.wesnoth.org/t" + topic);
400 }
401 
403 {
405 
406  dlg.set_title(_("Choose an icon")).set_path(filesystem::get_core_images_dir() + "/icons/");
407 
408  if(dlg.show()) {
409  std::string path = dlg.path();
410  if(path.find(filesystem::get_core_images_dir()) == 0) {
411  std::string icon = path.substr(filesystem::get_core_images_dir().length() + 1);
412  // setting this programmatically doesn't seem to trigger connect_signal_notify_modified()
413  find_widget<text_box>(get_window(), "icon", false).set_value(icon);
414  find_widget<drawing>(get_window(), "preview", false).set_label(icon);
415  } else {
416  std::string uri = filesystem::read_file_as_data_uri(path);
417 
418  if(!uri.empty()) {
419  find_widget<text_box>(get_window(), "icon", false).set_value(uri);
420  find_widget<drawing>(get_window(), "preview", false).set_label(uri);
421  }
422  }
423  }
424 }
425 
426 } // namespace gui2::dialogs
Class for writing a config out to a file in pieces.
void write(const config &cfg)
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:159
config & mandatory_child(config_key_type key, int n=0)
Returns the nth child with the given key, or throws an error if there is none.
Definition: config.cpp:367
bool has_child(config_key_type key) const
Determine whether a config has a child or not.
Definition: config.cpp:317
child_itors child_range(config_key_type key)
Definition: config.cpp:273
const attribute_value * get(config_key_type key) const
Returns a pointer to the attribute with the given key or nullptr if it does not exist.
Definition: config.cpp:687
config & add_child(config_key_type key)
Definition: config.cpp:441
Simple push button.
Definition: button.hpp:36
virtual void set_active(const bool active) override
See styled_widget::set_active.
Definition: button.cpp:63
editor_edit_pbl(const std::string &pbl, const std::string &current_addon)
Definition: edit_pbl.cpp:71
virtual void post_show(window &window) override
Actions to be taken after the window has been shown.
Definition: edit_pbl.cpp:220
virtual void pre_show(window &window) override
The execute function.
Definition: edit_pbl.cpp:98
std::vector< std::string > dirs_
Definition: edit_pbl.hpp:70
file_dialog & set_path(const std::string &value)
Sets the initial file selection.
file_dialog & set_title(const std::string &value)
Sets the current dialog title text.
Definition: file_dialog.hpp:59
std::string path() const
Gets the current file selection.
Abstract base class for all modal dialogs.
bool show(const unsigned auto_close_time=0)
Shows the window.
int get_retval() const
Returns the cached window exit code.
window * get_window()
Returns a pointer to the dialog's window.
A drawing is widget with a fixed size and gives access to the canvas of the widget in the window inst...
Definition: drawing.hpp:49
Base container class.
Definition: grid.hpp:32
void set_active(const bool active)
Activates all children.
Definition: grid.cpp:167
widget * find(const std::string &id, const bool must_be_active) override
See widget::find.
Definition: grid.cpp:645
A label displays text that can be wrapped but no scrollbars are provided.
Definition: label.hpp:56
void set_link_aware(bool l)
Definition: label.cpp:88
The listbox class.
Definition: listbox.hpp:43
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:59
const grid * get_row_grid(const unsigned row) const
Returns the grid of the wanted row.
Definition: listbox.cpp:230
void remove_row(const unsigned row, unsigned count=1)
Removes a row in the listbox.
Definition: listbox.cpp:79
int get_selected_row() const
Returns the first selected row.
Definition: listbox.cpp:268
unsigned get_item_count() const
Returns the number of items in the listbox.
Definition: listbox.cpp:124
A menu_button is a styled_widget to choose an element from a list of elements.
Definition: menu_button.hpp:59
void set_selected(unsigned selected, bool fire_event=true)
void set_values(const std::vector<::config > &values, unsigned selected=0)
A multimenu_button is a styled_widget to choose an element from a list of elements.
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.
const t_string & get_label() const
virtual void set_label(const t_string &text)
virtual void set_use_markup(bool use_markup)
virtual void set_value(const std::string &text)
The set_value is virtual for the password_box class.
Class for a single line text area.
Definition: text_box.hpp:142
Class for a toggle button.
@ visible
The user sets the widget visible, that means:
@ invisible
The user set the widget invisible, that means:
base class of top level items, the only item which needs to store the final canvases to draw on.
Definition: window.hpp:63
void keyboard_capture(widget *widget)
Definition: window.cpp:1221
Realization of serialization/validator.hpp abstract validator.
Main (common) editor header.
#define ERR_ED
Declarations for File-IO.
std::size_t i
Definition: function.cpp:968
static std::string _(const char *str)
Definition: gettext.hpp:93
std::map< std::string, addon_info > addons_list
Definition: info.hpp:27
void get_files_in_dir(const std::string &dir, std::vector< std::string > *files, std::vector< std::string > *dirs, name_mode mode, filter_mode filter, reorder_mode reorder, file_tree_checksum *checksum)
Get a list of all files and/or directories in a given directory.
Definition: filesystem.cpp:405
static bool file_exists(const bfs::path &fpath)
Definition: filesystem.cpp:319
std::string get_wml_location(const std::string &filename, const std::string &current_dir)
Returns a complete path to the actual WML file or directory or an empty string if the file isn't pres...
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)
Throws io_exception if an error occurs.
std::string get_addons_dir()
std::string get_core_images_dir()
std::string selected
std::string path
Definition: filesystem.cpp:84
void remove()
Removes a tip.
Definition: tooltip.cpp:109
const std::array type_values
Definition: edit_pbl.cpp:46
REGISTER_DIALOG(editor_edit_unit)
const std::array tag_values
Definition: edit_pbl.cpp:62
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:34
std::map< std::string, t_string > widget_item
Definition: widget.hpp:31
void show_error_message(const std::string &msg, bool message_use_markup)
Shows an error message to the user.
Definition: message.cpp:203
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:150
@ OK
Dialog was closed with the OK button.
Definition: retval.hpp:35
std::string language()
Definition: general.cpp:535
std::string join(const T &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.
void read(config &cfg, std::istream &in, abstract_validator *validator)
Definition: parser.cpp:627
#define e