Show ListBox.cpp syntax highlighted
/* GG is a GUI for SDL and OpenGL.
Copyright (C) 2003 T. Zachary Laine
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public License
as published by the Free Software Foundation; either version 2.1
of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free
Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
02111-1307 USA
If you do not wish to comply with the terms of the LGPL please
contact the author as other terms are available for a fee.
Zach Laine
whatwasthataddress@hotmail.com */
#include <GG/ListBox.h>
#include <GG/GUI.h>
#include <GG/DrawUtil.h>
#include <GG/Layout.h>
#include <GG/Scroll.h>
#include <GG/StaticGraphic.h>
#include <GG/StyleFactory.h>
#include <GG/TextControl.h>
#include <GG/WndEvent.h>
#include <GG/WndEditor.h>
#include <boost/assign/list_of.hpp>
#include <cmath>
#include <numeric>
using namespace GG;
namespace {
const int SCROLL_WIDTH = 14;
const int DEFAULT_ROW_WIDTH = 50;
const int DEFAULT_ROW_HEIGHT = 22;
class RowSorter // used to sort rows by a certain column (which may contain some empty cells)
{
public:
RowSorter(int col, bool less) : sort_col(col), return_less(less) {}
bool operator()(const ListBox::Row* lr, const ListBox::Row* rr)
{
return return_less ? lr->SortKey(sort_col) < rr->SortKey(sort_col) : lr->SortKey(sort_col) > rr->SortKey(sort_col);
}
private:
const int sort_col;
const bool return_less;
};
struct SetStyleAction : AttributeChangedAction<Flags<ListBoxStyle> >
{
SetStyleAction(ListBox* list_box) : m_list_box(list_box) {}
void operator()(const Flags<ListBoxStyle>& style) {m_list_box->SetStyle(style);}
private:
ListBox* m_list_box;
};
struct SetSortColAction : AttributeChangedAction<int>
{
SetSortColAction(ListBox* list_box) : m_list_box(list_box) {}
void operator()(const int& sort_col) {m_list_box->SetSortCol(sort_col);}
private:
ListBox* m_list_box;
};
}
///////////////////////////////////////
// ListBoxStyle
///////////////////////////////////////
const ListBoxStyle GG::LIST_NONE (0);
const ListBoxStyle GG::LIST_VCENTER (1 << 0);
const ListBoxStyle GG::LIST_TOP (1 << 1);
const ListBoxStyle GG::LIST_BOTTOM (1 << 2);
const ListBoxStyle GG::LIST_CENTER (1 << 3);
const ListBoxStyle GG::LIST_LEFT (1 << 4);
const ListBoxStyle GG::LIST_RIGHT (1 << 5);
const ListBoxStyle GG::LIST_NOSORT (1 << 6);
const ListBoxStyle GG::LIST_SORTDESCENDING (1 << 7);
const ListBoxStyle GG::LIST_NOSEL (1 << 8);
const ListBoxStyle GG::LIST_SINGLESEL (1 << 9);
const ListBoxStyle GG::LIST_QUICKSEL (1 << 10);
const ListBoxStyle GG::LIST_USERDELETE (1 << 11);
const ListBoxStyle GG::LIST_BROWSEUPDATES (1 << 12);
GG_FLAGSPEC_IMPL(ListBoxStyle);
namespace {
bool RegisterListBoxStyles()
{
FlagSpec<ListBoxStyle>& spec = FlagSpec<ListBoxStyle>::instance();
spec.insert(LIST_NONE, "LIST_NONE", true);
spec.insert(LIST_VCENTER, "LIST_VCENTER", true);
spec.insert(LIST_TOP, "LIST_TOP", true);
spec.insert(LIST_BOTTOM, "LIST_BOTTOM", true);
spec.insert(LIST_CENTER, "LIST_CENTER", true);
spec.insert(LIST_LEFT, "LIST_LEFT", true);
spec.insert(LIST_RIGHT, "LIST_RIGHT", true);
spec.insert(LIST_NOSORT, "LIST_NOSORT", true);
spec.insert(LIST_SORTDESCENDING, "LIST_SORTDESCENDING", true);
spec.insert(LIST_NOSEL, "LIST_NOSEL", true);
spec.insert(LIST_SINGLESEL, "LIST_SINGLESEL", true);
spec.insert(LIST_QUICKSEL, "LIST_QUICKSEL", true);
spec.insert(LIST_USERDELETE, "LIST_USERDELETE", true);
spec.insert(LIST_BROWSEUPDATES, "LIST_BROWSEUPDATES", true);
return true;
}
bool dummy = RegisterListBoxStyles();
}
////////////////////////////////////////////////
// GG::ListBox::Row
////////////////////////////////////////////////
ListBox::Row::Row() :
Control(0, 0, DEFAULT_ROW_WIDTH, DEFAULT_ROW_HEIGHT),
m_row_alignment(ALIGN_VCENTER),
m_margin(2)
{}
ListBox::Row::Row(int w, int h, const std::string& drag_drop_data_type, Alignment align/* = ALIGN_VCENTER*/, int margin/* = 2*/) :
Control(0, 0, w, h),
m_row_alignment(align),
m_margin(margin)
{
SetDragDropDataType(drag_drop_data_type);
}
ListBox::Row::~Row()
{}
const std::string& ListBox::Row::SortKey(int column) const
{
return at(column)->WindowText();
}
size_t ListBox::Row::size() const
{
return m_cells.size();
}
bool ListBox::Row::empty() const
{
return m_cells.empty();
}
Control* ListBox::Row::operator[](size_t n) const
{
return m_cells[n];
}
Control* ListBox::Row::at(size_t n) const
{
return m_cells.at(n);
}
Alignment ListBox::Row::RowAlignment() const
{
return m_row_alignment;
}
Alignment ListBox::Row::ColAlignment(int n) const
{
return m_col_alignments[n];
}
int ListBox::Row::ColWidth(int n) const
{
return m_col_widths[n];
}
int ListBox::Row::Margin() const
{
return m_margin;
}
Control* ListBox::Row::CreateControl(const std::string& str, const boost::shared_ptr<Font>& font, Clr color) const
{
return GetStyleFactory()->NewTextControl(0, 0, str, font, color);
}
Control* ListBox::Row::CreateControl(const SubTexture& st) const
{
return new StaticGraphic(0, 0, st.Width(), st.Height(), st, GRAPHIC_SHRINKFIT);
}
void ListBox::Row::Render()
{}
void ListBox::Row::push_back(Control* c)
{
m_cells.push_back(c);
m_col_widths.push_back(5);
m_col_alignments.push_back(ALIGN_CENTER);
if (1 < m_cells.size()) {
m_col_widths.back() = m_col_widths[m_cells.size() - 1];
}
AdjustLayout();
}
void ListBox::Row::push_back(const std::string& str, const boost::shared_ptr<Font>& font, Clr color/* = CLR_BLACK*/)
{
push_back(CreateControl(str, font, color));
}
void ListBox::Row::push_back(const std::string& str, const std::string& font_filename, int pts, Clr color/* = CLR_BLACK*/)
{
push_back(CreateControl(str, GUI::GetGUI()->GetFont(font_filename, pts), color));
}
void ListBox::Row::push_back(const SubTexture& st)
{
push_back(CreateControl(st));
}
void ListBox::Row::clear()
{
m_cells.clear();
AdjustLayout();
}
void ListBox::Row::resize(size_t n)
{
size_t old_size = m_cells.size();
for (size_t i = n; i < old_size; ++i) {
delete m_cells[i];
}
m_cells.resize(n);
m_col_widths.resize(n);
m_col_alignments.resize(n);
for (unsigned int i = old_size; i < n; ++i) {
m_col_widths[i] = old_size ? m_col_widths[old_size - 1] : 5; // assign new cells reasonable default widths
m_col_alignments[i] = ALIGN_CENTER;
}
AdjustLayout();
}
void ListBox::Row::SetCell(int n, Control* c)
{
delete m_cells[n];
m_cells[n] = c;
AdjustLayout();
}
void ListBox::Row::SetRowAlignment(Alignment align)
{
m_row_alignment = align;
AdjustLayout();
}
void ListBox::Row::SetColAlignment(int n, Alignment align)
{
m_col_alignments[n] = align;
AdjustLayout();
}
void ListBox::Row::SetColWidth(int n, int width)
{
m_col_widths[n] = width;
AdjustLayout();
}
void ListBox::Row::SetColAlignments(const std::vector<Alignment>& aligns)
{
m_col_alignments = aligns;
AdjustLayout();
}
void ListBox::Row::SetColWidths(const std::vector<int>& widths)
{
m_col_widths = widths;
AdjustLayout();
}
void ListBox::Row::SetMargin(int margin)
{
m_margin = margin;
AdjustLayout();
}
void ListBox::Row::AdjustLayout(bool adjust_for_push_back/* = false*/)
{
RemoveLayout();
DetachChildren();
bool nonempty_cell_found = false;
for (unsigned int i = 0; i < m_cells.size(); ++i) {
if (m_cells[i]) {
nonempty_cell_found = true;
break;
}
}
if (!nonempty_cell_found)
return;
SetLayout(new Layout(0, 0, Width(), Height(), 1, m_cells.size(), m_margin, m_margin));
Layout* layout = GetLayout();
for (unsigned int i = 0; i < m_cells.size(); ++i) {
layout->SetMinimumColumnWidth(i, m_col_widths[i]);
if (m_cells[i]) {
layout->Add(m_cells[i], 0, i, m_row_alignment | m_col_alignments[i]);
}
}
}
////////////////////////////////////////////////
// GG::ListBox
////////////////////////////////////////////////
// static(s)
const int ListBox::BORDER_THICK = 2;
ListBox::ListBox() :
Control(),
m_vscroll(0),
m_hscroll(0),
m_caret(-1),
m_old_sel_row(-1),
m_old_sel_row_selected(false),
m_old_rdown_row(-1),
m_lclick_row(-1),
m_rclick_row(-1),
m_last_row_browsed(-1),
m_suppress_erase_signal(false),
m_first_row_shown(0),
m_first_col_shown(0),
m_cell_margin(2),
m_style(LIST_NONE),
m_header_row(0),
m_keep_col_widths(false),
m_clip_cells(false),
m_sort_col(0),
m_auto_scroll_during_drag_drops(true),
m_auto_scroll_margin(8),
m_auto_scrolling_up(false),
m_auto_scrolling_down(false),
m_auto_scrolling_left(false),
m_auto_scrolling_right(false),
m_auto_scroll_timer(250)
{
m_auto_scroll_timer.Stop();
m_auto_scroll_timer.Connect(this);
}
ListBox::ListBox(int x, int y, int w, int h, Clr color, Clr interior/* = CLR_ZERO*/,
Flags<WndFlag> flags/* = CLICKABLE*/) :
Control(x, y, w, h, flags),
m_vscroll(0),
m_hscroll(0),
m_caret(-1),
m_old_sel_row(-1),
m_old_sel_row_selected(false),
m_old_rdown_row(-1),
m_lclick_row(-1),
m_rclick_row(-1),
m_last_row_browsed(-1),
m_suppress_erase_signal(false),
m_first_row_shown(0),
m_first_col_shown(0),
m_cell_margin(2),
m_int_color(interior),
m_hilite_color(CLR_SHADOW),
m_style(LIST_NONE),
m_header_row(new Row()),
m_keep_col_widths(false),
m_clip_cells(false),
m_sort_col(0),
m_auto_scroll_during_drag_drops(true),
m_auto_scroll_margin(8),
m_auto_scrolling_up(false),
m_auto_scrolling_down(false),
m_auto_scrolling_left(false),
m_auto_scrolling_right(false),
m_auto_scroll_timer(250)
{
Control::SetColor(color);
ValidateStyle();
SetText("ListBox");
EnableChildClipping();
m_auto_scroll_timer.Stop();
m_auto_scroll_timer.Connect(this);
}
ListBox::~ListBox()
{
delete m_header_row;
}
Pt ListBox::MinUsableSize() const
{
return Pt(5 * SCROLL_WIDTH + 2 * BORDER_THICK,
5 * SCROLL_WIDTH + 2 * BORDER_THICK);
}
Pt ListBox::ClientUpperLeft() const
{
return UpperLeft() + Pt(BORDER_THICK, BORDER_THICK + (m_header_row->empty() ? 0 : m_header_row->Height()));
}
Pt ListBox::ClientLowerRight() const
{
return LowerRight() - Pt(BORDER_THICK + RightMargin(), BORDER_THICK + BottomMargin());
}
bool ListBox::Empty() const
{
return m_rows.empty();
}
const ListBox::Row& ListBox::GetRow(int n) const
{
return *m_rows.at(n);
}
int ListBox::Caret() const
{
return m_caret;
}
const std::set<int>& ListBox::Selections() const
{
return m_selections;
}
bool ListBox::Selected(int n) const
{
return m_selections.find(n) != m_selections.end();
}
Clr ListBox::InteriorColor() const
{
return m_int_color;
}
Clr ListBox::HiliteColor() const
{
return m_hilite_color;
}
Flags<ListBoxStyle> ListBox::Style() const
{
return m_style;
}
const ListBox::Row& ListBox::ColHeaders() const
{
return *m_header_row;
}
int ListBox::FirstRowShown() const
{
return m_first_row_shown;
}
int ListBox::FirstColShown() const
{
return m_first_col_shown;
}
int ListBox::LastVisibleRow() const
{
int visible_pixels = ClientSize().y;
int acc = 0;
int i = m_first_row_shown;
for (; i < static_cast<int>(m_rows.size()); ++i) {
acc += m_rows[i]->Height();
if (visible_pixels <= acc)
break;
}
if (static_cast<int>(m_rows.size()) <= i)
i = static_cast<int>(m_rows.size()) - 1;
return i;
}
int ListBox::LastVisibleCol() const
{
int visible_pixels = ClientSize().x;
int acc = 0;
int i = m_first_col_shown;
for (; i < static_cast<int>(m_col_widths.size()); ++i) {
acc += m_col_widths[i];
if (visible_pixels <= acc)
break;
}
if (static_cast<int>(m_col_widths.size()) <= i)
i = static_cast<int>(m_col_widths.size()) - 1;
return i;
}
int ListBox::NumRows() const
{
return int(m_rows.size());
}
int ListBox::NumCols() const
{
return m_col_widths.size();
}
bool ListBox::KeepColWidths() const
{
return m_keep_col_widths;
}
int ListBox::SortCol() const
{
return (m_sort_col >= 0 && m_sort_col < static_cast<int>(m_col_widths.size()) ? m_sort_col : 0);
}
int ListBox::ColWidth(int n) const
{
return m_col_widths[n];
}
Alignment ListBox::ColAlignment(int n) const
{
return m_col_alignments[n];
}
Alignment ListBox::RowAlignment(int n) const
{
return m_rows[n]->RowAlignment();
}
const std::set<std::string>& ListBox::AllowedDropTypes() const
{
return m_allowed_drop_types;
}
bool ListBox::AutoScrollDuringDragDrops() const
{
return m_auto_scroll_during_drag_drops;
}
int ListBox::AutoScrollMargin() const
{
return m_auto_scroll_margin;
}
int ListBox::AutoScrollInterval() const
{
return m_auto_scroll_timer.Interval();
}
void ListBox::StartingChildDragDrop(const Wnd* wnd, const Pt& offset)
{
if (m_selections.empty())
return;
int vertical_offset = offset.y;
int wnd_idx = -1;
for (unsigned int i = 0; i < m_rows.size(); ++i) {
if (m_rows[i] == wnd) {
wnd_idx = i;
break;
}
}
assert(wnd_idx != -1);
std::set<int>::iterator wnd_it = m_selections.find(wnd_idx);
assert(wnd_it != m_selections.end());
for (std::set<int>::iterator it = m_selections.begin(); it != wnd_it; ++it) {
vertical_offset += m_rows[*it]->Height();
}
for (std::set<int>::iterator it = m_selections.begin(); it != m_selections.end(); ++it) {
Wnd* row_wnd = static_cast<Wnd*>(m_rows[*it]);
if (row_wnd != wnd) {
GUI::GetGUI()->RegisterDragDropWnd(row_wnd, Pt(offset.x, vertical_offset), this);
vertical_offset -= row_wnd->Height();
} else {
vertical_offset -= wnd->Height();
}
}
}
void ListBox::AcceptDrops(std::list<Wnd*>& wnds, const Pt& pt)
{
for (std::list<Wnd*>::iterator it = wnds.begin(); it != wnds.end(); ) {
std::list<Wnd*>::iterator tmp_it = it++;
Wnd* wnd = *tmp_it;
if (m_allowed_drop_types.find("") != m_allowed_drop_types.end() ||
m_allowed_drop_types.find(wnd->DragDropDataType()) != m_allowed_drop_types.end()) {
if (Row* row = dynamic_cast<Row*>(wnd)) {
int insertion_row = RowUnderPt(pt);
try {
Insert(row, insertion_row, true);
} catch (const DontAcceptDrop&) {
wnds.erase(tmp_it);
}
}
}
}
}
void ListBox::ChildrenDraggedAway(const std::list<Wnd*>& wnds, const Wnd* destination)
{
for (std::list<Wnd*>::const_iterator it = wnds.begin(); it != wnds.end(); ++it) {
Row* row = dynamic_cast<Row*>(*it);
assert(row);
int idx = -1;
for (unsigned int i = 0; i < m_rows.size(); ++i) {
if (m_rows[i] == row) {
idx = i;
break;
}
}
assert(0 <= idx && idx < static_cast<int>(m_rows.size()));
if (!MatchesOrContains(this, destination))
Erase(idx);
}
}
void ListBox::Render()
{
// draw beveled rectangle around client area
Pt ul = UpperLeft(), lr = LowerRight();
Pt cl_ul = ClientUpperLeft(), cl_lr = ClientLowerRight();
Clr color_to_use = Disabled() ? DisabledColor(Color()) : Color();
Clr int_color_to_use = Disabled() ? DisabledColor(m_int_color) : m_int_color;
Clr hilite_color_to_use = Disabled() ? DisabledColor(m_hilite_color) : m_hilite_color;
BeveledRectangle(ul.x, ul.y, lr.x, lr.y, int_color_to_use, color_to_use, false, BORDER_THICK);
int last_visible_row = LastVisibleRow();
BeginClipping();
// draw selection hiliting
int prev_sel = -1;
int top = cl_ul.y, bottom = 0;
for (std::set<int>::iterator it = m_selections.begin(); it != m_selections.end(); ++it) {
int curr_sel = *it;
if (curr_sel >= m_first_row_shown && curr_sel <= last_visible_row) {
// no need to look for the current selection's height, if we just found it on the last iteration
top = (prev_sel == curr_sel - 1) ? bottom : 0;
if (!top) {
for (int i = m_first_row_shown; i < curr_sel; ++i)
top += m_rows[i]->Height();
}
bottom = top + m_rows[curr_sel]->Height();
if (bottom > cl_lr.y)
bottom = cl_lr.y;
FlatRectangle(cl_ul.x, cl_ul.y + top, cl_lr.x, cl_ul.y + bottom, hilite_color_to_use, CLR_ZERO, 0);
prev_sel = curr_sel;
}
}
// draw caret
if (m_caret >= m_first_row_shown && m_caret <= last_visible_row &&
MatchesOrContains(this, GUI::GetGUI()->FocusWnd())) {
Pt row_ul = m_rows[m_caret]->UpperLeft();
Pt row_lr = m_rows[m_caret]->LowerRight();
row_lr.x = ClientLowerRight().x;
FlatRectangle(row_ul.x, row_ul.y, row_lr.x, row_lr.y, CLR_ZERO, CLR_SHADOW, 2);
}
EndClipping();
// HACK! This gets around the issue of how to render headers and scrolls, which do not fall within the client area.
if (!m_header_row->empty()) {
Rect header_area(Pt(ul.x + BORDER_THICK, m_header_row->UpperLeft().y), Pt(lr.x - BORDER_THICK, m_header_row->LowerRight().y));
BeginScissorClipping(header_area.ul, header_area.lr);
GUI::GetGUI()->RenderWindow(m_header_row);
EndScissorClipping();
}
if (m_vscroll)
GUI::GetGUI()->RenderWindow(m_vscroll);
if (m_hscroll)
GUI::GetGUI()->RenderWindow(m_hscroll);
// ensure that data in occluded cells is not rendered
for (int i = 0; i < NumRows(); ++i) {
if (i < m_first_row_shown || last_visible_row < i)
m_rows[i]->Hide();
else
m_rows[i]->Show();
}
}
void ListBox::KeyPress(Key key, Flags<ModKey> mod_keys)
{
if (!Disabled()) {
switch (key) {
case GGK_SPACE: // space bar (selects item under caret like a mouse click)
if (m_caret != -1) {
m_old_sel_row_selected = m_selections.find(m_caret) != m_selections.end();
ClickAtRow(m_caret, mod_keys);
}
break;
case GGK_DELETE: // delete key
if (m_style & LIST_USERDELETE) {
std::set<int>::reverse_iterator r_it;
if (m_style & LIST_NOSEL) {
if (m_caret != -1) {
Row* row = Erase(m_caret);
delete row;
}
} else {
while ((r_it = m_selections.rbegin()) != m_selections.rend()) {
Row* row = Erase(*r_it);
delete row;
}
m_selections.clear();
}
}
break;
// vertical scrolling keys
case GGK_UP: // arrow-up (not numpad arrow)
if (m_caret > 0)
--m_caret;
break;
case GGK_DOWN: // arrow-down (not numpad arrow)
if (m_caret != -1 && m_caret < static_cast<int>(m_rows.size()) - 1)
++m_caret;
break;
case GGK_PAGEUP: // page up key (not numpad key)
if (m_caret != -1) {
int space = ClientSize().y;
while (m_caret >= 1 && (space -= m_rows[m_caret - 1]->Height()) > 0)
--m_caret;
}
break;
case GGK_PAGEDOWN: // page down key (not numpad key)
if (m_caret != -1) {
int space = ClientSize().y;
while (m_caret < static_cast<int>(m_rows.size()) - 1 && (space -= m_rows[m_caret]->Height()) > 0)
++m_caret;
}
break;
case GGK_HOME: // home key (not numpad)
if (m_caret != -1 && !m_rows.empty())
m_caret = 0;
break;
case GGK_END: // end key (not numpad)
if (m_caret != -1)
m_caret = static_cast<int>(m_rows.size()) - 1;
break;
// horizontal scrolling keys
case GGK_LEFT: // left key (not numpad key)
if (m_first_col_shown) {
--m_first_col_shown;
m_hscroll->ScrollTo(std::accumulate(m_col_widths.begin(), m_col_widths.begin() + m_first_col_shown, 0));
}
break;
case GGK_RIGHT:{ // right key (not numpad)
int last_fully_visible_col = LastVisibleCol();
if (std::accumulate(m_col_widths.begin(), m_col_widths.begin() + last_fully_visible_col, 0) > ClientSize().x)
--last_fully_visible_col;
if (last_fully_visible_col < static_cast<int>(m_col_widths.size()) - 1) {
++m_first_col_shown;
m_hscroll->ScrollTo(std::accumulate(m_col_widths.begin(), m_col_widths.begin() + m_first_col_shown, 0));
}
break;}
// any other key gets passed along to the parent
default:
Control::KeyPress(key, mod_keys);
}
if (key != GGK_SPACE && key != GGK_DELETE && key != GGK_LEFT && key != GGK_RIGHT)
BringCaretIntoView();
} else {
Control::KeyPress(key, mod_keys);
}
}
void ListBox::MouseWheel(const Pt& pt, int move, Flags<ModKey> mod_keys)
{
if (m_vscroll) {
for (int i = 0; i < move; ++i) {
if (0 < m_first_row_shown)
m_vscroll->ScrollTo(m_vscroll->PosnRange().first - m_rows[m_first_row_shown - 1]->Height());
}
for (int i = 0; i < -move; ++i) {
if (m_first_row_shown < static_cast<int>(m_rows.size() - 1))
m_vscroll->ScrollTo(m_vscroll->PosnRange().first + m_rows[m_first_row_shown]->Height());
}
}
}
void ListBox::DragDropEnter(const Pt& pt, const std::map<Wnd*, Pt>& drag_drop_wnds, Flags<ModKey> mod_keys)
{
ResetAutoScrollVars();
DragDropHere(pt, drag_drop_wnds, mod_keys);
}
void ListBox::DragDropHere(const Pt& pt, const std::map<Wnd*, Pt>& drag_drop_wnds, Flags<ModKey> mod_keys)
{
if (!m_rows.empty() && m_auto_scroll_during_drag_drops && InClient(pt)) {
const Pt MARGIN_OFFSET(m_auto_scroll_margin, m_auto_scroll_margin);
Rect client_no_scroll_hole(ClientUpperLeft() + MARGIN_OFFSET, ClientLowerRight() - MARGIN_OFFSET);
m_auto_scrolling_up = pt.y < client_no_scroll_hole.ul.y;
m_auto_scrolling_down = client_no_scroll_hole.lr.y < pt.y;
m_auto_scrolling_left = pt.x < client_no_scroll_hole.ul.x;
m_auto_scrolling_right = client_no_scroll_hole.lr.x < pt.x;
if (m_auto_scrolling_up || m_auto_scrolling_down || m_auto_scrolling_left || m_auto_scrolling_right) {
bool acceptible_drop = false;
for (std::map<Wnd*, GG::Pt>::const_iterator it = drag_drop_wnds.begin(); it != drag_drop_wnds.end(); ++it) {
if (m_allowed_drop_types.find("") != m_allowed_drop_types.end() ||
m_allowed_drop_types.find(it->first->DragDropDataType()) != m_allowed_drop_types.end()) {
acceptible_drop = true;
break;
}
}
if (acceptible_drop) {
if (!m_auto_scroll_timer.Running()) {
m_auto_scroll_timer.Reset(GUI::GetGUI()->Ticks());
m_auto_scroll_timer.Start();
}
} else {
DragDropLeave();
}
}
}
}
void ListBox::DragDropLeave()
{
ResetAutoScrollVars();
}
void ListBox::TimerFiring(int ticks, Timer* timer)
{
if (timer == &m_auto_scroll_timer && !m_rows.empty()) {
if (m_vscroll) {
if (m_auto_scrolling_up && 0 < m_first_row_shown)
m_vscroll->ScrollTo(m_vscroll->PosnRange().first - m_rows[m_first_row_shown - 1]->Height());
if (m_auto_scrolling_down) {
int last_visible_row = LastVisibleRow();
if (last_visible_row < static_cast<int>(m_rows.size() - 1) ||
ClientLowerRight().y < m_rows[last_visible_row]->LowerRight().y)
m_vscroll->ScrollTo(m_vscroll->PosnRange().first + m_rows[m_first_row_shown]->Height());
}
}
if (m_hscroll) {
if (m_auto_scrolling_left && 0 < m_first_col_shown)
m_hscroll->ScrollTo(m_hscroll->PosnRange().first - m_col_widths[m_first_col_shown - 1]);
if (m_auto_scrolling_right) {
int last_visible_col = LastVisibleCol();
if (last_visible_col < static_cast<int>(m_col_widths.size() - 1) ||
ClientLowerRight().x < m_rows.front()->LowerRight().x)
m_hscroll->ScrollTo(m_hscroll->PosnRange().first + m_col_widths[m_first_col_shown]);
}
}
}
}
void ListBox::SizeMove(const Pt& ul, const Pt& lr)
{
Wnd::SizeMove(ul, lr);
AdjustScrolls(true);
}
void ListBox::Disable(bool b/* = true*/)
{
Control::Disable(b);
if (m_vscroll)
m_vscroll->Disable(b);
if (m_hscroll)
m_hscroll->Disable(b);
}
void ListBox::SetColor(Clr c)
{
Control::SetColor(c);
if (m_vscroll)
m_vscroll->SetColor(c);
if (m_hscroll)
m_hscroll->SetColor(c);
}
int ListBox::Insert(Row* row, int at/*= -1*/)
{
return Insert(row, at, false);
}
ListBox::Row* ListBox::Erase(int idx)
{
return Erase(idx, false);
}
void ListBox::Clear()
{
bool signal = !m_rows.empty(); // inhibit signal if the list box was already empty
m_rows.clear();
m_caret = -1;
DeleteChildren();
m_vscroll = 0;
m_hscroll = 0;
m_first_row_shown = m_first_col_shown = 0;
m_selections.clear();
m_old_sel_row = -1;
m_lclick_row = -1;
if (!m_keep_col_widths) { // remove column widths and alignments, if needed
m_col_widths.clear();
m_col_alignments.clear();
}
AdjustScrolls(false);
if (signal)
ClearedSignal();
}
void ListBox::SelectRow(int n)
{
if (!(m_style & LIST_NOSEL) && n >= 0 && n < static_cast<int>(m_rows.size()) && m_selections.find(n) == m_selections.end()) {
bool emit_signal = m_selections.find(n) == m_selections.end();
if (m_style & LIST_SINGLESEL)
m_selections.clear();
m_selections.insert(n);
if (emit_signal)
SelChangedSignal(m_selections);
}
}
void ListBox::DeselectRow(int n)
{
if (n >= 0 && n < static_cast<int>(m_rows.size()) && m_selections.find(n) != m_selections.end()) {
m_selections.erase(n);
SelChangedSignal(m_selections);
}
}
void ListBox::SelectAll()
{
bool emit_signal = false;
if (m_selections.size() < m_rows.size())
emit_signal = true;
for (unsigned int i = 0; i < m_rows.size(); ++i) {
m_selections.insert(i);
}
if (emit_signal)
SelChangedSignal(m_selections);
}
void ListBox::DeselectAll()
{
bool emit_signal = false;
if (!m_selections.empty())
emit_signal = true;
m_selections.clear();
m_caret = -1;
if (emit_signal)
SelChangedSignal(m_selections);
}
ListBox::Row& ListBox::GetRow(int n)
{
return *m_rows.at(n);
}
void ListBox::SetSelections(const std::set<int>& s)
{
m_selections = s;
}
void ListBox::SetCaret(int idx)
{
m_rows.at(idx);
m_caret = idx;
}
void ListBox::BringRowIntoView(int n)
{
if (0 <= n && n < static_cast<int>(m_rows.size())) {
if (0 <= n && n < m_first_row_shown) {
m_first_row_shown = n;
} else if (LastVisibleRow() <= n) { // find the row that preceeds the target row by about ClientSize().y pixels, and make it the first row shown
m_first_row_shown = FirstRowShownWhenBottomIs(n, ClientHeight());
}
if (m_vscroll) {
int acc = 0;
for (int i = 0; i < m_first_row_shown; ++i)
acc += m_rows[i]->Height();
m_vscroll->ScrollTo(acc);
}
}
}
void ListBox::SetInteriorColor(Clr c)
{
m_int_color = c;
}
void ListBox::SetHiliteColor(Clr c)
{
m_hilite_color = c;
}
void ListBox::SetStyle(Flags<ListBoxStyle> s)
{
// if we're going from an unsorted style to a sorted one, do the sorting now
if (m_style & LIST_NOSORT) {
if (!(s & LIST_NOSORT))
std::stable_sort(m_rows.begin(), m_rows.end(), RowSorter(m_sort_col, !(s & LIST_SORTDESCENDING)));
} else { // if we're changing the sorting order of a sorted list, reverse the contents
if (static_cast<bool>(m_style & LIST_SORTDESCENDING) != static_cast<bool>(s & LIST_SORTDESCENDING))
std::reverse(m_rows.begin(), m_rows.end());
}
m_style = s;
ValidateStyle();
}
void ListBox::SetColHeaders(Row* r)
{
int client_height = ClientHeight();
delete m_header_row;
if (r) {
m_header_row = r;
// if this column header is being added to an empty listbox, the listbox takes on some of the
// attributes of the header, similar to the insertion of a row into an empty listbox; see Insert()
if (m_rows.empty() && m_col_widths.empty()) {
m_col_widths.resize(m_header_row->size(), (ClientSize().x - SCROLL_WIDTH) / m_header_row->size());
// put the remainder in the last column, so the total width == ClientSize().x - SCROLL_WIDTH
m_col_widths.back() += (ClientSize().x - SCROLL_WIDTH) % m_header_row->size();
Alignment alignment = ALIGN_NONE;
if (m_style & LIST_LEFT)
alignment = ALIGN_LEFT;
if (m_style & LIST_CENTER)
alignment = ALIGN_CENTER;
if (m_style & LIST_RIGHT)
alignment = ALIGN_RIGHT;
m_col_alignments.resize(m_header_row->size(), alignment);
}
NormalizeRow(m_header_row);
m_header_row->MoveTo(Pt(0, -m_header_row->Height()));
AttachChild(m_header_row);
} else {
m_header_row = new Row();
}
if (client_height != ClientHeight())
AdjustScrolls(true);
}
void ListBox::RemoveColHeaders()
{
SetColHeaders(0);
}
void ListBox::SetNumCols(int n)
{
if (n > 0) {
if (m_col_widths.size()) {
m_col_widths.resize(n);
m_col_alignments.resize(n);
} else {
m_col_widths.resize(n, ClientSize().x / n);
m_col_widths.back() += ClientSize().x % n;
Alignment alignment = ALIGN_NONE;
if (m_style & LIST_LEFT)
alignment = ALIGN_LEFT;
if (m_style & LIST_CENTER)
alignment = ALIGN_CENTER;
if (m_style & LIST_RIGHT)
alignment = ALIGN_RIGHT;
m_col_alignments.resize(n, alignment);
}
if (m_sort_col <= n)
m_sort_col = 0;
for (unsigned int i = 0; i < m_rows.size(); ++i) {
NormalizeRow(m_rows[i]);
}
}
AdjustScrolls(false);
}
void ListBox::SetSortCol(int n)
{
if (m_sort_col != n && !(m_style & LIST_NOSORT))
std::stable_sort(m_rows.begin(), m_rows.end(), RowSorter(n, !(m_style & LIST_SORTDESCENDING)));
m_sort_col = n;
}
void ListBox::SetColWidth(int n, int w)
{
m_col_widths[n] = w;
for (unsigned int i = 0; i < m_rows.size(); ++i) {
m_rows[i]->SetColWidth(n, w);
}
AdjustScrolls(false);
}
void ListBox::LockColWidths()
{
m_keep_col_widths = true;
}
void ListBox::UnLockColWidths()
{
m_keep_col_widths = false;
}
void ListBox::SetColAlignment(int n, Alignment align)
{
m_col_alignments[n] = align;
for (unsigned int i = 0; i < m_rows.size(); ++i) {
m_rows[i]->SetColAlignment(n, align);
}
}
void ListBox::SetRowAlignment(int n, Alignment align)
{
m_rows[n]->SetRowAlignment(align);
}
void ListBox::AllowDropType(const std::string& str)
{
m_allowed_drop_types.insert(str);
}
void ListBox::DisallowDropType(const std::string& str)
{
m_allowed_drop_types.erase(str);
}
void ListBox::AutoScrollDuringDragDrops(bool auto_scroll)
{
m_auto_scroll_during_drag_drops = auto_scroll;
}
void ListBox::SetAutoScrollMargin(int margin)
{
m_auto_scroll_margin = margin;
}
void ListBox::SetAutoScrollInterval(int interval)
{
m_auto_scroll_timer.SetInterval(interval);
}
void ListBox::DefineAttributes(WndEditor* editor)
{
if (!editor)
return;
Control::DefineAttributes(editor);
editor->Label("ListBox");
// TODO: handle specifying column widths and alignments
editor->Attribute("Cell Margin", m_cell_margin);
editor->Attribute("Interior Color", m_int_color);
editor->Attribute("Highlighting Color", m_hilite_color);
boost::shared_ptr<SetStyleAction> set_style_action(new SetStyleAction(this));
editor->BeginFlags<ListBoxStyle>(m_style, set_style_action);
typedef std::vector<ListBoxStyle> FlagVec;
using boost::assign::list_of;
editor->FlagGroup("V. Alignment", FlagVec() = list_of(LIST_TOP)(LIST_VCENTER)(LIST_BOTTOM));
editor->FlagGroup("H. Alignment", FlagVec() = list_of(LIST_LEFT)(LIST_CENTER)(LIST_RIGHT));
editor->Flag("Do not Sort", LIST_NOSORT);
editor->Flag("Sort Descending", LIST_SORTDESCENDING);
editor->Flag("No Selections", LIST_NOSEL);
editor->Flag("Single-Selection", LIST_SINGLESEL);
editor->Flag("Quick-Selection", LIST_QUICKSEL);
editor->Flag("User Deletions", LIST_USERDELETE);
editor->Flag("Browse Updates", LIST_BROWSEUPDATES);
editor->EndFlags();
// TODO: handle setting header row
editor->Attribute("Keep Column Widths", m_keep_col_widths);
editor->Attribute("Clip Cells", m_clip_cells);
boost::shared_ptr<SetSortColAction> set_sort_col_action(new SetSortColAction(this));
editor->Attribute<int>("Sort Column", m_sort_col,
0, static_cast<int>(m_col_widths.size()),
set_sort_col_action);
// TODO: handle specifying allowed drop types
}
int ListBox::RightMargin() const
{
return (m_vscroll ? SCROLL_WIDTH : 0);
}
int ListBox::BottomMargin() const
{
return (m_hscroll ? SCROLL_WIDTH : 0);
}
int ListBox::RowUnderPt(const Pt& pt) const
{
int retval;
int acc = ClientUpperLeft().y;
for (retval = m_first_row_shown; retval < static_cast<int>(m_rows.size()); ++retval) {
acc += m_rows[retval]->Height();
if (pt.y <= acc) break;
}
return retval;
}
int ListBox::OldSelRow() const
{
return m_old_sel_row;
}
int ListBox::OldRDownRow() const
{
return m_old_rdown_row;
}
int ListBox::LClickRow() const
{
return m_lclick_row;
}
int ListBox::RClickRow() const
{
return m_rclick_row;
}
bool ListBox::AutoScrollingUp() const
{
return m_auto_scrolling_up;
}
bool ListBox::AutoScrollingDown() const
{
return m_auto_scrolling_down;
}
bool ListBox::AutoScrollingLeft() const
{
return m_auto_scrolling_left;
}
bool ListBox::AutoScrollingRight() const
{
return m_auto_scrolling_right;
}
int ListBox::VerticalScrollPadding(int client_height_without_horizontal_scroll)
{
int first_row_shown_when_caret_at_bottom =
FirstRowShownWhenBottomIs(m_rows.size() - 1, client_height_without_horizontal_scroll);
int height_of_rows_visible_at_bottom = 0;
for (unsigned int i = first_row_shown_when_caret_at_bottom; i < m_rows.size(); ++i) {
height_of_rows_visible_at_bottom += m_rows[i]->Height();
}
return client_height_without_horizontal_scroll - height_of_rows_visible_at_bottom;
}
int ListBox::HorizontalScrollPadding(int client_width_without_vertical_scroll)
{
int first_col_shown_when_caret_at_right =
FirstColShownWhenRightIs(m_col_widths.size() - 1, client_width_without_vertical_scroll);
int width_of_cols_visible_at_right = 0;
for (unsigned int i = first_col_shown_when_caret_at_right; i < m_col_widths.size(); ++i) {
width_of_cols_visible_at_right += m_col_widths[i];
}
return client_width_without_vertical_scroll - width_of_cols_visible_at_right;
}
bool ListBox::EventFilter(Wnd* w, const WndEvent& event)
{
assert(dynamic_cast<Row*>(w));
if (!Disabled()) {
Pt pt = event.Point();
Flags<ModKey> mod_keys = event.ModKeys();
switch (event.Type()) {
case WndEvent::LButtonDown: {
m_old_sel_row = RowUnderPt(pt);
if (m_old_sel_row >= static_cast<int>(m_rows.size()) || !InClient(pt)) {
m_old_sel_row = -1;
} else {
m_old_sel_row_selected = m_selections.find(m_old_sel_row) != m_selections.end();
if (!(m_style & LIST_NOSEL) && !m_old_sel_row_selected)
ClickAtRow(m_old_sel_row, mod_keys);
}
break;
}
case WndEvent::LButtonUp: {
m_old_sel_row = -1;
break;
}
case WndEvent::LClick: {
if (m_old_sel_row >= 0 && InClient(pt)) {
int sel_row = RowUnderPt(pt);
if (sel_row == m_old_sel_row) {
if (!(m_style & LIST_NOSEL))
ClickAtRow(sel_row, mod_keys);
else
m_caret = sel_row;
m_lclick_row = sel_row;
LeftClickedSignal(sel_row, m_rows[sel_row], pt);
}
}
break;
}
case WndEvent::LDoubleClick: {
int row = RowUnderPt(pt);
if (row >= 0 && row == m_lclick_row && InClient(pt)) {
DoubleClickedSignal(row, m_rows[row]);
m_old_sel_row = -1;
} else {
LClick(pt, mod_keys);
}
break;
}
case WndEvent::RButtonDown: {
int row = RowUnderPt(pt);
if (row < static_cast<int>(m_rows.size()) && InClient(pt)) {
m_old_rdown_row = row;
} else {
m_old_rdown_row = -1;
}
break;
}
case WndEvent::RClick: {
int row = RowUnderPt(pt);
if (row >= 0 && row == m_old_rdown_row && InClient(pt)) {
m_rclick_row = row;
RightClickedSignal(row, m_rows[row], pt);
}
m_old_rdown_row = -1;
break;
}
case WndEvent::MouseEnter: {
if (m_style & LIST_BROWSEUPDATES) {
int sel_row = RowUnderPt(pt);
if (sel_row >= static_cast<int>(m_rows.size()))
sel_row = -1;
if (m_last_row_browsed != sel_row)
BrowsedSignal(m_last_row_browsed = sel_row);
}
break;
}
case WndEvent::MouseHere:
break;
case WndEvent::MouseLeave: {
if (m_style & LIST_BROWSEUPDATES) {
if (m_last_row_browsed != -1)
BrowsedSignal(m_last_row_browsed = -1);
}
break;
}
case WndEvent::GainingFocus: {
GUI::GetGUI()->SetFocusWnd(this);
break;
}
case WndEvent::MouseWheel:
return false;
case WndEvent::DragDropEnter:
case WndEvent::DragDropHere:
case WndEvent::DragDropLeave:
HandleEvent(event);
break;
case WndEvent::KeyPress:
case WndEvent::KeyRelease:
case WndEvent::TimerFiring:
return false;
default:
break;
}
}
return true;
}
int ListBox::Insert(Row* row, int at, bool dropped)
{
// track the originating row if this is an intra-ListBox drag-drop
int dropped_row_original_index = -1;
if (dropped) {
for (unsigned int i = 0; i < m_rows.size(); ++i) {
if (m_rows[i] == row) {
dropped_row_original_index = i;
break;
}
}
}
int retval = at;
// the first row inserted into an empty list box defines the number of columns, and initializes the column widths
if (m_rows.empty() && (m_col_widths.empty() || !m_keep_col_widths)) {
m_col_widths.resize(row->size(), (ClientSize().x - SCROLL_WIDTH) / row->size());
// put the remainder in the last column, so the total width == ClientSize().x - SCROLL_WIDTH
m_col_widths.back() += (ClientSize().x - SCROLL_WIDTH) % row->size();
Alignment alignment = ALIGN_NONE;
if (m_style & LIST_LEFT)
alignment = ALIGN_LEFT;
if (m_style & LIST_CENTER)
alignment = ALIGN_CENTER;
if (m_style & LIST_RIGHT)
alignment = ALIGN_RIGHT;
m_col_alignments.resize(row->size(), alignment);
if (!m_header_row->empty())
NormalizeRow(m_header_row);
}
row->InstallEventFilter(this);
NormalizeRow(row);
Pt insertion_pt;
if (m_rows.empty()) {
retval = 0;
m_rows.push_back(row);
} else {
if (m_style & LIST_NOSORT) {
if (at < 0 || at > static_cast<int>(m_rows.size())) // if at is out of bounds, insert item at the end
at = m_rows.size();
retval = at;
} else {
const std::string& row_str = (*row)[m_sort_col]->WindowText();
retval = 0;
while ((retval < static_cast<int>(m_rows.size())) &&
((m_style & LIST_SORTDESCENDING) ? (row_str <= (*m_rows[retval])[m_sort_col]->WindowText()) :
(row_str >= (*m_rows[retval])[m_sort_col]->WindowText())))
++retval;
}
std::vector<Row*>::iterator insertion_it = m_rows.begin() + retval;
int y = insertion_it == m_rows.end() ?
m_rows.back()->RelativeLowerRight().y : (*insertion_it)->RelativeUpperLeft().y;
insertion_pt = Pt(0, y);
m_rows.insert(insertion_it, row);
}
int row_height = row->Height();
if (retval <= m_caret) // move caret down, if needed
++m_caret;
if (retval <= dropped_row_original_index)
++dropped_row_original_index;
// "bump" the positions of, and selections on, lower rows down one row
for (int i = static_cast<int>(m_rows.size() - 1); i > retval; --i) {
m_rows[i]->OffsetMove(Pt(0, row_height));
if (m_selections.find(i - 1) != m_selections.end()) {
m_selections.insert(i);
m_selections.erase(i - 1);
}
}
Pt original_ul = row->RelativeUpperLeft();
Wnd* original_parent = row->Parent();
AttachChild(row);
row->MoveTo(insertion_pt);
AdjustScrolls(false);
if (dropped) {
// ensure that no one has a problem with this drop in user space (if so, they should throw DontAcceptDrop)
try {
DroppedSignal(retval, row);
if (0 <= dropped_row_original_index && dropped_row_original_index < static_cast<int>(m_rows.size()))
Erase(dropped_row_original_index, true);
} catch (const DontAcceptDrop&) {
// if there is a problem, silently undo the drop
if (dropped_row_original_index == -1) {
m_suppress_erase_signal = true;
Erase(retval);
m_suppress_erase_signal = false;
} else {
Erase(retval, true);
}
row->MoveTo(original_ul);
if (original_parent)
original_parent->AttachChild(row);
else
DetachChild(row);
throw; // re-throw so that AcceptDrop() knows to return false
}
} else {
InsertedSignal(retval, row);
}
return retval;
}
ListBox::Row* ListBox::Erase(int idx, bool removing_duplicate)
{
if (0 <= idx && idx < static_cast<int>(m_rows.size())) { // remove row
Row* row = m_rows[idx];
int row_height = row->Height();
m_rows.erase(m_rows.begin() + idx);
if (!removing_duplicate) {
DetachChild(row);
row->RemoveEventFilter(this);
}
// "bump" all the hiliting and positions up one row
m_selections.erase(idx);
for (unsigned int i = idx; i < m_rows.size(); ++i) {
m_rows[i]->OffsetMove(Pt(0, -row_height));
if (m_selections.find(i + 1) != m_selections.end()) {
m_selections.insert(i);
m_selections.erase(i + 1);
}
}
if (idx <= m_caret) // move caret up, if needed
--m_caret;
AdjustScrolls(false);
if (!removing_duplicate && !m_suppress_erase_signal)
ErasedSignal(idx, row);
return row;
} else {
return 0;
}
}
void ListBox::BringCaretIntoView()
{
BringRowIntoView(m_caret);
}
void ListBox::RecreateScrolls()
{
delete m_vscroll;
delete m_hscroll;
m_vscroll = m_hscroll = 0;
AdjustScrolls(false);
}
void ListBox::ResetAutoScrollVars()
{
m_auto_scrolling_up = false;
m_auto_scrolling_down = false;
m_auto_scrolling_left = false;
m_auto_scrolling_right = false;
m_auto_scroll_timer.Stop();
}
void ListBox::ConnectSignals()
{
if (m_vscroll)
Connect(m_vscroll->ScrolledSignal, &ListBox::VScrolled, this);
if (m_hscroll)
Connect(m_hscroll->ScrolledSignal, &ListBox::HScrolled, this);
}
void ListBox::ValidateStyle()
{
int dup_ct = 0; // duplication count
if (m_style & LIST_LEFT) ++dup_ct;
if (m_style & LIST_RIGHT) ++dup_ct;
if (m_style & LIST_CENTER) ++dup_ct;
if (dup_ct != 1) { // exactly one must be picked; when none or multiples are picked, use LIST_LEFT by default
m_style &= ~(LIST_RIGHT | LIST_CENTER);
m_style |= LIST_LEFT;
}
dup_ct = 0;
if (m_style & LIST_TOP) ++dup_ct;
if (m_style & LIST_BOTTOM) ++dup_ct;
if (m_style & LIST_VCENTER) ++dup_ct;
if (dup_ct != 1) { // exactly one must be picked; when none or multiples are picked, use LIST_VCENTER by default
m_style &= ~(LIST_TOP | LIST_BOTTOM);
m_style |= LIST_VCENTER;
}
dup_ct = 0;
if (m_style & LIST_NOSEL) ++dup_ct;
if (m_style & LIST_SINGLESEL) ++dup_ct;
if (m_style & LIST_QUICKSEL) ++dup_ct;
if (dup_ct > 1) // at most one of these may be picked; when multiples are picked, disable all of them
m_style &= ~(LIST_NOSEL | LIST_SINGLESEL | LIST_QUICKSEL);
}
void ListBox::AdjustScrolls(bool adjust_for_resize)
{
// this client area calculation disregards the thickness of scrolls
Pt cl_sz = (LowerRight() - Pt(BORDER_THICK, BORDER_THICK)) -
(UpperLeft() + Pt(BORDER_THICK, BORDER_THICK + (m_header_row->empty() ? 0 : m_header_row->Height())));
int total_x_extent = std::accumulate(m_col_widths.begin(), m_col_widths.end(), 0);
int total_y_extent = 0;
if (!m_rows.empty())
total_y_extent = m_rows.back()->LowerRight().y - m_rows.front()->UpperLeft().y;
bool vertical_needed =
m_rows.size() && (total_y_extent > cl_sz.y ||
(total_y_extent > cl_sz.y - SCROLL_WIDTH &&
total_x_extent > cl_sz.x - SCROLL_WIDTH));
bool horizontal_needed =
m_rows.size() && (total_x_extent > cl_sz.x ||
(total_x_extent > cl_sz.x - SCROLL_WIDTH &&
total_y_extent > cl_sz.y - SCROLL_WIDTH));
boost::shared_ptr<StyleFactory> style = GetStyleFactory();
if (m_vscroll) { // if scroll already exists...
if (!vertical_needed) { // remove scroll
DeleteChild(m_vscroll);
m_vscroll = 0;
} else { // ensure vertical scroll has the right logical and physical dimensions
int scroll_x = cl_sz.x - SCROLL_WIDTH;
int scroll_y = 0;
if (adjust_for_resize)
m_vscroll->SizeMove(Pt(scroll_x, scroll_y), Pt(scroll_x + SCROLL_WIDTH, scroll_y + cl_sz.y - (horizontal_needed ? SCROLL_WIDTH : 0)));
total_y_extent += VerticalScrollPadding(cl_sz.y);
m_vscroll->SizeScroll(0, total_y_extent - 1, cl_sz.y / 8, cl_sz.y - (horizontal_needed ? SCROLL_WIDTH : 0));
MoveChildUp(m_vscroll);
}
} else if (!m_vscroll && vertical_needed) { // if scroll doesn't exist but is needed
m_vscroll = style->NewListBoxVScroll(cl_sz.x - SCROLL_WIDTH, 0, SCROLL_WIDTH, cl_sz.y - (horizontal_needed ? SCROLL_WIDTH : 0), m_color, CLR_SHADOW);
total_y_extent += VerticalScrollPadding(cl_sz.y);
m_vscroll->SizeScroll(0, total_y_extent - 1, cl_sz.y / 8, cl_sz.y - (horizontal_needed ? SCROLL_WIDTH : 0));
AttachChild(m_vscroll);
Connect(m_vscroll->ScrolledSignal, &ListBox::VScrolled, this);
}
if (m_hscroll) { // if scroll already exists...
if (!horizontal_needed) { // remove scroll
DeleteChild(m_hscroll);
m_hscroll = 0;
} else { // ensure horizontal scroll has the right logical and physical dimensions
int scroll_x = 0;
int scroll_y = cl_sz.y - SCROLL_WIDTH;
if (adjust_for_resize)
m_hscroll->SizeMove(Pt(scroll_x, scroll_y), Pt(scroll_x + cl_sz.x - (vertical_needed ? SCROLL_WIDTH : 0), scroll_y + SCROLL_WIDTH));
total_x_extent += HorizontalScrollPadding(cl_sz.x);
m_hscroll->SizeScroll(0, total_x_extent - 1, cl_sz.x / 8, cl_sz.x - (vertical_needed ? SCROLL_WIDTH : 0));
MoveChildUp(m_hscroll);
}
} else if (!m_hscroll && horizontal_needed) { // if scroll doesn't exist but is needed
m_hscroll = style->NewListBoxHScroll(0, cl_sz.y - SCROLL_WIDTH, cl_sz.x - (vertical_needed ? SCROLL_WIDTH : 0), SCROLL_WIDTH, m_color, CLR_SHADOW);
total_x_extent += HorizontalScrollPadding(cl_sz.x);
m_hscroll->SizeScroll(0, total_x_extent - 1, cl_sz.x / 8, cl_sz.x - (vertical_needed ? SCROLL_WIDTH : 0));
AttachChild(m_hscroll);
Connect(m_hscroll->ScrolledSignal, &ListBox::HScrolled, this);
}
assert(!m_vscroll || m_vscroll->PageSize() == ClientHeight());
assert(!m_hscroll || m_hscroll->PageSize() == ClientWidth());
}
void ListBox::VScrolled(int tab_low, int tab_high, int low, int high)
{
m_first_row_shown = 0;
int accum = 0;
int position = 0;
for (unsigned int i = 0; i < m_rows.size(); ++i) {
int row_height = m_rows[i]->Height();
if (tab_low < accum + row_height / 2) {
m_first_row_shown = i;
position = -accum;
break;
}
accum += row_height;
}
int initial_x = m_rows.empty() ? 0 : m_rows[0]->RelativeUpperLeft().x;
for (unsigned int i = 0; i < m_rows.size(); ++i) {
m_rows[i]->MoveTo(Pt(initial_x, position));
position += m_rows[i]->Height();
}
}
void ListBox::HScrolled(int tab_low, int tab_high, int low, int high)
{
m_first_col_shown = 0;
int accum = 0;
int position = 0;
for (unsigned int i = 0; i < m_col_widths.size(); ++i) {
int col_width = m_col_widths[i];
if (tab_low < accum + col_width / 2) {
m_first_col_shown = i;
position = -accum;
break;
}
accum += col_width;
}
for (unsigned int i = 0; i < m_rows.size(); ++i) {
m_rows[i]->MoveTo(Pt(position, m_rows[i]->RelativeUpperLeft().y));
}
m_header_row->MoveTo(Pt(position, m_header_row->RelativeUpperLeft().y));
}
void ListBox::ClickAtRow(int row, Flags<ModKey> mod_keys)
{
std::set<int> previous_selections = m_selections;
if (m_style & LIST_SINGLESEL) { // no special keys are being used; just clear all previous selections, select this row, set the caret here
m_selections.clear();
m_selections.insert(row);
m_caret = row;
} else {
if (mod_keys & MOD_KEY_CTRL) { // control key depressed
if (mod_keys & MOD_KEY_SHIFT && m_caret != -1) { // both shift and control keys depressed
int low = m_caret < row ? m_caret : row;
int high = m_caret < row ? row : m_caret;
bool erase = m_selections.find(m_caret) == m_selections.end();
for (int i = low; i <= high; ++i) {
if (erase)
m_selections.erase(i);
else
m_selections.insert(i);
}
} else { // just the control key is depressed: toggle the item selected, adjust the caret
if (m_old_sel_row_selected)
m_selections.erase(row);
else
m_selections.insert(row);
m_caret = row;
}
} else if (mod_keys & MOD_KEY_SHIFT) { // shift key depressed
bool erase = m_selections.find(m_caret) == m_selections.end();
if (!(m_style & LIST_QUICKSEL))
m_selections.clear();
if (m_caret == -1) { // no previous caret exists; select this row, mark it as the caret
m_selections.insert(row);
m_caret = row;
} else { // select all rows between the caret and this row (inclusive), don't move the caret
int low = m_caret < row ? m_caret : row;
int high = m_caret < row ? row : m_caret;
for (int i = low; i <= high; ++i) {
if (erase)
m_selections.erase(i);
else
m_selections.insert(i);
}
}
} else { // unless LIST_QUICKSEL is used, this is treated just like LIST_SINGLESEL above
if (m_style & LIST_QUICKSEL) {
if (m_old_sel_row_selected)
m_selections.erase(row);
else
m_selections.insert(row);
m_caret = row;
} else {
m_selections.clear();
m_selections.insert(row);
m_caret = row;
}
}
}
if (previous_selections != m_selections)
SelChangedSignal(m_selections);
}
void ListBox::NormalizeRow(Row* row)
{
row->resize(m_col_widths.size());
row->SetColWidths(m_col_widths);
row->SetColAlignments(m_col_alignments);
row->SetMargin(m_cell_margin);
row->Resize(Pt(std::accumulate(m_col_widths.begin(), m_col_widths.end(), 0), row->Height()));
}
int ListBox::FirstRowShownWhenBottomIs(int bottom_row, int client_height)
{
if (bottom_row < 0)
return 0;
int available_space = client_height - m_rows[bottom_row]->Height();
int i = bottom_row;
while (0 < i && m_rows[i - 1]->Height() <= available_space) {
available_space -= m_rows[--i]->Height();
}
return i;
}
int ListBox::FirstColShownWhenRightIs(int right_col, int client_width)
{
if (right_col < 0)
return 0;
int available_space = client_width - m_col_widths[right_col];
int i = right_col;
while (0 < i && m_col_widths[i - 1] <= available_space) {
available_space -= m_col_widths[--i];
}
return i;
}
See more files for this project here