diff options
Diffstat (limited to 'indra/llui/lltexteditor.cpp')
-rw-r--r-- | indra/llui/lltexteditor.cpp | 6140 |
1 files changed, 3070 insertions, 3070 deletions
diff --git a/indra/llui/lltexteditor.cpp b/indra/llui/lltexteditor.cpp index e030861f20..daffd58dfe 100644 --- a/indra/llui/lltexteditor.cpp +++ b/indra/llui/lltexteditor.cpp @@ -1,3070 +1,3070 @@ -/** - * @file lltexteditor.cpp - * - * $LicenseInfo:firstyear=2001&license=viewerlgpl$ - * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; - * version 2.1 of the License only. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - * - * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA - * $/LicenseInfo$ - */ - -// Text editor widget to let users enter a a multi-line ASCII document. - -#include "linden_common.h" - -#define LLTEXTEDITOR_CPP -#include "lltexteditor.h" - -#include "llfontfreetype.h" // for LLFontFreetype::FIRST_CHAR -#include "llfontgl.h" -#include "llgl.h" // LLGLSUIDefault() -#include "lllocalcliprect.h" -#include "llrender.h" -#include "llui.h" -#include "lluictrlfactory.h" -#include "llrect.h" -#include "llfocusmgr.h" -#include "lltimer.h" -#include "llmath.h" - -#include "llclipboard.h" -#include "llemojihelper.h" -#include "llscrollbar.h" -#include "llstl.h" -#include "llstring.h" -#include "llkeyboard.h" -#include "llkeywords.h" -#include "llundo.h" -#include "llviewborder.h" -#include "llcontrol.h" -#include "llwindow.h" -#include "lltextparser.h" -#include "llscrollcontainer.h" -#include "llspellcheck.h" -#include "llpanel.h" -#include "llurlregistry.h" -#include "lltooltip.h" -#include "llmenugl.h" - -#include <queue> -#include "llcombobox.h" - -// -// Globals -// -static LLDefaultChildRegistry::Register<LLTextEditor> r("simple_text_editor"); - -// Compiler optimization, generate extern template -template class LLTextEditor* LLView::getChild<class LLTextEditor>( - const std::string& name, BOOL recurse) const; - -// -// Constants -// -const S32 SPACES_PER_TAB = 4; -const F32 SPELLCHECK_DELAY = 0.5f; // delay between the last keypress and spell checking the word the cursor is on - -/////////////////////////////////////////////////////////////////// - -class LLTextEditor::TextCmdInsert : public LLTextBase::TextCmd -{ -public: - TextCmdInsert(S32 pos, BOOL group_with_next, const LLWString &ws, LLTextSegmentPtr segment) - : TextCmd(pos, group_with_next, segment), mWString(ws) - { - } - virtual ~TextCmdInsert() {} - virtual BOOL execute( LLTextBase* editor, S32* delta ) - { - *delta = insert(editor, getPosition(), mWString ); - LLWStringUtil::truncate(mWString, *delta); - //mWString = wstring_truncate(mWString, *delta); - return (*delta != 0); - } - virtual S32 undo( LLTextBase* editor ) - { - remove(editor, getPosition(), mWString.length() ); - return getPosition(); - } - virtual S32 redo( LLTextBase* editor ) - { - insert(editor, getPosition(), mWString ); - return getPosition() + mWString.length(); - } - -private: - LLWString mWString; -}; - -/////////////////////////////////////////////////////////////////// -class LLTextEditor::TextCmdAddChar : public LLTextBase::TextCmd -{ -public: - TextCmdAddChar( S32 pos, BOOL group_with_next, llwchar wc, LLTextSegmentPtr segment) - : TextCmd(pos, group_with_next, segment), mWString(1, wc), mBlockExtensions(FALSE) - { - } - virtual void blockExtensions() - { - mBlockExtensions = TRUE; - } - virtual BOOL canExtend(S32 pos) const - { - // cannot extend text with custom segments - if (!mSegments.empty()) return FALSE; - - return !mBlockExtensions && (pos == getPosition() + (S32)mWString.length()); - } - virtual BOOL execute( LLTextBase* editor, S32* delta ) - { - *delta = insert(editor, getPosition(), mWString); - LLWStringUtil::truncate(mWString, *delta); - //mWString = wstring_truncate(mWString, *delta); - return (*delta != 0); - } - virtual BOOL extendAndExecute( LLTextBase* 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( LLTextBase* editor ) - { - remove(editor, getPosition(), mWString.length() ); - return getPosition(); - } - virtual S32 redo( LLTextBase* editor ) - { - insert(editor, getPosition(), mWString ); - return getPosition() + mWString.length(); - } - -private: - LLWString mWString; - BOOL mBlockExtensions; - -}; - -/////////////////////////////////////////////////////////////////// - -class LLTextEditor::TextCmdOverwriteChar : public LLTextBase::TextCmd -{ -public: - TextCmdOverwriteChar( S32 pos, BOOL group_with_next, llwchar wc) - : TextCmd(pos, group_with_next), mChar(wc), mOldChar(0) {} - - virtual BOOL execute( LLTextBase* editor, S32* delta ) - { - mOldChar = editor->getWText()[getPosition()]; - overwrite(editor, getPosition(), mChar); - *delta = 0; - return TRUE; - } - virtual S32 undo( LLTextBase* editor ) - { - overwrite(editor, getPosition(), mOldChar); - return getPosition(); - } - virtual S32 redo( LLTextBase* editor ) - { - overwrite(editor, getPosition(), mChar); - return getPosition()+1; - } - -private: - llwchar mChar; - llwchar mOldChar; -}; - -/////////////////////////////////////////////////////////////////// - -class LLTextEditor::TextCmdRemove : public LLTextBase::TextCmd -{ -public: - TextCmdRemove( S32 pos, BOOL group_with_next, S32 len, segment_vec_t& segments ) : - TextCmd(pos, group_with_next), mLen(len) - { - std::swap(mSegments, segments); - } - virtual BOOL execute( LLTextBase* editor, S32* delta ) - { - mWString = editor->getWText().substr(getPosition(), mLen); - *delta = remove(editor, getPosition(), mLen ); - return (*delta != 0); - } - virtual S32 undo( LLTextBase* editor ) - { - insert(editor, getPosition(), mWString); - return getPosition() + mWString.length(); - } - virtual S32 redo( LLTextBase* editor ) - { - remove(editor, getPosition(), mLen ); - return getPosition(); - } -private: - LLWString mWString; - S32 mLen; -}; - - -/////////////////////////////////////////////////////////////////// -LLTextEditor::Params::Params() -: default_text("default_text"), - prevalidator("prevalidator"), - embedded_items("embedded_items", false), - ignore_tab("ignore_tab", true), - auto_indent("auto_indent", true), - default_color("default_color"), - commit_on_focus_lost("commit_on_focus_lost", false), - show_context_menu("show_context_menu"), - show_emoji_helper("show_emoji_helper"), - enable_tooltip_paste("enable_tooltip_paste") -{ - addSynonym(prevalidator, "prevalidate_callback"); - addSynonym(prevalidator, "text_type"); -} - -LLTextEditor::LLTextEditor(const LLTextEditor::Params& p) : - LLTextBase(p), - mAutoreplaceCallback(), - mBaseDocIsPristine(TRUE), - mPristineCmd( NULL ), - mLastCmd( NULL ), - mDefaultColor( p.default_color() ), - mAutoIndent(p.auto_indent), - mParseOnTheFly(false), - mCommitOnFocusLost( p.commit_on_focus_lost), - mAllowEmbeddedItems( p.embedded_items ), - mMouseDownX(0), - mMouseDownY(0), - mTabsToNextField(p.ignore_tab), - mPrevalidator(p.prevalidator()), - mShowContextMenu(p.show_context_menu), - mShowEmojiHelper(p.show_emoji_helper), - mEnableTooltipPaste(p.enable_tooltip_paste), - mPassDelete(false), - mKeepSelectionOnReturn(false) -{ - mSourceID.generate(); - - //FIXME: use image? - LLViewBorder::Params params; - params.name = "text ed border"; - params.rect = getLocalRect(); - params.bevel_style = LLViewBorder::BEVEL_IN; - params.border_thickness = 1; - params.visible = p.border_visible; - mBorder = LLUICtrlFactory::create<LLViewBorder> (params); - addChild( mBorder ); - setText(p.default_text()); - - mParseOnTheFly = true; -} - -void LLTextEditor::initFromParams( const LLTextEditor::Params& p) -{ - LLTextBase::initFromParams(p); - - // HACK: text editors always need to be enabled so that we can scroll - LLView::setEnabled(true); - - if (p.commit_on_focus_lost.isProvided()) - { - mCommitOnFocusLost = p.commit_on_focus_lost; - } - - updateAllowingLanguageInput(); -} - -LLTextEditor::~LLTextEditor() -{ - gFocusMgr.releaseFocusIfNeeded( this ); // calls onCommit() while LLTextEditor still valid - - // Scrollbar is deleted by LLView - std::for_each(mUndoStack.begin(), mUndoStack.end(), DeletePointer()); - mUndoStack.clear(); - // Mark the menu as dead or its retained in memory till shutdown. - LLContextMenu* menu = static_cast<LLContextMenu*>(mContextMenuHandle.get()); - if(menu) - { - menu->die(); - mContextMenuHandle.markDead(); - } -} - -//////////////////////////////////////////////////////////// -// LLTextEditor -// Public methods - -void LLTextEditor::setText(const LLStringExplicit &utf8str, const LLStyle::Params& input_params) -{ - // validate incoming text if necessary - if (mPrevalidator) - { - if (!mPrevalidator.validate(utf8str)) - { - LLUI::getInstance()->reportBadKeystroke(); - mPrevalidator.showLastErrorUsingTimeout(); - - // not valid text, nothing to do - return; - } - } - - blockUndo(); - deselect(); - - mParseOnTheFly = false; - LLTextBase::setText(utf8str, input_params); - mParseOnTheFly = true; - - resetDirty(); -} - -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. - setCursorPos(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); - - 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) -{ - startOfDoc(); - selectNext(search_text, case_insensitive, FALSE); - - BOOL replaced = TRUE; - while ( replaced ) - { - replaced = replaceText(search_text,replace_text, case_insensitive, FALSE); - } -} - -S32 LLTextEditor::prevWordPos(S32 cursorPos) const -{ - LLWString wtext(getWText()); - while( (cursorPos > 0) && (wtext[cursorPos-1] == ' ') ) - { - cursorPos--; - } - while( (cursorPos > 0) && LLWStringUtil::isPartOfWord( wtext[cursorPos-1] ) ) - { - cursorPos--; - } - return cursorPos; -} - -S32 LLTextEditor::nextWordPos(S32 cursorPos) const -{ - LLWString wtext(getWText()); - while( (cursorPos < getLength()) && LLWStringUtil::isPartOfWord( wtext[cursorPos] ) ) - { - cursorPos++; - } - while( (cursorPos < getLength()) && (wtext[cursorPos] == ' ') ) - { - cursorPos++; - } - return cursorPos; -} - -const LLTextSegmentPtr LLTextEditor::getPreviousSegment() const -{ - static LLPointer<LLIndexSegment> index_segment = new LLIndexSegment; - - index_segment->setStart(mCursorPos); - index_segment->setEnd(mCursorPos); - - // find segment index at character to left of cursor (or rightmost edge of selection) - segment_set_t::const_iterator it = mSegments.lower_bound(index_segment); - - if (it != mSegments.end()) - { - return *it; - } - else - { - return LLTextSegmentPtr(); - } -} - -void LLTextEditor::getSelectedSegments(LLTextEditor::segment_vec_t& segments) const -{ - S32 left = hasSelection() ? llmin(mSelectionStart, mSelectionEnd) : mCursorPos; - S32 right = hasSelection() ? llmax(mSelectionStart, mSelectionEnd) : mCursorPos; - - return getSegmentsInRange(segments, left, right, true); -} - -void LLTextEditor::getSegmentsInRange(LLTextEditor::segment_vec_t& segments_out, S32 start, S32 end, bool include_partial) const -{ - segment_set_t::const_iterator first_it = getSegIterContaining(start); - segment_set_t::const_iterator end_it = getSegIterContaining(end - 1); - if (end_it != mSegments.end()) ++end_it; - - for (segment_set_t::const_iterator it = first_it; it != end_it; ++it) - { - LLTextSegmentPtr segment = *it; - if (include_partial - || (segment->getStart() >= start - && segment->getEnd() <= end)) - { - segments_out.push_back(segment); - } - } -} - -void LLTextEditor::setShowEmojiHelper(bool show) -{ - if (!mShowEmojiHelper) - { - LLEmojiHelper::instance().hideHelper(this); - } - - mShowEmojiHelper = show; -} - -BOOL LLTextEditor::selectionContainsLineBreaks() -{ - if (hasSelection()) - { - S32 left = llmin(mSelectionStart, mSelectionEnd); - S32 right = left + llabs(mSelectionStart - mSelectionEnd); - - LLWString wtext = getWText(); - 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++) - { - LLWString wtext = getWText(); - if (wtext[pos] == ' ') - { - delta_spaces += remove( pos, 1, FALSE ); - } - } - } - - return delta_spaces; -} - -void LLTextEditor::indentSelectedLines( S32 spaces ) -{ - if( hasSelection() ) - { - LLWString text = getWText(); - 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++; - } - } - - // Disabling parsing on the fly to avoid updating text segments - // until all indentation commands are executed. - mParseOnTheFly = false; - - // 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 = getWText(); - - // Find the next new line - while( (cur < right) && (text[cur] != '\n') ) - { - cur++; - } - } - while( cur < right ); - - mParseOnTheFly = true; - - 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; - } - setCursorPos(mSelectionEnd); - } -} - -//virtual -BOOL LLTextEditor::canSelectAll() const -{ - return TRUE; -} - -// virtual -void LLTextEditor::selectAll() -{ - mSelectionStart = getLength(); - mSelectionEnd = 0; - setCursorPos(mSelectionEnd); - updatePrimary(); -} - -void LLTextEditor::selectByCursorPosition(S32 prev_cursor_pos, S32 next_cursor_pos) -{ - setCursorPos(prev_cursor_pos); - startSelection(); - setCursorPos(next_cursor_pos); - endSelection(); -} - -void LLTextEditor::insertEmoji(llwchar emoji) -{ - LL_INFOS() << "LLTextEditor::insertEmoji(" << wchar_utf8_preview(emoji) << ")" << LL_ENDL; - auto styleParams = LLStyle::Params(); - styleParams.font = LLFontGL::getFontEmojiLarge(); - auto segment = new LLEmojiTextSegment(new LLStyle(styleParams), mCursorPos, mCursorPos + 1, *this); - insert(mCursorPos, LLWString(1, emoji), false, segment); - setCursorPos(mCursorPos + 1); -} - -void LLTextEditor::handleEmojiCommit(llwchar emoji) -{ - S32 shortCodePos; - if (LLEmojiHelper::isCursorInEmojiCode(getWText(), mCursorPos, &shortCodePos)) - { - remove(shortCodePos, mCursorPos - shortCodePos, true); - setCursorPos(shortCodePos); - - insertEmoji(emoji); - } -} - -BOOL LLTextEditor::handleMouseDown(S32 x, S32 y, MASK mask) -{ - BOOL handled = FALSE; - - // set focus first, in case click callbacks want to change it - // RN: do we really need to have a tab stop? - if (hasTabStop()) - { - setFocus( TRUE ); - } - - // Let scrollbar have first dibs - handled = LLTextBase::handleMouseDown(x, y, mask); - - if( !handled ) - { - 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()) - { - mSelectionEnd = mCursorPos; - } - else - { - mSelectionStart = old_cursor_pos; - mSelectionEnd = mCursorPos; - } - // assume we're starting a drag select - mIsSelecting = TRUE; - } - else - { - setCursorAtLocalPos( x, y, true ); - startSelection(); - } - } - - handled = TRUE; - } - - // Delay cursor flashing - resetCursorBlink(); - - if (handled && !gFocusMgr.getMouseCapture()) - { - gFocusMgr.setMouseCapture( this ); - } - return handled; -} - -BOOL LLTextEditor::handleRightMouseDown(S32 x, S32 y, MASK mask) -{ - if (hasTabStop()) - { - setFocus(TRUE); - } - - bool show_menu = false; - - // Prefer editor menu if it has selection. See EXT-6806. - if (hasSelection()) - { - S32 click_pos = getDocIndexFromLocalCoord(x, y, FALSE); - if (click_pos > mSelectionStart && click_pos < mSelectionEnd) - { - show_menu = true; - } - } - - // Let segments handle the click, if nothing does, show editor menu - if (!show_menu && !LLTextBase::handleRightMouseDown(x, y, mask)) - { - show_menu = true; - } - - if (show_menu && getShowContextMenu()) - { - showContextMenu(x, y); - } - - return TRUE; -} - - - -BOOL LLTextEditor::handleMiddleMouseDown(S32 x, S32 y, MASK mask) -{ - if (hasTabStop()) - { - setFocus(TRUE); - } - - if (!LLTextBase::handleMouseDown(x, y, mask)) - { - if( canPastePrimary() ) - { - setCursorAtLocalPos( x, y, true ); - // does not rely on focus being set - pastePrimary(); - } - } - return TRUE; -} - - -BOOL LLTextEditor::handleHover(S32 x, S32 y, MASK mask) -{ - BOOL handled = FALSE; - - if(hasMouseCapture() ) - { - if( mIsSelecting ) - { - if(mScroller) - { - mScroller->autoScroll(x, y); - } - S32 clamped_x = llclamp(x, mVisibleTextRect.mLeft, mVisibleTextRect.mRight); - S32 clamped_y = llclamp(y, mVisibleTextRect.mBottom, mVisibleTextRect.mTop); - setCursorAtLocalPos( clamped_x, clamped_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 = LLTextBase::handleHover(x, y, mask); - } - - if( handled ) - { - // Delay cursor flashing - resetCursorBlink(); - } - - if( !handled ) - { - getWindow()->setCursor(UI_CURSOR_IBEAM); - handled = TRUE; - } - - return handled; -} - - -BOOL LLTextEditor::handleMouseUp(S32 x, S32 y, MASK mask) -{ - BOOL handled = FALSE; - - // if I'm not currently selecting text - if (!(mIsSelecting && hasMouseCapture())) - { - // let text segments handle mouse event - handled = LLTextBase::handleMouseUp(x, y, mask); - } - - if( !handled ) - { - if( mIsSelecting ) - { - if(mScroller) - { - mScroller->autoScroll(x, y); - } - S32 clamped_x = llclamp(x, mVisibleTextRect.mLeft, mVisibleTextRect.mRight); - S32 clamped_y = llclamp(y, mVisibleTextRect.mBottom, mVisibleTextRect.mTop); - setCursorAtLocalPos( clamped_x, clamped_y, true ); - endSelection(); - } - - // take selection to 'primary' clipboard - updatePrimary(); - - handled = TRUE; - } - - // Delay cursor flashing - resetCursorBlink(); - - if( hasMouseCapture() ) - { - gFocusMgr.setMouseCapture( NULL ); - - handled = TRUE; - } - - return handled; -} - - -BOOL LLTextEditor::handleDoubleClick(S32 x, S32 y, MASK mask) -{ - BOOL handled = FALSE; - - // let scrollbar and text segments have first dibs - handled = LLTextBase::handleDoubleClick(x, y, mask); - - if( !handled ) - { - setCursorAtLocalPos( x, y, false ); - deselect(); - - LLWString text = getWText(); - - if( LLWStringUtil::isPartOfWord( text[mCursorPos] ) ) - { - // Select word the cursor is over - while ((mCursorPos > 0) && LLWStringUtil::isPartOfWord(text[mCursorPos-1])) - { - if (!setCursorPos(mCursorPos - 1)) break; - } - startSelection(); - - while ((mCursorPos < (S32)text.length()) && LLWStringUtil::isPartOfWord( text[mCursorPos] ) ) - { - if (!setCursorPos(mCursorPos + 1)) break; - } - - mSelectionEnd = mCursorPos; - } - else if ((mCursorPos < (S32)text.length()) && !iswspace( text[mCursorPos]) ) - { - // Select the character the cursor is over - startSelection(); - setCursorPos(mCursorPos + 1); - 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 - resetCursorBlink(); - - // take selection to 'primary' clipboard - updatePrimary(); - - handled = TRUE; - } - - return handled; -} - - -//---------------------------------------------------------------------------- -// Returns change in number of characters in mText - -S32 LLTextEditor::execute( TextCmd* cmd ) -{ - if (!mReadOnly && mShowEmojiHelper) - { - // Any change to our contents should always hide the helper - LLEmojiHelper::instance().hideHelper(this); - } - - 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); - 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; - - bool need_to_rollback = mPrevalidator && !mPrevalidator.validate(getViewModel()->getDisplay()); - if (need_to_rollback) - { - LLUI::getInstance()->reportBadKeystroke(); - mPrevalidator.showLastErrorUsingTimeout(); - - // get rid of this last command and clean up undo stack - undo(); - - // remove any evidence of this command from redo history - mUndoStack.pop_front(); - delete cmd; - - // failure, nothing changed - delta = 0; - } - } - else - { - // Operation failed, so don't put it on the undo stack. - delete cmd; - } - - return delta; -} - -S32 LLTextEditor::insert(S32 pos, const LLWString &wstr, bool group_with_next_op, LLTextSegmentPtr segment) -{ - return execute( new TextCmdInsert( pos, group_with_next_op, wstr, segment ) ); -} - -S32 LLTextEditor::remove(S32 pos, S32 length, bool group_with_next_op) -{ - S32 end_pos = getEditableIndex(pos + length, true); - BOOL removedChar = FALSE; - - segment_vec_t segments_to_remove; - // store text segments - getSegmentsInRange(segments_to_remove, pos, pos + length, false); - - if (pos <= end_pos) - { - removedChar = execute( new TextCmdRemove( pos, group_with_next_op, end_pos - pos, segments_to_remove ) ); - } - - return removedChar; -} - -S32 LLTextEditor::overwriteChar(S32 pos, llwchar wc) -{ - if ((S32)getLength() == pos) - { - return addChar(pos, wc); - } - else - { - return execute(new TextCmdOverwriteChar(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; - - LLWString text = getWText(); - if (text[mCursorPos - 1] == ' ') - { - // Try to remove a "tab" - S32 offset = getLineOffsetFromDocIndex(mCursorPos); - 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); - } - - tryToShowEmojiHelper(); - } - else - { - LLUI::getInstance()->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); - tryToShowEmojiHelper(); - } - else - { - LLUI::getInstance()->reportBadKeystroke(); - } -} - -// Add a single character to the text -S32 LLTextEditor::addChar(S32 pos, llwchar wc) -{ - if ((wstring_utf8_length(getWText()) + wchar_utf8_length(wc)) > mMaxTextByteLength) - { - LLUI::getInstance()->reportBadKeystroke(); - return 0; - } - - if (mLastCmd && mLastCmd->canExtend(pos)) - { - if (mPrevalidator) - { - // get a copy of current text contents - LLWString test_string(getViewModel()->getDisplay()); - - // modify text contents as if this addChar succeeded - llassert(pos <= (S32)test_string.size()); - test_string.insert(pos, 1, wc); - if (!mPrevalidator.validate(test_string)) - { - LLUI::getInstance()->reportBadKeystroke(); - mPrevalidator.showLastErrorUsingTimeout(); - return 0; - } - } - - S32 delta = 0; - mLastCmd->extendAndExecute(this, pos, wc, &delta); - - return delta; - } - - return execute(new TextCmdAddChar(pos, FALSE, wc, LLTextSegmentPtr())); -} - -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 )); - tryToShowEmojiHelper(); - - if (!mReadOnly && mAutoreplaceCallback != NULL) - { - // autoreplace the text, if necessary - S32 replacement_start; - S32 replacement_length; - LLWString replacement_string; - S32 new_cursor_pos = mCursorPos; - mAutoreplaceCallback(replacement_start, replacement_length, replacement_string, new_cursor_pos, getWText()); - - if (replacement_length > 0 || !replacement_string.empty()) - { - remove(replacement_start, replacement_length, true); - insert(replacement_start, replacement_string, false, LLTextSegmentPtr()); - setCursorPos(new_cursor_pos); - } - } -} - -void LLTextEditor::showEmojiHelper() -{ - if (mReadOnly || !mShowEmojiHelper) - return; - - const LLRect cursorRect(getLocalRectFromDocIndex(mCursorPos)); - auto cb = [this](llwchar emoji) { insertEmoji(emoji); }; - LLEmojiHelper::instance().showHelper(this, cursorRect.mLeft, cursorRect.mTop, LLStringUtil::null, cb); -} - -void LLTextEditor::tryToShowEmojiHelper() -{ - if (mReadOnly || !mShowEmojiHelper) - return; - - S32 shortCodePos; - LLWString wtext(getWText()); - if (LLEmojiHelper::isCursorInEmojiCode(wtext, mCursorPos, &shortCodePos)) - { - const LLRect cursorRect(getLocalRectFromDocIndex(shortCodePos)); - const LLWString wpart(wtext.substr(shortCodePos, mCursorPos - shortCodePos)); - const std::string part(wstring_to_utf8str(wpart)); - auto cb = [this](llwchar emoji) { handleEmojiCommit(emoji); }; - LLEmojiHelper::instance().showHelper(this, cursorRect.mLeft, cursorRect.mTop, part, cb); - } - else - { - LLEmojiHelper::instance().hideHelper(); - } -} - -void LLTextEditor::addLineBreakChar(BOOL group_together) -{ - if( !getEnabled() ) - { - return; - } - if( hasSelection() ) - { - deleteSelection(TRUE); - } - else if (LL_KIM_OVERWRITE == gKeyboard->getInsertMode()) - { - removeChar(mCursorPos); - } - - LLStyleConstSP sp(new LLStyle(LLStyle::Params())); - LLTextSegmentPtr segment = new LLLineBreakTextSegment(sp, mCursorPos); - - S32 pos = execute(new TextCmdAddChar(mCursorPos, group_together, '\n', segment)); - - setCursorPos(mCursorPos + pos); -} - - -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(); - setCursorPos(mCursorPos - 1); - if( mask & MASK_CONTROL ) - { - setCursorPos(prevWordPos(mCursorPos)); - } - mSelectionEnd = mCursorPos; - } - break; - - case KEY_RIGHT: - if( mCursorPos < getLength() ) - { - startSelection(); - setCursorPos(mCursorPos + 1); - if( mask & MASK_CONTROL ) - { - setCursorPos(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 ) - { - setCursorPos(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 ) - { - setCursorPos(getLength()); - } - else - { - endOfLine(); - } - mSelectionEnd = mCursorPos; - break; - - default: - handled = FALSE; - break; - } - } - - 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: - changeLine( -1 ); - break; - - case KEY_PAGE_UP: - changePage( -1 ); - break; - - case KEY_HOME: - startOfLine(); - break; - - case KEY_DOWN: - changeLine( 1 ); - deselect(); - break; - - case KEY_PAGE_DOWN: - changePage( 1 ); - break; - - case KEY_END: - endOfLine(); - break; - - case KEY_LEFT: - if( hasSelection() ) - { - setCursorPos(llmin( mSelectionStart, mSelectionEnd )); - } - else - { - if( 0 < mCursorPos ) - { - setCursorPos(mCursorPos - 1); - } - else - { - LLUI::getInstance()->reportBadKeystroke(); - } - } - break; - - case KEY_RIGHT: - if( hasSelection() ) - { - setCursorPos(llmax( mSelectionStart, mSelectionEnd )); - } - else - { - if( mCursorPos < getLength() ) - { - setCursorPos(mCursorPos + 1); - } - else - { - LLUI::getInstance()->reportBadKeystroke(); - } - } - break; - - default: - handled = FALSE; - break; - } - } - - if (handled) - { - deselect(); - } - - 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 ); - LLClipboard::instance().copyToClipboard( getWText(), left_pos, length); - deleteSelection( FALSE ); - - onKeyStroke(); -} - -BOOL LLTextEditor::canCopy() const -{ - return hasSelection(); -} - -// copy selection to clipboard -void LLTextEditor::copy() -{ - if( !canCopy() ) - { - return; - } - S32 left_pos = llmin( mSelectionStart, mSelectionEnd ); - S32 length = llabs( mSelectionStart - mSelectionEnd ); - LLClipboard::instance().copyToClipboard(getWText(), left_pos, length); -} - -BOOL LLTextEditor::canPaste() const -{ - return !mReadOnly && LLClipboard::instance().isTextAvailable(); -} - -// 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) -{ - struct BoolReset - { - BoolReset(bool& value) : mValuePtr(&value) { *mValuePtr = false; } - ~BoolReset() { *mValuePtr = true; } - bool* mValuePtr; - } reset(mParseOnTheFly); - - bool can_paste_it; - if (is_primary) - { - can_paste_it = canPastePrimary(); - } - else - { - can_paste_it = canPaste(); - } - - if (!can_paste_it) - { - return; - } - - LLWString paste; - LLClipboard::instance().pasteFromClipboard(paste, is_primary); - - 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); - cleanStringForPaste(clean_string); - - // Insert the new text into the existing text. - - //paste text with linebreaks. - pasteTextWithLinebreaks(clean_string); - - deselect(); - - onKeyStroke(); -} - - -// Clean up string (replace tabs and remove characters that our fonts don't support). -void LLTextEditor::cleanStringForPaste(LLWString & clean_string) -{ - std::string clean_string_utf = wstring_to_utf8str(clean_string); - std::replace( clean_string_utf.begin(), clean_string_utf.end(), '\r', '\n'); - clean_string = utf8str_to_wstring(clean_string_utf); - - 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); - } - } - } -} - - -void LLTextEditor::pasteTextWithLinebreaks(LLWString & clean_string) -{ - std::basic_string<llwchar>::size_type start = 0; - std::basic_string<llwchar>::size_type pos = clean_string.find('\n',start); - - while((pos != -1) && (pos != clean_string.length() -1)) - { - if(pos!=start) - { - std::basic_string<llwchar> str = std::basic_string<llwchar>(clean_string,start,pos-start); - setCursorPos(mCursorPos + insert(mCursorPos, str, TRUE, LLTextSegmentPtr())); - } - addLineBreakChar(TRUE); // Add a line break and group with the next addition. - - start = pos+1; - pos = clean_string.find('\n',start); - } - - if (pos != start) - { - std::basic_string<llwchar> str = std::basic_string<llwchar>(clean_string,start,clean_string.length()-start); - setCursorPos(mCursorPos + insert(mCursorPos, str, FALSE, LLTextSegmentPtr())); - } - else - { - addLineBreakChar(FALSE); // Add a line break and end the grouping. - } -} - -// copy selection to primary -void LLTextEditor::copyPrimary() -{ - if( !canCopy() ) - { - return; - } - S32 left_pos = llmin( mSelectionStart, mSelectionEnd ); - S32 length = llabs( mSelectionStart - mSelectionEnd ); - LLClipboard::instance().copyToClipboard(getWText(), left_pos, length, true); -} - -BOOL LLTextEditor::canPastePrimary() const -{ - return !mReadOnly && LLClipboard::instance().isTextAvailable(true); -} - -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(); - setCursorPos(0); - mSelectionEnd = mCursorPos; - } - else - { - // Ctrl-Home, Ctrl-Left, Ctrl-Right, Ctrl-Down - // all move the cursor as if clicking, so should deselect. - deselect(); - startOfDoc(); - } - 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; - - default: - handled = FALSE; - break; - } - } - - if (handled && !gFocusMgr.getMouseCapture()) - { - updatePrimary(); - } - - return handled; -} - - -BOOL LLTextEditor::handleSpecialKey(const KEY key, const MASK mask) - { - BOOL handled = TRUE; - - if (mReadOnly) return FALSE; - - switch( key ) - { - case KEY_INSERT: - if (mask == MASK_NONE) - { - gKeyboard->toggleInsertMode(); - } - break; - - case KEY_BACKSPACE: - if( hasSelection() ) - { - deleteSelection(FALSE); - } - else - if( 0 < mCursorPos ) - { - removeCharOrTab(); - } - else - { - LLUI::getInstance()->reportBadKeystroke(); - } - break; - - - case KEY_RETURN: - if (mask == MASK_NONE) - { - if( hasSelection() && !mKeepSelectionOnReturn ) - { - deleteSelection(FALSE); - } - if (mAutoIndent) - { - autoIndent(); - } - } - 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 offset = getLineOffsetFromDocIndex(mCursorPos); - - 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 ) - { - LLWString text = getWText(); - if( ' ' == text[ mCursorPos - 1 ] ) - { - S32 line = getLineNumFromDocIndex(mCursorPos, false); - S32 line_start = getLineStart(line); - - // Jump over spaces in the current line - while ((' ' == text[line_start]) && (line_start < mCursorPos)) - { - line_start++; - } - - // Make sure there is nothing but ' ' before the Brace we are unindenting - if (line_start == mCursorPos) - { - removeCharOrTab(); - } - } - } -} - - -BOOL LLTextEditor::handleKeyHere(KEY key, MASK mask ) -{ - BOOL handled = FALSE; - - // 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; - } - - if (mReadOnly && mScroller) - { - handled = (mScroller && mScroller->handleKeyHere( key, mask )) - || handleSelectionKey(key, mask) - || handleControlKey(key, mask); - } - else - { - if (!mReadOnly && mShowEmojiHelper && LLEmojiHelper::instance().handleKey(this, key, mask)) - { - return TRUE; - } - - if (mEnableTooltipPaste && - LLToolTipMgr::instance().toolTipVisible() && - LLToolTipMgr::instance().isTooltipPastable() && - KEY_TAB == key) - { // Paste the first line of a tooltip into the editor - std::string message; - LLToolTipMgr::instance().getToolTipMessage(message); - LLWString tool_tip_text(utf8str_to_wstring(message)); - - if (tool_tip_text.size() > 0) - { - // Delete any selected characters (the tooltip text replaces them) - if(hasSelection()) - { - deleteSelection(TRUE); - } - - std::basic_string<llwchar>::size_type pos = tool_tip_text.find('\n',0); - if (pos != -1) - { // Extract the first line of the tooltip - tool_tip_text = std::basic_string<llwchar>(tool_tip_text, 0, pos); - } - - // Add the text - cleanStringForPaste(tool_tip_text); - pasteTextWithLinebreaks(tool_tip_text); - handled = TRUE; - } - } - else - { // Normal key handling - handled = handleNavigationKey( key, mask ) - || handleSelectionKey(key, mask) - || handleControlKey(key, mask) - || handleSpecialKey(key, mask); - } - } - - if( handled ) - { - resetCursorBlink(); - needsScroll(); - - if (mShowEmojiHelper) - { - // Dismiss the helper whenever we handled a key that it didn't - LLEmojiHelper::instance().hideHelper(this); - } - } - - return handled; -} - - -BOOL LLTextEditor::handleUnicodeCharHere(llwchar uni_char) -{ - if ((uni_char < 0x20) || (uni_char == 0x7F)) // Control character or DEL - { - return FALSE; - } - - BOOL handled = FALSE; - - // Handle most keys only if the text editor is writeable. - if( !mReadOnly ) - { - if (mShowEmojiHelper && uni_char < 0x80 && LLEmojiHelper::instance().handleKey(this, (KEY)uni_char, MASK_NONE)) - { - return TRUE; - } - - if( mAutoIndent && '}' == 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 ) - { - resetCursorBlink(); - - // Most keystrokes will make the selection box go away, but not all will. - deselect(); - - onKeyStroke(); - } - - return handled; -} - - -// virtual -BOOL LLTextEditor::canDoDelete() const -{ - return !mReadOnly && ( !mPassDelete || ( hasSelection() || (mCursorPos < getLength())) ); -} - -void LLTextEditor::doDelete() -{ - if( !canDoDelete() ) - { - return; - } - if( hasSelection() ) - { - deleteSelection(FALSE); - } - else - if( mCursorPos < getLength() ) - { - S32 i; - S32 chars_to_remove = 1; - LLWString text = getWText(); - if( (text[ mCursorPos ] == ' ') && (mCursorPos + SPACES_PER_TAB < getLength()) ) - { - // Try to remove a full tab's worth of spaces - S32 offset = getLineOffsetFromDocIndex(mCursorPos); - 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(); - } - - } - - 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); - - 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); - - onKeyStroke(); -} - -void LLTextEditor::onFocusReceived() -{ - LLTextBase::onFocusReceived(); - updateAllowingLanguageInput(); -} - -void LLTextEditor::focusLostHelper() -{ - updateAllowingLanguageInput(); - - // Route menu back to the default - if( gEditMenuHandler == this ) - { - gEditMenuHandler = NULL; - } - - if (mCommitOnFocusLost) - { - onCommit(); - } - - // Make sure cursor is shown again - getWindow()->showCursorFromMouseMove(); -} - -void LLTextEditor::onFocusLost() -{ - focusLostHelper(); - LLTextBase::onFocusLost(); -} - -void LLTextEditor::onCommit() -{ - setControlValue(getValue()); - LLTextBase::onCommit(); -} - -void LLTextEditor::setEnabled(BOOL enabled) -{ - // just treat enabled as read-only flag - bool read_only = !enabled; - if (read_only != mReadOnly) - { - //mReadOnly = read_only; - LLTextBase::setReadOnly(read_only); - updateSegments(); - updateAllowingLanguageInput(); - } -} - -void LLTextEditor::showContextMenu(S32 x, S32 y) -{ - LLContextMenu* menu = static_cast<LLContextMenu*>(mContextMenuHandle.get()); - if (!menu) - { - llassert(LLMenuGL::sMenuContainer != NULL); - menu = LLUICtrlFactory::createFromFile<LLContextMenu>("menu_text_editor.xml", - LLMenuGL::sMenuContainer, - LLMenuHolderGL::child_registry_t::instance()); - if(!menu) - { - LL_WARNS() << "Failed to create menu for LLTextEditor: " << getName() << LL_ENDL; - return; - } - mContextMenuHandle = menu->getHandle(); - } - - // Route menu to this class - // previously this was done in ::handleRightMoseDown: - //if(hasTabStop()) - // setFocus(TRUE) - why? weird... - // and then inside setFocus - // .... - // gEditMenuHandler = this; - // .... - // but this didn't work in all cases and just weird... - //why not here? - // (all this was done for EXT-4443) - - gEditMenuHandler = this; - - S32 screen_x, screen_y; - localPointToScreen(x, y, &screen_x, &screen_y); - - setCursorAtLocalPos(x, y, false); - if (hasSelection()) - { - if ( (mCursorPos < llmin(mSelectionStart, mSelectionEnd)) || (mCursorPos > llmax(mSelectionStart, mSelectionEnd)) ) - { - deselect(); - } - else - { - setCursorPos(llmax(mSelectionStart, mSelectionEnd)); - } - } - - bool use_spellcheck = getSpellCheck(), is_misspelled = false; - if (use_spellcheck) - { - mSuggestionList.clear(); - - // If the cursor is on a misspelled word, retrieve suggestions for it - std::string misspelled_word = getMisspelledWord(mCursorPos); - if ((is_misspelled = !misspelled_word.empty()) == true) - { - LLSpellChecker::instance().getSuggestions(misspelled_word, mSuggestionList); - } - } - - menu->setItemVisible("Suggestion Separator", (use_spellcheck) && (!mSuggestionList.empty())); - menu->setItemVisible("Add to Dictionary", (use_spellcheck) && (is_misspelled)); - menu->setItemVisible("Add to Ignore", (use_spellcheck) && (is_misspelled)); - menu->setItemVisible("Spellcheck Separator", (use_spellcheck) && (is_misspelled)); - menu->show(screen_x, screen_y, this); -} - - -void LLTextEditor::drawPreeditMarker() -{ - static LLUICachedControl<F32> preedit_marker_brightness ("UIPreeditMarkerBrightness", 0); - static LLUICachedControl<S32> preedit_marker_gap ("UIPreeditMarkerGap", 0); - static LLUICachedControl<S32> preedit_marker_position ("UIPreeditMarkerPosition", 0); - static LLUICachedControl<S32> preedit_marker_thickness ("UIPreeditMarkerThickness", 0); - static LLUICachedControl<F32> preedit_standout_brightness ("UIPreeditStandoutBrightness", 0); - static LLUICachedControl<S32> preedit_standout_gap ("UIPreeditStandoutGap", 0); - static LLUICachedControl<S32> preedit_standout_position ("UIPreeditStandoutPosition", 0); - static LLUICachedControl<S32> preedit_standout_thickness ("UIPreeditStandoutThickness", 0); - - if (!hasPreeditString()) - { - return; - } - - const LLWString textString(getWText()); - const llwchar *text = textString.c_str(); - const S32 text_len = getLength(); - const S32 num_lines = getLineCount(); - - S32 cur_line = getFirstVisibleLine(); - if (cur_line >= num_lines) - { - return; - } - - const S32 line_height = mFont->getLineHeight(); - - S32 line_start = getLineStart(cur_line); - S32 line_y = mVisibleTextRect.mTop - line_height; - while((mVisibleTextRect.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; - } - - line_info& line = mLineInfoList[cur_line]; - LLRect text_rect(line.mRect); - text_rect.mRight = mDocumentView->getRect().getWidth(); // clamp right edge to document extents - text_rect.translate(mDocumentView->getRect().mLeft, mDocumentView->getRect().mBottom); // adjust by scroll position - - S32 preedit_left = text_rect.mLeft; - if (left > line_start) - { - preedit_left += mFont->getWidth(text, line_start, left - line_start); - } - S32 preedit_right = text_rect.mLeft; - if (right < line_end) - { - preedit_right += mFont->getWidth(text, line_start, right - line_start); - } - else - { - preedit_right += mFont->getWidth(text, line_start, line_end - line_start); - } - - if (mPreeditStandouts[i]) - { - gl_rect_2d(preedit_left + preedit_standout_gap, - text_rect.mBottom + mFont->getDescenderHeight() - 1, - preedit_right - preedit_standout_gap - 1, - text_rect.mBottom + mFont->getDescenderHeight() - 1 - preedit_standout_thickness, - (mCursorColor.get() * preedit_standout_brightness + mWriteableBgColor.get() * (1 - preedit_standout_brightness)).setAlpha(1.0f)); - } - else - { - gl_rect_2d(preedit_left + preedit_marker_gap, - text_rect.mBottom + mFont->getDescenderHeight() - 1, - preedit_right - preedit_marker_gap - 1, - text_rect.mBottom + mFont->getDescenderHeight() - 1 - preedit_marker_thickness, - (mCursorColor.get() * preedit_marker_brightness + mWriteableBgColor.get() * (1 - preedit_marker_brightness)).setAlpha(1.0f)); - } - } - } - - // move down one line - line_y -= line_height; - line_start = next_start; - cur_line++; - } -} - -void LLTextEditor::draw() -{ - { - // pad clipping rectangle so that cursor can draw at full width - // when at left edge of mVisibleTextRect - LLRect clip_rect(mVisibleTextRect); - clip_rect.stretch(1); - LLLocalClipRect clip(clip_rect); - } - - LLTextBase::draw(); - - drawPreeditMarker(); - - //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( hasFocus() );// && !mReadOnly); -} - -// 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); - } - - LLTextBase::setFocus( new_state ); - - if( new_state ) - { - // Route menu to this class - gEditMenuHandler = this; - - // Don't start the cursor flashing right away - resetCursorBlink(); - } - else - { - // Route menu back to the default - if( gEditMenuHandler == this ) - { - gEditMenuHandler = NULL; - } - - endSelection(); - } -} - -// public -void LLTextEditor::setCursorAndScrollToEnd() -{ - deselect(); - endOfDoc(); -} - -void LLTextEditor::getCurrentLineAndColumn( S32* line, S32* col, BOOL include_wordwrap ) -{ - *line = getLineNumFromDocIndex(mCursorPos, include_wordwrap); - *col = getLineOffsetFromDocIndex(mCursorPos, include_wordwrap); -} - -void LLTextEditor::autoIndent() -{ - // Count the number of spaces in the current line - S32 line = getLineNumFromDocIndex(mCursorPos, false); - S32 line_start = getLineStart(line); - S32 space_count = 0; - S32 i; - - LLWString text = getWText(); - S32 offset = getLineOffsetFromDocIndex(mCursorPos); - while(( ' ' == text[line_start] ) && (space_count < offset)) - { - 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 - - //appendLineBreakSegment(LLStyle::Params());//addChar( '\n' ); - addLineBreakChar(); - - for( i = 0; i < space_count; i++ ) - { - addChar( ' ' ); - } -} - -// Inserts new text at the cursor position -void LLTextEditor::insertText(const std::string &new_text) -{ - BOOL enabled = getEnabled(); - setEnabled( TRUE ); - - // Delete any selected characters (the insertion replaces them) - if( hasSelection() ) - { - deleteSelection(TRUE); - } - - setCursorPos(mCursorPos + insert( mCursorPos, utf8str_to_wstring(new_text), FALSE, LLTextSegmentPtr() )); - - setEnabled( enabled ); -} - -void LLTextEditor::insertText(LLWString &new_text) -{ - BOOL enabled = getEnabled(); - setEnabled( TRUE ); - - // Delete any selected characters (the insertion replaces them) - if( hasSelection() ) - { - deleteSelection(TRUE); - } - - setCursorPos(mCursorPos + insert( mCursorPos, new_text, FALSE, LLTextSegmentPtr() )); - - setEnabled( enabled ); -} - -void LLTextEditor::appendWidget(const LLInlineViewSegment::Params& params, const std::string& text, bool allow_undo) -{ - // Save old state - S32 selection_start = mSelectionStart; - S32 selection_end = mSelectionEnd; - BOOL was_selecting = mIsSelecting; - S32 cursor_pos = mCursorPos; - S32 old_length = getLength(); - BOOL cursor_was_at_end = (mCursorPos == old_length); - - deselect(); - - setCursorPos(old_length); - - LLWString widget_wide_text = utf8str_to_wstring(text); - - LLTextSegmentPtr segment = new LLInlineViewSegment(params, old_length, old_length + widget_wide_text.size()); - insert(getLength(), widget_wide_text, FALSE, segment); - - // Set the cursor and scroll position - if( selection_start != selection_end ) - { - mSelectionStart = selection_start; - mSelectionEnd = selection_end; - - mIsSelecting = was_selecting; - setCursorPos(cursor_pos); - } - else if( cursor_was_at_end ) - { - setCursorPos(getLength()); - } - else - { - setCursorPos(cursor_pos); - } - - if (!allow_undo) - { - blockUndo(); - } -} - -void LLTextEditor::removeTextFromEnd(S32 num_chars) -{ - if (num_chars <= 0) return; - - remove(getLength() - num_chars, num_chars, FALSE); - - S32 len = getLength(); - setCursorPos (llclamp(mCursorPos, 0, len)); - mSelectionStart = llclamp(mSelectionStart, 0, len); - mSelectionEnd = llclamp(mSelectionEnd, 0, len); - - needsScroll(); -} - -//---------------------------------------------------------------------------- - -void LLTextEditor::onSpellCheckPerformed() -{ - if (isPristine()) - { - mBaseDocIsPristine = FALSE; - } -} - -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--; - } - } - } - - return isPristine(); // TRUE => success -} - -void LLTextEditor::updateLinkSegments() -{ - LLWString wtext = getWText(); - - // update any segments that contain a link - for (segment_set_t::iterator it = mSegments.begin(); it != mSegments.end(); ++it) - { - LLTextSegment *segment = *it; - if (segment && segment->getStyle() && segment->getStyle()->isLink()) - { - LLStyleConstSP style = segment->getStyle(); - LLStyleSP new_style(new LLStyle(*style)); - LLWString url_label = wtext.substr(segment->getStart(), segment->getEnd()-segment->getStart()); - - segment_set_t::const_iterator next_it = mSegments.upper_bound(segment); - LLTextSegment *next_segment = *next_it; - if (next_segment) - { - LLWString next_url_label = wtext.substr(next_segment->getStart(), next_segment->getEnd()-next_segment->getStart()); - std::string link_check = wstring_to_utf8str(url_label) + wstring_to_utf8str(next_url_label); - LLUrlMatch match; - - if ( LLUrlRegistry::instance().findUrl(link_check, match)) - { - if(match.getQuery() == wstring_to_utf8str(next_url_label)) - { - continue; - } - } - } - - // if the link's label (what the user can edit) is a valid Url, - // then update the link's HREF to be the same as the label text. - // This lets users edit Urls in-place. - if (acceptsTextInput() && LLUrlRegistry::instance().hasUrl(url_label)) - { - std::string new_url = wstring_to_utf8str(url_label); - LLStringUtil::trim(new_url); - new_style->setLinkHREF(new_url); - LLStyleConstSP sp(new_style); - segment->setStyle(sp); - } - } - } -} - - - -void LLTextEditor::onMouseCaptureLost() -{ - endSelection(); -} - -/////////////////////////////////////////////////////////////////// -// Hack for Notecards - -BOOL LLTextEditor::importBuffer(const char* buffer, S32 length ) -{ - std::istringstream instream(buffer); - - // Version 1 format: - // Linden text version 1\n - // {\n - // <EmbeddedItemList chunk> - // Text length <bytes without \0>\n - // <text without \0> (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) - { - LLError::LLUserWarningMsg::showOutOfMemory(); - 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; - - startOfDoc(); - deselect(); - - 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", getLength() ); - outstream << getText(); - outstream << "}\n"; - - return TRUE; -} - -void LLTextEditor::updateAllowingLanguageInput() -{ - LLWindow* window = getWindow(); - if (!window) - { - // test app, no window available - return; - } - if (hasFocus() && !mReadOnly) - { - window->allowLanguageTextInput(this, TRUE); - } - else - { - window->allowLanguageTextInput(this, FALSE); - } -} - -// Preedit is managed off the undo/redo command stack. - -BOOL LLTextEditor::hasPreeditString() const -{ - return (mPreeditPositions.size() > 1); -} - -void LLTextEditor::resetPreedit() -{ - if (hasSelection()) - { - if (hasPreeditString()) - { - LL_WARNS() << "Preedit and selection!" << LL_ENDL; - deselect(); - } - else - { - deleteSelection(TRUE); - } - } - if (hasPreeditString()) - { - if (hasSelection()) - { - LL_WARNS() << "Preedit and selection!" << LL_ENDL; - deselect(); - } - - setCursorPos(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 = getWText().substr(insert_preedit_at, mPreeditWString.length()); - removeStringNoUndo(insert_preedit_at, mPreeditWString.length()); - } - else - { - mPreeditOverwrittenWString.clear(); - } - - segment_vec_t segments; - //pass empty segments to let "insertStringNoUndo" make new LLNormalTextSegment and insert it, if needed. - insertStringNoUndo(insert_preedit_at, mPreeditWString, &segments); - - mPreeditStandouts = preedit_standouts; - - setCursorPos(insert_preedit_at + caret_position); - - // Update of the preedit should be caused by some key strokes. - resetCursorBlink(); - - onKeyStroke(); -} - -BOOL LLTextEditor::getPreeditLocation(S32 query_offset, LLCoordGL *coord, LLRect *bounds, LLRect *control) const -{ - if (control) - { - LLRect control_rect_screen; - localRectToScreen(mVisibleTextRect, &control_rect_screen); - LLUI::getInstance()->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 = getFirstVisibleLine(); - 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 LLWString textString(getWText()); - const llwchar * const text = textString.c_str(); - const S32 line_height = mFont->getLineHeight(); - - if (coord) - { - const S32 query_x = mVisibleTextRect.mLeft + mFont->getWidth(text, current_line_start, query - current_line_start); - const S32 query_y = mVisibleTextRect.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::getInstance()->screenPointToGL(query_screen_x, query_screen_y, &coord->mX, &coord->mY); - } - - if (bounds) - { - S32 preedit_left = mVisibleTextRect.mLeft; - if (preedit_left_position > current_line_start) - { - preedit_left += mFont->getWidth(text, current_line_start, preedit_left_position - current_line_start); - } - - S32 preedit_right = mVisibleTextRect.mLeft; - if (preedit_right_position < current_line_end) - { - preedit_right += mFont->getWidth(text, current_line_start, preedit_right_position - current_line_start); - } - else - { - preedit_right += mFont->getWidth(text, current_line_start, current_line_end - current_line_start); - } - - const S32 preedit_top = mVisibleTextRect.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::getInstance()->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( getWText(), 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((F32)mFont->getLineHeight() * LLUI::getScaleFactor().mV[VY]); -} - -BOOL LLTextEditor::isDirty() const -{ - if(mReadOnly) - { - return FALSE; - } - - if( mPristineCmd ) - { - return ( mPristineCmd == mLastCmd ); - } - else - { - return ( NULL != mLastCmd ); - } -} - -void LLTextEditor::setKeystrokeCallback(const keystroke_signal_t::slot_type& callback) -{ - mKeystrokeSignal.connect(callback); -} - -void LLTextEditor::onKeyStroke() -{ - mKeystrokeSignal(this); - - mSpellCheckStart = mSpellCheckEnd = -1; - mSpellCheckTimer.setTimerExpirySec(SPELLCHECK_DELAY); -} - -//virtual -void LLTextEditor::clear() -{ - getViewModel()->setDisplay(LLWStringUtil::null); - clearSegments(); -} - -bool LLTextEditor::canLoadOrSaveToFile() -{ - return !mReadOnly; -} - -S32 LLTextEditor::spacesPerTab() -{ - return SPACES_PER_TAB; -} +/**
+ * @file lltexteditor.cpp
+ *
+ * $LicenseInfo:firstyear=2001&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2010, Linden Research, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
+ * $/LicenseInfo$
+ */
+
+// Text editor widget to let users enter a a multi-line ASCII document.
+
+#include "linden_common.h"
+
+#define LLTEXTEDITOR_CPP
+#include "lltexteditor.h"
+
+#include "llfontfreetype.h" // for LLFontFreetype::FIRST_CHAR
+#include "llfontgl.h"
+#include "llgl.h" // LLGLSUIDefault()
+#include "lllocalcliprect.h"
+#include "llrender.h"
+#include "llui.h"
+#include "lluictrlfactory.h"
+#include "llrect.h"
+#include "llfocusmgr.h"
+#include "lltimer.h"
+#include "llmath.h"
+
+#include "llclipboard.h"
+#include "llemojihelper.h"
+#include "llscrollbar.h"
+#include "llstl.h"
+#include "llstring.h"
+#include "llkeyboard.h"
+#include "llkeywords.h"
+#include "llundo.h"
+#include "llviewborder.h"
+#include "llcontrol.h"
+#include "llwindow.h"
+#include "lltextparser.h"
+#include "llscrollcontainer.h"
+#include "llspellcheck.h"
+#include "llpanel.h"
+#include "llurlregistry.h"
+#include "lltooltip.h"
+#include "llmenugl.h"
+
+#include <queue>
+#include "llcombobox.h"
+
+//
+// Globals
+//
+static LLDefaultChildRegistry::Register<LLTextEditor> r("simple_text_editor");
+
+// Compiler optimization, generate extern template
+template class LLTextEditor* LLView::getChild<class LLTextEditor>(
+ const std::string& name, bool recurse) const;
+
+//
+// Constants
+//
+const S32 SPACES_PER_TAB = 4;
+const F32 SPELLCHECK_DELAY = 0.5f; // delay between the last keypress and spell checking the word the cursor is on
+
+///////////////////////////////////////////////////////////////////
+
+class LLTextEditor::TextCmdInsert : public LLTextBase::TextCmd
+{
+public:
+ TextCmdInsert(S32 pos, bool group_with_next, const LLWString &ws, LLTextSegmentPtr segment)
+ : TextCmd(pos, group_with_next, segment), mWString(ws)
+ {
+ }
+ virtual ~TextCmdInsert() {}
+ virtual bool execute( LLTextBase* editor, S32* delta )
+ {
+ *delta = insert(editor, getPosition(), mWString );
+ LLWStringUtil::truncate(mWString, *delta);
+ //mWString = wstring_truncate(mWString, *delta);
+ return (*delta != 0);
+ }
+ virtual S32 undo( LLTextBase* editor )
+ {
+ remove(editor, getPosition(), mWString.length() );
+ return getPosition();
+ }
+ virtual S32 redo( LLTextBase* editor )
+ {
+ insert(editor, getPosition(), mWString );
+ return getPosition() + mWString.length();
+ }
+
+private:
+ LLWString mWString;
+};
+
+///////////////////////////////////////////////////////////////////
+class LLTextEditor::TextCmdAddChar : public LLTextBase::TextCmd
+{
+public:
+ TextCmdAddChar( S32 pos, bool group_with_next, llwchar wc, LLTextSegmentPtr segment)
+ : TextCmd(pos, group_with_next, segment), mWString(1, wc), mBlockExtensions(false)
+ {
+ }
+ virtual void blockExtensions()
+ {
+ mBlockExtensions = true;
+ }
+ virtual bool canExtend(S32 pos) const
+ {
+ // cannot extend text with custom segments
+ if (!mSegments.empty()) return false;
+
+ return !mBlockExtensions && (pos == getPosition() + (S32)mWString.length());
+ }
+ virtual bool execute( LLTextBase* editor, S32* delta )
+ {
+ *delta = insert(editor, getPosition(), mWString);
+ LLWStringUtil::truncate(mWString, *delta);
+ //mWString = wstring_truncate(mWString, *delta);
+ return (*delta != 0);
+ }
+ virtual bool extendAndExecute( LLTextBase* 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( LLTextBase* editor )
+ {
+ remove(editor, getPosition(), mWString.length() );
+ return getPosition();
+ }
+ virtual S32 redo( LLTextBase* editor )
+ {
+ insert(editor, getPosition(), mWString );
+ return getPosition() + mWString.length();
+ }
+
+private:
+ LLWString mWString;
+ bool mBlockExtensions;
+
+};
+
+///////////////////////////////////////////////////////////////////
+
+class LLTextEditor::TextCmdOverwriteChar : public LLTextBase::TextCmd
+{
+public:
+ TextCmdOverwriteChar( S32 pos, bool group_with_next, llwchar wc)
+ : TextCmd(pos, group_with_next), mChar(wc), mOldChar(0) {}
+
+ virtual bool execute( LLTextBase* editor, S32* delta )
+ {
+ mOldChar = editor->getWText()[getPosition()];
+ overwrite(editor, getPosition(), mChar);
+ *delta = 0;
+ return true;
+ }
+ virtual S32 undo( LLTextBase* editor )
+ {
+ overwrite(editor, getPosition(), mOldChar);
+ return getPosition();
+ }
+ virtual S32 redo( LLTextBase* editor )
+ {
+ overwrite(editor, getPosition(), mChar);
+ return getPosition()+1;
+ }
+
+private:
+ llwchar mChar;
+ llwchar mOldChar;
+};
+
+///////////////////////////////////////////////////////////////////
+
+class LLTextEditor::TextCmdRemove : public LLTextBase::TextCmd
+{
+public:
+ TextCmdRemove( S32 pos, bool group_with_next, S32 len, segment_vec_t& segments ) :
+ TextCmd(pos, group_with_next), mLen(len)
+ {
+ std::swap(mSegments, segments);
+ }
+ virtual bool execute( LLTextBase* editor, S32* delta )
+ {
+ mWString = editor->getWText().substr(getPosition(), mLen);
+ *delta = remove(editor, getPosition(), mLen );
+ return (*delta != 0);
+ }
+ virtual S32 undo( LLTextBase* editor )
+ {
+ insert(editor, getPosition(), mWString);
+ return getPosition() + mWString.length();
+ }
+ virtual S32 redo( LLTextBase* editor )
+ {
+ remove(editor, getPosition(), mLen );
+ return getPosition();
+ }
+private:
+ LLWString mWString;
+ S32 mLen;
+};
+
+
+///////////////////////////////////////////////////////////////////
+LLTextEditor::Params::Params()
+: default_text("default_text"),
+ prevalidator("prevalidator"),
+ embedded_items("embedded_items", false),
+ ignore_tab("ignore_tab", true),
+ auto_indent("auto_indent", true),
+ default_color("default_color"),
+ commit_on_focus_lost("commit_on_focus_lost", false),
+ show_context_menu("show_context_menu"),
+ show_emoji_helper("show_emoji_helper"),
+ enable_tooltip_paste("enable_tooltip_paste")
+{
+ addSynonym(prevalidator, "prevalidate_callback");
+ addSynonym(prevalidator, "text_type");
+}
+
+LLTextEditor::LLTextEditor(const LLTextEditor::Params& p) :
+ LLTextBase(p),
+ mAutoreplaceCallback(),
+ mBaseDocIsPristine(true),
+ mPristineCmd( NULL ),
+ mLastCmd( NULL ),
+ mDefaultColor( p.default_color() ),
+ mAutoIndent(p.auto_indent),
+ mParseOnTheFly(false),
+ mCommitOnFocusLost( p.commit_on_focus_lost),
+ mAllowEmbeddedItems( p.embedded_items ),
+ mMouseDownX(0),
+ mMouseDownY(0),
+ mTabsToNextField(p.ignore_tab),
+ mPrevalidator(p.prevalidator()),
+ mShowContextMenu(p.show_context_menu),
+ mShowEmojiHelper(p.show_emoji_helper),
+ mEnableTooltipPaste(p.enable_tooltip_paste),
+ mPassDelete(false),
+ mKeepSelectionOnReturn(false)
+{
+ mSourceID.generate();
+
+ //FIXME: use image?
+ LLViewBorder::Params params;
+ params.name = "text ed border";
+ params.rect = getLocalRect();
+ params.bevel_style = LLViewBorder::BEVEL_IN;
+ params.border_thickness = 1;
+ params.visible = p.border_visible;
+ mBorder = LLUICtrlFactory::create<LLViewBorder> (params);
+ addChild( mBorder );
+ setText(p.default_text());
+
+ mParseOnTheFly = true;
+}
+
+void LLTextEditor::initFromParams( const LLTextEditor::Params& p)
+{
+ LLTextBase::initFromParams(p);
+
+ // HACK: text editors always need to be enabled so that we can scroll
+ LLView::setEnabled(true);
+
+ if (p.commit_on_focus_lost.isProvided())
+ {
+ mCommitOnFocusLost = p.commit_on_focus_lost;
+ }
+
+ updateAllowingLanguageInput();
+}
+
+LLTextEditor::~LLTextEditor()
+{
+ gFocusMgr.releaseFocusIfNeeded( this ); // calls onCommit() while LLTextEditor still valid
+
+ // Scrollbar is deleted by LLView
+ std::for_each(mUndoStack.begin(), mUndoStack.end(), DeletePointer());
+ mUndoStack.clear();
+ // Mark the menu as dead or its retained in memory till shutdown.
+ LLContextMenu* menu = static_cast<LLContextMenu*>(mContextMenuHandle.get());
+ if(menu)
+ {
+ menu->die();
+ mContextMenuHandle.markDead();
+ }
+}
+
+////////////////////////////////////////////////////////////
+// LLTextEditor
+// Public methods
+
+void LLTextEditor::setText(const LLStringExplicit &utf8str, const LLStyle::Params& input_params)
+{
+ // validate incoming text if necessary
+ if (mPrevalidator)
+ {
+ if (!mPrevalidator.validate(utf8str))
+ {
+ LLUI::getInstance()->reportBadKeystroke();
+ mPrevalidator.showLastErrorUsingTimeout();
+
+ // not valid text, nothing to do
+ return;
+ }
+ }
+
+ blockUndo();
+ deselect();
+
+ mParseOnTheFly = false;
+ LLTextBase::setText(utf8str, input_params);
+ mParseOnTheFly = true;
+
+ resetDirty();
+}
+
+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.
+ setCursorPos(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);
+
+ 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)
+{
+ startOfDoc();
+ selectNext(search_text, case_insensitive, false);
+
+ bool replaced = true;
+ while ( replaced )
+ {
+ replaced = replaceText(search_text,replace_text, case_insensitive, false);
+ }
+}
+
+S32 LLTextEditor::prevWordPos(S32 cursorPos) const
+{
+ LLWString wtext(getWText());
+ while( (cursorPos > 0) && (wtext[cursorPos-1] == ' ') )
+ {
+ cursorPos--;
+ }
+ while( (cursorPos > 0) && LLWStringUtil::isPartOfWord( wtext[cursorPos-1] ) )
+ {
+ cursorPos--;
+ }
+ return cursorPos;
+}
+
+S32 LLTextEditor::nextWordPos(S32 cursorPos) const
+{
+ LLWString wtext(getWText());
+ while( (cursorPos < getLength()) && LLWStringUtil::isPartOfWord( wtext[cursorPos] ) )
+ {
+ cursorPos++;
+ }
+ while( (cursorPos < getLength()) && (wtext[cursorPos] == ' ') )
+ {
+ cursorPos++;
+ }
+ return cursorPos;
+}
+
+const LLTextSegmentPtr LLTextEditor::getPreviousSegment() const
+{
+ static LLPointer<LLIndexSegment> index_segment = new LLIndexSegment;
+
+ index_segment->setStart(mCursorPos);
+ index_segment->setEnd(mCursorPos);
+
+ // find segment index at character to left of cursor (or rightmost edge of selection)
+ segment_set_t::const_iterator it = mSegments.lower_bound(index_segment);
+
+ if (it != mSegments.end())
+ {
+ return *it;
+ }
+ else
+ {
+ return LLTextSegmentPtr();
+ }
+}
+
+void LLTextEditor::getSelectedSegments(LLTextEditor::segment_vec_t& segments) const
+{
+ S32 left = hasSelection() ? llmin(mSelectionStart, mSelectionEnd) : mCursorPos;
+ S32 right = hasSelection() ? llmax(mSelectionStart, mSelectionEnd) : mCursorPos;
+
+ return getSegmentsInRange(segments, left, right, true);
+}
+
+void LLTextEditor::getSegmentsInRange(LLTextEditor::segment_vec_t& segments_out, S32 start, S32 end, bool include_partial) const
+{
+ segment_set_t::const_iterator first_it = getSegIterContaining(start);
+ segment_set_t::const_iterator end_it = getSegIterContaining(end - 1);
+ if (end_it != mSegments.end()) ++end_it;
+
+ for (segment_set_t::const_iterator it = first_it; it != end_it; ++it)
+ {
+ LLTextSegmentPtr segment = *it;
+ if (include_partial
+ || (segment->getStart() >= start
+ && segment->getEnd() <= end))
+ {
+ segments_out.push_back(segment);
+ }
+ }
+}
+
+void LLTextEditor::setShowEmojiHelper(bool show)
+{
+ if (!mShowEmojiHelper)
+ {
+ LLEmojiHelper::instance().hideHelper(this);
+ }
+
+ mShowEmojiHelper = show;
+}
+
+bool LLTextEditor::selectionContainsLineBreaks()
+{
+ if (hasSelection())
+ {
+ S32 left = llmin(mSelectionStart, mSelectionEnd);
+ S32 right = left + llabs(mSelectionStart - mSelectionEnd);
+
+ LLWString wtext = getWText();
+ 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++)
+ {
+ LLWString wtext = getWText();
+ if (wtext[pos] == ' ')
+ {
+ delta_spaces += remove( pos, 1, false );
+ }
+ }
+ }
+
+ return delta_spaces;
+}
+
+void LLTextEditor::indentSelectedLines( S32 spaces )
+{
+ if( hasSelection() )
+ {
+ LLWString text = getWText();
+ 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++;
+ }
+ }
+
+ // Disabling parsing on the fly to avoid updating text segments
+ // until all indentation commands are executed.
+ mParseOnTheFly = false;
+
+ // 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 = getWText();
+
+ // Find the next new line
+ while( (cur < right) && (text[cur] != '\n') )
+ {
+ cur++;
+ }
+ }
+ while( cur < right );
+
+ mParseOnTheFly = true;
+
+ 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;
+ }
+ setCursorPos(mSelectionEnd);
+ }
+}
+
+//virtual
+bool LLTextEditor::canSelectAll() const
+{
+ return true;
+}
+
+// virtual
+void LLTextEditor::selectAll()
+{
+ mSelectionStart = getLength();
+ mSelectionEnd = 0;
+ setCursorPos(mSelectionEnd);
+ updatePrimary();
+}
+
+void LLTextEditor::selectByCursorPosition(S32 prev_cursor_pos, S32 next_cursor_pos)
+{
+ setCursorPos(prev_cursor_pos);
+ startSelection();
+ setCursorPos(next_cursor_pos);
+ endSelection();
+}
+
+void LLTextEditor::insertEmoji(llwchar emoji)
+{
+ LL_INFOS() << "LLTextEditor::insertEmoji(" << wchar_utf8_preview(emoji) << ")" << LL_ENDL;
+ auto styleParams = LLStyle::Params();
+ styleParams.font = LLFontGL::getFontEmojiLarge();
+ auto segment = new LLEmojiTextSegment(new LLStyle(styleParams), mCursorPos, mCursorPos + 1, *this);
+ insert(mCursorPos, LLWString(1, emoji), false, segment);
+ setCursorPos(mCursorPos + 1);
+}
+
+void LLTextEditor::handleEmojiCommit(llwchar emoji)
+{
+ S32 shortCodePos;
+ if (LLEmojiHelper::isCursorInEmojiCode(getWText(), mCursorPos, &shortCodePos))
+ {
+ remove(shortCodePos, mCursorPos - shortCodePos, true);
+ setCursorPos(shortCodePos);
+
+ insertEmoji(emoji);
+ }
+}
+
+bool LLTextEditor::handleMouseDown(S32 x, S32 y, MASK mask)
+{
+ bool handled = false;
+
+ // set focus first, in case click callbacks want to change it
+ // RN: do we really need to have a tab stop?
+ if (hasTabStop())
+ {
+ setFocus( true );
+ }
+
+ // Let scrollbar have first dibs
+ handled = LLTextBase::handleMouseDown(x, y, mask);
+
+ if( !handled )
+ {
+ 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())
+ {
+ mSelectionEnd = mCursorPos;
+ }
+ else
+ {
+ mSelectionStart = old_cursor_pos;
+ mSelectionEnd = mCursorPos;
+ }
+ // assume we're starting a drag select
+ mIsSelecting = true;
+ }
+ else
+ {
+ setCursorAtLocalPos( x, y, true );
+ startSelection();
+ }
+ }
+
+ handled = true;
+ }
+
+ // Delay cursor flashing
+ resetCursorBlink();
+
+ if (handled && !gFocusMgr.getMouseCapture())
+ {
+ gFocusMgr.setMouseCapture( this );
+ }
+ return handled;
+}
+
+bool LLTextEditor::handleRightMouseDown(S32 x, S32 y, MASK mask)
+{
+ if (hasTabStop())
+ {
+ setFocus(true);
+ }
+
+ bool show_menu = false;
+
+ // Prefer editor menu if it has selection. See EXT-6806.
+ if (hasSelection())
+ {
+ S32 click_pos = getDocIndexFromLocalCoord(x, y, false);
+ if (click_pos > mSelectionStart && click_pos < mSelectionEnd)
+ {
+ show_menu = true;
+ }
+ }
+
+ // Let segments handle the click, if nothing does, show editor menu
+ if (!show_menu && !LLTextBase::handleRightMouseDown(x, y, mask))
+ {
+ show_menu = true;
+ }
+
+ if (show_menu && getShowContextMenu())
+ {
+ showContextMenu(x, y);
+ }
+
+ return true;
+}
+
+
+
+bool LLTextEditor::handleMiddleMouseDown(S32 x, S32 y, MASK mask)
+{
+ if (hasTabStop())
+ {
+ setFocus(true);
+ }
+
+ if (!LLTextBase::handleMouseDown(x, y, mask))
+ {
+ if( canPastePrimary() )
+ {
+ setCursorAtLocalPos( x, y, true );
+ // does not rely on focus being set
+ pastePrimary();
+ }
+ }
+ return true;
+}
+
+
+bool LLTextEditor::handleHover(S32 x, S32 y, MASK mask)
+{
+ bool handled = false;
+
+ if(hasMouseCapture() )
+ {
+ if( mIsSelecting )
+ {
+ if(mScroller)
+ {
+ mScroller->autoScroll(x, y);
+ }
+ S32 clamped_x = llclamp(x, mVisibleTextRect.mLeft, mVisibleTextRect.mRight);
+ S32 clamped_y = llclamp(y, mVisibleTextRect.mBottom, mVisibleTextRect.mTop);
+ setCursorAtLocalPos( clamped_x, clamped_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 = LLTextBase::handleHover(x, y, mask);
+ }
+
+ if( handled )
+ {
+ // Delay cursor flashing
+ resetCursorBlink();
+ }
+
+ if( !handled )
+ {
+ getWindow()->setCursor(UI_CURSOR_IBEAM);
+ handled = true;
+ }
+
+ return handled;
+}
+
+
+bool LLTextEditor::handleMouseUp(S32 x, S32 y, MASK mask)
+{
+ bool handled = false;
+
+ // if I'm not currently selecting text
+ if (!(mIsSelecting && hasMouseCapture()))
+ {
+ // let text segments handle mouse event
+ handled = LLTextBase::handleMouseUp(x, y, mask);
+ }
+
+ if( !handled )
+ {
+ if( mIsSelecting )
+ {
+ if(mScroller)
+ {
+ mScroller->autoScroll(x, y);
+ }
+ S32 clamped_x = llclamp(x, mVisibleTextRect.mLeft, mVisibleTextRect.mRight);
+ S32 clamped_y = llclamp(y, mVisibleTextRect.mBottom, mVisibleTextRect.mTop);
+ setCursorAtLocalPos( clamped_x, clamped_y, true );
+ endSelection();
+ }
+
+ // take selection to 'primary' clipboard
+ updatePrimary();
+
+ handled = true;
+ }
+
+ // Delay cursor flashing
+ resetCursorBlink();
+
+ if( hasMouseCapture() )
+ {
+ gFocusMgr.setMouseCapture( NULL );
+
+ handled = true;
+ }
+
+ return handled;
+}
+
+
+bool LLTextEditor::handleDoubleClick(S32 x, S32 y, MASK mask)
+{
+ bool handled = false;
+
+ // let scrollbar and text segments have first dibs
+ handled = LLTextBase::handleDoubleClick(x, y, mask);
+
+ if( !handled )
+ {
+ setCursorAtLocalPos( x, y, false );
+ deselect();
+
+ LLWString text = getWText();
+
+ if( LLWStringUtil::isPartOfWord( text[mCursorPos] ) )
+ {
+ // Select word the cursor is over
+ while ((mCursorPos > 0) && LLWStringUtil::isPartOfWord(text[mCursorPos-1]))
+ {
+ if (!setCursorPos(mCursorPos - 1)) break;
+ }
+ startSelection();
+
+ while ((mCursorPos < (S32)text.length()) && LLWStringUtil::isPartOfWord( text[mCursorPos] ) )
+ {
+ if (!setCursorPos(mCursorPos + 1)) break;
+ }
+
+ mSelectionEnd = mCursorPos;
+ }
+ else if ((mCursorPos < (S32)text.length()) && !iswspace( text[mCursorPos]) )
+ {
+ // Select the character the cursor is over
+ startSelection();
+ setCursorPos(mCursorPos + 1);
+ 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
+ resetCursorBlink();
+
+ // take selection to 'primary' clipboard
+ updatePrimary();
+
+ handled = true;
+ }
+
+ return handled;
+}
+
+
+//----------------------------------------------------------------------------
+// Returns change in number of characters in mText
+
+S32 LLTextEditor::execute( TextCmd* cmd )
+{
+ if (!mReadOnly && mShowEmojiHelper)
+ {
+ // Any change to our contents should always hide the helper
+ LLEmojiHelper::instance().hideHelper(this);
+ }
+
+ 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);
+ 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;
+
+ bool need_to_rollback = mPrevalidator && !mPrevalidator.validate(getViewModel()->getDisplay());
+ if (need_to_rollback)
+ {
+ LLUI::getInstance()->reportBadKeystroke();
+ mPrevalidator.showLastErrorUsingTimeout();
+
+ // get rid of this last command and clean up undo stack
+ undo();
+
+ // remove any evidence of this command from redo history
+ mUndoStack.pop_front();
+ delete cmd;
+
+ // failure, nothing changed
+ delta = 0;
+ }
+ }
+ else
+ {
+ // Operation failed, so don't put it on the undo stack.
+ delete cmd;
+ }
+
+ return delta;
+}
+
+S32 LLTextEditor::insert(S32 pos, const LLWString &wstr, bool group_with_next_op, LLTextSegmentPtr segment)
+{
+ return execute( new TextCmdInsert( pos, group_with_next_op, wstr, segment ) );
+}
+
+S32 LLTextEditor::remove(S32 pos, S32 length, bool group_with_next_op)
+{
+ S32 end_pos = getEditableIndex(pos + length, true);
+ bool removedChar = false;
+
+ segment_vec_t segments_to_remove;
+ // store text segments
+ getSegmentsInRange(segments_to_remove, pos, pos + length, false);
+
+ if (pos <= end_pos)
+ {
+ removedChar = execute( new TextCmdRemove( pos, group_with_next_op, end_pos - pos, segments_to_remove ) );
+ }
+
+ return removedChar;
+}
+
+S32 LLTextEditor::overwriteChar(S32 pos, llwchar wc)
+{
+ if ((S32)getLength() == pos)
+ {
+ return addChar(pos, wc);
+ }
+ else
+ {
+ return execute(new TextCmdOverwriteChar(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;
+
+ LLWString text = getWText();
+ if (text[mCursorPos - 1] == ' ')
+ {
+ // Try to remove a "tab"
+ S32 offset = getLineOffsetFromDocIndex(mCursorPos);
+ 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);
+ }
+
+ tryToShowEmojiHelper();
+ }
+ else
+ {
+ LLUI::getInstance()->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);
+ tryToShowEmojiHelper();
+ }
+ else
+ {
+ LLUI::getInstance()->reportBadKeystroke();
+ }
+}
+
+// Add a single character to the text
+S32 LLTextEditor::addChar(S32 pos, llwchar wc)
+{
+ if ((wstring_utf8_length(getWText()) + wchar_utf8_length(wc)) > mMaxTextByteLength)
+ {
+ LLUI::getInstance()->reportBadKeystroke();
+ return 0;
+ }
+
+ if (mLastCmd && mLastCmd->canExtend(pos))
+ {
+ if (mPrevalidator)
+ {
+ // get a copy of current text contents
+ LLWString test_string(getViewModel()->getDisplay());
+
+ // modify text contents as if this addChar succeeded
+ llassert(pos <= (S32)test_string.size());
+ test_string.insert(pos, 1, wc);
+ if (!mPrevalidator.validate(test_string))
+ {
+ LLUI::getInstance()->reportBadKeystroke();
+ mPrevalidator.showLastErrorUsingTimeout();
+ return 0;
+ }
+ }
+
+ S32 delta = 0;
+ mLastCmd->extendAndExecute(this, pos, wc, &delta);
+
+ return delta;
+ }
+
+ return execute(new TextCmdAddChar(pos, false, wc, LLTextSegmentPtr()));
+}
+
+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 ));
+ tryToShowEmojiHelper();
+
+ if (!mReadOnly && mAutoreplaceCallback != NULL)
+ {
+ // autoreplace the text, if necessary
+ S32 replacement_start;
+ S32 replacement_length;
+ LLWString replacement_string;
+ S32 new_cursor_pos = mCursorPos;
+ mAutoreplaceCallback(replacement_start, replacement_length, replacement_string, new_cursor_pos, getWText());
+
+ if (replacement_length > 0 || !replacement_string.empty())
+ {
+ remove(replacement_start, replacement_length, true);
+ insert(replacement_start, replacement_string, false, LLTextSegmentPtr());
+ setCursorPos(new_cursor_pos);
+ }
+ }
+}
+
+void LLTextEditor::showEmojiHelper()
+{
+ if (mReadOnly || !mShowEmojiHelper)
+ return;
+
+ const LLRect cursorRect(getLocalRectFromDocIndex(mCursorPos));
+ auto cb = [this](llwchar emoji) { insertEmoji(emoji); };
+ LLEmojiHelper::instance().showHelper(this, cursorRect.mLeft, cursorRect.mTop, LLStringUtil::null, cb);
+}
+
+void LLTextEditor::tryToShowEmojiHelper()
+{
+ if (mReadOnly || !mShowEmojiHelper)
+ return;
+
+ S32 shortCodePos;
+ LLWString wtext(getWText());
+ if (LLEmojiHelper::isCursorInEmojiCode(wtext, mCursorPos, &shortCodePos))
+ {
+ const LLRect cursorRect(getLocalRectFromDocIndex(shortCodePos));
+ const LLWString wpart(wtext.substr(shortCodePos, mCursorPos - shortCodePos));
+ const std::string part(wstring_to_utf8str(wpart));
+ auto cb = [this](llwchar emoji) { handleEmojiCommit(emoji); };
+ LLEmojiHelper::instance().showHelper(this, cursorRect.mLeft, cursorRect.mTop, part, cb);
+ }
+ else
+ {
+ LLEmojiHelper::instance().hideHelper();
+ }
+}
+
+void LLTextEditor::addLineBreakChar(bool group_together)
+{
+ if( !getEnabled() )
+ {
+ return;
+ }
+ if( hasSelection() )
+ {
+ deleteSelection(true);
+ }
+ else if (LL_KIM_OVERWRITE == gKeyboard->getInsertMode())
+ {
+ removeChar(mCursorPos);
+ }
+
+ LLStyleConstSP sp(new LLStyle(LLStyle::Params()));
+ LLTextSegmentPtr segment = new LLLineBreakTextSegment(sp, mCursorPos);
+
+ S32 pos = execute(new TextCmdAddChar(mCursorPos, group_together, '\n', segment));
+
+ setCursorPos(mCursorPos + pos);
+}
+
+
+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();
+ setCursorPos(mCursorPos - 1);
+ if( mask & MASK_CONTROL )
+ {
+ setCursorPos(prevWordPos(mCursorPos));
+ }
+ mSelectionEnd = mCursorPos;
+ }
+ break;
+
+ case KEY_RIGHT:
+ if( mCursorPos < getLength() )
+ {
+ startSelection();
+ setCursorPos(mCursorPos + 1);
+ if( mask & MASK_CONTROL )
+ {
+ setCursorPos(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 )
+ {
+ setCursorPos(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 )
+ {
+ setCursorPos(getLength());
+ }
+ else
+ {
+ endOfLine();
+ }
+ mSelectionEnd = mCursorPos;
+ break;
+
+ default:
+ handled = false;
+ break;
+ }
+ }
+
+ 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:
+ changeLine( -1 );
+ break;
+
+ case KEY_PAGE_UP:
+ changePage( -1 );
+ break;
+
+ case KEY_HOME:
+ startOfLine();
+ break;
+
+ case KEY_DOWN:
+ changeLine( 1 );
+ deselect();
+ break;
+
+ case KEY_PAGE_DOWN:
+ changePage( 1 );
+ break;
+
+ case KEY_END:
+ endOfLine();
+ break;
+
+ case KEY_LEFT:
+ if( hasSelection() )
+ {
+ setCursorPos(llmin( mSelectionStart, mSelectionEnd ));
+ }
+ else
+ {
+ if( 0 < mCursorPos )
+ {
+ setCursorPos(mCursorPos - 1);
+ }
+ else
+ {
+ LLUI::getInstance()->reportBadKeystroke();
+ }
+ }
+ break;
+
+ case KEY_RIGHT:
+ if( hasSelection() )
+ {
+ setCursorPos(llmax( mSelectionStart, mSelectionEnd ));
+ }
+ else
+ {
+ if( mCursorPos < getLength() )
+ {
+ setCursorPos(mCursorPos + 1);
+ }
+ else
+ {
+ LLUI::getInstance()->reportBadKeystroke();
+ }
+ }
+ break;
+
+ default:
+ handled = false;
+ break;
+ }
+ }
+
+ if (handled)
+ {
+ deselect();
+ }
+
+ 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 );
+ LLClipboard::instance().copyToClipboard( getWText(), left_pos, length);
+ deleteSelection( false );
+
+ onKeyStroke();
+}
+
+bool LLTextEditor::canCopy() const
+{
+ return hasSelection();
+}
+
+// copy selection to clipboard
+void LLTextEditor::copy()
+{
+ if( !canCopy() )
+ {
+ return;
+ }
+ S32 left_pos = llmin( mSelectionStart, mSelectionEnd );
+ S32 length = llabs( mSelectionStart - mSelectionEnd );
+ LLClipboard::instance().copyToClipboard(getWText(), left_pos, length);
+}
+
+bool LLTextEditor::canPaste() const
+{
+ return !mReadOnly && LLClipboard::instance().isTextAvailable();
+}
+
+// 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)
+{
+ struct BoolReset
+ {
+ BoolReset(bool& value) : mValuePtr(&value) { *mValuePtr = false; }
+ ~BoolReset() { *mValuePtr = true; }
+ bool* mValuePtr;
+ } reset(mParseOnTheFly);
+
+ bool can_paste_it;
+ if (is_primary)
+ {
+ can_paste_it = canPastePrimary();
+ }
+ else
+ {
+ can_paste_it = canPaste();
+ }
+
+ if (!can_paste_it)
+ {
+ return;
+ }
+
+ LLWString paste;
+ LLClipboard::instance().pasteFromClipboard(paste, is_primary);
+
+ 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);
+ cleanStringForPaste(clean_string);
+
+ // Insert the new text into the existing text.
+
+ //paste text with linebreaks.
+ pasteTextWithLinebreaks(clean_string);
+
+ deselect();
+
+ onKeyStroke();
+}
+
+
+// Clean up string (replace tabs and remove characters that our fonts don't support).
+void LLTextEditor::cleanStringForPaste(LLWString & clean_string)
+{
+ std::string clean_string_utf = wstring_to_utf8str(clean_string);
+ std::replace( clean_string_utf.begin(), clean_string_utf.end(), '\r', '\n');
+ clean_string = utf8str_to_wstring(clean_string_utf);
+
+ 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);
+ }
+ }
+ }
+}
+
+
+void LLTextEditor::pasteTextWithLinebreaks(LLWString & clean_string)
+{
+ std::basic_string<llwchar>::size_type start = 0;
+ std::basic_string<llwchar>::size_type pos = clean_string.find('\n',start);
+
+ while((pos != -1) && (pos != clean_string.length() -1))
+ {
+ if(pos!=start)
+ {
+ std::basic_string<llwchar> str = std::basic_string<llwchar>(clean_string,start,pos-start);
+ setCursorPos(mCursorPos + insert(mCursorPos, str, true, LLTextSegmentPtr()));
+ }
+ addLineBreakChar(true); // Add a line break and group with the next addition.
+
+ start = pos+1;
+ pos = clean_string.find('\n',start);
+ }
+
+ if (pos != start)
+ {
+ std::basic_string<llwchar> str = std::basic_string<llwchar>(clean_string,start,clean_string.length()-start);
+ setCursorPos(mCursorPos + insert(mCursorPos, str, false, LLTextSegmentPtr()));
+ }
+ else
+ {
+ addLineBreakChar(false); // Add a line break and end the grouping.
+ }
+}
+
+// copy selection to primary
+void LLTextEditor::copyPrimary()
+{
+ if( !canCopy() )
+ {
+ return;
+ }
+ S32 left_pos = llmin( mSelectionStart, mSelectionEnd );
+ S32 length = llabs( mSelectionStart - mSelectionEnd );
+ LLClipboard::instance().copyToClipboard(getWText(), left_pos, length, true);
+}
+
+bool LLTextEditor::canPastePrimary() const
+{
+ return !mReadOnly && LLClipboard::instance().isTextAvailable(true);
+}
+
+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();
+ setCursorPos(0);
+ mSelectionEnd = mCursorPos;
+ }
+ else
+ {
+ // Ctrl-Home, Ctrl-Left, Ctrl-Right, Ctrl-Down
+ // all move the cursor as if clicking, so should deselect.
+ deselect();
+ startOfDoc();
+ }
+ 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;
+
+ default:
+ handled = false;
+ break;
+ }
+ }
+
+ if (handled && !gFocusMgr.getMouseCapture())
+ {
+ updatePrimary();
+ }
+
+ return handled;
+}
+
+
+bool LLTextEditor::handleSpecialKey(const KEY key, const MASK mask)
+ {
+ bool handled = true;
+
+ if (mReadOnly) return false;
+
+ switch( key )
+ {
+ case KEY_INSERT:
+ if (mask == MASK_NONE)
+ {
+ gKeyboard->toggleInsertMode();
+ }
+ break;
+
+ case KEY_BACKSPACE:
+ if( hasSelection() )
+ {
+ deleteSelection(false);
+ }
+ else
+ if( 0 < mCursorPos )
+ {
+ removeCharOrTab();
+ }
+ else
+ {
+ LLUI::getInstance()->reportBadKeystroke();
+ }
+ break;
+
+
+ case KEY_RETURN:
+ if (mask == MASK_NONE)
+ {
+ if( hasSelection() && !mKeepSelectionOnReturn )
+ {
+ deleteSelection(false);
+ }
+ if (mAutoIndent)
+ {
+ autoIndent();
+ }
+ }
+ 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 offset = getLineOffsetFromDocIndex(mCursorPos);
+
+ 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 )
+ {
+ LLWString text = getWText();
+ if( ' ' == text[ mCursorPos - 1 ] )
+ {
+ S32 line = getLineNumFromDocIndex(mCursorPos, false);
+ S32 line_start = getLineStart(line);
+
+ // Jump over spaces in the current line
+ while ((' ' == text[line_start]) && (line_start < mCursorPos))
+ {
+ line_start++;
+ }
+
+ // Make sure there is nothing but ' ' before the Brace we are unindenting
+ if (line_start == mCursorPos)
+ {
+ removeCharOrTab();
+ }
+ }
+ }
+}
+
+
+bool LLTextEditor::handleKeyHere(KEY key, MASK mask )
+{
+ bool handled = false;
+
+ // 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;
+ }
+
+ if (mReadOnly && mScroller)
+ {
+ handled = (mScroller && mScroller->handleKeyHere( key, mask ))
+ || handleSelectionKey(key, mask)
+ || handleControlKey(key, mask);
+ }
+ else
+ {
+ if (!mReadOnly && mShowEmojiHelper && LLEmojiHelper::instance().handleKey(this, key, mask))
+ {
+ return true;
+ }
+
+ if (mEnableTooltipPaste &&
+ LLToolTipMgr::instance().toolTipVisible() &&
+ LLToolTipMgr::instance().isTooltipPastable() &&
+ KEY_TAB == key)
+ { // Paste the first line of a tooltip into the editor
+ std::string message;
+ LLToolTipMgr::instance().getToolTipMessage(message);
+ LLWString tool_tip_text(utf8str_to_wstring(message));
+
+ if (tool_tip_text.size() > 0)
+ {
+ // Delete any selected characters (the tooltip text replaces them)
+ if(hasSelection())
+ {
+ deleteSelection(true);
+ }
+
+ std::basic_string<llwchar>::size_type pos = tool_tip_text.find('\n',0);
+ if (pos != -1)
+ { // Extract the first line of the tooltip
+ tool_tip_text = std::basic_string<llwchar>(tool_tip_text, 0, pos);
+ }
+
+ // Add the text
+ cleanStringForPaste(tool_tip_text);
+ pasteTextWithLinebreaks(tool_tip_text);
+ handled = true;
+ }
+ }
+ else
+ { // Normal key handling
+ handled = handleNavigationKey( key, mask )
+ || handleSelectionKey(key, mask)
+ || handleControlKey(key, mask)
+ || handleSpecialKey(key, mask);
+ }
+ }
+
+ if( handled )
+ {
+ resetCursorBlink();
+ needsScroll();
+
+ if (mShowEmojiHelper)
+ {
+ // Dismiss the helper whenever we handled a key that it didn't
+ LLEmojiHelper::instance().hideHelper(this);
+ }
+ }
+
+ return handled;
+}
+
+
+bool LLTextEditor::handleUnicodeCharHere(llwchar uni_char)
+{
+ if ((uni_char < 0x20) || (uni_char == 0x7F)) // Control character or DEL
+ {
+ return false;
+ }
+
+ bool handled = false;
+
+ // Handle most keys only if the text editor is writeable.
+ if( !mReadOnly )
+ {
+ if (mShowEmojiHelper && uni_char < 0x80 && LLEmojiHelper::instance().handleKey(this, (KEY)uni_char, MASK_NONE))
+ {
+ return true;
+ }
+
+ if( mAutoIndent && '}' == 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 )
+ {
+ resetCursorBlink();
+
+ // Most keystrokes will make the selection box go away, but not all will.
+ deselect();
+
+ onKeyStroke();
+ }
+
+ return handled;
+}
+
+
+// virtual
+bool LLTextEditor::canDoDelete() const
+{
+ return !mReadOnly && ( !mPassDelete || ( hasSelection() || (mCursorPos < getLength())) );
+}
+
+void LLTextEditor::doDelete()
+{
+ if( !canDoDelete() )
+ {
+ return;
+ }
+ if( hasSelection() )
+ {
+ deleteSelection(false);
+ }
+ else
+ if( mCursorPos < getLength() )
+ {
+ S32 i;
+ S32 chars_to_remove = 1;
+ LLWString text = getWText();
+ if( (text[ mCursorPos ] == ' ') && (mCursorPos + SPACES_PER_TAB < getLength()) )
+ {
+ // Try to remove a full tab's worth of spaces
+ S32 offset = getLineOffsetFromDocIndex(mCursorPos);
+ 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();
+ }
+
+ }
+
+ 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);
+
+ 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);
+
+ onKeyStroke();
+}
+
+void LLTextEditor::onFocusReceived()
+{
+ LLTextBase::onFocusReceived();
+ updateAllowingLanguageInput();
+}
+
+void LLTextEditor::focusLostHelper()
+{
+ updateAllowingLanguageInput();
+
+ // Route menu back to the default
+ if( gEditMenuHandler == this )
+ {
+ gEditMenuHandler = NULL;
+ }
+
+ if (mCommitOnFocusLost)
+ {
+ onCommit();
+ }
+
+ // Make sure cursor is shown again
+ getWindow()->showCursorFromMouseMove();
+}
+
+void LLTextEditor::onFocusLost()
+{
+ focusLostHelper();
+ LLTextBase::onFocusLost();
+}
+
+void LLTextEditor::onCommit()
+{
+ setControlValue(getValue());
+ LLTextBase::onCommit();
+}
+
+void LLTextEditor::setEnabled(bool enabled)
+{
+ // just treat enabled as read-only flag
+ bool read_only = !enabled;
+ if (read_only != mReadOnly)
+ {
+ //mReadOnly = read_only;
+ LLTextBase::setReadOnly(read_only);
+ updateSegments();
+ updateAllowingLanguageInput();
+ }
+}
+
+void LLTextEditor::showContextMenu(S32 x, S32 y)
+{
+ LLContextMenu* menu = static_cast<LLContextMenu*>(mContextMenuHandle.get());
+ if (!menu)
+ {
+ llassert(LLMenuGL::sMenuContainer != NULL);
+ menu = LLUICtrlFactory::createFromFile<LLContextMenu>("menu_text_editor.xml",
+ LLMenuGL::sMenuContainer,
+ LLMenuHolderGL::child_registry_t::instance());
+ if(!menu)
+ {
+ LL_WARNS() << "Failed to create menu for LLTextEditor: " << getName() << LL_ENDL;
+ return;
+ }
+ mContextMenuHandle = menu->getHandle();
+ }
+
+ // Route menu to this class
+ // previously this was done in ::handleRightMoseDown:
+ //if(hasTabStop())
+ // setFocus(true) - why? weird...
+ // and then inside setFocus
+ // ....
+ // gEditMenuHandler = this;
+ // ....
+ // but this didn't work in all cases and just weird...
+ //why not here?
+ // (all this was done for EXT-4443)
+
+ gEditMenuHandler = this;
+
+ S32 screen_x, screen_y;
+ localPointToScreen(x, y, &screen_x, &screen_y);
+
+ setCursorAtLocalPos(x, y, false);
+ if (hasSelection())
+ {
+ if ( (mCursorPos < llmin(mSelectionStart, mSelectionEnd)) || (mCursorPos > llmax(mSelectionStart, mSelectionEnd)) )
+ {
+ deselect();
+ }
+ else
+ {
+ setCursorPos(llmax(mSelectionStart, mSelectionEnd));
+ }
+ }
+
+ bool use_spellcheck = getSpellCheck(), is_misspelled = false;
+ if (use_spellcheck)
+ {
+ mSuggestionList.clear();
+
+ // If the cursor is on a misspelled word, retrieve suggestions for it
+ std::string misspelled_word = getMisspelledWord(mCursorPos);
+ if ((is_misspelled = !misspelled_word.empty()))
+ {
+ LLSpellChecker::instance().getSuggestions(misspelled_word, mSuggestionList);
+ }
+ }
+
+ menu->setItemVisible("Suggestion Separator", (use_spellcheck) && (!mSuggestionList.empty()));
+ menu->setItemVisible("Add to Dictionary", (use_spellcheck) && (is_misspelled));
+ menu->setItemVisible("Add to Ignore", (use_spellcheck) && (is_misspelled));
+ menu->setItemVisible("Spellcheck Separator", (use_spellcheck) && (is_misspelled));
+ menu->show(screen_x, screen_y, this);
+}
+
+
+void LLTextEditor::drawPreeditMarker()
+{
+ static LLUICachedControl<F32> preedit_marker_brightness ("UIPreeditMarkerBrightness", 0);
+ static LLUICachedControl<S32> preedit_marker_gap ("UIPreeditMarkerGap", 0);
+ static LLUICachedControl<S32> preedit_marker_position ("UIPreeditMarkerPosition", 0);
+ static LLUICachedControl<S32> preedit_marker_thickness ("UIPreeditMarkerThickness", 0);
+ static LLUICachedControl<F32> preedit_standout_brightness ("UIPreeditStandoutBrightness", 0);
+ static LLUICachedControl<S32> preedit_standout_gap ("UIPreeditStandoutGap", 0);
+ static LLUICachedControl<S32> preedit_standout_position ("UIPreeditStandoutPosition", 0);
+ static LLUICachedControl<S32> preedit_standout_thickness ("UIPreeditStandoutThickness", 0);
+
+ if (!hasPreeditString())
+ {
+ return;
+ }
+
+ const LLWString textString(getWText());
+ const llwchar *text = textString.c_str();
+ const S32 text_len = getLength();
+ const S32 num_lines = getLineCount();
+
+ S32 cur_line = getFirstVisibleLine();
+ if (cur_line >= num_lines)
+ {
+ return;
+ }
+
+ const S32 line_height = mFont->getLineHeight();
+
+ S32 line_start = getLineStart(cur_line);
+ S32 line_y = mVisibleTextRect.mTop - line_height;
+ while((mVisibleTextRect.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;
+ }
+
+ line_info& line = mLineInfoList[cur_line];
+ LLRect text_rect(line.mRect);
+ text_rect.mRight = mDocumentView->getRect().getWidth(); // clamp right edge to document extents
+ text_rect.translate(mDocumentView->getRect().mLeft, mDocumentView->getRect().mBottom); // adjust by scroll position
+
+ S32 preedit_left = text_rect.mLeft;
+ if (left > line_start)
+ {
+ preedit_left += mFont->getWidth(text, line_start, left - line_start);
+ }
+ S32 preedit_right = text_rect.mLeft;
+ if (right < line_end)
+ {
+ preedit_right += mFont->getWidth(text, line_start, right - line_start);
+ }
+ else
+ {
+ preedit_right += mFont->getWidth(text, line_start, line_end - line_start);
+ }
+
+ if (mPreeditStandouts[i])
+ {
+ gl_rect_2d(preedit_left + preedit_standout_gap,
+ text_rect.mBottom + mFont->getDescenderHeight() - 1,
+ preedit_right - preedit_standout_gap - 1,
+ text_rect.mBottom + mFont->getDescenderHeight() - 1 - preedit_standout_thickness,
+ (mCursorColor.get() * preedit_standout_brightness + mWriteableBgColor.get() * (1 - preedit_standout_brightness)).setAlpha(1.0f));
+ }
+ else
+ {
+ gl_rect_2d(preedit_left + preedit_marker_gap,
+ text_rect.mBottom + mFont->getDescenderHeight() - 1,
+ preedit_right - preedit_marker_gap - 1,
+ text_rect.mBottom + mFont->getDescenderHeight() - 1 - preedit_marker_thickness,
+ (mCursorColor.get() * preedit_marker_brightness + mWriteableBgColor.get() * (1 - preedit_marker_brightness)).setAlpha(1.0f));
+ }
+ }
+ }
+
+ // move down one line
+ line_y -= line_height;
+ line_start = next_start;
+ cur_line++;
+ }
+}
+
+void LLTextEditor::draw()
+{
+ {
+ // pad clipping rectangle so that cursor can draw at full width
+ // when at left edge of mVisibleTextRect
+ LLRect clip_rect(mVisibleTextRect);
+ clip_rect.stretch(1);
+ LLLocalClipRect clip(clip_rect);
+ }
+
+ LLTextBase::draw();
+
+ drawPreeditMarker();
+
+ //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( hasFocus() );// && !mReadOnly);
+}
+
+// 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);
+ }
+
+ LLTextBase::setFocus( new_state );
+
+ if( new_state )
+ {
+ // Route menu to this class
+ gEditMenuHandler = this;
+
+ // Don't start the cursor flashing right away
+ resetCursorBlink();
+ }
+ else
+ {
+ // Route menu back to the default
+ if( gEditMenuHandler == this )
+ {
+ gEditMenuHandler = NULL;
+ }
+
+ endSelection();
+ }
+}
+
+// public
+void LLTextEditor::setCursorAndScrollToEnd()
+{
+ deselect();
+ endOfDoc();
+}
+
+void LLTextEditor::getCurrentLineAndColumn( S32* line, S32* col, bool include_wordwrap )
+{
+ *line = getLineNumFromDocIndex(mCursorPos, include_wordwrap);
+ *col = getLineOffsetFromDocIndex(mCursorPos, include_wordwrap);
+}
+
+void LLTextEditor::autoIndent()
+{
+ // Count the number of spaces in the current line
+ S32 line = getLineNumFromDocIndex(mCursorPos, false);
+ S32 line_start = getLineStart(line);
+ S32 space_count = 0;
+ S32 i;
+
+ LLWString text = getWText();
+ S32 offset = getLineOffsetFromDocIndex(mCursorPos);
+ while(( ' ' == text[line_start] ) && (space_count < offset))
+ {
+ 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
+
+ //appendLineBreakSegment(LLStyle::Params());//addChar( '\n' );
+ addLineBreakChar();
+
+ for( i = 0; i < space_count; i++ )
+ {
+ addChar( ' ' );
+ }
+}
+
+// Inserts new text at the cursor position
+void LLTextEditor::insertText(const std::string &new_text)
+{
+ bool enabled = getEnabled();
+ setEnabled( true );
+
+ // Delete any selected characters (the insertion replaces them)
+ if( hasSelection() )
+ {
+ deleteSelection(true);
+ }
+
+ setCursorPos(mCursorPos + insert( mCursorPos, utf8str_to_wstring(new_text), false, LLTextSegmentPtr() ));
+
+ setEnabled( enabled );
+}
+
+void LLTextEditor::insertText(LLWString &new_text)
+{
+ bool enabled = getEnabled();
+ setEnabled( true );
+
+ // Delete any selected characters (the insertion replaces them)
+ if( hasSelection() )
+ {
+ deleteSelection(true);
+ }
+
+ setCursorPos(mCursorPos + insert( mCursorPos, new_text, false, LLTextSegmentPtr() ));
+
+ setEnabled( enabled );
+}
+
+void LLTextEditor::appendWidget(const LLInlineViewSegment::Params& params, const std::string& text, bool allow_undo)
+{
+ // Save old state
+ S32 selection_start = mSelectionStart;
+ S32 selection_end = mSelectionEnd;
+ bool was_selecting = mIsSelecting;
+ S32 cursor_pos = mCursorPos;
+ S32 old_length = getLength();
+ bool cursor_was_at_end = (mCursorPos == old_length);
+
+ deselect();
+
+ setCursorPos(old_length);
+
+ LLWString widget_wide_text = utf8str_to_wstring(text);
+
+ LLTextSegmentPtr segment = new LLInlineViewSegment(params, old_length, old_length + widget_wide_text.size());
+ insert(getLength(), widget_wide_text, false, segment);
+
+ // Set the cursor and scroll position
+ if( selection_start != selection_end )
+ {
+ mSelectionStart = selection_start;
+ mSelectionEnd = selection_end;
+
+ mIsSelecting = was_selecting;
+ setCursorPos(cursor_pos);
+ }
+ else if( cursor_was_at_end )
+ {
+ setCursorPos(getLength());
+ }
+ else
+ {
+ setCursorPos(cursor_pos);
+ }
+
+ if (!allow_undo)
+ {
+ blockUndo();
+ }
+}
+
+void LLTextEditor::removeTextFromEnd(S32 num_chars)
+{
+ if (num_chars <= 0) return;
+
+ remove(getLength() - num_chars, num_chars, false);
+
+ S32 len = getLength();
+ setCursorPos (llclamp(mCursorPos, 0, len));
+ mSelectionStart = llclamp(mSelectionStart, 0, len);
+ mSelectionEnd = llclamp(mSelectionEnd, 0, len);
+
+ needsScroll();
+}
+
+//----------------------------------------------------------------------------
+
+void LLTextEditor::onSpellCheckPerformed()
+{
+ if (isPristine())
+ {
+ mBaseDocIsPristine = false;
+ }
+}
+
+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--;
+ }
+ }
+ }
+
+ return isPristine(); // true => success
+}
+
+void LLTextEditor::updateLinkSegments()
+{
+ LLWString wtext = getWText();
+
+ // update any segments that contain a link
+ for (segment_set_t::iterator it = mSegments.begin(); it != mSegments.end(); ++it)
+ {
+ LLTextSegment *segment = *it;
+ if (segment && segment->getStyle() && segment->getStyle()->isLink())
+ {
+ LLStyleConstSP style = segment->getStyle();
+ LLStyleSP new_style(new LLStyle(*style));
+ LLWString url_label = wtext.substr(segment->getStart(), segment->getEnd()-segment->getStart());
+
+ segment_set_t::const_iterator next_it = mSegments.upper_bound(segment);
+ LLTextSegment *next_segment = *next_it;
+ if (next_segment)
+ {
+ LLWString next_url_label = wtext.substr(next_segment->getStart(), next_segment->getEnd()-next_segment->getStart());
+ std::string link_check = wstring_to_utf8str(url_label) + wstring_to_utf8str(next_url_label);
+ LLUrlMatch match;
+
+ if ( LLUrlRegistry::instance().findUrl(link_check, match))
+ {
+ if(match.getQuery() == wstring_to_utf8str(next_url_label))
+ {
+ continue;
+ }
+ }
+ }
+
+ // if the link's label (what the user can edit) is a valid Url,
+ // then update the link's HREF to be the same as the label text.
+ // This lets users edit Urls in-place.
+ if (acceptsTextInput() && LLUrlRegistry::instance().hasUrl(url_label))
+ {
+ std::string new_url = wstring_to_utf8str(url_label);
+ LLStringUtil::trim(new_url);
+ new_style->setLinkHREF(new_url);
+ LLStyleConstSP sp(new_style);
+ segment->setStyle(sp);
+ }
+ }
+ }
+}
+
+
+
+void LLTextEditor::onMouseCaptureLost()
+{
+ endSelection();
+}
+
+///////////////////////////////////////////////////////////////////
+// Hack for Notecards
+
+bool LLTextEditor::importBuffer(const char* buffer, S32 length )
+{
+ std::istringstream instream(buffer);
+
+ // Version 1 format:
+ // Linden text version 1\n
+ // {\n
+ // <EmbeddedItemList chunk>
+ // Text length <bytes without \0>\n
+ // <text without \0> (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)
+ {
+ LLError::LLUserWarningMsg::showOutOfMemory();
+ 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;
+
+ startOfDoc();
+ deselect();
+
+ 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", getLength() );
+ outstream << getText();
+ outstream << "}\n";
+
+ return true;
+}
+
+void LLTextEditor::updateAllowingLanguageInput()
+{
+ LLWindow* window = getWindow();
+ if (!window)
+ {
+ // test app, no window available
+ return;
+ }
+ if (hasFocus() && !mReadOnly)
+ {
+ window->allowLanguageTextInput(this, true);
+ }
+ else
+ {
+ window->allowLanguageTextInput(this, false);
+ }
+}
+
+// Preedit is managed off the undo/redo command stack.
+
+bool LLTextEditor::hasPreeditString() const
+{
+ return (mPreeditPositions.size() > 1);
+}
+
+void LLTextEditor::resetPreedit()
+{
+ if (hasSelection())
+ {
+ if (hasPreeditString())
+ {
+ LL_WARNS() << "Preedit and selection!" << LL_ENDL;
+ deselect();
+ }
+ else
+ {
+ deleteSelection(true);
+ }
+ }
+ if (hasPreeditString())
+ {
+ if (hasSelection())
+ {
+ LL_WARNS() << "Preedit and selection!" << LL_ENDL;
+ deselect();
+ }
+
+ setCursorPos(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 = getWText().substr(insert_preedit_at, mPreeditWString.length());
+ removeStringNoUndo(insert_preedit_at, mPreeditWString.length());
+ }
+ else
+ {
+ mPreeditOverwrittenWString.clear();
+ }
+
+ segment_vec_t segments;
+ //pass empty segments to let "insertStringNoUndo" make new LLNormalTextSegment and insert it, if needed.
+ insertStringNoUndo(insert_preedit_at, mPreeditWString, &segments);
+
+ mPreeditStandouts = preedit_standouts;
+
+ setCursorPos(insert_preedit_at + caret_position);
+
+ // Update of the preedit should be caused by some key strokes.
+ resetCursorBlink();
+
+ onKeyStroke();
+}
+
+bool LLTextEditor::getPreeditLocation(S32 query_offset, LLCoordGL *coord, LLRect *bounds, LLRect *control) const
+{
+ if (control)
+ {
+ LLRect control_rect_screen;
+ localRectToScreen(mVisibleTextRect, &control_rect_screen);
+ LLUI::getInstance()->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 = getFirstVisibleLine();
+ 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 LLWString textString(getWText());
+ const llwchar * const text = textString.c_str();
+ const S32 line_height = mFont->getLineHeight();
+
+ if (coord)
+ {
+ const S32 query_x = mVisibleTextRect.mLeft + mFont->getWidth(text, current_line_start, query - current_line_start);
+ const S32 query_y = mVisibleTextRect.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::getInstance()->screenPointToGL(query_screen_x, query_screen_y, &coord->mX, &coord->mY);
+ }
+
+ if (bounds)
+ {
+ S32 preedit_left = mVisibleTextRect.mLeft;
+ if (preedit_left_position > current_line_start)
+ {
+ preedit_left += mFont->getWidth(text, current_line_start, preedit_left_position - current_line_start);
+ }
+
+ S32 preedit_right = mVisibleTextRect.mLeft;
+ if (preedit_right_position < current_line_end)
+ {
+ preedit_right += mFont->getWidth(text, current_line_start, preedit_right_position - current_line_start);
+ }
+ else
+ {
+ preedit_right += mFont->getWidth(text, current_line_start, current_line_end - current_line_start);
+ }
+
+ const S32 preedit_top = mVisibleTextRect.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::getInstance()->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( getWText(), 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((F32)mFont->getLineHeight() * LLUI::getScaleFactor().mV[VY]);
+}
+
+bool LLTextEditor::isDirty() const
+{
+ if(mReadOnly)
+ {
+ return false;
+ }
+
+ if( mPristineCmd )
+ {
+ return ( mPristineCmd == mLastCmd );
+ }
+ else
+ {
+ return ( NULL != mLastCmd );
+ }
+}
+
+void LLTextEditor::setKeystrokeCallback(const keystroke_signal_t::slot_type& callback)
+{
+ mKeystrokeSignal.connect(callback);
+}
+
+void LLTextEditor::onKeyStroke()
+{
+ mKeystrokeSignal(this);
+
+ mSpellCheckStart = mSpellCheckEnd = -1;
+ mSpellCheckTimer.setTimerExpirySec(SPELLCHECK_DELAY);
+}
+
+//virtual
+void LLTextEditor::clear()
+{
+ getViewModel()->setDisplay(LLWStringUtil::null);
+ clearSegments();
+}
+
+bool LLTextEditor::canLoadOrSaveToFile()
+{
+ return !mReadOnly;
+}
+
+S32 LLTextEditor::spacesPerTab()
+{
+ return SPACES_PER_TAB;
+}
|