diff options
author | James Cook <james@lindenlab.com> | 2007-01-02 08:33:20 +0000 |
---|---|---|
committer | James Cook <james@lindenlab.com> | 2007-01-02 08:33:20 +0000 |
commit | 420b91db29485df39fd6e724e782c449158811cb (patch) | |
tree | b471a94563af914d3ed3edd3e856d21cb1b69945 /indra/llui/lltexteditor.cpp |
Print done when done.
Diffstat (limited to 'indra/llui/lltexteditor.cpp')
-rw-r--r-- | indra/llui/lltexteditor.cpp | 4144 |
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; +} |