From 2061b06eccca67595c50edd81c44c5b961bf108b Mon Sep 17 00:00:00 2001 From: Vlad Voicu 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 + +#include +#include +#include + + +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& 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 +#include +#include +#include + +#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& 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 + +#include +#include +#include + +#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& 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 + +#include +#include +#include + +#ifdef HAVE_HUNSPELL +#include +#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 + +#include +#include +#include + +#include + +namespace lex = boost::spirit::lex; + +namespace Swift { + +template +struct word_count_tokens : lex::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 + 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 > 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 +#include +#include + +#include + +namespace Swift { + enum token_ids + { + ID_WWW = 1, + ID_HTTP = 2, + ID_WORD = 3, + ID_CHAR = 4, + }; + + typedef boost::tuple PositionPair; + typedef std::vector 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 +#include + +#include + +#include + +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 SettingConstants::SHOW_OFFLINE("showOfflin const SettingsProvider::Setting SettingConstants::EXPANDED_ROSTER_GROUPS("GroupExpandiness", ""); const SettingsProvider::Setting SettingConstants::PLAY_SOUNDS("playSounds", true); const SettingsProvider::Setting SettingConstants::HIGHLIGHT_RULES("highlightRules", "@"); +const SettingsProvider::Setting SettingConstants::SPELL_CHECKER("spellChecker", false); +const SettingsProvider::Setting SettingConstants::DICT_PATH("dictPath", "/usr/share/myspell/dicts/"); +const SettingsProvider::Setting SettingConstants::PERSONAL_DICT_PATH("personaldictPath", "/home/"); +const SettingsProvider::Setting 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 EXPANDED_ROSTER_GROUPS; static const SettingsProvider::Setting PLAY_SOUNDS; static const SettingsProvider::Setting HIGHLIGHT_RULES; + static const SettingsProvider::Setting SPELL_CHECKER; + static const SettingsProvider::Setting DICT_PATH; + static const SettingsProvider::Setting PERSONAL_DICT_PATH; + static const SettingsProvider::Setting 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 @@ - - - @@ -63,6 +60,9 @@ + + + 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 +#include +#include +#include + +#include +#include +#include +#include + +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 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 + +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 @@ + + + QtSpellCheckerWindow + + + + 0 + 0 + 353 + 207 + + + + Dialog + + + + + + + + Cancel + + + + + + + Apply + + + + + + + + + Spell Checker Enabled + + + + + + + Dictionary Path: + + + + + + + + + + Change + + + + + + + Current Language: + + + + + + + + + + + + + + + + + Language: + + + + + + + + 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 +#include +#include + +#include + +#include +#include + #include +#include +#include +#include +#include #include #include +#include +#include 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::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 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::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 + +#include +#include + #include +#include 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 replaceWordActions_; + PositionPairList misspelledPositions_; + SettingsProvider *settings_; + QPointer 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