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