Squashed commit of the following:

commit a1f8cef250bcc033d120d87aaeafd0794e0c7252
Author: Jordan Sherer <jordan@widefido.com>
Date:   Sun Dec 30 20:16:47 2018 -0500

    Added word replacement from suggestions menu

commit 51af18c06d3268b34dd5b472f8a94787e47af04c
Author: Jordan Sherer <jordan@widefido.com>
Date:   Sun Dec 30 11:21:24 2018 -0500

    Simplified word checker to use text stats signal for computation

commit aa831492784fec30c8a6d804a4ae7ca718f865fe
Author: Jordan Sherer <jordan@widefido.com>
Date:   Sat Dec 29 22:50:24 2018 -0500

    Initial working implmementation of spell check highlighting
This commit is contained in:
Jordan Sherer 2018-12-30 20:18:35 -05:00
parent 1aed1fde31
commit 95bbfb8232
8 changed files with 314 additions and 3 deletions

View File

@ -237,6 +237,7 @@ set (wsjtx_CXXSRCS
jsc.cpp
jsc_list.cpp
jsc_map.cpp
jsc_checker.cpp
SelfDestructMessageBox.cpp
messagereplydialog.cpp
keyeater.cpp

View File

@ -159,6 +159,13 @@ QString JSC::decompress(Codeword const& bitvec){
return out.join("");
}
bool JSC::exists(QString w, quint32 *pIndex){
bool found = false;
quint32 index = lookup(w, &found);
if(pIndex) *pIndex = index;
return found && JSC::map[index].size == w.length();
}
quint32 JSC::lookup(QString w, bool * ok){
if(LOOKUP_CACHE.contains(w)){
if(ok) *ok = true;

1
jsc.h
View File

@ -32,6 +32,7 @@ public:
static QList<CodewordPair> compress(QString text);
static QString decompress(Codeword const& bits);
static bool exists(QString w, quint32 *pIndex);
static quint32 lookup(QString w, bool *ok);
static quint32 lookup(char const* b, bool *ok);

205
jsc_checker.cpp Normal file
View File

@ -0,0 +1,205 @@
/**
* This file is part of JS8Call.
*
* 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
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* (C) 2018 Jordan Sherer <kn4crd@gmail.com> - All Rights Reserved
*
**/
#include "jsc_checker.h"
#include <QTextEdit>
#include <QTextBlock>
#include <QTextCursor>
#include <QTextDocument>
#include <QTextLayout>
#include <QDebug>
#include "jsc.h"
#include "varicode.h"
const int CORRECT = QTextFormat::UserProperty + 10;
const QString ALPHABET = { "ABCDEFGHIJKLMNOPQRSTUVWXYZ" };
JSCChecker::JSCChecker(QObject *parent) :
QObject(parent)
{
}
bool cursorHasProperty(const QTextCursor &cursor, int property){
if(property < QTextFormat::UserProperty) {
return false;
}
if(cursor.charFormat().intProperty(property) == 1) {
return true;
}
const QList<QTextLayout::FormatRange>& formats = cursor.block().layout()->additionalFormats();
int pos = cursor.positionInBlock();
foreach(const QTextLayout::FormatRange& range, formats) {
if(pos > range.start && pos <= range.start + range.length && range.format.intProperty(property) == 1) {
return true;
}
}
return false;
}
QString nextChar(QTextCursor c){
QTextCursor cur(c);
cur.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
return cur.selectedText();
}
bool isNumeric(QString s){
return s.indexOf(QRegExp("^\\d+$")) == 0;
}
bool isWordChar(QString ch){
return ch.contains(QRegExp("^\\w$"));
}
void JSCChecker::checkRange(QTextEdit* edit, int start, int end)
{
if(end == -1){
QTextCursor tmpCursor(edit->textCursor());
tmpCursor.movePosition(QTextCursor::End);
end = tmpCursor.position();
}
// stop contentsChange signals from being emitted due to changed charFormats
edit->document()->blockSignals(true);
qDebug() << "checking range " << start << " - " << end;
QTextCharFormat errorFmt;
errorFmt.setFontUnderline(true);
errorFmt.setUnderlineColor(Qt::red);
errorFmt.setUnderlineStyle(QTextCharFormat::WaveUnderline);
QTextCharFormat defaultFormat = QTextCharFormat();
auto cursor = edit->textCursor();
cursor.beginEditBlock();
{
cursor.setPosition(start);
while(cursor.position() < end) {
bool correct = false;
cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
if(cursor.selectedText() == "@"){
cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
}
if(cursorHasProperty(cursor, CORRECT)){
correct = true;
} else {
QString word = cursor.selectedText();
// three or less is always "correct"
if(word.length() < 4 || isNumeric(word)){
correct = true;
} else {
bool found = false;
quint32 index = JSC::lookup(word, &found);
if(found){
correct = JSC::map[index].size == word.length();
}
if(!correct){
correct = Varicode::isValidCallsign(word, nullptr);
}
}
}
if(correct){
QTextCharFormat fmt = cursor.charFormat();
fmt.setFontUnderline(defaultFormat.fontUnderline());
fmt.setUnderlineColor(defaultFormat.underlineColor());
fmt.setUnderlineStyle(defaultFormat.underlineStyle());
cursor.setCharFormat(fmt);
} else {
cursor.mergeCharFormat(errorFmt);
}
// Go to next word start
//while(cursor.position() < end && !isWordChar(nextChar(cursor))){
// cursor.movePosition(QTextCursor::NextCharacter);
//}
cursor.movePosition(QTextCursor::NextCharacter);
}
}
cursor.endEditBlock();
edit->document()->blockSignals(false);
}
QStringList JSCChecker::suggestions(QString word, int n, bool *pFound){
QStringList s;
qDebug() << "computing suggestions for word" << word;
QMap<quint32, QString> m;
bool prefixFound = false;
// lookup actual word prefix that is not a single character
quint32 index = JSC::lookup(word, &prefixFound);
if(prefixFound){
auto t = JSC::map[index];
if(t.size > 1){
m[index] = QString::fromLatin1(t.str, t.size);
}
}
// 1-edit distance words (i.e., prefixed/suffixed/edited/removed characters)
for(int i = 0; i < 26; i++){
auto prefixed = ALPHABET.mid(i, 1) + word;
if(JSC::exists(prefixed, &index)){
m[index] = prefixed;
}
auto suffixed = word + ALPHABET.mid(i, 1);
if(JSC::exists(suffixed, &index)){
m[index] = suffixed;
}
for(int j = 0; j < word.length(); j++){
auto edited = word.mid(0, j) + ALPHABET.mid(i, 1) + word.mid(j + 1, word.length() - j);
if(JSC::exists(edited, &index)){
m[index] = edited;
}
}
}
for(int j = 0; j < word.length(); j++){
auto deleted = word.mid(0, j) + word.mid(j + 1, word.length() - j);
if(JSC::exists(deleted, &index)){
m[index] = deleted;
}
}
// return in order of probability (i.e., index rank)
int i = 0;
foreach(auto key, m.keys()){
if(i >= n){
break;
}
s.append(m[key]);
i++;
}
if(pFound) *pFound = prefixFound;
return s;
}

30
jsc_checker.h Normal file
View File

@ -0,0 +1,30 @@
#ifndef JSC_CHECKER_H
#define JSC_CHECKER_H
/**
* (C) 2018 Jordan Sherer <kn4crd@gmail.com> - All Rights Reserved
**/
#include <QObject>
class QTextEdit;
class JSCChecker : public QObject
{
Q_OBJECT
public:
explicit JSCChecker(/*QTextEdit * edit, */ QObject *parent = nullptr);
static void checkRange(QTextEdit * edit, int start, int end);
static QStringList suggestions(QString word, int n, bool *pFound);
signals:
public slots:
//void handleContentsChange(int, int, int);
private:
//QTextEdit *m_edit;
};
#endif // JSC_CHECKER_H

View File

@ -64,6 +64,7 @@
#include "messagereplydialog.h"
#include "DriftingDateTime.h"
#include "jsc.h"
#include "jsc_checker.h"
#include "ui_mainwindow.h"
#include "moc_mainwindow.cpp"
@ -1108,6 +1109,8 @@ MainWindow::MainWindow(QDir const& temp_directory, bool multiple,
ui->spotButton->setChecked(m_config.spot_to_reporting_networks());
//m_checker = new JSCChecker(ui->extFreeTextMsgEdit, this);
auto enterFilter = new EnterKeyPressEater();
connect(enterFilter, &EnterKeyPressEater::enterKeyPressed, this, [this](QObject *, QKeyEvent *, bool *pProcessed){
if(QApplication::keyboardModifiers() & Qt::ShiftModifier){
@ -1174,6 +1177,9 @@ MainWindow::MainWindow(QDir const& temp_directory, bool multiple,
connect(ui->extFreeTextMsgEdit, &QTableWidget::customContextMenuRequested, this, [this, clearAction2, clearActionAll, restoreAction](QPoint const &point){
QMenu * menu = new QMenu(ui->extFreeTextMsgEdit);
// spelling suggestions...
buildSuggestionsMenu(menu, ui->extFreeTextMsgEdit, point);
auto selectedCall = callsignSelected();
bool missingCallsign = selectedCall.isEmpty();
@ -5741,10 +5747,18 @@ void MainWindow::on_extFreeTextMsgEdit_currentTextChanged (QString const& text)
if(x != text){
int pos = ui->extFreeTextMsgEdit->textCursor().position();
int maxpos = x.size();
ui->extFreeTextMsgEdit->setPlainText(x);
// don't emit current text changed or text contents changed
ui->extFreeTextMsgEdit->blockSignals(true);
{
ui->extFreeTextMsgEdit->setPlainText(x);
}
ui->extFreeTextMsgEdit->blockSignals(false);
QTextCursor c = ui->extFreeTextMsgEdit->textCursor();
c.setPosition(pos < maxpos ? pos : maxpos, QTextCursor::MoveAnchor);
// highlight the block with our fonts
highlightBlock(c.block(), m_config.compose_text_font(), m_config.color_compose_foreground(), QColor(Qt::transparent));
ui->extFreeTextMsgEdit->setTextCursor(c);
@ -7425,6 +7439,46 @@ QString MainWindow::replaceMacros(QString const &text, QMap<QString, QString> va
return output;
}
void MainWindow::buildSuggestionsMenu(QMenu *menu, QTextEdit *edit, const QPoint &point){
bool found = false;
auto c = edit->cursorForPosition(point);
if(c.charFormat().underlineStyle() != QTextCharFormat::WaveUnderline){
return;
}
c.movePosition(QTextCursor::StartOfWord);
c.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
auto word = c.selectedText();
if(word.isEmpty()){
return;
}
QStringList suggestions = JSCChecker::suggestions(word, 10, &found);
if(suggestions.isEmpty() && !found){
return;
}
auto suggestionsMenu = menu->addMenu("Suggestions...");
if(suggestions.isEmpty()){
auto a = suggestionsMenu->addAction("No suggestions");
a->setDisabled(true);
} else {
foreach(auto suggestion, suggestions){
auto a = suggestionsMenu->addAction(suggestion);
connect(a, &QAction::triggered, this, [this, edit, point, suggestion](){
auto c = edit->cursorForPosition(point);
c.select(QTextCursor::WordUnderCursor);
c.insertText(suggestion);
});
}
}
menu->addSeparator();
}
void MainWindow::buildSavedMessagesMenu(QMenu *menu){
auto values = buildMacroValues();
@ -8623,6 +8677,7 @@ void MainWindow::refreshTextDisplay(){
m_txFrameCountEstimate = count;
m_txTextDirty = false;
updateTextWordCheckerDisplay();
updateTextStatsDisplay(transmitText, count);
updateTxButtonDisplay();
@ -8661,6 +8716,7 @@ void MainWindow::refreshTextDisplay(){
#endif
m_txTextDirty = false;
updateTextWordCheckerDisplay();
updateTextStatsDisplay(transmitText, m_txFrameCountEstimate);
updateTxButtonDisplay();
@ -8669,6 +8725,10 @@ void MainWindow::refreshTextDisplay(){
#endif
}
void MainWindow::updateTextWordCheckerDisplay(){
JSCChecker::checkRange(ui->extFreeTextMsgEdit, 0, -1);
}
void MainWindow::updateTextStatsDisplay(QString text, int count){
if(count > 0){
auto words = text.split(" ", QString::SkipEmptyParts).length();

View File

@ -90,6 +90,7 @@ class Detector;
class MultiSettings;
class EqualizationToolsDialog;
class DecodedText;
class JSCChecker;
using namespace std;
typedef std::function<void()> Callback;
@ -298,6 +299,7 @@ private slots:
void buildQueryMenu(QMenu *, QString callsign);
QMap<QString, QString> buildMacroValues();
QString replaceMacros(QString const &text, QMap<QString, QString> values, bool prune);
void buildSuggestionsMenu(QMenu *menu, QTextEdit *edit, const QPoint &point);
void buildSavedMessagesMenu(QMenu *menu);
void buildRelayMenu(QMenu *menu);
QAction* buildRelayAction(QString call);
@ -819,6 +821,8 @@ private:
QMap<QString, QMap<int, QList<ActivityDetail>>> m_bandActivityCache; // band -> band activity
QMap<QString, QString> m_rxTextCache; // band -> rx text
JSCChecker * m_checker;
QSet<QString> m_callSeenHeartbeat; // call
int m_previousFreq;
bool m_shouldRestoreFreq;
@ -915,6 +919,7 @@ private:
void updateRepeatButtonDisplay();
void updateTextDisplay();
void updateFrameCountEstimate(int count);
void updateTextWordCheckerDisplay();
void updateTextStatsDisplay(QString text, int count);
void updateTxButtonDisplay();
bool isMyCallIncluded(QString const &text);

View File

@ -77,7 +77,8 @@ SOURCES += \
DriftingDateTime.cpp \
jsc.cpp \
jsc_list.cpp \
jsc_map.cpp
jsc_map.cpp \
jsc_checker.cpp
HEADERS += qt_helpers.hpp \
pimpl_h.hpp pimpl_impl.hpp \
@ -105,7 +106,8 @@ HEADERS += qt_helpers.hpp \
messagereplydialog.h \
keyeater.h \
DriftingDateTime.h \
jsc.h
jsc.h \
jsc_checker.h
INCLUDEPATH += qmake_only