guiTable.cpp 34 KB

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