1 /*
2    Copyright (C) 2008 - 2018 by Mark de Wever <koraq@xs4all.nl>
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-lib"
16 
17 #include "gui/widgets/listbox.hpp"
18 
19 #include "gettext.hpp"
20 #include "gui/auxiliary/find_widget.hpp"
21 #include "gui/core/layout_exception.hpp"
22 #include "gui/core/log.hpp"
23 #include "gui/core/register_widget.hpp"
24 #include "gui/core/widget_definition.hpp"
25 #include "gui/core/window_builder.hpp"
26 #include "gui/core/window_builder/helper.hpp"
27 #include "gui/widgets/pane.hpp"
28 #include "gui/widgets/selectable_item.hpp"
29 #include "gui/widgets/settings.hpp"
30 #include "gui/widgets/toggle_button.hpp"
31 #include "gui/widgets/viewport.hpp"
32 #include "gui/widgets/widget_helpers.hpp"
33 #include "gui/widgets/window.hpp"
34 #include "sdl/rect.hpp"
35 #include "utils/functional.hpp"
36 
37 #include <boost/optional.hpp>
38 
39 #define LOG_SCOPE_HEADER get_control_type() + " [" + id() + "] " + __func__
40 #define LOG_HEADER LOG_SCOPE_HEADER + ':'
41 
42 namespace gui2
43 {
44 // ------------ WIDGET -----------{
45 
46 REGISTER_WIDGET(listbox)
REGISTER_WIDGET3(listbox_definition,horizontal_listbox,nullptr)47 REGISTER_WIDGET3(listbox_definition, horizontal_listbox, nullptr)
48 REGISTER_WIDGET3(listbox_definition, grid_listbox, nullptr)
49 
50 listbox::listbox(const implementation::builder_styled_widget& builder,
51 		const generator_base::placement placement,
52 		builder_grid_ptr list_builder,
53 		const bool has_minimum,
54 		const bool has_maximum,
55 		const bool select)
56 	: scrollbar_container(builder, type())
57 	, generator_(generator_base::build(has_minimum, has_maximum, placement, select))
58 	, is_horizontal_(placement == generator_base::horizontal_list)
59 	, list_builder_(list_builder)
60 	, need_layout_(false)
61 	, orders_()
62 	, callback_order_change_()
63 {
64 }
65 
add_row(const string_map & item,const int index)66 grid& listbox::add_row(const string_map& item, const int index)
67 {
68 	assert(generator_);
69 	grid& row = generator_->create_item(index, list_builder_, item, std::bind(&listbox::list_item_clicked, this, _1));
70 
71 	resize_content(row);
72 
73 	return row;
74 }
75 
add_row(const std::map<std::string,string_map> & data,const int index)76 grid& listbox::add_row(const std::map<std::string /* widget id */, string_map>& data, const int index)
77 {
78 	assert(generator_);
79 	grid& row = generator_->create_item(index, list_builder_, data, std::bind(&listbox::list_item_clicked, this, _1));
80 
81 	resize_content(row);
82 
83 	return row;
84 }
85 
remove_row(const unsigned row,unsigned count)86 void listbox::remove_row(const unsigned row, unsigned count)
87 {
88 	assert(generator_);
89 
90 	if(row >= get_item_count()) {
91 		return;
92 	}
93 
94 	if(!count || count + row > get_item_count()) {
95 		count = get_item_count() - row;
96 	}
97 
98 	int height_reduced = 0;
99 	int width_reduced = 0;
100 
101 	// TODO: Fix this for horizontal listboxes
102 	// Note the we have to use content_grid_ and cannot use "_list_grid" which is what generator_ uses.
103 	int row_pos_y = is_horizontal_ ? -1 : generator_->item(row).get_y() - content_grid_->get_y();
104 	int row_pos_x = is_horizontal_ ? -1 : 0;
105 
106 	for(; count; --count) {
107 		if(generator_->item(row).get_visible() != visibility::invisible) {
108 			if(is_horizontal_) {
109 				width_reduced += generator_->item(row).get_width();
110 			} else {
111 				height_reduced += generator_->item(row).get_height();
112 			}
113 		}
114 
115 		generator_->delete_item(row);
116 	}
117 
118 	if((height_reduced != 0 || width_reduced != 0) && get_item_count() != 0) {
119 		resize_content(-width_reduced, -height_reduced, row_pos_x, row_pos_y);
120 	} else {
121 		update_content_size();
122 	}
123 }
124 
clear()125 void listbox::clear()
126 {
127 	generator_->clear();
128 	update_content_size();
129 }
130 
get_item_count() const131 unsigned listbox::get_item_count() const
132 {
133 	assert(generator_);
134 	return generator_->get_item_count();
135 }
136 
set_row_active(const unsigned row,const bool active)137 void listbox::set_row_active(const unsigned row, const bool active)
138 {
139 	assert(generator_);
140 	generator_->item(row).set_active(active);
141 }
142 
set_row_shown(const unsigned row,const bool shown)143 void listbox::set_row_shown(const unsigned row, const bool shown)
144 {
145 	assert(generator_);
146 
147 	window* window = get_window();
148 	assert(window);
149 
150 	const int selected_row = get_selected_row();
151 
152 	bool resize_needed = false;
153 
154 	// Local scope for invalidate_layout_blocker
155 	{
156 		window::invalidate_layout_blocker invalidate_layout_blocker(*window);
157 
158 		generator_->set_item_shown(row, shown);
159 
160 		point best_size = generator_->calculate_best_size();
161 		generator_->place(generator_->get_origin(), {std::max(best_size.x, content_visible_area().w), best_size.y});
162 
163 		resize_needed = !content_resize_request();
164 	}
165 
166 	if(resize_needed) {
167 		window->invalidate_layout();
168 	} else {
169 		content_grid_->set_visible_rectangle(content_visible_area());
170 		set_is_dirty(true);
171 	}
172 
173 	if(selected_row != get_selected_row()) {
174 		fire(event::NOTIFY_MODIFIED, *this, nullptr);
175 	}
176 }
177 
set_row_shown(const boost::dynamic_bitset<> & shown)178 void listbox::set_row_shown(const boost::dynamic_bitset<>& shown)
179 {
180 	assert(generator_);
181 	assert(shown.size() == get_item_count());
182 
183 	if(generator_->get_items_shown() == shown) {
184 		LOG_GUI_G << LOG_HEADER << " returning early" << std::endl;
185 		return;
186 	}
187 
188 	window* window = get_window();
189 	assert(window);
190 
191 	const int selected_row = get_selected_row();
192 
193 	bool resize_needed = false;
194 
195 	// Local scope for invalidate_layout_blocker
196 	{
197 		window::invalidate_layout_blocker invalidate_layout_blocker(*window);
198 
199 		for(size_t i = 0; i < shown.size(); ++i) {
200 			generator_->set_item_shown(i, shown[i]);
201 		}
202 
203 		point best_size = generator_->calculate_best_size();
204 		generator_->place(generator_->get_origin(), {std::max(best_size.x, content_visible_area().w), best_size.y});
205 
206 		resize_needed = !content_resize_request();
207 	}
208 
209 	if(resize_needed) {
210 		window->invalidate_layout();
211 	} else {
212 		content_grid_->set_visible_rectangle(content_visible_area());
213 		set_is_dirty(true);
214 	}
215 
216 	if(selected_row != get_selected_row()) {
217 		fire(event::NOTIFY_MODIFIED, *this, nullptr);
218 	}
219 }
220 
get_rows_shown() const221 boost::dynamic_bitset<> listbox::get_rows_shown() const
222 {
223 	return generator_->get_items_shown();
224 }
225 
any_rows_shown() const226 bool listbox::any_rows_shown() const
227 {
228 	for(size_t i = 0; i < get_item_count(); i++) {
229 		if(generator_->get_item_shown(i)) {
230 			return true;
231 		}
232 	}
233 
234 	return false;
235 }
236 
get_row_grid(const unsigned row) const237 const grid* listbox::get_row_grid(const unsigned row) const
238 {
239 	assert(generator_);
240 	// rename this function and can we return a reference??
241 	return &generator_->item(row);
242 }
243 
get_row_grid(const unsigned row)244 grid* listbox::get_row_grid(const unsigned row)
245 {
246 	assert(generator_);
247 	return &generator_->item(row);
248 }
249 
select_row(const unsigned row,const bool select)250 bool listbox::select_row(const unsigned row, const bool select)
251 {
252 	assert(generator_);
253 
254 	unsigned int before = generator_->get_selected_item_count();
255 	generator_->select_item(row, select);
256 
257 	return before != generator_->get_selected_item_count();
258 }
259 
select_row_at(const unsigned row,const bool select)260 bool listbox::select_row_at(const unsigned row, const bool select)
261 {
262 	assert(generator_);
263 	return select_row(generator_->get_item_at_ordered(row), select);
264 }
265 
row_selected(const unsigned row)266 bool listbox::row_selected(const unsigned row)
267 {
268 	assert(generator_);
269 	return generator_->is_selected(row);
270 }
271 
get_selected_row() const272 int listbox::get_selected_row() const
273 {
274 	assert(generator_);
275 	return generator_->get_selected_item();
276 }
277 
list_item_clicked(widget & caller)278 void listbox::list_item_clicked(widget& caller)
279 {
280 	assert(generator_);
281 
282 	/** @todo Hack to capture the keyboard focus. */
283 	get_window()->keyboard_capture(this);
284 
285 	for(size_t i = 0; i < generator_->get_item_count(); ++i) {
286 		if(generator_->item(i).has_widget(caller)) {
287 			toggle_button* checkbox = dynamic_cast<toggle_button*>(&caller);
288 
289 			if(checkbox != nullptr) {
290 				generator_->select_item(i, checkbox->get_value_bool());
291 			} else {
292 				generator_->toggle_item(i);
293 			}
294 
295 			// TODO: enable this code once toggle_panel::set_value dispatches
296 			// NOTIFY_MODIFED events. See comment in said function for more details.
297 #if 0
298 			selectable_item& selectable = dynamic_cast<selectable_item&>(caller);
299 
300 			generator_->select_item(i, selectable.get_value_bool());
301 #endif
302 
303 			fire(event::NOTIFY_MODIFIED, *this, nullptr);
304 			break;
305 		}
306 	}
307 
308 	const int selected_item = generator_->get_selected_item();
309 	if(selected_item == -1) {
310 		return;
311 	}
312 
313 	const SDL_Rect& visible = content_visible_area();
314 	SDL_Rect rect = generator_->item(selected_item).get_rectangle();
315 
316 	if(sdl::rects_overlap(visible, rect)) {
317 		rect.x = visible.x;
318 		rect.w = visible.w;
319 
320 		show_content_rect(rect);
321 	}
322 }
323 
set_self_active(const bool)324 void listbox::set_self_active(const bool /*active*/)
325 {
326 	/* DO NOTHING */
327 }
328 
update_content_size()329 bool listbox::update_content_size()
330 {
331 	if(get_visible() == widget::visibility::invisible) {
332 		return true;
333 	}
334 
335 	if(get_size() == point()) {
336 		return false;
337 	}
338 
339 	if(content_resize_request(true)) {
340 		content_grid_->set_visible_rectangle(content_visible_area());
341 		set_is_dirty(true);
342 		return true;
343 	}
344 
345 	return false;
346 }
347 
place(const point & origin,const point & size)348 void listbox::place(const point& origin, const point& size)
349 {
350 	boost::optional<unsigned> vertical_scrollbar_position, horizontal_scrollbar_position;
351 
352 	// Check if this is the first time placing the list box
353 	if(get_origin() != point {-1, -1}) {
354 		vertical_scrollbar_position = get_vertical_scrollbar_item_position();
355 		horizontal_scrollbar_position = get_horizontal_scrollbar_item_position();
356 	}
357 
358 	// Inherited.
359 	scrollbar_container::place(origin, size);
360 
361 	const int selected_item = generator_->get_selected_item();
362 	if(vertical_scrollbar_position && horizontal_scrollbar_position) {
363 		LOG_GUI_L << LOG_HEADER << " restoring scroll position" << std::endl;
364 
365 		set_vertical_scrollbar_item_position(*vertical_scrollbar_position);
366 		set_horizontal_scrollbar_item_position(*horizontal_scrollbar_position);
367 	} else if(selected_item != -1) {
368 		LOG_GUI_L << LOG_HEADER << " making the initially selected item visible" << std::endl;
369 
370 		const SDL_Rect& visible = content_visible_area();
371 		SDL_Rect rect = generator_->item(selected_item).get_rectangle();
372 
373 		rect.x = visible.x;
374 		rect.w = visible.w;
375 
376 		show_content_rect(rect);
377 	}
378 }
379 
resize_content(const int width_modification,const int height_modification,const int width_modification_pos,const int height_modification_pos)380 void listbox::resize_content(const int width_modification,
381 		const int height_modification,
382 		const int width_modification_pos,
383 		const int height_modification_pos)
384 {
385 	DBG_GUI_L << LOG_HEADER << " current size " << content_grid()->get_size() << " width_modification "
386 			  << width_modification << " height_modification " << height_modification << ".\n";
387 
388 	if(content_resize_request(
389 		width_modification, height_modification, width_modification_pos, height_modification_pos))
390 	{
391 		// Calculate new size.
392 		point size = content_grid()->get_size();
393 		size.x += width_modification;
394 		size.y += height_modification;
395 
396 		// Set new size.
397 		content_grid()->set_size(size);
398 
399 		// Set status.
400 		need_layout_ = true;
401 
402 		// If the content grows assume it "overwrites" the old content.
403 		if(width_modification < 0 || height_modification < 0) {
404 			set_is_dirty(true);
405 		}
406 
407 		DBG_GUI_L << LOG_HEADER << " succeeded.\n";
408 	} else {
409 		DBG_GUI_L << LOG_HEADER << " failed.\n";
410 	}
411 }
412 
resize_content(const widget & row)413 void listbox::resize_content(const widget& row)
414 {
415 	if(row.get_visible() == visibility::invisible) {
416 		return;
417 	}
418 
419 	DBG_GUI_L << LOG_HEADER << " current size " << content_grid()->get_size() << " row size " << row.get_best_size()
420 			  << ".\n";
421 
422 	const point content = content_grid()->get_size();
423 	point size = row.get_best_size();
424 
425 	if(size.x < content.x) {
426 		size.x = 0;
427 	} else {
428 		size.x -= content.x;
429 	}
430 
431 	resize_content(size.x, size.y);
432 }
433 
layout_children()434 void listbox::layout_children()
435 {
436 	layout_children(false);
437 }
438 
child_populate_dirty_list(window & caller,const std::vector<widget * > & call_stack)439 void listbox::child_populate_dirty_list(window& caller, const std::vector<widget*>& call_stack)
440 {
441 	// Inherited.
442 	scrollbar_container::child_populate_dirty_list(caller, call_stack);
443 
444 	assert(generator_);
445 	std::vector<widget*> child_call_stack = call_stack;
446 	generator_->populate_dirty_list(caller, child_call_stack);
447 }
448 
calculate_best_size() const449 point listbox::calculate_best_size() const
450 {
451 	// Get the size from the base class, then add any extra space for the header and footer.
452 	point result = scrollbar_container::calculate_best_size();
453 
454 	if(const grid* header = find_widget<const grid>(&get_grid(), "_header_grid", false, false)) {
455 		result.y += header->get_best_size().y;
456 	}
457 
458 	if(const grid* footer = find_widget<const grid>(&get_grid(), "_footer_grid", false, false)) {
459 		result.y += footer->get_best_size().y;
460 	}
461 
462 	return result;
463 }
464 
update_visible_area_on_key_event(const KEY_SCROLL_DIRECTION direction)465 void listbox::update_visible_area_on_key_event(const KEY_SCROLL_DIRECTION direction)
466 {
467 	const SDL_Rect& visible = content_visible_area();
468 	SDL_Rect rect = generator_->item(generator_->get_selected_item()).get_rectangle();
469 
470 	// When scrolling make sure the new items are visible...
471 	if(direction == KEY_VERTICAL) {
472 		// ...but leave the horizontal scrollbar position.
473 		rect.x = visible.x;
474 		rect.w = visible.w;
475 	} else {
476 		// ...but leave the vertical scrollbar position.
477 		rect.y = visible.y;
478 		rect.h = visible.h;
479 	}
480 
481 	show_content_rect(rect);
482 
483 	fire(event::NOTIFY_MODIFIED, *this, nullptr);
484 }
485 
handle_key_up_arrow(SDL_Keymod modifier,bool & handled)486 void listbox::handle_key_up_arrow(SDL_Keymod modifier, bool& handled)
487 {
488 	assert(generator_);
489 
490 	generator_->handle_key_up_arrow(modifier, handled);
491 
492 	if(handled) {
493 		update_visible_area_on_key_event(KEY_VERTICAL);
494 	} else {
495 		// Inherited.
496 		scrollbar_container::handle_key_up_arrow(modifier, handled);
497 	}
498 }
499 
handle_key_down_arrow(SDL_Keymod modifier,bool & handled)500 void listbox::handle_key_down_arrow(SDL_Keymod modifier, bool& handled)
501 {
502 	assert(generator_);
503 
504 	generator_->handle_key_down_arrow(modifier, handled);
505 
506 	if(handled) {
507 		update_visible_area_on_key_event(KEY_VERTICAL);
508 	} else {
509 		// Inherited.
510 		scrollbar_container::handle_key_up_arrow(modifier, handled);
511 	}
512 }
513 
handle_key_left_arrow(SDL_Keymod modifier,bool & handled)514 void listbox::handle_key_left_arrow(SDL_Keymod modifier, bool& handled)
515 {
516 	assert(generator_);
517 
518 	generator_->handle_key_left_arrow(modifier, handled);
519 
520 	// Inherited.
521 	if(handled) {
522 		update_visible_area_on_key_event(KEY_HORIZONTAL);
523 	} else {
524 		scrollbar_container::handle_key_left_arrow(modifier, handled);
525 	}
526 }
527 
handle_key_right_arrow(SDL_Keymod modifier,bool & handled)528 void listbox::handle_key_right_arrow(SDL_Keymod modifier, bool& handled)
529 {
530 	assert(generator_);
531 
532 	generator_->handle_key_right_arrow(modifier, handled);
533 
534 	// Inherited.
535 	if(handled) {
536 		update_visible_area_on_key_event(KEY_HORIZONTAL);
537 	} else {
538 		scrollbar_container::handle_key_left_arrow(modifier, handled);
539 	}
540 }
541 
finalize(builder_grid_const_ptr header,builder_grid_const_ptr footer,const std::vector<std::map<std::string,string_map>> & list_data)542 void listbox::finalize(builder_grid_const_ptr header,
543 		builder_grid_const_ptr footer,
544 		const std::vector<std::map<std::string, string_map>>& list_data)
545 {
546 	// "Inherited."
547 	scrollbar_container::finalize_setup();
548 
549 	assert(generator_);
550 
551 	if(header) {
552 		swap_grid(&get_grid(), content_grid(), header->build(), "_header_grid");
553 	}
554 
555 	grid& p = find_widget<grid>(this, "_header_grid", false);
556 
557 	for(unsigned i = 0, max = std::max(p.get_cols(), p.get_rows()); i < max; ++i) {
558 		//
559 		// TODO: I had to change this to case to a toggle_button in order to use a signal handler.
560 		// Should probably look into a way to make it more general like it was before (used to be
561 		// cast to selectable_item).
562 		//
563 		// - vultraz, 2017-08-23
564 		//
565 		if(toggle_button* selectable = find_widget<toggle_button>(&p, "sort_" + std::to_string(i), false, false)) {
566 			// Register callback to sort the list.
567 			connect_signal_notify_modified(*selectable, std::bind(&listbox::order_by_column, this, i, _1));
568 
569 			if(orders_.size() < max) {
570 				orders_.resize(max);
571 			}
572 
573 			orders_[i].first = selectable;
574 		}
575 	}
576 
577 	if(footer) {
578 		swap_grid(&get_grid(), content_grid(), footer->build(), "_footer_grid");
579 	}
580 
581 	generator_->create_items(-1, list_builder_, list_data, std::bind(&listbox::list_item_clicked, this, _1));
582 	swap_grid(nullptr, content_grid(), generator_, "_list_grid");
583 }
584 
order_by_column(unsigned column,widget & widget)585 void listbox::order_by_column(unsigned column, widget& widget)
586 {
587 	selectable_item& selectable = dynamic_cast<selectable_item&>(widget);
588 	if(column >= orders_.size()) {
589 		return;
590 	}
591 
592 	for(auto& pair : orders_) {
593 		if(pair.first != nullptr && pair.first != &selectable) {
594 			pair.first->set_value(SORT_NONE);
595 		}
596 	}
597 
598 	SORT_ORDER order = static_cast<SORT_ORDER>(selectable.get_value());
599 
600 	if(static_cast<unsigned int>(order) > orders_[column].second.size()) {
601 		return;
602 	}
603 
604 	if(order == SORT_NONE) {
605 		order_by(std::less<unsigned>());
606 	} else {
607 		order_by(orders_[column].second[order - 1]);
608 	}
609 
610 	if(callback_order_change_ != nullptr) {
611 		callback_order_change_(column, order);
612 	}
613 }
614 
order_by(const generator_base::order_func & func)615 void listbox::order_by(const generator_base::order_func& func)
616 {
617 	generator_->set_order(func);
618 
619 	set_is_dirty(true);
620 	need_layout_ = true;
621 }
622 
set_column_order(unsigned col,const generator_sort_array & func)623 void listbox::set_column_order(unsigned col, const generator_sort_array& func)
624 {
625 	if(col >= orders_.size()) {
626 		orders_.resize(col + 1);
627 	}
628 
629 	orders_[col].second = func;
630 }
631 
register_translatable_sorting_option(const int col,translatable_sorter_func_t f)632 void listbox::register_translatable_sorting_option(const int col, translatable_sorter_func_t f)
633 {
634 	set_column_order(col, {{
635 		[f](int lhs, int rhs) { return translation::icompare(f(lhs), f(rhs)) < 0; },
636 		[f](int lhs, int rhs) { return translation::icompare(f(lhs), f(rhs)) > 0; }
637 	}});
638 }
639 
set_active_sorting_option(const order_pair & sort_by,const bool select_first)640 void listbox::set_active_sorting_option(const order_pair& sort_by, const bool select_first)
641 {
642 	// TODO: should this be moved to a public header_grid() getter function?
643 	grid& header_grid = find_widget<grid>(this, "_header_grid", false);
644 
645 	selectable_item& w = find_widget<selectable_item>(&header_grid, "sort_" + std::to_string(sort_by.first), false);
646 
647 	// Set the sorting toggle widgets' value (in this case, its state) to the given sorting
648 	// order. This is necessary since the widget's value is used to determine the order in
649 	// @ref order_by_column in lieu of a direction being passed directly.
650 	w.set_value(static_cast<int>(sort_by.second));
651 
652 	order_by_column(sort_by.first, dynamic_cast<widget&>(w));
653 
654 	if(select_first && generator_->get_item_count() > 0) {
655 		select_row_at(0);
656 	}
657 }
658 
get_active_sorting_option()659 const listbox::order_pair listbox::get_active_sorting_option()
660 {
661 	for(unsigned int column = 0; column < orders_.size(); ++column) {
662 		selectable_item* w = orders_[column].first;
663 
664 		if(w && w->get_value() != SORT_NONE) {
665 			return std::make_pair(column, static_cast<SORT_ORDER>(w->get_value()));
666 		}
667 	}
668 
669 	return std::make_pair(-1, SORT_NONE);
670 }
671 
mark_as_unsorted()672 void listbox::mark_as_unsorted()
673 {
674 	for(auto& pair : orders_) {
675 		if(pair.first != nullptr) {
676 			pair.first->set_value(SORT_NONE);
677 		}
678 	}
679 }
680 
set_content_size(const point & origin,const point & size)681 void listbox::set_content_size(const point& origin, const point& size)
682 {
683 	/** @todo This function needs more testing. */
684 	assert(content_grid());
685 
686 	const int best_height = content_grid()->get_best_size().y;
687 	const point s(size.x, size.y < best_height ? size.y : best_height);
688 
689 	content_grid()->place(origin, s);
690 }
691 
layout_children(const bool force)692 void listbox::layout_children(const bool force)
693 {
694 	assert(content_grid());
695 
696 	if(need_layout_ || force) {
697 		content_grid()->place(content_grid()->get_origin(), content_grid()->get_size());
698 
699 		const SDL_Rect& visible = content_visible_area_;
700 
701 		content_grid()->set_visible_rectangle(visible);
702 
703 		need_layout_ = false;
704 		set_is_dirty(true);
705 	}
706 }
707 
708 // }---------- DEFINITION ---------{
709 
listbox_definition(const config & cfg)710 listbox_definition::listbox_definition(const config& cfg)
711 	: styled_widget_definition(cfg)
712 {
713 	DBG_GUI_P << "Parsing listbox " << id << '\n';
714 
715 	load_resolutions<resolution>(cfg);
716 }
717 
718 /*WIKI
719  * @page = GUIWidgetDefinitionWML
720  * @order = 1_listbox
721  * @begin{parent}{name="gui/"}
722  * @begin{tag}{name="listbox_definition"}{min=0}{max=-1}{super="generic/widget_definition"}
723  * == Listbox ==
724  *
725  * @macro = listbox_description
726  *
727  * The definition of a listbox contains the definition of its scrollbar.
728  *
729  * The resolution for a listbox also contains the following keys:
730  * @begin{tag}{name="resolution"}{min=0}{max=-1}{super=generic/widget_definition/resolution}
731  * @begin{table}{config}
732  *     scrollbar & section & &         A grid containing the widgets for the
733  *                                     scrollbar. The scrollbar has some special
734  *                                     widgets so it can make default behavior
735  *                                     for certain widgets. $
736  * @end{table}
737  * @begin{table}{dialog_widgets}
738  *     _begin & & clickable & o &      Moves the position to the beginning
739  *                                     of the list. $
740  *     _line_up & & clickable & o &    Move the position one item up. (NOTE
741  *                                     if too many items to move per item it
742  *                                     might be more items.) $
743  *     _half_page_up & & clickable & o &
744  *                                     Move the position half the number of the
745  *                                     visible items up. (See note at
746  *                                     _line_up.) $
747  *     _page_up & & clickable & o &    Move the position the number of
748  *                                     visible items up. (See note at
749  *                                     _line_up.) $
750  *
751  *     _end & & clickable & o &        Moves the position to the end of the
752  *                                     list. $
753  *     _line_down & & clickable & o &  Move the position one item down.(See
754  *                                     note at _line_up.) $
755  *     _half_page_down & & clickable & o &
756  *                                     Move the position half the number of the
757  *                                     visible items down. (See note at
758  *                                     _line_up.) $
759  *     _page_down & & clickable & o &  Move the position the number of
760  *                                     visible items down. (See note at
761  *                                     _line_up.) $
762  *
763  *     _scrollbar & & vertical_scrollbar & m &
764  *                                     This is the scrollbar so the user can
765  *                                     scroll through the list. $
766  * @end{table}
767  * A clickable is one of:
768  * * button
769  * * repeating_button
770  * @{allow}{link}{name="gui/window/resolution/grid/row/column/button"}
771  * @{allow}{link}{name="gui/window/resolution/grid/row/column/repeating_button"}
772  * The following states exist:
773  * * state_enabled, the listbox is enabled.
774  * * state_disabled, the listbox is disabled.
775  * @begin{tag}{name="state_enabled"}{min=0}{max=1}{super="generic/state"}
776  * @end{tag}{name="state_enabled"}
777  * @begin{tag}{name="state_disabled"}{min=0}{max=1}{super="generic/state"}
778  * @end{tag}{name="state_disabled"}
779  * @allow{link}{name="gui/window/resolution/grid"}
780  * @end{tag}{name="resolution"}
781  * @end{tag}{name="listbox_definition"}
782  * @end{parent}{name="gui/"}
783  */
784 
785 /*WIKI
786  * @page = GUIWidgetDefinitionWML
787  * @order = 1_horizonal_listbox
788  *
789  * == Horizontal listbox ==
790  * @begin{parent}{name="gui/"}
791  * @begin{tag}{name="horizontal_listbox_definition"}{min=0}{max=-1}{super="gui/listbox_definition"}
792  * @end{tag}{name="horizontal_listbox_definition"}
793  * @end{parent}{name="gui/"}
794  * @macro = horizontal_listbox_description
795  * The definition of a horizontal listbox is the same as for a normal listbox.
796  */
resolution(const config & cfg)797 listbox_definition::resolution::resolution(const config& cfg)
798 	: resolution_definition(cfg)
799 	, grid(nullptr)
800 {
801 	// Note the order should be the same as the enum state_t in listbox.hpp.
802 	state.emplace_back(cfg.child("state_enabled"));
803 	state.emplace_back(cfg.child("state_disabled"));
804 
805 	const config& child = cfg.child("grid");
806 	VALIDATE(child, _("No grid defined."));
807 
808 	grid = std::make_shared<builder_grid>(child);
809 }
810 
811 // }---------- BUILDER -----------{
812 
813 /*WIKI_MACRO
814  * @begin{macro}{listbox_description}
815  *
816  *        A listbox is a styled_widget that holds several items of the same type.
817  *        Normally the items in a listbox are ordered in rows, this version
818  *        might allow more options for ordering the items in the future.
819  * @end{macro}
820  */
821 
822 /*WIKI
823  * @page = GUIWidgetInstanceWML
824  * @order = 2_listbox
825  *
826  * == Listbox ==
827  * @begin{parent}{name="gui/window/resolution/grid/row/column/"}
828  * @begin{tag}{name="listbox"}{min=0}{max=-1}{super="generic/widget_instance"}
829  * @macro = listbox_description
830  *
831  * List with the listbox specific variables:
832  * @begin{table}{config}
833  *     vertical_scrollbar_mode & scrollbar_mode & initial_auto &
834  *                                     Determines whether or not to show the
835  *                                     scrollbar. $
836  *     horizontal_scrollbar_mode & scrollbar_mode & initial_auto &
837  *                                     Determines whether or not to show the
838  *                                     scrollbar. $
839  *
840  *     header & grid & [] &            Defines the grid for the optional
841  *                                     header. (This grid will automatically
842  *                                     get the id _header_grid.) $
843  *     footer & grid & [] &            Defines the grid for the optional
844  *                                     footer. (This grid will automatically
845  *                                     get the id _footer_grid.) $
846  *
847  *     list_definition & section & &   This defines how a listbox item
848  *                                     looks. It must contain the grid
849  *                                     definition for 1 row of the list. $
850  *
851  *     list_data & section & [] &      A grid alike section which stores the
852  *                                     initial data for the listbox. Every row
853  *                                     must have the same number of columns as
854  *                                     the 'list_definition'. $
855  *
856  *     has_minimum & bool & true &     If false, less than one row can be selected. $
857  *
858  *     has_maximum & bool & true &     If false, more than one row can be selected. $
859  *
860  * @end{table}
861  * @begin{tag}{name="header"}{min=0}{max=1}{super="gui/window/resolution/grid"}
862  * @end{tag}{name="header"}
863  * @begin{tag}{name="footer"}{min=0}{max=1}{super="gui/window/resolution/grid"}
864  * @end{tag}{name="footer"}
865  * @begin{tag}{name="list_definition"}{min=0}{max=1}
866  * @begin{tag}{name="row"}{min=1}{max=1}{super="generic/listbox_grid/row"}
867  * @end{tag}{name="row"}
868  * @end{tag}{name="list_definition"}x
869  * @begin{tag}{name="list_data"}{min=0}{max=1}{super="generic/listbox_grid"}
870  * @end{tag}{name="list_data"}
871  *
872  * In order to force widgets to be the same size inside a listbox, the widgets
873  * need to be inside a linked_group.
874  *
875  * Inside the list section there are only the following widgets allowed
876  * * grid (to nest)
877  * * selectable widgets which are
878  * ** toggle_button
879  * ** toggle_panel
880  * @end{tag}{name="listbox"}
881  *
882  * @end{parent}{name="gui/window/resolution/grid/row/column/"}
883  */
884 
885 /*WIKI
886  * @begin{parent}{name="generic/"}
887  * @begin{tag}{name="listbox_grid"}{min="0"}{max="-1"}
888  * @begin{tag}{name="row"}{min="0"}{max="-1"}
889  * @begin{table}{config}
890  *     grow_factor & unsigned & 0 &      The grow factor for a row. $
891  * @end{table}
892  * @begin{tag}{name="column"}{min="0"}{max="-1"}{super="gui/window/resolution/grid/row/column"}
893  * @begin{table}{config}
894  *     label & t_string & "" &  $
895  *     tooltip & t_string & "" &  $
896  *     icon & t_string & "" &  $
897  * @end{table}
898  * @allow{link}{name="gui/window/resolution/grid/row/column/toggle_button"}
899  * @allow{link}{name="gui/window/resolution/grid/row/column/toggle_panel"}
900  * @end{tag}{name="column"}
901  * @end{tag}{name="row"}
902  * @end{tag}{name="listbox_grid"}
903  * @end{parent}{name="generic/"}
904  */
905 
906 namespace implementation
907 {
parse_list_data(const config & data,const unsigned int req_cols)908 static std::vector<std::map<std::string, string_map>> parse_list_data(const config& data, const unsigned int req_cols)
909 {
910 	std::vector<std::map<std::string, string_map>> list_data;
911 
912 	for(const auto& row : data.child_range("row")) {
913 		auto cols = row.child_range("column");
914 
915 		VALIDATE(static_cast<unsigned>(cols.size()) == req_cols,
916 			_("'list_data' must have the same number of columns as the 'list_definition'.")
917 		);
918 
919 		for(const auto& c : cols) {
920 			list_data.emplace_back();
921 
922 			for(const auto& i : c.attribute_range()) {
923 				list_data.back()[""][i.first] = i.second;
924 			}
925 
926 			for(const auto& w : c.child_range("widget")) {
927 				VALIDATE(w.has_attribute("id"), missing_mandatory_wml_key("[list_data][row][column][widget]", "id"));
928 
929 				for(const auto& i : w.attribute_range()) {
930 					list_data.back()[w["id"]][i.first] = i.second;
931 				}
932 			}
933 		}
934 	}
935 
936 	return list_data;
937 }
938 
builder_listbox(const config & cfg)939 builder_listbox::builder_listbox(const config& cfg)
940 	: builder_styled_widget(cfg)
941 	, vertical_scrollbar_mode(get_scrollbar_mode(cfg["vertical_scrollbar_mode"]))
942 	, horizontal_scrollbar_mode(get_scrollbar_mode(cfg["horizontal_scrollbar_mode"]))
943 	, header(nullptr)
944 	, footer(nullptr)
945 	, list_builder(nullptr)
946 	, list_data()
947 	, has_minimum_(cfg["has_minimum"].to_bool(true))
948 	, has_maximum_(cfg["has_maximum"].to_bool(true))
949 {
950 	if(const config& h = cfg.child("header")) {
951 		header = std::make_shared<builder_grid>(h);
952 	}
953 
954 	if(const config& f = cfg.child("footer")) {
955 		footer = std::make_shared<builder_grid>(f);
956 	}
957 
958 	const config& l = cfg.child("list_definition");
959 
960 	VALIDATE(l, _("No list defined."));
961 
962 	list_builder = std::make_shared<builder_grid>(l);
963 	assert(list_builder);
964 
965 	VALIDATE(list_builder->rows == 1, _("A 'list_definition' should contain one row."));
966 
967 	if(cfg.has_child("list_data")) {
968 		list_data = parse_list_data(cfg.child("list_data"), list_builder->cols);
969 	}
970 }
971 
build() const972 widget* builder_listbox::build() const
973 {
974 
975 	listbox* widget = new listbox(*this, generator_base::vertical_list, list_builder, has_minimum_, has_maximum_);
976 
977 	widget->set_vertical_scrollbar_mode(vertical_scrollbar_mode);
978 	widget->set_horizontal_scrollbar_mode(horizontal_scrollbar_mode);
979 
980 	DBG_GUI_G << "Window builder: placed listbox '" << id << "' with definition '" << definition << "'.\n";
981 
982 	const auto conf = widget->cast_config_to<listbox_definition>();
983 	assert(conf);
984 
985 	widget->init_grid(conf->grid);
986 
987 	widget->finalize(header, footer, list_data);
988 
989 	return widget;
990 }
991 
992 /*WIKI_MACRO
993  * @begin{macro}{horizontal_listbox_description}
994  *
995  *        A horizontal listbox is a styled_widget that holds several items of the
996  *        same type.  Normally the items in a listbox are ordered in rows,
997  *        this version orders them in columns instead.
998  * @end{macro}
999  */
1000 
1001 /*WIKI
1002  * @page = GUIWidgetInstanceWML
1003  * @order = 2_horizontal_listbox
1004  * @begin{parent}{name="gui/window/resolution/grid/row/column/"}
1005  * @begin{tag}{name="horizontal_listbox"}{min="0"}{max="-1"}{super="generic/widget_instance"}
1006  * == Horizontal listbox ==
1007  *
1008  * @macro = horizontal_listbox_description
1009  *
1010  * List with the horizontal listbox specific variables:
1011  * @begin{table}{config}
1012  *     vertical_scrollbar_mode & scrollbar_mode & initial_auto &
1013  *                                     Determines whether or not to show the
1014  *                                     scrollbar. $
1015  *     horizontal_scrollbar_mode & scrollbar_mode & initial_auto &
1016  *                                     Determines whether or not to show the
1017  *                                     scrollbar. $
1018  *
1019  *     list_definition & section & &   This defines how a listbox item
1020  *                                     looks. It must contain the grid
1021  *                                     definition for 1 column of the list. $
1022  *
1023  *     list_data & section & [] &      A grid alike section which stores the
1024  *                                     initial data for the listbox. Every row
1025  *                                     must have the same number of columns as
1026  *                                     the 'list_definition'. $
1027  *
1028  *     has_minimum & bool & true &     If false, less than one row can be selected. $
1029  *
1030  *     has_maximum & bool & true &     If false, more than one row can be selected. $
1031  *
1032  * @end{table}
1033  * @begin{tag}{name="header"}{min=0}{max=1}{super="gui/window/resolution/grid"}
1034  * @end{tag}{name="header"}
1035  * @begin{tag}{name="footer"}{min=0}{max=1}{super="gui/window/resolution/grid"}
1036  * @end{tag}{name="footer"}
1037  * @begin{tag}{name="list_definition"}{min=0}{max=1}
1038  * @begin{tag}{name="row"}{min=1}{max=1}{super="generic/listbox_grid/row"}
1039  * @end{tag}{name="row"}
1040  * @end{tag}{name="list_definition"}
1041  * @begin{tag}{name="list_data"}{min=0}{max=1}{super="generic/listbox_grid"}
1042  * @end{tag}{name="list_data"}
1043  * In order to force widgets to be the same size inside a horizontal listbox,
1044  * the widgets need to be inside a linked_group.
1045  *
1046  * Inside the list section there are only the following widgets allowed
1047  * * grid (to nest)
1048  * * selectable widgets which are
1049  * ** toggle_button
1050  * ** toggle_panel
1051  * @end{tag}{name="horizontal_listbox"}
1052  * @end{parent}{name="gui/window/resolution/grid/row/column/"}
1053  */
1054 
builder_horizontal_listbox(const config & cfg)1055 builder_horizontal_listbox::builder_horizontal_listbox(const config& cfg)
1056 	: builder_styled_widget(cfg)
1057 	, vertical_scrollbar_mode(get_scrollbar_mode(cfg["vertical_scrollbar_mode"]))
1058 	, horizontal_scrollbar_mode(get_scrollbar_mode(cfg["horizontal_scrollbar_mode"]))
1059 	, list_builder(nullptr)
1060 	, list_data()
1061 	, has_minimum_(cfg["has_minimum"].to_bool(true))
1062 	, has_maximum_(cfg["has_maximum"].to_bool(true))
1063 {
1064 	const config& l = cfg.child("list_definition");
1065 
1066 	VALIDATE(l, _("No list defined."));
1067 
1068 	list_builder = std::make_shared<builder_grid>(l);
1069 	assert(list_builder);
1070 
1071 	VALIDATE(list_builder->rows == 1, _("A 'list_definition' should contain one row."));
1072 
1073 	if(cfg.has_child("list_data")) {
1074 		list_data = parse_list_data(cfg.child("list_data"), list_builder->cols);
1075 	}
1076 }
1077 
build() const1078 widget* builder_horizontal_listbox::build() const
1079 {
1080 
1081 	listbox* widget = new listbox(*this, generator_base::horizontal_list, list_builder, has_minimum_, has_maximum_);
1082 
1083 	widget->set_vertical_scrollbar_mode(vertical_scrollbar_mode);
1084 	widget->set_horizontal_scrollbar_mode(horizontal_scrollbar_mode);
1085 
1086 	DBG_GUI_G << "Window builder: placed listbox '" << id << "' with definition '" << definition << "'.\n";
1087 
1088 	const auto conf = widget->cast_config_to<listbox_definition>();
1089 	assert(conf);
1090 
1091 	widget->init_grid(conf->grid);
1092 
1093 	widget->finalize(nullptr, nullptr, list_data);
1094 
1095 	return widget;
1096 }
1097 
1098 /*WIKI_MACRO
1099  * @begin{macro}{grid_listbox_description}
1100  *
1101  *        A grid listbox is a styled_widget that holds several items of the
1102  *        same type.  Normally the items in a listbox are ordered in rows,
1103  *        this version orders them in a grid instead.
1104  * @end{macro}
1105  */
1106 
1107 /*WIKI
1108  * @page = GUIWidgetInstanceWML
1109  * @order = 2_grid_listbox
1110  * @begin{parent}{name="gui/window/resolution/grid/row/column/"}
1111  * @begin{tag}{name="grid_listbox"}{min="0"}{max="-1"}{super="generic/widget_instance"}
1112  * == Horizontal listbox ==
1113  *
1114  * @macro = grid_listbox_description
1115  *
1116  * List with the grid listbox specific variables:
1117  * @begin{table}{config}
1118  *     vertical_scrollbar_mode & scrollbar_mode & initial_auto &
1119  *                                     Determines whether or not to show the
1120  *                                     scrollbar. $
1121  *     horizontal_scrollbar_mode & scrollbar_mode & initial_auto &
1122  *                                     Determines whether or not to show the
1123  *                                     scrollbar. $
1124  *
1125  *     list_definition & section & &   This defines how a listbox item
1126  *                                     looks. It must contain the grid
1127  *                                     definition for 1 column of the list. $
1128  *
1129  *     list_data & section & [] &      A grid alike section which stores the
1130  *                                     initial data for the listbox. Every row
1131  *                                     must have the same number of columns as
1132  *                                     the 'list_definition'. $
1133  *
1134  *     has_minimum & bool & true &     If false, less than one cell can be selected. $
1135  *
1136  *     has_maximum & bool & true &     If false, more than one cell can be selected. $
1137  *
1138  * @end{table}
1139  * @begin{tag}{name="header"}{min=0}{max=1}{super="gui/window/resolution/grid"}
1140  * @end{tag}{name="header"}
1141  * @begin{tag}{name="footer"}{min=0}{max=1}{super="gui/window/resolution/grid"}
1142  * @end{tag}{name="footer"}
1143  * @begin{tag}{name="list_definition"}{min=0}{max=1}
1144  * @begin{tag}{name="row"}{min=1}{max=1}{super="generic/listbox_grid/row"}
1145  * @end{tag}{name="row"}
1146  * @end{tag}{name="list_definition"}
1147  * @begin{tag}{name="list_data"}{min=0}{max=1}{super="generic/listbox_grid"}
1148  * @end{tag}{name="list_data"}
1149  * In order to force widgets to be the same size inside a grid listbox,
1150  * the widgets need to be inside a linked_group.
1151  *
1152  * Inside the list section there are only the following widgets allowed
1153  * * grid (to nest)
1154  * * selectable widgets which are
1155  * ** toggle_button
1156  * ** toggle_panel
1157  * @end{tag}{name="grid_listbox"}
1158  * @end{parent}{name="gui/window/resolution/grid/row/column/"}
1159  */
1160 
builder_grid_listbox(const config & cfg)1161 builder_grid_listbox::builder_grid_listbox(const config& cfg)
1162 	: builder_styled_widget(cfg)
1163 	, vertical_scrollbar_mode(get_scrollbar_mode(cfg["vertical_scrollbar_mode"]))
1164 	, horizontal_scrollbar_mode(get_scrollbar_mode(cfg["horizontal_scrollbar_mode"]))
1165 	, list_builder(nullptr)
1166 	, list_data()
1167 	, has_minimum_(cfg["has_minimum"].to_bool(true))
1168 	, has_maximum_(cfg["has_maximum"].to_bool(true))
1169 {
1170 	const config& l = cfg.child("list_definition");
1171 
1172 	VALIDATE(l, _("No list defined."));
1173 
1174 	list_builder = std::make_shared<builder_grid>(l);
1175 	assert(list_builder);
1176 
1177 	VALIDATE(list_builder->rows == 1, _("A 'list_definition' should contain one row."));
1178 
1179 	if(cfg.has_child("list_data")) {
1180 		list_data = parse_list_data(cfg.child("list_data"), list_builder->cols);
1181 	}
1182 }
1183 
build() const1184 widget* builder_grid_listbox::build() const
1185 {
1186 	listbox* widget = new listbox(*this, generator_base::table, list_builder, has_minimum_, has_maximum_);
1187 
1188 	widget->set_vertical_scrollbar_mode(vertical_scrollbar_mode);
1189 	widget->set_horizontal_scrollbar_mode(horizontal_scrollbar_mode);
1190 
1191 	DBG_GUI_G << "Window builder: placed listbox '" << id << "' with definition '" << definition << "'.\n";
1192 
1193 	const auto conf = widget->cast_config_to<listbox_definition>();
1194 	assert(conf);
1195 
1196 	widget->init_grid(conf->grid);
1197 
1198 	widget->finalize(nullptr, nullptr, list_data);
1199 
1200 	return widget;
1201 }
1202 
1203 } // namespace implementation
1204 
1205 // }------------ END --------------
1206 
1207 } // namespace gui2
1208