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 race_map::value_type& i : unit_types.races()) {
96  const std::string& race_name = i.second.id();
97  race_list_.emplace_back("label", race_name, "icon", i.second.get_icon_path_stem() + "_30.png");
98  }
99 
100  if (!race_list_.empty()) {
101  races.set_values(race_list_);
102  }
103 
104  button& load = find_widget<button>("load_unit_type");
105  std::stringstream tooltip;
106  tooltip << t_string("Hotkey(s): ", "wesnoth");
107  #ifdef __APPLE__
108  tooltip << "cmd+o";
109  #else
110  tooltip << "ctrl+o";
111  #endif
112  load.set_tooltip(tooltip.str());
114 
116  find_widget<button>("browse_unit_image"),
117  std::bind(&editor_edit_unit::select_file, this, "data/core/images/units", "unit_image"));
119  find_widget<button>("preview_unit_image"),
120  std::bind(&editor_edit_unit::update_image, this, "unit_image"));
122  find_widget<button>("browse_portrait_image"),
123  std::bind(&editor_edit_unit::select_file, this, "data/core/images/portraits", "portrait_image"));
125  find_widget<button>("preview_portrait_image"),
126  std::bind(&editor_edit_unit::update_image, this, "portrait_image"));
127 
129  find_widget<text_box>("name_box"),
130  std::bind(&editor_edit_unit::button_state_change, this));
132  find_widget<text_box>("id_box"),
133  std::bind(&editor_edit_unit::button_state_change, this));
134 
135  //
136  // Advanced Tab
137  //
138 
139  menu_button& movetypes = find_widget<menu_button>("movetype_list");
140  for(const auto& mt : unit_types.movement_types()) {
141  movetype_list_.emplace_back("label", mt.first);
142  }
143 
144  if (!movetype_list_.empty()) {
145  movetypes.set_values(movetype_list_);
146  }
147 
148  menu_button& defenses = find_widget<menu_button>("defense_list");
149  const config& defense_attr = game_config_
150  .mandatory_child("units")
151  .mandatory_child("movetype")
152  .mandatory_child("defense");
153  for (const auto& [key, _] : defense_attr.attribute_range()) {
154  defense_list_.emplace_back("label", key);
155  }
156 
157  menu_button& movement_costs = find_widget<menu_button>("movement_costs_list");
158  if (!defense_list_.empty()) {
159  defenses.set_values(defense_list_);
160  def_toggles_.resize(defense_list_.size());
161  movement_costs.set_values(defense_list_);
162  move_toggles_.resize(defense_list_.size());
163  }
164 
165  menu_button& resistances = find_widget<menu_button>("resistances_list");
166 
167  const config& resistances_attr = game_config_
168  .mandatory_child("units")
169  .mandatory_child("movetype")
170  .mandatory_child("resistance");
171  for (const auto& [key, _] : resistances_attr.attribute_range()) {
172  resistances_list_.emplace_back("label", key, "icon", "icons/profiles/" + key + ".png");
173  }
174 
175  if (!resistances_list_.empty()) {
176  resistances.set_values(resistances_list_);
177  res_toggles_.resize(resistances_list_.size());
178  }
179 
180  menu_button& usage_types = find_widget<menu_button>("usage_list");
181  usage_type_list_.emplace_back("label", _("scout"));
182  usage_type_list_.emplace_back("label", _("fighter"));
183  usage_type_list_.emplace_back("label", _("archer"));
184  usage_type_list_.emplace_back("label", _("mixed fighter"));
185  usage_type_list_.emplace_back("label", _("healer"));
186  usage_types.set_values(usage_type_list_);
187 
188  multimenu_button& abilities = find_widget<multimenu_button>("abilities_list");
189  abilities.set_values(abilities_list_);
190 
192  find_widget<button>("browse_small_profile_image"),
193  std::bind(&editor_edit_unit::select_file, this, "data/core/images/portraits", "small_profile_image"));
195  find_widget<button>("preview_small_profile_image"),
196  std::bind(&editor_edit_unit::update_image, this, "small_profile_image"));
198  find_widget<button>("load_movetype"),
199  std::bind(&editor_edit_unit::load_movetype, this));
201  find_widget<slider>("resistances_slider"),
202  std::bind(&editor_edit_unit::store_resistances, this));
204  find_widget<menu_button>("resistances_list"),
205  std::bind(&editor_edit_unit::update_resistances, this));
207  find_widget<toggle_button>("resistances_checkbox"),
209 
211  find_widget<slider>("defense_slider"),
212  std::bind(&editor_edit_unit::store_defenses, this));
214  find_widget<menu_button>("defense_list"),
215  std::bind(&editor_edit_unit::update_defenses, this));
217  find_widget<toggle_button>("defense_checkbox"),
218  std::bind(&editor_edit_unit::enable_defense_slider, this));
219 
221  find_widget<slider>("movement_costs_slider"),
222  std::bind(&editor_edit_unit::store_movement_costs, this));
224  find_widget<menu_button>("movement_costs_list"),
225  std::bind(&editor_edit_unit::update_movement_costs, this));
227  find_widget<toggle_button>("movement_costs_checkbox"),
228  std::bind(&editor_edit_unit::enable_movement_slider, this));
229 
230  if (!res_toggles_.empty()) {
232  }
233 
234  if (!def_toggles_.empty()) {
236  }
237 
238  if (!move_toggles_.empty()) {
240  }
241 
242  //
243  // Attack Tab
244  //
245 
246  multimenu_button& specials = find_widget<multimenu_button>("weapon_specials_list");
247  specials.set_values(specials_list_);
248 
249  combobox& attack_types = find_widget<combobox>("attack_type_list");
250  if (!resistances_list_.empty()) {
251  attack_types.set_values(resistances_list_);
252  }
253 
254  // Connect signals
256  find_widget<button>("browse_attack_image"),
257  std::bind(&editor_edit_unit::select_file, this, "data/core/images/attacks", "attack_image"));
259  find_widget<button>("preview_attack_image"),
260  std::bind(&editor_edit_unit::update_image, this, "attack_image"));
262  find_widget<menu_button>("atk_list"),
263  std::bind(&editor_edit_unit::select_attack, this));
265  find_widget<button>("atk_new"),
266  std::bind(&editor_edit_unit::add_attack, this));
268  find_widget<button>("atk_delete"),
269  std::bind(&editor_edit_unit::delete_attack, this));
271  find_widget<button>("atk_next"),
272  std::bind(&editor_edit_unit::next_attack, this));
274  find_widget<button>("atk_prev"),
275  std::bind(&editor_edit_unit::prev_attack, this));
276 
277  update_index();
278 
279  // Disable OK button at start, since ID and Name boxes are empty
281 }
282 
284 {
285  save_unit_type();
286 
287  tab_container& tabs = find_widget<tab_container>("tabs");
288  if (tabs.get_active_tab_index() == 3) {
289  update_wml_view();
290  }
291 }
292 
293 void editor_edit_unit::select_file(const std::string& default_dir, const std::string& id_stem)
294 {
296  dlg.set_title(_("Choose File"))
297  .set_ok_label(_("Select"))
298  .set_path(default_dir)
299  .set_read_only(true);
300 
301  // If the file is found inside Wesnoth's data, give its relative path
302  // if not, ask user if they want to copy it into their addon.
303  // If yes, copy and return correct relative path inside addon.
304  // return empty otherwise.
305  auto find_or_copy = [](const std::string& path, const std::string& addon_id, const std::string& type) {
306  const std::string message
307  = _("This file is outside Wesnoth’s data dirs. Do you wish to copy it into your add-on?");
308  const auto optional_path = filesystem::to_asset_path(path, addon_id, type);
309 
310  if(optional_path.has_value()) {
311  return optional_path.value();
314  output_path /= type;
315  output_path /= boost::filesystem::path(path).filename();
316  filesystem::copy_file(path, output_path.string());
317  return output_path.filename().string();
318  } else {
319  return std::string();
320  }
321  };
322 
323  if (dlg.show()) {
324  if((id_stem == "unit_image")
325  || (id_stem == "portrait_image")
326  || (id_stem == "small_profile_image")
327  || (id_stem == "attack_image"))
328  {
329  find_widget<text_box>("path_"+id_stem).set_value(find_or_copy(dlg.path(), addon_id_, "images"));
330  update_image(id_stem);
331  }
332  }
333 }
334 
336  const auto& all_type_list = unit_types.types_list();
337  const auto& type_select = gui2::dialogs::units_dialog::build_create_dialog(all_type_list);
338  type_select->set_ok_label("Load");
339 
340  if (!type_select->show() && !type_select->is_selected()) {
341  return;
342  }
343 
344  const auto& type = all_type_list[type_select->get_selected_index()];
345 
346  find_widget<text_box>("id_box").set_value(type->id());
347  find_widget<text_box>("name_box").set_value(type->type_name().base_str());
348  find_widget<spinner>("level_box").set_value(type->level());
349  find_widget<slider>("cost_slider").set_value(type->cost());
350  find_widget<text_box>("adv_box").set_value(utils::join(type->advances_to()));
351  find_widget<slider>("hp_slider").set_value(type->hitpoints());
352  find_widget<slider>("xp_slider").set_value(type->experience_needed());
353  find_widget<slider>("move_slider").set_value(type->movement());
354  find_widget<scroll_text>("desc_box").set_value(type->unit_description().base_str());
355  find_widget<text_box>("path_unit_image").set_value(type->image());
356  find_widget<text_box>("path_portrait_image").set_value(type->big_profile());
357 
358  for (const auto& gender : type->genders())
359  {
360  if (gender == unit_race::GENDER::MALE) {
361  find_widget<toggle_button>("gender_male").set_value(true);
362  }
363 
364  if (gender == unit_race::GENDER::FEMALE) {
365  find_widget<toggle_button>("gender_female").set_value(true);
366  }
367  }
368 
370  find_widget<menu_button>("race_list"),
371  race_list_,
372  type->race_id());
373 
375  find_widget<menu_button>("alignment_list"),
376  align_list_,
377  unit_alignments::get_string(type->alignment()));
378 
379  update_image("unit_image");
380 
381  boost::dynamic_bitset<> enabled_abilities(abilities_list_.size());
382  for (size_t i = 0; i < abilities_list_.size(); i++) {
383  for (auto special : type->abilities()) {
384  const std::string id = special->cfg()["unique_id"].str(special->id());
385  if (abilities_list_[i]["label"] == id) {
386  enabled_abilities[i] = true;
387  }
388  }
389  }
390  find_widget<multimenu_button>("abilities_list").select_options(enabled_abilities);
391 
392  find_widget<text_box>("path_small_profile_image").set_value(type->small_profile());
393 
395  find_widget<menu_button>("movetype_list"),
397  type->movement_type_id());
398 
399  config cfg;
400  type->movement_type().write(cfg, false);
401  movement_ = cfg.mandatory_child("movement_costs");
402  defenses_ = cfg.mandatory_child("defense");
403  resistances_ = cfg.mandatory_child("resistance");
404 
405  // Overrides for resistance/defense/movement costs
406  for (unsigned i = 0; i < resistances_list_.size(); i++) {
407  if (!type->get_cfg().has_child("resistance")) {
408  break;
409  }
410 
411  for (const auto& [key, _] : type->get_cfg().mandatory_child("resistance").attribute_range()) {
412  if (resistances_list_.at(i)["label"] == key) {
413  res_toggles_[i] = 1;
414  }
415  }
416  }
417 
418  for (unsigned i = 0; i < defense_list_.size(); i++) {
419  if (type->get_cfg().has_child("defense")) {
420  for (const auto& [key, _] : type->get_cfg().mandatory_child("defense").attribute_range()) {
421  if (defense_list_.at(i)["label"] == key) {
422  def_toggles_[i] = 1;
423  }
424  }
425  }
426 
427  if (type->get_cfg().has_child("movement_costs")) {
428  for (const auto& [key, _] : type->get_cfg().mandatory_child("movement_costs").attribute_range()) {
429  if (defense_list_.at(i)["label"] == key) {
430  move_toggles_[i] = 1;
431  }
432  }
433  }
434  }
435 
437  update_defenses();
439 
441  find_widget<menu_button>("usage_list"),
443  type->usage());
444 
445  update_image("small_profile_image");
446 
447  attacks_.clear();
448  for(const auto& atk : type->attacks())
449  {
450  config attack;
451  attack["name"] = atk.id();
452  attack["description"] = atk.name().base_str();
453  attack["icon"] = atk.icon();
454  attack["range"] = atk.range();
455  attack["damage"] = atk.damage();
456  attack["number"] = atk.num_attacks();
457  attack["type"] = atk.type();
458 
459  boost::dynamic_bitset<> enabled_specials(specials_list_.size());
460 
461  for (size_t i = 0; i < specials_list_.size(); i++) {
462  for (auto special : atk.specials()) {
463  const std::string id = special->cfg()["unique_id"].str(special->id());
464  if (specials_list_[i]["label"] == id) {
465  enabled_specials[i] = true;
466  }
467  }
468  }
469 
470  attacks_.push_back(std::make_pair(enabled_specials, attack));
471  }
472 
473  if (!type->attacks().empty()) {
474  selected_attack_ = 1;
475  update_attacks();
476  }
477 
478  update_index();
479 
482 }
483 
485 
486  // Clear the config
487  type_cfg_.clear();
488 
489  // Textdomain
490  std::string current_textdomain = "wesnoth-" + addon_id_;
491 
492  config& utype = type_cfg_.add_child("unit_type");
493  utype["id"] = find_widget<text_box>("id_box").get_value();
494  utype["name"] = t_string(find_widget<text_box>("name_box").get_value(), current_textdomain);
495  utype["image"] = find_widget<text_box>("path_unit_image").get_value();
496  utype["profile"] = find_widget<text_box>("path_portrait_image").get_value();
497  utype["level"] = find_widget<spinner>("level_box").get_value();
498  utype["advances_to"] = find_widget<text_box>("adv_box").get_value();
499  utype["hitpoints"] = find_widget<slider>("hp_slider").get_value();
500  utype["experience"] = find_widget<slider>("xp_slider").get_value();
501  utype["cost"] = find_widget<slider>("cost_slider").get_value();
502  utype["movement"] = find_widget<slider>("move_slider").get_value();
503  utype["description"] = t_string(find_widget<scroll_text>("desc_box").get_value(), current_textdomain);
504  utype["race"] = find_widget<menu_button>("race_list").get_value_string();
505  utype["alignment"] = unit_alignments::values[find_widget<menu_button>("alignment_list").get_value()];
506 
507  // Gender
508  if (find_widget<toggle_button>("gender_male").get_value()) {
509  if (find_widget<toggle_button>("gender_female").get_value()) {
510  utype["gender"] = "male,female";
511  } else {
512  utype["gender"] = "male";
513  }
514  } else {
515  if (find_widget<toggle_button>("gender_female").get_value()) {
516  utype["gender"] = "female";
517  }
518  }
519 
520  // Page 2
521 
522  utype["small_profile"] = find_widget<text_box>("path_small_profile_image").get_value();
523  utype["movement_type"] = find_widget<menu_button>("movetype_list").get_value_string();
524  utype["usage"] = find_widget<menu_button>("usage_list").get_value_string();
525 
526  if (res_toggles_.any()) {
527  config& resistances = utype.add_child("resistance");
528  int i = 0;
529  for (const auto& [key, _] : resistances_.attribute_range()) {
530  if (res_toggles_[i]) {
531  resistances[key] = resistances_[key];
532  }
533  i++;
534  }
535  }
536 
537  if (def_toggles_.any()) {
538  config& defenses = utype.add_child("defense");
539  int i = 0;
540  for (const auto& [key, _] : defenses_.attribute_range()) {
541  if (def_toggles_[i]) {
542  defenses[key] = defenses_[key];
543  }
544  i++;
545  }
546  }
547 
548  if (move_toggles_.any()) {
549  config& movement_costs = utype.add_child("movement_costs");
550  int i = 0;
551  for (const auto& [key, _] : movement_.attribute_range()) {
552  if (move_toggles_[i]) {
553  movement_costs[key] = movement_[key];
554  }
555  i++;
556  }
557  }
558 
559  std::vector<std::string> selected_abilities;
560  const auto& abilities_states = find_widget<multimenu_button>("abilities_list").get_toggle_states();
561  if (abilities_states.any()) {
562  for (size_t i = 0; i < abilities_states.size(); i++) {
563  if (abilities_states[i]) {
564  selected_abilities.push_back(abilities_list_[i]["label"]);
565  }
566  }
567  }
568  if (!selected_abilities.empty()) {
569  utype["abilities_list"] = utils::join(selected_abilities);
570  }
571 
572  // Page 3 (Attacks)
573 
574  for (const auto& [special_bits, cfg] : attacks_) {
575  config& atk = utype.add_child("attack", cfg);
576  std::vector<std::string> selected_specials;
577  for (size_t i = 0; i < special_bits.size(); i++) {
578  if (special_bits[i]) {
579  selected_specials.push_back(specials_list_[i]["label"]);
580  }
581  }
582  if (!selected_specials.empty()) {
583  atk["specials_list"] = utils::join(selected_specials);
584  }
585  }
586 }
587 
589  find_widget<slider>("resistances_slider")
590  .set_value(
591  100 - resistances_[find_widget<menu_button>("resistances_list").get_value_string()].to_int());
592 
593  find_widget<slider>("resistances_slider")
594  .set_active(res_toggles_[find_widget<menu_button>("resistances_list").get_value()]);
595 
596  find_widget<toggle_button>("resistances_checkbox")
597  .set_value(res_toggles_[find_widget<menu_button>("resistances_list").get_value()]);
598 }
599 
601  resistances_[find_widget<menu_button>("resistances_list").get_value_string()]
602  = 100 - find_widget<slider>("resistances_slider").get_value();
603 }
604 
606  bool toggle = find_widget<toggle_button>("resistances_checkbox").get_value();
607  res_toggles_[find_widget<menu_button>("resistances_list").get_value()] = toggle;
608  find_widget<slider>("resistances_slider").set_active(toggle);
609 }
610 
612  find_widget<slider>("defense_slider")
613  .set_value(
614  100 - defenses_[find_widget<menu_button>("defense_list").get_value_string()].to_int());
615 
616  find_widget<slider>("defense_slider")
617  .set_active(def_toggles_[find_widget<menu_button>("defense_list").get_value()]);
618 
619  find_widget<toggle_button>("defense_checkbox")
620  .set_value(def_toggles_[find_widget<menu_button>("defense_list").get_value()]);
621 }
622 
624  defenses_[find_widget<menu_button>("defense_list").get_value_string()]
625  = 100 - find_widget<slider>("defense_slider").get_value();
626 }
627 
629  bool toggle = find_widget<toggle_button>("defense_checkbox").get_value();
630  def_toggles_[find_widget<menu_button>("defense_list").get_value()] = toggle;
631  find_widget<slider>("defense_slider").set_active(toggle);
632 }
633 
635  find_widget<slider>("movement_costs_slider")
636  .set_value(
637  movement_[find_widget<menu_button>("movement_costs_list").get_value_string()].to_int());
638 
639  find_widget<slider>("movement_costs_slider")
640  .set_active(move_toggles_[find_widget<menu_button>("movement_costs_list").get_value()]);
641 
642  find_widget<toggle_button>("movement_costs_checkbox")
643  .set_value(move_toggles_[find_widget<menu_button>("movement_costs_list").get_value()]);
644 }
645 
647  movement_[find_widget<menu_button>("movement_costs_list").get_value_string()]
648  = find_widget<slider>("movement_costs_slider").get_value();
649 }
650 
652  bool toggle = find_widget<toggle_button>("movement_costs_checkbox").get_value();
653  move_toggles_[find_widget<menu_button>("movement_costs_list").get_value()] = toggle;
654  find_widget<slider>("movement_costs_slider").set_active(toggle);
655 }
656 
658  // Textdomain
659  std::string current_textdomain = "wesnoth-"+addon_id_;
660 
661  // Save current attack data
662  if (selected_attack_ < 1) {
663  return;
664  }
665 
666  config attack;
667  attack["name"] = find_widget<text_box>("atk_id_box").get_value();
668  attack["description"] = t_string(find_widget<text_box>("atk_name_box").get_value(), current_textdomain);
669  attack["icon"] = find_widget<text_box>("path_attack_image").get_value();
670  attack["type"] = find_widget<combobox>("attack_type_list").get_value();
671  attack["damage"] = find_widget<slider>("dmg_box").get_value();
672  attack["number"] = find_widget<slider>("dmg_num_box").get_value();
673  const std::string range_val = find_widget<combobox>("range_list").get_value();
674  if (range_val == "Ranged" || range_val == "ranged") {
675  attack["range"] = "ranged";
676  } else if (range_val == "Melee" || range_val == "melee") {
677  attack["range"] = "melee";
678  } else {
679  attack["range"] = range_val;
680  }
681 
682  attacks_.at(selected_attack_-1) = {
683  find_widget<multimenu_button>("weapon_specials_list").get_toggle_states(),
684  attack
685  };
686 }
687 
689  //Load data
690  if (selected_attack_ < 1) {
691  return;
692  }
693 
694  config& attack = attacks_.at(selected_attack_-1).second;
695 
696  find_widget<text_box>("atk_id_box").set_value(attack["name"]);
697  find_widget<text_box>("atk_name_box").set_value(attack["description"]);
698  find_widget<text_box>("path_attack_image").set_value(attack["icon"]);
699  update_image("attack_image");
700  find_widget<slider>("dmg_box").set_value(attack["damage"].to_int());
701  find_widget<slider>("dmg_num_box").set_value(attack["number"].to_int());
702  find_widget<combobox>("range_list").set_value(attack["range"]);
703 
705  find_widget<combobox>("attack_type_list"), resistances_list_, attack["type"]);
706 
707  find_widget<multimenu_button>("weapon_specials_list")
708  .select_options(attacks_.at(selected_attack_-1).first);
709 }
710 
712  find_widget<button>("atk_prev").set_active(selected_attack_ > 1);
713  find_widget<button>("atk_delete").set_active(selected_attack_ > 0);
714  find_widget<button>("atk_next").set_active(selected_attack_ != attacks_.size());
715 
716  if (!attacks_.empty()) {
717  std::vector<config> atk_name_list;
718  for(const auto& atk_data : attacks_) {
719  atk_name_list.emplace_back("label", atk_data.second["name"]);
720  }
721  menu_button& atk_list = find_widget<menu_button>("atk_list");
722  atk_list.set_values(atk_name_list);
723  atk_list.set_selected(selected_attack_-1, false);
724  }
725 
726  // Set index
727  const std::string new_index_str = formatter() << selected_attack_ << "/" << attacks_.size();
728  find_widget<label>("atk_number").set_label(new_index_str);
729 }
730 
732  // Textdomain
733  std::string current_textdomain = "wesnoth-"+addon_id_;
734 
735  config attack;
736  attack["name"] = find_widget<text_box>("atk_id_box").get_value();
737  attack["description"] = t_string(find_widget<text_box>("atk_name_box").get_value(), current_textdomain);
738  attack["icon"] = find_widget<text_box>("path_attack_image").get_value();
739  attack["type"] = find_widget<combobox>("attack_type_list").get_value();
740  attack["damage"] = find_widget<slider>("dmg_box").get_value();
741  attack["number"] = find_widget<slider>("dmg_num_box").get_value();
742  const std::string range_val = find_widget<combobox>("range_list").get_value();
743  if (range_val == "Ranged" || range_val == "ranged") {
744  attack["range"] = "ranged";
745  } else if (range_val == "Melee" || range_val == "melee") {
746  attack["range"] = "melee";
747  } else {
748  attack["range"] = range_val;
749  }
750 
752 
753  attacks_.insert(
754  attacks_.begin() + selected_attack_ - 1
755  , std::make_pair(find_widget<multimenu_button>("weapon_specials_list").get_toggle_states(), attack));
756 
757  update_index();
758 }
759 
761  if (!attacks_.empty()) {
762  attacks_.erase(attacks_.begin() + selected_attack_ - 1);
763  }
764 
765  if (attacks_.empty()) {
766  // clear fields instead since there are no attacks to show
767  selected_attack_ = 0;
768  find_widget<button>("atk_delete").set_active(false);
769  } else if (selected_attack_ == 1) {
770  // 1st attack removed, show the next one
771  next_attack();
772  } else {
773  // show previous attack otherwise
774  prev_attack();
775  }
776 
777  update_index();
778 }
779 
781  store_attack();
782 
783  if (attacks_.size() > 1) {
785  update_attacks();
786  }
787 
788  update_index();
789 }
790 
792  store_attack();
793 
794  if (selected_attack_ > 0) {
796  }
797 
798  if (attacks_.size() > 1) {
799  update_attacks();
800  }
801 
802  update_index();
803 }
804 
806  selected_attack_ = find_widget<menu_button>("atk_list").get_value()+1;
807  update_attacks();
808  update_index();
809 }
810 
811 //TODO Check if works with non-mainline movetypes
813  for(const auto& movetype : game_config_
814  .mandatory_child("units")
815  .child_range("movetype")) {
816  if (movetype["name"] == find_widget<menu_button>("movetype_list").get_value_string()) {
817  // Set resistances
818  resistances_ = movetype.mandatory_child("resistance");
820  // Set defense
821  defenses_ = movetype.mandatory_child("defense");
822  update_defenses();
823  // Set movement
824  movement_ = movetype.mandatory_child("movement_costs");
826  }
827  }
828 }
829 
831  store_attack();
832  save_unit_type();
833 
834  std::stringstream wml_stream;
835 
836  // Textdomain
837  std::string current_textdomain = "wesnoth-" + addon_id_;
838 
839  wml_stream
840  << "#textdomain " << current_textdomain << "\n"
841  << "#\n"
842  << "# This file was generated using the scenario editor.\n"
843  << "#\n";
844 
845  config_writer out(wml_stream, false);
846 
847  config& utype_cfg = type_cfg_.mandatory_child("unit_type");
848 
849  // Update movement, defense and resistance in cfg
850  if (!movement_.empty() && (move_toggles_.size() <= movement_.attribute_count()) && move_toggles_.any())
851  {
852  config& mvt_cfg = utype_cfg.add_child("movement_costs");
853  int i = 0;
854  for (const auto& [key, value] : movement_.attribute_range()) {
855  if (move_toggles_[i] == 1) {
856  mvt_cfg[key] = value;
857  }
858  i++;
859  }
860  }
861 
862  if (!defenses_.empty() && def_toggles_.any() && (def_toggles_.size() <= defenses_.attribute_count()))
863  {
864  config& def_cfg = utype_cfg.add_child("defense");
865  int i = 0;
866  for (const auto& [key, value] : defenses_.attribute_range()) {
867  if (def_toggles_[i] == 1) {
868  def_cfg[key] = value;
869  }
870  i++;
871  }
872  }
873 
874  if (!resistances_.empty() && res_toggles_.any() && (res_toggles_.size() <= resistances_.attribute_count()))
875  {
876  config& res_cfg = utype_cfg.add_child("resistance");
877  int i = 0;
878  for (const auto& [key, value] : resistances_.attribute_range()) {
879  if (res_toggles_[i] == 1) {
880  res_cfg[key] = value;
881  }
882  i++;
883  }
884  }
885 
886  out.write(type_cfg_);
887  generated_wml = wml_stream.str();
888  find_widget<scroll_text>("wml_view").set_label(generated_wml);
889 }
890 
891 void editor_edit_unit::update_image(const std::string& id_stem) {
892  std::string rel_path = find_widget<text_box>("path_"+id_stem).get_value();
893 
894  // remove IPF
895  if (rel_path.find("~") != std::string::npos) {
896  rel_path = rel_path.substr(0, rel_path.find("~"));
897  }
898 
899  int scale_size = 200; // TODO: Arbitrary, can be changed later.
900  if (rel_path.size() > 0) {
901  point img_size = ::image::get_size(::image::locator{rel_path});
902  float aspect_ratio = static_cast<float>(img_size.x)/img_size.y;
903  if(img_size.x > scale_size) {
904  rel_path.append("~SCALE(" + std::to_string(scale_size) + "," + std::to_string(scale_size*aspect_ratio) + ")");
905  } else if (img_size.y > scale_size) {
906  rel_path.append("~SCALE(" + std::to_string(scale_size/aspect_ratio) + "," + std::to_string(scale_size) + ")");
907  }
908  }
909 
910  if (id_stem == "portrait_image") {
911  // portrait image uses same [image] as unit_image
912  find_widget<image>("unit_image").set_label(rel_path);
913  } else {
914  find_widget<image>(id_stem).set_label(rel_path);
915  }
916 
918  queue_redraw();
919 }
920 
921 bool editor_edit_unit::check_id(const std::string& id) {
922  for(char c : id) {
923  if (!(std::isalnum(c) || c == '_' || c == ' ')) {
924  // One bad char means entire id string is invalid
925  return false;
926  }
927  }
928  return true;
929 }
930 
932  std::string id = find_widget<text_box>("id_box").get_value();
933  std::string name = find_widget<text_box>("name_box").get_value();
934 
935  find_widget<button>("ok").set_active(!id.empty() && !name.empty() && check_id(id));
936 
937  queue_redraw();
938 }
939 
941  const std::string& message
942  = _("Unsaved changes will be lost. Do you want to leave?");
945  }
946 }
947 
949  // Write the file
950  update_wml_view();
951 
952  std::string unit_name = type_cfg_.mandatory_child("unit_type")["name"];
953  boost::algorithm::replace_all(unit_name, " ", "_");
954 
955  // Path to <unit_type_name>.cfg
956  std::string unit_path = filesystem::get_current_editor_dir(addon_id_) + "/units/" + unit_name + filesystem::wml_extension;
957 
958  // Write to file
959  try {
961  gui2::show_transient_message("", _("Unit type saved."));
962  } catch(const filesystem::io_exception& e) {
963  gui2::show_transient_message("", e.what());
964  }
965 }
966 
968  bool& handled,
969  const SDL_Keycode key,
970  SDL_Keymod modifier)
971 {
972  #ifdef __APPLE__
973  // Idiomatic modifier key in macOS computers.
974  const SDL_Keycode modifier_key = KMOD_GUI;
975  #else
976  // Idiomatic modifier key in Microsoft desktop environments. Common in
977  // GNU/Linux as well, to some extent.
978  const SDL_Keycode modifier_key = KMOD_CTRL;
979  #endif
980 
981  // Ctrl+O shortcut for Load Unit Type
982  switch(key) {
983  case SDLK_o:
984  if (modifier & modifier_key) {
985  handled = true;
986  load_unit_type();
987  }
988  break;
989  }
990 
991 }
992 
993 }
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:274
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:948
void quit_confirmation()
Quit confirmation.
Definition: edit_unit.cpp:940
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:657
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:611
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:484
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:931
void signal_handler_sdl_key_down(const event::ui_event, bool &handled, const SDL_Keycode key, SDL_Keymod modifier)
Definition: edit_unit.cpp:967
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:293
boost::dynamic_bitset move_toggles_
Definition: edit_unit.hpp:54
void update_movement_costs()
Callbacks for movement list.
Definition: edit_unit.cpp:634
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:283
void update_resistances()
Callback for resistance list.
Definition: edit_unit.cpp:588
bool check_id(const std::string &id)
Utility method to check if ID contains any invalid characters.
Definition: edit_unit.cpp:921
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:335
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:830
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:812
void update_image(const std::string &id_stem)
Callback for image update.
Definition: edit_unit.cpp:891
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:61
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