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