diff --git a/indra/integration_tests/llui_libtest/CMakeLists.txt b/indra/integration_tests/llui_libtest/CMakeLists.txt
index 1ccdb0f20bb0343bb33acc7b57641bca1ace5600..84e3477ce62933b9ea231bb169fc1d09f19995a3 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 a7f899ce416f0516a762803853128a12c181aacc..790f2d5729f546153e353e28efe08ddbc2dbb6d2 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 637642cdcd1f5df0e9cebd8bc617be4ba96d4f04..b9a253aac8283c671261bfea95aa1728828b4c6e 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 253a58ab73226438c95c8de7520fd04a8c71a5b5..7a7e5be0bed6ce1562458a423b675911dc203065 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 @@ class LLScrollListCtrl : public LLUICtrl, public LLEditMenuHandler,
 
 	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 @@ class LLScrollListCtrl : public LLUICtrl, public LLEditMenuHandler,
 	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 @@ class LLScrollListCtrl : public LLUICtrl, public LLEditMenuHandler,
 
 	S32				mHighlightedItem;
 	class LLViewBorder*	mBorder;
+	LLContextMenu	*mPopupMenu;
 
 	LLWString		mSearchString;
 	LLFrameTimer	mSearchTimer;
@@ -438,6 +449,8 @@ class LLScrollListCtrl : public LLUICtrl, public LLEditMenuHandler,
 	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 929a809d88ed518b0e59912173680cc651afbd5f..c16ac0801443a5f4a499dc627bb8efba0dd112e3 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 dcf274a651346234537bf5a31612dc3b1c72a31d..5e8883afd7f780d64e1ee7570bbcab2e9bb51abe 100644
--- a/indra/llui/llstyle.h
+++ b/indra/llui/llstyle.h
@@ -69,9 +69,9 @@ class LLStyle : public LLRefCount
 
 	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 @@ class LLStyle : public LLRefCount
 			&& 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 @@ class LLStyle : public LLRefCount
 	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 0000000000000000000000000000000000000000..038ea2188f3aca0ea13cfd9ea1399856c9a9ef5b
--- /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 0000000000000000000000000000000000000000..27b88761a8bb1bd5b5a133239ef0bcfbd1b0c606
--- /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 96e72487b85d8af9575a9760cba7a3898ca5f80f..7a92bfb74c9dfeaaacb565cc608ad97b801c75c7 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 d807fe7639425a3cdd5b4a45c3024e940c2dafb8..940b82000409be4ea77be955681e941316480954 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 @@ class LLTextBox
 	{
 		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 @@ class LLTextBox
 							length;
 
 		Optional<LLUIColor>	text_color,
-							hover_color,
 							disabled_color,
 							background_color,
 							border_color;
@@ -90,15 +89,14 @@ class LLTextBox
 	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 @@ class LLTextBox
 	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 921041d17fe7592ab8af1b6560658b71ea35c607..296ccea0e462016e8f6b5f89812495630561904c 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();
@@ -4753,193 +4583,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 67c67d0f676de2813968ace41439d19929c72771..d5377511308be488dcb75076ed31bf0ddf6498aa 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 @@ class LLInlineViewSegment : public LLTextSegment
 	~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 @@ class LLTextEditor : public LLUICtrl, LLEditMenuHandler, protected LLPreeditor
 		}
 	};
 
-	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 @@ class LLTextEditor : public LLUICtrl, LLEditMenuHandler, protected LLPreeditor
 	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 @@ class LLTextEditor : public LLUICtrl, LLEditMenuHandler, protected LLPreeditor
 	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 @@ class LLTextEditor : public LLUICtrl, LLEditMenuHandler, protected LLPreeditor
 	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 @@ class LLTextEditor : public LLUICtrl, LLEditMenuHandler, protected LLPreeditor
 	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 @@ class LLTextEditor : public LLUICtrl, LLEditMenuHandler, protected LLPreeditor
 	
 	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 @@ class LLTextEditor : public LLUICtrl, LLEditMenuHandler, protected LLPreeditor
 	
 	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 @@ class LLTextEditor : public LLUICtrl, LLEditMenuHandler, protected LLPreeditor
 	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 @@ class LLTextEditor : public LLUICtrl, LLEditMenuHandler, protected LLPreeditor
 	LLUIColor		mLinkColor;
 
 	BOOL			mReadOnly;
