diff --git a/src/chat.cpp b/src/chat.cpp index df65117f7..6da5a7585 100644 --- a/src/chat.cpp +++ b/src/chat.cpp @@ -35,6 +35,7 @@ ChatBuffer::ChatBuffer(u32 scrollback): if (m_scrollback == 0) m_scrollback = 1; m_empty_formatted_line.first = true; + m_empty_formatted_line.line_index = 0; } void ChatBuffer::addLine(const std::wstring &name, const std::wstring &text) @@ -47,7 +48,7 @@ void ChatBuffer::addLine(const std::wstring &name, const std::wstring &text) if (m_rows > 0) { // m_formatted is valid and must be kept valid bool scrolled_at_bottom = (m_scroll == getBottomScrollPos()); - u32 num_added = formatChatLine(line, m_cols, m_formatted); + u32 num_added = formatChatLine(line, m_unformatted.size() - 1, m_cols, m_formatted); if (scrolled_at_bottom) m_scroll += num_added; } @@ -172,7 +173,7 @@ void ChatBuffer::reformat(u32 cols, u32 rows) { if (i == restore_scroll_unformatted) restore_scroll_formatted = m_formatted.size(); - formatChatLine(m_unformatted[i], cols, m_formatted); + formatChatLine(m_unformatted[i], i, cols, m_formatted); } } @@ -223,7 +224,7 @@ void ChatBuffer::scrollBottom() m_scroll = getBottomScrollPos(); } -u32 ChatBuffer::formatChatLine(const ChatLine& line, u32 cols, +u32 ChatBuffer::formatChatLine(const ChatLine& line, int line_index, u32 cols, std::vector& destination) const { u32 num_added = 0; @@ -266,6 +267,7 @@ u32 ChatBuffer::formatChatLine(const ChatLine& line, u32 cols, //EnrichedString line_text(line.text); next_line.first = true; + next_line.line_index = line_index; bool text_processing = false; // Produce fragments and layout them into lines diff --git a/src/chat.h b/src/chat.h index 3aad935b8..d742d6b8c 100644 --- a/src/chat.h +++ b/src/chat.h @@ -67,6 +67,8 @@ struct ChatFormattedLine std::vector fragments; // true if first line of one formatted ChatLine bool first; + // Line index in ChatLine buffer + int line_index; }; class ChatBuffer @@ -111,6 +113,9 @@ public: // Scroll to top of buffer (oldest) void scrollTop(); + s32 getScrollPos() { return m_scroll; } + u32 getColsCount() { return m_cols; } + // Functions for keeping track of whether the lines were modified by any // preceding operations // If they were not changed, getLineCount() and getLine() output the same as @@ -121,11 +126,11 @@ public: // Format a chat line for the given number of columns. // Appends the formatted lines to the destination array and // returns the number of formatted lines. - u32 formatChatLine(const ChatLine& line, u32 cols, + u32 formatChatLine(const ChatLine& line, int line_index, u32 cols, std::vector& destination) const; void resize(u32 scrollback); -protected: + s32 getTopScrollPos() const; s32 getBottomScrollPos() const; diff --git a/src/client/game.cpp b/src/client/game.cpp index de73ec1cf..195614fa5 100644 --- a/src/client/game.cpp +++ b/src/client/game.cpp @@ -954,10 +954,6 @@ private: #ifdef HAVE_TOUCHSCREENGUI bool m_cache_touchtarget; #endif - -#if defined(__ANDROID__) || defined(__IOS__) - bool m_android_chat_open = false; -#endif }; Game::Game() : @@ -1885,8 +1881,8 @@ void Game::processUserInput(f32 dtime) } #endif - if (!guienv->hasFocus(gui_chat_console) && gui_chat_console->isOpen()) { - gui_chat_console->closeConsoleAtOnce(); + if (!gui_chat_console->hasFocus() && gui_chat_console->isOpen()) { + gui_chat_console->closeConsole(); } // Input handler step() (used by the random input generator) @@ -1924,7 +1920,7 @@ void Game::processKeyInput() openInventory(); } else if (input->cancelPressed()) { #if defined(__ANDROID__) || defined(__IOS__) - m_android_chat_open = false; + gui_chat_console->setAndroidChatOpen(false); #endif if (!gui_chat_console->isOpenInhibited()) { showPauseMenu(); @@ -2137,11 +2133,20 @@ void Game::openConsole(float scale, const wchar_t *line) { assert(scale > 0.0f && scale <= 1.0f); + if (gui_chat_console->getAndroidChatOpen()) + return; + #if defined(__ANDROID__) || defined(__IOS__) if (!porting::hasRealKeyboard()) { porting::showInputDialog("", "", 2); - m_android_chat_open = true; - } else { + gui_chat_console->setAndroidChatOpen(true); + } +#endif +#if defined(__ANDROID__) + return; +#elif defined(__IOS__) + if (!g_settings->getBool("device_is_tablet")) + return; #endif if (gui_chat_console->isOpenInhibited()) return; @@ -2150,18 +2155,19 @@ void Game::openConsole(float scale, const wchar_t *line) gui_chat_console->setCloseOnEnter(true); gui_chat_console->replaceAndAddToHistory(line); } -#if defined(__ANDROID__) || defined(__IOS__) - } -#endif } #if defined(__ANDROID__) || defined(__IOS__) void Game::handleAndroidChatInput() { - if (m_android_chat_open && porting::getInputDialogState() == 0) { + if (gui_chat_console->getAndroidChatOpen() && + porting::getInputDialogState() == 0) { std::string text = porting::getInputDialogValue(); client->typeChatMessage(utf8_to_wide(text)); - m_android_chat_open = false; + gui_chat_console->setAndroidChatOpen(false); + if (!text.empty() && gui_chat_console->isOpen()) { + gui_chat_console->closeConsole(); + } } } #endif diff --git a/src/client/inputhandler.cpp b/src/client/inputhandler.cpp index e6ab0869d..6c3eebc6c 100644 --- a/src/client/inputhandler.cpp +++ b/src/client/inputhandler.cpp @@ -20,6 +20,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "util/numeric.h" #include "inputhandler.h" +#include "gui/guiChatConsole.h" #include "gui/mainmenumanager.h" #include "hud.h" @@ -115,6 +116,13 @@ bool MyEventReceiver::OnEvent(const SEvent &event) } #endif + GUIChatConsole* chat_console = GUIChatConsole::getChatConsole(); + if (chat_console && chat_console->isOpen()) { + bool result = chat_console->preprocessEvent(event); + if (result) + return true; + } + /* React to nothing here if a menu is active */ diff --git a/src/gui/guiChatConsole.cpp b/src/gui/guiChatConsole.cpp index 71e754957..27617d5a8 100644 --- a/src/gui/guiChatConsole.cpp +++ b/src/gui/guiChatConsole.cpp @@ -45,6 +45,7 @@ inline u32 clamp_u8(s32 value) return (u32) MYMIN(MYMAX(value, 0), 255); } +GUIChatConsole* GUIChatConsole::m_chat_console = nullptr; GUIChatConsole::GUIChatConsole( gui::IGUIEnvironment* env, @@ -61,6 +62,8 @@ GUIChatConsole::GUIChatConsole( m_menumgr(menumgr), m_animate_time_old(porting::getTimeMs()) { + m_chat_console = this; + // load background settings s32 console_alpha = g_settings->getS32("console_alpha"); m_background_color.setAlpha(clamp_u8(console_alpha)); @@ -93,12 +96,19 @@ GUIChatConsole::GUIChatConsole( m_fontsize.X = MYMAX(m_fontsize.X, 1); m_fontsize.Y = MYMAX(m_fontsize.Y, 1); + createVScrollBar(); + // set default cursor options setCursor(true, true, 2.0, 0.1); } GUIChatConsole::~GUIChatConsole() { + m_chat_console = nullptr; + + removeChild(m_vscrollbar); + delete m_vscrollbar; + #ifdef _IRR_COMPILE_WITH_SDL_DEVICE_ if (porting::hasRealKeyboard() && SDL_IsTextInputActive()) SDL_StopTextInput(); @@ -114,10 +124,17 @@ void GUIChatConsole::openConsole(f32 scale) m_open = true; m_desired_height_fraction = scale; + + if (g_settings->getU32("fps_max") < 60) { + m_desired_height_fraction *= m_screensize.Y; + m_height = m_desired_height_fraction; + } + m_desired_height = scale * m_screensize.Y; reformatConsole(); m_animate_time_old = porting::getTimeMs(); IGUIElement::setVisible(true); + m_vscrollbar->setVisible(true); Environment->setFocus(this); m_menumgr->createdMenu(this); @@ -143,17 +160,16 @@ void GUIChatConsole::closeConsole() Environment->removeFocus(this); m_menumgr->deletingMenu(this); + if (g_settings->getU32("fps_max") < 60) { + m_height = 0; + recalculateConsolePosition(); + } + #ifdef _IRR_COMPILE_WITH_SDL_DEVICE_ if (porting::hasRealKeyboard() && SDL_IsTextInputActive()) SDL_StopTextInput(); #endif -} -void GUIChatConsole::closeConsoleAtOnce() -{ - closeConsole(); - m_height = 0; - recalculateConsolePosition(); #ifdef HAVE_TOUCHSCREENGUI if (g_touchscreengui && g_touchscreengui->isActive()) g_touchscreengui->show(); @@ -197,6 +213,8 @@ void GUIChatConsole::draw() if(!IsVisible) return; + updateVScrollBar(); + video::IVideoDriver* driver = Environment->getVideoDriver(); // Check screen size @@ -230,19 +248,24 @@ void GUIChatConsole::draw() void GUIChatConsole::reformatConsole() { - s32 cols = m_screensize.X / m_fontsize.X - 2; // make room for a margin (looks better) + s32 cols = (m_screensize.X - m_scrollbar_width) / 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); + + updateVScrollBar(); } void GUIChatConsole::recalculateConsolePosition() { core::rect rect(0, 0, m_screensize.X, m_height); DesiredRect = rect; - recalculateAbsolutePosition(false); + recalculateAbsolutePosition(true); + + irr::core::rect scrollbarrect(m_screensize.X - m_scrollbar_width, 0, m_screensize.X, m_height); + m_vscrollbar->setRelativePosition(scrollbarrect); } void GUIChatConsole::animate(u32 msec) @@ -253,8 +276,10 @@ void GUIChatConsole::animate(u32 msec) // 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) + if (!m_open && m_height == 0 && m_open_inhibited == 0) { + m_vscrollbar->setVisible(false); IGUIElement::setVisible(false); + } if (m_height != goal) { @@ -338,6 +363,55 @@ void GUIChatConsole::drawText() if (y + line_height < 0) continue; + s32 scroll_pos = buf.getScrollPos(); + ChatSelection real_mark_begin = m_mark_end > m_mark_begin ? m_mark_begin : m_mark_end; + ChatSelection real_mark_end = m_mark_end > m_mark_begin ? m_mark_end : m_mark_begin; + if (real_mark_begin != real_mark_end && + (s32)row + scroll_pos >= real_mark_begin.row + real_mark_begin.scroll && + (s32)row + scroll_pos <= real_mark_end.row + real_mark_end.scroll) { + ChatFormattedFragment fragment_first = line.fragments[0]; + + if ((s32)row + scroll_pos == real_mark_begin.row + real_mark_begin.scroll && + real_mark_begin.fragment < line.fragments.size()) { + fragment_first = line.fragments[real_mark_begin.fragment]; + } + + ChatFormattedFragment fragment_last = line.fragments[line.fragments.size() - 1]; + + if ((s32)row + scroll_pos == real_mark_end.row + real_mark_end.scroll && + real_mark_end.fragment < line.fragments.size()) { + fragment_last = line.fragments[real_mark_end.fragment]; + } + + s32 x_begin = (fragment_first.column + 1) * m_fontsize.X; + s32 text_size = m_font->getDimension(fragment_last.text.c_str()).Width; + s32 x_end = (fragment_last.column + 1) * m_fontsize.X + text_size; + + if ((s32)row + scroll_pos == real_mark_begin.row + real_mark_begin.scroll) { + irr::core::stringw text = fragment_first.text.c_str(); + text = text.subString(0, real_mark_begin.character); + s32 text_size = m_font->getDimension(text.c_str()).Width; + x_begin = (fragment_first.column + 1) * m_fontsize.X + text_size; + + if (real_mark_begin.x_max) + x_begin = x_end; + } + + if ((s32)row + scroll_pos == real_mark_end.row + real_mark_end.scroll && + (real_mark_end.character < fragment_last.text.size()) && + !real_mark_end.x_max) { + irr::core::stringw text = fragment_last.text.c_str(); + text = text.subString(0, real_mark_end.character); + s32 text_size = m_font->getDimension(text.c_str()).Width; + x_end = (fragment_last.column + 1) * m_fontsize.X + text_size; + } + + core::rect destrect(x_begin, y, x_end, y + m_fontsize.Y); + video::IVideoDriver* driver = Environment->getVideoDriver(); + IGUISkin* skin = Environment->getSkin(); + driver->draw2DRectangle(skin->getColor(EGDC_HIGH_LIGHT), destrect, &AbsoluteClippingRect); + } + for (const ChatFormattedFragment &fragment : line.fragments) { s32 x = (fragment.column + 1) * m_fontsize.X; core::rect destrect( @@ -423,9 +497,166 @@ void GUIChatConsole::drawPrompt() } + +ChatSelection GUIChatConsole::getCursorPos(s32 x, s32 y) +{ + ChatSelection selection; + + if (m_font == NULL) + return selection; + + ChatBuffer& buf = m_chat_backend->getConsoleBuffer(); + selection.scroll = buf.getScrollPos(); + selection.initialized = true; + + s32 line_height = m_fontsize.Y; + s32 y_min = m_height - m_desired_height; + s32 y_max = buf.getRows() * line_height + y_min; + + if (y <= y_min) { + selection.row = 0; + } else if (y >= y_max) { + selection.row = buf.getRows() - 1; + } else { + for (u32 row = 0; row < buf.getRows(); row++) { + s32 y1 = row * line_height + m_height - m_desired_height; + s32 y2 = y1 + line_height; + + if (y1 + line_height < 0) + return selection; + + if (y >= y1 && y <= y2) { + selection.row = row; + break; + } + } + } + + ChatFormattedLine line = buf.getFormattedLine(selection.row); + selection.row_buf = line.line_index; + int current_row = selection.row; + + while (!line.first) { + current_row--; + line = buf.getFormattedLine(current_row); + selection.line++; + } + + line = buf.getFormattedLine(selection.row); + + if (line.fragments.empty()) + return selection; + + const ChatFormattedFragment &fragment_first = line.fragments[0]; + const ChatFormattedFragment &fragment_last = line.fragments[line.fragments.size() - 1]; + s32 x_min = (fragment_first.column + 1) * m_fontsize.X; + s32 text_size = m_font->getDimension(fragment_last.text.c_str()).Width; + s32 x_max = (fragment_last.column + 1) * m_fontsize.X + text_size; + + if (x < x_min) { + x = x_min; + } else if (x > x_max) { + x = x_max; + selection.x_max = true; + } + + for (unsigned int i = 0; i < line.fragments.size(); i++) { + const ChatFormattedFragment &fragment = line.fragments[i]; + s32 fragment_x = (fragment.column + 1) * m_fontsize.X; + + if (x < fragment_x) + continue; + + if (i < line.fragments.size() - 1) { + const ChatFormattedFragment &fragment_next = line.fragments[i + 1]; + s32 fragment_next_x = (fragment_next.column + 1) * m_fontsize.X; + + if (x >= fragment_next_x) + continue; + } + + s32 index = m_font->getCharacterFromPos(fragment.text.c_str(), x - fragment_x); + + selection.fragment = i; + selection.character = index > -1 ? index : fragment.text.size() - 1; + return selection; + } + + return selection; +} + +irr::core::stringc GUIChatConsole::getSelectedText() +{ + if (m_font == NULL) + return ""; + + if (m_mark_begin == m_mark_end) + return ""; + + bool add_to_string = false; + irr::core::stringw text = ""; + + ChatSelection real_mark_begin = m_mark_end > m_mark_begin ? m_mark_begin : m_mark_end; + ChatSelection real_mark_end = m_mark_end > m_mark_begin ? m_mark_end : m_mark_begin; + + ChatBuffer& buf = m_chat_backend->getConsoleBuffer(); + + for (int row = real_mark_begin.row_buf; row < real_mark_end.row_buf + 1; row++) { + const ChatLine& line = buf.getLine(row); + + std::vector formatted_lines; + buf.formatChatLine(line, 0, buf.getColsCount(), formatted_lines); + + for (unsigned int i = 0; i < formatted_lines.size(); i++) { + const ChatFormattedLine &line = formatted_lines[i]; + + for (unsigned int j = 0; j < line.fragments.size(); j++) { + const ChatFormattedFragment &fragment = line.fragments[j]; + + for (unsigned int k = 0; k < fragment.text.size(); k++) { + if (!add_to_string && + row == real_mark_begin.row_buf && + i == real_mark_begin.line && + j == real_mark_begin.fragment && + k == real_mark_begin.character) { + add_to_string = true; + + if (real_mark_begin.x_max) + continue; + } + + if (add_to_string) { + if (row == real_mark_end.row_buf && + i == real_mark_end.line && + j == real_mark_end.fragment && + k == real_mark_end.character) { + if (real_mark_end.x_max) + text += fragment.text.c_str()[k]; + + irr::core::stringc text_c; + text_c = wide_to_utf8(text.c_str()).c_str(); + return text_c; + } + + text += fragment.text.c_str()[k]; + } + } + } + + if (row < real_mark_end.row_buf) { + text += L"\n"; + } + } + } + + irr::core::stringc text_c; + text_c = wide_to_utf8(text.c_str()).c_str(); + return text_c; +} + + bool GUIChatConsole::OnEvent(const SEvent& event) { - ChatPrompt &prompt = m_chat_backend->getPrompt(); if(event.EventType == EET_KEY_INPUT_EVENT && event.KeyInput.PressedDown) @@ -441,7 +672,7 @@ bool GUIChatConsole::OnEvent(const SEvent& event) } if (event.KeyInput.Key == KEY_ESCAPE || event.KeyInput.Key == KEY_CANCEL) { - closeConsoleAtOnce(); + closeConsole(); 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" @@ -449,11 +680,17 @@ bool GUIChatConsole::OnEvent(const SEvent& event) } else if(event.KeyInput.Key == KEY_PRIOR) { + ChatBuffer& buf = m_chat_backend->getConsoleBuffer(); + s32 rows = -(s32)buf.getRows(); + m_vscrollbar->setPos(m_vscrollbar->getPos() + rows); m_chat_backend->scrollPageUp(); return true; } else if(event.KeyInput.Key == KEY_NEXT) { + ChatBuffer& buf = m_chat_backend->getConsoleBuffer(); + s32 rows = buf.getRows(); + m_vscrollbar->setPos(m_vscrollbar->getPos() + rows); m_chat_backend->scrollPageDown(); return true; } @@ -463,7 +700,7 @@ bool GUIChatConsole::OnEvent(const SEvent& event) std::wstring text = prompt.replace(L""); m_client->typeChatMessage(text); if (m_close_on_enter) { - closeConsoleAtOnce(); + closeConsole(); m_close_on_enter = false; } return true; @@ -558,6 +795,12 @@ bool GUIChatConsole::OnEvent(const SEvent& event) } else if(event.KeyInput.Key == KEY_KEY_C && event.KeyInput.Control) { + if (m_mark_begin != m_mark_end) { + irr::core::stringc text = getSelectedText(); + Environment->getOSOperator()->copyToClipboard(text.c_str()); + return true; + } + // Ctrl-C pressed // Copy text to clipboard if (prompt.getCursorLength() <= 0) @@ -649,8 +892,9 @@ bool GUIChatConsole::OnEvent(const SEvent& event) for (u32 i = 0; i < text.size(); i++) prompt.input(text[i]); - return true; } + + return true; } #endif else if(event.EventType == EET_MOUSE_INPUT_EVENT) @@ -659,6 +903,91 @@ bool GUIChatConsole::OnEvent(const SEvent& event) { s32 rows = myround(-3.0 * event.MouseInput.Wheel); m_chat_backend->scroll(rows); + m_vscrollbar->setPos(m_vscrollbar->getPos() + rows); + } else if (event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN) { + m_mouse_marking = true; + m_mark_begin = getCursorPos(event.MouseInput.X, event.MouseInput.Y); + m_mark_end = m_mark_begin; + } else if (event.MouseInput.Event == EMIE_LMOUSE_LEFT_UP) { + if (m_mouse_marking) { + m_mark_end = getCursorPos(event.MouseInput.X, event.MouseInput.Y); + m_mouse_marking = false; + + if (m_mark_begin == m_mark_end) { + m_mark_begin.reset(); + m_mark_end.reset(); + } + } + } else if (event.MouseInput.Event == EMIE_MOUSE_MOVED) { + if (m_mouse_marking) { + m_mark_end = getCursorPos(event.MouseInput.X, event.MouseInput.Y); + } + } + + return true; + } +#ifdef HAVE_TOUCHSCREENGUI + else if (event.EventType == EET_TOUCH_INPUT_EVENT) { + if (event.TouchInput.Event == irr::ETIE_PRESSED_DOWN) { + m_mouse_marking = false; + m_long_press = false; + m_cursor_press_pos = getCursorPos(event.TouchInput.X, event.TouchInput.Y); + + ChatSelection real_mark_begin = m_mark_end > m_mark_begin ? m_mark_begin : m_mark_end; + ChatSelection real_mark_end = m_mark_end > m_mark_begin ? m_mark_end : m_mark_begin; + + if (m_cursor_press_pos < real_mark_begin || m_cursor_press_pos > real_mark_end) { + m_mark_begin = m_cursor_press_pos; + m_mark_end = m_cursor_press_pos; + m_mouse_marking = true; + } + } else if (event.TouchInput.Event == irr::ETIE_LEFT_UP) { + ChatSelection cursor_pos = getCursorPos(event.TouchInput.X, event.TouchInput.Y); + + if (!m_long_press && m_cursor_press_pos == cursor_pos) { + m_mark_begin.reset(); + m_mark_end.reset(); + } + + m_cursor_press_pos.reset(); + m_mouse_marking = false; + m_long_press = false; + } else if (event.TouchInput.Event == irr::ETIE_MOVED) { + ChatSelection cursor_pos = getCursorPos(event.TouchInput.X, event.TouchInput.Y); + + if (!m_mouse_marking && !m_long_press && m_cursor_press_pos.initialized && + m_cursor_press_pos != cursor_pos) { + m_mark_begin = m_cursor_press_pos; + m_mark_end = m_cursor_press_pos; + m_mouse_marking = true; + } + + if (m_mouse_marking) { + m_mark_end = cursor_pos; + } + } else if (event.TouchInput.Event == irr::ETIE_PRESSED_LONG) { + if (!m_mouse_marking) { + m_long_press = true; + if (m_mark_begin != m_mark_end) { + irr::core::stringc text = getSelectedText(); + Environment->getOSOperator()->copyToClipboard(text.c_str()); +#ifdef __ANDROID__ + SDL_AndroidShowToast( + "Copied to clipboard", 2, + -1, 0, 0); +#elif __IOS__ + porting::showToast("Copied to clipboard"); +#endif + } + } + } + + return true; + } +#endif + else if (event.EventType == EET_GUI_EVENT) { + if (event.GUIEvent.EventType == EGET_SCROLL_BAR_CHANGED) { + updateVScrollBar(); } } @@ -669,9 +998,174 @@ void GUIChatConsole::setVisible(bool visible) { m_open = visible; IGUIElement::setVisible(visible); + m_vscrollbar->setVisible(visible); if (!visible) { m_height = 0; recalculateConsolePosition(); } } +//! create a vertical scroll bar +void GUIChatConsole::createVScrollBar() +{ + IGUISkin *skin = 0; + if (Environment) + skin = Environment->getSkin(); + + m_scrollbar_width = skin ? skin->getSize(gui::EGDS_SCROLLBAR_SIZE) : 16; + m_scrollbar_width *= 2; + + irr::core::rect scrollbarrect(m_screensize.X - m_scrollbar_width, + 0, m_screensize.X, m_height); + m_vscrollbar = new GUIScrollBar(Environment, getParent(), -1, + scrollbarrect, false, true); + + m_vscrollbar->setVisible(false); + m_vscrollbar->setMax(0); + m_vscrollbar->setPos(0); + m_vscrollbar->setPageSize(0); + m_vscrollbar->setSmallStep(1); + m_vscrollbar->setLargeStep(1); + m_vscrollbar->setArrowsVisible(GUIScrollBar::ArrowVisibility::SHOW); + + ITextureSource *tsrc = m_client->getTextureSource(); + m_vscrollbar->setTextures({ + tsrc->getTexture("gui/scrollbar_bg.png"), + tsrc->getTexture("gui/scrollbar_slider_long.png"), + tsrc->getTexture("gui/scrollbar_up.png"), + tsrc->getTexture("gui/scrollbar_down.png"), + }); + + addChild(m_vscrollbar); +} + +void GUIChatConsole::updateVScrollBar() +{ + if (!m_vscrollbar) + return; + + ChatBuffer& buf = m_chat_backend->getConsoleBuffer(); + + if (m_bottom_scroll_pos != buf.getBottomScrollPos()) { + m_bottom_scroll_pos = buf.getBottomScrollPos(); + + if (buf.getBottomScrollPos() > 0) { + buf.scrollAbsolute(m_bottom_scroll_pos); + m_vscrollbar->setMax(m_bottom_scroll_pos); + m_vscrollbar->setPos(m_bottom_scroll_pos); + } else { + m_vscrollbar->setMax(0); + m_vscrollbar->setPos(0); + } + } + + s32 page_size = (m_bottom_scroll_pos + buf.getRows() + 1) * m_fontsize.Y; + if (m_vscrollbar->getPageSize() != page_size) { + m_vscrollbar->setPageSize(page_size); + } + + if (m_vscrollbar->getPos() != buf.getScrollPos()) { + if (buf.getScrollPos() >= 0) { + s32 deltaScrollY = m_vscrollbar->getPos() - buf.getScrollPos(); + m_chat_backend->scroll(deltaScrollY); + } + } + + if (IsVisible) { + if (m_vscrollbar->isVisible() && m_vscrollbar->getMax() == 0) + m_vscrollbar->setVisible(false); + else if (!m_vscrollbar->isVisible() && m_vscrollbar->getMax() > 0) + m_vscrollbar->setVisible(true); + } +} + +bool GUIChatConsole::hasFocus() +{ + if (Environment->hasFocus(this)) + return true; + + if (Environment->hasFocus(m_vscrollbar)) + return true; + + const core::list &children = m_vscrollbar->getChildren(); + + for (gui::IGUIElement *it : children) { + if (Environment->hasFocus(it)) + return true; + } + + return false; +} + +bool GUIChatConsole::convertToMouseEvent( + SEvent &mouse_event, SEvent touch_event) const noexcept +{ +#ifdef HAVE_TOUCHSCREENGUI + mouse_event = {}; + mouse_event.EventType = EET_MOUSE_INPUT_EVENT; + mouse_event.MouseInput.X = touch_event.TouchInput.X; + mouse_event.MouseInput.Y = touch_event.TouchInput.Y; + switch (touch_event.TouchInput.Event) { + case ETIE_PRESSED_DOWN: + mouse_event.MouseInput.Event = EMIE_LMOUSE_PRESSED_DOWN; + mouse_event.MouseInput.ButtonStates = EMBSM_LEFT; + break; + case ETIE_MOVED: + mouse_event.MouseInput.Event = EMIE_MOUSE_MOVED; + mouse_event.MouseInput.ButtonStates = EMBSM_LEFT; + break; + case ETIE_LEFT_UP: + mouse_event.MouseInput.Event = EMIE_LMOUSE_LEFT_UP; + mouse_event.MouseInput.ButtonStates = 0; + break; + default: + return false; + } + + return true; +#else + + return false; +#endif +} + +bool GUIChatConsole::preprocessEvent(SEvent event) +{ + updateVScrollBar(); + +#ifdef HAVE_TOUCHSCREENGUI + if (event.EventType == irr::EET_TOUCH_INPUT_EVENT) { + const core::position2di p(event.TouchInput.X, event.TouchInput.Y); + + u32 row = m_chat_backend->getConsoleBuffer().getRows(); + s32 prompt_y = row * m_fontsize.Y + m_height - m_desired_height; + + if (m_vscrollbar->isPointInside(p) || !isPointInside(p)) { + SEvent mouse_event = {}; + bool success = convertToMouseEvent(mouse_event, event); + if (success) { + Environment->postEventFromUser(mouse_event); + } + } +#if defined(__ANDROID__) || defined(__IOS__) + else if (!porting::hasRealKeyboard() && + event.TouchInput.Y >= prompt_y && + event.TouchInput.Y <= m_height) { + if (event.TouchInput.Event == ETIE_PRESSED_DOWN && + !m_android_chat_open) { + ChatPrompt& prompt = m_chat_backend->getPrompt(); + porting::showInputDialog("", "", 2); + m_android_chat_open = true; + } + } +#endif + else { + OnEvent(event); + } + + return true; + } +#endif + + return false; +} diff --git a/src/gui/guiChatConsole.h b/src/gui/guiChatConsole.h index b7f85893c..b57375263 100644 --- a/src/gui/guiChatConsole.h +++ b/src/gui/guiChatConsole.h @@ -23,6 +23,75 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "modalMenu.h" #include "chat.h" #include "config.h" +#include "guiScrollBar.h" + +struct ChatSelection +{ + ChatSelection() : initialized(false), scroll(0), row(0), row_buf(0), + line(0), fragment(0), character(0), x_max(false) {}; + + void reset() { + initialized = false; + scroll = 0; + row = 0; + row_buf = 0; + line = 0; + fragment = 0; + character = 0; + x_max = false; + } + + bool operator== (const ChatSelection &other) const { + return (row + scroll == other.row + other.scroll && + row_buf == other.row_buf && + line == other.line && + fragment == other.fragment && + character == other.character && + x_max == other.x_max); + } + + bool operator< (const ChatSelection &other) const { + if (row + scroll != other.row + other.scroll) + return (row + scroll < other.row + other.scroll); + if (row_buf != other.row_buf) + return (row_buf < other.row_buf); + if (line != other.line) + return (line < other.line); + if (fragment != other.fragment) + return (fragment < other.fragment); + if (character != other.character) + return (character < other.character); + if (x_max != other.x_max) + return (x_max < other.x_max); + + return false; + } + + bool operator> (const ChatSelection &other) { + return other < *this; + } + + bool operator<= (const ChatSelection &other) { + return !(*this > other); + } + + bool operator>= (const ChatSelection &other) { + return !(*this < other); + } + + bool operator!= (const ChatSelection &other) const { + return !this->operator==(other); + } + + bool initialized; + int scroll; + int row; + int row_buf; + unsigned int line; + unsigned int fragment; + unsigned int character; + bool x_max; +}; class Client; @@ -50,8 +119,6 @@ public: // 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; } @@ -72,6 +139,18 @@ public: virtual void setVisible(bool visible); + bool hasFocus(); + + bool convertToMouseEvent( + SEvent &mouse_event, SEvent touch_event) const noexcept; + + bool preprocessEvent(SEvent event); + + bool getAndroidChatOpen() { return m_android_chat_open; } + void setAndroidChatOpen(bool value) { m_android_chat_open = value; } + + static GUIChatConsole* getChatConsole() { return m_chat_console; } + private: void reformatConsole(); void recalculateConsolePosition(); @@ -82,7 +161,14 @@ private: void drawText(); void drawPrompt(); + ChatSelection getCursorPos(s32 x, s32 y); + irr::core::stringc getSelectedText(); + void createVScrollBar(); + void updateVScrollBar(); + private: + static GUIChatConsole* m_chat_console; + ChatBackend* m_chat_backend; Client* m_client; IMenuManager* m_menumgr; @@ -124,4 +210,16 @@ private: // font gui::IGUIFont *m_font = nullptr; v2u32 m_fontsize; + + ChatSelection m_mark_begin; + ChatSelection m_mark_end; + bool m_mouse_marking = false; + bool m_long_press = false; + ChatSelection m_cursor_press_pos; + + u32 m_scrollbar_width = 0; + GUIScrollBar *m_vscrollbar = nullptr; + s32 m_bottom_scroll_pos = 0; + + bool m_android_chat_open = false; }; diff --git a/src/gui/guiScrollBar.cpp b/src/gui/guiScrollBar.cpp index 8a646115c..e19a85d11 100644 --- a/src/gui/guiScrollBar.cpp +++ b/src/gui/guiScrollBar.cpp @@ -17,11 +17,12 @@ the arrow buttons where there is insufficient space. GUIScrollBar::GUIScrollBar(IGUIEnvironment *environment, IGUIElement *parent, s32 id, core::rect rectangle, bool horizontal, bool auto_scale) : IGUIElement(EGUIET_ELEMENT, environment, parent, id, rectangle), - up_button(nullptr), down_button(nullptr), is_dragging(false), - is_horizontal(horizontal), is_auto_scaling(auto_scale), - dragged_by_slider(false), tray_clicked(false), scroll_pos(0), - draw_center(0), thumb_size(0), min_pos(0), max_pos(100), small_step(10), - large_step(50), drag_offset(0), page_size(100), border_size(0) + up_button(nullptr), down_button(nullptr), bg_image(nullptr), + slider_image(nullptr), is_dragging(false), is_horizontal(horizontal), + is_auto_scaling(auto_scale), dragged_by_slider(false), + tray_clicked(false), scroll_pos(0), draw_center(0), thumb_size(0), + min_pos(0), max_pos(100), small_step(10), large_step(50), + drag_offset(0), page_size(100), border_size(0) { refreshControls(); setNotClipped(false); @@ -217,9 +218,13 @@ void GUIScrollBar::draw() if (is_horizontal) rect = {h, 0, w - h, h}; - gui::IGUIImage *e = Environment->addImage(rect, this); - e->setImage(m_textures[0]); - e->setScaleImage(true); + if (!bg_image) { + bg_image = Environment->addImage(rect, this); + bg_image->setImage(m_textures[0]); + bg_image->setScaleImage(true); + } else { + bg_image->setRelativePosition(rect); + } } else { skin->draw2DRectangle(this, skin->getColor(EGDC_SCROLLBAR), slider_rect, &AbsoluteClippingRect); @@ -247,9 +252,13 @@ void GUIScrollBar::draw() if (is_horizontal) rect = {draw_center - (w / 2), 0, draw_center + w - (w / 2), h}; - gui::IGUIImage *e = Environment->addImage(core::rect(rect), this); - e->setImage(m_textures[1]); - e->setScaleImage(true); + if (!slider_image) { + slider_image = Environment->addImage(core::rect(rect), this); + slider_image->setImage(m_textures[1]); + slider_image->setScaleImage(true); + } else { + slider_image->setRelativePosition(rect); + } } else { skin->draw3DButtonPaneStandard(this, slider_rect, &AbsoluteClippingRect); } diff --git a/src/gui/guiScrollBar.h b/src/gui/guiScrollBar.h index 19a49cf76..7e53dc9c7 100644 --- a/src/gui/guiScrollBar.h +++ b/src/gui/guiScrollBar.h @@ -40,6 +40,7 @@ public: s32 getLargeStep() const { return large_step; } s32 getSmallStep() const { return small_step; } s32 getPos() const; + s32 getPageSize() const { return page_size; } void setMax(const s32 &max); void setMin(const s32 &min); @@ -57,6 +58,8 @@ private: IGUIButton *up_button; IGUIButton *down_button; + gui::IGUIImage *bg_image; + gui::IGUIImage *slider_image; ArrowVisibility arrow_visibility = DEFAULT; bool is_dragging; bool is_horizontal; diff --git a/textures/base/pack/gui/scrollbar_bg.png b/textures/base/pack/gui/scrollbar_bg.png new file mode 100644 index 000000000..fafabb28c Binary files /dev/null and b/textures/base/pack/gui/scrollbar_bg.png differ diff --git a/textures/base/pack/gui/scrollbar_down.png b/textures/base/pack/gui/scrollbar_down.png new file mode 100644 index 000000000..6ee8de85d Binary files /dev/null and b/textures/base/pack/gui/scrollbar_down.png differ diff --git a/textures/base/pack/gui/scrollbar_slider_long.png b/textures/base/pack/gui/scrollbar_slider_long.png new file mode 100644 index 000000000..016739eb0 Binary files /dev/null and b/textures/base/pack/gui/scrollbar_slider_long.png differ diff --git a/textures/base/pack/gui/scrollbar_up.png b/textures/base/pack/gui/scrollbar_up.png new file mode 100644 index 000000000..2e53043d4 Binary files /dev/null and b/textures/base/pack/gui/scrollbar_up.png differ