From 2061b06eccca67595c50edd81c44c5b961bf108b Mon Sep 17 00:00:00 2001
From: Vlad Voicu <vladvoic@gmail.com>
Date: Mon, 28 Nov 2011 18:37:32 +0200
Subject: Spell checker implementation using Hunspell

Change-Id: Ia15b6532edf6eef7c45bdfb273e77f65ce998f13
License: This patch is BSD-licensed, see Documentation/Licenses/BSD-simplified.txt for details

diff --git a/BuildTools/SCons/SConscript.boot b/BuildTools/SCons/SConscript.boot
index a8b7446..3e9e48c 100644
--- a/BuildTools/SCons/SConscript.boot
+++ b/BuildTools/SCons/SConscript.boot
@@ -38,6 +38,7 @@ if os.name == "nt" :
 	vars.Add(PackageVariable("bonjour", "Bonjour SDK location", "yes"))
 vars.Add(PackageVariable("openssl", "OpenSSL location", "yes"))
 vars.Add(BoolVariable("openssl_force_bundled", "Force use of the bundled OpenSSL", "no"))
+vars.Add(PackageVariable("hunspell", "Hunspell location", False))
 vars.Add(PathVariable("boost_includedir", "Boost headers location", None, PathVariable.PathAccept))
 vars.Add(PathVariable("boost_libdir", "Boost library location", None, PathVariable.PathAccept))
 vars.Add(PathVariable("expat_includedir", "Expat headers location", None, PathVariable.PathAccept))
diff --git a/BuildTools/SCons/SConstruct b/BuildTools/SCons/SConstruct
index b4c3740..698217f 100644
--- a/BuildTools/SCons/SConstruct
+++ b/BuildTools/SCons/SConstruct
@@ -445,6 +445,22 @@ else :
 
 	openssl_conf.Finish()
 
+#Hunspell
+hunspell_env = conf_env.Clone()
+hunspell_prefix = env["hunspell"] if isinstance(env.get("hunspell", False), str) else ""
+hunspell_flags = {}
+if hunspell_prefix :
+	hunspell_flags = {"CPPPATH":[os.path.join(hunspell_prefix, "include")], "LIBPATH":[os.path.join(hunspell_prefix, "lib")]}
+hunspell_env.MergeFlags(hunspell_flags)
+
+env["HAVE_HUNSPELL"] = 0;
+hunspell_conf = Configure(hunspell_env)
+if hunspell_conf.CheckCXXHeader("hunspell/hunspell.hxx") and hunspell_conf.CheckLib("hunspell") :
+	env["HAVE_HUNSPELL"] = 1
+	hunspell_flags["LIBS"] = ["hunspell"]
+	env["HUNSPELL_FLAGS"] = hunspell_flags
+hunspell_conf.Finish()
+
 # Bonjour
 if env["PLATFORM"] == "darwin" :
 	env["HAVE_BONJOUR"] = 1
diff --git a/SwifTools/HunspellChecker.cpp b/SwifTools/HunspellChecker.cpp
new file mode 100644
index 0000000..ecd352e
--- /dev/null
+++ b/SwifTools/HunspellChecker.cpp
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2011 Vlad Voicu
+ * Licensed under the Simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#include <SwifTools/HunspellChecker.h>
+
+#include <algorithm>
+#include <hunspell/hunspell.hxx>
+#include <boost/algorithm/string.hpp>
+
+
+namespace Swift {
+
+HunspellChecker::HunspellChecker(const char* affix_path, const char* dictionary_path) {
+	speller_ = new Hunspell(affix_path, dictionary_path);
+}
+
+HunspellChecker::~HunspellChecker() {
+	delete speller_;
+}
+
+bool HunspellChecker::isCorrect(const std::string& word) {
+	return speller_->spell(word.c_str());
+}
+
+void HunspellChecker::getSuggestions(const std::string& word, std::vector<std::string>& list) {
+	char **suggestList;
+	int words_returned;
+	if (!word.empty()) {
+		words_returned = speller_->suggest(&suggestList, word.c_str());
+	}
+	for (int i = 0; i < words_returned; ++i) {
+		list.push_back(suggestList[i]);
+		free(suggestList[i]);
+	}
+	free(suggestList);
+}
+
+void HunspellChecker::checkFragment(const std::string& fragment, PositionPairList& misspelledPositions) {
+	if (!fragment.empty()) {
+		parser_->check(fragment, misspelledPositions);
+		for (PositionPairList::iterator it = misspelledPositions.begin(); it != misspelledPositions.end();) {
+			if (isCorrect(fragment.substr(boost::get<0>(*it), boost::get<1>(*it) - boost::get<0>(*it)))) {
+				it = misspelledPositions.erase(it);
+			}
+			else {
+				++it;
+			}
+		}
+	}
+}
+
+}
diff --git a/SwifTools/HunspellChecker.h b/SwifTools/HunspellChecker.h
new file mode 100644
index 0000000..12c0485
--- /dev/null
+++ b/SwifTools/HunspellChecker.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2011 Vlad Voicu
+ * Licensed under the Simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#include <vector>
+#include <boost/algorithm/string.hpp>
+#include <boost/tuple/tuple.hpp>
+#include <SwifTools/SpellChecker.h>
+
+#pragma once
+
+class Hunspell;
+
+namespace Swift {
+	class HunspellChecker : public SpellChecker {
+		public:
+			HunspellChecker(const char* affix_path, const char* dict_path);
+			virtual ~HunspellChecker();
+			virtual bool isCorrect(const std::string& word);
+			virtual void getSuggestions(const std::string& word, std::vector<std::string>& list);
+			virtual void checkFragment(const std::string& fragment, PositionPairList& misspelledPositions);
+		private:
+			Hunspell* speller_;
+	};
+}
diff --git a/SwifTools/SConscript b/SwifTools/SConscript
index fa2686a..eaf5787 100644
--- a/SwifTools/SConscript
+++ b/SwifTools/SConscript
@@ -9,6 +9,8 @@ if env["SCONS_STAGE"] == "flags" :
 			"LIBPATH": [Dir(".")],
 			"LIBS": ["SwifTools"]
 		}