-	BOOL			mWordWrap;
 	BOOL			mShowLineNumbers;
 
 	void			updateSegments();
+	void			updateLinkSegments();
 
 private:
 
@@ -584,7 +491,6 @@ class LLTextEditor : public LLUICtrl, LLEditMenuHandler, protected LLPreeditor
 	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 @@ class LLTextEditor : public LLUICtrl, LLEditMenuHandler, protected LLPreeditor
 	// 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 @@ class LLTextEditor : public LLUICtrl, LLEditMenuHandler, protected LLPreeditor
 
 	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 0000000000000000000000000000000000000000..3b689b93c0af66d39ea6a2a5eac8e8d5dd8f7170
--- /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 0000000000000000000000000000000000000000..6b9d565b44ca97e9aebd599576375fe9c77224a1
--- /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 0000000000000000000000000000000000000000..85f9064115146cdfcb17ac4b0e1e386ccf2f376a
--- /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 0000000000000000000000000000000000000000..f3e76dbec00dfe4b96ad75068d94c4b734a5db8e
--- /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 0000000000000000000000000000000000000000..7eec4c4a659c62fd02ff3cb074345f34e9ef3689
--- /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 0000000000000000000000000000000000000000..0711e41443823774e5866fcb3bcc4a2f5b6382c3
--- /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 0000000000000000000000000000000000000000..938375ad13e514c91869954ca8b9804483871de3
--- /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 0000000000000000000000000000000000000000..84b033036c580b4c47d78f2bd2d72be2a3551e33
--- /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 0000000000000000000000000000000000000000..05bd5d8bb30f0250ea7f5493a51af69fe18e26be
--- /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 0000000000000000000000000000000000000000..f8e6aa65d06602efd1d80f100d58cdfcbd29e36b
--- /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 0000000000000000000000000000000000000000..fcf8f4d62f2469671aed5cea8e4d677ebc2c729f
--- /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 de4f7ab0912e799cd65f637c465c8a496b0de639..3d7465f2cbf3be2a2b4213133e80218873ef6d3c 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 080d540f4a06329d9ecd46b710384a1dfaeed91b..e0322e26b90fc78b1cf3945cbbaa9007b578e263 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 fb5ab8ec5a3d8ab21fe65dae7a0a3dd470739d08..e6398dd47aba2f64e8a16ac2c0447dfacdc4fc13 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 61035499c7c72826cc4b61656fbaa7813e0ce274..b81b740bdc6260f783e53a3b3cf8f7cb19459c47 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 56c5eaa70e1700f6b17a66b616b281acdbac24ca..caa10e945274c9b3d46c252614b5a37241eacd97 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 eb73bd6d8f44c50057f9f3af87b3a216cf87dfa2..0c77d88efb91d6038f76109c51889563f5425d24 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 7a88612f1ac642f677f11b66620ec8d7256ae5d7..b1f40d9d1d90afb5b2e5688298505a5df81ac453 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 4cd09faaaff46f5f42482896c0863bd21784c1c6..e5f5e8eedb0a37b987214fe544f709ef326c5bf6 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 278fd5b9f6f5487e542de56adcc970d2fa7a0b4a..73d3a60701aa4ed91521f12af357ecd5299db90c 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 9cf3e57e222559cf64bdeb82925b6a3852b86a29..674fff40409323bcbc272fa62af402ed1c7c640b 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 1b82c2dc18d5dc9251cf1d80fa6af1cfda68d0aa..8ef6b25c509cf7d3570c456219e9d97d4eb966f0 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 6e94b087a64417f7b3aebe82de95707ee28f8ea4..b2d606ab4d123c1f54eaf994b8757949482e99ad 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 51bd619901bac0b448ffcfca236ed05e911a36f4..4ee4cb6e87eaf97276fa177bc6a41aba1ba9ffaa 100644
--- a/indra/newview/llpanelavatar.h
+++ b/indra/newview/llpanelavatar.h
@@ -203,9 +203,6 @@ class LLPanelAvatarMeProfile
 
 	void onStatusChanged();
 	void onStatusMessageChanged();
