Commits

Scott Lawrence  committed 39add27 Merge

merge changes for storm-276

  • Participants
  • Parent commits 509af91, 8b1a648

Comments (0)

Files changed (49)

 oz_viewer-beta-review.login_channel = "Second Life Beta Viewer"
 oz_viewer-beta-review.email = oz@lindenlab.com
 
+oz_project-7.build_debug_release_separately = true
+oz_project-7.codeticket_add_context = false
+oz_project-7.email = "sldev@catznip.com oz@lindenlab.com"
+
 # =================================================================
 # asset delivery 2010 projects
 # =================================================================

File autobuild.xml

           </map>
         </map>
       </map>
+      <key>dictionaries</key>
+      <map>
+        <key>license</key>
+        <string>various open</string>
+        <key>license_file</key>
+        <string>LICENSES/dictionaries.txt</string>
+        <key>name</key>
+        <string>dictionaries</string>
+        <key>platforms</key>
+        <map>
+          <key>darwin</key>
+          <map>
+            <key>archive</key>
+            <map>
+              <key>hash</key>
+              <string>cefa829b9eca8f22ecd595921e189c11</string>
+              <key>url</key>
+              <string>http://automated-builds-secondlife-com.s3.amazonaws.com/hg/repo/oz_3p-dictionaries/rev/256063/arch/Darwin/installer/dictionaries-1-darwin-20120508.tar.bz2</string>
+            </map>
+            <key>name</key>
+            <string>darwin</string>
+          </map>
+          <key>linux</key>
+          <map>
+            <key>archive</key>
+            <map>
+              <key>hash</key>
+              <string>dbc8fd886fb639ddbd6614c00654e7b1</string>
+              <key>url</key>
+              <string>http://automated-builds-secondlife-com.s3.amazonaws.com/hg/repo/oz_3p-dictionaries/rev/256063/arch/Linux/installer/dictionaries-1-linux-20120508.tar.bz2</string>
+            </map>
+            <key>name</key>
+            <string>linux</string>
+          </map>
+          <key>windows</key>
+          <map>
+            <key>archive</key>
+            <map>
+              <key>hash</key>
+              <string>983fd8afd02b975f85603545983713ed</string>
+              <key>url</key>
+              <string>http://automated-builds-secondlife-com.s3.amazonaws.com/hg/repo/oz_3p-dictionaries/rev/256063/arch/CYGWIN/installer/dictionaries-1-windows-20120508.tar.bz2</string>
+            </map>
+            <key>name</key>
+            <string>windows</string>
+          </map>
+        </map>
+      </map>
       <key>elfio</key>
       <map>
         <key>license</key>
           </map>
         </map>
       </map>
+      <key>libhunspell</key>
+      <map>
+        <key>license</key>
+        <string>libhunspell</string>
+        <key>license_file</key>
+        <string>LICENSES/hunspell.txt</string>
+        <key>name</key>
+        <string>libhunspell</string>
+        <key>platforms</key>
+        <map>
+          <key>darwin</key>
+          <map>
+            <key>archive</key>
+            <map>
+              <key>hash</key>
+              <string>a3f0bea9e014773353e2c32223aae054</string>
+              <key>url</key>
+              <string>http://automated-builds-secondlife-com.s3.amazonaws.com/hg/repo/oz_3p-hunspell/rev/255153/arch/Darwin/installer/libhunspell-1.3.2-darwin-20120430.tar.bz2</string>
+            </map>
+            <key>name</key>
+            <string>darwin</string>
+          </map>
+          <key>linux</key>
+          <map>
+            <key>archive</key>
+            <map>
+              <key>hash</key>
+              <string>add23aa863505b1fc168d660ffc4be62</string>
+              <key>url</key>
+              <string>http://automated-builds-secondlife-com.s3.amazonaws.com/hg/repo/oz_3p-hunspell/rev/255153/arch/Linux/installer/libhunspell-1.3.2-linux-20120430.tar.bz2</string>
+            </map>
+            <key>name</key>
+            <string>linux</string>
+          </map>
+          <key>windows</key>
+          <map>
+            <key>archive</key>
+            <map>
+              <key>hash</key>
+              <string>82df72ddf653392e7a65c96c0e0c6b5d</string>
+              <key>url</key>
+              <string>http://automated-builds-secondlife-com.s3.amazonaws.com/hg/repo/oz_3p-hunspell/rev/255153/arch/CYGWIN/installer/libhunspell-1.3.2-windows-20120430.tar.bz2</string>
+            </map>
+            <key>name</key>
+            <string>windows</string>
+          </map>
+        </map>
+      </map>
       <key>libpng</key>
       <map>
         <key>license</key>
       <map>
         <key>license</key>
         <string>havok</string>
