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