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 <IGUIScrollBar.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 "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),
52 m_has_tree_column(false),
55 m_sel_doubleclick(false),
59 m_color(255, 255, 255, 255),
60 m_background(255, 0, 0, 0),
61 m_highlight(255, 70, 100, 50),
62 m_highlight_text(255, 255, 255, 255),
69 gui::IGUISkin* skin = Environment->getSkin();
71 m_font = skin->getFont();
74 m_rowheight = m_font->getDimension(L"A").Height + 4;
75 m_rowheight = MYMAX(m_rowheight, 1);
78 const s32 s = skin->getSize(gui::EGDS_SCROLLBAR_SIZE);
79 m_scrollbar = Environment->addScrollBar(false,
80 core::rect<s32>(RelativeRect.getWidth() - s,
82 RelativeRect.getWidth(),
83 RelativeRect.getHeight()),
85 m_scrollbar->setSubElement(true);
86 m_scrollbar->setTabStop(false);
87 m_scrollbar->setAlignment(gui::EGUIA_LOWERRIGHT, gui::EGUIA_LOWERRIGHT,
88 gui::EGUIA_UPPERLEFT, gui::EGUIA_LOWERRIGHT);
89 m_scrollbar->setVisible(false);
90 m_scrollbar->setPos(0);
94 updateAbsolutePosition();
96 core::rect<s32> relative_rect = m_scrollbar->getRelativePosition();
97 s32 width = (relative_rect.getWidth()/(2.0/3.0)) * porting::getDisplayDensity() *
98 g_settings->getFloat("gui_scaling");
99 m_scrollbar->setRelativePosition(core::rect<s32>(
100 relative_rect.LowerRightCorner.X-width,relative_rect.UpperLeftCorner.Y,
101 relative_rect.LowerRightCorner.X,relative_rect.LowerRightCorner.Y
105 GUITable::~GUITable()
107 for (size_t i = 0; i < m_rows.size(); ++i)
108 delete[] m_rows[i].cells;
113 m_scrollbar->remove();
116 GUITable::Option GUITable::splitOption(const std::string &str)
118 size_t equal_pos = str.find('=');
119 if (equal_pos == std::string::npos)
120 return GUITable::Option(str, "");
122 return GUITable::Option(str.substr(0, equal_pos),
123 str.substr(equal_pos + 1));
126 void GUITable::setTextList(const std::vector<std::string> &content,
132 m_background.setAlpha(0);
136 m_is_textlist = true;
138 s32 empty_string_index = allocString("");
140 m_rows.resize(content.size());
141 for (s32 i = 0; i < (s32) content.size(); ++i) {
142 Row *row = &m_rows[i];
143 row->cells = new Cell[1];
146 row->visible_index = i;
147 m_visible_rows.push_back(i);
149 Cell *cell = row->cells;
151 cell->xmax = 0x7fff; // something large enough
153 cell->content_type = COLUMN_TYPE_TEXT;
154 cell->content_index = empty_string_index;
155 cell->tooltip_index = empty_string_index;
156 cell->color.set(255, 255, 255, 255);
157 cell->color_defined = false;
158 cell->reported_column = 1;
160 // parse row content (color)
161 const std::string &s = content[i];
162 if (s[0] == '#' && s[1] == '#') {
163 // double # to escape
164 cell->content_index = allocString(s.substr(2));
166 else if (s[0] == '#' && s.size() >= 7 &&
168 s.substr(0,7), cell->color, false)) {
169 // single # for color
170 cell->color_defined = true;
171 cell->content_index = allocString(s.substr(7));
175 cell->content_index = allocString(s);
180 allocationComplete();
182 // Clamp scroll bar position
186 void GUITable::setTable(const TableOptions &options,
187 const TableColumns &columns,
188 std::vector<std::string> &content)
192 // Naming conventions:
193 // i is always a row index, 0-based
194 // j is always a column index, 0-based
195 // k is another index, for example an option index
197 // Handle a stupid error case... (issue #1187)
198 if (columns.empty()) {
199 TableColumn text_column;
200 text_column.type = "text";
201 TableColumns new_columns;
202 new_columns.push_back(text_column);
203 setTable(options, new_columns, content);
207 // Handle table options
208 video::SColor default_color(255, 255, 255, 255);
210 for (size_t k = 0; k < options.size(); ++k) {
211 const std::string &name = options[k].name;
212 const std::string &value = options[k].value;
214 parseColorString(value, m_color, false);
215 else if (name == "background")
216 parseColorString(value, m_background, false);
217 else if (name == "border")
218 m_border = is_yes(value);
219 else if (name == "highlight")
220 parseColorString(value, m_highlight, false);
221 else if (name == "highlight_text")
222 parseColorString(value, m_highlight_text, false);
223 else if (name == "opendepth")
224 opendepth = stoi(value);
226 errorstream<<"Invalid table option: \""<<name<<"\""
227 <<" (value=\""<<value<<"\")"<<std::endl;
230 // Get number of columns and rows
231 // note: error case columns.size() == 0 was handled above
232 s32 colcount = columns.size();
233 assert(colcount >= 1);
234 // rowcount = ceil(cellcount / colcount) but use integer arithmetic
235 s32 rowcount = (content.size() + colcount - 1) / colcount;
236 assert(rowcount >= 0);
237 // Append empty strings to content if there is an incomplete row
238 s32 cellcount = rowcount * colcount;
239 while (content.size() < (u32) cellcount)
240 content.push_back("");
242 // Create temporary rows (for processing columns)
244 // Current horizontal position (may different between rows due
245 // to indent/tree columns, or text/image columns with width<0)
247 // Tree indentation level
249 // Next cell: Index into m_strings or m_images
251 // Next cell: Width in pixels
253 // Vector of completed cells in this row
254 std::vector<Cell> cells;
255 // Stores colors and how long they last (maximum column index)
256 std::vector<std::pair<video::SColor, s32> > colors;
258 TempRow(): x(0), indent(0), content_index(0), content_width(0) {}
260 TempRow *rows = new TempRow[rowcount];
262 // Get em width. Pedantically speaking, the width of "M" is not
263 // necessarily the same as the em width, but whatever, close enough.
266 em = m_font->getDimension(L"M").Width;
268 s32 default_tooltip_index = allocString("");
270 std::map<s32, s32> active_image_indices;
272 // Process content in column-major order
273 for (s32 j = 0; j < colcount; ++j) {
275 ColumnType columntype = COLUMN_TYPE_TEXT;
276 if (columns[j].type == "text")
277 columntype = COLUMN_TYPE_TEXT;
278 else if (columns[j].type == "image")
279 columntype = COLUMN_TYPE_IMAGE;
280 else if (columns[j].type == "color")
281 columntype = COLUMN_TYPE_COLOR;
282 else if (columns[j].type == "indent")
283 columntype = COLUMN_TYPE_INDENT;
284 else if (columns[j].type == "tree")
285 columntype = COLUMN_TYPE_TREE;
287 errorstream<<"Invalid table column type: \""
288 <<columns[j].type<<"\""<<std::endl;
290 // Process column options
291 s32 padding = myround(0.5 * em);
292 s32 tooltip_index = default_tooltip_index;
297 if (columntype == COLUMN_TYPE_INDENT) {
298 padding = 0; // default indent padding
300 if (columntype == COLUMN_TYPE_INDENT ||
301 columntype == COLUMN_TYPE_TREE) {
302 width = myround(em * 1.5); // default indent width
305 for (size_t k = 0; k < columns[j].options.size(); ++k) {
306 const std::string &name = columns[j].options[k].name;
307 const std::string &value = columns[j].options[k].value;
308 if (name == "padding")
309 padding = myround(stof(value) * em);
310 else if (name == "tooltip")
311 tooltip_index = allocString(value);
312 else if (name == "align" && value == "left")
314 else if (name == "align" && value == "center")
316 else if (name == "align" && value == "right")
318 else if (name == "align" && value == "inline")
320 else if (name == "width")
321 width = myround(stof(value) * em);
322 else if (name == "span" && columntype == COLUMN_TYPE_COLOR)
324 else if (columntype == COLUMN_TYPE_IMAGE &&
326 string_allowed(name, "0123456789")) {
327 s32 content_index = allocImage(value);
328 active_image_indices.insert(std::make_pair(
333 errorstream<<"Invalid table column option: \""<<name<<"\""
334 <<" (value=\""<<value<<"\")"<<std::endl;
338 // If current column type can use information from "color" columns,
339 // find out which of those is currently active
340 if (columntype == COLUMN_TYPE_TEXT) {
341 for (s32 i = 0; i < rowcount; ++i) {
342 TempRow *row = &rows[i];
343 while (!row->colors.empty() && row->colors.back().second < j)
344 row->colors.pop_back();
348 // Make template for new cells
350 memset(&newcell, 0, sizeof newcell);
351 newcell.content_type = columntype;
352 newcell.tooltip_index = tooltip_index;
353 newcell.reported_column = j+1;
355 if (columntype == COLUMN_TYPE_TEXT) {
356 // Find right edge of column
358 for (s32 i = 0; i < rowcount; ++i) {
359 TempRow *row = &rows[i];
360 row->content_index = allocString(content[i * colcount + j]);
361 const core::stringw &text = m_strings[row->content_index];
362 row->content_width = m_font ?
363 m_font->getDimension(text.c_str()).Width : 0;
364 row->content_width = MYMAX(row->content_width, width);
365 s32 row_xmax = row->x + padding + row->content_width;
366 xmax = MYMAX(xmax, row_xmax);
368 // Add a new cell (of text type) to each row
369 for (s32 i = 0; i < rowcount; ++i) {
370 newcell.xmin = rows[i].x + padding;
371 alignContent(&newcell, xmax, rows[i].content_width, align);
372 newcell.content_index = rows[i].content_index;
373 newcell.color_defined = !rows[i].colors.empty();
374 if (newcell.color_defined)
375 newcell.color = rows[i].colors.back().first;
376 rows[i].cells.push_back(newcell);
377 rows[i].x = newcell.xmax;
380 else if (columntype == COLUMN_TYPE_IMAGE) {
381 // Find right edge of column
383 for (s32 i = 0; i < rowcount; ++i) {
384 TempRow *row = &rows[i];
385 row->content_index = -1;
387 // Find content_index. Image indices are defined in
388 // column options so check active_image_indices.
389 s32 image_index = stoi(content[i * colcount + j]);
390 std::map<s32, s32>::iterator image_iter =
391 active_image_indices.find(image_index);
392 if (image_iter != active_image_indices.end())
393 row->content_index = image_iter->second;
395 // Get texture object (might be NULL)
396 video::ITexture *image = NULL;
397 if (row->content_index >= 0)
398 image = m_images[row->content_index];
400 // Get content width and update xmax
401 row->content_width = image ? image->getOriginalSize().Width : 0;
402 row->content_width = MYMAX(row->content_width, width);
403 s32 row_xmax = row->x + padding + row->content_width;
404 xmax = MYMAX(xmax, row_xmax);
406 // Add a new cell (of image type) to each row
407 for (s32 i = 0; i < rowcount; ++i) {
408 newcell.xmin = rows[i].x + padding;
409 alignContent(&newcell, xmax, rows[i].content_width, align);
410 newcell.content_index = rows[i].content_index;
411 rows[i].cells.push_back(newcell);
412 rows[i].x = newcell.xmax;
414 active_image_indices.clear();
416 else if (columntype == COLUMN_TYPE_COLOR) {
417 for (s32 i = 0; i < rowcount; ++i) {
418 video::SColor cellcolor(255, 255, 255, 255);
419 if (parseColorString(content[i * colcount + j], cellcolor, true))
420 rows[i].colors.push_back(std::make_pair(cellcolor, j+span));
423 else if (columntype == COLUMN_TYPE_INDENT ||
424 columntype == COLUMN_TYPE_TREE) {
425 // For column type "tree", reserve additional space for +/-
426 // Also enable special processing for treeview-type tables
427 s32 content_width = 0;
428 if (columntype == COLUMN_TYPE_TREE) {
429 content_width = m_font ? m_font->getDimension(L"+").Width : 0;
430 m_has_tree_column = true;
432 // Add a new cell (of indent or tree type) to each row
433 for (s32 i = 0; i < rowcount; ++i) {
434 TempRow *row = &rows[i];
436 s32 indentlevel = stoi(content[i * colcount + j]);
437 indentlevel = MYMAX(indentlevel, 0);
438 if (columntype == COLUMN_TYPE_TREE)
439 row->indent = indentlevel;
441 newcell.xmin = row->x + padding;
442 newcell.xpos = newcell.xmin + indentlevel * width;
443 newcell.xmax = newcell.xpos + content_width;
444 newcell.content_index = 0;
445 newcell.color_defined = !rows[i].colors.empty();
446 if (newcell.color_defined)
447 newcell.color = rows[i].colors.back().first;
448 row->cells.push_back(newcell);
449 row->x = newcell.xmax;
454 // Copy temporary rows to not so temporary rows
456 m_rows.resize(rowcount);
457 for (s32 i = 0; i < rowcount; ++i) {
458 Row *row = &m_rows[i];
459 row->cellcount = rows[i].cells.size();
460 row->cells = new Cell[row->cellcount];
461 memcpy((void*) row->cells, (void*) &rows[i].cells[0],
462 row->cellcount * sizeof(Cell));
463 row->indent = rows[i].indent;
464 row->visible_index = i;
465 m_visible_rows.push_back(i);
469 if (m_has_tree_column) {
470 // Treeview: convert tree to indent cells on leaf rows
471 for (s32 i = 0; i < rowcount; ++i) {
472 if (i == rowcount-1 || m_rows[i].indent >= m_rows[i+1].indent)
473 for (s32 j = 0; j < m_rows[i].cellcount; ++j)
474 if (m_rows[i].cells[j].content_type == COLUMN_TYPE_TREE)
475 m_rows[i].cells[j].content_type = COLUMN_TYPE_INDENT;
478 // Treeview: close rows according to opendepth option
479 std::set<s32> opened_trees;
480 for (s32 i = 0; i < rowcount; ++i)
481 if (m_rows[i].indent < opendepth)
482 opened_trees.insert(i);
483 setOpenedTrees(opened_trees);
486 // Delete temporary information used only during setTable()
488 allocationComplete();
490 // Clamp scroll bar position
494 void GUITable::clear()
496 // Clean up cells and rows
497 for (size_t i = 0; i < m_rows.size(); ++i)
498 delete[] m_rows[i].cells;
500 m_visible_rows.clear();
502 // Get colors from skin
503 gui::IGUISkin *skin = Environment->getSkin();
504 m_color = skin->getColor(gui::EGDC_BUTTON_TEXT);
505 m_background = skin->getColor(gui::EGDC_3D_HIGH_LIGHT);
506 m_highlight = skin->getColor(gui::EGDC_HIGH_LIGHT);
507 m_highlight_text = skin->getColor(gui::EGDC_HIGH_LIGHT_TEXT);
510 m_is_textlist = false;
511 m_has_tree_column = false;
514 m_sel_doubleclick = false;
516 m_keynav_buffer = L"";
520 m_alloc_strings.clear();
521 m_alloc_images.clear();
524 std::string GUITable::checkEvent()
526 s32 sel = getSelected();
533 std::ostringstream os(std::ios::binary);
534 if (m_sel_doubleclick) {
536 m_sel_doubleclick = false;
542 if (!m_is_textlist) {
543 os<<":"<<m_sel_column;
548 s32 GUITable::getSelected() const
553 assert(m_selected >= 0 && m_selected < (s32) m_visible_rows.size());
554 return m_visible_rows[m_selected] + 1;
557 void GUITable::setSelected(s32 index)
561 m_sel_doubleclick = false;
565 s32 rowcount = m_rows.size();
567 if (index >= rowcount)
568 index = rowcount - 1;
569 while (index >= 0 && m_rows[index].visible_index < 0)
572 m_selected = m_rows[index].visible_index;
573 assert(m_selected >= 0 && m_selected < (s32) m_visible_rows.size());
579 GUITable::DynamicData GUITable::getDynamicData() const
582 dyndata.selected = getSelected();
583 dyndata.scrollpos = m_scrollbar->getPos();
584 dyndata.keynav_time = m_keynav_time;
585 dyndata.keynav_buffer = m_keynav_buffer;
586 if (m_has_tree_column)
587 getOpenedTrees(dyndata.opened_trees);
591 void GUITable::setDynamicData(const DynamicData &dyndata)
593 if (m_has_tree_column)
594 setOpenedTrees(dyndata.opened_trees);
596 m_keynav_time = dyndata.keynav_time;
597 m_keynav_buffer = dyndata.keynav_buffer;
599 m_scrollbar->setPos(dyndata.scrollpos);
601 setSelected(dyndata.selected);
603 m_sel_doubleclick = false;
606 const c8* GUITable::getTypeName() const
611 void GUITable::updateAbsolutePosition()
613 IGUIElement::updateAbsolutePosition();
617 void GUITable::draw()
622 gui::IGUISkin *skin = Environment->getSkin();
626 bool draw_background = m_background.getAlpha() > 0;
628 skin->draw3DSunkenPane(this, m_background,
629 true, draw_background,
630 AbsoluteRect, &AbsoluteClippingRect);
631 else if (draw_background)
632 skin->draw2DRectangle(this, m_background,
633 AbsoluteRect, &AbsoluteClippingRect);
637 core::rect<s32> client_clip(AbsoluteRect);
638 client_clip.UpperLeftCorner.Y += 1;
639 client_clip.UpperLeftCorner.X += 1;
640 client_clip.LowerRightCorner.Y -= 1;
641 client_clip.LowerRightCorner.X -= 1;
642 if (m_scrollbar->isVisible()) {
643 client_clip.LowerRightCorner.X =
644 m_scrollbar->getAbsolutePosition().UpperLeftCorner.X;
646 client_clip.clipAgainst(AbsoluteClippingRect);
650 s32 scrollpos = m_scrollbar->getPos();
651 s32 row_min = scrollpos / m_rowheight;
652 s32 row_max = (scrollpos + AbsoluteRect.getHeight() - 1)
654 row_max = MYMIN(row_max, (s32) m_visible_rows.size());
656 core::rect<s32> row_rect(AbsoluteRect);
657 if (m_scrollbar->isVisible())
658 row_rect.LowerRightCorner.X -=
659 skin->getSize(gui::EGDS_SCROLLBAR_SIZE);
660 row_rect.UpperLeftCorner.Y += row_min * m_rowheight - scrollpos;
661 row_rect.LowerRightCorner.Y = row_rect.UpperLeftCorner.Y + m_rowheight;
663 for (s32 i = row_min; i < row_max; ++i) {
664 Row *row = &m_rows[m_visible_rows[i]];
665 bool is_sel = i == m_selected;
666 video::SColor color = m_color;
669 skin->draw2DRectangle(this, m_highlight, row_rect, &client_clip);
670 color = m_highlight_text;
673 for (s32 j = 0; j < row->cellcount; ++j)
674 drawCell(&row->cells[j], color, row_rect, client_clip);
676 row_rect.UpperLeftCorner.Y += m_rowheight;
677 row_rect.LowerRightCorner.Y += m_rowheight;
684 void GUITable::drawCell(const Cell *cell, video::SColor color,
685 const core::rect<s32> &row_rect,
686 const core::rect<s32> &client_clip)
688 if ((cell->content_type == COLUMN_TYPE_TEXT)
689 || (cell->content_type == COLUMN_TYPE_TREE)) {
691 core::rect<s32> text_rect = row_rect;
692 text_rect.UpperLeftCorner.X = row_rect.UpperLeftCorner.X
694 text_rect.LowerRightCorner.X = row_rect.UpperLeftCorner.X
697 if (cell->color_defined)
701 if (cell->content_type == COLUMN_TYPE_TEXT)
702 m_font->draw(m_strings[cell->content_index],
704 false, true, &client_clip);
706 m_font->draw(cell->content_index ? L"+" : L"-",
708 false, true, &client_clip);
711 else if (cell->content_type == COLUMN_TYPE_IMAGE) {
713 if (cell->content_index < 0)
716 video::IVideoDriver *driver = Environment->getVideoDriver();
717 video::ITexture *image = m_images[cell->content_index];
720 core::position2d<s32> dest_pos =
721 row_rect.UpperLeftCorner;
722 dest_pos.X += cell->xpos;
723 core::rect<s32> source_rect(
724 core::position2d<s32>(0, 0),
725 image->getOriginalSize());
726 s32 imgh = source_rect.LowerRightCorner.Y;
727 s32 rowh = row_rect.getHeight();
729 dest_pos.Y += (rowh - imgh) / 2;
731 source_rect.LowerRightCorner.Y = rowh;
733 video::SColor color(255, 255, 255, 255);
735 driver->draw2DImage(image, dest_pos, source_rect,
736 &client_clip, color, true);
741 bool GUITable::OnEvent(const SEvent &event)
744 return IGUIElement::OnEvent(event);
746 if (event.EventType == EET_KEY_INPUT_EVENT) {
747 if (event.KeyInput.PressedDown && (
748 event.KeyInput.Key == KEY_DOWN ||
749 event.KeyInput.Key == KEY_UP ||
750 event.KeyInput.Key == KEY_HOME ||
751 event.KeyInput.Key == KEY_END ||
752 event.KeyInput.Key == KEY_NEXT ||
753 event.KeyInput.Key == KEY_PRIOR)) {
755 switch (event.KeyInput.Key) {
763 offset = - (s32) m_visible_rows.size();
766 offset = m_visible_rows.size();
769 offset = AbsoluteRect.getHeight() / m_rowheight;
772 offset = - (s32) (AbsoluteRect.getHeight() / m_rowheight);
777 s32 old_selected = m_selected;
778 s32 rowcount = m_visible_rows.size();
780 m_selected = rangelim(m_selected + offset, 0, rowcount-1);
784 if (m_selected != old_selected)
785 sendTableEvent(0, false);
789 else if (event.KeyInput.PressedDown && (
790 event.KeyInput.Key == KEY_LEFT ||
791 event.KeyInput.Key == KEY_RIGHT)) {
792 // Open/close subtree via keyboard
793 if (m_selected >= 0) {
794 int dir = event.KeyInput.Key == KEY_LEFT ? -1 : 1;
795 toggleVisibleTree(m_selected, dir, true);
799 else if (!event.KeyInput.PressedDown && (
800 event.KeyInput.Key == KEY_RETURN ||
801 event.KeyInput.Key == KEY_SPACE)) {
802 sendTableEvent(0, true);
805 else if (event.KeyInput.Key == KEY_ESCAPE ||
806 event.KeyInput.Key == KEY_SPACE) {
809 else if (event.KeyInput.PressedDown && event.KeyInput.Char) {
810 // change selection based on text as it is typed
811 s32 now = getTimeMs();
812 if (now - m_keynav_time >= 500)
813 m_keynav_buffer = L"";
816 // add to key buffer if not a key repeat
817 if (!(m_keynav_buffer.size() == 1 &&
818 m_keynav_buffer[0] == event.KeyInput.Char)) {
819 m_keynav_buffer.append(event.KeyInput.Char);
822 // find the selected item, starting at the current selection
823 // don't change selection if the key buffer matches the current item
824 s32 old_selected = m_selected;
825 s32 start = MYMAX(m_selected, 0);
826 s32 rowcount = m_visible_rows.size();
827 for (s32 k = 1; k < rowcount; ++k) {
828 s32 current = start + k;
829 if (current >= rowcount)
831 if (doesRowStartWith(getRow(current), m_keynav_buffer)) {
832 m_selected = current;
837 if (m_selected != old_selected)
838 sendTableEvent(0, false);
843 if (event.EventType == EET_MOUSE_INPUT_EVENT) {
844 core::position2d<s32> p(event.MouseInput.X, event.MouseInput.Y);
846 if (event.MouseInput.Event == EMIE_MOUSE_WHEEL) {
847 m_scrollbar->setPos(m_scrollbar->getPos() +
848 (event.MouseInput.Wheel < 0 ? -3 : 3) *
849 - (s32) m_rowheight / 2);
853 // Find hovered row and cell
854 bool really_hovering = false;
855 s32 row_i = getRowAt(p.Y, really_hovering);
856 const Cell *cell = NULL;
857 if (really_hovering) {
858 s32 cell_j = getCellAt(p.X, row_i);
860 cell = &(getRow(row_i)->cells[cell_j]);
864 setToolTipText(cell ? m_strings[cell->tooltip_index].c_str() : L"");
866 // Fix for #1567/#1806:
867 // IGUIScrollBar passes double click events to its parent,
868 // which we don't want. Detect this case and discard the event
869 if (event.MouseInput.Event != EMIE_MOUSE_MOVED &&
870 m_scrollbar->isVisible() &&
871 m_scrollbar->isPointInside(p))
874 if (event.MouseInput.isLeftPressed() &&
876 event.MouseInput.Event == EMIE_MOUSE_MOVED)) {
878 bool sel_doubleclick = (event.MouseInput.Event
879 == EMIE_LMOUSE_DOUBLE_CLICK);
880 bool plusminus_clicked = false;
882 // For certain events (left click), report column
883 // Also open/close subtrees when the +/- is clicked
885 event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN ||
886 event.MouseInput.Event == EMIE_LMOUSE_DOUBLE_CLICK ||
887 event.MouseInput.Event == EMIE_LMOUSE_TRIPLE_CLICK)) {
888 sel_column = cell->reported_column;
889 if (cell->content_type == COLUMN_TYPE_TREE)
890 plusminus_clicked = true;
893 if (plusminus_clicked) {
894 if (event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN) {
895 toggleVisibleTree(row_i, 0, false);
900 s32 old_selected = m_selected;
904 if (m_selected != old_selected ||
907 sendTableEvent(sel_column, sel_doubleclick);
913 if (event.EventType == EET_GUI_EVENT &&
914 event.GUIEvent.EventType == gui::EGET_SCROLL_BAR_CHANGED &&
915 event.GUIEvent.Caller == m_scrollbar) {
916 // Don't pass events from our scrollbar to the parent
920 return IGUIElement::OnEvent(event);
923 /******************************************************************************/
924 /* GUITable helper functions */
925 /******************************************************************************/
927 s32 GUITable::allocString(const std::string &text)
929 std::map<std::string, s32>::iterator it = m_alloc_strings.find(text);
930 if (it == m_alloc_strings.end()) {
931 s32 id = m_strings.size();
932 std::wstring wtext = utf8_to_wide(text);
933 m_strings.push_back(core::stringw(wtext.c_str()));
934 m_alloc_strings.insert(std::make_pair(text, id));
942 s32 GUITable::allocImage(const std::string &imagename)
944 std::map<std::string, s32>::iterator it = m_alloc_images.find(imagename);
945 if (it == m_alloc_images.end()) {
946 s32 id = m_images.size();
947 m_images.push_back(m_tsrc->getTexture(imagename));
948 m_alloc_images.insert(std::make_pair(imagename, id));
956 void GUITable::allocationComplete()
958 // Called when done with creating rows and cells from table data,
959 // i.e. when allocString and allocImage won't be called anymore
960 m_alloc_strings.clear();
961 m_alloc_images.clear();
964 const GUITable::Row* GUITable::getRow(s32 i) const
966 if (i >= 0 && i < (s32) m_visible_rows.size())
967 return &m_rows[m_visible_rows[i]];
972 bool GUITable::doesRowStartWith(const Row *row, const core::stringw &str) const
977 for (s32 j = 0; j < row->cellcount; ++j) {
978 Cell *cell = &row->cells[j];
979 if (cell->content_type == COLUMN_TYPE_TEXT) {
980 const core::stringw &cellstr = m_strings[cell->content_index];
981 if (cellstr.size() >= str.size() &&
982 str.equals_ignore_case(cellstr.subString(0, str.size())))
989 s32 GUITable::getRowAt(s32 y, bool &really_hovering) const
991 really_hovering = false;
993 s32 rowcount = m_visible_rows.size();
997 // Use arithmetic to find row
998 s32 rel_y = y - AbsoluteRect.UpperLeftCorner.Y - 1;
999 s32 i = (rel_y + m_scrollbar->getPos()) / m_rowheight;
1001 if (i >= 0 && i < rowcount) {
1002 really_hovering = true;
1008 return rowcount - 1;
1012 s32 GUITable::getCellAt(s32 x, s32 row_i) const
1014 const Row *row = getRow(row_i);
1018 // Use binary search to find cell in row
1019 s32 rel_x = x - AbsoluteRect.UpperLeftCorner.X - 1;
1021 s32 jmax = row->cellcount - 1;
1022 while (jmin < jmax) {
1023 s32 pivot = jmin + (jmax - jmin) / 2;
1024 assert(pivot >= 0 && pivot < row->cellcount);
1025 const Cell *cell = &row->cells[pivot];
1027 if (rel_x >= cell->xmin && rel_x <= cell->xmax)
1029 else if (rel_x < cell->xmin)
1035 if (jmin >= 0 && jmin < row->cellcount &&
1036 rel_x >= row->cells[jmin].xmin &&
1037 rel_x <= row->cells[jmin].xmax)
1043 void GUITable::autoScroll()
1045 if (m_selected >= 0) {
1046 s32 pos = m_scrollbar->getPos();
1047 s32 maxpos = m_selected * m_rowheight;
1048 s32 minpos = maxpos - (AbsoluteRect.getHeight() - m_rowheight);
1050 m_scrollbar->setPos(maxpos);
1051 else if (pos < minpos)
1052 m_scrollbar->setPos(minpos);
1056 void GUITable::updateScrollBar()
1058 s32 totalheight = m_rowheight * m_visible_rows.size();
1059 s32 scrollmax = MYMAX(0, totalheight - AbsoluteRect.getHeight());
1060 m_scrollbar->setVisible(scrollmax > 0);
1061 m_scrollbar->setMax(scrollmax);
1062 m_scrollbar->setSmallStep(m_rowheight);
1063 m_scrollbar->setLargeStep(2 * m_rowheight);
1066 void GUITable::sendTableEvent(s32 column, bool doubleclick)
1068 m_sel_column = column;
1069 m_sel_doubleclick = doubleclick;
1072 memset(&e, 0, sizeof e);
1073 e.EventType = EET_GUI_EVENT;
1074 e.GUIEvent.Caller = this;
1075 e.GUIEvent.Element = 0;
1076 e.GUIEvent.EventType = gui::EGET_TABLE_CHANGED;
1081 void GUITable::getOpenedTrees(std::set<s32> &opened_trees) const
1083 opened_trees.clear();
1084 s32 rowcount = m_rows.size();
1085 for (s32 i = 0; i < rowcount - 1; ++i) {
1086 if (m_rows[i].indent < m_rows[i+1].indent &&
1087 m_rows[i+1].visible_index != -2)
1088 opened_trees.insert(i);
1092 void GUITable::setOpenedTrees(const std::set<s32> &opened_trees)
1094 s32 old_selected = getSelected();
1096 std::vector<s32> parents;
1097 std::vector<s32> closed_parents;
1099 m_visible_rows.clear();
1101 for (size_t i = 0; i < m_rows.size(); ++i) {
1102 Row *row = &m_rows[i];
1104 // Update list of ancestors
1105 while (!parents.empty() && m_rows[parents.back()].indent >= row->indent)
1107 while (!closed_parents.empty() &&
1108 m_rows[closed_parents.back()].indent >= row->indent)
1109 closed_parents.pop_back();
1111 assert(closed_parents.size() <= parents.size());
1113 if (closed_parents.empty()) {
1115 row->visible_index = m_visible_rows.size();
1116 m_visible_rows.push_back(i);
1118 else if (parents.back() == closed_parents.back()) {
1119 // Invisible row, direct parent is closed
1120 row->visible_index = -2;
1123 // Invisible row, direct parent is open, some ancestor is closed
1124 row->visible_index = -1;
1127 // If not a leaf, add to parents list
1128 if (i < m_rows.size()-1 && row->indent < m_rows[i+1].indent) {
1129 parents.push_back(i);
1131 s32 content_index = 0; // "-", open
1132 if (opened_trees.count(i) == 0) {
1133 closed_parents.push_back(i);
1134 content_index = 1; // "+", closed
1137 // Update all cells of type "tree"
1138 for (s32 j = 0; j < row->cellcount; ++j)
1139 if (row->cells[j].content_type == COLUMN_TYPE_TREE)
1140 row->cells[j].content_index = content_index;
1146 setSelected(old_selected);
1149 void GUITable::openTree(s32 to_open)
1151 std::set<s32> opened_trees;
1152 getOpenedTrees(opened_trees);
1153 opened_trees.insert(to_open);
1154 setOpenedTrees(opened_trees);
1157 void GUITable::closeTree(s32 to_close)
1159 std::set<s32> opened_trees;
1160 getOpenedTrees(opened_trees);
1161 opened_trees.erase(to_close);
1162 setOpenedTrees(opened_trees);
1165 // The following function takes a visible row index (hidden rows skipped)
1166 // dir: -1 = left (close), 0 = auto (toggle), 1 = right (open)
1167 void GUITable::toggleVisibleTree(s32 row_i, int dir, bool move_selection)
1169 // Check if the chosen tree is currently open
1170 const Row *row = getRow(row_i);
1174 bool was_open = false;
1175 for (s32 j = 0; j < row->cellcount; ++j) {
1176 if (row->cells[j].content_type == COLUMN_TYPE_TREE) {
1177 was_open = row->cells[j].content_index == 0;
1182 // Check if the chosen tree should be opened
1183 bool do_open = !was_open;
1189 // Close or open the tree; the heavy lifting is done by setOpenedTrees
1190 if (was_open && !do_open)
1191 closeTree(m_visible_rows[row_i]);
1192 else if (!was_open && do_open)
1193 openTree(m_visible_rows[row_i]);
1195 // Change selected row if requested by caller,
1196 // this is useful for keyboard navigation
1197 if (move_selection) {
1199 if (was_open && do_open) {
1200 // Move selection to first child
1201 const Row *maybe_child = getRow(sel + 1);
1202 if (maybe_child && maybe_child->indent > row->indent)
1205 else if (!was_open && !do_open) {
1206 // Move selection to parent
1207 assert(getRow(sel) != NULL);
1208 while (sel > 0 && getRow(sel - 1)->indent >= row->indent)
1211 if (sel < 0) // was root already selected?
1214 if (sel != m_selected) {
1217 sendTableEvent(0, false);
1222 void GUITable::alignContent(Cell *cell, s32 xmax, s32 content_width, s32 align)
1224 // requires that cell.xmin, cell.xmax are properly set
1225 // align = 0: left aligned, 1: centered, 2: right aligned, 3: inline
1227 cell->xpos = cell->xmin;
1230 else if (align == 1) {
1231 cell->xpos = (cell->xmin + xmax - content_width) / 2;
1234 else if (align == 2) {
1235 cell->xpos = xmax - content_width;
1239 // inline alignment: the cells of the column don't have an aligned
1240 // right border, the right border of each cell depends on the content
1241 cell->xpos = cell->xmin;
1242 cell->xmax = cell->xmin + content_width;