Skip to content
This repository has been archived by the owner on Jun 11, 2020. It is now read-only.

Spellchecking #100 #120

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ before_install:
- if [ "$CXX" = "g++" ]; then sudo apt-get install g++-4.8; fi
- if [ "$CXX" = "g++" ]; then sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.8 50; fi
- if [ "$CXX" = "g++" ]; then sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-4.8 50; fi
- sudo apt-get install libhunspell-dev
- wget -O Qt5.2.0.tar.xz https://dl.dropboxusercontent.com/u/20447449/Qt5.2.0.tar.xz
- mkdir ~/Qt5.2.0
- tar -xJf Qt5.2.0.tar.xz -C ~/Qt5.2.0
Expand All @@ -20,8 +21,9 @@ before_install:
- ./autogen.sh
- ./configure && make -j3 check
- sudo make install
- sudo ldconfig
- cd ..
- sudo ldconfig


script:
- ~/Qt5.2.0/5.2.0/gcc_64/bin/qmake -v
Expand All @@ -35,4 +37,4 @@ notifications:
- "chat.freenode.net#Tox-Qt-GUI"
on_success: always
on_failure: always


14 changes: 8 additions & 6 deletions projectfiles/QtCreator/TOX-Qt-GUI.pro
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,16 @@ TEMPLATE = app
CONFIG += c++11

INCLUDEPATH += ../../src/ ../../submodules/ProjectTox-Core/toxcore/
win32:INCLUDEPATH += ../../libs/sodium/include/
win32:INCLUDEPATH += ../../libs/sodium/include/ ../../libs/hunspell/include/
macx:INCLUDEPATH += /usr/local/include

win32 {
LIBS += -lWS2_32 ../../libs/sodium/lib/libsodium.a
LIBS += -lWS2_32 ../../libs/sodium/lib/libsodium.a ../../libs/hunspell/lib/libhunspell.a
} else {
macx {
LIBS += -L/usr/local/lib -lsodium
LIBS += -L/usr/local/lib -lsodium -lhunspell
} else {
LIBS += -lsodium
LIBS += -lsodium -lhunspell
}
}

Expand Down Expand Up @@ -92,7 +92,8 @@ SOURCES += \
../../src/frienditemdelegate.cpp \
../../src/editablelabelwidget.cpp \
../../src/esclineedit.cpp \
../../src/copyableelidelabel.cpp
../../src/copyableelidelabel.cpp \
../../src/spellchecker.cpp

HEADERS += \
../../src/mainwindow.hpp \
Expand Down Expand Up @@ -135,7 +136,8 @@ HEADERS += \
../../src/frienditemdelegate.hpp \
../../src/editablelabelwidget.hpp \
../../src/esclineedit.hpp \
../../src/copyableelidelabel.hpp
../../src/copyableelidelabel.hpp \
../../src/spellchecker.hpp

### ToxCore section. Please keep it alphabetical ###

Expand Down
41 changes: 37 additions & 4 deletions src/inputtextwidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,13 @@
#include <QKeyEvent>
#include <QMenu>
#include <QTextDocumentFragment>
#include <QList>

#include "smileypack.hpp"
#include "Settings/settings.hpp"

InputTextWidget::InputTextWidget(QWidget* parent) :
QTextEdit(parent)
QTextEdit(parent), spellchecker(this), maxSuggestions(4)
{
setMinimumSize(10, 50);

Expand Down Expand Up @@ -143,9 +144,37 @@ void InputTextWidget::cutPlainText()

void InputTextWidget::showContextMenu(const QPoint &pos)
{
QPoint globalPos = mapToGlobal(pos);

const QPoint globalPos = mapToGlobal(pos);
QMenu contextMenu;

// get current selected word and - if neccessary - the suggested
// words by the spellchecker
// create a QAction for each suggested word and handle them after
// the execution of the context menu
QList<QAction*> actions;
QTextCursor cursor = cursorForPosition(pos);
cursor.select(QTextCursor::WordUnderCursor);
const QString selectedWord = cursor.selectedText();
// cursor.position() points to the end of the selected word
// substract selectedWord.length() to get the start position
if (!spellchecker.skipRange(cursor.position() - selectedWord.length(), cursor.position()) &&
!spellchecker.isCorrect(selectedWord)) {
QStringList suggestions;
spellchecker.suggest(selectedWord, suggestions);

if (!suggestions.isEmpty()) {
QStringListIterator it(suggestions);
for (int i = 0; i < maxSuggestions && it.hasNext(); i++) {
QString suggestion = it.next();
QAction* action = new QAction(suggestion, this);
action->setData(suggestion);
contextMenu.addAction(action);
actions.append(action);
}
contextMenu.addSeparator();
}
}

contextMenu.addAction(actionUndo);
contextMenu.addAction(actionRedo);
contextMenu.addSeparator();
Expand All @@ -155,5 +184,9 @@ void InputTextWidget::showContextMenu(const QPoint &pos)

actionPaste->setDisabled(QApplication::clipboard()->text().isEmpty());

contextMenu.exec(globalPos);
QAction* selected = contextMenu.exec(globalPos);
if (actions.contains(selected)) {
cursor.insertText(selected->data().toString());
}
qDeleteAll(actions);
}
4 changes: 4 additions & 0 deletions src/inputtextwidget.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#ifndef INPUTTEXTWIDGET_HPP
#define INPUTTEXTWIDGET_HPP

#include "spellchecker.hpp"
#include <QTextEdit>

class InputTextWidget : public QTextEdit
Expand All @@ -43,6 +44,9 @@ private slots:
private:
QString desmile(QString htmlText);

Spellchecker spellchecker;
const int maxSuggestions;

QAction *actionUndo;
QAction *actionRedo;
QAction *actionCut;
Expand Down
151 changes: 151 additions & 0 deletions src/spellchecker.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
Copyright (C) 2013 by retuxx <github@retux.de>

This file is part of Tox Qt GUI.

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program 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 COPYING file for more details.
*/

#include "spellchecker.hpp"

#include <QFileInfo>
#include <QStandardPaths>
#include <QString>
#include <QStringList>
#include <QStringListIterator>
#include <QTextCharFormat>
#include <QTextEdit>

#include <hunspell/hunspell.hxx>

Spellchecker::Spellchecker(QTextEdit* parent)
: QSyntaxHighlighter(parent),
textEdit(parent),
regEx("\\W"), /* any non-word character. */
format(),
skippedPosition(NO_SKIPPING), /* skipping should be disabled by default. */
contentChanged(false)
{
QString basePath;
#ifdef Q_OS_WIN
basePath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + '/' + "hunspell" + '/';
#else
basePath = "/usr/share/hunspell/";
#endif
QFileInfo aff(basePath + "en_US.aff");
QFileInfo dic(basePath + "en_US.dic");
hunspell = new Hunspell(
aff.absoluteFilePath().toLocal8Bit().constData(),
dic.absoluteFilePath().toLocal8Bit().constData()
);

format.setUnderlineColor(QColor(255,0,0));
format.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);

connect(textEdit, &QTextEdit::cursorPositionChanged, this, &Spellchecker::cursorPositionChanged);
connect(textEdit->document(), &QTextDocument::contentsChange, this, &Spellchecker::contentsChanged);
// this is a simple hack to ensure that the connected slot above
// will be triggered before the highlighting will be applied.
setDocument(textEdit->document());

}

