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