+	if env["HAVE_HUNSPELL"] :
+		env.MergeFlags(env["HUNSPELL_FLAGS"])
 
 ################################################################################
 # Build
@@ -30,6 +32,16 @@ if env["SCONS_STAGE"] == "build" :
 			"TabComplete.cpp",
 			"LastLineTracker.cpp",
 		]
+	
+	if swiftools_env["HAVE_HUNSPELL"] :
+		swiftools_env.MergeFlags(swiftools_env["HUNSPELL_FLAGS"])
+		swiftools_env.Append(CPPDEFINES = ["HAVE_HUNSPELL"])
+		sources += [
+			"SpellCheckerFactory.cpp",
+			"HunspellChecker.cpp",
+			"SpellParser.cpp",
+		]
+
 
 	if swiftools_env.get("HAVE_SPARKLE", 0) :
 		swiftools_env.MergeFlags(swiftools_env["SPARKLE_FLAGS"])
diff --git a/SwifTools/SpellChecker.h b/SwifTools/SpellChecker.h
new file mode 100644
index 0000000..746fcaf
--- /dev/null
+++ b/SwifTools/SpellChecker.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2011 Vlad Voicu
+ * Licensed under the Simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#include <SwifTools/SpellParser.h>
+
+#include <boost/algorithm/string.hpp>
+#include <boost/tuple/tuple.hpp>
+#include <vector>
+
+#pragma once
+
+namespace Swift {
+	class SpellChecker {
+		public:
+			SpellChecker() {
+				parser_ = new SpellParser();
+			}
+			virtual ~SpellChecker() {
+				delete parser_;
+			};
+			virtual bool isCorrect(const std::string& word) = 0;
+			virtual void getSuggestions(const std::string& word, std::vector<std::string>& list) = 0;
+			virtual void checkFragment(const std::string& fragment, PositionPairList& misspelledPositions) = 0;
+		protected:
+			SpellParser *parser_;
+	};
+}
diff --git a/SwifTools/SpellCheckerFactory.cpp b/SwifTools/SpellCheckerFactory.cpp
new file mode 100644
index 0000000..6061d78
--- /dev/null
+++ b/SwifTools/SpellCheckerFactory.cpp
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2011 Vlad Voicu
+ * Licensed under the Simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#include <boost/filesystem/operations.hpp>
+
+#include <SwifTools/SpellChecker.h>
+#include <SwifTools/HunspellChecker.h>
+#include <SwifTools/SpellCheckerFactory.h>
+
+#ifdef HAVE_HUNSPELL
+#include <hunspell/hunspell.hxx>
+#endif
+
+namespace Swift {
+
+SpellCheckerFactory::SpellCheckerFactory() {
+}
+
+SpellChecker* SpellCheckerFactory::createSpellChecker(const std::string& dictFile) {
+#ifdef HAVE_HUNSPELL
+	std::string affixFile(dictFile);
+	boost::replace_all(affixFile, ".dic", ".aff");
+	if ((boost::filesystem::exists(dictFile)) && (boost::filesystem::exists(affixFile))) {
+		return new HunspellChecker(affixFile.c_str(), dictFile.c_str());
+	}
+	// If dictionaries don't exist disable the checker
+#endif
+	return NULL;
+}
+
+}
diff --git a/SwifTools/SpellCheckerFactory.h b/SwifTools/SpellCheckerFactory.h
new file mode 100644
index 0000000..086ea66
--- /dev/null
+++ b/SwifTools/SpellCheckerFactory.h
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2011 Vlad Voicu
+ * Licensed under the Simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#pragma once
+
+#ifdef HAVE_HUNSPELL
+#define HAVE_SPELLCHECKER
+#endif
+
+namespace Swift {
+	class SpellChecker;
+	class SpellCheckerFactory {
+		public:
+			SpellCheckerFactory();
+			SpellChecker* createSpellChecker(const std::string& dictFile);
+	};
+}
diff --git a/SwifTools/SpellParser.cpp b/SwifTools/SpellParser.cpp
new file mode 100644
index 0000000..7208cdb
--- /dev/null
+++ b/SwifTools/SpellParser.cpp
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2011 Vlad Voicu
+ * Licensed under the Simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#include <SwifTools/SpellParser.h>
+
+#include <boost/spirit/include/lex_lexertl.hpp>
+#include <boost/bind.hpp>
+#include <boost/ref.hpp>
+
+#include <string>
+
+namespace lex = boost::spirit::lex;
+
+namespace Swift {
+
+template <typename Lexer>
+struct word_count_tokens : lex::lexer<Lexer>
+{
+	word_count_tokens()
+	{
+	// define tokens (regular expresions) to match strings
+	// order is important
+		this->self.add
+			("w{3}.[^ ]+", ID_WWW)
+			("http:\\/\\/[^ ]+", ID_HTTP)
+			("\\w{1,}['?|\\-?]?\\w{1,}", ID_WORD)
+			(".", ID_CHAR);
+	}
+};
+
+struct counter
+{
+	typedef bool result_type;
+	// the function operator gets called for each of the matched tokens
+	template <typename Token>
+	bool operator()(Token const& t, PositionPairList& wordPositions, std::size_t& position) const
+	{
+		switch (t.id()) {
+			case ID_WWW:
+				position += t.value().size();
+				break;
+			case ID_HTTP:
+				position += t.value().size();
+				break;
+			case ID_WORD:       // matched a word
+				wordPositions.push_back(boost::tuples::make_tuple(position, position + t.value().size()));
+				position += t.value().size(); 
+				break;
+			case ID_CHAR:       // match a simple char
+				++position; 
+				break;
+		}
+		return true;        // always continue to tokenize
+	}
+};
+
+void SpellParser::check(const std::string& fragment, PositionPairList& wordPositions) {
+	std::size_t position = 0;
+	// create the token definition instance needed to invoke the lexical analyzer
+	word_count_tokens<lex::lexertl::lexer<> > word_count_functor;
+	char const* first = fragment.c_str();
+	char const* last = &first[fragment.size()];
+	lex::tokenize(first, last, word_count_functor, boost::bind(counter(), _1, boost::ref(wordPositions), boost::ref(position)));
+}
+
+}
diff --git a/SwifTools/SpellParser.h b/SwifTools/SpellParser.h
new file mode 100644
index 0000000..a6eafb5
--- /dev/null
+++ b/SwifTools/SpellParser.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2011 Vlad Voicu
+ * Licensed under the Simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#pragma once
+
+#include <boost/algorithm/string.hpp>
+#include <boost/tuple/tuple.hpp>
+#include <boost/algorithm/string.hpp>
+
+#include <vector>
+
+namespace Swift {
+	enum token_ids
+	{
+		ID_WWW = 1,
+		ID_HTTP = 2,
+		ID_WORD = 3,
+		ID_CHAR = 4,
+	};
+
+	typedef boost::tuple<int, int> PositionPair;
+	typedef std::vector<PositionPair > PositionPairList;
+
+	class SpellParser{
+		public:
+			void check(const std::string& fragment, PositionPairList& wordPositions);
+	};
+}
diff --git a/SwifTools/UnitTest/SConscript b/SwifTools/UnitTest/SConscript
index e469deb..dbd1ce5 100644
--- a/SwifTools/UnitTest/SConscript
+++ b/SwifTools/UnitTest/SConscript
@@ -5,3 +5,8 @@ env.Append(UNITTEST_SOURCES = [
 		File("TabCompleteTest.cpp"),
 		File("LastLineTrackerTest.cpp"),
 	])
