guiTable.cpp 34 KB

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