Spell Checker

This commit is contained in:
Christophe Deschamps 2023-10-17 19:42:45 +00:00
parent a63348993d
commit 222c70e7c8
21 changed files with 802 additions and 6 deletions

View file

@ -329,6 +329,7 @@ set(SOURCES
src/components/other/timeZone/TimeZoneListModel.cpp
src/components/other/timeZone/TimeZoneProxyModel.cpp
src/components/other/units/Units.cpp
src/components/other/spell-checker/SpellChecker.cpp
src/components/participant/ParticipantModel.cpp
src/components/participant/ParticipantListModel.cpp
src/components/participant/ParticipantProxyModel.cpp
@ -479,6 +480,7 @@ set(HEADERS
src/components/other/timeZone/TimeZoneListModel.hpp
src/components/other/timeZone/TimeZoneProxyModel.hpp
src/components/other/units/Units.hpp
src/components/other/spell-checker/SpellChecker.hpp
src/components/participant/ParticipantModel.hpp
src/components/participant/ParticipantListModel.hpp
src/components/participant/ParticipantProxyModel.hpp
@ -554,6 +556,7 @@ if (APPLE)
src/components/other/desktop-tools/DesktopToolsMacOsNative.mm
src/components/other/desktop-tools/screen-saver/ScreenSaverMacOs.m
src/components/other/desktop-tools/state-process/StateProcessMacOs.mm
src/components/other/spell-checker/SpellCheckerMacOsNative.mm
)
list(APPEND HEADERS
#src/app/single-application/SingleApplicationPrivate.hpp
@ -565,6 +568,7 @@ elseif (WIN32)
#src/app/single-application/SingleApplication.cpp
src/components/core/event-count-notifier/EventCountNotifierSystemTrayIcon.cpp
src/components/other/desktop-tools/DesktopToolsWindows.cpp
src/components/other/spell-checker/SpellCheckerWindowsNative.cpp
)
list(APPEND HEADERS
#src/app/single-application/SingleApplicationPrivate.hpp
@ -578,6 +582,7 @@ else ()
src/components/other/desktop-tools/DesktopToolsLinux.cpp
src/components/other/desktop-tools/screen-saver/ScreenSaverDBus.cpp
src/components/other/desktop-tools/screen-saver/ScreenSaverXdg.cpp
src/components/other/spell-checker/SpellCheckerLinux.cpp
)
list(APPEND HEADERS
#src/app/single-application/SingleApplicationDBusPrivate.hpp

View file

@ -5188,4 +5188,23 @@ Click here: <a href="%1">%1</a>
</translation>
</message>
</context>
<context>
<name>SpellCheckerMenu</name>
<message>
<source>spellCheckingMenuDidYouMean</source>
<translation>Did you mean ?</translation>
</message>
<message>
<source>spellCheckingMenuAddToDictionary</source>
<translation>Add to Dictionnary</translation>
</message>
<message>
<source>spellCheckingMenuIgnoreOnce</source>
<translation>Ignore Once</translation>
</message>
<message>
<source>spellCheckingMenuIgnoreAll</source>
<translation>Ignore All</translation>
</message>
</context>
</TS>

View file

@ -5163,4 +5163,23 @@ Cliquez ici : &lt;a href=&quot;%1&quot;&gt;%1&lt;/a&gt;
</translation>
</message>
</context>
<context>
<name>SpellCheckerMenu</name>
<message>
<source>spellCheckingMenuDidYouMean</source>
<translation>Voulez-vous dire ?</translation>
</message>
<message>
<source>spellCheckingMenuAddToDictionary</source>
<translation>Ajouter au Dictionnaire</translation>
</message>
<message>
<source>spellCheckingMenuIgnoreOnce</source>
<translation>Ignorer une fois</translation>
</message>
<message>
<source>spellCheckingMenuIgnoreAll</source>
<translation>Ignorer tout</translation>
</message>
</context>
</TS>

View file

@ -199,6 +199,7 @@
<file>ui/modules/Common/Form/ComboBox.qml</file>
<file>ui/modules/Common/Form/CommonItemDelegate.qml</file>
<file>ui/modules/Common/Form/DroppableTextArea.qml</file>
<file>ui/modules/Common/Form/SpellCheckerMenu.qml</file>
<file>ui/modules/Common/Form/Fields/HexField.qml</file>
<file>ui/modules/Common/Form/Fields/NumericField.qml</file>
<file>ui/modules/Common/Form/Fields/PasswordField.qml</file>
@ -246,6 +247,7 @@
<file>ui/modules/Common/Menus/DropDownStaticMenuEntry.qml</file>
<file>ui/modules/Common/Menus/DropDownStaticMenu.qml</file>
<file>ui/modules/Common/Menus/MenuItem.qml</file>
<file>ui/modules/Common/Menus/MenuSeparator.qml</file>
<file>ui/modules/Common/Menus/Menu.qml</file>
<file>ui/modules/Common/Misc/Borders.qml</file>
<file>ui/modules/Common/Misc/ForceScrollBar.qml</file>
@ -300,6 +302,7 @@
<file>ui/modules/Common/Styles/Menus/DropDownStaticMenuStyle.qml</file>
<file>ui/modules/Common/Styles/Menus/MenuItemStyle.qml</file>
<file>ui/modules/Common/Styles/Menus/MenuStyle.qml</file>
<file>ui/modules/Common/Styles/Menus/MenuSeparatorStyle.qml</file>
<file>ui/modules/Common/Styles/Misc/ForceScrollBarStyle.qml</file>
<file>ui/modules/Common/Styles/Misc/MessageBannerStyle.qml</file>
<file>ui/modules/Common/Styles/Misc/PanedStyle.qml</file>

View file

@ -55,6 +55,7 @@
#include "components/history/CallHistoryProxyModel.hpp"
#include "components/other/desktop-tools/DesktopTools.hpp"
#include "components/other/date/DateModel.hpp"
#include "components/other/spell-checker/SpellChecker.hpp"
#include "components/settings/EmojisSettingsModel.hpp"
#include "components/timeline/TimelineModel.hpp"
@ -758,6 +759,7 @@ void App::registerTypes () {
registerType<ParticipantDeviceProxyModel>("ParticipantDeviceProxyModel");
registerType<SoundPlayer>("SoundPlayer");
registerType<TelephoneNumbersModel>("TelephoneNumbersModel");
registerType<SpellChecker>("SpellChecker");
registerSingletonType<AudioCodecsModel>("AudioCodecsModel");
registerSingletonType<OwnPresenceModel>("OwnPresenceModel");

View file

@ -130,4 +130,4 @@ void AppController::initQtAppDetails(){
QCoreApplication::setApplicationName(EXECUTABLE_NAME);
QApplication::setOrganizationDomain(EXECUTABLE_NAME);
QCoreApplication::setApplicationVersion(APPLICATION_SEMVER);
}
}

View file

@ -20,6 +20,7 @@
#include <QClipboard>
#include <QGuiApplication>
#include <QMimeData>
#include "Clipboard.hpp"
@ -36,3 +37,35 @@ QString Clipboard::getText () const {
void Clipboard::setText (const QString &text) {
QGuiApplication::clipboard()->setText(text, QClipboard::Clipboard);
}
void Clipboard::backup () {
if (QGuiApplication::clipboard() != nullptr) {
const QMimeData * clipboardData = QGuiApplication::clipboard()->mimeData();
mMimeCopy = new QMimeData();
foreach(const QString & format, clipboardData->formats())
mMimeCopy->setData(format, clipboardData->data(format));
}
}
void Clipboard::restore () {
if (QGuiApplication::clipboard() != nullptr && mMimeCopy != nullptr)
QGuiApplication::clipboard()->setMimeData(mMimeCopy);
}
QString Clipboard::getChatFormattedText () const {
QString text = getText();
if (text.isEmpty())
return text;
#ifdef linux
QString cr = "\n";
#endif
#ifdef WIN32
QString cr = "\n\r";
#endif
#ifdef __APPLE__
QString cr = "\n";
#endif
return text.replace(cr,"\u2028");
}

View file

@ -29,9 +29,12 @@ class Clipboard : public QObject {
Q_OBJECT;
Q_PROPERTY(QString text READ getText WRITE setText NOTIFY textChanged);
public:
Clipboard (QObject *parent = Q_NULLPTR);
Q_INVOKABLE void backup();
Q_INVOKABLE void restore();
Q_INVOKABLE QString getChatFormattedText() const;
signals:
void textChanged ();
@ -40,6 +43,7 @@ private:
QString getText () const;
void setText (const QString &text);
QMimeData *mMimeCopy;
};
#endif // ifndef CLIPBOARD_H_

View file

@ -0,0 +1,197 @@
/*
* Copyright (c) 2010-2024 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "SpellChecker.hpp"
#include <QRegularExpression>
#include <QTimer>
#include <QAbstractTextDocumentLayout>
#include <QTextEdit>
#ifdef WIN32
#include <spellcheck.h>
#endif
SpellChecker::SpellChecker(QObject *parent) : QSyntaxHighlighter(parent) {
errorFormater.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
errorFormater.setFontUnderline(true);
errorFormater.setUnderlineColor(Qt::red); // not supported before Qt6.2
QFontMetrics fm = QFontMetrics(QApplication::font());
wave = QString(u8"\uFE4B");
QRect boundingRect = fm.boundingRect(wave);
waveWidth = boundingRect.width();
graceTimer = new QTimer(this);
graceTimer->setSingleShot(true);
connect(graceTimer, SIGNAL(timeout()), SLOT(highlightAfterGracePeriod()));
setLanguage();
}
SpellChecker::~SpellChecker () {
graceTimer->stop();
#ifdef WIN32
if (mNativeSpellChecker != nullptr)
mNativeSpellChecker->Release();
#endif
}
void SpellChecker::setTextDocument(QQuickTextDocument *textDocument) {
setDocument(textDocument->textDocument());
}
/////////////////////////////////////////////////////////////////////////////////////////
// Should be this option but TextEdit/TextArea does not support setUnderlineColor
// (although QTextEdit does) until QT 6.2 (1d44ddf576 of qtdeclarative submodule)
/////////////////////////////////////////////////////////////////////////////////////////
void SpellChecker::highlightBlock(const QString &text) {
// setFormat(begin, length, errorFormater);
}
/////////////////////////////////////////////////////////////////////////////////////.
// QT5 Option using repeater/unicode inside QML with calculation of redline positions.
/////////////////////////////////////////////////////////////////////////////////////.
QString SpellChecker::underLine(qreal minLength) {
return wave.repeated(1+minLength/waveWidth);
}
void SpellChecker::highlightDocument() {
if (!fromTimer && QDateTime::currentMSecsSinceEpoch() <= mLastHightlight + GRACE_PERIOD_SECS*1000) {
scheduleHighlight();
return;
}
redLines.clear();
if (document() == nullptr) {
emit redLinesChanged();
return;
}
mLastHightlight = QDateTime::currentMSecsSinceEpoch();
QTextBlock::iterator blockIterator = document()->begin().begin();
QStringList newWords;
bool hadActiveWord = false;
QRegularExpression expression(WORD_DELIMITERS_REGEXP);
while (!blockIterator.atEnd()) {
QTextFragment fragment = blockIterator.fragment();
QFontMetrics metrics(fragment.charFormat().font());
QString text = fragment.text();
int position = 0;
QRegularExpressionMatchIterator blockWordsIterator = expression.globalMatch(text);
while (blockWordsIterator.hasNext()) {
QRegularExpressionMatch match = blockWordsIterator.next();
QString word = match.captured();
bool wordActive = !fromTimer && isWordActive(words, word, position);
int begin = match.capturedStart();
int length = match.capturedLength();
bool ignoreOnce = wasIgnoredOnce(word, match.capturedStart(),match.capturedEnd());
if (!wordActive && !ignoredAllWords.contains(word) && !ignoreOnce && !isValid(word) && !fragment.glyphRuns(begin,length).empty()) {
QRectF boundingRect = fragment.glyphRuns(begin,length).first().boundingRect();
QPointF start = boundingRect.bottomLeft();
qreal width = boundingRect.width();
redLines.append(QString::number(start.x())+","+QString::number(start.y())+","+QString::number(width)+","+underLine(width));
}
if (wordActive) {
hadActiveWord = wordActive;
}
newWords.append(word);
position++;
}
blockIterator++;
}
words = newWords;
if (hadActiveWord && !fromTimer) {
scheduleHighlight();
}
emit redLinesChanged();
}
void SpellChecker::clearHighlighting() {
redLines.clear();
emit redLinesChanged();
}
void SpellChecker::scheduleHighlight() {
graceTimer->start(GRACE_PERIOD_SECS*1000);
}
bool SpellChecker::isWordActive(QStringList words, QString word, int index) {
if (index >= words.length())
return true;
return words.at(index) != word ;
}
void SpellChecker::highlightAfterGracePeriod() {
fromTimer = true;
highlightDocument();
fromTimer = false;
}
int SpellChecker::wordPosition(int x, int y) {
int position = document()->documentLayout()->hitTest( QPointF( x, y ), Qt::ExactHit );
return position;
}
bool SpellChecker::isWordAtPositionValid(int cursorPosition) {
QTextCursor cursor(document());
cursor.setPosition(cursorPosition);
cursor.select(QTextCursor::WordUnderCursor);
QString word = cursor.selectedText();
return isValid(word);
}
void SpellChecker::ignoreAll(QString word) {
if (!ignoredAllWords.contains(word)) {
ignoredAllWords.append(word);
}
highlightDocument();
}
void SpellChecker::ignoreOnce(QString word, int cursorPosition) {
QTextCursor cursor(document());
cursor.setPosition(cursorPosition);
QTextBlock block = cursor.block();
ignoredOnce[cursorPosition] = word;
highlightDocument();
}
bool SpellChecker::wasIgnoredOnce(QString word, int wordStartIndex, int wordEndIndex) {
for (int i = wordStartIndex; i<=wordEndIndex; i++) {
if (ignoredOnce[i] == word)
return true;
}
return false;
}
void SpellChecker::replace(QString word, QString byWord, int cursorPosition) {
QTextCursor cursor(document());
cursor.setPosition(cursorPosition);
cursor.clearSelection();
cursor.movePosition(QTextCursor::StartOfWord, QTextCursor::MoveAnchor);
cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
cursor.insertText(byWord);
highlightDocument();
}

View file

@ -0,0 +1,106 @@
/*
* Copyright (c) 2010-2024 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef SPELLCHECKER_HPP_
#define SPELLCHECKER_HPP_
#include <stdio.h>
#include <QObject>
#include <QString>
#include <QDebug>
#include <QTextCharFormat>
#include <QSyntaxHighlighter>
#include <QTextDocument>
#include <QStringLiteral>
#include <QQuickTextDocument>
#include <QDateTime>
#include <QRegularExpression>
#include <QStringList>
#include <QTimer>
#include "app/App.hpp"
#define SUGGESTIONS_LIMIT 10
#define GRACE_PERIOD_SECS 1.0
#define LOG_TAG "[SpellChecker]"
#define WORD_DELIMITERS_REGEXP "[^\r\n\t\u2028 ]+"
#ifdef WIN32
class ISpellChecker;
#endif
class SpellChecker : public QSyntaxHighlighter {
Q_OBJECT
public:
SpellChecker(QObject* parent = nullptr);
~SpellChecker();
// Common
static QString currentLanguage() { return App::getInstance()->getLocale().name();}
Q_INVOKABLE void setTextDocument(QQuickTextDocument *textDocument);
Q_INVOKABLE int wordPosition(int x, int y);
Q_INVOKABLE bool isWordAtPositionValid(int cursorPosition);
Q_INVOKABLE void highlightDocument();
Q_INVOKABLE void clearHighlighting();
Q_INVOKABLE void ignoreOnce(QString word, int cursorPosition);
Q_INVOKABLE void ignoreAll(QString word);
Q_INVOKABLE void replace(QString word, QString byWord, int cursorPosition);
Q_PROPERTY(QStringList redLines MEMBER redLines NOTIFY redLinesChanged);
// Native (Mac/Windows) or ISpell
Q_INVOKABLE void learn(QString word);
Q_INVOKABLE QStringList suggestionsForWord(QString word);
bool isValid(QString word);
protected:
void highlightBlock(const QString &text) override;
public slots:
void highlightAfterGracePeriod();
signals:
void redLinesChanged();
private:
QTextCharFormat errorFormater;
QTimer *graceTimer;
bool fromTimer = false;
QStringList ignoredAllWords;
QList<QPair<QString, int>> ignoredOnceWords;
QStringList redLines;
QStringList words;
QHash<int,QString> ignoredOnce;
QString wave;
qreal waveWidth;
qint64 mLastHightlight;
void setLanguage();
bool isWordActive(QStringList words, QString word, int index);
bool wasIgnoredOnce(QString word, int wordStartIndex, int wordEndIndex);
void scheduleHighlight();
QString underLine(qreal minLength);
#ifdef WIN32
ISpellChecker* mNativeSpellChecker = nullptr;
#endif
};
#endif /* SPELLCHECKER_HPP_ */

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2010-2024 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* 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 <http://www.gnu.org/licenses/>.
*/
#import "SpellChecker.hpp"
void SpellChecker::setLanguage() {
}
bool SpellChecker::isValid(QString word) {
return true;
}
void SpellChecker::learn(QString word){
}
QStringList SpellChecker::suggestionsForWord(QString word) {
QStringList suggestions;
return suggestions;
}

View file

@ -0,0 +1,66 @@
/*
* Copyright (c) 2010-2024 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* 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 <http://www.gnu.org/licenses/>.
*/
#import <AppKit/AppKit.h>
#import "SpellChecker.hpp"
void SpellChecker::setLanguage() {
NSSpellChecker *spellChecker = [NSSpellChecker sharedSpellChecker];
QString locale = SpellChecker::currentLanguage();
if ([spellChecker setLanguage:locale.toNSString()]) {
[spellChecker updatePanels];
qDebug() << LOG_TAG << "Macos native spell checker Language set to " << locale;
} else {
qWarning() << LOG_TAG << "Macos native spell checker unable to set language to " << locale;
}
}
bool SpellChecker::isValid(QString word) {
NSSpellChecker *spellChecker = [NSSpellChecker sharedSpellChecker];
QString locale = SpellChecker::currentLanguage();
bool isValid = [spellChecker checkSpellingOfString:word.toNSString() startingAt:0 language:locale.toNSString() wrap:NO inSpellDocumentWithTag:0 wordCount:nullptr].length == 0;
return isValid;
}
void SpellChecker::learn(QString word){
NSSpellChecker *spellChecker = [NSSpellChecker sharedSpellChecker];
NSString *_word = word.toNSString();
if (![spellChecker hasLearnedWord:_word]) {
[spellChecker learnWord:_word];
[spellChecker updatePanels];
}
highlightDocument();
}
QStringList SpellChecker::suggestionsForWord(QString word) {
NSSpellChecker *spellChecker = [NSSpellChecker sharedSpellChecker];
NSString *_word = word.toNSString();
QString locale = SpellChecker::currentLanguage();
NSArray *_suggestions = [spellChecker guessesForWordRange:NSMakeRange(0, word.length()) inString:_word language:locale.toNSString() inSpellDocumentWithTag:0];
QStringList suggestions;
for (NSString *_suggestion in _suggestions) {
QString suggestion = QString::fromNSString(_suggestion);
suggestions << suggestion;
if (suggestions.length() >= SUGGESTIONS_LIMIT)
return suggestions;
}
return suggestions;
}

View file

@ -0,0 +1,113 @@
/*
* Copyright (c) 2010-2024 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include <spellcheck.h>
#include "SpellChecker.hpp"
void SpellChecker::setLanguage() {
ISpellCheckerFactory* spellCheckerFactory;
HRESULT hr = CoCreateInstance(__uuidof(SpellCheckerFactory), nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&spellCheckerFactory));
if (!SUCCEEDED(hr)) {
qWarning() << LOG_TAG << "Windows native spell checker unable to create spell checker factory";
return;
}
QString locale = SpellChecker::currentLanguage().toUpper();
LPCWSTR _locale = (const wchar_t*) locale.utf16();
BOOL isSupported = FALSE;
hr = spellCheckerFactory->IsSupported(_locale, &isSupported);
if (!SUCCEEDED(hr)) {
qWarning() << LOG_TAG << "Windows native spell checker unable to check if language is supported" << locale;
return;
}
if (!isSupported) {
qWarning() << LOG_TAG << "Windows native spell checker Language is not supported" << locale;
locale = locale.mid(0,2);
qWarning() << LOG_TAG << "Windows native spell checker trying with" << locale;
_locale = (const wchar_t*) locale.utf16();
hr = spellCheckerFactory->IsSupported(_locale, &isSupported);
if (!SUCCEEDED(hr)) {
qWarning() << LOG_TAG << "Windows native spell checker unable to check if language is supported" << locale;
return;
}
if (!isSupported) {
qWarning() << LOG_TAG << "Windows native spell checker Language is not supported" << locale;
return;
}
}
hr = spellCheckerFactory->CreateSpellChecker(_locale, &mNativeSpellChecker);
if (!SUCCEEDED(hr)) {
qWarning() << LOG_TAG << "Windows native spell checker unable to create spell checker";
return;
}
qWarning() << LOG_TAG << "Windows native spell checker created for locale" << locale;
}
bool SpellChecker::isValid(QString word) {
if (mNativeSpellChecker == nullptr)
return true;
wchar_t *text = reinterpret_cast<wchar_t *>(word.data());
IEnumSpellingError* enumSpellingError = nullptr;
ISpellingError* spellingError = nullptr;
HRESULT hr = mNativeSpellChecker->Check(text, &enumSpellingError);
if (SUCCEEDED(hr)) {
hr = enumSpellingError->Next(&spellingError);
enumSpellingError->Release();
return hr != S_OK;
} else
return true;
}
void SpellChecker::learn(QString word){
if (mNativeSpellChecker == nullptr)
return;
wchar_t *text = reinterpret_cast<wchar_t *>(word.data());
HRESULT hr = mNativeSpellChecker->Add(text);
if (!SUCCEEDED(hr))
qWarning() << LOG_TAG << "Windows native spell checke unable to add word to dictionary" << word;
highlightDocument();
}
QStringList SpellChecker::suggestionsForWord(QString word) {
QStringList suggestions;
if (mNativeSpellChecker == nullptr)
return suggestions;
wchar_t *text = reinterpret_cast<wchar_t *>(word.data());
IEnumString* enumString = nullptr;
HRESULT hr = mNativeSpellChecker->Suggest(text, &enumString);
if (SUCCEEDED(hr)) {
while (S_OK == hr) {
LPOLESTR string = nullptr;
hr = enumString->Next(1, &string, nullptr);
if (S_OK == hr) {
suggestions << QString::fromWCharArray(string);
CoTaskMemFree(string);
if (suggestions.length() >= SUGGESTIONS_LIMIT) {
enumString->Release();
return suggestions;
}
}
}
enumString->Release();
}
return suggestions;
}

View file

@ -9,6 +9,7 @@ import Common.Styles 1.0
import Units 1.0
import Utils 1.0
import UtilsCpp 1.0
import Clipboard 1.0
import 'qrc:/ui/scripts/Utils/utils.js' as Utils
@ -184,6 +185,7 @@ Item {
clearDelay.restart()
}
}
spellChecker.highlightDocument()
}
function handleValidation () {
var plainText = getText(0, text.length)
@ -213,7 +215,46 @@ Item {
height:flickableArea.height
//onHeightChanged: height=flickableArea.height//TextArea change its height from content text. Force it to parent
Component.onCompleted: forceActiveFocus()
property int previousFlickableAreaWidth
onWidthChanged: {
if (previousFlickableAreaWidth != flickableArea.width) {
spellChecker.clearHighlighting()
spellChecker.highlightDocument()
}
previousFlickableAreaWidth = flickableArea.width
}
SpellChecker {
id: spellChecker
}
SpellCheckerMenu {
id: spellCheckerMenu
}
Repeater {
id: spellCheckerUnderliner
model: spellChecker.redLines
Rectangle {
clip: true
height: 5
x: textArea.leftPadding + parseFloat(modelData.split(',')[0])
y: textArea.topPadding + parseFloat(modelData.split(',')[1])-3
width: parseFloat(modelData.split(',')[2])
color: 'transparent'
Text {
anchors.top: parent.top
text: modelData.split(',')[3]
color: DroppableTextAreaStyle.spellChecker.underlineWave.colorModel.color
}
}
}
Component.onCompleted: {
forceActiveFocus()
spellChecker.setTextDocument(textDocument)
spellCheckerMenu.spellChecker = spellChecker
}
property var isAutoRepeating : false // shutdown repeating key feature to let optional menu appears and do normal stuff (like accents menu)
Keys.onReleased: {
@ -222,7 +263,9 @@ Item {
if(event.key > Qt.Key_Any && event.key <= Qt.Key_ydiaeresis)// Remove the previous character if it is a printable character
textArea.remove(cursorPosition-1, cursorPosition)
}
}else
} else if (event.matches(StandardKey.Paste)) {
Clipboard.restore()
} else
isAutoRepeating = false// We are no more repeating. Final decision is done on Releasing
}
Keys.onPressed: {
@ -231,10 +274,27 @@ Item {
if(event.key > Qt.Key_Any && event.key <= Qt.Key_ydiaeresis){// Ignore character if it is repeating and printable character
event.accepted = true
}
}else if (event.matches(StandardKey.InsertLineSeparator)) {
} else if (event.matches(StandardKey.InsertLineSeparator)) {
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
handleValidation()
event.accepted = true
} else if (event.matches(StandardKey.Paste)) {
Clipboard.backup() // Restored on Keys.onReleased
Clipboard.text = Clipboard.getChatFormattedText()
event.accepted = false
}
}
onPressed: {
if (event.button == Qt.RightButton) {
var wordPosition = spellChecker.wordPosition(event.x,event.y)
if (wordPosition != -1) {
var wordValid = spellChecker.isWordAtPositionValid(wordPosition)
if (!wordValid) {
cursorPosition = wordPosition
selectWord()
spellCheckerMenu.open(selectedText,wordPosition)
}
}
}
}
}

View file

@ -0,0 +1,87 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
import QtQuick.Controls 2.7
import Clipboard 1.0
import Common 1.0
import Linphone 1.0
import Common.Styles 1.0
import Linphone.Styles 1.0
import Utils 1.0
import Units 1.0
import ConstantsCpp 1.0
import UtilsCpp 1.0
import LinphoneEnums 1.0
// =============================================================================
// SpellCheckerMenu
Item {
property string _word
property int _position
property SpellChecker spellChecker
property var suggestions: []
function open(word, position){
_word = word
_position = position
suggestions = spellChecker.suggestionsForWord(word)
spellCheckerMenu.popup()
}
Menu {
id: spellCheckerMenu
menuStyle : MenuStyle.aux
MenuItem {
//: 'Did you mean ?' : Suggest new words
text: qsTr('spellCheckingMenuDidYouMean')
menuItemStyle : MenuItemStyle.aux
visible:suggestions.length != 0
enabled: false
}
Repeater {
model: suggestions
MenuItem {
text:modelData
menuItemStyle : MenuItemStyle.aux
onTriggered: spellChecker.replace(_word,modelData,_position)
fontItalic: true
}
}
MenuItem { // Work around to anchor separator below.
visible:false
}
MenuSeparator {
visible:suggestions.length != 0
}
MenuItem {
//: 'Add to dictionary' : Add word to dictionary
text: qsTr('spellCheckingMenuAddToDictionary')
menuItemStyle : MenuItemStyle.aux
onTriggered: spellChecker.learn(_word)
}
MenuItem {
//: 'Ignore Once' : Ignore spell checking only for this occurences
text: qsTr('spellCheckingMenuIgnoreOnce')
menuItemStyle : MenuItemStyle.aux
onTriggered: spellChecker.ignoreOnce(_word, _position)
}
MenuItem {
//: 'Ignore All' : Ignore spell checking for all occurences
text: qsTr('spellCheckingMenuIgnoreAll')
menuItemStyle : MenuItemStyle.aux
onTriggered: spellChecker.ignoreAll(_word)
}
}
}

View file

@ -26,7 +26,8 @@ Controls.MenuItem {
property int offsetBottomMargin : 0
property bool displaySelection: true
property bool isTabBar: false
property bool fontItalic: false
height:visible?undefined:0
Component.onCompleted: if(!isTabBar) menu.width = Math.max(menu.width, implicitWidth)
@ -87,6 +88,7 @@ Controls.MenuItem {
font {
weight: menuItemStyle.text.weight
pointSize: menuItemStyle.text.pointSize
italic: fontItalic
}
text: button.text

View file

@ -0,0 +1,17 @@
import QtQuick 2.7
import QtQuick.Controls 2.3 as Controls
import Common 1.0
import Common.Styles 1.0
// =============================================================================
Controls.MenuSeparator {
padding: 0
topPadding: MenuSeparatorStyle.topPadding
bottomPadding: MenuSeparatorStyle.bottomPadding
contentItem: Rectangle {
implicitHeight: MenuSeparatorStyle.height
color: MenuSeparatorStyle.colorModel.color
}
}

View file

@ -3,6 +3,8 @@ import QtQml 2.2
import Units 1.0
import ColorsList 1.0
import Clipboard 1.0
// =============================================================================
@ -92,4 +94,10 @@ QtObject {
property var colorModel: ColorsList.add(sectionName+'_Chat_text', 'd')
property int pointSize: Units.dp * 10
}
property QtObject spellChecker: QtObject {
property QtObject underlineWave: QtObject {
property var colorModel: ColorsList.add(sectionName+'_Chat_spell', 'error')
}
}
}

View file

@ -0,0 +1,14 @@
pragma Singleton
import QtQml 2.2
import ColorsList 1.0
// =============================================================================
QtObject {
property string sectionName: 'MenuSeparator'
property var colorModel: ColorsList.add(sectionName+'_n', 'u')
property int topPadding: 0
property int bottomPadding: 0
property int height : 1
}

View file

@ -54,6 +54,8 @@ singleton ApplicationMenuStyle 1.0 Menus/ApplicationMenuStyle.qml
singleton DropDownStaticMenuStyle 1.0 Menus/DropDownStaticMenuStyle.qml
singleton MenuItemStyle 1.0 Menus/MenuItemStyle.qml
singleton MenuStyle 1.0 Menus/MenuStyle.qml
singleton MenuSeparatorStyle 1.0 Menus/MenuSeparatorStyle.qml
singleton ForceScrollBarStyle 1.0 Misc/ForceScrollBarStyle.qml
singleton MessageBannerStyle 1.0 Misc/MessageBannerStyle.qml

View file

@ -86,6 +86,8 @@ DropDownStaticMenu 1.0 Menus/DropDownStaticMenu.qml
DropDownStaticMenuEntry 1.0 Menus/DropDownStaticMenuEntry.qml
Menu 1.0 Menus/Menu.qml
MenuItem 1.0 Menus/MenuItem.qml
MenuSeparator 1.0 Menus/MenuSeparator.qml
Borders 1.0 Misc/Borders.qml
ForceScrollBar 1.0 Misc/ForceScrollBar.qml