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