The Battle for Wesnoth  1.15.12+dev
button.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
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 
17 #include "widgets/button.hpp"
18 
19 #include "filesystem.hpp"
20 #include "game_config.hpp"
21 #include "game_errors.hpp"
22 #include "picture.hpp"
23 #include "log.hpp"
24 #include "font/sdl_ttf_compat.hpp"
25 #include "font/standard_colors.hpp"
26 #include "sdl/rect.hpp"
28 #include "sound.hpp"
29 #include "video.hpp"
30 #include "wml_separators.hpp"
31 
32 #include <boost/algorithm/string/predicate.hpp>
33 
34 static lg::log_domain log_display("display");
35 #define ERR_DP LOG_STREAM(err, log_display)
36 
37 namespace gui {
38 
40 
41 button::button(CVideo& video, const std::string& label, button::TYPE type,
42  std::string button_image_name, SPACE_CONSUMPTION spacing,
43  const bool auto_join, std::string overlay_image, int font_size)
44  : widget(video, auto_join), type_(type),
45  label_text_(label),
46  image_(nullptr), pressedImage_(nullptr), activeImage_(nullptr), pressedActiveImage_(nullptr),
47  disabledImage_(nullptr), pressedDisabledImage_(nullptr),
48  overlayImage_(nullptr), overlayPressedImage_(nullptr), overlayActiveImage_(nullptr),
49  state_(NORMAL), pressed_(false),
50  spacing_(spacing), base_height_(0), base_width_(0),
51  button_image_name_(), button_overlay_image_name_(overlay_image),
52  button_image_path_suffix_(),
53  font_size_(font_size <= 0 ? (type != TYPE_CHECK && type != TYPE_RADIO ? default_font_size : font::SIZE_SMALL) : font_size),
54  horizontal_padding_(font_size_),
55  checkbox_horizontal_padding_(font_size_ / 2),
56  vertical_padding_(font_size_ / 2)
57 {
58  if (button_image_name.empty()) {
59 
60  switch (type_) {
61  case TYPE_PRESS:
62  button_image_name_ = "buttons/button_normal/button_H22";
63  break;
64  case TYPE_TURBO:
65  button_image_name_ = "buttons/button_menu/menu_button_copper_H20";
66  break;
67  case TYPE_CHECK:
68  button_image_name_ = "buttons/checkbox";
69  break;
70  case TYPE_RADIO:
71  button_image_name_ = "buttons/radiobox";
72  break;
73  default:
74  break;
75  }
76  } else {
77  button_image_name_ = "buttons/" + button_image_name;
78  }
79 
80  load_images();
81 }
82 
84 
85  std::string size_postfix;
86 
87  switch (location().h) {
88  case 25:
89  size_postfix = "_25";
90  break;
91  case 30:
92  size_postfix = "_30";
93  break;
94  case 60:
95  size_postfix = "_60";
96  break;
97  default:
98  break;
99  }
100 
102  surface pressed_image(image::get_image(button_image_name_ + "-pressed.png"+ button_image_path_suffix_));
104  surface disabled_image;
105  if (filesystem::file_exists(game_config::path + "/images/" + button_image_name_ + "-disabled.png"))
106  disabled_image = image::get_image(button_image_name_ + "-disabled.png"+ button_image_path_suffix_);
107  surface pressed_disabled_image, pressed_active_image, touched_image;
108 
109  if (!button_overlay_image_name_.empty()) {
110 
111  if (button_overlay_image_name_.length() > size_postfix.length() &&
113  button_overlay_image_name_.resize(button_overlay_image_name_.length() - size_postfix.length());
114  }
115 
118 
119  if (filesystem::file_exists(game_config::path + "/images/" + button_overlay_image_name_ + size_postfix + "-active.png"))
121 
122  if (filesystem::file_exists(game_config::path + "/images/" + button_overlay_image_name_ + size_postfix + "-disabled.png"))
126 
127  if (filesystem::file_exists(game_config::path + "/images/" + button_overlay_image_name_ + size_postfix + "-disabled-pressed.png"))
131  } else {
132  overlayImage_ = nullptr;
133  }
134 
135  if (disabled_image == nullptr) {
136  disabled_image = image::get_image(button_image_name_ + ".png~GS()" + button_image_path_suffix_);
137  }
138 
139  if (!pressed_image)
140  pressed_image = button_image;
141 
142  if (!active_image)
143  active_image = button_image;
144 
145  if (type_ == TYPE_CHECK || type_ == TYPE_RADIO) {
146  touched_image = image::get_image(button_image_name_ + "-touched.png"+ button_image_path_suffix_);
147  if (!touched_image)
148  touched_image = pressed_image;
149 
150  pressed_active_image = image::get_image(button_image_name_ + "-active-pressed.png"+ button_image_path_suffix_);
151  if (!pressed_active_image)
152  pressed_active_image = pressed_image;
153 
154  if (filesystem::file_exists(game_config::path + "/images/" + button_image_name_ + size_postfix + "-disabled-pressed.png"))
155  pressed_disabled_image = image::get_image(button_image_name_ + "-disabled-pressed.png"+ button_image_path_suffix_);
156  if (!pressed_disabled_image)
157  pressed_disabled_image = image::get_image(button_image_name_ + "-pressed.png~GS()"+ button_image_path_suffix_);
158  }
159 
160  if (!button_image) {
161  std::string err_msg = "error initializing button images! file name: ";
162  err_msg += button_image_name_;
163  err_msg += ".png";
164  ERR_DP << err_msg << std::endl;
165  throw game::error(err_msg);
166  }
167 
168  base_height_ = button_image->h;
169  base_width_ = button_image->w;
170 
171  if (type_ != TYPE_IMAGE) {
173  }
174 
175  if(type_ == TYPE_PRESS || type_ == TYPE_TURBO) {
176  image_ = scale_surface(button_image,location().w,location().h);
177  pressedImage_ = scale_surface(pressed_image,location().w,location().h);
178  activeImage_ = scale_surface(active_image,location().w,location().h);
179  disabledImage_ = scale_surface(disabled_image,location().w,location().h);
180  } else {
181  image_ = scale_surface(button_image,button_image->w,button_image->h);
182  activeImage_ = scale_surface(active_image,button_image->w,button_image->h);
183  disabledImage_ = scale_surface(disabled_image,button_image->w,button_image->h);
184  pressedImage_ = scale_surface(pressed_image,button_image->w,button_image->h);
185  if (type_ == TYPE_CHECK || type_ == TYPE_RADIO) {
186  pressedDisabledImage_ = scale_surface(pressed_disabled_image,button_image->w,button_image->h);
187  pressedActiveImage_ = scale_surface(pressed_active_image, button_image->w, button_image->h);
188  touchedImage_ = scale_surface(touched_image, button_image->w, button_image->h);
189  }
190  }
191 
192  if (type_ == TYPE_IMAGE){
193  calculate_size();
194  }
195 }
196 
198 {
199 }
200 
202 {
203  if (type_ == TYPE_IMAGE){
204  SDL_Rect loc_image = location();
205  loc_image.h = image_->h;
206  loc_image.w = image_->w;
207  set_location(loc_image);
208  return;
209  }
210 
211  if (type_ != TYPE_IMAGE){
212  textRect_ = font::pango_draw_text(nullptr, video().screen_area(), font_size_, font::BUTTON_COLOR, label_text_, 0, 0);
213  }
214 
215  // TODO: There's a weird text clipping bug, allowing the code below to run fixes it.
216  // The proper fix should possibly be in the draw_contents() function.
217 #if 0
218  if (!change_size)
219  return;
220 #endif
221 
223  if(type_ == TYPE_PRESS || type_ == TYPE_TURBO) {
224  if(spacing_ == MINIMUM_SPACE) {
226  } else {
228  }
229  } else {
230  if(label_text_.empty()) {
232  } else {
234  }
235  }
236 }
237 
239 {
240  if (type_ != TYPE_CHECK && type_ != TYPE_RADIO && type_ != TYPE_IMAGE)
241  return;
242  STATE new_state;
243 
244  if (check) {
245  new_state = (state_ == ACTIVE || state_ == PRESSED_ACTIVE)? PRESSED_ACTIVE : PRESSED;
246  } else {
247  new_state = (state_ == ACTIVE || state_ == PRESSED_ACTIVE)? ACTIVE : NORMAL;
248  }
249 
250  if (state_ != new_state) {
251  state_ = new_state;
252  set_dirty();
253  }
254 }
255 
256 void button::set_active(bool active)
257 {
258  if ((state_ == NORMAL) && active) {
259  state_ = ACTIVE;
260  set_dirty();
261  } else if ((state_ == ACTIVE) && !active) {
262  state_ = NORMAL;
263  set_dirty();
264  }
265 }
266 
267 bool button::checked() const
268 {
270 }
271 
272 void button::enable(bool new_val)
273 {
274  if(new_val != enabled())
275  {
276  pressed_ = false;
277  // check buttons should keep their state
278  if(type_ != TYPE_CHECK) {
279  state_ = NORMAL;
280  }
281  widget::enable(new_val);
282  }
283 }
284 
286 {
287  surface image = image_;
288  const int image_w = image_->w;
289 
290  int offset = 0;
291  switch(state_) {
292  case ACTIVE:
293  image = activeImage_;
294  break;
295  case PRESSED:
296  image = pressedImage_;
297  if (type_ == TYPE_PRESS)
298  offset = 1;
299  break;
300  case PRESSED_ACTIVE:
301  image = pressedActiveImage_;
302  break;
303  case TOUCHED_NORMAL:
304  case TOUCHED_PRESSED:
305  image = touchedImage_;
306  break;
307  default:
308  break;
309  }
310 
311  const SDL_Rect& loc = location();
312  SDL_Rect clipArea = loc;
313  const int texty = loc.y + loc.h / 2 - textRect_.h / 2 + offset;
314  int textx;
315 
316  if (type_ != TYPE_CHECK && type_ != TYPE_RADIO && type_ != TYPE_IMAGE)
317  textx = loc.x + image->w / 2 - textRect_.w / 2 + offset;
318  else {
319  clipArea.w += image_w + checkbox_horizontal_padding_;
320  textx = loc.x + image_w + checkbox_horizontal_padding_ / 2;
321  }
322 
323  color_t button_color = font::BUTTON_COLOR;
324 
325  if (!enabled()) {
326 
327  if (state_ == PRESSED || state_ == PRESSED_ACTIVE)
328  image = pressedDisabledImage_;
329  else image = disabledImage_;
330 
331  button_color = font::GRAY_COLOR;
332  }
333 
334  if (overlayImage_) {
335 
337 
338  if (overlayPressedImage_) {
339  switch (state_) {
340  case ACTIVE:
342  noverlay = &overlayActiveImage_;
343  break;
344  case PRESSED:
345  case PRESSED_ACTIVE:
346  case TOUCHED_NORMAL:
347  case TOUCHED_PRESSED:
349  break;
350  default:
351  break;
352  }
353  }
354 
355  surface nimage = image.clone();
356  sdl_blit(*noverlay, nullptr, nimage, nullptr);
357  image = nimage;
358  }
359 
360  video().blit_surface(loc.x, loc.y, image);
361  if (type_ != TYPE_IMAGE){
362  clipArea.x += offset;
363  clipArea.y += offset;
364  clipArea.w -= 2*offset;
365  clipArea.h -= 2*offset;
366  font::pango_draw_text(&video(), clipArea, font_size_, button_color, label_text_, textx, texty);
367  }
368 }
369 
370 bool button::hit(int x, int y) const
371 {
372  return sdl::point_in_rect(x,y,location());
373 }
374 
375 static bool is_valid_image(const std::string& str) { return !str.empty() && str[0] != IMAGE_PREFIX; }
376 
377 void button::set_image(const std::string& image_file)
378 {
379  if(!is_valid_image(image_file)) {
380  return;
381  }
382 
383  button_image_name_ = "buttons/" + image_file;
384  load_images();
385  set_dirty();
386 }
387 
388 void button::set_overlay(const std::string& image_file)
389 {
390  // We allow empty paths for overlays
391  if(image_file[0] == IMAGE_PREFIX) {
392  return;
393  }
394 
395  button_overlay_image_name_ = image_file;
396  load_images();
397  set_dirty();
398 }
399 
400 void button::set_label(const std::string& val)
401 {
402  label_text_ = val;
403 
404  //if we have a list of items, use the first one that isn't an image
405  if (std::find(label_text_.begin(), label_text_.end(), COLUMN_SEPARATOR) != label_text_.end()) {
406  const std::vector<std::string>& items = utils::split(label_text_, COLUMN_SEPARATOR);
407  const std::vector<std::string>::const_iterator i = std::find_if(items.begin(),items.end(),is_valid_image);
408  if(i != items.end()) {
409  label_text_ = *i;
410  }
411  }
412 
413  calculate_size();
414 
415  set_dirty(true);
416 }
417 
418 void button::mouse_motion(const SDL_MouseMotionEvent& event)
419 {
420  if (hit(event.x, event.y)) {
421  // the cursor is over the widget
422  if (state_ == NORMAL)
423  state_ = ACTIVE;
424  else if (state_ == PRESSED && (type_ == TYPE_CHECK || type_ == TYPE_RADIO))
426  } else {
427  // the cursor is not over the widget
428 
429  if (type_ == TYPE_CHECK || type_ == TYPE_RADIO) {
430 
431  switch (state_) {
432  case TOUCHED_NORMAL:
433  state_ = NORMAL;
434  break;
435  case TOUCHED_PRESSED:
436  state_ = PRESSED;
437  break;
438  case PRESSED_ACTIVE:
439  state_ = PRESSED;
440  break;
441  case ACTIVE:
442  state_ = NORMAL;
443  break;
444  default:
445  break;
446  }
447  } else if ((type_ != TYPE_IMAGE) || state_ != PRESSED)
448  state_ = NORMAL;
449  }
450 }
451 
452 void button::mouse_down(const SDL_MouseButtonEvent& event)
453 {
454  if (hit(event.x, event.y) && event.button == SDL_BUTTON_LEFT) {
455 
456  switch (type_) {
457  case TYPE_RADIO:
458  case TYPE_CHECK:
459  if (state_ == PRESSED_ACTIVE)
461  else if (state_ == ACTIVE)
463  break;
464  case TYPE_TURBO:
466  state_ = PRESSED;
467  break;
468  default:
469  state_ = PRESSED;
470  break;
471  }
472  }
473 }
474 
476  state_ = NORMAL;
477  draw_contents();
478 }
479 
480 void button::mouse_up(const SDL_MouseButtonEvent& event)
481 {
482  if (!(hit(event.x, event.y) && event.button == SDL_BUTTON_LEFT))
483  return;
484 
485  // the user has stopped pressing the mouse left button while on the widget
486  switch (type_) {
487  case TYPE_CHECK:
488 
489  switch (state_) {
490  case TOUCHED_NORMAL:
492  pressed_ = true;
493  break;
494  case TOUCHED_PRESSED:
495  state_ = ACTIVE;
496  pressed_ = true;
497  break;
498  default:
499  break;
500  }
502  break;
503  case TYPE_RADIO:
506  pressed_ = true;
507  // exit(0);
509  }
510  //} else if (state_ == TOUCHED_PRESSED) {
511  // state_ = PRESSED_ACTIVE;
512  //}
513  break;
514  case TYPE_PRESS:
515  if (state_ == PRESSED) {
516  state_ = ACTIVE;
517  pressed_ = true;
519  }
520  break;
521  case TYPE_TURBO:
522  state_ = ACTIVE;
523  break;
524  case TYPE_IMAGE:
525  pressed_ = true;
527  break;
528  }
529 }
530 
531 void button::handle_event(const SDL_Event& event)
532 {
534 
535  if (hidden() || !enabled())
536  return;
537 
538  STATE start_state = state_;
539 
540  if (!mouse_locked())
541  {
542  switch(event.type) {
543  case SDL_MOUSEBUTTONDOWN:
544  mouse_down(event.button);
545  break;
546  case SDL_MOUSEBUTTONUP:
547  mouse_up(event.button);
548  break;
549  case SDL_MOUSEMOTION:
550  mouse_motion(event.motion);
551  break;
552  default:
553  return;
554  }
555  }
556 
557  if (start_state != state_)
558  set_dirty(true);
559 }
560 
562 {
563  if (type_ != TYPE_TURBO) {
564  const bool res = pressed_;
565  pressed_ = false;
566  return res;
567  } else
569 }
570 
571 }
const color_t BUTTON_COLOR
surface get_image(const image::locator &i_locator, TYPE type)
Caches and returns an image.
Definition: picture.cpp:815
#define ERR_DP
Definition: button.cpp:35
std::string button_image_name_
Definition: button.hpp:116
bool enabled() const
Definition: widget.cpp:202
void set_check(bool check)
Definition: button.cpp:238
virtual void mouse_up(const SDL_MouseButtonEvent &event)
Definition: button.cpp:480
std::string button_image_path_suffix_
Definition: button.hpp:118
static void check(LexState *ls, int c)
Definition: lparser.cpp:107
bool pressed_
Definition: button.hpp:110
virtual void enable(bool new_val=true)
Definition: widget.cpp:194
const color_t GRAY_COLOR
bool hidden() const
Definition: widget.cpp:188
Collection of helper functions relating to Pango formatting.
int base_height_
Definition: button.hpp:114
void set_label(const std::string &val)
Definition: button.cpp:400
surface pressedActiveImage_
Definition: button.hpp:101
bool hit(int x, int y) const
Definition: button.cpp:370
void set_image(const std::string &image_file_base)
Definition: button.cpp:377
static l_noret error(LoadState *S, const char *why)
Definition: lundump.cpp:40
surface pressedImage_
Definition: button.hpp:101
Definition: video.hpp:31
virtual void enable(bool new_val=true)
Definition: button.cpp:272
static bool file_exists(const bfs::path &fpath)
Definition: filesystem.cpp:263
bool ends_with(const std::string &str, const std::string &suffix)
button(CVideo &video, const std::string &label, TYPE type=TYPE_PRESS, std::string button_image="", SPACE_CONSUMPTION spacing=DEFAULT_SPACE, const bool auto_join=true, std::string overlay_image="", int font_size=-1)
Definition: button.cpp:41
surface overlayPressedImage_
Definition: button.hpp:101
surface activeImage_
Definition: button.hpp:101
General purpose widgets.
const int default_font_size
Definition: button.cpp:39
void set_width(int w)
Definition: widget.cpp:109
#define h
surface disabledImage_
Definition: button.hpp:101
int checkbox_horizontal_padding_
Definition: button.hpp:122
const std::vector< std::string > items
surface scale_surface(const surface &surf, int w, int h)
Scale a surface using alpha-weighted modified bilinear filtering Note: causes artifacts with alpha gr...
Definition: utils.cpp:196
void blit_surface(int x, int y, surface surf, SDL_Rect *srcrect=nullptr, SDL_Rect *clip_rect=nullptr)
Draws a surface directly onto the screen framebuffer.
Definition: video.cpp:168
virtual void mouse_down(const SDL_MouseButtonEvent &event)
Definition: button.cpp:452
void load_images()
Definition: button.cpp:83
virtual void draw_contents()
Definition: button.cpp:285
surface clone() const
Makes a copy of this surface.
Definition: surface.cpp:75
char const IMAGE_PREFIX
int base_width_
Definition: button.hpp:114
void release()
Definition: button.cpp:475
void set_dirty(bool dirty=true)
Definition: widget.cpp:207
surface overlayImage_
Definition: button.hpp:101
virtual void handle_event(const SDL_Event &)
Definition: widget.cpp:330
std::string button_overlay_image_name_
Definition: button.hpp:117
static bool is_valid_image(const std::string &str)
Definition: button.cpp:375
void calculate_size()
Definition: button.cpp:201
const SDL_Rect & location() const
Definition: widget.cpp:134
SPACE_CONSUMPTION spacing_
Definition: button.hpp:112
TYPE type_
Definition: button.hpp:91
SDL_Rect pango_draw_text(CVideo *gui, const SDL_Rect &area, int size, const color_t &color, const std::string &text, int x, int y, bool use_tooltips, pango_text::FONT_STYLE style)
Draws text on the screen.
surface touchedImage_
Definition: button.hpp:101
virtual void mouse_motion(const SDL_MouseMotionEvent &event)
Definition: button.cpp:418
SPACE_CONSUMPTION
Definition: button.hpp:61
void set_overlay(const std::string &image_file_base)
Definition: button.cpp:388
std::string path
Definition: game_config.cpp:38
bool pressed()
Definition: button.cpp:561
const int SIZE_BUTTON
Definition: constants.cpp:24
virtual void set_location(const SDL_Rect &rect)
Definition: widget.cpp:75
bool checked() const
Definition: button.cpp:267
surface image_
Definition: button.hpp:101
bool point_in_rect(int x, int y, const SDL_Rect &rect)
Tests whether a point is inside a rectangle.
Definition: rect.cpp:22
std::size_t i
Definition: function.cpp:940
virtual void handle_event(const SDL_Event &event)
Definition: button.cpp:531
Declarations for File-IO.
int w
bool mouse_locked() const
Definition: widget.cpp:62
surface overlayPressedDisabledImage_
Definition: button.hpp:101
int font_size_
Definition: button.hpp:120
CVideo & video() const
Definition: widget.hpp:83
const std::string button_press
surface overlayActiveImage_
Definition: button.hpp:101
std::string label_text_
Definition: button.hpp:99
SDL_Rect textRect_
Definition: button.hpp:105
Contains the SDL_Rect helper code.
char const COLUMN_SEPARATOR
virtual ~button()
Default implementation, but defined out-of-line for efficiency reasons.
Definition: button.cpp:197
std::vector< std::string > split(const config_attribute_value &val)
int horizontal_padding_
Definition: button.hpp:121
Functions to load and save images from/to disk.
void play_UI_sound(const std::string &files)
Definition: sound.cpp:1051
Standard logging facilities (interface).
surface pressedDisabledImage_
Definition: button.hpp:101
STATE state_
Definition: button.hpp:108
int vertical_padding_
Definition: button.hpp:123
void sdl_blit(const surface &src, SDL_Rect *src_rect, surface &dst, SDL_Rect *dst_rect)
Definition: utils.hpp:31
static lg::log_domain log_display("display")
void set_height(int h)
Definition: widget.cpp:114
Transitional API for porting SDL_ttf-based code to Pango.
surface overlayDisabledImage_
Definition: button.hpp:101
const int SIZE_SMALL
Definition: constants.cpp:23
void set_active(bool active)
Definition: button.cpp:256
const std::string checkbox_release