From 8966c16ad298f94be1f4542afa6b081a1d286eda Mon Sep 17 00:00:00 2001 From: Kahrl Date: Fri, 23 Aug 2013 12:24:11 +0200 Subject: Add formspec table --- src/CMakeLists.txt | 1 + src/guiFormSpecMenu.cpp | 322 +++++----- src/guiFormSpecMenu.h | 33 +- src/guiTable.cpp | 1212 +++++++++++++++++++++++++++++++++++++ src/guiTable.h | 269 ++++++++ src/script/lua_api/l_mainmenu.cpp | 22 +- src/script/lua_api/l_mainmenu.h | 2 + 7 files changed, 1667 insertions(+), 194 deletions(-) create mode 100644 src/guiTable.cpp create mode 100644 src/guiTable.h (limited to 'src') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4bc9f890c..26f8b6e29 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -374,6 +374,7 @@ set(minetest_SRCS guiMessageMenu.cpp guiTextInputMenu.cpp guiFormSpecMenu.cpp + guiTable.cpp guiPauseMenu.cpp guiPasswordChange.cpp guiVolumeChange.cpp diff --git a/src/guiFormSpecMenu.cpp b/src/guiFormSpecMenu.cpp index 6f98b3d4f..628ea3548 100644 --- a/src/guiFormSpecMenu.cpp +++ b/src/guiFormSpecMenu.cpp @@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include #include "guiFormSpecMenu.h" +#include "guiTable.h" #include "constants.h" #include "gamedef.h" #include "keycode.h" @@ -33,9 +34,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include #include -#include #include -#include #include #include "log.h" #include "tile.h" // ITextureSource @@ -83,10 +82,6 @@ GUIFormSpecMenu::GUIFormSpecMenu(irr::IrrlichtDevice* dev, m_selected_item(NULL), m_selected_amount(0), m_selected_dragging(false), - m_listbox_click_fname(), - m_listbox_click_index(-1), - m_listbox_click_time(0), - m_listbox_doubleclick(false), m_tooltip_element(NULL), m_allowclose(true), m_lock(false) @@ -142,7 +137,7 @@ void GUIFormSpecMenu::setInitialFocus() // Set initial focus according to following order of precedence: // 1. first empty editbox // 2. first editbox - // 3. first listbox + // 3. first table // 4. last button // 5. first focusable (not statictext, not tabheader) // 6. first child element @@ -177,10 +172,10 @@ void GUIFormSpecMenu::setInitialFocus() } } - // 3. first listbox + // 3. first table for (core::list::Iterator it = children.begin(); it != children.end(); ++it) { - if ((*it)->getType() == gui::EGUIET_LIST_BOX) { + if ((*it)->getTypeName() == std::string("GUITable")) { Environment->setFocus(*it); return; } @@ -212,86 +207,13 @@ void GUIFormSpecMenu::setInitialFocus() Environment->setFocus(*(children.begin())); } -int GUIFormSpecMenu::getListboxIndex(std::string listboxname) { - - std::wstring wlistboxname = narrow_to_wide(listboxname.c_str()); - - for(unsigned int i=0; i < m_listboxes.size(); i++) { - - std::wstring name(m_listboxes[i].first.fname.c_str()); - if ( name == wlistboxname) { - return m_listboxes[i].second->getSelected(); - } - } - return -1; -} - -bool GUIFormSpecMenu::checkListboxClick(std::wstring wlistboxname, - int eventtype) +GUITable* GUIFormSpecMenu::getTable(std::wstring tablename) { - // WARNING: BLACK IRRLICHT MAGIC - // Used to fix Irrlicht's subpar reporting of single clicks and double - // clicks in listboxes (gui::EGET_LISTBOX_CHANGED, - // gui::EGET_LISTBOX_SELECTED_AGAIN): - // 1. IGUIListBox::setSelected() is counted as a click. - // Including the initial setSelected() done by parseTextList(). - // 2. Clicking on a the selected item and then dragging for less - // than 500ms is counted as a doubleclick, no matter when the - // item was previously selected (e.g. more than 500ms ago) - - // So when Irrlicht reports a doubleclick, we need to check - // for ourselves if really was a doubleclick. Or just a fake. - - for(unsigned int i=0; i < m_listboxes.size(); i++) { - std::wstring name(m_listboxes[i].first.fname.c_str()); - int selected = m_listboxes[i].second->getSelected(); - if (name == wlistboxname && selected >= 0) { - u32 now = getTimeMs(); - bool doubleclick = - (eventtype == gui::EGET_LISTBOX_SELECTED_AGAIN) - && (name == m_listbox_click_fname) - && (selected == m_listbox_click_index) - && (m_listbox_click_time >= now - 500); - m_listbox_click_fname = name; - m_listbox_click_index = selected; - m_listbox_click_time = now; - m_listbox_doubleclick = doubleclick; - return true; - } - } - return false; -} - -gui::IGUIScrollBar* GUIFormSpecMenu::getListboxScrollbar( - gui::IGUIListBox *listbox) -{ - // WARNING: BLACK IRRLICHT MAGIC - // Ordinarily, due to how formspecs work (recreating the entire GUI - // when something changes), when you select an item in a textlist - // with more items than fit in the visible area, the newly selected - // item is scrolled to the bottom of the visible area. This is - // annoying and breaks GUI designs that use double clicks. - - // This function helps fixing this problem by giving direct access - // to a listbox's scrollbar. This works because CGUIListBox doesn't - // cache the scrollbar position anywhere. - - // If this stops working in a future irrlicht version, consider - // maintaining a local copy of irr::gui::CGUIListBox, possibly also - // fixing the other reasons why black irrlicht magic is needed. - - core::list children = listbox->getChildren(); - for(core::list::Iterator it = children.begin(); - it != children.end(); ++it) { - gui::IGUIElement* child = *it; - if (child && child->getType() == gui::EGUIET_SCROLL_BAR) { - return static_cast(child); - } + for (u32 i = 0; i < m_tables.size(); ++i) { + if (tablename == m_tables[i].first.fname) + return m_tables[i].second; } - - verbosestream<<"getListboxScrollbar: WARNING: " - <<"listbox has no scrollbar"< split(const std::string &s, char delim) { @@ -643,10 +565,40 @@ void GUIFormSpecMenu::parseBackground(parserData* data,std::string element) { errorstream<< "Invalid background element(" << parts.size() << "): '" << element << "'" << std::endl; } -void GUIFormSpecMenu::parseTextList(parserData* data,std::string element) { +void GUIFormSpecMenu::parseTableOptions(parserData* data,std::string element) { + std::vector parts = split(element,';'); + + data->table_options.clear(); + for (size_t i = 0; i < parts.size(); ++i) { + // Parse table option + std::string opt = unescape_string(parts[i]); + data->table_options.push_back(GUITable::splitOption(opt)); + } +} + +void GUIFormSpecMenu::parseTableColumns(parserData* data,std::string element) { std::vector parts = split(element,';'); - if ((parts.size() == 5) || (parts.size() == 6)) { + data->table_columns.clear(); + for (size_t i = 0; i < parts.size(); ++i) { + std::vector col_parts = split(parts[i],','); + GUITable::TableColumn column; + // Parse column type + if (!col_parts.empty()) + column.type = col_parts[0]; + // Parse column options + for (size_t j = 1; j < col_parts.size(); ++j) { + std::string opt = unescape_string(col_parts[j]); + column.options.push_back(GUITable::splitOption(opt)); + } + data->table_columns.push_back(column); + } +} + +void GUIFormSpecMenu::parseTable(parserData* data,std::string element) { + std::vector parts = split(element,';'); + + if ((parts.size() == 4) || (parts.size() == 5)) { std::vector v_pos = split(parts[0],','); std::vector v_geom = split(parts[1],','); std::string name = parts[2]; @@ -657,11 +609,8 @@ void GUIFormSpecMenu::parseTextList(parserData* data,std::string element) { if (parts.size() >= 5) str_initial_selection = parts[4]; - if (parts.size() >= 6) - str_transparent = parts[5]; - - MY_CHECKPOS("textlist",0); - MY_CHECKGEOM("textlist",1); + MY_CHECKPOS("table",0); + MY_CHECKGEOM("table",1); v2s32 pos = padding; pos.X += stof(v_pos[0]) * (float)spacing.X; @@ -683,63 +632,104 @@ void GUIFormSpecMenu::parseTextList(parserData* data,std::string element) { 258+m_fields.size() ); - spec.ftype = f_ListBox; + spec.ftype = f_Table; - //now really show list - gui::IGUIListBox *e = Environment->addListBox(rect, this,spec.fid); + for (unsigned int i = 0; i < items.size(); ++i) { + items[i] = unescape_string(items[i]); + } + + //now really show table + GUITable *e = new GUITable(Environment, this, spec.fid, rect, + m_tsrc); + e->drop(); // IGUIElement maintains the remaining reference if (spec.fname == data->focused_fieldname) { Environment->setFocus(e); } - if (str_transparent == "false") - e->setDrawBackground(true); + e->setTable(data->table_options, data->table_columns, items); - for (unsigned int i=0; i < items.size(); i++) { - if (items[i].c_str()[0] == '#') { - if (items[i].c_str()[1] == '#') { - e->addItem(narrow_to_wide(unescape_string(items[i])).c_str() +1); - } - else { - std::string color = items[i].substr(0,7); - std::wstring toadd = - narrow_to_wide(unescape_string(items[i]).c_str() + 7); + if (data->table_dyndata.find(fname_w) != data->table_dyndata.end()) { + e->setDynamicData(data->table_dyndata[fname_w]); + } - e->addItem(toadd.c_str()); + if ((str_initial_selection != "") && + (str_initial_selection != "0")) + e->setSelected(stoi(str_initial_selection.c_str())); - video::SColor tmp_color; + m_tables.push_back(std::pair(spec, e)); + m_fields.push_back(spec); + return; + } + errorstream<< "Invalid table element(" << parts.size() << "): '" << element << "'" << std::endl; +} - if (parseColor(color, tmp_color, false)) - e->setItemOverrideColor(i,tmp_color); - } - } - else { - e->addItem(narrow_to_wide(unescape_string(items[i])).c_str()); - } - } +void GUIFormSpecMenu::parseTextList(parserData* data,std::string element) { + std::vector parts = split(element,';'); - if (data->listbox_selections.find(fname_w) != data->listbox_selections.end()) { - e->setSelected(data->listbox_selections[fname_w]); + if ((parts.size() == 4) || (parts.size() == 5) || (parts.size() == 6)) { + std::vector v_pos = split(parts[0],','); + std::vector v_geom = split(parts[1],','); + std::string name = parts[2]; + std::vector items = split(parts[3],','); + std::string str_initial_selection = ""; + std::string str_transparent = "false"; + + if (parts.size() >= 5) + str_initial_selection = parts[4]; + + if (parts.size() >= 6) + str_transparent = parts[5]; + + MY_CHECKPOS("textlist",0); + MY_CHECKGEOM("textlist",1); + + v2s32 pos = padding; + pos.X += stof(v_pos[0]) * (float)spacing.X; + pos.Y += stof(v_pos[1]) * (float)spacing.Y; + + v2s32 geom; + geom.X = stof(v_geom[0]) * (float)spacing.X; + geom.Y = stof(v_geom[1]) * (float)spacing.Y; + + + core::rect rect = core::rect(pos.X, pos.Y, pos.X+geom.X, pos.Y+geom.Y); + + std::wstring fname_w = narrow_to_wide(name.c_str()); + + FieldSpec spec = FieldSpec( + fname_w, + L"", + L"", + 258+m_fields.size() + ); + + spec.ftype = f_Table; + + for (unsigned int i = 0; i < items.size(); ++i) { + items[i] = unescape_string(items[i]); } - if (data->listbox_scroll.find(fname_w) != data->listbox_scroll.end()) { - gui::IGUIScrollBar *scrollbar = getListboxScrollbar(e); - if (scrollbar) { - scrollbar->setPos(data->listbox_scroll[fname_w]); - } + //now really show list + GUITable *e = new GUITable(Environment, this, spec.fid, rect, + m_tsrc); + e->drop(); // IGUIElement maintains the remaining reference + + if (spec.fname == data->focused_fieldname) { + Environment->setFocus(e); } - else { - gui::IGUIScrollBar *scrollbar = getListboxScrollbar(e); - if (scrollbar) { - scrollbar->setPos(0); - } + + e->setTextList(items, is_yes(str_transparent)); + + if (data->table_dyndata.find(fname_w) != data->table_dyndata.end()) { + e->setDynamicData(data->table_dyndata[fname_w]); } if ((str_initial_selection != "") && (str_initial_selection != "0")) - e->setSelected(stoi(str_initial_selection.c_str())-1); + e->setSelected(stoi(str_initial_selection.c_str())); - m_listboxes.push_back(std::pair(spec,e)); + m_tables.push_back(std::pair(spec, e)); m_fields.push_back(spec); return; } @@ -1478,6 +1468,21 @@ void GUIFormSpecMenu::parseElement(parserData* data,std::string element) { return; } + if (type == "tableoptions"){ + parseTableOptions(data,description); + return; + } + + if (type == "tablecolumns"){ + parseTableColumns(data,description); + return; + } + + if (type == "table"){ + parseTable(data,description); + return; + } + if (type == "textlist"){ parseTextList(data,description); return; @@ -1550,20 +1555,11 @@ void GUIFormSpecMenu::regenerateGui(v2u32 screensize) { parserData mydata; - //preserve listboxes - for (unsigned int i = 0; i < m_listboxes.size(); i++) { - std::wstring listboxname = m_listboxes[i].first.fname; - gui::IGUIListBox *listbox = m_listboxes[i].second; - - int selection = listbox->getSelected(); - if (selection != -1) { - mydata.listbox_selections[listboxname] = selection; - } - - gui::IGUIScrollBar *scrollbar = getListboxScrollbar(listbox); - if (scrollbar) { - mydata.listbox_scroll[listboxname] = scrollbar->getPos(); - } + //preserve tables + for (u32 i = 0; i < m_tables.size(); ++i) { + std::wstring tablename = m_tables[i].first.fname; + GUITable *table = m_tables[i].second; + mydata.table_dyndata[tablename] = table->getDynamicData(); } //preserve focus @@ -1603,7 +1599,7 @@ void GUIFormSpecMenu::regenerateGui(v2u32 screensize) m_images.clear(); m_backgrounds.clear(); m_itemimages.clear(); - m_listboxes.clear(); + m_tables.clear(); m_checkboxes.clear(); m_fields.clear(); m_boxes.clear(); @@ -2175,17 +2171,12 @@ void GUIFormSpecMenu::acceptInput(bool quit=false) { fields[wide_to_narrow(s.fname.c_str())] = wide_to_narrow(s.flabel.c_str()); } - else if(s.ftype == f_ListBox) { - std::stringstream ss; - - if (m_listbox_doubleclick) { - ss << "DCL:"; - } - else { - ss << "CHG:"; + else if(s.ftype == f_Table) { + GUITable *table = getTable(s.fname); + if (table) { + fields[wide_to_narrow(s.fname.c_str())] + = table->checkEvent(); } - ss << (getListboxIndex(wide_to_narrow(s.fname.c_str()))+1); - fields[wide_to_narrow(s.fname.c_str())] = ss.str(); } else if(s.ftype == f_DropDown) { // no dynamic cast possible due to some distributions shipped @@ -2249,7 +2240,7 @@ void GUIFormSpecMenu::acceptInput(bool quit=false) bool GUIFormSpecMenu::preprocessEvent(const SEvent& event) { - // Fix Esc/Return key being eaten by checkboxen and listboxen + // Fix Esc/Return key being eaten by checkboxen and tables if(event.EventType==EET_KEY_INPUT_EVENT) { KeyPress kp(event.KeyInput); @@ -2706,8 +2697,7 @@ bool GUIFormSpecMenu::OnEvent(const SEvent& event) } } - if((event.GUIEvent.EventType==gui::EGET_LISTBOX_SELECTED_AGAIN) || - (event.GUIEvent.EventType==gui::EGET_LISTBOX_CHANGED)) + if(event.GUIEvent.EventType==gui::EGET_TABLE_CHANGED) { int current_id = event.GUIEvent.Caller->getID(); if(current_id > 257) @@ -2716,13 +2706,9 @@ bool GUIFormSpecMenu::OnEvent(const SEvent& event) for(u32 i=0; iOnEvent(event) : false; } -bool GUIFormSpecMenu::parseColor(std::string &value, video::SColor &color, bool quiet) +bool GUIFormSpecMenu::parseColor(const std::string &value, video::SColor &color, bool quiet) { const char *hexpattern = NULL; if (value[0] == '#') { diff --git a/src/guiFormSpecMenu.h b/src/guiFormSpecMenu.h index 8b0e50379..1946f88eb 100644 --- a/src/guiFormSpecMenu.h +++ b/src/guiFormSpecMenu.h @@ -27,6 +27,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "inventory.h" #include "inventorymanager.h" #include "modalMenu.h" +#include "guiTable.h" class IGameDef; class InventoryManager; @@ -34,7 +35,7 @@ class ISimpleTextureSource; typedef enum { f_Button, - f_ListBox, + f_Table, f_TabHeader, f_CheckBox, f_DropDown, @@ -231,7 +232,10 @@ public: bool preprocessEvent(const SEvent& event); bool OnEvent(const SEvent& event); - int getListboxIndex(std::string listboxname); + GUITable* getTable(std::wstring tablename); + + static bool parseColor(const std::string &value, + video::SColor &color, bool quiet); protected: v2s32 getBasePos() const @@ -260,7 +264,7 @@ protected: std::vector m_itemimages; std::vector m_boxes; std::vector m_fields; - std::vector > m_listboxes; + std::vector > m_tables; std::vector > m_checkboxes; ItemSpec *m_selected_item; @@ -273,12 +277,6 @@ protected: ItemStack m_selected_content_guess; InventoryLocation m_selected_content_guess_inventory; - // WARNING: BLACK IRRLICHT MAGIC, see checkListboxClick() - std::wstring m_listbox_click_fname; - int m_listbox_click_index; - u32 m_listbox_click_time; - bool m_listbox_doubleclick; - v2s32 m_pointer; gui::IGUIStaticText *m_tooltip_element; @@ -302,8 +300,10 @@ private: int bp_set; v2u32 screensize; std::wstring focused_fieldname; - std::map listbox_selections; - std::map listbox_scroll; + GUITable::TableOptions table_options; + GUITable::TableColumns table_columns; + // used to restore table selection/scroll/treeview state + std::map table_dyndata; } parserData; typedef struct { @@ -315,12 +315,6 @@ private: fs_key_pendig current_keys_pending; - // Determine whether listbox click was double click - // (Using some black Irrlicht magic) - bool checkListboxClick(std::wstring wlistboxname, int eventtype); - - gui::IGUIScrollBar* getListboxScrollbar(gui::IGUIListBox *listbox); - void parseElement(parserData* data,std::string element); void parseSize(parserData* data,std::string element); @@ -330,6 +324,9 @@ private: void parseItemImage(parserData* data,std::string element); void parseButton(parserData* data,std::string element,std::string typ); void parseBackground(parserData* data,std::string element); + void parseTableOptions(parserData* data,std::string element); + void parseTableColumns(parserData* data,std::string element); + void parseTable(parserData* data,std::string element); void parseTextList(parserData* data,std::string element); void parseDropDown(parserData* data,std::string element); void parsePwdField(parserData* data,std::string element); @@ -344,8 +341,6 @@ private: void parseBox(parserData* data,std::string element); void parseBackgroundColor(parserData* data,std::string element); void parseListColors(parserData* data,std::string element); - - bool parseColor(std::string &value, video::SColor &color, bool quiet); }; class FormspecFormSource: public IFormSource diff --git a/src/guiTable.cpp b/src/guiTable.cpp new file mode 100644 index 000000000..5febb8370 --- /dev/null +++ b/src/guiTable.cpp @@ -0,0 +1,1212 @@ +/* +Minetest +Copyright (C) 2013 celeron55, Perttu Ahola + +This program 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 program 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 program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + + +#include "guiTable.h" +#include +#include +#include +#include +#include +#include +#include +#include "debug.h" +#include "log.h" +#include "tile.h" +#include "gettime.h" +#include "util/string.h" +#include "util/numeric.h" +#include "guiFormSpecMenu.h" // for parseColor() + +/* + GUITable +*/ + +GUITable::GUITable(gui::IGUIEnvironment *env, + gui::IGUIElement* parent, s32 id, + core::rect rectangle, + ISimpleTextureSource *tsrc +): + gui::IGUIElement(gui::EGUIET_ELEMENT, env, parent, id, rectangle), + m_tsrc(tsrc), + m_is_textlist(false), + m_has_tree_column(false), + m_selected(-1), + m_sel_column(0), + m_sel_doubleclick(false), + m_keynav_time(0), + m_keynav_buffer(L""), + m_border(true), + m_color(255, 255, 255, 255), + m_background(255, 0, 0, 0), + m_highlight(255, 70, 100, 50), + m_highlight_text(255, 255, 255, 255), + m_rowheight(1), + m_font(NULL), + m_scrollbar(NULL) +{ + assert(tsrc != NULL); + + gui::IGUISkin* skin = Environment->getSkin(); + + m_font = skin->getFont(); + if (m_font) { + m_font->grab(); + m_rowheight = m_font->getDimension(L"A").Height + 4; + m_rowheight = MYMAX(m_rowheight, 1); + } + + const s32 s = skin->getSize(gui::EGDS_SCROLLBAR_SIZE); + m_scrollbar = Environment->addScrollBar(false, + core::rect(RelativeRect.getWidth() - s, + 0, + RelativeRect.getWidth(), + RelativeRect.getHeight()), + this, -1); + m_scrollbar->setSubElement(true); + m_scrollbar->setTabStop(false); + m_scrollbar->setAlignment(gui::EGUIA_LOWERRIGHT, gui::EGUIA_LOWERRIGHT, + gui::EGUIA_UPPERLEFT, gui::EGUIA_LOWERRIGHT); + m_scrollbar->setVisible(false); + m_scrollbar->setPos(0); + + setTabStop(true); + setTabOrder(-1); + updateAbsolutePosition(); +} + +GUITable::~GUITable() +{ + for (size_t i = 0; i < m_rows.size(); ++i) + delete[] m_rows[i].cells; + + if (m_font) + m_font->drop(); +} + +GUITable::Option GUITable::splitOption(const std::string &str) +{ + size_t equal_pos = str.find('='); + if (equal_pos == std::string::npos) + return GUITable::Option(str, ""); + else + return GUITable::Option(str.substr(0, equal_pos), + str.substr(equal_pos + 1)); +} + +void GUITable::setTextList(const std::vector &content, + bool transparent) +{ + clear(); + + if (transparent) { + m_background.setAlpha(0); + m_border = false; + } + + m_is_textlist = true; + + s32 empty_string_index = allocString(""); + + m_rows.resize(content.size()); + for (s32 i = 0; i < (s32) content.size(); ++i) { + Row *row = &m_rows[i]; + row->cells = new Cell[1]; + row->cellcount = 1; + row->indent = 0; + row->visible_index = i; + m_visible_rows.push_back(i); + + Cell *cell = row->cells; + cell->xmin = 0; + cell->xmax = 0x7fff; // something large enough + cell->xpos = 6; + cell->content_type = COLUMN_TYPE_TEXT; + cell->content_index = empty_string_index; + cell->tooltip_index = empty_string_index; + cell->color.set(255, 255, 255, 255); + cell->color_defined = false; + cell->reported_column = 1; + + // parse row content (color) + const std::string &s = content[i]; + if (s[0] == '#' && s[1] == '#') { + // double # to escape + cell->content_index = allocString(s.substr(2)); + } + else if (s[0] == '#' && s.size() >= 7 && + GUIFormSpecMenu::parseColor( + s.substr(0,7), cell->color, false)) { + // single # for color + cell->color_defined = true; + cell->content_index = allocString(s.substr(7)); + } + else { + // no #, just text + cell->content_index = allocString(s); + } + + } + + allocationComplete(); + + // Clamp scroll bar position + updateScrollBar(); +} + +void GUITable::setTable(const TableOptions &options, + const TableColumns &columns, + std::vector &content) +{ + clear(); + + // Naming conventions: + // i is always a row index, 0-based + // j is always a column index, 0-based + // k is another index, for example an option index + + // Handle table options + video::SColor default_color(255, 255, 255, 255); + s32 opendepth = 0; + for (size_t k = 0; k < options.size(); ++k) { + const std::string &name = options[k].name; + const std::string &value = options[k].value; + if (name == "color") + GUIFormSpecMenu::parseColor(value, m_color, false); + else if (name == "background") + GUIFormSpecMenu::parseColor(value, m_background, false); + else if (name == "border") + m_border = is_yes(value); + else if (name == "highlight") + GUIFormSpecMenu::parseColor(value, m_highlight, false); + else if (name == "highlight_text") + GUIFormSpecMenu::parseColor(value, m_highlight_text, false); + else if (name == "opendepth") + opendepth = stoi(value); + else + errorstream<<"Invalid table option: \""<= 1); + // rowcount = ceil(cellcount / colcount) but use integer arithmetic + s32 rowcount = (content.size() + colcount - 1) / colcount; + assert(rowcount >= 0); + // Append empty strings to content if there is an incomplete row + s32 cellcount = rowcount * colcount; + while (content.size() < (u32) cellcount) + content.push_back(""); + + // Create temporary rows (for processing columns) + struct TempRow { + // Current horizontal position (may different between rows due + // to indent/tree columns, or text/image columns with width<0) + s32 x; + // Tree indentation level + s32 indent; + // Next cell: Index into m_strings or m_images + s32 content_index; + // Next cell: Width in pixels + s32 content_width; + // Vector of completed cells in this row + std::vector cells; + // Stores colors and how long they last (maximum column index) + std::vector > colors; + + TempRow(): x(0), indent(0), content_index(0), content_width(0) {} + }; + TempRow *rows = new TempRow[rowcount]; + + // Get em width. Pedantically speaking, the width of "M" is not + // necessarily the same as the em width, but whatever, close enough. + s32 em = 6; + if (m_font) + em = m_font->getDimension(L"M").Width; + + s32 default_tooltip_index = allocString(""); + + std::map active_image_indices; + + // Process content in column-major order + for (s32 j = 0; j < colcount; ++j) { + // Check column type + ColumnType columntype = COLUMN_TYPE_TEXT; + if (columns[j].type == "text") + columntype = COLUMN_TYPE_TEXT; + else if (columns[j].type == "image") + columntype = COLUMN_TYPE_IMAGE; + else if (columns[j].type == "color") + columntype = COLUMN_TYPE_COLOR; + else if (columns[j].type == "indent") + columntype = COLUMN_TYPE_INDENT; + else if (columns[j].type == "tree") + columntype = COLUMN_TYPE_TREE; + else + errorstream<<"Invalid table column type: \"" + <colors.empty() && row->colors.back().second < j) + row->colors.pop_back(); + } + } + + // Make template for new cells + Cell newcell; + memset(&newcell, 0, sizeof newcell); + newcell.content_type = columntype; + newcell.tooltip_index = tooltip_index; + newcell.reported_column = j+1; + + if (columntype == COLUMN_TYPE_TEXT) { + // Find right edge of column + s32 xmax = 0; + for (s32 i = 0; i < rowcount; ++i) { + TempRow *row = &rows[i]; + row->content_index = allocString(content[i * colcount + j]); + const core::stringw &text = m_strings[row->content_index]; + row->content_width = m_font ? + m_font->getDimension(text.c_str()).Width : 0; + row->content_width = MYMAX(row->content_width, width); + s32 row_xmax = row->x + padding + row->content_width; + xmax = MYMAX(xmax, row_xmax); + } + // Add a new cell (of text type) to each row + for (s32 i = 0; i < rowcount; ++i) { + newcell.xmin = rows[i].x + padding; + alignContent(&newcell, xmax, rows[i].content_width, align); + newcell.content_index = rows[i].content_index; + newcell.color_defined = !rows[i].colors.empty(); + if (newcell.color_defined) + newcell.color = rows[i].colors.back().first; + rows[i].cells.push_back(newcell); + rows[i].x = newcell.xmax; + } + } + else if (columntype == COLUMN_TYPE_IMAGE) { + // Find right edge of column + s32 xmax = 0; + for (s32 i = 0; i < rowcount; ++i) { + TempRow *row = &rows[i]; + row->content_index = -1; + + // Find content_index. Image indices are defined in + // column options so check active_image_indices. + s32 image_index = stoi(content[i * colcount + j]); + std::map::iterator image_iter = + active_image_indices.find(image_index); + if (image_iter != active_image_indices.end()) + row->content_index = image_iter->second; + + // Get texture object (might be NULL) + video::ITexture *image = NULL; + if (row->content_index >= 0) + image = m_images[row->content_index]; + + // Get content width and update xmax + row->content_width = image ? image->getOriginalSize().Width : 0; + row->content_width = MYMAX(row->content_width, width); + s32 row_xmax = row->x + padding + row->content_width; + xmax = MYMAX(xmax, row_xmax); + } + // Add a new cell (of image type) to each row + for (s32 i = 0; i < rowcount; ++i) { + newcell.xmin = rows[i].x + padding; + alignContent(&newcell, xmax, rows[i].content_width, align); + newcell.content_index = rows[i].content_index; + rows[i].cells.push_back(newcell); + rows[i].x = newcell.xmax; + } + active_image_indices.clear(); + } + else if (columntype == COLUMN_TYPE_COLOR) { + for (s32 i = 0; i < rowcount; ++i) { + video::SColor cellcolor(255, 255, 255, 255); + if (GUIFormSpecMenu::parseColor(content[i * colcount + j], cellcolor, true)) + rows[i].colors.push_back(std::make_pair(cellcolor, j+span)); + } + } + else if (columntype == COLUMN_TYPE_INDENT || + columntype == COLUMN_TYPE_TREE) { + // For column type "tree", reserve additional space for +/- + // Also enable special processing for treeview-type tables + s32 content_width = 0; + if (columntype == COLUMN_TYPE_TREE) { + content_width = m_font ? m_font->getDimension(L"+").Width : 0; + m_has_tree_column = true; + } + // Add a new cell (of indent or tree type) to each row + for (s32 i = 0; i < rowcount; ++i) { + TempRow *row = &rows[i]; + + s32 indentlevel = stoi(content[i * colcount + j]); + indentlevel = MYMAX(indentlevel, 0); + if (columntype == COLUMN_TYPE_TREE) + row->indent = indentlevel; + + newcell.xmin = row->x + padding; + newcell.xpos = newcell.xmin + indentlevel * width; + newcell.xmax = newcell.xpos + content_width; + newcell.content_index = 0; + newcell.color_defined = !rows[i].colors.empty(); + if (newcell.color_defined) + newcell.color = rows[i].colors.back().first; + row->cells.push_back(newcell); + row->x = newcell.xmax; + } + } + } + + // Copy temporary rows to not so temporary rows + if (rowcount >= 1) { + m_rows.resize(rowcount); + for (s32 i = 0; i < rowcount; ++i) { + Row *row = &m_rows[i]; + row->cellcount = rows[i].cells.size(); + row->cells = new Cell[row->cellcount]; + memcpy((void*) row->cells, (void*) &rows[i].cells[0], + row->cellcount * sizeof(Cell)); + row->indent = rows[i].indent; + row->visible_index = i; + m_visible_rows.push_back(i); + } + } + + if (m_has_tree_column) { + // Treeview: convent tree to indent cells on leaf rows + for (s32 i = 0; i < rowcount; ++i) { + if (i == rowcount-1 || m_rows[i].indent >= m_rows[i+1].indent) + for (s32 j = 0; j < m_rows[i].cellcount; ++j) + if (m_rows[i].cells[j].content_type == COLUMN_TYPE_TREE) + m_rows[i].cells[j].content_type = COLUMN_TYPE_INDENT; + } + + // Treeview: close rows according to opendepth option + std::set opened_trees; + for (s32 i = 0; i < rowcount; ++i) + if (m_rows[i].indent < opendepth) + opened_trees.insert(i); + setOpenedTrees(opened_trees); + } + + // Delete temporary information used only during setTable() + delete[] rows; + allocationComplete(); + + // Clamp scroll bar position + updateScrollBar(); +} + +void GUITable::clear() +{ + // Clean up cells and rows + for (size_t i = 0; i < m_rows.size(); ++i) + delete[] m_rows[i].cells; + m_rows.clear(); + m_visible_rows.clear(); + + // Get colors from skin + gui::IGUISkin *skin = Environment->getSkin(); + m_color = skin->getColor(gui::EGDC_BUTTON_TEXT); + m_background = skin->getColor(gui::EGDC_3D_HIGH_LIGHT); + m_highlight = skin->getColor(gui::EGDC_HIGH_LIGHT); + m_highlight_text = skin->getColor(gui::EGDC_HIGH_LIGHT_TEXT); + + // Reset members + m_is_textlist = false; + m_has_tree_column = false; + m_selected = -1; + m_sel_column = 0; + m_sel_doubleclick = false; + m_keynav_time = 0; + m_keynav_buffer = L""; + m_border = true; + m_strings.clear(); + m_images.clear(); + m_alloc_strings.clear(); + m_alloc_images.clear(); +} + +std::string GUITable::checkEvent() +{ + s32 sel = getSelected(); + assert(sel >= 0); + + if (sel == 0) { + return "INV"; + } + + std::ostringstream os(std::ios::binary); + if (m_sel_doubleclick) { + os<<"DCL:"; + m_sel_doubleclick = false; + } + else { + os<<"CHG:"; + } + os<= 0 && m_selected < (s32) m_visible_rows.size()); + return m_visible_rows[m_selected] + 1; +} + +void GUITable::setSelected(s32 index) +{ + m_selected = -1; + m_sel_column = 0; + m_sel_doubleclick = false; + + --index; + + s32 rowcount = m_rows.size(); + + if (index >= rowcount) + index = rowcount - 1; + while (index >= 0 && m_rows[index].visible_index < 0) + --index; + if (index >= 0) { + m_selected = m_rows[index].visible_index; + assert(m_selected >= 0 && m_selected < (s32) m_visible_rows.size()); + } + + autoScroll(); +} + +GUITable::DynamicData GUITable::getDynamicData() const +{ + DynamicData dyndata; + dyndata.selected = getSelected(); + dyndata.scrollpos = m_scrollbar->getPos(); + dyndata.keynav_time = m_keynav_time; + dyndata.keynav_buffer = m_keynav_buffer; + if (m_has_tree_column) + getOpenedTrees(dyndata.opened_trees); + return dyndata; +} + +void GUITable::setDynamicData(const DynamicData &dyndata) +{ + if (m_has_tree_column) + setOpenedTrees(dyndata.opened_trees); + + m_keynav_time = dyndata.keynav_time; + m_keynav_buffer = dyndata.keynav_buffer; + + m_scrollbar->setPos(dyndata.scrollpos); + + setSelected(dyndata.selected); + m_sel_column = 0; + m_sel_doubleclick = false; +} + +const c8* GUITable::getTypeName() const +{ + return "GUITable"; +} + +void GUITable::updateAbsolutePosition() +{ + IGUIElement::updateAbsolutePosition(); + updateScrollBar(); +} + +void GUITable::draw() +{ + if (!IsVisible) + return; + + gui::IGUISkin *skin = Environment->getSkin(); + + // draw background + + bool draw_background = m_background.getAlpha() > 0; + if (m_border) + skin->draw3DSunkenPane(this, m_background, + true, draw_background, + AbsoluteRect, &AbsoluteClippingRect); + else if (draw_background) + skin->draw2DRectangle(this, m_background, + AbsoluteRect, &AbsoluteClippingRect); + + // get clipping rect + + core::rect client_clip(AbsoluteRect); + client_clip.UpperLeftCorner.Y += 1; + client_clip.UpperLeftCorner.X += 1; + client_clip.LowerRightCorner.Y -= 1; + client_clip.LowerRightCorner.X -= + m_scrollbar->isVisible() ? + skin->getSize(gui::EGDS_SCROLLBAR_SIZE) : + 1; + client_clip.clipAgainst(AbsoluteClippingRect); + + // draw visible rows + + s32 scrollpos = m_scrollbar->getPos(); + s32 row_min = scrollpos / m_rowheight; + s32 row_max = (scrollpos + AbsoluteRect.getHeight() - 1) + / m_rowheight + 1; + row_max = MYMIN(row_max, (s32) m_visible_rows.size()); + + core::rect row_rect(AbsoluteRect); + if (m_scrollbar->isVisible()) + row_rect.LowerRightCorner.X -= + skin->getSize(gui::EGDS_SCROLLBAR_SIZE); + row_rect.UpperLeftCorner.Y += row_min * m_rowheight - scrollpos; + row_rect.LowerRightCorner.Y = row_rect.UpperLeftCorner.Y + m_rowheight; + + for (s32 i = row_min; i < row_max; ++i) { + Row *row = &m_rows[m_visible_rows[i]]; + bool is_sel = i == m_selected; + video::SColor color = m_color; + + if (is_sel) { + skin->draw2DRectangle(this, m_highlight, row_rect, &client_clip); + color = m_highlight_text; + } + + for (s32 j = 0; j < row->cellcount; ++j) + drawCell(&row->cells[j], color, row_rect, client_clip); + + row_rect.UpperLeftCorner.Y += m_rowheight; + row_rect.LowerRightCorner.Y += m_rowheight; + } + + // Draw children + IGUIElement::draw(); +} + +void GUITable::drawCell(const Cell *cell, video::SColor color, + const core::rect &row_rect, + const core::rect &client_clip) +{ + if ((cell->content_type == COLUMN_TYPE_TEXT) + || (cell->content_type == COLUMN_TYPE_TREE)) { + + core::rect text_rect = row_rect; + text_rect.UpperLeftCorner.X = row_rect.UpperLeftCorner.X + + cell->xpos; + text_rect.LowerRightCorner.X = row_rect.UpperLeftCorner.X + + cell->xmax; + + if (cell->color_defined) + color = cell->color; + + if (m_font) { + if (cell->content_type == COLUMN_TYPE_TEXT) + m_font->draw(m_strings[cell->content_index], + text_rect, color, + false, true, &client_clip); + else // tree + m_font->draw(cell->content_index ? L"+" : L"-", + text_rect, color, + false, true, &client_clip); + } + } + else if (cell->content_type == COLUMN_TYPE_IMAGE) { + + if (cell->content_index < 0) + return; + + video::IVideoDriver *driver = Environment->getVideoDriver(); + video::ITexture *image = m_images[cell->content_index]; + + if (image) { + core::position2d dest_pos = + row_rect.UpperLeftCorner; + dest_pos.X += cell->xpos; + core::rect source_rect( + core::position2d(0, 0), + image->getOriginalSize()); + s32 imgh = source_rect.LowerRightCorner.Y; + s32 rowh = row_rect.getHeight(); + if (imgh < rowh) + dest_pos.Y += (rowh - imgh) / 2; + else + source_rect.LowerRightCorner.Y = rowh; + + video::SColor color(255, 255, 255, 255); + + driver->draw2DImage(image, dest_pos, source_rect, + &client_clip, color, true); + } + } +} + +bool GUITable::OnEvent(const SEvent &event) +{ + if (!isEnabled()) + return IGUIElement::OnEvent(event); + + if (event.EventType == EET_KEY_INPUT_EVENT) { + if (event.KeyInput.PressedDown && ( + event.KeyInput.Key == KEY_DOWN || + event.KeyInput.Key == KEY_UP || + event.KeyInput.Key == KEY_HOME || + event.KeyInput.Key == KEY_END || + event.KeyInput.Key == KEY_NEXT || + event.KeyInput.Key == KEY_PRIOR)) { + s32 offset = 0; + switch (event.KeyInput.Key) { + case KEY_DOWN: + offset = 1; + break; + case KEY_UP: + offset = -1; + break; + case KEY_HOME: + offset = - (s32) m_visible_rows.size(); + break; + case KEY_END: + offset = m_visible_rows.size(); + break; + case KEY_NEXT: + offset = AbsoluteRect.getHeight() / m_rowheight; + break; + case KEY_PRIOR: + offset = - (s32) (AbsoluteRect.getHeight() / m_rowheight); + break; + default: + break; + } + s32 old_selected = m_selected; + s32 rowcount = m_visible_rows.size(); + if (rowcount != 0) { + m_selected = rangelim(m_selected + offset, 0, rowcount-1); + autoScroll(); + } + + if (m_selected != old_selected) + sendTableEvent(0, false); + + return true; + } + else if (event.KeyInput.PressedDown && ( + event.KeyInput.Key == KEY_LEFT || + event.KeyInput.Key == KEY_RIGHT)) { + // Open/close subtree via keyboard + if (m_selected >= 0) { + int dir = event.KeyInput.Key == KEY_LEFT ? -1 : 1; + toggleVisibleTree(m_selected, dir, true); + } + return true; + } + else if (!event.KeyInput.PressedDown && ( + event.KeyInput.Key == KEY_RETURN || + event.KeyInput.Key == KEY_SPACE)) { + sendTableEvent(0, true); + return true; + } + else if (event.KeyInput.Key == KEY_ESCAPE || + event.KeyInput.Key == KEY_SPACE) { + // pass to parent + } + else if (event.KeyInput.PressedDown && event.KeyInput.Char) { + // change selection based on text as it is typed + s32 now = getTimeMs(); + if (now - m_keynav_time >= 500) + m_keynav_buffer = L""; + m_keynav_time = now; + + // add to key buffer if not a key repeat + if (!(m_keynav_buffer.size() == 1 && + m_keynav_buffer[0] == event.KeyInput.Char)) { + m_keynav_buffer.append(event.KeyInput.Char); + } + + // find the selected item, starting at the current selection + // dont change selection if the key buffer matches the current item + s32 old_selected = m_selected; + s32 start = MYMAX(m_selected, 0); + s32 rowcount = m_visible_rows.size(); + for (s32 k = 1; k < rowcount; ++k) { + s32 current = start + k; + if (current >= rowcount) + current -= rowcount; + if (doesRowStartWith(getRow(current), m_keynav_buffer)) { + m_selected = current; + break; + } + } + autoScroll(); + if (m_selected != old_selected) + sendTableEvent(0, false); + + return true; + } + } + if (event.EventType == EET_MOUSE_INPUT_EVENT) { + core::position2d p(event.MouseInput.X, event.MouseInput.Y); + + if (event.MouseInput.Event == EMIE_MOUSE_WHEEL) { + m_scrollbar->setPos(m_scrollbar->getPos() + + (event.MouseInput.Wheel < 0 ? -1 : 1) * + - (s32) m_rowheight / 2); + return true; + } + + // Find hovered row and cell + bool really_hovering = false; + s32 row_i = getRowAt(p.Y, really_hovering); + const Cell *cell = NULL; + if (really_hovering) { + s32 cell_j = getCellAt(p.X, row_i); + if (cell_j >= 0) + cell = &(getRow(row_i)->cells[cell_j]); + } + + // Update tooltip + setToolTipText(cell ? m_strings[cell->tooltip_index].c_str() : L""); + + if (event.MouseInput.isLeftPressed() && + (isPointInside(p) || + event.MouseInput.Event == EMIE_MOUSE_MOVED)) { + s32 sel_column = 0; + bool sel_doubleclick = (event.MouseInput.Event + == EMIE_LMOUSE_DOUBLE_CLICK); + bool plusminus_clicked = false; + + // For certain events (left click), report column + // Also open/close subtrees when the +/- is clicked + if (cell && ( + event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN || + event.MouseInput.Event == EMIE_LMOUSE_DOUBLE_CLICK || + event.MouseInput.Event == EMIE_LMOUSE_TRIPLE_CLICK)) { + sel_column = cell->reported_column; + if (cell->content_type == COLUMN_TYPE_TREE) + plusminus_clicked = true; + } + + if (plusminus_clicked) { + if (event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN) { + toggleVisibleTree(row_i, 0, false); + } + } + else { + // Normal selection + s32 old_selected = m_selected; + m_selected = row_i; + autoScroll(); + + if (m_selected != old_selected || + sel_column >= 1 || + sel_doubleclick) { + sendTableEvent(sel_column, sel_doubleclick); + } + } + } + return true; + } + if (event.EventType == EET_GUI_EVENT && + event.GUIEvent.EventType == gui::EGET_SCROLL_BAR_CHANGED && + event.GUIEvent.Caller == m_scrollbar) { + // Don't pass events from our scrollbar to the parent + return true; + } + + return IGUIElement::OnEvent(event); +} + +/******************************************************************************/ +/* GUITable helper functions */ +/******************************************************************************/ + +s32 GUITable::allocString(const std::string &text) +{ + std::map::iterator it = m_alloc_strings.find(text); + if (it == m_alloc_strings.end()) { + s32 id = m_strings.size(); + std::wstring wtext = narrow_to_wide(text); + m_strings.push_back(core::stringw(wtext.c_str())); + m_alloc_strings.insert(std::make_pair(text, id)); + return id; + } + else { + return it->second; + } +} + +s32 GUITable::allocImage(const std::string &imagename) +{ + std::map::iterator it = m_alloc_images.find(imagename); + if (it == m_alloc_images.end()) { + s32 id = m_images.size(); + m_images.push_back(m_tsrc->getTexture(imagename)); + m_alloc_images.insert(std::make_pair(imagename, id)); + return id; + } + else { + return it->second; + } +} + +void GUITable::allocationComplete() +{ + // Called when done with creating rows and cells from table data, + // i.e. when allocString and allocImage won't be called anymore + m_alloc_strings.clear(); + m_alloc_images.clear(); +} + +const GUITable::Row* GUITable::getRow(s32 i) const +{ + if (i >= 0 && i < (s32) m_visible_rows.size()) + return &m_rows[m_visible_rows[i]]; + else + return NULL; +} + +bool GUITable::doesRowStartWith(const Row *row, const core::stringw &str) const +{ + if (row == NULL) + return false; + + for (s32 j = 0; j < row->cellcount; ++j) { + Cell *cell = &row->cells[j]; + if (cell->content_type == COLUMN_TYPE_TEXT) { + const core::stringw &cellstr = m_strings[cell->content_index]; + if (cellstr.size() >= str.size() && + str.equals_ignore_case(cellstr.subString(0, str.size()))) + return true; + } + } + return false; +} + +s32 GUITable::getRowAt(s32 y, bool &really_hovering) const +{ + really_hovering = false; + + s32 rowcount = m_visible_rows.size(); + if (rowcount == 0) + return -1; + + // Use arithmetic to find row + s32 rel_y = y - AbsoluteRect.UpperLeftCorner.Y - 1; + s32 i = (rel_y + m_scrollbar->getPos()) / m_rowheight; + + if (i >= 0 && i < rowcount) { + really_hovering = true; + return i; + } + else if (i < 0) + return 0; + else + return rowcount - 1; + +} + +s32 GUITable::getCellAt(s32 x, s32 row_i) const +{ + const Row *row = getRow(row_i); + if (row == NULL) + return -1; + + // Use binary search to find cell in row + s32 rel_x = x - AbsoluteRect.UpperLeftCorner.X - 1; + s32 jmin = 0; + s32 jmax = row->cellcount - 1; + while (jmin < jmax) { + s32 pivot = jmin + (jmax - jmin) / 2; + assert(pivot >= 0 && pivot < row->cellcount); + const Cell *cell = &row->cells[pivot]; + + if (rel_x >= cell->xmin && rel_x <= cell->xmax) + return pivot; + else if (rel_x < cell->xmin) + jmax = pivot - 1; + else + jmin = pivot + 1; + } + + if (jmin >= 0 && jmin < row->cellcount && + rel_x >= row->cells[jmin].xmin && + rel_x <= row->cells[jmin].xmax) + return jmin; + else + return -1; +} + +void GUITable::autoScroll() +{ + if (m_selected >= 0) { + s32 pos = m_scrollbar->getPos(); + s32 maxpos = m_selected * m_rowheight; + s32 minpos = maxpos - (AbsoluteRect.getHeight() - m_rowheight); + if (pos > maxpos) + m_scrollbar->setPos(maxpos); + else if (pos < minpos) + m_scrollbar->setPos(minpos); + } +} + +void GUITable::updateScrollBar() +{ + s32 totalheight = m_rowheight * m_visible_rows.size(); + s32 scrollmax = MYMAX(0, totalheight - AbsoluteRect.getHeight()); + m_scrollbar->setVisible(scrollmax > 0); + m_scrollbar->setMax(scrollmax); + m_scrollbar->setSmallStep(m_rowheight); + m_scrollbar->setLargeStep(2 * m_rowheight); +} + +void GUITable::sendTableEvent(s32 column, bool doubleclick) +{ + m_sel_column = column; + m_sel_doubleclick = doubleclick; + if (Parent) { + SEvent e; + memset(&e, 0, sizeof e); + e.EventType = EET_GUI_EVENT; + e.GUIEvent.Caller = this; + e.GUIEvent.Element = 0; + e.GUIEvent.EventType = gui::EGET_TABLE_CHANGED; + Parent->OnEvent(e); + } +} + +void GUITable::getOpenedTrees(std::set &opened_trees) const +{ + opened_trees.clear(); + s32 rowcount = m_rows.size(); + for (s32 i = 0; i < rowcount - 1; ++i) { + if (m_rows[i].indent < m_rows[i+1].indent && + m_rows[i+1].visible_index != -2) + opened_trees.insert(i); + } +} + +void GUITable::setOpenedTrees(const std::set &opened_trees) +{ + s32 old_selected = getSelected(); + + std::vector parents; + std::vector closed_parents; + + m_visible_rows.clear(); + + for (size_t i = 0; i < m_rows.size(); ++i) { + Row *row = &m_rows[i]; + + // Update list of ancestors + while (!parents.empty() && m_rows[parents.back()].indent >= row->indent) + parents.pop_back(); + while (!closed_parents.empty() && + m_rows[closed_parents.back()].indent >= row->indent) + closed_parents.pop_back(); + + assert(closed_parents.size() <= parents.size()); + + if (closed_parents.empty()) { + // Visible row + row->visible_index = m_visible_rows.size(); + m_visible_rows.push_back(i); + } + else if (parents.back() == closed_parents.back()) { + // Invisible row, direct parent is closed + row->visible_index = -2; + } + else { + // Invisible row, direct parent is open, some ancestor is closed + row->visible_index = -1; + } + + // If not a leaf, add to parents list + if (i < m_rows.size()-1 && row->indent < m_rows[i+1].indent) { + parents.push_back(i); + + s32 content_index = 0; // "-", open + if (opened_trees.count(i) == 0) { + closed_parents.push_back(i); + content_index = 1; // "+", closed + } + + // Update all cells of type "tree" + for (s32 j = 0; j < row->cellcount; ++j) + if (row->cells[j].content_type == COLUMN_TYPE_TREE) + row->cells[j].content_index = content_index; + } + } + + updateScrollBar(); + + setSelected(old_selected); +} + +void GUITable::openTree(s32 to_open) +{ + std::set opened_trees; + getOpenedTrees(opened_trees); + opened_trees.insert(to_open); + setOpenedTrees(opened_trees); +} + +void GUITable::closeTree(s32 to_close) +{ + std::set opened_trees; + getOpenedTrees(opened_trees); + opened_trees.erase(to_close); + setOpenedTrees(opened_trees); +} + +// The following function takes a visible row index (hidden rows skipped) +// dir: -1 = left (close), 0 = auto (toggle), 1 = right (open) +void GUITable::toggleVisibleTree(s32 row_i, int dir, bool move_selection) +{ + // Check if the chosen tree is currently open + const Row *row = getRow(row_i); + if (row == NULL) + return; + + bool was_open = false; + for (s32 j = 0; j < row->cellcount; ++j) { + if (row->cells[j].content_type == COLUMN_TYPE_TREE) { + was_open = row->cells[j].content_index == 0; + break; + } + } + + // Check if the chosen tree should be opened + bool do_open = !was_open; + if (dir < 0) + do_open = false; + else if (dir > 0) + do_open = true; + + // Close or open the tree; the heavy lifting is done by setOpenedTrees + if (was_open && !do_open) + closeTree(m_visible_rows[row_i]); + else if (!was_open && do_open) + openTree(m_visible_rows[row_i]); + + // Change selected row if requested by caller, + // this is useful for keyboard navigation + if (move_selection) { + s32 sel = row_i; + if (was_open && do_open) { + // Move selection to first child + const Row *maybe_child = getRow(sel + 1); + if (maybe_child && maybe_child->indent > row->indent) + sel++; + } + else if (!was_open && !do_open) { + // Move selection to parent + assert(getRow(sel) != NULL); + while (sel > 0 && getRow(sel - 1)->indent >= row->indent) + sel--; + sel--; + if (sel < 0) // was root already selected? + sel = row_i; + } + if (sel != m_selected) { + m_selected = sel; + autoScroll(); + sendTableEvent(0, false); + } + } +} + +void GUITable::alignContent(Cell *cell, s32 xmax, s32 content_width, s32 align) +{ + // requires that cell.xmin, cell.xmax are properly set + // align = 0: left aligned, 1: centered, 2: right aligned, 3: inline + if (align == 0) { + cell->xpos = cell->xmin; + cell->xmax = xmax; + } + else if (align == 1) { + cell->xpos = (cell->xmin + xmax - content_width) / 2; + cell->xmax = xmax; + } + else if (align == 2) { + cell->xpos = xmax - content_width; + cell->xmax = xmax; + } + else { + // inline alignment: the cells of the column don't have an aligned + // right border, the right border of each cell depends on the content + cell->xpos = cell->xmin; + cell->xmax = cell->xmin + content_width; + } +} diff --git a/src/guiTable.h b/src/guiTable.h new file mode 100644 index 000000000..4d5b39166 --- /dev/null +++ b/src/guiTable.h @@ -0,0 +1,269 @@ +/* +Minetest +Copyright (C) 2013 celeron55, Perttu Ahola + +This program 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 program 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 program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + + +#ifndef GUITABLE_HEADER +#define GUITABLE_HEADER + +#include +#include +#include +#include +#include + +#include "irrlichttypes_extrabloated.h" + +class ISimpleTextureSource; + +/* + A table GUI element for GUIFormSpecMenu. + + Sends a EGET_TABLE_CHANGED event to the parent when + an item is selected or double-clicked. + Call checkEvent() to get info. + + Credits: The interface and implementation of this class are (very) + loosely based on the Irrlicht classes CGUITable and CGUIListBox. + CGUITable and CGUIListBox are licensed under the Irrlicht license; + they are Copyright (C) 2002-2012 Nikolaus Gebhardt +*/ +class GUITable : public gui::IGUIElement +{ +public: + /* + Stores dynamic data that should be preserved + when updating a formspec + */ + struct DynamicData + { + s32 selected; + s32 scrollpos; + s32 keynav_time; + core::stringw keynav_buffer; + std::set opened_trees; + + DynamicData() + { + selected = 0; + scrollpos = 0; + keynav_time = 0; + } + }; + + /* + An option of the form = + */ + struct Option + { + std::string name; + std::string value; + + Option(const std::string &name_, const std::string &value_) + { + name = name_; + value = value_; + } + }; + + /* + A list of options that concern the entire table + */ + typedef std::vector