The Battle for Wesnoth  1.17.23+dev
location_palette.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2003 - 2023
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 #include "tooltips.hpp"
30 
31 #include <boost/regex.hpp>
32 
33 static bool is_positive_integer(const std::string& str) {
34  return str != "0" && std::find_if(str.begin(), str.end(), [](char c) { return !std::isdigit(c); }) == str.end();
35 }
36 
38 {
39 public:
40  struct state_t {
42  : selected(false)
43  , mouseover(false)
44  {}
45  bool selected;
46  bool mouseover;
47  friend bool operator==(state_t r, state_t l)
48  {
49  return r.selected == l.selected && r.mouseover == l.mouseover;
50  }
51 
52  };
54  : gui::widget(true)
55  , parent_(parent)
56  {
57  }
58 
59  void draw_contents() override
60  {
61  if (state_.mouseover) {
62  draw::fill(location(), 200, 200, 200, 26);
63  }
64  if (state_.selected) {
65  draw::rect(location(), 255, 255, 255, 255);
66  }
67  font::pango_draw_text(true, location(), 16, font::NORMAL_COLOR, desc_.empty() ? id_ : desc_, location().x + 2, location().y, 0);
68  }
69 
70  //TODO move to widget
71  bool hit(int x, int y) const
72  {
73  return location().contains(x, y);
74  }
75 
76  void mouse_up(const SDL_MouseButtonEvent& e)
77  {
78  if (!(hit(e.x, e.y)))
79  return;
80  if (e.button == SDL_BUTTON_LEFT) {
82  }
83  if (e.button == SDL_BUTTON_RIGHT) {
84  //TODO: add a context menu with the following options:
85  // 1) 'copy it to clipboard'
86  // 2) 'jump to item'
87  // 3) 'delete item'.
88  }
89  }
90 
91  void handle_event(const SDL_Event& e) override
92  {
94 
95  if (hidden() || !enabled() || mouse_locked())
96  return;
97 
98  state_t start_state = state_;
99 
100  switch (e.type) {
101  case SDL_MOUSEBUTTONUP:
102  mouse_up(e.button);
103  break;
104  case SDL_MOUSEMOTION:
105  state_.mouseover = hit(e.motion.x, e.motion.y);
106  break;
107  default:
108  return;
109  }
110 
111  if (!(start_state == state_))
112  set_dirty(true);
113  }
114 
115  void set_item_id(const std::string& id)
116  {
117  id_ = id;
118  if (is_positive_integer(id)) {
119  desc_ = VGETTEXT("Player $side_num", utils::string_map{ {"side_num", id} });
120  }
121  else {
122  desc_ = "";
123  }
124  }
126  {
128  }
129 
130 private:
131  std::string id_;
132  std::string desc_;
135 };
136 
138 {
139 public:
140  location_palette_button(const SDL_Rect& location, const std::string& text, const std::function<void (void)>& callback)
141  : gui::button(text)
142  , callback_(callback)
143  {
144  this->set_location(location.x, location.y);
145  this->hide(false);
146  }
147 protected:
148  virtual void mouse_up(const SDL_MouseButtonEvent& e) override
149  {
151  if (callback_) {
152  if (this->pressed()) {
153  callback_();
154  }
155  }
156  }
157  std::function<void (void)> callback_;
158 
159 };
160 namespace editor {
162  editor_toolkit &toolkit)
163  : common_palette()
164  , item_size_(20)
165  //TODO avoid magic number
166  , item_space_(20 + 3)
167  , items_start_(0)
168  , selected_item_()
169  , items_()
170  , toolkit_(toolkit)
171  , buttons_()
172  , button_add_()
173  , button_delete_()
174  , button_goto_()
175  , disp_(gui)
176  {
177  for (int i = 1; i < 10; ++i) {
178  items_.push_back(std::to_string(i));
179  }
180  selected_item_ = items_[0];
181  }
182 
184 {
186  for (gui::widget& b : buttons_) {
187  h.push_back(&b);
188  }
189  if (button_add_) { h.push_back(button_add_.get()); }
190  if (button_delete_) { h.push_back(button_delete_.get()); }
191  if (button_goto_) { h.push_back(button_goto_.get()); }
192  return h;
193 }
194 
195 void location_palette::hide(bool hidden)
196 {
197  widget::hide(hidden);
198 
200 
201  std::shared_ptr<gui::button> palette_menu_button = disp_.find_menu_button("menu-editor-terrain");
202  palette_menu_button->set_overlay("");
203  palette_menu_button->enable(false);
204 
205  for(auto& w : handler_members()) {
206  static_cast<gui::widget&>(*w).hide(hidden);
207  }
208 }
209 
211 {
212  bool scrolled = false;
213  if(can_scroll_up()) {
214  --items_start_;
215  scrolled = true;
216  set_dirty(true);
217  }
218 
219  return scrolled;
220 }
222 {
223  return (items_start_ != 0);
224 }
225 
227 {
228  return (items_start_ + num_visible_items() + 1 <= num_items());
229 }
230 
232 {
233  bool scrolled = false;
234  if(can_scroll_down()) {
235  ++items_start_;
236  scrolled = true;
237  set_dirty(true);
238  }
239 
240  return scrolled;
241 }
242 
243 void location_palette::adjust_size(const SDL_Rect& target)
244 {
245  const int button_height = 22;
246  const int button_y = 30;
247  int bottom = target.y + target.h;
248  if (!button_goto_) {
249  button_goto_.reset(new location_palette_button(SDL_Rect{ target.x , bottom -= button_y, target.w - 10, button_height }, _("Go To"), [this]() {
250  //static_cast<mouse_action_starting_position&>(toolkit_.get_mouse_action()). ??
252  if (pos.valid()) {
254  }
255  }));
256  button_add_.reset(new location_palette_button(SDL_Rect{ target.x , bottom -= button_y, target.w - 10, button_height }, _("Add"), [this]() {
257  std::string newid;
258  if (gui2::dialogs::edit_text::execute(_("New Location Identifier"), "", newid)) {
259  static const boost::regex valid_id("[a-zA-Z0-9_]+");
260  if(boost::regex_match(newid, valid_id)) {
261  add_item(newid);
262  }
263  else {
265  _("Error"),
266  _("Invalid location id")
267  );
268  //TODO: a user visible messae would be nice.
269  ERR_ED << "entered invalid location id";
270  }
271  }
272  }));
273  button_delete_.reset(new location_palette_button(SDL_Rect{ target.x , bottom -= button_y, target.w - 10, button_height }, _("Delete"), nullptr));
274  }
275  else {
276  button_goto_->set_location(SDL_Rect{ target.x , bottom -= button_y, target.w - 10, button_height });
277  button_add_->set_location(SDL_Rect{ target.x , bottom -= button_y, target.w - 10, button_height });
278  button_delete_->set_location(SDL_Rect{ target.x , bottom -= button_y, target.w - 10, button_height });
279  }
280 
281  const int space_for_items = bottom - target.y;
282  const int items_fitting = space_for_items / item_space_;
283  // This might be called while the palette is not visible onscreen.
284  // If that happens, no items will fit and we'll have a negative number here.
285  // Just skip it in that case.
286  if(items_fitting > 0) {
287  // Items may be added dynamically via add_item(), so this creates all the buttons that
288  // fit in the space, even if some of them will be hidden until more items are added.
289  // This simplifies the scrolling code in add_item.
290  const std::size_t buttons_needed = items_fitting;
291  if(buttons_.size() != buttons_needed) {
292  location_palette_item lpi(this);
293  buttons_.resize(buttons_needed, lpi);
294  }
295  }
296 
297  // Update button locations and sizes. Needs to be done even if the number of buttons hasn't changed,
298  // because adjust_size() also handles moving left and right when the window's width is changed.
299  SDL_Rect dstrect;
300  dstrect.w = target.w - 10;
301  dstrect.h = item_size_ + 2;
302  for(std::size_t i = 0; i < buttons_.size(); ++i) {
303  dstrect.x = target.x;
304  dstrect.y = target.y + static_cast<int>(i) * item_space_;
305  buttons_[i].set_location(dstrect);
306  }
307 
308  set_location(target);
309  set_dirty(true);
311 }
312 
313 void location_palette::select_item(const std::string& item_id)
314 {
315  if (selected_item_ != item_id) {
316  selected_item_ = item_id;
317  set_dirty();
318  }
320 }
321 
323 {
324  return items_.size();
325 }
327 {
328  return buttons_.size();
329 }
330 
331 bool location_palette::is_selected_item(const std::string& id)
332 {
333  return selected_item_ == id;
334 }
335 
337 {
338  if (!dirty()) {
339  return;
340  }
341 
343 
344  // The hotkey system will automatically enable and disable the buttons when it runs, but it doesn't
345  // get triggered when handling mouse-wheel scrolling. Therefore duplicate that functionality here.
346  std::shared_ptr<gui::button> upscroll_button = disp_.find_action_button("upscroll-button-editor");
347  if(upscroll_button)
348  upscroll_button->enable(can_scroll_up());
349  std::shared_ptr<gui::button> downscroll_button = disp_.find_action_button("downscroll-button-editor");
350  if(downscroll_button)
351  downscroll_button->enable(can_scroll_down());
352 
353  if(button_goto_) {
354  button_goto_->set_dirty(true);
355  }
356  if(button_add_) {
357  button_add_->set_dirty(true);
358  }
359  if(button_delete_) {
360  button_delete_->set_dirty(true);
361  }
362  for(std::size_t i = 0; i < num_visible_items(); ++i) {
363  const auto item_index = items_start_ + i;
365 
366  tile.hide(true);
367 
368  // If we've scrolled to the end of the list, or if there aren't many items, leave the button hidden
369  if(item_index >= num_items()) {
370  // We want to hide all following buttons so we cannot use break here.
371  continue;
372  }
373 
374  const std::string item_id = items_[item_index];
375 
376  // These could have tooltips, but currently don't. Adding their hex co-ordinates would be an option,
377  // and for player starts adding the raw ID next might be good.
378  std::stringstream tooltip_text;
379 
380  tile.set_tooltip_string(tooltip_text.str());
381  tile.set_item_id(item_id);
382  tile.set_selected(is_selected_item(item_id));
383  tile.set_dirty(true);
384  tile.hide(false);
385  }
386 
387  set_dirty(false);
388 }
389 
391 {
392  // This is unnecessary as every GUI1 widget is a TLD.
393  //for(std::size_t i = 0; i < num_visible_items(); ++i) {
394  // location_palette_item& tile = buttons_[i];
395  // tile.draw();
396  //}
397 }
398 
399 std::vector<std::string> location_palette::action_pressed() const
400 {
401  std::vector<std::string> res;
402  if (button_delete_ && button_delete_->pressed()) {
403  res.push_back("editor-remove-location");
404  }
405  return res;
406 }
407 
409 {
410 }
411 
412 // Sort numbers before all other strings.
413 static bool loc_id_comp(const std::string& lhs, const std::string& rhs) {
414  if(is_positive_integer(lhs)) {
415  if(is_positive_integer(rhs)) {
416  return std::stoi(lhs) < std::stoi(rhs);
417  } else {
418  return true;
419  }
420  }
421  if(is_positive_integer(rhs)) {
422  return false;
423  }
424  return lhs < rhs;
425 }
426 
427 void location_palette::add_item(const std::string& id)
428 {
429  decltype(items_)::difference_type pos;
430 
431  // Insert the new ID at the sorted location, unless it's already in the list
432  const auto itor = std::upper_bound(items_.begin(), items_.end(), id, loc_id_comp);
433  if(itor == items_.begin() || *(itor - 1) != id) {
434  pos = std::distance(items_.begin(), items_.insert(itor, id));
435  } else {
436  pos = std::distance(items_.begin(), itor);
437  }
438  selected_item_ = id;
439 
440  // pos will always be positive because begin() was used as the first arg of std::distance(),
441  // but we need this (or casts) to prevent warnings about signed/unsigned comparisons.
442  const std::size_t unsigned_pos = pos;
443 
444  // Scroll if necessary so that the new item is visible
445  if(unsigned_pos < items_start_ || unsigned_pos >= items_start_ + num_visible_items()) {
446  if(unsigned_pos < num_visible_items()) {
447  items_start_ = 0;
448  } else if(unsigned_pos + num_visible_items() > num_items()) {
449  // This can't underflow, because unsigned_pos < num_items() and the
450  // previous conditional block would have been entered instead.
452  } else {
453  items_start_ = unsigned_pos - num_visible_items() / 2;
454  }
455  }
456 
457  // No need to call adjust_size(), because initialisation creates all possible buttons even when num_visible_items() > num_items().
458 }
459 
460 } // 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:810
std::shared_ptr< gui::button > find_menu_button(const std::string &id)
Definition: display.cpp:820
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:2075
const gamemap & get_map() const
Definition: display.hpp:105
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:312
A button is a control that can be pushed to start an action or close a dialog.
Definition: button.hpp:52
bool pressed()
Definition: button.cpp:568
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: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:91
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:51
void rect(const SDL_Rect &rect)
Draw a rectangle.
Definition: draw.cpp:151
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:54
mock_char c
#define e
#define h
#define b