The Battle for Wesnoth  1.19.0-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  editor_toolkit &toolkit)
162  : common_palette()
163  , item_size_(20)
164  //TODO avoid magic number
165  , item_space_(20 + 3)
166  , items_start_(0)
167  , selected_item_()
168  , items_()
169  , toolkit_(toolkit)
170  , buttons_()
171  , button_add_()
172  , button_delete_()
173  , button_goto_()
174  , disp_(gui)
175  {
176  for (int i = 1; i < 10; ++i) {
177  items_.push_back(std::to_string(i));
178  }
179  selected_item_ = items_[0];
180  }
181 
183 {
185  for (gui::widget& b : buttons_) {
186  h.push_back(&b);
187  }
188  if (button_add_) { h.push_back(button_add_.get()); }
189  if (button_delete_) { h.push_back(button_delete_.get()); }
190  if (button_goto_) { h.push_back(button_goto_.get()); }
191  return h;
192 }
193 
194 void location_palette::hide(bool hidden)
195 {
196  widget::hide(hidden);
197 
199 
200  std::shared_ptr<gui::button> palette_menu_button = disp_.find_menu_button("menu-editor-terrain");
201  palette_menu_button->set_overlay("");
202  palette_menu_button->enable(false);
203 
204  for(auto& w : handler_members()) {
205  static_cast<gui::widget&>(*w).hide(hidden);
206  }
207 }
208 
210 {
211  bool scrolled = false;
212  if(can_scroll_up()) {
213  --items_start_;
214  scrolled = true;
215  set_dirty(true);
216  }
217 
218  return scrolled;
219 }
221 {
222  return (items_start_ != 0);
223 }
224 
226 {
227  return (items_start_ + num_visible_items() + 1 <= num_items());
228 }
229 
231 {
232  bool scrolled = false;
233  if(can_scroll_down()) {
234  ++items_start_;
235  scrolled = true;
236  set_dirty(true);
237  }
238 
239  return scrolled;
240 }
241 
242 void location_palette::adjust_size(const SDL_Rect& target)
243 {
244  const int button_height = 22;
245  const int button_y = 30;
246  int bottom = target.y + target.h;
247  if (!button_goto_) {
248  button_goto_.reset(new location_palette_button(SDL_Rect{ target.x , bottom -= button_y, target.w - 10, button_height }, _("Go To"), [this]() {
249  //static_cast<mouse_action_starting_position&>(toolkit_.get_mouse_action()). ??
251  if (pos.valid()) {
253  }
254  }));
255  button_add_.reset(new location_palette_button(SDL_Rect{ target.x , bottom -= button_y, target.w - 10, button_height }, _("Add"), [this]() {
256  std::string newid;
257  if (gui2::dialogs::edit_text::execute(_("New Location Identifier"), "", newid)) {
258  static const boost::regex valid_id("[a-zA-Z0-9_]+");
259  if(boost::regex_match(newid, valid_id)) {
260  add_item(newid);
261  }
262  else {
264  _("Error"),
265  _("Invalid location id")
266  );
267  //TODO: a user visible messae would be nice.
268  ERR_ED << "entered invalid location id";
269  }
270  }
271  }));
272  button_delete_.reset(new location_palette_button(SDL_Rect{ target.x , bottom -= button_y, target.w - 10, button_height }, _("Delete"), nullptr));
273  }
274  else {
275  button_goto_->set_location(SDL_Rect{ target.x , bottom -= button_y, target.w - 10, button_height });
276  button_add_->set_location(SDL_Rect{ target.x , bottom -= button_y, target.w - 10, button_height });
277  button_delete_->set_location(SDL_Rect{ target.x , bottom -= button_y, target.w - 10, button_height });
278  }
279 
280  const int space_for_items = bottom - target.y;
281  const int items_fitting = space_for_items / item_space_;
282  // This might be called while the palette is not visible onscreen.
283  // If that happens, no items will fit and we'll have a negative number here.
284  // Just skip it in that case.
285  if(items_fitting > 0) {
286  // Items may be added dynamically via add_item(), so this creates all the buttons that
287  // fit in the space, even if some of them will be hidden until more items are added.
288  // This simplifies the scrolling code in add_item.
289  const std::size_t buttons_needed = items_fitting;
290  if(buttons_.size() != buttons_needed) {
291  location_palette_item lpi(this);
292  buttons_.resize(buttons_needed, lpi);
293  }
294  }
295 
296  // Update button locations and sizes. Needs to be done even if the number of buttons hasn't changed,
297  // because adjust_size() also handles moving left and right when the window's width is changed.
298  SDL_Rect dstrect;
299  dstrect.w = target.w - 10;
300  dstrect.h = item_size_ + 2;
301  for(std::size_t i = 0; i < buttons_.size(); ++i) {
302  dstrect.x = target.x;
303  dstrect.y = target.y + static_cast<int>(i) * item_space_;
304  buttons_[i].set_location(dstrect);
305  }
306 
307  set_location(target);
308  set_dirty(true);
310 }
311 
312 void location_palette::select_item(const std::string& item_id)
313 {
314  if (selected_item_ != item_id) {
315  selected_item_ = item_id;
316  set_dirty();
317  }
319 }
320 
322 {
323  return items_.size();
324 }
326 {
327  return buttons_.size();
328 }
329 
330 bool location_palette::is_selected_item(const std::string& id)
331 {
332  return selected_item_ == id;
333 }
334 
336 {
337  if (!dirty()) {
338  return;
339  }
340 
342 
343  // The hotkey system will automatically enable and disable the buttons when it runs, but it doesn't
344  // get triggered when handling mouse-wheel scrolling. Therefore duplicate that functionality here.
345  std::shared_ptr<gui::button> upscroll_button = disp_.find_action_button("upscroll-button-editor");
346  if(upscroll_button)
347  upscroll_button->enable(can_scroll_up());
348  std::shared_ptr<gui::button> downscroll_button = disp_.find_action_button("downscroll-button-editor");
349  if(downscroll_button)
350  downscroll_button->enable(can_scroll_down());
351 
352  if(button_goto_) {
353  button_goto_->set_dirty(true);
354  }
355  if(button_add_) {
356  button_add_->set_dirty(true);
357  }
358  if(button_delete_) {
359  button_delete_->set_dirty(true);
360  }
361  for(std::size_t i = 0; i < num_visible_items(); ++i) {
362  const auto item_index = items_start_ + i;
364 
365  tile.hide(true);
366 
367  // If we've scrolled to the end of the list, or if there aren't many items, leave the button hidden
368  if(item_index >= num_items()) {
369  // We want to hide all following buttons so we cannot use break here.
370  continue;
371  }
372 
373  const std::string item_id = items_[item_index];
374 
375  // These could have tooltips, but currently don't. Adding their hex co-ordinates would be an option,
376  // and for player starts adding the raw ID next might be good.
377  std::stringstream tooltip_text;
378 
379  tile.set_tooltip_string(tooltip_text.str());
380  tile.set_item_id(item_id);
381  tile.set_selected(is_selected_item(item_id));
382  tile.set_dirty(true);
383  tile.hide(false);
384  }
385 
386  set_dirty(false);
387 }
388 
390 {
391  // This is unnecessary as every GUI1 widget is a TLD.
392  //for(std::size_t i = 0; i < num_visible_items(); ++i) {
393  // location_palette_item& tile = buttons_[i];
394  // tile.draw();
395  //}
396 }
397 
398 std::vector<std::string> location_palette::action_pressed() const
399 {
400  std::vector<std::string> res;
401  if (button_delete_ && button_delete_->pressed()) {
402  res.push_back("editor-remove-location");
403  }
404  return res;
405 }
406 
408 {
409 }
410 
411 // Sort numbers before all other strings.
412 static bool loc_id_comp(const std::string& lhs, const std::string& rhs) {
413  if(is_positive_integer(lhs)) {
414  if(is_positive_integer(rhs)) {
415  return std::stoi(lhs) < std::stoi(rhs);
416  } else {
417  return true;
418  }
419  }
420  if(is_positive_integer(rhs)) {
421  return false;
422  }
423  return lhs < rhs;
424 }
425 
426 void location_palette::add_item(const std::string& id)
427 {
428  decltype(items_)::difference_type pos;
429 
430  // Insert the new ID at the sorted location, unless it's already in the list
431  const auto itor = std::upper_bound(items_.begin(), items_.end(), id, loc_id_comp);
432  if(itor == items_.begin() || *(itor - 1) != id) {
433  pos = std::distance(items_.begin(), items_.insert(itor, id));
434  } else {
435  pos = std::distance(items_.begin(), itor);
436  }
437  selected_item_ = id;
438 
439  // pos will always be positive because begin() was used as the first arg of std::distance(),
440  // but we need this (or casts) to prevent warnings about signed/unsigned comparisons.
441  const std::size_t unsigned_pos = pos;
442 
443  // Scroll if necessary so that the new item is visible
444  if(unsigned_pos < items_start_ || unsigned_pos >= items_start_ + num_visible_items()) {
445  if(unsigned_pos < num_visible_items()) {
446  items_start_ = 0;
447  } else if(unsigned_pos + num_visible_items() > num_items()) {
448  // This can't underflow, because unsigned_pos < num_items() and the
449  // previous conditional block would have been entered instead.
451  } else {
452  items_start_ = unsigned_pos - num_visible_items() / 2;
453  }
454  }
455 
456  // No need to call adjust_size(), because initialisation creates all possible buttons even when num_visible_items() > num_items().
457 }
458 
459 } // 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:805
std::shared_ptr< gui::button > find_menu_button(const std::string &id)
Definition: display.cpp:815
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:2025
const gamemap & get_map() const
Definition: display.hpp:106
void set_help_string(const std::string &str)
Sets and shows the tooltip-like text at the top or bottom of the map area.
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
location_palette(editor_display &gui, const game_config_view &, editor_toolkit &toolkit)
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::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_
virtual std::string get_help_string()
A class grating read only view to a vector of config objects, viewed as one config with all children ...
map_location special_location(const std::string &id) const
Definition: map.cpp:311
bool pressed()
Definition: button.cpp:567
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
virtual void mouse_up(const SDL_MouseButtonEvent &event)
Definition: button.cpp:485
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:190
#define VGETTEXT(msgid,...)
Handy wrappers around interpolate_variables_into_string and gettext.
std::size_t i
Definition: function.cpp:968
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.
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:38
bool valid() const
Definition: location.hpp:89
bool contains(int x, int y) const
Whether the given point lies within the rectangle.
Definition: rect.cpp:52
mock_char c
#define e
#define h
#define b