summaryrefslogtreecommitdiff
path: root/indra/llui/lltexteditor.cpp
diff options
context:
space:
mode:
authorJames Cook <james@lindenlab.com>2007-01-02 08:33:20 +0000
committerJames Cook <james@lindenlab.com>2007-01-02 08:33:20 +0000
commit420b91db29485df39fd6e724e782c449158811cb (patch)
treeb471a94563af914d3ed3edd3e856d21cb1b69945 /indra/llui/lltexteditor.cpp
Print done when done.
Diffstat (limited to 'indra/llui/lltexteditor.cpp')
-rw-r--r--indra/llui/lltexteditor.cpp4144
1 files changed, 4144 insertions, 0 deletions
diff --git a/indra/llui/lltexteditor.cpp b/indra/llui/lltexteditor.cpp
new file mode 100644
index 0000000000..a4747aef67
--- /dev/null
+++ b/indra/llui/lltexteditor.cpp
@@ -0,0 +1,4144 @@
+/**
+ * @file lltexteditor.cpp
+ * @brief LLTextEditor base class
+ *
+ * Copyright (c) 2001-$CurrentYear$, Linden Research, Inc.
+ * $License$
+ */
+
+// Text editor widget to let users enter a a multi-line ASCII document.
+
+#include "linden_common.h"
+
+#include "lltexteditor.h"
+
+#include "llfontgl.h"
+#include "llgl.h"
+#include "llui.h"
+#include "lluictrlfactory.h"
+#include "llrect.h"
+#include "llfocusmgr.h"
+#include "sound_ids.h"
+#include "lltimer.h"
+#include "llmath.h"
+
+#include "audioengine.h"
+#include "llclipboard.h"
+#include "llscrollbar.h"
+#include "llstl.h"
+#include "llkeyboard.h"
+#include "llkeywords.h"
+#include "llundo.h"
+#include "llviewborder.h"
+#include "llcontrol.h"
+#include "llimagegl.h"
+#include "llwindow.h"
+#include "llglheaders.h"
+#include <queue>
+
+//
+// Globals
+//
+
+BOOL gDebugTextEditorTips = FALSE;
+
+//
+// Constants
+//
+
+const S32 UI_TEXTEDITOR_BUFFER_BLOCK_SIZE = 512;
+
+const S32 UI_TEXTEDITOR_BORDER = 1;
+const S32 UI_TEXTEDITOR_H_PAD = 4;
+const S32 UI_TEXTEDITOR_V_PAD_TOP = 4;
+const F32 CURSOR_FLASH_DELAY = 1.0f; // in seconds
+const S32 CURSOR_THICKNESS = 2;
+const S32 SPACES_PER_TAB = 4;
+
+LLColor4 LLTextEditor::mLinkColor = LLColor4::blue;
+void (* LLTextEditor::mURLcallback)(const char*) = NULL;
+BOOL (* LLTextEditor::mSecondlifeURLcallback)(LLString) = NULL;
+
+///////////////////////////////////////////////////////////////////
+//virtuals
+BOOL LLTextCmd::canExtend(S32 pos)
+{
+ return FALSE;
+}
+
+void LLTextCmd::blockExtensions()
+{
+}
+
+BOOL LLTextCmd::extendAndExecute( LLTextEditor* editor, S32 pos, llwchar c, S32* delta )
+{
+ llassert(0);
+ return 0;
+}
+
+BOOL LLTextCmd::hasExtCharValue( llwchar value )
+{
+ return FALSE;
+}
+
+// Utility funcs
+S32 LLTextCmd::insert(LLTextEditor* editor, S32 pos, const LLWString &utf8str)
+{
+ return editor->insertStringNoUndo( pos, utf8str );
+}
+S32 LLTextCmd::remove(LLTextEditor* editor, S32 pos, S32 length)
+{
+ return editor->removeStringNoUndo( pos, length );
+}
+S32 LLTextCmd::overwrite(LLTextEditor* editor, S32 pos, llwchar wc)
+{
+ return editor->overwriteCharNoUndo(pos, wc);
+}
+
+///////////////////////////////////////////////////////////////////
+
+class LLTextCmdInsert : public LLTextCmd
+{
+public:
+ LLTextCmdInsert(S32 pos, BOOL group_with_next, const LLWString &ws)
+ : LLTextCmd(pos, group_with_next), mString(ws)
+ {
+ }
+ virtual BOOL execute( LLTextEditor* editor, S32* delta )
+ {
+ *delta = insert(editor, mPos, mString );
+ LLWString::truncate(mString, *delta);
+ //mString = wstring_truncate(mString, *delta);
+ return (*delta != 0);
+ }
+ virtual S32 undo( LLTextEditor* editor )
+ {
+ remove(editor, mPos, mString.length() );
+ return mPos;
+ }
+ virtual S32 redo( LLTextEditor* editor )
+ {
+ insert(editor, mPos, mString );
+ return mPos + mString.length();
+ }
+
+private:
+ LLWString mString;
+};
+
+///////////////////////////////////////////////////////////////////
+
+class LLTextCmdAddChar : public LLTextCmd
+{
+public:
+ LLTextCmdAddChar( S32 pos, BOOL group_with_next, llwchar wc)
+ : LLTextCmd(pos, group_with_next), mString(1, wc), mBlockExtensions(FALSE)
+ {
+ }
+ virtual void blockExtensions()
+ {
+ mBlockExtensions = TRUE;
+ }
+ virtual BOOL canExtend(S32 pos)
+ {
+ return !mBlockExtensions && (pos == mPos + (S32)mString.length());
+ }
+ virtual BOOL execute( LLTextEditor* editor, S32* delta )
+ {
+ *delta = insert(editor, mPos, mString);
+ LLWString::truncate(mString, *delta);
+ //mString = wstring_truncate(mString, *delta);
+ return (*delta != 0);
+ }
+ virtual BOOL extendAndExecute( LLTextEditor* editor, S32 pos, llwchar wc, S32* delta )
+ {
+ LLWString ws;
+ ws += wc;
+
+ *delta = insert(editor, pos, ws);
+ if( *delta > 0 )
+ {
+ mString += wc;
+ }
+ return (*delta != 0);
+ }
+ virtual S32 undo( LLTextEditor* editor )
+ {
+ remove(editor, mPos, mString.length() );
+ return mPos;
+ }
+ virtual S32 redo( LLTextEditor* editor )
+ {
+ insert(editor, mPos, mString );
+ return mPos + mString.length();
+ }
+
+private:
+ LLWString mString;
+ BOOL mBlockExtensions;
+
+};
+
+///////////////////////////////////////////////////////////////////
+
+class LLTextCmdOverwriteChar : public LLTextCmd
+{
+public:
+ LLTextCmdOverwriteChar( S32 pos, BOOL group_with_next, llwchar wc)
+ : LLTextCmd(pos, group_with_next), mChar(wc), mOldChar(0) {}
+
+ virtual BOOL execute( LLTextEditor* editor, S32* delta )
+ {
+ mOldChar = editor->getWChar(mPos);
+ overwrite(editor, mPos, mChar);
+ *delta = 0;
+ return TRUE;
+ }
+ virtual S32 undo( LLTextEditor* editor )
+ {
+ overwrite(editor, mPos, mOldChar);
+ return mPos;
+ }
+ virtual S32 redo( LLTextEditor* editor )
+ {
+ overwrite(editor, mPos, mChar);
+ return mPos+1;
+ }
+
+private:
+ llwchar mChar;
+ llwchar mOldChar;
+};
+
+///////////////////////////////////////////////////////////////////
+
+class LLTextCmdRemove : public LLTextCmd
+{
+public:
+ LLTextCmdRemove( S32 pos, BOOL group_with_next, S32 len ) :
+ LLTextCmd(pos, group_with_next), mLen(len)
+ {
+ }
+ virtual BOOL execute( LLTextEditor* editor, S32* delta )
+ {
+ mString = editor->getWSubString(mPos, mLen);
+ *delta = remove(editor, mPos, mLen );
+ return (*delta != 0);
+ }
+ virtual S32 undo( LLTextEditor* editor )
+ {
+ insert(editor, mPos, mString );
+ return mPos + mString.length();
+ }
+ virtual S32 redo( LLTextEditor* editor )
+ {
+ remove(editor, mPos, mLen );
+ return mPos;
+ }
+private:
+ LLWString mString;
+ S32 mLen;
+};
+
+///////////////////////////////////////////////////////////////////
+
+//
+// Member functions
+//
+
+LLTextEditor::LLTextEditor(
+ const LLString& name,
+ const LLRect& rect,
+ S32 max_length,
+ const LLString &default_text,
+ const LLFontGL* font,
+ BOOL allow_embedded_items)
+ :
+ LLUICtrl( name, rect, TRUE, NULL, NULL, FOLLOWS_TOP | FOLLOWS_LEFT ),
+ mTextIsUpToDate(TRUE),
+ mMaxTextLength( max_length ),
+ mBaseDocIsPristine(TRUE),
+ mPristineCmd( NULL ),
+ mLastCmd( NULL ),
+ mCursorPos( 0 ),
+ mIsSelecting( FALSE ),
+ mSelectionStart( 0 ),
+ mSelectionEnd( 0 ),
+ mOnScrollEndCallback( NULL ),
+ mOnScrollEndData( NULL ),
+ mCursorColor( LLUI::sColorsGroup->getColor( "TextCursorColor" ) ),
+ mFgColor( LLUI::sColorsGroup->getColor( "TextFgColor" ) ),
+ mReadOnlyFgColor( LLUI::sColorsGroup->getColor( "TextFgReadOnlyColor" ) ),
+ mWriteableBgColor( LLUI::sColorsGroup->getColor( "TextBgWriteableColor" ) ),
+ mReadOnlyBgColor( LLUI::sColorsGroup->getColor( "TextBgReadOnlyColor" ) ),
+ mFocusBgColor( LLUI::sColorsGroup->getColor( "TextBgFocusColor" ) ),
+ mReadOnly(FALSE),
+ mWordWrap( FALSE ),
+ mTabToNextField( TRUE ),
+ mCommitOnFocusLost( FALSE ),
+ mTakesFocus( TRUE ),
+ mHideScrollbarForShortDocs( FALSE ),
+ mTakesNonScrollClicks( TRUE ),
+ mAllowEmbeddedItems( allow_embedded_items ),
+ mAcceptCallingCardNames(FALSE),
+ mHandleEditKeysDirectly( FALSE ),
+ mMouseDownX(0),
+ mMouseDownY(0),
+ mLastSelectionX(-1),
+ mLastSelectionY(-1)
+{
+ mSourceID.generate();
+
+ if (font)
+ {
+ mGLFont = font;
+ }
+ else
+ {
+ mGLFont = LLFontGL::sSansSerif;
+ }
+
+ updateTextRect();
+
+ S32 line_height = llround( mGLFont->getLineHeight() );
+ S32 page_size = mTextRect.getHeight() / line_height;
+
+ // Init the scrollbar
+ LLRect scroll_rect;
+ scroll_rect.setOriginAndSize(
+ mRect.getWidth() - UI_TEXTEDITOR_BORDER - SCROLLBAR_SIZE,
+ UI_TEXTEDITOR_BORDER,
+ SCROLLBAR_SIZE,
+ mRect.getHeight() - 2 * UI_TEXTEDITOR_BORDER );
+ S32 lines_in_doc = getLineCount();
+ mScrollbar = new LLScrollbar( "Scrollbar", scroll_rect,
+ LLScrollbar::VERTICAL,
+ lines_in_doc,
+ 0,
+ page_size,
+ NULL, this );
+ mScrollbar->setFollowsRight();
+ mScrollbar->setFollowsTop();
+ mScrollbar->setFollowsBottom();
+ mScrollbar->setEnabled( TRUE );
+ mScrollbar->setVisible( TRUE );
+ mScrollbar->setOnScrollEndCallback(mOnScrollEndCallback, mOnScrollEndData);
+ addChild(mScrollbar);
+
+ mBorder = new LLViewBorder( "text ed border", LLRect(0, mRect.getHeight(), mRect.getWidth(), 0), LLViewBorder::BEVEL_IN, LLViewBorder::STYLE_LINE, UI_TEXTEDITOR_BORDER );
+ addChild( mBorder );
+
+ setText(default_text);
+
+ mParseHTML=FALSE;
+ mHTML="";
+}
+
+
+LLTextEditor::~LLTextEditor()
+{
+ gFocusMgr.releaseFocusIfNeeded( this ); // calls onCommit()
+
+ // Route menu back to the default
+ if( gEditMenuHandler == this )
+ {
+ gEditMenuHandler = NULL;
+ }
+
+ // Scrollbar is deleted by LLView
+ mHoverSegment = NULL;
+ std::for_each(mSegments.begin(), mSegments.end(), DeletePointer());
+
+ std::for_each(mUndoStack.begin(), mUndoStack.end(), DeletePointer());
+}
+
+//virtual
+LLString LLTextEditor::getWidgetTag() const
+{
+ return LL_TEXT_EDITOR_TAG;
+}
+
+void LLTextEditor::setTrackColor( const LLColor4& color )
+{
+ mScrollbar->setTrackColor(color);
+}
+
+void LLTextEditor::setThumbColor( const LLColor4& color )
+{
+ mScrollbar->setThumbColor(color);
+}
+
+void LLTextEditor::setHighlightColor( const LLColor4& color )
+{
+ mScrollbar->setHighlightColor(color);
+}
+
+void LLTextEditor::setShadowColor( const LLColor4& color )
+{
+ mScrollbar->setShadowColor(color);
+}
+
+void LLTextEditor::updateLineStartList(S32 startpos)
+{
+ updateSegments();
+
+ bindEmbeddedChars( mGLFont );
+
+ S32 seg_num = mSegments.size();
+ S32 seg_idx = 0;
+ S32 seg_offset = 0;
+
+ if (!mLineStartList.empty())
+ {
+ getSegmentAndOffset(startpos, &seg_idx, &seg_offset);
+ line_info t(seg_idx, seg_offset);
+ line_list_t::iterator iter = std::upper_bound(mLineStartList.begin(), mLineStartList.end(), t, line_info_compare());
+ if (iter != mLineStartList.begin()) --iter;
+ seg_idx = iter->mSegment;
+ seg_offset = iter->mOffset;
+ mLineStartList.erase(iter, mLineStartList.end());
+ }
+
+ while( seg_idx < seg_num )
+ {
+ mLineStartList.push_back(line_info(seg_idx,seg_offset));
+ BOOL line_ended = FALSE;
+ S32 line_width = 0;
+ while(!line_ended && seg_idx < seg_num)
+ {
+ LLTextSegment* segment = mSegments[seg_idx];
+ S32 start_idx = segment->getStart() + seg_offset;
+ S32 end_idx = start_idx;
+ while (end_idx < segment->getEnd() && mWText[end_idx] != '\n')
+ {
+ end_idx++;
+ }
+ if (start_idx == end_idx)
+ {
+ if (end_idx >= segment->getEnd())
+ {
+ // empty segment
+ seg_idx++;
+ seg_offset = 0;
+ }
+ else
+ {
+ // empty line
+ line_ended = TRUE;
+ seg_offset++;
+ }
+ }
+ else
+ {
+ const llwchar* str = mWText.c_str() + start_idx;
+ S32 drawn = mGLFont->maxDrawableChars(str, (F32)mTextRect.getWidth() - line_width,
+ end_idx - start_idx, mWordWrap, mAllowEmbeddedItems );
+ if( 0 == drawn && line_width == 0)
+ {
+ // If at the beginning of a line, draw at least one character, even if it doesn't all fit.
+ drawn = 1;
+ }
+ seg_offset += drawn;
+ line_width += mGLFont->getWidth(str, 0, drawn, mAllowEmbeddedItems);
+ end_idx = segment->getStart() + seg_offset;
+ if (end_idx < segment->getEnd())
+ {
+ line_ended = TRUE;
+ if (mWText[end_idx] == '\n')
+ {
+ seg_offset++; // skip newline
+ }
+ }
+ else
+ {
+ // finished with segment
+ seg_idx++;
+ seg_offset = 0;
+ }
+ }
+ }
+ }
+
+ unbindEmbeddedChars(mGLFont);
+
+ mScrollbar->setDocSize( getLineCount() );
+
+ if (mHideScrollbarForShortDocs)
+ {
+ BOOL short_doc = (mScrollbar->getDocSize() <= mScrollbar->getPageSize());
+ mScrollbar->setVisible(!short_doc);
+ }
+
+}
+
+////////////////////////////////////////////////////////////
+// LLTextEditor
+// Public methods
+
+//static
+BOOL LLTextEditor::isPartOfWord(llwchar c) { return (c == '_') || isalnum(c); }
+
+
+
+void LLTextEditor::truncate()
+{
+ if (mWText.size() > (size_t)mMaxTextLength)
+ {
+ LLWString::truncate(mWText, mMaxTextLength);
+ mTextIsUpToDate = FALSE;
+ }
+}
+
+void LLTextEditor::setText(const LLString &utf8str)
+{
+ mUTF8Text = utf8str;
+ mWText = utf8str_to_wstring(utf8str);
+ mTextIsUpToDate = TRUE;
+
+ truncate();
+ blockUndo();
+
+ setCursorPos(0);
+ deselect();
+
+ updateLineStartList();
+ updateScrollFromCursor();
+}
+
+void LLTextEditor::setWText(const LLWString &wtext)
+{
+ mWText = wtext;
+ mUTF8Text.clear();
+ mTextIsUpToDate = FALSE;
+
+ truncate();
+ blockUndo();
+
+ setCursorPos(0);
+ deselect();
+
+ updateLineStartList();
+ updateScrollFromCursor();
+}
+
+void LLTextEditor::setValue(const LLSD& value)
+{
+ setText(value.asString());
+}
+
+const LLString& LLTextEditor::getText() const
+{
+ if (!mTextIsUpToDate)
+ {
+ if (mAllowEmbeddedItems)
+ {
+ llwarns << "getText() called on text with embedded items (not supported)" << llendl;
+ }
+ mUTF8Text = wstring_to_utf8str(mWText);
+ mTextIsUpToDate = TRUE;
+ }
+ return mUTF8Text;
+}
+
+LLSD LLTextEditor::getValue() const
+{
+ return LLSD(getText());
+}
+
+void LLTextEditor::setWordWrap(BOOL b)
+{
+ mWordWrap = b;
+
+ setCursorPos(0);
+ deselect();
+
+ updateLineStartList();
+ updateScrollFromCursor();
+}
+
+
+void LLTextEditor::setBorderVisible(BOOL b)
+{
+ mBorder->setVisible(b);
+}
+
+
+
+void LLTextEditor::setHideScrollbarForShortDocs(BOOL b)
+{
+ mHideScrollbarForShortDocs = b;
+
+ if (mHideScrollbarForShortDocs)
+ {
+ BOOL short_doc = (mScrollbar->getDocSize() <= mScrollbar->getPageSize());
+ mScrollbar->setVisible(!short_doc);
+ }
+}
+
+void LLTextEditor::selectNext(const LLString& 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)
+ {
+ LLWString::toLower(text);
+ LLWString::toLower(search_text);
+ }
+
+ if (mIsSelecting)
+ {
+ LLWString selected_text = text.substr(mSelectionEnd, mSelectionStart - mSelectionEnd);
+
+ if (selected_text == search_text)
+ {
+ // We already have this word selected, we are searching for the next.
+ mCursorPos += search_text.size();
+ }
+ }
+
+ S32 loc = text.find(search_text,mCursorPos);
+
+ // If Maybe we wrapped, search again
+ if (wrap && (-1 == loc))
+ {
+ loc = text.find(search_text);
+ }
+
+ // If still -1, then search_text just isn't found.
+ if (-1 == loc)
+ {
+ mIsSelecting = FALSE;
+ mSelectionEnd = 0;
+ mSelectionStart = 0;
+ return;
+ }
+
+ setCursorPos(loc);
+
+ mIsSelecting = TRUE;
+ mSelectionEnd = mCursorPos;
+ mSelectionStart = llmin((S32)getLength(), (S32)(mCursorPos + search_text.size()));
+}
+
+BOOL LLTextEditor::replaceText(const LLString& search_text_in, const LLString& 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)
+ {
+ LLWString::toLower(selected_text);
+ LLWString::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 LLString& search_text, const LLString& replace_text, BOOL case_insensitive)
+{
+ S32 cur_pos = mScrollbar->getDocPos();
+
+ setCursorPos(0);
+ selectNext(search_text, case_insensitive, FALSE);
+
+ BOOL replaced = TRUE;
+ while ( replaced )
+ {
+ replaced = replaceText(search_text,replace_text, case_insensitive, FALSE);
+ }
+
+ mScrollbar->setDocPos(cur_pos);
+}
+
+void LLTextEditor::setTakesNonScrollClicks(BOOL b)
+{
+ mTakesNonScrollClicks = b;
+}
+
+
+// Picks a new cursor position based on the screen size of text being drawn.
+void LLTextEditor::setCursorAtLocalPos( S32 local_x, S32 local_y, BOOL round )
+{
+ setCursorPos(getCursorPosFromLocalCoord(local_x, local_y, round));
+}
+
+S32 LLTextEditor::prevWordPos(S32 cursorPos) const
+{
+ const LLWString& wtext = mWText;
+ while( (cursorPos > 0) && (wtext[cursorPos-1] == ' ') )
+ {
+ cursorPos--;
+ }
+ while( (cursorPos > 0) && isPartOfWord( wtext[cursorPos-1] ) )
+ {
+ cursorPos--;
+ }
+ return cursorPos;
+}
+
+S32 LLTextEditor::nextWordPos(S32 cursorPos) const
+{
+ const LLWString& wtext = mWText;
+ while( (cursorPos < getLength()) && isPartOfWord( wtext[cursorPos+1] ) )
+ {
+ cursorPos++;
+ }
+ while( (cursorPos < getLength()) && (wtext[cursorPos+1] == ' ') )
+ {
+ cursorPos++;
+ }
+ return cursorPos;
+}
+
+S32 LLTextEditor::getLineCount()
+{
+ return mLineStartList.size();
+}
+
+S32 LLTextEditor::getLineStart( S32 line )
+{
+ S32 num_lines = getLineCount();
+ if (num_lines == 0)
+ {
+ return 0;
+ }
+ line = llclamp(line, 0, num_lines-1);
+ S32 segidx = mLineStartList[line].mSegment;
+ S32 segoffset = mLineStartList[line].mOffset;
+ LLTextSegment* seg = mSegments[segidx];
+ S32 res = seg->getStart() + segoffset;
+ if (res > seg->getEnd()) llerrs << "wtf" << llendl;
+ return res;
+}
+
+// Given an offset into text (pos), find the corresponding line (from the start of the doc) and an offset into the line.
+void LLTextEditor::getLineAndOffset( S32 startpos, S32* linep, S32* offsetp )
+{
+ if (mLineStartList.empty())
+ {
+ *linep = 0;
+ *offsetp = startpos;
+ }
+ else
+ {
+ S32 seg_idx, seg_offset;
+ getSegmentAndOffset( startpos, &seg_idx, &seg_offset );
+
+ line_info tline(seg_idx, seg_offset);
+ line_list_t::iterator iter = std::upper_bound(mLineStartList.begin(), mLineStartList.end(), tline, line_info_compare());
+ if (iter != mLineStartList.begin()) --iter;
+ *linep = iter - mLineStartList.begin();
+ S32 line_start = mSegments[iter->mSegment]->getStart() + iter->mOffset;
+ *offsetp = startpos - line_start;
+ }
+}
+
+void LLTextEditor::getSegmentAndOffset( S32 startpos, S32* segidxp, S32* offsetp )
+{
+ if (mSegments.empty())
+ {
+ *segidxp = -1;
+ *offsetp = startpos;
+ }
+
+ LLTextSegment tseg(startpos);
+ segment_list_t::iterator seg_iter;
+ seg_iter = std::upper_bound(mSegments.begin(), mSegments.end(), &tseg, LLTextSegment::compare());
+ if (seg_iter != mSegments.begin()) --seg_iter;
+ *segidxp = seg_iter - mSegments.begin();
+ *offsetp = startpos - (*seg_iter)->getStart();
+}
+
+const LLWString& LLTextEditor::getWText() const
+{
+ return mWText;
+}
+
+S32 LLTextEditor::getLength() const
+{
+ return mWText.length();
+}
+
+llwchar LLTextEditor::getWChar(S32 pos)
+{
+ return mWText[pos];
+}
+
+LLWString LLTextEditor::getWSubString(S32 pos, S32 len)
+{
+ return mWText.substr(pos, len);
+}
+
+S32 LLTextEditor::getCursorPosFromLocalCoord( S32 local_x, S32 local_y, BOOL round )
+{
+ // If round is true, if the position is on the right half of a character, the cursor
+ // will be put to its right. If round is false, the cursor will always be put to the
+ // character's left.
+
+ // Figure out which line we're nearest to.
+ S32 total_lines = getLineCount();
+ S32 line_height = llround( mGLFont->getLineHeight() );
+ S32 max_visible_lines = mTextRect.getHeight() / line_height;
+ S32 scroll_lines = mScrollbar->getDocPos();
+ S32 visible_lines = llmin( total_lines - scroll_lines, max_visible_lines ); // Lines currently visible
+
+ //S32 line = S32( 0.5f + ((mTextRect.mTop - local_y) / mGLFont->getLineHeight()) );
+ S32 line = (mTextRect.mTop - 1 - local_y) / line_height;
+ if (line >= total_lines)
+ {
+ return getLength(); // past the end
+ }
+
+ line = llclamp( line, 0, visible_lines ) + scroll_lines;
+
+ S32 line_start = getLineStart(line);
+ S32 next_start = getLineStart(line+1);
+ S32 line_end = (next_start != line_start) ? next_start - 1 : getLength();
+
+ if(line_start == -1)
+ {
+ return 0;
+ }
+ else
+ {
+ S32 line_len = line_end - line_start;
+ S32 pos;
+
+ if (mAllowEmbeddedItems)
+ {
+ // Figure out which character we're nearest to.
+ bindEmbeddedChars(mGLFont);
+ pos = mGLFont->charFromPixelOffset(mWText.c_str(), line_start,
+ (F32)(local_x - mTextRect.mLeft),
+ (F32)(mTextRect.getWidth()),
+ line_len,
+ round, TRUE);
+ unbindEmbeddedChars(mGLFont);
+ }
+ else
+ {
+ pos = mGLFont->charFromPixelOffset(mWText.c_str(), line_start,
+ (F32)(local_x - mTextRect.mLeft),
+ (F32)mTextRect.getWidth(),
+ line_len,
+ round);
+ }
+
+ return line_start + pos;
+ }
+}
+
+void LLTextEditor::setCursor(S32 row, S32 column)
+{
+ const llwchar* doc = mWText.c_str();
+ const char CR = 10;
+ while(row--)
+ {
+ while (CR != *doc++);
+ }
+ doc += column;
+ setCursorPos(doc - mWText.c_str());
+ updateScrollFromCursor();
+}
+
+void LLTextEditor::setCursorPos(S32 offset)
+{
+ mCursorPos = llclamp(offset, 0, (S32)getLength());
+ updateScrollFromCursor();
+}
+
+
+BOOL LLTextEditor::canDeselect()
+{
+ return hasSelection();
+}
+
+
+void LLTextEditor::deselect()
+{
+ mSelectionStart = 0;
+ mSelectionEnd = 0;
+ mIsSelecting = FALSE;
+}
+
+
+void LLTextEditor::startSelection()
+{
+ if( !mIsSelecting )
+ {
+ mIsSelecting = TRUE;
+ mSelectionStart = mCursorPos;
+ mSelectionEnd = mCursorPos;
+ }
+}
+
+void LLTextEditor::endSelection()
+{
+ if( mIsSelecting )
+ {
+ mIsSelecting = FALSE;
+ mSelectionEnd = mCursorPos;
+ }
+ if (mParseHTML && mHTML.length() > 0)
+ {
+ //Special handling for slurls
+ if ( (mSecondlifeURLcallback!=NULL) && !(*mSecondlifeURLcallback)(mHTML) )
+ {
+ if (mURLcallback!=NULL) (*mURLcallback)(mHTML.c_str());
+
+ //load_url(url.c_str());
+ }
+ mHTML="";
+ }
+}
+
+BOOL LLTextEditor::selectionContainsLineBreaks()
+{
+ if (hasSelection())
+ {
+ S32 left = llmin(mSelectionStart, mSelectionEnd);
+ S32 right = left + abs(mSelectionStart - mSelectionEnd);
+
+ const LLWString &wtext = mWText;
+ for( S32 i = left; i < right; i++ )
+ {
+ if (wtext[i] == '\n')
+ {
+ return TRUE;
+ }
+ }
+ }
+ return FALSE;
+}
+
+
+S32 LLTextEditor::indentLine( S32 pos, S32 spaces )
+{
+ // Assumes that pos is at the start of the line
+ // spaces may be positive (indent) or negative (unindent).
+ // Returns the actual number of characters added or removed.
+
+ llassert(pos >= 0);
+ llassert(pos <= getLength() );
+
+ S32 delta_spaces = 0;
+
+ if (spaces >= 0)
+ {
+ // Indent
+ for(S32 i=0; i < spaces; i++)
+ {
+ delta_spaces += addChar(pos, ' ');
+ }
+ }
+ else
+ {
+ // Unindent
+ for(S32 i=0; i < -spaces; i++)
+ {
+ const LLWString &wtext = mWText;
+ if (wtext[pos] == ' ')
+ {
+ delta_spaces += remove( pos, 1, FALSE );
+ }
+ }
+ }
+
+ return delta_spaces;
+}
+
+void LLTextEditor::indentSelectedLines( S32 spaces )
+{
+ if( hasSelection() )
+ {
+ const LLWString &text = mWText;
+ S32 left = llmin( mSelectionStart, mSelectionEnd );
+ S32 right = left + abs( mSelectionStart - mSelectionEnd );
+ BOOL cursor_on_right = (mSelectionEnd > mSelectionStart);
+ S32 cur = left;
+
+ // Expand left to start of line
+ while( (cur > 0) && (text[cur] != '\n') )
+ {
+ cur--;
+ }
+ left = cur;
+ if( cur > 0 )
+ {
+ left++;
+ }
+
+ // Expand right to end of line
+ if( text[right - 1] == '\n' )
+ {
+ right--;
+ }
+ else
+ {
+ while( (text[right] != '\n') && (right <= getLength() ) )
+ {
+ right++;
+ }
+ }
+
+ // Find each start-of-line and indent it
+ do
+ {
+ if( text[cur] == '\n' )
+ {
+ cur++;
+ }
+
+ S32 delta_spaces = indentLine( cur, spaces );
+ if( delta_spaces > 0 )
+ {
+ cur += delta_spaces;
+ }
+ right += delta_spaces;
+
+ //text = mWText;
+
+ // Find the next new line
+ while( (cur < right) && (text[cur] != '\n') )
+ {
+ cur++;
+ }
+ }
+ while( cur < right );
+
+ if( (right < getLength()) && (text[right] == '\n') )
+ {
+ right++;
+ }
+
+ // Set the selection and cursor
+ if( cursor_on_right )
+ {
+ mSelectionStart = left;
+ mSelectionEnd = right;
+ }
+ else
+ {
+ mSelectionStart = right;
+ mSelectionEnd = left;
+ }
+ mCursorPos = mSelectionEnd;
+ }
+}
+
+
+BOOL LLTextEditor::canSelectAll()
+{
+ return TRUE;
+}
+
+void LLTextEditor::selectAll()
+{
+ mSelectionStart = getLength();
+ mSelectionEnd = 0;
+ mCursorPos = mSelectionEnd;
+}
+
+
+BOOL LLTextEditor::handleToolTip(S32 x, S32 y, LLString& msg, LLRect* sticky_rect_screen)
+{
+ if (pointInView(x, y) && getVisible())
+ {
+ for ( child_list_const_iter_t child_it = getChildList()->begin();
+ child_it != getChildList()->end(); ++child_it)
+ {
+ LLView* viewp = *child_it;
+ S32 local_x = x - viewp->getRect().mLeft;
+ S32 local_y = y - viewp->getRect().mBottom;
+ if( viewp->handleToolTip(local_x, local_y, msg, sticky_rect_screen ) )
+ {
+ return TRUE;
+ }
+ }
+
+ if( mSegments.empty() )
+ {
+ return TRUE;
+ }
+
+ LLTextSegment* cur_segment = getSegmentAtLocalPos( x, y );
+ if( cur_segment )
+ {
+ BOOL has_tool_tip = FALSE;
+ has_tool_tip = cur_segment->getToolTip( msg );
+
+ if( has_tool_tip )
+ {
+ // Just use a slop area around the cursor
+ // Convert rect local to screen coordinates
+ S32 SLOP = 8;
+ localPointToScreen(
+ x - SLOP, y - SLOP,
+ &(sticky_rect_screen->mLeft), &(sticky_rect_screen->mBottom) );
+ sticky_rect_screen->mRight = sticky_rect_screen->mLeft + 2 * SLOP;
+ sticky_rect_screen->mTop = sticky_rect_screen->mBottom + 2 * SLOP;
+ }
+ }
+ return TRUE;
+ }
+ return FALSE;
+}
+
+BOOL LLTextEditor::handleScrollWheel(S32 x, S32 y, S32 clicks)
+{
+ // Pretend the mouse is over the scrollbar
+ if (getVisible())
+ {
+ return mScrollbar->handleScrollWheel( 0, 0, clicks );
+ }
+ else
+ {
+ return FALSE;
+ }
+}
+
+BOOL LLTextEditor::handleMouseDown(S32 x, S32 y, MASK mask)
+{
+ BOOL handled = FALSE;
+
+ // Let scrollbar have first dibs
+ handled = LLView::childrenHandleMouseDown(x, y, mask) != NULL;
+
+ if( !handled && mTakesNonScrollClicks)
+ {
+ if (!(mask & MASK_SHIFT))
+ {
+ deselect();
+ }
+
+ BOOL start_select = TRUE;
+ if( start_select )
+ {
+ // If we're not scrolling (handled by child), then we're selecting
+ if (mask & MASK_SHIFT)
+ {
+ S32 old_cursor_pos = mCursorPos;
+ setCursorAtLocalPos( x, y, TRUE );
+
+ if (hasSelection())
+ {
+ /* Mac-like behavior - extend selection towards the cursor
+ if (mCursorPos < mSelectionStart
+ && mCursorPos < mSelectionEnd)
+ {
+ // ...left of selection
+ mSelectionStart = llmax(mSelectionStart, mSelectionEnd);
+ mSelectionEnd = mCursorPos;
+ }
+ else if (mCursorPos > mSelectionStart
+ && mCursorPos > mSelectionEnd)
+ {
+ // ...right of selection
+ mSelectionStart = llmin(mSelectionStart, mSelectionEnd);
+ mSelectionEnd = mCursorPos;
+ }
+ else
+ {
+ mSelectionEnd = mCursorPos;
+ }
+ */
+ // Windows behavior
+ mSelectionEnd = mCursorPos;
+ }
+ else
+ {
+ mSelectionStart = old_cursor_pos;
+ mSelectionEnd = mCursorPos;
+ }
+ // assume we're starting a drag select
+ mIsSelecting = TRUE;
+ }
+ else
+ {
+ setCursorAtLocalPos( x, y, TRUE );
+ startSelection();
+ }
+ gFocusMgr.setMouseCapture( this, &LLTextEditor::onMouseCaptureLost );
+ }
+
+ handled = TRUE;
+ }
+
+ if (mTakesFocus)
+ {
+ setFocus( TRUE );
+ handled = TRUE;
+ }
+
+ // Delay cursor flashing
+ mKeystrokeTimer.reset();
+
+ return handled;
+}
+
+
+BOOL LLTextEditor::handleHover(S32 x, S32 y, MASK mask)
+{
+ BOOL handled = FALSE;
+
+ mHoverSegment = NULL;
+ if( getVisible() )
+ {
+ if(gFocusMgr.getMouseCapture() == this )
+ {
+ if( mIsSelecting )
+ {
+ if (x != mLastSelectionX || y != mLastSelectionY)
+ {
+ mLastSelectionX = x;
+ mLastSelectionY = y;
+ }
+
+ if( y > mTextRect.mTop )
+ {
+ mScrollbar->setDocPos( mScrollbar->getDocPos() - 1 );
+ }
+ else
+ if( y < mTextRect.mBottom )
+ {
+ mScrollbar->setDocPos( mScrollbar->getDocPos() + 1 );
+ }
+
+ setCursorAtLocalPos( x, y, TRUE );
+ mSelectionEnd = mCursorPos;
+
+ updateScrollFromCursor();
+ }
+
+ lldebugst(LLERR_USER_INPUT) << "hover handled by " << getName() << " (active)" << llendl;
+ getWindow()->setCursor(UI_CURSOR_IBEAM);
+ handled = TRUE;
+ }
+
+ if( !handled )
+ {
+ // Pass to children
+ handled = LLView::childrenHandleHover(x, y, mask) != NULL;
+ }
+
+ if( handled )
+ {
+ // Delay cursor flashing
+ mKeystrokeTimer.reset();
+ }
+
+ // Opaque
+ if( !handled && mTakesNonScrollClicks)
+ {
+ // Check to see if we're over an HTML-style link
+ if( !mSegments.empty() )
+ {
+ LLTextSegment* cur_segment = getSegmentAtLocalPos( x, y );
+ if( cur_segment )
+ {
+ if(cur_segment->getStyle().isLink())
+ {
+ lldebugst(LLERR_USER_INPUT) << "hover handled by " << getName() << " (over link, inactive)" << llendl;
+ getWindow()->setCursor(UI_CURSOR_HAND);
+ handled = TRUE;
+ }
+ else
+ if(cur_segment->getStyle().getIsEmbeddedItem())
+ {
+ lldebugst(LLERR_USER_INPUT) << "hover handled by " << getName() << " (over embedded item, inactive)" << llendl;
+ getWindow()->setCursor(UI_CURSOR_HAND);
+ //getWindow()->setCursor(UI_CURSOR_ARROW);
+ handled = TRUE;
+ }
+ mHoverSegment = cur_segment;
+ }
+ }
+
+ if( !handled )
+ {
+ lldebugst(LLERR_USER_INPUT) << "hover handled by " << getName() << " (inactive)" << llendl;
+ if (!mScrollbar->getVisible() || x < mRect.getWidth() - SCROLLBAR_SIZE)
+ {
+ getWindow()->setCursor(UI_CURSOR_IBEAM);
+ }
+ else
+ {
+ getWindow()->setCursor(UI_CURSOR_ARROW);
+ }
+ handled = TRUE;
+ }
+ }
+ }
+
+ if (mOnScrollEndCallback && mOnScrollEndData && (mScrollbar->getDocPos() == mScrollbar->getDocPosMax()))
+ {
+ mOnScrollEndCallback(mOnScrollEndData);
+ }
+ return handled;
+}
+
+
+BOOL LLTextEditor::handleMouseUp(S32 x, S32 y, MASK mask)
+{
+ BOOL handled = FALSE;
+
+ // let scrollbar have first dibs
+ handled = LLView::childrenHandleMouseUp(x, y, mask) != NULL;
+
+ if( !handled && mTakesNonScrollClicks)
+ {
+ if( mIsSelecting )
+ {
+ // Finish selection
+ if( y > mTextRect.mTop )
+ {
+ mScrollbar->setDocPos( mScrollbar->getDocPos() - 1 );
+ }
+ else
+ if( y < mTextRect.mBottom )
+ {
+ mScrollbar->setDocPos( mScrollbar->getDocPos() + 1 );
+ }
+
+ setCursorAtLocalPos( x, y, TRUE );
+ endSelection();
+
+ updateScrollFromCursor();
+ }
+
+ if( !hasSelection() )
+ {
+ handleMouseUpOverSegment( x, y, mask );
+ }
+
+ handled = TRUE;
+ }
+
+ // Delay cursor flashing
+ mKeystrokeTimer.reset();
+
+ if( gFocusMgr.getMouseCapture() == this )
+ {
+ gFocusMgr.setMouseCapture( NULL, NULL );
+ handled = TRUE;
+ }
+
+ return handled;
+}
+
+
+BOOL LLTextEditor::handleDoubleClick(S32 x, S32 y, MASK mask)
+{
+ BOOL handled = FALSE;
+
+ // let scrollbar have first dibs
+ handled = LLView::childrenHandleDoubleClick(x, y, mask) != NULL;
+
+ if( !handled && mTakesNonScrollClicks)
+ {
+ if (mTakesFocus)
+ {
+ setFocus( TRUE );
+ }
+
+ setCursorAtLocalPos( x, y, FALSE );
+ deselect();
+
+ const LLWString &text = mWText;
+
+ if( isPartOfWord( text[mCursorPos] ) )
+ {
+ // Select word the cursor is over
+ while ((mCursorPos > 0) && isPartOfWord(text[mCursorPos-1]))
+ {
+ mCursorPos--;
+ }
+ startSelection();
+
+ while ((mCursorPos < (S32)text.length()) && isPartOfWord( text[mCursorPos] ) )
+ {
+ mCursorPos++;
+ }
+
+ mSelectionEnd = mCursorPos;
+ }
+ else if ((mCursorPos < (S32)text.length()) && !iswspace( text[mCursorPos]) )
+ {
+ // Select the character the cursor is over
+ startSelection();
+ mCursorPos++;
+ mSelectionEnd = mCursorPos;
+ }
+
+ // We don't want handleMouseUp() to "finish" the selection (and thereby
+ // set mSelectionEnd to where the mouse is), so we finish the selection here.
+ mIsSelecting = FALSE;
+
+ // delay cursor flashing
+ mKeystrokeTimer.reset();
+
+ handled = TRUE;
+ }
+ return handled;
+}
+
+
+// Allow calling cards to be dropped onto text fields. Append the name and
+// a carriage return.
+// virtual
+BOOL LLTextEditor::handleDragAndDrop(S32 x, S32 y, MASK mask,
+ BOOL drop, EDragAndDropType cargo_type, void *cargo_data,
+ EAcceptance *accept,
+ LLString& tooltip_msg)
+{
+ *accept = ACCEPT_NO;
+
+ return TRUE;
+}
+
+//----------------------------------------------------------------------------
+// Returns change in number of characters in mText
+
+S32 LLTextEditor::execute( LLTextCmd* cmd )
+{
+ S32 delta = 0;
+ if( cmd->execute(this, &delta) )
+ {
+ // Delete top of undo stack
+ undo_stack_t::iterator enditer = std::find(mUndoStack.begin(), mUndoStack.end(), mLastCmd);
+ if (enditer != mUndoStack.begin())
+ {
+ --enditer;
+ std::for_each(mUndoStack.begin(), enditer, DeletePointer());
+ mUndoStack.erase(mUndoStack.begin(), enditer);
+ }
+ // Push the new command is now on the top (front) of the undo stack.
+ mUndoStack.push_front(cmd);
+ mLastCmd = cmd;
+ }
+ else
+ {
+ // Operation failed, so don't put it on the undo stack.
+ delete cmd;
+ }
+
+ return delta;
+}
+
+S32 LLTextEditor::insert(const S32 pos, const LLWString &wstr, const BOOL group_with_next_op)
+{
+ return execute( new LLTextCmdInsert( pos, group_with_next_op, wstr ) );
+}
+
+S32 LLTextEditor::remove(const S32 pos, const S32 length, const BOOL group_with_next_op)
+{
+ return execute( new LLTextCmdRemove( pos, group_with_next_op, length ) );
+}
+
+S32 LLTextEditor::append(const LLWString &wstr, const BOOL group_with_next_op)
+{
+ return insert(mWText.length(), wstr, group_with_next_op);
+}
+
+S32 LLTextEditor::overwriteChar(S32 pos, llwchar wc)
+{
+ if ((S32)mWText.length() == pos)
+ {
+ return addChar(pos, wc);
+ }
+ else
+ {
+ return execute(new LLTextCmdOverwriteChar(pos, FALSE, wc));
+ }
+}
+
+// Remove a single character from the text. Tries to remove
+// a pseudo-tab (up to for spaces in a row)
+void LLTextEditor::removeCharOrTab()
+{
+ if( getEnabled() )
+ {
+ if( mCursorPos > 0 )
+ {
+ S32 chars_to_remove = 1;
+
+ const LLWString &text = mWText;
+ if (text[mCursorPos - 1] == ' ')
+ {
+ // Try to remove a "tab"
+ S32 line, offset;
+ getLineAndOffset(mCursorPos, &line, &offset);
+ if (offset > 0)
+ {
+ chars_to_remove = offset % SPACES_PER_TAB;
+ if( chars_to_remove == 0 )
+ {
+ chars_to_remove = SPACES_PER_TAB;
+ }
+
+ for( S32 i = 0; i < chars_to_remove; i++ )
+ {
+ if (text[ mCursorPos - i - 1] != ' ')
+ {
+ // Fewer than a full tab's worth of spaces, so
+ // just delete a single character.
+ chars_to_remove = 1;
+ break;
+ }
+ }
+ }
+ }
+
+ for (S32 i = 0; i < chars_to_remove; i++)
+ {
+ setCursorPos(mCursorPos - 1);
+ remove( mCursorPos, 1, FALSE );
+ }
+ }
+ else
+ {
+ reportBadKeystroke();
+ }
+ }
+}
+
+// Remove a single character from the text
+S32 LLTextEditor::removeChar(S32 pos)
+{
+ return remove( pos, 1, FALSE );
+}
+
+void LLTextEditor::removeChar()
+{
+ if (getEnabled())
+ {
+ if (mCursorPos > 0)
+ {
+ setCursorPos(mCursorPos - 1);
+ removeChar(mCursorPos);
+ }
+ else
+ {
+ reportBadKeystroke();
+ }
+ }
+}
+
+// Add a single character to the text
+S32 LLTextEditor::addChar(S32 pos, llwchar wc)
+{
+ if ((S32)mWText.length() == mMaxTextLength)
+ {
+ make_ui_sound("UISndBadKeystroke");
+ return 0;
+ }
+
+ if (mLastCmd && mLastCmd->canExtend(pos))
+ {
+ S32 delta = 0;
+ mLastCmd->extendAndExecute(this, pos, wc, &delta);
+ return delta;
+ }
+ else
+ {
+ return execute(new LLTextCmdAddChar(pos, FALSE, wc));
+ }
+}
+
+void LLTextEditor::addChar(llwchar wc)
+{
+ if( getEnabled() )
+ {
+ if( hasSelection() )
+ {
+ deleteSelection(TRUE);
+ }
+ else if (LL_KIM_OVERWRITE == gKeyboard->getInsertMode())
+ {
+ removeChar(mCursorPos);
+ }
+
+ setCursorPos(mCursorPos + addChar( mCursorPos, wc ));
+ }
+}
+
+
+BOOL LLTextEditor::handleSelectionKey(const KEY key, const MASK mask)
+{
+ BOOL handled = FALSE;
+
+ if( mask & MASK_SHIFT )
+ {
+ handled = TRUE;
+
+ switch( key )
+ {
+ case KEY_LEFT:
+ if( 0 < mCursorPos )
+ {
+ startSelection();
+ mCursorPos--;
+ if( mask & MASK_CONTROL )
+ {
+ mCursorPos = prevWordPos(mCursorPos);
+ }
+ mSelectionEnd = mCursorPos;
+ }
+ break;
+
+ case KEY_RIGHT:
+ if( mCursorPos < getLength() )
+ {
+ startSelection();
+ mCursorPos++;
+ if( mask & MASK_CONTROL )
+ {
+ mCursorPos = nextWordPos(mCursorPos);
+ }
+ mSelectionEnd = mCursorPos;
+ }
+ break;
+
+ case KEY_UP:
+ startSelection();
+ changeLine( -1 );
+ mSelectionEnd = mCursorPos;
+ break;
+
+ case KEY_PAGE_UP:
+ startSelection();
+ changePage( -1 );
+ mSelectionEnd = mCursorPos;
+ break;
+
+ case KEY_HOME:
+ startSelection();
+ if( mask & MASK_CONTROL )
+ {
+ mCursorPos = 0;
+ }
+ else
+ {
+ startOfLine();
+ }
+ mSelectionEnd = mCursorPos;
+ break;
+
+ case KEY_DOWN:
+ startSelection();
+ changeLine( 1 );
+ mSelectionEnd = mCursorPos;
+ break;
+
+ case KEY_PAGE_DOWN:
+ startSelection();
+ changePage( 1 );
+ mSelectionEnd = mCursorPos;
+ break;
+
+ case KEY_END:
+ startSelection();
+ if( mask & MASK_CONTROL )
+ {
+ mCursorPos = getLength();
+ }
+ else
+ {
+ endOfLine();
+ }
+ mSelectionEnd = mCursorPos;
+ break;
+
+ default:
+ handled = FALSE;
+ break;
+ }
+ }
+
+
+ if( !handled && mHandleEditKeysDirectly )
+ {
+ if( (MASK_CONTROL & mask) && ('A' == key) )
+ {
+ if( canSelectAll() )
+ {
+ selectAll();
+ }
+ else
+ {
+ reportBadKeystroke();
+ }
+ handled = TRUE;
+ }
+ }
+
+ return handled;
+}
+
+BOOL LLTextEditor::handleNavigationKey(const KEY key, const MASK mask)
+{
+ BOOL handled = FALSE;
+
+ // Ignore capslock key
+ if( MASK_NONE == mask )
+ {
+ handled = TRUE;
+ switch( key )
+ {
+ case KEY_UP:
+ if (mReadOnly)
+ {
+ mScrollbar->setDocPos(mScrollbar->getDocPos() - 1);
+ }
+ else
+ {
+ changeLine( -1 );
+ }
+ break;
+
+ case KEY_PAGE_UP:
+ changePage( -1 );
+ break;
+
+ case KEY_HOME:
+ if (mReadOnly)
+ {
+ mScrollbar->setDocPos(0);
+ }
+ else
+ {
+ startOfLine();
+ }
+ break;
+
+ case KEY_DOWN:
+ if (mReadOnly)
+ {
+ mScrollbar->setDocPos(mScrollbar->getDocPos() + 1);
+ }
+ else
+ {
+ changeLine( 1 );
+ }
+ break;
+
+ case KEY_PAGE_DOWN:
+ changePage( 1 );
+ break;
+
+ case KEY_END:
+ if (mReadOnly)
+ {
+ mScrollbar->setDocPos(mScrollbar->getDocPosMax());
+ }
+ else
+ {
+ endOfLine();
+ }
+ break;
+
+ case KEY_LEFT:
+ if (mReadOnly)
+ {
+ break;
+ }
+ if( hasSelection() )
+ {
+ setCursorPos(llmin( mCursorPos - 1, mSelectionStart, mSelectionEnd ));
+ }
+ else
+ {
+ if( 0 < mCursorPos )
+ {
+ setCursorPos(mCursorPos - 1);
+ }
+ else
+ {
+ reportBadKeystroke();
+ }
+ }
+ break;
+
+ case KEY_RIGHT:
+ if (mReadOnly)
+ {
+ break;
+ }
+ if( hasSelection() )
+ {
+ setCursorPos(llmax( mCursorPos + 1, mSelectionStart, mSelectionEnd ));
+ }
+ else
+ {
+ if( mCursorPos < getLength() )
+ {
+ setCursorPos(mCursorPos + 1);
+ }
+ else
+ {
+ reportBadKeystroke();
+ }
+ }
+ break;
+
+ default:
+ handled = FALSE;
+ break;
+ }
+ }
+
+ if (mOnScrollEndCallback && mOnScrollEndData && (mScrollbar->getDocPos() == mScrollbar->getDocPosMax()))
+ {
+ mOnScrollEndCallback(mOnScrollEndData);
+ }
+ return handled;
+}
+
+void LLTextEditor::deleteSelection(BOOL group_with_next_op )
+{
+ if( getEnabled() && hasSelection() )
+ {
+ S32 pos = llmin( mSelectionStart, mSelectionEnd );
+ S32 length = abs( mSelectionStart - mSelectionEnd );
+
+ remove( pos, length, group_with_next_op );
+
+ deselect();
+ setCursorPos(pos);
+ }
+}
+
+BOOL LLTextEditor::canCut()
+{
+ return !mReadOnly && hasSelection();
+}
+
+// cut selection to clipboard
+void LLTextEditor::cut()
+{
+ if( canCut() )
+ {
+ S32 left_pos = llmin( mSelectionStart, mSelectionEnd );
+ S32 length = abs( mSelectionStart - mSelectionEnd );
+ gClipboard.copyFromSubstring( mWText, left_pos, length, mSourceID );
+ deleteSelection( FALSE );
+
+ updateLineStartList();
+ updateScrollFromCursor();
+ }
+}
+
+BOOL LLTextEditor::canCopy()
+{
+ return hasSelection();
+}
+
+
+// copy selection to clipboard
+void LLTextEditor::copy()
+{
+ if( canCopy() )
+ {
+ S32 left_pos = llmin( mSelectionStart, mSelectionEnd );
+ S32 length = abs( mSelectionStart - mSelectionEnd );
+ gClipboard.copyFromSubstring(mWText, left_pos, length, mSourceID);
+ }
+}
+
+BOOL LLTextEditor::canPaste()
+{
+ return !mReadOnly && gClipboard.canPasteString();
+}
+
+
+// paste from clipboard
+void LLTextEditor::paste()
+{
+ if (canPaste())
+ {
+ LLUUID source_id;
+ LLWString paste = gClipboard.getPasteWString(&source_id);
+ if (!paste.empty())
+ {
+ // Delete any selected characters (the paste replaces them)
+ if( hasSelection() )
+ {
+ deleteSelection(TRUE);
+ }
+
+ // Clean up string (replace tabs and remove characters that our fonts don't support).
+ LLWString clean_string(paste);
+ LLWString::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 < LLFont::FIRST_CHAR) && (wc != LF) )
+ {
+ clean_string[i] = LL_UNKNOWN_CHAR;
+ }
+ else if (wc >= FIRST_EMBEDDED_CHAR && wc <= LAST_EMBEDDED_CHAR)
+ {
+ clean_string[i] = pasteEmbeddedItem(wc);
+ }
+ }
+ }
+
+ // Insert the new text into the existing text.
+ setCursorPos(mCursorPos + insert(mCursorPos, clean_string, FALSE));
+ deselect();
+
+ updateLineStartList();
+ updateScrollFromCursor();
+ }
+ }
+}
+
+
+BOOL LLTextEditor::handleControlKey(const KEY key, const MASK mask)
+{
+ BOOL handled = FALSE;
+
+ if( mask & MASK_CONTROL )
+ {
+ handled = TRUE;
+
+ switch( key )
+ {
+ case KEY_HOME:
+ if( mask & MASK_SHIFT )
+ {
+ startSelection();
+ mCursorPos = 0;
+ mSelectionEnd = mCursorPos;
+ }
+ else
+ {
+ // Ctrl-Home, Ctrl-Left, Ctrl-Right, Ctrl-Down
+ // all move the cursor as if clicking, so should deselect.
+ deselect();
+ setCursorPos(0);
+ }
+ break;
+
+ case KEY_END:
+ {
+ if( mask & MASK_SHIFT )
+ {
+ startSelection();
+ }
+ else
+ {
+ // Ctrl-Home, Ctrl-Left, Ctrl-Right, Ctrl-Down
+ // all move the cursor as if clicking, so should deselect.
+ deselect();
+ }
+ endOfDoc();
+ if( mask & MASK_SHIFT )
+ {
+ mSelectionEnd = mCursorPos;
+ }
+ break;
+ }
+
+ case KEY_RIGHT:
+ if( mCursorPos < getLength() )
+ {
+ // Ctrl-Home, Ctrl-Left, Ctrl-Right, Ctrl-Down
+ // all move the cursor as if clicking, so should deselect.
+ deselect();
+
+ setCursorPos(nextWordPos(mCursorPos + 1));
+ }
+ break;
+
+
+ case KEY_LEFT:
+ if( mCursorPos > 0 )
+ {
+ // Ctrl-Home, Ctrl-Left, Ctrl-Right, Ctrl-Down
+ // all move the cursor as if clicking, so should deselect.
+ deselect();
+
+ setCursorPos(prevWordPos(mCursorPos - 1));
+ }
+ break;
+
+ default:
+ handled = FALSE;
+ break;
+ }
+ }
+
+ return handled;
+}
+
+BOOL LLTextEditor::handleEditKey(const KEY key, const MASK mask)
+{
+ BOOL handled = FALSE;
+
+ // Standard edit keys (Ctrl-X, Delete, etc,) are handled here instead of routed by the menu system.
+ if( KEY_DELETE == key )
+ {
+ if( canDoDelete() )
+ {
+ doDelete();
+ }
+ else
+ {
+ reportBadKeystroke();
+ }
+ handled = TRUE;
+ }
+ else
+ if( MASK_CONTROL & mask )
+ {
+ if( 'C' == key )
+ {
+ if( canCopy() )
+ {
+ copy();
+ }
+ else
+ {
+ reportBadKeystroke();
+ }
+ handled = TRUE;
+ }
+ else
+ if( 'V' == key )
+ {
+ if( canPaste() )
+ {
+ paste();
+ }
+ else
+ {
+ reportBadKeystroke();
+ }
+ handled = TRUE;
+ }
+ else
+ if( 'X' == key )
+ {
+ if( canCut() )
+ {
+ cut();
+ }
+ else
+ {
+ reportBadKeystroke();
+ }
+ handled = TRUE;
+ }
+ }
+
+ return handled;
+}
+
+
+BOOL LLTextEditor::handleSpecialKey(const KEY key, const MASK mask, BOOL* return_key_hit)
+{
+ *return_key_hit = FALSE;
+ BOOL handled = TRUE;
+
+ switch( key )
+ {
+ case KEY_INSERT:
+ if (mask == MASK_NONE)
+ {
+ gKeyboard->toggleInsertMode();
+ }
+ break;
+
+ case KEY_BACKSPACE:
+ if( hasSelection() )
+ {
+ deleteSelection(FALSE);
+ }
+ else
+ if( 0 < mCursorPos )
+ {
+ removeCharOrTab();
+ }
+ else
+ {
+ reportBadKeystroke();
+ }
+ break;
+
+
+ case KEY_RETURN:
+ if (mask == MASK_NONE)
+ {
+ if( hasSelection() )
+ {
+ deleteSelection(FALSE);
+ }
+ autoIndent(); // TODO: make this optional
+ }
+ else
+ {
+ handled = FALSE;
+ break;
+ }
+ break;
+
+ case KEY_TAB:
+ if (mask & MASK_CONTROL)
+ {
+ handled = FALSE;
+ break;
+ }
+ if( hasSelection() && selectionContainsLineBreaks() )
+ {
+ indentSelectedLines( (mask & MASK_SHIFT) ? -SPACES_PER_TAB : SPACES_PER_TAB );
+ }
+ else
+ {
+ if( hasSelection() )
+ {
+ deleteSelection(FALSE);
+ }
+
+ S32 line, offset;
+ getLineAndOffset( mCursorPos, &line, &offset );
+
+ S32 spaces_needed = SPACES_PER_TAB - (offset % SPACES_PER_TAB);
+ for( S32 i=0; i < spaces_needed; i++ )
+ {
+ addChar( ' ' );
+ }
+ }
+ break;
+
+ default:
+ handled = FALSE;
+ break;
+ }
+
+ return handled;
+}
+
+
+void LLTextEditor::unindentLineBeforeCloseBrace()
+{
+ if( mCursorPos >= 1 )
+ {
+ const LLWString &text = mWText;
+ if( ' ' == text[ mCursorPos - 1 ] )
+ {
+ removeCharOrTab();
+ }
+ }
+}
+
+
+BOOL LLTextEditor::handleKeyHere(KEY key, MASK mask, BOOL called_from_parent )
+{
+ BOOL handled = FALSE;
+ BOOL selection_modified = FALSE;
+ BOOL return_key_hit = FALSE;
+ BOOL text_may_have_changed = TRUE;
+
+ if ( (gFocusMgr.getKeyboardFocus() == this) && getVisible())
+ {
+ // 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 && mTabToNextField)
+ {
+ return FALSE;
+ }
+
+ handled = handleNavigationKey( key, mask );
+ if( handled )
+ {
+ text_may_have_changed = FALSE;
+ }
+
+ if( !handled )
+ {
+ handled = handleSelectionKey( key, mask );
+ if( handled )
+ {
+ selection_modified = TRUE;
+ }
+ }
+
+ if( !handled )
+ {
+ handled = handleControlKey( key, mask );
+ if( handled )
+ {
+ selection_modified = TRUE;
+ }
+ }
+
+ if( !handled && mHandleEditKeysDirectly )
+ {
+ handled = handleEditKey( key, mask );
+ if( handled )
+ {
+ selection_modified = TRUE;
+ text_may_have_changed = TRUE;
+ }
+ }
+
+ // Handle most keys only if the text editor is writeable.
+ if( !mReadOnly )
+ {
+ if( !handled )
+ {
+ handled = handleSpecialKey( key, mask, &return_key_hit );
+ if( handled )
+ {
+ selection_modified = TRUE;
+ text_may_have_changed = TRUE;
+ }
+ }
+
+ }
+
+ if( handled )
+ {
+ mKeystrokeTimer.reset();
+
+ // Most keystrokes will make the selection box go away, but not all will.
+ if( !selection_modified &&
+ KEY_SHIFT != key &&
+ KEY_CONTROL != key &&
+ KEY_ALT != key &&
+ KEY_CAPSLOCK )
+ {
+ deselect();
+ }
+
+ if(text_may_have_changed)
+ {
+ updateLineStartList();
+ }
+ updateScrollFromCursor();
+ }
+ }
+
+ return handled;
+}
+
+
+BOOL LLTextEditor::handleUnicodeCharHere(llwchar uni_char, BOOL called_from_parent)
+{
+ if ((uni_char < 0x20) || (uni_char == 0x7F)) // Control character or DEL
+ {
+ return FALSE;
+ }
+
+ BOOL handled = FALSE;
+
+ if ( (gFocusMgr.getKeyboardFocus() == this) && getVisible())
+ {
+ // Handle most keys only if the text editor is writeable.
+ if( !mReadOnly )
+ {
+ if( '}' == uni_char )
+ {
+ unindentLineBeforeCloseBrace();
+ }
+
+ // TODO: KLW Add auto show of tool tip on (
+ addChar( uni_char );
+
+ // Keys that add characters temporarily hide the cursor
+ getWindow()->hideCursorUntilMouseMove();
+
+ handled = TRUE;
+ }
+
+ if( handled )
+ {
+ mKeystrokeTimer.reset();
+
+ // Most keystrokes will make the selection box go away, but not all will.
+ deselect();
+
+ updateLineStartList();
+ updateScrollFromCursor();
+ }
+ }
+
+ return handled;
+}
+
+
+
+BOOL LLTextEditor::canDoDelete()
+{
+ return !mReadOnly && ( hasSelection() || (mCursorPos < getLength()) );
+}
+
+void LLTextEditor::doDelete()
+{
+ if( canDoDelete() )
+ {
+ if( hasSelection() )
+ {
+ deleteSelection(FALSE);
+ }
+ else
+ if( mCursorPos < getLength() )
+ {
+ S32 i;
+ S32 chars_to_remove = 1;
+ const LLWString &text = mWText;
+ if( (text[ mCursorPos ] == ' ') && (mCursorPos + SPACES_PER_TAB < getLength()) )
+ {
+ // Try to remove a full tab's worth of spaces
+ S32 line, offset;
+ getLineAndOffset( mCursorPos, &line, &offset );
+ chars_to_remove = SPACES_PER_TAB - (offset % SPACES_PER_TAB);
+ if( chars_to_remove == 0 )
+ {
+ chars_to_remove = SPACES_PER_TAB;
+ }
+
+ for( i = 0; i < chars_to_remove; i++ )
+ {
+ if( text[mCursorPos + i] != ' ' )
+ {
+ chars_to_remove = 1;
+ break;
+ }
+ }
+ }
+
+
+ for( i = 0; i < chars_to_remove; i++ )
+ {
+ setCursorPos(mCursorPos + 1);
+ removeChar();
+ }
+ }
+
+ updateLineStartList();
+ updateScrollFromCursor();
+ }
+}
+
+//----------------------------------------------------------------------------
+
+
+void LLTextEditor::blockUndo()
+{
+ mBaseDocIsPristine = FALSE;
+ mLastCmd = NULL;
+ std::for_each(mUndoStack.begin(), mUndoStack.end(), DeletePointer());
+ mUndoStack.clear();
+}
+
+
+BOOL LLTextEditor::canUndo()
+{
+ return !mReadOnly && mLastCmd != NULL;
+}
+
+void LLTextEditor::undo()
+{
+ if( canUndo() )
+ {
+ 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);
+
+ updateLineStartList();
+ updateScrollFromCursor();
+ }
+}
+
+BOOL LLTextEditor::canRedo()
+{
+ return !mReadOnly && (mUndoStack.size() > 0) && (mLastCmd != mUndoStack.front());
+}
+
+void LLTextEditor::redo()
+{
+ if( canRedo() )
+ {
+ 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);
+
+ updateLineStartList();
+ updateScrollFromCursor();
+ }
+}
+
+
+// virtual, from LLView
+void LLTextEditor::onFocusLost()
+{
+ // Route menu back to the default
+ if( gEditMenuHandler == this )
+ {
+ gEditMenuHandler = NULL;
+ }
+
+ if (mCommitOnFocusLost)
+ {
+ onCommit();
+ }
+
+ // Make sure cursor is shown again
+ getWindow()->showCursorFromMouseMove();
+}
+
+void LLTextEditor::setEnabled(BOOL enabled)
+{
+ // just treat enabled as read-only flag
+ BOOL read_only = !enabled;
+ if (read_only != mReadOnly)
+ {
+ mReadOnly = read_only;
+ updateSegments();
+ }
+}
+
+void LLTextEditor::drawBackground()
+{
+ S32 left = 0;
+ S32 top = mRect.getHeight();
+ S32 right = mRect.getWidth();
+ S32 bottom = 0;
+
+ LLColor4 bg_color = mReadOnlyBgColor;
+
+ if( !mReadOnly )
+ {
+ if (gFocusMgr.getKeyboardFocus() == this)
+ {
+ bg_color = mFocusBgColor;
+ }
+ else
+ {
+ bg_color = mWriteableBgColor;
+ }
+ }
+ gl_rect_2d(left, top, right, bottom, bg_color);
+
+ LLView::draw();
+}
+
+// Draws the black box behind the selected text
+void LLTextEditor::drawSelectionBackground()
+{
+ // Draw selection even if we don't have keyboard focus for search/replace
+ if( hasSelection() )
+ {
+ const LLWString &text = mWText;
+ const S32 text_len = getLength();
+ std::queue<S32> line_endings;
+
+ S32 line_height = llround( mGLFont->getLineHeight() );
+
+ S32 selection_left = llmin( mSelectionStart, mSelectionEnd );
+ S32 selection_right = llmax( mSelectionStart, mSelectionEnd );
+ S32 selection_left_x = mTextRect.mLeft;
+ S32 selection_left_y = mTextRect.mTop - line_height;
+ S32 selection_right_x = mTextRect.mRight;
+ S32 selection_right_y = mTextRect.mBottom;
+
+ BOOL selection_left_visible = FALSE;
+ BOOL selection_right_visible = FALSE;
+
+ // Skip through the lines we aren't drawing.
+ S32 cur_line = mScrollbar->getDocPos();
+
+ S32 left_line_num = cur_line;
+ S32 num_lines = getLineCount();
+ S32 right_line_num = num_lines - 1;
+
+ S32 line_start = -1;
+ if (cur_line >= num_lines)
+ {
+ return;
+ }
+
+ line_start = getLineStart(cur_line);
+
+ S32 left_visible_pos = line_start;
+ S32 right_visible_pos = line_start;
+
+ S32 text_y = mTextRect.mTop - line_height;
+
+ // Find the coordinates of the selected area
+ while((cur_line < num_lines))
+ {
+ S32 next_line = -1;
+ S32 line_end = text_len;
+
+ if ((cur_line + 1) < num_lines)
+ {
+ next_line = getLineStart(cur_line + 1);
+ line_end = next_line;
+
+ line_end = ( (line_end - line_start)==0 || text[next_line-1] == '\n' || text[next_line-1] == '\0' || text[next_line-1] == ' ' || text[next_line-1] == '\t' ) ? next_line-1 : next_line;
+ }
+
+ const llwchar* line = text.c_str() + line_start;
+
+ if( line_start <= selection_left && selection_left <= line_end )
+ {
+ left_line_num = cur_line;
+ selection_left_visible = TRUE;
+ selection_left_x = mTextRect.mLeft + mGLFont->getWidth(line, 0, selection_left - line_start, mAllowEmbeddedItems);
+ selection_left_y = text_y;
+ }
+ if( line_start <= selection_right && selection_right <= line_end )
+ {
+ right_line_num = cur_line;
+ selection_right_visible = TRUE;
+ selection_right_x = mTextRect.mLeft + mGLFont->getWidth(line, 0, selection_right - line_start, mAllowEmbeddedItems);
+ if (selection_right == line_end)
+ {
+ // add empty space for "newline"
+ //selection_right_x += mGLFont->getWidth("n");
+ }
+ selection_right_y = text_y;
+ }
+
+ // if selection spans end of current line...
+ if (selection_left <= line_end && line_end < selection_right && selection_left != selection_right)
+ {
+ // extend selection slightly beyond end of line
+ // to indicate selection of newline character (use "n" character to determine width)
+ const LLWString nstr(utf8str_to_wstring(LLString("n")));
+ line_endings.push(mTextRect.mLeft + mGLFont->getWidth(line, 0, line_end - line_start, mAllowEmbeddedItems) + mGLFont->getWidth(nstr.c_str()));
+ }
+
+ // move down one line
+ text_y -= line_height;
+
+ right_visible_pos = line_end;
+ line_start = next_line;
+ cur_line++;
+
+ if (selection_right_visible)
+ {
+ break;
+ }
+ }
+
+ // Draw the selection box (we're using a box instead of reversing the colors on the selected text).
+ BOOL selection_visible = (left_visible_pos <= selection_right) && (selection_left <= right_visible_pos);
+ if( selection_visible )
+ {
+ LLGLSNoTexture no_texture;
+ const LLColor4& color = mReadOnly ? mReadOnlyBgColor : mWriteableBgColor;
+ glColor3f( 1.f - color.mV[0], 1.f - color.mV[1], 1.f - color.mV[2] );
+
+ if( selection_left_y == selection_right_y )
+ {
+ // Draw from selection start to selection end
+ gl_rect_2d( selection_left_x, selection_left_y + line_height + 1,
+ selection_right_x, selection_right_y);
+ }
+ else
+ {
+ // Draw from selection start to the end of the first line
+ if( mTextRect.mRight == selection_left_x )
+ {
+ selection_left_x -= CURSOR_THICKNESS;
+ }
+
+ S32 line_end = line_endings.front();
+ line_endings.pop();
+ gl_rect_2d( selection_left_x, selection_left_y + line_height + 1,
+ line_end, selection_left_y );
+
+ S32 line_num = left_line_num + 1;
+ while(line_endings.size())
+ {
+ S32 vert_offset = -(line_num - left_line_num) * line_height;
+ // Draw the block between the two lines
+ gl_rect_2d( mTextRect.mLeft, selection_left_y + vert_offset + line_height + 1,
+ line_endings.front(), selection_left_y + vert_offset);
+ line_endings.pop();
+ line_num++;
+ }
+
+ // Draw from the start of the last line to selection end
+ if( mTextRect.mLeft == selection_right_x )
+ {
+ selection_right_x += CURSOR_THICKNESS;
+ }
+ gl_rect_2d( mTextRect.mLeft, selection_right_y + line_height + 1,
+ selection_right_x, selection_right_y );
+ }
+ }
+ }
+}
+
+void LLTextEditor::drawCursor()
+{
+ if( gFocusMgr.getKeyboardFocus() == this
+ && gShowTextEditCursor && !mReadOnly)
+ {
+ const LLWString &text = mWText;
+ const S32 text_len = getLength();
+
+ // Skip through the lines we aren't drawing.
+ S32 cur_pos = mScrollbar->getDocPos();
+
+ S32 num_lines = getLineCount();
+ if (cur_pos >= num_lines)
+ {
+ return;
+ }
+ S32 line_start = getLineStart(cur_pos);
+
+ F32 line_height = mGLFont->getLineHeight();
+ F32 text_y = (F32)(mTextRect.mTop) - line_height;
+
+ F32 cursor_left = 0.f;
+ F32 next_char_left = 0.f;
+ F32 cursor_bottom = 0.f;
+ BOOL cursor_visible = FALSE;
+
+ S32 line_end = 0;
+ // Determine if the cursor is visible and if so what its coordinates are.
+ while( (mTextRect.mBottom <= llround(text_y)) && (cur_pos < num_lines))
+ {
+ line_end = text_len + 1;
+ S32 next_line = -1;
+
+ if ((cur_pos + 1) < num_lines)
+ {
+ next_line = getLineStart(cur_pos + 1);
+ line_end = next_line - 1;
+ }
+
+ const llwchar* line = text.c_str() + line_start;
+
+ // Find the cursor and selection bounds
+ if( line_start <= mCursorPos && mCursorPos <= line_end )
+ {
+ cursor_visible = TRUE;
+ next_char_left = (F32)mTextRect.mLeft + mGLFont->getWidthF32(line, 0, mCursorPos - line_start, mAllowEmbeddedItems );
+ cursor_left = next_char_left - 1.f;
+ cursor_bottom = text_y;
+ break;
+ }
+
+ // move down one line
+ text_y -= line_height;
+ line_start = next_line;
+ cur_pos++;
+ }
+
+ // Draw the cursor
+ if( cursor_visible )
+ {
+ // (Flash the cursor every half second starting a fixed time after the last keystroke)
+ F32 elapsed = mKeystrokeTimer.getElapsedTimeF32();
+ if( (elapsed < CURSOR_FLASH_DELAY ) || (S32(elapsed * 2) & 1) )
+ {
+ F32 cursor_top = cursor_bottom + line_height + 1.f;
+ F32 cursor_right = cursor_left + (F32)CURSOR_THICKNESS;
+ if (LL_KIM_OVERWRITE == gKeyboard->getInsertMode() && !hasSelection())
+ {
+ cursor_left += CURSOR_THICKNESS;
+ const LLWString space(utf8str_to_wstring(LLString(" ")));
+ F32 spacew = mGLFont->getWidthF32(space.c_str());
+ if (mCursorPos == line_end)
+ {
+ cursor_right = cursor_left + spacew;
+ }
+ else
+ {
+ F32 width = mGLFont->getWidthF32(text.c_str(), mCursorPos, 1, mAllowEmbeddedItems);
+ cursor_right = cursor_left + llmax(spacew, width);
+ }
+ }
+
+ LLGLSNoTexture no_texture;
+
+ glColor4fv( mCursorColor.mV );
+
+ gl_rect_2d(llfloor(cursor_left), llfloor(cursor_top),
+ llfloor(cursor_right), llfloor(cursor_bottom));
+
+ if (LL_KIM_OVERWRITE == gKeyboard->getInsertMode() && !hasSelection() && text[mCursorPos] != '\n')
+ {
+ LLTextSegment* segmentp = getSegmentAtOffset(mCursorPos);
+ LLColor4 text_color;
+ if (segmentp)
+ {
+ text_color = segmentp->getColor();
+ }
+ else if (mReadOnly)
+ {
+ text_color = mReadOnlyFgColor;
+ }
+ else
+ {
+ text_color = mFgColor;
+ }
+ LLGLSTexture texture;
+ mGLFont->render(text, mCursorPos, next_char_left, cursor_bottom + line_height,
+ LLColor4(1.f - text_color.mV[VRED], 1.f - text_color.mV[VGREEN], 1.f - text_color.mV[VBLUE], 1.f),
+ LLFontGL::LEFT, LLFontGL::TOP,
+ LLFontGL::NORMAL,
+ 1);
+ }
+
+
+ }
+ }
+ }
+}
+
+
+void LLTextEditor::drawText()
+{
+ const LLWString &text = mWText;
+ const S32 text_len = getLength();
+
+ if( text_len > 0 )
+ {
+ S32 selection_left = -1;
+ S32 selection_right = -1;
+ // Draw selection even if we don't have keyboard focus for search/replace
+ if( hasSelection())
+ {
+ selection_left = llmin( mSelectionStart, mSelectionEnd );
+ selection_right = llmax( mSelectionStart, mSelectionEnd );
+ }
+
+ LLGLSUIDefault gls_ui;
+
+ S32 cur_line = mScrollbar->getDocPos();
+ S32 num_lines = getLineCount();
+ if (cur_line >= num_lines)
+ {
+ return;
+ }
+
+ S32 line_start = getLineStart(cur_line);
+ LLTextSegment t(line_start);
+ segment_list_t::iterator seg_iter;
+ seg_iter = std::upper_bound(mSegments.begin(), mSegments.end(), &t, LLTextSegment::compare());
+ if (seg_iter == mSegments.end() || (*seg_iter)->getStart() > line_start) --seg_iter;
+ LLTextSegment* cur_segment = *seg_iter;
+
+ S32 line_height = llround( mGLFont->getLineHeight() );
+ F32 text_y = (F32)(mTextRect.mTop - line_height);
+ while((mTextRect.mBottom <= text_y) && (cur_line < num_lines))
+ {
+ 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;
+ }
+
+ F32 text_x = (F32)mTextRect.mLeft;
+
+ S32 seg_start = line_start;
+ while( seg_start < line_end )
+ {
+ while( cur_segment->getEnd() <= seg_start )
+ {
+ seg_iter++;
+ if (seg_iter == mSegments.end())
+ {
+ llwarns << "Ran off the segmentation end!" << llendl;
+ return;
+ }
+ cur_segment = *seg_iter;
+ }
+
+ // Draw a segment within the line
+ S32 clipped_end = llmin( line_end, cur_segment->getEnd() );
+ S32 clipped_len = clipped_end - seg_start;
+ if( clipped_len > 0 )
+ {
+ LLStyle style = cur_segment->getStyle();
+ if ( style.isImage() && (cur_segment->getStart() >= seg_start) && (cur_segment->getStart() <= clipped_end))
+ {
+ LLImageGL *image = style.getImage();
+
+ gl_draw_scaled_image( llround(text_x), llround(text_y)+line_height-style.mImageHeight, style.mImageWidth, style.mImageHeight, image, LLColor4::white );
+
+ }
+
+ if (cur_segment == mHoverSegment && style.getIsEmbeddedItem())
+ {
+ style.mUnderline = TRUE;
+ }
+
+ S32 left_pos = llmin( mSelectionStart, mSelectionEnd );
+
+ if ( (mParseHTML) && (left_pos > seg_start) && (left_pos < clipped_end) && mIsSelecting && (mSelectionStart == mSelectionEnd) )
+ {
+ mHTML = style.getLinkHREF();
+ }
+
+ drawClippedSegment( text, seg_start, clipped_end, text_x, text_y, selection_left, selection_right, style, &text_x );
+
+ // Note: text_x is incremented by drawClippedSegment()
+ seg_start += clipped_len;
+ }
+ }
+
+ // move down one line
+ text_y -= (F32)line_height;
+
+ line_start = next_start;
+ cur_line++;
+ }
+ }
+}
+
+// Draws a single text segment, reversing the color for selection if needed.
+void LLTextEditor::drawClippedSegment(const LLWString &text, S32 seg_start, S32 seg_end, F32 x, F32 y, S32 selection_left, S32 selection_right, const LLStyle& style, F32* right_x )
+{
+ const LLFontGL* font = mGLFont;
+
+ LLColor4 color;
+
+ if (!style.isVisible())
+ {
+ return;
+ }
+
+ color = style.getColor();
+
+ if ( style.getFontString()[0] )
+ {
+ font = gResMgr->getRes(style.getFontID());
+ }
+
+ U8 font_flags = LLFontGL::NORMAL;
+
+ if (style.mBold)
+ {
+ font_flags |= LLFontGL::BOLD;
+ }
+ if (style.mItalic)
+ {
+ font_flags |= LLFontGL::ITALIC;
+ }
+ if (style.mUnderline)
+ {
+ font_flags |= LLFontGL::UNDERLINE;
+ }
+
+ if (style.getIsEmbeddedItem())
+ {
+ if (mReadOnly)
+ {
+ color = LLUI::sColorsGroup->getColor("TextEmbeddedItemReadOnlyColor");
+ }
+ else
+ {
+ color = LLUI::sColorsGroup->getColor("TextEmbeddedItemColor");
+ }
+ }
+
+ F32 y_top = y + (F32)llround(font->getLineHeight());
+
+ if( selection_left > seg_start )
+ {
+ // Draw normally
+ S32 start = seg_start;
+ S32 end = llmin( selection_left, seg_end );
+ S32 length = end - start;
+ font->render(text, start, x, y_top, color, LLFontGL::LEFT, LLFontGL::TOP, font_flags, length, S32_MAX, right_x, mAllowEmbeddedItems);
+ }
+ x = *right_x;
+
+ if( (selection_left < seg_end) && (selection_right > seg_start) )
+ {
+ // Draw reversed
+ S32 start = llmax( selection_left, seg_start );
+ S32 end = llmin( selection_right, seg_end );
+ S32 length = end - start;
+
+ font->render(text, start, x, y_top,
+ LLColor4( 1.f - color.mV[0], 1.f - color.mV[1], 1.f - color.mV[2], 1.f ),
+ LLFontGL::LEFT, LLFontGL::TOP, font_flags, length, S32_MAX, right_x, mAllowEmbeddedItems);
+ }
+ x = *right_x;
+ if( selection_right < seg_end )
+ {
+ // Draw normally
+ S32 start = llmax( selection_right, seg_start );
+ S32 end = seg_end;
+ S32 length = end - start;
+ font->render(text, start, x, y_top, color, LLFontGL::LEFT, LLFontGL::TOP, font_flags, length, S32_MAX, right_x, mAllowEmbeddedItems);
+ }
+ }
+
+
+void LLTextEditor::draw()
+{
+ if( getVisible() )
+ {
+ {
+ LLGLEnable scissor_test(GL_SCISSOR_TEST);
+ LLUI::setScissorRegionLocal(LLRect(0, mRect.getHeight(), mRect.getWidth() - (mScrollbar->getVisible() ? SCROLLBAR_SIZE : 0), 0));
+
+ bindEmbeddedChars( mGLFont );
+
+ drawBackground();
+ drawSelectionBackground();
+ drawText();
+ drawCursor();
+
+ unbindEmbeddedChars( mGLFont );
+
+ //RN: the decision was made to always show the orange border for keyboard focus but do not put an insertion caret
+ // when in readonly mode
+ mBorder->setKeyboardFocusHighlight( gFocusMgr.getKeyboardFocus() == this);// && !mReadOnly);
+ }
+ LLView::draw(); // Draw children (scrollbar and border)
+ }
+}
+
+void LLTextEditor::reportBadKeystroke()
+{
+ make_ui_sound("UISndBadKeystroke");
+}
+
+
+void LLTextEditor::onTabInto()
+{
+ // selecting all on tabInto causes users to hit tab twice and replace their text with a tab character
+ // theoretically, one could selectAll if mTabToNextField is true, but we couldn't think of a use case
+ // where you'd want to select all anyway
+ // preserve insertion point when returning to the editor
+ //selectAll();
+}
+
+void LLTextEditor::clear()
+{
+ setText("");
+}
+
+// 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;
+
+ LLUICtrl::setFocus( new_state );
+
+ if( new_state )
+ {
+ // Route menu to this class
+ gEditMenuHandler = this;
+
+ // Don't start the cursor flashing right away
+ mKeystrokeTimer.reset();
+ }
+ else
+ {
+ // Route menu back to the default
+ if( gEditMenuHandler == this )
+ {
+ gEditMenuHandler = NULL;
+ }
+
+ endSelection();
+ }
+}
+
+BOOL LLTextEditor::acceptsTextInput() const
+{
+ return !mReadOnly;
+}
+
+// Given a line (from the start of the doc) and an offset into the line, find the offset (pos) into text.
+S32 LLTextEditor::getPos( S32 line, S32 offset )
+{
+ S32 line_start = getLineStart(line);
+ S32 next_start = getLineStart(line+1);
+ if (next_start == line_start)
+ {
+ next_start = getLength() + 1;
+ }
+ S32 line_length = next_start - line_start - 1;
+ line_length = llmax(line_length, 0);
+ return line_start + llmin( offset, line_length );
+}
+
+
+void LLTextEditor::changePage( S32 delta )
+{
+ S32 line, offset;
+ getLineAndOffset( mCursorPos, &line, &offset );
+
+ // allow one line overlap
+ S32 page_size = mScrollbar->getPageSize() - 1;
+ if( delta == -1 )
+ {
+ line = llmax( line - page_size, 0);
+ setCursorPos(getPos( line, offset ));
+ mScrollbar->setDocPos( mScrollbar->getDocPos() - page_size );
+ }
+ else
+ if( delta == 1 )
+ {
+ setCursorPos(getPos( line + page_size, offset ));
+ mScrollbar->setDocPos( mScrollbar->getDocPos() + page_size );
+ }
+ if (mOnScrollEndCallback && mOnScrollEndData && (mScrollbar->getDocPos() == mScrollbar->getDocPosMax()))
+ {
+ mOnScrollEndCallback(mOnScrollEndData);
+ }
+}
+
+void LLTextEditor::changeLine( S32 delta )
+{
+ bindEmbeddedChars( mGLFont );
+
+ S32 line, offset;
+ getLineAndOffset( mCursorPos, &line, &offset );
+
+ S32 line_start = getLineStart(line);
+
+ S32 desired_x_pixel;
+
+ desired_x_pixel = mGLFont->getWidth(mWText.c_str(), line_start, offset, mAllowEmbeddedItems );
+
+ S32 new_line = 0;
+ if( (delta < 0) && (line > 0 ) )
+ {
+ new_line = line - 1;
+ }
+ else
+ if( (delta > 0) && (line < (getLineCount() - 1)) )
+ {
+ new_line = line + 1;
+ }
+ else
+ {
+ unbindEmbeddedChars( mGLFont );
+ return;
+ }
+
+ S32 num_lines = getLineCount();
+ S32 new_line_start = getLineStart(new_line);
+ S32 new_line_end = getLength();
+ if (new_line + 1 < num_lines)
+ {
+ new_line_end = getLineStart(new_line + 1) - 1;
+ }
+
+ S32 new_line_len = new_line_end - new_line_start;
+
+ S32 new_offset;
+ new_offset = mGLFont->charFromPixelOffset(mWText.c_str(), new_line_start,
+ (F32)desired_x_pixel,
+ (F32)mTextRect.getWidth(),
+ new_line_len,
+ mAllowEmbeddedItems);
+
+ setCursorPos (getPos( new_line, new_offset ));
+ unbindEmbeddedChars( mGLFont );
+}
+
+void LLTextEditor::startOfLine()
+{
+ S32 line, offset;
+ getLineAndOffset( mCursorPos, &line, &offset );
+ setCursorPos(mCursorPos - offset);
+}
+
+
+// public
+void LLTextEditor::setCursorAndScrollToEnd()
+{
+ deselect();
+ endOfDoc();
+ updateScrollFromCursor();
+}
+
+
+void LLTextEditor::getCurrentLineAndColumn( S32* line, S32* col, BOOL include_wordwrap )
+{
+ if( include_wordwrap )
+ {
+ getLineAndOffset( mCursorPos, line, col );
+ }
+ else
+ {
+ const LLWString &text = mWText;
+ S32 line_count = 0;
+ S32 line_start = 0;
+ S32 i;
+ for( i = 0; text[i] && (i < mCursorPos); i++ )
+ {
+ if( '\n' == text[i] )
+ {
+ line_start = i + 1;
+ line_count++;
+ }
+ }
+ *line = line_count;
+ *col = i - line_start;
+ }
+}
+
+
+void LLTextEditor::endOfLine()
+{
+ S32 line, offset;
+ getLineAndOffset( mCursorPos, &line, &offset );
+ S32 num_lines = getLineCount();
+ if (line + 1 >= num_lines)
+ {
+ setCursorPos(getLength());
+ }
+ else
+ {
+ setCursorPos( getLineStart(line + 1) - 1 );
+ }
+}
+
+void LLTextEditor::endOfDoc()
+{
+ mScrollbar->setDocPos( mScrollbar->getDocPosMax() );
+ S32 len = getLength();
+ if( len )
+ {
+ setCursorPos(len);
+ }
+ if (mOnScrollEndCallback && mOnScrollEndData && (mScrollbar->getDocPos() == mScrollbar->getDocPosMax()))
+ {
+ mOnScrollEndCallback(mOnScrollEndData);
+ }
+}
+
+// Sets the scrollbar from the cursor position
+void LLTextEditor::updateScrollFromCursor()
+{
+ mScrollbar->setDocSize( getLineCount() );
+
+ if (mReadOnly)
+ {
+ // no cursor in read only mode
+ return;
+ }
+
+ S32 line, offset;
+ getLineAndOffset( mCursorPos, &line, &offset );
+
+ S32 page_size = mScrollbar->getPageSize();
+
+ if( line < mScrollbar->getDocPos() )
+ {
+ // scroll so that the cursor is at the top of the page
+ mScrollbar->setDocPos( line );
+ }
+ else if( line >= mScrollbar->getDocPos() + page_size - 1 )
+ {
+ S32 new_pos = 0;
+ if( line < mScrollbar->getDocSize() - 1 )
+ {
+ // scroll so that the cursor is one line above the bottom of the page,
+ new_pos = line - page_size + 1;
+ }
+ else
+ {
+ // if there is less than a page of text remaining, scroll so that the cursor is at the bottom
+ new_pos = mScrollbar->getDocPosMax();
+ }
+ mScrollbar->setDocPos( new_pos );
+ }
+
+ // Check if we've scrolled to bottom for callback if asked for callback
+ if (mOnScrollEndCallback && mOnScrollEndData && (mScrollbar->getDocPos() == mScrollbar->getDocPosMax()))
+ {
+ mOnScrollEndCallback(mOnScrollEndData);
+ }
+}
+
+void LLTextEditor::reshape(S32 width, S32 height, BOOL called_from_parent)
+{
+ LLView::reshape( width, height, called_from_parent );
+
+ updateTextRect();
+
+ S32 line_height = llround( mGLFont->getLineHeight() );
+ S32 page_lines = mTextRect.getHeight() / line_height;
+ mScrollbar->setPageSize( page_lines );
+
+ updateLineStartList();
+}
+
+void LLTextEditor::autoIndent()
+{
+ // Count the number of spaces in the current line
+ S32 line, offset;
+ getLineAndOffset( mCursorPos, &line, &offset );
+ S32 line_start = getLineStart(line);
+ S32 space_count = 0;
+ S32 i;
+
+ const LLWString &text = mWText;
+ while( ' ' == text[line_start] )
+ {
+ space_count++;
+ line_start++;
+ }
+
+ // If we're starting a braced section, indent one level.
+ if( (mCursorPos > 0) && (text[mCursorPos -1] == '{') )
+ {
+ space_count += SPACES_PER_TAB;
+ }
+
+ // Insert that number of spaces on the new line
+ addChar( '\n' );
+ for( i = 0; i < space_count; i++ )
+ {
+ addChar( ' ' );
+ }
+}
+
+// Inserts new text at the cursor position
+void LLTextEditor::insertText(const LLString &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 ));
+
+ updateLineStartList();
+ updateScrollFromCursor();
+
+ setEnabled( enabled );
+}
+
+
+void LLTextEditor::appendColoredText(const LLString &new_text,
+ bool allow_undo,
+ bool prepend_newline,
+ const LLColor4 &color,
+ const LLString& font_name)
+{
+ LLStyle style;
+ style.setVisible(true);
+ style.setColor(color);
+ style.setFontName(font_name);
+ if(mParseHTML)
+ {
+
+ S32 start=0,end=0;
+ LLString text = new_text;
+ while ( findHTML(text, &start, &end) )
+ {
+ LLStyle html;
+ html.setVisible(true);
+ html.setColor(mLinkColor);
+ html.setFontName(font_name);
+ html.mUnderline = TRUE;
+
+ if (start > 0) appendText(text.substr(0,start),allow_undo, prepend_newline, &style);
+ html.setLinkHREF(text.substr(start,end-start));
+ appendText(text.substr(start, end-start),allow_undo, prepend_newline, &html);
+ if (end < (S32)text.length())
+ {
+ text = text.substr(end,text.length() - end);
+ end=0;
+ }
+ else
+ {
+ break;
+ }
+ }
+ if (end < (S32)text.length()) appendText(text,allow_undo, prepend_newline, &style);
+ }
+ else
+ {
+ appendText(new_text, allow_undo, prepend_newline, &style);
+ }
+}
+
+void LLTextEditor::appendStyledText(const LLString &new_text,
+ bool allow_undo,
+ bool prepend_newline,
+ const LLStyle &style)
+{
+ appendText(new_text, allow_undo, prepend_newline, &style);
+}
+
+// Appends new text to end of document
+void LLTextEditor::appendText(const LLString &new_text, bool allow_undo, bool prepend_newline,
+ const LLStyle* segment_style)
+{
+ // Save old state
+ BOOL was_scrolled_to_bottom = (mScrollbar->getDocPos() == mScrollbar->getDocPosMax());
+ S32 selection_start = mSelectionStart;
+ S32 selection_end = mSelectionEnd;
+ S32 cursor_pos = mCursorPos;
+ S32 old_length = getLength();
+ BOOL cursor_was_at_end = (mCursorPos == old_length);
+
+ deselect();
+
+ setCursorPos(old_length);
+
+ // Add carriage return if not first line
+ if (getLength() != 0
+ && prepend_newline)
+ {
+ LLString final_text = "\n";
+ final_text += new_text;
+ append(utf8str_to_wstring(final_text), TRUE);
+ }
+ else
+ {
+ append(utf8str_to_wstring(new_text), TRUE );
+ }
+
+ if (segment_style)
+ {
+ S32 segment_start = old_length;
+ S32 segment_end = getLength();
+ LLTextSegment* segment = new LLTextSegment(*segment_style, segment_start, segment_end );
+ mSegments.push_back(segment);
+ }
+
+ updateLineStartList(old_length);
+
+ // Set the cursor and scroll position
+ // Maintain the scroll position unless the scroll was at the end of the doc
+ // (in which case, move it to the new end of the doc)
+ if( was_scrolled_to_bottom )
+ {
+ endOfDoc();
+ }
+ else if( selection_start != selection_end )
+ {
+ mSelectionStart = selection_start;
+
+ mSelectionEnd = selection_end;
+ 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();
+ mCursorPos = llclamp(mCursorPos, 0, len);
+ mSelectionStart = llclamp(mSelectionStart, 0, len);
+ mSelectionEnd = llclamp(mSelectionEnd, 0, len);
+
+ pruneSegments();
+ updateLineStartList();
+}
+
+///////////////////////////////////////////////////////////////////
+// Returns change in number of characters in mWText
+
+S32 LLTextEditor::insertStringNoUndo(const S32 pos, const LLWString &wstr)
+{
+ S32 len = mWText.length();
+ S32 s_len = wstr.length();
+ S32 new_len = len + s_len;
+ if( new_len > mMaxTextLength )
+ {
+ new_len = mMaxTextLength;
+
+ // The user's not getting everything he's hoping for
+ make_ui_sound("UISndBadKeystroke");
+ }
+
+ mWText.insert(pos, wstr);
+ mTextIsUpToDate = FALSE;
+ truncate();
+
+ return new_len - len;
+}
+
+S32 LLTextEditor::removeStringNoUndo(S32 pos, S32 length)
+{
+ mWText.erase(pos, length);
+ mTextIsUpToDate = FALSE;
+ return -length; // This will be wrong if someone calls removeStringNoUndo with an excessive length
+}
+
+S32 LLTextEditor::overwriteCharNoUndo(S32 pos, llwchar wc)
+{
+ if (pos > (S32)mWText.length())
+ {
+ return 0;
+ }
+ mWText[pos] = wc;
+ mTextIsUpToDate = FALSE;
+ return 1;
+}
+
+//----------------------------------------------------------------------------
+
+void LLTextEditor::makePristine()
+{
+ mPristineCmd = mLastCmd;
+ mBaseDocIsPristine = !mLastCmd;
+
+ // Create a clean partition in the undo stack. We don't want a single command to extend from
+ // the "pre-pristine" state to the "post-pristine" state.
+ if( mLastCmd )
+ {
+ mLastCmd->blockExtensions();
+ }
+}
+
+BOOL LLTextEditor::isPristine() const
+{
+ if( mPristineCmd )
+ {
+ return (mPristineCmd == mLastCmd);
+ }
+ else
+ {
+ // No undo stack, so check if the version before and commands were done was the original version
+ return !mLastCmd && mBaseDocIsPristine;
+ }
+}
+
+BOOL LLTextEditor::tryToRevertToPristineState()
+{
+ if( !isPristine() )
+ {
+ deselect();
+ S32 i = 0;
+ while( !isPristine() && canUndo() )
+ {
+ undo();
+ i--;
+ }
+
+ while( !isPristine() && canRedo() )
+ {
+ redo();
+ i++;
+ }
+
+ if( !isPristine() )
+ {
+ // failed, so go back to where we started
+ while( i > 0 )
+ {
+ undo();
+ i--;
+ }
+ }
+
+ updateLineStartList();
+ updateScrollFromCursor();
+ }
+
+ return isPristine(); // TRUE => success
+}
+
+
+
+void LLTextEditor::updateTextRect()
+{
+ mTextRect.setOriginAndSize(
+ UI_TEXTEDITOR_BORDER + UI_TEXTEDITOR_H_PAD,
+ UI_TEXTEDITOR_BORDER,
+ mRect.getWidth() - SCROLLBAR_SIZE - 2 * (UI_TEXTEDITOR_BORDER + UI_TEXTEDITOR_H_PAD),
+ mRect.getHeight() - 2 * UI_TEXTEDITOR_BORDER - UI_TEXTEDITOR_V_PAD_TOP );
+}
+
+void LLTextEditor::loadKeywords(const LLString& filename,
+ const LLDynamicArray<const char*>& funcs,
+ const LLDynamicArray<const char*>& tooltips,
+ const LLColor3& color)
+{
+ if(mKeywords.loadFromFile(filename))
+ {
+ S32 count = funcs.count();
+ LLString name;
+ for(S32 i = 0; i < count; i++)
+ {
+ name = funcs.get(i);
+ name = utf8str_trim(name);
+ mKeywords.addToken(LLKeywordToken::WORD, name.c_str(), color, tooltips.get(i) );
+ }
+
+ mKeywords.findSegments( &mSegments, mWText );
+
+ llassert( mSegments.front()->getStart() == 0 );
+ llassert( mSegments.back()->getEnd() == getLength() );
+ }
+}
+
+void LLTextEditor::updateSegments()
+{
+ if (mKeywords.isLoaded())
+ {
+ // HACK: No non-ascii keywords for now
+ mKeywords.findSegments(&mSegments, mWText);
+ }
+ else if (mAllowEmbeddedItems)
+ {
+ findEmbeddedItemSegments();
+ }
+ // Make sure we have at least one segment
+ if (mSegments.size() == 1 && mSegments[0]->getIsDefault())
+ {
+ delete mSegments[0];
+ mSegments.clear(); // create default segment
+ }
+ if (mSegments.empty())
+ {
+ LLColor4& text_color = ( mReadOnly ? mReadOnlyFgColor : mFgColor );
+ LLTextSegment* default_segment = new LLTextSegment( text_color, 0, mWText.length() );
+ default_segment->setIsDefault(TRUE);
+ mSegments.push_back(default_segment);
+ }
+}
+
+// Only effective if text was removed from the end of the editor
+void LLTextEditor::pruneSegments()
+{
+ S32 len = mWText.length();
+ // Find and update the first valid segment
+ segment_list_t::iterator iter = mSegments.end();
+ while(iter != mSegments.begin())
+ {
+ --iter;
+ LLTextSegment* seg = *iter;
+ if (seg->getStart() < len)
+ {
+ // valid segment
+ if (seg->getEnd() > len)
+ {
+ seg->setEnd(len);
+ }
+ break; // done
+ }
+ }
+ // erase invalid segments
+ ++iter;
+ std::for_each(iter, mSegments.end(), DeletePointer());
+ mSegments.erase(iter, mSegments.end());
+}
+
+void LLTextEditor::findEmbeddedItemSegments()
+{
+ mHoverSegment = NULL;
+ std::for_each(mSegments.begin(), mSegments.end(), DeletePointer());
+ mSegments.clear();
+
+ BOOL found_embedded_items = FALSE;
+ const LLWString &text = mWText;
+ S32 idx = 0;
+ while( text[idx] )
+ {
+ if( text[idx] >= FIRST_EMBEDDED_CHAR && text[idx] <= LAST_EMBEDDED_CHAR )
+ {
+ found_embedded_items = TRUE;
+ break;
+ }
+ ++idx;
+ }
+
+ if( !found_embedded_items )
+ {
+ return;
+ }
+
+ S32 text_len = text.length();
+
+ BOOL in_text = FALSE;
+
+ LLColor4& text_color = ( mReadOnly ? mReadOnlyFgColor : mFgColor );
+
+ if( idx > 0 )
+ {
+ mSegments.push_back( new LLTextSegment( text_color, 0, text_len ) ); // text
+ in_text = TRUE;
+ }
+
+ LLStyle embedded_style;
+ embedded_style.setIsEmbeddedItem( TRUE );
+
+ // Start with i just after the first embedded item
+ while ( text[idx] )
+ {
+ if( text[idx] >= FIRST_EMBEDDED_CHAR && text[idx] <= LAST_EMBEDDED_CHAR )
+ {
+ if( in_text )
+ {
+ mSegments.back()->setEnd( idx );
+ }
+ mSegments.push_back( new LLTextSegment( embedded_style, idx, idx + 1 ) ); // item
+ in_text = FALSE;
+ }
+ else
+ if( !in_text )
+ {
+ mSegments.push_back( new LLTextSegment( text_color, idx, text_len ) ); // text
+ in_text = TRUE;
+ }
+ ++idx;
+ }
+}
+
+BOOL LLTextEditor::handleMouseUpOverSegment(S32 x, S32 y, MASK mask)
+{
+ return FALSE;
+}
+
+llwchar LLTextEditor::pasteEmbeddedItem(llwchar ext_char)
+{
+ return ext_char;
+}
+
+void LLTextEditor::bindEmbeddedChars(const LLFontGL* font)
+{
+}
+
+void LLTextEditor::unbindEmbeddedChars(const LLFontGL* font)
+{
+}
+
+// Finds the text segment (if any) at the give local screen position
+LLTextSegment* LLTextEditor::getSegmentAtLocalPos( S32 x, S32 y )
+{
+ // Find the cursor position at the requested local screen position
+ S32 offset = getCursorPosFromLocalCoord( x, y, FALSE );
+ S32 idx = getSegmentIdxAtOffset(offset);
+ return idx >= 0 ? mSegments[idx] : NULL;
+}
+
+LLTextSegment* LLTextEditor::getSegmentAtOffset(S32 offset)
+{
+ S32 idx = getSegmentIdxAtOffset(offset);
+ return idx >= 0 ? mSegments[idx] : NULL;
+}
+
+S32 LLTextEditor::getSegmentIdxAtOffset(S32 offset)
+{
+ if (mSegments.empty() || offset < 0 || offset >= getLength())
+ {
+ return -1;
+ }
+ else
+ {
+ S32 segidx, segoff;
+ getSegmentAndOffset(offset, &segidx, &segoff);
+ return segidx;
+ }
+}
+
+//static
+void LLTextEditor::onMouseCaptureLost( LLMouseHandler* old_captor )
+{
+ LLTextEditor* self = (LLTextEditor*) old_captor;
+ self->endSelection();
+}
+
+void LLTextEditor::setOnScrollEndCallback(void (*callback)(void*), void* userdata)
+{
+ mOnScrollEndCallback = callback;
+ mOnScrollEndData = userdata;
+ mScrollbar->setOnScrollEndCallback(callback, userdata);
+}
+
+///////////////////////////////////////////////////////////////////
+// Hack for Notecards
+
+BOOL LLTextEditor::importBuffer(const LLString& buffer )
+{
+ 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];
+
+ S32 version = 0;
+ instream.getline(tbuf, MAX_STRING);
+ if( 1 != sscanf(tbuf, "Linden text version %d", &version) )
+ {
+ llwarns << "Invalid Linden text file header " << llendl;
+ return FALSE;
+ }
+
+ if( 1 != version )
+ {
+ llwarns << "Invalid Linden text file version: " << version << llendl;
+ return FALSE;
+ }
+
+ instream.getline(tbuf, MAX_STRING);
+ if( 0 != sscanf(tbuf, "{") )
+ {
+ llwarns << "Invalid Linden text file format" << llendl;
+ return FALSE;
+ }
+
+ S32 text_len = 0;
+ instream.getline(tbuf, MAX_STRING);
+ if( 1 != sscanf(tbuf, "Text length %d", &text_len) )
+ {
+ llwarns << "Invalid Linden text length field" << llendl;
+ return FALSE;
+ }
+
+ if( text_len > mMaxTextLength )
+ {
+ llwarns << "Invalid Linden text length: " << text_len << llendl;
+ return FALSE;
+ }
+
+ BOOL success = TRUE;
+
+ char* text = new char[ text_len + 1];
+ instream.get(text, text_len + 1, '\0');
+ text[text_len] = '\0';
+ if( text_len != (S32)strlen(text) )
+ {
+ llwarns << llformat("Invalid text length: %d != %d ",strlen(text),text_len) << llendl;
+ success = FALSE;
+ }
+
+ instream.getline(tbuf, MAX_STRING);
+ if( success && (0 != sscanf(tbuf, "}")) )
+ {
+ llwarns << "Invalid Linden text file format: missing terminal }" << llendl;
+ success = FALSE;
+ }
+
+ if( success )
+ {
+ // Actually set the text
+ setText( text );
+ }
+
+ delete[] text;
+
+ setCursorPos(0);
+ deselect();
+
+ updateLineStartList();
+ updateScrollFromCursor();
+
+ return success;
+}
+
+BOOL LLTextEditor::exportBuffer(LLString &buffer )
+{
+ std::ostringstream outstream(buffer);
+
+ outstream << "Linden text version 1\n";
+ outstream << "{\n";
+
+ outstream << llformat("Text length %d\n", mWText.length() );
+ outstream << getText();
+ outstream << "}\n";
+
+ return TRUE;
+}
+
+//////////////////////////////////////////////////////////////////////////
+// LLTextSegment
+
+LLTextSegment::LLTextSegment(S32 start) : mStart(start)
+{
+}
+LLTextSegment::LLTextSegment( const LLStyle& style, S32 start, S32 end ) :
+ mStyle( style ),
+ mStart( start),
+ mEnd( end ),
+ mToken(NULL),
+ mIsDefault(FALSE)
+{
+}
+LLTextSegment::LLTextSegment(
+ const LLColor4& color, S32 start, S32 end, BOOL is_visible) :
+ mStyle( is_visible, color,"" ),
+ mStart( start),
+ mEnd( end ),
+ mToken(NULL),
+ mIsDefault(FALSE)
+{
+}
+LLTextSegment::LLTextSegment( const LLColor4& color, S32 start, S32 end ) :
+ mStyle( TRUE, color,"" ),
+ mStart( start),
+ mEnd( end ),
+ mToken(NULL),
+ mIsDefault(FALSE)
+{
+}
+LLTextSegment::LLTextSegment( const LLColor3& color, S32 start, S32 end ) :
+ mStyle( TRUE, color,"" ),
+ mStart( start),
+ mEnd( end ),
+ mToken(NULL),
+ mIsDefault(FALSE)
+{
+}
+
+BOOL LLTextSegment::getToolTip(LLString& msg)
+{
+ if (mToken && !mToken->getToolTip().empty())
+ {
+ const LLWString& wmsg = mToken->getToolTip();
+ msg = wstring_to_utf8str(wmsg);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+
+
+void LLTextSegment::dump()
+{
+ llinfos << "Segment [" <<
+// mColor.mV[VX] << ", " <<
+// mColor.mV[VY] << ", " <<
+// mColor.mV[VZ] << "]\t[" <<
+ mStart << ", " <<
+ getEnd() << "]" <<
+ llendl;
+
+}
+
+// virtual
+LLXMLNodePtr LLTextEditor::getXML(bool save_children) const
+{
+ LLXMLNodePtr node = LLUICtrl::getXML();
+
+ // Attributes
+
+ node->createChild("max_length", TRUE)->setIntValue(getMaxLength());
+
+ node->createChild("embedded_items", TRUE)->setBoolValue(mAllowEmbeddedItems);
+
+ node->createChild("font", TRUE)->setStringValue(LLFontGL::nameFromFont(mGLFont));
+
+ node->createChild("word_wrap", TRUE)->setBoolValue(mWordWrap);
+
+ addColorXML(node, mCursorColor, "cursor_color", "TextCursorColor");
+ addColorXML(node, mFgColor, "text_color", "TextFgColor");
+ addColorXML(node, mReadOnlyFgColor, "text_readonly_color", "TextFgReadOnlyColor");
+ addColorXML(node, mReadOnlyBgColor, "bg_readonly_color", "TextBgReadOnlyColor");
+ addColorXML(node, mWriteableBgColor, "bg_writeable_color", "TextBgWriteableColor");
+ addColorXML(node, mFocusBgColor, "bg_focus_color", "TextBgFocusColor");
+
+ // Contents
+ node->setStringValue(getText());
+
+ return node;
+}
+
+// static
+LLView* LLTextEditor::fromXML(LLXMLNodePtr node, LLView *parent, LLUICtrlFactory *factory)
+{
+ LLString name("text_editor");
+ node->getAttributeString("name", name);
+
+ LLRect rect;
+ createRect(node, rect, parent, LLRect());
+
+ U32 max_text_length = 255;
+ node->getAttributeU32("max_length", max_text_length);
+
+ BOOL allow_embedded_items;
+ node->getAttributeBOOL("embedded_items", allow_embedded_items);
+
+ LLFontGL* font = LLView::selectFont(node);
+
+ LLString text = node->getTextContents().substr(0, max_text_length - 1);
+
+ LLTextEditor* text_editor = new LLTextEditor(name,
+ rect,
+ max_text_length,
+ text,
+ font,
+ allow_embedded_items);
+
+ text_editor->setTextEditorParameters(node);
+
+ BOOL hide_scrollbar = FALSE;
+ node->getAttributeBOOL("hide_scrollbar",hide_scrollbar);
+ text_editor->setHideScrollbarForShortDocs(hide_scrollbar);
+
+ text_editor->initFromXML(node, parent);
+
+ return text_editor;
+}
+
+void LLTextEditor::setTextEditorParameters(LLXMLNodePtr node)
+{
+ BOOL word_wrap = FALSE;
+ node->getAttributeBOOL("word_wrap", word_wrap);
+ setWordWrap(word_wrap);
+
+ LLColor4 color;
+ if (LLUICtrlFactory::getAttributeColor(node,"cursor_color", color))
+ {
+ setCursorColor(color);
+ }
+ if(LLUICtrlFactory::getAttributeColor(node,"text_color", color))
+ {
+ setFgColor(color);
+ }
+ if(LLUICtrlFactory::getAttributeColor(node,"text_readonly_color", color))
+ {
+ setReadOnlyFgColor(color);
+ }
+ if(LLUICtrlFactory::getAttributeColor(node,"bg_readonly_color", color))
+ {
+ setReadOnlyBgColor(color);
+ }
+ if(LLUICtrlFactory::getAttributeColor(node,"bg_writeable_color", color))
+ {
+ setWriteableBgColor(color);
+ }
+}
+
+///////////////////////////////////////////////////////////////////
+S32 LLTextEditor::findHTMLToken(const LLString &line, S32 pos, BOOL reverse)
+{
+ LLString openers=" \t('\"[{<>";
+ LLString closers=" \t)'\"]}><;";
+
+ S32 m2;
+ S32 retval;
+
+ if (reverse)
+ {
+
+ for (retval=pos; retval>0; retval--)
+ {
+ m2 = openers.find(line.substr(retval,1));
+ if (m2 >= 0)
+ {
+ retval++;
+ break;
+ }
+ }
+ }
+ else
+ {
+
+ for (retval=pos; retval<(S32)line.length(); retval++)
+ {
+ m2 = closers.find(line.substr(retval,1));
+ if (m2 >= 0)
+ {
+ break;
+ }
+ }
+ }
+
+ return retval;
+}
+
+BOOL LLTextEditor::findHTML(const LLString &line, S32 *begin, S32 *end)
+{
+
+ S32 m1,m2,m3;
+ BOOL matched = FALSE;
+
+ m1=line.find("://",*end);
+
+ if (m1 >= 0) //Easy match.
+ {
+ *begin = findHTMLToken(line, m1, TRUE);
+ *end = findHTMLToken(line, m1, FALSE);
+
+ //Load_url only handles http and https so don't hilite ftp, smb, etc.
+ m2 = line.substr(*begin,(m1 - *begin)).find("http");
+ m3 = line.substr(*begin,(m1 - *begin)).find("secondlife");
+
+ LLString badneighbors=".,<>/?';\"][}{=-+_)(*&^%$#@!~`\t\r\n\\";
+
+ if (m2 >= 0 || m3>=0)
+ {
+ S32 bn = badneighbors.find(line.substr(m1+3,1));
+
+ if (bn < 0)
+ {
+ matched = TRUE;
+ }
+ }
+ }
+/* matches things like secondlife.com (no http://) needs a whitelist to really be effective.
+ else //Harder match.
+ {
+ m1 = line.find(".",*end);
+
+ if (m1 >= 0)
+ {
+ *end = findHTMLToken(line, m1, FALSE);
+ *begin = findHTMLToken(line, m1, TRUE);
+
+ m1 = line.rfind(".",*end);
+
+ if ( ( *end - m1 ) > 2 && m1 > *begin)
+ {
+ LLString badneighbors=".,<>/?';\"][}{=-+_)(*&^%$#@!~`";
+ m2 = badneighbors.find(line.substr(m1+1,1));
+ m3 = badneighbors.find(line.substr(m1-1,1));
+ if (m3<0 && m2<0)
+ {
+ matched = TRUE;
+ }
+ }
+ }
+ }
+ */
+
+ if (matched)
+ {
+ S32 strpos, strpos2;
+
+ LLString url = line.substr(*begin,*end - *begin);
+ LLString slurlID = "slurl.com/secondlife/";
+ strpos = url.find(slurlID);
+
+ if (strpos < 0)
+ {
+ slurlID="secondlife://";
+ strpos = url.find(slurlID);
+ }
+
+ if (strpos >= 0)
+ {
+ strpos+=slurlID.length();
+
+ while ( ( strpos2=url.find("/",strpos) ) == -1 )
+ {
+ if ((*end+2) >= (S32)line.length() || line.substr(*end,1) != " " )
+ {
+ matched=FALSE;
+ break;
+ }
+
+ strpos = (*end + 1) - *begin;
+
+ *end = findHTMLToken(line,(*begin + strpos),FALSE);
+ url = line.substr(*begin,*end - *begin);
+ }
+ }
+
+ }
+
+ if (!matched)
+ {
+ *begin=*end=0;
+ }
+ return matched;
+}