The Battle for Wesnoth  1.19.21+dev
edit_unit.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2023 - 2025
3  by Subhraman Sarkar (babaissarkar) <sbmskmm@protonmail.com>
4  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5 
6  This program is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY.
12 
13  See the COPYING file for more details.
14 */
15 
16 /* unit type editor dialog */
17 
18 #define GETTEXT_DOMAIN "wesnoth-lib"
19 
21 
22 #include "filesystem.hpp"
23 #include "gettext.hpp"
25 #include "gui/dialogs/message.hpp"
28 #include "gui/widgets/button.hpp"
29 #include "gui/widgets/combobox.hpp"
30 #include "gui/widgets/image.hpp"
31 #include "gui/widgets/label.hpp"
36 #include "gui/widgets/slider.hpp"
37 #include "gui/widgets/spinner.hpp"
39 #include "gui/widgets/text_box.hpp"
41 #include "picture.hpp"
43 #include "serialization/parser.hpp"
45 #include "units/types.hpp"
46 
47 #include <boost/algorithm/string/replace.hpp>
48 #include <boost/filesystem.hpp>
49 #include <sstream>
50 
51 namespace gui2::dialogs
52 {
53 
55 
56 // TODO properly support attributes from installed addons
58  : modal_dialog(window_id())
59  , game_config_(game_config)
60  , addon_id_(addon_id)
61 {
62  for (const auto& [id, cfg] : unit_types.specials()) {
63  specials_list_.emplace_back("label", id, "checkbox", false);
64  }
65 
66  for (const auto& [id, cfg] : unit_types.abilities()) {
67  abilities_list_.emplace_back("label", id, "checkbox", false);
68  }
69 
70  connect_signal<event::SDL_KEY_DOWN>(std::bind(
71  &editor_edit_unit::signal_handler_sdl_key_down, this, std::placeholders::_2, std::placeholders::_3, std::placeholders::_5, std::placeholders::_6));
72 }
73 
75  tab_container& tabs = find_widget<tab_container>("tabs");
77 
78  button& quit = find_widget<button>("exit");
80 
81  //
82  // Main Stats tab
83  //
84 
85  menu_button& alignments = find_widget<menu_button>("alignment_list");
86  for (auto& align : unit_alignments::values) {
87  // Show the user the translated strings,
88  // but use the untranslated align strings for generated WML
89  const std::string& icon_path = "icons/alignments/alignment_" + std::string(align) + "_30.png";
90  align_list_.emplace_back("label", t_string(static_cast<std::string>(align), "wesnoth"), "icon", icon_path);
91  }
92  alignments.set_values(align_list_);
93 
94  menu_button& races = find_widget<menu_button>("race_list");
95  for(const auto& [_, race] : unit_types.races()) {
96  race_list_.emplace_back("label", race.id(), "icon", race.get_icon_path_stem() + "_30.png");
97  }
98 
99  if (!race_list_.empty()) {
100  races.set_values(race_list_);
101  }
102 
103  button& load = find_widget<button>("load_unit_type");
104  std::stringstream tooltip;
105  tooltip << t_string("Hotkey(s): ", "wesnoth");
106  #ifdef __APPLE__
107  tooltip << "cmd+o";
108  #else
109  tooltip << "ctrl+o";
110  #endif
111  load.set_tooltip(tooltip.str());
113 
115  find_widget<button>("browse_unit_image"),
116  std::bind(&editor_edit_unit::select_file, this, "data/core/images/units", "unit_image"));
118  find_widget<button>("preview_unit_image"),
119  std::bind(&editor_edit_unit::update_image, this, "unit_image"));
121  find_widget<button>("browse_portrait_image"),
122  std::bind(&editor_edit_unit::select_file, this, "data/core/images/portraits", "portrait_image"));
124  find_widget<button>("preview_portrait_image"),
125  std::bind(&editor_edit_unit::update_image, this, "portrait_image"));
126 
128  find_widget<text_box>("name_box"),
129  std::bind(&editor_edit_unit::button_state_change, this));
131  find_widget<text_box>("id_box"),
132  std::bind(&editor_edit_unit::button_state_change, this));
133 
134  //
135  // Advanced Tab
136  //
137 
138  menu_button& movetypes = find_widget<menu_button>("movetype_list");
139  for(const auto& [name, _] : unit_types.movement_types()) {
140  movetype_list_.emplace_back("label", name);
141  }
142 
143  if (!movetype_list_.empty()) {
144  movetypes.set_values(movetype_list_);
145  }
146 
147  menu_button& defenses = find_widget<menu_button>("defense_list");
148  const config& defense_attr = game_config_
149  .mandatory_child("units")
150  .mandatory_child("movetype")
151  .mandatory_child("defense");
152  for (const auto& [key, _] : defense_attr.attribute_range()) {
153  defense_list_.emplace_back("label", key);
154  }
155 
156  menu_button& movement_costs = find_widget<menu_button>("movement_costs_list");
157  if (!defense_list_.empty()) {
158  defenses.set_values(defense_list_);
159  def_toggles_.resize(defense_list_.size());
160  movement_costs.set_values(defense_list_);
161  move_toggles_.resize(defense_list_.size());
162  }
163 
164  menu_button& resistances = find_widget<menu_button>("resistances_list");
165 
166  const config& resistances_attr = game_config_
167  .mandatory_child("units")
168  .mandatory_child("movetype")
169  .mandatory_child("resistance");
170  for (const auto& [key, _] : resistances_attr.attribute_range()) {
171  resistances_list_.emplace_back("label", key, "icon", "icons/profiles/" + key + ".png");
172  }
173 
174  if (!resistances_list_.empty()) {
175  resistances.set_values(resistances_list_);
176  res_toggles_.resize(resistances_list_.size());
177  }
178 
179  menu_button& usage_types = find_widget<menu_button>("usage_list");
180  usage_type_list_.emplace_back("label", _("scout"));
181  usage_type_list_.emplace_back("label", _("fighter"));
182  usage_type_list_.emplace_back("label", _("archer"));
183  usage_type_list_.emplace_back("label", _("mixed fighter"));
184  usage_type_list_.emplace_back("label", _("healer"));
185  usage_types.set_values(usage_type_list_);
186 
187  multimenu_button& abilities = find_widget<multimenu_button>("abilities_list");
188  abilities.set_values(abilities_list_);
189 
191  find_widget<button>("browse_small_profile_image"),
192  std::bind(&editor_edit_unit::select_file, this, "data/core/images/portraits", "small_profile_image"));
194  find_widget<button>("preview_small_profile_image"),
195  std::bind(&editor_edit_unit::update_image, this, "small_profile_image"));
197  find_widget<button>("load_movetype"),
198  std::bind(&editor_edit_unit::load_movetype, this));
200  find_widget<slider>("resistances_slider"),
201  std::bind(&editor_edit_unit::store_resistances, this));
203  find_widget<menu_button>("resistances_list"),
204  std::bind(&editor_edit_unit::update_resistances, this));
206  find_widget<toggle_button>("resistances_checkbox"),
208 
210  find_widget<slider>("defense_slider"),
211  std::bind(&editor_edit_unit::store_defenses, this));
213  find_widget<menu_button>("defense_list"),
214  std::bind(&editor_edit_unit::update_defenses, this));
216  find_widget<toggle_button>("defense_checkbox"),
217  std::bind(&editor_edit_unit::enable_defense_slider, this));
218 
220  find_widget<slider>("movement_costs_slider"),
221  std::bind(&editor_edit_unit::store_movement_costs, this));
223  find_widget<menu_button>("movement_costs_list"),
224  std::bind(&editor_edit_unit::update_movement_costs, this));
226  find_widget<toggle_button>("movement_costs_checkbox"),
227  std::bind(&editor_edit_unit::enable_movement_slider, this));
228 
229  if (!res_toggles_.empty()) {
231  }
232 
233  if (!def_toggles_.empty()) {
235  }
236 
237  if (!move_toggles_.empty()) {
239  }
240 
241  //
242  // Attack Tab
243  //
244 
245  multimenu_button& specials = find_widget<multimenu_button>("weapon_specials_list");
246  specials.set_values(specials_list_);
247 
248  combobox& attack_types = find_widget<combobox>("attack_type_list");
249  if (!resistances_list_.empty()) {
250  attack_types.set_values(resistances_list_);
251  }
252 
253  // Connect signals
255  find_widget<button>("browse_attack_image"),
256  std::bind(&editor_edit_unit::select_file, this, "data/core/images/attacks", "attack_image"));
258  find_widget<button>("preview_attack_image"),
259  std::bind(&editor_edit_unit::update_image, this, "attack_image"));
261  find_widget<menu_button>("atk_list"),
262  std::bind(&editor_edit_unit::select_attack, this));
264  find_widget<button>("atk_new"),
265  std::bind(&editor_edit_unit::add_attack, this));
267  find_widget<button>("atk_delete"),
268  std::bind(&editor_edit_unit::delete_attack, this));
270  find_widget<button>("atk_next"),
271  std::bind(&editor_edit_unit::next_attack, this));
273  find_widget<button>("atk_prev"),
274  std::bind(&editor_edit_unit::prev_attack, this));
275 
276  update_index();
277 
278  // Disable OK button at start, since ID and Name boxes are empty
280 }
281 
283 {
284  save_unit_type();
285 
286  tab_container& tabs = find_widget<tab_container>("tabs");
287  if (tabs.get_active_tab_index() == 3) {
288  update_wml_view();
289  }
290 }
291 
292 void editor_edit_unit::select_file(const std::string& default_dir, const std::string& id_stem)
293 {
295  dlg.set_title(_("Choose File"))
296  .set_ok_label(_("Select"))
297  .set_path(default_dir)
298  .set_read_only(true);
299 
300  // If the file is found inside Wesnoth's data, give its relative path
301  // if not, ask user if they want to copy it into their addon.
302  // If yes, copy and return correct relative path inside addon.
303  // return empty otherwise.
304  auto find_or_copy = [](const std::string& path, const std::string& addon_id, const std::string& type) {
305  const std::string message
306  = _("This file is outside Wesnoth’s data dirs. Do you wish to copy it into your add-on?");
307  const auto optional_path = filesystem::to_asset_path(path, addon_id, type);
308 
309  if(optional_path.has_value()) {
310  return optional_path.value();
313  output_path /= type;
314  output_path /= boost::filesystem::path(path).filename();
315  filesystem::copy_file(path, output_path.string());
316  return output_path.filename().string();
317  } else {
318  return std::string();
319  }
320  };
321 
322  if (dlg.show()) {
323  if((id_stem == "unit_image")
324  || (id_stem == "portrait_image")
325  || (id_stem == "small_profile_image")
326  || (id_stem == "attack_image"))
327  {
328  find_widget<text_box>("path_"+id_stem).set_value(find_or_copy(dlg.path(), addon_id_, "images"));
329  update_image(id_stem);
330  }
331  }
332 }
333 
335  const auto& all_type_list = unit_types.types_list();
336  const auto& type_select = gui2::dialogs::units_dialog::build_create_dialog(all_type_list);
337  type_select->set_ok_label("Load");
338 
339  if (!type_select->show() && !type_select->is_selected()) {
340  return;
341  }
342 
343  const auto& type = all_type_list[type_select->get_selected_index()];
344 
345  find_widget<text_box>("id_box").set_value(type->id());
346  find_widget<text_box>("name_box").set_value(type->type_name().base_str());
347  find_widget<spinner>("level_box").set_value(type->level());
348  find_widget<slider>("cost_slider").set_value(type->cost());
349  find_widget<text_box>("adv_box").set_value(utils::join(type->advances_to()));
350  find_widget<slider>("hp_slider").set_value(type->hitpoints());
351  find_widget<slider>("xp_slider").set_value(type->experience_needed());
352  find_widget<slider>("move_slider").set_value(type->movement());
353  find_widget<scroll_text>("desc_box").set_value(type->unit_description().base_str());
354  find_widget<text_box>("path_unit_image").set_value(type->image());
355  find_widget<text_box>("path_portrait_image").set_value(type->big_profile());
356 
357  for (const auto& gender : type->genders())
358  {
359  if (gender == unit_race::GENDER::MALE) {
360  find_widget<toggle_button>("gender_male").set_value(true);
361  }
362 
363  if (gender == unit_race::GENDER::FEMALE) {
364  find_widget<toggle_button>("gender_female").set_value(true);
365  }
366  }
367 
369  find_widget<menu_button>("race_list"),
370  race_list_,
371  type->race_id());
372 
374  find_widget<menu_button>("alignment_list"),
375  align_list_,
376  unit_alignments::get_string(type->alignment()));
377 
378  update_image("unit_image");
379 
380  boost::dynamic_bitset<> enabled_abilities(abilities_list_.size());
381  for (size_t i = 0; i < abilities_list_.size(); i++) {
382  for (auto special : type->abilities()) {
383  const std::string id = special->cfg()["unique_id"].str(special->id());
384  if (abilities_list_[i]["label"] == id) {
385  enabled_abilities[i] = true;
386  }
387  }
388  }
389  find_widget<multimenu_button>("abilities_list").select_options(enabled_abilities);
390 
391  find_widget<text_box>("path_small_profile_image").set_value(type->small_profile());
392 
394  find_widget<menu_button>("movetype_list"),
396  type->movement_type_id());
397 
398  config cfg;
399  type->movement_type().write(cfg, false);
400  movement_ = cfg.mandatory_child("movement_costs");
401  defenses_ = cfg.mandatory_child("defense");
402  resistances_ = cfg.mandatory_child("resistance");
403 
404  // Overrides for resistance/defense/movement costs
405  for (unsigned i = 0; i < resistances_list_.size(); i++) {
406  if (!type->get_cfg().has_child("resistance")) {
407  break;
408  }
409 
410  for (const auto& [key, _] : type->get_cfg().mandatory_child("resistance").attribute_range()) {
411  if (resistances_list_.at(i)["label"] == key) {
412  res_toggles_[i] = 1;
413  }
414  }
415  }
416 
417  for (unsigned i = 0; i < defense_list_.size(); i++) {
418  if (type->get_cfg().has_child("defense")) {
419  for (const auto& [key, _] : type->get_cfg().mandatory_child("defense").attribute_range()) {
420  if (defense_list_.at(i)["label"] == key) {
421  def_toggles_[i] = 1;
422  }
423  }
424  }
425 
426  if (type->get_cfg().has_child("movement_costs")) {
427  for (const auto& [key, _] : type->get_cfg().mandatory_child("movement_costs").attribute_range()) {
428  if (defense_list_.at(i)["label"] == key) {
429  move_toggles_[i] = 1;
430  }
431  }
432  }
433  }
434 
436  update_defenses();
438 
440  find_widget<menu_button>("usage_list"),
442  type->usage());
443 
444  update_image("small_profile_image");
445 
446  attacks_.clear();
447  for(const auto& atk : type->attacks())
448  {
449  config attack;
450  attack["name"] = atk.id();
451  attack["description"] = atk.name().base_str();
452  attack["icon"] = atk.icon();
453  attack["range"] = atk.range();
454  attack["damage"] = atk.damage();
455  attack["number"] = atk.num_attacks();
456  attack["type"] = atk.type();
457 
458  boost::dynamic_bitset<> enabled_specials(specials_list_.size());
459 
460  for (size_t i = 0; i < specials_list_.size(); i++) {
461  for (auto special : atk.specials()) {
462  const std::string id = special->cfg()["unique_id"].str(special->id());
463  if (specials_list_[i]["label"] == id) {
464  enabled_specials[i] = true;
465  }
466  }
467  }
468 
469  attacks_.push_back(std::make_pair(enabled_specials, attack));
470  }
471 
472  if (!type->attacks().empty()) {
473  selected_attack_ = 1;
474  update_attacks();
475  }
476 
477  update_index();
478 
481 }
482 
484 
485  // Clear the config
486  type_cfg_.clear();
487 
488  // Textdomain
489  std::string current_textdomain = "wesnoth-" + addon_id_;
490 
491  config& utype = type_cfg_.add_child("unit_type");
492  utype["id"] = find_widget<text_box>("id_box").get_value();
493  utype["name"] = t_string(find_widget<text_box>("name_box").get_value(), current_textdomain);
494  utype["image"] = find_widget<text_box>("path_unit_image").get_value();
495  utype["profile"] = find_widget<text_box>("path_portrait_image").get_value();
496  utype["level"] = find_widget<spinner>("level_box").get_value();
497  utype["advances_to"] = find_widget<text_box>("adv_box").get_value();
498  utype["hitpoints"] = find_widget<slider>("hp_slider").get_value();
499  utype["experience"] = find_widget<slider>("xp_slider").get_value();
500  utype["cost"] = find_widget<slider>("cost_slider").get_value();
501  utype["movement"] = find_widget<slider>("move_slider").get_value();
502  utype["description"] = t_string(find_widget<scroll_text>("desc_box").get_value(), current_textdomain);
503  utype["race"] = find_widget<menu_button>("race_list").get_value_string();
504  utype["alignment"] = unit_alignments::values[find_widget<menu_button>("alignment_list").get_value()];
505 
506  // Gender
507  if (find_widget<toggle_button>("gender_male").get_value()) {
508  if (find_widget<toggle_button>("gender_female").get_value()) {
509  utype["gender"] = "male,female";
510  } else {
511  utype["gender"] = "male";
512  }
513  } else {
514  if (find_widget<toggle_button>("gender_female").get_value()) {
515  utype["gender"] = "female";
516  }
517  }
518 
519  // Page 2
520 
521  utype["small_profile"] = find_widget<text_box>("path_small_profile_image").get_value();
522  utype["movement_type"] = find_widget<menu_button>("movetype_list").get_value_string();
523  utype["usage"] = find_widget<menu_button>("usage_list").get_value_string();
524 
525  if (res_toggles_.any()) {
526  config& resistances = utype.add_child("resistance");
527  int i = 0;
528  for (const auto& [key, _] : resistances_.attribute_range()) {
529  if (res_toggles_[i]) {
530  resistances[key] = resistances_[key];
531  }
532  i++;
533  }
534  }
535 
536  if (def_toggles_.any()) {
537  config& defenses = utype.add_child("defense");
538  int i = 0;
539  for (const auto& [key, _] : defenses_.attribute_range()) {
540  if (def_toggles_[i]) {
541  defenses[key] = defenses_[key];
542  }
543  i++;
544  }
545  }
546 
547  if (move_toggles_.any()) {
548  config& movement_costs = utype.add_child("movement_costs");
549  int i = 0;
550  for (const auto& [key, _] : movement_.attribute_range()) {
551  if (move_toggles_[i]) {
552  movement_costs[key] = movement_[key];
553  }
554  i++;
555  }
556  }
557 
558  std::vector<std::string> selected_abilities;
559  const auto& abilities_states = find_widget<multimenu_button>("abilities_list").get_toggle_states();
560  if (abilities_states.any()) {
561  for (size_t i = 0; i < abilities_states.size(); i++) {
562  if (abilities_states[i]) {
563  selected_abilities.push_back(abilities_list_[i]["label"]);
564  }
565  }
566  }
567  if (!selected_abilities.empty()) {
568  utype["abilities_list"] = utils::join(selected_abilities);
569  }
570 
571  // Page 3 (Attacks)
572 
573  for (const auto& [special_bits, cfg] : attacks_) {
574  config& atk = utype.add_child("attack", cfg);
575  std::vector<std::string> selected_specials;
576  for (size_t i = 0; i < special_bits.size(); i++) {
577  if (special_bits[i]) {
578  selected_specials.push_back(specials_list_[i]["label"]);
579  }
580  }
581  if (!selected_specials.empty()) {
582  atk["specials_list"] = utils::join(selected_specials);
583  }
584  }
585 }
586 
588  find_widget<slider>("resistances_slider")
589  .set_value(
590  100 - resistances_[find_widget<menu_button>("resistances_list").get_value_string()].to_int());
591 
592  find_widget<slider>("resistances_slider")
593  .set_active(res_toggles_[find_widget<menu_button>("resistances_list").get_value()]);
594 
595  find_widget<toggle_button>("resistances_checkbox")
596  .set_value(res_toggles_[find_widget<menu_button>("resistances_list").get_value()]);
597 }
598 
600  resistances_[find_widget<menu_button>("resistances_list").get_value_string()]
601  = 100 - find_widget<slider>("resistances_slider").get_value();
602 }
603 
605  bool toggle = find_widget<toggle_button>("resistances_checkbox").get_value();
606  res_toggles_[find_widget<menu_button>("resistances_list").get_value()] = toggle;
607  find_widget<slider>("resistances_slider").set_active(toggle);
608 }
609 
611  find_widget<slider>("defense_slider")
612  .set_value(
613  100 - defenses_[find_widget<menu_button>("defense_list").get_value_string()].to_int());
614 
615  find_widget<slider>("defense_slider")
616  .set_active(def_toggles_[find_widget<menu_button>("defense_list").get_value()]);
617 
618  find_widget<toggle_button>("defense_checkbox")
619  .set_value(def_toggles_[find_widget<menu_button>("defense_list").get_value()]);
620 }
621 
623  defenses_[find_widget<menu_button>("defense_list").get_value_string()]
624  = 100 - find_widget<slider>("defense_slider").get_value();
625 }
626 
628  bool toggle = find_widget<toggle_button>("defense_checkbox").get_value();
629  def_toggles_[find_widget<menu_button>("defense_list").get_value()] = toggle;
630  find_widget<slider>("defense_slider").set_active(toggle);
631 }
632 
634  find_widget<slider>("movement_costs_slider")
635  .set_value(
636  movement_[find_widget<menu_button>("movement_costs_list").get_value_string()].to_int());
637 
638  find_widget<slider>("movement_costs_slider")
639  .set_active(move_toggles_[find_widget<menu_button>("movement_costs_list").get_value()]);
640 
641  find_widget<toggle_button>("movement_costs_checkbox")
642  .set_value(move_toggles_[find_widget<menu_button>("movement_costs_list").get_value()]);
643 }
644 
646  movement_[find_widget<menu_button>("movement_costs_list").get_value_string()]
647  = find_widget<slider>("movement_costs_slider").get_value();
648 }
649 
651  bool toggle = find_widget<toggle_button>("movement_costs_checkbox").get_value();
652  move_toggles_[find_widget<menu_button>("movement_costs_list").get_value()] = toggle;
653  find_widget<slider>("movement_costs_slider").set_active(toggle);
654 }
655 
657  // Textdomain
658  std::string current_textdomain = "wesnoth-"+addon_id_;
659 
660  // Save current attack data
661  if (selected_attack_ < 1) {
662  return;
663  }
664 
665  config attack;
666  attack["name"] = find_widget<text_box>("atk_id_box").get_value();
667  attack["description"] = t_string(find_widget<text_box>("atk_name_box").get_value(), current_textdomain);
668  attack["icon"] = find_widget<text_box>("path_attack_image").get_value();
669  attack["type"] = find_widget<combobox>("attack_type_list").get_value();
670  attack["damage"] = find_widget<slider>("dmg_box").get_value();
671  attack["number"] = find_widget<slider>("dmg_num_box").get_value();
672  attack["range"] = find_widget<combobox>("range_list").get_value();
673 
674  attacks_.at(selected_attack_-1) = {
675  find_widget<multimenu_button>("weapon_specials_list").get_toggle_states(),
676  attack
677  };
678 }
679 
681  //Load data
682  if (selected_attack_ < 1) {
683  return;
684  }
685 
686  config& attack = attacks_.at(selected_attack_-1).second;
687 
688  find_widget<text_box>("atk_id_box").set_value(attack["name"]);
689  find_widget<text_box>("atk_name_box").set_value(attack["description"]);
690  find_widget<text_box>("path_attack_image").set_value(attack["icon"]);
691  update_image("attack_image");
692  find_widget<slider>("dmg_box").set_value(attack["damage"].to_int());
693  find_widget<slider>("dmg_num_box").set_value(attack["number"].to_int());
694  find_widget<combobox>("range_list").set_value(attack["range"]);
695 
697  find_widget<combobox>("attack_type_list"), resistances_list_, attack["type"]);
698 
699  find_widget<multimenu_button>("weapon_specials_list")
700  .select_options(attacks_.at(selected_attack_-1).first);
701 }
702 
704  find_widget<button>("atk_prev").set_active(selected_attack_ > 1);
705  find_widget<button>("atk_delete").set_active(selected_attack_ > 0);
706  find_widget<button>("atk_next").set_active(selected_attack_ != attacks_.size());
707 
708  if (!attacks_.empty()) {
709  std::vector<config> atk_name_list;
710  for(const auto& atk_data : attacks_) {
711  atk_name_list.emplace_back("label", atk_data.second["name"]);
712  }
713  menu_button& atk_list = find_widget<menu_button>("atk_list");
714  atk_list.set_values(atk_name_list);
715  atk_list.set_selected(selected_attack_-1, false);
716  }
717 
718  // Set index
719  const std::string new_index_str = formatter() << selected_attack_ << "/" << attacks_.size();
720  find_widget<label>("atk_number").set_label(new_index_str);
721 }
722 
724  // Textdomain
725  std::string current_textdomain = "wesnoth-"+addon_id_;
726 
727  config attack;
728  attack["name"] = find_widget<text_box>("atk_id_box").get_value();
729  attack["description"] = t_string(find_widget<text_box>("atk_name_box").get_value(), current_textdomain);
730  attack["icon"] = find_widget<text_box>("path_attack_image").get_value();
731  attack["type"] = find_widget<combobox>("attack_type_list").get_value();
732  attack["damage"] = find_widget<slider>("dmg_box").get_value();
733  attack["number"] = find_widget<slider>("dmg_num_box").get_value();
734  attack["range"] = find_widget<combobox>("range_list").get_value();
735 
737 
738  attacks_.insert(
739  attacks_.begin() + selected_attack_ - 1
740  , std::make_pair(find_widget<multimenu_button>("weapon_specials_list").get_toggle_states(), attack));
741 
742  update_index();
743 }
744 
746  if (!attacks_.empty()) {
747  attacks_.erase(attacks_.begin() + selected_attack_ - 1);
748  }
749 
750  if (attacks_.empty()) {
751  // clear fields instead since there are no attacks to show
752  selected_attack_ = 0;
753  find_widget<button>("atk_delete").set_active(false);
754  } else if (selected_attack_ == 1) {
755  // 1st attack removed, show the next one
756  next_attack();
757  } else {
758  // show previous attack otherwise
759  prev_attack();
760  }
761 
762  update_index();
763 }
764 
766  store_attack();
767 
768  if (attacks_.size() > 1) {
770  update_attacks();
771  }
772 
773  update_index();
774 }
775 
777  store_attack();
778 
779  if (selected_attack_ > 0) {
781  }
782 
783  if (attacks_.size() > 1) {
784  update_attacks();
785  }
786 
787  update_index();
788 }
789 
791  selected_attack_ = find_widget<menu_button>("atk_list").get_value()+1;
792  update_attacks();
793  update_index();
794 }
795 
796 //TODO Check if works with non-mainline movetypes
798  for(const auto& movetype : game_config_
799  .mandatory_child("units")
800  .child_range("movetype")) {
801  if (movetype["name"] == find_widget<menu_button>("movetype_list").get_value_string()) {
802  // Set resistances
803  resistances_ = movetype.mandatory_child("resistance");
805  // Set defense
806  defenses_ = movetype.mandatory_child("defense");
807  update_defenses();
808  // Set movement
809  movement_ = movetype.mandatory_child("movement_costs");
811  }
812  }
813 }
814 
816  store_attack();
817  save_unit_type();
818 
819  std::stringstream wml_stream;
820 
821  // Textdomain
822  std::string current_textdomain = "wesnoth-" + addon_id_;
823 
824  wml_stream
825  << "#textdomain " << current_textdomain << "\n"
826  << "#\n"
827  << "# This file was generated using the scenario editor.\n"
828  << "#\n";
829 
830  config_writer out(wml_stream, false);
831 
832  config& utype_cfg = type_cfg_.mandatory_child("unit_type");
833 
834  // Update movement, defense and resistance in cfg
835  if (!movement_.empty() && (move_toggles_.size() <= movement_.attribute_count()) && move_toggles_.any())
836  {
837  config& mvt_cfg = utype_cfg.add_child("movement_costs");
838  int i = 0;
839  for (const auto& [key, value] : movement_.attribute_range()) {
840  if (move_toggles_[i] == 1) {
841  mvt_cfg[key] = value;
842  }
843  i++;
844  }
845  }
846 
847  if (!defenses_.empty() && def_toggles_.any() && (def_toggles_.size() <= defenses_.attribute_count()))
848  {
849  config& def_cfg = utype_cfg.add_child("defense");
850  int i = 0;
851  for (const auto& [key, value] : defenses_.attribute_range()) {
852  if (def_toggles_[i] == 1) {
853  def_cfg[key] = value;
854  }
855  i++;
856  }
857  }
858 
859  if (!resistances_.empty() && res_toggles_.any() && (res_toggles_.size() <= resistances_.attribute_count()))
860  {
861  config& res_cfg = utype_cfg.add_child("resistance");
862  int i = 0;
863  for (const auto& [key, value] : resistances_.attribute_range()) {
864  if (res_toggles_[i] == 1) {
865  res_cfg[key] = value;
866  }
867  i++;
868  }
869  }
870 
871  out.write(type_cfg_);
872  generated_wml = wml_stream.str();
873  find_widget<scroll_text>("wml_view").set_label(generated_wml);
874 }
875 
876 void editor_edit_unit::update_image(const std::string& id_stem) {
877  std::string rel_path = find_widget<text_box>("path_"+id_stem).get_value();
878 
879  // remove IPF
880  if (rel_path.find("~") != std::string::npos) {
881  rel_path = rel_path.substr(0, rel_path.find("~"));
882  }
883 
884  int scale_size = 200; // TODO: Arbitrary, can be changed later.
885  if (rel_path.size() > 0) {
886  point img_size = ::image::get_size(::image::locator{rel_path});
887  float aspect_ratio = static_cast<float>(img_size.x)/img_size.y;
888  if(img_size.x > scale_size) {
889  rel_path.append("~SCALE(" + std::to_string(scale_size) + "," + std::to_string(scale_size*aspect_ratio) + ")");
890  } else if (img_size.y > scale_size) {
891  rel_path.append("~SCALE(" + std::to_string(scale_size/aspect_ratio) + "," + std::to_string(scale_size) + ")");
892  }
893  }
894 
895  if (id_stem == "portrait_image") {
896  // portrait image uses same [image] as unit_image
897  find_widget<image>("unit_image").set_label(rel_path);
898  } else {
899  find_widget<image>(id_stem).set_label(rel_path);
900  }
901 
903  queue_redraw();
904 }
905 
906 bool editor_edit_unit::check_id(const std::string& id) {
907  for(char c : id) {
908  if (!(std::isalnum(c) || c == '_' || c == ' ')) {
909  // One bad char means entire id string is invalid
910  return false;
911  }
912  }
913  return true;
914 }
915 
917  std::string id = find_widget<text_box>("id_box").get_value();
918  std::string name = find_widget<text_box>("name_box").get_value();
919 
920  find_widget<button>("ok").set_active(!id.empty() && !name.empty() && check_id(id));
921 
922  queue_redraw();
923 }
924 
926  const std::string& message
927  = _("Unsaved changes will be lost. Do you want to leave?");
930  }
931 }
932 
934  // Write the file
935  update_wml_view();
936 
937  std::string unit_name = type_cfg_.mandatory_child("unit_type")["name"];
938  boost::algorithm::replace_all(unit_name, " ", "_");
939 
940  // Path to <unit_type_name>.cfg
941  std::string unit_path = filesystem::get_current_editor_dir(addon_id_) + "/units/" + unit_name + filesystem::wml_extension;
942 
943  // Write to file
944  try {
946  gui2::show_transient_message("", _("Unit type saved."));
947  } catch(const filesystem::io_exception& e) {
948  gui2::show_transient_message("", e.what());
949  }
950 }
951 
953  bool& handled,
954  const SDL_Keycode key,
955  SDL_Keymod modifier)
956 {
957  #ifdef __APPLE__
958  // Idiomatic modifier key in macOS computers.
959  const SDL_Keycode modifier_key = KMOD_GUI;
960  #else
961  // Idiomatic modifier key in Microsoft desktop environments. Common in
962  // GNU/Linux as well, to some extent.
963  const SDL_Keycode modifier_key = KMOD_CTRL;
964  #endif
965 
966  // Ctrl+O shortcut for Load Unit Type
967  switch(key) {
968  case SDLK_o:
969  if (modifier & modifier_key) {
970  handled = true;
971  load_unit_type();
972  }
973  break;
974  }
975 
976 }
977 
978 }
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
std::size_t attribute_count() const
Count the number of non-blank attributes.
Definition: config.cpp:307
const_attr_itors attribute_range() const
Definition: config.cpp:740
bool empty() const
Definition: config.cpp:823
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
void clear()
Definition: config.cpp:802
std::ostringstream wrapper.
Definition: formatter.hpp:40
A class grating read only view to a vector of config objects, viewed as one config with all children ...
const config & mandatory_child(std::string_view key) const
Simple push button.
Definition: button.hpp:36
Class for a combobox.
Definition: combobox.hpp:37
void set_values(const std::vector<::config > &values, unsigned selected=0)
Definition: combobox.cpp:278
Dialog that allows user to create custom unit types.
Definition: edit_unit.hpp:33
unsigned int selected_attack_
0 means there are no attacks.
Definition: edit_unit.hpp:65
void write()
Write the cfg file.
Definition: edit_unit.cpp:933
void quit_confirmation()
Quit confirmation.
Definition: edit_unit.cpp:925
void set_selected_from_string(menu_button &list, std::vector< config > values, const std::string &item)
Definition: edit_unit.hpp:126
std::vector< config > defense_list_
Definition: edit_unit.hpp:56
boost::dynamic_bitset res_toggles_
Used to control checkboxes for various resistances, defences, etc.
Definition: edit_unit.hpp:54
const game_config_view & game_config_
Definition: edit_unit.hpp:44
std::vector< std::pair< boost::dynamic_bitset<>, config > > attacks_
Definition: edit_unit.hpp:59
std::vector< config > abilities_list_
Definition: edit_unit.hpp:57
void store_attack()
Callbacks for attack page.
Definition: edit_unit.cpp:656
std::vector< config > movetype_list_
Definition: edit_unit.hpp:56
std::vector< config > race_list_
Definition: edit_unit.hpp:56
void update_defenses()
Callbacks for defense list.
Definition: edit_unit.cpp:610
std::string generated_wml
Generated WML.
Definition: edit_unit.hpp:62
void save_unit_type()
Save Unit Type data to cfg.
Definition: edit_unit.cpp:483
std::vector< config > align_list_
Definition: edit_unit.hpp:56
void button_state_change()
Callback to enable/disable OK button if ID/Name is invalid.
Definition: edit_unit.cpp:916
void signal_handler_sdl_key_down(const event::ui_event, bool &handled, const SDL_Keycode key, SDL_Keymod modifier)
Definition: edit_unit.cpp:952
editor_edit_unit(const game_config_view &game_config, const std::string &addon_id)
Definition: edit_unit.cpp:57
void select_file(const std::string &default_dir, const std::string &id_stem)
Callback for file select button.
Definition: edit_unit.cpp:292
boost::dynamic_bitset move_toggles_
Definition: edit_unit.hpp:54
void update_movement_costs()
Callbacks for movement list.
Definition: edit_unit.cpp:633
std::vector< config > resistances_list_
Definition: edit_unit.hpp:56
void on_page_select()
Callback when an tab item in the "page" listbox is selected.
Definition: edit_unit.cpp:282
void update_resistances()
Callback for resistance list.
Definition: edit_unit.cpp:587
bool check_id(const std::string &id)
Utility method to check if ID contains any invalid characters.
Definition: edit_unit.cpp:906
const std::string & addon_id_
Definition: edit_unit.hpp:45
std::vector< config > specials_list_
Definition: edit_unit.hpp:57
boost::dynamic_bitset def_toggles_
Definition: edit_unit.hpp:54
void load_unit_type()
Load Unit Type data from cfg.
Definition: edit_unit.cpp:334
virtual void pre_show() override
Actions to be taken before showing the window.
Definition: edit_unit.cpp:74
void update_wml_view()
Update wml preview.
Definition: edit_unit.cpp:815
std::vector< config > usage_type_list_
Definition: edit_unit.hpp:56
void load_movetype()
Callback for loading movetype data in UI.
Definition: edit_unit.cpp:797
void update_image(const std::string &id_stem)
Callback for image update.
Definition: edit_unit.cpp:876
file_dialog & set_ok_label(const std::string &value)
Sets the OK button label.
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
file_dialog & set_read_only(bool value)
Whether to provide user interface elements for manipulating existing objects.
std::string path() const
Gets the current file selection.
Main class to show messages to the user.
Definition: message.hpp:36
@ yes_no_buttons
Shows a yes and no button.
Definition: message.hpp:81
Abstract base class for all modal dialogs.
bool show(const unsigned auto_close_time=0)
Shows the window.
At the moment two kinds of tips are known:
Definition: tooltip.cpp:41
static std::unique_ptr< units_dialog > build_create_dialog(const std::vector< const unit_type * > &types_list)
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.
const t_string & tooltip() const
A container widget that shows one of its pages of widgets depending on which tab the user clicked.
unsigned get_active_tab_index()
void queue_redraw()
Indicates that this widget should be redrawn.
Definition: widget.cpp:464
void set_retval(const int retval, const bool close_window=true)
Sets there return value of the window.
Definition: window.hpp:387
void invalidate_layout()
Updates the size of the window.
Definition: window.cpp:759
static const std::string & type()
Static type getter that does not rely on the widget being constructed.
Generic locator abstracting the location of an image.
Definition: picture.hpp:59
The basic "size" of the unit - flying, small land, large land, etc.
Definition: movetype.hpp:44
const std::map< std::string, config > & abilities() const
Definition: types.hpp:409
const movement_type_map & movement_types() const
Definition: types.hpp:408
const race_map & races() const
Definition: types.hpp:407
const std::vector< const unit_type * > types_list() const
Definition: types.hpp:398
const std::map< std::string, config > & specials() const
Definition: types.hpp:410
const config * cfg
Declarations for File-IO.
std::size_t i
Definition: function.cpp:1031
static std::string _(const char *str)
Definition: gettext.hpp:97
const std::string wml_extension
Definition: filesystem.cpp:281
void copy_file(const std::string &src, const std::string &dest)
Read a file and then writes it back out.
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_current_editor_dir(const std::string &addon_id)
utils::optional< std::string > to_asset_path(const std::string &path, const std::string &addon_id, const std::string &asset_type)
Helper function to convert absolute path to wesnoth relative path.
Game configuration data as global variables.
Definition: build_info.cpp:67
std::string path
Definition: filesystem.cpp:106
REGISTER_DIALOG(editor_edit_unit)
ui_event
The event sent to the dispatcher.
Definition: handler.hpp:115
void connect_signal_notify_modified(dispatcher &dispatcher, const signal_notification &signal)
Connects a signal handler for getting a notification upon modification.
Definition: dispatcher.cpp:189
void connect_signal_mouse_left_click(dispatcher &dispatcher, const signal &signal)
Connects a signal handler for a left mouse button click.
Definition: dispatcher.cpp:163
std::vector< game_tip > load(const config &cfg)
Loads the tips from a config.
Definition: tips.cpp:37
void show_transient_message(const std::string &title, const std::string &message, const std::string &image, const bool message_use_markup, const bool title_use_markup)
Shows a transient message to the user.
void show_message(const std::string &title, const std::string &msg, const std::string &button_caption, const bool auto_close, const bool message_use_markup, const bool title_use_markup)
Shows a message to the user.
Definition: message.cpp:148
@ OK
Dialog was closed with the OK button.
Definition: retval.hpp:35
@ CANCEL
Dialog was closed with the CANCEL button.
Definition: retval.hpp:38
point get_size(const locator &i_locator, bool skip_cache)
Returns the width and height of an image.
Definition: picture.cpp:811
constexpr auto values
Definition: ranges.hpp:46
std::string join(const T &v, const std::string &s=",")
Generates a new string joining container items in a list.
static config unit_name(const unit *u)
Definition: reports.cpp:163
An exception object used when an IO error occurs.
Definition: filesystem.hpp:67
Holds a 2D point.
Definition: point.hpp:25
static std::string get_string(enum_type key)
Converts a enum to its string equivalent.
Definition: enum_base.hpp:46
mock_char c
unit_type_data unit_types
Definition: types.cpp:1494
#define e