/** * @file lllineeditor.cpp * @brief LLLineEditor base class * * $LicenseInfo:firstyear=2001&license=viewerlgpl$ * Second Life Viewer Source Code * Copyright (C) 2010, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License only. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA * $/LicenseInfo$ */ // Text editor widget to let users enter a single line. #include "linden_common.h" #define LLLINEEDITOR_CPP #include "lllineeditor.h" #include "lltexteditor.h" #include "llmath.h" #include "llfontgl.h" #include "llgl.h" #include "lltimer.h" #include "llcalc.h" //#include "llclipboard.h" #include "llcontrol.h" #include "llbutton.h" #include "llfocusmgr.h" #include "llkeyboard.h" #include "llrect.h" #include "llresmgr.h" #include "llspellcheck.h" #include "llstring.h" #include "llwindow.h" #include "llui.h" #include "lluictrlfactory.h" #include "llclipboard.h" #include "llmenugl.h" // // Imported globals // // // Constants // const F32 CURSOR_FLASH_DELAY = 1.0f; // in seconds const S32 SCROLL_INCREMENT_ADD = 0; // make space for typing const S32 SCROLL_INCREMENT_DEL = 4; // make space for baskspacing const F32 AUTO_SCROLL_TIME = 0.05f; const F32 TRIPLE_CLICK_INTERVAL = 0.3f; // delay between double and triple click. *TODO: make this equal to the double click interval? const F32 SPELLCHECK_DELAY = 0.5f; // delay between the last keypress and spell checking the word the cursor is on const std::string PASSWORD_ASTERISK( "\xE2\x80\xA2" ); // U+2022 BULLET static LLDefaultChildRegistry::Register r1("line_editor"); // Compiler optimization, generate extern template template class LLLineEditor* LLView::getChild( std::string_view name, bool recurse) const; // // Member functions // LLLineEditor::Params::Params() : max_length(""), keystroke_callback("keystroke_callback"), prevalidator("prevalidator"), input_prevalidator("input_prevalidator"), background_image("background_image"), background_image_disabled("background_image_disabled"), background_image_focused("background_image_focused"), bg_image_always_focused("bg_image_always_focused", false), show_label_focused("show_label_focused", false), select_on_focus("select_on_focus", false), revert_on_esc("revert_on_esc", true), spellcheck("spellcheck", false), commit_on_focus_lost("commit_on_focus_lost", true), ignore_tab("ignore_tab", true), is_password("is_password", false), allow_emoji("allow_emoji", true), cursor_color("cursor_color"), use_bg_color("use_bg_color", false), bg_color("bg_color"), text_color("text_color"), text_readonly_color("text_readonly_color"), text_tentative_color("text_tentative_color"), highlight_color("highlight_color"), preedit_bg_color("preedit_bg_color"), border(""), bg_visible("bg_visible"), text_pad_left("text_pad_left"), text_pad_right("text_pad_right"), default_text("default_text") { changeDefault(mouse_opaque, true); addSynonym(prevalidator, "prevalidate_callback"); addSynonym(input_prevalidator, "prevalidate_input_callback"); addSynonym(select_on_focus, "select_all_on_focus_received"); addSynonym(border, "border"); addSynonym(label, "watermark_text"); addSynonym(max_length.chars, "max_length"); } LLLineEditor::LLLineEditor(const LLLineEditor::Params& p) : LLUICtrl(p), mMaxLengthBytes(p.max_length.bytes), mMaxLengthChars(p.max_length.chars), mCursorPos( 0 ), mScrollHPos( 0 ), mTextPadLeft(p.text_pad_left), mTextPadRight(p.text_pad_right), mTextLeftEdge(0), // computed in updateTextPadding() below mTextRightEdge(0), // computed in updateTextPadding() below mCommitOnFocusLost( p.commit_on_focus_lost ), mKeystrokeOnEsc(false), mRevertOnEsc( p.revert_on_esc ), mKeystrokeCallback( p.keystroke_callback() ), mIsSelecting( false ), mSelectionStart( 0 ), mSelectionEnd( 0 ), mLastSelectionX(-1), mLastSelectionY(-1), mLastSelectionStart(-1), mLastSelectionEnd(-1), mBorderThickness( 0 ), mIgnoreArrowKeys( false ), mIgnoreTab( p.ignore_tab ), mDrawAsterixes( p.is_password ), mAllowEmoji( p.allow_emoji ), mSpellCheck( p.spellcheck ), mSpellCheckStart(-1), mSpellCheckEnd(-1), mSelectAllonFocusReceived( p.select_on_focus ), mSelectAllonCommit( true ), mPassDelete(false), mReadOnly(false), mBgImage( p.background_image ), mBgImageDisabled( p.background_image_disabled ), mBgImageFocused( p.background_image_focused ), mShowImageFocused( p.bg_image_always_focused ), mShowLabelFocused( p.show_label_focused ), mUseBgColor(p.use_bg_color), mHaveHistory(false), mReplaceNewlinesWithSpaces( true ), mPrevalidator(p.prevalidator()), mInputPrevalidator(p.input_prevalidator()), mLabel(p.label), mCursorColor(p.cursor_color()), mBgColor(p.bg_color()), mFgColor(p.text_color()), mReadOnlyFgColor(p.text_readonly_color()), mTentativeFgColor(p.text_tentative_color()), mHighlightColor(p.highlight_color()), mPreeditBgColor(p.preedit_bg_color()), mGLFont(p.font), mContextMenuHandle(), mShowContextMenu(true) { llassert( mMaxLengthBytes > 0 ); LLUICtrl::setEnabled(true); setEnabled(p.enabled); mScrollTimer.reset(); mTripleClickTimer.reset(); setText(p.default_text()); if (p.initial_value.isProvided() && !p.control_name.isProvided()) { // Initial value often is descriptive, like "Type some ID here" // and can be longer than size limitation, ignore size setText(p.initial_value.getValue().asString(), false); } // Initialize current history line iterator mCurrentHistoryLine = mLineHistory.begin(); LLRect border_rect(getLocalRect()); // adjust for gl line drawing glitch border_rect.mTop -= 1; border_rect.mRight -=1; LLViewBorder::Params border_p(p.border); border_p.rect = border_rect; border_p.follows.flags = FOLLOWS_ALL; border_p.bevel_style = LLViewBorder::BEVEL_IN; mBorder = LLUICtrlFactory::create(border_p); addChild( mBorder ); // clamp text padding to current editor size updateTextPadding(); setCursor(mText.length()); if (mSpellCheck) { LLSpellChecker::setSettingsChangeCallback(boost::bind(&LLLineEditor::onSpellCheckSettingsChange, this)); } mSpellCheckTimer.reset(); updateAllowingLanguageInput(); } LLLineEditor::~LLLineEditor() { mCommitOnFocusLost = false; // Make sure no context menu linger around once the widget is deleted LLContextMenu* menu = static_cast(mContextMenuHandle.get()); if (menu) { menu->hide(); } setContextMenu(NULL); // calls onCommit() while LLLineEditor still valid gFocusMgr.releaseFocusIfNeeded( this ); } void LLLineEditor::initFromParams(const LLLineEditor::Params& params) { LLUICtrl::initFromParams(params); LLUICtrl::setEnabled(true); setEnabled(params.enabled); } void LLLineEditor::onFocusReceived() { gEditMenuHandler = this; LLUICtrl::onFocusReceived(); updateAllowingLanguageInput(); } void LLLineEditor::onFocusLost() { // The call to updateAllowLanguageInput() // when loosing the keyboard focus *may* // indirectly invoke handleUnicodeCharHere(), // so it must be called before onCommit. updateAllowingLanguageInput(); if( mCommitOnFocusLost && mText.getString() != mPrevText) { onCommit(); } if( gEditMenuHandler == this ) { gEditMenuHandler = NULL; } getWindow()->showCursorFromMouseMove(); LLUICtrl::onFocusLost(); } // virtual void LLLineEditor::onCommit() { // put current line into the line history updateHistory(); setControlValue(getValue()); LLUICtrl::onCommit(); resetDirty(); // Selection on commit needs to be turned off when evaluating maths // expressions, to allow indication of the error position if (mSelectAllonCommit) selectAll(); } // Returns true if user changed value at all // virtual bool LLLineEditor::isDirty() const { return mText.getString() != mPrevText; } // Clear dirty state // virtual void LLLineEditor::resetDirty() { mPrevText = mText.getString(); } // assumes UTF8 text // virtual void LLLineEditor::setValue(const LLSD& value ) { setText(value.asString()); } //virtual LLSD LLLineEditor::getValue() const { return LLSD(getText()); } // line history support void LLLineEditor::updateHistory() { // On history enabled line editors, remember committed line and // reset current history line number. // Be sure only to remember lines that are not empty and that are // different from the last on the list. if( mHaveHistory && getLength() ) { if( !mLineHistory.empty() ) { // When not empty, last line of history should always be blank. if( mLineHistory.back().empty() ) { // discard the empty line mLineHistory.pop_back(); } else { LL_WARNS("") << "Last line of history was not blank." << LL_ENDL; } } // Add text to history, ignoring duplicates if( mLineHistory.empty() || getText() != mLineHistory.back() ) { mLineHistory.push_back( getText() ); } // Restore the blank line and set mCurrentHistoryLine to point at it mLineHistory.push_back( "" ); mCurrentHistoryLine = mLineHistory.end() - 1; } } void LLLineEditor::reshape(S32 width, S32 height, bool called_from_parent) { LLUICtrl::reshape(width, height, called_from_parent); updateTextPadding(); // For clamping side-effect. setCursor(mCursorPos); // For clamping side-effect. } void LLLineEditor::setEnabled(bool enabled) { mReadOnly = !enabled; setTabStop(!mReadOnly); updateAllowingLanguageInput(); } void LLLineEditor::setMaxTextLength(S32 max_text_length) { S32 max_len = llmax(0, max_text_length); mMaxLengthBytes = max_len; } void LLLineEditor::setMaxTextChars(S32 max_text_chars) { S32 max_chars = llmax(0, max_text_chars); mMaxLengthChars = max_chars; } void LLLineEditor::getTextPadding(S32 *left, S32 *right) { *left = mTextPadLeft; *right = mTextPadRight; } void LLLineEditor::setTextPadding(S32 left, S32 right) { mTextPadLeft = left; mTextPadRight = right; updateTextPadding(); } void LLLineEditor::updateTextPadding() { mTextLeftEdge = llclamp(mTextPadLeft, 0, getRect().getWidth()); mTextRightEdge = getRect().getWidth() - llclamp(mTextPadRight, 0, getRect().getWidth()); } void LLLineEditor::setText(const LLStringExplicit &new_text) { setText(new_text, true); } void LLLineEditor::setText(const LLStringExplicit &new_text, bool use_size_limit) { // If new text is identical, don't copy and don't move insertion point if (mText.getString() == new_text) { return; } // Check to see if entire field is selected. S32 len = mText.length(); bool all_selected = (len > 0) && (( mSelectionStart == 0 && mSelectionEnd == len ) || ( mSelectionStart == len && mSelectionEnd == 0 )); // Do safe truncation so we don't split multi-byte characters // also consider entire string selected when mSelectAllonFocusReceived is set on an empty, focused line editor all_selected = all_selected || (len == 0 && hasFocus() && mSelectAllonFocusReceived); std::string truncated_utf8 = new_text; if (!mAllowEmoji) { // Cut emoji symbols if exist utf8str_remove_emojis(truncated_utf8); } if (use_size_limit && truncated_utf8.size() > (U32)mMaxLengthBytes) { truncated_utf8 = utf8str_truncate(new_text, mMaxLengthBytes); } mText.assign(truncated_utf8); if (use_size_limit && mMaxLengthChars) { mText.assign(utf8str_symbol_truncate(truncated_utf8, mMaxLengthChars)); } if (all_selected) { // ...keep whole thing selected selectAll(); } else { // try to preserve insertion point, but deselect text deselect(); } setCursor(llmin((S32)mText.length(), getCursor())); // Set current history line to end of history. if (mLineHistory.empty()) { mCurrentHistoryLine = mLineHistory.end(); } else { mCurrentHistoryLine = mLineHistory.end() - 1; } mPrevText = mText; } // Picks a new cursor position based on the actual screen size of text being drawn. void LLLineEditor::setCursorAtLocalPos( S32 local_mouse_x ) { S32 cursor_pos = calcCursorPos(local_mouse_x); S32 left_pos = llmin( mSelectionStart, cursor_pos ); S32 length = llabs( mSelectionStart - cursor_pos ); const LLWString& substr = mText.getWString().substr(left_pos, length); if (mIsSelecting && !prevalidateInput(substr)) return; setCursor(cursor_pos); } void LLLineEditor::setCursor( S32 pos ) { S32 old_cursor_pos = getCursor(); mCursorPos = llclamp( pos, 0, mText.length()); // position of end of next character after cursor S32 pixels_after_scroll = findPixelNearestPos(); if( pixels_after_scroll > mTextRightEdge ) { S32 width_chars_to_left = mGLFont->getWidth(mText.getWString().c_str(), 0, mScrollHPos); S32 last_visible_char = mGLFont->maxDrawableChars(mText.getWString().c_str(), llmax(0.f, (F32)(mTextRightEdge - mTextLeftEdge + width_chars_to_left))); // character immediately to left of cursor should be last one visible (SCROLL_INCREMENT_ADD will scroll in more characters) // or first character if cursor is at beginning S32 new_last_visible_char = llmax(0, getCursor() - 1); S32 min_scroll = mGLFont->firstDrawableChar(mText.getWString().c_str(), (F32)(mTextRightEdge - mTextLeftEdge), mText.length(), new_last_visible_char); if (old_cursor_pos == last_visible_char) { mScrollHPos = llmin(mText.length(), llmax(min_scroll, mScrollHPos + SCROLL_INCREMENT_ADD)); } else { mScrollHPos = min_scroll; } } else if (getCursor() < mScrollHPos) { if (old_cursor_pos == mScrollHPos) { mScrollHPos = llmax(0, llmin(getCursor(), mScrollHPos - SCROLL_INCREMENT_DEL)); } else { mScrollHPos = getCursor(); } } } void LLLineEditor::setCursorToEnd() { setCursor(mText.length()); deselect(); } void LLLineEditor::resetScrollPosition() { mScrollHPos = 0; // make sure cursor says in visible range setCursor(getCursor()); } bool LLLineEditor::canDeselect() const { return hasSelection(); } void LLLineEditor::deselect() { mSelectionStart = 0; mSelectionEnd = 0; mIsSelecting = false; } void LLLineEditor::startSelection() { mIsSelecting = true; mSelectionStart = getCursor(); mSelectionEnd = getCursor(); } void LLLineEditor::endSelection() { if( mIsSelecting ) { mIsSelecting = false; mSelectionEnd = getCursor(); } } bool LLLineEditor::canSelectAll() const { return true; } void LLLineEditor::selectAll() { if (!prevalidateInput(mText.getWString())) { return; } mSelectionStart = mText.length(); mSelectionEnd = 0; setCursor(mSelectionEnd); //mScrollHPos = 0; mIsSelecting = true; updatePrimary(); } bool LLLineEditor::getSpellCheck() const { return (LLSpellChecker::getUseSpellCheck()) && (!mReadOnly) && (mSpellCheck); } const std::string& LLLineEditor::getSuggestion(U32 index) const { return (index < mSuggestionList.size()) ? mSuggestionList[index] : LLStringUtil::null; } U32 LLLineEditor::getSuggestionCount() const { return static_cast(mSuggestionList.size()); } void LLLineEditor::replaceWithSuggestion(U32 index) { for (std::list >::const_iterator it = mMisspellRanges.begin(); it != mMisspellRanges.end(); ++it) { if ( (it->first <= (U32)mCursorPos) && (it->second >= (U32)mCursorPos) ) { LLWString suggestion = utf8str_to_wstring(mSuggestionList[index]); if (!mAllowEmoji) { // Cut emoji symbols if exist wstring_remove_emojis(suggestion); } if (suggestion.empty()) return; deselect(); // Delete the misspelled word mText.erase(it->first, it->second - it->first); // Insert the suggestion in its place mText.insert(it->first, suggestion); setCursor(it->first + (S32)suggestion.length()); break; } } mSpellCheckStart = mSpellCheckEnd = -1; } void LLLineEditor::addToDictionary() { if (canAddToDictionary()) { LLSpellChecker::instance().addToCustomDictionary(getMisspelledWord(mCursorPos)); } } bool LLLineEditor::canAddToDictionary() const { return (getSpellCheck()) && (isMisspelledWord(mCursorPos)); } void LLLineEditor::addToIgnore() { if (canAddToIgnore()) { LLSpellChecker::instance().addToIgnoreList(getMisspelledWord(mCursorPos)); } } bool LLLineEditor::canAddToIgnore() const { return (getSpellCheck()) && (isMisspelledWord(mCursorPos)); } std::string LLLineEditor::getMisspelledWord(U32 pos) const { for (std::list >::const_iterator it = mMisspellRanges.begin(); it != mMisspellRanges.end(); ++it) { if ( (it->first <= pos) && (it->second >= pos) ) { return wstring_to_utf8str(mText.getWString().substr(it->first, it->second - it->first)); } } return LLStringUtil::null; } bool LLLineEditor::isMisspelledWord(U32 pos) const { for (std::list >::const_iterator it = mMisspellRanges.begin(); it != mMisspellRanges.end(); ++it) { if ( (it->first <= pos) && (it->second >= pos) ) { return true; } } return false; } void LLLineEditor::onSpellCheckSettingsChange() { // Recheck the spelling on every change mMisspellRanges.clear(); mSpellCheckStart = mSpellCheckEnd = -1; } bool LLLineEditor::handleDoubleClick(S32 x, S32 y, MASK mask) { setFocus( true ); mTripleClickTimer.setTimerExpirySec(TRIPLE_CLICK_INTERVAL); if (mSelectionEnd == 0 && mSelectionStart == mText.length()) { // if everything is selected, handle this as a normal click to change insertion point handleMouseDown(x, y, mask); } else { const LLWString& wtext = mText.getWString(); bool doSelectAll = true; // Select the word we're on if( LLWStringUtil::isPartOfWord( wtext[mCursorPos] ) ) { S32 old_selection_start = mLastSelectionStart; S32 old_selection_end = mLastSelectionEnd; // Select word the cursor is over while ((mCursorPos > 0) && LLWStringUtil::isPartOfWord( wtext[mCursorPos-1] )) { // Find the start of the word mCursorPos--; } startSelection(); while ((mCursorPos < (S32)wtext.length()) && LLWStringUtil::isPartOfWord( wtext[mCursorPos] ) ) { // Find the end of the word mCursorPos++; } mSelectionEnd = mCursorPos; // If nothing changed, then the word was already selected. Select the whole line. doSelectAll = (old_selection_start == mSelectionStart) && (old_selection_end == mSelectionEnd); } if ( doSelectAll ) { // Select everything selectAll(); } } // 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(); // take selection to 'primary' clipboard updatePrimary(); return true; } bool LLLineEditor::handleMouseDown(S32 x, S32 y, MASK mask) { // Check first whether the "clear search" button wants to deal with this. if(childrenHandleMouseDown(x, y, mask) != NULL) { return true; } if (!mSelectAllonFocusReceived || gFocusMgr.getKeyboardFocus() == this) { mLastSelectionStart = -1; mLastSelectionStart = -1; if (mask & MASK_SHIFT) { // assume we're starting a drag select mIsSelecting = true; // Handle selection extension S32 old_cursor_pos = getCursor(); setCursorAtLocalPos(x); if (hasSelection()) { /* Mac-like behavior - extend selection towards the cursor if (getCursor() < mSelectionStart && getCursor() < mSelectionEnd) { // ...left of selection mSelectionStart = llmax(mSelectionStart, mSelectionEnd); mSelectionEnd = getCursor(); } else if (getCursor() > mSelectionStart && getCursor() > mSelectionEnd) { // ...right of selection mSelectionStart = llmin(mSelectionStart, mSelectionEnd); mSelectionEnd = getCursor(); } else { mSelectionEnd = getCursor(); } */ // Windows behavior mSelectionEnd = getCursor(); } else { mSelectionStart = old_cursor_pos; mSelectionEnd = getCursor(); } } else { if (mTripleClickTimer.hasExpired()) { // Save selection for word/line selecting on double-click mLastSelectionStart = mSelectionStart; mLastSelectionEnd = mSelectionEnd; // Move cursor and deselect for regular click setCursorAtLocalPos( x ); deselect(); startSelection(); } else // handle triple click { selectAll(); // 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; } } gFocusMgr.setMouseCapture( this ); } setFocus(true); // delay cursor flashing mKeystrokeTimer.reset(); if (mMouseDownSignal) (*mMouseDownSignal)(this,x,y,mask); return true; } bool LLLineEditor::handleMiddleMouseDown(S32 x, S32 y, MASK mask) { // LL_INFOS() << "MiddleMouseDown" << LL_ENDL; setFocus( true ); if( canPastePrimary() ) { setCursorAtLocalPos(x); pastePrimary(); } return true; } bool LLLineEditor::handleRightMouseDown(S32 x, S32 y, MASK mask) { setFocus(true); if (!LLUICtrl::handleRightMouseDown(x, y, mask) && getShowContextMenu()) { showContextMenu(x, y); } return true; } bool LLLineEditor::handleHover(S32 x, S32 y, MASK mask) { bool handled = false; // Check first whether the "clear search" button wants to deal with this. if(!hasMouseCapture()) { if(childrenHandleHover(x, y, mask) != NULL) { return true; } } if( (hasMouseCapture()) && mIsSelecting ) { if (x != mLastSelectionX || y != mLastSelectionY) { mLastSelectionX = x; mLastSelectionY = y; } // Scroll if mouse cursor outside of bounds if (mScrollTimer.hasExpired()) { S32 increment = ll_round(mScrollTimer.getElapsedTimeF32() / AUTO_SCROLL_TIME); mScrollTimer.reset(); mScrollTimer.setTimerExpirySec(AUTO_SCROLL_TIME); if( (x < mTextLeftEdge) && (mScrollHPos > 0 ) ) { // Scroll to the left mScrollHPos = llclamp(mScrollHPos - increment, 0, mText.length()); } else if( (x > mTextRightEdge) && (mCursorPos < (S32)mText.length()) ) { // If scrolling one pixel would make a difference... S32 pixels_after_scrolling_one_char = findPixelNearestPos(1); if( pixels_after_scrolling_one_char >= mTextRightEdge ) { // ...scroll to the right mScrollHPos = llclamp(mScrollHPos + increment, 0, mText.length()); } } } setCursorAtLocalPos( x ); mSelectionEnd = getCursor(); // delay cursor flashing mKeystrokeTimer.reset(); getWindow()->setCursor(UI_CURSOR_IBEAM); LL_DEBUGS("UserInput") << "hover handled by " << getName() << " (active)" << LL_ENDL; handled = true; } if( !handled ) { getWindow()->setCursor(UI_CURSOR_IBEAM); LL_DEBUGS("UserInput") << "hover handled by " << getName() << " (inactive)" << LL_ENDL; handled = true; } return handled; } bool LLLineEditor::handleMouseUp(S32 x, S32 y, MASK mask) { bool handled = false; if( hasMouseCapture() ) { gFocusMgr.setMouseCapture( NULL ); handled = true; } // Check first whether the "clear search" button wants to deal with this. if(!handled && childrenHandleMouseUp(x, y, mask) != NULL) { return true; } if( mIsSelecting ) { setCursorAtLocalPos( x ); mSelectionEnd = getCursor(); handled = true; } if( handled ) { // delay cursor flashing mKeystrokeTimer.reset(); // take selection to 'primary' clipboard updatePrimary(); } // We won't call LLUICtrl::handleMouseUp to avoid double calls of childrenHandleMouseUp().Just invoke the signal manually. if (mMouseUpSignal) (*mMouseUpSignal)(this,x,y, mask); return handled; } // Remove a single character from the text void LLLineEditor::removeChar() { if( getCursor() > 0 ) { if (!prevalidateInput(mText.getWString().substr(getCursor()-1, 1))) return; mText.erase(getCursor() - 1, 1); setCursor(getCursor() - 1); } else { LLUI::getInstance()->reportBadKeystroke(); } } void LLLineEditor::addChar(const llwchar uni_char) { if (!mAllowEmoji && LLStringOps::isEmoji(uni_char)) return; llwchar new_c = uni_char; if (hasSelection()) { deleteSelection(); } else if (LL_KIM_OVERWRITE == gKeyboard->getInsertMode()) { if (!prevalidateInput(mText.getWString().substr(getCursor(), 1))) return; mText.erase(getCursor(), 1); } S32 cur_bytes = static_cast(mText.getString().size()); S32 new_bytes = wchar_utf8_length(new_c); bool allow_char = true; // Check byte length limit if ((new_bytes + cur_bytes) > mMaxLengthBytes) { allow_char = false; } else if (mMaxLengthChars) { auto wide_chars = mText.getWString().size(); if ((wide_chars + 1) > mMaxLengthChars) { allow_char = false; } } if (allow_char) { // Will we need to scroll? LLWString w_buf; w_buf.assign(1, new_c); mText.insert(getCursor(), w_buf); setCursor(getCursor() + 1); } else { LLUI::getInstance()->reportBadKeystroke(); } getWindow()->hideCursorUntilMouseMove(); } // Extends the selection box to the new cursor position void LLLineEditor::extendSelection( S32 new_cursor_pos ) { if( !mIsSelecting ) { startSelection(); } S32 left_pos = llmin( mSelectionStart, new_cursor_pos ); S32 selection_length = llabs( mSelectionStart - new_cursor_pos ); const LLWString& selection = mText.getWString().substr(left_pos, selection_length); if (!prevalidateInput(selection)) return; setCursor(new_cursor_pos); mSelectionEnd = getCursor(); } void LLLineEditor::setSelection(S32 start, S32 end) { S32 len = mText.length(); mIsSelecting = true; // JC, yes, this seems odd, but I think you have to presume a // selection dragged from the end towards the start. mSelectionStart = llclamp(end, 0, len); mSelectionEnd = llclamp(start, 0, len); setCursor(start); } void LLLineEditor::setDrawAsterixes(bool b) { mDrawAsterixes = b; updateAllowingLanguageInput(); } S32 LLLineEditor::prevWordPos(S32 cursorPos) const { const LLWString& wtext = mText.getWString(); while( (cursorPos > 0) && (wtext[cursorPos-1] == ' ') ) { cursorPos--; } while( (cursorPos > 0) && LLWStringUtil::isPartOfWord( wtext[cursorPos-1] ) ) { cursorPos--; } return cursorPos; } S32 LLLineEditor::nextWordPos(S32 cursorPos) const { const LLWString& wtext = mText.getWString(); while( (cursorPos < getLength()) && LLWStringUtil::isPartOfWord( wtext[cursorPos] ) ) { cursorPos++; } while( (cursorPos < getLength()) && (wtext[cursorPos] == ' ') ) { cursorPos++; } return cursorPos; } bool LLLineEditor::handleSelectionKey(KEY key, MASK mask) { bool handled = false; if( mask & MASK_SHIFT ) { handled = true; switch( key ) { case KEY_LEFT: if( 0 < getCursor() ) { S32 cursorPos = getCursor() - 1; if( mask & MASK_CONTROL ) { cursorPos = prevWordPos(cursorPos); } extendSelection( cursorPos ); } else { LLUI::getInstance()->reportBadKeystroke(); } break; case KEY_RIGHT: if( getCursor() < mText.length()) { S32 cursorPos = getCursor() + 1; if( mask & MASK_CONTROL ) { cursorPos = nextWordPos(cursorPos); } extendSelection( cursorPos ); } else { LLUI::getInstance()->reportBadKeystroke(); } break; case KEY_PAGE_UP: case KEY_HOME: extendSelection( 0 ); break; case KEY_PAGE_DOWN: case KEY_END: { S32 len = mText.length(); if( len ) { extendSelection( len ); } break; } default: handled = false; break; } } if(handled) { // take selection to 'primary' clipboard updatePrimary(); } return handled; } void LLLineEditor::deleteSelection() { if( !mReadOnly && hasSelection() ) { S32 left_pos, selection_length; getSelectionRange(&left_pos, &selection_length); const LLWString& selection = mText.getWString().substr(left_pos, selection_length); if (!prevalidateInput(selection)) return; mText.erase(left_pos, selection_length); deselect(); setCursor(left_pos); } } bool LLLineEditor::canCut() const { return !mReadOnly && !mDrawAsterixes && hasSelection(); } // cut selection to clipboard void LLLineEditor::cut() { if( canCut() ) { S32 left_pos, length; getSelectionRange(&left_pos, &length); const LLWString& selection = mText.getWString().substr(left_pos, length); if (!prevalidateInput(selection)) return; // Prepare for possible rollback LLLineEditorRollback rollback( this ); LLClipboard::instance().copyToClipboard( mText.getWString(), left_pos, length ); deleteSelection(); // Validate new string and rollback the if needed. bool need_to_rollback = mPrevalidator && !mPrevalidator.validate(mText.getWString()); if (need_to_rollback) { rollback.doRollback( this ); LLUI::getInstance()->reportBadKeystroke(); mPrevalidator.showLastErrorUsingTimeout(); } else { onKeystroke(); } } } bool LLLineEditor::canCopy() const { return !mDrawAsterixes && hasSelection(); } // copy selection to clipboard void LLLineEditor::copy() { if( canCopy() ) { S32 left_pos = llmin( mSelectionStart, mSelectionEnd ); S32 length = llabs( mSelectionStart - mSelectionEnd ); LLClipboard::instance().copyToClipboard( mText.getWString(), left_pos, length ); } } bool LLLineEditor::canPaste() const { return !mReadOnly && LLClipboard::instance().isTextAvailable(); } void LLLineEditor::paste() { bool is_primary = false; pasteHelper(is_primary); } void LLLineEditor::pastePrimary() { bool is_primary = true; pasteHelper(is_primary); } // paste from primary (is_primary==true) or clipboard (is_primary==false) void LLLineEditor::pasteHelper(bool is_primary) { bool can_paste_it; if (is_primary) { can_paste_it = canPastePrimary(); } else { can_paste_it = canPaste(); } if (can_paste_it) { LLWString paste; LLClipboard::instance().pasteFromClipboard(paste, is_primary); if (!paste.empty()) { if (!mAllowEmoji) { wstring_remove_emojis(paste); } if (!prevalidateInput(paste)) return; // Prepare for possible rollback LLLineEditorRollback rollback(this); // Delete any selected characters if ((!is_primary) && hasSelection()) { deleteSelection(); } // Clean up string (replace tabs and returns and remove characters that our fonts don't support.) LLWString clean_string(paste); LLWStringUtil::replaceTabsWithSpaces(clean_string, 1); //clean_string = wstring_detabify(paste, 1); LLWStringUtil::replaceChar(clean_string, '\n', mReplaceNewlinesWithSpaces ? ' ' : 182); // 182 == paragraph character // Insert the string // Check to see that the size isn't going to be larger than the max number of bytes U32 available_bytes = mMaxLengthBytes - wstring_utf8_length(mText); if ( available_bytes < (U32) wstring_utf8_length(clean_string) ) { // Doesn't all fit llwchar current_symbol = clean_string[0]; U32 wchars_that_fit = 0; U32 total_bytes = wchar_utf8_length(current_symbol); //loop over the "wide" characters (symbols) //and check to see how large (in bytes) each symbol is. while ( total_bytes <= available_bytes ) { //while we still have available bytes //"accept" the current symbol and check the size //of the next one current_symbol = clean_string[++wchars_that_fit]; total_bytes += wchar_utf8_length(current_symbol); } // Truncate the clean string at the limit of what will fit clean_string = clean_string.substr(0, wchars_that_fit); LLUI::getInstance()->reportBadKeystroke(); } if (mMaxLengthChars) { auto available_chars = mMaxLengthChars - mText.getWString().size(); if (available_chars < clean_string.size()) { clean_string = clean_string.substr(0, available_chars); } LLUI::getInstance()->reportBadKeystroke(); } mText.insert(getCursor(), clean_string); setCursor( getCursor() + (S32)clean_string.length() ); deselect(); // Validate new string and rollback the if needed. bool need_to_rollback = mPrevalidator && !mPrevalidator.validate(mText.getWString()); if (need_to_rollback) { rollback.doRollback( this ); LLUI::getInstance()->reportBadKeystroke(); mPrevalidator.showLastErrorUsingTimeout(); } else { onKeystroke(); } } } } // copy selection to primary void LLLineEditor::copyPrimary() { if( canCopy() ) { S32 left_pos = llmin( mSelectionStart, mSelectionEnd ); S32 length = llabs( mSelectionStart - mSelectionEnd ); LLClipboard::instance().copyToClipboard( mText.getWString(), left_pos, length, true); } } bool LLLineEditor::canPastePrimary() const { return !mReadOnly && LLClipboard::instance().isTextAvailable(true); } void LLLineEditor::updatePrimary() { if(canCopy() ) { copyPrimary(); } } bool LLLineEditor::handleSpecialKey(KEY key, MASK mask) { bool handled = false; switch( key ) { case KEY_INSERT: if (mask == MASK_NONE) { gKeyboard->toggleInsertMode(); } handled = true; break; case KEY_BACKSPACE: if (!mReadOnly) { //LL_INFOS() << "Handling backspace" << LL_ENDL; if( hasSelection() ) { deleteSelection(); } else if( 0 < getCursor() ) { removeChar(); } else { LLUI::getInstance()->reportBadKeystroke(); } } handled = true; break; case KEY_PAGE_UP: case KEY_HOME: if (!mIgnoreArrowKeys) { setCursor(0); handled = true; } break; case KEY_PAGE_DOWN: case KEY_END: if (!mIgnoreArrowKeys) { S32 len = mText.length(); if( len ) { setCursor(len); } handled = true; } break; case KEY_LEFT: if (mIgnoreArrowKeys && mask == MASK_NONE) break; if ((mask & MASK_ALT) == 0) { if( hasSelection() ) { setCursor(llmin( getCursor() - 1, mSelectionStart, mSelectionEnd )); } else if( 0 < getCursor() ) { S32 cursorPos = getCursor() - 1; if( mask & MASK_CONTROL ) { cursorPos = prevWordPos(cursorPos); } setCursor(cursorPos); } else { LLUI::getInstance()->reportBadKeystroke(); } handled = true; } break; case KEY_RIGHT: if (mIgnoreArrowKeys && mask == MASK_NONE) break; if ((mask & MASK_ALT) == 0) { if (hasSelection()) { setCursor(llmax(getCursor() + 1, mSelectionStart, mSelectionEnd)); } else if (getCursor() < mText.length()) { S32 cursorPos = getCursor() + 1; if( mask & MASK_CONTROL ) { cursorPos = nextWordPos(cursorPos); } setCursor(cursorPos); } else { LLUI::getInstance()->reportBadKeystroke(); } handled = true; } break; // handle ctrl-uparrow if we have a history enabled line editor. case KEY_UP: if (mHaveHistory && (!mIgnoreArrowKeys || (MASK_CONTROL == mask))) { if (mCurrentHistoryLine > mLineHistory.begin()) { mText.assign(*(--mCurrentHistoryLine)); setCursorToEnd(); } else { LLUI::getInstance()->reportBadKeystroke(); } handled = true; } break; // handle [ctrl]-downarrow if we have a history enabled line editor case KEY_DOWN: if (mHaveHistory && (!mIgnoreArrowKeys || (MASK_CONTROL == mask))) { if (!mLineHistory.empty() && mCurrentHistoryLine < mLineHistory.end() - 1) { mText.assign( *(++mCurrentHistoryLine) ); setCursorToEnd(); } else { LLUI::getInstance()->reportBadKeystroke(); } handled = true; } break; case KEY_RETURN: // store sent line in history updateHistory(); break; case KEY_ESCAPE: if (mRevertOnEsc && mText.getString() != mPrevText) { setText(mPrevText); // Note, don't set handled, still want to loose focus (won't commit becase text is now unchanged) if (mKeystrokeOnEsc) { onKeystroke(); } } break; default: break; } return handled; } bool LLLineEditor::handleKeyHere(KEY key, MASK mask ) { bool handled = false; bool selection_modified = false; if ( gFocusMgr.getKeyboardFocus() == this ) { LLLineEditorRollback rollback( this ); if( !handled ) { handled = handleSelectionKey( key, mask ); selection_modified = handled; } // Handle most keys only if the text editor is writeable. if ( !mReadOnly ) { if( !handled ) { handled = handleSpecialKey( key, mask ); } } 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 != key) { deselect(); } bool prevalidator_failed = false; // If read-only, don't allow changes bool need_to_rollback = mReadOnly && (mText.getString() == rollback.getText()); // Validate new string and rollback the keystroke if needed. if (!need_to_rollback && mPrevalidator) { prevalidator_failed = !mPrevalidator.validate(mText.getWString()); need_to_rollback |= prevalidator_failed; } if (need_to_rollback) { rollback.doRollback(this); LLUI::getInstance()->reportBadKeystroke(); if (prevalidator_failed) { mPrevalidator.showLastErrorUsingTimeout(); } } // Notify owner if requested if (!need_to_rollback && handled) { onKeystroke(); if ( (!selection_modified) && (KEY_BACKSPACE == key) ) { mSpellCheckTimer.setTimerExpirySec(SPELLCHECK_DELAY); } } } } return handled; } bool LLLineEditor::handleUnicodeCharHere(llwchar uni_char) { if ((uni_char < 0x20) || (uni_char == 0x7F)) // Control character or DEL { return false; } bool handled = false; if ( (gFocusMgr.getKeyboardFocus() == this) && getVisible() && !mReadOnly) { handled = true; LLLineEditorRollback rollback( this ); { LLWString u_char; u_char.assign(1, uni_char); if (!prevalidateInput(u_char)) return handled; } addChar(uni_char); mKeystrokeTimer.reset(); deselect(); // Validate new string and rollback the keystroke if needed. bool need_to_rollback = mPrevalidator && !mPrevalidator.validate(mText.getWString()); if (need_to_rollback) { rollback.doRollback( this ); LLUI::getInstance()->reportBadKeystroke(); mPrevalidator.showLastErrorUsingTimeout(); } // Notify owner if requested if (!need_to_rollback && handled) { // HACK! The only usage of this callback doesn't do anything with the character. // We'll have to do something about this if something ever changes! - Doug onKeystroke(); mSpellCheckTimer.setTimerExpirySec(SPELLCHECK_DELAY); } } return handled; } bool LLLineEditor::canDoDelete() const { return ( !mReadOnly && (!mPassDelete || (hasSelection() || (getCursor() < mText.length()))) ); } void LLLineEditor::doDelete() { if (canDoDelete() && mText.length() > 0) { // Prepare for possible rollback LLLineEditorRollback rollback( this ); if (hasSelection()) { deleteSelection(); } else if ( getCursor() < mText.length()) { const LLWString& text_to_delete = mText.getWString().substr(getCursor(), 1); if (!prevalidateInput(text_to_delete)) { onKeystroke(); return; } setCursor(getCursor() + 1); removeChar(); } // Validate new string and rollback the if needed. bool need_to_rollback = mPrevalidator && !mPrevalidator.validate(mText.getWString()); if (need_to_rollback) { rollback.doRollback(this); LLUI::getInstance()->reportBadKeystroke(); mPrevalidator.showLastErrorUsingTimeout(); } else { onKeystroke(); mSpellCheckTimer.setTimerExpirySec(SPELLCHECK_DELAY); } } } void LLLineEditor::drawBackground() { F32 alpha = getCurrentTransparency(); if (mUseBgColor) { gl_rect_2d(getLocalRect(), mBgColor % alpha, true); } else { bool has_focus = hasFocus(); LLUIImage* image; if (mReadOnly) { image = mBgImageDisabled; } else if (has_focus || mShowImageFocused) { image = mBgImageFocused; } else { image = mBgImage; } if (!image) return; // optionally draw programmatic border if (has_focus) { LLColor4 tmp_color = gFocusMgr.getFocusColor(); tmp_color.setAlpha(alpha); image->drawBorder(0, 0, getRect().getWidth(), getRect().getHeight(), tmp_color, gFocusMgr.getFocusFlashWidth()); } LLColor4 tmp_color = UI_VERTEX_COLOR; tmp_color.setAlpha(alpha); image->draw(getLocalRect(), tmp_color); } } //virtual void LLLineEditor::draw() { F32 alpha = getDrawContext().mAlpha; S32 text_len = mText.length(); static LLUICachedControl lineeditor_cursor_thickness ("UILineEditorCursorThickness", 0); static LLUICachedControl preedit_marker_brightness ("UIPreeditMarkerBrightness", 0); static LLUICachedControl preedit_marker_gap ("UIPreeditMarkerGap", 0); static LLUICachedControl preedit_marker_position ("UIPreeditMarkerPosition", 0); static LLUICachedControl preedit_marker_thickness ("UIPreeditMarkerThickness", 0); static LLUICachedControl preedit_standout_brightness ("UIPreeditStandoutBrightness", 0); static LLUICachedControl preedit_standout_gap ("UIPreeditStandoutGap", 0); static LLUICachedControl preedit_standout_position ("UIPreeditStandoutPosition", 0); static LLUICachedControl preedit_standout_thickness ("UIPreeditStandoutThickness", 0); std::string saved_text; if (mDrawAsterixes) { saved_text = mText.getString(); std::string text; for (S32 i = 0; i < mText.length(); i++) { text += PASSWORD_ASTERISK; } mText = text; } // draw rectangle for the background LLRect background( 0, getRect().getHeight(), getRect().getWidth(), 0 ); background.stretch( -mBorderThickness ); S32 lineeditor_v_pad = (background.getHeight() - mGLFont->getLineHeight()) / 2; if (mSpellCheck) { lineeditor_v_pad += 1; } drawBackground(); // draw text // With viewer-2 art files, input region is 2 pixels up S32 cursor_bottom = background.mBottom + 2; S32 cursor_top = background.mTop - 1; LLColor4 text_color; if (!mReadOnly) { if (!getTentative()) { text_color = mFgColor.get(); } else { text_color = mTentativeFgColor.get(); } } else { text_color = mReadOnlyFgColor.get(); } text_color.setAlpha(alpha); LLColor4 label_color = mTentativeFgColor.get(); label_color.setAlpha(alpha); if (hasPreeditString()) { // Draw preedit markers. This needs to be before drawing letters. for (U32 i = 0; i < mPreeditStandouts.size(); i++) { const S32 preedit_left = mPreeditPositions[i]; const S32 preedit_right = mPreeditPositions[i + 1]; if (preedit_right > mScrollHPos) { S32 preedit_pixels_left = findPixelNearestPos(llmax(preedit_left, mScrollHPos) - getCursor()); S32 preedit_pixels_right = llmin(findPixelNearestPos(preedit_right - getCursor()), background.mRight); if (preedit_pixels_left >= background.mRight) { break; } if (mPreeditStandouts[i]) { gl_rect_2d(preedit_pixels_left + preedit_standout_gap, background.mBottom + preedit_standout_position, preedit_pixels_right - preedit_standout_gap - 1, background.mBottom + preedit_standout_position - preedit_standout_thickness, (text_color * preedit_standout_brightness + mPreeditBgColor * (1 - preedit_standout_brightness)).setAlpha(alpha/*1.0f*/)); } else { gl_rect_2d(preedit_pixels_left + preedit_marker_gap, background.mBottom + preedit_marker_position, preedit_pixels_right - preedit_marker_gap - 1, background.mBottom + preedit_marker_position - preedit_marker_thickness, (text_color * preedit_marker_brightness + mPreeditBgColor * (1 - preedit_marker_brightness)).setAlpha(alpha/*1.0f*/)); } } } } S32 rendered_text = 0; F32 rendered_pixels_right = (F32)mTextLeftEdge; F32 text_bottom = (F32)background.mBottom + (F32)lineeditor_v_pad; if( (gFocusMgr.getKeyboardFocus() == this) && hasSelection() ) { S32 select_left; S32 select_right; if (mSelectionStart < mSelectionEnd) { select_left = mSelectionStart; select_right = mSelectionEnd; } else { select_left = mSelectionEnd; select_right = mSelectionStart; } if( select_left > mScrollHPos ) { // unselected, left side rendered_text = mGLFont->render( mText, mScrollHPos, rendered_pixels_right, text_bottom, text_color, LLFontGL::LEFT, LLFontGL::BOTTOM, 0, LLFontGL::NO_SHADOW, select_left - mScrollHPos, mTextRightEdge - ll_round(rendered_pixels_right), &rendered_pixels_right); } if( (rendered_pixels_right < (F32)mTextRightEdge) && (rendered_text < text_len) ) { LLColor4 color = mHighlightColor; color.setAlpha(alpha); // selected middle S32 width = mGLFont->getWidth(mText.getWString().c_str(), mScrollHPos + rendered_text, select_right - mScrollHPos - rendered_text); width = llmin(width, mTextRightEdge - ll_round(rendered_pixels_right)); gl_rect_2d(ll_round(rendered_pixels_right), cursor_top, ll_round(rendered_pixels_right)+width, cursor_bottom, color); LLColor4 tmp_color( 1.f - text_color.mV[0], 1.f - text_color.mV[1], 1.f - text_color.mV[2], alpha ); rendered_text += mGLFont->render( mText, mScrollHPos + rendered_text, rendered_pixels_right, text_bottom, tmp_color, LLFontGL::LEFT, LLFontGL::BOTTOM, 0, LLFontGL::NO_SHADOW, select_right - mScrollHPos - rendered_text, mTextRightEdge - ll_round(rendered_pixels_right), &rendered_pixels_right); } if( (rendered_pixels_right < (F32)mTextRightEdge) && (rendered_text < text_len) ) { // unselected, right side rendered_text += mGLFont->render( mText, mScrollHPos + rendered_text, rendered_pixels_right, text_bottom, text_color, LLFontGL::LEFT, LLFontGL::BOTTOM, 0, LLFontGL::NO_SHADOW, S32_MAX, mTextRightEdge - ll_round(rendered_pixels_right), &rendered_pixels_right); } } else { rendered_text = mGLFont->render( mText, mScrollHPos, rendered_pixels_right, text_bottom, text_color, LLFontGL::LEFT, LLFontGL::BOTTOM, 0, LLFontGL::NO_SHADOW, S32_MAX, mTextRightEdge - ll_round(rendered_pixels_right), &rendered_pixels_right); } #if 1 // for when we're ready for image art. mBorder->setVisible(false); // no more programmatic art. #endif if ( (getSpellCheck()) && (mText.length() > 2) ) { // Calculate start and end indices for the first and last visible word U32 start = prevWordPos(mScrollHPos), end = nextWordPos(mScrollHPos + rendered_text); if ( (mSpellCheckStart != start) || (mSpellCheckEnd != end) ) { const LLWString& text = mText.getWString().substr(start, end); // Find the start of the first word U32 word_start = 0, word_end = 0; while ( (word_start < text.length()) && (!LLStringOps::isAlpha(text[word_start])) ) { word_start++; } // Iterate over all words in the text block and check them one by one mMisspellRanges.clear(); while (word_start < text.length()) { // Find the end of the current word (special case handling for "'" when it's used as a contraction) word_end = word_start + 1; while ( (word_end < text.length()) && ((LLWStringUtil::isPartOfWord(text[word_end])) || ((L'\'' == text[word_end]) && (word_end + 1 < text.length()) && (LLStringOps::isAlnum(text[word_end - 1])) && (LLStringOps::isAlnum(text[word_end + 1])))) ) { word_end++; } if (word_end > text.length()) { break; } // Don't process words shorter than 3 characters std::string word = wstring_to_utf8str(text.substr(word_start, word_end - word_start)); if ( (word.length() >= 3) && (!LLSpellChecker::instance().checkSpelling(word)) ) { mMisspellRanges.push_back(std::pair(start + word_start, start + word_end)); } // Find the start of the next word word_start = word_end + 1; while ( (word_start < text.length()) && (!LLWStringUtil::isPartOfWord(text[word_start])) ) { word_start++; } } mSpellCheckStart = start; mSpellCheckEnd = end; } // Draw squiggly lines under any (visible) misspelled words for (std::list >::const_iterator it = mMisspellRanges.begin(); it != mMisspellRanges.end(); ++it) { // Skip over words that aren't (partially) visible if ( ((it->first < start) && (it->second < start)) || (it->first > end) ) { continue; } // Skip the current word if the user is still busy editing it if ( (!mSpellCheckTimer.hasExpired()) && (it->first <= (U32)mCursorPos) && (it->second >= (U32)mCursorPos) ) { continue; } S32 pxWidth = getRect().getWidth(); S32 pxStart = findPixelNearestPos(it->first - getCursor()); if (pxStart > pxWidth) { continue; } S32 pxEnd = findPixelNearestPos(it->second - getCursor()); if (pxEnd > pxWidth) { pxEnd = pxWidth; } S32 pxBottom = (S32)(text_bottom + mGLFont->getDescenderHeight()); gGL.color4ub(255, 0, 0, 200); while (pxStart + 1 < pxEnd) { gl_line_2d(pxStart, pxBottom, pxStart + 2, pxBottom - 2); if (pxStart + 3 < pxEnd) { gl_line_2d(pxStart + 2, pxBottom - 3, pxStart + 4, pxBottom - 1); } pxStart += 4; } } } // If we're editing... if( hasFocus()) { //mBorder->setVisible(true); // ok, programmer art just this once. // (Flash the cursor every half second) if (!mReadOnly && gFocusMgr.getAppHasFocus()) { F32 elapsed = mKeystrokeTimer.getElapsedTimeF32(); if( (elapsed < CURSOR_FLASH_DELAY ) || (S32(elapsed * 2) & 1) ) { S32 cursor_left = findPixelNearestPos(); cursor_left -= lineeditor_cursor_thickness / 2; S32 cursor_right = cursor_left + lineeditor_cursor_thickness; if (LL_KIM_OVERWRITE == gKeyboard->getInsertMode() && !hasSelection()) { const LLWString space(utf8str_to_wstring(std::string(" "))); S32 wswidth = mGLFont->getWidth(space.c_str()); S32 width = mGLFont->getWidth(mText.getWString().c_str(), getCursor(), 1) + 1; cursor_right = cursor_left + llmax(wswidth, width); } // Use same color as text for the Cursor gl_rect_2d(cursor_left, cursor_top, cursor_right, cursor_bottom, text_color); if (LL_KIM_OVERWRITE == gKeyboard->getInsertMode() && !hasSelection()) { LLColor4 tmp_color( 1.f - text_color.mV[0], 1.f - text_color.mV[1], 1.f - text_color.mV[2], alpha ); mGLFont->render(mText, getCursor(), (F32)(cursor_left + lineeditor_cursor_thickness / 2), text_bottom, tmp_color, LLFontGL::LEFT, LLFontGL::BOTTOM, 0, LLFontGL::NO_SHADOW, 1); } // Make sure the IME is in the right place S32 pixels_after_scroll = findPixelNearestPos(); // RCalculcate for IME position LLRect screen_pos = calcScreenRect(); LLCoordGL ime_pos( screen_pos.mLeft + pixels_after_scroll, screen_pos.mTop - lineeditor_v_pad ); ime_pos.mX = (S32) (ime_pos.mX * LLUI::getScaleFactor().mV[VX]); ime_pos.mY = (S32) (ime_pos.mY * LLUI::getScaleFactor().mV[VY]); getWindow()->setLanguageTextInput( ime_pos ); } } //draw label if no text is provided //but we should draw it in a different color //to give indication that it is not text you typed in if (0 == mText.length() && (mReadOnly || mShowLabelFocused)) { mGLFont->render(mLabel.getWString(), 0, mTextLeftEdge, (F32)text_bottom, label_color, LLFontGL::LEFT, LLFontGL::BOTTOM, 0, LLFontGL::NO_SHADOW, S32_MAX, mTextRightEdge - ll_round(rendered_pixels_right), &rendered_pixels_right, false); } // Draw children (border) //mBorder->setVisible(true); mBorder->setKeyboardFocusHighlight( true ); LLView::draw(); mBorder->setKeyboardFocusHighlight( false ); //mBorder->setVisible(false); } else // does not have keyboard input { // draw label if no text provided if (0 == mText.length()) { mGLFont->render(mLabel.getWString(), 0, mTextLeftEdge, (F32)text_bottom, label_color, LLFontGL::LEFT, LLFontGL::BOTTOM, 0, LLFontGL::NO_SHADOW, S32_MAX, mTextRightEdge - ll_round(rendered_pixels_right), &rendered_pixels_right); } // Draw children (border) LLView::draw(); } if (mDrawAsterixes) { mText = saved_text; } } // Returns the local screen space X coordinate associated with the text cursor position. S32 LLLineEditor::findPixelNearestPos(const S32 cursor_offset) const { S32 dpos = getCursor() - mScrollHPos + cursor_offset; S32 result = mGLFont->getWidth(mText.getWString().c_str(), mScrollHPos, dpos) + mTextLeftEdge; return result; } S32 LLLineEditor::calcCursorPos(S32 mouse_x) { const llwchar* wtext = mText.getWString().c_str(); LLWString asterix_text; if (mDrawAsterixes) { for (S32 i = 0; i < mText.length(); i++) { asterix_text += utf8str_to_wstring(PASSWORD_ASTERISK); } wtext = asterix_text.c_str(); } S32 cur_pos = mScrollHPos + mGLFont->charFromPixelOffset( wtext, mScrollHPos, (F32)(mouse_x - mTextLeftEdge), (F32)(mTextRightEdge - mTextLeftEdge + 1)); // min-max range is inclusive return cur_pos; } //virtual void LLLineEditor::clear() { mText.clear(); setCursor(0); } //virtual void LLLineEditor::onTabInto() { selectAll(); LLUICtrl::onTabInto(); } //virtual bool LLLineEditor::acceptsTextInput() const { return true; } // Start or stop the editor from accepting text-editing keystrokes void LLLineEditor::setFocus( bool new_state ) { bool old_state = hasFocus(); if (!new_state) { getWindow()->allowLanguageTextInput(this, false); } // getting focus when we didn't have it before, and we want to select all if (!old_state && new_state && mSelectAllonFocusReceived) { selectAll(); // 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; } if( new_state ) { gEditMenuHandler = this; // Don't start the cursor flashing right away mKeystrokeTimer.reset(); } else { // Not really needed, since loss of keyboard focus should take care of this, // but limited paranoia is ok. if( gEditMenuHandler == this ) { gEditMenuHandler = NULL; } endSelection(); } LLUICtrl::setFocus( new_state ); if (new_state) { // Allow Language Text Input only when this LineEditor has // no prevalidate function attached. This criterion works // fine on 1.15.0.2, since all prevalidate func reject any // non-ASCII characters. I'm not sure on future versions, // however. getWindow()->allowLanguageTextInput(this, !mPrevalidator); } } //virtual void LLLineEditor::setRect(const LLRect& rect) { LLUICtrl::setRect(rect); if (mBorder) { LLRect border_rect = mBorder->getRect(); // Scalable UI somehow made these rectangles off-by-one. // I don't know why. JC border_rect.setOriginAndSize(border_rect.mLeft, border_rect.mBottom, rect.getWidth()-1, rect.getHeight()-1); mBorder->setRect(border_rect); } } void LLLineEditor::setPrevalidate(LLTextValidate::Validator validator) { mPrevalidator = validator; updateAllowingLanguageInput(); } void LLLineEditor::setPrevalidateInput(LLTextValidate::Validator validator) { mInputPrevalidator = validator; updateAllowingLanguageInput(); } bool LLLineEditor::prevalidateInput(const LLWString& wstr) { return mInputPrevalidator.validate(wstr); } // static bool LLLineEditor::postvalidateFloat(const std::string &str) { LLLocale locale(LLLocale::USER_LOCALE); bool success = true; bool has_decimal = false; bool has_digit = false; LLWString trimmed = utf8str_to_wstring(str); LLWStringUtil::trim(trimmed); auto len = trimmed.length(); if( 0 < len ) { size_t i = 0; // First character can be a negative sign if( '-' == trimmed[0] ) { i++; } // May be a comma or period, depending on the locale llwchar decimal_point = (llwchar)LLResMgr::getInstance()->getDecimalPoint(); for( ; i < len; i++ ) { if( decimal_point == trimmed[i] ) { if( has_decimal ) { // can't have two success = false; break; } else { has_decimal = true; } } else if( LLStringOps::isDigit( trimmed[i] ) ) { has_digit = true; } else { success = false; break; } } } // Gotta have at least one success = has_digit; return success; } bool LLLineEditor::evaluateFloat() { bool success; F32 result = 0.f; std::string expr = getText(); LLStringUtil::toUpper(expr); success = LLCalc::getInstance()->evalString(expr, result); if (!success) { // Move the cursor to near the error on failure setCursor(static_cast(LLCalc::getInstance()->getLastErrorPos())); // *TODO: Translated error message indicating the type of error? Select error text? } else { // Replace the expression with the result std::string result_str = llformat("%f",result); setText(result_str); selectAll(); } return success; } void LLLineEditor::onMouseCaptureLost() { endSelection(); } void LLLineEditor::setSelectAllonFocusReceived(bool b) { mSelectAllonFocusReceived = b; } void LLLineEditor::onKeystroke() { if (mKeystrokeCallback) { mKeystrokeCallback(this); } mSpellCheckStart = mSpellCheckEnd = -1; } void LLLineEditor::setKeystrokeCallback(callback_t callback, void* user_data) { mKeystrokeCallback = boost::bind(callback, _1, user_data); } bool LLLineEditor::setTextArg( const std::string& key, const LLStringExplicit& text ) { mText.setArg(key, text); return true; } bool LLLineEditor::setLabelArg( const std::string& key, const LLStringExplicit& text ) { mLabel.setArg(key, text); return true; } void LLLineEditor::updateAllowingLanguageInput() { // Allow Language Text Input only when this LineEditor has // no prevalidate function attached (as long as other criteria // common to LLTextEditor). This criterion works // fine on 1.15.0.2, since all prevalidate func reject any // non-ASCII characters. I'm not sure on future versions, // however... LLWindow* window = getWindow(); if (!window) { // test app, no window available return; } if (hasFocus() && !mReadOnly && !mDrawAsterixes && !mPrevalidator) { window->allowLanguageTextInput(this, true); } else { window->allowLanguageTextInput(this, false); } } bool LLLineEditor::hasPreeditString() const { return (mPreeditPositions.size() > 1); } void LLLineEditor::resetPreedit() { if (hasSelection()) { if (hasPreeditString()) { LL_WARNS() << "Preedit and selection!" << LL_ENDL; deselect(); } else { deleteSelection(); } } if (hasPreeditString()) { const S32 preedit_pos = mPreeditPositions.front(); mText.erase(preedit_pos, mPreeditPositions.back() - preedit_pos); mText.insert(preedit_pos, mPreeditOverwrittenWString); setCursor(preedit_pos); mPreeditWString.clear(); mPreeditOverwrittenWString.clear(); mPreeditPositions.clear(); // Don't reset key stroke timer nor invoke keystroke callback, // because a call to updatePreedit should be follow soon in // normal course of operation, and timer and callback will be // maintained there. Doing so here made an odd sound. (VWR-3410) } } void LLLineEditor::updatePreedit(const LLWString &preedit_string, const segment_lengths_t &preedit_segment_lengths, const standouts_t &preedit_standouts, S32 caret_position) { // Just in case. if (mReadOnly) { return; } // Note that call to updatePreedit is always preceeded by resetPreedit, // so we have no existing selection/preedit. S32 insert_preedit_at = getCursor(); mPreeditWString = preedit_string; mPreeditPositions.resize(preedit_segment_lengths.size() + 1); S32 position = insert_preedit_at; for (segment_lengths_t::size_type i = 0; i < preedit_segment_lengths.size(); i++) { mPreeditPositions[i] = position; position += preedit_segment_lengths[i]; } mPreeditPositions.back() = position; if (LL_KIM_OVERWRITE == gKeyboard->getInsertMode()) { mPreeditOverwrittenWString.assign( LLWString( mText, insert_preedit_at, mPreeditWString.length() ) ); mText.erase(insert_preedit_at, static_cast(mPreeditWString.length())); } else { mPreeditOverwrittenWString.clear(); } mText.insert(insert_preedit_at, mPreeditWString); mPreeditStandouts = preedit_standouts; setCursor(position); setCursor(mPreeditPositions.front() + caret_position); // Update of the preedit should be caused by some key strokes. mKeystrokeTimer.reset(); onKeystroke(); mSpellCheckTimer.setTimerExpirySec(SPELLCHECK_DELAY); } bool LLLineEditor::getPreeditLocation(S32 query_offset, LLCoordGL *coord, LLRect *bounds, LLRect *control) const { if (control) { LLRect control_rect_screen; localRectToScreen(getRect(), &control_rect_screen); LLUI::getInstance()->screenRectToGL(control_rect_screen, control); } S32 preedit_left_column, preedit_right_column; if (hasPreeditString()) { preedit_left_column = mPreeditPositions.front(); preedit_right_column = mPreeditPositions.back(); } else { preedit_left_column = preedit_right_column = getCursor(); } if (preedit_right_column < mScrollHPos) { // This should not occure... return false; } const S32 query = (query_offset >= 0 ? preedit_left_column + query_offset : getCursor()); if (query < mScrollHPos || query < preedit_left_column || query > preedit_right_column) { return false; } if (coord) { S32 query_local = findPixelNearestPos(query - getCursor()); S32 query_screen_x, query_screen_y; localPointToScreen(query_local, getRect().getHeight() / 2, &query_screen_x, &query_screen_y); LLUI::getInstance()->screenPointToGL(query_screen_x, query_screen_y, &coord->mX, &coord->mY); } if (bounds) { S32 preedit_left_local = findPixelNearestPos(llmax(preedit_left_column, mScrollHPos) - getCursor()); S32 preedit_right_local = llmin(findPixelNearestPos(preedit_right_column - getCursor()), getRect().getWidth() - mBorderThickness); if (preedit_left_local > preedit_right_local) { // Is this condition possible? preedit_right_local = preedit_left_local; } LLRect preedit_rect_local(preedit_left_local, getRect().getHeight(), preedit_right_local, 0); LLRect preedit_rect_screen; localRectToScreen(preedit_rect_local, &preedit_rect_screen); LLUI::getInstance()->screenRectToGL(preedit_rect_screen, bounds); } return true; } void LLLineEditor::getPreeditRange(S32 *position, S32 *length) const { if (hasPreeditString()) { *position = mPreeditPositions.front(); *length = mPreeditPositions.back() - mPreeditPositions.front(); } else { *position = mCursorPos; *length = 0; } } void LLLineEditor::getSelectionRange(S32 *position, S32 *length) const { if (hasSelection()) { *position = llmin(mSelectionStart, mSelectionEnd); *length = llabs(mSelectionStart - mSelectionEnd); } else { *position = mCursorPos; *length = 0; } } void LLLineEditor::markAsPreedit(S32 position, S32 length) { deselect(); setCursor(position); if (hasPreeditString()) { LL_WARNS() << "markAsPreedit invoked when hasPreeditString is true." << LL_ENDL; } mPreeditWString.assign( LLWString( mText.getWString(), position, length ) ); if (length > 0) { mPreeditPositions.resize(2); mPreeditPositions[0] = position; mPreeditPositions[1] = position + length; mPreeditStandouts.resize(1); mPreeditStandouts[0] = false; } else { mPreeditPositions.clear(); mPreeditStandouts.clear(); } if (LL_KIM_OVERWRITE == gKeyboard->getInsertMode()) { mPreeditOverwrittenWString = mPreeditWString; } else { mPreeditOverwrittenWString.clear(); } } S32 LLLineEditor::getPreeditFontSize() const { return ll_round(mGLFont->getLineHeight() * LLUI::getScaleFactor().mV[VY]); } void LLLineEditor::setReplaceNewlinesWithSpaces(bool replace) { mReplaceNewlinesWithSpaces = replace; } LLWString LLLineEditor::getConvertedText() const { LLWString text = getWText(); LLWStringUtil::trim(text); if (!mReplaceNewlinesWithSpaces) { LLWStringUtil::replaceChar(text,182,'\n'); // Convert paragraph symbols back into newlines. } return text; } void LLLineEditor::showContextMenu(S32 x, S32 y) { LLContextMenu* menu = static_cast(mContextMenuHandle.get()); if (!menu) { llassert(LLMenuGL::sMenuContainer != NULL); menu = LLUICtrlFactory::createFromFile ("menu_text_editor.xml", LLMenuGL::sMenuContainer, LLMenuHolderGL::child_registry_t::instance()); setContextMenu(menu); } if (menu) { gEditMenuHandler = this; S32 screen_x, screen_y; localPointToScreen(x, y, &screen_x, &screen_y); setCursorAtLocalPos(x); if (hasSelection()) { if ( (mCursorPos < llmin(mSelectionStart, mSelectionEnd)) || (mCursorPos > llmax(mSelectionStart, mSelectionEnd)) ) { deselect(); } else { setCursor(llmax(mSelectionStart, mSelectionEnd)); } } bool use_spellcheck = getSpellCheck(), is_misspelled = false; if (use_spellcheck) { mSuggestionList.clear(); // If the cursor is on a misspelled word, retrieve suggestions for it std::string misspelled_word = getMisspelledWord(mCursorPos); if ((is_misspelled = !misspelled_word.empty())) { LLSpellChecker::instance().getSuggestions(misspelled_word, mSuggestionList); } } menu->setItemVisible("Suggestion Separator", (use_spellcheck) && (!mSuggestionList.empty())); menu->setItemVisible("Add to Dictionary", (use_spellcheck) && (is_misspelled)); menu->setItemVisible("Add to Ignore", (use_spellcheck) && (is_misspelled)); menu->setItemVisible("Spellcheck Separator", (use_spellcheck) && (is_misspelled)); menu->show(screen_x, screen_y, this); } } void LLLineEditor::setContextMenu(LLContextMenu* new_context_menu) { LLContextMenu* menu = static_cast(mContextMenuHandle.get()); if (menu) { menu->die(); mContextMenuHandle.markDead(); } if (new_context_menu) { mContextMenuHandle = new_context_menu->getHandle(); } } void LLLineEditor::setFont(const LLFontGL* font) { mGLFont = font; }