The Battle for Wesnoth  1.19.11+dev
edit_unit.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2023 - 2025
3  by Subhraman Sarkar (babaissarkar) <suvrax@gmail.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  //TODO some weapon specials can have args (PLAGUE_TYPE)
63  io::read(*preprocess_file(game_config::path+"/data/core/macros/weapon_specials.cfg", &specials_map_));
64  for (const auto& x : specials_map_) {
65  specials_list_.emplace_back("label", x.first, "checkbox", false);
66  }
67 
68  io::read(*preprocess_file(game_config::path+"/data/core/macros/abilities.cfg", &abilities_map_));
69  for (const auto& x : abilities_map_) {
70  // Don't add any macros that have INTERNAL
71  if (x.first.find("INTERNAL") == std::string::npos) {
72  abilities_list_.emplace_back("label", x.first, "checkbox", false);
73  }
74  }
75 
76  connect_signal<event::SDL_KEY_DOWN>(std::bind(
77  &editor_edit_unit::signal_handler_sdl_key_down, this, std::placeholders::_2, std::placeholders::_3, std::placeholders::_5, std::placeholders::_6));
78 }
79 
81  tab_container& tabs = find_widget<tab_container>("tabs");
83 
84  button& quit = find_widget<button>("exit");
86 
87  //
88  // Main Stats tab
89  //
90 
91  tabs.select_tab(0);
92 
93  menu_button& alignments = find_widget<menu_button>("alignment_list");
94  for (auto& align : unit_alignments::values) {
95  // Show the user the translated strings,
96  // but use the untranslated align strings for generated WML
97  const std::string& icon_path = "icons/alignments/alignment_" + std::string(align) + "_30.png";
98  align_list_.emplace_back("label", t_string(static_cast<std::string>(align), "wesnoth"), "icon", icon_path);
99  }
100  alignments.set_values(align_list_);
101 
102  menu_button& races = find_widget<menu_button>("race_list");
103  for(const race_map::value_type& i : unit_types.races()) {
104  const std::string& race_name = i.second.id();
105  race_list_.emplace_back("label", race_name, "icon", i.second.get_icon_path_stem() + "_30.png");
106  }
107 
108  if (!race_list_.empty()) {
109  races.set_values(race_list_);
110  }
111 
112  button& load = find_widget<button>("load_unit_type");
113  std::stringstream tooltip;
114  tooltip << t_string("Hotkey(s): ", "wesnoth");
115  #ifdef __APPLE__
116  tooltip << "cmd+o";
117  #else
118  tooltip << "ctrl+o";
119  #endif
120  load.set_tooltip(tooltip.str());
122 
124  find_widget<button>("browse_unit_image"),
125  std::bind(&editor_edit_unit::select_file, this, "data/core/images/units", "unit_image"));
127  find_widget<button>("preview_unit_image"),
128  std::bind(&editor_edit_unit::update_image, this, "unit_image"));
130  find_widget<button>("browse_portrait_image"),
131  std::bind(&editor_edit_unit::select_file, this, "data/core/images/portraits", "portrait_image"));
133  find_widget<button>("preview_portrait_image"),
134  std::bind(&editor_edit_unit::update_image, this, "portrait_image"));
135 
137  find_widget<text_box>("name_box"),
138  std::bind(&editor_edit_unit::button_state_change, this));
140  find_widget<text_box>("id_box"),
141  std::bind(&editor_edit_unit::button_state_change, this));
142 
143  //
144  // Advanced Tab
145  //
146  tabs.select_tab(1);
147 
148  menu_button& movetypes = find_widget<menu_button>("movetype_list");
149  for(const auto& mt : unit_types.movement_types()) {
150  movetype_list_.emplace_back("label", mt.first);
151  }
152 
153  if (!movetype_list_.empty()) {
154  movetypes.set_values(movetype_list_);
155  }
156 
157  menu_button& defenses = find_widget<menu_button>("defense_list");
158  const config& defense_attr = game_config_
159  .mandatory_child("units")
160  .mandatory_child("movetype")
161  .mandatory_child("defense");
162  for (const auto& [key, _] : defense_attr.attribute_range()) {
163  defense_list_.emplace_back("label", key);
164  }
165 
166  menu_button& movement_costs = find_widget<menu_button>("movement_costs_list");
167  if (!defense_list_.empty()) {
168  defenses.set_values(defense_list_);
169  def_toggles_.resize(defense_list_.size());
170  movement_costs.set_values(defense_list_);
171  move_toggles_.resize(defense_list_.size());
172  }
173 
174  menu_button& resistances = find_widget<menu_button>("resistances_list");
175 
176  const config& resistances_attr = game_config_
177  .mandatory_child("units")
178  .mandatory_child("movetype")
179  .mandatory_child("resistance");
180  for (const auto& [key, _] : resistances_attr.attribute_range()) {
181  resistances_list_.emplace_back("label", key, "icon", "icons/profiles/" + key + ".png");
182  }
183 
184  if (!resistances_list_.empty()) {
185  resistances.set_values(resistances_list_);
186  res_toggles_.resize(resistances_list_.size());
187  }
188 
189  menu_button& usage_types = find_widget<menu_button>("usage_list");
190  usage_type_list_.emplace_back("label", _("scout"));
191  usage_type_list_.emplace_back("label", _("fighter"));
192  usage_type_list_.emplace_back("label", _("archer"));
193  usage_type_list_.emplace_back("label", _("mixed fighter"));
194  usage_type_list_.emplace_back("label", _("healer"));
195  usage_types.set_values(usage_type_list_);
196 
197  multimenu_button& abilities = find_widget<multimenu_button>("abilities_list");
198  abilities.set_values(abilities_list_);
199 
201  find_widget<button>("browse_small_profile_image"),
202  std::bind(&editor_edit_unit::select_file, this, "data/core/images/portraits", "small_profile_image"));
204  find_widget<button>("preview_small_profile_image"),
205  std::bind(&editor_edit_unit::update_image, this, "small_profile_image"));
207  find_widget<button>("load_movetype"),
208  std::bind(&editor_edit_unit::load_movetype, this));
210  find_widget<slider>("resistances_slider"),
211  std::bind(&editor_edit_unit::store_resistances, this));
213  find_widget<menu_button>("resistances_list"),
214  std::bind(&editor_edit_unit::update_resistances, this));
216  find_widget<toggle_button>("resistances_checkbox"),
218 
220  find_widget<slider>("defense_slider"),
221  std::bind(&editor_edit_unit::store_defenses, this));
223  find_widget<menu_button>("defense_list"),
224  std::bind(&editor_edit_unit::update_defenses, this));
226  find_widget<toggle_button>("defense_checkbox"),
227  std::bind(&editor_edit_unit::enable_defense_slider, this));
228 
230  find_widget<slider>("movement_costs_slider"),
231  std::bind(&editor_edit_unit::store_movement_costs, this));
233  find_widget<menu_button>("movement_costs_list"),
234  std::bind(&editor_edit_unit::update_movement_costs, this));
236  find_widget<toggle_button>("movement_costs_checkbox"),
237  std::bind(&editor_edit_unit::enable_movement_slider, this));
238 
239  if (!res_toggles_.empty()) {
241  }
242 
243  if (!def_toggles_.empty()) {
245  }
246 
247  if (!move_toggles_.empty()) {
249  }
250 
251  //
252  // Attack Tab
253  //
254  tabs.select_tab(2);
255  multimenu_button& specials = find_widget<multimenu_button>("weapon_specials_list");
256  specials.set_values(specials_list_);
257 
258  combobox& attack_types = find_widget<combobox>("attack_type_list");
259  if (!resistances_list_.empty()) {
260  attack_types.set_values(resistances_list_);
261  }
262 
263  // Connect signals
265  find_widget<button>("browse_attack_image"),
266  std::bind(&editor_edit_unit::select_file, this, "data/core/images/attacks", "attack_image"));
268  find_widget<button>("preview_attack_image"),
269  std::bind(&editor_edit_unit::update_image, this, "attack_image"));
271  find_widget<menu_button>("atk_list"),
272  std::bind(&editor_edit_unit::select_attack, this));
274  find_widget<button>("atk_new"),
275  std::bind(&editor_edit_unit::add_attack, this));
277  find_widget<button>("atk_delete"),
278  std::bind(&editor_edit_unit::delete_attack, this));
280  find_widget<button>("atk_next"),
281  std::bind(&editor_edit_unit::next_attack, this));
283  find_widget<button>("atk_prev"),
284  std::bind(&editor_edit_unit::prev_attack, this));
285 
286  update_index();
287 
288  tabs.select_tab(0);
289 
290  // Disable OK button at start, since ID and Name boxes are empty
292 }
293 
295 {
296  save_unit_type();
297 
298  tab_container& tabs = find_widget<tab_container>("tabs");
299  if (tabs.get_active_tab_index() == 3) {
300  update_wml_view();
301  }
302 }
303 
304 void editor_edit_unit::select_file(const std::string& default_dir, const std::string& id_stem)
305 {
307  dlg.set_title(_("Choose File"))
308  .set_ok_label(_("Select"))
309  .set_path(default_dir)
310  .set_read_only(true);
311 
312  // If the file is found inside Wesnoth's data, give its relative path
313  // if not, ask user if they want to copy it into their addon.
314  // If yes, copy and return correct relative path inside addon.
315  // return empty otherwise.
316  auto find_or_copy = [](const std::string& path, const std::string& addon_id, const std::string& type) {
317  const std::string message
318  = _("This file is outside Wesnoth’s data dirs. Do you wish to copy it into your add-on?");
319  const auto optional_path = filesystem::to_asset_path(path, addon_id, type);
320 
321  if(optional_path.has_value()) {
322  return optional_path.value();
325  output_path /= type;
326  output_path /= boost::filesystem::path(path).filename();
327  filesystem::copy_file(path, output_path.string());
328  return output_path.filename().string();
329  } else {
330  return std::string();
331  }
332  };
333 
334  if (dlg.show()) {
335  if((id_stem == "unit_image")
336  || (id_stem == "portrait_image")
337  || (id_stem == "small_profile_image")
338  || (id_stem == "attack_image"))
339  {
340  find_widget<text_box>("path_"+id_stem).set_value(find_or_copy(dlg.path(), addon_id_, "images"));
341  update_image(id_stem);
342  }
343  }
344 }
345 
347  const auto& all_type_list = unit_types.types_list();
348  const auto& type_select = gui2::dialogs::units_dialog::build_create_dialog(all_type_list);
349 
350  if (!type_select->show() && !type_select->is_selected()) {
351  return;
352  }
353 
354  const auto& type = all_type_list[type_select->get_selected_index()];
355 
356  tab_container& tabs = find_widget<tab_container>("tabs");
357  tabs.select_tab(0);
358 
359  find_widget<text_box>("id_box").set_value(type->id());
360  find_widget<text_box>("name_box").set_value(type->type_name().base_str());
361  find_widget<spinner>("level_box").set_value(type->level());
362  find_widget<slider>("cost_slider").set_value(type->cost());
363  find_widget<text_box>("adv_box").set_value(utils::join(type->advances_to()));
364  find_widget<slider>("hp_slider").set_value(type->hitpoints());
365  find_widget<slider>("xp_slider").set_value(type->experience_needed());
366  find_widget<slider>("move_slider").set_value(type->movement());
367  find_widget<scroll_text>("desc_box").set_value(type->unit_description().base_str());
368  find_widget<text_box>("path_unit_image").set_value(type->image());
369  find_widget<text_box>("path_portrait_image").set_value(type->big_profile());
370 
371  for (const auto& gender : type->genders())
372  {
373  if (gender == unit_race::GENDER::MALE) {
374  find_widget<toggle_button>("gender_male").set_value(true);
375  }
376 
377  if (gender == unit_race::GENDER::FEMALE) {
378  find_widget<toggle_button>("gender_female").set_value(true);
379  }
380  }
381 
383  find_widget<menu_button>("race_list"),
384  race_list_,
385  type->race_id());
386 
388  find_widget<menu_button>("alignment_list"),
389  align_list_,
390  unit_alignments::get_string(type->alignment()));
391 
392  update_image("unit_image");
393 
394  tabs.select_tab(1);
395  find_widget<text_box>("path_small_profile_image").set_value(type->small_profile());
396 
398  find_widget<menu_button>("movetype_list"),
400  type->movement_type_id());
401 
402  config cfg;
403  type->movement_type().write(cfg, false);
404  movement_ = cfg.mandatory_child("movement_costs");
405  defenses_ = cfg.mandatory_child("defense");
406  resistances_ = cfg.mandatory_child("resistance");
407 
408  // Overrides for resistance/defense/movement costs
409  for (unsigned i = 0; i < resistances_list_.size(); i++) {
410  if (!type->get_cfg().has_child("resistance")) {
411  break;
412  }
413 
414  for (const auto& [key, _] : type->get_cfg().mandatory_child("resistance").attribute_range()) {
415  if (resistances_list_.at(i)["label"] == key) {
416  res_toggles_[i] = 1;
417  }
418  }
419  }
420 
421  for (unsigned i = 0; i < defense_list_.size(); i++) {
422  if (type->get_cfg().has_child("defense")) {
423  for (const auto& [key, _] : type->get_cfg().mandatory_child("defense").attribute_range()) {
424  if (defense_list_.at(i)["label"] == key) {
425  def_toggles_[i] = 1;
426  }
427  }
428  }
429 
430  if (type->get_cfg().has_child("movement_costs")) {
431  for (const auto& [key, _] : type->get_cfg().mandatory_child("movement_costs").attribute_range()) {
432  if (defense_list_.at(i)["label"] == key) {
433  move_toggles_[i] = 1;
434  }
435  }
436  }
437  }
438 
440  update_defenses();
442 
444  find_widget<menu_button>("usage_list"),
446  type->usage());
447 
448  update_image("small_profile_image");
449 
450  tabs.select_tab(2);
451  attacks_.clear();
452  for(const auto& atk : type->attacks())
453  {
454  config attack;
455  boost::dynamic_bitset<> enabled(specials_list_.size());
456  attack["name"] = atk.id();
457  attack["description"] = atk.name().base_str();
458  attack["icon"] = atk.icon();
459  attack["range"] = atk.range();
460  attack["damage"] = atk.damage();
461  attack["number"] = atk.num_attacks();
462  attack["type"] = atk.type();
463  attacks_.push_back(std::make_pair(enabled, attack));
464  }
465 
466  if (!type->attacks().empty()) {
467  selected_attack_ = 1;
468  update_attacks();
469  }
470 
471  update_index();
472 
473  tabs.select_tab(0);
474 
477 }
478 
480 
481  // Clear the config
482  type_cfg_.clear();
483 
484  // Textdomain
485  std::string current_textdomain = "wesnoth-"+addon_id_;
486 
487  tab_container& tabs = find_widget<tab_container>("tabs");
488 
489  // Page 1
490  grid* grid = tabs.get_tab_grid(0);
491 
492  config& utype = type_cfg_.add_child("unit_type");
493  utype["id"] = grid->find_widget<text_box>("id_box").get_value();
494  utype["name"] = t_string(grid->find_widget<text_box>("name_box").get_value(), current_textdomain);
495  utype["image"] = grid->find_widget<text_box>("path_unit_image").get_value();
496  utype["profile"] = grid->find_widget<text_box>("path_portrait_image").get_value();
497  utype["level"] = grid->find_widget<spinner>("level_box").get_value();
498  utype["advances_to"] = grid->find_widget<text_box>("adv_box").get_value();
499  utype["hitpoints"] = grid->find_widget<slider>("hp_slider").get_value();
500  utype["experience"] = grid->find_widget<slider>("xp_slider").get_value();
501  utype["cost"] = grid->find_widget<slider>("cost_slider").get_value();
502  utype["movement"] = grid->find_widget<slider>("move_slider").get_value();
503  utype["description"] = t_string(grid->find_widget<scroll_text>("desc_box").get_value(), current_textdomain);
504  utype["race"] = grid->find_widget<menu_button>("race_list").get_value_string();
505  utype["alignment"] = unit_alignments::values[grid->find_widget<menu_button>("alignment_list").get_value()];
506 
507  // Gender
508  if (grid->find_widget<toggle_button>("gender_male").get_value()) {
509  if (grid->find_widget<toggle_button>("gender_female").get_value()) {
510  utype["gender"] = "male,female";
511  } else {
512  utype["gender"] = "male";
513  }
514  } else {
515  if (grid->find_widget<toggle_button>("gender_female").get_value()) {
516  utype["gender"] = "female";
517  }
518  }
519 
520  // Page 2
521  grid = tabs.get_tab_grid(1);
522 
523  utype["small_profile"] = grid->find_widget<text_box>("path_small_profile_image").get_value();
524  utype["movement_type"] = grid->find_widget<menu_button>("movetype_list").get_value_string();
525  utype["usage"] = grid->find_widget<menu_button>("usage_list").get_value_string();
526 
527  if (res_toggles_.any()) {
528  config& resistances = utype.add_child("resistance");
529  int i = 0;
530  for (const auto& [key, _] : resistances_.attribute_range()) {
531  if (res_toggles_[i]) {
532  resistances[key] = resistances_[key];
533  }
534  i++;
535  }
536  }
537 
538  if (def_toggles_.any()) {
539  config& defenses = utype.add_child("defense");
540  int i = 0;
541  for (const auto& [key, _] : defenses_.attribute_range()) {
542  if (def_toggles_[i]) {
543  defenses[key] = defenses_[key];
544  }
545  i++;
546  }
547  }
548 
549  if (move_toggles_.any()) {
550  config& movement_costs = utype.add_child("movement_costs");
551  int i = 0;
552  for (const auto& [key, _] : movement_.attribute_range()) {
553  if (move_toggles_[i]) {
554  movement_costs[key] = movement_[key];
555  }
556  i++;
557  }
558  }
559 
560  const auto& abilities_states = grid->find_widget<multimenu_button>("abilities_list").get_toggle_states();
561  if (abilities_states.any()) {
562  unsigned int i = 0;
563  sel_abilities_.clear();
564  for (const auto& x : abilities_map_) {
565  if (i >= abilities_states.size()) {
566  break;
567  }
568 
569  if (abilities_states[i] == true) {
570  sel_abilities_.push_back(x.first);
571  }
572 
573  i++;
574  }
575  }
576 
577  // Note : attacks and abilities are not written to the config, since they have macros.
578 }
579 
581  find_widget<slider>("resistances_slider")
582  .set_value(
583  100 - resistances_[find_widget<menu_button>("resistances_list").get_value_string()].to_int());
584 
585  find_widget<slider>("resistances_slider")
586  .set_active(res_toggles_[find_widget<menu_button>("resistances_list").get_value()]);
587 
588  find_widget<toggle_button>("resistances_checkbox")
589  .set_value(res_toggles_[find_widget<menu_button>("resistances_list").get_value()]);
590 }
591 
593  resistances_[find_widget<menu_button>("resistances_list").get_value_string()]
594  = 100 - find_widget<slider>("resistances_slider").get_value();
595 }
596 
598  bool toggle = find_widget<toggle_button>("resistances_checkbox").get_value();
599  res_toggles_[find_widget<menu_button>("resistances_list").get_value()] = toggle;
600  find_widget<slider>("resistances_slider").set_active(toggle);
601 }
602 
604  find_widget<slider>("defense_slider")
605  .set_value(
606  100 - defenses_[find_widget<menu_button>("defense_list").get_value_string()].to_int());
607 
608  find_widget<slider>("defense_slider")
609  .set_active(def_toggles_[find_widget<menu_button>("defense_list").get_value()]);
610 
611  find_widget<toggle_button>("defense_checkbox")
612  .set_value(def_toggles_[find_widget<menu_button>("defense_list").get_value()]);
613 }
614 
616  defenses_[find_widget<menu_button>("defense_list").get_value_string()]
617  = 100 - find_widget<slider>("defense_slider").get_value();
618 }
619 
621  bool toggle = find_widget<toggle_button>("defense_checkbox").get_value();
622  def_toggles_[find_widget<menu_button>("defense_list").get_value()] = toggle;
623  find_widget<slider>("defense_slider").set_active(toggle);
624 }
625 
627  find_widget<slider>("movement_costs_slider")
628  .set_value(
629  movement_[find_widget<menu_button>("movement_costs_list").get_value_string()].to_int());
630 
631  find_widget<slider>("movement_costs_slider")
632  .set_active(move_toggles_[find_widget<menu_button>("movement_costs_list").get_value()]);
633 
634  find_widget<toggle_button>("movement_costs_checkbox")
635  .set_value(move_toggles_[find_widget<menu_button>("movement_costs_list").get_value()]);
636 }
637 
639  movement_[find_widget<menu_button>("movement_costs_list").get_value_string()]
640  = find_widget<slider>("movement_costs_slider").get_value();
641 }
642 
644  bool toggle = find_widget<toggle_button>("movement_costs_checkbox").get_value();
645  move_toggles_[find_widget<menu_button>("movement_costs_list").get_value()] = toggle;
646  find_widget<slider>("movement_costs_slider").set_active(toggle);
647 }
648 
650  // Textdomain
651  std::string current_textdomain = "wesnoth-"+addon_id_;
652 
653  // Save current attack data
654  if (selected_attack_ < 1) {
655  return;
656  }
657 
658  config& attack = attacks_.at(selected_attack_-1).second;
659 
660  tab_container& tabs = find_widget<tab_container>("tabs");
661  int prev_tab = tabs.get_active_tab_index();
662  tabs.select_tab(2);
663 
664  attack["name"] = find_widget<text_box>("atk_id_box").get_value();
665  attack["description"] = t_string(find_widget<text_box>("atk_name_box").get_value(), current_textdomain);
666  attack["icon"] = find_widget<text_box>("path_attack_image").get_value();
667  attack["type"] = find_widget<combobox>("attack_type_list").get_value();
668  attack["damage"] = find_widget<slider>("dmg_box").get_value();
669  attack["number"] = find_widget<slider>("dmg_num_box").get_value();
670  attack["range"] = find_widget<combobox>("range_list").get_value();
671 
672  attacks_.at(selected_attack_-1).first = find_widget<multimenu_button>("weapon_specials_list").get_toggle_states();
673 
674  tabs.select_tab(prev_tab);
675 }
676 
678  //Load data
679  if (selected_attack_ < 1) {
680  return;
681  }
682 
683  config& attack = attacks_.at(selected_attack_-1).second;
684 
685  tab_container& tabs = find_widget<tab_container>("tabs");
686  int prev_tab = tabs.get_active_tab_index();
687  tabs.select_tab(2);
688 
689  find_widget<text_box>("atk_id_box").set_value(attack["name"]);
690  find_widget<text_box>("atk_name_box").set_value(attack["description"]);
691  find_widget<text_box>("path_attack_image").set_value(attack["icon"]);
692  update_image("attack_image");
693  find_widget<slider>("dmg_box").set_value(attack["damage"].to_int());
694  find_widget<slider>("dmg_num_box").set_value(attack["number"].to_int());
695  find_widget<combobox>("range_list").set_value(attack["range"]);
696 
698  find_widget<combobox>("attack_type_list"), resistances_list_, attack["type"]);
699 
700  find_widget<multimenu_button>("weapon_specials_list")
701  .select_options(attacks_.at(selected_attack_-1).first);
702 
703  tabs.select_tab(prev_tab);
704 }
705 
707  tab_container& tabs = find_widget<tab_container>("tabs");
708  int prev_tab = tabs.get_active_tab_index();
709  tabs.select_tab(2);
710 
711  find_widget<button>("atk_prev").set_active(selected_attack_ > 1);
712  find_widget<button>("atk_delete").set_active(selected_attack_ > 0);
713  find_widget<button>("atk_next").set_active(selected_attack_ != attacks_.size());
714 
715  if (!attacks_.empty()) {
716  std::vector<config> atk_name_list;
717  for(const auto& atk_data : attacks_) {
718  atk_name_list.emplace_back("label", atk_data.second["name"]);
719  }
720  menu_button& atk_list = find_widget<menu_button>("atk_list");
721  atk_list.set_values(atk_name_list);
722  atk_list.set_selected(selected_attack_-1, false);
723  }
724 
725  //Set index
726  const std::string new_index_str = formatter() << selected_attack_ << "/" << attacks_.size();
727  find_widget<label>("atk_number").set_label(new_index_str);
728 
729  tabs.select_tab(prev_tab);
730 }
731 
733  // Textdomain
734  std::string current_textdomain = "wesnoth-"+addon_id_;
735 
736  tab_container& tabs = find_widget<tab_container>("tabs");
737  int prev_tab = tabs.get_active_tab_index();
738  tabs.select_tab(2);
739 
740  config attack;
741 
742  attack["name"] = find_widget<text_box>("atk_id_box").get_value();
743  attack["description"] = t_string(find_widget<text_box>("atk_name_box").get_value(), current_textdomain);
744  attack["icon"] = find_widget<text_box>("path_attack_image").get_value();
745  attack["type"] = find_widget<combobox>("attack_type_list").get_value();
746  attack["damage"] = find_widget<slider>("dmg_box").get_value();
747  attack["number"] = find_widget<slider>("dmg_num_box").get_value();
748  attack["range"] = find_widget<combobox>("range_list").get_value();
749 
751 
752  attacks_.insert(
753  attacks_.begin() + selected_attack_ - 1
754  , std::make_pair(find_widget<multimenu_button>("weapon_specials_list").get_toggle_states(), attack));
755 
756  update_index();
757 
758  tabs.select_tab(prev_tab);
759 }
760 
762  tab_container& tabs = find_widget<tab_container>("tabs");
763  int prev_tab = tabs.get_active_tab_index();
764  tabs.select_tab(2);
765 
766  //remove attack
767  if (!attacks_.empty()) {
768  attacks_.erase(attacks_.begin() + selected_attack_ - 1);
769  }
770 
771  if (attacks_.empty()) {
772  // clear fields instead since there are no attacks to show
773  selected_attack_ = 0;
774  find_widget<button>("atk_delete").set_active(false);
775  } else if (selected_attack_ == 1) {
776  // 1st attack removed, show the next one
777  next_attack();
778  } else {
779  // show previous attack otherwise
780  prev_attack();
781  }
782 
783  update_index();
784 
785  tabs.select_tab(prev_tab);
786 }
787 
789  store_attack();
790 
791  if (attacks_.size() > 1) {
793  update_attacks();
794  }
795 
796  update_index();
797 }
798 
800  store_attack();
801 
802  if (selected_attack_ > 0) {
804  }
805 
806  if (attacks_.size() > 1) {
807  update_attacks();
808  }
809 
810  update_index();
811 }
812 
814  selected_attack_ = find_widget<menu_button>("atk_list").get_value()+1;
815  update_attacks();
816  update_index();
817 }
818 
819 //TODO Check if works with non-mainline movetypes
821  tab_container& tabs = find_widget<tab_container>("tabs");
822  int prev_tab = tabs.get_active_tab_index();
823  tabs.select_tab(1);
824 
825  for(const auto& movetype : game_config_
826  .mandatory_child("units")
827  .child_range("movetype")) {
828  if (movetype["name"] == find_widget<menu_button>("movetype_list").get_value_string()) {
829  // Set resistances
830  resistances_ = movetype.mandatory_child("resistance");
832  // Set defense
833  defenses_ = movetype.mandatory_child("defense");
834  update_defenses();
835  // Set movement
836  movement_ = movetype.mandatory_child("movement_costs");
838  }
839  }
840 
841  tabs.select_tab(prev_tab);
842 }
843 
844 void editor_edit_unit::write_macro(std::ostream& out, unsigned level, const std::string& macro_name)
845 {
846  for(unsigned i = 0; i < level; i++)
847  {
848  out << "\t";
849  }
850  out << "{" << macro_name << "}\n";
851 }
852 
854  store_attack();
855  save_unit_type();
856 
857  tab_container& tabs = find_widget<tab_container>("tabs");
858  tabs.select_tab(3);
859 
860  std::stringstream wml_stream;
861 
862  // Textdomain
863  std::string current_textdomain = "wesnoth-"+addon_id_;
864 
865  wml_stream
866  << "#textdomain " << current_textdomain << "\n"
867  << "#\n"
868  << "# This file was generated using the scenario editor.\n"
869  << "#\n";
870 
871  {
872  config_writer out(wml_stream, false);
873  int level = 0;
874 
875  out.open_child("unit_type");
876 
877  level++;
878  for (const auto& [key, value] : type_cfg_.mandatory_child("unit_type").attribute_range()) {
879  io::write_key_val(wml_stream, key, value, level, current_textdomain);
880  }
881 
882  // Abilities
883  if (!sel_abilities_.empty()) {
884  out.open_child("abilities");
885  level++;
886  for (const std::string& ability : sel_abilities_) {
887  write_macro(wml_stream, level, ability);
888  }
889  level--;
890  out.close_child("abilities");
891  }
892 
893  // Attacks
894  if (!attacks_.empty()) {
895  for (const auto& atk : attacks_) {
896  out.open_child("attack");
897  level++;
898  for (const auto& [key, value] : atk.second.attribute_range()) {
899  if (!value.empty()) {
900  io::write_key_val(wml_stream, key, value, level, current_textdomain);
901  }
902  }
903 
904  if(atk.first.any()) {
905  out.open_child("specials");
906  level++;
907  int i = 0;
908  for (const auto& attr : specials_map_) {
909  if (atk.first[i]) {
910  write_macro(wml_stream, level, attr.first);
911  }
912  i++;
913  }
914  level--;
915  out.close_child("specials");
916 
917  }
918  level--;
919  out.close_child("attack");
920  }
921  }
922 
923  if (!movement_.empty() && (move_toggles_.size() <= movement_.attribute_count()) && move_toggles_.any())
924  {
925  out.open_child("movement_costs");
926  level++;
927  int i = 0;
928  for (const auto& [key, value] : movement_.attribute_range()) {
929  if (move_toggles_[i] == 1) {
930  io::write_key_val(wml_stream, key, value, level, current_textdomain);
931  }
932  i++;
933  }
934  level--;
935  out.close_child("movement_costs");
936  }
937 
938  if (!defenses_.empty() && def_toggles_.any() && (def_toggles_.size() <= defenses_.attribute_count()))
939  {
940  out.open_child("defense");
941  level++;
942  int i = 0;
943  for (const auto& [key, value] : defenses_.attribute_range()) {
944  if (def_toggles_[i] == 1) {
945  io::write_key_val(wml_stream, key, value, level, current_textdomain);
946  }
947  i++;
948  }
949  level--;
950  out.close_child("defense");
951  }
952 
953  if (!resistances_.empty() && res_toggles_.any() && (res_toggles_.size() <= resistances_.attribute_count()))
954  {
955  out.open_child("resistance");
956  level++;
957  int i = 0;
958  for (const auto& [key, value] : resistances_.attribute_range()) {
959  if (res_toggles_[i] == 1) {
960  io::write_key_val(wml_stream, key, value, level, current_textdomain);
961  }
962  i++;
963  }
964  level--;
965  out.close_child("resistance");
966  }
967 
968  out.close_child("unit_type");
969  }
970 
971  generated_wml = wml_stream.str();
972 
973  find_widget<scroll_text>("wml_view").set_label(generated_wml);
974 }
975 
976 void editor_edit_unit::update_image(const std::string& id_stem) {
977  std::string rel_path = find_widget<text_box>("path_"+id_stem).get_value();
978 
979  // remove IPF
980  if (rel_path.find("~") != std::string::npos) {
981  rel_path = rel_path.substr(0, rel_path.find("~"));
982  }
983 
984  int scale_size = 200; // TODO: Arbitrary, can be changed later.
985  if (rel_path.size() > 0) {
986  point img_size = ::image::get_size(::image::locator{rel_path});
987  float aspect_ratio = static_cast<float>(img_size.x)/img_size.y;
988  if(img_size.x > scale_size) {
989  rel_path.append("~SCALE(" + std::to_string(scale_size) + "," + std::to_string(scale_size*aspect_ratio) + ")");
990  } else if (img_size.y > scale_size) {
991  rel_path.append("~SCALE(" + std::to_string(scale_size/aspect_ratio) + "," + std::to_string(scale_size) + ")");
992  }
993  }
994 
995  if (id_stem == "portrait_image") {
996  // portrait image uses same [image] as unit_image
997  find_widget<image>("unit_image").set_label(rel_path);
998  } else {
999  find_widget<image>(id_stem).set_label(rel_path);
1000  }
1001 
1003  queue_redraw();
1004 }
1005 
1006 bool editor_edit_unit::check_id(const std::string& id) {
1007  for(char c : id) {
1008  if (!(std::isalnum(c) || c == '_' || c == ' ')) {
1009  // One bad char means entire id string is invalid
1010  return false;
1011  }
1012  }
1013  return true;
1014 }
1015 
1017  grid* grid = find_widget<tab_container>("tabs").get_tab_grid(0);
1018 
1019  std::string id = grid->find_widget<text_box>("id_box").get_value();
1020  std::string name = grid->find_widget<text_box>("name_box").get_value();
1021 
1022  find_widget<button>("ok").set_active(!id.empty() && !name.empty() && check_id(id));
1023 
1024  queue_redraw();
1025 }
1026 
1028  const std::string& message
1029  = _("Unsaved changes will be lost. Do you want to leave?");
1032  }
1033 }
1034 
1036  // Write the file
1037  update_wml_view();
1038 
1039  std::string unit_name = type_cfg_.mandatory_child("unit_type")["name"];
1040  boost::algorithm::replace_all(unit_name, " ", "_");
1041 
1042  // Path to <unit_type_name>.cfg
1043  std::string unit_path = filesystem::get_current_editor_dir(addon_id_) + "/units/" + unit_name + filesystem::wml_extension;
1044 
1045  // Write to file
1046  try {
1048  gui2::show_transient_message("", _("Unit type saved."));
1049  } catch(const filesystem::io_exception& e) {
1050  gui2::show_transient_message("", e.what());
1051  }
1052 }
1053 
1055  bool& handled,
1056  const SDL_Keycode key,
1057  SDL_Keymod modifier)
1058 {
1059  #ifdef __APPLE__
1060  // Idiomatic modifier key in macOS computers.
1061  const SDL_Keycode modifier_key = KMOD_GUI;
1062  #else
1063  // Idiomatic modifier key in Microsoft desktop environments. Common in
1064  // GNU/Linux as well, to some extent.
1065  const SDL_Keycode modifier_key = KMOD_CTRL;
1066  #endif
1067 
1068  // Ctrl+O shortcut for Load Unit Type
1069  switch(key) {
1070  case SDLK_o:
1071  if (modifier & modifier_key) {
1072  handled = true;
1073  load_unit_type();
1074  }
1075  break;
1076  }
1077 
1078 }
1079 
1080 }
Class for writing a config out to a file in pieces.
void close_child(const std::string &key)
void open_child(const std::string &key)
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:158
std::size_t attribute_count() const
Count the number of non-blank attributes.
Definition: config.cpp:307
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:362
const_attr_itors attribute_range() const
Definition: config.cpp:756
bool empty() const
Definition: config.cpp:845
void clear()
Definition: config.cpp:824
config & add_child(config_key_type key)
Definition: config.cpp:436
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(config_key_type 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:69
void write()
Write the cfg file.
Definition: edit_unit.cpp:1035
void quit_confirmation()
Quit confirmation.
Definition: edit_unit.cpp:1027
std::vector< config > defense_list_
Definition: edit_unit.hpp:57
boost::dynamic_bitset res_toggles_
Used to control checkboxes for various resistances, defences, etc.
Definition: edit_unit.hpp:55
const game_config_view & game_config_
Definition: edit_unit.hpp:44
std::vector< std::string > sel_abilities_
Need this because can't store macros in config.
Definition: edit_unit.hpp:63
std::vector< std::pair< boost::dynamic_bitset<>, config > > attacks_
Definition: edit_unit.hpp:60
std::vector< config > abilities_list_
Definition: edit_unit.hpp:58
void store_attack()
Callbacks for attack page.
Definition: edit_unit.cpp:649
void set_selected_from_string(menu_button &list, std::vector< config > values, std::string item)
Definition: edit_unit.hpp:133
std::vector< config > movetype_list_
Definition: edit_unit.hpp:57
std::vector< config > race_list_
Definition: edit_unit.hpp:57
void update_defenses()
Callbacks for defense list.
Definition: edit_unit.cpp:603
std::string generated_wml
Generated WML.
Definition: edit_unit.hpp:66
void save_unit_type()
Save Unit Type data to cfg.
Definition: edit_unit.cpp:479
std::vector< config > align_list_
Definition: edit_unit.hpp:57
void button_state_change()
Callback to enable/disable OK button if ID/Name is invalid.
Definition: edit_unit.cpp:1016
void signal_handler_sdl_key_down(const event::ui_event, bool &handled, const SDL_Keycode key, SDL_Keymod modifier)
Definition: edit_unit.cpp:1054
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:304
boost::dynamic_bitset move_toggles_
Definition: edit_unit.hpp:55
void update_movement_costs()
Callbacks for movement list.
Definition: edit_unit.cpp:626
std::vector< config > resistances_list_
Definition: edit_unit.hpp:57
void on_page_select()
Callback when an tab item in the "page" listbox is selected.
Definition: edit_unit.cpp:294
void update_resistances()
Callback for resistance list.
Definition: edit_unit.cpp:580
bool check_id(const std::string &id)
Utility method to check if ID contains any invalid characters.
Definition: edit_unit.cpp:1006
const std::string & addon_id_
Definition: edit_unit.hpp:45
std::vector< config > specials_list_
Definition: edit_unit.hpp:58
boost::dynamic_bitset def_toggles_
Definition: edit_unit.hpp:55
void load_unit_type()
Load Unit Type data from cfg.
Definition: edit_unit.cpp:346
virtual void pre_show() override
Actions to be taken before showing the window.
Definition: edit_unit.cpp:80
void update_wml_view()
Update wml preview.
Definition: edit_unit.cpp:853
void write_macro(std::ostream &out, unsigned level, const std::string &macro_name)
Write macro to a stream at specified tab level.
Definition: edit_unit.cpp:844
std::vector< config > usage_type_list_
Definition: edit_unit.hpp:57
void load_movetype()
Callback for loading movetype data in UI.
Definition: edit_unit.cpp:820
void update_image(const std::string &id_stem)
Callback for image update.
Definition: edit_unit.cpp:976
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)
Base container class.
Definition: grid.hpp:32
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.
std::string get_value()
Definition: scroll_text.cpp:76
const t_string & tooltip() const
A container widget that shows one of its pages of widgets depending on which tab the user clicked.
grid * get_tab_grid(unsigned i)
void select_tab(unsigned index)
unsigned get_active_tab_index()
std::string get_value() const
A widget that allows the user to input text in single line.
Definition: text_box.hpp:125
virtual unsigned get_value() const override
Inherited from selectable_item.
void queue_redraw()
Indicates that this widget should be redrawn.
Definition: widget.cpp:464
T * find_widget(const std::string_view id, const bool must_be_active, const bool must_exist)
Gets a widget with the wanted id.
Definition: widget.hpp:747
void set_retval(const int retval, const bool close_window=true)
Sets there return value of the window.
Definition: window.hpp:395
void invalidate_layout()
Updates the size of the window.
Definition: window.cpp:760
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 movement_type_map & movement_types() const
Definition: types.hpp:407
const race_map & races() const
Definition: types.hpp:406
const std::vector< const unit_type * > types_list() const
Definition: types.hpp:397
Declarations for File-IO.
std::size_t i
Definition: function.cpp:1030
static std::string _(const char *str)
Definition: gettext.hpp:97
const std::string wml_extension
Definition: filesystem.hpp:81
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:102
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:799
config read(std::istream &in, abstract_validator *validator)
Definition: parser.cpp:600
void write_key_val(std::ostream &out, const std::string &key, const config::attribute_value &value, unsigned level, std::string &textdomain)
Definition: parser.cpp:667
constexpr auto values
Definition: ranges.hpp:42
std::string join(const T &v, const std::string &s=",")
Generates a new string joining container items in a list.
filesystem::scoped_istream preprocess_file(const std::string &fname, preproc_map *defines)
Function to use the WML preprocessor on a file.
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:1504
#define e