The Battle for Wesnoth  1.19.0-dev
button.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2003 - 2024
3  by David White <dave@whitevine.net>
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 
18 #include "widgets/button.hpp"
19 
20 #include "draw.hpp"
21 #include "filesystem.hpp"
22 #include "game_config.hpp"
23 #include "picture.hpp"
24 #include "log.hpp"
25 #include "font/sdl_ttf_compat.hpp"
26 #include "font/standard_colors.hpp"
27 #include "sdl/rect.hpp"
29 #include "sound.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(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(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 
83 // This function is a mess and i can only hope it someday dies a horrible death
85 
86  std::string size_postfix;
87 
88  switch (location().h) {
89  case 25:
90  size_postfix = "_25";
91  break;
92  case 30:
93  size_postfix = "_30";
94  break;
95  case 60:
96  size_postfix = "_60";
97  break;
98  default:
99  break;
100  }
101 
105  button_image_name_ + "-pressed.png" + button_image_path_suffix_);
108  if (filesystem::file_exists(game_config::path + "/images/" + button_image_name_ + "-disabled.png")) {
110  button_image_name_ + "-disabled.png" + button_image_path_suffix_);
111  } else {
113  }
114 
115  if (!button_overlay_image_name_.empty()) {
116 
117  if (button_overlay_image_name_.length() > size_postfix.length() &&
119  button_overlay_image_name_.resize(button_overlay_image_name_.length() - size_postfix.length());
120  }
121 
124 
125  if (filesystem::file_exists(game_config::path + "/images/" + button_overlay_image_name_ + size_postfix + "-active.png"))
127 
128  if (filesystem::file_exists(game_config::path + "/images/" + button_overlay_image_name_ + size_postfix + "-disabled.png"))
132 
133  if (filesystem::file_exists(game_config::path + "/images/" + button_overlay_image_name_ + size_postfix + "-disabled-pressed.png"))
137  } else {
139  }
140 
141  if (!disabledImage_) {
144  }
145 
146  if (!pressedImage_) {
148  }
149 
150  if (!activeImage_) {
152  }
153 
154  if (type_ == TYPE_CHECK || type_ == TYPE_RADIO) {
157  if (!touchedImage_) {
159  }
160 
162  button_image_name_ + "-active-pressed.png"+ button_image_path_suffix_);
163  if (!pressedActiveImage_) {
165  }
166 
167  if (filesystem::file_exists(game_config::path + "/images/" + button_image_name_ + size_postfix + "-disabled-pressed.png")) {
169  button_image_name_ + "-disabled-pressed.png"+ button_image_path_suffix_);
170  }
171  if (!pressedDisabledImage_) {
173  button_image_name_ + "-pressed.png~GS()"+ button_image_path_suffix_);
174  }
175  }
176 
177  if (!image_) {
178  std::string err_msg = "error initializing button images! file name: ";
179  err_msg += button_image_name_;
180  err_msg += ".png";
181  ERR_DP << err_msg;
182  throw game::error(err_msg);
183  }
184 
185  base_height_ = image_.h();
186  base_width_ = image_.w();
187 
188  if (type_ != TYPE_IMAGE) {
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){
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 
238 void button::set_check(bool check)
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  queue_redraw();
253  }
254 }
255 
256 void button::set_active(bool active)
257 {
258  if ((state_ == NORMAL) && active) {
259  state_ = ACTIVE;
260  queue_redraw();
261  } else if ((state_ == ACTIVE) && !active) {
262  state_ = NORMAL;
263  queue_redraw();
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 
285 // I can only assume this is working because nobody has complained it isn't.
287 {
288  texture image = image_;
289 
290  int offset = 0;
291  switch(state_) {
292  case ACTIVE:
294  break;
295  case PRESSED:
297  if (type_ == TYPE_PRESS)
298  offset = 1;
299  break;
300  case PRESSED_ACTIVE:
302  break;
303  case TOUCHED_NORMAL:
304  case TOUCHED_PRESSED:
306  break;
307  default:
308  break;
309  }
310 
311  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  if (state_ == PRESSED || state_ == PRESSED_ACTIVE)
328  else image = disabledImage_;
329 
330  button_color = font::GRAY_COLOR;
331  }
332 
333  SDL_Rect dest = loc;
334  if(type_ != TYPE_PRESS && type_ != TYPE_TURBO) {
335  // Scale other button types to match the base image?
336  dest.w = image_.w();
337  dest.h = image_.h();
338  }
339 
340  draw::blit(image, dest);
341 
342  if (overlayImage_) {
344 
345  if (overlayPressedImage_) {
346  switch (state_) {
347  case ACTIVE:
350  break;
351  case PRESSED:
352  case PRESSED_ACTIVE:
353  case TOUCHED_NORMAL:
354  case TOUCHED_PRESSED:
356  break;
357  default:
358  break;
359  }
360  }
361 
362  dest.w = overlay.w();
363  dest.h = overlay.h();
364  draw::blit(overlay, dest);
365  }
366 
367  if (type_ != TYPE_IMAGE){
368  clipArea.x += offset;
369  clipArea.y += offset;
370  clipArea.w -= 2*offset;
371  clipArea.h -= 2*offset;
372  font::pango_draw_text(true, clipArea, font_size_, button_color, label_text_, textx, texty);
373  }
374 }
375 
376 bool button::hit(int x, int y) const
377 {
378  return location().contains(x, y);
379 }
380 
381 static bool is_valid_image(const std::string& str) { return !str.empty() && str[0] != IMAGE_PREFIX; }
382 
383 void button::set_image(const std::string& image_file)
384 {
385  if(!is_valid_image(image_file)) {
386  return;
387  }
388 
389  button_image_name_ = "buttons/" + image_file;
390  load_images();
391  queue_redraw();
392 }
393 
394 void button::set_overlay(const std::string& image_file)
395 {
396  // We allow empty paths for overlays
397  if(image_file[0] == IMAGE_PREFIX) {
398  return;
399  }
400 
401  button_overlay_image_name_ = image_file;
402  load_images();
403  queue_redraw();
404 }
405 
406 void button::set_label(const std::string& val)
407 {
408  label_text_ = val;
409 
410  //if we have a list of items, use the first one that isn't an image
411  if (std::find(label_text_.begin(), label_text_.end(), COLUMN_SEPARATOR) != label_text_.end()) {
412  const std::vector<std::string>& items = utils::split(label_text_, COLUMN_SEPARATOR);
413  const std::vector<std::string>::const_iterator i = std::find_if(items.begin(),items.end(),is_valid_image);
414  if(i != items.end()) {
415  label_text_ = *i;
416  }
417  }
418 
419  calculate_size();
420  queue_redraw();
421 }
422 
423 void button::mouse_motion(const SDL_MouseMotionEvent& event)
424 {
425  if (hit(event.x, event.y)) {
426  // the cursor is over the widget
427  if (state_ == NORMAL)
428  state_ = ACTIVE;
429  else if (state_ == PRESSED && (type_ == TYPE_CHECK || type_ == TYPE_RADIO))
431  } else {
432  // the cursor is not over the widget
433 
434  if (type_ == TYPE_CHECK || type_ == TYPE_RADIO) {
435 
436  switch (state_) {
437  case TOUCHED_NORMAL:
438  state_ = NORMAL;
439  break;
440  case TOUCHED_PRESSED:
441  state_ = PRESSED;
442  break;
443  case PRESSED_ACTIVE:
444  state_ = PRESSED;
445  break;
446  case ACTIVE:
447  state_ = NORMAL;
448  break;
449  default:
450  break;
451  }
452  } else if ((type_ != TYPE_IMAGE) || state_ != PRESSED)
453  state_ = NORMAL;
454  }
455 }
456 
457 void button::mouse_down(const SDL_MouseButtonEvent& event)
458 {
459  if (hit(event.x, event.y) && event.button == SDL_BUTTON_LEFT) {
460 
461  switch (type_) {
462  case TYPE_RADIO:
463  case TYPE_CHECK:
464  if (state_ == PRESSED_ACTIVE)
466  else if (state_ == ACTIVE)
468  break;
469  case TYPE_TURBO:
471  state_ = PRESSED;
472  break;
473  default:
474  state_ = PRESSED;
475  break;
476  }
477  }
478 }
479 
481  state_ = NORMAL;
482  draw_contents();
483 }
484 
485 void button::mouse_up(const SDL_MouseButtonEvent& event)
486 {
487  if (!(hit(event.x, event.y) && event.button == SDL_BUTTON_LEFT))
488  return;
489 
490  // the user has stopped pressing the mouse left button while on the widget
491  switch (type_) {
492  case TYPE_CHECK:
493 
494  switch (state_) {
495  case TOUCHED_NORMAL:
497  pressed_ = true;
498  break;
499  case TOUCHED_PRESSED:
500  state_ = ACTIVE;
501  pressed_ = true;
502  break;
503  default:
504  break;
505  }
507  break;
508  case TYPE_RADIO:
511  pressed_ = true;
512  // exit(0);
514  }
515  //} else if (state_ == TOUCHED_PRESSED) {
516  // state_ = PRESSED_ACTIVE;
517  //}
518  break;
519  case TYPE_PRESS:
520  if (state_ == PRESSED) {
521  state_ = ACTIVE;
522  pressed_ = true;
524  }
525  break;
526  case TYPE_TURBO:
527  state_ = ACTIVE;
528  break;
529  case TYPE_IMAGE:
530  pressed_ = true;
532  break;
533  }
534 }
535 
536 void button::handle_event(const SDL_Event& event)
537 {
539 
540  if (hidden() || !enabled())
541  return;
542 
543  STATE start_state = state_;
544 
545  if (!mouse_locked())
546  {
547  switch(event.type) {
548  case SDL_MOUSEBUTTONDOWN:
549  mouse_down(event.button);
550  break;
551  case SDL_MOUSEBUTTONUP:
552  mouse_up(event.button);
553  break;
554  case SDL_MOUSEMOTION:
555  mouse_motion(event.motion);
556  break;
557  default:
558  return;
559  }
560  }
561 
562  if (start_state != state_) {
563  queue_redraw();
564  }
565 }
566 
568 {
569  if (type_ != TYPE_TURBO) {
570  const bool res = pressed_;
571  pressed_ = false;
572  return res;
573  } else
575 }
576 
577 }
int checkbox_horizontal_padding_
Definition: button.hpp:124
void set_overlay(const std::string &image_file_base)
Definition: button.cpp:394
texture image_
Definition: button.hpp:103
int base_width_
Definition: button.hpp:116
virtual void handle_event(const SDL_Event &event)
Definition: button.cpp:536
void load_images()
Definition: button.cpp:84
texture disabledImage_
Definition: button.hpp:104
bool hit(int x, int y) const
Definition: button.cpp:376
texture touchedImage_
Definition: button.hpp:104
bool pressed()
Definition: button.cpp:567
virtual void enable(bool new_val=true)
Definition: button.cpp:272
int font_size_
Definition: button.hpp:122
TYPE type_
Definition: button.hpp:93
int base_height_
Definition: button.hpp:116
button(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
std::string button_image_path_suffix_
Definition: button.hpp:120
void calculate_size()
Definition: button.cpp:201
texture overlayPressedImage_
Definition: button.hpp:105
virtual void draw_contents()
Definition: button.cpp:286
bool pressed_
Definition: button.hpp:112
void release()
Definition: button.cpp:480
STATE state_
Definition: button.hpp:110
SPACE_CONSUMPTION spacing_
Definition: button.hpp:114
std::string button_image_name_
Definition: button.hpp:118
void set_active(bool active)
Definition: button.cpp:256
SPACE_CONSUMPTION
Definition: button.hpp:63
@ MINIMUM_SPACE
Definition: button.hpp:63
texture overlayActiveImage_
Definition: button.hpp:106
virtual void mouse_up(const SDL_MouseButtonEvent &event)
Definition: button.cpp:485
texture activeImage_
Definition: button.hpp:103
std::string label_text_
Definition: button.hpp:101
texture pressedActiveImage_
Definition: button.hpp:103
@ TYPE_TURBO
Definition: button.hpp:60
@ TYPE_PRESS
Definition: button.hpp:60
@ TYPE_IMAGE
Definition: button.hpp:60
@ TYPE_CHECK
Definition: button.hpp:60
@ TYPE_RADIO
Definition: button.hpp:60
texture overlayDisabledImage_
Definition: button.hpp:105
int horizontal_padding_
Definition: button.hpp:123
texture overlayImage_
Definition: button.hpp:105
texture pressedDisabledImage_
Definition: button.hpp:104
void set_image(const std::string &image_file_base)
Definition: button.cpp:383
int vertical_padding_
Definition: button.hpp:125
void set_label(const std::string &val)
Definition: button.cpp:406
virtual void mouse_down(const SDL_MouseButtonEvent &event)
Definition: button.cpp:457
bool checked() const
Definition: button.cpp:267
virtual void mouse_motion(const SDL_MouseMotionEvent &event)
Definition: button.cpp:423
@ TOUCHED_NORMAL
Definition: button.hpp:109
@ TOUCHED_PRESSED
Definition: button.hpp:109
@ PRESSED_ACTIVE
Definition: button.hpp:109
texture pressedImage_
Definition: button.hpp:103
SDL_Rect textRect_
Definition: button.hpp:107
texture overlayPressedDisabledImage_
Definition: button.hpp:105
std::string button_overlay_image_name_
Definition: button.hpp:119
void set_check(bool check)
Definition: button.cpp:238
virtual ~button()
Default implementation, but defined out-of-line for efficiency reasons.
Definition: button.cpp:197
void set_width(int w)
Definition: widget.cpp:98
virtual void enable(bool new_val=true)
Definition: widget.cpp:167
void set_height(int h)
Definition: widget.cpp:103
virtual void set_location(const SDL_Rect &rect)
Definition: widget.cpp:69
virtual void handle_event(const SDL_Event &) override
Definition: widget.hpp:90
const rect & location() const
Definition: widget.cpp:123
void queue_redraw()
Indicate that the widget should be redrawn.
Definition: widget.cpp:215
bool enabled() const
Definition: widget.cpp:175
bool hidden() const
Definition: widget.cpp:161
bool mouse_locked() const
Definition: widget.cpp:64
Wrapper class to encapsulate creation and management of an SDL_Texture.
Definition: texture.hpp:33
int w() const
The draw-space width of the texture, in pixels.
Definition: texture.hpp:105
void reset()
Releases ownership of the managed texture and resets the ptr to null.
Definition: texture.cpp:208
int h() const
The draw-space height of the texture, in pixels.
Definition: texture.hpp:114
Drawing functions, for drawing things on the screen.
Declarations for File-IO.
std::size_t i
Definition: function.cpp:968
std::string label
What to show in the filter's drop-down list.
Definition: manager.cpp:209
Standard logging facilities (interface).
@ NORMAL
Definition: cursor.hpp:28
void blit(const texture &tex, const SDL_Rect &dst)
Draws a texture, or part of a texture, at the given location.
Definition: draw.cpp:310
static bool file_exists(const bfs::path &fpath)
Definition: filesystem.cpp:318
bool ends_with(const std::string &str, const std::string &suffix)
Collection of helper functions relating to Pango formatting.
const color_t BUTTON_COLOR
rect pango_draw_text(bool actually_draw, const 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.
const int SIZE_SMALL
Definition: constants.cpp:24
const color_t GRAY_COLOR
const int SIZE_BUTTON
Definition: constants.cpp:25
const std::string checkbox_release
const std::string button_press
std::string path
Definition: filesystem.cpp:83
General purpose widgets.
const int default_font_size
Definition: button.cpp:39
static bool is_valid_image(const std::string &str)
Definition: button.cpp:381
Functions to load and save images from/to disk.
texture get_texture(const image::locator &i_locator, TYPE type, bool skip_cache)
Returns an image texture suitable for hardware-accelerated rendering.
Definition: picture.cpp:960
const std::vector< std::string > items
constexpr const SDL_Rect empty_rect
Definition: rect.hpp:30
void play_UI_sound(const std::string &files)
Definition: sound.cpp:1064
std::vector< std::string > split(const config_attribute_value &val)
Contains the SDL_Rect helper code.
Transitional API for porting SDL_ttf-based code to Pango.
The basic class for representing 8-bit RGB or RGBA colour values.
Definition: color.hpp:59
Base class for all the errors encountered by the engine.
Definition: exceptions.hpp:29
bool contains(int x, int y) const
Whether the given point lies within the rectangle.
Definition: rect.cpp:52
static lg::log_domain log_display("display")
#define ERR_DP
Definition: button.cpp:35
char const IMAGE_PREFIX
char const COLUMN_SEPARATOR
#define h