-        <key>license_file</key>
-        <string>on_file</string>
         <key>name</key>
         <string>llconvexdecomposition</string>
         <key>platforms</key>
     </map>
     <key>package_description</key>
     <map>
+      <key>description</key>
+      <string>Spell checking dictionaries</string>
+      <key>license</key>
+      <string>various open</string>
       <key>name</key>
-      <string>viewer_development</string>
+      <string>dictionaries</string>
       <key>platforms</key>
       <map>
         <key>common</key>
           <string>windows</string>
         </map>
       </map>
+      <key>version</key>
+      <string>1.0</string>
     </map>
     <key>type</key>
     <string>autobuild</string>
 # * The basic convention is that the build name can be mapped onto a mercurial URL,
 #   which is also used as the "branch" name.
 
+check_for()
+{
+    if [ -e "$2" ]; then found_dict='FOUND'; else found_dict='MISSING'; fi
+    echo "$1 ${found_dict} '$2' " 1>&2
+}
+
 build_dir_Darwin()
 {
   echo build-darwin-i386
     && [ -r "$master_message_template_checkout/message_template.msg" ] \
     && template_verifier_master_url="-DTEMPLATE_VERIFIER_MASTER_URL=file://$master_message_template_checkout/message_template.msg"
 
+    check_for "Before 'autobuild configure'" ${build_dir}/packages/dictionaries
+
     "$AUTOBUILD" configure -c $variant -- \
      -DPACKAGE:BOOL=ON \
      -DRELEASE_CRASH_REPORTING:BOOL=ON \
      -DGRID:STRING="\"$viewer_grid\"" \
      -DLL_TESTS:BOOL="$run_tests" \
      -DTEMPLATE_VERIFIER_OPTIONS:STRING="$template_verifier_options" $template_verifier_master_url
- end_section "Pre$variant"
+
+    check_for "After 'autobuild configure'" ${build_dir}/packages/dictionaries
+
+  end_section "Pre$variant"
 }
 
 build()
   if $build_viewer
   then
     begin_section "Viewer$variant"
+
+    check_for "Before 'autobuild build'" ${build_dir}/packages/dictionaries
+
     if "$AUTOBUILD" build --no-configure -c $variant
     then
       echo true >"$build_dir"/build_ok
     else
       echo false >"$build_dir"/build_ok
     fi
+    check_for "After 'autobuild configure'" ${build_dir}/packages/dictionaries
+
     end_section "Viewer$variant"
   fi
 }
 # dump environment variables for debugging
 env|sort
 
+check_for "Before 'autobuild install'" ${build_dir}/packages/dictionaries
 
+
+check_for "After 'autobuild install'" ${build_dir}/packages/dictionaries
 # Now run the build
 succeeded=true
 build_processes=

File indra/cmake/CMakeLists.txt

     GLOD.cmake
     GStreamer010Plugin.cmake
     GooglePerfTools.cmake
+    Hunspell.cmake
     JPEG.cmake
     LLAddBuildTest.cmake
     LLAudio.cmake

File indra/cmake/Copy3rdPartyLibs.cmake

         libeay32.dll
         libcollada14dom22-d.dll
         glod.dll    
