The Battle for Wesnoth  1.19.16+dev
theme.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2003 - 2025
3  by David White <dave@whitevine.net>
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 #include "theme.hpp"
17 
18 #include "desktop/battery_info.hpp"
19 #include "gettext.hpp"
21 #include "hotkey/hotkey_item.hpp"
22 #include "log.hpp"
23 #include "sdl/rect.hpp"
25 #include "utils/from_chars.hpp"
26 #include "wml_exception.hpp"
27 #include "game_config_view.hpp"
28 
29 #include <sstream>
30 #include <utility>
31 
32 static lg::log_domain log_display("display");
33 #define DBG_DP LOG_STREAM(debug, log_display)
34 #define LOG_DP LOG_STREAM(info, log_display)
35 #define ERR_DP LOG_STREAM(err, log_display)
36 
37 namespace
38 {
39 
40 const std::size_t DefaultFontSize = font::SIZE_NORMAL;
41 const color_t DefaultFontRGB {200, 200, 200};
42 
43 _rect ref_rect {0, 0, 0, 0};
44 }
45 
46 static int parse_signed_string(std::string_view expr)
47 {
48  if(expr.empty()) {
49  return 0;
50  }
51 
52  if(expr[0] == '+') {
53  // charconv doesn't support leading + signs, strip them out
54  expr = expr.substr(1);
55  }
56 
57  return utils::from_chars<int>(expr).value_or(0);
58 }
59 
60 static std::size_t compute(std::string expr, std::size_t ref1, std::size_t ref2 = 0)
61 {
62  std::size_t ref = 0;
63  if(expr[0] == '=') {
64  ref = ref1;
65  expr = expr.substr(1);
66  } else if((expr[0] == '+') || (expr[0] == '-')) {
67  ref = ref2;
68  }
69 
70  return ref + parse_signed_string(expr);
71 }
72 
73 // If x2 or y2 are not specified, use x1 and y1 values
74 static _rect read_rect(const config& cfg)
75 {
76  _rect rect {0, 0, 0, 0};
77  std::vector<std::string> items = utils::split(cfg["rect"].str());
78  if(items.size() >= 1)
79  rect.x1 = parse_signed_string(items[0]);
80 
81  if(items.size() >= 2)
82  rect.y1 = parse_signed_string(items[1]);
83 
84  if(items.size() >= 3)
85  rect.x2 = parse_signed_string(items[2]);
86  else
87  rect.x2 = rect.x1;
88 
89  if(items.size() >= 4)
90  rect.y2 = parse_signed_string(items[3]);
91  else
92  rect.y2 = rect.y1;
93 
94  return rect;
95 }
96 
97 static rect read_sdl_rect(const config& cfg)
98 {
99  rect sdlrect;
100  const _rect rect = read_rect(cfg);
101  sdlrect.x = rect.x1;
102  sdlrect.y = rect.y1;
103  sdlrect.w = (rect.x2 > rect.x1) ? (rect.x2 - rect.x1) : 0;
104  sdlrect.h = (rect.y2 > rect.y1) ? (rect.y2 - rect.y1) : 0;
105 
106  return sdlrect;
107 }
108 
109 static std::string resolve_rect(const std::string& rect_str)
110 {
111  _rect rect {0, 0, 0, 0};
112  std::stringstream resolved;
113  const std::vector<std::string> items = utils::split(rect_str.c_str());
114  if(items.size() >= 1) {
115  rect.x1 = compute(items[0], ref_rect.x1, ref_rect.x2);
116  resolved << rect.x1;
117  }
118  if(items.size() >= 2) {
119  rect.y1 = compute(items[1], ref_rect.y1, ref_rect.y2);
120  resolved << "," << rect.y1;
121  }
122  if(items.size() >= 3) {
123  rect.x2 = compute(items[2], ref_rect.x2, rect.x1);
124  resolved << "," << rect.x2;
125  }
126  if(items.size() >= 4) {
127  rect.y2 = compute(items[3], ref_rect.y2, rect.y1);
128  resolved << "," << rect.y2;
129  }
130 
131  // DBG_DP << "Rect " << rect_str << "\t: " << resolved.str();
132 
133  ref_rect = rect;
134  return resolved.str();
135 }
136 
137 static config& find_ref(const std::string& id, config& cfg, bool remove = false)
138 {
139  static config empty_config;
140 
142  for(config::all_children_iterator i = itors.begin(); i != itors.end(); ++i) {
143  config& icfg = i->cfg;
144  if(i->cfg["id"] == id) {
145  if(remove) {
146  cfg.erase(i);
147  return empty_config;
148  } else {
149  return icfg;
150  }
151  }
152 
153  // Recursively look in children.
154  config& c = find_ref(id, icfg, remove);
155  if(&c != &empty_config) {
156  return c;
157  }
158  }
159 
160  // Not found.
161  return empty_config;
162 }
163 
164 #ifdef DEBUG
165 
166 // to be called from gdb
167 static config& find_ref(const char* id, config& cfg)
168 {
169  return find_ref(std::string(id), cfg);
170 }
171 
172 namespace
173 {
174 // avoid some compiler warnings in stricter mode.
175 static config cfg;
176 static config& result = find_ref("", cfg);
177 } // namespace
178 
179 #endif
180 
181 /**
182  * Returns a copy of the wanted resolution.
183  *
184  * The function returns a copy since our caller uses a copy of this resolution
185  * as base to expand a partial resolution.
186  *
187  * @param resolutions A config object containing the expanded
188  * resolutions.
189  * @param id The id of the resolution to return.
190  *
191  * @throw config::error If the @p id is not found.
192  *
193  * @returns A copy of the resolution config.
194  */
195 static config get_resolution(const config& resolutions, const std::string& id)
196 {
197  for(const auto& resolution : resolutions.child_range("resolution")) {
198  if(resolution["id"] == id) {
199  return resolution;
200  }
201  }
202 
203  throw config::error("[partialresolution] refers to non-existent [resolution] " + id);
204 }
205 
206 /**
207  * Returns a config with all partial resolutions of a theme expanded.
208  *
209  * @param theme The original object, whose objects need to be
210  * expanded.
211  *
212  * @returns A new object with the expanded resolutions in
213  * a theme. This object no longer contains
214  * partial resolutions.
215  */
217 {
218  config result;
219 
220  // Add all the resolutions
221  for(const auto& resolution : theme.child_range("resolution")) {
222  result.add_child("resolution", resolution);
223  }
224 
225  // Resolve all the partialresolutions
226  for(const auto& part : theme.child_range("partialresolution")) {
227  config resolution = get_resolution(result, part["inherits"]);
228  resolution.merge_attributes(part);
229 
230  for(const auto& remove : part.child_range("remove")) {
231  VALIDATE(!remove["id"].empty(), missing_mandatory_wml_key("[theme][partialresolution][remove]", "id"));
232 
233  find_ref(remove["id"], resolution, true);
234  }
235 
236  for(const auto& change : part.child_range("change")) {
237  VALIDATE(!change["id"].empty(), missing_mandatory_wml_key("[theme][partialresolution][change]", "id"));
238 
239  config& target = find_ref(change["id"], resolution, false);
240  target.merge_attributes(change);
241  }
242 
243  // cannot add [status] sub-elements, but who cares
244  for(const auto& add : part.child_range("add")) {
245  for(const auto [key, cfg] : add.all_children_view()) {
246  resolution.add_child(key, cfg);
247  }
248  }
249 
250  result.add_child("resolution", resolution);
251  }
252 
253  return result;
254 }
255 
256 static void do_resolve_rects(const config& cfg, config& resolved_config, config* resol_cfg = nullptr)
257 {
258  // recursively resolve children
259  for(const auto [child_key, child_cfg] : cfg.all_children_view()) {
260  config& dest = resolved_config.add_child(child_key);
261  do_resolve_rects(child_cfg, dest, child_key == "resolution" ? &dest : resol_cfg);
262  }
263 
264  // copy all key/values
265  resolved_config.merge_attributes(cfg);
266 
267  // override default reference rect with "ref" parameter if any
268  if(!cfg["ref"].empty()) {
269  if(resol_cfg == nullptr) {
270  ERR_DP << "Use of ref= outside a [resolution] block";
271  } else {
272  // DBG_DP << ">> Looking for " << cfg["ref"];
273  const config& ref = find_ref(cfg["ref"], *resol_cfg);
274 
275  if(ref["id"].empty()) {
276  ERR_DP << "Reference to non-existent rect id \"" << cfg["ref"] << "\"";
277  } else if(ref["rect"].empty()) {
278  ERR_DP << "Reference to id \"" << cfg["ref"] << "\" which does not have a \"rect\"";
279  } else {
280  ref_rect = read_rect(ref);
281  }
282  }
283  }
284  // resolve the rect value to absolute coordinates
285  if(!cfg["rect"].empty()) {
286  resolved_config["rect"] = resolve_rect(cfg["rect"]);
287  }
288 }
289 
291  : location_modified_(false)
292  , id_()
293  , loc_()
294  , relative_loc_()
295  , last_screen_()
296  , xanchor_(object::FIXED)
297  , yanchor_(object::FIXED)
298  , spec_width_(0)
299  , spec_height_(0)
300 {
301 }
302 
303 theme::object::object(std::size_t sw, std::size_t sh, const config& cfg)
304  : location_modified_(false)
305  , id_(cfg["id"])
306  , loc_(read_sdl_rect(cfg))
307  , relative_loc_()
308  , last_screen_()
309  , xanchor_(read_anchor(cfg["xanchor"]))
310  , yanchor_(read_anchor(cfg["yanchor"]))
311  , spec_width_(sw)
312  , spec_height_(sh)
313 {
314 }
315 
317  : size(0.0)
318  , background_image()
319  , tile_image()
320  , show_border(true)
321 {
322 }
323 
325  : size(cfg["border_size"].to_double())
326  , background_image(cfg["background_image"])
327  , tile_image(cfg["tile_image"])
328  , show_border(cfg["show_border"].to_bool(true))
329 {
330  VALIDATE(size >= 0.0 && size <= 0.5, _("border_size should be between 0.0 and 0.5."));
331 }
332 
333 rect& theme::object::location(const rect& screen) const
334 {
335  if(last_screen_ == screen && !location_modified_)
336  return relative_loc_;
337 
338  last_screen_ = screen;
339 
340  switch(xanchor_) {
341  case FIXED:
342  relative_loc_.x = loc_.x;
343  relative_loc_.w = loc_.w;
344  break;
345  case TOP_ANCHORED:
346  relative_loc_.x = loc_.x;
347  relative_loc_.w = loc_.w + screen.w - std::min<std::size_t>(spec_width_, loc_.w + screen.w);
348  break;
349  case BOTTOM_ANCHORED:
350  relative_loc_.x = loc_.x + screen.w - std::min<std::size_t>(spec_width_, loc_.x + screen.w);
351  relative_loc_.w = loc_.w;
352  break;
353  case PROPORTIONAL:
354  relative_loc_.x = (loc_.x * screen.w) / spec_width_;
355  relative_loc_.w = (loc_.w * screen.w) / spec_width_;
356  break;
357  default:
358  assert(false);
359  }
360 
361  switch(yanchor_) {
362  case FIXED:
363  relative_loc_.y = loc_.y;
364  relative_loc_.h = loc_.h;
365  break;
366  case TOP_ANCHORED:
367  relative_loc_.y = loc_.y;
368  relative_loc_.h = loc_.h + screen.h - std::min<std::size_t>(spec_height_, loc_.h + screen.h);
369  break;
370  case BOTTOM_ANCHORED:
371  relative_loc_.y = loc_.y + screen.h - std::min<std::size_t>(spec_height_, loc_.y + screen.h);
372  relative_loc_.h = loc_.h;
373  break;
374  case PROPORTIONAL:
375  relative_loc_.y = (loc_.y * screen.h) / spec_height_;
376  relative_loc_.h = (loc_.h * screen.h) / spec_height_;
377  break;
378  default:
379  assert(false);
380  }
381 
382  relative_loc_.w = std::min<int>(relative_loc_.w, screen.w);
383  if(relative_loc_.x > screen.w) {
384  relative_loc_.x = std::min<int>(relative_loc_.x, screen.w - relative_loc_.w);
385  }
386  relative_loc_.w = std::min<int>(relative_loc_.w, screen.w - relative_loc_.x);
387 
388  relative_loc_.h = std::min<int>(relative_loc_.h, screen.h);
389  if(relative_loc_.y > screen.h) {
390  relative_loc_.y = std::min<int>(relative_loc_.y, screen.h - relative_loc_.h);
391  }
392  relative_loc_.h = std::min<int>(relative_loc_.h, screen.h - relative_loc_.y);
393 
394  return relative_loc_;
395 }
396 
398 {
399  static const std::string top_anchor = "top", left_anchor = "left", bot_anchor = "bottom", right_anchor = "right",
400  proportional_anchor = "proportional";
401  if(str == top_anchor || str == left_anchor)
402  return TOP_ANCHORED;
403  else if(str == bot_anchor || str == right_anchor)
404  return BOTTOM_ANCHORED;
405  else if(str == proportional_anchor)
406  return PROPORTIONAL;
407  else
408  return FIXED;
409 }
410 
412 {
413  loc_.x = rect.x1;
414  loc_.y = rect.y1;
415  loc_.w = rect.x2 - rect.x1;
416  loc_.h = rect.y2 - rect.y1;
417  location_modified_ = true;
418 }
419 
420 void theme::object::modify_location(const std::string& rect_str, rect location_ref_rect)
421 {
422  _rect rect {0, 0, 0, 0};
423  const std::vector<std::string> items = utils::split(rect_str.c_str());
424  if(items.size() >= 1) {
425  rect.x1 = compute(items[0], location_ref_rect.x, location_ref_rect.x + location_ref_rect.w);
426  }
427  if(items.size() >= 2) {
428  rect.y1 = compute(items[1], location_ref_rect.y, location_ref_rect.y + location_ref_rect.h);
429  }
430  if(items.size() >= 3) {
431  rect.x2 = compute(items[2], location_ref_rect.x + location_ref_rect.w, rect.x1);
432  }
433  if(items.size() >= 4) {
434  rect.y2 = compute(items[3], location_ref_rect.y + location_ref_rect.h, rect.y1);
435  }
436  modify_location(rect);
437 }
438 
440  : text_()
441  , icon_()
442  , font_()
443  , font_rgb_set_(false)
444  , font_rgb_(DefaultFontRGB)
445 {
446 }
447 
448 theme::label::label(std::size_t sw, std::size_t sh, const config& cfg)
449  : object(sw, sh, cfg)
450  , text_(cfg["prefix"].str() + cfg["prefix_literal"].str() + cfg["text"].str() + cfg["postfix_literal"].str() + cfg["postfix"].str())
451  , icon_(cfg["icon"])
452  , font_(cfg["font_size"].to_size_t())
453  , font_rgb_set_(false)
454  , font_rgb_(DefaultFontRGB)
455 {
456  if(font_ == 0)
457  font_ = DefaultFontSize;
458 
459  if(cfg.has_attribute("font_rgb")) {
460  font_rgb_ = color_t::from_rgb_string(cfg["font_rgb"].str());
461  font_rgb_set_ = true;
462  }
463 }
464 
465 theme::status_item::status_item(std::size_t sw, std::size_t sh, const config& cfg)
466  : object(sw, sh, cfg)
467  , prefix_(cfg["prefix"].str() + cfg["prefix_literal"].str())
468  , postfix_(cfg["postfix_literal"].str() + cfg["postfix"].str())
469  , label_()
470  , font_(cfg["font_size"].to_size_t())
471  , font_rgb_set_(false)
472  , font_rgb_(DefaultFontRGB)
473 {
474  if(font_ == 0)
475  font_ = DefaultFontSize;
476 
477  if(auto label_child = cfg.optional_child("label")) {
478  label_ = label(sw, sh, *label_child);
479  }
480 
481  if(cfg.has_attribute("font_rgb")) {
482  font_rgb_ = color_t::from_rgb_string(cfg["font_rgb"].str());
483  font_rgb_set_ = true;
484  }
485 }
486 
487 theme::panel::panel(std::size_t sw, std::size_t sh, const config& cfg)
488  : object(sw, sh, cfg)
489  , image_(cfg["image"])
490 {
491 }
492 
494  : object()
495  , title_()
496  , tooltip_()
497  , image_()
498  , overlay_()
499  , black_line_(false)
500 {
501 }
502 theme::slider::slider(std::size_t sw, std::size_t sh, const config& cfg)
503  : object(sw, sh, cfg)
504  , title_(cfg["title"].str() + cfg["title_literal"].str())
505  , tooltip_(cfg["tooltip"])
506  , image_(cfg["image"])
507  , overlay_(cfg["overlay"])
508  , black_line_(cfg["black_line"].to_bool(false))
509 {
510 }
511 
513  : object()
514  , button_(true)
515  , context_(false)
516  , title_()
517  , tooltip_()
518  , image_()
519  , overlay_()
520  , items_()
521 {
522 }
523 
524 theme::menu::menu(std::size_t sw, std::size_t sh, const config& cfg)
525  : object(sw, sh, cfg)
526  , button_(cfg["button"].to_bool(true))
527  , context_(cfg["is_context_menu"].to_bool(false))
528  , title_(cfg["title"].str() + cfg["title_literal"].str())
529  , tooltip_(cfg["tooltip"])
530  , image_(cfg["image"])
531  , overlay_(cfg["overlay"])
532  , items_()
533 {
534  for(const auto& item : utils::split(cfg["items"])) {
535  items_.emplace_back("id", item);
536  }
537 
538  const auto& cmd = hotkey::get_hotkey_command(items_[0]["id"].str());
539  if(cfg["auto_tooltip"].to_bool() && tooltip_.empty() && items_.size() == 1) {
540  tooltip_ = cmd.description + hotkey::get_names(items_[0]["id"]) + "\n" + cmd.tooltip;
541  } else if(cfg["tooltip_name_prepend"].to_bool() && items_.size() == 1) {
542  tooltip_ = cmd.description + hotkey::get_names(items_[0]["id"]) + "\n" + tooltip_;
543  }
544 }
545 
547  : object()
548  , context_(false)
549  , auto_tooltip_(false)
550  , tooltip_name_prepend_(false)
551  , title_()
552  , tooltip_()
553  , image_()
554  , overlay_()
555  , type_()
556  , items_()
557 {
558 }
559 
560 theme::action::action(std::size_t sw, std::size_t sh, const config& cfg)
561  : object(sw, sh, cfg)
562  , context_(cfg["is_context_menu"].to_bool())
563  , auto_tooltip_(cfg["auto_tooltip"].to_bool(false))
564  , tooltip_name_prepend_(cfg["tooltip_name_prepend"].to_bool(false))
565  , title_(cfg["title"].str() + cfg["title_literal"].str())
566  , tooltip_(cfg["tooltip"])
567  , image_(cfg["image"])
568  , overlay_(cfg["overlay"])
569  , type_(cfg["type"])
570  , items_(utils::split(cfg["items"]))
571 {
572 }
573 
574 const std::string theme::action::tooltip(std::size_t index) const
575 {
576  const auto& cmd = hotkey::get_hotkey_command(items_[index]);
577  std::stringstream result;
578  if(auto_tooltip_ && tooltip_.empty() && items_.size() > index) {
579  result << cmd.description;
580  if(!hotkey::get_names(items_[index]).empty())
581  result << "\n" << _("Hotkey(s): ") << hotkey::get_names(items_[index]);
582  result << "\n" << cmd.tooltip;
583  } else if(tooltip_name_prepend_ && items_.size() == 1) {
584  result << cmd.description;
585  if(!hotkey::get_names(items_[index]).empty())
586  result << "\n" << _("Hotkey(s): ") << hotkey::get_names(items_[index]);
587  result << "\n" << tooltip_;
588  } else {
589  result << tooltip_;
590  }
591 
592  return result.str();
593 }
594 
595 theme::theme(const config& cfg, const rect& screen)
596  : theme_reset_event_("theme_reset")
597  , cur_theme()
598  , cfg_()
599  , panels_()
600  , labels_()
601  , menus_()
602  , actions_()
603  , context_()
604  , status_()
605  , main_map_()
606  , mini_map_()
607  , unit_image_()
608  , palette_()
609  , border_()
610  , screen_dimensions_(screen)
611  , cur_spec_width_(0)
612  , cur_spec_height_(0)
613 {
615  set_resolution(screen);
616 }
617 
618 bool theme::set_resolution(const rect& screen)
619 {
620  screen_dimensions_ = screen;
621 
622  bool result = false;
623 
624  int current_rating = 1000000;
625  const config* current = nullptr;
626  for(const config& i : cfg_.child_range("resolution")) {
627  int width = i["width"].to_int();
628  int height = i["height"].to_int();
629  LOG_DP << "comparing resolution " << screen.w << "," << screen.h << " to " << width << "," << height;
630  if(screen.w >= width && screen.h >= height) {
631  LOG_DP << "loading theme: " << width << "," << height;
632  current = &i;
633  result = true;
634  break;
635  }
636 
637  const int rating = width * height;
638  if(rating < current_rating) {
639  current = &i;
640  current_rating = rating;
641  }
642  }
643 
644  if(!current) {
645  if(cfg_.child_count("resolution")) {
646  ERR_DP << "No valid resolution found";
647  }
648  return false;
649  }
650  cur_spec_width_ = (*current)["width"].to_size_t();
651  cur_spec_height_ = (*current)["height"].to_size_t();
652 
653  std::map<std::string, std::string> title_stash_menus;
655  for(m = menus_.begin(); m != menus_.end(); ++m) {
656  if(!m->title().empty() && !m->get_id().empty())
657  title_stash_menus[m->get_id()] = m->title();
658  }
659 
660  std::map<std::string, std::string> title_stash_actions;
662  for(a = actions_.begin(); a != actions_.end(); ++a) {
663  if(!a->title().empty() && !a->get_id().empty())
664  title_stash_actions[a->get_id()] = a->title();
665  }
666 
667  panels_.clear();
668  labels_.clear();
669  status_.clear();
670  menus_.clear();
671  actions_.clear();
672  sliders_.clear();
673 
675 
676  for(m = menus_.begin(); m != menus_.end(); ++m) {
677  if(title_stash_menus.find(m->get_id()) != title_stash_menus.end())
678  m->set_title(title_stash_menus[m->get_id()]);
679  }
680 
681  for(a = actions_.begin(); a != actions_.end(); ++a) {
682  if(title_stash_actions.find(a->get_id()) != title_stash_actions.end())
683  a->set_title(title_stash_actions[a->get_id()]);
684  }
685 
687 
688  return result;
689 }
690 
691 void theme::add_object(std::size_t sw, std::size_t sh, const config& cfg)
692 {
693  if(const auto c = cfg.optional_child("main_map")) {
694  main_map_ = object(sw, sh, c.value());
695  }
696 
697  if(const auto c = cfg.optional_child("mini_map")) {
698  mini_map_ = object(sw, sh, c.value());
699  }
700 
701  if(const auto c = cfg.optional_child("palette")) {
702  palette_ = object(sw, sh, c.value());
703  }
704 
705  if(const auto status_cfg = cfg.optional_child("status")) {
706  for(const auto [child_key, child_cfg] : status_cfg->all_children_view()) {
707  status_[child_key].reset(new status_item(sw, sh, child_cfg));
708  }
709  if(const auto unit_image_cfg = status_cfg->optional_child("unit_image")) {
710  unit_image_ = object(sw, sh, unit_image_cfg.value());
711  } else {
712  unit_image_ = object();
713  }
714  }
715 
716  for(const config& p : cfg.child_range("panel")) {
717  panel new_panel(sw, sh, p);
718  set_object_location(new_panel, p["rect"], p["ref"]);
719  panels_.push_back(new_panel);
720  }
721 
722  for(const config& lb : cfg.child_range("label")) {
723  label new_label(sw, sh, lb);
724  set_object_location(new_label, lb["rect"], lb["ref"]);
725  labels_.push_back(new_label);
726  }
727 
728  for(const config& m : cfg.child_range("menu")) {
729  menu new_menu(sw, sh, m);
730  DBG_DP << "adding menu: " << (new_menu.is_context() ? "is context" : "not context");
731  if(new_menu.is_context())
732  context_ = new_menu;
733  else {
734  set_object_location(new_menu, m["rect"], m["ref"]);
735  menus_.push_back(new_menu);
736  }
737 
738  DBG_DP << "done adding menu...";
739  }
740 
741  for(const config& a : cfg.child_range("action")) {
742  action new_action(sw, sh, a);
743  DBG_DP << "adding action: " << (new_action.is_context() ? "is context" : "not context");
744  if(new_action.is_context())
745  action_context_ = new_action;
746  else {
747  set_object_location(new_action, a["rect"], a["ref"]);
748  actions_.push_back(new_action);
749  }
750 
751  DBG_DP << "done adding action...";
752  }
753 
754  for(const config& s : cfg.child_range("slider")) {
755  slider new_slider(sw, sh, s);
756  DBG_DP << "adding slider";
757  set_object_location(new_slider, s["rect"], s["ref"]);
758  sliders_.push_back(new_slider);
759 
760  DBG_DP << "done adding slider...";
761  }
762 
763  if(auto c = cfg.optional_child("main_map_border")) {
764  border_ = border_t(*c);
765  }
766 
767  // Battery charge indicator is always hidden if there isn't enough horizontal space
768  // (GitHub issue #3714)
769  static const int BATTERY_ICON_MIN_WIDTH = 1152;
770  if(!desktop::battery_info::does_device_have_battery() || screen_dimensions_.w < BATTERY_ICON_MIN_WIDTH) {
771  if(auto c = cfg.optional_child("no_battery")) {
772  modify(*c);
773  }
774  }
775 }
776 
777 void theme::remove_object(const std::string& id)
778 {
779  if(status_.erase(id) > 0u) {
780  return;
781  }
782 
783  for(auto p = panels_.begin(); p != panels_.end(); ++p) {
784  if(p->get_id() == id) {
785  panels_.erase(p);
786  return;
787  }
788  }
789  for(auto l = labels_.begin(); l != labels_.end(); ++l) {
790  if(l->get_id() == id) {
791  labels_.erase(l);
792  return;
793  }
794  }
795  for(auto m = menus_.begin(); m != menus_.end(); ++m) {
796  if(m->get_id() == id) {
797  menus_.erase(m);
798  return;
799  }
800  }
801  for(auto a = actions_.begin(); a != actions_.end(); ++a) {
802  if(a->get_id() == id) {
803  actions_.erase(a);
804  return;
805  }
806  }
807  for(auto s = sliders_.begin(); s != sliders_.end(); ++s) {
808  if(s->get_id() == id) {
809  sliders_.erase(s);
810  return;
811  }
812  }
813 
814  std::stringstream stream;
815  stream << "theme object " << id << " not found";
816  throw config::error(stream.str());
817 }
818 
819 void theme::set_object_location(theme::object& element, const std::string& rect_str, std::string ref_id)
820 {
821  theme::object ref_element = element;
822  if(ref_id.empty()) {
823  ref_id = element.get_id();
824  } else {
825  ref_element = find_element(ref_id);
826  }
827  if(ref_element.get_id() == ref_id) {
828  rect location_ref_rect = ref_element.get_location();
829  element.modify_location(rect_str, location_ref_rect);
830  }
831 }
832 
834 {
835  std::map<std::string, std::string> title_stash;
837  for(m = menus_.begin(); m != menus_.end(); ++m) {
838  if(!m->title().empty() && !m->get_id().empty())
839  title_stash[m->get_id()] = m->title();
840  }
841 
843  for(a = actions_.begin(); a != actions_.end(); ++a) {
844  if(!a->title().empty() && !a->get_id().empty())
845  title_stash[a->get_id()] = a->title();
846  }
847 
848  // Change existing theme objects.
849  for(const config& c : cfg.child_range("change")) {
850  std::string id = c["id"];
851  std::string ref_id = c["ref"];
852  theme::object& element = find_element(id);
853  if(element.get_id() == id)
854  set_object_location(element, c["rect"], ref_id);
855  }
856 
857  // Add new theme objects.
858  for(const config& c : cfg.child_range("add")) {
860  }
861 
862  // Remove existent theme objects.
863  for(const config& c : cfg.child_range("remove")) {
864  remove_object(c["id"]);
865  }
866 
867  for(m = menus_.begin(); m != menus_.end(); ++m) {
868  if(title_stash.find(m->get_id()) != title_stash.end())
869  m->set_title(title_stash[m->get_id()]);
870  }
871  for(a = actions_.begin(); a != actions_.end(); ++a) {
872  if(title_stash.find(a->get_id()) != title_stash.end())
873  a->set_title(title_stash[a->get_id()]);
874  }
875 }
876 
877 theme::object& theme::find_element(const std::string& id)
878 {
879  static theme::object empty_object;
880  theme::object* res = &empty_object;
881 
882  auto status_item_it = status_.find(id);
883  if(status_item_it != status_.end()) {
884  res = status_item_it->second.get();
885  }
886 
887  for(std::vector<theme::panel>::iterator p = panels_.begin(); p != panels_.end(); ++p) {
888  if(p->get_id() == id) {
889  res = &(*p);
890  }
891  }
892  for(std::vector<theme::label>::iterator l = labels_.begin(); l != labels_.end(); ++l) {
893  if(l->get_id() == id) {
894  res = &(*l);
895  }
896  }
897  for(std::vector<theme::menu>::iterator m = menus_.begin(); m != menus_.end(); ++m) {
898  if(m->get_id() == id) {
899  res = &(*m);
900  }
901  }
902  for(std::vector<theme::action>::iterator a = actions_.begin(); a != actions_.end(); ++a) {
903  if(a->get_id() == id) {
904  res = &(*a);
905  }
906  }
907  if(id == "main-map") {
908  res = &main_map_;
909  }
910  if(id == "mini-map") {
911  res = &mini_map_;
912  }
913  if(id == "palette") {
914  res = &palette_;
915  }
916  if(id == "unit-image") {
917  res = &unit_image_;
918  }
919  return *res;
920 }
921 
922 const theme::status_item* theme::get_status_item(const std::string& key) const
923 {
924  const auto& i = status_.find(key);
925  if(i != status_.end())
926  return i->second.get();
927  else
928  return nullptr;
929 }
930 
931 const theme::menu* theme::get_menu_item(const std::string& key) const
932 {
933  for(const theme::menu& m : menus_) {
934  if(m.get_id() == key)
935  return &m;
936  }
937  return nullptr;
938 }
939 
940 const theme::action* theme::get_action_item(const std::string& key) const
941 {
942  for(const theme::action& a : actions_) {
943  if(a.get_id() == key)
944  return &a;
945  }
946  return nullptr;
947 }
948 
949 theme::object* theme::refresh_title(const std::string& id, const std::string& new_title)
950 {
951  theme::object* res = nullptr;
952 
953  for(std::vector<theme::action>::iterator a = actions_.begin(); a != actions_.end(); ++a) {
954  if(a->get_id() == id) {
955  res = &(*a);
956  a->set_title(new_title);
957  }
958  }
959 
960  for(std::vector<theme::menu>::iterator m = menus_.begin(); m != menus_.end(); ++m) {
961  if(m->get_id() == id) {
962  res = &(*m);
963  m->set_title(new_title);
964  }
965  }
966 
967  return res;
968 }
969 
970 theme::object* theme::refresh_title2(const std::string& id, const std::string& title_tag)
971 {
972  std::string new_title;
973 
974  const config& cfg = find_ref(id, cfg_, false);
975  if(!cfg[title_tag].empty())
976  new_title = cfg[title_tag].str();
977 
978  return refresh_title(id, new_title + cfg["title_literal"].str());
979 }
980 
981 void theme::modify_label(const std::string& id, const std::string& text)
982 {
983  theme::label* label = dynamic_cast<theme::label*>(&find_element(id));
984  if(!label) {
985  LOG_DP << "Theme contains no label called '" << id << "'.";
986  return;
987  }
988  label->set_text(text);
989 }
990 
992 {
993  if(cfg) {
994  known_themes.clear();
995  for(const config& thm : cfg->child_range("theme")) {
996  known_themes[thm["id"]] = thm;
997  }
998  }
999 }
1000 
1001 std::vector<theme_info> theme::get_basic_theme_info(bool include_hidden)
1002 {
1003  std::vector<theme_info> res;
1004 
1005  for(const auto& [id, cfg] : known_themes) {
1006  if(!cfg["hidden"].to_bool(false) || include_hidden) {
1007  auto& info = res.emplace_back();
1008  info.id = id;
1009  info.name = cfg["name"].t_str();
1010  info.description = cfg["description"].t_str();
1011  }
1012  }
1013 
1014  return res;
1015 }
1016 
1017 const config& theme::get_theme_config(const std::string& id)
1018 {
1019  auto iter = known_themes.find(id);
1020  if(iter != known_themes.end()) {
1021  return iter->second;
1022  }
1023 
1024  if (!id.empty()) { // (treat empty id as request for default theme)
1025  ERR_DP << "Theme '" << id << "' not found."
1026  << " Falling back to default theme.";
1027  }
1028 
1029  iter = known_themes.find("Default");
1030  if(iter != known_themes.end()) {
1031  return iter->second;
1032  }
1033 
1034  ERR_DP << "Default theme not found.";
1035 
1036  static config empty;
1037  return empty;
1038 }
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:157
config & add_child(std::string_view key)
Definition: config.cpp:435
all_children_iterator erase(const all_children_iterator &i)
Definition: config.cpp:617
optional_config_impl< config > optional_child(std::string_view key, int n=0)
Equivalent to mandatory_child, but returns an empty optional if the nth child was not found.
Definition: config.cpp:379
auto all_children_view() const
In-order iteration over all children.
Definition: config.hpp:795
void merge_attributes(const config &)
Definition: config.cpp:721
child_itors child_range(std::string_view key)
Definition: config.cpp:267
const_all_children_itors all_children_range() const
In-order iteration over all children.
Definition: config.cpp:857
boost::iterator_range< all_children_iterator > all_children_itors
Definition: config.hpp:761
bool has_attribute(std::string_view key) const
Definition: config.cpp:156
std::size_t child_count(std::string_view key) const
Definition: config.cpp:291
virtual void notify_observers()
A class grating read only view to a vector of config objects, viewed as one config with all children ...
bool is_context() const
Definition: theme.hpp:171
const std::string tooltip(std::size_t index) const
Definition: theme.cpp:574
bool font_rgb_set_
Definition: theme.hpp:120
std::size_t font_
Definition: theme.hpp:119
void set_text(const std::string &text)
Definition: theme.hpp:109
color_t font_rgb_
Definition: theme.hpp:121
std::vector< config > items_
Definition: theme.hpp:243
bool is_context() const
Definition: theme.hpp:226
std::string tooltip_
Definition: theme.hpp:242
void modify_location(const _rect &rect)
Definition: theme.cpp:411
virtual rect & location(const rect &screen) const
Definition: theme.cpp:333
const rect & get_location() const
Definition: theme.hpp:53
static ANCHORING read_anchor(const std::string &str)
Definition: theme.cpp:397
const std::string & get_id() const
Definition: theme.hpp:54
panel(std::size_t sw, std::size_t sh, const config &cfg)
Definition: theme.cpp:487
color_t font_rgb_
Definition: theme.hpp:147
std::size_t font_
Definition: theme.hpp:145
status_item(std::size_t sw, std::size_t sh, const config &cfg)
Definition: theme.cpp:465
Definition: theme.hpp:43
object main_map_
Definition: theme.hpp:309
theme::object & find_element(const std::string &id)
Definition: theme.cpp:877
std::vector< panel > panels_
Definition: theme.hpp:298
void add_object(std::size_t sw, std::size_t sh, const config &cfg)
Definition: theme.cpp:691
std::vector< action > actions_
Definition: theme.hpp:301
void set_object_location(theme::object &element, const std::string &rect_str, std::string ref_id)
Definition: theme.cpp:819
std::map< std::string, std::unique_ptr< status_item > > status_
Definition: theme.hpp:307
static NOT_DANGLING const config & get_theme_config(const std::string &id)
Returns the saved config for the theme with the given ID.
Definition: theme.cpp:1017
object mini_map_
Definition: theme.hpp:309
bool set_resolution(const rect &screen)
Definition: theme.cpp:618
theme(const config &cfg, const rect &screen)
Definition: theme.cpp:595
object unit_image_
Definition: theme.hpp:309
const action * get_action_item(const std::string &key) const
Definition: theme.cpp:940
object * refresh_title(const std::string &id, const std::string &new_title)
Definition: theme.cpp:949
const menu * get_menu_item(const std::string &key) const
Definition: theme.cpp:931
menu context_
Definition: theme.hpp:304
void remove_object(const std::string &id)
Definition: theme.cpp:777
object * refresh_title2(const std::string &id, const std::string &title_tag)
Definition: theme.cpp:970
void modify(const config &cfg)
Definition: theme.cpp:833
border_t border_
Definition: theme.hpp:311
static std::map< std::string, config > known_themes
Definition: theme.hpp:316
std::vector< menu > menus_
Definition: theme.hpp:300
rect screen_dimensions_
Definition: theme.hpp:313
std::size_t cur_spec_height_
Definition: theme.hpp:314
std::vector< slider > sliders_
Definition: theme.hpp:302
std::size_t cur_spec_width_
Definition: theme.hpp:314
static std::vector< theme_info > get_basic_theme_info(bool include_hidden=false)
Returns minimal info about saved themes, optionally including hidden ones.
Definition: theme.cpp:1001
action action_context_
Definition: theme.hpp:305
void modify_label(const std::string &id, const std::string &text)
Definition: theme.cpp:981
std::vector< label > labels_
Definition: theme.hpp:299
config cfg_
Definition: theme.hpp:297
static void set_known_themes(const game_config_view *cfg)
Copies the theme configs from the main game config.
Definition: theme.cpp:991
object palette_
Definition: theme.hpp:309
events::generic_event theme_reset_event_
Definition: theme.hpp:294
const status_item * get_status_item(const std::string &item) const
Definition: theme.cpp:922
std::string cur_theme
Definition: theme.hpp:296
const config * cfg
std::size_t i
Definition: function.cpp:1032
static std::string _(const char *str)
Definition: gettext.hpp:97
std::string label
What to show in the filter's drop-down list.
Definition: manager.cpp:201
std::string id
Text to match against addon_info.tags()
Definition: manager.cpp:199
Standard logging facilities (interface).
void rect(const ::rect &rect)
Draw a rectangle.
Definition: draw.cpp:159
const int SIZE_NORMAL
Definition: constants.cpp:20
void remove()
Removes a tip.
Definition: tooltip.cpp:94
std::string get_names(const std::string &id)
Returns a comma-separated string of hotkey names.
const hotkey_command & get_hotkey_command(std::string_view command)
Returns the hotkey_command with the given id.
logger & info()
Definition: log.cpp:351
boost::variant< constant, n_var, boost::recursive_wrapper< not_op >, boost::recursive_wrapper< ternary_op > > expr
std::size_t size(std::string_view str)
Length in characters of a UTF-8 string.
Definition: unicode.cpp:81
std::size_t index(std::string_view str, const std::size_t index)
Codepoint index corresponding to the nth character in a UTF-8 string.
Definition: unicode.cpp:70
std::vector< std::string > split(const config_attribute_value &val)
std::string::const_iterator iterator
Definition: tokenizer.hpp:25
Contains the SDL_Rect helper code.
Definition: theme.hpp:33
The basic class for representing 8-bit RGB or RGBA colour values.
Definition: color.hpp:61
static color_t from_rgb_string(std::string_view c)
Creates a new opaque color_t object from a string variable in "R,G,B" format.
Definition: color.cpp:44
An abstract description of a rectangle with integer coordinates.
Definition: rect.hpp:49
double size
Definition: theme.hpp:90
mock_char c
mock_party p
static map_location::direction sw
static map_location::direction s
static int parse_signed_string(std::string_view expr)
Definition: theme.cpp:46
static _rect read_rect(const config &cfg)
Definition: theme.cpp:74
static config get_resolution(const config &resolutions, const std::string &id)
Returns a copy of the wanted resolution.
Definition: theme.cpp:195
static rect read_sdl_rect(const config &cfg)
Definition: theme.cpp:97
#define LOG_DP
Definition: theme.cpp:34
static config & find_ref(const std::string &id, config &cfg, bool remove=false)
Definition: theme.cpp:137
static std::string resolve_rect(const std::string &rect_str)
Definition: theme.cpp:109
static lg::log_domain log_display("display")
static std::size_t compute(std::string expr, std::size_t ref1, std::size_t ref2=0)
Definition: theme.cpp:60
static config expand_partialresolution(const config &theme)
Returns a config with all partial resolutions of a theme expanded.
Definition: theme.cpp:216
#define ERR_DP
Definition: theme.cpp:35
#define DBG_DP
Definition: theme.cpp:33
static void do_resolve_rects(const config &cfg, config &resolved_config, config *resol_cfg=nullptr)
Definition: theme.cpp:256
Definitions related to theme-support.
std::string missing_mandatory_wml_key(const std::string &section, const std::string &key, const std::string &primary_key, const std::string &primary_value)
Returns a standard message for a missing wml key (attribute).
Add a special kind of assert to validate whether the input from WML doesn't contain any problems that...
#define VALIDATE(cond, message)
The macro to use for the validation of WML.