/** * @file lltextbase.cpp * @author Martin Reddy * @brief The base class of text box/editor, providing Url handling support * * $LicenseInfo:firstyear=2009&license=viewerlgpl$ * Second Life Viewer Source Code * 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. * * 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. * * 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 * * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA * $/LicenseInfo$ */ #include "linden_common.h" #include "lltextbase.h" #include "llemojidictionary.h" #include "llemojihelper.h" #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 "lltrans.h" #include "lluictrl.h" #include "llurlaction.h" #include "llurlregistry.h" #include "llview.h" #include "llwindow.h" #include 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), mDocIndexEnd(index_end), mRect(rect), mLineNum(line_num) {} bool LLTextBase::compare_segment_end::operator()(const LLTextSegmentPtr& a, const LLTextSegmentPtr& b) const { // sort empty spans (e.g. 11-11) after previous non-empty spans (e.g. 5-11) if (a->getEnd() == b->getEnd()) { return a->getStart() < b->getStart(); } else { return a->getEnd() < b->getEnd(); } } // helper functors bool LLTextBase::compare_bottom::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 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 bool LLTextBase::compare_top::operator()(const S32& a, const LLTextBase::line_info& b) const { return a > b.mRect.mTop; // 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 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 { bool operator()(const S32& pos, const LLTextBase::line_info& info) const { return (pos < info.mDocIndexEnd); } bool operator()(const LLTextBase::line_info& info, const S32& pos) const { return (info.mDocIndexEnd < pos); } bool operator()(const LLTextBase::line_info& a, const LLTextBase::line_info& b) const { return (a.mDocIndexEnd < b.mDocIndexEnd); } }; ////////////////////////////////////////////////////////////////////////// // // LLTextBase // // register LLTextBase::Params under name "textbase" static LLWidgetNameRegistry::StaticRegistrar sRegisterTextBaseParams(&typeid(LLTextBase::Params), "textbase"); LLTextBase::LineSpacingParams::LineSpacingParams() : multiple("multiple", 1.f), pixels("pixels", 0) { } 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), skip_link_underline("skip_link_underline", 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"), text_valign("text_valign"), wrap("wrap"), trusted_content("trusted_content", true), always_show_icons("always_show_icons", false), use_ellipses("use_ellipses", false), use_emoji("use_emoji", true), use_color("use_color", true), parse_urls("parse_urls", false), force_urls_external("force_urls_external", 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(NULL), mIsFriendSignal(NULL), mIsObjectBlockedSignal(NULL), mMaxTextByteLength( p.max_text_length ), mFont(p.font), mFontShadow(p.font_shadow), mPopupMenuHandle(), mReadOnly(p.read_only), mSkipTripleClick(false), mSkipLinkUnderline(p.skip_link_underline), 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), mDesiredXPixel(-1), mHPad(p.h_pad), mVPad(p.v_pad), mHAlign(p.font_halign), mVAlign(p.font_valign), mTextVAlign(p.text_valign.isProvided() ? p.text_valign.getValue() : p.font_valign.getValue()), mLineSpacingMult(p.line_spacing.multiple), mLineSpacingPixels(p.line_spacing.pixels), mClip(p.clip), mClipPartial(p.clip_partial && !p.allow_scroll), mTrustedContent(p.trusted_content), mAlwaysShowIcons(p.always_show_icons), mTrackEnd( p.track_end ), mScrollIndex(-1), mSelectionStart( 0 ), mSelectionEnd( 0 ), mIsSelecting( false ), mPlainText ( p.plain_text ), mWordWrap(p.wrap), mUseEllipses( p.use_ellipses ), mUseEmoji(p.use_emoji), mUseColor(p.use_color), mParseHTML(p.parse_urls), mForceUrlsExternal(p.force_urls_external), mParseHighlights(p.parse_highlights), mBGVisible(p.bg_visible), mScroller(NULL), mStyleDirty(true) { if(p.allow_scroll) { LLScrollContainer::Params scroll_params; scroll_params.name = "text scroller"; scroll_params.rect = getLocalRect(); scroll_params.follows.flags = FOLLOWS_ALL; scroll_params.is_opaque = false; scroll_params.mouse_opaque = false; scroll_params.min_auto_scroll_rate = 200; scroll_params.max_auto_scroll_rate = 800; scroll_params.border_visible = p.border_visible; mScroller = LLUICtrlFactory::create(scroll_params); addChild(mScroller); } LLView::Params view_params; view_params.name = "text_contents"; view_params.rect = LLRect(0, 500, 500, 0); view_params.mouse_opaque = false; mDocumentView = LLUICtrlFactory::create(view_params); if (mScroller) { mScroller->addChild(mDocumentView); } else { addChild(mDocumentView); } if (mSpellCheck) { LLSpellChecker::setSettingsChangeCallback(boost::bind(&LLTextBase::onSpellCheckSettingsChange, this)); } mSpellCheckTimer.reset(); createDefaultSegment(); updateRects(); } LLTextBase::~LLTextBase() { mSegments.clear(); LLContextMenu* menu = static_cast(mPopupMenuHandle.get()); if (menu) { menu->die(); mPopupMenuHandle.markDead(); } delete mURLClickSignal; delete mIsFriendSignal; delete mIsObjectBlockedSignal; } void LLTextBase::initFromParams(const LLTextBase::Params& p) { LLUICtrl::initFromParams(p); resetDirty(); // Update saved text state updateSegments(); // HACK: work around enabled == readonly design bug -- RN // setEnabled will modify our read only status, so do this after // LLTextBase::initFromParams if (p.read_only.isProvided()) { mReadOnly = p.read_only; } } bool LLTextBase::truncate() { bool did_truncate = false; // First rough check - if we're less than 1/4th the size, we're OK if (getLength() >= S32(mMaxTextByteLength / 4)) { // Have to check actual byte size S32 utf8_byte_size = 0; LLSD value = getViewModel()->getValue(); if (value.type() == LLSD::TypeString) { // save a copy for strings. utf8_byte_size = static_cast(value.size()); } else { // non string LLSDs need explicit conversion to string utf8_byte_size = static_cast(value.asString().size()); } if ( utf8_byte_size > mMaxTextByteLength ) { // Truncate safely in UTF-8 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. removeStringNoUndo(static_cast(text.size()), static_cast(getWText().size() - text.size())); did_truncate = true; } } return did_truncate; } const LLStyle::Params& LLTextBase::getStyleParams() { //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) { } std::vector LLTextBase::getSelectionRects() { // Nor supposed to be called without selection llassert(hasSelection()); llassert(!mLineInfoList.empty()); std::vector selection_rects; S32 selection_left = llmin(mSelectionStart, mSelectionEnd); S32 selection_right = llmax(mSelectionStart, mSelectionEnd); // 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::upper_bound(mLineInfoList.begin(), mLineInfoList.end(), content_display_rect.mBottom, compare_top()); bool done = false; // Find the coordinates of the selected area for (; line_iter != end_iter && !done; ++line_iter) { // is selection visible on this line? if (line_iter->mDocIndexEnd > selection_left && line_iter->mDocIndexStart < selection_right) { segment_set_t::iterator segment_iter; S32 segment_offset; getSegmentAndOffset(line_iter->mDocIndexStart, &segment_iter, &segment_offset); // Use F32 otherwise a string of multiple segments // will accumulate a large error F32 left_precise = (F32)line_iter->mRect.mLeft; F32 right_precise = (F32)line_iter->mRect.mLeft; for (; segment_iter != mSegments.end(); ++segment_iter, segment_offset = 0) { LLTextSegmentPtr segmentp = *segment_iter; S32 segment_line_start = segmentp->getStart() + segment_offset; S32 segment_line_end = llmin(segmentp->getEnd(), line_iter->mDocIndexEnd); if (segment_line_start > segment_line_end) break; F32 segment_width = 0; S32 segment_height = 0; // if selection after beginning of segment if (selection_left >= segment_line_start) { S32 num_chars = llmin(selection_left, segment_line_end) - segment_line_start; segmentp->getDimensionsF32(segment_offset, num_chars, segment_width, segment_height); left_precise += segment_width; } // if selection_right == segment_line_end then that means we are the first character of the next segment // or first character of the next line, in either case we want to add the length of the current segment // to the selection rectangle and continue. // if selection right > segment_line_end then selection spans end of current segment... if (selection_right >= segment_line_end) { // extend selection slightly beyond end of line // to indicate selection of newline character (use "n" character to determine width) S32 num_chars = segment_line_end - segment_line_start; segmentp->getDimensionsF32(segment_offset, num_chars, segment_width, segment_height); right_precise += segment_width; } // else if selection ends on current segment... else { S32 num_chars = selection_right - segment_line_start; segmentp->getDimensionsF32(segment_offset, num_chars, segment_width, segment_height); right_precise += segment_width; break; } } LLRect selection_rect; selection_rect.mLeft = (S32)left_precise; selection_rect.mRight = (S32)right_precise; selection_rect.mBottom = line_iter->mRect.mBottom; selection_rect.mTop = line_iter->mRect.mTop; selection_rects.push_back(selection_rect); } } return selection_rects; } // Draws the black box behind the selected text void LLTextBase::drawSelectionBackground() { // Draw selection even if we don't have keyboard focus for search/replace if (hasSelection() && !mLineInfoList.empty()) { std::vector selection_rects = getSelectionRects(); // 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 = mSelectedBGColor; F32 alpha = hasFocus() ? 0.7f : 0.3f; alpha *= getDrawContext().mAlpha; LLColor4 selection_color(color.mV[VRED], color.mV[VGREEN], color.mV[VBLUE], alpha); LLRect content_display_rect = getVisibleDocumentRect(); for (std::vector::iterator rect_it = selection_rects.begin(); rect_it != selection_rects.end(); ++rect_it) { LLRect selection_rect = *rect_it; if (mScroller) { // If scroller is On content_display_rect has correct rect and safe to use as is // Note: we might need to account for border selection_rect.translate(mVisibleTextRect.mLeft - content_display_rect.mLeft, mVisibleTextRect.mBottom - content_display_rect.mBottom); } else { // If scroller is Off content_display_rect will have rect from document, adjusted to text width, heigh and position // and we have to acount for offset depending on position S32 v_delta = 0; S32 h_delta = 0; switch (mVAlign) { case LLFontGL::TOP: v_delta = mVisibleTextRect.mTop - content_display_rect.mTop - mVPad; break; case LLFontGL::VCENTER: v_delta = (llmax(mVisibleTextRect.getHeight() - content_display_rect.mTop, -content_display_rect.mBottom) + (mVisibleTextRect.mBottom - content_display_rect.mBottom)) / 2; break; case LLFontGL::BOTTOM: v_delta = mVisibleTextRect.mBottom - content_display_rect.mBottom; break; default: break; } switch (mHAlign) { case LLFontGL::LEFT: h_delta = mVisibleTextRect.mLeft - content_display_rect.mLeft + mHPad; break; case LLFontGL::HCENTER: h_delta = (llmax(mVisibleTextRect.getWidth() - content_display_rect.mLeft, -content_display_rect.mRight) + (mVisibleTextRect.mRight - content_display_rect.mRight)) / 2; break; case LLFontGL::RIGHT: h_delta = mVisibleTextRect.mRight - content_display_rect.mRight; break; default: break; } selection_rect.translate(h_delta, v_delta); } gl_rect_2d(selection_rect, selection_color); } } } void LLTextBase::drawCursor() { F32 alpha = getDrawContext().mAlpha; if( hasFocus() && gFocusMgr.getAppHasFocus() && !mReadOnly) { const LLWString &wtext = getWText(); const llwchar* text = wtext.c_str(); LLRect cursor_rect = getLocalRectFromDocIndex(mCursorPos); cursor_rect.translate(-1, 0); segment_set_t::iterator seg_it = getSegIterContaining(mCursorPos); // take style from last segment LLTextSegmentPtr segmentp; if (seg_it != mSegments.end()) { segmentp = *seg_it; } else { return; } // Draw the cursor // (Flash the cursor every half second starting a fixed time after the last keystroke) F32 elapsed = mCursorBlinkTimer.getElapsedTimeF32(); if( (elapsed < CURSOR_FLASH_DELAY ) || (S32(elapsed * 2) & 1) ) { if (LL_KIM_OVERWRITE == gKeyboard->getInsertMode() && !hasSelection()) { S32 segment_width = 0; S32 segment_height = 0; segmentp->getDimensions(mCursorPos - segmentp->getStart(), 1, segment_width, segment_height); S32 width = llmax(CURSOR_THICKNESS, segment_width); cursor_rect.mRight = cursor_rect.mLeft + width; } else { cursor_rect.mRight = cursor_rect.mLeft + CURSOR_THICKNESS; } gGL.getTexUnit(0)->unbind(LLTexUnit::TT_TEXTURE); LLColor4 cursor_color = mCursorColor.get() % alpha; gGL.color4fv( cursor_color.mV ); gl_rect_2d(cursor_rect); if (LL_KIM_OVERWRITE == gKeyboard->getInsertMode() && !hasSelection() && text[mCursorPos] != '\n') { const LLFontGL* fontp; const LLColor4& 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, mTextVAlign, LLFontGL::NORMAL, LLFontGL::NO_SHADOW, 1); } // Make sure the IME is in the right place LLRect screen_pos = calcScreenRect(); LLCoordGL ime_pos( screen_pos.mLeft + cursor_rect.mLeft, screen_pos.mBottom + cursor_rect.mTop ); 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 ); } } } void LLTextBase::drawText() { S32 text_len = getLength(); if (text_len <= 0 && mLabel.empty()) { return; } else if (useLabel()) { text_len = static_cast(mLabel.getWString().length()); } S32 selection_left = -1; S32 selection_right = -1; // Draw selection even if we don't have keyboard focus for search/replace if( hasSelection()) { selection_left = llmin( mSelectionStart, mSelectionEnd ); selection_right = llmax( mSelectionStart, mSelectionEnd ); } std::pair line_range = getVisibleLines(mClipPartial); S32 first_line = line_range.first; S32 last_line = line_range.second; if (first_line >= last_line) { return; } S32 line_start = getLineStart(first_line); // find first text segment that spans top of visible portion of text buffer segment_set_t::iterator seg_iter = getSegIterContaining(line_start); if (seg_iter == mSegments.end()) { 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 = static_cast(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(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 >::const_iterator misspell_it = std::lower_bound(mMisspellRanges.begin(), mMisspellRanges.end(), std::pair(line_start, 0)); for (S32 cur_line = first_line; cur_line < last_line; cur_line++) { S32 next_line = cur_line + 1; line_info& line = mLineInfoList[cur_line]; S32 next_start = -1; S32 line_end = text_len; if (next_line < getLineCount()) { next_start = getLineStart(next_line); line_end = next_start; } LLRectf text_rect((F32)line.mRect.mLeft, (F32)line.mRect.mTop, (F32)line.mRect.mRight, (F32)line.mRect.mBottom); text_rect.mRight = (F32)mDocumentView->getRect().getWidth(); // clamp right edge to document extents text_rect.translate((F32)mDocumentView->getRect().mLeft, (F32)mDocumentView->getRect().mBottom); // adjust by scroll position // draw a single line of text S32 seg_start = line_start; while( seg_start < line_end ) { while( cur_segment->getEnd() <= seg_start ) { seg_iter++; if (seg_iter == mSegments.end()) { LL_WARNS() << "Ran off the segmentation end!" << LL_ENDL; return; } cur_segment = *seg_iter; } 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 && next_line == last_line // this is the last visible line && last_line < (S32)mLineInfoList.size()) // and there is more text to display { // more lines of text to go, but we can't fit them // so shrink text rect to force ellipses text_rect.mRight -= 2; } // Draw squiggly lines under any visible misspelled words while ( (mMisspellRanges.end() != misspell_it) && (misspell_it->first < (U32)seg_end) && (misspell_it->second > (U32)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(misspell_it->first, (U32)seg_start), misspell_end = llmin(misspell_it->second, (U32)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 += (S32)text_rect.mLeft; pony = (squiggle_end + 3) / 6; squiggle_start += squiggle_end / 2 - pony * 3; squiggle_end = squiggle_start + pony * 6; S32 squiggle_bottom = (S32)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 > (U32)seg_end) { break; } ++misspell_it; } text_rect.mLeft = cur_segment->draw(seg_start - cur_segment->getStart(), clipped_end, selection_left, selection_right, text_rect); seg_start = clipped_end + cur_segment->getStart(); } line_start = next_start; } } /////////////////////////////////////////////////////////////////// // Returns change in number of characters in mWText S32 LLTextBase::insertStringNoUndo(S32 pos, const LLWString &wstr, LLTextBase::segment_vec_t* segments ) { beforeValueChange(); S32 old_len = getLength(); // length() returns character length S32 insert_len = static_cast(wstr.length()); pos = getEditableIndex(pos, true); if (pos > old_len) { pos = old_len; // Should not happen, // if you encounter this, check where wrong position comes from llassert(false); } segment_set_t::iterator seg_iter = getEditableSegIterContaining(pos); LLTextSegmentPtr default_segment; LLTextSegmentPtr segmentp; if (seg_iter != mSegments.end()) { segmentp = *seg_iter; } else { //segmentp = mSegments.back(); return pos; } if (segmentp->canEdit()) { segmentp->setEnd(segmentp->getEnd() + insert_len); if (seg_iter != mSegments.end()) { ++seg_iter; } } else { // create default editable segment to hold new text LLStyleConstSP sp(new LLStyle(getStyleParams())); default_segment = new LLNormalTextSegment( sp, pos, pos + insert_len, *this); } // shift remaining segments to right for(;seg_iter != mSegments.end(); ++seg_iter) { LLTextSegmentPtr segmentp = *seg_iter; segmentp->setStart(segmentp->getStart() + insert_len); segmentp->setEnd(segmentp->getEnd() + insert_len); } // insert new segments if (segments) { if (default_segment.notNull()) { // potentially overwritten by segments passed in insertSegment(default_segment); } for (segment_vec_t::iterator seg_iter = segments->begin(); seg_iter != segments->end(); ++seg_iter) { LLTextSegment* segmentp = *seg_iter; insertSegment(segmentp); } } // Insert special segments where necessary (insertSegment takes care of splitting normal text segments around them for us) if (mUseEmoji) { LLStyleSP emoji_style; LLEmojiDictionary* ed = LLEmojiDictionary::instanceExists() ? LLEmojiDictionary::getInstance() : NULL; for (S32 text_kitty = 0, text_len = static_cast(wstr.size()); text_kitty < text_len; text_kitty++) { llwchar code = wstr[text_kitty]; bool isEmoji = ed ? ed->isEmoji(code) : LLStringOps::isEmoji(code); if (isEmoji) { if (!emoji_style) { emoji_style = new LLStyle(getStyleParams()); emoji_style->setFont(LLFontGL::getFontEmojiLarge()); } S32 new_seg_start = pos + text_kitty; insertSegment(new LLEmojiTextSegment(emoji_style, new_seg_start, new_seg_start + 1, *this)); } } } getViewModel()->getEditableDisplay().insert(pos, wstr); if ( truncate() ) { insert_len = getLength() - old_len; } onValueChange(pos, pos + insert_len); needsReflow(pos); return insert_len; } S32 LLTextBase::removeStringNoUndo(S32 pos, S32 length) { beforeValueChange(); segment_set_t::iterator seg_iter = getSegIterContaining(pos); while(seg_iter != mSegments.end()) { LLTextSegmentPtr segmentp = *seg_iter; S32 end = pos + length; if (segmentp->getStart() < pos) { // deleting from middle of segment if (segmentp->getEnd() > end) { segmentp->setEnd(segmentp->getEnd() - length); } // truncating segment else { segmentp->setEnd(pos); } } else if (segmentp->getStart() < end) { // deleting entire segment if (segmentp->getEnd() <= end) { // remove segment segmentp->unlinkFromDocument(this); segment_set_t::iterator seg_to_erase(seg_iter++); mSegments.erase(seg_to_erase); continue; } // deleting head of segment else { segmentp->setStart(pos); segmentp->setEnd(segmentp->getEnd() - length); } } else { // shifting segments backward to fill deleted portion segmentp->setStart(segmentp->getStart() - length); segmentp->setEnd(segmentp->getEnd() - length); } ++seg_iter; } getViewModel()->getEditableDisplay().erase(pos, length); // recreate default segment in case we erased everything createDefaultSegment(); onValueChange(pos, pos); needsReflow(pos); return -length; // This will be wrong if someone calls removeStringNoUndo with an excessive length } S32 LLTextBase::overwriteCharNoUndo(S32 pos, llwchar wc) { beforeValueChange(); if (pos > (S32)getLength()) { return 0; } getViewModel()->getEditableDisplay()[pos] = wc; onValueChange(pos, pos + 1); needsReflow(pos); return 1; } void LLTextBase::createDefaultSegment() { // ensures that there is always at least one segment if (mSegments.empty()) { LLStyleConstSP sp(new LLStyle(getStyleParams())); LLTextSegmentPtr default_segment = new LLNormalTextSegment( sp, 0, getLength() + 1, *this); mSegments.insert(default_segment); default_segment->linkToDocument(this); } } void LLTextBase::insertSegment(LLTextSegmentPtr segment_to_insert) { if (segment_to_insert.isNull()) { return; } segment_set_t::iterator cur_seg_iter = getSegIterContaining(segment_to_insert->getStart()); S32 reflow_start_index = 0; if (cur_seg_iter == mSegments.end()) { mSegments.insert(segment_to_insert); segment_to_insert->linkToDocument(this); reflow_start_index = segment_to_insert->getStart(); } else { LLTextSegmentPtr cur_segmentp = *cur_seg_iter; reflow_start_index = cur_segmentp->getStart(); if (cur_segmentp->getStart() < segment_to_insert->getStart()) { S32 old_segment_end = cur_segmentp->getEnd(); // split old at start point for new segment cur_segmentp->setEnd(segment_to_insert->getStart()); // advance to next segment // insert remainder of old segment LLStyleConstSP sp = cur_segmentp->getStyle(); LLTextSegmentPtr remainder_segment = new LLNormalTextSegment( sp, segment_to_insert->getStart(), old_segment_end, *this); mSegments.insert(cur_seg_iter, remainder_segment); remainder_segment->linkToDocument(this); // insert new segment before remainder of old segment mSegments.insert(cur_seg_iter, segment_to_insert); segment_to_insert->linkToDocument(this); // at this point, there will be two overlapping segments owning the text // associated with the incoming segment } else { mSegments.insert(cur_seg_iter, segment_to_insert); segment_to_insert->linkToDocument(this); } // now delete/truncate remaining segments as necessary // cur_seg_iter points to segment before incoming segment while(cur_seg_iter != mSegments.end()) { cur_segmentp = *cur_seg_iter; if (cur_segmentp == segment_to_insert) { ++cur_seg_iter; continue; } if (cur_segmentp->getStart() >= segment_to_insert->getStart()) { if(cur_segmentp->getEnd() <= segment_to_insert->getEnd()) { cur_segmentp->unlinkFromDocument(this); // grab copy of iterator to erase, and bump it segment_set_t::iterator seg_to_erase(cur_seg_iter++); mSegments.erase(seg_to_erase); continue; } else { // last overlapping segment, clip to end of incoming segment // and stop traversal cur_segmentp->setStart(segment_to_insert->getEnd()); break; } } ++cur_seg_iter; } } // layout potentially changed needsReflow(reflow_start_index); } //virtual bool LLTextBase::handleMouseDown(S32 x, S32 y, MASK mask) { // handle triple click if (!mTripleClickTimer.hasExpired()) { if (mSkipTripleClick) { return true; } S32 real_line = getLineNumFromDocIndex(mCursorPos, false); S32 line_start = -1; S32 line_end = -1; for (line_list_t::const_iterator it = mLineInfoList.begin(), end_it = mLineInfoList.end(); it != end_it; ++it) { if (it->mLineNum < real_line) { continue; } if (it->mLineNum > real_line) { break; } if (line_start == -1) { line_start = it->mDocIndexStart; } line_end = it->mDocIndexEnd; line_end = llclamp(line_end, 0, getLength()); } if (line_start == -1) { return true; } mSelectionEnd = line_start; mSelectionStart = line_end; setCursorPos(line_start); return true; } LLTextSegmentPtr cur_segment = getSegmentAtLocalPos(x, y); if (cur_segment && cur_segment->handleMouseDown(x, y, mask)) { return true; } return LLUICtrl::handleMouseDown(x, y, mask); } //virtual bool LLTextBase::handleMouseUp(S32 x, S32 y, MASK mask) { LLTextSegmentPtr cur_segment = getSegmentAtLocalPos(x, y); if (hasMouseCapture() && cur_segment && cur_segment->handleMouseUp(x, y, mask)) { // Did we just click on a link? if (mURLClickSignal && cur_segment->getStyle() && cur_segment->getStyle()->isLink()) { // *TODO: send URL here? (*mURLClickSignal)(this, LLSD() ); } return true; } return LLUICtrl::handleMouseUp(x, y, mask); } //virtual bool LLTextBase::handleMiddleMouseDown(S32 x, S32 y, MASK mask) { LLTextSegmentPtr cur_segment = getSegmentAtLocalPos(x, y); if (cur_segment && cur_segment->handleMiddleMouseDown(x, y, mask)) { return true; } return LLUICtrl::handleMiddleMouseDown(x, y, mask); } //virtual bool LLTextBase::handleMiddleMouseUp(S32 x, S32 y, MASK mask) { LLTextSegmentPtr cur_segment = getSegmentAtLocalPos(x, y); if (cur_segment && cur_segment->handleMiddleMouseUp(x, y, mask)) { return true; } return LLUICtrl::handleMiddleMouseUp(x, y, mask); } //virtual bool LLTextBase::handleRightMouseDown(S32 x, S32 y, MASK mask) { LLTextSegmentPtr cur_segment = getSegmentAtLocalPos(x, y); if (cur_segment && cur_segment->handleRightMouseDown(x, y, mask)) { return true; } return LLUICtrl::handleRightMouseDown(x, y, mask); } //virtual bool LLTextBase::handleRightMouseUp(S32 x, S32 y, MASK mask) { LLTextSegmentPtr cur_segment = getSegmentAtLocalPos(x, y); if (cur_segment && cur_segment->handleRightMouseUp(x, y, mask)) { return true; } return LLUICtrl::handleRightMouseUp(x, y, mask); } //virtual 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)) { return true; } return LLUICtrl::handleDoubleClick(x, y, mask); } //virtual bool LLTextBase::handleHover(S32 x, S32 y, MASK mask) { LLTextSegmentPtr cur_segment = getSegmentAtLocalPos(x, y); if (cur_segment && cur_segment->handleHover(x, y, mask)) { return true; } return LLUICtrl::handleHover(x, y, mask); } //virtual bool LLTextBase::handleScrollWheel(S32 x, S32 y, S32 clicks) { LLTextSegmentPtr cur_segment = getSegmentAtLocalPos(x, y); if (cur_segment && cur_segment->handleScrollWheel(x, y, clicks)) { return true; } return LLUICtrl::handleScrollWheel(x, y, clicks); } //virtual bool LLTextBase::handleToolTip(S32 x, S32 y, MASK mask) { LLTextSegmentPtr cur_segment = getSegmentAtLocalPos(x, y); if (cur_segment && cur_segment->handleToolTip(x, y, mask)) { return true; } return LLUICtrl::handleToolTip(x, y, mask); } //virtual void LLTextBase::reshape(S32 width, S32 height, bool called_from_parent) { if (width != getRect().getWidth() || height != getRect().getHeight() || LLView::sForceReshape) { 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(); needsReflow(); } } //virtual void LLTextBase::draw() { // reflow if needed, on demand reflow(); // then update scroll position, as cursor may have moved if (!mReadOnly) { updateScrollFromCursor(); } LLRect text_rect; if (mScroller) { mScroller->localRectToOtherView(mScroller->getContentWindowRect(), &text_rect, this); } else { LLRect visible_lines_rect; std::pair 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 LLRect bg_rect = mVisibleTextRect; if (mScroller) { bg_rect.intersectWith(text_rect); } const LLColor4& bg_color = mReadOnly ? mReadOnlyBgColor.get() : hasFocus() ? mFocusBgColor.get() : mWriteableBgColor.get(); gl_rect_2d(text_rect, bg_color % alpha, true); } // Draw highlighted if needed if( ll::ui::SearchableControl::getHighlighted() ) { const LLColor4& bg_color = ll::ui::SearchableControl::getHighlightColor(); LLRect bg_rect = mVisibleTextRect; if( mScroller ) bg_rect.intersectWith( text_rect ); gl_rect_2d( text_rect, bg_color, true ); } 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->setVisibleDirect(false); LLUICtrl::draw(); mDocumentView->setVisibleDirect(true); } //virtual void LLTextBase::setColor( const LLUIColor& c ) { mFgColor = c; mStyleDirty = true; } //virtual void LLTextBase::setReadOnlyColor(const LLUIColor &c) { mReadOnlyFgColor = c; mStyleDirty = true; } //virtual void LLTextBase::onVisibilityChange( bool new_visibility ) { LLContextMenu* menu = static_cast(mPopupMenuHandle.get()); if(!new_visibility && menu) { menu->hide(); } LLUICtrl::onVisibilityChange(new_visibility); } //virtual void LLTextBase::setValue(const LLSD& value ) { setText(value.asString()); } //virtual bool LLTextBase::canDeselect() const { return hasSelection(); } //virtual void LLTextBase::deselect() { mSelectionStart = 0; mSelectionEnd = 0; 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 static_cast(mSuggestionList.size()); } void LLTextBase::replaceWithSuggestion(U32 index) { for (std::list >::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()); onSpellCheckPerformed(); 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 >::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 >::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() { // Update scroll position even in read-only mode (when there's no cursor displayed) // because startOfDoc()/endOfDoc() modify cursor position. See EXT-736. if (!mScrollNeeded || !mScroller) { return; } mScrollNeeded = false; // scroll so that the cursor is at the top of the page LLRect scroller_doc_window = getVisibleDocumentRect(); LLRect cursor_rect_doc = getDocRectFromDocIndex(mCursorPos); mScroller->scrollToShowRect(cursor_rect_doc, LLRect(0, scroller_doc_window.getHeight() - 5, scroller_doc_window.getWidth(), 5)); } S32 LLTextBase::getLeftOffset(S32 width) { switch (mHAlign) { case LLFontGL::LEFT: return mHPad; case LLFontGL::HCENTER: return mHPad + llmax(0, (mVisibleTextRect.getWidth() - width - mHPad) / 2); case LLFontGL::RIGHT: { // Font's rendering rounds string size, if value gets rounded // down last symbol might not have enough space to render, // compensate by adding an extra pixel as padding const S32 right_padding = 1; return llmax(mHPad, mVisibleTextRect.getWidth() - width - right_padding); } default: return mHPad; } } void LLTextBase::reflow() { LL_PROFILE_ZONE_SCOPED_CATEGORY_UI; 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 if (mWordWrap) { mDocumentView->reshape(mVisibleTextRect.getWidth(), mDocumentView->getRect().getHeight()); } S32 cur_top = 0; segment_set_t::iterator seg_iter = mSegments.begin(); S32 seg_offset = 0; S32 line_start_index = 0; const F32 text_available_width = (F32)(mVisibleTextRect.getWidth() - mHPad); // reserve room for margin F32 remaining_pixels = text_available_width; S32 line_count = 0; // find and erase line info structs starting at start_index and going to end of document if (!mLineInfoList.empty()) { // find first element whose end comes after start_index line_list_t::iterator iter = std::upper_bound(mLineInfoList.begin(), mLineInfoList.end(), start_index, line_end_compare()); if (iter != mLineInfoList.end()) { line_start_index = iter->mDocIndexStart; line_count = iter->mLineNum; cur_top = iter->mRect.mTop; getSegmentAndOffset(iter->mDocIndexStart, &seg_iter, &seg_offset); mLineInfoList.erase(iter, mLineInfoList.end()); } } S32 line_height = 0; S32 seg_line_offset = line_count + 1; while(seg_iter != mSegments.end()) { LLTextSegmentPtr segment = *seg_iter; // track maximum height of any segment on this line S32 cur_index = segment->getStart() + seg_offset; // ask segment how many character fit in remaining space S32 character_count = segment->getNumChars(getWordWrap() ? llmax(0, ll_round(remaining_pixels)) : S32_MAX, seg_offset, cur_index - line_start_index, S32_MAX, line_count - seg_line_offset); F32 segment_width; S32 segment_height; bool force_newline = segment->getDimensionsF32(seg_offset, character_count, segment_width, segment_height); // grow line height as necessary based on reported height of this segment line_height = llmax(line_height, segment_height); remaining_pixels -= segment_width; seg_offset += character_count; S32 last_segment_char_on_line = segment->getStart() + seg_offset; // Note: make sure text will fit in width - use ceil, but also make sure // ceil is used only once per line S32 text_actual_width = llceil(text_available_width - remaining_pixels); S32 text_left = getLeftOffset(text_actual_width); LLRect line_rect(text_left, cur_top, text_left + text_actual_width, cur_top - line_height); // if we didn't finish the current segment... if (last_segment_char_on_line < segment->getEnd()) { // add line info and keep going mLineInfoList.push_back(line_info( line_start_index, last_segment_char_on_line, line_rect, line_count)); line_start_index = segment->getStart() + seg_offset; cur_top -= ll_round((F32)line_height * mLineSpacingMult) + mLineSpacingPixels; remaining_pixels = text_available_width; line_height = 0; } // ...just consumed last segment.. else if (++segment_set_t::iterator(seg_iter) == mSegments.end()) { mLineInfoList.push_back(line_info( line_start_index, last_segment_char_on_line, line_rect, line_count)); cur_top -= ll_round((F32)line_height * mLineSpacingMult) + mLineSpacingPixels; break; } // ...or finished a segment and there are segments remaining on this line else { // subtract pixels used and increment segment if (force_newline) { mLineInfoList.push_back(line_info( line_start_index, last_segment_char_on_line, line_rect, line_count)); line_start_index = segment->getStart() + seg_offset; cur_top -= ll_round((F32)line_height * mLineSpacingMult) + mLineSpacingPixels; line_height = 0; remaining_pixels = text_available_width; } ++seg_iter; seg_offset = 0; seg_line_offset = force_newline ? line_count + 1 : line_count; } if (force_newline) { line_count++; } } // calculate visible region for diplaying text updateRects(); for (segment_set_t::iterator segment_it = mSegments.begin(); segment_it != mSegments.end(); ++segment_it) { LLTextSegmentPtr segmentp = *segment_it; segmentp->updateLayout(*this); } } // apply scroll constraints after reflowing text if (!hasMouseCapture() && mScroller) { 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); 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); // 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() { reflow(); return mTextBoundingRect; } void LLTextBase::clearSegments() { mSegments.clear(); createDefaultSegment(); } 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; } 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; } S32 LLTextBase::getLineNumFromDocIndex( S32 doc_index, bool include_wordwrap) const { if (mLineInfoList.empty()) { return 0; } else { line_list_t::const_iterator iter = std::upper_bound(mLineInfoList.begin(), mLineInfoList.end(), doc_index, line_end_compare()); if (include_wordwrap) { return (S32)(iter - mLineInfoList.begin()); } else { if (iter == mLineInfoList.end()) { return mLineInfoList.back().mLineNum; } else { return iter->mLineNum; } } } } // Given an offset into text (pos), find the corresponding line (from the start of the doc) and an offset into the line. S32 LLTextBase::getLineOffsetFromDocIndex( S32 startpos, bool include_wordwrap) const { if (mLineInfoList.empty()) { return startpos; } else { line_list_t::const_iterator iter = std::upper_bound(mLineInfoList.begin(), mLineInfoList.end(), startpos, line_end_compare()); return startpos - iter->mDocIndexStart; } } S32 LLTextBase::getFirstVisibleLine() const { LLRect visible_region = getVisibleDocumentRect(); // binary search for line that starts before top of visible buffer line_list_t::const_iterator iter = std::lower_bound(mLineInfoList.begin(), mLineInfoList.end(), visible_region.mTop, compare_bottom()); return (S32)(iter - mLineInfoList.begin()); } std::pair LLTextBase::getVisibleLines(bool require_fully_visible) { LLRect visible_region = getVisibleDocumentRect(); line_list_t::const_iterator first_iter; line_list_t::const_iterator last_iter; // make sure we have an up-to-date mLineInfoList reflow(); if (require_fully_visible) { first_iter = std::lower_bound(mLineInfoList.begin(), mLineInfoList.end(), visible_region.mTop, compare_top()); last_iter = std::upper_bound(mLineInfoList.begin(), mLineInfoList.end(), visible_region.mBottom, compare_bottom()); } else { 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)(first_iter - mLineInfoList.begin()), (S32)(last_iter - mLineInfoList.begin())); } LLTextViewModel* LLTextBase::getViewModel() const { return (LLTextViewModel*)mViewModel.get(); } void LLTextBase::addDocumentChild(LLView* view) { mDocumentView->addChild(view); } void LLTextBase::removeDocumentChild(LLView* view) { mDocumentView->removeChild(view); } void LLTextBase::updateSegments() { LL_PROFILE_ZONE_SCOPED_CATEGORY_UI; createDefaultSegment(); } 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::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) { static LLPointer index_segment = new LLIndexSegment(); // 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 { static LLPointer index_segment = new LLIndexSegment(); // 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; } // Finds the text segment (if any) at the give local screen position LLTextSegmentPtr LLTextBase::getSegmentAtLocalPos( S32 x, S32 y, bool hit_past_end_of_line) { // Find the cursor position at the requested local screen position S32 offset = getDocIndexFromLocalCoord( x, y, false, hit_past_end_of_line); segment_set_t::iterator seg_iter = getSegIterContaining(offset); if (seg_iter != mSegments.end()) { return *seg_iter; } else { return LLTextSegmentPtr(); } } void LLTextBase::createUrlContextMenu(S32 x, S32 y, 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; } std::string xui_file = match.getMenuName(); if (xui_file.empty()) { return; } // 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, true)); registrar.add("Url.Block", boost::bind(&LLUrlAction::blockObject, url)); registrar.add("Url.Unblock", boost::bind(&LLUrlAction::unblockObject, 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.ReportAbuse", boost::bind(&LLUrlAction::reportAbuse, 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 LLContextMenu* menu = static_cast(mPopupMenuHandle.get()); if (menu) { menu->die(); mPopupMenuHandle.markDead(); } llassert(LLMenuGL::sMenuContainer != NULL); menu = LLUICtrlFactory::getInstance()->createFromFile(xui_file, LLMenuGL::sMenuContainer, LLMenuHolderGL::child_registry_t::instance()); if (menu) { mPopupMenuHandle = menu->getHandle(); if (mIsFriendSignal) { bool isFriend = *(*mIsFriendSignal)(LLUUID(LLUrlAction::getUserID(url))); LLView* addFriendButton = menu->getChild("add_friend"); LLView* removeFriendButton = menu->getChild("remove_friend"); if (addFriendButton && removeFriendButton) { addFriendButton->setEnabled(!isFriend); removeFriendButton->setEnabled(isFriend); } } if (mIsObjectBlockedSignal) { bool is_blocked = *(*mIsObjectBlockedSignal)(LLUUID(LLUrlAction::getObjectId(url)), LLUrlAction::getObjectName(url)); LLView* blockButton = menu->getChild("block_object"); LLView* unblockButton = menu->getChild("unblock_object"); if (blockButton && unblockButton) { blockButton->setVisible(!is_blocked); unblockButton->setVisible(is_blocked); } } menu->show(x, y); LLMenuGL::showPopup(this, menu, x, y); } } void LLTextBase::setText(const LLStringExplicit &utf8str, const LLStyle::Params& input_params) { // clear out the existing text and segments getViewModel()->setDisplay(LLWStringUtil::null); clearSegments(); // createDefaultSegment(); deselect(); // append the new text (supports Url linking) std::string text(utf8str); LLStringUtil::removeCRLF(text); // appendText modifies mCursorPos... appendText(text, false, input_params); // ...so move cursor to top after appending text if (!mTrackEnd) { startOfDoc(); } onValueChange(0, getLength()); } // virtual const std::string& LLTextBase::getText() const { return getViewModel()->getStringValue(); } // 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); } } void LLTextBase::appendTextImpl(const std::string &new_text, const LLStyle::Params& input_params) { LL_PROFILE_ZONE_SCOPED_CATEGORY_UI; LLStyle::Params style_params(getStyleParams()); style_params.overwriteFrom(input_params); S32 part = (S32)LLTextParser::WHOLE; if (mParseHTML && !style_params.is_link) // Don't search for URLs inside a link segment (STORM-358). { S32 start=0,end=0; LLUrlMatch match; std::string text = new_text; while (LLUrlRegistry::instance().findUrl(text, match, boost::bind(&LLTextBase::replaceUrl, this, _1, _2, _3), isContentTrusted() || mAlwaysShowIcons)) { start = match.getStart(); end = match.getEnd()+1; LLStyle::Params link_params(style_params); link_params.overwriteFrom(match.getStyle()); // output the text before the Url if (start > 0) { if (part == (S32)LLTextParser::WHOLE || part == (S32)LLTextParser::START) { part = (S32)LLTextParser::START; } else { part = (S32)LLTextParser::MIDDLE; } std::string subtext=text.substr(0,start); appendAndHighlightText(subtext, part, style_params); } // add icon before url if need LLTextUtil::processUrlMatch(&match, this, isContentTrusted() || match.isTrusted() || mAlwaysShowIcons); if ((isContentTrusted() || match.isTrusted()) && !match.getIcon().empty() ) { setLastSegmentToolTip(LLTrans::getString("TooltipSLIcon")); } // output the styled Url appendAndHighlightTextImpl(match.getLabel(), part, link_params, match.underlineOnHoverOnly()); bool tooltip_required = !match.getTooltip().empty(); // set the tooltip for the Url label if (tooltip_required) { setLastSegmentToolTip(match.getTooltip()); } // show query part of url with gray color only for LLUrlEntryHTTP url entries std::string label = match.getQuery(); if (label.size()) { link_params.color = LLColor4::grey; link_params.readonly_color = LLColor4::grey; appendAndHighlightTextImpl(label, part, link_params, match.underlineOnHoverOnly()); // set the tooltip for the query part of url if (tooltip_required) { setLastSegmentToolTip(match.getTooltip()); } } // move on to the rest of the text after the Url if (end < (S32)text.length()) { text = text.substr(end,text.length() - end); end=0; part=(S32)LLTextParser::END; } else { break; } } if (part != (S32)LLTextParser::WHOLE) part=(S32)LLTextParser::END; if (end < (S32)text.length()) appendAndHighlightText(text, part, style_params); } else { appendAndHighlightText(new_text, part, style_params); } } void LLTextBase::setLastSegmentToolTip(const std::string &tooltip) { segment_set_t::iterator it = getSegIterContaining(getLength()-1); if (it != mSegments.end()) { LLTextSegmentPtr segment = *it; segment->setToolTip(tooltip); } } void LLTextBase::appendText(const std::string &new_text, bool prepend_newline, const LLStyle::Params& input_params) { LL_PROFILE_ZONE_SCOPED_CATEGORY_UI; 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, static_cast(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) { LL_DEBUGS() << "reflow on object " << (void*)this << " index = " << mReflowIndex << ", new index = " << index << LL_ENDL; mReflowIndex = llmin(mReflowIndex, index); } S32 LLTextBase::removeFirstLine() { if (!mLineInfoList.empty()) { S32 length = getLineEnd(0); deselect(); removeStringNoUndo(0, length); return length; } return 0; } void LLTextBase::appendLineBreakSegment(const LLStyle::Params& style_params) { 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() + static_cast(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; bool was_selecting = mIsSelecting; S32 cursor_pos = mCursorPos; S32 old_length = getLength(); bool cursor_was_at_end = (mCursorPos == old_length); deselect(); setCursorPos(old_length); if (mParseHighlights) { LLStyle::Params highlight_params(style_params); auto pieces = LLTextParser::instance().parsePartialLineHighlights(new_text, highlight_params.color, (LLTextParser::EHighlightPosition)highlight_part); for (S32 i = 0; i < pieces.size(); i++) { const auto& piece_pair = pieces[i]; highlight_params.color = piece_pair.second; LLWString wide_text; wide_text = utf8str_to_wstring(piece_pair.first); S32 cur_length = getLength(); LLStyleConstSP sp(new LLStyle(highlight_params)); LLTextSegmentPtr segmentp; if (underline_on_hover_only || mSkipLinkUnderline) { highlight_params.font.style("NORMAL"); LLStyleConstSP normal_sp(new LLStyle(highlight_params)); segmentp = new LLOnHoverChangeableTextSegment(sp, normal_sp, cur_length, cur_length + static_cast(wide_text.size()), *this); } else { segmentp = new LLNormalTextSegment(sp, cur_length, cur_length + static_cast(wide_text.size()), *this); } segment_vec_t segments; segments.push_back(segmentp); insertStringNoUndo(cur_length, wide_text, &segments); } } else { LLWString wide_text; wide_text = utf8str_to_wstring(new_text); segment_vec_t segments; S32 segment_start = old_length; S32 segment_end = old_length + static_cast(wide_text.size()); LLStyleConstSP sp(new LLStyle(style_params)); if (underline_on_hover_only || mSkipLinkUnderline) { 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); } // Set the cursor and scroll position if (selection_start != selection_end) { mSelectionStart = selection_start; mSelectionEnd = selection_end; mIsSelecting = was_selecting; setCursorPos(cursor_pos); } else if (cursor_was_at_end) { setCursorPos(getLength()); } else { 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 != std::string::npos) { 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); } std::string str = std::string(new_text, start, new_text.length() - start); appendAndHighlightTextImpl(str, highlight_part, style_params, underline_on_hover_only); } 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(); 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; LLStyleConstSP 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->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 + static_cast(wlabel.size())); 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(); } // update the editor with the new (wide) text string if (modified) { getViewModel()->setDisplay(text); deselect(); setCursorPos(mCursorPos); needsReflow(); } } void LLTextBase::setWText(const LLWString& text) { setText(wstring_to_utf8str(text)); } const LLWString& LLTextBase::getWText() const { return getViewModel()->getDisplay(); } // If 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. 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 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(), doc_y, compare_bottom()); if (!mLineInfoList.size() || line_iter == mLineInfoList.end()) { return getLength(); // past the end } S32 pos = getLength(); F32 start_x = (F32)(line_iter->mRect.mLeft + doc_rect.mLeft); segment_set_t::iterator line_seg_iter; S32 line_seg_offset; for(getSegmentAndOffset(line_iter->mDocIndexStart, &line_seg_iter, &line_seg_offset); line_seg_iter != mSegments.end(); ++line_seg_iter, line_seg_offset = 0) { const LLTextSegmentPtr segmentp = *line_seg_iter; S32 segment_line_start = segmentp->getStart() + line_seg_offset; S32 segment_line_length = llmin(segmentp->getEnd(), line_iter->mDocIndexEnd) - segment_line_start; F32 text_width; S32 text_height; bool newline = segmentp->getDimensionsF32(line_seg_offset, segment_line_length, text_width, text_height); if(newline) { pos = segment_line_start + segmentp->getOffset(local_x - (S32)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 && 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 { // Figure out which character we're nearest to. S32 offset; if (!segmentp->canEdit()) { F32 segment_width; S32 segment_height; segmentp->getDimensionsF32(0, segmentp->getEnd() - segmentp->getStart(), segment_width, segment_height); if (round && local_x - start_x > segment_width / 2) { offset = segment_line_length; } else { offset = 0; } } else { offset = segmentp->getOffset(local_x - (S32)start_x, line_seg_offset, segment_line_length, round); } pos = segment_line_start + offset; break; } else if (hit_past_end_of_line && segmentp->getEnd() >= 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; } return pos; } // returns rectangle of insertion caret // in document coordinate frame from given index into text LLRect LLTextBase::getDocRectFromDocIndex(S32 pos) const { if (mLineInfoList.empty()) { return LLRect(); } // clamp pos to valid values pos = llclamp(pos, 0, mLineInfoList.back().mDocIndexEnd - 1); line_list_t::const_iterator line_iter = std::upper_bound(mLineInfoList.begin(), mLineInfoList.end(), pos, line_end_compare()); segment_set_t::iterator line_seg_iter; S32 line_seg_offset; segment_set_t::iterator cursor_seg_iter; S32 cursor_seg_offset; getSegmentAndOffset(line_iter->mDocIndexStart, &line_seg_iter, &line_seg_offset); getSegmentAndOffset(pos, &cursor_seg_iter, &cursor_seg_offset); F32 doc_left_precise = (F32)line_iter->mRect.mLeft; while(line_seg_iter != mSegments.end()) { const LLTextSegmentPtr segmentp = *line_seg_iter; if (line_seg_iter == cursor_seg_iter) { // cursor advanced to right based on difference in offset of cursor to start of line F32 segment_width; S32 segment_height; segmentp->getDimensionsF32(line_seg_offset, cursor_seg_offset - line_seg_offset, segment_width, segment_height); doc_left_precise += segment_width; break; } else { // add remainder of current text segment to cursor position F32 segment_width; S32 segment_height; segmentp->getDimensionsF32(line_seg_offset, (segmentp->getEnd() - segmentp->getStart()) - line_seg_offset, segment_width, segment_height); doc_left_precise += segment_width; // offset will be 0 for all segments after the first line_seg_offset = 0; // go to next text segment on this line ++line_seg_iter; } } LLRect doc_rect; doc_rect.mLeft = (S32)doc_left_precise; doc_rect.mBottom = line_iter->mRect.mBottom; doc_rect.mTop = line_iter->mRect.mTop; // set rect to 0 width doc_rect.mRight = doc_rect.mLeft; return doc_rect; } 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 = content_window_rect; local_rect.mBottom = local_rect.mTop - mFont->getLineHeight(); return local_rect; } // get the rect in document coordinates LLRect doc_rect = getDocRectFromDocIndex(pos); // compensate for scrolled, inset view of doc LLRect scrolled_view_rect = getVisibleDocumentRect(); local_rect = doc_rect; local_rect.translate(content_window_rect.mLeft - scrolled_view_rect.mLeft, content_window_rect.mBottom - scrolled_view_rect.mBottom); return local_rect; } void LLTextBase::updateCursorXPos() { // reset desired x cursor position mDesiredXPixel = getLocalRectFromDocIndex(mCursorPos).mLeft; } void LLTextBase::startOfLine() { S32 offset = getLineOffsetFromDocIndex(mCursorPos); setCursorPos(mCursorPos - offset); } void LLTextBase::endOfLine() { S32 line = getLineNumFromDocIndex(mCursorPos); S32 num_lines = getLineCount(); if (line + 1 >= num_lines) { setCursorPos(getLength()); } else { setCursorPos( getLineStart(line + 1) - 1 ); } } void LLTextBase::startOfDoc() { setCursorPos(0); if (mScroller) { mScroller->goToTop(); } } void LLTextBase::endOfDoc() { setCursorPos(getLength()); if (mScroller) { mScroller->goToBottom(); } } void LLTextBase::changePage( S32 delta ) { const S32 PIXEL_OVERLAP_ON_PAGE_CHANGE = 10; if (delta == 0 || !mScroller) return; LLRect cursor_rect = getLocalRectFromDocIndex(mCursorPos); if( delta == -1 ) { mScroller->pageUp(PIXEL_OVERLAP_ON_PAGE_CHANGE); } else if( delta == 1 ) { mScroller->pageDown(PIXEL_OVERLAP_ON_PAGE_CHANGE); } if (getLocalRectFromDocIndex(mCursorPos) == cursor_rect) { // cursor didn't change apparent position, so move to top or bottom of document, respectively if (delta < 0) { startOfDoc(); } else { endOfDoc(); } } else { setCursorAtLocalPos(cursor_rect.getCenterX(), cursor_rect.getCenterY(), true, false); } } // Picks a new cursor position based on the screen size of text being drawn. void LLTextBase::setCursorAtLocalPos( S32 local_x, S32 local_y, bool round, bool keep_cursor_offset ) { setCursorPos(getDocIndexFromLocalCoord(local_x, local_y, round), keep_cursor_offset); } 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); 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() { return mScroller->isAtTop(); } bool LLTextBase::scrolledToEnd() { return mScroller->isAtBottom(); } bool LLTextBase::setCursor(S32 row, S32 column) { if (row < 0 || column < 0) return false; S32 n_lines = static_cast(mLineInfoList.size()); for (S32 line = row; line < n_lines; ++line) { 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; // invalid row or column specified } bool LLTextBase::setCursorPos(S32 cursor_pos, bool keep_cursor_offset) { S32 new_cursor_pos = cursor_pos; if (new_cursor_pos != mCursorPos) { new_cursor_pos = getEditableIndex(new_cursor_pos, new_cursor_pos >= mCursorPos); } mCursorPos = llclamp(new_cursor_pos, 0, (S32)getLength()); needsScroll(); if (!keep_cursor_offset) updateCursorXPos(); // did we get requested position? return new_cursor_pos == cursor_pos; } // constraint cursor to editable segments of document S32 LLTextBase::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; if (segmentp->canEdit()) { return segmentp->getStart() + offset; } else if (segmentp->getStart() < index && index < segmentp->getEnd()) { // bias towards document end if (increasing_direction) { return segmentp->getEnd(); } // bias towards document start else { return segmentp->getStart(); } } else { return index; } } void LLTextBase::updateRects() { LLRect old_text_rect = mVisibleTextRect; mVisibleTextRect = mScroller ? mScroller->getContentWindowRect() : getLocalRect(); if (mLineInfoList.empty()) { mTextBoundingRect = LLRect(0, mVPad, mHPad, 0); } else { mTextBoundingRect = mLineInfoList.begin()->mRect; for (line_list_t::const_iterator line_iter = ++mLineInfoList.begin(); line_iter != mLineInfoList.end(); ++line_iter) { mTextBoundingRect.unionWith(line_iter->mRect); } mTextBoundingRect.mTop += mVPad; 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) { it->mRect.translate(0, delta_pos); } mTextBoundingRect.translate(0, delta_pos); } // update document container dimensions according to text contents 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 mVisibleTextRect = mScroller ? mScroller->getContentWindowRect() : getLocalRect(); //FIXME: replace border with image? if (mBorderVisible) { mVisibleTextRect.stretch(-1); } if (mVisibleTextRect != old_text_rect) { 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); } void LLTextBase::startSelection() { if( !mIsSelecting ) { mIsSelecting = true; mSelectionStart = mCursorPos; mSelectionEnd = mCursorPos; } } void LLTextBase::endSelection() { if( mIsSelecting ) { mIsSelecting = false; mSelectionEnd = mCursorPos; } } // get portion of document that is visible in text editor LLRect LLTextBase::getVisibleDocumentRect() const { if (mScroller) { return mScroller->getVisibleContentRect(); } else if (mClip) { 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 doc_rect.mBottom = doc_rect.getHeight() - mVisibleTextRect.getHeight(); return doc_rect; } } 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); } boost::signals2::connection LLTextBase::setIsObjectBlockedCallback(const is_blocked_signal_t::slot_type& cb) { if (!mIsObjectBlockedSignal) { mIsObjectBlockedSignal = new is_blocked_signal_t(); } return mIsObjectBlockedSignal->connect(cb); } // // LLTextSegment // LLTextSegment::~LLTextSegment() {} bool LLTextSegment::getDimensionsF32(S32 first_char, S32 num_chars, F32& width, S32& height) const { width = 0; height = 0; return false; } bool LLTextSegment::getDimensions(S32 first_char, S32 num_chars, S32& width, S32& height) const { F32 fwidth = 0; bool result = getDimensionsF32(first_char, num_chars, fwidth, height); width = ll_round(fwidth); return result; } 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, S32 line_ind) const { return 0; } void LLTextSegment::updateLayout(const LLTextBase& editor) {} F32 LLTextSegment::draw(S32 start, S32 end, S32 selection_start, S32 selection_end, const LLRectf& draw_rect) { return draw_rect.mLeft; } bool LLTextSegment::canEdit() const { return false; } void LLTextSegment::unlinkFromDocument(LLTextBase*) {} void LLTextSegment::linkToDocument(LLTextBase*) {} const LLUIColor& LLTextSegment::getColor() const { static const LLUIColor white = LLUIColorTable::instance().getColor("White", LLColor4::white); return white; } //void LLTextSegment::setColor(const LLUIColor &color) {} LLStyleConstSP LLTextSegment::getStyle() const {static LLStyleConstSP sp(new LLStyle()); return sp; } void LLTextSegment::setStyle(LLStyleConstSP style) {} void LLTextSegment::setToken( LLKeywordToken* token ) {} LLKeywordToken* LLTextSegment::getToken() const { return NULL; } void LLTextSegment::setToolTip( const std::string &msg ) {} void LLTextSegment::dump() const {} bool LLTextSegment::handleMouseDown(S32 x, S32 y, MASK mask) { return false; } bool LLTextSegment::handleMouseUp(S32 x, S32 y, MASK mask) { return false; } bool LLTextSegment::handleMiddleMouseDown(S32 x, S32 y, MASK mask) { return false; } bool LLTextSegment::handleMiddleMouseUp(S32 x, S32 y, MASK mask) { return false; } bool LLTextSegment::handleRightMouseDown(S32 x, S32 y, MASK mask) { return false; } bool LLTextSegment::handleRightMouseUp(S32 x, S32 y, MASK mask) { return false; } 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::handleScrollHWheel(S32 x, S32 y, S32 clicks) { return false; } bool LLTextSegment::handleToolTip(S32 x, S32 y, MASK mask) { return false; } 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 {} bool LLTextSegment::hasMouseCapture() { return false; } // // LLNormalTextSegment // LLNormalTextSegment::LLNormalTextSegment( LLStyleConstSP style, S32 start, S32 end, LLTextBase& editor ) : LLTextSegment(start, end), mStyle( style ), mToken(NULL), mEditor(editor) { mFontHeight = mStyle->getFont()->getLineHeight(); LLUIImagePtr image = mStyle->getImage(); if (image.notNull()) { mImageLoadedConnection = image->addLoadedCallback(boost::bind(&LLTextBase::needsReflow, &mEditor, start)); } } LLNormalTextSegment::LLNormalTextSegment( const LLUIColor& color, S32 start, S32 end, LLTextBase& editor, bool is_visible) : LLTextSegment(start, end), mToken(NULL), mEditor(editor) { mStyle = new LLStyle(LLStyle::Params().visible(is_visible).color(color)); mFontHeight = mStyle->getFont()->getLineHeight(); } LLNormalTextSegment::~LLNormalTextSegment() { mImageLoadedConnection.disconnect(); } F32 LLNormalTextSegment::draw(S32 start, S32 end, S32 selection_start, S32 selection_end, const LLRectf& draw_rect) { if( end - start > 0 ) { return drawClippedSegment( getStart() + start, getStart() + end, selection_start, selection_end, draw_rect); } 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, LLRectf rect) { F32 alpha = LLViewDrawContext::getCurrentContext().mAlpha; const LLWString &text = getWText(); F32 right_x = rect.mLeft; if (!mStyle->isVisible()) { return right_x; } const LLFontGL* font = mStyle->getFont(); LLColor4 color = (mEditor.getReadOnly() ? mStyle->getReadOnlyColor() : mStyle->getColor()) % (alpha * mStyle->getAlpha()); 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, color, LLFontGL::LEFT, mEditor.mTextVAlign, LLFontGL::NORMAL, mStyle->getShadowType(), length, &right_x, mEditor.getUseEllipses(), mEditor.getUseColor()); } rect.mLeft = 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, rect, mStyle->getSelectedColor().get(), LLFontGL::LEFT, mEditor.mTextVAlign, LLFontGL::NORMAL, LLFontGL::NO_SHADOW, length, &right_x, mEditor.getUseEllipses(), mEditor.getUseColor()); } rect.mLeft = 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, rect, color, LLFontGL::LEFT, mEditor.mTextVAlign, LLFontGL::NORMAL, mStyle->getShadowType(), length, &right_x, mEditor.getUseEllipses(), mEditor.getUseColor()); } return right_x; } bool LLNormalTextSegment::handleHover(S32 x, S32 y, MASK mask) { if (getStyle() && getStyle()->isLink()) { // 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) { LLUI::getInstance()->getWindow()->setCursor(UI_CURSOR_HAND); return true; } } return false; } bool LLNormalTextSegment::handleRightMouseDown(S32 x, S32 y, MASK mask) { if (getStyle() && getStyle()->isLink()) { // 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) { mEditor.createUrlContextMenu(x, y, getStyle()->getLinkHREF()); return true; } } return false; } bool LLNormalTextSegment::handleMouseDown(S32 x, S32 y, MASK mask) { if (getStyle() && getStyle()->isLink()) { // 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) { // eat mouse down event on hyperlinks, so we get the mouse up return true; } } return false; } bool LLNormalTextSegment::handleMouseUp(S32 x, S32 y, MASK mask) { if (getStyle() && getStyle()->isLink()) { // 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) { std::string url = getStyle()->getLinkHREF(); if (!mEditor.mForceUrlsExternal) { LLUrlAction::clickAction(url, mEditor.isContentTrusted()); } else if (!LLUrlAction::executeSLURL(url, mEditor.isContentTrusted())) { LLUrlAction::openURLExternal(url); } return true; } } return false; } bool LLNormalTextSegment::handleToolTip(S32 x, S32 y, MASK mask) { std::string msg; // do we have a tooltip for a loaded keyword (for script editor)? if (mToken && !mToken->getToolTip().empty()) { const LLWString& wmsg = mToken->getToolTip(); LLToolTipMgr::instance().show(wstring_to_utf8str(wmsg), (mToken->getType() == LLKeywordToken::TT_FUNCTION)); return true; } // or do we have an explicitly set tooltip (e.g., for Urls) if (!mTooltip.empty()) { LLToolTipMgr::instance().show(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) { LL_WARNS() << "LLTextSegment::setToolTip: cannot replace keyword tooltip." << LL_ENDL; return; } mTooltip = tooltip; } bool LLNormalTextSegment::getDimensionsF32(S32 first_char, S32 num_chars, F32& width, S32& height) const { height = 0; width = 0; if (num_chars > 0) { height = mFontHeight; const LLWString &text = getWText(); // if last character is a newline, then return true, forcing line break width = mStyle->getFont()->getWidthF32(text.c_str(), mStart + first_char, num_chars, true); } return false; } S32 LLNormalTextSegment::getOffset(S32 segment_local_x_coord, S32 start_offset, S32 num_chars, bool round) const { const LLWString &text = 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, S32 line_ind) const { const LLWString &text = getWText(); LLUIImagePtr image = mStyle->getImage(); if( image.notNull()) { num_pixels = llmax(0, num_pixels - image->getWidth()); } 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)); // if no character yet displayed on this line, don't require word wrapping since // we can just move to the next line, otherwise insist on it so we make forward progress LLFontGL::EWordWrapStyle word_wrap_style = (line_offset == 0) ? LLFontGL::WORD_BOUNDARY_IF_POSSIBLE : LLFontGL::ONLY_WORD_BOUNDARIES; S32 offsetLength = static_cast(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); 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; } // include *either* the EOF or newline character in this run of text // but not both 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 >= getLength())) { num_chars++; } return num_chars; } void LLNormalTextSegment::dump() const { LL_INFOS() << "Segment [" << // mColor.mV[VRED] << ", " << // mColor.mV[VGREEN] << ", " << // mColor.mV[VBLUE] << "]\t[" << mStart << ", " << getEnd() << "]" << 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 LLUIColor& 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 static_cast(mEditor.getWlabel().length()); } // // LLEmojiTextSegment // LLEmojiTextSegment::LLEmojiTextSegment(LLStyleConstSP style, S32 start, S32 end, LLTextBase& editor) : LLNormalTextSegment(style, start, end, editor) { } LLEmojiTextSegment::LLEmojiTextSegment(const LLUIColor& color, S32 start, S32 end, LLTextBase& editor, bool is_visible) : LLNormalTextSegment(color, start, end, editor, is_visible) { } bool LLEmojiTextSegment::handleToolTip(S32 x, S32 y, MASK mask) { if (mTooltip.empty()) { LLWString emoji = getWText().substr(getStart(), getEnd() - getStart()); if (!emoji.empty()) { mTooltip = LLEmojiHelper::instance().getToolTip(emoji[0]); } } return LLNormalTextSegment::handleToolTip(x, y, mask); } // // 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 LLRectf& 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 = mEditor.getSkipLinkUnderline() ? mNormalStyle : mHoveredStyle; return LLNormalTextSegment::handleHover(x, y, mask); } // // LLInlineViewSegment // LLInlineViewSegment::LLInlineViewSegment(const Params& p, S32 start, S32 end) : LLTextSegment(start, end), mView(p.view), mForceNewLine(p.force_newline), mLeftPad(p.left_pad), mRightPad(p.right_pad), mTopPad(p.top_pad), mBottomPad(p.bottom_pad) { } LLInlineViewSegment::~LLInlineViewSegment() { mView->die(); } bool LLInlineViewSegment::getDimensionsF32(S32 first_char, S32 num_chars, F32& width, S32& height) const { if (first_char == 0 && num_chars == 0) { // We didn't fit on a line or were forced to new string // the widget will fall on the next line, so width here is 0 width = 0; if (mForceNewLine) { // Chat, string can't be smaller then font height even if it is empty LLStyleSP s(new LLStyle(LLStyle::Params().visible(true))); height = s->getFont()->getLineHeight(); return true; // new line } else { // height from previous segment in same string will be used, word-wrap height = 0; } } else { width = (F32)(mLeftPad + mRightPad + mView->getRect().getWidth()); height = mBottomPad + mTopPad + mView->getRect().getHeight(); } return false; } S32 LLInlineViewSegment::getNumChars(S32 num_pixels, S32 segment_offset, S32 line_offset, S32 max_chars, S32 line_ind) const { // if putting a widget anywhere but at the beginning of a line // and the widget doesn't fit or mForceNewLine is true // then return 0 chars for that line, and all characters for the next if (mForceNewLine && line_ind == 0) { return 0; } else if (line_offset != 0 && num_pixels < mView->getRect().getWidth()) { return 0; } else { return mEnd - mStart; } } void LLInlineViewSegment::updateLayout(const LLTextBase& editor) { LLRect start_rect = editor.getDocRectFromDocIndex(mStart); mView->setOrigin(start_rect.mLeft + mLeftPad, start_rect.mBottom + mBottomPad); } F32 LLInlineViewSegment::draw(S32 start, S32 end, S32 selection_start, S32 selection_end, const LLRectf& draw_rect) { // return padded width of widget // widget is actually drawn during mDocumentView's draw() return (F32)(draw_rect.mLeft + mView->getRect().getWidth() + mLeftPad + mRightPad); } void LLInlineViewSegment::unlinkFromDocument(LLTextBase* editor) { editor->removeDocumentChild(mView); } 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::getDimensionsF32(S32 first_char, S32 num_chars, F32& 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, S32 line_ind) const { return 1; } F32 LLLineBreakTextSegment::draw(S32 start, S32 end, S32 selection_start, S32 selection_end, const LLRectf& 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::getDimensionsF32(S32 first_char, S32 num_chars, F32& 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, S32 line_ind) 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; } bool LLImageTextSegment::handleToolTip(S32 x, S32 y, MASK mask) { if (!mTooltip.empty()) { LLToolTipMgr::instance().show(mTooltip); return true; } return false; } void LLImageTextSegment::setToolTip(const std::string& tooltip) { mTooltip = tooltip; } F32 LLImageTextSegment::draw(S32 start, S32 end, S32 selection_start, S32 selection_end, const LLRectf& 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 = (S32)(draw_rect.mTop - (draw_rect.getHeight() / 2.f)); // Align image to center of draw rect S32 image_bottom = text_center - (style_image_height / 2); image->draw((S32)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; }