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-editor"
16
17 #include "editor/palette/location_palette.hpp"
18
19 #include "gettext.hpp"
20 #include "font/marked-up_text.hpp"
21 #include "font/standard_colors.hpp"
22 #include "tooltips.hpp"
23
24 #include "editor/editor_common.hpp"
25 #include "editor/toolkit/editor_toolkit.hpp"
26 #include "gui/dialogs/edit_text.hpp"
27 #include "gui/dialogs/transient_message.hpp"
28
29 #include "formula/string_utils.hpp"
30
31 #include <boost/regex.hpp>
32
is_positive_integer(const std::string & str)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
37 class location_palette_item : public gui::widget
38 {
39 public:
40 struct state_t {
state_tlocation_palette_item::state_t41 state_t()
42 : selected(false)
43 , mouseover(false)
44 {}
45 bool selected;
46 bool mouseover;
operator ==(state_t r,state_t l)47 friend bool operator==(state_t r, state_t l)
48 {
49 return r.selected == l.selected && r.mouseover == l.mouseover;
50 }
51
52 };
location_palette_item(CVideo & video,editor::location_palette & parent)53 location_palette_item(CVideo& video, editor::location_palette& parent)
54 : gui::widget(video, true)
55 , parent_(parent)
56 {
57 }
58
draw_contents()59 void draw_contents() override
60 {
61 if (state_.mouseover) {
62 sdl::fill_rectangle(location(), {200, 200, 200, 26});
63 }
64 if (state_.selected) {
65 sdl::draw_rectangle(location(), {255, 255, 255, 255});
66 }
67 font::draw_text(&video(), location(), 16, font::NORMAL_COLOR, desc_.empty() ? id_ : desc_, location().x + 2, location().y, 0);
68 }
69
70 //TODO move to widget
hit(int x,int y) const71 bool hit(int x, int y) const
72 {
73 return sdl::point_in_rect(x, y, location());
74 }
75
mouse_up(const SDL_MouseButtonEvent & e)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) {
81 parent_.select_item(id_);
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
handle_event(const SDL_Event & e)91 void handle_event(const SDL_Event& e) override
92 {
93 gui::widget::handle_event(e);
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
set_item_id(const std::string & id)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 }
set_selected(bool selected)125 void set_selected(bool selected)
126 {
127 state_.selected = selected;
128 }
draw()129 void draw() override { gui::widget::draw(); }
130 private:
131 std::string id_;
132 std::string desc_;
133 state_t state_;
134 editor::location_palette& parent_;
135 };
136
137 class location_palette_button : public gui::button
138 {
139 public:
location_palette_button(CVideo & video,const SDL_Rect & location,const std::string & text,const std::function<void (void)> & callback)140 location_palette_button(CVideo& video, const SDL_Rect& location, const std::string& text, const std::function<void (void)>& callback)
141 : gui::button(video, text)
142 , callback_(callback)
143 {
144 this->set_location(location.x, location.y);
145 this->hide(false);
146 }
147 protected:
mouse_up(const SDL_MouseButtonEvent & e)148 virtual void mouse_up(const SDL_MouseButtonEvent& e) override
149 {
150 gui::button::mouse_up(e);
151 if (callback_) {
152 if (this->pressed()) {
153 callback_();
154 }
155 }
156 }
157 std::function<void (void)> callback_;
158
159 };
160 namespace editor {
location_palette(editor_display & gui,const config &,editor_toolkit & toolkit)161 location_palette::location_palette(editor_display &gui, const config& /*cfg*/,
162 editor_toolkit &toolkit)
163 : common_palette(gui.video())
164 , item_size_(20)
165 //TODO avoid magic number
166 , item_space_(20 + 3)
167 , palette_y_(0)
168 , palette_x_(0)
169 , items_start_(0)
170 , selected_item_()
171 , items_()
172 , toolkit_(toolkit)
173 , buttons_()
174 , button_add_()
175 , button_delete_()
176 , button_goto_()
177 , help_handle_(-1)
178 , disp_(gui)
179 {
180 for (int i = 1; i < 10; ++i) {
181 items_.push_back(std::to_string(i));
182 }
183 selected_item_ = items_[0];
184 }
185
handler_members()186 sdl_handler_vector location_palette::handler_members()
187 {
188 sdl_handler_vector h;
189 for (gui::widget& b : buttons_) {
190 h.push_back(&b);
191 }
192 if (button_add_) { h.push_back(button_add_.get()); }
193 if (button_delete_) { h.push_back(button_delete_.get()); }
194 if (button_goto_) { h.push_back(button_goto_.get()); }
195 return h;
196 }
197
hide(bool hidden)198 void location_palette::hide(bool hidden)
199 {
200 widget::hide(hidden);
201
202 disp_.video().clear_help_string(help_handle_);
203
204 std::shared_ptr<gui::button> palette_menu_button = disp_.find_menu_button("menu-editor-terrain");
205 palette_menu_button->set_overlay("");
206 palette_menu_button->enable(false);
207
208 for(auto& w : handler_members()) {
209 static_cast<gui::widget&>(*w).hide(hidden);
210 }
211 }
212
scroll_up()213 bool location_palette::scroll_up()
214 {
215 int decrement = 1;
216 if(items_start_ >= decrement) {
217 items_start_ -= decrement;
218 draw();
219 return true;
220 }
221 return false;
222 }
can_scroll_up()223 bool location_palette::can_scroll_up()
224 {
225 return (items_start_ != 0);
226 }
227
can_scroll_down()228 bool location_palette::can_scroll_down()
229 {
230 return (items_start_ + num_visible_items() + 1 <= num_items());
231 }
232
scroll_down()233 bool location_palette::scroll_down()
234 {
235 bool end_reached = (!(items_start_ + num_visible_items() + 1 <= num_items()));
236 bool scrolled = false;
237
238 // move downwards
239 if(!end_reached) {
240 items_start_ += 1;
241 scrolled = true;
242 set_dirty(true);
243 }
244 draw();
245 return scrolled;
246 }
247
adjust_size(const SDL_Rect & target)248 void location_palette::adjust_size(const SDL_Rect& target)
249 {
250 palette_x_ = target.x;
251 palette_y_ = target.y;
252 const int button_height = 22;
253 const int button_y = 30;
254 int bottom = target.y + target.h;
255 if (!button_goto_) {
256 button_goto_.reset(new location_palette_button(video(), SDL_Rect{ target.x , bottom -= button_y, target.w - 10, button_height }, _("Go To"), [this]() {
257 //static_cast<mouse_action_starting_position&>(toolkit_.get_mouse_action()). ??
258 map_location pos = disp_.get_map().special_location(selected_item_);
259 if (pos.valid()) {
260 disp_.scroll_to_tile(pos, display::WARP);
261 }
262 }));
263 button_add_.reset(new location_palette_button(video(), SDL_Rect{ target.x , bottom -= button_y, target.w - 10, button_height }, _("Add"), [this]() {
264 std::string newid;
265 if (gui2::dialogs::edit_text::execute(_("New Location Identifier"), "", newid)) {
266 static const boost::regex valid_id("[a-zA-Z0-9_]+");
267 if(boost::regex_match(newid, valid_id)) {
268 add_item(newid);
269 }
270 else {
271 gui2::show_transient_message(
272 _("Error"),
273 _("Invalid location id")
274 );
275 //TODO: a user visible messae would be nice.
276 ERR_ED << "entered invalid location id\n";
277 }
278 }
279 }));
280 button_delete_.reset(new location_palette_button(video(), SDL_Rect{ target.x , bottom -= button_y, target.w - 10, button_height }, _("Delete"), nullptr));
281 }
282 else {
283 button_goto_->set_location(SDL_Rect{ target.x , bottom -= button_y, target.w - 10, button_height });
284 button_add_->set_location(SDL_Rect{ target.x , bottom -= button_y, target.w - 10, button_height });
285 button_delete_->set_location(SDL_Rect{ target.x , bottom -= button_y, target.w - 10, button_height });
286 }
287
288 const int space_for_items = bottom - target.y;
289 const int items_fitting = space_for_items / item_space_;
290 // This might be called while the palette is not visible onscreen.
291 // If that happens, no items will fit and we'll have a negative number here.
292 // Just skip it in that case.
293 if(items_fitting > 0 && num_visible_items() != items_fitting) {
294 location_palette_item lpi(disp_.video(), *this);
295 //Why does this need a pointer to a non-const as second paraeter?
296 //TODO: we should write our own ptr_vector class, boost::ptr_vector has a lot of flaws.
297 buttons_.resize(items_fitting, &lpi);
298 }
299
300 set_location(target);
301 set_dirty(true);
302 disp_.video().clear_help_string(help_handle_);
303 help_handle_ = disp_.video().set_help_string(get_help_string());
304 }
305
select_item(const std::string & item_id)306 void location_palette::select_item(const std::string& item_id)
307 {
308 if (selected_item_ != item_id) {
309 selected_item_ = item_id;
310 set_dirty();
311 }
312 disp_.video().clear_help_string(help_handle_);
313 help_handle_ = disp_.video().set_help_string(get_help_string());
314 }
315
num_items()316 int location_palette::num_items()
317 {
318 return items_.size();
319 }
num_visible_items()320 int location_palette::num_visible_items()
321 {
322 return buttons_.size();
323 }
324
is_selected_item(const std::string & id)325 bool location_palette::is_selected_item(const std::string& id)
326 {
327 return selected_item_ == id;
328 }
329
draw_contents()330 void location_palette::draw_contents()
331 {
332 toolkit_.set_mouseover_overlay(disp_);
333 int y = palette_y_;
334 const int x = palette_x_;
335 const int starting = items_start_;
336 int ending = std::min<int>(starting + num_visible_items(), num_items());
337 std::shared_ptr<gui::button> upscroll_button = disp_.find_action_button("upscroll-button-editor");
338 if (upscroll_button)
339 upscroll_button->enable(starting != 0);
340 std::shared_ptr<gui::button> downscroll_button = disp_.find_action_button("downscroll-button-editor");
341 if (downscroll_button)
342 downscroll_button->enable(ending != num_items());
343
344 if (button_goto_) {
345 button_goto_->set_dirty(true);
346 }
347 if (button_add_) {
348 button_add_->set_dirty(true);
349 }
350 if (button_delete_) {
351 button_delete_->set_dirty(true);
352 }
353 for (int i = 0, size = num_visible_items(); i < size; i++) {
354
355 location_palette_item & tile = buttons_[i];
356
357 tile.hide(true);
358
359 if (i >= ending) {
360 //We want to hide all following buttons so we cannot use break here.
361 continue;
362 }
363
364 const std::string item_id = items_[starting + i];
365
366 std::stringstream tooltip_text;
367
368 SDL_Rect dstrect;
369 dstrect.x = x;
370 dstrect.y = y;
371 dstrect.w = location().w - 10;
372 dstrect.h = item_size_ + 2;
373
374 tile.set_location(dstrect);
375 tile.set_tooltip_string(tooltip_text.str());
376 tile.set_item_id(item_id);
377 tile.set_selected(is_selected_item(item_id));
378 tile.set_dirty(true);
379 tile.hide(false);
380 tile.draw();
381
382 // Adjust location
383 y += item_space_;
384 }
385 }
386
action_pressed() const387 std::vector<std::string> location_palette::action_pressed() const
388 {
389 std::vector<std::string> res;
390 if (button_delete_ && button_delete_->pressed()) {
391 res.push_back("editor-remove-location");
392 }
393 return res;
394 }
395
~location_palette()396 location_palette::~location_palette()
397 {
398 }
399
400 // Sort numbers before all other strings.
loc_id_comp(const std::string & lhs,const std::string & rhs)401 static bool loc_id_comp(const std::string& lhs, const std::string& rhs) {
402 if(is_positive_integer(lhs)) {
403 if(is_positive_integer(rhs)) {
404 return std::stoi(lhs) < std::stoi(rhs);
405 } else {
406 return true;
407 }
408 }
409 if(is_positive_integer(rhs)) {
410 return false;
411 }
412 return lhs < rhs;
413 }
414
add_item(const std::string & id)415 void location_palette::add_item(const std::string& id)
416 {
417 int pos;
418 // Insert the new ID at the sorted location, unless it's already in the list
419 const auto itor = std::upper_bound(items_.begin(), items_.end(), id, loc_id_comp);
420 if(itor == items_.begin() || *(itor - 1) != id) {
421 pos = std::distance(items_.begin(), items_.insert(itor, id));
422 }
423 else {
424 pos = std::distance(items_.begin(), itor);
425 }
426 selected_item_ = id;
427 if(num_visible_items() == 0) {
428 items_start_ = 0;
429 } else {
430 items_start_ = std::max(pos - num_visible_items() + 1, items_start_);
431 items_start_ = std::min(pos, items_start_);
432 }
433 adjust_size(location());
434 }
435
436 } // end namespace editor
437