+
+if env["HAVE_HUNSPELL"] :
+	env.Append(UNITTEST_SOURCES = [
+		File("SpellParserTest.cpp"),
+	])
diff --git a/SwifTools/UnitTest/SpellParserTest.cpp b/SwifTools/UnitTest/SpellParserTest.cpp
new file mode 100644
index 0000000..09e686c
--- /dev/null
+++ b/SwifTools/UnitTest/SpellParserTest.cpp
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2012 Vlad Voicu
+ * Licensed under the Simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#include <cppunit/extensions/HelperMacros.h>
+#include <cppunit/extensions/TestFactoryRegistry.h>
+
+#include <boost/algorithm/string.hpp>
+
+#include <SwifTools/SpellParser.h>
+
+using namespace Swift;
+
+class SpellParserTest : public CppUnit::TestFixture {
+	CPPUNIT_TEST_SUITE(SpellParserTest);
+	CPPUNIT_TEST(testSimpleCheckFragment);
+	CPPUNIT_TEST(testWWWCheckFragment);
+	CPPUNIT_TEST_SUITE_END();
+	public:
+		SpellParserTest() {
+			parser_ = new SpellParser();
+		};
+		void tearDown() {
+			position_.clear();
+		}
+		void testSimpleCheckFragment() {
+			parser_->check("fragment test", position_);
+			int size = position_.size();
+			CPPUNIT_ASSERT_EQUAL(2, size);
+			CPPUNIT_ASSERT_EQUAL(0, boost::get<0>(position_.front()));
+			CPPUNIT_ASSERT_EQUAL(8, boost::get<1>(position_.front()));
+			CPPUNIT_ASSERT_EQUAL(9, boost::get<0>(position_.back()));
+			CPPUNIT_ASSERT_EQUAL(13, boost::get<1>(position_.back()));
+		}
+		void testWWWCheckFragment() {
+			parser_->check("www.link.com fragment test", position_);
+			int size = position_.size();
+			CPPUNIT_ASSERT_EQUAL(2, size);
+			CPPUNIT_ASSERT_EQUAL(13, boost::get<0>(position_.front()));
+			CPPUNIT_ASSERT_EQUAL(21, boost::get<1>(position_.front()));
+			CPPUNIT_ASSERT_EQUAL(22, boost::get<0>(position_.back()));
+			CPPUNIT_ASSERT_EQUAL(26, boost::get<1>(position_.back()));
+		}
+	private:
+		SpellParser *parser_;
+		PositionPairList position_;
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(SpellParserTest);
diff --git a/Swift/Controllers/SettingConstants.cpp b/Swift/Controllers/SettingConstants.cpp
index e430c77..0717fd5 100644
--- a/Swift/Controllers/SettingConstants.cpp
+++ b/Swift/Controllers/SettingConstants.cpp
@@ -20,4 +20,8 @@ const SettingsProvider::Setting<bool> SettingConstants::SHOW_OFFLINE("showOfflin
 const SettingsProvider::Setting<std::string> SettingConstants::EXPANDED_ROSTER_GROUPS("GroupExpandiness", "");
 const SettingsProvider::Setting<bool> SettingConstants::PLAY_SOUNDS("playSounds", true);
 const SettingsProvider::Setting<std::string> SettingConstants::HIGHLIGHT_RULES("highlightRules", "@");
+const SettingsProvider::Setting<bool> SettingConstants::SPELL_CHECKER("spellChecker", false);
+const SettingsProvider::Setting<std::string> SettingConstants::DICT_PATH("dictPath", "/usr/share/myspell/dicts/");
+const SettingsProvider::Setting<std::string> SettingConstants::PERSONAL_DICT_PATH("personaldictPath", "/home/");
+const SettingsProvider::Setting<std::string> SettingConstants::DICT_FILE("dictFile", "en_US.dic");
 }
diff --git a/Swift/Controllers/SettingConstants.h b/Swift/Controllers/SettingConstants.h
index cc3af47..a3a1650 100644
--- a/Swift/Controllers/SettingConstants.h
+++ b/Swift/Controllers/SettingConstants.h
@@ -23,5 +23,9 @@ namespace Swift {
 			static const SettingsProvider::Setting<std::string> EXPANDED_ROSTER_GROUPS;
 			static const SettingsProvider::Setting<bool> PLAY_SOUNDS;
 			static const SettingsProvider::Setting<std::string> HIGHLIGHT_RULES;
+			static const SettingsProvider::Setting<bool> SPELL_CHECKER;
+			static const SettingsProvider::Setting<std::string> DICT_PATH;
+			static const SettingsProvider::Setting<std::string> PERSONAL_DICT_PATH;
+			static const SettingsProvider::Setting<std::string> DICT_FILE;
 	};
 }
