diff options
author | pecksin <78765996+pecksin@users.noreply.github.com> | 2021-06-20 11:20:24 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-06-20 17:20:24 +0200 |
commit | 1805775f3d54043c3b1e75e47b9b85e3b12bab00 (patch) | |
tree | 64037a771d4e7022fbae593d3ae775774fbba177 /src/chat.cpp | |
parent | e1b297a14baa8849711896677691a8d4fe855dcc (diff) | |
download | minetest-1805775f3d54043c3b1e75e47b9b85e3b12bab00.tar.gz minetest-1805775f3d54043c3b1e75e47b9b85e3b12bab00.tar.bz2 minetest-1805775f3d54043c3b1e75e47b9b85e3b12bab00.zip |
Make chat web links clickable (#11092)
If enabled in minetest.conf, provides colored, clickable (middle-mouse or ctrl-left-mouse) weblinks in chat output, to open the OS' default web browser.
Diffstat (limited to 'src/chat.cpp')
-rw-r--r-- | src/chat.cpp | 133 |
1 files changed, 105 insertions, 28 deletions
diff --git a/src/chat.cpp b/src/chat.cpp index c9317a079..e44d73ac0 100644 --- a/src/chat.cpp +++ b/src/chat.cpp @@ -35,6 +35,17 @@ ChatBuffer::ChatBuffer(u32 scrollback): if (m_scrollback == 0) m_scrollback = 1; m_empty_formatted_line.first = true; + + 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(const std::wstring &name, const std::wstring &text) @@ -263,78 +274,144 @@ u32 ChatBuffer::formatChatLine(const ChatLine& line, u32 cols, //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(next_frags.begin()); - } - else - { + } 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 (iswspace(line.text.getString()[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++; } |