summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--builtin/gamemgr.lua2
-rw-r--r--builtin/mainmenu.lua22
-rw-r--r--builtin/misc_helpers.lua51
-rw-r--r--builtin/modmgr.lua6
-rw-r--r--doc/lua_api.txt62
-rw-r--r--doc/menu_lua_api.txt40
-rw-r--r--src/CMakeLists.txt1
-rw-r--r--src/guiFormSpecMenu.cpp322
-rw-r--r--src/guiFormSpecMenu.h33
-rw-r--r--src/guiTable.cpp1212
-rw-r--r--src/guiTable.h269
-rw-r--r--src/script/lua_api/l_mainmenu.cpp22
-rw-r--r--src/script/lua_api/l_mainmenu.h2
13 files changed, 1798 insertions, 246 deletions
diff --git a/builtin/gamemgr.lua b/builtin/gamemgr.lua
index 7a5e9790f..c99c2de21 100644
--- a/builtin/gamemgr.lua
+++ b/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
diff --git a/builtin/mainmenu.lua b/builtin/mainmenu.lua
index d8c2b63ec..8ef306354 100644
--- a/builtin/mainmenu.lua
+++ b/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)
diff --git a/builtin/misc_helpers.lua b/builtin/misc_helpers.lua
index 097c65865..a7a8f6b1c 100644
--- a/builtin/misc_helpers.lua
+++ b/builtin/misc_helpers.lua
@@ -116,26 +116,6 @@ function math.hypot(x, y)
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)
@@ -369,6 +349,37 @@ if minetest then
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
--------------------------------------------------------------------------------
if engine ~= nil then
diff --git a/builtin/modmgr.lua b/builtin/modmgr.lua
index 13f81c6e0..f530ccc4a 100644
--- a/builtin/modmgr.lua
+++ b/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
diff --git a/doc/lua_api.txt b/doc/lua_api.txt
index 1fae0ebbf..8bd83995e 100644
--- a/doc/lua_api.txt
+++ b/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)
diff --git a/doc/menu_lua_api.txt b/doc/menu_lua_api.txt
index b50e43887..87e37dd0d 100644
--- a/doc/menu_lua_api.txt
+++ b/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
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 4bc9f890c..26f8b6e29 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -374,6 +374,7 @@ set(minetest_SRCS
guiMessageMenu.cpp
guiTextInputMenu.cpp
guiFormSpecMenu.cpp
+ guiTable.cpp
guiPauseMenu.cpp
guiPasswordChange.cpp
guiVolumeChange.cpp
diff --git a/src/guiFormSpecMenu.cpp b/src/guiFormSpecMenu.cpp
index 6f98b3d4f..628ea3548 100644
--- a/src/guiFormSpecMenu.cpp
+++ b/src/guiFormSpecMenu.cpp
@@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include <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] == '#') {
diff --git a/src/guiFormSpecMenu.h b/src/guiFormSpecMenu.h
index 8b0e50379..1946f88eb 100644
--- a/src/guiFormSpecMenu.h
+++ b/src/guiFormSpecMenu.h
@@ -27,6 +27,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "inventory.h"
#include "inventorymanager.h"
#include "modalMenu.h"
+#include "guiTable.h"
class IGameDef;
class InventoryManager;
@@ -34,7 +35,7 @@ class ISimpleTextureSource;
typedef enum {
f_Button,
- f_ListBox,
+ f_Table,
f_TabHeader,
f_CheckBox,
f_DropDown,
@@ -231,7 +232,10 @@ public:
bool preprocessEvent(const SEvent& event);
bool OnEvent(const SEvent& event);
- int getListboxIndex(std::string listboxname);
+ GUITable* getTable(std::wstring tablename);
+
+ static bool parseColor(const std::string &value,
+ video::SColor &color, bool quiet);
protected:
v2s32 getBasePos() const
@@ -260,7 +264,7 @@ protected:
std::vector<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
index 000000000..5febb8370
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/src/guiTable.h b/src/guiTable.h
new file mode 100644
index 000000000..4d5b39166
--- /dev/null
+++ b/src/guiTable.h
@@ -0,0 +1,269 @@
+/*
+Minetest
+Copyright (C) 2013 celeron55, Perttu Ahola <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
+
diff --git a/src/script/lua_api/l_mainmenu.cpp b/src/script/lua_api/l_mainmenu.cpp
index abf2a8f81..ef16efde2 100644
--- a/src/script/lua_api/l_mainmenu.cpp
+++ b/src/script/lua_api/l_mainmenu.cpp
@@ -184,17 +184,24 @@ 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);
diff --git a/src/script/lua_api/l_mainmenu.h b/src/script/lua_api/l_mainmenu.h
index db11ae794..69011c7b5 100644
--- a/src/script/lua_api/l_mainmenu.h
+++ b/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);