The Battle for Wesnoth  1.19.0-dev
edit_unit.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2023 - 2024
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 "formula/string_utils.hpp"
24 #include "gettext.hpp"
25 #include "units/types.hpp"
28 #include "gui/widgets/button.hpp"
29 #include "gui/widgets/image.hpp"
31 #include "gui/widgets/spinner.hpp"
32 #include "gui/widgets/label.hpp"
33 #include "gui/widgets/listbox.hpp"
37 #include "gui/widgets/slider.hpp"
39 #include "gui/widgets/text_box.hpp"
43 #include "serialization/parser.hpp"
45 
46 #include <boost/filesystem.hpp>
47 #include <boost/algorithm/string/replace.hpp>
48 #include <sstream>
49 
50 namespace gui2::dialogs
51 {
52 
54 
56  : modal_dialog(window_id())
57  , game_config_(game_config)
58  , addon_id_(addon_id)
59 {
60  config specials;
61 
62  read(specials, *(preprocess_file(game_config::path+"/data/core/macros/weapon_specials.cfg", &specials_map_)));
63  for (const auto& x : specials_map_) {
64  specials_list_.emplace_back("label", x.first, "checkbox", false);
65  }
66 
67  read(specials, *(preprocess_file(game_config::path+"/data/core/macros/abilities.cfg", &abilities_map_)));
68  for (const auto& x : abilities_map_) {
69  /** Don't add any macros that have INTERNAL */
70  if (x.first.find("INTERNAL") == std::string::npos) {
71  abilities_list_.emplace_back("label", x.first, "checkbox", false);
72  }
73  }
74 
75  connect_signal<event::SDL_KEY_DOWN>(std::bind(
76  &editor_edit_unit::signal_handler_sdl_key_down, this, std::placeholders::_2, std::placeholders::_3, std::placeholders::_5, std::placeholders::_6));
77 }
78 
80  menu_button& alignments = find_widget<menu_button>(&win, "alignment_list", false);
81  // TODO:change alignments to existing translatable strings
82  align_list_.emplace_back("label", _("lawful"));
83  align_list_.emplace_back("label", _("chaotic"));
84  align_list_.emplace_back("label", _("neutral"));
85  align_list_.emplace_back("label", _("liminal"));
86  alignments.set_values(align_list_);
87 
88  menu_button& races = find_widget<menu_button>(&win, "race_list", false);
89  for(const race_map::value_type &i : unit_types.races()) {
90  const std::string& race_name = i.second.id();
91  race_list_.emplace_back("label", race_name);
92  }
93 
94  if (race_list_.size() > 0) {
95  races.set_values(race_list_);
96  }
97 
98  menu_button& movetypes = find_widget<menu_button>(&win, "movetype_list", false);
99  for(const auto& mt : unit_types.movement_types()) {
100  movetype_list_.emplace_back("label", mt.first);
101  }
102 
103  if (movetype_list_.size() > 0) {
104  movetypes.set_values(movetype_list_);
105  }
106 
107  menu_button& defenses = find_widget<menu_button>(&win, "defense_list", false);
108  const config& defense_attr = game_config_
109  .mandatory_child("units")
110  .mandatory_child("movetype")
111  .mandatory_child("defense");
112  for (const auto& attribute : defense_attr.attribute_range()) {
113  defense_list_.emplace_back("label", attribute.first);
114  }
115 
116  if (defense_list_.size() > 0) {
117  defenses.set_values(defense_list_);
118  def_toggles_.resize(defense_list_.size());
119  }
120 
121  menu_button& movement_costs = find_widget<menu_button>(&win, "movement_costs_list", false);
122  if (defense_list_.size() > 0) {
123  movement_costs.set_values(defense_list_);
124  move_toggles_.resize(defense_list_.size());
125  }
126 
127  menu_button& resistances = find_widget<menu_button>(&win, "resistances_list", false);
128  menu_button& attack_types = find_widget<menu_button>(&win, "attack_type_list", false);
129 
130  const config& resistances_attr = game_config_
131  .mandatory_child("units")
132  .mandatory_child("movetype")
133  .mandatory_child("resistance");
134  for (const auto& attribute : resistances_attr.attribute_range()) {
135  resistances_list_.emplace_back("label", attribute.first);
136  }
137 
138  if (resistances_list_.size() > 0) {
139  resistances.set_values(resistances_list_);
140  attack_types.set_values(resistances_list_);
141  res_toggles_.resize(resistances_list_.size());
142  }
143 
144  menu_button& usage_types = find_widget<menu_button>(&win, "usage_list", false);
145  usage_type_list_.emplace_back("label", _("scout"));
146  usage_type_list_.emplace_back("label", _("fighter"));
147  usage_type_list_.emplace_back("label", _("archer"));
148  usage_type_list_.emplace_back("label", _("mixed fighter"));
149  usage_type_list_.emplace_back("label", _("healer"));
150  usage_types.set_values(usage_type_list_);
151 
152  multimenu_button& specials = find_widget<multimenu_button>(&win, "weapon_specials_list", false);
153  specials.set_values(specials_list_);
154  multimenu_button& abilities = find_widget<multimenu_button>(&win, "abilities_list", false);
155  abilities.set_values(abilities_list_);
156 
157  group<std::string> range_group;
158  range_group.add_member(find_widget<toggle_button>(&win, "range_melee", false, true), "melee");
159  range_group.add_member(find_widget<toggle_button>(&win, "range_ranged", false, true), "ranged");
160  range_group.set_member_states("melee");
161 
162  // Connect signals
164  find_widget<button>(&win, "browse_unit_image", false),
165  std::bind(&editor_edit_unit::select_file, this, "data/core/images/units", "unit_image"));
167  find_widget<button>(&win, "preview_unit_image", false),
168  std::bind(&editor_edit_unit::update_image, this, "unit_image"));
170  find_widget<button>(&win, "browse_portrait_image", false),
171  std::bind(&editor_edit_unit::select_file, this, "data/core/images/portraits", "portrait_image"));
173  find_widget<button>(&win, "preview_portrait_image", false),
174  std::bind(&editor_edit_unit::update_image, this, "portrait_image"));
176  find_widget<button>(&win, "browse_small_profile_image", false),
177  std::bind(&editor_edit_unit::select_file, this, "data/core/images/portraits", "small_profile_image"));
179  find_widget<button>(&win, "preview_small_profile_image", false),
180  std::bind(&editor_edit_unit::update_image, this, "small_profile_image"));
182  find_widget<button>(&win, "browse_attack_image", false),
183  std::bind(&editor_edit_unit::select_file, this, "data/core/images/attacks", "attack_image"));
185  find_widget<button>(&win, "preview_attack_image", false),
186  std::bind(&editor_edit_unit::update_image, this, "attack_image"));
187 
188  button& load = find_widget<button>(&win, "load_unit_type", false);
190  std::stringstream tooltip;
191  tooltip << vgettext_impl("wesnoth", "Hotkey(s): ", {{}});
192  #ifdef __APPLE__
193  tooltip << "cmd+o";
194  #else
195  tooltip << "ctrl+o";
196  #endif
197  load.set_tooltip(tooltip.str());
198 
200  find_widget<button>(&win, "load_movetype", false),
201  std::bind(&editor_edit_unit::load_movetype, this));
202 
204  find_widget<slider>(&win, "resistances_slider", false),
205  std::bind(&editor_edit_unit::store_resistances, this));
207  find_widget<menu_button>(&win, "resistances_list", false),
208  std::bind(&editor_edit_unit::update_resistances, this));
210  find_widget<toggle_button>(&win, "resistances_checkbox", false),
212 
214  find_widget<slider>(&win, "defense_slider", false),
215  std::bind(&editor_edit_unit::store_defenses, this));
217  find_widget<menu_button>(&win, "defense_list", false),
218  std::bind(&editor_edit_unit::update_defenses, this));
220  find_widget<toggle_button>(&win, "defense_checkbox", false),
221  std::bind(&editor_edit_unit::enable_defense_slider, this));
222 
224  find_widget<slider>(&win, "movement_costs_slider", false),
225  std::bind(&editor_edit_unit::store_movement_costs, this));
227  find_widget<menu_button>(&win, "movement_costs_list", false),
228  std::bind(&editor_edit_unit::update_movement_costs, this));
230  find_widget<toggle_button>(&win, "movement_costs_checkbox", false),
231  std::bind(&editor_edit_unit::enable_movement_slider, this));
232 
233  if (!res_toggles_.empty()) {
235  }
236 
237  if (!def_toggles_.empty()) {
239  }
240 
241  if (!move_toggles_.empty()) {
243  }
244 
246  find_widget<menu_button>(&win, "atk_list", false),
247  std::bind(&editor_edit_unit::select_attack, this));
248 
250  find_widget<text_box>(&win, "name_box", false),
251  std::bind(&editor_edit_unit::button_state_change, this));
253  find_widget<text_box>(&win, "id_box", false),
254  std::bind(&editor_edit_unit::button_state_change, this));
255 
256  // Disable OK button at start, since ID and Name boxes are empty
258 
259  // Attack page
261  find_widget<button>(&win, "atk_new", false),
262  std::bind(&editor_edit_unit::add_attack, this));
264  find_widget<button>(&win, "atk_delete", false),
265  std::bind(&editor_edit_unit::delete_attack, this));
267  find_widget<button>(&win, "atk_next", false),
268  std::bind(&editor_edit_unit::next_attack, this));
270  find_widget<button>(&win, "atk_prev", false),
271  std::bind(&editor_edit_unit::prev_attack, this));
272 
273  update_index();
274 
275  // Setup tabs
276  listbox& selector = find_widget<listbox>(&win, "tabs", false);
278  std::bind(&editor_edit_unit::on_page_select, this));
279  stacked_widget& page = find_widget<stacked_widget>(&win, "page", false);
280  win.keyboard_capture(&selector);
281 
282  int main_index = 0;
283  selector.select_row(main_index);
284  page.select_layer(main_index);
285 }
286 
288 {
289  save_unit_type();
290 
291  const int selected_row =
292  std::max(0, find_widget<listbox>(get_window(), "tabs", false).get_selected_row());
293  find_widget<stacked_widget>(get_window(), "page", false).select_layer(static_cast<unsigned int>(selected_row));
294 
295  if (selected_row == 3) {
296  update_wml_view();
297  }
298 
300 }
301 
302 void editor_edit_unit::select_file(const std::string& default_dir, const std::string& id_stem)
303 {
305  dlg.set_title(_("Choose File"))
306  .set_ok_label(_("Select"))
308  .set_read_only(true);
309 
310  if (dlg.show()) {
311 
312  /* Convert absolute path to wesnoth relative path */
313  std::string images_dir = filesystem::get_core_images_dir();
314  std::string addons_dir = filesystem::get_current_editor_dir(addon_id_) + "/images";
315  std::stringstream path;
316 
317  if ((dlg.path().find(images_dir) == std::string::npos)
318  && (dlg.path().find(addons_dir) == std::string::npos)) {
319  /* choosen file is outside wesnoth's images dir,
320  * copy image to addons directory */
321  std::string filename = boost::filesystem::path(dlg.path()).filename().string();
322 
323  if (id_stem == "unit_image") {
324  path << addons_dir + "/units/" + filename;
325  } else if ((id_stem == "portrait_image")||(id_stem == "small_profile_image")) {
326  path << addons_dir + "/portraits/" + filename;
327  } else if (id_stem == "attack_image") {
328  path << addons_dir + "/attacks/" + filename;
329  }
330 
331  filesystem::copy_file(dlg.path(), path.str());
332  } else {
333  path << dlg.path();
334  }
335 
336  if (path.str().find(images_dir) != std::string::npos) {
337  // Image in Wesnoth core dir
338  path.str(path.str().replace(0, images_dir.size()+1, ""));
339  } else if (path.str().find(addons_dir) != std::string::npos) {
340  // Image in addons dir
341  path.str(path.str().replace(0, addons_dir.size()+1, ""));
342  }
343 
344  path.seekp(0, std::ios_base::end);
345 
346  unsigned size = 200; // TODO: Arbitrary, can be changed later.
347  if (check_big(dlg.path(), size)) {
348  path << "~SCALE(" << size << "," << size << ")";
349  }
350 
351  find_widget<text_box>(get_window(), "path_"+id_stem, false).set_value(path.str());
352  update_image(id_stem);
353  }
354 }
355 
358  if (dlg_uc.show()) {
359  const unit_type *type = unit_types.find(dlg_uc.choice());
360 
361  stacked_widget& page = find_widget<stacked_widget>(get_window(), "page", false);
362  page.select_layer(0);
363  find_widget<text_box>(get_window(), "id_box", false).set_value(type->id());
364  find_widget<text_box>(get_window(), "name_box", false).set_value(type->type_name().base_str());
365  find_widget<spinner>(get_window(), "level_box", false).set_value(type->level());
366  find_widget<slider>(get_window(), "cost_slider", false).set_value(type->cost());
367  find_widget<text_box>(get_window(), "adv_box", false).set_value(utils::join(type->advances_to()));
368  find_widget<slider>(get_window(), "hp_slider", false).set_value(type->hitpoints());
369  find_widget<slider>(get_window(), "xp_slider", false).set_value(type->experience_needed());
370  find_widget<slider>(get_window(), "move_slider", false).set_value(type->movement());
371  find_widget<scroll_text>(get_window(), "desc_box", false).set_value(type->unit_description().base_str());
372  find_widget<text_box>(get_window(), "adv_box", false).set_value(utils::join(type->advances_to(), ", "));
373  find_widget<text_box>(get_window(), "path_unit_image", false).set_value(type->image());
374  find_widget<text_box>(get_window(), "path_portrait_image", false).set_value(type->big_profile());
375 
376  for (const auto& gender : type->genders())
377  {
378  if (gender == unit_race::GENDER::MALE) {
379  find_widget<toggle_button>(get_window(), "gender_male", false).set_value(true);
380  }
381 
382  if (gender == unit_race::GENDER::FEMALE) {
383  find_widget<toggle_button>(get_window(), "gender_female", false).set_value(true);
384  }
385  }
386 
388  find_widget<menu_button>(get_window(), "race_list", false),
389  race_list_,
390  type->race_id());
391 
393  find_widget<menu_button>(get_window(), "alignment_list", false),
394  align_list_,
395  unit_alignments::get_string(type->alignment()));
396 
397  update_image("unit_image");
398 
399  page.select_layer(1);
400  find_widget<text_box>(get_window(), "path_small_profile_image", false).set_value(type->small_profile());
401 
403  find_widget<menu_button>(get_window(), "movetype_list", false),
405  type->movement_type_id());
406 
407  config cfg;
408  type->movement_type().write(cfg, false);
409  movement_ = cfg.mandatory_child("movement_costs");
410  defenses_ = cfg.mandatory_child("defense");
411  resistances_ = cfg.mandatory_child("resistance");
412 
413  // Overrides for resistance/defense/movement costs
414  for (unsigned i = 0; i < resistances_list_.size(); i++) {
415  if (!type->get_cfg().has_child("resistance")) {
416  break;
417  }
418 
419  for (const auto& attr : type->get_cfg().mandatory_child("resistance").attribute_range()) {
420  if (resistances_list_.at(i)["label"] == attr.first) {
421  res_toggles_[i] = 1;
422  }
423  }
424  }
425 
426  for (unsigned i = 0; i < defense_list_.size(); i++) {
427  if (type->get_cfg().has_child("defense")) {
428  for (const auto& attr : type->get_cfg().mandatory_child("defense").attribute_range()) {
429  if (defense_list_.at(i)["label"] == attr.first) {
430  def_toggles_[i] = 1;
431  }
432  }
433  }
434 
435  if (type->get_cfg().has_child("movement_costs")) {
436  for (const auto& attr : type->get_cfg().mandatory_child("movement_costs").attribute_range()) {
437  if (defense_list_.at(i)["label"] == attr.first) {
438  move_toggles_[i] = 1;
439  }
440  }
441  }
442  }
443 
445  update_defenses();
447 
449  find_widget<menu_button>(get_window(), "usage_list", false),
451  type->usage());
452 
453  update_image("small_profile_image");
454 
455  page.select_layer(2);
456  attacks_.clear();
457  for(const auto& atk : type->attacks())
458  {
459  config attack;
460  boost::dynamic_bitset<> enabled(specials_list_.size());
461  attack["name"] = atk.id();
462  attack["description"] = atk.name().base_str();
463  attack["icon"] = atk.icon();
464  attack["range"] = atk.range();
465  attack["damage"] = atk.damage();
466  attack["number"] = atk.num_attacks();
467  attack["type"] = atk.type();
468  attacks_.push_back(std::make_pair(enabled, attack));
469  }
470 
471  selected_attack_ = 1;
472  update_attacks();
473  update_index();
474 
475  page.select_layer(0);
476 
478  }
479 }
480 
482 
483  // Clear the config
484  type_cfg_.clear();
485 
486  // Textdomain
487  std::string current_textdomain = "wesnoth-"+addon_id_;
488 
489  stacked_widget& page = find_widget<stacked_widget>(get_window(), "page", false);
490 
491  // Page 1
492  page.select_layer(0);
493 
494  config& utype = type_cfg_.add_child("unit_type");
495  utype["id"] = find_widget<text_box>(get_window(), "id_box", false).get_value();
496  utype["name"] = t_string(find_widget<text_box>(get_window(), "name_box", false).get_value(), current_textdomain);
497  utype["image"] = find_widget<text_box>(get_window(), "path_unit_image", false).get_value();
498  utype["profile"] = find_widget<text_box>(get_window(), "path_portrait_image", false).get_value();
499  utype["level"] = find_widget<spinner>(get_window(), "level_box", false).get_value();
500  utype["advances_to"] = find_widget<text_box>(get_window(), "adv_box", false).get_value();
501  utype["hitpoints"] = find_widget<slider>(get_window(), "hp_slider", false).get_value();
502  utype["experience"] = find_widget<slider>(get_window(), "xp_slider", false).get_value();
503  utype["cost"] = find_widget<slider>(get_window(), "cost_slider", false).get_value();
504  utype["movement"] = find_widget<slider>(get_window(), "move_slider", false).get_value();
505  utype["description"] = t_string(find_widget<scroll_text>(get_window(), "desc_box", false).get_value(), current_textdomain);
506  utype["race"] = find_widget<menu_button>(get_window(), "race_list", false).get_value_string();
507  utype["alignment"] = find_widget<menu_button>(get_window(), "alignment_list", false).get_value_string();
508 
509  // Gender
510  if (find_widget<toggle_button>(get_window(), "gender_male", false).get_value()) {
511  if (find_widget<toggle_button>(get_window(), "gender_female", false).get_value()) {
512  utype["gender"] = "male,female";
513  } else {
514  utype["gender"] = "male";
515  }
516  } else {
517  if (find_widget<toggle_button>(get_window(), "gender_female", false).get_value()) {
518  utype["gender"] = "female";
519  }
520  }
521 
522  // Page 2
523  page.select_layer(1);
524 
525  utype["small_profile"] = find_widget<text_box>(get_window(), "path_small_profile_image", false).get_value();
526  utype["movement_type"] = find_widget<menu_button>(get_window(), "movetype_list", false).get_value_string();
527  utype["usage"] = find_widget<menu_button>(get_window(), "usage_list", false).get_value_string();
528 
529  if (res_toggles_.any()) {
530  config& resistances = utype.add_child("resistance");
531  int i = 0;
532  for (const auto& attr : resistances_.attribute_range()) {
533  if (res_toggles_[i]) {
534  resistances[attr.first] = resistances_[attr.first];
535  }
536  i++;
537  }
538  }
539 
540  if (def_toggles_.any()) {
541  config& defenses = utype.add_child("defense");
542  int i = 0;
543  for (const auto& attr : defenses_.attribute_range()) {
544  if (def_toggles_[i]) {
545  defenses[attr.first] = defenses_[attr.first];
546  }
547  i++;
548  }
549  }
550 
551  if (move_toggles_.any()) {
552  config& movement_costs = utype.add_child("movement_costs");
553  int i = 0;
554  for (const auto& attr : movement_.attribute_range()) {
555  if (move_toggles_[i]) {
556  movement_costs[attr.first] = movement_[attr.first];
557  }
558  i++;
559  }
560  }
561 
562  const auto& abilities_states = find_widget<multimenu_button>(get_window(), "abilities_list", false).get_toggle_states();
563  if (abilities_states.any()) {
564  unsigned int i = 0;
565  sel_abilities_.clear();
566  for (const auto& x : abilities_map_) {
567  if (i >= abilities_states.size()) {
568  break;
569  }
570 
571  if (abilities_states[i] == true) {
572  sel_abilities_.push_back(x.first);
573  }
574 
575  i++;
576  }
577  }
578 
579  // Note : attacks and abilities are not written to the config, since they have macros.
580 }
581 
583  find_widget<slider>(get_window(), "resistances_slider", false)
584  .set_value(
585  100 - resistances_[find_widget<menu_button>(get_window(), "resistances_list", false).get_value_string()]);
586 
587  find_widget<slider>(get_window(), "resistances_slider", false)
588  .set_active(res_toggles_[find_widget<menu_button>(get_window(), "resistances_list", false).get_value()]);
589 
590  find_widget<toggle_button>(get_window(), "resistances_checkbox", false)
591  .set_value(res_toggles_[find_widget<menu_button>(get_window(), "resistances_list", false).get_value()]);
592 }
593 
595  resistances_[find_widget<menu_button>(get_window(), "resistances_list", false).get_value_string()]
596  = 100 - find_widget<slider>(get_window(), "resistances_slider", false).get_value();
597 }
598 
600  bool toggle = find_widget<toggle_button>(get_window(), "resistances_checkbox", false).get_value();
601  res_toggles_[find_widget<menu_button>(get_window(), "resistances_list", false).get_value()] = toggle;
602  find_widget<slider>(get_window(), "resistances_slider", false).set_active(toggle);
603 }
604 
606  find_widget<slider>(get_window(), "defense_slider", false)
607  .set_value(
608  100 - defenses_[find_widget<menu_button>(get_window(), "defense_list", false).get_value_string()]);
609 
610  find_widget<slider>(get_window(), "defense_slider", false)
611  .set_active(def_toggles_[find_widget<menu_button>(get_window(), "defense_list", false).get_value()]);
612 
613  find_widget<toggle_button>(get_window(), "defense_checkbox", false)
614  .set_value(def_toggles_[find_widget<menu_button>(get_window(), "defense_list", false).get_value()]);
615 }
616 
618  defenses_[find_widget<menu_button>(get_window(), "defense_list", false).get_value_string()]
619  = 100 - find_widget<slider>(get_window(), "defense_slider", false).get_value();
620 }
621 
623  bool toggle = find_widget<toggle_button>(get_window(), "defense_checkbox", false).get_value();
624  def_toggles_[find_widget<menu_button>(get_window(), "defense_list", false).get_value()] = toggle;
625  find_widget<slider>(get_window(), "defense_slider", false).set_active(toggle);
626 }
627 
629  find_widget<slider>(get_window(), "movement_costs_slider", false)
630  .set_value(
631  movement_[find_widget<menu_button>(get_window(), "movement_costs_list", false).get_value_string()]);
632 
633  find_widget<slider>(get_window(), "movement_costs_slider", false)
634  .set_active(move_toggles_[find_widget<menu_button>(get_window(), "movement_costs_list", false).get_value()]);
635 
636  find_widget<toggle_button>(get_window(), "movement_costs_checkbox", false)
637  .set_value(move_toggles_[find_widget<menu_button>(get_window(), "movement_costs_list", false).get_value()]);
638 }
639 
641  movement_[find_widget<menu_button>(get_window(), "movement_costs_list", false).get_value_string()]
642  = find_widget<slider>(get_window(), "movement_costs_slider", false).get_value();
643 }
644 
646  bool toggle = find_widget<toggle_button>(get_window(), "movement_costs_checkbox", false).get_value();
647  move_toggles_[find_widget<menu_button>(get_window(), "movement_costs_list", false).get_value()] = toggle;
648  find_widget<slider>(get_window(), "movement_costs_slider", false).set_active(toggle);
649 }
650 
652  // Textdomain
653  std::string current_textdomain = "wesnoth-"+addon_id_;
654 
655  // Save current attack data
656  if (selected_attack_ < 1) {
657  return;
658  }
659 
660  config& attack = attacks_.at(selected_attack_-1).second;
661  stacked_widget& page = find_widget<stacked_widget>(get_window(), "page", false);
662  page.select_layer(2);
663 
664  attack["name"] = find_widget<text_box>(get_window(), "atk_id_box", false).get_value();
665  attack["description"] = t_string(find_widget<text_box>(get_window(), "atk_name_box", false).get_value(), current_textdomain);
666  attack["icon"] = find_widget<text_box>(get_window(), "path_attack_image", false).get_value();
667  attack["type"] = find_widget<menu_button>(get_window(), "attack_type_list", false).get_value_string();
668  attack["damage"] = find_widget<slider>(get_window(), "dmg_box", false).get_value();
669  attack["number"] = find_widget<slider>(get_window(), "dmg_num_box", false).get_value();
670  if (find_widget<toggle_button>(get_window(), "range_melee", false).get_value()) {
671  attack["range"] = "melee";
672  }
673  if (find_widget<toggle_button>(get_window(), "range_ranged", false).get_value()) {
674  attack["range"] = "ranged";
675  }
676 
677  attacks_.at(selected_attack_-1).first = find_widget<multimenu_button>(get_window(), "weapon_specials_list", false).get_toggle_states();
678 }
679 
681  //Load data
682  config& attack = attacks_.at(selected_attack_-1).second;
683 
684  stacked_widget& page = find_widget<stacked_widget>(get_window(), "page", false);
685  page.select_layer(2);
686 
687  find_widget<text_box>(get_window(), "atk_id_box", false).set_value(attack["name"]);
688  find_widget<text_box>(get_window(), "atk_name_box", false).set_value(attack["description"]);
689  find_widget<text_box>(get_window(), "path_attack_image", false).set_value(attack["icon"]);
690  update_image("attack_image");
691  find_widget<slider>(get_window(), "dmg_box", false).set_value(attack["damage"]);
692  find_widget<slider>(get_window(), "dmg_num_box", false).set_value(attack["number"]);
693 
694  if (attack["range"] == "melee") {
695  find_widget<toggle_button>(get_window(), "range_melee", false).set_value(true);
696  find_widget<toggle_button>(get_window(), "range_ranged", false).set_value(false);
697  }
698  if (attack["range"] == "ranged") {
699  find_widget<toggle_button>(get_window(), "range_melee", false).set_value(false);
700  find_widget<toggle_button>(get_window(), "range_ranged", false).set_value(true);
701  }
702 
704  find_widget<menu_button>(get_window(), "attack_type_list", false),
706  attack["type"]);
707 
708  find_widget<multimenu_button>(get_window(), "weapon_specials_list", false)
709  .select_options(attacks_.at(selected_attack_-1).first);
710 }
711 
713  if (selected_attack_ <= 1) {
714  find_widget<button>(get_window(), "atk_prev", false).set_active(false);
715  } else {
716  find_widget<button>(get_window(), "atk_prev", false).set_active(true);
717  }
718 
719  if (selected_attack_ > 0) {
720  find_widget<button>(get_window(), "atk_delete", false).set_active(true);
721  } else {
722  find_widget<button>(get_window(), "atk_delete", false).set_active(false);
723  }
724 
725  if (selected_attack_ == attacks_.size()) {
726  find_widget<button>(get_window(), "atk_next", false).set_active(false);
727  } else {
728  find_widget<button>(get_window(), "atk_next", false).set_active(true);
729  }
730 
731  if (attacks_.size() > 0) {
732  std::vector<config> atk_name_list;
733  for(const auto& atk_data : attacks_) {
734  atk_name_list.emplace_back("label", atk_data.second["name"]);
735  }
736  menu_button& atk_list = find_widget<menu_button>(get_window(), "atk_list", false);
737  atk_list.set_values(atk_name_list);
738  atk_list.set_selected(selected_attack_-1, false);
739  }
740 
741  //Set index
742  const std::string new_index_str = formatter() << selected_attack_ << "/" << attacks_.size();
743  find_widget<label>(get_window(), "atk_number", false).set_label(new_index_str);
744 }
745 
747  // Textdomain
748  std::string current_textdomain = "wesnoth-"+addon_id_;
749 
750  config attack;
751 
752  stacked_widget& page = find_widget<stacked_widget>(get_window(), "page", false);
753  page.select_layer(2);
754  attack["name"] = find_widget<text_box>(get_window(), "atk_id_box", false).get_value();
755  attack["description"] = t_string(find_widget<text_box>(get_window(), "atk_name_box", false).get_value(), current_textdomain);
756  attack["icon"] = find_widget<text_box>(get_window(), "path_attack_image", false).get_value();
757  attack["type"] = find_widget<menu_button>(get_window(), "attack_type_list", false).get_value_string();
758  attack["damage"] = find_widget<slider>(get_window(), "dmg_box", false).get_value();
759  attack["number"] = find_widget<slider>(get_window(), "dmg_num_box", false).get_value();
760  if (find_widget<toggle_button>(get_window(), "range_melee", false).get_value()) {
761  attack["range"] = "melee";
762  }
763  if (find_widget<toggle_button>(get_window(), "range_ranged", false).get_value()) {
764  attack["range"] = "ranged";
765  }
766 
768 
769  attacks_.insert(attacks_.begin() + selected_attack_ - 1
770  , std::make_pair(
771  find_widget<multimenu_button>(get_window(), "weapon_specials_list", false).get_toggle_states()
772  , attack));
773 
774  update_index();
775 }
776 
778  //remove attack
779  if (attacks_.size() > 0) {
780  attacks_.erase(attacks_.begin() + selected_attack_ - 1);
781  }
782 
783  if (attacks_.size() == 0) {
784  // clear fields instead since there are no attacks to show
785  selected_attack_ = 0;
786  find_widget<button>(get_window(), "atk_delete", false).set_active(false);
787  update_index();
788  } else {
789  if (selected_attack_ == 1) {
790  // 1st attack removed, show the next one
791  next_attack();
792  } else {
793  // show previous attack otherwise
794  prev_attack();
795  }
796  }
797 
798  update_index();
799 }
800 
802  store_attack();
803 
804  if (attacks_.size() > 1) {
806  update_attacks();
807  }
808 
809  update_index();
810 }
811 
813  store_attack();
814 
815  if (selected_attack_ > 0) {
817  }
818 
819  if (attacks_.size() > 1) {
820  update_attacks();
821  }
822 
823  update_index();
824 }
825 
827  selected_attack_ = find_widget<menu_button>(get_window(), "atk_list", false).get_value()+1;
828  update_attacks();
829  update_index();
830 }
831 
833  for(const auto& movetype : game_config_
834  .mandatory_child("units")
835  .child_range("movetype")) {
836  if (movetype["name"] == find_widget<menu_button>(get_window(), "movetype_list", false).get_value_string()) {
837  // Set resistances
838  resistances_ = movetype.mandatory_child("resistance");
840  // Set defense
841  defenses_ = movetype.mandatory_child("defense");
842  update_defenses();
843  // Set movement
844  movement_ = movetype.mandatory_child("movement_costs");
846  }
847  }
848 }
849 
850 void editor_edit_unit::write_macro(std::ostream& out, unsigned level, const std::string macro_name)
851 {
852  for(unsigned i = 0; i < level; i++)
853  {
854  out << "\t";
855  }
856  out << "{" << macro_name << "}\n";
857 }
858 
860  store_attack();
861  save_unit_type();
862 
863  stacked_widget& page = find_widget<stacked_widget>(get_window(), "page", false);
864  page.select_layer(3);
865 
866  std::stringstream wml_stream;
867 
868  // Textdomain
869  std::string current_textdomain = "wesnoth-"+addon_id_;
870 
871  wml_stream
872  << "#textdomain " << current_textdomain << "\n"
873  << "#\n"
874  << "# This file was generated using the scenario editor.\n"
875  << "#\n";
876 
877  {
878  config_writer out(wml_stream, false);
879  int level = 0;
880 
881  out.open_child("unit_type");
882 
883  level++;
884  for (const auto& attr : type_cfg_.mandatory_child("unit_type").attribute_range()) {
885  ::write_key_val(wml_stream, attr.first, attr.second, level, current_textdomain);
886  }
887 
888  // Abilities
889  if (!sel_abilities_.empty()) {
890  out.open_child("abilities");
891  level++;
892  for (const std::string& ability : sel_abilities_) {
893  write_macro(wml_stream, level, ability);
894  }
895  level--;
896  out.close_child("abilities");
897  }
898 
899  // Attacks
900  if (!attacks_.empty()) {
901  for (const auto& atk : attacks_) {
902  out.open_child("attack");
903  level++;
904  for (const auto& attr : atk.second.attribute_range()) {
905  if (!attr.second.empty()) {
906  ::write_key_val(wml_stream, attr.first, attr.second, level, current_textdomain);
907  }
908  }
909 
910  if(atk.first.any()) {
911  out.open_child("specials");
912  level++;
913  int i = 0;
914  for (const auto& attr : specials_map_) {
915  if (atk.first[i]) {
916  write_macro(wml_stream, level, attr.first);
917  }
918  i++;
919  }
920  level--;
921  out.close_child("specials");
922 
923  }
924  level--;
925  out.close_child("attack");
926  }
927  }
928 
929  if (!movement_.empty() && (move_toggles_.size() <= movement_.attribute_count()) && move_toggles_.any())
930  {
931  out.open_child("movement_costs");
932  level++;
933  int i = 0;
934  for (const auto& attr : movement_.attribute_range()) {
935  if (move_toggles_[i] == 1) {
936  ::write_key_val(wml_stream, attr.first, attr.second, level, current_textdomain);
937  }
938  i++;
939  }
940  level--;
941  out.close_child("movement_costs");
942  }
943 
944  if (!defenses_.empty() && def_toggles_.any() && (def_toggles_.size() <= defenses_.attribute_count()))
945  {
946  out.open_child("defense");
947  level++;
948  int i = 0;
949  for (const auto& attr : defenses_.attribute_range()) {
950  if (def_toggles_[i] == 1) {
951  ::write_key_val(wml_stream, attr.first, attr.second, level, current_textdomain);
952  }
953  i++;
954  }
955  level--;
956  out.close_child("defense");
957  }
958 
959  if (!resistances_.empty() && res_toggles_.any() && (res_toggles_.size() <= resistances_.attribute_count()))
960  {
961  out.open_child("resistance");
962  level++;
963  int i = 0;
964  for (const auto& attr : resistances_.attribute_range()) {
965  if (res_toggles_[i] == 1) {
966  ::write_key_val(wml_stream, attr.first, attr.second, level, current_textdomain);
967  }
968  i++;
969  }
970  level--;
971  out.close_child("resistance");
972  }
973 
974  out.close_child("unit_type");
975  }
976 
977  generated_wml = wml_stream.str();
978 
979  find_widget<scroll_text>(get_window(), "wml_view", false).set_label(generated_wml);
980 }
981 
982 void editor_edit_unit::update_image(const std::string& id_stem) {
983  std::string rel_path = find_widget<text_box>(get_window(), "path_"+id_stem, false).get_value();
984 
985  // remove IPF
986  if (rel_path.find("~") != std::string::npos) {
987  rel_path = rel_path.substr(0, rel_path.find("~"));
988  }
989 
990  std::string abs_path = filesystem::get_binary_file_location("images", rel_path);
991 
992  std::stringstream mod_path(rel_path);
993  mod_path.seekp(0, std::ios_base::end);
994  unsigned size = 200; // TODO: Arbitrary, can be changed later.
995  if ((abs_path.size() > 0) && check_big(abs_path, size)) {
996  mod_path << "~SCALE(" << size << "," << size << ")";
997  }
998 
999  if (id_stem == "portrait_image") {
1000  // portrait image uses same [image] as unit_image
1001  find_widget<image>(get_window(), "unit_image", false).set_label(mod_path.str());
1002  } else {
1003  find_widget<image>(get_window(), id_stem, false).set_label(mod_path.str());
1004  }
1005 
1007  get_window()->queue_redraw();
1008 }
1009 
1010 bool editor_edit_unit::check_id(std::string id) {
1011  for(char c : id) {
1012  if (!(std::isalnum(c) || c == '_' || c == ' ')) {
1013  /* One bad char means entire id string is invalid */
1014  return false;
1015  }
1016  }
1017  return true;
1018 }
1019 
1021  std::string id = find_widget<text_box>(get_window(), "id_box", false).get_value();
1022  std::string name = find_widget<text_box>(get_window(), "name_box", false).get_value();
1023  if (
1024  id.empty()
1025  || name.empty()
1026  || !check_id(id)
1027  ) {
1028  find_widget<button>(get_window(), "ok", false).set_active(false);
1029  } else {
1030  find_widget<button>(get_window(), "ok", false).set_active(true);
1031  }
1032 
1033  get_window()->queue_redraw();
1034 }
1035 
1037  std::string id = find_widget<text_box>(get_window(), "id_box", false).get_value();
1038  find_widget<button>(get_window(), "ok", false).set_active( !id.empty() || check_id(id) );
1039 
1040  get_window()->queue_redraw();
1041 }
1042 
1044  // Write the file
1045  update_wml_view();
1046 
1047  std::string unit_name = type_cfg_.mandatory_child("unit_type")["name"];
1048  boost::algorithm::replace_all(unit_name, " ", "_");
1049 
1050  // Path to <unit_type_name>.cfg
1051  std::string unit_path = filesystem::get_current_editor_dir(addon_id_) + "/units/" + unit_name + ".cfg";
1052 
1053  // Write to file
1054  try {
1056  } catch(const filesystem::io_exception& /*e*/) {
1057  // TODO : Needs an error message
1058  }
1059 }
1060 
1062  bool& handled,
1063  const SDL_Keycode key,
1064  SDL_Keymod modifier)
1065 {
1066  #ifdef __APPLE__
1067  // Idiomatic modifier key in macOS computers.
1068  const SDL_Keycode modifier_key = KMOD_GUI;
1069  #else
1070  // Idiomatic modifier key in Microsoft desktop environments. Common in
1071  // GNU/Linux as well, to some extent.
1072  const SDL_Keycode modifier_key = KMOD_CTRL;
1073  #endif
1074 
1075  // Ctrl+O shortcut for Load Unit Type
1076  switch(key) {
1077  case SDLK_o:
1078  if (modifier & modifier_key) {
1079  handled = true;
1080  load_unit_type();
1081  }
1082  break;
1083  }
1084 
1085 }
1086 
1087 }
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:159
std::size_t attribute_count() const
Count the number of non-blank attributes.
Definition: config.cpp:312
config & mandatory_child(config_key_type key, int n=0)
Returns the nth child with the given key, or throws an error if there is none.
Definition: config.cpp:367
const_attr_itors attribute_range() const
Definition: config.cpp:763
bool empty() const
Definition: config.cpp:852
void clear()
Definition: config.cpp:831
config & add_child(config_key_type key)
Definition: config.cpp:441
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
Dialog that allows user to create custom unit types.
Definition: edit_unit.hpp:38
unsigned int selected_attack_
0 means there are no attacks.
Definition: edit_unit.hpp:70
void write()
Write the cfg file.
Definition: edit_unit.cpp:1043
bool check_id(std::string id)
Utility method to check if ID contains any invalid characters.
Definition: edit_unit.cpp:1010
std::vector< config > defense_list_
Definition: edit_unit.hpp:58
boost::dynamic_bitset res_toggles_
Used to control checkboxes, so that only specific values are overridden.
Definition: edit_unit.hpp:56
const game_config_view & game_config_
Definition: edit_unit.hpp:49
std::vector< std::string > sel_abilities_
Need this because can't store macros in config.
Definition: edit_unit.hpp:64
std::vector< std::pair< boost::dynamic_bitset<>, config > > attacks_
Definition: edit_unit.hpp:61
std::vector< config > abilities_list_
Definition: edit_unit.hpp:59
void store_attack()
Callbacks for attack page.
Definition: edit_unit.cpp:651
void set_selected_from_string(menu_button &list, std::vector< config > values, std::string item)
Utility method to set state of menu_button from a string.
Definition: edit_unit.hpp:141
std::vector< config > movetype_list_
Definition: edit_unit.hpp:58
std::vector< config > race_list_
Definition: edit_unit.hpp:58
void update_defenses()
Callbacks for defense list.
Definition: edit_unit.cpp:605
std::string generated_wml
Generated WML.
Definition: edit_unit.hpp:67
void save_unit_type()
Save Unit Type data to cfg.
Definition: edit_unit.cpp:481
std::vector< config > align_list_
Definition: edit_unit.hpp:58
void button_state_change()
Callback to enable/disable OK button if ID/Name is invalid.
Definition: edit_unit.cpp:1020
bool check_big(std::string img_abs_path, const int scale_size)
Check if width/height bigger than a specified size.
Definition: edit_unit.hpp:84
void signal_handler_sdl_key_down(const event::ui_event, bool &handled, const SDL_Keycode key, SDL_Keymod modifier)
Definition: edit_unit.cpp:1061
editor_edit_unit(const game_config_view &game_config, const std::string &addon_id)
Definition: edit_unit.cpp:55
void select_file(const std::string &default_dir, const std::string &id_stem)
Callback for file select button.
Definition: edit_unit.cpp:302
boost::dynamic_bitset move_toggles_
Definition: edit_unit.hpp:56
void update_movement_costs()
Callbacks for movement list.
Definition: edit_unit.cpp:628
std::vector< config > resistances_list_
Definition: edit_unit.hpp:58
void on_page_select()
Callback when an tab item in the "page" listbox is selected.
Definition: edit_unit.cpp:287
void update_resistances()
Callback for resistance list.
Definition: edit_unit.cpp:582
const std::string & addon_id_
Definition: edit_unit.hpp:50
std::vector< config > specials_list_
Definition: edit_unit.hpp:59
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:850
boost::dynamic_bitset def_toggles_
Definition: edit_unit.hpp:56
void load_unit_type()
Load Unit Type data from cfg.
Definition: edit_unit.cpp:356
void update_wml_view()
Update wml preview.
Definition: edit_unit.cpp:859
std::vector< config > usage_type_list_
Definition: edit_unit.hpp:58
virtual void pre_show(window &window) override
Actions to be taken before showing the window.
Definition: edit_unit.cpp:79
void load_movetype()
Callback for loading movetype data in UI.
Definition: edit_unit.cpp:832
void update_image(const std::string &id_stem)
Callback for image update.
Definition: edit_unit.cpp:982
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.
Abstract base class for all modal dialogs.
bool show(const unsigned auto_close_time=0)
Shows the window.
window * get_window()
Returns a pointer to the dialog's window.
At the moment two kinds of tips are known:
Definition: tooltip.cpp:42
const std::string & choice() const
Unit type choice from the user.
Definition: unit_create.hpp:40
void add_member(selectable_item *w, const T &value)
Adds a widget/value pair to the group map.
Definition: group.hpp:42
void set_member_states(const T &value)
Sets the toggle values for all widgets besides the one associated with the specified value to false.
Definition: group.hpp:111
The listbox class.
Definition: listbox.hpp:43
bool select_row(const unsigned row, const bool select=true)
Selects a row.
Definition: listbox.cpp:243
void set_selected(unsigned selected, bool fire_event=true)
void set_values(const std::vector<::config > &values, unsigned selected=0)
void set_values(const std::vector<::config > &values)
Set the available menu options.
void select_layer(const int layer)
Selects and displays a particular layer.
const t_string & tooltip() const
void queue_redraw()
Indicates that this widget should be redrawn.
Definition: widget.cpp:454
base class of top level items, the only item which needs to store the final canvases to draw on.
Definition: window.hpp:61
void keyboard_capture(widget *widget)
Definition: window.cpp:1221
void invalidate_layout()
Updates the size of the window.
Definition: window.cpp:773
static const std::string & type()
Static type getter that does not rely on the widget being constructed.
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:397
const unit_type * find(const std::string &key, unit_type::BUILD_STATUS status=unit_type::FULL) const
Finds a unit_type by its id() and makes sure it is built to the specified level.
Definition: types.cpp:1267
const race_map & races() const
Definition: types.hpp:396
A single unit type that the player may recruit.
Definition: types.hpp:43
Declarations for File-IO.
std::string vgettext_impl(const char *domain, const char *msgid, const utils::string_map &symbols)
Implementation functions for the VGETTEXT and VNGETTEXT macros.
std::size_t i
Definition: function.cpp:968
static std::string _(const char *str)
Definition: gettext.hpp:93
void copy_file(const std::string &src, const std::string &dest)
Read a file and then writes it back out.
std::string get_binary_file_location(const std::string &type, const std::string &filename)
Returns a complete path to the actual file of a given type or an empty string if the file isn't prese...
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_core_images_dir()
std::string get_current_editor_dir(const std::string &addon_id)
Game configuration data as global variables.
Definition: build_info.cpp:61
std::string path
Definition: filesystem.cpp:84
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:203
void connect_signal_mouse_left_click(dispatcher &dispatcher, const signal &signal)
Connects a signal handler for a left mouse button click.
Definition: dispatcher.cpp:177
std::vector< game_tip > load(const config &cfg)
Loads the tips from a config.
Definition: tips.cpp:36
std::string default_dir()
Definition: editor.cpp:33
std::size_t size(const std::string &str)
Length in characters of a UTF-8 string.
Definition: unicode.cpp:85
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:157
void read(config &cfg, std::istream &in, abstract_validator *validator)
Definition: parser.cpp:627
void write_key_val(std::ostream &out, const std::string &key, const config::attribute_value &value, unsigned level, std::string &textdomain)
Definition: parser.cpp:694
An exception object used when an IO error occurs.
Definition: filesystem.hpp:64
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:1486