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 babaissarkar(Subhraman Sarkar) <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());
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());
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["id"] = atk.id();
462  attack["name"] = atk.name();
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  }
477 }
478 
480 
481  // Clear the config
482  type_cfg_.clear();
483 
484  // Textdomain
485  std::string current_textdomain = "wesnoth-"+addon_id_;
486 
487  stacked_widget& page = find_widget<stacked_widget>(get_window(), "page", false);
488 
489  // Page 1
490  page.select_layer(0);
491 
492  config& utype = type_cfg_.add_child("unit_type");
493  utype["id"] = find_widget<text_box>(get_window(), "id_box", false).get_value();
494  utype["name"] = t_string(find_widget<text_box>(get_window(), "name_box", false).get_value(), current_textdomain);
495  utype["image"] = find_widget<text_box>(get_window(), "path_unit_image", false).get_value();
496  utype["profile"] = find_widget<text_box>(get_window(), "path_portrait_image", false).get_value();
497  utype["level"] = find_widget<spinner>(get_window(), "level_box", false).get_value();
498  utype["advances_to"] = find_widget<text_box>(get_window(), "adv_box", false).get_value();
499  utype["hitpoints"] = find_widget<slider>(get_window(), "hp_slider", false).get_value();
500  utype["experience"] = find_widget<slider>(get_window(), "xp_slider", false).get_value();
501  utype["cost"] = find_widget<slider>(get_window(), "cost_slider", false).get_value();
502  utype["movement"] = find_widget<slider>(get_window(), "move_slider", false).get_value();
503  utype["description"] = t_string(find_widget<scroll_text>(get_window(), "desc_box", false).get_value(), current_textdomain);
504  utype["race"] = find_widget<menu_button>(get_window(), "race_list", false).get_value_string();
505  utype["alignment"] = find_widget<menu_button>(get_window(), "alignment_list", false).get_value_string();
506 
507  // Gender
508  if (find_widget<toggle_button>(get_window(), "gender_male", false).get_value()) {
509  if (find_widget<toggle_button>(get_window(), "gender_female", false).get_value()) {
510  utype["gender"] = "male,female";
511  } else {
512  utype["gender"] = "male";
513  }
514  } else {
515  if (find_widget<toggle_button>(get_window(), "gender_female", false).get_value()) {
516  utype["gender"] = "female";
517  }
518  }
519 
520  // Page 2
521  page.select_layer(1);
522 
523  utype["small_profile"] = find_widget<text_box>(get_window(), "path_small_profile_image", false).get_value();
524  utype["movement_type"] = find_widget<menu_button>(get_window(), "movetype_list", false).get_value_string();
525  utype["usage"] = find_widget<menu_button>(get_window(), "usage_list", false).get_value_string();
526 
527  if (res_toggles_.any()) {
528  config& resistances = utype.add_child("resistance");
529  int i = 0;
530  for (const auto& attr : resistances_.attribute_range()) {
531  if (res_toggles_[i]) {
532  resistances[attr.first] = resistances_[attr.first];
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& attr : defenses_.attribute_range()) {
542  if (def_toggles_[i]) {
543  defenses[attr.first] = defenses_[attr.first];
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& attr : movement_.attribute_range()) {
553  if (move_toggles_[i]) {
554  movement_costs[attr.first] = movement_[attr.first];
555  }
556  i++;
557  }
558  }
559 
560  const auto& abilities_states = find_widget<multimenu_button>(get_window(), "abilities_list", false).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>(get_window(), "resistances_slider", false)
582  .set_value(
583  100 - resistances_[find_widget<menu_button>(get_window(), "resistances_list", false).get_value_string()]);
584 
585  find_widget<slider>(get_window(), "resistances_slider", false)
586  .set_active(res_toggles_[find_widget<menu_button>(get_window(), "resistances_list", false).get_value()]);
587 
588  find_widget<toggle_button>(get_window(), "resistances_checkbox", false)
589  .set_value(res_toggles_[find_widget<menu_button>(get_window(), "resistances_list", false).get_value()]);
590 }
591 
593  resistances_[find_widget<menu_button>(get_window(), "resistances_list", false).get_value_string()]
594  = 100 - find_widget<slider>(get_window(), "resistances_slider", false).get_value();
595 }
596 
598  bool toggle = find_widget<toggle_button>(get_window(), "resistances_checkbox", false).get_value();
599  res_toggles_[find_widget<menu_button>(get_window(), "resistances_list", false).get_value()] = toggle;
600  find_widget<slider>(get_window(), "resistances_slider", false).set_active(toggle);
601 }
602 
604  find_widget<slider>(get_window(), "defense_slider", false)
605  .set_value(
606  100 - defenses_[find_widget<menu_button>(get_window(), "defense_list", false).get_value_string()]);
607 
608  find_widget<slider>(get_window(), "defense_slider", false)
609  .set_active(def_toggles_[find_widget<menu_button>(get_window(), "defense_list", false).get_value()]);
610 
611  find_widget<toggle_button>(get_window(), "defense_checkbox", false)
612  .set_value(def_toggles_[find_widget<menu_button>(get_window(), "defense_list", false).get_value()]);
613 }
614 
616  defenses_[find_widget<menu_button>(get_window(), "defense_list", false).get_value_string()]
617  = 100 - find_widget<slider>(get_window(), "defense_slider", false).get_value();
618 }
619 
621  bool toggle = find_widget<toggle_button>(get_window(), "defense_checkbox", false).get_value();
622  def_toggles_[find_widget<menu_button>(get_window(), "defense_list", false).get_value()] = toggle;
623  find_widget<slider>(get_window(), "defense_slider", false).set_active(toggle);
624 }
625 
627  find_widget<slider>(get_window(), "movement_costs_slider", false)
628  .set_value(
629  movement_[find_widget<menu_button>(get_window(), "movement_costs_list", false).get_value_string()]);
630 
631  find_widget<slider>(get_window(), "movement_costs_slider", false)
632  .set_active(move_toggles_[find_widget<menu_button>(get_window(), "movement_costs_list", false).get_value()]);
633 
634  find_widget<toggle_button>(get_window(), "movement_costs_checkbox", false)
635  .set_value(move_toggles_[find_widget<menu_button>(get_window(), "movement_costs_list", false).get_value()]);
636 }
637 
639  movement_[find_widget<menu_button>(get_window(), "movement_costs_list", false).get_value_string()]
640  = find_widget<slider>(get_window(), "movement_costs_slider", false).get_value();
641 }
642 
644  bool toggle = find_widget<toggle_button>(get_window(), "movement_costs_checkbox", false).get_value();
645  move_toggles_[find_widget<menu_button>(get_window(), "movement_costs_list", false).get_value()] = toggle;
646  find_widget<slider>(get_window(), "movement_costs_slider", false).set_active(toggle);
647 }
648 
650  // Save current attack data
651  if (selected_attack_ < 1) {
652  return;
653  }
654 
655  config& attack = attacks_.at(selected_attack_-1).second;
656  stacked_widget& page = find_widget<stacked_widget>(get_window(), "page", false);
657  page.select_layer(2);
658 
659  attack["id"] = find_widget<text_box>(get_window(), "atk_id_box", false).get_value();
660  attack["name"] = find_widget<text_box>(get_window(), "atk_name_box", false).get_value();
661  attack["icon"] = find_widget<text_box>(get_window(), "path_attack_image", false).get_value();
662  attack["type"] = find_widget<menu_button>(get_window(), "attack_type_list", false).get_value_string();
663  attack["damage"] = find_widget<slider>(get_window(), "dmg_box", false).get_value();
664  attack["number"] = find_widget<slider>(get_window(), "dmg_num_box", false).get_value();
665  if (find_widget<toggle_button>(get_window(), "range_melee", false).get_value()) {
666  attack["range"] = "melee";
667  }
668  if (find_widget<toggle_button>(get_window(), "range_ranged", false).get_value()) {
669  attack["range"] = "ranged";
670  }
671 
672  attacks_.at(selected_attack_-1).first = find_widget<multimenu_button>(get_window(), "weapon_specials_list", false).get_toggle_states();
673 }
674 
676  //Load data
677  config& attack = attacks_.at(selected_attack_-1).second;
678 
679  stacked_widget& page = find_widget<stacked_widget>(get_window(), "page", false);
680  page.select_layer(2);
681 
682  find_widget<text_box>(get_window(), "atk_id_box", false).set_value(attack["id"]);
683  find_widget<text_box>(get_window(), "atk_name_box", false).set_value(attack["name"]);
684  find_widget<text_box>(get_window(), "path_attack_image", false).set_value(attack["icon"]);
685  update_image("attack_image");
686  find_widget<slider>(get_window(), "dmg_box", false).set_value(attack["damage"]);
687  find_widget<slider>(get_window(), "dmg_num_box", false).set_value(attack["number"]);
688 
689  if (attack["range"] == "melee") {
690  find_widget<toggle_button>(get_window(), "range_melee", false).set_value(true);
691  find_widget<toggle_button>(get_window(), "range_ranged", false).set_value(false);
692  }
693  if (attack["range"] == "ranged") {
694  find_widget<toggle_button>(get_window(), "range_melee", false).set_value(false);
695  find_widget<toggle_button>(get_window(), "range_ranged", false).set_value(true);
696  }
697 
698 // for (unsigned int i = 0; i < resistances_list_.size(); i++) {
699 // if (resistances_list_.at(i)["label"] == attack["type"]) {
700 // find_widget<menu_button>(get_window(), "attack_type_list", false).set_value(i);
701 // break;
702 // }
703 // }
704 
705 // find_widget<menu_button>(get_window(), "attack_type_list", false).set_selected_from_string(attack["type"]);
707  find_widget<menu_button>(get_window(), "attack_type_list", false),
709  attack["type"]);
710 
711 
712  find_widget<multimenu_button>(get_window(), "weapon_specials_list", false)
713  .select_options(attacks_.at(selected_attack_-1).first);
714 
715 }
716 
718  if (selected_attack_ <= 1) {
719  find_widget<button>(get_window(), "atk_prev", false).set_active(false);
720  } else {
721  find_widget<button>(get_window(), "atk_prev", false).set_active(true);
722  }
723 
724  if (selected_attack_ > 0) {
725  find_widget<button>(get_window(), "atk_delete", false).set_active(true);
726  } else {
727  find_widget<button>(get_window(), "atk_delete", false).set_active(false);
728  }
729 
730  if (selected_attack_ == attacks_.size()) {
731  find_widget<button>(get_window(), "atk_next", false).set_active(false);
732  } else {
733  find_widget<button>(get_window(), "atk_next", false).set_active(true);
734  }
735 
736  if (attacks_.size() > 0) {
737  std::vector<config> atk_name_list;
738  for(const auto& atk_data : attacks_) {
739  atk_name_list.emplace_back("label", atk_data.second["name"]);
740  }
741  menu_button& atk_list = find_widget<menu_button>(get_window(), "atk_list", false);
742  atk_list.set_values(atk_name_list);
743  atk_list.set_selected(selected_attack_-1, false);
744  }
745 
746  //Set index
747  const std::string new_index_str = formatter() << selected_attack_ << "/" << attacks_.size();
748  find_widget<label>(get_window(), "atk_number", false).set_label(new_index_str);
749 }
750 
752  config attack;
753 
754  stacked_widget& page = find_widget<stacked_widget>(get_window(), "page", false);
755  page.select_layer(2);
756  attack["id"] = find_widget<text_box>(get_window(), "atk_id_box", false).get_value();
757  attack["name"] = find_widget<text_box>(get_window(), "atk_name_box", false).get_value();
758  attack["icon"] = find_widget<text_box>(get_window(), "path_attack_image", false).get_value();
759  attack["type"] = find_widget<menu_button>(get_window(), "attack_type_list", false).get_value_string();
760  attack["damage"] = find_widget<slider>(get_window(), "dmg_box", false).get_value();
761  attack["number"] = find_widget<slider>(get_window(), "dmg_num_box", false).get_value();
762  if (find_widget<toggle_button>(get_window(), "range_melee", false).get_value()) {
763  attack["range"] = "melee";
764  }
765  if (find_widget<toggle_button>(get_window(), "range_ranged", false).get_value()) {
766  attack["range"] = "ranged";
767  }
768 
770 
771  attacks_.insert(attacks_.begin() + selected_attack_ - 1
772  , std::make_pair(
773  find_widget<multimenu_button>(get_window(), "weapon_specials_list", false).get_toggle_states()
774  , attack));
775 
776  update_index();
777 }
778 
780  //remove attack
781  if (attacks_.size() > 0) {
782  attacks_.erase(attacks_.begin() + selected_attack_ - 1);
783  }
784 
785  if (attacks_.size() == 0) {
786  // clear fields instead since there are no attacks to show
787  selected_attack_ = 0;
788  find_widget<button>(get_window(), "atk_delete", false).set_active(false);
789  update_index();
790  } else {
791  if (selected_attack_ == 1) {
792  // 1st attack removed, show the next one
793  next_attack();
794  } else {
795  // show previous attack otherwise
796  prev_attack();
797  }
798  }
799 
800  update_index();
801 }
802 
804  store_attack();
805 
806  if (attacks_.size() > 1) {
808  update_attacks();
809  }
810 
811  update_index();
812 }
813 
815  store_attack();
816 
817  if (selected_attack_ > 0) {
819  }
820 
821  if (attacks_.size() > 1) {
822  update_attacks();
823  }
824 
825  update_index();
826 }
827 
829  selected_attack_ = find_widget<menu_button>(get_window(), "atk_list", false).get_value()+1;
830  update_attacks();
831  update_index();
832 }
833 
835  for(const auto& movetype : game_config_
836  .mandatory_child("units")
837  .child_range("movetype")) {
838  if (movetype["name"] == find_widget<menu_button>(get_window(), "movetype_list", false).get_value_string()) {
839  // Set resistances
840  resistances_ = movetype.mandatory_child("resistance");
842  // Set defense
843  defenses_ = movetype.mandatory_child("defense");
844  update_defenses();
845  // Set movement
846  movement_ = movetype.mandatory_child("movement_costs");
848  }
849  }
850 }
851 
852 void editor_edit_unit::write_macro(std::ostream& out, int level, std::string macro_name)
853 {
854  for(int i = 0; i < level; i++)
855  {
856  out << "\t";
857  }
858  out << "{" << macro_name << "}\n";
859 }
860 
862  store_attack();
863  save_unit_type();
864 
865  stacked_widget& page = find_widget<stacked_widget>(get_window(), "page", false);
866  page.select_layer(3);
867 
868  std::stringstream wml_stream;
869  // Textdomain
870  std::string current_textdomain = "wesnoth-"+addon_id_;
871 
872  wml_stream
873  << "#\n"
874  << "# This file was generated using the scenario editor.\n"
875  << "#\n";
876 
877  {
878  config_writer out(wml_stream, false);
879  //out.write(type_cfg_);
880  int level = 0;
881 
882  out.open_child("unit_type");
883 
884  level++;
885  for (const auto& attr : type_cfg_.mandatory_child("unit_type").attribute_range()) {
886  ::write_key_val(wml_stream, attr.first, attr.second, level, current_textdomain);
887  }
888 
889  // Abilities
890  if (!sel_abilities_.empty()) {
891  out.open_child("abilities");
892  level++;
893  for (const std::string& ability : sel_abilities_) {
894  write_macro(wml_stream, level, ability);
895  }
896  level--;
897  out.close_child("abilities");
898  }
899 
900  // Attacks
901  if (!attacks_.empty()) {
902  for (const auto& atk : attacks_) {
903  out.open_child("attack");
904  level++;
905  for (const auto& attr : atk.second.attribute_range()) {
906  if (!attr.second.empty()) {
907  ::write_key_val(wml_stream, attr.first, attr.second, level, current_textdomain);
908  }
909  }
910 
911  if(atk.first.any()) {
912  out.open_child("specials");
913  level++;
914  int i = 0;
915  for (const auto& attr : specials_map_) {
916  if (atk.first[i]) {
917  write_macro(wml_stream, level, attr.first);
918  }
919  i++;
920  }
921  level--;
922  out.close_child("specials");
923 
924  }
925  level--;
926  out.close_child("attack");
927  }
928  }
929 
930  if (!movement_.empty() && (move_toggles_.size() <= movement_.attribute_count()) && move_toggles_.any())
931  {
932  out.open_child("movement_costs");
933  level++;
934  int i = 0;
935  for (const auto& attr : movement_.attribute_range()) {
936  if (move_toggles_[i] == 1) {
937  ::write_key_val(wml_stream, attr.first, attr.second, level, current_textdomain);
938  }
939  i++;
940  }
941  level--;
942  out.close_child("movement_costs");
943  }
944 
945  if (!defenses_.empty() && def_toggles_.any() && (def_toggles_.size() <= defenses_.attribute_count()))
946  {
947  out.open_child("defense");
948  level++;
949  int i = 0;
950  for (const auto& attr : defenses_.attribute_range()) {
951  if (def_toggles_[i] == 1) {
952  ::write_key_val(wml_stream, attr.first, attr.second, level, current_textdomain);
953  }
954  i++;
955  }
956  level--;
957  out.close_child("defense");
958  }
959 
960  if (!resistances_.empty() && res_toggles_.any() && (res_toggles_.size() <= resistances_.attribute_count()))
961  {
962  out.open_child("resistance");
963  level++;
964  int i = 0;
965  for (const auto& attr : resistances_.attribute_range()) {
966  if (res_toggles_[i] == 1) {
967  ::write_key_val(wml_stream, attr.first, attr.second, level, current_textdomain);
968  }
969  i++;
970  }
971  level--;
972  out.close_child("resistance");
973  }
974 
975  out.close_child("unit_type");
976  }
977 
978  generated_wml = wml_stream.str();
979 
980  find_widget<scroll_text>(get_window(), "wml_view", false).set_label(generated_wml);
981 }
982 
983 void editor_edit_unit::update_image(const std::string& id_stem) {
984  std::string rel_path = find_widget<text_box>(get_window(), "path_"+id_stem, false).get_value();
985 
986  // remove IPF
987  if (rel_path.find("~") != std::string::npos) {
988  rel_path = rel_path.substr(0, rel_path.find("~"));
989  }
990 
991  std::string abs_path = filesystem::get_binary_file_location("images", rel_path);
992 
993  std::stringstream mod_path(rel_path);
994  mod_path.seekp(0, std::ios_base::end);
995  unsigned size = 200; // TODO: Arbitrary, can be changed later.
996  if ((abs_path.size() > 0) && check_big(abs_path, size)) {
997  mod_path << "~SCALE(" << size << "," << size << ")";
998  }
999 
1000  if (id_stem == "portrait_image") {
1001  // portrait image uses same [image] as unit_image
1002  find_widget<image>(get_window(), "unit_image", false).set_label(mod_path.str());
1003  } else {
1004  find_widget<image>(get_window(), id_stem, false).set_label(mod_path.str());
1005  }
1006 
1008  get_window()->queue_redraw();
1009 }
1010 
1011 bool editor_edit_unit::check_id(std::string id) {
1012  for(char c : id) {
1013  if (!(std::isalnum(c) || c == '_')) {
1014  /* One bad char means entire id string is invalid */
1015  return false;
1016  }
1017  }
1018  return true;
1019 }
1020 
1022  std::string id = find_widget<text_box>(get_window(), "id_box", false).get_value();
1023  std::string name = find_widget<text_box>(get_window(), "name_box", false).get_value();
1024  if (
1025  id.empty()
1026  || name.empty()
1027  || !check_id(id)
1028  ) {
1029  find_widget<button>(get_window(), "ok", false).set_active(false);
1030  } else {
1031  find_widget<button>(get_window(), "ok", false).set_active(true);
1032  }
1033 
1034  get_window()->queue_redraw();
1035 }
1036 
1038  std::string id = find_widget<text_box>(get_window(), "id_box", false).get_value();
1039  find_widget<button>(get_window(), "ok", false).set_active( !id.empty() || check_id(id) );
1040 
1041  get_window()->queue_redraw();
1042 }
1043 
1045  /** Write the file */
1046  update_wml_view();
1047 
1048  std::string unit_name = type_cfg_.mandatory_child("unit_type")["name"];
1049  boost::algorithm::replace_all(unit_name, " ", "_");
1050 
1051  // Path to <unit_type_name>.cfg
1052  std::string unit_path = filesystem::get_current_editor_dir(addon_id_) + "/units/" + unit_name + ".cfg";
1053 
1054  // Write to file
1055  try {
1057  } catch(const filesystem::io_exception& /*e*/) {
1058  // TODO : Needs an error message
1059  }
1060 }
1061 
1063  bool& handled,
1064  const SDL_Keycode key,
1065  SDL_Keymod modifier)
1066 {
1067  #ifdef __APPLE__
1068  // Idiomatic modifier key in macOS computers.
1069  const SDL_Keycode modifier_key = KMOD_GUI;
1070  #else
1071  // Idiomatic modifier key in Microsoft desktop environments. Common in
1072  // GNU/Linux as well, to some extent.
1073  const SDL_Keycode modifier_key = KMOD_CTRL;
1074  #endif
1075 
1076  // Ctrl+O shortcut for Load Unit Type
1077  switch(key) {
1078  case SDLK_o:
1079  if (modifier & modifier_key) {
1080  handled = true;
1081  load_unit_type();
1082  }
1083  break;
1084  }
1085 
1086 }
1087 
1088 }
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:40
unsigned int selected_attack_
0 means there are no attacks.
Definition: edit_unit.hpp:72
void write()
Write the cfg file.
Definition: edit_unit.cpp:1044
bool check_id(std::string id)
Utility method to check if ID contains any invalid characters.
Definition: edit_unit.cpp:1011
std::vector< config > defense_list_
Definition: edit_unit.hpp:60
boost::dynamic_bitset res_toggles_
Used to control checkboxes, so that only specific values are overridden.
Definition: edit_unit.hpp:58
const game_config_view & game_config_
Definition: edit_unit.hpp:51
std::vector< std::string > sel_abilities_
Need this because can't store macros in config.
Definition: edit_unit.hpp:66
std::vector< std::pair< boost::dynamic_bitset<>, config > > attacks_
Definition: edit_unit.hpp:63
std::vector< config > abilities_list_
Definition: edit_unit.hpp:61
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)
Utility method to set state of menu_button from a string.
Definition: edit_unit.hpp:143
std::vector< config > movetype_list_
Definition: edit_unit.hpp:60
std::vector< config > race_list_
Definition: edit_unit.hpp:60
void update_defenses()
Callbacks for defense list.
Definition: edit_unit.cpp:603
std::string generated_wml
Generated WML.
Definition: edit_unit.hpp:69
void save_unit_type()
Save Unit Type data to cfg.
Definition: edit_unit.cpp:479
std::vector< config > align_list_
Definition: edit_unit.hpp:60
void button_state_change()
Callback to enable/disable OK button if ID/Name is invalid.
Definition: edit_unit.cpp:1021
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:86
void signal_handler_sdl_key_down(const event::ui_event, bool &handled, const SDL_Keycode key, SDL_Keymod modifier)
Definition: edit_unit.cpp:1062
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
void write_macro(std::ostream &out, int level, std::string macro_name)
Write macro to a stream at specified tab level.
Definition: edit_unit.cpp:852
boost::dynamic_bitset move_toggles_
Definition: edit_unit.hpp:58
void update_movement_costs()
Callbacks for movement list.
Definition: edit_unit.cpp:626
std::vector< config > resistances_list_
Definition: edit_unit.hpp:60
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:580
const std::string & addon_id_
Definition: edit_unit.hpp:52
std::vector< config > specials_list_
Definition: edit_unit.hpp:61
boost::dynamic_bitset def_toggles_
Definition: edit_unit.hpp:58
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:861
std::vector< config > usage_type_list_
Definition: edit_unit.hpp:60
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:834
void update_image(const std::string &id_stem)
Callback for image update.
Definition: edit_unit.cpp:983
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.
Class to show the tips.
Definition: tooltip.cpp:56
This shows the debug-mode dialog to create new units on the map.
Definition: unit_create.hpp:47
const std::string & choice() const
Unit type choice from the user.
Definition: unit_create.hpp:52
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
A menu_button is a styled_widget to choose an element from a list of elements.
Definition: menu_button.hpp:59
void set_selected(unsigned selected, bool fire_event=true)
void set_values(const std::vector<::config > &values, unsigned selected=0)
A multimenu_button is a styled_widget to choose an element from a list of elements.
void set_values(const std::vector<::config > &values)
Set the available menu options.
A stacked widget holds several widgets on top of each other.
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:455
base class of top level items, the only item which needs to store the final canvases to draw on.
Definition: window.hpp:63
void keyboard_capture(widget *widget)
Definition: window.cpp:1221
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:60
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