From 330840af7c363accc8e12d073ba91b871c578d27 Mon Sep 17 00:00:00 2001
From: Martin Reddy <lynx@lindenlab.com>
Date: Fri, 4 Sep 2009 12:11:27 +0000
Subject: [PATCH] Merging the SLURLs Everywhere branch (viewer-2.0.0-slurls-3)
 into Viewer 2.0 (viewer-2.0.0-3). This provides support for clickable Urls in
 text editors and textboxes, with right-click context menus, tooltips, and
 alternate link labels. This includes alert boxes, the login progress window,
 local chat and IM interfaces, etc. As well as context menus for avatars and
 groups in list widgets. Includes fixes for the following individual JIRAs:

DEV-8763 VWR-10636: Hyperlinks in alert dialogs should be selectable (clickable)!
DEV-38829 EXT-742: Remove LLLink class
DEV-35459 VWR-14679: SLURLs and teleport Links not parsed properly
DEV-19842 VWR-8773: Closing parenthesis ")" breaks urls
DEV-21577 VWR-9405: In-world SLURLs containing "(" or ")" are not treated as a hyperlink in chat
DEV-37652 SEC-435: Object Chat/IMs are untraceable (VWR-2388) Fix has left flaw
DEV-10353: URLs in chat log terminated incorrectly when newline in chat
DEV-2925: In chat history, use a teleport hyperlink as source name for object IMs
DEV-36192: Need a way to copy Avatar names and Group names
DEV-2926: Allow viewer hyperlinks to have different text than the actual url
DEV-27253: Add easy way to copy URLs from viewer chat
DEV-38274: Make About Second Life window use new Url hyperlinking features
DEV-39076: No url support in Text Editors
DEV-7476 VWR-2172: Add hyperlinks to chat console for easier access
DEV-7475: Add hyperlinks to notecards!
DEV-35375 EXT-128: HTTPS urls aren't loaded in the internal browser by click

Master JIRA issues: DEV-32819, DEV-323820, DEV-7474

Testing performed against QAR-1789

svn merge -r 131623:131889 svn+ssh://svn.lindenlab.com/svn/linden/branches/viewer/viewer-2.0.0-slurl-3
svn merge -r 131978:132515 svn+ssh://svn.lindenlab.com/svn/linden/branches/viewer/viewer-2.0.0-slurl-3
---
 .../llui_libtest/CMakeLists.txt               |   2 +
 indra/llui/CMakeLists.txt                     |  37 +-
 indra/llui/llscrolllistctrl.cpp               |  75 +-
 indra/llui/llscrolllistctrl.h                 |  13 +
 indra/llui/llstyle.cpp                        |   8 +-
 indra/llui/llstyle.h                          |   8 +-
 indra/llui/lltextbase.cpp                     | 451 ++++++++++++
 indra/llui/lltextbase.h                       | 198 +++++
 indra/llui/lltextbox.cpp                      | 498 ++++++++++---
 indra/llui/lltextbox.h                        |  49 +-
 indra/llui/lltexteditor.cpp                   | 677 +++++-------------
 indra/llui/lltexteditor.h                     | 129 +---
 indra/llui/llurlaction.cpp                    | 137 ++++
 indra/llui/llurlaction.h                      |  93 +++
 indra/llui/llurlentry.cpp                     | 546 ++++++++++++++
 indra/llui/llurlentry.h                       | 252 +++++++
 indra/llui/llurlmatch.cpp                     |  61 ++
 indra/llui/llurlmatch.h                       |  98 +++
 indra/llui/llurlregistry.cpp                  | 165 +++++
 indra/llui/llurlregistry.h                    |  87 +++
 indra/llui/tests/llurlentry_stub.cpp          |  64 ++
 indra/llui/tests/llurlentry_test.cpp          | 535 ++++++++++++++
 indra/llui/tests/llurlmatch_test.cpp          | 177 +++++
 indra/newview/llappviewer.cpp                 |  12 +-
 indra/newview/llavatarlist.cpp                |   3 +
 indra/newview/llchatmsgbox.cpp                | 372 ++--------
 indra/newview/llchatmsgbox.h                  | 129 +---
 indra/newview/llfloaterabout.cpp              |  52 +-
 indra/newview/llfloaterfriends.cpp            |   1 +
 indra/newview/llfloatergroups.cpp             |  19 +-
 indra/newview/llfloaterland.cpp               |   7 +
 indra/newview/llgrouplist.cpp                 |   2 +
 indra/newview/llimpanel.cpp                   |   1 +
 indra/newview/llnamelistctrl.cpp              |   4 +-
 indra/newview/llpanelavatar.cpp               |  19 +-
 indra/newview/llpanelavatar.h                 |   3 -
 indra/newview/llpanelgroupgeneral.cpp         |   1 +
 indra/newview/llpanelgrouproles.cpp           |   4 +
 indra/newview/llpreviewnotecard.cpp           |   3 +-
 indra/newview/llprogressview.cpp              |   6 -
 indra/newview/llprogressview.h                |   1 -
 indra/newview/llviewermessage.cpp             |   8 +-
 indra/newview/llviewertexteditor.cpp          |  45 +-
 indra/newview/llviewertexteditor.h            |   4 -
 indra/newview/llweb.cpp                       |  10 +-
 indra/newview/llweb.h                         |  20 +-
 .../skins/default/xui/en/menu_url_agent.xml   |  28 +
 .../skins/default/xui/en/menu_url_group.xml   |  28 +
 .../skins/default/xui/en/menu_url_http.xml    |  37 +
 .../default/xui/en/menu_url_objectim.xml      |  37 +
 .../skins/default/xui/en/menu_url_parcel.xml  |  21 +
 .../skins/default/xui/en/menu_url_slapp.xml   |  21 +
 .../skins/default/xui/en/menu_url_slurl.xml   |  30 +
 .../default/xui/en/menu_url_teleport.xml      |  21 +
 .../default/xui/en/panel_edit_profile.xml     |  12 +-
 .../skins/default/xui/en/panel_profile.xml    |  13 +-
 .../skins/default/xui/en/panel_progress.xml   |   4 +-
 .../newview/skins/default/xui/en/strings.xml  |  13 +
 58 files changed, 4043 insertions(+), 1308 deletions(-)
 create mode 100644 indra/llui/lltextbase.cpp
 create mode 100644 indra/llui/lltextbase.h
 create mode 100644 indra/llui/llurlaction.cpp
 create mode 100644 indra/llui/llurlaction.h
 create mode 100644 indra/llui/llurlentry.cpp
 create mode 100644 indra/llui/llurlentry.h
 create mode 100644 indra/llui/llurlmatch.cpp
 create mode 100644 indra/llui/llurlmatch.h
 create mode 100644 indra/llui/llurlregistry.cpp
 create mode 100644 indra/llui/llurlregistry.h
 create mode 100644 indra/llui/tests/llurlentry_stub.cpp
 create mode 100644 indra/llui/tests/llurlentry_test.cpp
 create mode 100644 indra/llui/tests/llurlmatch_test.cpp
 create mode 100644 indra/newview/skins/default/xui/en/menu_url_agent.xml
 create mode 100644 indra/newview/skins/default/xui/en/menu_url_group.xml
 create mode 100644 indra/newview/skins/default/xui/en/menu_url_http.xml
 create mode 100644 indra/newview/skins/default/xui/en/menu_url_objectim.xml
 create mode 100644 indra/newview/skins/default/xui/en/menu_url_parcel.xml
 create mode 100644 indra/newview/skins/default/xui/en/menu_url_slapp.xml
 create mode 100644 indra/newview/skins/default/xui/en/menu_url_slurl.xml
 create mode 100644 indra/newview/skins/default/xui/en/menu_url_teleport.xml

diff --git a/indra/integration_tests/llui_libtest/CMakeLists.txt b/indra/integration_tests/llui_libtest/CMakeLists.txt
index 1ccdb0f20b..84e3477ce6 100644
--- a/indra/integration_tests/llui_libtest/CMakeLists.txt
+++ b/indra/integration_tests/llui_libtest/CMakeLists.txt
@@ -11,6 +11,7 @@ include(LLCommon)
 include(LLImage)
 include(LLImageJ2COJ)   # ugh, needed for images
 include(LLMath)
+include(LLMessage)
 include(LLRender)
 include(LLWindow)
 include(LLUI)
@@ -67,6 +68,7 @@ endif (DARWIN)
 # Sort by high-level to low-level
 target_link_libraries(llui_libtest
     llui
+    llmessage
     ${OS_LIBRARIES}
     ${GOOGLE_PERFTOOLS_LIBRARIES}
     )
