sortable listboxes for gui2

Fixes http://gna.org/bugs/index.php?23751

Gui2 listboxes can now be sorted. As an example how to use it i changed the
gui2 loadgame dialog to use this new feature.

To make your listbox sortable you have to add togglebuttons/panels with the
id sort_n where n is a number in [0, numer_of_columns) to the [header] of
the listbox, then the listbox will be sorted when a user clicks on that
togglebutton/panel. Also you have to register the comparision functions for
each column with listbox::set_column_order.

Alternatively you can sort the listbox manually by calling listbox::order_by

This feautre still suffers from http://gna.org/bugs/?15763 . But for small
listboxes without scrollbars this is already a good feature.
This commit is contained in:
gfgtdf 2015-08-06 14:52:18 +02:00
parent 1359e6a885
commit a4806343f5
9 changed files with 437 additions and 45 deletions

View file

@ -0,0 +1,190 @@
#textdomain wesnoth-lib
###
### Definition of the default toggle button.
### Since for this class 'default' is a bit hard we now use the checkbox as default.
###
#define _GUI_TEXT FONT_SIZE FONT_COLOR
[text]
x = 0
y = {GUI__TEXT_VERTICALLY_CENTRED}
w = "(width)"
h = "(text_height)"
font_size = {FONT_SIZE}
color = {FONT_COLOR}
text = "(text)"
[/text]
#enddef
#define _GUI_RESOLUTION RESOLUTION WIDTH HEIGHT IMAGE_Y FONT_SIZE
[resolution]
{RESOLUTION}
min_width = {WIDTH}
min_height = {HEIGHT}
default_width = {WIDTH}
default_height = {HEIGHT}
max_width = 0
max_height = {HEIGHT}
text_extra_width = 0
text_font_size = {FONT_SIZE}
[state]
[enabled]
[draw]
{_GUI_TEXT ({FONT_SIZE}) ({GUI__FONT_COLOR_ENABLED__TITLE}) }
[/draw]
[/enabled]
[disabled]
[draw]
{_GUI_TEXT ({FONT_SIZE}) ({GUI__FONT_COLOR_DISABLED__TITLE}) }
[/draw]
[/disabled]
[focussed]
[draw]
{_GUI_TEXT ({FONT_SIZE}) ({GUI__FONT_COLOR_ENABLED__TITLE}) }
[/draw]
[/focussed]
[/state]
###
### Down
###
[state]
[enabled]
[draw]
{_GUI_TEXT ({FONT_SIZE}) ({GUI__FONT_COLOR_ENABLED__TITLE}) }
[image]
x = "(width - 10)"
y = {IMAGE_Y}
name = "buttons/sliders/slider_arrow_blue.png~ROTATE(180)"
[/image]
[/draw]
[/enabled]
[disabled]
[draw]
{_GUI_TEXT ({FONT_SIZE}) ({GUI__FONT_COLOR_DISABLED__TITLE}) }
[image]
x = "(width - 10)"
y = {IMAGE_Y}
name = "buttons/sliders/slider_arrow_blue.png~ROTATE(180)~GS()"
[/image]
[/draw]
[/disabled]
[focussed]
[draw]
{_GUI_TEXT ({FONT_SIZE}) ({GUI__FONT_COLOR_ENABLED__TITLE}) }
[image]
x = "(width - 10)"
y = {IMAGE_Y}
name = "buttons/sliders/slider_arrow_blue.png~ROTATE(180)"
[/image]
[/draw]
[/focussed]
[/state]
###
### Up
###
[state]
[enabled]
[draw]
{_GUI_TEXT ({FONT_SIZE}) ({GUI__FONT_COLOR_ENABLED__TITLE}) }
[image]
x = "(width - 10)"
y = {IMAGE_Y}
name = "buttons/sliders/slider_arrow_blue.png"
[/image]
[/draw]
[/enabled]
[disabled]
[draw]
{_GUI_TEXT ({FONT_SIZE}) ({GUI__FONT_COLOR_DISABLED__TITLE}) }
[image]
x = "(width - 10)"
y = {IMAGE_Y}
name = "buttons/sliders/slider_arrow_blue.png~GS()"
[/image]
[/draw]
[/disabled]
[focussed]
[draw]
{_GUI_TEXT ({FONT_SIZE}) ({GUI__FONT_COLOR_ENABLED__TITLE}) }
[image]
x = "(width - 10)"
y = {IMAGE_Y}
name = "buttons/sliders/slider_arrow_blue.png"
[/image]
[/draw]
[/focussed]
[/state]
[/resolution]
#enddef
[toggle_button_definition]
id = "listbox_header"
description = "Checkbox."
# Tiny gui sizes haven't been tested yet so might need some tuning.
{_GUI_RESOLUTION ({GUI_TINY__RESOLUTION}) 20 11 0 ({GUI_TINY__FONT_SIZE__SMALL}) }
{_GUI_RESOLUTION () 30 18 5 ({GUI_NORMAL__FONT_SIZE__SMALL}) }
[/toggle_button_definition]
#undef _GUI_TEXT
#undef _GUI_RESOLUTION