diff --git a/Swift/QtUI/QtChatWindow.cpp b/Swift/QtUI/QtChatWindow.cpp
index 7f27cb6..11e64ab 100644
--- a/Swift/QtUI/QtChatWindow.cpp
+++ b/Swift/QtUI/QtChatWindow.cpp
@@ -152,7 +152,7 @@ QtChatWindow::QtChatWindow(const QString &contact, QtChatTheme* theme, UIEventSt
 	QHBoxLayout* inputBarLayout = new QHBoxLayout();
 	inputBarLayout->setContentsMargins(0,0,0,0);
 	inputBarLayout->setSpacing(2);
-	input_ = new QtTextEdit(this);
+	input_ = new QtTextEdit(settings_, this);
 	input_->setAcceptRichText(false);
 	inputBarLayout->addWidget(midBar_);
 	inputBarLayout->addWidget(input_);
diff --git a/Swift/QtUI/QtJoinMUCWindow.ui b/Swift/QtUI/QtJoinMUCWindow.ui
index 5a69292..9225f6f 100644
--- a/Swift/QtUI/QtJoinMUCWindow.ui
+++ b/Swift/QtUI/QtJoinMUCWindow.ui
@@ -43,9 +43,6 @@
        </property>
       </widget>
      </item>
-     <item row="1" column="1" colspan="2">
-      <widget class="QLineEdit" name="nickName"/>
-     </item>
      <item row="0" column="1">
       <widget class="QLineEdit" name="room">
        <property name="text">
@@ -63,6 +60,9 @@
      <item row="2" column="1">
       <widget class="QLineEdit" name="password"/>
      </item>
+     <item row="1" column="1" colspan="2">
+      <widget class="QLineEdit" name="nickName"/>
+     </item>
     </layout>
    </item>
    <item>
diff --git a/Swift/QtUI/QtSpellCheckerWindow.cpp b/Swift/QtUI/QtSpellCheckerWindow.cpp
new file mode 100644
index 0000000..e2c5b0d
--- /dev/null
+++ b/Swift/QtUI/QtSpellCheckerWindow.cpp
@@ -0,0 +1,104 @@
+/*
+ * Copyright (c) 2011 Vlad Voicu
+ * Licensed under the Simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#include "Swift/QtUI/QtSpellCheckerWindow.h"
+
+#include <Swift/Controllers/Settings/SettingsProvider.h>
+#include <Swift/Controllers/SettingConstants.h>
+#include <Swift/QtUI/QtUISettingConstants.h>
+#include <Swift/QtUI/QtSwiftUtil.h>
+
+#include <QCoreApplication>
+#include <QFileDialog>
+#include <QDir>
+#include <QStringList>
+
+namespace Swift {
+
+QtSpellCheckerWindow::QtSpellCheckerWindow(SettingsProvider* settings, QWidget* parent) : QDialog(parent) {
+	settings_ = settings;
+	ui_.setupUi(this);
+	connect(ui_.spellChecker, SIGNAL(toggled(bool)), this, SLOT(handleChecker(bool)));
+	connect(ui_.cancel, SIGNAL(clicked()), this, SLOT(handleCancel()));
+	connect(ui_.apply, SIGNAL(clicked()), this, SLOT(handleApply()));
+	connect(ui_.pathButton, SIGNAL(clicked()), this, SLOT(handlePathButton()));
+	setFromSettings();
+}
+
+void QtSpellCheckerWindow::setFromSettings() {
+	ui_.spellChecker->setChecked(settings_->getSetting(SettingConstants::SPELL_CHECKER));
+	ui_.pathContent->setText(P2QSTRING(settings_->getSetting(SettingConstants::DICT_PATH)));
+	ui_.currentLanguageValue->setText(P2QSTRING(settings_->getSetting(SettingConstants::DICT_FILE)));
+	std::string currentPath = settings_->getSetting(SettingConstants::DICT_PATH);
+	QString filename = "*.dic";
+	QDir dictDirectory = QDir(P2QSTRING(currentPath));
+	QStringList files = dictDirectory.entryList(QStringList(filename), QDir::Files);
+	showFiles(files);
+	setEnabled(settings_->getSetting(SettingConstants::SPELL_CHECKER));
+}
+
+void QtSpellCheckerWindow::handleChecker(bool state) {
+	setEnabled(state);
+}
+
+void QtSpellCheckerWindow::setEnabled(bool state) {
+	ui_.pathContent->setEnabled(state);
+	ui_.languageView->setEnabled(state);
+	ui_.pathButton->setEnabled(state);
+	ui_.pathLabel->setEnabled(state);
+	ui_.currentLanguage->setEnabled(state);
+	ui_.currentLanguageValue->setEnabled(state);
+	ui_.language->setEnabled(state);
+}
+
+void QtSpellCheckerWindow::handleApply() {
+	settings_->storeSetting(SettingConstants::SPELL_CHECKER, ui_.spellChecker->isChecked());
+	QList<QListWidgetItem* > selectedLanguage = ui_.languageView->selectedItems();
+	if (!selectedLanguage.empty()) {
+		settings_->storeSetting(SettingConstants::DICT_FILE, Q2PSTRING((selectedLanguage.first())->text()));
+	}
+	this->done(0);
+}
+
+void QtSpellCheckerWindow::handleCancel() {
+	this->done(0);
+}
+
+void QtSpellCheckerWindow::handlePathButton() {
+	std::string currentPath = settings_->getSetting(SettingConstants::DICT_PATH);
+	QString dirpath = QFileDialog::getExistingDirectory(this, tr("Dictionary Path"), P2QSTRING(currentPath));
+	if (dirpath != P2QSTRING(currentPath)) {
+		ui_.languageView->clear();
+		settings_->storeSetting(SettingConstants::DICT_FILE, "");
+		ui_.currentLanguageValue->setText(" ");
+	}
+	if (!dirpath.isEmpty()) {
+		if (!dirpath.endsWith("/")) {
+			dirpath.append("/");
+		}
+		settings_->storeSetting(SettingConstants::DICT_PATH, Q2PSTRING(dirpath));
+		QDir dictDirectory = QDir(dirpath);
+		ui_.pathContent->setText(dirpath);
+		QString filename = "*.dic";
+		QStringList files = dictDirectory.entryList(QStringList(filename), QDir::Files);
+		showFiles(files);
+	}
+}
+
+void QtSpellCheckerWindow::handlePersonalPathButton() {
+	std::string currentPath = settings_->getSetting(SettingConstants::PERSONAL_DICT_PATH);
+	QString filename = QFileDialog::getOpenFileName(this, tr("Select Personal Dictionary"), P2QSTRING(currentPath), tr("(*.dic"));
+	settings_->storeSetting(SettingConstants::PERSONAL_DICT_PATH, Q2PSTRING(filename));
+}
+
+void QtSpellCheckerWindow::showFiles(const QStringList& files) {
+	ui_.languageView->clear();
+	for (int i = 0; i < files.size(); ++i) {
+		ui_.languageView->insertItem(i, files[i]);
+	}
+}
+
+}
diff --git a/Swift/QtUI/QtSpellCheckerWindow.h b/Swift/QtUI/QtSpellCheckerWindow.h
new file mode 100644
index 0000000..ad94907
--- /dev/null
+++ b/Swift/QtUI/QtSpellCheckerWindow.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2011 Vlad Voicu
+ * Licensed under the Simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#pragma once
+
+#include "ui_QtSpellCheckerWindow.h"
+
+#include <QDialog>
+
+namespace Swift {
+	class SettingsProvider;
+	class QtSpellCheckerWindow : public QDialog, protected Ui::QtSpellCheckerWindow {
+		Q_OBJECT
+		public:
+			QtSpellCheckerWindow(SettingsProvider* settings, QWidget* parent = NULL);
+		public slots:
+			void handleChecker(bool state);
+			void handleCancel();
+			void handlePathButton();
+			void handlePersonalPathButton();
+			void handleApply();
+
+		private:
+			void setEnabled(bool state);
+			void setFromSettings();
+			void showFiles(const QStringList& files);
+			SettingsProvider* settings_;
+			Ui::QtSpellCheckerWindow ui_;
+	};
+}
diff --git a/Swift/QtUI/QtSpellCheckerWindow.ui b/Swift/QtUI/QtSpellCheckerWindow.ui
new file mode 100644
index 0000000..b98bb6d
--- /dev/null
+++ b/Swift/QtUI/QtSpellCheckerWindow.ui
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>QtSpellCheckerWindow</class>
+ <widget class="QDialog" name="QtSpellCheckerWindow">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>353</width>
+    <height>207</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Dialog</string>
+  </property>
+  <layout class="QGridLayout" name="gridLayout">
+   <item row="4" column="3" colspan="2">
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <widget class="QPushButton" name="cancel">
+       <property name="text">
+        <string>Cancel</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="apply">
+       <property name="text">
+        <string>Apply</string>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item row="0" column="0" colspan="3">
+    <widget class="QCheckBox" name="spellChecker">
+     <property name="text">
+      <string>Spell Checker Enabled</string>
+     </property>
+    </widget>
+   </item>
+   <item row="1" column="0" colspan="2">
+    <widget class="QLabel" name="pathLabel">
+     <property name="text">
+      <string>Dictionary Path:</string>
+     </property>
+    </widget>
+   </item>
+   <item row="1" column="2" colspan="2">
+    <widget class="QLineEdit" name="pathContent"/>
+   </item>
+   <item row="1" column="4">
+    <widget class="QPushButton" name="pathButton">
+     <property name="text">
+      <string>Change</string>
+     </property>
+    </widget>
+   </item>
+   <item row="2" column="0" colspan="2">
+    <widget class="QLabel" name="currentLanguage">
+     <property name="text">
+      <string>Current Language:</string>
+     </property>
+    </widget>
+   </item>
+   <item row="2" column="2">
+    <widget class="QLabel" name="currentLanguageValue">
+     <property name="text">
+      <string/>
+     </property>
+    </widget>
+   </item>
+   <item row="3" column="1" colspan="4">
+    <widget class="QListWidget" name="languageView"/>
+   </item>
+   <item row="3" column="0">
+    <widget class="QLabel" name="language">
+     <property name="text">
+      <string>Language:</string>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/Swift/QtUI/QtTextEdit.cpp b/Swift/QtUI/QtTextEdit.cpp
index 0497d01..d1a75dd 100644
--- a/Swift/QtUI/QtTextEdit.cpp
+++ b/Swift/QtUI/QtTextEdit.cpp
@@ -4,18 +4,42 @@
  * See Documentation/Licenses/GPLv3.txt for more information.
  */
 
+#include <boost/tuple/tuple.hpp>
+#include <boost/algorithm/string.hpp>
+#include <boost/bind.hpp>
+
+#include <Swiften/Base/foreach.h>
+
+#include <SwifTools/SpellCheckerFactory.h>
+#include <SwifTools/SpellChecker.h>
+
 #include <Swift/QtUI/QtTextEdit.h>
+#include <Swift/QtUI/QtSwiftUtil.h>
+#include <Swift/QtUI/QtSpellCheckerWindow.h>
+#include <Swift/Controllers/SettingConstants.h>
 
+#include <QApplication>
 #include <QFontMetrics>
 #include <QKeyEvent>
+#include <QDebug>
+#include <QMenu>
 
 namespace Swift {
 
-QtTextEdit::QtTextEdit(QWidget* parent) : QTextEdit(parent){
+QtTextEdit::QtTextEdit(SettingsProvider* settings, QWidget* parent) : QTextEdit(parent) {
 	connect(this, SIGNAL(textChanged()), this, SLOT(handleTextChanged()));
+	checker_ = NULL;
+	settings_ = settings;
+#ifdef HAVE_SPELLCHECKER
+	setUpSpellChecker();
+#endif
 	handleTextChanged();
 }
 
+QtTextEdit::~QtTextEdit() {
+	delete checker_;
+}
+
 void QtTextEdit::keyPressEvent(QKeyEvent* event) {
 	int key = event->key();
 	Qt::KeyboardModifiers modifiers = event->modifiers();
@@ -35,13 +59,41 @@ void QtTextEdit::keyPressEvent(QKeyEvent* event) {
 		emit unhandledKeyPressEvent(event);
 	}
 	else if ((key == Qt::Key_Up)
-			   || (key == Qt::Key_Down)
-	){
+			   || (key == Qt::Key_Down)) {
 		emit unhandledKeyPressEvent(event);
 		QTextEdit::keyPressEvent(event);
 	}
 	else {
 		QTextEdit::keyPressEvent(event);
+#ifdef HAVE_SPELLCHECKER
+		if (settings_->getSetting(SettingConstants::SPELL_CHECKER)) {
+			underlineMisspells();
+		}
+#endif
+	}
+}
+
+void QtTextEdit::underlineMisspells() {
+	QTextCursor cursor = textCursor();
+	misspelledPositions_.clear();
+	QTextCharFormat normalFormat;
+	cursor.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor, 1);
+	cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor, 1);
+	cursor.setCharFormat(normalFormat);
+	if (checker_ == NULL) {
+		return;
+	}
+	QTextCharFormat spellingErrorFormat;
+	spellingErrorFormat.setUnderlineColor(QColor(Qt::red));
+	spellingErrorFormat.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
+	std::string fragment = Q2PSTRING(cursor.selectedText());
+	checker_->checkFragment(fragment, misspelledPositions_);
+	foreach (PositionPair position, misspelledPositions_) {
+		cursor.setPosition(boost::get<0>(position), QTextCursor::MoveAnchor);
+		cursor.setPosition(boost::get<1>(position), QTextCursor::KeepAnchor);
+		cursor.setCharFormat(spellingErrorFormat);
+		cursor.clearSelection();
+		cursor.setCharFormat(normalFormat);
 	}
 }
 
@@ -53,6 +105,24 @@ void QtTextEdit::handleTextChanged() {
 	}
 }
 
+void QtTextEdit::replaceMisspelledWord(const QString& word, int cursorPosition) {
+	QTextCursor cursor = textCursor();
+	PositionPair wordPosition = getWordFromCursor(cursorPosition);
+	cursor.setPosition(boost::get<0>(wordPosition), QTextCursor::MoveAnchor);
+	cursor.setPosition(boost::get<1>(wordPosition), QTextCursor::KeepAnchor);
+	QTextCharFormat normalFormat;
+	cursor.insertText(word, normalFormat);
+}
+
+PositionPair QtTextEdit::getWordFromCursor(int cursorPosition) {
+	for (PositionPairList::iterator it = misspelledPositions_.begin(); it != misspelledPositions_.end(); ++it) {
+		if (cursorPosition >= boost::get<0>(*it) && cursorPosition <= boost::get<1>(*it)) {
+			return *it;
+		}
+	}
+	return boost::make_tuple(-1,-1);
+}
+
 QSize QtTextEdit::sizeHint() const {
 	QFontMetrics inputMetrics(currentFont());
 	QRect horizontalBounds = contentsRect().adjusted(0,0,0,9999);
@@ -66,7 +136,100 @@ QSize QtTextEdit::sizeHint() const {
 	//return QSize(QTextEdit::sizeHint().width(), lineHeight * numberOfLines);
 }
 
+void QtTextEdit::contextMenuEvent(QContextMenuEvent* event) {
+	QMenu* menu = createStandardContextMenu();
+	QTextCursor cursor = cursorForPosition(event->pos());
+#ifdef HAVE_SPELLCHECKER
+	QAction* insertPoint = menu->actions().first();
+	QAction* settingsAction = new QAction(QApplication::translate("QtTextEdit", "Spell Checker Options", 0, QApplication::UnicodeUTF8), menu);
+	menu->insertAction(insertPoint, settingsAction);
+	menu->insertAction(insertPoint, menu->addSeparator());
+	addSuggestions(menu, event);
+	QAction* result = menu->exec(event->globalPos());
+	if (result == settingsAction) {
+		spellCheckerSettingsWindow();
+	}
+	for (std::vector<QAction*>::iterator it = replaceWordActions_.begin(); it != replaceWordActions_.end(); ++it) {
+		if (*it == result) {
+			replaceMisspelledWord((*it)->text(), cursor.position());
+		}
+	}
+#else
+	menu->exec(event->globalPos());
+#endif
+	delete menu;
 }
 
+void QtTextEdit::addSuggestions(QMenu* menu, QContextMenuEvent* event)
+{
+	replaceWordActions_.clear();
+	QAction* insertPoint = menu->actions().first();
+	QTextCursor cursor = cursorForPosition(event->pos());
+	PositionPair wordPosition = getWordFromCursor(cursor.position());
+	if (boost::get<0>(wordPosition) < 0) {
+		// The click was executed outside a spellable word so no
+		// suggestions are necessary
+		return;
+	}
+	cursor.setPosition(boost::get<0>(wordPosition), QTextCursor::MoveAnchor);
+	cursor.setPosition(boost::get<1>(wordPosition), QTextCursor::KeepAnchor);
+	std::vector<std::string> wordList;
+	checker_->getSuggestions(Q2PSTRING(cursor.selectedText()), wordList);
+	if (wordList.size() == 0) {
+		QAction* noSuggestions = new QAction(QApplication::translate("QtTextEdit", "No Suggestions", 0, QApplication::UnicodeUTF8), menu);
+		noSuggestions->setDisabled(true);
+		menu->insertAction(insertPoint, noSuggestions);
+	}
+	else {
+		for (std::vector<std::string>::iterator it = wordList.begin(); it != wordList.end(); ++it) {
+			QAction* wordAction = new QAction(it->c_str(), menu);
+			menu->insertAction(insertPoint, wordAction);
+			replaceWordActions_.push_back(wordAction);
+		}
+	}
+	menu->insertAction(insertPoint, menu->addSeparator());
+}
 
 
+#ifdef HAVE_SPELLCHECKER
+void QtTextEdit::setUpSpellChecker()
+{
+	SpellCheckerFactory* checkerFactory = new SpellCheckerFactory();
+	delete checker_;
+	if (settings_->getSetting(SettingConstants::SPELL_CHECKER)) {
+		std::string dictPath = settings_->getSetting(SettingConstants::DICT_PATH);
+		std::string dictFile = settings_->getSetting(SettingConstants::DICT_FILE);
+		checker_ = checkerFactory->createSpellChecker(dictPath + dictFile);
+		delete checkerFactory;
+	}
+	else {
+		checker_ = NULL;
+	}
+}
+#endif
+
+void QtTextEdit::spellCheckerSettingsWindow() {
+	if (!spellCheckerWindow_) {
+		spellCheckerWindow_ = new QtSpellCheckerWindow(settings_);
+		settings_->onSettingChanged.connect(boost::bind(&QtTextEdit::handleSettingChanged, this, _1));
+		spellCheckerWindow_->show();
+	}
+	else {
+		spellCheckerWindow_->show();
+		spellCheckerWindow_->raise();
+		spellCheckerWindow_->activateWindow();
+	}
+}
+
+void QtTextEdit::handleSettingChanged(const std::string& settings) {
+	if (settings == SettingConstants::SPELL_CHECKER.getKey()
+		|| settings == SettingConstants::DICT_PATH.getKey()
+		|| settings == SettingConstants::DICT_FILE.getKey()) {
+#ifdef HAVE_SPELLCHECKER
+		setUpSpellChecker();
+		underlineMisspells();
+#endif
+	}
+}
+
+}
diff --git a/Swift/QtUI/QtTextEdit.h b/Swift/QtUI/QtTextEdit.h
index 075728b..a8df4d3 100644
--- a/Swift/QtUI/QtTextEdit.h
+++ b/Swift/QtUI/QtTextEdit.h
@@ -5,20 +5,46 @@
  */
 
 #pragma once
