diff options
author | Martin Reddy <lynx@lindenlab.com> | 2009-09-04 12:11:27 +0000 |
---|---|---|
committer | Martin Reddy <lynx@lindenlab.com> | 2009-09-04 12:11:27 +0000 |
commit | 330840af7c363accc8e12d073ba91b871c578d27 (patch) | |
tree | 4539a78e8201ee2508b6d598639efe9a989b6531 /indra/llui | |
parent | 2273376dffb80af0cbf1bc1d8cc8990e1d1456f3 (diff) |
Merging the SLURLs Everywhere branch (viewer-2.0.0-slurls-3) into
Viewer 2.0 (viewer-2.0.0-3). This provides support for clickable Urls
in text editors and textboxes, with right-click context menus,
tooltips, and alternate link labels. This includes alert boxes, the
login progress window, local chat and IM interfaces, etc. As well as
context menus for avatars and groups in list widgets. Includes fixes
for the following individual JIRAs:
DEV-8763 VWR-10636: Hyperlinks in alert dialogs should be selectable (clickable)!
DEV-38829 EXT-742: Remove LLLink class
DEV-35459 VWR-14679: SLURLs and teleport Links not parsed properly
DEV-19842 VWR-8773: Closing parenthesis ")" breaks urls
DEV-21577 VWR-9405: In-world SLURLs containing "(" or ")" are not treated as a hyperlink in chat
DEV-37652 SEC-435: Object Chat/IMs are untraceable (VWR-2388) Fix has left flaw
DEV-10353: URLs in chat log terminated incorrectly when newline in chat
DEV-2925: In chat history, use a teleport hyperlink as source name for object IMs
DEV-36192: Need a way to copy Avatar names and Group names
DEV-2926: Allow viewer hyperlinks to have different text than the actual url
DEV-27253: Add easy way to copy URLs from viewer chat
DEV-38274: Make About Second Life window use new Url hyperlinking features
DEV-39076: No url support in Text Editors
DEV-7476 VWR-2172: Add hyperlinks to chat console for easier access
DEV-7475: Add hyperlinks to notecards!
DEV-35375 EXT-128: HTTPS urls aren't loaded in the internal browser by click
Master JIRA issues: DEV-32819, DEV-323820, DEV-7474
Testing performed against QAR-1789
svn merge -r 131623:131889 svn+ssh://svn.lindenlab.com/svn/linden/branches/viewer/viewer-2.0.0-slurl-3
svn merge -r 131978:132515 svn+ssh://svn.lindenlab.com/svn/linden/branches/viewer/viewer-2.0.0-slurl-3
Diffstat (limited to 'indra/llui')
-rw-r--r-- | indra/llui/CMakeLists.txt | 37 | ||||
-rw-r--r-- | indra/llui/llscrolllistctrl.cpp | 75 | ||||
-rw-r--r-- | indra/llui/llscrolllistctrl.h | 13 | ||||
-rw-r--r-- | indra/llui/llstyle.cpp | 8 | ||||
-rw-r--r-- | indra/llui/llstyle.h | 8 | ||||
-rw-r--r-- | indra/llui/lltextbase.cpp | 451 | ||||
-rw-r--r-- | indra/llui/lltextbase.h | 198 | ||||
-rw-r--r-- | indra/llui/lltextbox.cpp | 498 | ||||
-rw-r--r-- | indra/llui/lltextbox.h | 49 | ||||
-rw-r--r-- | indra/llui/lltexteditor.cpp | 677 | ||||
-rw-r--r-- | indra/llui/lltexteditor.h | 129 | ||||
-rw-r--r-- | indra/llui/llurlaction.cpp | 137 | ||||
-rw-r--r-- | indra/llui/llurlaction.h | 93 | ||||
-rw-r--r-- | indra/llui/llurlentry.cpp | 546 | ||||
-rw-r--r-- | indra/llui/llurlentry.h | 252 | ||||
-rw-r--r-- | indra/llui/llurlmatch.cpp | 61 | ||||
-rw-r--r-- | indra/llui/llurlmatch.h | 98 | ||||
-rw-r--r-- | indra/llui/llurlregistry.cpp | 165 | ||||
-rw-r--r-- | indra/llui/llurlregistry.h | 87 | ||||
-rw-r--r-- | indra/llui/tests/llurlentry_stub.cpp | 64 | ||||
-rw-r--r-- | indra/llui/tests/llurlentry_test.cpp | 535 | ||||
-rw-r--r-- | indra/llui/tests/llurlmatch_test.cpp | 177 |
22 files changed, 3603 insertions, 755 deletions
diff --git a/indra/llui/CMakeLists.txt b/indra/llui/CMakeLists.txt index a7f899ce41..790f2d5729 100644 --- a/indra/llui/CMakeLists.txt +++ b/indra/llui/CMakeLists.txt @@ -47,7 +47,6 @@ set(llui_SOURCE_FILES llkeywords.cpp lllayoutstack.cpp lllineeditor.cpp - lllink.cpp lllistctrl.cpp llmenugl.cpp llmodaldialog.cpp @@ -79,6 +78,7 @@ set(llui_SOURCE_FILES llstatview.cpp llstyle.cpp lltabcontainer.cpp + lltextbase.cpp lltextbox.cpp lltexteditor.cpp lltextparser.cpp @@ -90,6 +90,10 @@ set(llui_SOURCE_FILES lluiimage.cpp lluistring.cpp llundo.cpp + llurlaction.cpp + llurlentry.cpp + llurlmatch.cpp + llurlregistry.cpp llviewborder.cpp llviewmodel.cpp llview.cpp @@ -124,7 +128,6 @@ set(llui_HEADER_FILES lllayoutstack.h lllazyvalue.h lllineeditor.h - lllink.h lllistctrl.h llmenugl.h llmodaldialog.h @@ -156,6 +159,7 @@ set(llui_HEADER_FILES llstatview.h llstyle.h lltabcontainer.h + lltextbase.h lltextbox.h lltexteditor.h lltextparser.h @@ -169,6 +173,10 @@ set(llui_HEADER_FILES lluiimage.h lluistring.h llundo.h + llurlaction.h + llurlentry.h + llurlmatch.h + llurlregistry.h llviewborder.h llviewmodel.h llview.h @@ -184,12 +192,21 @@ add_library (llui ${llui_SOURCE_FILES}) # Libraries on which this library depends, needed for Linux builds # Sort by high-level to low-level target_link_libraries(llui - llrender - llwindow - llimage - llvfs # ugh, just for LLDir - llxuixml - llxml - llcommon # must be after llimage, llwindow, llrender - llmath + ${LLMESSAGE_LIBRARIES} + ${LLRENDER_LIBRARIES} + ${LLWINDOW_LIBRARIES} + ${LLIMAGE_LIBRARIES} + ${LLVFS_LIBRARIES} # ugh, just for LLDir + ${LLXUIXML_LIBRARIES} + ${LLXML_LIBRARIES} + ${LLMATH_LIBRARIES} + ${LLCOMMON_LIBRARIES} # must be after llimage, llwindow, llrender ) + +# Add tests +include(LLAddBuildTest) +SET(llui_TEST_SOURCE_FILES + llurlmatch.cpp + llurlentry.cpp + ) +LL_ADD_PROJECT_UNIT_TESTS(llui "${llui_TEST_SOURCE_FILES}") diff --git a/indra/llui/llscrolllistctrl.cpp b/indra/llui/llscrolllistctrl.cpp index 637642cdcd..b9a253aac8 100644 --- a/indra/llui/llscrolllistctrl.cpp +++ b/indra/llui/llscrolllistctrl.cpp @@ -57,6 +57,11 @@ #include "llviewborder.h" #include "lltextbox.h" #include "llsdparam.h" +#include "llcachename.h" +#include "llmenugl.h" +#include "llurlaction.h" + +#include <boost/bind.hpp> static LLDefaultChildRegistry::Register<LLScrollListCtrl> r("scroll_list"); @@ -157,6 +162,7 @@ LLScrollListCtrl::LLScrollListCtrl(const LLScrollListCtrl::Params& p) mOnSortChangedCallback( NULL ), mHighlightedItem(-1), mBorder(NULL), + mPopupMenu(NULL), mNumDynamicWidthColumns(0), mTotalStaticColumnWidth(0), mTotalColumnPadding(0), @@ -179,7 +185,8 @@ LLScrollListCtrl::LLScrollListCtrl(const LLScrollListCtrl::Params& p) mHighlightedColor(p.highlighted_color()), mHoveredColor(p.hovered_color()), mSearchColumn(p.search_column), - mColumnPadding(p.column_padding) + mColumnPadding(p.column_padding), + mContextMenuType(MENU_NONE) { mItemListRect.setOriginAndSize( mBorderThickness, @@ -1692,6 +1699,72 @@ BOOL LLScrollListCtrl::handleMouseUp(S32 x, S32 y, MASK mask) return LLUICtrl::handleMouseUp(x, y, mask); } +// virtual +BOOL LLScrollListCtrl::handleRightMouseDown(S32 x, S32 y, MASK mask) +{ + LLScrollListItem *item = hitItem(x, y); + if (item) + { + // check to see if we have a UUID for this row + std::string id = item->getValue().asString(); + LLUUID uuid(id); + if (! uuid.isNull() && mContextMenuType != MENU_NONE) + { + // set up the callbacks for all of the avatar/group menu items + // (N.B. callbacks don't take const refs as id is local scope) + bool is_group = (mContextMenuType == MENU_GROUP); + LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar; + registrar.add("Url.Execute", boost::bind(&LLScrollListCtrl::showNameDetails, id, is_group)); + registrar.add("Url.CopyLabel", boost::bind(&LLScrollListCtrl::copyNameToClipboard, id, is_group)); + registrar.add("Url.CopyUrl", boost::bind(&LLScrollListCtrl::copySLURLToClipboard, id, is_group)); + + // create the context menu from the XUI file and display it + std::string menu_name = is_group ? "menu_url_group.xml" : "menu_url_agent.xml"; + delete mPopupMenu; + mPopupMenu = LLUICtrlFactory::getInstance()->createFromFile<LLContextMenu>( + menu_name, LLMenuGL::sMenuContainer, LLMenuHolderGL::child_registry_t::instance()); + if (mPopupMenu) + { + mPopupMenu->show(x, y); + LLMenuGL::showPopup(this, mPopupMenu, x, y); + return TRUE; + } + } + } + return FALSE; +} + +void LLScrollListCtrl::showNameDetails(std::string id, bool is_group) +{ + // show the resident's profile or the group profile + std::string sltype = is_group ? "group" : "agent"; + std::string slurl = "secondlife:///app/" + sltype + "/" + id + "/about"; + LLUrlAction::clickAction(slurl); +} + +void LLScrollListCtrl::copyNameToClipboard(std::string id, bool is_group) +{ + // copy the name of the avatar or group to the clipboard + std::string name; + if (is_group) + { + gCacheName->getGroupName(LLUUID(id), name); + } + else + { + gCacheName->getFullName(LLUUID(id), name); + } + LLUrlAction::copyURLToClipboard(name); +} + +void LLScrollListCtrl::copySLURLToClipboard(std::string id, bool is_group) +{ + // copy a SLURL for the avatar or group to the clipboard + std::string sltype = is_group ? "group" : "agent"; + std::string slurl = "secondlife:///app/" + sltype + "/" + id + "/about"; + LLUrlAction::copyURLToClipboard(slurl); +} + BOOL LLScrollListCtrl::handleDoubleClick(S32 x, S32 y, MASK mask) { //BOOL handled = FALSE; diff --git a/indra/llui/llscrolllistctrl.h b/indra/llui/llscrolllistctrl.h index 253a58ab73..7a7e5be0be 100644 --- a/indra/llui/llscrolllistctrl.h +++ b/indra/llui/llscrolllistctrl.h @@ -54,6 +54,7 @@ class LLScrollListCell; class LLTextBox; +class LLContextMenu; class LLScrollListCtrl : public LLUICtrl, public LLEditMenuHandler, public LLCtrlListInterface, public LLCtrlScrollInterface @@ -270,10 +271,15 @@ public: void clearSearchString() { mSearchString.clear(); } + // support right-click context menus for avatar/group lists + enum ContextMenuType { MENU_NONE, MENU_AVATAR, MENU_GROUP }; + void setContextMenu(const ContextMenuType &menu) { mContextMenuType = menu; } + // Overridden from LLView /*virtual*/ void draw(); /*virtual*/ BOOL handleMouseDown(S32 x, S32 y, MASK mask); /*virtual*/ BOOL handleMouseUp(S32 x, S32 y, MASK mask); + /*virtual*/ BOOL handleRightMouseDown(S32 x, S32 y, MASK mask); /*virtual*/ BOOL handleDoubleClick(S32 x, S32 y, MASK mask); /*virtual*/ BOOL handleHover(S32 x, S32 y, MASK mask); /*virtual*/ BOOL handleKeyHere(KEY key, MASK mask); @@ -375,6 +381,10 @@ private: void commitIfChanged(); BOOL setSort(S32 column, BOOL ascending); + static void showNameDetails(std::string id, bool is_group); + static void copyNameToClipboard(std::string id, bool is_group); + static void copySLURLToClipboard(std::string id, bool is_group); + S32 mLineHeight; // the max height of a single line S32 mScrollLines; // how many lines we've scrolled down S32 mPageLines; // max number of lines is it possible to see on the screen given mRect and mLineHeight @@ -421,6 +431,7 @@ private: S32 mHighlightedItem; class LLViewBorder* mBorder; + LLContextMenu *mPopupMenu; LLWString mSearchString; LLFrameTimer mSearchTimer; @@ -438,6 +449,8 @@ private: BOOL mDirty; S32 mOriginalSelection; + ContextMenuType mContextMenuType; + typedef std::vector<LLScrollListColumn*> ordered_columns_t; ordered_columns_t mColumnsIndexed; diff --git a/indra/llui/llstyle.cpp b/indra/llui/llstyle.cpp index 929a809d88..c16ac08014 100644 --- a/indra/llui/llstyle.cpp +++ b/indra/llui/llstyle.cpp @@ -54,8 +54,6 @@ LLStyle::LLStyle(const LLStyle::Params& p) mFont(p.font()), mLink(p.link_href), mDropShadow(p.drop_shadow), - mImageHeight(0), - mImageWidth(0), mImagep(p.image()) {} @@ -100,9 +98,7 @@ void LLStyle::setImage(const LLUUID& src) mImagep = LLUI::getUIImageByID(src); } - -void LLStyle::setImageSize(S32 width, S32 height) +void LLStyle::setImage(const std::string& name) { - mImageWidth = width; - mImageHeight = height; + mImagep = LLUI::getUIImage(name); } diff --git a/indra/llui/llstyle.h b/indra/llui/llstyle.h index dcf274a651..5e8883afd7 100644 --- a/indra/llui/llstyle.h +++ b/indra/llui/llstyle.h @@ -69,9 +69,9 @@ public: LLUIImagePtr getImage() const; void setImage(const LLUUID& src); + void setImage(const std::string& name); - BOOL isImage() const { return ((mImageWidth != 0) && (mImageHeight != 0)); } - void setImageSize(S32 width, S32 height); + BOOL isImage() const { return mImagep.notNull(); } // inlined here to make it easier to compare to member data below. -MG bool operator==(const LLStyle &rhs) const @@ -82,8 +82,6 @@ public: && mFont == rhs.mFont && mLink == rhs.mLink && mImagep == rhs.mImagep - && mImageHeight == rhs.mImageHeight - && mImageWidth == rhs.mImageWidth && mItalic == rhs.mItalic && mBold == rhs.mBold && mUnderline == rhs.mUnderline @@ -97,8 +95,6 @@ public: BOOL mBold; BOOL mUnderline; BOOL mDropShadow; - S32 mImageWidth; - S32 mImageHeight; protected: ~LLStyle() { } diff --git a/indra/llui/lltextbase.cpp b/indra/llui/lltextbase.cpp new file mode 100644 index 0000000000..038ea2188f --- /dev/null +++ b/indra/llui/lltextbase.cpp @@ -0,0 +1,451 @@ +/** + * @file lltextbase.cpp + * @author Martin Reddy + * @brief The base class of text box/editor, providing Url handling support + * + * $LicenseInfo:firstyear=2009&license=viewergpl$ + * + * Copyright (c) 2009, Linden Research, Inc. + * + * Second Life Viewer Source Code + * The source code in this file ("Source Code") is provided by Linden Lab + * to you under the terms of the GNU General Public License, version 2.0 + * ("GPL"), unless you have obtained a separate licensing agreement + * ("Other License"), formally executed by you and Linden Lab. Terms of + * the GPL can be found in doc/GPL-license.txt in this distribution, or + * online at http://secondlifegrid.net/programs/open_source/licensing/gplv2 + * + * There are special exceptions to the terms and conditions of the GPL as + * it is applied to this Source Code. View the full text of the exception + * in the file doc/FLOSS-exception.txt in this software distribution, or + * online at + * http://secondlifegrid.net/programs/open_source/licensing/flossexception + * + * By copying, modifying or distributing this software, you acknowledge + * that you have read and understood your obligations described above, + * and agree to abide by those obligations. + * + * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO + * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY, + * COMPLETENESS OR PERFORMANCE. + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "lltextbase.h" +#include "llstl.h" +#include "llview.h" +#include "llwindow.h" +#include "llmenugl.h" +#include "lluictrl.h" +#include "llurlaction.h" +#include "llurlregistry.h" + +#include <boost/bind.hpp> + +// global state for all text fields +LLUIColor LLTextBase::mLinkColor = LLColor4::blue; + +bool LLTextBase::compare_segment_end::operator()(const LLTextSegmentPtr& a, const LLTextSegmentPtr& b) const +{ + return a->getEnd() < b->getEnd(); +} + +// +// LLTextSegment +// + +LLTextSegment::~LLTextSegment() +{} + +S32 LLTextSegment::getWidth(S32 first_char, S32 num_chars) const { return 0; } +S32 LLTextSegment::getOffset(S32 segment_local_x_coord, S32 start_offset, S32 num_chars, bool round) const { return 0; } +S32 LLTextSegment::getNumChars(S32 num_pixels, S32 segment_offset, S32 line_offset, S32 max_chars) const { return 0; } +void LLTextSegment::updateLayout(const LLTextBase& editor) {} +F32 LLTextSegment::draw(S32 start, S32 end, S32 selection_start, S32 selection_end, const LLRect& draw_rect) { return draw_rect.mLeft; } +S32 LLTextSegment::getMaxHeight() const { return 0; } +bool LLTextSegment::canEdit() const { return false; } +void LLTextSegment::unlinkFromDocument(LLTextBase*) {} +void LLTextSegment::linkToDocument(LLTextBase*) {} +void LLTextSegment::setHasMouseHover(bool hover) {} +const LLColor4& LLTextSegment::getColor() const { return LLColor4::white; } +void LLTextSegment::setColor(const LLColor4 &color) {} +const LLStyleSP LLTextSegment::getStyle() const {static LLStyleSP sp(new LLStyle()); return sp; } +void LLTextSegment::setStyle(const LLStyleSP &style) {} +void LLTextSegment::setToken( LLKeywordToken* token ) {} +LLKeywordToken* LLTextSegment::getToken() const { return NULL; } +BOOL LLTextSegment::getToolTip( std::string& msg ) const { return FALSE; } +void LLTextSegment::setToolTip( const std::string &msg ) {} +void LLTextSegment::dump() const {} + + +// +// LLNormalTextSegment +// + +LLNormalTextSegment::LLNormalTextSegment( const LLStyleSP& style, S32 start, S32 end, LLTextBase& editor ) +: LLTextSegment(start, end), + mStyle( style ), + mToken(NULL), + mHasMouseHover(false), + mEditor(editor) +{ + mMaxHeight = llceil(mStyle->getFont()->getLineHeight()); +} + +LLNormalTextSegment::LLNormalTextSegment( const LLColor4& color, S32 start, S32 end, LLTextBase& editor, BOOL is_visible) +: LLTextSegment(start, end), + mToken(NULL), + mHasMouseHover(false), + mEditor(editor) +{ + mStyle = new LLStyle(LLStyle::Params().visible(is_visible).color(color)); + + mMaxHeight = llceil(mStyle->getFont()->getLineHeight()); +} + +F32 LLNormalTextSegment::draw(S32 start, S32 end, S32 selection_start, S32 selection_end, const LLRect& draw_rect) +{ + if( end - start > 0 ) + { + if ( mStyle->isImage() && (start >= 0) && (end <= mEnd - mStart)) + { + LLUIImagePtr image = mStyle->getImage(); + S32 style_image_height = image->getHeight(); + S32 style_image_width = image->getWidth(); + image->draw(draw_rect.mLeft, draw_rect.mTop-style_image_height, + style_image_width, style_image_height); + } + + return drawClippedSegment( getStart() + start, getStart() + end, selection_start, selection_end, draw_rect.mLeft, draw_rect.mBottom); + } + return draw_rect.mLeft; +} + +// Draws a single text segment, reversing the color for selection if needed. +F32 LLNormalTextSegment::drawClippedSegment(S32 seg_start, S32 seg_end, S32 selection_start, S32 selection_end, F32 x, F32 y) +{ + const LLWString &text = mEditor.getWText(); + + F32 right_x = x; + if (!mStyle->isVisible()) + { + return right_x; + } + + const LLFontGL* font = mStyle->getFont(); + + LLColor4 color = mStyle->getColor(); + + font = mStyle->getFont(); + + if( selection_start > seg_start ) + { + // Draw normally + S32 start = seg_start; + S32 end = llmin( selection_start, seg_end ); + S32 length = end - start; + font->render(text, start, x, y, color, LLFontGL::LEFT, LLFontGL::BOTTOM, 0, LLFontGL::NO_SHADOW, length, S32_MAX, &right_x, mEditor.allowsEmbeddedItems()); + } + x = right_x; + + if( (selection_start < seg_end) && (selection_end > seg_start) ) + { + // Draw reversed + S32 start = llmax( selection_start, seg_start ); + S32 end = llmin( selection_end, seg_end ); + S32 length = end - start; + + font->render(text, start, x, y, + LLColor4( 1.f - color.mV[0], 1.f - color.mV[1], 1.f - color.mV[2], 1.f ), + LLFontGL::LEFT, LLFontGL::BOTTOM, 0, LLFontGL::NO_SHADOW, length, S32_MAX, &right_x, mEditor.allowsEmbeddedItems()); + } + x = right_x; + if( selection_end < seg_end ) + { + // Draw normally + S32 start = llmax( selection_end, seg_start ); + S32 end = seg_end; + S32 length = end - start; + font->render(text, start, x, y, color, LLFontGL::LEFT, LLFontGL::BOTTOM, 0, LLFontGL::NO_SHADOW, length, S32_MAX, &right_x, mEditor.allowsEmbeddedItems()); + } + return right_x; +} + +S32 LLNormalTextSegment::getMaxHeight() const +{ + return mMaxHeight; +} + +BOOL LLNormalTextSegment::getToolTip(std::string& msg) const +{ + // do we have a tooltip for a loaded keyword (for script editor)? + if (mToken && !mToken->getToolTip().empty()) + { + const LLWString& wmsg = mToken->getToolTip(); + msg = wstring_to_utf8str(wmsg); + return TRUE; + } + // or do we have an explicitly set tooltip (e.g., for Urls) + if (! mTooltip.empty()) + { + msg = mTooltip; + return TRUE; + } + return FALSE; +} + +void LLNormalTextSegment::setToolTip(const std::string& tooltip) +{ + // we cannot replace a keyword tooltip that's loaded from a file + if (mToken) + { + llwarns << "LLTextSegment::setToolTip: cannot replace keyword tooltip." << llendl; + return; + } + mTooltip = tooltip; +} + +S32 LLNormalTextSegment::getWidth(S32 first_char, S32 num_chars) const +{ + LLWString text = mEditor.getWText(); + return mStyle->getFont()->getWidth(text.c_str(), mStart + first_char, num_chars); +} + +S32 LLNormalTextSegment::getOffset(S32 segment_local_x_coord, S32 start_offset, S32 num_chars, bool round) const +{ + LLWString text = mEditor.getWText(); + return mStyle->getFont()->charFromPixelOffset(text.c_str(), mStart + start_offset, + (F32)segment_local_x_coord, + F32_MAX, + num_chars, + round); +} + +S32 LLNormalTextSegment::getNumChars(S32 num_pixels, S32 segment_offset, S32 line_offset, S32 max_chars) const +{ + LLWString text = mEditor.getWText(); + S32 num_chars = mStyle->getFont()->maxDrawableChars(text.c_str() + segment_offset + mStart, + (F32)num_pixels, + max_chars, + mEditor.getWordWrap()); + + if (num_chars == 0 + && line_offset == 0 + && max_chars > 0) + { + // If at the beginning of a line, and a single character won't fit, draw it anyway + num_chars = 1; + } + if (mStart + segment_offset + num_chars == mEditor.getLength()) + { + // include terminating NULL + num_chars++; + } + return num_chars; +} + +void LLNormalTextSegment::dump() const +{ + llinfos << "Segment [" << +// mColor.mV[VX] << ", " << +// mColor.mV[VY] << ", " << +// mColor.mV[VZ] << "]\t[" << + mStart << ", " << + getEnd() << "]" << + llendl; +} + +////////////////////////////////////////////////////////////////////////// +// +// LLTextBase +// + +LLTextBase::LLTextBase(const LLUICtrl::Params &p) : + mHoverSegment(NULL), + mDefaultFont(p.font), + mParseHTML(TRUE), + mPopupMenu(NULL) +{ +} + +LLTextBase::~LLTextBase() +{ + clearSegments(); +} + +void LLTextBase::clearSegments() +{ + setHoverSegment(NULL); + mSegments.clear(); +} + +void LLTextBase::setHoverSegment(LLTextSegmentPtr segment) +{ + if (mHoverSegment) + { + mHoverSegment->setHasMouseHover(false); + } + if (segment) + { + segment->setHasMouseHover(true); + } + mHoverSegment = segment; +} + +void LLTextBase::getSegmentAndOffset( S32 startpos, segment_set_t::const_iterator* seg_iter, S32* offsetp ) const +{ + *seg_iter = getSegIterContaining(startpos); + if (*seg_iter == mSegments.end()) + { + *offsetp = 0; + } + else + { + *offsetp = startpos - (**seg_iter)->getStart(); + } +} + +void LLTextBase::getSegmentAndOffset( S32 startpos, segment_set_t::iterator* seg_iter, S32* offsetp ) +{ + *seg_iter = getSegIterContaining(startpos); + if (*seg_iter == mSegments.end()) + { + *offsetp = 0; + } + else + { + *offsetp = startpos - (**seg_iter)->getStart(); + } +} + +LLTextBase::segment_set_t::iterator LLTextBase::getSegIterContaining(S32 index) +{ + segment_set_t::iterator it = mSegments.upper_bound(new LLIndexSegment(index)); + return it; +} + +LLTextBase::segment_set_t::const_iterator LLTextBase::getSegIterContaining(S32 index) const +{ + LLTextBase::segment_set_t::const_iterator it = mSegments.upper_bound(new LLIndexSegment(index)); + return it; +} + +// Finds the text segment (if any) at the give local screen position +LLTextSegmentPtr LLTextBase::getSegmentAtLocalPos( S32 x, S32 y ) +{ + // Find the cursor position at the requested local screen position + S32 offset = getDocIndexFromLocalCoord( x, y, FALSE ); + segment_set_t::iterator seg_iter = getSegIterContaining(offset); + if (seg_iter != mSegments.end()) + { + return *seg_iter; + } + else + { + return LLTextSegmentPtr(); + } +} + +BOOL LLTextBase::handleHoverOverUrl(S32 x, S32 y) +{ + setHoverSegment(NULL); + + // Check to see if we're over an HTML-style link + LLTextSegment* cur_segment = getSegmentAtLocalPos( x, y ); + if (cur_segment) + { + setHoverSegment(cur_segment); + + LLStyleSP style = cur_segment->getStyle(); + if (style && style->isLink()) + { + return TRUE; + } + } + + return FALSE; +} + +BOOL LLTextBase::handleMouseUpOverUrl(S32 x, S32 y) +{ + if (mParseHTML && mHoverSegment) + { + LLStyleSP style = mHoverSegment->getStyle(); + if (style && style->isLink()) + { + LLUrlAction::clickAction(style->getLinkHREF()); + return TRUE; + } + } + + return FALSE; +} + +BOOL LLTextBase::handleRightMouseDownOverUrl(LLView *view, S32 x, S32 y) +{ + // pop up a context menu for any Url under the cursor + const LLTextSegment* cur_segment = getSegmentAtLocalPos(x, y); + if (cur_segment && cur_segment->getStyle() && cur_segment->getStyle()->isLink()) + { + delete mPopupMenu; + mPopupMenu = createUrlContextMenu(cur_segment->getStyle()->getLinkHREF()); + if (mPopupMenu) + { + mPopupMenu->show(x, y); + LLMenuGL::showPopup(view, mPopupMenu, x, y); + return TRUE; + } + } + + return FALSE; +} + +BOOL LLTextBase::handleToolTipForUrl(LLView *view, S32 x, S32 y, std::string& msg, LLRect* sticky_rect_screen) +{ + const LLTextSegment* cur_segment = getSegmentAtLocalPos( x, y ); + if (cur_segment && cur_segment->getToolTip( msg ) && view) + { + // Use a slop area around the cursor + const S32 SLOP = 8; + // Convert rect local to screen coordinates + view->localPointToScreen(x - SLOP, y - SLOP, &(sticky_rect_screen->mLeft), + &(sticky_rect_screen->mBottom)); + sticky_rect_screen->mRight = sticky_rect_screen->mLeft + 2 * SLOP; + sticky_rect_screen->mTop = sticky_rect_screen->mBottom + 2 * SLOP; + } + return TRUE; +} + +LLContextMenu *LLTextBase::createUrlContextMenu(const std::string &in_url) +{ + // work out the XUI menu file to use for this url + LLUrlMatch match; + std::string url = in_url; + if (! LLUrlRegistry::instance().findUrl(url, match)) + { + return NULL; + } + + std::string xui_file = match.getMenuName(); + if (xui_file.empty()) + { + return NULL; + } + + // set up the callbacks for all of the potential menu items, N.B. we + // don't use const ref strings in callbacks in case url goes out of scope + LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar; + registrar.add("Url.Open", boost::bind(&LLUrlAction::openURL, url)); + registrar.add("Url.OpenInternal", boost::bind(&LLUrlAction::openURLInternal, url)); + registrar.add("Url.OpenExternal", boost::bind(&LLUrlAction::openURLExternal, url)); + registrar.add("Url.Execute", boost::bind(&LLUrlAction::executeSLURL, url)); + registrar.add("Url.Teleport", boost::bind(&LLUrlAction::teleportToLocation, url)); + registrar.add("Url.CopyLabel", boost::bind(&LLUrlAction::copyLabelToClipboard, url)); + registrar.add("Url.CopyUrl", boost::bind(&LLUrlAction::copyURLToClipboard, url)); + + // create and return the context menu from the XUI file + return LLUICtrlFactory::getInstance()->createFromFile<LLContextMenu>(xui_file, LLMenuGL::sMenuContainer, + LLMenuHolderGL::child_registry_t::instance()); +} diff --git a/indra/llui/lltextbase.h b/indra/llui/lltextbase.h new file mode 100644 index 0000000000..27b88761a8 --- /dev/null +++ b/indra/llui/lltextbase.h @@ -0,0 +1,198 @@ +/** + * @file lltextbase.h + * @author Martin Reddy + * @brief The base class of text box/editor, providing Url handling support + * + * $LicenseInfo:firstyear=2009&license=viewergpl$ + * + * Copyright (c) 2009, Linden Research, Inc. + * + * Second Life Viewer Source Code + * The source code in this file ("Source Code") is provided by Linden Lab + * to you under the terms of the GNU General Public License, version 2.0 + * ("GPL"), unless you have obtained a separate licensing agreement + * ("Other License"), formally executed by you and Linden Lab. Terms of + * the GPL can be found in doc/GPL-license.txt in this distribution, or + * online at http://secondlifegrid.net/programs/open_source/licensing/gplv2 + * + * There are special exceptions to the terms and conditions of the GPL as + * it is applied to this Source Code. View the full text of the exception + * in the file doc/FLOSS-exception.txt in this software distribution, or + * online at + * http://secondlifegrid.net/programs/open_source/licensing/flossexception + * + * By copying, modifying or distributing this software, you acknowledge + * that you have read and understood your obligations described above, + * and agree to abide by those obligations. + * + * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO + * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY, + * COMPLETENESS OR PERFORMANCE. + * $/LicenseInfo$ + */ + +#ifndef LL_LLTEXTBASE_H +#define LL_LLTEXTBASE_H + +#include "v4color.h" +#include "llstyle.h" +#include "llkeywords.h" +#include "lluictrl.h" + +#include <string> +#include <set> + +class LLContextMenu; +class LLTextSegment; + +typedef LLPointer<LLTextSegment> LLTextSegmentPtr; + +/// +/// The LLTextBase class provides a base class for all text fields, such +/// as LLTextEditor and LLTextBox. It implements shared functionality +/// such as Url highlighting and opening. +/// +class LLTextBase +{ +public: + LLTextBase(const LLUICtrl::Params &p); + virtual ~LLTextBase(); + + /// specify the color to display Url hyperlinks in the text + static void setLinkColor(LLColor4 color) { mLinkColor = color; } + + /// enable/disable the automatic hyperlinking of Urls in the text + void setParseHTML(BOOL parsing) { mParseHTML=parsing; } + + // public text editing virtual methods + virtual LLWString getWText() const = 0; + virtual BOOL allowsEmbeddedItems() const { return FALSE; } + virtual BOOL getWordWrap() { return mWordWrap; } + virtual S32 getLength() const = 0; + +protected: + struct compare_segment_end + { + bool operator()(const LLTextSegmentPtr& a, const LLTextSegmentPtr& b) const; + }; + typedef std::multiset<LLTextSegmentPtr, compare_segment_end> segment_set_t; + + // routines to manage segments + void getSegmentAndOffset( S32 startpos, segment_set_t::const_iterator* seg_iter, S32* offsetp ) const; + void getSegmentAndOffset( S32 startpos, segment_set_t::iterator* seg_iter, S32* offsetp ); + LLTextSegmentPtr getSegmentAtLocalPos( S32 x, S32 y ); + segment_set_t::iterator getSegIterContaining(S32 index); + segment_set_t::const_iterator getSegIterContaining(S32 index) const; + void clearSegments(); + void setHoverSegment(LLTextSegmentPtr segment); + + // event handling for Urls within the text field + BOOL handleHoverOverUrl(S32 x, S32 y); + BOOL handleMouseUpOverUrl(S32 x, S32 y); + BOOL handleRightMouseDownOverUrl(LLView *view, S32 x, S32 y); + BOOL handleToolTipForUrl(LLView *view, S32 x, S32 y, std::string& msg, LLRect* sticky_rect_screen); + + // pure virtuals that have to be implemented by any subclasses + virtual S32 getLineCount() const = 0; + virtual S32 getLineStart( S32 line ) const = 0; + virtual S32 getDocIndexFromLocalCoord( S32 local_x, S32 local_y, BOOL round ) const = 0; + + // protected member variables + static LLUIColor mLinkColor; + const LLFontGL *mDefaultFont; + segment_set_t mSegments; + LLTextSegmentPtr mHoverSegment; + BOOL mParseHTML; + BOOL mWordWrap; + +private: + // create a popup context menu for the given Url + static LLContextMenu *createUrlContextMenu(const std::string &url); + + LLContextMenu *mPopupMenu; +}; + +/// +/// A text segment is used to specify a subsection of a text string +/// that should be formatted differently, such as a hyperlink. It +/// includes a start/end offset from the start of the string, a +/// style to render with, an optional tooltip, etc. +/// +class LLTextSegment : public LLRefCount +{ +public: + LLTextSegment(S32 start, S32 end) : mStart(start), mEnd(end){}; + virtual ~LLTextSegment(); + + virtual S32 getWidth(S32 first_char, S32 num_chars) const; + virtual S32 getOffset(S32 segment_local_x_coord, S32 start_offset, S32 num_chars, bool round) const; + virtual S32 getNumChars(S32 num_pixels, S32 segment_offset, S32 line_offset, S32 max_chars) const; + virtual void updateLayout(const class LLTextBase& editor); + virtual F32 draw(S32 start, S32 end, S32 selection_start, S32 selection_end, const LLRect& draw_rect); + virtual S32 getMaxHeight() const; + virtual bool canEdit() const; + virtual void unlinkFromDocument(class LLTextBase* editor); + virtual void linkToDocument(class LLTextBase* editor); + + virtual void setHasMouseHover(bool hover); + virtual const LLColor4& getColor() const; + virtual void setColor(const LLColor4 &color); + virtual const LLStyleSP getStyle() const; + virtual void setStyle(const LLStyleSP &style); + virtual void setToken( LLKeywordToken* token ); + virtual LLKeywordToken* getToken() const; + virtual BOOL getToolTip( std::string& msg ) const; + virtual void setToolTip(const std::string& tooltip); + virtual void dump() const; + + S32 getStart() const { return mStart; } + void setStart(S32 start) { mStart = start; } + S32 getEnd() const { return mEnd; } + void setEnd( S32 end ) { mEnd = end; } + +protected: + S32 mStart; + S32 mEnd; +}; + +class LLNormalTextSegment : public LLTextSegment +{ +public: + LLNormalTextSegment( const LLStyleSP& style, S32 start, S32 end, LLTextBase& editor ); + LLNormalTextSegment( const LLColor4& color, S32 start, S32 end, LLTextBase& editor, BOOL is_visible = TRUE); + + /*virtual*/ S32 getWidth(S32 first_char, S32 num_chars) const; + /*virtual*/ S32 getOffset(S32 segment_local_x_coord, S32 start_offset, S32 num_chars, bool round) const; + /*virtual*/ S32 getNumChars(S32 num_pixels, S32 segment_offset, S32 line_offset, S32 max_chars) const; + /*virtual*/ F32 draw(S32 start, S32 end, S32 selection_start, S32 selection_end, const LLRect& draw_rect); + /*virtual*/ S32 getMaxHeight() const; + /*virtual*/ bool canEdit() const { return true; } + /*virtual*/ void setHasMouseHover(bool hover) { mHasMouseHover = hover; } + /*virtual*/ const LLColor4& getColor() const { return mStyle->getColor(); } + /*virtual*/ void setColor(const LLColor4 &color) { mStyle->setColor(color); } + /*virtual*/ const LLStyleSP getStyle() const { return mStyle; } + /*virtual*/ void setStyle(const LLStyleSP &style) { mStyle = style; } + /*virtual*/ void setToken( LLKeywordToken* token ) { mToken = token; } + /*virtual*/ LLKeywordToken* getToken() const { return mToken; } + /*virtual*/ BOOL getToolTip( std::string& msg ) const; + /*virtual*/ void setToolTip(const std::string& tooltip); + /*virtual*/ void dump() const; + +protected: + F32 drawClippedSegment(S32 seg_start, S32 seg_end, S32 selection_start, S32 selection_end, F32 x, F32 y); + + class LLTextBase& mEditor; + LLStyleSP mStyle; + S32 mMaxHeight; + LLKeywordToken* mToken; + bool mHasMouseHover; + std::string mTooltip; +}; + +class LLIndexSegment : public LLTextSegment +{ +public: + LLIndexSegment(S32 pos) : LLTextSegment(pos, pos) {} +}; + +#endif diff --git a/indra/llui/lltextbox.cpp b/indra/llui/lltextbox.cpp index 96e72487b8..7a92bfb74c 100644 --- a/indra/llui/lltextbox.cpp +++ b/indra/llui/lltextbox.cpp @@ -32,30 +32,24 @@ #include "linden_common.h" #include "lltextbox.h" -#include "lllink.h" #include "lluictrlfactory.h" #include "llfocusmgr.h" #include "llwindow.h" +#include "llurlregistry.h" +#include "llstyle.h" static LLDefaultChildRegistry::Register<LLTextBox> r("text"); -//*NOTE -// LLLink is not used in code for now, therefor Visual Studio doesn't build it. -// "link" is registered here to force Visual Studio to build LLLink class. -static LLDefaultChildRegistry::Register<LLLink> register_link("link"); - LLTextBox::Params::Params() : text_color("text_color"), length("length"), type("type"), - highlight_on_hover("hover", false), border_visible("border_visible", false), border_drop_shadow_visible("border_drop_shadow_visible", false), bg_visible("bg_visible", false), use_ellipses("use_ellipses"), word_wrap("word_wrap", false), drop_shadow_visible("drop_shadow_visible"), - hover_color("hover_color"), disabled_color("disabled_color"), background_color("background_color"), border_color("border_color"), @@ -68,9 +62,7 @@ LLTextBox::Params::Params() LLTextBox::LLTextBox(const LLTextBox::Params& p) : LLUICtrl(p), - mFontGL(p.font), - mHoverActive( p.highlight_on_hover ), - mHasHover( FALSE ), + LLTextBase(p), mBackgroundVisible( p.bg_visible ), mBorderVisible( p.border_visible ), mShadowType( p.font_shadow ), @@ -84,12 +76,11 @@ LLTextBox::LLTextBox(const LLTextBox::Params& p) mDisabledColor(p.disabled_color()), mBackgroundColor(p.background_color()), mBorderColor(p.border_color()), - mHoverColor(p.hover_color()), mHAlign(p.font_halign), mLineSpacing(p.line_spacing), - mWordWrap( p.word_wrap ), mDidWordWrap(FALSE) { + mWordWrap = p.word_wrap; setText( p.text() ); } @@ -97,9 +88,9 @@ BOOL LLTextBox::handleMouseDown(S32 x, S32 y, MASK mask) { BOOL handled = FALSE; - // HACK: Only do this if there actually is a click callback, so that + // HACK: Only do this if there actually is something to click, so that // overly large text boxes in the older UI won't start eating clicks. - if (mClickedCallback) + if (isClickable()) { handled = TRUE; @@ -121,10 +112,9 @@ BOOL LLTextBox::handleMouseUp(S32 x, S32 y, MASK mask) // We only handle the click if the click both started and ended within us - // HACK: Only do this if there actually is a click callback, so that + // HACK: Only do this if there actually is something to click, so that // overly large text boxes in the older UI won't start eating clicks. - if (mClickedCallback - && hasMouseCapture()) + if (isClickable() && hasMouseCapture()) { handled = TRUE; @@ -136,27 +126,44 @@ BOOL LLTextBox::handleMouseUp(S32 x, S32 y, MASK mask) make_ui_sound("UISndClickRelease"); } - // DO THIS AT THE VERY END to allow the button to be destroyed as a result of being clicked. - // If mouseup in the widget, it's been clicked - if (mClickedCallback) + // handle clicks on Urls in the textbox first + if (! handleMouseUpOverUrl(x, y)) { - mClickedCallback(); + // DO THIS AT THE VERY END to allow the button to be destroyed + // as a result of being clicked. If mouseup in the widget, + // it's been clicked + if (mClickedCallback && ! handled) + { + mClickedCallback(); + } } } return handled; } +BOOL LLTextBox::handleRightMouseDown(S32 x, S32 y, MASK mask) +{ + // pop up a context menu for any Url under the cursor + return handleRightMouseDownOverUrl(this, x, y); +} + BOOL LLTextBox::handleHover(S32 x, S32 y, MASK mask) { - BOOL handled = LLView::handleHover(x,y,mask); - if(mHoverActive) + // Check to see if we're over an HTML-style link + if (handleHoverOverUrl(x, y)) { - mHasHover = TRUE; // This should be set every frame during a hover. - getWindow()->setCursor(UI_CURSOR_ARROW); + lldebugst(LLERR_USER_INPUT) << "hover handled by " << getName() << llendl; + getWindow()->setCursor(UI_CURSOR_HAND); + return TRUE; } - return (handled || mHasHover); + return LLView::handleHover(x,y,mask); +} + +BOOL LLTextBox::handleToolTip(S32 x, S32 y, std::string& msg, LLRect* sticky_rect_screen) +{ + return handleToolTipForUrl(this, x, y, msg, sticky_rect_screen); } void LLTextBox::setText(const LLStringExplicit& text) @@ -168,7 +175,7 @@ void LLTextBox::setText(const LLStringExplicit& text) else { mText.assign(text); - setLineLengths(); + updateDisplayTextAndSegments(); } } @@ -177,11 +184,11 @@ void LLTextBox::setLineLengths() mLineLengthList.clear(); std::string::size_type cur = 0; - std::string::size_type len = mText.getWString().size(); + std::string::size_type len = mDisplayText.size(); while (cur < len) { - std::string::size_type end = mText.getWString().find('\n', cur); + std::string::size_type end = mDisplayText.find('\n', cur); std::string::size_type runLen; if (end == std::string::npos) @@ -199,20 +206,12 @@ void LLTextBox::setLineLengths() } } -void LLTextBox::setWrappedText(const LLStringExplicit& in_text, F32 max_width) +LLWString LLTextBox::wrapText(const LLWString &wtext, S32 &hoffset, S32 &line_num, F32 max_width) { - if (max_width < 0.0f) - { - max_width = (F32)getRect().getWidth(); - } - - LLWString wtext = utf8str_to_wstring(in_text); LLWString final_wtext; - LLWString::size_type cur = 0;; - LLWString::size_type len = wtext.size(); - F32 line_height = mFontGL->getLineHeight(); - S32 line_num = 1; + LLWString::size_type cur = 0; + LLWString::size_type len = wtext.size(); while (cur < len) { LLWString::size_type end = wtext.find('\n', cur); @@ -221,41 +220,121 @@ void LLTextBox::setWrappedText(const LLStringExplicit& in_text, F32 max_width) end = len; } + bool charsRemaining = true; LLWString::size_type runLen = end - cur; if (runLen > 0) { + // work out how many chars can fit onto the current line LLWString run(wtext, cur, runLen); LLWString::size_type useLen = - mFontGL->maxDrawableChars(run.c_str(), max_width, runLen, TRUE); + mDefaultFont->maxDrawableChars(run.c_str(), max_width-hoffset, runLen, TRUE); + charsRemaining = (cur + useLen < len); + // try to break lines on word boundaries + if (useLen < run.size()) + { + LLWString::size_type prev_use_len = useLen; + while (useLen > 0 && ! isspace(run[useLen-1]) && ! ispunct(run[useLen-1])) + { + --useLen; + } + if (useLen == 0) + { + useLen = prev_use_len; + } + } + + // add the chars that could fit onto one line to our result final_wtext.append(wtext, cur, useLen); cur += useLen; - // not enough room to add any more characters - if (useLen == 0) break; + hoffset += mDefaultFont->getWidth(run.substr(0, useLen).c_str()); + + // abort if not enough room to add any more characters + if (useLen == 0) + { + break; + } } - if (cur < len) + if (charsRemaining) { if (wtext[cur] == '\n') { cur += 1; } - line_num +=1; - // Don't wrap the last line if the text is going to spill off - // the bottom of the rectangle. Assume we prefer to run off - // the right edge. - // *TODO: Is this the right behavior? - if((line_num-1)*line_height <= (F32)getRect().getHeight()) + final_wtext += '\n'; + hoffset = 0; + line_num += 1; + } + } + + return final_wtext; +} + +void LLTextBox::setWrappedText(const LLStringExplicit& in_text, F32 max_width) +{ + mDidWordWrap = TRUE; + setText(wstring_to_utf8str(getWrappedText(in_text, max_width))); +} + +LLWString LLTextBox::getWrappedText(const LLStringExplicit& in_text, F32 max_width) +{ + // + // we don't want to wrap Urls otherwise we won't be able to detect their + // presence for hyperlinking. So we look for all Urls, and then word wrap + // the text before and after, but never break a Url in the middle. We + // also need to consider that the Url will be displayed as a label (not + // necessary the actual Url string). + // + + if (max_width < 0.0f) + { + max_width = (F32)getRect().getWidth(); + } + + LLWString wtext = utf8str_to_wstring(in_text); + LLWString final_wtext; + S32 line_num = 1; + S32 hoffset = 0; + + // find the next Url in the text string + LLUrlMatch match; + while ( LLUrlRegistry::instance().findUrl(wstring_to_utf8str(wtext), match)) + { + S32 start = match.getStart(); + S32 end = match.getEnd() + 1; + + // perform word wrap on the text before the Url + final_wtext += wrapText(wtext.substr(0, start), hoffset, line_num, max_width); + + // add the Url (but compute width based on its label) + S32 label_width = mDefaultFont->getWidth(match.getLabel()); + if (hoffset > 0 && hoffset + label_width > max_width) + { + final_wtext += '\n'; + line_num++; + hoffset = 0; + } + final_wtext += wtext.substr(start, end-start); + hoffset += label_width; + if (hoffset > max_width) + { + final_wtext += '\n'; + line_num++; + hoffset = 0; + // eat any leading whitespace on the next line + while (isspace(wtext[end]) && end < (S32)wtext.size()) { - final_wtext += '\n'; + end++; } } + + // move on to the rest of the text after the Url + wtext = wtext.substr(end, wtext.size() - end + 1); } - - mDidWordWrap = TRUE; - std::string final_text = wstring_to_utf8str(final_wtext); - setText(final_text); + final_wtext += wrapText(wtext, hoffset, line_num, max_width); + return final_wtext; } S32 LLTextBox::getTextPixelWidth() @@ -268,7 +347,7 @@ S32 LLTextBox::getTextPixelWidth() iter != mLineLengthList.end(); ++iter) { S32 line_length = *iter; - S32 line_width = mFontGL->getWidth( mText.getWString().c_str(), cur_pos, line_length ); + S32 line_width = mDefaultFont->getWidth( mDisplayText.c_str(), cur_pos, line_length ); if( line_width > max_line_width ) { max_line_width = line_width; @@ -278,7 +357,7 @@ S32 LLTextBox::getTextPixelWidth() } else { - max_line_width = mFontGL->getWidth(mText.getWString().c_str()); + max_line_width = mDefaultFont->getWidth(mDisplayText.c_str()); } return max_line_width; } @@ -290,7 +369,7 @@ S32 LLTextBox::getTextPixelHeight() { num_lines = 1; } - return (S32)(num_lines * mFontGL->getLineHeight()); + return (S32)(num_lines * mDefaultFont->getLineHeight()); } void LLTextBox::setValue(const LLSD& value ) @@ -302,7 +381,7 @@ void LLTextBox::setValue(const LLSD& value ) BOOL LLTextBox::setTextArg( const std::string& key, const LLStringExplicit& text ) { mText.setArg(key, text); - setLineLengths(); + updateDisplayTextAndSegments(); return TRUE; } @@ -345,18 +424,11 @@ void LLTextBox::draw() if ( getEnabled() ) { - if(mHasHover) - { - drawText( text_x, text_y, mHoverColor.get() ); - } - else - { - drawText( text_x, text_y, mTextColor.get() ); - } + drawText( text_x, text_y, mDisplayText, mTextColor.get() ); } else { - drawText( text_x, text_y, mDisabledColor.get() ); + drawText( text_x, text_y, mDisplayText, mDisabledColor.get() ); } if (sDebugRects) @@ -370,41 +442,46 @@ void LLTextBox::draw() //{ // drawDebugRect(); //} - - mHasHover = FALSE; // This is reset every frame. } void LLTextBox::reshape(S32 width, S32 height, BOOL called_from_parent) { - // reparse line lengths + // reparse line lengths (don't need to recalculate the display text) setLineLengths(); LLView::reshape(width, height, called_from_parent); } -void LLTextBox::drawText( S32 x, S32 y, const LLColor4& color ) +void LLTextBox::drawText( S32 x, S32 y, const LLWString &text, const LLColor4& color ) { - if( mLineLengthList.empty() ) + if (mSegments.size() > 1) { - mFontGL->render(mText.getWString(), 0, (F32)x, (F32)y, color, - mHAlign, mVAlign, - 0, - mShadowType, - S32_MAX, getRect().getWidth(), NULL, mUseEllipses); + // we have Urls (or other multi-styled segments) + drawTextSegments(x, y, text); + } + else if( mLineLengthList.empty() ) + { + // simple case of 1 line of text in one style + mDefaultFont->render(text, 0, (F32)x, (F32)y, color, + mHAlign, mVAlign, + 0, + mShadowType, + S32_MAX, getRect().getWidth(), NULL, mUseEllipses); } else { + // simple case of multiple lines of text, all in the same style S32 cur_pos = 0; for (std::vector<S32>::iterator iter = mLineLengthList.begin(); iter != mLineLengthList.end(); ++iter) { S32 line_length = *iter; - mFontGL->render(mText.getWString(), cur_pos, (F32)x, (F32)y, color, - mHAlign, mVAlign, - 0, - mShadowType, - line_length, getRect().getWidth(), NULL, mUseEllipses ); + mDefaultFont->render(text, cur_pos, (F32)x, (F32)y, color, + mHAlign, mVAlign, + 0, + mShadowType, + line_length, getRect().getWidth(), NULL, mUseEllipses ); cur_pos += line_length + 1; - y -= llfloor(mFontGL->getLineHeight()) + mLineSpacing; + y -= llfloor(mDefaultFont->getLineHeight()) + mLineSpacing; } } } @@ -415,3 +492,254 @@ void LLTextBox::reshapeToFitText() S32 height = getTextPixelHeight(); reshape( width + 2 * mHPad, height + 2 * mVPad ); } + +S32 LLTextBox::getDocIndexFromLocalCoord( S32 local_x, S32 local_y, BOOL round ) const +{ + // Returns the character offset for the character under the local (x, y) coordinate. + // When round is true, if the position is on the right half of a character, the cursor + // will be put to its right. If round is false, the cursor will always be put to the + // character's left. + + LLRect rect = getLocalRect(); + rect.mLeft += mHPad; + rect.mRight -= mHPad; + rect.mTop += mVPad; + rect.mBottom -= mVPad; + + // Figure out which line we're nearest to. + S32 total_lines = getLineCount(); + S32 line_height = llround( mDefaultFont->getLineHeight() ) + mLineSpacing; + S32 line = (rect.mTop - 1 - local_y) / line_height; + if (line >= total_lines) + { + return getLength(); // past the end + } + + line = llclamp( line, 0, total_lines ); + S32 line_start = getLineStart(line); + S32 next_start = getLineStart(line+1); + S32 line_end = (next_start != line_start) ? next_start - 1 : getLength(); + if (line_start == -1) + { + return 0; + } + + S32 line_len = line_end - line_start; + S32 pos = mDefaultFont->charFromPixelOffset(mDisplayText.c_str(), line_start, + (F32)(local_x - rect.mLeft), + (F32)rect.getWidth(), + line_len, round); + + return line_start + pos; +} + +S32 LLTextBox::getLineStart( S32 line ) const +{ + line = llclamp(line, 0, getLineCount()-1); + + S32 result = 0; + for (int i = 0; i < line; i++) + { + result += mLineLengthList[i] + 1 /* add newline */; + } + + return result; +} + +void LLTextBox::updateDisplayTextAndSegments() +{ + // remove any previous segment list + clearSegments(); + + // if URL parsing is turned off, then not much to bo + if (! mParseHTML) + { + mDisplayText = mText.getWString(); + setLineLengths(); + return; + } + + // create unique text segments for Urls + mDisplayText.clear(); + S32 end = 0; + LLUrlMatch match; + LLWString text = mText.getWString(); + + // find the next Url in the text string + while ( LLUrlRegistry::instance().findUrl(wstring_to_utf8str(text), match, + boost::bind(&LLTextBox::onUrlLabelUpdated, this, _1, _2)) ) + { + // work out the char offset for the start/end of the url + S32 seg_start = mDisplayText.size(); + S32 start = seg_start + match.getStart(); + end = start + match.getLabel().size(); + + // create a segment for the text before the Url + mSegments.insert(new LLNormalTextSegment(new LLStyle(), seg_start, start, *this)); + mDisplayText += text.substr(0, match.getStart()); + + // create a segment for the Url text + LLStyleSP html(new LLStyle); + html->setVisible(true); + html->setColor(mLinkColor); + html->mUnderline = TRUE; + html->setLinkHREF(match.getUrl()); + + LLNormalTextSegment *html_seg = new LLNormalTextSegment(html, start, end, *this); + html_seg->setToolTip(match.getTooltip()); + + mSegments.insert(html_seg); + mDisplayText += utf8str_to_wstring(match.getLabel()); + + // move on to the rest of the text after the Url + text = text.substr(match.getEnd()+1, text.size() - match.getEnd()); + } + + // output a segment for the remaining text + if (text.size() > 0) + { + mSegments.insert(new LLNormalTextSegment(new LLStyle(), end, end + text.size(), *this)); + mDisplayText += text; + } + + // strip whitespace from the end of the text + while (mDisplayText.size() > 0 && isspace(mDisplayText[mDisplayText.size()-1])) + { + mDisplayText = mDisplayText.substr(0, mDisplayText.size() - 1); + + segment_set_t::iterator it = getSegIterContaining(mDisplayText.size()); + if (it != mSegments.end()) + { + LLTextSegmentPtr seg = *it; + seg->setEnd(seg->getEnd()-1); + } + } + + // we may have changed the line lengths, so recalculate them + setLineLengths(); +} + +void LLTextBox::onUrlLabelUpdated(const std::string &url, const std::string &label) +{ + if (mDidWordWrap) + { + // re-word wrap as the url label lengths may have changed + setWrappedText(mText.getString()); + } + else + { + // or just update the display text with the latest Url labels + updateDisplayTextAndSegments(); + } +} + +bool LLTextBox::isClickable() const +{ + // return true if we have been given a click callback + if (mClickedCallback) + { + return true; + } + + // also return true if we have a clickable Url in the text + segment_set_t::const_iterator it; + for (it = mSegments.begin(); it != mSegments.end(); ++it) + { + LLTextSegmentPtr segmentp = *it; + if (segmentp) + { + const LLStyleSP style = segmentp->getStyle(); + if (style && style->isLink()) + { + return true; + } + } + } + + // otherwise there is nothing clickable here + return false; +} + +void LLTextBox::drawTextSegments(S32 init_x, S32 init_y, const LLWString &text) +{ + const S32 text_len = text.length(); + if (text_len <= 0) + { + return; + } + + S32 cur_line = 0; + S32 num_lines = getLineCount(); + S32 line_start = getLineStart(cur_line); + S32 line_height = llround( mDefaultFont->getLineHeight() ) + mLineSpacing; + F32 text_y = (F32) init_y; + segment_set_t::iterator cur_seg = mSegments.begin(); + + // render a line of text at a time + const LLRect textRect = getLocalRect(); + while((textRect.mBottom <= text_y) && (cur_line < num_lines)) + { + S32 next_start = -1; + S32 line_end = text_len; + + if ((cur_line + 1) < num_lines) + { + next_start = getLineStart(cur_line + 1); + line_end = next_start; + } + if ( text[line_end-1] == '\n' ) + { + --line_end; + } + + // render all segments on this line + F32 text_x = init_x; + S32 seg_start = line_start; + while (seg_start < line_end && cur_seg != mSegments.end()) + { + // move to the next segment (or continue the previous one) + LLTextSegment *cur_segment = *cur_seg; + while (cur_segment->getEnd() <= seg_start) + { + if (++cur_seg == mSegments.end()) + { + return; + } + cur_segment = *cur_seg; + } + + // Draw a segment within the line + S32 clipped_end = llmin( line_end, cur_segment->getEnd() ); + S32 clipped_len = clipped_end - seg_start; + if( clipped_len > 0 ) + { + LLStyleSP style = cur_segment->getStyle(); + if (style && style->isVisible()) + { + // work out the color for the segment + LLColor4 color ; + if (getEnabled()) + { + color = style->isLink() ? mLinkColor.get() : mTextColor.get(); + } + else + { + color = mDisabledColor.get(); + } + + // render a single line worth for this segment + mDefaultFont->render(text, seg_start, text_x, text_y, color, + mHAlign, mVAlign, 0, mShadowType, clipped_len, + textRect.getWidth(), &text_x, mUseEllipses); + } + + seg_start += clipped_len; + } + } + + // move down one line + text_y -= (F32)line_height; + line_start = next_start; + cur_line++; + } +} diff --git a/indra/llui/lltextbox.h b/indra/llui/lltextbox.h index d807fe7639..940b820004 100644 --- a/indra/llui/lltextbox.h +++ b/indra/llui/lltextbox.h @@ -37,10 +37,11 @@ #include "v4color.h" #include "llstring.h" #include "lluistring.h" +#include "lltextbase.h" - -class LLTextBox -: public LLUICtrl +class LLTextBox : + public LLTextBase, + public LLUICtrl { public: @@ -51,8 +52,7 @@ public: { Optional<std::string> text; - Optional<bool> highlight_on_hover, - border_visible, + Optional<bool> border_visible, border_drop_shadow_visible, bg_visible, use_ellipses, @@ -65,7 +65,6 @@ public: length; Optional<LLUIColor> text_color, - hover_color, disabled_color, background_color, border_color; @@ -90,15 +89,14 @@ public: virtual BOOL handleMouseDown(S32 x, S32 y, MASK mask); virtual BOOL handleMouseUp(S32 x, S32 y, MASK mask); virtual BOOL handleHover(S32 x, S32 y, MASK mask); + virtual BOOL handleRightMouseDown(S32 x, S32 y, MASK mask); + virtual BOOL handleToolTip(S32 x, S32 y, std::string& msg, LLRect* sticky_rect_screen); void setColor( const LLColor4& c ) { mTextColor = c; } void setDisabledColor( const LLColor4& c) { mDisabledColor = c; } void setBackgroundColor( const LLColor4& c) { mBackgroundColor = c; } void setBorderColor( const LLColor4& c) { mBorderColor = c; } - void setHoverColor( const LLColor4& c ) { mHoverColor = c; } - void setHoverActive( BOOL active ) { mHoverActive = active; } - void setText( const LLStringExplicit& text ); void setWrappedText(const LLStringExplicit& text, F32 max_width = -1.f); // -1 means use existing control width void setUseEllipses( BOOL use_ellipses ) { mUseEllipses = use_ellipses; } @@ -112,35 +110,42 @@ public: void setHAlign( LLFontGL::HAlign align ) { mHAlign = align; } void setClickedCallback( boost::function<void (void*)> cb, void* userdata = NULL ){ mClickedCallback = boost::bind(cb, userdata); } // mouse down and up within button - const LLFontGL* getFont() const { return mFontGL; } + const LLFontGL* getFont() const { return mDefaultFont; } void reshapeToFitText(); const std::string& getText() const { return mText.getString(); } + LLWString getWText() const { return mDisplayText; } S32 getTextPixelWidth(); S32 getTextPixelHeight(); + S32 getLength() const { return mDisplayText.length(); } virtual void setValue(const LLSD& value ); virtual LLSD getValue() const { return LLSD(getText()); } virtual BOOL setTextArg( const std::string& key, const LLStringExplicit& text ); -private: +protected: + S32 getLineCount() const { return mLineLengthList.size(); } + S32 getLineStart( S32 line ) const; + S32 getDocIndexFromLocalCoord( S32 local_x, S32 local_y, BOOL round ) const; + LLWString getWrappedText(const LLStringExplicit& in_text, F32 max_width = -1.f); void setLineLengths(); - void drawText(S32 x, S32 y, const LLColor4& color ); + void updateDisplayTextAndSegments(); + virtual void drawText(S32 x, S32 y, const LLWString &text, const LLColor4& color ); + void onUrlLabelUpdated(const std::string &url, const std::string &label); + bool isClickable() const; + LLWString wrapText(const LLWString &wtext, S32 &hoffset, S32 &line_num, F32 max_width); + void drawTextSegments(S32 x, S32 y, const LLWString &text); LLUIString mText; - const LLFontGL* mFontGL; - LLUIColor mTextColor; - LLUIColor mDisabledColor; - LLUIColor mBackgroundColor; - LLUIColor mBorderColor; - LLUIColor mHoverColor; - - BOOL mHoverActive; - BOOL mHasHover; + LLWString mDisplayText; + LLUIColor mTextColor; + LLUIColor mDisabledColor; + LLUIColor mBackgroundColor; + LLUIColor mBorderColor; + BOOL mBackgroundVisible; BOOL mBorderVisible; - BOOL mWordWrap; BOOL mDidWordWrap; LLFontGL::ShadowType mShadowType; diff --git a/indra/llui/lltexteditor.cpp b/indra/llui/lltexteditor.cpp index 921041d17f..296ccea0e4 100644 --- a/indra/llui/lltexteditor.cpp +++ b/indra/llui/lltexteditor.cpp @@ -59,6 +59,7 @@ #include "lltextparser.h" #include "llscrollcontainer.h" #include "llpanel.h" +#include "llurlregistry.h" #include <queue> #include "llcombobox.h" @@ -78,10 +79,6 @@ const S32 CURSOR_THICKNESS = 2; const S32 SPACES_PER_TAB = 4; -void (* LLTextEditor::sURLcallback)(const std::string&) = NULL; -bool (* LLTextEditor::sSecondlifeURLcallback)(const std::string&) = NULL; -bool (* LLTextEditor::sSecondlifeURLcallbackRightClick)(const std::string&) = NULL; - // helper functors struct LLTextEditor::compare_bottom { @@ -331,8 +328,9 @@ LLTextEditor::Params::Params() is_unicode("is_unicode")// ignored {} -LLTextEditor::LLTextEditor(const LLTextEditor::Params& p) - : LLUICtrl(p, LLTextViewModelPtr(new LLTextViewModel)), +LLTextEditor::LLTextEditor(const LLTextEditor::Params& p) : + LLUICtrl(p, LLTextViewModelPtr(new LLTextViewModel)), + LLTextBase(p), mMaxTextByteLength( p.max_text_length ), mBaseDocIsPristine(TRUE), mPristineCmd( NULL ), @@ -351,7 +349,6 @@ LLTextEditor::LLTextEditor(const LLTextEditor::Params& p) mFocusBgColor( p.bg_focus_color() ), mLinkColor( p.link_color() ), mReadOnly(p.read_only), - mWordWrap( p.word_wrap ), mShowLineNumbers ( p.show_line_numbers ), mCommitOnFocusLost( p.commit_on_focus_lost), mTrackBottom( p.track_bottom ), @@ -363,14 +360,16 @@ LLTextEditor::LLTextEditor(const LLTextEditor::Params& p) mReflowNeeded(FALSE), mScrollNeeded(FALSE), mLastSelectionY(-1), - mParseHTML(FALSE), mParseHighlights(FALSE), mTabsToNextField(p.ignore_tab), - mDefaultFont(p.font), mScrollIndex(-1) { static LLUICachedControl<S32> scrollbar_size ("UIScrollbarSize", 0); + mWordWrap = p.word_wrap; + mDefaultFont = p.font; + mParseHTML = FALSE; + mSourceID.generate(); // reset desired x cursor position @@ -413,7 +412,6 @@ LLTextEditor::LLTextEditor(const LLTextEditor::Params& p) appendText(p.default_text, FALSE, FALSE); - mHTML.clear(); } void LLTextEditor::initFromParams( const LLTextEditor::Params& p) @@ -451,7 +449,6 @@ LLTextEditor::~LLTextEditor() } // Scrollbar is deleted by LLView - mHoverSegment = NULL; std::for_each(mUndoStack.begin(), mUndoStack.end(), DeletePointer()); } @@ -666,18 +663,12 @@ BOOL LLTextEditor::truncate() return did_truncate; } -void LLTextEditor::clearSegments() -{ - mHoverSegment = NULL; - mSegments.clear(); -} - void LLTextEditor::setText(const LLStringExplicit &utf8str) { + // clear out the existing text and segments clearSegments(); - // LLStringUtil::removeCRLF(utf8str); - getViewModel()->setValue(utf8str_removeCRLF(utf8str)); + getViewModel()->setValue(""); truncate(); blockUndo(); @@ -687,6 +678,11 @@ void LLTextEditor::setText(const LLStringExplicit &utf8str) startOfDoc(); deselect(); + // append the new text (supports Url linking) + std::string text(utf8str); + LLStringUtil::removeCRLF(text); + appendStyledText(text, false, false, LLStyle::Params()); + needsReflow(); resetDirty(); @@ -696,9 +692,10 @@ void LLTextEditor::setText(const LLStringExplicit &utf8str) void LLTextEditor::setWText(const LLWString &wtext) { + // clear out the existing text and segments clearSegments(); - getViewModel()->setDisplay(wtext); + getViewModel()->setDisplay(LLWString()); truncate(); blockUndo(); @@ -708,6 +705,9 @@ void LLTextEditor::setWText(const LLWString &wtext) startOfDoc(); deselect(); + // append the new text (supports Url linking) + appendStyledText(wstring_to_utf8str(wtext), false, false, LLStyle::Params()); + needsReflow(); resetDirty(); @@ -913,32 +913,6 @@ void LLTextEditor::getLineAndOffset( S32 startpos, S32* linep, S32* offsetp, boo } } -void LLTextEditor::getSegmentAndOffset( S32 startpos, segment_set_t::const_iterator* seg_iter, S32* offsetp ) const -{ - *seg_iter = getSegIterContaining(startpos); - if (*seg_iter == mSegments.end()) - { - *offsetp = 0; - } - else - { - *offsetp = startpos - (**seg_iter)->getStart(); - } -} - -void LLTextEditor::getSegmentAndOffset( S32 startpos, segment_set_t::iterator* seg_iter, S32* offsetp ) -{ - *seg_iter = getSegIterContaining(startpos); - if (*seg_iter == mSegments.end()) - { - *offsetp = 0; - } - else - { - *offsetp = startpos - (**seg_iter)->getStart(); - } -} - const LLTextSegmentPtr LLTextEditor::getPreviousSegment() const { // find segment index at character to left of cursor (or rightmost edge of selection) @@ -1154,6 +1128,10 @@ S32 LLTextEditor::getEditableIndex(S32 index, bool increasing_direction) segment_set_t::iterator segment_iter; S32 offset; getSegmentAndOffset(index, &segment_iter, &offset); + if (segment_iter == mSegments.end()) + { + return 0; + } LLTextSegmentPtr segmentp = *segment_iter; @@ -1377,25 +1355,7 @@ BOOL LLTextEditor::handleToolTip(S32 x, S32 y, std::string& msg, LLRect* sticky_ } } - const LLTextSegmentPtr cur_segment = getSegmentAtLocalPos( x, y ); - if( cur_segment ) - { - BOOL has_tool_tip = FALSE; - has_tool_tip = cur_segment->getToolTip( msg ); - - if( has_tool_tip ) - { - // Just use a slop area around the cursor - // Convert rect local to screen coordinates - S32 SLOP = 8; - localPointToScreen( - x - SLOP, y - SLOP, - &(sticky_rect_screen->mLeft), &(sticky_rect_screen->mBottom) ); - sticky_rect_screen->mRight = sticky_rect_screen->mLeft + 2 * SLOP; - sticky_rect_screen->mTop = sticky_rect_screen->mBottom + 2 * SLOP; - } - } - return TRUE; + return handleToolTipForUrl(this, x, y, msg, sticky_rect_screen); } BOOL LLTextEditor::handleMouseDown(S32 x, S32 y, MASK mask) @@ -1480,12 +1440,6 @@ BOOL LLTextEditor::handleHover(S32 x, S32 y, MASK mask) static LLUICachedControl<S32> scrollbar_size ("UIScrollbarSize", 0); BOOL handled = FALSE; - if (mHoverSegment) - { - mHoverSegment->setHasMouseHover(false); - } - mHoverSegment = NULL; - if(hasMouseCapture() ) { if( mIsSelecting ) @@ -1525,30 +1479,11 @@ BOOL LLTextEditor::handleHover(S32 x, S32 y, MASK mask) if( !handled ) { // Check to see if we're over an HTML-style link - LLTextSegmentPtr cur_segment = getSegmentAtLocalPos( x, y ); - if( cur_segment ) + handled = handleHoverOverUrl(x, y); + if( handled ) { - if(cur_segment->getStyle()->isLink()) - { - lldebugst(LLERR_USER_INPUT) << "hover handled by " << getName() << " (over link, inactive)" << llendl; - getWindow()->setCursor(UI_CURSOR_HAND); - handled = TRUE; - } - //else - //if(cur_segment->getStyle()->getIsEmbeddedItem()) - //{ - // lldebugst(LLERR_USER_INPUT) << "hover handled by " << getName() << " (over embedded item, inactive)" << llendl; - // getWindow()->setCursor(UI_CURSOR_HAND); - // //getWindow()->setCursor(UI_CURSOR_ARROW); - // handled = TRUE; - //} - if (mHoverSegment) - { - mHoverSegment->setHasMouseHover(false); - } - cur_segment->setHasMouseHover(true); - mHoverSegment = cur_segment; - mHTML = mHoverSegment->getStyle()->getLinkHREF(); + lldebugst(LLERR_USER_INPUT) << "hover handled by " << getName() << llendl; + getWindow()->setCursor(UI_CURSOR_HAND); } if( !handled ) @@ -1581,9 +1516,9 @@ BOOL LLTextEditor::handleMouseUp(S32 x, S32 y, MASK mask) endSelection(); } - if( !hasSelection() ) + if( !hasSelection() && hasMouseCapture() ) { - handleMouseUpOverSegment( x, y, mask ); + handleMouseUpOverUrl(x, y); } // take selection to 'primary' clipboard @@ -3596,14 +3531,20 @@ void LLTextEditor::appendStyledText(const std::string &new_text, { S32 start=0,end=0; + LLUrlMatch match; std::string text = new_text; - while ( findHTML(text, &start, &end) ) + while ( LLUrlRegistry::instance().findUrl(text, match, + boost::bind(&LLTextEditor::onUrlLabelUpdated, this, _1, _2)) ) { + start = match.getStart(); + end = match.getEnd()+1; + LLStyle::Params link_params = style_params; link_params.color = mLinkColor; link_params.font.style = "UNDERLINE"; - link_params.link_href = text.substr(start,end-start); + link_params.link_href = match.getUrl(); + // output the text before the Url if (start > 0) { if (part == (S32)LLTextParser::WHOLE || @@ -3617,9 +3558,38 @@ void LLTextEditor::appendStyledText(const std::string &new_text, } std::string subtext=text.substr(0,start); appendHighlightedText(subtext,allow_undo, prepend_newline, part, style_params); + prepend_newline = false; } - - appendText(text.substr(start, end-start),allow_undo, prepend_newline, link_params); + + // output the styled Url + appendText(match.getLabel(),allow_undo, prepend_newline, link_params); + prepend_newline = false; + + // set the tooltip for the Url label + if (! match.getTooltip().empty()) + { + segment_set_t::iterator it = getSegIterContaining(getLength()-1); + if (it != mSegments.end()) + { + LLTextSegmentPtr segment = *it; + segment->setToolTip(match.getTooltip()); + } + } + + // output an optional icon after the Url + if (! match.getIcon().empty()) + { + LLUIImagePtr image = LLUI::getUIImage(match.getIcon()); + if (image) + { + LLStyle::Params icon; + icon.image = image; + // TODO: fix spacing of images and remove the fixed char spacing + appendText(" ", allow_undo, prepend_newline, icon); + } + } + + // move on to the rest of the text after the Url if (end < (S32)text.length()) { text = text.substr(end,text.length() - end); @@ -3711,7 +3681,7 @@ void LLTextEditor::appendText(const std::string &new_text, bool allow_undo, bool } append(wide_text, TRUE, segmentp); - + needsReflow(); // Set the cursor and scroll position @@ -3795,6 +3765,58 @@ void LLTextEditor::appendWidget(LLView* widget, const std::string &widget_text, } } +void LLTextEditor::onUrlLabelUpdated(const std::string &url, + const std::string &label) +{ + // LLUrlRegistry has given us a new label for one of our Urls + replaceUrlLabel(url, label); +} + +void LLTextEditor::replaceUrlLabel(const std::string &url, + const std::string &label) +{ + // get the full (wide) text for the editor so we can change it + LLWString text = getWText(); + LLWString wlabel = utf8str_to_wstring(label); + bool modified = false; + S32 seg_start = 0; + + // iterate through each segment looking for ones styled as links + segment_set_t::iterator it; + for (it = mSegments.begin(); it != mSegments.end(); ++it) + { + LLTextSegment *seg = *it; + const LLStyleSP style = seg->getStyle(); + + // update segment start/end length in case we replaced text earlier + S32 seg_length = seg->getEnd() - seg->getStart(); + seg->setStart(seg_start); + seg->setEnd(seg_start + seg_length); + + // if we find a link with our Url, then replace the label + if (style->isLink() && style->getLinkHREF() == url) + { + S32 start = seg->getStart(); + S32 end = seg->getEnd(); + text = text.substr(0, start) + wlabel + text.substr(end, text.size() - end + 1); + seg->setEnd(start + wlabel.size()); + modified = true; + } + + // work out the character offset for the next segment + seg_start = seg->getEnd(); + } + + // update the editor with the new (wide) text string + if (modified) + { + getViewModel()->setDisplay(text); + deselect(); + setCursorPos(mCursorPos); + needsReflow(); + } +} + void LLTextEditor::removeTextFromEnd(S32 num_chars) { if (num_chars <= 0) return; @@ -4097,7 +4119,7 @@ void LLTextEditor::updateSegments() segment_vec_t segment_list; mKeywords.findSegments(&segment_list, getWText(), mDefaultColor.get(), *this); - mSegments.clear(); + clearSegments(); segment_set_t::iterator insert_it = mSegments.begin(); for (segment_vec_t::iterator list_it = segment_list.begin(); list_it != segment_list.end(); ++list_it) { @@ -4106,7 +4128,29 @@ void LLTextEditor::updateSegments() } createDefaultSegment(); +} +void LLTextEditor::updateLinkSegments() +{ + // update any segments that contain a link + for (segment_set_t::iterator it = mSegments.begin(); it != mSegments.end(); ++it) + { + LLTextSegment *segment = *it; + if (segment && segment->getStyle() && segment->getStyle()->isLink()) + { + // if the link's label (what the user can edit) is a valid Url, + // then update the link's HREF to be the same as the label text. + // This lets users edit Urls in-place. + LLUrlMatch match; + LLStyleSP style = static_cast<LLStyleSP>(segment->getStyle()); + std::string url_label = getText().substr(segment->getStart(), segment->getEnd()-segment->getStart()); + if (LLUrlRegistry::instance().findUrl(url_label, match)) + { + LLStringUtil::trim(url_label); + style->setLinkHREF(url_label); + } + } + } } void LLTextEditor::insertSegment(LLTextSegmentPtr segment_to_insert) @@ -4170,57 +4214,6 @@ void LLTextEditor::insertSegment(LLTextSegmentPtr segment_to_insert) } } -BOOL LLTextEditor::handleMouseUpOverSegment(S32 x, S32 y, MASK mask) -{ - if ( hasMouseCapture() ) - { - // This mouse up was part of a click. - // Regardless of where the cursor is, see if we recently touched a link - // and launch it if we did. - if (mParseHTML && mHTML.length() > 0) - { - //Special handling for slurls - if ( (sSecondlifeURLcallback!=NULL) && !(*sSecondlifeURLcallback)(mHTML) ) - { - if (sURLcallback!=NULL) (*sURLcallback)(mHTML); - } - mHTML.clear(); - } - } - - return FALSE; -} - - -// Finds the text segment (if any) at the give local screen position -LLTextSegmentPtr LLTextEditor::getSegmentAtLocalPos( S32 x, S32 y ) -{ - // Find the cursor position at the requested local screen position - S32 offset = getDocIndexFromLocalCoord( x, y, FALSE ); - segment_set_t::iterator seg_iter = getSegIterContaining(offset); - if (seg_iter != mSegments.end()) - { - return *seg_iter; - } - else - { - return LLTextSegmentPtr(); - } -} - -LLTextEditor::segment_set_t::iterator LLTextEditor::getSegIterContaining(S32 index) -{ - segment_set_t::iterator it = mSegments.upper_bound(new LLIndexSegment(index)); - return it; -} - -LLTextEditor::segment_set_t::const_iterator LLTextEditor::getSegIterContaining(S32 index) const -{ - LLTextEditor::segment_set_t::const_iterator it = mSegments.upper_bound(new LLIndexSegment(index)); - return it; -} - - void LLTextEditor::onMouseCaptureLost() { endSelection(); @@ -4330,169 +4323,6 @@ BOOL LLTextEditor::exportBuffer(std::string &buffer ) return TRUE; } -/////////////////////////////////////////////////////////////////// -// Refactoring note: We may eventually want to replace this with boost::regex or -// boost::tokenizer capabilities since we've already fixed at least two JIRAs -// concerning logic issues associated with this function. -S32 LLTextEditor::findHTMLToken(const std::string &line, S32 pos, BOOL reverse) const -{ - std::string openers=" \t\n('\"[{<>"; - std::string closers=" \t\n)'\"]}><;"; - - if (reverse) - { - for (int index=pos; index >= 0; index--) - { - char c = line[index]; - S32 m2 = openers.find(c); - if (m2 >= 0) - { - return index+1; - } - } - return 0; // index is -1, don't want to return that. - } - else - { - // adjust the search slightly, to allow matching parenthesis inside the URL - S32 paren_count = 0; - for (int index=pos; index<(S32)line.length(); index++) - { - char c = line[index]; - - if (c == '(') - { - paren_count++; - } - else if (c == ')') - { - if (paren_count <= 0) - { - return index; - } - else - { - paren_count--; - } - } - else - { - S32 m2 = closers.find(c); - if (m2 >= 0) - { - return index; - } - } - } - return line.length(); - } -} - -BOOL LLTextEditor::findHTML(const std::string &line, S32 *begin, S32 *end) const -{ - - S32 m1,m2,m3; - BOOL matched = FALSE; - - m1=line.find("://",*end); - - if (m1 >= 0) //Easy match. - { - *begin = findHTMLToken(line, m1, TRUE); - *end = findHTMLToken(line, m1, FALSE); - - //Load_url only handles http and https so don't hilite ftp, smb, etc. - m2 = line.substr(*begin,(m1 - *begin)).find("http"); - m3 = line.substr(*begin,(m1 - *begin)).find("secondlife"); - - std::string badneighbors=".,<>?';\"][}{=-+_)(*&^%$#@!~`\t\r\n\\"; - - if (m2 >= 0 || m3>=0) - { - S32 bn = badneighbors.find(line.substr(m1+3,1)); - - if (bn < 0) - { - matched = TRUE; - } - } - } -/* matches things like secondlife.com (no http://) needs a whitelist to really be effective. - else //Harder match. - { - m1 = line.find(".",*end); - - if (m1 >= 0) - { - *end = findHTMLToken(line, m1, FALSE); - *begin = findHTMLToken(line, m1, TRUE); - - m1 = line.rfind(".",*end); - - if ( ( *end - m1 ) > 2 && m1 > *begin) - { - std::string badneighbors=".,<>/?';\"][}{=-+_)(*&^%$#@!~`"; - m2 = badneighbors.find(line.substr(m1+1,1)); - m3 = badneighbors.find(line.substr(m1-1,1)); - if (m3<0 && m2<0) - { - matched = TRUE; - } - } - } - } - */ - - if (matched) - { - S32 strpos, strpos2; - - std::string url = line.substr(*begin,*end - *begin); - std::string slurlID = "slurl.com/secondlife/"; - strpos = url.find(slurlID); - - if (strpos < 0) - { - slurlID="secondlife://"; - strpos = url.find(slurlID); - } - - if (strpos < 0) - { - slurlID="sl://"; - strpos = url.find(slurlID); - } - - if (strpos >= 0) - { - strpos+=slurlID.length(); - - while ( ( strpos2=url.find("/",strpos) ) == -1 ) - { - if ((*end+2) >= (S32)line.length() || line.substr(*end,1) != " " ) - { - matched=FALSE; - break; - } - - strpos = (*end + 1) - *begin; - - *end = findHTMLToken(line,(*begin + strpos),FALSE); - url = line.substr(*begin,*end - *begin); - } - } - - } - - if (!matched) - { - *begin=*end=0; - } - return matched; -} - - - void LLTextEditor::updateAllowingLanguageInput() { LLWindow* window = getWindow(); @@ -4754,193 +4584,6 @@ void LLTextEditor::onValueChange(S32 start, S32 end) } // -// LLTextSegment -// - -LLTextSegment::~LLTextSegment() -{} - -S32 LLTextSegment::getWidth(S32 first_char, S32 num_chars) const { return 0; } -S32 LLTextSegment::getOffset(S32 segment_local_x_coord, S32 start_offset, S32 num_chars, bool round) const { return 0; } -S32 LLTextSegment::getNumChars(S32 num_pixels, S32 segment_offset, S32 line_offset, S32 max_chars) const { return 0; } -void LLTextSegment::updateLayout(const LLTextEditor& editor) {} -F32 LLTextSegment::draw(S32 start, S32 end, S32 selection_start, S32 selection_end, const LLRect& draw_rect) { return draw_rect.mLeft; } -S32 LLTextSegment::getMaxHeight() const { return 0; } -bool LLTextSegment::canEdit() const { return false; } -void LLTextSegment::unlinkFromDocument(LLTextEditor*) {} -void LLTextSegment::linkToDocument(LLTextEditor*) {} -void LLTextSegment::setHasMouseHover(bool hover) {} -const LLColor4& LLTextSegment::getColor() const { return LLColor4::white; } -void LLTextSegment::setColor(const LLColor4 &color) {} -const LLStyleSP LLTextSegment::getStyle() const {static LLStyleSP sp(new LLStyle()); return sp; } -void LLTextSegment::setStyle(const LLStyleSP &style) {} -void LLTextSegment::setToken( LLKeywordToken* token ) {} -LLKeywordToken* LLTextSegment::getToken() const { return NULL; } -BOOL LLTextSegment::getToolTip( std::string& msg ) const { return FALSE; } -void LLTextSegment::dump() const {} - - -// -// LLNormalTextSegment -// - -LLNormalTextSegment::LLNormalTextSegment( const LLStyleSP& style, S32 start, S32 end, LLTextEditor& editor ) -: LLTextSegment(start, end), - mStyle( style ), - mToken(NULL), - mHasMouseHover(false), - mEditor(editor) -{ - mMaxHeight = llceil(mStyle->getFont()->getLineHeight()); -} - -LLNormalTextSegment::LLNormalTextSegment( const LLColor4& color, S32 start, S32 end, LLTextEditor& editor, BOOL is_visible) -: LLTextSegment(start, end), - mToken(NULL), - mHasMouseHover(false), - mEditor(editor) -{ - mStyle = new LLStyle(LLStyle::Params().visible(is_visible).color(color)); - - mMaxHeight = llceil(mStyle->getFont()->getLineHeight()); -} - -F32 LLNormalTextSegment::draw(S32 start, S32 end, S32 selection_start, S32 selection_end, const LLRect& draw_rect) -{ - if( end - start > 0 ) - { - if ( mStyle->isImage() && (start >= 0) && (end <= mEnd - mStart)) - { - S32 style_image_height = mStyle->mImageHeight; - S32 style_image_width = mStyle->mImageWidth; - LLUIImagePtr image = mStyle->getImage(); - image->draw(draw_rect.mLeft, draw_rect.mTop-style_image_height, - style_image_width, style_image_height); - } - - return drawClippedSegment( getStart() + start, getStart() + end, selection_start, selection_end, draw_rect.mLeft, draw_rect.mBottom); - } - return draw_rect.mLeft; -} - -// Draws a single text segment, reversing the color for selection if needed. -F32 LLNormalTextSegment::drawClippedSegment(S32 seg_start, S32 seg_end, S32 selection_start, S32 selection_end, F32 x, F32 y) -{ - const LLWString &text = mEditor.getWText(); - - F32 right_x = x; - if (!mStyle->isVisible()) - { - return right_x; - } - - const LLFontGL* font = mStyle->getFont(); - - LLColor4 color = mStyle->getColor(); - - font = mStyle->getFont(); - - if( selection_start > seg_start ) - { - // Draw normally - S32 start = seg_start; - S32 end = llmin( selection_start, seg_end ); - S32 length = end - start; - font->render(text, start, x, y, color, LLFontGL::LEFT, LLFontGL::BOTTOM, 0, LLFontGL::NO_SHADOW, length, S32_MAX, &right_x, mEditor.allowsEmbeddedItems()); - } - x = right_x; - - if( (selection_start < seg_end) && (selection_end > seg_start) ) - { - // Draw reversed - S32 start = llmax( selection_start, seg_start ); - S32 end = llmin( selection_end, seg_end ); - S32 length = end - start; - - font->render(text, start, x, y, - LLColor4( 1.f - color.mV[0], 1.f - color.mV[1], 1.f - color.mV[2], 1.f ), - LLFontGL::LEFT, LLFontGL::BOTTOM, 0, LLFontGL::NO_SHADOW, length, S32_MAX, &right_x, mEditor.allowsEmbeddedItems()); - } - x = right_x; - if( selection_end < seg_end ) - { - // Draw normally - S32 start = llmax( selection_end, seg_start ); - S32 end = seg_end; - S32 length = end - start; - font->render(text, start, x, y, color, LLFontGL::LEFT, LLFontGL::BOTTOM, 0, LLFontGL::NO_SHADOW, length, S32_MAX, &right_x, mEditor.allowsEmbeddedItems()); - } - return right_x; -} - -S32 LLNormalTextSegment::getMaxHeight() const -{ - return mMaxHeight; -} - -BOOL LLNormalTextSegment::getToolTip(std::string& msg) const -{ - if (mToken && !mToken->getToolTip().empty()) - { - const LLWString& wmsg = mToken->getToolTip(); - msg = wstring_to_utf8str(wmsg); - return TRUE; - } - return FALSE; -} - - -S32 LLNormalTextSegment::getWidth(S32 first_char, S32 num_chars) const -{ - LLWString text = mEditor.getWText(); - return mStyle->getFont()->getWidth(text.c_str(), mStart + first_char, num_chars); -} - -S32 LLNormalTextSegment::getOffset(S32 segment_local_x_coord, S32 start_offset, S32 num_chars, bool round) const -{ - LLWString text = mEditor.getWText(); - return mStyle->getFont()->charFromPixelOffset(text.c_str(), mStart + start_offset, - (F32)segment_local_x_coord, - F32_MAX, - num_chars, - round); -} - -S32 LLNormalTextSegment::getNumChars(S32 num_pixels, S32 segment_offset, S32 line_offset, S32 max_chars) const -{ - LLWString text = mEditor.getWText(); - S32 num_chars = mStyle->getFont()->maxDrawableChars(text.c_str() + segment_offset + mStart, - (F32)num_pixels, - max_chars, - mEditor.getWordWrap()); - - if (num_chars == 0 - && line_offset == 0 - && max_chars > 0) - { - // If at the beginning of a line, and a single character won't fit, draw it anyway - num_chars = 1; - } - if (mStart + segment_offset + num_chars == mEditor.getLength()) - { - // include terminating NULL - num_chars++; - } - return num_chars; -} - -void LLNormalTextSegment::dump() const -{ - llinfos << "Segment [" << -// mColor.mV[VX] << ", " << -// mColor.mV[VY] << ", " << -// mColor.mV[VZ] << "]\t[" << - mStart << ", " << - getEnd() << "]" << - llendl; -} - -// // LLInlineViewSegment // @@ -4979,11 +4622,15 @@ S32 LLInlineViewSegment::getNumChars(S32 num_pixels, S32 segment_offset, S32 lin } } -void LLInlineViewSegment::updateLayout(const LLTextEditor& editor) +void LLInlineViewSegment::updateLayout(const LLTextBase& editor) { - LLRect start_rect = editor.getLocalRectFromDocIndex(mStart); - LLRect doc_rect = editor.getDocumentPanel()->getRect(); - mView->setOrigin(doc_rect.mLeft + start_rect.mLeft, doc_rect.mBottom + start_rect.mBottom); + const LLTextEditor *ed = dynamic_cast<const LLTextEditor *>(&editor); + if (ed) + { + LLRect start_rect = ed->getLocalRectFromDocIndex(mStart); + LLRect doc_rect = ed->getDocumentPanel()->getRect(); + mView->setOrigin(doc_rect.mLeft + start_rect.mLeft, doc_rect.mBottom + start_rect.mBottom); + } } F32 LLInlineViewSegment::draw(S32 start, S32 end, S32 selection_start, S32 selection_end, const LLRect& draw_rect) @@ -4996,12 +4643,20 @@ S32 LLInlineViewSegment::getMaxHeight() const return mView->getRect().getHeight(); } -void LLInlineViewSegment::unlinkFromDocument(LLTextEditor* editor) +void LLInlineViewSegment::unlinkFromDocument(LLTextBase* editor) { - editor->removeDocumentChild(mView); + LLTextEditor *ed = dynamic_cast<LLTextEditor *>(editor); + if (ed) + { + ed->removeDocumentChild(mView); + } } -void LLInlineViewSegment::linkToDocument(LLTextEditor* editor) +void LLInlineViewSegment::linkToDocument(LLTextBase* editor) { - editor->addDocumentChild(mView); + LLTextEditor *ed = dynamic_cast<LLTextEditor *>(editor); + if (ed) + { + ed->addDocumentChild(mView); + } } diff --git a/indra/llui/lltexteditor.h b/indra/llui/lltexteditor.h index 67c67d0f67..d537751130 100644 --- a/indra/llui/lltexteditor.h +++ b/indra/llui/lltexteditor.h @@ -44,6 +44,7 @@ #include "lleditmenuhandler.h" #include "lldarray.h" #include "llviewborder.h" // for params +#include "lltextbase.h" #include "llpreeditor.h" #include "llcontrol.h" @@ -55,76 +56,6 @@ class LLTextCmd; class LLUICtrlFactory; class LLScrollContainer; -class LLTextSegment : public LLRefCount -{ -public: - LLTextSegment(S32 start, S32 end) : mStart(start), mEnd(end){}; - virtual ~LLTextSegment(); - - virtual S32 getWidth(S32 first_char, S32 num_chars) const; - virtual S32 getOffset(S32 segment_local_x_coord, S32 start_offset, S32 num_chars, bool round) const; - virtual S32 getNumChars(S32 num_pixels, S32 segment_offset, S32 line_offset, S32 max_chars) const; - virtual void updateLayout(const class LLTextEditor& editor); - virtual F32 draw(S32 start, S32 end, S32 selection_start, S32 selection_end, const LLRect& draw_rect); - virtual S32 getMaxHeight() const; - virtual bool canEdit() const; - virtual void unlinkFromDocument(class LLTextEditor* editor); - virtual void linkToDocument(class LLTextEditor* editor); - - virtual void setHasMouseHover(bool hover); - virtual const LLColor4& getColor() const; - virtual void setColor(const LLColor4 &color); - virtual const LLStyleSP getStyle() const; - virtual void setStyle(const LLStyleSP &style); - virtual void setToken( LLKeywordToken* token ); - virtual LLKeywordToken* getToken() const; - virtual BOOL getToolTip( std::string& msg ) const; - virtual void dump() const; - - S32 getStart() const { return mStart; } - void setStart(S32 start) { mStart = start; } - S32 getEnd() const { return mEnd; } - void setEnd( S32 end ) { mEnd = end; } - -protected: - S32 mStart; - S32 mEnd; -}; - -class LLNormalTextSegment : public LLTextSegment -{ -public: - LLNormalTextSegment( const LLStyleSP& style, S32 start, S32 end, LLTextEditor& editor ); - LLNormalTextSegment( const LLColor4& color, S32 start, S32 end, LLTextEditor& editor, BOOL is_visible = TRUE); - - /*virtual*/ S32 getWidth(S32 first_char, S32 num_chars) const; - /*virtual*/ S32 getOffset(S32 segment_local_x_coord, S32 start_offset, S32 num_chars, bool round) const; - /*virtual*/ S32 getNumChars(S32 num_pixels, S32 segment_offset, S32 line_offset, S32 max_chars) const; - /*virtual*/ F32 draw(S32 start, S32 end, S32 selection_start, S32 selection_end, const LLRect& draw_rect); - /*virtual*/ S32 getMaxHeight() const; - /*virtual*/ bool canEdit() const { return true; } - /*virtual*/ void setHasMouseHover(bool hover) { mHasMouseHover = hover; } - /*virtual*/ const LLColor4& getColor() const { return mStyle->getColor(); } - /*virtual*/ void setColor(const LLColor4 &color) { mStyle->setColor(color); } - /*virtual*/ const LLStyleSP getStyle() const { return mStyle; } - /*virtual*/ void setStyle(const LLStyleSP &style) { mStyle = style; } - /*virtual*/ void setToken( LLKeywordToken* token ) { mToken = token; } - /*virtual*/ LLKeywordToken* getToken() const { return mToken; } - /*virtual*/ BOOL getToolTip( std::string& msg ) const; - /*virtual*/ void dump() const; - -protected: - F32 drawClippedSegment(S32 seg_start, S32 seg_end, S32 selection_start, S32 selection_end, F32 x, F32 y); - - class LLTextEditor& mEditor; - LLStyleSP mStyle; - S32 mMaxHeight; - LLKeywordToken* mToken; - bool mHasMouseHover; -}; - -typedef LLPointer<LLTextSegment> LLTextSegmentPtr; - class LLInlineViewSegment : public LLTextSegment { public: @@ -132,24 +63,22 @@ public: ~LLInlineViewSegment(); /*virtual*/ S32 getWidth(S32 first_char, S32 num_chars) const; /*virtual*/ S32 getNumChars(S32 num_pixels, S32 segment_offset, S32 line_offset, S32 max_chars) const; - /*virtual*/ void updateLayout(const class LLTextEditor& editor); + /*virtual*/ void updateLayout(const class LLTextBase& editor); /*virtual*/ F32 draw(S32 start, S32 end, S32 selection_start, S32 selection_end, const LLRect& draw_rect); /*virtuaL*/ S32 getMaxHeight() const; /*virtual*/ bool canEdit() const { return false; } - /*virtual*/ void unlinkFromDocument(class LLTextEditor* editor); - /*virtual*/ void linkToDocument(class LLTextEditor* editor); + /*virtual*/ void unlinkFromDocument(class LLTextBase* editor); + /*virtual*/ void linkToDocument(class LLTextBase* editor); private: LLView* mView; }; -class LLIndexSegment : public LLTextSegment -{ -public: - LLIndexSegment(S32 pos) : LLTextSegment(pos, pos) {} -}; - -class LLTextEditor : public LLUICtrl, LLEditMenuHandler, protected LLPreeditor +class LLTextEditor : + public LLTextBase, + public LLUICtrl, + private LLEditMenuHandler, + protected LLPreeditor { public: struct Params : public LLInitParam::Block<Params, LLUICtrl::Params> @@ -208,11 +137,8 @@ public: } }; - typedef std::multiset<LLTextSegmentPtr, compare_segment_end> segment_set_t; - virtual ~LLTextEditor(); - void setParseHTML(BOOL parsing) {mParseHTML=parsing;} void setParseHighlights(BOOL parsing) {mParseHighlights=parsing;} // mousehandler overrides @@ -277,6 +203,7 @@ public: BOOL replaceText(const std::string& search_text, const std::string& replace_text, BOOL case_insensitive, BOOL wrap = TRUE); void replaceTextAll(const std::string& search_text, const std::string& replace_text, BOOL case_insensitive); BOOL hasSelection() const { return (mSelectionStart !=mSelectionEnd); } + void replaceUrlLabel(const std::string &url, const std::string &label); // Undo/redo stack void blockUndo(); @@ -285,7 +212,6 @@ public: virtual void makePristine(); BOOL isPristine() const; BOOL allowsEmbeddedItems() const { return mAllowEmbeddedItems; } - BOOL getWordWrap() { return mWordWrap; } S32 getLength() const { return getWText().length(); } void setReadOnly(bool read_only) { mReadOnly = read_only; } bool getReadOnly() { return mReadOnly; } @@ -352,13 +278,11 @@ public: const LLUUID& getSourceID() const { return mSourceID; } // Callbacks - static void setURLCallbacks(void (*callback1) (const std::string& url), - bool (*callback2) (const std::string& url), - bool (*callback3) (const std::string& url) ) - { sURLcallback = callback1; sSecondlifeURLcallback = callback2; sSecondlifeURLcallbackRightClick = callback3;} - std::string getText() const; + // Callback for when a Url has been resolved by the server + void onUrlLabelUpdated(const std::string &url, const std::string &label); + // Getters LLWString getWText() const; llwchar getWChar(S32 pos) const { return getWText()[pos]; } @@ -382,8 +306,6 @@ protected: void startOfDoc(); void endOfDoc(); - void getSegmentAndOffset( S32 startpos, segment_set_t::const_iterator* seg_iter, S32* offsetp ) const; - void getSegmentAndOffset( S32 startpos, segment_set_t::iterator* seg_iter, S32* offsetp ) ; void drawPreeditMarker(); void needsReflow() { mReflowNeeded = TRUE; } @@ -399,16 +321,12 @@ protected: void removeCharOrTab(); void setCursorAtLocalPos(S32 x, S32 y, bool round, bool keep_cursor_offset = false); - S32 getDocIndexFromLocalCoord( S32 local_x, S32 local_y, BOOL round ) const; + /*virtual*/ S32 getDocIndexFromLocalCoord( S32 local_x, S32 local_y, BOOL round ) const; void indentSelectedLines( S32 spaces ); S32 indentLine( S32 pos, S32 spaces ); void unindentLineBeforeCloseBrace(); - LLTextSegmentPtr getSegmentAtLocalPos(S32 x, S32 y); - segment_set_t::iterator getSegIterContaining(S32 index); - segment_set_t::const_iterator getSegIterContaining(S32 index) const; - void reportBadKeystroke() { make_ui_sound("UISndBadKeystroke"); } BOOL handleNavigationKey(const KEY key, const MASK mask); @@ -438,15 +356,9 @@ protected: void findEmbeddedItemSegments(S32 start, S32 end); void insertSegment(LLTextSegmentPtr segment_to_insert); - - virtual BOOL handleMouseUpOverSegment(S32 x, S32 y, MASK mask); - virtual llwchar pasteEmbeddedItem(llwchar ext_char) { return ext_char; } - S32 findHTMLToken(const std::string &line, S32 pos, BOOL reverse) const; - BOOL findHTML(const std::string &line, S32 *begin, S32 *end) const; - // Abstract inner base class representing an undoable editor command. // Concrete sub-classes can be defined for operations such as insert, remove, etc. // Used as arguments to the execute() method below. @@ -538,13 +450,8 @@ protected: S32 mLastSelectionX; S32 mLastSelectionY; - BOOL mParseHTML; BOOL mParseHighlights; - std::string mHTML; - segment_set_t mSegments; - LLTextSegmentPtr mHoverSegment; - // Scrollbar data class DocumentPanel* mDocumentPanel; LLScrollContainer* mScroller; @@ -569,10 +476,10 @@ protected: LLUIColor mLinkColor; BOOL mReadOnly; - BOOL mWordWrap; BOOL mShowLineNumbers; void updateSegments(); + void updateLinkSegments(); private: @@ -584,7 +491,6 @@ private: virtual LLTextViewModel* getViewModel() const; void reflow(S32 startpos = 0); - void clearSegments(); void createDefaultSegment(); LLStyleSP getDefaultStyle(); S32 getEditableIndex(S32 index, bool increasing_direction); @@ -601,9 +507,6 @@ private: // Data // LLKeywords mKeywords; - static void (*sURLcallback) (const std::string& url); - static bool (*sSecondlifeURLcallback) (const std::string& url); - static bool (*sSecondlifeURLcallbackRightClick) (const std::string& url); // Concrete LLTextCmd sub-classes used by the LLTextEditor base class class LLTextCmdInsert; @@ -613,8 +516,6 @@ private: S32 mMaxTextByteLength; // Maximum length mText is allowed to be in bytes - const LLFontGL* mDefaultFont; - class LLViewBorder* mBorder; BOOL mBaseDocIsPristine; diff --git a/indra/llui/llurlaction.cpp b/indra/llui/llurlaction.cpp new file mode 100644 index 0000000000..3b689b93c0 --- /dev/null +++ b/indra/llui/llurlaction.cpp @@ -0,0 +1,137 @@ +/** + * @file llurlaction.cpp + * @author Martin Reddy + * @brief A set of actions that can performed on Urls + * + * $LicenseInfo:firstyear=2009&license=viewergpl$ + * + * Copyright (c) 2009, Linden Research, Inc. + * + * Second Life Viewer Source Code + * The source code in this file ("Source Code") is provided by Linden Lab + * to you under the terms of the GNU General Public License, version 2.0 + * ("GPL"), unless you have obtained a separate licensing agreement + * ("Other License"), formally executed by you and Linden Lab. Terms of + * the GPL can be found in doc/GPL-license.txt in this distribution, or + * online at http://secondlifegrid.net/programs/open_source/licensing/gplv2 + * + * There are special exceptions to the terms and conditions of the GPL as + * it is applied to this Source Code. View the full text of the exception + * in the file doc/FLOSS-exception.txt in this software distribution, or + * online at + * http://secondlifegrid.net/programs/open_source/licensing/flossexception + * + * By copying, modifying or distributing this software, you acknowledge + * that you have read and understood your obligations described above, + * and agree to abide by those obligations. + * + * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO + * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY, + * COMPLETENESS OR PERFORMANCE. + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "llurlaction.h" +#include "llview.h" +#include "llwindow.h" +#include "llurlregistry.h" + +// global state for the callback functions +void (*LLUrlAction::sOpenURLCallback) (const std::string& url) = NULL; +void (*LLUrlAction::sOpenURLInternalCallback) (const std::string& url) = NULL; +void (*LLUrlAction::sOpenURLExternalCallback) (const std::string& url) = NULL; +bool (*LLUrlAction::sExecuteSLURLCallback) (const std::string& url) = NULL; + + +void LLUrlAction::setOpenURLCallback(void (*cb) (const std::string& url)) +{ + sOpenURLCallback = cb; +} + +void LLUrlAction::setOpenURLInternalCallback(void (*cb) (const std::string& url)) +{ + sOpenURLInternalCallback = cb; +} + +void LLUrlAction::setOpenURLExternalCallback(void (*cb) (const std::string& url)) +{ + sOpenURLExternalCallback = cb; +} + +void LLUrlAction::setExecuteSLURLCallback(bool (*cb) (const std::string& url)) +{ + sExecuteSLURLCallback = cb; +} + +void LLUrlAction::openURL(std::string url) +{ + if (sOpenURLCallback) + { + (*sOpenURLCallback)(url); + } +} + +void LLUrlAction::openURLInternal(std::string url) +{ + if (sOpenURLInternalCallback) + { + (*sOpenURLInternalCallback)(url); + } +} + +void LLUrlAction::openURLExternal(std::string url) +{ + if (sOpenURLExternalCallback) + { + (*sOpenURLExternalCallback)(url); + } +} + +void LLUrlAction::executeSLURL(std::string url) +{ + if (sExecuteSLURLCallback) + { + (*sExecuteSLURLCallback)(url); + } +} + +void LLUrlAction::clickAction(std::string url) +{ + // Try to handle as SLURL first, then http Url + if ( (sExecuteSLURLCallback) && !(*sExecuteSLURLCallback)(url) ) + { + if (sOpenURLCallback) + { + (*sOpenURLCallback)(url); + } + } +} + +void LLUrlAction::teleportToLocation(std::string url) +{ + LLUrlMatch match; + if (LLUrlRegistry::instance().findUrl(url, match)) + { + if (! match.getLocation().empty()) + { + executeSLURL("secondlife:///app/teleport/" + match.getLocation()); + } + } +} + +void LLUrlAction::copyURLToClipboard(std::string url) +{ + LLView::getWindow()->copyTextToClipboard(utf8str_to_wstring(url)); +} + +void LLUrlAction::copyLabelToClipboard(std::string url) +{ + LLUrlMatch match; + if (LLUrlRegistry::instance().findUrl(url, match)) + { + LLView::getWindow()->copyTextToClipboard(utf8str_to_wstring(match.getLabel())); + } +} + diff --git a/indra/llui/llurlaction.h b/indra/llui/llurlaction.h new file mode 100644 index 0000000000..6b9d565b44 --- /dev/null +++ b/indra/llui/llurlaction.h @@ -0,0 +1,93 @@ +/** + * @file llurlaction.h + * @author Martin Reddy + * @brief A set of actions that can performed on Urls + * + * $LicenseInfo:firstyear=2009&license=viewergpl$ + * + * Copyright (c) 2009, Linden Research, Inc. + * + * Second Life Viewer Source Code + * The source code in this file ("Source Code") is provided by Linden Lab + * to you under the terms of the GNU General Public License, version 2.0 + * ("GPL"), unless you have obtained a separate licensing agreement + * ("Other License"), formally executed by you and Linden Lab. Terms of + * the GPL can be found in doc/GPL-license.txt in this distribution, or + * online at http://secondlifegrid.net/programs/open_source/licensing/gplv2 + * + * There are special exceptions to the terms and conditions of the GPL as + * it is applied to this Source Code. View the full text of the exception + * in the file doc/FLOSS-exception.txt in this software distribution, or + * online at + * http://secondlifegrid.net/programs/open_source/licensing/flossexception + * + * By copying, modifying or distributing this software, you acknowledge + * that you have read and understood your obligations described above, + * and agree to abide by those obligations. + * + * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO + * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY, + * COMPLETENESS OR PERFORMANCE. + * $/LicenseInfo$ + */ + +#ifndef LL_LLURLACTION_H +#define LL_LLURLACTION_H + +#include <string> + +/// +/// The LLUrlAction class provides a number of static functions that +/// let you open Urls in web browsers, execute SLURLs, and copy Urls +/// to the clipboard. Many of these functions are not available at +/// the llui level, and must be supplied via a set of callbacks. +/// +/// N.B. The action functions specifically do not use const ref +/// strings so that a url parameter can be used into a boost::bind() +/// call under situations when that input string is deallocated before +/// the callback is executed. +/// +class LLUrlAction +{ +public: + LLUrlAction(); + + /// load a Url in the user's preferred web browser + static void openURL(std::string url); + + /// load a Url in the internal Second Life web browser + static void openURLInternal(std::string url); + + /// load a Url in the operating system's default web browser + static void openURLExternal(std::string url); + + /// execute the given secondlife: SLURL + static void executeSLURL(std::string url); + + /// if the Url specifies an SL location, teleport there + static void teleportToLocation(std::string url); + + /// perform the appropriate action for left-clicking on a Url + static void clickAction(std::string url); + + /// copy the label for a Url to the clipboard + static void copyLabelToClipboard(std::string url); + + /// copy a Url to the clipboard + static void copyURLToClipboard(std::string url); + + /// specify the callbacks to enable this class's functionality + static void setOpenURLCallback(void (*cb) (const std::string& url)); + static void setOpenURLInternalCallback(void (*cb) (const std::string& url)); + static void setOpenURLExternalCallback(void (*cb) (const std::string& url)); + static void setExecuteSLURLCallback(bool (*cb) (const std::string& url)); + +private: + // callbacks for operations we can perform on Urls + static void (*sOpenURLCallback) (const std::string& url); + static void (*sOpenURLInternalCallback) (const std::string& url); + static void (*sOpenURLExternalCallback) (const std::string& url); + static bool (*sExecuteSLURLCallback) (const std::string& url); +}; + +#endif diff --git a/indra/llui/llurlentry.cpp b/indra/llui/llurlentry.cpp new file mode 100644 index 0000000000..85f9064115 --- /dev/null +++ b/indra/llui/llurlentry.cpp @@ -0,0 +1,546 @@ +/** + * @file llurlentry.cpp + * @author Martin Reddy + * @brief Describes the Url types that can be registered in LLUrlRegistry + * + * $LicenseInfo:firstyear=2009&license=viewergpl$ + * + * Copyright (c) 2009, Linden Research, Inc. + * + * Second Life Viewer Source Code + * The source code in this file ("Source Code") is provided by Linden Lab + * to you under the terms of the GNU General Public License, version 2.0 + * ("GPL"), unless you have obtained a separate licensing agreement + * ("Other License"), formally executed by you and Linden Lab. Terms of + * the GPL can be found in doc/GPL-license.txt in this distribution, or + * online at http://secondlifegrid.net/programs/open_source/licensing/gplv2 + * + * There are special exceptions to the terms and conditions of the GPL as + * it is applied to this Source Code. View the full text of the exception + * in the file doc/FLOSS-exception.txt in this software distribution, or + * online at + * http://secondlifegrid.net/programs/open_source/licensing/flossexception + * + * By copying, modifying or distributing this software, you acknowledge + * that you have read and understood your obligations described above, + * and agree to abide by those obligations. + * + * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO + * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY, + * COMPLETENESS OR PERFORMANCE. + * $/LicenseInfo$ + */ + +#include "linden_common.h" +#include "llurlentry.h" +#include "lluri.h" +#include "llcachename.h" +#include "lltrans.h" + +LLUrlEntryBase::LLUrlEntryBase() +{ +} + +LLUrlEntryBase::~LLUrlEntryBase() +{ +} + +std::string LLUrlEntryBase::getUrl(const std::string &string) +{ + return escapeUrl(string); +} + +std::string LLUrlEntryBase::getIDStringFromUrl(const std::string &url) const +{ + // return the id from a SLURL in the format /app/{cmd}/{id}/about + LLURI uri(url); + LLSD path_array = uri.pathArray(); + if (path_array.size() == 4) + { + return path_array.get(2).asString(); + } + return ""; +} + +std::string LLUrlEntryBase::unescapeUrl(const std::string &url) const +{ + return LLURI::unescape(url); +} + +std::string LLUrlEntryBase::escapeUrl(const std::string &url) const +{ + static std::string no_escape_chars; + static bool initialized = false; + if (!initialized) + { + no_escape_chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789" + "-._~!$?&()*+,@:;=/%"; + + std::sort(no_escape_chars.begin(), no_escape_chars.end()); + initialized = true; + } + return LLURI::escape(url, no_escape_chars, true); +} + +std::string LLUrlEntryBase::getLabelFromWikiLink(const std::string &url) +{ + // return the label part from [http://www.example.org Label] + const char *text = url.c_str(); + S32 start = 0; + while (! isspace(text[start])) + { + start++; + } + while (text[start] == ' ' || text[start] == '\t') + { + start++; + } + return url.substr(start, url.size()-start-1); +} + +std::string LLUrlEntryBase::getUrlFromWikiLink(const std::string &string) +{ + // return the url part from [http://www.example.org Label] + const char *text = string.c_str(); + S32 end = 0; + while (! isspace(text[end])) + { + end++; + } + return escapeUrl(string.substr(1, end-1)); +} + +void LLUrlEntryBase::addObserver(const std::string &id, + const std::string &url, + const LLUrlLabelCallback &cb) +{ + // add a callback to be notified when we have a label for the uuid + LLUrlEntryObserver observer; + observer.url = url; + observer.signal = new LLUrlLabelSignal(); + if (observer.signal) + { + observer.signal->connect(cb); + mObservers.insert(std::pair<std::string, LLUrlEntryObserver>(id, observer)); + } +} + +void LLUrlEntryBase::callObservers(const std::string &id, const std::string &label) +{ + // notify all callbacks waiting on the given uuid + std::multimap<std::string, LLUrlEntryObserver>::iterator it; + for (it = mObservers.find(id); it != mObservers.end();) + { + // call the callback - give it the new label + LLUrlEntryObserver &observer = it->second; + (*observer.signal)(it->second.url, label); + // then remove the signal - we only need to call it once + delete observer.signal; + mObservers.erase(it++); + } +} + +// +// LLUrlEntryHTTP Describes generic http: and https: Urls +// +LLUrlEntryHTTP::LLUrlEntryHTTP() +{ + mPattern = boost::regex("https?://([-\\w\\.]+)+(:\\d+)?(:\\w+)?(@\\d+)?(@\\w+)?/?\\S*", + boost::regex::perl|boost::regex::icase); + mMenuName = "menu_url_http.xml"; + mTooltip = LLTrans::getString("TooltipHttpUrl"); + //mIcon = "gear.tga"; +} + +std::string LLUrlEntryHTTP::getLabel(const std::string &url, const LLUrlLabelCallback &cb) +{ + return unescapeUrl(url); +} + +// +// LLUrlEntryHTTP Describes generic http: and https: Urls with custom label +// We use the wikipedia syntax of [http://www.example.org Text] +// +LLUrlEntryHTTPLabel::LLUrlEntryHTTPLabel() +{ + mPattern = boost::regex("\\[https?://\\S+[ \t]+[^\\]]+\\]", + boost::regex::perl|boost::regex::icase); + mMenuName = "menu_url_http.xml"; + mTooltip = LLTrans::getString("TooltipHttpUrl"); +} + +std::string LLUrlEntryHTTPLabel::getLabel(const std::string &url, const LLUrlLabelCallback &cb) +{ + return getLabelFromWikiLink(url); +} + +std::string LLUrlEntryHTTPLabel::getUrl(const std::string &string) +{ + return getUrlFromWikiLink(string); +} + +// +// LLUrlEntrySLURL Describes generic http: and https: Urls +// +LLUrlEntrySLURL::LLUrlEntrySLURL() +{ + // see http://slurl.com/about.php for details on the SLURL format + mPattern = boost::regex("http://slurl.com/secondlife/\\S+/?(\\d+)?/?(\\d+)?/?(\\d+)?/?\\S*", + boost::regex::perl|boost::regex::icase); + mMenuName = "menu_url_slurl.xml"; + mTooltip = LLTrans::getString("TooltipSLURL"); +} + +std::string LLUrlEntrySLURL::getLabel(const std::string &url, const LLUrlLabelCallback &cb) +{ + // + // we handle SLURLs in the following formats: + // - http://slurl.com/secondlife/Place/X/Y/Z + // - http://slurl.com/secondlife/Place/X/Y + // - http://slurl.com/secondlife/Place/X + // - http://slurl.com/secondlife/Place + // + LLURI uri(url); + LLSD path_array = uri.pathArray(); + S32 path_parts = path_array.size(); + if (path_parts == 5) + { + // handle slurl with (X,Y,Z) coordinates + std::string location = unescapeUrl(path_array[path_parts-4]); + std::string x = path_array[path_parts-3]; + std::string y = path_array[path_parts-2]; + std::string z = path_array[path_parts-1]; + return location + " (" + x + "," + y + "," + z + ")"; + } + else if (path_parts == 4) + { + // handle slurl with (X,Y) coordinates + std::string location = unescapeUrl(path_array[path_parts-3]); + std::string x = path_array[path_parts-2]; + std::string y = path_array[path_parts-1]; + return location + " (" + x + "," + y + ")"; + } + else if (path_parts == 3) + { + // handle slurl with (X) coordinate + std::string location = unescapeUrl(path_array[path_parts-2]); + std::string x = path_array[path_parts-1]; + return location + " (" + x + ")"; + } + else if (path_parts == 2) + { + // handle slurl with no coordinates + std::string location = unescapeUrl(path_array[path_parts-1]); + return location; + } + + return url; +} + +std::string LLUrlEntrySLURL::getLocation(const std::string &url) const +{ + // return the part of the Url after slurl.com/secondlife/ + const std::string search_string = "secondlife"; + size_t pos = url.find(search_string); + if (pos == std::string::npos) + { + return ""; + } + + pos += search_string.size() + 1; + return url.substr(pos, url.size() - pos); +} + +// +// LLUrlEntryAgent Describes a Second Life agent Url, e.g., +// secondlife:///app/agent/0e346d8b-4433-4d66-a6b0-fd37083abc4c/about +// +LLUrlEntryAgent::LLUrlEntryAgent() +{ + mPattern = boost::regex("secondlife:///app/agent/[\\da-f-]+/about", + boost::regex::perl|boost::regex::icase); + mMenuName = "menu_url_agent.xml"; + mTooltip = LLTrans::getString("TooltipAgentUrl"); +} + +void LLUrlEntryAgent::onAgentNameReceived(const LLUUID& id, + const std::string& first, + const std::string& last, + BOOL is_group) +{ + // received the agent name from the server - tell our observers + callObservers(id.asString(), first + " " + last); +} + +std::string LLUrlEntryAgent::getLabel(const std::string &url, const LLUrlLabelCallback &cb) +{ + std::string id = getIDStringFromUrl(url); + if (gCacheName && ! id.empty()) + { + LLUUID uuid(id); + std::string full_name; + if (gCacheName->getFullName(uuid, full_name)) + { + return full_name; + } + else + { + gCacheName->get(uuid, FALSE, boost::bind(&LLUrlEntryAgent::onAgentNameReceived, this, _1, _2, _3, _4)); + addObserver(id, url, cb); + } + } + + return unescapeUrl(url); +} + +// +// LLUrlEntryGroup Describes a Second Life group Url, e.g., +// secondlife:///app/group/00005ff3-4044-c79f-9de8-fb28ae0df991/about +// +LLUrlEntryGroup::LLUrlEntryGroup() +{ + mPattern = boost::regex("secondlife:///app/group/[\\da-f-]+/about", + boost::regex::perl|boost::regex::icase); + mMenuName = "menu_url_group.xml"; + mTooltip = LLTrans::getString("TooltipGroupUrl"); +} + +void LLUrlEntryGroup::onGroupNameReceived(const LLUUID& id, + const std::string& first, + const std::string& last, + BOOL is_group) +{ + // received the group name from the server - tell our observers + callObservers(id.asString(), first); +} + +std::string LLUrlEntryGroup::getLabel(const std::string &url, const LLUrlLabelCallback &cb) +{ + std::string id = getIDStringFromUrl(url); + if (gCacheName && ! id.empty()) + { + LLUUID uuid(id); + std::string group_name; + if (gCacheName->getGroupName(uuid, group_name)) + { + return group_name; + } + else + { + gCacheName->get(uuid, TRUE, boost::bind(&LLUrlEntryGroup::onGroupNameReceived, this, _1, _2, _3, _4)); + addObserver(id, url, cb); + } + } + + return unescapeUrl(url); +} + +/// +/// LLUrlEntryEvent Describes a Second Life event Url, e.g., +/// secondlife:///app/event/700727/about +/// +LLUrlEntryEvent::LLUrlEntryEvent() +{ + mPattern = boost::regex("secondlife:///app/event/[\\da-f-]+/about", + boost::regex::perl|boost::regex::icase); + mMenuName = "menu_url_event.xml"; + mTooltip = LLTrans::getString("TooltipEventUrl"); +} + +std::string LLUrlEntryEvent::getLabel(const std::string &url, const LLUrlLabelCallback &cb) +{ + return unescapeUrl(url); +} + +/// +/// LLUrlEntryClassified Describes a Second Life classified Url, e.g., +/// secondlife:///app/classified/00128854-c36a-5649-7ca6-5dfaa7514ab2/about +/// +LLUrlEntryClassified::LLUrlEntryClassified() +{ + mPattern = boost::regex("secondlife:///app/classified/[\\da-f-]+/about", + boost::regex::perl|boost::regex::icase); + mMenuName = "menu_url_classified.xml"; + mTooltip = LLTrans::getString("TooltipClassifiedUrl"); +} + +std::string LLUrlEntryClassified::getLabel(const std::string &url, const LLUrlLabelCallback &cb) +{ + return unescapeUrl(url); +} + +/// +/// LLUrlEntryParcel Describes a Second Life parcel Url, e.g., +/// secondlife:///app/parcel/0000060e-4b39-e00b-d0c3-d98b1934e3a8/about +/// +LLUrlEntryParcel::LLUrlEntryParcel() +{ + mPattern = boost::regex("secondlife:///app/parcel/[\\da-f-]+/about", + boost::regex::perl|boost::regex::icase); + mMenuName = "menu_url_parcel.xml"; + mTooltip = LLTrans::getString("TooltipParcelUrl"); +} + +std::string LLUrlEntryParcel::getLabel(const std::string &url, const LLUrlLabelCallback &cb) +{ + return unescapeUrl(url); +} + +// +// LLUrlEntryTeleport Describes a Second Life teleport Url, e.g., +// secondlife:///app/teleport/Ahern/50/50/50/ +// +LLUrlEntryTeleport::LLUrlEntryTeleport() +{ + mPattern = boost::regex("secondlife:///app/teleport/\\S+(/\\d+)?(/\\d+)?(/\\d+)?/?\\S*", + boost::regex::perl|boost::regex::icase); + mMenuName = "menu_url_teleport.xml"; + mTooltip = LLTrans::getString("TooltipTeleportUrl"); +} + +std::string LLUrlEntryTeleport::getLabel(const std::string &url, const LLUrlLabelCallback &cb) +{ + // + // we handle teleport SLURLs in the following formats: + // - secondlife:///app/teleport/Place/X/Y/Z + // - secondlife:///app/teleport/Place/X/Y + // - secondlife:///app/teleport/Place/X + // - secondlife:///app/teleport/Place + // + LLURI uri(url); + LLSD path_array = uri.pathArray(); + S32 path_parts = path_array.size(); + if (path_parts == 6) + { + // handle teleport url with (X,Y,Z) coordinates + std::string location = unescapeUrl(path_array[path_parts-4]); + std::string x = path_array[path_parts-3]; + std::string y = path_array[path_parts-2]; + std::string z = path_array[path_parts-1]; + return "Teleport to " + location + " (" + x + "," + y + "," + z + ")"; + } + else if (path_parts == 5) + { + // handle teleport url with (X,Y) coordinates + std::string location = unescapeUrl(path_array[path_parts-3]); + std::string x = path_array[path_parts-2]; + std::string y = path_array[path_parts-1]; + return "Teleport to " + location + " (" + x + "," + y + ")"; + } + else if (path_parts == 4) + { + // handle teleport url with (X) coordinate only + std::string location = unescapeUrl(path_array[path_parts-2]); + std::string x = path_array[path_parts-1]; + return "Teleport to " + location + " (" + x + ")"; + } + else if (path_parts == 3) + { + // handle teleport url with no coordinates + std::string location = unescapeUrl(path_array[path_parts-1]); + return "Teleport to " + location; + } + + return url; +} + +std::string LLUrlEntryTeleport::getLocation(const std::string &url) const +{ + // return the part of the Url after ///app/teleport + const std::string search_string = "teleport"; + size_t pos = url.find(search_string); + if (pos == std::string::npos) + { + return ""; + } + + pos += search_string.size() + 1; + return url.substr(pos, url.size() - pos); +} + +/// +/// LLUrlEntryObjectIM Describes a Second Life object instant msg Url, e.g., +/// secondlife:///app/objectim/<sessionid> +/// +LLUrlEntryObjectIM::LLUrlEntryObjectIM() +{ + mPattern = boost::regex("secondlife:///app/objectim/[\\da-f-]+\\??\\S*", + boost::regex::perl|boost::regex::icase); + mMenuName = "menu_url_objectim.xml"; + mTooltip = LLTrans::getString("TooltipObjectIMUrl"); +} + +std::string LLUrlEntryObjectIM::getLabel(const std::string &url, const LLUrlLabelCallback &cb) +{ + LLURI uri(url); + LLSD params = uri.queryMap(); + if (params.has("name")) + { + // look for a ?name=<obj-name> param in the url + // and use that as the label if present. + std::string name = params.get("name"); + LLStringUtil::trim(name); + if (name.empty()) + { + name = LLTrans::getString("Unnamed"); + } + return name; + } + + return unescapeUrl(url); +} + +std::string LLUrlEntryObjectIM::getLocation(const std::string &url) const +{ + LLURI uri(url); + LLSD params = uri.queryMap(); + if (params.has("slurl")) + { + return params.get("slurl"); + } + + return ""; +} + +// +// LLUrlEntrySL Describes a generic SLURL, e.g., a Url that starts +// with secondlife:// (used as a catch-all for cases not matched above) +// +LLUrlEntrySL::LLUrlEntrySL() +{ + mPattern = boost::regex("secondlife://(\\w+)?(:\\d+)?/\\S+", + boost::regex::perl|boost::regex::icase); + mMenuName = "menu_url_slapp.xml"; + mTooltip = LLTrans::getString("TooltipSLAPP"); +} + +std::string LLUrlEntrySL::getLabel(const std::string &url, const LLUrlLabelCallback &cb) +{ + return unescapeUrl(url); +} + +// +// LLUrlEntrySLLabel Describes a generic SLURL, e.g., a Url that starts +/// with secondlife:// with the ability to specify a custom label. +// +LLUrlEntrySLLabel::LLUrlEntrySLLabel() +{ + mPattern = boost::regex("\\[secondlife://\\S+[ \t]+[^\\]]+\\]", + boost::regex::perl|boost::regex::icase); + mMenuName = "menu_url_slapp.xml"; + mTooltip = LLTrans::getString("TooltipSLAPP"); +} + +std::string LLUrlEntrySLLabel::getLabel(const std::string &url, const LLUrlLabelCallback &cb) +{ + return getLabelFromWikiLink(url); +} + +std::string LLUrlEntrySLLabel::getUrl(const std::string &string) +{ + return getUrlFromWikiLink(string); +} + diff --git a/indra/llui/llurlentry.h b/indra/llui/llurlentry.h new file mode 100644 index 0000000000..f3e76dbec0 --- /dev/null +++ b/indra/llui/llurlentry.h @@ -0,0 +1,252 @@ +/** + * @file llurlentry.h + * @author Martin Reddy + * @brief Describes the Url types that can be registered in LLUrlRegistry + * + * $LicenseInfo:firstyear=2009&license=viewergpl$ + * + * Copyright (c) 2009, Linden Research, Inc. + * + * Second Life Viewer Source Code + * The source code in this file ("Source Code") is provided by Linden Lab + * to you under the terms of the GNU General Public License, version 2.0 + * ("GPL"), unless you have obtained a separate licensing agreement + * ("Other License"), formally executed by you and Linden Lab. Terms of + * the GPL can be found in doc/GPL-license.txt in this distribution, or + * online at http://secondlifegrid.net/programs/open_source/licensing/gplv2 + * + * There are special exceptions to the terms and conditions of the GPL as + * it is applied to this Source Code. View the full text of the exception + * in the file doc/FLOSS-exception.txt in this software distribution, or + * online at + * http://secondlifegrid.net/programs/open_source/licensing/flossexception + * + * By copying, modifying or distributing this software, you acknowledge + * that you have read and understood your obligations described above, + * and agree to abide by those obligations. + * + * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO + * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY, + * COMPLETENESS OR PERFORMANCE. + * $/LicenseInfo$ + */ + +#ifndef LL_LLURLENTRY_H +#define LL_LLURLENTRY_H + +#include "lluuid.h" + +#include <boost/signals2.hpp> +#include <boost/regex.hpp> +#include <string> +#include <map> + +typedef boost::signals2::signal<void (const std::string& url, + const std::string& label)> LLUrlLabelSignal; +typedef LLUrlLabelSignal::slot_type LLUrlLabelCallback; + +/// +/// LLUrlEntryBase is the base class of all Url types registered in the +/// LLUrlRegistry. Each derived classes provides a regular expression +/// to match the Url type (e.g., http://... or secondlife://...) along +/// with an optional icon to display next to instances of the Url in +/// a text display and a XUI file to use for any context menu popup. +/// Functions are also provided to compute an appropriate label and +/// tooltip/status bar text for the Url. +/// +/// Some derived classes of LLUrlEntryBase may wish to compute an +/// appropriate label for a Url by asking the server for information. +/// You must therefore provide a callback method, so that you can be +/// notified when an updated label has been received from the server. +/// This label should then be used to replace any previous label +/// that you received from getLabel() for the Url in question. +/// +class LLUrlEntryBase +{ +public: + LLUrlEntryBase(); + virtual ~LLUrlEntryBase(); + + /// Return the regex pattern that matches this Url + boost::regex getPattern() const { return mPattern; } + + /// Return the url from a string that matched the regex + virtual std::string getUrl(const std::string &string); + + /// Given a matched Url, return a label for the Url + virtual std::string getLabel(const std::string &url, const LLUrlLabelCallback &cb) { return url; } + + /// Return an icon that can be displayed next to Urls of this type + const std::string &getIcon() const { return mIcon; } + + /// Given a matched Url, return a tooltip string for the hyperlink + std::string getTooltip() const { return mTooltip; } + + /// Return the name of a XUI file containing the context menu items + const std::string getMenuName() const { return mMenuName; } + + /// Return the name of a SL location described by this Url, if any + virtual std::string getLocation(const std::string &url) const { return ""; } + +protected: + std::string getIDStringFromUrl(const std::string &url) const; + std::string escapeUrl(const std::string &url) const; + std::string unescapeUrl(const std::string &url) const; + std::string getLabelFromWikiLink(const std::string &url); + std::string getUrlFromWikiLink(const std::string &string); + void addObserver(const std::string &id, const std::string &url, const LLUrlLabelCallback &cb); + void callObservers(const std::string &id, const std::string &label); + + typedef struct { + std::string url; + LLUrlLabelSignal *signal; + } LLUrlEntryObserver; + + boost::regex mPattern; + std::string mIcon; + std::string mMenuName; + std::string mTooltip; + std::multimap<std::string, LLUrlEntryObserver> mObservers; +}; + +/// +/// LLUrlEntryHTTP Describes generic http: and https: Urls +/// +class LLUrlEntryHTTP : public LLUrlEntryBase +{ +public: + LLUrlEntryHTTP(); + /*virtual*/ std::string getLabel(const std::string &url, const LLUrlLabelCallback &cb); +}; + +/// +/// LLUrlEntryHTTPLabel Describes generic http: and https: Urls with custom labels +/// +class LLUrlEntryHTTPLabel : public LLUrlEntryBase +{ +public: + LLUrlEntryHTTPLabel(); + /*virtual*/ std::string getLabel(const std::string &url, const LLUrlLabelCallback &cb); + /*virtual*/ std::string getUrl(const std::string &string); +}; + +/// +/// LLUrlEntrySLURL Describes http://slurl.com/... Urls +/// +class LLUrlEntrySLURL : public LLUrlEntryBase +{ +public: + LLUrlEntrySLURL(); + /*virtual*/ std::string getLabel(const std::string &url, const LLUrlLabelCallback &cb); + /*virtual*/ std::string getLocation(const std::string &url) const; +}; + +/// +/// LLUrlEntryAgent Describes a Second Life agent Url, e.g., +/// secondlife:///app/agent/0e346d8b-4433-4d66-a6b0-fd37083abc4c/about +/// +class LLUrlEntryAgent : public LLUrlEntryBase +{ +public: + LLUrlEntryAgent(); + /*virtual*/ std::string getLabel(const std::string &url, const LLUrlLabelCallback &cb); +private: + void onAgentNameReceived(const LLUUID& id, const std::string& first, + const std::string& last, BOOL is_group); +}; + +/// +/// LLUrlEntryGroup Describes a Second Life group Url, e.g., +/// secondlife:///app/group/00005ff3-4044-c79f-9de8-fb28ae0df991/about +/// +class LLUrlEntryGroup : public LLUrlEntryBase +{ +public: + LLUrlEntryGroup(); + /*virtual*/ std::string getLabel(const std::string &url, const LLUrlLabelCallback &cb); +private: + void onGroupNameReceived(const LLUUID& id, const std::string& first, + const std::string& last, BOOL is_group); +}; + +/// +/// LLUrlEntryEvent Describes a Second Life event Url, e.g., +/// secondlife:///app/event/700727/about +/// +class LLUrlEntryEvent : public LLUrlEntryBase +{ +public: + LLUrlEntryEvent(); + /*virtual*/ std::string getLabel(const std::string &url, const LLUrlLabelCallback &cb); +}; + +/// +/// LLUrlEntryClassified Describes a Second Life classified Url, e.g., +/// secondlife:///app/classified/00128854-c36a-5649-7ca6-5dfaa7514ab2/about +/// +class LLUrlEntryClassified : public LLUrlEntryBase +{ +public: + LLUrlEntryClassified(); + /*virtual*/ std::string getLabel(const std::string &url, const LLUrlLabelCallback &cb); +}; + +/// +/// LLUrlEntryParcel Describes a Second Life parcel Url, e.g., +/// secondlife:///app/parcel/0000060e-4b39-e00b-d0c3-d98b1934e3a8/about +/// +class LLUrlEntryParcel : public LLUrlEntryBase +{ +public: + LLUrlEntryParcel(); + /*virtual*/ std::string getLabel(const std::string &url, const LLUrlLabelCallback &cb); +}; + +/// +/// LLUrlEntryTeleport Describes a Second Life teleport Url, e.g., +/// secondlife:///app/teleport/Ahern/50/50/50/ +/// +class LLUrlEntryTeleport : public LLUrlEntryBase +{ +public: + LLUrlEntryTeleport(); + /*virtual*/ std::string getLabel(const std::string &url, const LLUrlLabelCallback &cb); + /*virtual*/ std::string getLocation(const std::string &url) const; +}; + +/// +/// LLUrlEntryObjectIM Describes a Second Life object instant msg Url, e.g., +/// secondlife:///app/objectim/<sessionid>?name=Foo +/// +class LLUrlEntryObjectIM : public LLUrlEntryBase +{ +public: + LLUrlEntryObjectIM(); + /*virtual*/ std::string getLabel(const std::string &url, const LLUrlLabelCallback &cb); + /*virtual*/ std::string getLocation(const std::string &url) const; +}; + +/// +/// LLUrlEntrySL Describes a generic SLURL, e.g., a Url that starts +/// with secondlife:// (used as a catch-all for cases not matched above) +/// +class LLUrlEntrySL : public LLUrlEntryBase +{ +public: + LLUrlEntrySL(); + /*virtual*/ std::string getLabel(const std::string &url, const LLUrlLabelCallback &cb); +}; + +/// +/// LLUrlEntrySLLabel Describes a generic SLURL, e.g., a Url that starts +/// with secondlife:// with the ability to specify a custom label. +/// +class LLUrlEntrySLLabel : public LLUrlEntryBase +{ +public: + LLUrlEntrySLLabel(); + /*virtual*/ std::string getLabel(const std::string &url, const LLUrlLabelCallback &cb); + /*virtual*/ std::string getUrl(const std::string &string); +}; + +#endif diff --git a/indra/llui/llurlmatch.cpp b/indra/llui/llurlmatch.cpp new file mode 100644 index 0000000000..7eec4c4a65 --- /dev/null +++ b/indra/llui/llurlmatch.cpp @@ -0,0 +1,61 @@ +/** + * @file llurlmatch.cpp + * @author Martin Reddy + * @brief Specifies a matched Url in a string, as returned by LLUrlRegistry + * + * $LicenseInfo:firstyear=2009&license=viewergpl$ + * + * Copyright (c) 2009, Linden Research, Inc. + * + * Second Life Viewer Source Code + * The source code in this file ("Source Code") is provided by Linden Lab + * to you under the terms of the GNU General Public License, version 2.0 + * ("GPL"), unless you have obtained a separate licensing agreement + * ("Other License"), formally executed by you and Linden Lab. Terms of + * the GPL can be found in doc/GPL-license.txt in this distribution, or + * online at http://secondlifegrid.net/programs/open_source/licensing/gplv2 + * + * There are special exceptions to the terms and conditions of the GPL as + * it is applied to this Source Code. View the full text of the exception + * in the file doc/FLOSS-exception.txt in this software distribution, or + * online at + * http://secondlifegrid.net/programs/open_source/licensing/flossexception + * + * By copying, modifying or distributing this software, you acknowledge + * that you have read and understood your obligations described above, + * and agree to abide by those obligations. + * + * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO + * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY, + * COMPLETENESS OR PERFORMANCE. + * $/LicenseInfo$ + */ + +#include "linden_common.h" +#include "llurlmatch.h" + +LLUrlMatch::LLUrlMatch() : + mStart(0), + mEnd(0), + mUrl(""), + mLabel(""), + mTooltip(""), + mIcon(""), + mMenuName("") +{ +} + +void LLUrlMatch::setValues(U32 start, U32 end, const std::string &url, + const std::string &label, const std::string &tooltip, + const std::string &icon, const std::string &menu, + const std::string &location) +{ + mStart = start; + mEnd = end; + mUrl = url; + mLabel = label; + mTooltip = tooltip; + mIcon = icon; + mMenuName = menu; + mLocation = location; +} diff --git a/indra/llui/llurlmatch.h b/indra/llui/llurlmatch.h new file mode 100644 index 0000000000..0711e41443 --- /dev/null +++ b/indra/llui/llurlmatch.h @@ -0,0 +1,98 @@ +/** + * @file llurlmatch.h + * @author Martin Reddy + * @brief Specifies a matched Url in a string, as returned by LLUrlRegistry + * + * $LicenseInfo:firstyear=2009&license=viewergpl$ + * + * Copyright (c) 2009, Linden Research, Inc. + * + * Second Life Viewer Source Code + * The source code in this file ("Source Code") is provided by Linden Lab + * to you under the terms of the GNU General Public License, version 2.0 + * ("GPL"), unless you have obtained a separate licensing agreement + * ("Other License"), formally executed by you and Linden Lab. Terms of + * the GPL can be found in doc/GPL-license.txt in this distribution, or + * online at http://secondlifegrid.net/programs/open_source/licensing/gplv2 + * + * There are special exceptions to the terms and conditions of the GPL as + * it is applied to this Source Code. View the full text of the exception + * in the file doc/FLOSS-exception.txt in this software distribution, or + * online at + * http://secondlifegrid.net/programs/open_source/licensing/flossexception + * + * By copying, modifying or distributing this software, you acknowledge + * that you have read and understood your obligations described above, + * and agree to abide by those obligations. + * + * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO + * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY, + * COMPLETENESS OR PERFORMANCE. + * $/LicenseInfo$ + */ + +#ifndef LL_LLURLMATCH_H +#define LL_LLURLMATCH_H + +#include "linden_common.h" + +#include <string> +#include <vector> + +/// +/// LLUrlMatch describes a single Url that was matched within a string by +/// the LLUrlRegistry::findUrl() method. It includes the actual Url that +/// was matched along with its first/last character offset in the string. +/// An alternate label is also provided for creating a hyperlink, as well +/// as tooltip/status text, an icon, and a XUI file for a context menu +/// that can be used in a popup for a Url (e.g., Open, Copy URL, etc.) +/// +class LLUrlMatch +{ +public: + LLUrlMatch(); + + /// return true if this object does not contain a valid Url match yet + bool empty() const { return mUrl.empty(); } + + /// return the offset in the string for the first character of the Url + U32 getStart() const { return mStart; } + + /// return the offset in the string for the last character of the Url + U32 getEnd() const { return mEnd; } + + /// return the Url that has been matched in the input string + const std::string &getUrl() const { return mUrl; } + + /// return a label that can be used for the display of this Url + const std::string &getLabel() const { return mLabel; } + + /// return a message that could be displayed in a tooltip or status bar + const std::string &getTooltip() const { return mTooltip; } + + /// return the filename for an icon that can be displayed next to this Url + const std::string &getIcon() const { return mIcon; } + + /// Return the name of a XUI file containing the context menu items + const std::string getMenuName() const { return mMenuName; } + + /// return the SL location that this Url describes, or "" if none. + const std::string &getLocation() const { return mLocation; } + + /// Change the contents of this match object (used by LLUrlRegistry) + void setValues(U32 start, U32 end, const std::string &url, const std::string &label, + const std::string &tooltip, const std::string &icon, + const std::string &menu, const std::string &location); + +private: + U32 mStart; + U32 mEnd; + std::string mUrl; + std::string mLabel; + std::string mTooltip; + std::string mIcon; + std::string mMenuName; + std::string mLocation; +}; + +#endif diff --git a/indra/llui/llurlregistry.cpp b/indra/llui/llurlregistry.cpp new file mode 100644 index 0000000000..938375ad13 --- /dev/null +++ b/indra/llui/llurlregistry.cpp @@ -0,0 +1,165 @@ +/** + * @file llurlregistry.cpp + * @author Martin Reddy + * @brief Contains a set of Url types that can be matched in a string + * + * $LicenseInfo:firstyear=2009&license=viewergpl$ + * + * Copyright (c) 2009, Linden Research, Inc. + * + * Second Life Viewer Source Code + * The source code in this file ("Source Code") is provided by Linden Lab + * to you under the terms of the GNU General Public License, version 2.0 + * ("GPL"), unless you have obtained a separate licensing agreement + * ("Other License"), formally executed by you and Linden Lab. Terms of + * the GPL can be found in doc/GPL-license.txt in this distribution, or + * online at http://secondlifegrid.net/programs/open_source/licensing/gplv2 + * + * There are special exceptions to the terms and conditions of the GPL as + * it is applied to this Source Code. View the full text of the exception + * in the file doc/FLOSS-exception.txt in this software distribution, or + * online at + * http://secondlifegrid.net/programs/open_source/licensing/flossexception + * + * By copying, modifying or distributing this software, you acknowledge + * that you have read and understood your obligations described above, + * and agree to abide by those obligations. + * + * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO + * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY, + * COMPLETENESS OR PERFORMANCE. + * $/LicenseInfo$ + */ + +#include "linden_common.h" +#include "llurlregistry.h" + +#include <boost/regex.hpp> + +// default dummy callback that ignores any label updates from the server +void LLUrlRegistryNullCallback(const std::string &url, const std::string &label) +{ +} + +LLUrlRegistry::LLUrlRegistry() +{ + // Urls are matched in the order that they were registered + registerUrl(new LLUrlEntrySLURL()); + registerUrl(new LLUrlEntryHTTP()); + registerUrl(new LLUrlEntryHTTPLabel()); + registerUrl(new LLUrlEntryAgent()); + registerUrl(new LLUrlEntryGroup()); + registerUrl(new LLUrlEntryEvent()); + registerUrl(new LLUrlEntryClassified()); + registerUrl(new LLUrlEntryParcel()); + registerUrl(new LLUrlEntryTeleport()); + registerUrl(new LLUrlEntryObjectIM()); + registerUrl(new LLUrlEntrySL()); + registerUrl(new LLUrlEntrySLLabel()); +} + +LLUrlRegistry::~LLUrlRegistry() +{ + // free all of the LLUrlEntryBase objects we are holding + std::vector<LLUrlEntryBase *>::iterator it; + for (it = mUrlEntry.begin(); it != mUrlEntry.end(); ++it) + { + delete *it; + } +} + +void LLUrlRegistry::registerUrl(LLUrlEntryBase *url) +{ + if (url) + { + mUrlEntry.push_back(url); + } +} + +static bool matchRegex(const char *text, boost::regex regex, U32 &start, U32 &end) +{ + boost::cmatch result; + bool found; + + // regex_search can potentially throw an exception, so check for it + try + { + found = boost::regex_search(text, result, regex); + } + catch (std::runtime_error &) + { + return false; + } + + if (! found) + { + return false; + } + + // return the first/last character offset for the matched substring + start = static_cast<U32>(result[0].first - text); + end = static_cast<U32>(result[0].second - text) - 1; + + // we allow certain punctuation to terminate a Url but not match it, + // e.g., "http://foo.com/." should just match "http://foo.com/" + if (text[end] == '.' || text[end] == ',') + { + end--; + } + // ignore a terminating ')' when Url contains no matching '(' + // see DEV-19842 for details + else if (text[end] == ')' && std::string(text+start, end-start).find('(') == std::string::npos) + { + end--; + } + + return true; +} + +bool LLUrlRegistry::findUrl(const std::string &text, LLUrlMatch &match, const LLUrlLabelCallback &cb) +{ + // test for the trivial case of no text and get out fast + if (text.empty()) + { + return false; + } + + // find the first matching regex from all url entries in the registry + U32 match_start = 0, match_end = 0; + LLUrlEntryBase *match_entry = NULL; + + std::vector<LLUrlEntryBase *>::iterator it; + for (it = mUrlEntry.begin(); it != mUrlEntry.end(); ++it) + { + LLUrlEntryBase *url_entry = *it; + + U32 start = 0, end = 0; + if (matchRegex(text.c_str(), url_entry->getPattern(), start, end)) + { + // does this match occur in the string before any other match + if (start < match_start || match_entry == NULL) + { + match_start = start; + match_end = end; + match_entry = url_entry; + } + } + } + + // did we find a match? if so, return its details in the match object + if (match_entry) + { + // fill in the LLUrlMatch object and return it + std::string url = text.substr(match_start, match_end - match_start + 1); + match.setValues(match_start, match_end, + match_entry->getUrl(url), + match_entry->getLabel(url, cb), + match_entry->getTooltip(), + match_entry->getIcon(), + match_entry->getMenuName(), + match_entry->getLocation(url)); + return true; + } + + return false; +} diff --git a/indra/llui/llurlregistry.h b/indra/llui/llurlregistry.h new file mode 100644 index 0000000000..84b033036c --- /dev/null +++ b/indra/llui/llurlregistry.h @@ -0,0 +1,87 @@ +/** + * @file llurlregistry.h + * @author Martin Reddy + * @brief Contains a set of Url types that can be matched in a string + * + * $LicenseInfo:firstyear=2009&license=viewergpl$ + * + * Copyright (c) 2009, Linden Research, Inc. + * + * Second Life Viewer Source Code + * The source code in this file ("Source Code") is provided by Linden Lab + * to you under the terms of the GNU General Public License, version 2.0 + * ("GPL"), unless you have obtained a separate licensing agreement + * ("Other License"), formally executed by you and Linden Lab. Terms of + * the GPL can be found in doc/GPL-license.txt in this distribution, or + * online at http://secondlifegrid.net/programs/open_source/licensing/gplv2 + * + * There are special exceptions to the terms and conditions of the GPL as + * it is applied to this Source Code. View the full text of the exception + * in the file doc/FLOSS-exception.txt in this software distribution, or + * online at + * http://secondlifegrid.net/programs/open_source/licensing/flossexception + * + * By copying, modifying or distributing this software, you acknowledge + * that you have read and understood your obligations described above, + * and agree to abide by those obligations. + * + * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO + * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY, + * COMPLETENESS OR PERFORMANCE. + * $/LicenseInfo$ + */ + +#ifndef LL_LLURLREGISTRY_H +#define LL_LLURLREGISTRY_H + +#include "llurlentry.h" +#include "llurlmatch.h" +#include "llsingleton.h" + +#include <string> +#include <vector> +#include <map> + +/// This default callback for findUrl() simply ignores any label updates +void LLUrlRegistryNullCallback(const std::string &url, const std::string &label); + +/// +/// LLUrlRegistry is a singleton that contains a set of Url types that +/// can be matched in string. E.g., http:// or secondlife:// Urls. +/// +/// Clients call the findUrl() method on a string to locate the first +/// occurence of a supported Urls in that string. If findUrl() returns +/// true, the LLUrlMatch object will be updated to describe the Url +/// that was matched, including a label that can be used to hyperlink +/// the Url, an icon to display next to the Url, and a XUI menu that +/// can be used as a popup context menu for that Url. +/// +/// New Url types can be added to the registry with the registerUrl +/// method. E.g., to add support for a new secondlife:///app/ Url. +/// +/// Computing the label for a Url could involve a roundtrip request +/// to the server (e.g., to find the actual agent or group name). +/// As such, you can provide a callback method that will get invoked +/// when a new label is available for one of your matched Urls. +/// +class LLUrlRegistry : public LLSingleton<LLUrlRegistry> +{ +public: + ~LLUrlRegistry(); + + /// add a new Url handler to the registry (will be freed on destruction) + void registerUrl(LLUrlEntryBase *url); + + /// get the next Url in an input string, starting at a given character offset + /// your callback is invoked if the matched Url's label changes in the future + bool findUrl(const std::string &text, LLUrlMatch &match, + const LLUrlLabelCallback &cb = &LLUrlRegistryNullCallback); + +private: + LLUrlRegistry(); + friend class LLSingleton<LLUrlRegistry>; + + std::vector<LLUrlEntryBase *> mUrlEntry; +}; + +#endif diff --git a/indra/llui/tests/llurlentry_stub.cpp b/indra/llui/tests/llurlentry_stub.cpp new file mode 100644 index 0000000000..05bd5d8bb3 --- /dev/null +++ b/indra/llui/tests/llurlentry_stub.cpp @@ -0,0 +1,64 @@ +/** + * @file llurlentry_stub.cpp + * @author Martin Reddy + * @brief Stub implementations for LLUrlEntry unit test dependencies + * + * $LicenseInfo:firstyear=2009&license=internal$ + * + * Copyright (c) 2009, Linden Research, Inc. + * + * The following source code is PROPRIETARY AND CONFIDENTIAL. Use of + * this source code is governed by the Linden Lab Source Code Disclosure + * Agreement ("Agreement") previously entered between you and Linden + * Lab. By accessing, using, copying, modifying or distributing this + * software, you acknowledge that you have been informed of your + * obligations under the Agreement and agree to abide by those obligations. + * + * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO + * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY, + * COMPLETENESS OR PERFORMANCE. + * $/LicenseInfo$ + */ + +#include "llstring.h" +#include "llfile.h" +#include "llcachename.h" +#include "lluuid.h" + +#include <string> + +// +// Stub implementation for LLCacheName +// +BOOL LLCacheName::getFullName(const LLUUID& id, std::string& fullname) +{ + fullname = "Lynx Linden"; + return TRUE; +} + +BOOL LLCacheName::getGroupName(const LLUUID& id, std::string& group) +{ + group = "My Group"; + return TRUE; +} + +boost::signals2::connection LLCacheName::get(const LLUUID& id, BOOL is_group, const LLCacheNameCallback& callback) +{ + return boost::signals2::connection(); +} + +LLCacheName* gCacheName = NULL; + +// +// Stub implementation for LLTrans +// +class LLTrans +{ +public: + static std::string getString(const std::string &xml_desc, const LLStringUtil::format_map_t& args); +}; + +std::string LLTrans::getString(const std::string &xml_desc, const LLStringUtil::format_map_t& args) +{ + return std::string(); +} diff --git a/indra/llui/tests/llurlentry_test.cpp b/indra/llui/tests/llurlentry_test.cpp new file mode 100644 index 0000000000..f8e6aa65d0 --- /dev/null +++ b/indra/llui/tests/llurlentry_test.cpp @@ -0,0 +1,535 @@ +/** + * @file llurlentry_test.cpp + * @author Martin Reddy + * @brief Unit tests for LLUrlEntry objects + * + * $LicenseInfo:firstyear=2009&license=internal$ + * + * Copyright (c) 2009, Linden Research, Inc. + * + * The following source code is PROPRIETARY AND CONFIDENTIAL. Use of + * this source code is governed by the Linden Lab Source Code Disclosure + * Agreement ("Agreement") previously entered between you and Linden + * Lab. By accessing, using, copying, modifying or distributing this + * software, you acknowledge that you have been informed of your + * obligations under the Agreement and agree to abide by those obligations. + * + * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO + * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY, + * COMPLETENESS OR PERFORMANCE. + * $/LicenseInfo$ + */ + +#include "linden_common.h" +#include "../llurlentry.h" +#include "llurlentry_stub.cpp" +#include "lltut.h" + +#include <boost/regex.hpp> + +namespace tut +{ + struct LLUrlEntryData + { + }; + + typedef test_group<LLUrlEntryData> factory; + typedef factory::object object; +} + +namespace +{ + tut::factory tf("LLUrlEntry"); +} + +namespace tut +{ + void testRegex(const std::string &testname, boost::regex regex, + const char *text, const std::string &expected) + { + std::string url = ""; + boost::cmatch result; + bool found = boost::regex_search(text, result, regex); + if (found) + { + S32 start = static_cast<U32>(result[0].first - text); + S32 end = static_cast<U32>(result[0].second - text); + url = std::string(text+start, end-start); + } + ensure_equals(testname, url, expected); + } + + template<> template<> + void object::test<1>() + { + // + // test LLUrlEntryHTTP - standard http Urls + // + LLUrlEntryHTTP url; + boost::regex r = url.getPattern(); + + testRegex("no valid url", r, + "htp://slurl.com/", + ""); + + testRegex("simple http (1)", r, + "http://slurl.com/", + "http://slurl.com/"); + + testRegex("simple http (2)", r, + "http://slurl.com", + "http://slurl.com"); + + testRegex("simple http (3)", r, + "http://slurl.com/about.php", + "http://slurl.com/about.php"); + + testRegex("simple https", r, + "https://slurl.com/about.php", + "https://slurl.com/about.php"); + + testRegex("http in text (1)", r, + "XX http://slurl.com/ XX", + "http://slurl.com/"); + + testRegex("http in text (2)", r, + "XX http://slurl.com/about.php XX", + "http://slurl.com/about.php"); + + testRegex("https in text", r, + "XX https://slurl.com/about.php XX", + "https://slurl.com/about.php"); + + testRegex("two http urls", r, + "XX http://slurl.com/about.php http://secondlife.com/ XX", + "http://slurl.com/about.php"); + + testRegex("http url with port and username", r, + "XX http://nobody@slurl.com:80/about.php http://secondlife.com/ XX", + "http://nobody@slurl.com:80/about.php"); + + testRegex("http url with port, username, and query string", r, + "XX http://nobody@slurl.com:80/about.php?title=hi%20there http://secondlife.com/ XX", + "http://nobody@slurl.com:80/about.php?title=hi%20there"); + + // note: terminating commas will be removed by LLUrlRegistry:findUrl() + testRegex("http url with commas in middle and terminating", r, + "XX http://slurl.com/?title=Hi,There, XX", + "http://slurl.com/?title=Hi,There,"); + + // note: terminating periods will be removed by LLUrlRegistry:findUrl() + testRegex("http url with periods in middle and terminating", r, + "XX http://slurl.com/index.php. XX", + "http://slurl.com/index.php."); + + // DEV-19842: Closing parenthesis ")" breaks urls + testRegex("http url with brackets (1)", r, + "XX http://en.wikipedia.org/wiki/JIRA_(software) XX", + "http://en.wikipedia.org/wiki/JIRA_(software)"); + + // DEV-19842: Closing parenthesis ")" breaks urls + testRegex("http url with brackets (2)", r, + "XX http://jira.secondlife.com/secure/attachment/17990/eggy+avs+in+1.21.0+(93713)+public+nightly.jpg XX", + "http://jira.secondlife.com/secure/attachment/17990/eggy+avs+in+1.21.0+(93713)+public+nightly.jpg"); + + // DEV-10353: URLs in chat log terminated incorrectly when newline in chat + testRegex("http url with newlines", r, + "XX\nhttp://www.secondlife.com/\nXX", + "http://www.secondlife.com/"); + } + + template<> template<> + void object::test<2>() + { + // + // test LLUrlEntryHTTPLabel - wiki-style http Urls with labels + // + LLUrlEntryHTTPLabel url; + boost::regex r = url.getPattern(); + + testRegex("invalid wiki url [1]", r, + "[http://www.example.org]", + ""); + + testRegex("invalid wiki url [2]", r, + "[http://www.example.org", + ""); + + testRegex("invalid wiki url [3]", r, + "[http://www.example.org Label", + ""); + + testRegex("example.org with label (spaces)", r, + "[http://www.example.org Text]", + "[http://www.example.org Text]"); + + testRegex("example.org with label (tabs)", r, + "[http://www.example.org\t Text]", + "[http://www.example.org\t Text]"); + + testRegex("SL http URL with label", r, + "[http://www.secondlife.com/ Second Life]", + "[http://www.secondlife.com/ Second Life]"); + + testRegex("SL https URL with label", r, + "XXX [https://www.secondlife.com/ Second Life] YYY", + "[https://www.secondlife.com/ Second Life]"); + + testRegex("SL http URL with label", r, + "[http://www.secondlife.com/?test=Hi%20There Second Life]", + "[http://www.secondlife.com/?test=Hi%20There Second Life]"); + } + + template<> template<> + void object::test<3>() + { + // + // test LLUrlEntrySLURL - second life URLs + // + LLUrlEntrySLURL url; + boost::regex r = url.getPattern(); + + testRegex("no valid slurl [1]", r, + "htp://slurl.com/secondlife/Ahern/50/50/50/", + ""); + + testRegex("no valid slurl [2]", r, + "http://slurl.com/secondlife/", + ""); + + testRegex("no valid slurl [3]", r, + "hhtp://slurl.com/secondlife/Ahern/50/FOO/50/", + ""); + + testRegex("Ahern (50,50,50) [1]", r, + "http://slurl.com/secondlife/Ahern/50/50/50/", + "http://slurl.com/secondlife/Ahern/50/50/50/"); + + testRegex("Ahern (50,50,50) [2]", r, + "XXX http://slurl.com/secondlife/Ahern/50/50/50/ XXX", + "http://slurl.com/secondlife/Ahern/50/50/50/"); + + testRegex("Ahern (50,50,50) [3]", r, + "XXX http://slurl.com/secondlife/Ahern/50/50/50 XXX", + "http://slurl.com/secondlife/Ahern/50/50/50"); + + testRegex("Ahern (50,50,50) multicase", r, + "XXX http://SLUrl.com/SecondLife/Ahern/50/50/50/ XXX", + "http://SLUrl.com/SecondLife/Ahern/50/50/50/"); + + testRegex("Ahern (50,50) [1]", r, + "XXX http://slurl.com/secondlife/Ahern/50/50/ XXX", + "http://slurl.com/secondlife/Ahern/50/50/"); + + testRegex("Ahern (50,50) [2]", r, + "XXX http://slurl.com/secondlife/Ahern/50/50 XXX", + "http://slurl.com/secondlife/Ahern/50/50"); + + testRegex("Ahern (50)", r, + "XXX http://slurl.com/secondlife/Ahern/50 XXX", + "http://slurl.com/secondlife/Ahern/50"); + + testRegex("Ahern", r, + "XXX http://slurl.com/secondlife/Ahern/ XXX", + "http://slurl.com/secondlife/Ahern/"); + + testRegex("Ahern SLURL with title", r, + "XXX http://slurl.com/secondlife/Ahern/50/50/50/?title=YOUR%20TITLE%20HERE! XXX", + "http://slurl.com/secondlife/Ahern/50/50/50/?title=YOUR%20TITLE%20HERE!"); + + testRegex("Ahern SLURL with msg", r, + "XXX http://slurl.com/secondlife/Ahern/50/50/50/?msg=Your%20text%20here. XXX", + "http://slurl.com/secondlife/Ahern/50/50/50/?msg=Your%20text%20here."); + + // DEV-21577: In-world SLURLs containing "(" or ")" are not treated as a hyperlink in chat + testRegex("SLURL with brackets", r, + "XXX http://slurl.com/secondlife/Burning%20Life%20(Hyper)/27/210/30 XXX", + "http://slurl.com/secondlife/Burning%20Life%20(Hyper)/27/210/30"); + + // DEV-35459: SLURLs and teleport Links not parsed properly + testRegex("SLURL with quote", r, + "XXX http://slurl.com/secondlife/A'ksha%20Oasis/41/166/701 XXX", + "http://slurl.com/secondlife/A'ksha%20Oasis/41/166/701"); + } + + template<> template<> + void object::test<4>() + { + // + // test LLUrlEntryAgent - secondlife://app/agent Urls + // + LLUrlEntryAgent url; + boost::regex r = url.getPattern(); + + testRegex("Invalid Agent Url", r, + "secondlife:///app/agent/0e346d8b-4433-4d66-XXXX-fd37083abc4c/about", + ""); + + testRegex("Agent Url ", r, + "secondlife:///app/agent/0e346d8b-4433-4d66-a6b0-fd37083abc4c/about", + "secondlife:///app/agent/0e346d8b-4433-4d66-a6b0-fd37083abc4c/about"); + + testRegex("Agent Url in text", r, + "XXX secondlife:///app/agent/0e346d8b-4433-4d66-a6b0-fd37083abc4c/about XXX", + "secondlife:///app/agent/0e346d8b-4433-4d66-a6b0-fd37083abc4c/about"); + + testRegex("Agent Url multicase", r, + "XXX secondlife:///App/AGENT/0E346D8B-4433-4d66-a6b0-fd37083abc4c/About XXX", + "secondlife:///App/AGENT/0E346D8B-4433-4d66-a6b0-fd37083abc4c/About"); + } + + template<> template<> + void object::test<5>() + { + // + // test LLUrlEntryGroup - secondlife://app/group Urls + // + LLUrlEntryGroup url; + boost::regex r = url.getPattern(); + + testRegex("Invalid Group Url", r, + "secondlife:///app/group/00005ff3-4044-c79f-XXXX-fb28ae0df991/about", + ""); + + testRegex("Group Url ", r, + "secondlife:///app/group/00005ff3-4044-c79f-9de8-fb28ae0df991/about", + "secondlife:///app/group/00005ff3-4044-c79f-9de8-fb28ae0df991/about"); + + testRegex("Group Url in text", r, + "XXX secondlife:///app/group/00005ff3-4044-c79f-9de8-fb28ae0df991/about XXX", + "secondlife:///app/group/00005ff3-4044-c79f-9de8-fb28ae0df991/about"); + + testRegex("Group Url multicase", r, + "XXX secondlife:///APP/Group/00005FF3-4044-c79f-9de8-fb28ae0df991/About XXX", + "secondlife:///APP/Group/00005FF3-4044-c79f-9de8-fb28ae0df991/About"); + } + + template<> template<> + void object::test<6>() + { + // + // test LLUrlEntryEvent - secondlife://app/event Urls + // + LLUrlEntryEvent url; + boost::regex r = url.getPattern(); + + testRegex("Invalid Event Url", r, + "secondlife:///app/event/FOO/about", + ""); + + testRegex("Event Url ", r, + "secondlife:///app/event/700727/about", + "secondlife:///app/event/700727/about"); + + testRegex("Event Url in text", r, + "XXX secondlife:///app/event/700727/about XXX", + "secondlife:///app/event/700727/about"); + + testRegex("Event Url multicase", r, + "XXX secondlife:///APP/Event/700727/about XXX", + "secondlife:///APP/Event/700727/about"); + } + + template<> template<> + void object::test<7>() + { + // + // test LLUrlEntryClassified - secondlife://app/classified Urls + // + LLUrlEntryClassified url; + boost::regex r = url.getPattern(); + + testRegex("Invalid Classified Url", r, + "secondlife:///app/classified/00128854-XXXX-5649-7ca6-5dfaa7514ab2/about", + ""); + + testRegex("Classified Url ", r, + "secondlife:///app/classified/00128854-c36a-5649-7ca6-5dfaa7514ab2/about", + "secondlife:///app/classified/00128854-c36a-5649-7ca6-5dfaa7514ab2/about"); + + testRegex("Classified Url in text", r, + "XXX secondlife:///app/classified/00128854-c36a-5649-7ca6-5dfaa7514ab2/about XXX", + "secondlife:///app/classified/00128854-c36a-5649-7ca6-5dfaa7514ab2/about"); + + testRegex("Classified Url multicase", r, + "XXX secondlife:///APP/Classified/00128854-c36a-5649-7ca6-5dfaa7514ab2/About XXX", + "secondlife:///APP/Classified/00128854-c36a-5649-7ca6-5dfaa7514ab2/About"); + } + + template<> template<> + void object::test<8>() + { + // + // test LLUrlEntryParcel - secondlife://app/parcel Urls + // + LLUrlEntryParcel url; + boost::regex r = url.getPattern(); + + testRegex("Invalid Classified Url", r, + "secondlife:///app/parcel/0000060e-4b39-e00b-XXXX-d98b1934e3a8/about", + ""); + + testRegex("Classified Url ", r, + "secondlife:///app/parcel/0000060e-4b39-e00b-d0c3-d98b1934e3a8/about", + "secondlife:///app/parcel/0000060e-4b39-e00b-d0c3-d98b1934e3a8/about"); + + testRegex("Classified Url in text", r, + "XXX secondlife:///app/parcel/0000060e-4b39-e00b-d0c3-d98b1934e3a8/about XXX", + "secondlife:///app/parcel/0000060e-4b39-e00b-d0c3-d98b1934e3a8/about"); + + testRegex("Classified Url multicase", r, + "XXX secondlife:///APP/Parcel/0000060e-4b39-e00b-d0c3-d98b1934e3a8/About XXX", + "secondlife:///APP/Parcel/0000060e-4b39-e00b-d0c3-d98b1934e3a8/About"); + } + template<> template<> + void object::test<9>() + { + // + // test LLUrlEntryTeleport - secondlife://app/teleport URLs + // + LLUrlEntryTeleport url; + boost::regex r = url.getPattern(); + + testRegex("no valid teleport [1]", r, + "http://slurl.com/secondlife/Ahern/50/50/50/", + ""); + + testRegex("no valid teleport [2]", r, + "secondlife:///app/teleport/", + ""); + + testRegex("no valid teleport [3]", r, + "second-life:///app/teleport/Ahern/50/50/50/", + ""); + + testRegex("no valid teleport [3]", r, + "hhtp://slurl.com/secondlife/Ahern/50/FOO/50/", + ""); + + testRegex("Ahern (50,50,50) [1]", r, + "secondlife:///app/teleport/Ahern/50/50/50/", + "secondlife:///app/teleport/Ahern/50/50/50/"); + + testRegex("Ahern (50,50,50) [2]", r, + "XXX secondlife:///app/teleport/Ahern/50/50/50/ XXX", + "secondlife:///app/teleport/Ahern/50/50/50/"); + + testRegex("Ahern (50,50,50) [3]", r, + "XXX secondlife:///app/teleport/Ahern/50/50/50 XXX", + "secondlife:///app/teleport/Ahern/50/50/50"); + + testRegex("Ahern (50,50,50) multicase", r, + "XXX secondlife:///app/teleport/Ahern/50/50/50/ XXX", + "secondlife:///app/teleport/Ahern/50/50/50/"); + + testRegex("Ahern (50,50) [1]", r, + "XXX secondlife:///app/teleport/Ahern/50/50/ XXX", + "secondlife:///app/teleport/Ahern/50/50/"); + + testRegex("Ahern (50,50) [2]", r, + "XXX secondlife:///app/teleport/Ahern/50/50 XXX", + "secondlife:///app/teleport/Ahern/50/50"); + + testRegex("Ahern (50)", r, + "XXX secondlife:///app/teleport/Ahern/50 XXX", + "secondlife:///app/teleport/Ahern/50"); + + testRegex("Ahern", r, + "XXX secondlife:///app/teleport/Ahern/ XXX", + "secondlife:///app/teleport/Ahern/"); + + testRegex("Ahern teleport with title", r, + "XXX secondlife:///app/teleport/Ahern/50/50/50/?title=YOUR%20TITLE%20HERE! XXX", + "secondlife:///app/teleport/Ahern/50/50/50/?title=YOUR%20TITLE%20HERE!"); + + testRegex("Ahern teleport with msg", r, + "XXX secondlife:///app/teleport/Ahern/50/50/50/?msg=Your%20text%20here. XXX", + "secondlife:///app/teleport/Ahern/50/50/50/?msg=Your%20text%20here."); + + // DEV-21577: In-world SLURLs containing "(" or ")" are not treated as a hyperlink in chat + testRegex("Teleport with brackets", r, + "XXX secondlife:///app/teleport/Burning%20Life%20(Hyper)/27/210/30 XXX", + "secondlife:///app/teleport/Burning%20Life%20(Hyper)/27/210/30"); + + // DEV-35459: SLURLs and teleport Links not parsed properly + testRegex("Teleport url with quote", r, + "XXX secondlife:///app/teleport/A'ksha%20Oasis/41/166/701 XXX", + "secondlife:///app/teleport/A'ksha%20Oasis/41/166/701"); + } + + template<> template<> + void object::test<10>() + { + // + // test LLUrlEntrySL - general secondlife:// URLs + // + LLUrlEntrySL url; + boost::regex r = url.getPattern(); + + testRegex("no valid slapp [1]", r, + "http:///app/", + ""); + + testRegex("valid slapp [1]", r, + "secondlife:///app/", + "secondlife:///app/"); + + testRegex("valid slapp [2]", r, + "secondlife:///app/teleport/Ahern/50/50/50/", + "secondlife:///app/teleport/Ahern/50/50/50/"); + + testRegex("valid slapp [3]", r, + "secondlife:///app/foo", + "secondlife:///app/foo"); + + testRegex("valid slapp [4]", r, + "secondlife:///APP/foo?title=Hi%20There", + "secondlife:///APP/foo?title=Hi%20There"); + + testRegex("valid slapp [5]", r, + "secondlife://host/app/", + "secondlife://host/app/"); + + testRegex("valid slapp [6]", r, + "secondlife://host:8080/foo/bar", + "secondlife://host:8080/foo/bar"); + } + + template<> template<> + void object::test<11>() + { + // + // test LLUrlEntrySLLabel - general secondlife:// URLs with labels + // + LLUrlEntrySLLabel url; + boost::regex r = url.getPattern(); + + testRegex("invalid wiki url [1]", r, + "[secondlife:///app/]", + ""); + + testRegex("invalid wiki url [2]", r, + "[secondlife:///app/", + ""); + + testRegex("invalid wiki url [3]", r, + "[secondlife:///app/ Label", + ""); + + testRegex("agent slurl with label (spaces)", r, + "[secondlife:///app/agent/0e346d8b-4433-4d66-a6b0-fd37083abc4c/about Text]", + "[secondlife:///app/agent/0e346d8b-4433-4d66-a6b0-fd37083abc4c/about Text]"); + + testRegex("agent slurl with label (tabs)", r, + "[secondlife:///app/agent/0e346d8b-4433-4d66-a6b0-fd37083abc4c/about\t Text]", + "[secondlife:///app/agent/0e346d8b-4433-4d66-a6b0-fd37083abc4c/about\t Text]"); + + testRegex("agent slurl with label", r, + "[secondlife:///app/agent/0e346d8b-4433-4d66-a6b0-fd37083abc4c/about FirstName LastName]", + "[secondlife:///app/agent/0e346d8b-4433-4d66-a6b0-fd37083abc4c/about FirstName LastName]"); + + testRegex("teleport slurl with label", r, + "XXX [secondlife:///app/teleport/Ahern/50/50/50/ Teleport to Ahern] YYY", + "[secondlife:///app/teleport/Ahern/50/50/50/ Teleport to Ahern]"); + } +} diff --git a/indra/llui/tests/llurlmatch_test.cpp b/indra/llui/tests/llurlmatch_test.cpp new file mode 100644 index 0000000000..fcf8f4d62f --- /dev/null +++ b/indra/llui/tests/llurlmatch_test.cpp @@ -0,0 +1,177 @@ +/** + * @file llurlmatch_test.cpp + * @author Martin Reddy + * @brief Unit tests for LLUrlMatch + * + * $LicenseInfo:firstyear=2009&license=internal$ + * + * Copyright (c) 2009, Linden Research, Inc. + * + * The following source code is PROPRIETARY AND CONFIDENTIAL. Use of + * this source code is governed by the Linden Lab Source Code Disclosure + * Agreement ("Agreement") previously entered between you and Linden + * Lab. By accessing, using, copying, modifying or distributing this + * software, you acknowledge that you have been informed of your + * obligations under the Agreement and agree to abide by those obligations. + * + * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO + * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY, + * COMPLETENESS OR PERFORMANCE. + * $/LicenseInfo$ + */ + +#include "../llurlmatch.h" +#include "lltut.h" + +namespace tut +{ + struct LLUrlMatchData + { + }; + + typedef test_group<LLUrlMatchData> factory; + typedef factory::object object; +} + +namespace +{ + tut::factory tf("LLUrlMatch"); +} + +namespace tut +{ + template<> template<> + void object::test<1>() + { + // + // test the empty() method + // + LLUrlMatch match; + ensure("empty()", match.empty()); + + match.setValues(0, 1, "http://secondlife.com", "Second Life", "", "", "", ""); + ensure("! empty()", ! match.empty()); + } + + template<> template<> + void object::test<2>() + { + // + // test the getStart() method + // + LLUrlMatch match; + ensure_equals("getStart() == 0", match.getStart(), 0); + + match.setValues(10, 20, "", "", "", "", "", ""); + ensure_equals("getStart() == 10", match.getStart(), 10); + } + + template<> template<> + void object::test<3>() + { + // + // test the getEnd() method + // + LLUrlMatch match; + ensure_equals("getEnd() == 0", match.getEnd(), 0); + + match.setValues(10, 20, "", "", "", "", "", ""); + ensure_equals("getEnd() == 20", match.getEnd(), 20); + } + + template<> template<> + void object::test<4>() + { + // + // test the getUrl() method + // + LLUrlMatch match; + ensure_equals("getUrl() == ''", match.getUrl(), ""); + + match.setValues(10, 20, "http://slurl.com/", "", "", "", "", ""); + ensure_equals("getUrl() == 'http://slurl.com/'", match.getUrl(), "http://slurl.com/"); + + match.setValues(10, 20, "", "", "", "", "", ""); + ensure_equals("getUrl() == '' (2)", match.getUrl(), ""); + } + + template<> template<> + void object::test<5>() + { + // + // test the getLabel() method + // + LLUrlMatch match; + ensure_equals("getLabel() == ''", match.getLabel(), ""); + + match.setValues(10, 20, "", "Label", "", "", "", ""); + ensure_equals("getLabel() == 'Label'", match.getLabel(), "Label"); + + match.setValues(10, 20, "", "", "", "", "", ""); + ensure_equals("getLabel() == '' (2)", match.getLabel(), ""); + } + + template<> template<> + void object::test<6>() + { + // + // test the getTooltip() method + // + LLUrlMatch match; + ensure_equals("getTooltip() == ''", match.getTooltip(), ""); + + match.setValues(10, 20, "", "", "Info", "", "", ""); + ensure_equals("getTooltip() == 'Info'", match.getTooltip(), "Info"); + + match.setValues(10, 20, "", "", "", "", "", ""); + ensure_equals("getTooltip() == '' (2)", match.getTooltip(), ""); + } + + template<> template<> + void object::test<7>() + { + // + // test the getIcon() method + // + LLUrlMatch match; + ensure_equals("getIcon() == ''", match.getIcon(), ""); + + match.setValues(10, 20, "", "", "", "Icon", "", ""); + ensure_equals("getIcon() == 'Icon'", match.getIcon(), "Icon"); + + match.setValues(10, 20, "", "", "", "", "", ""); + ensure_equals("getIcon() == '' (2)", match.getIcon(), ""); + } + + template<> template<> + void object::test<8>() + { + // + // test the getMenuName() method + // + LLUrlMatch match; + ensure("getMenuName() empty", match.getMenuName().empty()); + + match.setValues(10, 20, "", "", "", "Icon", "xui_file.xml", ""); + ensure_equals("getMenuName() == \"xui_file.xml\"", match.getMenuName(), "xui_file.xml"); + + match.setValues(10, 20, "", "", "", "", "", ""); + ensure("getMenuName() empty (2)", match.getMenuName().empty()); + } + + template<> template<> + void object::test<9>() + { + // + // test the getLocation() method + // + LLUrlMatch match; + ensure("getLocation() empty", match.getLocation().empty()); + + match.setValues(10, 20, "", "", "", "Icon", "xui_file.xml", "Paris"); + ensure_equals("getLocation() == \"Paris\"", match.getLocation(), "Paris"); + + match.setValues(10, 20, "", "", "", "", "", ""); + ensure("getLocation() empty (2)", match.getLocation().empty()); + } +} |