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