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.
28 #include "client/renderingengine.h"
31 #include "client/tile.h"
33 #include "util/string.h"
34 #include "util/numeric.h"
35 #include "util/string.h" // for parseColorString()
36 #include "settings.h" // for settings
37 #include "porting.h" // for dpi
38 #include "client/guiscalingfilter.h"
44 GUITable::GUITable(gui::IGUIEnvironment *env,
45 gui::IGUIElement* parent, s32 id,
46 core::rect<s32> rectangle,
47 ISimpleTextureSource *tsrc
49 gui::IGUIElement(gui::EGUIET_ELEMENT, env, parent, id, rectangle),
54 gui::IGUISkin* skin = Environment->getSkin();
56 m_font = skin->getFont();
59 m_rowheight = m_font->getDimension(L"Ay").Height + 4;
60 m_rowheight = MYMAX(m_rowheight, 1);
63 const s32 s = skin->getSize(gui::EGDS_SCROLLBAR_SIZE);
64 m_scrollbar = new GUIScrollBar(Environment, this, -1,
65 core::rect<s32>(RelativeRect.getWidth() - s,
67 RelativeRect.getWidth(),
68 RelativeRect.getHeight()),
70 m_scrollbar->setSubElement(true);
71 m_scrollbar->setTabStop(false);
72 m_scrollbar->setAlignment(gui::EGUIA_LOWERRIGHT, gui::EGUIA_LOWERRIGHT,
73 gui::EGUIA_UPPERLEFT, gui::EGUIA_LOWERRIGHT);
74 m_scrollbar->setVisible(false);
75 m_scrollbar->setPos(0);
79 updateAbsolutePosition();
80 #ifdef HAVE_TOUCHSCREENGUI
81 float density = 1; // dp scaling is applied by the skin
83 float density = RenderingEngine::getDisplayDensity();
85 core::rect<s32> relative_rect = m_scrollbar->getRelativePosition();
86 s32 width = (relative_rect.getWidth() / (2.0 / 3.0)) * density *
87 g_settings->getFloat("gui_scaling", 0.5f, 20.0f);
88 m_scrollbar->setRelativePosition(core::rect<s32>(
89 relative_rect.LowerRightCorner.X-width,relative_rect.UpperLeftCorner.Y,
90 relative_rect.LowerRightCorner.X,relative_rect.LowerRightCorner.Y
96 for (GUITable::Row &row : m_rows)
106 GUITable::Option GUITable::splitOption(const std::string &str)
108 size_t equal_pos = str.find('=');
109 if (equal_pos == std::string::npos)
110 return GUITable::Option(str, "");
112 return GUITable::Option(str.substr(0, equal_pos),
113 str.substr(equal_pos + 1));
116 void GUITable::setTextList(const std::vector<std::string> &content,
122 m_background.setAlpha(0);
126 m_is_textlist = true;
128 s32 empty_string_index = allocString("");
130 m_rows.resize(content.size());
131 for (s32 i = 0; i < (s32) content.size(); ++i) {
132 Row *row = &m_rows[i];
133 row->cells = new Cell[1];
136 row->visible_index = i;
137 m_visible_rows.push_back(i);
139 Cell *cell = row->cells;
141 cell->xmax = 0x7fff; // something large enough
143 cell->content_type = COLUMN_TYPE_TEXT;
144 cell->content_index = empty_string_index;
145 cell->tooltip_index = empty_string_index;
146 cell->color.set(255, 255, 255, 255);
147 cell->color_defined = false;
148 cell->reported_column = 1;
150 // parse row content (color)
151 const std::string &s = content[i];
152 if (s[0] == '#' && s[1] == '#') {
153 // double # to escape
154 cell->content_index = allocString(s.substr(2));
156 else if (s[0] == '#' && s.size() >= 7 &&
158 s.substr(0,7), cell->color, false)) {
159 // single # for color
160 cell->color_defined = true;
161 cell->content_index = allocString(s.substr(7));
165 cell->content_index = allocString(s);
170 allocationComplete();
172 // Clamp scroll bar position
176 void GUITable::setTable(const TableOptions &options,
177 const TableColumns &columns,
178 std::vector<std::string> &content)
182 // Naming conventions:
183 // i is always a row index, 0-based
184 // j is always a column index, 0-based
185 // k is another index, for example an option index
187 // Handle a stupid error case... (issue #1187)
188 if (columns.empty()) {
189 TableColumn text_column;
190 text_column.type = "text";
191 TableColumns new_columns;
192 new_columns.push_back(text_column);
193 setTable(options, new_columns, content);
197 // Handle table options
198 video::SColor default_color(255, 255, 255, 255);
200 for (const Option &option : options) {
201 const std::string &name = option.name;
202 const std::string &value = option.value;
204 parseColorString(value, m_color, false);
205 else if (name == "background")
206 parseColorString(value, m_background, false);
207 else if (name == "border")
208 m_border = is_yes(value);
209 else if (name == "highlight")
210 parseColorString(value, m_highlight, false);
211 else if (name == "highlight_text")
212 parseColorString(value, m_highlight_text, false);
213 else if (name == "opendepth")
214 opendepth = stoi(value);
216 errorstream<<"Invalid table option: \""<<name<<"\""
217 <<" (value=\""<<value<<"\")"<<std::endl;
220 // Get number of columns and rows
221 // note: error case columns.size() == 0 was handled above
222 s32 colcount = columns.size();
223 assert(colcount >= 1);
224 // rowcount = ceil(cellcount / colcount) but use integer arithmetic
225 s32 rowcount = (content.size() + colcount - 1) / colcount;
226 assert(rowcount >= 0);
227 // Append empty strings to content if there is an incomplete row
228 s32 cellcount = rowcount * colcount;
229 while (content.size() < (u32) cellcount)
230 content.emplace_back("");
232 // Create temporary rows (for processing columns)
234 // Current horizontal position (may different between rows due
235 // to indent/tree columns, or text/image columns with width<0)
237 // Tree indentation level
239 // Next cell: Index into m_strings or m_images
241 // Next cell: Width in pixels
243 // Vector of completed cells in this row
244 std::vector<Cell> cells;
245 // Stores colors and how long they last (maximum column index)
246 std::vector<std::pair<video::SColor, s32> > colors;
248 TempRow(): x(0), indent(0), content_index(0), content_width(0) {}
250 TempRow *rows = new TempRow[rowcount];
252 // Get em width. Pedantically speaking, the width of "M" is not
253 // necessarily the same as the em width, but whatever, close enough.
256 em = m_font->getDimension(L"M").Width;
258 s32 default_tooltip_index = allocString("");
260 std::map<s32, s32> active_image_indices;
262 // Process content in column-major order
263 for (s32 j = 0; j < colcount; ++j) {
265 ColumnType columntype = COLUMN_TYPE_TEXT;
266 if (columns[j].type == "text")
267 columntype = COLUMN_TYPE_TEXT;
268 else if (columns[j].type == "image")
269 columntype = COLUMN_TYPE_IMAGE;
270 else if (columns[j].type == "color")
271 columntype = COLUMN_TYPE_COLOR;
272 else if (columns[j].type == "indent")
273 columntype = COLUMN_TYPE_INDENT;
274 else if (columns[j].type == "tree")
275 columntype = COLUMN_TYPE_TREE;
277 errorstream<<"Invalid table column type: \""
278 <<columns[j].type<<"\""<<std::endl;
280 // Process column options
281 s32 padding = myround(0.5 * em);
282 s32 tooltip_index = default_tooltip_index;
287 if (columntype == COLUMN_TYPE_INDENT) {
288 padding = 0; // default indent padding
290 if (columntype == COLUMN_TYPE_INDENT ||
291 columntype == COLUMN_TYPE_TREE) {
292 width = myround(em * 1.5); // default indent width
295 for (const Option &option : columns[j].options) {
296 const std::string &name = option.name;
297 const std::string &value = option.value;
298 if (name == "padding")
299 padding = myround(stof(value) * em);
300 else if (name == "tooltip")
301 tooltip_index = allocString(value);
302 else if (name == "align" && value == "left")
304 else if (name == "align" && value == "center")
306 else if (name == "align" && value == "right")
308 else if (name == "align" && value == "inline")
310 else if (name == "width")
311 width = myround(stof(value) * em);
312 else if (name == "span" && columntype == COLUMN_TYPE_COLOR)
314 else if (columntype == COLUMN_TYPE_IMAGE &&
316 string_allowed(name, "0123456789")) {
317 s32 content_index = allocImage(value);
318 active_image_indices.insert(std::make_pair(
323 errorstream<<"Invalid table column option: \""<<name<<"\""
324 <<" (value=\""<<value<<"\")"<<std::endl;
328 // If current column type can use information from "color" columns,
329 // find out which of those is currently active
330 if (columntype == COLUMN_TYPE_TEXT) {
331 for (s32 i = 0; i < rowcount; ++i) {
332 TempRow *row = &rows[i];
333 while (!row->colors.empty() && row->colors.back().second < j)
334 row->colors.pop_back();
338 // Make template for new cells
340 newcell.content_type = columntype;
341 newcell.tooltip_index = tooltip_index;
342 newcell.reported_column = j+1;
344 if (columntype == COLUMN_TYPE_TEXT) {
345 // Find right edge of column
347 for (s32 i = 0; i < rowcount; ++i) {
348 TempRow *row = &rows[i];
349 row->content_index = allocString(content[i * colcount + j]);
350 const core::stringw &text = m_strings[row->content_index];
351 row->content_width = m_font ?
352 m_font->getDimension(text.c_str()).Width : 0;
353 row->content_width = MYMAX(row->content_width, width);
354 s32 row_xmax = row->x + padding + row->content_width;
355 xmax = MYMAX(xmax, row_xmax);
357 // Add a new cell (of text type) to each row
358 for (s32 i = 0; i < rowcount; ++i) {
359 newcell.xmin = rows[i].x + padding;
360 alignContent(&newcell, xmax, rows[i].content_width, align);
361 newcell.content_index = rows[i].content_index;
362 newcell.color_defined = !rows[i].colors.empty();
363 if (newcell.color_defined)
364 newcell.color = rows[i].colors.back().first;
365 rows[i].cells.push_back(newcell);
366 rows[i].x = newcell.xmax;
369 else if (columntype == COLUMN_TYPE_IMAGE) {
370 // Find right edge of column
372 for (s32 i = 0; i < rowcount; ++i) {
373 TempRow *row = &rows[i];
374 row->content_index = -1;
376 // Find content_index. Image indices are defined in
377 // column options so check active_image_indices.
378 s32 image_index = stoi(content[i * colcount + j]);
379 std::map<s32, s32>::iterator image_iter =
380 active_image_indices.find(image_index);
381 if (image_iter != active_image_indices.end())
382 row->content_index = image_iter->second;
384 // Get texture object (might be NULL)
385 video::ITexture *image = NULL;
386 if (row->content_index >= 0)
387 image = m_images[row->content_index];
389 // Get content width and update xmax
390 row->content_width = image ? image->getOriginalSize().Width : 0;
391 row->content_width = MYMAX(row->content_width, width);
392 s32 row_xmax = row->x + padding + row->content_width;
393 xmax = MYMAX(xmax, row_xmax);
395 // Add a new cell (of image type) to each row
396 for (s32 i = 0; i < rowcount; ++i) {
397 newcell.xmin = rows[i].x + padding;
398 alignContent(&newcell, xmax, rows[i].content_width, align);
399 newcell.content_index = rows[i].content_index;
400 rows[i].cells.push_back(newcell);
401 rows[i].x = newcell.xmax;
403 active_image_indices.clear();
405 else if (columntype == COLUMN_TYPE_COLOR) {
406 for (s32 i = 0; i < rowcount; ++i) {
407 video::SColor cellcolor(255, 255, 255, 255);
408 if (parseColorString(content[i * colcount + j], cellcolor, true))
409 rows[i].colors.emplace_back(cellcolor, j+span);
412 else if (columntype == COLUMN_TYPE_INDENT ||
413 columntype == COLUMN_TYPE_TREE) {
414 // For column type "tree", reserve additional space for +/-
415 // Also enable special processing for treeview-type tables
416 s32 content_width = 0;
417 if (columntype == COLUMN_TYPE_TREE) {
418 content_width = m_font ? m_font->getDimension(L"+").Width : 0;
419 m_has_tree_column = true;
421 // Add a new cell (of indent or tree type) to each row
422 for (s32 i = 0; i < rowcount; ++i) {
423 TempRow *row = &rows[i];
425 s32 indentlevel = stoi(content[i * colcount + j]);
426 indentlevel = MYMAX(indentlevel, 0);
427 if (columntype == COLUMN_TYPE_TREE)
428 row->indent = indentlevel;
430 newcell.xmin = row->x + padding;
431 newcell.xpos = newcell.xmin + indentlevel * width;
432 newcell.xmax = newcell.xpos + content_width;
433 newcell.content_index = 0;
434 newcell.color_defined = !rows[i].colors.empty();
435 if (newcell.color_defined)
436 newcell.color = rows[i].colors.back().first;
437 row->cells.push_back(newcell);
438 row->x = newcell.xmax;
443 // Copy temporary rows to not so temporary rows
445 m_rows.resize(rowcount);
446 for (s32 i = 0; i < rowcount; ++i) {
447 Row *row = &m_rows[i];
448 row->cellcount = rows[i].cells.size();
449 row->cells = new Cell[row->cellcount];
450 memcpy((void*) row->cells, (void*) &rows[i].cells[0],
451 row->cellcount * sizeof(Cell));
452 row->indent = rows[i].indent;
453 row->visible_index = i;
454 m_visible_rows.push_back(i);
458 if (m_has_tree_column) {
459 // Treeview: convert tree to indent cells on leaf rows
460 for (s32 i = 0; i < rowcount; ++i) {
461 if (i == rowcount-1 || m_rows[i].indent >= m_rows[i+1].indent)
462 for (s32 j = 0; j < m_rows[i].cellcount; ++j)
463 if (m_rows[i].cells[j].content_type == COLUMN_TYPE_TREE)
464 m_rows[i].cells[j].content_type = COLUMN_TYPE_INDENT;
467 // Treeview: close rows according to opendepth option
468 std::set<s32> opened_trees;
469 for (s32 i = 0; i < rowcount; ++i)
470 if (m_rows[i].indent < opendepth)
471 opened_trees.insert(i);
472 setOpenedTrees(opened_trees);
475 // Delete temporary information used only during setTable()
477 allocationComplete();
479 // Clamp scroll bar position
483 void GUITable::clear()
485 // Clean up cells and rows
486 for (GUITable::Row &row : m_rows)
489 m_visible_rows.clear();
491 // Get colors from skin
492 gui::IGUISkin *skin = Environment->getSkin();
493 m_color = skin->getColor(gui::EGDC_BUTTON_TEXT);
494 m_background = skin->getColor(gui::EGDC_3D_HIGH_LIGHT);
495 m_highlight = skin->getColor(gui::EGDC_HIGH_LIGHT);
496 m_highlight_text = skin->getColor(gui::EGDC_HIGH_LIGHT_TEXT);
499 m_is_textlist = false;
500 m_has_tree_column = false;
503 m_sel_doubleclick = false;
505 m_keynav_buffer = L"";
509 m_alloc_strings.clear();
510 m_alloc_images.clear();
513 std::string GUITable::checkEvent()
515 s32 sel = getSelected();
522 std::ostringstream os(std::ios::binary);
523 if (m_sel_doubleclick) {
525 m_sel_doubleclick = false;
531 if (!m_is_textlist) {
532 os<<":"<<m_sel_column;
537 s32 GUITable::getSelected() const
542 assert(m_selected >= 0 && m_selected < (s32) m_visible_rows.size());
543 return m_visible_rows[m_selected] + 1;
546 void GUITable::setSelected(s32 index)
548 s32 old_selected = m_selected;
552 m_sel_doubleclick = false;
554 --index; // Switch from 1-based indexing to 0-based indexing
556 s32 rowcount = m_rows.size();
557 if (rowcount == 0 || index < 0) {
561 if (index >= rowcount) {
562 index = rowcount - 1;
565 // If the selected row is not visible, open its ancestors to make it visible
566 bool selection_invisible = m_rows[index].visible_index < 0;
567 if (selection_invisible) {
568 std::set<s32> opened_trees;
569 getOpenedTrees(opened_trees);
570 s32 indent = m_rows[index].indent;
571 for (s32 j = index - 1; j >= 0; --j) {
572 if (m_rows[j].indent < indent) {
573 opened_trees.insert(j);
574 indent = m_rows[j].indent;
577 setOpenedTrees(opened_trees);
581 m_selected = m_rows[index].visible_index;
582 assert(m_selected >= 0 && m_selected < (s32) m_visible_rows.size());
585 if (m_selected != old_selected || selection_invisible) {
590 void GUITable::setOverrideFont(IGUIFont *font)
596 font = Environment->getSkin()->getFont();
604 m_rowheight = m_font->getDimension(L"Ay").Height + 4;
605 m_rowheight = MYMAX(m_rowheight, 1);
610 IGUIFont *GUITable::getOverrideFont() const
615 GUITable::DynamicData GUITable::getDynamicData() const
618 dyndata.selected = getSelected();
619 dyndata.scrollpos = m_scrollbar->getPos();
620 dyndata.keynav_time = m_keynav_time;
621 dyndata.keynav_buffer = m_keynav_buffer;
622 if (m_has_tree_column)
623 getOpenedTrees(dyndata.opened_trees);
627 void GUITable::setDynamicData(const DynamicData &dyndata)
629 if (m_has_tree_column)
630 setOpenedTrees(dyndata.opened_trees);
632 m_keynav_time = dyndata.keynav_time;
633 m_keynav_buffer = dyndata.keynav_buffer;
635 setSelected(dyndata.selected);
637 m_sel_doubleclick = false;
639 m_scrollbar->setPos(dyndata.scrollpos);
642 const c8* GUITable::getTypeName() const
647 void GUITable::updateAbsolutePosition()
649 IGUIElement::updateAbsolutePosition();
653 void GUITable::draw()
658 gui::IGUISkin *skin = Environment->getSkin();
662 bool draw_background = m_background.getAlpha() > 0;
664 skin->draw3DSunkenPane(this, m_background,
665 true, draw_background,
666 AbsoluteRect, &AbsoluteClippingRect);
667 else if (draw_background)
668 skin->draw2DRectangle(this, m_background,
669 AbsoluteRect, &AbsoluteClippingRect);
673 core::rect<s32> client_clip(AbsoluteRect);
674 client_clip.UpperLeftCorner.Y += 1;
675 client_clip.UpperLeftCorner.X += 1;
676 client_clip.LowerRightCorner.Y -= 1;
677 client_clip.LowerRightCorner.X -= 1;
678 if (m_scrollbar->isVisible()) {
679 client_clip.LowerRightCorner.X =
680 m_scrollbar->getAbsolutePosition().UpperLeftCorner.X;
682 client_clip.clipAgainst(AbsoluteClippingRect);
686 s32 scrollpos = m_scrollbar->getPos();
687 s32 row_min = scrollpos / m_rowheight;
688 s32 row_max = (scrollpos + AbsoluteRect.getHeight() - 1)
690 row_max = MYMIN(row_max, (s32) m_visible_rows.size());
692 core::rect<s32> row_rect(AbsoluteRect);
693 if (m_scrollbar->isVisible())
694 row_rect.LowerRightCorner.X -=
695 skin->getSize(gui::EGDS_SCROLLBAR_SIZE);
696 row_rect.UpperLeftCorner.Y += row_min * m_rowheight - scrollpos;
697 row_rect.LowerRightCorner.Y = row_rect.UpperLeftCorner.Y + m_rowheight;
699 for (s32 i = row_min; i < row_max; ++i) {
700 Row *row = &m_rows[m_visible_rows[i]];
701 bool is_sel = i == m_selected;
702 video::SColor color = m_color;
705 skin->draw2DRectangle(this, m_highlight, row_rect, &client_clip);
706 color = m_highlight_text;
709 for (s32 j = 0; j < row->cellcount; ++j)
710 drawCell(&row->cells[j], color, row_rect, client_clip);
712 row_rect.UpperLeftCorner.Y += m_rowheight;
713 row_rect.LowerRightCorner.Y += m_rowheight;
720 void GUITable::drawCell(const Cell *cell, video::SColor color,
721 const core::rect<s32> &row_rect,
722 const core::rect<s32> &client_clip)
724 if ((cell->content_type == COLUMN_TYPE_TEXT)
725 || (cell->content_type == COLUMN_TYPE_TREE)) {
727 core::rect<s32> text_rect = row_rect;
728 text_rect.UpperLeftCorner.X = row_rect.UpperLeftCorner.X
730 text_rect.LowerRightCorner.X = row_rect.UpperLeftCorner.X
733 if (cell->color_defined)
737 if (cell->content_type == COLUMN_TYPE_TEXT)
738 m_font->draw(m_strings[cell->content_index],
740 false, true, &client_clip);
742 m_font->draw(cell->content_index ? L"+" : L"-",
744 false, true, &client_clip);
747 else if (cell->content_type == COLUMN_TYPE_IMAGE) {
749 if (cell->content_index < 0)
752 video::IVideoDriver *driver = Environment->getVideoDriver();
753 video::ITexture *image = m_images[cell->content_index];
756 core::position2d<s32> dest_pos =
757 row_rect.UpperLeftCorner;
758 dest_pos.X += cell->xpos;
759 core::rect<s32> source_rect(
760 core::position2d<s32>(0, 0),
761 image->getOriginalSize());
762 s32 imgh = source_rect.LowerRightCorner.Y;
763 s32 rowh = row_rect.getHeight();
765 dest_pos.Y += (rowh - imgh) / 2;
767 source_rect.LowerRightCorner.Y = rowh;
769 video::SColor color(255, 255, 255, 255);
771 driver->draw2DImage(image, dest_pos, source_rect,
772 &client_clip, color, true);
777 bool GUITable::OnEvent(const SEvent &event)
780 return IGUIElement::OnEvent(event);
782 if (event.EventType == EET_KEY_INPUT_EVENT) {
783 if (event.KeyInput.PressedDown && (
784 event.KeyInput.Key == KEY_DOWN ||
785 event.KeyInput.Key == KEY_UP ||
786 event.KeyInput.Key == KEY_HOME ||
787 event.KeyInput.Key == KEY_END ||
788 event.KeyInput.Key == KEY_NEXT ||
789 event.KeyInput.Key == KEY_PRIOR)) {
791 switch (event.KeyInput.Key) {
799 offset = - (s32) m_visible_rows.size();
802 offset = m_visible_rows.size();
805 offset = AbsoluteRect.getHeight() / m_rowheight;
808 offset = - (s32) (AbsoluteRect.getHeight() / m_rowheight);
813 s32 old_selected = m_selected;
814 s32 rowcount = m_visible_rows.size();
816 m_selected = rangelim(m_selected + offset, 0, rowcount-1);
820 if (m_selected != old_selected)
821 sendTableEvent(0, false);
826 if (event.KeyInput.PressedDown && (
827 event.KeyInput.Key == KEY_LEFT ||
828 event.KeyInput.Key == KEY_RIGHT)) {
829 // Open/close subtree via keyboard
830 if (m_selected >= 0) {
831 int dir = event.KeyInput.Key == KEY_LEFT ? -1 : 1;
832 toggleVisibleTree(m_selected, dir, true);
836 else if (!event.KeyInput.PressedDown && (
837 event.KeyInput.Key == KEY_RETURN ||
838 event.KeyInput.Key == KEY_SPACE)) {
839 sendTableEvent(0, true);
842 else if (event.KeyInput.Key == KEY_ESCAPE ||
843 event.KeyInput.Key == KEY_SPACE) {
846 else if (event.KeyInput.PressedDown && event.KeyInput.Char) {
847 // change selection based on text as it is typed
848 u64 now = porting::getTimeMs();
849 if (now - m_keynav_time >= 500)
850 m_keynav_buffer = L"";
853 // add to key buffer if not a key repeat
854 if (!(m_keynav_buffer.size() == 1 &&
855 m_keynav_buffer[0] == event.KeyInput.Char)) {
856 m_keynav_buffer.append(event.KeyInput.Char);
859 // find the selected item, starting at the current selection
860 // don't change selection if the key buffer matches the current item
861 s32 old_selected = m_selected;
862 s32 start = MYMAX(m_selected, 0);
863 s32 rowcount = m_visible_rows.size();
864 for (s32 k = 1; k < rowcount; ++k) {
865 s32 current = start + k;
866 if (current >= rowcount)
868 if (doesRowStartWith(getRow(current), m_keynav_buffer)) {
869 m_selected = current;
874 if (m_selected != old_selected)
875 sendTableEvent(0, false);
880 if (event.EventType == EET_MOUSE_INPUT_EVENT) {
881 core::position2d<s32> p(event.MouseInput.X, event.MouseInput.Y);
883 if (event.MouseInput.Event == EMIE_MOUSE_WHEEL) {
884 m_scrollbar->setPos(m_scrollbar->getPos() +
885 (event.MouseInput.Wheel < 0 ? -3 : 3) *
886 - (s32) m_rowheight / 2);
890 // Find hovered row and cell
891 bool really_hovering = false;
892 s32 row_i = getRowAt(p.Y, really_hovering);
893 const Cell *cell = NULL;
894 if (really_hovering) {
895 s32 cell_j = getCellAt(p.X, row_i);
897 cell = &(getRow(row_i)->cells[cell_j]);
901 setToolTipText(cell ? m_strings[cell->tooltip_index].c_str() : L"");
903 // Fix for #1567/#1806:
904 // IGUIScrollBar passes double click events to its parent,
905 // which we don't want. Detect this case and discard the event
906 if (event.MouseInput.Event != EMIE_MOUSE_MOVED &&
907 m_scrollbar->isVisible() &&
908 m_scrollbar->isPointInside(p))
911 if (event.MouseInput.isLeftPressed() &&
913 event.MouseInput.Event == EMIE_MOUSE_MOVED)) {
915 bool sel_doubleclick = (event.MouseInput.Event
916 == EMIE_LMOUSE_DOUBLE_CLICK);
917 bool plusminus_clicked = false;
919 // For certain events (left click), report column
920 // Also open/close subtrees when the +/- is clicked
922 event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN ||
923 event.MouseInput.Event == EMIE_LMOUSE_DOUBLE_CLICK ||
924 event.MouseInput.Event == EMIE_LMOUSE_TRIPLE_CLICK)) {
925 sel_column = cell->reported_column;
926 if (cell->content_type == COLUMN_TYPE_TREE)
927 plusminus_clicked = true;
930 if (plusminus_clicked) {
931 if (event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN) {
932 toggleVisibleTree(row_i, 0, false);
937 s32 old_selected = m_selected;
941 if (m_selected != old_selected ||
944 sendTableEvent(sel_column, sel_doubleclick);
947 // Treeview: double click opens/closes trees
948 if (m_has_tree_column && sel_doubleclick) {
949 toggleVisibleTree(m_selected, 0, false);
955 if (event.EventType == EET_GUI_EVENT &&
956 event.GUIEvent.EventType == gui::EGET_SCROLL_BAR_CHANGED &&
957 event.GUIEvent.Caller == m_scrollbar) {
958 // Don't pass events from our scrollbar to the parent
962 return IGUIElement::OnEvent(event);
965 /******************************************************************************/
966 /* GUITable helper functions */
967 /******************************************************************************/
969 s32 GUITable::allocString(const std::string &text)
971 std::map<std::string, s32>::iterator it = m_alloc_strings.find(text);
972 if (it == m_alloc_strings.end()) {
973 s32 id = m_strings.size();
974 std::wstring wtext = utf8_to_wide(text);
975 m_strings.emplace_back(wtext.c_str());
976 m_alloc_strings.insert(std::make_pair(text, id));
983 s32 GUITable::allocImage(const std::string &imagename)
985 std::map<std::string, s32>::iterator it = m_alloc_images.find(imagename);
986 if (it == m_alloc_images.end()) {
987 s32 id = m_images.size();
988 m_images.push_back(m_tsrc->getTexture(imagename));
989 m_alloc_images.insert(std::make_pair(imagename, id));
996 void GUITable::allocationComplete()
998 // Called when done with creating rows and cells from table data,
999 // i.e. when allocString and allocImage won't be called anymore
1000 m_alloc_strings.clear();
1001 m_alloc_images.clear();
1004 const GUITable::Row* GUITable::getRow(s32 i) const
1006 if (i >= 0 && i < (s32) m_visible_rows.size())
1007 return &m_rows[m_visible_rows[i]];
1012 bool GUITable::doesRowStartWith(const Row *row, const core::stringw &str) const
1017 for (s32 j = 0; j < row->cellcount; ++j) {
1018 Cell *cell = &row->cells[j];
1019 if (cell->content_type == COLUMN_TYPE_TEXT) {
1020 const core::stringw &cellstr = m_strings[cell->content_index];
1021 if (cellstr.size() >= str.size() &&
1022 str.equals_ignore_case(cellstr.subString(0, str.size())))
1029 s32 GUITable::getRowAt(s32 y, bool &really_hovering) const
1031 really_hovering = false;
1033 s32 rowcount = m_visible_rows.size();
1037 // Use arithmetic to find row
1038 s32 rel_y = y - AbsoluteRect.UpperLeftCorner.Y - 1;
1039 s32 i = (rel_y + m_scrollbar->getPos()) / m_rowheight;
1041 if (i >= 0 && i < rowcount) {
1042 really_hovering = true;
1048 return rowcount - 1;
1051 s32 GUITable::getCellAt(s32 x, s32 row_i) const
1053 const Row *row = getRow(row_i);
1057 // Use binary search to find cell in row
1058 s32 rel_x = x - AbsoluteRect.UpperLeftCorner.X - 1;
1060 s32 jmax = row->cellcount - 1;
1061 while (jmin < jmax) {
1062 s32 pivot = jmin + (jmax - jmin) / 2;
1063 assert(pivot >= 0 && pivot < row->cellcount);
1064 const Cell *cell = &row->cells[pivot];
1066 if (rel_x >= cell->xmin && rel_x <= cell->xmax)
1069 if (rel_x < cell->xmin)
1075 if (jmin >= 0 && jmin < row->cellcount &&
1076 rel_x >= row->cells[jmin].xmin &&
1077 rel_x <= row->cells[jmin].xmax)
1083 void GUITable::autoScroll()
1085 if (m_selected >= 0) {
1086 s32 pos = m_scrollbar->getPos();
1087 s32 maxpos = m_selected * m_rowheight;
1088 s32 minpos = maxpos - (AbsoluteRect.getHeight() - m_rowheight);
1090 m_scrollbar->setPos(maxpos);
1091 else if (pos < minpos)
1092 m_scrollbar->setPos(minpos);
1096 void GUITable::updateScrollBar()
1098 s32 totalheight = m_rowheight * m_visible_rows.size();
1099 s32 scrollmax = MYMAX(0, totalheight - AbsoluteRect.getHeight());
1100 m_scrollbar->setVisible(scrollmax > 0);
1101 m_scrollbar->setMax(scrollmax);
1102 m_scrollbar->setSmallStep(m_rowheight);
1103 m_scrollbar->setLargeStep(2 * m_rowheight);
1104 m_scrollbar->setPageSize(totalheight);
1107 void GUITable::sendTableEvent(s32 column, bool doubleclick)
1109 m_sel_column = column;
1110 m_sel_doubleclick = doubleclick;
1113 memset(&e, 0, sizeof e);
1114 e.EventType = EET_GUI_EVENT;
1115 e.GUIEvent.Caller = this;
1116 e.GUIEvent.Element = 0;
1117 e.GUIEvent.EventType = gui::EGET_TABLE_CHANGED;
1122 void GUITable::getOpenedTrees(std::set<s32> &opened_trees) const
1124 opened_trees.clear();
1125 s32 rowcount = m_rows.size();
1126 for (s32 i = 0; i < rowcount - 1; ++i) {
1127 if (m_rows[i].indent < m_rows[i+1].indent &&
1128 m_rows[i+1].visible_index != -2)
1129 opened_trees.insert(i);
1133 void GUITable::setOpenedTrees(const std::set<s32> &opened_trees)
1135 s32 old_selected = -1;
1136 if (m_selected >= 0)
1137 old_selected = m_visible_rows[m_selected];
1139 std::vector<s32> parents;
1140 std::vector<s32> closed_parents;
1142 m_visible_rows.clear();
1144 for (size_t i = 0; i < m_rows.size(); ++i) {
1145 Row *row = &m_rows[i];
1147 // Update list of ancestors
1148 while (!parents.empty() && m_rows[parents.back()].indent >= row->indent)
1150 while (!closed_parents.empty() &&
1151 m_rows[closed_parents.back()].indent >= row->indent)
1152 closed_parents.pop_back();
1154 assert(closed_parents.size() <= parents.size());
1156 if (closed_parents.empty()) {
1158 row->visible_index = m_visible_rows.size();
1159 m_visible_rows.push_back(i);
1161 else if (parents.back() == closed_parents.back()) {
1162 // Invisible row, direct parent is closed
1163 row->visible_index = -2;
1166 // Invisible row, direct parent is open, some ancestor is closed
1167 row->visible_index = -1;
1170 // If not a leaf, add to parents list
1171 if (i < m_rows.size()-1 && row->indent < m_rows[i+1].indent) {
1172 parents.push_back(i);
1174 s32 content_index = 0; // "-", open
1175 if (opened_trees.count(i) == 0) {
1176 closed_parents.push_back(i);
1177 content_index = 1; // "+", closed
1180 // Update all cells of type "tree"
1181 for (s32 j = 0; j < row->cellcount; ++j)
1182 if (row->cells[j].content_type == COLUMN_TYPE_TREE)
1183 row->cells[j].content_index = content_index;
1189 // m_selected must be updated since it is a visible row index
1190 if (old_selected >= 0)
1191 m_selected = m_rows[old_selected].visible_index;
1194 void GUITable::openTree(s32 to_open)
1196 std::set<s32> opened_trees;
1197 getOpenedTrees(opened_trees);
1198 opened_trees.insert(to_open);
1199 setOpenedTrees(opened_trees);
1202 void GUITable::closeTree(s32 to_close)
1204 std::set<s32> opened_trees;
1205 getOpenedTrees(opened_trees);
1206 opened_trees.erase(to_close);
1207 setOpenedTrees(opened_trees);
1210 // The following function takes a visible row index (hidden rows skipped)
1211 // dir: -1 = left (close), 0 = auto (toggle), 1 = right (open)
1212 void GUITable::toggleVisibleTree(s32 row_i, int dir, bool move_selection)
1214 // Check if the chosen tree is currently open
1215 const Row *row = getRow(row_i);
1219 bool was_open = false;
1220 for (s32 j = 0; j < row->cellcount; ++j) {
1221 if (row->cells[j].content_type == COLUMN_TYPE_TREE) {
1222 was_open = row->cells[j].content_index == 0;
1227 // Check if the chosen tree should be opened
1228 bool do_open = !was_open;
1234 // Close or open the tree; the heavy lifting is done by setOpenedTrees
1235 if (was_open && !do_open)
1236 closeTree(m_visible_rows[row_i]);
1237 else if (!was_open && do_open)
1238 openTree(m_visible_rows[row_i]);
1240 // Change selected row if requested by caller,
1241 // this is useful for keyboard navigation
1242 if (move_selection) {
1244 if (was_open && do_open) {
1245 // Move selection to first child
1246 const Row *maybe_child = getRow(sel + 1);
1247 if (maybe_child && maybe_child->indent > row->indent)
1250 else if (!was_open && !do_open) {
1251 // Move selection to parent
1252 assert(getRow(sel) != NULL);
1253 while (sel > 0 && getRow(sel - 1)->indent >= row->indent)
1256 if (sel < 0) // was root already selected?
1259 if (sel != m_selected) {
1262 sendTableEvent(0, false);
1267 void GUITable::alignContent(Cell *cell, s32 xmax, s32 content_width, s32 align)
1269 // requires that cell.xmin, cell.xmax are properly set
1270 // align = 0: left aligned, 1: centered, 2: right aligned, 3: inline
1272 cell->xpos = cell->xmin;
1275 else if (align == 1) {
1276 cell->xpos = (cell->xmin + xmax - content_width) / 2;
1279 else if (align == 2) {
1280 cell->xpos = xmax - content_width;
1284 // inline alignment: the cells of the column don't have an aligned
1285 // right border, the right border of each cell depends on the content
1286 cell->xpos = cell->xmin;
1287 cell->xmax = cell->xmin + content_width;