/** * @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 // // 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 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& funcs, const LLDynamicArray& 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 // // Text length \n // (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; }