diff --git a/src/gui/control/tree_list.cpp b/src/gui/control/tree_list.cpp new file mode 100644 index 00000000..45b5a218 --- /dev/null +++ b/src/gui/control/tree_list.cpp @@ -0,0 +1,355 @@ +//+----------------------------------------------------------------------------+ +//| Description: Magic Set Editor - Program to make Magic (tm) cards | +//| Copyright: (C) 2001 - 2007 Twan van Laarhoven | +//| License: GNU General Public License 2 or later (see file COPYING) | +//+----------------------------------------------------------------------------+ + +// ----------------------------------------------------------------------------- : Includes + +#include +#include +#include +#include + +DECLARE_TYPEOF_COLLECTION(TreeList::Item); + +// ----------------------------------------------------------------------------- : TreeList : item managment + +void TreeList::rebuild(bool full) { + if (full) initItems(); + calcItemCount(); + UpdateScrollbar(); + Refresh(false); +} + +bool TreeList::hasChildren(size_t item) const { + return item < items.size() && items[item].level < items[item+1].level; +} + +void TreeList::expand(size_t item, bool expand) { + if (hasChildren(item) && items[item].expanded != expand) { + items[item].expanded = expand; + rebuild(false); + } +} + +void TreeList::select(size_t item) { + if (item >= items.size() || selection == item) return; + size_t oldpos = selection < items.size() ? items[selection].position : 0; + selection = item; + size_t pos = items[selection].position; + if (pos < first_line) { + ScrollToLine(pos); + } else if (pos >= first_line + visible_lines_t) { + ScrollToLine(pos - visible_lines_t + 1); + } else { + if (oldpos != NOTHING) RefreshLine(oldpos); + RefreshLine(pos); + } +} + +void TreeList::calcItemCount() { + // item count + total_lines = 0; + int visible_level = 0; + FOR_EACH(i,items) { + if (i.level <= visible_level) { + i.position = total_lines; + ++total_lines; + if (i.expanded) visible_level = i.level + 1; + else visible_level = i.level; + } else { + i.position = NOTHING; + } + } + // update lines + UInt lines = 0; + FOR_EACH_REVERSE(i,items) { + if (i.visible()) { + i.lines = lines; + lines &= (1 << i.level) - 1; + lines |= 1 << i.level; + } + } + // selection hidden? move to first visible item before it + if (selection < items.size()) { + for ( ; selection + 1 > 0 ; --selection) { + if (items[selection].visible()) break; // visible + } + if (selection >= items.size()) selection = 0; + } +} + +size_t TreeList::findItemByPos(int y) const { + if (y < header_height) return false; + return findItem(first_line + (y - header_height) / item_height); +} +size_t TreeList::findItem(size_t line, size_t start) const { + for (size_t i = start ; i < items.size() ; ++i) { + if (items[i].visible() && items[i].position >= line) return i; + } + return items.size(); +} +size_t TreeList::findLastItem(size_t start) const { + for (size_t i = min(items.size(), start) - 1 ; i + 1 > 0 ; --i) { + if (items[i].visible()) return i; + } + return items.size(); +} +size_t TreeList::findParent(size_t start) const { + int level = items[start].level; + for (size_t i = start - 1 ; i + 1 > 0 ; --i) { + if (items[i].visible() && items[i].level < level) return i; + } + return items.size(); +} + +// ----------------------------------------------------------------------------- : TreeList : UI + +TreeList::TreeList(Window* parent, int id, long style) + : wxPanel(parent, id, wxDefaultPosition, wxDefaultSize, style | wxWANTS_CHARS | wxVSCROLL) + , total_lines(0) + , first_line(0) + , selection(NOTHING) +{ + // determine item size + wxClientDC dc(this); + dc.SetFont(*wxNORMAL_FONT); + int h; + dc.GetTextExtent(_("X"), 0, &h); + item_height = h + 2; +} + +void TreeList::onPaint(wxPaintEvent& ev) { + wxBufferedPaintDC dc(this); + size_t cols = columnCount(); + wxRendererNative& rn = wxRendererNative::GetDefault(); + // clear background + wxSize cs = GetClientSize(); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.SetBrush(wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE)); + dc.DrawRectangle(0,0,cs.x,header_height); + dc.SetBrush(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + dc.DrawRectangle(0,header_height,cs.x,cs.y-header_height); + // draw header + dc.SetFont(*wxNORMAL_FONT); + dc.SetTextForeground(wxSystemSettings::GetColour(wxSYS_COLOUR_BTNTEXT)); + int x = 0, y = 0; + for (size_t j = 0 ; j < cols ; ++j) { + int w = columnWidth(j); + wxRect rect(x,0,w-1,header_height-1); + rn.DrawHeaderButton(this, dc, rect); + dc.DrawText(columnText(j),x+3,2); + x += w; + } + y += header_height; + // draw items + size_t start = findItem(first_line); + size_t end = findItem(first_line + visible_lines); + for (size_t i = start ; i < end ; ++i) { + const Item& item = items[i]; + if (!item.visible()) continue; // invisible + x = level_width * (item.level + 1); + // draw lines + dc.SetPen(lerp(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW), + wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT),0.4)); + for (int j = 0 ; j < 32 && j <= item.level ; ++j) { + if (item.lines & (1 << j)) { + // draw line + dc.DrawLine(8 + level_width*j,y,8 + level_width*j,y+item_height); + } + } + dc.DrawLine(x-1, y + item_height/2, x - 9, y + item_height/2); + if (!(item.lines & (1 << item.level))) { + dc.DrawLine(8 + level_width*item.level,y,8 + level_width*item.level,y+item_height/2+1); + } + // draw expand button + if (hasChildren(i)) { + wxRect rect(x - 13, y + (item_height - 9)/2, 9, 9); + rn.DrawTreeItemButton(this, dc, rect, item.expanded ? wxCONTROL_EXPANDED : 0); + } + if (selection == i) { + dc.SetPen(*wxTRANSPARENT_PEN); + dc.SetBrush(wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHT)); + dc.SetTextForeground(wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHTTEXT)); + dc.DrawRectangle(x, y, cs.x-x, item_height); + } + // draw text(s) + for (size_t j = 0 ; j < cols ; ++j) { + int w = columnWidth(j); + if (selection != i) + dc.SetTextForeground(itemColor(i,j)); + dc.DrawText(itemText(i,j),x+1,y+1); + if (j == 0) x = 0; + x += w; + } + y += item_height; + } +} + +void TreeList::onChar(wxKeyEvent& ev) { + switch (ev.GetKeyCode()) { + case WXK_UP: { + select(findLastItem(selection)); + break; + } case WXK_DOWN: { + select(findItem(0, selection + 1)); + break; + } case WXK_LEFT: { + if (selection < items.size()) { + if (hasChildren(selection) && items[selection].expanded) { + expand(selection, false); + } else { + // select parent + select(findParent(selection)); + } + } + break; + } case WXK_RIGHT: { + if (selection < items.size() && hasChildren(selection)) { + if (items[selection].expanded) { + // select first child + select(selection+1); + Refresh(false); + } else { + expand(selection, true); + } + } + break; + } case WXK_PAGEUP: { + ScrollToLine(first_line > visible_lines_t ? first_line - visible_lines_t : 0); + break; + } case WXK_PAGEDOWN: { + ScrollToLine(first_line + visible_lines_t); + break; + } case WXK_HOME: { + select(findItem(0)); + break; + } case WXK_END: { + select(findLastItem(items.size())); + break; + } case WXK_TAB: { + // send a navigation event to our parent, to select another control + // we need this because of wxWANTS_CHARS + wxNavigationKeyEvent nev; + nev.SetDirection(!ev.ShiftDown()); + GetParent()->ProcessEvent(nev); + break; + } + } +} + +void TreeList::onLeftDown(wxMouseEvent& ev) { + size_t i = findItemByPos(ev.GetY()); + if (i >= items.size()) return; + int left = items[i].level * level_width; + if (hasChildren(i) && ev.GetX() >= left && ev.GetX() < left + level_width) { + expand(i, !items[i].expanded); + } else { + select(i); + } + ev.Skip(); +} + +void TreeList::onLeftDClick(wxMouseEvent& ev) { + size_t i = findItemByPos(ev.GetY()); + if (i >= items.size()) return; + if (hasChildren(i)) { + expand(i, !items[i].expanded); + } + ev.Skip(); +} + +// ----------------------------------------------------------------------------- : TreeList : Copy of VScrolledWindow + +void TreeList::ScrollToLine(size_t line) { + // Based on VScrolledWindow::ScrollToLine + + // determine the real first line to scroll to: we shouldn't scroll beyond the end + line = (size_t)min((int)line, (int)(total_lines - visible_lines_t)); + + // nothing to do? + if (line == first_line) return; + + //%size_t first_line_old = first_line; + first_line = line; + + UpdateScrollbar(); + + Refresh(false); + /* + // finally refresh the display -- but only redraw as few lines as possible to avoid flicker + wxSize cs = GetClientSize(); + int dy = (int)(first_line_old - first_line) * item_height; + if (abs(dy) >= cs.y - header_height) { + Refresh(false); + } else { + wxRect rect(0, header_height, cs.x, cs.y - header_height); + ScrollWindow(0, dy, &rect); + }*/ +} + +void TreeList::UpdateScrollbar() { + // how many lines fit on the screen? + int h = GetClientSize().y - header_height; + visible_lines = (h + item_height - 1) / item_height; + visible_lines_t = h / item_height; + // set the scrollbar parameters to reflect this + SetScrollbar(wxVERTICAL, (int)first_line, (int)visible_lines_t, (int)total_lines); +} + +void TreeList::RefreshLine(size_t line) { + if (line < first_line || line >= first_line + visible_lines) return; + // calculate the rect occupied by this line on screen + wxRect rect; + rect.x = 0; + rect.y = header_height + (int)(line - first_line) * item_height; + rect.width = GetClientSize().x; + rect.height = item_height; + // do refresh it + RefreshRect(rect, false); +} + +void TreeList::onScroll(wxScrollWinEvent& ev) { + wxEventType type = ev.GetEventType(); + if (type == wxEVT_SCROLLWIN_TOP) { + ScrollToLine(0); + } else if (type == wxEVT_SCROLLWIN_BOTTOM) { + ScrollToLine(total_lines); + } else if (type == wxEVT_SCROLLWIN_LINEUP) { + ScrollToLine(first_line > 0 ? first_line - 1 : 0); + } else if (type == wxEVT_SCROLLWIN_LINEDOWN) { + ScrollToLine(first_line + 1); + } else if (type == wxEVT_SCROLLWIN_PAGEUP) { + ScrollToLine(first_line > visible_lines_t ? first_line - visible_lines_t : 0); + } else if (type == wxEVT_SCROLLWIN_PAGEDOWN) { + ScrollToLine(first_line + visible_lines_t); + } else { + ScrollToLine(ev.GetPosition()); + } +} + +void TreeList::onSize(wxSizeEvent& ev) { + UpdateScrollbar(); + ev.Skip(); +} + +void TreeList::onMouseWheel(wxMouseEvent& ev) { + ScrollLines(-ev.GetWheelRotation() * ev.GetLinesPerAction() / ev.GetWheelDelta()); + Refresh(false); +} + + +// ----------------------------------------------------------------------------- : TreeList : Event table + + +BEGIN_EVENT_TABLE(TreeList, wxPanel) + EVT_PAINT (TreeList::onPaint) + EVT_SIZE (TreeList::onSize) + EVT_SCROLLWIN (TreeList::onScroll) + EVT_CHAR (TreeList::onChar) + EVT_LEFT_DOWN (TreeList::onLeftDown) + EVT_LEFT_DCLICK(TreeList::onLeftDClick) + EVT_MOUSEWHEEL (TreeList::onMouseWheel) +END_EVENT_TABLE() + diff --git a/src/gui/control/tree_list.hpp b/src/gui/control/tree_list.hpp new file mode 100644 index 00000000..c830a697 --- /dev/null +++ b/src/gui/control/tree_list.hpp @@ -0,0 +1,107 @@ +//+----------------------------------------------------------------------------+ +//| Description: Magic Set Editor - Program to make Magic (tm) cards | +//| Copyright: (C) 2001 - 2007 Twan van Laarhoven | +//| License: GNU General Public License 2 or later (see file COPYING) | +//+----------------------------------------------------------------------------+ + +#ifndef HEADER_GUI_CONTROL_TREE_LIST +#define HEADER_GUI_CONTROL_TREE_LIST + +// ----------------------------------------------------------------------------- : Includes + +#include +#include + +typedef intrusive_ptr VoidP; + +// ----------------------------------------------------------------------------- : TreeList + +/// A combination of a TreeCtrl and a ListCtrl. A tree with multiple columns. +class TreeList : public wxPanel { + public: + TreeList(Window* parent, int id, long style = wxSUNKEN_BORDER); + + /// Expand/collapse an item + void expand(size_t item, bool expand = true); + /// Select an item + void select(size_t item); + + /// (re)build the list + void rebuild(bool full = true); + + public: + + /// An item in the tree list + struct Item { + Item() {} + Item(int level, bool expanded = false, VoidP data = VoidP()) + : level(level), expanded(expanded), data(data) + {} + + int level; + bool expanded; + VoidP data; + size_t position; // NOTHING if invisible, otherwise the line the item is on + UInt lines; // lines in front of this item (bit set) + inline bool visible() const { return position != NOTHING; } + }; + + protected: + + /// The items in the tree list + vector items; + + static const size_t NOTHING = (size_t)-1; + size_t selection; + + /// Initialize the items + virtual void initItems() = 0; + + /// Get the text of an item + virtual String itemText(size_t item, size_t column) const = 0; + /// Get the color of an item + virtual Color itemColor(size_t item, size_t column) const = 0; + + /// The number of columns + virtual size_t columnCount() const = 0; + /// The text of a column + virtual String columnText(size_t column) const = 0; + /// The width of a column in pixels + virtual int columnWidth(size_t column) const = 0; + + private: + int item_height; + static const int header_height = 17; + static const int level_width = 17; + + size_t total_lines; // number of shown items + size_t first_line; // first visible line + size_t visible_lines; // number of (partially) visible lines + size_t visible_lines_t; // number of totally visible lines + + void calcItemCount(); + size_t findItemByPos(int y) const; + size_t findItem(size_t line, size_t start = 0) const; + size_t findLastItem(size_t end) const; + size_t findParent(size_t item) const; + bool hasChildren(size_t item) const; + + DECLARE_EVENT_TABLE(); + + void onPaint(wxPaintEvent&); + void onChar(wxKeyEvent& ev); + void onLeftDown(wxMouseEvent& ev); + void onLeftDClick(wxMouseEvent& ev); + + // VScrolledWindow clone + + void onSize(wxSizeEvent& ev); + void onScroll(wxScrollWinEvent& ev); + void onMouseWheel(wxMouseEvent& ev); + void ScrollToLine(size_t line); + void UpdateScrollbar(); + void RefreshLine(size_t line); +}; + +// ----------------------------------------------------------------------------- : EOF +#endif