X-Git-Url: https://git.lizzy.rs/?a=blobdiff_plain;f=src%2Fchat.cpp;h=92df038e83ab0f69d42ab07c699d526fccfe271a;hb=da71e86633d0b27cd02d7aac9fdac625d141ca13;hp=b9d115bd088c937fb7934a3c23ec252696d777ca;hpb=d0ea6f9920d30f46d1f5d44e8823a8d932f9f29d;p=minetest.git diff --git a/src/chat.cpp b/src/chat.cpp index b9d115bd0..92df038e8 100644 --- a/src/chat.cpp +++ b/src/chat.cpp @@ -1,6 +1,6 @@ /* -Minetest-c55 -Copyright (C) 2011 celeron55, Perttu Ahola +Minetest +Copyright (C) 2013 celeron55, Perttu Ahola 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 @@ -18,38 +18,44 @@ with this program; if not, write to the Free Software Foundation, Inc., */ #include "chat.h" -#include "debug.h" -#include + +#include #include #include + +#include "config.h" +#include "debug.h" +#include "util/strfnd.h" #include "util/string.h" #include "util/numeric.h" ChatBuffer::ChatBuffer(u32 scrollback): - m_scrollback(scrollback), - m_unformatted(), - m_cols(0), - m_rows(0), - m_scroll(0), - m_formatted(), - m_empty_formatted_line() + m_scrollback(scrollback) { if (m_scrollback == 0) m_scrollback = 1; m_empty_formatted_line.first = true; -} -ChatBuffer::~ChatBuffer() -{ + m_cache_clickable_chat_weblinks = false; + // Curses mode cannot access g_settings here + if (g_settings != nullptr) { + m_cache_clickable_chat_weblinks = g_settings->getBool("clickable_chat_weblinks"); + if (m_cache_clickable_chat_weblinks) { + std::string colorval = g_settings->get("chat_weblink_color"); + parseColorString(colorval, m_cache_chat_weblink_color, false, 255); + m_cache_chat_weblink_color.setAlpha(255); + } + } } -void ChatBuffer::addLine(std::wstring name, std::wstring text) +void ChatBuffer::addLine(const std::wstring &name, const std::wstring &text) { + m_lines_modified = true; + ChatLine line(name, text); m_unformatted.push_back(line); - if (m_rows > 0) - { + 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); @@ -58,8 +64,7 @@ void ChatBuffer::addLine(std::wstring name, std::wstring text) } // Limit number of lines by m_scrollback - if (m_unformatted.size() > m_scrollback) - { + if (m_unformatted.size() > m_scrollback) { deleteOldest(m_unformatted.size() - m_scrollback); } } @@ -69,6 +74,7 @@ void ChatBuffer::clear() m_unformatted.clear(); m_formatted.clear(); m_scroll = 0; + m_lines_modified = true; } u32 ChatBuffer::getLineCount() const @@ -76,38 +82,32 @@ u32 ChatBuffer::getLineCount() const return m_unformatted.size(); } -u32 ChatBuffer::getScrollback() const -{ - return m_scrollback; -} - const ChatLine& ChatBuffer::getLine(u32 index) const { - assert(index < getLineCount()); + assert(index < getLineCount()); // pre-condition return m_unformatted[index]; } void ChatBuffer::step(f32 dtime) { - for (u32 i = 0; i < m_unformatted.size(); ++i) - { - m_unformatted[i].age += dtime; + for (ChatLine &line : m_unformatted) { + line.age += dtime; } } void ChatBuffer::deleteOldest(u32 count) { + bool at_bottom = (m_scroll == getBottomScrollPos()); + u32 del_unformatted = 0; u32 del_formatted = 0; - while (count > 0 && del_unformatted < m_unformatted.size()) - { + while (count > 0 && del_unformatted < m_unformatted.size()) { ++del_unformatted; // keep m_formatted in sync - if (del_formatted < m_formatted.size()) - { - assert(m_formatted[del_formatted].first); + if (del_formatted < m_formatted.size()) { + sanity_check(m_formatted[del_formatted].first); ++del_formatted; while (del_formatted < m_formatted.size() && !m_formatted[del_formatted].first) @@ -117,8 +117,16 @@ void ChatBuffer::deleteOldest(u32 count) --count; } - m_unformatted.erase(0, del_unformatted); - m_formatted.erase(0, del_formatted); + m_unformatted.erase(m_unformatted.begin(), m_unformatted.begin() + del_unformatted); + m_formatted.erase(m_formatted.begin(), m_formatted.begin() + del_formatted); + + if (del_unformatted > 0) + m_lines_modified = true; + + if (at_bottom) + m_scroll = getBottomScrollPos(); + else + scrollAbsolute(m_scroll - del_formatted); } void ChatBuffer::deleteByAge(f32 maxAge) @@ -129,11 +137,6 @@ void ChatBuffer::deleteByAge(f32 maxAge) deleteOldest(count); } -u32 ChatBuffer::getColumns() const -{ - return m_cols; -} - u32 ChatBuffer::getRows() const { return m_rows; @@ -151,7 +154,7 @@ void ChatBuffer::reformat(u32 cols, u32 rows) } else if (cols != m_cols || rows != m_rows) { - // TODO: Avoid reformatting ALL lines (even inivisble ones) + // TODO: Avoid reformatting ALL lines (even invisible ones) // each time the console size changes. // Find out the scroll position in *unformatted* lines @@ -200,8 +203,8 @@ const ChatFormattedLine& ChatBuffer::getFormattedLine(u32 row) const s32 index = m_scroll + (s32) row; if (index >= 0 && index < (s32) m_formatted.size()) return m_formatted[index]; - else - return m_empty_formatted_line; + + return m_empty_formatted_line; } void ChatBuffer::scroll(s32 rows) @@ -226,16 +229,11 @@ void ChatBuffer::scrollBottom() m_scroll = getBottomScrollPos(); } -void ChatBuffer::scrollTop() -{ - m_scroll = getTopScrollPos(); -} - u32 ChatBuffer::formatChatLine(const ChatLine& line, u32 cols, - core::array& destination) const + std::vector& destination) const { u32 num_added = 0; - core::array next_frags; + std::vector next_frags; ChatFormattedLine next_line; ChatFormattedFragment temp_frag; u32 out_column = 0; @@ -243,8 +241,7 @@ u32 ChatBuffer::formatChatLine(const ChatLine& line, u32 cols, u32 hanging_indentation = 0; // Format the sender name and produce fragments - if (!line.name.empty()) - { + if (!line.name.empty()) { temp_frag.text = L"<"; temp_frag.column = 0; //temp_frag.bold = 0; @@ -259,96 +256,160 @@ u32 ChatBuffer::formatChatLine(const ChatLine& line, u32 cols, next_frags.push_back(temp_frag); } + std::wstring name_sanitized = line.name.c_str(); + // Choose an indentation level - if (line.name.empty()) - { + if (line.name.empty()) { // Server messages hanging_indentation = 0; - } - else if (line.name.size() + 3 <= cols/2) - { + } else if (name_sanitized.size() + 3 <= cols/2) { // Names shorter than about half the console width hanging_indentation = line.name.size() + 3; - } - else - { + } else { // Very long names hanging_indentation = 2; } + //EnrichedString line_text(line.text); next_line.first = true; - bool text_processing = false; + // Set/use forced newline after the last frag in each line + bool mark_newline = false; // Produce fragments and layout them into lines - while (!next_frags.empty() || in_pos < line.text.size()) - { + while (!next_frags.empty() || in_pos < line.text.size()) { + mark_newline = false; // now using this to USE line-end frag + // Layout fragments into lines - while (!next_frags.empty()) - { + while (!next_frags.empty()) { ChatFormattedFragment& frag = next_frags[0]; - if (frag.text.size() <= cols - out_column) - { + + // Force newline after this frag, if marked + if (frag.column == INT_MAX) + mark_newline = true; + + if (frag.text.size() <= cols - out_column) { // Fragment fits into current line frag.column = out_column; next_line.fragments.push_back(frag); out_column += frag.text.size(); - next_frags.erase(0, 1); - } - else - { + next_frags.erase(next_frags.begin()); + } else { // Fragment does not fit into current line // So split it up temp_frag.text = frag.text.substr(0, cols - out_column); temp_frag.column = out_column; - //temp_frag.bold = frag.bold; + temp_frag.weblink = frag.weblink; + next_line.fragments.push_back(temp_frag); frag.text = frag.text.substr(cols - out_column); + frag.column = 0; out_column = cols; } - if (out_column == cols || text_processing) - { + + if (out_column == cols || mark_newline) { // End the current line destination.push_back(next_line); num_added++; next_line.fragments.clear(); next_line.first = false; - out_column = text_processing ? hanging_indentation : 0; + out_column = hanging_indentation; + mark_newline = false; } } - // Produce fragment - if (in_pos < line.text.size()) - { - u32 remaining_in_input = line.text.size() - in_pos; - u32 remaining_in_output = cols - out_column; + // Produce fragment(s) for next formatted line + if (!(in_pos < line.text.size())) + continue; + + const std::wstring &linestring = line.text.getString(); + u32 remaining_in_output = cols - out_column; + size_t http_pos = std::wstring::npos; + mark_newline = false; // now using this to SET line-end frag + // Construct all frags for next output line + while (!mark_newline) { // Determine a fragment length <= the minimum of // remaining_in_{in,out}put. Try to end the fragment // on a word boundary. - u32 frag_length = 1, space_pos = 0; + u32 frag_length = 0, space_pos = 0; + u32 remaining_in_input = line.text.size() - in_pos; + + if (m_cache_clickable_chat_weblinks) { + // Note: unsigned(-1) on fail + http_pos = linestring.find(L"https://", in_pos); + if (http_pos == std::wstring::npos) + http_pos = linestring.find(L"http://", in_pos); + if (http_pos != std::wstring::npos) + http_pos -= in_pos; + } + while (frag_length < remaining_in_input && - frag_length < remaining_in_output) - { - if (isspace(line.text[in_pos + frag_length])) + frag_length < remaining_in_output) { + if (iswspace(linestring[in_pos + frag_length])) space_pos = frag_length; ++frag_length; } + + if (http_pos >= remaining_in_output) { + // Http not in range, grab until space or EOL, halt as normal. + // Note this works because (http_pos = npos) is unsigned(-1) + + mark_newline = true; + } else if (http_pos == 0) { + // At http, grab ALL until FIRST whitespace or end marker. loop. + // If at end of string, next loop will be empty string to mark end of weblink. + + frag_length = 6; // Frag is at least "http://" + + // Chars to mark end of weblink + // TODO? replace this with a safer (slower) regex whitelist? + static const std::wstring delim_chars = L"\'\";,"; + wchar_t tempchar = linestring[in_pos+frag_length]; + while (frag_length < remaining_in_input && + !iswspace(tempchar) && + delim_chars.find(tempchar) == std::wstring::npos) { + ++frag_length; + tempchar = linestring[in_pos+frag_length]; + } + + space_pos = frag_length - 1; + // This frag may need to be force-split. That's ok, urls aren't "words" + if (frag_length >= remaining_in_output) { + mark_newline = true; + } + } else { + // Http in range, grab until http, loop + + space_pos = http_pos - 1; + frag_length = http_pos; + } + + // Include trailing space in current frag if (space_pos != 0 && frag_length < remaining_in_input) frag_length = space_pos + 1; temp_frag.text = line.text.substr(in_pos, frag_length); - temp_frag.column = 0; - //temp_frag.bold = 0; + // A hack so this frag remembers mark_newline for the layout phase + temp_frag.column = mark_newline ? INT_MAX : 0; + + if (http_pos == 0) { + // Discard color stuff from the source frag + temp_frag.text = EnrichedString(temp_frag.text.getString()); + temp_frag.text.setDefaultColor(m_cache_chat_weblink_color); + // Set weblink in the frag meta + temp_frag.weblink = wide_to_utf8(temp_frag.text.getString()); + } else { + temp_frag.weblink.clear(); + } next_frags.push_back(temp_frag); in_pos += frag_length; - text_processing = true; + remaining_in_output -= std::min(frag_length, remaining_in_output); } } // End the last line - if (num_added == 0 || !next_line.fragments.empty()) - { + if (num_added == 0 || !next_line.fragments.empty()) { destination.push_back(next_line); num_added++; } @@ -362,10 +423,11 @@ s32 ChatBuffer::getTopScrollPos() const s32 rows = (s32) m_rows; if (rows == 0) return 0; - else if (formatted_count <= rows) + + if (formatted_count <= rows) return formatted_count - rows; - else - return 0; + + return 0; } s32 ChatBuffer::getBottomScrollPos() const @@ -374,27 +436,21 @@ s32 ChatBuffer::getBottomScrollPos() const s32 rows = (s32) m_rows; if (rows == 0) return 0; - else - return formatted_count - rows; -} - + return formatted_count - rows; +} -ChatPrompt::ChatPrompt(std::wstring prompt, u32 history_limit): - m_prompt(prompt), - m_line(L""), - m_history(), - m_history_index(0), - m_history_limit(history_limit), - m_cols(0), - m_view(0), - m_cursor(0), - m_nick_completion_start(0), - m_nick_completion_end(0) +void ChatBuffer::resize(u32 scrollback) { + m_scrollback = scrollback; + if (m_unformatted.size() > m_scrollback) + deleteOldest(m_unformatted.size() - m_scrollback); } -ChatPrompt::~ChatPrompt() + +ChatPrompt::ChatPrompt(const std::wstring &prompt, u32 history_limit): + m_prompt(prompt), + m_history_limit(history_limit) { } @@ -407,20 +463,28 @@ void ChatPrompt::input(wchar_t ch) m_nick_completion_end = 0; } -std::wstring ChatPrompt::submit() +void ChatPrompt::input(const std::wstring &str) { - std::wstring line = m_line; - m_line.clear(); - if (!line.empty()) + m_line.insert(m_cursor, str); + m_cursor += str.size(); + clampView(); + m_nick_completion_start = 0; + m_nick_completion_end = 0; +} + +void ChatPrompt::addToHistory(const std::wstring &line) +{ + if (!line.empty() && + (m_history.size() == 0 || m_history.back() != line)) { + // Remove all duplicates + m_history.erase(std::remove(m_history.begin(), m_history.end(), + line), m_history.end()); + // Push unique line m_history.push_back(line); + } if (m_history.size() > m_history_limit) - m_history.erase(0); + m_history.erase(m_history.begin()); m_history_index = m_history.size(); - m_view = 0; - m_cursor = 0; - m_nick_completion_start = 0; - m_nick_completion_end = 0; - return line; } void ChatPrompt::clear() @@ -432,13 +496,15 @@ void ChatPrompt::clear() m_nick_completion_end = 0; } -void ChatPrompt::replace(std::wstring line) +std::wstring ChatPrompt::replace(const std::wstring &line) { + std::wstring old_line = m_line; m_line = line; m_view = m_cursor = line.size(); clampView(); m_nick_completion_start = 0; m_nick_completion_end = 0; + return old_line; } void ChatPrompt::historyPrev() @@ -464,7 +530,7 @@ void ChatPrompt::historyNext() } } -void ChatPrompt::nickCompletion(const core::list& names, bool backwards) +void ChatPrompt::nickCompletion(const std::list& names, bool backwards) { // Two cases: // (a) m_nick_completion_start == m_nick_completion_end == 0 @@ -482,9 +548,9 @@ void ChatPrompt::nickCompletion(const core::list& names, bool back { // no previous nick completion is active prefix_start = prefix_end = m_cursor; - while (prefix_start > 0 && !isspace(m_line[prefix_start-1])) + while (prefix_start > 0 && !iswspace(m_line[prefix_start-1])) --prefix_start; - while (prefix_end < m_line.size() && !isspace(m_line[prefix_end])) + while (prefix_end < m_line.size() && !iswspace(m_line[prefix_end])) ++prefix_end; if (prefix_start == prefix_end) return; @@ -492,19 +558,16 @@ void ChatPrompt::nickCompletion(const core::list& names, bool back std::wstring prefix = m_line.substr(prefix_start, prefix_end - prefix_start); // find all names that start with the selected prefix - core::array completions; - for (core::list::ConstIterator - i = names.begin(); - i != names.end(); i++) - { - if (str_starts_with(*i, prefix, true)) - { - std::wstring completion = *i; + std::vector completions; + for (const std::string &name : names) { + std::wstring completion = utf8_to_wide(name); + if (str_starts_with(completion, prefix, true)) { if (prefix_start == 0) - completion += L":"; + completion += L": "; completions.push_back(completion); } } + if (completions.empty()) return; @@ -513,7 +576,7 @@ void ChatPrompt::nickCompletion(const core::list& names, bool back u32 replacement_index = 0; if (!initial) { - while (word_end < m_line.size() && !isspace(m_line[word_end])) + while (word_end < m_line.size() && !iswspace(m_line[word_end])) ++word_end; std::wstring word = m_line.substr(prefix_start, word_end - prefix_start); @@ -531,8 +594,8 @@ void ChatPrompt::nickCompletion(const core::list& names, bool back } } } - std::wstring replacement = completions[replacement_index] + L" "; - if (word_end < m_line.size() && isspace(word_end)) + std::wstring replacement = completions[replacement_index]; + if (word_end < m_line.size() && iswspace(m_line[word_end])) ++word_end; // replace existing word with replacement word, @@ -580,54 +643,60 @@ void ChatPrompt::cursorOperation(CursorOp op, CursorOpDir dir, CursorOpScope sco s32 length = m_line.size(); s32 increment = (dir == CURSOROP_DIR_RIGHT) ? 1 : -1; - if (scope == CURSOROP_SCOPE_CHARACTER) - { + switch (scope) { + case CURSOROP_SCOPE_CHARACTER: new_cursor += increment; - } - else if (scope == CURSOROP_SCOPE_WORD) - { - if (increment > 0) - { + break; + case CURSOROP_SCOPE_WORD: + if (dir == CURSOROP_DIR_RIGHT) { // skip one word to the right - while (new_cursor < length && isspace(m_line[new_cursor])) + while (new_cursor < length && iswspace(m_line[new_cursor])) new_cursor++; - while (new_cursor < length && !isspace(m_line[new_cursor])) + while (new_cursor < length && !iswspace(m_line[new_cursor])) new_cursor++; - while (new_cursor < length && isspace(m_line[new_cursor])) + while (new_cursor < length && iswspace(m_line[new_cursor])) new_cursor++; - } - else - { + } else { // skip one word to the left - while (new_cursor >= 1 && isspace(m_line[new_cursor - 1])) + while (new_cursor >= 1 && iswspace(m_line[new_cursor - 1])) new_cursor--; - while (new_cursor >= 1 && !isspace(m_line[new_cursor - 1])) + while (new_cursor >= 1 && !iswspace(m_line[new_cursor - 1])) new_cursor--; } - } - else if (scope == CURSOROP_SCOPE_LINE) - { + break; + case CURSOROP_SCOPE_LINE: new_cursor += increment * length; + break; + case CURSOROP_SCOPE_SELECTION: + break; } new_cursor = MYMAX(MYMIN(new_cursor, length), 0); - if (op == CURSOROP_MOVE) - { + switch (op) { + case CURSOROP_MOVE: m_cursor = new_cursor; - } - else if (op == CURSOROP_DELETE) - { - if (new_cursor < old_cursor) - { - m_line.erase(new_cursor, old_cursor - new_cursor); - m_cursor = new_cursor; + m_cursor_len = 0; + break; + case CURSOROP_DELETE: + if (m_cursor_len > 0) { // Delete selected text first + m_line.erase(m_cursor, m_cursor_len); + } else { + m_cursor = MYMIN(new_cursor, old_cursor); + m_line.erase(m_cursor, abs(new_cursor - old_cursor)); } - else if (new_cursor > old_cursor) - { - m_line.erase(old_cursor, new_cursor - old_cursor); - m_cursor = old_cursor; + m_cursor_len = 0; + break; + case CURSOROP_SELECT: + if (scope == CURSOROP_SCOPE_LINE) { + m_cursor = 0; + m_cursor_len = length; + } else { + m_cursor = MYMIN(new_cursor, old_cursor); + m_cursor_len += abs(new_cursor - old_cursor); + m_cursor_len = MYMIN(m_cursor_len, length - m_cursor); } + break; } clampView(); @@ -661,15 +730,12 @@ ChatBackend::ChatBackend(): { } -ChatBackend::~ChatBackend() -{ -} - -void ChatBackend::addMessage(std::wstring name, std::wstring text) +void ChatBackend::addMessage(const std::wstring &name, std::wstring text) { // Note: A message may consist of multiple lines, for example the MOTD. + text = translate_string(text); WStrfnd fnd(text); - while (!fnd.atend()) + while (!fnd.at_end()) { std::wstring line = fnd.next(L"\n"); m_console_buffer.addLine(name, line); @@ -710,19 +776,21 @@ ChatBuffer& ChatBackend::getRecentBuffer() return m_recent_buffer; } -std::wstring ChatBackend::getRecentChat() +EnrichedString ChatBackend::getRecentChat() const { - std::wostringstream stream; - for (u32 i = 0; i < m_recent_buffer.getLineCount(); ++i) - { + EnrichedString result; + for (u32 i = 0; i < m_recent_buffer.getLineCount(); ++i) { const ChatLine& line = m_recent_buffer.getLine(i); if (i != 0) - stream << L"\n"; - if (!line.name.empty()) - stream << L"<" << line.name << L"> "; - stream << line.text; + result += L"\n"; + if (!line.name.empty()) { + result += L"<"; + result += line.name; + result += L"> "; + } + result += line.text; } - return stream.str(); + return result; } ChatPrompt& ChatBackend::getPrompt() @@ -745,6 +813,14 @@ void ChatBackend::clearRecentChat() m_recent_buffer.clear(); } + +void ChatBackend::applySettings() +{ + u32 recent_lines = g_settings->getU32("recent_chat_messages"); + recent_lines = rangelim(recent_lines, 2, 20); + m_recent_buffer.resize(recent_lines); +} + void ChatBackend::step(float dtime) { m_recent_buffer.step(dtime); @@ -765,5 +841,5 @@ void ChatBackend::scrollPageDown() void ChatBackend::scrollPageUp() { - m_console_buffer.scroll(-m_console_buffer.getRows()); + m_console_buffer.scroll(-(s32)m_console_buffer.getRows()); }