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