diff options
Diffstat (limited to 'indra')
38 files changed, 1558 insertions, 32 deletions
| diff --git a/indra/cmake/Copy3rdPartyLibs.cmake b/indra/cmake/Copy3rdPartyLibs.cmake index 394db362b1..ebeae3e5be 100644 --- a/indra/cmake/Copy3rdPartyLibs.cmake +++ b/indra/cmake/Copy3rdPartyLibs.cmake @@ -41,6 +41,7 @@ if(WINDOWS)          libeay32.dll          libcollada14dom22-d.dll          glod.dll     +        libhunspell.dll          )      set(release_src_dir "${ARCH_PREBUILT_DIRS_RELEASE}") @@ -53,6 +54,7 @@ if(WINDOWS)          libeay32.dll          libcollada14dom22.dll          glod.dll +        libhunspell.dll          )      if(USE_GOOGLE_PERFTOOLS) @@ -215,6 +217,7 @@ elseif(DARWIN)      libllqtwebkit.dylib      libminizip.a          libndofdev.dylib +        libhunspell-1.3.dylib          libexception_handler.dylib      libcollada14dom.dylib         ) diff --git a/indra/cmake/ViewerMiscLibs.cmake b/indra/cmake/ViewerMiscLibs.cmake index df013b1665..f907181929 100644 --- a/indra/cmake/ViewerMiscLibs.cmake +++ b/indra/cmake/ViewerMiscLibs.cmake @@ -2,6 +2,7 @@  include(Prebuilt)  if (NOT STANDALONE) +  use_prebuilt_binary(libhunspell)    use_prebuilt_binary(libuuid)    use_prebuilt_binary(slvoice)    use_prebuilt_binary(fontconfig) 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/CMakeLists.txt b/indra/llui/CMakeLists.txt index 772f173f17..1377336bb4 100644 --- a/indra/llui/CMakeLists.txt +++ b/indra/llui/CMakeLists.txt @@ -87,6 +87,7 @@ set(llui_SOURCE_FILES      llsearcheditor.cpp      llslider.cpp      llsliderctrl.cpp +    llspellcheck.cpp      llspinctrl.cpp      llstatbar.cpp      llstatgraph.cpp @@ -192,6 +193,8 @@ set(llui_HEADER_FILES      llsdparam.h      llsliderctrl.h      llslider.h +    llspellcheck.h +    llspellcheckmenuhandler.h      llspinctrl.h      llstatbar.h      llstatgraph.h diff --git a/indra/llui/lllineeditor.cpp b/indra/llui/lllineeditor.cpp index 06dfc90d83..42cfc4cae9 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 spell checking 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,95 @@ 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 +	mMisspellRanges.clear(); +	mSpellCheckStart = mSpellCheckEnd = -1; +}  BOOL LLLineEditor::handleDoubleClick(S32 x, S32 y, MASK mask)  { @@ -1058,9 +1159,8 @@ void LLLineEditor::cut()  			LLUI::reportBadKeystroke();  		}  		else -		if( mKeystrokeCallback )  		{ -			mKeystrokeCallback( this ); +			onKeystroke();  		}  	}  } @@ -1194,9 +1294,8 @@ void LLLineEditor::pasteHelper(bool is_primary)  				LLUI::reportBadKeystroke();  			}  			else -			if( mKeystrokeCallback )  			{ -				mKeystrokeCallback( this ); +				onKeystroke();  			}  		}  	} @@ -1449,9 +1548,10 @@ BOOL LLLineEditor::handleKeyHere(KEY key, MASK mask )  			// Notify owner if requested  			if (!need_to_rollback && handled)  			{ -				if (mKeystrokeCallback) +				onKeystroke(); +				if ( (!selection_modified) && (KEY_BACKSPACE == key) )  				{ -					mKeystrokeCallback(this); +					mSpellCheckTimer.setTimerExpirySec(SPELLCHECK_DELAY);  				}  			}  		} @@ -1504,12 +1604,11 @@ BOOL LLLineEditor::handleUnicodeCharHere(llwchar uni_char)  		// Notify owner if requested  		if( !need_to_rollback && handled )  		{ -			if( mKeystrokeCallback ) -			{ -				// 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 -				mKeystrokeCallback( this ); -			} +			// 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; @@ -1538,9 +1637,7 @@ void LLLineEditor::doDelete()  			if (!prevalidateInput(text_to_delete))  			{ -				if( mKeystrokeCallback ) -					mKeystrokeCallback( this ); - +				onKeystroke();  				return;  			}  			setCursor(getCursor() + 1); @@ -1556,10 +1653,9 @@ void LLLineEditor::doDelete()  		}  		else  		{ -			if( mKeystrokeCallback ) -			{ -				mKeystrokeCallback( this ); -			} +			onKeystroke(); + +			mSpellCheckTimer.setTimerExpirySec(SPELLCHECK_DELAY);  		}  	}  } @@ -1756,7 +1852,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 +1866,7 @@ void LLLineEditor::draw()  	}  	else  	{ -		mGLFont->render(  +		rendered_text = mGLFont->render(   			mText, mScrollHPos,   			rendered_pixels_right, text_bottom,  			text_color, @@ -1785,6 +1881,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())  	{ @@ -2116,6 +2286,15 @@ 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)  { @@ -2238,10 +2417,9 @@ void LLLineEditor::updatePreedit(const LLWString &preedit_string,  	// Update of the preedit should be caused by some key strokes.  	mKeystrokeTimer.reset(); -	if( mKeystrokeCallback ) -	{ -		mKeystrokeCallback( this ); -	} +	onKeystroke(); + +	mSpellCheckTimer.setTimerExpirySec(SPELLCHECK_DELAY);  }  BOOL LLLineEditor::getPreeditLocation(S32 query_offset, LLCoordGL *coord, LLRect *bounds, LLRect *control) const @@ -2393,7 +2571,34 @@ 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); + +		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()) == 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..40f931ecc1 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); @@ -223,6 +243,7 @@ public:  	void			setSelectAllonFocusReceived(BOOL b);  	void			setSelectAllonCommit(BOOL b) { mSelectAllonCommit = b; } +	void			onKeystroke();  	typedef boost::function<void (LLLineEditor* caller, void* user_data)> callback_t;  	void			setKeystrokeCallback(callback_t callback, void* user_data); @@ -322,6 +343,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/llui/llmenugl.cpp b/indra/llui/llmenugl.cpp index 95ecbb1c94..2a65262bbb 100644 --- a/indra/llui/llmenugl.cpp +++ b/indra/llui/llmenugl.cpp @@ -3854,7 +3854,7 @@ void LLContextMenu::setVisible(BOOL visible)  }  // Takes cursor position in screen space? -void LLContextMenu::show(S32 x, S32 y) +void LLContextMenu::show(S32 x, S32 y, LLView* spawning_view)  {  	if (getChildList()->empty())  	{ @@ -3908,6 +3908,10 @@ void LLContextMenu::show(S32 x, S32 y)  	setRect(rect);  	arrange(); +	if (spawning_view) +		mSpawningViewHandle = spawning_view->getHandle(); +	else +		mSpawningViewHandle.markDead();  	LLView::setVisible(TRUE);  } diff --git a/indra/llui/llmenugl.h b/indra/llui/llmenugl.h index 36f3ba34b9..67b3e1fbe6 100644 --- a/indra/llui/llmenugl.h +++ b/indra/llui/llmenugl.h @@ -670,7 +670,7 @@ public:  	virtual void	draw				(); -	virtual void	show				(S32 x, S32 y); +	virtual void	show				(S32 x, S32 y, LLView* spawning_view = NULL);  	virtual void	hide				();  	virtual BOOL	handleHover			( S32 x, S32 y, MASK mask ); @@ -683,10 +683,14 @@ public:  			LLHandle<LLContextMenu> getHandle() { return getDerivedHandle<LLContextMenu>(); } +			LLView*	getSpawningView() const		{ return mSpawningViewHandle.get(); } +			void	setSpawningView(LLHandle<LLView> spawning_view) { mSpawningViewHandle = spawning_view; } +  protected:  	BOOL						mHoveredAnyItem;  	LLMenuItemGL*				mHoverItem;  	LLRootHandle<LLContextMenu>	mHandle; +	LLHandle<LLView>			mSpawningViewHandle;  }; diff --git a/indra/llui/llspellcheck.cpp b/indra/llui/llspellcheck.cpp new file mode 100644 index 0000000000..aa39e21a96 --- /dev/null +++ b/indra/llui/llspellcheck.cpp @@ -0,0 +1,353 @@ +/**  + * @file llspellcheck.cpp + * @brief Spell checking functionality + * + * $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$ + */ + +#include "linden_common.h" + +#include "lldir.h" +#include "llsdserialize.h" + +#include "llspellcheck.h" +#if LL_WINDOWS +	#include <hunspell/hunspelldll.h> +	#pragma comment(lib, "libhunspell.lib") +#else +	#include <hunspell/hunspell.hxx> +#endif + +static const std::string DICT_DIR = "dictionaries"; +static const std::string DICT_CUSTOM_SUFFIX = "_custom"; +static const std::string DICT_IGNORE_SUFFIX = "_ignore"; + +LLSD LLSpellChecker::sDictMap; +LLSpellChecker::settings_change_signal_t LLSpellChecker::sSettingsChangeSignal; + +LLSpellChecker::LLSpellChecker() +	: mHunspell(NULL) +{ +	// Load initial dictionary information +	refreshDictionaryMap(); +} + +LLSpellChecker::~LLSpellChecker() +{ +	delete mHunspell; +} + +bool LLSpellChecker::checkSpelling(const std::string& word) const +{ +	if ( (!mHunspell) || (word.length() < 3) || (0 != mHunspell->spell(word.c_str())) ) +	{ +		return true; +	} +	if (mIgnoreList.size() > 0) +	{ +		std::string word_lower(word); +		LLStringUtil::toLower(word_lower); +		return (mIgnoreList.end() != std::find(mIgnoreList.begin(), mIgnoreList.end(), word_lower)); +	} +	return false; +} + +S32 LLSpellChecker::getSuggestions(const std::string& word, std::vector<std::string>& suggestions) const +{ +	suggestions.clear(); +	if ( (!mHunspell) || (word.length() < 3) ) +		return 0; + +	char** suggestion_list; int suggestion_cnt = 0; +	if ( (suggestion_cnt = mHunspell->suggest(&suggestion_list, word.c_str())) != 0 ) +	{ +		for (int suggestion_index = 0; suggestion_index < suggestion_cnt; suggestion_index++) +			suggestions.push_back(suggestion_list[suggestion_index]); +		mHunspell->free_list(&suggestion_list, suggestion_cnt);	 +	} +	return suggestions.size(); +} + +// static +const LLSD LLSpellChecker::getDictionaryData(const std::string& dict_name) +{ +	for (LLSD::array_const_iterator it = sDictMap.beginArray(); it != sDictMap.endArray(); ++it) +	{ +		const LLSD& dict_entry = *it; +		if (dict_name == dict_entry["language"].asString()) +			return dict_entry; +	} +	return LLSD(); +} + +// static +void LLSpellChecker::refreshDictionaryMap() +{ +	const std::string app_path = getDictionaryAppPath(); +	const std::string user_path = getDictionaryUserPath(); + +	// Load dictionary information (file name, friendly name, ...) +	llifstream user_map(user_path + "dictionaries.xml", std::ios::binary); +	if ( (!user_map.is_open()) || (0 == LLSDSerialize::fromXMLDocument(sDictMap, user_map)) || (0 == sDictMap.size()) ) +	{ +		llifstream app_map(app_path + "dictionaries.xml", std::ios::binary); +		if ( (!app_map.is_open()) || (0 == LLSDSerialize::fromXMLDocument(sDictMap, app_map)) || (0 == sDictMap.size()) ) +			return; +	} + +	// Look for installed dictionaries +	std::string tmp_app_path, tmp_user_path; +	for (LLSD::array_iterator it = sDictMap.beginArray(); it != sDictMap.endArray(); ++it) +	{ +		LLSD& sdDict = *it; +		tmp_app_path = (sdDict.has("name")) ? app_path + sdDict["name"].asString() : LLStringUtil::null; +		tmp_user_path = (sdDict.has("name")) ? user_path + sdDict["name"].asString() : LLStringUtil::null; +		sdDict["installed"] =  +			(!tmp_app_path.empty()) && ((gDirUtilp->fileExists(tmp_user_path + ".dic")) || (gDirUtilp->fileExists(tmp_app_path + ".dic"))); +		sdDict["has_custom"] = (!tmp_user_path.empty()) && (gDirUtilp->fileExists(tmp_user_path + DICT_CUSTOM_SUFFIX + ".dic")); +		sdDict["has_ignore"] = (!tmp_user_path.empty()) && (gDirUtilp->fileExists(tmp_user_path + DICT_IGNORE_SUFFIX + ".dic")); +	} +} + +void LLSpellChecker::addToCustomDictionary(const std::string& word) +{ +	if (mHunspell) +	{ +		mHunspell->add(word.c_str()); +	} +	addToDictFile(getDictionaryUserPath() + mDictFile + DICT_CUSTOM_SUFFIX + ".dic", word); +	sSettingsChangeSignal(); +} + +void LLSpellChecker::addToIgnoreList(const std::string& word) +{ +	std::string word_lower(word); +	LLStringUtil::toLower(word_lower); +	if (mIgnoreList.end() != std::find(mIgnoreList.begin(), mIgnoreList.end(), word_lower)) +	{ +		mIgnoreList.push_back(word_lower); +		addToDictFile(getDictionaryUserPath() + mDictFile + DICT_IGNORE_SUFFIX + ".dic", word_lower); +		sSettingsChangeSignal(); +	} +} + +void LLSpellChecker::addToDictFile(const std::string& dict_path, const std::string& word) +{ +	std::vector<std::string> word_list; + +	if (gDirUtilp->fileExists(dict_path)) +	{ +		llifstream file_in(dict_path, std::ios::in); +		if (file_in.is_open()) +		{ +			std::string word; int line_num = 0; +			while (getline(file_in, word)) +			{ +				// Skip over the first line since that's just a line count +				if (0 != line_num) +					word_list.push_back(word); +				line_num++; +			} +		} +		else +		{ +			// TODO: show error message? +			return; +		} +	} + +	word_list.push_back(word); + +	llofstream file_out(dict_path, std::ios::out | std::ios::trunc);	 +	if (file_out.is_open()) +	{ +		file_out << word_list.size() << std::endl; +		for (std::vector<std::string>::const_iterator itWord = word_list.begin(); itWord != word_list.end(); ++itWord) +			file_out << *itWord << std::endl; +		file_out.close(); +	} +} + +void LLSpellChecker::setSecondaryDictionaries(dict_list_t dict_list) +{ +	if (!getUseSpellCheck()) +	{ +		return; +	} + +	// Check if we're only adding secondary dictionaries, or removing them +	dict_list_t dict_add(llmax(dict_list.size(), mDictSecondary.size())), dict_rem(llmax(dict_list.size(), mDictSecondary.size())); +	dict_list.sort(); +	mDictSecondary.sort(); +	dict_list_t::iterator end_added = std::set_difference(dict_list.begin(), dict_list.end(), mDictSecondary.begin(), mDictSecondary.end(), dict_add.begin()); +	dict_list_t::iterator end_removed = std::set_difference(mDictSecondary.begin(), mDictSecondary.end(), dict_list.begin(), dict_list.end(), dict_rem.begin()); + +	if (end_removed != dict_rem.begin())		// We can't remove secondary dictionaries so we need to recreate the Hunspell instance +	{ +		mDictSecondary = dict_list; + +		std::string dict_name = mDictName; +		initHunspell(dict_name); +	} +	else if (end_added != dict_add.begin())		// Add the new secondary dictionaries one by one +	{ +		const std::string app_path = getDictionaryAppPath(); +		const std::string user_path = getDictionaryUserPath(); +		for (dict_list_t::const_iterator it_added = dict_add.begin(); it_added != end_added; ++it_added) +		{ +			const LLSD dict_entry = getDictionaryData(*it_added); +			if ( (!dict_entry.isDefined()) || (!dict_entry["installed"].asBoolean()) ) +				continue; + +			const std::string strFileDic = dict_entry["name"].asString() + ".dic"; +			if (gDirUtilp->fileExists(user_path + strFileDic)) +				mHunspell->add_dic((user_path + strFileDic).c_str()); +			else if (gDirUtilp->fileExists(app_path + strFileDic)) +				mHunspell->add_dic((app_path + strFileDic).c_str()); +		} +		mDictSecondary = dict_list; +		sSettingsChangeSignal(); +	} +} + +void LLSpellChecker::initHunspell(const std::string& dict_name) +{ +	if (mHunspell) +	{ +		delete mHunspell; +		mHunspell = NULL; +		mDictName.clear(); +		mDictFile.clear(); +		mIgnoreList.clear(); +	} + +	const LLSD dict_entry = (!dict_name.empty()) ? getDictionaryData(dict_name) : LLSD(); +	if ( (!dict_entry.isDefined()) || (!dict_entry["installed"].asBoolean()) || (!dict_entry["is_primary"].asBoolean())) +	{ +		sSettingsChangeSignal(); +		return; +	} + +	const std::string app_path = getDictionaryAppPath(); +	const std::string user_path = getDictionaryUserPath(); +	if (dict_entry.has("name")) +	{ +		const std::string filename_aff = dict_entry["name"].asString() + ".aff"; +		const std::string filename_dic = dict_entry["name"].asString() + ".dic"; +		if ( (gDirUtilp->fileExists(user_path + filename_aff)) && (gDirUtilp->fileExists(user_path + filename_dic)) ) +			mHunspell = new Hunspell((user_path + filename_aff).c_str(), (user_path + filename_dic).c_str()); +		else if ( (gDirUtilp->fileExists(app_path + filename_aff)) && (gDirUtilp->fileExists(app_path + filename_dic)) ) +			mHunspell = new Hunspell((app_path + filename_aff).c_str(), (app_path + filename_dic).c_str()); +		if (!mHunspell) +			return; + +		mDictName = dict_name; +		mDictFile = dict_entry["name"].asString(); + +		if (dict_entry["has_custom"].asBoolean()) +		{ +			const std::string filename_dic = user_path + mDictFile + DICT_CUSTOM_SUFFIX + ".dic"; +			mHunspell->add_dic(filename_dic.c_str()); +		} + +		if (dict_entry["has_ignore"].asBoolean()) +		{ +			llifstream file_in(user_path + mDictFile + DICT_IGNORE_SUFFIX + ".dic", std::ios::in); +			if (file_in.is_open()) +			{ +				std::string word; int idxLine = 0; +				while (getline(file_in, word)) +				{ +					// Skip over the first line since that's just a line count +					if (0 != idxLine) +					{ +						LLStringUtil::toLower(word); +						mIgnoreList.push_back(word); +					} +					idxLine++; +				} +			} +		} + +		for (dict_list_t::const_iterator it = mDictSecondary.begin(); it != mDictSecondary.end(); ++it) +		{ +			const LLSD dict_entry = getDictionaryData(*it); +			if ( (!dict_entry.isDefined()) || (!dict_entry["installed"].asBoolean()) ) +				continue; + +			const std::string filename_dic = dict_entry["name"].asString() + ".dic"; +			if (gDirUtilp->fileExists(user_path + filename_dic)) +				mHunspell->add_dic((user_path + filename_dic).c_str()); +			else if (gDirUtilp->fileExists(app_path + filename_dic)) +				mHunspell->add_dic((app_path + filename_dic).c_str()); +		} +	} + +	sSettingsChangeSignal(); +} + +// static +const std::string LLSpellChecker::getDictionaryAppPath() +{ +	std::string dict_path = gDirUtilp->getExpandedFilename(LL_PATH_APP_SETTINGS, DICT_DIR, ""); +	return dict_path; +} + +// static +const std::string LLSpellChecker::getDictionaryUserPath() +{ +	std::string dict_path = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, DICT_DIR, ""); +	if (!gDirUtilp->fileExists(dict_path)) +	{ +		LLFile::mkdir(dict_path); +	} +	return dict_path; +} + +// static +bool LLSpellChecker::getUseSpellCheck() +{ +	return (LLSpellChecker::instanceExists()) && (LLSpellChecker::instance().mHunspell); +} + +// static +boost::signals2::connection LLSpellChecker::setSettingsChangeCallback(const settings_change_signal_t::slot_type& cb) +{ +	return sSettingsChangeSignal.connect(cb); +} + +// static +void LLSpellChecker::setUseSpellCheck(const std::string& dict_name) +{ +	if ( (((dict_name.empty()) && (getUseSpellCheck())) || (!dict_name.empty())) &&  +		 (LLSpellChecker::instance().mDictName != dict_name) ) +	{ +		LLSpellChecker::instance().initHunspell(dict_name); +	} +} + +// static +void LLSpellChecker::initClass() +{ +	if (sDictMap.isUndefined()) +		refreshDictionaryMap(); +} diff --git a/indra/llui/llspellcheck.h b/indra/llui/llspellcheck.h new file mode 100644 index 0000000000..d736a7f082 --- /dev/null +++ b/indra/llui/llspellcheck.h @@ -0,0 +1,84 @@ +/**  + * @file llspellcheck.h + * @brief Spell checking functionality + * + * $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$ + */ + +#ifndef LLSPELLCHECK_H +#define LLSPELLCHECK_H + +#include "llsingleton.h" +#include "llui.h" +#include <boost/signals2.hpp> + +class Hunspell; + +class LLSpellChecker : public LLSingleton<LLSpellChecker>, public LLInitClass<LLSpellChecker> +{ +	friend class LLSingleton<LLSpellChecker>; +	friend class LLInitClass<LLSpellChecker>; +protected: +	LLSpellChecker(); +	~LLSpellChecker(); + +public: +	void addToCustomDictionary(const std::string& word); +	void addToIgnoreList(const std::string& word); +	bool checkSpelling(const std::string& word) const; +	S32  getSuggestions(const std::string& word, std::vector<std::string>& suggestions) const; +protected: +	void addToDictFile(const std::string& dict_path, const std::string& word); +	void initHunspell(const std::string& dict_name); + +public: +	typedef std::list<std::string> dict_list_t; + +	const std::string&	getActiveDictionary() const { return mDictName; } +	const dict_list_t&	getSecondaryDictionaries() const { return mDictSecondary; } +	void				setSecondaryDictionaries(dict_list_t dict_list); + +	static const std::string getDictionaryAppPath(); +	static const std::string getDictionaryUserPath(); +	static const LLSD		 getDictionaryData(const std::string& dict_name); +	static const LLSD&		 getDictionaryMap() { return sDictMap; } +	static bool				 getUseSpellCheck(); +	static void				 refreshDictionaryMap(); +	static void				 setUseSpellCheck(const std::string& dict_name); + +	typedef boost::signals2::signal<void()> settings_change_signal_t; +	static boost::signals2::connection setSettingsChangeCallback(const settings_change_signal_t::slot_type& cb); +protected: +	static void initClass(); + +protected: +	Hunspell*	mHunspell; +	std::string	mDictName; +	std::string	mDictFile; +	dict_list_t	mDictSecondary; +	std::vector<std::string> mIgnoreList; + +	static LLSD						sDictMap; +	static settings_change_signal_t	sSettingsChangeSignal; +}; + +#endif // LLSPELLCHECK_H diff --git a/indra/llui/llspellcheckmenuhandler.h b/indra/llui/llspellcheckmenuhandler.h new file mode 100644 index 0000000000..d5c95bad39 --- /dev/null +++ b/indra/llui/llspellcheckmenuhandler.h @@ -0,0 +1,46 @@ +/**  + * @file llspellcheckmenuhandler.h + * @brief Interface used by spell check menu handling + * + * $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$ + */ + +#ifndef LLSPELLCHECKMENUHANDLER_H +#define LLSPELLCHECKMENUHANDLER_H + +class LLSpellCheckMenuHandler +{ +public: +	virtual bool	getSpellCheck() const			{ return false; } + +	virtual const std::string& getSuggestion(U32 index) const	{ return LLStringUtil::null; } +	virtual U32		getSuggestionCount() const		{ return 0; } +	virtual void	replaceWithSuggestion(U32 index){} + +	virtual void	addToDictionary()				{} +	virtual bool	canAddToDictionary() const		{ return false; } + +	virtual void	addToIgnore()					{} +	virtual bool	canAddToIgnore() const			{ return false; } +}; + +#endif // LLSPELLCHECKMENUHANDLER_H diff --git a/indra/llui/lltextbase.cpp b/indra/llui/lltextbase.cpp index 0040be45c7..ce405cd4a4 100644 --- a/indra/llui/lltextbase.cpp +++ b/indra/llui/lltextbase.cpp @@ -32,6 +32,7 @@  #include "lllocalcliprect.h"  #include "llmenugl.h"  #include "llscrollcontainer.h" +#include "llspellcheck.h"  #include "llstl.h"  #include "lltextparser.h"  #include "lltextutil.h" @@ -155,6 +156,7 @@ LLTextBase::Params::Params()  	plain_text("plain_text",false),  	track_end("track_end", false),  	read_only("read_only", false), +	spellcheck("spellcheck", false),  	v_pad("v_pad", 0),  	h_pad("h_pad", 0),  	clip("clip", true), @@ -181,6 +183,9 @@ LLTextBase::LLTextBase(const LLTextBase::Params &p)  	mFontShadow(p.font_shadow),  	mPopupMenu(NULL),  	mReadOnly(p.read_only), +	mSpellCheck(p.spellcheck), +	mSpellCheckStart(-1), +	mSpellCheckEnd(-1),  	mCursorColor(p.cursor_color),  	mFgColor(p.text_color),  	mBorderVisible( p.border_visible ), @@ -246,6 +251,12 @@ LLTextBase::LLTextBase(const LLTextBase::Params &p)  		addChild(mDocumentView);  	} +	if (mSpellCheck) +	{ +		LLSpellChecker::setSettingsChangeCallback(boost::bind(&LLTextBase::onSpellCheckSettingsChange, this)); +	} +	mSpellCheckTimer.reset(); +  	createDefaultSegment();  	updateRects(); @@ -530,8 +541,86 @@ void LLTextBase::drawText()  		return;  	} +	// Perform spell check if needed +	if ( (getSpellCheck()) && (getWText().length() > 2) ) +	{ +		// Calculate start and end indices for the spell checking range +		S32 start = line_start, end = getLineEnd(last_line); + +		if ( (mSpellCheckStart != start) || (mSpellCheckEnd != end) ) +		{ +			const LLWString& wstrText = getWText();  +			mMisspellRanges.clear(); + +			segment_set_t::const_iterator seg_it = getSegIterContaining(start); +			while (mSegments.end() != seg_it) +			{ +				LLTextSegmentPtr text_segment = *seg_it; +				if ( (text_segment.isNull()) || (text_segment->getStart() >= end) ) +				{ +					break; +				} + +				if (!text_segment->canEdit()) +				{ +					++seg_it; +					continue; +				} + +				// Combine adjoining text segments into one +				U32 seg_start = text_segment->getStart(), seg_end = llmin(text_segment->getEnd(), end); +				while (mSegments.end() != ++seg_it) +				{ +					text_segment = *seg_it; +					if ( (text_segment.isNull()) || (!text_segment->canEdit()) || (text_segment->getStart() >= end) ) +					{ +						break; +					} +					seg_end = llmin(text_segment->getEnd(), end); +				} + +				// Find the start of the first word +				U32 word_start = seg_start, word_end = -1; +				while ( (word_start < wstrText.length()) && (!LLStringOps::isAlpha(wstrText[word_start])) ) +					word_start++; + +				// Iterate over all words in the text block and check them one by one +				while (word_start < seg_end) +				{ +					// 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 < seg_end) &&  +							((LLWStringUtil::isPartOfWord(wstrText[word_end])) || +								((L'\'' == wstrText[word_end]) &&  +								(LLStringOps::isAlnum(wstrText[word_end - 1])) && (LLStringOps::isAlnum(wstrText[word_end + 1])))) ) +					{ +						word_end++; +					} +					if (word_end > seg_end) +						break; + +					// Don't process words shorter than 3 characters +					std::string word = wstring_to_utf8str(wstrText.substr(word_start, word_end - word_start)); +					if ( (word.length() >= 3) && (!LLSpellChecker::instance().checkSpelling(word)) ) +					{ +						mMisspellRanges.push_back(std::pair<U32, U32>(word_start, word_end)); +					} + +					// Find the start of the next word +					word_start = word_end + 1; +					while ( (word_start < seg_end) && (!LLWStringUtil::isPartOfWord(wstrText[word_start])) ) +						word_start++; +				} +			} + +			mSpellCheckStart = start; +			mSpellCheckEnd = end; +		} +	} +  	LLTextSegmentPtr cur_segment = *seg_iter; +	std::list<std::pair<U32, U32> >::const_iterator misspell_it = std::lower_bound(mMisspellRanges.begin(), mMisspellRanges.end(), std::pair<U32, U32>(line_start, 0));  	for (S32 cur_line = first_line; cur_line < last_line; cur_line++)  	{  		S32 next_line = cur_line + 1; @@ -566,7 +655,8 @@ void LLTextBase::drawText()  				cur_segment = *seg_iter;  			} -			S32 clipped_end	=	llmin( line_end, cur_segment->getEnd() )  - cur_segment->getStart(); +			S32 seg_end = llmin(line_end, cur_segment->getEnd()); +			S32 clipped_end	= seg_end - cur_segment->getStart();  			if (mUseEllipses								// using ellipses  				&& clipped_end == line_end					// last segment on line @@ -578,6 +668,39 @@ void LLTextBase::drawText()  				text_rect.mRight -= 2;  			} +			// Draw squiggly lines under any visible misspelled words +			while ( (mMisspellRanges.end() != misspell_it) && (misspell_it->first < seg_end) && (misspell_it->second > seg_start) ) +			{ +				// Skip the current word if the user is still busy editing it +				if ( (!mSpellCheckTimer.hasExpired()) && (misspell_it->first <= (U32)mCursorPos) && (misspell_it->second >= (U32)mCursorPos) ) +				{ +					++misspell_it; + 					continue; +				} + +				U32 misspell_start = llmax<U32>(misspell_it->first, seg_start), misspell_end = llmin<U32>(misspell_it->second, seg_end); +				S32 squiggle_start = 0, squiggle_end = 0, pony = 0; +				cur_segment->getDimensions(seg_start - cur_segment->getStart(), misspell_start - seg_start, squiggle_start, pony); +				cur_segment->getDimensions(misspell_start - cur_segment->getStart(), misspell_end - misspell_start, squiggle_end, pony); +				squiggle_start += text_rect.mLeft; + +				pony = (squiggle_end + 3) / 6; +				squiggle_start += squiggle_end / 2 - pony * 3; +				squiggle_end = squiggle_start + pony * 6; + +				gGL.color4ub(255, 0, 0, 200); +				while (squiggle_start < squiggle_end) +				{ +					gl_line_2d(squiggle_start, text_rect.mBottom - 2, squiggle_start + 3, text_rect.mBottom + 1); +					gl_line_2d(squiggle_start + 3, text_rect.mBottom + 1, squiggle_start + 6, text_rect.mBottom - 2); +					squiggle_start += 6; +				} + +				if (misspell_it->second > seg_end) +					break; +				++misspell_it; +			} +  			text_rect.mLeft = (S32)(cur_segment->draw(seg_start - cur_segment->getStart(), clipped_end, selection_left, selection_right, text_rect));  			seg_start = clipped_end + cur_segment->getStart(); @@ -1103,6 +1226,95 @@ void LLTextBase::deselect()  	mIsSelecting = FALSE;  } +bool LLTextBase::getSpellCheck() const +{ +	return (LLSpellChecker::getUseSpellCheck()) && (!mReadOnly) && (mSpellCheck); +} + +const std::string& LLTextBase::getSuggestion(U32 index) const +{ +	return (index < mSuggestionList.size()) ? mSuggestionList[index] : LLStringUtil::null; +} + +U32 LLTextBase::getSuggestionCount() const +{ +	return mSuggestionList.size(); +} + +void LLTextBase::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 +			removeStringNoUndo(it->first, it->second - it->first); + +			// Insert the suggestion in its place +			LLWString suggestion = utf8str_to_wstring(mSuggestionList[index]); +			insertStringNoUndo(it->first, utf8str_to_wstring(mSuggestionList[index])); +			setCursorPos(it->first + (S32)suggestion.length()); + +			break; +		} +	} +	mSpellCheckStart = mSpellCheckEnd = -1; +} + +void LLTextBase::addToDictionary() +{ +	if (canAddToDictionary()) +	{ +		LLSpellChecker::instance().addToCustomDictionary(getMisspelledWord(mCursorPos)); +	} +} + +bool LLTextBase::canAddToDictionary() const +{ +	return (getSpellCheck()) && (isMisspelledWord(mCursorPos)); +} + +void LLTextBase::addToIgnore() +{ +	if (canAddToIgnore()) +	{ +		LLSpellChecker::instance().addToIgnoreList(getMisspelledWord(mCursorPos)); +	} +} + +bool LLTextBase::canAddToIgnore() const +{ +	return (getSpellCheck()) && (isMisspelledWord(mCursorPos)); +} + +std::string LLTextBase::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(getWText().substr(it->first, it->second - it->first)); +	} +	return LLStringUtil::null; +} + +bool LLTextBase::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 LLTextBase::onSpellCheckSettingsChange() +{ +	// Recheck the spelling on every change +	mMisspellRanges.clear(); +	mSpellCheckStart = mSpellCheckEnd = -1; +}  // Sets the scrollbar from the cursor position  void LLTextBase::updateScrollFromCursor() diff --git a/indra/llui/lltextbase.h b/indra/llui/lltextbase.h index 0549141b72..90b147cee1 100644 --- a/indra/llui/lltextbase.h +++ b/indra/llui/lltextbase.h @@ -30,6 +30,7 @@  #include "v4color.h"  #include "lleditmenuhandler.h" +#include "llspellcheckmenuhandler.h"  #include "llstyle.h"  #include "llkeywords.h"  #include "llpanel.h" @@ -230,7 +231,8 @@ typedef LLPointer<LLTextSegment> LLTextSegmentPtr;  ///  class LLTextBase   :	public LLUICtrl, -	protected LLEditMenuHandler +	protected LLEditMenuHandler, +	public LLSpellCheckMenuHandler  {  public:  	friend class LLTextSegment; @@ -259,6 +261,7 @@ public:  								border_visible,  								track_end,  								read_only, +								spellcheck,  								allow_scroll,  								plain_text,  								wrap, @@ -311,6 +314,24 @@ public:  	/*virtual*/ BOOL		canDeselect() const;  	/*virtual*/ void		deselect(); +	// 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(); +  	// used by LLTextSegment layout code  	bool					getWordWrap() { return mWordWrap; }  	bool					getUseEllipses() { return mUseEllipses; } @@ -540,6 +561,14 @@ protected:  	BOOL						mIsSelecting;		// Are we in the middle of a drag-select?  +	// spell checking +	bool						mSpellCheck; +	S32							mSpellCheckStart; +	S32							mSpellCheckEnd; +	LLTimer						mSpellCheckTimer; +	std::list<std::pair<U32, U32> > mMisspellRanges; +	std::vector<std::string>		mSuggestionList; +  	// configuration  	S32							mHPad;				// padding on left of text  	S32							mVPad;				// padding above text diff --git a/indra/llui/lltexteditor.cpp b/indra/llui/lltexteditor.cpp index 3a23ce1cac..c5957838ba 100644 --- a/indra/llui/lltexteditor.cpp +++ b/indra/llui/lltexteditor.cpp @@ -54,6 +54,7 @@  #include "llwindow.h"  #include "lltextparser.h"  #include "llscrollcontainer.h" +#include "llspellcheck.h"  #include "llpanel.h"  #include "llurlregistry.h"  #include "lltooltip.h" @@ -77,6 +78,7 @@ template class LLTextEditor* LLView::getChild<class LLTextEditor>(  const S32	UI_TEXTEDITOR_LINE_NUMBER_MARGIN = 32;  const S32	UI_TEXTEDITOR_LINE_NUMBER_DIGITS = 4;  const S32	SPACES_PER_TAB = 4; +const F32	SPELLCHECK_DELAY = 0.5f;	// delay between the last keypress and spell checking the word the cursor is on  /////////////////////////////////////////////////////////////////// @@ -1961,7 +1963,34 @@ void LLTextEditor::showContextMenu(S32 x, S32 y)  	S32 screen_x, screen_y;  	localPointToScreen(x, y, &screen_x, &screen_y); -	mContextMenu->show(screen_x, screen_y); + +	setCursorAtLocalPos(x, y, false); +	if (hasSelection()) +	{ +		if ( (mCursorPos < llmin(mSelectionStart, mSelectionEnd)) || (mCursorPos > llmax(mSelectionStart, mSelectionEnd)) ) +			deselect(); +		else +			setCursorPos(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()) == true) +		{ +			LLSpellChecker::instance().getSuggestions(misspelled_word, mSuggestionList); +		} +	} + +	mContextMenu->setItemVisible("Suggestion Separator", (use_spellcheck) && (!mSuggestionList.empty())); +	mContextMenu->setItemVisible("Add to Dictionary", (use_spellcheck) && (is_misspelled)); +	mContextMenu->setItemVisible("Add to Ignore", (use_spellcheck) && (is_misspelled)); +	mContextMenu->setItemVisible("Spellcheck Separator", (use_spellcheck) && (is_misspelled)); +	mContextMenu->show(screen_x, screen_y, this);  } @@ -2846,6 +2875,9 @@ void LLTextEditor::setKeystrokeCallback(const keystroke_signal_t::slot_type& cal  void LLTextEditor::onKeyStroke()  {  	mKeystrokeSignal(this); + +	mSpellCheckStart = mSpellCheckEnd = -1; +	mSpellCheckTimer.setTimerExpirySec(SPELLCHECK_DELAY);  }  //virtual diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index f85b943c70..0ec3e0e08a 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -71,6 +71,7 @@ include_directories(      ${LLLOGIN_INCLUDE_DIRS}      ${UPDATER_INCLUDE_DIRS}      ${LIBS_PREBUILT_DIR}/include/collada +    ${LIBS_PREBUILD_DIR}/include/hunspell      ${OPENAL_LIB_INCLUDE_DIRS}      ${LIBS_PREBUILT_DIR}/include/collada/1.4      ) @@ -1570,6 +1571,9 @@ if (WINDOWS)        ${SHARED_LIB_STAGING_DIR}/RelWithDebInfo/msvcp100.dll        ${SHARED_LIB_STAGING_DIR}/Debug/msvcr100d.dll        ${SHARED_LIB_STAGING_DIR}/Debug/msvcp100d.dll +      ${SHARED_LIB_STAGING_DIR}/Release/libhunspell.dll +      ${SHARED_LIB_STAGING_DIR}/RelWithDebInfo/libhunspell.dll +      ${SHARED_LIB_STAGING_DIR}/Debug/libhunspell.dll        ${SHARED_LIB_STAGING_DIR}/${CMAKE_CFG_INTDIR}/SLVoice.exe        ${SHARED_LIB_STAGING_DIR}/${CMAKE_CFG_INTDIR}/vivoxsdk.dll        ${SHARED_LIB_STAGING_DIR}/${CMAKE_CFG_INTDIR}/ortp.dll @@ -1749,6 +1753,7 @@ target_link_libraries(${VIEWER_BINARY_NAME}      ${LLMATH_LIBRARIES}      ${LLCOMMON_LIBRARIES}      ${NDOF_LIBRARY} +    ${HUNSPELL_LIBRARY}      ${viewer_LIBRARIES}      ${BOOST_PROGRAM_OPTIONS_LIBRARY}      ${BOOST_REGEX_LIBRARY} diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index 0e26013152..e15822ed1f 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -12148,6 +12148,28 @@        <key>Value</key>        <real>10.0</real>      </map> +    <key>SpellCheck</key> +    <map> +      <key>Comment</key> +      <string>Enable spellchecking on line and text editors</string> +      <key>Persist</key> +      <integer>1</integer> +      <key>Type</key> +      <string>Boolean</string> +      <key>Value</key> +      <integer>1</integer> +    </map> +    <key>SpellCheckDictionary</key> +    <map> +      <key>Comment</key> +      <string>Current primary and secondary dictionaries used for spell checking</string> +      <key>Persist</key> +      <integer>1</integer> +      <key>Type</key> +      <string>String</string> +      <key>Value</key> +      <string>English (United States)</string> +    </map>      <key>UseNewWalkRun</key>      <map>        <key>Comment</key> diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index 1174d108d2..5a68cd531e 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -93,6 +93,7 @@  #include "llsecondlifeurls.h"  #include "llupdaterservice.h"  #include "llcallfloater.h" +#include "llspellcheck.h"  // Linden library includes  #include "llavatarnamecache.h" @@ -114,6 +115,7 @@  // Third party library includes  #include <boost/bind.hpp>  #include <boost/foreach.hpp> +#include <boost/algorithm/string.hpp> @@ -2497,6 +2499,18 @@ bool LLAppViewer::initConfiguration()  		//gDirUtilp->setSkinFolder("default");      } +	if (gSavedSettings.getBOOL("SpellCheck")) +	{ +		std::list<std::string> dict_list; +		boost::split(dict_list, gSavedSettings.getString("SpellCheckDictionary"), boost::is_any_of(std::string(","))); +		if (!dict_list.empty()) +		{ +			LLSpellChecker::setUseSpellCheck(dict_list.front()); +			dict_list.pop_front(); +			LLSpellChecker::instance().setSecondaryDictionaries(dict_list); +		} +	} +      mYieldTime = gSavedSettings.getS32("YieldTime");  	// Read skin/branding settings if specified. diff --git a/indra/newview/llavatariconctrl.cpp b/indra/newview/llavatariconctrl.cpp index b539ac38ed..b539ac38ed 100755..100644 --- a/indra/newview/llavatariconctrl.cpp +++ b/indra/newview/llavatariconctrl.cpp diff --git a/indra/newview/llfloaterpreference.cpp b/indra/newview/llfloaterpreference.cpp index a333989e7e..c444d2bb6f 100755 --- a/indra/newview/llfloaterpreference.cpp +++ b/indra/newview/llfloaterpreference.cpp @@ -66,6 +66,7 @@  #include "llsky.h"  #include "llscrolllistctrl.h"  #include "llscrolllistitem.h" +#include "llspellcheck.h"  #include "llsliderctrl.h"  #include "lltabcontainer.h"  #include "lltrans.h" @@ -110,6 +111,8 @@  #include "lllogininstance.h"        // to check if logged in yet  #include "llsdserialize.h" +#include <boost/algorithm/string.hpp> +  const F32 MAX_USER_FAR_CLIP = 512.f;  const F32 MIN_USER_FAR_CLIP = 64.f;  const F32 BANDWIDTH_UPDATER_TIMEOUT = 0.5f; @@ -445,6 +448,11 @@ BOOL LLFloaterPreference::postBuild()  	getChild<LLComboBox>("language_combobox")->setCommitCallback(boost::bind(&LLFloaterPreference::onLanguageChange, this)); +	gSavedSettings.getControl("SpellCheck")->getSignal()->connect(boost::bind(&LLFloaterPreference::refreshDictLists, this, false)); +	getChild<LLUICtrl>("combo_spellcheck_dict")->setCommitCallback(boost::bind(&LLFloaterPreference::refreshDictLists, this, false)); +	getChild<LLUICtrl>("btn_spellcheck_moveleft")->setCommitCallback(boost::bind(&LLFloaterPreference::onClickDictMove, this, "list_spellcheck_active", "list_spellcheck_available")); +	getChild<LLUICtrl>("btn_spellcheck_moveright")->setCommitCallback(boost::bind(&LLFloaterPreference::onClickDictMove, this, "list_spellcheck_available", "list_spellcheck_active")); +  	// if floater is opened before login set default localized busy message  	if (LLStartUp::getStartupState() < STATE_STARTED)  	{ @@ -577,6 +585,24 @@ void LLFloaterPreference::apply()  		}  	} +	if (hasChild("check_spellcheck"), TRUE) +	{ +		std::list<std::string> list_dict; + +		LLComboBox* dict_combo = findChild<LLComboBox>("combo_spellcheck_dict"); +		const std::string dict_name = dict_combo->getSelectedItemLabel(); +		if (!dict_name.empty()) +		{ +			list_dict.push_back(dict_name); + +			LLScrollListCtrl* list_ctrl = findChild<LLScrollListCtrl>("list_spellcheck_active"); +			std::vector<LLScrollListItem*> list_items = list_ctrl->getAllData(); +			for (std::vector<LLScrollListItem*>::const_iterator item_it = list_items.begin(); item_it != list_items.end(); ++item_it) +				list_dict.push_back((*item_it)->getColumn(0)->getValue().asString()); +		} +		gSavedSettings.setString("SpellCheckDictionary", boost::join(list_dict, ",")); +	} +  	saveAvatarProperties();  	if (mClickActionDirty) @@ -687,6 +713,8 @@ void LLFloaterPreference::onOpen(const LLSD& key)  	// Load (double-)click to walk/teleport settings.  	updateClickActionControls(); +	refreshDictLists(true); +  	// Enabled/disabled popups, might have been changed by user actions  	// while preferences floater was closed.  	buildPopupLists(); @@ -865,6 +893,25 @@ void LLFloaterPreference::onNameTagOpacityChange(const LLSD& newvalue)  	}  } +void LLFloaterPreference::onClickDictMove(const std::string& from, const std::string& to) +{ +	LLScrollListCtrl* from_ctrl = findChild<LLScrollListCtrl>(from); +	LLScrollListCtrl* to_ctrl = findChild<LLScrollListCtrl>(to); + +	LLSD row; +	row["columns"][0]["column"] = "name"; +	row["columns"][0]["font"]["name"] = "SANSSERIF_SMALL"; +	row["columns"][0]["font"]["style"] = "NORMAL"; + +	std::vector<LLScrollListItem*> sel_items = from_ctrl->getAllSelected(); +	for (std::vector<LLScrollListItem*>::const_iterator sel_it = sel_items.begin(); sel_it != sel_items.end(); ++sel_it) +	{ +		row["columns"][0]["value"] = (*sel_it)->getColumn(0)->getValue(); +		to_ctrl->addElement(row); +	} +	from_ctrl->deleteSelectedItems(); +} +  void LLFloaterPreference::onClickSetCache()  {  	std::string cur_name(gSavedSettings.getString("CacheLocation")); @@ -930,6 +977,83 @@ void LLFloaterPreference::refreshSkin(void* data)  	self->getChild<LLRadioGroup>("skin_selection", true)->setValue(sSkin);  } +void LLFloaterPreference::refreshDictLists(bool from_settings) +{ +	bool enabled = gSavedSettings.getBOOL("SpellCheck"); +	getChild<LLUICtrl>("btn_spellcheck_moveleft")->setEnabled(enabled); +	getChild<LLUICtrl>("btn_spellcheck_moveright")->setEnabled(enabled); + +	// Populate the dictionary combobox +	LLComboBox* dict_combo = findChild<LLComboBox>("combo_spellcheck_dict"); +	std::string dict_cur = dict_combo->getSelectedItemLabel(); +	if ((dict_cur.empty() || from_settings) && (LLSpellChecker::getUseSpellCheck())) +		dict_cur = LLSpellChecker::instance().getActiveDictionary(); +	dict_combo->clearRows(); +	dict_combo->setEnabled(enabled); + +	const LLSD& dict_map = LLSpellChecker::getDictionaryMap(); +	if (dict_map.size()) +	{ +		for (LLSD::array_const_iterator dict_it = dict_map.beginArray(); dict_it != dict_map.endArray(); ++dict_it) +		{ +			const LLSD& dict = *dict_it; +			if ( (dict["installed"].asBoolean()) && (dict["is_primary"].asBoolean()) && (dict.has("language")) ) +				dict_combo->add(dict["language"].asString()); +		} +		if (!dict_combo->selectByValue(dict_cur)) +			dict_combo->clear(); +	} + +	// Populate the available and active dictionary list +	LLScrollListCtrl* avail_ctrl = findChild<LLScrollListCtrl>("list_spellcheck_available"); +	LLScrollListCtrl* active_ctrl = findChild<LLScrollListCtrl>("list_spellcheck_active"); + +	LLSpellChecker::dict_list_t active_list; +	if ( ((!avail_ctrl->getItemCount()) && (!active_ctrl->getItemCount())) || (from_settings) ) +	{ +		if (LLSpellChecker::getUseSpellCheck()) +			active_list = LLSpellChecker::instance().getSecondaryDictionaries(); +	} +	else +	{ +		std::vector<LLScrollListItem*> active_items = active_ctrl->getAllData(); +		for (std::vector<LLScrollListItem*>::const_iterator item_it = active_items.begin(); item_it != active_items.end(); ++item_it) +		{ +			std::string dict = (*item_it)->getColumn(0)->getValue().asString(); +			if (dict_cur != dict) +				active_list.push_back(dict); +		} +	} + +	LLSD row; +	row["columns"][0]["column"] = "name"; +	row["columns"][0]["font"]["name"] = "SANSSERIF_SMALL"; +	row["columns"][0]["font"]["style"] = "NORMAL"; + +	active_ctrl->clearRows(); +	active_ctrl->setEnabled(enabled); +	active_ctrl->sortByColumnIndex(0, true); +	for (LLSpellChecker::dict_list_t::const_iterator it = active_list.begin(); it != active_list.end(); ++it) +	{ +		row["columns"][0]["value"] = *it; +		active_ctrl->addElement(row); +	} +	active_list.push_back(dict_cur); + +	avail_ctrl->clearRows(); +	avail_ctrl->setEnabled(enabled); +	avail_ctrl->sortByColumnIndex(0, true); +	for (LLSD::array_const_iterator dict_it = dict_map.beginArray(); dict_it != dict_map.endArray(); ++dict_it) +	{ +		const LLSD& dict = *dict_it; +		if ( (dict["installed"].asBoolean()) && (dict.has("language")) &&  +			 (active_list.end() == std::find(active_list.begin(), active_list.end(), dict["language"].asString())) ) +		{ +			row["columns"][0]["value"] = dict["language"].asString(); +			avail_ctrl->addElement(row); +		} +	} +}  void LLFloaterPreference::buildPopupLists()  { diff --git a/indra/newview/llfloaterpreference.h b/indra/newview/llfloaterpreference.h index 7ee3294478..f75f71cc3d 100644 --- a/indra/newview/llfloaterpreference.h +++ b/indra/newview/llfloaterpreference.h @@ -121,6 +121,7 @@ public:  	void setCacheLocation(const LLStringExplicit& location); +	void onClickDictMove(const std::string& from, const std::string& to);  	void onClickSetCache();  	void onClickResetCache();  	void onClickSkin(LLUICtrl* ctrl,const LLSD& userdata); @@ -161,6 +162,7 @@ public:  	void getUIColor(LLUICtrl* ctrl, const LLSD& param);  	void buildPopupLists(); +	void refreshDictLists(bool from_settings);  	static void refreshSkin(void* data);  private:  	static std::string sSkin; diff --git a/indra/newview/llmeshrepository.cpp b/indra/newview/llmeshrepository.cpp index b02bf79a28..b02bf79a28 100755..100644 --- a/indra/newview/llmeshrepository.cpp +++ b/indra/newview/llmeshrepository.cpp diff --git a/indra/newview/llstartup.cpp b/indra/newview/llstartup.cpp index 3923b4510a..25a765dc8d 100644 --- a/indra/newview/llstartup.cpp +++ b/indra/newview/llstartup.cpp @@ -739,6 +739,7 @@ bool idle_startup()  		{  			display_startup();  			initialize_edit_menu(); +			initialize_spellcheck_menu();  			display_startup();  			init_menus();  			display_startup(); diff --git a/indra/newview/llviewercontrol.cpp b/indra/newview/llviewercontrol.cpp index 093b84413a..7b6dbfaa0b 100644 --- a/indra/newview/llviewercontrol.cpp +++ b/indra/newview/llviewercontrol.cpp @@ -71,8 +71,12 @@  #include "llpaneloutfitsinventory.h"  #include "llpanellogin.h"  #include "llpaneltopinfobar.h" +#include "llspellcheck.h"  #include "llupdaterservice.h" +// Third party library includes +#include <boost/algorithm/string.hpp> +  #ifdef TOGGLE_HACKED_GODLIKE_VIEWER  BOOL 				gHackGodmode = FALSE;  #endif @@ -498,6 +502,24 @@ bool handleForceShowGrid(const LLSD& newvalue)  	return true;  } +bool handleSpellCheckChanged() +{ +	if (gSavedSettings.getBOOL("SpellCheck")) +	{ +		std::list<std::string> dict_list; +		boost::split(dict_list, gSavedSettings.getString("SpellCheckDictionary"), boost::is_any_of(std::string(","))); +		if (!dict_list.empty()) +		{ +			LLSpellChecker::setUseSpellCheck(dict_list.front()); +			dict_list.pop_front(); +			LLSpellChecker::instance().setSecondaryDictionaries(dict_list); +			return true; +		} +	} +	LLSpellChecker::setUseSpellCheck(LLStringUtil::null); +	return true; +} +  bool toggle_agent_pause(const LLSD& newvalue)  {  	if ( newvalue.asBoolean() ) @@ -704,6 +726,8 @@ void settings_setup_listeners()  	gSavedSettings.getControl("UpdaterServiceSetting")->getSignal()->connect(boost::bind(&toggle_updater_service_active, _2));  	gSavedSettings.getControl("ForceShowGrid")->getSignal()->connect(boost::bind(&handleForceShowGrid, _2));  	gSavedSettings.getControl("RenderTransparentWater")->getSignal()->connect(boost::bind(&handleRenderTransparentWaterChanged, _2)); +	gSavedSettings.getControl("SpellCheck")->getSignal()->connect(boost::bind(&handleSpellCheckChanged)); +	gSavedSettings.getControl("SpellCheckDictionary")->getSignal()->connect(boost::bind(&handleSpellCheckChanged));  }  #if TEST_CACHED_CONTROL diff --git a/indra/newview/llviewermenu.cpp b/indra/newview/llviewermenu.cpp index 99540ccce9..e4cc26885d 100644 --- a/indra/newview/llviewermenu.cpp +++ b/indra/newview/llviewermenu.cpp @@ -81,6 +81,7 @@  #include "llrootview.h"  #include "llsceneview.h"  #include "llselectmgr.h" +#include "llspellcheckmenuhandler.h"  #include "llstatusbar.h"  #include "lltextureview.h"  #include "lltoolcomp.h" @@ -5011,6 +5012,78 @@ class LLEditDelete : public view_listener_t  	}  }; +void handle_spellcheck_replace_with_suggestion(const LLUICtrl* ctrl, const LLSD& param) +{ +	const LLContextMenu* menu = dynamic_cast<const LLContextMenu*>(ctrl->getParent()); +	LLSpellCheckMenuHandler* spellcheck_handler = (menu) ? dynamic_cast<LLSpellCheckMenuHandler*>(menu->getSpawningView()) : NULL; +	if ( (!spellcheck_handler) || (!spellcheck_handler->getSpellCheck()) ) +	{ +		return; +	} + +	U32 index = 0; +	if ( (!LLStringUtil::convertToU32(param.asString(), index)) || (index >= spellcheck_handler->getSuggestionCount()) ) +	{ +		return; +	} + +	spellcheck_handler->replaceWithSuggestion(index); +} + +bool visible_spellcheck_suggestion(LLUICtrl* ctrl, const LLSD& param) +{ +	LLMenuItemGL* item = dynamic_cast<LLMenuItemGL*>(ctrl); +	const LLContextMenu* menu = (item) ? dynamic_cast<const LLContextMenu*>(item->getParent()) : NULL; +	const LLSpellCheckMenuHandler* spellcheck_handler = (menu) ? dynamic_cast<const LLSpellCheckMenuHandler*>(menu->getSpawningView()) : NULL; +	if ( (!spellcheck_handler) || (!spellcheck_handler->getSpellCheck()) ) +	{ +		return false; +	} + +	U32 index = 0; +	if ( (!LLStringUtil::convertToU32(param.asString(), index)) || (index >= spellcheck_handler->getSuggestionCount()) ) +	{ +		return false; +	} + +	item->setLabel(spellcheck_handler->getSuggestion(index)); +	return true; +} + +void handle_spellcheck_add_to_dictionary(const LLUICtrl* ctrl) +{ +	const LLContextMenu* menu = dynamic_cast<const LLContextMenu*>(ctrl->getParent()); +	LLSpellCheckMenuHandler* spellcheck_handler = (menu) ? dynamic_cast<LLSpellCheckMenuHandler*>(menu->getSpawningView()) : NULL; +	if ( (spellcheck_handler) && (spellcheck_handler->canAddToDictionary()) ) +	{ +		spellcheck_handler->addToDictionary(); +	} +} + +bool enable_spellcheck_add_to_dictionary(const LLUICtrl* ctrl) +{ +	const LLContextMenu* menu = dynamic_cast<const LLContextMenu*>(ctrl->getParent()); +	const LLSpellCheckMenuHandler* spellcheck_handler = (menu) ? dynamic_cast<const LLSpellCheckMenuHandler*>(menu->getSpawningView()) : NULL; +	return (spellcheck_handler) && (spellcheck_handler->canAddToDictionary()); +} + +void handle_spellcheck_add_to_ignore(const LLUICtrl* ctrl) +{ +	const LLContextMenu* menu = dynamic_cast<const LLContextMenu*>(ctrl->getParent()); +	LLSpellCheckMenuHandler* spellcheck_handler = (menu) ? dynamic_cast<LLSpellCheckMenuHandler*>(menu->getSpawningView()) : NULL; +	if ( (spellcheck_handler) && (spellcheck_handler->canAddToIgnore()) ) +	{ +		spellcheck_handler->addToIgnore(); +	} +} + +bool enable_spellcheck_add_to_ignore(const LLUICtrl* ctrl) +{ +	const LLContextMenu* menu = dynamic_cast<const LLContextMenu*>(ctrl->getParent()); +	const LLSpellCheckMenuHandler* spellcheck_handler = (menu) ? dynamic_cast<const LLSpellCheckMenuHandler*>(menu->getSpawningView()) : NULL; +	return (spellcheck_handler) && (spellcheck_handler->canAddToIgnore()); +} +  bool enable_object_delete()  {  	bool new_value =  @@ -7960,6 +8033,19 @@ void initialize_edit_menu()  } +void initialize_spellcheck_menu() +{ +	LLUICtrl::CommitCallbackRegistry::Registrar& commit = LLUICtrl::CommitCallbackRegistry::currentRegistrar(); +	LLUICtrl::EnableCallbackRegistry::Registrar& enable = LLUICtrl::EnableCallbackRegistry::currentRegistrar(); + +	commit.add("SpellCheck.ReplaceWithSuggestion", boost::bind(&handle_spellcheck_replace_with_suggestion, _1, _2)); +	enable.add("SpellCheck.VisibleSuggestion", boost::bind(&visible_spellcheck_suggestion, _1, _2)); +	commit.add("SpellCheck.AddToDictionary", boost::bind(&handle_spellcheck_add_to_dictionary, _1)); +	enable.add("SpellCheck.EnableAddToDictionary", boost::bind(&enable_spellcheck_add_to_dictionary, _1)); +	commit.add("SpellCheck.AddToIgnore", boost::bind(&handle_spellcheck_add_to_ignore, _1)); +	enable.add("SpellCheck.EnableAddToIgnore", boost::bind(&enable_spellcheck_add_to_ignore, _1)); +} +  void initialize_menus()  {  	// A parameterized event handler used as ctrl-8/9/0 zoom controls below. diff --git a/indra/newview/llviewermenu.h b/indra/newview/llviewermenu.h index 87cb4efbc4..8c40762865 100644 --- a/indra/newview/llviewermenu.h +++ b/indra/newview/llviewermenu.h @@ -39,6 +39,7 @@ class LLObjectSelection;  class LLSelectNode;  void initialize_edit_menu(); +void initialize_spellcheck_menu();  void init_menus();  void cleanup_menus(); diff --git a/indra/newview/skins/default/textures/textures.xml b/indra/newview/skins/default/textures/textures.xml index e4a8622a4b..b09bf1d816 100644 --- a/indra/newview/skins/default/textures/textures.xml +++ b/indra/newview/skins/default/textures/textures.xml @@ -54,6 +54,8 @@ with the same filename but different name    <texture name="Arrow_Down" file_name="widgets/Arrow_Down.png"	preload="true" />    <texture name="Arrow_Up" file_name="widgets/Arrow_Up.png" preload="true" /> +  <texture name="Arrow_Left" file_name="widgets/Arrow_Left.png" preload="true" /> +  <texture name="Arrow_Right" file_name="widgets/Arrow_Right.png" preload="true" />    <texture name="AudioMute_Off" file_name="icons/AudioMute_Off.png" preload="false" />    <texture name="AudioMute_Over" file_name="icons/AudioMute_Over.png" preload="false" /> diff --git a/indra/newview/skins/default/textures/widgets/Arrow_Left.png b/indra/newview/skins/default/textures/widgets/Arrow_Left.pngBinary files differ new file mode 100644 index 0000000000..a424282839 --- /dev/null +++ b/indra/newview/skins/default/textures/widgets/Arrow_Left.png diff --git a/indra/newview/skins/default/textures/widgets/Arrow_Right.png b/indra/newview/skins/default/textures/widgets/Arrow_Right.pngBinary files differ new file mode 100644 index 0000000000..e32bee8f34 --- /dev/null +++ b/indra/newview/skins/default/textures/widgets/Arrow_Right.png diff --git a/indra/newview/skins/default/xui/en/floater_chat_bar.xml b/indra/newview/skins/default/xui/en/floater_chat_bar.xml index 63992462b3..32a2c26cf0 100644 --- a/indra/newview/skins/default/xui/en/floater_chat_bar.xml +++ b/indra/newview/skins/default/xui/en/floater_chat_bar.xml @@ -46,6 +46,7 @@          left="0"          max_length_bytes="1023"          name="chat_box" +        spellcheck="true"          text_pad_left="5"          text_pad_right="25"          tool_tip="Press Enter to say, Ctrl+Enter to shout" diff --git a/indra/newview/skins/default/xui/en/floater_im_session.xml b/indra/newview/skins/default/xui/en/floater_im_session.xml index ca73883e53..8a73335261 100644 --- a/indra/newview/skins/default/xui/en/floater_im_session.xml +++ b/indra/newview/skins/default/xui/en/floater_im_session.xml @@ -83,6 +83,7 @@           label="To"           layout="bottomleft"           name="chat_editor" +         spellcheck="true"           tab_group="3"           width="249">          </line_editor> diff --git a/indra/newview/skins/default/xui/en/floater_preferences.xml b/indra/newview/skins/default/xui/en/floater_preferences.xml index 402868bb97..eebc3a9cca 100644 --- a/indra/newview/skins/default/xui/en/floater_preferences.xml +++ b/indra/newview/skins/default/xui/en/floater_preferences.xml @@ -120,6 +120,12 @@           layout="topleft"           help_topic="preferences_advanced1_tab"           name="advanced1" /> +        <panel +         class="panel_preference" +         filename="panel_preferences_spellcheck.xml" +         label="Spell Check" +         layout="topleft" +         name="spell_check" />      </tab_container>  </floater> diff --git a/indra/newview/skins/default/xui/en/floater_preview_notecard.xml b/indra/newview/skins/default/xui/en/floater_preview_notecard.xml index be3b2d179d..2e1c8ce670 100644 --- a/indra/newview/skins/default/xui/en/floater_preview_notecard.xml +++ b/indra/newview/skins/default/xui/en/floater_preview_notecard.xml @@ -70,6 +70,7 @@       max_length="65536"       name="Notecard Editor"       parse_urls="false"  +     spellcheck="true"       tab_group="1"       top="46"       width="392" 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"> diff --git a/indra/newview/skins/default/xui/en/panel_edit_pick.xml b/indra/newview/skins/default/xui/en/panel_edit_pick.xml index 0faa1598b1..553c112e6f 100644 --- a/indra/newview/skins/default/xui/en/panel_edit_pick.xml +++ b/indra/newview/skins/default/xui/en/panel_edit_pick.xml @@ -134,6 +134,7 @@           top_pad="2"           max_length="1023"           name="pick_desc" +         spellcheck="true"           text_color="black"           word_wrap="true" />          <text diff --git a/indra/newview/skins/default/xui/en/panel_group_notices.xml b/indra/newview/skins/default/xui/en/panel_group_notices.xml index 607e1bb213..6d5fb51e85 100644 --- a/indra/newview/skins/default/xui/en/panel_group_notices.xml +++ b/indra/newview/skins/default/xui/en/panel_group_notices.xml @@ -141,6 +141,7 @@ Maximum 200 per group daily           max_length_bytes="63"           name="create_subject"           prevalidate_callback="ascii" +         spellcheck="true"           width="218" />          <text           follows="left|top" @@ -161,6 +162,7 @@ Maximum 200 per group daily           left_pad="3"           max_length="511"           name="create_message" +         spellcheck="true"           top_delta="0"           width="218"           word_wrap="true" /> @@ -309,6 +311,7 @@ Maximum 200 per group daily           left_pad="3"           max_length_bytes="63"           name="view_subject" +         spellcheck="true"           top_delta="-1"           visible="false"           width="200" /> @@ -333,6 +336,7 @@ Maximum 200 per group daily           right="-1"           max_length="511"           name="view_message" +         spellcheck="true"           top_delta="-40"           width="313"           word_wrap="true" /> diff --git a/indra/newview/skins/default/xui/en/panel_nearby_chat_bar.xml b/indra/newview/skins/default/xui/en/panel_nearby_chat_bar.xml index 21c627cdfb..6bc9c48729 100644 --- a/indra/newview/skins/default/xui/en/panel_nearby_chat_bar.xml +++ b/indra/newview/skins/default/xui/en/panel_nearby_chat_bar.xml @@ -19,6 +19,7 @@       left="0"       max_length_bytes="1023"       name="chat_box" +     spellcheck="true"       text_pad_left="5"       text_pad_right="25"       tool_tip="Press Enter to say, Ctrl+Enter to shout" diff --git a/indra/newview/skins/default/xui/en/panel_preferences_spellcheck.xml b/indra/newview/skins/default/xui/en/panel_preferences_spellcheck.xml new file mode 100644 index 0000000000..f1b16c5d0d --- /dev/null +++ b/indra/newview/skins/default/xui/en/panel_preferences_spellcheck.xml @@ -0,0 +1,136 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes" ?> +<panel + border="true" + follows="left|top|right|bottom" + height="408" + label="Spell Check" + layout="topleft" + left="102" + name="spellcheck" + top="1" + width="517"> +    <check_box +     control_name="SpellCheck" +     enabled="true" +     follows="top|left" +     height="16" +     label="Enable spell checking" +     layout="topleft" +     left="30" +     name="check_spellcheck" +     top="30" +     width="250" +    /> +    <text +     enabled_control="SpellCheck" +     follows="top|left" +     height="10" +     label="Logs:" +     layout="topleft" +     left="55" +     mouse_opaque="false" +     name="text_spellcheck_dict" +     top_pad="15" +     type="string" +     width="90" +    > +      Main dictionary : +    </text> +    <combo_box +     enabled_control="SpellCheck" +     follows="top|left" +     height="23" +     layout="topleft" +     left_pad="10" +     name="combo_spellcheck_dict" +     top_pad="-15" +     width="175" +    /> + +    <text +     enabled_control="SpellCheck" +     follows="top|left" +     height="10" +     label="Logs:" +     layout="topleft" +     left="55" +     mouse_opaque="false" +     name="text_spellcheck_additional" +     top_pad="15" +     type="string" +     width="190" +    > +      Additional dictionaries : +    </text> +    <text +     follows="top|left" +     height="12" +     layout="topleft" +     left="80" +     length="1" +     name="text_spellcheck_available" +     top_pad="10" +     type="string" +     width="175"> +        Available +    </text> +    <text +     follows="top|left" +     height="12" +     type="string" +     left_pad="45" +     length="1" +     layout="topleft" +     name="text_spellcheck_active" +     width="175"> +        Active +    </text> +    <scroll_list +     enabled_control="SpellCheck" +     follows="top|left" +     height="155" +     layout="topleft" +     left="80" +     multi_select="true" +     name="list_spellcheck_available" +     sort_column="0" +     sort_ascending="true"  +     width="175" /> +    <button +     enabled_control="SpellCheck" +     follows="top|left" +     height="26" +     image_overlay="Arrow_Right" +     hover_glow_amount="0.15" +     layout="topleft" +     left_pad="10" +     name="btn_spellcheck_moveright" +     top_delta="50" +     width="25"> +    </button> +    <button +     enabled_control="SpellCheck" +     follows="top|left" +     height="26" +     image_overlay="Arrow_Left" +     hover_glow_amount="0.15" +     layout="topleft" +     name="btn_spellcheck_moveleft" +     top_delta="30" +     width="25"> +    </button> +    <scroll_list +     enabled_control="SpellCheck" +     follows="top|left" +     height="155" +     layout="topleft" +     left_pad="10" +     multi_select="true" +     name="list_spellcheck_active" +     sort_column="0" +     sort_ascending="true"  +     top_pad="-105" +     width="175" +    /> + +</panel> diff --git a/indra/newview/viewer_manifest.py b/indra/newview/viewer_manifest.py index 0931c4ec9b..1b732676e4 100644 --- a/indra/newview/viewer_manifest.py +++ b/indra/newview/viewer_manifest.py @@ -91,6 +91,8 @@ class ViewerManifest(LLManifest):                  # ... and the entire windlight directory                  self.path("windlight") +                # ... and the pre-installed spell checking dictionaries +                self.path("dictionaries")                  self.end_prefix("app_settings")              if self.prefix(src="character"): @@ -393,6 +395,9 @@ class WindowsManifest(ViewerManifest):              self.path("ssleay32.dll")              self.path("libeay32.dll") +            # Hunspell +            self.path("libhunspell.dll") +              # For google-perftools tcmalloc allocator.              try:                  if self.args['configuration'].lower() == 'debug': @@ -659,6 +664,7 @@ class DarwinManifest(ViewerManifest):              # copy additional libs in <bundle>/Contents/MacOS/              self.path("../packages/lib/release/libndofdev.dylib", dst="Resources/libndofdev.dylib") +            self.path("../packages/lib/release/libhunspell-1.3.dylib", dst="Resources/libhunspell-1.3.dylib")              self.path("../viewer_components/updater/scripts/darwin/update_install", "MacOS/update_install") @@ -1053,6 +1059,8 @@ class Linux_i686Manifest(LinuxManifest):              self.path("libopenjpeg.so.1.4.0")              self.path("libopenjpeg.so.1")              self.path("libopenjpeg.so") +            self.path("libhunspell-1.3.so") +            self.path("libhunspell-1.3.so.0")              self.path("libalut.so")              self.path("libopenal.so", "libopenal.so.1")              self.path("libopenal.so", "libvivoxoal.so.1") # vivox's sdk expects this soname | 
