1 /*
2    Copyright (C) 2017-2018 by Charles Dang <exodia339@gmail.com>
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 #define GETTEXT_DOMAIN "wesnoth-lib"
16 
17 #include "gui/dialogs/story_viewer.hpp"
18 
19 #include "formula/callable_objects.hpp"
20 #include "formula/variant.hpp"
21 #include "gui/auxiliary/find_widget.hpp"
22 #include "sdl/point.hpp"
23 #include "gui/core/timer.hpp"
24 #include "gui/widgets/button.hpp"
25 #include "gui/widgets/label.hpp"
26 #include "gui/widgets/scroll_label.hpp"
27 #include "gui/widgets/settings.hpp"
28 #include "gui/widgets/stacked_widget.hpp"
29 #include "gui/widgets/window.hpp"
30 #include "sound.hpp"
31 #include "variable.hpp"
32 
33 namespace gui2
34 {
35 namespace dialogs
36 {
37 
38 // Helper function to get the canvas shape data for the shading under the title area until
39 // I can figure out how to ensure it always stays on top of the canvas stack.
get_title_area_decor_config()40 static config get_title_area_decor_config()
41 {
42 	static config cfg;
43 	cfg["x"] = 0;
44 	cfg["y"] = 0;
45 	cfg["w"] = "(screen_width)";
46 	cfg["h"] = "(image_original_height * 2)";
47 	cfg["name"] = "dialogs/story_title_decor.png~O(75%)";
48 
49 	return cfg;
50 }
51 
52 // Stacked widget layer constants for the text stack.
53 static const unsigned int LAYER_BACKGROUND = 1;
54 static const unsigned int LAYER_TEXT = 2;
55 
REGISTER_DIALOG(story_viewer)56 REGISTER_DIALOG(story_viewer)
57 
58 story_viewer::story_viewer(const std::string& scenario_name, const config& cfg_parsed)
59 	: controller_(vconfig(cfg_parsed, true), scenario_name)
60 	, part_index_(0)
61 	, current_part_(nullptr)
62 	, timer_id_(0)
63 	, next_draw_(0)
64 	, fade_step_(0)
65 	, fade_state_(NOT_FADING)
66 {
67 	update_current_part_ptr();
68 }
69 
clear_image_timer()70 void story_viewer::clear_image_timer()
71 {
72 	if(timer_id_ != 0) {
73 		remove_timer(timer_id_);
74 		timer_id_ = 0;
75 	}
76 }
77 
~story_viewer()78 story_viewer::~story_viewer()
79 {
80 	clear_image_timer();
81 }
82 
pre_show(window & window)83 void story_viewer::pre_show(window& window)
84 {
85 	window.set_enter_disabled(true);
86 
87 	// Special callback handle key presses
88 	connect_signal_pre_key_press(window, std::bind(&story_viewer::key_press_callback, this, std::ref(window), _5));
89 
90 	connect_signal_mouse_left_click(find_widget<button>(&window, "next", false),
91 		std::bind(&story_viewer::nav_button_callback, this, std::ref(window), DIR_FORWARD));
92 
93 	connect_signal_mouse_left_click(find_widget<button>(&window, "back", false),
94 		std::bind(&story_viewer::nav_button_callback, this, std::ref(window), DIR_BACKWARDS));
95 
96 	window.connect_signal<event::DRAW>(
97 		std::bind(&story_viewer::draw_callback, this, std::ref(window)), event::dispatcher::front_child);
98 
99 	display_part(window);
100 }
101 
update_current_part_ptr()102 void story_viewer::update_current_part_ptr()
103 {
104 	current_part_ = controller_.get_part(part_index_);
105 }
106 
display_part(window & window)107 void story_viewer::display_part(window& window)
108 {
109 	static const int VOICE_SOUND_SOURCE_ID = 255;
110 	// Update Back button state. Doing this here so it gets called in pre_show too.
111 	find_widget<button>(&window, "back", false).set_active(part_index_ != 0);
112 
113 	//
114 	// Music and sound
115 	//
116 	if(!current_part_->music().empty()) {
117 		config music_config;
118 		music_config["name"] = current_part_->music();
119 		music_config["ms_after"] = 2000;
120 		music_config["immediate"] = true;
121 
122 		sound::play_music_config(music_config);
123 	}
124 
125 	if(!current_part_->sound().empty()) {
126 		sound::play_sound(current_part_->sound());
127 	}
128 
129 	sound::stop_sound(VOICE_SOUND_SOURCE_ID);
130 	if(!current_part_->voice().empty()) {
131 		sound::play_sound_positioned(current_part_->voice(), VOICE_SOUND_SOURCE_ID, 0, 0);
132 	}
133 
134 	config cfg, image;
135 
136 	//
137 	// Background images
138 	//
139 	bool has_background = false;
140 	config* base_layer = nullptr;
141 
142 	for(const auto& layer : current_part_->get_background_layers()) {
143 		has_background |= !layer.file().empty();
144 
145 		const bool preserve_ratio = layer.keep_aspect_ratio();
146 		const bool tile_h = layer.tile_horizontally();
147 		const bool tile_v = layer.tile_vertically();
148 
149 		// By default, no scaling will be applied.
150 		std::string width_formula  = "(image_original_width)";
151 		std::string height_formula = "(image_original_height)";
152 
153 		// Background layers are almost always centered. In case of tiling, we want the full
154 		// area in the horizontal or vertical direction, so set the origin to 0 for that axis.
155 		// The resize mode will center the original image in the available area first/
156 		std::string x_formula;
157 		std::string y_formula;
158 
159 		if(tile_h) {
160 			x_formula = "0";
161 		} else {
162 			x_formula = "(max(pos, 0) where pos = (width  / 2 - image_width  / 2))";
163 		}
164 
165 		if(tile_v) {
166 			y_formula = "0";
167 		} else {
168 			y_formula = "(max(pos, 0) where pos = (height / 2 - image_height / 2))";
169 		}
170 
171 		if(layer.scale_horizontally() && preserve_ratio) {
172 			height_formula = "(min((image_original_height * width  / image_original_width), height))";
173 		} else if(layer.scale_vertically() || tile_v) {
174 			height_formula = "(height)";
175 		}
176 
177 		if(layer.scale_vertically() && preserve_ratio) {
178 			width_formula  = "(min((image_original_width  * height / image_original_height), width))";
179 		} else if(layer.scale_horizontally() || tile_h) {
180 			width_formula  = "(width)";
181 		}
182 
183 		image["x"] = x_formula;
184 		image["y"] = y_formula;
185 		image["w"] = width_formula;
186 		image["h"] = height_formula;
187 		image["name"] = layer.file();
188 		image["resize_mode"] = (tile_h || tile_v) ? "tile_center" : "scale";
189 
190 		config& layer_image = cfg.add_child("image", image);
191 
192 		if(base_layer == nullptr || layer.is_base_layer()) {
193 			base_layer = &layer_image;
194 		}
195 	}
196 
197 	canvas& window_canvas = window.get_canvas(0);
198 
199 	/* In order to avoid manually loading the image and calculating the scaling factor, we instead
200 	 * delegate the task of setting the necessary variables to the canvas once the calculations
201 	 * have been made internally.
202 	 *
203 	 * This sets the necessary values with the data for "this" image when its drawn. If no base
204 	 * layer was found (which would be the case if no backgrounds were provided at all), simply set
205 	 * some sane defaults directly.
206 	 */
207 	if(base_layer != nullptr) {
208 		(*base_layer)["actions"] = R"((
209 			[
210 				set_var('base_scale_x', as_decimal(image_width)  / as_decimal(image_original_width)),
211 				set_var('base_scale_y', as_decimal(image_height) / as_decimal(image_original_height)),
212 				set_var('base_origin', loc(clip_x, clip_y))
213 			]
214 		))";
215 	} else {
216 		window_canvas.set_variable("base_scale_x", wfl::variant(1));
217 		window_canvas.set_variable("base_scale_y", wfl::variant(1));
218 		window_canvas.set_variable("base_origin",  wfl::variant(std::make_shared<wfl::location_callable>(map_location::ZERO())));
219 	}
220 
221 	cfg.add_child("image", get_title_area_decor_config());
222 
223 	window_canvas.set_cfg(cfg);
224 
225 	// Needed to make the background redraw correctly.
226 	window_canvas.set_is_dirty(true);
227 	window.set_is_dirty(true);
228 
229 	//
230 	// Title
231 	//
232 	label& title_label = find_widget<label>(&window, "title", false);
233 
234 	std::string title_text = current_part_->title();
235 	bool showing_title;
236 
237 	if(current_part_->show_title() && !title_text.empty()) {
238 		showing_title = true;
239 
240 		PangoAlignment title_text_alignment = decode_text_alignment(current_part_->title_text_alignment());
241 
242 		title_label.set_visible(widget::visibility::visible);
243 		title_label.set_text_alignment(title_text_alignment);
244 		title_label.set_label(title_text);
245 	} else {
246 		showing_title = false;
247 
248 		title_label.set_visible(widget::visibility::invisible);
249 	}
250 
251 	//
252 	// Story text
253 	//
254 	stacked_widget& text_stack = find_widget<stacked_widget>(&window, "text_and_control_stack", false);
255 
256 	std::string new_panel_mode;
257 
258 	switch(current_part_->story_text_location()) {
259 		case storyscreen::part::BLOCK_TOP:
260 			new_panel_mode = "top";
261 			break;
262 		case storyscreen::part::BLOCK_MIDDLE:
263 			new_panel_mode = "center";
264 			break;
265 		case storyscreen::part::BLOCK_BOTTOM:
266 			new_panel_mode = "bottom";
267 			break;
268 	}
269 
270 	text_stack.set_vertical_alignment(new_panel_mode);
271 
272 	/* Set the panel mode control variables.
273 	 *
274 	 * We use get_layer_grid here to ensure the widget is always found regardless of
275 	 * whether the background is visible or not.
276 	 */
277 	canvas& panel_canvas = find_widget<panel>(text_stack.get_layer_grid(LAYER_BACKGROUND), "text_panel", false).get_canvas(0);
278 
279 	panel_canvas.set_variable("panel_position", wfl::variant(new_panel_mode));
280 	panel_canvas.set_variable("title_present", wfl::variant(static_cast<int>(showing_title))); // cast to 0/1
281 
282 	const std::string& part_text = current_part_->text();
283 
284 	if(part_text.empty() || !has_background) {
285 		// No text or no background for this part, hide the background layer.
286 		text_stack.select_layer(LAYER_TEXT);
287 	} else if(text_stack.current_layer() != -1)  {
288 		// If the background layer was previously hidden, re-show it.
289 		text_stack.select_layer(-1);
290 	}
291 
292 	// Convert the story part text alignment types into the Pango equivalents
293 	PangoAlignment story_text_alignment = decode_text_alignment(current_part_->story_text_alignment());
294 
295 	scroll_label& text_label = find_widget<scroll_label>(&window, "part_text", false);
296 
297 	text_label.set_text_alignment(story_text_alignment);
298 	text_label.set_text_alpha(0);
299 	text_label.set_label(part_text);
300 
301 	begin_fade_draw(true);
302 	// if the previous page was skipped, it is possible that we already have a timer running.
303 	clear_image_timer();
304 	//
305 	// Floating images (handle this last)
306 	//
307 	const auto& floating_images = current_part_->get_floating_images();
308 
309 	// If we have images to draw, draw the first one now. A new non-repeating timer is added
310 	// after every draw to schedule the next one after the specified interval.
311 	//
312 	// TODO: in the old GUI1 dialog, floating images delayed the appearance of the story panel until
313 	//       drawing was finished. Might be worth looking into restoring that.
314 	if(!floating_images.empty()) {
315 		draw_floating_image(window, floating_images.begin(), part_index_);
316 	}
317 }
318 
draw_floating_image(window & window,floating_image_list::const_iterator image_iter,int this_part_index)319 void story_viewer::draw_floating_image(window& window, floating_image_list::const_iterator image_iter, int this_part_index)
320 {
321 	const auto& images = current_part_->get_floating_images();
322 
323 	// If the current part has changed or we're out of images to draw, exit the draw loop.
324 	if((this_part_index != part_index_) || (image_iter == images.end())) {
325 		timer_id_ = 0;
326 		return;
327 	}
328 
329 	const auto& floating_image = *image_iter;
330 
331 	std::ostringstream x_ss;
332 	std::ostringstream y_ss;
333 
334 	// Floating images are scaled by the same factor as the background.
335 	x_ss << "(trunc(fi_ref_x * base_scale_x) + base_origin.x";
336 	y_ss << "(trunc(fi_ref_y * base_scale_y) + base_origin.y";
337 
338 	if(floating_image.centered()) {
339 		x_ss << " - (image_original_width  / 2)";
340 		y_ss << " - (image_original_height / 2)";
341 	}
342 
343 	x_ss << " where fi_ref_x = " << floating_image.ref_x() << ")";
344 	y_ss << " where fi_ref_y = " << floating_image.ref_y() << ")";
345 
346 	config cfg, image;
347 
348 	image["x"] = x_ss.str();
349 	image["y"] = y_ss.str();
350 	image["w"] = floating_image.autoscale() ? "(width)"  : "(image_width)";
351 	image["h"] = floating_image.autoscale() ? "(height)" : "(image_height)";
352 	image["name"] = floating_image.file();
353 
354 	// TODO: implement handling of the tiling options.
355 	//image["resize_mode"] = "tile_centered"
356 
357 	cfg.add_child("image", std::move(image));
358 
359 	canvas& window_canvas = window.get_canvas(0);
360 
361 	// Needed to make the background redraw correctly.
362 	window_canvas.append_cfg(cfg);
363 	window_canvas.set_is_dirty(true);
364 
365 	window.set_is_dirty(true);
366 
367 	++image_iter;
368 
369 	// If a delay is specified, schedule the next image draw. This *must* be a non-repeating timer!
370 	// Else draw the next image immediately.
371 	const unsigned int draw_delay = floating_image.display_delay();
372 
373 	if(draw_delay != 0) {
374 		timer_id_ = add_timer(draw_delay,
375 			std::bind(&story_viewer::draw_floating_image, this, std::ref(window), image_iter, this_part_index), false);
376 	} else {
377 		draw_floating_image(window, image_iter, this_part_index);
378 	}
379 }
380 
nav_button_callback(window & window,NAV_DIRECTION direction)381 void story_viewer::nav_button_callback(window& window, NAV_DIRECTION direction)
382 {
383 	// If a button is pressed while fading in, abort and set alpha to full opaque.
384 	if(fade_state_ == FADING_IN) {
385 		halt_fade_draw();
386 
387 		// Only set full alpha if Forward was pressed.
388 		if(direction == DIR_FORWARD) {
389 			find_widget<scroll_label>(&window, "part_text", false).set_text_alpha(ALPHA_OPAQUE);
390 			flag_stack_as_dirty(window);
391 			return;
392 		}
393 	}
394 
395 	// If a button is pressed while fading out, skip and show next part.
396 	if(fade_state_ == FADING_OUT) {
397 		display_part(window);
398 		return;
399 	}
400 
401 	assert(fade_state_ == NOT_FADING);
402 
403 	part_index_ = (direction == DIR_FORWARD ? part_index_ + 1 : part_index_ -1);
404 
405 	// If we've viewed all the parts, close the dialog.
406 	if(part_index_ >= controller_.max_parts()) {
407 		window.close();
408 		return;
409 	}
410 
411 	if(part_index_ < 0) {
412 		part_index_ = 0;
413 	}
414 
415 	update_current_part_ptr();
416 
417 	begin_fade_draw(false);
418 }
419 
key_press_callback(window & window,const SDL_Keycode key)420 void story_viewer::key_press_callback(window& window, const SDL_Keycode key)
421 {
422 	const bool next_keydown =
423 		   key == SDLK_SPACE
424 		|| key == SDLK_RETURN
425 		|| key == SDLK_KP_ENTER
426 		|| key == SDLK_RIGHT;
427 
428 	const bool back_keydown =
429 		   key == SDLK_BACKSPACE
430 		|| key == SDLK_LEFT;
431 
432 	if(next_keydown) {
433 		nav_button_callback(window, DIR_FORWARD);
434 	} else if(back_keydown) {
435 		nav_button_callback(window, DIR_BACKWARDS);
436 	}
437 }
438 
set_next_draw()439 void story_viewer::set_next_draw()
440 {
441 	next_draw_ = SDL_GetTicks() + 20;
442 }
443 
begin_fade_draw(bool fade_in)444 void story_viewer::begin_fade_draw(bool fade_in)
445 {
446 	set_next_draw();
447 
448 	fade_step_ = fade_in ? 0 : 10;
449 	fade_state_ = fade_in ? FADING_IN : FADING_OUT;
450 }
451 
halt_fade_draw()452 void story_viewer::halt_fade_draw()
453 {
454 	next_draw_ = 0;
455 	fade_step_ = -1;
456 	fade_state_ = NOT_FADING;
457 }
458 
draw_callback(window & window)459 void story_viewer::draw_callback(window& window)
460 {
461 	if(next_draw_ && SDL_GetTicks() < next_draw_) {
462 		return;
463 	}
464 
465 	if(fade_state_ == NOT_FADING) {
466 		return;
467 	}
468 
469 	// If we've faded fully in...
470 	if(fade_state_ == FADING_IN && fade_step_ > 10) {
471 		halt_fade_draw();
472 		return;
473 	}
474 
475 	// If we've faded fully out...
476 	if(fade_state_ == FADING_OUT && fade_step_ < 0) {
477 		halt_fade_draw();
478 
479 		display_part(window);
480 		return;
481 	}
482 
483 	unsigned short new_alpha = utils::clamp<short>(fade_step_ * 25.5, 0, ALPHA_OPAQUE);
484 	find_widget<scroll_label>(&window, "part_text", false).set_text_alpha(new_alpha);
485 
486 	// The text stack also needs to be marked dirty so the background panel redraws correctly.
487 	flag_stack_as_dirty(window);
488 
489 	if(fade_state_ == FADING_IN) {
490 		fade_step_ ++;
491 	} else if(fade_state_ == FADING_OUT) {
492 		fade_step_ --;
493 	}
494 
495 	set_next_draw();
496 }
497 
flag_stack_as_dirty(window & window)498 void story_viewer::flag_stack_as_dirty(window& window)
499 {
500 	find_widget<stacked_widget>(&window, "text_and_control_stack", false).set_is_dirty(true);
501 }
502 
503 } // namespace dialogs
504 } // namespace gui2
505