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