diff options
| author | Ansariel <ansariel.hiller@phoenixviewer.com> | 2024-05-22 21:25:21 +0200 |
|---|---|---|
| committer | Andrey Lihatskiy <alihatskiy@productengine.com> | 2024-05-22 22:40:26 +0300 |
| commit | e2e37cced861b98de8c1a7c9c0d3a50d2d90e433 (patch) | |
| tree | 1bb897489ce524986f6196201c10ac0d8861aa5f /indra/llui/lltexteditor.cpp | |
| parent | 069ea06848f766466f1a281144c82a0f2bd79f3a (diff) | |
Fix line endlings
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 daffd58dfe..b4254524ad 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()))
- {
- 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; +} |