+
+#include <SwifTools/SpellParser.h>
+
+#include <Swift/Controllers/Settings/SettingsProvider.h>
+#include <Swift/Controllers/SettingConstants.h>
+
 #include <QTextEdit>
+#include <QPointer>
 
 namespace Swift {
+	class SpellChecker;
+	class QtSpellCheckerWindow;
 	class QtTextEdit : public QTextEdit {
 		Q_OBJECT
 	public:
-		QtTextEdit(QWidget* parent = 0);
+		QtTextEdit(SettingsProvider* settings, QWidget* parent = 0);
+		virtual ~QtTextEdit();
 		virtual QSize sizeHint() const;
 	signals:
+		void wordCorrected(QString& word);
 		void returnPressed();
 		void unhandledKeyPressEvent(QKeyEvent* event);
+	public slots:
+		void handleSettingChanged(const std::string& settings);
 	protected:
 		virtual void keyPressEvent(QKeyEvent* event);
+		virtual void contextMenuEvent(QContextMenuEvent* event);
 	private slots:
 		void handleTextChanged();
+	private:
+		SpellChecker *checker_;
+		std::vector<QAction*> replaceWordActions_;
+		PositionPairList misspelledPositions_;
+		SettingsProvider *settings_;
+		QPointer<QtSpellCheckerWindow> spellCheckerWindow_;
+		void addSuggestions(QMenu* menu, QContextMenuEvent* event);
+		void replaceMisspelledWord(const QString& word, int cursorPosition);
+		void setUpSpellChecker();
+		void underlineMisspells();
+		void spellCheckerSettingsWindow();
+		PositionPair getWordFromCursor(int cursorPosition);
 	};
 }
