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