diff --git a/indra/llui/CMakeLists.txt b/indra/llui/CMakeLists.txt
index a7f899ce41..790f2d5729 100644
--- a/indra/llui/CMakeLists.txt
+++ b/indra/llui/CMakeLists.txt
@@ -47,7 +47,6 @@ set(llui_SOURCE_FILES
     llkeywords.cpp
     lllayoutstack.cpp
     lllineeditor.cpp
-    lllink.cpp
     lllistctrl.cpp
     llmenugl.cpp
     llmodaldialog.cpp
@@ -79,6 +78,7 @@ set(llui_SOURCE_FILES
     llstatview.cpp
     llstyle.cpp
     lltabcontainer.cpp
+    lltextbase.cpp
     lltextbox.cpp
     lltexteditor.cpp
     lltextparser.cpp
@@ -90,6 +90,10 @@ set(llui_SOURCE_FILES
     lluiimage.cpp
     lluistring.cpp
     llundo.cpp
+    llurlaction.cpp
+    llurlentry.cpp
+    llurlmatch.cpp
+    llurlregistry.cpp
     llviewborder.cpp
     llviewmodel.cpp
     llview.cpp
@@ -124,7 +128,6 @@ set(llui_HEADER_FILES
     lllayoutstack.h
     lllazyvalue.h
     lllineeditor.h
-    lllink.h
     lllistctrl.h
     llmenugl.h
     llmodaldialog.h
@@ -156,6 +159,7 @@ set(llui_HEADER_FILES
     llstatview.h
     llstyle.h
     lltabcontainer.h
+    lltextbase.h
     lltextbox.h
     lltexteditor.h
     lltextparser.h
@@ -169,6 +173,10 @@ set(llui_HEADER_FILES
     lluiimage.h
     lluistring.h
     llundo.h
+    llurlaction.h
+    llurlentry.h
+    llurlmatch.h
+    llurlregistry.h
     llviewborder.h
     llviewmodel.h
     llview.h
@@ -184,12 +192,21 @@ add_library (llui ${llui_SOURCE_FILES})
 # Libraries on which this library depends, needed for Linux builds
 # Sort by high-level to low-level
 target_link_libraries(llui
-    llrender
-    llwindow
-    llimage
-    llvfs       # ugh, just for LLDir
-    llxuixml
-    llxml
-    llcommon    # must be after llimage, llwindow, llrender
-    llmath
+    ${LLMESSAGE_LIBRARIES}
+    ${LLRENDER_LIBRARIES}
+    ${LLWINDOW_LIBRARIES}
+    ${LLIMAGE_LIBRARIES}
+    ${LLVFS_LIBRARIES}    # ugh, just for LLDir
+    ${LLXUIXML_LIBRARIES}
+    ${LLXML_LIBRARIES}
+    ${LLMATH_LIBRARIES}
+    ${LLCOMMON_LIBRARIES} # must be after llimage, llwindow, llrender
     )
+
+# Add tests
+include(LLAddBuildTest)
+SET(llui_TEST_SOURCE_FILES
+    llurlmatch.cpp
+    llurlentry.cpp
+    )
+LL_ADD_PROJECT_UNIT_TESTS(llui "${llui_TEST_SOURCE_FILES}")
diff --git a/indra/llui/llscrolllistctrl.cpp b/indra/llui/llscrolllistctrl.cpp
index 637642cdcd..b9a253aac8 100644
--- a/indra/llui/llscrolllistctrl.cpp
+++ b/indra/llui/llscrolllistctrl.cpp
@@ -57,6 +57,11 @@
 #include "llviewborder.h"
 #include "lltextbox.h"
 #include "llsdparam.h"
+#include "llcachename.h"
+#include "llmenugl.h"
+#include "llurlaction.h"
+
+#include <boost/bind.hpp>
 
 static LLDefaultChildRegistry::Register<LLScrollListCtrl> r("scroll_list");
 
@@ -157,6 +162,7 @@ LLScrollListCtrl::LLScrollListCtrl(const LLScrollListCtrl::Params& p)
 	mOnSortChangedCallback( NULL ),
 	mHighlightedItem(-1),
 	mBorder(NULL),
+	mPopupMenu(NULL),
 	mNumDynamicWidthColumns(0),
 	mTotalStaticColumnWidth(0),
 	mTotalColumnPadding(0),
@@ -179,7 +185,8 @@ LLScrollListCtrl::LLScrollListCtrl(const LLScrollListCtrl::Params& p)
 	mHighlightedColor(p.highlighted_color()),
 	mHoveredColor(p.hovered_color()),
 	mSearchColumn(p.search_column),
-	mColumnPadding(p.column_padding)
+	mColumnPadding(p.column_padding),
+	mContextMenuType(MENU_NONE)
 {
 	mItemListRect.setOriginAndSize(
 		mBorderThickness,
@@ -1692,6 +1699,72 @@ BOOL LLScrollListCtrl::handleMouseUp(S32 x, S32 y, MASK mask)
 	return LLUICtrl::handleMouseUp(x, y, mask);
 }
 
+// virtual
+BOOL LLScrollListCtrl::handleRightMouseDown(S32 x, S32 y, MASK mask)
+{
+	LLScrollListItem *item = hitItem(x, y);
+	if (item)
+	{
+		// check to see if we have a UUID for this row
+		std::string id = item->getValue().asString();
+		LLUUID uuid(id);
+		if (! uuid.isNull() && mContextMenuType != MENU_NONE)
+		{
+			// set up the callbacks for all of the avatar/group menu items
+			// (N.B. callbacks don't take const refs as id is local scope)
+			bool is_group = (mContextMenuType == MENU_GROUP);
+			LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar;
+			registrar.add("Url.Execute", boost::bind(&LLScrollListCtrl::showNameDetails, id, is_group));
+			registrar.add("Url.CopyLabel", boost::bind(&LLScrollListCtrl::copyNameToClipboard, id, is_group));
+			registrar.add("Url.CopyUrl", boost::bind(&LLScrollListCtrl::copySLURLToClipboard, id, is_group));
+
+			// create the context menu from the XUI file and display it
+			std::string menu_name = is_group ? "menu_url_group.xml" : "menu_url_agent.xml";
+			delete mPopupMenu;
+			mPopupMenu = LLUICtrlFactory::getInstance()->createFromFile<LLContextMenu>(
+				menu_name, LLMenuGL::sMenuContainer, LLMenuHolderGL::child_registry_t::instance());
+			if (mPopupMenu)
+			{
+				mPopupMenu->show(x, y);
+				LLMenuGL::showPopup(this, mPopupMenu, x, y);
+				return TRUE;
+			}
+		}
+	}
+	return FALSE;
+}
+
+void LLScrollListCtrl::showNameDetails(std::string id, bool is_group)
+{
+	// show the resident's profile or the group profile
+	std::string sltype = is_group ? "group" : "agent";
+	std::string slurl = "secondlife:///app/" + sltype + "/" + id + "/about";
+	LLUrlAction::clickAction(slurl);
+}
+
+void LLScrollListCtrl::copyNameToClipboard(std::string id, bool is_group)
+{
+	// copy the name of the avatar or group to the clipboard
+	std::string name;
+	if (is_group)
+	{
+		gCacheName->getGroupName(LLUUID(id), name);
+	}
+	else
+	{
+		gCacheName->getFullName(LLUUID(id), name);
+	}
+	LLUrlAction::copyURLToClipboard(name);
+}
+
+void LLScrollListCtrl::copySLURLToClipboard(std::string id, bool is_group)
+{
+	// copy a SLURL for the avatar or group to the clipboard
+	std::string sltype = is_group ? "group" : "agent";
+	std::string slurl = "secondlife:///app/" + sltype + "/" + id + "/about";
+	LLUrlAction::copyURLToClipboard(slurl);
+}
+
 BOOL LLScrollListCtrl::handleDoubleClick(S32 x, S32 y, MASK mask)
 {
 	//BOOL handled = FALSE;
diff --git a/indra/llui/llscrolllistctrl.h b/indra/llui/llscrolllistctrl.h
index 253a58ab73..7a7e5be0be 100644
--- a/indra/llui/llscrolllistctrl.h
+++ b/indra/llui/llscrolllistctrl.h
@@ -54,6 +54,7 @@
 
 class LLScrollListCell;
 class LLTextBox;
+class LLContextMenu;
 
 class LLScrollListCtrl : public LLUICtrl, public LLEditMenuHandler, 
 	public LLCtrlListInterface, public LLCtrlScrollInterface
@@ -270,10 +271,15 @@ public:
 
 	void			clearSearchString() { mSearchString.clear(); }
 
+	// support right-click context menus for avatar/group lists
+	enum ContextMenuType { MENU_NONE, MENU_AVATAR, MENU_GROUP };
+	void setContextMenu(const ContextMenuType &menu) { mContextMenuType = menu; }
+
 	// Overridden from LLView
 	/*virtual*/ void    draw();
 	/*virtual*/ BOOL	handleMouseDown(S32 x, S32 y, MASK mask);
 	/*virtual*/ BOOL	handleMouseUp(S32 x, S32 y, MASK mask);
+	/*virtual*/ BOOL	handleRightMouseDown(S32 x, S32 y, MASK mask);
 	/*virtual*/ BOOL	handleDoubleClick(S32 x, S32 y, MASK mask);
 	/*virtual*/ BOOL	handleHover(S32 x, S32 y, MASK mask);
 	/*virtual*/ BOOL	handleKeyHere(KEY key, MASK mask);
@@ -375,6 +381,10 @@ private:
 	void			commitIfChanged();
 	BOOL			setSort(S32 column, BOOL ascending);
 
+	static void		showNameDetails(std::string id, bool is_group);
+	static void		copyNameToClipboard(std::string id, bool is_group);
+	static void		copySLURLToClipboard(std::string id, bool is_group);
+
 	S32				mLineHeight;	// the max height of a single line
 	S32				mScrollLines;	// how many lines we've scrolled down
 	S32				mPageLines;		// max number of lines is it possible to see on the screen given mRect and mLineHeight
@@ -421,6 +431,7 @@ private:
 
 	S32				mHighlightedItem;
 	class LLViewBorder*	mBorder;
+	LLContextMenu	*mPopupMenu;
 
 	LLWString		mSearchString;
 	LLFrameTimer	mSearchTimer;
@@ -438,6 +449,8 @@ private:
 	BOOL			mDirty;
 	S32				mOriginalSelection;
 
+	ContextMenuType mContextMenuType;
+
 	typedef std::vector<LLScrollListColumn*> ordered_columns_t;
 	ordered_columns_t	mColumnsIndexed;
 
diff --git a/indra/llui/llstyle.cpp b/indra/llui/llstyle.cpp
index 929a809d88..c16ac08014 100644
--- a/indra/llui/llstyle.cpp
+++ b/indra/llui/llstyle.cpp
@@ -54,8 +54,6 @@ LLStyle::LLStyle(const LLStyle::Params& p)
 	mFont(p.font()),
 	mLink(p.link_href),
 	mDropShadow(p.drop_shadow),
-	mImageHeight(0),
-	mImageWidth(0),
 	mImagep(p.image())
 {}
 
@@ -100,9 +98,7 @@ void LLStyle::setImage(const LLUUID& src)
 	mImagep = LLUI::getUIImageByID(src);
 }
 
-
-void LLStyle::setImageSize(S32 width, S32 height)
+void LLStyle::setImage(const std::string& name)
 {
-    mImageWidth = width;
-    mImageHeight = height;
+	mImagep = LLUI::getUIImage(name);
 }
diff --git a/indra/llui/llstyle.h b/indra/llui/llstyle.h
index dcf274a651..5e8883afd7 100644
--- a/indra/llui/llstyle.h
+++ b/indra/llui/llstyle.h
@@ -69,9 +69,9 @@ public:
 
 	LLUIImagePtr getImage() const;
 	void setImage(const LLUUID& src);
+	void setImage(const std::string& name);
 
-	BOOL isImage() const { return ((mImageWidth != 0) && (mImageHeight != 0)); }
-	void setImageSize(S32 width, S32 height);
+	BOOL isImage() const { return mImagep.notNull(); }
 
 	// inlined here to make it easier to compare to member data below. -MG
 	bool operator==(const LLStyle &rhs) const
@@ -82,8 +82,6 @@ public:
 			&& mFont == rhs.mFont
 			&& mLink == rhs.mLink
 			&& mImagep == rhs.mImagep
-			&& mImageHeight == rhs.mImageHeight
-			&& mImageWidth == rhs.mImageWidth
 			&& mItalic == rhs.mItalic
 			&& mBold == rhs.mBold
 			&& mUnderline == rhs.mUnderline
@@ -97,8 +95,6 @@ public:
 	BOOL        mBold;
 	BOOL        mUnderline;
 	BOOL		mDropShadow;
-	S32         mImageWidth;
-	S32         mImageHeight;
 
 protected:
 	~LLStyle() { }
diff --git a/indra/llui/lltextbase.cpp b/indra/llui/lltextbase.cpp
new file mode 100644
index 0000000000..038ea2188f
--- /dev/null
+++ b/indra/llui/lltextbase.cpp
@@ -0,0 +1,451 @@
+/** 
+ * @file lltextbase.cpp
+ * @author Martin Reddy
+ * @brief The base class of text box/editor, providing Url handling support
+ *
+ * $LicenseInfo:firstyear=2009&license=viewergpl$
+ * 
+ * Copyright (c) 2009, Linden Research, Inc.
+ * 
+ * Second Life Viewer Source Code
+ * The source code in this file ("Source Code") is provided by Linden Lab
+ * to you under the terms of the GNU General Public License, version 2.0
+ * ("GPL"), unless you have obtained a separate licensing agreement
+ * ("Other License"), formally executed by you and Linden Lab.  Terms of
+ * the GPL can be found in doc/GPL-license.txt in this distribution, or
+ * online at http://secondlifegrid.net/programs/open_source/licensing/gplv2
+ * 
+ * There are special exceptions to the terms and conditions of the GPL as
+ * it is applied to this Source Code. View the full text of the exception
+ * in the file doc/FLOSS-exception.txt in this software distribution, or
+ * online at
+ * http://secondlifegrid.net/programs/open_source/licensing/flossexception
+ * 
+ * By copying, modifying or distributing this software, you acknowledge
+ * that you have read and understood your obligations described above,
+ * and agree to abide by those obligations.
+ * 
+ * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO
+ * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY,
+ * COMPLETENESS OR PERFORMANCE.
+ * $/LicenseInfo$
+ */
+
+#include "linden_common.h"
+
+#include "lltextbase.h"
+#include "llstl.h"
+#include "llview.h"
+#include "llwindow.h"
+#include "llmenugl.h"
+#include "lluictrl.h"
+#include "llurlaction.h"
+#include "llurlregistry.h"
+
+#include <boost/bind.hpp>
+
+// global state for all text fields
+LLUIColor LLTextBase::mLinkColor = LLColor4::blue;
+
+bool LLTextBase::compare_segment_end::operator()(const LLTextSegmentPtr& a, const LLTextSegmentPtr& b) const
+{
+	return a->getEnd() < b->getEnd();
+}
+
+//
+// LLTextSegment
+//
+
+LLTextSegment::~LLTextSegment()
+{}
+
+S32	LLTextSegment::getWidth(S32 first_char, S32 num_chars) const { return 0; }
+S32	LLTextSegment::getOffset(S32 segment_local_x_coord, S32 start_offset, S32 num_chars, bool round) const { return 0; }
+S32	LLTextSegment::getNumChars(S32 num_pixels, S32 segment_offset, S32 line_offset, S32 max_chars) const { return 0; }
+void LLTextSegment::updateLayout(const LLTextBase& editor) {}
+F32	LLTextSegment::draw(S32 start, S32 end, S32 selection_start, S32 selection_end, const LLRect& draw_rect) { return draw_rect.mLeft; }
+S32	LLTextSegment::getMaxHeight() const { return 0; }
+bool LLTextSegment::canEdit() const { return false; }
+void LLTextSegment::unlinkFromDocument(LLTextBase*) {}
+void LLTextSegment::linkToDocument(LLTextBase*) {}
+void LLTextSegment::setHasMouseHover(bool hover) {}
+const LLColor4& LLTextSegment::getColor() const { return LLColor4::white; }
+void LLTextSegment::setColor(const LLColor4 &color) {}
+const LLStyleSP LLTextSegment::getStyle() const {static LLStyleSP sp(new LLStyle()); return sp; }
+void LLTextSegment::setStyle(const LLStyleSP &style) {}
+void LLTextSegment::setToken( LLKeywordToken* token ) {}
+LLKeywordToken*	LLTextSegment::getToken() const { return NULL; }
+BOOL LLTextSegment::getToolTip( std::string& msg ) const { return FALSE; }
+void LLTextSegment::setToolTip( const std::string &msg ) {}
+void LLTextSegment::dump() const {}
+
+
+//
+// LLNormalTextSegment
+//
+
+LLNormalTextSegment::LLNormalTextSegment( const LLStyleSP& style, S32 start, S32 end, LLTextBase& editor ) 
+:	LLTextSegment(start, end),
+	mStyle( style ),
+	mToken(NULL),
+	mHasMouseHover(false),
+	mEditor(editor)
+{
+	mMaxHeight = llceil(mStyle->getFont()->getLineHeight());
+}
+
+LLNormalTextSegment::LLNormalTextSegment( const LLColor4& color, S32 start, S32 end, LLTextBase& editor, BOOL is_visible) 
+:	LLTextSegment(start, end),
+	mToken(NULL),
+	mHasMouseHover(false),
+	mEditor(editor)
+{
+	mStyle = new LLStyle(LLStyle::Params().visible(is_visible).color(color));
+
+	mMaxHeight = llceil(mStyle->getFont()->getLineHeight());
+}
+
+F32 LLNormalTextSegment::draw(S32 start, S32 end, S32 selection_start, S32 selection_end, const LLRect& draw_rect)
+{
+	if( end - start > 0 )
+	{
+		if ( mStyle->isImage() && (start >= 0) && (end <= mEnd - mStart))
+		{
+			LLUIImagePtr image = mStyle->getImage();
+			S32 style_image_height = image->getHeight();
+			S32 style_image_width = image->getWidth();
+			image->draw(draw_rect.mLeft, draw_rect.mTop-style_image_height, 
+				style_image_width, style_image_height);
+		}
+
+		return drawClippedSegment( getStart() + start, getStart() + end, selection_start, selection_end, draw_rect.mLeft, draw_rect.mBottom);
+	}
+	return draw_rect.mLeft;
+}
+
+// Draws a single text segment, reversing the color for selection if needed.
+F32 LLNormalTextSegment::drawClippedSegment(S32 seg_start, S32 seg_end, S32 selection_start, S32 selection_end, F32 x, F32 y)
+{
+	const LLWString &text = mEditor.getWText();
+
+	F32 right_x = x;
+	if (!mStyle->isVisible())
+	{
+		return right_x;
+	}
+
+	const LLFontGL* font = mStyle->getFont();
+
+	LLColor4 color = mStyle->getColor();
+
+	font = mStyle->getFont();
+
+  	if( selection_start > seg_start )
+	{
+		// Draw normally
+		S32 start = seg_start;
+		S32 end = llmin( selection_start, seg_end );
+		S32 length =  end - start;
+		font->render(text, start, x, y, color, LLFontGL::LEFT, LLFontGL::BOTTOM, 0, LLFontGL::NO_SHADOW, length, S32_MAX, &right_x, mEditor.allowsEmbeddedItems());
+	}
+	x = right_x;
+	
+	if( (selection_start < seg_end) && (selection_end > seg_start) )
+	{
+		// Draw reversed
+		S32 start = llmax( selection_start, seg_start );
+		S32 end = llmin( selection_end, seg_end );
+		S32 length = end - start;
+
+		font->render(text, start, x, y,
+					 LLColor4( 1.f - color.mV[0], 1.f - color.mV[1], 1.f - color.mV[2], 1.f ),
+					 LLFontGL::LEFT, LLFontGL::BOTTOM, 0, LLFontGL::NO_SHADOW, length, S32_MAX, &right_x, mEditor.allowsEmbeddedItems());
+	}
+	x = right_x;
+	if( selection_end < seg_end )
+	{
+		// Draw normally
+		S32 start = llmax( selection_end, seg_start );
+		S32 end = seg_end;
+		S32 length = end - start;
+		font->render(text, start, x, y, color, LLFontGL::LEFT, LLFontGL::BOTTOM, 0, LLFontGL::NO_SHADOW, length, S32_MAX, &right_x, mEditor.allowsEmbeddedItems());
+	}
+	return right_x;
+}
+
+S32	LLNormalTextSegment::getMaxHeight() const	
+{ 
+	return mMaxHeight; 
+}
+
+BOOL LLNormalTextSegment::getToolTip(std::string& msg) const
+{
+	// do we have a tooltip for a loaded keyword (for script editor)?
+	if (mToken && !mToken->getToolTip().empty())
+	{
+		const LLWString& wmsg = mToken->getToolTip();
+		msg = wstring_to_utf8str(wmsg);
+		return TRUE;
+	}
+	// or do we have an explicitly set tooltip (e.g., for Urls)
+	if (! mTooltip.empty())
+	{
+		msg = mTooltip;
+		return TRUE;
+	}
+	return FALSE;
+}
+
+void LLNormalTextSegment::setToolTip(const std::string& tooltip)
+{
+	// we cannot replace a keyword tooltip that's loaded from a file
+	if (mToken)
+	{
+		llwarns << "LLTextSegment::setToolTip: cannot replace keyword tooltip." << llendl;
+		return;
+	}
+	mTooltip = tooltip;
+}
+
+S32	LLNormalTextSegment::getWidth(S32 first_char, S32 num_chars) const
+{
+	LLWString text = mEditor.getWText();
+	return mStyle->getFont()->getWidth(text.c_str(), mStart + first_char, num_chars);
+}
+
+S32	LLNormalTextSegment::getOffset(S32 segment_local_x_coord, S32 start_offset, S32 num_chars, bool round) const
+{
+	LLWString text = mEditor.getWText();
+	return mStyle->getFont()->charFromPixelOffset(text.c_str(), mStart + start_offset,
+											   (F32)segment_local_x_coord,
+											   F32_MAX,
+											   num_chars,
+											   round);
+}
+
+S32	LLNormalTextSegment::getNumChars(S32 num_pixels, S32 segment_offset, S32 line_offset, S32 max_chars) const
+{
+	LLWString text = mEditor.getWText();
+	S32 num_chars = mStyle->getFont()->maxDrawableChars(text.c_str() + segment_offset + mStart, 
+												(F32)num_pixels,
+												max_chars, 
+												mEditor.getWordWrap());
+
+	if (num_chars == 0 
+		&& line_offset == 0 
+		&& max_chars > 0)
+	{
+		// If at the beginning of a line, and a single character won't fit, draw it anyway
+		num_chars = 1;
+	}
+	if (mStart + segment_offset + num_chars == mEditor.getLength())
+	{
+		// include terminating NULL
+		num_chars++;
+	}
+	return num_chars;
+}
+
+void LLNormalTextSegment::dump() const
+{
+	llinfos << "Segment [" << 
+//			mColor.mV[VX] << ", " <<
+//			mColor.mV[VY] << ", " <<
+//			mColor.mV[VZ] << "]\t[" <<
+		mStart << ", " <<
+		getEnd() << "]" <<
+		llendl;
+}
+
+//////////////////////////////////////////////////////////////////////////
+//
+// LLTextBase
+//
+
+LLTextBase::LLTextBase(const LLUICtrl::Params &p) :
+	mHoverSegment(NULL),
+	mDefaultFont(p.font),
+	mParseHTML(TRUE),
+	mPopupMenu(NULL)
+{
+}
+
+LLTextBase::~LLTextBase()
+{
+	clearSegments();
+}
+
+void LLTextBase::clearSegments()
+{
+	setHoverSegment(NULL);
+	mSegments.clear();
+}
+
+void LLTextBase::setHoverSegment(LLTextSegmentPtr segment)
+{
+	if (mHoverSegment)
+	{
+		mHoverSegment->setHasMouseHover(false);
+	}
+	if (segment)
+	{
+		segment->setHasMouseHover(true);
+	}
+	mHoverSegment = segment;
+}
+
+void LLTextBase::getSegmentAndOffset( S32 startpos, segment_set_t::const_iterator* seg_iter, S32* offsetp ) const
+{
+	*seg_iter = getSegIterContaining(startpos);
+	if (*seg_iter == mSegments.end())
+	{
+		*offsetp = 0;
+	}
+	else
+	{
+		*offsetp = startpos - (**seg_iter)->getStart();
+	}
+}
+
+void LLTextBase::getSegmentAndOffset( S32 startpos, segment_set_t::iterator* seg_iter, S32* offsetp )
+{
+	*seg_iter = getSegIterContaining(startpos);
+	if (*seg_iter == mSegments.end())
+	{
+		*offsetp = 0;
+	}
+	else
+	{
+		*offsetp = startpos - (**seg_iter)->getStart();
+	}
+}
+
+LLTextBase::segment_set_t::iterator LLTextBase::getSegIterContaining(S32 index)
+{
+	segment_set_t::iterator it = mSegments.upper_bound(new LLIndexSegment(index));
+	return it;
+}
+
+LLTextBase::segment_set_t::const_iterator LLTextBase::getSegIterContaining(S32 index) const
+{
+	LLTextBase::segment_set_t::const_iterator it =  mSegments.upper_bound(new LLIndexSegment(index));
+	return it;
+}
+
+// Finds the text segment (if any) at the give local screen position
+LLTextSegmentPtr LLTextBase::getSegmentAtLocalPos( S32 x, S32 y )
+{
+	// Find the cursor position at the requested local screen position
+	S32 offset = getDocIndexFromLocalCoord( x, y, FALSE );
+	segment_set_t::iterator seg_iter = getSegIterContaining(offset);
+	if (seg_iter != mSegments.end())
+	{
+		return *seg_iter;
+	}
+	else
+	{
+		return LLTextSegmentPtr();
+	}
+}
+
+BOOL LLTextBase::handleHoverOverUrl(S32 x, S32 y)
+{
+	setHoverSegment(NULL);
+
+	// Check to see if we're over an HTML-style link
+	LLTextSegment* cur_segment = getSegmentAtLocalPos( x, y );
+	if (cur_segment)
+	{
+		setHoverSegment(cur_segment);
+
+		LLStyleSP style =  cur_segment->getStyle();
+		if (style && style->isLink())
+		{
+			return TRUE;
+		}
+	}
+
+	return FALSE;
+}
+
+BOOL LLTextBase::handleMouseUpOverUrl(S32 x, S32 y)
+{
+	if (mParseHTML && mHoverSegment)
+	{
+		LLStyleSP style = mHoverSegment->getStyle();
+		if (style && style->isLink())
+		{
+			LLUrlAction::clickAction(style->getLinkHREF());
+			return TRUE;
+		}
+	}
+
+	return FALSE;
+}
+
+BOOL LLTextBase::handleRightMouseDownOverUrl(LLView *view, S32 x, S32 y)
+{
+	// pop up a context menu for any Url under the cursor
+	const LLTextSegment* cur_segment = getSegmentAtLocalPos(x, y);
+	if (cur_segment && cur_segment->getStyle() && cur_segment->getStyle()->isLink())
+	{
+		delete mPopupMenu;
+		mPopupMenu = createUrlContextMenu(cur_segment->getStyle()->getLinkHREF());
+		if (mPopupMenu)
+		{
+			mPopupMenu->show(x, y);
+			LLMenuGL::showPopup(view, mPopupMenu, x, y);
+			return TRUE;
+		}
+	}
+
+	return FALSE;
+}
+
+BOOL LLTextBase::handleToolTipForUrl(LLView *view, S32 x, S32 y, std::string& msg, LLRect* sticky_rect_screen)
+{
+	const LLTextSegment* cur_segment = getSegmentAtLocalPos( x, y );
+	if (cur_segment && cur_segment->getToolTip( msg ) && view)
+	{
+		// Use a slop area around the cursor
+		const S32 SLOP = 8;
+		// Convert rect local to screen coordinates
+		view->localPointToScreen(x - SLOP, y - SLOP, &(sticky_rect_screen->mLeft),
+								 &(sticky_rect_screen->mBottom));
+		sticky_rect_screen->mRight = sticky_rect_screen->mLeft + 2 * SLOP;
+		sticky_rect_screen->mTop = sticky_rect_screen->mBottom + 2 * SLOP;
+	}
+	return TRUE;
+}
+
+LLContextMenu *LLTextBase::createUrlContextMenu(const std::string &in_url)
+{
+	// work out the XUI menu file to use for this url
+	LLUrlMatch match;
+	std::string url = in_url;
+	if (! LLUrlRegistry::instance().findUrl(url, match))
+	{
+		return NULL;
+	}
+	
+	std::string xui_file = match.getMenuName();
+	if (xui_file.empty())
+	{
+		return NULL;
+	}
+
+	// set up the callbacks for all of the potential menu items, N.B. we
+	// don't use const ref strings in callbacks in case url goes out of scope
+	LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar;
+	registrar.add("Url.Open", boost::bind(&LLUrlAction::openURL, url));
+	registrar.add("Url.OpenInternal", boost::bind(&LLUrlAction::openURLInternal, url));
+	registrar.add("Url.OpenExternal", boost::bind(&LLUrlAction::openURLExternal, url));
+	registrar.add("Url.Execute", boost::bind(&LLUrlAction::executeSLURL, url));
+	registrar.add("Url.Teleport", boost::bind(&LLUrlAction::teleportToLocation, url));
+	registrar.add("Url.CopyLabel", boost::bind(&LLUrlAction::copyLabelToClipboard, url));
+	registrar.add("Url.CopyUrl", boost::bind(&LLUrlAction::copyURLToClipboard, url));
+
+	// create and return the context menu from the XUI file
+	return LLUICtrlFactory::getInstance()->createFromFile<LLContextMenu>(xui_file, LLMenuGL::sMenuContainer,
+																		 LLMenuHolderGL::child_registry_t::instance());	
+}
diff --git a/indra/llui/lltextbase.h b/indra/llui/lltextbase.h
new file mode 100644
index 0000000000..27b88761a8
--- /dev/null
+++ b/indra/llui/lltextbase.h
@@ -0,0 +1,198 @@
+/** 
+ * @file lltextbase.h
+ * @author Martin Reddy
+ * @brief The base class of text box/editor, providing Url handling support
+ *
+ * $LicenseInfo:firstyear=2009&license=viewergpl$
+ * 
+ * Copyright (c) 2009, Linden Research, Inc.
+ * 
+ * Second Life Viewer Source Code
+ * The source code in this file ("Source Code") is provided by Linden Lab
+ * to you under the terms of the GNU General Public License, version 2.0
+ * ("GPL"), unless you have obtained a separate licensing agreement
+ * ("Other License"), formally executed by you and Linden Lab.  Terms of
+ * the GPL can be found in doc/GPL-license.txt in this distribution, or
+ * online at http://secondlifegrid.net/programs/open_source/licensing/gplv2
+ * 
+ * There are special exceptions to the terms and conditions of the GPL as
+ * it is applied to this Source Code. View the full text of the exception
+ * in the file doc/FLOSS-exception.txt in this software distribution, or
+ * online at
+ * http://secondlifegrid.net/programs/open_source/licensing/flossexception
+ * 
+ * By copying, modifying or distributing this software, you acknowledge
+ * that you have read and understood your obligations described above,
+ * and agree to abide by those obligations.
+ * 
+ * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO
+ * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY,
+ * COMPLETENESS OR PERFORMANCE.
+ * $/LicenseInfo$
+ */
+
+#ifndef LL_LLTEXTBASE_H
+#define LL_LLTEXTBASE_H
+
+#include "v4color.h"
+#include "llstyle.h"
+#include "llkeywords.h"
+#include "lluictrl.h"
+
+#include <string>
+#include <set>
+
+class LLContextMenu;
+class LLTextSegment;
+
+typedef LLPointer<LLTextSegment> LLTextSegmentPtr;
+
+///
+/// The LLTextBase class provides a base class for all text fields, such
+/// as LLTextEditor and LLTextBox. It implements shared functionality
+/// such as Url highlighting and opening.
+///
+class LLTextBase
+{
+public:
+	LLTextBase(const LLUICtrl::Params &p);
+	virtual ~LLTextBase();
+
+	/// specify the color to display Url hyperlinks in the text
+	static void setLinkColor(LLColor4 color) { mLinkColor = color; }
+
+	/// enable/disable the automatic hyperlinking of Urls in the text
+	void        setParseHTML(BOOL parsing) { mParseHTML=parsing; }
+
+	// public text editing virtual methods
+	virtual LLWString getWText() const = 0;
+	virtual BOOL      allowsEmbeddedItems() const { return FALSE; }
+	virtual BOOL      getWordWrap() { return mWordWrap; }
+	virtual S32       getLength() const = 0;
+
+protected:
+	struct compare_segment_end
+	{
+		bool operator()(const LLTextSegmentPtr& a, const LLTextSegmentPtr& b) const;
+	};
+	typedef std::multiset<LLTextSegmentPtr, compare_segment_end> segment_set_t;
+
+	// routines to manage segments 
+	void                getSegmentAndOffset( S32 startpos, segment_set_t::const_iterator* seg_iter, S32* offsetp ) const;
+	void                getSegmentAndOffset( S32 startpos, segment_set_t::iterator* seg_iter, S32* offsetp );
+	LLTextSegmentPtr    getSegmentAtLocalPos( S32 x, S32 y );
+	segment_set_t::iterator			getSegIterContaining(S32 index);
+	segment_set_t::const_iterator	getSegIterContaining(S32 index) const;
+	void                clearSegments();
+	void                setHoverSegment(LLTextSegmentPtr segment);
+
+	// event handling for Urls within the text field
+	BOOL                handleHoverOverUrl(S32 x, S32 y);
+	BOOL                handleMouseUpOverUrl(S32 x, S32 y);
+	BOOL                handleRightMouseDownOverUrl(LLView *view, S32 x, S32 y);
+	BOOL                handleToolTipForUrl(LLView *view, S32 x, S32 y, std::string& msg, LLRect* sticky_rect_screen);
+
+	// pure virtuals that have to be implemented by any subclasses
+	virtual S32         getLineCount() const = 0;
+	virtual S32         getLineStart( S32 line ) const = 0;
+	virtual S32         getDocIndexFromLocalCoord( S32 local_x, S32 local_y, BOOL round ) const = 0;
+
+	// protected member variables
+	static LLUIColor    mLinkColor;
+	const LLFontGL      *mDefaultFont;
+	segment_set_t       mSegments;
+	LLTextSegmentPtr    mHoverSegment;	
+	BOOL                mParseHTML;
+	BOOL                mWordWrap;
+
+private:
+	// create a popup context menu for the given Url
+	static LLContextMenu *createUrlContextMenu(const std::string &url);
+
+	LLContextMenu        *mPopupMenu;
+};
+
+///
+/// A text segment is used to specify a subsection of a text string
+/// that should be formatted differently, such as a hyperlink. It
+/// includes a start/end offset from the start of the string, a
+/// style to render with, an optional tooltip, etc.
+///
+class LLTextSegment : public LLRefCount
+{
+public:
+	LLTextSegment(S32 start, S32 end) : mStart(start), mEnd(end){};
+	virtual ~LLTextSegment();
+
+	virtual S32					getWidth(S32 first_char, S32 num_chars) const;
+	virtual S32					getOffset(S32 segment_local_x_coord, S32 start_offset, S32 num_chars, bool round) const;
+	virtual S32					getNumChars(S32 num_pixels, S32 segment_offset, S32 line_offset, S32 max_chars) const;
+	virtual void				updateLayout(const class LLTextBase& editor);
+	virtual F32					draw(S32 start, S32 end, S32 selection_start, S32 selection_end, const LLRect& draw_rect);
+	virtual S32					getMaxHeight() const;
+	virtual bool				canEdit() const;
+	virtual void				unlinkFromDocument(class LLTextBase* editor);
+	virtual void				linkToDocument(class LLTextBase* editor);
+
+	virtual void				setHasMouseHover(bool hover);
+	virtual const LLColor4&		getColor() const;
+	virtual void 				setColor(const LLColor4 &color);
+	virtual const LLStyleSP		getStyle() const;
+	virtual void 				setStyle(const LLStyleSP &style);
+	virtual void				setToken( LLKeywordToken* token );
+	virtual LLKeywordToken*		getToken() const;
+	virtual BOOL				getToolTip( std::string& msg ) const;
+	virtual void				setToolTip(const std::string& tooltip);
+	virtual void				dump() const;
+
+	S32							getStart() const 					{ return mStart; }
+	void						setStart(S32 start)					{ mStart = start; }
+	S32							getEnd() const						{ return mEnd; }
+	void						setEnd( S32 end )					{ mEnd = end; }
+
+protected:
+	S32				mStart;
+	S32				mEnd;
+};
+
+class LLNormalTextSegment : public LLTextSegment
+{
+public:
+	LLNormalTextSegment( const LLStyleSP& style, S32 start, S32 end, LLTextBase& editor );
+	LLNormalTextSegment( const LLColor4& color, S32 start, S32 end, LLTextBase& editor, BOOL is_visible = TRUE);
+
+	/*virtual*/ S32					getWidth(S32 first_char, S32 num_chars) const;
+	/*virtual*/ S32					getOffset(S32 segment_local_x_coord, S32 start_offset, S32 num_chars, bool round) const;
+	/*virtual*/ S32					getNumChars(S32 num_pixels, S32 segment_offset, S32 line_offset, S32 max_chars) const;
+	/*virtual*/ F32					draw(S32 start, S32 end, S32 selection_start, S32 selection_end, const LLRect& draw_rect);
+	/*virtual*/ S32					getMaxHeight() const;
+	/*virtual*/ bool				canEdit() const { return true; }
+	/*virtual*/ void				setHasMouseHover(bool hover)		{ mHasMouseHover = hover; }
+	/*virtual*/ const LLColor4&		getColor() const					{ return mStyle->getColor(); }
+	/*virtual*/ void 				setColor(const LLColor4 &color)		{ mStyle->setColor(color); }
+	/*virtual*/ const LLStyleSP		getStyle() const					{ return mStyle; }
+	/*virtual*/ void 				setStyle(const LLStyleSP &style)	{ mStyle = style; }
+	/*virtual*/ void				setToken( LLKeywordToken* token )	{ mToken = token; }
+	/*virtual*/ LLKeywordToken*		getToken() const					{ return mToken; }
+	/*virtual*/ BOOL				getToolTip( std::string& msg ) const;
+	/*virtual*/ void				setToolTip(const std::string& tooltip);
+	/*virtual*/ void				dump() const;
+
+protected:
+	F32				drawClippedSegment(S32 seg_start, S32 seg_end, S32 selection_start, S32 selection_end, F32 x, F32 y);
+
+	class LLTextBase&	mEditor;
+	LLStyleSP		mStyle;
+	S32				mMaxHeight;
+	LLKeywordToken* mToken;
+	bool			mHasMouseHover;
+	std::string     mTooltip;
+};
+
+class LLIndexSegment : public LLTextSegment
+{
+public:
+	LLIndexSegment(S32 pos) : LLTextSegment(pos, pos) {}
+};
+
+#endif
diff --git a/indra/llui/lltextbox.cpp b/indra/llui/lltextbox.cpp
index 96e72487b8..7a92bfb74c 100644
--- a/indra/llui/lltextbox.cpp
+++ b/indra/llui/lltextbox.cpp
@@ -32,30 +32,24 @@
 
 #include "linden_common.h"
 #include "lltextbox.h"
-#include "lllink.h"
 #include "lluictrlfactory.h"
 #include "llfocusmgr.h"
 #include "llwindow.h"
+#include "llurlregistry.h"
+#include "llstyle.h"
 
 static LLDefaultChildRegistry::Register<LLTextBox> r("text");
 
-//*NOTE
-// LLLink is not used in code for now, therefor Visual Studio doesn't build it.
-// "link" is registered here to force Visual Studio to build LLLink class.
-static LLDefaultChildRegistry::Register<LLLink>	register_link("link");
-
 LLTextBox::Params::Params()
 :	text_color("text_color"),
 	length("length"),
 	type("type"),
-	highlight_on_hover("hover", false),
 	border_visible("border_visible", false),
 	border_drop_shadow_visible("border_drop_shadow_visible", false),
 	bg_visible("bg_visible", false),
 	use_ellipses("use_ellipses"),
 	word_wrap("word_wrap", false),
 	drop_shadow_visible("drop_shadow_visible"),
-	hover_color("hover_color"),
 	disabled_color("disabled_color"),
 	background_color("background_color"),
 	border_color("border_color"),
@@ -68,9 +62,7 @@ LLTextBox::Params::Params()
 
 LLTextBox::LLTextBox(const LLTextBox::Params& p)
 :	LLUICtrl(p),
-    mFontGL(p.font),
-	mHoverActive( p.highlight_on_hover ),
-	mHasHover( FALSE ),
+	LLTextBase(p),
 	mBackgroundVisible( p.bg_visible ),
 	mBorderVisible( p.border_visible ),
 	mShadowType( p.font_shadow ),
@@ -84,12 +76,11 @@ LLTextBox::LLTextBox(const LLTextBox::Params& p)
 	mDisabledColor(p.disabled_color()),
 	mBackgroundColor(p.background_color()),
 	mBorderColor(p.border_color()),
-	mHoverColor(p.hover_color()),
 	mHAlign(p.font_halign),
 	mLineSpacing(p.line_spacing),
-	mWordWrap( p.word_wrap ),
 	mDidWordWrap(FALSE)
 {
+	mWordWrap = p.word_wrap;
 	setText( p.text() );
 }
 
@@ -97,9 +88,9 @@ BOOL LLTextBox::handleMouseDown(S32 x, S32 y, MASK mask)
 {
 	BOOL	handled = FALSE;
 
-	// HACK: Only do this if there actually is a click callback, so that
+	// HACK: Only do this if there actually is something to click, so that
 	// overly large text boxes in the older UI won't start eating clicks.
-	if (mClickedCallback)
+	if (isClickable())
 	{
 		handled = TRUE;
 
@@ -121,10 +112,9 @@ BOOL LLTextBox::handleMouseUp(S32 x, S32 y, MASK mask)
 
 	// We only handle the click if the click both started and ended within us
 
-	// HACK: Only do this if there actually is a click callback, so that
+	// HACK: Only do this if there actually is something to click, so that
 	// overly large text boxes in the older UI won't start eating clicks.
-	if (mClickedCallback
-		&& hasMouseCapture())
+	if (isClickable() && hasMouseCapture())
 	{
 		handled = TRUE;
 
@@ -136,27 +126,44 @@ BOOL LLTextBox::handleMouseUp(S32 x, S32 y, MASK mask)
 			make_ui_sound("UISndClickRelease");
 		}
 
-		// DO THIS AT THE VERY END to allow the button to be destroyed as a result of being clicked.
-		// If mouseup in the widget, it's been clicked
-		if (mClickedCallback)
+		// handle clicks on Urls in the textbox first
+		if (! handleMouseUpOverUrl(x, y))
 		{
-			mClickedCallback();
+			// DO THIS AT THE VERY END to allow the button to be destroyed
+			// as a result of being clicked.  If mouseup in the widget,
+			// it's been clicked
+			if (mClickedCallback && ! handled)
+			{
+				mClickedCallback();
+			}
 		}
 	}
 
 	return handled;
 }
 
+BOOL LLTextBox::handleRightMouseDown(S32 x, S32 y, MASK mask)
+{
+	// pop up a context menu for any Url under the cursor
+	return handleRightMouseDownOverUrl(this, x, y);
+}
+
 BOOL LLTextBox::handleHover(S32 x, S32 y, MASK mask)
 {
-	BOOL handled = LLView::handleHover(x,y,mask);
-	if(mHoverActive)
+	// Check to see if we're over an HTML-style link
+	if (handleHoverOverUrl(x, y))
 	{
-		mHasHover = TRUE; // This should be set every frame during a hover.
-		getWindow()->setCursor(UI_CURSOR_ARROW);
+		lldebugst(LLERR_USER_INPUT) << "hover handled by " << getName() << llendl;		
+		getWindow()->setCursor(UI_CURSOR_HAND);
+		return TRUE;
 	}
 
-	return (handled || mHasHover);
+	return LLView::handleHover(x,y,mask);
+}
+
+BOOL LLTextBox::handleToolTip(S32 x, S32 y, std::string& msg, LLRect* sticky_rect_screen)
+{
+	return handleToolTipForUrl(this, x, y, msg, sticky_rect_screen);
 }
 
 void LLTextBox::setText(const LLStringExplicit& text)
@@ -168,7 +175,7 @@ void LLTextBox::setText(const LLStringExplicit& text)
 	else
 	{
 		mText.assign(text);
-		setLineLengths();
+		updateDisplayTextAndSegments();
 	}
 }
 
@@ -177,11 +184,11 @@ void LLTextBox::setLineLengths()
 	mLineLengthList.clear();
 	
 	std::string::size_type  cur = 0;
-	std::string::size_type  len = mText.getWString().size();
+	std::string::size_type  len = mDisplayText.size();
 
 	while (cur < len) 
 	{
-		std::string::size_type end = mText.getWString().find('\n', cur);
+		std::string::size_type end = mDisplayText.find('\n', cur);
 		std::string::size_type runLen;
 		
 		if (end == std::string::npos)
@@ -199,20 +206,12 @@ void LLTextBox::setLineLengths()
 	}
 }
 
-void LLTextBox::setWrappedText(const LLStringExplicit& in_text, F32 max_width)
+LLWString LLTextBox::wrapText(const LLWString &wtext, S32 &hoffset, S32 &line_num, F32 max_width)
 {
-	if (max_width < 0.0f)
-	{
-		max_width = (F32)getRect().getWidth();
-	}
-
-	LLWString wtext = utf8str_to_wstring(in_text);
 	LLWString final_wtext;
 
-	LLWString::size_type  cur = 0;;
-	LLWString::size_type  len = wtext.size();
-	F32 line_height =  mFontGL->getLineHeight();
-	S32 line_num = 1;
+	LLWString::size_type cur = 0;
+	LLWString::size_type len = wtext.size();
 	while (cur < len)
 	{
 		LLWString::size_type end = wtext.find('\n', cur);
@@ -221,41 +220,121 @@ void LLTextBox::setWrappedText(const LLStringExplicit& in_text, F32 max_width)
 			end = len;
 		}
 		
+		bool charsRemaining = true;
 		LLWString::size_type runLen = end - cur;
 		if (runLen > 0)
 		{
+			// work out how many chars can fit onto the current line
 			LLWString run(wtext, cur, runLen);
 			LLWString::size_type useLen =
-				mFontGL->maxDrawableChars(run.c_str(), max_width, runLen, TRUE);
+				mDefaultFont->maxDrawableChars(run.c_str(), max_width-hoffset, runLen, TRUE);
+			charsRemaining = (cur + useLen < len);
 
+			// try to break lines on word boundaries
+			if (useLen < run.size())
+			{
+				LLWString::size_type prev_use_len = useLen;
+				while (useLen > 0 && ! isspace(run[useLen-1]) && ! ispunct(run[useLen-1]))
+				{
+					--useLen;
+				}
+				if (useLen == 0)
+				{
+					useLen = prev_use_len;
+				}
+			}
+
+			// add the chars that could fit onto one line to our result
 			final_wtext.append(wtext, cur, useLen);
 			cur += useLen;
-			// not enough room to add any more characters
-			if (useLen == 0) break;
+			hoffset += mDefaultFont->getWidth(run.substr(0, useLen).c_str());
+
+			// abort if not enough room to add any more characters
+			if (useLen == 0)
+			{
+				break;
+			}
 		}
 
-		if (cur < len)
+		if (charsRemaining)
 		{
 			if (wtext[cur] == '\n')
 			{
 				cur += 1;
 			}
-			line_num +=1;
-			// Don't wrap the last line if the text is going to spill off
-			// the bottom of the rectangle.  Assume we prefer to run off
-			// the right edge.
-			// *TODO: Is this the right behavior?
-			if((line_num-1)*line_height <= (F32)getRect().getHeight())
+			final_wtext += '\n';
+			hoffset = 0;
+			line_num += 1;
+		}
+	}
+
+	return final_wtext;
+}
+
+void LLTextBox::setWrappedText(const LLStringExplicit& in_text, F32 max_width)
+{
+	mDidWordWrap = TRUE;
+	setText(wstring_to_utf8str(getWrappedText(in_text, max_width)));
+}
+
+LLWString LLTextBox::getWrappedText(const LLStringExplicit& in_text, F32 max_width)
+{
+	//
+	// we don't want to wrap Urls otherwise we won't be able to detect their
+	// presence for hyperlinking. So we look for all Urls, and then word wrap
+	// the text before and after, but never break a Url in the middle. We
+	// also need to consider that the Url will be displayed as a label (not
+	// necessary the actual Url string).
+	//
+
+	if (max_width < 0.0f)
+	{
+		max_width = (F32)getRect().getWidth();
+	}
+
+	LLWString wtext = utf8str_to_wstring(in_text);
+	LLWString final_wtext;
+	S32 line_num = 1;
+	S32 hoffset = 0;
+
+	// find the next Url in the text string
+	LLUrlMatch match;
+	while ( LLUrlRegistry::instance().findUrl(wstring_to_utf8str(wtext), match))
+	{
+		S32 start = match.getStart();
+		S32 end = match.getEnd() + 1;
+
+		// perform word wrap on the text before the Url
+		final_wtext += wrapText(wtext.substr(0, start), hoffset, line_num, max_width);
+
+		// add the Url (but compute width based on its label)
+		S32 label_width = mDefaultFont->getWidth(match.getLabel());
+		if (hoffset > 0 && hoffset + label_width > max_width)
+		{
+			final_wtext += '\n';
+			line_num++;
+			hoffset = 0;
+		}
+		final_wtext += wtext.substr(start, end-start);
+		hoffset += label_width;
+		if (hoffset > max_width)
+		{
+			final_wtext += '\n';
+			line_num++;
+			hoffset = 0;
+			// eat any leading whitespace on the next line
+			while (isspace(wtext[end]) && end < (S32)wtext.size())
 			{
-				final_wtext += '\n';
+				end++;
 			}
 		}
+
+		// move on to the rest of the text after the Url
+		wtext = wtext.substr(end, wtext.size() - end + 1);
 	}
-	
-	mDidWordWrap = TRUE;
-	std::string final_text = wstring_to_utf8str(final_wtext);
-	setText(final_text);
 
+	final_wtext += wrapText(wtext, hoffset, line_num, max_width);
+	return final_wtext;
 }
 
 S32 LLTextBox::getTextPixelWidth()
@@ -268,7 +347,7 @@ S32 LLTextBox::getTextPixelWidth()
 			iter != mLineLengthList.end(); ++iter)
 		{
 			S32 line_length = *iter;
-			S32 line_width = mFontGL->getWidth( mText.getWString().c_str(), cur_pos, line_length );
+			S32 line_width = mDefaultFont->getWidth( mDisplayText.c_str(), cur_pos, line_length );
 			if( line_width > max_line_width )
 			{
 				max_line_width = line_width;
@@ -278,7 +357,7 @@ S32 LLTextBox::getTextPixelWidth()
 	}
 	else
 	{
-		max_line_width = mFontGL->getWidth(mText.getWString().c_str());
+		max_line_width = mDefaultFont->getWidth(mDisplayText.c_str());
 	}
 	return max_line_width;
 }
@@ -290,7 +369,7 @@ S32 LLTextBox::getTextPixelHeight()
 	{
 		num_lines = 1;
 	}
-	return (S32)(num_lines * mFontGL->getLineHeight());
+	return (S32)(num_lines * mDefaultFont->getLineHeight());
 }
 
 void LLTextBox::setValue(const LLSD& value )
@@ -302,7 +381,7 @@ void LLTextBox::setValue(const LLSD& value )
 BOOL LLTextBox::setTextArg( const std::string& key, const LLStringExplicit& text )
 {
 	mText.setArg(key, text);
-	setLineLengths();
+	updateDisplayTextAndSegments();
 	return TRUE;
 }
 
@@ -345,18 +424,11 @@ void LLTextBox::draw()
 
 	if ( getEnabled() )
 	{
-		if(mHasHover)
-		{
-			drawText( text_x, text_y, mHoverColor.get() );
-		}
-		else
-		{
-			drawText( text_x, text_y, mTextColor.get() );
-		}				
+		drawText( text_x, text_y, mDisplayText, mTextColor.get() );
 	}
 	else
 	{
-		drawText( text_x, text_y, mDisabledColor.get() );
+		drawText( text_x, text_y, mDisplayText, mDisabledColor.get() );
 	}
 
 	if (sDebugRects)
@@ -370,41 +442,46 @@ void LLTextBox::draw()
 	//{
 	//	drawDebugRect();
 	//}
-
-	mHasHover = FALSE; // This is reset every frame.
 }
 
 void LLTextBox::reshape(S32 width, S32 height, BOOL called_from_parent)
 {
-	// reparse line lengths
+	// reparse line lengths (don't need to recalculate the display text)
 	setLineLengths();
 	LLView::reshape(width, height, called_from_parent);
 }
 
-void LLTextBox::drawText( S32 x, S32 y, const LLColor4& color )
+void LLTextBox::drawText( S32 x, S32 y, const LLWString &text, const LLColor4& color )
 {
-	if( mLineLengthList.empty() )
+	if (mSegments.size() > 1)
 	{
-		mFontGL->render(mText.getWString(), 0, (F32)x, (F32)y, color,
-						mHAlign, mVAlign, 
-						0,
-						mShadowType,
-						S32_MAX, getRect().getWidth(), NULL, mUseEllipses);
+		// we have Urls (or other multi-styled segments)
+		drawTextSegments(x, y, text);
+	}
+	else if( mLineLengthList.empty() )
+	{
+		// simple case of 1 line of text in one style
+		mDefaultFont->render(text, 0, (F32)x, (F32)y, color,
+							 mHAlign, mVAlign, 
+							 0,
+							 mShadowType,
+							 S32_MAX, getRect().getWidth(), NULL, mUseEllipses);
 	}
 	else
 	{
+		// simple case of multiple lines of text, all in the same style
 		S32 cur_pos = 0;
 		for (std::vector<S32>::iterator iter = mLineLengthList.begin();
 			iter != mLineLengthList.end(); ++iter)
 		{
 			S32 line_length = *iter;
-			mFontGL->render(mText.getWString(), cur_pos, (F32)x, (F32)y, color,
-							mHAlign, mVAlign,
-							0,
-							mShadowType,
-							line_length, getRect().getWidth(), NULL, mUseEllipses );
+			mDefaultFont->render(text, cur_pos, (F32)x, (F32)y, color,
+								 mHAlign, mVAlign,
+								 0,
+								 mShadowType,
+								 line_length, getRect().getWidth(), NULL, mUseEllipses );
 			cur_pos += line_length + 1;
-			y -= llfloor(mFontGL->getLineHeight()) + mLineSpacing;
+			y -= llfloor(mDefaultFont->getLineHeight()) + mLineSpacing;
 		}
 	}
 }
@@ -415,3 +492,254 @@ void LLTextBox::reshapeToFitText()
 	S32 height = getTextPixelHeight();
 	reshape( width + 2 * mHPad, height + 2 * mVPad );
 }
