Skip to content
Snippets Groups Projects
Commit 2a72da56 authored by Jérôme Lamy's avatar Jérôme Lamy :chicken: Committed by Page Magnier-Slimani
Browse files

spellcheck: for linux system dicts

Implement a first version of the spellcheck for linux that use the
systemwide installed dictionaries.

GitLab: #1997

Change-Id: I7158e6c61061e7d0a7fe651069247227bbe399a4
parent 88d05390
Branches
Tags
No related merge requests found
Showing
with 527 additions and 66 deletions
...@@ -31,3 +31,7 @@ ...@@ -31,3 +31,7 @@
path = 3rdparty/zxing-cpp path = 3rdparty/zxing-cpp
url = https://github.com/nu-book/zxing-cpp.git url = https://github.com/nu-book/zxing-cpp.git
ignore = dirty ignore = dirty
[submodule "3rdparty/hunspell"]
path = 3rdparty/hunspell
url = https://gitlab.savoirfairelinux.com/jami/hunspell.git
ignore = dirty
hunspell @ 525f9f22
Subproject commit 525f9f22766a28e0f81c435217fcf4528e01c013
...@@ -255,7 +255,7 @@ set(PYTHON_EXEC ${Python3_EXECUTABLE}) ...@@ -255,7 +255,7 @@ set(PYTHON_EXEC ${Python3_EXECUTABLE})
# Versioning and build ID generation # Versioning and build ID generation
set(VERSION_FILE ${CMAKE_CURRENT_BINARY_DIR}/version_info.cpp) set(VERSION_FILE ${CMAKE_CURRENT_BINARY_DIR}/version_info.cpp)
# Touch the file to make sure it exists at configure time as # Touch the file to ensure it exists at configure time as
# we add it to the target_sources below. # we add it to the target_sources below.
file(TOUCH ${VERSION_FILE}) file(TOUCH ${VERSION_FILE})
add_custom_target( add_custom_target(
...@@ -347,6 +347,7 @@ set(COMMON_SOURCES ...@@ -347,6 +347,7 @@ set(COMMON_SOURCES
${APP_SRC_DIR}/conversationlistmodel.cpp ${APP_SRC_DIR}/conversationlistmodel.cpp
${APP_SRC_DIR}/searchresultslistmodel.cpp ${APP_SRC_DIR}/searchresultslistmodel.cpp
${APP_SRC_DIR}/calloverlaymodel.cpp ${APP_SRC_DIR}/calloverlaymodel.cpp
${APP_SRC_DIR}/spellcheckdictionarymanager.cpp
${APP_SRC_DIR}/filestosendlistmodel.cpp ${APP_SRC_DIR}/filestosendlistmodel.cpp
${APP_SRC_DIR}/wizardviewstepmodel.cpp ${APP_SRC_DIR}/wizardviewstepmodel.cpp
${APP_SRC_DIR}/avatarregistry.cpp ${APP_SRC_DIR}/avatarregistry.cpp
...@@ -361,13 +362,13 @@ set(COMMON_SOURCES ...@@ -361,13 +362,13 @@ set(COMMON_SOURCES
${APP_SRC_DIR}/currentcall.cpp ${APP_SRC_DIR}/currentcall.cpp
${APP_SRC_DIR}/messageparser.cpp ${APP_SRC_DIR}/messageparser.cpp
${APP_SRC_DIR}/previewengine.cpp ${APP_SRC_DIR}/previewengine.cpp
${APP_SRC_DIR}/imagedownloader.cpp ${APP_SRC_DIR}/filedownloader.cpp
${APP_SRC_DIR}/pluginversionmanager.cpp ${APP_SRC_DIR}/pluginversionmanager.cpp
${APP_SRC_DIR}/connectioninfolistmodel.cpp ${APP_SRC_DIR}/connectioninfolistmodel.cpp
${APP_SRC_DIR}/pluginversionmanager.cpp ${APP_SRC_DIR}/pluginversionmanager.cpp
${APP_SRC_DIR}/linkdevicemodel.cpp ${APP_SRC_DIR}/linkdevicemodel.cpp
${APP_SRC_DIR}/qrcodescannermodel.cpp ${APP_SRC_DIR}/qrcodescannermodel.cpp
) ${APP_SRC_DIR}/spellchecker.cpp)
set(COMMON_HEADERS set(COMMON_HEADERS
${APP_SRC_DIR}/global.h ${APP_SRC_DIR}/global.h
...@@ -419,6 +420,7 @@ set(COMMON_HEADERS ...@@ -419,6 +420,7 @@ set(COMMON_HEADERS
${APP_SRC_DIR}/conversationlistmodel.h ${APP_SRC_DIR}/conversationlistmodel.h
${APP_SRC_DIR}/searchresultslistmodel.h ${APP_SRC_DIR}/searchresultslistmodel.h
${APP_SRC_DIR}/calloverlaymodel.h ${APP_SRC_DIR}/calloverlaymodel.h
${APP_SRC_DIR}/spellcheckdictionarymanager.h
${APP_SRC_DIR}/filestosendlistmodel.h ${APP_SRC_DIR}/filestosendlistmodel.h
${APP_SRC_DIR}/wizardviewstepmodel.h ${APP_SRC_DIR}/wizardviewstepmodel.h
${APP_SRC_DIR}/avatarregistry.h ${APP_SRC_DIR}/avatarregistry.h
...@@ -433,7 +435,7 @@ set(COMMON_HEADERS ...@@ -433,7 +435,7 @@ set(COMMON_HEADERS
${APP_SRC_DIR}/currentcall.h ${APP_SRC_DIR}/currentcall.h
${APP_SRC_DIR}/messageparser.h ${APP_SRC_DIR}/messageparser.h
${APP_SRC_DIR}/htmlparser.h ${APP_SRC_DIR}/htmlparser.h
${APP_SRC_DIR}/imagedownloader.h ${APP_SRC_DIR}/filedownloader.h
${APP_SRC_DIR}/pluginversionmanager.h ${APP_SRC_DIR}/pluginversionmanager.h
${APP_SRC_DIR}/connectioninfolistmodel.h ${APP_SRC_DIR}/connectioninfolistmodel.h
${APP_SRC_DIR}/pttlistener.h ${APP_SRC_DIR}/pttlistener.h
...@@ -441,7 +443,7 @@ set(COMMON_HEADERS ...@@ -441,7 +443,7 @@ set(COMMON_HEADERS
${APP_SRC_DIR}/crashreporter.h ${APP_SRC_DIR}/crashreporter.h
${APP_SRC_DIR}/linkdevicemodel.h ${APP_SRC_DIR}/linkdevicemodel.h
${APP_SRC_DIR}/qrcodescannermodel.h ${APP_SRC_DIR}/qrcodescannermodel.h
) ${APP_SRC_DIR}/spellchecker.h)
# For libavutil/avframe. # For libavutil/avframe.
set(LIBJAMI_CONTRIB_DIR "${DAEMON_DIR}/contrib") set(LIBJAMI_CONTRIB_DIR "${DAEMON_DIR}/contrib")
...@@ -469,6 +471,48 @@ if(ENABLE_CRASHREPORTS) ...@@ -469,6 +471,48 @@ if(ENABLE_CRASHREPORTS)
endif() endif()
endif() endif()
find_package(PkgConfig REQUIRED)
# hunspell
pkg_check_modules(HUNSPELL hunspell)
if(MSVC)
elseif (NOT APPLE)
set(HUNSPELL_DICT_DIR "/usr/share/hunspell/")
else()
set(HUNSPELL_DICT_DIR "/Library/Spelling/")
endif()
if(HUNSPELL_FOUND)
message(STATUS "hunspell found")
include_directories(${HUNSPELL_INCLUDE_DIR})
find_path(HUNSPELL_INCLUDE_DIRS
NAMES hunspell.hxx
PATH_SUFFIXES hunspell
HINTS ${HUNSPELL_INCLUDE_DIRS}
)
find_library(HUNSPELL_LIBRARIES
NAMES ${HUNSPELL_LIBRARIES}
hunspell
hunspell-1.7
libhunspell
libhunspell-1.7
libhunspell-devel
libhunspell-dev
HINTS ${HUNSPELL_LIBRARY_DIRS}
)
else()
message(STATUS "hunspell not found - building hunspell")
set(HUNSPELL_DIR ${PROJECT_SOURCE_DIR}/3rdparty/hunspell)
# Build using the submodule and its CMakeLists.txt
add_subdirectory(${HUNSPELL_DIR} hunspell_build)
set(HUNSPELL_INCLUDE_DIR ${HUNSPELL_DIR}/src)
set(HUNSPELL_LIBRARIES hunspell::hunspell)
endif()
if(MSVC) if(MSVC)
set(WINDOWS_SYS_LIBS set(WINDOWS_SYS_LIBS
windowsapp.lib windowsapp.lib
...@@ -531,8 +575,6 @@ elseif (NOT APPLE) ...@@ -531,8 +575,6 @@ elseif (NOT APPLE)
${APP_SRC_DIR}/screencastportal.h) ${APP_SRC_DIR}/screencastportal.h)
list(APPEND QT_MODULES DBus) list(APPEND QT_MODULES DBus)
find_package(PkgConfig REQUIRED)
pkg_check_modules(GLIB REQUIRED glib-2.0) pkg_check_modules(GLIB REQUIRED glib-2.0)
if(GLIB_FOUND) if(GLIB_FOUND)
add_definitions(${GLIB_CFLAGS_OTHER}) add_definitions(${GLIB_CFLAGS_OTHER})
...@@ -615,6 +657,13 @@ else() # APPLE ...@@ -615,6 +657,13 @@ else() # APPLE
endif() endif()
endif() endif()
message(STATUS "Adding HUNSPELL_INCLUDE_DIR" ${HUNSPELL_INCLUDE_DIR})
list(APPEND CLIENT_INCLUDE_DIRS ${HUNSPELL_INCLUDE_DIR} ${CMAKE_BINARY_DIR}/include
${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/hunspell/src)
message(STATUS "Adding HUNSPELL_LIBRARIES" ${HUNSPELL_INCLUDE_DIR})
list(APPEND CLIENT_LIBS ${HUNSPELL_LIBRARIES})
# Qt find package # Qt find package
if(QT6_VER AND QT6_PATH) if(QT6_VER AND QT6_PATH)
message(STATUS "Using custom Qt version") message(STATUS "Using custom Qt version")
...@@ -703,7 +752,9 @@ qt_add_executable( ...@@ -703,7 +752,9 @@ qt_add_executable(
${QML_RESOURCES_QML} ${QML_RESOURCES_QML}
${SFPM_OBJECTS}) ${SFPM_OBJECTS})
# Make sure we can find the generated version file add_dependencies(${PROJECT_NAME} hunspell)
# Ensure the generated version file can be found.
add_dependencies(${PROJECT_NAME} generate_version_info) add_dependencies(${PROJECT_NAME} generate_version_info)
foreach(MODULE ${QT_MODULES}) foreach(MODULE ${QT_MODULES})
...@@ -797,6 +848,11 @@ elseif (NOT APPLE) ...@@ -797,6 +848,11 @@ elseif (NOT APPLE)
PRIVATE PRIVATE
JAMI_INSTALL_PREFIX="${JAMI_DATA_PREFIX}") JAMI_INSTALL_PREFIX="${JAMI_DATA_PREFIX}")
target_compile_definitions(
${PROJECT_NAME}
PRIVATE
HUNSPELL_INSTALL_DIR="${HUNSPELL_DICT_DIR}")
# Logos # Logos
install( install(
FILES resources/images/jami.svg FILES resources/images/jami.svg
......
...@@ -112,7 +112,7 @@ ZYPPER_CLIENT_DEPENDENCIES = [ ...@@ -112,7 +112,7 @@ ZYPPER_CLIENT_DEPENDENCIES = [
'qt6-svg-devel', 'qt6-multimedia-devel', 'qt6-multimedia-imports', 'qt6-svg-devel', 'qt6-multimedia-devel', 'qt6-multimedia-imports',
'qt6-declarative-devel', 'qt6-qmlcompiler-private-devel', 'qt6-declarative-devel', 'qt6-qmlcompiler-private-devel',
'qt6-quickcontrols2-devel', 'qt6-shadertools-devel', 'qt6-quickcontrols2-devel', 'qt6-shadertools-devel',
'qrencode-devel', 'NetworkManager-devel' 'qrencode-devel', 'NetworkManager-devel', 'hunspell-devel', 'libhunspell-devel'
] ]
ZYPPER_QT_WEBENGINE = [ ZYPPER_QT_WEBENGINE = [
...@@ -139,7 +139,7 @@ DNF_CLIENT_DEPENDENCIES = [ ...@@ -139,7 +139,7 @@ DNF_CLIENT_DEPENDENCIES = [
'libnotify-devel', 'libnotify-devel',
'qt6-qtbase-devel', 'qt6-qtbase-devel',
'qt6-qtsvg-devel', 'qt6-qtmultimedia-devel', 'qt6-qtdeclarative-devel', 'qt6-qtsvg-devel', 'qt6-qtmultimedia-devel', 'qt6-qtdeclarative-devel',
'qrencode-devel', 'NetworkManager-libnm-devel' 'qrencode-devel', 'NetworkManager-libnm-devel', 'hunspell-devel', 'libhunspell-devel'
] ]
DNF_QT_WEBENGINE = ['qt6-qtwebengine-devel'] DNF_QT_WEBENGINE = ['qt6-qtwebengine-devel']
...@@ -171,7 +171,7 @@ APT_CLIENT_DEPENDENCIES = [ ...@@ -171,7 +171,7 @@ APT_CLIENT_DEPENDENCIES = [
'qml6-module-qtquick-dialogs', 'qml6-module-qtquick-layouts', 'qml6-module-qtquick-dialogs', 'qml6-module-qtquick-layouts',
'qml6-module-qtquick-shapes', 'qml6-module-qtquick-window', 'qml6-module-qtquick-shapes', 'qml6-module-qtquick-window',
'qml6-module-qtquick-templates', 'qml6-module-qt-labs-platform', 'qml6-module-qtquick-templates', 'qml6-module-qt-labs-platform',
'libqrencode-dev', 'libnm-dev' 'libqrencode-dev', 'libnm-dev', 'hunspell', 'libhunspell-dev'
] ]
APT_QT_WEBENGINE = [ APT_QT_WEBENGINE = [
...@@ -194,7 +194,7 @@ PACMAN_CLIENT_DEPENDENCIES = [ ...@@ -194,7 +194,7 @@ PACMAN_CLIENT_DEPENDENCIES = [
'qt6-declarative', 'qt6-5compat', 'qt6-multimedia', 'qt6-declarative', 'qt6-5compat', 'qt6-multimedia',
'qt6-networkauth', 'qt6-shadertools', 'qt6-networkauth', 'qt6-shadertools',
'qt6-svg', 'qt6-tools', 'qt6-svg', 'qt6-tools',
'qrencode', 'libnm' 'qrencode', 'libnm', 'hunspell'
] ]
PACMAN_QT_WEBENGINE = ['qt6-webengine'] PACMAN_QT_WEBENGINE = ['qt6-webengine']
......
...@@ -63,6 +63,8 @@ extern const QString defaultDownloadPath; ...@@ -63,6 +63,8 @@ extern const QString defaultDownloadPath;
X(WindowState, QWindow::AutomaticVisibility) \ X(WindowState, QWindow::AutomaticVisibility) \
X(EnableExperimentalSwarm, false) \ X(EnableExperimentalSwarm, false) \
X(LANG, "SYSTEM") \ X(LANG, "SYSTEM") \
X(SpellLang, "None") \
X(EnableSpellCheck, true) \
X(PluginStoreEndpoint, "https://plugins.jami.net") \ X(PluginStoreEndpoint, "https://plugins.jami.net") \
X(PositionShareDuration, 15) \ X(PositionShareDuration, 15) \
X(PositionShareLimit, true) \ X(PositionShareLimit, true) \
......
...@@ -15,8 +15,11 @@ ...@@ -15,8 +15,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import QtQuick import QtQuick
import net.jami.Adapters 1.1
import net.jami.Constants 1.1 import net.jami.Constants 1.1
import "contextmenu" import "contextmenu"
import "../mainview"
import "../mainview/components"
ContextMenuAutoLoader { ContextMenuAutoLoader {
id: root id: root
...@@ -27,8 +30,16 @@ ContextMenuAutoLoader { ...@@ -27,8 +30,16 @@ ContextMenuAutoLoader {
property var selectionEnd property var selectionEnd
property bool customizePaste: false property bool customizePaste: false
property bool selectOnly: false property bool selectOnly: false
property bool checkSpell: false
property var suggestionList
property var menuItemsLength
property var language
signal contextMenuRequirePaste signal contextMenuRequirePaste
SpellLanguageContextMenu {
id: spellLanguageContextMenu
active: checkSpell
}
property list<GeneralMenuItem> menuItems: [ property list<GeneralMenuItem> menuItems: [
GeneralMenuItem { GeneralMenuItem {
...@@ -38,9 +49,8 @@ ContextMenuAutoLoader { ...@@ -38,9 +49,8 @@ ContextMenuAutoLoader {
isActif: lineEditObj.selectedText.length isActif: lineEditObj.selectedText.length
itemName: JamiStrings.copy itemName: JamiStrings.copy
hasIcon: false hasIcon: false
onClicked: { onClicked:
lineEditObj.copy(); lineEditObj.copy();
}
}, },
GeneralMenuItem { GeneralMenuItem {
id: cut id: cut
...@@ -49,9 +59,8 @@ ContextMenuAutoLoader { ...@@ -49,9 +59,8 @@ ContextMenuAutoLoader {
isActif: lineEditObj.selectedText.length && !selectOnly isActif: lineEditObj.selectedText.length && !selectOnly
itemName: JamiStrings.cut itemName: JamiStrings.cut
hasIcon: false hasIcon: false
onClicked: { onClicked:
lineEditObj.cut(); lineEditObj.cut();
}
}, },
GeneralMenuItem { GeneralMenuItem {
id: paste id: paste
...@@ -65,9 +74,68 @@ ContextMenuAutoLoader { ...@@ -65,9 +74,68 @@ ContextMenuAutoLoader {
else else
lineEditObj.paste(); lineEditObj.paste();
} }
},
GeneralMenuItem {
id: language
visible: checkSpell
canTrigger: checkSpell
itemName: JamiStrings.language
hasIcon: false
onClicked: {
spellLanguageContextMenu.openMenu();
}
} }
] ]
ListView {
model: ListModel {
id: dynamicModel
}
Instantiator {
model: dynamicModel
delegate: GeneralMenuItem {
id: suggestion
canTrigger: true
isActif: true
itemName: model.name
hasIcon: false
onClicked: {
replaceWord(model.name);
}
}
onObjectAdded: {
menuItems.push(object);
}
onObjectRemoved: {
menuItems.splice(menuItemsLength, suggestionList.length);
}
}
}
function removeItems() {
dynamicModel.remove(0, suggestionList.length);
suggestionList.length = 0;
}
function addMenuItem(wordList) {
menuItemsLength = menuItems.length; // Keep initial number of items for easier removal
suggestionList = wordList;
for (var i = 0; i < suggestionList.length; ++i) {
dynamicModel.append({
"name": suggestionList[i]
});
}
}
function replaceWord(word) {
lineEditObj.remove(selectionStart, selectionEnd);
lineEditObj.insert(lineEditObj.cursorPosition, word);
}
function openMenuAt(mouseEvent) { function openMenuAt(mouseEvent) {
if (lineEditObj.selectedText.length === 0 && selectOnly) if (lineEditObj.selectedText.length === 0 && selectOnly)
return; return;
...@@ -85,6 +153,12 @@ ContextMenuAutoLoader { ...@@ -85,6 +153,12 @@ ContextMenuAutoLoader {
function onOpened() { function onOpened() {
lineEditObj.select(selectionStart, selectionEnd); lineEditObj.select(selectionStart, selectionEnd);
} }
function onClosed() {
if (!suggestionList || suggestionList.length == 0) {
return;
}
removeItems();
}
} }
Component.onCompleted: menuItemsToLoad = menuItems Component.onCompleted: menuItemsToLoad = menuItems
......
/*
* Copyright (C) 2020-2025 Savoir-faire Linux Inc.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick
import net.jami.Adapters 1.1
import net.jami.Constants 1.1
import net.jami.Models 1.1
import net.jami.Enums 1.1
import "contextmenu"
import "../mainview"
import "../mainview/components"
ContextMenuAutoLoader {
id: root
signal languageChanged()
CachedFile {
id: cachedFile
}
function openMenuAt(mouseEvent) {
x = mouseEvent.x;
y = mouseEvent.y;
root.openMenu();
}
onOpenRequested: {
// Create the menu items from the installed dictionaries
menuItemsToLoad = generateMenuItems();
}
function generateMenuItems() {
var menuItems = [];
// Create new menu items
var dictionaries = SpellCheckDictionaryManager.installedDictionaries();
var keys = Object.keys(dictionaries);
for (var i = 0; i < keys.length; ++i) {
var menuItem = Qt.createComponent("qrc:/commoncomponents/contextmenu/GeneralMenuItem.qml", Component.PreferSynchronous);
if (menuItem.status !== Component.Ready) {
console.error("Error loading component:", menuItem.errorString());
continue;
}
let menuItemObject = menuItem.createObject(root, {
"parent": root,
"canTrigger": true,
"isActif": true,
"itemName": dictionaries[keys[i]],
"hasIcon": false,
"content": keys[i],
});
if (menuItemObject === null) {
console.error("Error creating menu item:", menuItem.errorString());
continue;
}
menuItemObject.clicked.connect(function () {
UtilsAdapter.setAppValue(Settings.Key.SpellLang, menuItemObject.content);
});
// Log the object pointer
menuItems.push(menuItemObject);
}
return menuItems;
}
}
...@@ -44,11 +44,15 @@ Menu { ...@@ -44,11 +44,15 @@ Menu {
function loadMenuItems(menuItems) { function loadMenuItems(menuItems) {
root.addItem(menuTopBorder); root.addItem(menuTopBorder);
// Establish the preferred width of the menu by taking the maximum width of the items
for (var j = 0; j < menuItems.length; ++j) { for (var j = 0; j < menuItems.length; ++j) {
var currentItemWidth = menuItems[j].itemPreferredWidth; var currentItemWidth = menuItems[j].itemPreferredWidth;
if (currentItemWidth !== JamiTheme.menuItemsPreferredWidth && currentItemWidth > menuPreferredWidth && menuItems[j].canTrigger) if (currentItemWidth !== JamiTheme.menuItemsPreferredWidth && currentItemWidth > menuPreferredWidth && menuItems[j].canTrigger)
menuPreferredWidth = currentItemWidth; menuPreferredWidth = currentItemWidth;
} }
// Add the items to the menu
for (var i = 0; i < menuItems.length; ++i) { for (var i = 0; i < menuItems.length; ++i) {
if (menuItems[i].canTrigger) { if (menuItems[i].canTrigger) {
menuItems[i].parentMenu = root; menuItems[i].parentMenu = root;
......
...@@ -27,11 +27,14 @@ Loader { ...@@ -27,11 +27,14 @@ Loader {
property int contextMenuItemPreferredHeight: 0 property int contextMenuItemPreferredHeight: 0
property int contextMenuSeparatorPreferredHeight: 0 property int contextMenuSeparatorPreferredHeight: 0
signal openRequested
active: false active: false
visible: false visible: false
function openMenu() { function openMenu() {
openRequested();
root.active = true; root.active = true;
root.sourceComponent = menuComponent; root.sourceComponent = menuComponent;
} }
......
...@@ -28,6 +28,7 @@ MenuItem { ...@@ -28,6 +28,7 @@ MenuItem {
id: menuItem id: menuItem
property string itemName: "" property string itemName: ""
property string content: ""
property alias iconSource: contextMenuItemImage.source property alias iconSource: contextMenuItemImage.source
property string iconColor: "" property string iconColor: ""
property bool canTrigger: true property bool canTrigger: true
......
...@@ -15,32 +15,32 @@ ...@@ -15,32 +15,32 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
#include "imagedownloader.h" #include "filedownloader.h"
#include <QDir> #include <QDir>
#include <QLockFile> #include <QLockFile>
ImageDownloader::ImageDownloader(ConnectivityMonitor* cm, QObject* parent) FileDownloader::FileDownloader(ConnectivityMonitor* cm, QObject* parent)
: NetworkManager(cm, parent) : NetworkManager(cm, parent)
{} {}
void void
ImageDownloader::downloadImage(const QUrl& url, const QString& localPath) FileDownloader::downloadFile(const QUrl& url, const QString& localPath)
{ {
Utils::oneShotConnect(this, &NetworkManager::errorOccurred, this, [this, localPath]() { Utils::oneShotConnect(this, &NetworkManager::errorOccurred, this, [this, localPath]() {
onDownloadImageFinished({}, localPath); onDownloadFileFinished({}, localPath);
}); });
sendGetRequest(url, [this, localPath](const QByteArray& imageData) { sendGetRequest(url, [this, localPath](const QByteArray& fileData) {
onDownloadImageFinished(imageData, localPath); onDownloadFileFinished(fileData, localPath);
}); });
} }
void void
ImageDownloader::onDownloadImageFinished(const QByteArray& data, const QString& localPath) FileDownloader::onDownloadFileFinished(const QByteArray& data, const QString& localPath)
{ {
if (data.isEmpty()) { if (data.isEmpty()) {
Q_EMIT downloadImageFailed(localPath); Q_EMIT downloadFileFailed(localPath);
return; return;
} }
...@@ -49,7 +49,7 @@ ImageDownloader::onDownloadImageFinished(const QByteArray& data, const QString& ...@@ -49,7 +49,7 @@ ImageDownloader::onDownloadImageFinished(const QByteArray& data, const QString&
const QDir dir; const QDir dir;
if (!dir.mkpath(dirPath)) { if (!dir.mkpath(dirPath)) {
qWarning() << Q_FUNC_INFO << "Failed to create directory" << dirPath; qWarning() << Q_FUNC_INFO << "Failed to create directory" << dirPath;
Q_EMIT downloadImageFailed(localPath); Q_EMIT downloadFileFailed(localPath);
return; return;
} }
...@@ -58,10 +58,10 @@ ImageDownloader::onDownloadImageFinished(const QByteArray& data, const QString& ...@@ -58,10 +58,10 @@ ImageDownloader::onDownloadImageFinished(const QByteArray& data, const QString&
if (lf.lock() && file.open(QIODevice::WriteOnly)) { if (lf.lock() && file.open(QIODevice::WriteOnly)) {
file.write(data); file.write(data);
file.close(); file.close();
Q_EMIT downloadImageSuccessful(localPath); Q_EMIT downloadFileSuccessful(localPath);
return; return;
} }
qWarning() << Q_FUNC_INFO << "Failed to write image to" << localPath; qWarning() << Q_FUNC_INFO << "Failed to write file to" << localPath;
Q_EMIT downloadImageFailed(localPath); Q_EMIT downloadFileFailed(localPath);
} }
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
#include <QQmlEngine> // QML registration #include <QQmlEngine> // QML registration
#include <QApplication> // QML registration #include <QApplication> // QML registration
class ImageDownloader : public NetworkManager class FileDownloader : public NetworkManager
{ {
Q_OBJECT Q_OBJECT
QML_SINGLETON QML_SINGLETON
...@@ -32,23 +32,23 @@ class ImageDownloader : public NetworkManager ...@@ -32,23 +32,23 @@ class ImageDownloader : public NetworkManager
QML_PROPERTY(QString, cachePath) QML_PROPERTY(QString, cachePath)
public: public:
static ImageDownloader* create(QQmlEngine*, QJSEngine*) static FileDownloader* create(QQmlEngine*, QJSEngine*)
{ {
return new ImageDownloader( return new FileDownloader(
qApp->property("ConnectivityMonitor").value<ConnectivityMonitor*>()); qApp->property("ConnectivityMonitor").value<ConnectivityMonitor*>());
} }
explicit ImageDownloader(ConnectivityMonitor* cm, QObject* parent = nullptr); explicit FileDownloader(ConnectivityMonitor* cm, QObject* parent = nullptr);
~ImageDownloader() = default; ~FileDownloader() = default;
// Download an image and call onDownloadImageFinished when done // Download an image and call onDownloadFileFinished when done
Q_INVOKABLE void downloadImage(const QUrl& url, const QString& localPath); Q_INVOKABLE void downloadFile(const QUrl& url, const QString& localPath);
Q_SIGNALS: Q_SIGNALS:
void downloadImageSuccessful(const QString& localPath); void downloadFileSuccessful(const QString& localPath);
void downloadImageFailed(const QString& localPath); void downloadFileFailed(const QString& localPath);
private Q_SLOTS: private Q_SLOTS:
// Saves the image to the localPath and emits the appropriate signal // Saves the image to the localPath and emits the appropriate signal
void onDownloadImageFinished(const QByteArray& reply, const QString& localPath); void onDownloadFileFinished(const QByteArray& reply, const QString& localPath);
}; };
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
#include "global.h" #include "global.h"
#include "qmlregister.h" #include "qmlregister.h"
#include "appsettingsmanager.h" #include "appsettingsmanager.h"
#include "spellcheckdictionarymanager.h"
#include "connectivitymonitor.h" #include "connectivitymonitor.h"
#include "systemtray.h" #include "systemtray.h"
#include "previewengine.h" #include "previewengine.h"
...@@ -190,6 +191,7 @@ MainApplication::init() ...@@ -190,6 +191,7 @@ MainApplication::init()
// to any other initialization. This won't do anything if crashpad isn't // to any other initialization. This won't do anything if crashpad isn't
// enabled. // enabled.
settingsManager_ = new AppSettingsManager(this); settingsManager_ = new AppSettingsManager(this);
spellCheckDictionaryManager_ = new SpellCheckDictionaryManager(settingsManager_, this);
crashReporter_ = new CrashReporter(settingsManager_, this); crashReporter_ = new CrashReporter(settingsManager_, this);
// This 2-phase initialisation prevents ephemeral instances from // This 2-phase initialisation prevents ephemeral instances from
...@@ -423,6 +425,7 @@ MainApplication::initQmlLayer() ...@@ -423,6 +425,7 @@ MainApplication::initQmlLayer()
lrcInstance_.get(), lrcInstance_.get(),
systemTray_, systemTray_,
settingsManager_, settingsManager_,
spellCheckDictionaryManager_,
connectivityMonitor_, connectivityMonitor_,
previewEngine_, previewEngine_,
&screenInfo_, &screenInfo_,
......
...@@ -31,6 +31,7 @@ ...@@ -31,6 +31,7 @@
class ConnectivityMonitor; class ConnectivityMonitor;
class SystemTray; class SystemTray;
class AppSettingsManager; class AppSettingsManager;
class SpellCheckDictionaryManager;
class CrashReporter; class CrashReporter;
class PreviewEngine; class PreviewEngine;
...@@ -118,6 +119,7 @@ private: ...@@ -118,6 +119,7 @@ private:
ConnectivityMonitor* connectivityMonitor_; ConnectivityMonitor* connectivityMonitor_;
SystemTray* systemTray_; SystemTray* systemTray_;
AppSettingsManager* settingsManager_; AppSettingsManager* settingsManager_;
SpellCheckDictionaryManager* spellCheckDictionaryManager_;
PreviewEngine* previewEngine_; PreviewEngine* previewEngine_;
CrashReporter* crashReporter_; CrashReporter* crashReporter_;
......
/*
* Copyright (C) 2025 Savoir-faire Linux Inc.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import net.jami.Adapters 1.1
import net.jami.Constants 1.1
import net.jami.Helpers 1.1
import net.jami.Models 1.1
import "../../commoncomponents"
Item {
id: cachedFile
property string dictionaryPath: SpellCheckDictionaryManager.getDictionariesPath()
function updateDictionnary(languagePath) {
var file = dictionaryPath + languagePath;
MessagesAdapter.updateDictionnary(file);
}
}
...@@ -64,8 +64,8 @@ Item { ...@@ -64,8 +64,8 @@ Item {
} }
Connections { Connections {
target: ImageDownloader target: FileDownloader
function onDownloadImageSuccessful(localPath) { function onDownloadFileSuccessful(localPath) {
if (localPath === cachedImage.localPath) { if (localPath === cachedImage.localPath) {
image.source = UtilsAdapter.urlFromLocalPath(localPath); image.source = UtilsAdapter.urlFromLocalPath(localPath);
} }
...@@ -90,7 +90,7 @@ Item { ...@@ -90,7 +90,7 @@ Item {
} }
if (downloadUrl && downloadUrl !== "" && localPath !== "") { if (downloadUrl && downloadUrl !== "" && localPath !== "") {
if (!UtilsAdapter.fileExists(localPath)) { if (!UtilsAdapter.fileExists(localPath)) {
ImageDownloader.downloadImage(downloadUrl, localPath); FileDownloader.downloadFile(downloadUrl, localPath);
} else { } else {
image.source = UtilsAdapter.urlFromLocalPath(localPath); image.source = UtilsAdapter.urlFromLocalPath(localPath);
if (image.isGif) { if (image.isGif) {
......
...@@ -116,6 +116,9 @@ Rectangle { ...@@ -116,6 +116,9 @@ Rectangle {
spacing: 0 spacing: 0
ElidedTextLabel {
id: title
LineEditContextMenu { LineEditContextMenu {
id: displayNameContextMenu id: displayNameContextMenu
lineEditObj: title lineEditObj: title
...@@ -130,9 +133,6 @@ Rectangle { ...@@ -130,9 +133,6 @@ Rectangle {
} }
} }
ElidedTextLabel {
id: title
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
font.pointSize: JamiTheme.textFontSize + 2 font.pointSize: JamiTheme.textFontSize + 2
......
...@@ -17,19 +17,17 @@ ...@@ -17,19 +17,17 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import net.jami.Adapters 1.1 import net.jami.Adapters 1.1
import net.jami.Constants 1.1 import net.jami.Constants 1.1
import net.jami.Enums 1.1 import net.jami.Enums 1.1
import net.jami.Models 1.1 import net.jami.Models 1.1
import SortFilterProxyModel 0.2 import SortFilterProxyModel 0.2
import "../../commoncomponents" import "../../commoncomponents"
JamiFlickable { JamiFlickable {
id: root id: root
property int underlineHeight: JamiTheme.messageUnderlineHeight
property alias text: textArea.text property alias text: textArea.text
property var textAreaObj: textArea property var textAreaObj: textArea
property alias placeholderText: textArea.placeholderText property alias placeholderText: textArea.placeholderText
...@@ -39,9 +37,12 @@ JamiFlickable { ...@@ -39,9 +37,12 @@ JamiFlickable {
property bool showPreview: false property bool showPreview: false
property bool isShowTypo: UtilsAdapter.getAppValue(Settings.Key.ShowMardownOption) property bool isShowTypo: UtilsAdapter.getAppValue(Settings.Key.ShowMardownOption)
property int textWidth: textArea.contentWidth property int textWidth: textArea.contentWidth
property var spellCheckActive: AppSettingsManager.getValue(Settings.EnableSpellCheck)
property var language: AppSettingsManager.getValue(Settings.SpellLang)
// Used to cache the editable text when showing the preview message // Used to cache the editable text when showing the preview message
// and also to debounce the textChanged signal's effect on the composing status. // and also to debounce the textChanged signal's effect on the composing status.
property var underlineList: []
property string cachedText property string cachedText
property string debounceText property string debounceText
...@@ -72,6 +73,7 @@ JamiFlickable { ...@@ -72,6 +73,7 @@ JamiFlickable {
lineEditObj: textArea lineEditObj: textArea
customizePaste: true customizePaste: true
checkSpell: (Qt.platform.os.toString() === "linux") ? true : false
onContextMenuRequirePaste: { onContextMenuRequirePaste: {
// Intercept paste event to use C++ QMimeData // Intercept paste event to use C++ QMimeData
...@@ -115,9 +117,79 @@ JamiFlickable { ...@@ -115,9 +117,79 @@ JamiFlickable {
TextArea.flickable: TextArea { TextArea.flickable: TextArea {
id: textArea id: textArea
CachedFile {
id: cachedFile
}
function updateCorrection(language) {
cachedFile.updateDictionnary(language);
textArea.updateUnderlineText();
}
// Listen to settings changes and apply it to this widget
Connections {
target: UtilsAdapter
function onChangeLanguage() {
textArea.updateUnderlineText();
}
function onChangeFontSize() {
textArea.updateUnderlineText();
}
function onSpellLanguageChanged() {
root.language = SpellCheckDictionaryManager.getSpellLanguage();
if ((Qt.platform.os.toString() !== "linux") || (AppSettingsManager.getValue(Settings.SpellLang) === "NONE")) {
spellCheckActive = false;
} else {
spellCheckActive = AppSettingsManager.getValue(Settings.EnableSpellCheck);
}
if (spellCheckActive === true) {
root.language = SpellCheckDictionaryManager.getSpellLanguage();
textArea.updateCorrection(root.language);
} else {
textArea.clearUnderlines();
}
}
function onEnableSpellCheckChanged() {
// Disable spell check on non-linux platforms yet
if ((Qt.platform.os.toString() !== "linux") || (AppSettingsManager.getValue(Settings.SpellLang) === "NONE")) {
spellCheckActive = false;
} else {
spellCheckActive = AppSettingsManager.getValue(Settings.EnableSpellCheck);
}
if (spellCheckActive === true) {
root.language = SpellCheckDictionaryManager.getSpellLanguage();
textArea.updateCorrection(root.language);
} else {
textArea.clearUnderlines();
}
}
}
// Initialize the settings if the component wasn't loaded when changing settings
Component.onCompleted: {
if ((Qt.platform.os.toString() !== "linux") || (AppSettingsManager.getValue(Settings.SpellLang) === "NONE")) {
spellCheckActive = false;
} else {
spellCheckActive = AppSettingsManager.getValue(Settings.EnableSpellCheck);
}
if (spellCheckActive === true) {
root.language = SpellCheckDictionaryManager.getSpellLanguage();
textArea.updateCorrection(root.language);
} else {
textArea.clearUnderlines();
}
}
readOnly: showPreview readOnly: showPreview
leftPadding: JamiTheme.scrollBarHandleSize leftPadding: JamiTheme.scrollBarHandleSize
rightPadding: JamiTheme.scrollBarHandleSize rightPadding: JamiTheme.scrollBarHandleSize
topPadding: 0
bottomPadding: underlineHeight
persistentSelection: true persistentSelection: true
verticalAlignment: TextEdit.AlignVCenter verticalAlignment: TextEdit.AlignVCenter
font.pointSize: JamiTheme.textFontSize + 2 font.pointSize: JamiTheme.textFontSize + 2
...@@ -135,12 +207,37 @@ JamiFlickable { ...@@ -135,12 +207,37 @@ JamiFlickable {
color: "transparent" color: "transparent"
} }
TextMetrics {
id: textMetrics
elide: Text.ElideMiddle
font.family: textArea.font.family
font.pointSize: JamiTheme.textFontSize + 2
}
Text {
id: highlight
color: "black"
font.bold: true
visible: false
}
onReleased: function (event) { onReleased: function (event) {
if (event.button === Qt.RightButton) if (event.button === Qt.RightButton) {
var position = textArea.positionAt(event.x, event.y);
textArea.moveCursorSelection(position, TextInput.SelectWords);
textArea.selectWord();
if (!MessagesAdapter.spell(textArea.selectedText)) {
var wordList = MessagesAdapter.spellSuggestionsRequest(textArea.selectedText);
if (wordList.length !== 0) {
textAreaContextMenu.addMenuItem(wordList);
}
}
textAreaContextMenu.openMenuAt(event); textAreaContextMenu.openMenuAt(event);
} }
}
onTextChanged: { onTextChanged: {
updateUnderlineText();
if (text !== debounceText && !showPreview) { if (text !== debounceText && !showPreview) {
debounceText = text; debounceText = text;
MessagesAdapter.userIsComposing(text ? true : false); MessagesAdapter.userIsComposing(text ? true : false);
...@@ -152,6 +249,8 @@ JamiFlickable { ...@@ -152,6 +249,8 @@ JamiFlickable {
// eg. Enter -> Send messages // eg. Enter -> Send messages
// Shift + Enter -> Next Line // Shift + Enter -> Next Line
Keys.onPressed: function (keyEvent) { Keys.onPressed: function (keyEvent) {
// Update underline on each input to take into account deleted text and sent ones
updateUnderlineText();
if (keyEvent.matches(StandardKey.Paste)) { if (keyEvent.matches(StandardKey.Paste)) {
MessagesAdapter.onPaste(); MessagesAdapter.onPaste();
keyEvent.accepted = true; keyEvent.accepted = true;
...@@ -180,5 +279,41 @@ JamiFlickable { ...@@ -180,5 +279,41 @@ JamiFlickable {
keyEvent.accepted = true; keyEvent.accepted = true;
} }
} }
function updateUnderlineText() {
clearUnderlines();
// We iterate over the whole text to find words to check and underline them if needed
if (spellCheckActive) {
var text = textArea.text;
var words = MessagesAdapter.findWords(text);
if (!words)
return;
for (var i = 0; i < words.length; i++) {
var wordInfo = words[i];
if (wordInfo && wordInfo.word && !MessagesAdapter.spell(wordInfo.word)) {
textMetrics.text = wordInfo.word;
var xPos = textArea.positionToRectangle(wordInfo.position).x;
var yPos = textArea.positionToRectangle(wordInfo.position).y + textArea.positionToRectangle(wordInfo.position).height;
var underlineObject = Qt.createQmlObject('import QtQuick; Rectangle {height: 2; color: "red";}', textArea);
underlineObject.x = xPos;
underlineObject.y = yPos;
underlineObject.width = textMetrics.width;
underlineList.push(underlineObject);
}
}
}
}
function clearUnderlines() {
// Destroy all of the underline boxes
while (underlineList.length > 0) {
// Get the previous item
var underlineObject = underlineList[underlineList.length - 1];
// Remove the last item
underlineList.pop();
// Destroy the removed item
underlineObject.destroy();
}
}
} }
} }
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
#include "qtutils.h" #include "qtutils.h"
#include "messageparser.h" #include "messageparser.h"
#include "previewengine.h" #include "previewengine.h"
#include "spellchecker.h"
#include <api/datatransfermodel.h> #include <api/datatransfermodel.h>
#include <api/contact.h> #include <api/contact.h>
...@@ -39,17 +40,25 @@ ...@@ -39,17 +40,25 @@
#include <QtMath> #include <QtMath>
#include <QRegExp> #include <QRegExp>
#define SUGGESTIONS_MAX_SIZE 3 // limit the number of spelling suggestions
MessagesAdapter::MessagesAdapter(AppSettingsManager* settingsManager, MessagesAdapter::MessagesAdapter(AppSettingsManager* settingsManager,
PreviewEngine* previewEngine, PreviewEngine* previewEngine,
SpellCheckDictionaryManager* spellCheckDictionaryManager,
LRCInstance* instance, LRCInstance* instance,
QObject* parent) QObject* parent)
: QmlAdapterBase(instance, parent) : QmlAdapterBase(instance, parent)
, settingsManager_(settingsManager) , settingsManager_(settingsManager)
, spellCheckDictionaryManager_(spellCheckDictionaryManager)
, messageParser_(new MessageParser(previewEngine, this)) , messageParser_(new MessageParser(previewEngine, this))
, filteredMsgListModel_(new FilteredMsgListModel(this)) , filteredMsgListModel_(new FilteredMsgListModel(this))
, mediaInteractions_(std::make_unique<MessageListModel>(nullptr)) , mediaInteractions_(std::make_unique<MessageListModel>(nullptr))
, timestampTimer_(new QTimer(this)) , timestampTimer_(new QTimer(this))
{ {
#if defined(Q_OS_LINUX)
// Initialize with make_shared
spellChecker_ = std::make_shared<SpellChecker>(spellCheckDictionaryManager_->getDictionaryPath());
#endif
setObjectName(typeid(*this).name()); setObjectName(typeid(*this).name());
set_messageListModel(QVariant::fromValue(filteredMsgListModel_)); set_messageListModel(QVariant::fromValue(filteredMsgListModel_));
...@@ -727,3 +736,53 @@ MessagesAdapter::getMsgListSourceModel() const ...@@ -727,3 +736,53 @@ MessagesAdapter::getMsgListSourceModel() const
// However it may be a nullptr if not yet set. // However it may be a nullptr if not yet set.
return static_cast<MessageListModel*>(filteredMsgListModel_->sourceModel()); return static_cast<MessageListModel*>(filteredMsgListModel_->sourceModel());
} }
bool
MessagesAdapter::spell(const QString& word)
{
return spellChecker_->spell(word);
}
QVariantList
MessagesAdapter::spellSuggestionsRequest(const QString& word)
{
QStringList suggestionsList;
QVariantList variantList;
if (spellChecker_ == nullptr || spellChecker_->spell(word)) {
return variantList;
}
suggestionsList = spellChecker_->suggest(word);
for (const auto& suggestion : suggestionsList) {
if (variantList.size() >= SUGGESTIONS_MAX_SIZE) {
break;
}
variantList.append(QVariant(suggestion));
}
return variantList;
}
QVariantList
MessagesAdapter::findWords(const QString& text)
{
QVariantList result;
if (!spellChecker_)
return result;
auto words = spellChecker_->findWords(text);
for (const auto& word : words) {
QVariantMap wordInfo;
wordInfo["word"] = word.word;
wordInfo["position"] = word.position;
wordInfo["length"] = word.length;
result.append(wordInfo);
}
return result;
}
void
MessagesAdapter::updateDictionnary(const QString& path)
{
return spellChecker_->replaceDictionary(path);
}
...@@ -23,6 +23,8 @@ ...@@ -23,6 +23,8 @@
#include "previewengine.h" #include "previewengine.h"
#include "messageparser.h" #include "messageparser.h"
#include "appsettingsmanager.h" #include "appsettingsmanager.h"
#include "spellchecker.h"
#include "spellcheckdictionarymanager.h"
#include <QObject> #include <QObject>
#include <QString> #include <QString>
...@@ -46,7 +48,6 @@ public: ...@@ -46,7 +48,6 @@ public:
connect(this, &QAbstractItemModel::rowsRemoved, this, &FilteredMsgListModel::countChanged); connect(this, &QAbstractItemModel::rowsRemoved, this, &FilteredMsgListModel::countChanged);
connect(this, &QAbstractItemModel::modelReset, this, &FilteredMsgListModel::countChanged); connect(this, &QAbstractItemModel::modelReset, this, &FilteredMsgListModel::countChanged);
connect(this, &QAbstractItemModel::layoutChanged, this, &FilteredMsgListModel::countChanged); connect(this, &QAbstractItemModel::layoutChanged, this, &FilteredMsgListModel::countChanged);
} }
bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override
{ {
...@@ -101,11 +102,14 @@ public: ...@@ -101,11 +102,14 @@ public:
{ {
return new MessagesAdapter(qApp->property("AppSettingsManager").value<AppSettingsManager*>(), return new MessagesAdapter(qApp->property("AppSettingsManager").value<AppSettingsManager*>(),
qApp->property("PreviewEngine").value<PreviewEngine*>(), qApp->property("PreviewEngine").value<PreviewEngine*>(),
qApp->property("SpellCheckDictionaryManager")
.value<SpellCheckDictionaryManager*>(),
qApp->property("LRCInstance").value<LRCInstance*>()); qApp->property("LRCInstance").value<LRCInstance*>());
} }
explicit MessagesAdapter(AppSettingsManager* settingsManager, explicit MessagesAdapter(AppSettingsManager* settingsManager,
PreviewEngine* previewEngine, PreviewEngine* previewEngine,
SpellCheckDictionaryManager* spellCheckDictionaryManager,
LRCInstance* instance, LRCInstance* instance,
QObject* parent = nullptr); QObject* parent = nullptr);
~MessagesAdapter() = default; ~MessagesAdapter() = default;
...@@ -164,6 +168,10 @@ public: ...@@ -164,6 +168,10 @@ public:
Q_INVOKABLE QVariant dataForInteraction(const QString& interactionId, Q_INVOKABLE QVariant dataForInteraction(const QString& interactionId,
int role = Qt::DisplayRole) const; int role = Qt::DisplayRole) const;
Q_INVOKABLE void startSearch(const QString& text, bool isMedia); Q_INVOKABLE void startSearch(const QString& text, bool isMedia);
Q_INVOKABLE QVariantList spellSuggestionsRequest(const QString& word);
Q_INVOKABLE bool spell(const QString& word);
Q_INVOKABLE void updateDictionnary(const QString& path);
Q_INVOKABLE QVariantList findWords(const QString& text);
// Run corrsponding js functions, c++ to qml. // Run corrsponding js functions, c++ to qml.
void setMessagesImageContent(const QString& path, bool isBased64 = false); void setMessagesImageContent(const QString& path, bool isBased64 = false);
...@@ -198,14 +206,12 @@ private: ...@@ -198,14 +206,12 @@ private:
QList<QString> conversationTypersUrlToName(const QSet<QString>& typersSet); QList<QString> conversationTypersUrlToName(const QSet<QString>& typersSet);
AppSettingsManager* settingsManager_; AppSettingsManager* settingsManager_;
SpellCheckDictionaryManager* spellCheckDictionaryManager_;
MessageParser* messageParser_; MessageParser* messageParser_;
FilteredMsgListModel* filteredMsgListModel_; FilteredMsgListModel* filteredMsgListModel_;
static constexpr const int loadChunkSize_ {20};
std::unique_ptr<MessageListModel> mediaInteractions_; std::unique_ptr<MessageListModel> mediaInteractions_;
QTimer* timestampTimer_;
QTimer* timestampTimer_ {nullptr}; std::shared_ptr<SpellChecker> spellChecker_;
static constexpr const int loadChunkSize_ {20};
static constexpr const int timestampUpdateIntervalMs_ {1000}; static constexpr const int timestampUpdateIntervalMs_ {1000};
}; };
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment