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