+        libhunspell.dll
         )
 
     set(release_src_dir "${ARCH_PREBUILT_DIRS_RELEASE}")
         libeay32.dll
         libcollada14dom22.dll
         glod.dll
+        libhunspell.dll
         )
 
     if(USE_GOOGLE_PERFTOOLS)
         libexpat.1.5.2.dylib
         libexpat.dylib
         libGLOD.dylib
-    libllqtwebkit.dylib
-    libminizip.a
+        libllqtwebkit.dylib
+        libminizip.a
         libndofdev.dylib
+        libhunspell-1.3.0.dylib
         libexception_handler.dylib
-    libcollada14dom.dylib
+        libcollada14dom.dylib
        )
 
     # fmod is statically linked on darwin
         libdb-5.1.so
         libexpat.so
         libexpat.so.1
-    libglod.so
+        libglod.so
         libgmock_main.so
         libgmock.so.0
         libgmodule-2.0.so
         libgobject-2.0.so
         libgtest_main.so
         libgtest.so.0
-    libminizip.so
+        libhunspell-1.3.so.0.0.0
+        libminizip.so
         libopenal.so
         libopenjpeg.so
         libssl.so

File indra/cmake/FindHUNSPELL.cmake

+# -*- cmake -*-
+
+# - Find HUNSPELL
+# This module defines
+#  HUNSPELL_INCLUDE_DIR, where to find libhunspell.h, etc.
+#  HUNSPELL_LIBRARY, the library needed to use HUNSPELL.
+#  HUNSPELL_FOUND, If false, do not try to use HUNSPELL.
+
+find_path(HUNSPELL_INCLUDE_DIR hunspell.h
+  PATH_SUFFIXES hunspell
+  )
+
+set(HUNSPELL_NAMES ${HUNSPELL_NAMES} libhunspell-1.3.0 libhunspell)
+find_library(HUNSPELL_LIBRARY
+  NAMES ${HUNSPELL_NAMES}
+  )
+
+if (HUNSPELL_LIBRARY AND HUNSPELL_INCLUDE_DIR)
+  set(HUNSPELL_FOUND "YES")
+else (HUNSPELL_LIBRARY AND HUNSPELL_INCLUDE_DIR)
+  set(HUNSPELL_FOUND "NO")
+endif (HUNSPELL_LIBRARY AND HUNSPELL_INCLUDE_DIR)
+
+
+if (HUNSPELL_FOUND)
+  if (NOT HUNSPELL_FIND_QUIETLY)
+    message(STATUS "Found Hunspell: Library in '${HUNSPELL_LIBRARY}' and header in '${HUNSPELL_INCLUDE_DIR}' ")
+  endif (NOT HUNSPELL_FIND_QUIETLY)
+else (HUNSPELL_FOUND)
+  if (HUNSPELL_FIND_REQUIRED)
+    message(FATAL_ERROR " * * *\nCould not find HUNSPELL library! * * *")
+  endif (HUNSPELL_FIND_REQUIRED)
+endif (HUNSPELL_FOUND)
+
+mark_as_advanced(
+  HUNSPELL_LIBRARY
+  HUNSPELL_INCLUDE_DIR
+  )

File indra/cmake/Hunspell.cmake

+# -*- cmake -*-
+include(Prebuilt)
+
+set(HUNSPELL_FIND_QUIETLY ON)
+set(HUNSPELL_FIND_REQUIRED ON)
+
+if (STANDALONE)
+  include(FindHUNSPELL)
+else (STANDALONE)
+  use_prebuilt_binary(libhunspell)
+  if (WINDOWS)
+    set(HUNSPELL_LIBRARY libhunspell)
+  elseif(DARWIN)
+    set(HUNSPELL_LIBRARY hunspell-1.3.0)
+  elseif(LINUX)
+    set(HUNSPELL_LIBRARY hunspell-1.3)
+  else()
+    message(FATAL_ERROR "Invalid platform")
+  endif()
+  set(HUNSPELL_INCLUDE_DIRS ${LIBS_PREBUILT_DIR}/include/hunspell)
+  use_prebuilt_binary(dictionaries)
+endif (STANDALONE)

