]> git.lizzy.rs Git - dragonfireclient.git/blob - src/gui/guiFormSpecMenu.cpp
StaticText/EnrichedString: Styling support (#9187)
[dragonfireclient.git] / src / gui / guiFormSpecMenu.cpp
1 /*
2 Minetest
3 Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
4
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.
9
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.
14
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.
18 */
19
20
21 #include <cstdlib>
22 #include <algorithm>
23 #include <iterator>
24 #include <limits>
25 #include <sstream>
26 #include "guiFormSpecMenu.h"
27 #include "guiScrollBar.h"
28 #include "guiTable.h"
29 #include "constants.h"
30 #include "gamedef.h"
31 #include "client/keycode.h"
32 #include "util/strfnd.h"
33 #include <IGUIButton.h>
34 #include <IGUICheckBox.h>
35 #include <IGUIComboBox.h>
36 #include <IGUIEditBox.h>
37 #include <IGUIStaticText.h>
38 #include <IGUIFont.h>
39 #include <IGUITabControl.h>
40 #include "client/renderingengine.h"
41 #include "log.h"
42 #include "client/tile.h" // ITextureSource
43 #include "client/hud.h" // drawItemStack
44 #include "filesys.h"
45 #include "gettime.h"
46 #include "gettext.h"
47 #include "scripting_server.h"
48 #include "mainmenumanager.h"
49 #include "porting.h"
50 #include "settings.h"
51 #include "client/client.h"
52 #include "client/fontengine.h"
53 #include "util/hex.h"
54 #include "util/numeric.h"
55 #include "util/string.h" // for parseColorString()
56 #include "irrlicht_changes/static_text.h"
57 #include "client/guiscalingfilter.h"
58 #include "guiBackgroundImage.h"
59 #include "guiBox.h"
60 #include "guiButton.h"
61 #include "guiButtonImage.h"
62 #include "guiButtonItemImage.h"
63 #include "guiEditBoxWithScrollbar.h"
64 #include "guiItemImage.h"
65 #include "guiScrollBar.h"
66 #include "guiTable.h"
67 #include "intlGUIEditBox.h"
68 #include "guiHyperText.h"
69
70 #define MY_CHECKPOS(a,b)                                                                                                        \
71         if (v_pos.size() != 2) {                                                                                                \
72                 errorstream<< "Invalid pos for element " << a << "specified: \""        \
73                         << parts[b] << "\"" << std::endl;                                                               \
74                         return;                                                                                                                 \
75         }
76
77 #define MY_CHECKGEOM(a,b)                                                                                                       \
78         if (v_geom.size() != 2) {                                                                                               \
79                 errorstream<< "Invalid geometry for element " << a <<                           \
80                         "specified: \"" << parts[b] << "\"" << std::endl;                               \
81                         return;                                                                                                                 \
82         }
83 /*
84         GUIFormSpecMenu
85 */
86 static unsigned int font_line_height(gui::IGUIFont *font)
87 {
88         return font->getDimension(L"Ay").Height + font->getKerningHeight();
89 }
90
91 inline u32 clamp_u8(s32 value)
92 {
93         return (u32) MYMIN(MYMAX(value, 0), 255);
94 }
95
96 GUIFormSpecMenu::GUIFormSpecMenu(JoystickController *joystick,
97                 gui::IGUIElement *parent, s32 id, IMenuManager *menumgr,
98                 Client *client, ISimpleTextureSource *tsrc, IFormSource *fsrc, TextDest *tdst,
99                 const std::string &formspecPrepend,
100                 bool remap_dbl_click):
101         GUIModalMenu(RenderingEngine::get_gui_env(), parent, id, menumgr),
102         m_invmgr(client),
103         m_tsrc(tsrc),
104         m_client(client),
105         m_formspec_prepend(formspecPrepend),
106         m_form_src(fsrc),
107         m_text_dst(tdst),
108         m_joystick(joystick),
109         m_remap_dbl_click(remap_dbl_click)
110 {
111         current_keys_pending.key_down = false;
112         current_keys_pending.key_up = false;
113         current_keys_pending.key_enter = false;
114         current_keys_pending.key_escape = false;
115
116         m_doubleclickdetect[0].time = 0;
117         m_doubleclickdetect[1].time = 0;
118
119         m_doubleclickdetect[0].pos = v2s32(0, 0);
120         m_doubleclickdetect[1].pos = v2s32(0, 0);
121
122         m_tooltip_show_delay = (u32)g_settings->getS32("tooltip_show_delay");
123         m_tooltip_append_itemname = g_settings->getBool("tooltip_append_itemname");
124 }
125
126 GUIFormSpecMenu::~GUIFormSpecMenu()
127 {
128         removeChildren();
129
130         for (auto &table_it : m_tables)
131                 table_it.second->drop();
132         for (auto &inventorylist_it : m_inventorylists)
133                 inventorylist_it.e->drop();
134         for (auto &checkbox_it : m_checkboxes)
135                 checkbox_it.second->drop();
136         for (auto &scrollbar_it : m_scrollbars)
137                 scrollbar_it.second->drop();
138         for (auto &background_it : m_backgrounds)
139                 background_it->drop();
140         for (auto &tooltip_rect_it : m_tooltip_rects)
141                 tooltip_rect_it.first->drop();
142
143         delete m_selected_item;
144         delete m_form_src;
145         delete m_text_dst;
146 }
147
148 void GUIFormSpecMenu::create(GUIFormSpecMenu *&cur_formspec, Client *client,
149         JoystickController *joystick, IFormSource *fs_src, TextDest *txt_dest,
150         const std::string &formspecPrepend)
151 {
152         if (cur_formspec == nullptr) {
153                 cur_formspec = new GUIFormSpecMenu(joystick, guiroot, -1, &g_menumgr,
154                         client, client->getTextureSource(), fs_src, txt_dest, formspecPrepend);
155                 cur_formspec->doPause = false;
156
157                 /*
158                         Caution: do not call (*cur_formspec)->drop() here --
159                         the reference might outlive the menu, so we will
160                         periodically check if *cur_formspec is the only
161                         remaining reference (i.e. the menu was removed)
162                         and delete it in that case.
163                 */
164
165         } else {
166                 cur_formspec->setFormspecPrepend(formspecPrepend);
167                 cur_formspec->setFormSource(fs_src);
168                 cur_formspec->setTextDest(txt_dest);
169         }
170 }
171
172 void GUIFormSpecMenu::removeChildren()
173 {
174         const core::list<gui::IGUIElement*> &children = getChildren();
175
176         while (!children.empty()) {
177                 (*children.getLast())->remove();
178         }
179
180         if (m_tooltip_element) {
181                 m_tooltip_element->remove();
182                 m_tooltip_element->drop();
183                 m_tooltip_element = nullptr;
184         }
185 }
186
187 void GUIFormSpecMenu::setInitialFocus()
188 {
189         // Set initial focus according to following order of precedence:
190         // 1. first empty editbox
191         // 2. first editbox
192         // 3. first table
193         // 4. last button
194         // 5. first focusable (not statictext, not tabheader)
195         // 6. first child element
196
197         core::list<gui::IGUIElement*> children = getChildren();
198
199         // in case "children" contains any NULL elements, remove them
200         for (core::list<gui::IGUIElement*>::Iterator it = children.begin();
201                         it != children.end();) {
202                 if (*it)
203                         ++it;
204                 else
205                         it = children.erase(it);
206         }
207
208         // 1. first empty editbox
209         for (gui::IGUIElement *it : children) {
210                 if (it->getType() == gui::EGUIET_EDIT_BOX
211                                 && it->getText()[0] == 0) {
212                         Environment->setFocus(it);
213                         return;
214                 }
215         }
216
217         // 2. first editbox
218         for (gui::IGUIElement *it : children) {
219                 if (it->getType() == gui::EGUIET_EDIT_BOX) {
220                         Environment->setFocus(it);
221                         return;
222                 }
223         }
224
225         // 3. first table
226         for (gui::IGUIElement *it : children) {
227                 if (it->getTypeName() == std::string("GUITable")) {
228                         Environment->setFocus(it);
229                         return;
230                 }
231         }
232
233         // 4. last button
234         for (core::list<gui::IGUIElement*>::Iterator it = children.getLast();
235                         it != children.end(); --it) {
236                 if ((*it)->getType() == gui::EGUIET_BUTTON) {
237                         Environment->setFocus(*it);
238                         return;
239                 }
240         }
241
242         // 5. first focusable (not statictext, not tabheader)
243         for (gui::IGUIElement *it : children) {
244                 if (it->getType() != gui::EGUIET_STATIC_TEXT &&
245                         it->getType() != gui::EGUIET_TAB_CONTROL) {
246                         Environment->setFocus(it);
247                         return;
248                 }
249         }
250
251         // 6. first child element
252         if (children.empty())
253                 Environment->setFocus(this);
254         else
255                 Environment->setFocus(*(children.begin()));
256 }
257
258 GUITable* GUIFormSpecMenu::getTable(const std::string &tablename)
259 {
260         for (auto &table : m_tables) {
261                 if (tablename == table.first.fname)
262                         return table.second;
263         }
264         return 0;
265 }
266
267 std::vector<std::string>* GUIFormSpecMenu::getDropDownValues(const std::string &name)
268 {
269         for (auto &dropdown : m_dropdowns) {
270                 if (name == dropdown.first.fname)
271                         return &dropdown.second;
272         }
273         return NULL;
274 }
275
276 v2s32 GUIFormSpecMenu::getElementBasePos(const std::vector<std::string> *v_pos)
277 {
278         v2f32 pos_f = v2f32(padding.X, padding.Y) + pos_offset * spacing;
279         if (v_pos) {
280                 pos_f.X += stof((*v_pos)[0]) * spacing.X;
281                 pos_f.Y += stof((*v_pos)[1]) * spacing.Y;
282         }
283         return v2s32(pos_f.X, pos_f.Y);
284 }
285
286 v2s32 GUIFormSpecMenu::getRealCoordinateBasePos(const std::vector<std::string> &v_pos)
287 {
288         return v2s32((stof(v_pos[0]) + pos_offset.X) * imgsize.X,
289                 (stof(v_pos[1]) + pos_offset.Y) * imgsize.Y);
290 }
291
292 v2s32 GUIFormSpecMenu::getRealCoordinateGeometry(const std::vector<std::string> &v_geom)
293 {
294         return v2s32(stof(v_geom[0]) * imgsize.X, stof(v_geom[1]) * imgsize.Y);
295 }
296
297 void GUIFormSpecMenu::parseSize(parserData* data, const std::string &element)
298 {
299         std::vector<std::string> parts = split(element,',');
300
301         if (((parts.size() == 2) || parts.size() == 3) ||
302                 ((parts.size() > 3) && (m_formspec_version > FORMSPEC_API_VERSION)))
303         {
304                 if (parts[1].find(';') != std::string::npos)
305                         parts[1] = parts[1].substr(0,parts[1].find(';'));
306
307                 data->invsize.X = MYMAX(0, stof(parts[0]));
308                 data->invsize.Y = MYMAX(0, stof(parts[1]));
309
310                 lockSize(false);
311 #ifndef __ANDROID__
312                 if (parts.size() == 3) {
313                         if (parts[2] == "true") {
314                                 lockSize(true,v2u32(800,600));
315                         }
316                 }
317 #endif
318                 data->explicit_size = true;
319                 return;
320         }
321         errorstream<< "Invalid size element (" << parts.size() << "): '" << element << "'"  << std::endl;
322 }
323
324 void GUIFormSpecMenu::parseContainer(parserData* data, const std::string &element)
325 {
326         std::vector<std::string> parts = split(element, ',');
327
328         if (parts.size() >= 2) {
329                 if (parts[1].find(';') != std::string::npos)
330                         parts[1] = parts[1].substr(0, parts[1].find(';'));
331
332                 container_stack.push(pos_offset);
333                 pos_offset.X += stof(parts[0]);
334                 pos_offset.Y += stof(parts[1]);
335                 return;
336         }
337         errorstream<< "Invalid container start element (" << parts.size() << "): '" << element << "'"  << std::endl;
338 }
339
340 void GUIFormSpecMenu::parseContainerEnd(parserData* data)
341 {
342         if (container_stack.empty()) {
343                 errorstream<< "Invalid container end element, no matching container start element"  << std::endl;
344         } else {
345                 pos_offset = container_stack.top();
346                 container_stack.pop();
347         }
348 }
349
350 void GUIFormSpecMenu::parseList(parserData* data, const std::string &element)
351 {
352         if (m_client == 0) {
353                 warningstream<<"invalid use of 'list' with m_client==0"<<std::endl;
354                 return;
355         }
356
357         std::vector<std::string> parts = split(element,';');
358
359         if (((parts.size() == 4) || (parts.size() == 5)) ||
360                 ((parts.size() > 5) && (m_formspec_version > FORMSPEC_API_VERSION)))
361         {
362                 std::string location = parts[0];
363                 std::string listname = parts[1];
364                 std::vector<std::string> v_pos  = split(parts[2],',');
365                 std::vector<std::string> v_geom = split(parts[3],',');
366                 std::string startindex;
367                 if (parts.size() == 5)
368                         startindex = parts[4];
369
370                 MY_CHECKPOS("list",2);
371                 MY_CHECKGEOM("list",3);
372
373                 InventoryLocation loc;
374
375                 if (location == "context" || location == "current_name")
376                         loc = m_current_inventory_location;
377                 else
378                         loc.deSerialize(location);
379
380                 v2s32 geom;
381                 geom.X = stoi(v_geom[0]);
382                 geom.Y = stoi(v_geom[1]);
383
384                 s32 start_i = 0;
385                 if (!startindex.empty())
386                         start_i = stoi(startindex);
387
388                 if (geom.X < 0 || geom.Y < 0 || start_i < 0) {
389                         errorstream<< "Invalid list element: '" << element << "'"  << std::endl;
390                         return;
391                 }
392
393                 // check for the existence of inventory and list
394                 Inventory *inv = m_invmgr->getInventory(loc);
395                 if (!inv) {
396                         warningstream << "GUIFormSpecMenu::parseList(): "
397                                         << "The inventory location "
398                                         << "\"" << loc.dump() << "\" doesn't exist"
399                                         << std::endl;
400                         return;
401                 }
402                 InventoryList *ilist = inv->getList(listname);
403                 if (!ilist) {
404                         warningstream << "GUIFormSpecMenu::parseList(): "
405                                         << "The inventory list \"" << listname << "\" "
406                                         << "@ \"" << loc.dump() << "\" doesn't exist"
407                                         << std::endl;
408                         return;
409                 }
410
411                 // trim geom if it is larger than the actual inventory size
412                 s32 list_size = (s32)ilist->getSize();
413                 if (list_size < geom.X * geom.Y + start_i) {
414                         list_size -= MYMAX(start_i, 0);
415                         geom.Y = list_size / geom.X;
416                         geom.Y += list_size % geom.X > 0 ? 1 : 0;
417                         if (geom.Y <= 1)
418                                 geom.X = list_size;
419                 }
420
421                 if (!data->explicit_size)
422                         warningstream << "invalid use of list without a size[] element" << std::endl;
423
424                 FieldSpec spec(
425                         "",
426                         L"",
427                         L"",
428                         258 + m_fields.size(),
429                         3
430                 );
431
432                 v2s32 pos;
433                 core::rect<s32> rect;
434
435                 if (data->real_coordinates) {
436                         pos = getRealCoordinateBasePos(v_pos);
437                         rect = core::rect<s32>(pos.X, pos.Y,
438                                         pos.X + (geom.X - 1) * (imgsize.X * 1.25) + imgsize.X,
439                                         pos.Y + (geom.Y - 1) * (imgsize.Y * 1.25) + imgsize.Y);
440                 } else {
441                         pos = getElementBasePos(&v_pos);
442                         rect = core::rect<s32>(pos.X, pos.Y,
443                                         pos.X + (geom.X - 1) * spacing.X + imgsize.X,
444                                         pos.Y + (geom.Y - 1) * spacing.Y + imgsize.Y);
445                 }
446
447                 gui::IGUIElement *e = new gui::IGUIElement(EGUIET_ELEMENT, Environment,
448                                 this, spec.fid, rect);
449
450                 // the element the list is bound to should not block mouse-clicks
451                 e->setVisible(false);
452
453                 m_inventorylists.emplace_back(loc, listname, e, geom, start_i,
454                                 data->real_coordinates);
455                 m_fields.push_back(spec);
456                 return;
457         }
458         errorstream<< "Invalid list element(" << parts.size() << "): '" << element << "'"  << std::endl;
459 }
460
461 void GUIFormSpecMenu::parseListRing(parserData* data, const std::string &element)
462 {
463         if (m_client == 0) {
464                 errorstream << "WARNING: invalid use of 'listring' with m_client==0" << std::endl;
465                 return;
466         }
467
468         std::vector<std::string> parts = split(element, ';');
469
470         if (parts.size() == 2) {
471                 std::string location = parts[0];
472                 std::string listname = parts[1];
473
474                 InventoryLocation loc;
475
476                 if (location == "context" || location == "current_name")
477                         loc = m_current_inventory_location;
478                 else
479                         loc.deSerialize(location);
480
481                 m_inventory_rings.emplace_back(loc, listname);
482                 return;
483         }
484
485         if (element.empty() && m_inventorylists.size() > 1) {
486                 size_t siz = m_inventorylists.size();
487                 // insert the last two inv list elements into the list ring
488                 const ListDrawSpec &spa = m_inventorylists[siz - 2];
489                 const ListDrawSpec &spb = m_inventorylists[siz - 1];
490                 m_inventory_rings.emplace_back(spa.inventoryloc, spa.listname);
491                 m_inventory_rings.emplace_back(spb.inventoryloc, spb.listname);
492                 return;
493         }
494
495         errorstream<< "Invalid list ring element(" << parts.size() << ", "
496                 << m_inventorylists.size() << "): '" << element << "'"  << std::endl;
497 }
498
499 void GUIFormSpecMenu::parseCheckbox(parserData* data, const std::string &element)
500 {
501         std::vector<std::string> parts = split(element,';');
502
503         if (((parts.size() >= 3) && (parts.size() <= 4)) ||
504                 ((parts.size() > 4) && (m_formspec_version > FORMSPEC_API_VERSION)))
505         {
506                 std::vector<std::string> v_pos = split(parts[0],',');
507                 std::string name = parts[1];
508                 std::string label = parts[2];
509                 std::string selected;
510
511                 if (parts.size() >= 4)
512                         selected = parts[3];
513
514                 MY_CHECKPOS("checkbox",0);
515
516                 bool fselected = false;
517
518                 if (selected == "true")
519                         fselected = true;
520
521                 std::wstring wlabel = translate_string(utf8_to_wide(unescape_string(label)));
522                 const core::dimension2d<u32> label_size = m_font->getDimension(wlabel.c_str());
523                 s32 cb_size = Environment->getSkin()->getSize(gui::EGDS_CHECK_BOX_WIDTH);
524                 s32 y_center = (std::max(label_size.Height, (u32)cb_size) + 1) / 2;
525
526                 v2s32 pos;
527                 core::rect<s32> rect;
528
529                 if (data->real_coordinates) {
530                         pos = getRealCoordinateBasePos(v_pos);
531
532                         rect = core::rect<s32>(
533                                         pos.X,
534                                         pos.Y - y_center,
535                                         pos.X + label_size.Width + cb_size + 7,
536                                         pos.Y + y_center
537                                 );
538                 } else {
539                         pos = getElementBasePos(&v_pos);
540                         rect = core::rect<s32>(
541                                         pos.X,
542                                         pos.Y + imgsize.Y / 2 - y_center,
543                                         pos.X + label_size.Width + cb_size + 7,
544                                         pos.Y + imgsize.Y / 2 + y_center
545                                 );
546                 }
547
548                 FieldSpec spec(
549                                 name,
550                                 wlabel, //Needed for displaying text on MSVC
551                                 wlabel,
552                                 258+m_fields.size()
553                         );
554
555                 spec.ftype = f_CheckBox;
556
557                 gui::IGUICheckBox *e = Environment->addCheckBox(fselected, rect, this,
558                                         spec.fid, spec.flabel.c_str());
559
560                 auto style = getStyleForElement("checkbox", name);
561                 e->setNotClipped(style.getBool(StyleSpec::NOCLIP, false));
562
563                 if (spec.fname == data->focused_fieldname) {
564                         Environment->setFocus(e);
565                 }
566
567                 e->grab();
568                 m_checkboxes.emplace_back(spec, e);
569                 m_fields.push_back(spec);
570                 return;
571         }
572         errorstream<< "Invalid checkbox element(" << parts.size() << "): '" << element << "'"  << std::endl;
573 }
574
575 void GUIFormSpecMenu::parseScrollBar(parserData* data, const std::string &element)
576 {
577         std::vector<std::string> parts = split(element,';');
578
579         if (parts.size() >= 5) {
580                 std::vector<std::string> v_pos = split(parts[0],',');
581                 std::vector<std::string> v_geom = split(parts[1],',');
582                 std::string name = parts[3];
583                 std::string value = parts[4];
584
585                 MY_CHECKPOS("scrollbar",0);
586                 MY_CHECKGEOM("scrollbar",1);
587
588                 v2s32 pos;
589                 v2s32 dim;
590
591                 if (data->real_coordinates) {
592                         pos = getRealCoordinateBasePos(v_pos);
593                         dim = getRealCoordinateGeometry(v_geom);
594                 } else {
595                         pos = getElementBasePos(&v_pos);
596                         dim.X = stof(v_geom[0]) * spacing.X;
597                         dim.Y = stof(v_geom[1]) * spacing.Y;
598                 }
599
600                 core::rect<s32> rect =
601                                 core::rect<s32>(pos.X, pos.Y, pos.X + dim.X, pos.Y + dim.Y);
602
603                 FieldSpec spec(
604                                 name,
605                                 L"",
606                                 L"",
607                                 258+m_fields.size()
608                         );
609
610                 bool is_horizontal = true;
611
612                 if (parts[2] == "vertical")
613                         is_horizontal = false;
614
615                 spec.ftype = f_ScrollBar;
616                 spec.send  = true;
617                 GUIScrollBar *e = new GUIScrollBar(Environment, this, spec.fid, rect,
618                                 is_horizontal, true);
619
620                 auto style = getStyleForElement("scrollbar", name);
621                 e->setNotClipped(style.getBool(StyleSpec::NOCLIP, false));
622                 e->setArrowsVisible(data->scrollbar_options.arrow_visiblity);
623
624                 s32 max = data->scrollbar_options.max;
625                 s32 min = data->scrollbar_options.min;
626
627                 e->setMax(max);
628                 e->setMin(min);
629
630                 e->setPos(stoi(parts[4]));
631
632                 e->setSmallStep(data->scrollbar_options.small_step);
633                 e->setLargeStep(data->scrollbar_options.large_step);
634
635                 s32 scrollbar_size = is_horizontal ? dim.X : dim.Y;
636
637                 e->setPageSize(scrollbar_size * (max - min + 1) / data->scrollbar_options.thumb_size);
638
639                 m_scrollbars.emplace_back(spec,e);
640                 m_fields.push_back(spec);
641                 return;
642         }
643         errorstream << "Invalid scrollbar element(" << parts.size() << "): '" << element
644                 << "'" << std::endl;
645 }
646
647 void GUIFormSpecMenu::parseScrollBarOptions(parserData* data, const std::string &element)
648 {
649         std::vector<std::string> parts = split(element, ';');
650
651         if (parts.size() == 0) {
652                 warningstream << "Invalid scrollbaroptions element(" << parts.size() << "): '" <<
653                         element << "'"  << std::endl;
654                 return;
655         }
656
657         for (const std::string &i : parts) {
658                 std::vector<std::string> options = split(i, '=');
659
660                 if (options.size() != 2) {
661                         warningstream << "Invalid scrollbaroptions option syntax: '" <<
662                                 element << "'" << std::endl;
663                         continue; // Go to next option
664                 }
665
666                 if (options[0] == "max") {
667                         data->scrollbar_options.max = stoi(options[1]);
668                         continue;
669                 } else if (options[0] == "min") {
670                         data->scrollbar_options.min = stoi(options[1]);
671                         continue;
672                 } else if (options[0] == "smallstep") {
673                         int value = stoi(options[1]);
674                         data->scrollbar_options.small_step = value < 0 ? 10 : value;
675                         continue;
676                 } else if (options[0] == "largestep") {
677                         int value = stoi(options[1]);
678                         data->scrollbar_options.large_step = value < 0 ? 100 : value;
679                         continue;
680                 } else if (options[0] == "thumbsize") {
681                         int value = stoi(options[1]);
682                         data->scrollbar_options.thumb_size = value <= 0 ? 1 : value;
683                         continue;
684                 } else if (options[0] == "arrows") {
685                         std::string value = trim(options[1]);
686                         if (value == "hide")
687                                 data->scrollbar_options.arrow_visiblity = GUIScrollBar::HIDE;
688                         else if (value == "show")
689                                 data->scrollbar_options.arrow_visiblity = GUIScrollBar::SHOW;
690                         else // Auto hide/show
691                                 data->scrollbar_options.arrow_visiblity = GUIScrollBar::DEFAULT;
692                         continue;
693                 }
694
695                 warningstream << "Invalid scrollbaroptions option(" << options[0] <<
696                         "): '" << element << "'" << std::endl;
697         }
698 }
699
700 void GUIFormSpecMenu::parseImage(parserData* data, const std::string &element)
701 {
702         std::vector<std::string> parts = split(element,';');
703
704         if ((parts.size() == 3) ||
705                 ((parts.size() > 3) && (m_formspec_version > FORMSPEC_API_VERSION)))
706         {
707                 std::vector<std::string> v_pos = split(parts[0],',');
708                 std::vector<std::string> v_geom = split(parts[1],',');
709                 std::string name = unescape_string(parts[2]);
710
711                 MY_CHECKPOS("image", 0);
712                 MY_CHECKGEOM("image", 1);
713
714                 v2s32 pos;
715                 v2s32 geom;
716
717                 if (data->real_coordinates) {
718                         pos = getRealCoordinateBasePos(v_pos);
719                         geom = getRealCoordinateGeometry(v_geom);
720                 } else {
721                         pos = getElementBasePos(&v_pos);
722                         geom.X = stof(v_geom[0]) * (float)imgsize.X;
723                         geom.Y = stof(v_geom[1]) * (float)imgsize.Y;
724                 }
725
726                 if (!data->explicit_size)
727                         warningstream<<"invalid use of image without a size[] element"<<std::endl;
728
729                 video::ITexture *texture = m_tsrc->getTexture(name);
730                 if (!texture) {
731                         errorstream << "GUIFormSpecMenu::parseImage() Unable to load texture:"
732                                         << std::endl << "\t" << name << std::endl;
733                         return;
734                 }
735
736                 FieldSpec spec(
737                         name,
738                         L"",
739                         L"",
740                         258 + m_fields.size(),
741                         1
742                 );
743                 core::rect<s32> rect(pos, pos + geom);
744                 gui::IGUIImage *e = Environment->addImage(rect, this, spec.fid, 0, true);
745                 e->setImage(texture);
746                 e->setScaleImage(true);
747                 auto style = getStyleForElement("image", spec.fname);
748                 e->setNotClipped(style.getBool(StyleSpec::NOCLIP, m_formspec_version < 3));
749                 m_fields.push_back(spec);
750
751                 return;
752         }
753
754         if (parts.size() == 2) {
755                 std::vector<std::string> v_pos = split(parts[0],',');
756                 std::string name = unescape_string(parts[1]);
757
758                 MY_CHECKPOS("image", 0);
759
760                 v2s32 pos = getElementBasePos(&v_pos);
761
762                 if (!data->explicit_size)
763                         warningstream<<"invalid use of image without a size[] element"<<std::endl;
764
765                 video::ITexture *texture = m_tsrc->getTexture(name);
766                 if (!texture) {
767                         errorstream << "GUIFormSpecMenu::parseImage() Unable to load texture:"
768                                         << std::endl << "\t" << name << std::endl;
769                         return;
770                 }
771
772                 FieldSpec spec(
773                         name,
774                         L"",
775                         L"",
776                         258 + m_fields.size()
777                 );
778                 gui::IGUIImage *e = Environment->addImage(texture, pos, true, this,
779                                 spec.fid, 0);
780                 auto style = getStyleForElement("image", spec.fname);
781                 e->setNotClipped(style.getBool(StyleSpec::NOCLIP, m_formspec_version < 3));
782                 m_fields.push_back(spec);
783
784                 return;
785         }
786         errorstream<< "Invalid image element(" << parts.size() << "): '" << element << "'"  << std::endl;
787 }
788
789 void GUIFormSpecMenu::parseItemImage(parserData* data, const std::string &element)
790 {
791         std::vector<std::string> parts = split(element,';');
792
793         if ((parts.size() == 3) ||
794                 ((parts.size() > 3) && (m_formspec_version > FORMSPEC_API_VERSION)))
795         {
796                 std::vector<std::string> v_pos = split(parts[0],',');
797                 std::vector<std::string> v_geom = split(parts[1],',');
798                 std::string name = parts[2];
799
800                 MY_CHECKPOS("itemimage",0);
801                 MY_CHECKGEOM("itemimage",1);
802
803                 v2s32 pos;
804                 v2s32 geom;
805
806                 if (data->real_coordinates) {
807                         pos = getRealCoordinateBasePos(v_pos);
808                         geom = getRealCoordinateGeometry(v_geom);
809                 } else {
810                         pos = getElementBasePos(&v_pos);
811                         geom.X = stof(v_geom[0]) * (float)imgsize.X;
812                         geom.Y = stof(v_geom[1]) * (float)imgsize.Y;
813                 }
814
815                 if(!data->explicit_size)
816                         warningstream<<"invalid use of item_image without a size[] element"<<std::endl;
817
818                 FieldSpec spec(
819                         "",
820                         L"",
821                         L"",
822                         258 + m_fields.size(),
823                         2
824                 );
825                 spec.ftype = f_ItemImage;
826
827                 GUIItemImage *e = new GUIItemImage(Environment, this, spec.fid,
828                                 core::rect<s32>(pos, pos + geom), name, m_font, m_client);
829                 auto style = getStyleForElement("item_image", spec.fname);
830                 e->setNotClipped(style.getBool(StyleSpec::NOCLIP, false));
831                 e->drop();
832
833                 m_fields.push_back(spec);
834                 return;
835         }
836         errorstream<< "Invalid ItemImage element(" << parts.size() << "): '" << element << "'"  << std::endl;
837 }
838
839 void GUIFormSpecMenu::parseButton(parserData* data, const std::string &element,
840                 const std::string &type)
841 {
842         std::vector<std::string> parts = split(element,';');
843
844         if ((parts.size() == 4) ||
845                 ((parts.size() > 4) && (m_formspec_version > FORMSPEC_API_VERSION)))
846         {
847                 std::vector<std::string> v_pos = split(parts[0],',');
848                 std::vector<std::string> v_geom = split(parts[1],',');
849                 std::string name = parts[2];
850                 std::string label = parts[3];
851
852                 MY_CHECKPOS("button",0);
853                 MY_CHECKGEOM("button",1);
854
855                 v2s32 pos;
856                 v2s32 geom;
857                 core::rect<s32> rect;
858
859                 if (data->real_coordinates) {
860                         pos = getRealCoordinateBasePos(v_pos);
861                         geom = getRealCoordinateGeometry(v_geom);
862                         rect = core::rect<s32>(pos.X, pos.Y, pos.X+geom.X,
863                                 pos.Y+geom.Y);
864                 } else {
865                         pos = getElementBasePos(&v_pos);
866                         geom.X = (stof(v_geom[0]) * spacing.X) - (spacing.X - imgsize.X);
867                         pos.Y += (stof(v_geom[1]) * (float)imgsize.Y)/2;
868
869                         rect = core::rect<s32>(pos.X, pos.Y - m_btn_height,
870                                                 pos.X + geom.X, pos.Y + m_btn_height);
871                 }
872
873                 if(!data->explicit_size)
874                         warningstream<<"invalid use of button without a size[] element"<<std::endl;
875
876                 std::wstring wlabel = translate_string(utf8_to_wide(unescape_string(label)));
877
878                 FieldSpec spec(
879                         name,
880                         wlabel,
881                         L"",
882                         258 + m_fields.size()
883                 );
884                 spec.ftype = f_Button;
885                 if(type == "button_exit")
886                         spec.is_exit = true;
887
888                 GUIButton *e = GUIButton::addButton(Environment, rect, this, spec.fid, spec.flabel.c_str());
889
890                 auto style = getStyleForElement(type, name, (type != "button") ? "button" : "");
891                 e->setFromStyle(style, m_tsrc);
892
893                 if (spec.fname == data->focused_fieldname) {
894                         Environment->setFocus(e);
895                 }
896
897                 m_fields.push_back(spec);
898                 return;
899         }
900         errorstream<< "Invalid button element(" << parts.size() << "): '" << element << "'"  << std::endl;
901 }
902
903 void GUIFormSpecMenu::parseBackground(parserData* data, const std::string &element)
904 {
905         std::vector<std::string> parts = split(element,';');
906
907         if ((parts.size() >= 3 && parts.size() <= 5) ||
908                         (parts.size() > 5 && m_formspec_version > FORMSPEC_API_VERSION)) {
909                 std::vector<std::string> v_pos = split(parts[0],',');
910                 std::vector<std::string> v_geom = split(parts[1],',');
911                 std::string name = unescape_string(parts[2]);
912
913                 MY_CHECKPOS("background",0);
914                 MY_CHECKGEOM("background",1);
915
916                 v2s32 pos;
917                 v2s32 geom;
918
919                 if (data->real_coordinates) {
920                         pos = getRealCoordinateBasePos(v_pos);
921                         geom = getRealCoordinateGeometry(v_geom);
922                 } else {
923                         pos = getElementBasePos(&v_pos);
924                         pos.X -= (spacing.X - (float)imgsize.X) / 2;
925                         pos.Y -= (spacing.Y - (float)imgsize.Y) / 2;
926
927                         geom.X = stof(v_geom[0]) * spacing.X;
928                         geom.Y = stof(v_geom[1]) * spacing.Y;
929                 }
930
931                 bool clip = false;
932                 if (parts.size() >= 4 && is_yes(parts[3])) {
933                         if (data->real_coordinates) {
934                                 pos = getRealCoordinateBasePos(v_pos) * -1;
935                                 geom = v2s32(0, 0);
936                         } else {
937                                 pos.X = stoi(v_pos[0]); //acts as offset
938                                 pos.Y = stoi(v_pos[1]);
939                         }
940                         clip = true;
941                 }
942
943                 core::rect<s32> middle;
944                 if (parts.size() >= 5) {
945                         std::vector<std::string> v_middle = split(parts[4], ',');
946                         if (v_middle.size() == 1) {
947                                 s32 x = stoi(v_middle[0]);
948                                 middle.UpperLeftCorner = core::vector2di(x, x);
949                                 middle.LowerRightCorner = core::vector2di(-x, -x);
950                         } else if (v_middle.size() == 2) {
951                                 s32 x = stoi(v_middle[0]);
952                                 s32 y = stoi(v_middle[1]);
953                                 middle.UpperLeftCorner = core::vector2di(x, y);
954                                 middle.LowerRightCorner = core::vector2di(-x, -y);
955                                 // `-x` is interpreted as `w - x`
956                         } else if (v_middle.size() == 4) {
957                                 middle.UpperLeftCorner = core::vector2di(stoi(v_middle[0]), stoi(v_middle[1]));
958                                 middle.LowerRightCorner = core::vector2di(stoi(v_middle[2]), stoi(v_middle[3]));
959                         } else {
960                                 warningstream << "Invalid rectangle given to middle param of background[] element" << std::endl;
961                         }
962                 }
963
964                 if (!data->explicit_size && !clip)
965                         warningstream << "invalid use of unclipped background without a size[] element" << std::endl;
966
967                 FieldSpec spec(
968                         name,
969                         L"",
970                         L"",
971                         258 + m_fields.size()
972                 );
973
974                 core::rect<s32> rect;
975                 if (!clip) {
976                         // no auto_clip => position like normal image
977                         rect = core::rect<s32>(pos, pos + geom);
978                 } else {
979                         // it will be auto-clipped when drawing
980                         rect = core::rect<s32>(-pos, pos);
981                 }
982
983                 GUIBackgroundImage *e = new GUIBackgroundImage(Environment, this, spec.fid,
984                                 rect, name, middle, m_tsrc, clip);
985
986                 FATAL_ERROR_IF(!e, "Failed to create background formspec element");
987
988                 e->setNotClipped(true);
989
990                 e->setVisible(false); // the element is drawn manually before all others
991
992                 m_backgrounds.push_back(e);
993                 m_fields.push_back(spec);
994                 return;
995         }
996         errorstream<< "Invalid background element(" << parts.size() << "): '" << element << "'"  << std::endl;
997 }
998
999 void GUIFormSpecMenu::parseTableOptions(parserData* data, const std::string &element)
1000 {
1001         std::vector<std::string> parts = split(element,';');
1002
1003         data->table_options.clear();
1004         for (const std::string &part : parts) {
1005                 // Parse table option
1006                 std::string opt = unescape_string(part);
1007                 data->table_options.push_back(GUITable::splitOption(opt));
1008         }
1009 }
1010
1011 void GUIFormSpecMenu::parseTableColumns(parserData* data, const std::string &element)
1012 {
1013         std::vector<std::string> parts = split(element,';');
1014
1015         data->table_columns.clear();
1016         for (const std::string &part : parts) {
1017                 std::vector<std::string> col_parts = split(part,',');
1018                 GUITable::TableColumn column;
1019                 // Parse column type
1020                 if (!col_parts.empty())
1021                         column.type = col_parts[0];
1022                 // Parse column options
1023                 for (size_t j = 1; j < col_parts.size(); ++j) {
1024                         std::string opt = unescape_string(col_parts[j]);
1025                         column.options.push_back(GUITable::splitOption(opt));
1026                 }
1027                 data->table_columns.push_back(column);
1028         }
1029 }
1030
1031 void GUIFormSpecMenu::parseTable(parserData* data, const std::string &element)
1032 {
1033         std::vector<std::string> parts = split(element,';');
1034
1035         if (((parts.size() == 4) || (parts.size() == 5)) ||
1036                 ((parts.size() > 5) && (m_formspec_version > FORMSPEC_API_VERSION)))
1037         {
1038                 std::vector<std::string> v_pos = split(parts[0],',');
1039                 std::vector<std::string> v_geom = split(parts[1],',');
1040                 std::string name = parts[2];
1041                 std::vector<std::string> items = split(parts[3],',');
1042                 std::string str_initial_selection;
1043                 std::string str_transparent = "false";
1044
1045                 if (parts.size() >= 5)
1046                         str_initial_selection = parts[4];
1047
1048                 MY_CHECKPOS("table",0);
1049                 MY_CHECKGEOM("table",1);
1050
1051                 v2s32 pos;
1052                 v2s32 geom;
1053
1054                 if (data->real_coordinates) {
1055                         pos = getRealCoordinateBasePos(v_pos);
1056                         geom = getRealCoordinateGeometry(v_geom);
1057                 } else {
1058                         pos = getElementBasePos(&v_pos);
1059                         geom.X = stof(v_geom[0]) * spacing.X;
1060                         geom.Y = stof(v_geom[1]) * spacing.Y;
1061                 }
1062
1063                 core::rect<s32> rect = core::rect<s32>(pos.X, pos.Y, pos.X+geom.X, pos.Y+geom.Y);
1064
1065                 FieldSpec spec(
1066                         name,
1067                         L"",
1068                         L"",
1069                         258 + m_fields.size()
1070                 );
1071
1072                 spec.ftype = f_Table;
1073
1074                 for (std::string &item : items) {
1075                         item = wide_to_utf8(unescape_translate(utf8_to_wide(unescape_string(item))));
1076                 }
1077
1078                 //now really show table
1079                 GUITable *e = new GUITable(Environment, this, spec.fid, rect, m_tsrc);
1080
1081                 if (spec.fname == data->focused_fieldname) {
1082                         Environment->setFocus(e);
1083                 }
1084
1085                 e->setTable(data->table_options, data->table_columns, items);
1086
1087                 if (data->table_dyndata.find(name) != data->table_dyndata.end()) {
1088                         e->setDynamicData(data->table_dyndata[name]);
1089                 }
1090
1091                 if (!str_initial_selection.empty() && str_initial_selection != "0")
1092                         e->setSelected(stoi(str_initial_selection));
1093
1094                 auto style = getStyleForElement("table", name);
1095                 e->setNotClipped(style.getBool(StyleSpec::NOCLIP, false));
1096
1097                 m_tables.emplace_back(spec, e);
1098                 m_fields.push_back(spec);
1099                 return;
1100         }
1101         errorstream<< "Invalid table element(" << parts.size() << "): '" << element << "'"  << std::endl;
1102 }
1103
1104 void GUIFormSpecMenu::parseTextList(parserData* data, const std::string &element)
1105 {
1106         std::vector<std::string> parts = split(element,';');
1107
1108         if (((parts.size() == 4) || (parts.size() == 5) || (parts.size() == 6)) ||
1109                 ((parts.size() > 6) && (m_formspec_version > FORMSPEC_API_VERSION)))
1110         {
1111                 std::vector<std::string> v_pos = split(parts[0],',');
1112                 std::vector<std::string> v_geom = split(parts[1],',');
1113                 std::string name = parts[2];
1114                 std::vector<std::string> items = split(parts[3],',');
1115                 std::string str_initial_selection;
1116                 std::string str_transparent = "false";
1117
1118                 if (parts.size() >= 5)
1119                         str_initial_selection = parts[4];
1120
1121                 if (parts.size() >= 6)
1122                         str_transparent = parts[5];
1123
1124                 MY_CHECKPOS("textlist",0);
1125                 MY_CHECKGEOM("textlist",1);
1126
1127                 v2s32 pos;
1128                 v2s32 geom;
1129
1130                 if (data->real_coordinates) {
1131                         pos = getRealCoordinateBasePos(v_pos);
1132                         geom = getRealCoordinateGeometry(v_geom);
1133                 } else {
1134                         pos = getElementBasePos(&v_pos);
1135                         geom.X = stof(v_geom[0]) * spacing.X;
1136                         geom.Y = stof(v_geom[1]) * spacing.Y;
1137                 }
1138
1139                 core::rect<s32> rect = core::rect<s32>(pos.X, pos.Y, pos.X+geom.X, pos.Y+geom.Y);
1140
1141                 FieldSpec spec(
1142                         name,
1143                         L"",
1144                         L"",
1145                         258 + m_fields.size()
1146                 );
1147
1148                 spec.ftype = f_Table;
1149
1150                 for (std::string &item : items) {
1151                         item = wide_to_utf8(unescape_translate(utf8_to_wide(unescape_string(item))));
1152                 }
1153
1154                 //now really show list
1155                 GUITable *e = new GUITable(Environment, this, spec.fid, rect, m_tsrc);
1156
1157                 if (spec.fname == data->focused_fieldname) {
1158                         Environment->setFocus(e);
1159                 }
1160
1161                 e->setTextList(items, is_yes(str_transparent));
1162
1163                 if (data->table_dyndata.find(name) != data->table_dyndata.end()) {
1164                         e->setDynamicData(data->table_dyndata[name]);
1165                 }
1166
1167                 if (!str_initial_selection.empty() && str_initial_selection != "0")
1168                         e->setSelected(stoi(str_initial_selection));
1169
1170                 auto style = getStyleForElement("textlist", name);
1171                 e->setNotClipped(style.getBool(StyleSpec::NOCLIP, false));
1172
1173                 m_tables.emplace_back(spec, e);
1174                 m_fields.push_back(spec);
1175                 return;
1176         }
1177         errorstream<< "Invalid textlist element(" << parts.size() << "): '" << element << "'"  << std::endl;
1178 }
1179
1180
1181 void GUIFormSpecMenu::parseDropDown(parserData* data, const std::string &element)
1182 {
1183         std::vector<std::string> parts = split(element,';');
1184
1185         if ((parts.size() == 5) ||
1186                 ((parts.size() > 5) && (m_formspec_version > FORMSPEC_API_VERSION)))
1187         {
1188                 std::vector<std::string> v_pos = split(parts[0],',');
1189                 std::string name = parts[2];
1190                 std::vector<std::string> items = split(parts[3],',');
1191                 std::string str_initial_selection;
1192                 str_initial_selection = parts[4];
1193
1194                 MY_CHECKPOS("dropdown",0);
1195
1196                 v2s32 pos;
1197                 v2s32 geom;
1198                 core::rect<s32> rect;
1199
1200                 if (data->real_coordinates) {
1201                         std::vector<std::string> v_geom = split(parts[1],',');
1202
1203                         if (v_geom.size() == 1)
1204                                 v_geom.emplace_back("1");
1205
1206                         MY_CHECKGEOM("dropdown",1);
1207
1208                         pos = getRealCoordinateBasePos(v_pos);
1209                         geom = getRealCoordinateGeometry(v_geom);
1210                         rect = core::rect<s32>(pos.X, pos.Y, pos.X+geom.X, pos.Y+geom.Y);
1211                 } else {
1212                         pos = getElementBasePos(&v_pos);
1213
1214                         s32 width = stof(parts[1]) * spacing.Y;
1215
1216                         rect = core::rect<s32>(pos.X, pos.Y,
1217                                         pos.X + width, pos.Y + (m_btn_height * 2));
1218                 }
1219
1220                 FieldSpec spec(
1221                         name,
1222                         L"",
1223                         L"",
1224                         258 + m_fields.size()
1225                 );
1226
1227                 spec.ftype = f_DropDown;
1228                 spec.send = true;
1229
1230                 //now really show list
1231                 gui::IGUIComboBox *e = Environment->addComboBox(rect, this, spec.fid);
1232
1233                 if (spec.fname == data->focused_fieldname) {
1234                         Environment->setFocus(e);
1235                 }
1236
1237                 for (const std::string &item : items) {
1238                         e->addItem(unescape_translate(unescape_string(
1239                                 utf8_to_wide(item))).c_str());
1240                 }
1241
1242                 if (!str_initial_selection.empty())
1243                         e->setSelected(stoi(str_initial_selection)-1);
1244
1245                 auto style = getStyleForElement("dropdown", name);
1246                 e->setNotClipped(style.getBool(StyleSpec::NOCLIP, false));
1247
1248                 m_fields.push_back(spec);
1249
1250                 m_dropdowns.emplace_back(spec, std::vector<std::string>());
1251                 std::vector<std::string> &values = m_dropdowns.back().second;
1252                 for (const std::string &item : items) {
1253                         values.push_back(unescape_string(item));
1254                 }
1255
1256                 return;
1257         }
1258         errorstream << "Invalid dropdown element(" << parts.size() << "): '"
1259                                 << element << "'"  << std::endl;
1260 }
1261
1262 void GUIFormSpecMenu::parseFieldCloseOnEnter(parserData *data, const std::string &element)
1263 {
1264         std::vector<std::string> parts = split(element,';');
1265         if (parts.size() == 2 ||
1266                         (parts.size() > 2 && m_formspec_version > FORMSPEC_API_VERSION)) {
1267                 field_close_on_enter[parts[0]] = is_yes(parts[1]);
1268         }
1269 }
1270
1271 void GUIFormSpecMenu::parsePwdField(parserData* data, const std::string &element)
1272 {
1273         std::vector<std::string> parts = split(element,';');
1274
1275         if ((parts.size() == 4) || (parts.size() == 5) ||
1276                 ((parts.size() > 5) && (m_formspec_version > FORMSPEC_API_VERSION)))
1277         {
1278                 std::vector<std::string> v_pos = split(parts[0],',');
1279                 std::vector<std::string> v_geom = split(parts[1],',');
1280                 std::string name = parts[2];
1281                 std::string label = parts[3];
1282
1283                 MY_CHECKPOS("pwdfield",0);
1284                 MY_CHECKGEOM("pwdfield",1);
1285
1286                 v2s32 pos;
1287                 v2s32 geom;
1288
1289                 if (data->real_coordinates) {
1290                         pos = getRealCoordinateBasePos(v_pos);
1291                         geom = getRealCoordinateGeometry(v_geom);
1292                 } else {
1293                         pos = getElementBasePos(&v_pos);
1294                         pos -= padding;
1295
1296                         geom.X = (stof(v_geom[0]) * spacing.X) - (spacing.X - imgsize.X);
1297
1298                         pos.Y += (stof(v_geom[1]) * (float)imgsize.Y)/2;
1299                         pos.Y -= m_btn_height;
1300                         geom.Y = m_btn_height*2;
1301                 }
1302
1303                 core::rect<s32> rect = core::rect<s32>(pos.X, pos.Y, pos.X+geom.X, pos.Y+geom.Y);
1304
1305                 std::wstring wlabel = translate_string(utf8_to_wide(unescape_string(label)));
1306
1307                 FieldSpec spec(
1308                         name,
1309                         wlabel,
1310                         L"",
1311                         258 + m_fields.size(),
1312                         0,
1313                         ECI_IBEAM
1314                         );
1315
1316                 spec.send = true;
1317                 gui::IGUIEditBox * e = Environment->addEditBox(0, rect, true, this, spec.fid);
1318
1319                 if (spec.fname == data->focused_fieldname) {
1320                         Environment->setFocus(e);
1321                 }
1322
1323                 if (label.length() >= 1) {
1324                         int font_height = g_fontengine->getTextHeight();
1325                         rect.UpperLeftCorner.Y -= font_height;
1326                         rect.LowerRightCorner.Y = rect.UpperLeftCorner.Y + font_height;
1327                         gui::StaticText::add(Environment, spec.flabel.c_str(), rect, false, true,
1328                                 this, 0);
1329                 }
1330
1331                 e->setPasswordBox(true,L'*');
1332
1333                 auto style = getStyleForElement("pwdfield", name, "field");
1334                 e->setNotClipped(style.getBool(StyleSpec::NOCLIP, false));
1335                 e->setDrawBorder(style.getBool(StyleSpec::BORDER, true));
1336                 e->setOverrideColor(style.getColor(StyleSpec::TEXTCOLOR, video::SColor(0xFFFFFFFF)));
1337
1338                 irr::SEvent evt;
1339                 evt.EventType            = EET_KEY_INPUT_EVENT;
1340                 evt.KeyInput.Key         = KEY_END;
1341                 evt.KeyInput.Char        = 0;
1342                 evt.KeyInput.Control     = false;
1343                 evt.KeyInput.Shift       = false;
1344                 evt.KeyInput.PressedDown = true;
1345                 e->OnEvent(evt);
1346
1347                 if (parts.size() >= 5) {
1348                         // TODO: remove after 2016-11-03
1349                         warningstream << "pwdfield: use field_close_on_enter[name, enabled]" <<
1350                                         " instead of the 5th param" << std::endl;
1351                         field_close_on_enter[name] = is_yes(parts[4]);
1352                 }
1353
1354                 m_fields.push_back(spec);
1355                 return;
1356         }
1357         errorstream<< "Invalid pwdfield element(" << parts.size() << "): '" << element << "'"  << std::endl;
1358 }
1359
1360 void GUIFormSpecMenu::createTextField(parserData *data, FieldSpec &spec,
1361         core::rect<s32> &rect, bool is_multiline)
1362 {
1363         bool is_editable = !spec.fname.empty();
1364         if (!is_editable && !is_multiline) {
1365                 // spec field id to 0, this stops submit searching for a value that isn't there
1366                 gui::StaticText::add(Environment, spec.flabel.c_str(), rect, false, true,
1367                                 this, spec.fid);
1368                 return;
1369         }
1370
1371         if (is_editable) {
1372                 spec.send = true;
1373         } else if (is_multiline &&
1374                         spec.fdefault.empty() && !spec.flabel.empty()) {
1375                 // Multiline textareas: swap default and label for backwards compat
1376                 spec.flabel.swap(spec.fdefault);
1377         }
1378
1379         gui::IGUIEditBox *e = nullptr;
1380         static constexpr bool use_intl_edit_box = USE_FREETYPE &&
1381                 IRRLICHT_VERSION_MAJOR == 1 && IRRLICHT_VERSION_MINOR < 9;
1382
1383         if (use_intl_edit_box && g_settings->getBool("freetype")) {
1384                 e = new gui::intlGUIEditBox(spec.fdefault.c_str(), true, Environment,
1385                                 this, spec.fid, rect, is_editable, is_multiline);
1386         } else {
1387                 if (is_multiline) {
1388                         e = new GUIEditBoxWithScrollBar(spec.fdefault.c_str(), true,
1389                                         Environment, this, spec.fid, rect, is_editable, true);
1390                 } else if (is_editable) {
1391                         e = Environment->addEditBox(spec.fdefault.c_str(), rect, true, this,
1392                                         spec.fid);
1393                         e->grab();
1394                 }
1395         }
1396
1397         auto style = getStyleForElement(is_multiline ? "textarea" : "field", spec.fname);
1398
1399         if (e) {
1400                 if (is_editable && spec.fname == data->focused_fieldname)
1401                         Environment->setFocus(e);
1402
1403                 if (is_multiline) {
1404                         e->setMultiLine(true);
1405                         e->setWordWrap(true);
1406                         e->setTextAlignment(gui::EGUIA_UPPERLEFT, gui::EGUIA_UPPERLEFT);
1407                 } else {
1408                         irr::SEvent evt;
1409                         evt.EventType            = EET_KEY_INPUT_EVENT;
1410                         evt.KeyInput.Key         = KEY_END;
1411                         evt.KeyInput.Char        = 0;
1412                         evt.KeyInput.Control     = 0;
1413                         evt.KeyInput.Shift       = 0;
1414                         evt.KeyInput.PressedDown = true;
1415                         e->OnEvent(evt);
1416                 }
1417
1418                 e->setNotClipped(style.getBool(StyleSpec::NOCLIP, false));
1419                 e->setDrawBorder(style.getBool(StyleSpec::BORDER, true));
1420                 e->setOverrideColor(style.getColor(StyleSpec::TEXTCOLOR, video::SColor(0xFFFFFFFF)));
1421                 if (style.get(StyleSpec::BGCOLOR, "") == "transparent") {
1422                         e->setDrawBackground(false);
1423                 }
1424
1425                 e->drop();
1426         }
1427
1428         if (!spec.flabel.empty()) {
1429                 int font_height = g_fontengine->getTextHeight();
1430                 rect.UpperLeftCorner.Y -= font_height;
1431                 rect.LowerRightCorner.Y = rect.UpperLeftCorner.Y + font_height;
1432                 IGUIElement *t = gui::StaticText::add(Environment, spec.flabel.c_str(),
1433                                 rect, false, true, this, 0);
1434
1435                 if (t)
1436                         t->setNotClipped(style.getBool(StyleSpec::NOCLIP, false));
1437         }
1438 }
1439
1440 void GUIFormSpecMenu::parseSimpleField(parserData *data,
1441         std::vector<std::string> &parts)
1442 {
1443         std::string name = parts[0];
1444         std::string label = parts[1];
1445         std::string default_val = parts[2];
1446
1447         core::rect<s32> rect;
1448
1449         if (data->explicit_size)
1450                 warningstream << "invalid use of unpositioned \"field\" in inventory" << std::endl;
1451
1452         v2s32 pos = getElementBasePos(nullptr);
1453         pos.Y = (data->simple_field_count + 2) * 60;
1454         v2s32 size = DesiredRect.getSize();
1455
1456         rect = core::rect<s32>(
1457                         size.X / 2 - 150,       pos.Y,
1458                         size.X / 2 - 150 + 300, pos.Y + m_btn_height * 2
1459         );
1460
1461
1462         if (m_form_src)
1463                 default_val = m_form_src->resolveText(default_val);
1464
1465
1466         std::wstring wlabel = translate_string(utf8_to_wide(unescape_string(label)));
1467
1468         FieldSpec spec(
1469                 name,
1470                 wlabel,
1471                 utf8_to_wide(unescape_string(default_val)),
1472                 258 + m_fields.size(),
1473                 0,
1474                 ECI_IBEAM
1475         );
1476
1477         createTextField(data, spec, rect, false);
1478
1479         m_fields.push_back(spec);
1480
1481         data->simple_field_count++;
1482 }
1483
1484 void GUIFormSpecMenu::parseTextArea(parserData* data, std::vector<std::string>& parts,
1485                 const std::string &type)
1486 {
1487         std::vector<std::string> v_pos = split(parts[0],',');
1488         std::vector<std::string> v_geom = split(parts[1],',');
1489         std::string name = parts[2];
1490         std::string label = parts[3];
1491         std::string default_val = parts[4];
1492
1493         MY_CHECKPOS(type,0);
1494         MY_CHECKGEOM(type,1);
1495
1496         v2s32 pos;
1497         v2s32 geom;
1498
1499         if (data->real_coordinates) {
1500                 pos = getRealCoordinateBasePos(v_pos);
1501                 geom = getRealCoordinateGeometry(v_geom);
1502         } else {
1503                 pos = getElementBasePos(&v_pos);
1504                 pos -= padding;
1505
1506                 geom.X = (stof(v_geom[0]) * spacing.X) - (spacing.X - imgsize.X);
1507
1508                 if (type == "textarea")
1509                 {
1510                         geom.Y = (stof(v_geom[1]) * (float)imgsize.Y) - (spacing.Y-imgsize.Y);
1511                         pos.Y += m_btn_height;
1512                 }
1513                 else
1514                 {
1515                         pos.Y += (stof(v_geom[1]) * (float)imgsize.Y)/2;
1516                         pos.Y -= m_btn_height;
1517                         geom.Y = m_btn_height*2;
1518                 }
1519         }
1520
1521         core::rect<s32> rect = core::rect<s32>(pos.X, pos.Y, pos.X+geom.X, pos.Y+geom.Y);
1522
1523         if(!data->explicit_size)
1524                 warningstream<<"invalid use of positioned "<<type<<" without a size[] element"<<std::endl;
1525
1526         if(m_form_src)
1527                 default_val = m_form_src->resolveText(default_val);
1528
1529
1530         std::wstring wlabel = translate_string(utf8_to_wide(unescape_string(label)));
1531
1532         FieldSpec spec(
1533                 name,
1534                 wlabel,
1535                 utf8_to_wide(unescape_string(default_val)),
1536                 258 + m_fields.size(),
1537                 0,
1538                 ECI_IBEAM
1539         );
1540
1541         createTextField(data, spec, rect, type == "textarea");
1542
1543         if (parts.size() >= 6) {
1544                 // TODO: remove after 2016-11-03
1545                 warningstream << "field/textarea: use field_close_on_enter[name, enabled]" <<
1546                                 " instead of the 6th param" << std::endl;
1547                 field_close_on_enter[name] = is_yes(parts[5]);
1548         }
1549
1550         m_fields.push_back(spec);
1551 }
1552
1553 void GUIFormSpecMenu::parseField(parserData* data, const std::string &element,
1554                 const std::string &type)
1555 {
1556         std::vector<std::string> parts = split(element,';');
1557
1558         if (parts.size() == 3 || parts.size() == 4) {
1559                 parseSimpleField(data,parts);
1560                 return;
1561         }
1562
1563         if ((parts.size() == 5) || (parts.size() == 6) ||
1564                 ((parts.size() > 6) && (m_formspec_version > FORMSPEC_API_VERSION)))
1565         {
1566                 parseTextArea(data,parts,type);
1567                 return;
1568         }
1569         errorstream<< "Invalid field element(" << parts.size() << "): '" << element << "'"  << std::endl;
1570 }
1571
1572 void GUIFormSpecMenu::parseHyperText(parserData *data, const std::string &element)
1573 {
1574         std::vector<std::string> parts = split(element, ';');
1575
1576         if (parts.size() != 4 && m_formspec_version < FORMSPEC_API_VERSION) {
1577                 errorstream << "Invalid text element(" << parts.size() << "): '" << element << "'"  << std::endl;
1578                 return;
1579         }
1580
1581         std::vector<std::string> v_pos = split(parts[0], ',');
1582         std::vector<std::string> v_geom = split(parts[1], ',');
1583         std::string name = parts[2];
1584         std::string text = parts[3];
1585
1586         MY_CHECKPOS("hypertext", 0);
1587         MY_CHECKGEOM("hypertext", 1);
1588
1589         v2s32 pos;
1590         v2s32 geom;
1591
1592         if (data->real_coordinates) {
1593                 pos = getRealCoordinateBasePos(v_pos);
1594                 geom = getRealCoordinateGeometry(v_geom);
1595         } else {
1596                 pos = getElementBasePos(&v_pos);
1597                 pos -= padding;
1598
1599                 pos.X += stof(v_pos[0]) * spacing.X;
1600                 pos.Y += stof(v_pos[1]) * spacing.Y + (m_btn_height * 2);
1601
1602                 geom.X = (stof(v_geom[0]) * spacing.X) - (spacing.X - imgsize.X);
1603                 geom.Y = (stof(v_geom[1]) * imgsize.Y) - (spacing.Y - imgsize.Y);
1604         }
1605
1606         core::rect<s32> rect = core::rect<s32>(pos.X, pos.Y, pos.X + geom.X, pos.Y + geom.Y);
1607
1608         if(m_form_src)
1609                 text = m_form_src->resolveText(text);
1610
1611         FieldSpec spec(
1612                 name,
1613                 utf8_to_wide(unescape_string(text)),
1614                 L"",
1615                 258 + m_fields.size()
1616         );
1617
1618         spec.ftype = f_Unknown;
1619         new GUIHyperText(
1620                 spec.flabel.c_str(), Environment, this, spec.fid, rect, m_client, m_tsrc);
1621
1622         m_fields.push_back(spec);
1623 }
1624
1625 void GUIFormSpecMenu::parseLabel(parserData* data, const std::string &element)
1626 {
1627         std::vector<std::string> parts = split(element,';');
1628
1629         if ((parts.size() == 2) ||
1630                 ((parts.size() > 2) && (m_formspec_version > FORMSPEC_API_VERSION)))
1631         {
1632                 std::vector<std::string> v_pos = split(parts[0],',');
1633                 std::string text = parts[1];
1634
1635                 MY_CHECKPOS("label",0);
1636
1637                 if(!data->explicit_size)
1638                         warningstream<<"invalid use of label without a size[] element"<<std::endl;
1639
1640                 std::vector<std::string> lines = split(text, '\n');
1641
1642                 for (unsigned int i = 0; i != lines.size(); i++) {
1643                         std::wstring wlabel_colors = translate_string(
1644                                 utf8_to_wide(unescape_string(lines[i])));
1645                         // Without color escapes to get the font dimensions
1646                         std::wstring wlabel_plain = unescape_enriched(wlabel_colors);
1647
1648                         core::rect<s32> rect;
1649
1650                         if (data->real_coordinates) {
1651                                 // Lines are spaced at the distance of 1/2 imgsize.
1652                                 // This alows lines that line up with the new elements
1653                                 // easily without sacrificing good line distance.  If
1654                                 // it was one whole imgsize, it would have too much
1655                                 // spacing.
1656                                 v2s32 pos = getRealCoordinateBasePos(v_pos);
1657
1658                                 // Labels are positioned by their center, not their top.
1659                                 pos.Y += (((float) imgsize.Y) / -2) + (((float) imgsize.Y) * i / 2);
1660
1661                                 rect = core::rect<s32>(
1662                                         pos.X, pos.Y,
1663                                         pos.X + m_font->getDimension(wlabel_plain.c_str()).Width,
1664                                         pos.Y + imgsize.Y);
1665
1666                         } else {
1667                                 // Lines are spaced at the nominal distance of
1668                                 // 2/5 inventory slot, even if the font doesn't
1669                                 // quite match that.  This provides consistent
1670                                 // form layout, at the expense of sometimes
1671                                 // having sub-optimal spacing for the font.
1672                                 // We multiply by 2 and then divide by 5, rather
1673                                 // than multiply by 0.4, to get exact results
1674                                 // in the integer cases: 0.4 is not exactly
1675                                 // representable in binary floating point.
1676
1677                                 v2s32 pos = getElementBasePos(nullptr);
1678                                 pos.X += stof(v_pos[0]) * spacing.X;
1679                                 pos.Y += (stof(v_pos[1]) + 7.0f / 30.0f) * spacing.Y;
1680
1681                                 pos.Y += ((float) i) * spacing.Y * 2.0 / 5.0;
1682
1683                                 rect = core::rect<s32>(
1684                                         pos.X, pos.Y - m_btn_height,
1685                                         pos.X + m_font->getDimension(wlabel_plain.c_str()).Width,
1686                                         pos.Y + m_btn_height);
1687                         }
1688
1689                         FieldSpec spec(
1690                                 "",
1691                                 wlabel_colors,
1692                                 L"",
1693                                 258 + m_fields.size(),
1694                                 4
1695                         );
1696                         gui::IGUIStaticText *e = gui::StaticText::add(Environment,
1697                                         spec.flabel.c_str(), rect, false, false, this, spec.fid);
1698                         e->setTextAlignment(gui::EGUIA_UPPERLEFT, gui::EGUIA_CENTER);
1699
1700                         auto style = getStyleForElement("label", spec.fname);
1701                         e->setNotClipped(style.getBool(StyleSpec::NOCLIP, false));
1702                         e->setOverrideColor(style.getColor(StyleSpec::TEXTCOLOR, video::SColor(0xFFFFFFFF)));
1703
1704                         m_fields.push_back(spec);
1705                 }
1706
1707                 return;
1708         }
1709         errorstream << "Invalid label element(" << parts.size() << "): '" << element
1710                 << "'"  << std::endl;
1711 }
1712
1713 void GUIFormSpecMenu::parseVertLabel(parserData* data, const std::string &element)
1714 {
1715         std::vector<std::string> parts = split(element,';');
1716
1717         if ((parts.size() == 2) ||
1718                 ((parts.size() > 2) && (m_formspec_version > FORMSPEC_API_VERSION)))
1719         {
1720                 std::vector<std::string> v_pos = split(parts[0],',');
1721                 std::wstring text = unescape_translate(
1722                         unescape_string(utf8_to_wide(parts[1])));
1723
1724                 MY_CHECKPOS("vertlabel",1);
1725
1726                 v2s32 pos;
1727                 core::rect<s32> rect;
1728
1729                 if (data->real_coordinates) {
1730                         pos = getRealCoordinateBasePos(v_pos);
1731
1732                         // Vertlabels are positioned by center, not left.
1733                         pos.X -= imgsize.X / 2;
1734
1735                         // We use text.length + 1 because without it, the rect
1736                         // isn't quite tall enough and cuts off the text.
1737                         rect = core::rect<s32>(pos.X, pos.Y,
1738                                 pos.X + imgsize.X,
1739                                 pos.Y + font_line_height(m_font) *
1740                                 (text.length() + 1));
1741
1742                 } else {
1743                         pos = getElementBasePos(&v_pos);
1744
1745                         // As above, the length must be one longer. The width of
1746                         // the rect (15 pixels) seems rather arbitrary, but
1747                         // changing it might break something.
1748                         rect = core::rect<s32>(
1749                                 pos.X, pos.Y+((imgsize.Y/2) - m_btn_height),
1750                                 pos.X+15, pos.Y +
1751                                         font_line_height(m_font) *
1752                                         (text.length() + 1) +
1753                                         ((imgsize.Y/2) - m_btn_height));
1754                 }
1755
1756                 if(!data->explicit_size)
1757                         warningstream<<"invalid use of label without a size[] element"<<std::endl;
1758
1759                 std::wstring label;
1760
1761                 for (wchar_t i : text) {
1762                         label += i;
1763                         label += L"\n";
1764                 }
1765
1766                 FieldSpec spec(
1767                         "",
1768                         label,
1769                         L"",
1770                         258 + m_fields.size()
1771                 );
1772                 gui::IGUIStaticText *e = gui::StaticText::add(Environment, spec.flabel.c_str(),
1773                                 rect, false, false, this, spec.fid);
1774                 e->setTextAlignment(gui::EGUIA_CENTER, gui::EGUIA_CENTER);
1775
1776                 auto style = getStyleForElement("vertlabel", spec.fname, "label");
1777                 e->setNotClipped(style.getBool(StyleSpec::NOCLIP, false));
1778                 e->setOverrideColor(style.getColor(StyleSpec::TEXTCOLOR, video::SColor(0xFFFFFFFF)));
1779
1780                 m_fields.push_back(spec);
1781                 return;
1782         }
1783         errorstream<< "Invalid vertlabel element(" << parts.size() << "): '" << element << "'"  << std::endl;
1784 }
1785
1786 void GUIFormSpecMenu::parseImageButton(parserData* data, const std::string &element,
1787                 const std::string &type)
1788 {
1789         std::vector<std::string> parts = split(element,';');
1790
1791         if ((((parts.size() >= 5) && (parts.size() <= 8)) && (parts.size() != 6)) ||
1792                 ((parts.size() > 8) && (m_formspec_version > FORMSPEC_API_VERSION)))
1793         {
1794                 std::vector<std::string> v_pos = split(parts[0],',');
1795                 std::vector<std::string> v_geom = split(parts[1],',');
1796                 std::string image_name = parts[2];
1797                 std::string name = parts[3];
1798                 std::string label = parts[4];
1799
1800                 MY_CHECKPOS("imagebutton",0);
1801                 MY_CHECKGEOM("imagebutton",1);
1802
1803                 bool noclip     = false;
1804                 bool drawborder = true;
1805                 std::string pressed_image_name;
1806
1807                 if (parts.size() >= 7) {
1808                         if (parts[5] == "true")
1809                                 noclip = true;
1810                         if (parts[6] == "false")
1811                                 drawborder = false;
1812                 }
1813
1814                 if (parts.size() >= 8) {
1815                         pressed_image_name = parts[7];
1816                 }
1817
1818                 v2s32 pos;
1819                 v2s32 geom;
1820
1821                 if (data->real_coordinates) {
1822                         pos = getRealCoordinateBasePos(v_pos);
1823                         geom = getRealCoordinateGeometry(v_geom);
1824                 } else {
1825                         pos = getElementBasePos(&v_pos);
1826                         geom.X = (stof(v_geom[0]) * spacing.X) - (spacing.X - imgsize.X);
1827                         geom.Y = (stof(v_geom[1]) * spacing.Y) - (spacing.Y - imgsize.Y);
1828                 }
1829
1830                 core::rect<s32> rect = core::rect<s32>(pos.X, pos.Y, pos.X+geom.X,
1831                         pos.Y+geom.Y);
1832
1833                 if (!data->explicit_size)
1834                         warningstream<<"invalid use of image_button without a size[] element"<<std::endl;
1835
1836                 image_name = unescape_string(image_name);
1837                 pressed_image_name = unescape_string(pressed_image_name);
1838
1839                 std::wstring wlabel = utf8_to_wide(unescape_string(label));
1840
1841                 FieldSpec spec(
1842                         name,
1843                         wlabel,
1844                         utf8_to_wide(image_name),
1845                         258 + m_fields.size()
1846                 );
1847                 spec.ftype = f_Button;
1848                 if (type == "image_button_exit")
1849                         spec.is_exit = true;
1850
1851                 GUIButtonImage *e = GUIButtonImage::addButton(Environment, rect, this, spec.fid, spec.flabel.c_str());
1852
1853                 if (spec.fname == data->focused_fieldname) {
1854                         Environment->setFocus(e);
1855                 }
1856
1857                 auto style = getStyleForElement("image_button", spec.fname);
1858                 e->setFromStyle(style, m_tsrc);
1859
1860                 // We explicitly handle these arguments *after* the style properties in
1861                 // order to override them if they are provided
1862                 if (!image_name.empty())
1863                 {
1864                         video::ITexture *texture = m_tsrc->getTexture(image_name);
1865                         e->setForegroundImage(guiScalingImageButton(
1866                                 Environment->getVideoDriver(), texture, geom.X, geom.Y));
1867                 }
1868                 if (!pressed_image_name.empty()) {
1869                         video::ITexture *pressed_texture = m_tsrc->getTexture(pressed_image_name);
1870                         e->setPressedForegroundImage(guiScalingImageButton(
1871                                                 Environment->getVideoDriver(), pressed_texture, geom.X, geom.Y));
1872                 }
1873                 e->setScaleImage(true);
1874
1875                 if (parts.size() >= 7) {
1876                         e->setNotClipped(noclip);
1877                         e->setDrawBorder(drawborder);
1878                 }
1879
1880                 m_fields.push_back(spec);
1881                 return;
1882         }
1883
1884         errorstream<< "Invalid imagebutton element(" << parts.size() << "): '" << element << "'"  << std::endl;
1885 }
1886
1887 void GUIFormSpecMenu::parseTabHeader(parserData* data, const std::string &element)
1888 {
1889         std::vector<std::string> parts = split(element, ';');
1890
1891         if (((parts.size() == 4) || (parts.size() == 6)) || (parts.size() == 7 &&
1892                 data->real_coordinates) || ((parts.size() > 6) &&
1893                 (m_formspec_version > FORMSPEC_API_VERSION)))
1894         {
1895                 std::vector<std::string> v_pos = split(parts[0],',');
1896
1897                 // If we're using real coordinates, add an extra field for height.
1898                 // Width is not here because tabs are the width of the text, and
1899                 // there's no reason to change that.
1900                 unsigned int i = 0;
1901                 std::vector<std::string> v_geom = {"1", "0.75"}; // Dummy width and default height
1902                 bool auto_width = true;
1903                 if (parts.size() == 7) {
1904                         i++;
1905
1906                         v_geom = split(parts[1], ',');
1907                         if (v_geom.size() == 1)
1908                                 v_geom.insert(v_geom.begin(), "1"); // Dummy value
1909                         else
1910                                 auto_width = false;
1911                 }
1912
1913                 std::string name = parts[i+1];
1914                 std::vector<std::string> buttons = split(parts[i+2], ',');
1915                 std::string str_index = parts[i+3];
1916                 bool show_background = true;
1917                 bool show_border = true;
1918                 int tab_index = stoi(str_index) - 1;
1919
1920                 MY_CHECKPOS("tabheader", 0);
1921
1922                 if (parts.size() == 6 + i) {
1923                         if (parts[4+i] == "true")
1924                                 show_background = false;
1925                         if (parts[5+i] == "false")
1926                                 show_border = false;
1927                 }
1928
1929                 FieldSpec spec(
1930                         name,
1931                         L"",
1932                         L"",
1933                         258 + m_fields.size()
1934                 );
1935
1936                 spec.ftype = f_TabHeader;
1937
1938                 v2s32 pos;
1939                 v2s32 geom;
1940
1941                 if (data->real_coordinates) {
1942                         pos = getRealCoordinateBasePos(v_pos);
1943
1944                         geom = getRealCoordinateGeometry(v_geom);
1945                         pos.Y -= geom.Y; // TabHeader base pos is the bottom, not the top.
1946                         if (auto_width)
1947                                 geom.X = DesiredRect.getWidth(); // Set automatic width
1948
1949                         MY_CHECKGEOM("tabheader", 1);
1950                 } else {
1951                         v2f32 pos_f = pos_offset * spacing;
1952                         pos_f.X += stof(v_pos[0]) * spacing.X;
1953                         pos_f.Y += stof(v_pos[1]) * spacing.Y - m_btn_height * 2;
1954                         pos = v2s32(pos_f.X, pos_f.Y);
1955
1956                         geom.Y = m_btn_height * 2;
1957                         geom.X = DesiredRect.getWidth();
1958                 }
1959
1960                 core::rect<s32> rect = core::rect<s32>(pos.X, pos.Y, pos.X+geom.X,
1961                                 pos.Y+geom.Y);
1962
1963                 gui::IGUITabControl *e = Environment->addTabControl(rect, this,
1964                                 show_background, show_border, spec.fid);
1965                 e->setAlignment(irr::gui::EGUIA_UPPERLEFT, irr::gui::EGUIA_UPPERLEFT,
1966                                 irr::gui::EGUIA_UPPERLEFT, irr::gui::EGUIA_LOWERRIGHT);
1967                 e->setTabHeight(geom.Y);
1968
1969                 if (spec.fname == data->focused_fieldname) {
1970                         Environment->setFocus(e);
1971                 }
1972
1973                 auto style = getStyleForElement("tabheader", name);
1974                 e->setNotClipped(style.getBool(StyleSpec::NOCLIP, true));
1975
1976                 for (const std::string &button : buttons) {
1977                         auto tab = e->addTab(unescape_translate(unescape_string(
1978                                 utf8_to_wide(button))).c_str(), -1);
1979                         if (style.isNotDefault(StyleSpec::BGCOLOR))
1980                                 tab->setBackgroundColor(style.getColor(StyleSpec::BGCOLOR));
1981
1982                         tab->setTextColor(style.getColor(StyleSpec::TEXTCOLOR, video::SColor(0xFFFFFFFF)));
1983                 }
1984
1985                 if ((tab_index >= 0) &&
1986                                 (buttons.size() < INT_MAX) &&
1987                                 (tab_index < (int) buttons.size()))
1988                         e->setActiveTab(tab_index);
1989
1990                 m_fields.push_back(spec);
1991                 return;
1992         }
1993         errorstream << "Invalid TabHeader element(" << parts.size() << "): '"
1994                         << element << "'"  << std::endl;
1995 }
1996
1997 void GUIFormSpecMenu::parseItemImageButton(parserData* data, const std::string &element)
1998 {
1999
2000         if (m_client == 0) {
2001                 warningstream << "invalid use of item_image_button with m_client==0"
2002                         << std::endl;
2003                 return;
2004         }
2005
2006         std::vector<std::string> parts = split(element,';');
2007
2008         if ((parts.size() == 5) ||
2009                 ((parts.size() > 5) && (m_formspec_version > FORMSPEC_API_VERSION)))
2010         {
2011                 std::vector<std::string> v_pos = split(parts[0],',');
2012                 std::vector<std::string> v_geom = split(parts[1],',');
2013                 std::string item_name = parts[2];
2014                 std::string name = parts[3];
2015                 std::string label = parts[4];
2016
2017                 label = unescape_string(label);
2018                 item_name = unescape_string(item_name);
2019
2020                 MY_CHECKPOS("itemimagebutton",0);
2021                 MY_CHECKGEOM("itemimagebutton",1);
2022
2023                 v2s32 pos;
2024                 v2s32 geom;
2025
2026                 if (data->real_coordinates) {
2027                         pos = getRealCoordinateBasePos(v_pos);
2028                         geom = getRealCoordinateGeometry(v_geom);
2029                 } else {
2030                         pos = getElementBasePos(&v_pos);
2031                         geom.X = (stof(v_geom[0]) * spacing.X) - (spacing.X - imgsize.X);
2032                         geom.Y = (stof(v_geom[1]) * spacing.Y) - (spacing.Y - imgsize.Y);
2033                 }
2034
2035                 core::rect<s32> rect = core::rect<s32>(pos.X, pos.Y, pos.X+geom.X, pos.Y+geom.Y);
2036
2037                 if(!data->explicit_size)
2038                         warningstream<<"invalid use of item_image_button without a size[] element"<<std::endl;
2039
2040                 IItemDefManager *idef = m_client->idef();
2041                 ItemStack item;
2042                 item.deSerialize(item_name, idef);
2043
2044                 m_tooltips[name] =
2045                         TooltipSpec(utf8_to_wide(item.getDefinition(idef).description),
2046                                                 m_default_tooltip_bgcolor,
2047                                                 m_default_tooltip_color);
2048
2049                 // the spec for the button
2050                 FieldSpec spec_btn(
2051                         name,
2052                         utf8_to_wide(label),
2053                         utf8_to_wide(item_name),
2054                         258 + m_fields.size(),
2055                         2
2056                 );
2057
2058                 GUIButtonItemImage *e_btn = GUIButtonItemImage::addButton(Environment, rect, this, spec_btn.fid, spec_btn.flabel.c_str(), item_name, m_client);
2059
2060                 auto style = getStyleForElement("item_image_button", spec_btn.fname, "image_button");
2061                 e_btn->setFromStyle(style, m_tsrc);
2062
2063                 if (spec_btn.fname == data->focused_fieldname) {
2064                         Environment->setFocus(e_btn);
2065                 }
2066
2067                 spec_btn.ftype = f_Button;
2068                 rect += data->basepos-padding;
2069                 spec_btn.rect = rect;
2070                 m_fields.push_back(spec_btn);
2071                 return;
2072         }
2073         errorstream<< "Invalid ItemImagebutton element(" << parts.size() << "): '" << element << "'"  << std::endl;
2074 }
2075
2076 void GUIFormSpecMenu::parseBox(parserData* data, const std::string &element)
2077 {
2078         std::vector<std::string> parts = split(element,';');
2079
2080         if ((parts.size() == 3) ||
2081                 ((parts.size() > 3) && (m_formspec_version > FORMSPEC_API_VERSION)))
2082         {
2083                 std::vector<std::string> v_pos = split(parts[0],',');
2084                 std::vector<std::string> v_geom = split(parts[1],',');
2085
2086                 MY_CHECKPOS("box",0);
2087                 MY_CHECKGEOM("box",1);
2088
2089                 v2s32 pos;
2090                 v2s32 geom;
2091
2092                 if (data->real_coordinates) {
2093                         pos = getRealCoordinateBasePos(v_pos);
2094                         geom = getRealCoordinateGeometry(v_geom);
2095                 } else {
2096                         pos = getElementBasePos(&v_pos);
2097                         geom.X = stof(v_geom[0]) * spacing.X;
2098                         geom.Y = stof(v_geom[1]) * spacing.Y;
2099                 }
2100
2101                 video::SColor tmp_color;
2102
2103                 if (parseColorString(parts[2], tmp_color, false, 0x8C)) {
2104                         FieldSpec spec(
2105                                 "",
2106                                 L"",
2107                                 L"",
2108                                 258 + m_fields.size(),
2109                                 -2
2110                         );
2111                         spec.ftype = f_Box;
2112
2113                         core::rect<s32> rect(pos, pos + geom);
2114
2115                         GUIBox *e = new GUIBox(Environment, this, spec.fid, rect, tmp_color);
2116
2117                         auto style = getStyleForElement("box", spec.fname);
2118                         e->setNotClipped(style.getBool(StyleSpec::NOCLIP, m_formspec_version < 3));
2119
2120                         e->drop();
2121
2122                         m_fields.push_back(spec);
2123
2124                 } else {
2125                         errorstream<< "Invalid Box element(" << parts.size() << "): '" << element << "'  INVALID COLOR"  << std::endl;
2126                 }
2127                 return;
2128         }
2129         errorstream<< "Invalid Box element(" << parts.size() << "): '" << element << "'"  << std::endl;
2130 }
2131
2132 void GUIFormSpecMenu::parseBackgroundColor(parserData* data, const std::string &element)
2133 {
2134         std::vector<std::string> parts = split(element,';');
2135         const u32 parameter_count = parts.size();
2136
2137         if ((parameter_count > 2 && m_formspec_version < 3) ||
2138                         (parameter_count > 3 && m_formspec_version <= FORMSPEC_API_VERSION)) {
2139                 errorstream << "Invalid bgcolor element(" << parameter_count << "): '"
2140                                 << element << "'" << std::endl;
2141                 return;
2142         }
2143
2144         // bgcolor
2145         if (parameter_count >= 1 && parts[0] != "")
2146                 parseColorString(parts[0], m_bgcolor, false);
2147
2148         // fullscreen
2149         if (parameter_count >= 2) {
2150                 if (parts[1] == "both") {
2151                         m_bgnonfullscreen = true;
2152                         m_bgfullscreen = true;
2153                 } else if (parts[1] == "neither") {
2154                         m_bgnonfullscreen = false;
2155                         m_bgfullscreen = false;
2156                 } else if (parts[1] != "" || m_formspec_version < 3) {
2157                         m_bgfullscreen = is_yes(parts[1]);
2158                         m_bgnonfullscreen = !m_bgfullscreen;
2159                 }
2160         }
2161
2162         // fbgcolor
2163         if (parameter_count >= 3 && parts[2] != "")
2164                 parseColorString(parts[2], m_fullscreen_bgcolor, false);
2165 }
2166
2167 void GUIFormSpecMenu::parseListColors(parserData* data, const std::string &element)
2168 {
2169         std::vector<std::string> parts = split(element,';');
2170
2171         if (((parts.size() == 2) || (parts.size() == 3) || (parts.size() == 5)) ||
2172                 ((parts.size() > 5) && (m_formspec_version > FORMSPEC_API_VERSION)))
2173         {
2174                 parseColorString(parts[0], m_slotbg_n, false);
2175                 parseColorString(parts[1], m_slotbg_h, false);
2176
2177                 if (parts.size() >= 3) {
2178                         if (parseColorString(parts[2], m_slotbordercolor, false)) {
2179                                 m_slotborder = true;
2180                         }
2181                 }
2182                 if (parts.size() == 5) {
2183                         video::SColor tmp_color;
2184
2185                         if (parseColorString(parts[3], tmp_color, false))
2186                                 m_default_tooltip_bgcolor = tmp_color;
2187                         if (parseColorString(parts[4], tmp_color, false))
2188                                 m_default_tooltip_color = tmp_color;
2189                 }
2190                 return;
2191         }
2192         errorstream<< "Invalid listcolors element(" << parts.size() << "): '" << element << "'"  << std::endl;
2193 }
2194
2195 void GUIFormSpecMenu::parseTooltip(parserData* data, const std::string &element)
2196 {
2197         std::vector<std::string> parts = split(element,';');
2198         if (parts.size() < 2) {
2199                 errorstream << "Invalid tooltip element(" << parts.size() << "): '"
2200                                 << element << "'"  << std::endl;
2201                 return;
2202         }
2203
2204         // Get mode and check size
2205         bool rect_mode = parts[0].find(',') != std::string::npos;
2206         size_t base_size = rect_mode ? 3 : 2;
2207         if (parts.size() != base_size && parts.size() != base_size + 2) {
2208                 errorstream << "Invalid tooltip element(" << parts.size() << "): '"
2209                                 << element << "'"  << std::endl;
2210                 return;
2211         }
2212
2213         // Read colors
2214         video::SColor bgcolor = m_default_tooltip_bgcolor;
2215         video::SColor color   = m_default_tooltip_color;
2216         if (parts.size() == base_size + 2 &&
2217                         (!parseColorString(parts[base_size], bgcolor, false) ||
2218                                 !parseColorString(parts[base_size + 1], color, false))) {
2219                 errorstream << "Invalid color in tooltip element(" << parts.size()
2220                                 << "): '" << element << "'"  << std::endl;
2221                 return;
2222         }
2223
2224         // Make tooltip spec
2225         std::string text = unescape_string(parts[rect_mode ? 2 : 1]);
2226         TooltipSpec spec(utf8_to_wide(text), bgcolor, color);
2227
2228         // Add tooltip
2229         if (rect_mode) {
2230                 std::vector<std::string> v_pos  = split(parts[0], ',');
2231                 std::vector<std::string> v_geom = split(parts[1], ',');
2232
2233                 MY_CHECKPOS("tooltip", 0);
2234                 MY_CHECKGEOM("tooltip", 1);
2235
2236                 v2s32 pos;
2237                 v2s32 geom;
2238
2239                 if (data->real_coordinates) {
2240                         pos = getRealCoordinateBasePos(v_pos);
2241                         geom = getRealCoordinateGeometry(v_geom);
2242                 } else {
2243                         pos = getElementBasePos(&v_pos);
2244                         geom.X = stof(v_geom[0]) * spacing.X;
2245                         geom.Y = stof(v_geom[1]) * spacing.Y;
2246                 }
2247
2248                 FieldSpec fieldspec(
2249                         "",
2250                         L"",
2251                         L"",
2252                         258 + m_fields.size()
2253                 );
2254
2255                 core::rect<s32> rect(pos, pos + geom);
2256
2257                 gui::IGUIElement *e = new gui::IGUIElement(EGUIET_ELEMENT, Environment,
2258                                 this, fieldspec.fid, rect);
2259
2260                 // the element the rect tooltip is bound to should not block mouse-clicks
2261                 e->setVisible(false);
2262
2263                 m_fields.push_back(fieldspec);
2264                 m_tooltip_rects.emplace_back(e, spec);
2265
2266         } else {
2267                 m_tooltips[parts[0]] = spec;
2268         }
2269 }
2270
2271 bool GUIFormSpecMenu::parseVersionDirect(const std::string &data)
2272 {
2273         //some prechecks
2274         if (data.empty())
2275                 return false;
2276
2277         std::vector<std::string> parts = split(data,'[');
2278
2279         if (parts.size() < 2) {
2280                 return false;
2281         }
2282
2283         if (trim(parts[0]) != "formspec_version") {
2284                 return false;
2285         }
2286
2287         if (is_number(parts[1])) {
2288                 m_formspec_version = mystoi(parts[1]);
2289                 return true;
2290         }
2291
2292         return false;
2293 }
2294
2295 bool GUIFormSpecMenu::parseSizeDirect(parserData* data, const std::string &element)
2296 {
2297         if (element.empty())
2298                 return false;
2299
2300         std::vector<std::string> parts = split(element,'[');
2301
2302         if (parts.size() < 2)
2303                 return false;
2304
2305         std::string type = trim(parts[0]);
2306         std::string description = trim(parts[1]);
2307
2308         if (type != "size" && type != "invsize")
2309                 return false;
2310
2311         if (type == "invsize")
2312                 log_deprecated("Deprecated formspec element \"invsize\" is used");
2313
2314         parseSize(data, description);
2315
2316         return true;
2317 }
2318
2319 bool GUIFormSpecMenu::parsePositionDirect(parserData *data, const std::string &element)
2320 {
2321         if (element.empty())
2322                 return false;
2323
2324         std::vector<std::string> parts = split(element, '[');
2325
2326         if (parts.size() != 2)
2327                 return false;
2328
2329         std::string type = trim(parts[0]);
2330         std::string description = trim(parts[1]);
2331
2332         if (type != "position")
2333                 return false;
2334
2335         parsePosition(data, description);
2336
2337         return true;
2338 }
2339
2340 void GUIFormSpecMenu::parsePosition(parserData *data, const std::string &element)
2341 {
2342         std::vector<std::string> parts = split(element, ',');
2343
2344         if (parts.size() == 2) {
2345                 data->offset.X = stof(parts[0]);
2346                 data->offset.Y = stof(parts[1]);
2347                 return;
2348         }
2349
2350         errorstream << "Invalid position element (" << parts.size() << "): '" << element << "'" << std::endl;
2351 }
2352
2353 bool GUIFormSpecMenu::parseAnchorDirect(parserData *data, const std::string &element)
2354 {
2355         if (element.empty())
2356                 return false;
2357
2358         std::vector<std::string> parts = split(element, '[');
2359
2360         if (parts.size() != 2)
2361                 return false;
2362
2363         std::string type = trim(parts[0]);
2364         std::string description = trim(parts[1]);
2365
2366         if (type != "anchor")
2367                 return false;
2368
2369         parseAnchor(data, description);
2370
2371         return true;
2372 }
2373
2374 void GUIFormSpecMenu::parseAnchor(parserData *data, const std::string &element)
2375 {
2376         std::vector<std::string> parts = split(element, ',');
2377
2378         if (parts.size() == 2) {
2379                 data->anchor.X = stof(parts[0]);
2380                 data->anchor.Y = stof(parts[1]);
2381                 return;
2382         }
2383
2384         errorstream << "Invalid anchor element (" << parts.size() << "): '" << element
2385                         << "'" << std::endl;
2386 }
2387
2388 bool GUIFormSpecMenu::parseStyle(parserData *data, const std::string &element, bool style_type)
2389 {
2390         std::vector<std::string> parts = split(element, ';');
2391
2392         if (parts.size() < 2) {
2393                 errorstream << "Invalid style element (" << parts.size() << "): '" << element
2394                                         << "'" << std::endl;
2395                 return false;
2396         }
2397
2398         std::string selector = trim(parts[0]);
2399         if (selector.empty()) {
2400                 errorstream << "Invalid style element (Selector required): '" << element
2401                                         << "'" << std::endl;
2402                 return false;
2403         }
2404
2405         StyleSpec spec;
2406
2407         for (size_t i = 1; i < parts.size(); i++) {
2408                 size_t equal_pos = parts[i].find('=');
2409                 if (equal_pos == std::string::npos) {
2410                         errorstream << "Invalid style element (Property missing value): '" << element
2411                                                 << "'" << std::endl;
2412                         return false;
2413                 }
2414
2415                 std::string propname = trim(parts[i].substr(0, equal_pos));
2416                 std::string value    = trim(unescape_string(parts[i].substr(equal_pos + 1)));
2417
2418                 std::transform(propname.begin(), propname.end(), propname.begin(), ::tolower);
2419
2420                 StyleSpec::Property prop = StyleSpec::GetPropertyByName(propname);
2421                 if (prop == StyleSpec::NONE) {
2422                         if (property_warned.find(propname) != property_warned.end()) {
2423                                 warningstream << "Invalid style element (Unknown property " << propname << "): '"
2424                                                 << element
2425                                                 << "'" << std::endl;
2426                                 property_warned.insert(propname);
2427                         }
2428                         return false;
2429                 }
2430
2431                 spec.set(prop, value);
2432         }
2433
2434         if (style_type) {
2435                 theme_by_type[selector] |= spec;
2436         } else {
2437                 theme_by_name[selector] |= spec;
2438         }
2439
2440         return true;
2441 }
2442
2443 void GUIFormSpecMenu::parseElement(parserData* data, const std::string &element)
2444 {
2445         //some prechecks
2446         if (element.empty())
2447                 return;
2448
2449         if (parseVersionDirect(element))
2450                 return;
2451
2452         std::vector<std::string> parts = split(element,'[');
2453
2454         // ugly workaround to keep compatibility
2455         if (parts.size() > 2) {
2456                 if (trim(parts[0]) == "image") {
2457                         for (unsigned int i=2;i< parts.size(); i++) {
2458                                 parts[1] += "[" + parts[i];
2459                         }
2460                 }
2461                 else { return; }
2462         }
2463
2464         if (parts.size() < 2) {
2465                 return;
2466         }
2467
2468         std::string type = trim(parts[0]);
2469         std::string description = trim(parts[1]);
2470
2471         if (type == "container") {
2472                 parseContainer(data, description);
2473                 return;
2474         }
2475
2476         if (type == "container_end") {
2477                 parseContainerEnd(data);
2478                 return;
2479         }
2480
2481         if (type == "list") {
2482                 parseList(data, description);
2483                 return;
2484         }
2485
2486         if (type == "listring") {
2487                 parseListRing(data, description);
2488                 return;
2489         }
2490
2491         if (type == "checkbox") {
2492                 parseCheckbox(data, description);
2493                 return;
2494         }
2495
2496         if (type == "image") {
2497                 parseImage(data, description);
2498                 return;
2499         }
2500
2501         if (type == "item_image") {
2502                 parseItemImage(data, description);
2503                 return;
2504         }
2505
2506         if (type == "button" || type == "button_exit") {
2507                 parseButton(data, description, type);
2508                 return;
2509         }
2510
2511         if (type == "background" || type == "background9") {
2512                 parseBackground(data, description);
2513                 return;
2514         }
2515
2516         if (type == "tableoptions"){
2517                 parseTableOptions(data,description);
2518                 return;
2519         }
2520
2521         if (type == "tablecolumns"){
2522                 parseTableColumns(data,description);
2523                 return;
2524         }
2525
2526         if (type == "table"){
2527                 parseTable(data,description);
2528                 return;
2529         }
2530
2531         if (type == "textlist"){
2532                 parseTextList(data,description);
2533                 return;
2534         }
2535
2536         if (type == "dropdown"){
2537                 parseDropDown(data,description);
2538                 return;
2539         }
2540
2541         if (type == "field_close_on_enter") {
2542                 parseFieldCloseOnEnter(data, description);
2543                 return;
2544         }
2545
2546         if (type == "pwdfield") {
2547                 parsePwdField(data,description);
2548                 return;
2549         }
2550
2551         if ((type == "field") || (type == "textarea")){
2552                 parseField(data,description,type);
2553                 return;
2554         }
2555
2556         if (type == "hypertext") {
2557                 parseHyperText(data,description);
2558                 return;
2559         }
2560
2561         if (type == "label") {
2562                 parseLabel(data,description);
2563                 return;
2564         }
2565
2566         if (type == "vertlabel") {
2567                 parseVertLabel(data,description);
2568                 return;
2569         }
2570
2571         if (type == "item_image_button") {
2572                 parseItemImageButton(data,description);
2573                 return;
2574         }
2575
2576         if ((type == "image_button") || (type == "image_button_exit")) {
2577                 parseImageButton(data,description,type);
2578                 return;
2579         }
2580
2581         if (type == "tabheader") {
2582                 parseTabHeader(data,description);
2583                 return;
2584         }
2585
2586         if (type == "box") {
2587                 parseBox(data,description);
2588                 return;
2589         }
2590
2591         if (type == "bgcolor") {
2592                 parseBackgroundColor(data,description);
2593                 return;
2594         }
2595
2596         if (type == "listcolors") {
2597                 parseListColors(data,description);
2598                 return;
2599         }
2600
2601         if (type == "tooltip") {
2602                 parseTooltip(data,description);
2603                 return;
2604         }
2605
2606         if (type == "scrollbar") {
2607                 parseScrollBar(data, description);
2608                 return;
2609         }
2610
2611         if (type == "real_coordinates") {
2612                 data->real_coordinates = is_yes(description);
2613                 return;
2614         }
2615
2616         if (type == "style") {
2617                 parseStyle(data, description, false);
2618                 return;
2619         }
2620
2621         if (type == "style_type") {
2622                 parseStyle(data, description, true);
2623                 return;
2624         }
2625
2626         if (type == "scrollbaroptions") {
2627                 parseScrollBarOptions(data, description);
2628                 return;
2629         }
2630
2631         // Ignore others
2632         infostream << "Unknown DrawSpec: type=" << type << ", data=\"" << description << "\""
2633                         << std::endl;
2634 }
2635
2636 void GUIFormSpecMenu::regenerateGui(v2u32 screensize)
2637 {
2638         /* useless to regenerate without a screensize */
2639         if ((screensize.X <= 0) || (screensize.Y <= 0)) {
2640                 return;
2641         }
2642
2643         parserData mydata;
2644
2645         //preserve tables
2646         for (auto &m_table : m_tables) {
2647                 std::string tablename = m_table.first.fname;
2648                 GUITable *table = m_table.second;
2649                 mydata.table_dyndata[tablename] = table->getDynamicData();
2650         }
2651
2652         //set focus
2653         if (!m_focused_element.empty())
2654                 mydata.focused_fieldname = m_focused_element;
2655
2656         //preserve focus
2657         gui::IGUIElement *focused_element = Environment->getFocus();
2658         if (focused_element && focused_element->getParent() == this) {
2659                 s32 focused_id = focused_element->getID();
2660                 if (focused_id > 257) {
2661                         for (const GUIFormSpecMenu::FieldSpec &field : m_fields) {
2662                                 if (field.fid == focused_id) {
2663                                         mydata.focused_fieldname = field.fname;
2664                                         break;
2665                                 }
2666                         }
2667                 }
2668         }
2669
2670         // Remove children
2671         removeChildren();
2672
2673         for (auto &table_it : m_tables)
2674                 table_it.second->drop();
2675         for (auto &inventorylist_it : m_inventorylists)
2676                 inventorylist_it.e->drop();
2677         for (auto &checkbox_it : m_checkboxes)
2678                 checkbox_it.second->drop();
2679         for (auto &scrollbar_it : m_scrollbars)
2680                 scrollbar_it.second->drop();
2681         for (auto &background_it : m_backgrounds)
2682                 background_it->drop();
2683         for (auto &tooltip_rect_it : m_tooltip_rects)
2684                 tooltip_rect_it.first->drop();
2685
2686         mydata.size= v2s32(100,100);
2687         mydata.screensize = screensize;
2688         mydata.offset = v2f32(0.5f, 0.5f);
2689         mydata.anchor = v2f32(0.5f, 0.5f);
2690         mydata.simple_field_count = 0;
2691
2692         // Base position of contents of form
2693         mydata.basepos = getBasePos();
2694
2695         /* Convert m_init_draw_spec to m_inventorylists */
2696
2697         m_inventorylists.clear();
2698         m_backgrounds.clear();
2699         m_tables.clear();
2700         m_checkboxes.clear();
2701         m_scrollbars.clear();
2702         m_fields.clear();
2703         m_tooltips.clear();
2704         m_tooltip_rects.clear();
2705         m_inventory_rings.clear();
2706         m_dropdowns.clear();
2707         theme_by_name.clear();
2708         theme_by_type.clear();
2709
2710         m_bgnonfullscreen = true;
2711         m_bgfullscreen = false;
2712
2713         m_formspec_version = 1;
2714
2715         {
2716                 v3f formspec_bgcolor = g_settings->getV3F("formspec_default_bg_color");
2717                 m_bgcolor = video::SColor(
2718                         (u8) clamp_u8(g_settings->getS32("formspec_default_bg_opacity")),
2719                         clamp_u8(myround(formspec_bgcolor.X)),
2720                         clamp_u8(myround(formspec_bgcolor.Y)),
2721                         clamp_u8(myround(formspec_bgcolor.Z))
2722                 );
2723         }
2724
2725         {
2726                 v3f formspec_bgcolor = g_settings->getV3F("formspec_fullscreen_bg_color");
2727                 m_fullscreen_bgcolor = video::SColor(
2728                         (u8) clamp_u8(g_settings->getS32("formspec_fullscreen_bg_opacity")),
2729                         clamp_u8(myround(formspec_bgcolor.X)),
2730                         clamp_u8(myround(formspec_bgcolor.Y)),
2731                         clamp_u8(myround(formspec_bgcolor.Z))
2732                 );
2733         }
2734
2735         m_slotbg_n = video::SColor(255,128,128,128);
2736         m_slotbg_h = video::SColor(255,192,192,192);
2737
2738         m_default_tooltip_bgcolor = video::SColor(255,110,130,60);
2739         m_default_tooltip_color = video::SColor(255,255,255,255);
2740
2741         m_slotbordercolor = video::SColor(200,0,0,0);
2742         m_slotborder = false;
2743
2744         // Add tooltip
2745         {
2746                 assert(!m_tooltip_element);
2747                 // Note: parent != this so that the tooltip isn't clipped by the menu rectangle
2748                 m_tooltip_element = gui::StaticText::add(Environment, L"",
2749                         core::rect<s32>(0, 0, 110, 18));
2750                 m_tooltip_element->enableOverrideColor(true);
2751                 m_tooltip_element->setBackgroundColor(m_default_tooltip_bgcolor);
2752                 m_tooltip_element->setDrawBackground(true);
2753                 m_tooltip_element->setDrawBorder(true);
2754                 m_tooltip_element->setOverrideColor(m_default_tooltip_color);
2755                 m_tooltip_element->setTextAlignment(gui::EGUIA_CENTER, gui::EGUIA_CENTER);
2756                 m_tooltip_element->setWordWrap(false);
2757                 //we're not parent so no autograb for this one!
2758                 m_tooltip_element->grab();
2759         }
2760
2761         std::vector<std::string> elements = split(m_formspec_string,']');
2762         unsigned int i = 0;
2763
2764         /* try to read version from first element only */
2765         if (!elements.empty()) {
2766                 if (parseVersionDirect(elements[0])) {
2767                         i++;
2768                 }
2769         }
2770
2771         /* we need size first in order to calculate image scale */
2772         mydata.explicit_size = false;
2773         for (; i< elements.size(); i++) {
2774                 if (!parseSizeDirect(&mydata, elements[i])) {
2775                         break;
2776                 }
2777         }
2778
2779         /* "position" element is always after "size" element if it used */
2780         for (; i< elements.size(); i++) {
2781                 if (!parsePositionDirect(&mydata, elements[i])) {
2782                         break;
2783                 }
2784         }
2785
2786         /* "anchor" element is always after "position" (or  "size" element) if it used */
2787         for (; i< elements.size(); i++) {
2788                 if (!parseAnchorDirect(&mydata, elements[i])) {
2789                         break;
2790                 }
2791         }
2792
2793         /* "no_prepend" element is always after "position" (or  "size" element) if it used */
2794         bool enable_prepends = true;
2795         for (; i < elements.size(); i++) {
2796                 if (elements[i].empty())
2797                         break;
2798
2799                 std::vector<std::string> parts = split(elements[i], '[');
2800                 if (trim(parts[0]) == "no_prepend")
2801                         enable_prepends = false;
2802                 else
2803                         break;
2804         }
2805
2806         /* Copy of the "real_coordinates" element for after the form size. */
2807         mydata.real_coordinates = m_formspec_version >= 2;
2808         for (; i < elements.size(); i++) {
2809                 std::vector<std::string> parts = split(elements[i], '[');
2810                 std::string name = trim(parts[0]);
2811                 if (name != "real_coordinates" || parts.size() != 2)
2812                         break; // Invalid format
2813
2814                 mydata.real_coordinates = is_yes(trim(parts[1]));
2815         }
2816
2817         if (mydata.explicit_size) {
2818                 // compute scaling for specified form size
2819                 if (m_lock) {
2820                         v2u32 current_screensize = RenderingEngine::get_video_driver()->getScreenSize();
2821                         v2u32 delta = current_screensize - m_lockscreensize;
2822
2823                         if (current_screensize.Y > m_lockscreensize.Y)
2824                                 delta.Y /= 2;
2825                         else
2826                                 delta.Y = 0;
2827
2828                         if (current_screensize.X > m_lockscreensize.X)
2829                                 delta.X /= 2;
2830                         else
2831                                 delta.X = 0;
2832
2833                         offset = v2s32(delta.X,delta.Y);
2834
2835                         mydata.screensize = m_lockscreensize;
2836                 } else {
2837                         offset = v2s32(0,0);
2838                 }
2839
2840                 double gui_scaling = g_settings->getFloat("gui_scaling");
2841                 double screen_dpi = RenderingEngine::getDisplayDensity() * 96;
2842
2843                 double use_imgsize;
2844                 if (m_lock) {
2845                         // In fixed-size mode, inventory image size
2846                         // is 0.53 inch multiplied by the gui_scaling
2847                         // config parameter.  This magic size is chosen
2848                         // to make the main menu (15.5 inventory images
2849                         // wide, including border) just fit into the
2850                         // default window (800 pixels wide) at 96 DPI
2851                         // and default scaling (1.00).
2852                         use_imgsize = 0.5555 * screen_dpi * gui_scaling;
2853                 } else {
2854                         // In variable-size mode, we prefer to make the
2855                         // inventory image size 1/15 of screen height,
2856                         // multiplied by the gui_scaling config parameter.
2857                         // If the preferred size won't fit the whole
2858                         // form on the screen, either horizontally or
2859                         // vertically, then we scale it down to fit.
2860                         // (The magic numbers in the computation of what
2861                         // fits arise from the scaling factors in the
2862                         // following stanza, including the form border,
2863                         // help text space, and 0.1 inventory slot spare.)
2864                         // However, a minimum size is also set, that
2865                         // the image size can't be less than 0.3 inch
2866                         // multiplied by gui_scaling, even if this means
2867                         // the form doesn't fit the screen.
2868 #ifdef __ANDROID__
2869                         // For mobile devices these magic numbers are
2870                         // different and forms should always use the
2871                         // maximum screen space available.
2872                         double prefer_imgsize = mydata.screensize.Y / 10 * gui_scaling;
2873                         double fitx_imgsize = mydata.screensize.X /
2874                                 ((12.0 / 8.0) * (0.5 + mydata.invsize.X));
2875                         double fity_imgsize = mydata.screensize.Y /
2876                                 ((15.0 / 11.0) * (0.85 + mydata.invsize.Y));
2877                         use_imgsize = MYMIN(prefer_imgsize,
2878                                         MYMIN(fitx_imgsize, fity_imgsize));
2879 #else
2880                         double prefer_imgsize = mydata.screensize.Y / 15 * gui_scaling;
2881                         double fitx_imgsize = mydata.screensize.X /
2882                                 ((5.0 / 4.0) * (0.5 + mydata.invsize.X));
2883                         double fity_imgsize = mydata.screensize.Y /
2884                                 ((15.0 / 13.0) * (0.85 * mydata.invsize.Y));
2885                         double screen_dpi = RenderingEngine::getDisplayDensity() * 96;
2886                         double min_imgsize = 0.3 * screen_dpi * gui_scaling;
2887                         use_imgsize = MYMAX(min_imgsize, MYMIN(prefer_imgsize,
2888                                 MYMIN(fitx_imgsize, fity_imgsize)));
2889 #endif
2890                 }
2891
2892                 // Everything else is scaled in proportion to the
2893                 // inventory image size.  The inventory slot spacing
2894                 // is 5/4 image size horizontally and 15/13 image size
2895                 // vertically.  The padding around the form (incorporating
2896                 // the border of the outer inventory slots) is 3/8
2897                 // image size.  Font height (baseline to baseline)
2898                 // is 2/5 vertical inventory slot spacing, and button
2899                 // half-height is 7/8 of font height.
2900                 imgsize = v2s32(use_imgsize, use_imgsize);
2901                 spacing = v2f32(use_imgsize*5.0/4, use_imgsize*15.0/13);
2902                 padding = v2s32(use_imgsize*3.0/8, use_imgsize*3.0/8);
2903                 m_btn_height = use_imgsize*15.0/13 * 0.35;
2904
2905                 m_font = g_fontengine->getFont();
2906
2907                 if (mydata.real_coordinates) {
2908                         mydata.size = v2s32(
2909                                 mydata.invsize.X*imgsize.X,
2910                                 mydata.invsize.Y*imgsize.Y
2911                         );
2912                 } else {
2913                         mydata.size = v2s32(
2914                                 padding.X*2+spacing.X*(mydata.invsize.X-1.0)+imgsize.X,
2915                                 padding.Y*2+spacing.Y*(mydata.invsize.Y-1.0)+imgsize.Y + m_btn_height*2.0/3.0
2916                         );
2917                 }
2918
2919                 DesiredRect = mydata.rect = core::rect<s32>(
2920                                 (s32)((f32)mydata.screensize.X * mydata.offset.X) - (s32)(mydata.anchor.X * (f32)mydata.size.X) + offset.X,
2921                                 (s32)((f32)mydata.screensize.Y * mydata.offset.Y) - (s32)(mydata.anchor.Y * (f32)mydata.size.Y) + offset.Y,
2922                                 (s32)((f32)mydata.screensize.X * mydata.offset.X) + (s32)((1.0 - mydata.anchor.X) * (f32)mydata.size.X) + offset.X,
2923                                 (s32)((f32)mydata.screensize.Y * mydata.offset.Y) + (s32)((1.0 - mydata.anchor.Y) * (f32)mydata.size.Y) + offset.Y
2924                 );
2925         } else {
2926                 // Non-size[] form must consist only of text fields and
2927                 // implicit "Proceed" button.  Use default font, and
2928                 // temporary form size which will be recalculated below.
2929                 m_font = g_fontengine->getFont();
2930                 m_btn_height = font_line_height(m_font) * 0.875;
2931                 DesiredRect = core::rect<s32>(
2932                         (s32)((f32)mydata.screensize.X * mydata.offset.X) - (s32)(mydata.anchor.X * 580.0),
2933                         (s32)((f32)mydata.screensize.Y * mydata.offset.Y) - (s32)(mydata.anchor.Y * 300.0),
2934                         (s32)((f32)mydata.screensize.X * mydata.offset.X) + (s32)((1.0 - mydata.anchor.X) * 580.0),
2935                         (s32)((f32)mydata.screensize.Y * mydata.offset.Y) + (s32)((1.0 - mydata.anchor.Y) * 300.0)
2936                 );
2937         }
2938         recalculateAbsolutePosition(false);
2939         mydata.basepos = getBasePos();
2940         m_tooltip_element->setOverrideFont(m_font);
2941
2942         gui::IGUISkin *skin = Environment->getSkin();
2943         sanity_check(skin);
2944         gui::IGUIFont *old_font = skin->getFont();
2945         skin->setFont(m_font);
2946
2947         pos_offset = v2f32();
2948
2949         // used for formspec versions < 3
2950         core::list<IGUIElement *>::Iterator legacy_sort_start = Children.getLast();
2951
2952         if (enable_prepends) {
2953                 // Backup the coordinates so that prepends can use the coordinates of choice.
2954                 bool rc_backup = mydata.real_coordinates;
2955                 u16 version_backup = m_formspec_version;
2956                 mydata.real_coordinates = false; // Old coordinates by default.
2957
2958                 std::vector<std::string> prepend_elements = split(m_formspec_prepend, ']');
2959                 for (const auto &element : prepend_elements)
2960                         parseElement(&mydata, element);
2961
2962                 // legacy sorting for formspec versions < 3
2963                 if (m_formspec_version >= 3)
2964                         // prepends do not need to be reordered
2965                         legacy_sort_start = Children.getLast();
2966                 else if (version_backup >= 3)
2967                         // only prepends elements have to be reordered
2968                         legacySortElements(legacy_sort_start);
2969
2970                 m_formspec_version = version_backup;
2971                 mydata.real_coordinates = rc_backup; // Restore coordinates
2972         }
2973
2974         for (; i< elements.size(); i++) {
2975                 parseElement(&mydata, elements[i]);
2976         }
2977
2978         if (!container_stack.empty()) {
2979                 errorstream << "Invalid formspec string: container was never closed!"
2980                         << std::endl;
2981         }
2982
2983         // If there are fields without explicit size[], add a "Proceed"
2984         // button and adjust size to fit all the fields.
2985         if (mydata.simple_field_count > 0 && !mydata.explicit_size) {
2986                 mydata.rect = core::rect<s32>(
2987                                 mydata.screensize.X / 2 - 580 / 2,
2988                                 mydata.screensize.Y / 2 - 300 / 2,
2989                                 mydata.screensize.X / 2 + 580 / 2,
2990                                 mydata.screensize.Y / 2 + 240 / 2 + mydata.simple_field_count * 60
2991                 );
2992
2993                 DesiredRect = mydata.rect;
2994                 recalculateAbsolutePosition(false);
2995                 mydata.basepos = getBasePos();
2996
2997                 {
2998                         v2s32 pos = mydata.basepos;
2999                         pos.Y = (mydata.simple_field_count + 2) * 60;
3000
3001                         v2s32 size = DesiredRect.getSize();
3002                         mydata.rect = core::rect<s32>(
3003                                         size.X / 2 - 70,       pos.Y,
3004                                         size.X / 2 - 70 + 140, pos.Y + m_btn_height * 2
3005                         );
3006                         const wchar_t *text = wgettext("Proceed");
3007                         GUIButton::addButton(Environment, mydata.rect, this, 257, text);
3008                         delete[] text;
3009                 }
3010         }
3011
3012         //set initial focus if parser didn't set it
3013         focused_element = Environment->getFocus();
3014         if (!focused_element
3015                         || !isMyChild(focused_element)
3016                         || focused_element->getType() == gui::EGUIET_TAB_CONTROL)
3017                 setInitialFocus();
3018
3019         skin->setFont(old_font);
3020
3021         // legacy sorting
3022         if (m_formspec_version < 3)
3023                 legacySortElements(legacy_sort_start);
3024 }
3025
3026 void GUIFormSpecMenu::legacySortElements(core::list<IGUIElement *>::Iterator from)
3027 {
3028         /*
3029                 Draw order for formspec_version <= 2:
3030                 -3  bgcolor
3031                 -2  background
3032                 -1  box
3033                 0   All other elements
3034                 1   image
3035                 2   item_image, item_image_button
3036                 3   list
3037                 4   label
3038         */
3039
3040         if (from == Children.end())
3041                 from = Children.begin();
3042         else
3043                 from++;
3044
3045         core::list<IGUIElement *>::Iterator to = Children.end();
3046         // 1: Copy into a sortable container
3047         std::vector<IGUIElement *> elements;
3048         for (auto it = from; it != to; ++it)
3049                 elements.emplace_back(*it);
3050
3051         // 2: Sort the container
3052         std::stable_sort(elements.begin(), elements.end(),
3053                         [this] (const IGUIElement *a, const IGUIElement *b) -> bool {
3054                 const FieldSpec *spec_a = getSpecByID(a->getID());
3055                 const FieldSpec *spec_b = getSpecByID(b->getID());
3056                 return spec_a && spec_b &&
3057                         spec_a->priority < spec_b->priority;
3058         });
3059
3060         // 3: Re-assign the pointers
3061         for (auto e : elements) {
3062                 *from = e;
3063                 from++;
3064         }
3065 }
3066
3067 #ifdef __ANDROID__
3068 bool GUIFormSpecMenu::getAndroidUIInput()
3069 {
3070         if (!hasAndroidUIInput())
3071                 return false;
3072
3073         std::string fieldname = m_jni_field_name;
3074         m_jni_field_name.clear();
3075
3076         for(std::vector<FieldSpec>::iterator iter =  m_fields.begin();
3077                         iter != m_fields.end(); ++iter) {
3078
3079                 if (iter->fname != fieldname) {
3080                         continue;
3081                 }
3082                 IGUIElement *tochange = getElementFromId(iter->fid, true);
3083
3084                 if (tochange == 0) {
3085                         return false;
3086                 }
3087
3088                 if (tochange->getType() != irr::gui::EGUIET_EDIT_BOX) {
3089                         return false;
3090                 }
3091
3092                 std::string text = porting::getInputDialogValue();
3093
3094                 ((gui::IGUIEditBox *)tochange)->setText(utf8_to_wide(text).c_str());
3095         }
3096         return false;
3097 }
3098 #endif
3099
3100 GUIFormSpecMenu::ItemSpec GUIFormSpecMenu::getItemAtPos(v2s32 p) const
3101 {
3102         core::rect<s32> imgrect(0, 0, imgsize.X, imgsize.Y);
3103
3104         for (const GUIFormSpecMenu::ListDrawSpec &s : m_inventorylists) {
3105                 core::rect<s32> clipping_rect = s.e->getAbsoluteClippingRect();
3106                 v2s32 base_pos = s.e->getAbsolutePosition().UpperLeftCorner;
3107                 for(s32 i=0; i<s.geom.X*s.geom.Y; i++) {
3108                         s32 item_i = i + s.start_item_i;
3109
3110                         s32 x;
3111                         s32 y;
3112                         if (s.real_coordinates) {
3113                                 x = (i%s.geom.X) * (imgsize.X * 1.25);
3114                                 y = (i/s.geom.X) * (imgsize.Y * 1.25);
3115                         } else {
3116                                 x = (i%s.geom.X) * spacing.X;
3117                                 y = (i/s.geom.X) * spacing.Y;
3118                         }
3119                         v2s32 p0(x,y);
3120                         core::rect<s32> rect = imgrect + base_pos + p0;
3121                         rect.clipAgainst(clipping_rect);
3122                         if (rect.getArea() > 0 && rect.isPointInside(p))
3123                                 return ItemSpec(s.inventoryloc, s.listname, item_i);
3124                 }
3125         }
3126
3127         return ItemSpec(InventoryLocation(), "", -1);
3128 }
3129
3130 void GUIFormSpecMenu::drawList(const ListDrawSpec &s, int layer,
3131                 bool &item_hovered)
3132 {
3133         video::IVideoDriver* driver = Environment->getVideoDriver();
3134
3135         Inventory *inv = m_invmgr->getInventory(s.inventoryloc);
3136         if (!inv) {
3137                 warningstream<<"GUIFormSpecMenu::drawList(): "
3138                                 << "The inventory location "
3139                                 << "\"" << s.inventoryloc.dump() << "\" doesn't exist anymore"
3140                                 << std::endl;
3141                 return;
3142         }
3143         InventoryList *ilist = inv->getList(s.listname);
3144         if (!ilist) {
3145                 warningstream << "GUIFormSpecMenu::drawList(): "
3146                                 << "The inventory list \"" << s.listname << "\" @ \""
3147                                 << s.inventoryloc.dump() << "\" doesn't exist anymore"
3148                                 << std::endl;
3149                 return;
3150         }
3151
3152         core::rect<s32> imgrect(0, 0, imgsize.X, imgsize.Y);
3153         core::rect<s32> clipping_rect = s.e->getAbsoluteClippingRect();
3154         v2s32 base_pos = s.e->getAbsolutePosition().UpperLeftCorner;
3155
3156         for (s32 i = 0; i < s.geom.X * s.geom.Y; i++) {
3157                 s32 item_i = i + s.start_item_i;
3158                 if (item_i >= (s32)ilist->getSize())
3159                         break;
3160
3161                 s32 x;
3162                 s32 y;
3163                 if (s.real_coordinates) {
3164                         x = (i%s.geom.X) * (imgsize.X * 1.25);
3165                         y = (i/s.geom.X) * (imgsize.Y * 1.25);
3166                 } else {
3167                         x = (i%s.geom.X) * spacing.X;
3168                         y = (i/s.geom.X) * spacing.Y;
3169                 }
3170                 v2s32 p(x,y);
3171                 core::rect<s32> rect = imgrect + base_pos + p;
3172                 ItemStack item = ilist->getItem(item_i);
3173
3174                 bool selected = m_selected_item
3175                         && m_invmgr->getInventory(m_selected_item->inventoryloc) == inv
3176                         && m_selected_item->listname == s.listname
3177                         && m_selected_item->i == item_i;
3178                 core::rect<s32> clipped_rect(rect);
3179                 clipped_rect.clipAgainst(clipping_rect);
3180                 bool hovering = clipped_rect.getArea() > 0 &&
3181                                 clipped_rect.isPointInside(m_pointer);
3182                 ItemRotationKind rotation_kind = selected ? IT_ROT_SELECTED :
3183                         (hovering ? IT_ROT_HOVERED : IT_ROT_NONE);
3184
3185                 if (layer == 0) {
3186                         if (hovering) {
3187                                 item_hovered = true;
3188                                 driver->draw2DRectangle(m_slotbg_h, rect, &clipping_rect);
3189                         } else {
3190                                 driver->draw2DRectangle(m_slotbg_n, rect, &clipping_rect);
3191                         }
3192                 }
3193
3194                 //Draw inv slot borders
3195                 if (m_slotborder) {
3196                         s32 x1 = rect.UpperLeftCorner.X;
3197                         s32 y1 = rect.UpperLeftCorner.Y;
3198                         s32 x2 = rect.LowerRightCorner.X;
3199                         s32 y2 = rect.LowerRightCorner.Y;
3200                         s32 border = 1;
3201                         driver->draw2DRectangle(m_slotbordercolor,
3202                                 core::rect<s32>(v2s32(x1 - border, y1 - border),
3203                                                                 v2s32(x2 + border, y1)), &clipping_rect);
3204                         driver->draw2DRectangle(m_slotbordercolor,
3205                                 core::rect<s32>(v2s32(x1 - border, y2),
3206                                                                 v2s32(x2 + border, y2 + border)), &clipping_rect);
3207                         driver->draw2DRectangle(m_slotbordercolor,
3208                                 core::rect<s32>(v2s32(x1 - border, y1),
3209                                                                 v2s32(x1, y2)), &clipping_rect);
3210                         driver->draw2DRectangle(m_slotbordercolor,
3211                                 core::rect<s32>(v2s32(x2, y1),
3212                                                                 v2s32(x2 + border, y2)), &clipping_rect);
3213                 }
3214
3215                 if (layer == 1) {
3216                         if (selected)
3217                                 item.takeItem(m_selected_amount);
3218
3219                         if (!item.empty()) {
3220                                 // Draw item stack
3221                                 drawItemStack(driver, m_font, item,
3222                                         rect, &clipping_rect, m_client, rotation_kind);
3223                                 // Draw tooltip
3224                                 if (hovering && !m_selected_item) {
3225                                         std::string tooltip = item.getDescription(m_client->idef());
3226                                         if (m_tooltip_append_itemname)
3227                                                 tooltip += "\n[" + item.name + "]";
3228                                         showTooltip(utf8_to_wide(tooltip), m_default_tooltip_color,
3229                                                         m_default_tooltip_bgcolor);
3230                                 }
3231                         }
3232                 }
3233         }
3234 }
3235
3236 void GUIFormSpecMenu::drawSelectedItem()
3237 {
3238         video::IVideoDriver* driver = Environment->getVideoDriver();
3239
3240         if (!m_selected_item) {
3241                 // reset rotation time
3242                 drawItemStack(driver, m_font, ItemStack(),
3243                                 core::rect<s32>(v2s32(0, 0), v2s32(0, 0)), NULL,
3244                                 m_client, IT_ROT_DRAGGED);
3245                 return;
3246         }
3247
3248         Inventory *inv = m_invmgr->getInventory(m_selected_item->inventoryloc);
3249         sanity_check(inv);
3250         InventoryList *list = inv->getList(m_selected_item->listname);
3251         sanity_check(list);
3252         ItemStack stack = list->getItem(m_selected_item->i);
3253         stack.count = m_selected_amount;
3254
3255         core::rect<s32> imgrect(0,0,imgsize.X,imgsize.Y);
3256         core::rect<s32> rect = imgrect + (m_pointer - imgrect.getCenter());
3257         rect.constrainTo(driver->getViewPort());
3258         drawItemStack(driver, m_font, stack, rect, NULL, m_client, IT_ROT_DRAGGED);
3259 }
3260
3261 void GUIFormSpecMenu::drawMenu()
3262 {
3263         if (m_form_src) {
3264                 const std::string &newform = m_form_src->getForm();
3265                 if (newform != m_formspec_string) {
3266                         m_formspec_string = newform;
3267                         regenerateGui(m_screensize_old);
3268                 }
3269         }
3270
3271         gui::IGUISkin* skin = Environment->getSkin();
3272         sanity_check(skin != NULL);
3273         gui::IGUIFont *old_font = skin->getFont();
3274         skin->setFont(m_font);
3275
3276         updateSelectedItem();
3277
3278         video::IVideoDriver* driver = Environment->getVideoDriver();
3279
3280         /*
3281                 Draw background color
3282         */
3283         v2u32 screenSize = driver->getScreenSize();
3284         core::rect<s32> allbg(0, 0, screenSize.X, screenSize.Y);
3285
3286         if (m_bgfullscreen)
3287                 driver->draw2DRectangle(m_fullscreen_bgcolor, allbg, &allbg);
3288         if (m_bgnonfullscreen)
3289                 driver->draw2DRectangle(m_bgcolor, AbsoluteRect, &AbsoluteClippingRect);
3290
3291         /*
3292                 Draw rect_mode tooltip
3293         */
3294         m_tooltip_element->setVisible(false);
3295
3296         for (const auto &pair : m_tooltip_rects) {
3297                 const core::rect<s32> &rect = pair.first->getAbsoluteClippingRect();
3298                 if (rect.getArea() > 0 && rect.isPointInside(m_pointer)) {
3299                         const std::wstring &text = pair.second.tooltip;
3300                         if (!text.empty()) {
3301                                 showTooltip(text, pair.second.color, pair.second.bgcolor);
3302                                 break;
3303                         }
3304                 }
3305         }
3306
3307         /*
3308                 Draw backgrounds
3309         */
3310         for (gui::IGUIElement *e : m_backgrounds) {
3311                 e->setVisible(true);
3312                 e->draw();
3313                 e->setVisible(false);
3314         }
3315
3316         /*
3317                 Call base class
3318         */
3319         gui::IGUIElement::draw();
3320
3321         /*
3322                 Draw items
3323                 Layer 0: Item slot rectangles
3324                 Layer 1: Item images; prepare tooltip
3325         */
3326         bool item_hovered = false;
3327         for (int layer = 0; layer < 2; layer++) {
3328                 for (const GUIFormSpecMenu::ListDrawSpec &spec : m_inventorylists) {
3329                         drawList(spec, layer, item_hovered);
3330                 }
3331         }
3332         if (!item_hovered) {
3333                 // reset rotation time
3334                 drawItemStack(driver, m_font, ItemStack(),
3335                         core::rect<s32>(v2s32(0, 0), v2s32(0, 0)),
3336                         NULL, m_client, IT_ROT_HOVERED);
3337         }
3338
3339 /* TODO find way to show tooltips on touchscreen */
3340 #ifndef HAVE_TOUCHSCREENGUI
3341         m_pointer = RenderingEngine::get_raw_device()->getCursorControl()->getPosition();
3342 #endif
3343
3344         /*
3345                 Draw fields/buttons tooltips and update the mouse cursor
3346         */
3347         gui::IGUIElement *hovered =
3348                         Environment->getRootGUIElement()->getElementFromPoint(m_pointer);
3349
3350 #ifndef HAVE_TOUCHSCREENGUI
3351         gui::ICursorControl *cursor_control = RenderingEngine::get_raw_device()->
3352                         getCursorControl();
3353         gui::ECURSOR_ICON current_cursor_icon = cursor_control->getActiveIcon();
3354 #endif
3355         bool hovered_element_found = false;
3356
3357         if (hovered != NULL) {
3358                 s32 id = hovered->getID();
3359
3360                 u64 delta = 0;
3361                 if (id == -1) {
3362                         m_old_tooltip_id = id;
3363                 } else {
3364                         if (id == m_old_tooltip_id) {
3365                                 delta = porting::getDeltaMs(m_hovered_time, porting::getTimeMs());
3366                         } else {
3367                                 m_hovered_time = porting::getTimeMs();
3368                                 m_old_tooltip_id = id;
3369                         }
3370                 }
3371
3372                 // Find and update the current tooltip and cursor icon
3373                 if (id != -1) {
3374                         for (const FieldSpec &field : m_fields) {
3375
3376                                 if (field.fid != id)
3377                                         continue;
3378
3379                                 if (delta >= m_tooltip_show_delay) {
3380                                         const std::wstring &text = m_tooltips[field.fname].tooltip;
3381                                         if (!text.empty())
3382                                                 showTooltip(text, m_tooltips[field.fname].color,
3383                                                         m_tooltips[field.fname].bgcolor);
3384                                 }
3385
3386 #ifndef HAVE_TOUCHSCREENGUI
3387                                 if (current_cursor_icon != field.fcursor_icon)
3388                                         cursor_control->setActiveIcon(field.fcursor_icon);
3389 #endif
3390
3391                                 hovered_element_found = true;
3392
3393                                 break;
3394                         }
3395                 }
3396         }
3397
3398         if (!hovered_element_found) {
3399                 // no element is hovered
3400 #ifndef HAVE_TOUCHSCREENGUI
3401                 if (current_cursor_icon != ECI_NORMAL)
3402                         cursor_control->setActiveIcon(ECI_NORMAL);
3403 #endif
3404         }
3405
3406         m_tooltip_element->draw();
3407
3408         /*
3409                 Draw dragged item stack
3410         */
3411         drawSelectedItem();
3412
3413         skin->setFont(old_font);
3414 }
3415
3416
3417 void GUIFormSpecMenu::showTooltip(const std::wstring &text,
3418         const irr::video::SColor &color, const irr::video::SColor &bgcolor)
3419 {
3420         EnrichedString ntext(text);
3421         ntext.setDefaultColor(color);
3422         ntext.setBackground(bgcolor);
3423
3424         setStaticText(m_tooltip_element, ntext);
3425
3426         // Tooltip size and offset
3427         s32 tooltip_width = m_tooltip_element->getTextWidth() + m_btn_height;
3428         s32 tooltip_height = m_tooltip_element->getTextHeight() + 5;
3429
3430         v2u32 screenSize = Environment->getVideoDriver()->getScreenSize();
3431         int tooltip_offset_x = m_btn_height;
3432         int tooltip_offset_y = m_btn_height;
3433 #ifdef __ANDROID__
3434         tooltip_offset_x *= 3;
3435         tooltip_offset_y  = 0;
3436         if (m_pointer.X > (s32)screenSize.X / 2)
3437                 tooltip_offset_x = -(tooltip_offset_x + tooltip_width);
3438 #endif
3439
3440         // Calculate and set the tooltip position
3441         s32 tooltip_x = m_pointer.X + tooltip_offset_x;
3442         s32 tooltip_y = m_pointer.Y + tooltip_offset_y;
3443         if (tooltip_x + tooltip_width > (s32)screenSize.X)
3444                 tooltip_x = (s32)screenSize.X - tooltip_width  - m_btn_height;
3445         if (tooltip_y + tooltip_height > (s32)screenSize.Y)
3446                 tooltip_y = (s32)screenSize.Y - tooltip_height - m_btn_height;
3447
3448         m_tooltip_element->setRelativePosition(
3449                 core::rect<s32>(
3450                         core::position2d<s32>(tooltip_x, tooltip_y),
3451                         core::dimension2d<s32>(tooltip_width, tooltip_height)
3452                 )
3453         );
3454
3455         // Display the tooltip
3456         m_tooltip_element->setVisible(true);
3457         bringToFront(m_tooltip_element);
3458 }
3459
3460 void GUIFormSpecMenu::updateSelectedItem()
3461 {
3462         verifySelectedItem();
3463
3464         // If craftresult is nonempty and nothing else is selected, select it now.
3465         if (!m_selected_item) {
3466                 for (const GUIFormSpecMenu::ListDrawSpec &s : m_inventorylists) {
3467                         if (s.listname != "craftpreview")
3468                                 continue;
3469
3470                         Inventory *inv = m_invmgr->getInventory(s.inventoryloc);
3471                         if (!inv)
3472                                 continue;
3473
3474                         InventoryList *list = inv->getList("craftresult");
3475
3476                         if (!list || list->getSize() == 0)
3477                                 continue;
3478
3479                         const ItemStack &item = list->getItem(0);
3480                         if (item.empty())
3481                                 continue;
3482
3483                         // Grab selected item from the crafting result list
3484                         m_selected_item = new ItemSpec;
3485                         m_selected_item->inventoryloc = s.inventoryloc;
3486                         m_selected_item->listname = "craftresult";
3487                         m_selected_item->i = 0;
3488                         m_selected_amount = item.count;
3489                         m_selected_dragging = false;
3490                         break;
3491                 }
3492         }
3493
3494         // If craftresult is selected, keep the whole stack selected
3495         if (m_selected_item && m_selected_item->listname == "craftresult")
3496                 m_selected_amount = verifySelectedItem().count;
3497 }
3498
3499 ItemStack GUIFormSpecMenu::verifySelectedItem()
3500 {
3501         // If the selected stack has become empty for some reason, deselect it.
3502         // If the selected stack has become inaccessible, deselect it.
3503         // If the selected stack has become smaller, adjust m_selected_amount.
3504         // Return the selected stack.
3505
3506         if(m_selected_item)
3507         {
3508                 if(m_selected_item->isValid())
3509                 {
3510                         Inventory *inv = m_invmgr->getInventory(m_selected_item->inventoryloc);
3511                         if(inv)
3512                         {
3513                                 InventoryList *list = inv->getList(m_selected_item->listname);
3514                                 if(list && (u32) m_selected_item->i < list->getSize())
3515                                 {
3516                                         ItemStack stack = list->getItem(m_selected_item->i);
3517                                         if (!m_selected_swap.empty()) {
3518                                                 if (m_selected_swap.name == stack.name &&
3519                                                                 m_selected_swap.count == stack.count)
3520                                                         m_selected_swap.clear();
3521                                         } else {
3522                                                 m_selected_amount = std::min(m_selected_amount, stack.count);
3523                                         }
3524
3525                                         if (!stack.empty())
3526                                                 return stack;
3527                                 }
3528                         }
3529                 }
3530
3531                 // selection was not valid
3532                 delete m_selected_item;
3533                 m_selected_item = NULL;
3534                 m_selected_amount = 0;
3535                 m_selected_dragging = false;
3536         }
3537         return ItemStack();
3538 }
3539
3540 void GUIFormSpecMenu::acceptInput(FormspecQuitMode quitmode=quit_mode_no)
3541 {
3542         if(m_text_dst)
3543         {
3544                 StringMap fields;
3545
3546                 if (quitmode == quit_mode_accept) {
3547                         fields["quit"] = "true";
3548                 }
3549
3550                 if (quitmode == quit_mode_cancel) {
3551                         fields["quit"] = "true";
3552                         m_text_dst->gotText(fields);
3553                         return;
3554                 }
3555
3556                 if (current_keys_pending.key_down) {
3557                         fields["key_down"] = "true";
3558                         current_keys_pending.key_down = false;
3559                 }
3560
3561                 if (current_keys_pending.key_up) {
3562                         fields["key_up"] = "true";
3563                         current_keys_pending.key_up = false;
3564                 }
3565
3566                 if (current_keys_pending.key_enter) {
3567                         fields["key_enter"] = "true";
3568                         current_keys_pending.key_enter = false;
3569                 }
3570
3571                 if (!current_field_enter_pending.empty()) {
3572                         fields["key_enter_field"] = current_field_enter_pending;
3573                         current_field_enter_pending = "";
3574                 }
3575
3576                 if (current_keys_pending.key_escape) {
3577                         fields["key_escape"] = "true";
3578                         current_keys_pending.key_escape = false;
3579                 }
3580
3581                 for (const GUIFormSpecMenu::FieldSpec &s : m_fields) {
3582                         if(s.send) {
3583                                 std::string name = s.fname;
3584                                 if (s.ftype == f_Button) {
3585                                         fields[name] = wide_to_utf8(s.flabel);
3586                                 } else if (s.ftype == f_Table) {
3587                                         GUITable *table = getTable(s.fname);
3588                                         if (table) {
3589                                                 fields[name] = table->checkEvent();
3590                                         }
3591                                 }
3592                                 else if(s.ftype == f_DropDown) {
3593                                         // no dynamic cast possible due to some distributions shipped
3594                                         // without rtti support in irrlicht
3595                                         IGUIElement *element = getElementFromId(s.fid, true);
3596                                         gui::IGUIComboBox *e = NULL;
3597                                         if ((element) && (element->getType() == gui::EGUIET_COMBO_BOX)) {
3598                                                 e = static_cast<gui::IGUIComboBox*>(element);
3599                                         } else {
3600                                                 warningstream << "GUIFormSpecMenu::acceptInput: dropdown "
3601                                                                 << "field without dropdown element" << std::endl;
3602                                                 continue;
3603                                         }
3604                                         s32 selected = e->getSelected();
3605                                         if (selected >= 0) {
3606                                                 std::vector<std::string> *dropdown_values =
3607                                                         getDropDownValues(s.fname);
3608                                                 if (dropdown_values && selected < (s32)dropdown_values->size()) {
3609                                                         fields[name] = (*dropdown_values)[selected];
3610                                                 }
3611                                         }
3612                                 }
3613                                 else if (s.ftype == f_TabHeader) {
3614                                         // no dynamic cast possible due to some distributions shipped
3615                                         // without rttzi support in irrlicht
3616                                         IGUIElement *element = getElementFromId(s.fid, true);
3617                                         gui::IGUITabControl *e = nullptr;
3618                                         if ((element) && (element->getType() == gui::EGUIET_TAB_CONTROL)) {
3619                                                 e = static_cast<gui::IGUITabControl *>(element);
3620                                         }
3621
3622                                         if (e != 0) {
3623                                                 std::stringstream ss;
3624                                                 ss << (e->getActiveTab() +1);
3625                                                 fields[name] = ss.str();
3626                                         }
3627                                 }
3628                                 else if (s.ftype == f_CheckBox) {
3629                                         // no dynamic cast possible due to some distributions shipped
3630                                         // without rtti support in irrlicht
3631                                         IGUIElement *element = getElementFromId(s.fid, true);
3632                                         gui::IGUICheckBox *e = nullptr;
3633                                         if ((element) && (element->getType() == gui::EGUIET_CHECK_BOX)) {
3634                                                 e = static_cast<gui::IGUICheckBox*>(element);
3635                                         }
3636
3637                                         if (e != 0) {
3638                                                 if (e->isChecked())
3639                                                         fields[name] = "true";
3640                                                 else
3641                                                         fields[name] = "false";
3642                                         }
3643                                 }
3644                                 else if (s.ftype == f_ScrollBar) {
3645                                         // no dynamic cast possible due to some distributions shipped
3646                                         // without rtti support in irrlicht
3647                                         IGUIElement *element = getElementFromId(s.fid, true);
3648                                         GUIScrollBar *e = nullptr;
3649                                         if (element && element->getType() == gui::EGUIET_ELEMENT)
3650                                                 e = static_cast<GUIScrollBar *>(element);
3651
3652                                         if (e) {
3653                                                 std::stringstream os;
3654                                                 os << e->getPos();
3655                                                 if (s.fdefault == L"Changed")
3656                                                         fields[name] = "CHG:" + os.str();
3657                                                 else
3658                                                         fields[name] = "VAL:" + os.str();
3659                                         }
3660                                 }
3661                                 else {
3662                                         IGUIElement *e = getElementFromId(s.fid, true);
3663                                         if (e)
3664                                                 fields[name] = wide_to_utf8(e->getText());
3665                                 }
3666                         }
3667                 }
3668
3669                 m_text_dst->gotText(fields);
3670         }
3671 }
3672
3673 static bool isChild(gui::IGUIElement * tocheck, gui::IGUIElement * parent)
3674 {
3675         while(tocheck != NULL) {
3676                 if (tocheck == parent) {
3677                         return true;
3678                 }
3679                 tocheck = tocheck->getParent();
3680         }
3681         return false;
3682 }
3683
3684 bool GUIFormSpecMenu::preprocessEvent(const SEvent& event)
3685 {
3686         // The IGUITabControl renders visually using the skin's selected
3687         // font, which we override for the duration of form drawing,
3688         // but computes tab hotspots based on how it would have rendered
3689         // using the font that is selected at the time of button release.
3690         // To make these two consistent, temporarily override the skin's
3691         // font while the IGUITabControl is processing the event.
3692         if (event.EventType == EET_MOUSE_INPUT_EVENT &&
3693                         event.MouseInput.Event == EMIE_LMOUSE_LEFT_UP) {
3694                 s32 x = event.MouseInput.X;
3695                 s32 y = event.MouseInput.Y;
3696                 gui::IGUIElement *hovered =
3697                         Environment->getRootGUIElement()->getElementFromPoint(
3698                                 core::position2d<s32>(x, y));
3699                 if (hovered && isMyChild(hovered) &&
3700                                 hovered->getType() == gui::EGUIET_TAB_CONTROL) {
3701                         gui::IGUISkin* skin = Environment->getSkin();
3702                         sanity_check(skin != NULL);
3703                         gui::IGUIFont *old_font = skin->getFont();
3704                         skin->setFont(m_font);
3705                         bool retval = hovered->OnEvent(event);
3706                         skin->setFont(old_font);
3707                         return retval;
3708                 }
3709         }
3710
3711         // Fix Esc/Return key being eaten by checkboxen and tables
3712         if(event.EventType==EET_KEY_INPUT_EVENT) {
3713                 KeyPress kp(event.KeyInput);
3714                 if (kp == EscapeKey || kp == CancelKey
3715                                 || kp == getKeySetting("keymap_inventory")
3716                                 || event.KeyInput.Key==KEY_RETURN) {
3717                         gui::IGUIElement *focused = Environment->getFocus();
3718                         if (focused && isMyChild(focused) &&
3719                                         (focused->getType() == gui::EGUIET_LIST_BOX ||
3720                                         focused->getType() == gui::EGUIET_CHECK_BOX) &&
3721                                         (focused->getParent()->getType() != gui::EGUIET_COMBO_BOX ||
3722                                         event.KeyInput.Key != KEY_RETURN)) {
3723                                 OnEvent(event);
3724                                 return true;
3725                         }
3726                 }
3727         }
3728         // Mouse wheel and move events: send to hovered element instead of focused
3729         if (event.EventType == EET_MOUSE_INPUT_EVENT &&
3730                         (event.MouseInput.Event == EMIE_MOUSE_WHEEL ||
3731                          event.MouseInput.Event == EMIE_MOUSE_MOVED)) {
3732                 s32 x = event.MouseInput.X;
3733                 s32 y = event.MouseInput.Y;
3734                 gui::IGUIElement *hovered =
3735                         Environment->getRootGUIElement()->getElementFromPoint(
3736                                 core::position2d<s32>(x, y));
3737                 if (hovered && isMyChild(hovered)) {
3738                         hovered->OnEvent(event);
3739                         return event.MouseInput.Event == EMIE_MOUSE_WHEEL;
3740                 }
3741         }
3742
3743         if (event.EventType == EET_MOUSE_INPUT_EVENT) {
3744                 s32 x = event.MouseInput.X;
3745                 s32 y = event.MouseInput.Y;
3746                 gui::IGUIElement *hovered =
3747                         Environment->getRootGUIElement()->getElementFromPoint(
3748                                 core::position2d<s32>(x, y));
3749                 if (event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN) {
3750                         m_old_tooltip_id = -1;
3751                 }
3752                 if (!isChild(hovered,this)) {
3753                         if (DoubleClickDetection(event)) {
3754                                 return true;
3755                         }
3756                 }
3757         }
3758
3759         if (event.EventType == irr::EET_JOYSTICK_INPUT_EVENT) {
3760                 /* TODO add a check like:
3761                 if (event.JoystickEvent != joystick_we_listen_for)
3762                         return false;
3763                 */
3764                 bool handled = m_joystick->handleEvent(event.JoystickEvent);
3765                 if (handled) {
3766                         if (m_joystick->wasKeyDown(KeyType::ESC)) {
3767                                 tryClose();
3768                         } else if (m_joystick->wasKeyDown(KeyType::JUMP)) {
3769                                 if (m_allowclose) {
3770                                         acceptInput(quit_mode_accept);
3771                                         quitMenu();
3772                                 }
3773                         }
3774                 }
3775                 return handled;
3776         }
3777
3778         return GUIModalMenu::preprocessEvent(event);
3779 }
3780
3781 /******************************************************************************/
3782 bool GUIFormSpecMenu::DoubleClickDetection(const SEvent event)
3783 {
3784         /* The following code is for capturing double-clicks of the mouse button
3785          * and translating the double-click into an EET_KEY_INPUT_EVENT event
3786          * -- which closes the form -- under some circumstances.
3787          *
3788          * There have been many github issues reporting this as a bug even though it
3789          * was an intended feature.  For this reason, remapping the double-click as
3790          * an ESC must be explicitly set when creating this class via the
3791          * /p remap_dbl_click parameter of the constructor.
3792          */
3793
3794         if (!m_remap_dbl_click)
3795                 return false;
3796
3797         if (event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN) {
3798                 m_doubleclickdetect[0].pos  = m_doubleclickdetect[1].pos;
3799                 m_doubleclickdetect[0].time = m_doubleclickdetect[1].time;
3800
3801                 m_doubleclickdetect[1].pos  = m_pointer;
3802                 m_doubleclickdetect[1].time = porting::getTimeMs();
3803         }
3804         else if (event.MouseInput.Event == EMIE_LMOUSE_LEFT_UP) {
3805                 u64 delta = porting::getDeltaMs(m_doubleclickdetect[0].time, porting::getTimeMs());
3806                 if (delta > 400) {
3807                         return false;
3808                 }
3809
3810                 double squaredistance =
3811                                 m_doubleclickdetect[0].pos
3812                                 .getDistanceFromSQ(m_doubleclickdetect[1].pos);
3813
3814                 if (squaredistance > (30*30)) {
3815                         return false;
3816                 }
3817
3818                 SEvent* translated = new SEvent();
3819                 assert(translated != 0);
3820                 //translate doubleclick to escape
3821                 memset(translated, 0, sizeof(SEvent));
3822                 translated->EventType = irr::EET_KEY_INPUT_EVENT;
3823                 translated->KeyInput.Key         = KEY_ESCAPE;
3824                 translated->KeyInput.Control     = false;
3825                 translated->KeyInput.Shift       = false;
3826                 translated->KeyInput.PressedDown = true;
3827                 translated->KeyInput.Char        = 0;
3828                 OnEvent(*translated);
3829
3830                 // no need to send the key up event as we're already deleted
3831                 // and no one else did notice this event
3832                 delete translated;
3833                 return true;
3834         }
3835
3836         return false;
3837 }
3838
3839 void GUIFormSpecMenu::tryClose()
3840 {
3841         if (m_allowclose) {
3842                 doPause = false;
3843                 acceptInput(quit_mode_cancel);
3844                 quitMenu();
3845         } else {
3846                 m_text_dst->gotText(L"MenuQuit");
3847         }
3848 }
3849
3850 enum ButtonEventType : u8
3851 {
3852         BET_LEFT,
3853         BET_RIGHT,
3854         BET_MIDDLE,
3855         BET_WHEEL_UP,
3856         BET_WHEEL_DOWN,
3857         BET_UP,
3858         BET_DOWN,
3859         BET_MOVE,
3860         BET_OTHER
3861 };
3862
3863 bool GUIFormSpecMenu::OnEvent(const SEvent& event)
3864 {
3865         if (event.EventType==EET_KEY_INPUT_EVENT) {
3866                 KeyPress kp(event.KeyInput);
3867                 if (event.KeyInput.PressedDown && (
3868                                 (kp == EscapeKey) || (kp == CancelKey) ||
3869                                 ((m_client != NULL) && (kp == getKeySetting("keymap_inventory"))))) {
3870                         tryClose();
3871                         return true;
3872                 }
3873
3874                 if (m_client != NULL && event.KeyInput.PressedDown &&
3875                                 (kp == getKeySetting("keymap_screenshot"))) {
3876                         m_client->makeScreenshot();
3877                 }
3878                 if (event.KeyInput.PressedDown &&
3879                         (event.KeyInput.Key==KEY_RETURN ||
3880                          event.KeyInput.Key==KEY_UP ||
3881                          event.KeyInput.Key==KEY_DOWN)
3882                         ) {
3883                         switch (event.KeyInput.Key) {
3884                                 case KEY_RETURN:
3885                                         current_keys_pending.key_enter = true;
3886                                         break;
3887                                 case KEY_UP:
3888                                         current_keys_pending.key_up = true;
3889                                         break;
3890                                 case KEY_DOWN:
3891                                         current_keys_pending.key_down = true;
3892                                         break;
3893                                 break;
3894                                 default:
3895                                         //can't happen at all!
3896                                         FATAL_ERROR("Reached a source line that can't ever been reached");
3897                                         break;
3898                         }
3899                         if (current_keys_pending.key_enter && m_allowclose) {
3900                                 acceptInput(quit_mode_accept);
3901                                 quitMenu();
3902                         } else {
3903                                 acceptInput();
3904                         }
3905                         return true;
3906                 }
3907
3908         }
3909
3910         /* Mouse event other than movement, or crossing the border of inventory
3911           field while holding right mouse button
3912          */
3913         if (event.EventType == EET_MOUSE_INPUT_EVENT &&
3914                         (event.MouseInput.Event != EMIE_MOUSE_MOVED ||
3915                          (event.MouseInput.Event == EMIE_MOUSE_MOVED &&
3916                           event.MouseInput.isRightPressed() &&
3917                           getItemAtPos(m_pointer).i != getItemAtPos(m_old_pointer).i))) {
3918
3919                 // Get selected item and hovered/clicked item (s)
3920
3921                 m_old_tooltip_id = -1;
3922                 updateSelectedItem();
3923                 ItemSpec s = getItemAtPos(m_pointer);
3924
3925                 Inventory *inv_selected = NULL;
3926                 Inventory *inv_s = NULL;
3927                 InventoryList *list_s = NULL;
3928
3929                 if (m_selected_item) {
3930                         inv_selected = m_invmgr->getInventory(m_selected_item->inventoryloc);
3931                         sanity_check(inv_selected);
3932                         sanity_check(inv_selected->getList(m_selected_item->listname) != NULL);
3933                 }
3934
3935                 u32 s_count = 0;
3936
3937                 if (s.isValid())
3938                 do { // breakable
3939                         inv_s = m_invmgr->getInventory(s.inventoryloc);
3940
3941                         if (!inv_s) {
3942                                 errorstream << "InventoryMenu: The selected inventory location "
3943                                                 << "\"" << s.inventoryloc.dump() << "\" doesn't exist"
3944                                                 << std::endl;
3945                                 s.i = -1;  // make it invalid again
3946                                 break;
3947                         }
3948
3949                         list_s = inv_s->getList(s.listname);
3950                         if (list_s == NULL) {
3951                                 verbosestream << "InventoryMenu: The selected inventory list \""
3952                                                 << s.listname << "\" does not exist" << std::endl;
3953                                 s.i = -1;  // make it invalid again
3954                                 break;
3955                         }
3956
3957                         if ((u32)s.i >= list_s->getSize()) {
3958                                 infostream << "InventoryMenu: The selected inventory list \""
3959                                                 << s.listname << "\" is too small (i=" << s.i << ", size="
3960                                                 << list_s->getSize() << ")" << std::endl;
3961                                 s.i = -1;  // make it invalid again
3962                                 break;
3963                         }
3964
3965                         s_count = list_s->getItem(s.i).count;
3966                 } while(0);
3967
3968                 bool identical = m_selected_item && s.isValid() &&
3969                         (inv_selected == inv_s) &&
3970                         (m_selected_item->listname == s.listname) &&
3971                         (m_selected_item->i == s.i);
3972
3973                 ButtonEventType button = BET_LEFT;
3974                 ButtonEventType updown = BET_OTHER;
3975                 switch (event.MouseInput.Event) {
3976                 case EMIE_LMOUSE_PRESSED_DOWN:
3977                         button = BET_LEFT; updown = BET_DOWN;
3978                         break;
3979                 case EMIE_RMOUSE_PRESSED_DOWN:
3980                         button = BET_RIGHT; updown = BET_DOWN;
3981                         break;
3982                 case EMIE_MMOUSE_PRESSED_DOWN:
3983                         button = BET_MIDDLE; updown = BET_DOWN;
3984                         break;
3985                 case EMIE_MOUSE_WHEEL:
3986                         button = (event.MouseInput.Wheel > 0) ?
3987                                 BET_WHEEL_UP : BET_WHEEL_DOWN;
3988                         updown = BET_DOWN;
3989                         break;
3990                 case EMIE_LMOUSE_LEFT_UP:
3991                         button = BET_LEFT; updown = BET_UP;
3992                         break;
3993                 case EMIE_RMOUSE_LEFT_UP:
3994                         button = BET_RIGHT; updown = BET_UP;
3995                         break;
3996                 case EMIE_MMOUSE_LEFT_UP:
3997                         button = BET_MIDDLE; updown = BET_UP;
3998                         break;
3999                 case EMIE_MOUSE_MOVED:
4000                         updown = BET_MOVE;
4001                         break;
4002                 default:
4003                         break;
4004                 }
4005
4006                 // Set this number to a positive value to generate a move action
4007                 // from m_selected_item to s.
4008                 u32 move_amount = 0;
4009
4010                 // Set this number to a positive value to generate a move action
4011                 // from s to the next inventory ring.
4012                 u32 shift_move_amount = 0;
4013
4014                 // Set this number to a positive value to generate a drop action
4015                 // from m_selected_item.
4016                 u32 drop_amount = 0;
4017
4018                 // Set this number to a positive value to generate a craft action at s.
4019                 u32 craft_amount = 0;
4020
4021                 switch (updown) {
4022                 case BET_DOWN:
4023                         // Some mouse button has been pressed
4024
4025                         //infostream<<"Mouse button "<<button<<" pressed at p=("
4026                         //      <<p.X<<","<<p.Y<<")"<<std::endl;
4027
4028                         m_selected_dragging = false;
4029
4030                         if (s.isValid() && s.listname == "craftpreview") {
4031                                 // Craft preview has been clicked: craft
4032                                 craft_amount = (button == BET_MIDDLE ? 10 : 1);
4033                         } else if (!m_selected_item) {
4034                                 if (s_count && button != BET_WHEEL_UP) {
4035                                         // Non-empty stack has been clicked: select or shift-move it
4036                                         m_selected_item = new ItemSpec(s);
4037
4038                                         u32 count;
4039                                         if (button == BET_RIGHT)
4040                                                 count = (s_count + 1) / 2;
4041                                         else if (button == BET_MIDDLE)
4042                                                 count = MYMIN(s_count, 10);
4043                                         else if (button == BET_WHEEL_DOWN)
4044                                                 count = 1;
4045                                         else  // left
4046                                                 count = s_count;
4047
4048                                         if (!event.MouseInput.Shift) {
4049                                                 // no shift: select item
4050                                                 m_selected_amount = count;
4051                                                 m_selected_dragging = button != BET_WHEEL_DOWN;
4052                                                 m_auto_place = false;
4053                                         } else {
4054                                                 // shift pressed: move item, right click moves 1
4055                                                 shift_move_amount = button == BET_RIGHT ? 1 : count;
4056                                         }
4057                                 }
4058                         } else { // m_selected_item != NULL
4059                                 assert(m_selected_amount >= 1);
4060
4061                                 if (s.isValid()) {
4062                                         // Clicked a slot: move
4063                                         if (button == BET_RIGHT || button == BET_WHEEL_UP)
4064                                                 move_amount = 1;
4065                                         else if (button == BET_MIDDLE)
4066                                                 move_amount = MYMIN(m_selected_amount, 10);
4067                                         else if (button == BET_LEFT)
4068                                                 move_amount = m_selected_amount;
4069                                         // else wheeldown
4070
4071                                         if (identical) {
4072                                                 if (button == BET_WHEEL_DOWN) {
4073                                                         if (m_selected_amount < s_count)
4074                                                                 ++m_selected_amount;
4075                                                 } else {
4076                                                         if (move_amount >= m_selected_amount)
4077                                                                 m_selected_amount = 0;
4078                                                         else
4079                                                                 m_selected_amount -= move_amount;
4080                                                         move_amount = 0;
4081                                                 }
4082                                         }
4083                                 } else if (!getAbsoluteClippingRect().isPointInside(m_pointer)
4084                                                 && button != BET_WHEEL_DOWN) {
4085                                         // Clicked outside of the window: drop
4086                                         if (button == BET_RIGHT || button == BET_WHEEL_UP)
4087                                                 drop_amount = 1;
4088                                         else if (button == BET_MIDDLE)
4089                                                 drop_amount = MYMIN(m_selected_amount, 10);
4090                                         else  // left
4091                                                 drop_amount = m_selected_amount;
4092                                 }
4093                         }
4094                 break;
4095                 case BET_UP:
4096                         // Some mouse button has been released
4097
4098                         //infostream<<"Mouse button "<<button<<" released at p=("
4099                         //      <<p.X<<","<<p.Y<<")"<<std::endl;
4100
4101                         if (m_selected_dragging && m_selected_item) {
4102                                 if (s.isValid()) {
4103                                         if (!identical) {
4104                                                 // Dragged to different slot: move all selected
4105                                                 move_amount = m_selected_amount;
4106                                         }
4107                                 } else if (!getAbsoluteClippingRect().isPointInside(m_pointer)) {
4108                                         // Dragged outside of window: drop all selected
4109                                         drop_amount = m_selected_amount;
4110                                 }
4111                         }
4112
4113                         m_selected_dragging = false;
4114                         // Keep track of whether the mouse button be released
4115                         // One click is drag without dropping. Click + release
4116                         // + click changes to drop item when moved mode
4117                         if (m_selected_item)
4118                                 m_auto_place = true;
4119                 break;
4120                 case BET_MOVE:
4121                         // Mouse has been moved and rmb is down and mouse pointer just
4122                         // entered a new inventory field (checked in the entry-if, this
4123                         // is the only action here that is generated by mouse movement)
4124                         if (m_selected_item && s.isValid() && s.listname != "craftpreview") {
4125                                 // Move 1 item
4126                                 // TODO: middle mouse to move 10 items might be handy
4127                                 if (m_auto_place) {
4128                                         // Only move an item if the destination slot is empty
4129                                         // or contains the same item type as what is going to be
4130                                         // moved
4131                                         InventoryList *list_from = inv_selected->getList(m_selected_item->listname);
4132                                         InventoryList *list_to = list_s;
4133                                         assert(list_from && list_to);
4134                                         ItemStack stack_from = list_from->getItem(m_selected_item->i);
4135                                         ItemStack stack_to = list_to->getItem(s.i);
4136                                         if (stack_to.empty() || stack_to.name == stack_from.name)
4137                                                 move_amount = 1;
4138                                 }
4139                         }
4140                 break;
4141                 default:
4142                         break;
4143                 }
4144
4145                 // Possibly send inventory action to server
4146                 if (move_amount > 0) {
4147                         // Send IAction::Move
4148
4149                         assert(m_selected_item && m_selected_item->isValid());
4150                         assert(s.isValid());
4151
4152                         assert(inv_selected && inv_s);
4153                         InventoryList *list_from = inv_selected->getList(m_selected_item->listname);
4154                         InventoryList *list_to = list_s;
4155                         assert(list_from && list_to);
4156                         ItemStack stack_from = list_from->getItem(m_selected_item->i);
4157                         ItemStack stack_to = list_to->getItem(s.i);
4158
4159                         // Check how many items can be moved
4160                         move_amount = stack_from.count = MYMIN(move_amount, stack_from.count);
4161                         ItemStack leftover = stack_to.addItem(stack_from, m_client->idef());
4162                         bool move = true;
4163                         // If source stack cannot be added to destination stack at all,
4164                         // they are swapped
4165                         if (leftover.count == stack_from.count &&
4166                                         leftover.name == stack_from.name) {
4167
4168                                 if (m_selected_swap.empty()) {
4169                                         m_selected_amount = stack_to.count;
4170                                         m_selected_dragging = false;
4171
4172                                         // WARNING: BLACK MAGIC, BUT IN A REDUCED SET
4173                                         // Skip next validation checks due async inventory calls
4174                                         m_selected_swap = stack_to;
4175                                 } else {
4176                                         move = false;
4177                                 }
4178                         }
4179                         // Source stack goes fully into destination stack
4180                         else if (leftover.empty()) {
4181                                 m_selected_amount -= move_amount;
4182                         }
4183                         // Source stack goes partly into destination stack
4184                         else {
4185                                 move_amount -= leftover.count;
4186                                 m_selected_amount -= move_amount;
4187                         }
4188
4189                         if (move) {
4190                                 infostream << "Handing IAction::Move to manager" << std::endl;
4191                                 IMoveAction *a = new IMoveAction();
4192                                 a->count = move_amount;
4193                                 a->from_inv = m_selected_item->inventoryloc;
4194                                 a->from_list = m_selected_item->listname;
4195                                 a->from_i = m_selected_item->i;
4196                                 a->to_inv = s.inventoryloc;
4197                                 a->to_list = s.listname;
4198                                 a->to_i = s.i;
4199                                 m_invmgr->inventoryAction(a);
4200                         }
4201                 } else if (shift_move_amount > 0) {
4202                         u32 mis = m_inventory_rings.size();
4203                         u32 i = 0;
4204                         for (; i < mis; i++) {
4205                                 const ListRingSpec &sp = m_inventory_rings[i];
4206                                 if (sp.inventoryloc == s.inventoryloc
4207                                                 && sp.listname == s.listname)
4208                                         break;
4209                         }
4210                         do {
4211                                 if (i >= mis) // if not found
4212                                         break;
4213                                 u32 to_inv_ind = (i + 1) % mis;
4214                                 const ListRingSpec &to_inv_sp = m_inventory_rings[to_inv_ind];
4215                                 InventoryList *list_from = list_s;
4216                                 if (!s.isValid())
4217                                         break;
4218                                 Inventory *inv_to = m_invmgr->getInventory(to_inv_sp.inventoryloc);
4219                                 if (!inv_to)
4220                                         break;
4221                                 InventoryList *list_to = inv_to->getList(to_inv_sp.listname);
4222                                 if (!list_to)
4223                                         break;
4224                                 ItemStack stack_from = list_from->getItem(s.i);
4225                                 assert(shift_move_amount <= stack_from.count);
4226
4227                                 infostream << "Handing IAction::Move to manager" << std::endl;
4228                                 IMoveAction *a = new IMoveAction();
4229                                 a->count = shift_move_amount;
4230                                 a->from_inv = s.inventoryloc;
4231                                 a->from_list = s.listname;
4232                                 a->from_i = s.i;
4233                                 a->to_inv = to_inv_sp.inventoryloc;
4234                                 a->to_list = to_inv_sp.listname;
4235                                 a->move_somewhere = true;
4236                                 m_invmgr->inventoryAction(a);
4237                         } while (0);
4238                 } else if (drop_amount > 0) {
4239                         // Send IAction::Drop
4240
4241                         assert(m_selected_item && m_selected_item->isValid());
4242                         assert(inv_selected);
4243                         InventoryList *list_from = inv_selected->getList(m_selected_item->listname);
4244                         assert(list_from);
4245                         ItemStack stack_from = list_from->getItem(m_selected_item->i);
4246
4247                         // Check how many items can be dropped
4248                         drop_amount = stack_from.count = MYMIN(drop_amount, stack_from.count);
4249                         assert(drop_amount > 0 && drop_amount <= m_selected_amount);
4250                         m_selected_amount -= drop_amount;
4251
4252                         infostream << "Handing IAction::Drop to manager" << std::endl;
4253                         IDropAction *a = new IDropAction();
4254                         a->count = drop_amount;
4255                         a->from_inv = m_selected_item->inventoryloc;
4256                         a->from_list = m_selected_item->listname;
4257                         a->from_i = m_selected_item->i;
4258                         m_invmgr->inventoryAction(a);
4259                 } else if (craft_amount > 0) {
4260                         assert(s.isValid());
4261
4262                         // if there are no items selected or the selected item
4263                         // belongs to craftresult list, proceed with crafting
4264                         if (m_selected_item == NULL ||
4265                                         !m_selected_item->isValid() || m_selected_item->listname == "craftresult") {
4266
4267                                 assert(inv_s);
4268
4269                                 // Send IACTION_CRAFT
4270                                 infostream << "Handing IACTION_CRAFT to manager" << std::endl;
4271                                 ICraftAction *a = new ICraftAction();
4272                                 a->count = craft_amount;
4273                                 a->craft_inv = s.inventoryloc;
4274                                 m_invmgr->inventoryAction(a);
4275                         }
4276                 }
4277
4278                 // If m_selected_amount has been decreased to zero, deselect
4279                 if (m_selected_amount == 0) {
4280                         m_selected_swap.clear();
4281                         delete m_selected_item;
4282                         m_selected_item = NULL;
4283                         m_selected_amount = 0;
4284                         m_selected_dragging = false;
4285                 }
4286                 m_old_pointer = m_pointer;
4287         }
4288
4289         if (event.EventType == EET_GUI_EVENT) {
4290                 if (event.GUIEvent.EventType == gui::EGET_TAB_CHANGED
4291                                 && isVisible()) {
4292                         // find the element that was clicked
4293                         for (GUIFormSpecMenu::FieldSpec &s : m_fields) {
4294                                 if ((s.ftype == f_TabHeader) &&
4295                                                 (s.fid == event.GUIEvent.Caller->getID())) {
4296                                         s.send = true;
4297                                         acceptInput();
4298                                         s.send = false;
4299                                         return true;
4300                                 }
4301                         }
4302                 }
4303                 if (event.GUIEvent.EventType == gui::EGET_ELEMENT_FOCUS_LOST
4304                                 && isVisible()) {
4305                         if (!canTakeFocus(event.GUIEvent.Element)) {
4306                                 infostream<<"GUIFormSpecMenu: Not allowing focus change."
4307                                                 <<std::endl;
4308                                 // Returning true disables focus change
4309                                 return true;
4310                         }
4311                 }
4312                 if ((event.GUIEvent.EventType == gui::EGET_BUTTON_CLICKED) ||
4313                                 (event.GUIEvent.EventType == gui::EGET_CHECKBOX_CHANGED) ||
4314                                 (event.GUIEvent.EventType == gui::EGET_COMBO_BOX_CHANGED) ||
4315                                 (event.GUIEvent.EventType == gui::EGET_SCROLL_BAR_CHANGED)) {
4316                         unsigned int btn_id = event.GUIEvent.Caller->getID();
4317
4318                         if (btn_id == 257) {
4319                                 if (m_allowclose) {
4320                                         acceptInput(quit_mode_accept);
4321                                         quitMenu();
4322                                 } else {
4323                                         acceptInput();
4324                                         m_text_dst->gotText(L"ExitButton");
4325                                 }
4326                                 // quitMenu deallocates menu
4327                                 return true;
4328                         }
4329
4330                         // find the element that was clicked
4331                         for (GUIFormSpecMenu::FieldSpec &s : m_fields) {
4332                                 // if its a button, set the send field so
4333                                 // lua knows which button was pressed
4334                                 if ((s.ftype == f_Button || s.ftype == f_CheckBox) &&
4335                                                 s.fid == event.GUIEvent.Caller->getID()) {
4336                                         s.send = true;
4337                                         if (s.is_exit) {
4338                                                 if (m_allowclose) {
4339                                                         acceptInput(quit_mode_accept);
4340                                                         quitMenu();
4341                                                 } else {
4342                                                         m_text_dst->gotText(L"ExitButton");
4343                                                 }
4344                                                 return true;
4345                                         }
4346
4347                                         acceptInput(quit_mode_no);
4348                                         s.send = false;
4349                                         return true;
4350
4351                                 } else if ((s.ftype == f_DropDown) &&
4352                                                 (s.fid == event.GUIEvent.Caller->getID())) {
4353                                         // only send the changed dropdown
4354                                         for (GUIFormSpecMenu::FieldSpec &s2 : m_fields) {
4355                                                 if (s2.ftype == f_DropDown) {
4356                                                         s2.send = false;
4357                                                 }
4358                                         }
4359                                         s.send = true;
4360                                         acceptInput(quit_mode_no);
4361
4362                                         // revert configuration to make sure dropdowns are sent on
4363                                         // regular button click
4364                                         for (GUIFormSpecMenu::FieldSpec &s2 : m_fields) {
4365                                                 if (s2.ftype == f_DropDown) {
4366                                                         s2.send = true;
4367                                                 }
4368                                         }
4369                                         return true;
4370                                 } else if ((s.ftype == f_ScrollBar) &&
4371                                                 (s.fid == event.GUIEvent.Caller->getID())) {
4372                                         s.fdefault = L"Changed";
4373                                         acceptInput(quit_mode_no);
4374                                         s.fdefault = L"";
4375                                 } else if ((s.ftype == f_Unknown) &&
4376                                                 (s.fid == event.GUIEvent.Caller->getID())) {
4377                                         s.send = true;
4378                                         acceptInput();
4379                                         s.send = false;
4380                                 }
4381                         }
4382                 }
4383
4384                 if (event.GUIEvent.EventType == gui::EGET_EDITBOX_ENTER) {
4385                         if (event.GUIEvent.Caller->getID() > 257) {
4386                                 bool close_on_enter = true;
4387                                 for (GUIFormSpecMenu::FieldSpec &s : m_fields) {
4388                                         if (s.ftype == f_Unknown &&
4389                                                         s.fid == event.GUIEvent.Caller->getID()) {
4390                                                 current_field_enter_pending = s.fname;
4391                                                 std::unordered_map<std::string, bool>::const_iterator it =
4392                                                         field_close_on_enter.find(s.fname);
4393                                                 if (it != field_close_on_enter.end())
4394                                                         close_on_enter = (*it).second;
4395
4396                                                 break;
4397                                         }
4398                                 }
4399
4400                                 if (m_allowclose && close_on_enter) {
4401                                         current_keys_pending.key_enter = true;
4402                                         acceptInput(quit_mode_accept);
4403                                         quitMenu();
4404                                 } else {
4405                                         current_keys_pending.key_enter = true;
4406                                         acceptInput();
4407                                 }
4408                                 // quitMenu deallocates menu
4409                                 return true;
4410                         }
4411                 }
4412
4413                 if (event.GUIEvent.EventType == gui::EGET_TABLE_CHANGED) {
4414                         int current_id = event.GUIEvent.Caller->getID();
4415                         if (current_id > 257) {
4416                                 // find the element that was clicked
4417                                 for (GUIFormSpecMenu::FieldSpec &s : m_fields) {
4418                                         // if it's a table, set the send field
4419                                         // so lua knows which table was changed
4420                                         if ((s.ftype == f_Table) && (s.fid == current_id)) {
4421                                                 s.send = true;
4422                                                 acceptInput();
4423                                                 s.send=false;
4424                                         }
4425                                 }
4426                                 return true;
4427                         }
4428                 }
4429         }
4430
4431         return Parent ? Parent->OnEvent(event) : false;
4432 }
4433
4434 /**
4435  * get name of element by element id
4436  * @param id of element
4437  * @return name string or empty string
4438  */
4439 std::string GUIFormSpecMenu::getNameByID(s32 id)
4440 {
4441         for (FieldSpec &spec : m_fields) {
4442                 if (spec.fid == id)
4443                         return spec.fname;
4444         }
4445         return "";
4446 }
4447
4448
4449 const GUIFormSpecMenu::FieldSpec *GUIFormSpecMenu::getSpecByID(s32 id)
4450 {
4451         for (FieldSpec &spec : m_fields) {
4452                 if (spec.fid == id)
4453                         return &spec;
4454         }
4455         return nullptr;
4456 }
4457
4458 /**
4459  * get label of element by id
4460  * @param id of element
4461  * @return label string or empty string
4462  */
4463 std::wstring GUIFormSpecMenu::getLabelByID(s32 id)
4464 {
4465         for (FieldSpec &spec : m_fields) {
4466                 if (spec.fid == id)
4467                         return spec.flabel;
4468         }
4469         return L"";
4470 }
4471
4472 StyleSpec GUIFormSpecMenu::getStyleForElement(const std::string &type,
4473                 const std::string &name, const std::string &parent_type) {
4474         StyleSpec ret;
4475
4476         if (!parent_type.empty()) {
4477                 auto it = theme_by_type.find(parent_type);
4478                 if (it != theme_by_type.end()) {
4479                         ret |= it->second;
4480                 }
4481         }
4482
4483         auto it = theme_by_type.find(type);
4484         if (it != theme_by_type.end()) {
4485                 ret |= it->second;
4486         }
4487
4488         it = theme_by_name.find(name);
4489         if (it != theme_by_name.end()) {
4490                 ret |= it->second;
4491         }
4492
4493         return ret;
4494 }