The Battle for Wesnoth  1.19.7+dev
location_palette.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-editor"
17 
19 
20 #include "draw.hpp"
21 #include "editor/editor_common.hpp"
23 #include "font/sdl_ttf_compat.hpp"
24 #include "font/standard_colors.hpp"
25 #include "formula/string_utils.hpp"
26 #include "gettext.hpp"
29 
30 #include <boost/regex.hpp>
31 
32 static bool is_positive_integer(const std::string& str) {
33  return str != "0" && std::find_if(str.begin(), str.end(), [](char c) { return !std::isdigit(c); }) == str.end();
34 }
35 
37 {
38 public:
39  struct state_t {
41  : selected(false)
42  , mouseover(false)
43  {}
44  bool selected;
45  bool mouseover;
46  friend bool operator==(state_t r, state_t l)
47  {
48  return r.selected == l.selected && r.mouseover == l.mouseover;
49  }
50 
51  };
53  : gui::widget(true)
54  , parent_(parent)
55  {
56  }
57 
58  void draw_contents() override
59  {
60  if (state_.mouseover) {
61  draw::fill(location(), 200, 200, 200, 26);
62  }
63  if (state_.selected) {
64  draw::rect(location(), 255, 255, 255, 255);
65  }
66  font::pango_draw_text(true, location(), 16, font::NORMAL_COLOR, desc_.empty() ? id_ : desc_, location().x + 2, location().y, 0);
67  }
68 
69  //TODO move to widget
70  bool hit(int x, int y) const
71  {
72  return location().contains(x, y);
73  }
74 
75  void mouse_up(const SDL_MouseButtonEvent& e)
76  {
77  if (!(hit(e.x, e.y)))
78  return;
79  if (e.button == SDL_BUTTON_LEFT) {
81  }
82  if (e.button == SDL_BUTTON_RIGHT) {
83  //TODO: add a context menu with the following options:
84  // 1) 'copy it to clipboard'
85  // 2) 'jump to item'
86  // 3) 'delete item'.
87  }
88  }
89 
90  void handle_event(const SDL_Event& e) override
91  {
93 
94  if (hidden() || !enabled() || mouse_locked())
95  return;
96 
97  state_t start_state = state_;
98 
99  switch (e.type) {
100  case SDL_MOUSEBUTTONUP:
101  mouse_up(e.button);
102  break;
103  case SDL_MOUSEMOTION:
104  state_.mouseover = hit(e.motion.x, e.motion.y);
105  break;
106  default:
107  return;
108  }
109 
110  if (!(start_state == state_))
111  set_dirty(true);
112  }
113 
114  void set_item_id(const std::string& id)
115  {
116  id_ = id;
117  if (is_positive_integer(id)) {
118  desc_ = VGETTEXT("Player $side_num", utils::string_map{ {"side_num", id} });
119  }
120  else {
121  desc_ = "";
122  }
123  }
125  {
127  }
128 
129 private:
130  std::string id_;
131  std::string desc_;
134 };
135 
137 {
138 public:
139  location_palette_button(const SDL_Rect& location, const std::string& text, const std::function<void (void)>& callback)
140  : gui::button(text)
141  , callback_(callback)
142  {
143  this->set_location(location.x, location.y);
144  this->hide(false);
145  }
146 protected:
147  virtual void mouse_up(const SDL_MouseButtonEvent& e) override
148  {
150  if (callback_) {
151  if (this->pressed()) {
152  callback_();
153  }
154  }
155  }
156  std::function<void (void)> callback_;
157 
158 };
159 namespace editor {
161  : common_palette()
162  , item_size_(20)
163  //TODO avoid magic number
164  , item_space_(20 + 3)
165  , items_start_(0)
166  , selected_item_()
167  , items_()
168  , toolkit_(toolkit)
169  , buttons_()
170  , button_add_()
171  , button_delete_()
172  , button_goto_()
173  , disp_(gui)
174  {
175  for (int i = 1; i < 10; ++i) {
176  items_.push_back(std::to_string(i));
177  }
178  selected_item_ = items_[0];
179  }
180 
182 {
184  for (gui::widget& b : buttons_) {
185  h.push_back(&b);
186  }
187  if (button_add_) { h.push_back(button_add_.get()); }
188  if (button_delete_) { h.push_back(button_delete_.get()); }
189  if (button_goto_) { h.push_back(button_goto_.get()); }
190  return h;
191 }
192 
193 void location_palette::hide(bool hidden)
194 {
195  widget::hide(hidden);
196 
198 
199  std::shared_ptr<gui::button> palette_menu_button = disp_.find_menu_button("menu-editor-terrain");
200  palette_menu_button->set_overlay("");
201  palette_menu_button->enable(false);
202 
203  for(auto& w : handler_members()) {
204  static_cast<gui::widget&>(*w).hide(hidden);
205  }
206 }
207 
209 {
210  bool scrolled = false;
211  if(can_scroll_up()) {
212  --items_start_;
213  scrolled = true;
214  set_dirty(true);
215  }
216 
217  return scrolled;
218 }
220 {
221  return (items_start_ != 0);
222 }
223 
225 {
226  return (items_start_ + num_visible_items() + 1 <= num_items());
227 }
228 
230 {
231  bool scrolled = false;
232  if(can_scroll_down()) {
233  ++items_start_;
234  scrolled = true;
235  set_dirty(true);
236  }
237 
238  return scrolled;
239 }
240 
241 void location_palette::adjust_size(const SDL_Rect& target)
242 {
243  const int button_height = 22;
244  const int button_y = 30;
245  int bottom = target.y + target.h;
246  if (!button_goto_) {
247  button_goto_.reset(new location_palette_button(SDL_Rect{ target.x , bottom -= button_y, target.w - 10, button_height }, _("Go To"), [this]() {
248  //static_cast<mouse_action_starting_position&>(toolkit_.get_mouse_action()). ??
250  if (pos.valid()) {
252  }
253  }));
254  button_add_.reset(new location_palette_button(SDL_Rect{ target.x , bottom -= button_y, target.w - 10, button_height }, _("Add"), [this]() {
255  std::string newid;
256  if (gui2::dialogs::edit_text::execute(_("New Location Identifier"), "", newid)) {
257  static const boost::regex valid_id("[a-zA-Z0-9_]+");
258  if(boost::regex_match(newid, valid_id)) {
259  add_item(newid);
260  }
261  else {
263  _("Error"),
264  _("Invalid location id")
265  );
266  //TODO: a user visible messae would be nice.
267  ERR_ED << "entered invalid location id";
268  }
269  }
270  }));
271  button_delete_.reset(new location_palette_button(SDL_Rect{ target.x , bottom -= button_y, target.w - 10, button_height }, _("Delete"), nullptr));
272  }
273  else {
274  button_goto_->set_location(SDL_Rect{ target.x , bottom -= button_y, target.w - 10, button_height });
275  button_add_->set_location(SDL_Rect{ target.x , bottom -= button_y, target.w - 10, button_height });
276  button_delete_->set_location(SDL_Rect{ target.x , bottom -= button_y, target.w - 10, button_height });
277  }
278 
279  const int space_for_items = bottom - target.y;
280  const int items_fitting = space_for_items / item_space_;
281  // This might be called while the palette is not visible onscreen.
282  // If that happens, no items will fit and we'll have a negative number here.
283  // Just skip it in that case.
284  if(items_fitting > 0) {
285  // Items may be added dynamically via add_item(), so this creates all the buttons that
286  // fit in the space, even if some of them will be hidden until more items are added.
287  // This simplifies the scrolling code in add_item.
288  const std::size_t buttons_needed = items_fitting;
289  if(buttons_.size() != buttons_needed) {
290  location_palette_item lpi(this);
291  buttons_.resize(buttons_needed, lpi);
292  }
293  }
294 
295  // Update button locations and sizes. Needs to be done even if the number of buttons hasn't changed,
296  // because adjust_size() also handles moving left and right when the window's width is changed.
297  SDL_Rect dstrect;
298  dstrect.w = target.w - 10;
299  dstrect.h = item_size_ + 2;
300  for(std::size_t i = 0; i < buttons_.size(); ++i) {
301  dstrect.x = target.x;
302  dstrect.y = target.y + static_cast<int>(i) * item_space_;
303  buttons_[i].set_location(dstrect);
304  }
305 
306  set_location(target);
307  set_dirty(true);
309 }
310 
311 void location_palette::select_item(const std::string& item_id)
312 {
313  if (selected_item_ != item_id) {
314  selected_item_ = item_id;
315  set_dirty();
316  }
318 }
319 
321 {
322  return items_.size();
323 }
325 {
326  return buttons_.size();
327 }
328 
329 bool location_palette::is_selected_item(const std::string& id)
330 {
331  return selected_item_ == id;
332 }
333 
335 {
336  if (!dirty()) {
337  return;
338  }
339 
341 
342  // The hotkey system will automatically enable and disable the buttons when it runs, but it doesn't
343  // get triggered when handling mouse-wheel scrolling. Therefore duplicate that functionality here.
344  std::shared_ptr<gui::button> upscroll_button = disp_.find_action_button("upscroll-button-editor");
345  if(upscroll_button)
346  upscroll_button->enable(can_scroll_up());
347  std::shared_ptr<gui::button> downscroll_button = disp_.find_action_button("downscroll-button-editor");
348  if(downscroll_button)
349  downscroll_button->enable(can_scroll_down());
350 
351  if(button_goto_) {
352  button_goto_->set_dirty(true);
353  }
354  if(button_add_) {
355  button_add_->set_dirty(true);
356  }
357  if(button_delete_) {
358  button_delete_->set_dirty(true);
359  }
360  for(std::size_t i = 0; i < num_visible_items(); ++i) {
361  const auto item_index = items_start_ + i;
363 
364  tile.hide(true);
365 
366  // If we've scrolled to the end of the list, or if there aren't many items, leave the button hidden
367  if(item_index >= num_items()) {
368  // We want to hide all following buttons so we cannot use break here.
369  continue;
370  }
371 
372  const std::string item_id = items_[item_index];
373 
374  // These could have tooltips, but currently don't. Adding their hex co-ordinates would be an option,
375  // and for player starts adding the raw ID next might be good.
376  std::stringstream tooltip_text;
377 
378  tile.set_tooltip_string(tooltip_text.str());
379  tile.set_item_id(item_id);
380  tile.set_selected(is_selected_item(item_id));
381  tile.set_dirty(true);
382  tile.hide(false);
383  }
384 
385  set_dirty(false);
386 }
387 
389 {
390  // This is unnecessary as every GUI1 widget is a TLD.
391  //for(std::size_t i = 0; i < num_visible_items(); ++i) {
392  // location_palette_item& tile = buttons_[i];
393  // tile.draw();
394  //}
395 }
396 
397 std::vector<std::string> location_palette::action_pressed() const
398 {
399  std::vector<std::string> res;
400  if (button_delete_ && button_delete_->pressed()) {
401  res.push_back("editor-remove-location");
402  }
403  return res;
404 }
405 
407 {
408 }
409 
410 // Sort numbers before all other strings.
411 static bool loc_id_comp(const std::string& lhs, const std::string& rhs) {
412  if(is_positive_integer(lhs)) {
413  if(is_positive_integer(rhs)) {
414  return std::stoi(lhs) < std::stoi(rhs);
415  } else {
416  return true;
417  }
418  }
419  if(is_positive_integer(rhs)) {
420  return false;
421  }
422  return lhs < rhs;
423 }
424 
425 void location_palette::add_item(const std::string& id)
426 {
427  decltype(items_)::difference_type pos;
428 
429  // Insert the new ID at the sorted location, unless it's already in the list
430  const auto itor = std::upper_bound(items_.begin(), items_.end(), id, loc_id_comp);
431  if(itor == items_.begin() || *(itor - 1) != id) {
432  pos = std::distance(items_.begin(), items_.insert(itor, id));
433  } else {
434  pos = std::distance(items_.begin(), itor);
435  }
436  selected_item_ = id;
437 
438  // pos will always be positive because begin() was used as the first arg of std::distance(),
439  // but we need this (or casts) to prevent warnings about signed/unsigned comparisons.
440  const std::size_t unsigned_pos = pos;
441 
442  // Scroll if necessary so that the new item is visible
443  if(unsigned_pos < items_start_ || unsigned_pos >= items_start_ + num_visible_items()) {
444  if(unsigned_pos < num_visible_items()) {
445  items_start_ = 0;
446  } else if(unsigned_pos + num_visible_items() > num_items()) {
447  // This can't underflow, because unsigned_pos < num_items() and the
448  // previous conditional block would have been entered instead.
450  } else {
451  items_start_ = unsigned_pos - num_visible_items() / 2;
452  }
453  }
454 
455  // No need to call adjust_size(), because initialisation creates all possible buttons even when num_visible_items() > num_items().
456 }
457 
458 } // end namespace editor
std::shared_ptr< gui::button > find_action_button(const std::string &id)
Retrieves a pointer to a theme UI button.
Definition: display.cpp:770
std::shared_ptr< gui::button > find_menu_button(const std::string &id)
Definition: display.cpp:780
void scroll_to_tile(const map_location &loc, SCROLL_TYPE scroll_type=ONSCREEN, bool check_fogged=true, bool force=true)
Scroll such that location loc is on-screen.
Definition: display.cpp:1975
void set_help_string(const std::string &str)
Sets and shows the tooltip-like text at the top or bottom of the map area.
const editor_map & get_map() const
void clear_help_string()
Removes the help string.
void set_mouseover_overlay(editor_display &gui)
List of starting locations and location ids.
virtual bool can_scroll_down() override
std::unique_ptr< location_palette_button > button_goto_
void adjust_size(const SDL_Rect &target) override
Update the size of this widget.
std::vector< std::string > items_
virtual bool scroll_up() override
Scroll the editor-palette up one step if possible.
virtual void layout() override
Called by draw_manager to validate layout before drawing.
void hide(bool hidden) override
std::vector< location_palette_item > buttons_
std::size_t num_items() override
Return the number of items in the palette.
virtual bool can_scroll_up() override
virtual void draw_contents() override
Called by widget::draw()
void add_item(const std::string &id)
virtual bool is_selected_item(const std::string &id)
std::string get_help_string() const
std::unique_ptr< location_palette_button > button_add_
virtual sdl_handler_vector handler_members() override
virtual std::vector< std::string > action_pressed() const override
std::size_t num_visible_items()
Return the number of GUI elements that can show items.
virtual void select_item(const std::string &item_id)
virtual bool scroll_down() override
Scroll the editor-palette down one step if possible.
std::unique_ptr< location_palette_button > button_delete_
location_palette(editor_display &gui, editor_toolkit &toolkit)
map_location special_location(const std::string &id) const
Definition: map.cpp:312
bool pressed()
Definition: button.cpp:568
button(const std::string &label, TYPE type=TYPE_PRESS, const 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:42
virtual void mouse_up(const SDL_MouseButtonEvent &event)
Definition: button.cpp:486
widget(const bool auto_join=true)
Definition: widget.cpp:33
void set_dirty(bool dirty=true)
Definition: widget.cpp:180
bool dirty() const
Definition: widget.cpp:193
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 std::string & id() const
Definition: widget.cpp:198
const rect & location() const
Definition: widget.cpp:123
virtual void hide(bool value=true)
Definition: widget.cpp:141
bool enabled() const
Definition: widget.cpp:175
void set_tooltip_string(const std::string &str)
Definition: widget.cpp:246
bool hidden() const
Definition: widget.cpp:161
bool mouse_locked() const
Definition: widget.cpp:64
location_palette_button(const SDL_Rect &location, const std::string &text, const std::function< void(void)> &callback)
virtual void mouse_up(const SDL_MouseButtonEvent &e) override
std::function< void(void)> callback_
void mouse_up(const SDL_MouseButtonEvent &e)
void set_item_id(const std::string &id)
editor::location_palette * parent_
void set_selected(bool selected)
location_palette_item(editor::location_palette *parent)
bool hit(int x, int y) const
void handle_event(const SDL_Event &e) override
void draw_contents() override
Drawing functions, for drawing things on the screen.
Main (common) editor header.
#define ERR_ED
std::vector< events::sdl_handler * > sdl_handler_vector
Definition: events.hpp:182
#define VGETTEXT(msgid,...)
Handy wrappers around interpolate_variables_into_string and gettext.
std::size_t i
Definition: function.cpp:1029
int w
static std::string _(const char *str)
Definition: gettext.hpp:93
static bool is_positive_integer(const std::string &str)
void fill(const SDL_Rect &rect, uint8_t r, uint8_t g, uint8_t b, uint8_t a)
Fill an area with the given colour.
Definition: draw.cpp:50
void rect(const SDL_Rect &rect)
Draw a rectangle.
Definition: draw.cpp:150
Manage the empty-palette in the editor.
Definition: action.cpp:31
static bool loc_id_comp(const std::string &lhs, const std::string &rhs)
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 color_t NORMAL_COLOR
std::string selected
void show_transient_message(const std::string &title, const std::string &message, const std::string &image, const bool message_use_markup, const bool title_use_markup)
Shows a transient message to the user.
General purpose widgets.
int stoi(std::string_view str)
Same interface as std::stoi and meant as a drop in replacement, except:
Definition: charconv.hpp:154
std::map< std::string, t_string > string_map
Transitional API for porting SDL_ttf-based code to Pango.
friend bool operator==(state_t r, state_t l)
Encapsulates the map of the game.
Definition: location.hpp:45
bool valid() const
Definition: location.hpp:110
bool contains(int x, int y) const
Whether the given point lies within the rectangle.
Definition: rect.cpp:53
mock_char c
#define e
#define h
#define b