summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpecksin <78765996+pecksin@users.noreply.github.com>2021-06-20 11:20:24 -0400
committerGitHub <noreply@github.com>2021-06-20 17:20:24 +0200
commit1805775f3d54043c3b1e75e47b9b85e3b12bab00 (patch)
tree64037a771d4e7022fbae593d3ae775774fbba177
parente1b297a14baa8849711896677691a8d4fe855dcc (diff)
downloadminetest-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.
-rw-r--r--builtin/settingtypes.txt6
-rw-r--r--minetest.conf.example8
-rw-r--r--po/minetest.pot9
-rw-r--r--src/chat.cpp133
-rw-r--r--src/chat.h8
-rw-r--r--src/defaultsettings.cpp2
-rw-r--r--src/gui/guiChatConsole.cpp98
-rw-r--r--src/gui/guiChatConsole.h8
8 files changed, 242 insertions, 30 deletions
diff --git a/builtin/settingtypes.txt b/builtin/settingtypes.txt
index 57857cabb..fd7d8b9b9 100644
--- a/builtin/settingtypes.txt
+++ b/builtin/settingtypes.txt
@@ -973,6 +973,12 @@ mute_sound (Mute sound) bool false
[Client]
+# Clickable weblinks (middle-click or ctrl-left-click) enabled in chat console output.
+clickable_chat_weblinks (Chat weblinks) bool false
+
+# Optional override for chat weblink color.
+chat_weblink_color (Weblink color) string
+
[*Network]
# Address to connect to.
diff --git a/minetest.conf.example b/minetest.conf.example
index 718cb0c75..b252f4f70 100644
--- a/minetest.conf.example
+++ b/minetest.conf.example
@@ -1155,6 +1155,14 @@
# Client
#
+# If enabled, http links in chat can be middle-clicked or ctrl-left-clicked to open the link in the OS's default web browser.
+# type: bool
+# clickable_chat_weblinks = false
+
+# If clickable_chat_weblinks is enabled, specify the color (as 24-bit hexadecimal) of weblinks in chat.
+# type: string
+# chat_weblink_color = #8888FF
+
## Network
# Address to connect to.
diff --git a/po/minetest.pot b/po/minetest.pot
index 4ed1e2434..53b706f5f 100644
--- a/po/minetest.pot
+++ b/po/minetest.pot
@@ -6551,3 +6551,12 @@ msgid ""
"be queued.\n"
"This should be lower than curl_parallel_limit."
msgstr ""
+
+#: src/gui/guiChatConsole.cpp
+msgid "Opening webpage"
+msgstr ""
+
+#: src/gui/guiChatConsole.cpp
+msgid "Failed to open webpage"
+msgstr ""
+
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++;
}
diff --git a/src/chat.h b/src/chat.h
index 0b98e4d3c..aabb0821e 100644
--- a/src/chat.h
+++ b/src/chat.h
@@ -57,6 +57,8 @@ struct ChatFormattedFragment
EnrichedString text;
// starting column
u32 column;
+ // web link is empty for most frags
+ std::string weblink;
// formatting
//u8 bold:1;
};
@@ -118,6 +120,7 @@ public:
std::vector<ChatFormattedLine>& destination) const;
void resize(u32 scrollback);
+
protected:
s32 getTopScrollPos() const;
s32 getBottomScrollPos() const;
@@ -138,6 +141,11 @@ private:
std::vector<ChatFormattedLine> m_formatted;
// Empty formatted line, for error returns
ChatFormattedLine m_empty_formatted_line;
+
+ // Enable clickable chat weblinks
+ bool m_cache_clickable_chat_weblinks;
+ // Color of clickable chat weblinks
+ irr::video::SColor m_cache_chat_weblink_color;
};
class ChatPrompt
diff --git a/src/defaultsettings.cpp b/src/defaultsettings.cpp
index 0895bf898..6791fccf5 100644
--- a/src/defaultsettings.cpp
+++ b/src/defaultsettings.cpp
@@ -65,6 +65,8 @@ void set_default_settings()
settings->setDefault("max_out_chat_queue_size", "20");
settings->setDefault("pause_on_lost_focus", "false");
settings->setDefault("enable_register_confirmation", "true");
+ settings->setDefault("clickable_chat_weblinks", "false");
+ settings->setDefault("chat_weblink_color", "#8888FF");
// Keymap
settings->setDefault("remote_port", "30000");
diff --git a/src/gui/guiChatConsole.cpp b/src/gui/guiChatConsole.cpp
index baaaea5e8..85617d862 100644
--- a/src/gui/guiChatConsole.cpp
+++ b/src/gui/guiChatConsole.cpp
@@ -41,6 +41,10 @@ inline u32 clamp_u8(s32 value)
return (u32) MYMIN(MYMAX(value, 0), 255);
}
+inline bool isInCtrlKeys(const irr::EKEY_CODE& kc)
+{
+ return kc == KEY_LCONTROL || kc == KEY_RCONTROL || kc == KEY_CONTROL;
+}
GUIChatConsole::GUIChatConsole(
gui::IGUIEnvironment* env,
@@ -91,6 +95,10 @@ GUIChatConsole::GUIChatConsole(
// set default cursor options
setCursor(true, true, 2.0, 0.1);
+
+ // track ctrl keys for mouse event
+ m_is_ctrl_down = false;
+ m_cache_clickable_chat_weblinks = g_settings->getBool("clickable_chat_weblinks");
}
GUIChatConsole::~GUIChatConsole()
@@ -405,8 +413,21 @@ bool GUIChatConsole::OnEvent(const SEvent& event)
ChatPrompt &prompt = m_chat_backend->getPrompt();
- if(event.EventType == EET_KEY_INPUT_EVENT && event.KeyInput.PressedDown)
+ if (event.EventType == EET_KEY_INPUT_EVENT && !event.KeyInput.PressedDown)
+ {
+ // CTRL up
+ if (isInCtrlKeys(event.KeyInput.Key))
+ {
+ m_is_ctrl_down = false;
+ }
+ }
+ else if(event.EventType == EET_KEY_INPUT_EVENT && event.KeyInput.PressedDown)
{
+ // CTRL down
+ if (isInCtrlKeys(event.KeyInput.Key)) {
+ m_is_ctrl_down = true;
+ }
+
// Key input
if (KeyPress(event.KeyInput) == getKeySetting("keymap_console")) {
closeConsole();
@@ -613,11 +634,24 @@ bool GUIChatConsole::OnEvent(const SEvent& event)
}
else if(event.EventType == EET_MOUSE_INPUT_EVENT)
{
- if(event.MouseInput.Event == EMIE_MOUSE_WHEEL)
+ if (event.MouseInput.Event == EMIE_MOUSE_WHEEL)
{
s32 rows = myround(-3.0 * event.MouseInput.Wheel);
m_chat_backend->scroll(rows);
}
+ // Middle click or ctrl-click opens weblink, if enabled in config
+ else if(m_cache_clickable_chat_weblinks && (
+ event.MouseInput.Event == EMIE_MMOUSE_PRESSED_DOWN ||
+ (event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN && m_is_ctrl_down)
+ ))
+ {
+ // If clicked within console output region
+ if (event.MouseInput.Y / m_fontsize.Y < (m_height / m_fontsize.Y) - 1 )
+ {
+ // Translate pixel position to font position
+ middleClick(event.MouseInput.X / m_fontsize.X, event.MouseInput.Y / m_fontsize.Y);
+ }
+ }
}
#if (IRRLICHT_VERSION_MT_REVISION >= 2)
else if(event.EventType == EET_STRING_INPUT_EVENT)
@@ -640,3 +674,63 @@ void GUIChatConsole::setVisible(bool visible)
}
}
+void GUIChatConsole::middleClick(s32 col, s32 row)
+{
+ // Prevent accidental rapid clicking
+ static u64 s_oldtime = 0;
+ u64 newtime = porting::getTimeMs();
+
+ // 0.6 seconds should suffice
+ if (newtime - s_oldtime < 600)
+ return;
+ s_oldtime = newtime;
+
+ const std::vector<ChatFormattedFragment> &
+ frags = m_chat_backend->getConsoleBuffer().getFormattedLine(row).fragments;
+ std::string weblink = ""; // from frag meta
+
+ // Identify targetted fragment, if exists
+ int indx = frags.size() - 1;
+ if (indx < 0) {
+ // Invalid row, frags is empty
+ return;
+ }
+ // Scan from right to left, offset by 1 font space because left margin
+ while (indx > -1 && (u32)col < frags[indx].column + 1) {
+ --indx;
+ }
+ if (indx > -1) {
+ weblink = frags[indx].weblink;
+ // Note if(indx < 0) then a frag somehow had a corrupt column field
+ }
+
+ /*
+ // Debug help. Please keep this in case adjustments are made later.
+ std::string ws;
+ ws = "Middleclick: (" + std::to_string(col) + ',' + std::to_string(row) + ')' + " frags:";
+ // show all frags <position>(<length>) for the clicked row
+ for (u32 i=0;i<frags.size();++i) {
+ if (indx == int(i))
+ // tag the actual clicked frag
+ ws += '*';
+ ws += std::to_string(frags.at(i).column) + '('
+ + std::to_string(frags.at(i).text.size()) + "),";
+ }
+ actionstream << ws << std::endl;
+ */
+
+ // User notification
+ if (weblink.size() != 0) {
+ std::ostringstream msg;
+ msg << " * ";
+ if (porting::open_url(weblink)) {
+ msg << gettext("Opening webpage");
+ }
+ else {
+ msg << gettext("Failed to open webpage");
+ }
+ msg << " '" << weblink << "'";
+ msg.flush();
+ m_chat_backend->addUnparsedMessage(utf8_to_wide(msg.str()));
+ }
+}
diff --git a/src/gui/guiChatConsole.h b/src/gui/guiChatConsole.h
index 1152f2b2d..32628f0d8 100644
--- a/src/gui/guiChatConsole.h
+++ b/src/gui/guiChatConsole.h
@@ -84,6 +84,9 @@ private:
void drawText();
void drawPrompt();
+ // If clicked fragment has a web url, send it to the system default web browser
+ void middleClick(s32 col, s32 row);
+
private:
ChatBackend* m_chat_backend;
Client* m_client;
@@ -126,4 +129,9 @@ private:
// font
gui::IGUIFont *m_font = nullptr;
v2u32 m_fontsize;
+
+ // Enable clickable chat weblinks
+ bool m_cache_clickable_chat_weblinks;
+ // Track if a ctrl key is currently held down
+ bool m_is_ctrl_down;
};