/*
Minetest
Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include <cstdlib>
#include <algorithm>
#include <iterator>
#include <sstream>
#include <limits>
#include "guiFormSpecMenu.h"
#include "constants.h"
#include "gamedef.h"
#include "keycode.h"
#include "strfnd.h"
#include <IGUICheckBox.h>
#include <IGUIEditBox.h>
#include <IGUIButton.h>
#include <IGUIStaticText.h>
#include <IGUIFont.h>
#include <IGUIListBox.h>
#include <IGUITabControl.h>
#include <IGUIScrollBar.h>
#include <IGUIComboBox.h>
#include "log.h"
#include "tile.h" // ITextureSource
#include "hud.h" // drawItemStack
#include "util/string.h"
#include "util/numeric.h"
#include "filesys.h"
#include "gettime.h"
#include "gettext.h"
#define MY_CHECKPOS(a,b) \
if (v_pos.size() != 2) { \
errorstream<< "Invalid pos for element " << a << "specified: \"" \
<< parts[b] << "\"" << std::endl; \
return; \
}
#define MY_CHECKGEOM(a,b) \
if (v_geom.size() != 2) { \
errorstream<< "Invalid pos for element " << a << "specified: \"" \
<< parts[b] << "\"" << std::endl; \
return; \
}
/*
GUIFormSpecMenu
*/
GUIFormSpecMenu::GUIFormSpecMenu(irr::IrrlichtDevice* dev,
gui::IGUIElement* parent, s32 id,
IMenuManager *menumgr,
InventoryManager *invmgr,
IGameDef *gamedef,
ISimpleTextureSource *tsrc
):
GUIModalMenu(dev->getGUIEnvironment(), parent, id, menumgr),
m_device(dev),
m_invmgr(invmgr),
m_gamedef(gamedef),
m_tsrc(tsrc),
m_form_src(NULL),
m_text_dst(NULL),
m_selected_item(NULL),
m_selected_amount(0),
m_selected_dragging(false),
m_listbox_click_fname(),
m_listbox_click_index(-1),
m_listbox_click_time(0),
m_listbox_doubleclick(false),
m_tooltip_element(NULL),
m_allowclose(true),
m_lock(false)
{
current_keys_pending.key_down = false;
current_keys_pending.key_up = false;
current_keys_pending.key_enter = false;
current_keys_pending.key_escape = false;
}
GUIFormSpecMenu::~GUIFormSpecMenu()
{
removeChildren();
delete m_selected_item;
delete m_form_src;
delete m_text_dst;
}
void GUIFormSpecMenu::removeChildren()
{
const core::list<gui::IGUIElement*> &children = getChildren();
core::list<gui::IGUIElement*> children_copy;
for(core::list<gui::IGUIElement*>::ConstIterator
i = children.begin(); i != children.end(); i++)
{
children_copy.push_back(*i);
}
for(core::list<gui::IGUIElement*>::Iterator
i = children_copy.begin();
i != children_copy.end(); i++)
{
(*i)->remove();
}
/*{
gui::IGUIElement *e = getElementFromId(256);
if(e != NULL)
e->remove();
}*/
if(m_tooltip_element)
{
m_tooltip_element->remove();
m_tooltip_element->drop();
m_tooltip_element = NULL;
}
}
void GUIFormSpecMenu::setInitialFocus()
{
// Set initial focus according to following order of precedence:
// 1. first empty editbox
// 2. first editbox
// 3. first listbox
// 4. last button
// 5. first focusable (not statictext, not tabheader)
// 6. first child element
core::list<gui::IGUIElement*> children = getChildren();
// in case "children" contains any NULL elements, remove them
for (core::list<gui::IGUIElement*>::Iterator it = children.begin();
it != children.end();) {
if (*it)
++it;
else
it = children.erase(it);
}
// 1. first empty editbox
for (core::list<gui::IGUIElement*>::Iterator it = children.begin();
it != children.end(); ++it) {
if ((*it)->getType() == gui::EGUIET_EDIT_BOX
&& (*it)->getText()[0] == 0) {
Environment->setFocus(*it);
return;
}
}
// 2. first editbox
for (core::list<gui::IGUIElement*>::Iterator it = children.begin();
it != children.end(); ++it) {
if ((*it)->getType() == gui::EGUIET_EDIT_BOX) {
Environment->setFocus(*it);
return;
}
}
// 3. first listbox
for (core::list<gui::IGUIElement*>::Iterator it = children.begin();
it != children.end(); ++it) {
if ((*it)->getType() == gui::EGUIET_LIST_BOX) {
Environment->setFocus(*it);
return;
}
}
// 4. last button
for (core::list<gui::IGUIElement*>::Iterator it = children.getLast();
it != children.end(); --it) {
if ((*it)->getType() == gui::EGUIET_BUTTON) {
Environment->setFocus(*it);
return;
}
}
// 5. first focusable (not statictext, not tabheader)
for (core::list<gui::IGUIElement*>::Iterator it = children.begin();
it != children.end(); ++it) {
if ((*it)->getType() != gui::EGUIET_STATIC_TEXT &&
(*it)->getType() != gui::EGUIET_TAB_CONTROL) {
Environment->setFocus(*it);
return;
}
}
// 6. first child element
if (children.empty())
Environment->setFocus(this);
else
Environment->setFocus(*(children.begin()));
}
int GUIFormSpecMenu::getListboxIndex(std::string listboxname) {
std::wstring wlistboxname = narrow_to_wide(listboxname.c_str());
for(unsigned int i=0; i < m_listboxes.size(); i++) {
std::wstring name(m_listboxes[i].first.fname.c_str());
if ( name == wlistboxname) {
return m_listboxes[i].second->getSelected();
}
}
return -1;
}
bool GUIFormSpecMenu::checkListboxClick(std::wstring wlistboxname,
int eventtype)
{
// WARNING: BLACK IRRLICHT MAGIC
// Used to fix Irrlicht's subpar reporting of single clicks and double
// clicks in listboxes (gui::EGET_LISTBOX_CHANGED,
// gui::EGET_LISTBOX_SELECTED_AGAIN):
// 1. IGUIListBox::setSelected() is counted as a click.
// Including the initial setSelected() done by parseTextList().
// 2. Clicking on a the selected item and then dragging for less
// than 500ms is counted as a doubleclick, no matter when the
// item was previously selected (e.g. more than 500ms ago)
// So when Irrlicht reports a doubleclick, we need to check
// for ourselves if really was a doubleclick. Or just a fake.
for(unsigned int i=0; i < m_listboxes.size(); i++) {
std::wstring name(m_listboxes[i].first.fname.c_str());
int selected = m_listboxes[i].second->getSelected();
if (name == wlistboxname && selected >= 0) {
u32 now = getTimeMs();
bool doubleclick =
(eventtype == gui::EGET_LISTBOX_SELECTED_AGAIN)
&& (name == m_listbox_click_fname)
&& (selected == m_listbox_click_index)
&& (m_listbox_click_time >= now - 500);
m_listbox_click_fname = name;
m_listbox_click_index = selected;
m_listbox_click_time = now;
m_listbox_doubleclick = doubleclick;
return true;
}
}
return false;
}
gui::IGUIScrollBar* GUIFormSpecMenu::getListboxScrollbar(
gui::IGUIListBox *listbox)
{
// WARNING: BLACK IRRLICHT MAGIC
// Ordinarily, due to how formspecs work (recreating the entire GUI
// when something changes), when you select an item in a textlist
// with more items than fit in the visible area, the newly selected
// item is scrolled to the bottom of the visible area. This is
// annoying and breaks GUI designs that use double clicks.
// This function helps fixing this problem by giving direct access
// to a listbox's scrollbar. This works because CGUIListBox doesn't
// cache the scrollbar position anywhere.
// If this stops working in a future irrlicht version, consider
// maintaining a local copy of irr::gui::CGUIListBox, possibly also
// fixing the other reasons why black irrlicht magic is needed.
core::list<gui::IGUIElement*> children = listbox->getChildren();
for(core::list<gui::IGUIElement*>::Iterator it = children.begin();
it != children.end(); ++it) {
gui::IGUIElement* child = *it;
if (child && child->getType() == gui::EGUIET_SCROLL_BAR) {
return static_cast<gui::IGUIScrollBar*>(child);
}
}
verbosestream<<"getListboxScrollbar: WARNING: "
<<"listbox has no scrollbar"<<std::endl;
return NULL;
}
std::vector<std::string> split(const std::string &s, char delim) {
std::vector<std::string> tokens;
std::string current = "";
bool last_was_escape = false;
for(unsigned int i=0; i < s.size(); i++) {
if (last_was_escape) {
current += '\\';
current += s.c_str()[i];
last_was_escape = false;
}
else {
if (s.c_str()[i] == delim) {
tokens.push_back(current);
current = "";
last_was_escape = false;
}
else if (s.c_str()[i] == '\\'){
last_was_escape = true;
}
else {
current += s.c_str()[i];
last_was_escape = false;
}
}
}
//push last element
tokens.push_back(current);
return tokens;
}
void GUIFormSpecMenu::parseSize(parserData* data,std::string element) {
std::vector<std::string> parts = split(element,',');
if (parts.size() == 2) {
v2f invsize;
if (parts[1].find(';') != std::string::npos)
parts[1] = parts[1].substr(0,parts[1].find(';'));
invsize.X = stof(parts[0]);
invsize.Y = stof(parts[1]);
if (m_lock) {
v2u32 current_screensize = m_device->getVideoDriver()->getScreenSize();
v2u32 delta = current_screensize - m_lockscreensize;
if (current_screensize.Y > m_lockscreensize.Y)
delta.Y /= 2;
else
delta.Y = 0;
if (current_screensize.X > m_lockscreensize.X)
delta.X /= 2;
else
delta.X = 0;
offset = v2s32(delta.X,delta.Y);
data->screensize = m_lockscreensize;
}
else {
offset = v2s32(0,0);
}
padding = v2s32(data->screensize.Y/40, data->screensize.Y/40);
spacing = v2s32(data->screensize.Y/12, data->screensize.Y/13);
imgsize = v2s32(data->screensize.Y/15, data->screensize.Y/15);
data->size = v2s32(
padding.X*2+spacing.X*(invsize.X-1.0)+imgsize.X,
padding.Y*2+spacing.Y*(invsize.Y-1.0)+imgsize.Y + (data->helptext_h-5)
);
data->rect = core::rect<s32>(
data->screensize.X/2 - data->size.X/2 + offset.X,
data->screensize.Y/2 - data->size.Y/2 + offset.Y,
data->screensize.X/2 + data->size.X/2 + offset.X,
data->screensize.Y/2 + data->size.Y/2 + offset.Y
);
DesiredRect = data->rect;
recalculateAbsolutePosition(false);
data->basepos = getBasePos();
data->bp_set = 2;
return;
}
errorstream<< "Invalid size element (" << parts.size() << "): '" << element << "'" << std::endl;
}
void GUIFormSpecMenu::parseList(parserData* data,std::string element) {
if (m_gamedef == 0) {
errorstream<<"WARNING: invalid use of 'list' with m_gamedef==0"<<std::endl;
return;
}
std::vector<std::string> parts = split(element,';');
if ((parts.size() == 4) || (parts.size() == 5)) {
std::string location = parts[0];
std::string listname = parts[1];
std::vector<std::string> v_pos = split(parts[2],',');
std::vector<std::string> v_geom = split(parts[3],',');
std::string startindex = "";
if (parts.size() == 5)
startindex = parts[4];
MY_CHECKPOS("list",2);
MY_CHECKGEOM("list",3);
InventoryLocation loc;
if(location == "context" || location == "current_name")
loc = m_current_inventory_location;
else
loc.deSerialize(location);
v2s32 pos = padding + AbsoluteRect.UpperLeftCorner;
pos.X += stof(v_pos[0]) * (float)spacing.X;
pos.Y += stof(v_pos[1]) * (float)spacing.Y;
v2s32 geom;
geom.X = stoi(v_geom[0]);
geom.Y = stoi(v_geom[1]);
s32 start_i = 0;
if(startindex != "")
start_i = stoi(startindex);
if(data->bp_set != 2)
errorstream<<"WARNING: invalid use of list without a size[] element"<<std::endl;
m_inventorylists.push_back(ListDrawSpec(loc, listname, pos, geom, start_i));
return;
}
errorstream<< "Invalid list element(" << parts.size() << "): '" << element << "'" << std::endl;
}
void GUIFormSpecMenu::parseCheckbox(parserData* data,std::string element) {
std::vector<std::string> parts = split(element,';');
|