File indra/cmake/ViewerMiscLibs.cmake

 include(Prebuilt)
 
 if (NOT STANDALONE)
+  use_prebuilt_binary(libhunspell)
   use_prebuilt_binary(libuuid)
   use_prebuilt_binary(slvoice)
   use_prebuilt_binary(fontconfig)

File indra/integration_tests/llui_libtest/CMakeLists.txt

 include(LLVFS)        # ugh, needed for LLDir
 include(LLXML)
 include(LLXUIXML)
+include(Hunspell)
 include(Linking)
 # include(Tut)
 
     ${LLWINDOW_INCLUDE_DIRS}
     ${LLXML_INCLUDE_DIRS}
     ${LLXUIXML_INCLUDE_DIRS}
+    ${LIBS_PREBUILD_DIR}/include/hunspell
     )
 
 set(llui_libtest_SOURCE_FILES
     ${LLIMAGEJ2COJ_LIBRARIES}
     ${OS_LIBRARIES}
     ${GOOGLE_PERFTOOLS_LIBRARIES}
+    ${HUNSPELL_LIBRARY}
     )
 
 if (WINDOWS)

File indra/llcommon/llstring.h

 	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; }
 

File indra/llrender/llfontgl.cpp

 }
 
 // font metrics - override for LLFontFreetype that returns units of virtual pixels
+F32 LLFontGL::getAscenderHeight() const
+{ 
+	return mFontFreetype->getAscenderHeight() / sScaleY;
+}
+
+F32 LLFontGL::getDescenderHeight() const
+{ 
+	return mFontFreetype->getDescenderHeight() / sScaleY;
+}
+
 S32 LLFontGL::getLineHeight() const
 { 
 	return llceil(mFontFreetype->getAscenderHeight() / sScaleY) + llceil(mFontFreetype->getDescenderHeight() / sScaleY);

File indra/llrender/llfontgl.h

 	S32 renderUTF8(const std::string &text, S32 begin_offset, S32 x, S32 y, const LLColor4 &color, HAlign halign, VAlign valign, U8 style = NORMAL, ShadowType shadow = NO_SHADOW) const;
 
 	// font metrics - override for LLFontFreetype that returns units of virtual pixels
+	F32 getAscenderHeight() const;
+	F32 getDescenderHeight() const;
 	S32 getLineHeight() const;
 
 	S32 getWidth(const std::string& utf8text) const;

File indra/llui/CMakeLists.txt

     ${LLVFS_INCLUDE_DIRS}
     ${LLXML_INCLUDE_DIRS}
     ${LLXUIXML_INCLUDE_DIRS}
+    ${LIBS_PREBUILD_DIR}/include/hunspell
     )
 
 set(llui_SOURCE_FILES
     llsearcheditor.cpp
     llslider.cpp
     llsliderctrl.cpp
+    llspellcheck.cpp
     llspinctrl.cpp
     llstatbar.cpp
     llstatgraph.cpp
     llsdparam.h
     llsliderctrl.h
     llslider.h
+    llspellcheck.h
+    llspellcheckmenuhandler.h
     llspinctrl.h
     llstatbar.h
     llstatgraph.h
     ${LLXUIXML_LIBRARIES}
     ${LLXML_LIBRARIES}
     ${LLMATH_LIBRARIES}
+    ${HUNSPELL_LIBRARY}
     ${LLCOMMON_LIBRARIES} # must be after llimage, llwindow, llrender
     )
 

File indra/llui/lllineeditor.cpp

 #include "llkeyboard.h"
 #include "llrect.h"
 #include "llresmgr.h"
+#include "llspellcheck.h"
 #include "llstring.h"
 #include "llwindow.h"
 #include "llui.h"
 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
 
 	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),
 	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),
 	updateTextPadding();
 	setCursor(mText.length());
 
