#include "LinkText.h" #include "ClientUI.h" #include "CUIControls.h" #include "../Empire/Empire.h" #include "../universe/UniverseObject.h" #include "../util/AppInterface.h" #include "../util/Logger.h" #include "../util/VarText.h" #include "../util/i18n.h" #include "../util/Directories.h" #include "../universe/Enums.h" #include #include #include #include // TextLinker static(s) const std::string TextLinker::ENCYCLOPEDIA_TAG("encyclopedia"); const std::string TextLinker::GRAPH_TAG("graph"); const std::string TextLinker::URL_TAG("url"); const std::string TextLinker::BROWSE_PATH_TAG("browsepath"); namespace { static const bool RENDER_DEBUGGING_LINK_RECTS = false; // closing format tag static const std::string LINK_FORMAT_CLOSE = ""; std::string AllParamsAsString(const std::vector& params) { std::string retval; for (const auto& param : params) { if (!retval.empty()) retval.append(" "); retval.append(param); } return retval; } std::string ResolveNestedPathTypes(const std::string& text) { if (text.empty()) return text; std::string new_text = text; for (const auto& path_type : PathTypeStrings()) { std::string path_string = PathToString(GetPath(path_type)); boost::replace_all(new_text, path_type, path_string); } return new_text; } namespace xpr = boost::xpressive; const xpr::sregex REGEX_NON_BRACKET = *~(xpr::set= '<', '>'); const std::string BROWSEPATH_TAG_OPEN_PRE("<" + TextLinker::BROWSE_PATH_TAG); const std::string BROWSEPATH_TAG_CLOSE(""); const xpr::sregex BROWSEPATH_SEARCH = BROWSEPATH_TAG_OPEN_PRE >> xpr::_s >> (xpr::s1 = REGEX_NON_BRACKET) >> ">" >> (xpr::s2 = REGEX_NON_BRACKET) >> BROWSEPATH_TAG_CLOSE; /** Parses TextLinker::BROWSE_PATH_TAG%s within @p text, replacing string representation of PathType%s with * current path for that PathType. If link label is empty, inserts resolved link argument as label */ std::string BrowsePathLinkText(const std::string& text) { if (!boost::contains(text, BROWSEPATH_TAG_CLOSE)) return text; std::string retval(text); auto text_it = retval.begin(); xpr::smatch match; auto invalid_path_str = PathTypeToString(PATH_INVALID); while (true) { if (!xpr::regex_search(text_it, retval.end(), match, BROWSEPATH_SEARCH, xpr::regex_constants::match_default)) break; auto link_arg = ResolveNestedPathTypes(match[1]); std::string link_label(match[2]); if (link_label.empty()) link_label = link_arg; else link_label = ResolveNestedPathTypes(link_label); // Only support argument containing at least one valid PathType if (link_arg == match[1].str() || boost::contains(link_arg, invalid_path_str)) { link_arg = invalid_path_str; link_label = UserString("ERROR") + ": " + link_label; } auto resolved_link = BROWSEPATH_TAG_OPEN_PRE + " " + link_arg + ">" + link_label + BROWSEPATH_TAG_CLOSE; retval.replace(text_it + match.position(), text_it + match.position() + match.length(), resolved_link); text_it = retval.end() - match.suffix().length(); } return retval; } } /////////////////////////////////////// // LinkText /////////////////////////////////////// LinkText::LinkText(GG::X x, GG::Y y, GG::X w, const std::string& str, const std::shared_ptr& font, GG::Flags format/* = GG::FORMAT_NONE*/, GG::Clr color/* = GG::CLR_BLACK*/) : GG::TextControl(x, y, w, GG::Y1, str, font, color, format, GG::INTERACTIVE), TextLinker(), m_raw_text(str) { Resize(TextLowerRight() - TextUpperLeft()); FindLinks(); MarkLinks(); } LinkText::LinkText(GG::X x, GG::Y y, const std::string& str, const std::shared_ptr& font, GG::Clr color/* = GG::CLR_BLACK*/) : GG::TextControl(x, y, GG::X1, GG::Y1, str, font, color, GG::FORMAT_NOWRAP, GG::INTERACTIVE), TextLinker(), m_raw_text(str) { FindLinks(); MarkLinks(); } void LinkText::Render() { GG::TextControl::Render(); TextLinker::Render_(); } void LinkText::SetText(const std::string& str) { m_raw_text = str; FindLinks(); MarkLinks(); } void LinkText::SetLinkedText(const std::string& str) { GG::TextControl::SetText(str); } GG::Pt LinkText::TextUpperLeft() const { return GG::TextControl::TextUpperLeft(); } GG::Pt LinkText::TextLowerRight() const { return GG::TextControl::TextLowerRight(); } const std::vector& LinkText::GetLineData() const { return GG::TextControl::GetLineData(); } const std::shared_ptr& LinkText::GetFont() const { return GG::TextControl::GetFont(); } const std::string& LinkText::RawText() const { return m_raw_text; } void LinkText::LClick(const GG::Pt& pt, GG::Flags mod_keys) { TextLinker::LClick_(pt, mod_keys); } void LinkText::RClick(const GG::Pt& pt, GG::Flags mod_keys) { auto rclick_action = [this, pt, mod_keys]() { TextLinker::RClick_(pt, mod_keys); }; auto copy_action = [this]() { GG::GUI::GetGUI()->CopyWndText(this); }; // create popup menu auto popup = GG::Wnd::Create(pt.x, pt.y); if (GetLinkUnderPt(pt) != -1) { popup->AddMenuItem(GG::MenuItem(UserString("OPEN"), false, false, rclick_action)); popup->AddMenuItem(GG::MenuItem(true)); // separator } popup->AddMenuItem(GG::MenuItem(UserString("HOTKEY_COPY"), false, false, copy_action)); popup->Run(); } void LinkText::MouseHere(const GG::Pt& pt, GG::Flags mod_keys) { TextLinker::MouseHere_(pt, mod_keys); } void LinkText::MouseLeave() { TextLinker::MouseLeave_(); } void LinkText::SizeMove(const GG::Pt& ul, const GG::Pt& lr) { bool resized = Size() != (lr-ul); GG::TextControl::SizeMove(ul, lr); if (resized) LocateLinks(); } /////////////////////////////////////// // LinkDecorator /////////////////////////////////////// std::string LinkDecorator::Decorate(const std::string& target, const std::string& content) const{ return GG::RgbaTag(ClientUI::DefaultLinkColor()) + content + LINK_FORMAT_CLOSE; } std::string LinkDecorator::DecorateRollover(const std::string& target, const std::string& content) const{ return GG::RgbaTag(ClientUI::RolloverLinkColor()) + content + LINK_FORMAT_CLOSE; } int LinkDecorator::CastStringToInt(const std::string& str) { std::stringstream ss; ss << str; int retval = -1; ss >> retval; if (ss.eof()) return retval; return -1; } std::string ColorByOwner::Decorate(const std::string& object_id_str, const std::string& content) const { GG::Clr color = ClientUI::DefaultLinkColor(); const Empire* empire = nullptr; // get object indicated by object_id, and then get object's owner, if any int object_id = CastStringToInt(object_id_str); auto object = Objects().get(object_id); if (object && !object->Unowned()) empire = GetEmpire(object->Owner()); if (empire) color = empire->Color(); return GG::RgbaTag(color) + content + ""; } std::string PathTypeDecorator::Decorate(const std::string& path_type, const std::string& content) const { return LinkDecorator::Decorate(path_type, BrowsePathLinkText(content)); } std::string PathTypeDecorator::DecorateRollover(const std::string& path_type, const std::string& content) const { return LinkDecorator::DecorateRollover(path_type, BrowsePathLinkText(content)); } /////////////////////////////////////// // TextLinker::Link /////////////////////////////////////// struct TextLinker::Link { std::string type; ///< contents of type field of link tag (eg "planet" in ) std::string data; ///< contents of data field of link tag (eg "3" in ) std::vector rects; ///< the rectangles in which this link falls, in window coordinates (some links may span more than one line) std::pair text_posn; ///< the index of the first (.first) and last + 1 (.second) characters in the raw link text std::pair real_text_posn; ///< the index of the first and last + 1 characters in the current (potentially decorated) content string }; /////////////////////////////////////// // TextLinker /////////////////////////////////////// TextLinker::TextLinker() : m_links(), m_rollover_link(-1) { RegisterLinkTags(); } TextLinker::~TextLinker() {} void TextLinker::SetDecorator(const std::string& link_type, LinkDecorator* decorator) { m_decorators[link_type] = std::shared_ptr(decorator); MarkLinks(); } std::string TextLinker::LinkDefaultFormatTag(const Link& link, const std::string& content) const { const LinkDecorator* decorator = &DEFAULT_DECORATOR; auto it = m_decorators.find(link.type); if (it != m_decorators.end()){ decorator = it->second.get(); } return decorator->Decorate(link.data, content); } std::string TextLinker::LinkRolloverFormatTag(const Link& link, const std::string& content) const { const LinkDecorator* decorator = &DEFAULT_DECORATOR; auto it = m_decorators.find(link.type); if (it != m_decorators.end()) decorator = it->second.get(); return decorator->DecorateRollover(link.data, content); } void TextLinker::Render_() { if (!RENDER_DEBUGGING_LINK_RECTS) return; // draw yellow box around whole text block GG::Rect bounds(TextUpperLeft(), TextLowerRight()); FlatRectangle(bounds.ul, bounds.lr, GG::CLR_ZERO, GG::CLR_YELLOW, 1); // draw red box around individual linkified bits of text within block for (const Link& link : m_links) { for (const GG::Rect& rect : link.rects) { GG::Rect r = TextUpperLeft() + rect; FlatRectangle(r.ul, r.lr, GG::CLR_ZERO, GG::CLR_RED, 1); } } } void TextLinker::LClick_(const GG::Pt& pt, GG::Flags mod_keys) { int sel_link = GetLinkUnderPt(pt); if (sel_link == -1) return; if (sel_link < 0 || sel_link >= static_cast(m_links.size())) { ErrorLogger() << "TextLinker::LClick_ found out of bounds sel_link!"; return; } const std::string LINK_TYPE = m_links[sel_link].type; const std::string DATA = m_links[sel_link].data; LinkClickedSignal(LINK_TYPE, DATA); } void TextLinker::RClick_(const GG::Pt& pt, GG::Flags mod_keys) { int sel_link = GetLinkUnderPt(pt); if (sel_link == -1) return; if (sel_link < 0 || sel_link >= static_cast(m_links.size())) { ErrorLogger() << "TextLinker::RClick_ found out of bounds sel_link!"; return; } const std::string& LINK_TYPE = m_links[sel_link].type; const std::string& DATA = m_links[sel_link].data; LinkRightClickedSignal(LINK_TYPE, DATA); } void TextLinker::LDoubleClick_(const GG::Pt& pt, GG::Flags mod_keys) { int sel_link = GetLinkUnderPt(pt); if (sel_link == -1) return; if (sel_link < 0 || sel_link >= static_cast(m_links.size())) { ErrorLogger() << "TextLinker::DoubleClick_ found out of bounds sel_link!"; return; } const std::string& LINK_TYPE = m_links[sel_link].type; const std::string& DATA = m_links[sel_link].data; LinkDoubleClickedSignal(LINK_TYPE, DATA); } void TextLinker::MouseHere_(const GG::Pt& pt, GG::Flags mod_keys) { int rollover_link = GetLinkUnderPt(pt); if (rollover_link != m_rollover_link) { m_rollover_link = rollover_link; MarkLinks(); } } void TextLinker::MouseLeave_() { m_rollover_link = -1; MarkLinks(); } void TextLinker::FindLinks() { m_links.clear(); GG::Y y_posn(0); // y-coordinate of the top of the current line Link link; // control text needs to be updated so that the line data is calculated from // the raw text and not the marked text set in MarkText(). SetLinkedText(RawText()); for (const auto& curr_line : GetLineData()) { for (const auto& curr_char : curr_line.char_data) { for (const auto& tag : curr_char.tags) { if (tag->tag_name == VarText::PLANET_ID_TAG || tag->tag_name == VarText::SYSTEM_ID_TAG || tag->tag_name == VarText::SHIP_ID_TAG || tag->tag_name == VarText::FLEET_ID_TAG || tag->tag_name == VarText::BUILDING_ID_TAG || tag->tag_name == VarText::FIELD_ID_TAG || tag->tag_name == VarText::COMBAT_ID_TAG || tag->tag_name == VarText::EMPIRE_ID_TAG || tag->tag_name == VarText::DESIGN_ID_TAG || tag->tag_name == VarText::PREDEFINED_DESIGN_TAG || tag->tag_name == VarText::TECH_TAG || tag->tag_name == VarText::BUILDING_TYPE_TAG || tag->tag_name == VarText::SPECIAL_TAG || tag->tag_name == VarText::SHIP_HULL_TAG || tag->tag_name == VarText::SHIP_PART_TAG || tag->tag_name == VarText::SPECIES_TAG || tag->tag_name == VarText::FIELD_TYPE_TAG || tag->tag_name == VarText::METER_TYPE_TAG || tag->tag_name == TextLinker::ENCYCLOPEDIA_TAG || tag->tag_name == TextLinker::GRAPH_TAG || tag->tag_name == TextLinker::URL_TAG || tag->tag_name == TextLinker::BROWSE_PATH_TAG) { link.type = tag->tag_name; if (tag->close_tag) { link.text_posn.second = Value(curr_char.string_index); m_links.push_back(link); link = Link(); } else { if (!tag->params.empty()) { if (tag->tag_name == TextLinker::BROWSE_PATH_TAG) { auto all_param_str(AllParamsAsString(tag->params)); link.data = ResolveNestedPathTypes(all_param_str); // BROWSE_PATH_TAG requires a PathType within param if (link.data == all_param_str) { ErrorLogger() << "Invalid param \"" << link.data << "\" for tag " << TextLinker::BROWSE_PATH_TAG; link.data = PathTypeToString(PATH_INVALID); } } else { link.data = tag->params[0]; } } else { link.data.clear(); } link.text_posn.first = Value(curr_char.string_index); for (auto& itag : curr_char.tags) { link.text_posn.first -= Value(itag->StringSize()); } } // Before decoration, the real positions are the same as the raw ones link.real_text_posn = link.text_posn; } } } } LocateLinks(); } void TextLinker::LocateLinks() { if (m_links.empty()) return; GG::Y y_posn(0); // y-coordinate of the top of the current line const auto& font = GetFont(); // We assume that links are stored in m_links in the order they appear in the text. // We shall iterate through the text, updating the rectangles of a link whenever we know we are inside it auto current_link = m_links.begin(); bool inside_link = false; for (const auto& curr_line : GetLineData()) { // if the last line ended without the current tag ending if (inside_link) current_link->rects.push_back(GG::Rect(GG::X0, y_posn, GG::X0, y_posn + font->Height())); for (unsigned int i = 0; i < curr_line.char_data.size(); ++i) { // The link text_posn is at the beginning of the tag, whereas // char_data jumps over tags. That is why we cannot test for precise equality if (!inside_link && curr_line.char_data[i].string_index >= current_link->real_text_posn.first && curr_line.char_data[i].string_index < current_link->real_text_posn.second) { inside_link = true; // Clear out the old rectangles current_link->rects.clear(); current_link->rects.push_back(GG::Rect(i ? curr_line.char_data[i - 1].extent : GG::X0, y_posn, GG::X0, y_posn + font->Height())); } else if (inside_link && curr_line.char_data[i].string_index >= current_link->real_text_posn.second) { inside_link = false; current_link->rects.back().lr.x = i ? curr_line.char_data[i - 1].extent : GG::X0; ++current_link; if (current_link == m_links.end()) return; } } // if a line is ending without the current tag ending if (inside_link) current_link->rects.back().lr.x = curr_line.char_data.empty() ? GG::X0 : curr_line.char_data.back().extent; y_posn += font->Lineskip(); } } int TextLinker::GetLinkUnderPt(const GG::Pt& pt) { std::vector links; try { links = m_links; } catch (...) { ErrorLogger() << "exception caught copying links in GetLinkUnderPt"; return -1; } GG::Pt tex_ul = TextUpperLeft(); for (unsigned int i = 0; i < links.size(); ++i) { const Link& link = links[i]; for (const GG::Rect& link_rect : link.rects) { GG::Rect r = tex_ul + link_rect; if (r.Contains(pt)) return i; } } return -1; // no link found } void TextLinker::MarkLinks() { const std::string& raw_text = RawText(); if (m_links.empty()) { SetLinkedText(raw_text); return; } int copy_start_index = 0; auto raw_text_start_it = raw_text.begin(); std::string marked_text; // copy text from current copy_start_index up to just before start of next link for (int i = 0; i < static_cast(m_links.size()); ++i) { Link& link = m_links[i]; int link_start_index = link.text_posn.first; int link_end_index = link.text_posn.second; // copy raw text up to start of first link std::copy(raw_text_start_it + copy_start_index, raw_text_start_it + link_start_index, std::back_inserter(marked_text)); std::string content = std::string(raw_text_start_it + link_start_index, raw_text_start_it + link_end_index); int length_before = marked_text.size(); // add link markup open tag if (i == m_rollover_link) marked_text += LinkRolloverFormatTag(link, content); else marked_text += LinkDefaultFormatTag(link, content); // update copy point for following text copy_start_index = link_end_index; // update m_links to know the real positions of the links in the decorated text // this makes you able to call LocateLinks without resetting the text. link.real_text_posn.first = length_before; link.real_text_posn.second = marked_text.size(); } // copy remaining text after last link std::copy(raw_text_start_it + copy_start_index, raw_text.end(), std::back_inserter(marked_text)); // set underlying UI control text SetLinkedText(marked_text); } const LinkDecorator TextLinker::DEFAULT_DECORATOR; std::string LinkTaggedText(const std::string& tag, const std::string& stringtable_entry) { return "<" + tag + " " + stringtable_entry + ">" + UserString(stringtable_entry) + ""; } std::string LinkTaggedIDText(const std::string& tag, int id, const std::string& text) { return "<" + tag + " " + std::to_string(id) + ">" + text + ""; } std::string LinkTaggedPresetText(const std::string& tag, const std::string& stringtable_entry, const std::string& display_text) { return "<" + tag + " " + stringtable_entry + ">" + display_text + ""; } namespace { static bool link_tags_registered = false; } void RegisterLinkTags() { if (link_tags_registered) return; link_tags_registered = true; // need to register the tags that link text uses so GG::Font will know how to (not) render them GG::Font::RegisterKnownTag(VarText::PLANET_ID_TAG); GG::Font::RegisterKnownTag(VarText::SYSTEM_ID_TAG); GG::Font::RegisterKnownTag(VarText::SHIP_ID_TAG); GG::Font::RegisterKnownTag(VarText::FLEET_ID_TAG); GG::Font::RegisterKnownTag(VarText::BUILDING_ID_TAG); GG::Font::RegisterKnownTag(VarText::FIELD_ID_TAG); GG::Font::RegisterKnownTag(VarText::COMBAT_ID_TAG); GG::Font::RegisterKnownTag(VarText::EMPIRE_ID_TAG); GG::Font::RegisterKnownTag(VarText::DESIGN_ID_TAG); GG::Font::RegisterKnownTag(VarText::PREDEFINED_DESIGN_TAG); GG::Font::RegisterKnownTag(VarText::TECH_TAG); GG::Font::RegisterKnownTag(VarText::BUILDING_TYPE_TAG); GG::Font::RegisterKnownTag(VarText::SPECIAL_TAG); GG::Font::RegisterKnownTag(VarText::SHIP_HULL_TAG); GG::Font::RegisterKnownTag(VarText::SHIP_PART_TAG); GG::Font::RegisterKnownTag(VarText::SPECIES_TAG); GG::Font::RegisterKnownTag(VarText::FIELD_TYPE_TAG); GG::Font::RegisterKnownTag(VarText::METER_TYPE_TAG); GG::Font::RegisterKnownTag(TextLinker::ENCYCLOPEDIA_TAG); GG::Font::RegisterKnownTag(TextLinker::GRAPH_TAG); GG::Font::RegisterKnownTag(TextLinker::URL_TAG); GG::Font::RegisterKnownTag(TextLinker::BROWSE_PATH_TAG); }