/** * @file lltexteditor.cpp * @brief LLTextEditor base class * * $LicenseInfo:firstyear=2001&license=viewergpl$ * * Copyright (c) 2001-2009, Linden Research, Inc. * * Second Life Viewer Source Code * The source code in this file ("Source Code") is provided by Linden Lab * to you under the terms of the GNU General Public License, version 2.0 * ("GPL"), unless you have obtained a separate licensing agreement * ("Other License"), formally executed by you and Linden Lab. Terms of * the GPL can be found in doc/GPL-license.txt in this distribution, or * online at http://secondlifegrid.net/programs/open_source/licensing/gplv2 * * There are special exceptions to the terms and conditions of the GPL as * it is applied to this Source Code. View the full text of the exception * in the file doc/FLOSS-exception.txt in this software distribution, or * online at * http://secondlifegrid.net/programs/open_source/licensing/flossexception * * By copying, modifying or distributing this software, you acknowledge * that you have read and understood your obligations described above, * and agree to abide by those obligations. * * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY, * COMPLETENESS OR PERFORMANCE. * $/LicenseInfo$ */ // Text editor widget to let users enter a a multi-line ASCII document. #include "linden_common.h" #include "lltexteditor.h" #include "llfontgl.h" #include "lllocalcliprect.h" #include "llrender.h" #include "llui.h" #include "lluictrlfactory.h" #include "lluiimage.h" #include "llurlaction.h" #include "llurlregistry.h" #include "llrect.h" #include "llfocusmgr.h" #include "lltimer.h" #include "llmath.h" #include "llclipboard.h" #include "llscrollbar.h" #include "llstl.h" #include "llstring.h" #include "llkeyboard.h" #include "llkeywords.h" #include "llundo.h" #include "llviewborder.h" #include "llfasttimer.h" #include "llcontrol.h" #include "llimagegl.h" #include "llwindow.h" #include "lltextparser.h" #include "lldir.h" #include #include "llmemberlistener.h" #include "llmenugl.h" #include "../newview/lgghunspell_wrapper.h" // // Globals // static LLRegisterWidget r("simple_text_editor"); BOOL gDebugTextEditorTips = FALSE; // // Constants // const S32 UI_TEXTEDITOR_BUFFER_BLOCK_SIZE = 512; const S32 UI_TEXTEDITOR_BORDER = 1; const S32 UI_TEXTEDITOR_H_PAD = 4; const S32 UI_TEXTEDITOR_V_PAD_TOP = 4; const S32 UI_TEXTEDITOR_LINE_NUMBER_MARGIN = 32; const S32 UI_TEXTEDITOR_LINE_NUMBER_DIGITS = 4; const F32 CURSOR_FLASH_DELAY = 1.0f; // in seconds const S32 CURSOR_THICKNESS = 2; const S32 SPACES_PER_TAB = 4; const F32 PREEDIT_MARKER_BRIGHTNESS = 0.4f; const S32 PREEDIT_MARKER_GAP = 1; const S32 PREEDIT_MARKER_POSITION = 2; const S32 PREEDIT_MARKER_THICKNESS = 1; const F32 PREEDIT_STANDOUT_BRIGHTNESS = 0.6f; const S32 PREEDIT_STANDOUT_GAP = 1; const S32 PREEDIT_STANDOUT_POSITION = 2; const S32 PREEDIT_STANDOUT_THICKNESS = 2; /////////////////////////////////////////////////////////////////// class LLTextEditor::LLTextCmdInsert : public LLTextEditor::LLTextCmd { public: LLTextCmdInsert(S32 pos, BOOL group_with_next, const LLWString &ws) : LLTextCmd(pos, group_with_next), mWString(ws) { } virtual ~LLTextCmdInsert() {} virtual BOOL execute( LLTextEditor* editor, S32* delta ) { *delta = insert(editor, getPosition(), mWString ); LLWStringUtil::truncate(mWString, *delta); //mWString = wstring_truncate(mWString, *delta); return (*delta != 0); } virtual S32 undo( LLTextEditor* editor ) { remove(editor, getPosition(), mWString.length() ); return getPosition(); } virtual S32 redo( LLTextEditor* editor ) { insert(editor, getPosition(), mWString ); return getPosition() + mWString.length(); } private: LLWString mWString; }; /////////////////////////////////////////////////////////////////// class LLTextEditor::LLTextCmdAddChar : public LLTextEditor::LLTextCmd { public: LLTextCmdAddChar( S32 pos, BOOL group_with_next, llwchar wc) : LLTextCmd(pos, group_with_next), mWString(1, wc), mBlockExtensions(FALSE) { } virtual void blockExtensions() { mBlockExtensions = TRUE; } virtual BOOL canExtend(S32 pos) const { return !mBlockExtensions && (pos == getPosition() + (S32)mWString.length()); } virtual BOOL execute( LLTextEditor* editor, S32* delta ) { *delta = insert(editor, getPosition(), mWString); LLWStringUtil::truncate(mWString, *delta); //mWString = wstring_truncate(mWString, *delta); return (*delta != 0); } virtual BOOL extendAndExecute( LLTextEditor* editor, S32 pos, llwchar wc, S32* delta ) { LLWString ws; ws += wc; *delta = insert(editor, pos, ws); if( *delta > 0 ) { mWString += wc; } return (*delta != 0); } virtual S32 undo( LLTextEditor* editor ) { remove(editor, getPosition(), mWString.length() ); return getPosition(); } virtual S32 redo( LLTextEditor* editor ) { insert(editor, getPosition(), mWString ); return getPosition() + mWString.length(); } private: LLWString mWString; BOOL mBlockExtensions; }; /////////////////////////////////////////////////////////////////// class LLTextEditor::LLTextCmdOverwriteChar : public LLTextEditor::LLTextCmd { public: LLTextCmdOverwriteChar( S32 pos, BOOL group_with_next, llwchar wc) : LLTextCmd(pos, group_with_next), mChar(wc), mOldChar(0) {} virtual BOOL execute( LLTextEditor* editor, S32* delta ) { mOldChar = editor->getWChar(getPosition()); overwrite(editor, getPosition(), mChar); *delta = 0; return TRUE; } virtual S32 undo( LLTextEditor* editor ) { overwrite(editor, getPosition(), mOldChar); return getPosition(); } virtual S32 redo( LLTextEditor* editor ) { overwrite(editor, getPosition(), mChar); return getPosition()+1; } private: llwchar mChar; llwchar mOldChar; }; /////////////////////////////////////////////////////////////////// class LLTextEditor::LLTextCmdRemove : public LLTextEditor::LLTextCmd { public: LLTextCmdRemove( S32 pos, BOOL group_with_next, S32 len ) : LLTextCmd(pos, group_with_next), mLen(len) { } virtual BOOL execute( LLTextEditor* editor, S32* delta ) { mWString = editor->getWSubString(getPosition(), mLen); *delta = remove(editor, getPosition(), mLen ); return (*delta != 0); } virtual S32 undo( LLTextEditor* editor ) { insert(editor, getPosition(), mWString ); return getPosition() + mWString.length(); } virtual S32 redo( LLTextEditor* editor ) { remove(editor, getPosition(), mLen ); return getPosition(); } private: LLWString mWString; S32 mLen; }; /////////////////////////////////////////////////////////////////// LLTextEditor::is_friend_signal_t* LLTextEditor::mIsFriendSignal = nullptr; LLTextEditor::is_blocked_signal_t* LLTextEditor::mIsObjectBlockedSignal = nullptr; LLTextEditor::LLTextEditor( const std::string& name, const LLRect& rect, S32 max_length, // In bytes const std::string &default_text, const LLFontGL* font, BOOL allow_embedded_items, bool parse_html) : LLUICtrl(name, rect, TRUE, NULL, FOLLOWS_TOP | FOLLOWS_LEFT), mTextIsUpToDate(TRUE), mMaxTextByteLength(max_length), mPopupMenuHandle(), mBaseDocIsPristine(TRUE), mPristineCmd(NULL), mLastCmd(NULL), mCursorPos(0), mIsSelecting(FALSE), mSelectionStart(0), mSelectionEnd(0), mScrolledToBottom(TRUE), mOnScrollEndCallback(NULL), mOnScrollEndData(NULL), mCursorColor(LLUI::sColorsGroup->getColor("TextCursorColor")), mFgColor(LLUI::sColorsGroup->getColor("TextFgColor")), mDefaultColor(LLUI::sColorsGroup->getColor("TextDefaultColor")), mReadOnlyFgColor(LLUI::sColorsGroup->getColor("TextFgReadOnlyColor")), mWriteableBgColor(LLUI::sColorsGroup->getColor("TextBgWriteableColor")), mReadOnlyBgColor(LLUI::sColorsGroup->getColor("TextBgReadOnlyColor")), mFocusBgColor(LLUI::sColorsGroup->getColor("TextBgFocusColor")), mLinkColor(nullptr), mReadOnly(parse_html), mWordWrap(FALSE), mShowLineNumbers(FALSE), mTabsToNextField(TRUE), mCommitOnFocusLost(FALSE), mHideScrollbarForShortDocs(FALSE), mTakesNonScrollClicks(TRUE), mTrackBottom(FALSE), mAllowEmbeddedItems(allow_embedded_items), mAcceptCallingCardNames(FALSE), mHandleEditKeysDirectly(FALSE), mMouseDownX(0), mMouseDownY(0), mLastSelectionX(-1), mLastSelectionY(-1), mLastContextMenuX(-1), mLastContextMenuY(-1), mReflowNeeded(FALSE), mScrollNeeded(FALSE), mSpellCheckable(FALSE) { mSourceID.generate(); // reset desired x cursor position mDesiredXPixel = -1; if (font) { mGLFont = font; } else { mGLFont = LLFontGL::getFontSansSerif(); } updateTextRect(); S32 line_height = ll_round(mGLFont->getLineHeight()); S32 page_size = mTextRect.getHeight() / line_height; // Init the scrollbar LLRect scroll_rect; scroll_rect.setOriginAndSize( getRect().getWidth() - SCROLLBAR_SIZE, 1, SCROLLBAR_SIZE, getRect().getHeight() - 1); S32 lines_in_doc = getLineCount(); mScrollbar = new LLScrollbar(std::string("Scrollbar"), scroll_rect, LLScrollbar::VERTICAL, lines_in_doc, 0, page_size, NULL); mScrollbar->setFollowsRight(); mScrollbar->setFollowsTop(); mScrollbar->setFollowsBottom(); mScrollbar->setEnabled(TRUE); mScrollbar->setVisible(TRUE); mScrollbar->setOnScrollEndCallback(mOnScrollEndCallback, mOnScrollEndData); addChild(mScrollbar); mBorder = new LLViewBorder(std::string("text ed border"), LLRect(0, getRect().getHeight(), getRect().getWidth(), 0), LLViewBorder::BEVEL_IN, LLViewBorder::STYLE_LINE, UI_TEXTEDITOR_BORDER); addChild(mBorder); mParseHTML = parse_html; appendText(default_text, FALSE, FALSE); resetDirty(); // Update saved text state setCommitCallback(boost::bind(&LLTextEditor::setControlValue, this, _2)); } LLTextEditor::~LLTextEditor() { gFocusMgr.releaseFocusIfNeeded( this ); // calls onCommit() if (mLinkColor) { delete mLinkColor; mLinkColor = nullptr; } // Scrollbar is deleted by LLView std::for_each(mUndoStack.begin(), mUndoStack.end(), DeletePointer()); mSegments.clear(); auto menu = mPopupMenuHandle.get(); if (menu) { menu->die(); mPopupMenuHandle.markDead(); } /* Singu Note: Static this, we'll use it wherever we can! delete mIsFriendSignal; delete mIsObjectBlockedSignal;*/ } const std::string& LLTextEditor::getMenuSegmentUrl() const { auto segment = getSegmentAtLocalPos(mLastContextMenuX, mLastContextMenuY); auto style = segment->getStyle(); return style ? style->getLinkHREF() : LLStringUtil::null; } static LLTextEditor* get_focused_text_editor() { auto* list = dynamic_cast(gFocusMgr.getKeyboardFocus()); llassert(list); // This listener only applies to lists return list; } class ContextText : public LLMemberListener { bool handleEvent(LLPointer, const LLSD& userdata) override { auto text = get_focused_text_editor(); const auto& op = userdata.asStringRef(); if (op == "Cut") text->cut(); else if (op == "Copy") text->copy(); else if (op == "CopyRaw") text->copyRaw(); else if (op == "Paste") text->paste(); else if (op == "Delete") text->doDelete(); else if (op == "SelectAll") text->selectAll(); return true; } }; static const std::string& get_focused_url() { return get_focused_text_editor()->getMenuSegmentUrl(); } class ContextUrl : public LLMemberListener { bool handleEvent(LLPointer, const LLSD& userdata) override { const auto& url = get_focused_url(); const auto& op = userdata.asStringRef(); if (op == "Open") LLUrlAction::openURL(url); else if (op == "OpenInternal") LLUrlAction::openURLInternal(url); else if (op == "OpenExternal") LLUrlAction::openURLExternal(url); else if (op == "Execute") LLUrlAction::executeSLURL(url, true); else if (op == "Block") LLUrlAction::blockObject(url); else if (op == "Unblock") LLUrlAction::unblockObject(url); else if (op == "Teleport") LLUrlAction::teleportToLocation(url); else if (op == "ShowProfile") LLUrlAction::showProfile(url); else if (op == "AddFriend") LLUrlAction::addFriend(url); else if (op == "RemoveFriend") LLUrlAction::removeFriend(url); else if (op == "SendIM") LLUrlAction::sendIM(url); else if (op == "ShowOnMap") LLUrlAction::showLocationOnMap(url); else if (op == "CopyLabel") LLUrlAction::copyLabelToClipboard(url); else if (op == "CopyUrl") LLUrlAction::copyURLToClipboard(url); return true; } }; class ContextUrlCopy : public LLMemberListener { bool handleEvent(LLPointer, const LLSD& userdata) override { const auto& url = get_focused_url(); const auto& type = userdata.asStringRef(); // Empty works like avatar and group, "object" is an object (you needed to be told this) const auto& id = type.empty() ? LLUrlAction::getUserID(url) : LLUrlAction::getObjectId(url); LLView::getWindow()->copyTextToClipboard(utf8str_to_wstring(id)); return true; } }; void LLTextEditor::spell_correct(void* data) { SpellMenuBind* tempBind = (SpellMenuBind*)data; LLTextEditor* line = tempBind->origin; if(tempBind && line) { LL_INFOS() << tempBind->menuItem->getName() << " : " << tempBind->origin->getName() << " : " << tempBind->word << LL_ENDL; if(line)line->spellReplace(tempBind); } } void LLTextEditor::spell_show(void * data) { SpellMenuBind* tempBind = (SpellMenuBind*)data; LLTextEditor* line = tempBind->origin; if(tempBind && line) { BOOL show = (tempBind->word == "Show Misspellings"); glggHunSpell->setSpellCheckHighlight(show); } } std::vector LLTextEditor::getMisspelledWordsPositions() { resetSpellDirty(); std::vector thePosesOfBadWords; LLWString& text = mWText; S32 wordStart=0; S32 wordEnd=spellStart;//start at the scroll start while(wordEnd < spellEnd) { //go through all the chars... XD if( LLTextEditor::isPartOfWord( text[wordEnd] ) ) { // Select word the cursor is over while ((wordEnd > 0) && LLTextEditor::isPartOfWord(text[wordEnd-1])) { wordEnd--; } wordStart=wordEnd; while ((wordEnd < (S32)text.length()) && LLTextEditor::isPartOfWord( text[wordEnd] ) ) { wordEnd++; } //got a word :D std::string regText(text.begin(),text.end()); std::string selectedWord(regText.substr(wordStart,wordEnd-wordStart)); if(!glggHunSpell->isSpelledRight(selectedWord)) { //misspelled word here, and you have just right clicked on it thePosesOfBadWords.push_back(wordStart); thePosesOfBadWords.push_back(wordEnd); } } wordEnd++; } return thePosesOfBadWords; } void LLTextEditor::spell_add(void* data) { SpellMenuBind* tempBind = (SpellMenuBind*)data; if(tempBind) { glggHunSpell->addWordToCustomDictionary(tempBind->word); tempBind->origin->mPrevSpelledText.erase();//make it update } } //static void LLTextEditor::addMenuListeners() { (new ContextText)->registerListener(LLMenuGL::sMenuContainer, "Text"); (new ContextUrl)->registerListener(LLMenuGL::sMenuContainer, "Text.Url"); (new ContextUrlCopy)->registerListener(LLMenuGL::sMenuContainer, "Text.Url.CopyUUID"); } void LLTextEditor::setTrackColor( const LLColor4& color ) { mScrollbar->setTrackColor(color); } void LLTextEditor::setThumbColor( const LLColor4& color ) { mScrollbar->setThumbColor(color); } void LLTextEditor::setHighlightColor( const LLColor4& color ) { mScrollbar->setHighlightColor(color); } void LLTextEditor::setShadowColor( const LLColor4& color ) { mScrollbar->setShadowColor(color); } void LLTextEditor::updateLineStartList(S32 startpos) { updateSegments(); bindEmbeddedChars(mGLFont); S32 seg_num = mSegments.size(); S32 seg_idx = 0; S32 seg_offset = 0; if (!mLineStartList.empty()) { getSegmentAndOffset(startpos, &seg_idx, &seg_offset); line_info t(seg_idx, seg_offset); line_list_t::iterator iter = std::upper_bound(mLineStartList.begin(), mLineStartList.end(), t, line_info_compare()); if (iter != mLineStartList.begin()) --iter; seg_idx = iter->mSegment; seg_offset = iter->mOffset; mLineStartList.erase(iter, mLineStartList.end()); } while( seg_idx < seg_num ) { mLineStartList.push_back(line_info(seg_idx,seg_offset)); BOOL line_ended = FALSE; S32 start_x = mShowLineNumbers ? UI_TEXTEDITOR_LINE_NUMBER_MARGIN : 0; S32 line_width = start_x; while(!line_ended && seg_idx < seg_num) { LLTextSegment* segment = mSegments[seg_idx]; S32 start_idx = segment->getStart() + seg_offset; S32 end_idx = start_idx; while (end_idx < segment->getEnd() && mWText[end_idx] != '\n') { end_idx++; } if (start_idx == end_idx) { if (end_idx >= segment->getEnd()) { // empty segment seg_idx++; seg_offset = 0; } else { // empty line line_ended = TRUE; seg_offset++; } } else { //Scratch buffer. Avoid needless realloc. static LLWString buf; if(start_idx) { buf.resize(end_idx - start_idx); std::copy(mWText.begin() + start_idx, mWText.begin() + end_idx, buf.begin()); } const LLWString& str = start_idx ? buf : mWText; S32 drawn = mGLFont->maxDrawableChars(str, (F32)abs(mTextRect.getWidth()) - line_width, end_idx - start_idx, mWordWrap ? LLFontGL::WORD_BOUNDARY_IF_POSSIBLE : LLFontGL::ANYWHERE, mAllowEmbeddedItems ); if( 0 == drawn && line_width == start_x) { // If at the beginning of a line, draw at least one character, even if it doesn't all fit. drawn = 1; } seg_offset += drawn; line_width += mGLFont->getWidth(str, 0, drawn, mAllowEmbeddedItems); end_idx = segment->getStart() + seg_offset; if (end_idx < segment->getEnd()) { line_ended = TRUE; if (mWText[end_idx] == '\n') { seg_offset++; // skip newline } } else { // finished with segment seg_idx++; seg_offset = 0; } } } } unbindEmbeddedChars(mGLFont); mScrollbar->setDocSize( getLineCount() ); if (mHideScrollbarForShortDocs) { BOOL short_doc = (mScrollbar->getDocSize() <= mScrollbar->getPageSize()); mScrollbar->setVisible(!short_doc); } // if scrolled to bottom, stay at bottom // unless user is selecting text // do this after updating page size if (mScrolledToBottom && mTrackBottom && !hasMouseCapture()) { endOfDoc(); } } //////////////////////////////////////////////////////////// // LLTextEditor // Public methods BOOL LLTextEditor::truncate() { BOOL did_truncate = FALSE; // First rough check - if we're less than 1/4th the size, we're OK if (mWText.size() >= (size_t) (mMaxTextByteLength / 4)) { // Have to check actual byte size S32 utf8_byte_size = wstring_utf8_length( mWText ); if ( utf8_byte_size > mMaxTextByteLength ) { // Truncate safely in UTF-8 std::string temp_utf8_text = wstring_to_utf8str( mWText ); temp_utf8_text = utf8str_truncate( temp_utf8_text, mMaxTextByteLength ); mWText = utf8str_to_wstring( temp_utf8_text ); mTextIsUpToDate = FALSE; did_truncate = TRUE; } } return did_truncate; } LLMenuGL* LLTextEditor::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 nullptr; } std::string xui_file = match.getMenuName(); if (xui_file.empty()) { return nullptr; } // create and return the context menu from the XUI file llassert(LLMenuGL::sMenuContainer != NULL); auto menu = LLUICtrlFactory::getInstance()->buildMenu(xui_file, LLMenuGL::sMenuContainer); if (menu) { if (mIsFriendSignal) { bool isFriend = *(*mIsFriendSignal)(LLUUID(LLUrlAction::getUserID(url))); LLView* addFriendButton = menu->findChild("add_friend"); LLView* removeFriendButton = menu->findChild("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->findChild("block_object"); LLView* unblockButton = menu->findChild("unblock_object"); if (blockButton && unblockButton) { blockButton->setVisible(!is_blocked); unblockButton->setVisible(is_blocked); } } } return menu; } void LLTextEditor::setText(const LLStringExplicit &utf8str) { // clear out the existing text and segments mWText.clear(); clearSegments(); // createDefaultSegment(); deselect(); // append the new text (supports Url linking) std::string text(utf8str); //LLStringUtil::removeCRLF(text); // appendText modifies mCursorPos... appendText(utf8str, false, false); // ...so move cursor to top after appending text setCursorPos(0); resetDirty(); mUTF8Text = text; mTextIsUpToDate = true; } void LLTextEditor::setWText(const LLWString& text) { setText(wstring_to_utf8str(text)); } // virtual void LLTextEditor::setValue(const LLSD& value) { setText(value.asString()); } const std::string& LLTextEditor::getText() const { if (!mTextIsUpToDate) { if (mAllowEmbeddedItems) { LL_WARNS() << "getText() called on text with embedded items (not supported)" << LL_ENDL; } mUTF8Text = wstring_to_utf8str(mWText); mTextIsUpToDate = TRUE; } return mUTF8Text; } // virtual LLSD LLTextEditor::getValue() const { return LLSD(getText()); } void LLTextEditor::setWordWrap(BOOL b) { mWordWrap = b; setCursorPos(0); deselect(); needsReflow(); } void LLTextEditor::setBorderVisible(BOOL b) { mBorder->setVisible(b); } BOOL LLTextEditor::isBorderVisible() const { return mBorder->getVisible(); } void LLTextEditor::setHideScrollbarForShortDocs(BOOL b) { mHideScrollbarForShortDocs = b; if (mHideScrollbarForShortDocs) { BOOL short_doc = (mScrollbar->getDocSize() <= mScrollbar->getPageSize()); mScrollbar->setVisible(!short_doc); } } void LLTextEditor::selectNext(const std::string& search_text_in, BOOL case_insensitive, BOOL wrap) { if (search_text_in.empty()) { return; } LLWString text = getWText(); LLWString search_text = utf8str_to_wstring(search_text_in); if (case_insensitive) { LLWStringUtil::toLower(text); LLWStringUtil::toLower(search_text); } if (mIsSelecting) { LLWString selected_text = text.substr(mSelectionEnd, mSelectionStart - mSelectionEnd); if (selected_text == search_text) { // We already have this word selected, we are searching for the next. mCursorPos += search_text.size(); } } S32 loc = text.find(search_text,mCursorPos); // If Maybe we wrapped, search again if (wrap && (-1 == loc)) { loc = text.find(search_text); } // If still -1, then search_text just isn't found. if (-1 == loc) { mIsSelecting = FALSE; mSelectionEnd = 0; mSelectionStart = 0; return; } setCursorPos(loc); scrollToPos(mCursorPos); mIsSelecting = TRUE; mSelectionEnd = mCursorPos; mSelectionStart = llmin((S32)getLength(), (S32)(mCursorPos + search_text.size())); } BOOL LLTextEditor::replaceText(const std::string& search_text_in, const std::string& replace_text, BOOL case_insensitive, BOOL wrap) { BOOL replaced = FALSE; if (search_text_in.empty()) { return replaced; } LLWString search_text = utf8str_to_wstring(search_text_in); if (mIsSelecting) { LLWString text = getWText(); LLWString selected_text = text.substr(mSelectionEnd, mSelectionStart - mSelectionEnd); if (case_insensitive) { LLWStringUtil::toLower(selected_text); LLWStringUtil::toLower(search_text); } if (selected_text == search_text) { insertText(replace_text); replaced = TRUE; } } selectNext(search_text_in, case_insensitive, wrap); return replaced; } void LLTextEditor::replaceTextAll(const std::string& search_text, const std::string& replace_text, BOOL case_insensitive) { S32 cur_pos = mScrollbar->getDocPos(); setCursorPos(0); selectNext(search_text, case_insensitive, FALSE); BOOL replaced = TRUE; while ( replaced ) { replaced = replaceText(search_text,replace_text, case_insensitive, FALSE); } mScrollbar->setDocPos(cur_pos); } // Picks a new cursor position based on the screen size of text being drawn. void LLTextEditor::setCursorAtLocalPos( S32 local_x, S32 local_y, BOOL round ) { setCursorPos(getCursorPosFromLocalCoord(local_x, local_y, round)); } S32 LLTextEditor::prevWordPos(S32 cursorPos) const { const LLWString& wtext = mWText; while( (cursorPos > 0) && (wtext[cursorPos-1] == ' ') ) { cursorPos--; } while( (cursorPos > 0) && isPartOfWord( wtext[cursorPos-1] ) ) { cursorPos--; } return cursorPos; } S32 LLTextEditor::nextWordPos(S32 cursorPos) const { const LLWString& wtext = mWText; while( (cursorPos < getLength()) && isPartOfWord( wtext[cursorPos] ) ) { cursorPos++; } while( (cursorPos < getLength()) && (wtext[cursorPos] == ' ') ) { cursorPos++; } return cursorPos; } BOOL LLTextEditor::getWordBoundriesAt(const S32 at, S32* word_begin, S32* word_length) const { S32 pos = at; if (isPartOfWord(mWText[pos])) { while ( (pos > 0) && isPartOfWord(mWText[pos - 1]) ) { pos--; } *word_begin = pos; while ( (pos < getLength()) && isPartOfWord(mWText[pos]) ) { pos++; } *word_length = pos - *word_begin; return TRUE; } return FALSE; } S32 LLTextEditor::getLineStart( S32 line ) const { S32 num_lines = getLineCount(); if (num_lines == 0) { return 0; } line = llclamp(line, 0, num_lines-1); S32 segidx = mLineStartList[line].mSegment; S32 segoffset = mLineStartList[line].mOffset; LLTextSegment* seg = mSegments[segidx]; S32 res = seg->getStart() + segoffset; if (res > seg->getEnd()) { //LL_ERRS() << "wtf" << LL_ENDL; // This happens when creating a new notecard using the AO on certain opensims. // Play it safe instead of bringing down the viewer - MC LL_WARNS() << "BAD JOOJOO! Text length (" << res << ") greater than text end (" << seg->getEnd() << "). Setting line start to " << seg->getEnd() << LL_ENDL; res = seg->getEnd(); } return res; } // Given an offset into text (pos), find the corresponding line (from the start of the doc) and an offset into the line. void LLTextEditor::getLineAndOffset( S32 startpos, S32* linep, S32* offsetp ) const { if (mLineStartList.empty()) { *linep = 0; *offsetp = startpos; } else { S32 seg_idx, seg_offset; getSegmentAndOffset( startpos, &seg_idx, &seg_offset ); line_info tline(seg_idx, seg_offset); line_list_t::const_iterator iter = std::upper_bound(mLineStartList.begin(), mLineStartList.end(), tline, line_info_compare()); if (iter != mLineStartList.begin()) --iter; *linep = iter - mLineStartList.begin(); S32 line_start = mSegments[iter->mSegment]->getStart() + iter->mOffset; *offsetp = startpos - line_start; } } void LLTextEditor::getSegmentAndOffset( S32 startpos, S32* segidxp, S32* offsetp ) const { if (mSegments.empty()) { *segidxp = -1; *offsetp = startpos; } LLTextSegment tseg(startpos); segment_list_t::const_iterator seg_iter; seg_iter = std::upper_bound(mSegments.begin(), mSegments.end(), &tseg, LLTextSegment::compare()); if (seg_iter != mSegments.begin()) --seg_iter; *segidxp = seg_iter - mSegments.begin(); *offsetp = startpos - (*seg_iter)->getStart(); } const LLTextSegment* LLTextEditor::getPreviousSegment() const { // find segment index at character to left of cursor (or rightmost edge of selection) S32 idx = llmax(0, getSegmentIdxAtOffset(mCursorPos) - 1); return idx >= 0 ? mSegments[idx] : LLTextSegmentPtr(); } void LLTextEditor::getSelectedSegments(std::vector& segments) const { S32 left = hasSelection() ? llmin(mSelectionStart, mSelectionEnd) : mCursorPos; S32 right = hasSelection() ? llmax(mSelectionStart, mSelectionEnd) : mCursorPos; S32 first_idx = llmax(0, getSegmentIdxAtOffset(left)); S32 last_idx = llmax(0, first_idx, getSegmentIdxAtOffset(right)); for (S32 idx = first_idx; idx <= last_idx; ++idx) { segments.push_back(mSegments[idx]); } } S32 LLTextEditor::getCursorPosFromLocalCoord( S32 local_x, S32 local_y, BOOL round ) const { if(mShowLineNumbers) { local_x -= UI_TEXTEDITOR_LINE_NUMBER_MARGIN; } // 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. // Figure out which line we're nearest to. S32 total_lines = getLineCount(); S32 line_height = ll_round( mGLFont->getLineHeight() ); S32 max_visible_lines = mTextRect.getHeight() / line_height; S32 scroll_lines = mScrollbar->getDocPos(); S32 visible_lines = llmin( total_lines - scroll_lines, max_visible_lines ); // Lines currently visible //S32 line = S32( 0.5f + ((mTextRect.mTop - local_y) / mGLFont->getLineHeight()) ); S32 line = (mTextRect.mTop - 1 - local_y) / line_height; if (line >= total_lines) { return getLength(); // past the end } line = llclamp( line, 0, visible_lines ) + scroll_lines; S32 line_start = getLineStart(line); S32 next_start = getLineStart(line+1); S32 line_end = (next_start != line_start) ? next_start - 1 : getLength(); if(line_start == -1) { return 0; } else { S32 line_len = line_end - line_start; S32 pos; if (mAllowEmbeddedItems) { // Figure out which character we're nearest to. bindEmbeddedChars(mGLFont); pos = mGLFont->charFromPixelOffset(mWText.c_str(), line_start, (F32)(local_x - mTextRect.mLeft), (F32)(mTextRect.getWidth()), line_len, round, TRUE); unbindEmbeddedChars(mGLFont); } else { pos = mGLFont->charFromPixelOffset(mWText.c_str(), line_start, (F32)(local_x - mTextRect.mLeft), (F32)mTextRect.getWidth(), line_len, round); } return line_start + pos; } } void LLTextEditor::setCursor(S32 row, S32 column) { const llwchar* doc = mWText.c_str(); const char CR = 10; //why are row and column S32s anyways? if(row < 0) row = 0; if(column < 0) column = 0; while(row--) { while (CR != *doc++); } doc += column; setCursorPos(doc - mWText.c_str()); } void LLTextEditor::setCursorPos(S32 offset) { mCursorPos = llclamp(offset, 0, (S32)getLength()); needsScroll(); // reset desired x cursor position mDesiredXPixel = -1; } // virtual BOOL LLTextEditor::canDeselect() const { return hasSelection(); } void LLTextEditor::deselect() { mSelectionStart = 0; mSelectionEnd = 0; mIsSelecting = FALSE; } void LLTextEditor::startSelection() { if( !mIsSelecting ) { mIsSelecting = TRUE; mSelectionStart = mCursorPos; mSelectionEnd = mCursorPos; } } void LLTextEditor::endSelection() { if( mIsSelecting ) { mIsSelecting = FALSE; mSelectionEnd = mCursorPos; } } BOOL LLTextEditor::selectionContainsLineBreaks() { if (hasSelection()) { S32 left = llmin(mSelectionStart, mSelectionEnd); S32 right = left + llabs(mSelectionStart - mSelectionEnd); const LLWString &wtext = mWText; for( S32 i = left; i < right; i++ ) { if (wtext[i] == '\n') { return TRUE; } } } return FALSE; } S32 LLTextEditor::indentLine( S32 pos, S32 spaces ) { // Assumes that pos is at the start of the line // spaces may be positive (indent) or negative (unindent). // Returns the actual number of characters added or removed. llassert(pos >= 0); llassert(pos <= getLength() ); S32 delta_spaces = 0; if (spaces >= 0) { // Indent for(S32 i=0; i < spaces; i++) { delta_spaces += addChar(pos, ' '); } } else { // Unindent for(S32 i=0; i < -spaces; i++) { const LLWString &wtext = mWText; if (wtext[pos] == ' ') { delta_spaces += remove( pos, 1, FALSE ); } } } return delta_spaces; } void LLTextEditor::indentSelectedLines( S32 spaces ) { if( hasSelection() ) { const LLWString &text = mWText; S32 left = llmin( mSelectionStart, mSelectionEnd ); S32 right = left + llabs( mSelectionStart - mSelectionEnd ); BOOL cursor_on_right = (mSelectionEnd > mSelectionStart); S32 cur = left; // Expand left to start of line while( (cur > 0) && (text[cur] != '\n') ) { cur--; } left = cur; if( cur > 0 ) { left++; } // Expand right to end of line if( text[right - 1] == '\n' ) { right--; } else { while( (text[right] != '\n') && (right <= getLength() ) ) { right++; } } // Find each start-of-line and indent it do { if( text[cur] == '\n' ) { cur++; } S32 delta_spaces = indentLine( cur, spaces ); if( delta_spaces > 0 ) { cur += delta_spaces; } right += delta_spaces; //text = mWText; // Find the next new line while( (cur < right) && (text[cur] != '\n') ) { cur++; } } while( cur < right ); if( (right < getLength()) && (text[right] == '\n') ) { right++; } // Set the selection and cursor if( cursor_on_right ) { mSelectionStart = left; mSelectionEnd = right; } else { mSelectionStart = right; mSelectionEnd = left; } mCursorPos = mSelectionEnd; } } //virtual BOOL LLTextEditor::canSelectAll() const { return TRUE; } // virtual void LLTextEditor::selectAll() { mSelectionStart = getLength(); mSelectionEnd = 0; mCursorPos = mSelectionEnd; } BOOL LLTextEditor::handleToolTip(S32 x, S32 y, std::string& msg, LLRect* sticky_rect_screen) { for ( child_list_const_iter_t child_it = getChildList()->begin(); child_it != getChildList()->end(); ++child_it) { LLView* viewp = *child_it; S32 local_x = x - viewp->getRect().mLeft; S32 local_y = y - viewp->getRect().mBottom; if( viewp->handleToolTip(local_x, local_y, msg, sticky_rect_screen ) ) { return TRUE; } } if( mSegments.empty() ) { return TRUE; } const LLTextSegment* cur_segment = getSegmentAtLocalPos( x, y ); if( cur_segment ) { BOOL has_tool_tip = FALSE; has_tool_tip = cur_segment->getToolTip( msg ); if( has_tool_tip ) { // Just use a slop area around the cursor // Convert rect local to screen coordinates S32 SLOP = 8; localPointToScreen( x - SLOP, y - SLOP, &(sticky_rect_screen->mLeft), &(sticky_rect_screen->mBottom) ); sticky_rect_screen->mRight = sticky_rect_screen->mLeft + 2 * SLOP; sticky_rect_screen->mTop = sticky_rect_screen->mBottom + 2 * SLOP; } } return TRUE; } BOOL LLTextEditor::handleScrollWheel(S32 x, S32 y, S32 clicks) { // Pretend the mouse is over the scrollbar mScrollbar->handleScrollWheel( 0, 0, clicks ); return TRUE; } BOOL LLTextEditor::handleMouseDown(S32 x, S32 y, MASK mask) { BOOL handled = FALSE; // SL-51858: Key presses are not being passed to the Popup menu. // A proper fix is non-trivial so instead just close the menu. LLMenuGL* menu = (LLMenuGL*)mPopupMenuHandle.get(); if (menu && menu->isOpen()) { LLMenuGL::sMenuContainer->hideMenus(); } // Let scrollbar have first dibs handled = LLView::childrenHandleMouseDown(x, y, mask) != NULL; if( !handled && mTakesNonScrollClicks) { if (!(mask & MASK_SHIFT)) { deselect(); } BOOL start_select = TRUE; if( start_select ) { // If we're not scrolling (handled by child), then we're selecting if (mask & MASK_SHIFT) { S32 old_cursor_pos = mCursorPos; setCursorAtLocalPos( x, y, TRUE ); if (hasSelection()) { /* Mac-like behavior - extend selection towards the cursor if (mCursorPos < mSelectionStart && mCursorPos < mSelectionEnd) { // ...left of selection mSelectionStart = llmax(mSelectionStart, mSelectionEnd); mSelectionEnd = mCursorPos; } else if (mCursorPos > mSelectionStart && mCursorPos > mSelectionEnd) { // ...right of selection mSelectionStart = llmin(mSelectionStart, mSelectionEnd); mSelectionEnd = mCursorPos; } else { mSelectionEnd = mCursorPos; } */ // Windows behavior mSelectionEnd = mCursorPos; } else { mSelectionStart = old_cursor_pos; mSelectionEnd = mCursorPos; } // assume we're starting a drag select mIsSelecting = TRUE; } else { setCursorAtLocalPos( x, y, TRUE ); startSelection(); } gFocusMgr.setMouseCapture( this ); } handled = TRUE; } if (hasTabStop()) { setFocus( TRUE ); handled = TRUE; } // Delay cursor flashing resetKeystrokeTimer(); return handled; } BOOL LLTextEditor::handleRightMouseDown( S32 x, S32 y, MASK mask ) { setFocus(TRUE); //setCursorAtLocalPos( x, y, TRUE ); S32 wordStart = 0; S32 wordLen = 0; S32 pos = getCursorPosFromLocalCoord(x,y,TRUE); LLMenuGL* menu = (LLMenuGL*)mPopupMenuHandle.get(); if (menu) { for (auto tempBind : suggestionMenuItems) { if (tempBind) { menu->removeChild(tempBind->menuItem); tempBind->menuItem->die(); delete tempBind; } } suggestionMenuItems.clear(); menu->die(); } auto segment = getSegmentAtLocalPos(x, y); const LLStyleSP style = segment ? segment->getStyle() : nullptr; auto submenu = (style && style->isLink()) ? createUrlContextMenu(x, y, style->getLinkHREF()) : nullptr; // Add url menu to base menu if we have a selection, otherwise make it the menu. menu = (submenu && !hasSelection()) ? submenu : LLUICtrlFactory::getInstance()->buildMenu("menu_texteditor.xml", LLMenuGL::sMenuContainer); mPopupMenuHandle = menu->getHandle(); if (menu) { if (submenu && submenu != menu) { submenu->removeChild(submenu->getChild("Select All")); // There can be only one! menu->appendMenu(submenu); } // spell_check="true" in xui if (!mReadOnly && mSpellCheckable) { bool is_word_part = getWordBoundriesAt(pos, &wordStart, &wordLen); if (is_word_part) { const LLWString &text = mWText; std::string selectedWord(std::string(text.begin(), text.end()).substr(wordStart, wordLen)); if (!glggHunSpell->isSpelledRight(selectedWord)) { //misspelled word here, and you have just right clicked on it! std::vector suggs = glggHunSpell->getSuggestionList(selectedWord); menu->addSeparator(); for (auto word : suggs) { SpellMenuBind * tempStruct = new SpellMenuBind; tempStruct->origin = this; tempStruct->word = word; tempStruct->wordPositionEnd = wordStart + wordLen; tempStruct->wordPositionStart = wordStart; tempStruct->wordY = y; LLMenuItemCallGL * suggMenuItem = new LLMenuItemCallGL( tempStruct->word, spell_correct, NULL, tempStruct); tempStruct->menuItem = suggMenuItem; suggestionMenuItems.push_back(tempStruct); menu->addChild(suggMenuItem); } SpellMenuBind * tempStruct = new SpellMenuBind; tempStruct->origin = this; tempStruct->word = selectedWord; tempStruct->wordPositionEnd = wordStart + wordLen; tempStruct->wordPositionStart = wordStart; tempStruct->wordY = y; LLMenuItemCallGL * suggMenuItem = new LLMenuItemCallGL( "Add Word", spell_add, NULL, tempStruct); tempStruct->menuItem = suggMenuItem; suggestionMenuItems.push_back(tempStruct); menu->addChild(suggMenuItem); } } SpellMenuBind * tempStruct = new SpellMenuBind; tempStruct->origin = this; if (glggHunSpell->getSpellCheckHighlight()) { tempStruct->word = "Hide Misspellings"; } else { tempStruct->word = "Show Misspellings"; } LLMenuItemCallGL * suggMenuItem = new LLMenuItemCallGL( tempStruct->word, spell_show, NULL, tempStruct); tempStruct->menuItem = suggMenuItem; suggestionMenuItems.push_back(tempStruct); menu->addChild(suggMenuItem); } mLastContextMenuX = x; mLastContextMenuY = y; menu->buildDrawLabels(); menu->updateParent(LLMenuGL::sMenuContainer); LLMenuGL::showPopup(this, menu, x, y); } return TRUE; } BOOL LLTextEditor::handleMiddleMouseDown(S32 x, S32 y, MASK mask) { setFocus( TRUE ); if( canPastePrimary() ) { setCursorAtLocalPos( x, y, TRUE ); pastePrimary(); } return TRUE; } bool always_underline_links() { static const LLUICachedControl always_underline("SinguAlwaysUnderlineLinks"); return always_underline; } BOOL LLTextEditor::handleHover(S32 x, S32 y, MASK mask) { BOOL handled = FALSE; auto old_hover = mHoverSegment; mHoverSegment = NULL; if(hasMouseCapture() ) { if( mIsSelecting ) { if (x != mLastSelectionX || y != mLastSelectionY) { mLastSelectionX = x; mLastSelectionY = y; } if( y > mTextRect.mTop ) { mScrollbar->setDocPos( mScrollbar->getDocPos() - 1 ); } else if( y < mTextRect.mBottom ) { mScrollbar->setDocPos( mScrollbar->getDocPos() + 1 ); } setCursorAtLocalPos( x, y, TRUE ); mSelectionEnd = mCursorPos; } LL_DEBUGS("UserInput") << "hover handled by " << getName() << " (active)" << LL_ENDL; getWindow()->setCursor(UI_CURSOR_IBEAM); handled = TRUE; } if( !handled ) { // Pass to children handled = LLView::childrenHandleHover(x, y, mask) != NULL; } if( handled ) { // Delay cursor flashing resetKeystrokeTimer(); } // Opaque if( !handled && mTakesNonScrollClicks) { // Check to see if we're over an HTML-style link if( !mSegments.empty() ) { LLTextSegment* cur_segment = getSegmentAtLocalPos( x, y ); if( cur_segment ) { if(cur_segment->getStyle()->isLink()) { LL_DEBUGS("UserInput") << "hover handled by " << getName() << " (over link, inactive)" << LL_ENDL; getWindow()->setCursor(UI_CURSOR_HAND); handled = TRUE; } else if(cur_segment->getStyle()->getIsEmbeddedItem()) { LL_DEBUGS("UserInput") << "hover handled by " << getName() << " (over embedded item, inactive)" << LL_ENDL; getWindow()->setCursor(UI_CURSOR_HAND); //getWindow()->setCursor(UI_CURSOR_ARROW); handled = TRUE; } mHoverSegment = cur_segment; } } if( !handled ) { LL_DEBUGS("UserInput") << "hover handled by " << getName() << " (inactive)" << LL_ENDL; if (!mScrollbar->getVisible() || x < getRect().getWidth() - SCROLLBAR_SIZE) { getWindow()->setCursor(UI_CURSOR_IBEAM); } else { getWindow()->setCursor(UI_CURSOR_ARROW); } handled = TRUE; } } if (old_hover != mHoverSegment) { if (old_hover && !always_underline_links()) old_hover->underlineOnHover(false); if (mHoverSegment) mHoverSegment->underlineOnHover(true); } if (mOnScrollEndCallback && mOnScrollEndData && (mScrollbar->getDocPos() == mScrollbar->getDocPosMax())) { mOnScrollEndCallback(mOnScrollEndData); } return handled; } void LLTextEditor::onMouseLeave(S32 x, S32 y, MASK mask) { if (mHoverSegment && !always_underline_links()) { mHoverSegment->underlineOnHover(false); mHoverSegment = nullptr; } } BOOL LLTextEditor::handleMouseUp(S32 x, S32 y, MASK mask) { BOOL handled = FALSE; // let scrollbar have first dibs handled = LLView::childrenHandleMouseUp(x, y, mask) != NULL; if( !handled && mTakesNonScrollClicks) { if( mIsSelecting ) { // Finish selection if( y > mTextRect.mTop ) { mScrollbar->setDocPos( mScrollbar->getDocPos() - 1 ); } else if( y < mTextRect.mBottom ) { mScrollbar->setDocPos( mScrollbar->getDocPos() + 1 ); } setCursorAtLocalPos( x, y, TRUE ); endSelection(); } if( !hasSelection() ) { handleMouseUpOverSegment( x, y, mask ); } // take selection to 'primary' clipboard updatePrimary(); handled = TRUE; } // Delay cursor flashing resetKeystrokeTimer(); if( hasMouseCapture() ) { gFocusMgr.setMouseCapture( NULL ); handled = TRUE; } return handled; } BOOL LLTextEditor::handleDoubleClick(S32 x, S32 y, MASK mask) { BOOL handled = FALSE; // let scrollbar have first dibs handled = LLView::childrenHandleDoubleClick(x, y, mask) != NULL; if( !handled && mTakesNonScrollClicks) { setCursorAtLocalPos( x, y, FALSE ); deselect(); const LLWString &text = mWText; if( isPartOfWord( text[mCursorPos] ) ) { // Select word the cursor is over while ((mCursorPos > 0) && isPartOfWord(text[mCursorPos-1])) { mCursorPos--; } startSelection(); while ((mCursorPos < (S32)text.length()) && isPartOfWord( text[mCursorPos] ) ) { mCursorPos++; } mSelectionEnd = mCursorPos; } else if ((mCursorPos < (S32)text.length()) && !iswspace( text[mCursorPos]) ) { // Select the character the cursor is over startSelection(); mCursorPos++; mSelectionEnd = mCursorPos; } // We don't want handleMouseUp() to "finish" the selection (and thereby // set mSelectionEnd to where the mouse is), so we finish the selection here. mIsSelecting = FALSE; // delay cursor flashing resetKeystrokeTimer(); // take selection to 'primary' clipboard updatePrimary(); handled = TRUE; } return handled; } // Allow calling cards to be dropped onto text fields. Append the name and // a carriage return. // virtual BOOL LLTextEditor::handleDragAndDrop(S32 x, S32 y, MASK mask, BOOL drop, EDragAndDropType cargo_type, void *cargo_data, EAcceptance *accept, std::string& tooltip_msg) { *accept = ACCEPT_NO; return TRUE; } //---------------------------------------------------------------------------- // Returns change in number of characters in mText S32 LLTextEditor::execute( LLTextCmd* cmd ) { S32 delta = 0; if( cmd->execute(this, &delta) ) { // Delete top of undo stack undo_stack_t::iterator enditer = std::find(mUndoStack.begin(), mUndoStack.end(), mLastCmd); if (enditer != mUndoStack.begin()) { --enditer; std::for_each(mUndoStack.begin(), enditer, DeletePointer()); mUndoStack.erase(mUndoStack.begin(), enditer); } // Push the new command is now on the top (front) of the undo stack. mUndoStack.push_front(cmd); mLastCmd = cmd; } else { // Operation failed, so don't put it on the undo stack. delete cmd; } return delta; } S32 LLTextEditor::insert(const S32 pos, const LLWString &wstr, const BOOL group_with_next_op) { return execute( new LLTextCmdInsert( pos, group_with_next_op, wstr ) ); } S32 LLTextEditor::remove(const S32 pos, const S32 length, const BOOL group_with_next_op) { return execute( new LLTextCmdRemove( pos, group_with_next_op, length ) ); } S32 LLTextEditor::overwriteChar(S32 pos, llwchar wc) { if ((S32)mWText.length() == pos) { return addChar(pos, wc); } else { return execute(new LLTextCmdOverwriteChar(pos, FALSE, wc)); } } // Remove a single character from the text. Tries to remove // a pseudo-tab (up to for spaces in a row) void LLTextEditor::removeCharOrTab() { if( !getEnabled() ) { return; } if( mCursorPos > 0 ) { S32 chars_to_remove = 1; const LLWString &text = mWText; if (text[mCursorPos - 1] == ' ') { // Try to remove a "tab" S32 line, offset; getLineAndOffset(mCursorPos, &line, &offset); if (offset > 0) { chars_to_remove = offset % SPACES_PER_TAB; if( chars_to_remove == 0 ) { chars_to_remove = SPACES_PER_TAB; } for( S32 i = 0; i < chars_to_remove; i++ ) { if (text[ mCursorPos - i - 1] != ' ') { // Fewer than a full tab's worth of spaces, so // just delete a single character. chars_to_remove = 1; break; } } } } for (S32 i = 0; i < chars_to_remove; i++) { setCursorPos(mCursorPos - 1); remove( mCursorPos, 1, FALSE ); } } else { reportBadKeystroke(); } } // Remove a single character from the text S32 LLTextEditor::removeChar(S32 pos) { return remove( pos, 1, FALSE ); } void LLTextEditor::removeChar() { if (!getEnabled()) { return; } if (mCursorPos > 0) { setCursorPos(mCursorPos - 1); removeChar(mCursorPos); } else { reportBadKeystroke(); } } // Remove a word (set of characters up to next space/punctuation) from the text void LLTextEditor::removeWord(bool prev) { const S32& pos(mCursorPos); if (prev ? pos > 0 : pos < getLength()) { S32 new_pos(prev ? prevWordPos(pos) : nextWordPos(pos)); if (new_pos == pos) // Other character we don't jump over new_pos = prev ? prevWordPos(new_pos-1) : nextWordPos(new_pos+1); const U32 diff(labs(pos - new_pos)); if (prev) { remove(new_pos, diff, false); setCursorPos(new_pos); } else { remove(pos, diff, false); } } else { reportBadKeystroke(); } } // Add a single character to the text S32 LLTextEditor::addChar(S32 pos, llwchar wc) { if ( (wstring_utf8_length( mWText ) + wchar_utf8_length( wc )) >= mMaxTextByteLength) { make_ui_sound("UISndBadKeystroke"); return 0; } if (mLastCmd && mLastCmd->canExtend(pos)) { S32 delta = 0; mLastCmd->extendAndExecute(this, pos, wc, &delta); return delta; } else { return execute(new LLTextCmdAddChar(pos, FALSE, wc)); } } void LLTextEditor::addChar(llwchar wc) { if( !getEnabled() ) { return; } if( hasSelection() ) { deleteSelection(TRUE); } else if (LL_KIM_OVERWRITE == gKeyboard->getInsertMode()) { removeChar(mCursorPos); } setCursorPos(mCursorPos + addChar( mCursorPos, wc )); } BOOL LLTextEditor::handleSelectionKey(const KEY key, const MASK mask) { BOOL handled = FALSE; if( mask & MASK_SHIFT ) { handled = TRUE; switch( key ) { case KEY_LEFT: if( 0 < mCursorPos ) { startSelection(); mCursorPos--; if( mask & MASK_CONTROL ) { mCursorPos = prevWordPos(mCursorPos); } mSelectionEnd = mCursorPos; } break; case KEY_RIGHT: if( mCursorPos < getLength() ) { startSelection(); mCursorPos++; if( mask & MASK_CONTROL ) { mCursorPos = nextWordPos(mCursorPos); } mSelectionEnd = mCursorPos; } break; case KEY_UP: startSelection(); changeLine( -1 ); mSelectionEnd = mCursorPos; break; case KEY_PAGE_UP: startSelection(); changePage( -1 ); mSelectionEnd = mCursorPos; break; case KEY_HOME: startSelection(); if( mask & MASK_CONTROL ) { mCursorPos = 0; } else { startOfLine(); } mSelectionEnd = mCursorPos; break; case KEY_DOWN: startSelection(); changeLine( 1 ); mSelectionEnd = mCursorPos; break; case KEY_PAGE_DOWN: startSelection(); changePage( 1 ); mSelectionEnd = mCursorPos; break; case KEY_END: startSelection(); if( mask & MASK_CONTROL ) { mCursorPos = getLength(); } else { endOfLine(); } mSelectionEnd = mCursorPos; break; default: handled = FALSE; break; } } if( !handled && mHandleEditKeysDirectly ) { if( (MASK_CONTROL & mask) && ('A' == key) ) { if( canSelectAll() ) { selectAll(); } else { reportBadKeystroke(); } handled = TRUE; } } if( handled ) { // take selection to 'primary' clipboard updatePrimary(); } return handled; } BOOL LLTextEditor::handleNavigationKey(const KEY key, const MASK mask) { BOOL handled = FALSE; // Ignore capslock key if( MASK_NONE == mask ) { handled = TRUE; switch( key ) { case KEY_UP: if (mReadOnly) { mScrollbar->setDocPos(mScrollbar->getDocPos() - 1); } else { changeLine( -1 ); } break; case KEY_PAGE_UP: changePage( -1 ); break; case KEY_HOME: if (mReadOnly) { mScrollbar->setDocPos(0); } else { startOfLine(); } break; case KEY_DOWN: if (mReadOnly) { mScrollbar->setDocPos(mScrollbar->getDocPos() + 1); } else { changeLine( 1 ); } break; case KEY_PAGE_DOWN: changePage( 1 ); break; case KEY_END: if (mReadOnly) { mScrollbar->setDocPos(mScrollbar->getDocPosMax()); } else { endOfLine(); } break; case KEY_LEFT: if (mReadOnly) { break; } if( hasSelection() ) { setCursorPos(llmin( mCursorPos - 1, mSelectionStart, mSelectionEnd )); } else { if( 0 < mCursorPos ) { setCursorPos(mCursorPos - 1); } else { reportBadKeystroke(); } } break; case KEY_RIGHT: if (mReadOnly) { break; } if( hasSelection() ) { setCursorPos(llmax( mCursorPos + 1, mSelectionStart, mSelectionEnd )); } else { if( mCursorPos < getLength() ) { setCursorPos(mCursorPos + 1); } else { reportBadKeystroke(); } } break; default: handled = FALSE; break; } } if (mOnScrollEndCallback && mOnScrollEndData && (mScrollbar->getDocPos() == mScrollbar->getDocPosMax())) { mOnScrollEndCallback(mOnScrollEndData); } return handled; } void LLTextEditor::deleteSelection(BOOL group_with_next_op ) { if( getEnabled() && hasSelection() ) { S32 pos = llmin( mSelectionStart, mSelectionEnd ); S32 length = llabs( mSelectionStart - mSelectionEnd ); remove( pos, length, group_with_next_op ); deselect(); setCursorPos(pos); } } // virtual BOOL LLTextEditor::canCut() const { return !mReadOnly && hasSelection(); } // cut selection to clipboard void LLTextEditor::cut() { if( !canCut() ) { return; } S32 left_pos = llmin( mSelectionStart, mSelectionEnd ); S32 length = llabs( mSelectionStart - mSelectionEnd ); gClipboard.copyFromSubstring( mWText, left_pos, length, mSourceID ); deleteSelection( FALSE ); needsReflow(); onKeyStroke(); } BOOL LLTextEditor::canCopy() const { return hasSelection(); } // copy selection to clipboard void LLTextEditor::copy(bool raw) { if( !canCopy() ) { return; } S32 left_pos = llmin( mSelectionStart, mSelectionEnd ); S32 length = llabs( mSelectionStart - mSelectionEnd ); // Does our selection include any Segments with links? if (mParseHTML && raw) { auto begin = std::find_if(mSegments.begin(), mSegments.end(), [left_pos](const LLTextSegmentPtr& ptr) { return ptr->getEnd() > left_pos; }); auto last = mSegments.end(); if (begin == last || begin->isNull()) { gClipboard.copyFromSubstring(mWText, left_pos, length, mSourceID); return; } S32 right_pos = left_pos + length, offset = 0; { // If our selection starts in the middle of a link, set our left_pos to the beginning of its segment. auto segment = **begin; if (auto style = segment.getStyle()) if (style->isLink()) left_pos = llmin(segment.getStart(), left_pos); } auto text = mWText.substr(left_pos, length); for (; begin->notNull() && begin != last && (*begin)->getStart() <= right_pos; ++begin) { auto segment = **begin; //llassert(segment->getStyle()); // If someone is stores the result of the S32 constructor, they're in so much trouble!! const auto& link = segment.getStyle()->getLinkHREF(); if (!link.empty()) { const S32 label_length = (segment.getEnd() - segment.getStart()); const S32 start = (segment.getStart()+offset)-left_pos; const auto label = text.substr(start, label_length); const auto wlink = utf8str_to_wstring(link); const auto pos = wlink.find(label); // Do not replace if normal link, or contains normal link (but may omit protocol) but ends the same way // (i.e. [http://foo.bar/baz foo.bar] should still be restored here but not foo.bar/baz or foo.bar if (pos == std::string::npos // Label is not (part of) the url || (pos != 0 && wlink[pos-1] != '/') || pos+label.size() != wlink.size()) // Label is part of the url but there's more on either side of the url after the protocol { constexpr llwchar startchar = '[', space = ' ', endchar = ']'; const auto raw_html = startchar + wlink + space + label + endchar; text.replace(start, label_length, raw_html); offset += raw_html.size() - label_length; // Track how much we've offset the string by replacing labels with their raw html and thus adding characters } } } gClipboard.copyFromSubstring(text, 0, text.length(), mSourceID); } else gClipboard.copyFromSubstring(mWText, left_pos, length, mSourceID); } BOOL LLTextEditor::canPaste() const { return !mReadOnly && gClipboard.canPasteString(); } void LLTextEditor::spellReplace(SpellMenuBind* spellData) { remove( spellData->wordPositionStart, spellData->wordPositionEnd - spellData->wordPositionStart, TRUE ); LLWString clean_string = utf8str_to_wstring(spellData->word); insert(spellData->wordPositionStart, clean_string, FALSE); mCursorPos+=clean_string.length() - (spellData->wordPositionEnd-spellData->wordPositionStart); needsReflow(); } // paste from clipboard void LLTextEditor::paste() { bool is_primary = false; pasteHelper(is_primary); } // paste from primary void LLTextEditor::pastePrimary() { bool is_primary = true; pasteHelper(is_primary); } // paste from primary (itsprimary==true) or clipboard (itsprimary==false) void LLTextEditor::pasteHelper(bool is_primary) { bool can_paste_it; if (is_primary) { can_paste_it = canPastePrimary(); } else { can_paste_it = canPaste(); } if (!can_paste_it) { return; } LLUUID source_id; LLWString paste; if (is_primary) { paste = gClipboard.getPastePrimaryWString(&source_id); } else { paste = gClipboard.getPasteWString(&source_id); } if (paste.empty()) { return; } // Delete any selected characters (the paste replaces them) if( (!is_primary) && hasSelection() ) { deleteSelection(TRUE); } // Clean up string (replace tabs and remove characters that our fonts don't support). LLWString clean_string(paste); LLWStringUtil::replaceTabsWithSpaces(clean_string, SPACES_PER_TAB); if( mAllowEmbeddedItems ) { const llwchar LF = 10; S32 len = clean_string.length(); for( S32 i = 0; i < len; i++ ) { llwchar wc = clean_string[i]; if( (wc < LLFontFreetype::FIRST_CHAR) && (wc != LF) ) { clean_string[i] = LL_UNKNOWN_CHAR; } else if (wc >= FIRST_EMBEDDED_CHAR && wc <= LAST_EMBEDDED_CHAR) { clean_string[i] = pasteEmbeddedItem(wc); } } } // Insert the new text into the existing text. setCursorPos(mCursorPos + insert(mCursorPos, clean_string, FALSE)); deselect(); needsReflow(); onKeyStroke(); } // copy selection to primary void LLTextEditor::copyPrimary() { if( !canCopy() ) { return; } S32 left_pos = llmin( mSelectionStart, mSelectionEnd ); S32 length = llabs( mSelectionStart - mSelectionEnd ); gClipboard.copyFromPrimarySubstring(mWText, left_pos, length, mSourceID); } BOOL LLTextEditor::canPastePrimary() const { return !mReadOnly && gClipboard.canPastePrimaryString(); } void LLTextEditor::updatePrimary() { if (canCopy()) { copyPrimary(); } } BOOL LLTextEditor::handleControlKey(const KEY key, const MASK mask) { BOOL handled = FALSE; if( mask & MASK_CONTROL ) { handled = TRUE; switch( key ) { case KEY_HOME: if( mask & MASK_SHIFT ) { startSelection(); mCursorPos = 0; mSelectionEnd = mCursorPos; } else { // Ctrl-Home, Ctrl-Left, Ctrl-Right, Ctrl-Down // all move the cursor as if clicking, so should deselect. deselect(); setCursorPos(0); } break; case KEY_END: { if( mask & MASK_SHIFT ) { startSelection(); } else { // Ctrl-Home, Ctrl-Left, Ctrl-Right, Ctrl-Down // all move the cursor as if clicking, so should deselect. deselect(); } endOfDoc(); if( mask & MASK_SHIFT ) { mSelectionEnd = mCursorPos; } break; } case KEY_RIGHT: if( mCursorPos < getLength() ) { // Ctrl-Home, Ctrl-Left, Ctrl-Right, Ctrl-Down // all move the cursor as if clicking, so should deselect. deselect(); setCursorPos(nextWordPos(mCursorPos + 1)); } break; case KEY_LEFT: if( mCursorPos > 0 ) { // Ctrl-Home, Ctrl-Left, Ctrl-Right, Ctrl-Down // all move the cursor as if clicking, so should deselect. deselect(); setCursorPos(prevWordPos(mCursorPos - 1)); } break; case KEY_DELETE: if (getEnabled()) removeWord(false); else handled = false; break; default: handled = FALSE; break; } } if (handled) { updatePrimary(); } return handled; } BOOL LLTextEditor::handleEditKey(const KEY key, const MASK mask) { BOOL handled = FALSE; // Standard edit keys (Ctrl-X, Delete, etc,) are handled here instead of routed by the menu system. if( KEY_DELETE == key ) { if( canDoDelete() ) { doDelete(); } else { reportBadKeystroke(); } handled = TRUE; } else if ((mask & (MASK_CONTROL|MASK_SHIFT)) == (MASK_CONTROL|MASK_SHIFT)) { if (key == 'C') { if (canCopy()) copy(true); else reportBadKeystroke(); handled = TRUE; } } else if( MASK_CONTROL & mask ) { if( 'C' == key ) { if( canCopy() ) { copy(); } else { reportBadKeystroke(); } handled = TRUE; } else if( 'V' == key ) { if( canPaste() ) { paste(); } else { reportBadKeystroke(); } handled = TRUE; } else if( 'X' == key ) { if( canCut() ) { cut(); } else { reportBadKeystroke(); } handled = TRUE; } } if( handled ) { // take selection to 'primary' clipboard updatePrimary(); } return handled; } BOOL LLTextEditor::handleSpecialKey(const KEY key, const MASK mask, BOOL* return_key_hit) { *return_key_hit = FALSE; BOOL handled = TRUE; switch( key ) { case KEY_INSERT: if (mask == MASK_NONE) { gKeyboard->toggleInsertMode(); } break; case KEY_BACKSPACE: if( hasSelection() ) { deleteSelection(FALSE); } else if( 0 < mCursorPos ) { if (mask == MASK_CONTROL) removeWord(true); else removeCharOrTab(); } else { reportBadKeystroke(); } break; case KEY_RETURN: if (mask == MASK_NONE) { if( hasSelection() ) { deleteSelection(FALSE); } autoIndent(); // TODO: make this optional } else { handled = FALSE; break; } break; case KEY_TAB: if (mask & MASK_CONTROL) { handled = FALSE; break; } if( hasSelection() && selectionContainsLineBreaks() ) { indentSelectedLines( (mask & MASK_SHIFT) ? -SPACES_PER_TAB : SPACES_PER_TAB ); } else { if( hasSelection() ) { deleteSelection(FALSE); } S32 line, offset; getLineAndOffset( mCursorPos, &line, &offset ); S32 spaces_needed = SPACES_PER_TAB - (offset % SPACES_PER_TAB); for( S32 i=0; i < spaces_needed; i++ ) { addChar( ' ' ); } } break; default: handled = FALSE; break; } if (handled) { onKeyStroke(); } return handled; } void LLTextEditor::unindentLineBeforeCloseBrace() { if( mCursorPos >= 1 ) { const LLWString &text = mWText; if( ' ' == text[ mCursorPos - 1 ] ) { removeCharOrTab(); } } } BOOL LLTextEditor::handleKeyHere(KEY key, MASK mask ) { BOOL handled = FALSE; BOOL selection_modified = FALSE; BOOL return_key_hit = FALSE; BOOL text_may_have_changed = TRUE; // SL-51858: Key presses are not being passed to the Popup menu. // A proper fix is non-trivial so instead just close the menu. LLMenuGL* menu = (LLMenuGL*)mPopupMenuHandle.get(); if (menu && menu->isOpen()) { LLMenuGL::sMenuContainer->hideMenus(); } if ( gFocusMgr.getKeyboardFocus() == this ) { // Special case for TAB. If want to move to next field, report // not handled and let the parent take care of field movement. if (KEY_TAB == key && mTabsToNextField) { return FALSE; } handled = handleNavigationKey( key, mask ); if( handled ) { text_may_have_changed = FALSE; } if( !handled ) { handled = handleSelectionKey( key, mask ); if( handled ) { selection_modified = TRUE; } } if( !handled ) { handled = handleControlKey( key, mask ); if( handled ) { selection_modified = TRUE; } } if( !handled && mHandleEditKeysDirectly ) { handled = handleEditKey( key, mask ); if( handled ) { selection_modified = TRUE; text_may_have_changed = TRUE; } } // SL-51858: Key presses are not being passed to the Popup menu. // A proper fix is non-trivial so instead just close the menu. LLMenuGL* menu = (LLMenuGL*)mPopupMenuHandle.get(); if (menu && menu->isOpen()) { LLMenuGL::sMenuContainer->hideMenus(); } // Handle most keys only if the text editor is writeable. if( !mReadOnly ) { if( !handled ) { handled = handleSpecialKey( key, mask, &return_key_hit ); if( handled ) { selection_modified = TRUE; text_may_have_changed = TRUE; } } } if( handled ) { resetKeystrokeTimer(); // Most keystrokes will make the selection box go away, but not all will. if( !selection_modified && KEY_SHIFT != key && KEY_CONTROL != key && KEY_ALT != key && KEY_CAPSLOCK ) { deselect(); } if(text_may_have_changed) { needsReflow(); } needsScroll(); } } return handled; } BOOL LLTextEditor::handleUnicodeCharHere(llwchar uni_char) { if ((uni_char < 0x20) || (uni_char == 0x7F)) // Control character or DEL { return FALSE; } BOOL handled = FALSE; if ( gFocusMgr.getKeyboardFocus() == this ) { // Handle most keys only if the text editor is writeable. if( !mReadOnly ) { if( '}' == uni_char ) { unindentLineBeforeCloseBrace(); } // TODO: KLW Add auto show of tool tip on ( addChar( uni_char ); // Keys that add characters temporarily hide the cursor getWindow()->hideCursorUntilMouseMove(); handled = TRUE; } if( handled ) { resetKeystrokeTimer(); // Most keystrokes will make the selection box go away, but not all will. deselect(); needsReflow(); onKeyStroke(); } } return handled; } // virtual BOOL LLTextEditor::canDoDelete() const { return !mReadOnly && ( hasSelection() || (mCursorPos < getLength()) ); } void LLTextEditor::doDelete() { if( !canDoDelete() ) { return; } if( hasSelection() ) { deleteSelection(FALSE); } else if( mCursorPos < getLength() ) { S32 i; S32 chars_to_remove = 1; const LLWString &text = mWText; if( (text[ mCursorPos ] == ' ') && (mCursorPos + SPACES_PER_TAB < getLength()) ) { // Try to remove a full tab's worth of spaces S32 line, offset; getLineAndOffset( mCursorPos, &line, &offset ); chars_to_remove = SPACES_PER_TAB - (offset % SPACES_PER_TAB); if( chars_to_remove == 0 ) { chars_to_remove = SPACES_PER_TAB; } for( i = 0; i < chars_to_remove; i++ ) { if( text[mCursorPos + i] != ' ' ) { chars_to_remove = 1; break; } } } for( i = 0; i < chars_to_remove; i++ ) { setCursorPos(mCursorPos + 1); removeChar(); } } needsReflow(); onKeyStroke(); } //---------------------------------------------------------------------------- void LLTextEditor::blockUndo() { mBaseDocIsPristine = FALSE; mLastCmd = NULL; std::for_each(mUndoStack.begin(), mUndoStack.end(), DeletePointer()); mUndoStack.clear(); } // virtual BOOL LLTextEditor::canUndo() const { return !mReadOnly && mLastCmd != NULL; } void LLTextEditor::undo() { if( !canUndo() ) { return; } deselect(); S32 pos = 0; do { pos = mLastCmd->undo(this); undo_stack_t::iterator iter = std::find(mUndoStack.begin(), mUndoStack.end(), mLastCmd); if (iter != mUndoStack.end()) ++iter; if (iter != mUndoStack.end()) mLastCmd = *iter; else mLastCmd = NULL; } while( mLastCmd && mLastCmd->groupWithNext() ); setCursorPos(pos); needsReflow(); onKeyStroke(); } BOOL LLTextEditor::canRedo() const { return !mReadOnly && (mUndoStack.size() > 0) && (mLastCmd != mUndoStack.front()); } void LLTextEditor::redo() { if( !canRedo() ) { return; } deselect(); S32 pos = 0; do { if( !mLastCmd ) { mLastCmd = mUndoStack.back(); } else { undo_stack_t::iterator iter = std::find(mUndoStack.begin(), mUndoStack.end(), mLastCmd); if (iter != mUndoStack.begin()) mLastCmd = *(--iter); else mLastCmd = NULL; } if( mLastCmd ) { pos = mLastCmd->redo(this); } } while( mLastCmd && mLastCmd->groupWithNext() && (mLastCmd != mUndoStack.front()) ); setCursorPos(pos); needsReflow(); onKeyStroke(); } void LLTextEditor::onFocusReceived() { LLUICtrl::onFocusReceived(); updateAllowingLanguageInput(); } // virtual, from LLView void LLTextEditor::onFocusLost() { updateAllowingLanguageInput(); // Route menu back to the default if( gEditMenuHandler == this ) { gEditMenuHandler = NULL; } if (mCommitOnFocusLost) { onCommit(); } // Make sure cursor is shown again getWindow()->showCursorFromMouseMove(); LLUICtrl::onFocusLost(); } void LLTextEditor::setEnabled(BOOL enabled) { // just treat enabled as read-only flag BOOL read_only = !enabled; if (read_only != mReadOnly) { mReadOnly = read_only; updateSegments(); updateAllowingLanguageInput(); } } void LLTextEditor::drawBackground() { S32 left = 0; S32 top = getRect().getHeight(); S32 right = getRect().getWidth(); S32 bottom = 0; LLColor4 bg_color = mReadOnly ? mReadOnlyBgColor : gFocusMgr.getKeyboardFocus() == this ? mFocusBgColor : mWriteableBgColor; if( mShowLineNumbers ) { gl_rect_2d(left, top, UI_TEXTEDITOR_LINE_NUMBER_MARGIN, bottom, mReadOnlyBgColor ); // line number area always read-only gl_rect_2d(UI_TEXTEDITOR_LINE_NUMBER_MARGIN, top, right, bottom, bg_color); // body text area to the right of line numbers gl_rect_2d(UI_TEXTEDITOR_LINE_NUMBER_MARGIN, top, UI_TEXTEDITOR_LINE_NUMBER_MARGIN-1, bottom, LLColor4::grey3); // separator } else { gl_rect_2d(left, top, right, bottom, bg_color); // body text area } LLView::draw(); } // Draws the black box behind the selected text void LLTextEditor::drawSelectionBackground() { // Draw selection even if we don't have keyboard focus for search/replace if( hasSelection() ) { const LLWString &text = mWText; const S32 text_len = getLength(); std::queue line_endings; S32 line_height = ll_round( mGLFont->getLineHeight() ); S32 selection_left = llmin( mSelectionStart, mSelectionEnd ); S32 selection_right = llmax( mSelectionStart, mSelectionEnd ); S32 selection_left_x = mTextRect.mLeft; S32 selection_left_y = mTextRect.mTop - line_height; S32 selection_right_x = mTextRect.mRight; S32 selection_right_y = mTextRect.mBottom; BOOL selection_left_visible = FALSE; BOOL selection_right_visible = FALSE; // Skip through the lines we aren't drawing. S32 cur_line = mScrollbar->getDocPos(); S32 left_line_num = cur_line; S32 num_lines = getLineCount(); S32 right_line_num = num_lines - 1; S32 line_start = -1; if (cur_line >= num_lines) { return; } line_start = getLineStart(cur_line); S32 left_visible_pos = line_start; S32 right_visible_pos = line_start; S32 text_y = mTextRect.mTop - line_height; // Find the coordinates of the selected area while((cur_line < num_lines)) { S32 next_line = -1; S32 line_end = text_len; if ((cur_line + 1) < num_lines) { next_line = getLineStart(cur_line + 1); line_end = next_line; line_end = ( (line_end - line_start)==0 || text[next_line-1] == '\n' || text[next_line-1] == '\0' || text[next_line-1] == ' ' || text[next_line-1] == '\t' ) ? next_line-1 : next_line; } const llwchar* line = text.c_str() + line_start; if( line_start <= selection_left && selection_left <= line_end ) { left_line_num = cur_line; selection_left_visible = TRUE; selection_left_x = mTextRect.mLeft + mGLFont->getWidth(line, 0, selection_left - line_start, mAllowEmbeddedItems); selection_left_y = text_y; } if( line_start <= selection_right && selection_right <= line_end ) { right_line_num = cur_line; selection_right_visible = TRUE; selection_right_x = mTextRect.mLeft + mGLFont->getWidth(line, 0, selection_right - line_start, mAllowEmbeddedItems); if (selection_right == line_end) { // add empty space for "newline" //selection_right_x += mGLFont->getWidth("n"); } selection_right_y = text_y; } // if selection spans end of current line... if (selection_left <= line_end && line_end < selection_right && selection_left != selection_right) { // extend selection slightly beyond end of line // to indicate selection of newline character (use "n" character to determine width) const LLWString nstr(utf8str_to_wstring(std::string("n"))); line_endings.push(mTextRect.mLeft + mGLFont->getWidth(line, 0, line_end - line_start, mAllowEmbeddedItems) + mGLFont->getWidth(nstr.c_str())); } // move down one line text_y -= line_height; right_visible_pos = line_end; line_start = next_line; cur_line++; if (selection_right_visible) { break; } } // Draw the selection box (we're using a box instead of reversing the colors on the selected text). BOOL selection_visible = (left_visible_pos <= selection_right) && (selection_left <= right_visible_pos); if( selection_visible ) { gGL.getTexUnit(0)->unbind(LLTexUnit::TT_TEXTURE); const LLColor4& color = mReadOnly ? mReadOnlyBgColor : mWriteableBgColor; F32 alpha = hasFocus() ? 1.f : 0.8f; gGL.color4f( 1.f - color.mV[0], 1.f - color.mV[1], 1.f - color.mV[2], alpha ); S32 margin_offset = mShowLineNumbers ? UI_TEXTEDITOR_LINE_NUMBER_MARGIN : 0; if( selection_left_y == selection_right_y ) { // Draw from selection start to selection end gl_rect_2d( selection_left_x + margin_offset, selection_left_y + line_height + 1, selection_right_x + margin_offset, selection_right_y); } else { // Draw from selection start to the end of the first line if( mTextRect.mRight == selection_left_x ) { selection_left_x -= CURSOR_THICKNESS; } S32 line_end = line_endings.front(); line_endings.pop(); gl_rect_2d( selection_left_x + margin_offset, selection_left_y + line_height + 1, line_end + margin_offset, selection_left_y ); S32 line_num = left_line_num + 1; while(line_endings.size()) { S32 vert_offset = -(line_num - left_line_num) * line_height; // Draw the block between the two lines gl_rect_2d( mTextRect.mLeft + margin_offset, selection_left_y + vert_offset + line_height + 1, line_endings.front() + margin_offset, selection_left_y + vert_offset); line_endings.pop(); line_num++; } // Draw from the start of the last line to selection end if( mTextRect.mLeft == selection_right_x ) { selection_right_x += CURSOR_THICKNESS; } gl_rect_2d( mTextRect.mLeft + margin_offset, selection_right_y + line_height + 1, selection_right_x + margin_offset, selection_right_y ); } } } } void LLTextEditor::drawMisspelled() { if (!mReadOnly && mSpellCheckable) { if( ( ((getLength()<400)||(false)) &&( (S32(mSpellTimer.getElapsedTimeF32() / 1) & 1) )) || (S32(mKeystrokeTimer.getElapsedTimeF32() / 1) & 1) ) { S32 newSpellStart = getLineStart(mScrollbar->getDocPos());//start at the scroll start S32 newSpellEnd = getLineStart(mScrollbar->getDocPos() + 1 + mScrollbar->getDocSize()-mScrollbar->getDocPosMax());//end at the end o.o if (mScrollbar->getDocPos() == mScrollbar->getDocPosMax()) { newSpellEnd = (S32)mWText.length(); } if (isSpellDirty() || (newSpellEnd!=spellEnd || newSpellStart!=spellStart)) { spellEnd = newSpellEnd; spellStart = newSpellStart; misspellLocations = getMisspelledWordsPositions(); } } //draw if (glggHunSpell->getSpellCheckHighlight()) { for (int i = 0; i<(int)misspellLocations.size() ;i++) { S32 wstart = misspellLocations[i]; S32 wend = misspellLocations[++i]; //start curor code mod const LLWString &text = mWText; const S32 text_len = getLength(); // Skip through the lines we aren't drawing. S32 search_pos = mScrollbar->getDocPos(); S32 num_lines = getLineCount(); if (search_pos >= num_lines) { return; } S32 line_start = getLineStart(search_pos); F32 line_height = mGLFont->getLineHeight(); F32 text_y = (F32)(mTextRect.mTop) - line_height; F32 word_left = 0.f; F32 word_right = 0.f; F32 word_bottom = 0.f; BOOL word_visible = FALSE; S32 line_end = 0; // Determine if the cursor is visible and if so what its coordinates are. while( (mTextRect.mBottom <= ll_round(text_y)) && (search_pos < num_lines)) { line_end = text_len + 1; S32 next_line = -1; if ((search_pos + 1) < num_lines) { next_line = getLineStart(search_pos + 1); line_end = next_line - 1; } const llwchar* line = text.c_str() + line_start; // Find the cursor and selection bounds if( line_start <= wstart && wend <= line_end ) { word_visible = TRUE; word_left = (F32)mTextRect.mLeft + mGLFont->getWidthF32(line, 0, wstart - line_start, mAllowEmbeddedItems )-1.f; word_right = (F32)mTextRect.mLeft + mGLFont->getWidthF32(line, 0, wend - line_start, mAllowEmbeddedItems )+1.f; word_bottom = text_y; break; } // move down one line text_y -= line_height; line_start = next_line; search_pos++; } if (mShowLineNumbers) { word_left += UI_TEXTEDITOR_LINE_NUMBER_MARGIN; word_right += UI_TEXTEDITOR_LINE_NUMBER_MARGIN; } // Draw the cursor if (word_visible) { //end cursor code mod gGL.color4ub(255,0,0,200); while (word_leftgetDocPos(); S32 num_lines = getLineCount(); if (cur_pos >= num_lines) { return; } S32 line_start = getLineStart(cur_pos); F32 line_height = mGLFont->getLineHeight(); F32 text_y = (F32)(mTextRect.mTop) - line_height; F32 cursor_left = 0.f; F32 next_char_left = 0.f; F32 cursor_bottom = 0.f; BOOL cursor_visible = FALSE; S32 line_end = 0; // Determine if the cursor is visible and if so what its coordinates are. while( (mTextRect.mBottom <= ll_round(text_y)) && (cur_pos < num_lines)) { line_end = text_len + 1; S32 next_line = -1; if ((cur_pos + 1) < num_lines) { next_line = getLineStart(cur_pos + 1); line_end = next_line - 1; } const llwchar* line = text.c_str() + line_start; // Find the cursor and selection bounds if( line_start <= mCursorPos && mCursorPos <= line_end ) { cursor_visible = TRUE; next_char_left = (F32)mTextRect.mLeft + mGLFont->getWidthF32(line, 0, mCursorPos - line_start, mAllowEmbeddedItems ); cursor_left = next_char_left - 1.f; cursor_bottom = text_y; break; } // move down one line text_y -= line_height; line_start = next_line; cur_pos++; } if(mShowLineNumbers) { cursor_left += UI_TEXTEDITOR_LINE_NUMBER_MARGIN; } // Draw the cursor if( cursor_visible ) { // (Flash the cursor every half second starting a fixed time after the last keystroke) F32 elapsed = mKeystrokeTimer.getElapsedTimeF32(); if( (elapsed < CURSOR_FLASH_DELAY ) || (S32(elapsed * 2) & 1) ) { F32 cursor_top = cursor_bottom + line_height + 1.f; F32 cursor_right = cursor_left + (F32)CURSOR_THICKNESS; if (LL_KIM_OVERWRITE == gKeyboard->getInsertMode() && !hasSelection()) { cursor_left += CURSOR_THICKNESS; const LLWString space(utf8str_to_wstring(std::string(" "))); F32 spacew = mGLFont->getWidthF32(space.c_str()); if (mCursorPos == line_end) { cursor_right = cursor_left + spacew; } else { F32 width = mGLFont->getWidthF32(text.c_str(), mCursorPos, 1, mAllowEmbeddedItems); cursor_right = cursor_left + llmax(spacew, width); } } gGL.getTexUnit(0)->unbind(LLTexUnit::TT_TEXTURE); gGL.color4fv( mCursorColor.mV ); gl_rect_2d(llfloor(cursor_left), llfloor(cursor_top), llfloor(cursor_right), llfloor(cursor_bottom)); if (LL_KIM_OVERWRITE == gKeyboard->getInsertMode() && !hasSelection() && text[mCursorPos] != '\n') { const LLTextSegment* segmentp = getSegmentAtOffset(mCursorPos); LLColor4 text_color; if (segmentp) { text_color = segmentp->getColor(); } else if (mReadOnly) { text_color = mReadOnlyFgColor; } else { text_color = mFgColor; } mGLFont->render(text, mCursorPos, next_char_left, cursor_bottom + line_height, LLColor4(1.f - text_color.mV[VRED], 1.f - text_color.mV[VGREEN], 1.f - text_color.mV[VBLUE], 1.f), LLFontGL::LEFT, LLFontGL::TOP, 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 + llfloor(cursor_left), screen_pos.mBottom + llfloor(cursor_top) ); 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 LLTextEditor::drawPreeditMarker() { if (!hasPreeditString()) { return; } const llwchar *text = mWText.c_str(); const S32 text_len = getLength(); const S32 num_lines = getLineCount(); S32 cur_line = mScrollbar->getDocPos(); if (cur_line >= num_lines) { return; } const S32 line_height = ll_round( mGLFont->getLineHeight() ); S32 line_start = getLineStart(cur_line); S32 line_y = mTextRect.mTop - line_height; while((mTextRect.mBottom <= line_y) && (num_lines > cur_line)) { S32 next_start = -1; S32 line_end = text_len; if ((cur_line + 1) < num_lines) { next_start = getLineStart(cur_line + 1); line_end = next_start; } if ( text[line_end-1] == '\n' ) { --line_end; } // Does this line contain preedits? if (line_start >= mPreeditPositions.back()) { // We have passed the preedits. break; } if (line_end > mPreeditPositions.front()) { for (U32 i = 0; i < mPreeditStandouts.size(); i++) { S32 left = mPreeditPositions[i]; S32 right = mPreeditPositions[i + 1]; if (right <= line_start || left >= line_end) { continue; } S32 preedit_left = mTextRect.mLeft; if (left > line_start) { preedit_left += mGLFont->getWidth(text, line_start, left - line_start, mAllowEmbeddedItems); } S32 preedit_right = mTextRect.mLeft; if (right < line_end) { preedit_right += mGLFont->getWidth(text, line_start, right - line_start, mAllowEmbeddedItems); } else { preedit_right += mGLFont->getWidth(text, line_start, line_end - line_start, mAllowEmbeddedItems); } if (mPreeditStandouts[i]) { gl_rect_2d(preedit_left + PREEDIT_STANDOUT_GAP, line_y + PREEDIT_STANDOUT_POSITION, preedit_right - PREEDIT_STANDOUT_GAP - 1, line_y + PREEDIT_STANDOUT_POSITION - PREEDIT_STANDOUT_THICKNESS, (mCursorColor * PREEDIT_STANDOUT_BRIGHTNESS + mWriteableBgColor * (1 - PREEDIT_STANDOUT_BRIGHTNESS)).setAlpha(1.0f)); } else { gl_rect_2d(preedit_left + PREEDIT_MARKER_GAP, line_y + PREEDIT_MARKER_POSITION, preedit_right - PREEDIT_MARKER_GAP - 1, line_y + PREEDIT_MARKER_POSITION - PREEDIT_MARKER_THICKNESS, (mCursorColor * PREEDIT_MARKER_BRIGHTNESS + mWriteableBgColor * (1 - PREEDIT_MARKER_BRIGHTNESS)).setAlpha(1.0f)); } } } // move down one line line_y -= line_height; line_start = next_start; cur_line++; } } void LLTextEditor::drawText() { const LLWString &text = mWText; const S32 text_len = getLength(); if( text_len <= 0 ) return; 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 ); } LLGLSUIDefault gls_ui; // There are several concepts that are important for understanding the following drawing code. // The document is logically a sequence of characters (stored in a LLWString). // Variables below with "start" or "end" in their names refer to positions or offsets into it. // Next there are two kinds of "line" variables to understand. Newline characters in the // character sequence represent logical lines. These are what get numbered and so variables // representing this kind of line have "num" in their names. // The others represent line fragments or displayed lines which the scrollbar deals with. // When the "show line numbers" property is turned on, we draw line numbers to the left of the // beginning of each logical line and not in front of wrapped "continuation" display lines. -MG S32 cur_line = mScrollbar->getDocPos(); // scrollbar counts each wrap as a new line. S32 num_lines = getLineCount(); if (cur_line >= num_lines) return; S32 line_start = getLineStart(cur_line); S32 prev_start = getLineStart(cur_line-1); S32 cur_line_num = getLineForPosition(line_start); // doesn't count wraps. i.e. only counts newlines. S32 prev_line_num = getLineForPosition(prev_start); BOOL cur_line_is_continuation = cur_line_num > 0 && cur_line_num == prev_line_num; BOOL line_wraps = FALSE; LLTextSegment t(line_start); segment_list_t::iterator seg_iter; seg_iter = std::upper_bound(mSegments.begin(), mSegments.end(), &t, LLTextSegment::compare()); if (seg_iter == mSegments.end() || (*seg_iter)->getStart() > line_start) --seg_iter; LLTextSegment* cur_segment = *seg_iter; S32 line_height = ll_round( mGLFont->getLineHeight() ); F32 text_y = (F32)(mTextRect.mTop - line_height); while((mTextRect.mBottom <= text_y) && (cur_line < num_lines)) { S32 next_line = cur_line + 1; S32 next_start = -1; S32 line_end = text_len; if (next_line < num_lines) { next_start = getLineStart(next_line); line_end = next_start; } line_wraps = text[line_end-1] != '\n'; if ( ! line_wraps ) { --line_end; // don't attempt to draw the newline char. } F32 text_start = (F32)mTextRect.mLeft; F32 text_x = text_start + (mShowLineNumbers ? UI_TEXTEDITOR_LINE_NUMBER_MARGIN : 0); // draw the line numbers if( mShowLineNumbers && !cur_line_is_continuation) { const LLFontGL *num_font = LLFontGL::getFontMonospace(); F32 y_top = text_y + ((F32)ll_round(num_font->getLineHeight()) / 2); BOOL is_cur_line = getCurrentLine() == cur_line_num; const U8 style = is_cur_line ? LLFontGL::BOLD : LLFontGL::NORMAL; const LLColor4 fg_color = is_cur_line ? mCursorColor : mReadOnlyFgColor; num_font->renderUTF8( llformat("%*d", UI_TEXTEDITOR_LINE_NUMBER_DIGITS, cur_line_num), // string to draw 0, // begin offset 3., // x y_top, // y fg_color, LLFontGL::LEFT, // horizontal alignment LLFontGL::VCENTER, // vertical alignment style, LLFontGL::NO_SHADOW, S32_MAX, // max chars UI_TEXTEDITOR_LINE_NUMBER_MARGIN, NULL); // max pixels } // 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; } // Draw a segment within the line S32 clipped_end = llmin( line_end, cur_segment->getEnd() ); S32 clipped_len = clipped_end - seg_start; if( clipped_len > 0 ) { LLStyleSP style = cur_segment->getStyle(); if ( style->isImage() && (cur_segment->getStart() >= seg_start) && (cur_segment->getStart() <= clipped_end)) { S32 style_image_height = style->mImageHeight; S32 style_image_width = style->mImageWidth; LLUIImagePtr image = style->getImage(); image->draw(ll_round(text_x), ll_round(text_y)+line_height-style_image_height, style_image_width, style_image_height); } if (cur_segment == mHoverSegment && style->getIsEmbeddedItem()) { style->mUnderline = TRUE; } drawClippedSegment( text, seg_start, clipped_end, text_x, text_y, selection_left, selection_right, style, &text_x ); if( text_x == text_start && mShowLineNumbers ) { text_x += UI_TEXTEDITOR_LINE_NUMBER_MARGIN; } // Note: text_x is incremented by drawClippedSegment() seg_start += clipped_len; } } // move down one line text_y -= (F32)line_height; if( line_wraps ) { cur_line_num--; } cur_line_is_continuation = line_wraps; // so as to not not number the continuation lines line_start = next_start; cur_line++; cur_line_num++; } } // Draws a single text segment, reversing the color for selection if needed. void LLTextEditor::drawClippedSegment(const LLWString &text, S32 seg_start, S32 seg_end, F32 x, F32 y, S32 selection_left, S32 selection_right, const LLStyleSP& style, F32* right_x ) { if (!style->isVisible()) { return; } const LLFontGL* font = mGLFont; LLColor4 color = style->getColor(); if ( style->getFontString()[0] ) { font = LLResMgr::getInstance()->getRes(style->getFontID()); } U8 font_flags = LLFontGL::NORMAL; if (style->mBold) { font_flags |= LLFontGL::BOLD; } if (style->mItalic) { font_flags |= LLFontGL::ITALIC; } if (style->mUnderline) { font_flags |= LLFontGL::UNDERLINE; } if (style->getIsEmbeddedItem()) { if (mReadOnly) { color = LLUI::sColorsGroup->getColor("TextEmbeddedItemReadOnlyColor"); } else { color = LLUI::sColorsGroup->getColor("TextEmbeddedItemColor"); } } F32 y_top = y + (F32)ll_round(font->getLineHeight()); if( selection_left > seg_start ) { // Draw normally S32 start = seg_start; S32 end = llmin( selection_left, seg_end ); S32 length = end - start; font->render(text, start, x, y_top, color, LLFontGL::LEFT, LLFontGL::TOP, font_flags, LLFontGL::NO_SHADOW, length, S32_MAX, right_x, mAllowEmbeddedItems); } x = *right_x; if( (selection_left < seg_end) && (selection_right > seg_start) ) { // Draw reversed S32 start = llmax( selection_left, seg_start ); S32 end = llmin( selection_right, seg_end ); S32 length = end - start; font->render(text, start, x, y_top, LLColor4( 1.f - color.mV[0], 1.f - color.mV[1], 1.f - color.mV[2], 1.f ), LLFontGL::LEFT, LLFontGL::TOP, font_flags, LLFontGL::NO_SHADOW, length, S32_MAX, right_x, mAllowEmbeddedItems); } x = *right_x; if( selection_right < seg_end ) { // Draw normally S32 start = llmax( selection_right, seg_start ); S32 end = seg_end; S32 length = end - start; font->render(text, start, x, y_top, color, LLFontGL::LEFT, LLFontGL::TOP, font_flags, LLFontGL::NO_SHADOW, length, S32_MAX, right_x, mAllowEmbeddedItems); } } void LLTextEditor::draw() { // do on-demand reflow if (mReflowNeeded) { updateLineStartList(); mReflowNeeded = FALSE; } // then update scroll position, as cursor may have moved if (mScrollNeeded) { updateScrollFromCursor(); mScrollNeeded = FALSE; } { LLLocalClipRect clip(LLRect(0, getRect().getHeight(), getRect().getWidth() - (mScrollbar->getVisible() ? SCROLLBAR_SIZE : 0), 0)); bindEmbeddedChars(mGLFont); drawBackground(); drawSelectionBackground(); drawPreeditMarker(); drawText(); drawCursor(); drawMisspelled(); resetSpellDirty(); unbindEmbeddedChars(mGLFont); //RN: the decision was made to always show the orange border for keyboard focus but do not put an insertion caret // when in readonly mode mBorder->setKeyboardFocusHighlight( gFocusMgr.getKeyboardFocus() == this);// && !mReadOnly); } LLView::draw(); // Draw children (scrollbar and border) // remember if we are supposed to be at the bottom of the buffer mScrolledToBottom = isScrolledToBottom(); } void LLTextEditor::onTabInto() { // selecting all on tabInto causes users to hit tab twice and replace their text with a tab character // theoretically, one could selectAll if mTabsToNextField is true, but we couldn't think of a use case // where you'd want to select all anyway // preserve insertion point when returning to the editor //selectAll(); } // virtual void LLTextEditor::clear() { setText(LLStringUtil::null); mSegments.clear(); } // Start or stop the editor from accepting text-editing keystrokes // see also LLLineEditor void LLTextEditor::setFocus( BOOL new_state ) { BOOL old_state = hasFocus(); // Don't change anything if the focus state didn't change if (new_state == old_state) return; // Notify early if we are losing focus. if (!new_state) { getWindow()->allowLanguageTextInput(this, FALSE); } LLUICtrl::setFocus( new_state ); if( new_state ) { // Route menu to this class gEditMenuHandler = this; // Don't start the cursor flashing right away resetKeystrokeTimer(); } else { // Route menu back to the default if( gEditMenuHandler == this ) { gEditMenuHandler = NULL; } endSelection(); } } // virtual BOOL LLTextEditor::acceptsTextInput() const { return !mReadOnly; } // Given a line (from the start of the doc) and an offset into the line, find the offset (pos) into text. S32 LLTextEditor::getPos( S32 line, S32 offset ) { S32 line_start = getLineStart(line); S32 next_start = getLineStart(line+1); if (next_start == line_start) { next_start = getLength() + 1; } S32 line_length = next_start - line_start - 1; line_length = llmax(line_length, 0); return line_start + llmin( offset, line_length ); } void LLTextEditor::changePage( S32 delta ) { S32 line, offset; getLineAndOffset( mCursorPos, &line, &offset ); // get desired x position to remember previous position S32 desired_x_pixel = mDesiredXPixel; // allow one line overlap S32 page_size = mScrollbar->getPageSize() - 1; if( delta == -1 ) { line = llmax( line - page_size, 0); setCursorPos(getPos( line, offset )); mScrollbar->setDocPos( mScrollbar->getDocPos() - page_size ); } else if( delta == 1 ) { setCursorPos(getPos( line + page_size, offset )); mScrollbar->setDocPos( mScrollbar->getDocPos() + page_size ); } // put desired position into remember-buffer after setCursorPos() mDesiredXPixel = desired_x_pixel; if (mOnScrollEndCallback && mOnScrollEndData && (mScrollbar->getDocPos() == mScrollbar->getDocPosMax())) { mOnScrollEndCallback(mOnScrollEndData); } } void LLTextEditor::changeLine( S32 delta ) { bindEmbeddedChars(mGLFont); S32 line, offset; getLineAndOffset( mCursorPos, &line, &offset ); S32 line_start = getLineStart(line); // set desired x position to remembered previous position S32 desired_x_pixel = mDesiredXPixel; // if remembered position was reset (thus -1), calculate new one here if( desired_x_pixel == -1 ) { desired_x_pixel = mGLFont->getWidth(mWText.c_str(), line_start, offset, mAllowEmbeddedItems ); } S32 new_line = 0; if( (delta < 0) && (line > 0 ) ) { new_line = line - 1; } else if( (delta > 0) && (line < (getLineCount() - 1)) ) { new_line = line + 1; } else { unbindEmbeddedChars(mGLFont); return; } S32 num_lines = getLineCount(); S32 new_line_start = getLineStart(new_line); S32 new_line_end = getLength(); if (new_line + 1 < num_lines) { new_line_end = getLineStart(new_line + 1) - 1; } S32 new_line_len = new_line_end - new_line_start; S32 new_offset; new_offset = mGLFont->charFromPixelOffset(mWText.c_str(), new_line_start, (F32)desired_x_pixel, (F32)mTextRect.getWidth(), new_line_len, mAllowEmbeddedItems); setCursorPos (getPos( new_line, new_offset )); // put desired position into remember-buffer after setCursorPos() mDesiredXPixel = desired_x_pixel; unbindEmbeddedChars(mGLFont); } BOOL LLTextEditor::isScrolledToTop() { return mScrollbar->isAtBeginning(); } BOOL LLTextEditor::isScrolledToBottom() { return mScrollbar->isAtEnd(); } void LLTextEditor::startOfLine() { S32 line, offset; getLineAndOffset( mCursorPos, &line, &offset ); setCursorPos(mCursorPos - offset); } // public void LLTextEditor::setCursorAndScrollToEnd() { deselect(); endOfDoc(); needsScroll(); } void LLTextEditor::scrollToPos(S32 pos) { mScrollbar->setDocSize( getLineCount() ); S32 line, offset; getLineAndOffset(pos, &line, &offset ); S32 page_size = mScrollbar->getPageSize(); if( line < mScrollbar->getDocPos() ) { // scroll so that the cursor is at the top of the page mScrollbar->setDocPos( line ); } else if( line >= mScrollbar->getDocPos() + page_size - 1 ) { S32 new_pos = 0; if( line < mScrollbar->getDocSize() - 1 ) { // scroll so that the cursor is one line above the bottom of the page, new_pos = line - page_size + 1; } else { // if there is less than a page of text remaining, scroll so that the cursor is at the bottom new_pos = mScrollbar->getDocPosMax(); } mScrollbar->setDocPos( new_pos ); } // Check if we've scrolled to bottom for callback if asked for callback if (mOnScrollEndCallback && mOnScrollEndData && (mScrollbar->getDocPos() == mScrollbar->getDocPosMax())) { mOnScrollEndCallback(mOnScrollEndData); } } void LLTextEditor::getLineAndColumnForPosition( S32 position, S32* line, S32* col, BOOL include_wordwrap ) { if( include_wordwrap ) { getLineAndOffset( mCursorPos, line, col ); } else { const LLWString &text = mWText; S32 line_count = 0; S32 line_start = 0; S32 i; for( i = 0; text[i] && (i < position); i++ ) { if( '\n' == text[i] ) { line_start = i + 1; line_count++; } } *line = line_count; *col = i - line_start; } } void LLTextEditor::getCurrentLineAndColumn( S32* line, S32* col, BOOL include_wordwrap ) { getLineAndColumnForPosition(mCursorPos, line, col, include_wordwrap); } S32 LLTextEditor::getCurrentLine() { return getLineForPosition(mCursorPos); } S32 LLTextEditor::getLineForPosition(S32 position) { S32 line, col; getLineAndColumnForPosition(position, &line, &col, FALSE); return line; } void LLTextEditor::endOfLine() { S32 line, offset; getLineAndOffset( mCursorPos, &line, &offset ); S32 num_lines = getLineCount(); if (line + 1 >= num_lines) { setCursorPos(getLength()); } else { setCursorPos( getLineStart(line + 1) - 1 ); } } void LLTextEditor::endOfDoc() { mScrollbar->setDocPos(mScrollbar->getDocPosMax()); mScrolledToBottom = true; S32 len = getLength(); if( len ) { setCursorPos(len); } if (mOnScrollEndCallback && mOnScrollEndData && (mScrollbar->getDocPos() == mScrollbar->getDocPosMax())) { mOnScrollEndCallback(mOnScrollEndData); } } // Sets the scrollbar from the cursor position void LLTextEditor::updateScrollFromCursor() { if (mReadOnly) { // no cursor in read only mode return; } scrollToPos(mCursorPos); } void LLTextEditor::reshape(S32 width, S32 height, BOOL called_from_parent) { LLView::reshape( width, height, called_from_parent ); // do this first after reshape, because other things depend on // up-to-date mTextRect updateTextRect(); needsReflow(); // propagate shape information to scrollbar mScrollbar->setDocSize( getLineCount() ); S32 line_height = ll_round( mGLFont->getLineHeight() ); S32 page_lines = mTextRect.getHeight() / line_height; mScrollbar->setPageSize( page_lines ); } void LLTextEditor::autoIndent() { // Count the number of spaces in the current line S32 line, offset; getLineAndOffset( mCursorPos, &line, &offset ); S32 line_start = getLineStart(line); S32 space_count = 0; S32 i; const LLWString &text = mWText; while( ' ' == text[line_start] ) { space_count++; line_start++; } // If we're starting a braced section, indent one level. if( (mCursorPos > 0) && (text[mCursorPos -1] == '{') ) { space_count += SPACES_PER_TAB; } // Insert that number of spaces on the new line addChar( '\n' ); for( i = 0; i < space_count; i++ ) { addChar( ' ' ); } } // Inserts new text at the cursor position void LLTextEditor::insertText(const std::string &new_text,BOOL deleteCurrentSelection) { BOOL enabled = getEnabled(); setEnabled( TRUE ); // Delete any selected characters (the insertion replaces them) if( hasSelection() && (deleteCurrentSelection)) { deleteSelection(TRUE); } setCursorPos(mCursorPos + insert( mCursorPos, utf8str_to_wstring(new_text), FALSE )); needsReflow(); setEnabled( enabled ); } void LLTextEditor::appendColoredText(const std::string &new_text, bool allow_undo, bool prepend_newline, const LLColor4 &color, const std::string& font_name) { LLColor4 lcolor=color; // If LindenUserDir is empty then we didn't login yet. // In that case we can't instantiate LLTextParser, which // is initialized per user. if (mParseHighlights && !gDirUtilp->getLindenUserDir(true).empty()) { LLTextParser* highlight = LLTextParser::getInstance(); highlight->parseFullLineHighlights(new_text, &lcolor); } LLStyleSP style(new LLStyle); style->setVisible(true); style->setColor(lcolor); style->setFontName(font_name); appendText(new_text, allow_undo, prepend_newline, style); } static LLTrace::BlockTimerStatHandle FTM_APPEND_TEXT("Append Text"); void LLTextEditor::appendText(const std::string &new_text, bool allow_undo, bool prepend_newline, const LLStyleSP style, bool force_replace_links) { LL_RECORD_BLOCK_TIME(FTM_APPEND_TEXT); if (new_text.empty()) return; std::string text = prepend_newline && !mWText.empty() ? ('\n' + new_text) : new_text; appendTextImpl(text, style); if (!allow_undo) { blockUndo(); } } static LLTrace::BlockTimerStatHandle FTM_PARSE_HTML("Parse HTML"); // Appends new text to end of document void LLTextEditor::appendTextImpl(const std::string &new_text, const LLStyleSP style, bool force_replace_links) { std::string text = new_text; static const LLUICachedControl replace_links("SinguReplaceLinks"); force_replace_links = force_replace_links || replace_links; bool is_link = style && style->isLink(); // Don't search for URLs inside a link segment (STORM-358). S32 part = (S32)LLTextParser::WHOLE; if (mReadOnly && mParseHTML && !is_link) // Singu Note: Do not replace html if the user is going to edit it. (Like in profiles) { LL_RECORD_BLOCK_TIME(FTM_PARSE_HTML); S32 start=0,end=0; LLUrlMatch match; auto append_substr = [&](const size_t& pos, const size_t& count) { appendAndHighlightText(text.substr(pos, count), part, style); }; auto append_link = [&](const std::string& link, LLStyleSP link_style) { if (style) // Respect styling { const auto& text_style = *style; link_style->mItalic = text_style.mItalic; link_style->mBold = text_style.mBold; link_style->mUnderline = text_style.mUnderline; } // Hack around colors looking bad on some backgrounds by allowing setting link color for this editor if (mLinkColor) link_style->setColor(*mLinkColor); const auto always_underline(always_underline_links()); if (always_underline) link_style->mUnderline = true; appendAndHighlightText(link, part, link_style, !always_underline/*match.underlineOnHoverOnly()*/); }; const auto&& cb = force_replace_links ? boost::bind(&LLTextEditor::replaceUrl, this, _1, _2, _3) : LLUrlLabelCallback::slot_function_type(); while (!text.empty() && LLUrlRegistry::instance().findUrl(text, match, cb)) { start = match.getStart(); end = match.getEnd()+1; // 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; } append_substr(0, start); } auto url = match.getUrl(); const auto& label = match.getLabel(); if (force_replace_links || replace_links || url == label) { // add icon before url if need /* Singu TODO: Icons next to links? LLTextUtil::processUrlMatch(&match, this, isContentTrusted() || match.isTrusted()); if ((isContentTrusted() || match.isTrusted()) && !match.getIcon().empty() ) { link_style->setImage(match.getIcon()); setLastSegmentToolTip(LLTrans::getString("TooltipSLIcon")); }*/ // output the styled url append_link(label + match.getQuery(), match.getStyle()); bool tooltip_required = !match.getTooltip().empty(); // set the tooltip for the Url label if (tooltip_required) { setLastSegmentToolTip(match.getTooltip()); } /* Singu Note: Upstream uses hardcoded Grey here, they have no care for skins, this could be awful! Also it splits up the link! For now just make it append it to the same segment with its label above. // show query part of url with gray color only for LLUrlEntryHTTP and LLUrlEntryHTTPNoProtocol url entries std::string label = match.getQuery(); if (!label.empty()) { link_params.color = LLColor4::grey; link_params.readonly_color = LLColor4::grey; appendAndHighlightTextImpl(label, part, link_params, match.underlineOnHoverOnly()); append_link(label); // set the tooltip for the query part of url if (tooltip_required) { setLastSegmentToolTip(match.getTooltip()); } }*/ } else // Still link the link itself { const auto pos = text.find(url); bool fallback(pos == std::string::npos); // In special cases like no protocol and brackets bool brackets(fallback && text.find('[') == start); // Brackets if (fallback == brackets && start < pos) // Leading text, only in special case if brackets present append_substr(start, brackets ? 1 : pos-start); // In the special cases, only link exactly the url, this might not have a protocol so calculate the exact string if (fallback) url = brackets ? text.substr(start+1, text.find(' ', start+2)-start) : text.substr(start, end-start); append_link(url, match.getStyle()); // Append the link const auto url_end = pos + url.size(); if (fallback == brackets && end > url_end) // Ending text, only in special case if brackets present append_substr(url_end, end-url_end); } // 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); } else { appendAndHighlightText(text, part, style); } } void LLTextEditor::setLastSegmentToolTip(const std::string &tooltip) { LLTextSegmentPtr segment = mSegments.back(); segment->setToolTip(tooltip); } void LLTextEditor::appendLineBreakSegment(/*const LLStyle::Params& style_params*/) { /* segment_vec_t segments; LLStyleSP sp(new LLStyle(style_params)); const auto& length = getLength(); segments.push_back(new LLTextSegment(sp, length, length + 1));*/ insertStringNoUndo(getLength(), LLWString(1, '\n')); } void LLTextEditor::appendAndHighlightText(const std::string& new_text, S32 highlight_part, const LLStyleSP stylep, bool underline_on_hover) { if (new_text.empty()) return; std::string::size_type start = 0; /*std::string::size_type pos = new_text.find('\n',start); while(pos!=-1) { if(pos!=start) { std::string str = std::string(new_text,start,pos-start); appendAndHighlightTextImpl(str,highlight_part, stylep); } appendLineBreakSegment(); 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, stylep, underline_on_hover); } void LLTextEditor::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 for (auto it = mSegments.begin(); it != mSegments.end(); ++it) { LLTextSegment *seg = *it; LLStyleSP style = seg->getStyle(); // update segment start/end length in case we replaced text earlier S32 seg_length = seg->getEnd() - seg->getStart(); seg->setStart(seg_start); seg->setEnd(seg_start + seg_length); // if we find a link with our Url, then replace the label if (style->getLinkHREF() == url) { S32 start = seg->getStart(); S32 end = seg->getEnd(); const auto& old_label = text.substr(start, end - start); if (wlabel != old_label) { const auto difference = std::abs((S32)wlabel.length() - (S32)old_label.length()); if (mSelectionEnd >= end) // Selection stays at/after end { mSelectionEnd += difference; if (mSelectionStart >= end) mSelectionStart += difference; } if (mCursorPos >= end) // Cursor stays at/after end mCursorPos += difference; text.replace(start, end - start, wlabel); seg->setEnd(start + wlabel.size()); modified = true; } } /* Singu TODO: Icons with Urls? // 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) { mWText = text; mTextIsUpToDate = FALSE; setCursorPos(mCursorPos); needsReflow(); } } void LLTextEditor::appendAndHighlightTextImpl(const std::string& new_text, S32 highlight_part, const LLStyleSP stylep, bool underline_on_hover) { // Save old state BOOL was_scrolled_to_bottom = (mScrollbar->getDocPos() == mScrollbar->getDocPosMax()); 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); // This is where we appendHighlightedText // If LindenUserDir is empty then we didn't login yet. // In that case we can't instantiate LLTextParser, which is initialized per user. if (mParseHighlights && !gDirUtilp->getLindenUserDir(true).empty()) { LLTextParser* highlight = LLTextParser::getInstance(); if (highlight && stylep) { LLStyleSP highlight_params(new LLStyle(*stylep)); LLSD pieces = highlight->parsePartialLineHighlights(new_text, highlight_params->getColor(), (LLTextParser::EHighlightPosition)highlight_part); for (S32 i=0;isetColor(lcolor); LLWString wide_text; wide_text = utf8str_to_wstring(pieces[i]["text"].asString()); S32 cur_length = getLength(); insertStringNoUndo(cur_length, wide_text); LLStyleSP sp(new LLStyle(*highlight_params)); LLTextSegmentPtr segmentp; segmentp = new LLTextSegment(sp, cur_length, cur_length + wide_text.size()); if (underline_on_hover) segmentp->setUnderlineOnHover(true); mSegments.push_back(segmentp); } return; } } //else { LLWString wide_text; wide_text = utf8str_to_wstring(new_text); auto length = getLength(); auto insert_len = length + insertStringNoUndo(length, utf8str_to_wstring(new_text)); if (stylep) { S32 segment_start = old_length; S32 segment_end = old_length + wide_text.size(); LLTextSegmentPtr segment = new LLTextSegment(stylep, segment_start, segment_end); if (underline_on_hover) segment->setUnderlineOnHover(true); mSegments.push_back(segment); } else // If no style, still make a segment, { auto segment = mSegments.empty() ? nullptr : mSegments.back(); if (!segment || !segment->getIsDefault()) { LLColor4& text_color = (mReadOnly ? mReadOnlyFgColor : mFgColor); LLTextSegmentPtr segment = new LLTextSegment(text_color, length, insert_len); segment->setIsDefault(true); // call it a default segment so we can consolidate later. mSegments.push_back(segment); } else // It's later! { segment->setEnd(insert_len); } } } // Set the cursor and scroll position // Maintain the scroll position unless the scroll was at the end of the doc (in which // case, move it to the new end of the doc) or unless the user was doing actively selecting if( was_scrolled_to_bottom && !was_selecting ) { if( selection_start != selection_end ) { // maintain an existing non-active selection mSelectionStart = selection_start; mSelectionEnd = selection_end; } endOfDoc(); } else 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 LLTextEditor::removeTextFromEnd(S32 num_chars) { if (num_chars <= 0) return; num_chars = llclamp(num_chars, 0, getLength()); remove(getLength() - num_chars, num_chars, FALSE); S32 len = getLength(); mCursorPos = llclamp(mCursorPos, 0, len); mSelectionStart = llclamp(mSelectionStart, 0, len); mSelectionEnd = llclamp(mSelectionEnd, 0, len); pruneSegments(); // pruneSegments will invalidate mLineStartList. updateLineStartList(); needsScroll(); } /////////////////////////////////////////////////////////////////// // Returns change in number of characters in mWText S32 LLTextEditor::insertStringNoUndo(const S32 pos, const LLWString &wstr) { S32 old_len = mWText.length(); // length() returns character length S32 insert_len = wstr.length(); mWText.insert(pos, wstr); mTextIsUpToDate = FALSE; //HACK: If we are readonly we shouldn't need to truncate if (!mReadOnly && truncate()) { // The user's not getting everything he's hoping for make_ui_sound("UISndBadKeystroke"); insert_len = mWText.length() - old_len; } needsReflow(/*pos*/); return insert_len; } S32 LLTextEditor::removeStringNoUndo(S32 pos, S32 length) { auto seg_iter = getSegIterContaining(pos); S32 end = pos + length; while(seg_iter != mSegments.end()) { LLTextSegmentPtr segmentp = *seg_iter; 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 seg_iter = mSegments.erase(seg_iter); 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; } mWText.erase(pos, length); // recreate default segment in case we erased everything createDefaultSegment(); mTextIsUpToDate = FALSE; needsReflow(/*pos*/); return -length; // This will be wrong if someone calls removeStringNoUndo with an excessive length } S32 LLTextEditor::overwriteCharNoUndo(S32 pos, llwchar wc) { if (pos > (S32)mWText.length()) { return 0; } mWText[pos] = wc; mTextIsUpToDate = FALSE; return 1; } //---------------------------------------------------------------------------- void LLTextEditor::makePristine() { mPristineCmd = mLastCmd; mBaseDocIsPristine = !mLastCmd; // Create a clean partition in the undo stack. We don't want a single command to extend from // the "pre-pristine" state to the "post-pristine" state. if( mLastCmd ) { mLastCmd->blockExtensions(); } } BOOL LLTextEditor::isPristine() const { if( mPristineCmd ) { return (mPristineCmd == mLastCmd); } else { // No undo stack, so check if the version before and commands were done was the original version return !mLastCmd && mBaseDocIsPristine; } } BOOL LLTextEditor::tryToRevertToPristineState() { if( !isPristine() ) { deselect(); S32 i = 0; while( !isPristine() && canUndo() ) { undo(); i--; } while( !isPristine() && canRedo() ) { redo(); i++; } if( !isPristine() ) { // failed, so go back to where we started while( i > 0 ) { undo(); i--; } } needsReflow(); } return isPristine(); // TRUE => success } void LLTextEditor::updateTextRect() { mTextRect.setOriginAndSize( UI_TEXTEDITOR_BORDER + UI_TEXTEDITOR_H_PAD, UI_TEXTEDITOR_BORDER, getRect().getWidth() - SCROLLBAR_SIZE - 2 * (UI_TEXTEDITOR_BORDER + UI_TEXTEDITOR_H_PAD), getRect().getHeight() - 2 * UI_TEXTEDITOR_BORDER - UI_TEXTEDITOR_V_PAD_TOP ); } void LLTextEditor::loadKeywords(const std::string& filename, const std::vector& funcs, const std::vector& tooltips, const LLColor3& color) { if(mKeywords.loadFromFile(filename)) { S32 count = llmin(funcs.size(), tooltips.size()); for(S32 i = 0; i < count; i++) { std::string name = utf8str_trim(funcs[i]); mKeywords.addToken(LLKeywordToken::WORD, name, color, tooltips[i] ); } segment_list_t segment_list; mKeywords.findSegments(&segment_list, getWText(), mDefaultColor); mSegments.clear(); segment_list_t::iterator insert_it = mSegments.begin(); for (segment_list_t::iterator list_it = segment_list.begin(); list_it != segment_list.end(); ++list_it) { insert_it = mSegments.insert(insert_it, *list_it); } } } static LLTrace::BlockTimerStatHandle FTM_SYNTAX_HIGHLIGHTING("Syntax Highlighting"); static LLTrace::BlockTimerStatHandle FTM_UPDATE_TEXT_SEGMENTS("Update Text Segments"); void LLTextEditor::createDefaultSegment() { if (mSegments.empty()) { LLColor4& text_color = (mReadOnly ? mReadOnlyFgColor : mFgColor); LLTextSegmentPtr default_segment = new LLTextSegment(text_color, 0, mWText.length()); default_segment->setIsDefault(TRUE); mSegments.push_back(default_segment); } } void LLTextEditor::clearSegments() { mLineStartList.clear(); mSegments.clear(); createDefaultSegment(); } void LLTextEditor::updateSegments() { { LL_RECORD_BLOCK_TIME(FTM_SYNTAX_HIGHLIGHTING); if (mKeywords.isLoaded()) { // HACK: No non-ascii keywords for now mKeywords.findSegments(&mSegments, mWText, mDefaultColor); } else if (mAllowEmbeddedItems) { findEmbeddedItemSegments(); } } LL_RECORD_BLOCK_TIME(FTM_UPDATE_TEXT_SEGMENTS); // Make sure we have at least one segment if (mSegments.size() == 1 && mSegments[0]->getIsDefault()) { clearSegments(); // create default segment } else createDefaultSegment(); } // Only effective if text was removed from the end of the editor // *NOTE: Using this will invalidate references to mSegments from mLineStartList. void LLTextEditor::pruneSegments() { S32 len = mWText.length(); // Find and update the first valid segment segment_list_t::iterator iter = mSegments.end(); while(iter != mSegments.begin()) { --iter; LLTextSegment* seg = *iter; if (seg->getStart() < len) { // valid segment if (seg->getEnd() > len) { seg->setEnd(len); } break; // done } } if (iter != mSegments.end()) { // erase invalid segments ++iter; mSegments.erase(iter, mSegments.end()); } else { LL_WARNS() << "Tried to erase end of empty LLTextEditor" << LL_ENDL; } } void LLTextEditor::findEmbeddedItemSegments() { mHoverSegment = NULL; mSegments.clear(); BOOL found_embedded_items = FALSE; const LLWString &text = mWText; S32 idx = 0; while( text[idx] ) { if( text[idx] >= FIRST_EMBEDDED_CHAR && text[idx] <= LAST_EMBEDDED_CHAR ) { found_embedded_items = TRUE; break; } ++idx; } if( !found_embedded_items ) { return; } S32 text_len = text.length(); BOOL in_text = FALSE; LLColor4& text_color = ( mReadOnly ? mReadOnlyFgColor : mFgColor ); if( idx > 0 ) { mSegments.push_back( new LLTextSegment( text_color, 0, text_len ) ); // text in_text = TRUE; } LLStyleSP embedded_style(new LLStyle); embedded_style->setIsEmbeddedItem( TRUE ); // Start with i just after the first embedded item while ( text[idx] ) { if( text[idx] >= FIRST_EMBEDDED_CHAR && text[idx] <= LAST_EMBEDDED_CHAR ) { if( in_text ) { mSegments.back()->setEnd( idx ); } mSegments.push_back( new LLTextSegment( embedded_style, idx, idx + 1 ) ); // item in_text = FALSE; } else if( !in_text ) { mSegments.push_back( new LLTextSegment( text_color, idx, text_len ) ); // text in_text = TRUE; } ++idx; } } BOOL LLTextEditor::handleMouseUpOverSegment(S32 x, S32 y, MASK mask) { if ( hasMouseCapture() ) { // This mouse up was part of a click. // Regardless of where the cursor is, see if we recently touched a link // and launch it if we did. if (mParseHTML) { if (auto segment = getSegmentAtLocalPos(x, y)) { if (auto style = segment->getStyle()) { if (style->isLink()) LLUrlAction::clickAction(style->getLinkHREF(), true); } } } } return FALSE; } LLTextEditor::segment_list_t::iterator LLTextEditor::getSegIterContaining(S32 index) { S32 text_len = getLength(); if (index > text_len) { return mSegments.end(); } // when there are no segments, we return the end iterator, which must be checked by caller if (mSegments.size() <= 1) { return mSegments.begin(); } LLPointer index_segment = new LLTextSegment(index); auto it = std::lower_bound(mSegments.begin(), mSegments.end(), index_segment, LLTextSegment::compare()); return it; } LLTextEditor::segment_list_t::const_iterator LLTextEditor::getSegIterContaining(S32 index) const { S32 text_len = getLength(); if (index > text_len) { return mSegments.end(); } // when there are no segments, we return the end iterator, which must be checked by caller if (mSegments.size() <= 1) { return mSegments.begin(); } LLPointer index_segment = new LLTextSegment(index); auto it = std::lower_bound(mSegments.begin(), mSegments.end(), index_segment, LLTextSegment::compare()); return it; } // Finds the text segment (if any) at the give local screen position LLTextSegment* LLTextEditor::getSegmentAtLocalPos( S32 x, S32 y ) const { // Find the cursor position at the requested local screen position S32 offset = getCursorPosFromLocalCoord( x, y, FALSE ); S32 idx = getSegmentIdxAtOffset(offset); return idx >= 0 ? mSegments[idx] : LLTextSegmentPtr(); } const LLTextSegment* LLTextEditor::getSegmentAtOffset(S32 offset) const { S32 idx = getSegmentIdxAtOffset(offset); return idx >= 0 ? mSegments[idx] : LLTextSegmentPtr(); } S32 LLTextEditor::getSegmentIdxAtOffset(S32 offset) const { if (mSegments.empty() || offset < 0 || offset >= getLength()) { return -1; } else { S32 segidx, segoff; getSegmentAndOffset(offset, &segidx, &segoff); return segidx; } } void LLTextEditor::onMouseCaptureLost() { endSelection(); } void LLTextEditor::setOnScrollEndCallback(void (*callback)(void*), void* userdata) { mOnScrollEndCallback = callback; mOnScrollEndData = userdata; mScrollbar->setOnScrollEndCallback(callback, userdata); } /////////////////////////////////////////////////////////////////// // Hack for Notecards BOOL LLTextEditor::importBuffer(const char* buffer, S32 length ) { std::istringstream instream(buffer); // Version 1 format: // Linden text version 1\n // {\n // // Text length \n // (text may contain ext_char_values) // }\n char tbuf[MAX_STRING]; /* Flawfinder: ignore */ S32 version = 0; instream.getline(tbuf, MAX_STRING); if( 1 != sscanf(tbuf, "Linden text version %d", &version) ) { LL_WARNS() << "Invalid Linden text file header " << LL_ENDL; return FALSE; } if( 1 != version ) { LL_WARNS() << "Invalid Linden text file version: " << version << LL_ENDL; return FALSE; } instream.getline(tbuf, MAX_STRING); if( 0 != sscanf(tbuf, "{") ) { LL_WARNS() << "Invalid Linden text file format" << LL_ENDL; return FALSE; } S32 text_len = 0; instream.getline(tbuf, MAX_STRING); if( 1 != sscanf(tbuf, "Text length %d", &text_len) ) { LL_WARNS() << "Invalid Linden text length field" << LL_ENDL; return FALSE; } if( text_len > mMaxTextByteLength ) { LL_WARNS() << "Invalid Linden text length: " << text_len << LL_ENDL; return FALSE; } BOOL success = TRUE; char* text = new char[ text_len + 1]; if (text == NULL) { LL_ERRS() << "Memory allocation failure." << LL_ENDL; return FALSE; } instream.get(text, text_len + 1, '\0'); text[text_len] = '\0'; if( text_len != (S32)strlen(text) )/* Flawfinder: ignore */ { LL_WARNS() << llformat("Invalid text length: %d != %d ",strlen(text),text_len) << LL_ENDL;/* Flawfinder: ignore */ success = FALSE; } instream.getline(tbuf, MAX_STRING); if( success && (0 != sscanf(tbuf, "}")) ) { LL_WARNS() << "Invalid Linden text file format: missing terminal }" << LL_ENDL; success = FALSE; } if( success ) { // Actually set the text setText( LLStringExplicit(text) ); } delete[] text; setCursorPos(0); deselect(); needsReflow(); return success; } BOOL LLTextEditor::exportBuffer(std::string &buffer ) { std::ostringstream outstream(buffer); outstream << "Linden text version 1\n"; outstream << "{\n"; outstream << llformat("Text length %d\n", mWText.length() ); outstream << getText(); outstream << "}\n"; return TRUE; } boost::signals2::connection LLTextEditor::setIsFriendCallback(const is_friend_signal_t::slot_type& cb) { if (!mIsFriendSignal) { mIsFriendSignal = new is_friend_signal_t(); } return mIsFriendSignal->connect(cb); } boost::signals2::connection LLTextEditor::setIsObjectBlockedCallback(const is_blocked_signal_t::slot_type& cb) { if (!mIsObjectBlockedSignal) { mIsObjectBlockedSignal = new is_blocked_signal_t(); } return mIsObjectBlockedSignal->connect(cb); } ////////////////////////////////////////////////////////////////////////// // LLTextSegment LLTextSegment::LLTextSegment(S32 start) : mStart(start), mEnd(0), mToken(NULL), mIsDefault(FALSE) { } LLTextSegment::LLTextSegment( const LLStyleSP& style, S32 start, S32 end ) : mStyle( style ), mStart( start), mEnd( end ), mToken(NULL), mIsDefault(FALSE) { } LLTextSegment::LLTextSegment( const LLColor4& color, S32 start, S32 end, BOOL is_visible) : mStyle(new LLStyle(is_visible,color,LLStringUtil::null)), mStart( start), mEnd( end ), mToken(NULL), mIsDefault(FALSE) { } LLTextSegment::LLTextSegment( const LLColor4& color, S32 start, S32 end ) : mStyle(new LLStyle(TRUE, color,LLStringUtil::null )), mStart( start), mEnd( end ), mToken(NULL), mIsDefault(FALSE) { } LLTextSegment::LLTextSegment( const LLColor3& color, S32 start, S32 end ) : mStyle(new LLStyle(TRUE, color,LLStringUtil::null )), mStart( start), mEnd( end ), mToken(NULL), mIsDefault(FALSE) { } BOOL LLTextSegment::getToolTip(std::string& msg) const { // do we have a tooltip for a loaded keyword (for script editor)? if (mToken && !mToken->getToolTip().empty()) { const LLWString& wmsg = mToken->getToolTip(); msg = wstring_to_utf8str(wmsg); return TRUE; } // or do we have an explicitly set tooltip (e.g., for Urls) if (!mTooltip.empty()) { msg = mTooltip; return TRUE; } return FALSE; } void LLTextSegment::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; } void LLTextSegment::dump() const { LL_INFOS() << "Segment [" << // mColor.mV[VX] << ", " << // mColor.mV[VY] << ", " << // mColor.mV[VZ] << "]\t[" << mStart << ", " << getEnd() << "]" << LL_ENDL; } // virtual LLXMLNodePtr LLTextEditor::getXML(bool save_children) const { LLXMLNodePtr node = LLUICtrl::getXML(); node->setName(LL_SIMPLE_TEXT_EDITOR_TAG); // Attributes node->createChild("max_length", TRUE)->setIntValue(getMaxLength()); node->createChild("embedded_items", TRUE)->setBoolValue(mAllowEmbeddedItems); node->createChild("font", TRUE)->setStringValue(LLFontGL::nameFromFont(mGLFont)); node->createChild("word_wrap", TRUE)->setBoolValue(mWordWrap); node->createChild("hide_scrollbar", TRUE)->setBoolValue(mHideScrollbarForShortDocs); node->createChild("hide_border", TRUE)->setBoolValue(!mBorder->getVisible()); addColorXML(node, mCursorColor, "cursor_color", "TextCursorColor"); addColorXML(node, mFgColor, "text_color", "TextFgColor"); addColorXML(node, mDefaultColor, "text_default_color", "TextDefaultColor"); addColorXML(node, mReadOnlyFgColor, "text_readonly_color", "TextFgReadOnlyColor"); addColorXML(node, mReadOnlyBgColor, "bg_readonly_color", "TextBgReadOnlyColor"); addColorXML(node, mWriteableBgColor, "bg_writeable_color", "TextBgWriteableColor"); addColorXML(node, mFocusBgColor, "bg_focus_color", "TextBgFocusColor"); // Contents node->setStringValue(getText()); return node; } void LLTextEditor::setTextEditorParameters(LLXMLNodePtr node) { BOOL word_wrap = FALSE; node->getAttributeBOOL("word_wrap", word_wrap); setWordWrap(word_wrap); node->getAttributeBOOL("show_line_numbers", mShowLineNumbers); node->getAttributeBOOL("track_bottom", mTrackBottom); node->getAttributeBOOL("spell_check", mSpellCheckable); LLColor4 color; if (LLUICtrlFactory::getAttributeColor(node,"cursor_color", color)) { setCursorColor(color); } if(LLUICtrlFactory::getAttributeColor(node,"text_color", color)) { setFgColor(color); } if(LLUICtrlFactory::getAttributeColor(node,"text_readonly_color", color)) { setReadOnlyFgColor(color); } if(LLUICtrlFactory::getAttributeColor(node,"bg_readonly_color", color)) { setReadOnlyBgColor(color); } if(LLUICtrlFactory::getAttributeColor(node,"bg_writeable_color", color)) { setWriteableBgColor(color); } } void LLTextEditor::updateAllowingLanguageInput() { if (hasFocus() && !mReadOnly) { getWindow()->allowLanguageTextInput(this, TRUE); } else { getWindow()->allowLanguageTextInput(this, FALSE); } } // Preedit is managed off the undo/redo command stack. BOOL LLTextEditor::hasPreeditString() const { return (mPreeditPositions.size() > 1); } void LLTextEditor::resetPreedit() { if (hasPreeditString()) { if (hasSelection()) { LL_WARNS() << "Preedit and selection!" << LL_ENDL; deselect(); } mCursorPos = mPreeditPositions.front(); removeStringNoUndo(mCursorPos, mPreeditPositions.back() - mCursorPos); insertStringNoUndo(mCursorPos, mPreeditOverwrittenWString); mPreeditWString.clear(); mPreeditOverwrittenWString.clear(); mPreeditPositions.clear(); // A call to updatePreedit should soon follow under a // normal course of operation, so we don't need to // maintain internal variables such as line start // positions now. } } void LLTextEditor::updatePreedit(const LLWString &preedit_string, const segment_lengths_t &preedit_segment_lengths, const standouts_t &preedit_standouts, S32 caret_position) { // Just in case. if (mReadOnly) { return; } getWindow()->hideCursorUntilMouseMove(); S32 insert_preedit_at = mCursorPos; mPreeditWString = preedit_string; mPreeditPositions.resize(preedit_segment_lengths.size() + 1); S32 position = insert_preedit_at; for (segment_lengths_t::size_type i = 0; i < preedit_segment_lengths.size(); i++) { mPreeditPositions[i] = position; position += preedit_segment_lengths[i]; } mPreeditPositions.back() = position; if (LL_KIM_OVERWRITE == gKeyboard->getInsertMode()) { mPreeditOverwrittenWString = getWSubString(insert_preedit_at, mPreeditWString.length()); removeStringNoUndo(insert_preedit_at, mPreeditWString.length()); } else { mPreeditOverwrittenWString.clear(); } insertStringNoUndo(insert_preedit_at, mPreeditWString); mPreeditStandouts = preedit_standouts; needsReflow(); setCursorPos(insert_preedit_at + caret_position); // Update of the preedit should be caused by some key strokes. mKeystrokeTimer.reset(); onKeyStroke(); } BOOL LLTextEditor::getPreeditLocation(S32 query_offset, LLCoordGL *coord, LLRect *bounds, LLRect *control) const { if (control) { LLRect control_rect_screen; localRectToScreen(mTextRect, &control_rect_screen); LLUI::screenRectToGL(control_rect_screen, control); } S32 preedit_left_position, preedit_right_position; if (hasPreeditString()) { preedit_left_position = mPreeditPositions.front(); preedit_right_position = mPreeditPositions.back(); } else { preedit_left_position = preedit_right_position = mCursorPos; } const S32 query = (query_offset >= 0 ? preedit_left_position + query_offset : mCursorPos); if (query < preedit_left_position || query > preedit_right_position) { return FALSE; } const S32 first_visible_line = mScrollbar->getDocPos(); if (query < getLineStart(first_visible_line)) { return FALSE; } S32 current_line = first_visible_line; S32 current_line_start, current_line_end; for (;;) { current_line_start = getLineStart(current_line); current_line_end = getLineStart(current_line + 1); if (query >= current_line_start && query < current_line_end) { break; } if (current_line_start == current_line_end) { // We have reached on the last line. The query position must be here. break; } current_line++; } const llwchar * const text = mWText.c_str(); const S32 line_height = ll_round(mGLFont->getLineHeight()); if (coord) { const S32 query_x = mTextRect.mLeft + mGLFont->getWidth(text, current_line_start, query - current_line_start, mAllowEmbeddedItems); const S32 query_y = mTextRect.mTop - (current_line - first_visible_line) * line_height - line_height / 2; S32 query_screen_x, query_screen_y; localPointToScreen(query_x, query_y, &query_screen_x, &query_screen_y); LLUI::screenPointToGL(query_screen_x, query_screen_y, &coord->mX, &coord->mY); } if (bounds) { S32 preedit_left = mTextRect.mLeft; if (preedit_left_position > current_line_start) { preedit_left += mGLFont->getWidth(text, current_line_start, preedit_left_position - current_line_start, mAllowEmbeddedItems); } S32 preedit_right = mTextRect.mLeft; if (preedit_right_position < current_line_end) { preedit_right += mGLFont->getWidth(text, current_line_start, preedit_right_position - current_line_start, mAllowEmbeddedItems); } else { preedit_right += mGLFont->getWidth(text, current_line_start, current_line_end - current_line_start, mAllowEmbeddedItems); } const S32 preedit_top = mTextRect.mTop - (current_line - first_visible_line) * line_height; const S32 preedit_bottom = preedit_top - line_height; const LLRect preedit_rect_local(preedit_left, preedit_top, preedit_right, preedit_bottom); LLRect preedit_rect_screen; localRectToScreen(preedit_rect_local, &preedit_rect_screen); LLUI::screenRectToGL(preedit_rect_screen, bounds); } return TRUE; } void LLTextEditor::getSelectionRange(S32 *position, S32 *length) const { if (hasSelection()) { *position = llmin(mSelectionStart, mSelectionEnd); *length = llabs(mSelectionStart - mSelectionEnd); } else { *position = mCursorPos; *length = 0; } } void LLTextEditor::getPreeditRange(S32 *position, S32 *length) const { if (hasPreeditString()) { *position = mPreeditPositions.front(); *length = mPreeditPositions.back() - mPreeditPositions.front(); } else { *position = mCursorPos; *length = 0; } } void LLTextEditor::markAsPreedit(S32 position, S32 length) { deselect(); setCursorPos(position); if (hasPreeditString()) { LL_WARNS() << "markAsPreedit invoked when hasPreeditString is true." << LL_ENDL; } mPreeditWString = LLWString( mWText, position, length ); if (length > 0) { mPreeditPositions.resize(2); mPreeditPositions[0] = position; mPreeditPositions[1] = position + length; mPreeditStandouts.resize(1); mPreeditStandouts[0] = FALSE; } else { mPreeditPositions.clear(); mPreeditStandouts.clear(); } if (LL_KIM_OVERWRITE == gKeyboard->getInsertMode()) { mPreeditOverwrittenWString = mPreeditWString; } else { mPreeditOverwrittenWString.clear(); } } S32 LLTextEditor::getPreeditFontSize() const { return ll_round(mGLFont->getLineHeight() * LLUI::getScaleFactor().mV[VY]); } void LLTextEditor::setKeystrokeCallback(const keystroke_signal_t::slot_type& callback) { mKeystrokeSignal.connect(callback); } void LLTextEditor::onKeyStroke() { mKeystrokeSignal(this); }