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