diff options
Diffstat (limited to 'indra/llui')
| -rw-r--r-- | indra/llui/CMakeLists.txt | 6 | ||||
| -rwxr-xr-x[-rw-r--r--] | indra/llui/llcontainerview.cpp | 0 | ||||
| -rw-r--r-- | indra/llui/llhandle.h | 181 | ||||
| -rw-r--r-- | indra/llui/lllineeditor.cpp | 306 | ||||
| -rw-r--r-- | indra/llui/lllineeditor.h | 33 | ||||
| -rw-r--r-- | indra/llui/llmenugl.cpp | 10 | ||||
| -rw-r--r-- | indra/llui/llmenugl.h | 6 | ||||
| -rw-r--r-- | indra/llui/llnotifications.cpp | 4 | ||||
| -rw-r--r-- | indra/llui/llnotificationtemplate.h | 2 | ||||
| -rw-r--r-- | indra/llui/llscrollcontainer.cpp | 3 | ||||
| -rw-r--r-- | indra/llui/llscrolllistctrl.cpp | 21 | ||||
| -rw-r--r-- | indra/llui/llscrolllistctrl.h | 2 | ||||
| -rw-r--r-- | indra/llui/llspellcheck.cpp | 505 | ||||
| -rw-r--r-- | indra/llui/llspellcheck.h | 93 | ||||
| -rw-r--r-- | indra/llui/llspellcheckmenuhandler.h | 46 | ||||
| -rw-r--r-- | indra/llui/lltextbase.cpp | 273 | ||||
| -rw-r--r-- | indra/llui/lltextbase.h | 31 | ||||
| -rw-r--r-- | indra/llui/lltextbox.cpp | 2 | ||||
| -rw-r--r-- | indra/llui/lltexteditor.cpp | 38 | ||||
| -rw-r--r-- | indra/llui/llui.cpp | 6 | ||||
| -rw-r--r-- | indra/llui/lluictrlfactory.h | 14 | ||||
| -rw-r--r-- | indra/llui/llviewmodel.h | 1 | 
22 files changed, 1333 insertions, 250 deletions
| diff --git a/indra/llui/CMakeLists.txt b/indra/llui/CMakeLists.txt index 20c3456a56..d92b6aa1c0 100644 --- a/indra/llui/CMakeLists.txt +++ b/indra/llui/CMakeLists.txt @@ -23,6 +23,7 @@ include_directories(      ${LLWINDOW_INCLUDE_DIRS}      ${LLVFS_INCLUDE_DIRS}      ${LLXML_INCLUDE_DIRS} +    ${LIBS_PREBUILD_DIR}/include/hunspell      )  set(llui_SOURCE_FILES @@ -84,6 +85,7 @@ set(llui_SOURCE_FILES      llsearcheditor.cpp      llslider.cpp      llsliderctrl.cpp +    llspellcheck.cpp      llspinctrl.cpp      llstatbar.cpp      llstatgraph.cpp @@ -153,7 +155,6 @@ set(llui_HEADER_FILES      llflyoutbutton.h       llfocusmgr.h      llfunctorregistry.h -    llhandle.h      llhelp.h      lliconctrl.h      llkeywords.h @@ -191,6 +192,8 @@ set(llui_HEADER_FILES      llscrolllistitem.h      llsliderctrl.h      llslider.h +    llspellcheck.h +    llspellcheckmenuhandler.h      llspinctrl.h      llstatbar.h      llstatgraph.h @@ -260,6 +263,7 @@ target_link_libraries(llui      ${LLXUIXML_LIBRARIES}      ${LLXML_LIBRARIES}      ${LLMATH_LIBRARIES} +    ${HUNSPELL_LIBRARY}      ${LLCOMMON_LIBRARIES} # must be after llimage, llwindow, llrender      ) diff --git a/indra/llui/llcontainerview.cpp b/indra/llui/llcontainerview.cpp index e08ccb0b78..e08ccb0b78 100644..100755 --- a/indra/llui/llcontainerview.cpp +++ b/indra/llui/llcontainerview.cpp diff --git a/indra/llui/llhandle.h b/indra/llui/llhandle.h deleted file mode 100644 index 37c657dd92..0000000000 --- a/indra/llui/llhandle.h +++ /dev/null @@ -1,181 +0,0 @@ -/**  -* @file llhandle.h -* @brief "Handle" to an object (usually a floater) whose lifetime you don't -* control. -* -* $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 LLHANDLE_H -#define LLHANDLE_H - -#include "llpointer.h" -#include <boost/type_traits/is_convertible.hpp> -#include <boost/utility/enable_if.hpp> - -class LLTombStone : public LLRefCount -{ -public: -	LLTombStone(void* target = NULL) : mTarget(target) {} - -	void setTarget(void* target) { mTarget = target; } -	void* getTarget() const { return mTarget; } -private: -	mutable void* mTarget; -}; - -//	LLHandles are used to refer to objects whose lifetime you do not control or influence.   -//	Calling get() on a handle will return a pointer to the referenced object or NULL,  -//	if the object no longer exists.  Note that during the lifetime of the returned pointer,  -//	you are assuming that the object will not be deleted by any action you perform,  -//	or any other thread, as normal when using pointers, so avoid using that pointer outside of -//	the local code block. -//  -//  https://wiki.lindenlab.com/mediawiki/index.php?title=LLHandle&oldid=79669 - -template <typename T> -class LLHandle -{ -	template <typename U> friend class LLHandle; -	template <typename U> friend class LLHandleProvider; -public: -	LLHandle() : mTombStone(getDefaultTombStone()) {} - -	template<typename U> -	LLHandle(const LLHandle<U>& other, typename boost::enable_if< typename boost::is_convertible<U*, T*> >::type* dummy = 0) -	: mTombStone(other.mTombStone) -	{} - -	bool isDead() const  -	{  -		return mTombStone->getTarget() == NULL;  -	} - -	void markDead()  -	{  -		mTombStone = getDefaultTombStone(); -	} - -	T* get() const -	{ -		return reinterpret_cast<T*>(mTombStone->getTarget()); -	} - -	friend bool operator== (const LLHandle<T>& lhs, const LLHandle<T>& rhs) -	{ -		return lhs.mTombStone == rhs.mTombStone; -	} -	friend bool operator!= (const LLHandle<T>& lhs, const LLHandle<T>& rhs) -	{ -		return !(lhs == rhs); -	} -	friend bool	operator< (const LLHandle<T>& lhs, const LLHandle<T>& rhs) -	{ -		return lhs.mTombStone < rhs.mTombStone; -	} -	friend bool	operator> (const LLHandle<T>& lhs, const LLHandle<T>& rhs) -	{ -		return lhs.mTombStone > rhs.mTombStone; -	} - -protected: -	LLPointer<LLTombStone> mTombStone; - -private: -	typedef T* pointer_t; -	static LLPointer<LLTombStone>& getDefaultTombStone() -	{ -		static LLPointer<LLTombStone> sDefaultTombStone = new LLTombStone; -		return sDefaultTombStone; -	} -}; - -template <typename T> -class LLRootHandle : public LLHandle<T> -{ -public: -	typedef LLRootHandle<T> self_t; -	typedef LLHandle<T> base_t; - -	LLRootHandle(T* object) { bind(object); } -	LLRootHandle() {}; -	~LLRootHandle() { unbind(); } - -	// this is redundant, since an LLRootHandle *is* an LLHandle -	//LLHandle<T> getHandle() { return LLHandle<T>(*this); } - -	void bind(T* object)  -	{  -		// unbind existing tombstone -		if (LLHandle<T>::mTombStone.notNull()) -		{ -			if (LLHandle<T>::mTombStone->getTarget() == (void*)object) return; -			LLHandle<T>::mTombStone->setTarget(NULL); -		} -		// tombstone reference counted, so no paired delete -		LLHandle<T>::mTombStone = new LLTombStone((void*)object); -	} - -	void unbind()  -	{ -		LLHandle<T>::mTombStone->setTarget(NULL); -	} - -	//don't allow copying of root handles, since there should only be one -private: -	LLRootHandle(const LLRootHandle& other) {}; -}; - -// Use this as a mixin for simple classes that need handles and when you don't -// want handles at multiple points of the inheritance hierarchy -template <typename T> -class LLHandleProvider -{ -public: -	LLHandle<T> getHandle() const -	{  -		// perform lazy binding to avoid small tombstone allocations for handle -		// providers whose handles are never referenced -		mHandle.bind(static_cast<T*>(const_cast<LLHandleProvider<T>* >(this)));  -		return mHandle;  -	} - -protected: -	typedef LLHandle<T> handle_type_t; -	LLHandleProvider()  -	{ -		// provided here to enforce T deriving from LLHandleProvider<T> -	}  - -	template <typename U> -	LLHandle<U> getDerivedHandle(typename boost::enable_if< typename boost::is_convertible<U*, T*> >::type* dummy = 0) const -	{ -		LLHandle<U> downcast_handle; -		downcast_handle.mTombStone = getHandle().mTombStone; -		return downcast_handle; -	} - - -private: -	mutable LLRootHandle<T> mHandle; -}; - -#endif diff --git a/indra/llui/lllineeditor.cpp b/indra/llui/lllineeditor.cpp index d0fbf4b913..48d49af588 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), @@ -151,7 +157,8 @@ LLLineEditor::LLLineEditor(const LLLineEditor::Params& p)  	mHighlightColor(p.highlight_color()),  	mPreeditBgColor(p.preedit_bg_color()),  	mGLFont(p.font), -	mContextMenuHandle() +	mContextMenuHandle(), +	mAutoreplaceCallback()  {  	llassert( mMaxLengthBytes > 0 ); @@ -177,6 +184,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()); @@ -195,7 +208,6 @@ LLLineEditor::~LLLineEditor()  	gFocusMgr.releaseFocusIfNeeded( this );  } -  void LLLineEditor::onFocusReceived()  {  	gEditMenuHandler = this; @@ -519,6 +531,99 @@ 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)  { @@ -866,6 +971,12 @@ void LLLineEditor::addChar(const llwchar uni_char)  		LLUI::reportBadKeystroke();  	} +	if (!mReadOnly && mAutoreplaceCallback != NULL) +	{ +		// call callback +		mAutoreplaceCallback(mText, mCursorPos); +	} +  	getWindow()->hideCursorUntilMouseMove();  } @@ -1058,9 +1169,8 @@ void LLLineEditor::cut()  			LLUI::reportBadKeystroke();  		}  		else -		if( mKeystrokeCallback )  		{ -			mKeystrokeCallback( this ); +			onKeystroke();  		}  	}  } @@ -1187,9 +1297,8 @@ void LLLineEditor::pasteHelper(bool is_primary)  				LLUI::reportBadKeystroke();  			}  			else -			if( mKeystrokeCallback )  			{ -				mKeystrokeCallback( this ); +				onKeystroke();  			}  		}  	} @@ -1442,9 +1551,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);  				}  			}  		} @@ -1497,12 +1607,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; @@ -1531,9 +1640,7 @@ void LLLineEditor::doDelete()  			if (!prevalidateInput(text_to_delete))  			{ -				if( mKeystrokeCallback ) -					mKeystrokeCallback( this ); - +				onKeystroke();  				return;  			}  			setCursor(getCursor() + 1); @@ -1549,10 +1656,9 @@ void LLLineEditor::doDelete()  		}  		else  		{ -			if( mKeystrokeCallback ) -			{ -				mKeystrokeCallback( this ); -			} +			onKeystroke(); + +			mSpellCheckTimer.setTimerExpirySec(SPELLCHECK_DELAY);  		}  	}  } @@ -1624,6 +1730,10 @@ void LLLineEditor::draw()  	background.stretch( -mBorderThickness );  	S32 lineeditor_v_pad = (background.getHeight() - mGLFont->getLineHeight()) / 2; +	if (mSpellCheck) +	{ +		lineeditor_v_pad += 1; +	}  	drawBackground(); @@ -1698,14 +1808,14 @@ void LLLineEditor::draw()  	{  		S32 select_left;  		S32 select_right; -		if( mSelectionStart < getCursor() ) +		if (mSelectionStart < mSelectionEnd)  		{  			select_left = mSelectionStart; -			select_right = getCursor(); +			select_right = mSelectionEnd;  		}  		else  		{ -			select_left = getCursor(); +			select_left = mSelectionEnd;  			select_right = mSelectionStart;  		} @@ -1749,7 +1859,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, @@ -1763,7 +1873,7 @@ void LLLineEditor::draw()  	}  	else  	{ -		mGLFont->render(  +		rendered_text = mGLFont->render(   			mText, mScrollHPos,   			rendered_pixels_right, text_bottom,  			text_color, @@ -1778,6 +1888,101 @@ 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; +			} + +			S32 pxBottom = (S32)(text_bottom + mGLFont->getDescenderHeight()); + +			gGL.color4ub(255, 0, 0, 200); +			while (pxStart + 1 < pxEnd) +			{ +				gl_line_2d(pxStart, pxBottom, pxStart + 2, pxBottom - 2); +				if (pxStart + 3 < pxEnd) +				{ +					gl_line_2d(pxStart + 2, pxBottom - 3, pxStart + 4, pxBottom - 1); +				} +				pxStart += 4; +			} +		} +	} +  	// If we're editing...  	if( hasFocus())  	{ @@ -2109,6 +2314,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)  { @@ -2231,10 +2445,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 @@ -2386,7 +2599,38 @@ 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..71dd53f608 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); @@ -169,6 +189,9 @@ public:  	virtual BOOL	setTextArg( const std::string& key, const LLStringExplicit& text );  	virtual BOOL	setLabelArg( const std::string& key, const LLStringExplicit& text ); +	typedef boost::function<void(LLUIString&, S32&)> autoreplace_callback_t; +	autoreplace_callback_t mAutoreplaceCallback; +	void			setAutoreplaceCallback(autoreplace_callback_t cb) { mAutoreplaceCallback = cb; }  	void			setLabel(const LLStringExplicit &new_label) { mLabel = new_label; }  	const std::string& 	getLabel()	{ return mLabel.getString(); } @@ -223,6 +246,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 +346,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 ff6928ffda..efb9848a90 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,14 @@ 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/llnotifications.cpp b/indra/llui/llnotifications.cpp index 09480968a6..629eef2c3b 100644 --- a/indra/llui/llnotifications.cpp +++ b/indra/llui/llnotifications.cpp @@ -1488,6 +1488,10 @@ bool LLNotifications::loadTemplates()  			{  				replaceFormText(notification.form_ref.form, "$canceltext", notification.form_ref.form_template.cancel_text);  			} +			if(notification.form_ref.form_template.help_text.isProvided()) +			{ +				replaceFormText(notification.form_ref.form, "$helptext", notification.form_ref.form_template.help_text); +			}  			if(notification.form_ref.form_template.ignore_text.isProvided())  			{  				replaceFormText(notification.form_ref.form, "$ignoretext", notification.form_ref.form_template.ignore_text); diff --git a/indra/llui/llnotificationtemplate.h b/indra/llui/llnotificationtemplate.h index 72973789db..b3b0bae862 100644 --- a/indra/llui/llnotificationtemplate.h +++ b/indra/llui/llnotificationtemplate.h @@ -121,6 +121,7 @@ struct LLNotificationTemplate  		Optional<std::string>	yes_text,  								no_text,  								cancel_text, +								help_text,  								ignore_text;  		TemplateRef() @@ -128,6 +129,7 @@ struct LLNotificationTemplate  			yes_text("yestext"),  			no_text("notext"),  			cancel_text("canceltext"), +			help_text("helptext"),  			ignore_text("ignoretext")  		{}  	}; diff --git a/indra/llui/llscrollcontainer.cpp b/indra/llui/llscrollcontainer.cpp index 9b7e30bb04..2fd187a526 100644 --- a/indra/llui/llscrollcontainer.cpp +++ b/indra/llui/llscrollcontainer.cpp @@ -389,12 +389,11 @@ void LLScrollContainer::calcVisibleSize( S32 *visible_width, S32 *visible_height  		{  			*show_h_scrollbar = TRUE;  			*visible_height -= scrollbar_size; - +			// Note: Do *not* recompute *show_v_scrollbar here because with  			// The view inside the scroll container should not be extended  			// to container's full height to ensure the correct computation  			// of *show_v_scrollbar after subtracting horizontal scrollbar_size. -			// Must retest now that visible_height has changed  			if( !*show_v_scrollbar && ((doc_height - *visible_height) > 1) )  			{  				*show_v_scrollbar = TRUE; diff --git a/indra/llui/llscrolllistctrl.cpp b/indra/llui/llscrolllistctrl.cpp index b3e1b63db5..b3499693dd 100644 --- a/indra/llui/llscrolllistctrl.cpp +++ b/indra/llui/llscrolllistctrl.cpp @@ -389,6 +389,22 @@ std::vector<LLScrollListItem*> LLScrollListCtrl::getAllSelected() const  	return ret;  } +S32 LLScrollListCtrl::getNumSelected() const +{ +	S32 numSelected = 0; + +	for(item_list::const_iterator iter = mItemList.begin(); iter != mItemList.end(); ++iter) +	{ +		LLScrollListItem* item  = *iter; +		if (item->getSelected()) +		{ +			++numSelected; +		} +	} + +	return numSelected; +} +  S32 LLScrollListCtrl::getFirstSelectedIndex() const  {  	S32 CurSelectedIndex = 0; @@ -2704,6 +2720,11 @@ BOOL LLScrollListCtrl::hasSortOrder() const  	return !mSortColumns.empty();  } +void LLScrollListCtrl::clearSortOrder() +{ +	mSortColumns.clear(); +} +  void LLScrollListCtrl::clearColumns()  {  	column_map_t::iterator itor; diff --git a/indra/llui/llscrolllistctrl.h b/indra/llui/llscrolllistctrl.h index ae8aea9245..e83794e173 100644 --- a/indra/llui/llscrolllistctrl.h +++ b/indra/llui/llscrolllistctrl.h @@ -257,6 +257,7 @@ public:  	LLScrollListItem*	getFirstSelected() const;  	virtual S32			getFirstSelectedIndex() const;  	std::vector<LLScrollListItem*> getAllSelected() const; +	S32                 getNumSelected() const;  	LLScrollListItem*	getLastSelectedItem() const { return mLastSelected; }  	// iterate over all items @@ -373,6 +374,7 @@ public:  	std::string     getSortColumnName();  	BOOL			getSortAscending() { return mSortColumns.empty() ? TRUE : mSortColumns.back().second; }  	BOOL			hasSortOrder() const; +	void			clearSortOrder();  	S32		selectMultiple( uuid_vec_t ids );  	// conceptually const, but mutates mItemList diff --git a/indra/llui/llspellcheck.cpp b/indra/llui/llspellcheck.cpp new file mode 100644 index 0000000000..a189375fbe --- /dev/null +++ b/indra/llui/llspellcheck.cpp @@ -0,0 +1,505 @@ +/**  + * @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_FILE_CUSTOM = "user_custom.dic"; +static const std::string DICT_FILE_IGNORE = "user_ignore.dic"; + +static const std::string DICT_FILE_MAIN = "dictionaries.xml"; +static const std::string DICT_FILE_USER = "user_dictionaries.xml"; + +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_language) +{ +	for (LLSD::array_const_iterator it = sDictMap.beginArray(); it != sDictMap.endArray(); ++it) +	{ +		const LLSD& dict_entry = *it; +		if (dict_language == dict_entry["language"].asString()) +		{ +			return dict_entry; +		} +	} +	return LLSD(); +} + +// static +bool LLSpellChecker::hasDictionary(const std::string& dict_language, bool check_installed) +{ +	const LLSD dict_info = getDictionaryData(dict_language); +	return dict_info.has("language") && ( (!check_installed) || (dict_info["installed"].asBoolean()) ); +} + +// static +void LLSpellChecker::setDictionaryData(const LLSD& dict_info) +{ +	const std::string dict_language = dict_info["language"].asString(); +	if (dict_language.empty()) +	{ +		return; +	} + +	for (LLSD::array_iterator it = sDictMap.beginArray(); it != sDictMap.endArray(); ++it) +	{ +		LLSD& dict_entry = *it; +		if (dict_language == dict_entry["language"].asString()) +		{ +			dict_entry = dict_info; +			return; +		} +	} +	sDictMap.append(dict_info); +	return; +} + +// 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_file(user_path + DICT_FILE_MAIN, std::ios::binary); +	if ( (!user_file.is_open()) || (0 == LLSDSerialize::fromXMLDocument(sDictMap, user_file)) || (0 == sDictMap.size()) ) +	{ +		llifstream app_file(app_path + DICT_FILE_MAIN, std::ios::binary); +		if ( (!app_file.is_open()) || (0 == LLSDSerialize::fromXMLDocument(sDictMap, app_file)) || (0 == sDictMap.size()) ) +		{ +			return; +		} +	} + +	// Load user installed dictionary information +	llifstream custom_file(user_path + DICT_FILE_USER, std::ios::binary); +	if (custom_file.is_open()) +	{ +		LLSD custom_dict_map; +		LLSDSerialize::fromXMLDocument(custom_dict_map, custom_file); +		for (LLSD::array_iterator it = custom_dict_map.beginArray(); it != custom_dict_map.endArray(); ++it) +		{ +			LLSD& dict_info = *it; +			dict_info["user_installed"] = true; +			setDictionaryData(dict_info); +		} +		custom_file.close(); +	} + +	// 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"))); +	} + +	sSettingsChangeSignal(); +} + +void LLSpellChecker::addToCustomDictionary(const std::string& word) +{ +	if (mHunspell) +	{ +		mHunspell->add(word.c_str()); +	} +	addToDictFile(getDictionaryUserPath() + DICT_FILE_CUSTOM, 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() + DICT_FILE_IGNORE, 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(); +	} +} + +bool LLSpellChecker::isActiveDictionary(const std::string& dict_language) const +{ +	return +		(mDictLanguage == dict_language) ||  +		(mDictSecondary.end() != std::find(mDictSecondary.begin(), mDictSecondary.end(), dict_language)); +} + +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_language = mDictLanguage; +		initHunspell(dict_language); +	} +	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_language) +{ +	if (mHunspell) +	{ +		delete mHunspell; +		mHunspell = NULL; +		mDictLanguage.clear(); +		mDictFile.clear(); +		mIgnoreList.clear(); +	} + +	const LLSD dict_entry = (!dict_language.empty()) ? getDictionaryData(dict_language) : 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; +		} + +		mDictLanguage = dict_language; +		mDictFile = dict_entry["name"].asString(); + +		if (gDirUtilp->fileExists(user_path + DICT_FILE_CUSTOM)) +		{ +			mHunspell->add_dic((user_path + DICT_FILE_CUSTOM).c_str()); +		} + +		if (gDirUtilp->fileExists(user_path + DICT_FILE_IGNORE)) +		{ +			llifstream file_in(user_path + DICT_FILE_IGNORE, 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 +bool LLSpellChecker::canRemoveDictionary(const std::string& dict_language) +{ +	// Only user-installed inactive dictionaries can be removed +	const LLSD dict_info = getDictionaryData(dict_language); +	return  +		(dict_info["user_installed"].asBoolean()) &&  +		( (!getUseSpellCheck()) || (!LLSpellChecker::instance().isActiveDictionary(dict_language)) ); +} + +// static +void LLSpellChecker::removeDictionary(const std::string& dict_language) +{ +	if (!canRemoveDictionary(dict_language)) +	{ +		return; +	} + +	LLSD dict_map = loadUserDictionaryMap(); +	for (LLSD::array_const_iterator it = dict_map.beginArray(); it != dict_map.endArray(); ++it) +	{ +		const LLSD& dict_info = *it; +		if (dict_info["language"].asString() == dict_language) +		{ +			const std::string dict_dic = getDictionaryUserPath() + dict_info["name"].asString() + ".dic"; +			if (gDirUtilp->fileExists(dict_dic)) +			{ +				LLFile::remove(dict_dic); +			} +			const std::string dict_aff = getDictionaryUserPath() + dict_info["name"].asString() + ".aff"; +			if (gDirUtilp->fileExists(dict_aff)) +			{ +				LLFile::remove(dict_aff); +			} +			dict_map.erase(it - dict_map.beginArray()); +			break; +		} +	} +	saveUserDictionaryMap(dict_map); + +	refreshDictionaryMap(); +} + +// static +LLSD LLSpellChecker::loadUserDictionaryMap() +{ +	LLSD dict_map; +	llifstream dict_file(getDictionaryUserPath() + DICT_FILE_USER, std::ios::binary); +	if (dict_file.is_open()) +	{ +		LLSDSerialize::fromXMLDocument(dict_map, dict_file); +		dict_file.close(); +	} +	return dict_map; +} + +// static +void LLSpellChecker::saveUserDictionaryMap(const LLSD& dict_map) +{ +	llofstream dict_file(getDictionaryUserPath() + DICT_FILE_USER, std::ios::trunc); +	if (dict_file.is_open()) +	{ +		LLSDSerialize::toPrettyXML(dict_map, dict_file); +		dict_file.close(); +	} +} + +// 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_language) +{ +	if ( (((dict_language.empty()) && (getUseSpellCheck())) || (!dict_language.empty())) &&  +		 (LLSpellChecker::instance().mDictLanguage != dict_language) ) +	{ +		LLSpellChecker::instance().initHunspell(dict_language); +	} +} + +// 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..4ab80195ea --- /dev/null +++ b/indra/llui/llspellcheck.h @@ -0,0 +1,93 @@ +/**  + * @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_language); + +public: +	typedef std::list<std::string> dict_list_t; + +	const std::string&	getPrimaryDictionary() const { return mDictLanguage; } +	const dict_list_t&	getSecondaryDictionaries() const { return mDictSecondary; } +	bool				isActiveDictionary(const std::string& dict_language) const; +	void				setSecondaryDictionaries(dict_list_t dict_list); + +	static bool				 canRemoveDictionary(const std::string& dict_language); +	static const std::string getDictionaryAppPath(); +	static const std::string getDictionaryUserPath(); +	static const LLSD		 getDictionaryData(const std::string& dict_language); +	static const LLSD&		 getDictionaryMap() { return sDictMap; } +	static bool				 getUseSpellCheck(); +	static bool				 hasDictionary(const std::string& dict_language, bool check_installed = false); +	static void				 refreshDictionaryMap(); +	static void				 removeDictionary(const std::string& dict_language); +	static void				 setUseSpellCheck(const std::string& dict_language); +protected: +	static LLSD				 loadUserDictionaryMap(); +	static void				 setDictionaryData(const LLSD& dict_info); +	static void				 saveUserDictionaryMap(const LLSD& dict_map); + +public: +	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	mDictLanguage; +	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 7aeeae298f..3815eec447 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(); @@ -280,12 +291,23 @@ bool LLTextBase::truncate()  	if (getLength() >= S32(mMaxTextByteLength / 4))  	{	  		// Have to check actual byte size -		LLWString text(getWText()); -		S32 utf8_byte_size = wstring_utf8_length(text); +		S32 utf8_byte_size = 0; +		LLSD value = getViewModel()->getValue(); +		if (value.type() == LLSD::TypeString) +		{ +			// save a copy for strings. +			utf8_byte_size = value.size(); +		} +		else +		{ +			// non string LLSDs need explicit conversion to string +			utf8_byte_size = value.asString().size(); +		} +  		if ( utf8_byte_size > mMaxTextByteLength )  		{  			// Truncate safely in UTF-8 -			std::string temp_utf8_text = wstring_to_utf8str(text); +			std::string temp_utf8_text = value.asString();  			temp_utf8_text = utf8str_truncate( temp_utf8_text, mMaxTextByteLength );  			LLWString text = utf8str_to_wstring( temp_utf8_text );  			// remove extra bit of current string, to preserve formatting, etc. @@ -530,8 +552,92 @@ 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 +672,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 +685,46 @@ 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; + +				S32 squiggle_bottom = text_rect.mBottom + (S32)cur_segment->getStyle()->getFont()->getDescenderHeight(); + +				gGL.color4ub(255, 0, 0, 200); +				while (squiggle_start + 1 < squiggle_end) +				{ +					gl_line_2d(squiggle_start, squiggle_bottom, squiggle_start + 2, squiggle_bottom - 2); +					if (squiggle_start + 3 < squiggle_end) +					{ +						gl_line_2d(squiggle_start + 2, squiggle_bottom - 3, squiggle_start + 4, squiggle_bottom - 1); +					} +					squiggle_start += 4; +				} + +				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(); @@ -592,8 +739,7 @@ void LLTextBase::drawText()  S32 LLTextBase::insertStringNoUndo(S32 pos, const LLWString &wstr, LLTextBase::segment_vec_t* segments )  { -	LLWString text(getWText()); -	S32 old_len = text.length();		// length() returns character length +	S32 old_len = getLength();		// length() returns character length  	S32 insert_len = wstr.length();  	pos = getEditableIndex(pos, true); @@ -653,8 +799,7 @@ S32 LLTextBase::insertStringNoUndo(S32 pos, const LLWString &wstr, LLTextBase::s  		}  	} -	text.insert(pos, wstr); -	getViewModel()->setDisplay(text); +	getViewModel()->getEditableDisplay().insert(pos, wstr);  	if ( truncate() )  	{ @@ -669,7 +814,6 @@ S32 LLTextBase::insertStringNoUndo(S32 pos, const LLWString &wstr, LLTextBase::s  S32 LLTextBase::removeStringNoUndo(S32 pos, S32 length)  { -	LLWString text(getWText());  	segment_set_t::iterator seg_iter = getSegIterContaining(pos);  	while(seg_iter != mSegments.end())  	{ @@ -715,8 +859,7 @@ S32 LLTextBase::removeStringNoUndo(S32 pos, S32 length)  		++seg_iter;  	} -	text.erase(pos, length); -	getViewModel()->setDisplay(text); +	getViewModel()->getEditableDisplay().erase(pos, length);  	// recreate default segment in case we erased everything  	createDefaultSegment(); @@ -733,9 +876,7 @@ S32 LLTextBase::overwriteCharNoUndo(S32 pos, llwchar wc)  	{  		return 0;  	} -	LLWString text(getWText()); -	text[pos] = wc; -	getViewModel()->setDisplay(text); +	getViewModel()->getEditableDisplay()[pos] = wc;  	onValueChange(pos, pos + 1);  	needsReflow(pos); @@ -1103,6 +1244,99 @@ 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() @@ -1685,6 +1919,8 @@ static LLUIImagePtr image_from_icon_name(const std::string& icon_name)  	}  } +static LLFastTimer::DeclareTimer FTM_PARSE_HTML("Parse HTML"); +  void LLTextBase::appendTextImpl(const std::string &new_text, const LLStyle::Params& input_params)  {  	LLStyle::Params style_params(input_params); @@ -1693,15 +1929,13 @@ void LLTextBase::appendTextImpl(const std::string &new_text, const LLStyle::Para  	S32 part = (S32)LLTextParser::WHOLE;  	if (mParseHTML && !style_params.is_link) // Don't search for URLs inside a link segment (STORM-358).  	{ +		LLFastTimer _(FTM_PARSE_HTML);  		S32 start=0,end=0;  		LLUrlMatch match;  		std::string text = new_text;  		while ( LLUrlRegistry::instance().findUrl(text, match,  				boost::bind(&LLTextBase::replaceUrl, this, _1, _2, _3)) )  		{ -			 -			LLTextUtil::processUrlMatch(&match,this); -  			start = match.getStart();  			end = match.getEnd()+1; @@ -1737,6 +1971,8 @@ void LLTextBase::appendTextImpl(const std::string &new_text, const LLStyle::Para  					}  			} +			LLTextUtil::processUrlMatch(&match,this); +  			// move on to the rest of the text after the Url  			if (end < (S32)text.length())   			{ @@ -1760,8 +1996,11 @@ void LLTextBase::appendTextImpl(const std::string &new_text, const LLStyle::Para  	}  } +static LLFastTimer::DeclareTimer FTM_APPEND_TEXT("Append Text"); +  void LLTextBase::appendText(const std::string &new_text, bool prepend_newline, const LLStyle::Params& input_params)  { +	LLFastTimer _(FTM_APPEND_TEXT);  	if (new_text.empty())   		return; 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/lltextbox.cpp b/indra/llui/lltextbox.cpp index 6a905b7ec0..11cfa1d263 100644 --- a/indra/llui/lltextbox.cpp +++ b/indra/llui/lltextbox.cpp @@ -150,7 +150,7 @@ S32 LLTextBox::getTextPixelHeight()  LLSD LLTextBox::getValue() const  { -	return LLSD(getText()); +	return getViewModel()->getValue();  }  BOOL LLTextBox::setTextArg( const std::string& key, const LLStringExplicit& text ) diff --git a/indra/llui/lltexteditor.cpp b/indra/llui/lltexteditor.cpp index 9720dded6c..144b6960a1 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  /////////////////////////////////////////////////////////////////// @@ -1953,7 +1955,38 @@ 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);  } @@ -2838,6 +2871,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/llui/llui.cpp b/indra/llui/llui.cpp index b5e27616b7..87bf518aa1 100644 --- a/indra/llui/llui.cpp +++ b/indra/llui/llui.cpp @@ -831,7 +831,11 @@ void gl_stippled_line_3d( const LLVector3& start, const LLVector3& end, const LL  	gGL.flush();  	glLineWidth(2.5f); -	glLineStipple(2, 0x3333 << shift); + +	if (!LLGLSLShader::sNoFixedFunction) +	{ +		glLineStipple(2, 0x3333 << shift); +	}  	gGL.begin(LLRender::LINES);  	{ diff --git a/indra/llui/lluictrlfactory.h b/indra/llui/lluictrlfactory.h index d612ad5005..4e54354731 100644 --- a/indra/llui/lluictrlfactory.h +++ b/indra/llui/lluictrlfactory.h @@ -31,18 +31,10 @@  #include "llinitparam.h"  #include "llregistry.h"  #include "llxuiparser.h" +#include "llstl.h"  class LLView; -// sort functor for typeid maps -struct LLCompareTypeID -{ -	bool operator()(const std::type_info* lhs, const std::type_info* rhs) const -	{ -		return lhs->before(*rhs); -	} -}; -  // lookup widget constructor funcs by widget name  template <typename DERIVED_TYPE>  class LLChildRegistry : public LLRegistrySingleton<std::string, LLWidgetCreatorFunc, DERIVED_TYPE> @@ -71,14 +63,14 @@ protected:  // lookup widget name by type  class LLWidgetNameRegistry  -:	public LLRegistrySingleton<const std::type_info*, std::string, LLWidgetNameRegistry , LLCompareTypeID> +:	public LLRegistrySingleton<const std::type_info*, std::string, LLWidgetNameRegistry>  {};  // lookup function for generating empty param block by widget type  // this is used for schema generation  //typedef const LLInitParam::BaseBlock& (*empty_param_block_func_t)();  //class LLDefaultParamBlockRegistry -//:	public LLRegistrySingleton<const std::type_info*, empty_param_block_func_t, LLDefaultParamBlockRegistry, LLCompareTypeID> +//:	public LLRegistrySingleton<const std::type_info*, empty_param_block_func_t, LLDefaultParamBlockRegistry>  //{};  extern LLFastTimer::DeclareTimer FTM_WIDGET_SETUP; diff --git a/indra/llui/llviewmodel.h b/indra/llui/llviewmodel.h index 763af5d8a2..ef2e314799 100644 --- a/indra/llui/llviewmodel.h +++ b/indra/llui/llviewmodel.h @@ -102,6 +102,7 @@ public:  	// New functions      /// Get the stored value in string form      const LLWString& getDisplay() const { return mDisplay; } +	LLWString& getEditableDisplay() { mDirty = true; mUpdateFromDisplay = true; return mDisplay; }      /**       * Set the display string directly (see LLTextEditor). What the user is | 