-	void onUpdateAccountTextboxClicked();
-	void onMyAccountTextboxClicked();
-	void onPartnerEditTextboxClicked();
 
 private:
 
diff --git a/indra/newview/llpanelgroupgeneral.cpp b/indra/newview/llpanelgroupgeneral.cpp
index 4b2a1a4e4832bc54f6a7d2cc63b2dfb7688441ff..f3893a104c91641fc50d823b12cb16fb82b155d5 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 59f7319b2b95dd3fd893e165494e06d832d07a0f..48c9c16780751e1392d706a8e3a7db814531c301 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 cadab71ba840c4d034b5331a1867c75a275e8baf..29320522d92653cc7606926979f0b7c3ad796a0a 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 f70cfc59ec79ebfefc9e3064742b6088c5202000..aa603c417f1e66d45751d5d016264648e1e64cbc 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 83574ff52ad5526037755c876788875d58a45dff..865646c85d0b036c35bbe9864704847273851210 100644
--- a/indra/newview/llprogressview.h
+++ b/indra/newview/llprogressview.h
@@ -74,7 +74,6 @@ class LLProgressView : public LLPanel
 	LLFrameTimer mProgressTimer;
 	LLRect mOutlineRect;
 	bool mMouseDownInActiveArea;
-	bool mURLInMessage;
 
 	static LLProgressView* sInstance;
 };
diff --git a/indra/newview/llviewermessage.cpp b/indra/newview/llviewermessage.cpp
index 18fab3ec2ecdbe4e8a07c6b679299e1bfd67b623..dfb1c330e531f73ebea446c55dc4526e3a5902eb 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 de01e7980390de1b784dd534287f28208f804419..5bb0c9a120ad957b3b5874384742f49d4c26ded1 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 9567bfbc48e1fb0d755c5d2409a7a3c5079d123d..2dfea4a5893d076a0ff521bbbe94887809c25754 100644
--- a/indra/newview/llviewertexteditor.h
+++ b/indra/newview/llviewertexteditor.h
@@ -35,7 +35,6 @@
 
 #include "lltexteditor.h"
 
-
 //
 // Classes
 //
@@ -137,9 +136,6 @@ class LLViewerTextEditor : public LLTextEditor
 
 	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 300a5db7c390960fe11c491304c9452dfebbbc46..3204c2d264208624c1c6b8fb53fa28b3901e5ec0 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,11 +77,18 @@ 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)
 {
diff --git a/indra/newview/llweb.h b/indra/newview/llweb.h
index 71cc236621a9225d163ebeaad1ad55544a068878..96a53db2ca053cd136f5c4ea82d8179df9d0f1e1 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 0000000000000000000000000000000000000000..463a9fccb6581280e85f398d16cc119aefbf1b9c
--- /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 0000000000000000000000000000000000000000..cec0aa421e4ea93f7e481185dd8270695a454db6
--- /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 0000000000000000000000000000000000000000..2503b4a2a377d4e7e1c66e90ee245300ad3ca757
--- /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 0000000000000000000000000000000000000000..7d09403b15818518d89eef0e470e3dfe3bfb1ff2
--- /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 0000000000000000000000000000000000000000..bbd63c6d8c090deef7ca1cb4f777179cb963979a
--- /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 0000000000000000000000000000000000000000..19df721b2bac631d120cfed4defc2ec5fdef7b15
--- /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 0000000000000000000000000000000000000000..0e9525fa4b6f7847c38eaa01a3c3a5c80dd0dad8
--- /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 0000000000000000000000000000000000000000..22cc035e49d6a305f56e36b36400c9a22070c761
--- /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 c0366437db4f00b09901b8a4dd44f610e9fda096..fa02cdb4b2947a2e3d1912db766294d60ce64db0 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 d90be9ea25d194a86c4e82454c708c2b0e195c3b..135dcb167b9608889fa261b4e785343d023f5d5d 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 4f23c4d26d3eeaa129df64efbccf6ed7a77c5850..9b2461db7cbc2b1119190c6f809698fd009a52a6 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 323c08ec4cdd82ca84edab10f399d2fa5c6c178a..b8152a4956bab632b927ee2e77bf1233f9f415bb 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 (&#8984;-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>