aboutsummaryrefslogtreecommitdiff
Commit message (Collapse)AuthorAge
...
* Translated using Weblate (Japanese)Rui2015-12-21
| | | | Currently translated at 40.0% (315 of 787 strings)
* Translated using Weblate (Dutch)Rogier2015-12-21
| | | | | | Currently translated at 95.1% (749 of 787 strings) Squashed two translation commits to one
* Translated using Weblate (Russian)Anton Tsyganenko2015-12-21
| | | | | | Currently translated at 48.7% (384 of 787 strings) Squashed two translation commits to one
* Translated using Weblate (Italian)Luca Argentieri2015-12-21
| | | | Currently translated at 43.8% (345 of 787 strings)
* Translated using Weblate (Czech)Jakub Vaněk2015-12-21
| | | | | | Currently translated at 56.6% (446 of 787 strings) Squashed two translation commits to one
* Translated using Weblate (Chinese (Taiwan))Jeff Huang2015-12-21
| | | | Currently translated at 59.7% (470 of 787 strings)
* Translated using Weblate (Hebrew)ChaosWormz2015-12-21
| | | | Currently translated at 3.1% (25 of 787 strings)
* Translated using Weblate (Japanese)Rui2015-12-21
| | | | Currently translated at 39.8% (314 of 787 strings)
* Translated using Weblate (Czech)Jakub Vaněk2015-12-21
| | | | | | Currently translated at 49.0% (386 of 787 strings) Squashed two translation commits to one
* Translated using Weblate (Lojban)senpi2015-12-21
| | | | Currently translated at 11.4% (90 of 787 strings)
* Translated using Weblate (Lithuanian)Liudas Ališauskas2015-12-21
| | | | | | Currently translated at 30.7% (242 of 787 strings) Squashed two translation commits to one
* Translated using Weblate (Estonian)Kristjan Räts2015-12-21
| | | | Currently translated at 23.7% (187 of 787 strings)
* Translated using Weblate (Italian)Andrea Di Pietro Ulla2015-12-21
| | | | Currently translated at 42.0% (331 of 787 strings)
* Fix lua object:get_properties() being brokenSapier2015-12-21
|
* Fix missing localization for objHybridDog2015-12-20
|
* Android: Fix extra files being copied to the APKShadowNinja2015-12-20
| | | | | | | dcb91cf0c0c9a20622feeb4e5e8104ffbc9fa8ec hacked around the biggest issue this caused, but wasted a lot of CPU time and disk space It also still included a lot of other unwanted files. This removes all of `doc/` except the license, and also removes the server list.
* Fix missing popest312015-12-20
| | | | | | | | | | | Previous commit 70ea5d552e283bd5bce3278cbf3819bd87ba2602 "Add support for limiting rotation of automatic face movement dir entitys" by sapier has broken minetest's feature to open worlds. This was due to a missing stack pop operation. Thanks to @oleastre for reporting this bug and suggesting the fix.
* Add support for limiting rotation of automatic face movement dir entitysSapier2015-12-19
|
* Android: Implement Autohiding button bars to cleanup screenSapier2015-12-18
|
* Android: Remove unused build target curl_binarySapier2015-12-17
|
* Android: Don't put html docs to apkSapier2015-12-17
|
* Android: Remove non freetype fonts from apkSapier2015-12-17
|
* Android: Fix pressed buttons not beeing cleared on opening menuSapier2015-12-17
|
* Add option to give every object a nametagBlockMen2015-12-15
| | | | or change the nametag text of players
* Get movement setting instead of hard coded valueasl972015-12-15
|
* Mgfractal: Add 3D and 4D fractalsparamat2015-12-15
| | | | | | | | | | | | 3D Mandelbrot/Mandelbar 3D Christmas Tree 3D Mandelbulb 3D Cosine Mandelbulb 4D Mandelbulb Plus corresponding julia set for each Add credits for formulas Rename parameter 'formula' to 'fractal' Speed optimisations
* Fix events on WindowsBlockMen2015-12-11
|
* Fix android buildest312015-12-11
| | | | | | | | | | Fix android build since commit 696148e29889b2923f926b27f76979454676506d "Fix Event implementation" by @ShadowNinja. Fixes #3444. Thanks @kwolekr for pointing out the fix.
* Fix some setting documentationest312015-12-10
| | | | | | | * Horizontal and vertical are used wrongly. Use height and width because horizontal/vertical describes different things. Thanks @kilbith for pointing out. * Update minetest.conf.example and settings_translation_file.cpp * Correct maximum/minimum copy paste mistake.
* Speed up and make more accurate relief mappingRealBadAngel2015-12-10
| | | | using linear + binary search.
* Fix Event implementationShadowNinja2015-12-07
| | | | | | On non-windows platforms this just used a semaphore, which meant that multiple calls to signal() would result in wait() returning multiple times.
* Add seperate cache pathShadowNinja2015-12-07
| | | | | This is set to the XDG cache path where possible. It's set to the app's cache path on Android.
* Fix threshold typeest312015-12-07
| | | | | | | | | | | Fix the type of the threshold value for mapgen. The commit a78dd7f2b6b0e1fefdbaa1ae21b722dd4459e4f4 "Fix spelling of noise_threshold" has changed it to be read as int, but it can have non-integral values too. Thanks to @kwolekr for pointing this out.
* lua_api.txt: add blank lines before * listsest312015-12-07
| | | | | If rendered as markdown, lists need a blank line before them so that they are recognized as such.
* Mapgen: Add propagate_shadow bool to calcLightingparamat2015-12-07
| | | | | | | | | | To terminate unwanted shadows from floatlands or realms above Also add to LuaVoxelManip calc_lighting for use in mapgen mods Remove the 2 argument calcLighting, mapgens now use the 5 argument form to specify the volumes for propagateSunlight and spreadLight In mgsinglenode replace calcLighting with setLighting and clean-up use of tabs and spaces
* Fix spelling of noise_thresholdJun Zhang2015-12-06
|
* Improve LuaJIT detectionFerdinand Thiessen2015-12-05
| | | | | | | On openSUSE luajit is not detected correctly. This is because openSUSE is using a lua version suffix, like other Linux distributions do it also. So the include directory is: include/luajit-5_1-2.0
* Make travis work againest312015-12-05
| | | | | | | | Now we do sudo apt-get update to download package sources. This fixes travis build with the new GCE based infrastructure. Closes #3427. Closes #3426.
* Fix build if BUILD_SHARED_LIBS defaults to "ON"Ferdinand Thiessen2015-12-03
| | | | | | openSUSE sets that option to ON. Fixes #3420.
* Document limitations of minetest.get_password_hashest312015-12-02
|
* Mgv5/v7/flat/fractal: More large pseudorandom cavesparamat2015-12-02
| | | | Mgv7/flat/fractal: Reduce tunnel noise spreads to 96
* Add on_secondary_use when right clicking an item in the airAlex Ford2015-12-02
|
* Dungeongen: Fix rarely triggered segfaultparamat2015-12-01
| | | | | | | A segfault exposed a missing 'vm->m_area.contains()' check in makeCorridor that allowed the calculation of vm index for a node outside the vm area. The huge and invalid index number caused getContent to fail
* Remove unused OpenALSoundManager::m_can_vorbis and EXT_vorbis checkJay Arndt2015-12-01
|
* Lua_api.txt: Add documentation for biome definitionparamat2015-11-29
|
* Dungeongen: Also preserve river water nodesparamat2015-11-29
| | | | | | For future river mapgens Dungeons will not generate in river water, to avoid dungeons filling and blocking river channels
* Re-enable texture pre-filters on wielditems, fixing #3178.Aaron Suen2015-11-24
|
* Simplify regex used in check_modname_prefix and other improvements.Robert Zenz2015-11-24
| | | | | Simplified the regex used, added comments and changed the error message to contain the correct mod name.
* Mgfractal: Move julia set selection into formula parameterparamat2015-11-23
| | | | | | Improve default parameters Update and improve documentation Unhide mapgen, but is still unstable
* Update HUD flags on server like on clientest312015-11-22
| | | | | | | | | | | | | Fixes bug for which commit 6c37e89f08f962eaba788a31f5d3c798ceaa65e6 "Fix old client showing duplicated health bar on new server" by @sapier laid the groundwork, where the server has updated its copy of the hud flags without respecting the mask. Fixes #3395.
960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273
/*
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 "client/tile.h"
#include "gettime.h"
#include "util/string.h"
#include "util/numeric.h"
#include "util/string.h" // for parseColorString()
#include "settings.h" // for settings
#include "porting.h" // for dpi
#include "guiscalingfilter.h"

/*
	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();

	core::rect<s32> relative_rect = m_scrollbar->getRelativePosition();
	s32 width = (relative_rect.getWidth()/(2.0/3.0)) * porting::getDisplayDensity() *
			g_settings->getFloat("gui_scaling");
	m_scrollbar->setRelativePosition(core::rect<s32>(
			relative_rect.LowerRightCorner.X-width,relative_rect.UpperLeftCorner.Y,
			relative_rect.LowerRightCorner.X,relative_rect.LowerRightCorner.Y
			));
}

GUITable::~GUITable()
{
	for (size_t i = 0; i < m_rows.size(); ++i)
		delete[] m_rows[i].cells;

	if (m_font)
		m_font->drop();
	
	m_scrollbar->remove();
}

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 &&
				parseColorString(
					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 a stupid error case... (issue #1187)
	if (columns.empty()) {
		TableColumn text_column;
		text_column.type = "text";
		TableColumns new_columns;
		new_columns.push_back(text_column);
		setTable(options, new_columns, content);
		return;
	}

	// 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")
			parseColorString(value, m_color, false);
		else if (name == "background")
			parseColorString(value, m_background, false);
		else if (name == "border")
			m_border = is_yes(value);
		else if (name == "highlight")
			parseColorString(value, m_highlight, false);
		else if (name == "highlight_text")
			parseColorString(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 (parseColorString(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: convert 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)
{
	s32 old_selected = m_selected;

	m_selected = -1;
	m_sel_column = 0;
	m_sel_doubleclick = false;

	--index; // Switch from 1-based indexing to 0-based indexing

	s32 rowcount = m_rows.size();
	if (rowcount == 0 || index < 0) {
		return;
	} else if (index >= rowcount) {
		index = rowcount - 1;
	}

	// If the selected row is not visible, open its ancestors to make it visible
	bool selection_invisible = m_rows[index].visible_index < 0;
	if (selection_invisible) {
		std::set<s32> opened_trees;
		getOpenedTrees(opened_trees);
		s32 indent = m_rows[index].indent;
		for (s32 j = index - 1; j >= 0; --j) {
			if (m_rows[j].indent < indent) {
				opened_trees.insert(j);
				indent = m_rows[j].indent;
			}
		}
		setOpenedTrees(opened_trees);
	}

	if (index >= 0) {
		m_selected = m_rows[index].visible_index;
		assert(m_selected >= 0 && m_selected < (s32) m_visible_rows.size());
	}

	if (m_selected != old_selected || selection_invisible) {
		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;

	setSelected(dyndata.selected);
	m_sel_column = 0;
	m_sel_doubleclick = false;

	m_scrollbar->setPos(dyndata.scrollpos);
}

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 -= 1;
	if (m_scrollbar->isVisible()) {
		client_clip.LowerRightCorner.X =
				m_scrollbar->getAbsolutePosition().UpperLeftCorner.X;
	}
	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
			// don't 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 ? -3 : 3) *
					- (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"");

		// Fix for #1567/#1806:
		// IGUIScrollBar passes double click events to its parent,
		// which we don't want. Detect this case and discard the event
		if (event.MouseInput.Event != EMIE_MOUSE_MOVED &&
				m_scrollbar->isVisible() &&
				m_scrollbar->isPointInside(p))
			return true;

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

				// Treeview: double click opens/closes trees
				if (m_has_tree_column && sel_doubleclick) {
					toggleVisibleTree(m_selected, 0, false);
				}
			}
		}
		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 = utf8_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 = -1;
	if (m_selected >= 0)
		old_selected = m_visible_rows[m_selected];

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

	// m_selected must be updated since it is a visible row index
	if (old_selected >= 0)
		m_selected = m_rows[old_selected].visible_index;
}

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