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