From 2e5bcdf2488ba56f39664c8da0893206aa553497 Mon Sep 17 00:00:00 2001 From: Nimetu Date: Sat, 21 Jan 2017 13:29:34 +0200 Subject: [PATCH] Added: Clickable urls in chat text (issue 282) --- .../gamedev/interfaces_v3/interaction.lua | 14 + .../gamedev/interfaces_v3/interaction.xml | 15 + .../src/interface_v3/chat_text_manager.cpp | 326 ++++++++++++++++-- .../src/interface_v3/chat_text_manager.h | 7 +- 4 files changed, 335 insertions(+), 27 deletions(-) diff --git a/code/ryzom/client/data/gamedev/interfaces_v3/interaction.lua b/code/ryzom/client/data/gamedev/interfaces_v3/interaction.lua index 489484ee8..9f672e961 100644 --- a/code/ryzom/client/data/gamedev/interfaces_v3/interaction.lua +++ b/code/ryzom/client/data/gamedev/interfaces_v3/interaction.lua @@ -537,3 +537,17 @@ function game:openWebIGBrowserHeader() ui.w = ui_webig_browser_w; end end + +------------------------------------------------------------------------------------------------------------ +local SavedUrl = ""; +function game:chatUrl(url) + SavedUrl = url + runAH(nil, "active_menu", "menu=ui:interface:chat_uri_action_menu"); +end +function game:chatUrlCopy() + runAH(nil, "copy_to_clipboard", SavedUrl) +end +function game:chatUrlBrowse() + runAH(nil, "browse", "name=ui:interface:webig:content:html|url=" .. SavedUrl) +end + diff --git a/code/ryzom/client/data/gamedev/interfaces_v3/interaction.xml b/code/ryzom/client/data/gamedev/interfaces_v3/interaction.xml index d6d4a4cba..cbf4b15ce 100644 --- a/code/ryzom/client/data/gamedev/interfaces_v3/interaction.xml +++ b/code/ryzom/client/data/gamedev/interfaces_v3/interaction.xml @@ -3127,4 +3127,19 @@ + + + + + + + diff --git a/code/ryzom/client/src/interface_v3/chat_text_manager.cpp b/code/ryzom/client/src/interface_v3/chat_text_manager.cpp index 750630a17..5195d0b51 100644 --- a/code/ryzom/client/src/interface_v3/chat_text_manager.cpp +++ b/code/ryzom/client/src/interface_v3/chat_text_manager.cpp @@ -20,6 +20,7 @@ // client #include "chat_text_manager.h" #include "nel/gui/view_text.h" +#include "nel/gui/group_paragraph.h" #include "interface_manager.h" using namespace std; @@ -149,34 +150,165 @@ static CInterfaceGroup *buildLineWithCommand(CInterfaceGroup *commandGroup, CVie return group; } -//================================================================================= -CViewBase *CChatTextManager::createMsgText(const ucstring &cstMsg, NLMISC::CRGBA col, bool justified /*=false*/) +static inline bool isUrlTag(const ucstring &s, ucstring::size_type index, ucstring::size_type textSize) { - ucstring msg = cstMsg; - CInterfaceGroup *commandGroup = parseCommandTag(msg); - CViewText *vt = new CViewText(CViewText::TCtorParam()); - // get parameters from config.xml - vt->setShadow(isTextShadowed()); - vt->setShadowOutline(false); - vt->setFontSize(getTextFontSize()); - vt->setMultiLine(true); - vt->setTextMode(justified ? CViewText::Justified : CViewText::DontClipWord); - vt->setMultiLineSpace(getTextMultiLineSpace()); - vt->setModulateGlobalColor(false); - - ucstring cur_time; - if (showTimestamps()) + // Format http://, https:// + // or markdown style (title)[http://..] + if(textSize > index+7) { - CCDBNodeLeaf *node = NLGUI::CDBManager::getInstance()->getDbProp("UI:SAVE:SHOW_CLOCK_12H", false); - if (node && node->getValueBool()) - cur_time = CInterfaceManager::getTimestampHuman("[%I:%M:%S %p] "); - else - cur_time = CInterfaceManager::getTimestampHuman(); + bool markdown = false; + ucstring::size_type i = index; + // advance index to url section if markdown style link is detected + if (s[i] == '(') + { + // scan for ')[http://' + while(i < textSize-9) + { + if (s[i] == ')' && s[i+1] == '[') + { + i += 2; + markdown = true; + break; + } + else + if (s[i] == ')') + { + i += 1; + break; + } + i++; + } + } + + if (textSize > i + 7) + { + bool isUrl = (toLower(s.substr(i, 7)) == ucstring("http://") || toLower(s.substr(i, 8)) == ucstring("https://")); + // match "text http://" and not "texthttp://" + if (isUrl && i > 0 && !markdown) + { + // '}' is in the list because of color tags, ie "@{FFFF}http://..." + const ucchar chars[] = {' ', '"', '\'', '(', '[', '}'}; + isUrl = std::find(std::begin(chars), std::end(chars), s[i - 1]) != std::end(chars); + } + return isUrl; + } } - // if text contain any color code, set the text formated and white, - // otherwise, set text normal and apply global color - size_t codePos = msg.find(ucstring("@{")); + return false; +} + +// *************************************************************************** +// isUrlTag must match +static inline void getUrlTag(const ucstring &s, ucstring::size_type &index, ucstring &url, ucstring &title) +{ + bool isMarkdown = false; + ucstring::size_type textSize = s.size(); + ucstring::size_type pos; + + // see if we have markdown format + if (s[index] == '(') + { + index++; + pos = index; + while(pos < textSize-9) + { + if (s[pos] == ')' && s[pos + 1] == '[') + { + isMarkdown = true; + title = s.substr(index, pos - index); + index = pos + 2; + break; + } + else if (s[pos] == ')') + { + break; + } + + pos++; + } + } + + ucchar chOpen = ' '; + ucchar chClose = ' '; + if (isMarkdown) + { + chOpen = '['; + chClose = ']'; + } + else if (index > 0) + { + chOpen = s[index - 1]; + if (chOpen == '\'') chClose = '\''; + else if (chOpen == '"') chClose = '"'; + else if (chOpen == '(') chClose = ')'; + else if (chOpen == '[') chClose = ']'; + else chClose = ' '; + } + + if (chOpen == chClose) + { + pos = s.find_first_of(chClose, index); + + // handle common special case: 'text http://.../, text' + if (pos != ucstring::npos && index > 0) + { + if (s[index-1] == ' ' && (s[pos-1] == ',' || s[pos-1] == '.')) + { + pos--; + } + } + } + else + { + // scan for nested open/close tags + pos = index; + sint nested = 0; + while(pos < textSize) + { + if (s[pos] == chOpen) + { + nested++; + } + else if (s[pos] == chClose) + { + if (nested == 0) + { + break; + } + else + { + nested--; + } + } + + pos++; + } + } + + // fallback to full string length as we did match http:// already and url spans to the end probably + if (pos == ucstring::npos) + { + pos = textSize; + } + + url = s.substr(index, pos - index); + index = pos; + + // skip ']' closing char + if (isMarkdown) index++; +} + +//================================================================================= +static void prependTimestamp(ucstring &msg) +{ + ucstring cur_time; + CCDBNodeLeaf *node = NLGUI::CDBManager::getInstance()->getDbProp("UI:SAVE:SHOW_CLOCK_12H", false); + if (node && node->getValueBool()) + cur_time = CInterfaceManager::getTimestampHuman("[%I:%M:%S %p] "); + else + cur_time = CInterfaceManager::getTimestampHuman(); + + ucstring::size_type codePos = msg.find(ucstring("@{")); if (codePos != ucstring::npos) { // Prepend the current time (do it after the color if the color at first position. @@ -189,13 +321,49 @@ CViewBase *CChatTextManager::createMsgText(const ucstring &cstMsg, NLMISC::CRGBA { msg = cur_time + msg; } - + } + else + { + msg = cur_time + msg; + } +} + +//================================================================================= +CViewBase *CChatTextManager::createMsgText(const ucstring &cstMsg, NLMISC::CRGBA col, bool justified /*=false*/, bool plaintext /*=false*/) +{ + ucstring msg = cstMsg; + CInterfaceGroup *commandGroup = parseCommandTag(msg); + + if (showTimestamps()) + prependTimestamp(msg); + + // must wrap all lines to CGroupParagraph because CGroupList will calculate + // width from previous line which ends up as CViewText otherwise + return createMsgTextComplex(msg, col, justified, plaintext, commandGroup); +} + +//================================================================================= +CViewBase *CChatTextManager::createMsgTextSimple(const ucstring &msg, NLMISC::CRGBA col, bool justified, CInterfaceGroup *commandGroup) +{ + CViewText *vt = new CViewText(CViewText::TCtorParam()); + // get parameters from config.xml + vt->setShadow(isTextShadowed()); + vt->setShadowOutline(false); + vt->setFontSize(getTextFontSize()); + vt->setMultiLine(true); + vt->setTextMode(justified ? CViewText::Justified : CViewText::DontClipWord); + vt->setMultiLineSpace(getTextMultiLineSpace()); + vt->setModulateGlobalColor(false); + + // if text contain any color code, set the text formated and white, + // otherwise, set text normal and apply global color + if (msg.find(ucstring("@{")) != ucstring::npos) + { vt->setTextFormatTaged(msg); vt->setColor(NLMISC::CRGBA::White); } else { - msg = cur_time + msg; vt->setText(msg); vt->setColor(col); } @@ -210,6 +378,112 @@ CViewBase *CChatTextManager::createMsgText(const ucstring &cstMsg, NLMISC::CRGBA } } +//================================================================================= +CViewBase *CChatTextManager::createMsgTextComplex(const ucstring &msg, NLMISC::CRGBA col, bool justified, bool plaintext, CInterfaceGroup *commandGroup) +{ + ucstring::size_type textSize = msg.size(); + + CGroupParagraph *para = new CGroupParagraph(CViewBase::TCtorParam()); + para->setId("line"); + para->setSizeRef("w"); + para->setResizeFromChildH(true); + + if (plaintext) + { + CViewBase *vt = createMsgTextSimple(msg, col, justified, NULL); + vt->setId("text"); + para->addChild(vt); + + return para; + } + + + // quickly check if text has links or not + bool hasUrl; + { + ucstring s = toLower(msg); + hasUrl = (s.find(ucstring("http://")) || s.find(ucstring("https://"))); + } + + ucstring::size_type pos = 0; + for (ucstring::size_type i = 0; i< textSize;) + { + if (hasUrl && isUrlTag(msg, i, textSize)) + { + if (pos != i) + { + CViewBase *vt = createMsgTextSimple(msg.substr(pos, i - pos), col, justified, NULL); + para->addChild(vt); + } + + ucstring url; + ucstring title; + getUrlTag(msg, i, url, title); + if (url.size() > 0) + { + CViewLink *vt = new CViewLink(CViewBase::TCtorParam()); + vt->setId("link"); + vt->setUnderlined(true); + vt->setShadow(isTextShadowed()); + vt->setShadowOutline(false); + vt->setFontSize(getTextFontSize()); + vt->setMultiLine(true); + vt->setTextMode(justified ? CViewText::Justified : CViewText::DontClipWord); + vt->setMultiLineSpace(getTextMultiLineSpace()); + vt->setModulateGlobalColor(false); + + //NLMISC::CRGBA color; + //color.blendFromui(col, CRGBA(255, 153, 0, 255), 100); + //vt->setColor(color); + vt->setColor(col); + + if (title.size() > 0) + { + vt->LinkTitle = title.toUtf8(); + vt->setText(title); + } + else + { + vt->LinkTitle = url.toUtf8(); + vt->setText(url); + } + + if (url.find_first_of('\'') != string::npos) + { + ucstring clean; + for(string::size_type i = 0; i< url.size(); ++i) + { + if (url[i] == '\'') + clean += ucstring("%27"); + else + clean += url[i]; + } + url = clean; + } + vt->setActionOnLeftClick("lua"); + vt->setParamsOnLeftClick("game:chatUrl('" + url.toUtf8() + "')"); + + para->addChildLink(vt); + + pos = i; + } + } + else + { + ++i; + } + } + + if (pos < textSize) + { + CViewBase *vt = createMsgTextSimple(msg.substr(pos, textSize - pos), col, justified, NULL); + vt->setId("text"); + para->addChild(vt); + } + + return para; +} + //================================================================================= CChatTextManager &CChatTextManager::getInstance() { diff --git a/code/ryzom/client/src/interface_v3/chat_text_manager.h b/code/ryzom/client/src/interface_v3/chat_text_manager.h index 0dfe05673..41326f067 100644 --- a/code/ryzom/client/src/interface_v3/chat_text_manager.h +++ b/code/ryzom/client/src/interface_v3/chat_text_manager.h @@ -24,6 +24,7 @@ namespace NLGUI { class CViewBase; + class CInterfaceGroup; } class ucstring; @@ -49,8 +50,9 @@ public: * \param msg the actual text * \param col the color of the text * \param justified Should be true for justified text (stretch spaces of line to fill the full width) + * \param plaintext Text will not be parsed for uri markup links */ - NLGUI::CViewBase *createMsgText(const ucstring &msg, NLMISC::CRGBA col, bool justified = false); + NLGUI::CViewBase *createMsgText(const ucstring &msg, NLMISC::CRGBA col, bool justified = false, bool plaintext = false); // Singleton access static CChatTextManager &getInstance(); @@ -73,6 +75,9 @@ private: ~CChatTextManager(); bool showTimestamps() const; + + NLGUI::CViewBase *createMsgTextSimple(const ucstring &msg, NLMISC::CRGBA col, bool justified, NLGUI::CInterfaceGroup *commandGroup); + NLGUI::CViewBase *createMsgTextComplex(const ucstring &msg, NLMISC::CRGBA col, bool justified, bool plaintext, NLGUI::CInterfaceGroup *commandGroup); }; // shortcut to get text manager instance