diff options
author | Vitaliy <silverunicorn2011@yandex.ru> | 2017-11-09 01:56:20 +0300 |
---|---|---|
committer | Loïc Blot <nerzhul@users.noreply.github.com> | 2017-11-08 23:56:20 +0100 |
commit | 20a85d76d94c9c5c7fbe198c3d0e1fbee97a485f (patch) | |
tree | 67378802190117d8271b3b6d489a92bcb16203b7 /src/gui | |
parent | fc9747eb4b7f75e59a28957bc50f7a78256b3d66 (diff) | |
download | minetest-20a85d76d94c9c5c7fbe198c3d0e1fbee97a485f.tar.gz minetest-20a85d76d94c9c5c7fbe198c3d0e1fbee97a485f.tar.bz2 minetest-20a85d76d94c9c5c7fbe198c3d0e1fbee97a485f.zip |
Move files to subdirectories (#6599)
* Move files around
Diffstat (limited to 'src/gui')
26 files changed, 14065 insertions, 0 deletions
diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt new file mode 100644 index 000000000..067ba09a8 --- /dev/null +++ b/src/gui/CMakeLists.txt @@ -0,0 +1,13 @@ +set(gui_SRCS + ${CMAKE_CURRENT_SOURCE_DIR}/guiChatConsole.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/guiEditBoxWithScrollbar.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/guiEngine.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/guiFormSpecMenu.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/guiKeyChangeMenu.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/guiPasswordChange.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/guiPathSelectMenu.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/guiTable.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/guiVolumeChange.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/intlGUIEditBox.cpp + PARENT_SCOPE +) diff --git a/src/gui/guiChatConsole.cpp b/src/gui/guiChatConsole.cpp new file mode 100644 index 000000000..b194834e2 --- /dev/null +++ b/src/gui/guiChatConsole.cpp @@ -0,0 +1,642 @@ +/* +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 "guiChatConsole.h" +#include "chat.h" +#include "client.h" +#include "debug.h" +#include "gettime.h" +#include "keycode.h" +#include "settings.h" +#include "porting.h" +#include "client/tile.h" +#include "fontengine.h" +#include "log.h" +#include "gettext.h" +#include <string> + +#if USE_FREETYPE + #include "irrlicht_changes/CGUITTFont.h" +#endif + +inline u32 clamp_u8(s32 value) +{ + return (u32) MYMIN(MYMAX(value, 0), 255); +} + + +GUIChatConsole::GUIChatConsole( + gui::IGUIEnvironment* env, + gui::IGUIElement* parent, + s32 id, + ChatBackend* backend, + Client* client, + IMenuManager* menumgr +): + IGUIElement(gui::EGUIET_ELEMENT, env, parent, id, + core::rect<s32>(0,0,100,100)), + m_chat_backend(backend), + m_client(client), + m_menumgr(menumgr), + m_animate_time_old(porting::getTimeMs()) +{ + // load background settings + s32 console_alpha = g_settings->getS32("console_alpha"); + m_background_color.setAlpha(clamp_u8(console_alpha)); + + // load the background texture depending on settings + ITextureSource *tsrc = client->getTextureSource(); + if (tsrc->isKnownSourceImage("background_chat.jpg")) { + m_background = tsrc->getTexture("background_chat.jpg"); + m_background_color.setRed(255); + m_background_color.setGreen(255); + m_background_color.setBlue(255); + } else { + v3f console_color = g_settings->getV3F("console_color"); + m_background_color.setRed(clamp_u8(myround(console_color.X))); + m_background_color.setGreen(clamp_u8(myround(console_color.Y))); + m_background_color.setBlue(clamp_u8(myround(console_color.Z))); + } + + m_font = g_fontengine->getFont(FONT_SIZE_UNSPECIFIED, FM_Mono); + + if (!m_font) { + errorstream << "GUIChatConsole: Unable to load mono font "; + } else { + core::dimension2d<u32> dim = m_font->getDimension(L"M"); + m_fontsize = v2u32(dim.Width, dim.Height); + m_font->grab(); + } + m_fontsize.X = MYMAX(m_fontsize.X, 1); + m_fontsize.Y = MYMAX(m_fontsize.Y, 1); + + // set default cursor options + setCursor(true, true, 2.0, 0.1); +} + +GUIChatConsole::~GUIChatConsole() +{ + if (m_font) + m_font->drop(); +} + +void GUIChatConsole::openConsole(f32 scale) +{ + assert(scale > 0.0f && scale <= 1.0f); + + m_open = true; + m_desired_height_fraction = scale; + m_desired_height = scale * m_screensize.Y; + reformatConsole(); + m_animate_time_old = porting::getTimeMs(); + IGUIElement::setVisible(true); + Environment->setFocus(this); + m_menumgr->createdMenu(this); +} + +bool GUIChatConsole::isOpen() const +{ + return m_open; +} + +bool GUIChatConsole::isOpenInhibited() const +{ + return m_open_inhibited > 0; +} + +void GUIChatConsole::closeConsole() +{ + m_open = false; + Environment->removeFocus(this); + m_menumgr->deletingMenu(this); +} + +void GUIChatConsole::closeConsoleAtOnce() +{ + closeConsole(); + m_height = 0; + recalculateConsolePosition(); +} + +f32 GUIChatConsole::getDesiredHeight() const +{ + return m_desired_height_fraction; +} + +void GUIChatConsole::replaceAndAddToHistory(std::wstring line) +{ + ChatPrompt& prompt = m_chat_backend->getPrompt(); + prompt.addToHistory(prompt.getLine()); + prompt.replace(line); +} + + +void GUIChatConsole::setCursor( + bool visible, bool blinking, f32 blink_speed, f32 relative_height) +{ + if (visible) + { + if (blinking) + { + // leave m_cursor_blink unchanged + m_cursor_blink_speed = blink_speed; + } + else + { + m_cursor_blink = 0x8000; // on + m_cursor_blink_speed = 0.0; + } + } + else + { + m_cursor_blink = 0; // off + m_cursor_blink_speed = 0.0; + } + m_cursor_height = relative_height; +} + +void GUIChatConsole::draw() +{ + if(!IsVisible) + return; + + video::IVideoDriver* driver = Environment->getVideoDriver(); + + // Check screen size + v2u32 screensize = driver->getScreenSize(); + if (screensize != m_screensize) + { + // screen size has changed + // scale current console height to new window size + if (m_screensize.Y != 0) + m_height = m_height * screensize.Y / m_screensize.Y; + m_screensize = screensize; + m_desired_height = m_desired_height_fraction * m_screensize.Y; + reformatConsole(); + } + + // Animation + u64 now = porting::getTimeMs(); + animate(now - m_animate_time_old); + m_animate_time_old = now; + + // Draw console elements if visible + if (m_height > 0) + { + drawBackground(); + drawText(); + drawPrompt(); + } + + gui::IGUIElement::draw(); +} + +void GUIChatConsole::reformatConsole() +{ + s32 cols = m_screensize.X / m_fontsize.X - 2; // make room for a margin (looks better) + s32 rows = m_desired_height / m_fontsize.Y - 1; // make room for the input prompt + if (cols <= 0 || rows <= 0) + cols = rows = 0; + recalculateConsolePosition(); + m_chat_backend->reformat(cols, rows); +} + +void GUIChatConsole::recalculateConsolePosition() +{ + core::rect<s32> rect(0, 0, m_screensize.X, m_height); + DesiredRect = rect; + recalculateAbsolutePosition(false); +} + +void GUIChatConsole::animate(u32 msec) +{ + // animate the console height + s32 goal = m_open ? m_desired_height : 0; + + // Set invisible if close animation finished (reset by openConsole) + // This function (animate()) is never called once its visibility becomes false so do not + // actually set visible to false before the inhibited period is over + if (!m_open && m_height == 0 && m_open_inhibited == 0) + IGUIElement::setVisible(false); + + if (m_height != goal) + { + s32 max_change = msec * m_screensize.Y * (m_height_speed / 1000.0); + if (max_change == 0) + max_change = 1; + + if (m_height < goal) + { + // increase height + if (m_height + max_change < goal) + m_height += max_change; + else + m_height = goal; + } + else + { + // decrease height + if (m_height > goal + max_change) + m_height -= max_change; + else + m_height = goal; + } + + recalculateConsolePosition(); + } + + // blink the cursor + if (m_cursor_blink_speed != 0.0) + { + u32 blink_increase = 0x10000 * msec * (m_cursor_blink_speed / 1000.0); + if (blink_increase == 0) + blink_increase = 1; + m_cursor_blink = ((m_cursor_blink + blink_increase) & 0xffff); + } + + // decrease open inhibit counter + if (m_open_inhibited > msec) + m_open_inhibited -= msec; + else + m_open_inhibited = 0; +} + +void GUIChatConsole::drawBackground() +{ + video::IVideoDriver* driver = Environment->getVideoDriver(); + if (m_background != NULL) + { + core::rect<s32> sourcerect(0, -m_height, m_screensize.X, 0); + driver->draw2DImage( + m_background, + v2s32(0, 0), + sourcerect, + &AbsoluteClippingRect, + m_background_color, + false); + } + else + { + driver->draw2DRectangle( + m_background_color, + core::rect<s32>(0, 0, m_screensize.X, m_height), + &AbsoluteClippingRect); + } +} + +void GUIChatConsole::drawText() +{ + if (m_font == NULL) + return; + + ChatBuffer& buf = m_chat_backend->getConsoleBuffer(); + for (u32 row = 0; row < buf.getRows(); ++row) + { + const ChatFormattedLine& line = buf.getFormattedLine(row); + if (line.fragments.empty()) + continue; + + s32 line_height = m_fontsize.Y; + s32 y = row * line_height + m_height - m_desired_height; + if (y + line_height < 0) + continue; + + for (const ChatFormattedFragment &fragment : line.fragments) { + s32 x = (fragment.column + 1) * m_fontsize.X; + core::rect<s32> destrect( + x, y, x + m_fontsize.X * fragment.text.size(), y + m_fontsize.Y); + + + #if USE_FREETYPE + // Draw colored text if FreeType is enabled + irr::gui::CGUITTFont *tmp = dynamic_cast<irr::gui::CGUITTFont *>(m_font); + tmp->draw( + fragment.text, + destrect, + video::SColor(255, 255, 255, 255), + false, + false, + &AbsoluteClippingRect); + #else + // Otherwise use standard text + m_font->draw( + fragment.text.c_str(), + destrect, + video::SColor(255, 255, 255, 255), + false, + false, + &AbsoluteClippingRect); + #endif + } + } +} + +void GUIChatConsole::drawPrompt() +{ + if (!m_font) + return; + + u32 row = m_chat_backend->getConsoleBuffer().getRows(); + s32 line_height = m_fontsize.Y; + s32 y = row * line_height + m_height - m_desired_height; + + ChatPrompt& prompt = m_chat_backend->getPrompt(); + std::wstring prompt_text = prompt.getVisiblePortion(); + + // FIXME Draw string at once, not character by character + // That will only work with the cursor once we have a monospace font + for (u32 i = 0; i < prompt_text.size(); ++i) + { + wchar_t ws[2] = {prompt_text[i], 0}; + s32 x = (1 + i) * m_fontsize.X; + core::rect<s32> destrect( + x, y, x + m_fontsize.X, y + m_fontsize.Y); + m_font->draw( + ws, + destrect, + video::SColor(255, 255, 255, 255), + false, + false, + &AbsoluteClippingRect); + } + + // Draw the cursor during on periods + if ((m_cursor_blink & 0x8000) != 0) + { + s32 cursor_pos = prompt.getVisibleCursorPosition(); + if (cursor_pos >= 0) + { + s32 cursor_len = prompt.getCursorLength(); + video::IVideoDriver* driver = Environment->getVideoDriver(); + s32 x = (1 + cursor_pos) * m_fontsize.X; + core::rect<s32> destrect( + x, + y + m_fontsize.Y * (1.0 - m_cursor_height), + x + m_fontsize.X * MYMAX(cursor_len, 1), + y + m_fontsize.Y * (cursor_len ? m_cursor_height+1 : 1) + ); + video::SColor cursor_color(255,255,255,255); + driver->draw2DRectangle( + cursor_color, + destrect, + &AbsoluteClippingRect); + } + } + +} + +bool GUIChatConsole::OnEvent(const SEvent& event) +{ + + ChatPrompt &prompt = m_chat_backend->getPrompt(); + + if(event.EventType == EET_KEY_INPUT_EVENT && event.KeyInput.PressedDown) + { + // Key input + if (KeyPress(event.KeyInput) == getKeySetting("keymap_console")) { + closeConsole(); + + // inhibit open so the_game doesn't reopen immediately + m_open_inhibited = 50; + m_close_on_enter = false; + return true; + } + + if (event.KeyInput.Key == KEY_ESCAPE) { + closeConsoleAtOnce(); + m_close_on_enter = false; + // inhibit open so the_game doesn't reopen immediately + m_open_inhibited = 1; // so the ESCAPE button doesn't open the "pause menu" + return true; + } + else if(event.KeyInput.Key == KEY_PRIOR) + { + m_chat_backend->scrollPageUp(); + return true; + } + else if(event.KeyInput.Key == KEY_NEXT) + { + m_chat_backend->scrollPageDown(); + return true; + } + else if(event.KeyInput.Key == KEY_RETURN) + { + prompt.addToHistory(prompt.getLine()); + std::wstring text = prompt.replace(L""); + m_client->typeChatMessage(text); + if (m_close_on_enter) { + closeConsoleAtOnce(); + m_close_on_enter = false; + } + return true; + } + else if(event.KeyInput.Key == KEY_UP) + { + // Up pressed + // Move back in history + prompt.historyPrev(); + return true; + } + else if(event.KeyInput.Key == KEY_DOWN) + { + // Down pressed + // Move forward in history + prompt.historyNext(); + return true; + } + else if(event.KeyInput.Key == KEY_LEFT || event.KeyInput.Key == KEY_RIGHT) + { + // Left/right pressed + // Move/select character/word to the left depending on control and shift keys + ChatPrompt::CursorOp op = event.KeyInput.Shift ? + ChatPrompt::CURSOROP_SELECT : + ChatPrompt::CURSOROP_MOVE; + ChatPrompt::CursorOpDir dir = event.KeyInput.Key == KEY_LEFT ? + ChatPrompt::CURSOROP_DIR_LEFT : + ChatPrompt::CURSOROP_DIR_RIGHT; + ChatPrompt::CursorOpScope scope = event.KeyInput.Control ? + ChatPrompt::CURSOROP_SCOPE_WORD : + ChatPrompt::CURSOROP_SCOPE_CHARACTER; + prompt.cursorOperation(op, dir, scope); + return true; + } + else if(event.KeyInput.Key == KEY_HOME) + { + // Home pressed + // move to beginning of line + prompt.cursorOperation( + ChatPrompt::CURSOROP_MOVE, + ChatPrompt::CURSOROP_DIR_LEFT, + ChatPrompt::CURSOROP_SCOPE_LINE); + return true; + } + else if(event.KeyInput.Key == KEY_END) + { + // End pressed + // move to end of line + prompt.cursorOperation( + ChatPrompt::CURSOROP_MOVE, + ChatPrompt::CURSOROP_DIR_RIGHT, + ChatPrompt::CURSOROP_SCOPE_LINE); + return true; + } + else if(event.KeyInput.Key == KEY_BACK) + { + // Backspace or Ctrl-Backspace pressed + // delete character / word to the left + ChatPrompt::CursorOpScope scope = + event.KeyInput.Control ? + ChatPrompt::CURSOROP_SCOPE_WORD : + ChatPrompt::CURSOROP_SCOPE_CHARACTER; + prompt.cursorOperation( + ChatPrompt::CURSOROP_DELETE, + ChatPrompt::CURSOROP_DIR_LEFT, + scope); + return true; + } + else if(event.KeyInput.Key == KEY_DELETE) + { + // Delete or Ctrl-Delete pressed + // delete character / word to the right + ChatPrompt::CursorOpScope scope = + event.KeyInput.Control ? + ChatPrompt::CURSOROP_SCOPE_WORD : + ChatPrompt::CURSOROP_SCOPE_CHARACTER; + prompt.cursorOperation( + ChatPrompt::CURSOROP_DELETE, + ChatPrompt::CURSOROP_DIR_RIGHT, + scope); + return true; + } + else if(event.KeyInput.Key == KEY_KEY_A && event.KeyInput.Control) + { + // Ctrl-A pressed + // Select all text + prompt.cursorOperation( + ChatPrompt::CURSOROP_SELECT, + ChatPrompt::CURSOROP_DIR_LEFT, // Ignored + ChatPrompt::CURSOROP_SCOPE_LINE); + return true; + } + else if(event.KeyInput.Key == KEY_KEY_C && event.KeyInput.Control) + { + // Ctrl-C pressed + // Copy text to clipboard + if (prompt.getCursorLength() <= 0) + return true; + std::wstring wselected = prompt.getSelection(); + std::string selected(wselected.begin(), wselected.end()); + Environment->getOSOperator()->copyToClipboard(selected.c_str()); + return true; + } + else if(event.KeyInput.Key == KEY_KEY_V && event.KeyInput.Control) + { + // Ctrl-V pressed + // paste text from clipboard + if (prompt.getCursorLength() > 0) { + // Delete selected section of text + prompt.cursorOperation( + ChatPrompt::CURSOROP_DELETE, + ChatPrompt::CURSOROP_DIR_LEFT, // Ignored + ChatPrompt::CURSOROP_SCOPE_SELECTION); + } + IOSOperator *os_operator = Environment->getOSOperator(); + const c8 *text = os_operator->getTextFromClipboard(); + if (!text) + return true; + std::basic_string<unsigned char> str((const unsigned char*)text); + prompt.input(std::wstring(str.begin(), str.end())); + return true; + } + else if(event.KeyInput.Key == KEY_KEY_X && event.KeyInput.Control) + { + // Ctrl-X pressed + // Cut text to clipboard + if (prompt.getCursorLength() <= 0) + return true; + std::wstring wselected = prompt.getSelection(); + std::string selected(wselected.begin(), wselected.end()); + Environment->getOSOperator()->copyToClipboard(selected.c_str()); + prompt.cursorOperation( + ChatPrompt::CURSOROP_DELETE, + ChatPrompt::CURSOROP_DIR_LEFT, // Ignored + ChatPrompt::CURSOROP_SCOPE_SELECTION); + return true; + } + else if(event.KeyInput.Key == KEY_KEY_U && event.KeyInput.Control) + { + // Ctrl-U pressed + // kill line to left end + prompt.cursorOperation( + ChatPrompt::CURSOROP_DELETE, + ChatPrompt::CURSOROP_DIR_LEFT, + ChatPrompt::CURSOROP_SCOPE_LINE); + return true; + } + else if(event.KeyInput.Key == KEY_KEY_K && event.KeyInput.Control) + { + // Ctrl-K pressed + // kill line to right end + prompt.cursorOperation( + ChatPrompt::CURSOROP_DELETE, + ChatPrompt::CURSOROP_DIR_RIGHT, + ChatPrompt::CURSOROP_SCOPE_LINE); + return true; + } + else if(event.KeyInput.Key == KEY_TAB) + { + // Tab or Shift-Tab pressed + // Nick completion + std::list<std::string> names = m_client->getConnectedPlayerNames(); + bool backwards = event.KeyInput.Shift; + prompt.nickCompletion(names, backwards); + return true; + } else if (!iswcntrl(event.KeyInput.Char) && !event.KeyInput.Control) { + #if defined(__linux__) && (IRRLICHT_VERSION_MAJOR == 1 && IRRLICHT_VERSION_MINOR < 9) + wchar_t wc = L'_'; + mbtowc( &wc, (char *) &event.KeyInput.Char, sizeof(event.KeyInput.Char) ); + prompt.input(wc); + #else + prompt.input(event.KeyInput.Char); + #endif + return true; + } + } + else if(event.EventType == EET_MOUSE_INPUT_EVENT) + { + if(event.MouseInput.Event == EMIE_MOUSE_WHEEL) + { + s32 rows = myround(-3.0 * event.MouseInput.Wheel); + m_chat_backend->scroll(rows); + } + } + + return Parent ? Parent->OnEvent(event) : false; +} + +void GUIChatConsole::setVisible(bool visible) +{ + m_open = visible; + IGUIElement::setVisible(visible); + if (!visible) { + m_height = 0; + recalculateConsolePosition(); + } +} + diff --git a/src/gui/guiChatConsole.h b/src/gui/guiChatConsole.h new file mode 100644 index 000000000..ef8a87673 --- /dev/null +++ b/src/gui/guiChatConsole.h @@ -0,0 +1,133 @@ +/* +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. +*/ + +#pragma once + +#include "irrlichttypes_extrabloated.h" +#include "modalMenu.h" +#include "chat.h" +#include "config.h" + +class Client; + +class GUIChatConsole : public gui::IGUIElement +{ +public: + GUIChatConsole(gui::IGUIEnvironment* env, + gui::IGUIElement* parent, + s32 id, + ChatBackend* backend, + Client* client, + IMenuManager* menumgr); + virtual ~GUIChatConsole(); + + // Open the console (height = desired fraction of screen size) + // This doesn't open immediately but initiates an animation. + // You should call isOpenInhibited() before this. + void openConsole(f32 scale); + + bool isOpen() const; + + // Check if the console should not be opened at the moment + // This is to avoid reopening the console immediately after closing + bool isOpenInhibited() const; + // Close the console, equivalent to openConsole(0). + // This doesn't close immediately but initiates an animation. + void closeConsole(); + // Close the console immediately, without animation. + void closeConsoleAtOnce(); + // Set whether to close the console after the user presses enter. + void setCloseOnEnter(bool close) { m_close_on_enter = close; } + + // Return the desired height (fraction of screen size) + // Zero if the console is closed or getting closed + f32 getDesiredHeight() const; + + // Replace actual line when adding the actual to the history (if there is any) + void replaceAndAddToHistory(std::wstring line); + + // Change how the cursor looks + void setCursor( + bool visible, + bool blinking = false, + f32 blink_speed = 1.0, + f32 relative_height = 1.0); + + // Irrlicht draw method + virtual void draw(); + + bool canTakeFocus(gui::IGUIElement* element) { return false; } + + virtual bool OnEvent(const SEvent& event); + + virtual void setVisible(bool visible); + +private: + void reformatConsole(); + void recalculateConsolePosition(); + + // These methods are called by draw + void animate(u32 msec); + void drawBackground(); + void drawText(); + void drawPrompt(); + +private: + ChatBackend* m_chat_backend; + Client* m_client; + IMenuManager* m_menumgr; + + // current screen size + v2u32 m_screensize; + + // used to compute how much time passed since last animate() + u64 m_animate_time_old; + + // should the console be opened or closed? + bool m_open = false; + // should it close after you press enter? + bool m_close_on_enter = false; + // current console height [pixels] + s32 m_height = 0; + // desired height [pixels] + f32 m_desired_height = 0.0f; + // desired height [screen height fraction] + f32 m_desired_height_fraction = 0.0f; + // console open/close animation speed [screen height fraction / second] + f32 m_height_speed = 5.0f; + // if nonzero, opening the console is inhibited [milliseconds] + u32 m_open_inhibited = 0; + + // cursor blink frame (16-bit value) + // cursor is off during [0,32767] and on during [32768,65535] + u32 m_cursor_blink = 0; + // cursor blink speed [on/off toggles / second] + f32 m_cursor_blink_speed = 0.0f; + // cursor height [line height] + f32 m_cursor_height = 0.0f; + + // background texture + video::ITexture *m_background = nullptr; + // background color (including alpha) + video::SColor m_background_color = video::SColor(255, 0, 0, 0); + + // font + gui::IGUIFont *m_font = nullptr; + v2u32 m_fontsize; +}; diff --git a/src/gui/guiEditBoxWithScrollbar.cpp b/src/gui/guiEditBoxWithScrollbar.cpp new file mode 100644 index 000000000..d4d2a0c1c --- /dev/null +++ b/src/gui/guiEditBoxWithScrollbar.cpp @@ -0,0 +1,1524 @@ +// Copyright (C) 2002-2012 Nikolaus Gebhardt +// Modified by Mustapha T. +// This file is part of the "Irrlicht Engine". +// For conditions of distribution and use, see copyright notice in irrlicht.h + +#include "guiEditBoxWithScrollbar.h" + +#include "IGUISkin.h" +#include "IGUIEnvironment.h" +#include "IGUIFont.h" +#include "IVideoDriver.h" +#include "rect.h" +#include "porting.h" +#include "Keycodes.h" + + +/* +todo: +optional scrollbars [done] +ctrl+left/right to select word +double click/ctrl click: word select + drag to select whole words, triple click to select line +optional? dragging selected text +numerical +*/ + + +//! constructor +GUIEditBoxWithScrollBar::GUIEditBoxWithScrollBar(const wchar_t* text, bool border, + IGUIEnvironment* environment, IGUIElement* parent, s32 id, + const core::rect<s32>& rectangle, bool writable, bool has_vscrollbar) + : IGUIEditBox(environment, parent, id, rectangle), m_mouse_marking(false), + m_border(border), m_background(true), m_override_color_enabled(false), m_mark_begin(0), m_mark_end(0), + m_override_color(video::SColor(101, 255, 255, 255)), m_override_font(0), m_last_break_font(0), + m_operator(0), m_blink_start_time(0), m_cursor_pos(0), m_hscroll_pos(0), m_vscroll_pos(0), m_max(0), + m_word_wrap(false), m_multiline(false), m_autoscroll(true), m_passwordbox(false), + m_passwordchar(L'*'), m_halign(EGUIA_UPPERLEFT), m_valign(EGUIA_CENTER), + m_current_text_rect(0, 0, 1, 1), m_frame_rect(rectangle), + m_scrollbar_width(0), m_vscrollbar(NULL), m_writable(writable), + m_bg_color_used(false) +{ +#ifdef _DEBUG + setDebugName("GUIEditBoxWithScrollBar"); +#endif + + + Text = text; + + if (Environment) + m_operator = Environment->getOSOperator(); + + if (m_operator) + m_operator->grab(); + + // this element can be tabbed to + setTabStop(true); + setTabOrder(-1); + + if (has_vscrollbar) { + createVScrollBar(); + } + + calculateFrameRect(); + breakText(); + + calculateScrollPos(); + setWritable(writable); +} + + +//! destructor +GUIEditBoxWithScrollBar::~GUIEditBoxWithScrollBar() +{ + if (m_override_font) + m_override_font->drop(); + + if (m_operator) + m_operator->drop(); + + m_vscrollbar->remove(); +} + + +//! Sets another skin independent font. +void GUIEditBoxWithScrollBar::setOverrideFont(IGUIFont* font) +{ + if (m_override_font == font) + return; + + if (m_override_font) + m_override_font->drop(); + + m_override_font = font; + + if (m_override_font) + m_override_font->grab(); + + breakText(); +} + +//! Gets the override font (if any) +IGUIFont * GUIEditBoxWithScrollBar::getOverrideFont() const +{ + return m_override_font; +} + +//! Get the font which is used right now for drawing +IGUIFont* GUIEditBoxWithScrollBar::getActiveFont() const +{ + if (m_override_font) + return m_override_font; + IGUISkin* skin = Environment->getSkin(); + if (skin) + return skin->getFont(); + return 0; +} + +//! Sets another color for the text. +void GUIEditBoxWithScrollBar::setOverrideColor(video::SColor color) +{ + m_override_color = color; + m_override_color_enabled = true; +} + + +video::SColor GUIEditBoxWithScrollBar::getOverrideColor() const +{ + return m_override_color; +} + + +//! Turns the border on or off +void GUIEditBoxWithScrollBar::setDrawBorder(bool border) +{ + m_border = border; +} + +//! Sets whether to draw the background +void GUIEditBoxWithScrollBar::setDrawBackground(bool draw) +{ + m_background = draw; +} + +//! Sets if the text should use the overide color or the color in the gui skin. +void GUIEditBoxWithScrollBar::enableOverrideColor(bool enable) +{ + m_override_color_enabled = enable; +} + +bool GUIEditBoxWithScrollBar::isOverrideColorEnabled() const +{ + _IRR_IMPLEMENT_MANAGED_MARSHALLING_BUGFIX; + return m_override_color_enabled; +} + +//! Enables or disables word wrap +void GUIEditBoxWithScrollBar::setWordWrap(bool enable) +{ + m_word_wrap = enable; + breakText(); +} + + +void GUIEditBoxWithScrollBar::updateAbsolutePosition() +{ + core::rect<s32> old_absolute_rect(AbsoluteRect); + IGUIElement::updateAbsolutePosition(); + if (old_absolute_rect != AbsoluteRect) { + calculateFrameRect(); + breakText(); + calculateScrollPos(); + } +} + +//! Checks if word wrap is enabled +bool GUIEditBoxWithScrollBar::isWordWrapEnabled() const +{ + _IRR_IMPLEMENT_MANAGED_MARSHALLING_BUGFIX; + return m_word_wrap; +} + + +//! Enables or disables newlines. +void GUIEditBoxWithScrollBar::setMultiLine(bool enable) +{ + m_multiline = enable; +} + + +//! Checks if multi line editing is enabled +bool GUIEditBoxWithScrollBar::isMultiLineEnabled() const +{ + _IRR_IMPLEMENT_MANAGED_MARSHALLING_BUGFIX; + return m_multiline; +} + + +void GUIEditBoxWithScrollBar::setPasswordBox(bool password_box, wchar_t password_char) +{ + m_passwordbox = password_box; + if (m_passwordbox) { + m_passwordchar = password_char; + setMultiLine(false); + setWordWrap(false); + m_broken_text.clear(); + } +} + + +bool GUIEditBoxWithScrollBar::isPasswordBox() const +{ + _IRR_IMPLEMENT_MANAGED_MARSHALLING_BUGFIX; + return m_passwordbox; +} + + +//! Sets text justification +void GUIEditBoxWithScrollBar::setTextAlignment(EGUI_ALIGNMENT horizontal, EGUI_ALIGNMENT vertical) +{ + m_halign = horizontal; + m_valign = vertical; +} + + +//! called if an event happened. +bool GUIEditBoxWithScrollBar::OnEvent(const SEvent& event) +{ + if (isEnabled()) { + switch (event.EventType) + { + case EET_GUI_EVENT: + if (event.GUIEvent.EventType == EGET_ELEMENT_FOCUS_LOST) { + if (event.GUIEvent.Caller == this) { + m_mouse_marking = false; + setTextMarkers(0, 0); + } + } + break; + case EET_KEY_INPUT_EVENT: + if (processKey(event)) + return true; + break; + case EET_MOUSE_INPUT_EVENT: + if (processMouse(event)) + return true; + break; + default: + break; + } + } + + return IGUIElement::OnEvent(event); +} + + +bool GUIEditBoxWithScrollBar::processKey(const SEvent& event) +{ + if (!m_writable) { + return false; + } + + if (!event.KeyInput.PressedDown) + return false; + + bool text_changed = false; + s32 new_mark_begin = m_mark_begin; + s32 new_mark_end = m_mark_end; + + // control shortcut handling + + if (event.KeyInput.Control) { + + // german backlash '\' entered with control + '?' + if (event.KeyInput.Char == '\\') { + inputChar(event.KeyInput.Char); + return true; + } + + switch (event.KeyInput.Key) { + case KEY_KEY_A: + // select all + new_mark_begin = 0; + new_mark_end = Text.size(); + break; + case KEY_KEY_C: + // copy to clipboard + if (!m_passwordbox && m_operator && m_mark_begin != m_mark_end) + { + const s32 realmbgn = m_mark_begin < m_mark_end ? m_mark_begin : m_mark_end; + const s32 realmend = m_mark_begin < m_mark_end ? m_mark_end : m_mark_begin; + + core::stringc s; + s = Text.subString(realmbgn, realmend - realmbgn).c_str(); + m_operator->copyToClipboard(s.c_str()); + } + break; + case KEY_KEY_X: + // cut to the clipboard + if (!m_passwordbox && m_operator && m_mark_begin != m_mark_end) { + const s32 realmbgn = m_mark_begin < m_mark_end ? m_mark_begin : m_mark_end; + const s32 realmend = m_mark_begin < m_mark_end ? m_mark_end : m_mark_begin; + + // copy + core::stringc sc; + sc = Text.subString(realmbgn, realmend - realmbgn).c_str(); + m_operator->copyToClipboard(sc.c_str()); + + if (isEnabled()) + { + // delete + core::stringw s; + s = Text.subString(0, realmbgn); + s.append(Text.subString(realmend, Text.size() - realmend)); + Text = s; + + m_cursor_pos = realmbgn; + new_mark_begin = 0; + new_mark_end = 0; + text_changed = true; + } + } + break; + case KEY_KEY_V: + if (!isEnabled()) + break; + + // paste from the clipboard + if (m_operator) { + const s32 realmbgn = m_mark_begin < m_mark_end ? m_mark_begin : m_mark_end; + const s32 realmend = m_mark_begin < m_mark_end ? m_mark_end : m_mark_begin; + + // add new character + const c8* p = m_operator->getTextFromClipboard(); + if (p) { + if (m_mark_begin == m_mark_end) { + // insert text + core::stringw s = Text.subString(0, m_cursor_pos); + s.append(p); + s.append(Text.subString(m_cursor_pos, Text.size() - m_cursor_pos)); + + if (!m_max || s.size() <= m_max) // thx to Fish FH for fix + { + Text = s; + s = p; + m_cursor_pos += s.size(); + } + } else { + // replace text + + core::stringw s = Text.subString(0, realmbgn); + s.append(p); + s.append(Text.subString(realmend, Text.size() - realmend)); + + if (!m_max || s.size() <= m_max) // thx to Fish FH for fix + { + Text = s; + s = p; + m_cursor_pos = realmbgn + s.size(); + } + } + } + + new_mark_begin = 0; + new_mark_end = 0; + text_changed = true; + } + break; + case KEY_HOME: + // move/highlight to start of text + if (event.KeyInput.Shift) { + new_mark_end = m_cursor_pos; + new_mark_begin = 0; + m_cursor_pos = 0; + } else { + m_cursor_pos = 0; + new_mark_begin = 0; + new_mark_end = 0; + } + break; + case KEY_END: + // move/highlight to end of text + if (event.KeyInput.Shift) { + new_mark_begin = m_cursor_pos; + new_mark_end = Text.size(); + m_cursor_pos = 0; + } else { + m_cursor_pos = Text.size(); + new_mark_begin = 0; + new_mark_end = 0; + } + break; + default: + return false; + } + } + // default keyboard handling + else + switch (event.KeyInput.Key) { + case KEY_END: + { + s32 p = Text.size(); + if (m_word_wrap || m_multiline) { + p = getLineFromPos(m_cursor_pos); + p = m_broken_text_positions[p] + (s32)m_broken_text[p].size(); + if (p > 0 && (Text[p - 1] == L'\r' || Text[p - 1] == L'\n')) + p -= 1; + } + + if (event.KeyInput.Shift) { + if (m_mark_begin == m_mark_end) + new_mark_begin = m_cursor_pos; + + new_mark_end = p; + } else { + new_mark_begin = 0; + new_mark_end = 0; + } + m_cursor_pos = p; + m_blink_start_time = porting::getTimeMs(); + } + break; + case KEY_HOME: + { + + s32 p = 0; + if (m_word_wrap || m_multiline) { + p = getLineFromPos(m_cursor_pos); + p = m_broken_text_positions[p]; + } + + if (event.KeyInput.Shift) { + if (m_mark_begin == m_mark_end) + new_mark_begin = m_cursor_pos; + new_mark_end = p; + } else { + new_mark_begin = 0; + new_mark_end = 0; + } + m_cursor_pos = p; + m_blink_start_time = porting::getTimeMs(); + } + break; + case KEY_RETURN: + if (m_multiline) { + inputChar(L'\n'); + } else { + calculateScrollPos(); + sendGuiEvent(EGET_EDITBOX_ENTER); + } + return true; + case KEY_LEFT: + + if (event.KeyInput.Shift) { + if (m_cursor_pos > 0) { + if (m_mark_begin == m_mark_end) + new_mark_begin = m_cursor_pos; + + new_mark_end = m_cursor_pos - 1; + } + } else { + new_mark_begin = 0; + new_mark_end = 0; + } + + if (m_cursor_pos > 0) + m_cursor_pos--; + m_blink_start_time = porting::getTimeMs(); + break; + + case KEY_RIGHT: + if (event.KeyInput.Shift) { + if (Text.size() > (u32)m_cursor_pos) { + if (m_mark_begin == m_mark_end) + new_mark_begin = m_cursor_pos; + + new_mark_end = m_cursor_pos + 1; + } + } else { + new_mark_begin = 0; + new_mark_end = 0; + } + + if (Text.size() > (u32)m_cursor_pos) + m_cursor_pos++; + m_blink_start_time = porting::getTimeMs(); + break; + case KEY_UP: + if (m_multiline || (m_word_wrap && m_broken_text.size() > 1)) { + s32 lineNo = getLineFromPos(m_cursor_pos); + s32 mb = (m_mark_begin == m_mark_end) ? m_cursor_pos : (m_mark_begin > m_mark_end ? m_mark_begin : m_mark_end); + if (lineNo > 0) { + s32 cp = m_cursor_pos - m_broken_text_positions[lineNo]; + if ((s32)m_broken_text[lineNo - 1].size() < cp) + m_cursor_pos = m_broken_text_positions[lineNo - 1] + core::max_((u32)1, m_broken_text[lineNo - 1].size()) - 1; + else + m_cursor_pos = m_broken_text_positions[lineNo - 1] + cp; + } + + if (event.KeyInput.Shift) { + new_mark_begin = mb; + new_mark_end = m_cursor_pos; + } else { + new_mark_begin = 0; + new_mark_end = 0; + } + } else { + return false; + } + break; + case KEY_DOWN: + if (m_multiline || (m_word_wrap && m_broken_text.size() > 1)) { + s32 lineNo = getLineFromPos(m_cursor_pos); + s32 mb = (m_mark_begin == m_mark_end) ? m_cursor_pos : (m_mark_begin < m_mark_end ? m_mark_begin : m_mark_end); + if (lineNo < (s32)m_broken_text.size() - 1) + { + s32 cp = m_cursor_pos - m_broken_text_positions[lineNo]; + if ((s32)m_broken_text[lineNo + 1].size() < cp) + m_cursor_pos = m_broken_text_positions[lineNo + 1] + core::max_((u32)1, m_broken_text[lineNo + 1].size()) - 1; + else + m_cursor_pos = m_broken_text_positions[lineNo + 1] + cp; + } + + if (event.KeyInput.Shift) { + new_mark_begin = mb; + new_mark_end = m_cursor_pos; + } else { + new_mark_begin = 0; + new_mark_end = 0; + } + + } else { + return false; + } + break; + + case KEY_BACK: + if (!isEnabled()) + break; + + if (Text.size()) { + core::stringw s; + + if (m_mark_begin != m_mark_end) { + // delete marked text + const s32 realmbgn = m_mark_begin < m_mark_end ? m_mark_begin : m_mark_end; + const s32 realmend = m_mark_begin < m_mark_end ? m_mark_end : m_mark_begin; + + s = Text.subString(0, realmbgn); + s.append(Text.subString(realmend, Text.size() - realmend)); + Text = s; + + m_cursor_pos = realmbgn; + } else { + // delete text behind cursor + if (m_cursor_pos > 0) + s = Text.subString(0, m_cursor_pos - 1); + else + s = L""; + s.append(Text.subString(m_cursor_pos, Text.size() - m_cursor_pos)); + Text = s; + --m_cursor_pos; + } + + if (m_cursor_pos < 0) + m_cursor_pos = 0; + m_blink_start_time = porting::getTimeMs(); // os::Timer::getTime(); + new_mark_begin = 0; + new_mark_end = 0; + text_changed = true; + } + break; + case KEY_DELETE: + if (!isEnabled()) + break; + + if (Text.size() != 0) { + core::stringw s; + + if (m_mark_begin != m_mark_end) { + // delete marked text + const s32 realmbgn = m_mark_begin < m_mark_end ? m_mark_begin : m_mark_end; + const s32 realmend = m_mark_begin < m_mark_end ? m_mark_end : m_mark_begin; + + s = Text.subString(0, realmbgn); + s.append(Text.subString(realmend, Text.size() - realmend)); + Text = s; + + m_cursor_pos = realmbgn; + } else { + // delete text before cursor + s = Text.subString(0, m_cursor_pos); + s.append(Text.subString(m_cursor_pos + 1, Text.size() - m_cursor_pos - 1)); + Text = s; + } + + if (m_cursor_pos > (s32)Text.size()) + m_cursor_pos = (s32)Text.size(); + + m_blink_start_time = porting::getTimeMs(); // os::Timer::getTime(); + new_mark_begin = 0; + new_mark_end = 0; + text_changed = true; + } + break; + + case KEY_ESCAPE: + case KEY_TAB: + case KEY_SHIFT: + case KEY_F1: + case KEY_F2: + case KEY_F3: + case KEY_F4: + case KEY_F5: + case KEY_F6: + case KEY_F7: + case KEY_F8: + case KEY_F9: + case KEY_F10: + case KEY_F11: + case KEY_F12: + case KEY_F13: + case KEY_F14: + case KEY_F15: + case KEY_F16: + case KEY_F17: + case KEY_F18: + case KEY_F19: + case KEY_F20: + case KEY_F21: + case KEY_F22: + case KEY_F23: + case KEY_F24: + // ignore these keys + return false; + + default: + inputChar(event.KeyInput.Char); + return true; + } + + // Set new text markers + setTextMarkers(new_mark_begin, new_mark_end); + + // break the text if it has changed + if (text_changed) { + breakText(); + calculateScrollPos(); + sendGuiEvent(EGET_EDITBOX_CHANGED); + } + else + { + calculateScrollPos(); + } + + return true; +} + + +//! draws the element and its children +void GUIEditBoxWithScrollBar::draw() +{ + if (!IsVisible) + return; + + const bool focus = Environment->hasFocus(this); + + IGUISkin* skin = Environment->getSkin(); + if (!skin) + return; + + video::SColor default_bg_color; + video::SColor bg_color; + + default_bg_color = m_writable ? skin->getColor(EGDC_WINDOW) : video::SColor(0); + bg_color = m_bg_color_used ? m_bg_color : default_bg_color; + + if (!m_border && m_background) { + skin->draw2DRectangle(this, bg_color, AbsoluteRect, &AbsoluteClippingRect); + } + + // draw the border + + if (m_border) { + + if (m_writable) { + skin->draw3DSunkenPane(this, bg_color, false, m_background, + AbsoluteRect, &AbsoluteClippingRect); + } + + calculateFrameRect(); + } + + core::rect<s32> local_clip_rect = m_frame_rect; + local_clip_rect.clipAgainst(AbsoluteClippingRect); + + // draw the text + + IGUIFont* font = getActiveFont(); + + s32 cursor_line = 0; + s32 charcursorpos = 0; + + if (font) { + if (m_last_break_font != font) { + breakText(); + } + + // calculate cursor pos + + core::stringw *txt_line = &Text; + s32 start_pos = 0; + + core::stringw s, s2; + + // get mark position + const bool ml = (!m_passwordbox && (m_word_wrap || m_multiline)); + const s32 realmbgn = m_mark_begin < m_mark_end ? m_mark_begin : m_mark_end; + const s32 realmend = m_mark_begin < m_mark_end ? m_mark_end : m_mark_begin; + const s32 hline_start = ml ? getLineFromPos(realmbgn) : 0; + const s32 hline_count = ml ? getLineFromPos(realmend) - hline_start + 1 : 1; + const s32 line_count = ml ? m_broken_text.size() : 1; + + // Save the override color information. + // Then, alter it if the edit box is disabled. + const bool prevOver = m_override_color_enabled; + const video::SColor prevColor = m_override_color; + + if (Text.size()) { + if (!isEnabled() && !m_override_color_enabled) { + m_override_color_enabled = true; + m_override_color = skin->getColor(EGDC_GRAY_TEXT); + } + + for (s32 i = 0; i < line_count; ++i) { + setTextRect(i); + + // clipping test - don't draw anything outside the visible area + core::rect<s32> c = local_clip_rect; + c.clipAgainst(m_current_text_rect); + if (!c.isValid()) + continue; + + // get current line + if (m_passwordbox) { + if (m_broken_text.size() != 1) { + m_broken_text.clear(); + m_broken_text.push_back(core::stringw()); + } + if (m_broken_text[0].size() != Text.size()){ + m_broken_text[0] = Text; + for (u32 q = 0; q < Text.size(); ++q) + { + m_broken_text[0][q] = m_passwordchar; + } + } + txt_line = &m_broken_text[0]; + start_pos = 0; + } else { + txt_line = ml ? &m_broken_text[i] : &Text; + start_pos = ml ? m_broken_text_positions[i] : 0; + } + + + // draw normal text + font->draw(txt_line->c_str(), m_current_text_rect, + m_override_color_enabled ? m_override_color : skin->getColor(EGDC_BUTTON_TEXT), + false, true, &local_clip_rect); + + // draw mark and marked text + if (focus && m_mark_begin != m_mark_end && i >= hline_start && i < hline_start + hline_count) { + + s32 mbegin = 0, mend = 0; + s32 lineStartPos = 0, lineEndPos = txt_line->size(); + + if (i == hline_start) { + // highlight start is on this line + s = txt_line->subString(0, realmbgn - start_pos); + mbegin = font->getDimension(s.c_str()).Width; + + // deal with kerning + mbegin += font->getKerningWidth( + &((*txt_line)[realmbgn - start_pos]), + realmbgn - start_pos > 0 ? &((*txt_line)[realmbgn - start_pos - 1]) : 0); + + lineStartPos = realmbgn - start_pos; + } + if (i == hline_start + hline_count - 1) { + // highlight end is on this line + s2 = txt_line->subString(0, realmend - start_pos); + mend = font->getDimension(s2.c_str()).Width; + lineEndPos = (s32)s2.size(); + } else { + mend = font->getDimension(txt_line->c_str()).Width; + } + + + m_current_text_rect.UpperLeftCorner.X += mbegin; + m_current_text_rect.LowerRightCorner.X = m_current_text_rect.UpperLeftCorner.X + mend - mbegin; + + + // draw mark + skin->draw2DRectangle(this, skin->getColor(EGDC_HIGH_LIGHT), m_current_text_rect, &local_clip_rect); + + // draw marked text + s = txt_line->subString(lineStartPos, lineEndPos - lineStartPos); + + if (s.size()) + font->draw(s.c_str(), m_current_text_rect, + m_override_color_enabled ? m_override_color : skin->getColor(EGDC_HIGH_LIGHT_TEXT), + false, true, &local_clip_rect); + + } + } + + // Return the override color information to its previous settings. + m_override_color_enabled = prevOver; + m_override_color = prevColor; + } + + // draw cursor + if (IsEnabled && m_writable) { + if (m_word_wrap || m_multiline) { + cursor_line = getLineFromPos(m_cursor_pos); + txt_line = &m_broken_text[cursor_line]; + start_pos = m_broken_text_positions[cursor_line]; + } + s = txt_line->subString(0, m_cursor_pos - start_pos); + charcursorpos = font->getDimension(s.c_str()).Width + + font->getKerningWidth(L"_", m_cursor_pos - start_pos > 0 ? &((*txt_line)[m_cursor_pos - start_pos - 1]) : 0); + + if (focus && (porting::getTimeMs() - m_blink_start_time) % 700 < 350) { + setTextRect(cursor_line); + m_current_text_rect.UpperLeftCorner.X += charcursorpos; + + font->draw(L"_", m_current_text_rect, + m_override_color_enabled ? m_override_color : skin->getColor(EGDC_BUTTON_TEXT), + false, true, &local_clip_rect); + } + } + } + + // draw children + IGUIElement::draw(); +} + + +//! Sets the new caption of this element. +void GUIEditBoxWithScrollBar::setText(const wchar_t* text) +{ + Text = text; + if (u32(m_cursor_pos) > Text.size()) + m_cursor_pos = Text.size(); + m_hscroll_pos = 0; + breakText(); +} + + +//! Enables or disables automatic scrolling with cursor position +//! \param enable: If set to true, the text will move around with the cursor position +void GUIEditBoxWithScrollBar::setAutoScroll(bool enable) +{ + m_autoscroll = enable; +} + + +//! Checks to see if automatic scrolling is enabled +//! \return true if automatic scrolling is enabled, false if not +bool GUIEditBoxWithScrollBar::isAutoScrollEnabled() const +{ + _IRR_IMPLEMENT_MANAGED_MARSHALLING_BUGFIX; + return m_autoscroll; +} + + +//! Gets the area of the text in the edit box +//! \return Returns the size in pixels of the text +core::dimension2du GUIEditBoxWithScrollBar::getTextDimension() +{ + core::rect<s32> ret; + + setTextRect(0); + ret = m_current_text_rect; + + for (u32 i = 1; i < m_broken_text.size(); ++i) { + setTextRect(i); + ret.addInternalPoint(m_current_text_rect.UpperLeftCorner); + ret.addInternalPoint(m_current_text_rect.LowerRightCorner); + } + + return core::dimension2du(ret.getSize()); +} + + +//! Sets the maximum amount of characters which may be entered in the box. +//! \param max: Maximum amount of characters. If 0, the character amount is +//! infinity. +void GUIEditBoxWithScrollBar::setMax(u32 max) +{ + m_max = max; + + if (Text.size() > m_max && m_max != 0) + Text = Text.subString(0, m_max); +} + + +//! Returns maximum amount of characters, previously set by setMax(); +u32 GUIEditBoxWithScrollBar::getMax() const +{ + return m_max; +} + + +bool GUIEditBoxWithScrollBar::processMouse(const SEvent& event) +{ + switch (event.MouseInput.Event) + { + case irr::EMIE_LMOUSE_LEFT_UP: + if (Environment->hasFocus(this)) { + m_cursor_pos = getCursorPos(event.MouseInput.X, event.MouseInput.Y); + if (m_mouse_marking) { + setTextMarkers(m_mark_begin, m_cursor_pos); + } + m_mouse_marking = false; + calculateScrollPos(); + return true; + } + break; + case irr::EMIE_MOUSE_MOVED: + { + if (m_mouse_marking) { + m_cursor_pos = getCursorPos(event.MouseInput.X, event.MouseInput.Y); + setTextMarkers(m_mark_begin, m_cursor_pos); + calculateScrollPos(); + return true; + } + } + break; + case EMIE_LMOUSE_PRESSED_DOWN: + + if (!Environment->hasFocus(this)) { + m_blink_start_time = porting::getTimeMs(); + m_mouse_marking = true; + m_cursor_pos = getCursorPos(event.MouseInput.X, event.MouseInput.Y); + setTextMarkers(m_cursor_pos, m_cursor_pos); + calculateScrollPos(); + return true; + } else { + if (!AbsoluteClippingRect.isPointInside( + core::position2d<s32>(event.MouseInput.X, event.MouseInput.Y))) { + return false; + } else { + // move cursor + m_cursor_pos = getCursorPos(event.MouseInput.X, event.MouseInput.Y); + + s32 newMarkBegin = m_mark_begin; + if (!m_mouse_marking) + newMarkBegin = m_cursor_pos; + + m_mouse_marking = true; + setTextMarkers(newMarkBegin, m_cursor_pos); + calculateScrollPos(); + return true; + } + } + default: + break; + } + + return false; +} + + +s32 GUIEditBoxWithScrollBar::getCursorPos(s32 x, s32 y) +{ + IGUIFont* font = getActiveFont(); + + const u32 line_count = (m_word_wrap || m_multiline) ? m_broken_text.size() : 1; + + core::stringw *txt_line = 0; + s32 start_pos = 0; + x += 3; + + for (u32 i = 0; i < line_count; ++i) { + setTextRect(i); + if (i == 0 && y < m_current_text_rect.UpperLeftCorner.Y) + y = m_current_text_rect.UpperLeftCorner.Y; + if (i == line_count - 1 && y > m_current_text_rect.LowerRightCorner.Y) + y = m_current_text_rect.LowerRightCorner.Y; + + // is it inside this region? + if (y >= m_current_text_rect.UpperLeftCorner.Y && y <= m_current_text_rect.LowerRightCorner.Y) { + // we've found the clicked line + txt_line = (m_word_wrap || m_multiline) ? &m_broken_text[i] : &Text; + start_pos = (m_word_wrap || m_multiline) ? m_broken_text_positions[i] : 0; + break; + } + } + + if (x < m_current_text_rect.UpperLeftCorner.X) + x = m_current_text_rect.UpperLeftCorner.X; + + if (!txt_line) + return 0; + + s32 idx = font->getCharacterFromPos(txt_line->c_str(), x - m_current_text_rect.UpperLeftCorner.X); + + // click was on or left of the line + if (idx != -1) + return idx + start_pos; + + // click was off the right edge of the line, go to end. + return txt_line->size() + start_pos; +} + + +//! Breaks the single text line. +void GUIEditBoxWithScrollBar::breakText() +{ + if ((!m_word_wrap && !m_multiline)) + return; + + m_broken_text.clear(); // need to reallocate :/ + m_broken_text_positions.clear(); + + IGUIFont* font = getActiveFont(); + if (!font) + return; + + m_last_break_font = font; + + core::stringw line; + core::stringw word; + core::stringw whitespace; + s32 last_line_start = 0; + s32 size = Text.size(); + s32 length = 0; + s32 el_width = RelativeRect.getWidth() - 6; + wchar_t c; + + for (s32 i = 0; i < size; ++i) { + c = Text[i]; + bool line_break = false; + + if (c == L'\r') { // Mac or Windows breaks + + line_break = true; + c = 0; + if (Text[i + 1] == L'\n') { // Windows breaks + // TODO: I (Michael) think that we shouldn't change the text given by the user for whatever reason. + // Instead rework the cursor positioning to be able to handle this (but not in stable release + // branch as users might already expect this behavior). + Text.erase(i + 1); + --size; + if (m_cursor_pos > i) + --m_cursor_pos; + } + } else if (c == L'\n') { // Unix breaks + line_break = true; + c = 0; + } + + // don't break if we're not a multi-line edit box + if (!m_multiline) + line_break = false; + + if (c == L' ' || c == 0 || i == (size - 1)) { + // here comes the next whitespace, look if + // we can break the last word to the next line + // We also break whitespace, otherwise cursor would vanish beside the right border. + s32 whitelgth = font->getDimension(whitespace.c_str()).Width; + s32 worldlgth = font->getDimension(word.c_str()).Width; + + if (m_word_wrap && length + worldlgth + whitelgth > el_width && line.size() > 0) { + // break to next line + length = worldlgth; + m_broken_text.push_back(line); + m_broken_text_positions.push_back(last_line_start); + last_line_start = i - (s32)word.size(); + line = word; + } else { + // add word to line + line += whitespace; + line += word; + length += whitelgth + worldlgth; + } + + word = L""; + whitespace = L""; + + + if (c) + whitespace += c; + + // compute line break + if (line_break) { + line += whitespace; + line += word; + m_broken_text.push_back(line); + m_broken_text_positions.push_back(last_line_start); + last_line_start = i + 1; + line = L""; + word = L""; + whitespace = L""; + length = 0; + } + } else { + // yippee this is a word.. + word += c; + } + } + + line += whitespace; + line += word; + m_broken_text.push_back(line); + m_broken_text_positions.push_back(last_line_start); +} + +// TODO: that function does interpret VAlign according to line-index (indexed line is placed on top-center-bottom) +// but HAlign according to line-width (pixels) and not by row. +// Intuitively I suppose HAlign handling is better as VScrollPos should handle the line-scrolling. +// But please no one change this without also rewriting (and this time fucking testing!!!) autoscrolling (I noticed this when fixing the old autoscrolling). +void GUIEditBoxWithScrollBar::setTextRect(s32 line) +{ + if (line < 0) + return; + + IGUIFont* font = getActiveFont(); + if (!font) + return; + + core::dimension2du d; + + // get text dimension + const u32 line_count = (m_word_wrap || m_multiline) ? m_broken_text.size() : 1; + if (m_word_wrap || m_multiline) { + d = font->getDimension(m_broken_text[line].c_str()); + } else { + d = font->getDimension(Text.c_str()); + d.Height = AbsoluteRect.getHeight(); + } + d.Height += font->getKerningHeight(); + + // justification + switch (m_halign) { + case EGUIA_CENTER: + // align to h centre + m_current_text_rect.UpperLeftCorner.X = (m_frame_rect.getWidth() / 2) - (d.Width / 2); + m_current_text_rect.LowerRightCorner.X = (m_frame_rect.getWidth() / 2) + (d.Width / 2); + break; + case EGUIA_LOWERRIGHT: + // align to right edge + m_current_text_rect.UpperLeftCorner.X = m_frame_rect.getWidth() - d.Width; + m_current_text_rect.LowerRightCorner.X = m_frame_rect.getWidth(); + break; + default: + // align to left edge + m_current_text_rect.UpperLeftCorner.X = 0; + m_current_text_rect.LowerRightCorner.X = d.Width; + + } + + switch (m_valign) { + case EGUIA_CENTER: + // align to v centre + m_current_text_rect.UpperLeftCorner.Y = + (m_frame_rect.getHeight() / 2) - (line_count*d.Height) / 2 + d.Height*line; + break; + case EGUIA_LOWERRIGHT: + // align to bottom edge + m_current_text_rect.UpperLeftCorner.Y = + m_frame_rect.getHeight() - line_count*d.Height + d.Height*line; + break; + default: + // align to top edge + m_current_text_rect.UpperLeftCorner.Y = d.Height*line; + break; + } + + m_current_text_rect.UpperLeftCorner.X -= m_hscroll_pos; + m_current_text_rect.LowerRightCorner.X -= m_hscroll_pos; + m_current_text_rect.UpperLeftCorner.Y -= m_vscroll_pos; + m_current_text_rect.LowerRightCorner.Y = m_current_text_rect.UpperLeftCorner.Y + d.Height; + + m_current_text_rect += m_frame_rect.UpperLeftCorner; +} + + +s32 GUIEditBoxWithScrollBar::getLineFromPos(s32 pos) +{ + if (!m_word_wrap && !m_multiline) + return 0; + + s32 i = 0; + while (i < (s32)m_broken_text_positions.size()) { + if (m_broken_text_positions[i] > pos) + return i - 1; + ++i; + } + return (s32)m_broken_text_positions.size() - 1; +} + + +void GUIEditBoxWithScrollBar::inputChar(wchar_t c) +{ + if (!isEnabled()) + return; + + if (c != 0) { + if (Text.size() < m_max || m_max == 0) { + core::stringw s; + + if (m_mark_begin != m_mark_end) { + // replace marked text + const s32 realmbgn = m_mark_begin < m_mark_end ? m_mark_begin : m_mark_end; + const s32 realmend = m_mark_begin < m_mark_end ? m_mark_end : m_mark_begin; + + s = Text.subString(0, realmbgn); + s.append(c); + s.append(Text.subString(realmend, Text.size() - realmend)); + Text = s; + m_cursor_pos = realmbgn + 1; + } else { + // add new character + s = Text.subString(0, m_cursor_pos); + s.append(c); + s.append(Text.subString(m_cursor_pos, Text.size() - m_cursor_pos)); + Text = s; + ++m_cursor_pos; + } + + m_blink_start_time = porting::getTimeMs(); + setTextMarkers(0, 0); + } + } + breakText(); + calculateScrollPos(); + sendGuiEvent(EGET_EDITBOX_CHANGED); +} + +// calculate autoscroll +void GUIEditBoxWithScrollBar::calculateScrollPos() +{ + if (!m_autoscroll) + return; + + IGUISkin* skin = Environment->getSkin(); + if (!skin) + return; + IGUIFont* font = m_override_font ? m_override_font : skin->getFont(); + if (!font) + return; + + s32 curs_line = getLineFromPos(m_cursor_pos); + if (curs_line < 0) + return; + setTextRect(curs_line); + const bool has_broken_text = m_multiline || m_word_wrap; + + // Check horizonal scrolling + // NOTE: Calculations different to vertical scrolling because setTextRect interprets VAlign relative to line but HAlign not relative to row + { + // get cursor position + IGUIFont* font = getActiveFont(); + if (!font) + return; + + // get cursor area + irr::u32 cursor_width = font->getDimension(L"_").Width; + core::stringw *txt_line = has_broken_text ? &m_broken_text[curs_line] : &Text; + s32 cpos = has_broken_text ? m_cursor_pos - m_broken_text_positions[curs_line] : m_cursor_pos; // column + s32 cstart = font->getDimension(txt_line->subString(0, cpos).c_str()).Width; // pixels from text-start + s32 cend = cstart + cursor_width; + s32 txt_width = font->getDimension(txt_line->c_str()).Width; + + if (txt_width < m_frame_rect.getWidth()) { + // TODO: Needs a clean left and right gap removal depending on HAlign, similar to vertical scrolling tests for top/bottom. + // This check just fixes the case where it was most noticable (text smaller than clipping area). + + m_hscroll_pos = 0; + setTextRect(curs_line); + } + + if (m_current_text_rect.UpperLeftCorner.X + cstart < m_frame_rect.UpperLeftCorner.X) { + // cursor to the left of the clipping area + m_hscroll_pos -= m_frame_rect.UpperLeftCorner.X - (m_current_text_rect.UpperLeftCorner.X + cstart); + setTextRect(curs_line); + + // TODO: should show more characters to the left when we're scrolling left + // and the cursor reaches the border. + } else if (m_current_text_rect.UpperLeftCorner.X + cend > m_frame_rect.LowerRightCorner.X) { + // cursor to the right of the clipping area + m_hscroll_pos += (m_current_text_rect.UpperLeftCorner.X + cend) - m_frame_rect.LowerRightCorner.X; + setTextRect(curs_line); + } + } + + // calculate vertical scrolling + if (has_broken_text) { + irr::u32 line_height = font->getDimension(L"A").Height + font->getKerningHeight(); + // only up to 1 line fits? + if (line_height >= (irr::u32)m_frame_rect.getHeight()) { + m_vscroll_pos = 0; + setTextRect(curs_line); + s32 unscrolledPos = m_current_text_rect.UpperLeftCorner.Y; + s32 pivot = m_frame_rect.UpperLeftCorner.Y; + switch (m_valign) { + case EGUIA_CENTER: + pivot += m_frame_rect.getHeight() / 2; + unscrolledPos += line_height / 2; + break; + case EGUIA_LOWERRIGHT: + pivot += m_frame_rect.getHeight(); + unscrolledPos += line_height; + break; + default: + break; + } + m_vscroll_pos = unscrolledPos - pivot; + setTextRect(curs_line); + } else { + // First 2 checks are necessary when people delete lines + setTextRect(0); + if (m_current_text_rect.UpperLeftCorner.Y > m_frame_rect.UpperLeftCorner.Y && m_valign != EGUIA_LOWERRIGHT) { + // first line is leaving a gap on top + m_vscroll_pos = 0; + } else if (m_valign != EGUIA_UPPERLEFT) { + u32 lastLine = m_broken_text_positions.empty() ? 0 : m_broken_text_positions.size() - 1; + setTextRect(lastLine); + if (m_current_text_rect.LowerRightCorner.Y < m_frame_rect.LowerRightCorner.Y) + { + // last line is leaving a gap on bottom + m_vscroll_pos -= m_frame_rect.LowerRightCorner.Y - m_current_text_rect.LowerRightCorner.Y; + } + } + + setTextRect(curs_line); + if (m_current_text_rect.UpperLeftCorner.Y < m_frame_rect.UpperLeftCorner.Y) { + // text above valid area + m_vscroll_pos -= m_frame_rect.UpperLeftCorner.Y - m_current_text_rect.UpperLeftCorner.Y; + setTextRect(curs_line); + } else if (m_current_text_rect.LowerRightCorner.Y > m_frame_rect.LowerRightCorner.Y){ + // text below valid area + m_vscroll_pos += m_current_text_rect.LowerRightCorner.Y - m_frame_rect.LowerRightCorner.Y; + setTextRect(curs_line); + } + } + } + + if (m_vscrollbar) { + m_vscrollbar->setPos(m_vscroll_pos); + } +} + +void GUIEditBoxWithScrollBar::calculateFrameRect() +{ + m_frame_rect = AbsoluteRect; + + + IGUISkin *skin = 0; + if (Environment) + skin = Environment->getSkin(); + if (m_border && skin) { + m_frame_rect.UpperLeftCorner.X += skin->getSize(EGDS_TEXT_DISTANCE_X) + 1; + m_frame_rect.UpperLeftCorner.Y += skin->getSize(EGDS_TEXT_DISTANCE_Y) + 1; + m_frame_rect.LowerRightCorner.X -= skin->getSize(EGDS_TEXT_DISTANCE_X) + 1; + m_frame_rect.LowerRightCorner.Y -= skin->getSize(EGDS_TEXT_DISTANCE_Y) + 1; + } + + updateVScrollBar(); +} + +//! set text markers +void GUIEditBoxWithScrollBar::setTextMarkers(s32 begin, s32 end) +{ + if (begin != m_mark_begin || end != m_mark_end) { + m_mark_begin = begin; + m_mark_end = end; + sendGuiEvent(EGET_EDITBOX_MARKING_CHANGED); + } +} + +//! send some gui event to parent +void GUIEditBoxWithScrollBar::sendGuiEvent(EGUI_EVENT_TYPE type) +{ + if (Parent) { + SEvent e; + e.EventType = EET_GUI_EVENT; + e.GUIEvent.Caller = this; + e.GUIEvent.Element = 0; + e.GUIEvent.EventType = type; + + Parent->OnEvent(e); + } +} + +//! create a vertical scroll bar +void GUIEditBoxWithScrollBar::createVScrollBar() +{ + IGUISkin *skin = 0; + if (Environment) + skin = Environment->getSkin(); + + m_scrollbar_width = skin ? skin->getSize(gui::EGDS_SCROLLBAR_SIZE) : 16; + + irr::core::rect<s32> scrollbarrect = m_frame_rect; + scrollbarrect.UpperLeftCorner.X += m_frame_rect.getWidth() - m_scrollbar_width; + m_vscrollbar = Environment->addScrollBar(false, scrollbarrect, getParent(), getID()); + m_vscrollbar->setVisible(false); + m_vscrollbar->setSmallStep(1); + m_vscrollbar->setLargeStep(1); +} + +void GUIEditBoxWithScrollBar::updateVScrollBar() +{ + if (!m_vscrollbar) { + return; + } + + // OnScrollBarChanged(...) + if (m_vscrollbar->getPos() != m_vscroll_pos) { + s32 deltaScrollY = m_vscrollbar->getPos() - m_vscroll_pos; + m_current_text_rect.UpperLeftCorner.Y -= deltaScrollY; + m_current_text_rect.LowerRightCorner.Y -= deltaScrollY; + + s32 scrollymax = getTextDimension().Height - m_frame_rect.getHeight(); + if (scrollymax != m_vscrollbar->getMax()) { + // manage a newline or a deleted line + m_vscrollbar->setMax(scrollymax); + calculateScrollPos(); + } else { + // manage a newline or a deleted line + m_vscroll_pos = m_vscrollbar->getPos(); + } + } + + // check if a vertical scrollbar is needed ? + if (getTextDimension().Height > (u32) m_frame_rect.getHeight()) { + m_frame_rect.LowerRightCorner.X -= m_scrollbar_width; + + s32 scrollymax = getTextDimension().Height - m_frame_rect.getHeight(); + if (scrollymax != m_vscrollbar->getMax()) { + m_vscrollbar->setMax(scrollymax); + } + + if (!m_vscrollbar->isVisible()) { + m_vscrollbar->setVisible(true); + } + } else { + if (m_vscrollbar->isVisible()) + { + m_vscrollbar->setVisible(false); + m_vscroll_pos = 0; + m_vscrollbar->setPos(0); + m_vscrollbar->setMax(1); + } + } + + +} + +//! set true if this editbox is writable +void GUIEditBoxWithScrollBar::setWritable(bool writable) +{ + m_writable = writable; +} + +//! Change the background color +void GUIEditBoxWithScrollBar::setBackgroundColor(const video::SColor &bg_color) +{ + m_bg_color = bg_color; + m_bg_color_used = true; +} + +//! Writes attributes of the element. +void GUIEditBoxWithScrollBar::serializeAttributes(io::IAttributes* out, io::SAttributeReadWriteOptions* options = 0) const +{ + // IGUIEditBox::serializeAttributes(out,options); + + out->addBool("Border", m_border); + out->addBool("Background", m_background); + out->addBool("OverrideColorEnabled", m_override_color_enabled); + out->addColor("OverrideColor", m_override_color); + // out->addFont("OverrideFont", OverrideFont); + out->addInt("MaxChars", m_max); + out->addBool("WordWrap", m_word_wrap); + out->addBool("MultiLine", m_multiline); + out->addBool("AutoScroll", m_autoscroll); + out->addBool("PasswordBox", m_passwordbox); + core::stringw ch = L" "; + ch[0] = m_passwordchar; + out->addString("PasswordChar", ch.c_str()); + out->addEnum("HTextAlign", m_halign, GUIAlignmentNames); + out->addEnum("VTextAlign", m_valign, GUIAlignmentNames); + out->addBool("Writable", m_writable); + + IGUIEditBox::serializeAttributes(out, options); +} + + +//! Reads attributes of the element +void GUIEditBoxWithScrollBar::deserializeAttributes(io::IAttributes* in, io::SAttributeReadWriteOptions* options = 0) +{ + IGUIEditBox::deserializeAttributes(in, options); + + setDrawBorder(in->getAttributeAsBool("Border")); + setDrawBackground(in->getAttributeAsBool("Background")); + setOverrideColor(in->getAttributeAsColor("OverrideColor")); + enableOverrideColor(in->getAttributeAsBool("OverrideColorEnabled")); + setMax(in->getAttributeAsInt("MaxChars")); + setWordWrap(in->getAttributeAsBool("WordWrap")); + setMultiLine(in->getAttributeAsBool("MultiLine")); + setAutoScroll(in->getAttributeAsBool("AutoScroll")); + core::stringw ch = in->getAttributeAsStringW("PasswordChar"); + + if (!ch.size()) + setPasswordBox(in->getAttributeAsBool("PasswordBox")); + else + setPasswordBox(in->getAttributeAsBool("PasswordBox"), ch[0]); + + setTextAlignment((EGUI_ALIGNMENT)in->getAttributeAsEnumeration("HTextAlign", GUIAlignmentNames), + (EGUI_ALIGNMENT)in->getAttributeAsEnumeration("VTextAlign", GUIAlignmentNames)); + + // setOverrideFont(in->getAttributeAsFont("OverrideFont")); + setWritable(in->getAttributeAsBool("Writable")); +} diff --git a/src/gui/guiEditBoxWithScrollbar.h b/src/gui/guiEditBoxWithScrollbar.h new file mode 100644 index 000000000..cca2f6536 --- /dev/null +++ b/src/gui/guiEditBoxWithScrollbar.h @@ -0,0 +1,192 @@ +// Copyright (C) 2002-2012 Nikolaus Gebhardt, Modified by Mustapha Tachouct +// This file is part of the "Irrlicht Engine". +// For conditions of distribution and use, see copyright notice in irrlicht.h + +#ifndef GUIEDITBOXWITHSCROLLBAR_HEADER +#define GUIEDITBOXWITHSCROLLBAR_HEADER + +#include "IGUIEditBox.h" +#include "IOSOperator.h" +#include "IGUIScrollBar.h" +#include <vector> + +using namespace irr; +using namespace irr::gui; + +class GUIEditBoxWithScrollBar : public IGUIEditBox +{ +public: + + //! constructor + GUIEditBoxWithScrollBar(const wchar_t* text, bool border, IGUIEnvironment* environment, + IGUIElement* parent, s32 id, const core::rect<s32>& rectangle, + bool writable = true, bool has_vscrollbar = true); + + //! destructor + virtual ~GUIEditBoxWithScrollBar(); + + //! Sets another skin independent font. + virtual void setOverrideFont(IGUIFont* font = 0); + + //! Gets the override font (if any) + /** \return The override font (may be 0) */ + virtual IGUIFont* getOverrideFont() const; + + //! Get the font which is used right now for drawing + /** Currently this is the override font when one is set and the + font of the active skin otherwise */ + virtual IGUIFont* getActiveFont() const; + + //! Sets another color for the text. + virtual void setOverrideColor(video::SColor color); + + //! Gets the override color + virtual video::SColor getOverrideColor() const; + + //! Sets if the text should use the overide color or the + //! color in the gui skin. + virtual void enableOverrideColor(bool enable); + + //! Checks if an override color is enabled + /** \return true if the override color is enabled, false otherwise */ + virtual bool isOverrideColorEnabled(void) const; + + //! Sets whether to draw the background + virtual void setDrawBackground(bool draw); + + //! Turns the border on or off + virtual void setDrawBorder(bool border); + + //! Enables or disables word wrap for using the edit box as multiline text editor. + virtual void setWordWrap(bool enable); + + //! Checks if word wrap is enabled + //! \return true if word wrap is enabled, false otherwise + virtual bool isWordWrapEnabled() const; + + //! Enables or disables newlines. + /** \param enable: If set to true, the EGET_EDITBOX_ENTER event will not be fired, + instead a newline character will be inserted. */ + virtual void setMultiLine(bool enable); + + //! Checks if multi line editing is enabled + //! \return true if mult-line is enabled, false otherwise + virtual bool isMultiLineEnabled() const; + + //! Enables or disables automatic scrolling with cursor position + //! \param enable: If set to true, the text will move around with the cursor position + virtual void setAutoScroll(bool enable); + + //! Checks to see if automatic scrolling is enabled + //! \return true if automatic scrolling is enabled, false if not + virtual bool isAutoScrollEnabled() const; + + //! Gets the size area of the text in the edit box + //! \return Returns the size in pixels of the text + virtual core::dimension2du getTextDimension(); + + //! Sets text justification + virtual void setTextAlignment(EGUI_ALIGNMENT horizontal, EGUI_ALIGNMENT vertical); + + //! called if an event happened. + virtual bool OnEvent(const SEvent& event); + + //! draws the element and its children + virtual void draw(); + + //! Sets the new caption of this element. + virtual void setText(const wchar_t* text); + + //! Sets the maximum amount of characters which may be entered in the box. + //! \param max: Maximum amount of characters. If 0, the character amount is + //! infinity. + virtual void setMax(u32 max); + + //! Returns maximum amount of characters, previously set by setMax(); + virtual u32 getMax() const; + + //! Sets whether the edit box is a password box. Setting this to true will + /** disable MultiLine, WordWrap and the ability to copy with ctrl+c or ctrl+x + \param passwordBox: true to enable password, false to disable + \param passwordChar: the character that is displayed instead of letters */ + virtual void setPasswordBox(bool passwordBox, wchar_t passwordChar = L'*'); + + //! Returns true if the edit box is currently a password box. + virtual bool isPasswordBox() const; + + //! Updates the absolute position, splits text if required + virtual void updateAbsolutePosition(); + + virtual void setWritable(bool writable); + + //! Change the background color + virtual void setBackgroundColor(const video::SColor &bg_color); + + //! Writes attributes of the element. + virtual void serializeAttributes(io::IAttributes* out, io::SAttributeReadWriteOptions* options) const; + + //! Reads attributes of the element + virtual void deserializeAttributes(io::IAttributes* in, io::SAttributeReadWriteOptions* options); + +protected: + //! Breaks the single text line. + void breakText(); + //! sets the area of the given line + void setTextRect(s32 line); + //! returns the line number that the cursor is on + s32 getLineFromPos(s32 pos); + //! adds a letter to the edit box + void inputChar(wchar_t c); + //! calculates the current scroll position + void calculateScrollPos(); + //! calculated the FrameRect + void calculateFrameRect(); + //! send some gui event to parent + void sendGuiEvent(EGUI_EVENT_TYPE type); + //! set text markers + void setTextMarkers(s32 begin, s32 end); + //! create a Vertical ScrollBar + void createVScrollBar(); + //! update the vertical scrollBar (visibilty & position) + void updateVScrollBar(); + + bool processKey(const SEvent& event); + bool processMouse(const SEvent& event); + s32 getCursorPos(s32 x, s32 y); + + bool m_mouse_marking; + bool m_border; + bool m_background; + bool m_override_color_enabled; + s32 m_mark_begin; + s32 m_mark_end; + + video::SColor m_override_color; + gui::IGUIFont *m_override_font, *m_last_break_font; + IOSOperator* m_operator; + + u32 m_blink_start_time; + s32 m_cursor_pos; + s32 m_hscroll_pos, m_vscroll_pos; // scroll position in characters + u32 m_max; + + bool m_word_wrap, m_multiline, m_autoscroll, m_passwordbox; + wchar_t m_passwordchar; + EGUI_ALIGNMENT m_halign, m_valign; + + std::vector<core::stringw> m_broken_text; + std::vector<s32> m_broken_text_positions; + + core::rect<s32> m_current_text_rect, m_frame_rect; // temporary values + + u32 m_scrollbar_width; + IGUIScrollBar *m_vscrollbar; + bool m_writable; + + bool m_bg_color_used; + video::SColor m_bg_color; +}; + + +#endif // GUIEDITBOXWITHSCROLLBAR_HEADER + diff --git a/src/gui/guiEngine.cpp b/src/gui/guiEngine.cpp new file mode 100644 index 000000000..e9b4e54c1 --- /dev/null +++ b/src/gui/guiEngine.cpp @@ -0,0 +1,587 @@ +/* +Minetest +Copyright (C) 2013 sapier + +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 "guiEngine.h" + +#include <IGUIStaticText.h> +#include <ICameraSceneNode.h> +#include "client/renderingengine.h" +#include "scripting_mainmenu.h" +#include "util/numeric.h" +#include "config.h" +#include "version.h" +#include "porting.h" +#include "filesys.h" +#include "settings.h" +#include "guiMainMenu.h" +#include "sound.h" +#include "sound_openal.h" +#include "clouds.h" +#include "httpfetch.h" +#include "log.h" +#include "fontengine.h" +#include "guiscalingfilter.h" +#include "irrlicht_changes/static_text.h" + +#ifdef __ANDROID__ +#include "client/tile.h" +#include <GLES/gl.h> +#endif + + +/******************************************************************************/ +void TextDestGuiEngine::gotText(const StringMap &fields) +{ + m_engine->getScriptIface()->handleMainMenuButtons(fields); +} + +/******************************************************************************/ +void TextDestGuiEngine::gotText(const std::wstring &text) +{ + m_engine->getScriptIface()->handleMainMenuEvent(wide_to_utf8(text)); +} + +/******************************************************************************/ +MenuTextureSource::~MenuTextureSource() +{ + for (const std::string &texture_to_delete : m_to_delete) { + const char *tname = texture_to_delete.c_str(); + video::ITexture *texture = m_driver->getTexture(tname); + m_driver->removeTexture(texture); + } +} + +/******************************************************************************/ +video::ITexture *MenuTextureSource::getTexture(const std::string &name, u32 *id) +{ + if(id) + *id = 0; + if(name.empty()) + return NULL; + m_to_delete.insert(name); + +#ifdef __ANDROID__ + video::IImage *image = m_driver->createImageFromFile(name.c_str()); + if (image) { + image = Align2Npot2(image, m_driver); + video::ITexture* retval = m_driver->addTexture(name.c_str(), image); + image->drop(); + return retval; + } +#endif + return m_driver->getTexture(name.c_str()); +} + +/******************************************************************************/ +/** MenuMusicFetcher */ +/******************************************************************************/ +void MenuMusicFetcher::fetchSounds(const std::string &name, + std::set<std::string> &dst_paths, + std::set<std::string> &dst_datas) +{ + if(m_fetched.count(name)) + return; + m_fetched.insert(name); + std::string base; + base = porting::path_share + DIR_DELIM + "sounds"; + dst_paths.insert(base + DIR_DELIM + name + ".ogg"); + int i; + for(i=0; i<10; i++) + dst_paths.insert(base + DIR_DELIM + name + "."+itos(i)+".ogg"); + base = porting::path_user + DIR_DELIM + "sounds"; + dst_paths.insert(base + DIR_DELIM + name + ".ogg"); + for(i=0; i<10; i++) + dst_paths.insert(base + DIR_DELIM + name + "."+itos(i)+".ogg"); +} + +/******************************************************************************/ +/** GUIEngine */ +/******************************************************************************/ +GUIEngine::GUIEngine(JoystickController *joystick, + gui::IGUIElement *parent, + IMenuManager *menumgr, + MainMenuData *data, + bool &kill) : + m_parent(parent), + m_menumanager(menumgr), + m_smgr(RenderingEngine::get_scene_manager()), + m_data(data), + m_kill(kill) +{ + //initialize texture pointers + for (image_definition &texture : m_textures) { + texture.texture = NULL; + } + // is deleted by guiformspec! + m_buttonhandler = new TextDestGuiEngine(this); + + //create texture source + m_texture_source = new MenuTextureSource(RenderingEngine::get_video_driver()); + + //create soundmanager + MenuMusicFetcher soundfetcher; +#if USE_SOUND + m_sound_manager = createOpenALSoundManager(&soundfetcher); +#endif + if(!m_sound_manager) + m_sound_manager = &dummySoundManager; + + //create topleft header + m_toplefttext = L""; + + core::rect<s32> rect(0, 0, g_fontengine->getTextWidth(m_toplefttext.c_str()), + g_fontengine->getTextHeight()); + rect += v2s32(4, 0); + + m_irr_toplefttext = + addStaticText(RenderingEngine::get_gui_env(), m_toplefttext, + rect, false, true, 0, -1); + + //create formspecsource + m_formspecgui = new FormspecFormSource(""); + + /* Create menu */ + m_menu = new GUIFormSpecMenu(joystick, + m_parent, + -1, + m_menumanager, + NULL /* &client */, + m_texture_source, + m_formspecgui, + m_buttonhandler, + false); + + m_menu->allowClose(false); + m_menu->lockSize(true,v2u32(800,600)); + + // Initialize scripting + + infostream << "GUIEngine: Initializing Lua" << std::endl; + + m_script = new MainMenuScripting(this); + + try { + m_script->setMainMenuData(&m_data->script_data); + m_data->script_data.errormessage = ""; + + if (!loadMainMenuScript()) { + errorstream << "No future without main menu!" << std::endl; + abort(); + } + + run(); + } catch (LuaError &e) { + errorstream << "Main menu error: " << e.what() << std::endl; + m_data->script_data.errormessage = e.what(); + } + + m_menu->quitMenu(); + m_menu->drop(); + m_menu = NULL; +} + +/******************************************************************************/ +bool GUIEngine::loadMainMenuScript() +{ + // Set main menu path (for core.get_mainmenu_path()) + m_scriptdir = g_settings->get("main_menu_path"); + if (m_scriptdir.empty()) { + m_scriptdir = porting::path_share + DIR_DELIM + "builtin" + DIR_DELIM + "mainmenu"; + } + + // Load builtin (which will load the main menu script) + std::string script = porting::path_share + DIR_DELIM "builtin" + DIR_DELIM "init.lua"; + try { + m_script->loadScript(script); + // Menu script loaded + return true; + } catch (const ModError &e) { + errorstream << "GUIEngine: execution of menu script failed: " + << e.what() << std::endl; + } + + return false; +} + +/******************************************************************************/ +void GUIEngine::run() +{ + // Always create clouds because they may or may not be + // needed based on the game selected + video::IVideoDriver *driver = RenderingEngine::get_video_driver(); + + cloudInit(); + + unsigned int text_height = g_fontengine->getTextHeight(); + + irr::core::dimension2d<u32> previous_screen_size(g_settings->getU16("screen_w"), + g_settings->getU16("screen_h")); + + while (RenderingEngine::run() && (!m_startgame) && (!m_kill)) { + + const irr::core::dimension2d<u32> ¤t_screen_size = + RenderingEngine::get_video_driver()->getScreenSize(); + // Verify if window size has changed and save it if it's the case + // Ensure evaluating settings->getBool after verifying screensize + // First condition is cheaper + if (previous_screen_size != current_screen_size && + current_screen_size != irr::core::dimension2d<u32>(0,0) && + g_settings->getBool("autosave_screensize")) { + g_settings->setU16("screen_w", current_screen_size.Width); + g_settings->setU16("screen_h", current_screen_size.Height); + previous_screen_size = current_screen_size; + } + + //check if we need to update the "upper left corner"-text + if (text_height != g_fontengine->getTextHeight()) { + updateTopLeftTextSize(); + text_height = g_fontengine->getTextHeight(); + } + + driver->beginScene(true, true, video::SColor(255,140,186,250)); + + if (m_clouds_enabled) + { + cloudPreProcess(); + drawOverlay(driver); + } + else + drawBackground(driver); + + drawHeader(driver); + drawFooter(driver); + + RenderingEngine::get_gui_env()->drawAll(); + + driver->endScene(); + + if (m_clouds_enabled) + cloudPostProcess(); + else + sleep_ms(25); + + m_script->step(); + +#ifdef __ANDROID__ + m_menu->getAndroidUIInput(); +#endif + } +} + +/******************************************************************************/ +GUIEngine::~GUIEngine() +{ + if (m_sound_manager != &dummySoundManager){ + delete m_sound_manager; + m_sound_manager = NULL; + } + + infostream<<"GUIEngine: Deinitializing scripting"<<std::endl; + delete m_script; + + m_irr_toplefttext->setText(L""); + + //clean up texture pointers + for (image_definition &texture : m_textures) { + if (texture.texture) + RenderingEngine::get_video_driver()->removeTexture(texture.texture); + } + + delete m_texture_source; + + if (m_cloud.clouds) + m_cloud.clouds->drop(); +} + +/******************************************************************************/ +void GUIEngine::cloudInit() +{ + m_cloud.clouds = new Clouds(m_smgr, -1, rand()); + m_cloud.clouds->setHeight(100.0f); + m_cloud.clouds->update(v3f(0, 0, 0), video::SColor(255,200,200,255)); + + m_cloud.camera = m_smgr->addCameraSceneNode(0, + v3f(0,0,0), v3f(0, 60, 100)); + m_cloud.camera->setFarValue(10000); + + m_cloud.lasttime = RenderingEngine::get_timer_time(); +} + +/******************************************************************************/ +void GUIEngine::cloudPreProcess() +{ + u32 time = RenderingEngine::get_timer_time(); + + if(time > m_cloud.lasttime) + m_cloud.dtime = (time - m_cloud.lasttime) / 1000.0; + else + m_cloud.dtime = 0; + + m_cloud.lasttime = time; + + m_cloud.clouds->step(m_cloud.dtime*3); + m_cloud.clouds->render(); + m_smgr->drawAll(); +} + +/******************************************************************************/ +void GUIEngine::cloudPostProcess() +{ + float fps_max = g_settings->getFloat("pause_fps_max"); + // Time of frame without fps limit + u32 busytime_u32; + + // not using getRealTime is necessary for wine + u32 time = RenderingEngine::get_timer_time(); + if(time > m_cloud.lasttime) + busytime_u32 = time - m_cloud.lasttime; + else + busytime_u32 = 0; + + // FPS limiter + u32 frametime_min = 1000./fps_max; + + if (busytime_u32 < frametime_min) { + u32 sleeptime = frametime_min - busytime_u32; + RenderingEngine::get_raw_device()->sleep(sleeptime); + } +} + +/******************************************************************************/ +void GUIEngine::drawBackground(video::IVideoDriver *driver) +{ + v2u32 screensize = driver->getScreenSize(); + + video::ITexture* texture = m_textures[TEX_LAYER_BACKGROUND].texture; + + /* If no texture, draw background of solid color */ + if(!texture){ + video::SColor color(255,80,58,37); + core::rect<s32> rect(0, 0, screensize.X, screensize.Y); + driver->draw2DRectangle(color, rect, NULL); + return; + } + + v2u32 sourcesize = texture->getOriginalSize(); + + if (m_textures[TEX_LAYER_BACKGROUND].tile) + { + v2u32 tilesize( + MYMAX(sourcesize.X,m_textures[TEX_LAYER_BACKGROUND].minsize), + MYMAX(sourcesize.Y,m_textures[TEX_LAYER_BACKGROUND].minsize)); + for (unsigned int x = 0; x < screensize.X; x += tilesize.X ) + { + for (unsigned int y = 0; y < screensize.Y; y += tilesize.Y ) + { + draw2DImageFilterScaled(driver, texture, + core::rect<s32>(x, y, x+tilesize.X, y+tilesize.Y), + core::rect<s32>(0, 0, sourcesize.X, sourcesize.Y), + NULL, NULL, true); + } + } + return; + } + + /* Draw background texture */ + draw2DImageFilterScaled(driver, texture, + core::rect<s32>(0, 0, screensize.X, screensize.Y), + core::rect<s32>(0, 0, sourcesize.X, sourcesize.Y), + NULL, NULL, true); +} + +/******************************************************************************/ +void GUIEngine::drawOverlay(video::IVideoDriver *driver) +{ + v2u32 screensize = driver->getScreenSize(); + + video::ITexture* texture = m_textures[TEX_LAYER_OVERLAY].texture; + + /* If no texture, draw nothing */ + if(!texture) + return; + + /* Draw background texture */ + v2u32 sourcesize = texture->getOriginalSize(); + draw2DImageFilterScaled(driver, texture, + core::rect<s32>(0, 0, screensize.X, screensize.Y), + core::rect<s32>(0, 0, sourcesize.X, sourcesize.Y), + NULL, NULL, true); +} + +/******************************************************************************/ +void GUIEngine::drawHeader(video::IVideoDriver *driver) +{ + core::dimension2d<u32> screensize = driver->getScreenSize(); + + video::ITexture* texture = m_textures[TEX_LAYER_HEADER].texture; + + /* If no texture, draw nothing */ + if(!texture) + return; + + f32 mult = (((f32)screensize.Width / 2.0)) / + ((f32)texture->getOriginalSize().Width); + + v2s32 splashsize(((f32)texture->getOriginalSize().Width) * mult, + ((f32)texture->getOriginalSize().Height) * mult); + + // Don't draw the header if there isn't enough room + s32 free_space = (((s32)screensize.Height)-320)/2; + + if (free_space > splashsize.Y) { + core::rect<s32> splashrect(0, 0, splashsize.X, splashsize.Y); + splashrect += v2s32((screensize.Width/2)-(splashsize.X/2), + ((free_space/2)-splashsize.Y/2)+10); + + video::SColor bgcolor(255,50,50,50); + + draw2DImageFilterScaled(driver, texture, splashrect, + core::rect<s32>(core::position2d<s32>(0,0), + core::dimension2di(texture->getOriginalSize())), + NULL, NULL, true); + } +} + +/******************************************************************************/ +void GUIEngine::drawFooter(video::IVideoDriver *driver) +{ + core::dimension2d<u32> screensize = driver->getScreenSize(); + + video::ITexture* texture = m_textures[TEX_LAYER_FOOTER].texture; + + /* If no texture, draw nothing */ + if(!texture) + return; + + f32 mult = (((f32)screensize.Width)) / + ((f32)texture->getOriginalSize().Width); + + v2s32 footersize(((f32)texture->getOriginalSize().Width) * mult, + ((f32)texture->getOriginalSize().Height) * mult); + + // Don't draw the footer if there isn't enough room + s32 free_space = (((s32)screensize.Height)-320)/2; + + if (free_space > footersize.Y) { + core::rect<s32> rect(0,0,footersize.X,footersize.Y); + rect += v2s32(screensize.Width/2,screensize.Height-footersize.Y); + rect -= v2s32(footersize.X/2, 0); + + draw2DImageFilterScaled(driver, texture, rect, + core::rect<s32>(core::position2d<s32>(0,0), + core::dimension2di(texture->getOriginalSize())), + NULL, NULL, true); + } +} + +/******************************************************************************/ +bool GUIEngine::setTexture(texture_layer layer, std::string texturepath, + bool tile_image, unsigned int minsize) +{ + video::IVideoDriver *driver = RenderingEngine::get_video_driver(); + + if (m_textures[layer].texture) { + driver->removeTexture(m_textures[layer].texture); + m_textures[layer].texture = NULL; + } + + if (texturepath.empty() || !fs::PathExists(texturepath)) { + return false; + } + + m_textures[layer].texture = driver->getTexture(texturepath.c_str()); + m_textures[layer].tile = tile_image; + m_textures[layer].minsize = minsize; + + if (!m_textures[layer].texture) { + return false; + } + + return true; +} + +/******************************************************************************/ +bool GUIEngine::downloadFile(const std::string &url, const std::string &target) +{ +#if USE_CURL + std::ofstream target_file(target.c_str(), std::ios::out | std::ios::binary); + + if (!target_file.good()) { + return false; + } + + HTTPFetchRequest fetch_request; + HTTPFetchResult fetch_result; + fetch_request.url = url; + fetch_request.caller = HTTPFETCH_SYNC; + fetch_request.timeout = g_settings->getS32("curl_file_download_timeout"); + httpfetch_sync(fetch_request, fetch_result); + + if (!fetch_result.succeeded) { + return false; + } + target_file << fetch_result.data; + + return true; +#else + return false; +#endif +} + +/******************************************************************************/ +void GUIEngine::setTopleftText(const std::string &text) +{ + m_toplefttext = translate_string(utf8_to_wide(text)); + + updateTopLeftTextSize(); +} + +/******************************************************************************/ +void GUIEngine::updateTopLeftTextSize() +{ + core::rect<s32> rect(0, 0, g_fontengine->getTextWidth(m_toplefttext.c_str()), + g_fontengine->getTextHeight()); + rect += v2s32(4, 0); + + m_irr_toplefttext->remove(); + m_irr_toplefttext = + addStaticText(RenderingEngine::get_gui_env(), m_toplefttext, + rect, false, true, 0, -1); +} + +/******************************************************************************/ +s32 GUIEngine::playSound(SimpleSoundSpec spec, bool looped) +{ + s32 handle = m_sound_manager->playSound(spec, looped); + return handle; +} + +/******************************************************************************/ +void GUIEngine::stopSound(s32 handle) +{ + m_sound_manager->stopSound(handle); +} + +/******************************************************************************/ +unsigned int GUIEngine::queueAsync(const std::string &serialized_func, + const std::string &serialized_params) +{ + return m_script->queueAsync(serialized_func, serialized_params); +} + diff --git a/src/gui/guiEngine.h b/src/gui/guiEngine.h new file mode 100644 index 000000000..817d76014 --- /dev/null +++ b/src/gui/guiEngine.h @@ -0,0 +1,304 @@ +/* +Minetest +Copyright (C) 2013 sapier + +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. +*/ + +#pragma once + +/******************************************************************************/ +/* Includes */ +/******************************************************************************/ +#include "irrlichttypes.h" +#include "modalMenu.h" +#include "guiFormSpecMenu.h" +#include "sound.h" +#include "client/tile.h" +#include "util/enriched_string.h" + +/******************************************************************************/ +/* Typedefs and macros */ +/******************************************************************************/ +/** texture layer ids */ +typedef enum { + TEX_LAYER_BACKGROUND = 0, + TEX_LAYER_OVERLAY, + TEX_LAYER_HEADER, + TEX_LAYER_FOOTER, + TEX_LAYER_MAX +} texture_layer; + +typedef struct { + video::ITexture *texture = nullptr; + bool tile; + unsigned int minsize; +} image_definition; + +/******************************************************************************/ +/* forward declarations */ +/******************************************************************************/ +class GUIEngine; +class MainMenuScripting; +class Clouds; +struct MainMenuData; + +/******************************************************************************/ +/* declarations */ +/******************************************************************************/ + +/** GUIEngine specific implementation of TextDest used within guiFormSpecMenu */ +class TextDestGuiEngine : public TextDest +{ +public: + /** + * default constructor + * @param engine the engine data is transmitted for further processing + */ + TextDestGuiEngine(GUIEngine* engine) : m_engine(engine) {}; + + /** + * receive fields transmitted by guiFormSpecMenu + * @param fields map containing formspec field elements currently active + */ + void gotText(const StringMap &fields); + + /** + * receive text/events transmitted by guiFormSpecMenu + * @param text textual representation of event + */ + void gotText(const std::wstring &text); + +private: + /** target to transmit data to */ + GUIEngine *m_engine = nullptr; +}; + +/** GUIEngine specific implementation of ISimpleTextureSource */ +class MenuTextureSource : public ISimpleTextureSource +{ +public: + /** + * default constructor + * @param driver the video driver to load textures from + */ + MenuTextureSource(video::IVideoDriver *driver) : m_driver(driver) {}; + + /** + * destructor, removes all loaded textures + */ + virtual ~MenuTextureSource(); + + /** + * get a texture, loading it if required + * @param name path to the texture + * @param id receives the texture ID, always 0 in this implementation + */ + video::ITexture *getTexture(const std::string &name, u32 *id = NULL); + +private: + /** driver to get textures from */ + video::IVideoDriver *m_driver = nullptr; + /** set of texture names to delete */ + std::set<std::string> m_to_delete; +}; + +/** GUIEngine specific implementation of OnDemandSoundFetcher */ +class MenuMusicFetcher: public OnDemandSoundFetcher +{ +public: + /** + * get sound file paths according to sound name + * @param name sound name + * @param dst_paths receives possible paths to sound files + * @param dst_datas receives binary sound data (not used here) + */ + void fetchSounds(const std::string &name, + std::set<std::string> &dst_paths, + std::set<std::string> &dst_datas); + +private: + /** set of fetched sound names */ + std::set<std::string> m_fetched; +}; + +/** implementation of main menu based uppon formspecs */ +class GUIEngine { + /** grant ModApiMainMenu access to private members */ + friend class ModApiMainMenu; + friend class ModApiSound; + +public: + /** + * default constructor + * @param dev device to draw at + * @param parent parent gui element + * @param menumgr manager to add menus to + * @param smgr scene manager to add scene elements to + * @param data struct to transfer data to main game handling + */ + GUIEngine(JoystickController *joystick, + gui::IGUIElement *parent, + IMenuManager *menumgr, + MainMenuData *data, + bool &kill); + + /** default destructor */ + virtual ~GUIEngine(); + + /** + * return MainMenuScripting interface + */ + MainMenuScripting *getScriptIface() + { + return m_script; + } + + /** + * return dir of current menuscript + */ + std::string getScriptDir() + { + return m_scriptdir; + } + + /** pass async callback to scriptengine **/ + unsigned int queueAsync(const std::string &serialized_fct, + const std::string &serialized_params); + +private: + + /** find and run the main menu script */ + bool loadMainMenuScript(); + + /** run main menu loop */ + void run(); + + /** update size of topleftext element */ + void updateTopLeftTextSize(); + + /** parent gui element */ + gui::IGUIElement *m_parent = nullptr; + /** manager to add menus to */ + IMenuManager *m_menumanager = nullptr; + /** scene manager to add scene elements to */ + scene::ISceneManager *m_smgr = nullptr; + /** pointer to data beeing transfered back to main game handling */ + MainMenuData *m_data = nullptr; + /** pointer to texture source */ + ISimpleTextureSource *m_texture_source = nullptr; + /** pointer to soundmanager*/ + ISoundManager *m_sound_manager = nullptr; + + /** representation of form source to be used in mainmenu formspec */ + FormspecFormSource *m_formspecgui = nullptr; + /** formspec input receiver */ + TextDestGuiEngine *m_buttonhandler = nullptr; + /** the formspec menu */ + GUIFormSpecMenu *m_menu = nullptr; + + /** reference to kill variable managed by SIGINT handler */ + bool &m_kill; + + /** variable used to abort menu and return back to main game handling */ + bool m_startgame = false; + + /** scripting interface */ + MainMenuScripting *m_script = nullptr; + + /** script basefolder */ + std::string m_scriptdir = ""; + + /** + * draw background layer + * @param driver to use for drawing + */ + void drawBackground(video::IVideoDriver *driver); + /** + * draw overlay layer + * @param driver to use for drawing + */ + void drawOverlay(video::IVideoDriver *driver); + /** + * draw header layer + * @param driver to use for drawing + */ + void drawHeader(video::IVideoDriver *driver); + /** + * draw footer layer + * @param driver to use for drawing + */ + void drawFooter(video::IVideoDriver *driver); + + /** + * load a texture for a specified layer + * @param layer draw layer to specify texture + * @param texturepath full path of texture to load + */ + bool setTexture(texture_layer layer, std::string texturepath, + bool tile_image, unsigned int minsize); + + /** + * download a file using curl + * @param url url to download + * @param target file to store to + */ + static bool downloadFile(const std::string &url, const std::string &target); + + /** array containing pointers to current specified texture layers */ + image_definition m_textures[TEX_LAYER_MAX]; + + /** + * specify text to appear as top left string + * @param text to set + */ + void setTopleftText(const std::string &text); + + /** pointer to gui element shown at topleft corner */ + irr::gui::IGUIStaticText *m_irr_toplefttext = nullptr; + /** and text that is in it */ + EnrichedString m_toplefttext; + + /** initialize cloud subsystem */ + void cloudInit(); + /** do preprocessing for cloud subsystem */ + void cloudPreProcess(); + /** do postprocessing for cloud subsystem */ + void cloudPostProcess(); + + /** internam data required for drawing clouds */ + struct clouddata { + /** delta time since last cloud processing */ + f32 dtime; + /** absolute time of last cloud processing */ + u32 lasttime; + /** pointer to cloud class */ + Clouds *clouds = nullptr; + /** camera required for drawing clouds */ + scene::ICameraSceneNode *camera = nullptr; + }; + + /** is drawing of clouds enabled atm */ + bool m_clouds_enabled = true; + /** data used to draw clouds */ + clouddata m_cloud; + + /** start playing a sound and return handle */ + s32 playSound(SimpleSoundSpec spec, bool looped); + /** stop playing a sound started with playSound() */ + void stopSound(s32 handle); + + +}; diff --git a/src/gui/guiFormSpecMenu.cpp b/src/gui/guiFormSpecMenu.cpp new file mode 100644 index 000000000..0691bc598 --- /dev/null +++ b/src/gui/guiFormSpecMenu.cpp @@ -0,0 +1,3864 @@ +/* +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 "guiTable.h" +#include "constants.h" +#include "gamedef.h" +#include "keycode.h" +#include "util/strfnd.h" +#include <IGUICheckBox.h> +#include <IGUIEditBox.h> +#include <IGUIButton.h> +#include <IGUIStaticText.h> +#include <IGUIFont.h> +#include <IGUITabControl.h> +#include <IGUIComboBox.h> +#include "client/renderingengine.h" +#include "log.h" +#include "client/tile.h" // ITextureSource +#include "hud.h" // drawItemStack +#include "filesys.h" +#include "gettime.h" +#include "gettext.h" +#include "scripting_server.h" +#include "porting.h" +#include "settings.h" +#include "client.h" +#include "fontengine.h" +#include "util/hex.h" +#include "util/numeric.h" +#include "util/string.h" // for parseColorString() +#include "irrlicht_changes/static_text.h" +#include "guiscalingfilter.h" +#include "guiEditBoxWithScrollbar.h" + +#if USE_FREETYPE && IRRLICHT_VERSION_MAJOR == 1 && IRRLICHT_VERSION_MINOR < 9 +#include "intlGUIEditBox.h" +#endif + +#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 +*/ +static unsigned int font_line_height(gui::IGUIFont *font) +{ + return font->getDimension(L"Ay").Height + font->getKerningHeight(); +} + +inline u32 clamp_u8(s32 value) +{ + return (u32) MYMIN(MYMAX(value, 0), 255); +} + +GUIFormSpecMenu::GUIFormSpecMenu(JoystickController *joystick, + gui::IGUIElement *parent, s32 id, IMenuManager *menumgr, + Client *client, ISimpleTextureSource *tsrc, IFormSource *fsrc, TextDest *tdst, + bool remap_dbl_click) : + GUIModalMenu(RenderingEngine::get_gui_env(), parent, id, menumgr), + m_invmgr(client), + m_tsrc(tsrc), + m_client(client), + m_form_src(fsrc), + m_text_dst(tdst), + m_joystick(joystick), + m_remap_dbl_click(remap_dbl_click) +#ifdef __ANDROID__ + , m_JavaDialogFieldName("") +#endif +{ + current_keys_pending.key_down = false; + current_keys_pending.key_up = false; + current_keys_pending.key_enter = false; + current_keys_pending.key_escape = false; + + m_doubleclickdetect[0].time = 0; + m_doubleclickdetect[1].time = 0; + + m_doubleclickdetect[0].pos = v2s32(0, 0); + m_doubleclickdetect[1].pos = v2s32(0, 0); + + m_tooltip_show_delay = (u32)g_settings->getS32("tooltip_show_delay"); + m_tooltip_append_itemname = g_settings->getBool("tooltip_append_itemname"); +} + +GUIFormSpecMenu::~GUIFormSpecMenu() +{ + removeChildren(); + + for (auto &table_it : m_tables) { + table_it.second->drop(); + } + + delete m_selected_item; + delete m_form_src; + delete m_text_dst; +} + +void GUIFormSpecMenu::removeChildren() +{ + const core::list<gui::IGUIElement*> &children = getChildren(); + + while(!children.empty()) { + (*children.getLast())->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 table + // 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 (gui::IGUIElement *it : children) { + if (it->getType() == gui::EGUIET_EDIT_BOX + && it->getText()[0] == 0) { + Environment->setFocus(it); + return; + } + } + + // 2. first editbox + for (gui::IGUIElement *it : children) { + if (it->getType() == gui::EGUIET_EDIT_BOX) { + Environment->setFocus(it); + return; + } + } + + // 3. first table + for (gui::IGUIElement *it : children) { + if (it->getTypeName() == std::string("GUITable")) { + 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 (gui::IGUIElement *it : children) { + 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())); +} + +GUITable* GUIFormSpecMenu::getTable(const std::string &tablename) +{ + for (auto &table : m_tables) { + if (tablename == table.first.fname) + return table.second; + } + return 0; +} + +std::vector<std::string>* GUIFormSpecMenu::getDropDownValues(const std::string &name) +{ + for (auto &dropdown : m_dropdowns) { + if (name == dropdown.first.fname) + return &dropdown.second; + } + return NULL; +} + +void GUIFormSpecMenu::parseSize(parserData* data, const std::string &element) +{ + std::vector<std::string> parts = split(element,','); + + if (((parts.size() == 2) || parts.size() == 3) || + ((parts.size() > 3) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + if (parts[1].find(';') != std::string::npos) + parts[1] = parts[1].substr(0,parts[1].find(';')); + + data->invsize.X = MYMAX(0, stof(parts[0])); + data->invsize.Y = MYMAX(0, stof(parts[1])); + + lockSize(false); + if (parts.size() == 3) { + if (parts[2] == "true") { + lockSize(true,v2u32(800,600)); + } + } + + data->explicit_size = true; + return; + } + errorstream<< "Invalid size element (" << parts.size() << "): '" << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseContainer(parserData* data, const std::string &element) +{ + std::vector<std::string> parts = split(element, ','); + + if (parts.size() >= 2) { + if (parts[1].find(';') != std::string::npos) + parts[1] = parts[1].substr(0, parts[1].find(';')); + + container_stack.push(pos_offset); + pos_offset.X += MYMAX(0, stof(parts[0])); + pos_offset.Y += MYMAX(0, stof(parts[1])); + return; + } + errorstream<< "Invalid container start element (" << parts.size() << "): '" << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseContainerEnd(parserData* data) +{ + if (container_stack.empty()) { + errorstream<< "Invalid container end element, no matching container start element" << std::endl; + } else { + pos_offset = container_stack.top(); + container_stack.pop(); + } +} + +void GUIFormSpecMenu::parseList(parserData* data, const std::string &element) +{ + if (m_client == 0) { + warningstream<<"invalid use of 'list' with m_client==0"<<std::endl; + return; + } + + std::vector<std::string> parts = split(element,';'); + + if (((parts.size() == 4) || (parts.size() == 5)) || + ((parts.size() > 5) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + 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_offset * spacing; + 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.empty()) + start_i = stoi(startindex); + + if (geom.X < 0 || geom.Y < 0 || start_i < 0) { + errorstream<< "Invalid list element: '" << element << "'" << std::endl; + return; + } + + if(!data->explicit_size) + warningstream<<"invalid use of list without a size[] element"<<std::endl; + m_inventorylists.emplace_back(loc, listname, pos, geom, start_i); + return; + } + errorstream<< "Invalid list element(" << parts.size() << "): '" << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseListRing(parserData* data, const std::string &element) +{ + if (m_client == 0) { + errorstream << "WARNING: invalid use of 'listring' with m_client==0" << std::endl; + return; + } + + std::vector<std::string> parts = split(element, ';'); + + if (parts.size() == 2) { + std::string location = parts[0]; + std::string listname = parts[1]; + + InventoryLocation loc; + + if (location == "context" || location == "current_name") + loc = m_current_inventory_location; + else + loc.deSerialize(location); + + m_inventory_rings.emplace_back(loc, listname); + return; + } + + if (element.empty() && m_inventorylists.size() > 1) { + size_t siz = m_inventorylists.size(); + // insert the last two inv list elements into the list ring + const ListDrawSpec &spa = m_inventorylists[siz - 2]; + const ListDrawSpec &spb = m_inventorylists[siz - 1]; + m_inventory_rings.emplace_back(spa.inventoryloc, spa.listname); + m_inventory_rings.emplace_back(spb.inventoryloc, spb.listname); + return; + } + + errorstream<< "Invalid list ring element(" << parts.size() << ", " + << m_inventorylists.size() << "): '" << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseCheckbox(parserData* data, const std::string &element) +{ + std::vector<std::string> parts = split(element,';'); + + if (((parts.size() >= 3) && (parts.size() <= 4)) || + ((parts.size() > 4) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + std::vector<std::string> v_pos = split(parts[0],','); + std::string name = parts[1]; + std::string label = parts[2]; + std::string selected; + + if (parts.size() >= 4) + selected = parts[3]; + + MY_CHECKPOS("checkbox",0); + + v2s32 pos = padding + pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float) spacing.X; + pos.Y += stof(v_pos[1]) * (float) spacing.Y; + + bool fselected = false; + + if (selected == "true") + fselected = true; + + std::wstring wlabel = translate_string(utf8_to_wide(unescape_string(label))); + + core::rect<s32> rect = core::rect<s32>( + pos.X, pos.Y + ((imgsize.Y/2) - m_btn_height), + pos.X + m_font->getDimension(wlabel.c_str()).Width + 25, // text size + size of checkbox + pos.Y + ((imgsize.Y/2) + m_btn_height)); + + FieldSpec spec( + name, + wlabel, //Needed for displaying text on MSVC + wlabel, + 258+m_fields.size() + ); + + spec.ftype = f_CheckBox; + + gui::IGUICheckBox* e = Environment->addCheckBox(fselected, rect, this, + spec.fid, spec.flabel.c_str()); + + if (spec.fname == data->focused_fieldname) { + Environment->setFocus(e); + } + + m_checkboxes.emplace_back(spec,e); + m_fields.push_back(spec); + return; + } + errorstream<< "Invalid checkbox element(" << parts.size() << "): '" << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseScrollBar(parserData* data, const std::string &element) +{ + std::vector<std::string> parts = split(element,';'); + + if (parts.size() >= 5) { + std::vector<std::string> v_pos = split(parts[0],','); + std::vector<std::string> v_dim = split(parts[1],','); + std::string name = parts[3]; + std::string value = parts[4]; + + MY_CHECKPOS("scrollbar",0); + + v2s32 pos = padding + pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float) spacing.X; + pos.Y += stof(v_pos[1]) * (float) spacing.Y; + + if (v_dim.size() != 2) { + errorstream<< "Invalid size for element " << "scrollbar" + << "specified: \"" << parts[1] << "\"" << std::endl; + return; + } + + v2s32 dim; + dim.X = stof(v_dim[0]) * (float) spacing.X; + dim.Y = stof(v_dim[1]) * (float) spacing.Y; + + core::rect<s32> rect = + core::rect<s32>(pos.X, pos.Y, pos.X + dim.X, pos.Y + dim.Y); + + FieldSpec spec( + name, + L"", + L"", + 258+m_fields.size() + ); + + bool is_horizontal = true; + + if (parts[2] == "vertical") + is_horizontal = false; + + spec.ftype = f_ScrollBar; + spec.send = true; + gui::IGUIScrollBar* e = + Environment->addScrollBar(is_horizontal,rect,this,spec.fid); + + e->setMax(1000); + e->setMin(0); + e->setPos(stoi(parts[4])); + e->setSmallStep(10); + e->setLargeStep(100); + + m_scrollbars.emplace_back(spec,e); + m_fields.push_back(spec); + return; + } + errorstream<< "Invalid scrollbar element(" << parts.size() << "): '" << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseImage(parserData* data, const std::string &element) +{ + std::vector<std::string> parts = split(element,';'); + + if ((parts.size() == 3) || + ((parts.size() > 3) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + std::vector<std::string> v_pos = split(parts[0],','); + std::vector<std::string> v_geom = split(parts[1],','); + std::string name = unescape_string(parts[2]); + + MY_CHECKPOS("image", 0); + MY_CHECKGEOM("image", 1); + + v2s32 pos = padding + AbsoluteRect.UpperLeftCorner + pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float) spacing.X; + pos.Y += stof(v_pos[1]) * (float) spacing.Y; + + v2s32 geom; + geom.X = stof(v_geom[0]) * (float)imgsize.X; + geom.Y = stof(v_geom[1]) * (float)imgsize.Y; + + if (!data->explicit_size) + warningstream<<"invalid use of image without a size[] element"<<std::endl; + m_images.emplace_back(name, pos, geom); + return; + } + + if (parts.size() == 2) { + std::vector<std::string> v_pos = split(parts[0],','); + std::string name = unescape_string(parts[1]); + + MY_CHECKPOS("image", 0); + + v2s32 pos = padding + AbsoluteRect.UpperLeftCorner + pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float) spacing.X; + pos.Y += stof(v_pos[1]) * (float) spacing.Y; + + if (!data->explicit_size) + warningstream<<"invalid use of image without a size[] element"<<std::endl; + m_images.emplace_back(name, pos); + return; + } + errorstream<< "Invalid image element(" << parts.size() << "): '" << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseItemImage(parserData* data, const std::string &element) +{ + std::vector<std::string> parts = split(element,';'); + + if ((parts.size() == 3) || + ((parts.size() > 3) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + std::vector<std::string> v_pos = split(parts[0],','); + std::vector<std::string> v_geom = split(parts[1],','); + std::string name = parts[2]; + + MY_CHECKPOS("itemimage",0); + MY_CHECKGEOM("itemimage",1); + + v2s32 pos = padding + AbsoluteRect.UpperLeftCorner + pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float) spacing.X; + pos.Y += stof(v_pos[1]) * (float) spacing.Y; + + v2s32 geom; + geom.X = stof(v_geom[0]) * (float)imgsize.X; + geom.Y = stof(v_geom[1]) * (float)imgsize.Y; + + if(!data->explicit_size) + warningstream<<"invalid use of item_image without a size[] element"<<std::endl; + m_itemimages.emplace_back("", name, pos, geom); + return; + } + errorstream<< "Invalid ItemImage element(" << parts.size() << "): '" << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseButton(parserData* data, const std::string &element, + const std::string &type) +{ + std::vector<std::string> parts = split(element,';'); + + if ((parts.size() == 4) || + ((parts.size() > 4) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + std::vector<std::string> v_pos = split(parts[0],','); + std::vector<std::string> v_geom = split(parts[1],','); + std::string name = parts[2]; + std::string label = parts[3]; + + MY_CHECKPOS("button",0); + MY_CHECKGEOM("button",1); + + v2s32 pos = padding + pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float)spacing.X; + pos.Y += stof(v_pos[1]) * (float)spacing.Y; + + v2s32 geom; + geom.X = (stof(v_geom[0]) * (float)spacing.X)-(spacing.X-imgsize.X); + pos.Y += (stof(v_geom[1]) * (float)imgsize.Y)/2; + + core::rect<s32> rect = + core::rect<s32>(pos.X, pos.Y - m_btn_height, + pos.X + geom.X, pos.Y + m_btn_height); + + if(!data->explicit_size) + warningstream<<"invalid use of button without a size[] element"<<std::endl; + + std::wstring wlabel = translate_string(utf8_to_wide(unescape_string(label))); + + FieldSpec spec( + name, + wlabel, + L"", + 258+m_fields.size() + ); + spec.ftype = f_Button; + if(type == "button_exit") + spec.is_exit = true; + gui::IGUIButton* e = Environment->addButton(rect, this, spec.fid, + spec.flabel.c_str()); + + if (spec.fname == data->focused_fieldname) { + Environment->setFocus(e); + } + + m_fields.push_back(spec); + return; + } + errorstream<< "Invalid button element(" << parts.size() << "): '" << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseBackground(parserData* data, const std::string &element) +{ + std::vector<std::string> parts = split(element,';'); + + if (((parts.size() == 3) || (parts.size() == 4)) || + ((parts.size() > 4) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + std::vector<std::string> v_pos = split(parts[0],','); + std::vector<std::string> v_geom = split(parts[1],','); + std::string name = unescape_string(parts[2]); + + MY_CHECKPOS("background",0); + MY_CHECKGEOM("background",1); + + v2s32 pos = padding + AbsoluteRect.UpperLeftCorner + pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float)spacing.X - ((float)spacing.X - (float)imgsize.X)/2; + pos.Y += stof(v_pos[1]) * (float)spacing.Y - ((float)spacing.Y - (float)imgsize.Y)/2; + + v2s32 geom; + geom.X = stof(v_geom[0]) * (float)spacing.X; + geom.Y = stof(v_geom[1]) * (float)spacing.Y; + + if (!data->explicit_size) + warningstream<<"invalid use of background without a size[] element"<<std::endl; + + bool clip = false; + if (parts.size() == 4 && is_yes(parts[3])) { + pos.X = stoi(v_pos[0]); //acts as offset + pos.Y = stoi(v_pos[1]); //acts as offset + clip = true; + } + m_backgrounds.emplace_back(name, pos, geom, clip); + + return; + } + errorstream<< "Invalid background element(" << parts.size() << "): '" << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseTableOptions(parserData* data, const std::string &element) +{ + std::vector<std::string> parts = split(element,';'); + + data->table_options.clear(); + for (const std::string &part : parts) { + // Parse table option + std::string opt = unescape_string(part); + data->table_options.push_back(GUITable::splitOption(opt)); + } +} + +void GUIFormSpecMenu::parseTableColumns(parserData* data, const std::string &element) +{ + std::vector<std::string> parts = split(element,';'); + + data->table_columns.clear(); + for (const std::string &part : parts) { + std::vector<std::string> col_parts = split(part,','); + GUITable::TableColumn column; + // Parse column type + if (!col_parts.empty()) + column.type = col_parts[0]; + // Parse column options + for (size_t j = 1; j < col_parts.size(); ++j) { + std::string opt = unescape_string(col_parts[j]); + column.options.push_back(GUITable::splitOption(opt)); + } + data->table_columns.push_back(column); + } +} + +void GUIFormSpecMenu::parseTable(parserData* data, const std::string &element) +{ + std::vector<std::string> parts = split(element,';'); + + if (((parts.size() == 4) || (parts.size() == 5)) || + ((parts.size() > 5) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + std::vector<std::string> v_pos = split(parts[0],','); + std::vector<std::string> v_geom = split(parts[1],','); + std::string name = parts[2]; + std::vector<std::string> items = split(parts[3],','); + std::string str_initial_selection; + std::string str_transparent = "false"; + + if (parts.size() >= 5) + str_initial_selection = parts[4]; + + MY_CHECKPOS("table",0); + MY_CHECKGEOM("table",1); + + v2s32 pos = padding + pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float)spacing.X; + pos.Y += stof(v_pos[1]) * (float)spacing.Y; + + v2s32 geom; + geom.X = stof(v_geom[0]) * (float)spacing.X; + geom.Y = stof(v_geom[1]) * (float)spacing.Y; + + core::rect<s32> rect = core::rect<s32>(pos.X, pos.Y, pos.X+geom.X, pos.Y+geom.Y); + + FieldSpec spec( + name, + L"", + L"", + 258+m_fields.size() + ); + + spec.ftype = f_Table; + + for (std::string &item : items) { + item = wide_to_utf8(unescape_translate(utf8_to_wide(unescape_string(item)))); + } + + //now really show table + GUITable *e = new GUITable(Environment, this, spec.fid, rect, + m_tsrc); + + if (spec.fname == data->focused_fieldname) { + Environment->setFocus(e); + } + + e->setTable(data->table_options, data->table_columns, items); + + if (data->table_dyndata.find(name) != data->table_dyndata.end()) { + e->setDynamicData(data->table_dyndata[name]); + } + + if (!str_initial_selection.empty() && str_initial_selection != "0") + e->setSelected(stoi(str_initial_selection)); + + m_tables.emplace_back(spec, e); + m_fields.push_back(spec); + return; + } + errorstream<< "Invalid table element(" << parts.size() << "): '" << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseTextList(parserData* data, const std::string &element) +{ + std::vector<std::string> parts = split(element,';'); + + if (((parts.size() == 4) || (parts.size() == 5) || (parts.size() == 6)) || + ((parts.size() > 6) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + std::vector<std::string> v_pos = split(parts[0],','); + std::vector<std::string> v_geom = split(parts[1],','); + std::string name = parts[2]; + std::vector<std::string> items = split(parts[3],','); + std::string str_initial_selection; + std::string str_transparent = "false"; + + if (parts.size() >= 5) + str_initial_selection = parts[4]; + + if (parts.size() >= 6) + str_transparent = parts[5]; + + MY_CHECKPOS("textlist",0); + MY_CHECKGEOM("textlist",1); + + v2s32 pos = padding + pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float)spacing.X; + pos.Y += stof(v_pos[1]) * (float)spacing.Y; + + v2s32 geom; + geom.X = stof(v_geom[0]) * (float)spacing.X; + geom.Y = stof(v_geom[1]) * (float)spacing.Y; + + + core::rect<s32> rect = core::rect<s32>(pos.X, pos.Y, pos.X+geom.X, pos.Y+geom.Y); + + FieldSpec spec( + name, + L"", + L"", + 258+m_fields.size() + ); + + spec.ftype = f_Table; + + for (std::string &item : items) { + item = wide_to_utf8(unescape_translate(utf8_to_wide(unescape_string(item)))); + } + + //now really show list + GUITable *e = new GUITable(Environment, this, spec.fid, rect, + m_tsrc); + + if (spec.fname == data->focused_fieldname) { + Environment->setFocus(e); + } + + e->setTextList(items, is_yes(str_transparent)); + + if (data->table_dyndata.find(name) != data->table_dyndata.end()) { + e->setDynamicData(data->table_dyndata[name]); + } + + if (!str_initial_selection.empty() && str_initial_selection != "0") + e->setSelected(stoi(str_initial_selection)); + + m_tables.emplace_back(spec, e); + m_fields.push_back(spec); + return; + } + errorstream<< "Invalid textlist element(" << parts.size() << "): '" << element << "'" << std::endl; +} + + +void GUIFormSpecMenu::parseDropDown(parserData* data, const std::string &element) +{ + std::vector<std::string> parts = split(element,';'); + + if ((parts.size() == 5) || + ((parts.size() > 5) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + std::vector<std::string> v_pos = split(parts[0],','); + std::string name = parts[2]; + std::vector<std::string> items = split(parts[3],','); + std::string str_initial_selection; + str_initial_selection = parts[4]; + + MY_CHECKPOS("dropdown",0); + + v2s32 pos = padding + pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float)spacing.X; + pos.Y += stof(v_pos[1]) * (float)spacing.Y; + + s32 width = stof(parts[1]) * (float)spacing.Y; + + core::rect<s32> rect = core::rect<s32>(pos.X, pos.Y, + pos.X + width, pos.Y + (m_btn_height * 2)); + + FieldSpec spec( + name, + L"", + L"", + 258+m_fields.size() + ); + + spec.ftype = f_DropDown; + spec.send = true; + + //now really show list + gui::IGUIComboBox *e = Environment->addComboBox(rect, this,spec.fid); + + if (spec.fname == data->focused_fieldname) { + Environment->setFocus(e); + } + + for (const std::string &item : items) { + e->addItem(unescape_translate(unescape_string( + utf8_to_wide(item))).c_str()); + } + + if (!str_initial_selection.empty()) + e->setSelected(stoi(str_initial_selection)-1); + + m_fields.push_back(spec); + + m_dropdowns.emplace_back(spec, std::vector<std::string>()); + std::vector<std::string> &values = m_dropdowns.back().second; + for (const std::string &item : items) { + values.push_back(unescape_string(item)); + } + + return; + } + errorstream << "Invalid dropdown element(" << parts.size() << "): '" + << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseFieldCloseOnEnter(parserData *data, const std::string &element) +{ + std::vector<std::string> parts = split(element,';'); + if (parts.size() == 2 || + (parts.size() > 2 && m_formspec_version > FORMSPEC_API_VERSION)) { + field_close_on_enter[parts[0]] = is_yes(parts[1]); + } +} + +void GUIFormSpecMenu::parsePwdField(parserData* data, const std::string &element) +{ + std::vector<std::string> parts = split(element,';'); + + if ((parts.size() == 4) || (parts.size() == 5) || + ((parts.size() > 5) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + std::vector<std::string> v_pos = split(parts[0],','); + std::vector<std::string> v_geom = split(parts[1],','); + std::string name = parts[2]; + std::string label = parts[3]; + + MY_CHECKPOS("pwdfield",0); + MY_CHECKGEOM("pwdfield",1); + + v2s32 pos = pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float)spacing.X; + pos.Y += stof(v_pos[1]) * (float)spacing.Y; + + v2s32 geom; + geom.X = (stof(v_geom[0]) * (float)spacing.X)-(spacing.X-imgsize.X); + + pos.Y += (stof(v_geom[1]) * (float)imgsize.Y)/2; + pos.Y -= m_btn_height; + geom.Y = m_btn_height*2; + + core::rect<s32> rect = core::rect<s32>(pos.X, pos.Y, pos.X+geom.X, pos.Y+geom.Y); + + std::wstring wlabel = translate_string(utf8_to_wide(unescape_string(label))); + + FieldSpec spec( + name, + wlabel, + L"", + 258+m_fields.size() + ); + + spec.send = true; + gui::IGUIEditBox * e = Environment->addEditBox(0, rect, true, this, spec.fid); + + if (spec.fname == data->focused_fieldname) { + Environment->setFocus(e); + } + + if (label.length() >= 1) + { + int font_height = g_fontengine->getTextHeight(); + rect.UpperLeftCorner.Y -= font_height; + rect.LowerRightCorner.Y = rect.UpperLeftCorner.Y + font_height; + addStaticText(Environment, spec.flabel.c_str(), rect, false, true, this, 0); + } + + e->setPasswordBox(true,L'*'); + + irr::SEvent evt; + evt.EventType = EET_KEY_INPUT_EVENT; + evt.KeyInput.Key = KEY_END; + evt.KeyInput.Char = 0; + evt.KeyInput.Control = false; + evt.KeyInput.Shift = false; + evt.KeyInput.PressedDown = true; + e->OnEvent(evt); + + if (parts.size() >= 5) { + // TODO: remove after 2016-11-03 + warningstream << "pwdfield: use field_close_on_enter[name, enabled]" << + " instead of the 5th param" << std::endl; + field_close_on_enter[name] = is_yes(parts[4]); + } + + m_fields.push_back(spec); + return; + } + errorstream<< "Invalid pwdfield element(" << parts.size() << "): '" << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseSimpleField(parserData* data, + std::vector<std::string> &parts) +{ + std::string name = parts[0]; + std::string label = parts[1]; + std::string default_val = parts[2]; + + core::rect<s32> rect; + + if(data->explicit_size) + warningstream<<"invalid use of unpositioned \"field\" in inventory"<<std::endl; + + v2s32 pos = padding + AbsoluteRect.UpperLeftCorner + pos_offset * spacing; + pos.Y = ((m_fields.size()+2)*60); + v2s32 size = DesiredRect.getSize(); + + rect = core::rect<s32>(size.X / 2 - 150, pos.Y, + (size.X / 2 - 150) + 300, pos.Y + (m_btn_height*2)); + + + if(m_form_src) + default_val = m_form_src->resolveText(default_val); + + + std::wstring wlabel = translate_string(utf8_to_wide(unescape_string(label))); + + FieldSpec spec( + name, + wlabel, + utf8_to_wide(unescape_string(default_val)), + 258+m_fields.size() + ); + + if (name.empty()) { + // spec field id to 0, this stops submit searching for a value that isn't there + addStaticText(Environment, spec.flabel.c_str(), rect, false, true, this, spec.fid); + } else { + spec.send = true; + gui::IGUIElement *e; +#if USE_FREETYPE && IRRLICHT_VERSION_MAJOR == 1 && IRRLICHT_VERSION_MINOR < 9 + if (g_settings->getBool("freetype")) { + e = (gui::IGUIElement *) new gui::intlGUIEditBox(spec.fdefault.c_str(), + true, Environment, this, spec.fid, rect); + e->drop(); + } else { +#else + { +#endif + e = Environment->addEditBox(spec.fdefault.c_str(), rect, true, this, spec.fid); + } + if (spec.fname == data->focused_fieldname) { + Environment->setFocus(e); + } + + irr::SEvent evt; + evt.EventType = EET_KEY_INPUT_EVENT; + evt.KeyInput.Key = KEY_END; + evt.KeyInput.Char = 0; + evt.KeyInput.Control = 0; + evt.KeyInput.Shift = 0; + evt.KeyInput.PressedDown = true; + e->OnEvent(evt); + + if (label.length() >= 1) + { + int font_height = g_fontengine->getTextHeight(); + rect.UpperLeftCorner.Y -= font_height; + rect.LowerRightCorner.Y = rect.UpperLeftCorner.Y + font_height; + addStaticText(Environment, spec.flabel.c_str(), rect, false, true, this, 0); + } + } + + if (parts.size() >= 4) { + // TODO: remove after 2016-11-03 + warningstream << "field/simple: use field_close_on_enter[name, enabled]" << + " instead of the 4th param" << std::endl; + field_close_on_enter[name] = is_yes(parts[3]); + } + + m_fields.push_back(spec); +} + +void GUIFormSpecMenu::parseTextArea(parserData* data, std::vector<std::string>& parts, + const std::string &type) +{ + + std::vector<std::string> v_pos = split(parts[0],','); + std::vector<std::string> v_geom = split(parts[1],','); + std::string name = parts[2]; + std::string label = parts[3]; + std::string default_val = parts[4]; + + MY_CHECKPOS(type,0); + MY_CHECKGEOM(type,1); + + v2s32 pos = pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float) spacing.X; + pos.Y += stof(v_pos[1]) * (float) spacing.Y; + + v2s32 geom; + + geom.X = (stof(v_geom[0]) * (float)spacing.X)-(spacing.X-imgsize.X); + + if (type == "textarea") + { + geom.Y = (stof(v_geom[1]) * (float)imgsize.Y) - (spacing.Y-imgsize.Y); + pos.Y += m_btn_height; + } + else + { + pos.Y += (stof(v_geom[1]) * (float)imgsize.Y)/2; + pos.Y -= m_btn_height; + geom.Y = m_btn_height*2; + } + + core::rect<s32> rect = core::rect<s32>(pos.X, pos.Y, pos.X+geom.X, pos.Y+geom.Y); + + if(!data->explicit_size) + warningstream<<"invalid use of positioned "<<type<<" without a size[] element"<<std::endl; + + if(m_form_src) + default_val = m_form_src->resolveText(default_val); + + + std::wstring wlabel = translate_string(utf8_to_wide(unescape_string(label))); + + FieldSpec spec( + name, + wlabel, + utf8_to_wide(unescape_string(default_val)), + 258+m_fields.size() + ); + + bool is_editable = !name.empty(); + + if (is_editable) + spec.send = true; + + gui::IGUIEditBox *e = nullptr; + const wchar_t *text = spec.fdefault.empty() ? + wlabel.c_str() : spec.fdefault.c_str(); + +#if USE_FREETYPE && IRRLICHT_VERSION_MAJOR == 1 && IRRLICHT_VERSION_MINOR < 9 + if (g_settings->getBool("freetype")) { + e = (gui::IGUIEditBox *) new gui::intlGUIEditBox(text, + true, Environment, this, spec.fid, rect, is_editable, true); + e->drop(); + } else { +#else + { +#endif + e = new GUIEditBoxWithScrollBar(text, true, + Environment, this, spec.fid, rect, is_editable, true); + } + + if (is_editable && spec.fname == data->focused_fieldname) + Environment->setFocus(e); + + if (e) { + if (type == "textarea") + { + e->setMultiLine(true); + e->setWordWrap(true); + e->setTextAlignment(gui::EGUIA_UPPERLEFT, gui::EGUIA_UPPERLEFT); + } else { + irr::SEvent evt; + evt.EventType = EET_KEY_INPUT_EVENT; + evt.KeyInput.Key = KEY_END; + evt.KeyInput.Char = 0; + evt.KeyInput.Control = 0; + evt.KeyInput.Shift = 0; + evt.KeyInput.PressedDown = true; + e->OnEvent(evt); + } + } + + if (is_editable && !label.empty()) { + int font_height = g_fontengine->getTextHeight(); + rect.UpperLeftCorner.Y -= font_height; + rect.LowerRightCorner.Y = rect.UpperLeftCorner.Y + font_height; + addStaticText(Environment, spec.flabel.c_str(), rect, false, true, this, 0); + } + + if (parts.size() >= 6) { + // TODO: remove after 2016-11-03 + warningstream << "field/textarea: use field_close_on_enter[name, enabled]" << + " instead of the 6th param" << std::endl; + field_close_on_enter[name] = is_yes(parts[5]); + } + + m_fields.push_back(spec); +} + +void GUIFormSpecMenu::parseField(parserData* data, const std::string &element, + const std::string &type) +{ + std::vector<std::string> parts = split(element,';'); + + if (parts.size() == 3 || parts.size() == 4) { + parseSimpleField(data,parts); + return; + } + + if ((parts.size() == 5) || (parts.size() == 6) || + ((parts.size() > 6) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + parseTextArea(data,parts,type); + return; + } + errorstream<< "Invalid field element(" << parts.size() << "): '" << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseLabel(parserData* data, const std::string &element) +{ + std::vector<std::string> parts = split(element,';'); + + if ((parts.size() == 2) || + ((parts.size() > 2) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + std::vector<std::string> v_pos = split(parts[0],','); + std::string text = parts[1]; + + MY_CHECKPOS("label",0); + + v2s32 pos = padding + pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float)spacing.X; + pos.Y += (stof(v_pos[1]) + 7.0/30.0) * (float)spacing.Y; + + if(!data->explicit_size) + warningstream<<"invalid use of label without a size[] element"<<std::endl; + + std::vector<std::string> lines = split(text, '\n'); + + for (unsigned int i = 0; i != lines.size(); i++) { + // Lines are spaced at the nominal distance of + // 2/5 inventory slot, even if the font doesn't + // quite match that. This provides consistent + // form layout, at the expense of sometimes + // having sub-optimal spacing for the font. + // We multiply by 2 and then divide by 5, rather + // than multiply by 0.4, to get exact results + // in the integer cases: 0.4 is not exactly + // representable in binary floating point. + s32 posy = pos.Y + ((float)i) * spacing.Y * 2.0 / 5.0; + std::wstring wlabel = utf8_to_wide(unescape_string(lines[i])); + core::rect<s32> rect = core::rect<s32>( + pos.X, posy - m_btn_height, + pos.X + m_font->getDimension(wlabel.c_str()).Width, + posy + m_btn_height); + FieldSpec spec( + "", + wlabel, + L"", + 258+m_fields.size() + ); + gui::IGUIStaticText *e = + addStaticText(Environment, spec.flabel.c_str(), + rect, false, false, this, spec.fid); + e->setTextAlignment(gui::EGUIA_UPPERLEFT, + gui::EGUIA_CENTER); + m_fields.push_back(spec); + } + + return; + } + errorstream<< "Invalid label element(" << parts.size() << "): '" << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseVertLabel(parserData* data, const std::string &element) +{ + std::vector<std::string> parts = split(element,';'); + + if ((parts.size() == 2) || + ((parts.size() > 2) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + std::vector<std::string> v_pos = split(parts[0],','); + std::wstring text = unescape_translate( + unescape_string(utf8_to_wide(parts[1]))); + + MY_CHECKPOS("vertlabel",1); + + v2s32 pos = padding + pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float)spacing.X; + pos.Y += stof(v_pos[1]) * (float)spacing.Y; + + core::rect<s32> rect = core::rect<s32>( + pos.X, pos.Y+((imgsize.Y/2)- m_btn_height), + pos.X+15, pos.Y + + font_line_height(m_font) + * (text.length()+1) + +((imgsize.Y/2)- m_btn_height)); + //actually text.length() would be correct but adding +1 avoids to break all mods + + if(!data->explicit_size) + warningstream<<"invalid use of label without a size[] element"<<std::endl; + + std::wstring label; + + for (wchar_t i : text) { + label += i; + label += L"\n"; + } + + FieldSpec spec( + "", + label, + L"", + 258+m_fields.size() + ); + gui::IGUIStaticText *t = + addStaticText(Environment, spec.flabel.c_str(), rect, false, false, this, spec.fid); + t->setTextAlignment(gui::EGUIA_CENTER, gui::EGUIA_CENTER); + m_fields.push_back(spec); + return; + } + errorstream<< "Invalid vertlabel element(" << parts.size() << "): '" << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseImageButton(parserData* data, const std::string &element, + const std::string &type) +{ + std::vector<std::string> parts = split(element,';'); + + if ((((parts.size() >= 5) && (parts.size() <= 8)) && (parts.size() != 6)) || + ((parts.size() > 8) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + std::vector<std::string> v_pos = split(parts[0],','); + std::vector<std::string> v_geom = split(parts[1],','); + std::string image_name = parts[2]; + std::string name = parts[3]; + std::string label = parts[4]; + + MY_CHECKPOS("imagebutton",0); + MY_CHECKGEOM("imagebutton",1); + + v2s32 pos = padding + pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float)spacing.X; + pos.Y += stof(v_pos[1]) * (float)spacing.Y; + v2s32 geom; + geom.X = (stof(v_geom[0]) * (float)spacing.X)-(spacing.X-imgsize.X); + geom.Y = (stof(v_geom[1]) * (float)spacing.Y)-(spacing.Y-imgsize.Y); + + bool noclip = false; + bool drawborder = true; + std::string pressed_image_name; + + if (parts.size() >= 7) { + if (parts[5] == "true") + noclip = true; + if (parts[6] == "false") + drawborder = false; + } + + if (parts.size() >= 8) { + pressed_image_name = parts[7]; + } + + core::rect<s32> rect = core::rect<s32>(pos.X, pos.Y, pos.X+geom.X, pos.Y+geom.Y); + + if(!data->explicit_size) + warningstream<<"invalid use of image_button without a size[] element"<<std::endl; + + image_name = unescape_string(image_name); + pressed_image_name = unescape_string(pressed_image_name); + + std::wstring wlabel = utf8_to_wide(unescape_string(label)); + + FieldSpec spec( + name, + wlabel, + utf8_to_wide(image_name), + 258+m_fields.size() + ); + spec.ftype = f_Button; + if(type == "image_button_exit") + spec.is_exit = true; + + video::ITexture *texture = 0; + video::ITexture *pressed_texture = 0; + texture = m_tsrc->getTexture(image_name); + if (!pressed_image_name.empty()) + pressed_texture = m_tsrc->getTexture(pressed_image_name); + else + pressed_texture = texture; + + gui::IGUIButton *e = Environment->addButton(rect, this, spec.fid, spec.flabel.c_str()); + + if (spec.fname == data->focused_fieldname) { + Environment->setFocus(e); + } + + e->setUseAlphaChannel(true); + e->setImage(guiScalingImageButton( + Environment->getVideoDriver(), texture, geom.X, geom.Y)); + e->setPressedImage(guiScalingImageButton( + Environment->getVideoDriver(), pressed_texture, geom.X, geom.Y)); + e->setScaleImage(true); + e->setNotClipped(noclip); + e->setDrawBorder(drawborder); + + m_fields.push_back(spec); + return; + } + + errorstream<< "Invalid imagebutton element(" << parts.size() << "): '" << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseTabHeader(parserData* data, const std::string &element) +{ + std::vector<std::string> parts = split(element,';'); + + if (((parts.size() == 4) || (parts.size() == 6)) || + ((parts.size() > 6) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + std::vector<std::string> v_pos = split(parts[0],','); + std::string name = parts[1]; + std::vector<std::string> buttons = split(parts[2],','); + std::string str_index = parts[3]; + bool show_background = true; + bool show_border = true; + int tab_index = stoi(str_index) -1; + + MY_CHECKPOS("tabheader",0); + + if (parts.size() == 6) { + if (parts[4] == "true") + show_background = false; + if (parts[5] == "false") + show_border = false; + } + + FieldSpec spec( + name, + L"", + L"", + 258+m_fields.size() + ); + + spec.ftype = f_TabHeader; + + v2s32 pos = pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float)spacing.X; + pos.Y += stof(v_pos[1]) * (float)spacing.Y - m_btn_height * 2; + v2s32 geom; + geom.X = DesiredRect.getWidth(); + geom.Y = m_btn_height*2; + + core::rect<s32> rect = core::rect<s32>(pos.X, pos.Y, pos.X+geom.X, + pos.Y+geom.Y); + + gui::IGUITabControl *e = Environment->addTabControl(rect, this, + show_background, show_border, spec.fid); + e->setAlignment(irr::gui::EGUIA_UPPERLEFT, irr::gui::EGUIA_UPPERLEFT, + irr::gui::EGUIA_UPPERLEFT, irr::gui::EGUIA_LOWERRIGHT); + e->setTabHeight(m_btn_height*2); + + if (spec.fname == data->focused_fieldname) { + Environment->setFocus(e); + } + + e->setNotClipped(true); + + for (const std::string &button : buttons) { + e->addTab(unescape_translate(unescape_string( + utf8_to_wide(button))).c_str(), -1); + } + + if ((tab_index >= 0) && + (buttons.size() < INT_MAX) && + (tab_index < (int) buttons.size())) + e->setActiveTab(tab_index); + + m_fields.push_back(spec); + return; + } + errorstream << "Invalid TabHeader element(" << parts.size() << "): '" + << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseItemImageButton(parserData* data, const std::string &element) +{ + + if (m_client == 0) { + warningstream << "invalid use of item_image_button with m_client==0" + << std::endl; + return; + } + + std::vector<std::string> parts = split(element,';'); + + if ((parts.size() == 5) || + ((parts.size() > 5) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + std::vector<std::string> v_pos = split(parts[0],','); + std::vector<std::string> v_geom = split(parts[1],','); + std::string item_name = parts[2]; + std::string name = parts[3]; + std::string label = parts[4]; + + label = unescape_string(label); + item_name = unescape_string(item_name); + + MY_CHECKPOS("itemimagebutton",0); + MY_CHECKGEOM("itemimagebutton",1); + + v2s32 pos = padding + pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float)spacing.X; + pos.Y += stof(v_pos[1]) * (float)spacing.Y; + v2s32 geom; + geom.X = (stof(v_geom[0]) * (float)spacing.X)-(spacing.X-imgsize.X); + geom.Y = (stof(v_geom[1]) * (float)spacing.Y)-(spacing.Y-imgsize.Y); + + core::rect<s32> rect = core::rect<s32>(pos.X, pos.Y, pos.X+geom.X, pos.Y+geom.Y); + + if(!data->explicit_size) + warningstream<<"invalid use of item_image_button without a size[] element"<<std::endl; + + IItemDefManager *idef = m_client->idef(); + ItemStack item; + item.deSerialize(item_name, idef); + + m_tooltips[name] = + TooltipSpec(utf8_to_wide(item.getDefinition(idef).description), + m_default_tooltip_bgcolor, + m_default_tooltip_color); + + FieldSpec spec( + name, + utf8_to_wide(label), + utf8_to_wide(item_name), + 258 + m_fields.size() + ); + + gui::IGUIButton *e = Environment->addButton(rect, this, spec.fid, L""); + + if (spec.fname == data->focused_fieldname) { + Environment->setFocus(e); + } + + spec.ftype = f_Button; + rect+=data->basepos-padding; + spec.rect=rect; + m_fields.push_back(spec); + pos = padding + AbsoluteRect.UpperLeftCorner + pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float) spacing.X; + pos.Y += stof(v_pos[1]) * (float) spacing.Y; + m_itemimages.emplace_back("", item_name, e, pos, geom); + m_static_texts.emplace_back(utf8_to_wide(label), rect, e); + return; + } + errorstream<< "Invalid ItemImagebutton element(" << parts.size() << "): '" << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseBox(parserData* data, const std::string &element) +{ + std::vector<std::string> parts = split(element,';'); + + if ((parts.size() == 3) || + ((parts.size() > 3) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + std::vector<std::string> v_pos = split(parts[0],','); + std::vector<std::string> v_geom = split(parts[1],','); + + MY_CHECKPOS("box",0); + MY_CHECKGEOM("box",1); + + v2s32 pos = padding + AbsoluteRect.UpperLeftCorner + pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float) spacing.X; + pos.Y += stof(v_pos[1]) * (float) spacing.Y; + + v2s32 geom; + geom.X = stof(v_geom[0]) * (float)spacing.X; + geom.Y = stof(v_geom[1]) * (float)spacing.Y; + + video::SColor tmp_color; + + if (parseColorString(parts[2], tmp_color, false)) { + BoxDrawSpec spec(pos, geom, tmp_color); + + m_boxes.push_back(spec); + } + else { + errorstream<< "Invalid Box element(" << parts.size() << "): '" << element << "' INVALID COLOR" << std::endl; + } + return; + } + errorstream<< "Invalid Box element(" << parts.size() << "): '" << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseBackgroundColor(parserData* data, const std::string &element) +{ + std::vector<std::string> parts = split(element,';'); + + if (((parts.size() == 1) || (parts.size() == 2)) || + ((parts.size() > 2) && (m_formspec_version > FORMSPEC_API_VERSION))) { + parseColorString(parts[0], m_bgcolor, false); + + if (parts.size() == 2) { + std::string fullscreen = parts[1]; + m_bgfullscreen = is_yes(fullscreen); + } + + return; + } + + errorstream << "Invalid bgcolor element(" << parts.size() << "): '" << element << "'" + << std::endl; +} + +void GUIFormSpecMenu::parseListColors(parserData* data, const std::string &element) +{ + std::vector<std::string> parts = split(element,';'); + + if (((parts.size() == 2) || (parts.size() == 3) || (parts.size() == 5)) || + ((parts.size() > 5) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + parseColorString(parts[0], m_slotbg_n, false); + parseColorString(parts[1], m_slotbg_h, false); + + if (parts.size() >= 3) { + if (parseColorString(parts[2], m_slotbordercolor, false)) { + m_slotborder = true; + } + } + if (parts.size() == 5) { + video::SColor tmp_color; + + if (parseColorString(parts[3], tmp_color, false)) + m_default_tooltip_bgcolor = tmp_color; + if (parseColorString(parts[4], tmp_color, false)) + m_default_tooltip_color = tmp_color; + } + return; + } + errorstream<< "Invalid listcolors element(" << parts.size() << "): '" << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseTooltip(parserData* data, const std::string &element) +{ + std::vector<std::string> parts = split(element,';'); + if (parts.size() == 2) { + std::string name = parts[0]; + m_tooltips[name] = TooltipSpec(utf8_to_wide(unescape_string(parts[1])), + m_default_tooltip_bgcolor, m_default_tooltip_color); + return; + } + + if (parts.size() == 4) { + std::string name = parts[0]; + video::SColor tmp_color1, tmp_color2; + if ( parseColorString(parts[2], tmp_color1, false) && parseColorString(parts[3], tmp_color2, false) ) { + m_tooltips[name] = TooltipSpec(utf8_to_wide(unescape_string(parts[1])), + tmp_color1, tmp_color2); + return; + } + } + errorstream<< "Invalid tooltip element(" << parts.size() << "): '" << element << "'" << std::endl; +} + +bool GUIFormSpecMenu::parseVersionDirect(const std::string &data) +{ + //some prechecks + if (data.empty()) + return false; + + std::vector<std::string> parts = split(data,'['); + + if (parts.size() < 2) { + return false; + } + + if (parts[0] != "formspec_version") { + return false; + } + + if (is_number(parts[1])) { + m_formspec_version = mystoi(parts[1]); + return true; + } + + return false; +} + +bool GUIFormSpecMenu::parseSizeDirect(parserData* data, const std::string &element) +{ + if (element.empty()) + return false; + + std::vector<std::string> parts = split(element,'['); + + if (parts.size() < 2) + return false; + + std::string type = trim(parts[0]); + std::string description = trim(parts[1]); + + if (type != "size" && type != "invsize") + return false; + + if (type == "invsize") + log_deprecated("Deprecated formspec element \"invsize\" is used"); + + parseSize(data, description); + + return true; +} + +bool GUIFormSpecMenu::parsePositionDirect(parserData *data, const std::string &element) +{ + if (element.empty()) + return false; + + std::vector<std::string> parts = split(element, '['); + + if (parts.size() != 2) + return false; + + std::string type = trim(parts[0]); + std::string description = trim(parts[1]); + + if (type != "position") + return false; + + parsePosition(data, description); + + return true; +} + +void GUIFormSpecMenu::parsePosition(parserData *data, const std::string &element) +{ + std::vector<std::string> parts = split(element, ','); + + if (parts.size() == 2) { + data->offset.X = stof(parts[0]); + data->offset.Y = stof(parts[1]); + return; + } + + errorstream << "Invalid position element (" << parts.size() << "): '" << element << "'" << std::endl; +} + +bool GUIFormSpecMenu::parseAnchorDirect(parserData *data, const std::string &element) +{ + if (element.empty()) + return false; + + std::vector<std::string> parts = split(element, '['); + + if (parts.size() != 2) + return false; + + std::string type = trim(parts[0]); + std::string description = trim(parts[1]); + + if (type != "anchor") + return false; + + parseAnchor(data, description); + + return true; +} + +void GUIFormSpecMenu::parseAnchor(parserData *data, const std::string &element) +{ + std::vector<std::string> parts = split(element, ','); + + if (parts.size() == 2) { + data->anchor.X = stof(parts[0]); + data->anchor.Y = stof(parts[1]); + return; + } + + errorstream << "Invalid anchor element (" << parts.size() << "): '" << element + << "'" << std::endl; +} + +void GUIFormSpecMenu::parseElement(parserData* data, const std::string &element) +{ + //some prechecks + if (element.empty()) + return; + + std::vector<std::string> parts = split(element,'['); + + // ugly workaround to keep compatibility + if (parts.size() > 2) { + if (trim(parts[0]) == "image") { + for (unsigned int i=2;i< parts.size(); i++) { + parts[1] += "[" + parts[i]; + } + } + else { return; } + } + + if (parts.size() < 2) { + return; + } + + std::string type = trim(parts[0]); + std::string description = trim(parts[1]); + + if (type == "container") { + parseContainer(data, description); + return; + } + + if (type == "container_end") { + parseContainerEnd(data); + return; + } + + if (type == "list") { + parseList(data, description); + return; + } + + if (type == "listring") { + parseListRing(data, description); + return; + } + + if (type == "checkbox") { + parseCheckbox(data, description); + return; + } + + if (type == "image") { + parseImage(data, description); + return; + } + + if (type == "item_image") { + parseItemImage(data, description); + return; + } + + if (type == "button" || type == "button_exit") { + parseButton(data, description, type); + return; + } + + if (type == "background") { + parseBackground(data,description); + return; + } + + if (type == "tableoptions"){ + parseTableOptions(data,description); + return; + } + + if (type == "tablecolumns"){ + parseTableColumns(data,description); + return; + } + + if (type == "table"){ + parseTable(data,description); + return; + } + + if (type == "textlist"){ + parseTextList(data,description); + return; + } + + if (type == "dropdown"){ + parseDropDown(data,description); + return; + } + + if (type == "field_close_on_enter") { + parseFieldCloseOnEnter(data, description); + return; + } + + if (type == "pwdfield") { + parsePwdField(data,description); + return; + } + + if ((type == "field") || (type == "textarea")){ + parseField(data,description,type); + return; + } + + if (type == "label") { + parseLabel(data,description); + return; + } + + if (type == "vertlabel") { + parseVertLabel(data,description); + return; + } + + if (type == "item_image_button") { + parseItemImageButton(data,description); + return; + } + + if ((type == "image_button") || (type == "image_button_exit")) { + parseImageButton(data,description,type); + return; + } + + if (type == "tabheader") { + parseTabHeader(data,description); + return; + } + + if (type == "box") { + parseBox(data,description); + return; + } + + if (type == "bgcolor") { + parseBackgroundColor(data,description); + return; + } + + if (type == "listcolors") { + parseListColors(data,description); + return; + } + + if (type == "tooltip") { + parseTooltip(data,description); + return; + } + + if (type == "scrollbar") { + parseScrollBar(data, description); + return; + } + + // Ignore others + infostream << "Unknown DrawSpec: type=" << type << ", data=\"" << description << "\"" + << std::endl; +} + +void GUIFormSpecMenu::regenerateGui(v2u32 screensize) +{ + /* useless to regenerate without a screensize */ + if ((screensize.X <= 0) || (screensize.Y <= 0)) { + return; + } + + parserData mydata; + + //preserve tables + for (auto &m_table : m_tables) { + std::string tablename = m_table.first.fname; + GUITable *table = m_table.second; + mydata.table_dyndata[tablename] = table->getDynamicData(); + } + + //set focus + if (!m_focused_element.empty()) + mydata.focused_fieldname = m_focused_element; + + //preserve focus + gui::IGUIElement *focused_element = Environment->getFocus(); + if (focused_element && focused_element->getParent() == this) { + s32 focused_id = focused_element->getID(); + if (focused_id > 257) { + for (const GUIFormSpecMenu::FieldSpec &field : m_fields) { + if (field.fid == focused_id) { + mydata.focused_fieldname = field.fname; + break; + } + } + } + } + + // Remove children + removeChildren(); + + for (auto &table_it : m_tables) { + table_it.second->drop(); + } + + mydata.size= v2s32(100,100); + mydata.screensize = screensize; + mydata.offset = v2f32(0.5f, 0.5f); + mydata.anchor = v2f32(0.5f, 0.5f); + + // Base position of contents of form + mydata.basepos = getBasePos(); + + /* Convert m_init_draw_spec to m_inventorylists */ + + m_inventorylists.clear(); + m_images.clear(); + m_backgrounds.clear(); + m_itemimages.clear(); + m_tables.clear(); + m_checkboxes.clear(); + m_scrollbars.clear(); + m_fields.clear(); + m_boxes.clear(); + m_tooltips.clear(); + m_inventory_rings.clear(); + m_static_texts.clear(); + m_dropdowns.clear(); + + m_bgfullscreen = false; + + { + v3f formspec_bgcolor = g_settings->getV3F("formspec_default_bg_color"); + m_bgcolor = video::SColor( + (u8) clamp_u8(g_settings->getS32("formspec_default_bg_opacity")), + clamp_u8(myround(formspec_bgcolor.X)), + clamp_u8(myround(formspec_bgcolor.Y)), + clamp_u8(myround(formspec_bgcolor.Z)) + ); + } + + { + v3f formspec_bgcolor = g_settings->getV3F("formspec_fullscreen_bg_color"); + m_fullscreen_bgcolor = video::SColor( + (u8) clamp_u8(g_settings->getS32("formspec_fullscreen_bg_opacity")), + clamp_u8(myround(formspec_bgcolor.X)), + clamp_u8(myround(formspec_bgcolor.Y)), + clamp_u8(myround(formspec_bgcolor.Z)) + ); + } + + + m_slotbg_n = video::SColor(255,128,128,128); + m_slotbg_h = video::SColor(255,192,192,192); + + m_default_tooltip_bgcolor = video::SColor(255,110,130,60); + m_default_tooltip_color = video::SColor(255,255,255,255); + + m_slotbordercolor = video::SColor(200,0,0,0); + m_slotborder = false; + + // Add tooltip + { + assert(!m_tooltip_element); + // Note: parent != this so that the tooltip isn't clipped by the menu rectangle + m_tooltip_element = addStaticText(Environment, L"",core::rect<s32>(0,0,110,18)); + m_tooltip_element->enableOverrideColor(true); + m_tooltip_element->setBackgroundColor(m_default_tooltip_bgcolor); + m_tooltip_element->setDrawBackground(true); + m_tooltip_element->setDrawBorder(true); + m_tooltip_element->setOverrideColor(m_default_tooltip_color); + m_tooltip_element->setTextAlignment(gui::EGUIA_CENTER, gui::EGUIA_CENTER); + m_tooltip_element->setWordWrap(false); + //we're not parent so no autograb for this one! + m_tooltip_element->grab(); + } + + std::vector<std::string> elements = split(m_formspec_string,']'); + unsigned int i = 0; + + /* try to read version from first element only */ + if (!elements.empty()) { + if ( parseVersionDirect(elements[0]) ) { + i++; + } + } + + /* we need size first in order to calculate image scale */ + mydata.explicit_size = false; + for (; i< elements.size(); i++) { + if (!parseSizeDirect(&mydata, elements[i])) { + break; + } + } + + /* "position" element is always after "size" element if it used */ + for (; i< elements.size(); i++) { + if (!parsePositionDirect(&mydata, elements[i])) { + break; + } + } + + /* "anchor" element is always after "position" (or "size" element) if it used */ + for (; i< elements.size(); i++) { + if (!parseAnchorDirect(&mydata, elements[i])) { + break; + } + } + + + if (mydata.explicit_size) { + // compute scaling for specified form size + if (m_lock) { + v2u32 current_screensize = RenderingEngine::get_video_driver()->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); + + mydata.screensize = m_lockscreensize; + } else { + offset = v2s32(0,0); + } + + double gui_scaling = g_settings->getFloat("gui_scaling"); + double screen_dpi = RenderingEngine::getDisplayDensity() * 96; + + double use_imgsize; + if (m_lock) { + // In fixed-size mode, inventory image size + // is 0.53 inch multiplied by the gui_scaling + // config parameter. This magic size is chosen + // to make the main menu (15.5 inventory images + // wide, including border) just fit into the + // default window (800 pixels wide) at 96 DPI + // and default scaling (1.00). + use_imgsize = 0.5555 * screen_dpi * gui_scaling; + } else { + // In variable-size mode, we prefer to make the + // inventory image size 1/15 of screen height, + // multiplied by the gui_scaling config parameter. + // If the preferred size won't fit the whole + // form on the screen, either horizontally or + // vertically, then we scale it down to fit. + // (The magic numbers in the computation of what + // fits arise from the scaling factors in the + // following stanza, including the form border, + // help text space, and 0.1 inventory slot spare.) + // However, a minimum size is also set, that + // the image size can't be less than 0.3 inch + // multiplied by gui_scaling, even if this means + // the form doesn't fit the screen. + double prefer_imgsize = mydata.screensize.Y / 15 * + gui_scaling; + double fitx_imgsize = mydata.screensize.X / + ((5.0/4.0) * (0.5 + mydata.invsize.X)); + double fity_imgsize = mydata.screensize.Y / + ((15.0/13.0) * (0.85 * mydata.invsize.Y)); + double screen_dpi = RenderingEngine::getDisplayDensity() * 96; + double min_imgsize = 0.3 * screen_dpi * gui_scaling; + use_imgsize = MYMAX(min_imgsize, MYMIN(prefer_imgsize, + MYMIN(fitx_imgsize, fity_imgsize))); + } + + // Everything else is scaled in proportion to the + // inventory image size. The inventory slot spacing + // is 5/4 image size horizontally and 15/13 image size + // vertically. The padding around the form (incorporating + // the border of the outer inventory slots) is 3/8 + // image size. Font height (baseline to baseline) + // is 2/5 vertical inventory slot spacing, and button + // half-height is 7/8 of font height. + imgsize = v2s32(use_imgsize, use_imgsize); + spacing = v2s32(use_imgsize*5.0/4, use_imgsize*15.0/13); + padding = v2s32(use_imgsize*3.0/8, use_imgsize*3.0/8); + m_btn_height = use_imgsize*15.0/13 * 0.35; + + m_font = g_fontengine->getFont(); + + mydata.size = v2s32( + padding.X*2+spacing.X*(mydata.invsize.X-1.0)+imgsize.X, + padding.Y*2+spacing.Y*(mydata.invsize.Y-1.0)+imgsize.Y + m_btn_height*2.0/3.0 + ); + DesiredRect = mydata.rect = core::rect<s32>( + (s32)((f32)mydata.screensize.X * mydata.offset.X) - (s32)(mydata.anchor.X * (f32)mydata.size.X) + offset.X, + (s32)((f32)mydata.screensize.Y * mydata.offset.Y) - (s32)(mydata.anchor.Y * (f32)mydata.size.Y) + offset.Y, + (s32)((f32)mydata.screensize.X * mydata.offset.X) + (s32)((1.0 - mydata.anchor.X) * (f32)mydata.size.X) + offset.X, + (s32)((f32)mydata.screensize.Y * mydata.offset.Y) + (s32)((1.0 - mydata.anchor.Y) * (f32)mydata.size.Y) + offset.Y + ); + } else { + // Non-size[] form must consist only of text fields and + // implicit "Proceed" button. Use default font, and + // temporary form size which will be recalculated below. + m_font = g_fontengine->getFont(); + m_btn_height = font_line_height(m_font) * 0.875; + DesiredRect = core::rect<s32>( + (s32)((f32)mydata.screensize.X * mydata.offset.X) - (s32)(mydata.anchor.X * 580.0), + (s32)((f32)mydata.screensize.Y * mydata.offset.Y) - (s32)(mydata.anchor.Y * 300.0), + (s32)((f32)mydata.screensize.X * mydata.offset.X) + (s32)((1.0 - mydata.anchor.X) * 580.0), + (s32)((f32)mydata.screensize.Y * mydata.offset.Y) + (s32)((1.0 - mydata.anchor.Y) * 300.0) + ); + } + recalculateAbsolutePosition(false); + mydata.basepos = getBasePos(); + m_tooltip_element->setOverrideFont(m_font); + + gui::IGUISkin* skin = Environment->getSkin(); + sanity_check(skin); + gui::IGUIFont *old_font = skin->getFont(); + skin->setFont(m_font); + + pos_offset = v2s32(); + for (; i< elements.size(); i++) { + parseElement(&mydata, elements[i]); + } + + if (!container_stack.empty()) { + errorstream << "Invalid formspec string: container was never closed!" + << std::endl; + } + + // If there are fields without explicit size[], add a "Proceed" + // button and adjust size to fit all the fields. + if (!m_fields.empty() && !mydata.explicit_size) { + mydata.rect = core::rect<s32>( + mydata.screensize.X/2 - 580/2, + mydata.screensize.Y/2 - 300/2, + mydata.screensize.X/2 + 580/2, + mydata.screensize.Y/2 + 240/2+(m_fields.size()*60) + ); + DesiredRect = mydata.rect; + recalculateAbsolutePosition(false); + mydata.basepos = getBasePos(); + + { + v2s32 pos = mydata.basepos; + pos.Y = ((m_fields.size()+2)*60); + + v2s32 size = DesiredRect.getSize(); + mydata.rect = + core::rect<s32>(size.X/2-70, pos.Y, + (size.X/2-70)+140, pos.Y + (m_btn_height*2)); + const wchar_t *text = wgettext("Proceed"); + Environment->addButton(mydata.rect, this, 257, text); + delete[] text; + } + + } + + //set initial focus if parser didn't set it + focused_element = Environment->getFocus(); + if (!focused_element + || !isMyChild(focused_element) + || focused_element->getType() == gui::EGUIET_TAB_CONTROL) + setInitialFocus(); + + skin->setFont(old_font); +} + +#ifdef __ANDROID__ +bool GUIFormSpecMenu::getAndroidUIInput() +{ + /* no dialog shown */ + if (m_JavaDialogFieldName == "") { + return false; + } + + /* still waiting */ + if (porting::getInputDialogState() == -1) { + return true; + } + + std::string fieldname = m_JavaDialogFieldName; + m_JavaDialogFieldName = ""; + + /* no value abort dialog processing */ + if (porting::getInputDialogState() != 0) { + return false; + } + + for(std::vector<FieldSpec>::iterator iter = m_fields.begin(); + iter != m_fields.end(); ++iter) { + + if (iter->fname != fieldname) { + continue; + } + IGUIElement* tochange = getElementFromId(iter->fid); + + if (tochange == 0) { + return false; + } + + if (tochange->getType() != irr::gui::EGUIET_EDIT_BOX) { + return false; + } + + std::string text = porting::getInputDialogValue(); + + ((gui::IGUIEditBox*) tochange)-> + setText(utf8_to_wide(text).c_str()); + } + return false; +} +#endif + +GUIFormSpecMenu::ItemSpec GUIFormSpecMenu::getItemAtPos(v2s32 p) const +{ + core::rect<s32> imgrect(0,0,imgsize.X,imgsize.Y); + + for (const GUIFormSpecMenu::ListDrawSpec &s : m_inventorylists) { + for(s32 i=0; i<s.geom.X*s.geom.Y; i++) { + s32 item_i = i + s.start_item_i; + s32 x = (i%s.geom.X) * spacing.X; + s32 y = (i/s.geom.X) * spacing.Y; + v2s32 p0(x,y); + core::rect<s32> rect = imgrect + s.pos + p0; + if(rect.isPointInside(p)) + { + return ItemSpec(s.inventoryloc, s.listname, item_i); + } + } + } + + return ItemSpec(InventoryLocation(), "", -1); +} + +void GUIFormSpecMenu::drawList(const ListDrawSpec &s, int phase, + bool &item_hovered) +{ + video::IVideoDriver* driver = Environment->getVideoDriver(); + + Inventory *inv = m_invmgr->getInventory(s.inventoryloc); + if(!inv){ + warningstream<<"GUIFormSpecMenu::drawList(): " + <<"The inventory location " + <<"\""<<s.inventoryloc.dump()<<"\" doesn't exist" + <<std::endl; + return; + } + InventoryList *ilist = inv->getList(s.listname); + if(!ilist){ + warningstream<<"GUIFormSpecMenu::drawList(): " + <<"The inventory list \""<<s.listname<<"\" @ \"" + <<s.inventoryloc.dump()<<"\" doesn't exist" + <<std::endl; + return; + } + + core::rect<s32> imgrect(0,0,imgsize.X,imgsize.Y); + + for(s32 i=0; i<s.geom.X*s.geom.Y; i++) + { + s32 item_i = i + s.start_item_i; + if(item_i >= (s32) ilist->getSize()) + break; + s32 x = (i%s.geom.X) * spacing.X; + s32 y = (i/s.geom.X) * spacing.Y; + v2s32 p(x,y); + core::rect<s32> rect = imgrect + s.pos + p; + ItemStack item; + if(ilist) + item = ilist->getItem(item_i); + + bool selected = m_selected_item + && m_invmgr->getInventory(m_selected_item->inventoryloc) == inv + && m_selected_item->listname == s.listname + && m_selected_item->i == item_i; + bool hovering = rect.isPointInside(m_pointer); + ItemRotationKind rotation_kind = selected ? IT_ROT_SELECTED : + (hovering ? IT_ROT_HOVERED : IT_ROT_NONE); + + if (phase == 0) { + if (hovering) { + item_hovered = true; + driver->draw2DRectangle(m_slotbg_h, rect, &AbsoluteClippingRect); + } else { + driver->draw2DRectangle(m_slotbg_n, rect, &AbsoluteClippingRect); + } + } + + //Draw inv slot borders + if (m_slotborder) { + s32 x1 = rect.UpperLeftCorner.X; + s32 y1 = rect.UpperLeftCorner.Y; + s32 x2 = rect.LowerRightCorner.X; + s32 y2 = rect.LowerRightCorner.Y; + s32 border = 1; + driver->draw2DRectangle(m_slotbordercolor, + core::rect<s32>(v2s32(x1 - border, y1 - border), + v2s32(x2 + border, y1)), NULL); + driver->draw2DRectangle(m_slotbordercolor, + core::rect<s32>(v2s32(x1 - border, y2), + v2s32(x2 + border, y2 + border)), NULL); + driver->draw2DRectangle(m_slotbordercolor, + core::rect<s32>(v2s32(x1 - border, y1), + v2s32(x1, y2)), NULL); + driver->draw2DRectangle(m_slotbordercolor, + core::rect<s32>(v2s32(x2, y1), + v2s32(x2 + border, y2)), NULL); + } + + if(phase == 1) + { + // Draw item stack + if(selected) + { + item.takeItem(m_selected_amount); + } + if(!item.empty()) + { + drawItemStack(driver, m_font, item, + rect, &AbsoluteClippingRect, m_client, + rotation_kind); + } + + // Draw tooltip + std::wstring tooltip_text; + if (hovering && !m_selected_item) { + const std::string &desc = item.metadata.getString("description"); + if (desc.empty()) + tooltip_text = + utf8_to_wide(item.getDefinition(m_client->idef()).description); + else + tooltip_text = utf8_to_wide(desc); + + if (!item.name.empty()) { + if (tooltip_text.empty()) + tooltip_text = utf8_to_wide(item.name); + if (m_tooltip_append_itemname) + tooltip_text += utf8_to_wide(" [" + item.name + "]"); + } + } + if (!tooltip_text.empty()) { + showTooltip(tooltip_text, m_default_tooltip_color, + m_default_tooltip_bgcolor); + } + } + } +} + +void GUIFormSpecMenu::drawSelectedItem() +{ + video::IVideoDriver* driver = Environment->getVideoDriver(); + + if (!m_selected_item) { + drawItemStack(driver, m_font, ItemStack(), + core::rect<s32>(v2s32(0, 0), v2s32(0, 0)), + NULL, m_client, IT_ROT_DRAGGED); + return; + } + + Inventory *inv = m_invmgr->getInventory(m_selected_item->inventoryloc); + sanity_check(inv); + InventoryList *list = inv->getList(m_selected_item->listname); + sanity_check(list); + ItemStack stack = list->getItem(m_selected_item->i); + stack.count = m_selected_amount; + + core::rect<s32> imgrect(0,0,imgsize.X,imgsize.Y); + core::rect<s32> rect = imgrect + (m_pointer - imgrect.getCenter()); + rect.constrainTo(driver->getViewPort()); + drawItemStack(driver, m_font, stack, rect, NULL, m_client, IT_ROT_DRAGGED); +} + +void GUIFormSpecMenu::drawMenu() +{ + if (m_form_src) { + const std::string &newform = m_form_src->getForm(); + if (newform != m_formspec_string) { + m_formspec_string = newform; + regenerateGui(m_screensize_old); + } + } + + gui::IGUISkin* skin = Environment->getSkin(); + sanity_check(skin != NULL); + gui::IGUIFont *old_font = skin->getFont(); + skin->setFont(m_font); + + updateSelectedItem(); + + video::IVideoDriver* driver = Environment->getVideoDriver(); + + v2u32 screenSize = driver->getScreenSize(); + core::rect<s32> allbg(0, 0, screenSize.X, screenSize.Y); + + if (m_bgfullscreen) + driver->draw2DRectangle(m_fullscreen_bgcolor, allbg, &allbg); + else + driver->draw2DRectangle(m_bgcolor, AbsoluteRect, &AbsoluteClippingRect); + + m_tooltip_element->setVisible(false); + + /* + Draw backgrounds + */ + for (const GUIFormSpecMenu::ImageDrawSpec &spec : m_backgrounds) { + video::ITexture *texture = m_tsrc->getTexture(spec.name); + + if (texture != 0) { + // Image size on screen + core::rect<s32> imgrect(0, 0, spec.geom.X, spec.geom.Y); + // Image rectangle on screen + core::rect<s32> rect = imgrect + spec.pos; + + if (spec.clip) { + core::dimension2d<s32> absrec_size = AbsoluteRect.getSize(); + rect = core::rect<s32>(AbsoluteRect.UpperLeftCorner.X - spec.pos.X, + AbsoluteRect.UpperLeftCorner.Y - spec.pos.Y, + AbsoluteRect.UpperLeftCorner.X + absrec_size.Width + spec.pos.X, + AbsoluteRect.UpperLeftCorner.Y + absrec_size.Height + spec.pos.Y); + } + + const video::SColor color(255,255,255,255); + const video::SColor colors[] = {color,color,color,color}; + draw2DImageFilterScaled(driver, texture, rect, + core::rect<s32>(core::position2d<s32>(0,0), + core::dimension2di(texture->getOriginalSize())), + NULL/*&AbsoluteClippingRect*/, colors, true); + } else { + errorstream << "GUIFormSpecMenu::drawMenu() Draw backgrounds unable to load texture:" << std::endl; + errorstream << "\t" << spec.name << std::endl; + } + } + + /* + Draw Boxes + */ + for (const GUIFormSpecMenu::BoxDrawSpec &spec : m_boxes) { + irr::video::SColor todraw = spec.color; + + todraw.setAlpha(140); + + core::rect<s32> rect(spec.pos.X,spec.pos.Y, + spec.pos.X + spec.geom.X,spec.pos.Y + spec.geom.Y); + + driver->draw2DRectangle(todraw, rect, 0); + } + + /* + Call base class + */ + gui::IGUIElement::draw(); + + /* + Draw images + */ + for (const GUIFormSpecMenu::ImageDrawSpec &spec : m_images) { + video::ITexture *texture = m_tsrc->getTexture(spec.name); + + if (texture != 0) { + const core::dimension2d<u32>& img_origsize = texture->getOriginalSize(); + // Image size on screen + core::rect<s32> imgrect; + + if (spec.scale) + imgrect = core::rect<s32>(0,0,spec.geom.X, spec.geom.Y); + else { + + imgrect = core::rect<s32>(0,0,img_origsize.Width,img_origsize.Height); + } + // Image rectangle on screen + core::rect<s32> rect = imgrect + spec.pos; + const video::SColor color(255,255,255,255); + const video::SColor colors[] = {color,color,color,color}; + draw2DImageFilterScaled(driver, texture, rect, + core::rect<s32>(core::position2d<s32>(0,0),img_origsize), + NULL/*&AbsoluteClippingRect*/, colors, true); + } + else { + errorstream << "GUIFormSpecMenu::drawMenu() Draw images unable to load texture:" << std::endl; + errorstream << "\t" << spec.name << std::endl; + } + } + + /* + Draw item images + */ + for (const GUIFormSpecMenu::ImageDrawSpec &spec : m_itemimages) { + if (m_client == 0) + break; + + IItemDefManager *idef = m_client->idef(); + ItemStack item; + item.deSerialize(spec.item_name, idef); + core::rect<s32> imgrect(0, 0, spec.geom.X, spec.geom.Y); + // Viewport rectangle on screen + core::rect<s32> rect = imgrect + spec.pos; + if (spec.parent_button && spec.parent_button->isPressed()) { +#if (IRRLICHT_VERSION_MAJOR == 1 && IRRLICHT_VERSION_MINOR < 8) + rect += core::dimension2d<s32>( + 0.05 * (float)rect.getWidth(), 0.05 * (float)rect.getHeight()); +#else + rect += core::dimension2d<s32>( + skin->getSize(irr::gui::EGDS_BUTTON_PRESSED_IMAGE_OFFSET_X), + skin->getSize(irr::gui::EGDS_BUTTON_PRESSED_IMAGE_OFFSET_Y)); +#endif + } + drawItemStack(driver, m_font, item, rect, &AbsoluteClippingRect, + m_client, IT_ROT_NONE); + } + + /* + Draw items + Phase 0: Item slot rectangles + Phase 1: Item images; prepare tooltip + */ + bool item_hovered = false; + int start_phase = 0; + for (int phase = start_phase; phase <= 1; phase++) { + for (const GUIFormSpecMenu::ListDrawSpec &spec : m_inventorylists) { + drawList(spec, phase, item_hovered); + } + } + if (!item_hovered) { + drawItemStack(driver, m_font, ItemStack(), + core::rect<s32>(v2s32(0, 0), v2s32(0, 0)), + NULL, m_client, IT_ROT_HOVERED); + } + +/* TODO find way to show tooltips on touchscreen */ +#ifndef HAVE_TOUCHSCREENGUI + m_pointer = RenderingEngine::get_raw_device()->getCursorControl()->getPosition(); +#endif + + /* + Draw static text elements + */ + for (const GUIFormSpecMenu::StaticTextSpec &spec : m_static_texts) { + core::rect<s32> rect = spec.rect; + if (spec.parent_button && spec.parent_button->isPressed()) { +#if (IRRLICHT_VERSION_MAJOR == 1 && IRRLICHT_VERSION_MINOR < 8) + rect += core::dimension2d<s32>( + 0.05 * (float)rect.getWidth(), 0.05 * (float)rect.getHeight()); +#else + // Use image offset instead of text's because its a bit smaller + // and fits better, also TEXT_OFFSET_X is always 0 + rect += core::dimension2d<s32>( + skin->getSize(irr::gui::EGDS_BUTTON_PRESSED_IMAGE_OFFSET_X), + skin->getSize(irr::gui::EGDS_BUTTON_PRESSED_IMAGE_OFFSET_Y)); +#endif + } + video::SColor color(255, 255, 255, 255); + m_font->draw(spec.text.c_str(), rect, color, true, true, &rect); + } + + /* + Draw fields/buttons tooltips + */ + gui::IGUIElement *hovered = + Environment->getRootGUIElement()->getElementFromPoint(m_pointer); + + if (hovered != NULL) { + s32 id = hovered->getID(); + + u64 delta = 0; + if (id == -1) { + m_old_tooltip_id = id; + } else { + if (id == m_old_tooltip_id) { + delta = porting::getDeltaMs(m_hovered_time, porting::getTimeMs()); + } else { + m_hovered_time = porting::getTimeMs(); + m_old_tooltip_id = id; + } + } + + // Find and update the current tooltip + if (id != -1 && delta >= m_tooltip_show_delay) { + for (const FieldSpec &field : m_fields) { + + if (field.fid != id) + continue; + + const std::wstring &text = m_tooltips[field.fname].tooltip; + if (!text.empty()) + showTooltip(text, m_tooltips[field.fname].color, + m_tooltips[field.fname].bgcolor); + + break; + } + } + } + + m_tooltip_element->draw(); + + /* + Draw dragged item stack + */ + drawSelectedItem(); + + skin->setFont(old_font); +} + + +void GUIFormSpecMenu::showTooltip(const std::wstring &text, + const irr::video::SColor &color, const irr::video::SColor &bgcolor) +{ + const std::wstring ntext = translate_string(text); + m_tooltip_element->setOverrideColor(color); + m_tooltip_element->setBackgroundColor(bgcolor); + setStaticText(m_tooltip_element, ntext.c_str()); + + // Tooltip size and offset + s32 tooltip_width = m_tooltip_element->getTextWidth() + m_btn_height; +#if (IRRLICHT_VERSION_MAJOR <= 1 && IRRLICHT_VERSION_MINOR <= 8 && IRRLICHT_VERSION_REVISION < 2) || USE_FREETYPE == 1 + std::vector<std::wstring> text_rows = str_split(ntext, L'\n'); + s32 tooltip_height = m_tooltip_element->getTextHeight() * text_rows.size() + 5; +#else + s32 tooltip_height = m_tooltip_element->getTextHeight() + 5; +#endif + v2u32 screenSize = Environment->getVideoDriver()->getScreenSize(); + int tooltip_offset_x = m_btn_height; + int tooltip_offset_y = m_btn_height; +#ifdef __ANDROID__ + tooltip_offset_x *= 3; + tooltip_offset_y = 0; + if (m_pointer.X > (s32)screenSize.X / 2) + tooltip_offset_x = -(tooltip_offset_x + tooltip_width); +#endif + + // Calculate and set the tooltip position + s32 tooltip_x = m_pointer.X + tooltip_offset_x; + s32 tooltip_y = m_pointer.Y + tooltip_offset_y; + if (tooltip_x + tooltip_width > (s32)screenSize.X) + tooltip_x = (s32)screenSize.X - tooltip_width - m_btn_height; + if (tooltip_y + tooltip_height > (s32)screenSize.Y) + tooltip_y = (s32)screenSize.Y - tooltip_height - m_btn_height; + + m_tooltip_element->setRelativePosition( + core::rect<s32>( + core::position2d<s32>(tooltip_x, tooltip_y), + core::dimension2d<s32>(tooltip_width, tooltip_height) + ) + ); + + // Display the tooltip + m_tooltip_element->setVisible(true); + bringToFront(m_tooltip_element); +} + +void GUIFormSpecMenu::updateSelectedItem() +{ + // If the selected stack has become empty for some reason, deselect it. + // If the selected stack has become inaccessible, deselect it. + // If the selected stack has become smaller, adjust m_selected_amount. + ItemStack selected = verifySelectedItem(); + + // WARNING: BLACK MAGIC + // See if there is a stack suited for our current guess. + // If such stack does not exist, clear the guess. + if (!m_selected_content_guess.name.empty() && + selected.name == m_selected_content_guess.name && + selected.count == m_selected_content_guess.count){ + // Selected item fits the guess. Skip the black magic. + } else if (!m_selected_content_guess.name.empty()) { + bool found = false; + for(u32 i=0; i<m_inventorylists.size() && !found; i++){ + const ListDrawSpec &s = m_inventorylists[i]; + Inventory *inv = m_invmgr->getInventory(s.inventoryloc); + if(!inv) + continue; + InventoryList *list = inv->getList(s.listname); + if(!list) + continue; + for(s32 i=0; i<s.geom.X*s.geom.Y && !found; i++){ + u32 item_i = i + s.start_item_i; + if(item_i >= list->getSize()) + continue; + ItemStack stack = list->getItem(item_i); + if(stack.name == m_selected_content_guess.name && + stack.count == m_selected_content_guess.count){ + found = true; + infostream<<"Client: Changing selected content guess to " + <<s.inventoryloc.dump()<<" "<<s.listname + <<" "<<item_i<<std::endl; + delete m_selected_item; + m_selected_item = new ItemSpec(s.inventoryloc, s.listname, item_i); + m_selected_amount = stack.count; + } + } + } + if(!found){ + infostream<<"Client: Discarding selected content guess: " + <<m_selected_content_guess.getItemString()<<std::endl; + m_selected_content_guess.name = ""; + } + } + + // If craftresult is nonempty and nothing else is selected, select it now. + if(!m_selected_item) + { + for (const GUIFormSpecMenu::ListDrawSpec &s : m_inventorylists) { + if(s.listname == "craftpreview") + { + Inventory *inv = m_invmgr->getInventory(s.inventoryloc); + InventoryList *list = inv->getList("craftresult"); + if(list && list->getSize() >= 1 && !list->getItem(0).empty()) + { + m_selected_item = new ItemSpec; + m_selected_item->inventoryloc = s.inventoryloc; + m_selected_item->listname = "craftresult"; + m_selected_item->i = 0; + m_selected_amount = 0; + m_selected_dragging = false; + break; + } + } + } + } + + // If craftresult is selected, keep the whole stack selected + if(m_selected_item && m_selected_item->listname == "craftresult") + { + m_selected_amount = verifySelectedItem().count; + } +} + +ItemStack GUIFormSpecMenu::verifySelectedItem() +{ + // If the selected stack has become empty for some reason, deselect it. + // If the selected stack has become inaccessible, deselect it. + // If the selected stack has become smaller, adjust m_selected_amount. + // Return the selected stack. + + if(m_selected_item) + { + if(m_selected_item->isValid()) + { + Inventory *inv = m_invmgr->getInventory(m_selected_item->inventoryloc); + if(inv) + { + InventoryList *list = inv->getList(m_selected_item->listname); + if(list && (u32) m_selected_item->i < list->getSize()) + { + ItemStack stack = list->getItem(m_selected_item->i); + if(m_selected_amount > stack.count) + m_selected_amount = stack.count; + if(!stack.empty()) + return stack; + } + } + } + + // selection was not valid + delete m_selected_item; + m_selected_item = NULL; + m_selected_amount = 0; + m_selected_dragging = false; + } + return ItemStack(); +} + +void GUIFormSpecMenu::acceptInput(FormspecQuitMode quitmode=quit_mode_no) +{ + if(m_text_dst) + { + StringMap fields; + + if (quitmode == quit_mode_accept) { + fields["quit"] = "true"; + } + + if (quitmode == quit_mode_cancel) { + fields["quit"] = "true"; + m_text_dst->gotText(fields); + return; + } + + if (current_keys_pending.key_down) { + fields["key_down"] = "true"; + current_keys_pending.key_down = false; + } + + if (current_keys_pending.key_up) { + fields["key_up"] = "true"; + current_keys_pending.key_up = false; + } + + if (current_keys_pending.key_enter) { + fields["key_enter"] = "true"; + current_keys_pending.key_enter = false; + } + + if (!current_field_enter_pending.empty()) { + fields["key_enter_field"] = current_field_enter_pending; + current_field_enter_pending = ""; + } + + if (current_keys_pending.key_escape) { + fields["key_escape"] = "true"; + current_keys_pending.key_escape = false; + } + + for (const GUIFormSpecMenu::FieldSpec &s : m_fields) { + if(s.send) { + std::string name = s.fname; + if (s.ftype == f_Button) { + fields[name] = wide_to_utf8(s.flabel); + } else if (s.ftype == f_Table) { + GUITable *table = getTable(s.fname); + if (table) { + fields[name] = table->checkEvent(); + } + } + else if(s.ftype == f_DropDown) { + // no dynamic cast possible due to some distributions shipped + // without rtti support in irrlicht + IGUIElement * element = getElementFromId(s.fid); + gui::IGUIComboBox *e = NULL; + if ((element) && (element->getType() == gui::EGUIET_COMBO_BOX)) { + e = static_cast<gui::IGUIComboBox*>(element); + } + s32 selected = e->getSelected(); + if (selected >= 0) { + std::vector<std::string> *dropdown_values = + getDropDownValues(s.fname); + if (dropdown_values && selected < (s32)dropdown_values->size()) { + fields[name] = (*dropdown_values)[selected]; + } + } + } + else if (s.ftype == f_TabHeader) { + // no dynamic cast possible due to some distributions shipped + // without rttzi support in irrlicht + IGUIElement * element = getElementFromId(s.fid); + gui::IGUITabControl *e = NULL; + if ((element) && (element->getType() == gui::EGUIET_TAB_CONTROL)) { + e = static_cast<gui::IGUITabControl *>(element); + } + + if (e != 0) { + std::stringstream ss; + ss << (e->getActiveTab() +1); + fields[name] = ss.str(); + } + } + else if (s.ftype == f_CheckBox) { + // no dynamic cast possible due to some distributions shipped + // without rtti support in irrlicht + IGUIElement * element = getElementFromId(s.fid); + gui::IGUICheckBox *e = NULL; + if ((element) && (element->getType() == gui::EGUIET_CHECK_BOX)) { + e = static_cast<gui::IGUICheckBox*>(element); + } + + if (e != 0) { + if (e->isChecked()) + fields[name] = "true"; + else + fields[name] = "false"; + } + } + else if (s.ftype == f_ScrollBar) { + // no dynamic cast possible due to some distributions shipped + // without rtti support in irrlicht + IGUIElement * element = getElementFromId(s.fid); + gui::IGUIScrollBar *e = NULL; + if ((element) && (element->getType() == gui::EGUIET_SCROLL_BAR)) { + e = static_cast<gui::IGUIScrollBar*>(element); + } + + if (e != 0) { + std::stringstream os; + os << e->getPos(); + if (s.fdefault == L"Changed") + fields[name] = "CHG:" + os.str(); + else + fields[name] = "VAL:" + os.str(); + } + } + else + { + IGUIElement* e = getElementFromId(s.fid); + if(e != NULL) { + fields[name] = wide_to_utf8(e->getText()); + } + } + } + } + + m_text_dst->gotText(fields); + } +} + +static bool isChild(gui::IGUIElement * tocheck, gui::IGUIElement * parent) +{ + while(tocheck != NULL) { + if (tocheck == parent) { + return true; + } + tocheck = tocheck->getParent(); + } + return false; +} + +bool GUIFormSpecMenu::preprocessEvent(const SEvent& event) +{ + // The IGUITabControl renders visually using the skin's selected + // font, which we override for the duration of form drawing, + // but computes tab hotspots based on how it would have rendered + // using the font that is selected at the time of button release. + // To make these two consistent, temporarily override the skin's + // font while the IGUITabControl is processing the event. + if (event.EventType == EET_MOUSE_INPUT_EVENT && + event.MouseInput.Event == EMIE_LMOUSE_LEFT_UP) { + s32 x = event.MouseInput.X; + s32 y = event.MouseInput.Y; + gui::IGUIElement *hovered = + Environment->getRootGUIElement()->getElementFromPoint( + core::position2d<s32>(x, y)); + if (hovered && isMyChild(hovered) && + hovered->getType() == gui::EGUIET_TAB_CONTROL) { + gui::IGUISkin* skin = Environment->getSkin(); + sanity_check(skin != NULL); + gui::IGUIFont *old_font = skin->getFont(); + skin->setFont(m_font); + bool retval = hovered->OnEvent(event); + skin->setFont(old_font); + return retval; + } + } + + // Fix Esc/Return key being eaten by checkboxen and tables + if(event.EventType==EET_KEY_INPUT_EVENT) { + KeyPress kp(event.KeyInput); + if (kp == EscapeKey || kp == CancelKey + || kp == getKeySetting("keymap_inventory") + || event.KeyInput.Key==KEY_RETURN) { + gui::IGUIElement *focused = Environment->getFocus(); + if (focused && isMyChild(focused) && + (focused->getType() == gui::EGUIET_LIST_BOX || + focused->getType() == gui::EGUIET_CHECK_BOX)) { + OnEvent(event); + return true; + } + } + } + // Mouse wheel events: send to hovered element instead of focused + if(event.EventType==EET_MOUSE_INPUT_EVENT + && event.MouseInput.Event == EMIE_MOUSE_WHEEL) { + s32 x = event.MouseInput.X; + s32 y = event.MouseInput.Y; + gui::IGUIElement *hovered = + Environment->getRootGUIElement()->getElementFromPoint( + core::position2d<s32>(x, y)); + if (hovered && isMyChild(hovered)) { + hovered->OnEvent(event); + return true; + } + } + + if (event.EventType == EET_MOUSE_INPUT_EVENT) { + s32 x = event.MouseInput.X; + s32 y = event.MouseInput.Y; + gui::IGUIElement *hovered = + Environment->getRootGUIElement()->getElementFromPoint( + core::position2d<s32>(x, y)); + if (event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN) { + m_old_tooltip_id = -1; + } + if (!isChild(hovered,this)) { + if (DoubleClickDetection(event)) { + return true; + } + } + } + + #ifdef __ANDROID__ + // display software keyboard when clicking edit boxes + if (event.EventType == EET_MOUSE_INPUT_EVENT + && event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN) { + gui::IGUIElement *hovered = + Environment->getRootGUIElement()->getElementFromPoint( + core::position2d<s32>(event.MouseInput.X, event.MouseInput.Y)); + if ((hovered) && (hovered->getType() == irr::gui::EGUIET_EDIT_BOX)) { + bool retval = hovered->OnEvent(event); + if (retval) { + Environment->setFocus(hovered); + } + m_JavaDialogFieldName = getNameByID(hovered->getID()); + std::string message = gettext("Enter "); + std::string label = wide_to_utf8(getLabelByID(hovered->getID())); + if (label == "") { + label = "text"; + } + message += gettext(label) + ":"; + + /* single line text input */ + int type = 2; + + /* multi line text input */ + if (((gui::IGUIEditBox*) hovered)->isMultiLineEnabled()) { + type = 1; + } + + /* passwords are always single line */ + if (((gui::IGUIEditBox*) hovered)->isPasswordBox()) { + type = 3; + } + + porting::showInputDialog(gettext("ok"), "", + wide_to_utf8(((gui::IGUIEditBox*) hovered)->getText()), + type); + return retval; + } + } + + if (event.EventType == EET_TOUCH_INPUT_EVENT) + { + SEvent translated; + memset(&translated, 0, sizeof(SEvent)); + translated.EventType = EET_MOUSE_INPUT_EVENT; + gui::IGUIElement* root = Environment->getRootGUIElement(); + + if (!root) { + errorstream + << "GUIFormSpecMenu::preprocessEvent unable to get root element" + << std::endl; + return false; + } + gui::IGUIElement* hovered = root->getElementFromPoint( + core::position2d<s32>( + event.TouchInput.X, + event.TouchInput.Y)); + + translated.MouseInput.X = event.TouchInput.X; + translated.MouseInput.Y = event.TouchInput.Y; + translated.MouseInput.Control = false; + + bool dont_send_event = false; + + if (event.TouchInput.touchedCount == 1) { + switch (event.TouchInput.Event) { + case ETIE_PRESSED_DOWN: + m_pointer = v2s32(event.TouchInput.X,event.TouchInput.Y); + translated.MouseInput.Event = EMIE_LMOUSE_PRESSED_DOWN; + translated.MouseInput.ButtonStates = EMBSM_LEFT; + m_down_pos = m_pointer; + break; + case ETIE_MOVED: + m_pointer = v2s32(event.TouchInput.X,event.TouchInput.Y); + translated.MouseInput.Event = EMIE_MOUSE_MOVED; + translated.MouseInput.ButtonStates = EMBSM_LEFT; + break; + case ETIE_LEFT_UP: + translated.MouseInput.Event = EMIE_LMOUSE_LEFT_UP; + translated.MouseInput.ButtonStates = 0; + hovered = root->getElementFromPoint(m_down_pos); + /* we don't have a valid pointer element use last + * known pointer pos */ + translated.MouseInput.X = m_pointer.X; + translated.MouseInput.Y = m_pointer.Y; + + /* reset down pos */ + m_down_pos = v2s32(0,0); + break; + default: + dont_send_event = true; + //this is not supposed to happen + errorstream + << "GUIFormSpecMenu::preprocessEvent unexpected usecase Event=" + << event.TouchInput.Event << std::endl; + } + } else if ( (event.TouchInput.touchedCount == 2) && + (event.TouchInput.Event == ETIE_PRESSED_DOWN) ) { + hovered = root->getElementFromPoint(m_down_pos); + + translated.MouseInput.Event = EMIE_RMOUSE_PRESSED_DOWN; + translated.MouseInput.ButtonStates = EMBSM_LEFT | EMBSM_RIGHT; + translated.MouseInput.X = m_pointer.X; + translated.MouseInput.Y = m_pointer.Y; + + if (hovered) { + hovered->OnEvent(translated); + } + + translated.MouseInput.Event = EMIE_RMOUSE_LEFT_UP; + translated.MouseInput.ButtonStates = EMBSM_LEFT; + + + if (hovered) { + hovered->OnEvent(translated); + } + dont_send_event = true; + } + /* ignore unhandled 2 touch events ... accidental moving for example */ + else if (event.TouchInput.touchedCount == 2) { + dont_send_event = true; + } + else if (event.TouchInput.touchedCount > 2) { + errorstream + << "GUIFormSpecMenu::preprocessEvent to many multitouch events " + << event.TouchInput.touchedCount << " ignoring them" << std::endl; + } + + if (dont_send_event) { + return true; + } + + /* check if translated event needs to be preprocessed again */ + if (preprocessEvent(translated)) { + return true; + } + if (hovered) { + grab(); + bool retval = hovered->OnEvent(translated); + + if (event.TouchInput.Event == ETIE_LEFT_UP) { + /* reset pointer */ + m_pointer = v2s32(0,0); + } + drop(); + return retval; + } + } + #endif + + if (event.EventType == irr::EET_JOYSTICK_INPUT_EVENT) { + /* TODO add a check like: + if (event.JoystickEvent != joystick_we_listen_for) + return false; + */ + bool handled = m_joystick->handleEvent(event.JoystickEvent); + if (handled) { + if (m_joystick->wasKeyDown(KeyType::ESC)) { + tryClose(); + } else if (m_joystick->wasKeyDown(KeyType::JUMP)) { + if (m_allowclose) { + acceptInput(quit_mode_accept); + quitMenu(); + } + } + } + return handled; + } + + return false; +} + +/******************************************************************************/ +bool GUIFormSpecMenu::DoubleClickDetection(const SEvent event) +{ + /* The following code is for capturing double-clicks of the mouse button + * and translating the double-click into an EET_KEY_INPUT_EVENT event + * -- which closes the form -- under some circumstances. + * + * There have been many github issues reporting this as a bug even though it + * was an intended feature. For this reason, remapping the double-click as + * an ESC must be explicitly set when creating this class via the + * /p remap_dbl_click parameter of the constructor. + */ + + if (!m_remap_dbl_click) + return false; + + if (event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN) { + m_doubleclickdetect[0].pos = m_doubleclickdetect[1].pos; + m_doubleclickdetect[0].time = m_doubleclickdetect[1].time; + + m_doubleclickdetect[1].pos = m_pointer; + m_doubleclickdetect[1].time = porting::getTimeMs(); + } + else if (event.MouseInput.Event == EMIE_LMOUSE_LEFT_UP) { + u64 delta = porting::getDeltaMs(m_doubleclickdetect[0].time, porting::getTimeMs()); + if (delta > 400) { + return false; + } + + double squaredistance = + m_doubleclickdetect[0].pos + .getDistanceFromSQ(m_doubleclickdetect[1].pos); + + if (squaredistance > (30*30)) { + return false; + } + + SEvent* translated = new SEvent(); + assert(translated != 0); + //translate doubleclick to escape + memset(translated, 0, sizeof(SEvent)); + translated->EventType = irr::EET_KEY_INPUT_EVENT; + translated->KeyInput.Key = KEY_ESCAPE; + translated->KeyInput.Control = false; + translated->KeyInput.Shift = false; + translated->KeyInput.PressedDown = true; + translated->KeyInput.Char = 0; + OnEvent(*translated); + + // no need to send the key up event as we're already deleted + // and no one else did notice this event + delete translated; + return true; + } + + return false; +} + +void GUIFormSpecMenu::tryClose() +{ + if (m_allowclose) { + doPause = false; + acceptInput(quit_mode_cancel); + quitMenu(); + } else { + m_text_dst->gotText(L"MenuQuit"); + } +} + +bool GUIFormSpecMenu::OnEvent(const SEvent& event) +{ + if (event.EventType==EET_KEY_INPUT_EVENT) { + KeyPress kp(event.KeyInput); + if (event.KeyInput.PressedDown && ( + (kp == EscapeKey) || (kp == CancelKey) || + ((m_client != NULL) && (kp == getKeySetting("keymap_inventory"))))) { + tryClose(); + return true; + } + + if (m_client != NULL && event.KeyInput.PressedDown && + (kp == getKeySetting("keymap_screenshot"))) { + m_client->makeScreenshot(); + } + if (event.KeyInput.PressedDown && + (event.KeyInput.Key==KEY_RETURN || + event.KeyInput.Key==KEY_UP || + event.KeyInput.Key==KEY_DOWN) + ) { + switch (event.KeyInput.Key) { + case KEY_RETURN: + current_keys_pending.key_enter = true; + break; + case KEY_UP: + current_keys_pending.key_up = true; + break; + case KEY_DOWN: + current_keys_pending.key_down = true; + break; + break; + default: + //can't happen at all! + FATAL_ERROR("Reached a source line that can't ever been reached"); + break; + } + if (current_keys_pending.key_enter && m_allowclose) { + acceptInput(quit_mode_accept); + quitMenu(); + } else { + acceptInput(); + } + return true; + } + + } + + /* Mouse event other than movement, or crossing the border of inventory + field while holding right mouse button + */ + if (event.EventType == EET_MOUSE_INPUT_EVENT && + (event.MouseInput.Event != EMIE_MOUSE_MOVED || + (event.MouseInput.Event == EMIE_MOUSE_MOVED && + event.MouseInput.isRightPressed() && + getItemAtPos(m_pointer).i != getItemAtPos(m_old_pointer).i))) { + + // Get selected item and hovered/clicked item (s) + + m_old_tooltip_id = -1; + updateSelectedItem(); + ItemSpec s = getItemAtPos(m_pointer); + + Inventory *inv_selected = NULL; + Inventory *inv_s = NULL; + InventoryList *list_s = NULL; + + if (m_selected_item) { + inv_selected = m_invmgr->getInventory(m_selected_item->inventoryloc); + sanity_check(inv_selected); + sanity_check(inv_selected->getList(m_selected_item->listname) != NULL); + } + + u32 s_count = 0; + + if (s.isValid()) + do { // breakable + inv_s = m_invmgr->getInventory(s.inventoryloc); + + if (!inv_s) { + errorstream << "InventoryMenu: The selected inventory location " + << "\"" << s.inventoryloc.dump() << "\" doesn't exist" + << std::endl; + s.i = -1; // make it invalid again + break; + } + + list_s = inv_s->getList(s.listname); + if (list_s == NULL) { + verbosestream << "InventoryMenu: The selected inventory list \"" + << s.listname << "\" does not exist" << std::endl; + s.i = -1; // make it invalid again + break; + } + + if ((u32)s.i >= list_s->getSize()) { + infostream << "InventoryMenu: The selected inventory list \"" + << s.listname << "\" is too small (i=" << s.i << ", size=" + << list_s->getSize() << ")" << std::endl; + s.i = -1; // make it invalid again + break; + } + + s_count = list_s->getItem(s.i).count; + } while(0); + + bool identical = (m_selected_item != NULL) && s.isValid() && + (inv_selected == inv_s) && + (m_selected_item->listname == s.listname) && + (m_selected_item->i == s.i); + + // buttons: 0 = left, 1 = right, 2 = middle + // up/down: 0 = down (press), 1 = up (release), 2 = unknown event, -1 movement + int button = 0; + int updown = 2; + if (event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN) + { button = 0; updown = 0; } + else if (event.MouseInput.Event == EMIE_RMOUSE_PRESSED_DOWN) + { button = 1; updown = 0; } + else if (event.MouseInput.Event == EMIE_MMOUSE_PRESSED_DOWN) + { button = 2; updown = 0; } + else if (event.MouseInput.Event == EMIE_LMOUSE_LEFT_UP) + { button = 0; updown = 1; } + else if (event.MouseInput.Event == EMIE_RMOUSE_LEFT_UP) + { button = 1; updown = 1; } + else if (event.MouseInput.Event == EMIE_MMOUSE_LEFT_UP) + { button = 2; updown = 1; } + else if (event.MouseInput.Event == EMIE_MOUSE_MOVED) + { updown = -1;} + + // Set this number to a positive value to generate a move action + // from m_selected_item to s. + u32 move_amount = 0; + + // Set this number to a positive value to generate a move action + // from s to the next inventory ring. + u32 shift_move_amount = 0; + + // Set this number to a positive value to generate a drop action + // from m_selected_item. + u32 drop_amount = 0; + + // Set this number to a positive value to generate a craft action at s. + u32 craft_amount = 0; + + if (updown == 0) { + // Some mouse button has been pressed + + //infostream<<"Mouse button "<<button<<" pressed at p=(" + // <<p.X<<","<<p.Y<<")"<<std::endl; + + m_selected_dragging = false; + + if (s.isValid() && s.listname == "craftpreview") { + // Craft preview has been clicked: craft + craft_amount = (button == 2 ? 10 : 1); + } else if (m_selected_item == NULL) { + if (s_count != 0) { + // Non-empty stack has been clicked: select or shift-move it + m_selected_item = new ItemSpec(s); + + u32 count; + if (button == 1) // right + count = (s_count + 1) / 2; + else if (button == 2) // middle + count = MYMIN(s_count, 10); + else // left + count = s_count; + + if (!event.MouseInput.Shift) { + // no shift: select item + m_selected_amount = count; + m_selected_dragging = true; + m_auto_place = false; + } else { + // shift pressed: move item + if (button != 1) + shift_move_amount = count; + else // count of 1 at left click like after drag & drop + shift_move_amount = 1; + } + } + } else { // m_selected_item != NULL + assert(m_selected_amount >= 1); + + if (s.isValid()) { + // Clicked a slot: move + if (button == 1) // right + move_amount = 1; + else if (button == 2) // middle + move_amount = MYMIN(m_selected_amount, 10); + else // left + move_amount = m_selected_amount; + + if (identical) { + if (move_amount >= m_selected_amount) + m_selected_amount = 0; + else + m_selected_amount -= move_amount; + move_amount = 0; + } + } + else if (!getAbsoluteClippingRect().isPointInside(m_pointer)) { + // Clicked outside of the window: drop + if (button == 1) // right + drop_amount = 1; + else if (button == 2) // middle + drop_amount = MYMIN(m_selected_amount, 10); + else // left + drop_amount = m_selected_amount; + } + } + } + else if (updown == 1) { + // Some mouse button has been released + + //infostream<<"Mouse button "<<button<<" released at p=(" + // <<p.X<<","<<p.Y<<")"<<std::endl; + + if (m_selected_item != NULL && m_selected_dragging && s.isValid()) { + if (!identical) { + // Dragged to different slot: move all selected + move_amount = m_selected_amount; + } + } else if (m_selected_item != NULL && m_selected_dragging && + !(getAbsoluteClippingRect().isPointInside(m_pointer))) { + // Dragged outside of window: drop all selected + drop_amount = m_selected_amount; + } + + m_selected_dragging = false; + // Keep track of whether the mouse button be released + // One click is drag without dropping. Click + release + // + click changes to drop item when moved mode + if (m_selected_item) + m_auto_place = true; + } else if (updown == -1) { + // Mouse has been moved and rmb is down and mouse pointer just + // entered a new inventory field (checked in the entry-if, this + // is the only action here that is generated by mouse movement) + if (m_selected_item != NULL && s.isValid()) { + // Move 1 item + // TODO: middle mouse to move 10 items might be handy + if (m_auto_place) { + // Only move an item if the destination slot is empty + // or contains the same item type as what is going to be + // moved + InventoryList *list_from = inv_selected->getList(m_selected_item->listname); + InventoryList *list_to = list_s; + assert(list_from && list_to); + ItemStack stack_from = list_from->getItem(m_selected_item->i); + ItemStack stack_to = list_to->getItem(s.i); + if (stack_to.empty() || stack_to.name == stack_from.name) + move_amount = 1; + } + } + } + + // Possibly send inventory action to server + if (move_amount > 0) { + // Send IAction::Move + + assert(m_selected_item && m_selected_item->isValid()); + assert(s.isValid()); + + assert(inv_selected && inv_s); + InventoryList *list_from = inv_selected->getList(m_selected_item->listname); + InventoryList *list_to = list_s; + assert(list_from && list_to); + ItemStack stack_from = list_from->getItem(m_selected_item->i); + ItemStack stack_to = list_to->getItem(s.i); + + // Check how many items can be moved + move_amount = stack_from.count = MYMIN(move_amount, stack_from.count); + ItemStack leftover = stack_to.addItem(stack_from, m_client->idef()); + // If source stack cannot be added to destination stack at all, + // they are swapped + if ((leftover.count == stack_from.count) && + (leftover.name == stack_from.name)) { + m_selected_amount = stack_to.count; + // In case the server doesn't directly swap them but instead + // moves stack_to somewhere else, set this + m_selected_content_guess = stack_to; + m_selected_content_guess_inventory = s.inventoryloc; + } + // Source stack goes fully into destination stack + else if (leftover.empty()) { + m_selected_amount -= move_amount; + m_selected_content_guess = ItemStack(); // Clear + } + // Source stack goes partly into destination stack + else { + move_amount -= leftover.count; + m_selected_amount -= move_amount; + m_selected_content_guess = ItemStack(); // Clear + } + + infostream << "Handing IAction::Move to manager" << std::endl; + IMoveAction *a = new IMoveAction(); + a->count = move_amount; + a->from_inv = m_selected_item->inventoryloc; + a->from_list = m_selected_item->listname; + a->from_i = m_selected_item->i; + a->to_inv = s.inventoryloc; + a->to_list = s.listname; + a->to_i = s.i; + m_invmgr->inventoryAction(a); + } else if (shift_move_amount > 0) { + u32 mis = m_inventory_rings.size(); + u32 i = 0; + for (; i < mis; i++) { + const ListRingSpec &sp = m_inventory_rings[i]; + if (sp.inventoryloc == s.inventoryloc + && sp.listname == s.listname) + break; + } + do { + if (i >= mis) // if not found + break; + u32 to_inv_ind = (i + 1) % mis; + const ListRingSpec &to_inv_sp = m_inventory_rings[to_inv_ind]; + InventoryList *list_from = list_s; + if (!s.isValid()) + break; + Inventory *inv_to = m_invmgr->getInventory(to_inv_sp.inventoryloc); + if (!inv_to) + break; + InventoryList *list_to = inv_to->getList(to_inv_sp.listname); + if (!list_to) + break; + ItemStack stack_from = list_from->getItem(s.i); + assert(shift_move_amount <= stack_from.count); + if (m_client->getProtoVersion() >= 25) { + infostream << "Handing IAction::Move to manager" << std::endl; + IMoveAction *a = new IMoveAction(); + a->count = shift_move_amount; + a->from_inv = s.inventoryloc; + a->from_list = s.listname; + a->from_i = s.i; + a->to_inv = to_inv_sp.inventoryloc; + a->to_list = to_inv_sp.listname; + a->move_somewhere = true; + m_invmgr->inventoryAction(a); + } else { + // find a place (or more than one) to add the new item + u32 ilt_size = list_to->getSize(); + ItemStack leftover; + for (u32 slot_to = 0; slot_to < ilt_size + && shift_move_amount > 0; slot_to++) { + list_to->itemFits(slot_to, stack_from, &leftover); + if (leftover.count < stack_from.count) { + infostream << "Handing IAction::Move to manager" << std::endl; + IMoveAction *a = new IMoveAction(); + a->count = MYMIN(shift_move_amount, + (u32) (stack_from.count - leftover.count)); + shift_move_amount -= a->count; + a->from_inv = s.inventoryloc; + a->from_list = s.listname; + a->from_i = s.i; + a->to_inv = to_inv_sp.inventoryloc; + a->to_list = to_inv_sp.listname; + a->to_i = slot_to; + m_invmgr->inventoryAction(a); + stack_from = leftover; + } + } + } + } while (0); + } else if (drop_amount > 0) { + m_selected_content_guess = ItemStack(); // Clear + + // Send IAction::Drop + + assert(m_selected_item && m_selected_item->isValid()); + assert(inv_selected); + InventoryList *list_from = inv_selected->getList(m_selected_item->listname); + assert(list_from); + ItemStack stack_from = list_from->getItem(m_selected_item->i); + + // Check how many items can be dropped + drop_amount = stack_from.count = MYMIN(drop_amount, stack_from.count); + assert(drop_amount > 0 && drop_amount <= m_selected_amount); + m_selected_amount -= drop_amount; + + infostream << "Handing IAction::Drop to manager" << std::endl; + IDropAction *a = new IDropAction(); + a->count = drop_amount; + a->from_inv = m_selected_item->inventoryloc; + a->from_list = m_selected_item->listname; + a->from_i = m_selected_item->i; + m_invmgr->inventoryAction(a); + } else if (craft_amount > 0) { + m_selected_content_guess = ItemStack(); // Clear + + // Send IAction::Craft + + assert(s.isValid()); + assert(inv_s); + + infostream << "Handing IAction::Craft to manager" << std::endl; + ICraftAction *a = new ICraftAction(); + a->count = craft_amount; + a->craft_inv = s.inventoryloc; + m_invmgr->inventoryAction(a); + } + + // If m_selected_amount has been decreased to zero, deselect + if (m_selected_amount == 0) { + delete m_selected_item; + m_selected_item = NULL; + m_selected_amount = 0; + m_selected_dragging = false; + m_selected_content_guess = ItemStack(); + } + m_old_pointer = m_pointer; + } + if (event.EventType == EET_GUI_EVENT) { + + if (event.GUIEvent.EventType == gui::EGET_TAB_CHANGED + && isVisible()) { + // find the element that was clicked + for (GUIFormSpecMenu::FieldSpec &s : m_fields) { + if ((s.ftype == f_TabHeader) && + (s.fid == event.GUIEvent.Caller->getID())) { + s.send = true; + acceptInput(); + s.send = false; + return true; + } + } + } + if (event.GUIEvent.EventType == gui::EGET_ELEMENT_FOCUS_LOST + && isVisible()) { + if (!canTakeFocus(event.GUIEvent.Element)) { + infostream<<"GUIFormSpecMenu: Not allowing focus change." + <<std::endl; + // Returning true disables focus change + return true; + } + } + if ((event.GUIEvent.EventType == gui::EGET_BUTTON_CLICKED) || + (event.GUIEvent.EventType == gui::EGET_CHECKBOX_CHANGED) || + (event.GUIEvent.EventType == gui::EGET_COMBO_BOX_CHANGED) || + (event.GUIEvent.EventType == gui::EGET_SCROLL_BAR_CHANGED)) { + unsigned int btn_id = event.GUIEvent.Caller->getID(); + + if (btn_id == 257) { + if (m_allowclose) { + acceptInput(quit_mode_accept); + quitMenu(); + } else { + acceptInput(); + m_text_dst->gotText(L"ExitButton"); + } + // quitMenu deallocates menu + return true; + } + + // find the element that was clicked + for (GUIFormSpecMenu::FieldSpec &s : m_fields) { + // if its a button, set the send field so + // lua knows which button was pressed + if ((s.ftype == f_Button || s.ftype == f_CheckBox) && + s.fid == event.GUIEvent.Caller->getID()) { + s.send = true; + if (s.is_exit) { + if (m_allowclose) { + acceptInput(quit_mode_accept); + quitMenu(); + } else { + m_text_dst->gotText(L"ExitButton"); + } + return true; + } + + acceptInput(quit_mode_no); + s.send = false; + return true; + + } else if ((s.ftype == f_DropDown) && + (s.fid == event.GUIEvent.Caller->getID())) { + // only send the changed dropdown + for (GUIFormSpecMenu::FieldSpec &s2 : m_fields) { + if (s2.ftype == f_DropDown) { + s2.send = false; + } + } + s.send = true; + acceptInput(quit_mode_no); + + // revert configuration to make sure dropdowns are sent on + // regular button click + for (GUIFormSpecMenu::FieldSpec &s2 : m_fields) { + if (s2.ftype == f_DropDown) { + s2.send = true; + } + } + return true; + } else if ((s.ftype == f_ScrollBar) && + (s.fid == event.GUIEvent.Caller->getID())) { + s.fdefault = L"Changed"; + acceptInput(quit_mode_no); + s.fdefault = L""; + } + } + } + + if (event.GUIEvent.EventType == gui::EGET_EDITBOX_ENTER) { + if (event.GUIEvent.Caller->getID() > 257) { + bool close_on_enter = true; + for (GUIFormSpecMenu::FieldSpec &s : m_fields) { + if (s.ftype == f_Unknown && + s.fid == event.GUIEvent.Caller->getID()) { + current_field_enter_pending = s.fname; + std::unordered_map<std::string, bool>::const_iterator it = + field_close_on_enter.find(s.fname); + if (it != field_close_on_enter.end()) + close_on_enter = (*it).second; + + break; + } + } + + if (m_allowclose && close_on_enter) { + current_keys_pending.key_enter = true; + acceptInput(quit_mode_accept); + quitMenu(); + } else { + current_keys_pending.key_enter = true; + acceptInput(); + } + // quitMenu deallocates menu + return true; + } + } + + if (event.GUIEvent.EventType == gui::EGET_TABLE_CHANGED) { + int current_id = event.GUIEvent.Caller->getID(); + if (current_id > 257) { + // find the element that was clicked + for (GUIFormSpecMenu::FieldSpec &s : m_fields) { + // if it's a table, set the send field + // so lua knows which table was changed + if ((s.ftype == f_Table) && (s.fid == current_id)) { + s.send = true; + acceptInput(); + s.send=false; + } + } + return true; + } + } + } + + return Parent ? Parent->OnEvent(event) : false; +} + +/** + * get name of element by element id + * @param id of element + * @return name string or empty string + */ +std::string GUIFormSpecMenu::getNameByID(s32 id) +{ + for (FieldSpec &spec : m_fields) { + if (spec.fid == id) { + return spec.fname; + } + } + return ""; +} + +/** + * get label of element by id + * @param id of element + * @return label string or empty string + */ +std::wstring GUIFormSpecMenu::getLabelByID(s32 id) +{ + for (FieldSpec &spec : m_fields) { + if (spec.fid == id) { + return spec.flabel; + } + } + return L""; +} diff --git a/src/gui/guiFormSpecMenu.h b/src/gui/guiFormSpecMenu.h new file mode 100644 index 000000000..071efb37f --- /dev/null +++ b/src/gui/guiFormSpecMenu.h @@ -0,0 +1,565 @@ +/* +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. +*/ + +#pragma once + +#include <utility> +#include <stack> + +#include "irrlichttypes_extrabloated.h" +#include "inventorymanager.h" +#include "modalMenu.h" +#include "guiTable.h" +#include "network/networkprotocol.h" +#include "client/joystick_controller.h" +#include "util/string.h" +#include "util/enriched_string.h" + +class InventoryManager; +class ISimpleTextureSource; +class Client; + +typedef enum { + f_Button, + f_Table, + f_TabHeader, + f_CheckBox, + f_DropDown, + f_ScrollBar, + f_Unknown +} FormspecFieldType; + +typedef enum { + quit_mode_no, + quit_mode_accept, + quit_mode_cancel +} FormspecQuitMode; + +struct TextDest +{ + virtual ~TextDest() = default; + + // This is deprecated I guess? -celeron55 + virtual void gotText(const std::wstring &text) {} + virtual void gotText(const StringMap &fields) = 0; + + std::string m_formname; +}; + +class IFormSource +{ +public: + virtual ~IFormSource() = default; + virtual const std::string &getForm() const = 0; + // Fill in variables in field text + virtual std::string resolveText(const std::string &str) { return str; } +}; + +class GUIFormSpecMenu : public GUIModalMenu +{ + struct ItemSpec + { + ItemSpec() = default; + + ItemSpec(const InventoryLocation &a_inventoryloc, + const std::string &a_listname, + s32 a_i) : + inventoryloc(a_inventoryloc), + listname(a_listname), + i(a_i) + { + } + + bool isValid() const { return i != -1; } + + InventoryLocation inventoryloc; + std::string listname; + s32 i = -1; + }; + + struct ListDrawSpec + { + ListDrawSpec() = default; + + ListDrawSpec(const InventoryLocation &a_inventoryloc, + const std::string &a_listname, + v2s32 a_pos, v2s32 a_geom, s32 a_start_item_i): + inventoryloc(a_inventoryloc), + listname(a_listname), + pos(a_pos), + geom(a_geom), + start_item_i(a_start_item_i) + { + } + + InventoryLocation inventoryloc; + std::string listname; + v2s32 pos; + v2s32 geom; + s32 start_item_i; + }; + + struct ListRingSpec + { + ListRingSpec() = default; + + ListRingSpec(const InventoryLocation &a_inventoryloc, + const std::string &a_listname): + inventoryloc(a_inventoryloc), + listname(a_listname) + { + } + + InventoryLocation inventoryloc; + std::string listname; + }; + + struct ImageDrawSpec + { + ImageDrawSpec(): + parent_button(NULL), + clip(false) + { + } + + ImageDrawSpec(const std::string &a_name, + const std::string &a_item_name, + gui::IGUIButton *a_parent_button, + const v2s32 &a_pos, const v2s32 &a_geom): + name(a_name), + item_name(a_item_name), + parent_button(a_parent_button), + pos(a_pos), + geom(a_geom), + scale(true), + clip(false) + { + } + + ImageDrawSpec(const std::string &a_name, + const std::string &a_item_name, + const v2s32 &a_pos, const v2s32 &a_geom): + name(a_name), + item_name(a_item_name), + parent_button(NULL), + pos(a_pos), + geom(a_geom), + scale(true), + clip(false) + { + } + + ImageDrawSpec(const std::string &a_name, + const v2s32 &a_pos, const v2s32 &a_geom, bool clip=false): + name(a_name), + parent_button(NULL), + pos(a_pos), + geom(a_geom), + scale(true), + clip(clip) + { + } + + ImageDrawSpec(const std::string &a_name, + const v2s32 &a_pos): + name(a_name), + parent_button(NULL), + pos(a_pos), + scale(false), + clip(false) + { + } + + std::string name; + std::string item_name; + gui::IGUIButton *parent_button; + v2s32 pos; + v2s32 geom; + bool scale; + bool clip; + }; + + struct FieldSpec + { + FieldSpec() = default; + + FieldSpec(const std::string &name, const std::wstring &label, + const std::wstring &default_text, int id) : + fname(name), + flabel(label), + fdefault(unescape_enriched(translate_string(default_text))), + fid(id), + send(false), + ftype(f_Unknown), + is_exit(false) + { + } + + std::string fname; + std::wstring flabel; + std::wstring fdefault; + int fid; + bool send; + FormspecFieldType ftype; + bool is_exit; + core::rect<s32> rect; + }; + + struct BoxDrawSpec + { + BoxDrawSpec(v2s32 a_pos, v2s32 a_geom,irr::video::SColor a_color): + pos(a_pos), + geom(a_geom), + color(a_color) + { + } + v2s32 pos; + v2s32 geom; + irr::video::SColor color; + }; + + struct TooltipSpec + { + TooltipSpec() = default; + TooltipSpec(const std::wstring &a_tooltip, irr::video::SColor a_bgcolor, + irr::video::SColor a_color): + tooltip(translate_string(a_tooltip)), + bgcolor(a_bgcolor), + color(a_color) + { + } + + std::wstring tooltip; + irr::video::SColor bgcolor; + irr::video::SColor color; + }; + + struct StaticTextSpec + { + StaticTextSpec(): + parent_button(NULL) + { + } + + StaticTextSpec(const std::wstring &a_text, + const core::rect<s32> &a_rect): + text(a_text), + rect(a_rect), + parent_button(NULL) + { + } + + StaticTextSpec(const std::wstring &a_text, + const core::rect<s32> &a_rect, + gui::IGUIButton *a_parent_button): + text(a_text), + rect(a_rect), + parent_button(a_parent_button) + { + } + + std::wstring text; + core::rect<s32> rect; + gui::IGUIButton *parent_button; + }; + +public: + GUIFormSpecMenu(JoystickController *joystick, + gui::IGUIElement* parent, s32 id, + IMenuManager *menumgr, + Client *client, + ISimpleTextureSource *tsrc, + IFormSource* fs_src, + TextDest* txt_dst, + bool remap_dbl_click = true); + + ~GUIFormSpecMenu(); + + void setFormSpec(const std::string &formspec_string, + const InventoryLocation ¤t_inventory_location) + { + m_formspec_string = formspec_string; + m_current_inventory_location = current_inventory_location; + regenerateGui(m_screensize_old); + } + + // form_src is deleted by this GUIFormSpecMenu + void setFormSource(IFormSource *form_src) + { + delete m_form_src; + m_form_src = form_src; + } + + // text_dst is deleted by this GUIFormSpecMenu + void setTextDest(TextDest *text_dst) + { + delete m_text_dst; + m_text_dst = text_dst; + } + + void allowClose(bool value) + { + m_allowclose = value; + } + + void lockSize(bool lock,v2u32 basescreensize=v2u32(0,0)) + { + m_lock = lock; + m_lockscreensize = basescreensize; + } + + void removeChildren(); + void setInitialFocus(); + + void setFocus(const std::string &elementname) + { + m_focused_element = elementname; + } + + /* + Remove and re-add (or reposition) stuff + */ + void regenerateGui(v2u32 screensize); + + ItemSpec getItemAtPos(v2s32 p) const; + void drawList(const ListDrawSpec &s, int phase, bool &item_hovered); + void drawSelectedItem(); + void drawMenu(); + void updateSelectedItem(); + ItemStack verifySelectedItem(); + + void acceptInput(FormspecQuitMode quitmode); + bool preprocessEvent(const SEvent& event); + bool OnEvent(const SEvent& event); + bool doPause; + bool pausesGame() { return doPause; } + + GUITable* getTable(const std::string &tablename); + std::vector<std::string>* getDropDownValues(const std::string &name); + +#ifdef __ANDROID__ + bool getAndroidUIInput(); +#endif + +protected: + v2s32 getBasePos() const + { + return padding + offset + AbsoluteRect.UpperLeftCorner; + } + + v2s32 padding; + v2s32 spacing; + v2s32 imgsize; + v2s32 offset; + v2s32 pos_offset; + std::stack<v2s32> container_stack; + + InventoryManager *m_invmgr; + ISimpleTextureSource *m_tsrc; + Client *m_client; + + std::string m_formspec_string; + InventoryLocation m_current_inventory_location; + + std::vector<ListDrawSpec> m_inventorylists; + std::vector<ListRingSpec> m_inventory_rings; + std::vector<ImageDrawSpec> m_backgrounds; + std::vector<ImageDrawSpec> m_images; + std::vector<ImageDrawSpec> m_itemimages; + std::vector<BoxDrawSpec> m_boxes; + std::unordered_map<std::string, bool> field_close_on_enter; + std::vector<FieldSpec> m_fields; + std::vector<StaticTextSpec> m_static_texts; + std::vector<std::pair<FieldSpec,GUITable*> > m_tables; + std::vector<std::pair<FieldSpec,gui::IGUICheckBox*> > m_checkboxes; + std::map<std::string, TooltipSpec> m_tooltips; + std::vector<std::pair<FieldSpec,gui::IGUIScrollBar*> > m_scrollbars; + std::vector<std::pair<FieldSpec, std::vector<std::string> > > m_dropdowns; + + ItemSpec *m_selected_item = nullptr; + u32 m_selected_amount = 0; + bool m_selected_dragging = false; + + // WARNING: BLACK MAGIC + // Used to guess and keep up with some special things the server can do. + // If name is "", no guess exists. + ItemStack m_selected_content_guess; + InventoryLocation m_selected_content_guess_inventory; + + v2s32 m_pointer; + v2s32 m_old_pointer; // Mouse position after previous mouse event + gui::IGUIStaticText *m_tooltip_element = nullptr; + + u64 m_tooltip_show_delay; + bool m_tooltip_append_itemname; + u64 m_hovered_time = 0; + s32 m_old_tooltip_id = -1; + + bool m_auto_place = false; + + bool m_allowclose = true; + bool m_lock = false; + v2u32 m_lockscreensize; + + bool m_bgfullscreen; + bool m_slotborder; + video::SColor m_bgcolor; + video::SColor m_fullscreen_bgcolor; + video::SColor m_slotbg_n; + video::SColor m_slotbg_h; + video::SColor m_slotbordercolor; + video::SColor m_default_tooltip_bgcolor; + video::SColor m_default_tooltip_color; + +private: + IFormSource *m_form_src; + TextDest *m_text_dst; + u32 m_formspec_version = 0; + std::string m_focused_element = ""; + JoystickController *m_joystick; + + typedef struct { + bool explicit_size; + v2f invsize; + v2s32 size; + v2f32 offset; + v2f32 anchor; + core::rect<s32> rect; + v2s32 basepos; + v2u32 screensize; + std::string focused_fieldname; + GUITable::TableOptions table_options; + GUITable::TableColumns table_columns; + // used to restore table selection/scroll/treeview state + std::unordered_map<std::string, GUITable::DynamicData> table_dyndata; + } parserData; + + typedef struct { + bool key_up; + bool key_down; + bool key_enter; + bool key_escape; + } fs_key_pendig; + + fs_key_pendig current_keys_pending; + std::string current_field_enter_pending = ""; + + void parseElement(parserData* data, const std::string &element); + + void parseSize(parserData* data, const std::string &element); + void parseContainer(parserData* data, const std::string &element); + void parseContainerEnd(parserData* data); + void parseList(parserData* data, const std::string &element); + void parseListRing(parserData* data, const std::string &element); + void parseCheckbox(parserData* data, const std::string &element); + void parseImage(parserData* data, const std::string &element); + void parseItemImage(parserData* data, const std::string &element); + void parseButton(parserData* data, const std::string &element, + const std::string &typ); + void parseBackground(parserData* data, const std::string &element); + void parseTableOptions(parserData* data, const std::string &element); + void parseTableColumns(parserData* data, const std::string &element); + void parseTable(parserData* data, const std::string &element); + void parseTextList(parserData* data, const std::string &element); + void parseDropDown(parserData* data, const std::string &element); + void parseFieldCloseOnEnter(parserData *data, const std::string &element); + void parsePwdField(parserData* data, const std::string &element); + void parseField(parserData* data, const std::string &element, const std::string &type); + void parseSimpleField(parserData* data,std::vector<std::string> &parts); + void parseTextArea(parserData* data,std::vector<std::string>& parts, + const std::string &type); + void parseLabel(parserData* data, const std::string &element); + void parseVertLabel(parserData* data, const std::string &element); + void parseImageButton(parserData* data, const std::string &element, + const std::string &type); + void parseItemImageButton(parserData* data, const std::string &element); + void parseTabHeader(parserData* data, const std::string &element); + void parseBox(parserData* data, const std::string &element); + void parseBackgroundColor(parserData* data, const std::string &element); + void parseListColors(parserData* data, const std::string &element); + void parseTooltip(parserData* data, const std::string &element); + bool parseVersionDirect(const std::string &data); + bool parseSizeDirect(parserData* data, const std::string &element); + void parseScrollBar(parserData* data, const std::string &element); + bool parsePositionDirect(parserData *data, const std::string &element); + void parsePosition(parserData *data, const std::string &element); + bool parseAnchorDirect(parserData *data, const std::string &element); + void parseAnchor(parserData *data, const std::string &element); + + void tryClose(); + + void showTooltip(const std::wstring &text, const irr::video::SColor &color, + const irr::video::SColor &bgcolor); + + /** + * check if event is part of a double click + * @param event event to evaluate + * @return true/false if a doubleclick was detected + */ + bool DoubleClickDetection(const SEvent event); + + struct clickpos + { + v2s32 pos; + s64 time; + }; + clickpos m_doubleclickdetect[2]; + + int m_btn_height; + gui::IGUIFont *m_font = nullptr; + + std::wstring getLabelByID(s32 id); + std::string getNameByID(s32 id); +#ifdef __ANDROID__ + v2s32 m_down_pos; + std::string m_JavaDialogFieldName; +#endif + + /* If true, remap a double-click (or double-tap) action to ESC. This is so + * that, for example, Android users can double-tap to close a formspec. + * + * This value can (currently) only be set by the class constructor + * and the default value for the setting is true. + */ + bool m_remap_dbl_click; + +}; + +class FormspecFormSource: public IFormSource +{ +public: + FormspecFormSource(const std::string &formspec): + m_formspec(formspec) + { + } + + ~FormspecFormSource() = default; + + void setForm(const std::string &formspec) + { + m_formspec = FORMSPEC_VERSION_STRING + formspec; + } + + const std::string &getForm() const + { + return m_formspec; + } + + std::string m_formspec; +}; diff --git a/src/gui/guiKeyChangeMenu.cpp b/src/gui/guiKeyChangeMenu.cpp new file mode 100644 index 000000000..53677a57b --- /dev/null +++ b/src/gui/guiKeyChangeMenu.cpp @@ -0,0 +1,436 @@ +/* + Minetest + Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com> + Copyright (C) 2013 Ciaran Gultnieks <ciaran@ciarang.com> + Copyright (C) 2013 teddydestodes <derkomtur@schattengang.net> + + 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 "guiKeyChangeMenu.h" +#include "debug.h" +#include "serialization.h" +#include <string> +#include <IGUICheckBox.h> +#include <IGUIEditBox.h> +#include <IGUIButton.h> +#include <IGUIStaticText.h> +#include <IGUIFont.h> +#include "settings.h" +#include <algorithm> + +#include "mainmenumanager.h" // for g_gamecallback + +#define KMaxButtonPerColumns 12 + +extern MainGameCallback *g_gamecallback; + +enum +{ + GUI_ID_BACK_BUTTON = 101, GUI_ID_ABORT_BUTTON, GUI_ID_SCROLL_BAR, + // buttons + GUI_ID_KEY_FORWARD_BUTTON, + GUI_ID_KEY_BACKWARD_BUTTON, + GUI_ID_KEY_LEFT_BUTTON, + GUI_ID_KEY_RIGHT_BUTTON, + GUI_ID_KEY_USE_BUTTON, + GUI_ID_KEY_FLY_BUTTON, + GUI_ID_KEY_FAST_BUTTON, + GUI_ID_KEY_JUMP_BUTTON, + GUI_ID_KEY_NOCLIP_BUTTON, + GUI_ID_KEY_CINEMATIC_BUTTON, + GUI_ID_KEY_CHAT_BUTTON, + GUI_ID_KEY_CMD_BUTTON, + GUI_ID_KEY_CMD_LOCAL_BUTTON, + GUI_ID_KEY_CONSOLE_BUTTON, + GUI_ID_KEY_SNEAK_BUTTON, + GUI_ID_KEY_DROP_BUTTON, + GUI_ID_KEY_INVENTORY_BUTTON, + GUI_ID_KEY_HOTBAR_PREV_BUTTON, + GUI_ID_KEY_HOTBAR_NEXT_BUTTON, + GUI_ID_KEY_MUTE_BUTTON, + GUI_ID_KEY_DEC_VOLUME_BUTTON, + GUI_ID_KEY_INC_VOLUME_BUTTON, + GUI_ID_KEY_RANGE_BUTTON, + GUI_ID_KEY_ZOOM_BUTTON, + GUI_ID_KEY_CAMERA_BUTTON, + GUI_ID_KEY_MINIMAP_BUTTON, + GUI_ID_KEY_SCREENSHOT_BUTTON, + GUI_ID_KEY_CHATLOG_BUTTON, + GUI_ID_KEY_HUD_BUTTON, + GUI_ID_KEY_FOG_BUTTON, + GUI_ID_KEY_DEC_RANGE_BUTTON, + GUI_ID_KEY_INC_RANGE_BUTTON, + GUI_ID_KEY_AUTOFWD_BUTTON, + // other + GUI_ID_CB_AUX1_DESCENDS, + GUI_ID_CB_DOUBLETAP_JUMP, +}; + +GUIKeyChangeMenu::GUIKeyChangeMenu(gui::IGUIEnvironment* env, + gui::IGUIElement* parent, s32 id, IMenuManager *menumgr) : +GUIModalMenu(env, parent, id, menumgr) +{ + init_keys(); + for (key_setting *ks : key_settings) + key_used.push_back(ks->key); +} + +GUIKeyChangeMenu::~GUIKeyChangeMenu() +{ + removeChildren(); + + for (key_setting *ks : key_settings) { + delete[] ks->button_name; + delete ks; + } + key_settings.clear(); +} + +void GUIKeyChangeMenu::removeChildren() +{ + const core::list<gui::IGUIElement*> &children = getChildren(); + core::list<gui::IGUIElement*> children_copy; + for (gui::IGUIElement*i : children) { + children_copy.push_back(i); + } + + for (gui::IGUIElement *i : children_copy) { + i->remove(); + } +} + +void GUIKeyChangeMenu::regenerateGui(v2u32 screensize) +{ + removeChildren(); + v2s32 size(745, 430); + + core::rect < s32 > rect(screensize.X / 2 - size.X / 2, + screensize.Y / 2 - size.Y / 2, screensize.X / 2 + size.X / 2, + screensize.Y / 2 + size.Y / 2); + + DesiredRect = rect; + recalculateAbsolutePosition(false); + + v2s32 topleft(0, 0); + + { + core::rect < s32 > rect(0, 0, 600, 40); + rect += topleft + v2s32(25, 3); + //gui::IGUIStaticText *t = + const wchar_t *text = wgettext("Keybindings. (If this menu screws up, remove stuff from minetest.conf)"); + Environment->addStaticText(text, + rect, false, true, this, -1); + delete[] text; + //t->setTextAlignment(gui::EGUIA_CENTER, gui::EGUIA_UPPERLEFT); + } + + // Build buttons + + v2s32 offset(25, 60); + + for(size_t i = 0; i < key_settings.size(); i++) + { + key_setting *k = key_settings.at(i); + { + core::rect < s32 > rect(0, 0, 150, 20); + rect += topleft + v2s32(offset.X, offset.Y); + Environment->addStaticText(k->button_name, rect, false, true, this, -1); + } + + { + core::rect < s32 > rect(0, 0, 100, 30); + rect += topleft + v2s32(offset.X + 120, offset.Y - 5); + const wchar_t *text = wgettext(k->key.name()); + k->button = Environment->addButton(rect, this, k->id, text); + delete[] text; + } + if ((i + 1) % KMaxButtonPerColumns == 0) { + offset.X += 230; + offset.Y = 60; + } else { + offset += v2s32(0, 25); + } + } + + { + s32 option_x = offset.X; + s32 option_y = offset.Y + 5; + u32 option_w = 180; + { + core::rect<s32> rect(0, 0, option_w, 30); + rect += topleft + v2s32(option_x, option_y); + const wchar_t *text = wgettext("\"Special\" = climb down"); + Environment->addCheckBox(g_settings->getBool("aux1_descends"), rect, this, + GUI_ID_CB_AUX1_DESCENDS, text); + delete[] text; + } + offset += v2s32(0, 25); + } + + { + s32 option_x = offset.X; + s32 option_y = offset.Y + 5; + u32 option_w = 280; + { + core::rect<s32> rect(0, 0, option_w, 30); + rect += topleft + v2s32(option_x, option_y); + const wchar_t *text = wgettext("Double tap \"jump\" to toggle fly"); + Environment->addCheckBox(g_settings->getBool("doubletap_jump"), rect, this, + GUI_ID_CB_DOUBLETAP_JUMP, text); + delete[] text; + } + offset += v2s32(0, 25); + } + + { + core::rect < s32 > rect(0, 0, 100, 30); + rect += topleft + v2s32(size.X / 2 - 105, size.Y - 40); + const wchar_t *text = wgettext("Save"); + Environment->addButton(rect, this, GUI_ID_BACK_BUTTON, + text); + delete[] text; + } + { + core::rect < s32 > rect(0, 0, 100, 30); + rect += topleft + v2s32(size.X / 2 + 5, size.Y - 40); + const wchar_t *text = wgettext("Cancel"); + Environment->addButton(rect, this, GUI_ID_ABORT_BUTTON, + text); + delete[] text; + } +} + +void GUIKeyChangeMenu::drawMenu() +{ + gui::IGUISkin* skin = Environment->getSkin(); + if (!skin) + return; + video::IVideoDriver* driver = Environment->getVideoDriver(); + + video::SColor bgcolor(140, 0, 0, 0); + + { + core::rect < s32 > rect(0, 0, 745, 620); + rect += AbsoluteRect.UpperLeftCorner; + driver->draw2DRectangle(bgcolor, rect, &AbsoluteClippingRect); + } + + gui::IGUIElement::draw(); +} + +bool GUIKeyChangeMenu::acceptInput() +{ + for (key_setting *k : key_settings) { + g_settings->set(k->setting_name, k->key.sym()); + } + + { + gui::IGUIElement *e = getElementFromId(GUI_ID_CB_AUX1_DESCENDS); + if(e != NULL && e->getType() == gui::EGUIET_CHECK_BOX) + g_settings->setBool("aux1_descends", ((gui::IGUICheckBox*)e)->isChecked()); + } + { + gui::IGUIElement *e = getElementFromId(GUI_ID_CB_DOUBLETAP_JUMP); + if(e != NULL && e->getType() == gui::EGUIET_CHECK_BOX) + g_settings->setBool("doubletap_jump", ((gui::IGUICheckBox*)e)->isChecked()); + } + + clearKeyCache(); + + g_gamecallback->signalKeyConfigChange(); + + return true; +} + +bool GUIKeyChangeMenu::resetMenu() +{ + if (activeKey >= 0) + { + for (key_setting *k : key_settings) { + if (k->id == activeKey) { + const wchar_t *text = wgettext(k->key.name()); + k->button->setText(text); + delete[] text; + break; + } + } + activeKey = -1; + return false; + } + return true; +} +bool GUIKeyChangeMenu::OnEvent(const SEvent& event) +{ + if (event.EventType == EET_KEY_INPUT_EVENT && activeKey >= 0 + && event.KeyInput.PressedDown) { + + bool prefer_character = shift_down; + KeyPress kp(event.KeyInput, prefer_character); + + bool shift_went_down = false; + if(!shift_down && + (event.KeyInput.Key == irr::KEY_SHIFT || + event.KeyInput.Key == irr::KEY_LSHIFT || + event.KeyInput.Key == irr::KEY_RSHIFT)) + shift_went_down = true; + + // Remove Key already in use message + if(this->key_used_text) + { + this->key_used_text->remove(); + this->key_used_text = NULL; + } + // Display Key already in use message + if (std::find(this->key_used.begin(), this->key_used.end(), kp) != this->key_used.end()) + { + core::rect < s32 > rect(0, 0, 600, 40); + rect += v2s32(0, 0) + v2s32(25, 30); + const wchar_t *text = wgettext("Key already in use"); + this->key_used_text = Environment->addStaticText(text, + rect, false, true, this, -1); + delete[] text; + //infostream << "Key already in use" << std::endl; + } + + // But go on + { + key_setting *k = NULL; + for (key_setting *ks : key_settings) { + if (ks->id == activeKey) { + k = ks; + break; + } + } + FATAL_ERROR_IF(k == NULL, "Key setting not found"); + k->key = kp; + const wchar_t *text = wgettext(k->key.name()); + k->button->setText(text); + delete[] text; + + this->key_used.push_back(kp); + + // Allow characters made with shift + if(shift_went_down){ + shift_down = true; + return false; + } + + activeKey = -1; + return true; + } + } else if (event.EventType == EET_KEY_INPUT_EVENT && activeKey < 0 + && event.KeyInput.PressedDown + && event.KeyInput.Key == irr::KEY_ESCAPE) { + quitMenu(); + return true; + } else if (event.EventType == EET_GUI_EVENT) { + if (event.GUIEvent.EventType == gui::EGET_ELEMENT_FOCUS_LOST + && isVisible()) + { + if (!canTakeFocus(event.GUIEvent.Element)) + { + dstream << "GUIMainMenu: Not allowing focus change." + << std::endl; + // Returning true disables focus change + return true; + } + } + if (event.GUIEvent.EventType == gui::EGET_BUTTON_CLICKED) + { + switch (event.GUIEvent.Caller->getID()) + { + case GUI_ID_BACK_BUTTON: //back + acceptInput(); + quitMenu(); + return true; + case GUI_ID_ABORT_BUTTON: //abort + quitMenu(); + return true; + default: + key_setting *k = NULL; + + for (key_setting *ks : key_settings) { + if (ks->id == event.GUIEvent.Caller->getID()) { + k = ks; + break; + } + } + FATAL_ERROR_IF(k == NULL, "Key setting not found"); + + resetMenu(); + shift_down = false; + activeKey = event.GUIEvent.Caller->getID(); + const wchar_t *text = wgettext("press key"); + k->button->setText(text); + delete[] text; + this->key_used.erase(std::remove(this->key_used.begin(), + this->key_used.end(), k->key), this->key_used.end()); + break; + } + Environment->setFocus(this); + } + } + return Parent ? Parent->OnEvent(event) : false; +} + +void GUIKeyChangeMenu::add_key(int id, const wchar_t *button_name, const std::string &setting_name) +{ + key_setting *k = new key_setting; + k->id = id; + + k->button_name = button_name; + k->setting_name = setting_name; + k->key = getKeySetting(k->setting_name.c_str()); + key_settings.push_back(k); +} + +void GUIKeyChangeMenu::init_keys() +{ + this->add_key(GUI_ID_KEY_FORWARD_BUTTON, wgettext("Forward"), "keymap_forward"); + this->add_key(GUI_ID_KEY_BACKWARD_BUTTON, wgettext("Backward"), "keymap_backward"); + this->add_key(GUI_ID_KEY_LEFT_BUTTON, wgettext("Left"), "keymap_left"); + this->add_key(GUI_ID_KEY_RIGHT_BUTTON, wgettext("Right"), "keymap_right"); + this->add_key(GUI_ID_KEY_USE_BUTTON, wgettext("Special"), "keymap_special1"); + this->add_key(GUI_ID_KEY_JUMP_BUTTON, wgettext("Jump"), "keymap_jump"); + this->add_key(GUI_ID_KEY_SNEAK_BUTTON, wgettext("Sneak"), "keymap_sneak"); + this->add_key(GUI_ID_KEY_DROP_BUTTON, wgettext("Drop"), "keymap_drop"); + this->add_key(GUI_ID_KEY_INVENTORY_BUTTON, wgettext("Inventory"), "keymap_inventory"); + this->add_key(GUI_ID_KEY_HOTBAR_PREV_BUTTON,wgettext("Prev. item"), "keymap_hotbar_previous"); + this->add_key(GUI_ID_KEY_HOTBAR_NEXT_BUTTON,wgettext("Next item"), "keymap_hotbar_next"); + this->add_key(GUI_ID_KEY_ZOOM_BUTTON, wgettext("Zoom"), "keymap_zoom"); + this->add_key(GUI_ID_KEY_CAMERA_BUTTON, wgettext("Change camera"), "keymap_camera_mode"); + this->add_key(GUI_ID_KEY_CINEMATIC_BUTTON, wgettext("Toggle Cinematic"), "keymap_cinematic"); + this->add_key(GUI_ID_KEY_MINIMAP_BUTTON, wgettext("Toggle minimap"), "keymap_minimap"); + this->add_key(GUI_ID_KEY_FLY_BUTTON, wgettext("Toggle fly"), "keymap_freemove"); + this->add_key(GUI_ID_KEY_FAST_BUTTON, wgettext("Toggle fast"), "keymap_fastmove"); + this->add_key(GUI_ID_KEY_NOCLIP_BUTTON, wgettext("Toggle noclip"), "keymap_noclip"); + this->add_key(GUI_ID_KEY_MUTE_BUTTON, wgettext("Mute"), "keymap_mute"); + this->add_key(GUI_ID_KEY_DEC_VOLUME_BUTTON,wgettext("Dec. volume"), "keymap_decrease_volume"); + this->add_key(GUI_ID_KEY_INC_VOLUME_BUTTON,wgettext("Inc. volume"), "keymap_increase_volume"); + this->add_key(GUI_ID_KEY_AUTOFWD_BUTTON, wgettext("Autoforward"), "keymap_autoforward"); + this->add_key(GUI_ID_KEY_CHAT_BUTTON, wgettext("Chat"), "keymap_chat"); + this->add_key(GUI_ID_KEY_SCREENSHOT_BUTTON,wgettext("Screenshot"), "keymap_screenshot"); + this->add_key(GUI_ID_KEY_RANGE_BUTTON, wgettext("Range select"), "keymap_rangeselect"); + this->add_key(GUI_ID_KEY_DEC_RANGE_BUTTON, wgettext("Dec. range"), "keymap_decrease_viewing_range_min"); + this->add_key(GUI_ID_KEY_INC_RANGE_BUTTON, wgettext("Inc. range"), "keymap_increase_viewing_range_min"); + this->add_key(GUI_ID_KEY_CONSOLE_BUTTON, wgettext("Console"), "keymap_console"); + this->add_key(GUI_ID_KEY_CMD_BUTTON, wgettext("Command"), "keymap_cmd"); + this->add_key(GUI_ID_KEY_CMD_LOCAL_BUTTON, wgettext("Local command"), "keymap_cmd_local"); + this->add_key(GUI_ID_KEY_HUD_BUTTON, wgettext("Toggle HUD"), "keymap_toggle_hud"); + this->add_key(GUI_ID_KEY_CHATLOG_BUTTON, wgettext("Toggle chat log"), "keymap_toggle_chat"); + this->add_key(GUI_ID_KEY_FOG_BUTTON, wgettext("Toggle fog"), "keymap_toggle_force_fog_off"); +} + diff --git a/src/gui/guiKeyChangeMenu.h b/src/gui/guiKeyChangeMenu.h new file mode 100644 index 000000000..7cf11d3f9 --- /dev/null +++ b/src/gui/guiKeyChangeMenu.h @@ -0,0 +1,74 @@ +/* + Minetest + Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com> + Copyright (C) 2013 Ciaran Gultnieks <ciaran@ciarang.com> + Copyright (C) 2013 teddydestodes <derkomtur@schattengang.net> + + 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. + */ + +#pragma once + +#include "irrlichttypes_extrabloated.h" +#include "modalMenu.h" +#include "gettext.h" +#include "keycode.h" +#include <string> +#include <vector> + +struct key_setting +{ + int id; + const wchar_t *button_name; + KeyPress key; + std::string setting_name; + gui::IGUIButton *button; +}; + +class GUIKeyChangeMenu : public GUIModalMenu +{ +public: + GUIKeyChangeMenu(gui::IGUIEnvironment *env, gui::IGUIElement *parent, s32 id, + IMenuManager *menumgr); + ~GUIKeyChangeMenu(); + + void removeChildren(); + /* + Remove and re-add (or reposition) stuff + */ + void regenerateGui(v2u32 screensize); + + void drawMenu(); + + bool acceptInput(); + + bool OnEvent(const SEvent &event); + + bool pausesGame() { return true; } + +private: + void init_keys(); + + bool resetMenu(); + + void add_key(int id, const wchar_t *button_name, const std::string &setting_name); + + bool shift_down = false; + s32 activeKey = -1; + + std::vector<KeyPress> key_used; + gui::IGUIStaticText *key_used_text = nullptr; + std::vector<key_setting *> key_settings; +}; diff --git a/src/gui/guiMainMenu.h b/src/gui/guiMainMenu.h new file mode 100644 index 000000000..43a3b1a33 --- /dev/null +++ b/src/gui/guiMainMenu.h @@ -0,0 +1,55 @@ +/* +Minetest +Copyright (C) 2010-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. +*/ + +#pragma once + +#include "irrlichttypes_extrabloated.h" +#include "modalMenu.h" +#include <string> +#include <list> + +struct MainMenuDataForScript { + + MainMenuDataForScript() = default; + + // Whether the server has requested a reconnect + bool reconnect_requested = false; + std::string errormessage = ""; +}; + +struct MainMenuData { + // Client options + std::string servername; + std::string serverdescription; + std::string address; + std::string port; + std::string name; + std::string password; + // Whether to reconnect + bool do_reconnect = false; + + // Server options + int selected_world = 0; + bool simple_singleplayer_mode = false; + + // Data to be passed to the script + MainMenuDataForScript script_data; + + MainMenuData() = default; +}; diff --git a/src/gui/guiPasswordChange.cpp b/src/gui/guiPasswordChange.cpp new file mode 100644 index 000000000..46de2026c --- /dev/null +++ b/src/gui/guiPasswordChange.cpp @@ -0,0 +1,261 @@ +/* +Part of Minetest +Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com> +Copyright (C) 2013 Ciaran Gultnieks <ciaran@ciarang.com> + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +#include "guiPasswordChange.h" +#include "client.h" +#include <IGUICheckBox.h> +#include <IGUIEditBox.h> +#include <IGUIButton.h> +#include <IGUIStaticText.h> +#include <IGUIFont.h> + +#include "gettext.h" + +const int ID_oldPassword = 256; +const int ID_newPassword1 = 257; +const int ID_newPassword2 = 258; +const int ID_change = 259; +const int ID_message = 260; +const int ID_cancel = 261; + +GUIPasswordChange::GUIPasswordChange(gui::IGUIEnvironment* env, + gui::IGUIElement* parent, s32 id, + IMenuManager *menumgr, + Client* client +): + GUIModalMenu(env, parent, id, menumgr), + m_client(client) +{ +} + +GUIPasswordChange::~GUIPasswordChange() +{ + removeChildren(); +} + +void GUIPasswordChange::removeChildren() +{ + const core::list<gui::IGUIElement *> &children = getChildren(); + core::list<gui::IGUIElement *> children_copy; + for (gui::IGUIElement *i : children) { + children_copy.push_back(i); + } + + for (gui::IGUIElement *i : children_copy) { + i->remove(); + } +} +void GUIPasswordChange::regenerateGui(v2u32 screensize) +{ + /* + save current input + */ + acceptInput(); + + /* + Remove stuff + */ + removeChildren(); + + /* + Calculate new sizes and positions + */ + core::rect<s32> rect( + screensize.X/2 - 580/2, + screensize.Y/2 - 300/2, + screensize.X/2 + 580/2, + screensize.Y/2 + 300/2 + ); + + DesiredRect = rect; + recalculateAbsolutePosition(false); + + v2s32 size = rect.getSize(); + v2s32 topleft_client(40, 0); + + const wchar_t *text; + + /* + Add stuff + */ + s32 ypos = 50; + { + core::rect<s32> rect(0, 0, 150, 20); + rect += topleft_client + v2s32(25, ypos + 6); + text = wgettext("Old Password"); + Environment->addStaticText(text, rect, false, true, this, -1); + delete[] text; + } + { + core::rect<s32> rect(0, 0, 230, 30); + rect += topleft_client + v2s32(160, ypos); + gui::IGUIEditBox *e = Environment->addEditBox( + m_oldpass.c_str(), rect, true, this, ID_oldPassword); + Environment->setFocus(e); + e->setPasswordBox(true); + } + ypos += 50; + { + core::rect<s32> rect(0, 0, 150, 20); + rect += topleft_client + v2s32(25, ypos + 6); + text = wgettext("New Password"); + Environment->addStaticText(text, rect, false, true, this, -1); + delete[] text; + } + { + core::rect<s32> rect(0, 0, 230, 30); + rect += topleft_client + v2s32(160, ypos); + gui::IGUIEditBox *e = Environment->addEditBox( + m_newpass.c_str(), rect, true, this, ID_newPassword1); + e->setPasswordBox(true); + } + ypos += 50; + { + core::rect<s32> rect(0, 0, 150, 20); + rect += topleft_client + v2s32(25, ypos + 6); + text = wgettext("Confirm Password"); + Environment->addStaticText(text, rect, false, true, this, -1); + delete[] text; + } + { + core::rect<s32> rect(0, 0, 230, 30); + rect += topleft_client + v2s32(160, ypos); + gui::IGUIEditBox *e = Environment->addEditBox( + m_newpass_confirm.c_str(), rect, true, this, ID_newPassword2); + e->setPasswordBox(true); + } + + ypos += 50; + { + core::rect<s32> rect(0, 0, 100, 30); + rect = rect + v2s32(size.X / 4 + 56, ypos); + text = wgettext("Change"); + Environment->addButton(rect, this, ID_change, text); + delete[] text; + } + { + core::rect<s32> rect(0, 0, 100, 30); + rect = rect + v2s32(size.X / 4 + 185, ypos); + text = wgettext("Cancel"); + Environment->addButton(rect, this, ID_cancel, text); + delete[] text; + } + + ypos += 50; + { + core::rect<s32> rect(0, 0, 300, 20); + rect += topleft_client + v2s32(35, ypos); + text = wgettext("Passwords do not match!"); + IGUIElement *e = + Environment->addStaticText( + text, rect, false, true, this, ID_message); + e->setVisible(false); + delete[] text; + } +} + +void GUIPasswordChange::drawMenu() +{ + gui::IGUISkin *skin = Environment->getSkin(); + if (!skin) + return; + video::IVideoDriver *driver = Environment->getVideoDriver(); + + video::SColor bgcolor(140, 0, 0, 0); + driver->draw2DRectangle(bgcolor, AbsoluteRect, &AbsoluteClippingRect); + + gui::IGUIElement::draw(); +} + +void GUIPasswordChange::acceptInput() +{ + gui::IGUIElement *e; + e = getElementFromId(ID_oldPassword); + if (e != NULL) + m_oldpass = e->getText(); + e = getElementFromId(ID_newPassword1); + if (e != NULL) + m_newpass = e->getText(); + e = getElementFromId(ID_newPassword2); + if (e != NULL) + m_newpass_confirm = e->getText(); +} + +bool GUIPasswordChange::processInput() +{ + if (m_newpass != m_newpass_confirm) { + gui::IGUIElement *e = getElementFromId(ID_message); + if (e != NULL) + e->setVisible(true); + return false; + } + m_client->sendChangePassword(wide_to_utf8(m_oldpass), wide_to_utf8(m_newpass)); + return true; +} + +bool GUIPasswordChange::OnEvent(const SEvent &event) +{ + if (event.EventType == EET_KEY_INPUT_EVENT) { + if (event.KeyInput.Key == KEY_ESCAPE && event.KeyInput.PressedDown) { + quitMenu(); + return true; + } + if (event.KeyInput.Key == KEY_RETURN && event.KeyInput.PressedDown) { + acceptInput(); + if (processInput()) + quitMenu(); + return true; + } + } + if (event.EventType == EET_GUI_EVENT) { + if (event.GUIEvent.EventType == gui::EGET_ELEMENT_FOCUS_LOST && + isVisible()) { + if (!canTakeFocus(event.GUIEvent.Element)) { + dstream << "GUIPasswordChange: Not allowing focus change." + << std::endl; + // Returning true disables focus change + return true; + } + } + if (event.GUIEvent.EventType == gui::EGET_BUTTON_CLICKED) { + switch (event.GUIEvent.Caller->getID()) { + case ID_change: + acceptInput(); + if (processInput()) + quitMenu(); + return true; + case ID_cancel: + quitMenu(); + return true; + } + } + if (event.GUIEvent.EventType == gui::EGET_EDITBOX_ENTER) { + switch (event.GUIEvent.Caller->getID()) { + case ID_oldPassword: + case ID_newPassword1: + case ID_newPassword2: + acceptInput(); + if (processInput()) + quitMenu(); + return true; + } + } + } + + return Parent ? Parent->OnEvent(event) : false; +} diff --git a/src/gui/guiPasswordChange.h b/src/gui/guiPasswordChange.h new file mode 100644 index 000000000..59f3513b2 --- /dev/null +++ b/src/gui/guiPasswordChange.h @@ -0,0 +1,53 @@ +/* +Part of Minetest +Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com> +Copyright (C) 2013 Ciaran Gultnieks <ciaran@ciarang.com> + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +#pragma once + +#include "irrlichttypes_extrabloated.h" +#include "modalMenu.h" +#include <string> + +class Client; + +class GUIPasswordChange : public GUIModalMenu +{ +public: + GUIPasswordChange(gui::IGUIEnvironment *env, gui::IGUIElement *parent, s32 id, + IMenuManager *menumgr, Client *client); + ~GUIPasswordChange(); + + void removeChildren(); + /* + Remove and re-add (or reposition) stuff + */ + void regenerateGui(v2u32 screensize); + + void drawMenu(); + + void acceptInput(); + + bool processInput(); + + bool OnEvent(const SEvent &event); + +private: + Client *m_client; + std::wstring m_oldpass = L""; + std::wstring m_newpass = L""; + std::wstring m_newpass_confirm = L""; +}; diff --git a/src/gui/guiPathSelectMenu.cpp b/src/gui/guiPathSelectMenu.cpp new file mode 100644 index 000000000..b999f0a68 --- /dev/null +++ b/src/gui/guiPathSelectMenu.cpp @@ -0,0 +1,113 @@ +/* + Minetest + Copyright (C) 2013 sapier + + 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 "guiPathSelectMenu.h" + +GUIFileSelectMenu::GUIFileSelectMenu(gui::IGUIEnvironment* env, + gui::IGUIElement* parent, s32 id, IMenuManager *menumgr, + const std::string &title, const std::string &formname, + bool is_file_select) : + GUIModalMenu(env, parent, id, menumgr), + m_title(utf8_to_wide(title)), + m_formname(formname), + m_file_select_dialog(is_file_select) +{ +} + +GUIFileSelectMenu::~GUIFileSelectMenu() +{ + removeChildren(); + setlocale(LC_NUMERIC, "C"); +} + +void GUIFileSelectMenu::regenerateGui(v2u32 screensize) +{ + removeChildren(); + m_fileOpenDialog = 0; + + core::dimension2du size(600, 400); + core::rect<s32> rect(0, 0, screensize.X, screensize.Y); + + DesiredRect = rect; + recalculateAbsolutePosition(false); + + m_fileOpenDialog = + Environment->addFileOpenDialog(m_title.c_str(), false, this, -1); + + core::position2di pos = core::position2di(screensize.X / 2 - size.Width / 2, + screensize.Y / 2 - size.Height / 2); + m_fileOpenDialog->setRelativePosition(pos); + m_fileOpenDialog->setMinSize(size); +} + +void GUIFileSelectMenu::drawMenu() +{ + gui::IGUISkin *skin = Environment->getSkin(); + if (!skin) + return; + + gui::IGUIElement::draw(); +} + +void GUIFileSelectMenu::acceptInput() +{ + if (m_text_dst && !m_formname.empty()) { + StringMap fields; + if (m_accepted) { + std::string path; + if (!m_file_select_dialog) { + core::string<fschar_t> string = + m_fileOpenDialog->getDirectoryName(); + path = std::string(string.c_str()); + } else { + path = wide_to_utf8(m_fileOpenDialog->getFileName()); + } + fields[m_formname + "_accepted"] = path; + } else { + fields[m_formname + "_canceled"] = m_formname; + } + m_text_dst->gotText(fields); + } + quitMenu(); +} + +bool GUIFileSelectMenu::OnEvent(const SEvent &event) +{ + if (event.EventType == irr::EET_GUI_EVENT) { + switch (event.GUIEvent.EventType) { + case gui::EGET_ELEMENT_CLOSED: + case gui::EGET_FILE_CHOOSE_DIALOG_CANCELLED: + m_accepted = false; + acceptInput(); + return true; + case gui::EGET_DIRECTORY_SELECTED: + m_accepted = !m_file_select_dialog; + acceptInput(); + return true; + case gui::EGET_FILE_SELECTED: + m_accepted = m_file_select_dialog; + acceptInput(); + return true; + default: + // ignore this event + break; + } + } + return Parent ? Parent->OnEvent(event) : false; +} diff --git a/src/gui/guiPathSelectMenu.h b/src/gui/guiPathSelectMenu.h new file mode 100644 index 000000000..f69d0acd7 --- /dev/null +++ b/src/gui/guiPathSelectMenu.h @@ -0,0 +1,59 @@ +/* + Minetest + Copyright (C) 2013 sapier + + 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. + */ + +#pragma once + +#include <string> + +#include "modalMenu.h" +#include "IGUIFileOpenDialog.h" +#include "guiFormSpecMenu.h" //required because of TextDest only !!! + +class GUIFileSelectMenu : public GUIModalMenu +{ +public: + GUIFileSelectMenu(gui::IGUIEnvironment *env, gui::IGUIElement *parent, s32 id, + IMenuManager *menumgr, const std::string &title, + const std::string &formid, bool is_file_select); + ~GUIFileSelectMenu(); + + /* + Remove and re-add (or reposition) stuff + */ + void regenerateGui(v2u32 screensize); + + void drawMenu(); + + bool OnEvent(const SEvent &event); + + void setTextDest(TextDest *dest) { m_text_dst = dest; } + +private: + void acceptInput(); + + std::wstring m_title; + bool m_accepted = false; + + gui::IGUIFileOpenDialog *m_fileOpenDialog = nullptr; + + TextDest *m_text_dst = nullptr; + + std::string m_formname; + bool m_file_select_dialog; +}; diff --git a/src/gui/guiTable.cpp b/src/gui/guiTable.cpp new file mode 100644 index 000000000..a2738afa9 --- /dev/null +++ b/src/gui/guiTable.cpp @@ -0,0 +1,1261 @@ +/* +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 "guiTable.h" +#include <queue> +#include <sstream> +#include <utility> +#include <cstring> +#include <IGUISkin.h> +#include <IGUIFont.h> +#include <IGUIScrollBar.h> +#include "client/renderingengine.h" +#include "debug.h" +#include "log.h" +#include "client/tile.h" +#include "gettime.h" +#include "util/string.h" +#include "util/numeric.h" +#include "util/string.h" // for parseColorString() +#include "settings.h" // for settings +#include "porting.h" // for dpi +#include "guiscalingfilter.h" + +/* + GUITable +*/ + +GUITable::GUITable(gui::IGUIEnvironment *env, + gui::IGUIElement* parent, s32 id, + core::rect<s32> rectangle, + ISimpleTextureSource *tsrc +): + gui::IGUIElement(gui::EGUIET_ELEMENT, env, parent, id, rectangle), + m_tsrc(tsrc) +{ + assert(tsrc != NULL); + + gui::IGUISkin* skin = Environment->getSkin(); + + m_font = skin->getFont(); + if (m_font) { + m_font->grab(); + m_rowheight = m_font->getDimension(L"A").Height + 4; + m_rowheight = MYMAX(m_rowheight, 1); + } + + const s32 s = skin->getSize(gui::EGDS_SCROLLBAR_SIZE); + m_scrollbar = Environment->addScrollBar(false, + core::rect<s32>(RelativeRect.getWidth() - s, + 0, + RelativeRect.getWidth(), + RelativeRect.getHeight()), + this, -1); + m_scrollbar->setSubElement(true); + m_scrollbar->setTabStop(false); + m_scrollbar->setAlignment(gui::EGUIA_LOWERRIGHT, gui::EGUIA_LOWERRIGHT, + gui::EGUIA_UPPERLEFT, gui::EGUIA_LOWERRIGHT); + m_scrollbar->setVisible(false); + m_scrollbar->setPos(0); + + setTabStop(true); + setTabOrder(-1); + updateAbsolutePosition(); + + core::rect<s32> relative_rect = m_scrollbar->getRelativePosition(); + s32 width = (relative_rect.getWidth()/(2.0/3.0)) * + RenderingEngine::getDisplayDensity() * + g_settings->getFloat("gui_scaling"); + m_scrollbar->setRelativePosition(core::rect<s32>( + relative_rect.LowerRightCorner.X-width,relative_rect.UpperLeftCorner.Y, + relative_rect.LowerRightCorner.X,relative_rect.LowerRightCorner.Y + )); +} + +GUITable::~GUITable() +{ + for (GUITable::Row &row : m_rows) + delete[] row.cells; + + if (m_font) + m_font->drop(); + + m_scrollbar->remove(); +} + +GUITable::Option GUITable::splitOption(const std::string &str) +{ + size_t equal_pos = str.find('='); + if (equal_pos == std::string::npos) + return GUITable::Option(str, ""); + + return GUITable::Option(str.substr(0, equal_pos), + str.substr(equal_pos + 1)); +} + +void GUITable::setTextList(const std::vector<std::string> &content, + bool transparent) +{ + clear(); + + if (transparent) { + m_background.setAlpha(0); + m_border = false; + } + + m_is_textlist = true; + + s32 empty_string_index = allocString(""); + + m_rows.resize(content.size()); + for (s32 i = 0; i < (s32) content.size(); ++i) { + Row *row = &m_rows[i]; + row->cells = new Cell[1]; + row->cellcount = 1; + row->indent = 0; + row->visible_index = i; + m_visible_rows.push_back(i); + + Cell *cell = row->cells; + cell->xmin = 0; + cell->xmax = 0x7fff; // something large enough + cell->xpos = 6; + cell->content_type = COLUMN_TYPE_TEXT; + cell->content_index = empty_string_index; + cell->tooltip_index = empty_string_index; + cell->color.set(255, 255, 255, 255); + cell->color_defined = false; + cell->reported_column = 1; + + // parse row content (color) + const std::string &s = content[i]; + if (s[0] == '#' && s[1] == '#') { + // double # to escape + cell->content_index = allocString(s.substr(2)); + } + else if (s[0] == '#' && s.size() >= 7 && + parseColorString( + s.substr(0,7), cell->color, false)) { + // single # for color + cell->color_defined = true; + cell->content_index = allocString(s.substr(7)); + } + else { + // no #, just text + cell->content_index = allocString(s); + } + + } + + allocationComplete(); + + // Clamp scroll bar position + updateScrollBar(); +} + +void GUITable::setTable(const TableOptions &options, + const TableColumns &columns, + std::vector<std::string> &content) +{ + clear(); + + // Naming conventions: + // i is always a row index, 0-based + // j is always a column index, 0-based + // k is another index, for example an option index + + // Handle a stupid error case... (issue #1187) + if (columns.empty()) { + TableColumn text_column; + text_column.type = "text"; + TableColumns new_columns; + new_columns.push_back(text_column); + setTable(options, new_columns, content); + return; + } + + // Handle table options + video::SColor default_color(255, 255, 255, 255); + s32 opendepth = 0; + for (const Option &option : options) { + const std::string &name = option.name; + const std::string &value = option.value; + if (name == "color") + parseColorString(value, m_color, false); + else if (name == "background") + parseColorString(value, m_background, false); + else if (name == "border") + m_border = is_yes(value); + else if (name == "highlight") + parseColorString(value, m_highlight, false); + else if (name == "highlight_text") + parseColorString(value, m_highlight_text, false); + else if (name == "opendepth") + opendepth = stoi(value); + else + errorstream<<"Invalid table option: \""<<name<<"\"" + <<" (value=\""<<value<<"\")"<<std::endl; + } + + // Get number of columns and rows + // note: error case columns.size() == 0 was handled above + s32 colcount = columns.size(); + assert(colcount >= 1); + // rowcount = ceil(cellcount / colcount) but use integer arithmetic + s32 rowcount = (content.size() + colcount - 1) / colcount; + assert(rowcount >= 0); + // Append empty strings to content if there is an incomplete row + s32 cellcount = rowcount * colcount; + while (content.size() < (u32) cellcount) + content.emplace_back(""); + + // Create temporary rows (for processing columns) + struct TempRow { + // Current horizontal position (may different between rows due + // to indent/tree columns, or text/image columns with width<0) + s32 x; + // Tree indentation level + s32 indent; + // Next cell: Index into m_strings or m_images + s32 content_index; + // Next cell: Width in pixels + s32 content_width; + // Vector of completed cells in this row + std::vector<Cell> cells; + // Stores colors and how long they last (maximum column index) + std::vector<std::pair<video::SColor, s32> > colors; + + TempRow(): x(0), indent(0), content_index(0), content_width(0) {} + }; + TempRow *rows = new TempRow[rowcount]; + + // Get em width. Pedantically speaking, the width of "M" is not + // necessarily the same as the em width, but whatever, close enough. + s32 em = 6; + if (m_font) + em = m_font->getDimension(L"M").Width; + + s32 default_tooltip_index = allocString(""); + + std::map<s32, s32> active_image_indices; + + // Process content in column-major order + for (s32 j = 0; j < colcount; ++j) { + // Check column type + ColumnType columntype = COLUMN_TYPE_TEXT; + if (columns[j].type == "text") + columntype = COLUMN_TYPE_TEXT; + else if (columns[j].type == "image") + columntype = COLUMN_TYPE_IMAGE; + else if (columns[j].type == "color") + columntype = COLUMN_TYPE_COLOR; + else if (columns[j].type == "indent") + columntype = COLUMN_TYPE_INDENT; + else if (columns[j].type == "tree") + columntype = COLUMN_TYPE_TREE; + else + errorstream<<"Invalid table column type: \"" + <<columns[j].type<<"\""<<std::endl; + + // Process column options + s32 padding = myround(0.5 * em); + s32 tooltip_index = default_tooltip_index; + s32 align = 0; + s32 width = 0; + s32 span = colcount; + + if (columntype == COLUMN_TYPE_INDENT) { + padding = 0; // default indent padding + } + if (columntype == COLUMN_TYPE_INDENT || + columntype == COLUMN_TYPE_TREE) { + width = myround(em * 1.5); // default indent width + } + + for (const Option &option : columns[j].options) { + const std::string &name = option.name; + const std::string &value = option.value; + if (name == "padding") + padding = myround(stof(value) * em); + else if (name == "tooltip") + tooltip_index = allocString(value); + else if (name == "align" && value == "left") + align = 0; + else if (name == "align" && value == "center") + align = 1; + else if (name == "align" && value == "right") + align = 2; + else if (name == "align" && value == "inline") + align = 3; + else if (name == "width") + width = myround(stof(value) * em); + else if (name == "span" && columntype == COLUMN_TYPE_COLOR) + span = stoi(value); + else if (columntype == COLUMN_TYPE_IMAGE && + !name.empty() && + string_allowed(name, "0123456789")) { + s32 content_index = allocImage(value); + active_image_indices.insert(std::make_pair( + stoi(name), + content_index)); + } + else { + errorstream<<"Invalid table column option: \""<<name<<"\"" + <<" (value=\""<<value<<"\")"<<std::endl; + } + } + + // If current column type can use information from "color" columns, + // find out which of those is currently active + if (columntype == COLUMN_TYPE_TEXT) { + for (s32 i = 0; i < rowcount; ++i) { + TempRow *row = &rows[i]; + while (!row->colors.empty() && row->colors.back().second < j) + row->colors.pop_back(); + } + } + + // Make template for new cells + Cell newcell; + memset(&newcell, 0, sizeof newcell); + newcell.content_type = columntype; + newcell.tooltip_index = tooltip_index; + newcell.reported_column = j+1; + + if (columntype == COLUMN_TYPE_TEXT) { + // Find right edge of column + s32 xmax = 0; + for (s32 i = 0; i < rowcount; ++i) { + TempRow *row = &rows[i]; + row->content_index = allocString(content[i * colcount + j]); + const core::stringw &text = m_strings[row->content_index]; + row->content_width = m_font ? + m_font->getDimension(text.c_str()).Width : 0; + row->content_width = MYMAX(row->content_width, width); + s32 row_xmax = row->x + padding + row->content_width; + xmax = MYMAX(xmax, row_xmax); + } + // Add a new cell (of text type) to each row + for (s32 i = 0; i < rowcount; ++i) { + newcell.xmin = rows[i].x + padding; + alignContent(&newcell, xmax, rows[i].content_width, align); + newcell.content_index = rows[i].content_index; + newcell.color_defined = !rows[i].colors.empty(); + if (newcell.color_defined) + newcell.color = rows[i].colors.back().first; + rows[i].cells.push_back(newcell); + rows[i].x = newcell.xmax; + } + } + else if (columntype == COLUMN_TYPE_IMAGE) { + // Find right edge of column + s32 xmax = 0; + for (s32 i = 0; i < rowcount; ++i) { + TempRow *row = &rows[i]; + row->content_index = -1; + + // Find content_index. Image indices are defined in + // column options so check active_image_indices. + s32 image_index = stoi(content[i * colcount + j]); + std::map<s32, s32>::iterator image_iter = + active_image_indices.find(image_index); + if (image_iter != active_image_indices.end()) + row->content_index = image_iter->second; + + // Get texture object (might be NULL) + video::ITexture *image = NULL; + if (row->content_index >= 0) + image = m_images[row->content_index]; + + // Get content width and update xmax + row->content_width = image ? image->getOriginalSize().Width : 0; + row->content_width = MYMAX(row->content_width, width); + s32 row_xmax = row->x + padding + row->content_width; + xmax = MYMAX(xmax, row_xmax); + } + // Add a new cell (of image type) to each row + for (s32 i = 0; i < rowcount; ++i) { + newcell.xmin = rows[i].x + padding; + alignContent(&newcell, xmax, rows[i].content_width, align); + newcell.content_index = rows[i].content_index; + rows[i].cells.push_back(newcell); + rows[i].x = newcell.xmax; + } + active_image_indices.clear(); + } + else if (columntype == COLUMN_TYPE_COLOR) { + for (s32 i = 0; i < rowcount; ++i) { + video::SColor cellcolor(255, 255, 255, 255); + if (parseColorString(content[i * colcount + j], cellcolor, true)) + rows[i].colors.emplace_back(cellcolor, j+span); + } + } + else if (columntype == COLUMN_TYPE_INDENT || + columntype == COLUMN_TYPE_TREE) { + // For column type "tree", reserve additional space for +/- + // Also enable special processing for treeview-type tables + s32 content_width = 0; + if (columntype == COLUMN_TYPE_TREE) { + content_width = m_font ? m_font->getDimension(L"+").Width : 0; + m_has_tree_column = true; + } + // Add a new cell (of indent or tree type) to each row + for (s32 i = 0; i < rowcount; ++i) { + TempRow *row = &rows[i]; + + s32 indentlevel = stoi(content[i * colcount + j]); + indentlevel = MYMAX(indentlevel, 0); + if (columntype == COLUMN_TYPE_TREE) + row->indent = indentlevel; + + newcell.xmin = row->x + padding; + newcell.xpos = newcell.xmin + indentlevel * width; + newcell.xmax = newcell.xpos + content_width; + newcell.content_index = 0; + newcell.color_defined = !rows[i].colors.empty(); + if (newcell.color_defined) + newcell.color = rows[i].colors.back().first; + row->cells.push_back(newcell); + row->x = newcell.xmax; + } + } + } + + // Copy temporary rows to not so temporary rows + if (rowcount >= 1) { + m_rows.resize(rowcount); + for (s32 i = 0; i < rowcount; ++i) { + Row *row = &m_rows[i]; + row->cellcount = rows[i].cells.size(); + row->cells = new Cell[row->cellcount]; + memcpy((void*) row->cells, (void*) &rows[i].cells[0], + row->cellcount * sizeof(Cell)); + row->indent = rows[i].indent; + row->visible_index = i; + m_visible_rows.push_back(i); + } + } + + if (m_has_tree_column) { + // Treeview: convert tree to indent cells on leaf rows + for (s32 i = 0; i < rowcount; ++i) { + if (i == rowcount-1 || m_rows[i].indent >= m_rows[i+1].indent) + for (s32 j = 0; j < m_rows[i].cellcount; ++j) + if (m_rows[i].cells[j].content_type == COLUMN_TYPE_TREE) + m_rows[i].cells[j].content_type = COLUMN_TYPE_INDENT; + } + + // Treeview: close rows according to opendepth option + std::set<s32> opened_trees; + for (s32 i = 0; i < rowcount; ++i) + if (m_rows[i].indent < opendepth) + opened_trees.insert(i); + setOpenedTrees(opened_trees); + } + + // Delete temporary information used only during setTable() + delete[] rows; + allocationComplete(); + + // Clamp scroll bar position + updateScrollBar(); +} + +void GUITable::clear() +{ + // Clean up cells and rows + for (GUITable::Row &row : m_rows) + delete[] row.cells; + m_rows.clear(); + m_visible_rows.clear(); + + // Get colors from skin + gui::IGUISkin *skin = Environment->getSkin(); + m_color = skin->getColor(gui::EGDC_BUTTON_TEXT); + m_background = skin->getColor(gui::EGDC_3D_HIGH_LIGHT); + m_highlight = skin->getColor(gui::EGDC_HIGH_LIGHT); + m_highlight_text = skin->getColor(gui::EGDC_HIGH_LIGHT_TEXT); + + // Reset members + m_is_textlist = false; + m_has_tree_column = false; + m_selected = -1; + m_sel_column = 0; + m_sel_doubleclick = false; + m_keynav_time = 0; + m_keynav_buffer = L""; + m_border = true; + m_strings.clear(); + m_images.clear(); + m_alloc_strings.clear(); + m_alloc_images.clear(); +} + +std::string GUITable::checkEvent() +{ + s32 sel = getSelected(); + assert(sel >= 0); + + if (sel == 0) { + return "INV"; + } + + std::ostringstream os(std::ios::binary); + if (m_sel_doubleclick) { + os<<"DCL:"; + m_sel_doubleclick = false; + } + else { + os<<"CHG:"; + } + os<<sel; + if (!m_is_textlist) { + os<<":"<<m_sel_column; + } + return os.str(); +} + +s32 GUITable::getSelected() const +{ + if (m_selected < 0) + return 0; + + assert(m_selected >= 0 && m_selected < (s32) m_visible_rows.size()); + return m_visible_rows[m_selected] + 1; +} + +void GUITable::setSelected(s32 index) +{ + s32 old_selected = m_selected; + + m_selected = -1; + m_sel_column = 0; + m_sel_doubleclick = false; + + --index; // Switch from 1-based indexing to 0-based indexing + + s32 rowcount = m_rows.size(); + if (rowcount == 0 || index < 0) { + return; + } + + if (index >= rowcount) { + index = rowcount - 1; + } + + // If the selected row is not visible, open its ancestors to make it visible + bool selection_invisible = m_rows[index].visible_index < 0; + if (selection_invisible) { + std::set<s32> opened_trees; + getOpenedTrees(opened_trees); + s32 indent = m_rows[index].indent; + for (s32 j = index - 1; j >= 0; --j) { + if (m_rows[j].indent < indent) { + opened_trees.insert(j); + indent = m_rows[j].indent; + } + } + setOpenedTrees(opened_trees); + } + + if (index >= 0) { + m_selected = m_rows[index].visible_index; + assert(m_selected >= 0 && m_selected < (s32) m_visible_rows.size()); + } + + if (m_selected != old_selected || selection_invisible) { + autoScroll(); + } +} + +GUITable::DynamicData GUITable::getDynamicData() const +{ + DynamicData dyndata; + dyndata.selected = getSelected(); + dyndata.scrollpos = m_scrollbar->getPos(); + dyndata.keynav_time = m_keynav_time; + dyndata.keynav_buffer = m_keynav_buffer; + if (m_has_tree_column) + getOpenedTrees(dyndata.opened_trees); + return dyndata; +} + +void GUITable::setDynamicData(const DynamicData &dyndata) +{ + if (m_has_tree_column) + setOpenedTrees(dyndata.opened_trees); + + m_keynav_time = dyndata.keynav_time; + m_keynav_buffer = dyndata.keynav_buffer; + + setSelected(dyndata.selected); + m_sel_column = 0; + m_sel_doubleclick = false; + + m_scrollbar->setPos(dyndata.scrollpos); +} + +const c8* GUITable::getTypeName() const +{ + return "GUITable"; +} + +void GUITable::updateAbsolutePosition() +{ + IGUIElement::updateAbsolutePosition(); + updateScrollBar(); +} + +void GUITable::draw() +{ + if (!IsVisible) + return; + + gui::IGUISkin *skin = Environment->getSkin(); + + // draw background + + bool draw_background = m_background.getAlpha() > 0; + if (m_border) + skin->draw3DSunkenPane(this, m_background, + true, draw_background, + AbsoluteRect, &AbsoluteClippingRect); + else if (draw_background) + skin->draw2DRectangle(this, m_background, + AbsoluteRect, &AbsoluteClippingRect); + + // get clipping rect + + core::rect<s32> client_clip(AbsoluteRect); + client_clip.UpperLeftCorner.Y += 1; + client_clip.UpperLeftCorner.X += 1; + client_clip.LowerRightCorner.Y -= 1; + client_clip.LowerRightCorner.X -= 1; + if (m_scrollbar->isVisible()) { + client_clip.LowerRightCorner.X = + m_scrollbar->getAbsolutePosition().UpperLeftCorner.X; + } + client_clip.clipAgainst(AbsoluteClippingRect); + + // draw visible rows + + s32 scrollpos = m_scrollbar->getPos(); + s32 row_min = scrollpos / m_rowheight; + s32 row_max = (scrollpos + AbsoluteRect.getHeight() - 1) + / m_rowheight + 1; + row_max = MYMIN(row_max, (s32) m_visible_rows.size()); + + core::rect<s32> row_rect(AbsoluteRect); + if (m_scrollbar->isVisible()) + row_rect.LowerRightCorner.X -= + skin->getSize(gui::EGDS_SCROLLBAR_SIZE); + row_rect.UpperLeftCorner.Y += row_min * m_rowheight - scrollpos; + row_rect.LowerRightCorner.Y = row_rect.UpperLeftCorner.Y + m_rowheight; + + for (s32 i = row_min; i < row_max; ++i) { + Row *row = &m_rows[m_visible_rows[i]]; + bool is_sel = i == m_selected; + video::SColor color = m_color; + + if (is_sel) { + skin->draw2DRectangle(this, m_highlight, row_rect, &client_clip); + color = m_highlight_text; + } + + for (s32 j = 0; j < row->cellcount; ++j) + drawCell(&row->cells[j], color, row_rect, client_clip); + + row_rect.UpperLeftCorner.Y += m_rowheight; + row_rect.LowerRightCorner.Y += m_rowheight; + } + + // Draw children + IGUIElement::draw(); +} + +void GUITable::drawCell(const Cell *cell, video::SColor color, + const core::rect<s32> &row_rect, + const core::rect<s32> &client_clip) +{ + if ((cell->content_type == COLUMN_TYPE_TEXT) + || (cell->content_type == COLUMN_TYPE_TREE)) { + + core::rect<s32> text_rect = row_rect; + text_rect.UpperLeftCorner.X = row_rect.UpperLeftCorner.X + + cell->xpos; + text_rect.LowerRightCorner.X = row_rect.UpperLeftCorner.X + + cell->xmax; + + if (cell->color_defined) + color = cell->color; + + if (m_font) { + if (cell->content_type == COLUMN_TYPE_TEXT) + m_font->draw(m_strings[cell->content_index], + text_rect, color, + false, true, &client_clip); + else // tree + m_font->draw(cell->content_index ? L"+" : L"-", + text_rect, color, + false, true, &client_clip); + } + } + else if (cell->content_type == COLUMN_TYPE_IMAGE) { + + if (cell->content_index < 0) + return; + + video::IVideoDriver *driver = Environment->getVideoDriver(); + video::ITexture *image = m_images[cell->content_index]; + + if (image) { + core::position2d<s32> dest_pos = + row_rect.UpperLeftCorner; + dest_pos.X += cell->xpos; + core::rect<s32> source_rect( + core::position2d<s32>(0, 0), + image->getOriginalSize()); + s32 imgh = source_rect.LowerRightCorner.Y; + s32 rowh = row_rect.getHeight(); + if (imgh < rowh) + dest_pos.Y += (rowh - imgh) / 2; + else + source_rect.LowerRightCorner.Y = rowh; + + video::SColor color(255, 255, 255, 255); + + driver->draw2DImage(image, dest_pos, source_rect, + &client_clip, color, true); + } + } +} + +bool GUITable::OnEvent(const SEvent &event) +{ + if (!isEnabled()) + return IGUIElement::OnEvent(event); + + if (event.EventType == EET_KEY_INPUT_EVENT) { + if (event.KeyInput.PressedDown && ( + event.KeyInput.Key == KEY_DOWN || + event.KeyInput.Key == KEY_UP || + event.KeyInput.Key == KEY_HOME || + event.KeyInput.Key == KEY_END || + event.KeyInput.Key == KEY_NEXT || + event.KeyInput.Key == KEY_PRIOR)) { + s32 offset = 0; + switch (event.KeyInput.Key) { + case KEY_DOWN: + offset = 1; + break; + case KEY_UP: + offset = -1; + break; + case KEY_HOME: + offset = - (s32) m_visible_rows.size(); + break; + case KEY_END: + offset = m_visible_rows.size(); + break; + case KEY_NEXT: + offset = AbsoluteRect.getHeight() / m_rowheight; + break; + case KEY_PRIOR: + offset = - (s32) (AbsoluteRect.getHeight() / m_rowheight); + break; + default: + break; + } + s32 old_selected = m_selected; + s32 rowcount = m_visible_rows.size(); + if (rowcount != 0) { + m_selected = rangelim(m_selected + offset, 0, rowcount-1); + autoScroll(); + } + + if (m_selected != old_selected) + sendTableEvent(0, false); + + return true; + } + + if (event.KeyInput.PressedDown && ( + event.KeyInput.Key == KEY_LEFT || + event.KeyInput.Key == KEY_RIGHT)) { + // Open/close subtree via keyboard + if (m_selected >= 0) { + int dir = event.KeyInput.Key == KEY_LEFT ? -1 : 1; + toggleVisibleTree(m_selected, dir, true); + } + return true; + } + else if (!event.KeyInput.PressedDown && ( + event.KeyInput.Key == KEY_RETURN || + event.KeyInput.Key == KEY_SPACE)) { + sendTableEvent(0, true); + return true; + } + else if (event.KeyInput.Key == KEY_ESCAPE || + event.KeyInput.Key == KEY_SPACE) { + // pass to parent + } + else if (event.KeyInput.PressedDown && event.KeyInput.Char) { + // change selection based on text as it is typed + u64 now = porting::getTimeMs(); + if (now - m_keynav_time >= 500) + m_keynav_buffer = L""; + m_keynav_time = now; + + // add to key buffer if not a key repeat + if (!(m_keynav_buffer.size() == 1 && + m_keynav_buffer[0] == event.KeyInput.Char)) { + m_keynav_buffer.append(event.KeyInput.Char); + } + + // find the selected item, starting at the current selection + // don't change selection if the key buffer matches the current item + s32 old_selected = m_selected; + s32 start = MYMAX(m_selected, 0); + s32 rowcount = m_visible_rows.size(); + for (s32 k = 1; k < rowcount; ++k) { + s32 current = start + k; + if (current >= rowcount) + current -= rowcount; + if (doesRowStartWith(getRow(current), m_keynav_buffer)) { + m_selected = current; + break; + } + } + autoScroll(); + if (m_selected != old_selected) + sendTableEvent(0, false); + + return true; + } + } + if (event.EventType == EET_MOUSE_INPUT_EVENT) { + core::position2d<s32> p(event.MouseInput.X, event.MouseInput.Y); + + if (event.MouseInput.Event == EMIE_MOUSE_WHEEL) { + m_scrollbar->setPos(m_scrollbar->getPos() + + (event.MouseInput.Wheel < 0 ? -3 : 3) * + - (s32) m_rowheight / 2); + return true; + } + + // Find hovered row and cell + bool really_hovering = false; + s32 row_i = getRowAt(p.Y, really_hovering); + const Cell *cell = NULL; + if (really_hovering) { + s32 cell_j = getCellAt(p.X, row_i); + if (cell_j >= 0) + cell = &(getRow(row_i)->cells[cell_j]); + } + + // Update tooltip + setToolTipText(cell ? m_strings[cell->tooltip_index].c_str() : L""); + + // Fix for #1567/#1806: + // IGUIScrollBar passes double click events to its parent, + // which we don't want. Detect this case and discard the event + if (event.MouseInput.Event != EMIE_MOUSE_MOVED && + m_scrollbar->isVisible() && + m_scrollbar->isPointInside(p)) + return true; + + if (event.MouseInput.isLeftPressed() && + (isPointInside(p) || + event.MouseInput.Event == EMIE_MOUSE_MOVED)) { + s32 sel_column = 0; + bool sel_doubleclick = (event.MouseInput.Event + == EMIE_LMOUSE_DOUBLE_CLICK); + bool plusminus_clicked = false; + + // For certain events (left click), report column + // Also open/close subtrees when the +/- is clicked + if (cell && ( + event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN || + event.MouseInput.Event == EMIE_LMOUSE_DOUBLE_CLICK || + event.MouseInput.Event == EMIE_LMOUSE_TRIPLE_CLICK)) { + sel_column = cell->reported_column; + if (cell->content_type == COLUMN_TYPE_TREE) + plusminus_clicked = true; + } + + if (plusminus_clicked) { + if (event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN) { + toggleVisibleTree(row_i, 0, false); + } + } + else { + // Normal selection + s32 old_selected = m_selected; + m_selected = row_i; + autoScroll(); + + if (m_selected != old_selected || + sel_column >= 1 || + sel_doubleclick) { + sendTableEvent(sel_column, sel_doubleclick); + } + + // Treeview: double click opens/closes trees + if (m_has_tree_column && sel_doubleclick) { + toggleVisibleTree(m_selected, 0, false); + } + } + } + return true; + } + if (event.EventType == EET_GUI_EVENT && + event.GUIEvent.EventType == gui::EGET_SCROLL_BAR_CHANGED && + event.GUIEvent.Caller == m_scrollbar) { + // Don't pass events from our scrollbar to the parent + return true; + } + + return IGUIElement::OnEvent(event); +} + +/******************************************************************************/ +/* GUITable helper functions */ +/******************************************************************************/ + +s32 GUITable::allocString(const std::string &text) +{ + std::map<std::string, s32>::iterator it = m_alloc_strings.find(text); + if (it == m_alloc_strings.end()) { + s32 id = m_strings.size(); + std::wstring wtext = utf8_to_wide(text); + m_strings.emplace_back(wtext.c_str()); + m_alloc_strings.insert(std::make_pair(text, id)); + return id; + } + + return it->second; +} + +s32 GUITable::allocImage(const std::string &imagename) +{ + std::map<std::string, s32>::iterator it = m_alloc_images.find(imagename); + if (it == m_alloc_images.end()) { + s32 id = m_images.size(); + m_images.push_back(m_tsrc->getTexture(imagename)); + m_alloc_images.insert(std::make_pair(imagename, id)); + return id; + } + + return it->second; +} + +void GUITable::allocationComplete() +{ + // Called when done with creating rows and cells from table data, + // i.e. when allocString and allocImage won't be called anymore + m_alloc_strings.clear(); + m_alloc_images.clear(); +} + +const GUITable::Row* GUITable::getRow(s32 i) const +{ + if (i >= 0 && i < (s32) m_visible_rows.size()) + return &m_rows[m_visible_rows[i]]; + + return NULL; +} + +bool GUITable::doesRowStartWith(const Row *row, const core::stringw &str) const +{ + if (row == NULL) + return false; + + for (s32 j = 0; j < row->cellcount; ++j) { + Cell *cell = &row->cells[j]; + if (cell->content_type == COLUMN_TYPE_TEXT) { + const core::stringw &cellstr = m_strings[cell->content_index]; + if (cellstr.size() >= str.size() && + str.equals_ignore_case(cellstr.subString(0, str.size()))) + return true; + } + } + return false; +} + +s32 GUITable::getRowAt(s32 y, bool &really_hovering) const +{ + really_hovering = false; + + s32 rowcount = m_visible_rows.size(); + if (rowcount == 0) + return -1; + + // Use arithmetic to find row + s32 rel_y = y - AbsoluteRect.UpperLeftCorner.Y - 1; + s32 i = (rel_y + m_scrollbar->getPos()) / m_rowheight; + + if (i >= 0 && i < rowcount) { + really_hovering = true; + return i; + } + if (i < 0) + return 0; + + return rowcount - 1; +} + +s32 GUITable::getCellAt(s32 x, s32 row_i) const +{ + const Row *row = getRow(row_i); + if (row == NULL) + return -1; + + // Use binary search to find cell in row + s32 rel_x = x - AbsoluteRect.UpperLeftCorner.X - 1; + s32 jmin = 0; + s32 jmax = row->cellcount - 1; + while (jmin < jmax) { + s32 pivot = jmin + (jmax - jmin) / 2; + assert(pivot >= 0 && pivot < row->cellcount); + const Cell *cell = &row->cells[pivot]; + + if (rel_x >= cell->xmin && rel_x <= cell->xmax) + return pivot; + + if (rel_x < cell->xmin) + jmax = pivot - 1; + else + jmin = pivot + 1; + } + + if (jmin >= 0 && jmin < row->cellcount && + rel_x >= row->cells[jmin].xmin && + rel_x <= row->cells[jmin].xmax) + return jmin; + + return -1; +} + +void GUITable::autoScroll() +{ + if (m_selected >= 0) { + s32 pos = m_scrollbar->getPos(); + s32 maxpos = m_selected * m_rowheight; + s32 minpos = maxpos - (AbsoluteRect.getHeight() - m_rowheight); + if (pos > maxpos) + m_scrollbar->setPos(maxpos); + else if (pos < minpos) + m_scrollbar->setPos(minpos); + } +} + +void GUITable::updateScrollBar() +{ + s32 totalheight = m_rowheight * m_visible_rows.size(); + s32 scrollmax = MYMAX(0, totalheight - AbsoluteRect.getHeight()); + m_scrollbar->setVisible(scrollmax > 0); + m_scrollbar->setMax(scrollmax); + m_scrollbar->setSmallStep(m_rowheight); + m_scrollbar->setLargeStep(2 * m_rowheight); +} + +void GUITable::sendTableEvent(s32 column, bool doubleclick) +{ + m_sel_column = column; + m_sel_doubleclick = doubleclick; + if (Parent) { + SEvent e; + memset(&e, 0, sizeof e); + e.EventType = EET_GUI_EVENT; + e.GUIEvent.Caller = this; + e.GUIEvent.Element = 0; + e.GUIEvent.EventType = gui::EGET_TABLE_CHANGED; + Parent->OnEvent(e); + } +} + +void GUITable::getOpenedTrees(std::set<s32> &opened_trees) const +{ + opened_trees.clear(); + s32 rowcount = m_rows.size(); + for (s32 i = 0; i < rowcount - 1; ++i) { + if (m_rows[i].indent < m_rows[i+1].indent && + m_rows[i+1].visible_index != -2) + opened_trees.insert(i); + } +} + +void GUITable::setOpenedTrees(const std::set<s32> &opened_trees) +{ + s32 old_selected = -1; + if (m_selected >= 0) + old_selected = m_visible_rows[m_selected]; + + std::vector<s32> parents; + std::vector<s32> closed_parents; + + m_visible_rows.clear(); + + for (size_t i = 0; i < m_rows.size(); ++i) { + Row *row = &m_rows[i]; + + // Update list of ancestors + while (!parents.empty() && m_rows[parents.back()].indent >= row->indent) + parents.pop_back(); + while (!closed_parents.empty() && + m_rows[closed_parents.back()].indent >= row->indent) + closed_parents.pop_back(); + + assert(closed_parents.size() <= parents.size()); + + if (closed_parents.empty()) { + // Visible row + row->visible_index = m_visible_rows.size(); + m_visible_rows.push_back(i); + } + else if (parents.back() == closed_parents.back()) { + // Invisible row, direct parent is closed + row->visible_index = -2; + } + else { + // Invisible row, direct parent is open, some ancestor is closed + row->visible_index = -1; + } + + // If not a leaf, add to parents list + if (i < m_rows.size()-1 && row->indent < m_rows[i+1].indent) { + parents.push_back(i); + + s32 content_index = 0; // "-", open + if (opened_trees.count(i) == 0) { + closed_parents.push_back(i); + content_index = 1; // "+", closed + } + + // Update all cells of type "tree" + for (s32 j = 0; j < row->cellcount; ++j) + if (row->cells[j].content_type == COLUMN_TYPE_TREE) + row->cells[j].content_index = content_index; + } + } + + updateScrollBar(); + + // m_selected must be updated since it is a visible row index + if (old_selected >= 0) + m_selected = m_rows[old_selected].visible_index; +} + +void GUITable::openTree(s32 to_open) +{ + std::set<s32> opened_trees; + getOpenedTrees(opened_trees); + opened_trees.insert(to_open); + setOpenedTrees(opened_trees); +} + +void GUITable::closeTree(s32 to_close) +{ + std::set<s32> opened_trees; + getOpenedTrees(opened_trees); + opened_trees.erase(to_close); + setOpenedTrees(opened_trees); +} + +// The following function takes a visible row index (hidden rows skipped) +// dir: -1 = left (close), 0 = auto (toggle), 1 = right (open) +void GUITable::toggleVisibleTree(s32 row_i, int dir, bool move_selection) +{ + // Check if the chosen tree is currently open + const Row *row = getRow(row_i); + if (row == NULL) + return; + + bool was_open = false; + for (s32 j = 0; j < row->cellcount; ++j) { + if (row->cells[j].content_type == COLUMN_TYPE_TREE) { + was_open = row->cells[j].content_index == 0; + break; + } + } + + // Check if the chosen tree should be opened + bool do_open = !was_open; + if (dir < 0) + do_open = false; + else if (dir > 0) + do_open = true; + + // Close or open the tree; the heavy lifting is done by setOpenedTrees + if (was_open && !do_open) + closeTree(m_visible_rows[row_i]); + else if (!was_open && do_open) + openTree(m_visible_rows[row_i]); + + // Change selected row if requested by caller, + // this is useful for keyboard navigation + if (move_selection) { + s32 sel = row_i; + if (was_open && do_open) { + // Move selection to first child + const Row *maybe_child = getRow(sel + 1); + if (maybe_child && maybe_child->indent > row->indent) + sel++; + } + else if (!was_open && !do_open) { + // Move selection to parent + assert(getRow(sel) != NULL); + while (sel > 0 && getRow(sel - 1)->indent >= row->indent) + sel--; + sel--; + if (sel < 0) // was root already selected? + sel = row_i; + } + if (sel != m_selected) { + m_selected = sel; + autoScroll(); + sendTableEvent(0, false); + } + } +} + +void GUITable::alignContent(Cell *cell, s32 xmax, s32 content_width, s32 align) +{ + // requires that cell.xmin, cell.xmax are properly set + // align = 0: left aligned, 1: centered, 2: right aligned, 3: inline + if (align == 0) { + cell->xpos = cell->xmin; + cell->xmax = xmax; + } + else if (align == 1) { + cell->xpos = (cell->xmin + xmax - content_width) / 2; + cell->xmax = xmax; + } + else if (align == 2) { + cell->xpos = xmax - content_width; + cell->xmax = xmax; + } + else { + // inline alignment: the cells of the column don't have an aligned + // right border, the right border of each cell depends on the content + cell->xpos = cell->xmin; + cell->xmax = cell->xmin + content_width; + } +} diff --git a/src/gui/guiTable.h b/src/gui/guiTable.h new file mode 100644 index 000000000..f9337ff6d --- /dev/null +++ b/src/gui/guiTable.h @@ -0,0 +1,256 @@ +/* +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. +*/ + +#pragma once + +#include <map> +#include <set> +#include <string> +#include <vector> +#include <iostream> + +#include "irrlichttypes_extrabloated.h" + +class ISimpleTextureSource; + +/* + A table GUI element for GUIFormSpecMenu. + + Sends a EGET_TABLE_CHANGED event to the parent when + an item is selected or double-clicked. + Call checkEvent() to get info. + + Credits: The interface and implementation of this class are (very) + loosely based on the Irrlicht classes CGUITable and CGUIListBox. + CGUITable and CGUIListBox are licensed under the Irrlicht license; + they are Copyright (C) 2002-2012 Nikolaus Gebhardt +*/ +class GUITable : public gui::IGUIElement +{ +public: + /* + Stores dynamic data that should be preserved + when updating a formspec + */ + struct DynamicData + { + s32 selected = 0; + s32 scrollpos = 0; + s32 keynav_time = 0; + core::stringw keynav_buffer; + std::set<s32> opened_trees; + }; + + /* + An option of the form <name>=<value> + */ + struct Option + { + std::string name; + std::string value; + + Option(const std::string &name_, const std::string &value_) : + name(name_), + value(value_) + {} + }; + + /* + A list of options that concern the entire table + */ + typedef std::vector<Option> TableOptions; + + /* + A column with options + */ + struct TableColumn + { + std::string type; + std::vector<Option> options; + }; + typedef std::vector<TableColumn> TableColumns; + + + GUITable(gui::IGUIEnvironment *env, + gui::IGUIElement *parent, s32 id, + core::rect<s32> rectangle, + ISimpleTextureSource *tsrc); + + virtual ~GUITable(); + + /* Split a string of the form "name=value" into name and value */ + static Option splitOption(const std::string &str); + + /* Set textlist-like options, columns and data */ + void setTextList(const std::vector<std::string> &content, + bool transparent); + + /* Set generic table options, columns and content */ + // Adds empty strings to end of content if there is an incomplete row + void setTable(const TableOptions &options, + const TableColumns &columns, + std::vector<std::string> &content); + + /* Clear the table */ + void clear(); + + /* Get info about last event (string such as "CHG:1:2") */ + // Call this after EGET_TABLE_CHANGED + std::string checkEvent(); + + /* Get index of currently selected row (first=1; 0 if none selected) */ + s32 getSelected() const; + + /* Set currently selected row (first=1; 0 if none selected) */ + // If given index is not visible at the moment, select its parent + // Autoscroll to make the selected row fully visible + void setSelected(s32 index); + + /* Get selection, scroll position and opened (sub)trees */ + DynamicData getDynamicData() const; + + /* Set selection, scroll position and opened (sub)trees */ + void setDynamicData(const DynamicData &dyndata); + + /* Returns "GUITable" */ + virtual const c8* getTypeName() const; + + /* Must be called when position or size changes */ + virtual void updateAbsolutePosition(); + + /* Irrlicht draw method */ + virtual void draw(); + + /* Irrlicht event handler */ + virtual bool OnEvent(const SEvent &event); + +protected: + enum ColumnType { + COLUMN_TYPE_TEXT, + COLUMN_TYPE_IMAGE, + COLUMN_TYPE_COLOR, + COLUMN_TYPE_INDENT, + COLUMN_TYPE_TREE, + }; + + struct Cell { + s32 xmin; + s32 xmax; + s32 xpos; + ColumnType content_type; + s32 content_index; + s32 tooltip_index; + video::SColor color; + bool color_defined; + s32 reported_column; + }; + + struct Row { + Cell *cells; + s32 cellcount; + s32 indent; + // visible_index >= 0: is index of row in m_visible_rows + // visible_index == -1: parent open but other ancestor closed + // visible_index == -2: parent closed + s32 visible_index; + }; + + // Texture source + ISimpleTextureSource *m_tsrc; + + // Table content (including hidden rows) + std::vector<Row> m_rows; + // Table content (only visible; indices into m_rows) + std::vector<s32> m_visible_rows; + bool m_is_textlist = false; + bool m_has_tree_column = false; + + // Selection status + s32 m_selected = -1; // index of row (1...n), or 0 if none selected + s32 m_sel_column = 0; + bool m_sel_doubleclick = false; + + // Keyboard navigation stuff + u64 m_keynav_time = 0; + core::stringw m_keynav_buffer = L""; + + // Drawing and geometry information + bool m_border = true; + video::SColor m_color = video::SColor(255, 255, 255, 255); + video::SColor m_background = video::SColor(255, 0, 0, 0); + video::SColor m_highlight = video::SColor(255, 70, 100, 50); + video::SColor m_highlight_text = video::SColor(255, 255, 255, 255); + s32 m_rowheight = 1; + gui::IGUIFont *m_font = nullptr; + gui::IGUIScrollBar *m_scrollbar = nullptr; + + // Allocated strings and images + std::vector<core::stringw> m_strings; + std::vector<video::ITexture*> m_images; + std::map<std::string, s32> m_alloc_strings; + std::map<std::string, s32> m_alloc_images; + + s32 allocString(const std::string &text); + s32 allocImage(const std::string &imagename); + void allocationComplete(); + + // Helper for draw() that draws a single cell + void drawCell(const Cell *cell, video::SColor color, + const core::rect<s32> &rowrect, + const core::rect<s32> &client_clip); + + // Returns the i-th visible row (NULL if i is invalid) + const Row *getRow(s32 i) const; + + // Key navigation helper + bool doesRowStartWith(const Row *row, const core::stringw &str) const; + + // Returns the row at a given screen Y coordinate + // Returns index i such that m_rows[i] is valid (or -1 on error) + s32 getRowAt(s32 y, bool &really_hovering) const; + + // Returns the cell at a given screen X coordinate within m_rows[row_i] + // Returns index j such that m_rows[row_i].cells[j] is valid + // (or -1 on error) + s32 getCellAt(s32 x, s32 row_i) const; + + // Make the selected row fully visible + void autoScroll(); + + // Should be called when m_rowcount or m_rowheight changes + void updateScrollBar(); + + // Sends EET_GUI_EVENT / EGET_TABLE_CHANGED to parent + void sendTableEvent(s32 column, bool doubleclick); + + // Functions that help deal with hidden rows + // The following functions take raw row indices (hidden rows not skipped) + void getOpenedTrees(std::set<s32> &opened_trees) const; + void setOpenedTrees(const std::set<s32> &opened_trees); + void openTree(s32 to_open); + void closeTree(s32 to_close); + // The following function takes a visible row index (hidden rows skipped) + // dir: -1 = left (close), 0 = auto (toggle), 1 = right (open) + void toggleVisibleTree(s32 row_i, int dir, bool move_selection); + + // Aligns cell content in column according to alignment specification + // align = 0: left aligned, 1: centered, 2: right aligned, 3: inline + static void alignContent(Cell *cell, s32 xmax, s32 content_width, + s32 align); +}; diff --git a/src/gui/guiVolumeChange.cpp b/src/gui/guiVolumeChange.cpp new file mode 100644 index 000000000..8c462312b --- /dev/null +++ b/src/gui/guiVolumeChange.cpp @@ -0,0 +1,196 @@ +/* +Part of Minetest +Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com> +Copyright (C) 2013 Ciaran Gultnieks <ciaran@ciarang.com> +Copyright (C) 2013 RealBadAngel, Maciej Kasatkin <mk@realbadangel.pl> + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +#include "guiVolumeChange.h" +#include "debug.h" +#include "serialization.h" +#include <string> +#include <IGUICheckBox.h> +#include <IGUIButton.h> +#include <IGUIScrollBar.h> +#include <IGUIStaticText.h> +#include <IGUIFont.h> +#include "settings.h" + +#include "gettext.h" + +const int ID_soundText = 263; +const int ID_soundExitButton = 264; +const int ID_soundSlider = 265; +const int ID_soundMuteButton = 266; + +GUIVolumeChange::GUIVolumeChange(gui::IGUIEnvironment* env, + gui::IGUIElement* parent, s32 id, + IMenuManager *menumgr +): + GUIModalMenu(env, parent, id, menumgr) +{ +} + +GUIVolumeChange::~GUIVolumeChange() +{ + removeChildren(); +} + +void GUIVolumeChange::removeChildren() +{ + if (gui::IGUIElement *e = getElementFromId(ID_soundText)) + e->remove(); + + if (gui::IGUIElement *e = getElementFromId(ID_soundExitButton)) + e->remove(); + + if (gui::IGUIElement *e = getElementFromId(ID_soundSlider)) + e->remove(); +} + +void GUIVolumeChange::regenerateGui(v2u32 screensize) +{ + /* + Remove stuff + */ + removeChildren(); + + /* + Calculate new sizes and positions + */ + DesiredRect = core::rect<s32>( + screensize.X/2 - 380/2, + screensize.Y/2 - 200/2, + screensize.X/2 + 380/2, + screensize.Y/2 + 200/2 + ); + recalculateAbsolutePosition(false); + + v2s32 size = DesiredRect.getSize(); + int volume = (int)(g_settings->getFloat("sound_volume") * 100); + + /* + Add stuff + */ + { + core::rect<s32> rect(0, 0, 160, 20); + rect = rect + v2s32(size.X / 2 - 80, size.Y / 2 - 70); + + const wchar_t *text = wgettext("Sound Volume: "); + core::stringw volume_text = text; + delete [] text; + + volume_text += core::stringw(volume) + core::stringw("%"); + Environment->addStaticText(volume_text.c_str(), rect, false, + true, this, ID_soundText); + } + { + core::rect<s32> rect(0, 0, 80, 30); + rect = rect + v2s32(size.X/2-80/2, size.Y/2+55); + const wchar_t *text = wgettext("Exit"); + Environment->addButton(rect, this, ID_soundExitButton, + text); + delete[] text; + } + { + core::rect<s32> rect(0, 0, 300, 20); + rect = rect + v2s32(size.X / 2 - 150, size.Y / 2); + gui::IGUIScrollBar *e = Environment->addScrollBar(true, + rect, this, ID_soundSlider); + e->setMax(100); + e->setPos(volume); + } + { + core::rect<s32> rect(0, 0, 160, 20); + rect = rect + v2s32(size.X / 2 - 80, size.Y / 2 - 35); + const wchar_t *text = wgettext("Muted"); + Environment->addCheckBox(g_settings->getBool("mute_sound"), rect, this, + ID_soundMuteButton, text); + delete[] text; + } +} + +void GUIVolumeChange::drawMenu() +{ + gui::IGUISkin* skin = Environment->getSkin(); + if (!skin) + return; + video::IVideoDriver* driver = Environment->getVideoDriver(); + video::SColor bgcolor(140, 0, 0, 0); + driver->draw2DRectangle(bgcolor, AbsoluteRect, &AbsoluteClippingRect); + gui::IGUIElement::draw(); +} + +bool GUIVolumeChange::OnEvent(const SEvent& event) +{ + if (event.EventType == EET_KEY_INPUT_EVENT) { + if (event.KeyInput.Key == KEY_ESCAPE && event.KeyInput.PressedDown) { + quitMenu(); + return true; + } + + if (event.KeyInput.Key == KEY_RETURN && event.KeyInput.PressedDown) { + quitMenu(); + return true; + } + } else if (event.EventType == EET_GUI_EVENT) { + if (event.GUIEvent.EventType == gui::EGET_CHECKBOX_CHANGED) { + gui::IGUIElement *e = getElementFromId(ID_soundMuteButton); + if (e != NULL && e->getType() == gui::EGUIET_CHECK_BOX) { + g_settings->setBool("mute_sound", ((gui::IGUICheckBox*)e)->isChecked()); + } + + Environment->setFocus(this); + return true; + } + + if (event.GUIEvent.EventType == gui::EGET_BUTTON_CLICKED) { + if (event.GUIEvent.Caller->getID() == ID_soundExitButton) { + quitMenu(); + return true; + } + Environment->setFocus(this); + } + + if (event.GUIEvent.EventType == gui::EGET_ELEMENT_FOCUS_LOST + && isVisible()) { + if (!canTakeFocus(event.GUIEvent.Element)) { + dstream << "GUIMainMenu: Not allowing focus change." + << std::endl; + // Returning true disables focus change + return true; + } + } + if (event.GUIEvent.EventType == gui::EGET_SCROLL_BAR_CHANGED) { + if (event.GUIEvent.Caller->getID() == ID_soundSlider) { + s32 pos = ((gui::IGUIScrollBar*)event.GUIEvent.Caller)->getPos(); + g_settings->setFloat("sound_volume", (float) pos / 100); + + gui::IGUIElement *e = getElementFromId(ID_soundText); + const wchar_t *text = wgettext("Sound Volume: "); + core::stringw volume_text = text; + delete [] text; + + volume_text += core::stringw(pos) + core::stringw("%"); + e->setText(volume_text.c_str()); + return true; + } + } + + } + + return Parent ? Parent->OnEvent(event) : false; +} + diff --git a/src/gui/guiVolumeChange.h b/src/gui/guiVolumeChange.h new file mode 100644 index 000000000..7c7e19a86 --- /dev/null +++ b/src/gui/guiVolumeChange.h @@ -0,0 +1,45 @@ +/* +Part of Minetest +Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com> +Copyright (C) 2013 Ciaran Gultnieks <ciaran@ciarang.com> +Copyright (C) 2013 RealBadAngel, Maciej Kasatkin <mk@realbadangel.pl> + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +#pragma once + +#include "irrlichttypes_extrabloated.h" +#include "modalMenu.h" +#include <string> + +class GUIVolumeChange : public GUIModalMenu +{ +public: + GUIVolumeChange(gui::IGUIEnvironment* env, + gui::IGUIElement* parent, s32 id, + IMenuManager *menumgr); + ~GUIVolumeChange(); + + void removeChildren(); + /* + Remove and re-add (or reposition) stuff + */ + void regenerateGui(v2u32 screensize); + + void drawMenu(); + + bool OnEvent(const SEvent& event); + + bool pausesGame() { return true; } +}; diff --git a/src/gui/intlGUIEditBox.cpp b/src/gui/intlGUIEditBox.cpp new file mode 100644 index 000000000..279e7a48a --- /dev/null +++ b/src/gui/intlGUIEditBox.cpp @@ -0,0 +1,1601 @@ +// 11.11.2011 11:11 ValkaTR +// +// This is a copy of intlGUIEditBox from the irrlicht, but with a +// fix in the OnEvent function, which doesn't allowed input of +// other keyboard layouts than latin-1 +// +// Characters like: ä ö ü õ ы й ю я ъ № € ° ... +// +// This fix is only needed for linux, because of a bug +// in the CIrrDeviceLinux.cpp:1014-1015 of the irrlicht +// +// Also locale in the programm should not be changed to +// a "C", "POSIX" or whatever, it should be set to "", +// or XLookupString will return nothing for the international +// characters. +// +// From the "man setlocale": +// +// On startup of the main program, the portable "C" locale +// is selected as default. A program may be made +// portable to all locales by calling: +// +// setlocale(LC_ALL, ""); +// +// after program initialization.... +// + +// Copyright (C) 2002-2013 Nikolaus Gebhardt +// This file is part of the "Irrlicht Engine". +// For conditions of distribution and use, see copyright notice in irrlicht.h + +#include <util/numeric.h> +#include "intlGUIEditBox.h" + +#if defined(_IRR_COMPILE_WITH_GUI_) && IRRLICHT_VERSION_MAJOR == 1 && IRRLICHT_VERSION_MINOR < 9 + +#include "IGUISkin.h" +#include "IGUIEnvironment.h" +#include "IGUIFont.h" +#include "IVideoDriver.h" +//#include "rect.h" +//#include "irrlicht/os.cpp" +#include "porting.h" +//#include "Keycodes.h" +#include "log.h" + +/* + todo: + optional scrollbars + ctrl+left/right to select word + double click/ctrl click: word select + drag to select whole words, triple click to select line + optional? dragging selected text + numerical +*/ + +namespace irr +{ +namespace gui +{ + +//! constructor +intlGUIEditBox::intlGUIEditBox(const wchar_t* text, bool border, + IGUIEnvironment* environment, IGUIElement* parent, s32 id, + const core::rect<s32>& rectangle, bool writable, bool has_vscrollbar) + : IGUIEditBox(environment, parent, id, rectangle), + Border(border), FrameRect(rectangle), + m_scrollbar_width(0), m_vscrollbar(NULL), m_writable(writable) +{ + #ifdef _DEBUG + setDebugName("intlintlGUIEditBox"); + #endif + + Text = text; + + if (Environment) + Operator = Environment->getOSOperator(); + + if (Operator) + Operator->grab(); + + // this element can be tabbed to + setTabStop(true); + setTabOrder(-1); + + IGUISkin *skin = 0; + if (Environment) + skin = Environment->getSkin(); + if (Border && skin) + { + FrameRect.UpperLeftCorner.X += skin->getSize(EGDS_TEXT_DISTANCE_X)+1; + FrameRect.UpperLeftCorner.Y += skin->getSize(EGDS_TEXT_DISTANCE_Y)+1; + FrameRect.LowerRightCorner.X -= skin->getSize(EGDS_TEXT_DISTANCE_X)+1; + FrameRect.LowerRightCorner.Y -= skin->getSize(EGDS_TEXT_DISTANCE_Y)+1; + } + + if (skin && has_vscrollbar) { + m_scrollbar_width = skin->getSize(gui::EGDS_SCROLLBAR_SIZE); + + if (m_scrollbar_width > 0) { + createVScrollBar(); + } + } + + breakText(); + + calculateScrollPos(); + setWritable(writable); +} + + +//! destructor +intlGUIEditBox::~intlGUIEditBox() +{ + if (OverrideFont) + OverrideFont->drop(); + + if (Operator) + Operator->drop(); +} + + +//! Sets another skin independent font. +void intlGUIEditBox::setOverrideFont(IGUIFont* font) +{ + if (OverrideFont == font) + return; + + if (OverrideFont) + OverrideFont->drop(); + + OverrideFont = font; + + if (OverrideFont) + OverrideFont->grab(); + + breakText(); +} + +IGUIFont * intlGUIEditBox::getOverrideFont() const +{ + return OverrideFont; +} + +//! Get the font which is used right now for drawing +IGUIFont* intlGUIEditBox::getActiveFont() const +{ + if ( OverrideFont ) + return OverrideFont; + IGUISkin* skin = Environment->getSkin(); + if (skin) + return skin->getFont(); + return 0; +} + +//! Sets another color for the text. +void intlGUIEditBox::setOverrideColor(video::SColor color) +{ + OverrideColor = color; + OverrideColorEnabled = true; +} + +video::SColor intlGUIEditBox::getOverrideColor() const +{ + return OverrideColor; +} + +//! Turns the border on or off +void intlGUIEditBox::setDrawBorder(bool border) +{ + Border = border; +} + +//! Sets whether to draw the background +void intlGUIEditBox::setDrawBackground(bool draw) +{ +} + +//! Sets if the text should use the overide color or the color in the gui skin. +void intlGUIEditBox::enableOverrideColor(bool enable) +{ + OverrideColorEnabled = enable; +} + +bool intlGUIEditBox::isOverrideColorEnabled() const +{ + _IRR_IMPLEMENT_MANAGED_MARSHALLING_BUGFIX; + return OverrideColorEnabled; +} + +//! Enables or disables word wrap +void intlGUIEditBox::setWordWrap(bool enable) +{ + WordWrap = enable; + breakText(); +} + + +void intlGUIEditBox::updateAbsolutePosition() +{ + core::rect<s32> oldAbsoluteRect(AbsoluteRect); + IGUIElement::updateAbsolutePosition(); + if ( oldAbsoluteRect != AbsoluteRect ) + { + breakText(); + } +} + + +//! Checks if word wrap is enabled +bool intlGUIEditBox::isWordWrapEnabled() const +{ + _IRR_IMPLEMENT_MANAGED_MARSHALLING_BUGFIX; + return WordWrap; +} + + +//! Enables or disables newlines. +void intlGUIEditBox::setMultiLine(bool enable) +{ + MultiLine = enable; +} + + +//! Checks if multi line editing is enabled +bool intlGUIEditBox::isMultiLineEnabled() const +{ + _IRR_IMPLEMENT_MANAGED_MARSHALLING_BUGFIX; + return MultiLine; +} + + +void intlGUIEditBox::setPasswordBox(bool passwordBox, wchar_t passwordChar) +{ + PasswordBox = passwordBox; + if (PasswordBox) + { + PasswordChar = passwordChar; + setMultiLine(false); + setWordWrap(false); + BrokenText.clear(); + } +} + + +bool intlGUIEditBox::isPasswordBox() const +{ + _IRR_IMPLEMENT_MANAGED_MARSHALLING_BUGFIX; + return PasswordBox; +} + + +//! Sets text justification +void intlGUIEditBox::setTextAlignment(EGUI_ALIGNMENT horizontal, EGUI_ALIGNMENT vertical) +{ + HAlign = horizontal; + VAlign = vertical; +} + + +//! called if an event happened. +bool intlGUIEditBox::OnEvent(const SEvent& event) +{ + if (IsEnabled) + { + + switch(event.EventType) + { + case EET_GUI_EVENT: + if (event.GUIEvent.EventType == EGET_ELEMENT_FOCUS_LOST) + { + if (event.GUIEvent.Caller == this) + { + MouseMarking = false; + setTextMarkers(0,0); + } + } + break; + case EET_KEY_INPUT_EVENT: + { +#if (defined(__linux__) || defined(__FreeBSD__)) + // ################################################################ + // ValkaTR: + // This part is the difference from the original intlGUIEditBox + // It converts UTF-8 character into a UCS-2 (wchar_t) + wchar_t wc = L'_'; + mbtowc( &wc, (char *) &event.KeyInput.Char, sizeof(event.KeyInput.Char) ); + + //printf( "char: %lc (%u) \r\n", wc, wc ); + + SEvent irrevent(event); + irrevent.KeyInput.Char = wc; + // ################################################################ + + if (processKey(irrevent)) + return true; +#else + if (processKey(event)) + return true; +#endif // defined(linux) + + break; + } + case EET_MOUSE_INPUT_EVENT: + if (processMouse(event)) + return true; + break; + default: + break; + } + } + + return IGUIElement::OnEvent(event); +} + + +bool intlGUIEditBox::processKey(const SEvent& event) +{ + if (!event.KeyInput.PressedDown) + return false; + + bool textChanged = false; + s32 newMarkBegin = MarkBegin; + s32 newMarkEnd = MarkEnd; + + // control shortcut handling + + if (event.KeyInput.Control) + { + // german backlash '\' entered with control + '?' + if ( event.KeyInput.Char == '\\' ) + { + inputChar(event.KeyInput.Char); + return true; + } + + switch(event.KeyInput.Key) + { + case KEY_KEY_A: + // select all + newMarkBegin = 0; + newMarkEnd = Text.size(); + break; + case KEY_KEY_C: + // copy to clipboard + if (!PasswordBox && Operator && MarkBegin != MarkEnd) + { + const s32 realmbgn = MarkBegin < MarkEnd ? MarkBegin : MarkEnd; + const s32 realmend = MarkBegin < MarkEnd ? MarkEnd : MarkBegin; + + core::stringc s; + s = Text.subString(realmbgn, realmend - realmbgn).c_str(); + Operator->copyToClipboard(s.c_str()); + } + break; + case KEY_KEY_X: + // cut to the clipboard + if (!PasswordBox && Operator && MarkBegin != MarkEnd) + { + const s32 realmbgn = MarkBegin < MarkEnd ? MarkBegin : MarkEnd; + const s32 realmend = MarkBegin < MarkEnd ? MarkEnd : MarkBegin; + + // copy + core::stringc sc; + sc = Text.subString(realmbgn, realmend - realmbgn).c_str(); + Operator->copyToClipboard(sc.c_str()); + + if (IsEnabled) + { + // delete + core::stringw s; + s = Text.subString(0, realmbgn); + s.append( Text.subString(realmend, Text.size()-realmend) ); + Text = s; + + CursorPos = realmbgn; + newMarkBegin = 0; + newMarkEnd = 0; + textChanged = true; + } + } + break; + case KEY_KEY_V: + if ( !IsEnabled ) + break; + + // paste from the clipboard + if (Operator) + { + const s32 realmbgn = MarkBegin < MarkEnd ? MarkBegin : MarkEnd; + const s32 realmend = MarkBegin < MarkEnd ? MarkEnd : MarkBegin; + + // add new character + const c8* p = Operator->getTextFromClipboard(); + if (p) + { + if (MarkBegin == MarkEnd) + { + // insert text + core::stringw s = Text.subString(0, CursorPos); + s.append(p); + s.append( Text.subString(CursorPos, Text.size()-CursorPos) ); + + if (!Max || s.size()<=Max) // thx to Fish FH for fix + { + Text = s; + s = p; + CursorPos += s.size(); + } + } + else + { + // replace text + + core::stringw s = Text.subString(0, realmbgn); + s.append(p); + s.append( Text.subString(realmend, Text.size()-realmend) ); + + if (!Max || s.size()<=Max) // thx to Fish FH for fix + { + Text = s; + s = p; + CursorPos = realmbgn + s.size(); + } + } + } + + newMarkBegin = 0; + newMarkEnd = 0; + textChanged = true; + } + break; + case KEY_HOME: + // move/highlight to start of text + if (event.KeyInput.Shift) + { + newMarkEnd = CursorPos; + newMarkBegin = 0; + CursorPos = 0; + } + else + { + CursorPos = 0; + newMarkBegin = 0; + newMarkEnd = 0; + } + break; + case KEY_END: + // move/highlight to end of text + if (event.KeyInput.Shift) + { + newMarkBegin = CursorPos; + newMarkEnd = Text.size(); + CursorPos = 0; + } + else + { + CursorPos = Text.size(); + newMarkBegin = 0; + newMarkEnd = 0; + } + break; + default: + return false; + } + } + // default keyboard handling + else + switch(event.KeyInput.Key) + { + case KEY_END: + { + s32 p = Text.size(); + if (WordWrap || MultiLine) + { + p = getLineFromPos(CursorPos); + p = BrokenTextPositions[p] + (s32)BrokenText[p].size(); + if (p > 0 && (Text[p-1] == L'\r' || Text[p-1] == L'\n' )) + p-=1; + } + + if (event.KeyInput.Shift) + { + if (MarkBegin == MarkEnd) + newMarkBegin = CursorPos; + + newMarkEnd = p; + } + else + { + newMarkBegin = 0; + newMarkEnd = 0; + } + CursorPos = p; + BlinkStartTime = porting::getTimeMs(); + } + break; + case KEY_HOME: + { + + s32 p = 0; + if (WordWrap || MultiLine) + { + p = getLineFromPos(CursorPos); + p = BrokenTextPositions[p]; + } + + if (event.KeyInput.Shift) + { + if (MarkBegin == MarkEnd) + newMarkBegin = CursorPos; + newMarkEnd = p; + } + else + { + newMarkBegin = 0; + newMarkEnd = 0; + } + CursorPos = p; + BlinkStartTime = porting::getTimeMs(); + } + break; + case KEY_RETURN: + if (MultiLine) + { + inputChar(L'\n'); + return true; + } + else + { + sendGuiEvent( EGET_EDITBOX_ENTER ); + } + break; + case KEY_LEFT: + + if (event.KeyInput.Shift) + { + if (CursorPos > 0) + { + if (MarkBegin == MarkEnd) + newMarkBegin = CursorPos; + + newMarkEnd = CursorPos-1; + } + } + else + { + newMarkBegin = 0; + newMarkEnd = 0; + } + + if (CursorPos > 0) CursorPos--; + BlinkStartTime = porting::getTimeMs(); + break; + + case KEY_RIGHT: + if (event.KeyInput.Shift) + { + if (Text.size() > (u32)CursorPos) + { + if (MarkBegin == MarkEnd) + newMarkBegin = CursorPos; + + newMarkEnd = CursorPos+1; + } + } + else + { + newMarkBegin = 0; + newMarkEnd = 0; + } + + if (Text.size() > (u32)CursorPos) CursorPos++; + BlinkStartTime = porting::getTimeMs(); + break; + case KEY_UP: + if (MultiLine || (WordWrap && BrokenText.size() > 1) ) + { + s32 lineNo = getLineFromPos(CursorPos); + s32 mb = (MarkBegin == MarkEnd) ? CursorPos : (MarkBegin > MarkEnd ? MarkBegin : MarkEnd); + if (lineNo > 0) + { + s32 cp = CursorPos - BrokenTextPositions[lineNo]; + if ((s32)BrokenText[lineNo-1].size() < cp) + CursorPos = BrokenTextPositions[lineNo-1] + (s32)BrokenText[lineNo-1].size()-1; + else + CursorPos = BrokenTextPositions[lineNo-1] + cp; + } + + if (event.KeyInput.Shift) + { + newMarkBegin = mb; + newMarkEnd = CursorPos; + } + else + { + newMarkBegin = 0; + newMarkEnd = 0; + } + + } + else + { + return false; + } + break; + case KEY_DOWN: + if (MultiLine || (WordWrap && BrokenText.size() > 1) ) + { + s32 lineNo = getLineFromPos(CursorPos); + s32 mb = (MarkBegin == MarkEnd) ? CursorPos : (MarkBegin < MarkEnd ? MarkBegin : MarkEnd); + if (lineNo < (s32)BrokenText.size()-1) + { + s32 cp = CursorPos - BrokenTextPositions[lineNo]; + if ((s32)BrokenText[lineNo+1].size() < cp) + CursorPos = BrokenTextPositions[lineNo+1] + BrokenText[lineNo+1].size()-1; + else + CursorPos = BrokenTextPositions[lineNo+1] + cp; + } + + if (event.KeyInput.Shift) + { + newMarkBegin = mb; + newMarkEnd = CursorPos; + } + else + { + newMarkBegin = 0; + newMarkEnd = 0; + } + + } + else + { + return false; + } + break; + + case KEY_BACK: + if ( !this->IsEnabled ) + break; + + if (!Text.empty()) { + core::stringw s; + + if (MarkBegin != MarkEnd) + { + // delete marked text + const s32 realmbgn = MarkBegin < MarkEnd ? MarkBegin : MarkEnd; + const s32 realmend = MarkBegin < MarkEnd ? MarkEnd : MarkBegin; + + s = Text.subString(0, realmbgn); + s.append( Text.subString(realmend, Text.size()-realmend) ); + Text = s; + + CursorPos = realmbgn; + } + else + { + // delete text behind cursor + if (CursorPos>0) + s = Text.subString(0, CursorPos-1); + else + s = L""; + s.append( Text.subString(CursorPos, Text.size()-CursorPos) ); + Text = s; + --CursorPos; + } + + if (CursorPos < 0) + CursorPos = 0; + BlinkStartTime = porting::getTimeMs(); + newMarkBegin = 0; + newMarkEnd = 0; + textChanged = true; + } + break; + case KEY_DELETE: + if ( !this->IsEnabled ) + break; + + if (!Text.empty()) { + core::stringw s; + + if (MarkBegin != MarkEnd) + { + // delete marked text + const s32 realmbgn = MarkBegin < MarkEnd ? MarkBegin : MarkEnd; + const s32 realmend = MarkBegin < MarkEnd ? MarkEnd : MarkBegin; + + s = Text.subString(0, realmbgn); + s.append( Text.subString(realmend, Text.size()-realmend) ); + Text = s; + + CursorPos = realmbgn; + } + else + { + // delete text before cursor + s = Text.subString(0, CursorPos); + s.append( Text.subString(CursorPos+1, Text.size()-CursorPos-1) ); + Text = s; + } + + if (CursorPos > (s32)Text.size()) + CursorPos = (s32)Text.size(); + + BlinkStartTime = porting::getTimeMs(); + newMarkBegin = 0; + newMarkEnd = 0; + textChanged = true; + } + break; + + case KEY_ESCAPE: + case KEY_TAB: + case KEY_SHIFT: + case KEY_F1: + case KEY_F2: + case KEY_F3: + case KEY_F4: + case KEY_F5: + case KEY_F6: + case KEY_F7: + case KEY_F8: + case KEY_F9: + case KEY_F10: + case KEY_F11: + case KEY_F12: + case KEY_F13: + case KEY_F14: + case KEY_F15: + case KEY_F16: + case KEY_F17: + case KEY_F18: + case KEY_F19: + case KEY_F20: + case KEY_F21: + case KEY_F22: + case KEY_F23: + case KEY_F24: + // ignore these keys + return false; + + default: + inputChar(event.KeyInput.Char); + return true; + } + + // Set new text markers + setTextMarkers( newMarkBegin, newMarkEnd ); + + // break the text if it has changed + if (textChanged) + { + breakText(); + sendGuiEvent(EGET_EDITBOX_CHANGED); + } + + calculateScrollPos(); + + return true; +} + + +//! draws the element and its children +void intlGUIEditBox::draw() +{ + if (!IsVisible) + return; + + const bool focus = Environment->hasFocus(this); + + IGUISkin* skin = Environment->getSkin(); + if (!skin) + return; + + FrameRect = AbsoluteRect; + + // draw the border + + if (Border) + { + if (m_writable) { + skin->draw3DSunkenPane(this, skin->getColor(EGDC_WINDOW), + false, true, FrameRect, &AbsoluteClippingRect); + } + + FrameRect.UpperLeftCorner.X += skin->getSize(EGDS_TEXT_DISTANCE_X)+1; + FrameRect.UpperLeftCorner.Y += skin->getSize(EGDS_TEXT_DISTANCE_Y)+1; + FrameRect.LowerRightCorner.X -= skin->getSize(EGDS_TEXT_DISTANCE_X)+1; + FrameRect.LowerRightCorner.Y -= skin->getSize(EGDS_TEXT_DISTANCE_Y)+1; + } + + updateVScrollBar(); + core::rect<s32> localClipRect = FrameRect; + localClipRect.clipAgainst(AbsoluteClippingRect); + + // draw the text + + IGUIFont* font = OverrideFont; + if (!OverrideFont) + font = skin->getFont(); + + s32 cursorLine = 0; + s32 charcursorpos = 0; + + if (font) + { + if (LastBreakFont != font) + { + breakText(); + } + + // calculate cursor pos + + core::stringw *txtLine = &Text; + s32 startPos = 0; + + core::stringw s, s2; + + // get mark position + const bool ml = (!PasswordBox && (WordWrap || MultiLine)); + const s32 realmbgn = MarkBegin < MarkEnd ? MarkBegin : MarkEnd; + const s32 realmend = MarkBegin < MarkEnd ? MarkEnd : MarkBegin; + const s32 hlineStart = ml ? getLineFromPos(realmbgn) : 0; + const s32 hlineCount = ml ? getLineFromPos(realmend) - hlineStart + 1 : 1; + const s32 lineCount = ml ? BrokenText.size() : 1; + + // Save the override color information. + // Then, alter it if the edit box is disabled. + const bool prevOver = OverrideColorEnabled; + const video::SColor prevColor = OverrideColor; + + if (!Text.empty()) { + if (!IsEnabled && !OverrideColorEnabled) + { + OverrideColorEnabled = true; + OverrideColor = skin->getColor(EGDC_GRAY_TEXT); + } + + for (s32 i=0; i < lineCount; ++i) + { + setTextRect(i); + + // clipping test - don't draw anything outside the visible area + core::rect<s32> c = localClipRect; + c.clipAgainst(CurrentTextRect); + if (!c.isValid()) + continue; + + // get current line + if (PasswordBox) + { + if (BrokenText.size() != 1) + { + BrokenText.clear(); + BrokenText.push_back(core::stringw()); + } + if (BrokenText[0].size() != Text.size()) + { + BrokenText[0] = Text; + for (u32 q = 0; q < Text.size(); ++q) + { + BrokenText[0] [q] = PasswordChar; + } + } + txtLine = &BrokenText[0]; + startPos = 0; + } + else + { + txtLine = ml ? &BrokenText[i] : &Text; + startPos = ml ? BrokenTextPositions[i] : 0; + } + + + // draw normal text + font->draw(txtLine->c_str(), CurrentTextRect, + OverrideColorEnabled ? OverrideColor : skin->getColor(EGDC_BUTTON_TEXT), + false, true, &localClipRect); + + // draw mark and marked text + if (focus && MarkBegin != MarkEnd && i >= hlineStart && i < hlineStart + hlineCount) + { + + s32 mbegin = 0, mend = 0; + s32 lineStartPos = 0, lineEndPos = txtLine->size(); + + if (i == hlineStart) + { + // highlight start is on this line + s = txtLine->subString(0, realmbgn - startPos); + mbegin = font->getDimension(s.c_str()).Width; + + // deal with kerning + mbegin += font->getKerningWidth( + &((*txtLine)[realmbgn - startPos]), + realmbgn - startPos > 0 ? &((*txtLine)[realmbgn - startPos - 1]) : 0); + + lineStartPos = realmbgn - startPos; + } + if (i == hlineStart + hlineCount - 1) + { + // highlight end is on this line + s2 = txtLine->subString(0, realmend - startPos); + mend = font->getDimension(s2.c_str()).Width; + lineEndPos = (s32)s2.size(); + } + else + mend = font->getDimension(txtLine->c_str()).Width; + + CurrentTextRect.UpperLeftCorner.X += mbegin; + CurrentTextRect.LowerRightCorner.X = CurrentTextRect.UpperLeftCorner.X + mend - mbegin; + + // draw mark + skin->draw2DRectangle(this, skin->getColor(EGDC_HIGH_LIGHT), CurrentTextRect, &localClipRect); + + // draw marked text + s = txtLine->subString(lineStartPos, lineEndPos - lineStartPos); + + if (!s.empty()) + font->draw(s.c_str(), CurrentTextRect, + OverrideColorEnabled ? OverrideColor : skin->getColor(EGDC_HIGH_LIGHT_TEXT), + false, true, &localClipRect); + + } + } + + // Return the override color information to its previous settings. + OverrideColorEnabled = prevOver; + OverrideColor = prevColor; + } + + // draw cursor + + if (WordWrap || MultiLine) + { + cursorLine = getLineFromPos(CursorPos); + txtLine = &BrokenText[cursorLine]; + startPos = BrokenTextPositions[cursorLine]; + } + s = txtLine->subString(0,CursorPos-startPos); + charcursorpos = font->getDimension(s.c_str()).Width + + font->getKerningWidth(L"_", CursorPos-startPos > 0 ? &((*txtLine)[CursorPos-startPos-1]) : 0); + + if (m_writable) { + if (focus && (porting::getTimeMs() - BlinkStartTime) % 700 < 350) { + setTextRect(cursorLine); + CurrentTextRect.UpperLeftCorner.X += charcursorpos; + + font->draw(L"_", CurrentTextRect, + OverrideColorEnabled ? OverrideColor : skin->getColor(EGDC_BUTTON_TEXT), + false, true, &localClipRect); + } + } + } + + // draw children + IGUIElement::draw(); +} + + +//! Sets the new caption of this element. +void intlGUIEditBox::setText(const wchar_t* text) +{ + Text = text; + if (u32(CursorPos) > Text.size()) + CursorPos = Text.size(); + HScrollPos = 0; + breakText(); +} + + +//! Enables or disables automatic scrolling with cursor position +//! \param enable: If set to true, the text will move around with the cursor position +void intlGUIEditBox::setAutoScroll(bool enable) +{ + AutoScroll = enable; +} + + +//! Checks to see if automatic scrolling is enabled +//! \return true if automatic scrolling is enabled, false if not +bool intlGUIEditBox::isAutoScrollEnabled() const +{ + _IRR_IMPLEMENT_MANAGED_MARSHALLING_BUGFIX; + return AutoScroll; +} + + +//! Gets the area of the text in the edit box +//! \return Returns the size in pixels of the text +core::dimension2du intlGUIEditBox::getTextDimension() +{ + core::rect<s32> ret; + + setTextRect(0); + ret = CurrentTextRect; + + for (u32 i=1; i < BrokenText.size(); ++i) + { + setTextRect(i); + ret.addInternalPoint(CurrentTextRect.UpperLeftCorner); + ret.addInternalPoint(CurrentTextRect.LowerRightCorner); + } + + return core::dimension2du(ret.getSize()); +} + + +//! Sets the maximum amount of characters which may be entered in the box. +//! \param max: Maximum amount of characters. If 0, the character amount is +//! infinity. +void intlGUIEditBox::setMax(u32 max) +{ + Max = max; + + if (Text.size() > Max && Max != 0) + Text = Text.subString(0, Max); +} + + +//! Returns maximum amount of characters, previously set by setMax(); +u32 intlGUIEditBox::getMax() const +{ + return Max; +} + + +bool intlGUIEditBox::processMouse(const SEvent& event) +{ + switch(event.MouseInput.Event) + { + case irr::EMIE_LMOUSE_LEFT_UP: + if (Environment->hasFocus(this)) + { + CursorPos = getCursorPos(event.MouseInput.X, event.MouseInput.Y); + if (MouseMarking) + { + setTextMarkers( MarkBegin, CursorPos ); + } + MouseMarking = false; + calculateScrollPos(); + return true; + } + break; + case irr::EMIE_MOUSE_MOVED: + { + if (MouseMarking) + { + CursorPos = getCursorPos(event.MouseInput.X, event.MouseInput.Y); + setTextMarkers( MarkBegin, CursorPos ); + calculateScrollPos(); + return true; + } + } + break; + case EMIE_LMOUSE_PRESSED_DOWN: + if (!Environment->hasFocus(this)) + { + BlinkStartTime = porting::getTimeMs(); + MouseMarking = true; + CursorPos = getCursorPos(event.MouseInput.X, event.MouseInput.Y); + setTextMarkers(CursorPos, CursorPos ); + calculateScrollPos(); + return true; + } + else + { + if (!AbsoluteClippingRect.isPointInside( + core::position2d<s32>(event.MouseInput.X, event.MouseInput.Y))) { + return false; + } + + + // move cursor + CursorPos = getCursorPos(event.MouseInput.X, event.MouseInput.Y); + + s32 newMarkBegin = MarkBegin; + if (!MouseMarking) + newMarkBegin = CursorPos; + + MouseMarking = true; + setTextMarkers( newMarkBegin, CursorPos); + calculateScrollPos(); + return true; + } + break; + case EMIE_MOUSE_WHEEL: + if (m_vscrollbar) { + s32 pos = m_vscrollbar->getPos(); + s32 step = m_vscrollbar->getSmallStep(); + m_vscrollbar->setPos(pos - event.MouseInput.Wheel * step); + } + break; + default: + break; + } + + return false; +} + + +s32 intlGUIEditBox::getCursorPos(s32 x, s32 y) +{ + IGUIFont* font = OverrideFont; + IGUISkin* skin = Environment->getSkin(); + if (!OverrideFont) + font = skin->getFont(); + + const u32 lineCount = (WordWrap || MultiLine) ? BrokenText.size() : 1; + + core::stringw *txtLine = NULL; + s32 startPos = 0; + u32 curr_line_idx = 0; + x += 3; + + for (; curr_line_idx < lineCount; ++curr_line_idx) { + setTextRect(curr_line_idx); + if (curr_line_idx == 0 && y < CurrentTextRect.UpperLeftCorner.Y) + y = CurrentTextRect.UpperLeftCorner.Y; + if (curr_line_idx == lineCount - 1 && y > CurrentTextRect.LowerRightCorner.Y) + y = CurrentTextRect.LowerRightCorner.Y; + + // is it inside this region? + if (y >= CurrentTextRect.UpperLeftCorner.Y && y <= CurrentTextRect.LowerRightCorner.Y) { + // we've found the clicked line + txtLine = (WordWrap || MultiLine) ? &BrokenText[curr_line_idx] : &Text; + startPos = (WordWrap || MultiLine) ? BrokenTextPositions[curr_line_idx] : 0; + break; + } + } + + if (x < CurrentTextRect.UpperLeftCorner.X) + x = CurrentTextRect.UpperLeftCorner.X; + else if (x > CurrentTextRect.LowerRightCorner.X) + x = CurrentTextRect.LowerRightCorner.X; + + s32 idx = font->getCharacterFromPos(txtLine->c_str(), x - CurrentTextRect.UpperLeftCorner.X); + // Special handling for last line, if we are on limits, add 1 extra shift because idx + // will be the last char, not null char of the wstring + if (curr_line_idx == lineCount - 1 && x == CurrentTextRect.LowerRightCorner.X) + idx++; + + return rangelim(idx + startPos, 0, S32_MAX); +} + + +//! Breaks the single text line. +void intlGUIEditBox::breakText() +{ + IGUISkin* skin = Environment->getSkin(); + + if ((!WordWrap && !MultiLine) || !skin) + return; + + BrokenText.clear(); // need to reallocate :/ + BrokenTextPositions.set_used(0); + + IGUIFont* font = OverrideFont; + if (!OverrideFont) + font = skin->getFont(); + + if (!font) + return; + + LastBreakFont = font; + + core::stringw line; + core::stringw word; + core::stringw whitespace; + s32 lastLineStart = 0; + s32 size = Text.size(); + s32 length = 0; + s32 elWidth = RelativeRect.getWidth() - 6; + wchar_t c; + + for (s32 i=0; i<size; ++i) + { + c = Text[i]; + bool lineBreak = false; + + if (c == L'\r') // Mac or Windows breaks + { + lineBreak = true; + c = ' '; + if (Text[i+1] == L'\n') // Windows breaks + { + Text.erase(i+1); + --size; + } + } + else if (c == L'\n') // Unix breaks + { + lineBreak = true; + c = ' '; + } + + // don't break if we're not a multi-line edit box + if (!MultiLine) + lineBreak = false; + + if (c == L' ' || c == 0 || i == (size-1)) + { + if (!word.empty()) { + // here comes the next whitespace, look if + // we can break the last word to the next line. + s32 whitelgth = font->getDimension(whitespace.c_str()).Width; + s32 worldlgth = font->getDimension(word.c_str()).Width; + + if (WordWrap && length + worldlgth + whitelgth > elWidth) + { + // break to next line + length = worldlgth; + BrokenText.push_back(line); + BrokenTextPositions.push_back(lastLineStart); + lastLineStart = i - (s32)word.size(); + line = word; + } + else + { + // add word to line + line += whitespace; + line += word; + length += whitelgth + worldlgth; + } + + word = L""; + whitespace = L""; + } + + whitespace += c; + + // compute line break + if (lineBreak) + { + line += whitespace; + line += word; + BrokenText.push_back(line); + BrokenTextPositions.push_back(lastLineStart); + lastLineStart = i+1; + line = L""; + word = L""; + whitespace = L""; + length = 0; + } + } + else + { + // yippee this is a word.. + word += c; + } + } + + line += whitespace; + line += word; + BrokenText.push_back(line); + BrokenTextPositions.push_back(lastLineStart); +} + + +void intlGUIEditBox::setTextRect(s32 line) +{ + core::dimension2du d; + + IGUISkin* skin = Environment->getSkin(); + if (!skin) + return; + + IGUIFont* font = OverrideFont ? OverrideFont : skin->getFont(); + + if (!font) + return; + + // get text dimension + const u32 lineCount = (WordWrap || MultiLine) ? BrokenText.size() : 1; + if (WordWrap || MultiLine) + { + d = font->getDimension(BrokenText[line].c_str()); + } + else + { + d = font->getDimension(Text.c_str()); + d.Height = AbsoluteRect.getHeight(); + } + d.Height += font->getKerningHeight(); + + // justification + switch (HAlign) + { + case EGUIA_CENTER: + // align to h centre + CurrentTextRect.UpperLeftCorner.X = (FrameRect.getWidth()/2) - (d.Width/2); + CurrentTextRect.LowerRightCorner.X = (FrameRect.getWidth()/2) + (d.Width/2); + break; + case EGUIA_LOWERRIGHT: + // align to right edge + CurrentTextRect.UpperLeftCorner.X = FrameRect.getWidth() - d.Width; + CurrentTextRect.LowerRightCorner.X = FrameRect.getWidth(); + break; + default: + // align to left edge + CurrentTextRect.UpperLeftCorner.X = 0; + CurrentTextRect.LowerRightCorner.X = d.Width; + + } + + switch (VAlign) + { + case EGUIA_CENTER: + // align to v centre + CurrentTextRect.UpperLeftCorner.Y = + (FrameRect.getHeight()/2) - (lineCount*d.Height)/2 + d.Height*line; + break; + case EGUIA_LOWERRIGHT: + // align to bottom edge + CurrentTextRect.UpperLeftCorner.Y = + FrameRect.getHeight() - lineCount*d.Height + d.Height*line; + break; + default: + // align to top edge + CurrentTextRect.UpperLeftCorner.Y = d.Height*line; + break; + } + + CurrentTextRect.UpperLeftCorner.X -= HScrollPos; + CurrentTextRect.LowerRightCorner.X -= HScrollPos; + CurrentTextRect.UpperLeftCorner.Y -= VScrollPos; + CurrentTextRect.LowerRightCorner.Y = CurrentTextRect.UpperLeftCorner.Y + d.Height; + + CurrentTextRect += FrameRect.UpperLeftCorner; + +} + + +s32 intlGUIEditBox::getLineFromPos(s32 pos) +{ + if (!WordWrap && !MultiLine) + return 0; + + s32 i=0; + while (i < (s32)BrokenTextPositions.size()) + { + if (BrokenTextPositions[i] > pos) + return i-1; + ++i; + } + return (s32)BrokenTextPositions.size() - 1; +} + + +void intlGUIEditBox::inputChar(wchar_t c) +{ + if (!IsEnabled) + return; + + if (c != 0) + { + if (Text.size() < Max || Max == 0) + { + core::stringw s; + + if (MarkBegin != MarkEnd) + { + // replace marked text + const s32 realmbgn = MarkBegin < MarkEnd ? MarkBegin : MarkEnd; + const s32 realmend = MarkBegin < MarkEnd ? MarkEnd : MarkBegin; + + s = Text.subString(0, realmbgn); + s.append(c); + s.append( Text.subString(realmend, Text.size()-realmend) ); + Text = s; + CursorPos = realmbgn+1; + } + else + { + // add new character + s = Text.subString(0, CursorPos); + s.append(c); + s.append( Text.subString(CursorPos, Text.size()-CursorPos) ); + Text = s; + ++CursorPos; + } + + BlinkStartTime = porting::getTimeMs(); + setTextMarkers(0, 0); + } + } + breakText(); + sendGuiEvent(EGET_EDITBOX_CHANGED); + calculateScrollPos(); +} + + +void intlGUIEditBox::calculateScrollPos() +{ + if (!AutoScroll) + return; + + // calculate horizontal scroll position + s32 cursLine = getLineFromPos(CursorPos); + setTextRect(cursLine); + + // don't do horizontal scrolling when wordwrap is enabled. + if (!WordWrap) + { + // get cursor position + IGUISkin* skin = Environment->getSkin(); + if (!skin) + return; + IGUIFont* font = OverrideFont ? OverrideFont : skin->getFont(); + if (!font) + return; + + core::stringw *txtLine = MultiLine ? &BrokenText[cursLine] : &Text; + s32 cPos = MultiLine ? CursorPos - BrokenTextPositions[cursLine] : CursorPos; + + s32 cStart = CurrentTextRect.UpperLeftCorner.X + HScrollPos + + font->getDimension(txtLine->subString(0, cPos).c_str()).Width; + + s32 cEnd = cStart + font->getDimension(L"_ ").Width; + + if (FrameRect.LowerRightCorner.X < cEnd) + HScrollPos = cEnd - FrameRect.LowerRightCorner.X; + else if (FrameRect.UpperLeftCorner.X > cStart) + HScrollPos = cStart - FrameRect.UpperLeftCorner.X; + else + HScrollPos = 0; + + // todo: adjust scrollbar + } + + // vertical scroll position + if (FrameRect.LowerRightCorner.Y < CurrentTextRect.LowerRightCorner.Y + VScrollPos) + VScrollPos = CurrentTextRect.LowerRightCorner.Y - FrameRect.LowerRightCorner.Y + VScrollPos; + + else if (FrameRect.UpperLeftCorner.Y > CurrentTextRect.UpperLeftCorner.Y + VScrollPos) + VScrollPos = CurrentTextRect.UpperLeftCorner.Y - FrameRect.UpperLeftCorner.Y + VScrollPos; + else + VScrollPos = 0; + + // todo: adjust scrollbar + if (m_vscrollbar) + m_vscrollbar->setPos(VScrollPos); +} + +//! set text markers +void intlGUIEditBox::setTextMarkers(s32 begin, s32 end) +{ + if ( begin != MarkBegin || end != MarkEnd ) + { + MarkBegin = begin; + MarkEnd = end; + sendGuiEvent(EGET_EDITBOX_MARKING_CHANGED); + } +} + +//! send some gui event to parent +void intlGUIEditBox::sendGuiEvent(EGUI_EVENT_TYPE type) +{ + if ( Parent ) + { + SEvent e; + e.EventType = EET_GUI_EVENT; + e.GUIEvent.Caller = this; + e.GUIEvent.Element = 0; + e.GUIEvent.EventType = type; + + Parent->OnEvent(e); + } +} + +//! Create a vertical scrollbar +void intlGUIEditBox::createVScrollBar() +{ + s32 fontHeight = 1; + + if (OverrideFont) { + fontHeight = OverrideFont->getDimension(L"").Height; + } else { + if (IGUISkin* skin = Environment->getSkin()) { + if (IGUIFont* font = skin->getFont()) { + fontHeight = font->getDimension(L"").Height; + } + } + } + + irr::core::rect<s32> scrollbarrect = FrameRect; + scrollbarrect.UpperLeftCorner.X += FrameRect.getWidth() - m_scrollbar_width; + m_vscrollbar = Environment->addScrollBar(false, scrollbarrect, getParent(), getID()); + m_vscrollbar->setVisible(false); + m_vscrollbar->setSmallStep(3 * fontHeight); + m_vscrollbar->setLargeStep(10 * fontHeight); +} + +//! Update the vertical scrollbar (visibilty & scroll position) +void intlGUIEditBox::updateVScrollBar() +{ + if (!m_vscrollbar) + return; + + // OnScrollBarChanged(...) + if (m_vscrollbar->getPos() != VScrollPos) { + s32 deltaScrollY = m_vscrollbar->getPos() - VScrollPos; + CurrentTextRect.UpperLeftCorner.Y -= deltaScrollY; + CurrentTextRect.LowerRightCorner.Y -= deltaScrollY; + + s32 scrollymax = getTextDimension().Height - FrameRect.getHeight(); + if (scrollymax != m_vscrollbar->getMax()) { + // manage a newline or a deleted line + m_vscrollbar->setMax(scrollymax); + calculateScrollPos(); + } else { + // manage a newline or a deleted line + VScrollPos = m_vscrollbar->getPos(); + } + } + + // check if a vertical scrollbar is needed ? + if (getTextDimension().Height > (u32) FrameRect.getHeight()) { + s32 scrollymax = getTextDimension().Height - FrameRect.getHeight(); + if (scrollymax != m_vscrollbar->getMax()) { + m_vscrollbar->setMax(scrollymax); + } + + if (!m_vscrollbar->isVisible() && MultiLine) { + AbsoluteRect.LowerRightCorner.X -= m_scrollbar_width; + + m_vscrollbar->setVisible(true); + } + } else { + if (m_vscrollbar->isVisible()) { + AbsoluteRect.LowerRightCorner.X += m_scrollbar_width; + + VScrollPos = 0; + m_vscrollbar->setPos(0); + m_vscrollbar->setMax(1); + m_vscrollbar->setVisible(false); + } + } +} + +void intlGUIEditBox::setWritable(bool can_write_text) +{ + m_writable = can_write_text; +} + +//! Writes attributes of the element. +void intlGUIEditBox::serializeAttributes(io::IAttributes* out, io::SAttributeReadWriteOptions* options=0) const +{ + // IGUIEditBox::serializeAttributes(out,options); + + out->addBool ("OverrideColorEnabled",OverrideColorEnabled ); + out->addColor ("OverrideColor", OverrideColor); + // out->addFont("OverrideFont",OverrideFont); + out->addInt ("MaxChars", Max); + out->addBool ("WordWrap", WordWrap); + out->addBool ("MultiLine", MultiLine); + out->addBool ("AutoScroll", AutoScroll); + out->addBool ("PasswordBox", PasswordBox); + core::stringw ch = L" "; + ch[0] = PasswordChar; + out->addString("PasswordChar", ch.c_str()); + out->addEnum ("HTextAlign", HAlign, GUIAlignmentNames); + out->addEnum ("VTextAlign", VAlign, GUIAlignmentNames); + out->addBool ("Writable", m_writable); + + IGUIEditBox::serializeAttributes(out,options); +} + + +//! Reads attributes of the element +void intlGUIEditBox::deserializeAttributes(io::IAttributes* in, io::SAttributeReadWriteOptions* options=0) +{ + IGUIEditBox::deserializeAttributes(in,options); + + setOverrideColor(in->getAttributeAsColor("OverrideColor")); + enableOverrideColor(in->getAttributeAsBool("OverrideColorEnabled")); + setMax(in->getAttributeAsInt("MaxChars")); + setWordWrap(in->getAttributeAsBool("WordWrap")); + setMultiLine(in->getAttributeAsBool("MultiLine")); + setAutoScroll(in->getAttributeAsBool("AutoScroll")); + core::stringw ch = in->getAttributeAsStringW("PasswordChar"); + + if (ch.empty()) + setPasswordBox(in->getAttributeAsBool("PasswordBox")); + else + setPasswordBox(in->getAttributeAsBool("PasswordBox"), ch[0]); + + setTextAlignment( (EGUI_ALIGNMENT) in->getAttributeAsEnumeration("HTextAlign", GUIAlignmentNames), + (EGUI_ALIGNMENT) in->getAttributeAsEnumeration("VTextAlign", GUIAlignmentNames)); + + setWritable(in->getAttributeAsBool("Writable")); + // setOverrideFont(in->getAttributeAsFont("OverrideFont")); +} + + +} // end namespace gui +} // end namespace irr + +#endif // _IRR_COMPILE_WITH_GUI_ diff --git a/src/gui/intlGUIEditBox.h b/src/gui/intlGUIEditBox.h new file mode 100644 index 000000000..aa35e2e71 --- /dev/null +++ b/src/gui/intlGUIEditBox.h @@ -0,0 +1,198 @@ +// Copyright (C) 2002-2013 Nikolaus Gebhardt +// This file is part of the "Irrlicht Engine". +// For conditions of distribution and use, see copyright notice in irrlicht.h + +#pragma once + +#include "IrrCompileConfig.h" +//#ifdef _IRR_COMPILE_WITH_GUI_ + +#include "IGUIEditBox.h" +#include "irrArray.h" +#include "IOSOperator.h" +#include "IGUIScrollBar.h" + +namespace irr +{ +namespace gui +{ + class intlGUIEditBox : public IGUIEditBox + { + public: + + //! constructor + intlGUIEditBox(const wchar_t* text, bool border, IGUIEnvironment* environment, + IGUIElement* parent, s32 id, const core::rect<s32>& rectangle, + bool writable = true, bool has_vscrollbar = false); + + //! destructor + virtual ~intlGUIEditBox(); + + //! Sets another skin independent font. + virtual void setOverrideFont(IGUIFont* font=0); + + //! Gets the override font (if any) + /** \return The override font (may be 0) */ + virtual IGUIFont* getOverrideFont() const; + + //! Get the font which is used right now for drawing + /** Currently this is the override font when one is set and the + font of the active skin otherwise */ + virtual IGUIFont* getActiveFont() const; + + //! Sets another color for the text. + virtual void setOverrideColor(video::SColor color); + + //! Gets the override color + virtual video::SColor getOverrideColor() const; + + //! Sets if the text should use the overide color or the + //! color in the gui skin. + virtual void enableOverrideColor(bool enable); + + //! Checks if an override color is enabled + /** \return true if the override color is enabled, false otherwise */ + virtual bool isOverrideColorEnabled(void) const; + + //! Sets whether to draw the background + virtual void setDrawBackground(bool draw); + + //! Turns the border on or off + virtual void setDrawBorder(bool border); + + //! Enables or disables word wrap for using the edit box as multiline text editor. + virtual void setWordWrap(bool enable); + + //! Checks if word wrap is enabled + //! \return true if word wrap is enabled, false otherwise + virtual bool isWordWrapEnabled() const; + + //! Enables or disables newlines. + /** \param enable: If set to true, the EGET_EDITBOX_ENTER event will not be fired, + instead a newline character will be inserted. */ + virtual void setMultiLine(bool enable); + + //! Checks if multi line editing is enabled + //! \return true if mult-line is enabled, false otherwise + virtual bool isMultiLineEnabled() const; + + //! Enables or disables automatic scrolling with cursor position + //! \param enable: If set to true, the text will move around with the cursor position + virtual void setAutoScroll(bool enable); + + //! Checks to see if automatic scrolling is enabled + //! \return true if automatic scrolling is enabled, false if not + virtual bool isAutoScrollEnabled() const; + + //! Gets the size area of the text in the edit box + //! \return Returns the size in pixels of the text + virtual core::dimension2du getTextDimension(); + + //! Sets text justification + virtual void setTextAlignment(EGUI_ALIGNMENT horizontal, EGUI_ALIGNMENT vertical); + + //! called if an event happened. + virtual bool OnEvent(const SEvent& event); + + //! draws the element and its children + virtual void draw(); + + //! Sets the new caption of this element. + virtual void setText(const wchar_t* text); + + //! Sets the maximum amount of characters which may be entered in the box. + //! \param max: Maximum amount of characters. If 0, the character amount is + //! infinity. + virtual void setMax(u32 max); + + //! Returns maximum amount of characters, previously set by setMax(); + virtual u32 getMax() const; + + //! Sets whether the edit box is a password box. Setting this to true will + /** disable MultiLine, WordWrap and the ability to copy with ctrl+c or ctrl+x + \param passwordBox: true to enable password, false to disable + \param passwordChar: the character that is displayed instead of letters */ + virtual void setPasswordBox(bool passwordBox, wchar_t passwordChar = L'*'); + + //! Returns true if the edit box is currently a password box. + virtual bool isPasswordBox() const; + + //! Updates the absolute position, splits text if required + virtual void updateAbsolutePosition(); + + //! set true if this EditBox is writable + virtual void setWritable(bool can_write_text); + + //! Writes attributes of the element. + virtual void serializeAttributes(io::IAttributes* out, io::SAttributeReadWriteOptions* options) const; + + //! Reads attributes of the element + virtual void deserializeAttributes(io::IAttributes* in, io::SAttributeReadWriteOptions* options); + + protected: + //! Breaks the single text line. + void breakText(); + //! sets the area of the given line + void setTextRect(s32 line); + //! returns the line number that the cursor is on + s32 getLineFromPos(s32 pos); + //! adds a letter to the edit box + void inputChar(wchar_t c); + //! calculates the current scroll position + void calculateScrollPos(); + //! send some gui event to parent + void sendGuiEvent(EGUI_EVENT_TYPE type); + //! set text markers + void setTextMarkers(s32 begin, s32 end); + + bool processKey(const SEvent& event); + bool processMouse(const SEvent& event); + s32 getCursorPos(s32 x, s32 y); + + //! Create a vertical scrollbar + void createVScrollBar(); + + //! Update the vertical scrollbar (visibilty & scroll position) + void updateVScrollBar(); + + bool MouseMarking = false; + bool Border; + bool OverrideColorEnabled = false; + s32 MarkBegin = 0; + s32 MarkEnd = 0; + + video::SColor OverrideColor = video::SColor(101,255,255,255); + gui::IGUIFont *OverrideFont = nullptr; + gui::IGUIFont *LastBreakFont = nullptr; + IOSOperator *Operator = nullptr; + + u64 BlinkStartTime = 0; + s32 CursorPos = 0; + s32 HScrollPos = 0; + s32 VScrollPos = 0; // scroll position in characters + u32 Max = 0; + + bool WordWrap = false; + bool MultiLine = false; + bool AutoScroll = true; + bool PasswordBox = false; + wchar_t PasswordChar = L'*'; + EGUI_ALIGNMENT HAlign = EGUIA_UPPERLEFT; + EGUI_ALIGNMENT VAlign = EGUIA_CENTER; + + core::array<core::stringw> BrokenText; + core::array<s32> BrokenTextPositions; + + core::rect<s32> CurrentTextRect = core::rect<s32>(0,0,1,1); + core::rect<s32> FrameRect; // temporary values + u32 m_scrollbar_width; + IGUIScrollBar *m_vscrollbar; + bool m_writable; + + }; + + +} // end namespace gui +} // end namespace irr + +//#endif // _IRR_COMPILE_WITH_GUI_ diff --git a/src/gui/mainmenumanager.h b/src/gui/mainmenumanager.h new file mode 100644 index 000000000..ea9327813 --- /dev/null +++ b/src/gui/mainmenumanager.h @@ -0,0 +1,166 @@ +/* +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. +*/ + +#pragma once + +/* + All kinds of stuff that needs to be exposed from main.cpp +*/ +#include "modalMenu.h" +#include <cassert> +#include <list> + +class IGameCallback +{ +public: + virtual void exitToOS() = 0; + virtual void keyConfig() = 0; + virtual void disconnect() = 0; + virtual void changePassword() = 0; + virtual void changeVolume() = 0; + + virtual void signalKeyConfigChange() = 0; +}; + +extern gui::IGUIEnvironment *guienv; +extern gui::IGUIStaticText *guiroot; + +// Handler for the modal menus + +class MainMenuManager : public IMenuManager +{ +public: + virtual void createdMenu(gui::IGUIElement *menu) + { +#ifndef NDEBUG + for (gui::IGUIElement *i : m_stack) { + assert(i != menu); + } +#endif + + if(!m_stack.empty()) + m_stack.back()->setVisible(false); + m_stack.push_back(menu); + } + + virtual void deletingMenu(gui::IGUIElement *menu) + { + // Remove all entries if there are duplicates + bool removed_entry; + do{ + removed_entry = false; + for(std::list<gui::IGUIElement*>::iterator + i = m_stack.begin(); + i != m_stack.end(); ++i) + { + if(*i == menu) + { + m_stack.erase(i); + removed_entry = true; + break; + } + } + }while(removed_entry); + + /*core::list<GUIModalMenu*>::Iterator i = m_stack.getLast(); + assert(*i == menu); + m_stack.erase(i);*/ + + if(!m_stack.empty()) + m_stack.back()->setVisible(true); + } + + // Returns true to prevent further processing + virtual bool preprocessEvent(const SEvent& event) + { + if (m_stack.empty()) + return false; + GUIModalMenu *mm = dynamic_cast<GUIModalMenu*>(m_stack.back()); + return mm && mm->preprocessEvent(event); + } + + u32 menuCount() + { + return m_stack.size(); + } + + bool pausesGame() + { + for (gui::IGUIElement *i : m_stack) { + GUIModalMenu *mm = dynamic_cast<GUIModalMenu*>(i); + if (mm && mm->pausesGame()) + return true; + } + return false; + } + + std::list<gui::IGUIElement*> m_stack; +}; + +extern MainMenuManager g_menumgr; + +extern bool isMenuActive(); + +class MainGameCallback : public IGameCallback +{ +public: + MainGameCallback() = default; + virtual ~MainGameCallback() = default; + + virtual void exitToOS() + { + shutdown_requested = true; + } + + virtual void disconnect() + { + disconnect_requested = true; + } + + virtual void changePassword() + { + changepassword_requested = true; + } + + virtual void changeVolume() + { + changevolume_requested = true; + } + + virtual void keyConfig() + { + keyconfig_requested = true; + } + + virtual void signalKeyConfigChange() + { + keyconfig_changed = true; + } + + + bool disconnect_requested = false; + bool changepassword_requested = false; + bool changevolume_requested = false; + bool keyconfig_requested = false; + bool shutdown_requested = false; + + bool keyconfig_changed = false; +}; + +extern MainGameCallback *g_gamecallback; diff --git a/src/gui/modalMenu.h b/src/gui/modalMenu.h new file mode 100644 index 000000000..f41591c3f --- /dev/null +++ b/src/gui/modalMenu.h @@ -0,0 +1,137 @@ +/* +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. +*/ + +#pragma once + +#include "irrlichttypes_extrabloated.h" +#ifdef HAVE_TOUCHSCREENGUI +#include "touchscreengui.h" +#endif + +class GUIModalMenu; + +class IMenuManager +{ +public: + // A GUIModalMenu calls these when this class is passed as a parameter + virtual void createdMenu(gui::IGUIElement *menu) = 0; + virtual void deletingMenu(gui::IGUIElement *menu) = 0; +}; + +/* + Remember to drop() the menu after creating, so that it can + remove itself when it wants to. +*/ + +class GUIModalMenu : public gui::IGUIElement +{ +public: + GUIModalMenu(gui::IGUIEnvironment* env, gui::IGUIElement* parent, s32 id, + IMenuManager *menumgr): + IGUIElement(gui::EGUIET_ELEMENT, env, parent, id, + core::rect<s32>(0,0,100,100)) + { + m_menumgr = menumgr; + + setVisible(true); + Environment->setFocus(this); + m_menumgr->createdMenu(this); + } + + virtual ~GUIModalMenu() + { + m_menumgr->deletingMenu(this); + } + + void allowFocusRemoval(bool allow) + { + m_allow_focus_removal = allow; + } + + bool canTakeFocus(gui::IGUIElement *e) + { + return (e && (e == this || isMyChild(e))) || m_allow_focus_removal; + } + + void draw() + { + if(!IsVisible) + return; + + video::IVideoDriver* driver = Environment->getVideoDriver(); + v2u32 screensize = driver->getScreenSize(); + if(screensize != m_screensize_old /*|| m_force_regenerate_gui*/) + { + m_screensize_old = screensize; + regenerateGui(screensize); + //m_force_regenerate_gui = false; + } + + drawMenu(); + } + + /* + This should be called when the menu wants to quit. + + WARNING: THIS DEALLOCATES THE MENU FROM MEMORY. Return + immediately if you call this from the menu itself. + + (More precisely, this decrements the reference count.) + */ + void quitMenu() + { + allowFocusRemoval(true); + // This removes Environment's grab on us + Environment->removeFocus(this); + m_menumgr->deletingMenu(this); + this->remove(); +#ifdef HAVE_TOUCHSCREENGUI + if (g_touchscreengui) + g_touchscreengui->show(); +#endif + } + + void removeChildren() + { + const core::list<gui::IGUIElement*> &children = getChildren(); + core::list<gui::IGUIElement*> children_copy; + for (gui::IGUIElement *i : children) { + children_copy.push_back(i); + } + + for (gui::IGUIElement *i : children_copy) { + i->remove(); + } + } + + virtual void regenerateGui(v2u32 screensize) = 0; + virtual void drawMenu() = 0; + virtual bool preprocessEvent(const SEvent& event) { return false; }; + virtual bool OnEvent(const SEvent& event) { return false; }; + virtual bool pausesGame(){ return false; } // Used for pause menu + +protected: + //bool m_force_regenerate_gui; + v2u32 m_screensize_old; +private: + IMenuManager *m_menumgr; + // This might be necessary to expose to the implementation if it + // wants to launch other menus + bool m_allow_focus_removal = false; +}; diff --git a/src/gui/touchscreengui.cpp b/src/gui/touchscreengui.cpp new file mode 100644 index 000000000..e849b4053 --- /dev/null +++ b/src/gui/touchscreengui.cpp @@ -0,0 +1,1063 @@ +/* +Copyright (C) 2014 sapier + +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 "touchscreengui.h" +#include "irrlichttypes.h" +#include "irr_v2d.h" +#include "log.h" +#include "keycode.h" +#include "settings.h" +#include "gettime.h" +#include "util/numeric.h" +#include "porting.h" +#include "guiscalingfilter.h" + +#include <iostream> +#include <algorithm> + +#include <ISceneCollisionManager.h> + +// Very slow button repeat frequency (in seconds) +#define SLOW_BUTTON_REPEAT (1.0f) + +using namespace irr::core; + +const char** touchgui_button_imagenames = (const char*[]) { + "up_arrow.png", + "down_arrow.png", + "left_arrow.png", + "right_arrow.png", + "jump_btn.png", + "down.png" +}; + +static irr::EKEY_CODE id2keycode(touch_gui_button_id id) +{ + std::string key = ""; + switch (id) { + case forward_id: + key = "forward"; + break; + case left_id: + key = "left"; + break; + case right_id: + key = "right"; + break; + case backward_id: + key = "backward"; + break; + case inventory_id: + key = "inventory"; + break; + case drop_id: + key = "drop"; + break; + case jump_id: + key = "jump"; + break; + case crunch_id: + key = "sneak"; + break; + case fly_id: + key = "freemove"; + break; + case noclip_id: + key = "noclip"; + break; + case fast_id: + key = "fastmove"; + break; + case debug_id: + key = "toggle_debug"; + break; + case chat_id: + key = "chat"; + break; + case camera_id: + key = "camera_mode"; + break; + case range_id: + key = "rangeselect"; + break; + } + assert(key != ""); + return keyname_to_keycode(g_settings->get("keymap_" + key).c_str()); +} + +TouchScreenGUI *g_touchscreengui; + +static void load_button_texture(button_info* btn, const char* path, + rect<s32> button_rect, ISimpleTextureSource* tsrc, video::IVideoDriver *driver) +{ + unsigned int tid; + video::ITexture *texture = guiScalingImageButton(driver, + tsrc->getTexture(path, &tid), button_rect.getWidth(), + button_rect.getHeight()); + if (texture) { + btn->guibutton->setUseAlphaChannel(true); + if (g_settings->getBool("gui_scaling_filter")) { + rect<s32> txr_rect = rect<s32>(0, 0, button_rect.getWidth(), button_rect.getHeight()); + btn->guibutton->setImage(texture, txr_rect); + btn->guibutton->setPressedImage(texture, txr_rect); + btn->guibutton->setScaleImage(false); + } else { + btn->guibutton->setImage(texture); + btn->guibutton->setPressedImage(texture); + btn->guibutton->setScaleImage(true); + } + btn->guibutton->setDrawBorder(false); + btn->guibutton->setText(L""); + } +} + +AutoHideButtonBar::AutoHideButtonBar(IrrlichtDevice *device, + IEventReceiver* receiver) : + m_driver(device->getVideoDriver()), + m_guienv(device->getGUIEnvironment()), + m_receiver(receiver) +{ +} + +void AutoHideButtonBar::init(ISimpleTextureSource* tsrc, + const char* starter_img, int button_id, v2s32 UpperLeft, + v2s32 LowerRight, autohide_button_bar_dir dir, float timeout) +{ + m_texturesource = tsrc; + + m_upper_left = UpperLeft; + m_lower_right = LowerRight; + + /* init settings bar */ + + irr::core::rect<int> current_button = rect<s32>(UpperLeft.X, UpperLeft.Y, + LowerRight.X, LowerRight.Y); + + m_starter.guibutton = m_guienv->addButton(current_button, 0, button_id, L"", 0); + m_starter.guibutton->grab(); + m_starter.repeatcounter = -1; + m_starter.keycode = KEY_OEM_8; // use invalid keycode as it's not relevant + m_starter.immediate_release = true; + m_starter.ids.clear(); + + load_button_texture(&m_starter, starter_img, current_button, + m_texturesource, m_driver); + + m_dir = dir; + m_timeout_value = timeout; + + m_initialized = true; +} + +AutoHideButtonBar::~AutoHideButtonBar() +{ + if (m_starter.guibutton) { + m_starter.guibutton->setVisible(false); + m_starter.guibutton->drop(); + } +} + +void AutoHideButtonBar::addButton(touch_gui_button_id button_id, + const wchar_t* caption, const char* btn_image) +{ + + if (!m_initialized) { + errorstream << "AutoHideButtonBar::addButton not yet initialized!" + << std::endl; + return; + } + int button_size = 0; + + if ((m_dir == AHBB_Dir_Top_Bottom) || (m_dir == AHBB_Dir_Bottom_Top)) { + button_size = m_lower_right.X - m_upper_left.X; + } else { + button_size = m_lower_right.Y - m_upper_left.Y; + } + + irr::core::rect<int> current_button; + + if ((m_dir == AHBB_Dir_Right_Left) || (m_dir == AHBB_Dir_Left_Right)) { + + int x_start = 0; + int x_end = 0; + + if (m_dir == AHBB_Dir_Left_Right) { + x_start = m_lower_right.X + (button_size * 1.25 * m_buttons.size()) + + (button_size * 0.25); + x_end = x_start + button_size; + } else { + x_end = m_upper_left.X - (button_size * 1.25 * m_buttons.size()) + - (button_size * 0.25); + x_start = x_end - button_size; + } + + current_button = rect<s32>(x_start, m_upper_left.Y, x_end, + m_lower_right.Y); + } else { + int y_start = 0; + int y_end = 0; + + if (m_dir == AHBB_Dir_Top_Bottom) { + y_start = m_lower_right.X + (button_size * 1.25 * m_buttons.size()) + + (button_size * 0.25); + y_end = y_start + button_size; + } else { + y_end = m_upper_left.X - (button_size * 1.25 * m_buttons.size()) + - (button_size * 0.25); + y_start = y_end - button_size; + } + + current_button = rect<s32>(m_upper_left.X, y_start, m_lower_right.Y, + y_end); + } + + button_info* btn = new button_info(); + btn->guibutton = m_guienv->addButton(current_button, 0, button_id, caption, 0); + btn->guibutton->grab(); + btn->guibutton->setVisible(false); + btn->guibutton->setEnabled(false); + btn->repeatcounter = -1; + btn->keycode = id2keycode(button_id); + btn->immediate_release = true; + btn->ids.clear(); + + load_button_texture(btn, btn_image, current_button, m_texturesource, + m_driver); + + m_buttons.push_back(btn); +} + +bool AutoHideButtonBar::isButton(const SEvent &event) +{ + IGUIElement* rootguielement = m_guienv->getRootGUIElement(); + + if (rootguielement == NULL) { + return false; + } + + gui::IGUIElement *element = rootguielement->getElementFromPoint( + core::position2d<s32>(event.TouchInput.X, event.TouchInput.Y)); + + if (element == NULL) { + return false; + } + + if (m_active) { + /* check for all buttons in vector */ + + std::vector<button_info*>::iterator iter = m_buttons.begin(); + + while (iter != m_buttons.end()) { + if ((*iter)->guibutton == element) { + + SEvent* translated = new SEvent(); + memset(translated, 0, sizeof(SEvent)); + translated->EventType = irr::EET_KEY_INPUT_EVENT; + translated->KeyInput.Key = (*iter)->keycode; + translated->KeyInput.Control = false; + translated->KeyInput.Shift = false; + translated->KeyInput.Char = 0; + + /* add this event */ + translated->KeyInput.PressedDown = true; + m_receiver->OnEvent(*translated); + + /* remove this event */ + translated->KeyInput.PressedDown = false; + m_receiver->OnEvent(*translated); + + delete translated; + + (*iter)->ids.push_back(event.TouchInput.ID); + + m_timeout = 0; + + return true; + } + ++iter; + } + } else { + /* check for starter button only */ + if (element == m_starter.guibutton) { + m_starter.ids.push_back(event.TouchInput.ID); + m_starter.guibutton->setVisible(false); + m_starter.guibutton->setEnabled(false); + m_active = true; + m_timeout = 0; + + std::vector<button_info*>::iterator iter = m_buttons.begin(); + + while (iter != m_buttons.end()) { + (*iter)->guibutton->setVisible(true); + (*iter)->guibutton->setEnabled(true); + ++iter; + } + + return true; + } + } + return false; +} + +bool AutoHideButtonBar::isReleaseButton(int eventID) +{ + std::vector<int>::iterator id = std::find(m_starter.ids.begin(), + m_starter.ids.end(), eventID); + + if (id != m_starter.ids.end()) { + m_starter.ids.erase(id); + return true; + } + + std::vector<button_info*>::iterator iter = m_buttons.begin(); + + while (iter != m_buttons.end()) { + std::vector<int>::iterator id = std::find((*iter)->ids.begin(), + (*iter)->ids.end(), eventID); + + if (id != (*iter)->ids.end()) { + (*iter)->ids.erase(id); + // TODO handle settings button release + return true; + } + ++iter; + } + + return false; +} + +void AutoHideButtonBar::step(float dtime) +{ + if (m_active) { + m_timeout += dtime; + + if (m_timeout > m_timeout_value) { + deactivate(); + } + } +} + +void AutoHideButtonBar::deactivate() +{ + if (m_visible) { + m_starter.guibutton->setVisible(true); + m_starter.guibutton->setEnabled(true); + } + m_active = false; + + std::vector<button_info*>::iterator iter = m_buttons.begin(); + + while (iter != m_buttons.end()) { + (*iter)->guibutton->setVisible(false); + (*iter)->guibutton->setEnabled(false); + ++iter; + } +} + +void AutoHideButtonBar::hide() +{ + m_visible = false; + m_starter.guibutton->setVisible(false); + m_starter.guibutton->setEnabled(false); + + std::vector<button_info*>::iterator iter = m_buttons.begin(); + + while (iter != m_buttons.end()) { + (*iter)->guibutton->setVisible(false); + (*iter)->guibutton->setEnabled(false); + ++iter; + } +} + +void AutoHideButtonBar::show() +{ + m_visible = true; + + if (m_active) { + std::vector<button_info*>::iterator iter = m_buttons.begin(); + + while (iter != m_buttons.end()) { + (*iter)->guibutton->setVisible(true); + (*iter)->guibutton->setEnabled(true); + ++iter; + } + } else { + m_starter.guibutton->setVisible(true); + m_starter.guibutton->setEnabled(true); + } +} + +TouchScreenGUI::TouchScreenGUI(IrrlichtDevice *device, IEventReceiver* receiver): + m_device(device), + m_guienv(device->getGUIEnvironment()), + m_receiver(receiver), + m_settingsbar(device, receiver), + m_rarecontrolsbar(device, receiver) +{ + for (unsigned int i=0; i < after_last_element_id; i++) { + m_buttons[i].guibutton = 0; + m_buttons[i].repeatcounter = -1; + m_buttons[i].repeatdelay = BUTTON_REPEAT_DELAY; + } + + m_screensize = m_device->getVideoDriver()->getScreenSize(); +} + +void TouchScreenGUI::initButton(touch_gui_button_id id, rect<s32> button_rect, + std::wstring caption, bool immediate_release, float repeat_delay) +{ + + button_info* btn = &m_buttons[id]; + btn->guibutton = m_guienv->addButton(button_rect, 0, id, caption.c_str()); + btn->guibutton->grab(); + btn->repeatcounter = -1; + btn->repeatdelay = repeat_delay; + btn->keycode = id2keycode(id); + btn->immediate_release = immediate_release; + btn->ids.clear(); + + load_button_texture(btn,touchgui_button_imagenames[id],button_rect, + m_texturesource, m_device->getVideoDriver()); +} + +static int getMaxControlPadSize(float density) { + return 200 * density * g_settings->getFloat("hud_scaling"); +} + +int TouchScreenGUI::getGuiButtonSize() +{ + u32 control_pad_size = MYMIN((2 * m_screensize.Y) / 3, + getMaxControlPadSize(porting::getDisplayDensity())); + + return control_pad_size / 3; +} + +void TouchScreenGUI::init(ISimpleTextureSource* tsrc) +{ + assert(tsrc != 0); + + u32 button_size = getGuiButtonSize(); + m_visible = true; + m_texturesource = tsrc; + /* + draw control pad + 0 1 2 + 3 4 5 + for now only 0, 1, 2, and 4 are used + */ + int number = 0; + for (int y = 0; y < 2; ++y) + for (int x = 0; x < 3; ++x, ++number) { + rect<s32> button_rect( + x * button_size, m_screensize.Y - button_size * (2 - y), + (x + 1) * button_size, m_screensize.Y - button_size * (1 - y) + ); + touch_gui_button_id id = after_last_element_id; + std::wstring caption; + switch (number) { + case 0: + id = left_id; + caption = L"<"; + break; + case 1: + id = forward_id; + caption = L"^"; + break; + case 2: + id = right_id; + caption = L">"; + break; + case 4: + id = backward_id; + caption = L"v"; + break; + } + if (id != after_last_element_id) { + initButton(id, button_rect, caption, false); + } + } + + /* init jump button */ + initButton(jump_id, + rect<s32>(m_screensize.X-(1.75*button_size), + m_screensize.Y - (0.5*button_size), + m_screensize.X-(0.25*button_size), + m_screensize.Y), + L"x",false); + + /* init crunch button */ + initButton(crunch_id, + rect<s32>(m_screensize.X-(3.25*button_size), + m_screensize.Y - (0.5*button_size), + m_screensize.X-(1.75*button_size), + m_screensize.Y), + L"H",false); + + m_settingsbar.init(m_texturesource, "gear_icon.png", settings_starter_id, + v2s32(m_screensize.X - (button_size / 2), + m_screensize.Y - ((SETTINGS_BAR_Y_OFFSET + 1) * button_size) + + (button_size * 0.5)), + v2s32(m_screensize.X, + m_screensize.Y - (SETTINGS_BAR_Y_OFFSET * button_size) + + (button_size * 0.5)), AHBB_Dir_Right_Left, + 3.0); + + m_settingsbar.addButton(fly_id, L"fly", "fly_btn.png"); + m_settingsbar.addButton(noclip_id, L"noclip", "noclip_btn.png"); + m_settingsbar.addButton(fast_id, L"fast", "fast_btn.png"); + m_settingsbar.addButton(debug_id, L"debug", "debug_btn.png"); + m_settingsbar.addButton(camera_id, L"camera", "camera_btn.png"); + m_settingsbar.addButton(range_id, L"rangeview", "rangeview_btn.png"); + + m_rarecontrolsbar.init(m_texturesource, "rare_controls.png", + rare_controls_starter_id, + v2s32(0, + m_screensize.Y + - ((RARE_CONTROLS_BAR_Y_OFFSET + 1) * button_size) + + (button_size * 0.5)), + v2s32(button_size / 2, + m_screensize.Y - (RARE_CONTROLS_BAR_Y_OFFSET * button_size) + + (button_size * 0.5)), AHBB_Dir_Left_Right, + 2); + + m_rarecontrolsbar.addButton(chat_id, L"Chat", "chat_btn.png"); + m_rarecontrolsbar.addButton(inventory_id, L"inv", "inventory_btn.png"); + m_rarecontrolsbar.addButton(drop_id, L"drop", "drop_btn.png"); + +} + +touch_gui_button_id TouchScreenGUI::getButtonID(s32 x, s32 y) +{ + IGUIElement* rootguielement = m_guienv->getRootGUIElement(); + + if (rootguielement != NULL) { + gui::IGUIElement *element = + rootguielement->getElementFromPoint(core::position2d<s32>(x,y)); + + if (element) { + for (unsigned int i=0; i < after_last_element_id; i++) { + if (element == m_buttons[i].guibutton) { + return (touch_gui_button_id) i; + } + } + } + } + return after_last_element_id; +} + +touch_gui_button_id TouchScreenGUI::getButtonID(int eventID) +{ + for (unsigned int i=0; i < after_last_element_id; i++) { + button_info* btn = &m_buttons[i]; + + std::vector<int>::iterator id = + std::find(btn->ids.begin(),btn->ids.end(), eventID); + + if (id != btn->ids.end()) + return (touch_gui_button_id) i; + } + + return after_last_element_id; +} + +bool TouchScreenGUI::isHUDButton(const SEvent &event) +{ + // check if hud item is pressed + for (std::map<int,rect<s32> >::iterator iter = m_hud_rects.begin(); + iter != m_hud_rects.end(); ++iter) { + if (iter->second.isPointInside( + v2s32(event.TouchInput.X, + event.TouchInput.Y) + )) { + if ( iter->first < 8) { + SEvent* translated = new SEvent(); + memset(translated,0,sizeof(SEvent)); + translated->EventType = irr::EET_KEY_INPUT_EVENT; + translated->KeyInput.Key = (irr::EKEY_CODE) (KEY_KEY_1 + iter->first); + translated->KeyInput.Control = false; + translated->KeyInput.Shift = false; + translated->KeyInput.PressedDown = true; + m_receiver->OnEvent(*translated); + m_hud_ids[event.TouchInput.ID] = translated->KeyInput.Key; + delete translated; + return true; + } + } + } + return false; +} + +bool TouchScreenGUI::isReleaseHUDButton(int eventID) +{ + std::map<int,irr::EKEY_CODE>::iterator iter = m_hud_ids.find(eventID); + + if (iter != m_hud_ids.end()) { + SEvent* translated = new SEvent(); + memset(translated,0,sizeof(SEvent)); + translated->EventType = irr::EET_KEY_INPUT_EVENT; + translated->KeyInput.Key = iter->second; + translated->KeyInput.PressedDown = false; + translated->KeyInput.Control = false; + translated->KeyInput.Shift = false; + m_receiver->OnEvent(*translated); + m_hud_ids.erase(iter); + delete translated; + return true; + } + return false; +} + +void TouchScreenGUI::handleButtonEvent(touch_gui_button_id button, + int eventID, bool action) +{ + button_info* btn = &m_buttons[button]; + SEvent* translated = new SEvent(); + memset(translated,0,sizeof(SEvent)); + translated->EventType = irr::EET_KEY_INPUT_EVENT; + translated->KeyInput.Key = btn->keycode; + translated->KeyInput.Control = false; + translated->KeyInput.Shift = false; + translated->KeyInput.Char = 0; + + /* add this event */ + if (action) { + assert(std::find(btn->ids.begin(),btn->ids.end(), eventID) == btn->ids.end()); + + btn->ids.push_back(eventID); + + if (btn->ids.size() > 1) return; + + btn->repeatcounter = 0; + translated->KeyInput.PressedDown = true; + translated->KeyInput.Key = btn->keycode; + m_receiver->OnEvent(*translated); + } + /* remove event */ + if ((!action) || (btn->immediate_release)) { + + std::vector<int>::iterator pos = + std::find(btn->ids.begin(),btn->ids.end(), eventID); + /* has to be in touch list */ + assert(pos != btn->ids.end()); + btn->ids.erase(pos); + + if (btn->ids.size() > 0) { return; } + + translated->KeyInput.PressedDown = false; + btn->repeatcounter = -1; + m_receiver->OnEvent(*translated); + } + delete translated; +} + + +void TouchScreenGUI::handleReleaseEvent(int evt_id) +{ + touch_gui_button_id button = getButtonID(evt_id); + + /* handle button events */ + if (button != after_last_element_id) { + handleButtonEvent(button, evt_id, false); + } + /* handle hud button events */ + else if (isReleaseHUDButton(evt_id)) { + /* nothing to do here */ + } else if (m_settingsbar.isReleaseButton(evt_id)) { + /* nothing to do here */ + } else if (m_rarecontrolsbar.isReleaseButton(evt_id)) { + /* nothing to do here */ + } + /* handle the point used for moving view */ + else if (evt_id == m_move_id) { + m_move_id = -1; + + /* if this pointer issued a mouse event issue symmetric release here */ + if (m_move_sent_as_mouse_event) { + SEvent* translated = new SEvent; + memset(translated,0,sizeof(SEvent)); + translated->EventType = EET_MOUSE_INPUT_EVENT; + translated->MouseInput.X = m_move_downlocation.X; + translated->MouseInput.Y = m_move_downlocation.Y; + translated->MouseInput.Shift = false; + translated->MouseInput.Control = false; + translated->MouseInput.ButtonStates = 0; + translated->MouseInput.Event = EMIE_LMOUSE_LEFT_UP; + m_receiver->OnEvent(*translated); + delete translated; + } + else { + /* do double tap detection */ + doubleTapDetection(); + } + } + else { + infostream + << "TouchScreenGUI::translateEvent released unknown button: " + << evt_id << std::endl; + } + + for (std::vector<id_status>::iterator iter = m_known_ids.begin(); + iter != m_known_ids.end(); ++iter) { + if (iter->id == evt_id) { + m_known_ids.erase(iter); + break; + } + } +} + +void TouchScreenGUI::translateEvent(const SEvent &event) +{ + if (!m_visible) { + infostream << "TouchScreenGUI::translateEvent got event but not visible?!" << std::endl; + return; + } + + if (event.EventType != EET_TOUCH_INPUT_EVENT) { + return; + } + + if (event.TouchInput.Event == ETIE_PRESSED_DOWN) { + + /* add to own copy of eventlist ... + * android would provide this information but irrlicht guys don't + * wanna design a efficient interface + */ + id_status toadd; + toadd.id = event.TouchInput.ID; + toadd.X = event.TouchInput.X; + toadd.Y = event.TouchInput.Y; + m_known_ids.push_back(toadd); + + int eventID = event.TouchInput.ID; + + touch_gui_button_id button = + getButtonID(event.TouchInput.X, event.TouchInput.Y); + + /* handle button events */ + if (button != after_last_element_id) { + handleButtonEvent(button, eventID, true); + m_settingsbar.deactivate(); + m_rarecontrolsbar.deactivate(); + } else if (isHUDButton(event)) { + m_settingsbar.deactivate(); + m_rarecontrolsbar.deactivate(); + /* already handled in isHUDButton() */ + } else if (m_settingsbar.isButton(event)) { + m_rarecontrolsbar.deactivate(); + /* already handled in isSettingsBarButton() */ + } else if (m_rarecontrolsbar.isButton(event)) { + m_settingsbar.deactivate(); + /* already handled in isSettingsBarButton() */ + } + /* handle non button events */ + else { + m_settingsbar.deactivate(); + m_rarecontrolsbar.deactivate(); + /* if we don't already have a moving point make this the moving one */ + if (m_move_id == -1) { + m_move_id = event.TouchInput.ID; + m_move_has_really_moved = false; + m_move_downtime = porting::getTimeMs(); + m_move_downlocation = v2s32(event.TouchInput.X, event.TouchInput.Y); + m_move_sent_as_mouse_event = false; + } + } + + m_pointerpos[event.TouchInput.ID] = v2s32(event.TouchInput.X, event.TouchInput.Y); + } + else if (event.TouchInput.Event == ETIE_LEFT_UP) { + verbosestream << "Up event for pointerid: " << event.TouchInput.ID << std::endl; + handleReleaseEvent(event.TouchInput.ID); + } + else { + assert(event.TouchInput.Event == ETIE_MOVED); + int move_idx = event.TouchInput.ID; + + if (m_pointerpos[event.TouchInput.ID] == + v2s32(event.TouchInput.X, event.TouchInput.Y)) { + return; + } + + if (m_move_id != -1) { + if ((event.TouchInput.ID == m_move_id) && + (!m_move_sent_as_mouse_event)) { + + double distance = sqrt( + (m_pointerpos[event.TouchInput.ID].X - event.TouchInput.X) * + (m_pointerpos[event.TouchInput.ID].X - event.TouchInput.X) + + (m_pointerpos[event.TouchInput.ID].Y - event.TouchInput.Y) * + (m_pointerpos[event.TouchInput.ID].Y - event.TouchInput.Y)); + + if ((distance > g_settings->getU16("touchscreen_threshold")) || + (m_move_has_really_moved)) { + m_move_has_really_moved = true; + s32 X = event.TouchInput.X; + s32 Y = event.TouchInput.Y; + + // update camera_yaw and camera_pitch + s32 dx = X - m_pointerpos[event.TouchInput.ID].X; + s32 dy = Y - m_pointerpos[event.TouchInput.ID].Y; + + /* adapt to similar behaviour as pc screen */ + double d = g_settings->getFloat("mouse_sensitivity") *4; + double old_yaw = m_camera_yaw_change; + double old_pitch = m_camera_pitch; + + m_camera_yaw_change -= dx * d; + m_camera_pitch = MYMIN(MYMAX(m_camera_pitch + (dy * d), -180), 180); + + // update shootline + m_shootline = m_device + ->getSceneManager() + ->getSceneCollisionManager() + ->getRayFromScreenCoordinates(v2s32(X, Y)); + m_pointerpos[event.TouchInput.ID] = v2s32(X, Y); + } + } + else if ((event.TouchInput.ID == m_move_id) && + (m_move_sent_as_mouse_event)) { + m_shootline = m_device + ->getSceneManager() + ->getSceneCollisionManager() + ->getRayFromScreenCoordinates( + v2s32(event.TouchInput.X,event.TouchInput.Y)); + } + } else { + handleChangedButton(event); + } + } +} + +void TouchScreenGUI::handleChangedButton(const SEvent &event) +{ + for (unsigned int i = 0; i < after_last_element_id; i++) { + + if (m_buttons[i].ids.empty()) { + continue; + } + for (std::vector<int>::iterator iter = m_buttons[i].ids.begin(); + iter != m_buttons[i].ids.end(); ++iter) { + + if (event.TouchInput.ID == *iter) { + + int current_button_id = + getButtonID(event.TouchInput.X, event.TouchInput.Y); + + if (current_button_id == i) { + continue; + } + + /* remove old button */ + handleButtonEvent((touch_gui_button_id) i,*iter,false); + + if (current_button_id == after_last_element_id) { + return; + } + handleButtonEvent((touch_gui_button_id) current_button_id,*iter,true); + return; + + } + } + } + + int current_button_id = getButtonID(event.TouchInput.X, event.TouchInput.Y); + + if (current_button_id == after_last_element_id) { + return; + } + + button_info* btn = &m_buttons[current_button_id]; + if (std::find(btn->ids.begin(),btn->ids.end(), event.TouchInput.ID) + == btn->ids.end()) + { + handleButtonEvent((touch_gui_button_id) current_button_id, + event.TouchInput.ID, true); + } + +} + +bool TouchScreenGUI::doubleTapDetection() +{ + m_key_events[0].down_time = m_key_events[1].down_time; + m_key_events[0].x = m_key_events[1].x; + m_key_events[0].y = m_key_events[1].y; + m_key_events[1].down_time = m_move_downtime; + m_key_events[1].x = m_move_downlocation.X; + m_key_events[1].y = m_move_downlocation.Y; + + u64 delta = porting::getDeltaMs(m_key_events[0].down_time, porting::getTimeMs()); + if (delta > 400) + return false; + + double distance = sqrt( + (m_key_events[0].x - m_key_events[1].x) * (m_key_events[0].x - m_key_events[1].x) + + (m_key_events[0].y - m_key_events[1].y) * (m_key_events[0].y - m_key_events[1].y)); + + + if (distance > (20 + g_settings->getU16("touchscreen_threshold"))) + return false; + + SEvent* translated = new SEvent(); + memset(translated, 0, sizeof(SEvent)); + translated->EventType = EET_MOUSE_INPUT_EVENT; + translated->MouseInput.X = m_key_events[0].x; + translated->MouseInput.Y = m_key_events[0].y; + translated->MouseInput.Shift = false; + translated->MouseInput.Control = false; + translated->MouseInput.ButtonStates = EMBSM_RIGHT; + + // update shootline + m_shootline = m_device + ->getSceneManager() + ->getSceneCollisionManager() + ->getRayFromScreenCoordinates(v2s32(m_key_events[0].x, m_key_events[0].y)); + + translated->MouseInput.Event = EMIE_RMOUSE_PRESSED_DOWN; + verbosestream << "TouchScreenGUI::translateEvent right click press" << std::endl; + m_receiver->OnEvent(*translated); + + translated->MouseInput.ButtonStates = 0; + translated->MouseInput.Event = EMIE_RMOUSE_LEFT_UP; + verbosestream << "TouchScreenGUI::translateEvent right click release" << std::endl; + m_receiver->OnEvent(*translated); + delete translated; + return true; + +} + +TouchScreenGUI::~TouchScreenGUI() +{ + for (unsigned int i = 0; i < after_last_element_id; i++) { + button_info* btn = &m_buttons[i]; + if (btn->guibutton != 0) { + btn->guibutton->drop(); + btn->guibutton = NULL; + } + } +} + +void TouchScreenGUI::step(float dtime) +{ + /* simulate keyboard repeats */ + for (unsigned int i = 0; i < after_last_element_id; i++) { + button_info* btn = &m_buttons[i]; + + if (btn->ids.size() > 0) { + btn->repeatcounter += dtime; + + /* in case we're moving around digging does not happen */ + if (m_move_id != -1) + m_move_has_really_moved = true; + + if (btn->repeatcounter < btn->repeatdelay) continue; + + btn->repeatcounter = 0; + SEvent translated; + memset(&translated, 0, sizeof(SEvent)); + translated.EventType = irr::EET_KEY_INPUT_EVENT; + translated.KeyInput.Key = btn->keycode; + translated.KeyInput.PressedDown = false; + m_receiver->OnEvent(translated); + + translated.KeyInput.PressedDown = true; + m_receiver->OnEvent(translated); + } + } + + /* if a new placed pointer isn't moved for some time start digging */ + if ((m_move_id != -1) && + (!m_move_has_really_moved) && + (!m_move_sent_as_mouse_event)) { + + u64 delta = porting::getDeltaMs(m_move_downtime, porting::getTimeMs()); + + if (delta > MIN_DIG_TIME_MS) { + m_shootline = m_device + ->getSceneManager() + ->getSceneCollisionManager() + ->getRayFromScreenCoordinates( + v2s32(m_move_downlocation.X,m_move_downlocation.Y)); + + SEvent translated; + memset(&translated, 0, sizeof(SEvent)); + translated.EventType = EET_MOUSE_INPUT_EVENT; + translated.MouseInput.X = m_move_downlocation.X; + translated.MouseInput.Y = m_move_downlocation.Y; + translated.MouseInput.Shift = false; + translated.MouseInput.Control = false; + translated.MouseInput.ButtonStates = EMBSM_LEFT; + translated.MouseInput.Event = EMIE_LMOUSE_PRESSED_DOWN; + verbosestream << "TouchScreenGUI::step left click press" << std::endl; + m_receiver->OnEvent(translated); + m_move_sent_as_mouse_event = true; + } + } + + m_settingsbar.step(dtime); + m_rarecontrolsbar.step(dtime); +} + +void TouchScreenGUI::resetHud() +{ + m_hud_rects.clear(); +} + +void TouchScreenGUI::registerHudItem(int index, const rect<s32> &rect) +{ + m_hud_rects[index] = rect; +} + +void TouchScreenGUI::Toggle(bool visible) +{ + m_visible = visible; + for (unsigned int i = 0; i < after_last_element_id; i++) { + button_info* btn = &m_buttons[i]; + if (btn->guibutton != 0) { + btn->guibutton->setVisible(visible); + } + } + + /* clear all active buttons */ + if (!visible) { + while (m_known_ids.size() > 0) { + handleReleaseEvent(m_known_ids.begin()->id); + } + + m_settingsbar.hide(); + m_rarecontrolsbar.hide(); + } else { + m_settingsbar.show(); + m_rarecontrolsbar.show(); + } +} + +void TouchScreenGUI::hide() +{ + if (!m_visible) + return; + + Toggle(false); +} + +void TouchScreenGUI::show() +{ + if (m_visible) + return; + + Toggle(true); +} diff --git a/src/gui/touchscreengui.h b/src/gui/touchscreengui.h new file mode 100644 index 000000000..da97381cd --- /dev/null +++ b/src/gui/touchscreengui.h @@ -0,0 +1,267 @@ +/* +Copyright (C) 2014 sapier + +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. +*/ + +#pragma once + +#include <IEventReceiver.h> +#include <IGUIButton.h> +#include <IGUIEnvironment.h> + +#include <map> +#include <vector> + +#include "client/tile.h" +#include "game.h" + +using namespace irr; +using namespace irr::core; +using namespace irr::gui; + +typedef enum { + forward_id = 0, + backward_id, + left_id, + right_id, + jump_id, + crunch_id, + after_last_element_id, + settings_starter_id, + rare_controls_starter_id, + fly_id, + noclip_id, + fast_id, + debug_id, + camera_id, + range_id, + chat_id, + inventory_id, + drop_id +} touch_gui_button_id; + +typedef enum { + AHBB_Dir_Top_Bottom, + AHBB_Dir_Bottom_Top, + AHBB_Dir_Left_Right, + AHBB_Dir_Right_Left +} autohide_button_bar_dir; + +#define MIN_DIG_TIME_MS 500 +#define MAX_TOUCH_COUNT 64 +#define BUTTON_REPEAT_DELAY 0.2f + +#define SETTINGS_BAR_Y_OFFSET 6.5 +#define RARE_CONTROLS_BAR_Y_OFFSET 4 + +extern const char **touchgui_button_imagenames; + +struct button_info +{ + float repeatcounter; + float repeatdelay; + irr::EKEY_CODE keycode; + std::vector<int> ids; + IGUIButton *guibutton = nullptr; + bool immediate_release; +}; + +class AutoHideButtonBar +{ +public: + AutoHideButtonBar(IrrlichtDevice *device, IEventReceiver *receiver); + + void init(ISimpleTextureSource *tsrc, const char *starter_img, int button_id, + v2s32 UpperLeft, v2s32 LowerRight, autohide_button_bar_dir dir, + float timeout); + + ~AutoHideButtonBar(); + + /* add button to be shown */ + void addButton(touch_gui_button_id id, const wchar_t *caption, + const char *btn_image); + + /* detect settings bar button events */ + bool isButton(const SEvent &event); + + /* handle released hud buttons */ + bool isReleaseButton(int eventID); + + /* step handler */ + void step(float dtime); + + /* deactivate button bar */ + void deactivate(); + + /* hide the whole buttonbar */ + void hide(); + + /* unhide the buttonbar */ + void show(); + +private: + ISimpleTextureSource *m_texturesource = nullptr; + irr::video::IVideoDriver *m_driver; + IGUIEnvironment *m_guienv; + IEventReceiver *m_receiver; + button_info m_starter; + std::vector<button_info *> m_buttons; + + v2s32 m_upper_left; + v2s32 m_lower_right; + + /* show settings bar */ + bool m_active = false; + + bool m_visible = true; + + /* settings bar timeout */ + float m_timeout = 0.0f; + float m_timeout_value = 3.0f; + bool m_initialized = false; + autohide_button_bar_dir m_dir = AHBB_Dir_Right_Left; +}; + +class TouchScreenGUI +{ +public: + TouchScreenGUI(IrrlichtDevice *device, IEventReceiver *receiver); + ~TouchScreenGUI(); + + void translateEvent(const SEvent &event); + + void init(ISimpleTextureSource *tsrc); + + double getYawChange() + { + double res = m_camera_yaw_change; + m_camera_yaw_change = 0; + return res; + } + + double getPitch() { return m_camera_pitch; } + + /*! + * Returns a line which describes what the player is pointing at. + * The starting point and looking direction are significant, + * the line should be scaled to match its length to the actual distance + * the player can reach. + * The line starts at the camera and ends on the camera's far plane. + * The coordinates do not contain the camera offset. + */ + line3d<f32> getShootline() { return m_shootline; } + + void step(float dtime); + void resetHud(); + void registerHudItem(int index, const rect<s32> &rect); + void Toggle(bool visible); + + void hide(); + void show(); + +private: + IrrlichtDevice *m_device; + IGUIEnvironment *m_guienv; + IEventReceiver *m_receiver; + ISimpleTextureSource *m_texturesource; + v2u32 m_screensize; + std::map<int, rect<s32>> m_hud_rects; + std::map<int, irr::EKEY_CODE> m_hud_ids; + bool m_visible; // is the gui visible + + /* value in degree */ + double m_camera_yaw_change = 0.0; + double m_camera_pitch = 0.0; + + /*! + * A line starting at the camera and pointing towards the + * selected object. + * The line ends on the camera's far plane. + * The coordinates do not contain the camera offset. + */ + line3d<f32> m_shootline; + + int m_move_id = -1; + bool m_move_has_really_moved = false; + s64 m_move_downtime = 0; + bool m_move_sent_as_mouse_event = false; + v2s32 m_move_downlocation = v2s32(-10000, -10000); + + button_info m_buttons[after_last_element_id]; + + /* gui button detection */ + touch_gui_button_id getButtonID(s32 x, s32 y); + + /* gui button by eventID */ + touch_gui_button_id getButtonID(int eventID); + + /* check if a button has changed */ + void handleChangedButton(const SEvent &event); + + /* initialize a button */ + void initButton(touch_gui_button_id id, rect<s32> button_rect, + std::wstring caption, bool immediate_release, + float repeat_delay = BUTTON_REPEAT_DELAY); + + struct id_status + { + int id; + int X; + int Y; + }; + + /* vector to store known ids and their initial touch positions*/ + std::vector<id_status> m_known_ids; + + /* handle a button event */ + void handleButtonEvent(touch_gui_button_id bID, int eventID, bool action); + + /* handle pressed hud buttons */ + bool isHUDButton(const SEvent &event); + + /* handle released hud buttons */ + bool isReleaseHUDButton(int eventID); + + /* handle double taps */ + bool doubleTapDetection(); + + /* handle release event */ + void handleReleaseEvent(int evt_id); + + /* get size of regular gui control button */ + int getGuiButtonSize(); + + /* doubleclick detection variables */ + struct key_event + { + unsigned int down_time; + s32 x; + s32 y; + }; + + /* array for saving last known position of a pointer */ + v2s32 m_pointerpos[MAX_TOUCH_COUNT]; + + /* array for doubletap detection */ + key_event m_key_events[2]; + + /* settings bar */ + AutoHideButtonBar m_settingsbar; + + /* rare controls bar */ + AutoHideButtonBar m_rarecontrolsbar; +}; +extern TouchScreenGUI *g_touchscreengui; |