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