diff options
| -rw-r--r-- | indra/llcommon/llstring.h | 3 | ||||
| -rw-r--r-- | indra/llui/lllineeditor.cpp | 217 | ||||
| -rw-r--r-- | indra/llui/lllineeditor.h | 29 | ||||
| -rw-r--r-- | indra/newview/skins/default/xui/en/menu_text_editor.xml | 79 | 
4 files changed, 324 insertions, 4 deletions
| diff --git a/indra/llcommon/llstring.h b/indra/llcommon/llstring.h index 7e41e787b5..3a27badb5a 100644 --- a/indra/llcommon/llstring.h +++ b/indra/llcommon/llstring.h @@ -182,6 +182,9 @@ public:  	static bool isPunct(char a) { return ispunct((unsigned char)a) != 0; }  	static bool isPunct(llwchar a) { return iswpunct(a) != 0; } +	static bool isAlpha(char a) { return isalpha((unsigned char)a) != 0; } +	static bool isAlpha(llwchar a) { return iswalpha(a) != 0; } +  	static bool isAlnum(char a) { return isalnum((unsigned char)a) != 0; }  	static bool isAlnum(llwchar a) { return iswalnum(a) != 0; } diff --git a/indra/llui/lllineeditor.cpp b/indra/llui/lllineeditor.cpp index 06dfc90d83..5479c080bd 100644 --- a/indra/llui/lllineeditor.cpp +++ b/indra/llui/lllineeditor.cpp @@ -45,6 +45,7 @@  #include "llkeyboard.h"  #include "llrect.h"  #include "llresmgr.h" +#include "llspellcheck.h"  #include "llstring.h"  #include "llwindow.h"  #include "llui.h" @@ -65,6 +66,7 @@ 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 showing spell checking feedback for the word the cursor is on  const std::string PASSWORD_ASTERISK( "\xE2\x80\xA2" ); // U+2022 BULLET @@ -88,6 +90,7 @@ LLLineEditor::Params::Params()  	background_image_focused("background_image_focused"),  	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), @@ -134,6 +137,9 @@ LLLineEditor::LLLineEditor(const LLLineEditor::Params& p)  	mIgnoreArrowKeys( FALSE ),  	mIgnoreTab( p.ignore_tab ),  	mDrawAsterixes( p.is_password ), +	mSpellCheck( p.spellcheck ), +	mSpellCheckStart(-1), +	mSpellCheckEnd(-1),  	mSelectAllonFocusReceived( p.select_on_focus ),  	mSelectAllonCommit( TRUE ),  	mPassDelete(FALSE), @@ -177,6 +183,12 @@ LLLineEditor::LLLineEditor(const LLLineEditor::Params& p)  	updateTextPadding();  	setCursor(mText.length()); +	if (mSpellCheck) +	{ +		LLSpellChecker::setSettingsChangeCallback(boost::bind(&LLLineEditor::onSpellCheckSettingsChange, this)); +	} +	mSpellCheckTimer.reset(); +  	setPrevalidateInput(p.prevalidate_input_callback());  	setPrevalidate(p.prevalidate_callback()); @@ -519,6 +531,94 @@ void LLLineEditor::selectAll()  	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) ) +		{ +			deselect(); + +			// Delete the misspelled word +			mText.erase(it->first, it->second - it->first); + +			// Insert the suggestion in its place +			LLWString suggestion = utf8str_to_wstring(mSuggestionList[index]); +			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 +	mSpellCheckStart = mSpellCheckEnd = -1; +}  BOOL LLLineEditor::handleDoubleClick(S32 x, S32 y, MASK mask)  { @@ -1453,6 +1553,10 @@ BOOL LLLineEditor::handleKeyHere(KEY key, MASK mask )  				{  					mKeystrokeCallback(this);  				} +				if ( (!selection_modified) && (KEY_BACKSPACE == key) ) +				{ +					mSpellCheckTimer.setTimerExpirySec(SPELLCHECK_DELAY); +				}  			}  		}  	} @@ -1510,6 +1614,7 @@ BOOL LLLineEditor::handleUnicodeCharHere(llwchar uni_char)  				// We'll have to do something about this if something ever changes! - Doug  				mKeystrokeCallback( this );  			} +			mSpellCheckTimer.setTimerExpirySec(SPELLCHECK_DELAY);  		}  	}  	return handled; @@ -1560,6 +1665,7 @@ void LLLineEditor::doDelete()  			{  				mKeystrokeCallback( this );  			} +			mSpellCheckTimer.setTimerExpirySec(SPELLCHECK_DELAY);  		}  	}  } @@ -1756,7 +1862,7 @@ void LLLineEditor::draw()  		if( (rendered_pixels_right < (F32)mTextRightEdge) && (rendered_text < text_len) )  		{  			// unselected, right side -			mGLFont->render(  +			rendered_text += mGLFont->render(   				mText, mScrollHPos + rendered_text,  				rendered_pixels_right, text_bottom,  				text_color, @@ -1770,7 +1876,7 @@ void LLLineEditor::draw()  	}  	else  	{ -		mGLFont->render(  +		rendered_text = mGLFont->render(   			mText, mScrollHPos,   			rendered_pixels_right, text_bottom,  			text_color, @@ -1785,6 +1891,80 @@ void LLLineEditor::draw()  	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; + +			gGL.color4ub(255, 0, 0, 200); +			while (pxStart < pxEnd) +			{ +				gl_line_2d(pxStart, text_bottom - 2, pxStart + 3, text_bottom + 1); +				gl_line_2d(pxStart + 3, text_bottom + 1, pxStart + 6, text_bottom - 2); +				pxStart += 6; +			} +		} +	} +  	// If we're editing...  	if( hasFocus())  	{ @@ -2242,6 +2422,7 @@ void LLLineEditor::updatePreedit(const LLWString &preedit_string,  	{  		mKeystrokeCallback( this );  	} +	mSpellCheckTimer.setTimerExpirySec(SPELLCHECK_DELAY);  }  BOOL LLLineEditor::getPreeditLocation(S32 query_offset, LLCoordGL *coord, LLRect *bounds, LLRect *control) const @@ -2393,7 +2574,37 @@ void LLLineEditor::showContextMenu(S32 x, S32 y)  		S32 screen_x, screen_y;  		localPointToScreen(x, y, &screen_x, &screen_y); -		menu->show(screen_x, screen_y); + +		if (hasSelection()) +		{ +			if ( (mCursorPos < llmin(mSelectionStart, mSelectionEnd)) || (mCursorPos > llmax(mSelectionStart, mSelectionEnd)) ) +				deselect(); +			else +				setCursor(llmax(mSelectionStart, mSelectionEnd)); +		} +		else +		{ +			setCursorAtLocalPos(x); +		} + +		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()) == true) +			{ +				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);  	}  } diff --git a/indra/llui/lllineeditor.h b/indra/llui/lllineeditor.h index 2518dbe3c7..9513274f21 100644 --- a/indra/llui/lllineeditor.h +++ b/indra/llui/lllineeditor.h @@ -40,6 +40,7 @@  #include "llframetimer.h"  #include "lleditmenuhandler.h" +#include "llspellcheckmenuhandler.h"  #include "lluictrl.h"  #include "lluiimage.h"  #include "lluistring.h" @@ -54,7 +55,7 @@ class LLButton;  class LLContextMenu;  class LLLineEditor -: public LLUICtrl, public LLEditMenuHandler, protected LLPreeditor +: public LLUICtrl, public LLEditMenuHandler, protected LLPreeditor, public LLSpellCheckMenuHandler  {  public: @@ -86,6 +87,7 @@ public:  		Optional<bool>					select_on_focus,  										revert_on_esc, +										spellcheck,  										commit_on_focus_lost,  										ignore_tab,  										is_password; @@ -146,6 +148,24 @@ public:  	virtual void	deselect();  	virtual BOOL	canDeselect() const; +	// LLSpellCheckMenuHandler overrides +	/*virtual*/ bool	getSpellCheck() const; + +	/*virtual*/ const std::string& getSuggestion(U32 index) const; +	/*virtual*/ U32		getSuggestionCount() const; +	/*virtual*/ void	replaceWithSuggestion(U32 index); + +	/*virtual*/ void	addToDictionary(); +	/*virtual*/ bool	canAddToDictionary() const; + +	/*virtual*/ void	addToIgnore(); +	/*virtual*/ bool	canAddToIgnore() const; + +	// Spell checking helper functions +	std::string			getMisspelledWord(U32 pos) const; +	bool				isMisspelledWord(U32 pos) const; +	void				onSpellCheckSettingsChange(); +  	// view overrides  	virtual void	draw();  	virtual void	reshape(S32 width,S32 height,BOOL called_from_parent=TRUE); @@ -322,6 +342,13 @@ protected:  	S32			mLastSelectionStart;  	S32			mLastSelectionEnd; +	bool		mSpellCheck; +	S32			mSpellCheckStart; +	S32			mSpellCheckEnd; +	LLTimer		mSpellCheckTimer; +	std::list<std::pair<U32, U32> > mMisspellRanges; +	std::vector<std::string>		mSuggestionList; +  	LLTextValidate::validate_func_t mPrevalidateFunc;  	LLTextValidate::validate_func_t mPrevalidateInputFunc; diff --git a/indra/newview/skins/default/xui/en/menu_text_editor.xml b/indra/newview/skins/default/xui/en/menu_text_editor.xml index fe8489166b..70b40dd89b 100644 --- a/indra/newview/skins/default/xui/en/menu_text_editor.xml +++ b/indra/newview/skins/default/xui/en/menu_text_editor.xml @@ -2,6 +2,85 @@  <context_menu   name="Text editor context menu">    <menu_item_call +   label="(unknown)" +   layout="topleft" +   name="Suggestion 1"> +    <menu_item_call.on_click +     function="SpellCheck.ReplaceWithSuggestion" +     parameter="0" /> +    <menu_item_call.on_visible +     function="SpellCheck.VisibleSuggestion" +     parameter="0" /> +  </menu_item_call> +  <menu_item_call +   label="(unknown)" +   layout="topleft" +   name="Suggestion 2"> +    <menu_item_call.on_click +     function="SpellCheck.ReplaceWithSuggestion" +     parameter="1" /> +    <menu_item_call.on_visible +     function="SpellCheck.VisibleSuggestion" +     parameter="1" /> +  </menu_item_call> +  <menu_item_call +   label="(unknown)" +   layout="topleft" +   name="Suggestion 3"> +    <menu_item_call.on_click +     function="SpellCheck.ReplaceWithSuggestion" +     parameter="2" /> +    <menu_item_call.on_visible +     function="SpellCheck.VisibleSuggestion" +     parameter="2" /> +  </menu_item_call> +  <menu_item_call +   label="(unknown)" +   layout="topleft" +   name="Suggestion 4"> +    <menu_item_call.on_click +     function="SpellCheck.ReplaceWithSuggestion" +     parameter="3" /> +    <menu_item_call.on_visible +     function="SpellCheck.VisibleSuggestion" +     parameter="3" /> +  </menu_item_call> +  <menu_item_call +   label="(unknown)" +   layout="topleft" +   name="Suggestion 5"> +    <menu_item_call.on_click +     function="SpellCheck.ReplaceWithSuggestion" +     parameter="4" /> +    <menu_item_call.on_visible +     function="SpellCheck.VisibleSuggestion" +     parameter="4" /> +  </menu_item_call> +  <menu_item_separator +   layout="topleft" +   name="Suggestion Separator" /> +  <menu_item_call +   label="Add to Dictionary" +   layout="topleft" +   name="Add to Dictionary"> +    <menu_item_call.on_click +     function="SpellCheck.AddToDictionary" /> +    <menu_item_call.on_enable +     function="SpellCheck.EnableAddToDictionary" /> +  </menu_item_call> +  <menu_item_call +   label="Add to Ignore" +   layout="topleft" +   name="Add to Ignore"> +    <menu_item_call.on_click +     function="SpellCheck.AddToIgnore" /> +    <menu_item_call.on_enable +     function="SpellCheck.EnableAddToIgnore" /> +  </menu_item_call> +  <menu_item_separator +   layout="topleft" +   name="Spellcheck Separator" /> +  <menu_item_call     label="Cut"     layout="topleft"     name="Cut"> | 
