Add formspec table
authorKahrl <kahrl@gmx.net>
Fri, 23 Aug 2013 10:24:11 +0000 (12:24 +0200)
committerShadowNinja <shadowninja@minetest.net>
Mon, 13 Jan 2014 23:11:08 +0000 (18:11 -0500)
13 files changed:
builtin/gamemgr.lua
builtin/mainmenu.lua
builtin/misc_helpers.lua
builtin/modmgr.lua
doc/lua_api.txt
doc/menu_lua_api.txt
src/CMakeLists.txt
src/guiFormSpecMenu.cpp
src/guiFormSpecMenu.h
src/guiTable.cpp [new file with mode: 0644]
src/guiTable.h [new file with mode: 0644]
src/script/lua_api/l_mainmenu.cpp
src/script/lua_api/l_mainmenu.h

index 7a5e9790fa0bc67c307e4b33a765b920c2b69d79..c99c2de21ab8e787cbd143e57d0065cb4991c832 100644 (file)
@@ -31,7 +31,7 @@ end
 --------------------------------------------------------------------------------
 function gamemgr.handle_games_buttons(fields)
        if fields["gamelist"] ~= nil then
-               local event = explode_textlist_event(fields["gamelist"])
+               local event = engine.explode_textlist_event(fields["gamelist"])
                gamemgr.selected_game = event.index
        end
        
index d8c2b63eceb682cbaeb3f0f1436fb416675c2f7e..8ef306354fcac7710579ac755013cabbfd4586b5 100644 (file)
@@ -459,8 +459,8 @@ function tabbuilder.handle_multiplayer_buttons(fields)
        end
 
        if fields["favourites"] ~= nil then
-               local event = explode_textlist_event(fields["favourites"])
-               if event.typ == "DCL" then
+               local event = engine.explode_textlist_event(fields["favourites"])
+               if event.type == "DCL" then
                        if event.index <= #menu.favorites then
                                gamedata.address = menu.favorites[event.index].address
                                gamedata.port = menu.favorites[event.index].port
@@ -484,7 +484,7 @@ function tabbuilder.handle_multiplayer_buttons(fields)
                        end
                end
 
-               if event.typ == "CHG" then
+               if event.type == "CHG" then
                        if event.index <= #menu.favorites then
                                local address = menu.favorites[event.index].address
                                local port = menu.favorites[event.index].port
@@ -586,12 +586,12 @@ function tabbuilder.handle_server_buttons(fields)
        local world_doubleclick = false
 
        if fields["srv_worlds"] ~= nil then
-               local event = explode_textlist_event(fields["srv_worlds"])
+               local event = engine.explode_textlist_event(fields["srv_worlds"])
 
-               if event.typ == "DCL" then
+               if event.type == "DCL" then
                        world_doubleclick = true
                end
-               if event.typ == "CHG" then
+               if event.type == "CHG" then
                        engine.setting_set("mainmenu_last_selected_world",
                                filterlist.get_raw_index(worldlist,engine.get_textlist_index("srv_worlds")))
                end
@@ -737,13 +737,13 @@ function tabbuilder.handle_singleplayer_buttons(fields)
        local world_doubleclick = false
 
        if fields["sp_worlds"] ~= nil then
-               local event = explode_textlist_event(fields["sp_worlds"])
+               local event = engine.explode_textlist_event(fields["sp_worlds"])
 
-               if event.typ == "DCL" then
+               if event.type == "DCL" then
                        world_doubleclick = true
                end
 
-               if event.typ == "CHG" then
+               if event.type == "CHG" then
                        engine.setting_set("mainmenu_last_selected_world",
                                filterlist.get_raw_index(worldlist,engine.get_textlist_index("sp_worlds")))
                end
@@ -813,8 +813,8 @@ end
 --------------------------------------------------------------------------------
 function tabbuilder.handle_texture_pack_buttons(fields)
        if fields["TPs"] ~= nil then
