]> git.lizzy.rs Git - dragonfireclient.git/blob - src/gui/guiTable.cpp
Android: Improve UI scaling on smaller high-density displays (#7834)
[dragonfireclient.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 <IGUIScrollBar.h>
29 #include "client/renderingengine.h"
30 #include "debug.h"
31 #include "log.h"
32 #include "client/tile.h"
33 #include "gettime.h"
34 #include "util/string.h"
35 #include "util/numeric.h"
36 #include "util/string.h" // for parseColorString()
37 #include "settings.h" // for settings
38 #include "porting.h" // for dpi
39 #include "guiscalingfilter.h"
40
41 /*
42         GUITable
43 */
44
45 GUITable::GUITable(gui::IGUIEnvironment *env,
46                 gui::IGUIElement* parent, s32 id,
47                 core::rect<s32> rectangle,
48                 ISimpleTextureSource *tsrc
49 ):
50         gui::IGUIElement(gui::EGUIET_ELEMENT, env, parent, id, rectangle),
51         m_tsrc(tsrc)
52 {
53         assert(tsrc != NULL);
54
55         gui::IGUISkin* skin = Environment->getSkin();
56
57         m_font = skin->getFont();
58         if (m_font) {
59                 m_font->grab();
60                 m_rowheight = m_font->getDimension(L"A").Height + 4;
61                 m_rowheight = MYMAX(m_rowheight, 1);
62         }
63
64         const s32 s = skin->getSize(gui::EGDS_SCROLLBAR_SIZE);
65         m_scrollbar = Environment->addScrollBar(false,
66                         core::rect<s32>(RelativeRect.getWidth() - s,
67                                         0,
68                                         RelativeRect.getWidth(),
69                                         RelativeRect.getHeight()),
70                         this, -1);
71         m_scrollbar->setSubElement(true);
72         m_scrollbar->setTabStop(false);
73         m_scrollbar->setAlignment(gui::EGUIA_LOWERRIGHT, gui::EGUIA_LOWERRIGHT,
74                         gui::EGUIA_UPPERLEFT, gui::EGUIA_LOWERRIGHT);
75         m_scrollbar->setVisible(false);
76         m_scrollbar->setPos(0);
77
78         setTabStop(true);
79         setTabOrder(-1);
80         updateAbsolutePosition();
81         float density = RenderingEngine::getDisplayDensity();
82 #ifdef __ANDROID__
83         density = 1; // dp scaling is applied by the skin
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");
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         m_scrollbar->remove();
103 }
104
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
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
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
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
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
536 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
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
589 GUITable::DynamicData GUITable::getDynamicData() const
590 {
591         DynamicData dyndata;
592         dyndata.selected = getSelected();
593         dyndata.scrollpos = m_scrollbar->getPos();
594         dyndata.keynav_time = m_keynav_time;
595         dyndata.keynav_buffer = m_keynav_buffer;
596         if (m_has_tree_column)
597                 getOpenedTrees(dyndata.opened_trees);
598         return dyndata;
599 }
600
601 void GUITable::setDynamicData(const DynamicData &dyndata)
602 {
603         if (m_has_tree_column)
604                 setOpenedTrees(dyndata.opened_trees);
605
606         m_keynav_time = dyndata.keynav_time;
607         m_keynav_buffer = dyndata.keynav_buffer;
608
609         setSelected(dyndata.selected);
610         m_sel_column = 0;
611         m_sel_doubleclick = false;
612
613         m_scrollbar->setPos(dyndata.scrollpos);
614 }
615
616 const c8* GUITable::getTypeName() const
617 {
618         return "GUITable";
619 }
620
621 void GUITable::updateAbsolutePosition()
622 {
623         IGUIElement::updateAbsolutePosition();
624         updateScrollBar();
625 }
626
627 void GUITable::draw()
628 {
629         if (!IsVisible)
630                 return;
631
632         gui::IGUISkin *skin = Environment->getSkin();
633
634         // draw background
635
636         bool draw_background = m_background.getAlpha() > 0;
637         if (m_border)
638                 skin->draw3DSunkenPane(this, m_background,
639                                 true, draw_background,
640                                 AbsoluteRect, &AbsoluteClippingRect);
641         else if (draw_background)
642                 skin->draw2DRectangle(this, m_background,
643                                 AbsoluteRect, &AbsoluteClippingRect);
644
645         // get clipping rect
646
647         core::rect<s32> client_clip(AbsoluteRect);
648         client_clip.UpperLeftCorner.Y += 1;
649         client_clip.UpperLeftCorner.X += 1;
650         client_clip.LowerRightCorner.Y -= 1;
651         client_clip.LowerRightCorner.X -= 1;
652         if (m_scrollbar->isVisible()) {
653                 client_clip.LowerRightCorner.X =
654                                 m_scrollbar->getAbsolutePosition().UpperLeftCorner.X;
655         }
656         client_clip.clipAgainst(AbsoluteClippingRect);
657
658         // draw visible rows
659
660         s32 scrollpos = m_scrollbar->getPos();
661         s32 row_min = scrollpos / m_rowheight;
662         s32 row_max = (scrollpos + AbsoluteRect.getHeight() - 1)
663                         / m_rowheight + 1;
664         row_max = MYMIN(row_max, (s32) m_visible_rows.size());
665
666         core::rect<s32> row_rect(AbsoluteRect);
667         if (m_scrollbar->isVisible())
668                 row_rect.LowerRightCorner.X -=
669                         skin->getSize(gui::EGDS_SCROLLBAR_SIZE);
670         row_rect.UpperLeftCorner.Y += row_min * m_rowheight - scrollpos;
671         row_rect.LowerRightCorner.Y = row_rect.UpperLeftCorner.Y + m_rowheight;
672
673         for (s32 i = row_min; i < row_max; ++i) {
674                 Row *row = &m_rows[m_visible_rows[i]];
675                 bool is_sel = i == m_selected;
676                 video::SColor color = m_color;
677
678                 if (is_sel) {
679                         skin->draw2DRectangle(this, m_highlight, row_rect, &client_clip);
680                         color = m_highlight_text;
681                 }
682
683                 for (s32 j = 0; j < row->cellcount; ++j)
684                         drawCell(&row->cells[j], color, row_rect, client_clip);
685
686                 row_rect.UpperLeftCorner.Y += m_rowheight;
687                 row_rect.LowerRightCorner.Y += m_rowheight;
688         }
689
690         // Draw children
691         IGUIElement::draw();
692 }
693
694 void GUITable::drawCell(const Cell *cell, video::SColor color,
695                 const core::rect<s32> &row_rect,
696                 const core::rect<s32> &client_clip)
697 {
698         if ((cell->content_type == COLUMN_TYPE_TEXT)
699                         || (cell->content_type == COLUMN_TYPE_TREE)) {
700
701                 core::rect<s32> text_rect = row_rect;
702                 text_rect.UpperLeftCorner.X = row_rect.UpperLeftCorner.X
703                                 + cell->xpos;
704                 text_rect.LowerRightCorner.X = row_rect.UpperLeftCorner.X
705                                 + cell->xmax;
706
707                 if (cell->color_defined)
708                         color = cell->color;
709
710                 if (m_font) {
711                         if (cell->content_type == COLUMN_TYPE_TEXT)
712                                 m_font->draw(m_strings[cell->content_index],
713                                                 text_rect, color,
714                                                 false, true, &client_clip);
715                         else // tree
716                                 m_font->draw(cell->content_index ? L"+" : L"-",
717                                                 text_rect, color,
718                                                 false, true, &client_clip);
719                 }
720         }
721         else if (cell->content_type == COLUMN_TYPE_IMAGE) {
722
723                 if (cell->content_index < 0)
724                         return;
725
726                 video::IVideoDriver *driver = Environment->getVideoDriver();
727                 video::ITexture *image = m_images[cell->content_index];
728
729                 if (image) {
730                         core::position2d<s32> dest_pos =
731                                         row_rect.UpperLeftCorner;
732                         dest_pos.X += cell->xpos;
733                         core::rect<s32> source_rect(
734                                         core::position2d<s32>(0, 0),
735                                         image->getOriginalSize());
736                         s32 imgh = source_rect.LowerRightCorner.Y;
737                         s32 rowh = row_rect.getHeight();
738                         if (imgh < rowh)
739                                 dest_pos.Y += (rowh - imgh) / 2;
740                         else
741                                 source_rect.LowerRightCorner.Y = rowh;
742
743                         video::SColor color(255, 255, 255, 255);
744
745                         driver->draw2DImage(image, dest_pos, source_rect,
746                                         &client_clip, color, true);
747                 }
748         }
749 }
750
751 bool GUITable::OnEvent(const SEvent &event)
752 {
753         if (!isEnabled())
754                 return IGUIElement::OnEvent(event);
755
756         if (event.EventType == EET_KEY_INPUT_EVENT) {
757                 if (event.KeyInput.PressedDown && (
758                                 event.KeyInput.Key == KEY_DOWN ||
759                                 event.KeyInput.Key == KEY_UP   ||
760                                 event.KeyInput.Key == KEY_HOME ||
761                                 event.KeyInput.Key == KEY_END  ||
762                                 event.KeyInput.Key == KEY_NEXT ||
763                                 event.KeyInput.Key == KEY_PRIOR)) {
764                         s32 offset = 0;
765                         switch (event.KeyInput.Key) {
766                                 case KEY_DOWN:
767                                         offset = 1;
768                                         break;
769                                 case KEY_UP:
770                                         offset = -1;
771                                         break;
772                                 case KEY_HOME:
773                                         offset = - (s32) m_visible_rows.size();
774                                         break;
775                                 case KEY_END:
776                                         offset = m_visible_rows.size();
777                                         break;
778                                 case KEY_NEXT:
779                                         offset = AbsoluteRect.getHeight() / m_rowheight;
780                                         break;
781                                 case KEY_PRIOR:
782                                         offset = - (s32) (AbsoluteRect.getHeight() / m_rowheight);
783                                         break;
784                                 default:
785                                         break;
786                         }
787                         s32 old_selected = m_selected;
788                         s32 rowcount = m_visible_rows.size();
789                         if (rowcount != 0) {
790                                 m_selected = rangelim(m_selected + offset, 0, rowcount-1);
791                                 autoScroll();
792                         }
793
794                         if (m_selected != old_selected)
795                                 sendTableEvent(0, false);
796
797                         return true;
798                 }
799
800                 if (event.KeyInput.PressedDown && (
801                                 event.KeyInput.Key == KEY_LEFT ||
802                                 event.KeyInput.Key == KEY_RIGHT)) {
803                         // Open/close subtree via keyboard
804                         if (m_selected >= 0) {
805                                 int dir = event.KeyInput.Key == KEY_LEFT ? -1 : 1;
806                                 toggleVisibleTree(m_selected, dir, true);
807                         }
808                         return true;
809                 }
810                 else if (!event.KeyInput.PressedDown && (
811                                 event.KeyInput.Key == KEY_RETURN ||
812                                 event.KeyInput.Key == KEY_SPACE)) {
813                         sendTableEvent(0, true);
814                         return true;
815                 }
816                 else if (event.KeyInput.Key == KEY_ESCAPE ||
817                                 event.KeyInput.Key == KEY_SPACE) {
818                         // pass to parent
819                 }
820                 else if (event.KeyInput.PressedDown && event.KeyInput.Char) {
821                         // change selection based on text as it is typed
822                         u64 now = porting::getTimeMs();
823                         if (now - m_keynav_time >= 500)
824                                 m_keynav_buffer = L"";
825                         m_keynav_time = now;
826
827                         // add to key buffer if not a key repeat
828                         if (!(m_keynav_buffer.size() == 1 &&
829                                         m_keynav_buffer[0] == event.KeyInput.Char)) {
830                                 m_keynav_buffer.append(event.KeyInput.Char);
831                         }
832
833                         // find the selected item, starting at the current selection
834                         // don't change selection if the key buffer matches the current item
835                         s32 old_selected = m_selected;
836                         s32 start = MYMAX(m_selected, 0);
837                         s32 rowcount = m_visible_rows.size();
838                         for (s32 k = 1; k < rowcount; ++k) {
839                                 s32 current = start + k;
840                                 if (current >= rowcount)
841                                         current -= rowcount;
842                                 if (doesRowStartWith(getRow(current), m_keynav_buffer)) {
843                                         m_selected = current;
844                                         break;
845                                 }
846                         }
847                         autoScroll();
848                         if (m_selected != old_selected)
849                                 sendTableEvent(0, false);
850
851                         return true;
852                 }
853         }
854         if (event.EventType == EET_MOUSE_INPUT_EVENT) {
855                 core::position2d<s32> p(event.MouseInput.X, event.MouseInput.Y);
856
857                 if (event.MouseInput.Event == EMIE_MOUSE_WHEEL) {
858                         m_scrollbar->setPos(m_scrollbar->getPos() +
859                                         (event.MouseInput.Wheel < 0 ? -3 : 3) *
860                                         - (s32) m_rowheight / 2);
861                         return true;
862                 }
863
864                 // Find hovered row and cell
865                 bool really_hovering = false;
866                 s32 row_i = getRowAt(p.Y, really_hovering);
867                 const Cell *cell = NULL;
868                 if (really_hovering) {
869                         s32 cell_j = getCellAt(p.X, row_i);
870                         if (cell_j >= 0)
871                                 cell = &(getRow(row_i)->cells[cell_j]);
872                 }
873
874                 // Update tooltip
875                 setToolTipText(cell ? m_strings[cell->tooltip_index].c_str() : L"");
876
877                 // Fix for #1567/#1806:
878                 // IGUIScrollBar passes double click events to its parent,
879                 // which we don't want. Detect this case and discard the event
880                 if (event.MouseInput.Event != EMIE_MOUSE_MOVED &&
881                                 m_scrollbar->isVisible() &&
882                                 m_scrollbar->isPointInside(p))
883                         return true;
884
885                 if (event.MouseInput.isLeftPressed() &&
886                                 (isPointInside(p) ||
887                                  event.MouseInput.Event == EMIE_MOUSE_MOVED)) {
888                         s32 sel_column = 0;
889                         bool sel_doubleclick = (event.MouseInput.Event
890                                         == EMIE_LMOUSE_DOUBLE_CLICK);
891                         bool plusminus_clicked = false;
892
893                         // For certain events (left click), report column
894                         // Also open/close subtrees when the +/- is clicked
895                         if (cell && (
896                                         event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN ||
897                                         event.MouseInput.Event == EMIE_LMOUSE_DOUBLE_CLICK ||
898                                         event.MouseInput.Event == EMIE_LMOUSE_TRIPLE_CLICK)) {
899                                 sel_column = cell->reported_column;
900                                 if (cell->content_type == COLUMN_TYPE_TREE)
901                                         plusminus_clicked = true;
902                         }
903
904                         if (plusminus_clicked) {
905                                 if (event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN) {
906                                         toggleVisibleTree(row_i, 0, false);
907                                 }
908                         }
909                         else {
910                                 // Normal selection
911                                 s32 old_selected = m_selected;
912                                 m_selected = row_i;
913                                 autoScroll();
914
915                                 if (m_selected != old_selected ||
916                                                 sel_column >= 1 ||
917                                                 sel_doubleclick) {
918                                         sendTableEvent(sel_column, sel_doubleclick);
919                                 }
920
921                                 // Treeview: double click opens/closes trees
922                                 if (m_has_tree_column && sel_doubleclick) {
923                                         toggleVisibleTree(m_selected, 0, false);
924                                 }
925                         }
926                 }
927                 return true;
928         }
929         if (event.EventType == EET_GUI_EVENT &&
930                         event.GUIEvent.EventType == gui::EGET_SCROLL_BAR_CHANGED &&
931                         event.GUIEvent.Caller == m_scrollbar) {
932                 // Don't pass events from our scrollbar to the parent
933                 return true;
934         }
935
936         return IGUIElement::OnEvent(event);
937 }
938
939 /******************************************************************************/
940 /* GUITable helper functions                                                  */
941 /******************************************************************************/
942
943 s32 GUITable::allocString(const std::string &text)
944 {
945         std::map<std::string, s32>::iterator it = m_alloc_strings.find(text);
946         if (it == m_alloc_strings.end()) {
947                 s32 id = m_strings.size();
948                 std::wstring wtext = utf8_to_wide(text);
949                 m_strings.emplace_back(wtext.c_str());
950                 m_alloc_strings.insert(std::make_pair(text, id));
951                 return id;
952         }
953
954         return it->second;
955 }
956
957 s32 GUITable::allocImage(const std::string &imagename)
958 {
959         std::map<std::string, s32>::iterator it = m_alloc_images.find(imagename);
960         if (it == m_alloc_images.end()) {
961                 s32 id = m_images.size();
962                 m_images.push_back(m_tsrc->getTexture(imagename));
963                 m_alloc_images.insert(std::make_pair(imagename, id));
964                 return id;
965         }
966
967         return it->second;
968 }
969
970 void GUITable::allocationComplete()
971 {
972         // Called when done with creating rows and cells from table data,
973         // i.e. when allocString and allocImage won't be called anymore
974         m_alloc_strings.clear();
975         m_alloc_images.clear();
976 }
977
978 const GUITable::Row* GUITable::getRow(s32 i) const
979 {
980         if (i >= 0 && i < (s32) m_visible_rows.size())
981                 return &m_rows[m_visible_rows[i]];
982
983         return NULL;
984 }
985
986 bool GUITable::doesRowStartWith(const Row *row, const core::stringw &str) const
987 {
988         if (row == NULL)
989                 return false;
990
991         for (s32 j = 0; j < row->cellcount; ++j) {
992                 Cell *cell = &row->cells[j];
993                 if (cell->content_type == COLUMN_TYPE_TEXT) {
994                         const core::stringw &cellstr = m_strings[cell->content_index];
995                         if (cellstr.size() >= str.size() &&
996                                         str.equals_ignore_case(cellstr.subString(0, str.size())))
997                                 return true;
998                 }
999         }
1000         return false;
1001 }
1002
1003 s32 GUITable::getRowAt(s32 y, bool &really_hovering) const
1004 {
1005         really_hovering = false;
1006
1007         s32 rowcount = m_visible_rows.size();
1008         if (rowcount == 0)
1009                 return -1;
1010
1011         // Use arithmetic to find row
1012         s32 rel_y = y - AbsoluteRect.UpperLeftCorner.Y - 1;
1013         s32 i = (rel_y + m_scrollbar->getPos()) / m_rowheight;
1014
1015         if (i >= 0 && i < rowcount) {
1016                 really_hovering = true;
1017                 return i;
1018         }
1019         if (i < 0)
1020                 return 0;
1021
1022         return rowcount - 1;
1023 }
1024
1025 s32 GUITable::getCellAt(s32 x, s32 row_i) const
1026 {
1027         const Row *row = getRow(row_i);
1028         if (row == NULL)
1029                 return -1;
1030
1031         // Use binary search to find cell in row
1032         s32 rel_x = x - AbsoluteRect.UpperLeftCorner.X - 1;
1033         s32 jmin = 0;
1034         s32 jmax = row->cellcount - 1;
1035         while (jmin < jmax) {
1036                 s32 pivot = jmin + (jmax - jmin) / 2;
1037                 assert(pivot >= 0 && pivot < row->cellcount);
1038                 const Cell *cell = &row->cells[pivot];
1039
1040                 if (rel_x >= cell->xmin && rel_x <= cell->xmax)
1041                         return pivot;
1042
1043                 if (rel_x < cell->xmin)
1044                         jmax = pivot - 1;
1045                 else
1046                         jmin = pivot + 1;
1047         }
1048
1049         if (jmin >= 0 && jmin < row->cellcount &&
1050                         rel_x >= row->cells[jmin].xmin &&
1051                         rel_x <= row->cells[jmin].xmax)
1052                 return jmin;
1053
1054         return -1;
1055 }
1056
1057 void GUITable::autoScroll()
1058 {
1059         if (m_selected >= 0) {
1060                 s32 pos = m_scrollbar->getPos();
1061                 s32 maxpos = m_selected * m_rowheight;
1062                 s32 minpos = maxpos - (AbsoluteRect.getHeight() - m_rowheight);
1063                 if (pos > maxpos)
1064                         m_scrollbar->setPos(maxpos);
1065                 else if (pos < minpos)
1066                         m_scrollbar->setPos(minpos);
1067         }
1068 }
1069
1070 void GUITable::updateScrollBar()
1071 {
1072         s32 totalheight = m_rowheight * m_visible_rows.size();
1073         s32 scrollmax = MYMAX(0, totalheight - AbsoluteRect.getHeight());
1074         m_scrollbar->setVisible(scrollmax > 0);
1075         m_scrollbar->setMax(scrollmax);
1076         m_scrollbar->setSmallStep(m_rowheight);
1077         m_scrollbar->setLargeStep(2 * m_rowheight);
1078 }
1079
1080 void GUITable::sendTableEvent(s32 column, bool doubleclick)
1081 {
1082         m_sel_column = column;
1083         m_sel_doubleclick = doubleclick;
1084         if (Parent) {
1085                 SEvent e;
1086                 memset(&e, 0, sizeof e);
1087                 e.EventType = EET_GUI_EVENT;
1088                 e.GUIEvent.Caller = this;
1089                 e.GUIEvent.Element = 0;
1090                 e.GUIEvent.EventType = gui::EGET_TABLE_CHANGED;
1091                 Parent->OnEvent(e);
1092         }
1093 }
1094
1095 void GUITable::getOpenedTrees(std::set<s32> &opened_trees) const
1096 {
1097         opened_trees.clear();
1098         s32 rowcount = m_rows.size();
1099         for (s32 i = 0; i < rowcount - 1; ++i) {
1100                 if (m_rows[i].indent < m_rows[i+1].indent &&
1101                                 m_rows[i+1].visible_index != -2)
1102                         opened_trees.insert(i);
1103         }
1104 }
1105
1106 void GUITable::setOpenedTrees(const std::set<s32> &opened_trees)
1107 {
1108         s32 old_selected = -1;
1109         if (m_selected >= 0)
1110                 old_selected = m_visible_rows[m_selected];
1111
1112         std::vector<s32> parents;
1113         std::vector<s32> closed_parents;
1114
1115         m_visible_rows.clear();
1116
1117         for (size_t i = 0; i < m_rows.size(); ++i) {
1118                 Row *row = &m_rows[i];
1119
1120                 // Update list of ancestors
1121                 while (!parents.empty() && m_rows[parents.back()].indent >= row->indent)
1122                         parents.pop_back();
1123                 while (!closed_parents.empty() &&
1124                                 m_rows[closed_parents.back()].indent >= row->indent)
1125                         closed_parents.pop_back();
1126
1127                 assert(closed_parents.size() <= parents.size());
1128
1129                 if (closed_parents.empty()) {
1130                         // Visible row
1131                         row->visible_index = m_visible_rows.size();
1132                         m_visible_rows.push_back(i);
1133                 }
1134                 else if (parents.back() == closed_parents.back()) {
1135                         // Invisible row, direct parent is closed
1136                         row->visible_index = -2;
1137                 }
1138                 else {
1139                         // Invisible row, direct parent is open, some ancestor is closed
1140                         row->visible_index = -1;
1141                 }
1142
1143                 // If not a leaf, add to parents list
1144                 if (i < m_rows.size()-1 && row->indent < m_rows[i+1].indent) {
1145                         parents.push_back(i);
1146
1147                         s32 content_index = 0; // "-", open
1148                         if (opened_trees.count(i) == 0) {
1149                                 closed_parents.push_back(i);
1150                                 content_index = 1; // "+", closed
1151                         }
1152
1153                         // Update all cells of type "tree"
1154                         for (s32 j = 0; j < row->cellcount; ++j)
1155                                 if (row->cells[j].content_type == COLUMN_TYPE_TREE)
1156                                         row->cells[j].content_index = content_index;
1157                 }
1158         }
1159
1160         updateScrollBar();
1161
1162         // m_selected must be updated since it is a visible row index
1163         if (old_selected >= 0)
1164                 m_selected = m_rows[old_selected].visible_index;
1165 }
1166
1167 void GUITable::openTree(s32 to_open)
1168 {
1169         std::set<s32> opened_trees;
1170         getOpenedTrees(opened_trees);
1171         opened_trees.insert(to_open);
1172         setOpenedTrees(opened_trees);
1173 }
1174
1175 void GUITable::closeTree(s32 to_close)
1176 {
1177         std::set<s32> opened_trees;
1178         getOpenedTrees(opened_trees);
1179         opened_trees.erase(to_close);
1180         setOpenedTrees(opened_trees);
1181 }
1182
1183 // The following function takes a visible row index (hidden rows skipped)
1184 // dir: -1 = left (close), 0 = auto (toggle), 1 = right (open)
1185 void GUITable::toggleVisibleTree(s32 row_i, int dir, bool move_selection)
1186 {
1187         // Check if the chosen tree is currently open
1188         const Row *row = getRow(row_i);
1189         if (row == NULL)
1190                 return;
1191
1192         bool was_open = false;
1193         for (s32 j = 0; j < row->cellcount; ++j) {
1194                 if (row->cells[j].content_type == COLUMN_TYPE_TREE) {
1195                         was_open = row->cells[j].content_index == 0;
1196                         break;
1197                 }
1198         }
1199
1200         // Check if the chosen tree should be opened
1201         bool do_open = !was_open;
1202         if (dir < 0)
1203                 do_open = false;
1204         else if (dir > 0)
1205                 do_open = true;
1206
1207         // Close or open the tree; the heavy lifting is done by setOpenedTrees
1208         if (was_open && !do_open)
1209                 closeTree(m_visible_rows[row_i]);
1210         else if (!was_open && do_open)
1211                 openTree(m_visible_rows[row_i]);
1212
1213         // Change selected row if requested by caller,
1214         // this is useful for keyboard navigation
1215         if (move_selection) {
1216                 s32 sel = row_i;
1217                 if (was_open && do_open) {
1218                         // Move selection to first child
1219                         const Row *maybe_child = getRow(sel + 1);
1220                         if (maybe_child && maybe_child->indent > row->indent)
1221                                 sel++;
1222                 }
1223                 else if (!was_open && !do_open) {
1224                         // Move selection to parent
1225                         assert(getRow(sel) != NULL);
1226                         while (sel > 0 && getRow(sel - 1)->indent >= row->indent)
1227                                 sel--;
1228                         sel--;
1229                         if (sel < 0)  // was root already selected?
1230                                 sel = row_i;
1231                 }
1232                 if (sel != m_selected) {
1233                         m_selected = sel;
1234                         autoScroll();
1235                         sendTableEvent(0, false);
1236                 }
1237         }
1238 }
1239
1240 void GUITable::alignContent(Cell *cell, s32 xmax, s32 content_width, s32 align)
1241 {
1242         // requires that cell.xmin, cell.xmax are properly set
1243         // align = 0: left aligned, 1: centered, 2: right aligned, 3: inline
1244         if (align == 0) {
1245                 cell->xpos = cell->xmin;
1246                 cell->xmax = xmax;
1247         }
1248         else if (align == 1) {
1249                 cell->xpos = (cell->xmin + xmax - content_width) / 2;
1250                 cell->xmax = xmax;
1251         }
1252         else if (align == 2) {
1253                 cell->xpos = xmax - content_width;
1254                 cell->xmax = xmax;
1255         }
1256         else {
1257                 // inline alignment: the cells of the column don't have an aligned
1258                 // right border, the right border of each cell depends on the content
1259                 cell->xpos = cell->xmin;
1260                 cell->xmax = cell->xmin + content_width;
1261         }
1262 }