diff --git a/Swift/QtUI/SConscript b/Swift/QtUI/SConscript
index 13d36fa..bfa35a5 100644
--- a/Swift/QtUI/SConscript
+++ b/Swift/QtUI/SConscript
@@ -45,6 +45,8 @@ if myenv["swift_mobile"] :
 if myenv.get("HAVE_SNARL", False) :
 	myenv.UseFlags(myenv["SNARL_FLAGS"])
 	myenv.Append(CPPDEFINES = ["HAVE_SNARL"])
+if myenv.get("HAVE_HUNSPELL", True):
+	myenv.Append(CPPDEFINES = ["HAVE_HUNSPELL"])
 if env["PLATFORM"] == "win32" :
 	myenv.Append(LIBS = ["cryptui"])
 myenv.UseFlags(myenv["PLATFORM_FLAGS"])
@@ -83,6 +85,7 @@ myenv.WriteVal("DefaultTheme.qrc", myenv.Value(generateDefaultTheme(myenv.Dir("#
 sources = [
     "main.cpp",
     "QtAboutWidget.cpp",
+    "QtSpellCheckerWindow.cpp",
     "QtAvatarWidget.cpp",
     "QtUIFactory.cpp",
     "QtChatWindowFactory.cpp",
@@ -270,6 +273,7 @@ myenv.Uic4("QtHistoryWindow.ui")
 myenv.Uic4("QtConnectionSettings.ui")
 myenv.Uic4("QtHighlightRuleWidget.ui")
 myenv.Uic4("QtHighlightEditorWidget.ui")
+myenv.Uic4("QtSpellCheckerWindow.ui")
 myenv.Qrc("DefaultTheme.qrc")
 myenv.Qrc("Swift.qrc")
 
-- 
cgit v0.10.2-6-g49f6