View file

@ -256,15 +256,13 @@
horizontal_grow = "true"
border = "all"
border_size = 5
[label]
id = "filename"
definition = "default"
[toggle_button]
id = "sort_0"
definition = "listbox_header"
linked_group = "filename"
label = _ "Name"
[/label]
[/toggle_button]
[/column]
[column]
@ -272,15 +270,13 @@
horizontal_grow = "true"
border = "all"
border_size = 5
[label]
id = "date"
definition = "default"
[toggle_button]
id = "sort_1"
definition = "listbox_header"
linked_group = "date"
label = _ "Date"
[/label]
[/toggle_button]
[/column]
[/row]

View file

@ -147,6 +147,26 @@ void tgame_load::pre_show(CVideo& /*video*/, twindow& window)
display_savegame(window);
}
bool tgame_load::compare_name(unsigned i1, unsigned i2) const
{
return games_[i1].name() < games_[i2].name();
}
bool tgame_load::compare_date(unsigned i1, unsigned i2) const
{
return games_[i1].modified() < games_[i2].modified();
}
bool tgame_load::compare_name_rev(unsigned i1, unsigned i2) const
{
return games_[i1].name() > games_[i2].name();
}
bool tgame_load::compare_date_rev(unsigned i1, unsigned i2) const
{
return games_[i1].modified() > games_[i2].modified();
}
void tgame_load::fill_game_list(twindow& window,
std::vector<savegame::save_info>& games)
{
@ -166,6 +186,13 @@ void tgame_load::fill_game_list(twindow& window,
list.add_row(data);
}
std::vector<tgenerator_::torder_func> order_funcs(2);
order_funcs[0] = boost::bind(&tgame_load::compare_name, this, _1, _2);
order_funcs[1] = boost::bind(&tgame_load::compare_name_rev, this, _1, _2);
list.set_column_order(0, order_funcs);
order_funcs[0] = boost::bind(&tgame_load::compare_date, this, _1, _2);
order_funcs[1] = boost::bind(&tgame_load::compare_date_rev, this, _1, _2);
list.set_column_order(1, order_funcs);
}
void tgame_load::list_item_clicked(twindow& window)

View file

@ -64,6 +64,12 @@ private:
void display_savegame(twindow& window);
void evaluate_summary_string(std::stringstream& str,
const config& cfg_summary);
bool compare_name(unsigned i1, unsigned i2) const;
bool compare_date(unsigned i1, unsigned i2) const;
bool compare_name_rev(unsigned i1, unsigned i2) const;
bool compare_date_rev(unsigned i1, unsigned i2) const;
void fill_game_list(twindow& window,
std::vector<savegame::save_info>& games);

View file