+
+S32 LLTextBox::getDocIndexFromLocalCoord( S32 local_x, S32 local_y, BOOL round ) const
+{
+	// Returns the character offset for the character under the local (x, y) coordinate.
+	// When round is true, if the position is on the right half of a character, the cursor
+	// will be put to its right.  If round is false, the cursor will always be put to the
+	// character's left.
+
+	LLRect rect = getLocalRect();
+	rect.mLeft += mHPad;
+	rect.mRight -= mHPad;
+	rect.mTop += mVPad;
+	rect.mBottom -= mVPad;
+
+	// Figure out which line we're nearest to.
+	S32 total_lines = getLineCount();
+	S32 line_height = llround( mDefaultFont->getLineHeight() ) + mLineSpacing;
+	S32 line = (rect.mTop - 1 - local_y) / line_height;
+	if (line >= total_lines)
+	{
+		return getLength(); // past the end
+	}
+
+	line = llclamp( line, 0, total_lines );
+	S32 line_start = getLineStart(line);
+	S32 next_start = getLineStart(line+1);
+	S32	line_end = (next_start != line_start) ? next_start - 1 : getLength();
+	if (line_start == -1)
+	{
+		return 0;
+	}
+
+	S32 line_len = line_end - line_start;
+	S32 pos = mDefaultFont->charFromPixelOffset(mDisplayText.c_str(), line_start,
+												(F32)(local_x - rect.mLeft),
+												(F32)rect.getWidth(),
+												line_len, round);
+
+	return line_start + pos;
+}
+
+S32 LLTextBox::getLineStart( S32 line ) const
+{
+	line = llclamp(line, 0, getLineCount()-1);
+
+	S32 result = 0;
+	for (int i = 0; i < line; i++)
+	{
+		result += mLineLengthList[i] + 1 /* add newline */;
+	}
+
+	return result;
+}
+
+void LLTextBox::updateDisplayTextAndSegments()
+{
+	// remove any previous segment list
+	clearSegments();
+
+	// if URL parsing is turned off, then not much to bo
+	if (! mParseHTML)
+	{
+		mDisplayText = mText.getWString();
+		setLineLengths();
+		return;
+	}
+
+	// create unique text segments for Urls
+	mDisplayText.clear();
+	S32 end = 0;
+	LLUrlMatch match;
+	LLWString text = mText.getWString();
+		
+	// find the next Url in the text string
+	while ( LLUrlRegistry::instance().findUrl(wstring_to_utf8str(text), match,
+											  boost::bind(&LLTextBox::onUrlLabelUpdated, this, _1, _2)) )
+	{
+		// work out the char offset for the start/end of the url
+		S32 seg_start = mDisplayText.size();
+		S32 start = seg_start + match.getStart();
+		end = start + match.getLabel().size();
+
+		// create a segment for the text before the Url
+		mSegments.insert(new LLNormalTextSegment(new LLStyle(), seg_start, start, *this));
+		mDisplayText += text.substr(0, match.getStart());
+
+		// create a segment for the Url text
+		LLStyleSP html(new LLStyle);
+		html->setVisible(true);
+		html->setColor(mLinkColor);
+		html->mUnderline = TRUE;
+		html->setLinkHREF(match.getUrl());
+
+		LLNormalTextSegment *html_seg = new LLNormalTextSegment(html, start, end, *this); 
+		html_seg->setToolTip(match.getTooltip());
+
+		mSegments.insert(html_seg);
+		mDisplayText += utf8str_to_wstring(match.getLabel());
+
+		// move on to the rest of the text after the Url
+		text = text.substr(match.getEnd()+1, text.size() - match.getEnd());
+	}
+
+	// output a segment for the remaining text
+	if (text.size() > 0)
+	{
+		mSegments.insert(new LLNormalTextSegment(new LLStyle(), end, end + text.size(), *this));
+		mDisplayText += text;
+	}
+
+	// strip whitespace from the end of the text
+	while (mDisplayText.size() > 0 && isspace(mDisplayText[mDisplayText.size()-1]))
+	{
+		mDisplayText = mDisplayText.substr(0, mDisplayText.size() - 1);
+
+		segment_set_t::iterator it = getSegIterContaining(mDisplayText.size());
+		if (it != mSegments.end())
+		{
+			LLTextSegmentPtr seg = *it;
+			seg->setEnd(seg->getEnd()-1);
+		}
+	}
+
+	// we may have changed the line lengths, so recalculate them
+	setLineLengths();
+}
+
+void LLTextBox::onUrlLabelUpdated(const std::string &url, const std::string &label)
+{
+	if (mDidWordWrap)
+	{
+		// re-word wrap as the url label lengths may have changed
+		setWrappedText(mText.getString());
+	}
+	else
+	{
+		// or just update the display text with the latest Url labels
+		updateDisplayTextAndSegments();
+	}
+}
+
+bool LLTextBox::isClickable() const
+{
+	// return true if we have been given a click callback
+	if (mClickedCallback)
+	{
+		return true;
+	}
+
+	// also return true if we have a clickable Url in the text
+	segment_set_t::const_iterator it;
+	for (it = mSegments.begin(); it != mSegments.end(); ++it)
+	{
+		LLTextSegmentPtr segmentp = *it;
+		if (segmentp)
+		{
+			const LLStyleSP style = segmentp->getStyle();
+			if (style && style->isLink())
+			{
+				return true;
+			}
+		}
+	}
+
+	// otherwise there is nothing clickable here
+	return false;
+}
+
+void LLTextBox::drawTextSegments(S32 init_x, S32 init_y, const LLWString &text)
+{
+	const S32 text_len = text.length();
+	if (text_len <= 0)
+	{
+		return;
+	}
+
+	S32 cur_line = 0;
+	S32 num_lines = getLineCount();
+	S32 line_start = getLineStart(cur_line);
+	S32 line_height = llround( mDefaultFont->getLineHeight() ) + mLineSpacing;
+	F32 text_y = (F32) init_y;
+	segment_set_t::iterator cur_seg = mSegments.begin();
+
+	// render a line of text at a time
+	const LLRect textRect = getLocalRect();
+	while((textRect.mBottom <= text_y) && (cur_line < num_lines))
+	{
+		S32 next_start = -1;
+		S32 line_end = text_len;
+
+		if ((cur_line + 1) < num_lines)
+		{
+			next_start = getLineStart(cur_line + 1);
+			line_end = next_start;
+		}
+		if ( text[line_end-1] == '\n' )
+		{
+			--line_end;
+		}
+		
+		// render all segments on this line
+		F32 text_x = init_x;
+		S32 seg_start = line_start;
+		while (seg_start < line_end && cur_seg != mSegments.end())
+		{
+			// move to the next segment (or continue the previous one)
+			LLTextSegment *cur_segment = *cur_seg;
+			while (cur_segment->getEnd() <= seg_start)
+			{
+				if (++cur_seg == mSegments.end())
+				{
+					return;
+				}
+				cur_segment = *cur_seg;
+			}
+
+			// Draw a segment within the line
+			S32 clipped_end	= llmin( line_end, cur_segment->getEnd() );
+			S32 clipped_len = clipped_end - seg_start;
+			if( clipped_len > 0 )
+			{
+				LLStyleSP style = cur_segment->getStyle();
+				if (style && style->isVisible())
+				{
+					// work out the color for the segment
+					LLColor4 color ;
+					if (getEnabled())
+					{
+						color = style->isLink() ? mLinkColor.get() : mTextColor.get();
+					}
+					else
+					{
+						color = mDisabledColor.get();
+					}
+
+					// render a single line worth for this segment
+					mDefaultFont->render(text, seg_start, text_x, text_y, color,
+										 mHAlign, mVAlign, 0, mShadowType, clipped_len,
+										 textRect.getWidth(), &text_x, mUseEllipses);
+				}
+
+				seg_start += clipped_len;
+			}
+		}
+
+		// move down one line
+		text_y -= (F32)line_height;
+		line_start = next_start;
+		cur_line++;
+	}
+}
diff --git a/indra/llui/lltextbox.h b/indra/llui/lltextbox.h
index d807fe7639..940b820004 100644
--- a/indra/llui/lltextbox.h
+++ b/indra/llui/lltextbox.h
@@ -37,10 +37,11 @@
 #include "v4color.h"
 #include "llstring.h"
 #include "lluistring.h"
+#include "lltextbase.h"
 
-
-class LLTextBox
-:	public LLUICtrl
+class LLTextBox :
+	public LLTextBase,
+	public LLUICtrl
 {
 public:
 	
@@ -51,8 +52,7 @@ public:
 	{
 		Optional<std::string> text;
 
-		Optional<bool>		highlight_on_hover,
-							border_visible,
+		Optional<bool>		border_visible,
 							border_drop_shadow_visible,
 							bg_visible,
 							use_ellipses,
@@ -65,7 +65,6 @@ public:
 							length;
 
 		Optional<LLUIColor>	text_color,
-							hover_color,
 							disabled_color,
 							background_color,
 							border_color;
@@ -90,15 +89,14 @@ public:
 	virtual BOOL	handleMouseDown(S32 x, S32 y, MASK mask);
 	virtual BOOL	handleMouseUp(S32 x, S32 y, MASK mask);
 	virtual BOOL	handleHover(S32 x, S32 y, MASK mask);
+	virtual BOOL	handleRightMouseDown(S32 x, S32 y, MASK mask);
+	virtual BOOL	handleToolTip(S32 x, S32 y, std::string& msg, LLRect* sticky_rect_screen);
 
 	void			setColor( const LLColor4& c )			{ mTextColor = c; }
 	void			setDisabledColor( const LLColor4& c)	{ mDisabledColor = c; }
 	void			setBackgroundColor( const LLColor4& c)	{ mBackgroundColor = c; }	
 	void			setBorderColor( const LLColor4& c)		{ mBorderColor = c; }	
 
-	void			setHoverColor( const LLColor4& c )		{ mHoverColor = c; }
-	void			setHoverActive( BOOL active )			{ mHoverActive = active; }
-
 	void			setText( const LLStringExplicit& text );
 	void			setWrappedText(const LLStringExplicit& text, F32 max_width = -1.f); // -1 means use existing control width
 	void			setUseEllipses( BOOL use_ellipses )		{ mUseEllipses = use_ellipses; }
@@ -112,35 +110,42 @@ public:
 	void			setHAlign( LLFontGL::HAlign align )		{ mHAlign = align; }
 	void			setClickedCallback( boost::function<void (void*)> cb, void* userdata = NULL ){ mClickedCallback = boost::bind(cb, userdata); }		// mouse down and up within button
 
-	const LLFontGL* getFont() const							{ return mFontGL; }
+	const LLFontGL* getFont() const							{ return mDefaultFont; }
 
 	void			reshapeToFitText();
 
 	const std::string&	getText() const							{ return mText.getString(); }
+	LLWString		getWText() const { return mDisplayText; }
 	S32				getTextPixelWidth();
 	S32				getTextPixelHeight();
+	S32				getLength() const { return mDisplayText.length(); }
 
 	virtual void	setValue(const LLSD& value );		
 	virtual LLSD	getValue() const						{ return LLSD(getText()); }
 	virtual BOOL	setTextArg( const std::string& key, const LLStringExplicit& text );
 
-private:
+protected:
+	S32 			getLineCount() const { return mLineLengthList.size(); }
+	S32 			getLineStart( S32 line ) const;
+	S32             getDocIndexFromLocalCoord( S32 local_x, S32 local_y, BOOL round ) const;
+	LLWString       getWrappedText(const LLStringExplicit& in_text, F32 max_width = -1.f);
 	void			setLineLengths();
-	void			drawText(S32 x, S32 y, const LLColor4& color );
+	void			updateDisplayTextAndSegments();
+	virtual void	drawText(S32 x, S32 y, const LLWString &text, const LLColor4& color );
+	void            onUrlLabelUpdated(const std::string &url, const std::string &label);
+	bool            isClickable() const;
+	LLWString       wrapText(const LLWString &wtext, S32 &hoffset, S32 &line_num, F32 max_width);
+	void            drawTextSegments(S32 x, S32 y, const LLWString &text);
 
 	LLUIString		mText;
-	const LLFontGL*	mFontGL;
-	LLUIColor	mTextColor;
-	LLUIColor	mDisabledColor;
-	LLUIColor	mBackgroundColor;
-	LLUIColor	mBorderColor;
-	LLUIColor	mHoverColor;
-
-	BOOL			mHoverActive;	
-	BOOL			mHasHover;
+	LLWString		mDisplayText;
+	LLUIColor		mTextColor;
+	LLUIColor		mDisabledColor;
+	LLUIColor		mBackgroundColor;
+	LLUIColor		mBorderColor;
+
 	BOOL			mBackgroundVisible;
 	BOOL			mBorderVisible;
-	BOOL			mWordWrap;
 	BOOL            mDidWordWrap;
 	
 	LLFontGL::ShadowType mShadowType;
diff --git a/indra/llui/lltexteditor.cpp b/indra/llui/lltexteditor.cpp
index 921041d17f..296ccea0e4 100644
--- a/indra/llui/lltexteditor.cpp
+++ b/indra/llui/lltexteditor.cpp
@@ -59,6 +59,7 @@
 #include "lltextparser.h"
 #include "llscrollcontainer.h"
 #include "llpanel.h"
+#include "llurlregistry.h"
 
 #include <queue>
 #include "llcombobox.h"
@@ -78,10 +79,6 @@ const S32	CURSOR_THICKNESS = 2;
 const S32	SPACES_PER_TAB = 4;
 
 
-void (* LLTextEditor::sURLcallback)(const std::string&)   = NULL;
-bool (* LLTextEditor::sSecondlifeURLcallback)(const std::string&)   = NULL;
-bool (* LLTextEditor::sSecondlifeURLcallbackRightClick)(const std::string&)   = NULL;
-
 // helper functors
 struct LLTextEditor::compare_bottom
 {
@@ -331,8 +328,9 @@ LLTextEditor::Params::Params()
 	is_unicode("is_unicode")// ignored
 {}
 
-LLTextEditor::LLTextEditor(const LLTextEditor::Params& p)
-	:	LLUICtrl(p, LLTextViewModelPtr(new LLTextViewModel)),
+LLTextEditor::LLTextEditor(const LLTextEditor::Params& p) :
+	LLUICtrl(p, LLTextViewModelPtr(new LLTextViewModel)),
+	LLTextBase(p),
 	mMaxTextByteLength( p.max_text_length ),
 	mBaseDocIsPristine(TRUE),
 	mPristineCmd( NULL ),
@@ -351,7 +349,6 @@ LLTextEditor::LLTextEditor(const LLTextEditor::Params& p)
 	mFocusBgColor(		p.bg_focus_color() ),
 	mLinkColor(			p.link_color() ),
 	mReadOnly(p.read_only),
-	mWordWrap( p.word_wrap ),
 	mShowLineNumbers ( p.show_line_numbers ),
 	mCommitOnFocusLost( p.commit_on_focus_lost),
 	mTrackBottom( p.track_bottom ),
@@ -363,14 +360,16 @@ LLTextEditor::LLTextEditor(const LLTextEditor::Params& p)
 	mReflowNeeded(FALSE),
 	mScrollNeeded(FALSE),
 	mLastSelectionY(-1),
-	mParseHTML(FALSE),
 	mParseHighlights(FALSE),
 	mTabsToNextField(p.ignore_tab),
-	mDefaultFont(p.font),
 	mScrollIndex(-1)
 {
 	static LLUICachedControl<S32> scrollbar_size ("UIScrollbarSize", 0);
 
+	mWordWrap = p.word_wrap;
+	mDefaultFont = p.font;
+	mParseHTML = FALSE;
+
 	mSourceID.generate();
 
 	// reset desired x cursor position
@@ -413,7 +412,6 @@ LLTextEditor::LLTextEditor(const LLTextEditor::Params& p)
 
 	appendText(p.default_text, FALSE, FALSE);
 
-	mHTML.clear();
 }
 
 void LLTextEditor::initFromParams( const LLTextEditor::Params& p)
@@ -451,7 +449,6 @@ LLTextEditor::~LLTextEditor()
 	}
 
 	// Scrollbar is deleted by LLView
-	mHoverSegment = NULL;
 	std::for_each(mUndoStack.begin(), mUndoStack.end(), DeletePointer());
 }
 
@@ -666,18 +663,12 @@ BOOL LLTextEditor::truncate()
 	return did_truncate;
 }
 
-void LLTextEditor::clearSegments()
-{
-	mHoverSegment = NULL;
-	mSegments.clear();
-}
-
 void LLTextEditor::setText(const LLStringExplicit &utf8str)
 {
+	// clear out the existing text and segments
 	clearSegments();
 
-	// LLStringUtil::removeCRLF(utf8str);
-	getViewModel()->setValue(utf8str_removeCRLF(utf8str));
+	getViewModel()->setValue("");
 
 	truncate();
 	blockUndo();
@@ -687,6 +678,11 @@ void LLTextEditor::setText(const LLStringExplicit &utf8str)
 	startOfDoc();
 	deselect();
 
+	// append the new text (supports Url linking)
+	std::string text(utf8str);
+	LLStringUtil::removeCRLF(text);
+	appendStyledText(text, false, false, LLStyle::Params());
+
 	needsReflow();
 
 	resetDirty();
@@ -696,9 +692,10 @@ void LLTextEditor::setText(const LLStringExplicit &utf8str)
 
 void LLTextEditor::setWText(const LLWString &wtext)
 {
+	// clear out the existing text and segments
 	clearSegments();
 
-	getViewModel()->setDisplay(wtext);
+	getViewModel()->setDisplay(LLWString());
 
 	truncate();
 	blockUndo();
@@ -708,6 +705,9 @@ void LLTextEditor::setWText(const LLWString &wtext)
 	startOfDoc();
 	deselect();
 
+	// append the new text (supports Url linking)
+	appendStyledText(wstring_to_utf8str(wtext), false, false, LLStyle::Params());
+
 	needsReflow();
 
 	resetDirty();
@@ -913,32 +913,6 @@ void LLTextEditor::getLineAndOffset( S32 startpos, S32* linep, S32* offsetp, boo
 	}
 }
 
-void LLTextEditor::getSegmentAndOffset( S32 startpos, segment_set_t::const_iterator* seg_iter, S32* offsetp ) const
-{
-	*seg_iter = getSegIterContaining(startpos);
-	if (*seg_iter == mSegments.end())
-	{
-		*offsetp = 0;
-	}
-	else
-	{
-		*offsetp = startpos - (**seg_iter)->getStart();
-	}
-}
-
-void LLTextEditor::getSegmentAndOffset( S32 startpos, segment_set_t::iterator* seg_iter, S32* offsetp )
-{
-	*seg_iter = getSegIterContaining(startpos);
-	if (*seg_iter == mSegments.end())
-	{
-		*offsetp = 0;
-	}
-	else
-	{
-		*offsetp = startpos - (**seg_iter)->getStart();
-	}
-}
-
 const LLTextSegmentPtr	LLTextEditor::getPreviousSegment() const
 {
 	// find segment index at character to left of cursor (or rightmost edge of selection)
@@ -1154,6 +1128,10 @@ S32 LLTextEditor::getEditableIndex(S32 index, bool increasing_direction)
 	segment_set_t::iterator segment_iter;
 	S32 offset;
 	getSegmentAndOffset(index, &segment_iter, &offset);
+	if (segment_iter == mSegments.end())
+	{
+		return 0;
+	}
 
 	LLTextSegmentPtr segmentp = *segment_iter;
 
@@ -1377,25 +1355,7 @@ BOOL LLTextEditor::handleToolTip(S32 x, S32 y, std::string& msg, LLRect* sticky_
 		}
 	}
 
-	const LLTextSegmentPtr cur_segment = getSegmentAtLocalPos( x, y );
-	if( cur_segment )
-	{
-		BOOL has_tool_tip = FALSE;
-		has_tool_tip = cur_segment->getToolTip( msg );
-
-		if( has_tool_tip )
-		{
-			// Just use a slop area around the cursor
-			// Convert rect local to screen coordinates
-			S32 SLOP = 8;
-			localPointToScreen( 
-				x - SLOP, y - SLOP, 
-				&(sticky_rect_screen->mLeft), &(sticky_rect_screen->mBottom) );
-			sticky_rect_screen->mRight = sticky_rect_screen->mLeft + 2 * SLOP;
-			sticky_rect_screen->mTop = sticky_rect_screen->mBottom + 2 * SLOP;
-		}
-	}
-	return TRUE;
+	return handleToolTipForUrl(this, x, y, msg, sticky_rect_screen);
 }
 
 BOOL LLTextEditor::handleMouseDown(S32 x, S32 y, MASK mask)
@@ -1480,12 +1440,6 @@ BOOL LLTextEditor::handleHover(S32 x, S32 y, MASK mask)
 	static LLUICachedControl<S32> scrollbar_size ("UIScrollbarSize", 0);
 	BOOL handled = FALSE;
 
