diff --git a/linphone-app/CMakeLists.txt b/linphone-app/CMakeLists.txt
index f4f03142c..4a5286389 100644
--- a/linphone-app/CMakeLists.txt
+++ b/linphone-app/CMakeLists.txt
@@ -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
diff --git a/linphone-app/assets/languages/en.ts b/linphone-app/assets/languages/en.ts
index 265e09d83..393fbe281 100644
--- a/linphone-app/assets/languages/en.ts
+++ b/linphone-app/assets/languages/en.ts
@@ -5188,4 +5188,23 @@ Click here: <a href="%1">%1</a>
+
+ SpellCheckerMenu
+
+ spellCheckingMenuDidYouMean
+ Did you mean ?
+
+
+ spellCheckingMenuAddToDictionary
+ Add to Dictionnary
+
+
+ spellCheckingMenuIgnoreOnce
+ Ignore Once
+
+
+ spellCheckingMenuIgnoreAll
+ Ignore All
+
+
diff --git a/linphone-app/assets/languages/fr_FR.ts b/linphone-app/assets/languages/fr_FR.ts
index ab8c20d6f..a71e268e7 100644
--- a/linphone-app/assets/languages/fr_FR.ts
+++ b/linphone-app/assets/languages/fr_FR.ts
@@ -5163,4 +5163,23 @@ Cliquez ici : <a href="%1">%1</a>
+
+ SpellCheckerMenu
+
+ spellCheckingMenuDidYouMean
+ Voulez-vous dire ?
+
+
+ spellCheckingMenuAddToDictionary
+ Ajouter au Dictionnaire
+
+
+ spellCheckingMenuIgnoreOnce
+ Ignorer une fois
+
+
+ spellCheckingMenuIgnoreAll
+ Ignorer tout
+
+
diff --git a/linphone-app/resources.qrc b/linphone-app/resources.qrc
index bbc54cd79..b889a0463 100644
--- a/linphone-app/resources.qrc
+++ b/linphone-app/resources.qrc
@@ -199,6 +199,7 @@
ui/modules/Common/Form/ComboBox.qml
ui/modules/Common/Form/CommonItemDelegate.qml
ui/modules/Common/Form/DroppableTextArea.qml
+ ui/modules/Common/Form/SpellCheckerMenu.qml
ui/modules/Common/Form/Fields/HexField.qml
ui/modules/Common/Form/Fields/NumericField.qml
ui/modules/Common/Form/Fields/PasswordField.qml
@@ -246,6 +247,7 @@
ui/modules/Common/Menus/DropDownStaticMenuEntry.qml
ui/modules/Common/Menus/DropDownStaticMenu.qml
ui/modules/Common/Menus/MenuItem.qml
+ ui/modules/Common/Menus/MenuSeparator.qml
ui/modules/Common/Menus/Menu.qml
ui/modules/Common/Misc/Borders.qml
ui/modules/Common/Misc/ForceScrollBar.qml
@@ -300,6 +302,7 @@
ui/modules/Common/Styles/Menus/DropDownStaticMenuStyle.qml
ui/modules/Common/Styles/Menus/MenuItemStyle.qml
ui/modules/Common/Styles/Menus/MenuStyle.qml
+ ui/modules/Common/Styles/Menus/MenuSeparatorStyle.qml
ui/modules/Common/Styles/Misc/ForceScrollBarStyle.qml
ui/modules/Common/Styles/Misc/MessageBannerStyle.qml
ui/modules/Common/Styles/Misc/PanedStyle.qml
diff --git a/linphone-app/src/app/App.cpp b/linphone-app/src/app/App.cpp
index 8f43b008f..e135fefec 100644
--- a/linphone-app/src/app/App.cpp
+++ b/linphone-app/src/app/App.cpp
@@ -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");
registerType("SoundPlayer");
registerType("TelephoneNumbersModel");
+ registerType("SpellChecker");
registerSingletonType("AudioCodecsModel");
registerSingletonType("OwnPresenceModel");
diff --git a/linphone-app/src/app/AppController.cpp b/linphone-app/src/app/AppController.cpp
index d1ae309af..7367308c8 100644
--- a/linphone-app/src/app/AppController.cpp
+++ b/linphone-app/src/app/AppController.cpp
@@ -130,4 +130,4 @@ void AppController::initQtAppDetails(){
QCoreApplication::setApplicationName(EXECUTABLE_NAME);
QApplication::setOrganizationDomain(EXECUTABLE_NAME);
QCoreApplication::setApplicationVersion(APPLICATION_SEMVER);
-}
\ No newline at end of file
+}
diff --git a/linphone-app/src/components/other/clipboard/Clipboard.cpp b/linphone-app/src/components/other/clipboard/Clipboard.cpp
index 297621c80..2d6b9cf05 100644
--- a/linphone-app/src/components/other/clipboard/Clipboard.cpp
+++ b/linphone-app/src/components/other/clipboard/Clipboard.cpp
@@ -20,6 +20,7 @@
#include
#include
+#include
#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");
+}
+
+
diff --git a/linphone-app/src/components/other/clipboard/Clipboard.hpp b/linphone-app/src/components/other/clipboard/Clipboard.hpp
index 04d7856cb..7da74b9f1 100644
--- a/linphone-app/src/components/other/clipboard/Clipboard.hpp
+++ b/linphone-app/src/components/other/clipboard/Clipboard.hpp
@@ -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_
diff --git a/linphone-app/src/components/other/spell-checker/SpellChecker.cpp b/linphone-app/src/components/other/spell-checker/SpellChecker.cpp
new file mode 100644
index 000000000..0bded389c
--- /dev/null
+++ b/linphone-app/src/components/other/spell-checker/SpellChecker.cpp
@@ -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 .
+ */
+
+#include "SpellChecker.hpp"
+#include
+#include
+#include
+#include
+#ifdef WIN32
+#include
+#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();
+}
diff --git a/linphone-app/src/components/other/spell-checker/SpellChecker.hpp b/linphone-app/src/components/other/spell-checker/SpellChecker.hpp
new file mode 100644
index 000000000..b409c97ed
--- /dev/null
+++ b/linphone-app/src/components/other/spell-checker/SpellChecker.hpp
@@ -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 .
+ */
+
+#ifndef SPELLCHECKER_HPP_
+#define SPELLCHECKER_HPP_
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#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> ignoredOnceWords;
+ QStringList redLines;
+ QStringList words;
+ QHash 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_ */
diff --git a/linphone-app/src/components/other/spell-checker/SpellCheckerLinux.cpp b/linphone-app/src/components/other/spell-checker/SpellCheckerLinux.cpp
new file mode 100644
index 000000000..92f4de3ec
--- /dev/null
+++ b/linphone-app/src/components/other/spell-checker/SpellCheckerLinux.cpp
@@ -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 .
+ */
+
+
+#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;
+}
diff --git a/linphone-app/src/components/other/spell-checker/SpellCheckerMacOsNative.mm b/linphone-app/src/components/other/spell-checker/SpellCheckerMacOsNative.mm
new file mode 100644
index 000000000..273072464
--- /dev/null
+++ b/linphone-app/src/components/other/spell-checker/SpellCheckerMacOsNative.mm
@@ -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 .
+ */
+
+
+#import
+#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;
+}
diff --git a/linphone-app/src/components/other/spell-checker/SpellCheckerWindowsNative.cpp b/linphone-app/src/components/other/spell-checker/SpellCheckerWindowsNative.cpp
new file mode 100644
index 000000000..9a761216e
--- /dev/null
+++ b/linphone-app/src/components/other/spell-checker/SpellCheckerWindowsNative.cpp
@@ -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 .
+ */
+
+#include
+#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(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(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(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;
+}
diff --git a/linphone-app/ui/modules/Common/Form/DroppableTextArea.qml b/linphone-app/ui/modules/Common/Form/DroppableTextArea.qml
index 98154cc55..597aaf6e3 100644
--- a/linphone-app/ui/modules/Common/Form/DroppableTextArea.qml
+++ b/linphone-app/ui/modules/Common/Form/DroppableTextArea.qml
@@ -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)
+ }
+ }
}
}
}
diff --git a/linphone-app/ui/modules/Common/Form/SpellCheckerMenu.qml b/linphone-app/ui/modules/Common/Form/SpellCheckerMenu.qml
new file mode 100644
index 000000000..625106c0b
--- /dev/null
+++ b/linphone-app/ui/modules/Common/Form/SpellCheckerMenu.qml
@@ -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)
+ }
+
+ }
+}
diff --git a/linphone-app/ui/modules/Common/Menus/MenuItem.qml b/linphone-app/ui/modules/Common/Menus/MenuItem.qml
index 91e301dad..8dd9eba28 100644
--- a/linphone-app/ui/modules/Common/Menus/MenuItem.qml
+++ b/linphone-app/ui/modules/Common/Menus/MenuItem.qml
@@ -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
diff --git a/linphone-app/ui/modules/Common/Menus/MenuSeparator.qml b/linphone-app/ui/modules/Common/Menus/MenuSeparator.qml
new file mode 100644
index 000000000..989daab6a
--- /dev/null
+++ b/linphone-app/ui/modules/Common/Menus/MenuSeparator.qml
@@ -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
+ }
+}
diff --git a/linphone-app/ui/modules/Common/Styles/Form/DroppableTextAreaStyle.qml b/linphone-app/ui/modules/Common/Styles/Form/DroppableTextAreaStyle.qml
index 6fff7fd76..6246c0c53 100644
--- a/linphone-app/ui/modules/Common/Styles/Form/DroppableTextAreaStyle.qml
+++ b/linphone-app/ui/modules/Common/Styles/Form/DroppableTextAreaStyle.qml
@@ -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')
+ }
+ }
}
diff --git a/linphone-app/ui/modules/Common/Styles/Menus/MenuSeparatorStyle.qml b/linphone-app/ui/modules/Common/Styles/Menus/MenuSeparatorStyle.qml
new file mode 100644
index 000000000..ee37c674c
--- /dev/null
+++ b/linphone-app/ui/modules/Common/Styles/Menus/MenuSeparatorStyle.qml
@@ -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
+}
diff --git a/linphone-app/ui/modules/Common/Styles/qmldir b/linphone-app/ui/modules/Common/Styles/qmldir
index c8381758b..9effa4eec 100644
--- a/linphone-app/ui/modules/Common/Styles/qmldir
+++ b/linphone-app/ui/modules/Common/Styles/qmldir
@@ -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
diff --git a/linphone-app/ui/modules/Common/qmldir b/linphone-app/ui/modules/Common/qmldir
index 1b6b6ce99..269e34232 100644
--- a/linphone-app/ui/modules/Common/qmldir
+++ b/linphone-app/ui/modules/Common/qmldir
@@ -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