diff options
58 files changed, 4043 insertions, 1308 deletions
diff --git a/indra/integration_tests/llui_libtest/CMakeLists.txt b/indra/integration_tests/llui_libtest/CMakeLists.txt index 1ccdb0f20b..84e3477ce6 100644 --- a/indra/integration_tests/llui_libtest/CMakeLists.txt +++ b/indra/integration_tests/llui_libtest/CMakeLists.txt @@ -11,6 +11,7 @@ include(LLCommon)  include(LLImage)  include(LLImageJ2COJ)   # ugh, needed for images  include(LLMath) +include(LLMessage)  include(LLRender)  include(LLWindow)  include(LLUI) @@ -67,6 +68,7 @@ endif (DARWIN)  # Sort by high-level to low-level  target_link_libraries(llui_libtest      llui +    llmessage      ${OS_LIBRARIES}      ${GOOGLE_PERFTOOLS_LIBRARIES}      ) 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()); +	} +} diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index de4f7ab091..3d7465f2cb 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -80,6 +80,7 @@  // Linden library includes  #include "llmemory.h" +#include "llurlaction.h"  // Third party library includes  #include <boost/bind.hpp> @@ -685,9 +686,14 @@ bool LLAppViewer::init()  	LLTransUtil::parseLanguageStrings("language_settings.xml");  	LLWeb::initClass();			  // do this after LLUI -	LLTextEditor::setURLCallbacks(&LLWeb::loadURL, -				&LLURLDispatcher::dispatchFromTextEditor, -				&LLURLDispatcher::dispatchFromTextEditor); +	// Provide the text fields with callbacks for opening Urls +	LLUrlAction::setOpenURLCallback(&LLWeb::loadURL); +	LLUrlAction::setOpenURLInternalCallback(&LLWeb::loadURLInternal); +	LLUrlAction::setOpenURLExternalCallback(&LLWeb::loadURLExternal); +	LLUrlAction::setExecuteSLURLCallback(&LLURLDispatcher::dispatchFromTextEditor); + +	// Set the link color for any Urls in text fields +	LLTextBase::setLinkColor( LLUIColorTable::instance().getColor("HTMLLinkColor") );  	// Load translations for tooltips  	LLFloater::initClass(); diff --git a/indra/newview/llavatarlist.cpp b/indra/newview/llavatarlist.cpp index 080d540f4a..e0322e26b9 100644 --- a/indra/newview/llavatarlist.cpp +++ b/indra/newview/llavatarlist.cpp @@ -62,6 +62,9 @@ LLAvatarList::LLAvatarList(const Params& p)  {  	setCommitOnSelectionChange(TRUE); // there's no such param in LLScrollListCtrl::Params +	// display a context menu appropriate for a list of avatar names +	setContextMenu(LLScrollListCtrl::MENU_AVATAR); +      // "volume" column      {      	LLScrollListColumn::Params col_params; diff --git a/indra/newview/llchatmsgbox.cpp b/indra/newview/llchatmsgbox.cpp index fb5ab8ec5a..e6398dd47a 100644 --- a/indra/newview/llchatmsgbox.cpp +++ b/indra/newview/llchatmsgbox.cpp @@ -1,10 +1,11 @@  /**    * @file llchatmsgbox.cpp + * @author Martin Reddy   * @brief chat history text box, able to show array of strings with separator   * - * $LicenseInfo:firstyear=2004&license=viewergpl$ + * $LicenseInfo:firstyear=2009&license=viewergpl$   *  - * Copyright (c) 2004-2009, Linden Research, Inc. + * Copyright (c) 2009, Linden Research, Inc.   *    * Second Life Viewer Source Code   * The source code in this file ("Source Code") is provided by Linden Lab @@ -30,361 +31,96 @@   * $/LicenseInfo$   */ -  #include "llviewerprecompiledheaders.h"  #include "llchatmsgbox.h"  #include "llwindow.h" -#include "llfocusmgr.h" - -static LLDefaultChildRegistry::Register<LLChatMsgBox> r("text_chat"); -LLChatMsgBox::Params::Params() -:	text_color("text_color"), -	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), -	hover_color("hover_color"), -	disabled_color("disabled_color"), -	background_color("background_color"), -	border_color("border_color"), -	line_spacing("line_spacing", 4), -	block_spacing("block_spacing",10), -	text("text"), -	font_shadow("font_shadow", LLFontGL::NO_SHADOW) -{} -LLChatMsgBox::LLChatMsgBox(const LLChatMsgBox::Params& p) -:	LLUICtrl(p), -    mFontGL(p.font), -	mHoverActive( p.highlight_on_hover ), -	mHasHover( FALSE ), -	mBackgroundVisible( p.bg_visible ), -	mBorderVisible( p.border_visible ), -	mShadowType( p.font_shadow ), -	mBorderDropShadowVisible( p.border_drop_shadow_visible ), -	mUseEllipses( p.use_ellipses ), -	mVAlign( LLFontGL::TOP ), -	mClickedCallback(NULL), -	mTextColor(p.text_color()), -	mDisabledColor(p.disabled_color()), -	mBackgroundColor(p.background_color()), -	mBorderColor(p.border_color()), -	mHoverColor(p.hover_color()), -	mHAlign(p.font_halign), -	mLineSpacing(p.line_spacing), -	mBlockSpasing(p.block_spacing), -	mWordWrap( p.word_wrap ), -	mFontStyle(LLFontGL::getStyleFromString(p.font.style)) -{ -	setText( p.text() ); -} +static LLDefaultChildRegistry::Register<LLChatMsgBox> r("text_chat"); -BOOL LLChatMsgBox::handleMouseDown(S32 x, S32 y, MASK mask) +LLChatMsgBox::Params::Params() : +	block_spacing("block_spacing", 10)  { -	BOOL	handled = FALSE; - -	// HACK: Only do this if there actually is a click callback, so that -	// overly large text boxes in the older UI won't start eating clicks. -	if (mClickedCallback) -	{ -		handled = TRUE; - -		// Route future Mouse messages here preemptively.  (Release on mouse up.) -		gFocusMgr.setMouseCapture( this ); -		 -		if (getSoundFlags() & MOUSE_DOWN) -		{ -			make_ui_sound("UISndClick"); -		} -	} - -	return handled; +	line_spacing = 4;  } -BOOL LLChatMsgBox::handleMouseUp(S32 x, S32 y, MASK mask) -{ -	BOOL	handled = FALSE; - -	// 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 -	// overly large text boxes in the older UI won't start eating clicks. -	if (mClickedCallback -		&& hasMouseCapture()) -	{ -		handled = TRUE; - -		// Release the mouse -		gFocusMgr.setMouseCapture( NULL ); - -		if (getSoundFlags() & MOUSE_UP) -		{ -			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) -		{ -			mClickedCallback(); -		} -	} - -	return handled; -} +LLChatMsgBox::LLChatMsgBox(const Params& p) : +	LLTextBox(p), +	mBlockSpacing(p.block_spacing) +{} -BOOL LLChatMsgBox::handleHover(S32 x, S32 y, MASK mask) +void LLChatMsgBox::addText( const LLStringExplicit& text )  { -	BOOL handled = LLView::handleHover(x,y,mask); -	if(mHoverActive) +	LLWString t = mText.getWString(); +	if (! t.empty())  	{ -		mHasHover = TRUE; // This should be set every frame during a hover. -		getWindow()->setCursor(UI_CURSOR_ARROW); +		t += '\n';  	} - -	return (handled || mHasHover); -} - -void	LLChatMsgBox::addText( const LLStringExplicit& text ) -{ -	boost::shared_ptr<text_block> t(new text_block()); -	t->text = wrapText(text); -	setLineLengths(*t); -	mTextStrings.push_back(t); +	t += getWrappedText(text); +	LLTextBox::setText(wstring_to_utf8str(t)); +	mSeparatorOffset.push_back(getLength());  }  void LLChatMsgBox::setText(const LLStringExplicit& text)  { -	mTextStrings.clear(); - +	mSeparatorOffset.clear(); +	mText.clear();  	addText(text); - -} - -void LLChatMsgBox::resetLineLengths() -{ -	for(std::vector< boost::shared_ptr<text_block> >::iterator it = mTextStrings.begin(); -			it!=mTextStrings.end();++it) -	{ -		boost::shared_ptr<text_block> tblock = *it; -		setLineLengths(*tblock); -	} -} - -void LLChatMsgBox::setLineLengths(text_block& t) -{ -	t.lines.clear(); -	 -	std::string::size_type  cur = 0; -	std::string::size_type  len = t.text.length(); - -	while (cur < len)  -	{ -		std::string::size_type end = t.text.getWString().find('\n', cur); -		std::string::size_type runLen; -		 -		if (end == std::string::npos) -		{ -			runLen = len - cur; -			cur = len; -		} -		else -		{ -			runLen = end - cur; -			cur = end + 1; // skip the new line character -		} - -		t.lines.push_back( (S32)runLen ); -	} -} - -std::string LLChatMsgBox::wrapText(const LLStringExplicit& in_text, 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(); -	while (cur < len) -	{ -		LLWString::size_type end = wtext.find('\n', cur); -		if (end == LLWString::npos) -		{ -			end = len; -		} -		 -		LLWString::size_type runLen = end - cur; -		if (runLen > 0) -		{ -			LLWString run(wtext, cur, runLen); -			LLWString::size_type useLen = -				mFontGL->maxDrawableChars(run.c_str(), max_width, runLen, TRUE); - -			final_wtext.append(wtext, cur, useLen); -			cur += useLen; -			// not enough room to add any more characters -			if (useLen == 0) break; -		} - -		if (cur < len) -		{ -			if (wtext[cur] == '\n') -				cur += 1; - -			// There is no need to to cut line ending symbols found in origin string, see EXT-702. -			final_wtext += '\n'; -		} -	} -	 -	std::string final_text = wstring_to_utf8str(final_wtext); -	return final_text;  } -S32	LLChatMsgBox::getTextLinesNum() -{ -	S32 num_lines = 0; -	for(std::vector< boost::shared_ptr<text_block> >::iterator it = mTextStrings.begin(); -			it!=mTextStrings.end();++it) -	{ -		boost::shared_ptr<text_block> tblock = *it; -		num_lines+=tblock->lines.size(); -	} - -	if( num_lines < 1 ) -	{ -		num_lines = 1; -	} - -	return num_lines; +void LLChatMsgBox::setValue(const LLSD& value ) +{  +	setText(value.asString());  }  S32 LLChatMsgBox::getTextPixelHeight()  { +	S32 num_blocks = mSeparatorOffset.size();  	S32 num_lines = getTextLinesNum(); -	return (S32)(num_lines * mFontGL->getLineHeight() +  (num_lines-1)*mLineSpacing + mBlockSpasing*(mTextStrings.size()-1) + 2*mLineSpacing);//some extra space -} - -void LLChatMsgBox::setValue(const LLSD& value ) -{  -	setText(value.asString()); +	return (S32)(num_lines * mDefaultFont->getLineHeight() + \ +				 (num_lines-1) * mLineSpacing + \ +				 (num_blocks-1) * mBlockSpacing + \ +				 2 * mLineSpacing);  } - -void LLChatMsgBox::draw() +S32 LLChatMsgBox::getTextLinesNum()  { -	if (mBorderVisible) -	{ -		gl_rect_2d_offset_local(getLocalRect(), 2, FALSE); -	} - -	if( mBorderDropShadowVisible ) -	{ -		static LLUICachedControl<LLColor4> color_drop_shadow ("ColorDropShadow", *(new LLColor4)); -		static LLUICachedControl<S32> drop_shadow_tooltip ("DropShadowTooltip", 0); -		gl_drop_shadow(0, getRect().getHeight(), getRect().getWidth(), 0, -			color_drop_shadow, drop_shadow_tooltip); -	} - -	if (mBackgroundVisible) -	{ -		LLRect r( 0, getRect().getHeight(), getRect().getWidth(), 0 ); -		gl_rect_2d( r, mBackgroundColor.get() ); -	} - -	S32 text_x = 0; -	switch( mHAlign ) -	{ -	case LLFontGL::LEFT:	 -		break; -	case LLFontGL::HCENTER: -		text_x = getRect().getWidth() / 2; -		break; -	case LLFontGL::RIGHT: -		text_x = getRect().getWidth() ; -		break; -	} - -	S32 text_y = getRect().getHeight() ; - -	if ( getEnabled() ) -	{ -		if(mHasHover) -		{ -			drawText( text_x, text_y, mHoverColor.get() ); -		} -		else -		{ -			drawText( text_x, text_y, mTextColor.get() ); -		}				 -	} -	else -	{ -		drawText( text_x, text_y, mDisabledColor.get() ); -	} - -	if (sDebugRects) +	S32 num_lines = getLineCount(); +	if (num_lines < 1)  	{ -		drawDebugRect(); +		num_lines = 1;  	} - -	//// *HACK: also draw debug rectangles around currently-being-edited LLView, and any elements that are being highlighted by GUI preview code (see LLFloaterUIPreview) -	//std::set<LLView*>::iterator iter = std::find(sPreviewHighlightedElements.begin(), sPreviewHighlightedElements.end(), this); -	//if ((sEditingUI && this == sEditingUIView) || (iter != sPreviewHighlightedElements.end() && sDrawPreviewHighlights)) -	//{ -	//	drawDebugRect(); -	//} - -	mHasHover = FALSE; // This is reset every frame. -} - -void LLChatMsgBox::reshape(S32 width, S32 height, BOOL called_from_parent) -{ -	// reparse line lengths -	LLView::reshape(width, height, called_from_parent); -	resetLineLengths(); +	 +	return num_lines;  } -void LLChatMsgBox::drawText( S32 x, S32 y, const LLColor4& color ) +void LLChatMsgBox::drawText(S32 x, S32 y, const LLWString &text, const LLColor4 &color)  { +	S32 start = 0;  	S32 width = getRect().getWidth()-10; -	 -	for(std::vector< boost::shared_ptr<text_block> >::iterator it = mTextStrings.begin(); -			it!=mTextStrings.end();++it) +	// iterate through each block of text that has been added +	y -= mLineSpacing; +	for (std::vector<S32>::iterator it = mSeparatorOffset.begin(); true ;)  	{ -		boost::shared_ptr<text_block> tblock = *it; +		// display the text for this block +		S32 num_chars = *it - start; +		LLWString text = mDisplayText.substr(start, num_chars); +		LLTextBox::drawText(x, y, text, color); -		S32 cur_pos = 0; -		for (std::vector<S32>::iterator iter = tblock->lines.begin(); -			iter != tblock->lines.end(); ++iter) +		// exit the loop if this is the last text block +		start += num_chars + 1;  // skip the newline +		if (++it == mSeparatorOffset.end())  		{ -			S32 line_length = *iter; -			mFontGL->render(tblock->text, cur_pos, (F32)x, (F32)y, color, -							mHAlign, mVAlign, -							mFontStyle, -							mShadowType, -							line_length, getRect().getWidth(), NULL, mUseEllipses ); -			cur_pos += line_length + 1; -			y -= llfloor(mFontGL->getLineHeight()) + mLineSpacing; - -		} -		std::vector< boost::shared_ptr<text_block> >::iterator next = it; -		++next; -		if(next == mTextStrings.end())  			break; -		//separator -		gl_line_2d(5,y-mBlockSpasing/2,width,y-mBlockSpasing/2,LLColor4::grey); -		y-=mBlockSpasing; -	} +		} +		// output a separator line between blocks +		S32 num_lines = std::count(text.begin(), text.end(), '\n') + 1; +		y -= num_lines * (llfloor(mDefaultFont->getLineHeight()) + mLineSpacing); +		S32 sep_y = y - mBlockSpacing/2 + mLineSpacing/2; +		gl_line_2d(5, sep_y, width, sep_y, LLColor4::grey); +		y -= mBlockSpacing; +	}  } - diff --git a/indra/newview/llchatmsgbox.h b/indra/newview/llchatmsgbox.h index 61035499c7..b81b740bdc 100644 --- a/indra/newview/llchatmsgbox.h +++ b/indra/newview/llchatmsgbox.h @@ -1,10 +1,11 @@  /**    * @file llchatmsgbox.h + * @author Martin Reddy   * @brief chat history text box, able to show array of strings with separator   * - * $LicenseInfo:firstyear=2004&license=viewergpl$ + * $LicenseInfo:firstyear=2009&license=viewergpl$   *  - * Copyright (c) 2004-2009, Linden Research, Inc. + * Copyright (c) 2009, Linden Research, Inc.   *    * Second Life Viewer Source Code   * The source code in this file ("Source Code") is provided by Linden Lab @@ -33,127 +34,45 @@  #ifndef LL_LLCHATMSGBOX_H  #define LL_LLCHATMSGBOX_H - +#include "lltextbox.h"  #include "lluictrl.h"  #include "v4color.h"  #include "llstring.h" -#include "lluistring.h" - -class LLChatMsgBox -:	public LLUICtrl +/// +/// LLChatMsgBox provides a text box with support for multiple blocks +/// of text that can be added incrementally. Each block of text is +/// visual separated from the previous block (e.g., with a horizontal +/// line). +/// +class LLChatMsgBox : +	public LLTextBox  { -protected: -	struct text_block -	{ -		LLUIString			text; -		std::vector<S32>	lines; -	};  public: -	typedef boost::function<void (void)> callback_t; - -	struct Params : public LLInitParam::Block<Params, LLUICtrl::Params> +	struct Params : public LLInitParam::Block<Params, LLTextBox::Params>  	{ -		Optional<std::string> text; - -		Optional<bool>		highlight_on_hover, -							border_visible, -							border_drop_shadow_visible, -							bg_visible, -							use_ellipses, -							word_wrap; - -		Optional<LLFontGL::ShadowType>	font_shadow; - -		Optional<LLUIColor>	text_color, -							hover_color, -							disabled_color, -							background_color, -							border_color; - -		Optional<S32>		line_spacing; -		 -		Optional<S32>		block_spacing; +		Optional<S32>	block_spacing;  		Params();  	}; +  protected:  	LLChatMsgBox(const Params&);  	friend class LLUICtrlFactory; -public: -	virtual void	draw(); -	virtual void	reshape(S32 width, S32 height, BOOL called_from_parent = TRUE); -	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); - -	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			addText( const LLStringExplicit& text ); -	 -	void			setUseEllipses( BOOL use_ellipses )		{ mUseEllipses = use_ellipses; } +public: +	void				setText(const LLStringExplicit &text); +	void				addText(const LLStringExplicit &text); -	void			setBackgroundVisible(BOOL visible)		{ mBackgroundVisible = visible; } -	void			setBorderVisible(BOOL visible)			{ mBorderVisible = visible; } -	void			setBorderDropshadowVisible(BOOL visible){ mBorderDropShadowVisible = visible; } -	void			setRightAlign()							{ mHAlign = LLFontGL::RIGHT; } -	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; } - -	S32				getTextPixelHeight(); -	S32				getTextLinesNum(); - -	virtual void	setValue(const LLSD& value );		 - +	S32					getTextPixelHeight(); +	S32					getTextLinesNum(); +	/*virtual*/ void	setValue(const LLSD &value); +	/*virtual*/ void	drawText(S32 x, S32 y, const LLWString &text, const LLColor4 &color);  private: -	std::string		wrapText			(const LLStringExplicit& in_text, F32 max_width = -1.0); - -	void			setLineLengths		(text_block& t); -	void			resetLineLengths	(); -	void			drawText			(S32 x, S32 y, const LLColor4& color ); - -	const LLFontGL*	mFontGL; -	LLUIColor	mTextColor; -	LLUIColor	mDisabledColor; -	LLUIColor	mBackgroundColor; -	LLUIColor	mBorderColor; -	LLUIColor	mHoverColor; - -	BOOL			mHoverActive;	 -	BOOL			mHasHover; -	BOOL			mBackgroundVisible; -	BOOL			mBorderVisible; -	BOOL			mWordWrap; -	 -	U8				mFontStyle; // style bit flags for font -	LLFontGL::ShadowType mShadowType; -	BOOL			mBorderDropShadowVisible; -	BOOL			mUseEllipses; - -	S32				mLineSpacing; -	S32				mBlockSpasing; - -	LLFontGL::HAlign mHAlign; -	LLFontGL::VAlign mVAlign; - -	callback_t		mClickedCallback; - - -	//same as mLineLengthList and mText in LLTextBox -	std::vector< boost::shared_ptr<text_block> > mTextStrings; - +	S32					mBlockSpacing; +	std::vector<S32>	mSeparatorOffset;  };  #endif diff --git a/indra/newview/llfloaterabout.cpp b/indra/newview/llfloaterabout.cpp index 56c5eaa70e..caa10e9452 100644 --- a/indra/newview/llfloaterabout.cpp +++ b/indra/newview/llfloaterabout.cpp @@ -99,24 +99,20 @@ BOOL LLFloaterAbout::postBuild()  	LLViewerTextEditor *credits_widget =   		getChild<LLViewerTextEditor>("credits_editor", true); -	// For some reason, adding style doesn't work unless this is true. +	// make sure that we handle hyperlinks in the About text  	support_widget->setParseHTML(TRUE); -	// Text styles for release notes hyperlinks -	LLStyle::Params link_style_params; -	link_style_params.color.control = "HTMLLinkColor"; -	link_style_params.link_href = get_viewer_release_notes_url(); -  	// Version string  	std::string version = LLTrans::getString("APP_NAME")  		+ llformat(" %d.%d.%d (%d) %s %s (%s)\n",  				   LL_VERSION_MAJOR, LL_VERSION_MINOR, LL_VERSION_PATCH, LL_VIEWER_BUILD,  				   __DATE__, __TIME__,  				   gSavedSettings.getString("VersionChannelName").c_str()); -	support_widget->appendColoredText(version, FALSE, FALSE, LLUIColorTable::instance().getColor("TextFgReadOnlyColor")); -	support_widget->appendStyledText(LLTrans::getString("ReleaseNotes"), false, false, link_style_params);  	std::string support; +	support.append(version); +	support.append("[" + get_viewer_release_notes_url() + " " + +				   LLTrans::getString("ReleaseNotes") + "]");  	support.append("\n\n");  #if LL_MSVC @@ -131,10 +127,6 @@ BOOL LLFloaterAbout::postBuild()  	LLViewerRegion* region = gAgent.getRegion();  	if (region)  	{ -		LLStyle::Params server_link_style_params; -		server_link_style_params.color.control = "HTMLLinkColor"; -		server_link_style_params.link_href = region->getCapability("ServerReleaseNotes"); -  		const LLVector3d &pos = gAgent.getPositionGlobal();  		LLUIString pos_text = getString("you_are_at");  		pos_text.setArg("[POSITION]", @@ -154,11 +146,9 @@ BOOL LLFloaterAbout::postBuild()  		support.append(")\n");  		support.append(gLastVersionChannel);  		support.append("\n"); - -		support_widget->appendColoredText(support, FALSE, FALSE, LLUIColorTable::instance().getColor("TextFgReadOnlyColor")); -		support_widget->appendStyledText(LLTrans::getString("ReleaseNotes"), false, false, server_link_style_params); - -		support = "\n\n"; +		support.append("[" + LLWeb::escapeURL(region->getCapability("ServerReleaseNotes")) + +					   " " + LLTrans::getString("ReleaseNotes") + "]"); +		support.append("\n\n");  	}  	// *NOTE: Do not translate text like GPU, Graphics Card, etc - @@ -248,20 +238,20 @@ BOOL LLFloaterAbout::postBuild()  } - static std::string get_viewer_release_notes_url() - { - 	std::ostringstream version; - 	version << LL_VERSION_MAJOR << "." - 		<< LL_VERSION_MINOR << "." - 		<< LL_VERSION_PATCH << "." - 		<< LL_VERSION_BUILD; +static std::string get_viewer_release_notes_url() +{ +	std::ostringstream version; +	version << LL_VERSION_MAJOR << "." +		<< LL_VERSION_MINOR << "." +		<< LL_VERSION_PATCH << "." +		<< LL_VERSION_BUILD; - 	LLSD query; - 	query["channel"] = gSavedSettings.getString("VersionChannelName"); - 	query["version"] = version.str(); +	LLSD query; +	query["channel"] = gSavedSettings.getString("VersionChannelName"); +	query["version"] = version.str(); - 	std::ostringstream url; -	 url << LLTrans::getString("RELEASE_NOTES_BASE_URL") << LLURI::mapToQueryString(query); +	std::ostringstream url; +	url << LLTrans::getString("RELEASE_NOTES_BASE_URL") << LLURI::mapToQueryString(query); - 	return url.str(); - } +	return LLWeb::escapeURL(url.str()); +} diff --git a/indra/newview/llfloaterfriends.cpp b/indra/newview/llfloaterfriends.cpp index eb73bd6d8f..0c77d88efb 100644 --- a/indra/newview/llfloaterfriends.cpp +++ b/indra/newview/llfloaterfriends.cpp @@ -194,6 +194,7 @@ BOOL LLPanelFriends::postBuild()  	mFriendsList->setMaxSelectable(MAX_FRIEND_SELECT);  	mFriendsList->setMaximumSelectCallback(boost::bind(&LLPanelFriends::onMaximumSelect));  	mFriendsList->setCommitOnSelectionChange(TRUE); +	mFriendsList->setContextMenu(LLScrollListCtrl::MENU_AVATAR);  	childSetCommitCallback("friend_list", onSelectName, this);  	getChild<LLScrollListCtrl>("friend_list")->setDoubleClickCallback(onClickIM, this); diff --git a/indra/newview/llfloatergroups.cpp b/indra/newview/llfloatergroups.cpp index 7a88612f1a..b1f40d9d1d 100644 --- a/indra/newview/llfloatergroups.cpp +++ b/indra/newview/llfloatergroups.cpp @@ -82,7 +82,12 @@ void LLFloaterGroupPicker::setPowersMask(U64 powers_mask)  BOOL LLFloaterGroupPicker::postBuild()  {  	LLScrollListCtrl* list_ctrl = getChild<LLScrollListCtrl>("group list"); -	init_group_list(list_ctrl, gAgent.getGroupID(), mPowersMask); +	if (list_ctrl) +	{ +		init_group_list(list_ctrl, gAgent.getGroupID(), mPowersMask); +		list_ctrl->setDoubleClickCallback(onBtnOK, this); +		list_ctrl->setContextMenu(LLScrollListCtrl::MENU_GROUP); +	}  	// Remove group "none" from list. Group "none" is added in init_group_list().   	// Some UI elements use group "none", we need to manually delete it here. @@ -100,8 +105,6 @@ BOOL LLFloaterGroupPicker::postBuild()  	setDefaultBtn("OK"); -	getChild<LLScrollListCtrl>("group list")->setDoubleClickCallback(onBtnOK, this); -  	childEnable("OK");  	return TRUE; @@ -183,7 +186,13 @@ BOOL LLPanelGroups::postBuild()  	childSetTextArg("groupcount", "[COUNT]", llformat("%d",gAgent.mGroups.count()));  	childSetTextArg("groupcount", "[MAX]", llformat("%d",MAX_AGENT_GROUPS)); -	init_group_list(getChild<LLScrollListCtrl>("group list"), gAgent.getGroupID()); +	LLScrollListCtrl *list = getChild<LLScrollListCtrl>("group list"); +	if (list) +	{ +		init_group_list(list, gAgent.getGroupID()); +		list->setDoubleClickCallback(onBtnIM, this); +		list->setContextMenu(LLScrollListCtrl::MENU_GROUP); +	}  	childSetAction("Activate", onBtnActivate, this); @@ -199,8 +208,6 @@ BOOL LLPanelGroups::postBuild()  	setDefaultBtn("IM"); -	getChild<LLScrollListCtrl>("group list")->setDoubleClickCallback(onBtnIM, this); -  	reset();  	return TRUE; diff --git a/indra/newview/llfloaterland.cpp b/indra/newview/llfloaterland.cpp index 4cd09faaaf..e5f5e8eedb 100644 --- a/indra/newview/llfloaterland.cpp +++ b/indra/newview/llfloaterland.cpp @@ -1061,6 +1061,7 @@ BOOL LLPanelLandObjects::postBuild()  	mOwnerList->sortByColumnIndex(3, FALSE);  	childSetCommitCallback("owner list", onCommitList, this);  	mOwnerList->setDoubleClickCallback(onDoubleClickOwner, this); +	mOwnerList->setContextMenu(LLScrollListCtrl::MENU_AVATAR);  	return TRUE;  } @@ -2297,11 +2298,17 @@ BOOL LLPanelLandAccess::postBuild()  	mListAccess = getChild<LLNameListCtrl>("AccessList");  	if (mListAccess) +	{  		mListAccess->sortByColumnIndex(0, TRUE); // ascending +		mListAccess->setContextMenu(LLScrollListCtrl::MENU_AVATAR); +	}  	mListBanned = getChild<LLNameListCtrl>("BannedList");  	if (mListBanned) +	{  		mListBanned->sortByColumnIndex(0, TRUE); // ascending +		mListBanned->setContextMenu(LLScrollListCtrl::MENU_AVATAR); +	}  	return TRUE;  } diff --git a/indra/newview/llgrouplist.cpp b/indra/newview/llgrouplist.cpp index 278fd5b9f6..73d3a60701 100644 --- a/indra/newview/llgrouplist.cpp +++ b/indra/newview/llgrouplist.cpp @@ -51,6 +51,8 @@ LLGroupList::Params::Params()  LLGroupList::LLGroupList(const Params& p)  :	LLAvatarList(p)  { +	// display a context menu appropriate for a list of group names +	setContextMenu(LLScrollListCtrl::MENU_GROUP);  }  static bool findInsensitive(std::string haystack, const std::string& needle_upper) diff --git a/indra/newview/llimpanel.cpp b/indra/newview/llimpanel.cpp index 9cf3e57e22..674fff4040 100644 --- a/indra/newview/llimpanel.cpp +++ b/indra/newview/llimpanel.cpp @@ -2125,6 +2125,7 @@ BOOL LLIMFloater::postBuild()  	childSetCommitCallback("chat_editor", onSendMsg, this);  	mHistoryEditor = getChild<LLViewerTextEditor>("im_text"); +	mHistoryEditor->setParseHTML(TRUE);  	setTitle(LLIMModel::instance().getName(mSessionID));  	setDocked(true); diff --git a/indra/newview/llnamelistctrl.cpp b/indra/newview/llnamelistctrl.cpp index 1b82c2dc18..8ef6b25c50 100644 --- a/indra/newview/llnamelistctrl.cpp +++ b/indra/newview/llnamelistctrl.cpp @@ -61,9 +61,9 @@ LLNameListCtrl::Params::Params()  LLNameListCtrl::LLNameListCtrl(const LLNameListCtrl::Params& p)  :	LLScrollListCtrl(p), -	mAllowCallingCardDrop(p.allow_calling_card_drop), +	mNameColumnIndex(p.name_column.column_index),  	mNameColumn(p.name_column.column_name), -	mNameColumnIndex(p.name_column.column_index) +	mAllowCallingCardDrop(p.allow_calling_card_drop)  {}  // public diff --git a/indra/newview/llpanelavatar.cpp b/indra/newview/llpanelavatar.cpp index 6e94b087a6..b2d606ab4d 100644 --- a/indra/newview/llpanelavatar.cpp +++ b/indra/newview/llpanelavatar.cpp @@ -595,9 +595,8 @@ BOOL LLPanelAvatarMeProfile::postBuild()  	childSetCommitCallback("status_combo", boost::bind(&LLPanelAvatarMeProfile::onStatusChanged, this), NULL);  	childSetCommitCallback("status_me_message_text", boost::bind(&LLPanelAvatarMeProfile::onStatusMessageChanged, this), NULL); -	childSetActionTextbox("payment_update_link", boost::bind(&LLPanelAvatarMeProfile::onUpdateAccountTextboxClicked, this)); -	childSetActionTextbox("my_account_link", boost::bind(&LLPanelAvatarMeProfile::onMyAccountTextboxClicked, this)); -	childSetActionTextbox("partner_edit_link", boost::bind(&LLPanelAvatarMeProfile::onPartnerEditTextboxClicked, this)); + +	childSetTextArg("partner_edit_link", "[URL]", getString("partner_edit_link_url"));  	resetControls();  	resetData(); @@ -677,17 +676,3 @@ void LLPanelAvatarMeProfile::onStatusMessageChanged()  	updateData();  } -void LLPanelAvatarMeProfile::onUpdateAccountTextboxClicked() -{ -	onUrlTextboxClicked(getString("payment_update_link_url")); -} - -void LLPanelAvatarMeProfile::onMyAccountTextboxClicked() -{ -	onUrlTextboxClicked(getString("my_account_link_url")); -} - -void LLPanelAvatarMeProfile::onPartnerEditTextboxClicked() -{ -	onUrlTextboxClicked(getString("partner_edit_link_url")); -} diff --git a/indra/newview/llpanelavatar.h b/indra/newview/llpanelavatar.h index 51bd619901..4ee4cb6e87 100644 --- a/indra/newview/llpanelavatar.h +++ b/indra/newview/llpanelavatar.h @@ -203,9 +203,6 @@ protected:  	void onStatusChanged();  	void onStatusMessageChanged(); -	void onUpdateAccountTextboxClicked(); -	void onMyAccountTextboxClicked(); -	void onPartnerEditTextboxClicked();  private: diff --git a/indra/newview/llpanelgroupgeneral.cpp b/indra/newview/llpanelgroupgeneral.cpp index 4b2a1a4e48..f3893a104c 100644 --- a/indra/newview/llpanelgroupgeneral.cpp +++ b/indra/newview/llpanelgroupgeneral.cpp @@ -113,6 +113,7 @@ BOOL LLPanelGroupGeneral::postBuild()  	if (mListVisibleMembers)  	{  		mListVisibleMembers->setDoubleClickCallback(openProfile, this); +		mListVisibleMembers->setContextMenu(LLScrollListCtrl::MENU_AVATAR);  	}  	// Options diff --git a/indra/newview/llpanelgrouproles.cpp b/indra/newview/llpanelgrouproles.cpp index 59f7319b2b..48c9c16780 100644 --- a/indra/newview/llpanelgrouproles.cpp +++ b/indra/newview/llpanelgrouproles.cpp @@ -843,6 +843,7 @@ BOOL LLPanelGroupMembersSubTab::postBuildSubTab(LLView* root)  	mMembersList->setCommitCallback(onMemberSelect, this);  	// Show the member's profile on double click.  	mMembersList->setDoubleClickCallback(onMemberDoubleClick, this); +	mMembersList->setContextMenu(LLScrollListCtrl::MENU_AVATAR);  	LLButton* button = parent->getChild<LLButton>("member_invite", recurse);  	if ( button ) @@ -1737,6 +1738,8 @@ BOOL LLPanelGroupRolesSubTab::postBuildSubTab(LLView* root)  	mRolesList->setCommitOnSelectionChange(TRUE);  	mRolesList->setCommitCallback(onRoleSelect, this); +	mAssignedMembersList->setContextMenu(LLScrollListCtrl::MENU_AVATAR); +  	mMemberVisibleCheck->setCommitCallback(onMemberVisibilityChange, this);  	mAllowedActionsList->setCommitOnSelectionChange(TRUE); @@ -2403,6 +2406,7 @@ BOOL LLPanelGroupActionsSubTab::postBuildSubTab(LLView* root)  	mActionList->setCommitOnSelectionChange(TRUE);  	mActionList->setCommitCallback(boost::bind(&LLPanelGroupActionsSubTab::handleActionSelect, this)); +	mActionList->setContextMenu(LLScrollListCtrl::MENU_AVATAR);  	update(GC_ALL); diff --git a/indra/newview/llpreviewnotecard.cpp b/indra/newview/llpreviewnotecard.cpp index cadab71ba8..29320522d9 100644 --- a/indra/newview/llpreviewnotecard.cpp +++ b/indra/newview/llpreviewnotecard.cpp @@ -87,6 +87,7 @@ BOOL LLPreviewNotecard::postBuild()  	LLViewerTextEditor *ed = getChild<LLViewerTextEditor>("Notecard Editor");  	if (ed)  	{ +		ed->setParseHTML(TRUE);  		ed->setNotecardInfo(mItemUUID, mObjectID, getKey());  		ed->makePristine();  	} @@ -126,7 +127,7 @@ void LLPreviewNotecard::draw()  {  	LLViewerTextEditor* editor = getChild<LLViewerTextEditor>("Notecard Editor");  	BOOL changed = !editor->isPristine(); -	 +  	childSetEnabled("Save", changed && getEnabled());  	LLPreview::draw(); diff --git a/indra/newview/llprogressview.cpp b/indra/newview/llprogressview.cpp index f70cfc59ec..aa603c417f 100644 --- a/indra/newview/llprogressview.cpp +++ b/indra/newview/llprogressview.cpp @@ -72,7 +72,6 @@ const S32 ANIMATION_FRAMES = 1; //13;  LLProgressView::LLProgressView(const LLRect &rect)   :	LLPanel(),  	mPercentDone( 0.f ), -	mURLInMessage(false),  	mMouseDownInActiveArea( false )  {  	LLUICtrlFactory::getInstance()->buildPanel(this, "panel_progress.xml"); @@ -207,12 +206,7 @@ void LLProgressView::setPercent(const F32 percent)  void LLProgressView::setMessage(const std::string& msg)  {  	mMessage = msg; -	mURLInMessage = (mMessage.find( "https://" ) != std::string::npos || -			 mMessage.find( "http://" ) != std::string::npos || -			 mMessage.find( "ftp://" ) != std::string::npos); -  	getChild<LLTextBox>("message_text")->setWrappedText(LLStringExplicit(mMessage)); -	getChild<LLTextBox>("message_text")->setHoverActive(mURLInMessage);  }  void LLProgressView::setCancelButtonVisible(BOOL b, const std::string& label) diff --git a/indra/newview/llprogressview.h b/indra/newview/llprogressview.h index 83574ff52a..865646c85d 100644 --- a/indra/newview/llprogressview.h +++ b/indra/newview/llprogressview.h @@ -74,7 +74,6 @@ protected:  	LLFrameTimer mProgressTimer;  	LLRect mOutlineRect;  	bool mMouseDownInActiveArea; -	bool mURLInMessage;  	static LLProgressView* sInstance;  }; diff --git a/indra/newview/llviewermessage.cpp b/indra/newview/llviewermessage.cpp index 18fab3ec2e..dfb1c330e5 100644 --- a/indra/newview/llviewermessage.cpp +++ b/indra/newview/llviewermessage.cpp @@ -1,4 +1,3 @@ -  /**    * @file llviewermessage.cpp   * @brief Dumping ground for viewer-side message system callbacks. @@ -1474,6 +1473,13 @@ void process_improved_im(LLMessageSystem *msg, void **user_data)  	binary_bucket_size = msg->getSizeFast(_PREHASH_MessageBlock, _PREHASH_BinaryBucket);  	EInstantMessage dialog = (EInstantMessage)d; +    // make sure that we don't have an empty or all-whitespace name +	LLStringUtil::trim(name); +	if (name.empty()) +	{ +        name = LLTrans::getString("Unnamed"); +	} +  	BOOL is_busy = gAgent.getBusy();  	BOOL is_muted = LLMuteList::getInstance()->isMuted(from_id, name, LLMute::flagTextChat);  	BOOL is_linden = LLMuteList::getInstance()->isLinden(name); diff --git a/indra/newview/llviewertexteditor.cpp b/indra/newview/llviewertexteditor.cpp index de01e79803..5bb0c9a120 100644 --- a/indra/newview/llviewertexteditor.cpp +++ b/indra/newview/llviewertexteditor.cpp @@ -814,38 +814,18 @@ BOOL LLViewerTextEditor::handleMouseUp(S32 x, S32 y, MASK mask)  BOOL LLViewerTextEditor::handleRightMouseDown(S32 x, S32 y, MASK mask)  { -	BOOL handled = childrenHandleRightMouseDown(x, y, mask) != NULL; - -	// *TODO: Add right click menus for SLURLs -// 	if(! handled) -// 	{ -// 		const LLTextSegment* cur_segment = getSegmentAtLocalPos( x, y ); -// 		if( cur_segment ) -// 		{ -// 			if(cur_segment->getStyle()->isLink()) -// 			{ -// 				handled = TRUE; -// 				mHTML = cur_segment->getStyle()->getLinkHREF(); -// 			} -// 		} -// 	} -// 	LLMenuGL* menu = (LLMenuGL*)mPopupMenuHandle.get(); -// 	if(handled && menu && mParseHTML && mHTML.length() > 0) -// 	{ -// 		menu->setVisible(TRUE); -// 		menu->arrange(); -// 		menu->updateParent(LLMenuGL::sMenuContainer); -// 		LLMenuGL::showPopup(this, menu, x, y); -// 		mHTML = ""; -// 	} -// 	else -// 	{ -// 		if(menu && menu->getVisible()) -// 		{ -// 			menu->setVisible(FALSE); -// 		} -// 	} -	return handled; +	// pop up a context menu for any Url under the cursor +	if (handleRightMouseDownOverUrl(this, x, y)) +	{ +		return TRUE; +	} + +	if (childrenHandleRightMouseDown(x, y, mask) != NULL) +	{ +		return TRUE; +	} + +	return FALSE;  }  BOOL LLViewerTextEditor::handleDoubleClick(S32 x, S32 y, MASK mask) @@ -1087,6 +1067,7 @@ llwchar LLViewerTextEditor::pasteEmbeddedItem(llwchar ext_char)  void LLViewerTextEditor::onValueChange(S32 start, S32 end)  {  	updateSegments(); +	updateLinkSegments();  	findEmbeddedItemSegments(start, end);  } diff --git a/indra/newview/llviewertexteditor.h b/indra/newview/llviewertexteditor.h index 9567bfbc48..2dfea4a589 100644 --- a/indra/newview/llviewertexteditor.h +++ b/indra/newview/llviewertexteditor.h @@ -35,7 +35,6 @@  #include "lltexteditor.h" -  //  // Classes  // @@ -137,9 +136,6 @@ private:  	LLPointer<class LLEmbeddedNotecardOpener> mInventoryCallback; -	// *TODO: Add right click menus for SLURLs -	//LLViewHandle mPopupMenuHandle; -  	//  	// Inner classes  	// diff --git a/indra/newview/llweb.cpp b/indra/newview/llweb.cpp index 300a5db7c3..3204c2d264 100644 --- a/indra/newview/llweb.cpp +++ b/indra/newview/llweb.cpp @@ -67,6 +67,7 @@ void LLWeb::initClass()  	LLAlertDialog::setURLLoader(&sAlertURLLoader);  } +  // static  void LLWeb::loadURL(const std::string& url)  { @@ -76,12 +77,19 @@ void LLWeb::loadURL(const std::string& url)  	}  	else  	{ -		LLFloaterReg::showInstance("media_browser",url); +		loadURLInternal(url);  	}  }  // static +void LLWeb::loadURLInternal(const std::string &url) +{ +	LLFloaterReg::showInstance("media_browser", url); +} + + +// static  void LLWeb::loadURLExternal(const std::string& url)  {  	std::string escaped_url = escapeURL(url); diff --git a/indra/newview/llweb.h b/indra/newview/llweb.h index 71cc236621..96a53db2ca 100644 --- a/indra/newview/llweb.h +++ b/indra/newview/llweb.h @@ -36,23 +36,29 @@  #include <string> +/// +/// The LLWeb class provides various static methods to display the +/// contents of a Url in a web browser. Variations are provided to  +/// let you specifically use the Second Life internal browser, the +/// operating system's default browser, or to respect the user's +/// setting for which of these two they prefer to use with SL. +///  class LLWeb  {  public:  	static void initClass(); -	// Loads unescaped url in either internal web browser or external -	// browser, depending on user settings. +	/// Load the given url in the user's preferred web browser  	static void loadURL(const std::string& url); -	 +	/// Load the given url in the user's preferred web browser	  	static void loadURL(const char* url) { loadURL( ll_safe_string(url) ); } - -	// Loads unescaped url in external browser. +	/// Load the given url in the Second Life internal web browser +	static void loadURLInternal(const std::string &url); +	/// Load the given url in the operating system's web browser  	static void loadURLExternal(const std::string& url); -	// Returns escaped (eg, " " to "%20") url +	// Returns escaped url (eg, " " to "%20") - used by all loadURL methods  	static std::string escapeURL(const std::string& url); -  };  #endif diff --git a/indra/newview/skins/default/xui/en/menu_url_agent.xml b/indra/newview/skins/default/xui/en/menu_url_agent.xml new file mode 100644 index 0000000000..463a9fccb6 --- /dev/null +++ b/indra/newview/skins/default/xui/en/menu_url_agent.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes" ?> +<context_menu + layout="topleft" + name="Url Popup"> +    <menu_item_call +     label="Show Resident Profile" +     layout="topleft" +     name="show_agent"> +        <menu_item_call.on_click +         function="Url.Execute" /> +    </menu_item_call> +    <menu_item_separator +     layout="topleft" /> +    <menu_item_call +     label="Copy Name To Clipboard" +     layout="topleft" +     name="url_copy_label"> +        <menu_item_call.on_click +         function="Url.CopyLabel" /> +    </menu_item_call> +    <menu_item_call +     label="Copy SLURL To Clipboard" +     layout="topleft" +     name="url_copy"> +        <menu_item_call.on_click +         function="Url.CopyUrl" /> +    </menu_item_call> +</context_menu> diff --git a/indra/newview/skins/default/xui/en/menu_url_group.xml b/indra/newview/skins/default/xui/en/menu_url_group.xml new file mode 100644 index 0000000000..cec0aa421e --- /dev/null +++ b/indra/newview/skins/default/xui/en/menu_url_group.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes" ?> +<context_menu + layout="topleft" + name="Url Popup"> +    <menu_item_call +     label="Show Group Information" +     layout="topleft" +     name="show_group"> +        <menu_item_call.on_click +         function="Url.Execute" /> +    </menu_item_call> +    <menu_item_separator +     layout="topleft" /> +    <menu_item_call +     label="Copy Group To Clipboard" +     layout="topleft" +     name="url_copy_label"> +        <menu_item_call.on_click +         function="Url.CopyLabel" /> +    </menu_item_call> +    <menu_item_call +     label="Copy SLURL To Clipboard" +     layout="topleft" +     name="url_copy"> +        <menu_item_call.on_click +         function="Url.CopyUrl" /> +    </menu_item_call> +</context_menu> diff --git a/indra/newview/skins/default/xui/en/menu_url_http.xml b/indra/newview/skins/default/xui/en/menu_url_http.xml new file mode 100644 index 0000000000..2503b4a2a3 --- /dev/null +++ b/indra/newview/skins/default/xui/en/menu_url_http.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes" ?> +<context_menu + layout="topleft" + name="Url Popup"> +    <menu_item_call +     label="Open Web Page" +     layout="topleft" +     name="url_open"> +        <menu_item_call.on_click +         function="Url.Open" /> +    </menu_item_call> +    <menu_item_separator +     layout="topleft" /> +    <menu_item_call +     label="Open in Internal Browser" +     layout="topleft" +     name="url_open_internal"> +        <menu_item_call.on_click +         function="Url.OpenInternal" /> +    </menu_item_call> +    <menu_item_call +     label="Open in External Browser" +     layout="topleft" +     name="url_open_external"> +        <menu_item_call.on_click +         function="Url.OpenExternal" /> +    </menu_item_call> +    <menu_item_separator +     layout="topleft" /> +    <menu_item_call +     label="Copy URL To Clipboard" +     layout="topleft" +     name="url_copy"> +        <menu_item_call.on_click +         function="Url.CopyUrl" /> +    </menu_item_call> +</context_menu> diff --git a/indra/newview/skins/default/xui/en/menu_url_objectim.xml b/indra/newview/skins/default/xui/en/menu_url_objectim.xml new file mode 100644 index 0000000000..7d09403b15 --- /dev/null +++ b/indra/newview/skins/default/xui/en/menu_url_objectim.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes" ?> +<context_menu + layout="topleft" + name="Url Popup"> +    <menu_item_call +     label="Show Object Information" +     layout="topleft" +     name="show_object"> +        <menu_item_call.on_click +         function="Url.Execute" /> +    </menu_item_call> +    <menu_item_separator +     layout="topleft" /> +    <menu_item_call +     label="Teleport to Object Location" +     layout="topleft" +     name="teleport_to_object"> +        <menu_item_call.on_click +         function="Url.Teleport" /> +    </menu_item_call> +    <menu_item_separator +     layout="topleft" /> +    <menu_item_call +     label="Copy Object Name To Clipboard" +     layout="topleft" +     name="url_copy_label"> +        <menu_item_call.on_click +         function="Url.CopyLabel" /> +    </menu_item_call> +    <menu_item_call +     label="Copy SLURL To Clipboard" +     layout="topleft" +     name="url_copy"> +        <menu_item_call.on_click +         function="Url.CopyUrl" /> +    </menu_item_call> +</context_menu> diff --git a/indra/newview/skins/default/xui/en/menu_url_parcel.xml b/indra/newview/skins/default/xui/en/menu_url_parcel.xml new file mode 100644 index 0000000000..bbd63c6d8c --- /dev/null +++ b/indra/newview/skins/default/xui/en/menu_url_parcel.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes" ?> +<context_menu + layout="topleft" + name="Url Popup"> +    <menu_item_call +     label="Show Parcel Information" +     layout="topleft" +     name="show_parcel"> +        <menu_item_call.on_click +         function="Url.Execute" /> +    </menu_item_call> +    <menu_item_separator +     layout="topleft" /> +    <menu_item_call +     label="Copy SLURL To Clipboard" +     layout="topleft" +     name="url_copy"> +        <menu_item_call.on_click +         function="Url.CopyUrl" /> +    </menu_item_call> +</context_menu> diff --git a/indra/newview/skins/default/xui/en/menu_url_slapp.xml b/indra/newview/skins/default/xui/en/menu_url_slapp.xml new file mode 100644 index 0000000000..19df721b2b --- /dev/null +++ b/indra/newview/skins/default/xui/en/menu_url_slapp.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes" ?> +<context_menu + layout="topleft" + name="Url Popup"> +    <menu_item_call +     label="Run This Command" +     layout="topleft" +     name="run_slapp"> +        <menu_item_call.on_click +         function="Url.Execute" /> +    </menu_item_call> +    <menu_item_separator +     layout="topleft" /> +    <menu_item_call +     label="Copy SLURL To Clipboard" +     layout="topleft" +     name="url_copy"> +        <menu_item_call.on_click +         function="Url.CopyUrl" /> +    </menu_item_call> +</context_menu> diff --git a/indra/newview/skins/default/xui/en/menu_url_slurl.xml b/indra/newview/skins/default/xui/en/menu_url_slurl.xml new file mode 100644 index 0000000000..0e9525fa4b --- /dev/null +++ b/indra/newview/skins/default/xui/en/menu_url_slurl.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes" ?> +<context_menu + layout="topleft" + name="Url Popup"> +    <menu_item_call +     label="Show Place Information" +     layout="topleft" +     name="show_place"> +        <menu_item_call.on_click +         function="Url.Execute" /> +    </menu_item_call> +    <menu_item_separator +     layout="topleft" /> +    <menu_item_call +     label="Teleport to Location" +     layout="topleft" +     name="teleport_to_location"> +        <menu_item_call.on_click +         function="Url.Teleport" /> +    </menu_item_call> +    <menu_item_separator +     layout="topleft" /> +    <menu_item_call +     label="Copy SLURL To Clipboard" +     layout="topleft" +     name="url_copy"> +        <menu_item_call.on_click +         function="Url.CopyUrl" /> +    </menu_item_call> +</context_menu> diff --git a/indra/newview/skins/default/xui/en/menu_url_teleport.xml b/indra/newview/skins/default/xui/en/menu_url_teleport.xml new file mode 100644 index 0000000000..22cc035e49 --- /dev/null +++ b/indra/newview/skins/default/xui/en/menu_url_teleport.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes" ?> +<context_menu + layout="topleft" + name="Url Popup"> +    <menu_item_call +     label="Teleport To This Location" +     layout="topleft" +     name="teleport"> +        <menu_item_call.on_click +         function="Url.Execute" /> +    </menu_item_call> +    <menu_item_separator +     layout="topleft" /> +    <menu_item_call +     label="Copy SLURL To Clipboard" +     layout="topleft" +     name="url_copy"> +        <menu_item_call.on_click +         function="Url.CopyUrl" /> +    </menu_item_call> +</context_menu> diff --git a/indra/newview/skins/default/xui/en/panel_edit_profile.xml b/indra/newview/skins/default/xui/en/panel_edit_profile.xml index c0366437db..fa02cdb4b2 100644 --- a/indra/newview/skins/default/xui/en/panel_edit_profile.xml +++ b/indra/newview/skins/default/xui/en/panel_edit_profile.xml @@ -12,6 +12,10 @@   name="edit_profile_panel"   top="10"   width="255"> +   <string +    name="partner_edit_link_url"> +       http://www.secondlife.com/account/partners.php?lang=en +   </string>     <scroll_container       color="DkGray2"       follows="left|top|right|bottom" @@ -243,11 +247,10 @@           top_pad="15"           value="Account Status:"           width="100" /> -        <link +        <text           type="string"           follows="left|top"           font="SansSerifSmall" -	 font.style="UNDERLINE"           height="15"           layout="topleft"           left_pad="10" @@ -277,15 +280,14 @@           top_pad="15"           value="Partner:"           width="100" /> -	<link +        <text           follows="left|top"           height="15" -	 font.style="UNDERLINE"           layout="topleft"           left_pad="10"           name="partner_edit_link"           top_delta="0" -         value="Edit" +         value="[[URL] Edit]"           width="100" />          <panel           follows="left|top|right" diff --git a/indra/newview/skins/default/xui/en/panel_profile.xml b/indra/newview/skins/default/xui/en/panel_profile.xml index d90be9ea25..135dcb167b 100644 --- a/indra/newview/skins/default/xui/en/panel_profile.xml +++ b/indra/newview/skins/default/xui/en/panel_profile.xml @@ -167,11 +167,10 @@           width="255">               Homepage:          </text> -        <link +        <text           follows="left|top|right"           height="15"           layout="topleft" -	 font.style="UNDERLINE"           left="10"           name="homepage_edit"           top_pad="5" @@ -212,11 +211,10 @@           top_pad="15"           value="Account Status:"           width="100" /> -       <!-- <link +       <!-- <text           type="string"           follows="left|top"           font="SansSerifSmall" -	 font.style="UNDERLINE"           height="15"           layout="topleft"           left_pad="10" @@ -246,16 +244,15 @@           top_pad="15"           value="Partner:"           width="100" /> -	<!--<link +        <text           follows="left|top"           height="15" -	 font.style="UNDERLINE"           layout="topleft"           left_pad="10"           name="partner_edit_link"           top_delta="0" -         value="Edit" -         width="100" />  --> +         value="[[URL] Edit]" +         width="100" />          <panel           follows="left|top|right"           height="15" diff --git a/indra/newview/skins/default/xui/en/panel_progress.xml b/indra/newview/skins/default/xui/en/panel_progress.xml index 4f23c4d26d..9b2461db7c 100644 --- a/indra/newview/skins/default/xui/en/panel_progress.xml +++ b/indra/newview/skins/default/xui/en/panel_progress.xml @@ -99,12 +99,12 @@                       halign="left"                       height="100"                       layout="topleft" -                     left="30" +                     left="45"                       line_spacing="2"                       name="message_text"                       text_color="LoginProgressBoxTextColor"                       top="145" -                     width="610" /> +                     width="550" />                  </layout_panel>                  <layout_panel                   height="200" diff --git a/indra/newview/skins/default/xui/en/strings.xml b/indra/newview/skins/default/xui/en/strings.xml index 323c08ec4c..b8152a4956 100644 --- a/indra/newview/skins/default/xui/en/strings.xml +++ b/indra/newview/skins/default/xui/en/strings.xml @@ -74,6 +74,18 @@  	<string name="TooltipMustSingleDrop">Only a single item can be dragged here</string>	  	<string name="TooltipAltLeft">Alt-Left arrow for previous tab</string>	  	<string name="TooltipAltRight">Alt-Right arrow for next tab</string>	 + +	<!-- tooltips for Urls --> +	<string name="TooltipHttpUrl">Click to view this web page</string> +	<string name="TooltipSLURL">Click to view this location's information</string> +	<string name="TooltipAgentUrl">Click to view this resident's profile</string> +	<string name="TooltipGroupUrl">Click to view this group's description</string> +	<string name="TooltipEventUrl">Click to view this event's description</string> +	<string name="TooltipClassifiedUrl">Click to view this classified</string> +	<string name="TooltipParcelUrl">Click to view this parcel's description</string> +	<string name="TooltipTeleportUrl">Click to teleport to this location</string> +	<string name="TooltipObjectIMUrl">Click to view this object's description</string> +	<string name="TooltipSLAPP">Click to run the secondlife:// command</string>  	<!-- ButtonToolTips, llfloater.cpp -->  	<string name="BUTTON_CLOSE_DARWIN">Close (⌘-W)</string> @@ -259,6 +271,7 @@  	<!-- IM -->  	<string name="IM_logging_string">-- Instant message logging enabled --</string> +	<string name="Unnamed">(Unnamed)</string>  	<!-- Sim Access labels -->  	<string name="SIM_ACCESS_PG">PG</string>  | 