-	if (mHoverSegment) 
-	{
-		mHoverSegment->setHasMouseHover(false);
-	}
-	mHoverSegment = NULL;
-
 	if(hasMouseCapture() )
 	{
 		if( mIsSelecting ) 
@@ -1525,30 +1479,11 @@ BOOL LLTextEditor::handleHover(S32 x, S32 y, MASK mask)
 	if( !handled )
 	{
 		// Check to see if we're over an HTML-style link
-		LLTextSegmentPtr cur_segment = getSegmentAtLocalPos( x, y );
-		if( cur_segment )
+		handled = handleHoverOverUrl(x, y);
+		if( handled )
 		{
-			if(cur_segment->getStyle()->isLink())
-			{
-				lldebugst(LLERR_USER_INPUT) << "hover handled by " << getName() << " (over link, inactive)" << llendl;		
-				getWindow()->setCursor(UI_CURSOR_HAND);
-				handled = TRUE;
-			}
-			//else
-			//if(cur_segment->getStyle()->getIsEmbeddedItem())
-			//{
-			//	lldebugst(LLERR_USER_INPUT) << "hover handled by " << getName() << " (over embedded item, inactive)" << llendl;		
-			//	getWindow()->setCursor(UI_CURSOR_HAND);
-			//	//getWindow()->setCursor(UI_CURSOR_ARROW);
-			//	handled = TRUE;
-			//}
-			if (mHoverSegment) 
-			{
-				mHoverSegment->setHasMouseHover(false);
-			}
-			cur_segment->setHasMouseHover(true);
-			mHoverSegment = cur_segment;
-			mHTML = mHoverSegment->getStyle()->getLinkHREF();
+			lldebugst(LLERR_USER_INPUT) << "hover handled by " << getName() << llendl;		
+			getWindow()->setCursor(UI_CURSOR_HAND);
 		}
 
 		if( !handled )
@@ -1581,9 +1516,9 @@ BOOL LLTextEditor::handleMouseUp(S32 x, S32 y, MASK mask)
 			endSelection();
 		}
 		
-		if( !hasSelection() )
+		if( !hasSelection() && hasMouseCapture() )
 		{
-			handleMouseUpOverSegment( x, y, mask );
+			handleMouseUpOverUrl(x, y);
 		}
 
 		// take selection to 'primary' clipboard
@@ -3596,14 +3531,20 @@ void LLTextEditor::appendStyledText(const std::string &new_text,
 	{
 
 		S32 start=0,end=0;
+		LLUrlMatch match;
 		std::string text = new_text;
-		while ( findHTML(text, &start, &end) )
+		while ( LLUrlRegistry::instance().findUrl(text, match,
+		        boost::bind(&LLTextEditor::onUrlLabelUpdated, this, _1, _2)) )
 		{
+			start = match.getStart();
+			end = match.getEnd()+1;
+
 			LLStyle::Params link_params = style_params;
 			link_params.color = mLinkColor;
 			link_params.font.style = "UNDERLINE";
-			link_params.link_href = text.substr(start,end-start);
+			link_params.link_href = match.getUrl();
 
+			// output the text before the Url
 			if (start > 0)
 			{
 				if (part == (S32)LLTextParser::WHOLE ||
@@ -3617,9 +3558,38 @@ void LLTextEditor::appendStyledText(const std::string &new_text,
 				}
 				std::string subtext=text.substr(0,start);
 				appendHighlightedText(subtext,allow_undo, prepend_newline, part, style_params); 
+				prepend_newline = false;
 			}
-			
-			appendText(text.substr(start, end-start),allow_undo, prepend_newline, link_params);
+
+			// output the styled Url
+			appendText(match.getLabel(),allow_undo, prepend_newline, link_params);
+			prepend_newline = false;
+
+			// set the tooltip for the Url label
+			if (! match.getTooltip().empty())
+			{
+				segment_set_t::iterator it = getSegIterContaining(getLength()-1);
+				if (it != mSegments.end())
+				{
+					LLTextSegmentPtr segment = *it;
+					segment->setToolTip(match.getTooltip());
+				}
+			}
+
+			// output an optional icon after the Url
+			if (! match.getIcon().empty())
+			{
+				LLUIImagePtr image = LLUI::getUIImage(match.getIcon());
+				if (image)
+				{
+					LLStyle::Params icon;
+					icon.image = image;
+					// TODO: fix spacing of images and remove the fixed char spacing
+					appendText("  ", allow_undo, prepend_newline, icon);
+				}
+			}
+
+			// move on to the rest of the text after the Url
 			if (end < (S32)text.length()) 
 			{
 				text = text.substr(end,text.length() - end);
@@ -3711,7 +3681,7 @@ void LLTextEditor::appendText(const std::string &new_text, bool allow_undo, bool
 	}
 
 	append(wide_text, TRUE, segmentp);
-	
+
 	needsReflow();
 	
 	// Set the cursor and scroll position
@@ -3795,6 +3765,58 @@ void LLTextEditor::appendWidget(LLView* widget, const std::string &widget_text,
 	}
 }
 
+void LLTextEditor::onUrlLabelUpdated(const std::string &url,
+									 const std::string &label)
+{
+	// LLUrlRegistry has given us a new label for one of our Urls
+	replaceUrlLabel(url, label);
+}
+
+void LLTextEditor::replaceUrlLabel(const std::string &url,
+								   const std::string &label)
+{
+	// get the full (wide) text for the editor so we can change it
+	LLWString text = getWText();
+	LLWString wlabel = utf8str_to_wstring(label);
+	bool modified = false;
+	S32 seg_start = 0;
+
+	// iterate through each segment looking for ones styled as links
+	segment_set_t::iterator it;
+	for (it = mSegments.begin(); it != mSegments.end(); ++it)
+	{
+		LLTextSegment *seg = *it;
+		const LLStyleSP style = seg->getStyle();
+
+		// update segment start/end length in case we replaced text earlier
+		S32 seg_length = seg->getEnd() - seg->getStart();
+		seg->setStart(seg_start);
+		seg->setEnd(seg_start + seg_length);
+
+		// if we find a link with our Url, then replace the label
+		if (style->isLink() && style->getLinkHREF() == url)
+		{
+			S32 start = seg->getStart();
+			S32 end = seg->getEnd();
+			text = text.substr(0, start) + wlabel + text.substr(end, text.size() - end + 1);
+			seg->setEnd(start + wlabel.size());
+			modified = true;
+		}
+
+		// work out the character offset for the next segment
+		seg_start = seg->getEnd();
+	}
+
+	// update the editor with the new (wide) text string
+	if (modified)
+	{
+		getViewModel()->setDisplay(text);
+		deselect();
+		setCursorPos(mCursorPos);
+		needsReflow();
+	}
+}
+
 void LLTextEditor::removeTextFromEnd(S32 num_chars)
 {
 	if (num_chars <= 0) return;
@@ -4097,7 +4119,7 @@ void LLTextEditor::updateSegments()
 		segment_vec_t segment_list;
 		mKeywords.findSegments(&segment_list, getWText(), mDefaultColor.get(), *this);
 
-		mSegments.clear();
+		clearSegments();
 		segment_set_t::iterator insert_it = mSegments.begin();
 		for (segment_vec_t::iterator list_it = segment_list.begin(); list_it != segment_list.end(); ++list_it)
 		{
@@ -4106,7 +4128,29 @@ void LLTextEditor::updateSegments()
 	}
 
 	createDefaultSegment();
+}
 
+void LLTextEditor::updateLinkSegments()
+{
+	// update any segments that contain a link
+	for (segment_set_t::iterator it = mSegments.begin(); it != mSegments.end(); ++it)
+	{
+		LLTextSegment *segment = *it;
+		if (segment && segment->getStyle() && segment->getStyle()->isLink())
+		{
+			// if the link's label (what the user can edit) is a valid Url,
+			// then update the link's HREF to be the same as the label text.
+			// This lets users edit Urls in-place.
+			LLUrlMatch match;
+			LLStyleSP style = static_cast<LLStyleSP>(segment->getStyle());
+			std::string url_label = getText().substr(segment->getStart(), segment->getEnd()-segment->getStart());
+			if (LLUrlRegistry::instance().findUrl(url_label, match))
+			{
+				LLStringUtil::trim(url_label);
+				style->setLinkHREF(url_label);
+			}
+		}
+	}
 }
 
 void LLTextEditor::insertSegment(LLTextSegmentPtr segment_to_insert)
@@ -4170,57 +4214,6 @@ void LLTextEditor::insertSegment(LLTextSegmentPtr segment_to_insert)
 	}
 }
 
-BOOL LLTextEditor::handleMouseUpOverSegment(S32 x, S32 y, MASK mask)
-{
-	if ( hasMouseCapture() )
-	{
-		// This mouse up was part of a click.
-		// Regardless of where the cursor is, see if we recently touched a link
-		// and launch it if we did.
-		if (mParseHTML && mHTML.length() > 0)
-		{
-				//Special handling for slurls
-			if ( (sSecondlifeURLcallback!=NULL) && !(*sSecondlifeURLcallback)(mHTML) )
-			{
-				if (sURLcallback!=NULL) (*sURLcallback)(mHTML);
-			}
-			mHTML.clear();
-		}
-	}
-
-	return FALSE;
-}
-
-
-// Finds the text segment (if any) at the give local screen position
-LLTextSegmentPtr LLTextEditor::getSegmentAtLocalPos( S32 x, S32 y )
-{
-	// Find the cursor position at the requested local screen position
-	S32 offset = getDocIndexFromLocalCoord( x, y, FALSE );
-	segment_set_t::iterator seg_iter = getSegIterContaining(offset);
-	if (seg_iter != mSegments.end())
-	{
-		return *seg_iter;
-	}
-	else
-	{
-		return LLTextSegmentPtr();
-	}
-}
-
-LLTextEditor::segment_set_t::iterator LLTextEditor::getSegIterContaining(S32 index)
-{
-	segment_set_t::iterator it = mSegments.upper_bound(new LLIndexSegment(index));
-	return it;
-}
-
-LLTextEditor::segment_set_t::const_iterator LLTextEditor::getSegIterContaining(S32 index) const
-{
-	LLTextEditor::segment_set_t::const_iterator it =  mSegments.upper_bound(new LLIndexSegment(index));
-	return it;
-}
-
-
 void LLTextEditor::onMouseCaptureLost()
 {
 	endSelection();
@@ -4330,169 +4323,6 @@ BOOL LLTextEditor::exportBuffer(std::string &buffer )
 	return TRUE;
 }
 
-///////////////////////////////////////////////////////////////////
-// Refactoring note: We may eventually want to replace this with boost::regex or 
-// boost::tokenizer capabilities since we've already fixed at least two JIRAs
-// concerning logic issues associated with this function.
-S32 LLTextEditor::findHTMLToken(const std::string &line, S32 pos, BOOL reverse) const
-{
-	std::string openers=" \t\n('\"[{<>";
-	std::string closers=" \t\n)'\"]}><;";
-
-	if (reverse)
-	{
-		for (int index=pos; index >= 0; index--)
-		{
-			char c = line[index];
-			S32 m2 = openers.find(c);
-			if (m2 >= 0)
-			{
-				return index+1;
-			}
-		}
-		return 0; // index is -1, don't want to return that. 
-	} 
-	else
-	{
-		// adjust the search slightly, to allow matching parenthesis inside the URL
-		S32 paren_count = 0;
-		for (int index=pos; index<(S32)line.length(); index++)
-		{
-			char c = line[index];
-
-			if (c == '(')
-			{
-				paren_count++;
-			}
-			else if (c == ')')
-			{
-				if (paren_count <= 0)
-				{
-					return index;
-				}
-				else
-				{
-					paren_count--;
-				}
-			}
-			else
-			{
-				S32 m2 = closers.find(c);
-				if (m2 >= 0)
-				{
-					return index;
-				}
-			}
-		} 
-		return line.length();
-	}		
-}
-
-BOOL LLTextEditor::findHTML(const std::string &line, S32 *begin, S32 *end) const
-{
-	  
-	S32 m1,m2,m3;
-	BOOL matched = FALSE;
-	
-	m1=line.find("://",*end);
-	
-	if (m1 >= 0) //Easy match.
-	{
-		*begin = findHTMLToken(line, m1, TRUE);
-		*end   = findHTMLToken(line, m1, FALSE);
-		
-		//Load_url only handles http and https so don't hilite ftp, smb, etc.
-		m2 = line.substr(*begin,(m1 - *begin)).find("http");
-		m3 = line.substr(*begin,(m1 - *begin)).find("secondlife");
-	
-		std::string badneighbors=".,<>?';\"][}{=-+_)(*&^%$#@!~`\t\r\n\\";
-	
-		if (m2 >= 0 || m3>=0)
-		{
-			S32 bn = badneighbors.find(line.substr(m1+3,1));
-			
-			if (bn < 0)
-			{
-				matched = TRUE;
-			}
-		}
-	}
-/*	matches things like secondlife.com (no http://) needs a whitelist to really be effective.
-	else	//Harder match.
-	{
-		m1 = line.find(".",*end);
-		
-		if (m1 >= 0)
-		{
-			*end   = findHTMLToken(line, m1, FALSE);
-			*begin = findHTMLToken(line, m1, TRUE);
-			
-			m1 = line.rfind(".",*end);
-
-			if ( ( *end - m1 ) > 2 && m1 > *begin)
-			{
-				std::string badneighbors=".,<>/?';\"][}{=-+_)(*&^%$#@!~`";
-				m2 = badneighbors.find(line.substr(m1+1,1));
-				m3 = badneighbors.find(line.substr(m1-1,1));
-				if (m3<0 && m2<0)
-				{
-					matched = TRUE;
-				}
-			}
-		}
-	}
-	*/
-	
-	if (matched)
-	{
-		S32 strpos, strpos2;
-
-		std::string url     = line.substr(*begin,*end - *begin);
-		std::string slurlID = "slurl.com/secondlife/";
-		strpos = url.find(slurlID);
-		
-		if (strpos < 0)
-		{
-			slurlID="secondlife://";
-			strpos = url.find(slurlID);
-		}
-	
-		if (strpos < 0)
-		{
-			slurlID="sl://";
-			strpos = url.find(slurlID);
-		}
-	
-		if (strpos >= 0) 
-		{
-			strpos+=slurlID.length();
-			
-			while ( ( strpos2=url.find("/",strpos) ) == -1 ) 
-			{
-				if ((*end+2) >= (S32)line.length() || line.substr(*end,1) != " " )
-				{
-					matched=FALSE;
-					break;
-				}
-				
-				strpos = (*end + 1) - *begin;
-								
-				*end = findHTMLToken(line,(*begin + strpos),FALSE);
-				url = line.substr(*begin,*end - *begin);
-			}
-		}
-
-	}
-	
-	if (!matched)
-	{
-		*begin=*end=0;
-	}
-	return matched;
-}
-
-
-
 void LLTextEditor::updateAllowingLanguageInput()
 {
 	LLWindow* window = getWindow();
@@ -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 67c67d0f67..d537751130 100644
--- a/indra/llui/lltexteditor.h
+++ b/indra/llui/lltexteditor.h
@@ -44,6 +44,7 @@
 #include "lleditmenuhandler.h"
 #include "lldarray.h"
 #include "llviewborder.h" // for params
+#include "lltextbase.h"
 
 #include "llpreeditor.h"
 #include "llcontrol.h"
@@ -55,76 +56,6 @@ class LLTextCmd;
 class LLUICtrlFactory;
 class LLScrollContainer;
 
-class LLTextSegment : public LLRefCount
-{
-public:
-	LLTextSegment(S32 start, S32 end) : mStart(start), mEnd(end){};
-	virtual ~LLTextSegment();
-
-	virtual S32					getWidth(S32 first_char, S32 num_chars) const;
-	virtual S32					getOffset(S32 segment_local_x_coord, S32 start_offset, S32 num_chars, bool round) const;
-	virtual S32					getNumChars(S32 num_pixels, S32 segment_offset, S32 line_offset, S32 max_chars) const;
-	virtual void				updateLayout(const class LLTextEditor& editor);
-	virtual F32					draw(S32 start, S32 end, S32 selection_start, S32 selection_end, const LLRect& draw_rect);
-	virtual S32					getMaxHeight() const;
-	virtual bool				canEdit() const;
-	virtual void				unlinkFromDocument(class LLTextEditor* editor);
-	virtual void				linkToDocument(class LLTextEditor* editor);
-
-	virtual void				setHasMouseHover(bool hover);
-	virtual const LLColor4&		getColor() const;
-	virtual void 				setColor(const LLColor4 &color);
-	virtual const LLStyleSP		getStyle() const;
-	virtual void 				setStyle(const LLStyleSP &style);
-	virtual void				setToken( LLKeywordToken* token );
-	virtual LLKeywordToken*		getToken() const;
-	virtual BOOL				getToolTip( std::string& msg ) const;
-	virtual void				dump() const;
-
-	S32							getStart() const 					{ return mStart; }
-	void						setStart(S32 start)					{ mStart = start; }
-	S32							getEnd() const						{ return mEnd; }
-	void						setEnd( S32 end )					{ mEnd = end; }
-
-protected:
-	S32				mStart;
-	S32				mEnd;
-};
-
-class LLNormalTextSegment : public LLTextSegment
-{
-public:
-	LLNormalTextSegment( const LLStyleSP& style, S32 start, S32 end, LLTextEditor& editor );
-	LLNormalTextSegment( const LLColor4& color, S32 start, S32 end, LLTextEditor& editor, BOOL is_visible = TRUE);
-
-	/*virtual*/ S32					getWidth(S32 first_char, S32 num_chars) const;
-	/*virtual*/ S32					getOffset(S32 segment_local_x_coord, S32 start_offset, S32 num_chars, bool round) const;
-	/*virtual*/ S32					getNumChars(S32 num_pixels, S32 segment_offset, S32 line_offset, S32 max_chars) const;
-	/*virtual*/ F32					draw(S32 start, S32 end, S32 selection_start, S32 selection_end, const LLRect& draw_rect);
-	/*virtual*/ S32					getMaxHeight() const;
-	/*virtual*/ bool				canEdit() const { return true; }
-	/*virtual*/ void				setHasMouseHover(bool hover)		{ mHasMouseHover = hover; }
-	/*virtual*/ const LLColor4&		getColor() const					{ return mStyle->getColor(); }
-	/*virtual*/ void 				setColor(const LLColor4 &color)		{ mStyle->setColor(color); }
-	/*virtual*/ const LLStyleSP		getStyle() const					{ return mStyle; }
-	/*virtual*/ void 				setStyle(const LLStyleSP &style)	{ mStyle = style; }
-	/*virtual*/ void				setToken( LLKeywordToken* token )	{ mToken = token; }
-	/*virtual*/ LLKeywordToken*		getToken() const					{ return mToken; }
-	/*virtual*/ BOOL				getToolTip( std::string& msg ) const;
-	/*virtual*/ void				dump() const;
-
-protected:
-	F32				drawClippedSegment(S32 seg_start, S32 seg_end, S32 selection_start, S32 selection_end, F32 x, F32 y);
-
-	class LLTextEditor&	mEditor;
-	LLStyleSP		mStyle;
-	S32				mMaxHeight;
-	LLKeywordToken* mToken;
-	bool			mHasMouseHover;
-};
-
-typedef LLPointer<LLTextSegment> LLTextSegmentPtr;
-
 class LLInlineViewSegment : public LLTextSegment
 {
 public:
@@ -132,24 +63,22 @@ public:
 	~LLInlineViewSegment();
 	/*virtual*/ S32			getWidth(S32 first_char, S32 num_chars) const;
 	/*virtual*/ S32			getNumChars(S32 num_pixels, S32 segment_offset, S32 line_offset, S32 max_chars) const;
-	/*virtual*/ void		updateLayout(const class LLTextEditor& editor);
+	/*virtual*/ void		updateLayout(const class LLTextBase& editor);
 	/*virtual*/ F32			draw(S32 start, S32 end, S32 selection_start, S32 selection_end, const LLRect& draw_rect);
 	/*virtuaL*/ S32			getMaxHeight() const;
 	/*virtual*/ bool		canEdit() const { return false; }
-	/*virtual*/ void		unlinkFromDocument(class LLTextEditor* editor);
-	/*virtual*/ void		linkToDocument(class LLTextEditor* editor);
+	/*virtual*/ void		unlinkFromDocument(class LLTextBase* editor);
+	/*virtual*/ void		linkToDocument(class LLTextBase* editor);
 
 private:
 	LLView* mView;
 };
 
-class LLIndexSegment : public LLTextSegment
-{
-public:
-	LLIndexSegment(S32 pos) : LLTextSegment(pos, pos) {}
-};
-
-class LLTextEditor : public LLUICtrl, LLEditMenuHandler, protected LLPreeditor
+class LLTextEditor :
+	public LLTextBase,
+	public LLUICtrl,
+	private LLEditMenuHandler,
+	protected LLPreeditor
 {
 public:
 	struct Params : public LLInitParam::Block<Params, LLUICtrl::Params>
@@ -208,11 +137,8 @@ public:
 		}
 	};
 
-	typedef std::multiset<LLTextSegmentPtr, compare_segment_end> segment_set_t;
-
 	virtual ~LLTextEditor();
 
-	void	setParseHTML(BOOL parsing) {mParseHTML=parsing;}
 	void	setParseHighlights(BOOL parsing) {mParseHighlights=parsing;}
 
 	// mousehandler overrides
@@ -277,6 +203,7 @@ public:
 	BOOL			replaceText(const std::string& search_text, const std::string& replace_text, BOOL case_insensitive, BOOL wrap = TRUE);
 	void			replaceTextAll(const std::string& search_text, const std::string& replace_text, BOOL case_insensitive);
 	BOOL			hasSelection() const		{ return (mSelectionStart !=mSelectionEnd); }
+	void			replaceUrlLabel(const std::string &url, const std::string &label);
 	
 	// Undo/redo stack
 	void			blockUndo();
@@ -285,7 +212,6 @@ public:
 	virtual void	makePristine();
 	BOOL			isPristine() const;
 	BOOL			allowsEmbeddedItems() const { return mAllowEmbeddedItems; }
-	BOOL			getWordWrap() { return mWordWrap; }
 	S32				getLength() const { return getWText().length(); }
 	void			setReadOnly(bool read_only) { mReadOnly = read_only; }
 	bool			getReadOnly() { return mReadOnly; }
@@ -352,13 +278,11 @@ public:
 	const LLUUID&	getSourceID() const						{ return mSourceID; }
 
 	// Callbacks
-	static void		setURLCallbacks(void (*callback1) (const std::string& url), 
-									bool (*callback2) (const std::string& url),      
-									bool (*callback3) (const std::string& url)	) 
-									{ sURLcallback = callback1; sSecondlifeURLcallback = callback2; sSecondlifeURLcallbackRightClick = callback3;}
-
  	std::string     getText() const;
 	
+	// Callback for when a Url has been resolved by the server
+	void            onUrlLabelUpdated(const std::string &url, const std::string &label);
+
 	// Getters
 	LLWString       getWText() const;
 	llwchar			getWChar(S32 pos) const { return getWText()[pos]; }
@@ -382,8 +306,6 @@ protected:
 	void			startOfDoc();
 	void			endOfDoc();
 
-	void			getSegmentAndOffset( S32 startpos, segment_set_t::const_iterator* seg_iter, S32* offsetp ) const;
-	void			getSegmentAndOffset( S32 startpos, segment_set_t::iterator* seg_iter, S32* offsetp ) ;
 	void			drawPreeditMarker();
 
 	void			needsReflow() { mReflowNeeded = TRUE; }
@@ -399,16 +321,12 @@ protected:
 	
 	void			removeCharOrTab();
 	void			setCursorAtLocalPos(S32 x, S32 y, bool round, bool keep_cursor_offset = false);
-	S32				getDocIndexFromLocalCoord( S32 local_x, S32 local_y, BOOL round ) const;
+	/*virtual*/ S32 getDocIndexFromLocalCoord( S32 local_x, S32 local_y, BOOL round ) const;
 
 	void			indentSelectedLines( S32 spaces );
 	S32				indentLine( S32 pos, S32 spaces );
 	void			unindentLineBeforeCloseBrace();
 
-	LLTextSegmentPtr				getSegmentAtLocalPos(S32 x, S32 y);
-	segment_set_t::iterator			getSegIterContaining(S32 index);
-	segment_set_t::const_iterator	getSegIterContaining(S32 index) const;
-
 	void			reportBadKeystroke() { make_ui_sound("UISndBadKeystroke"); }
 
 	BOOL			handleNavigationKey(const KEY key, const MASK mask);
@@ -438,15 +356,9 @@ protected:
 	
 	void			findEmbeddedItemSegments(S32 start, S32 end);
 	void			insertSegment(LLTextSegmentPtr segment_to_insert);
-
 	
-	virtual BOOL	handleMouseUpOverSegment(S32 x, S32 y, MASK mask);
-
 	virtual llwchar	pasteEmbeddedItem(llwchar ext_char) { return ext_char; }
 	
-	S32				findHTMLToken(const std::string &line, S32 pos, BOOL reverse) const;
-	BOOL			findHTML(const std::string &line, S32 *begin, S32 *end) const;
-
 	// Abstract inner base class representing an undoable editor command.
 	// Concrete sub-classes can be defined for operations such as insert, remove, etc.
 	// Used as arguments to the execute() method below.
@@ -538,13 +450,8 @@ protected:
 	S32				mLastSelectionX;
 	S32				mLastSelectionY;
 
-	BOOL			mParseHTML;
 	BOOL			mParseHighlights;
-	std::string		mHTML;
 
-	segment_set_t mSegments;
-	LLTextSegmentPtr	mHoverSegment;
-	
 	// Scrollbar data
 	class DocumentPanel*	mDocumentPanel;
 	LLScrollContainer*	mScroller;
@@ -569,10 +476,10 @@ protected:
 	LLUIColor		mLinkColor;
 
 	BOOL			mReadOnly;
-	BOOL			mWordWrap;
 	BOOL			mShowLineNumbers;
 
 	void			updateSegments();
+	void			updateLinkSegments();
 
 private:
 
@@ -584,7 +491,6 @@ private:
 	virtual 		LLTextViewModel* getViewModel() const;
 	void			reflow(S32 startpos = 0);
 
-	void			clearSegments();
 	void			createDefaultSegment();
 	LLStyleSP		getDefaultStyle();
 	S32				getEditableIndex(S32 index, bool increasing_direction);
@@ -601,9 +507,6 @@ private:
 	// Data
 	//
 	LLKeywords		mKeywords;
-	static void		(*sURLcallback) (const std::string& url);
-	static bool		(*sSecondlifeURLcallback) (const std::string& url);
-	static bool		(*sSecondlifeURLcallbackRightClick) (const std::string& url);
 
 	// Concrete LLTextCmd sub-classes used by the LLTextEditor base class
 	class LLTextCmdInsert;
@@ -613,8 +516,6 @@ private:
 
 	S32				mMaxTextByteLength;		// Maximum length mText is allowed to be in bytes
 
-	const LLFontGL*	mDefaultFont;
-
 	class LLViewBorder*	mBorder;
 
 	BOOL			mBaseDocIsPristine;
diff --git a/indra/llui/llurlaction.cpp b/indra/llui/llurlaction.cpp
new file mode 100644
index 0000000000..3b689b93c0
--- /dev/null
+++ b/indra/llui/llurlaction.cpp
@@ -0,0 +1,137 @@
+/** 
+ * @file llurlaction.cpp
+ * @author Martin Reddy
+ * @brief A set of actions that can performed on Urls
+ *
+ * $LicenseInfo:firstyear=2009&license=viewergpl$
+ * 
+ * Copyright (c) 2009, Linden Research, Inc.
+ * 
+ * Second Life Viewer Source Code
+ * The source code in this file ("Source Code") is provided by Linden Lab
+ * to you under the terms of the GNU General Public License, version 2.0
+ * ("GPL"), unless you have obtained a separate licensing agreement
+ * ("Other License"), formally executed by you and Linden Lab.  Terms of
+ * the GPL can be found in doc/GPL-license.txt in this distribution, or
+ * online at http://secondlifegrid.net/programs/open_source/licensing/gplv2
+ * 
+ * There are special exceptions to the terms and conditions of the GPL as
+ * it is applied to this Source Code. View the full text of the exception
+ * in the file doc/FLOSS-exception.txt in this software distribution, or
+ * online at
+ * http://secondlifegrid.net/programs/open_source/licensing/flossexception
+ * 
+ * By copying, modifying or distributing this software, you acknowledge
+ * that you have read and understood your obligations described above,
+ * and agree to abide by those obligations.
+ * 
+ * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO
+ * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY,
+ * COMPLETENESS OR PERFORMANCE.
+ * $/LicenseInfo$
+ */
+
+#include "linden_common.h"
+
+#include "llurlaction.h"
+#include "llview.h"
+#include "llwindow.h"
+#include "llurlregistry.h"
+
+// global state for the callback functions
+void (*LLUrlAction::sOpenURLCallback) (const std::string& url) = NULL;
+void (*LLUrlAction::sOpenURLInternalCallback) (const std::string& url) = NULL;
+void (*LLUrlAction::sOpenURLExternalCallback) (const std::string& url) = NULL;
+bool (*LLUrlAction::sExecuteSLURLCallback) (const std::string& url) = NULL;
+
+
+void LLUrlAction::setOpenURLCallback(void (*cb) (const std::string& url))
+{
+	sOpenURLCallback = cb;
+}
+
+void LLUrlAction::setOpenURLInternalCallback(void (*cb) (const std::string& url))
+{
+	sOpenURLInternalCallback = cb;
+}
+
+void LLUrlAction::setOpenURLExternalCallback(void (*cb) (const std::string& url))
+{
+	sOpenURLExternalCallback = cb;
+}
+
+void LLUrlAction::setExecuteSLURLCallback(bool (*cb) (const std::string& url))
+{
+	sExecuteSLURLCallback = cb;
+}
+
+void LLUrlAction::openURL(std::string url)
+{
+	if (sOpenURLCallback)
+	{
+		(*sOpenURLCallback)(url);
+	}
+}
+
+void LLUrlAction::openURLInternal(std::string url)
+{
+	if (sOpenURLInternalCallback)
+	{
+		(*sOpenURLInternalCallback)(url);
+	}
+}
+
+void LLUrlAction::openURLExternal(std::string url)
+{
+	if (sOpenURLExternalCallback)
+	{
+		(*sOpenURLExternalCallback)(url);
+	}
+}
+
+void LLUrlAction::executeSLURL(std::string url)
+{
+	if (sExecuteSLURLCallback)
+	{
+		(*sExecuteSLURLCallback)(url);
+	}
+}
+
+void LLUrlAction::clickAction(std::string url)
+{
+	// Try to handle as SLURL first, then http Url
+	if ( (sExecuteSLURLCallback) && !(*sExecuteSLURLCallback)(url) )
+	{
+		if (sOpenURLCallback)
+		{
+			(*sOpenURLCallback)(url);
+		}
+	}
+}
+
+void LLUrlAction::teleportToLocation(std::string url)
+{
+	LLUrlMatch match;
+	if (LLUrlRegistry::instance().findUrl(url, match))
+	{
+		if (! match.getLocation().empty())
+		{
+			executeSLURL("secondlife:///app/teleport/" + match.getLocation());
+		}
+	}	
+}
+
+void LLUrlAction::copyURLToClipboard(std::string url)
+{
+	LLView::getWindow()->copyTextToClipboard(utf8str_to_wstring(url));
+}
+
+void LLUrlAction::copyLabelToClipboard(std::string url)
+{
+	LLUrlMatch match;
+	if (LLUrlRegistry::instance().findUrl(url, match))
+	{
+		LLView::getWindow()->copyTextToClipboard(utf8str_to_wstring(match.getLabel()));
+	}	
+}
+
diff --git a/indra/llui/llurlaction.h b/indra/llui/llurlaction.h
new file mode 100644
index 0000000000..6b9d565b44
--- /dev/null
+++ b/indra/llui/llurlaction.h
@@ -0,0 +1,93 @@
+/** 
+ * @file llurlaction.h
+ * @author Martin Reddy
+ * @brief A set of actions that can performed on Urls
+ *
+ * $LicenseInfo:firstyear=2009&license=viewergpl$
+ * 
+ * Copyright (c) 2009, Linden Research, Inc.
+ * 
+ * Second Life Viewer Source Code
+ * The source code in this file ("Source Code") is provided by Linden Lab
+ * to you under the terms of the GNU General Public License, version 2.0
+ * ("GPL"), unless you have obtained a separate licensing agreement
+ * ("Other License"), formally executed by you and Linden Lab.  Terms of
+ * the GPL can be found in doc/GPL-license.txt in this distribution, or
+ * online at http://secondlifegrid.net/programs/open_source/licensing/gplv2
+ * 
+ * There are special exceptions to the terms and conditions of the GPL as
+ * it is applied to this Source Code. View the full text of the exception
+ * in the file doc/FLOSS-exception.txt in this software distribution, or
+ * online at
+ * http://secondlifegrid.net/programs/open_source/licensing/flossexception
+ * 
+ * By copying, modifying or distributing this software, you acknowledge
+ * that you have read and understood your obligations described above,
+ * and agree to abide by those obligations.
+ * 
+ * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO
+ * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY,
+ * COMPLETENESS OR PERFORMANCE.
+ * $/LicenseInfo$
+ */
+
+#ifndef LL_LLURLACTION_H
+#define LL_LLURLACTION_H
+
+#include <string>
+
+///
+/// The LLUrlAction class provides a number of static functions that
+/// let you open Urls in web browsers, execute SLURLs, and copy Urls
+/// to the clipboard. Many of these functions are not available at
+/// the llui level, and must be supplied via a set of callbacks.
+///
+/// N.B. The action functions specifically do not use const ref
+/// strings so that a url parameter can be used into a boost::bind()
+/// call under situations when that input string is deallocated before
+/// the callback is executed.
+///
+class LLUrlAction
+{
+public:
+	LLUrlAction();
+
+	/// load a Url in the user's preferred web browser
+	static void openURL(std::string url);
+
+	/// load a Url in the internal Second Life web browser
+	static void openURLInternal(std::string url);
+
+	/// load a Url in the operating system's default web browser
+	static void openURLExternal(std::string url);
+
+	/// execute the given secondlife: SLURL
+	static void executeSLURL(std::string url);
+
+	/// if the Url specifies an SL location, teleport there
+	static void teleportToLocation(std::string url);
+
+	/// perform the appropriate action for left-clicking on a Url
+	static void clickAction(std::string url);
+
+	/// copy the label for a Url to the clipboard
+	static void copyLabelToClipboard(std::string url);
+
+	/// copy a Url to the clipboard
+	static void copyURLToClipboard(std::string url);
+
+	/// specify the callbacks to enable this class's functionality
+	static void	setOpenURLCallback(void (*cb) (const std::string& url));
+	static void	setOpenURLInternalCallback(void (*cb) (const std::string& url));
+	static void	setOpenURLExternalCallback(void (*cb) (const std::string& url));
+	static void	setExecuteSLURLCallback(bool (*cb) (const std::string& url));
+
+private:
+	// callbacks for operations we can perform on Urls
+	static void (*sOpenURLCallback) (const std::string& url);
+	static void (*sOpenURLInternalCallback) (const std::string& url);
+	static void (*sOpenURLExternalCallback) (const std::string& url);
+	static bool (*sExecuteSLURLCallback) (const std::string& url);
+};
+
+#endif
diff --git a/indra/llui/llurlentry.cpp b/indra/llui/llurlentry.cpp
new file mode 100644
index 0000000000..85f9064115
--- /dev/null
+++ b/indra/llui/llurlentry.cpp
@@ -0,0 +1,546 @@
+/** 
+ * @file llurlentry.cpp
+ * @author Martin Reddy
+ * @brief Describes the Url types that can be registered in LLUrlRegistry
+ *
+ * $LicenseInfo:firstyear=2009&license=viewergpl$
+ * 
+ * Copyright (c) 2009, Linden Research, Inc.
+ * 
+ * Second Life Viewer Source Code
+ * The source code in this file ("Source Code") is provided by Linden Lab
+ * to you under the terms of the GNU General Public License, version 2.0
+ * ("GPL"), unless you have obtained a separate licensing agreement
+ * ("Other License"), formally executed by you and Linden Lab.  Terms of
+ * the GPL can be found in doc/GPL-license.txt in this distribution, or
+ * online at http://secondlifegrid.net/programs/open_source/licensing/gplv2
+ * 
+ * There are special exceptions to the terms and conditions of the GPL as
+ * it is applied to this Source Code. View the full text of the exception
+ * in the file doc/FLOSS-exception.txt in this software distribution, or
+ * online at
+ * http://secondlifegrid.net/programs/open_source/licensing/flossexception
+ * 
+ * By copying, modifying or distributing this software, you acknowledge
+ * that you have read and understood your obligations described above,
+ * and agree to abide by those obligations.
+ * 
+ * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO
+ * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY,
+ * COMPLETENESS OR PERFORMANCE.
+ * $/LicenseInfo$
+ */
+
+#include "linden_common.h"
+#include "llurlentry.h"
+#include "lluri.h"
+#include "llcachename.h"
+#include "lltrans.h"
+
+LLUrlEntryBase::LLUrlEntryBase()
+{
+}
+
+LLUrlEntryBase::~LLUrlEntryBase()
+{
+}
+
+std::string LLUrlEntryBase::getUrl(const std::string &string)
+{
+	return escapeUrl(string);
+}
+
+std::string LLUrlEntryBase::getIDStringFromUrl(const std::string &url) const
+{
+	// return the id from a SLURL in the format /app/{cmd}/{id}/about
+	LLURI uri(url);
+	LLSD path_array = uri.pathArray();
+	if (path_array.size() == 4) 
+	{
+		return path_array.get(2).asString();
+	}
+	return "";
+}
+
+std::string LLUrlEntryBase::unescapeUrl(const std::string &url) const
+{
+	return LLURI::unescape(url);
+}
+
+std::string LLUrlEntryBase::escapeUrl(const std::string &url) const
+{
+	static std::string no_escape_chars;
+	static bool initialized = false;
+	if (!initialized)
+	{
+		no_escape_chars = 
+			"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+			"abcdefghijklmnopqrstuvwxyz"
+			"0123456789"
+			"-._~!$?&()*+,@:;=/%";
+
+		std::sort(no_escape_chars.begin(), no_escape_chars.end());
+		initialized = true;
+	}
+	return LLURI::escape(url, no_escape_chars, true);
+}
+
+std::string LLUrlEntryBase::getLabelFromWikiLink(const std::string &url)
+{
+	// return the label part from [http://www.example.org Label]
+	const char *text = url.c_str();
+	S32 start = 0;
+	while (! isspace(text[start]))
+	{
+		start++;
+	}
+	while (text[start] == ' ' || text[start] == '\t')
+	{
+		start++;
+	}
+	return url.substr(start, url.size()-start-1);
+}
+
+std::string LLUrlEntryBase::getUrlFromWikiLink(const std::string &string)
+{
+	// return the url part from [http://www.example.org Label]
+	const char *text = string.c_str();
+	S32 end = 0;
+	while (! isspace(text[end]))
+	{
+		end++;
+	}
+	return escapeUrl(string.substr(1, end-1));
+}
+
+void LLUrlEntryBase::addObserver(const std::string &id,
+								 const std::string &url,
+								 const LLUrlLabelCallback &cb)
+{
+	// add a callback to be notified when we have a label for the uuid
+	LLUrlEntryObserver observer;
+	observer.url = url;
+	observer.signal = new LLUrlLabelSignal();
+	if (observer.signal)
+	{
+		observer.signal->connect(cb);
+		mObservers.insert(std::pair<std::string, LLUrlEntryObserver>(id, observer));
+	}
+}
+ 
+void LLUrlEntryBase::callObservers(const std::string &id, const std::string &label)
+{
+	// notify all callbacks waiting on the given uuid
+	std::multimap<std::string, LLUrlEntryObserver>::iterator it;
+	for (it = mObservers.find(id); it != mObservers.end();)
+	{
+		// call the callback - give it the new label
+		LLUrlEntryObserver &observer = it->second;
+		(*observer.signal)(it->second.url, label);
+		// then remove the signal - we only need to call it once
+		delete observer.signal;
+		mObservers.erase(it++);
+	}
+}
+
+//
+// LLUrlEntryHTTP Describes generic http: and https: Urls
+//
+LLUrlEntryHTTP::LLUrlEntryHTTP()
+{
+	mPattern = boost::regex("https?://([-\\w\\.]+)+(:\\d+)?(:\\w+)?(@\\d+)?(@\\w+)?/?\\S*",
+							boost::regex::perl|boost::regex::icase);
+	mMenuName = "menu_url_http.xml";
+	mTooltip = LLTrans::getString("TooltipHttpUrl");
+	//mIcon = "gear.tga";
+}
+
+std::string LLUrlEntryHTTP::getLabel(const std::string &url, const LLUrlLabelCallback &cb)
+{
+	return unescapeUrl(url);
+}
+
+//
+// LLUrlEntryHTTP Describes generic http: and https: Urls with custom label
+// We use the wikipedia syntax of [http://www.example.org Text]
+//
+LLUrlEntryHTTPLabel::LLUrlEntryHTTPLabel()
+{
+	mPattern = boost::regex("\\[https?://\\S+[ \t]+[^\\]]+\\]",
+							boost::regex::perl|boost::regex::icase);
+	mMenuName = "menu_url_http.xml";
+	mTooltip = LLTrans::getString("TooltipHttpUrl");
+}
+
+std::string LLUrlEntryHTTPLabel::getLabel(const std::string &url, const LLUrlLabelCallback &cb)
+{
+	return getLabelFromWikiLink(url);
+}
+
+std::string LLUrlEntryHTTPLabel::getUrl(const std::string &string)
+{
+	return getUrlFromWikiLink(string);
+}
+
+//
+// LLUrlEntrySLURL Describes generic http: and https: Urls
+//
+LLUrlEntrySLURL::LLUrlEntrySLURL()
+{
+	// see http://slurl.com/about.php for details on the SLURL format
+	mPattern = boost::regex("http://slurl.com/secondlife/\\S+/?(\\d+)?/?(\\d+)?/?(\\d+)?/?\\S*",
+							boost::regex::perl|boost::regex::icase);
+	mMenuName = "menu_url_slurl.xml";
+	mTooltip = LLTrans::getString("TooltipSLURL");
+}
+
+std::string LLUrlEntrySLURL::getLabel(const std::string &url, const LLUrlLabelCallback &cb)
+{
+	//
+	// we handle SLURLs in the following formats:
+	//   - http://slurl.com/secondlife/Place/X/Y/Z
+	//   - http://slurl.com/secondlife/Place/X/Y
+	//   - http://slurl.com/secondlife/Place/X
+	//   - http://slurl.com/secondlife/Place
+	//
+	LLURI uri(url);
+	LLSD path_array = uri.pathArray();
+	S32 path_parts = path_array.size();
+	if (path_parts == 5)
+	{
+		// handle slurl with (X,Y,Z) coordinates
+		std::string location = unescapeUrl(path_array[path_parts-4]);
+		std::string x = path_array[path_parts-3];
+		std::string y = path_array[path_parts-2];
+		std::string z = path_array[path_parts-1];
+		return location + " (" + x + "," + y + "," + z + ")";
+	}
+	else if (path_parts == 4)
+	{
+		// handle slurl with (X,Y) coordinates
+		std::string location = unescapeUrl(path_array[path_parts-3]);
+		std::string x = path_array[path_parts-2];
+		std::string y = path_array[path_parts-1];
+		return location + " (" + x + "," + y + ")";
+	}
+	else if (path_parts == 3)
+	{
+		// handle slurl with (X) coordinate
+		std::string location = unescapeUrl(path_array[path_parts-2]);
+		std::string x = path_array[path_parts-1];
+		return location + " (" + x + ")";
+	}
+	else if (path_parts == 2)
+	{
+		// handle slurl with no coordinates
+		std::string location = unescapeUrl(path_array[path_parts-1]);
+		return location;
+	}
+
+	return url;
+}
+
+std::string LLUrlEntrySLURL::getLocation(const std::string &url) const
+{
+	// return the part of the Url after slurl.com/secondlife/
+	const std::string search_string = "secondlife";
+	size_t pos = url.find(search_string);
+	if (pos == std::string::npos)
+	{
+		return "";
+	}
+
+	pos += search_string.size() + 1;
+	return url.substr(pos, url.size() - pos);
+}
+
+//
+// LLUrlEntryAgent Describes a Second Life agent Url, e.g.,
+// secondlife:///app/agent/0e346d8b-4433-4d66-a6b0-fd37083abc4c/about
+//
+LLUrlEntryAgent::LLUrlEntryAgent()
+{
+	mPattern = boost::regex("secondlife:///app/agent/[\\da-f-]+/about",
+							boost::regex::perl|boost::regex::icase);
+	mMenuName = "menu_url_agent.xml";
+	mTooltip = LLTrans::getString("TooltipAgentUrl");
+}
+
+void LLUrlEntryAgent::onAgentNameReceived(const LLUUID& id,
+										  const std::string& first,
+										  const std::string& last,
+										  BOOL is_group)
+{
+	// received the agent name from the server - tell our observers
+	callObservers(id.asString(), first + " " + last);
+}
+
+std::string LLUrlEntryAgent::getLabel(const std::string &url, const LLUrlLabelCallback &cb)
+{
+	std::string id = getIDStringFromUrl(url);
+	if (gCacheName && ! id.empty())
+	{
+		LLUUID uuid(id);
+		std::string full_name;
+		if (gCacheName->getFullName(uuid, full_name))
+		{
+			return full_name;
+		}
+		else
+		{
+			gCacheName->get(uuid, FALSE, boost::bind(&LLUrlEntryAgent::onAgentNameReceived, this, _1, _2, _3, _4));
+			addObserver(id, url, cb);
+		}
+	}
+
+	return unescapeUrl(url);
+}
+
+//
+// LLUrlEntryGroup Describes a Second Life group Url, e.g.,
+// secondlife:///app/group/00005ff3-4044-c79f-9de8-fb28ae0df991/about
+//
+LLUrlEntryGroup::LLUrlEntryGroup()
+{
+	mPattern = boost::regex("secondlife:///app/group/[\\da-f-]+/about",
+							boost::regex::perl|boost::regex::icase);
+	mMenuName = "menu_url_group.xml";
+	mTooltip = LLTrans::getString("TooltipGroupUrl");
+}
+
+void LLUrlEntryGroup::onGroupNameReceived(const LLUUID& id,
+										  const std::string& first,
+										  const std::string& last,
+										  BOOL is_group)
+{
+	// received the group name from the server - tell our observers
+	callObservers(id.asString(), first);
+}
+
+std::string LLUrlEntryGroup::getLabel(const std::string &url, const LLUrlLabelCallback &cb)
+{
+	std::string id = getIDStringFromUrl(url);
+	if (gCacheName && ! id.empty())
+	{
+		LLUUID uuid(id);
+		std::string group_name;
+		if (gCacheName->getGroupName(uuid, group_name))
+		{
+			return group_name;
+		}
+		else
+		{
+			gCacheName->get(uuid, TRUE, boost::bind(&LLUrlEntryGroup::onGroupNameReceived, this, _1, _2, _3, _4));
+			addObserver(id, url, cb);
+		}
+	}
+
+	return unescapeUrl(url);
+}
+
+///
+/// LLUrlEntryEvent Describes a Second Life event Url, e.g.,
+/// secondlife:///app/event/700727/about
+///
+LLUrlEntryEvent::LLUrlEntryEvent()
+{
+	mPattern = boost::regex("secondlife:///app/event/[\\da-f-]+/about",
+							boost::regex::perl|boost::regex::icase);
+	mMenuName = "menu_url_event.xml";
+	mTooltip = LLTrans::getString("TooltipEventUrl");
+}
+
+std::string LLUrlEntryEvent::getLabel(const std::string &url, const LLUrlLabelCallback &cb)
+{
+	return unescapeUrl(url);
+}
+
+///
+/// LLUrlEntryClassified Describes a Second Life classified Url, e.g.,
+/// secondlife:///app/classified/00128854-c36a-5649-7ca6-5dfaa7514ab2/about
+///
+LLUrlEntryClassified::LLUrlEntryClassified()
+{
+	mPattern = boost::regex("secondlife:///app/classified/[\\da-f-]+/about",
+							boost::regex::perl|boost::regex::icase);
+	mMenuName = "menu_url_classified.xml";
+	mTooltip = LLTrans::getString("TooltipClassifiedUrl");
+}
+
+std::string LLUrlEntryClassified::getLabel(const std::string &url, const LLUrlLabelCallback &cb)
+{
+	return unescapeUrl(url);
+}
+
+///
+/// LLUrlEntryParcel Describes a Second Life parcel Url, e.g.,
+/// secondlife:///app/parcel/0000060e-4b39-e00b-d0c3-d98b1934e3a8/about
+///
+LLUrlEntryParcel::LLUrlEntryParcel()
+{
+	mPattern = boost::regex("secondlife:///app/parcel/[\\da-f-]+/about",
+							boost::regex::perl|boost::regex::icase);
+	mMenuName = "menu_url_parcel.xml";
+	mTooltip = LLTrans::getString("TooltipParcelUrl");
+}
+
+std::string LLUrlEntryParcel::getLabel(const std::string &url, const LLUrlLabelCallback &cb)
+{
+	return unescapeUrl(url);
+}
+
+//
+// LLUrlEntryTeleport Describes a Second Life teleport Url, e.g.,
+// secondlife:///app/teleport/Ahern/50/50/50/
+//
+LLUrlEntryTeleport::LLUrlEntryTeleport()
+{
+	mPattern = boost::regex("secondlife:///app/teleport/\\S+(/\\d+)?(/\\d+)?(/\\d+)?/?\\S*",
+							boost::regex::perl|boost::regex::icase);
+	mMenuName = "menu_url_teleport.xml";
+	mTooltip = LLTrans::getString("TooltipTeleportUrl");
+}
+
+std::string LLUrlEntryTeleport::getLabel(const std::string &url, const LLUrlLabelCallback &cb)
+{
+	//
+	// we handle teleport SLURLs in the following formats:
+	//   - secondlife:///app/teleport/Place/X/Y/Z
+	//   - secondlife:///app/teleport/Place/X/Y
+	//   - secondlife:///app/teleport/Place/X
+	//   - secondlife:///app/teleport/Place
+	//
+	LLURI uri(url);
+	LLSD path_array = uri.pathArray();
+	S32 path_parts = path_array.size();
+	if (path_parts == 6)
+	{
+		// handle teleport url with (X,Y,Z) coordinates
+		std::string location = unescapeUrl(path_array[path_parts-4]);
+		std::string x = path_array[path_parts-3];
+		std::string y = path_array[path_parts-2];
+		std::string z = path_array[path_parts-1];
+		return "Teleport to " + location + " (" + x + "," + y + "," + z + ")";
+	}
+	else if (path_parts == 5)
+	{
+		// handle teleport url with (X,Y) coordinates
+		std::string location = unescapeUrl(path_array[path_parts-3]);
+		std::string x = path_array[path_parts-2];
+		std::string y = path_array[path_parts-1];
+		return "Teleport to " + location + " (" + x + "," + y + ")";
+	}
+	else if (path_parts == 4)
+	{
+		// handle teleport url with (X) coordinate only
+		std::string location = unescapeUrl(path_array[path_parts-2]);
+		std::string x = path_array[path_parts-1];
+		return "Teleport to " + location + " (" + x + ")";
+	}
+	else if (path_parts == 3)
+	{
+		// handle teleport url with no coordinates
+		std::string location = unescapeUrl(path_array[path_parts-1]);
+		return "Teleport to " + location;
+	}
+
+	return url;
+}
+
+std::string LLUrlEntryTeleport::getLocation(const std::string &url) const
+{
+	// return the part of the Url after ///app/teleport
+	const std::string search_string = "teleport";
+	size_t pos = url.find(search_string);
+	if (pos == std::string::npos)
+	{
+		return "";
+	}
+
+	pos += search_string.size() + 1;
+	return url.substr(pos, url.size() - pos);
+}
+
+///
+/// LLUrlEntryObjectIM Describes a Second Life object instant msg Url, e.g.,
+/// secondlife:///app/objectim/<sessionid>
+///
+LLUrlEntryObjectIM::LLUrlEntryObjectIM()
+{
+	mPattern = boost::regex("secondlife:///app/objectim/[\\da-f-]+\\??\\S*",
+							boost::regex::perl|boost::regex::icase);
+	mMenuName = "menu_url_objectim.xml";
+	mTooltip = LLTrans::getString("TooltipObjectIMUrl");
+}
+
+std::string LLUrlEntryObjectIM::getLabel(const std::string &url, const LLUrlLabelCallback &cb)
+{
+	LLURI uri(url);
+	LLSD params = uri.queryMap();
+	if (params.has("name"))
+	{
+		// look for a ?name=<obj-name> param in the url
+		// and use that as the label if present.
+		std::string name = params.get("name");
+		LLStringUtil::trim(name);
+		if (name.empty())
+		{
+			name = LLTrans::getString("Unnamed");
+		}
+		return name;
+	}
+
+	return unescapeUrl(url);
+}
+
+std::string LLUrlEntryObjectIM::getLocation(const std::string &url) const
+{
+	LLURI uri(url);
+	LLSD params = uri.queryMap();
+	if (params.has("slurl"))
+	{
+		return params.get("slurl");
+	}
+
+	return "";
+}
+
+//
+// LLUrlEntrySL Describes a generic SLURL, e.g., a Url that starts
+// with secondlife:// (used as a catch-all for cases not matched above)
+//
+LLUrlEntrySL::LLUrlEntrySL()
+{
+	mPattern = boost::regex("secondlife://(\\w+)?(:\\d+)?/\\S+",
+							boost::regex::perl|boost::regex::icase);
+	mMenuName = "menu_url_slapp.xml";
+	mTooltip = LLTrans::getString("TooltipSLAPP");
+}
+
+std::string LLUrlEntrySL::getLabel(const std::string &url, const LLUrlLabelCallback &cb)
+{
+	return unescapeUrl(url);
+}
+
+//
+// LLUrlEntrySLLabel Describes a generic SLURL, e.g., a Url that starts
+/// with secondlife:// with the ability to specify a custom label.
+//
+LLUrlEntrySLLabel::LLUrlEntrySLLabel()
+{
+	mPattern = boost::regex("\\[secondlife://\\S+[ \t]+[^\\]]+\\]",
+							boost::regex::perl|boost::regex::icase);
+	mMenuName = "menu_url_slapp.xml";
+	mTooltip = LLTrans::getString("TooltipSLAPP");
+}
+
+std::string LLUrlEntrySLLabel::getLabel(const std::string &url, const LLUrlLabelCallback &cb)
+{
+	return getLabelFromWikiLink(url);
+}
+
+std::string LLUrlEntrySLLabel::getUrl(const std::string &string)
+{
+	return getUrlFromWikiLink(string);
+}
+
diff --git a/indra/llui/llurlentry.h b/indra/llui/llurlentry.h
new file mode 100644
index 0000000000..f3e76dbec0
--- /dev/null
+++ b/indra/llui/llurlentry.h
@@ -0,0 +1,252 @@
+/** 
+ * @file llurlentry.h
+ * @author Martin Reddy
+ * @brief Describes the Url types that can be registered in LLUrlRegistry
+ *
+ * $LicenseInfo:firstyear=2009&license=viewergpl$
+ * 
+ * Copyright (c) 2009, Linden Research, Inc.
+ * 
+ * Second Life Viewer Source Code
+ * The source code in this file ("Source Code") is provided by Linden Lab
+ * to you under the terms of the GNU General Public License, version 2.0
+ * ("GPL"), unless you have obtained a separate licensing agreement
+ * ("Other License"), formally executed by you and Linden Lab.  Terms of
+ * the GPL can be found in doc/GPL-license.txt in this distribution, or
+ * online at http://secondlifegrid.net/programs/open_source/licensing/gplv2
+ * 
+ * There are special exceptions to the terms and conditions of the GPL as
+ * it is applied to this Source Code. View the full text of the exception
+ * in the file doc/FLOSS-exception.txt in this software distribution, or
+ * online at
+ * http://secondlifegrid.net/programs/open_source/licensing/flossexception
+ * 
+ * By copying, modifying or distributing this software, you acknowledge
+ * that you have read and understood your obligations described above,
+ * and agree to abide by those obligations.
+ * 
+ * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO
+ * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY,
+ * COMPLETENESS OR PERFORMANCE.
+ * $/LicenseInfo$
+ */
+
+#ifndef LL_LLURLENTRY_H
+#define LL_LLURLENTRY_H
+
+#include "lluuid.h"
+
+#include <boost/signals2.hpp>
+#include <boost/regex.hpp>
+#include <string>
+#include <map>
+
+typedef boost::signals2::signal<void (const std::string& url,
+									  const std::string& label)> LLUrlLabelSignal;
+typedef LLUrlLabelSignal::slot_type LLUrlLabelCallback;
+
+///
+/// LLUrlEntryBase is the base class of all Url types registered in the 
+/// LLUrlRegistry. Each derived classes provides a regular expression
+/// to match the Url type (e.g., http://... or secondlife://...) along
+/// with an optional icon to display next to instances of the Url in
+/// a text display and a XUI file to use for any context menu popup.
+/// Functions are also provided to compute an appropriate label and
+/// tooltip/status bar text for the Url.
+///
+/// Some derived classes of LLUrlEntryBase may wish to compute an
+/// appropriate label for a Url by asking the server for information.
+/// You must therefore provide a callback method, so that you can be
+/// notified when an updated label has been received from the server.
+/// This label should then be used to replace any previous label
+/// that you received from getLabel() for the Url in question.
+///
+class LLUrlEntryBase
+{
+public:
+	LLUrlEntryBase();
+	virtual ~LLUrlEntryBase();
+	
+	/// Return the regex pattern that matches this Url 
+	boost::regex getPattern() const { return mPattern; }
+
+	/// Return the url from a string that matched the regex
+	virtual std::string getUrl(const std::string &string);
+
+	/// Given a matched Url, return a label for the Url
+	virtual std::string getLabel(const std::string &url, const LLUrlLabelCallback &cb) { return url; }
+
+	/// Return an icon that can be displayed next to Urls of this type
+	const std::string &getIcon() const { return mIcon; }
+
+	/// Given a matched Url, return a tooltip string for the hyperlink
+	std::string getTooltip() const { return mTooltip; }
+
+	/// Return the name of a XUI file containing the context menu items
+	const std::string getMenuName() const { return mMenuName; }
+
+	/// Return the name of a SL location described by this Url, if any
+	virtual std::string getLocation(const std::string &url) const { return ""; }
+
+protected:
+	std::string getIDStringFromUrl(const std::string &url) const;
+	std::string escapeUrl(const std::string &url) const;
+	std::string unescapeUrl(const std::string &url) const;
+	std::string getLabelFromWikiLink(const std::string &url);
+	std::string getUrlFromWikiLink(const std::string &string);
+	void addObserver(const std::string &id, const std::string &url, const LLUrlLabelCallback &cb); 
+	void callObservers(const std::string &id, const std::string &label);
+
+	typedef struct {
+		std::string url;
+		LLUrlLabelSignal *signal;
+	} LLUrlEntryObserver;
+
+	boost::regex                                   mPattern;
+	std::string                                    mIcon;
+	std::string                                    mMenuName;
+	std::string                                    mTooltip;
+	std::multimap<std::string, LLUrlEntryObserver> mObservers;
+};
+
+///
+/// LLUrlEntryHTTP Describes generic http: and https: Urls
+///
+class LLUrlEntryHTTP : public LLUrlEntryBase
+{
+public:
+	LLUrlEntryHTTP();
+	/*virtual*/ std::string getLabel(const std::string &url, const LLUrlLabelCallback &cb);
+};
+
+///
+/// LLUrlEntryHTTPLabel Describes generic http: and https: Urls with custom labels
+///
+class LLUrlEntryHTTPLabel : public LLUrlEntryBase
+{
+public:
+	LLUrlEntryHTTPLabel();
+	/*virtual*/ std::string getLabel(const std::string &url, const LLUrlLabelCallback &cb);
+	/*virtual*/ std::string getUrl(const std::string &string);
+};
+
+///
+/// LLUrlEntrySLURL Describes http://slurl.com/... Urls
+///
+class LLUrlEntrySLURL : public LLUrlEntryBase
+{
+public:
+	LLUrlEntrySLURL();
+	/*virtual*/ std::string getLabel(const std::string &url, const LLUrlLabelCallback &cb);
+	/*virtual*/ std::string getLocation(const std::string &url) const;
+};
+
+///
+/// LLUrlEntryAgent Describes a Second Life agent Url, e.g.,
+/// secondlife:///app/agent/0e346d8b-4433-4d66-a6b0-fd37083abc4c/about
+///
+class LLUrlEntryAgent : public LLUrlEntryBase
+{
+public:
+	LLUrlEntryAgent();
+	/*virtual*/ std::string getLabel(const std::string &url, const LLUrlLabelCallback &cb);
+private:
+	void onAgentNameReceived(const LLUUID& id, const std::string& first,
+							 const std::string& last, BOOL is_group);
+};
+
+///
+/// LLUrlEntryGroup Describes a Second Life group Url, e.g.,
+/// secondlife:///app/group/00005ff3-4044-c79f-9de8-fb28ae0df991/about
+///
+class LLUrlEntryGroup : public LLUrlEntryBase
+{
+public:
+	LLUrlEntryGroup();
+	/*virtual*/ std::string getLabel(const std::string &url, const LLUrlLabelCallback &cb);
+private:
+	void onGroupNameReceived(const LLUUID& id, const std::string& first,
+							 const std::string& last, BOOL is_group);
+};
+
+///
+/// LLUrlEntryEvent Describes a Second Life event Url, e.g.,
+/// secondlife:///app/event/700727/about
+///
+class LLUrlEntryEvent : public LLUrlEntryBase
+{
+public:
+	LLUrlEntryEvent();
+	/*virtual*/ std::string getLabel(const std::string &url, const LLUrlLabelCallback &cb);
+};
+
+///
+/// LLUrlEntryClassified Describes a Second Life classified Url, e.g.,
+/// secondlife:///app/classified/00128854-c36a-5649-7ca6-5dfaa7514ab2/about
+///
+class LLUrlEntryClassified : public LLUrlEntryBase
+{
+public:
+	LLUrlEntryClassified();
+	/*virtual*/ std::string getLabel(const std::string &url, const LLUrlLabelCallback &cb);
+};
+
+///
+/// LLUrlEntryParcel Describes a Second Life parcel Url, e.g.,
+/// secondlife:///app/parcel/0000060e-4b39-e00b-d0c3-d98b1934e3a8/about
+///
+class LLUrlEntryParcel : public LLUrlEntryBase
+{
+public:
+	LLUrlEntryParcel();
+	/*virtual*/ std::string getLabel(const std::string &url, const LLUrlLabelCallback &cb);
+};
+
+///
+/// LLUrlEntryTeleport Describes a Second Life teleport Url, e.g.,
+/// secondlife:///app/teleport/Ahern/50/50/50/
+///
+class LLUrlEntryTeleport : public LLUrlEntryBase
+{
+public:
+	LLUrlEntryTeleport();
+	/*virtual*/ std::string getLabel(const std::string &url, const LLUrlLabelCallback &cb);
+	/*virtual*/ std::string getLocation(const std::string &url) const;
+};
+
+///
+/// LLUrlEntryObjectIM Describes a Second Life object instant msg Url, e.g.,
+/// secondlife:///app/objectim/<sessionid>?name=Foo
+///
+class LLUrlEntryObjectIM : public LLUrlEntryBase
+{
+public:
+	LLUrlEntryObjectIM();
+	/*virtual*/ std::string getLabel(const std::string &url, const LLUrlLabelCallback &cb);
+	/*virtual*/ std::string getLocation(const std::string &url) const;
+};
+
+///
+/// LLUrlEntrySL Describes a generic SLURL, e.g., a Url that starts
+/// with secondlife:// (used as a catch-all for cases not matched above)
+///
+class LLUrlEntrySL : public LLUrlEntryBase
+{
+public:
+	LLUrlEntrySL();
+	/*virtual*/ std::string getLabel(const std::string &url, const LLUrlLabelCallback &cb);
+};
+
+///
+/// LLUrlEntrySLLabel Describes a generic SLURL, e.g., a Url that starts
+/// with secondlife:// with the ability to specify a custom label.
+///
+class LLUrlEntrySLLabel : public LLUrlEntryBase
+{
+public:
+	LLUrlEntrySLLabel();
+	/*virtual*/ std::string getLabel(const std::string &url, const LLUrlLabelCallback &cb);
+	/*virtual*/ std::string getUrl(const std::string &string);
+};
+
+#endif
diff --git a/indra/llui/llurlmatch.cpp b/indra/llui/llurlmatch.cpp
new file mode 100644
index 0000000000..7eec4c4a65
--- /dev/null
+++ b/indra/llui/llurlmatch.cpp
@@ -0,0 +1,61 @@
+/** 
+ * @file llurlmatch.cpp
+ * @author Martin Reddy
+ * @brief Specifies a matched Url in a string, as returned by LLUrlRegistry
+ *
+ * $LicenseInfo:firstyear=2009&license=viewergpl$
+ * 
+ * Copyright (c) 2009, Linden Research, Inc.
+ * 
+ * Second Life Viewer Source Code
+ * The source code in this file ("Source Code") is provided by Linden Lab
+ * to you under the terms of the GNU General Public License, version 2.0
+ * ("GPL"), unless you have obtained a separate licensing agreement
+ * ("Other License"), formally executed by you and Linden Lab.  Terms of
+ * the GPL can be found in doc/GPL-license.txt in this distribution, or
+ * online at http://secondlifegrid.net/programs/open_source/licensing/gplv2
+ * 
+ * There are special exceptions to the terms and conditions of the GPL as
+ * it is applied to this Source Code. View the full text of the exception
+ * in the file doc/FLOSS-exception.txt in this software distribution, or
+ * online at
+ * http://secondlifegrid.net/programs/open_source/licensing/flossexception
+ * 
+ * By copying, modifying or distributing this software, you acknowledge
+ * that you have read and understood your obligations described above,
+ * and agree to abide by those obligations.
+ * 
+ * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO
+ * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY,
+ * COMPLETENESS OR PERFORMANCE.
+ * $/LicenseInfo$
+ */
+
+#include "linden_common.h"
+#include "llurlmatch.h"
+
+LLUrlMatch::LLUrlMatch() :
+	mStart(0),
+	mEnd(0),
+	mUrl(""),
+	mLabel(""),
+	mTooltip(""),
+	mIcon(""),
+	mMenuName("")
+{
+}
+
+void LLUrlMatch::setValues(U32 start, U32 end, const std::string &url,
+						   const std::string &label, const std::string &tooltip,
+						   const std::string &icon, const std::string &menu,
+						   const std::string &location)
+{
+	mStart = start;
+	mEnd = end;
+	mUrl = url;
+	mLabel = label;
+	mTooltip = tooltip;
+	mIcon = icon;
+	mMenuName = menu;
+	mLocation = location;
+}
diff --git a/indra/llui/llurlmatch.h b/indra/llui/llurlmatch.h
new file mode 100644
index 0000000000..0711e41443
--- /dev/null
+++ b/indra/llui/llurlmatch.h
@@ -0,0 +1,98 @@
+/** 
+ * @file llurlmatch.h
+ * @author Martin Reddy
+ * @brief Specifies a matched Url in a string, as returned by LLUrlRegistry
+ *
+ * $LicenseInfo:firstyear=2009&license=viewergpl$
+ * 
+ * Copyright (c) 2009, Linden Research, Inc.
+ * 
+ * Second Life Viewer Source Code
+ * The source code in this file ("Source Code") is provided by Linden Lab
+ * to you under the terms of the GNU General Public License, version 2.0
+ * ("GPL"), unless you have obtained a separate licensing agreement
+ * ("Other License"), formally executed by you and Linden Lab.  Terms of
+ * the GPL can be found in doc/GPL-license.txt in this distribution, or
+ * online at http://secondlifegrid.net/programs/open_source/licensing/gplv2
+ * 
+ * There are special exceptions to the terms and conditions of the GPL as
+ * it is applied to this Source Code. View the full text of the exception
+ * in the file doc/FLOSS-exception.txt in this software distribution, or
+ * online at
+ * http://secondlifegrid.net/programs/open_source/licensing/flossexception
+ * 
+ * By copying, modifying or distributing this software, you acknowledge
+ * that you have read and understood your obligations described above,
+ * and agree to abide by those obligations.
+ * 
+ * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO
+ * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY,
+ * COMPLETENESS OR PERFORMANCE.
+ * $/LicenseInfo$
+ */
+
+#ifndef LL_LLURLMATCH_H
+#define LL_LLURLMATCH_H
+
+#include "linden_common.h"
+
+#include <string>
+#include <vector>
+
+///
+/// LLUrlMatch describes a single Url that was matched within a string by 
+/// the LLUrlRegistry::findUrl() method. It includes the actual Url that
+/// was matched along with its first/last character offset in the string.
+/// An alternate label is also provided for creating a hyperlink, as well
+/// as tooltip/status text, an icon, and a XUI file for a context menu
+/// that can be used in a popup for a Url (e.g., Open, Copy URL, etc.)
+///
+class LLUrlMatch
+{
+public:
+	LLUrlMatch();
+
+	/// return true if this object does not contain a valid Url match yet
+	bool empty() const { return mUrl.empty(); }
+
+	/// return the offset in the string for the first character of the Url
+	U32 getStart() const { return mStart; }
+
+	/// return the offset in the string for the last character of the Url
+	U32 getEnd() const { return mEnd; }
+
+	/// return the Url that has been matched in the input string
+	const std::string &getUrl() const { return mUrl; }
+
+	/// return a label that can be used for the display of this Url
+	const std::string &getLabel() const { return mLabel; }
+
+	/// return a message that could be displayed in a tooltip or status bar
+	const std::string &getTooltip() const { return mTooltip; }
+
+	/// return the filename for an icon that can be displayed next to this Url
+	const std::string &getIcon() const { return mIcon; }
+
+	/// Return the name of a XUI file containing the context menu items
+	const std::string getMenuName() const { return mMenuName; }
+
+	/// return the SL location that this Url describes, or "" if none.
+	const std::string &getLocation() const { return mLocation; }
+
+	/// Change the contents of this match object (used by LLUrlRegistry)
+	void setValues(U32 start, U32 end, const std::string &url, const std::string &label,
+	               const std::string &tooltip, const std::string &icon,
+				   const std::string &menu, const std::string &location);
+
+private:
+	U32         mStart;
+	U32         mEnd;
+	std::string mUrl;
+	std::string mLabel;
+	std::string mTooltip;
+	std::string mIcon;
+	std::string mMenuName;
+	std::string mLocation;
+};
+
+#endif
diff --git a/indra/llui/llurlregistry.cpp b/indra/llui/llurlregistry.cpp
new file mode 100644
index 0000000000..938375ad13
--- /dev/null
+++ b/indra/llui/llurlregistry.cpp
@@ -0,0 +1,165 @@
+/** 
+ * @file llurlregistry.cpp
+ * @author Martin Reddy
+ * @brief Contains a set of Url types that can be matched in a string
+ *
+ * $LicenseInfo:firstyear=2009&license=viewergpl$
+ * 
+ * Copyright (c) 2009, Linden Research, Inc.
+ * 
+ * Second Life Viewer Source Code
+ * The source code in this file ("Source Code") is provided by Linden Lab
+ * to you under the terms of the GNU General Public License, version 2.0
+ * ("GPL"), unless you have obtained a separate licensing agreement
+ * ("Other License"), formally executed by you and Linden Lab.  Terms of
+ * the GPL can be found in doc/GPL-license.txt in this distribution, or
+ * online at http://secondlifegrid.net/programs/open_source/licensing/gplv2
+ * 
+ * There are special exceptions to the terms and conditions of the GPL as
+ * it is applied to this Source Code. View the full text of the exception
+ * in the file doc/FLOSS-exception.txt in this software distribution, or
+ * online at
+ * http://secondlifegrid.net/programs/open_source/licensing/flossexception
+ * 
+ * By copying, modifying or distributing this software, you acknowledge
+ * that you have read and understood your obligations described above,
+ * and agree to abide by those obligations.
+ * 
+ * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO
+ * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY,
+ * COMPLETENESS OR PERFORMANCE.
+ * $/LicenseInfo$
+ */
+
+#include "linden_common.h"
+#include "llurlregistry.h"
+
+#include <boost/regex.hpp>
+
+// default dummy callback that ignores any label updates from the server
+void LLUrlRegistryNullCallback(const std::string &url, const std::string &label)
+{
+}
+
+LLUrlRegistry::LLUrlRegistry()
+{
+	// Urls are matched in the order that they were registered
+	registerUrl(new LLUrlEntrySLURL());
+	registerUrl(new LLUrlEntryHTTP());
+	registerUrl(new LLUrlEntryHTTPLabel());
+	registerUrl(new LLUrlEntryAgent());
+	registerUrl(new LLUrlEntryGroup());
+	registerUrl(new LLUrlEntryEvent());
+	registerUrl(new LLUrlEntryClassified());
+	registerUrl(new LLUrlEntryParcel());
+	registerUrl(new LLUrlEntryTeleport());
+	registerUrl(new LLUrlEntryObjectIM());
+	registerUrl(new LLUrlEntrySL());
+	registerUrl(new LLUrlEntrySLLabel());
+}
+
+LLUrlRegistry::~LLUrlRegistry()
+{
+	// free all of the LLUrlEntryBase objects we are holding
+	std::vector<LLUrlEntryBase *>::iterator it;
+	for (it = mUrlEntry.begin(); it != mUrlEntry.end(); ++it)
+	{
+		delete *it;
+	}
+}
+
+void LLUrlRegistry::registerUrl(LLUrlEntryBase *url)
+{
+	if (url)
+	{
+		mUrlEntry.push_back(url);
+	}
+}
+
+static bool matchRegex(const char *text, boost::regex regex, U32 &start, U32 &end)
+{
+	boost::cmatch result;
+	bool found;
+
+	// regex_search can potentially throw an exception, so check for it
+	try
+	{
+		found = boost::regex_search(text, result, regex);
+	}
+	catch (std::runtime_error &)
+	{
+		return false;
+	}
+
+	if (! found)
+	{
+		return false;
+	}
+
+	// return the first/last character offset for the matched substring
+	start = static_cast<U32>(result[0].first - text);
+	end = static_cast<U32>(result[0].second - text) - 1;
+
+	// we allow certain punctuation to terminate a Url but not match it,
+	// e.g., "http://foo.com/." should just match "http://foo.com/"
+	if (text[end] == '.' || text[end] == ',')
+	{
+		end--;
+	}
+	// ignore a terminating ')' when Url contains no matching '('
+	// see DEV-19842 for details
+	else if (text[end] == ')' && std::string(text+start, end-start).find('(') == std::string::npos)
+	{
+		end--;
+	}
+
+	return true;
+}
+
+bool LLUrlRegistry::findUrl(const std::string &text, LLUrlMatch &match, const LLUrlLabelCallback &cb)
+{
+	// test for the trivial case of no text and get out fast
+	if (text.empty())
+	{
+		return false;
+	}
+
+	// find the first matching regex from all url entries in the registry
+	U32 match_start = 0, match_end = 0;
+	LLUrlEntryBase *match_entry = NULL;
+
+	std::vector<LLUrlEntryBase *>::iterator it;
+	for (it = mUrlEntry.begin(); it != mUrlEntry.end(); ++it)
+	{
+		LLUrlEntryBase *url_entry = *it;
+
+		U32 start = 0, end = 0;
+		if (matchRegex(text.c_str(), url_entry->getPattern(), start, end))
+		{
+			// does this match occur in the string before any other match
+			if (start < match_start || match_entry == NULL)
+			{
+				match_start = start;
+				match_end = end;
+				match_entry = url_entry;
+			}
+		}
+	}
+	
+	// did we find a match? if so, return its details in the match object
+	if (match_entry)
+	{
+		// fill in the LLUrlMatch object and return it
+		std::string url = text.substr(match_start, match_end - match_start + 1);
+		match.setValues(match_start, match_end,
+						match_entry->getUrl(url),
+						match_entry->getLabel(url, cb),
+						match_entry->getTooltip(),
+						match_entry->getIcon(),
+						match_entry->getMenuName(),
+						match_entry->getLocation(url));
+		return true;
+	}
+
+	return false;
+}
diff --git a/indra/llui/llurlregistry.h b/indra/llui/llurlregistry.h
new file mode 100644
index 0000000000..84b033036c
--- /dev/null
+++ b/indra/llui/llurlregistry.h
@@ -0,0 +1,87 @@
+/** 
+ * @file llurlregistry.h
+ * @author Martin Reddy
+ * @brief Contains a set of Url types that can be matched in a string
+ *
+ * $LicenseInfo:firstyear=2009&license=viewergpl$
+ * 
+ * Copyright (c) 2009, Linden Research, Inc.
+ * 
+ * Second Life Viewer Source Code
+ * The source code in this file ("Source Code") is provided by Linden Lab
+ * to you under the terms of the GNU General Public License, version 2.0
+ * ("GPL"), unless you have obtained a separate licensing agreement
+ * ("Other License"), formally executed by you and Linden Lab.  Terms of
+ * the GPL can be found in doc/GPL-license.txt in this distribution, or
+ * online at http://secondlifegrid.net/programs/open_source/licensing/gplv2
+ * 
+ * There are special exceptions to the terms and conditions of the GPL as
+ * it is applied to this Source Code. View the full text of the exception
+ * in the file doc/FLOSS-exception.txt in this software distribution, or
+ * online at
+ * http://secondlifegrid.net/programs/open_source/licensing/flossexception
+ * 
+ * By copying, modifying or distributing this software, you acknowledge
+ * that you have read and understood your obligations described above,
+ * and agree to abide by those obligations.
+ * 
+ * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO
+ * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY,
+ * COMPLETENESS OR PERFORMANCE.
+ * $/LicenseInfo$
+ */
+
+#ifndef LL_LLURLREGISTRY_H
+#define LL_LLURLREGISTRY_H
+
+#include "llurlentry.h"
+#include "llurlmatch.h"
+#include "llsingleton.h"
+
+#include <string>
+#include <vector>
+#include <map>
+
+/// This default callback for findUrl() simply ignores any label updates
+void LLUrlRegistryNullCallback(const std::string &url, const std::string &label);
+
+///
+/// LLUrlRegistry is a singleton that contains a set of Url types that
+/// can be matched in string. E.g., http:// or secondlife:// Urls.
+///
+/// Clients call the findUrl() method on a string to locate the first
+/// occurence of a supported Urls in that string. If findUrl() returns
+/// true, the LLUrlMatch object will be updated to describe the Url
+/// that was matched, including a label that can be used to hyperlink
+/// the Url, an icon to display next to the Url, and a XUI menu that
+/// can be used as a popup context menu for that Url.
+///
+/// New Url types can be added to the registry with the registerUrl
+/// method. E.g., to add support for a new secondlife:///app/ Url.
+///
+/// Computing the label for a Url could involve a roundtrip request
+/// to the server (e.g., to find the actual agent or group name).
+/// As such, you can provide a callback method that will get invoked
+/// when a new label is available for one of your matched Urls.
+///
+class LLUrlRegistry : public LLSingleton<LLUrlRegistry>
+{
+public:
+	~LLUrlRegistry();
+
+	/// add a new Url handler to the registry (will be freed on destruction)
+	void registerUrl(LLUrlEntryBase *url);
+
+	/// get the next Url in an input string, starting at a given character offset
+	/// your callback is invoked if the matched Url's label changes in the future
+	bool findUrl(const std::string &text, LLUrlMatch &match,
+				 const LLUrlLabelCallback &cb = &LLUrlRegistryNullCallback);
+
+private:
+	LLUrlRegistry();
+	friend class LLSingleton<LLUrlRegistry>;
+
+	std::vector<LLUrlEntryBase *> mUrlEntry;
+};
+
+#endif
diff --git a/indra/llui/tests/llurlentry_stub.cpp b/indra/llui/tests/llurlentry_stub.cpp
new file mode 100644
index 0000000000..05bd5d8bb3
--- /dev/null
+++ b/indra/llui/tests/llurlentry_stub.cpp
@@ -0,0 +1,64 @@
+/**
+ * @file llurlentry_stub.cpp
+ * @author Martin Reddy
+ * @brief Stub implementations for LLUrlEntry unit test dependencies
+ *
+ * $LicenseInfo:firstyear=2009&license=internal$
+ * 
+ * Copyright (c) 2009, Linden Research, Inc.
+ * 
+ * The following source code is PROPRIETARY AND CONFIDENTIAL. Use of
+ * this source code is governed by the Linden Lab Source Code Disclosure
+ * Agreement ("Agreement") previously entered between you and Linden
+ * Lab. By accessing, using, copying, modifying or distributing this
+ * software, you acknowledge that you have been informed of your
+ * obligations under the Agreement and agree to abide by those obligations.
+ * 
+ * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO
+ * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY,
+ * COMPLETENESS OR PERFORMANCE.
+ * $/LicenseInfo$
+ */
+
+#include "llstring.h"
+#include "llfile.h"
+#include "llcachename.h"
+#include "lluuid.h"
+
+#include <string>
+
+//
+// Stub implementation for LLCacheName
+//
+BOOL LLCacheName::getFullName(const LLUUID& id, std::string& fullname)
+{
+	fullname = "Lynx Linden";
+	return TRUE;
+}
+
+BOOL LLCacheName::getGroupName(const LLUUID& id, std::string& group)
+{
+	group = "My Group";
+	return TRUE;
+}
+
+boost::signals2::connection LLCacheName::get(const LLUUID& id, BOOL is_group, const LLCacheNameCallback& callback)
+{
+	return boost::signals2::connection();
+}
+
+LLCacheName* gCacheName = NULL;
+
+//
+// Stub implementation for LLTrans
+//
+class LLTrans
+{
+public:
+	static std::string getString(const std::string &xml_desc, const LLStringUtil::format_map_t& args);
+};
+
+std::string LLTrans::getString(const std::string &xml_desc, const LLStringUtil::format_map_t& args)
+{
+	return std::string();
+}
diff --git a/indra/llui/tests/llurlentry_test.cpp b/indra/llui/tests/llurlentry_test.cpp
new file mode 100644
index 0000000000..f8e6aa65d0
--- /dev/null
+++ b/indra/llui/tests/llurlentry_test.cpp
@@ -0,0 +1,535 @@
+/**
+ * @file llurlentry_test.cpp
+ * @author Martin Reddy
+ * @brief Unit tests for LLUrlEntry objects
+ *
+ * $LicenseInfo:firstyear=2009&license=internal$
+ * 
+ * Copyright (c) 2009, Linden Research, Inc.
+ * 
+ * The following source code is PROPRIETARY AND CONFIDENTIAL. Use of
+ * this source code is governed by the Linden Lab Source Code Disclosure
+ * Agreement ("Agreement") previously entered between you and Linden
+ * Lab. By accessing, using, copying, modifying or distributing this
+ * software, you acknowledge that you have been informed of your
+ * obligations under the Agreement and agree to abide by those obligations.
+ * 
+ * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO
+ * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY,
+ * COMPLETENESS OR PERFORMANCE.
+ * $/LicenseInfo$
+ */
+
+#include "linden_common.h"
+#include "../llurlentry.h"
+#include "llurlentry_stub.cpp"
+#include "lltut.h"
+
+#include <boost/regex.hpp>
+
+namespace tut
+{
+	struct LLUrlEntryData
+	{
+	};
+
+	typedef test_group<LLUrlEntryData> factory;
+	typedef factory::object object;
+}
+
+namespace
+{
+	tut::factory tf("LLUrlEntry");
+}
+
+namespace tut
+{
+	void testRegex(const std::string &testname, boost::regex regex,
+				   const char *text, const std::string &expected)
+	{
+		std::string url = "";
+		boost::cmatch result;
+		bool found = boost::regex_search(text, result, regex);
+		if (found)
+		{
+			S32 start = static_cast<U32>(result[0].first - text);
+			S32 end = static_cast<U32>(result[0].second - text);
+			url = std::string(text+start, end-start);
+		}
+		ensure_equals(testname, url, expected);
+	}
+
+	template<> template<>
+	void object::test<1>()
+	{
+		//
+		// test LLUrlEntryHTTP - standard http Urls
+		//
+		LLUrlEntryHTTP url;
+		boost::regex r = url.getPattern();
+
+		testRegex("no valid url", r,
+				  "htp://slurl.com/",
+				  "");
+
+		testRegex("simple http (1)", r,
+				  "http://slurl.com/",
+				  "http://slurl.com/");
+
+		testRegex("simple http (2)", r,
+				  "http://slurl.com",
+				  "http://slurl.com");
+
+		testRegex("simple http (3)", r,
+				  "http://slurl.com/about.php",
+				  "http://slurl.com/about.php");
+
+		testRegex("simple https", r,
+				  "https://slurl.com/about.php",
+				  "https://slurl.com/about.php");
+
+		testRegex("http in text (1)", r,
+				  "XX http://slurl.com/ XX",
+				  "http://slurl.com/");
+
+		testRegex("http in text (2)", r,
+				  "XX http://slurl.com/about.php XX",
+				  "http://slurl.com/about.php");
+
+		testRegex("https in text", r,
+				  "XX https://slurl.com/about.php XX",
+				  "https://slurl.com/about.php");
+
+		testRegex("two http urls", r,
+				  "XX http://slurl.com/about.php http://secondlife.com/ XX",
+				  "http://slurl.com/about.php");
+
+		testRegex("http url with port and username", r,
+				  "XX http://nobody@slurl.com:80/about.php http://secondlife.com/ XX",
+				  "http://nobody@slurl.com:80/about.php");
+
+		testRegex("http url with port, username, and query string", r,
+				  "XX http://nobody@slurl.com:80/about.php?title=hi%20there http://secondlife.com/ XX",
+				  "http://nobody@slurl.com:80/about.php?title=hi%20there");
+
+		// note: terminating commas will be removed by LLUrlRegistry:findUrl()
+		testRegex("http url with commas in middle and terminating", r,
+				  "XX http://slurl.com/?title=Hi,There, XX",
+				  "http://slurl.com/?title=Hi,There,");
+
+		// note: terminating periods will be removed by LLUrlRegistry:findUrl()
+		testRegex("http url with periods in middle and terminating", r,
+				  "XX http://slurl.com/index.php. XX",
+				  "http://slurl.com/index.php.");
+
+		// DEV-19842: Closing parenthesis ")" breaks urls
+		testRegex("http url with brackets (1)", r,
+				  "XX http://en.wikipedia.org/wiki/JIRA_(software) XX",
+				  "http://en.wikipedia.org/wiki/JIRA_(software)");
+
+		// DEV-19842: Closing parenthesis ")" breaks urls
+		testRegex("http url with brackets (2)", r, 
+				  "XX http://jira.secondlife.com/secure/attachment/17990/eggy+avs+in+1.21.0+(93713)+public+nightly.jpg XX",
+				  "http://jira.secondlife.com/secure/attachment/17990/eggy+avs+in+1.21.0+(93713)+public+nightly.jpg");
+
+		// DEV-10353: URLs in chat log terminated incorrectly when newline in chat
+		testRegex("http url with newlines", r,
+				  "XX\nhttp://www.secondlife.com/\nXX",
+				  "http://www.secondlife.com/");
+	}
+
+	template<> template<>
+	void object::test<2>()
+	{
+		//
+		// test LLUrlEntryHTTPLabel - wiki-style http Urls with labels
+		//
+		LLUrlEntryHTTPLabel url;
+		boost::regex r = url.getPattern();
+
+		testRegex("invalid wiki url [1]", r,
+				  "[http://www.example.org]",
+				  "");
+
+		testRegex("invalid wiki url [2]", r,
+				  "[http://www.example.org",
+				  "");
+
+		testRegex("invalid wiki url [3]", r,
+				  "[http://www.example.org Label",
+				  "");
+
+		testRegex("example.org with label (spaces)", r,
+				  "[http://www.example.org  Text]",
+				  "[http://www.example.org  Text]");
+
+		testRegex("example.org with label (tabs)", r,
+				  "[http://www.example.org\t Text]",
+				  "[http://www.example.org\t Text]");
+
+		testRegex("SL http URL with label", r,
+				  "[http://www.secondlife.com/ Second Life]",
+				  "[http://www.secondlife.com/ Second Life]");
+
+		testRegex("SL https URL with label", r,
+				  "XXX [https://www.secondlife.com/ Second Life] YYY",
+				  "[https://www.secondlife.com/ Second Life]");
+
+		testRegex("SL http URL with label", r,
+				  "[http://www.secondlife.com/?test=Hi%20There Second Life]",
+				  "[http://www.secondlife.com/?test=Hi%20There Second Life]");
+	}
+
+	template<> template<>
+	void object::test<3>()
+	{
+		//
+		// test LLUrlEntrySLURL - second life URLs
+		//
+		LLUrlEntrySLURL url;
+		boost::regex r = url.getPattern();
+
+		testRegex("no valid slurl [1]", r,
+				  "htp://slurl.com/secondlife/Ahern/50/50/50/",
+				  "");
+
+		testRegex("no valid slurl [2]", r,
+				  "http://slurl.com/secondlife/",
+				  "");
+
+		testRegex("no valid slurl [3]", r,
+				  "hhtp://slurl.com/secondlife/Ahern/50/FOO/50/",
+				  "");
+
+		testRegex("Ahern (50,50,50) [1]", r,
+				  "http://slurl.com/secondlife/Ahern/50/50/50/",
+				  "http://slurl.com/secondlife/Ahern/50/50/50/");
+
+		testRegex("Ahern (50,50,50) [2]", r,
+				  "XXX http://slurl.com/secondlife/Ahern/50/50/50/ XXX",
+				  "http://slurl.com/secondlife/Ahern/50/50/50/");
+
+		testRegex("Ahern (50,50,50) [3]", r,
+				  "XXX http://slurl.com/secondlife/Ahern/50/50/50 XXX",
+				  "http://slurl.com/secondlife/Ahern/50/50/50");
+
+		testRegex("Ahern (50,50,50) multicase", r,
+				  "XXX http://SLUrl.com/SecondLife/Ahern/50/50/50/ XXX",
+				  "http://SLUrl.com/SecondLife/Ahern/50/50/50/");
+
+		testRegex("Ahern (50,50) [1]", r,
+				  "XXX http://slurl.com/secondlife/Ahern/50/50/ XXX",
+				  "http://slurl.com/secondlife/Ahern/50/50/");
+
+		testRegex("Ahern (50,50) [2]", r,
+				  "XXX http://slurl.com/secondlife/Ahern/50/50 XXX",
+				  "http://slurl.com/secondlife/Ahern/50/50");
+
+		testRegex("Ahern (50)", r,
+				  "XXX http://slurl.com/secondlife/Ahern/50 XXX",
+				  "http://slurl.com/secondlife/Ahern/50");
+
+		testRegex("Ahern", r,
+				  "XXX http://slurl.com/secondlife/Ahern/ XXX",
+				  "http://slurl.com/secondlife/Ahern/");
+
+		testRegex("Ahern SLURL with title", r,
+				  "XXX http://slurl.com/secondlife/Ahern/50/50/50/?title=YOUR%20TITLE%20HERE! XXX",
+				  "http://slurl.com/secondlife/Ahern/50/50/50/?title=YOUR%20TITLE%20HERE!");
+
+		testRegex("Ahern SLURL with msg", r,
+				  "XXX http://slurl.com/secondlife/Ahern/50/50/50/?msg=Your%20text%20here. XXX",
+				  "http://slurl.com/secondlife/Ahern/50/50/50/?msg=Your%20text%20here.");
+
+		// DEV-21577: In-world SLURLs containing "(" or ")" are not treated as a hyperlink in chat
+		testRegex("SLURL with brackets", r,
+				  "XXX http://slurl.com/secondlife/Burning%20Life%20(Hyper)/27/210/30 XXX",
+				  "http://slurl.com/secondlife/Burning%20Life%20(Hyper)/27/210/30");
+
+		// DEV-35459: SLURLs and teleport Links not parsed properly
+		testRegex("SLURL with quote", r,
+				  "XXX http://slurl.com/secondlife/A'ksha%20Oasis/41/166/701 XXX",
+				  "http://slurl.com/secondlife/A'ksha%20Oasis/41/166/701");
+	}
+
+	template<> template<>
+	void object::test<4>()
+	{
+		//
+		// test LLUrlEntryAgent - secondlife://app/agent Urls
+		//
+		LLUrlEntryAgent url;
+		boost::regex r = url.getPattern();
+
+		testRegex("Invalid Agent Url", r,
+				  "secondlife:///app/agent/0e346d8b-4433-4d66-XXXX-fd37083abc4c/about",
+				  "");
+
+		testRegex("Agent Url ", r,
+				  "secondlife:///app/agent/0e346d8b-4433-4d66-a6b0-fd37083abc4c/about",
+				  "secondlife:///app/agent/0e346d8b-4433-4d66-a6b0-fd37083abc4c/about");
+
+		testRegex("Agent Url in text", r,
+				  "XXX secondlife:///app/agent/0e346d8b-4433-4d66-a6b0-fd37083abc4c/about XXX",
+				  "secondlife:///app/agent/0e346d8b-4433-4d66-a6b0-fd37083abc4c/about");
+
+		testRegex("Agent Url multicase", r,
+				  "XXX secondlife:///App/AGENT/0E346D8B-4433-4d66-a6b0-fd37083abc4c/About XXX",
+				  "secondlife:///App/AGENT/0E346D8B-4433-4d66-a6b0-fd37083abc4c/About");
+	}
+
+	template<> template<>
+	void object::test<5>()
+	{
+		//
+		// test LLUrlEntryGroup - secondlife://app/group Urls
+		//
+		LLUrlEntryGroup url;
+		boost::regex r = url.getPattern();
+
+		testRegex("Invalid Group Url", r,
+				  "secondlife:///app/group/00005ff3-4044-c79f-XXXX-fb28ae0df991/about",
+				  "");
+
+		testRegex("Group Url ", r,
+				  "secondlife:///app/group/00005ff3-4044-c79f-9de8-fb28ae0df991/about",
+				  "secondlife:///app/group/00005ff3-4044-c79f-9de8-fb28ae0df991/about");
+
+		testRegex("Group Url in text", r,
+				  "XXX secondlife:///app/group/00005ff3-4044-c79f-9de8-fb28ae0df991/about XXX",
+				  "secondlife:///app/group/00005ff3-4044-c79f-9de8-fb28ae0df991/about");
+
+		testRegex("Group Url multicase", r,
+				  "XXX secondlife:///APP/Group/00005FF3-4044-c79f-9de8-fb28ae0df991/About XXX",
+				  "secondlife:///APP/Group/00005FF3-4044-c79f-9de8-fb28ae0df991/About");
+	}
+
+	template<> template<>
+	void object::test<6>()
+	{
+		//
+		// test LLUrlEntryEvent - secondlife://app/event Urls
+		//
+		LLUrlEntryEvent url;
+		boost::regex r = url.getPattern();
+
+		testRegex("Invalid Event Url", r,
+				  "secondlife:///app/event/FOO/about",
+				  "");
+
+		testRegex("Event Url ", r,
+				  "secondlife:///app/event/700727/about",
+				  "secondlife:///app/event/700727/about");
+
+		testRegex("Event Url in text", r,
+				  "XXX secondlife:///app/event/700727/about XXX",
+				  "secondlife:///app/event/700727/about");
+
+		testRegex("Event Url multicase", r,
+				  "XXX secondlife:///APP/Event/700727/about XXX",
+				  "secondlife:///APP/Event/700727/about");
+	}
+
+	template<> template<>
+	void object::test<7>()
+	{
+		//
+		// test LLUrlEntryClassified - secondlife://app/classified Urls
+		//
+		LLUrlEntryClassified url;
+		boost::regex r = url.getPattern();
+
+		testRegex("Invalid Classified Url", r,
+				  "secondlife:///app/classified/00128854-XXXX-5649-7ca6-5dfaa7514ab2/about",
+				  "");
+
+		testRegex("Classified Url ", r,
+				  "secondlife:///app/classified/00128854-c36a-5649-7ca6-5dfaa7514ab2/about",
+				  "secondlife:///app/classified/00128854-c36a-5649-7ca6-5dfaa7514ab2/about");
+
+		testRegex("Classified Url in text", r,
+				  "XXX secondlife:///app/classified/00128854-c36a-5649-7ca6-5dfaa7514ab2/about XXX",
+				  "secondlife:///app/classified/00128854-c36a-5649-7ca6-5dfaa7514ab2/about");
+
+		testRegex("Classified Url multicase", r,
+				  "XXX secondlife:///APP/Classified/00128854-c36a-5649-7ca6-5dfaa7514ab2/About XXX",
+				  "secondlife:///APP/Classified/00128854-c36a-5649-7ca6-5dfaa7514ab2/About");
+	}
+
+	template<> template<>
+	void object::test<8>()
+	{
+		//
+		// test LLUrlEntryParcel - secondlife://app/parcel Urls
+		//
+		LLUrlEntryParcel url;
+		boost::regex r = url.getPattern();
+
+		testRegex("Invalid Classified Url", r,
+				  "secondlife:///app/parcel/0000060e-4b39-e00b-XXXX-d98b1934e3a8/about",
+				  "");
+
+		testRegex("Classified Url ", r,
+				  "secondlife:///app/parcel/0000060e-4b39-e00b-d0c3-d98b1934e3a8/about",
+				  "secondlife:///app/parcel/0000060e-4b39-e00b-d0c3-d98b1934e3a8/about");
+
+		testRegex("Classified Url in text", r,
+				  "XXX secondlife:///app/parcel/0000060e-4b39-e00b-d0c3-d98b1934e3a8/about XXX",
+				  "secondlife:///app/parcel/0000060e-4b39-e00b-d0c3-d98b1934e3a8/about");
+
+		testRegex("Classified Url multicase", r,
+				  "XXX secondlife:///APP/Parcel/0000060e-4b39-e00b-d0c3-d98b1934e3a8/About XXX",
+				  "secondlife:///APP/Parcel/0000060e-4b39-e00b-d0c3-d98b1934e3a8/About");
+	}
+	template<> template<>
+	void object::test<9>()
+	{
+		//
+		// test LLUrlEntryTeleport - secondlife://app/teleport URLs
+		//
+		LLUrlEntryTeleport url;
+		boost::regex r = url.getPattern();
+
+		testRegex("no valid teleport [1]", r,
+				  "http://slurl.com/secondlife/Ahern/50/50/50/",
+				  "");
+
+		testRegex("no valid teleport [2]", r,
+				  "secondlife:///app/teleport/",
+				  "");
+
+		testRegex("no valid teleport [3]", r,
+				  "second-life:///app/teleport/Ahern/50/50/50/",
+				  "");
+
+		testRegex("no valid teleport [3]", r,
+				  "hhtp://slurl.com/secondlife/Ahern/50/FOO/50/",
+				  "");
+
+		testRegex("Ahern (50,50,50) [1]", r,
+				  "secondlife:///app/teleport/Ahern/50/50/50/",
+				  "secondlife:///app/teleport/Ahern/50/50/50/");
+
+		testRegex("Ahern (50,50,50) [2]", r,
+				  "XXX secondlife:///app/teleport/Ahern/50/50/50/ XXX",
+				  "secondlife:///app/teleport/Ahern/50/50/50/");
+
+		testRegex("Ahern (50,50,50) [3]", r,
+				  "XXX secondlife:///app/teleport/Ahern/50/50/50 XXX",
+				  "secondlife:///app/teleport/Ahern/50/50/50");
+
+		testRegex("Ahern (50,50,50) multicase", r,
+				  "XXX secondlife:///app/teleport/Ahern/50/50/50/ XXX",
+				  "secondlife:///app/teleport/Ahern/50/50/50/");
+
+		testRegex("Ahern (50,50) [1]", r,
+				  "XXX secondlife:///app/teleport/Ahern/50/50/ XXX",
+				  "secondlife:///app/teleport/Ahern/50/50/");
+
+		testRegex("Ahern (50,50) [2]", r,
+				  "XXX secondlife:///app/teleport/Ahern/50/50 XXX",
+				  "secondlife:///app/teleport/Ahern/50/50");
+
+		testRegex("Ahern (50)", r,
+				  "XXX secondlife:///app/teleport/Ahern/50 XXX",
+				  "secondlife:///app/teleport/Ahern/50");
+
+		testRegex("Ahern", r,
+				  "XXX secondlife:///app/teleport/Ahern/ XXX",
+				  "secondlife:///app/teleport/Ahern/");
+
+		testRegex("Ahern teleport with title", r,
+				  "XXX secondlife:///app/teleport/Ahern/50/50/50/?title=YOUR%20TITLE%20HERE! XXX",
+				  "secondlife:///app/teleport/Ahern/50/50/50/?title=YOUR%20TITLE%20HERE!");
+
+		testRegex("Ahern teleport with msg", r,
+				  "XXX secondlife:///app/teleport/Ahern/50/50/50/?msg=Your%20text%20here. XXX",
+				  "secondlife:///app/teleport/Ahern/50/50/50/?msg=Your%20text%20here.");
+
+		// DEV-21577: In-world SLURLs containing "(" or ")" are not treated as a hyperlink in chat
+		testRegex("Teleport with brackets", r,
+				  "XXX secondlife:///app/teleport/Burning%20Life%20(Hyper)/27/210/30 XXX",
+				  "secondlife:///app/teleport/Burning%20Life%20(Hyper)/27/210/30");
+
+		// DEV-35459: SLURLs and teleport Links not parsed properly
+		testRegex("Teleport url with quote", r,
+				  "XXX secondlife:///app/teleport/A'ksha%20Oasis/41/166/701 XXX",
+				  "secondlife:///app/teleport/A'ksha%20Oasis/41/166/701");
+	}
+
+	template<> template<>
+	void object::test<10>()
+	{
+		//
+		// test LLUrlEntrySL - general secondlife:// URLs
+		//
+		LLUrlEntrySL url;
+		boost::regex r = url.getPattern();
+
+		testRegex("no valid slapp [1]", r,
+				  "http:///app/",
+				  "");
+
+		testRegex("valid slapp [1]", r,
+				  "secondlife:///app/",
+				  "secondlife:///app/");
+
+		testRegex("valid slapp [2]", r,
+				  "secondlife:///app/teleport/Ahern/50/50/50/",
+				  "secondlife:///app/teleport/Ahern/50/50/50/");
+
+		testRegex("valid slapp [3]", r,
+				  "secondlife:///app/foo",
+				  "secondlife:///app/foo");
+
+		testRegex("valid slapp [4]", r,
+				  "secondlife:///APP/foo?title=Hi%20There",
+				  "secondlife:///APP/foo?title=Hi%20There");
+
+		testRegex("valid slapp [5]", r,
+				  "secondlife://host/app/",
+				  "secondlife://host/app/");
+
+		testRegex("valid slapp [6]", r,
+				  "secondlife://host:8080/foo/bar",
+				  "secondlife://host:8080/foo/bar");
+	}
+
+	template<> template<>
+	void object::test<11>()
+	{
+		//
+		// test LLUrlEntrySLLabel - general secondlife:// URLs with labels
+		//
+		LLUrlEntrySLLabel url;
+		boost::regex r = url.getPattern();
+
+		testRegex("invalid wiki url [1]", r,
+				  "[secondlife:///app/]",
+				  "");
+
+		testRegex("invalid wiki url [2]", r,
+				  "[secondlife:///app/",
+				  "");
+
+		testRegex("invalid wiki url [3]", r,
+				  "[secondlife:///app/ Label",
+				  "");
+
+		testRegex("agent slurl with label (spaces)", r,
+				  "[secondlife:///app/agent/0e346d8b-4433-4d66-a6b0-fd37083abc4c/about  Text]",
+				  "[secondlife:///app/agent/0e346d8b-4433-4d66-a6b0-fd37083abc4c/about  Text]");
+
+		testRegex("agent slurl with label (tabs)", r,
+				  "[secondlife:///app/agent/0e346d8b-4433-4d66-a6b0-fd37083abc4c/about\t Text]",
+				  "[secondlife:///app/agent/0e346d8b-4433-4d66-a6b0-fd37083abc4c/about\t Text]");
+
+		testRegex("agent slurl with label", r,
+				  "[secondlife:///app/agent/0e346d8b-4433-4d66-a6b0-fd37083abc4c/about FirstName LastName]",
+				  "[secondlife:///app/agent/0e346d8b-4433-4d66-a6b0-fd37083abc4c/about FirstName LastName]");
+
+		testRegex("teleport slurl with label", r,
+				  "XXX [secondlife:///app/teleport/Ahern/50/50/50/ Teleport to Ahern] YYY",
+				  "[secondlife:///app/teleport/Ahern/50/50/50/ Teleport to Ahern]");
+	}
+}
diff --git a/indra/llui/tests/llurlmatch_test.cpp b/indra/llui/tests/llurlmatch_test.cpp
new file mode 100644
index 0000000000..fcf8f4d62f
--- /dev/null
+++ b/indra/llui/tests/llurlmatch_test.cpp
@@ -0,0 +1,177 @@
+/**
+ * @file llurlmatch_test.cpp
+ * @author Martin Reddy
+ * @brief Unit tests for LLUrlMatch
+ *
+ * $LicenseInfo:firstyear=2009&license=internal$
+ * 
+ * Copyright (c) 2009, Linden Research, Inc.
+ * 
+ * The following source code is PROPRIETARY AND CONFIDENTIAL. Use of
+ * this source code is governed by the Linden Lab Source Code Disclosure
+ * Agreement ("Agreement") previously entered between you and Linden
+ * Lab. By accessing, using, copying, modifying or distributing this
+ * software, you acknowledge that you have been informed of your
+ * obligations under the Agreement and agree to abide by those obligations.
+ * 
+ * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO
+ * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY,
+ * COMPLETENESS OR PERFORMANCE.
+ * $/LicenseInfo$
+ */
+
+#include "../llurlmatch.h"
+#include "lltut.h"
+
+namespace tut
+{
+	struct LLUrlMatchData
+	{
+	};
+
+	typedef test_group<LLUrlMatchData> factory;
+	typedef factory::object object;
+}
+
+namespace
+{
+	tut::factory tf("LLUrlMatch");
+}
+
+namespace tut
+{
+	template<> template<>
+	void object::test<1>()
+	{
+		//
+		// test the empty() method
+		//
+		LLUrlMatch match;
+		ensure("empty()", match.empty());
+
+		match.setValues(0, 1, "http://secondlife.com", "Second Life", "", "", "", "");
+		ensure("! empty()", ! match.empty());
+	}
+
+	template<> template<>
+	void object::test<2>()
+	{
+		//
+		// test the getStart() method
+		//
+		LLUrlMatch match;
+		ensure_equals("getStart() == 0", match.getStart(), 0);
+
+		match.setValues(10, 20, "", "", "", "", "", "");
+		ensure_equals("getStart() == 10", match.getStart(), 10);
+	}
+
+	template<> template<>
+	void object::test<3>()
+	{
+		//
+		// test the getEnd() method
+		//
+		LLUrlMatch match;
+		ensure_equals("getEnd() == 0", match.getEnd(), 0);
+
+		match.setValues(10, 20, "", "", "", "", "", "");
+		ensure_equals("getEnd() == 20", match.getEnd(), 20);
+	}
+
+	template<> template<>
+	void object::test<4>()
+	{
+		//
+		// test the getUrl() method
+		//
+		LLUrlMatch match;
+		ensure_equals("getUrl() == ''", match.getUrl(), "");
+
+		match.setValues(10, 20, "http://slurl.com/", "", "", "", "", "");
+		ensure_equals("getUrl() == 'http://slurl.com/'", match.getUrl(), "http://slurl.com/");
+
+		match.setValues(10, 20, "", "", "", "", "", "");
+		ensure_equals("getUrl() == '' (2)", match.getUrl(), "");
+	}
+
+	template<> template<>
+	void object::test<5>()
+	{
+		//
+		// test the getLabel() method
+		//
+		LLUrlMatch match;
+		ensure_equals("getLabel() == ''", match.getLabel(), "");
+
+		match.setValues(10, 20, "", "Label", "", "", "", "");
+		ensure_equals("getLabel() == 'Label'", match.getLabel(), "Label");
+
+		match.setValues(10, 20, "", "", "", "", "", "");
+		ensure_equals("getLabel() == '' (2)", match.getLabel(), "");
+	}
+
+	template<> template<>
+	void object::test<6>()
+	{
+		//
+		// test the getTooltip() method
+		//
+		LLUrlMatch match;
+		ensure_equals("getTooltip() == ''", match.getTooltip(), "");
+
+		match.setValues(10, 20, "", "", "Info", "", "", "");
+		ensure_equals("getTooltip() == 'Info'", match.getTooltip(), "Info");
+
+		match.setValues(10, 20, "", "", "", "", "", "");
+		ensure_equals("getTooltip() == '' (2)", match.getTooltip(), "");
+	}
+
+	template<> template<>
+	void object::test<7>()
+	{
+		//
+		// test the getIcon() method
+		//
+		LLUrlMatch match;
+		ensure_equals("getIcon() == ''", match.getIcon(), "");
+
+		match.setValues(10, 20, "", "", "", "Icon", "", "");
+		ensure_equals("getIcon() == 'Icon'", match.getIcon(), "Icon");
+
+		match.setValues(10, 20, "", "", "", "", "", "");
+		ensure_equals("getIcon() == '' (2)", match.getIcon(), "");
+	}
+
+	template<> template<>
+	void object::test<8>()
+	{
+		//
+		// test the getMenuName() method
+		//
+		LLUrlMatch match;
+		ensure("getMenuName() empty", match.getMenuName().empty());
+
+		match.setValues(10, 20, "", "", "", "Icon", "xui_file.xml", "");
+		ensure_equals("getMenuName() == \"xui_file.xml\"", match.getMenuName(), "xui_file.xml");
+
+		match.setValues(10, 20, "", "", "", "", "", "");
+		ensure("getMenuName() empty (2)", match.getMenuName().empty());
+	}
+
+	template<> template<>
+	void object::test<9>()
+	{
+		//
+		// test the getLocation() method
+		//
+		LLUrlMatch match;
+		ensure("getLocation() empty", match.getLocation().empty());
+
+		match.setValues(10, 20, "", "", "", "Icon", "xui_file.xml", "Paris");
+		ensure_equals("getLocation() == \"Paris\"", match.getLocation(), "Paris");
+
+		match.setValues(10, 20, "", "", "", "", "", "");
+		ensure("getLocation() empty (2)", match.getLocation().empty());
+	}
+}
diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp
index de4f7ab091..3d7465f2cb 100644
--- a/indra/newview/llappviewer.cpp
+++ b/indra/newview/llappviewer.cpp
@@ -80,6 +80,7 @@
 
 // Linden library includes
 #include "llmemory.h"
+#include "llurlaction.h"
 
 // Third party library includes
 #include <boost/bind.hpp>
@@ -685,9 +686,14 @@ bool LLAppViewer::init()
 	LLTransUtil::parseLanguageStrings("language_settings.xml");
 	LLWeb::initClass();			  // do this after LLUI
 
-	LLTextEditor::setURLCallbacks(&LLWeb::loadURL,
-				&LLURLDispatcher::dispatchFromTextEditor,
-				&LLURLDispatcher::dispatchFromTextEditor);
+	// Provide the text fields with callbacks for opening Urls
+	LLUrlAction::setOpenURLCallback(&LLWeb::loadURL);
+	LLUrlAction::setOpenURLInternalCallback(&LLWeb::loadURLInternal);
+	LLUrlAction::setOpenURLExternalCallback(&LLWeb::loadURLExternal);
+	LLUrlAction::setExecuteSLURLCallback(&LLURLDispatcher::dispatchFromTextEditor);
+
+	// Set the link color for any Urls in text fields
+	LLTextBase::setLinkColor( LLUIColorTable::instance().getColor("HTMLLinkColor") );
 
 	// Load translations for tooltips
 	LLFloater::initClass();
diff --git a/indra/newview/llavatarlist.cpp b/indra/newview/llavatarlist.cpp
index 080d540f4a..e0322e26b9 100644
--- a/indra/newview/llavatarlist.cpp
+++ b/indra/newview/llavatarlist.cpp
@@ -62,6 +62,9 @@ LLAvatarList::LLAvatarList(const Params& p)
 {
 	setCommitOnSelectionChange(TRUE); // there's no such param in LLScrollListCtrl::Params
 
+	// display a context menu appropriate for a list of avatar names
+	setContextMenu(LLScrollListCtrl::MENU_AVATAR);
+
     // "volume" column
     {
     	LLScrollListColumn::Params col_params;
diff --git a/indra/newview/llchatmsgbox.cpp b/indra/newview/llchatmsgbox.cpp
index fb5ab8ec5a..e6398dd47a 100644
--- a/indra/newview/llchatmsgbox.cpp
+++ b/indra/newview/llchatmsgbox.cpp
@@ -1,10 +1,11 @@
 /** 
  * @file llchatmsgbox.cpp
+ * @author Martin Reddy
  * @brief chat history text box, able to show array of strings with separator
  *
- * $LicenseInfo:firstyear=2004&license=viewergpl$
+ * $LicenseInfo:firstyear=2009&license=viewergpl$
  * 
- * Copyright (c) 2004-2009, Linden Research, Inc.
+ * Copyright (c) 2009, Linden Research, Inc.
  * 
  * Second Life Viewer Source Code
  * The source code in this file ("Source Code") is provided by Linden Lab
@@ -30,361 +31,96 @@
  * $/LicenseInfo$
  */
 
-
 #include "llviewerprecompiledheaders.h"
 
 #include "llchatmsgbox.h"
 #include "llwindow.h"
-#include "llfocusmgr.h"
-
-static LLDefaultChildRegistry::Register<LLChatMsgBox> r("text_chat");
 
-LLChatMsgBox::Params::Params()
-:	text_color("text_color"),
-	highlight_on_hover("hover", false),
-	border_visible("border_visible", false),
-	border_drop_shadow_visible("border_drop_shadow_visible", false),
-	bg_visible("bg_visible", false),
-	use_ellipses("use_ellipses"),
-	word_wrap("word_wrap", false),
-	hover_color("hover_color"),
-	disabled_color("disabled_color"),
-	background_color("background_color"),
-	border_color("border_color"),
-	line_spacing("line_spacing", 4),
-	block_spacing("block_spacing",10),
-	text("text"),
-	font_shadow("font_shadow", LLFontGL::NO_SHADOW)
-{}
 
-LLChatMsgBox::LLChatMsgBox(const LLChatMsgBox::Params& p)
-:	LLUICtrl(p),
-    mFontGL(p.font),
-	mHoverActive( p.highlight_on_hover ),
-	mHasHover( FALSE ),
-	mBackgroundVisible( p.bg_visible ),
-	mBorderVisible( p.border_visible ),
-	mShadowType( p.font_shadow ),
-	mBorderDropShadowVisible( p.border_drop_shadow_visible ),
-	mUseEllipses( p.use_ellipses ),
-	mVAlign( LLFontGL::TOP ),
-	mClickedCallback(NULL),
-	mTextColor(p.text_color()),
-	mDisabledColor(p.disabled_color()),
-	mBackgroundColor(p.background_color()),
-	mBorderColor(p.border_color()),
-	mHoverColor(p.hover_color()),
-	mHAlign(p.font_halign),
-	mLineSpacing(p.line_spacing),
-	mBlockSpasing(p.block_spacing),
-	mWordWrap( p.word_wrap ),
-	mFontStyle(LLFontGL::getStyleFromString(p.font.style))
-{
-	setText( p.text() );
-}
+static LLDefaultChildRegistry::Register<LLChatMsgBox> r("text_chat");
 
-BOOL LLChatMsgBox::handleMouseDown(S32 x, S32 y, MASK mask)
+LLChatMsgBox::Params::Params() :
+	block_spacing("block_spacing", 10)
 {
-	BOOL	handled = FALSE;
-
-	// HACK: Only do this if there actually is a click callback, so that
-	// overly large text boxes in the older UI won't start eating clicks.
-	if (mClickedCallback)
-	{
-		handled = TRUE;
-
-		// Route future Mouse messages here preemptively.  (Release on mouse up.)
-		gFocusMgr.setMouseCapture( this );
-		
-		if (getSoundFlags() & MOUSE_DOWN)
-		{
-			make_ui_sound("UISndClick");
-		}
-	}
-
-	return handled;
+	line_spacing = 4;
 }
 
-BOOL LLChatMsgBox::handleMouseUp(S32 x, S32 y, MASK mask)
-{
-	BOOL	handled = FALSE;
-
-	// We only handle the click if the click both started and ended within us
-
-	// HACK: Only do this if there actually is a click callback, so that
-	// overly large text boxes in the older UI won't start eating clicks.
-	if (mClickedCallback
-		&& hasMouseCapture())
-	{
-		handled = TRUE;
-
-		// Release the mouse
-		gFocusMgr.setMouseCapture( NULL );
-
-		if (getSoundFlags() & MOUSE_UP)
-		{
-			make_ui_sound("UISndClickRelease");
-		}
-
-		// DO THIS AT THE VERY END to allow the button to be destroyed as a result of being clicked.
-		// If mouseup in the widget, it's been clicked
-		if (mClickedCallback)
-		{
-			mClickedCallback();
-		}
-	}
-
-	return handled;
-}
+LLChatMsgBox::LLChatMsgBox(const Params& p) :
+	LLTextBox(p),
+	mBlockSpacing(p.block_spacing)
+{}
 
-BOOL LLChatMsgBox::handleHover(S32 x, S32 y, MASK mask)
+void LLChatMsgBox::addText( const LLStringExplicit& text )
 {
-	BOOL handled = LLView::handleHover(x,y,mask);
-	if(mHoverActive)
+	LLWString t = mText.getWString();
+	if (! t.empty())
 	{
-		mHasHover = TRUE; // This should be set every frame during a hover.
-		getWindow()->setCursor(UI_CURSOR_ARROW);
+		t += '\n';
 	}
-
-	return (handled || mHasHover);
-}
-
-void	LLChatMsgBox::addText( const LLStringExplicit& text )
-{
-	boost::shared_ptr<text_block> t(new text_block());
-	t->text = wrapText(text);
-	setLineLengths(*t);
-	mTextStrings.push_back(t);
+	t += getWrappedText(text);
+	LLTextBox::setText(wstring_to_utf8str(t));
+	mSeparatorOffset.push_back(getLength());
 }
 
 void LLChatMsgBox::setText(const LLStringExplicit& text)
 {
-	mTextStrings.clear();
-
+	mSeparatorOffset.clear();
+	mText.clear();
 	addText(text);
-
-}
-
-void LLChatMsgBox::resetLineLengths()
-{
-	for(std::vector< boost::shared_ptr<text_block> >::iterator it = mTextStrings.begin();
-			it!=mTextStrings.end();++it)
-	{
-		boost::shared_ptr<text_block> tblock = *it;
-		setLineLengths(*tblock);
-	}
-}
-
-void LLChatMsgBox::setLineLengths(text_block& t)
-{
-	t.lines.clear();
-	
-	std::string::size_type  cur = 0;
-	std::string::size_type  len = t.text.length();
-
-	while (cur < len) 
-	{
-		std::string::size_type end = t.text.getWString().find('\n', cur);
-		std::string::size_type runLen;
-		
-		if (end == std::string::npos)
-		{
-			runLen = len - cur;
-			cur = len;
-		}
-		else
-		{
-			runLen = end - cur;
-			cur = end + 1; // skip the new line character
-		}
-
-		t.lines.push_back( (S32)runLen );
-	}
-}
-
-std::string LLChatMsgBox::wrapText(const LLStringExplicit& in_text, F32 max_width)
-{
-	if (max_width < 0.0f)
-	{
-		max_width = (F32)getRect().getWidth();
-	}
-
-	LLWString wtext = utf8str_to_wstring(in_text);
-	LLWString final_wtext;
-
-	LLWString::size_type  cur = 0;;
-	LLWString::size_type  len = wtext.size();
-	while (cur < len)
-	{
-		LLWString::size_type end = wtext.find('\n', cur);
-		if (end == LLWString::npos)
-		{
-			end = len;
-		}
-		
-		LLWString::size_type runLen = end - cur;
-		if (runLen > 0)
-		{
-			LLWString run(wtext, cur, runLen);
-			LLWString::size_type useLen =
-				mFontGL->maxDrawableChars(run.c_str(), max_width, runLen, TRUE);
-
-			final_wtext.append(wtext, cur, useLen);
-			cur += useLen;
-			// not enough room to add any more characters
-			if (useLen == 0) break;
-		}
-
-		if (cur < len)
-		{
-			if (wtext[cur] == '\n')
-				cur += 1;
-
-			// There is no need to to cut line ending symbols found in origin string, see EXT-702.
-			final_wtext += '\n';
-		}
-	}
-	
-	std::string final_text = wstring_to_utf8str(final_wtext);
-	return final_text;
 }
 
-S32	LLChatMsgBox::getTextLinesNum()
-{
-	S32 num_lines = 0;
-	for(std::vector< boost::shared_ptr<text_block> >::iterator it = mTextStrings.begin();
-			it!=mTextStrings.end();++it)
-	{
-		boost::shared_ptr<text_block> tblock = *it;
-		num_lines+=tblock->lines.size();
-	}
-
-	if( num_lines < 1 )
-	{
-		num_lines = 1;
-	}
-
-	return num_lines;
+void LLChatMsgBox::setValue(const LLSD& value )
+{ 
+	setText(value.asString());
 }
 
 S32 LLChatMsgBox::getTextPixelHeight()
 {
+	S32 num_blocks = mSeparatorOffset.size();
 	S32 num_lines = getTextLinesNum();
-	return (S32)(num_lines * mFontGL->getLineHeight() +  (num_lines-1)*mLineSpacing + mBlockSpasing*(mTextStrings.size()-1) + 2*mLineSpacing);//some extra space
-}
-
-void LLChatMsgBox::setValue(const LLSD& value )
-{ 
-	setText(value.asString());
+	return (S32)(num_lines * mDefaultFont->getLineHeight() + \
+				 (num_lines-1) * mLineSpacing + \
+				 (num_blocks-1) * mBlockSpacing + \
+				 2 * mLineSpacing);
 }
 
-
-void LLChatMsgBox::draw()
+S32 LLChatMsgBox::getTextLinesNum()
 {
-	if (mBorderVisible)
-	{
-		gl_rect_2d_offset_local(getLocalRect(), 2, FALSE);
-	}
-
-	if( mBorderDropShadowVisible )
-	{
-		static LLUICachedControl<LLColor4> color_drop_shadow ("ColorDropShadow", *(new LLColor4));
-		static LLUICachedControl<S32> drop_shadow_tooltip ("DropShadowTooltip", 0);
-		gl_drop_shadow(0, getRect().getHeight(), getRect().getWidth(), 0,
-			color_drop_shadow, drop_shadow_tooltip);
-	}
-
-	if (mBackgroundVisible)
-	{
-		LLRect r( 0, getRect().getHeight(), getRect().getWidth(), 0 );
-		gl_rect_2d( r, mBackgroundColor.get() );
-	}
-
-	S32 text_x = 0;
-	switch( mHAlign )
-	{
-	case LLFontGL::LEFT:	
-		break;
-	case LLFontGL::HCENTER:
-		text_x = getRect().getWidth() / 2;
-		break;
-	case LLFontGL::RIGHT:
-		text_x = getRect().getWidth() ;
-		break;
-	}
-
-	S32 text_y = getRect().getHeight() ;
-
-	if ( getEnabled() )
-	{
-		if(mHasHover)
-		{
-			drawText( text_x, text_y, mHoverColor.get() );
-		}
-		else
-		{
-			drawText( text_x, text_y, mTextColor.get() );
-		}				
-	}
-	else
-	{
-		drawText( text_x, text_y, mDisabledColor.get() );
-	}
-
-	if (sDebugRects)
+	S32 num_lines = getLineCount();
+	if (num_lines < 1)
 	{
-		drawDebugRect();
+		num_lines = 1;
 	}
-
-	//// *HACK: also draw debug rectangles around currently-being-edited LLView, and any elements that are being highlighted by GUI preview code (see LLFloaterUIPreview)
-	//std::set<LLView*>::iterator iter = std::find(sPreviewHighlightedElements.begin(), sPreviewHighlightedElements.end(), this);
-	//if ((sEditingUI && this == sEditingUIView) || (iter != sPreviewHighlightedElements.end() && sDrawPreviewHighlights))
-	//{
-	//	drawDebugRect();
-	//}
-
-	mHasHover = FALSE; // This is reset every frame.
-}
-
-void LLChatMsgBox::reshape(S32 width, S32 height, BOOL called_from_parent)
-{
-	// reparse line lengths
-	LLView::reshape(width, height, called_from_parent);
-	resetLineLengths();
+	
+	return num_lines;
 }
 
-void LLChatMsgBox::drawText( S32 x, S32 y, const LLColor4& color )
+void LLChatMsgBox::drawText(S32 x, S32 y, const LLWString &text, const LLColor4 &color)
 {
+	S32 start = 0;
 	S32 width = getRect().getWidth()-10;
 
-	
-	for(std::vector< boost::shared_ptr<text_block> >::iterator it = mTextStrings.begin();
-			it!=mTextStrings.end();++it)
+	// iterate through each block of text that has been added
+	y -= mLineSpacing;
+	for (std::vector<S32>::iterator it = mSeparatorOffset.begin(); true ;)
 	{
-		boost::shared_ptr<text_block> tblock = *it;
+		// display the text for this block
+		S32 num_chars = *it - start;
+		LLWString text = mDisplayText.substr(start, num_chars);
+		LLTextBox::drawText(x, y, text, color);
 		
-		S32 cur_pos = 0;
-		for (std::vector<S32>::iterator iter = tblock->lines.begin();
-			iter != tblock->lines.end(); ++iter)
+		// exit the loop if this is the last text block
+		start += num_chars + 1;  // skip the newline
+		if (++it == mSeparatorOffset.end())
 		{
-			S32 line_length = *iter;
-			mFontGL->render(tblock->text, cur_pos, (F32)x, (F32)y, color,
-							mHAlign, mVAlign,
-							mFontStyle,
-							mShadowType,
-							line_length, getRect().getWidth(), NULL, mUseEllipses );
-			cur_pos += line_length + 1;
-			y -= llfloor(mFontGL->getLineHeight()) + mLineSpacing;
-
-		}
-		std::vector< boost::shared_ptr<text_block> >::iterator next = it;
-		++next;
-		if(next == mTextStrings.end())
 			break;
-		//separator
-		gl_line_2d(5,y-mBlockSpasing/2,width,y-mBlockSpasing/2,LLColor4::grey);
-		y-=mBlockSpasing;
-	}
+		}
 
+		// output a separator line between blocks
+		S32 num_lines = std::count(text.begin(), text.end(), '\n') + 1;
+		y -= num_lines * (llfloor(mDefaultFont->getLineHeight()) + mLineSpacing);
+		S32 sep_y = y - mBlockSpacing/2 + mLineSpacing/2;
+		gl_line_2d(5, sep_y, width, sep_y, LLColor4::grey);
+		y -= mBlockSpacing;
+	}
 }
-
diff --git a/indra/newview/llchatmsgbox.h b/indra/newview/llchatmsgbox.h
index 61035499c7..b81b740bdc 100644
--- a/indra/newview/llchatmsgbox.h
+++ b/indra/newview/llchatmsgbox.h
@@ -1,10 +1,11 @@
 /** 
  * @file llchatmsgbox.h
+ * @author Martin Reddy
  * @brief chat history text box, able to show array of strings with separator
  *
- * $LicenseInfo:firstyear=2004&license=viewergpl$
+ * $LicenseInfo:firstyear=2009&license=viewergpl$
  * 
- * Copyright (c) 2004-2009, Linden Research, Inc.
+ * Copyright (c) 2009, Linden Research, Inc.
  * 
  * Second Life Viewer Source Code
  * The source code in this file ("Source Code") is provided by Linden Lab
@@ -33,127 +34,45 @@
 #ifndef LL_LLCHATMSGBOX_H
 #define LL_LLCHATMSGBOX_H
 
-
+#include "lltextbox.h"
 #include "lluictrl.h"
 #include "v4color.h"
 #include "llstring.h"
-#include "lluistring.h"
-
 
-class LLChatMsgBox
-:	public LLUICtrl
+///
+/// LLChatMsgBox provides a text box with support for multiple blocks
+/// of text that can be added incrementally. Each block of text is
+/// visual separated from the previous block (e.g., with a horizontal
+/// line).
+///
+class LLChatMsgBox :
+	public LLTextBox
 {
-protected:
-	struct text_block
-	{
-		LLUIString			text;
-		std::vector<S32>	lines;
-	};
 public:
-	typedef boost::function<void (void)> callback_t;
-
-	struct Params : public LLInitParam::Block<Params, LLUICtrl::Params>
+	struct Params : public LLInitParam::Block<Params, LLTextBox::Params>
 	{
-		Optional<std::string> text;
-
-		Optional<bool>		highlight_on_hover,
-							border_visible,
-							border_drop_shadow_visible,
-							bg_visible,
-							use_ellipses,
-							word_wrap;
-
-		Optional<LLFontGL::ShadowType>	font_shadow;
-
-		Optional<LLUIColor>	text_color,
-							hover_color,
-							disabled_color,
-							background_color,
-							border_color;
-
-		Optional<S32>		line_spacing;
-		
-		Optional<S32>		block_spacing;
+		Optional<S32>	block_spacing;
 
 		Params();
 	};
+
 protected:
 	LLChatMsgBox(const Params&);
 	friend class LLUICtrlFactory;
-public:
-	virtual void	draw();
-	virtual void	reshape(S32 width, S32 height, BOOL called_from_parent = TRUE);
 
-	virtual BOOL	handleMouseDown(S32 x, S32 y, MASK mask);
-	virtual BOOL	handleMouseUp(S32 x, S32 y, MASK mask);
-	virtual BOOL	handleHover(S32 x, S32 y, MASK mask);
-
-	void			setColor( const LLColor4& c )			{ mTextColor = c; }
-	void			setDisabledColor( const LLColor4& c)	{ mDisabledColor = c; }
-	void			setBackgroundColor( const LLColor4& c)	{ mBackgroundColor = c; }	
-	void			setBorderColor( const LLColor4& c)		{ mBorderColor = c; }	
-
-	void			setHoverColor( const LLColor4& c )		{ mHoverColor = c; }
-	void			setHoverActive( BOOL active )			{ mHoverActive = active; }
-
-	void			setText( const LLStringExplicit& text );
-	void			addText( const LLStringExplicit& text );
-	
-	void			setUseEllipses( BOOL use_ellipses )		{ mUseEllipses = use_ellipses; }
+public:
+	void				setText(const LLStringExplicit &text);
+	void				addText(const LLStringExplicit &text);
 	
-	void			setBackgroundVisible(BOOL visible)		{ mBackgroundVisible = visible; }
-	void			setBorderVisible(BOOL visible)			{ mBorderVisible = visible; }
-	void			setBorderDropshadowVisible(BOOL visible){ mBorderDropShadowVisible = visible; }
-	void			setRightAlign()							{ mHAlign = LLFontGL::RIGHT; }
-	void			setHAlign( LLFontGL::HAlign align )		{ mHAlign = align; }
-	void			setClickedCallback( boost::function<void (void*)> cb, void* userdata = NULL ){ mClickedCallback = boost::bind(cb, userdata); }		// mouse down and up within button
-
-	const LLFontGL* getFont() const							{ return mFontGL; }
-
-	S32				getTextPixelHeight();
-	S32				getTextLinesNum();
-
-	virtual void	setValue(const LLSD& value );		
-
+	S32					getTextPixelHeight();
+	S32					getTextLinesNum();
 
+	/*virtual*/ void	setValue(const LLSD &value);
+	/*virtual*/ void	drawText(S32 x, S32 y, const LLWString &text, const LLColor4 &color);
 
 private:
-	std::string		wrapText			(const LLStringExplicit& in_text, F32 max_width = -1.0);
-
-	void			setLineLengths		(text_block& t);
-	void			resetLineLengths	();
-	void			drawText			(S32 x, S32 y, const LLColor4& color );
-
-	const LLFontGL*	mFontGL;
-	LLUIColor	mTextColor;
-	LLUIColor	mDisabledColor;
-	LLUIColor	mBackgroundColor;
-	LLUIColor	mBorderColor;
-	LLUIColor	mHoverColor;
-
-	BOOL			mHoverActive;	
-	BOOL			mHasHover;
-	BOOL			mBackgroundVisible;
-	BOOL			mBorderVisible;
-	BOOL			mWordWrap;
-	
-	U8				mFontStyle; // style bit flags for font
-	LLFontGL::ShadowType mShadowType;
-	BOOL			mBorderDropShadowVisible;
-	BOOL			mUseEllipses;
-
-	S32				mLineSpacing;
-	S32				mBlockSpasing;
-
-	LLFontGL::HAlign mHAlign;
-	LLFontGL::VAlign mVAlign;
-
-	callback_t		mClickedCallback;
-
-
-	//same as mLineLengthList and mText in LLTextBox
-	std::vector< boost::shared_ptr<text_block> > mTextStrings;
-
+	S32					mBlockSpacing;
+	std::vector<S32>	mSeparatorOffset;
 };
 
 #endif
diff --git a/indra/newview/llfloaterabout.cpp b/indra/newview/llfloaterabout.cpp
index 56c5eaa70e..caa10e9452 100644
--- a/indra/newview/llfloaterabout.cpp
+++ b/indra/newview/llfloaterabout.cpp
@@ -99,24 +99,20 @@ BOOL LLFloaterAbout::postBuild()
 	LLViewerTextEditor *credits_widget = 
 		getChild<LLViewerTextEditor>("credits_editor", true);
 
-	// For some reason, adding style doesn't work unless this is true.
+	// make sure that we handle hyperlinks in the About text
 	support_widget->setParseHTML(TRUE);
 
-	// Text styles for release notes hyperlinks
-	LLStyle::Params link_style_params;
-	link_style_params.color.control = "HTMLLinkColor";
-	link_style_params.link_href = get_viewer_release_notes_url();
-
 	// Version string
 	std::string version = LLTrans::getString("APP_NAME")
 		+ llformat(" %d.%d.%d (%d) %s %s (%s)\n",
 				   LL_VERSION_MAJOR, LL_VERSION_MINOR, LL_VERSION_PATCH, LL_VIEWER_BUILD,
 				   __DATE__, __TIME__,
 				   gSavedSettings.getString("VersionChannelName").c_str());
-	support_widget->appendColoredText(version, FALSE, FALSE, LLUIColorTable::instance().getColor("TextFgReadOnlyColor"));
-	support_widget->appendStyledText(LLTrans::getString("ReleaseNotes"), false, false, link_style_params);
 
 	std::string support;
+	support.append(version);
+	support.append("[" + get_viewer_release_notes_url() + " " +
+				   LLTrans::getString("ReleaseNotes") + "]");
 	support.append("\n\n");
 
 #if LL_MSVC
@@ -131,10 +127,6 @@ BOOL LLFloaterAbout::postBuild()
 	LLViewerRegion* region = gAgent.getRegion();
 	if (region)
 	{
-		LLStyle::Params server_link_style_params;
-		server_link_style_params.color.control = "HTMLLinkColor";
-		server_link_style_params.link_href = region->getCapability("ServerReleaseNotes");
-
 		const LLVector3d &pos = gAgent.getPositionGlobal();
 		LLUIString pos_text = getString("you_are_at");
 		pos_text.setArg("[POSITION]",
@@ -154,11 +146,9 @@ BOOL LLFloaterAbout::postBuild()
 		support.append(")\n");
 		support.append(gLastVersionChannel);
 		support.append("\n");
-
-		support_widget->appendColoredText(support, FALSE, FALSE, LLUIColorTable::instance().getColor("TextFgReadOnlyColor"));
-		support_widget->appendStyledText(LLTrans::getString("ReleaseNotes"), false, false, server_link_style_params);
-
-		support = "\n\n";
+		support.append("[" + LLWeb::escapeURL(region->getCapability("ServerReleaseNotes")) +
+					   " " + LLTrans::getString("ReleaseNotes") + "]");
+		support.append("\n\n");
 	}
 
 	// *NOTE: Do not translate text like GPU, Graphics Card, etc -
@@ -248,20 +238,20 @@ BOOL LLFloaterAbout::postBuild()
 }
 
 
- static std::string get_viewer_release_notes_url()
- {
- 	std::ostringstream version;
- 	version << LL_VERSION_MAJOR << "."
- 		<< LL_VERSION_MINOR << "."
- 		<< LL_VERSION_PATCH << "."
- 		<< LL_VERSION_BUILD;
+static std::string get_viewer_release_notes_url()
+{
+	std::ostringstream version;
+	version << LL_VERSION_MAJOR << "."
+		<< LL_VERSION_MINOR << "."
+		<< LL_VERSION_PATCH << "."
+		<< LL_VERSION_BUILD;
 
- 	LLSD query;
- 	query["channel"] = gSavedSettings.getString("VersionChannelName");
- 	query["version"] = version.str();
+	LLSD query;
+	query["channel"] = gSavedSettings.getString("VersionChannelName");
+	query["version"] = version.str();
 
- 	std::ostringstream url;
-	 url << LLTrans::getString("RELEASE_NOTES_BASE_URL") << LLURI::mapToQueryString(query);
+	std::ostringstream url;
+	url << LLTrans::getString("RELEASE_NOTES_BASE_URL") << LLURI::mapToQueryString(query);
 
- 	return url.str();
- }
+	return LLWeb::escapeURL(url.str());
+}
diff --git a/indra/newview/llfloaterfriends.cpp b/indra/newview/llfloaterfriends.cpp
index eb73bd6d8f..0c77d88efb 100644
--- a/indra/newview/llfloaterfriends.cpp
+++ b/indra/newview/llfloaterfriends.cpp
@@ -194,6 +194,7 @@ BOOL LLPanelFriends::postBuild()
 	mFriendsList->setMaxSelectable(MAX_FRIEND_SELECT);
 	mFriendsList->setMaximumSelectCallback(boost::bind(&LLPanelFriends::onMaximumSelect));
 	mFriendsList->setCommitOnSelectionChange(TRUE);
+	mFriendsList->setContextMenu(LLScrollListCtrl::MENU_AVATAR);
 	childSetCommitCallback("friend_list", onSelectName, this);
 	getChild<LLScrollListCtrl>("friend_list")->setDoubleClickCallback(onClickIM, this);
 
diff --git a/indra/newview/llfloatergroups.cpp b/indra/newview/llfloatergroups.cpp
index 7a88612f1a..b1f40d9d1d 100644
--- a/indra/newview/llfloatergroups.cpp
+++ b/indra/newview/llfloatergroups.cpp
@@ -82,7 +82,12 @@ void LLFloaterGroupPicker::setPowersMask(U64 powers_mask)
 BOOL LLFloaterGroupPicker::postBuild()
 {
 	LLScrollListCtrl* list_ctrl = getChild<LLScrollListCtrl>("group list");
-	init_group_list(list_ctrl, gAgent.getGroupID(), mPowersMask);
+	if (list_ctrl)
+	{
+		init_group_list(list_ctrl, gAgent.getGroupID(), mPowersMask);
+		list_ctrl->setDoubleClickCallback(onBtnOK, this);
+		list_ctrl->setContextMenu(LLScrollListCtrl::MENU_GROUP);
+	}
 	
 	// Remove group "none" from list. Group "none" is added in init_group_list(). 
 	// Some UI elements use group "none", we need to manually delete it here.
@@ -100,8 +105,6 @@ BOOL LLFloaterGroupPicker::postBuild()
 
 	setDefaultBtn("OK");
 
-	getChild<LLScrollListCtrl>("group list")->setDoubleClickCallback(onBtnOK, this);
-
 	childEnable("OK");
 
 	return TRUE;
@@ -183,7 +186,13 @@ BOOL LLPanelGroups::postBuild()
 	childSetTextArg("groupcount", "[COUNT]", llformat("%d",gAgent.mGroups.count()));
 	childSetTextArg("groupcount", "[MAX]", llformat("%d",MAX_AGENT_GROUPS));
 
-	init_group_list(getChild<LLScrollListCtrl>("group list"), gAgent.getGroupID());
+	LLScrollListCtrl *list = getChild<LLScrollListCtrl>("group list");
+	if (list)
+	{
+		init_group_list(list, gAgent.getGroupID());
+		list->setDoubleClickCallback(onBtnIM, this);
+		list->setContextMenu(LLScrollListCtrl::MENU_GROUP);
+	}
 
 	childSetAction("Activate", onBtnActivate, this);
 
@@ -199,8 +208,6 @@ BOOL LLPanelGroups::postBuild()
 
 	setDefaultBtn("IM");
 
-	getChild<LLScrollListCtrl>("group list")->setDoubleClickCallback(onBtnIM, this);
-
 	reset();
 
 	return TRUE;
diff --git a/indra/newview/llfloaterland.cpp b/indra/newview/llfloaterland.cpp
index 4cd09faaaf..e5f5e8eedb 100644
--- a/indra/newview/llfloaterland.cpp
+++ b/indra/newview/llfloaterland.cpp
@@ -1061,6 +1061,7 @@ BOOL LLPanelLandObjects::postBuild()
 	mOwnerList->sortByColumnIndex(3, FALSE);
 	childSetCommitCallback("owner list", onCommitList, this);
 	mOwnerList->setDoubleClickCallback(onDoubleClickOwner, this);
+	mOwnerList->setContextMenu(LLScrollListCtrl::MENU_AVATAR);
 
 	return TRUE;
 }
@@ -2297,11 +2298,17 @@ BOOL LLPanelLandAccess::postBuild()
 	
 	mListAccess = getChild<LLNameListCtrl>("AccessList");
 	if (mListAccess)
+	{
 		mListAccess->sortByColumnIndex(0, TRUE); // ascending
+		mListAccess->setContextMenu(LLScrollListCtrl::MENU_AVATAR);
+	}
 
 	mListBanned = getChild<LLNameListCtrl>("BannedList");
 	if (mListBanned)
+	{
 		mListBanned->sortByColumnIndex(0, TRUE); // ascending
+		mListBanned->setContextMenu(LLScrollListCtrl::MENU_AVATAR);
+	}
 
 	return TRUE;
 }
diff --git a/indra/newview/llgrouplist.cpp b/indra/newview/llgrouplist.cpp
index 278fd5b9f6..73d3a60701 100644
--- a/indra/newview/llgrouplist.cpp
+++ b/indra/newview/llgrouplist.cpp
@@ -51,6 +51,8 @@ LLGroupList::Params::Params()
 LLGroupList::LLGroupList(const Params& p)
 :	LLAvatarList(p)
 {
+	// display a context menu appropriate for a list of group names
+	setContextMenu(LLScrollListCtrl::MENU_GROUP);
 }
 
 static bool findInsensitive(std::string haystack, const std::string& needle_upper)
diff --git a/indra/newview/llimpanel.cpp b/indra/newview/llimpanel.cpp
index 9cf3e57e22..674fff4040 100644
--- a/indra/newview/llimpanel.cpp
+++ b/indra/newview/llimpanel.cpp
@@ -2125,6 +2125,7 @@ BOOL LLIMFloater::postBuild()
 	childSetCommitCallback("chat_editor", onSendMsg, this);
 	
 	mHistoryEditor = getChild<LLViewerTextEditor>("im_text");
+	mHistoryEditor->setParseHTML(TRUE);
 		
 	setTitle(LLIMModel::instance().getName(mSessionID));
 	setDocked(true);
diff --git a/indra/newview/llnamelistctrl.cpp b/indra/newview/llnamelistctrl.cpp
index 1b82c2dc18..8ef6b25c50 100644
--- a/indra/newview/llnamelistctrl.cpp
+++ b/indra/newview/llnamelistctrl.cpp
@@ -61,9 +61,9 @@ LLNameListCtrl::Params::Params()
 
 LLNameListCtrl::LLNameListCtrl(const LLNameListCtrl::Params& p)
 :	LLScrollListCtrl(p),
-	mAllowCallingCardDrop(p.allow_calling_card_drop),
+	mNameColumnIndex(p.name_column.column_index),
 	mNameColumn(p.name_column.column_name),
-	mNameColumnIndex(p.name_column.column_index)
+	mAllowCallingCardDrop(p.allow_calling_card_drop)
 {}
 
 // public
diff --git a/indra/newview/llpanelavatar.cpp b/indra/newview/llpanelavatar.cpp
index 6e94b087a6..b2d606ab4d 100644
--- a/indra/newview/llpanelavatar.cpp
+++ b/indra/newview/llpanelavatar.cpp
@@ -595,9 +595,8 @@ BOOL LLPanelAvatarMeProfile::postBuild()
 
 	childSetCommitCallback("status_combo", boost::bind(&LLPanelAvatarMeProfile::onStatusChanged, this), NULL);
 	childSetCommitCallback("status_me_message_text", boost::bind(&LLPanelAvatarMeProfile::onStatusMessageChanged, this), NULL);
-	childSetActionTextbox("payment_update_link", boost::bind(&LLPanelAvatarMeProfile::onUpdateAccountTextboxClicked, this));
-	childSetActionTextbox("my_account_link", boost::bind(&LLPanelAvatarMeProfile::onMyAccountTextboxClicked, this));
-	childSetActionTextbox("partner_edit_link", boost::bind(&LLPanelAvatarMeProfile::onPartnerEditTextboxClicked, this));
+
+	childSetTextArg("partner_edit_link", "[URL]", getString("partner_edit_link_url"));
 
 	resetControls();
 	resetData();
@@ -677,17 +676,3 @@ void LLPanelAvatarMeProfile::onStatusMessageChanged()
 	updateData();
 }
 
-void LLPanelAvatarMeProfile::onUpdateAccountTextboxClicked()
-{
-	onUrlTextboxClicked(getString("payment_update_link_url"));
-}
-
-void LLPanelAvatarMeProfile::onMyAccountTextboxClicked()
-{
-	onUrlTextboxClicked(getString("my_account_link_url"));
-}
-
-void LLPanelAvatarMeProfile::onPartnerEditTextboxClicked()
-{
-	onUrlTextboxClicked(getString("partner_edit_link_url"));
-}
diff --git a/indra/newview/llpanelavatar.h b/indra/newview/llpanelavatar.h
index 51bd619901..4ee4cb6e87 100644
--- a/indra/newview/llpanelavatar.h
+++ b/indra/newview/llpanelavatar.h
@@ -203,9 +203,6 @@ protected:
 
 	void onStatusChanged();
 	void onStatusMessageChanged();
-	void onUpdateAccountTextboxClicked();
-	void onMyAccountTextboxClicked();
-	void onPartnerEditTextboxClicked();
 
 private:
 
diff --git a/indra/newview/llpanelgroupgeneral.cpp b/indra/newview/llpanelgroupgeneral.cpp
index 4b2a1a4e48..f3893a104c 100644
--- a/indra/newview/llpanelgroupgeneral.cpp
+++ b/indra/newview/llpanelgroupgeneral.cpp
@@ -113,6 +113,7 @@ BOOL LLPanelGroupGeneral::postBuild()
 	if (mListVisibleMembers)
 	{
 		mListVisibleMembers->setDoubleClickCallback(openProfile, this);
+		mListVisibleMembers->setContextMenu(LLScrollListCtrl::MENU_AVATAR);
 	}
 
 	// Options
diff --git a/indra/newview/llpanelgrouproles.cpp b/indra/newview/llpanelgrouproles.cpp
index 59f7319b2b..48c9c16780 100644
--- a/indra/newview/llpanelgrouproles.cpp
+++ b/indra/newview/llpanelgrouproles.cpp
@@ -843,6 +843,7 @@ BOOL LLPanelGroupMembersSubTab::postBuildSubTab(LLView* root)
 	mMembersList->setCommitCallback(onMemberSelect, this);
 	// Show the member's profile on double click.
 	mMembersList->setDoubleClickCallback(onMemberDoubleClick, this);
+	mMembersList->setContextMenu(LLScrollListCtrl::MENU_AVATAR);
 
 	LLButton* button = parent->getChild<LLButton>("member_invite", recurse);
 	if ( button )
@@ -1737,6 +1738,8 @@ BOOL LLPanelGroupRolesSubTab::postBuildSubTab(LLView* root)
 	mRolesList->setCommitOnSelectionChange(TRUE);
 	mRolesList->setCommitCallback(onRoleSelect, this);
 
+	mAssignedMembersList->setContextMenu(LLScrollListCtrl::MENU_AVATAR);
+
 	mMemberVisibleCheck->setCommitCallback(onMemberVisibilityChange, this);
 
 	mAllowedActionsList->setCommitOnSelectionChange(TRUE);
@@ -2403,6 +2406,7 @@ BOOL LLPanelGroupActionsSubTab::postBuildSubTab(LLView* root)
 
 	mActionList->setCommitOnSelectionChange(TRUE);
 	mActionList->setCommitCallback(boost::bind(&LLPanelGroupActionsSubTab::handleActionSelect, this));
+	mActionList->setContextMenu(LLScrollListCtrl::MENU_AVATAR);
 
 	update(GC_ALL);
 
diff --git a/indra/newview/llpreviewnotecard.cpp b/indra/newview/llpreviewnotecard.cpp
index cadab71ba8..29320522d9 100644
--- a/indra/newview/llpreviewnotecard.cpp
+++ b/indra/newview/llpreviewnotecard.cpp
@@ -87,6 +87,7 @@ BOOL LLPreviewNotecard::postBuild()
 	LLViewerTextEditor *ed = getChild<LLViewerTextEditor>("Notecard Editor");
 	if (ed)
 	{
+		ed->setParseHTML(TRUE);
 		ed->setNotecardInfo(mItemUUID, mObjectID, getKey());
 		ed->makePristine();
 	}
@@ -126,7 +127,7 @@ void LLPreviewNotecard::draw()
 {
 	LLViewerTextEditor* editor = getChild<LLViewerTextEditor>("Notecard Editor");
 	BOOL changed = !editor->isPristine();
-	
+
 	childSetEnabled("Save", changed && getEnabled());
 	
 	LLPreview::draw();
diff --git a/indra/newview/llprogressview.cpp b/indra/newview/llprogressview.cpp
index f70cfc59ec..aa603c417f 100644
--- a/indra/newview/llprogressview.cpp
+++ b/indra/newview/llprogressview.cpp
@@ -72,7 +72,6 @@ const S32 ANIMATION_FRAMES = 1; //13;
 LLProgressView::LLProgressView(const LLRect &rect) 
 :	LLPanel(),
 	mPercentDone( 0.f ),
-	mURLInMessage(false),
 	mMouseDownInActiveArea( false )
 {
 	LLUICtrlFactory::getInstance()->buildPanel(this, "panel_progress.xml");
@@ -207,12 +206,7 @@ void LLProgressView::setPercent(const F32 percent)
 void LLProgressView::setMessage(const std::string& msg)
 {
 	mMessage = msg;
-	mURLInMessage = (mMessage.find( "https://" ) != std::string::npos ||
-			 mMessage.find( "http://" ) != std::string::npos ||
-			 mMessage.find( "ftp://" ) != std::string::npos);
-
 	getChild<LLTextBox>("message_text")->setWrappedText(LLStringExplicit(mMessage));
-	getChild<LLTextBox>("message_text")->setHoverActive(mURLInMessage);
 }
 
 void LLProgressView::setCancelButtonVisible(BOOL b, const std::string& label)
diff --git a/indra/newview/llprogressview.h b/indra/newview/llprogressview.h
index 83574ff52a..865646c85d 100644
--- a/indra/newview/llprogressview.h
+++ b/indra/newview/llprogressview.h
@@ -74,7 +74,6 @@ protected:
 	LLFrameTimer mProgressTimer;
 	LLRect mOutlineRect;
 	bool mMouseDownInActiveArea;
-	bool mURLInMessage;
 
 	static LLProgressView* sInstance;
 };
diff --git a/indra/newview/llviewermessage.cpp b/indra/newview/llviewermessage.cpp
index 18fab3ec2e..dfb1c330e5 100644
--- a/indra/newview/llviewermessage.cpp
+++ b/indra/newview/llviewermessage.cpp
@@ -1,4 +1,3 @@
-
 /** 
  * @file llviewermessage.cpp
  * @brief Dumping ground for viewer-side message system callbacks.
@@ -1474,6 +1473,13 @@ void process_improved_im(LLMessageSystem *msg, void **user_data)
 	binary_bucket_size = msg->getSizeFast(_PREHASH_MessageBlock, _PREHASH_BinaryBucket);
 	EInstantMessage dialog = (EInstantMessage)d;
 
+    // make sure that we don't have an empty or all-whitespace name
+	LLStringUtil::trim(name);
+	if (name.empty())
+	{
+        name = LLTrans::getString("Unnamed");
+	}
+
 	BOOL is_busy = gAgent.getBusy();
 	BOOL is_muted = LLMuteList::getInstance()->isMuted(from_id, name, LLMute::flagTextChat);
 	BOOL is_linden = LLMuteList::getInstance()->isLinden(name);
diff --git a/indra/newview/llviewertexteditor.cpp b/indra/newview/llviewertexteditor.cpp
index de01e79803..5bb0c9a120 100644
--- a/indra/newview/llviewertexteditor.cpp
+++ b/indra/newview/llviewertexteditor.cpp
@@ -814,38 +814,18 @@ BOOL LLViewerTextEditor::handleMouseUp(S32 x, S32 y, MASK mask)
 
 BOOL LLViewerTextEditor::handleRightMouseDown(S32 x, S32 y, MASK mask)
 {
-	BOOL handled = childrenHandleRightMouseDown(x, y, mask) != NULL;
-
-	// *TODO: Add right click menus for SLURLs
-// 	if(! handled)
-// 	{
-// 		const LLTextSegment* cur_segment = getSegmentAtLocalPos( x, y );
-// 		if( cur_segment )
-// 		{
-// 			if(cur_segment->getStyle()->isLink())
-// 			{
-// 				handled = TRUE;
-// 				mHTML = cur_segment->getStyle()->getLinkHREF();
-// 			}
-// 		}
-// 	}
-// 	LLMenuGL* menu = (LLMenuGL*)mPopupMenuHandle.get();
-// 	if(handled && menu && mParseHTML && mHTML.length() > 0)
-// 	{
-// 		menu->setVisible(TRUE);
-// 		menu->arrange();
-// 		menu->updateParent(LLMenuGL::sMenuContainer);
-// 		LLMenuGL::showPopup(this, menu, x, y);
-// 		mHTML = "";
-// 	}
-// 	else
-// 	{
-// 		if(menu && menu->getVisible())
-// 		{
-// 			menu->setVisible(FALSE);
-// 		}
-// 	}
-	return handled;
+	// pop up a context menu for any Url under the cursor
+	if (handleRightMouseDownOverUrl(this, x, y))
+	{
+		return TRUE;
+	}
+
+	if (childrenHandleRightMouseDown(x, y, mask) != NULL)
+	{
+		return TRUE;
+	}
+
+	return FALSE;
 }
 
 BOOL LLViewerTextEditor::handleDoubleClick(S32 x, S32 y, MASK mask)
@@ -1087,6 +1067,7 @@ llwchar LLViewerTextEditor::pasteEmbeddedItem(llwchar ext_char)
 void LLViewerTextEditor::onValueChange(S32 start, S32 end)
 {
 	updateSegments();
+	updateLinkSegments();
 	findEmbeddedItemSegments(start, end);
 }
 
diff --git a/indra/newview/llviewertexteditor.h b/indra/newview/llviewertexteditor.h
index 9567bfbc48..2dfea4a589 100644
--- a/indra/newview/llviewertexteditor.h
+++ b/indra/newview/llviewertexteditor.h
@@ -35,7 +35,6 @@
 
 #include "lltexteditor.h"
 
-
 //
 // Classes
 //
@@ -137,9 +136,6 @@ private:
 
 	LLPointer<class LLEmbeddedNotecardOpener> mInventoryCallback;
 
-	// *TODO: Add right click menus for SLURLs
-	//LLViewHandle mPopupMenuHandle;
-
 	//
 	// Inner classes
 	//
diff --git a/indra/newview/llweb.cpp b/indra/newview/llweb.cpp
index 300a5db7c3..3204c2d264 100644
--- a/indra/newview/llweb.cpp
+++ b/indra/newview/llweb.cpp
@@ -67,6 +67,7 @@ void LLWeb::initClass()
 	LLAlertDialog::setURLLoader(&sAlertURLLoader);
 }
 
+
 // static
 void LLWeb::loadURL(const std::string& url)
 {
@@ -76,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 71cc236621..96a53db2ca 100644
--- a/indra/newview/llweb.h
+++ b/indra/newview/llweb.h
@@ -36,23 +36,29 @@
 
 #include <string>
 
+///
+/// The LLWeb class provides various static methods to display the
+/// contents of a Url in a web browser. Variations are provided to 
+/// let you specifically use the Second Life internal browser, the
+/// operating system's default browser, or to respect the user's
+/// setting for which of these two they prefer to use with SL.
+///
 class LLWeb
 {
 public:
 	static void initClass();
 	
-	// Loads unescaped url in either internal web browser or external
-	// browser, depending on user settings.
+	/// Load the given url in the user's preferred web browser
 	static void loadURL(const std::string& url);
-	
+	/// Load the given url in the user's preferred web browser	
 	static void loadURL(const char* url) { loadURL( ll_safe_string(url) ); }
-
-	// Loads unescaped url in external browser.
+	/// Load the given url in the Second Life internal web browser
+	static void loadURLInternal(const std::string &url);
+	/// Load the given url in the operating system's web browser
 	static void loadURLExternal(const std::string& url);
 
-	// Returns escaped (eg, " " to "%20") url
+	// Returns escaped url (eg, " " to "%20") - used by all loadURL methods
 	static std::string escapeURL(const std::string& url);
-
 };
 
 #endif
diff --git a/indra/newview/skins/default/xui/en/menu_url_agent.xml b/indra/newview/skins/default/xui/en/menu_url_agent.xml
new file mode 100644
index 0000000000..463a9fccb6
--- /dev/null
+++ b/indra/newview/skins/default/xui/en/menu_url_agent.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
+<context_menu
+ layout="topleft"
+ name="Url Popup">
+    <menu_item_call
+     label="Show Resident Profile"
+     layout="topleft"
+     name="show_agent">
+        <menu_item_call.on_click
+         function="Url.Execute" />
+    </menu_item_call>
+    <menu_item_separator
+     layout="topleft" />
+    <menu_item_call
+     label="Copy Name To Clipboard"
+     layout="topleft"
+     name="url_copy_label">
+        <menu_item_call.on_click
+         function="Url.CopyLabel" />
+    </menu_item_call>
+    <menu_item_call
+     label="Copy SLURL To Clipboard"
+     layout="topleft"
+     name="url_copy">
+        <menu_item_call.on_click
+         function="Url.CopyUrl" />
+    </menu_item_call>
+</context_menu>
diff --git a/indra/newview/skins/default/xui/en/menu_url_group.xml b/indra/newview/skins/default/xui/en/menu_url_group.xml
new file mode 100644
index 0000000000..cec0aa421e
--- /dev/null
+++ b/indra/newview/skins/default/xui/en/menu_url_group.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
+<context_menu
+ layout="topleft"
+ name="Url Popup">
+    <menu_item_call
+     label="Show Group Information"
+     layout="topleft"
+     name="show_group">
+        <menu_item_call.on_click
+         function="Url.Execute" />
+    </menu_item_call>
+    <menu_item_separator
+     layout="topleft" />
+    <menu_item_call
+     label="Copy Group To Clipboard"
+     layout="topleft"
+     name="url_copy_label">
+        <menu_item_call.on_click
+         function="Url.CopyLabel" />
+    </menu_item_call>
+    <menu_item_call
+     label="Copy SLURL To Clipboard"
+     layout="topleft"
+     name="url_copy">
+        <menu_item_call.on_click
+         function="Url.CopyUrl" />
+    </menu_item_call>
+</context_menu>
diff --git a/indra/newview/skins/default/xui/en/menu_url_http.xml b/indra/newview/skins/default/xui/en/menu_url_http.xml
new file mode 100644
index 0000000000..2503b4a2a3
--- /dev/null
+++ b/indra/newview/skins/default/xui/en/menu_url_http.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
+<context_menu
+ layout="topleft"
+ name="Url Popup">
+    <menu_item_call
+     label="Open Web Page"
+     layout="topleft"
+     name="url_open">
+        <menu_item_call.on_click
+         function="Url.Open" />
+    </menu_item_call>
+    <menu_item_separator
+     layout="topleft" />
+    <menu_item_call
+     label="Open in Internal Browser"
+     layout="topleft"
+     name="url_open_internal">
+        <menu_item_call.on_click
+         function="Url.OpenInternal" />
+    </menu_item_call>
+    <menu_item_call
+     label="Open in External Browser"
+     layout="topleft"
+     name="url_open_external">
+        <menu_item_call.on_click
+         function="Url.OpenExternal" />
+    </menu_item_call>
+    <menu_item_separator
+     layout="topleft" />
+    <menu_item_call
+     label="Copy URL To Clipboard"
+     layout="topleft"
+     name="url_copy">
+        <menu_item_call.on_click
+         function="Url.CopyUrl" />
+    </menu_item_call>
+</context_menu>
diff --git a/indra/newview/skins/default/xui/en/menu_url_objectim.xml b/indra/newview/skins/default/xui/en/menu_url_objectim.xml
new file mode 100644
index 0000000000..7d09403b15
--- /dev/null
+++ b/indra/newview/skins/default/xui/en/menu_url_objectim.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
+<context_menu
+ layout="topleft"
+ name="Url Popup">
+    <menu_item_call
+     label="Show Object Information"
+     layout="topleft"
+     name="show_object">
+        <menu_item_call.on_click
+         function="Url.Execute" />
+    </menu_item_call>
+    <menu_item_separator
+     layout="topleft" />
+    <menu_item_call
+     label="Teleport to Object Location"
+     layout="topleft"
+     name="teleport_to_object">
+        <menu_item_call.on_click
+         function="Url.Teleport" />
+    </menu_item_call>
+    <menu_item_separator
+     layout="topleft" />
+    <menu_item_call
+     label="Copy Object Name To Clipboard"
+     layout="topleft"
+     name="url_copy_label">
+        <menu_item_call.on_click
+         function="Url.CopyLabel" />
+    </menu_item_call>
+    <menu_item_call
+     label="Copy SLURL To Clipboard"
+     layout="topleft"
+     name="url_copy">
+        <menu_item_call.on_click
+         function="Url.CopyUrl" />
+    </menu_item_call>
+</context_menu>
diff --git a/indra/newview/skins/default/xui/en/menu_url_parcel.xml b/indra/newview/skins/default/xui/en/menu_url_parcel.xml
new file mode 100644
index 0000000000..bbd63c6d8c
--- /dev/null
+++ b/indra/newview/skins/default/xui/en/menu_url_parcel.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
+<context_menu
+ layout="topleft"
+ name="Url Popup">
+    <menu_item_call
+     label="Show Parcel Information"
+     layout="topleft"
+     name="show_parcel">
+        <menu_item_call.on_click
+         function="Url.Execute" />
+    </menu_item_call>
+    <menu_item_separator
+     layout="topleft" />
+    <menu_item_call
+     label="Copy SLURL To Clipboard"
+     layout="topleft"
+     name="url_copy">
+        <menu_item_call.on_click
+         function="Url.CopyUrl" />
+    </menu_item_call>
+</context_menu>
diff --git a/indra/newview/skins/default/xui/en/menu_url_slapp.xml b/indra/newview/skins/default/xui/en/menu_url_slapp.xml
new file mode 100644
index 0000000000..19df721b2b
--- /dev/null
+++ b/indra/newview/skins/default/xui/en/menu_url_slapp.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
+<context_menu
+ layout="topleft"
+ name="Url Popup">
+    <menu_item_call
+     label="Run This Command"
+     layout="topleft"
+     name="run_slapp">
+        <menu_item_call.on_click
+         function="Url.Execute" />
+    </menu_item_call>
+    <menu_item_separator
+     layout="topleft" />
+    <menu_item_call
+     label="Copy SLURL To Clipboard"
+     layout="topleft"
+     name="url_copy">
+        <menu_item_call.on_click
+         function="Url.CopyUrl" />
+    </menu_item_call>
+</context_menu>
diff --git a/indra/newview/skins/default/xui/en/menu_url_slurl.xml b/indra/newview/skins/default/xui/en/menu_url_slurl.xml
new file mode 100644
index 0000000000..0e9525fa4b
--- /dev/null
+++ b/indra/newview/skins/default/xui/en/menu_url_slurl.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
+<context_menu
+ layout="topleft"
+ name="Url Popup">
+    <menu_item_call
+     label="Show Place Information"
+     layout="topleft"
+     name="show_place">
+        <menu_item_call.on_click
+         function="Url.Execute" />
+    </menu_item_call>
+    <menu_item_separator
+     layout="topleft" />
+    <menu_item_call
+     label="Teleport to Location"
+     layout="topleft"
+     name="teleport_to_location">
+        <menu_item_call.on_click
+         function="Url.Teleport" />
+    </menu_item_call>
+    <menu_item_separator
+     layout="topleft" />
+    <menu_item_call
+     label="Copy SLURL To Clipboard"
+     layout="topleft"
+     name="url_copy">
+        <menu_item_call.on_click
+         function="Url.CopyUrl" />
+    </menu_item_call>
+</context_menu>
diff --git a/indra/newview/skins/default/xui/en/menu_url_teleport.xml b/indra/newview/skins/default/xui/en/menu_url_teleport.xml
new file mode 100644
index 0000000000..22cc035e49
--- /dev/null
+++ b/indra/newview/skins/default/xui/en/menu_url_teleport.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
+<context_menu
+ layout="topleft"
+ name="Url Popup">
+    <menu_item_call
+     label="Teleport To This Location"
+     layout="topleft"
+     name="teleport">
+        <menu_item_call.on_click
+         function="Url.Execute" />
+    </menu_item_call>
+    <menu_item_separator
+     layout="topleft" />
+    <menu_item_call
+     label="Copy SLURL To Clipboard"
+     layout="topleft"
+     name="url_copy">
+        <menu_item_call.on_click
+         function="Url.CopyUrl" />
+    </menu_item_call>
+</context_menu>
diff --git a/indra/newview/skins/default/xui/en/panel_edit_profile.xml b/indra/newview/skins/default/xui/en/panel_edit_profile.xml
index c0366437db..fa02cdb4b2 100644
--- a/indra/newview/skins/default/xui/en/panel_edit_profile.xml
+++ b/indra/newview/skins/default/xui/en/panel_edit_profile.xml
@@ -12,6 +12,10 @@
  name="edit_profile_panel"
  top="10"
  width="255">
+   <string
+    name="partner_edit_link_url">
+       http://www.secondlife.com/account/partners.php?lang=en
+   </string>
    <scroll_container
      color="DkGray2"
      follows="left|top|right|bottom"
@@ -243,11 +247,10 @@
          top_pad="15"
          value="Account Status:"
          width="100" />
-        <link
+        <text
          type="string"
          follows="left|top"
          font="SansSerifSmall"
-	 font.style="UNDERLINE"
          height="15"
          layout="topleft"
          left_pad="10"
@@ -277,15 +280,14 @@
          top_pad="15"
          value="Partner:"
          width="100" />
-	<link
+        <text
          follows="left|top"
          height="15"
-	 font.style="UNDERLINE"
          layout="topleft"
          left_pad="10"
          name="partner_edit_link"
          top_delta="0"
-         value="Edit"
+         value="[[URL] Edit]"
          width="100" />
         <panel
          follows="left|top|right"
diff --git a/indra/newview/skins/default/xui/en/panel_profile.xml b/indra/newview/skins/default/xui/en/panel_profile.xml
index d90be9ea25..135dcb167b 100644
--- a/indra/newview/skins/default/xui/en/panel_profile.xml
+++ b/indra/newview/skins/default/xui/en/panel_profile.xml
@@ -167,11 +167,10 @@
          width="255">
              Homepage:
         </text>
-        <link
+        <text
          follows="left|top|right"
          height="15"
          layout="topleft"
-	 font.style="UNDERLINE"
          left="10"
          name="homepage_edit"
          top_pad="5"
@@ -212,11 +211,10 @@
          top_pad="15"
          value="Account Status:"
          width="100" />
-       <!-- <link
+       <!-- <text
          type="string"
          follows="left|top"
          font="SansSerifSmall"
-	 font.style="UNDERLINE"
          height="15"
          layout="topleft"
          left_pad="10"
@@ -246,16 +244,15 @@
          top_pad="15"
          value="Partner:"
          width="100" />
-	<!--<link
+        <text
          follows="left|top"
          height="15"
-	 font.style="UNDERLINE"
          layout="topleft"
          left_pad="10"
          name="partner_edit_link"
          top_delta="0"
-         value="Edit"
-         width="100" />  -->
+         value="[[URL] Edit]"
+         width="100" />
         <panel
          follows="left|top|right"
          height="15"
diff --git a/indra/newview/skins/default/xui/en/panel_progress.xml b/indra/newview/skins/default/xui/en/panel_progress.xml
index 4f23c4d26d..9b2461db7c 100644
--- a/indra/newview/skins/default/xui/en/panel_progress.xml
+++ b/indra/newview/skins/default/xui/en/panel_progress.xml
@@ -99,12 +99,12 @@
                      halign="left"
                      height="100"
                      layout="topleft"
-                     left="30"
+                     left="45"
                      line_spacing="2"
                      name="message_text"
                      text_color="LoginProgressBoxTextColor"
                      top="145"
-                     width="610" />
+                     width="550" />
                 </layout_panel>
                 <layout_panel
                  height="200"
diff --git a/indra/newview/skins/default/xui/en/strings.xml b/indra/newview/skins/default/xui/en/strings.xml
index 323c08ec4c..b8152a4956 100644
--- a/indra/newview/skins/default/xui/en/strings.xml
+++ b/indra/newview/skins/default/xui/en/strings.xml
@@ -74,6 +74,18 @@
 	<string name="TooltipMustSingleDrop">Only a single item can be dragged here</string>	
 	<string name="TooltipAltLeft">Alt-Left arrow for previous tab</string>	
 	<string name="TooltipAltRight">Alt-Right arrow for next tab</string>	
+
+	<!-- tooltips for Urls -->
+	<string name="TooltipHttpUrl">Click to view this web page</string>
+	<string name="TooltipSLURL">Click to view this location's information</string>
+	<string name="TooltipAgentUrl">Click to view this resident's profile</string>
+	<string name="TooltipGroupUrl">Click to view this group's description</string>
+	<string name="TooltipEventUrl">Click to view this event's description</string>
+	<string name="TooltipClassifiedUrl">Click to view this classified</string>
+	<string name="TooltipParcelUrl">Click to view this parcel's description</string>
+	<string name="TooltipTeleportUrl">Click to teleport to this location</string>
+	<string name="TooltipObjectIMUrl">Click to view this object's description</string>
+	<string name="TooltipSLAPP">Click to run the secondlife:// command</string>
 	
 	<!-- ButtonToolTips, llfloater.cpp -->
 	<string name="BUTTON_CLOSE_DARWIN">Close (&#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>
-- 
GitLab