diff options
Diffstat (limited to 'indra/llui/lllineeditor.cpp')
-rw-r--r-- | indra/llui/lllineeditor.cpp | 5470 |
1 files changed, 2735 insertions, 2735 deletions
diff --git a/indra/llui/lllineeditor.cpp b/indra/llui/lllineeditor.cpp index 8c5e69fbb2..5ec4c34f57 100644 --- a/indra/llui/lllineeditor.cpp +++ b/indra/llui/lllineeditor.cpp @@ -1,2735 +1,2735 @@ -/**
- * @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<LLLineEditor> r1("line_editor");
-
-// Compiler optimization, generate extern template
-template class LLLineEditor* LLView::getChild<class LLLineEditor>(
- const std::string& 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<LLViewBorder>(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<LLContextMenu*>(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 mSuggestionList.size();
-}
-
-void LLLineEditor::replaceWithSuggestion(U32 index)
-{
- for (std::list<std::pair<U32, U32> >::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<std::pair<U32, U32> >::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<std::pair<U32, U32> >::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 = 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)
- {
- S32 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)
- {
- U32 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<S32> lineeditor_cursor_thickness ("UILineEditorCursorThickness", 0);
- static LLUICachedControl<F32> preedit_marker_brightness ("UIPreeditMarkerBrightness", 0);
- static LLUICachedControl<S32> preedit_marker_gap ("UIPreeditMarkerGap", 0);
- static LLUICachedControl<S32> preedit_marker_position ("UIPreeditMarkerPosition", 0);
- static LLUICachedControl<S32> preedit_marker_thickness ("UIPreeditMarkerThickness", 0);
- static LLUICachedControl<F32> preedit_standout_brightness ("UIPreeditStandoutBrightness", 0);
- static LLUICachedControl<S32> preedit_standout_gap ("UIPreeditStandoutGap", 0);
- static LLUICachedControl<S32> preedit_standout_position ("UIPreeditStandoutPosition", 0);
- static LLUICachedControl<S32> preedit_standout_thickness ("UIPreeditStandoutThickness", 0);
-
- 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<U32, U32>(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<std::pair<U32, U32> >::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);
- S32 len = trimmed.length();
- if( 0 < len )
- {
- S32 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(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, 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<LLContextMenu*>(mContextMenuHandle.get());
- if (!menu)
- {
- llassert(LLMenuGL::sMenuContainer != NULL);
- menu = LLUICtrlFactory::createFromFile<LLContextMenu>
- ("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<LLContextMenu*>(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;
-}
+/** + * @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<LLLineEditor> r1("line_editor"); + +// Compiler optimization, generate extern template +template class LLLineEditor* LLView::getChild<class LLLineEditor>( + const std::string& 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<LLViewBorder>(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<LLContextMenu*>(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 mSuggestionList.size(); +} + +void LLLineEditor::replaceWithSuggestion(U32 index) +{ + for (std::list<std::pair<U32, U32> >::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<std::pair<U32, U32> >::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<std::pair<U32, U32> >::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 = 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) + { + S32 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) + { + U32 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<S32> lineeditor_cursor_thickness ("UILineEditorCursorThickness", 0); + static LLUICachedControl<F32> preedit_marker_brightness ("UIPreeditMarkerBrightness", 0); + static LLUICachedControl<S32> preedit_marker_gap ("UIPreeditMarkerGap", 0); + static LLUICachedControl<S32> preedit_marker_position ("UIPreeditMarkerPosition", 0); + static LLUICachedControl<S32> preedit_marker_thickness ("UIPreeditMarkerThickness", 0); + static LLUICachedControl<F32> preedit_standout_brightness ("UIPreeditStandoutBrightness", 0); + static LLUICachedControl<S32> preedit_standout_gap ("UIPreeditStandoutGap", 0); + static LLUICachedControl<S32> preedit_standout_position ("UIPreeditStandoutPosition", 0); + static LLUICachedControl<S32> preedit_standout_thickness ("UIPreeditStandoutThickness", 0); + + 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<U32, U32>(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<std::pair<U32, U32> >::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); + S32 len = trimmed.length(); + if( 0 < len ) + { + S32 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(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, 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<LLContextMenu*>(mContextMenuHandle.get()); + if (!menu) + { + llassert(LLMenuGL::sMenuContainer != NULL); + menu = LLUICtrlFactory::createFromFile<LLContextMenu> + ("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<LLContextMenu*>(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; +} |