The Battle for Wesnoth  1.19.5+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"
22 #include "sdl/point.hpp"
23 #include "gui/core/timer.hpp"
24 #include "gui/widgets/button.hpp"
25 #include "gui/widgets/grid.hpp"
26 #include "gui/widgets/image.hpp"
27 #include "gui/widgets/label.hpp"
29 #include "gui/widgets/settings.hpp"
31 #include "gui/widgets/window.hpp"
32 #include "sound.hpp"
33 #include "variable.hpp"
34 
35 namespace gui2::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.
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 
57 
58 story_viewer::story_viewer(const std::string& scenario_name, const config& cfg_parsed)
59  : modal_dialog(window_id())
60  , controller_(vconfig(cfg_parsed, true), scenario_name)
61  , part_index_(0)
62  , current_part_(nullptr)
63  , timer_id_(0)
64  , next_draw_(0)
65  , fade_step_(0)
66  , fade_state_(NOT_FADING)
67 {
68  update_current_part_ptr();
69 }
70 
72 {
73  if(timer_id_ != 0) {
75  timer_id_ = 0;
76  }
77 }
78 
80 {
82 }
83 
85 {
86  set_enter_disabled(true);
87 
88  // Special callback handle key presses
89  connect_signal_pre_key_press(*this, std::bind(&story_viewer::key_press_callback, this, std::placeholders::_5));
90 
91  connect_signal_mouse_left_click(find_widget<button>("next"),
93  connect_signal_mouse_left_click(find_widget<button>("prev"),
95 
96  find_widget<scroll_label>("part_text")
97  .connect_signal<event::LEFT_BUTTON_CLICK>(
98  std::bind(&story_viewer::nav_button_callback, this, DIR_FORWARD), queue_position::front_pre_child);
99 
100  // Tell the game display not to draw
103 
104  display_part();
105 }
106 
108 {
109  // Bring the game display back again, if appropriate
111 }
112 
114 {
116 }
117 
119 {
120  static const int VOICE_SOUND_SOURCE_ID = 255;
121  // Update Back button state. Doing this here so it gets called in pre_show too.
122  find_widget<button>("prev").set_active(part_index_ != 0);
123 
124  //
125  // Music and sound
126  //
127  if(!current_part_->music().empty()) {
128  config music_config;
129  music_config["name"] = current_part_->music();
130  music_config["ms_after"] = 2000;
131  music_config["immediate"] = true;
132 
133  sound::play_music_config(music_config);
134  }
135 
136  if(!current_part_->sound().empty()) {
138  }
139 
140  sound::stop_sound(VOICE_SOUND_SOURCE_ID);
141  if(!current_part_->voice().empty()) {
142  sound::play_sound_positioned(current_part_->voice(), VOICE_SOUND_SOURCE_ID, 0, 0);
143  }
144 
145  config cfg, image;
146 
147  //
148  // Background images
149  //
150  bool has_background = false;
151  config* base_layer = nullptr;
152 
153  for(const auto& layer : current_part_->get_background_layers()) {
154  has_background |= !layer.file().empty();
155 
156  const bool preserve_ratio = layer.keep_aspect_ratio();
157  const bool tile_h = layer.tile_horizontally();
158  const bool tile_v = layer.tile_vertically();
159 
160  // By default, no scaling will be applied.
161  std::string width_formula = "(image_original_width)";
162  std::string height_formula = "(image_original_height)";
163 
164  // Background layers are almost always centered. In case of tiling, we want the full
165  // area in the horizontal or vertical direction, so set the origin to 0 for that axis.
166  // The resize mode will center the original image in the available area first/
167  std::string x_formula;
168  std::string y_formula;
169 
170  if(tile_h) {
171  x_formula = "0";
172  } else {
173  x_formula = "(max(pos, 0) where pos = (width / 2 - image_width / 2))";
174  }
175 
176  if(tile_v) {
177  y_formula = "0";
178  } else {
179  y_formula = "(max(pos, 0) where pos = (height / 2 - image_height / 2))";
180  }
181 
182  if(layer.scale_horizontally() && preserve_ratio) {
183  height_formula = "(min((image_original_height * width / image_original_width), height))";
184  } else if(layer.scale_vertically() || tile_v) {
185  height_formula = "(height)";
186  }
187 
188  if(layer.scale_vertically() && preserve_ratio) {
189  width_formula = "(min((image_original_width * height / image_original_height), width))";
190  } else if(layer.scale_horizontally() || tile_h) {
191  width_formula = "(width)";
192  }
193 
194  image["x"] = x_formula;
195  image["y"] = y_formula;
196  image["w"] = width_formula;
197  image["h"] = height_formula;
198  image["name"] = layer.file();
199  image["resize_mode"] = (tile_h || tile_v) ? "tile_center" : "scale";
200 
201  config& layer_image = cfg.add_child("image", image);
202 
203  if(base_layer == nullptr || layer.is_base_layer()) {
204  base_layer = &layer_image;
205  }
206  }
207 
208  canvas& window_canvas = get_window()->get_canvas(0);
209 
210  /* In order to avoid manually loading the image and calculating the scaling factor, we instead
211  * delegate the task of setting the necessary variables to the canvas once the calculations
212  * have been made internally.
213  *
214  * This sets the necessary values with the data for "this" image when its drawn. If no base
215  * layer was found (which would be the case if no backgrounds were provided at all), simply set
216  * some sane defaults directly.
217  */
218  if(base_layer != nullptr) {
219  (*base_layer)["actions"] = R"((
220  [
221  set_var('base_scale_x', as_decimal(image_width) / as_decimal(image_original_width)),
222  set_var('base_scale_y', as_decimal(image_height) / as_decimal(image_original_height)),
223  set_var('base_origin_x', clip_x),
224  set_var('base_origin_y', clip_y)
225  ]
226  ))";
227  } else {
228  window_canvas.set_variable("base_scale_x", wfl::variant(1));
229  window_canvas.set_variable("base_scale_y", wfl::variant(1));
230  window_canvas.set_variable("base_origin_x", wfl::variant(0));
231  window_canvas.set_variable("base_origin_y", wfl::variant(0));
232  }
233 
234  cfg.add_child("image", get_title_area_decor_config());
235 
236  window_canvas.set_cfg(cfg);
237 
238  // Needed to make the background redraw correctly.
239  window_canvas.update_size_variables();
241 
242  //
243  // Title
244  //
245  label& title_label = find_widget<label>("title");
246 
247  std::string title_text = current_part_->title();
248  bool showing_title;
249 
250  if(current_part_->show_title() && !title_text.empty()) {
251  showing_title = true;
252 
253  PangoAlignment title_text_alignment = decode_text_alignment(current_part_->title_text_alignment());
254 
256  title_label.set_text_alignment(title_text_alignment);
257  title_label.set_label(title_text);
258  } else {
259  showing_title = false;
260 
262  }
263 
264  //
265  // Story text
266  //
267  stacked_widget& text_stack = find_widget<stacked_widget>("text_and_control_stack");
268 
269  std::string new_panel_mode;
270 
271  switch(current_part_->story_text_location()) {
272 
274  new_panel_mode = "top";
275  break;
277  new_panel_mode = "center";
278  break;
280  new_panel_mode = "bottom";
281  break;
282  }
283 
284  text_stack.set_vertical_alignment(new_panel_mode);
285 
286  /* Set the panel mode control variables.
287  *
288  * We use get_layer_grid here to ensure the widget is always found regardless of
289  * whether the background is visible or not.
290  */
291  canvas& panel_canvas = text_stack.get_layer_grid(LAYER_BACKGROUND)->find_widget<panel>("text_panel").get_canvas(0);
292 
293  panel_canvas.set_variable("panel_position", wfl::variant(new_panel_mode));
294  panel_canvas.set_variable("title_present", wfl::variant(static_cast<int>(showing_title))); // cast to 0/1
295 
296  const std::string& part_text = current_part_->text();
297 
298  if(part_text.empty() || !has_background) {
299  // No text or no background for this part, hide the background layer.
300  text_stack.select_layer(LAYER_TEXT);
301  } else if(text_stack.current_layer() != -1) {
302  // If the background layer was previously hidden, re-show it.
303  text_stack.select_layer(-1);
304  }
305 
306  // Convert the story part text alignment types into the Pango equivalents
307  PangoAlignment story_text_alignment = decode_text_alignment(current_part_->story_text_alignment());
308 
309  scroll_label& text_label = find_widget<scroll_label>("part_text");
310  text_label.set_text_alignment(story_text_alignment);
311  text_label.set_text_alpha(0);
312  text_label.set_label(part_text);
313 
314  // Regenerate any background blur texture
315  panel_canvas.queue_reblur();
316 
317  begin_fade_draw(true);
318  // if the previous page was skipped, it is possible that we already have a timer running.
320  //
321  // Floating images (handle this last)
322  //
323  const auto& floating_images = current_part_->get_floating_images();
324 
325  // If we have images to draw, draw the first one now. A new non-repeating timer is added
326  // after every draw to schedule the next one after the specified interval.
327  //
328  // TODO: in the old GUI1 dialog, floating images delayed the appearance of the story panel until
329  // drawing was finished. Might be worth looking into restoring that.
330  if(!floating_images.empty()) {
331  draw_floating_image(floating_images.begin(), part_index_);
332  }
333 }
334 
335 void story_viewer::draw_floating_image(floating_image_list::const_iterator image_iter, int this_part_index)
336 {
337  const auto& images = current_part_->get_floating_images();
338  canvas& window_canvas = get_window()->get_canvas(0);
339 
340  // If the current part has changed or we're out of images to draw, exit the draw loop.
341  while((this_part_index == part_index_) && (image_iter != images.end())) {
342  const auto& floating_image = *image_iter;
343  ++image_iter;
344 
345  std::ostringstream x_ss;
346  std::ostringstream y_ss;
347 
348  // Floating images' locations are scaled by the same factor as the background.
349  x_ss << "(trunc(" << floating_image.ref_x() << " * base_scale_x) + base_origin_x";
350  y_ss << "(trunc(" << floating_image.ref_y() << " * base_scale_y) + base_origin_y";
351 
352  if(floating_image.centered()) {
353  x_ss << " - (image_width / 2)";
354  y_ss << " - (image_height / 2)";
355  }
356 
357  x_ss << ")";
358  y_ss << ")";
359 
360  config image;
361  image["x"] = x_ss.str();
362  image["y"] = y_ss.str();
363 
364  // Width and height don't need to be set unless the image needs to be scaled.
365  if(floating_image.resize_with_background()) {
366  image["w"] = "(image_original_width * base_scale_x)";
367  image["h"] = "(image_original_height * base_scale_y)";
368  }
369 
370  image["name"] = floating_image.file();
371  config cfg{"image", std::move(image)};
372 
373  cfg.add_child("image", std::move(image));
374  window_canvas.append_cfg(std::move(cfg));
375 
376  // Needed to make the background redraw correctly.
377  window_canvas.update_size_variables();
379 
380  // If a delay is specified, schedule the next image draw and break out of the loop.
381  const auto& draw_delay = floating_image.display_delay();
382  if(draw_delay != std::chrono::milliseconds{0}) {
383  // This must be a non-repeating timer
384  timer_id_ = add_timer(draw_delay, std::bind(&story_viewer::draw_floating_image, this, image_iter, this_part_index), false);
385  return;
386  }
387  }
388 
389  timer_id_ = 0;
390 }
391 
393 {
394  // If a button is pressed while fading in, abort and set alpha to full opaque.
395  if(fade_state_ == FADING_IN) {
396  halt_fade_draw();
397 
398  // Only set full alpha if Forward was pressed.
399  if(direction == DIR_FORWARD) {
400  find_widget<scroll_label>("part_text").set_text_alpha(ALPHA_OPAQUE);
402  return;
403  }
404  }
405 
406  // If a button is pressed while fading out, skip and show next part.
407  if(fade_state_ == FADING_OUT) {
408  display_part();
409  return;
410  }
411 
412  assert(fade_state_ == NOT_FADING);
413 
414  part_index_ = (direction == DIR_FORWARD ? part_index_ + 1 : part_index_ -1);
415 
416  // If we've viewed all the parts, close the dialog.
418  get_window()->close();
419  return;
420  }
421 
422  if(part_index_ < 0) {
423  part_index_ = 0;
424  }
425 
427 
428  begin_fade_draw(false);
429 }
430 
431 void story_viewer::key_press_callback(const SDL_Keycode key)
432 {
433  const bool next_keydown =
434  key == SDLK_SPACE
435  || key == SDLK_RETURN
436  || key == SDLK_KP_ENTER
437  || key == SDLK_RIGHT;
438 
439  const bool back_keydown =
440  key == SDLK_BACKSPACE
441  || key == SDLK_LEFT;
442 
443  if(next_keydown) {
445  } else if(back_keydown) {
447  }
448 }
449 
451 {
452  next_draw_ = SDL_GetTicks() + 20;
453 }
454 
456 {
457  set_next_draw();
458 
459  fade_step_ = fade_in ? 0 : 10;
460  fade_state_ = fade_in ? FADING_IN : FADING_OUT;
461 }
462 
464 {
465  next_draw_ = 0;
466  fade_step_ = -1;
468 }
469 
471 {
473 
474  if(next_draw_ && SDL_GetTicks() < next_draw_) {
475  return;
476  }
477 
478  if(fade_state_ == NOT_FADING) {
479  return;
480  }
481 
482  // If we've faded fully in...
483  if(fade_state_ == FADING_IN && fade_step_ > 10) {
484  halt_fade_draw();
485  return;
486  }
487 
488  // If we've faded fully out...
489  if(fade_state_ == FADING_OUT && fade_step_ < 0) {
490  halt_fade_draw();
491 
492  display_part();
493  return;
494  }
495 
496  unsigned short new_alpha = std::clamp<short>(fade_step_ * 25.5, 0, ALPHA_OPAQUE);
497  find_widget<scroll_label>("part_text").set_text_alpha(new_alpha);
498 
499  // The text stack also needs to be marked dirty so the background panel redraws correctly.
501 
502  if(fade_state_ == FADING_IN) {
503  fade_step_ ++;
504  } else if(fade_state_ == FADING_OUT) {
505  fade_step_ --;
506  }
507 
508  set_next_draw();
509 }
510 
512 {
513  find_widget<stacked_widget>("text_and_control_stack").queue_redraw();
514 }
515 
516 } // namespace dialogs
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:172
bool empty() const
Definition: config.cpp:849
config & add_child(config_key_type key)
Definition: config.cpp:440
void set_prevent_draw(bool pd=true)
Prevent the game display from drawing.
Definition: display.cpp:2139
bool get_prevent_draw()
Definition: display.cpp:2148
static display * get_singleton()
Returns the display object if a display object exists.
Definition: display.hpp:111
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:629
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:702
void set_cfg(const config &cfg, const bool force=false)
Sets the config.
Definition: canvas.hpp:121
Abstract base class for all modal dialogs.
Dialog to view the storyscreen.
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)
virtual void pre_show() override
Actions to be taken before showing the window.
void nav_button_callback(NAV_DIRECTION direction)
virtual void post_show() override
Actions to be taken after the window has been shown.
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.
NOT_DANGLING T * find_widget(const std::string &id, const bool must_be_active, const bool must_exist)
Gets a widget with the wanted id.
Definition: widget.hpp:742
void set_visible(const visibility visible)
Definition: widget.cpp:479
void queue_redraw()
Indicates that this widget should be redrawn.
Definition: widget.cpp:464
window * get_window()
Get the parent window.
Definition: widget.cpp:117
@ 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:293
void set_enter_disabled(const bool enter_disabled)
Disable the enter key.
Definition: window.hpp:323
void close()
Requests to close the window.
Definition: window.hpp:219
part_pointer_type get_part(int index) const
Definition: controller.hpp:40
@ 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
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 std::chrono::milliseconds &interval, const std::function< void(std::size_t id)> &callback, const bool repeat)
Adds a new timer.
Definition: timer.cpp:123
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:164
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:1046
void play_music_config(const config &music_node, bool allow_interrupt_current_track, int i)
Definition: sound.cpp:715
void play_sound_positioned(const std::string &files, int id, int repeats, unsigned int distance)
Definition: sound.cpp:1053
void stop_sound()
Definition: sound.cpp:565
This file contains the settings handling of the widget library.
Contains the gui2 timer routines.