+	if (mSpellCheck)
+	{
+		LLSpellChecker::setSettingsChangeCallback(boost::bind(&LLLineEditor::onSpellCheckSettingsChange, this));
+	}
+	mSpellCheckTimer.reset();
+
 	setPrevalidateInput(p.prevalidate_input_callback());
 	setPrevalidate(p.prevalidate_callback());
 
 	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)
 {
 			LLUI::reportBadKeystroke();
 		}
 		else
-		if( mKeystrokeCallback )
 		{
-			mKeystrokeCallback( this );
+			onKeystroke();
 		}
 	}
 }
 				LLUI::reportBadKeystroke();
 			}
 			else
-			if( mKeystrokeCallback )
 			{
-				mKeystrokeCallback( this );
+				onKeystroke();
 			}
 		}
 	}
 			// Notify owner if requested
 			if (!need_to_rollback && handled)
 			{
-				if (mKeystrokeCallback)
+				onKeystroke();
+				if ( (!selection_modified) && (KEY_BACKSPACE == key) )
 				{
-					mKeystrokeCallback(this);
+					mSpellCheckTimer.setTimerExpirySec(SPELLCHECK_DELAY);
 				}
 			}
 		}
 		// 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;
 
 			if (!prevalidateInput(text_to_delete))
 			{
-				if( mKeystrokeCallback )
-					mKeystrokeCallback( this );
-
+				onKeystroke();
 				return;
 			}
 			setCursor(getCursor() + 1);
 		}
 		else
 		{
-			if( mKeystrokeCallback )
-			{
-				mKeystrokeCallback( this );
-			}
+			onKeystroke();
+
+			mSpellCheckTimer.setTimerExpirySec(SPELLCHECK_DELAY);
 		}
 	}
 }
 	background.stretch( -mBorderThickness );
 
 	S32 lineeditor_v_pad = (background.getHeight() - mGLFont->getLineHeight()) / 2;
+	if (mSpellCheck)
+	{
+		lineeditor_v_pad += 1;
+	}
 
 	drawBackground();
 	
 	{
 		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;
 		}
 		
 		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,
 	}
 	else
 	{
-		mGLFont->render( 
+		rendered_text = mGLFont->render( 
 			mText, mScrollHPos, 
 			rendered_pixels_right, text_bottom,
 			text_color,
 	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())
 	{
 	mSelectAllonFocusReceived = b;
 }
 
+void LLLineEditor::onKeystroke()
+{
+	if (mKeystrokeCallback)
+	{
+		mKeystrokeCallback(this);
+	}
+
+	mSpellCheckStart = mSpellCheckEnd = -1;
+}
 
 void LLLineEditor::setKeystrokeCallback(callback_t callback, void* user_data)
 {
 
 	// 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
 
 		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);
 	}
 }
 

File indra/llui/lllineeditor.h

 #include "llframetimer.h"
 
 #include "lleditmenuhandler.h"
+#include "llspellcheckmenuhandler.h"
 #include "lluictrl.h"
 #include "lluiimage.h"
 #include "lluistring.h"
 class LLContextMenu;
 
 class LLLineEditor
-: public LLUICtrl, public LLEditMenuHandler, protected LLPreeditor
+: public LLUICtrl, public LLEditMenuHandler, protected LLPreeditor, public LLSpellCheckMenuHandler
 {
 public:
 
 
 		Optional<bool>					select_on_focus,
 										revert_on_esc,
+										spellcheck,
 										commit_on_focus_lost,
 										ignore_tab,
 										is_password;
 	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);
 	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);
 
 	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;
 

File indra/llui/llmenugl.cpp

 }
 
 // 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())
 	{
 	setRect(rect);
 	arrange();
 
+	if (spawning_view)
+		mSpawningViewHandle = spawning_view->getHandle();
+	else
+		mSpawningViewHandle.markDead();
 	LLView::setVisible(TRUE);
 }
 

File indra/llui/llmenugl.h

 	
 	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 );
 
 			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;
 };
 
 

File indra/llui/llspellcheck.cpp

