summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--minetest.conf.example12
-rw-r--r--po/minetest.pot9
-rw-r--r--src/chat.cpp148
-rw-r--r--src/chat.h8
-rw-r--r--src/client/keycode.cpp9
-rw-r--r--src/client/keycode.h3
-rw-r--r--src/defaultsettings.cpp3
-rw-r--r--src/gui/guiChatConsole.cpp148
-rw-r--r--src/gui/guiChatConsole.h11
9 files changed, 334 insertions, 17 deletions
diff --git a/minetest.conf.example b/minetest.conf.example
index 47c03ff80..1e9f6b2d9 100644
--- a/minetest.conf.example
+++ b/minetest.conf.example
@@ -16,6 +16,18 @@
# Controls
#
+# 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
+
+# If clickable_chat_weblinks is enabled, specify the keys held down to treat left-click as middle-click.
+# type: string (comma-delimited)
+# chat_weblink_ctrl_keys = KEY_LCONTROL,KEY_RCONTROL,KEY_CONTROL
+
# If enabled, you can place blocks at the position (feet + eye level) where you stand.
# This is helpful when working with nodeboxes in small areas.
# type: bool
diff --git a/po/minetest.pot b/po/minetest.pot
index 9881f5032..6b454cf16 100644
--- a/po/minetest.pot
+++ b/po/minetest.pot
@@ -6370,3 +6370,12 @@ msgid ""
"be queued.\n"
"This should be lower than curl_parallel_limit."
msgstr ""
+
+#: src/chat.cpp
+msgid "Opening webpage"
+msgstr ""
+
+#: src/chat.cpp
+msgid "Failed to open webpage"
+msgstr ""
+
diff --git a/src/chat.cpp b/src/chat.cpp
index c9317a079..9bceb3535 100644
--- a/src/chat.cpp
+++ b/src/chat.cpp
@@ -35,6 +35,25 @@ ChatBuffer::ChatBuffer(u32 scrollback):
if (m_scrollback == 0)
m_scrollback = 1;
m_empty_formatted_line.first = true;
+
+ // Curses mode cannot access g_settings here
+ if(g_settings != NULL)
+ {
+ m_cache_clickable_chat_weblinks = g_settings->getBool("clickable_chat_weblinks");
+ if(m_cache_clickable_chat_weblinks)
+ {
+ std::string hexcode = g_settings->get("chat_weblink_color");
+ u32 colorval = strtol(hexcode.c_str(), NULL, 16);
+ u32 redval,greenval,blueval;
+ blueval = colorval % 256;
+ colorval /= 256;
+ greenval = colorval % 256;
+ colorval /= 256;
+ redval = colorval % 256;
+ // discard alpha, if included
+ m_cache_chat_weblink_color = irr::video::SColor(255,redval,greenval,blueval);
+ }
+ }
}
void ChatBuffer::addLine(const std::wstring &name, const std::wstring &text)
@@ -272,6 +291,11 @@ u32 ChatBuffer::formatChatLine(const ChatLine& line, u32 cols,
while (!next_frags.empty())
{
ChatFormattedFragment& frag = next_frags[0];
+
+ // Force text_processing on this frag. hacky.
+ if(frag.column == u32(INT_MAX))
+ text_processing = true;
+
if (frag.text.size() <= cols - out_column)
{
// Fragment fits into current line
@@ -286,9 +310,11 @@ u32 ChatBuffer::formatChatLine(const ChatLine& line, u32 cols,
// 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.meta = frag.meta;
+
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)
@@ -300,10 +326,11 @@ u32 ChatBuffer::formatChatLine(const ChatLine& line, u32 cols,
next_line.first = false;
out_column = text_processing ? hanging_indentation : 0;
+ text_processing = false;
}
}
- // Produce fragment
+ // Produce fragment(s) for next formatted line
if (in_pos < line.text.size())
{
u32 remaining_in_input = line.text.size() - in_pos;
@@ -313,22 +340,111 @@ u32 ChatBuffer::formatChatLine(const ChatLine& line, u32 cols,
// remaining_in_{in,out}put. Try to end the fragment
// on a word boundary.
u32 frag_length = 1, space_pos = 0;
- while (frag_length < remaining_in_input &&
- frag_length < remaining_in_output)
+
+ if(!m_cache_clickable_chat_weblinks)
+ {
+ while (frag_length < remaining_in_input &&
+ frag_length < remaining_in_output)
+ {
+ if (iswspace(line.text.getString()[in_pos + frag_length]))
+ space_pos = frag_length;
+ ++frag_length;
+ }
+ 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;
+ next_frags.push_back(temp_frag);
+ in_pos += frag_length;
+ text_processing = true;
+ }
+ // Unless weblinks are enabled. Then it's slightly more complex
+ else
{
- if (iswspace(line.text.getString()[in_pos + frag_length]))
- space_pos = frag_length;
- ++frag_length;
+ u32 http_pos = u32(std::wstring::npos);
+ text_processing = false; // set FALSE at end of this block
+ bool halt_fragloop = false;
+ while(!halt_fragloop)
+ {
+ remaining_in_input = line.text.size() - in_pos;
+ frag_length = 0;
+ space_pos = 0;
+
+ // Note: unsigned(-1) on fail
+ http_pos = u32(line.text.getString().find(L"https://", std::size_t(in_pos)));
+ if(http_pos == u32(std::wstring::npos))
+ http_pos = u32(line.text.getString().find(L"http://", std::size_t(in_pos)));
+ if(http_pos != u32(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]))
+ space_pos = frag_length;
+ ++frag_length;
+ }
+
+ // Http not in range, grab until space or EOL, halt as normal.
+ if(http_pos > remaining_in_output) // sufficient unless screen is infinitely wide
+ {
+ halt_fragloop = true;
+ // force text processing on THIS frag
+ text_processing = true;
+ }
+ // At http, grab ALL until FIRST whitespace or quote mark. loop.
+ // If at end of string, next loop will be empty string to mark end of weblink. This is intentional.
+ else if(http_pos == 0)
+ {
+ frag_length = 6;
+ while (frag_length < remaining_in_input &&
+ !iswspace(line.text.getString()[in_pos + frag_length]) &&
+ line.text.getString()[in_pos + frag_length] != L'\'' &&
+ line.text.getString()[in_pos + frag_length] != L'\"' &&
+ line.text.getString()[in_pos + frag_length] != L';'
+ )
+ {
+ ++frag_length;
+ }
+ space_pos = frag_length - 1;
+ if(frag_length >= remaining_in_output)
+ {
+ // Force text processing on THIS frag
+ text_processing = true;
+ halt_fragloop = true;
+ }
+ }
+ // Http in range, grab until http, loop
+ else
+ {
+ space_pos = http_pos - 1;
+ frag_length = http_pos;
+ }
+
+ 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 = text_processing ? u32(INT_MAX) : 0;
+ if(http_pos == 0)
+ {
+ temp_frag.meta = wide_to_utf8(temp_frag.text.getString());
+ temp_frag.text = EnrichedString(temp_frag.text.getString());
+ temp_frag.text.setDefaultColor(m_cache_chat_weblink_color);
+ }
+ else
+ {
+ temp_frag.meta = "";
+ }
+ next_frags.push_back(temp_frag);
+ in_pos += frag_length;
+ remaining_in_output -= std::min(frag_length, remaining_in_output);
+ }
+ // handled for fragments individually
+ text_processing = false;
}
- 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;
- next_frags.push_back(temp_frag);
- in_pos += frag_length;
- text_processing = true;
}
}
diff --git a/src/chat.h b/src/chat.h
index 0b98e4d3c..d7d27646f 100644
--- a/src/chat.h
+++ b/src/chat.h
@@ -57,6 +57,8 @@ struct ChatFormattedFragment
EnrichedString text;
// starting column
u32 column;
+ // web link for now. maybe future use?
+ std::string meta;
// 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/client/keycode.cpp b/src/client/keycode.cpp
index ce5214f54..08aff43ce 100644
--- a/src/client/keycode.cpp
+++ b/src/client/keycode.cpp
@@ -386,3 +386,12 @@ irr::EKEY_CODE keyname_to_keycode(const char *name)
{
return lookup_keyname(name).Key;
}
+
+irr::EKEY_CODE keyname_to_keycode_safemode(const char *name)
+{
+ try {
+ return lookup_keyname(name).Key;
+ } catch (UnknownKeycode &e) {
+ return irr::KEY_KEY_CODES_COUNT;
+ }
+}
diff --git a/src/client/keycode.h b/src/client/keycode.h
index 7036705d1..cb3114099 100644
--- a/src/client/keycode.h
+++ b/src/client/keycode.h
@@ -65,3 +65,6 @@ KeyPress getKeySetting(const char *settingname);
void clearKeyCache();
irr::EKEY_CODE keyname_to_keycode(const char *name);
+
+// Now with internal exception handling. return irr::KEY_KEY_CODES_COUNT on fail
+irr::EKEY_CODE keyname_to_keycode_safemode(const char *name);
diff --git a/src/defaultsettings.cpp b/src/defaultsettings.cpp
index 9d155f76c..d5bd56bc9 100644
--- a/src/defaultsettings.cpp
+++ b/src/defaultsettings.cpp
@@ -283,6 +283,9 @@ void set_default_settings()
settings->setDefault("repeat_joystick_button_time", "0.17");
settings->setDefault("joystick_frustum_sensitivity", "170");
settings->setDefault("joystick_deadzone", "2048");
+ settings->setDefault("clickable_chat_weblinks", "false");
+ settings->setDefault("chat_weblink_color", "8888FF");
+ settings->setDefault("chat_weblink_ctrl_keys", "KEY_LCONTROL,KEY_RCONTROL,KEY_CONTROL");
// Main menu
settings->setDefault("main_menu_path", "");
diff --git a/src/gui/guiChatConsole.cpp b/src/gui/guiChatConsole.cpp
index a4e91fe78..fd92cf298 100644
--- a/src/gui/guiChatConsole.cpp
+++ b/src/gui/guiChatConsole.cpp
@@ -90,6 +90,19 @@ GUIChatConsole::GUIChatConsole(
// set default cursor options
setCursor(true, true, 2.0, 0.1);
+
+ m_cache_clickable_chat_weblinks = g_settings->getBool("clickable_chat_weblinks");
+ if(m_cache_clickable_chat_weblinks)
+ {
+ std::string ctrlkeystoparse = g_settings->get("chat_weblink_ctrl_keys");
+ if(setupChatClickCtrlKeys(ctrlkeystoparse) == 0)
+ {
+ // if fail then try again w hardcoded string
+ g_logger.log(LL_WARNING, "Failed to parse chat_weblink_ctrl_keys. Using hardcoded default.");
+ ctrlkeystoparse = "KEY_CONTROL,KEY_LCONTROL,KEY_RCONTROL";
+ setupChatClickCtrlKeys(ctrlkeystoparse);
+ }
+ }
}
GUIChatConsole::~GUIChatConsole()
@@ -404,9 +417,24 @@ bool GUIChatConsole::OnEvent(const SEvent& event)
{
ChatPrompt &prompt = m_chat_backend->getPrompt();
+ static bool isctrldown; // track this for mouse event
- 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))
+ {
+ isctrldown = false;
+ }
+ }
+ else if(event.EventType == EET_KEY_INPUT_EVENT && event.KeyInput.PressedDown)
+ {
+ // CTRL down
+ if(isInCtrlKeys(event.KeyInput.Key))
+ {
+ isctrldown = true;
+ }
+
// Key input
if (KeyPress(event.KeyInput) == getKeySetting("keymap_console")) {
closeConsole();
@@ -618,6 +646,18 @@ bool GUIChatConsole::OnEvent(const SEvent& event)
s32 rows = myround(-3.0 * event.MouseInput.Wheel);
m_chat_backend->scroll(rows);
}
+ // Middle 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 && isctrldown)
+ ))
+ {
+ // because console prompt and hardcoded margins
+ if(event.MouseInput.Y / m_fontsize.Y < (m_height / m_fontsize.Y) - 1 )
+ {
+ middleClick(event.MouseInput.X / m_fontsize.X, event.MouseInput.Y / m_fontsize.Y);
+ }
+ }
}
return Parent ? Parent->OnEvent(event) : false;
@@ -633,3 +673,109 @@ void GUIChatConsole::setVisible(bool visible)
}
}
+// Return how many "ctrl" keycodes successfully found in string, or 0 on fail
+int GUIChatConsole::setupChatClickCtrlKeys(std::string inputline)
+{
+ m_cache_chat_weblink_ctrl_keys.clear();
+
+ irr::EKEY_CODE kc;
+ std::string stemp;
+ size_t startpos = 0, endpos = 0;
+ while(startpos < inputline.size())
+ {
+ // Foreach delimited string,
+ endpos = inputline.find(',', startpos);
+ endpos = std::min(endpos, inputline.find(' ', startpos));
+ // Ignore space/comma
+ if(endpos == startpos)
+ {
+ ++endpos;
+ }
+ // Ignore consecutive space/comma
+ else if(endpos - startpos > 1)
+ {
+ // If valid keycode, add it to cached list
+ stemp = inputline.substr(startpos, endpos - startpos);
+ kc = keyname_to_keycode_safemode(stemp.c_str());
+ if(kc != irr::KEY_KEY_CODES_COUNT)
+ {
+ m_cache_chat_weblink_ctrl_keys.push_back(kc);
+ }
+ else
+ {
+ stemp = "Ignoring unknown keycode '" + stemp + "' for chat_weblink_ctrl_keys, check your conf";
+ g_logger.log(LL_WARNING, stemp);
+ }
+ }
+ startpos = endpos;
+ }
+
+ return m_cache_chat_weblink_ctrl_keys.size();
+}
+
+bool GUIChatConsole::isInCtrlKeys(const irr::EKEY_CODE& kc)
+{
+ // To avoid including <algorithm>
+ for(size_t i=0; i<m_cache_chat_weblink_ctrl_keys.size(); ++i)
+ {
+ if(m_cache_chat_weblink_ctrl_keys.at(i) == kc)
+ return true;
+ }
+ return false;
+}
+
+void GUIChatConsole::middleClick(s32 col, s32 row)
+{
+ // Prevent accidental rapid clicking
+ static u32 oldtime = 0;
+ // seriously..
+ u32 newtime = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
+
+ // 0.6 seconds should suffice
+ if(newtime - oldtime < 600)
+ return;
+ 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 ind = frags.size() - 1;
+ while(u32(col - 1) < frags[ind].column)
+ {
+ --ind;
+ }
+ if(ind > -1)
+ {
+ weblink = frags[ind].meta;
+ }
+
+ // Debug help
+ std::string ws;
+ ws = "Middleclick: (" + std::to_string(col) + ',' + std::to_string(row) + ')' + " frags:";
+ for(u32 i=0;i<frags.size();++i)
+ {
+ if(ind == int(i))
+ ws += '*';
+ ws += std::to_string(frags.at(i).column) + '('
+ + std::to_string(frags.at(i).text.size()) + "),";
+ }
+ g_logger.log(LL_VERBOSE, ws);
+
+ // User notification
+ std::string mesg;
+ if(weblink.size() != 0)
+ {
+ mesg = " * ";
+ if(porting::open_url(weblink))
+ {
+ mesg += gettext("Opening webpage");
+ }
+ else
+ {
+ mesg += gettext("Failed to open webpage");
+ }
+ mesg += " '" + weblink + "'";
+ m_chat_backend->addUnparsedMessage(utf8_to_wide(mesg));
+ }
+}
diff --git a/src/gui/guiChatConsole.h b/src/gui/guiChatConsole.h
index 896342ab0..169fb7eb7 100644
--- a/src/gui/guiChatConsole.h
+++ b/src/gui/guiChatConsole.h
@@ -82,6 +82,12 @@ private:
void drawText();
void drawPrompt();
+ // Clickable weblink stuff
+ int setupChatClickCtrlKeys(std::string inputline);
+ bool isInCtrlKeys(const irr::EKEY_CODE& kc);
+ // 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;
@@ -124,4 +130,9 @@ private:
// font
gui::IGUIFont *m_font = nullptr;
v2u32 m_fontsize;
+
+ // Enable clickable chat weblinks
+ bool m_cache_clickable_chat_weblinks;
+ // Set of "control" keys for weblink mouseclicks
+ std::vector<irr::EKEY_CODE> m_cache_chat_weblink_ctrl_keys;
};