The Battle for Wesnoth  1.17.0-dev
story_viewer.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2017 - 2021
3  by Charles Dang <exodia339@gmail.com>
4  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5 
6  This program is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY.
12 
13  See the COPYING file for more details.
14 */
15 
16 #define GETTEXT_DOMAIN "wesnoth-lib"
17 
19 
20 #include "formula/variant.hpp"
22 #include "sdl/point.hpp"
23 #include "gui/core/timer.hpp"
24 #include "gui/widgets/button.hpp"
25 #include "gui/widgets/label.hpp"
27 #include "gui/widgets/settings.hpp"
29 #include "gui/widgets/window.hpp"
30 #include "sound.hpp"
31 #include "variable.hpp"
32 
33 namespace gui2::dialogs
34 {
35 
36 // Helper function to get the canvas shape data for the shading under the title area until
37 // I can figure out how to ensure it always stays on top of the canvas stack.
39 {
40  static config cfg;
41  cfg["x"] = 0;
42  cfg["y"] = 0;
43  cfg["w"] = "(screen_width)";
44  cfg["h"] = "(image_original_height * 2)";
45  cfg["name"] = "dialogs/story_title_decor.png~O(75%)";
46 
47  return cfg;
48 }
49 
50 // Stacked widget layer constants for the text stack.
51 static const unsigned int LAYER_BACKGROUND = 1;
52 static const unsigned int LAYER_TEXT = 2;
53 
55 
56 story_viewer::story_viewer(const std::string& scenario_name, const config& cfg_parsed)
57  : controller_(vconfig(cfg_parsed, true), scenario_name)
58  , part_index_(0)
59  , current_part_(nullptr)
60  , timer_id_(0)
61  , next_draw_(0)
62  , fade_step_(0)
63  , fade_state_(NOT_FADING)
64 {
65  update_current_part_ptr();
66 }
67 
69 {
70  if(timer_id_ != 0) {
72  timer_id_ = 0;
73  }
74 }
75 
77 {
79 }
80 
82 {
83  window.set_enter_disabled(true);
84 
85  // Special callback handle key presses
86  connect_signal_pre_key_press(window, std::bind(&story_viewer::key_press_callback, this, std::placeholders::_5));
87 
88  connect_signal_mouse_left_click(find_widget<button>(&window, "next", false),
90 
91  connect_signal_mouse_left_click(find_widget<button>(&window, "back", false),
93 
95  std::bind(&story_viewer::draw_callback, this));
96 
97  display_part();
98 }
99 
101 {
103 }
104 
106 {
107  static const int VOICE_SOUND_SOURCE_ID = 255;
108  // Update Back button state. Doing this here so it gets called in pre_show too.
109  find_widget<button>(get_window(), "back", false).set_active(part_index_ != 0);
110 
111  //
112  // Music and sound
113  //
114  if(!current_part_->music().empty()) {
115  config music_config;
116  music_config["name"] = current_part_->music();
117  music_config["ms_after"] = 2000;
118  music_config["immediate"] = true;
119 
120  sound::play_music_config(music_config);
121  }
122 
123  if(!current_part_->sound().empty()) {
125  }
126 
127  sound::stop_sound(VOICE_SOUND_SOURCE_ID);
128  if(!current_part_->voice().empty()) {
129  sound::play_sound_positioned(current_part_->voice(), VOICE_SOUND_SOURCE_ID, 0, 0);
130  }
131 
132  config cfg, image;
133 
134  //
135  // Background images
136  //
137  bool has_background = false;
138  config* base_layer = nullptr;
139 
140  for(const auto& layer : current_part_->get_background_layers()) {
141  has_background |= !layer.file().empty();
142 
143  const bool preserve_ratio = layer.keep_aspect_ratio();
144  const bool tile_h = layer.tile_horizontally();
145  const bool tile_v = layer.tile_vertically();
146 
147  // By default, no scaling will be applied.
148  std::string width_formula = "(image_original_width)";
149  std::string height_formula = "(image_original_height)";
150 
151  // Background layers are almost always centered. In case of tiling, we want the full
152  // area in the horizontal or vertical direction, so set the origin to 0 for that axis.
153  // The resize mode will center the original image in the available area first/
154  std::string x_formula;
155  std::string y_formula;
156 
157  if(tile_h) {
158  x_formula = "0";
159  } else {
160  x_formula = "(max(pos, 0) where pos = (width / 2 - image_width / 2))";
161  }
162 
163  if(tile_v) {
164  y_formula = "0";
165  } else {
166  y_formula = "(max(pos, 0) where pos = (height / 2 - image_height / 2))";
167  }
168 
169  if(layer.scale_horizontally() && preserve_ratio) {
170  height_formula = "(min((image_original_height * width / image_original_width), height))";
171  } else if(layer.scale_vertically() || tile_v) {
172  height_formula = "(height)";
173  }
174 
175  if(layer.scale_vertically() && preserve_ratio) {
176  width_formula = "(min((image_original_width * height / image_original_height), width))";
177  } else if(layer.scale_horizontally() || tile_h) {
178  width_formula = "(width)";
179  }
180 
181  image["x"] = x_formula;
182  image["y"] = y_formula;
183  image["w"] = width_formula;
184  image["h"] = height_formula;
185  image["name"] = layer.file();
186  image["resize_mode"] = (tile_h || tile_v) ? "tile_center" : "scale";
187 
188  config& layer_image = cfg.add_child("image", image);
189 
190  if(base_layer == nullptr || layer.is_base_layer()) {
191  base_layer = &layer_image;
192  }
193  }
194 
195  canvas& window_canvas = get_window()->get_canvas(0);
196 
197  /* In order to avoid manually loading the image and calculating the scaling factor, we instead
198  * delegate the task of setting the necessary variables to the canvas once the calculations
199  * have been made internally.
200  *
201  * This sets the necessary values with the data for "this" image when its drawn. If no base
202  * layer was found (which would be the case if no backgrounds were provided at all), simply set
203  * some sane defaults directly.
204  */
205  if(base_layer != nullptr) {
206  (*base_layer)["actions"] = R"((
207  [
208  set_var('base_scale_x', as_decimal(image_width) / as_decimal(image_original_width)),
209  set_var('base_scale_y', as_decimal(image_height) / as_decimal(image_original_height)),
210  set_var('base_origin_x', clip_x),
211  set_var('base_origin_y', clip_y)
212  ]
213  ))";
214  } else {
215  window_canvas.set_variable("base_scale_x", wfl::variant(1));
216  window_canvas.set_variable("base_scale_y", wfl::variant(1));
217  window_canvas.set_variable("base_origin_x", wfl::variant(0));
218  window_canvas.set_variable("base_origin_y", wfl::variant(0));
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  get_window()->set_is_dirty(true);
228 
229  //
230  // Title
231  //
232  label& title_label = find_widget<label>(get_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 
243  title_label.set_text_alignment(title_text_alignment);
244  title_label.set_label(title_text);
245  } else {
246  showing_title = false;
247 
249  }
250 
251  //
252  // Story text
253  //
254  stacked_widget& text_stack = find_widget<stacked_widget>(get_window(), "text_and_control_stack", false);
255 
256  std::string new_panel_mode;
257 
258  switch(current_part_->story_text_location()) {
260  new_panel_mode = "top";
261  break;
263  new_panel_mode = "center";
264  break;
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>(get_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.
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(floating_images.begin(), part_index_);
316  }
317 }
318 
319 void story_viewer::draw_floating_image(floating_image_list::const_iterator image_iter, int this_part_index)
320 {
321  const auto& images = current_part_->get_floating_images();
322  canvas& window_canvas = get_window()->get_canvas(0);
323 
324  // If the current part has changed or we're out of images to draw, exit the draw loop.
325  while((this_part_index == part_index_) && (image_iter != images.end())) {
326  const auto& floating_image = *image_iter;
327  ++image_iter;
328 
329  std::ostringstream x_ss;
330  std::ostringstream y_ss;
331 
332  // Floating images' locations are scaled by the same factor as the background.
333  x_ss << "(trunc(" << floating_image.ref_x() << " * base_scale_x) + base_origin_x";
334  y_ss << "(trunc(" << floating_image.ref_y() << " * base_scale_y) + base_origin_y";
335 
336  if(floating_image.centered()) {
337  x_ss << " - (image_width / 2)";
338  y_ss << " - (image_height / 2)";
339  }
340 
341  x_ss << ")";
342  y_ss << ")";
343 
344  config image;
345  image["x"] = x_ss.str();
346  image["y"] = y_ss.str();
347 
348  // Width and height don't need to be set unless the image needs to be scaled.
349  if(floating_image.resize_with_background()) {
350  image["w"] = "(image_original_width * base_scale_x)";
351  image["h"] = "(image_original_height * base_scale_y)";
352  }
353 
354  image["name"] = floating_image.file();
355  config cfg{"image", std::move(image)};
356 
357  cfg.add_child("image", std::move(image));
358  window_canvas.append_cfg(std::move(cfg));
359 
360  // Needed to make the background redraw correctly.
361  window_canvas.set_is_dirty(true);
362  get_window()->set_is_dirty(true);
363 
364  // If a delay is specified, schedule the next image draw and break out of the loop.
365  const unsigned int draw_delay = floating_image.display_delay();
366  if(draw_delay != 0) {
367  // This must be a non-repeating timer
368  timer_id_ = add_timer(draw_delay, std::bind(&story_viewer::draw_floating_image, this, image_iter, this_part_index), false);
369  return;
370  }
371  }
372 
373  timer_id_ = 0;
374 }
375 
377 {
378  // If a button is pressed while fading in, abort and set alpha to full opaque.
379  if(fade_state_ == FADING_IN) {
380  halt_fade_draw();
381 
382  // Only set full alpha if Forward was pressed.
383  if(direction == DIR_FORWARD) {
384  find_widget<scroll_label>(get_window(), "part_text", false).set_text_alpha(ALPHA_OPAQUE);
386  return;
387  }
388  }
389 
390  // If a button is pressed while fading out, skip and show next part.
391  if(fade_state_ == FADING_OUT) {
392  display_part();
393  return;
394  }
395 
396  assert(fade_state_ == NOT_FADING);
397 
398  part_index_ = (direction == DIR_FORWARD ? part_index_ + 1 : part_index_ -1);
399 
400  // If we've viewed all the parts, close the dialog.
402  get_window()->close();
403  return;
404  }
405 
406  if(part_index_ < 0) {
407  part_index_ = 0;
408  }
409 
411 
412  begin_fade_draw(false);
413 }
414 
415 void story_viewer::key_press_callback(const SDL_Keycode key)
416 {
417  const bool next_keydown =
418  key == SDLK_SPACE
419  || key == SDLK_RETURN
420  || key == SDLK_KP_ENTER
421  || key == SDLK_RIGHT;
422 
423  const bool back_keydown =
424  key == SDLK_BACKSPACE
425  || key == SDLK_LEFT;
426 
427  if(next_keydown) {
429  } else if(back_keydown) {
431  }
432 }
433 
435 {
436  next_draw_ = SDL_GetTicks() + 20;
437 }
438 
440 {
441  set_next_draw();
442 
443  fade_step_ = fade_in ? 0 : 10;
444  fade_state_ = fade_in ? FADING_IN : FADING_OUT;
445 }
446 
448 {
449  next_draw_ = 0;
450  fade_step_ = -1;
452 }
453 
455 {
456  if(next_draw_ && SDL_GetTicks() < next_draw_) {
457  return;
458  }
459 
460  if(fade_state_ == NOT_FADING) {
461  return;
462  }
463 
464  // If we've faded fully in...
465  if(fade_state_ == FADING_IN && fade_step_ > 10) {
466  halt_fade_draw();
467  return;
468  }
469 
470  // If we've faded fully out...
471  if(fade_state_ == FADING_OUT && fade_step_ < 0) {
472  halt_fade_draw();
473 
474  display_part();
475  return;
476  }
477 
478  unsigned short new_alpha = std::clamp<short>(fade_step_ * 25.5, 0, ALPHA_OPAQUE);
479  find_widget<scroll_label>(get_window(), "part_text", false).set_text_alpha(new_alpha);
480 
481  // The text stack also needs to be marked dirty so the background panel redraws correctly.
483 
484  if(fade_state_ == FADING_IN) {
485  fade_step_ ++;
486  } else if(fade_state_ == FADING_OUT) {
487  fade_step_ --;
488  }
489 
490  set_next_draw();
491 }
492 
494 {
495  find_widget<stacked_widget>(get_window(), "text_and_control_stack", false).set_is_dirty(true);
496 }
497 
498 } // namespace dialogs
storyscreen::controller::part_pointer_type current_part_
void close()
Requests to close the window.
Definition: window.hpp:182
Center of the screen.
Definition: part.hpp:237
static const unsigned int LAYER_TEXT
void play_sound_positioned(const std::string &files, int id, int repeats, unsigned int distance)
Definition: sound.cpp:1028
static const unsigned int LAYER_BACKGROUND
grid * get_layer_grid(unsigned int i)
Gets the grid for a specified layer.
void key_press_callback(const SDL_Keycode key)
void set_variable(const std::string &key, wfl::variant &&value)
Definition: canvas.hpp:168
int draw_delay()
Definition: general.cpp:875
This file contains the window object, this object is a top level container which has the event manage...
int current_layer() const
Gets the current visible layer number.
virtual void pre_show(window &window) override
Actions to be taken before showing the window.
void stop_sound()
Definition: sound.cpp:565
STL namespace.
window * get_window() const
Returns a pointer to the dialog&#39;s window.
A label displays a text, the text can be wrapped but no scrollbars are provided.
Definition: label.hpp:57
PangoAlignment decode_text_alignment(const std::string &alignment)
Converts a text alignment string to a text alignment.
Definition: helper.cpp:64
Bottom of the screen.
Definition: part.hpp:238
void set_cfg(const config &cfg, const bool force=false)
Sets the config.
Definition: canvas.hpp:125
void play_sound(const std::string &files, channel_group group, unsigned int repeats)
Definition: sound.cpp:1021
virtual void set_label(const t_string &label)
void begin_fade_draw(bool fade_in)
This file contains the settings handling of the widget library.
void append_cfg(const config &cfg)
Appends data to the config.
Definition: canvas.hpp:137
void set_visible(const visibility visible)
Definition: widget.cpp:476
void connect_signal_mouse_left_click(dispatcher &dispatcher, const signal_function &signal)
Connects a signal handler for a left mouse button click.
Definition: dispatcher.cpp:172
void set_is_dirty(const bool is_dirty)
Definition: widget.cpp:466
virtual void set_text_alignment(const PangoAlignment text_alignment)
Label showing a text.
virtual void set_text_alignment(const PangoAlignment text_alignment) override
See styled_widget::set_text_alignment.
A simple canvas which can be drawn upon.
Definition: canvas.hpp:42
static config get_title_area_decor_config()
void set_is_dirty(const bool is_dirty)
Definition: canvas.hpp:175
Top of the screen.
Definition: part.hpp:236
The user set the widget invisible, that means:
Contains the gui2 timer routines.
std::size_t add_timer(const uint32_t interval, const std::function< void(std::size_t id)> &callback, const bool repeat)
Adds a new timer.
Definition: timer.cpp:127
Dialog to view the storyscreen.
void select_layer(const int layer)
Selects and displays a particular layer.
config & add_child(config_key_type key)
Definition: config.cpp:514
part_pointer_type get_part(int index) const
Definition: controller.hpp:41
The user sets the widget visible, that means:
storyscreen::controller controller_
const uint8_t ALPHA_OPAQUE
Definition: color.hpp:49
void connect_signal_pre_key_press(dispatcher &dispatcher, const signal_keyboard_function &signal)
Connects the signal for &#39;snooping&#39; on the keypress.
Definition: dispatcher.cpp:167
void play_music_config(const config &music_node, bool allow_interrupt_current_track, int i)
Definition: sound.cpp:705
A variable-expanding proxy for the config class.
Definition: variable.hpp:44
Functions to load and save images from/to disk.
virtual void set_vertical_alignment(const std::string &alignment)
Sets the horizontal alignment of the widget within its parent grid.
Definition: widget.cpp:282
A stacked widget holds several widgets on top of each other.
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:61
canvas & get_canvas(const unsigned index)
base class of top level items, the only item which needs to store the final canvases to draw on...
Definition: window.hpp:65
bool empty() const
Definition: config.cpp:941
void draw_floating_image(floating_image_list::const_iterator image_iter, int this_part_index)
void nav_button_callback(NAV_DIRECTION direction)
void connect_signal_on_draw(dispatcher &dispatcher, const signal_function &signal)
Connects a signal handler for a callback when the widget is drawn.
Definition: dispatcher.cpp:192
bool remove_timer(const std::size_t id)
Removes a timer.
Definition: timer.cpp:168
void set_enter_disabled(const bool enter_disabled)
Disable the enter key.
Definition: window.hpp:286