Browse Source

Add formspec table

Kahrl 10 years ago
parent
commit
8966c16ad2

+ 1 - 1
builtin/gamemgr.lua

@@ -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
 	

+ 11 - 11
builtin/mainmenu.lua

@@ -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)

+ 31 - 20
builtin/misc_helpers.lua

@@ -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
 --------------------------------------------------------------------------------

+ 3 - 3
builtin/modmgr.lua

@@ -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

+ 62 - 0
doc/lua_api.txt

@@ -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)

+ 23 - 17
doc/menu_lua_api.txt

@@ -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

+ 1 - 0
src/CMakeLists.txt

@@ -374,6 +374,7 @@ set(minetest_SRCS
 	guiMessageMenu.cpp
 	guiTextInputMenu.cpp
 	guiFormSpecMenu.cpp
+	guiTable.cpp
 	guiPauseMenu.cpp
 	guiPasswordChange.cpp
 	guiVolumeChange.cpp

+ 154 - 168
src/guiFormSpecMenu.cpp

@@ -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] == '#') {

+ 14 - 19
src/guiFormSpecMenu.h

@@ -27,6 +27,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "inventory.h"
 #include "inventorymanager.h"
 #include "modalMenu.h"
+#include "guiTable.h"
 
 class IGameDef;
 class InventoryManager;
@@ -34,7 +35,7 @@ class ISimpleTextureSource;
 
 typedef enum {
 	f_Button,
-	f_ListBox,
+	f_Table,
 	f_TabHeader,
 	f_CheckBox,
 	f_DropDown,
@@ -231,7 +232,10 @@ public:
 	bool preprocessEvent(const SEvent& event);
 	bool OnEvent(const SEvent& event);
 
-	int getListboxIndex(std::string listboxname);
+	GUITable* getTable(std::wstring tablename);
+
+	static bool parseColor(const std::string &value,
+			video::SColor &color, bool quiet);
 
 protected:
 	v2s32 getBasePos() const
@@ -260,7 +264,7 @@ protected:
 	std::vector<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

+ 1212 - 0
src/guiTable.cpp

@@ -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;
+	}
+}

+ 269 - 0
src/guiTable.h

@@ -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
+

+ 15 - 7
src/script/lua_api/l_mainmenu.cpp

@@ -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);

+ 2 - 0
src/script/lua_api/l_mainmenu.h

@@ -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);