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