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