-               local event = explode_textlist_event(fields["TPs"])
-               if event.typ == "CHG" or event.typ=="DCL" then
+               local event = engine.explode_textlist_event(fields["TPs"])
+               if event.type == "CHG" or event.type == "DCL" then
                        local index = engine.get_textlist_index("TPs")
                        engine.setting_set("mainmenu_last_selected_TP",
                                index)
index 097c65865d186a9f2d2677e8a7260fd331f3fecf..a7a8f6b1c12ccc12a4d7e46c9fe1d6992e12b7b7 100644 (file)
@@ -115,26 +115,6 @@ function math.hypot(x, y)
        return x * math.sqrt(1 + t * t)
 end
 
---------------------------------------------------------------------------------
-function explode_textlist_event(text)
-       
-       local retval = {}
-       retval.typ = "INV"
-       
-       local parts = text:split(":")
-                               
-       if #parts == 2 then
-               retval.typ = parts[1]:trim()
-               retval.index= tonumber(parts[2]:trim())
-               
-               if type(retval.index) ~= "number" then
-                       retval.typ = "INV"
-               end
-       end
-       
-       return retval
-end
-
 --------------------------------------------------------------------------------
 function get_last_folder(text,count)
        local parts = text:split(DIR_DELIM)
@@ -368,6 +348,37 @@ if minetest then
        end
 end
 
+--------------------------------------------------------------------------------
+function tbl.explode_table_event(evt)
+       if evt ~= nil then
+               local parts = evt:split(":")
+               if #parts == 3 then
+                       local t = parts[1]:trim()
+                       local r = tonumber(parts[2]:trim())
+                       local c = tonumber(parts[3]:trim())
+                       if type(r) == "number" and type(c) == "number" and t ~= "INV" then
+                               return {type=t, row=r, column=c}
+                       end
+               end
+       end
+       return {type="INV", row=0, column=0}
+end
+
+--------------------------------------------------------------------------------
+function tbl.explode_textlist_event(evt)
+       if evt ~= nil then
+               local parts = evt:split(":")
+               if #parts == 2 then
+                       local t = parts[1]:trim()
+                       local r = tonumber(parts[2]:trim())
+                       if type(r) == "number" and t ~= "INV" then
+                               return {type=t, index=r}
+                       end
+               end
+       end
+       return {type="INV", index=0}
+end
+
 --------------------------------------------------------------------------------
 -- mainmenu only functions
 --------------------------------------------------------------------------------
index 13f81c6e01632263f37e7ba2470990ffada3fc49..f530ccc4a1e1b3b49495e9a0228b709e6647afa8 100644 (file)
@@ -572,7 +572,7 @@ function modmgr.handle_modmgr_buttons(fields)
                }
 
        if fields["modlist"] ~= nil then
-               local event = explode_textlist_event(fields["modlist"])
+               local event = engine.explode_textlist_event(fields["modlist"])
                modmgr.selected_mod = event.index
        end
 
@@ -693,10 +693,10 @@ end
 --------------------------------------------------------------------------------
 function modmgr.handle_configure_world_buttons(fields)
        if fields["world_config_modlist"] ~= nil then
-               local event = explode_textlist_event(fields["world_config_modlist"])
+               local event = engine.explode_textlist_event(fields["world_config_modlist"])
                modmgr.world_config_selected_mod = event.index
 
-               if event.typ == "DCL" then
+               if event.type == "DCL" then
                        modmgr.world_config_enable_mod(nil)
                end
        end
