3 Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
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.
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.
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.
27 #include "client/renderingengine.h"
30 #include "client/tile.h"
32 #include "util/string.h"
33 #include "util/numeric.h"
34 #include "util/string.h" // for parseColorString()
35 #include "settings.h" // for settings
36 #include "porting.h" // for dpi
37 #include "client/guiscalingfilter.h"
43 GUITable::GUITable(gui::IGUIEnvironment *env, gui::IGUIElement *parent, s32 id,
44 core::rect<s32> rectangle, ISimpleTextureSource *tsrc) :
45 gui::IGUIElement(gui::EGUIET_ELEMENT, env, parent, id, rectangle),
50 gui::IGUISkin *skin = Environment->getSkin();
52 m_font = skin->getFont();
55 m_rowheight = m_font->getDimension(L"A").Height + 4;
56 m_rowheight = MYMAX(m_rowheight, 1);
59 const s32 s = skin->getSize(gui::EGDS_SCROLLBAR_SIZE);
60 m_scrollbar = new GUIScrollBar(Environment, this, -1,
61 core::rect<s32>(RelativeRect.getWidth() - s, 0,
62 RelativeRect.getWidth(),
63 RelativeRect.getHeight()),
65 m_scrollbar->setSubElement(true);
66 m_scrollbar->setTabStop(false);
67 m_scrollbar->setAlignment(gui::EGUIA_LOWERRIGHT, gui::EGUIA_LOWERRIGHT,
68 gui::EGUIA_UPPERLEFT, gui::EGUIA_LOWERRIGHT);
69 m_scrollbar->setVisible(false);
70 m_scrollbar->setPos(0);
74 updateAbsolutePosition();
75 float density = RenderingEngine::getDisplayDensity();
77 density = 1; // dp scaling is applied by the skin
79 core::rect<s32> relative_rect = m_scrollbar->getRelativePosition();
80 s32 width = (relative_rect.getWidth() / (2.0 / 3.0)) * density *
81 g_settings->getFloat("gui_scaling");
82 m_scrollbar->setRelativePosition(core::rect<s32>(
83 relative_rect.LowerRightCorner.X - width,
84 relative_rect.UpperLeftCorner.Y, relative_rect.LowerRightCorner.X,
85 relative_rect.LowerRightCorner.Y));
90 for (GUITable::Row &row : m_rows)
100 GUITable::Option GUITable::splitOption(const std::string &str)
102 size_t equal_pos = str.find('=');
103 if (equal_pos == std::string::npos)
104 return GUITable::Option(str, "");
106 return GUITable::Option(str.substr(0, equal_pos), str.substr(equal_pos + 1));
109 void GUITable::setTextList(const std::vector<std::string> &content, bool transparent)
114 m_background.setAlpha(0);
118 m_is_textlist = true;
120 s32 empty_string_index = allocString("");
122 m_rows.resize(content.size());
123 for (s32 i = 0; i < (s32)content.size(); ++i) {
124 Row *row = &m_rows[i];
125 row->cells = new Cell[1];
128 row->visible_index = i;
129 m_visible_rows.push_back(i);
131 Cell *cell = row->cells;
133 cell->xmax = 0x7fff; // something large enough
135 cell->content_type = COLUMN_TYPE_TEXT;
136 cell->content_index = empty_string_index;
137 cell->tooltip_index = empty_string_index;
138 cell->color.set(255, 255, 255, 255);
139 cell->color_defined = false;
140 cell->reported_column = 1;
142 // parse row content (color)
143 const std::string &s = content[i];
144 if (s[0] == '#' && s[1] == '#') {
145 // double # to escape
146 cell->content_index = allocString(s.substr(2));
147 } else if (s[0] == '#' && s.size() >= 7 &&
148 parseColorString(s.substr(0, 7), cell->color, false)) {
149 // single # for color
150 cell->color_defined = true;
151 cell->content_index = allocString(s.substr(7));
154 cell->content_index = allocString(s);
158 allocationComplete();
160 // Clamp scroll bar position
164 void GUITable::setTable(const TableOptions &options, const TableColumns &columns,
165 std::vector<std::string> &content)
169 // Naming conventions:
170 // i is always a row index, 0-based
171 // j is always a column index, 0-based
172 // k is another index, for example an option index
174 // Handle a stupid error case... (issue #1187)
175 if (columns.empty()) {
176 TableColumn text_column;
177 text_column.type = "text";
178 TableColumns new_columns;
179 new_columns.push_back(text_column);
180 setTable(options, new_columns, content);
184 // Handle table options
185 video::SColor default_color(255, 255, 255, 255);
187 for (const Option &option : options) {
188 const std::string &name = option.name;
189 const std::string &value = option.value;
191 parseColorString(value, m_color, false);
192 else if (name == "background")
193 parseColorString(value, m_background, false);
194 else if (name == "border")
195 m_border = is_yes(value);
196 else if (name == "highlight")
197 parseColorString(value, m_highlight, false);
198 else if (name == "highlight_text")
199 parseColorString(value, m_highlight_text, false);
200 else if (name == "opendepth")
201 opendepth = stoi(value);
203 errorstream << "Invalid table option: \"" << name << "\""
204 << " (value=\"" << value << "\")" << std::endl;
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.emplace_back("");
219 // Create temporary rows (for processing columns)
222 // Current horizontal position (may different between rows due
223 // to indent/tree columns, or text/image columns with width<0)
225 // Tree indentation level
227 // Next cell: Index into m_strings or m_images
229 // Next cell: Width in pixels
231 // Vector of completed cells in this row
232 std::vector<Cell> cells;
233 // Stores colors and how long they last (maximum column index)
234 std::vector<std::pair<video::SColor, s32>> colors;
236 TempRow() : x(0), indent(0), content_index(0), content_width(0) {}
238 TempRow *rows = new TempRow[rowcount];
240 // Get em width. Pedantically speaking, the width of "M" is not
241 // necessarily the same as the em width, but whatever, close enough.
244 em = m_font->getDimension(L"M").Width;
246 s32 default_tooltip_index = allocString("");
248 std::map<s32, s32> active_image_indices;
250 // Process content in column-major order
251 for (s32 j = 0; j < colcount; ++j) {
253 ColumnType columntype = COLUMN_TYPE_TEXT;
254 if (columns[j].type == "text")
255 columntype = COLUMN_TYPE_TEXT;
256 else if (columns[j].type == "image")
257 columntype = COLUMN_TYPE_IMAGE;
258 else if (columns[j].type == "color")
259 columntype = COLUMN_TYPE_COLOR;
260 else if (columns[j].type == "indent")
261 columntype = COLUMN_TYPE_INDENT;
262 else if (columns[j].type == "tree")
263 columntype = COLUMN_TYPE_TREE;
265 errorstream << "Invalid table column type: \"" << columns[j].type
266 << "\"" << std::endl;
268 // Process column options
269 s32 padding = myround(0.5 * em);
270 s32 tooltip_index = default_tooltip_index;
275 if (columntype == COLUMN_TYPE_INDENT) {
276 padding = 0; // default indent padding
278 if (columntype == COLUMN_TYPE_INDENT || columntype == COLUMN_TYPE_TREE) {
279 width = myround(em * 1.5); // default indent width
282 for (const Option &option : columns[j].options) {
283 const std::string &name = option.name;
284 const std::string &value = option.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")
291 else if (name == "align" && value == "center")
293 else if (name == "align" && value == "right")
295 else if (name == "align" && value == "inline")
297 else if (name == "width")
298 width = myround(stof(value) * em);
299 else if (name == "span" && columntype == COLUMN_TYPE_COLOR)
301 else if (columntype == COLUMN_TYPE_IMAGE && !name.empty() &&
302 string_allowed(name, "0123456789")) {
303 s32 content_index = allocImage(value);
304 active_image_indices.insert(std::make_pair(
305 stoi(name), content_index));
307 errorstream << "Invalid table column option: \"" << name
309 << " (value=\"" << value << "\")"
314 // If current column type can use information from "color" columns,
315 // find out which of those is currently active
316 if (columntype == COLUMN_TYPE_TEXT) {
317 for (s32 i = 0; i < rowcount; ++i) {
318 TempRow *row = &rows[i];
319 while (!row->colors.empty() &&
320 row->colors.back().second < j)
321 row->colors.pop_back();
325 // Make template for new cells
327 newcell.content_type = columntype;
328 newcell.tooltip_index = tooltip_index;
329 newcell.reported_column = j + 1;
331 if (columntype == COLUMN_TYPE_TEXT) {
332 // Find right edge of column
334 for (s32 i = 0; i < rowcount; ++i) {
335 TempRow *row = &rows[i];
337 allocString(content[i * colcount + j]);
338 const core::stringw &text = m_strings[row->content_index];
340 m_font ? m_font->getDimension(text.c_str())
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);
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,
352 newcell.content_index = rows[i].content_index;
353 newcell.color_defined = !rows[i].colors.empty();
354 if (newcell.color_defined)
355 newcell.color = rows[i].colors.back().first;
356 rows[i].cells.push_back(newcell);
357 rows[i].x = newcell.xmax;
359 } else if (columntype == COLUMN_TYPE_IMAGE) {
360 // Find right edge of column
362 for (s32 i = 0; i < rowcount; ++i) {
363 TempRow *row = &rows[i];
364 row->content_index = -1;
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;
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];
379 // Get content width and update xmax
381 image ? image->getOriginalSize().Width
383 row->content_width = MYMAX(row->content_width, width);
384 s32 row_xmax = row->x + padding + row->content_width;
385 xmax = MYMAX(xmax, row_xmax);
387 // Add a new cell (of image type) to each row
388 for (s32 i = 0; i < rowcount; ++i) {
389 newcell.xmin = rows[i].x + padding;
390 alignContent(&newcell, xmax, rows[i].content_width,
392 newcell.content_index = rows[i].content_index;
393 rows[i].cells.push_back(newcell);
394 rows[i].x = newcell.xmax;
396 active_image_indices.clear();
397 } else if (columntype == COLUMN_TYPE_COLOR) {
398 for (s32 i = 0; i < rowcount; ++i) {
399 video::SColor cellcolor(255, 255, 255, 255);
400 if (parseColorString(content[i * colcount + j], cellcolor,
402 rows[i].colors.emplace_back(cellcolor, j + span);
404 } else if (columntype == COLUMN_TYPE_INDENT ||
405 columntype == COLUMN_TYPE_TREE) {
406 // For column type "tree", reserve additional space for +/-
407 // Also enable special processing for treeview-type tables
408 s32 content_width = 0;
409 if (columntype == COLUMN_TYPE_TREE) {
410 content_width = m_font ? m_font->getDimension(L"+").Width
412 m_has_tree_column = true;
414 // Add a new cell (of indent or tree type) to each row
415 for (s32 i = 0; i < rowcount; ++i) {
416 TempRow *row = &rows[i];
418 s32 indentlevel = stoi(content[i * colcount + j]);
419 indentlevel = MYMAX(indentlevel, 0);
420 if (columntype == COLUMN_TYPE_TREE)
421 row->indent = indentlevel;
423 newcell.xmin = row->x + padding;
424 newcell.xpos = newcell.xmin + indentlevel * width;
425 newcell.xmax = newcell.xpos + content_width;
426 newcell.content_index = 0;
427 newcell.color_defined = !rows[i].colors.empty();
428 if (newcell.color_defined)
429 newcell.color = rows[i].colors.back().first;
430 row->cells.push_back(newcell);
431 row->x = newcell.xmax;
436 // Copy temporary rows to not so temporary rows
438 m_rows.resize(rowcount);
439 for (s32 i = 0; i < rowcount; ++i) {
440 Row *row = &m_rows[i];
441 row->cellcount = rows[i].cells.size();
442 row->cells = new Cell[row->cellcount];
443 memcpy((void *)row->cells, (void *)&rows[i].cells[0],
444 row->cellcount * sizeof(Cell));
445 row->indent = rows[i].indent;
446 row->visible_index = i;
447 m_visible_rows.push_back(i);
451 if (m_has_tree_column) {
452 // Treeview: convert tree to indent cells on leaf rows
453 for (s32 i = 0; i < rowcount; ++i) {
454 if (i == rowcount - 1 || m_rows[i].indent >= m_rows[i + 1].indent)
455 for (s32 j = 0; j < m_rows[i].cellcount; ++j)
456 if (m_rows[i].cells[j].content_type ==
458 m_rows[i].cells[j].content_type =
462 // Treeview: close rows according to opendepth option
463 std::set<s32> opened_trees;
464 for (s32 i = 0; i < rowcount; ++i)
465 if (m_rows[i].indent < opendepth)
466 opened_trees.insert(i);
467 setOpenedTrees(opened_trees);
470 // Delete temporary information used only during setTable()
472 allocationComplete();
474 // Clamp scroll bar position
478 void GUITable::clear()
480 // Clean up cells and rows
481 for (GUITable::Row &row : m_rows)
484 m_visible_rows.clear();
486 // Get colors from skin
487 gui::IGUISkin *skin = Environment->getSkin();
488 m_color = skin->getColor(gui::EGDC_BUTTON_TEXT);
489 m_background = skin->getColor(gui::EGDC_3D_HIGH_LIGHT);
490 m_highlight = skin->getColor(gui::EGDC_HIGH_LIGHT);
491 m_highlight_text = skin->getColor(gui::EGDC_HIGH_LIGHT_TEXT);
494 m_is_textlist = false;
495 m_has_tree_column = false;
498 m_sel_doubleclick = false;
500 m_keynav_buffer = L"";
504 m_alloc_strings.clear();
505 m_alloc_images.clear();
508 std::string GUITable::checkEvent()
510 s32 sel = getSelected();
517 std::ostringstream os(std::ios::binary);
518 if (m_sel_doubleclick) {
520 m_sel_doubleclick = false;
525 if (!m_is_textlist) {
526 os << ":" << m_sel_column;
531 s32 GUITable::getSelected() const
536 assert(m_selected >= 0 && m_selected < (s32)m_visible_rows.size());
537 return m_visible_rows[m_selected] + 1;
540 void GUITable::setSelected(s32 index)
542 s32 old_selected = m_selected;
546 m_sel_doubleclick = false;
548 --index; // Switch from 1-based indexing to 0-based indexing
550 s32 rowcount = m_rows.size();
551 if (rowcount == 0 || index < 0) {
555 if (index >= rowcount) {
556 index = rowcount - 1;
559 // If the selected row is not visible, open its ancestors to make it visible
560 bool selection_invisible = m_rows[index].visible_index < 0;
561 if (selection_invisible) {
562 std::set<s32> opened_trees;
563 getOpenedTrees(opened_trees);
564 s32 indent = m_rows[index].indent;
565 for (s32 j = index - 1; j >= 0; --j) {
566 if (m_rows[j].indent < indent) {
567 opened_trees.insert(j);
568 indent = m_rows[j].indent;
571 setOpenedTrees(opened_trees);
575 m_selected = m_rows[index].visible_index;
576 assert(m_selected >= 0 && m_selected < (s32)m_visible_rows.size());
579 if (m_selected != old_selected || selection_invisible) {
584 GUITable::DynamicData GUITable::getDynamicData() const
587 dyndata.selected = getSelected();
588 dyndata.scrollpos = m_scrollbar->getPos();
589 dyndata.keynav_time = m_keynav_time;
590 dyndata.keynav_buffer = m_keynav_buffer;
591 if (m_has_tree_column)
592 getOpenedTrees(dyndata.opened_trees);
596 void GUITable::setDynamicData(const DynamicData &dyndata)
598 if (m_has_tree_column)
599 setOpenedTrees(dyndata.opened_trees);
601 m_keynav_time = dyndata.keynav_time;
602 m_keynav_buffer = dyndata.keynav_buffer;
604 setSelected(dyndata.selected);
606 m_sel_doubleclick = false;
608 m_scrollbar->setPos(dyndata.scrollpos);
611 const c8 *GUITable::getTypeName() const
616 void GUITable::updateAbsolutePosition()
618 IGUIElement::updateAbsolutePosition();
622 void GUITable::draw()
627 gui::IGUISkin *skin = Environment->getSkin();
631 bool draw_background = m_background.getAlpha() > 0;
633 skin->draw3DSunkenPane(this, m_background, true, draw_background,
634 AbsoluteRect, &AbsoluteClippingRect);
635 else if (draw_background)
636 skin->draw2DRectangle(
637 this, m_background, AbsoluteRect, &AbsoluteClippingRect);
641 core::rect<s32> client_clip(AbsoluteRect);
642 client_clip.UpperLeftCorner.Y += 1;
643 client_clip.UpperLeftCorner.X += 1;
644 client_clip.LowerRightCorner.Y -= 1;
645 client_clip.LowerRightCorner.X -= 1;
646 if (m_scrollbar->isVisible()) {
647 client_clip.LowerRightCorner.X =
648 m_scrollbar->getAbsolutePosition().UpperLeftCorner.X;
650 client_clip.clipAgainst(AbsoluteClippingRect);
654 s32 scrollpos = m_scrollbar->getPos();
655 s32 row_min = scrollpos / m_rowheight;
656 s32 row_max = (scrollpos + AbsoluteRect.getHeight() - 1) / m_rowheight + 1;
657 row_max = MYMIN(row_max, (s32)m_visible_rows.size());
659 core::rect<s32> row_rect(AbsoluteRect);
660 if (m_scrollbar->isVisible())
661 row_rect.LowerRightCorner.X -= skin->getSize(gui::EGDS_SCROLLBAR_SIZE);
662 row_rect.UpperLeftCorner.Y += row_min * m_rowheight - scrollpos;
663 row_rect.LowerRightCorner.Y = row_rect.UpperLeftCorner.Y + m_rowheight;
665 for (s32 i = row_min; i < row_max; ++i) {
666 Row *row = &m_rows[m_visible_rows[i]];
667 bool is_sel = i == m_selected;
668 video::SColor color = m_color;
671 skin->draw2DRectangle(this, m_highlight, row_rect, &client_clip);
672 color = m_highlight_text;
675 for (s32 j = 0; j < row->cellcount; ++j)
676 drawCell(&row->cells[j], color, row_rect, client_clip);
678 row_rect.UpperLeftCorner.Y += m_rowheight;
679 row_rect.LowerRightCorner.Y += m_rowheight;
686 void GUITable::drawCell(const Cell *cell, video::SColor color,
687 const core::rect<s32> &row_rect, const core::rect<s32> &client_clip)
689 if ((cell->content_type == COLUMN_TYPE_TEXT) ||
690 (cell->content_type == COLUMN_TYPE_TREE)) {
692 core::rect<s32> text_rect = row_rect;
693 text_rect.UpperLeftCorner.X = row_rect.UpperLeftCorner.X + cell->xpos;
694 text_rect.LowerRightCorner.X = row_rect.UpperLeftCorner.X + cell->xmax;
696 if (cell->color_defined)
700 if (cell->content_type == COLUMN_TYPE_TEXT)
701 m_font->draw(m_strings[cell->content_index], text_rect,
702 color, false, true, &client_clip);
704 m_font->draw(cell->content_index ? L"+" : L"-", text_rect,
705 color, false, true, &client_clip);
707 } else if (cell->content_type == COLUMN_TYPE_IMAGE) {
709 if (cell->content_index < 0)
712 video::IVideoDriver *driver = Environment->getVideoDriver();
713 video::ITexture *image = m_images[cell->content_index];
716 core::position2d<s32> dest_pos = row_rect.UpperLeftCorner;
717 dest_pos.X += cell->xpos;
718 core::rect<s32> source_rect(core::position2d<s32>(0, 0),
719 image->getOriginalSize());
720 s32 imgh = source_rect.LowerRightCorner.Y;
721 s32 rowh = row_rect.getHeight();
723 dest_pos.Y += (rowh - imgh) / 2;
725 source_rect.LowerRightCorner.Y = rowh;
727 video::SColor color(255, 255, 255, 255);
729 driver->draw2DImage(image, dest_pos, source_rect, &client_clip,
735 bool GUITable::OnEvent(const SEvent &event)
738 return IGUIElement::OnEvent(event);
740 if (event.EventType == EET_KEY_INPUT_EVENT) {
741 if (event.KeyInput.PressedDown &&
742 (event.KeyInput.Key == KEY_DOWN ||
743 event.KeyInput.Key == KEY_UP ||
744 event.KeyInput.Key == KEY_HOME ||
745 event.KeyInput.Key == KEY_END ||
746 event.KeyInput.Key == KEY_NEXT ||
747 event.KeyInput.Key == KEY_PRIOR)) {
749 switch (event.KeyInput.Key) {
757 offset = -(s32)m_visible_rows.size();
760 offset = m_visible_rows.size();
763 offset = AbsoluteRect.getHeight() / m_rowheight;
766 offset = -(s32)(AbsoluteRect.getHeight() / m_rowheight);
771 s32 old_selected = m_selected;
772 s32 rowcount = m_visible_rows.size();
774 m_selected = rangelim(
775 m_selected + offset, 0, rowcount - 1);
779 if (m_selected != old_selected)
780 sendTableEvent(0, false);
785 if (event.KeyInput.PressedDown &&
786 (event.KeyInput.Key == KEY_LEFT ||
787 event.KeyInput.Key == KEY_RIGHT)) {
788 // Open/close subtree via keyboard
789 if (m_selected >= 0) {
790 int dir = event.KeyInput.Key == KEY_LEFT ? -1 : 1;
791 toggleVisibleTree(m_selected, dir, true);
794 } else if (!event.KeyInput.PressedDown &&
795 (event.KeyInput.Key == KEY_RETURN ||
796 event.KeyInput.Key == KEY_SPACE)) {
797 sendTableEvent(0, true);
799 } else if (event.KeyInput.Key == KEY_ESCAPE ||
800 event.KeyInput.Key == KEY_SPACE) {
802 } else if (event.KeyInput.PressedDown && event.KeyInput.Char) {
803 // change selection based on text as it is typed
804 u64 now = porting::getTimeMs();
805 if (now - m_keynav_time >= 500)
806 m_keynav_buffer = L"";
809 // add to key buffer if not a key repeat
810 if (!(m_keynav_buffer.size() == 1 &&
811 m_keynav_buffer[0] == event.KeyInput.Char)) {
812 m_keynav_buffer.append(event.KeyInput.Char);
815 // find the selected item, starting at the current selection
816 // don't change selection if the key buffer matches the current
818 s32 old_selected = m_selected;
819 s32 start = MYMAX(m_selected, 0);
820 s32 rowcount = m_visible_rows.size();
821 for (s32 k = 1; k < rowcount; ++k) {
822 s32 current = start + k;
823 if (current >= rowcount)
825 if (doesRowStartWith(getRow(current), m_keynav_buffer)) {
826 m_selected = current;
831 if (m_selected != old_selected)
832 sendTableEvent(0, false);
837 if (event.EventType == EET_MOUSE_INPUT_EVENT) {
838 core::position2d<s32> p(event.MouseInput.X, event.MouseInput.Y);
840 if (event.MouseInput.Event == EMIE_MOUSE_WHEEL) {
841 m_scrollbar->setPos(m_scrollbar->getPos() +
842 (event.MouseInput.Wheel < 0 ? -3 : 3) *
843 -(s32)m_rowheight / 2);
847 // Find hovered row and cell
848 bool really_hovering = false;
849 s32 row_i = getRowAt(p.Y, really_hovering);
850 const Cell *cell = NULL;
851 if (really_hovering) {
852 s32 cell_j = getCellAt(p.X, row_i);
854 cell = &(getRow(row_i)->cells[cell_j]);
858 setToolTipText(cell ? m_strings[cell->tooltip_index].c_str() : L"");
860 // Fix for #1567/#1806:
861 // IGUIScrollBar passes double click events to its parent,
862 // which we don't want. Detect this case and discard the event
863 if (event.MouseInput.Event != EMIE_MOUSE_MOVED &&
864 m_scrollbar->isVisible() && m_scrollbar->isPointInside(p))
867 if (event.MouseInput.isLeftPressed() &&
868 (isPointInside(p) || event.MouseInput.Event ==
871 bool sel_doubleclick = (event.MouseInput.Event ==
872 EMIE_LMOUSE_DOUBLE_CLICK);
873 bool plusminus_clicked = false;
875 // For certain events (left click), report column
876 // Also open/close subtrees when the +/- is clicked
877 if (cell && (event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN ||
878 event.MouseInput.Event ==
879 EMIE_LMOUSE_DOUBLE_CLICK ||
880 event.MouseInput.Event ==
881 EMIE_LMOUSE_TRIPLE_CLICK)) {
882 sel_column = cell->reported_column;
883 if (cell->content_type == COLUMN_TYPE_TREE)
884 plusminus_clicked = true;
887 if (plusminus_clicked) {
888 if (event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN) {
889 toggleVisibleTree(row_i, 0, false);
893 s32 old_selected = m_selected;
897 if (m_selected != old_selected || sel_column >= 1 ||
899 sendTableEvent(sel_column, sel_doubleclick);
902 // Treeview: double click opens/closes trees
903 if (m_has_tree_column && sel_doubleclick) {
904 toggleVisibleTree(m_selected, 0, false);
910 if (event.EventType == EET_GUI_EVENT &&
911 event.GUIEvent.EventType == gui::EGET_SCROLL_BAR_CHANGED &&
912 event.GUIEvent.Caller == m_scrollbar) {
913 // Don't pass events from our scrollbar to the parent
917 return IGUIElement::OnEvent(event);
920 /******************************************************************************/
921 /* GUITable helper functions */
922 /******************************************************************************/
924 s32 GUITable::allocString(const std::string &text)
926 std::map<std::string, s32>::iterator it = m_alloc_strings.find(text);
927 if (it == m_alloc_strings.end()) {
928 s32 id = m_strings.size();
929 std::wstring wtext = utf8_to_wide(text);
930 m_strings.emplace_back(wtext.c_str());
931 m_alloc_strings.insert(std::make_pair(text, id));
938 s32 GUITable::allocImage(const std::string &imagename)
940 std::map<std::string, s32>::iterator it = m_alloc_images.find(imagename);
941 if (it == m_alloc_images.end()) {
942 s32 id = m_images.size();
943 m_images.push_back(m_tsrc->getTexture(imagename));
944 m_alloc_images.insert(std::make_pair(imagename, id));
951 void GUITable::allocationComplete()
953 // Called when done with creating rows and cells from table data,
954 // i.e. when allocString and allocImage won't be called anymore
955 m_alloc_strings.clear();
956 m_alloc_images.clear();
959 const GUITable::Row *GUITable::getRow(s32 i) const
961 if (i >= 0 && i < (s32)m_visible_rows.size())
962 return &m_rows[m_visible_rows[i]];
967 bool GUITable::doesRowStartWith(const Row *row, const core::stringw &str) const
972 for (s32 j = 0; j < row->cellcount; ++j) {
973 Cell *cell = &row->cells[j];
974 if (cell->content_type == COLUMN_TYPE_TEXT) {
975 const core::stringw &cellstr = m_strings[cell->content_index];
976 if (cellstr.size() >= str.size() &&
977 str.equals_ignore_case(
978 cellstr.subString(0, str.size())))
985 s32 GUITable::getRowAt(s32 y, bool &really_hovering) const
987 really_hovering = false;
989 s32 rowcount = m_visible_rows.size();
993 // Use arithmetic to find row
994 s32 rel_y = y - AbsoluteRect.UpperLeftCorner.Y - 1;
995 s32 i = (rel_y + m_scrollbar->getPos()) / m_rowheight;
997 if (i >= 0 && i < rowcount) {
998 really_hovering = true;
1004 return rowcount - 1;
1007 s32 GUITable::getCellAt(s32 x, s32 row_i) const
1009 const Row *row = getRow(row_i);
1013 // Use binary search to find cell in row
1014 s32 rel_x = x - AbsoluteRect.UpperLeftCorner.X - 1;
1016 s32 jmax = row->cellcount - 1;
1017 while (jmin < jmax) {
1018 s32 pivot = jmin + (jmax - jmin) / 2;
1019 assert(pivot >= 0 && pivot < row->cellcount);
1020 const Cell *cell = &row->cells[pivot];
1022 if (rel_x >= cell->xmin && rel_x <= cell->xmax)
1025 if (rel_x < cell->xmin)
1031 if (jmin >= 0 && jmin < row->cellcount && rel_x >= row->cells[jmin].xmin &&
1032 rel_x <= row->cells[jmin].xmax)
1038 void GUITable::autoScroll()
1040 if (m_selected >= 0) {
1041 s32 pos = m_scrollbar->getPos();
1042 s32 maxpos = m_selected * m_rowheight;
1043 s32 minpos = maxpos - (AbsoluteRect.getHeight() - m_rowheight);
1045 m_scrollbar->setPos(maxpos);
1046 else if (pos < minpos)
1047 m_scrollbar->setPos(minpos);
1051 void GUITable::updateScrollBar()
1053 s32 totalheight = m_rowheight * m_visible_rows.size();
1054 s32 scrollmax = MYMAX(0, totalheight - AbsoluteRect.getHeight());
1055 m_scrollbar->setVisible(scrollmax > 0);
1056 m_scrollbar->setMax(scrollmax);
1057 m_scrollbar->setSmallStep(m_rowheight);
1058 m_scrollbar->setLargeStep(2 * m_rowheight);
1059 m_scrollbar->setPageSize(totalheight);
1062 void GUITable::sendTableEvent(s32 column, bool doubleclick)
1064 m_sel_column = column;
1065 m_sel_doubleclick = doubleclick;
1068 memset(&e, 0, sizeof e);
1069 e.EventType = EET_GUI_EVENT;
1070 e.GUIEvent.Caller = this;
1071 e.GUIEvent.Element = 0;
1072 e.GUIEvent.EventType = gui::EGET_TABLE_CHANGED;
1077 void GUITable::getOpenedTrees(std::set<s32> &opened_trees) const
1079 opened_trees.clear();
1080 s32 rowcount = m_rows.size();
1081 for (s32 i = 0; i < rowcount - 1; ++i) {
1082 if (m_rows[i].indent < m_rows[i + 1].indent &&
1083 m_rows[i + 1].visible_index != -2)
1084 opened_trees.insert(i);
1088 void GUITable::setOpenedTrees(const std::set<s32> &opened_trees)
1090 s32 old_selected = -1;
1091 if (m_selected >= 0)
1092 old_selected = m_visible_rows[m_selected];
1094 std::vector<s32> parents;
1095 std::vector<s32> closed_parents;
1097 m_visible_rows.clear();
1099 for (size_t i = 0; i < m_rows.size(); ++i) {
1100 Row *row = &m_rows[i];
1102 // Update list of ancestors
1103 while (!parents.empty() && m_rows[parents.back()].indent >= row->indent)
1105 while (!closed_parents.empty() &&
1106 m_rows[closed_parents.back()].indent >= row->indent)
1107 closed_parents.pop_back();
1109 assert(closed_parents.size() <= parents.size());
1111 if (closed_parents.empty()) {
1113 row->visible_index = m_visible_rows.size();
1114 m_visible_rows.push_back(i);
1115 } else if (parents.back() == closed_parents.back()) {
1116 // Invisible row, direct parent is closed
1117 row->visible_index = -2;
1119 // Invisible row, direct parent is open, some ancestor is closed
1120 row->visible_index = -1;
1123 // If not a leaf, add to parents list
1124 if (i < m_rows.size() - 1 && row->indent < m_rows[i + 1].indent) {
1125 parents.push_back(i);
1127 s32 content_index = 0; // "-", open
1128 if (opened_trees.count(i) == 0) {
1129 closed_parents.push_back(i);
1130 content_index = 1; // "+", closed
1133 // Update all cells of type "tree"
1134 for (s32 j = 0; j < row->cellcount; ++j)
1135 if (row->cells[j].content_type == COLUMN_TYPE_TREE)
1136 row->cells[j].content_index = content_index;
1142 // m_selected must be updated since it is a visible row index
1143 if (old_selected >= 0)
1144 m_selected = m_rows[old_selected].visible_index;
1147 void GUITable::openTree(s32 to_open)
1149 std::set<s32> opened_trees;
1150 getOpenedTrees(opened_trees);
1151 opened_trees.insert(to_open);
1152 setOpenedTrees(opened_trees);
1155 void GUITable::closeTree(s32 to_close)
1157 std::set<s32> opened_trees;
1158 getOpenedTrees(opened_trees);
1159 opened_trees.erase(to_close);
1160 setOpenedTrees(opened_trees);
1163 // The following function takes a visible row index (hidden rows skipped)
1164 // dir: -1 = left (close), 0 = auto (toggle), 1 = right (open)
1165 void GUITable::toggleVisibleTree(s32 row_i, int dir, bool move_selection)
1167 // Check if the chosen tree is currently open
1168 const Row *row = getRow(row_i);
1172 bool was_open = false;
1173 for (s32 j = 0; j < row->cellcount; ++j) {
1174 if (row->cells[j].content_type == COLUMN_TYPE_TREE) {
1175 was_open = row->cells[j].content_index == 0;
1180 // Check if the chosen tree should be opened
1181 bool do_open = !was_open;
1187 // Close or open the tree; the heavy lifting is done by setOpenedTrees
1188 if (was_open && !do_open)
1189 closeTree(m_visible_rows[row_i]);
1190 else if (!was_open && do_open)
1191 openTree(m_visible_rows[row_i]);
1193 // Change selected row if requested by caller,
1194 // this is useful for keyboard navigation
1195 if (move_selection) {
1197 if (was_open && do_open) {
1198 // Move selection to first child
1199 const Row *maybe_child = getRow(sel + 1);
1200 if (maybe_child && maybe_child->indent > row->indent)
1202 } else if (!was_open && !do_open) {
1203 // Move selection to parent
1204 assert(getRow(sel) != NULL);
1205 while (sel > 0 && getRow(sel - 1)->indent >= row->indent)
1208 if (sel < 0) // was root already selected?
1211 if (sel != m_selected) {
1214 sendTableEvent(0, false);
1219 void GUITable::alignContent(Cell *cell, s32 xmax, s32 content_width, s32 align)
1221 // requires that cell.xmin, cell.xmax are properly set
1222 // align = 0: left aligned, 1: centered, 2: right aligned, 3: inline
1224 cell->xpos = cell->xmin;
1226 } else if (align == 1) {
1227 cell->xpos = (cell->xmin + xmax - content_width) / 2;
1229 } else if (align == 2) {
1230 cell->xpos = xmax - content_width;
1233 // inline alignment: the cells of the column don't have an aligned
1234 // right border, the right border of each cell depends on the content
1235 cell->xpos = cell->xmin;
1236 cell->xmax = cell->xmin + content_width;