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