diff options
Diffstat (limited to 'indra/llui/lltextbase.cpp')
-rwxr-xr-x[-rw-r--r--] | indra/llui/lltextbase.cpp | 1719 |
1 files changed, 1307 insertions, 412 deletions
diff --git a/indra/llui/lltextbase.cpp b/indra/llui/lltextbase.cpp index 5f4b16ec9e..602a703450 100644..100755 --- a/indra/llui/lltextbase.cpp +++ b/indra/llui/lltextbase.cpp @@ -3,31 +3,25 @@ * @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. - * + * $LicenseInfo:firstyear=2009&license=viewerlgpl$ * 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 + * Copyright (C) 2009-2010, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. * - * 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 + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. * - * 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. + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * - * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO - * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY, - * COMPLETENESS OR PERFORMANCE. + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA * $/LicenseInfo$ */ @@ -38,8 +32,10 @@ #include "lllocalcliprect.h" #include "llmenugl.h" #include "llscrollcontainer.h" +#include "llspellcheck.h" #include "llstl.h" #include "lltextparser.h" +#include "lltextutil.h" #include "lltooltip.h" #include "lluictrl.h" #include "llurlaction.h" @@ -50,6 +46,7 @@ const F32 CURSOR_FLASH_DELAY = 1.0f; // in seconds const S32 CURSOR_THICKNESS = 2; +const F32 TRIPLE_CLICK_INTERVAL = 0.3f; // delay between double and triple click. LLTextBase::line_info::line_info(S32 index_start, S32 index_end, LLRect rect, S32 line_num) : mDocIndexStart(index_start), @@ -65,48 +62,44 @@ bool LLTextBase::compare_segment_end::operator()(const LLTextSegmentPtr& a, cons { return a->getStart() < b->getStart(); } - return a->getEnd() < b->getEnd(); + else + { + return a->getEnd() < b->getEnd(); + } } // helper functors -struct LLTextBase::compare_bottom +bool LLTextBase::compare_bottom::operator()(const S32& a, const LLTextBase::line_info& b) const { - bool operator()(const S32& a, const LLTextBase::line_info& b) const - { - return a > b.mRect.mBottom; // bottom of a is higher than bottom of b - } - - bool operator()(const LLTextBase::line_info& a, const S32& b) const - { - return a.mRect.mBottom > b; // bottom of a is higher than bottom of b - } + return a > b.mRect.mBottom; // bottom of a is higher than bottom of b +} - bool operator()(const LLTextBase::line_info& a, const LLTextBase::line_info& b) const - { - return a.mRect.mBottom > b.mRect.mBottom; // bottom of a is higher than bottom of b - } +bool LLTextBase::compare_bottom::operator()(const LLTextBase::line_info& a, const S32& b) const +{ + return a.mRect.mBottom > b; // bottom of a is higher than bottom of b +} -}; +bool LLTextBase::compare_bottom::operator()(const LLTextBase::line_info& a, const LLTextBase::line_info& b) const +{ + return a.mRect.mBottom > b.mRect.mBottom; // bottom of a is higher than bottom of b +} // helper functors -struct LLTextBase::compare_top +bool LLTextBase::compare_top::operator()(const S32& a, const LLTextBase::line_info& b) const { - bool operator()(const S32& a, const LLTextBase::line_info& b) const - { - return a > b.mRect.mTop; // top of a is higher than top of b - } + return a > b.mRect.mTop; // top of a is higher than top of b +} - bool operator()(const LLTextBase::line_info& a, const S32& b) const - { - return a.mRect.mTop > b; // top of a is higher than top of b - } +bool LLTextBase::compare_top::operator()(const LLTextBase::line_info& a, const S32& b) const +{ + return a.mRect.mTop > b; // top of a is higher than top of b +} - bool operator()(const LLTextBase::line_info& a, const LLTextBase::line_info& b) const - { - return a.mRect.mTop > b.mRect.mTop; // top of a is higher than top of b - } -}; +bool LLTextBase::compare_top::operator()(const LLTextBase::line_info& a, const LLTextBase::line_info& b) const +{ + return a.mRect.mTop > b.mRect.mTop; // top of a is higher than top of b +} struct LLTextBase::line_end_compare { @@ -146,45 +139,60 @@ LLTextBase::Params::Params() : cursor_color("cursor_color"), text_color("text_color"), text_readonly_color("text_readonly_color"), + text_tentative_color("text_tentative_color"), bg_visible("bg_visible", false), border_visible("border_visible", false), bg_readonly_color("bg_readonly_color"), bg_writeable_color("bg_writeable_color"), bg_focus_color("bg_focus_color"), + text_selected_color("text_selected_color"), + bg_selected_color("bg_selected_color"), allow_scroll("allow_scroll", true), + plain_text("plain_text",false), track_end("track_end", false), read_only("read_only", false), + spellcheck("spellcheck", false), v_pad("v_pad", 0), h_pad("h_pad", 0), + clip("clip", true), clip_partial("clip_partial", true), line_spacing("line_spacing"), max_text_length("max_length", 255), font_shadow("font_shadow"), wrap("wrap"), + trusted_content("trusted_content", true), use_ellipses("use_ellipses", false), - allow_html("allow_html", false), + parse_urls("parse_urls", false), parse_highlights("parse_highlights", false) { addSynonym(track_end, "track_bottom"); addSynonym(wrap, "word_wrap"); + addSynonym(parse_urls, "allow_html"); } LLTextBase::LLTextBase(const LLTextBase::Params &p) : LLUICtrl(p, LLTextViewModelPtr(new LLTextViewModel)), - mURLClickSignal(), + mURLClickSignal(NULL), + mIsFriendSignal(NULL), mMaxTextByteLength( p.max_text_length ), - mDefaultFont(p.font), + mFont(p.font), mFontShadow(p.font_shadow), mPopupMenu(NULL), mReadOnly(p.read_only), + mSpellCheck(p.spellcheck), + mSpellCheckStart(-1), + mSpellCheckEnd(-1), mCursorColor(p.cursor_color), mFgColor(p.text_color), mBorderVisible( p.border_visible ), mReadOnlyFgColor(p.text_readonly_color), + mTentativeFgColor(p.text_tentative_color()), mWriteableBgColor(p.bg_writeable_color), mReadOnlyBgColor(p.bg_readonly_color), mFocusBgColor(p.bg_focus_color), + mTextSelectedColor(p.text_selected_color), + mSelectedBGColor(p.bg_selected_color), mReflowIndex(S32_MAX), mCursorPos( 0 ), mScrollNeeded(FALSE), @@ -192,20 +200,25 @@ LLTextBase::LLTextBase(const LLTextBase::Params &p) mHPad(p.h_pad), mVPad(p.v_pad), mHAlign(p.font_halign), + mVAlign(p.font_valign), mLineSpacingMult(p.line_spacing.multiple), mLineSpacingPixels(p.line_spacing.pixels), + mClip(p.clip), mClipPartial(p.clip_partial && !p.allow_scroll), + mTrustedContent(p.trusted_content), mTrackEnd( p.track_end ), mScrollIndex(-1), mSelectionStart( 0 ), mSelectionEnd( 0 ), mIsSelecting( FALSE ), + mPlainText ( p.plain_text ), mWordWrap(p.wrap), mUseEllipses( p.use_ellipses ), - mParseHTML(p.allow_html), + mParseHTML(p.parse_urls), mParseHighlights(p.parse_highlights), mBGVisible(p.bg_visible), - mScroller(NULL) + mScroller(NULL), + mStyleDirty(true) { if(p.allow_scroll) { @@ -237,6 +250,12 @@ LLTextBase::LLTextBase(const LLTextBase::Params &p) addChild(mDocumentView); } + if (mSpellCheck) + { + LLSpellChecker::setSettingsChangeCallback(boost::bind(&LLTextBase::onSpellCheckSettingsChange, this)); + } + mSpellCheckTimer.reset(); + createDefaultSegment(); updateRects(); @@ -244,9 +263,8 @@ LLTextBase::LLTextBase(const LLTextBase::Params &p) LLTextBase::~LLTextBase() { - // Menu, like any other LLUICtrl, is deleted by its parent - gMenuHolder - mSegments.clear(); + delete mURLClickSignal; } void LLTextBase::initFromParams(const LLTextBase::Params& p) @@ -262,9 +280,6 @@ void LLTextBase::initFromParams(const LLTextBase::Params& p) { mReadOnly = p.read_only; } - - // HACK: text editors always need to be enabled so that we can scroll - LLView::setEnabled(true); } bool LLTextBase::truncate() @@ -275,12 +290,23 @@ bool LLTextBase::truncate() if (getLength() >= S32(mMaxTextByteLength / 4)) { // Have to check actual byte size - LLWString text(getWText()); - S32 utf8_byte_size = wstring_utf8_length(text); + S32 utf8_byte_size = 0; + LLSD value = getViewModel()->getValue(); + if (value.type() == LLSD::TypeString) + { + // save a copy for strings. + utf8_byte_size = value.size(); + } + else + { + // non string LLSDs need explicit conversion to string + utf8_byte_size = value.asString().size(); + } + if ( utf8_byte_size > mMaxTextByteLength ) { // Truncate safely in UTF-8 - std::string temp_utf8_text = wstring_to_utf8str(text); + std::string temp_utf8_text = value.asString(); temp_utf8_text = utf8str_truncate( temp_utf8_text, mMaxTextByteLength ); LLWString text = utf8str_to_wstring( temp_utf8_text ); // remove extra bit of current string, to preserve formatting, etc. @@ -292,13 +318,26 @@ bool LLTextBase::truncate() return did_truncate; } -LLStyle::Params LLTextBase::getDefaultStyleParams() +const LLStyle::Params& LLTextBase::getStyleParams() { - return LLStyle::Params() - .color(LLUIColor(&mFgColor)) - .readonly_color(LLUIColor(&mReadOnlyFgColor)) - .font(mDefaultFont) - .drop_shadow(mFontShadow); + //FIXME: convert mDefaultStyle to a flyweight http://www.boost.org/doc/libs/1_40_0/libs/flyweight/doc/index.html + //and eliminate color member values + if (mStyleDirty) + { + mStyle + .color(LLUIColor(&mFgColor)) // pass linked color instead of copy of mFGColor + .readonly_color(LLUIColor(&mReadOnlyFgColor)) + .selected_color(LLUIColor(&mTextSelectedColor)) + .font(mFont) + .drop_shadow(mFontShadow); + mStyleDirty = false; + } + return mStyle; +} + +void LLTextBase::beforeValueChange() +{ + } void LLTextBase::onValueChange(S32 start, S32 end) @@ -316,14 +355,13 @@ void LLTextBase::drawSelectionBackground() S32 selection_left = llmin( mSelectionStart, mSelectionEnd ); S32 selection_right = llmax( mSelectionStart, mSelectionEnd ); - LLRect selection_rect = mVisibleTextRect; // Skip through the lines we aren't drawing. LLRect content_display_rect = getVisibleDocumentRect(); // binary search for line that starts before top of visible buffer line_list_t::const_iterator line_iter = std::lower_bound(mLineInfoList.begin(), mLineInfoList.end(), content_display_rect.mTop, compare_bottom()); - line_list_t::const_iterator end_iter = std::lower_bound(mLineInfoList.begin(), mLineInfoList.end(), content_display_rect.mBottom, compare_top()); + line_list_t::const_iterator end_iter = std::upper_bound(mLineInfoList.begin(), mLineInfoList.end(), content_display_rect.mBottom, compare_top()); bool done = false; @@ -391,7 +429,7 @@ void LLTextBase::drawSelectionBackground() // Draw the selection box (we're using a box instead of reversing the colors on the selected text). gGL.getTexUnit(0)->unbind(LLTexUnit::TT_TEXTURE); - const LLColor4& color = mReadOnly ? mReadOnlyFgColor.get() : mFgColor.get(); + const LLColor4& color = mSelectedBGColor; F32 alpha = hasFocus() ? 0.7f : 0.3f; alpha *= getDrawContext().mAlpha; LLColor4 selection_color(color.mV[VRED], color.mV[VGREEN], color.mV[VBLUE], alpha); @@ -401,6 +439,7 @@ void LLTextBase::drawSelectionBackground() ++rect_it) { LLRect selection_rect = *rect_it; + selection_rect = *rect_it; selection_rect.translate(mVisibleTextRect.mLeft - content_display_rect.mLeft, mVisibleTextRect.mBottom - content_display_rect.mBottom); gl_rect_2d(selection_rect, selection_color); } @@ -431,7 +470,6 @@ void LLTextBase::drawCursor() } else { - //segmentp = mSegments.back(); return; } @@ -465,24 +503,11 @@ void LLTextBase::drawCursor() { LLColor4 text_color; const LLFontGL* fontp; - if (segmentp) - { - text_color = segmentp->getColor(); - fontp = segmentp->getStyle()->getFont(); - } - else if (mReadOnly) - { - text_color = mReadOnlyFgColor.get(); - fontp = mDefaultFont; - } - else - { - text_color = mFgColor.get(); - fontp = mDefaultFont; - } - fontp->render(text, mCursorPos, cursor_rect.mLeft, cursor_rect.mTop, + text_color = segmentp->getColor(); + fontp = segmentp->getStyle()->getFont(); + fontp->render(text, mCursorPos, cursor_rect, LLColor4(1.f - text_color.mV[VRED], 1.f - text_color.mV[VGREEN], 1.f - text_color.mV[VBLUE], alpha), - LLFontGL::LEFT, LLFontGL::TOP, + LLFontGL::LEFT, mVAlign, LLFontGL::NORMAL, LLFontGL::NO_SHADOW, 1); @@ -492,8 +517,8 @@ void LLTextBase::drawCursor() LLRect screen_pos = calcScreenRect(); LLCoordGL ime_pos( screen_pos.mLeft + llfloor(cursor_rect.mLeft), screen_pos.mBottom + llfloor(cursor_rect.mTop) ); - ime_pos.mX = (S32) (ime_pos.mX * LLUI::sGLScaleFactor.mV[VX]); - ime_pos.mY = (S32) (ime_pos.mY * LLUI::sGLScaleFactor.mV[VY]); + ime_pos.mX = (S32) (ime_pos.mX * LLUI::getScaleFactor().mV[VX]); + ime_pos.mY = (S32) (ime_pos.mY * LLUI::getScaleFactor().mV[VY]); getWindow()->setLanguageTextInput( ime_pos ); } } @@ -501,11 +526,17 @@ void LLTextBase::drawCursor() void LLTextBase::drawText() { - const S32 text_len = getLength(); - if( text_len <= 0 ) + S32 text_len = getLength(); + + if (text_len <= 0 && mLabel.empty()) { return; } + else if (useLabel()) + { + text_len = mLabel.getWString().length(); + } + S32 selection_left = -1; S32 selection_right = -1; // Draw selection even if we don't have keyboard focus for search/replace @@ -515,7 +546,6 @@ void LLTextBase::drawText() selection_right = llmax( mSelectionStart, mSelectionEnd ); } - LLRect scrolled_view_rect = getVisibleDocumentRect(); std::pair<S32, S32> line_range = getVisibleLines(mClipPartial); S32 first_line = line_range.first; S32 last_line = line_range.second; @@ -532,8 +562,102 @@ void LLTextBase::drawText() return; } + // Perform spell check if needed + if ( (getSpellCheck()) && (getWText().length() > 2) ) + { + // Calculate start and end indices for the spell checking range + S32 start = line_start; + S32 end = getLineEnd(last_line); + + if ( (mSpellCheckStart != start) || (mSpellCheckEnd != end) ) + { + const LLWString& wstrText = getWText(); + mMisspellRanges.clear(); + + segment_set_t::const_iterator seg_it = getSegIterContaining(start); + while (mSegments.end() != seg_it) + { + LLTextSegmentPtr text_segment = *seg_it; + if ( (text_segment.isNull()) || (text_segment->getStart() >= end) ) + { + break; + } + + if (!text_segment->canEdit()) + { + ++seg_it; + continue; + } + + // Combine adjoining text segments into one + U32 seg_start = text_segment->getStart(), seg_end = llmin(text_segment->getEnd(), end); + while (mSegments.end() != ++seg_it) + { + text_segment = *seg_it; + if ( (text_segment.isNull()) || (!text_segment->canEdit()) || (text_segment->getStart() >= end) ) + { + break; + } + seg_end = llmin(text_segment->getEnd(), end); + } + + // Find the start of the first word + U32 word_start = seg_start, word_end = -1; + U32 text_length = wstrText.length(); + while ( (word_start < text_length) && (!LLStringOps::isAlpha(wstrText[word_start])) ) + { + word_start++; + } + + // Iterate over all words in the text block and check them one by one + while (word_start < seg_end) + { + // Find the end of the current word (special case handling for "'" when it's used as a contraction) + word_end = word_start + 1; + while ( (word_end < seg_end) && + ((LLWStringUtil::isPartOfWord(wstrText[word_end])) || + ((L'\'' == wstrText[word_end]) && + (LLStringOps::isAlnum(wstrText[word_end - 1])) && (LLStringOps::isAlnum(wstrText[word_end + 1])))) ) + { + word_end++; + } + if (word_end > seg_end) + { + break; + } + + if (word_start < text_length && word_end <= text_length && word_end > word_start) + { + std::string word = wstring_to_utf8str(wstrText.substr(word_start, word_end - word_start)); + + // Don't process words shorter than 3 characters + if ( (word.length() >= 3) && (!LLSpellChecker::instance().checkSpelling(word)) ) + { + mMisspellRanges.push_back(std::pair<U32, U32>(word_start, word_end)); + } + } + + // Find the start of the next word + word_start = word_end + 1; + while ( (word_start < seg_end) && (!LLWStringUtil::isPartOfWord(wstrText[word_start])) ) + { + word_start++; + } + } + } + + mSpellCheckStart = start; + mSpellCheckEnd = end; + } + } + else + { + mMisspellRanges.clear(); + } + LLTextSegmentPtr cur_segment = *seg_iter; + std::list<std::pair<U32, U32> >::const_iterator misspell_it = std::lower_bound(mMisspellRanges.begin(), mMisspellRanges.end(), std::pair<U32, U32>(line_start, 0)); for (S32 cur_line = first_line; cur_line < last_line; cur_line++) { S32 next_line = cur_line + 1; @@ -548,10 +672,9 @@ void LLTextBase::drawText() line_end = next_start; } - LLRect text_rect(line.mRect.mLeft + mVisibleTextRect.mLeft - scrolled_view_rect.mLeft, - line.mRect.mTop - scrolled_view_rect.mBottom + mVisibleTextRect.mBottom, - llmin(mDocumentView->getRect().getWidth(), line.mRect.mRight) - scrolled_view_rect.mLeft, - line.mRect.mBottom - scrolled_view_rect.mBottom + mVisibleTextRect.mBottom); + LLRect text_rect(line.mRect); + text_rect.mRight = mDocumentView->getRect().getWidth(); // clamp right edge to document extents + text_rect.translate(mDocumentView->getRect().mLeft, mDocumentView->getRect().mBottom); // adjust by scroll position // draw a single line of text S32 seg_start = line_start; @@ -562,14 +685,15 @@ void LLTextBase::drawText() seg_iter++; if (seg_iter == mSegments.end()) { - llwarns << "Ran off the segmentation end!" << llendl; + LL_WARNS() << "Ran off the segmentation end!" << LL_ENDL; return; } cur_segment = *seg_iter; } - S32 clipped_end = llmin( line_end, cur_segment->getEnd() ) - cur_segment->getStart(); + S32 seg_end = llmin(line_end, cur_segment->getEnd()); + S32 clipped_end = seg_end - cur_segment->getStart(); if (mUseEllipses // using ellipses && clipped_end == line_end // last segment on line @@ -581,6 +705,46 @@ void LLTextBase::drawText() text_rect.mRight -= 2; } + // Draw squiggly lines under any visible misspelled words + while ( (mMisspellRanges.end() != misspell_it) && (misspell_it->first < seg_end) && (misspell_it->second > seg_start) ) + { + // Skip the current word if the user is still busy editing it + if ( (!mSpellCheckTimer.hasExpired()) && (misspell_it->first <= (U32)mCursorPos) && (misspell_it->second >= (U32)mCursorPos) ) + { + ++misspell_it; + continue; + } + + U32 misspell_start = llmax<U32>(misspell_it->first, seg_start), misspell_end = llmin<U32>(misspell_it->second, seg_end); + S32 squiggle_start = 0, squiggle_end = 0, pony = 0; + cur_segment->getDimensions(seg_start - cur_segment->getStart(), misspell_start - seg_start, squiggle_start, pony); + cur_segment->getDimensions(misspell_start - cur_segment->getStart(), misspell_end - misspell_start, squiggle_end, pony); + squiggle_start += text_rect.mLeft; + + pony = (squiggle_end + 3) / 6; + squiggle_start += squiggle_end / 2 - pony * 3; + squiggle_end = squiggle_start + pony * 6; + + S32 squiggle_bottom = text_rect.mBottom + (S32)cur_segment->getStyle()->getFont()->getDescenderHeight(); + + gGL.color4ub(255, 0, 0, 200); + while (squiggle_start + 1 < squiggle_end) + { + gl_line_2d(squiggle_start, squiggle_bottom, squiggle_start + 2, squiggle_bottom - 2); + if (squiggle_start + 3 < squiggle_end) + { + gl_line_2d(squiggle_start + 2, squiggle_bottom - 3, squiggle_start + 4, squiggle_bottom - 1); + } + squiggle_start += 4; + } + + if (misspell_it->second > seg_end) + { + break; + } + ++misspell_it; + } + text_rect.mLeft = (S32)(cur_segment->draw(seg_start - cur_segment->getStart(), clipped_end, selection_left, selection_right, text_rect)); seg_start = clipped_end + cur_segment->getStart(); @@ -595,13 +759,14 @@ void LLTextBase::drawText() S32 LLTextBase::insertStringNoUndo(S32 pos, const LLWString &wstr, LLTextBase::segment_vec_t* segments ) { - LLWString text(getWText()); - S32 old_len = text.length(); // length() returns character length + beforeValueChange(); + + S32 old_len = getLength(); // length() returns character length S32 insert_len = wstr.length(); pos = getEditableIndex(pos, true); - segment_set_t::iterator seg_iter = getSegIterContaining(pos); + segment_set_t::iterator seg_iter = getEditableSegIterContaining(pos); LLTextSegmentPtr default_segment; @@ -627,7 +792,7 @@ S32 LLTextBase::insertStringNoUndo(S32 pos, const LLWString &wstr, LLTextBase::s else { // create default editable segment to hold new text - LLStyleConstSP sp(new LLStyle(getDefaultStyleParams())); + LLStyleConstSP sp(new LLStyle(getStyleParams())); default_segment = new LLNormalTextSegment( sp, pos, pos + insert_len, *this); } @@ -656,8 +821,7 @@ S32 LLTextBase::insertStringNoUndo(S32 pos, const LLWString &wstr, LLTextBase::s } } - text.insert(pos, wstr); - getViewModel()->setDisplay(text); + getViewModel()->getEditableDisplay().insert(pos, wstr); if ( truncate() ) { @@ -672,7 +836,8 @@ S32 LLTextBase::insertStringNoUndo(S32 pos, const LLWString &wstr, LLTextBase::s S32 LLTextBase::removeStringNoUndo(S32 pos, S32 length) { - LLWString text(getWText()); + + beforeValueChange(); segment_set_t::iterator seg_iter = getSegIterContaining(pos); while(seg_iter != mSegments.end()) { @@ -718,8 +883,7 @@ S32 LLTextBase::removeStringNoUndo(S32 pos, S32 length) ++seg_iter; } - text.erase(pos, length); - getViewModel()->setDisplay(text); + getViewModel()->getEditableDisplay().erase(pos, length); // recreate default segment in case we erased everything createDefaultSegment(); @@ -732,13 +896,13 @@ S32 LLTextBase::removeStringNoUndo(S32 pos, S32 length) S32 LLTextBase::overwriteCharNoUndo(S32 pos, llwchar wc) { + beforeValueChange(); + if (pos > (S32)getLength()) { return 0; } - LLWString text(getWText()); - text[pos] = wc; - getViewModel()->setDisplay(text); + getViewModel()->getEditableDisplay()[pos] = wc; onValueChange(pos, pos + 1); needsReflow(pos); @@ -752,7 +916,7 @@ void LLTextBase::createDefaultSegment() // ensures that there is always at least one segment if (mSegments.empty()) { - LLStyleConstSP sp(new LLStyle(getDefaultStyleParams())); + LLStyleConstSP sp(new LLStyle(getStyleParams())); LLTextSegmentPtr default_segment = new LLNormalTextSegment( sp, 0, getLength() + 1, *this); mSegments.insert(default_segment); default_segment->linkToDocument(this); @@ -842,6 +1006,13 @@ void LLTextBase::insertSegment(LLTextSegmentPtr segment_to_insert) BOOL LLTextBase::handleMouseDown(S32 x, S32 y, MASK mask) { + // handle triple click + if (!mTripleClickTimer.hasExpired()) + { + selectAll(); + return TRUE; + } + LLTextSegmentPtr cur_segment = getSegmentAtLocalPos(x, y); if (cur_segment && cur_segment->handleMouseDown(x, y, mask)) { @@ -854,14 +1025,15 @@ BOOL LLTextBase::handleMouseDown(S32 x, S32 y, MASK mask) BOOL LLTextBase::handleMouseUp(S32 x, S32 y, MASK mask) { LLTextSegmentPtr cur_segment = getSegmentAtLocalPos(x, y); - if (cur_segment && cur_segment->handleMouseUp(x, y, mask)) + if (hasMouseCapture() && cur_segment && cur_segment->handleMouseUp(x, y, mask)) { // Did we just click on a link? - if (cur_segment->getStyle() - && cur_segment->getStyle()->isLink()) + if (mURLClickSignal + && cur_segment->getStyle() + && cur_segment->getStyle()->isLink()) { // *TODO: send URL here? - mURLClickSignal(this, LLSD() ); + (*mURLClickSignal)(this, LLSD() ); } return TRUE; } @@ -915,6 +1087,14 @@ BOOL LLTextBase::handleRightMouseUp(S32 x, S32 y, MASK mask) BOOL LLTextBase::handleDoubleClick(S32 x, S32 y, MASK mask) { + //Don't start triple click timer if user have clicked on scrollbar + mVisibleTextRect = mScroller ? mScroller->getContentWindowRect() : getLocalRect(); + if (x >= mVisibleTextRect.mLeft && x <= mVisibleTextRect.mRight + && y >= mVisibleTextRect.mBottom && y <= mVisibleTextRect.mTop) + { + mTripleClickTimer.setTimerExpirySec(TRIPLE_CLICK_INTERVAL); + } + LLTextSegmentPtr cur_segment = getSegmentAtLocalPos(x, y); if (cur_segment && cur_segment->handleDoubleClick(x, y, mask)) { @@ -962,8 +1142,20 @@ void LLTextBase::reshape(S32 width, S32 height, BOOL called_from_parent) { if (width != getRect().getWidth() || height != getRect().getHeight()) { + bool scrolled_to_bottom = mScroller ? mScroller->isAtBottom() : false; + LLUICtrl::reshape( width, height, called_from_parent ); + if (mScroller && scrolled_to_bottom && mTrackEnd) + { + // keep bottom of text buffer visible + // do this here as well as in reflow to handle case + // where shrinking from top, which causes buffer to temporarily + // not be scrolled to the bottom, since the scroll index + // specified the _top_ of the visible document region + mScroller->goToBottom(); + } + // do this first after reshape, because other things depend on // up-to-date mVisibleTextRect updateRects(); @@ -983,39 +1175,68 @@ void LLTextBase::draw() updateScrollFromCursor(); } - LLRect doc_rect; + LLRect text_rect; if (mScroller) { - mScroller->localRectToOtherView(mScroller->getContentWindowRect(), &doc_rect, this); + mScroller->localRectToOtherView(mScroller->getContentWindowRect(), &text_rect, this); } else { - doc_rect = getLocalRect(); + LLRect visible_lines_rect; + std::pair<S32, S32> line_range = getVisibleLines(mClipPartial); + for (S32 i = line_range.first; i < line_range.second; i++) + { + if (visible_lines_rect.isEmpty()) + { + visible_lines_rect = mLineInfoList[i].mRect; + } + else + { + visible_lines_rect.unionWith(mLineInfoList[i].mRect); + } + } + text_rect = visible_lines_rect; + text_rect.translate(mDocumentView->getRect().mLeft, mDocumentView->getRect().mBottom); } if (mBGVisible) { + F32 alpha = getCurrentTransparency(); // clip background rect against extents, if we support scrolling - LLLocalClipRect clip(doc_rect, mScroller != NULL); - + LLRect bg_rect = mVisibleTextRect; + if (mScroller) + { + bg_rect.intersectWith(text_rect); + } LLColor4 bg_color = mReadOnly ? mReadOnlyBgColor.get() : hasFocus() ? mFocusBgColor.get() : mWriteableBgColor.get(); - gl_rect_2d(mVisibleTextRect, bg_color, TRUE); + gl_rect_2d(text_rect, bg_color % alpha, TRUE); } - // draw document view - LLUICtrl::draw(); - - { - // only clip if we support scrolling (mScroller != NULL) - LLLocalClipRect clip(doc_rect, mScroller != NULL); + bool should_clip = mClip || mScroller != NULL; + { LLLocalClipRect clip(text_rect, should_clip); + + // draw document view + if (mScroller) + { + drawChild(mScroller); + } + else + { + drawChild(mDocumentView); + } + drawSelectionBackground(); drawText(); drawCursor(); } + + mDocumentView->setVisible(FALSE); + LLUICtrl::draw(); + mDocumentView->setVisible(TRUE); } @@ -1023,22 +1244,24 @@ void LLTextBase::draw() void LLTextBase::setColor( const LLColor4& c ) { mFgColor = c; + mStyleDirty = true; } //virtual void LLTextBase::setReadOnlyColor(const LLColor4 &c) { mReadOnlyFgColor = c; + mStyleDirty = true; } //virtual -void LLTextBase::handleVisibilityChange( BOOL new_visibility ) +void LLTextBase::onVisibilityChange( BOOL new_visibility ) { if(!new_visibility && mPopupMenu) { mPopupMenu->hide(); } - LLUICtrl::handleVisibilityChange(new_visibility); + LLUICtrl::onVisibilityChange(new_visibility); } //virtual @@ -1048,6 +1271,13 @@ void LLTextBase::setValue(const LLSD& value ) } //virtual +BOOL LLTextBase::canDeselect() const +{ + return hasSelection(); +} + + +//virtual void LLTextBase::deselect() { mSelectionStart = 0; @@ -1055,6 +1285,119 @@ void LLTextBase::deselect() mIsSelecting = FALSE; } +bool LLTextBase::getSpellCheck() const +{ + return (LLSpellChecker::getUseSpellCheck()) && (!mReadOnly) && (mSpellCheck); +} + +const std::string& LLTextBase::getSuggestion(U32 index) const +{ + return (index < mSuggestionList.size()) ? mSuggestionList[index] : LLStringUtil::null; +} + +U32 LLTextBase::getSuggestionCount() const +{ + return mSuggestionList.size(); +} + +void LLTextBase::replaceWithSuggestion(U32 index) +{ + for (std::list<std::pair<U32, U32> >::const_iterator it = mMisspellRanges.begin(); it != mMisspellRanges.end(); ++it) + { + if ( (it->first <= (U32)mCursorPos) && (it->second >= (U32)mCursorPos) ) + { + deselect(); + // Insert the suggestion in its place + LLWString suggestion = utf8str_to_wstring(mSuggestionList[index]); + insertStringNoUndo(it->first, utf8str_to_wstring(mSuggestionList[index])); + + // Delete the misspelled word + removeStringNoUndo(it->first + (S32)suggestion.length(), it->second - it->first); + + + setCursorPos(it->first + (S32)suggestion.length()); + + break; + } + } + mSpellCheckStart = mSpellCheckEnd = -1; +} + +void LLTextBase::addToDictionary() +{ + if (canAddToDictionary()) + { + LLSpellChecker::instance().addToCustomDictionary(getMisspelledWord(mCursorPos)); + } +} + +bool LLTextBase::canAddToDictionary() const +{ + return (getSpellCheck()) && (isMisspelledWord(mCursorPos)); +} + +void LLTextBase::addToIgnore() +{ + if (canAddToIgnore()) + { + LLSpellChecker::instance().addToIgnoreList(getMisspelledWord(mCursorPos)); + } +} + +bool LLTextBase::canAddToIgnore() const +{ + return (getSpellCheck()) && (isMisspelledWord(mCursorPos)); +} + +std::string LLTextBase::getMisspelledWord(U32 pos) const +{ + for (std::list<std::pair<U32, U32> >::const_iterator it = mMisspellRanges.begin(); it != mMisspellRanges.end(); ++it) + { + if ( (it->first <= pos) && (it->second >= pos) ) + { + return wstring_to_utf8str(getWText().substr(it->first, it->second - it->first)); + } + } + return LLStringUtil::null; +} + +bool LLTextBase::isMisspelledWord(U32 pos) const +{ + for (std::list<std::pair<U32, U32> >::const_iterator it = mMisspellRanges.begin(); it != mMisspellRanges.end(); ++it) + { + if ( (it->first <= pos) && (it->second >= pos) ) + { + return true; + } + } + return false; +} + +void LLTextBase::onSpellCheckSettingsChange() +{ + // Recheck the spelling on every change + mMisspellRanges.clear(); + mSpellCheckStart = mSpellCheckEnd = -1; +} + +void LLTextBase::onFocusReceived() +{ + LLUICtrl::onFocusReceived(); + if (!getLength() && !mLabel.empty()) + { + // delete label which is LLLabelTextSegment + clearSegments(); + } +} + +void LLTextBase::onFocusLost() +{ + LLUICtrl::onFocusLost(); + if (!getLength() && !mLabel.empty()) + { + resetLabel(); + } +} // Sets the scrollbar from the cursor position void LLTextBase::updateScrollFromCursor() @@ -1070,8 +1413,7 @@ void LLTextBase::updateScrollFromCursor() // scroll so that the cursor is at the top of the page LLRect scroller_doc_window = getVisibleDocumentRect(); - LLRect cursor_rect_doc = getLocalRectFromDocIndex(mCursorPos); - cursor_rect_doc.translate(scroller_doc_window.mLeft, scroller_doc_window.mBottom); + LLRect cursor_rect_doc = getDocRectFromDocIndex(mCursorPos); mScroller->scrollToShowRect(cursor_rect_doc, LLRect(0, scroller_doc_window.getHeight() - 5, scroller_doc_window.getWidth(), 5)); } @@ -1082,7 +1424,7 @@ S32 LLTextBase::getLeftOffset(S32 width) case LLFontGL::LEFT: return mHPad; case LLFontGL::HCENTER: - return mHPad + (mVisibleTextRect.getWidth() - width - mHPad) / 2; + return mHPad + llmax(0, (mVisibleTextRect.getWidth() - width - mHPad) / 2); case LLFontGL::RIGHT: return mVisibleTextRect.getWidth() - width; default: @@ -1091,40 +1433,64 @@ S32 LLTextBase::getLeftOffset(S32 width) } -static LLFastTimer::DeclareTimer FTM_TEXT_REFLOW ("Text Reflow"); +static LLTrace::BlockTimerStatHandle FTM_TEXT_REFLOW ("Text Reflow"); void LLTextBase::reflow() { - LLFastTimer ft(FTM_TEXT_REFLOW); + LL_RECORD_BLOCK_TIME(FTM_TEXT_REFLOW); updateSegments(); + if (mReflowIndex == S32_MAX) + { + return; + } + + bool scrolled_to_bottom = mScroller ? mScroller->isAtBottom() : false; + + LLRect cursor_rect = getLocalRectFromDocIndex(mCursorPos); + bool follow_selection = getLocalRect().overlaps(cursor_rect); // cursor is (potentially) visible + + // store in top-left relative coordinates to avoid issues with horizontal scrollbar appearing and disappearing + cursor_rect.mTop = mVisibleTextRect.mTop - cursor_rect.mTop; + cursor_rect.mBottom = mVisibleTextRect.mTop - cursor_rect.mBottom; + + S32 first_line = getFirstVisibleLine(); + + // if scroll anchor not on first line, update it to first character of first line + if ((first_line < mLineInfoList.size()) + && (mScrollIndex < mLineInfoList[first_line].mDocIndexStart + || mScrollIndex >= mLineInfoList[first_line].mDocIndexEnd)) + { + mScrollIndex = mLineInfoList[first_line].mDocIndexStart; + } + LLRect first_char_rect = getLocalRectFromDocIndex(mScrollIndex); + // store in top-left relative coordinates to avoid issues with horizontal scrollbar appearing and disappearing + first_char_rect.mTop = mVisibleTextRect.mTop - first_char_rect.mTop; + first_char_rect.mBottom = mVisibleTextRect.mTop - first_char_rect.mBottom; + + S32 reflow_count = 0; while(mReflowIndex < S32_MAX) { + // we can get into an infinite loop if the document height does not monotonically increase + // with decreasing width (embedded ui elements with alternate layouts). In that case, + // we want to stop reflowing after 2 iterations. We use 2, since we need to handle the case + // of introducing a vertical scrollbar causing a reflow with less width. We should also always + // use an even number of iterations to avoid user visible oscillation of the layout + if(++reflow_count > 2) + { + LL_DEBUGS() << "Breaking out of reflow due to possible infinite loop in " << getName() << LL_ENDL; + break; + } + S32 start_index = mReflowIndex; mReflowIndex = S32_MAX; // shrink document to minimum size (visible portion of text widget) // to force inlined widgets with follows set to shrink - mDocumentView->reshape(mVisibleTextRect.getWidth(), mDocumentView->getRect().getHeight()); - - bool scrolled_to_bottom = mScroller ? mScroller->isAtBottom() : false; - - LLRect old_cursor_rect = getLocalRectFromDocIndex(mCursorPos); - bool follow_selection = mVisibleTextRect.overlaps(old_cursor_rect); // cursor is visible - old_cursor_rect.translate(-mVisibleTextRect.mLeft, -mVisibleTextRect.mBottom); - - S32 first_line = getFirstVisibleLine(); - - // if scroll anchor not on first line, update it to first character of first line - if (!mLineInfoList.empty() - && (mScrollIndex < mLineInfoList[first_line].mDocIndexStart - || mScrollIndex >= mLineInfoList[first_line].mDocIndexEnd)) + if (mWordWrap) { - mScrollIndex = mLineInfoList[first_line].mDocIndexStart; + mDocumentView->reshape(mVisibleTextRect.getWidth(), mDocumentView->getRect().getHeight()); } - LLRect first_char_rect = getLocalRectFromDocIndex(mScrollIndex); - // subtract off effect of horizontal scrollbar from local position of first char - first_char_rect.translate(-mVisibleTextRect.mLeft, -mVisibleTextRect.mBottom); S32 cur_top = 0; @@ -1167,11 +1533,6 @@ void LLTextBase::reflow() // grow line height as necessary based on reported height of this segment line_height = llmax(line_height, segment_height); remaining_pixels -= segment_width; - if (remaining_pixels < 0) - { - // getNumChars() and getDimensions() should return consistent results - remaining_pixels = 0; - } seg_offset += character_count; @@ -1195,7 +1556,7 @@ void LLTextBase::reflow() line_count)); line_start_index = segment->getStart() + seg_offset; - cur_top -= llround((F32)line_height * mLineSpacingMult) + mLineSpacingPixels; + cur_top -= ll_round((F32)line_height * mLineSpacingMult) + mLineSpacingPixels; remaining_pixels = text_available_width; line_height = 0; } @@ -1207,7 +1568,7 @@ void LLTextBase::reflow() last_segment_char_on_line, line_rect, line_count)); - cur_top -= llround((F32)line_height * mLineSpacingMult) + mLineSpacingPixels; + cur_top -= ll_round((F32)line_height * mLineSpacingMult) + mLineSpacingPixels; break; } // ...or finished a segment and there are segments remaining on this line @@ -1222,7 +1583,7 @@ void LLTextBase::reflow() line_rect, line_count)); line_start_index = segment->getStart() + seg_offset; - cur_top -= llround((F32)line_height * mLineSpacingMult) + mLineSpacingPixels; + cur_top -= ll_round((F32)line_height * mLineSpacingMult) + mLineSpacingPixels; line_height = 0; remaining_pixels = text_available_width; } @@ -1246,32 +1607,42 @@ void LLTextBase::reflow() segmentp->updateLayout(*this); } + } - // apply scroll constraints after reflowing text - if (!hasMouseCapture() && mScroller) + // apply scroll constraints after reflowing text + if (!hasMouseCapture() && mScroller) + { + if (scrolled_to_bottom && mTrackEnd) { - if (scrolled_to_bottom && mTrackEnd) - { - // keep bottom of text buffer visible - endOfDoc(); - } - else if (hasSelection() && follow_selection) - { - // keep cursor in same vertical position on screen when selecting text - LLRect new_cursor_rect_doc = getDocRectFromDocIndex(mCursorPos); - mScroller->scrollToShowRect(new_cursor_rect_doc, old_cursor_rect); - } - else - { - // keep first line of text visible - LLRect new_first_char_rect = getDocRectFromDocIndex(mScrollIndex); - mScroller->scrollToShowRect(new_first_char_rect, first_char_rect); - } + // keep bottom of text buffer visible + endOfDoc(); + } + else if (hasSelection() && follow_selection) + { + // keep cursor in same vertical position on screen when selecting text + LLRect new_cursor_rect_doc = getDocRectFromDocIndex(mCursorPos); + LLRect old_cursor_rect = cursor_rect; + old_cursor_rect.mTop = mVisibleTextRect.mTop - cursor_rect.mTop; + old_cursor_rect.mBottom = mVisibleTextRect.mTop - cursor_rect.mBottom; + + mScroller->scrollToShowRect(new_cursor_rect_doc, old_cursor_rect); } + else + { + // keep first line of text visible + LLRect new_first_char_rect = getDocRectFromDocIndex(mScrollIndex); - // reset desired x cursor position - updateCursorXPos(); + // pass in desired rect in the coordinate frame of the document viewport + LLRect old_first_char_rect = first_char_rect; + old_first_char_rect.mTop = mVisibleTextRect.mTop - first_char_rect.mTop; + old_first_char_rect.mBottom = mVisibleTextRect.mTop - first_char_rect.mBottom; + + mScroller->scrollToShowRect(new_first_char_rect, old_first_char_rect); + } } + + // reset desired x cursor position + updateCursorXPos(); } LLRect LLTextBase::getTextBoundingRect() @@ -1291,9 +1662,9 @@ S32 LLTextBase::getLineStart( S32 line ) const { S32 num_lines = getLineCount(); if (num_lines == 0) - { + { return 0; - } + } line = llclamp(line, 0, num_lines-1); return mLineInfoList[line].mDocIndexStart; @@ -1303,9 +1674,9 @@ S32 LLTextBase::getLineEnd( S32 line ) const { S32 num_lines = getLineCount(); if (num_lines == 0) - { + { return 0; - } + } line = llclamp(line, 0, num_lines-1); return mLineInfoList[line].mDocIndexEnd; @@ -1364,7 +1735,7 @@ S32 LLTextBase::getFirstVisibleLine() const return iter - mLineInfoList.begin(); } -std::pair<S32, S32> LLTextBase::getVisibleLines(bool fully_visible) +std::pair<S32, S32> LLTextBase::getVisibleLines(bool require_fully_visible) { LLRect visible_region = getVisibleDocumentRect(); line_list_t::const_iterator first_iter; @@ -1373,14 +1744,14 @@ std::pair<S32, S32> LLTextBase::getVisibleLines(bool fully_visible) // make sure we have an up-to-date mLineInfoList reflow(); - if (fully_visible) + if (require_fully_visible) { first_iter = std::lower_bound(mLineInfoList.begin(), mLineInfoList.end(), visible_region.mTop, compare_top()); - last_iter = std::lower_bound(mLineInfoList.begin(), mLineInfoList.end(), visible_region.mBottom, compare_bottom()); + last_iter = std::upper_bound(mLineInfoList.begin(), mLineInfoList.end(), visible_region.mBottom, compare_bottom()); } else { - first_iter = std::lower_bound(mLineInfoList.begin(), mLineInfoList.end(), visible_region.mTop, compare_bottom()); + first_iter = std::upper_bound(mLineInfoList.begin(), mLineInfoList.end(), visible_region.mTop, compare_bottom()); last_iter = std::lower_bound(mLineInfoList.begin(), mLineInfoList.end(), visible_region.mBottom, compare_top()); } return std::pair<S32, S32>(first_iter - mLineInfoList.begin(), last_iter - mLineInfoList.begin()); @@ -1404,10 +1775,10 @@ void LLTextBase::removeDocumentChild(LLView* view) } -static LLFastTimer::DeclareTimer FTM_UPDATE_TEXT_SEGMENTS("Update Text Segments"); +static LLTrace::BlockTimerStatHandle FTM_UPDATE_TEXT_SEGMENTS("Update Text Segments"); void LLTextBase::updateSegments() { - LLFastTimer ft(FTM_UPDATE_TEXT_SEGMENTS); + LL_RECORD_BLOCK_TIME(FTM_UPDATE_TEXT_SEGMENTS); createDefaultSegment(); } @@ -1437,15 +1808,93 @@ void LLTextBase::getSegmentAndOffset( S32 startpos, segment_set_t::iterator* seg } } +LLTextBase::segment_set_t::iterator LLTextBase::getEditableSegIterContaining(S32 index) +{ + segment_set_t::iterator it = getSegIterContaining(index); + segment_set_t::iterator orig_it = it; + + if (it == mSegments.end()) return it; + + if (!(*it)->canEdit() + && index == (*it)->getStart() + && it != mSegments.begin()) + { + it--; + if ((*it)->canEdit()) + { + return it; + } + } + return orig_it; +} + +LLTextBase::segment_set_t::const_iterator LLTextBase::getEditableSegIterContaining(S32 index) const +{ + segment_set_t::const_iterator it = getSegIterContaining(index); + segment_set_t::const_iterator orig_it = it; + if (it == mSegments.end()) return it; + + if (!(*it)->canEdit() + && index == (*it)->getStart() + && it != mSegments.begin()) + { + it--; + if ((*it)->canEdit()) + { + return it; + } + } + return orig_it; +} + LLTextBase::segment_set_t::iterator LLTextBase::getSegIterContaining(S32 index) { - segment_set_t::iterator it = mSegments.upper_bound(new LLIndexSegment(index)); + + static LLPointer<LLIndexSegment> index_segment = new LLIndexSegment(); + + S32 text_len = 0; + if (!useLabel()) + { + text_len = getLength(); + } + else + { + text_len = mLabel.getWString().length(); + } + + if (index > text_len) { return mSegments.end(); } + + // when there are no segments, we return the end iterator, which must be checked by caller + if (mSegments.size() <= 1) { return mSegments.begin(); } + + index_segment->setStart(index); + index_segment->setEnd(index); + segment_set_t::iterator it = mSegments.upper_bound(index_segment); 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)); + static LLPointer<LLIndexSegment> index_segment = new LLIndexSegment(); + + S32 text_len = 0; + if (!useLabel()) + { + text_len = getLength(); + } + else + { + text_len = mLabel.getWString().length(); + } + + if (index > text_len) { return mSegments.end(); } + + // when there are no segments, we return the end iterator, which must be checked by caller + if (mSegments.size() <= 1) { return mSegments.begin(); } + + index_segment->setStart(index); + index_segment->setEnd(index); + LLTextBase::segment_set_t::const_iterator it = mSegments.upper_bound(index_segment); return it; } @@ -1488,15 +1937,34 @@ void LLTextBase::createUrlContextMenu(S32 x, S32 y, const std::string &in_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.Block", boost::bind(&LLUrlAction::blockObject, url)); registrar.add("Url.Teleport", boost::bind(&LLUrlAction::teleportToLocation, url)); + registrar.add("Url.ShowProfile", boost::bind(&LLUrlAction::showProfile, url)); + registrar.add("Url.AddFriend", boost::bind(&LLUrlAction::addFriend, url)); + registrar.add("Url.RemoveFriend", boost::bind(&LLUrlAction::removeFriend, url)); + registrar.add("Url.SendIM", boost::bind(&LLUrlAction::sendIM, url)); registrar.add("Url.ShowOnMap", boost::bind(&LLUrlAction::showLocationOnMap, 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 delete mPopupMenu; + llassert(LLMenuGL::sMenuContainer != NULL); mPopupMenu = LLUICtrlFactory::getInstance()->createFromFile<LLContextMenu>(xui_file, LLMenuGL::sMenuContainer, LLMenuHolderGL::child_registry_t::instance()); + if (mIsFriendSignal) + { + bool isFriend = *(*mIsFriendSignal)(LLUUID(LLUrlAction::getUserID(url))); + LLView* addFriendButton = mPopupMenu->getChild<LLView>("add_friend"); + LLView* removeFriendButton = mPopupMenu->getChild<LLView>("remove_friend"); + + if (addFriendButton && removeFriendButton) + { + addFriendButton->setEnabled(!isFriend); + removeFriendButton->setEnabled(isFriend); + } + } + if (mPopupMenu) { mPopupMenu->show(x, y); @@ -1521,7 +1989,10 @@ void LLTextBase::setText(const LLStringExplicit &utf8str, const LLStyle::Params& // appendText modifies mCursorPos... appendText(text, false, input_params); // ...so move cursor to top after appending text - startOfDoc(); + if (!mTrackEnd) + { + startOfDoc(); + } onValueChange(0, getLength()); } @@ -1532,28 +2003,44 @@ std::string LLTextBase::getText() const return getViewModel()->getValue().asString(); } -void LLTextBase::appendText(const std::string &new_text, bool prepend_newline, const LLStyle::Params& input_params) +// IDEVO - icons can be UI image names or UUID sent from +// server with avatar display name +static LLUIImagePtr image_from_icon_name(const std::string& icon_name) +{ + if (LLUUID::validate(icon_name)) + { + return LLUI::getUIImageByID( LLUUID(icon_name) ); + } + else + { + return LLUI::getUIImage(icon_name); + } +} + +static LLTrace::BlockTimerStatHandle FTM_PARSE_HTML("Parse HTML"); + + + +void LLTextBase::appendTextImpl(const std::string &new_text, const LLStyle::Params& input_params) { LLStyle::Params style_params(input_params); - style_params.fillFrom(getDefaultStyleParams()); + style_params.fillFrom(getStyleParams()); S32 part = (S32)LLTextParser::WHOLE; - if(mParseHTML) + if (mParseHTML && !style_params.is_link) // Don't search for URLs inside a link segment (STORM-358). { + LL_RECORD_BLOCK_TIME(FTM_PARSE_HTML); S32 start=0,end=0; LLUrlMatch match; std::string text = new_text; while ( LLUrlRegistry::instance().findUrl(text, match, - boost::bind(&LLTextBase::replaceUrlLabel, this, _1, _2)) ) + boost::bind(&LLTextBase::replaceUrl, this, _1, _2, _3),isContentTrusted())) { start = match.getStart(); end = match.getEnd()+1; - LLStyle::Params link_params = style_params; - link_params.color = match.getColor(); - link_params.readonly_color = match.getColor(); - link_params.font.style("UNDERLINE"); - link_params.link_href = match.getUrl(); + LLStyle::Params link_params(style_params); + link_params.overwriteFrom(match.getStyle()); // output the text before the Url if (start > 0) @@ -1568,47 +2055,34 @@ void LLTextBase::appendText(const std::string &new_text, bool prepend_newline, c part = (S32)LLTextParser::MIDDLE; } std::string subtext=text.substr(0,start); - appendAndHighlightText(subtext, prepend_newline, part, style_params); - prepend_newline = false; + appendAndHighlightText(subtext, part, style_params); } - // output an optional icon before the Url - if (! match.getIcon().empty()) - { - LLUIImagePtr image = LLUI::getUIImage(match.getIcon()); - if (image) - { - LLStyle::Params icon; - icon.image = image; - // Text will be replaced during rendering with the icon, - // but string cannot be empty or the segment won't be - // added (or drawn). - appendAndHighlightText(" ", prepend_newline, part, icon); - prepend_newline = false; - } - } + // add icon before url if need + LLTextUtil::processUrlMatch(&match, this, isContentTrusted() || match.isTrusted()); + + // output the styled Url + appendAndHighlightTextImpl(match.getLabel(), part, link_params, match.underlineOnHoverOnly()); - // output the styled Url (unless we've been asked to suppress hyperlinking) - if (match.isLinkDisabled()) + // show query part of url with gray color only for LLUrlEntryHTTP and LLUrlEntryHTTPNoProtocol url entries + std::string label = match.getQuery(); + if (label.size()) { - appendAndHighlightText(match.getLabel(), prepend_newline, part, style_params); + link_params.color = LLColor4::grey; + link_params.readonly_color = LLColor4::grey; + appendAndHighlightTextImpl(label, part, link_params, match.underlineOnHoverOnly()); } - else + + // set the tooltip for the Url label + if (! match.getTooltip().empty()) { - appendAndHighlightText(match.getLabel(), prepend_newline, part, link_params); - - // set the tooltip for the Url label - if (! match.getTooltip().empty()) + segment_set_t::iterator it = getSegIterContaining(getLength()-1); + if (it != mSegments.end()) { - segment_set_t::iterator it = getSegIterContaining(getLength()-1); - if (it != mSegments.end()) - { - LLTextSegmentPtr segment = *it; - segment->setToolTip(match.getTooltip()); - } + LLTextSegmentPtr segment = *it; + segment->setToolTip(match.getTooltip()); } } - prepend_newline = false; // move on to the rest of the text after the Url if (end < (S32)text.length()) @@ -1622,25 +2096,107 @@ void LLTextBase::appendText(const std::string &new_text, bool prepend_newline, c break; } } - if (part != (S32)LLTextParser::WHOLE) part=(S32)LLTextParser::END; - if (end < (S32)text.length()) appendAndHighlightText(text, prepend_newline, part, style_params); + if (part != (S32)LLTextParser::WHOLE) + part=(S32)LLTextParser::END; + if (end < (S32)text.length()) + appendAndHighlightText(text, part, style_params); } else { - appendAndHighlightText(new_text, prepend_newline, part, style_params); + appendAndHighlightText(new_text, part, style_params); + } +} + +static LLTrace::BlockTimerStatHandle FTM_APPEND_TEXT("Append Text"); + +void LLTextBase::appendText(const std::string &new_text, bool prepend_newline, const LLStyle::Params& input_params) +{ + LL_RECORD_BLOCK_TIME(FTM_APPEND_TEXT); + if (new_text.empty()) + return; + + if(prepend_newline) + appendLineBreakSegment(input_params); + appendTextImpl(new_text,input_params); +} + +void LLTextBase::setLabel(const LLStringExplicit& label) +{ + mLabel = label; + resetLabel(); +} + +BOOL LLTextBase::setLabelArg(const std::string& key, const LLStringExplicit& text ) +{ + mLabel.setArg(key, text); + return TRUE; +} + +void LLTextBase::resetLabel() +{ + if (useLabel()) + { + clearSegments(); + + LLStyle* style = new LLStyle(getStyleParams()); + style->setColor(mTentativeFgColor); + LLStyleConstSP sp(style); + + LLTextSegmentPtr label = new LLLabelTextSegment(sp, 0, mLabel.getWString().length() + 1, *this); + insertSegment(label); } } +bool LLTextBase::useLabel() const +{ + return !getLength() && !mLabel.empty() && !hasFocus(); +} + +void LLTextBase::setFont(const LLFontGL* font) +{ + mFont = font; + mStyleDirty = true; +} + void LLTextBase::needsReflow(S32 index) { - lldebugs << "reflow on object " << (void*)this << " index = " << mReflowIndex << ", new index = " << index << llendl; + LL_DEBUGS() << "reflow on object " << (void*)this << " index = " << mReflowIndex << ", new index = " << index << LL_ENDL; mReflowIndex = llmin(mReflowIndex, index); } -void LLTextBase::appendAndHighlightText(const std::string &new_text, bool prepend_newline, S32 highlight_part, const LLStyle::Params& style_params) +void LLTextBase::appendLineBreakSegment(const LLStyle::Params& style_params) { - if (new_text.empty()) return; + segment_vec_t segments; + LLStyleConstSP sp(new LLStyle(style_params)); + segments.push_back(new LLLineBreakTextSegment(sp, getLength())); + insertStringNoUndo(getLength(), utf8str_to_wstring("\n"), &segments); +} + +void LLTextBase::appendImageSegment(const LLStyle::Params& style_params) +{ + if(getPlainText()) + { + return; + } + segment_vec_t segments; + LLStyleConstSP sp(new LLStyle(style_params)); + segments.push_back(new LLImageTextSegment(sp, getLength(),*this)); + + insertStringNoUndo(getLength(), utf8str_to_wstring(" "), &segments); +} + +void LLTextBase::appendWidget(const LLInlineViewSegment::Params& params, const std::string& text, bool allow_undo) +{ + segment_vec_t segments; + LLWString widget_wide_text = utf8str_to_wstring(text); + segments.push_back(new LLInlineViewSegment(params, getLength(), getLength() + widget_wide_text.size())); + + insertStringNoUndo(getLength(), widget_wide_text, &segments); +} + +void LLTextBase::appendAndHighlightTextImpl(const std::string &new_text, S32 highlight_part, const LLStyle::Params& style_params, bool underline_on_hover_only) +{ // Save old state S32 selection_start = mSelectionStart; S32 selection_end = mSelectionEnd; @@ -1653,13 +2209,11 @@ void LLTextBase::appendAndHighlightText(const std::string &new_text, bool prepen setCursorPos(old_length); - LLTextParser* highlight = LLTextParser::getInstance(); - - if (mParseHighlights && highlight) + if (mParseHighlights) { LLStyle::Params highlight_params(style_params); - LLSD pieces = highlight->parsePartialLineHighlights(new_text, highlight_params.color(), (LLTextParser::EHighlightPosition)highlight_part); + LLSD pieces = LLTextParser::instance().parsePartialLineHighlights(new_text, highlight_params.color(), (LLTextParser::EHighlightPosition)highlight_part); for (S32 i = 0; i < pieces.size(); i++) { LLSD color_llsd = pieces[i]["color"]; @@ -1668,17 +2222,21 @@ void LLTextBase::appendAndHighlightText(const std::string &new_text, bool prepen highlight_params.color = lcolor; LLWString wide_text; - if (prepend_newline && (i == 0 || pieces.size() <= 1 )) + wide_text = utf8str_to_wstring(pieces[i]["text"].asString()); + + S32 cur_length = getLength(); + LLStyleConstSP sp(new LLStyle(highlight_params)); + LLTextSegmentPtr segmentp; + if(underline_on_hover_only) { - wide_text = utf8str_to_wstring(std::string("\n") + pieces[i]["text"].asString()); + highlight_params.font.style("NORMAL"); + LLStyleConstSP normal_sp(new LLStyle(highlight_params)); + segmentp = new LLOnHoverChangeableTextSegment(sp, normal_sp, cur_length, cur_length + wide_text.size(), *this); } else { - wide_text = utf8str_to_wstring(pieces[i]["text"].asString()); + segmentp = new LLNormalTextSegment(sp, cur_length, cur_length + wide_text.size(), *this); } - S32 cur_length = getLength(); - LLStyleConstSP sp(new LLStyle(highlight_params)); - LLTextSegmentPtr segmentp = new LLNormalTextSegment(sp, cur_length, cur_length + wide_text.size(), *this); segment_vec_t segments; segments.push_back(segmentp); insertStringNoUndo(cur_length, wide_text, &segments); @@ -1687,23 +2245,23 @@ void LLTextBase::appendAndHighlightText(const std::string &new_text, bool prepen else { LLWString wide_text; - - // Add carriage return if not first line - if (getLength() != 0 - && prepend_newline) - { - wide_text = utf8str_to_wstring(std::string("\n") + new_text); - } - else - { - wide_text = utf8str_to_wstring(new_text); - } + wide_text = utf8str_to_wstring(new_text); segment_vec_t segments; S32 segment_start = old_length; S32 segment_end = old_length + wide_text.size(); LLStyleConstSP sp(new LLStyle(style_params)); + if (underline_on_hover_only) + { + LLStyle::Params normal_style_params(style_params); + normal_style_params.font.style("NORMAL"); + LLStyleConstSP normal_sp(new LLStyle(normal_style_params)); + segments.push_back(new LLOnHoverChangeableTextSegment(sp, normal_sp, segment_start, segment_end, *this )); + } + else + { segments.push_back(new LLNormalTextSegment(sp, segment_start, segment_end, *this )); + } insertStringNoUndo(getLength(), wide_text, &segments); } @@ -1725,16 +2283,35 @@ void LLTextBase::appendAndHighlightText(const std::string &new_text, bool prepen { setCursorPos(cursor_pos); } +} + +void LLTextBase::appendAndHighlightText(const std::string &new_text, S32 highlight_part, const LLStyle::Params& style_params, bool underline_on_hover_only) +{ + if (new_text.empty()) return; + + std::string::size_type start = 0; + std::string::size_type pos = new_text.find("\n",start); + + while(pos!=-1) + { + if(pos!=start) + { + std::string str = std::string(new_text,start,pos-start); + appendAndHighlightTextImpl(str,highlight_part, style_params, underline_on_hover_only); + } + appendLineBreakSegment(style_params); + start = pos+1; + pos = new_text.find("\n",start); + } - //if( !allow_undo ) - //{ - // blockUndo(); - //} + std::string str = std::string(new_text,start,new_text.length()-start); + appendAndHighlightTextImpl(str,highlight_part, style_params, underline_on_hover_only); } -void LLTextBase::replaceUrlLabel(const std::string &url, - const std::string &label) +void LLTextBase::replaceUrl(const std::string &url, + const std::string &label, + const std::string &icon) { // get the full (wide) text for the editor so we can change it LLWString text = getWText(); @@ -1755,7 +2332,7 @@ void LLTextBase::replaceUrlLabel(const std::string &url, seg->setEnd(seg_start + seg_length); // if we find a link with our Url, then replace the label - if (style->isLink() && style->getLinkHREF() == url) + if (style->getLinkHREF() == url) { S32 start = seg->getStart(); S32 end = seg->getEnd(); @@ -1764,6 +2341,21 @@ void LLTextBase::replaceUrlLabel(const std::string &url, modified = true; } + // Icon might be updated when more avatar or group info + // becomes available + if (style->isImage() && style->getLinkHREF() == url) + { + LLUIImagePtr image = image_from_icon_name( icon ); + if (image) + { + LLStyle::Params icon_params; + icon_params.image = image; + LLStyleConstSP new_style(new LLStyle(icon_params)); + seg->setStyle(new_style); + modified = true; + } + } + // work out the character offset for the next segment seg_start = seg->getEnd(); } @@ -1786,7 +2378,7 @@ void LLTextBase::setWText(const LLWString& text) const LLWString& LLTextBase::getWText() const { - return getViewModel()->getDisplay(); + return getViewModel()->getDisplay(); } // If round is true, if the position is on the right half of a character, the cursor @@ -1796,18 +2388,19 @@ const LLWString& LLTextBase::getWText() const S32 LLTextBase::getDocIndexFromLocalCoord( S32 local_x, S32 local_y, BOOL round, bool hit_past_end_of_line) const { // Figure out which line we're nearest to. - LLRect visible_region = getVisibleDocumentRect(); - + LLRect doc_rect = mDocumentView->getRect(); + S32 doc_y = local_y - doc_rect.mBottom; + // binary search for line that starts before local_y - line_list_t::const_iterator line_iter = std::lower_bound(mLineInfoList.begin(), mLineInfoList.end(), local_y - mVisibleTextRect.mBottom + visible_region.mBottom, compare_bottom()); + line_list_t::const_iterator line_iter = std::lower_bound(mLineInfoList.begin(), mLineInfoList.end(), doc_y, compare_bottom()); - if (line_iter == mLineInfoList.end()) + if (!mLineInfoList.size() || line_iter == mLineInfoList.end()) { return getLength(); // past the end } S32 pos = getLength(); - S32 start_x = mVisibleTextRect.mLeft + line_iter->mRect.mLeft; + S32 start_x = line_iter->mRect.mLeft + doc_rect.mLeft; segment_set_t::iterator line_seg_iter; S32 line_seg_offset; @@ -1822,14 +2415,19 @@ S32 LLTextBase::getDocIndexFromLocalCoord( S32 local_x, S32 local_y, BOOL round, S32 text_width, text_height; bool newline = segmentp->getDimensions(line_seg_offset, segment_line_length, text_width, text_height); + if(newline) + { + pos = segment_line_start + segmentp->getOffset(local_x - start_x, line_seg_offset, segment_line_length, round); + break; + } + // if we've reached a line of text *below* the mouse cursor, doc index is first character on that line - if (hit_past_end_of_line && local_y - mVisibleTextRect.mBottom + visible_region.mBottom > line_iter->mRect.mTop) + if (hit_past_end_of_line && doc_y > line_iter->mRect.mTop) { pos = segment_line_start; break; } - if (local_x < start_x + text_width // cursor to left of right edge of text - || newline) // or this line ends with a newline, set doc pos to newline char + if (local_x < start_x + text_width) // cursor to left of right edge of text { // Figure out which character we're nearest to. S32 offset; @@ -1853,13 +2451,21 @@ S32 LLTextBase::getDocIndexFromLocalCoord( S32 local_x, S32 local_y, BOOL round, pos = segment_line_start + offset; break; } - else if (hit_past_end_of_line && segmentp->getEnd() >= line_iter->mDocIndexEnd - 1) + else if (hit_past_end_of_line && segmentp->getEnd() >= line_iter->mDocIndexEnd) { - // segment wraps to next line, so just set doc pos to start of next line (represented by mDocIndexEnd) - pos = llmin(getLength(), line_iter->mDocIndexEnd); + if (getLineNumFromDocIndex(line_iter->mDocIndexEnd - 1) == line_iter->mLineNum) + { + // if segment wraps to the next line we should step one char back + // to compensate for the space char between words + // which is removed due to wrapping + pos = llclamp(line_iter->mDocIndexEnd - 1, 0, getLength()); + } + else + { + pos = llclamp(line_iter->mDocIndexEnd, 0, getLength()); + } break; } - start_x += text_width; } @@ -1880,7 +2486,6 @@ LLRect LLTextBase::getDocRectFromDocIndex(S32 pos) const // clamp pos to valid values pos = llclamp(pos, 0, mLineInfoList.back().mDocIndexEnd - 1); - // find line that contains cursor line_list_t::const_iterator line_iter = std::upper_bound(mLineInfoList.begin(), mLineInfoList.end(), pos, line_end_compare()); doc_rect.mLeft = line_iter->mRect.mLeft; @@ -1928,12 +2533,19 @@ LLRect LLTextBase::getDocRectFromDocIndex(S32 pos) const LLRect LLTextBase::getLocalRectFromDocIndex(S32 pos) const { + LLRect content_window_rect = mScroller ? mScroller->getContentWindowRect() : getLocalRect(); + if (mBorderVisible) + { + content_window_rect.stretch(-1); + } + LLRect local_rect; + if (mLineInfoList.empty()) { // return default height rect in upper left - local_rect = mVisibleTextRect; - local_rect.mBottom = local_rect.mTop - (S32)(mDefaultFont->getLineHeight()); + local_rect = content_window_rect; + local_rect.mBottom = local_rect.mTop - mFont->getLineHeight(); return local_rect; } @@ -1943,8 +2555,8 @@ LLRect LLTextBase::getLocalRectFromDocIndex(S32 pos) const // compensate for scrolled, inset view of doc LLRect scrolled_view_rect = getVisibleDocumentRect(); local_rect = doc_rect; - local_rect.translate(mVisibleTextRect.mLeft - scrolled_view_rect.mLeft, - mVisibleTextRect.mBottom - scrolled_view_rect.mBottom); + local_rect.translate(content_window_rect.mLeft - scrolled_view_rect.mLeft, + content_window_rect.mBottom - scrolled_view_rect.mBottom); return local_rect; } @@ -2039,21 +2651,24 @@ void LLTextBase::setCursorAtLocalPos( S32 local_x, S32 local_y, bool round, bool void LLTextBase::changeLine( S32 delta ) { S32 line = getLineNumFromDocIndex(mCursorPos); + S32 max_line_nb = getLineCount() - 1; + max_line_nb = (max_line_nb < 0 ? 0 : max_line_nb); + + S32 new_line = llclamp(line + delta, 0, max_line_nb); - S32 new_line = line; - if( (delta < 0) && (line > 0 ) ) - { - new_line = line - 1; - } - else if( (delta > 0) && (line < (getLineCount() - 1)) ) - { - new_line = line + 1; - } - - LLRect visible_region = getVisibleDocumentRect(); - - S32 new_cursor_pos = getDocIndexFromLocalCoord(mDesiredXPixel, mLineInfoList[new_line].mRect.mBottom + mVisibleTextRect.mBottom - visible_region.mBottom, TRUE); - setCursorPos(new_cursor_pos, true); + if (new_line != line) + { + LLRect visible_region = getVisibleDocumentRect(); + S32 new_cursor_pos = getDocIndexFromLocalCoord(mDesiredXPixel, + mLineInfoList[new_line].mRect.mBottom + mVisibleTextRect.mBottom - visible_region.mBottom, TRUE); + S32 actual_line = getLineNumFromDocIndex(new_cursor_pos); + if (actual_line != new_line) + { + // line edge, correcting position by 1 to move onto proper line + new_cursor_pos += new_line - actual_line; + } + setCursorPos(new_cursor_pos, true); + } } bool LLTextBase::scrolledToStart() @@ -2066,19 +2681,39 @@ bool LLTextBase::scrolledToEnd() return mScroller->isAtBottom(); } - bool LLTextBase::setCursor(S32 row, S32 column) { - if (0 <= row && row < (S32)mLineInfoList.size()) + if (row < 0 || column < 0) return false; + + S32 n_lines = mLineInfoList.size(); + for (S32 line = row; line < n_lines; ++line) { - S32 doc_pos = mLineInfoList[row].mDocIndexStart; - column = llclamp(column, 0, mLineInfoList[row].mDocIndexEnd - mLineInfoList[row].mDocIndexStart - 1); - doc_pos += column; - updateCursorXPos(); + const line_info& li = mLineInfoList[line]; + + if (li.mLineNum < row) + { + continue; + } + else if (li.mLineNum > row) + { + break; // invalid column specified + } + // Found the given row. + S32 line_length = li.mDocIndexEnd - li.mDocIndexStart;; + if (column >= line_length) + { + column -= line_length; + continue; + } + + // Found the given column. + updateCursorXPos(); + S32 doc_pos = li.mDocIndexStart + column; return setCursorPos(doc_pos); } - return false; + + return false; // invalid row or column specified } @@ -2136,6 +2771,9 @@ S32 LLTextBase::getEditableIndex(S32 index, bool increasing_direction) void LLTextBase::updateRects() { + LLRect old_text_rect = mVisibleTextRect; + mVisibleTextRect = mScroller ? mScroller->getContentWindowRect() : getLocalRect(); + if (mLineInfoList.empty()) { mTextBoundingRect = LLRect(0, mVPad, mHPad, 0); @@ -2151,10 +2789,24 @@ void LLTextBase::updateRects() } mTextBoundingRect.mTop += mVPad; - // subtract a pixel off the bottom to deal with rounding errors in measuring font height - mTextBoundingRect.mBottom -= 1; - S32 delta_pos = -mTextBoundingRect.mBottom; + S32 delta_pos = 0; + + switch(mVAlign) + { + case LLFontGL::TOP: + delta_pos = llmax(mVisibleTextRect.getHeight() - mTextBoundingRect.mTop, -mTextBoundingRect.mBottom); + break; + case LLFontGL::VCENTER: + delta_pos = (llmax(mVisibleTextRect.getHeight() - mTextBoundingRect.mTop, -mTextBoundingRect.mBottom) + (mVisibleTextRect.mBottom - mTextBoundingRect.mBottom)) / 2; + break; + case LLFontGL::BOTTOM: + delta_pos = mVisibleTextRect.mBottom - mTextBoundingRect.mBottom; + break; + case LLFontGL::BASELINE: + // do nothing + break; + } // move line segments to fit new document rect for (line_list_t::iterator it = mLineInfoList.begin(); it != mLineInfoList.end(); ++it) { @@ -2164,23 +2816,41 @@ void LLTextBase::updateRects() } // update document container dimensions according to text contents - LLRect doc_rect = mTextBoundingRect; + LLRect doc_rect; // use old mVisibleTextRect constraint document to width of viewable region + doc_rect.mBottom = llmin(mVisibleTextRect.mBottom, mTextBoundingRect.mBottom); doc_rect.mLeft = 0; // allow horizontal scrolling? // if so, use entire width of text contents // otherwise, stop at width of mVisibleTextRect + //FIXME: consider use of getWordWrap() instead doc_rect.mRight = mScroller ? llmax(mVisibleTextRect.getWidth(), mTextBoundingRect.mRight) : mVisibleTextRect.getWidth(); + doc_rect.mTop = llmax(mVisibleTextRect.mTop, mTextBoundingRect.mTop); + + if (!mScroller) + { + // push doc rect to top of text widget + switch(mVAlign) + { + case LLFontGL::TOP: + doc_rect.translate(0, mVisibleTextRect.getHeight() - doc_rect.mTop); + break; + case LLFontGL::VCENTER: + doc_rect.translate(0, (mVisibleTextRect.getHeight() - doc_rect.mTop) / 2); + case LLFontGL::BOTTOM: + default: + break; + } + } mDocumentView->setShape(doc_rect); //update mVisibleTextRect *after* mDocumentView has been resized // so that scrollbars are added if document needs to scroll // since mVisibleTextRect does not include scrollbars - LLRect old_text_rect = mVisibleTextRect; mVisibleTextRect = mScroller ? mScroller->getContentWindowRect() : getLocalRect(); //FIXME: replace border with image? if (mBorderVisible) @@ -2192,10 +2862,59 @@ void LLTextBase::updateRects() needsReflow(); } + // update mTextBoundingRect after mVisibleTextRect took scrolls into account + if (!mLineInfoList.empty() && mScroller) + { + S32 delta_pos = 0; + + switch(mVAlign) + { + case LLFontGL::TOP: + delta_pos = llmax(mVisibleTextRect.getHeight() - mTextBoundingRect.mTop, -mTextBoundingRect.mBottom); + break; + case LLFontGL::VCENTER: + delta_pos = (llmax(mVisibleTextRect.getHeight() - mTextBoundingRect.mTop, -mTextBoundingRect.mBottom) + (mVisibleTextRect.mBottom - mTextBoundingRect.mBottom)) / 2; + break; + case LLFontGL::BOTTOM: + delta_pos = mVisibleTextRect.mBottom - mTextBoundingRect.mBottom; + break; + case LLFontGL::BASELINE: + // do nothing + break; + } + // move line segments to fit new visible rect + if (delta_pos != 0) + { + for (line_list_t::iterator it = mLineInfoList.begin(); it != mLineInfoList.end(); ++it) + { + it->mRect.translate(0, delta_pos); + } + mTextBoundingRect.translate(0, delta_pos); + } + } + // update document container again, using new mVisibleTextRect (that has scrollbars enabled as needed) + doc_rect.mBottom = llmin(mVisibleTextRect.mBottom, mTextBoundingRect.mBottom); + doc_rect.mLeft = 0; doc_rect.mRight = mScroller ? llmax(mVisibleTextRect.getWidth(), mTextBoundingRect.mRight) : mVisibleTextRect.getWidth(); + doc_rect.mTop = llmax(mVisibleTextRect.getHeight(), mTextBoundingRect.getHeight()) + doc_rect.mBottom; + if (!mScroller) + { + // push doc rect to top of text widget + switch(mVAlign) + { + case LLFontGL::TOP: + doc_rect.translate(0, mVisibleTextRect.getHeight() - doc_rect.mTop); + break; + case LLFontGL::VCENTER: + doc_rect.translate(0, (mVisibleTextRect.getHeight() - doc_rect.mTop) / 2); + case LLFontGL::BOTTOM: + default: + break; + } + } mDocumentView->setShape(doc_rect); } @@ -2226,10 +2945,37 @@ LLRect LLTextBase::getVisibleDocumentRect() const { return mScroller->getVisibleContentRect(); } - else + else if (mClip) { - // entire document rect is visible when not scrolling + LLRect visible_text_rect = getVisibleTextRect(); + LLRect doc_rect = mDocumentView->getRect(); + visible_text_rect.translate(-doc_rect.mLeft, -doc_rect.mBottom); + + // reject partially visible lines + LLRect visible_lines_rect; + for (line_list_t::const_iterator it = mLineInfoList.begin(), end_it = mLineInfoList.end(); + it != end_it; + ++it) + { + bool line_visible = mClipPartial ? visible_text_rect.contains(it->mRect) : visible_text_rect.overlaps(it->mRect); + if (line_visible) + { + if (visible_lines_rect.isEmpty()) + { + visible_lines_rect = it->mRect; + } + else + { + visible_lines_rect.unionWith(it->mRect); + } + } + } + return visible_lines_rect; + } + else + { // entire document rect is visible // but offset according to height of widget + LLRect doc_rect = mDocumentView->getLocalRect(); doc_rect.mLeft -= mDocumentView->getRect().mLeft; // adjust for height of text above widget baseline @@ -2238,6 +2984,24 @@ LLRect LLTextBase::getVisibleDocumentRect() const } } +boost::signals2::connection LLTextBase::setURLClickedCallback(const commit_signal_t::slot_type& cb) +{ + if (!mURLClickSignal) + { + mURLClickSignal = new commit_signal_t(); + } + return mURLClickSignal->connect(cb); +} + +boost::signals2::connection LLTextBase::setIsFriendCallback(const is_friend_signal_t::slot_type& cb) +{ + if (!mIsFriendSignal) + { + mIsFriendSignal = new is_friend_signal_t(); + } + return mIsFriendSignal->connect(cb); +} + // // LLTextSegment // @@ -2271,7 +3035,10 @@ BOOL LLTextSegment::handleDoubleClick(S32 x, S32 y, MASK mask) { return FALSE; } BOOL LLTextSegment::handleHover(S32 x, S32 y, MASK mask) { return FALSE; } BOOL LLTextSegment::handleScrollWheel(S32 x, S32 y, S32 clicks) { return FALSE; } BOOL LLTextSegment::handleToolTip(S32 x, S32 y, MASK mask) { return FALSE; } -std::string LLTextSegment::getName() const { return ""; } +const std::string& LLTextSegment::getName() const +{ + return LLStringUtil::null; +} void LLTextSegment::onMouseCaptureLost() {} void LLTextSegment::screenPointToLocal(S32 screen_x, S32 screen_y, S32* local_x, S32* local_y) const {} void LLTextSegment::localPointToScreen(S32 local_x, S32 local_y, S32* screen_x, S32* screen_y) const {} @@ -2287,7 +3054,7 @@ LLNormalTextSegment::LLNormalTextSegment( LLStyleConstSP style, S32 start, S32 e mToken(NULL), mEditor(editor) { - mFontHeight = llceil(mStyle->getFont()->getLineHeight()); + mFontHeight = mStyle->getFont()->getLineHeight(); LLUIImagePtr image = mStyle->getImage(); if (image.notNull()) @@ -2303,7 +3070,7 @@ LLNormalTextSegment::LLNormalTextSegment( const LLColor4& color, S32 start, S32 { mStyle = new LLStyle(LLStyle::Params().visible(is_visible).color(color)); - mFontHeight = llceil(mStyle->getFont()->getLineHeight()); + mFontHeight = mStyle->getFont()->getLineHeight(); } LLNormalTextSegment::~LLNormalTextSegment() @@ -2316,25 +3083,6 @@ F32 LLNormalTextSegment::draw(S32 start, S32 end, S32 selection_start, S32 selec { if( end - start > 0 ) { - if ( mStyle->isImage() && (start >= 0) && (end <= mEnd - mStart)) - { - // ...for images, only render the image, not the underlying text, - // which is only a placeholder space - LLColor4 color = LLColor4::white % mEditor.getDrawContext().mAlpha; - LLUIImagePtr image = mStyle->getImage(); - S32 style_image_height = image->getHeight(); - S32 style_image_width = image->getWidth(); - // Text is drawn from the top of the draw_rect downward - S32 text_center = draw_rect.mTop - (mFontHeight / 2); - // Align image to center of text - S32 image_bottom = text_center - (style_image_height / 2); - image->draw(draw_rect.mLeft, image_bottom, - style_image_width, style_image_height, color); - - const S32 IMAGE_HPAD = 3; - return draw_rect.mLeft + style_image_width + IMAGE_HPAD; - } - return drawClippedSegment( getStart() + start, getStart() + end, selection_start, selection_end, draw_rect); } return draw_rect.mLeft; @@ -2345,12 +3093,7 @@ F32 LLNormalTextSegment::drawClippedSegment(S32 seg_start, S32 seg_end, S32 sele { F32 alpha = LLViewDrawContext::getCurrentContext().mAlpha; - const LLWString &text = mEditor.getWText(); - - if ( text[seg_end-1] == '\n' ) - { - --seg_end; - } + const LLWString &text = getWText(); F32 right_x = rect.mLeft; if (!mStyle->isVisible()) @@ -2362,21 +3105,21 @@ F32 LLNormalTextSegment::drawClippedSegment(S32 seg_start, S32 seg_end, S32 sele LLColor4 color = (mEditor.getReadOnly() ? mStyle->getReadOnlyColor() : mStyle->getColor()) % alpha; - if( selection_start > seg_start ) + 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, - rect.mLeft, rect.mTop, - color, - LLFontGL::LEFT, LLFontGL::TOP, - LLFontGL::NORMAL, - mStyle->getShadowType(), - length, rect.getWidth(), - &right_x, - mEditor.getUseEllipses()); + rect, + color, + LLFontGL::LEFT, mEditor.mVAlign, + LLFontGL::NORMAL, + mStyle->getShadowType(), + length, + &right_x, + mEditor.getUseEllipses()); } rect.mLeft = (S32)ceil(right_x); @@ -2388,14 +3131,14 @@ F32 LLNormalTextSegment::drawClippedSegment(S32 seg_start, S32 seg_end, S32 sele S32 length = end - start; font->render(text, start, - rect.mLeft, rect.mTop, - LLColor4( 1.f - color.mV[0], 1.f - color.mV[1], 1.f - color.mV[2], 1.f ), - LLFontGL::LEFT, LLFontGL::TOP, - LLFontGL::NORMAL, - LLFontGL::NO_SHADOW, - length, rect.getWidth(), - &right_x, - mEditor.getUseEllipses()); + rect, + mStyle->getSelectedColor().get(), + LLFontGL::LEFT, mEditor.mVAlign, + LLFontGL::NORMAL, + LLFontGL::NO_SHADOW, + length, + &right_x, + mEditor.getUseEllipses()); } rect.mLeft = (S32)ceil(right_x); if( selection_end < seg_end ) @@ -2405,14 +3148,14 @@ F32 LLNormalTextSegment::drawClippedSegment(S32 seg_start, S32 seg_end, S32 sele S32 end = seg_end; S32 length = end - start; font->render(text, start, - rect.mLeft, rect.mTop, - color, - LLFontGL::LEFT, LLFontGL::TOP, - LLFontGL::NORMAL, - mStyle->getShadowType(), - length, rect.getWidth(), - &right_x, - mEditor.getUseEllipses()); + rect, + color, + LLFontGL::LEFT, mEditor.mVAlign, + LLFontGL::NORMAL, + mStyle->getShadowType(), + length, + &right_x, + mEditor.getUseEllipses()); } return right_x; } @@ -2467,7 +3210,7 @@ BOOL LLNormalTextSegment::handleMouseUp(S32 x, S32 y, MASK mask) // Only process the click if it's actually in this segment, not to the right of the end-of-line. if(mEditor.getSegmentAtLocalPos(x, y, false) == this) { - LLUrlAction::clickAction(getStyle()->getLinkHREF()); + LLUrlAction::clickAction(getStyle()->getLinkHREF(), mEditor.isContentTrusted()); return TRUE; } } @@ -2500,7 +3243,7 @@ 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; + LL_WARNS() << "LLTextSegment::setToolTip: cannot replace keyword tooltip." << LL_ENDL; return; } mTooltip = tooltip; @@ -2510,38 +3253,19 @@ bool LLNormalTextSegment::getDimensions(S32 first_char, S32 num_chars, S32& widt { height = 0; width = 0; - bool force_newline = false; if (num_chars > 0) { height = mFontHeight; - const LLWString &text = mEditor.getWText(); + const LLWString &text = getWText(); // if last character is a newline, then return true, forcing line break - llwchar last_char = text[mStart + first_char + num_chars - 1]; - if (last_char == '\n') - { - force_newline = true; - // don't count newline in font width - width = mStyle->getFont()->getWidth(text.c_str(), mStart + first_char, num_chars - 1); - } - else - { - width = mStyle->getFont()->getWidth(text.c_str(), mStart + first_char, num_chars); - } + width = mStyle->getFont()->getWidth(text.c_str(), mStart + first_char, num_chars); } - - LLUIImagePtr image = mStyle->getImage(); - if( image.notNull()) - { - width += image->getWidth(); - height = llmax(height, image->getHeight()); - } - - return force_newline; + return false; } S32 LLNormalTextSegment::getOffset(S32 segment_local_x_coord, S32 start_offset, S32 num_chars, bool round) const { - const LLWString &text = mEditor.getWText(); + const LLWString &text = getWText(); return mStyle->getFont()->charFromPixelOffset(text.c_str(), mStart + start_offset, (F32)segment_local_x_coord, F32_MAX, @@ -2551,7 +3275,7 @@ S32 LLNormalTextSegment::getOffset(S32 segment_local_x_coord, S32 start_offset, S32 LLNormalTextSegment::getNumChars(S32 num_pixels, S32 segment_offset, S32 line_offset, S32 max_chars) const { - const LLWString &text = mEditor.getWText(); + const LLWString &text = getWText(); LLUIImagePtr image = mStyle->getImage(); if( image.notNull()) @@ -2559,15 +3283,7 @@ S32 LLNormalTextSegment::getNumChars(S32 num_pixels, S32 segment_offset, S32 lin num_pixels = llmax(0, num_pixels - image->getWidth()); } - // search for newline and if found, truncate there - S32 last_char = mStart + segment_offset; - for (; last_char != mEnd; ++last_char) - { - if (text[last_char] == '\n') - { - break; - } - } + S32 last_char = mEnd; // set max characters to length of segment, or to first newline max_chars = llmin(max_chars, last_char - (mStart + segment_offset)); @@ -2577,7 +3293,23 @@ S32 LLNormalTextSegment::getNumChars(S32 num_pixels, S32 segment_offset, S32 lin LLFontGL::EWordWrapStyle word_wrap_style = (line_offset == 0) ? LLFontGL::WORD_BOUNDARY_IF_POSSIBLE : LLFontGL::ONLY_WORD_BOUNDARIES; - S32 num_chars = mStyle->getFont()->maxDrawableChars(text.c_str() + segment_offset + mStart, + + + S32 offsetLength = text.length() - (segment_offset + mStart); + + if(getLength() < segment_offset + mStart) + { + LL_INFOS() << "getLength() < segment_offset + mStart\t getLength()\t" << getLength() << "\tsegment_offset:\t" + << segment_offset << "\tmStart:\t" << mStart << "\tsegments\t" << mEditor.mSegments.size() << "\tmax_chars\t" << max_chars << LL_ENDL; + } + + if( (offsetLength + 1) < max_chars) + { + LL_INFOS() << "offsetString.length() + 1 < max_chars\t max_chars:\t" << max_chars << "\toffsetString.length():\t" << offsetLength << " getLength() : " + << getLength() << "\tsegment_offset:\t" << segment_offset << "\tmStart:\t" << mStart << "\tsegments\t" << mEditor.mSegments.size() << LL_ENDL; + } + + S32 num_chars = mStyle->getFont()->maxDrawableChars( text.c_str() + (segment_offset + mStart), (F32)num_pixels, max_chars, word_wrap_style); @@ -2595,8 +3327,7 @@ S32 LLNormalTextSegment::getNumChars(S32 num_pixels, S32 segment_offset, S32 lin S32 last_char_in_run = mStart + segment_offset + num_chars; // check length first to avoid indexing off end of string if (last_char_in_run < mEnd - && (last_char_in_run >= mEditor.getLength() - || text[last_char_in_run] == '\n')) + && (last_char_in_run >= getLength())) { num_chars++; } @@ -2605,13 +3336,73 @@ S32 LLNormalTextSegment::getNumChars(S32 num_pixels, S32 segment_offset, S32 lin void LLNormalTextSegment::dump() const { - llinfos << "Segment [" << + LL_INFOS() << "Segment [" << // mColor.mV[VX] << ", " << // mColor.mV[VY] << ", " << // mColor.mV[VZ] << "]\t[" << mStart << ", " << getEnd() << "]" << - llendl; + LL_ENDL; +} + +/*virtual*/ +const LLWString& LLNormalTextSegment::getWText() const +{ + return mEditor.getWText(); +} + +/*virtual*/ +const S32 LLNormalTextSegment::getLength() const +{ + return mEditor.getLength(); +} + +LLLabelTextSegment::LLLabelTextSegment( LLStyleConstSP style, S32 start, S32 end, LLTextBase& editor ) +: LLNormalTextSegment(style, start, end, editor) +{ +} + +LLLabelTextSegment::LLLabelTextSegment( const LLColor4& color, S32 start, S32 end, LLTextBase& editor, BOOL is_visible) +: LLNormalTextSegment(color, start, end, editor, is_visible) +{ +} + +/*virtual*/ +const LLWString& LLLabelTextSegment::getWText() const +{ + return mEditor.getWlabel(); +} +/*virtual*/ +const S32 LLLabelTextSegment::getLength() const +{ + return mEditor.getWlabel().length(); +} + +// +// LLOnHoverChangeableTextSegment +// + +LLOnHoverChangeableTextSegment::LLOnHoverChangeableTextSegment( LLStyleConstSP style, LLStyleConstSP normal_style, S32 start, S32 end, LLTextBase& editor ): + LLNormalTextSegment(normal_style, start, end, editor), + mHoveredStyle(style), + mNormalStyle(normal_style){} + +/*virtual*/ +F32 LLOnHoverChangeableTextSegment::draw(S32 start, S32 end, S32 selection_start, S32 selection_end, const LLRect& draw_rect) +{ + F32 result = LLNormalTextSegment::draw(start, end, selection_start, selection_end, draw_rect); + if (end == mEnd - mStart) + { + mStyle = mNormalStyle; + } + return result; +} + +/*virtual*/ +BOOL LLOnHoverChangeableTextSegment::handleHover(S32 x, S32 y, MASK mask) +{ + mStyle = mHoveredStyle; + return LLNormalTextSegment::handleHover(x, y, mask); } @@ -2691,3 +3482,107 @@ void LLInlineViewSegment::linkToDocument(LLTextBase* editor) { editor->addDocumentChild(mView); } + +LLLineBreakTextSegment::LLLineBreakTextSegment(S32 pos):LLTextSegment(pos,pos+1) +{ + LLStyleSP s( new LLStyle(LLStyle::Params().visible(true))); + + mFontHeight = s->getFont()->getLineHeight(); +} +LLLineBreakTextSegment::LLLineBreakTextSegment(LLStyleConstSP style,S32 pos):LLTextSegment(pos,pos+1) +{ + mFontHeight = style->getFont()->getLineHeight(); +} +LLLineBreakTextSegment::~LLLineBreakTextSegment() +{ +} +bool LLLineBreakTextSegment::getDimensions(S32 first_char, S32 num_chars, S32& width, S32& height) const +{ + width = 0; + height = mFontHeight; + + return true; +} +S32 LLLineBreakTextSegment::getNumChars(S32 num_pixels, S32 segment_offset, S32 line_offset, S32 max_chars) const +{ + return 1; +} +F32 LLLineBreakTextSegment::draw(S32 start, S32 end, S32 selection_start, S32 selection_end, const LLRect& draw_rect) +{ + return draw_rect.mLeft; +} + +LLImageTextSegment::LLImageTextSegment(LLStyleConstSP style,S32 pos,class LLTextBase& editor) +: LLTextSegment(pos,pos+1), + mStyle( style ), + mEditor(editor) +{ +} + +LLImageTextSegment::~LLImageTextSegment() +{ +} + +static const S32 IMAGE_HPAD = 3; + +bool LLImageTextSegment::getDimensions(S32 first_char, S32 num_chars, S32& width, S32& height) const +{ + width = 0; + height = mStyle->getFont()->getLineHeight(); + + LLUIImagePtr image = mStyle->getImage(); + if( num_chars>0 && image.notNull()) + { + width += image->getWidth() + IMAGE_HPAD; + height = llmax(height, image->getHeight() + IMAGE_HPAD ); + } + return false; +} + +S32 LLImageTextSegment::getNumChars(S32 num_pixels, S32 segment_offset, S32 line_offset, S32 max_chars) const +{ + LLUIImagePtr image = mStyle->getImage(); + + if (image.isNull()) + { + return 1; + } + + S32 image_width = image->getWidth(); + if(line_offset == 0 || num_pixels>image_width + IMAGE_HPAD) + { + return 1; + } + + return 0; +} + +F32 LLImageTextSegment::draw(S32 start, S32 end, S32 selection_start, S32 selection_end, const LLRect& draw_rect) +{ + if ( (start >= 0) && (end <= mEnd - mStart)) + { + LLColor4 color = LLColor4::white % mEditor.getDrawContext().mAlpha; + LLUIImagePtr image = mStyle->getImage(); + if (image.notNull()) + { + S32 style_image_height = image->getHeight(); + S32 style_image_width = image->getWidth(); + // Text is drawn from the top of the draw_rect downward + + S32 text_center = draw_rect.mTop - (draw_rect.getHeight() / 2); + // Align image to center of draw rect + S32 image_bottom = text_center - (style_image_height / 2); + image->draw(draw_rect.mLeft, image_bottom, + style_image_width, style_image_height, color); + + const S32 IMAGE_HPAD = 3; + return draw_rect.mLeft + style_image_width + IMAGE_HPAD; + } + } + return 0.0; +} + +void LLTextBase::setWordWrap(bool wrap) +{ + mWordWrap = wrap; +} |