The Battle for Wesnoth  1.19.1+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"
27 #include "gui/dialogs/message.hpp"
30 #include "gui/widgets/button.hpp"
31 #include "gui/widgets/image.hpp"
32 #include "gui/widgets/label.hpp"
33 #include "gui/widgets/listbox.hpp"
38 #include "gui/widgets/slider.hpp"
39 #include "gui/widgets/spinner.hpp"
41 #include "gui/widgets/text_box.hpp"
43 #include "picture.hpp"
45 #include "serialization/parser.hpp"
47 #include "units/types.hpp"
48 
49 #include <boost/algorithm/string/replace.hpp>
50 #include <boost/filesystem.hpp>
51 #include <sstream>
52 
53 namespace gui2::dialogs
54 {
55 
57 
59  : modal_dialog(window_id())
60  , game_config_(game_config)
61  , addon_id_(addon_id)
62 {
63  config specials;
64 
65  read(specials, *(preprocess_file(game_config::path+"/data/core/macros/weapon_specials.cfg", &specials_map_)));
66  for (const auto& x : specials_map_) {
67  specials_list_.emplace_back("label", x.first, "checkbox", false);
68  }
69 
70  read(specials, *(preprocess_file(game_config::path+"/data/core/macros/abilities.cfg", &abilities_map_)));
71  for (const auto& x : abilities_map_) {
72  // Don't add any macros that have INTERNAL
73  if (x.first.find("INTERNAL") == std::string::npos) {
74  abilities_list_.emplace_back("label", x.first, "checkbox", false);
75  }
76  }
77 
78  connect_signal<event::SDL_KEY_DOWN>(std::bind(
79  &editor_edit_unit::signal_handler_sdl_key_down, this, std::placeholders::_2, std::placeholders::_3, std::placeholders::_5, std::placeholders::_6));
80 }
81 
83  tab_container& tabs = find_widget<tab_container>(&win, "tabs", false);
85 
86  //
87  // Main Stats tab
88  //
89 
90  tabs.select_tab(0);
91 
92  menu_button& alignments = find_widget<menu_button>(&win, "alignment_list", false);
93  // TODO:change alignments to existing translatable strings
94  align_list_.emplace_back("label", _("lawful"));
95  align_list_.emplace_back("label", _("chaotic"));
96  align_list_.emplace_back("label", _("neutral"));
97  align_list_.emplace_back("label", _("liminal"));
98  alignments.set_values(align_list_);
99 
100  menu_button& races = find_widget<menu_button>(&win, "race_list", false);
101  for(const race_map::value_type &i : unit_types.races()) {
102  const std::string& race_name = i.second.id();
103  race_list_.emplace_back("label", race_name);
104  }
105 
106  if (race_list_.size() > 0) {
107  races.set_values(race_list_);
108  }
109 
110  button& load = find_widget<button>(&win, "load_unit_type", false);
111  std::stringstream tooltip;
112  tooltip << vgettext_impl("wesnoth", "Hotkey(s): ", {{}});
113  #ifdef __APPLE__
114  tooltip << "cmd+o";
115  #else
116  tooltip << "ctrl+o";
117  #endif
118  load.set_tooltip(tooltip.str());
120 
122  find_widget<button>(&win, "browse_unit_image", false),
123  std::bind(&editor_edit_unit::select_file, this, "data/core/images/units", "unit_image"));
125  find_widget<button>(&win, "preview_unit_image", false),
126  std::bind(&editor_edit_unit::update_image, this, "unit_image"));
128  find_widget<button>(&win, "browse_portrait_image", false),
129  std::bind(&editor_edit_unit::select_file, this, "data/core/images/portraits", "portrait_image"));
131  find_widget<button>(&win, "preview_portrait_image", false),
132  std::bind(&editor_edit_unit::update_image, this, "portrait_image"));
133 
135  find_widget<text_box>(&win, "name_box", false),
136  std::bind(&editor_edit_unit::button_state_change, this));
138  find_widget<text_box>(&win, "id_box", false),
139  std::bind(&editor_edit_unit::button_state_change, this));
140 
141  //
142  // Advanced Tab
143  //
144  tabs.select_tab(1);
145 
146  menu_button& movetypes = find_widget<menu_button>(&win, "movetype_list", false);
147  for(const auto& mt : unit_types.movement_types()) {
148  movetype_list_.emplace_back("label", mt.first);
149  }
150 
151  if (movetype_list_.size() > 0) {
152  movetypes.set_values(movetype_list_);
153  }
154 
155  menu_button& defenses = find_widget<menu_button>(&win, "defense_list", false);
156  const config& defense_attr = game_config_
157  .mandatory_child("units")
158  .mandatory_child("movetype")
159  .mandatory_child("defense");
160  for (const auto& attribute : defense_attr.attribute_range()) {
161  defense_list_.emplace_back("label", attribute.first);
162  }
163 
164  if (defense_list_.size() > 0) {
165  defenses.set_values(defense_list_);
166  def_toggles_.resize(defense_list_.size());
167  }
168 
169  menu_button& movement_costs = find_widget<menu_button>(&win, "movement_costs_list", false);
170  if (defense_list_.size() > 0) {
171  movement_costs.set_values(defense_list_);
172  move_toggles_.resize(defense_list_.size());
173  }
174 
175  menu_button& resistances = find_widget<menu_button>(&win, "resistances_list", false);
176 
177  const config& resistances_attr = game_config_
178  .mandatory_child("units")
179  .mandatory_child("movetype")
180  .mandatory_child("resistance");
181  for (const auto& attribute : resistances_attr.attribute_range()) {
182  resistances_list_.emplace_back("label", attribute.first);
183  }
184 
185  if (resistances_list_.size() > 0) {
186  resistances.set_values(resistances_list_);
187  res_toggles_.resize(resistances_list_.size());
188  }
189 
190  menu_button& usage_types = find_widget<menu_button>(&win, "usage_list", false);
191  usage_type_list_.emplace_back("label", _("scout"));
192  usage_type_list_.emplace_back("label", _("fighter"));
193  usage_type_list_.emplace_back("label", _("archer"));
194  usage_type_list_.emplace_back("label", _("mixed fighter"));
195  usage_type_list_.emplace_back("label", _("healer"));
196  usage_types.set_values(usage_type_list_);
197 
198  multimenu_button& abilities = find_widget<multimenu_button>(&win, "abilities_list", false);
199  abilities.set_values(abilities_list_);
200 
202  find_widget<button>(&win, "browse_small_profile_image", false),
203  std::bind(&editor_edit_unit::select_file, this, "data/core/images/portraits", "small_profile_image"));
205  find_widget<button>(&win, "preview_small_profile_image", false),
206  std::bind(&editor_edit_unit::update_image, this, "small_profile_image"));
208  find_widget<button>(&win, "load_movetype", false),
209  std::bind(&editor_edit_unit::load_movetype, this));
211  find_widget<slider>(&win, "resistances_slider", false),
212  std::bind(&editor_edit_unit::store_resistances, this));
214  find_widget<menu_button>(&win, "resistances_list", false),
215  std::bind(&editor_edit_unit::update_resistances, this));
217  find_widget<toggle_button>(&win, "resistances_checkbox", false),
219 
221  find_widget<slider>(&win, "defense_slider", false),
222  std::bind(&editor_edit_unit::store_defenses, this));
224  find_widget<menu_button>(&win, "defense_list", false),
225  std::bind(&editor_edit_unit::update_defenses, this));
227  find_widget<toggle_button>(&win, "defense_checkbox", false),
228  std::bind(&editor_edit_unit::enable_defense_slider, this));
229 
231  find_widget<slider>(&win, "movement_costs_slider", false),
232  std::bind(&editor_edit_unit::store_movement_costs, this));
234  find_widget<menu_button>(&win, "movement_costs_list", false),
235  std::bind(&editor_edit_unit::update_movement_costs, this));
237  find_widget<toggle_button>(&win, "movement_costs_checkbox", false),
238  std::bind(&editor_edit_unit::enable_movement_slider, this));
239 
240  if (!res_toggles_.empty()) {
242  }
243 
244  if (!def_toggles_.empty()) {
246  }
247 
248  if (!move_toggles_.empty()) {
250  }
251 
252  //
253  // Attack Tab
254  //
255  tabs.select_tab(2);
256  multimenu_button& specials = find_widget<multimenu_button>(&win, "weapon_specials_list", false);
257  specials.set_values(specials_list_);
258  group<std::string> range_group;
259  range_group.add_member(find_widget<toggle_button>(&win, "range_melee", false, true), "melee");
260  range_group.add_member(find_widget<toggle_button>(&win, "range_ranged", false, true), "ranged");
261  range_group.set_member_states("melee");
262 
263  menu_button& attack_types = find_widget<menu_button>(&win, "attack_type_list", false);
264  if (resistances_list_.size() > 0) {
265  attack_types.set_values(resistances_list_);
266  }
267 
268  // Connect signals
270  find_widget<button>(&win, "browse_attack_image", false),
271  std::bind(&editor_edit_unit::select_file, this, "data/core/images/attacks", "attack_image"));
273  find_widget<button>(&win, "preview_attack_image", false),
274  std::bind(&editor_edit_unit::update_image, this, "attack_image"));
276  find_widget<menu_button>(&win, "atk_list", false),
277  std::bind(&editor_edit_unit::select_attack, this));
279  find_widget<button>(&win, "atk_new", false),
280  std::bind(&editor_edit_unit::add_attack, this));
282  find_widget<button>(&win, "atk_delete", false),
283  std::bind(&editor_edit_unit::delete_attack, this));
285  find_widget<button>(&win, "atk_next", false),
286  std::bind(&editor_edit_unit::next_attack, this));
288  find_widget<button>(&win, "atk_prev", false),
289  std::bind(&editor_edit_unit::prev_attack, this));
290 
291  update_index();
292 
293  tabs.select_tab(0);
294 
295  // Disable OK button at start, since ID and Name boxes are empty
297 }
298 
300 {
301  save_unit_type();
302 
303  tab_container& tabs = find_widget<tab_container>(get_window(), "tabs", false);
304  if (tabs.get_active_tab_index() == 3) {
305  update_wml_view();
306  }
307 }
308 
309 void editor_edit_unit::select_file(const std::string& default_dir, const std::string& id_stem)
310 {
312  dlg.set_title(_("Choose File"))
313  .set_ok_label(_("Select"))
314  .set_path(default_dir)
315  .set_read_only(true);
316 
317  if (dlg.show()) {
318 
319  std::string dn = dlg.path();
320  const std::string& message
321  = _("This file is outside Wesnoth's data dirs. Do you wish to copy it into your add-on?");
322 
323  if(id_stem == "unit_image") {
324 
325  if (!filesystem::to_asset_path(dn, addon_id_, "images")) {
327  filesystem::copy_file(dlg.path(), dn);
328  }
329  }
330 
331  } else if((id_stem == "portrait_image")||(id_stem == "small_profile_image")) {
332 
333  if (!filesystem::to_asset_path(dn, addon_id_, "images")) {
335  filesystem::copy_file(dlg.path(), dn);
336  }
337  }
338 
339  } else if(id_stem == "attack_image") {
340 
341  if (!filesystem::to_asset_path(dn, addon_id_, "images")) {
343  filesystem::copy_file(dlg.path(), dn);
344  }
345  }
346 
347  }
348 
349  find_widget<text_box>(get_window(), "path_"+id_stem, false).set_value(dn);
350  update_image(id_stem);
351  }
352 }
353 
356  if (dlg_uc.show()) {
357  const unit_type *type = unit_types.find(dlg_uc.choice());
358 
359  tab_container& tabs = find_widget<tab_container>(get_window(), "tabs", false);
360  tabs.select_tab(0);
361 
362  find_widget<text_box>(get_window(), "id_box", false).set_value(type->id());
363  find_widget<text_box>(get_window(), "name_box", false).set_value(type->type_name().base_str());
364  find_widget<spinner>(get_window(), "level_box", false).set_value(type->level());
365  find_widget<slider>(get_window(), "cost_slider", false).set_value(type->cost());
366  find_widget<text_box>(get_window(), "adv_box", false).set_value(utils::join(type->advances_to()));
367  find_widget<slider>(get_window(), "hp_slider", false).set_value(type->hitpoints());
368  find_widget<slider>(get_window(), "xp_slider", false).set_value(type->experience_needed());
369  find_widget<slider>(get_window(), "move_slider", false).set_value(type->movement());
370  find_widget<scroll_text>(get_window(), "desc_box", false).set_value(type->unit_description().base_str());
371  find_widget<text_box>(get_window(), "adv_box", false).set_value(utils::join(type->advances_to(), ", "));
372  find_widget<text_box>(get_window(), "path_unit_image", false).set_value(type->image());
373  find_widget<text_box>(get_window(), "path_portrait_image", false).set_value(type->big_profile());
374 
375  for (const auto& gender : type->genders())
376  {
377  if (gender == unit_race::GENDER::MALE) {
378  find_widget<toggle_button>(get_window(), "gender_male", false).set_value(true);
379  }
380 
381  if (gender == unit_race::GENDER::FEMALE) {
382  find_widget<toggle_button>(get_window(), "gender_female", false).set_value(true);
383  }
384  }
385 
387  find_widget<menu_button>(get_window(), "race_list", false),
388  race_list_,
389  type->race_id());
390 
392  find_widget<menu_button>(get_window(), "alignment_list", false),
393  align_list_,
394  unit_alignments::get_string(type->alignment()));
395 
396  update_image("unit_image");
397 
398  tabs.select_tab(1);
399  find_widget<text_box>(get_window(), "path_small_profile_image", false).set_value(type->small_profile());
400 
402  find_widget<menu_button>(get_window(), "movetype_list", false),
404  type->movement_type_id());
405 
406  config cfg;
407  type->movement_type().write(cfg, false);
408  movement_ = cfg.mandatory_child("movement_costs");
409  defenses_ = cfg.mandatory_child("defense");
410  resistances_ = cfg.mandatory_child("resistance");
411 
412  // Overrides for resistance/defense/movement costs
413  for (unsigned i = 0; i < resistances_list_.size(); i++) {
414  if (!type->get_cfg().has_child("resistance")) {
415  break;
416  }
417 
418  for (const auto& attr : type->get_cfg().mandatory_child("resistance").attribute_range()) {
419  if (resistances_list_.at(i)["label"] == attr.first) {
420  res_toggles_[i] = 1;
421  }
422  }
423  }
424 
425  for (unsigned i = 0; i < defense_list_.size(); i++) {
426  if (type->get_cfg().has_child("defense")) {
427  for (const auto& attr : type->get_cfg().mandatory_child("defense").attribute_range()) {
428  if (defense_list_.at(i)["label"] == attr.first) {
429  def_toggles_[i] = 1;
430  }
431  }
432  }
433 
434  if (type->get_cfg().has_child("movement_costs")) {
435  for (const auto& attr : type->get_cfg().mandatory_child("movement_costs").attribute_range()) {
436  if (defense_list_.at(i)["label"] == attr.first) {
437  move_toggles_[i] = 1;
438  }
439  }
440  }
441  }
442 
444  update_defenses();
446 
448  find_widget<menu_button>(get_window(), "usage_list", false),
450  type->usage());
451 
452  update_image("small_profile_image");
453 
454  tabs.select_tab(2);
455  attacks_.clear();
456  for(const auto& atk : type->attacks())
457  {
458  config attack;
459  boost::dynamic_bitset<> enabled(specials_list_.size());
460  attack["name"] = atk.id();
461  attack["description"] = atk.name().base_str();
462  attack["icon"] = atk.icon();
463  attack["range"] = atk.range();
464  attack["damage"] = atk.damage();
465  attack["number"] = atk.num_attacks();
466  attack["type"] = atk.type();
467  attacks_.push_back(std::make_pair(enabled, attack));
468  }
469 
470  selected_attack_ = 1;
471  update_attacks();
472  update_index();
473 
474  tabs.select_tab(0);
475 
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  tab_container& tabs = find_widget<tab_container>(get_window(), "tabs", false);
490 
491  // Page 1
492  grid* grid = tabs.get_tab_grid(0);
493 
494 
495  config& utype = type_cfg_.add_child("unit_type");
496  utype["id"] = find_widget<text_box>(grid, "id_box", false).get_value();
497  utype["name"] = t_string(find_widget<text_box>(grid, "name_box", false).get_value(), current_textdomain);
498  utype["image"] = find_widget<text_box>(grid, "path_unit_image", false).get_value();
499  utype["profile"] = find_widget<text_box>(grid, "path_portrait_image", false).get_value();
500  utype["level"] = find_widget<spinner>(grid, "level_box", false).get_value();
501  utype["advances_to"] = find_widget<text_box>(grid, "adv_box", false).get_value();
502  utype["hitpoints"] = find_widget<slider>(grid, "hp_slider", false).get_value();
503  utype["experience"] = find_widget<slider>(grid, "xp_slider", false).get_value();
504  utype["cost"] = find_widget<slider>(grid, "cost_slider", false).get_value();
505  utype["movement"] = find_widget<slider>(grid, "move_slider", false).get_value();
506  utype["description"] = t_string(find_widget<scroll_text>(grid, "desc_box", false).get_value(), current_textdomain);
507  utype["race"] = find_widget<menu_button>(grid, "race_list", false).get_value_string();
508  utype["alignment"] = find_widget<menu_button>(grid, "alignment_list", false).get_value_string();
509 
510  // Gender
511  if (find_widget<toggle_button>(grid, "gender_male", false).get_value()) {
512  if (find_widget<toggle_button>(grid, "gender_female", false).get_value()) {
513  utype["gender"] = "male,female";
514  } else {
515  utype["gender"] = "male";
516  }
517  } else {
518  if (find_widget<toggle_button>(grid, "gender_female", false).get_value()) {
519  utype["gender"] = "female";
520  }
521  }
522 
523  // Page 2
524  grid = tabs.get_tab_grid(1);
525 
526  utype["small_profile"] = find_widget<text_box>(grid, "path_small_profile_image", false).get_value();
527  utype["movement_type"] = find_widget<menu_button>(grid, "movetype_list", false).get_value_string();
528  utype["usage"] = find_widget<menu_button>(grid, "usage_list", false).get_value_string();
529 
530  if (res_toggles_.any()) {
531  config& resistances = utype.add_child("resistance");
532  int i = 0;
533  for (const auto& attr : resistances_.attribute_range()) {
534  if (res_toggles_[i]) {
535  resistances[attr.first] = resistances_[attr.first];
536  }
537  i++;
538  }
539  }
540 
541  if (def_toggles_.any()) {
542  config& defenses = utype.add_child("defense");
543  int i = 0;
544  for (const auto& attr : defenses_.attribute_range()) {
545  if (def_toggles_[i]) {
546  defenses[attr.first] = defenses_[attr.first];
547  }
548  i++;
549  }
550  }
551 
552  if (move_toggles_.any()) {
553  config& movement_costs = utype.add_child("movement_costs");
554  int i = 0;
555  for (const auto& attr : movement_.attribute_range()) {
556  if (move_toggles_[i]) {
557  movement_costs[attr.first] = movement_[attr.first];
558  }
559  i++;
560  }
561  }
562 
563  const auto& abilities_states = find_widget<multimenu_button>(grid, "abilities_list", false).get_toggle_states();
564  if (abilities_states.any()) {
565  unsigned int i = 0;
566  sel_abilities_.clear();
567  for (const auto& x : abilities_map_) {
568  if (i >= abilities_states.size()) {
569  break;
570  }
571 
572  if (abilities_states[i] == true) {
573  sel_abilities_.push_back(x.first);
574  }
575 
576  i++;
577  }
578  }
579 
580  // Note : attacks and abilities are not written to the config, since they have macros.
581 }
582 
584  find_widget<slider>(get_window(), "resistances_slider", false)
585  .set_value(
586  100 - resistances_[find_widget<menu_button>(get_window(), "resistances_list", false).get_value_string()]);
587 
588  find_widget<slider>(get_window(), "resistances_slider", false)
589  .set_active(res_toggles_[find_widget<menu_button>(get_window(), "resistances_list", false).get_value()]);
590 
591  find_widget<toggle_button>(get_window(), "resistances_checkbox", false)
592  .set_value(res_toggles_[find_widget<menu_button>(get_window(), "resistances_list", false).get_value()]);
593 }
594 
596  resistances_[find_widget<menu_button>(get_window(), "resistances_list", false).get_value_string()]
597  = 100 - find_widget<slider>(get_window(), "resistances_slider", false).get_value();
598 }
599 
601  bool toggle = find_widget<toggle_button>(get_window(), "resistances_checkbox", false).get_value();
602  res_toggles_[find_widget<menu_button>(get_window(), "resistances_list", false).get_value()] = toggle;
603  find_widget<slider>(get_window(), "resistances_slider", false).set_active(toggle);
604 }
605 
607  find_widget<slider>(get_window(), "defense_slider", false)
608  .set_value(
609  100 - defenses_[find_widget<menu_button>(get_window(), "defense_list", false).get_value_string()]);
610 
611  find_widget<slider>(get_window(), "defense_slider", false)
612  .set_active(def_toggles_[find_widget<menu_button>(get_window(), "defense_list", false).get_value()]);
613 
614  find_widget<toggle_button>(get_window(), "defense_checkbox", false)
615  .set_value(def_toggles_[find_widget<menu_button>(get_window(), "defense_list", false).get_value()]);
616 }
617 
619  defenses_[find_widget<menu_button>(get_window(), "defense_list", false).get_value_string()]
620  = 100 - find_widget<slider>(get_window(), "defense_slider", false).get_value();
621 }
622 
624  bool toggle = find_widget<toggle_button>(get_window(), "defense_checkbox", false).get_value();
625  def_toggles_[find_widget<menu_button>(get_window(), "defense_list", false).get_value()] = toggle;
626  find_widget<slider>(get_window(), "defense_slider", false).set_active(toggle);
627 }
628 
630  find_widget<slider>(get_window(), "movement_costs_slider", false)
631  .set_value(
632  movement_[find_widget<menu_button>(get_window(), "movement_costs_list", false).get_value_string()]);
633 
634  find_widget<slider>(get_window(), "movement_costs_slider", false)
635  .set_active(move_toggles_[find_widget<menu_button>(get_window(), "movement_costs_list", false).get_value()]);
636 
637  find_widget<toggle_button>(get_window(), "movement_costs_checkbox", false)
638  .set_value(move_toggles_[find_widget<menu_button>(get_window(), "movement_costs_list", false).get_value()]);
639 }
640 
642  movement_[find_widget<menu_button>(get_window(), "movement_costs_list", false).get_value_string()]
643  = find_widget<slider>(get_window(), "movement_costs_slider", false).get_value();
644 }
645 
647  bool toggle = find_widget<toggle_button>(get_window(), "movement_costs_checkbox", false).get_value();
648  move_toggles_[find_widget<menu_button>(get_window(), "movement_costs_list", false).get_value()] = toggle;
649  find_widget<slider>(get_window(), "movement_costs_slider", false).set_active(toggle);
650 }
651 
653  // Textdomain
654  std::string current_textdomain = "wesnoth-"+addon_id_;
655 
656  // Save current attack data
657  if (selected_attack_ < 1) {
658  return;
659  }
660 
661  config& attack = attacks_.at(selected_attack_-1).second;
662 
663  tab_container& tabs = find_widget<tab_container>(get_window(), "tabs", false);
664  int prev_tab = tabs.get_active_tab_index();
665  tabs.select_tab(2);
666 
667  attack["name"] = find_widget<text_box>(get_window(), "atk_id_box", false).get_value();
668  attack["description"] = t_string(find_widget<text_box>(get_window(), "atk_name_box", false).get_value(), current_textdomain);
669  attack["icon"] = find_widget<text_box>(get_window(), "path_attack_image", false).get_value();
670  attack["type"] = find_widget<menu_button>(get_window(), "attack_type_list", false).get_value_string();
671  attack["damage"] = find_widget<slider>(get_window(), "dmg_box", false).get_value();
672  attack["number"] = find_widget<slider>(get_window(), "dmg_num_box", false).get_value();
673  if (find_widget<toggle_button>(get_window(), "range_melee", false).get_value()) {
674  attack["range"] = "melee";
675  }
676  if (find_widget<toggle_button>(get_window(), "range_ranged", false).get_value()) {
677  attack["range"] = "ranged";
678  }
679 
680  attacks_.at(selected_attack_-1).first = find_widget<multimenu_button>(get_window(), "weapon_specials_list", false).get_toggle_states();
681 
682  tabs.select_tab(prev_tab);
683 }
684 
686  //Load data
687  config& attack = attacks_.at(selected_attack_-1).second;
688 
689  tab_container& tabs = find_widget<tab_container>(get_window(), "tabs", false);
690  int prev_tab = tabs.get_active_tab_index();
691  tabs.select_tab(2);
692 
693  find_widget<text_box>(get_window(), "atk_id_box", false).set_value(attack["name"]);
694  find_widget<text_box>(get_window(), "atk_name_box", false).set_value(attack["description"]);
695  find_widget<text_box>(get_window(), "path_attack_image", false).set_value(attack["icon"]);
696  update_image("attack_image");
697  find_widget<slider>(get_window(), "dmg_box", false).set_value(attack["damage"]);
698  find_widget<slider>(get_window(), "dmg_num_box", false).set_value(attack["number"]);
699 
700  if (attack["range"] == "melee") {
701  find_widget<toggle_button>(get_window(), "range_melee", false).set_value(true);
702  find_widget<toggle_button>(get_window(), "range_ranged", false).set_value(false);
703  }
704  if (attack["range"] == "ranged") {
705  find_widget<toggle_button>(get_window(), "range_melee", false).set_value(false);
706  find_widget<toggle_button>(get_window(), "range_ranged", false).set_value(true);
707  }
708 
710  find_widget<menu_button>(get_window(), "attack_type_list", false),
712  attack["type"]);
713 
714  find_widget<multimenu_button>(get_window(), "weapon_specials_list", false)
715  .select_options(attacks_.at(selected_attack_-1).first);
716 
717  tabs.select_tab(prev_tab);
718 }
719 
721  tab_container& tabs = find_widget<tab_container>(get_window(), "tabs", false);
722  int prev_tab = tabs.get_active_tab_index();
723  tabs.select_tab(2);
724 
725  if (selected_attack_ <= 1) {
726  find_widget<button>(get_window(), "atk_prev", false).set_active(false);
727  } else {
728  find_widget<button>(get_window(), "atk_prev", false).set_active(true);
729  }
730 
731  if (selected_attack_ > 0) {
732  find_widget<button>(get_window(), "atk_delete", false).set_active(true);
733  } else {
734  find_widget<button>(get_window(), "atk_delete", false).set_active(false);
735  }
736 
737  if (selected_attack_ == attacks_.size()) {
738  find_widget<button>(get_window(), "atk_next", false).set_active(false);
739  } else {
740  find_widget<button>(get_window(), "atk_next", false).set_active(true);
741  }
742 
743  if (attacks_.size() > 0) {
744  std::vector<config> atk_name_list;
745  for(const auto& atk_data : attacks_) {
746  atk_name_list.emplace_back("label", atk_data.second["name"]);
747  }
748  menu_button& atk_list = find_widget<menu_button>(get_window(), "atk_list", false);
749  atk_list.set_values(atk_name_list);
750  atk_list.set_selected(selected_attack_-1, false);
751  }
752 
753  //Set index
754  const std::string new_index_str = formatter() << selected_attack_ << "/" << attacks_.size();
755  find_widget<label>(get_window(), "atk_number", false).set_label(new_index_str);
756 
757  tabs.select_tab(prev_tab);
758 }
759 
761  // Textdomain
762  std::string current_textdomain = "wesnoth-"+addon_id_;
763 
764  tab_container& tabs = find_widget<tab_container>(get_window(), "tabs", false);
765  int prev_tab = tabs.get_active_tab_index();
766  tabs.select_tab(2);
767 
768  config attack;
769 
770  attack["name"] = find_widget<text_box>(get_window(), "atk_id_box", false).get_value();
771  attack["description"] = t_string(find_widget<text_box>(get_window(), "atk_name_box", false).get_value(), current_textdomain);
772  attack["icon"] = find_widget<text_box>(get_window(), "path_attack_image", false).get_value();
773  attack["type"] = find_widget<menu_button>(get_window(), "attack_type_list", false).get_value_string();
774  attack["damage"] = find_widget<slider>(get_window(), "dmg_box", false).get_value();
775  attack["number"] = find_widget<slider>(get_window(), "dmg_num_box", false).get_value();
776  if (find_widget<toggle_button>(get_window(), "range_melee", false).get_value()) {
777  attack["range"] = "melee";
778  }
779  if (find_widget<toggle_button>(get_window(), "range_ranged", false).get_value()) {
780  attack["range"] = "ranged";
781  }
782 
784 
785  attacks_.insert(attacks_.begin() + selected_attack_ - 1
786  , std::make_pair(
787  find_widget<multimenu_button>(get_window(), "weapon_specials_list", false).get_toggle_states()
788  , attack));
789 
790  update_index();
791 
792  tabs.select_tab(prev_tab);
793 }
794 
796  tab_container& tabs = find_widget<tab_container>(get_window(), "tabs", false);
797  int prev_tab = tabs.get_active_tab_index();
798  tabs.select_tab(2);
799 
800  //remove attack
801  if (attacks_.size() > 0) {
802  attacks_.erase(attacks_.begin() + selected_attack_ - 1);
803  }
804 
805  if (attacks_.size() == 0) {
806  // clear fields instead since there are no attacks to show
807  selected_attack_ = 0;
808  find_widget<button>(get_window(), "atk_delete", false).set_active(false);
809  update_index();
810  } else {
811  if (selected_attack_ == 1) {
812  // 1st attack removed, show the next one
813  next_attack();
814  } else {
815  // show previous attack otherwise
816  prev_attack();
817  }
818  }
819 
820  update_index();
821 
822  tabs.select_tab(prev_tab);
823 }
824 
826  store_attack();
827 
828  if (attacks_.size() > 1) {
830  update_attacks();
831  }
832 
833  update_index();
834 }
835 
837  store_attack();
838 
839  if (selected_attack_ > 0) {
841  }
842 
843  if (attacks_.size() > 1) {
844  update_attacks();
845  }
846 
847  update_index();
848 }
849 
851  selected_attack_ = find_widget<menu_button>(get_window(), "atk_list", false).get_value()+1;
852  update_attacks();
853  update_index();
854 }
855 
856 //TODO Check if works with non-mainline movetypes
858  tab_container& tabs = find_widget<tab_container>(get_window(), "tabs", false);
859  int prev_tab = tabs.get_active_tab_index();
860  tabs.select_tab(1);
861 
862  for(const auto& movetype : game_config_
863  .mandatory_child("units")
864  .child_range("movetype")) {
865  if (movetype["name"] == find_widget<menu_button>(get_window(), "movetype_list", false).get_value_string()) {
866  // Set resistances
867  resistances_ = movetype.mandatory_child("resistance");
869  // Set defense
870  defenses_ = movetype.mandatory_child("defense");
871  update_defenses();
872  // Set movement
873  movement_ = movetype.mandatory_child("movement_costs");
875  }
876  }
877 
878  tabs.select_tab(prev_tab);
879 }
880 
881 void editor_edit_unit::write_macro(std::ostream& out, unsigned level, const std::string macro_name)
882 {
883  for(unsigned i = 0; i < level; i++)
884  {
885  out << "\t";
886  }
887  out << "{" << macro_name << "}\n";
888 }
889 
891  store_attack();
892  save_unit_type();
893 
894  tab_container& tabs = find_widget<tab_container>(get_window(), "tabs", false);
895  tabs.select_tab(3);
896 
897  std::stringstream wml_stream;
898 
899  // Textdomain
900  std::string current_textdomain = "wesnoth-"+addon_id_;
901 
902  wml_stream
903  << "#textdomain " << current_textdomain << "\n"
904  << "#\n"
905  << "# This file was generated using the scenario editor.\n"
906  << "#\n";
907 
908  {
909  config_writer out(wml_stream, false);
910  int level = 0;
911 
912  out.open_child("unit_type");
913 
914  level++;
915  for (const auto& attr : type_cfg_.mandatory_child("unit_type").attribute_range()) {
916  ::write_key_val(wml_stream, attr.first, attr.second, level, current_textdomain);
917  }
918 
919  // Abilities
920  if (!sel_abilities_.empty()) {
921  out.open_child("abilities");
922  level++;
923  for (const std::string& ability : sel_abilities_) {
924  write_macro(wml_stream, level, ability);
925  }
926  level--;
927  out.close_child("abilities");
928  }
929 
930  // Attacks
931  if (!attacks_.empty()) {
932  for (const auto& atk : attacks_) {
933  out.open_child("attack");
934  level++;
935  for (const auto& attr : atk.second.attribute_range()) {
936  if (!attr.second.empty()) {
937  ::write_key_val(wml_stream, attr.first, attr.second, level, current_textdomain);
938  }
939  }
940 
941  if(atk.first.any()) {
942  out.open_child("specials");
943  level++;
944  int i = 0;
945  for (const auto& attr : specials_map_) {
946  if (atk.first[i]) {
947  write_macro(wml_stream, level, attr.first);
948  }
949  i++;
950  }
951  level--;
952  out.close_child("specials");
953 
954  }
955  level--;
956  out.close_child("attack");
957  }
958  }
959 
960  if (!movement_.empty() && (move_toggles_.size() <= movement_.attribute_count()) && move_toggles_.any())
961  {
962  out.open_child("movement_costs");
963  level++;
964  int i = 0;
965  for (const auto& attr : movement_.attribute_range()) {
966  if (move_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("movement_costs");
973  }
974 
975  if (!defenses_.empty() && def_toggles_.any() && (def_toggles_.size() <= defenses_.attribute_count()))
976  {
977  out.open_child("defense");
978  level++;
979  int i = 0;
980  for (const auto& attr : defenses_.attribute_range()) {
981  if (def_toggles_[i] == 1) {
982  ::write_key_val(wml_stream, attr.first, attr.second, level, current_textdomain);
983  }
984  i++;
985  }
986  level--;
987  out.close_child("defense");
988  }
989 
990  if (!resistances_.empty() && res_toggles_.any() && (res_toggles_.size() <= resistances_.attribute_count()))
991  {
992  out.open_child("resistance");
993  level++;
994  int i = 0;
995  for (const auto& attr : resistances_.attribute_range()) {
996  if (res_toggles_[i] == 1) {
997  ::write_key_val(wml_stream, attr.first, attr.second, level, current_textdomain);
998  }
999  i++;
1000  }
1001  level--;
1002  out.close_child("resistance");
1003  }
1004 
1005  out.close_child("unit_type");
1006  }
1007 
1008  generated_wml = wml_stream.str();
1009 
1010  find_widget<scroll_text>(get_window(), "wml_view", false).set_label(generated_wml);
1011 }
1012 
1013 void editor_edit_unit::update_image(const std::string& id_stem) {
1014  std::string rel_path = find_widget<text_box>(get_window(), "path_"+id_stem, false).get_value();
1015 
1016  // remove IPF
1017  if (rel_path.find("~") != std::string::npos) {
1018  rel_path = rel_path.substr(0, rel_path.find("~"));
1019  }
1020 
1021  int scale_size = 200; // TODO: Arbitrary, can be changed later.
1022  if (rel_path.size() > 0) {
1023  point img_size = ::image::get_size(::image::locator{rel_path});
1024  float aspect_ratio = static_cast<float>(img_size.x)/img_size.y;
1025  if(img_size.x > scale_size) {
1026  rel_path.append("~SCALE(" + std::to_string(scale_size) + "," + std::to_string(scale_size*aspect_ratio) + ")");
1027  } else if (img_size.y > scale_size) {
1028  rel_path.append("~SCALE(" + std::to_string(scale_size/aspect_ratio) + "," + std::to_string(scale_size) + ")");
1029  }
1030  }
1031 
1032  if (id_stem == "portrait_image") {
1033  // portrait image uses same [image] as unit_image
1034  find_widget<image>(get_window(), "unit_image", false).set_label(rel_path);
1035  } else {
1036  find_widget<image>(get_window(), id_stem, false).set_label(rel_path);
1037  }
1038 
1040  get_window()->queue_redraw();
1041 }
1042 
1043 bool editor_edit_unit::check_id(std::string id) {
1044  for(char c : id) {
1045  if (!(std::isalnum(c) || c == '_' || c == ' ')) {
1046  // One bad char means entire id string is invalid
1047  return false;
1048  }
1049  }
1050  return true;
1051 }
1052 
1054  tab_container& tabs = find_widget<tab_container>(get_window(), "tabs", false);
1055 
1056  std::string id = find_widget<text_box>(tabs.get_tab_grid(0), "id_box", false).get_value();
1057  std::string name = find_widget<text_box>(tabs.get_tab_grid(0), "name_box", false).get_value();
1058 
1059  if (
1060  id.empty()
1061  || name.empty()
1062  || !check_id(id)
1063  ) {
1064  find_widget<button>(get_window(), "ok", false).set_active(false);
1065  } else {
1066  find_widget<button>(get_window(), "ok", false).set_active(true);
1067  }
1068 
1069  get_window()->queue_redraw();
1070 }
1071 
1073  // Write the file
1074  update_wml_view();
1075 
1076  std::string unit_name = type_cfg_.mandatory_child("unit_type")["name"];
1077  boost::algorithm::replace_all(unit_name, " ", "_");
1078 
1079  // Path to <unit_type_name>.cfg
1080  std::string unit_path = filesystem::get_current_editor_dir(addon_id_) + "/units/" + unit_name + filesystem::wml_extension;
1081 
1082  // Write to file
1083  try {
1085  gui2::show_transient_message("", _("Unit type saved."));
1086  } catch(const filesystem::io_exception& e) {
1087  gui2::show_transient_message("", e.what());
1088  }
1089 }
1090 
1092  bool& handled,
1093  const SDL_Keycode key,
1094  SDL_Keymod modifier)
1095 {
1096  #ifdef __APPLE__
1097  // Idiomatic modifier key in macOS computers.
1098  const SDL_Keycode modifier_key = KMOD_GUI;
1099  #else
1100  // Idiomatic modifier key in Microsoft desktop environments. Common in
1101  // GNU/Linux as well, to some extent.
1102  const SDL_Keycode modifier_key = KMOD_CTRL;
1103  #endif
1104 
1105  // Ctrl+O shortcut for Load Unit Type
1106  switch(key) {
1107  case SDLK_o:
1108  if (modifier & modifier_key) {
1109  handled = true;
1110  load_unit_type();
1111  }
1112  break;
1113  }
1114 
1115 }
1116 
1117 }
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:36
unsigned int selected_attack_
0 means there are no attacks.
Definition: edit_unit.hpp:71
void write()
Write the cfg file.
Definition: edit_unit.cpp:1072
bool check_id(std::string id)
Utility method to check if ID contains any invalid characters.
Definition: edit_unit.cpp:1043
std::vector< config > defense_list_
Definition: edit_unit.hpp:59
boost::dynamic_bitset res_toggles_
Used to control checkboxes for various resistances, defences, etc.
Definition: edit_unit.hpp:57
const game_config_view & game_config_
Definition: edit_unit.hpp:47
std::vector< std::string > sel_abilities_
Need this because can't store macros in config.
Definition: edit_unit.hpp:65
std::vector< std::pair< boost::dynamic_bitset<>, config > > attacks_
Definition: edit_unit.hpp:62
std::vector< config > abilities_list_
Definition: edit_unit.hpp:60
void store_attack()
Callbacks for attack page.
Definition: edit_unit.cpp:652
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:133
std::vector< config > movetype_list_
Definition: edit_unit.hpp:59
std::vector< config > race_list_
Definition: edit_unit.hpp:59
void update_defenses()
Callbacks for defense list.
Definition: edit_unit.cpp:606
std::string generated_wml
Generated WML.
Definition: edit_unit.hpp:68
void save_unit_type()
Save Unit Type data to cfg.
Definition: edit_unit.cpp:481
std::vector< config > align_list_
Definition: edit_unit.hpp:59
void button_state_change()
Callback to enable/disable OK button if ID/Name is invalid.
Definition: edit_unit.cpp:1053
void signal_handler_sdl_key_down(const event::ui_event, bool &handled, const SDL_Keycode key, SDL_Keymod modifier)
Definition: edit_unit.cpp:1091
editor_edit_unit(const game_config_view &game_config, const std::string &addon_id)
Definition: edit_unit.cpp:58
void select_file(const std::string &default_dir, const std::string &id_stem)
Callback for file select button.
Definition: edit_unit.cpp:309
boost::dynamic_bitset move_toggles_
Definition: edit_unit.hpp:57
void update_movement_costs()
Callbacks for movement list.
Definition: edit_unit.cpp:629
std::vector< config > resistances_list_
Definition: edit_unit.hpp:59
void on_page_select()
Callback when an tab item in the "page" listbox is selected.
Definition: edit_unit.cpp:299
void update_resistances()
Callback for resistance list.
Definition: edit_unit.cpp:583
const std::string & addon_id_
Definition: edit_unit.hpp:48
std::vector< config > specials_list_
Definition: edit_unit.hpp:60
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:881
boost::dynamic_bitset def_toggles_
Definition: edit_unit.hpp:57
void load_unit_type()
Load Unit Type data from cfg.
Definition: edit_unit.cpp:354
void update_wml_view()
Update wml preview.
Definition: edit_unit.cpp:890
std::vector< config > usage_type_list_
Definition: edit_unit.hpp:59
virtual void pre_show(window &window) override
Actions to be taken before showing the window.
Definition: edit_unit.cpp:82
void load_movetype()
Callback for loading movetype data in UI.
Definition: edit_unit.cpp:857
void update_image(const std::string &id_stem)
Callback for image update.
Definition: edit_unit.cpp:1013
file_dialog & set_ok_label(const std::string &value)
Sets the OK button label.
file_dialog & set_path(const std::string &value)
Sets the initial file selection.
file_dialog & set_title(const std::string &value)
Sets the current dialog title text.
Definition: file_dialog.hpp:59
file_dialog & set_read_only(bool value)
Whether to provide user interface elements for manipulating existing objects.
std::string path() const
Gets the current file selection.
Main class to show messages to the user.
Definition: message.hpp:36
@ yes_no_buttons
Shows a yes and no button.
Definition: message.hpp:81
Abstract base class for all modal dialogs.
bool show(const unsigned auto_close_time=0)
Shows the window.
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
Base container class.
Definition: grid.hpp:32
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
void set_selected(unsigned selected, bool fire_event=true)
void set_values(const std::vector<::config > &values, unsigned selected=0)
void set_values(const std::vector<::config > &values)
Set the available menu options.
const t_string & tooltip() const
A container widget that shows one of its pages of widgets depending on which tab the user clicked.
grid * get_tab_grid(unsigned i)
void select_tab(unsigned index)
unsigned get_active_tab_index()
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 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.
Generic locator abstracting the location of an image.
Definition: picture.hpp:63
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
const std::string wml_extension
Definition: filesystem.hpp:80
void copy_file(const std::string &src, const std::string &dest)
Read a file and then writes it back out.
void write_file(const std::string &fname, const std::string &data, std::ios_base::openmode mode)
Throws io_exception if an error occurs.
bool to_asset_path(std::string &path, std::string addon_id, std::string asset_type)
Helper function to convert absolute path to wesnoth relative path.
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:89
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
void show_transient_message(const std::string &title, const std::string &message, const std::string &image, const bool message_use_markup, const bool title_use_markup)
Shows a transient message to the user.
void show_message(const std::string &title, const std::string &msg, const std::string &button_caption, const bool auto_close, const bool message_use_markup, const bool title_use_markup)
Shows a message to the user.
Definition: message.cpp:150
@ OK
Dialog was closed with the OK button.
Definition: retval.hpp:35
point get_size(const locator &i_locator, bool skip_cache)
Returns the width and height of an image.
Definition: picture.cpp:814
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:624
void write_key_val(std::ostream &out, const std::string &key, const config::attribute_value &value, unsigned level, std::string &textdomain)
Definition: parser.cpp:691
An exception object used when an IO error occurs.
Definition: filesystem.hpp:66
Holds a 2D point.
Definition: point.hpp:25
static std::string get_string(enum_type key)
Converts a enum to its string equivalent.
Definition: enum_base.hpp:46
mock_char c
unit_type_data unit_types
Definition: types.cpp:1486
#define e