The Battle for Wesnoth  1.17.17+dev
story_viewer.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2017 - 2023
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 "display.hpp"
21 #include "formula/variant.hpp"
23 #include "sdl/point.hpp"
24 #include "gui/core/timer.hpp"
25 #include "gui/widgets/button.hpp"
26 #include "gui/widgets/label.hpp"
28 #include "gui/widgets/settings.hpp"
30 #include "gui/widgets/window.hpp"
31 #include "sound.hpp"
32 #include "variable.hpp"
33 
34 namespace gui2::dialogs
35 {
36 
37 // Helper function to get the canvas shape data for the shading under the title area until
38 // I can figure out how to ensure it always stays on top of the canvas stack.
40 {
41  static config cfg;
42  cfg["x"] = 0;
43  cfg["y"] = 0;
44  cfg["w"] = "(screen_width)";
45  cfg["h"] = "(image_original_height * 2)";
46  cfg["name"] = "dialogs/story_title_decor.png~O(75%)";
47 
48  return cfg;
49 }
50 
51 // Stacked widget layer constants for the text stack.
52 static const unsigned int LAYER_BACKGROUND = 1;
53 static const unsigned int LAYER_TEXT = 2;
54 
56 
57 story_viewer::story_viewer(const std::string& scenario_name, const config& cfg_parsed)
58  : modal_dialog(window_id())
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 
71 {
72  if(timer_id_ != 0) {
74  timer_id_ = 0;
75  }
76 }
77 
79 {
81 }
82 
84 {
86 
87  // Special callback handle key presses
88  connect_signal_pre_key_press(window, std::bind(&story_viewer::key_press_callback, this, std::placeholders::_5));
89 
90  connect_signal_mouse_left_click(find_widget<button>(&window, "next", false),
92 
93  connect_signal_mouse_left_click(find_widget<button>(&window, "back", false),
95 
96  // Tell the game display not to draw
99 
100  display_part();
101 }
102 
104 {
105  // Bring the game display back again, if appropriate
107 }
108 
110 {
112 }
113 
115 {
116  static const int VOICE_SOUND_SOURCE_ID = 255;
117  // Update Back button state. Doing this here so it gets called in pre_show too.
118  find_widget<button>(get_window(), "back", false).set_active(part_index_ != 0);
119 
120  //
121  // Music and sound
122  //
123  if(!current_part_->music().empty()) {
124  config music_config;
125  music_config["name"] = current_part_->music();
126  music_config["ms_after"] = 2000;
127  music_config["immediate"] = true;
128 
129  sound::play_music_config(music_config);
130  }
131 
132  if(!current_part_->sound().empty()) {
134  }
135 
136  sound::stop_sound(VOICE_SOUND_SOURCE_ID);
137  if(!current_part_->voice().empty()) {
138  sound::play_sound_positioned(current_part_->voice(), VOICE_SOUND_SOURCE_ID, 0, 0);
139  }
140 
141  config cfg, image;
142 
143  //
144  // Background images
145  //
146  bool has_background = false;
147  config* base_layer = nullptr;
148 
149  for(const auto& layer : current_part_->get_background_layers()) {
150  has_background |= !layer.file().empty();
151 
152  const bool preserve_ratio = layer.keep_aspect_ratio();
153  const bool tile_h = layer.tile_horizontally();
154  const bool tile_v = layer.tile_vertically();
155 
156  // By default, no scaling will be applied.
157  std::string width_formula = "(image_original_width)";
158  std::string height_formula = "(image_original_height)";
159 
160  // Background layers are almost always centered. In case of tiling, we want the full
161  // area in the horizontal or vertical direction, so set the origin to 0 for that axis.
162  // The resize mode will center the original image in the available area first/
163  std::string x_formula;
164  std::string y_formula;
165 
166  if(tile_h) {
167  x_formula = "0";
168  } else {
169  x_formula = "(max(pos, 0) where pos = (width / 2 - image_width / 2))";
170  }
171 
172  if(tile_v) {
173  y_formula = "0";
174  } else {
175  y_formula = "(max(pos, 0) where pos = (height / 2 - image_height / 2))";
176  }
177 
178  if(layer.scale_horizontally() && preserve_ratio) {
179  height_formula = "(min((image_original_height * width / image_original_width), height))";
180  } else if(layer.scale_vertically() || tile_v) {
181  height_formula = "(height)";
182  }
183 
184  if(layer.scale_vertically() && preserve_ratio) {
185  width_formula = "(min((image_original_width * height / image_original_height), width))";
186  } else if(layer.scale_horizontally() || tile_h) {
187  width_formula = "(width)";
188  }
189 
190  image["x"] = x_formula;
191  image["y"] = y_formula;
192  image["w"] = width_formula;
193  image["h"] = height_formula;
194  image["name"] = layer.file();
195  image["resize_mode"] = (tile_h || tile_v) ? "tile_center" : "scale";
196 
197  config& layer_image = cfg.add_child("image", image);
198 
199  if(base_layer == nullptr || layer.is_base_layer()) {
200  base_layer = &layer_image;
201  }
202  }
203 
204  canvas& window_canvas = get_window()->get_canvas(0);
205 
206  /* In order to avoid manually loading the image and calculating the scaling factor, we instead
207  * delegate the task of setting the necessary variables to the canvas once the calculations
208  * have been made internally.
209  *
210  * This sets the necessary values with the data for "this" image when its drawn. If no base
211  * layer was found (which would be the case if no backgrounds were provided at all), simply set
212  * some sane defaults directly.
213  */
214  if(base_layer != nullptr) {
215  (*base_layer)["actions"] = R"((
216  [
217  set_var('base_scale_x', as_decimal(image_width) / as_decimal(image_original_width)),
218  set_var('base_scale_y', as_decimal(image_height) / as_decimal(image_original_height)),
219  set_var('base_origin_x', clip_x),
220  set_var('base_origin_y', clip_y)
221  ]
222  ))";
223  } else {
224  window_canvas.set_variable("base_scale_x", wfl::variant(1));
225  window_canvas.set_variable("base_scale_y", wfl::variant(1));
226  window_canvas.set_variable("base_origin_x", wfl::variant(0));
227  window_canvas.set_variable("base_origin_y", wfl::variant(0));
228  }
229 
230  cfg.add_child("image", get_title_area_decor_config());
231 
232  window_canvas.set_cfg(cfg);
233 
234  // Needed to make the background redraw correctly.
235  window_canvas.update_size_variables();
237 
238  //
239  // Title
240  //
241  label& title_label = find_widget<label>(get_window(), "title", false);
242 
243  std::string title_text = current_part_->title();
244  bool showing_title;
245 
246  if(current_part_->show_title() && !title_text.empty()) {
247  showing_title = true;
248 
249  PangoAlignment title_text_alignment = decode_text_alignment(current_part_->title_text_alignment());
250 
252  title_label.set_text_alignment(title_text_alignment);
253  title_label.set_label(title_text);
254  } else {
255  showing_title = false;
256 
258  }
259 
260  //
261  // Story text
262  //
263  stacked_widget& text_stack = find_widget<stacked_widget>(get_window(), "text_and_control_stack", false);
264 
265  std::string new_panel_mode;
266 
267  switch(current_part_->story_text_location()) {
269  new_panel_mode = "top";
270  break;
272  new_panel_mode = "center";
273  break;
275  new_panel_mode = "bottom";
276  break;
277  }
278 
279  text_stack.set_vertical_alignment(new_panel_mode);
280 
281  /* Set the panel mode control variables.
282  *
283  * We use get_layer_grid here to ensure the widget is always found regardless of
284  * whether the background is visible or not.
285  */
286  canvas& panel_canvas = find_widget<panel>(text_stack.get_layer_grid(LAYER_BACKGROUND), "text_panel", false).get_canvas(0);
287 
288  panel_canvas.set_variable("panel_position", wfl::variant(new_panel_mode));
289  panel_canvas.set_variable("title_present", wfl::variant(static_cast<int>(showing_title))); // cast to 0/1
290 
291  const std::string& part_text = current_part_->text();
292 
293  if(part_text.empty() || !has_background) {
294  // No text or no background for this part, hide the background layer.
295  text_stack.select_layer(LAYER_TEXT);
296  } else if(text_stack.current_layer() != -1) {
297  // If the background layer was previously hidden, re-show it.
298  text_stack.select_layer(-1);
299  }
300 
301  // Convert the story part text alignment types into the Pango equivalents
302  PangoAlignment story_text_alignment = decode_text_alignment(current_part_->story_text_alignment());
303 
304  scroll_label& text_label = find_widget<scroll_label>(get_window(), "part_text", false);
305 
306  text_label.set_text_alignment(story_text_alignment);
307  text_label.set_text_alpha(0);
308  text_label.set_label(part_text);
309 
310  begin_fade_draw(true);
311  // if the previous page was skipped, it is possible that we already have a timer running.
313  //
314  // Floating images (handle this last)
315  //
316  const auto& floating_images = current_part_->get_floating_images();
317 
318  // If we have images to draw, draw the first one now. A new non-repeating timer is added
319  // after every draw to schedule the next one after the specified interval.
320  //
321  // TODO: in the old GUI1 dialog, floating images delayed the appearance of the story panel until
322  // drawing was finished. Might be worth looking into restoring that.
323  if(!floating_images.empty()) {
324  draw_floating_image(floating_images.begin(), part_index_);
325  }
326 }
327 
328 void story_viewer::draw_floating_image(floating_image_list::const_iterator image_iter, int this_part_index)
329 {
330  const auto& images = current_part_->get_floating_images();
331  canvas& window_canvas = get_window()->get_canvas(0);
332 
333  // If the current part has changed or we're out of images to draw, exit the draw loop.
334  while((this_part_index == part_index_) && (image_iter != images.end())) {
335  const auto& floating_image = *image_iter;
336  ++image_iter;
337 
338  std::ostringstream x_ss;
339  std::ostringstream y_ss;
340 
341  // Floating images' locations are scaled by the same factor as the background.
342  x_ss << "(trunc(" << floating_image.ref_x() << " * base_scale_x) + base_origin_x";
343  y_ss << "(trunc(" << floating_image.ref_y() << " * base_scale_y) + base_origin_y";
344 
345  if(floating_image.centered()) {
346  x_ss << " - (image_width / 2)";
347  y_ss << " - (image_height / 2)";
348  }
349 
350  x_ss << ")";
351  y_ss << ")";
352 
353  config image;
354  image["x"] = x_ss.str();
355  image["y"] = y_ss.str();
356 
357  // Width and height don't need to be set unless the image needs to be scaled.
358  if(floating_image.resize_with_background()) {
359  image["w"] = "(image_original_width * base_scale_x)";
360  image["h"] = "(image_original_height * base_scale_y)";
361  }
362 
363  image["name"] = floating_image.file();
364  config cfg{"image", std::move(image)};
365 
366  cfg.add_child("image", std::move(image));
367  window_canvas.append_cfg(std::move(cfg));
368 
369  // Needed to make the background redraw correctly.
370  window_canvas.update_size_variables();
372 
373  // If a delay is specified, schedule the next image draw and break out of the loop.
374  const unsigned int draw_delay = floating_image.display_delay();
375  if(draw_delay != 0) {
376  // This must be a non-repeating timer
377  timer_id_ = add_timer(draw_delay, std::bind(&story_viewer::draw_floating_image, this, image_iter, this_part_index), false);
378  return;
379  }
380  }
381 
382  timer_id_ = 0;
383 }
384 
386 {
387  // If a button is pressed while fading in, abort and set alpha to full opaque.
388  if(fade_state_ == FADING_IN) {
389  halt_fade_draw();
390 
391  // Only set full alpha if Forward was pressed.
392  if(direction == DIR_FORWARD) {
393  find_widget<scroll_label>(get_window(), "part_text", false).set_text_alpha(ALPHA_OPAQUE);
395  return;
396  }
397  }
398 
399  // If a button is pressed while fading out, skip and show next part.
400  if(fade_state_ == FADING_OUT) {
401  display_part();
402  return;
403  }
404 
405  assert(fade_state_ == NOT_FADING);
406 
407  part_index_ = (direction == DIR_FORWARD ? part_index_ + 1 : part_index_ -1);
408 
409  // If we've viewed all the parts, close the dialog.
411  get_window()->close();
412  return;
413  }
414 
415  if(part_index_ < 0) {
416  part_index_ = 0;
417  }
418 
420 
421  begin_fade_draw(false);
422 }
423 
424 void story_viewer::key_press_callback(const SDL_Keycode key)
425 {
426  const bool next_keydown =
427  key == SDLK_SPACE
428  || key == SDLK_RETURN
429  || key == SDLK_KP_ENTER
430  || key == SDLK_RIGHT;
431 
432  const bool back_keydown =
433  key == SDLK_BACKSPACE
434  || key == SDLK_LEFT;
435 
436  if(next_keydown) {
438  } else if(back_keydown) {
440  }
441 }
442 
444 {
445  next_draw_ = SDL_GetTicks() + 20;
446 }
447 
449 {
450  set_next_draw();
451 
452  fade_step_ = fade_in ? 0 : 10;
453  fade_state_ = fade_in ? FADING_IN : FADING_OUT;
454 }
455 
457 {
458  next_draw_ = 0;
459  fade_step_ = -1;
461 }
462 
464 {
466 
467  if(next_draw_ && SDL_GetTicks() < next_draw_) {
468  return;
469  }
470 
471  if(fade_state_ == NOT_FADING) {
472  return;
473  }
474 
475  // If we've faded fully in...
476  if(fade_state_ == FADING_IN && fade_step_ > 10) {
477  halt_fade_draw();
478  return;
479  }
480 
481  // If we've faded fully out...
482  if(fade_state_ == FADING_OUT && fade_step_ < 0) {
483  halt_fade_draw();
484 
485  display_part();
486  return;
487  }
488 
489  unsigned short new_alpha = std::clamp<short>(fade_step_ * 25.5, 0, ALPHA_OPAQUE);
490  find_widget<scroll_label>(get_window(), "part_text", false).set_text_alpha(new_alpha);
491 
492  // The text stack also needs to be marked dirty so the background panel redraws correctly.
494 
495  if(fade_state_ == FADING_IN) {
496  fade_step_ ++;
497  } else if(fade_state_ == FADING_OUT) {
498  fade_step_ --;
499  }
500 
501  set_next_draw();
502 }
503 
505 {
506  find_widget<stacked_widget>(get_window(), "text_and_control_stack", false).queue_redraw();
507 }
508 
509 } // namespace dialogs
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:161
bool empty() const
Definition: config.cpp:856
config & add_child(config_key_type key)
Definition: config.cpp:445
void set_prevent_draw(bool pd)
Prevent the game display from drawing.
Definition: display.hpp:548
bool get_prevent_draw()
Definition: display.hpp:549
static display * get_singleton()
Returns the display object if a display object exists.
Definition: display.hpp:101
A simple canvas which can be drawn upon.
Definition: canvas.hpp:44
void set_variable(const std::string &key, wfl::variant &&value)
Definition: canvas.hpp:137
void append_cfg(const config &cfg)
Appends data to the config.
Definition: canvas.hpp:115
void update_size_variables()
Update WFL size variables.
Definition: canvas.cpp:579
void set_cfg(const config &cfg, const bool force=false)
Sets the config.
Definition: canvas.hpp:104
Abstract base class for all modal dialogs.
window * get_window()
Returns a pointer to the dialog's window.
Dialog to view the storyscreen.
virtual void pre_show(window &window) override
Actions to be taken before showing the window.
virtual void post_show(window &window) override
Actions to be taken after the window has been shown.
storyscreen::controller controller_
void begin_fade_draw(bool fade_in)
storyscreen::controller::part_pointer_type current_part_
virtual void update() override
top_level_drawable hook to animate the view
void draw_floating_image(floating_image_list::const_iterator image_iter, int this_part_index)
void nav_button_callback(NAV_DIRECTION direction)
void key_press_callback(const SDL_Keycode key)
A label displays a text, the text can be wrapped but no scrollbars are provided.
Definition: label.hpp:58
Label showing a text.
virtual void set_label(const t_string &label) override
See styled_widget::set_label.
virtual void set_text_alignment(const PangoAlignment text_alignment) override
See styled_widget::set_text_alignment.
void set_text_alpha(unsigned short alpha)
A stacked widget holds several widgets on top of each other.
int current_layer() const
Gets the current visible layer number.
grid * get_layer_grid(unsigned int i)
Gets the grid for a specified layer.
void select_layer(const int layer)
Selects and displays a particular layer.
virtual void set_label(const t_string &label)
virtual void set_text_alignment(const PangoAlignment text_alignment)
canvas & get_canvas(const unsigned index)
virtual void update()
Update state and any parameters that may effect layout, or any of the later stages.
void set_visible(const visibility visible)
Definition: widget.cpp:456
void queue_redraw()
Indicates that this widget should be redrawn.
Definition: widget.cpp:442
@ visible
The user sets the widget visible, that means:
@ invisible
The user set the widget invisible, that means:
virtual void set_vertical_alignment(const std::string &alignment)
Sets the horizontal alignment of the widget within its parent grid.
Definition: widget.cpp:283
base class of top level items, the only item which needs to store the final canvases to draw on.
Definition: window.hpp:67
void set_enter_disabled(const bool enter_disabled)
Disable the enter key.
Definition: window.hpp:286
void close()
Requests to close the window.
Definition: window.hpp:182
part_pointer_type get_part(int index) const
Definition: controller.hpp:41
@ BLOCK_BOTTOM
Bottom of the screen.
Definition: part.hpp:238
@ BLOCK_MIDDLE
Center of the screen.
Definition: part.hpp:237
@ BLOCK_TOP
Top of the screen.
Definition: part.hpp:236
A variable-expanding proxy for the config class.
Definition: variable.hpp:45
constexpr uint8_t ALPHA_OPAQUE
Definition: color.hpp:45
This file contains the window object, this object is a top level container which has the event manage...
#define REGISTER_DIALOG(window_id)
Wrapper for REGISTER_DIALOG2.
static config get_title_area_decor_config()
static const unsigned int LAYER_TEXT
static const unsigned int LAYER_BACKGROUND
void connect_signal_pre_key_press(dispatcher &dispatcher, const signal_keyboard &signal)
Connects the signal for 'snooping' on the keypress.
Definition: dispatcher.cpp:174
void connect_signal_mouse_left_click(dispatcher &dispatcher, const signal &signal)
Connects a signal handler for a left mouse button click.
Definition: dispatcher.cpp:179
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
PangoAlignment decode_text_alignment(const std::string &alignment)
Converts a text alignment string to a text alignment.
Definition: helper.cpp:60
bool remove_timer(const std::size_t id)
Removes a timer.
Definition: timer.cpp:168
Functions to load and save images from/to disk.
int draw_delay()
Definition: general.cpp:905
void play_sound(const std::string &files, channel_group group, unsigned int repeats)
Definition: sound.cpp:1035
void play_music_config(const config &music_node, bool allow_interrupt_current_track, int i)
Definition: sound.cpp:713
void play_sound_positioned(const std::string &files, int id, int repeats, unsigned int distance)
Definition: sound.cpp:1042
void stop_sound()
Definition: sound.cpp:565
This file contains the settings handling of the widget library.
Contains the gui2 timer routines.