1 /*
2 Minetest
3 Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
4 
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU Lesser General Public License as published by
7 the Free Software Foundation; either version 2.1 of the License, or
8 (at your option) any later version.
9 
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 GNU Lesser General Public License for more details.
14 
15 You should have received a copy of the GNU Lesser General Public License along
16 with this program; if not, write to the Free Software Foundation, Inc.,
17 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19 
20 
21 #include "guiTable.h"
22 #include <queue>
23 #include <sstream>
24 #include <utility>
25 #include <cstring>
26 #include <IGUISkin.h>
27 #include <IGUIFont.h>
28 #include "client/renderingengine.h"
29 #include "debug.h"
30 #include "log.h"
31 #include "client/tile.h"
32 #include "gettime.h"
33 #include "util/string.h"
34 #include "util/numeric.h"
35 #include "util/string.h" // for parseColorString()
36 #include "settings.h" // for settings
37 #include "porting.h" // for dpi
38 #include "client/guiscalingfilter.h"
39 
40 /*
41 	GUITable
42 */
43 
GUITable(gui::IGUIEnvironment * env,gui::IGUIElement * parent,s32 id,core::rect<s32> rectangle,ISimpleTextureSource * tsrc)44 GUITable::GUITable(gui::IGUIEnvironment *env,
45 		gui::IGUIElement* parent, s32 id,
46 		core::rect<s32> rectangle,
47 		ISimpleTextureSource *tsrc
48 ):
49 	gui::IGUIElement(gui::EGUIET_ELEMENT, env, parent, id, rectangle),
50 	m_tsrc(tsrc)
51 {
52 	assert(tsrc != NULL);
53 
54 	gui::IGUISkin* skin = Environment->getSkin();
55 
56 	m_font = skin->getFont();
57 	if (m_font) {
58 		m_font->grab();
59 		m_rowheight = m_font->getDimension(L"Ay").Height + 4;
60 		m_rowheight = MYMAX(m_rowheight, 1);
61 	}
62 
63 	const s32 s = skin->getSize(gui::EGDS_SCROLLBAR_SIZE);
64 	m_scrollbar = new GUIScrollBar(Environment, this, -1,
65 			core::rect<s32>(RelativeRect.getWidth() - s,
66 					0,
67 					RelativeRect.getWidth(),
68 					RelativeRect.getHeight()),
69 			false, true);
70 	m_scrollbar->setSubElement(true);
71 	m_scrollbar->setTabStop(false);
72 	m_scrollbar->setAlignment(gui::EGUIA_LOWERRIGHT, gui::EGUIA_LOWERRIGHT,
73 			gui::EGUIA_UPPERLEFT, gui::EGUIA_LOWERRIGHT);
74 	m_scrollbar->setVisible(false);
75 	m_scrollbar->setPos(0);
76 
77 	setTabStop(true);
78 	setTabOrder(-1);
79 	updateAbsolutePosition();
80 	float density = RenderingEngine::getDisplayDensity();
81 #ifdef __ANDROID__
82 	density = 1; // dp scaling is applied by the skin
83 #endif
84 	core::rect<s32> relative_rect = m_scrollbar->getRelativePosition();
85 	s32 width = (relative_rect.getWidth() / (2.0 / 3.0)) * density *
86 			g_settings->getFloat("gui_scaling");
87 	m_scrollbar->setRelativePosition(core::rect<s32>(
88 			relative_rect.LowerRightCorner.X-width,relative_rect.UpperLeftCorner.Y,
89 			relative_rect.LowerRightCorner.X,relative_rect.LowerRightCorner.Y
90 			));
91 }
92 
~GUITable()93 GUITable::~GUITable()
94 {
95 	for (GUITable::Row &row : m_rows)
96 		delete[] row.cells;
97 
98 	if (m_font)
99 		m_font->drop();
100 
101 	if (m_scrollbar)
102 		m_scrollbar->drop();
103 }
104 
splitOption(const std::string & str)105 GUITable::Option GUITable::splitOption(const std::string &str)
106 {
107 	size_t equal_pos = str.find('=');
108 	if (equal_pos == std::string::npos)
109 		return GUITable::Option(str, "");
110 
111 	return GUITable::Option(str.substr(0, equal_pos),
112 			str.substr(equal_pos + 1));
113 }
114 
setTextList(const std::vector<std::string> & content,bool transparent)115 void GUITable::setTextList(const std::vector<std::string> &content,
116 		bool transparent)
117 {
118 	clear();
119 
120 	if (transparent) {
121 		m_background.setAlpha(0);
122 		m_border = false;
123 	}
124 
125 	m_is_textlist = true;
126 
127 	s32 empty_string_index = allocString("");
128 
129 	m_rows.resize(content.size());
130 	for (s32 i = 0; i < (s32) content.size(); ++i) {
131 		Row *row = &m_rows[i];
132 		row->cells = new Cell[1];
133 		row->cellcount = 1;
134 		row->indent = 0;
135 		row->visible_index = i;
136 		m_visible_rows.push_back(i);
137 
138 		Cell *cell = row->cells;
139 		cell->xmin = 0;
140 		cell->xmax = 0x7fff;  // something large enough
141 		cell->xpos = 6;
142 		cell->content_type = COLUMN_TYPE_TEXT;
143 		cell->content_index = empty_string_index;
144 		cell->tooltip_index = empty_string_index;
145 		cell->color.set(255, 255, 255, 255);
146 		cell->color_defined = false;
147 		cell->reported_column = 1;
148 
149 		// parse row content (color)
150 		const std::string &s = content[i];
151 		if (s[0] == '#' && s[1] == '#') {
152 			// double # to escape
153 			cell->content_index = allocString(s.substr(2));
154 		}
155 		else if (s[0] == '#' && s.size() >= 7 &&
156 				parseColorString(
157 					s.substr(0,7), cell->color, false)) {
158 			// single # for color
159 			cell->color_defined = true;
160 			cell->content_index = allocString(s.substr(7));
161 		}
162 		else {
163 			// no #, just text
164 			cell->content_index = allocString(s);
165 		}
166 
167 	}
168 
169 	allocationComplete();
170 
171 	// Clamp scroll bar position
172 	updateScrollBar();
173 }
174 
setTable(const TableOptions & options,const TableColumns & columns,std::vector<std::string> & content)175 void GUITable::setTable(const TableOptions &options,
176 		const TableColumns &columns,
177 		std::vector<std::string> &content)
178 {
179 	clear();
180 
181 	// Naming conventions:
182 	// i is always a row index, 0-based
183 	// j is always a column index, 0-based
184 	// k is another index, for example an option index
185 
186 	// Handle a stupid error case... (issue #1187)
187 	if (columns.empty()) {
188 		TableColumn text_column;
189 		text_column.type = "text";
190 		TableColumns new_columns;
191 		new_columns.push_back(text_column);
192 		setTable(options, new_columns, content);
193 		return;
194 	}
195 
196 	// Handle table options
197 	video::SColor default_color(255, 255, 255, 255);
198 	s32 opendepth = 0;
199 	for (const Option &option : options) {
200 		const std::string &name = option.name;
201 		const std::string &value = option.value;
202 		if (name == "color")
203 			parseColorString(value, m_color, false);
204 		else if (name == "background")
205 			parseColorString(value, m_background, false);
206 		else if (name == "border")
207 			m_border = is_yes(value);
208 		else if (name == "highlight")
209 			parseColorString(value, m_highlight, false);
210 		else if (name == "highlight_text")
211 			parseColorString(value, m_highlight_text, false);
212 		else if (name == "opendepth")
213 			opendepth = stoi(value);
214 		else
215 			errorstream<<"Invalid table option: \""<<name<<"\""
216 				<<" (value=\""<<value<<"\")"<<std::endl;
217 	}
218 
219 	// Get number of columns and rows
220 	// note: error case columns.size() == 0 was handled above
221 	s32 colcount = columns.size();
222 	assert(colcount >= 1);
223 	// rowcount = ceil(cellcount / colcount) but use integer arithmetic
224 	s32 rowcount = (content.size() + colcount - 1) / colcount;
225 	assert(rowcount >= 0);
226 	// Append empty strings to content if there is an incomplete row
227 	s32 cellcount = rowcount * colcount;
228 	while (content.size() < (u32) cellcount)
229 		content.emplace_back("");
230 
231 	// Create temporary rows (for processing columns)
232 	struct TempRow {
233 		// Current horizontal position (may different between rows due
234 		// to indent/tree columns, or text/image columns with width<0)
235 		s32 x;
236 		// Tree indentation level
237 		s32 indent;
238 		// Next cell: Index into m_strings or m_images
239 		s32 content_index;
240 		// Next cell: Width in pixels
241 		s32 content_width;
242 		// Vector of completed cells in this row
243 		std::vector<Cell> cells;
244 		// Stores colors and how long they last (maximum column index)
245 		std::vector<std::pair<video::SColor, s32> > colors;
246 
247 		TempRow(): x(0), indent(0), content_index(0), content_width(0) {}
248 	};
249 	TempRow *rows = new TempRow[rowcount];
250 
251 	// Get em width. Pedantically speaking, the width of "M" is not
252 	// necessarily the same as the em width, but whatever, close enough.
253 	s32 em = 6;
254 	if (m_font)
255 		em = m_font->getDimension(L"M").Width;
256 
257 	s32 default_tooltip_index = allocString("");
258 
259 	std::map<s32, s32> active_image_indices;
260 
261 	// Process content in column-major order
262 	for (s32 j = 0; j < colcount; ++j) {
263 		// Check column type
264 		ColumnType columntype = COLUMN_TYPE_TEXT;
265 		if (columns[j].type == "text")
266 			columntype = COLUMN_TYPE_TEXT;
267 		else if (columns[j].type == "image")
268 			columntype = COLUMN_TYPE_IMAGE;
269 		else if (columns[j].type == "color")
270 			columntype = COLUMN_TYPE_COLOR;
271 		else if (columns[j].type == "indent")
272 			columntype = COLUMN_TYPE_INDENT;
273 		else if (columns[j].type == "tree")
274 			columntype = COLUMN_TYPE_TREE;
275 		else
276 			errorstream<<"Invalid table column type: \""
277 				<<columns[j].type<<"\""<<std::endl;
278 
279 		// Process column options
280 		s32 padding = myround(0.5 * em);
281 		s32 tooltip_index = default_tooltip_index;
282 		s32 align = 0;
283 		s32 width = 0;
284 		s32 span = colcount;
285 
286 		if (columntype == COLUMN_TYPE_INDENT) {
287 			padding = 0; // default indent padding
288 		}
289 		if (columntype == COLUMN_TYPE_INDENT ||
290 				columntype == COLUMN_TYPE_TREE) {
291 			width = myround(em * 1.5); // default indent width
292 		}
293 
294 		for (const Option &option : columns[j].options) {
295 			const std::string &name = option.name;
296 			const std::string &value = option.value;
297 			if (name == "padding")
298 				padding = myround(stof(value) * em);
299 			else if (name == "tooltip")
300 				tooltip_index = allocString(value);
301 			else if (name == "align" && value == "left")
302 				align = 0;
303 			else if (name == "align" && value == "center")
304 				align = 1;
305 			else if (name == "align" && value == "right")
306 				align = 2;
307 			else if (name == "align" && value == "inline")
308 				align = 3;
309 			else if (name == "width")
310 				width = myround(stof(value) * em);
311 			else if (name == "span" && columntype == COLUMN_TYPE_COLOR)
312 				span = stoi(value);
313 			else if (columntype == COLUMN_TYPE_IMAGE &&
314 					!name.empty() &&
315 					string_allowed(name, "0123456789")) {
316 				s32 content_index = allocImage(value);
317 				active_image_indices.insert(std::make_pair(
318 							stoi(name),
319 							content_index));
320 			}
321 			else {
322 				errorstream<<"Invalid table column option: \""<<name<<"\""
323 					<<" (value=\""<<value<<"\")"<<std::endl;
324 			}
325 		}
326 
327 		// If current column type can use information from "color" columns,
328 		// find out which of those is currently active
329 		if (columntype == COLUMN_TYPE_TEXT) {
330 			for (s32 i = 0; i < rowcount; ++i) {
331 				TempRow *row = &rows[i];
332 				while (!row->colors.empty() && row->colors.back().second < j)
333 					row->colors.pop_back();
334 			}
335 		}
336 
337 		// Make template for new cells
338 		Cell newcell;
339 		newcell.content_type = columntype;
340 		newcell.tooltip_index = tooltip_index;
341 		newcell.reported_column = j+1;
342 
343 		if (columntype == COLUMN_TYPE_TEXT) {
344 			// Find right edge of column
345 			s32 xmax = 0;
346 			for (s32 i = 0; i < rowcount; ++i) {
347 				TempRow *row = &rows[i];
348 				row->content_index = allocString(content[i * colcount + j]);
349 				const core::stringw &text = m_strings[row->content_index];
350 				row->content_width = m_font ?
351 					m_font->getDimension(text.c_str()).Width : 0;
352 				row->content_width = MYMAX(row->content_width, width);
353 				s32 row_xmax = row->x + padding + row->content_width;
354 				xmax = MYMAX(xmax, row_xmax);
355 			}
356 			// Add a new cell (of text type) to each row
357 			for (s32 i = 0; i < rowcount; ++i) {
358 				newcell.xmin = rows[i].x + padding;
359 				alignContent(&newcell, xmax, rows[i].content_width, align);
360 				newcell.content_index = rows[i].content_index;
361 				newcell.color_defined = !rows[i].colors.empty();
362 				if (newcell.color_defined)
363 					newcell.color = rows[i].colors.back().first;
364 				rows[i].cells.push_back(newcell);
365 				rows[i].x = newcell.xmax;
366 			}
367 		}
368 		else if (columntype == COLUMN_TYPE_IMAGE) {
369 			// Find right edge of column
370 			s32 xmax = 0;
371 			for (s32 i = 0; i < rowcount; ++i) {
372 				TempRow *row = &rows[i];
373 				row->content_index = -1;
374 
375 				// Find content_index. Image indices are defined in
376 				// column options so check active_image_indices.
377 				s32 image_index = stoi(content[i * colcount + j]);
378 				std::map<s32, s32>::iterator image_iter =
379 					active_image_indices.find(image_index);
380 				if (image_iter != active_image_indices.end())
381 					row->content_index = image_iter->second;
382 
383 				// Get texture object (might be NULL)
384 				video::ITexture *image = NULL;
385 				if (row->content_index >= 0)
386 					image = m_images[row->content_index];
387 
388 				// Get content width and update xmax
389 				row->content_width = image ? image->getOriginalSize().Width : 0;
390 				row->content_width = MYMAX(row->content_width, width);
391 				s32 row_xmax = row->x + padding + row->content_width;
392 				xmax = MYMAX(xmax, row_xmax);
393 			}
394 			// Add a new cell (of image type) to each row
395 			for (s32 i = 0; i < rowcount; ++i) {
396 				newcell.xmin = rows[i].x + padding;
397 				alignContent(&newcell, xmax, rows[i].content_width, align);
398 				newcell.content_index = rows[i].content_index;
399 				rows[i].cells.push_back(newcell);
400 				rows[i].x = newcell.xmax;
401 			}
402 			active_image_indices.clear();
403 		}
404 		else if (columntype == COLUMN_TYPE_COLOR) {
405 			for (s32 i = 0; i < rowcount; ++i) {
406 				video::SColor cellcolor(255, 255, 255, 255);
407 				if (parseColorString(content[i * colcount + j], cellcolor, true))
408 					rows[i].colors.emplace_back(cellcolor, j+span);
409 			}
410 		}
411 		else if (columntype == COLUMN_TYPE_INDENT ||
412 				columntype == COLUMN_TYPE_TREE) {
413 			// For column type "tree", reserve additional space for +/-
414 			// Also enable special processing for treeview-type tables
415 			s32 content_width = 0;
416 			if (columntype == COLUMN_TYPE_TREE) {
417 				content_width = m_font ? m_font->getDimension(L"+").Width : 0;
418 				m_has_tree_column = true;
419 			}
420 			// Add a new cell (of indent or tree type) to each row
421 			for (s32 i = 0; i < rowcount; ++i) {
422 				TempRow *row = &rows[i];
423 
424 				s32 indentlevel = stoi(content[i * colcount + j]);
425 				indentlevel = MYMAX(indentlevel, 0);
426 				if (columntype == COLUMN_TYPE_TREE)
427 					row->indent = indentlevel;
428 
429 				newcell.xmin = row->x + padding;
430 				newcell.xpos = newcell.xmin + indentlevel * width;
431 				newcell.xmax = newcell.xpos + content_width;
432 				newcell.content_index = 0;
433 				newcell.color_defined = !rows[i].colors.empty();
434 				if (newcell.color_defined)
435 					newcell.color = rows[i].colors.back().first;
436 				row->cells.push_back(newcell);
437 				row->x = newcell.xmax;
438 			}
439 		}
440 	}
441 
442 	// Copy temporary rows to not so temporary rows
443 	if (rowcount >= 1) {
444 		m_rows.resize(rowcount);
445 		for (s32 i = 0; i < rowcount; ++i) {
446 			Row *row = &m_rows[i];
447 			row->cellcount = rows[i].cells.size();
448 			row->cells = new Cell[row->cellcount];
449 			memcpy((void*) row->cells, (void*) &rows[i].cells[0],
450 					row->cellcount * sizeof(Cell));
451 			row->indent = rows[i].indent;
452 			row->visible_index = i;
453 			m_visible_rows.push_back(i);
454 		}
455 	}
456 
457 	if (m_has_tree_column) {
458 		// Treeview: convert tree to indent cells on leaf rows
459 		for (s32 i = 0; i < rowcount; ++i) {
460 			if (i == rowcount-1 || m_rows[i].indent >= m_rows[i+1].indent)
461 				for (s32 j = 0; j < m_rows[i].cellcount; ++j)
462 					if (m_rows[i].cells[j].content_type == COLUMN_TYPE_TREE)
463 						m_rows[i].cells[j].content_type = COLUMN_TYPE_INDENT;
464 		}
465 
466 		// Treeview: close rows according to opendepth option
467 		std::set<s32> opened_trees;
468 		for (s32 i = 0; i < rowcount; ++i)
469 			if (m_rows[i].indent < opendepth)
470 				opened_trees.insert(i);
471 		setOpenedTrees(opened_trees);
472 	}
473 
474 	// Delete temporary information used only during setTable()
475 	delete[] rows;
476 	allocationComplete();
477 
478 	// Clamp scroll bar position
479 	updateScrollBar();
480 }
481 
clear()482 void GUITable::clear()
483 {
484 	// Clean up cells and rows
485 	for (GUITable::Row &row : m_rows)
486 		delete[] row.cells;
487 	m_rows.clear();
488 	m_visible_rows.clear();
489 
490 	// Get colors from skin
491 	gui::IGUISkin *skin = Environment->getSkin();
492 	m_color          = skin->getColor(gui::EGDC_BUTTON_TEXT);
493 	m_background     = skin->getColor(gui::EGDC_3D_HIGH_LIGHT);
494 	m_highlight      = skin->getColor(gui::EGDC_HIGH_LIGHT);
495 	m_highlight_text = skin->getColor(gui::EGDC_HIGH_LIGHT_TEXT);
496 
497 	// Reset members
498 	m_is_textlist = false;
499 	m_has_tree_column = false;
500 	m_selected = -1;
501 	m_sel_column = 0;
502 	m_sel_doubleclick = false;
503 	m_keynav_time = 0;
504 	m_keynav_buffer = L"";
505 	m_border = true;
506 	m_strings.clear();
507 	m_images.clear();
508 	m_alloc_strings.clear();
509 	m_alloc_images.clear();
510 }
511 
checkEvent()512 std::string GUITable::checkEvent()
513 {
514 	s32 sel = getSelected();
515 	assert(sel >= 0);
516 
517 	if (sel == 0) {
518 		return "INV";
519 	}
520 
521 	std::ostringstream os(std::ios::binary);
522 	if (m_sel_doubleclick) {
523 		os<<"DCL:";
524 		m_sel_doubleclick = false;
525 	}
526 	else {
527 		os<<"CHG:";
528 	}
529 	os<<sel;
530 	if (!m_is_textlist) {
531 		os<<":"<<m_sel_column;
532 	}
533 	return os.str();
534 }
535 
getSelected() const536 s32 GUITable::getSelected() const
537 {
538 	if (m_selected < 0)
539 		return 0;
540 
541 	assert(m_selected >= 0 && m_selected < (s32) m_visible_rows.size());
542 	return m_visible_rows[m_selected] + 1;
543 }
544 
setSelected(s32 index)545 void GUITable::setSelected(s32 index)
546 {
547 	s32 old_selected = m_selected;
548 
549 	m_selected = -1;
550 	m_sel_column = 0;
551 	m_sel_doubleclick = false;
552 
553 	--index; // Switch from 1-based indexing to 0-based indexing
554 
555 	s32 rowcount = m_rows.size();
556 	if (rowcount == 0 || index < 0) {
557 		return;
558 	}
559 
560 	if (index >= rowcount) {
561 		index = rowcount - 1;
562 	}
563 
564 	// If the selected row is not visible, open its ancestors to make it visible
565 	bool selection_invisible = m_rows[index].visible_index < 0;
566 	if (selection_invisible) {
567 		std::set<s32> opened_trees;
568 		getOpenedTrees(opened_trees);
569 		s32 indent = m_rows[index].indent;
570 		for (s32 j = index - 1; j >= 0; --j) {
571 			if (m_rows[j].indent < indent) {
572 				opened_trees.insert(j);
573 				indent = m_rows[j].indent;
574 			}
575 		}
576 		setOpenedTrees(opened_trees);
577 	}
578 
579 	if (index >= 0) {
580 		m_selected = m_rows[index].visible_index;
581 		assert(m_selected >= 0 && m_selected < (s32) m_visible_rows.size());
582 	}
583 
584 	if (m_selected != old_selected || selection_invisible) {
585 		autoScroll();
586 	}
587 }
588 
setOverrideFont(IGUIFont * font)589 void GUITable::setOverrideFont(IGUIFont *font)
590 {
591 	if (m_font == font)
592 		return;
593 
594 	if (font == nullptr)
595 		font = Environment->getSkin()->getFont();
596 
597 	if (m_font)
598 		m_font->drop();
599 
600 	m_font = font;
601 	m_font->grab();
602 
603 	m_rowheight = m_font->getDimension(L"Ay").Height + 4;
604 	m_rowheight = MYMAX(m_rowheight, 1);
605 
606 	updateScrollBar();
607 }
608 
getOverrideFont() const609 IGUIFont *GUITable::getOverrideFont() const
610 {
611 	return m_font;
612 }
613 
getDynamicData() const614 GUITable::DynamicData GUITable::getDynamicData() const
615 {
616 	DynamicData dyndata;
617 	dyndata.selected = getSelected();
618 	dyndata.scrollpos = m_scrollbar->getPos();
619 	dyndata.keynav_time = m_keynav_time;
620 	dyndata.keynav_buffer = m_keynav_buffer;
621 	if (m_has_tree_column)
622 		getOpenedTrees(dyndata.opened_trees);
623 	return dyndata;
624 }
625 
setDynamicData(const DynamicData & dyndata)626 void GUITable::setDynamicData(const DynamicData &dyndata)
627 {
628 	if (m_has_tree_column)
629 		setOpenedTrees(dyndata.opened_trees);
630 
631 	m_keynav_time = dyndata.keynav_time;
632 	m_keynav_buffer = dyndata.keynav_buffer;
633 
634 	setSelected(dyndata.selected);
635 	m_sel_column = 0;
636 	m_sel_doubleclick = false;
637 
638 	m_scrollbar->setPos(dyndata.scrollpos);
639 }
640 
getTypeName() const641 const c8* GUITable::getTypeName() const
642 {
643 	return "GUITable";
644 }
645 
updateAbsolutePosition()646 void GUITable::updateAbsolutePosition()
647 {
648 	IGUIElement::updateAbsolutePosition();
649 	updateScrollBar();
650 }
651 
draw()652 void GUITable::draw()
653 {
654 	if (!IsVisible)
655 		return;
656 
657 	gui::IGUISkin *skin = Environment->getSkin();
658 
659 	// draw background
660 
661 	bool draw_background = m_background.getAlpha() > 0;
662 	if (m_border)
663 		skin->draw3DSunkenPane(this, m_background,
664 				true, draw_background,
665 				AbsoluteRect, &AbsoluteClippingRect);
666 	else if (draw_background)
667 		skin->draw2DRectangle(this, m_background,
668 				AbsoluteRect, &AbsoluteClippingRect);
669 
670 	// get clipping rect
671 
672 	core::rect<s32> client_clip(AbsoluteRect);
673 	client_clip.UpperLeftCorner.Y += 1;
674 	client_clip.UpperLeftCorner.X += 1;
675 	client_clip.LowerRightCorner.Y -= 1;
676 	client_clip.LowerRightCorner.X -= 1;
677 	if (m_scrollbar->isVisible()) {
678 		client_clip.LowerRightCorner.X =
679 				m_scrollbar->getAbsolutePosition().UpperLeftCorner.X;
680 	}
681 	client_clip.clipAgainst(AbsoluteClippingRect);
682 
683 	// draw visible rows
684 
685 	s32 scrollpos = m_scrollbar->getPos();
686 	s32 row_min = scrollpos / m_rowheight;
687 	s32 row_max = (scrollpos + AbsoluteRect.getHeight() - 1)
688 			/ m_rowheight + 1;
689 	row_max = MYMIN(row_max, (s32) m_visible_rows.size());
690 
691 	core::rect<s32> row_rect(AbsoluteRect);
692 	if (m_scrollbar->isVisible())
693 		row_rect.LowerRightCorner.X -=
694 			skin->getSize(gui::EGDS_SCROLLBAR_SIZE);
695 	row_rect.UpperLeftCorner.Y += row_min * m_rowheight - scrollpos;
696 	row_rect.LowerRightCorner.Y = row_rect.UpperLeftCorner.Y + m_rowheight;
697 
698 	for (s32 i = row_min; i < row_max; ++i) {
699 		Row *row = &m_rows[m_visible_rows[i]];
700 		bool is_sel = i == m_selected;
701 		video::SColor color = m_color;
702 
703 		if (is_sel) {
704 			skin->draw2DRectangle(this, m_highlight, row_rect, &client_clip);
705 			color = m_highlight_text;
706 		}
707 
708 		for (s32 j = 0; j < row->cellcount; ++j)
709 			drawCell(&row->cells[j], color, row_rect, client_clip);
710 
711 		row_rect.UpperLeftCorner.Y += m_rowheight;
712 		row_rect.LowerRightCorner.Y += m_rowheight;
713 	}
714 
715 	// Draw children
716 	IGUIElement::draw();
717 }
718 
drawCell(const Cell * cell,video::SColor color,const core::rect<s32> & row_rect,const core::rect<s32> & client_clip)719 void GUITable::drawCell(const Cell *cell, video::SColor color,
720 		const core::rect<s32> &row_rect,
721 		const core::rect<s32> &client_clip)
722 {
723 	if ((cell->content_type == COLUMN_TYPE_TEXT)
724 			|| (cell->content_type == COLUMN_TYPE_TREE)) {
725 
726 		core::rect<s32> text_rect = row_rect;
727 		text_rect.UpperLeftCorner.X = row_rect.UpperLeftCorner.X
728 				+ cell->xpos;
729 		text_rect.LowerRightCorner.X = row_rect.UpperLeftCorner.X
730 				+ cell->xmax;
731 
732 		if (cell->color_defined)
733 			color = cell->color;
734 
735 		if (m_font) {
736 			if (cell->content_type == COLUMN_TYPE_TEXT)
737 				m_font->draw(m_strings[cell->content_index],
738 						text_rect, color,
739 						false, true, &client_clip);
740 			else // tree
741 				m_font->draw(cell->content_index ? L"+" : L"-",
742 						text_rect, color,
743 						false, true, &client_clip);
744 		}
745 	}
746 	else if (cell->content_type == COLUMN_TYPE_IMAGE) {
747 
748 		if (cell->content_index < 0)
749 			return;
750 
751 		video::IVideoDriver *driver = Environment->getVideoDriver();
752 		video::ITexture *image = m_images[cell->content_index];
753 
754 		if (image) {
755 			core::position2d<s32> dest_pos =
756 					row_rect.UpperLeftCorner;
757 			dest_pos.X += cell->xpos;
758 			core::rect<s32> source_rect(
759 					core::position2d<s32>(0, 0),
760 					image->getOriginalSize());
761 			s32 imgh = source_rect.LowerRightCorner.Y;
762 			s32 rowh = row_rect.getHeight();
763 			if (imgh < rowh)
764 				dest_pos.Y += (rowh - imgh) / 2;
765 			else
766 				source_rect.LowerRightCorner.Y = rowh;
767 
768 			video::SColor color(255, 255, 255, 255);
769 
770 			driver->draw2DImage(image, dest_pos, source_rect,
771 					&client_clip, color, true);
772 		}
773 	}
774 }
775 
OnEvent(const SEvent & event)776 bool GUITable::OnEvent(const SEvent &event)
777 {
778 	if (!isEnabled())
779 		return IGUIElement::OnEvent(event);
780 
781 	if (event.EventType == EET_KEY_INPUT_EVENT) {
782 		if (event.KeyInput.PressedDown && (
783 				event.KeyInput.Key == KEY_DOWN ||
784 				event.KeyInput.Key == KEY_UP   ||
785 				event.KeyInput.Key == KEY_HOME ||
786 				event.KeyInput.Key == KEY_END  ||
787 				event.KeyInput.Key == KEY_NEXT ||
788 				event.KeyInput.Key == KEY_PRIOR)) {
789 			s32 offset = 0;
790 			switch (event.KeyInput.Key) {
791 				case KEY_DOWN:
792 					offset = 1;
793 					break;
794 				case KEY_UP:
795 					offset = -1;
796 					break;
797 				case KEY_HOME:
798 					offset = - (s32) m_visible_rows.size();
799 					break;
800 				case KEY_END:
801 					offset = m_visible_rows.size();
802 					break;
803 				case KEY_NEXT:
804 					offset = AbsoluteRect.getHeight() / m_rowheight;
805 					break;
806 				case KEY_PRIOR:
807 					offset = - (s32) (AbsoluteRect.getHeight() / m_rowheight);
808 					break;
809 				default:
810 					break;
811 			}
812 			s32 old_selected = m_selected;
813 			s32 rowcount = m_visible_rows.size();
814 			if (rowcount != 0) {
815 				m_selected = rangelim(m_selected + offset, 0, rowcount-1);
816 				autoScroll();
817 			}
818 
819 			if (m_selected != old_selected)
820 				sendTableEvent(0, false);
821 
822 			return true;
823 		}
824 
825 		if (event.KeyInput.PressedDown && (
826 				event.KeyInput.Key == KEY_LEFT ||
827 				event.KeyInput.Key == KEY_RIGHT)) {
828 			// Open/close subtree via keyboard
829 			if (m_selected >= 0) {
830 				int dir = event.KeyInput.Key == KEY_LEFT ? -1 : 1;
831 				toggleVisibleTree(m_selected, dir, true);
832 			}
833 			return true;
834 		}
835 		else if (!event.KeyInput.PressedDown && (
836 				event.KeyInput.Key == KEY_RETURN ||
837 				event.KeyInput.Key == KEY_SPACE)) {
838 			sendTableEvent(0, true);
839 			return true;
840 		}
841 		else if (event.KeyInput.Key == KEY_ESCAPE ||
842 				event.KeyInput.Key == KEY_SPACE) {
843 			// pass to parent
844 		}
845 		else if (event.KeyInput.PressedDown && event.KeyInput.Char) {
846 			// change selection based on text as it is typed
847 			u64 now = porting::getTimeMs();
848 			if (now - m_keynav_time >= 500)
849 				m_keynav_buffer = L"";
850 			m_keynav_time = now;
851 
852 			// add to key buffer if not a key repeat
853 			if (!(m_keynav_buffer.size() == 1 &&
854 					m_keynav_buffer[0] == event.KeyInput.Char)) {
855 				m_keynav_buffer.append(event.KeyInput.Char);
856 			}
857 
858 			// find the selected item, starting at the current selection
859 			// don't change selection if the key buffer matches the current item
860 			s32 old_selected = m_selected;
861 			s32 start = MYMAX(m_selected, 0);
862 			s32 rowcount = m_visible_rows.size();
863 			for (s32 k = 1; k < rowcount; ++k) {
864 				s32 current = start + k;
865 				if (current >= rowcount)
866 					current -= rowcount;
867 				if (doesRowStartWith(getRow(current), m_keynav_buffer)) {
868 					m_selected = current;
869 					break;
870 				}
871 			}
872 			autoScroll();
873 			if (m_selected != old_selected)
874 				sendTableEvent(0, false);
875 
876 			return true;
877 		}
878 	}
879 	if (event.EventType == EET_MOUSE_INPUT_EVENT) {
880 		core::position2d<s32> p(event.MouseInput.X, event.MouseInput.Y);
881 
882 		if (event.MouseInput.Event == EMIE_MOUSE_WHEEL) {
883 			m_scrollbar->setPos(m_scrollbar->getPos() +
884 					(event.MouseInput.Wheel < 0 ? -3 : 3) *
885 					- (s32) m_rowheight / 2);
886 			return true;
887 		}
888 
889 		// Find hovered row and cell
890 		bool really_hovering = false;
891 		s32 row_i = getRowAt(p.Y, really_hovering);
892 		const Cell *cell = NULL;
893 		if (really_hovering) {
894 			s32 cell_j = getCellAt(p.X, row_i);
895 			if (cell_j >= 0)
896 				cell = &(getRow(row_i)->cells[cell_j]);
897 		}
898 
899 		// Update tooltip
900 		setToolTipText(cell ? m_strings[cell->tooltip_index].c_str() : L"");
901 
902 		// Fix for #1567/#1806:
903 		// IGUIScrollBar passes double click events to its parent,
904 		// which we don't want. Detect this case and discard the event
905 		if (event.MouseInput.Event != EMIE_MOUSE_MOVED &&
906 				m_scrollbar->isVisible() &&
907 				m_scrollbar->isPointInside(p))
908 			return true;
909 
910 		if (event.MouseInput.isLeftPressed() &&
911 				(isPointInside(p) ||
912 				 event.MouseInput.Event == EMIE_MOUSE_MOVED)) {
913 			s32 sel_column = 0;
914 			bool sel_doubleclick = (event.MouseInput.Event
915 					== EMIE_LMOUSE_DOUBLE_CLICK);
916 			bool plusminus_clicked = false;
917 
918 			// For certain events (left click), report column
919 			// Also open/close subtrees when the +/- is clicked
920 			if (cell && (
921 					event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN ||
922 					event.MouseInput.Event == EMIE_LMOUSE_DOUBLE_CLICK ||
923 					event.MouseInput.Event == EMIE_LMOUSE_TRIPLE_CLICK)) {
924 				sel_column = cell->reported_column;
925 				if (cell->content_type == COLUMN_TYPE_TREE)
926 					plusminus_clicked = true;
927 			}
928 
929 			if (plusminus_clicked) {
930 				if (event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN) {
931 					toggleVisibleTree(row_i, 0, false);
932 				}
933 			}
934 			else {
935 				// Normal selection
936 				s32 old_selected = m_selected;
937 				m_selected = row_i;
938 				autoScroll();
939 
940 				if (m_selected != old_selected ||
941 						sel_column >= 1 ||
942 						sel_doubleclick) {
943 					sendTableEvent(sel_column, sel_doubleclick);
944 				}
945 
946 				// Treeview: double click opens/closes trees
947 				if (m_has_tree_column && sel_doubleclick) {
948 					toggleVisibleTree(m_selected, 0, false);
949 				}
950 			}
951 		}
952 		return true;
953 	}
954 	if (event.EventType == EET_GUI_EVENT &&
955 			event.GUIEvent.EventType == gui::EGET_SCROLL_BAR_CHANGED &&
956 			event.GUIEvent.Caller == m_scrollbar) {
957 		// Don't pass events from our scrollbar to the parent
958 		return true;
959 	}
960 
961 	return IGUIElement::OnEvent(event);
962 }
963 
964 /******************************************************************************/
965 /* GUITable helper functions                                                  */
966 /******************************************************************************/
967 
allocString(const std::string & text)968 s32 GUITable::allocString(const std::string &text)
969 {
970 	std::map<std::string, s32>::iterator it = m_alloc_strings.find(text);
971 	if (it == m_alloc_strings.end()) {
972 		s32 id = m_strings.size();
973 		std::wstring wtext = utf8_to_wide(text);
974 		m_strings.emplace_back(wtext.c_str());
975 		m_alloc_strings.insert(std::make_pair(text, id));
976 		return id;
977 	}
978 
979 	return it->second;
980 }
981 
allocImage(const std::string & imagename)982 s32 GUITable::allocImage(const std::string &imagename)
983 {
984 	std::map<std::string, s32>::iterator it = m_alloc_images.find(imagename);
985 	if (it == m_alloc_images.end()) {
986 		s32 id = m_images.size();
987 		m_images.push_back(m_tsrc->getTexture(imagename));
988 		m_alloc_images.insert(std::make_pair(imagename, id));
989 		return id;
990 	}
991 
992 	return it->second;
993 }
994 
allocationComplete()995 void GUITable::allocationComplete()
996 {
997 	// Called when done with creating rows and cells from table data,
998 	// i.e. when allocString and allocImage won't be called anymore
999 	m_alloc_strings.clear();
1000 	m_alloc_images.clear();
1001 }
1002 
getRow(s32 i) const1003 const GUITable::Row* GUITable::getRow(s32 i) const
1004 {
1005 	if (i >= 0 && i < (s32) m_visible_rows.size())
1006 		return &m_rows[m_visible_rows[i]];
1007 
1008 	return NULL;
1009 }
1010 
doesRowStartWith(const Row * row,const core::stringw & str) const1011 bool GUITable::doesRowStartWith(const Row *row, const core::stringw &str) const
1012 {
1013 	if (row == NULL)
1014 		return false;
1015 
1016 	for (s32 j = 0; j < row->cellcount; ++j) {
1017 		Cell *cell = &row->cells[j];
1018 		if (cell->content_type == COLUMN_TYPE_TEXT) {
1019 			const core::stringw &cellstr = m_strings[cell->content_index];
1020 			if (cellstr.size() >= str.size() &&
1021 					str.equals_ignore_case(cellstr.subString(0, str.size())))
1022 				return true;
1023 		}
1024 	}
1025 	return false;
1026 }
1027 
getRowAt(s32 y,bool & really_hovering) const1028 s32 GUITable::getRowAt(s32 y, bool &really_hovering) const
1029 {
1030 	really_hovering = false;
1031 
1032 	s32 rowcount = m_visible_rows.size();
1033 	if (rowcount == 0)
1034 		return -1;
1035 
1036 	// Use arithmetic to find row
1037 	s32 rel_y = y - AbsoluteRect.UpperLeftCorner.Y - 1;
1038 	s32 i = (rel_y + m_scrollbar->getPos()) / m_rowheight;
1039 
1040 	if (i >= 0 && i < rowcount) {
1041 		really_hovering = true;
1042 		return i;
1043 	}
1044 	if (i < 0)
1045 		return 0;
1046 
1047 	return rowcount - 1;
1048 }
1049 
getCellAt(s32 x,s32 row_i) const1050 s32 GUITable::getCellAt(s32 x, s32 row_i) const
1051 {
1052 	const Row *row = getRow(row_i);
1053 	if (row == NULL)
1054 		return -1;
1055 
1056 	// Use binary search to find cell in row
1057 	s32 rel_x = x - AbsoluteRect.UpperLeftCorner.X - 1;
1058 	s32 jmin = 0;
1059 	s32 jmax = row->cellcount - 1;
1060 	while (jmin < jmax) {
1061 		s32 pivot = jmin + (jmax - jmin) / 2;
1062 		assert(pivot >= 0 && pivot < row->cellcount);
1063 		const Cell *cell = &row->cells[pivot];
1064 
1065 		if (rel_x >= cell->xmin && rel_x <= cell->xmax)
1066 			return pivot;
1067 
1068 		if (rel_x < cell->xmin)
1069 			jmax = pivot - 1;
1070 		else
1071 			jmin = pivot + 1;
1072 	}
1073 
1074 	if (jmin >= 0 && jmin < row->cellcount &&
1075 			rel_x >= row->cells[jmin].xmin &&
1076 			rel_x <= row->cells[jmin].xmax)
1077 		return jmin;
1078 
1079 	return -1;
1080 }
1081 
autoScroll()1082 void GUITable::autoScroll()
1083 {
1084 	if (m_selected >= 0) {
1085 		s32 pos = m_scrollbar->getPos();
1086 		s32 maxpos = m_selected * m_rowheight;
1087 		s32 minpos = maxpos - (AbsoluteRect.getHeight() - m_rowheight);
1088 		if (pos > maxpos)
1089 			m_scrollbar->setPos(maxpos);
1090 		else if (pos < minpos)
1091 			m_scrollbar->setPos(minpos);
1092 	}
1093 }
1094 
updateScrollBar()1095 void GUITable::updateScrollBar()
1096 {
1097 	s32 totalheight = m_rowheight * m_visible_rows.size();
1098 	s32 scrollmax = MYMAX(0, totalheight - AbsoluteRect.getHeight());
1099 	m_scrollbar->setVisible(scrollmax > 0);
1100 	m_scrollbar->setMax(scrollmax);
1101 	m_scrollbar->setSmallStep(m_rowheight);
1102 	m_scrollbar->setLargeStep(2 * m_rowheight);
1103 	m_scrollbar->setPageSize(totalheight);
1104 }
1105 
sendTableEvent(s32 column,bool doubleclick)1106 void GUITable::sendTableEvent(s32 column, bool doubleclick)
1107 {
1108 	m_sel_column = column;
1109 	m_sel_doubleclick = doubleclick;
1110 	if (Parent) {
1111 		SEvent e;
1112 		memset(&e, 0, sizeof e);
1113 		e.EventType = EET_GUI_EVENT;
1114 		e.GUIEvent.Caller = this;
1115 		e.GUIEvent.Element = 0;
1116 		e.GUIEvent.EventType = gui::EGET_TABLE_CHANGED;
1117 		Parent->OnEvent(e);
1118 	}
1119 }
1120 
getOpenedTrees(std::set<s32> & opened_trees) const1121 void GUITable::getOpenedTrees(std::set<s32> &opened_trees) const
1122 {
1123 	opened_trees.clear();
1124 	s32 rowcount = m_rows.size();
1125 	for (s32 i = 0; i < rowcount - 1; ++i) {
1126 		if (m_rows[i].indent < m_rows[i+1].indent &&
1127 				m_rows[i+1].visible_index != -2)
1128 			opened_trees.insert(i);
1129 	}
1130 }
1131 
setOpenedTrees(const std::set<s32> & opened_trees)1132 void GUITable::setOpenedTrees(const std::set<s32> &opened_trees)
1133 {
1134 	s32 old_selected = -1;
1135 	if (m_selected >= 0)
1136 		old_selected = m_visible_rows[m_selected];
1137 
1138 	std::vector<s32> parents;
1139 	std::vector<s32> closed_parents;
1140 
1141 	m_visible_rows.clear();
1142 
1143 	for (size_t i = 0; i < m_rows.size(); ++i) {
1144 		Row *row = &m_rows[i];
1145 
1146 		// Update list of ancestors
1147 		while (!parents.empty() && m_rows[parents.back()].indent >= row->indent)
1148 			parents.pop_back();
1149 		while (!closed_parents.empty() &&
1150 				m_rows[closed_parents.back()].indent >= row->indent)
1151 			closed_parents.pop_back();
1152 
1153 		assert(closed_parents.size() <= parents.size());
1154 
1155 		if (closed_parents.empty()) {
1156 			// Visible row
1157 			row->visible_index = m_visible_rows.size();
1158 			m_visible_rows.push_back(i);
1159 		}
1160 		else if (parents.back() == closed_parents.back()) {
1161 			// Invisible row, direct parent is closed
1162 			row->visible_index = -2;
1163 		}
1164 		else {
1165 			// Invisible row, direct parent is open, some ancestor is closed
1166 			row->visible_index = -1;
1167 		}
1168 
1169 		// If not a leaf, add to parents list
1170 		if (i < m_rows.size()-1 && row->indent < m_rows[i+1].indent) {
1171 			parents.push_back(i);
1172 
1173 			s32 content_index = 0; // "-", open
1174 			if (opened_trees.count(i) == 0) {
1175 				closed_parents.push_back(i);
1176 				content_index = 1; // "+", closed
1177 			}
1178 
1179 			// Update all cells of type "tree"
1180 			for (s32 j = 0; j < row->cellcount; ++j)
1181 				if (row->cells[j].content_type == COLUMN_TYPE_TREE)
1182 					row->cells[j].content_index = content_index;
1183 		}
1184 	}
1185 
1186 	updateScrollBar();
1187 
1188 	// m_selected must be updated since it is a visible row index
1189 	if (old_selected >= 0)
1190 		m_selected = m_rows[old_selected].visible_index;
1191 }
1192 
openTree(s32 to_open)1193 void GUITable::openTree(s32 to_open)
1194 {
1195 	std::set<s32> opened_trees;
1196 	getOpenedTrees(opened_trees);
1197 	opened_trees.insert(to_open);
1198 	setOpenedTrees(opened_trees);
1199 }
1200 
closeTree(s32 to_close)1201 void GUITable::closeTree(s32 to_close)
1202 {
1203 	std::set<s32> opened_trees;
1204 	getOpenedTrees(opened_trees);
1205 	opened_trees.erase(to_close);
1206 	setOpenedTrees(opened_trees);
1207 }
1208 
1209 // The following function takes a visible row index (hidden rows skipped)
1210 // dir: -1 = left (close), 0 = auto (toggle), 1 = right (open)
toggleVisibleTree(s32 row_i,int dir,bool move_selection)1211 void GUITable::toggleVisibleTree(s32 row_i, int dir, bool move_selection)
1212 {
1213 	// Check if the chosen tree is currently open
1214 	const Row *row = getRow(row_i);
1215 	if (row == NULL)
1216 		return;
1217 
1218 	bool was_open = false;
1219 	for (s32 j = 0; j < row->cellcount; ++j) {
1220 		if (row->cells[j].content_type == COLUMN_TYPE_TREE) {
1221 			was_open = row->cells[j].content_index == 0;
1222 			break;
1223 		}
1224 	}
1225 
1226 	// Check if the chosen tree should be opened
1227 	bool do_open = !was_open;
1228 	if (dir < 0)
1229 		do_open = false;
1230 	else if (dir > 0)
1231 		do_open = true;
1232 
1233 	// Close or open the tree; the heavy lifting is done by setOpenedTrees
1234 	if (was_open && !do_open)
1235 		closeTree(m_visible_rows[row_i]);
1236 	else if (!was_open && do_open)
1237 		openTree(m_visible_rows[row_i]);
1238 
1239 	// Change selected row if requested by caller,
1240 	// this is useful for keyboard navigation
1241 	if (move_selection) {
1242 		s32 sel = row_i;
1243 		if (was_open && do_open) {
1244 			// Move selection to first child
1245 			const Row *maybe_child = getRow(sel + 1);
1246 			if (maybe_child && maybe_child->indent > row->indent)
1247 				sel++;
1248 		}
1249 		else if (!was_open && !do_open) {
1250 			// Move selection to parent
1251 			assert(getRow(sel) != NULL);
1252 			while (sel > 0 && getRow(sel - 1)->indent >= row->indent)
1253 				sel--;
1254 			sel--;
1255 			if (sel < 0)  // was root already selected?
1256 				sel = row_i;
1257 		}
1258 		if (sel != m_selected) {
1259 			m_selected = sel;
1260 			autoScroll();
1261 			sendTableEvent(0, false);
1262 		}
1263 	}
1264 }
1265 
alignContent(Cell * cell,s32 xmax,s32 content_width,s32 align)1266 void GUITable::alignContent(Cell *cell, s32 xmax, s32 content_width, s32 align)
1267 {
1268 	// requires that cell.xmin, cell.xmax are properly set
1269 	// align = 0: left aligned, 1: centered, 2: right aligned, 3: inline
1270 	if (align == 0) {
1271 		cell->xpos = cell->xmin;
1272 		cell->xmax = xmax;
1273 	}
1274 	else if (align == 1) {
1275 		cell->xpos = (cell->xmin + xmax - content_width) / 2;
1276 		cell->xmax = xmax;
1277 	}
1278 	else if (align == 2) {
1279 		cell->xpos = xmax - content_width;
1280 		cell->xmax = xmax;
1281 	}
1282 	else {
1283 		// inline alignment: the cells of the column don't have an aligned
1284 		// right border, the right border of each cell depends on the content
1285 		cell->xpos = cell->xmin;
1286 		cell->xmax = cell->xmin + content_width;
1287 	}
1288 }
1289