From 7e7420c5e787f4150884c1faa42a17171eb0ee41 Mon Sep 17 00:00:00 2001 From: Deve Date: Fri, 5 May 2023 21:13:08 +0200 Subject: [PATCH] Chat console fixes (#133) * Some fixes for text selection in chat. - Reset selection on chat open - Reset selection on new message - Reset selection when window size changed * Don't move to bottom when user is scrolling chat * Some work on prompt selection * Make it working when text length is larger than max visible length * Handle touch events for prompt * Avoid moving chat history on new messages during scrolling. Also simplify selection marks comparing. * Move scroll to bottom on sending new message when console is opened without close on enter * Fixed copy to clipboard when chat buffer is full and also keep text selected when new message arrives * Some improvements for prompt selection * Delete selected text on new input or backspace key press * Fixed text selection and cursor pos for regular font * Fixed drawing prompt with newlines * Fixed empty prompt selection with ctrl+a * Enable chat console on android tablets --- src/chat.cpp | 39 ++- src/chat.h | 37 ++- src/client/game.cpp | 13 +- src/gui/guiChatConsole.cpp | 563 ++++++++++++++++++++++++++++++++----- src/gui/guiChatConsole.h | 87 ++++-- 5 files changed, 634 insertions(+), 105 deletions(-) diff --git a/src/chat.cpp b/src/chat.cpp index 6da5a7585..6e130b0ce 100644 --- a/src/chat.cpp +++ b/src/chat.cpp @@ -42,13 +42,14 @@ void ChatBuffer::addLine(const std::wstring &name, const std::wstring &text) { m_lines_modified = true; - ChatLine line(name, text); + ChatLine line(name, text, m_current_line_index); m_unformatted.push_back(line); + m_current_line_index++; 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_unformatted.size() - 1, m_cols, m_formatted); + u32 num_added = formatChatLine(line, m_cols, m_formatted); if (scrolled_at_bottom) m_scroll += num_added; } @@ -63,6 +64,7 @@ void ChatBuffer::clear() { m_unformatted.clear(); m_formatted.clear(); + m_current_line_index = 0; m_scroll = 0; m_lines_modified = true; } @@ -117,6 +119,8 @@ void ChatBuffer::deleteOldest(u32 count) m_scroll = getBottomScrollPos(); else scrollAbsolute(m_scroll - del_formatted); + + m_del_formatted += del_formatted; } void ChatBuffer::deleteByAge(f32 maxAge) @@ -173,7 +177,7 @@ void ChatBuffer::reformat(u32 cols, u32 rows) { if (i == restore_scroll_unformatted) restore_scroll_formatted = m_formatted.size(); - formatChatLine(m_unformatted[i], i, cols, m_formatted); + formatChatLine(m_unformatted[i], cols, m_formatted); } } @@ -224,7 +228,7 @@ void ChatBuffer::scrollBottom() m_scroll = getBottomScrollPos(); } -u32 ChatBuffer::formatChatLine(const ChatLine& line, int line_index, u32 cols, +u32 ChatBuffer::formatChatLine(const ChatLine& line, u32 cols, std::vector& destination) const { u32 num_added = 0; @@ -267,7 +271,7 @@ u32 ChatBuffer::formatChatLine(const ChatLine& line, int line_index, u32 cols, //EnrichedString line_text(line.text); next_line.first = true; - next_line.line_index = line_index; + next_line.line_index = line.line_index; bool text_processing = false; // Produce fragments and layout them into lines @@ -391,6 +395,7 @@ void ChatPrompt::input(wchar_t ch) clampView(); m_nick_completion_start = 0; m_nick_completion_end = 0; + m_line_modified = true; } void ChatPrompt::input(const std::wstring &str) @@ -400,6 +405,7 @@ void ChatPrompt::input(const std::wstring &str) clampView(); m_nick_completion_start = 0; m_nick_completion_end = 0; + m_line_modified = true; } void ChatPrompt::addToHistory(const std::wstring &line) @@ -424,6 +430,7 @@ void ChatPrompt::clear() m_cursor = 0; m_nick_completion_start = 0; m_nick_completion_end = 0; + m_line_modified = true; } std::wstring ChatPrompt::replace(const std::wstring &line) @@ -434,6 +441,7 @@ std::wstring ChatPrompt::replace(const std::wstring &line) clampView(); m_nick_completion_start = 0; m_nick_completion_end = 0; + m_line_modified = true; return old_line; } @@ -535,6 +543,7 @@ void ChatPrompt::nickCompletion(const std::list& names, bool backwa clampView(); m_nick_completion_start = prefix_start; m_nick_completion_end = prefix_end; + m_line_modified = true; } void ChatPrompt::reformat(u32 cols) @@ -616,6 +625,7 @@ void ChatPrompt::cursorOperation(CursorOp op, CursorOpDir dir, CursorOpScope sco m_line.erase(m_cursor, abs(new_cursor - old_cursor)); } m_cursor_len = 0; + m_line_modified = true; break; case CURSOROP_SELECT: if (scope == CURSOROP_SCOPE_LINE) { @@ -635,6 +645,25 @@ void ChatPrompt::cursorOperation(CursorOp op, CursorOpDir dir, CursorOpScope sco m_nick_completion_end = 0; } +void ChatPrompt::setCursorPos(int cursor_pos) +{ + s32 length = m_line.size(); + m_cursor = MYMAX(MYMIN(cursor_pos, length), 0); + m_cursor_len = 0; + + clampView(); + + m_nick_completion_start = 0; + m_nick_completion_end = 0; +} + +void ChatPrompt::setViewPosition(int view) +{ + m_view = view; + + clampView(); +} + void ChatPrompt::clampView() { s32 length = m_line.size(); diff --git a/src/chat.h b/src/chat.h index d742d6b8c..0edacea99 100644 --- a/src/chat.h +++ b/src/chat.h @@ -37,16 +37,22 @@ struct ChatLine EnrichedString name; // message text EnrichedString text; + // Line index in ChatLine buffer + int line_index; - ChatLine(const std::wstring &a_name, const std::wstring &a_text): + ChatLine(const std::wstring &a_name, const std::wstring &a_text, + int a_line_index): name(a_name), - text(a_text) + text(a_text), + line_index(a_line_index) { } - ChatLine(const EnrichedString &a_name, const EnrichedString &a_text): + ChatLine(const EnrichedString &a_name, const EnrichedString &a_text, + int a_line_index): name(a_name), - text(a_text) + text(a_text), + line_index(a_line_index) { } }; @@ -123,10 +129,13 @@ public: bool getLinesModified() const { return m_lines_modified; } void resetLinesModified() { m_lines_modified = false; } + u32 getDelFormatted() const { return m_del_formatted; } + void resetDelFormatted() { m_del_formatted = 0; } + // 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, int line_index, u32 cols, + u32 formatChatLine(const ChatLine& line, u32 cols, std::vector& destination) const; void resize(u32 scrollback); @@ -155,6 +164,11 @@ private: // Is always set to true when m_unformatted is modified, because that's what // determines the output of getLineCount() and getLine() bool m_lines_modified = true; + + // How many formatted lines have been deleted + u32 m_del_formatted = 0; + + int m_current_line_index = 0; }; class ChatPrompt @@ -196,6 +210,8 @@ public: std::wstring getVisiblePortion() const; // Get cursor position (relative to visible portion). -1 if invalid s32 getVisibleCursorPosition() const; + // Get view position (absolute value) + s32 getViewPosition() const { return m_view; } // Get length of cursor selection s32 getCursorLength() const { return m_cursor_len; } @@ -231,6 +247,14 @@ public: // deletes the word to the left of the cursor. void cursorOperation(CursorOp op, CursorOpDir dir, CursorOpScope scope); + void setCursorPos(int cursor_pos); + void setViewPosition(int view); + + // Functions for keeping track of whether the line was modified by any + // preceding operations + bool getLineModified() const { return m_line_modified; } + void resetLineModified() { m_line_modified = false; } + protected: // set m_view to ensure that 0 <= m_view <= m_cursor < m_view + m_cols // if line can be fully shown, set m_view to zero @@ -262,6 +286,9 @@ private: s32 m_nick_completion_start = 0; // Last nick completion start (index into m_line) s32 m_nick_completion_end = 0; + + // True if line was modified + bool m_line_modified = true; }; class ChatBackend diff --git a/src/client/game.cpp b/src/client/game.cpp index 195614fa5..0b7a2dc1b 100644 --- a/src/client/game.cpp +++ b/src/client/game.cpp @@ -2141,13 +2141,11 @@ void Game::openConsole(float scale, const wchar_t *line) porting::showInputDialog("", "", 2); 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; gui_chat_console->openConsole(scale); @@ -2986,6 +2984,13 @@ void Game::updateChat(f32 dtime) if (buf.getLinesModified()) { buf.resetLinesModified(); m_game_ui->setChatText(chat_backend->getRecentChat(), buf.getLineCount()); + gui_chat_console->onLinesModified(); + } + + auto &prompt = chat_backend->getPrompt(); + if (prompt.getLineModified()) { + prompt.resetLineModified(); + gui_chat_console->onPromptModified(); } // Make sure that the size is still correct diff --git a/src/gui/guiChatConsole.cpp b/src/gui/guiChatConsole.cpp index 9c83fa870..ea6be6339 100644 --- a/src/gui/guiChatConsole.cpp +++ b/src/gui/guiChatConsole.cpp @@ -29,6 +29,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "client/fontengine.h" #include "log.h" #include "gettext.h" +#include #include #include "touchscreengui.h" @@ -132,6 +133,7 @@ void GUIChatConsole::openConsole(f32 scale) m_desired_height = scale * m_screensize.Y; reformatConsole(); + updateVScrollBar(false, true); m_animate_time_old = porting::getTimeMs(); IGUIElement::setVisible(true); m_vscrollbar->setVisible(true); @@ -228,6 +230,7 @@ void GUIChatConsole::draw() m_screensize = screensize; m_desired_height = m_desired_height_fraction * m_screensize.Y; reformatConsole(); + updateVScrollBar(true, false); } // Animation @@ -255,7 +258,12 @@ void GUIChatConsole::reformatConsole() recalculateConsolePosition(); m_chat_backend->reformat(cols, rows); - updateVScrollBar(); + m_mark_begin.reset(); + m_mark_end.reset(); + m_cursor_press_pos.reset(); + m_history_marking = false; + m_prompt_marking = false; + m_long_press = false; } void GUIChatConsole::recalculateConsolePosition() @@ -367,6 +375,8 @@ void GUIChatConsole::drawText() 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 && + real_mark_begin.selection_type == ChatSelection::SELECTION_HISTORY && + real_mark_end.selection_type == ChatSelection::SELECTION_HISTORY && (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]; @@ -454,24 +464,62 @@ void GUIChatConsole::drawPrompt() ChatPrompt& prompt = m_chat_backend->getPrompt(); std::wstring prompt_text = prompt.getVisiblePortion(); + std::replace_if(prompt_text.begin(), prompt_text.end(), + [](wchar_t c) { return (c == L'\n' || c == L'\r'); }, L' '); - // 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 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); + 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 && + real_mark_begin.selection_type == ChatSelection::SELECTION_PROMPT && + real_mark_end.selection_type == ChatSelection::SELECTION_PROMPT) { + std::wstring begin_text = L"]"; + int begin_text_size = m_font->getDimension(begin_text.c_str()).Width; + int text_pos = m_fontsize.X + begin_text_size; + + s32 x_begin = text_pos; + s32 text_size = m_font->getDimension(prompt_text.c_str()).Width; + s32 x_end = x_begin + text_size; + + int current_scroll = prompt.getViewPosition(); + if (real_mark_begin.character > 0) { + irr::core::stringw text = prompt_text.c_str(); + int scroll_offset = real_mark_begin.scroll - current_scroll; + int length = scroll_offset + real_mark_begin.character; + length = MYMIN(MYMAX(length, 0), prompt_text.size() - 1); + text = text.subString(1, length); + s32 text_size = m_font->getDimension(text.c_str()).Width; + x_begin = text_pos + text_size; + } + + if (real_mark_end.character < prompt_text.size() - 1) { + irr::core::stringw text = prompt_text.c_str(); + int scroll_offset = real_mark_end.scroll - current_scroll; + int length = scroll_offset + real_mark_end.character; + if (real_mark_end.x_max) + length++; + length = MYMIN(MYMAX(length, 0), prompt_text.size() - 1); + text = text.subString(1, length); + s32 text_size = m_font->getDimension(text.c_str()).Width; + x_end = text_pos + 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); } + core::rect destrect( + m_fontsize.X, y, prompt_text.size() * m_fontsize.X, y + m_fontsize.Y); + m_font->draw( + prompt_text.c_str(), + destrect, + video::SColor(255, 255, 255, 255), + false, + false, + &AbsoluteClippingRect); + // Draw the cursor during on periods if ((m_cursor_blink & 0x8000) != 0) { @@ -480,7 +528,9 @@ void GUIChatConsole::drawPrompt() { s32 cursor_len = prompt.getCursorLength(); video::IVideoDriver* driver = Environment->getVideoDriver(); - s32 x = (1 + cursor_pos) * m_fontsize.X; + std::wstring text = prompt_text.substr(0, cursor_pos); + s32 x = m_font->getDimension(text.c_str()).Width + m_fontsize.X; + core::rect destrect( x, y + m_fontsize.Y * (1.0 - m_cursor_height), @@ -494,7 +544,6 @@ void GUIChatConsole::drawPrompt() &AbsoluteClippingRect); } } - } @@ -507,7 +556,7 @@ ChatSelection GUIChatConsole::getCursorPos(s32 x, s32 y) ChatBuffer& buf = m_chat_backend->getConsoleBuffer(); selection.scroll = buf.getScrollPos(); - selection.initialized = true; + selection.selection_type = ChatSelection::SELECTION_HISTORY; s32 line_height = m_fontsize.Y; s32 y_min = m_height - m_desired_height; @@ -533,7 +582,7 @@ ChatSelection GUIChatConsole::getCursorPos(s32 x, s32 y) } ChatFormattedLine line = buf.getFormattedLine(selection.row); - selection.row_buf = line.line_index; + selection.line_index = line.line_index; int current_row = selection.row; while (!line.first) { @@ -585,6 +634,58 @@ ChatSelection GUIChatConsole::getCursorPos(s32 x, s32 y) return selection; } +ChatSelection GUIChatConsole::getPromptCursorPos(s32 x, s32 y) +{ + ChatSelection selection; + + if (m_font == NULL) + return selection; + + ChatPrompt& prompt = m_chat_backend->getPrompt(); + + selection.selection_type = ChatSelection::SELECTION_PROMPT; + selection.scroll = prompt.getViewPosition(); + + std::wstring prompt_text = prompt.getVisiblePortion(); + irr::core::stringw text = prompt_text.c_str(); + text = text.subString(1, prompt_text.size() - 1); + + std::wstring begin_text = L"]"; + int begin_text_size = m_font->getDimension(begin_text.c_str()).Width; + int text_pos = m_fontsize.X + begin_text_size; + int pos = m_font->getCharacterFromPos(text.c_str(), x - text_pos); + + if (pos == -1) { + selection.x_max = true; + selection.character = text.size() - 1; + } else { + selection.character = pos; + } + + return selection; +} + +ChatSelection GUIChatConsole::getCurrentPromptCursorPos() +{ + ChatSelection selection; + + if (m_font == NULL) + return selection; + + ChatPrompt& prompt = m_chat_backend->getPrompt(); + + selection.selection_type = ChatSelection::SELECTION_PROMPT; + selection.scroll = prompt.getViewPosition(); + selection.character = prompt.getVisibleCursorPosition() - 1; + + if ((unsigned int)selection.character > prompt.getLine().size() - selection.scroll - 1) { + selection.character--; + selection.x_max = true; + } + + return selection; +} + irr::core::stringc GUIChatConsole::getSelectedText() { if (m_font == NULL) @@ -601,11 +702,19 @@ irr::core::stringc GUIChatConsole::getSelectedText() ChatBuffer& buf = m_chat_backend->getConsoleBuffer(); - for (int row = real_mark_begin.row_buf; row < real_mark_end.row_buf + 1; row++) { + const ChatLine& first_line = buf.getLine(0); + int first_line_index = first_line.line_index; + int mark_begin_row_buf = real_mark_begin.line_index - first_line_index; + int mark_end_row_buf = real_mark_end.line_index - first_line_index; + + if (mark_begin_row_buf < 0 || mark_end_row_buf < 0) + return ""; + + for (int row = mark_begin_row_buf; row < mark_end_row_buf + 1; row++) { const ChatLine& line = buf.getLine(row); std::vector formatted_lines; - buf.formatChatLine(line, 0, buf.getColsCount(), formatted_lines); + buf.formatChatLine(line, buf.getColsCount(), formatted_lines); for (unsigned int i = 0; i < formatted_lines.size(); i++) { const ChatFormattedLine &line = formatted_lines[i]; @@ -615,7 +724,7 @@ irr::core::stringc GUIChatConsole::getSelectedText() for (unsigned int k = 0; k < fragment.text.size(); k++) { if (!add_to_string && - row == real_mark_begin.row_buf && + row == mark_begin_row_buf && i == real_mark_begin.line && j == real_mark_begin.fragment && k == real_mark_begin.character) { @@ -626,7 +735,7 @@ irr::core::stringc GUIChatConsole::getSelectedText() } if (add_to_string) { - if (row == real_mark_end.row_buf && + if (row == mark_end_row_buf && i == real_mark_end.line && j == real_mark_end.fragment && k == real_mark_end.character) { @@ -643,7 +752,7 @@ irr::core::stringc GUIChatConsole::getSelectedText() } } - if (row < real_mark_end.row_buf) { + if (row < mark_end_row_buf) { text += L"\n"; } } @@ -654,6 +763,80 @@ irr::core::stringc GUIChatConsole::getSelectedText() return text_c; } +irr::core::stringc GUIChatConsole::getPromptSelectedText() +{ + if (m_font == NULL) + return ""; + + if (m_mark_begin == m_mark_end) + return ""; + + ChatPrompt& prompt = m_chat_backend->getPrompt(); + + 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; + + std::wstring prompt_text = prompt.getLine(); + + if (real_mark_end.scroll + real_mark_end.character > prompt_text.size()) + return ""; + + irr::core::stringw text = prompt_text.c_str(); + int begin = real_mark_begin.scroll + real_mark_begin.character; + int length = real_mark_end.scroll + real_mark_end.character - begin; + if (real_mark_end.x_max) + length++; + text = text.subString(begin, length); + + irr::core::stringc text_c; + text_c = wide_to_utf8(text.c_str()).c_str(); + return text_c; +} + +void GUIChatConsole::movePromptCursor(s32 x, s32 y) +{ + ChatSelection selection = getPromptCursorPos(x, y); + + int cursor_pos = selection.scroll + selection.character; + if (selection.x_max) + cursor_pos++; + + ChatPrompt& prompt = m_chat_backend->getPrompt(); + prompt.setCursorPos(cursor_pos); +} + +void GUIChatConsole::deletePromptSelection() +{ + if (m_mark_begin.selection_type != ChatSelection::SELECTION_PROMPT || + m_mark_end.selection_type != ChatSelection::SELECTION_PROMPT || + m_mark_begin == m_mark_end) + return; + + ChatPrompt& prompt = m_chat_backend->getPrompt(); + int scroll_pos = prompt.getViewPosition(); + + 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; + + int pos_begin = real_mark_begin.scroll + real_mark_begin.character; + int pos_end = real_mark_end.scroll + real_mark_end.character; + if (real_mark_end.x_max) + pos_end++; + + std::wstring prompt_text = prompt.getLine(); + std::wstring new_text; + new_text = prompt_text.substr(0, pos_begin); + new_text += prompt_text.substr(pos_end, prompt_text.size() - pos_end); + + prompt.replace(new_text); + + int cursor_pos = real_mark_begin.scroll + real_mark_begin.character; + prompt.setCursorPos(cursor_pos); + prompt.setViewPosition(scroll_pos); + + m_mark_begin.reset(); + m_mark_end.reset(); +} bool GUIChatConsole::OnEvent(const SEvent& event) { @@ -702,6 +885,8 @@ bool GUIChatConsole::OnEvent(const SEvent& event) if (m_close_on_enter) { closeConsole(); m_close_on_enter = false; + } else { + updateVScrollBar(true, true); } return true; } @@ -721,11 +906,11 @@ bool GUIChatConsole::OnEvent(const SEvent& event) } else if(event.KeyInput.Key == KEY_LEFT || event.KeyInput.Key == KEY_RIGHT) { + ChatSelection old_pos = getCurrentPromptCursorPos(); + // 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::CursorOp op = ChatPrompt::CURSOROP_MOVE; ChatPrompt::CursorOpDir dir = event.KeyInput.Key == KEY_LEFT ? ChatPrompt::CURSOROP_DIR_LEFT : ChatPrompt::CURSOROP_DIR_RIGHT; @@ -733,30 +918,86 @@ bool GUIChatConsole::OnEvent(const SEvent& event) ChatPrompt::CURSOROP_SCOPE_WORD : ChatPrompt::CURSOROP_SCOPE_CHARACTER; prompt.cursorOperation(op, dir, scope); + + if (event.KeyInput.Shift) { + if (m_mark_begin.selection_type != ChatSelection::SELECTION_PROMPT || + m_mark_end.selection_type != ChatSelection::SELECTION_PROMPT) { + m_mark_begin = old_pos; + } + m_mark_end = getCurrentPromptCursorPos(); + } else { + if (m_mark_begin.selection_type == ChatSelection::SELECTION_PROMPT && + m_mark_end.selection_type == ChatSelection::SELECTION_PROMPT) { + m_mark_begin.reset(); + m_mark_end.reset(); + } + } + return true; } else if(event.KeyInput.Key == KEY_HOME) { + ChatSelection old_pos = getCurrentPromptCursorPos(); + // Home pressed // move to beginning of line prompt.cursorOperation( ChatPrompt::CURSOROP_MOVE, ChatPrompt::CURSOROP_DIR_LEFT, ChatPrompt::CURSOROP_SCOPE_LINE); + + if (event.KeyInput.Shift) { + if (m_mark_begin.selection_type != ChatSelection::SELECTION_PROMPT || + m_mark_end.selection_type != ChatSelection::SELECTION_PROMPT) { + m_mark_begin = old_pos; + } + m_mark_end = getCurrentPromptCursorPos(); + } else { + if (m_mark_begin.selection_type == ChatSelection::SELECTION_PROMPT && + m_mark_end.selection_type == ChatSelection::SELECTION_PROMPT) { + m_mark_begin.reset(); + m_mark_end.reset(); + } + } + return true; } else if(event.KeyInput.Key == KEY_END) { + ChatSelection old_pos = getCurrentPromptCursorPos(); + // End pressed // move to end of line prompt.cursorOperation( ChatPrompt::CURSOROP_MOVE, ChatPrompt::CURSOROP_DIR_RIGHT, ChatPrompt::CURSOROP_SCOPE_LINE); + + if (event.KeyInput.Shift) { + if (m_mark_begin.selection_type != ChatSelection::SELECTION_PROMPT || + m_mark_end.selection_type != ChatSelection::SELECTION_PROMPT) { + m_mark_begin = old_pos; + } + m_mark_end = getCurrentPromptCursorPos(); + } else { + if (m_mark_begin.selection_type == ChatSelection::SELECTION_PROMPT && + m_mark_end.selection_type == ChatSelection::SELECTION_PROMPT) { + m_mark_begin.reset(); + m_mark_end.reset(); + } + } + return true; } else if(event.KeyInput.Key == KEY_BACK) { + if (m_mark_begin.selection_type == ChatSelection::SELECTION_PROMPT && + m_mark_end.selection_type == ChatSelection::SELECTION_PROMPT && + m_mark_begin != m_mark_end) { + deletePromptSelection(); + return true; + } + // Backspace or Ctrl-Backspace pressed // delete character / word to the left ChatPrompt::CursorOpScope scope = @@ -771,6 +1012,13 @@ bool GUIChatConsole::OnEvent(const SEvent& event) } else if(event.KeyInput.Key == KEY_DELETE) { + if (m_mark_begin.selection_type == ChatSelection::SELECTION_PROMPT && + m_mark_end.selection_type == ChatSelection::SELECTION_PROMPT && + m_mark_begin != m_mark_end) { + deletePromptSelection(); + return true; + } + // Delete or Ctrl-Delete pressed // delete character / word to the right ChatPrompt::CursorOpScope scope = @@ -785,20 +1033,42 @@ bool GUIChatConsole::OnEvent(const SEvent& event) } 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); + if (prompt.getLine().size() > 0) { + ChatPrompt& prompt = m_chat_backend->getPrompt(); + + m_mark_begin.reset(); + m_mark_begin.selection_type = ChatSelection::SELECTION_PROMPT; + m_mark_begin.scroll = 0; + m_mark_begin.character = 0; + + m_mark_end.reset(); + m_mark_end.selection_type = ChatSelection::SELECTION_PROMPT; + m_mark_end.scroll = 0; + m_mark_end.character = prompt.getLine().size() - 1; + m_mark_end.x_max = true; + + prompt.cursorOperation( + ChatPrompt::CURSOROP_MOVE, + ChatPrompt::CURSOROP_DIR_RIGHT, + ChatPrompt::CURSOROP_SCOPE_LINE); + } + return true; } 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; + if (m_mark_begin.selection_type == ChatSelection::SELECTION_PROMPT && + m_mark_end.selection_type == ChatSelection::SELECTION_PROMPT) { + irr::core::stringc text = getPromptSelectedText(); + Environment->getOSOperator()->copyToClipboard(text.c_str()); + return true; + } else if (m_mark_begin.selection_type == ChatSelection::SELECTION_HISTORY && + m_mark_end.selection_type == ChatSelection::SELECTION_HISTORY) { + irr::core::stringc text = getSelectedText(); + Environment->getOSOperator()->copyToClipboard(text.c_str()); + return true; + } } // Ctrl-C pressed @@ -812,6 +1082,12 @@ bool GUIChatConsole::OnEvent(const SEvent& event) } else if(event.KeyInput.Key == KEY_KEY_V && event.KeyInput.Control) { + if (m_mark_begin.selection_type == ChatSelection::SELECTION_PROMPT && + m_mark_end.selection_type == ChatSelection::SELECTION_PROMPT && + m_mark_begin != m_mark_end) { + deletePromptSelection(); + } + // Ctrl-V pressed // paste text from clipboard if (prompt.getCursorLength() > 0) { @@ -830,6 +1106,15 @@ bool GUIChatConsole::OnEvent(const SEvent& event) } else if(event.KeyInput.Key == KEY_KEY_X && event.KeyInput.Control) { + if (m_mark_begin.selection_type == ChatSelection::SELECTION_PROMPT && + m_mark_end.selection_type == ChatSelection::SELECTION_PROMPT && + m_mark_begin != m_mark_end) { + irr::core::stringc text = getPromptSelectedText(); + Environment->getOSOperator()->copyToClipboard(text.c_str()); + deletePromptSelection(); + return true; + } + // Ctrl-X pressed // Cut text to clipboard if (prompt.getCursorLength() <= 0) @@ -872,6 +1157,12 @@ bool GUIChatConsole::OnEvent(const SEvent& event) prompt.nickCompletion(names, backwards); return true; } else if (!iswcntrl(event.KeyInput.Char) && !event.KeyInput.Control) { + if (m_mark_begin.selection_type == ChatSelection::SELECTION_PROMPT && + m_mark_end.selection_type == ChatSelection::SELECTION_PROMPT && + m_mark_begin != m_mark_end) { + deletePromptSelection(); + } + #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) ); @@ -887,6 +1178,12 @@ bool GUIChatConsole::OnEvent(const SEvent& event) { if (event.SDLTextEvent.Type == ESDLET_TEXTINPUT) { + if (m_mark_begin.selection_type == ChatSelection::SELECTION_PROMPT && + m_mark_end.selection_type == ChatSelection::SELECTION_PROMPT && + m_mark_begin != m_mark_end) { + deletePromptSelection(); + } + std::wstring text = utf8_to_wide(event.SDLTextEvent.Text); for (u32 i = 0; i < text.size(); i++) @@ -905,13 +1202,41 @@ bool GUIChatConsole::OnEvent(const SEvent& event) 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; + u32 row = m_chat_backend->getConsoleBuffer().getRows(); + s32 prompt_y = row * m_fontsize.Y + m_height - m_desired_height; + + if (event.MouseInput.Y >= prompt_y) { + m_prompt_marking = true; + if (event.MouseInput.Shift) { + if (m_mark_begin.selection_type != ChatSelection::SELECTION_PROMPT || + m_mark_end.selection_type != ChatSelection::SELECTION_PROMPT) { + m_mark_begin = getCurrentPromptCursorPos(); + m_mark_end = getPromptCursorPos(event.MouseInput.X, event.MouseInput.Y); + } + } else { + m_mark_begin = getPromptCursorPos(event.MouseInput.X, event.MouseInput.Y); + m_mark_end = m_mark_begin; + } + movePromptCursor(event.MouseInput.X, event.MouseInput.Y); + } else { + m_history_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) { + if (m_prompt_marking) { + m_mark_end = getPromptCursorPos(event.MouseInput.X, event.MouseInput.Y); + m_prompt_marking = false; + + if (!event.MouseInput.Shift) { + if (m_mark_begin == m_mark_end) { + m_mark_begin.reset(); + m_mark_end.reset(); + } + } + } else if (m_history_marking) { m_mark_end = getCursorPos(event.MouseInput.X, event.MouseInput.Y); - m_mouse_marking = false; + m_history_marking = false; if (m_mark_begin == m_mark_end) { m_mark_begin.reset(); @@ -919,7 +1244,10 @@ bool GUIChatConsole::OnEvent(const SEvent& event) } } } else if (event.MouseInput.Event == EMIE_MOUSE_MOVED) { - if (m_mouse_marking) { + if (m_prompt_marking) { + m_mark_end = getPromptCursorPos(event.MouseInput.X, event.MouseInput.Y); + movePromptCursor(event.MouseInput.X, event.MouseInput.Y); + } else if (m_history_marking) { m_mark_end = getCursorPos(event.MouseInput.X, event.MouseInput.Y); } } @@ -929,47 +1257,101 @@ bool GUIChatConsole::OnEvent(const SEvent& event) #ifdef HAVE_TOUCHSCREENGUI else if (event.EventType == EET_TOUCH_INPUT_EVENT) { if (event.TouchInput.Event == irr::ETIE_PRESSED_DOWN) { - m_mouse_marking = false; + m_history_marking = false; + m_prompt_marking = false; m_long_press = false; - m_cursor_press_pos = getCursorPos(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; 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 (event.TouchInput.Y >= prompt_y) { - if (!m_long_press && m_cursor_press_pos == cursor_pos) { - m_mark_begin.reset(); - m_mark_end.reset(); + m_cursor_press_pos = getPromptCursorPos(event.TouchInput.X, event.TouchInput.Y); + + if (real_mark_begin.selection_type != ChatSelection::SELECTION_PROMPT || + real_mark_end.selection_type != ChatSelection::SELECTION_PROMPT || + 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_prompt_marking = true; + } + } else { + m_cursor_press_pos = getCursorPos(event.TouchInput.X, event.TouchInput.Y); + + if (real_mark_begin.selection_type != ChatSelection::SELECTION_HISTORY || + real_mark_end.selection_type != ChatSelection::SELECTION_HISTORY || + 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_history_marking = true; + } + } + + } else if (event.TouchInput.Event == irr::ETIE_LEFT_UP) { + if (m_prompt_marking) { + ChatSelection cursor_pos = getPromptCursorPos(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_prompt_marking = false; + } else if (m_history_marking) { + 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_history_marking = false; } 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); + ChatSelection prompt_cursor_pos = getPromptCursorPos(event.TouchInput.X, event.TouchInput.Y); - if (!m_mouse_marking && !m_long_press && m_cursor_press_pos.initialized && + if (!m_prompt_marking && !m_long_press && + m_cursor_press_pos.selection_type == ChatSelection::SELECTION_PROMPT && + m_cursor_press_pos != prompt_cursor_pos) { + m_mark_begin = m_cursor_press_pos; + m_mark_end = m_cursor_press_pos; + m_prompt_marking = true; + } else if (!m_history_marking && !m_long_press && + m_cursor_press_pos.selection_type == ChatSelection::SELECTION_HISTORY && m_cursor_press_pos != cursor_pos) { m_mark_begin = m_cursor_press_pos; m_mark_end = m_cursor_press_pos; - m_mouse_marking = true; + m_history_marking = true; } - if (m_mouse_marking) { + if (m_prompt_marking) { + m_mark_end = prompt_cursor_pos; + } else if (m_history_marking) { m_mark_end = cursor_pos; } + } else if (event.TouchInput.Event == irr::ETIE_PRESSED_LONG) { - if (!m_mouse_marking) { + if (!m_history_marking && ! m_prompt_marking) { m_long_press = true; if (m_mark_begin != m_mark_end) { - irr::core::stringc text = getSelectedText(); + irr::core::stringc text; + if (m_mark_begin.selection_type == ChatSelection::SELECTION_PROMPT && + m_mark_end.selection_type == ChatSelection::SELECTION_PROMPT) { + text = getPromptSelectedText(); + } else if (m_mark_begin.selection_type == ChatSelection::SELECTION_HISTORY && + m_mark_end.selection_type == ChatSelection::SELECTION_HISTORY) { + text = getSelectedText(); + } Environment->getOSOperator()->copyToClipboard(text.c_str()); #ifdef __ANDROID__ SDL_AndroidShowToast( @@ -1041,20 +1423,22 @@ void GUIChatConsole::createVScrollBar() addChild(m_vscrollbar); } -void GUIChatConsole::updateVScrollBar() +void GUIChatConsole::updateVScrollBar(bool force_update, bool move_bottom) { if (!m_vscrollbar) return; ChatBuffer& buf = m_chat_backend->getConsoleBuffer(); - if (m_bottom_scroll_pos != buf.getBottomScrollPos()) { + if (m_bottom_scroll_pos != buf.getBottomScrollPos() || force_update) { + bool is_bottom = m_vscrollbar->getPos() == m_bottom_scroll_pos; 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); + if (is_bottom || move_bottom) + m_vscrollbar->setPos(m_bottom_scroll_pos); } else { m_vscrollbar->setMax(0); m_vscrollbar->setPos(0); @@ -1066,6 +1450,21 @@ void GUIChatConsole::updateVScrollBar() m_vscrollbar->setPageSize(page_size); } + if (buf.getDelFormatted() > 0) { + bool is_bottom = m_vscrollbar->getPos() == m_bottom_scroll_pos; + + if (!is_bottom && ! move_bottom) { + s32 pos = m_vscrollbar->getPos() - buf.getDelFormatted(); + + if (pos >= 0) + m_vscrollbar->setPos(pos); + } + + m_mark_begin.scroll -= buf.getDelFormatted(); + m_mark_end.scroll -= buf.getDelFormatted(); + buf.resetDelFormatted(); + } + if (m_vscrollbar->getPos() != buf.getScrollPos()) { if (buf.getScrollPos() >= 0) { s32 deltaScrollY = m_vscrollbar->getPos() - buf.getScrollPos(); @@ -1081,6 +1480,42 @@ void GUIChatConsole::updateVScrollBar() } } +void GUIChatConsole::onLinesModified() +{ + if (m_mark_begin.selection_type == ChatSelection::SELECTION_HISTORY && + m_mark_end.selection_type == ChatSelection::SELECTION_HISTORY) { + + ChatBuffer& buf = m_chat_backend->getConsoleBuffer(); + const ChatLine& first_line = buf.getLine(0); + int first_line_index = first_line.line_index; + + if (m_mark_begin.line_index < first_line_index || + m_mark_end.line_index < first_line_index) { + m_mark_begin.reset(); + m_mark_end.reset(); + m_cursor_press_pos.reset(); + m_history_marking = false; + m_long_press = false; + } + } + + updateVScrollBar(true); +} + +void GUIChatConsole::onPromptModified() +{ + if (m_mark_begin.selection_type == ChatSelection::SELECTION_PROMPT) + m_mark_begin.reset(); + if (m_mark_end.selection_type == ChatSelection::SELECTION_PROMPT) + m_mark_end.reset(); + if (m_cursor_press_pos.selection_type == ChatSelection::SELECTION_PROMPT) + m_cursor_press_pos.reset(); + if (m_prompt_marking) { + m_prompt_marking = false; + m_long_press = false; + } +} + bool GUIChatConsole::hasFocus() { if (Environment->hasFocus(this)) diff --git a/src/gui/guiChatConsole.h b/src/gui/guiChatConsole.h index b57375263..1ff952b81 100644 --- a/src/gui/guiChatConsole.h +++ b/src/gui/guiChatConsole.h @@ -27,14 +27,20 @@ with this program; if not, write to the Free Software Foundation, Inc., struct ChatSelection { - ChatSelection() : initialized(false), scroll(0), row(0), row_buf(0), - line(0), fragment(0), character(0), x_max(false) {}; + enum SelectionType { + SELECTION_NONE, + SELECTION_HISTORY, + SELECTION_PROMPT + }; + + ChatSelection() : selection_type(SELECTION_NONE), scroll(0), row(0), + line_index(0), line(0), fragment(0), character(0), x_max(false) {}; void reset() { - initialized = false; + selection_type = SELECTION_NONE; scroll = 0; row = 0; - row_buf = 0; + line_index = 0; line = 0; fragment = 0; character = 0; @@ -42,29 +48,47 @@ struct ChatSelection } 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); + if (selection_type == SELECTION_HISTORY && + other.selection_type == SELECTION_HISTORY) { + return (row + scroll == other.row + other.scroll && + line_index == other.line_index && + line == other.line && + fragment == other.fragment && + character == other.character && + x_max == other.x_max); + + } else { + return (scroll + character == other.scroll + 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); + if (selection_type == SELECTION_HISTORY && + other.selection_type == SELECTION_HISTORY) { + if (row + scroll != other.row + other.scroll) + return (row + scroll < other.row + other.scroll); + if (line_index != other.line_index) + return (line_index < other.line_index); + 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; + return false; + + } else { + if (scroll + character != other.scroll + other.character) + return (scroll + character < other.scroll + other.character); + if (x_max != other.x_max) + return (x_max < other.x_max); + + return false; + } } bool operator> (const ChatSelection &other) { @@ -83,10 +107,10 @@ struct ChatSelection return !this->operator==(other); } - bool initialized; + SelectionType selection_type; int scroll; int row; - int row_buf; + int line_index; unsigned int line; unsigned int fragment; unsigned int character; @@ -149,6 +173,9 @@ public: bool getAndroidChatOpen() { return m_android_chat_open; } void setAndroidChatOpen(bool value) { m_android_chat_open = value; } + void onLinesModified(); + void onPromptModified(); + static GUIChatConsole* getChatConsole() { return m_chat_console; } private: @@ -162,9 +189,14 @@ private: void drawPrompt(); ChatSelection getCursorPos(s32 x, s32 y); + ChatSelection getPromptCursorPos(s32 x, s32 y); + ChatSelection getCurrentPromptCursorPos(); irr::core::stringc getSelectedText(); + irr::core::stringc getPromptSelectedText(); + void movePromptCursor(s32 x, s32 y); + void deletePromptSelection(); void createVScrollBar(); - void updateVScrollBar(); + void updateVScrollBar(bool force_update = false, bool move_bottom = false); private: static GUIChatConsole* m_chat_console; @@ -213,7 +245,8 @@ private: ChatSelection m_mark_begin; ChatSelection m_mark_end; - bool m_mouse_marking = false; + bool m_history_marking = false; + bool m_prompt_marking = false; bool m_long_press = false; ChatSelection m_cursor_press_pos;