mirror of
https://gitlab.linphone.org/BC/public/linphone-desktop.git
synced 2026-04-17 20:08:28 +00:00
Spell Checker
This commit is contained in:
parent
a63348993d
commit
222c70e7c8
21 changed files with 802 additions and 6 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -5163,4 +5163,23 @@ Cliquez ici : <a href="%1">%1</a>
|
|||
</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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -130,4 +130,4 @@ void AppController::initQtAppDetails(){
|
|||
QCoreApplication::setApplicationName(EXECUTABLE_NAME);
|
||||
QApplication::setOrganizationDomain(EXECUTABLE_NAME);
|
||||
QCoreApplication::setApplicationVersion(APPLICATION_SEMVER);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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_
|
||||
|
|
|
|||
197
linphone-app/src/components/other/spell-checker/SpellChecker.cpp
Normal file
197
linphone-app/src/components/other/spell-checker/SpellChecker.cpp
Normal 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();
|
||||
}
|
||||
106
linphone-app/src/components/other/spell-checker/SpellChecker.hpp
Normal file
106
linphone-app/src/components/other/spell-checker/SpellChecker.hpp
Normal 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_ */
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
87
linphone-app/ui/modules/Common/Form/SpellCheckerMenu.qml
Normal file
87
linphone-app/ui/modules/Common/Form/SpellCheckerMenu.qml
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
17
linphone-app/ui/modules/Common/Menus/MenuSeparator.qml
Normal file
17
linphone-app/ui/modules/Common/Menus/MenuSeparator.qml
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue