The Battle for Wesnoth  1.15.2+dev
story_viewer.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2017-2018 by Charles Dang <exodia339@gmail.com>
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/callable_objects.hpp"
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
34 {
35 namespace 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  : 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 {
85  window.set_enter_disabled(true);
86 
87  // Special callback handle key presses
88  connect_signal_pre_key_press(window, std::bind(&story_viewer::key_press_callback, this, std::ref(window), _5));
89 
90  connect_signal_mouse_left_click(find_widget<button>(&window, "next", false),
91  std::bind(&story_viewer::nav_button_callback, this, std::ref(window), DIR_FORWARD));
92 
93  connect_signal_mouse_left_click(find_widget<button>(&window, "back", false),
94  std::bind(&story_viewer::nav_button_callback, this, std::ref(window), DIR_BACKWARDS));
95 
97  std::bind(&story_viewer::draw_callback, this, std::ref(window)));
98 
99  display_part(window);
100 }
101 
103 {
105 }
106 
108 {
109  static const int VOICE_SOUND_SOURCE_ID = 255;
110  // Update Back button state. Doing this here so it gets called in pre_show too.
111  find_widget<button>(&window, "back", false).set_active(part_index_ != 0);
112 
113  //
114  // Music and sound
115  //
116  if(!current_part_->music().empty()) {
117  config music_config;
118  music_config["name"] = current_part_->music();
119  music_config["ms_after"] = 2000;
120  music_config["immediate"] = true;
121 
122  sound::play_music_config(music_config);
123  }
124 
125  if(!current_part_->sound().empty()) {
127  }
128 
129  sound::stop_sound(VOICE_SOUND_SOURCE_ID);
130  if(!current_part_->voice().empty()) {
131  sound::play_sound_positioned(current_part_->voice(), VOICE_SOUND_SOURCE_ID, 0, 0);
132  }
133 
134  config cfg, image;
135 
136  //
137  // Background images
138  //
139  bool has_background = false;
140  config* base_layer = nullptr;
141 
142  for(const auto& layer : current_part_->get_background_layers()) {
143  has_background |= !layer.file().empty();
144 
145  const bool preserve_ratio = layer.keep_aspect_ratio();
146  const bool tile_h = layer.tile_horizontally();
147  const bool tile_v = layer.tile_vertically();
148 
149  // By default, no scaling will be applied.
150  std::string width_formula = "(image_original_width)";
151  std::string height_formula = "(image_original_height)";
152 
153  // Background layers are almost always centered. In case of tiling, we want the full
154  // area in the horizontal or vertical direction, so set the origin to 0 for that axis.
155  // The resize mode will center the original image in the available area first/
156  std::string x_formula;
157  std::string y_formula;
158 
159  if(tile_h) {
160  x_formula = "0";
161  } else {
162  x_formula = "(max(pos, 0) where pos = (width / 2 - image_width / 2))";
163  }
164 
165  if(tile_v) {
166  y_formula = "0";
167  } else {
168  y_formula = "(max(pos, 0) where pos = (height / 2 - image_height / 2))";
169  }
170 
171  if(layer.scale_horizontally() && preserve_ratio) {
172  height_formula = "(min((image_original_height * width / image_original_width), height))";
173  } else if(layer.scale_vertically() || tile_v) {
174  height_formula = "(height)";
175  }
176 
177  if(layer.scale_vertically() && preserve_ratio) {
178  width_formula = "(min((image_original_width * height / image_original_height), width))";
179  } else if(layer.scale_horizontally() || tile_h) {
180  width_formula = "(width)";
181  }
182 
183  image["x"] = x_formula;
184  image["y"] = y_formula;
185  image["w"] = width_formula;
186  image["h"] = height_formula;
187  image["name"] = layer.file();
188  image["resize_mode"] = (tile_h || tile_v) ? "tile_center" : "scale";
189 
190  config& layer_image = cfg.add_child("image", image);
191 
192  if(base_layer == nullptr || layer.is_base_layer()) {
193  base_layer = &layer_image;
194  }
195  }
196 
197  canvas& window_canvas = window.get_canvas(0);
198 
199  /* In order to avoid manually loading the image and calculating the scaling factor, we instead
200  * delegate the task of setting the necessary variables to the canvas once the calculations
201  * have been made internally.
202  *
203  * This sets the necessary values with the data for "this" image when its drawn. If no base
204  * layer was found (which would be the case if no backgrounds were provided at all), simply set
205  * some sane defaults directly.
206  */
207  if(base_layer != nullptr) {
208  (*base_layer)["actions"] = R"((
209  [
210  set_var('base_scale_x', as_decimal(image_width) / as_decimal(image_original_width)),
211  set_var('base_scale_y', as_decimal(image_height) / as_decimal(image_original_height)),
212  set_var('base_origin', loc(clip_x, clip_y))
213  ]
214  ))";
215  } else {
216  window_canvas.set_variable("base_scale_x", wfl::variant(1));
217  window_canvas.set_variable("base_scale_y", wfl::variant(1));
218  window_canvas.set_variable("base_origin", wfl::variant(std::make_shared<wfl::location_callable>(map_location::ZERO())));
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  window.set_is_dirty(true);
228 
229  //
230  // Title
231  //
232  label& title_label = find_widget<label>(&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>(&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>(&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(window, floating_images.begin(), part_index_);
316  }
317 }
318 
319 void story_viewer::draw_floating_image(window& window, floating_image_list::const_iterator image_iter, int this_part_index)
320 {
321  const auto& images = current_part_->get_floating_images();
322 
323  // If the current part has changed or we're out of images to draw, exit the draw loop.
324  if((this_part_index != part_index_) || (image_iter == images.end())) {
325  timer_id_ = 0;
326  return;
327  }
328 
329  const auto& floating_image = *image_iter;
330 
331  std::ostringstream x_ss;
332  std::ostringstream y_ss;
333 
334  // Floating images are scaled by the same factor as the background.
335  x_ss << "(trunc(fi_ref_x * base_scale_x) + base_origin.x";
336  y_ss << "(trunc(fi_ref_y * base_scale_y) + base_origin.y";
337 
338  if(floating_image.centered()) {
339  x_ss << " - (image_original_width / 2)";
340  y_ss << " - (image_original_height / 2)";
341  }
342 
343  x_ss << " where fi_ref_x = " << floating_image.ref_x() << ")";
344  y_ss << " where fi_ref_y = " << floating_image.ref_y() << ")";
345 
346  config cfg, image;
347 
348  image["x"] = x_ss.str();
349  image["y"] = y_ss.str();
350  image["w"] = floating_image.autoscale() ? "(width)" : "(image_width)";
351  image["h"] = floating_image.autoscale() ? "(height)" : "(image_height)";
352  image["name"] = floating_image.file();
353 
354  // TODO: implement handling of the tiling options.
355  //image["resize_mode"] = "tile_centered"
356 
357  cfg.add_child("image", std::move(image));
358 
359  canvas& window_canvas = window.get_canvas(0);
360 
361  // Needed to make the background redraw correctly.
362  window_canvas.append_cfg(cfg);
363  window_canvas.set_is_dirty(true);
364 
365  window.set_is_dirty(true);
366 
367  ++image_iter;
368 
369  // If a delay is specified, schedule the next image draw. This *must* be a non-repeating timer!
370  // Else draw the next image immediately.
371  const unsigned int draw_delay = floating_image.display_delay();
372 
373  if(draw_delay != 0) {
374  timer_id_ = add_timer(draw_delay,
375  std::bind(&story_viewer::draw_floating_image, this, std::ref(window), image_iter, this_part_index), false);
376  } else {
377  draw_floating_image(window, image_iter, this_part_index);
378  }
379 }
380 
382 {
383  // If a button is pressed while fading in, abort and set alpha to full opaque.
384  if(fade_state_ == FADING_IN) {
385  halt_fade_draw();
386 
387  // Only set full alpha if Forward was pressed.
388  if(direction == DIR_FORWARD) {
389  find_widget<scroll_label>(&window, "part_text", false).set_text_alpha(ALPHA_OPAQUE);
390  flag_stack_as_dirty(window);
391  return;
392  }
393  }
394 
395  // If a button is pressed while fading out, skip and show next part.
396  if(fade_state_ == FADING_OUT) {
397  display_part(window);
398  return;
399  }
400 
401  assert(fade_state_ == NOT_FADING);
402 
403  part_index_ = (direction == DIR_FORWARD ? part_index_ + 1 : part_index_ -1);
404 
405  // If we've viewed all the parts, close the dialog.
407  window.close();
408  return;
409  }
410 
411  if(part_index_ < 0) {
412  part_index_ = 0;
413  }
414 
416 
417  begin_fade_draw(false);
418 }
419 
420 void story_viewer::key_press_callback(window& window, const SDL_Keycode key)
421 {
422  const bool next_keydown =
423  key == SDLK_SPACE
424  || key == SDLK_RETURN
425  || key == SDLK_KP_ENTER
426  || key == SDLK_RIGHT;
427 
428  const bool back_keydown =
429  key == SDLK_BACKSPACE
430  || key == SDLK_LEFT;
431 
432  if(next_keydown) {
434  } else if(back_keydown) {
436  }
437 }
438 
440 {
441  next_draw_ = SDL_GetTicks() + 20;
442 }
443 
445 {
446  set_next_draw();
447 
448  fade_step_ = fade_in ? 0 : 10;
449  fade_state_ = fade_in ? FADING_IN : FADING_OUT;
450 }
451 
453 {
454  next_draw_ = 0;
455  fade_step_ = -1;
457 }
458 
460 {
461  if(next_draw_ && SDL_GetTicks() < next_draw_) {
462  return;
463  }
464 
465  if(fade_state_ == NOT_FADING) {
466  return;
467  }
468 
469  // If we've faded fully in...
470  if(fade_state_ == FADING_IN && fade_step_ > 10) {
471  halt_fade_draw();
472  return;
473  }
474 
475  // If we've faded fully out...
476  if(fade_state_ == FADING_OUT && fade_step_ < 0) {
477  halt_fade_draw();
478 
479  display_part(window);
480  return;
481  }
482 
483  unsigned short new_alpha = utils::clamp<short>(fade_step_ * 25.5, 0, ALPHA_OPAQUE);
484  find_widget<scroll_label>(&window, "part_text", false).set_text_alpha(new_alpha);
485 
486  // The text stack also needs to be marked dirty so the background panel redraws correctly.
487  flag_stack_as_dirty(window);
488 
489  if(fade_state_ == FADING_IN) {
490  fade_step_ ++;
491  } else if(fade_state_ == FADING_OUT) {
492  fade_step_ --;
493  }
494 
495  set_next_draw();
496 }
497 
499 {
500  find_widget<stacked_widget>(&window, "text_and_control_stack", false).set_is_dirty(true);
501 }
502 
503 } // namespace dialogs
504 } // namespace gui2
storyscreen::controller::part_pointer_type current_part_
void key_press_callback(window &window, const SDL_Keycode key)
void close()
Requests to close the window.
Definition: window.hpp:183
Center of the screen.
Definition: part.hpp:234
static const unsigned int LAYER_TEXT
void play_sound_positioned(const std::string &files, int id, int repeats, unsigned int distance)
Definition: sound.cpp:995
static const unsigned int LAYER_BACKGROUND
static const map_location & ZERO()
Definition: location.hpp:79
grid * get_layer_grid(unsigned int i)
Gets the grid for a specified layer.
int draw_delay()
Definition: general.cpp:845
void set_variable(const std::string &key, const wfl::variant &value)
Definition: canvas.hpp:171
void draw_callback(window &window)
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
Inherited from modal_dialog.
void stop_sound()
Definition: sound.cpp:533
STL namespace.
Label showing a text.
Definition: label.hpp:32
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:235
void set_cfg(const config &cfg, const bool force=false)
Sets the config.
Definition: canvas.hpp:121
void play_sound(const std::string &files, channel_group group, unsigned int repeats)
Definition: sound.cpp:988
Generic file dialog.
Definition: field-fwd.hpp:22
void nav_button_callback(window &window, NAV_DIRECTION direction)
virtual void set_label(const t_string &label)
void flag_stack_as_dirty(window &window)
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:135
void set_visible(const visibility visible)
Definition: widget.cpp:473
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:233
void set_is_dirty(const bool is_dirty)
Definition: widget.cpp:463
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.
Various uncategorised dialogs.
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:178
Top of the screen.
Definition: part.hpp:233
void draw_floating_image(window &window, floating_image_list::const_iterator image_iter, int this_part_index)
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:476
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:48
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:228
void display_part(window &window)
void play_music_config(const config &music_node, bool allow_interrupt_current_track, int i)
Definition: sound.cpp:673
A variable-expanding proxy for the config class.
Definition: variable.hpp:42
this module manages the cache of images.
virtual void set_vertical_alignment(const std::string &alignment)
Sets the horizontal alignment of the widget within its parent grid.
Definition: widget.cpp:279
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:68
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:63
bool empty() const
Definition: config.cpp:884
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:253
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:287