Spellchecker::~Spellchecker()
{
delete hunspell;
}

void Spellchecker::highlightBlock(const QString& text)
{
const QStringList tokens = text.split(regEx);
QStringListIterator it(tokens);
const int offset = currentBlock().position();
int start, length, end;
start = length = end = 0;

while (it.hasNext()) {
const QString& token = it.next();
length = token.length();
end = start + length;

if (!skipRange(offset + start, offset + end) &&
!isCorrect(token)) {
setFormat(start, length, format);
}

start += length + 1; // skip the non-word character
}
}

bool Spellchecker::isCorrect(const QString& word)
{
return hunspell->spell(word.toLocal8Bit().constData()) != 0;
}

void Spellchecker::suggest(const QString& word, QStringList& suggestions)
{
char** slst;
const int numberOfSuggestions = hunspell->suggest(&slst, word.toLocal8Bit().constData());
for (int i = 0; i < numberOfSuggestions; i++) {
suggestions << slst[i];
}
}

bool Spellchecker::skipRange(int start, int end)
{
return skippedPosition >= start && skippedPosition <= end;
}

int Spellchecker::toPositionInBlock(int position)
{
int counter = 0;
const QTextBlock block = textEdit->document()->findBlock(position);
const QString text = block.text();
const int mappedPosition = position - block.position();
const QStringList tokens = text.split(regEx);

QStringListIterator it(tokens);
int start = 0;

while (it.hasNext() && start < mappedPosition) {
start += it.next().length() + 1; // skip the non-word character
counter++;
}

return start <= mappedPosition ? -1 : counter;
}

void Spellchecker::contentsChanged(int position, int charsRemoved, int charsAdded)
{
contentChanged = true;
skippedPosition = textEdit->textCursor().position();
}

void Spellchecker::cursorPositionChanged()
{
// block signal if content was changed because
// skipping was already handled by slot Spellchecker::contentsChanged();
if (!contentChanged && skippedPosition >= 0) {
const int pos = textEdit->textCursor().position();
const int current = toPositionInBlock(skippedPosition);
const int moved = toPositionInBlock(pos);

if (current >= 0 && moved >= 0 && current == moved) {
skippedPosition = pos;
} else {
skippedPosition = NO_SKIPPING;
}

rehighlight();
}

contentChanged = false;
}
73 changes: 73 additions & 0 deletions src/spellchecker.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
Copyright (C) 2013 by retuxx <github@retux.de>

This file is part of Tox Qt GUI.

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program 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 COPYING file for more details.
*/

#ifndef SPELLCHECKER_HPP
#define SPELLCHECKER_HPP

#include <QRegularExpression>
#include <QSyntaxHighlighter>

class Hunspell;
class QTextCharFormat;
class QTextEdit;

class Spellchecker : public QSyntaxHighlighter
{
Q_OBJECT
public:
static const int NO_SKIPPING = -1;

Spellchecker(QTextEdit*);
~Spellchecker();

bool isCorrect(const QString&);
void suggest(const QString&, QStringList&);
bool skipRange(int /*inclusive*/, int /*inclusive*/);

protected:
void highlightBlock(const QString& text);

private:
/* the view to highlight */
QTextEdit* textEdit;

/* the current used dictionary */
Hunspell* hunspell;

/* the regular expression to use for tokenizing each line */
const QRegularExpression regEx;

/* the format to apply to misspelled words */
QTextCharFormat format;

/* the spell checker will ignore the word which
* has at least one character at this position (absolute) */
int skippedPosition;

/* indicates whether the content was changed or not
* do not use until you know what you are doing */
bool contentChanged;

/* maps the given absolute position of a character in the document
* to the relative position of its words inside its block */
int toPositionInBlock(int);

private slots:
void contentsChanged(int, int, int);
void cursorPositionChanged();
};

#endif // SPELLCHECKER_HPP