index 1fae0ebbf7a26bf3c6448b508d81b1b108e515f7..8bd83995e67d53a3b50cc2ca888bbbf354ccda62 100644 (file)
@@ -1011,6 +1011,7 @@ textlist[<X>,<Y>;<W>,<H>;<name>;<listelem 1>,<listelem 2>,...,<listelem n>;<sele
 ^    if you want a listelement to start with # write ##
 ^ index to be selected within textlist
 ^ true/false draw transparent background
+^ see also minetest.explode_textlist_event (main menu: engine.explode_textlist_event)
 
 tabheader[<X>,<Y>;<name>;<caption 1>,<caption 2>,...,<caption n>;<current_tab>;<transparent>;<draw_border>]
 ^ show a tabHEADER at specific position (ignores formsize)
@@ -1043,6 +1044,57 @@ checkbox[<X>,<Y>;<name>;<label>;<selected>]
 ^ label to be shown left of checkbox
 ^ selected (optional) true/false
 
+table[<X>,<Y>;<W>,<H>;<name>;<cell 1>,<cell 2>,...,<cell n>;<selected idx>]
+^ show scrollable table using options defined by the previous tableoptions[]
+^ displays cells as defined by the previous tablecolumns[]
+^ x and y position the itemlist relative to the top left of the menu
+^ w and h are the size of the itemlist
+^ name fieldname sent to server on row select or doubleclick
+^ cell 1...n cell contents given in row-major order
+^ selected idx: index of row to be selected within table (first row = 1)
+^ see also minetest.explode_table_event (main menu: engine.explode_table_event)
+
+tableoptions[<opt 1>;<opt 2>;...]
+^ sets options for table[]:
+^ color=#RRGGBB
+^^ default text color (HEX-Color), defaults to #FFFFFF
+^ background=#RRGGBB
+^^ table background color (HEX-Color), defaults to #000000
+^ border=<true/false>
+^^ should the table be drawn with a border? (default true)
+^ highlight=#RRGGBB
+^^ highlight background color (HEX-Color), defaults to #466432
+^ highlight_text=#RRGGBB
+^^ highlight text color (HEX-Color), defaults to #FFFFFF
+^ opendepth=<value>
+^^ all subtrees up to depth < value are open (default value = 0)
+^^ only useful when there is a column of type "tree"
+
+tablecolumns[<type 1>,<opt 1a>,<opt 1b>,...;<type 2>,<opt 2a>,<opt 2b>;...]
+^ sets columns for table[]:
+^ types: text, image, color, indent, tree
+^^ text:   show cell contents as text
+^^ image:  cell contents are an image index, use column options to define images
+^^ color:  cell contents are a HEX-Color and define color of following cell
+^^ indent: cell contents are a number and define indentation of following cell
+^^ tree:   same as indent, but user can open and close subtrees (treeview-like)
+^ column options:
+^^    align=<value>   for "text" and "image": content alignment within cells
+^^                    available values: left (default), center, right, inline
+^^    width=<value>   for "text" and "image": minimum width in em (default 0)
+^^                    for "indent" and "tree": indent width in em (default 1.5)
+^^    padding=<value> padding left of the column, in em (default 0.5)
+^^                    exception: defaults to 0 for indent columns
+^^    tooltip=<value> tooltip text (default empty)
+^ "image" column options:
+^^    0=<value>       sets image for image index 0
+^^    1=<value>       sets image for image index 1
+^^    2=<value>       sets image for image index 2
+^^                    and so on; defined indices need not be contiguous
+^^                    empty or non-numeric cells are treated as 0
+^ "color" column options:
+^^    span=<value>    number of following columns to affect (default infinite)
+
 Note: do NOT use a element name starting with "key_" those names are reserved to
 pass key press events to formspec! 
 
@@ -1346,11 +1398,21 @@ minetest.get_inventory(location) -> InvRef
 minetest.create_detached_inventory(name, callbacks) -> InvRef
 ^ callbacks: See "Detached inventory callbacks"
 ^ Creates a detached inventory. If it already exists, it is cleared.
+
+Formspec:
 minetest.show_formspec(playername, formname, formspec)
 ^ playername: name of player to show formspec
 ^ formname: name passed to on_player_receive_fields callbacks
 ^           should follow "modname:<whatever>" naming convention
 ^ formspec: formspec to display
+minetest.formspec_escape(string) -> string
+^ escapes characters [ ] \ , ; that can not be used in formspecs
+minetest.explode_table_event(string) -> table
+^ returns e.g. {type="CHG", row=1, column=2}
+^ type: "INV" (no row selected), "CHG" (selected) or "DCL" (double-click)
+minetest.explode_textlist_event(string) -> table
+^ returns e.g. {type="CHG", index=1}
+^ type: "INV" (no row selected), "CHG" (selected) or "DCL" (double-click)
 
 Item handling:
 minetest.inventorycube(img1, img2, img3)
index b50e4388766b875b982797f78b88608abacecfde..87e37dd0d8bb5629088a1fefdadab1d213a10d85 100644 (file)
@@ -89,12 +89,33 @@ engine.sound_play(spec, looped) -> handle
 ^ looped = bool
 engine.sound_stop(handle)
 
-GUI:
+Formspec:
 engine.update_formspec(formspec)
-- engine.set_background(type, texturepath)
+engine.get_table_index(tablename) -> index
+^ can also handle textlists
+engine.formspec_escape(string) -> string
+^ escapes characters [ ] \ , ; that can not be used in formspecs
+engine.explode_table_event(string) -> table
+^ returns e.g. {type="CHG", row=1, column=2}
+^ type: "INV" (no row selected), "CHG" (selected) or "DCL" (double-click)
+engine.explode_textlist_event(string) -> table
+^ returns e.g. {type="CHG", index=1}
+^ type: "INV" (no row selected), "CHG" (selected) or "DCL" (double-click)
+
+GUI:
+engine.set_background(type, texturepath)
 ^ type: "background", "overlay", "header" or "footer"
 engine.set_clouds(<true/false>)
 engine.set_topleft_text(text)
+engine.show_keys_menu()
+engine.file_open_dialog(formname,caption)
+^ shows a file open dialog
+^ formname is base name of dialog response returned in fields
+^     -if dialog was accepted "_accepted"
+^^       will be added to fieldname containing the path
+^     -if dialog was canceled "_cancelled"
+^        will be added to fieldname value is set to formname itself
+^ returns nil or selected file/folder
 
 Games:
 engine.get_game(index)
@@ -155,22 +176,7 @@ engine.get_worlds() -> list of worlds (possible in async calls)
 engine.create_world(worldname, gameid)
 engine.delete_world(index)
 
-
-UI:
-engine.get_textlist_index(textlistname) -> index
-engine.show_keys_menu()
-engine.file_open_dialog(formname,caption)
-^ shows a file open dialog
-^ formname is base name of dialog response returned in fields
-^     -if dialog was accepted "_accepted" 
-^^       will be added to fieldname containing the path
-^     -if dialog was canceled "_cancelled" 
-^        will be added to fieldname value is set to formname itself
-^ returns nil or selected file/folder
-
 Helpers:
-engine.formspec_escape(string) -> string
-^ escapes characters [ ] \ , ; that can not be used in formspecs
 engine.gettext(string) -> string
 ^ look up the translation of a string in the gettext message catalog
 fgettext(string, ...) -> string
index 4bc9f890c92bfe57dac0d9126c677ff23e0ae157..26f8b6e293269a8f21c664265d9012188014db54 100644 (file)
@@ -374,6 +374,7 @@ set(minetest_SRCS
        guiMessageMenu.cpp
        guiTextInputMenu.cpp
        guiFormSpecMenu.cpp
+       guiTable.cpp
        guiPauseMenu.cpp
        guiPasswordChange.cpp
        guiVolumeChange.cpp
index 6f98b3d4fd8425bf8cf95ff096753da7ec4ce321..628ea35487139d16b83cea6e1d7a4053fcce1081 100644 (file)
@@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include <sstream>
 #include <limits>
 #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 <IGUIButton.h>
 #include <IGUIStaticText.h>
 #include <IGUIFont.h>
-#include <IGUIListBox.h>
 #include <IGUITabControl.h>
-#include <IGUIScrollBar.h>
 #include <IGUIComboBox.h>
 #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<gui::IGUIElement*>::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<gui::IGUIElement*> children = listbox->getChildren();
-       for(core::list<gui::IGUIElement*>::Iterator it = children.begin();
-                       it != children.end(); ++it) {
-               gui::IGUIElement* child = *it;
-               if (child && child->getType() == gui::EGUIET_SCROLL_BAR) {
-                       return static_cast<gui::IGUIScrollBar*>(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"<<std::endl;
-       return NULL;
+       return 0;
 }
 
 std::vector<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> parts = split(element,';');
+
+       if ((parts.size() == 4) || (parts.size() == 5)) {
                std::vector<std::string> v_pos = split(parts[0],',');
                std::vector<std::string> 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<FieldSpec,GUITable*>(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<std::string> 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<std::string> v_pos = split(parts[0],',');
+               std::vector<std::string> v_geom = split(parts[1],',');
+               std::string name = parts[2];
+               std::vector<std::string> 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<s32> rect = core::rect<s32>(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<FieldSpec,gui::IGUIListBox*>(spec,e));
+               m_tables.push_back(std::pair<FieldSpec,GUITable*>(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; i<m_fields.size(); i++)
                                {
                                        FieldSpec &s = m_fields[i];
-                                       // if its a listbox, set the send field so
-                                       // lua knows which listbox was changed
-                                       // checkListboxClick() is black magic
-                                       // for properly handling double clicks
-                                       if ((s.ftype == f_ListBox) && (s.fid == current_id)
-                                                       && checkListboxClick(s.fname,
-                                                               event.GUIEvent.EventType))
+                                       // if it's a table, set the send field
+                                       // so lua knows which table was changed
+                                       if ((s.ftype == f_Table) && (s.fid == current_id))
                                        {
                                                s.send = true;
                                                acceptInput();
@@ -2737,7 +2723,7 @@ bool GUIFormSpecMenu::OnEvent(const SEvent& event)
        return Parent ? Parent->OnEvent(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] == '#') {
index 8b0e50379f86b27b421d85e39c7b26ca5bed6cfd..1946f88ebc3ba0c635e8c3261c977bd1b4a50326 100644 (file)
@@ -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<ImageDrawSpec> m_itemimages;
        std::vector<BoxDrawSpec> m_boxes;
        std::vector<FieldSpec> m_fields;
-       std::vector<std::pair<FieldSpec,gui::IGUIListBox*> > m_listboxes;
+       std::vector<std::pair<FieldSpec,GUITable*> > m_tables;
        std::vector<std::pair<FieldSpec,gui::IGUICheckBox*> > 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<std::wstring,int> listbox_selections;
-               std::map<std::wstring,int> listbox_scroll;
+               GUITable::TableOptions table_options;
+               GUITable::TableColumns table_columns;
+               // used to restore table selection/scroll/treeview state
+               std::map<std::wstring,GUITable::DynamicData> 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 (file)
index 0000000..5febb83
--- /dev/null
@@ -0,0 +1,1212 @@
+/*
+Minetest
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 <queue>
+#include <sstream>
+#include <utility>
+#include <string.h>
+#include <IGUISkin.h>
+#include <IGUIFont.h>
+#include <IGUIScrollBar.h>
+#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<s32> 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<s32>(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<std::string> &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<std::string> &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: \""<<name<<"\""
+                               <<" (value=\""<<value<<"\")"<<std::endl;
+       }
+
+       // Get number of columns and rows
+       // note: error case columns.size() == 0 was handled above
+       s32 colcount = columns.size();
+       assert(colcount >= 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<Cell> cells;
+               // Stores colors and how long they last (maximum column index)
+               std::vector<std::pair<video::SColor, s32> > 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<s32, s32> 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: \""
+                               <<columns[j].type<<"\""<<std::endl;
+
+               // Process column options
+               s32 padding = myround(0.5 * em);
+               s32 tooltip_index = default_tooltip_index;
+               s32 align = 0;
+               s32 width = 0;
+               s32 span = colcount;
+
+               if (columntype == COLUMN_TYPE_INDENT) {
+                       padding = 0; // default indent padding
+               }
+               if (columntype == COLUMN_TYPE_INDENT ||
+                               columntype == COLUMN_TYPE_TREE) {
+                       width = myround(em * 1.5); // default indent width
+               }
+
+               for (size_t k = 0; k < columns[j].options.size(); ++k) {
+                       const std::string &name = columns[j].options[k].name;
+                       const std::string &value = columns[j].options[k].value;
+                       if (name == "padding")
+                               padding = myround(stof(value) * em);
+                       else if (name == "tooltip")
+                               tooltip_index = allocString(value);
+                       else if (name == "align" && value == "left")
+                               align = 0;
+                       else if (name == "align" && value == "center")
+                               align = 1;
+                       else if (name == "align" && value == "right")
+                               align = 2;
+                       else if (name == "align" && value == "inline")
+                               align = 3;
+                       else if (name == "width")
+                               width = myround(stof(value) * em);
+                       else if (name == "span" && columntype == COLUMN_TYPE_COLOR)
+                               span = stoi(value);
+                       else if (columntype == COLUMN_TYPE_IMAGE &&
+                                       !name.empty() &&
+                                       string_allowed(name, "0123456789")) {
+                               s32 content_index = allocImage(value);
+                               active_image_indices.insert(std::make_pair(
+                                                       stoi(name),
+                                                       content_index));
+                       }
+                       else {
+                               errorstream<<"Invalid table column option: \""<<name<<"\""
+                                       <<" (value=\""<<value<<"\")"<<std::endl;
+                       }
+               }
+
+               // If current column type can use information from "color" columns,
+               // find out which of those is currently active
+               if (columntype == COLUMN_TYPE_TEXT) {
+                       for (s32 i = 0; i < rowcount; ++i) {
+                               TempRow *row = &rows[i];
+                               while (!row->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<s32, s32>::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<s32> 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<<sel;
+       if (!m_is_textlist) {
+               os<<":"<<m_sel_column;
+       }
+       return os.str();
+}
+
+s32 GUITable::getSelected() const
+{
+       if (m_selected < 0)
+               return 0;
+
+       assert(m_selected >= 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<s32> 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<s32> 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<s32> &row_rect,
+               const core::rect<s32> &client_clip)
+{
+       if ((cell->content_type == COLUMN_TYPE_TEXT)
+                       || (cell->content_type == COLUMN_TYPE_TREE)) {
+
+               core::rect<s32> 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<s32> dest_pos =
+                                       row_rect.UpperLeftCorner;
+                       dest_pos.X += cell->xpos;
+                       core::rect<s32> source_rect(
+                                       core::position2d<s32>(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<s32> 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<std::string, s32>::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<std::string, s32>::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<s32> &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<s32> &opened_trees)
+{
+       s32 old_selected = getSelected();
+
+       std::vector<s32> parents;
+       std::vector<s32> 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<s32> opened_trees;
+       getOpenedTrees(opened_trees);
+       opened_trees.insert(to_open);
+       setOpenedTrees(opened_trees);
+}
+
+void GUITable::closeTree(s32 to_close)
+{
+       std::set<s32> 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 (file)
index 0000000..4d5b391
--- /dev/null
@@ -0,0 +1,269 @@
+/*
+Minetest
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 <map>
+#include <set>
+#include <string>
+#include <vector>
+#include <iostream>
+
+#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<s32> opened_trees;
+
+               DynamicData()
+               {
+                       selected = 0;
+                       scrollpos = 0;
+                       keynav_time = 0;
+               }
+       };
+
+       /*
+               An option of the form <name>=<value>
+       */
+       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<Option> TableOptions;
+
+       /*
+               A column with options
+       */
+       struct TableColumn
+       {
+               std::string type;
+               std::vector<Option> options;
+       };
+       typedef std::vector<TableColumn> TableColumns;
+
+
+       GUITable(gui::IGUIEnvironment *env,
+                       gui::IGUIElement *parent, s32 id,
+                       core::rect<s32> rectangle,
+                       ISimpleTextureSource *tsrc);
+
+       virtual ~GUITable();
+
+       /* Split a string of the form "name=value" into name and value */
+       static Option splitOption(const std::string &str);
+
+       /* Set textlist-like options, columns and data */
+       void setTextList(const std::vector<std::string> &content,
+                       bool transparent);
+
+       /* Set generic table options, columns and content */
+       // Adds empty strings to end of content if there is an incomplete row
+       void setTable(const TableOptions &options,
+                       const TableColumns &columns,
+                       std::vector<std::string> &content);
+
+       /* Clear the table */
+       void clear();
+
+       /* Get info about last event (string such as "CHG:1:2") */
+       // Call this after EGET_TABLE_CHANGED
+       std::string checkEvent();
+
+       /* Get index of currently selected row (first=1; 0 if none selected) */
+       s32 getSelected() const;
+
+       /* Set currently selected row (first=1; 0 if none selected) */
+       // If given index is not visible at the moment, select its parent
+       // Autoscroll to make the selected row fully visible
+       void setSelected(s32 index);
+
+       /* Get selection, scroll position and opened (sub)trees */
+       DynamicData getDynamicData() const;
+
+       /* Set selection, scroll position and opened (sub)trees */
+       void setDynamicData(const DynamicData &dyndata);
+
+       /* Returns "GUITable" */
+       virtual const c8* getTypeName() const;
+
+       /* Must be called when position or size changes */
+       virtual void updateAbsolutePosition();
+
+       /* Irrlicht draw method */
+       virtual void draw();
+
+       /* Irrlicht event handler */
+       virtual bool OnEvent(const SEvent &event);
+
+protected:
+       enum ColumnType {
+               COLUMN_TYPE_TEXT,
+               COLUMN_TYPE_IMAGE,
+               COLUMN_TYPE_COLOR,
+               COLUMN_TYPE_INDENT,
+               COLUMN_TYPE_TREE,
+       };
+
+       struct Cell {
+               s32 xmin;
+               s32 xmax;
+               s32 xpos;
+               ColumnType content_type;
+               s32 content_index;
+               s32 tooltip_index;
+               video::SColor color;
+               bool color_defined;
+               s32 reported_column;
+       };
+
+       struct Row {
+               Cell *cells;
+               s32 cellcount;
+               s32 indent;
+               // visible_index >= 0: is index of row in m_visible_rows
+               // visible_index == -1: parent open but other ancestor closed
+               // visible_index == -2: parent closed
+               s32 visible_index;
+       };
+
+       // Texture source
+       ISimpleTextureSource *m_tsrc;
+
+       // Table content (including hidden rows)
+       std::vector<Row> m_rows;
+       // Table content (only visible; indices into m_rows)
+       std::vector<s32> m_visible_rows;
+       bool m_is_textlist;
+       bool m_has_tree_column;
+
+       // Selection status
+       s32 m_selected; // index of row (1...n), or 0 if none selected
+       s32 m_sel_column;
+       bool m_sel_doubleclick;
+
+       // Keyboard navigation stuff
+       s32 m_keynav_time;
+       core::stringw m_keynav_buffer;
+
+       // Drawing and geometry information
+       bool m_border;
+       video::SColor m_color;
+       video::SColor m_background;
+       video::SColor m_highlight;
+       video::SColor m_highlight_text;
+       s32 m_rowheight;
+       gui::IGUIFont *m_font;
+       gui::IGUIScrollBar *m_scrollbar;
+
+       // Allocated strings and images
+       std::vector<core::stringw> m_strings;
+       std::vector<video::ITexture*> m_images;
+       std::map<std::string, s32> m_alloc_strings;
+       std::map<std::string, s32> m_alloc_images;
+
+       s32 allocString(const std::string &text);
+       s32 allocImage(const std::string &imagename);
+       void allocationComplete();
+
+       // Helper for draw() that draws a single cell
+       void drawCell(const Cell *cell, video::SColor color,
+                       const core::rect<s32> &rowrect,
+                       const core::rect<s32> &client_clip);
+
+       // Returns the i-th visible row (NULL if i is invalid)
+       const Row *getRow(s32 i) const;
+
+       // Key navigation helper
+       bool doesRowStartWith(const Row *row, const core::stringw &str) const;
+
+       // Returns the row at a given screen Y coordinate
+       // Returns index i such that m_rows[i] is valid (or -1 on error)
+       s32 getRowAt(s32 y, bool &really_hovering) const;
+
+       // Returns the cell at a given screen X coordinate within m_rows[row_i]
+       // Returns index j such that m_rows[row_i].cells[j] is valid
+       // (or -1 on error)
+       s32 getCellAt(s32 x, s32 row_i) const;
+
+       // Make the selected row fully visible
+       void autoScroll();
+
+       // Should be called when m_rowcount or m_rowheight changes
+       void updateScrollBar();
+
+       // Sends EET_GUI_EVENT / EGET_TABLE_CHANGED to parent
+       void sendTableEvent(s32 column, bool doubleclick);
+
+       // Functions that help deal with hidden rows
+       // The following functions take raw row indices (hidden rows not skipped)
+       void getOpenedTrees(std::set<s32> &opened_trees) const;
+       void setOpenedTrees(const std::set<s32> &opened_trees);
+       void openTree(s32 to_open);
+       void closeTree(s32 to_close);
+       // The following function takes a visible row index (hidden rows skipped)
+       // dir: -1 = left (close), 0 = auto (toggle), 1 = right (open)
+       void toggleVisibleTree(s32 row_i, int dir, bool move_selection);
+
+       // Aligns cell content in column according to alignment specification
+       // align = 0: left aligned, 1: centered, 2: right aligned, 3: inline
+       static void alignContent(Cell *cell, s32 xmax, s32 content_width,
+                       s32 align);
+};
+
+#endif
+
index abf2a8f81a991e59a48c4f8a09eee43de5627ab6..ef16efde2a6a32100ade6080227e0ac8cff1b6cb 100644 (file)
@@ -183,18 +183,25 @@ int ModApiMainMenu::l_set_clouds(lua_State *L)
 
 /******************************************************************************/
 int ModApiMainMenu::l_get_textlist_index(lua_State *L)
+{
+       // get_table_index accepts both tables and textlists
+       return l_get_table_index(L);
+}
+
+/******************************************************************************/
+int ModApiMainMenu::l_get_table_index(lua_State *L)
 {
        GUIEngine* engine = getGuiEngine(L);
        assert(engine != 0);
 
-       std::string listboxname(luaL_checkstring(L, 1));
+       std::wstring tablename(narrow_to_wide(luaL_checkstring(L, 1)));
+       GUITable *table = engine->m_menu->getTable(tablename);
+       s32 selection = table ? table->getSelected() : 0;
 
-       int selection = engine->m_menu->getListboxIndex(listboxname);
-
-       if (selection >= 0)
-               selection++;
-
-       lua_pushinteger(L, selection);
+       if (selection >= 1)
+               lua_pushinteger(L, selection);
+       else
+               lua_pushnil(L);
        return 1;
 }
 
@@ -1026,6 +1033,7 @@ void ModApiMainMenu::Initialize(lua_State *L, int top)
        API_FCT(update_formspec);
        API_FCT(set_clouds);
        API_FCT(get_textlist_index);
+       API_FCT(get_table_index);
        API_FCT(get_worlds);
        API_FCT(get_games);
        API_FCT(start);
index db11ae7942e159d30b672c480624fdf6f7444208..69011c7b556afcee7d6b7242ec0b05c9fc501207 100644 (file)
@@ -97,6 +97,8 @@ private:
 
        static int l_get_textlist_index(lua_State *L);
 
+       static int l_get_table_index(lua_State *L);
+
        static int l_set_background(lua_State *L);
 
        static int l_update_formspec(lua_State *L);