+/** 
+ * @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();
+}

File indra/llui/llspellcheck.h

+/** 
+ * @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

File indra/llui/llspellcheckmenuhandler.h

+/** 
+ * @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

File indra/llui/lltextbase.cpp

 #include "lllocalcliprect.h"
 #include "llmenugl.h"
 #include "llscrollcontainer.h"
+#include "llspellcheck.h"
 #include "llstl.h"
 #include "lltextparser.h"
 #include "lltextutil.h"
 	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),
 	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 ),
 		addChild(mDocumentView);
 	}
 
+	if (mSpellCheck)
+	{
+		LLSpellChecker::setSettingsChangeCallback(boost::bind(&LLTextBase::onSpellCheckSettingsChange, this));
+	}
+	mSpellCheckTimer.reset();
+
 	createDefaultSegment();
 
 	updateRects();
 		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;
 				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
 				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();
 	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()

File indra/llui/lltextbase.h

 
 #include "v4color.h"
 #include "lleditmenuhandler.h"
+#include "llspellcheckmenuhandler.h"
 #include "llstyle.h"
 #include "llkeywords.h"
 #include "llpanel.h"
 ///
 class LLTextBase 
 :	public LLUICtrl,
-	protected LLEditMenuHandler
+	protected LLEditMenuHandler,
+	public LLSpellCheckMenuHandler
 {
 public:
 	friend class LLTextSegment;
 								border_visible,
 								track_end,
 								read_only,
+								spellcheck,
 								allow_scroll,
 								plain_text,
 								wrap,
 	/*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; }
 	
 	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

File indra/llui/lltexteditor.cpp

 #include "llwindow.h"
 #include "lltextparser.h"
 #include "llscrollcontainer.h"
+#include "llspellcheck.h"
 #include "llpanel.h"
 #include "llurlregistry.h"
 #include "lltooltip.h"
 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
 
 ///////////////////////////////////////////////////////////////////
 
 
 	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);
 }
 
 
 void LLTextEditor::onKeyStroke()
 {
 	mKeystrokeSignal(this);
+
+	mSpellCheckStart = mSpellCheckEnd = -1;
+	mSpellCheckTimer.setTimerExpirySec(SPELLCHECK_DELAY);
 }
 
 //virtual

File indra/newview/CMakeLists.txt

 include(FMOD)
 include(OPENAL)
 include(FindOpenGL)
+include(Hunspell)
 include(JsonCpp)
 include(LLAudio)
 include(LLCharacter)
     ${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
     )
     llfloatersidepanelcontainer.cpp
     llfloatersnapshot.cpp
     llfloatersounddevices.cpp
+    llfloaterspellchecksettings.cpp
     llfloatertelehub.cpp
     llfloatertestinspectors.cpp
     llfloatertestlistview.cpp
     llfloatersidepanelcontainer.h
     llfloatersnapshot.h
     llfloatersounddevices.h
+    llfloaterspellchecksettings.h
     llfloatertelehub.h
     llfloatertestinspectors.h
     llfloatertestlistview.h
       ${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
     ${LLMATH_LIBRARIES}
     ${LLCOMMON_LIBRARIES}
     ${NDOF_LIBRARY}
+    ${HUNSPELL_LIBRARY}
     ${viewer_LIBRARIES}
     ${BOOST_PROGRAM_OPTIONS_LIBRARY}
     ${BOOST_REGEX_LIBRARY}

File indra/newview/app_settings/settings.xml

       <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),Second Life Glossary</string>
+    </map>
     <key>UseNewWalkRun</key>
     <map>
       <key>Comment</key>

File indra/newview/llappviewer.cpp

 #include "llsecondlifeurls.h"
 #include "llupdaterservice.h"
 #include "llcallfloater.h"
+#include "llspellcheck.h"
 
 // Linden library includes
 #include "llavatarnamecache.h"
 // Third party library includes
 #include <boost/bind.hpp>
 #include <boost/foreach.hpp>
+#include <boost/algorithm/string.hpp>