@ -154,9 +154,9 @@ void thorizontal_list::place(const tpoint& origin, const tpoint& size)
tpoint current_origin = origin;
for(size_t i = 0; i < get_item_count(); ++i) {
tgrid& grid = item(i);
tgrid& grid = item_ordered(i);
if(grid.get_visible() == twidget::tvisible::invisible
|| !get_item_shown(i)) {
|| !get_item_shown(get_item_at_ordered(i))) {
continue;
}
@ -179,9 +179,9 @@ void thorizontal_list::set_origin(const tpoint& origin)
tpoint current_origin = origin;
for(size_t i = 0; i < get_item_count(); ++i) {
tgrid& grid = item(i);
tgrid& grid = item_ordered(i);
if(grid.get_visible() == twidget::tvisible::invisible
|| !get_item_shown(i)) {
|| !get_item_shown(get_item_at_ordered(i))) {
continue;
}
@ -201,7 +201,7 @@ void thorizontal_list::set_visible_rectangle(const SDL_Rect& rectangle)
*/
for(size_t i = 0; i < get_item_count(); ++i) {
tgrid& grid = item(i);
tgrid& grid = item_ordered(i);
grid.set_visible_rectangle(rectangle);
}
}
@ -261,13 +261,13 @@ void thorizontal_list::handle_key_left_arrow(SDLMod /*modifier*/, bool& handled)
// NOTE maybe this should only work if we can select only one item...
handled = true;
for(int i = get_selected_item() - 1; i >= 0; --i) {
for(int i = get_ordered_index(get_selected_item()) - 1; i >= 0; --i) {
// NOTE we check the first widget to be active since grids have no
// active flag. This method might not be entirely reliable.
tcontrol* control = dynamic_cast<tcontrol*>(item(i).widget(0, 0));
tcontrol* control = dynamic_cast<tcontrol*>(item(get_item_at_ordered(i)).widget(0, 0));
if(control && control->get_active()) {
select_item(i, true);
select_item(get_item_at_ordered(i), true);
return;
}
}
@ -283,19 +283,19 @@ void thorizontal_list::handle_key_right_arrow(SDLMod /*modifier*/,
// NOTE maybe this should only work if we can select only one item...
handled = true;
for(size_t i = get_selected_item() + 1; i < get_item_count(); ++i) {
for(size_t i = get_ordered_index(get_selected_item()) + 1; i < get_item_count(); ++i) {
if(item(i).get_visible() == twidget::tvisible::invisible
|| !get_item_shown(i)) {
if(item(get_item_at_ordered(i)).get_visible() == twidget::tvisible::invisible
|| !get_item_shown(get_item_at_ordered(i))) {
continue;
}
// NOTE we check the first widget to be active since grids have no
// active flag. This method might not be entirely reliable.
tcontrol* control = dynamic_cast<tcontrol*>(item(i).widget(0, 0));
tcontrol* control = dynamic_cast<tcontrol*>(item(get_item_at_ordered(i)).widget(0, 0));
if(control && control->get_active()) {
select_item(i, true);
select_item(get_item_at_ordered(i), true);
return;
}
}
@ -353,9 +353,9 @@ void tvertical_list::place(const tpoint& origin, const tpoint& size)
tpoint current_origin = origin;
for(size_t i = 0; i < get_item_count(); ++i) {
tgrid& grid = item(i);
tgrid& grid = item_ordered(i);
if(grid.get_visible() == twidget::tvisible::invisible
|| !get_item_shown(i)) {
|| !get_item_shown(get_item_at_ordered(i))) {
continue;
}
@ -378,9 +378,9 @@ void tvertical_list::set_origin(const tpoint& origin)
tpoint current_origin = origin;
for(size_t i = 0; i < get_item_count(); ++i) {
tgrid& grid = item(i);
tgrid& grid = item_ordered(i);
if(grid.get_visible() == twidget::tvisible::invisible
|| !get_item_shown(i)) {
|| !get_item_shown(get_item_at_ordered(i))) {
continue;
}
@ -461,13 +461,13 @@ void tvertical_list::handle_key_up_arrow(SDLMod /*modifier*/, bool& handled)
// NOTE maybe this should only work if we can select only one item...
handled = true;
for(int i = get_selected_item() - 1; i >= 0; --i) {
for(int i = get_ordered_index(get_selected_item()) - 1; i >= 0; --i) {
// NOTE we check the first widget to be active since grids have no
// active flag. This method might not be entirely reliable.
tcontrol* control = dynamic_cast<tcontrol*>(item(i).widget(0, 0));
tcontrol* control = dynamic_cast<tcontrol*>(item_ordered(i).widget(0, 0));
if(control && control->get_active()) {
select_item(i, true);
select_item(get_item_at_ordered(i), true);
return;
}
}
@ -482,19 +482,20 @@ void tvertical_list::handle_key_down_arrow(SDLMod /*modifier*/, bool& handled)
// NOTE maybe this should only work if we can select only one item...
handled = true;
for(size_t i = get_selected_item() + 1; i < get_item_count(); ++i) {
if(item(i).get_visible() == twidget::tvisible::invisible
|| !get_item_shown(i)) {
for(size_t i = get_ordered_index(get_selected_item()) + 1; i < get_item_count(); ++i) {
// why do we do this check here but not in handle_key_up_arrow?
if(item_ordered(i).get_visible() == twidget::tvisible::invisible
|| !get_item_shown(get_item_at_ordered(i))) {
continue;
}
// NOTE we check the first widget to be active since grids have no
// active flag. This method might not be entirely reliable.
tcontrol* control = dynamic_cast<tcontrol*>(item(i).widget(0, 0));
tcontrol* control = dynamic_cast<tcontrol*>(item_ordered(i).widget(0, 0));
if(control && control->get_active()) {
select_item(i, true);
select_item(get_item_at_ordered(i), true);
return;
}
}

View file

@ -236,6 +236,9 @@ public:
data,
const boost::function<void(twidget&)>& callback) = 0;
typedef boost::function<bool (unsigned, unsigned)> torder_func;
virtual void set_order(const torder_func& order) = 0;
/***** ***** ***** ***** Inherited ***** ***** ***** *****/
/*
@ -355,6 +358,15 @@ protected:
* @param index The index of a selected item.
*/
virtual void do_deselect_item(const unsigned index) = 0;
/** Gets the grid of an item. */
virtual tgrid& item_ordered(const unsigned index) = 0;
/** Gets the grid of an item. */
virtual const tgrid& item_ordered(const unsigned index) const = 0;
virtual unsigned get_ordered_index(unsigned index) const = 0;
virtual unsigned get_item_at_ordered(unsigned index_ordered) const = 0;
};
} // namespace gui2

View file

@ -602,6 +602,9 @@ public:
, selected_item_count_(0)
, last_selected_item_(-1)
, items_()
, order_()
, order_dirty_(true)
, order_func_()
{
}
@ -628,6 +631,7 @@ public:
delete items_[index];
items_.erase(items_.begin() + index);
order_dirty_ = true;
}
/** Inherited from tgenerator_. */
@ -637,6 +641,7 @@ public:
{
delete item;
}
order_dirty_ = true;
selected_item_count_ = 0;
}
@ -737,6 +742,22 @@ public:
return items_[index]->grid;
}
/** Inherited from tgenerator_. */
tgrid& item_ordered(const unsigned index)
{
calculate_order();
assert(index < items_.size());
return items_[order_[index]]->grid;
}
/** Inherited from tgenerator_. */
const tgrid& item_ordered(const unsigned index) const
{
calculate_order();
assert(index < items_.size());
return items_[order_[index]]->grid;
}
/** Inherited from tgenerator_. */
tgrid& create_item(const int index,
@ -767,6 +788,7 @@ public:
const unsigned item_index = index == -1 ? items_.size() : index;
items_.insert(items_.begin() + item_index, item);
order_dirty_ = true;
minimum_selection::create_item(item_index);
placement::create_item(item_index);
if(!is_selected(item_index)) {
@ -855,8 +877,10 @@ public:
{
assert(this->get_visible() == twidget::tvisible::visible);
FOREACH(AUTO item, items_)
calculate_order();
FOREACH(AUTO index, order_)
{
titem* item = items_[index];
if(item->grid.get_visible() == twidget::tvisible::visible
&& item->shown) {
@ -871,9 +895,10 @@ public:
int y_offset) OVERRIDE
{
assert(this->get_visible() == twidget::tvisible::visible);
FOREACH(AUTO item, items_)
calculate_order();
FOREACH(AUTO index, order_)
{
titem* item = items_[index];
if(item->grid.get_visible() == twidget::tvisible::visible
&& item->shown) {
@ -980,7 +1005,7 @@ private:
struct titem
{
titem() : grid(), selected(false), shown(true)
titem() : grid(), selected(false), shown(true), ordered_index(0)
{
}
@ -1000,6 +1025,8 @@ private:
* polishing.
*/
bool shown;
size_t ordered_index;
};
/** The number of selected items. */
@ -1009,8 +1036,78 @@ private:
int last_selected_item_;
/** The items in the generator. */
std::vector<titem*> items_;
typedef std::vector<titem*> titems;
titems items_;
/** the elements of order_ are indexes to items_ */
mutable std::vector<size_t> order_;
/** whether need to recalculate order_dirty_ */
mutable bool order_dirty_;
typedef boost::function<bool (unsigned, unsigned)> torder_func;
torder_func order_func_;
virtual void set_order(const torder_func& order) OVERRIDE
{
order_func_ = order;
order_dirty_ = true;
this->set_is_dirty(true);
}
struct calculate_order_helper
{
const torder_func& order_func_;
const titems& items_;
calculate_order_helper(const torder_func& order_func, const titems& items)
: order_func_(order_func)
, items_(items)
{
}
bool operator()(size_t a, size_t b)
{
return order_func_(a, b);
}
};
virtual unsigned get_ordered_index(unsigned index) const
{
assert(index < items_.size());
calculate_order();
return items_[index]->ordered_index;
}
virtual unsigned get_item_at_ordered(unsigned index_ordered) const
{
assert(index_ordered < items_.size());
calculate_order();
return order_[index_ordered];
}
void calculate_order() const
{
if(order_dirty_) {
if(order_.size() != items_.size()) {
order_.resize(items_.size());
for(size_t i = 0; i < items_.size(); ++i) {
order_[i] = i;
}
}
if(!order_func_.empty()) {
std::stable_sort(order_.begin(), order_.end(), calculate_order_helper(order_func_, items_));
}
for(size_t i = 0; i < order_.size(); ++i) {
items_[order_[i]]->ordered_index = i;
}
order_dirty_ = false;
}
else {
assert(order_.size() == items_.size());
}
}
/**
* Sets the selected state of an item.
*

View file

@ -26,6 +26,7 @@
#include "gui/auxiliary/window_builder/horizontal_listbox.hpp"
#include "gui/widgets/detail/register.tpp"
#include "gui/widgets/settings.hpp"
#include "gui/widgets/selectable.hpp"
#include "gui/widgets/window.hpp"
#include <boost/bind.hpp>
@ -60,6 +61,7 @@ tlistbox::tlistbox(const bool has_minimum,
, list_builder_(NULL)
, callback_value_changed_(NULL)
, need_layout_(false)
, orders_()
{
}
@ -515,7 +517,16 @@ void tlistbox::finalize(tbuilder_grid_const_ptr header,
if(header) {
swap_grid(&grid(), content_grid(), header->build(), "_header_grid");
}
tgrid& p = find_widget<tgrid>(this, "_header_grid", false);
for(unsigned i = 0, max = std::max(p.get_cols(), p.get_rows()); i < max; ++i) {
if(tselectable_* selectable = find_widget<tselectable_>(p.widget(0,i), "sort_" + lexical_cast<std::string>(i), false, false)) {
selectable->set_callback_state_change(boost::bind(&tlistbox::order_by_column, this, i, _1));
if(orders_.size() < max ) {
orders_.resize(max);
}
orders_[i].first = selectable;
}
}
if(footer) {
swap_grid(&grid(), content_grid(), footer->build(), "_footer_grid");
}
@ -524,6 +535,51 @@ void tlistbox::finalize(tbuilder_grid_const_ptr header,
-1, list_builder_, list_data, callback_list_item_clicked);
swap_grid(NULL, content_grid(), generator_, "_list_grid");
}
namespace {
bool default_sort(unsigned i1, unsigned i2)
{
return i1 < i2;
}
}
void tlistbox::order_by_column(unsigned column, twidget& widget)
{
tselectable_& selectable = dynamic_cast<tselectable_&>(widget);
if(column >= orders_.size()) {
return;
}
FOREACH(AUTO& pair, orders_)
{
if(pair.first != NULL && pair.first != &selectable) {
pair.first->set_value(0);
}
}
if(selectable.get_value() > orders_[column].second.size()) {
return;
}
if(selectable.get_value() == 0) {
order_by(tgenerator_::torder_func(&default_sort));
}
else {
order_by(orders_[column].second[selectable.get_value() - 1]);
}
}
void tlistbox::order_by(const tgenerator_::torder_func& func)
{
generator_->set_order(func);
set_is_dirty(true);
need_layout_ = true;
}
void tlistbox::set_column_order(unsigned col, const std::vector<tgenerator_::torder_func>& func)
{
if(col >= orders_.size()) {
orders_.resize(col + 1);
}
orders_[col].second = func;
}
void tlistbox::set_content_size(const tpoint& origin, const tpoint& size)
{

View file

@ -22,7 +22,7 @@
namespace gui2
{
class tselectable_;
namespace implementation
{
struct tbuilder_listbox;
@ -226,6 +226,9 @@ public:
list_builder_ = list_builder;
}
void order_by(const tgenerator_::torder_func& func);
void set_column_order(unsigned col, const std::vector<tgenerator_::torder_func>& func);
protected:
/***** ***** ***** ***** keyboard functions ***** ***** ***** *****/
@ -297,6 +300,8 @@ private:
bool need_layout_;
typedef std::vector<std::pair<tselectable_*, std::vector<tgenerator_::torder_func> > > torder_list;
torder_list orders_;
/**
* Resizes the content.
*
@ -332,6 +337,8 @@ private:
/** See @ref tcontrol::get_control_type. */
virtual const std::string& get_control_type() const OVERRIDE;
void order_by_column(unsigned column, twidget& widget);
};
} // namespace gui2