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:
parent
1aed1fde31
commit
95bbfb8232
@ -237,6 +237,7 @@ set (wsjtx_CXXSRCS
|
||||
jsc.cpp
|
||||
jsc_list.cpp
|
||||
jsc_map.cpp
|
||||
jsc_checker.cpp
|
||||
SelfDestructMessageBox.cpp
|
||||
messagereplydialog.cpp
|
||||
keyeater.cpp
|
||||
|
7
jsc.cpp
7
jsc.cpp
@ -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
1
jsc.h
@ -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
205
jsc_checker.cpp
Normal 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
30
jsc_checker.h
Normal 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
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user