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