mirror of
https://git.jami.net/savoirfairelinux/jami-client-qt.git
synced 2025-07-01 06:05:25 +02:00
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
This commit is contained in:
parent
88d0539085
commit
2a72da564e
34 changed files with 1076 additions and 77 deletions
4
.gitmodules
vendored
4
.gitmodules
vendored
|
@ -31,3 +31,7 @@
|
|||
path = 3rdparty/zxing-cpp
|
||||
url = https://github.com/nu-book/zxing-cpp.git
|
||||
ignore = dirty
|
||||
[submodule "3rdparty/hunspell"]
|
||||
path = 3rdparty/hunspell
|
||||
url = https://gitlab.savoirfairelinux.com/jami/hunspell.git
|
||||
ignore = dirty
|
||||
|
|
1
3rdparty/hunspell
vendored
Submodule
1
3rdparty/hunspell
vendored
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 525f9f22766a28e0f81c435217fcf4528e01c013
|
|
@ -255,7 +255,7 @@ set(PYTHON_EXEC ${Python3_EXECUTABLE})
|
|||
|
||||
# Versioning and build ID generation
|
||||
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.
|
||||
file(TOUCH ${VERSION_FILE})
|
||||
add_custom_target(
|
||||
|
@ -347,6 +347,7 @@ set(COMMON_SOURCES
|
|||
${APP_SRC_DIR}/conversationlistmodel.cpp
|
||||
${APP_SRC_DIR}/searchresultslistmodel.cpp
|
||||
${APP_SRC_DIR}/calloverlaymodel.cpp
|
||||
${APP_SRC_DIR}/spellcheckdictionarymanager.cpp
|
||||
${APP_SRC_DIR}/filestosendlistmodel.cpp
|
||||
${APP_SRC_DIR}/wizardviewstepmodel.cpp
|
||||
${APP_SRC_DIR}/avatarregistry.cpp
|
||||
|
@ -361,13 +362,13 @@ set(COMMON_SOURCES
|
|||
${APP_SRC_DIR}/currentcall.cpp
|
||||
${APP_SRC_DIR}/messageparser.cpp
|
||||
${APP_SRC_DIR}/previewengine.cpp
|
||||
${APP_SRC_DIR}/imagedownloader.cpp
|
||||
${APP_SRC_DIR}/filedownloader.cpp
|
||||
${APP_SRC_DIR}/pluginversionmanager.cpp
|
||||
${APP_SRC_DIR}/connectioninfolistmodel.cpp
|
||||
${APP_SRC_DIR}/pluginversionmanager.cpp
|
||||
${APP_SRC_DIR}/linkdevicemodel.cpp
|
||||
${APP_SRC_DIR}/qrcodescannermodel.cpp
|
||||
)
|
||||
${APP_SRC_DIR}/spellchecker.cpp)
|
||||
|
||||
set(COMMON_HEADERS
|
||||
${APP_SRC_DIR}/global.h
|
||||
|
@ -419,6 +420,7 @@ set(COMMON_HEADERS
|
|||
${APP_SRC_DIR}/conversationlistmodel.h
|
||||
${APP_SRC_DIR}/searchresultslistmodel.h
|
||||
${APP_SRC_DIR}/calloverlaymodel.h
|
||||
${APP_SRC_DIR}/spellcheckdictionarymanager.h
|
||||
${APP_SRC_DIR}/filestosendlistmodel.h
|
||||
${APP_SRC_DIR}/wizardviewstepmodel.h
|
||||
${APP_SRC_DIR}/avatarregistry.h
|
||||
|
@ -433,7 +435,7 @@ set(COMMON_HEADERS
|
|||
${APP_SRC_DIR}/currentcall.h
|
||||
${APP_SRC_DIR}/messageparser.h
|
||||
${APP_SRC_DIR}/htmlparser.h
|
||||
${APP_SRC_DIR}/imagedownloader.h
|
||||
${APP_SRC_DIR}/filedownloader.h
|
||||
${APP_SRC_DIR}/pluginversionmanager.h
|
||||
${APP_SRC_DIR}/connectioninfolistmodel.h
|
||||
${APP_SRC_DIR}/pttlistener.h
|
||||
|
@ -441,7 +443,7 @@ set(COMMON_HEADERS
|
|||
${APP_SRC_DIR}/crashreporter.h
|
||||
${APP_SRC_DIR}/linkdevicemodel.h
|
||||
${APP_SRC_DIR}/qrcodescannermodel.h
|
||||
)
|
||||
${APP_SRC_DIR}/spellchecker.h)
|
||||
|
||||
# For libavutil/avframe.
|
||||
set(LIBJAMI_CONTRIB_DIR "${DAEMON_DIR}/contrib")
|
||||
|
@ -469,6 +471,48 @@ if(ENABLE_CRASHREPORTS)
|
|||
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)
|
||||
set(WINDOWS_SYS_LIBS
|
||||
windowsapp.lib
|
||||
|
@ -531,8 +575,6 @@ elseif (NOT APPLE)
|
|||
${APP_SRC_DIR}/screencastportal.h)
|
||||
list(APPEND QT_MODULES DBus)
|
||||
|
||||
find_package(PkgConfig REQUIRED)
|
||||
|
||||
pkg_check_modules(GLIB REQUIRED glib-2.0)
|
||||
if(GLIB_FOUND)
|
||||
add_definitions(${GLIB_CFLAGS_OTHER})
|
||||
|
@ -615,6 +657,13 @@ else() # APPLE
|
|||
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
|
||||
if(QT6_VER AND QT6_PATH)
|
||||
message(STATUS "Using custom Qt version")
|
||||
|
@ -703,7 +752,9 @@ qt_add_executable(
|
|||
${QML_RESOURCES_QML}
|
||||
${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)
|
||||
|
||||
foreach(MODULE ${QT_MODULES})
|
||||
|
@ -797,6 +848,11 @@ elseif (NOT APPLE)
|
|||
PRIVATE
|
||||
JAMI_INSTALL_PREFIX="${JAMI_DATA_PREFIX}")
|
||||
|
||||
target_compile_definitions(
|
||||
${PROJECT_NAME}
|
||||
PRIVATE
|
||||
HUNSPELL_INSTALL_DIR="${HUNSPELL_DICT_DIR}")
|
||||
|
||||
# Logos
|
||||
install(
|
||||
FILES resources/images/jami.svg
|
||||
|
|
8
build.py
8
build.py
|
@ -112,7 +112,7 @@ ZYPPER_CLIENT_DEPENDENCIES = [
|
|||
'qt6-svg-devel', 'qt6-multimedia-devel', 'qt6-multimedia-imports',
|
||||
'qt6-declarative-devel', 'qt6-qmlcompiler-private-devel',
|
||||
'qt6-quickcontrols2-devel', 'qt6-shadertools-devel',
|
||||
'qrencode-devel', 'NetworkManager-devel'
|
||||
'qrencode-devel', 'NetworkManager-devel', 'hunspell-devel', 'libhunspell-devel'
|
||||
]
|
||||
|
||||
ZYPPER_QT_WEBENGINE = [
|
||||
|
@ -139,7 +139,7 @@ DNF_CLIENT_DEPENDENCIES = [
|
|||
'libnotify-devel',
|
||||
'qt6-qtbase-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']
|
||||
|
@ -171,7 +171,7 @@ APT_CLIENT_DEPENDENCIES = [
|
|||
'qml6-module-qtquick-dialogs', 'qml6-module-qtquick-layouts',
|
||||
'qml6-module-qtquick-shapes', 'qml6-module-qtquick-window',
|
||||
'qml6-module-qtquick-templates', 'qml6-module-qt-labs-platform',
|
||||
'libqrencode-dev', 'libnm-dev'
|
||||
'libqrencode-dev', 'libnm-dev', 'hunspell', 'libhunspell-dev'
|
||||
]
|
||||
|
||||
APT_QT_WEBENGINE = [
|
||||
|
@ -194,7 +194,7 @@ PACMAN_CLIENT_DEPENDENCIES = [
|
|||
'qt6-declarative', 'qt6-5compat', 'qt6-multimedia',
|
||||
'qt6-networkauth', 'qt6-shadertools',
|
||||
'qt6-svg', 'qt6-tools',
|
||||
'qrencode', 'libnm'
|
||||
'qrencode', 'libnm', 'hunspell'
|
||||
]
|
||||
|
||||
PACMAN_QT_WEBENGINE = ['qt6-webengine']
|
||||
|
|
|
@ -63,6 +63,8 @@ extern const QString defaultDownloadPath;
|
|||
X(WindowState, QWindow::AutomaticVisibility) \
|
||||
X(EnableExperimentalSwarm, false) \
|
||||
X(LANG, "SYSTEM") \
|
||||
X(SpellLang, "None") \
|
||||
X(EnableSpellCheck, true) \
|
||||
X(PluginStoreEndpoint, "https://plugins.jami.net") \
|
||||
X(PositionShareDuration, 15) \
|
||||
X(PositionShareLimit, true) \
|
||||
|
|
|
@ -15,8 +15,11 @@
|
|||
* 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 "contextmenu"
|
||||
import "../mainview"
|
||||
import "../mainview/components"
|
||||
|
||||
ContextMenuAutoLoader {
|
||||
id: root
|
||||
|
@ -27,8 +30,16 @@ ContextMenuAutoLoader {
|
|||
property var selectionEnd
|
||||
property bool customizePaste: false
|
||||
property bool selectOnly: false
|
||||
property bool checkSpell: false
|
||||
property var suggestionList
|
||||
property var menuItemsLength
|
||||
property var language
|
||||
|
||||
signal contextMenuRequirePaste
|
||||
SpellLanguageContextMenu {
|
||||
id: spellLanguageContextMenu
|
||||
active: checkSpell
|
||||
}
|
||||
|
||||
property list<GeneralMenuItem> menuItems: [
|
||||
GeneralMenuItem {
|
||||
|
@ -38,9 +49,8 @@ ContextMenuAutoLoader {
|
|||
isActif: lineEditObj.selectedText.length
|
||||
itemName: JamiStrings.copy
|
||||
hasIcon: false
|
||||
onClicked: {
|
||||
onClicked:
|
||||
lineEditObj.copy();
|
||||
}
|
||||
},
|
||||
GeneralMenuItem {
|
||||
id: cut
|
||||
|
@ -49,9 +59,8 @@ ContextMenuAutoLoader {
|
|||
isActif: lineEditObj.selectedText.length && !selectOnly
|
||||
itemName: JamiStrings.cut
|
||||
hasIcon: false
|
||||
onClicked: {
|
||||
onClicked:
|
||||
lineEditObj.cut();
|
||||
}
|
||||
},
|
||||
GeneralMenuItem {
|
||||
id: paste
|
||||
|
@ -65,9 +74,68 @@ ContextMenuAutoLoader {
|
|||
else
|
||||
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) {
|
||||
if (lineEditObj.selectedText.length === 0 && selectOnly)
|
||||
return;
|
||||
|
@ -85,6 +153,12 @@ ContextMenuAutoLoader {
|
|||
function onOpened() {
|
||||
lineEditObj.select(selectionStart, selectionEnd);
|
||||
}
|
||||
function onClosed() {
|
||||
if (!suggestionList || suggestionList.length == 0) {
|
||||
return;
|
||||
}
|
||||
removeItems();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: menuItemsToLoad = menuItems
|
||||
|
|
77
src/app/commoncomponents/SpellLanguageContextMenu.qml
Normal file
77
src/app/commoncomponents/SpellLanguageContextMenu.qml
Normal file
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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 {
|
|||
|
||||
function loadMenuItems(menuItems) {
|
||||
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) {
|
||||
var currentItemWidth = menuItems[j].itemPreferredWidth;
|
||||
if (currentItemWidth !== JamiTheme.menuItemsPreferredWidth && currentItemWidth > menuPreferredWidth && menuItems[j].canTrigger)
|
||||
menuPreferredWidth = currentItemWidth;
|
||||
}
|
||||
|
||||
// Add the items to the menu
|
||||
for (var i = 0; i < menuItems.length; ++i) {
|
||||
if (menuItems[i].canTrigger) {
|
||||
menuItems[i].parentMenu = root;
|
||||
|
|
|
@ -27,11 +27,14 @@ Loader {
|
|||
property int contextMenuItemPreferredHeight: 0
|
||||
property int contextMenuSeparatorPreferredHeight: 0
|
||||
|
||||
signal openRequested
|
||||
|
||||
active: false
|
||||
|
||||
visible: false
|
||||
|
||||
function openMenu() {
|
||||
openRequested();
|
||||
root.active = true;
|
||||
root.sourceComponent = menuComponent;
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ MenuItem {
|
|||
id: menuItem
|
||||
|
||||
property string itemName: ""
|
||||
property string content: ""
|
||||
property alias iconSource: contextMenuItemImage.source
|
||||
property string iconColor: ""
|
||||
property bool canTrigger: true
|
||||
|
|
|
@ -15,32 +15,32 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "imagedownloader.h"
|
||||
#include "filedownloader.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QLockFile>
|
||||
|
||||
ImageDownloader::ImageDownloader(ConnectivityMonitor* cm, QObject* parent)
|
||||
FileDownloader::FileDownloader(ConnectivityMonitor* cm, QObject* parent)
|
||||
: NetworkManager(cm, parent)
|
||||
{}
|
||||
|
||||
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]() {
|
||||
onDownloadImageFinished({}, localPath);
|
||||
onDownloadFileFinished({}, localPath);
|
||||
});
|
||||
|
||||
sendGetRequest(url, [this, localPath](const QByteArray& imageData) {
|
||||
onDownloadImageFinished(imageData, localPath);
|
||||
sendGetRequest(url, [this, localPath](const QByteArray& fileData) {
|
||||
onDownloadFileFinished(fileData, localPath);
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
ImageDownloader::onDownloadImageFinished(const QByteArray& data, const QString& localPath)
|
||||
FileDownloader::onDownloadFileFinished(const QByteArray& data, const QString& localPath)
|
||||
{
|
||||
if (data.isEmpty()) {
|
||||
Q_EMIT downloadImageFailed(localPath);
|
||||
Q_EMIT downloadFileFailed(localPath);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -49,7 +49,7 @@ ImageDownloader::onDownloadImageFinished(const QByteArray& data, const QString&
|
|||
const QDir dir;
|
||||
if (!dir.mkpath(dirPath)) {
|
||||
qWarning() << Q_FUNC_INFO << "Failed to create directory" << dirPath;
|
||||
Q_EMIT downloadImageFailed(localPath);
|
||||
Q_EMIT downloadFileFailed(localPath);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -58,10 +58,10 @@ ImageDownloader::onDownloadImageFinished(const QByteArray& data, const QString&
|
|||
if (lf.lock() && file.open(QIODevice::WriteOnly)) {
|
||||
file.write(data);
|
||||
file.close();
|
||||
Q_EMIT downloadImageSuccessful(localPath);
|
||||
Q_EMIT downloadFileSuccessful(localPath);
|
||||
return;
|
||||
}
|
||||
|
||||
qWarning() << Q_FUNC_INFO << "Failed to write image to" << localPath;
|
||||
Q_EMIT downloadImageFailed(localPath);
|
||||
qWarning() << Q_FUNC_INFO << "Failed to write file to" << localPath;
|
||||
Q_EMIT downloadFileFailed(localPath);
|
||||
}
|
|
@ -24,7 +24,7 @@
|
|||
#include <QQmlEngine> // QML registration
|
||||
#include <QApplication> // QML registration
|
||||
|
||||
class ImageDownloader : public NetworkManager
|
||||
class FileDownloader : public NetworkManager
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_SINGLETON
|
||||
|
@ -32,23 +32,23 @@ class ImageDownloader : public NetworkManager
|
|||
QML_PROPERTY(QString, cachePath)
|
||||
|
||||
public:
|
||||
static ImageDownloader* create(QQmlEngine*, QJSEngine*)
|
||||
static FileDownloader* create(QQmlEngine*, QJSEngine*)
|
||||
{
|
||||
return new ImageDownloader(
|
||||
return new FileDownloader(
|
||||
qApp->property("ConnectivityMonitor").value<ConnectivityMonitor*>());
|
||||
}
|
||||
|
||||
explicit ImageDownloader(ConnectivityMonitor* cm, QObject* parent = nullptr);
|
||||
~ImageDownloader() = default;
|
||||
explicit FileDownloader(ConnectivityMonitor* cm, QObject* parent = nullptr);
|
||||
~FileDownloader() = default;
|
||||
|
||||
// Download an image and call onDownloadImageFinished when done
|
||||
Q_INVOKABLE void downloadImage(const QUrl& url, const QString& localPath);
|
||||
// Download an image and call onDownloadFileFinished when done
|
||||
Q_INVOKABLE void downloadFile(const QUrl& url, const QString& localPath);
|
||||
|
||||
Q_SIGNALS:
|
||||
void downloadImageSuccessful(const QString& localPath);
|
||||
void downloadImageFailed(const QString& localPath);
|
||||
void downloadFileSuccessful(const QString& localPath);
|
||||
void downloadFileFailed(const QString& localPath);
|
||||
|
||||
private Q_SLOTS:
|
||||
// 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 @@
|
|||
#include "global.h"
|
||||
#include "qmlregister.h"
|
||||
#include "appsettingsmanager.h"
|
||||
#include "spellcheckdictionarymanager.h"
|
||||
#include "connectivitymonitor.h"
|
||||
#include "systemtray.h"
|
||||
#include "previewengine.h"
|
||||
|
@ -190,6 +191,7 @@ MainApplication::init()
|
|||
// to any other initialization. This won't do anything if crashpad isn't
|
||||
// enabled.
|
||||
settingsManager_ = new AppSettingsManager(this);
|
||||
spellCheckDictionaryManager_ = new SpellCheckDictionaryManager(settingsManager_, this);
|
||||
crashReporter_ = new CrashReporter(settingsManager_, this);
|
||||
|
||||
// This 2-phase initialisation prevents ephemeral instances from
|
||||
|
@ -423,6 +425,7 @@ MainApplication::initQmlLayer()
|
|||
lrcInstance_.get(),
|
||||
systemTray_,
|
||||
settingsManager_,
|
||||
spellCheckDictionaryManager_,
|
||||
connectivityMonitor_,
|
||||
previewEngine_,
|
||||
&screenInfo_,
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
class ConnectivityMonitor;
|
||||
class SystemTray;
|
||||
class AppSettingsManager;
|
||||
class SpellCheckDictionaryManager;
|
||||
class CrashReporter;
|
||||
class PreviewEngine;
|
||||
|
||||
|
@ -118,6 +119,7 @@ private:
|
|||
ConnectivityMonitor* connectivityMonitor_;
|
||||
SystemTray* systemTray_;
|
||||
AppSettingsManager* settingsManager_;
|
||||
SpellCheckDictionaryManager* spellCheckDictionaryManager_;
|
||||
PreviewEngine* previewEngine_;
|
||||
CrashReporter* crashReporter_;
|
||||
|
||||
|
|
34
src/app/mainview/components/CachedFile.qml
Normal file
34
src/app/mainview/components/CachedFile.qml
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 {
|
|||
}
|
||||
|
||||
Connections {
|
||||
target: ImageDownloader
|
||||
function onDownloadImageSuccessful(localPath) {
|
||||
target: FileDownloader
|
||||
function onDownloadFileSuccessful(localPath) {
|
||||
if (localPath === cachedImage.localPath) {
|
||||
image.source = UtilsAdapter.urlFromLocalPath(localPath);
|
||||
}
|
||||
|
@ -90,7 +90,7 @@ Item {
|
|||
}
|
||||
if (downloadUrl && downloadUrl !== "" && localPath !== "") {
|
||||
if (!UtilsAdapter.fileExists(localPath)) {
|
||||
ImageDownloader.downloadImage(downloadUrl, localPath);
|
||||
FileDownloader.downloadFile(downloadUrl, localPath);
|
||||
} else {
|
||||
image.source = UtilsAdapter.urlFromLocalPath(localPath);
|
||||
if (image.isGif) {
|
||||
|
|
|
@ -116,23 +116,23 @@ Rectangle {
|
|||
|
||||
spacing: 0
|
||||
|
||||
LineEditContextMenu {
|
||||
id: displayNameContextMenu
|
||||
lineEditObj: title
|
||||
selectOnly: true
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
cursorShape: Qt.IBeamCursor
|
||||
onClicked: function (mouse) {
|
||||
displayNameContextMenu.openMenuAt(mouse);
|
||||
}
|
||||
}
|
||||
|
||||
ElidedTextLabel {
|
||||
id: title
|
||||
|
||||
LineEditContextMenu {
|
||||
id: displayNameContextMenu
|
||||
lineEditObj: title
|
||||
selectOnly: true
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
cursorShape: Qt.IBeamCursor
|
||||
onClicked: function (mouse) {
|
||||
displayNameContextMenu.openMenuAt(mouse);
|
||||
}
|
||||
}
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
|
||||
|
||||
font.pointSize: JamiTheme.textFontSize + 2
|
||||
|
|
|
@ -17,19 +17,17 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import net.jami.Adapters 1.1
|
||||
import net.jami.Constants 1.1
|
||||
import net.jami.Enums 1.1
|
||||
import net.jami.Models 1.1
|
||||
|
||||
import SortFilterProxyModel 0.2
|
||||
|
||||
import "../../commoncomponents"
|
||||
|
||||
JamiFlickable {
|
||||
id: root
|
||||
|
||||
property int underlineHeight: JamiTheme.messageUnderlineHeight
|
||||
property alias text: textArea.text
|
||||
property var textAreaObj: textArea
|
||||
property alias placeholderText: textArea.placeholderText
|
||||
|
@ -39,9 +37,12 @@ JamiFlickable {
|
|||
property bool showPreview: false
|
||||
property bool isShowTypo: UtilsAdapter.getAppValue(Settings.Key.ShowMardownOption)
|
||||
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
|
||||
// and also to debounce the textChanged signal's effect on the composing status.
|
||||
property var underlineList: []
|
||||
property string cachedText
|
||||
property string debounceText
|
||||
|
||||
|
@ -72,6 +73,7 @@ JamiFlickable {
|
|||
|
||||
lineEditObj: textArea
|
||||
customizePaste: true
|
||||
checkSpell: (Qt.platform.os.toString() === "linux") ? true : false
|
||||
|
||||
onContextMenuRequirePaste: {
|
||||
// Intercept paste event to use C++ QMimeData
|
||||
|
@ -115,9 +117,79 @@ JamiFlickable {
|
|||
TextArea.flickable: 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
|
||||
leftPadding: JamiTheme.scrollBarHandleSize
|
||||
rightPadding: JamiTheme.scrollBarHandleSize
|
||||
topPadding: 0
|
||||
bottomPadding: underlineHeight
|
||||
|
||||
persistentSelection: true
|
||||
verticalAlignment: TextEdit.AlignVCenter
|
||||
font.pointSize: JamiTheme.textFontSize + 2
|
||||
|
@ -135,12 +207,37 @@ JamiFlickable {
|
|||
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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
onTextChanged: {
|
||||
updateUnderlineText();
|
||||
if (text !== debounceText && !showPreview) {
|
||||
debounceText = text;
|
||||
MessagesAdapter.userIsComposing(text ? true : false);
|
||||
|
@ -152,6 +249,8 @@ JamiFlickable {
|
|||
// eg. Enter -> Send messages
|
||||
// Shift + Enter -> Next Line
|
||||
Keys.onPressed: function (keyEvent) {
|
||||
// Update underline on each input to take into account deleted text and sent ones
|
||||
updateUnderlineText();
|
||||
if (keyEvent.matches(StandardKey.Paste)) {
|
||||
MessagesAdapter.onPaste();
|
||||
keyEvent.accepted = true;
|
||||
|
@ -180,5 +279,41 @@ JamiFlickable {
|
|||
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 @@
|
|||
#include "qtutils.h"
|
||||
#include "messageparser.h"
|
||||
#include "previewengine.h"
|
||||
#include "spellchecker.h"
|
||||
|
||||
#include <api/datatransfermodel.h>
|
||||
#include <api/contact.h>
|
||||
|
@ -39,17 +40,25 @@
|
|||
#include <QtMath>
|
||||
#include <QRegExp>
|
||||
|
||||
#define SUGGESTIONS_MAX_SIZE 3 // limit the number of spelling suggestions
|
||||
|
||||
MessagesAdapter::MessagesAdapter(AppSettingsManager* settingsManager,
|
||||
PreviewEngine* previewEngine,
|
||||
SpellCheckDictionaryManager* spellCheckDictionaryManager,
|
||||
LRCInstance* instance,
|
||||
QObject* parent)
|
||||
: QmlAdapterBase(instance, parent)
|
||||
, settingsManager_(settingsManager)
|
||||
, spellCheckDictionaryManager_(spellCheckDictionaryManager)
|
||||
, messageParser_(new MessageParser(previewEngine, this))
|
||||
, filteredMsgListModel_(new FilteredMsgListModel(this))
|
||||
, mediaInteractions_(std::make_unique<MessageListModel>(nullptr))
|
||||
, 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());
|
||||
|
||||
set_messageListModel(QVariant::fromValue(filteredMsgListModel_));
|
||||
|
@ -727,3 +736,53 @@ MessagesAdapter::getMsgListSourceModel() const
|
|||
// However it may be a nullptr if not yet set.
|
||||
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 @@
|
|||
#include "previewengine.h"
|
||||
#include "messageparser.h"
|
||||
#include "appsettingsmanager.h"
|
||||
#include "spellchecker.h"
|
||||
#include "spellcheckdictionarymanager.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
@ -46,7 +48,6 @@ public:
|
|||
connect(this, &QAbstractItemModel::rowsRemoved, this, &FilteredMsgListModel::countChanged);
|
||||
connect(this, &QAbstractItemModel::modelReset, this, &FilteredMsgListModel::countChanged);
|
||||
connect(this, &QAbstractItemModel::layoutChanged, this, &FilteredMsgListModel::countChanged);
|
||||
|
||||
}
|
||||
bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override
|
||||
{
|
||||
|
@ -101,11 +102,14 @@ public:
|
|||
{
|
||||
return new MessagesAdapter(qApp->property("AppSettingsManager").value<AppSettingsManager*>(),
|
||||
qApp->property("PreviewEngine").value<PreviewEngine*>(),
|
||||
qApp->property("SpellCheckDictionaryManager")
|
||||
.value<SpellCheckDictionaryManager*>(),
|
||||
qApp->property("LRCInstance").value<LRCInstance*>());
|
||||
}
|
||||
|
||||
explicit MessagesAdapter(AppSettingsManager* settingsManager,
|
||||
PreviewEngine* previewEngine,
|
||||
SpellCheckDictionaryManager* spellCheckDictionaryManager,
|
||||
LRCInstance* instance,
|
||||
QObject* parent = nullptr);
|
||||
~MessagesAdapter() = default;
|
||||
|
@ -164,6 +168,10 @@ public:
|
|||
Q_INVOKABLE QVariant dataForInteraction(const QString& interactionId,
|
||||
int role = Qt::DisplayRole) const;
|
||||
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.
|
||||
void setMessagesImageContent(const QString& path, bool isBased64 = false);
|
||||
|
@ -198,14 +206,12 @@ private:
|
|||
QList<QString> conversationTypersUrlToName(const QSet<QString>& typersSet);
|
||||
|
||||
AppSettingsManager* settingsManager_;
|
||||
SpellCheckDictionaryManager* spellCheckDictionaryManager_;
|
||||
MessageParser* messageParser_;
|
||||
|
||||
FilteredMsgListModel* filteredMsgListModel_;
|
||||
|
||||
static constexpr const int loadChunkSize_ {20};
|
||||
|
||||
std::unique_ptr<MessageListModel> mediaInteractions_;
|
||||
|
||||
QTimer* timestampTimer_ {nullptr};
|
||||
QTimer* timestampTimer_;
|
||||
std::shared_ptr<SpellChecker> spellChecker_;
|
||||
static constexpr const int loadChunkSize_ {20};
|
||||
static constexpr const int timestampUpdateIntervalMs_ {1000};
|
||||
};
|
||||
|
|
|
@ -275,6 +275,7 @@ Item {
|
|||
property string share: qsTr("Share")
|
||||
property string cut: qsTr("Cut")
|
||||
property string paste: qsTr("Paste")
|
||||
property string language: qsTr("Language")
|
||||
|
||||
// ConversationContextMenu
|
||||
property string startAudioCall: qsTr("Start audio call")
|
||||
|
@ -508,7 +509,7 @@ Item {
|
|||
property string displayHyperlinkPreviews: qsTr("Web link previews")
|
||||
property string displayHyperlinkPreviewsDescription: qsTr("Preview requires downloading content from third-party servers.")
|
||||
|
||||
property string language: qsTr("User interface language")
|
||||
property string userInterfaceLanguage: qsTr("User interface language")
|
||||
property string verticalViewOpt: qsTr("Vertical view")
|
||||
property string horizontalViewOpt: qsTr("Horizontal view")
|
||||
|
||||
|
@ -905,4 +906,12 @@ Item {
|
|||
property string copyAllData: qsTr("Copy all data")
|
||||
property string remote: qsTr("Remote: %1")
|
||||
property string view: qsTr("View")
|
||||
|
||||
// Spellchecker
|
||||
property string checkSpelling: qsTr("Check spelling while typing")
|
||||
property string textLanguage: qsTr("Text language")
|
||||
property string textLanguageDescription: qsTr("To install new dictionaries, use the system package manager.")
|
||||
property string spellchecking: qsTr("Spellchecking")
|
||||
property string refresh: qsTr("Refresh")
|
||||
property string refreshInstalledDictionaries: qsTr("Refresh installed dictionaries")
|
||||
}
|
||||
|
|
|
@ -516,6 +516,7 @@ Item {
|
|||
property int showTypoSecondToggleWidth: 540
|
||||
property int messageBarMaximumHeight: 150
|
||||
property int messageBarMinimumHeight: 36
|
||||
property int messageUnderlineHeight: 2
|
||||
|
||||
// InvitationView
|
||||
property real invitationViewAvatarSize: 112
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
#include "positionmanager.h"
|
||||
#include "tipsmodel.h"
|
||||
#include "connectivitymonitor.h"
|
||||
#include "imagedownloader.h"
|
||||
#include "filedownloader.h"
|
||||
#include "utilsadapter.h"
|
||||
#include "conversationsadapter.h"
|
||||
#include "currentcall.h"
|
||||
|
@ -36,6 +36,7 @@
|
|||
#include "currentaccounttomigrate.h"
|
||||
#include "pttlistener.h"
|
||||
#include "calloverlaymodel.h"
|
||||
#include "spellcheckdictionarymanager.h"
|
||||
#include "accountlistmodel.h"
|
||||
#include "mediacodeclistmodel.h"
|
||||
#include "audiodevicemodel.h"
|
||||
|
@ -64,6 +65,7 @@
|
|||
#include "wizardviewstepmodel.h"
|
||||
#include "linkdevicemodel.h"
|
||||
#include "qrcodescannermodel.h"
|
||||
#include "spellchecker.h"
|
||||
|
||||
#include "api/peerdiscoverymodel.h"
|
||||
#include "api/codecmodel.h"
|
||||
|
@ -117,6 +119,7 @@ registerTypes(QQmlEngine* engine,
|
|||
LRCInstance* lrcInstance,
|
||||
SystemTray* systemTray,
|
||||
AppSettingsManager* settingsManager,
|
||||
SpellCheckDictionaryManager* spellCheckDictionaryManager,
|
||||
ConnectivityMonitor* connectivityMonitor,
|
||||
PreviewEngine* previewEngine,
|
||||
ScreenInfo* screenInfo,
|
||||
|
@ -201,6 +204,7 @@ registerTypes(QQmlEngine* engine,
|
|||
qApp->setProperty("AppSettingsManager", QVariant::fromValue(settingsManager));
|
||||
qApp->setProperty("ConnectivityMonitor", QVariant::fromValue(connectivityMonitor));
|
||||
qApp->setProperty("PreviewEngine", QVariant::fromValue(previewEngine));
|
||||
qApp->setProperty("SpellCheckDictionaryManager", QVariant::fromValue(spellCheckDictionaryManager));
|
||||
|
||||
// qml adapter registration
|
||||
QML_REGISTERSINGLETON_TYPE(NS_HELPERS, QRCodeScannerModel);
|
||||
|
@ -220,7 +224,7 @@ registerTypes(QQmlEngine* engine,
|
|||
QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, TipsModel);
|
||||
QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, VideoDevices);
|
||||
QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, CurrentAccountToMigrate);
|
||||
QML_REGISTERSINGLETON_TYPE(NS_HELPERS, ImageDownloader);
|
||||
QML_REGISTERSINGLETON_TYPE(NS_HELPERS, FileDownloader);
|
||||
|
||||
// TODO: remove these
|
||||
QML_REGISTERSINGLETONTYPE_CUSTOM(NS_MODELS, AVModel, &lrcInstance->avModel())
|
||||
|
@ -237,6 +241,7 @@ registerTypes(QQmlEngine* engine,
|
|||
QML_REGISTERTYPE(NS_MODELS, PluginListPreferenceModel);
|
||||
QML_REGISTERTYPE(NS_MODELS, FilesToSendListModel);
|
||||
QML_REGISTERTYPE(NS_MODELS, CallInformationListModel);
|
||||
QML_REGISTERTYPE(NS_MODELS, SpellChecker);
|
||||
|
||||
// Roles & type enums for models
|
||||
QML_REGISTERNAMESPACE(NS_MODELS, AccountList::staticMetaObject, "AccountList");
|
||||
|
@ -250,6 +255,7 @@ registerTypes(QQmlEngine* engine,
|
|||
QML_REGISTERSINGLETONTYPE_POBJECT(NS_CONSTANTS, screenInfo, "CurrentScreenInfo")
|
||||
QML_REGISTERSINGLETONTYPE_POBJECT(NS_CONSTANTS, lrcInstance, "LRCInstance")
|
||||
QML_REGISTERSINGLETONTYPE_POBJECT(NS_CONSTANTS, settingsManager, "AppSettingsManager")
|
||||
QML_REGISTERSINGLETONTYPE_POBJECT(NS_CONSTANTS, spellCheckDictionaryManager, "SpellCheckDictionaryManager")
|
||||
|
||||
// Lrc namespaces, models, and singletons
|
||||
QML_REGISTERNAMESPACE(NS_MODELS, lrc::api::staticMetaObject, "Lrc");
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
class SystemTray;
|
||||
class LRCInstance;
|
||||
class AppSettingsManager;
|
||||
class SpellCheckDictionaryManager;
|
||||
class PreviewEngine;
|
||||
class ScreenInfo;
|
||||
class MainApplication;
|
||||
|
@ -61,6 +62,7 @@ void registerTypes(QQmlEngine* engine,
|
|||
LRCInstance* lrcInstance,
|
||||
SystemTray* systemTray,
|
||||
AppSettingsManager* appSettingsManager,
|
||||
SpellCheckDictionaryManager* spellCheckDictionaryManager,
|
||||
ConnectivityMonitor* connectivityMonitor,
|
||||
PreviewEngine* previewEngine,
|
||||
ScreenInfo* screenInfo,
|
||||
|
|
|
@ -38,7 +38,6 @@ SettingsPageBase {
|
|||
flickableContent: ColumnLayout {
|
||||
id: manageAccountColumnLayout
|
||||
|
||||
width: contentFlickableWidth
|
||||
spacing: JamiTheme.settingsBlockSpacing
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: JamiTheme.preferredSettingsMarginSize
|
||||
|
|
|
@ -23,6 +23,7 @@ import net.jami.Constants 1.1
|
|||
import net.jami.Enums 1.1
|
||||
import net.jami.Models 1.1
|
||||
import "../../commoncomponents"
|
||||
import SortFilterProxyModel 0.2
|
||||
|
||||
SettingsPageBase {
|
||||
id: root
|
||||
|
@ -157,8 +158,8 @@ SettingsPageBase {
|
|||
Layout.fillWidth: true
|
||||
height: JamiTheme.preferredFieldHeight
|
||||
|
||||
labelText: JamiStrings.language
|
||||
tipText: JamiStrings.language
|
||||
labelText: JamiStrings.userInterfaceLanguage
|
||||
tipText: JamiStrings.userInterfaceLanguage
|
||||
comboModel: ListModel {
|
||||
id: langModel
|
||||
Component.onCompleted: {
|
||||
|
@ -183,6 +184,132 @@ SettingsPageBase {
|
|||
UtilsAdapter.setAppValue(Settings.Key.LANG, comboModel.get(modelIndex).id);
|
||||
}
|
||||
}
|
||||
}
|
||||
ColumnLayout {
|
||||
|
||||
width: parent.width
|
||||
spacing: JamiTheme.settingsCategorySpacing
|
||||
visible: (Qt.platform.os.toString() !== "linux") ? false : true
|
||||
|
||||
Text {
|
||||
id: spellcheckingTitle
|
||||
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
Layout.preferredWidth: parent.width
|
||||
|
||||
text: JamiStrings.spellchecking
|
||||
color: JamiTheme.textColor
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
font.pixelSize: JamiTheme.settingsTitlePixelSize
|
||||
font.kerning: true
|
||||
}
|
||||
|
||||
ToggleSwitch {
|
||||
id: enableSpellCheckToggleSwitch
|
||||
Layout.fillWidth: true
|
||||
visible: true
|
||||
|
||||
checked: UtilsAdapter.getAppValue(Settings.Key.EnableSpellCheck)
|
||||
labelText: JamiStrings.checkSpelling
|
||||
descText: JamiStrings.textLanguageDescription
|
||||
tooltipText: JamiStrings.checkSpelling
|
||||
onSwitchToggled: {
|
||||
UtilsAdapter.setAppValue(Settings.Key.EnableSpellCheck, checked);
|
||||
}
|
||||
}
|
||||
|
||||
SettingsComboBox {
|
||||
id: spellCheckLangComboBoxSetting
|
||||
|
||||
Layout.fillWidth: true
|
||||
height: JamiTheme.preferredFieldHeight
|
||||
|
||||
labelText: JamiStrings.textLanguage
|
||||
tipText: JamiStrings.textLanguage
|
||||
comboModel: ListModel {
|
||||
id: installedSpellCheckLangModel
|
||||
Component.onCompleted: {
|
||||
var supported = SpellCheckDictionaryManager.installedDictionaries();
|
||||
var keys = Object.keys(supported);
|
||||
var currentKey = UtilsAdapter.getAppValue(Settings.Key.SpellLang);
|
||||
for (var i = 0; i < keys.length; ++i) {
|
||||
append({
|
||||
"textDisplay": supported[keys[i]],
|
||||
"id": keys[i]
|
||||
});
|
||||
if (keys[i] === currentKey)
|
||||
spellCheckLangComboBoxSetting.modelIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
widthOfComboBox: itemWidth
|
||||
role: "textDisplay"
|
||||
|
||||
onActivated: {
|
||||
UtilsAdapter.setAppValue(Settings.Key.SpellLang, comboModel.get(modelIndex).id);
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: JamiTheme.preferredFieldHeight
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.rightMargin: JamiTheme.preferredMarginSize
|
||||
|
||||
color: JamiTheme.textColor
|
||||
wrapMode: Text.WordWrap
|
||||
text: JamiStrings.refreshInstalledDictionaries
|
||||
font.pointSize: JamiTheme.settingsFontSize
|
||||
font.kerning: true
|
||||
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
MaterialButton {
|
||||
id: refreshInstalledDictionariesPushButton
|
||||
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
|
||||
preferredWidth: textSizeRefresh.width + 2 * JamiTheme.buttontextWizzardPadding
|
||||
buttontextHeightMargin: JamiTheme.buttontextHeightMargin
|
||||
|
||||
primary: true
|
||||
toolTipText: JamiStrings.refresh
|
||||
|
||||
text: JamiStrings.refresh
|
||||
|
||||
onClicked: {
|
||||
SpellCheckDictionaryManager.refreshDictionaries();
|
||||
var langIdx = spellCheckLangComboBoxSetting.modelIndex;
|
||||
installedSpellCheckLangModel.clear();
|
||||
var supported = SpellCheckDictionaryManager.installedDictionaries();
|
||||
var keys = Object.keys(supported);
|
||||
for (var i = 0; i < keys.length; ++i) {
|
||||
installedSpellCheckLangModel.append({
|
||||
"textDisplay": supported[keys[i]],
|
||||
"id": keys[i]
|
||||
});
|
||||
}
|
||||
spellCheckLangComboBoxSetting.modelIndex = langIdx;
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: textSizeRefresh
|
||||
font.weight: Font.Bold
|
||||
font.pixelSize: JamiTheme.wizardViewButtonFontPixelSize
|
||||
font.capitalization: Font.AllUppercase
|
||||
text: refreshInstalledDictionariesPushButton.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: UtilsAdapter
|
||||
|
@ -200,6 +327,21 @@ SettingsPageBase {
|
|||
}
|
||||
langComboBoxSetting.modelIndex = langIdx;
|
||||
}
|
||||
|
||||
// Repopulate the spell check language list
|
||||
function onSpellLanguageChanged() {
|
||||
var langIdx = spellCheckLangComboBoxSetting.modelIndex;
|
||||
installedSpellCheckLangModel.clear();
|
||||
var supported = SpellCheckDictionaryManager.installedDictionaries();
|
||||
var keys = Object.keys(supported);
|
||||
for (var i = 0; i < keys.length; ++i) {
|
||||
installedSpellCheckLangModel.append({
|
||||
"textDisplay": supported[keys[i]],
|
||||
"id": keys[i]
|
||||
});
|
||||
}
|
||||
spellCheckLangComboBoxSetting.modelIndex = langIdx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -257,6 +399,7 @@ SettingsPageBase {
|
|||
closeOrMinCheckBox.checked = UtilsAdapter.getDefault(Settings.Key.MinimizeOnClose);
|
||||
checkboxCallSwarm.checked = UtilsAdapter.getDefault(Settings.Key.EnableExperimentalSwarm);
|
||||
langComboBoxSetting.modelIndex = 0;
|
||||
spellCheckLangComboBoxSetting.modelIndex = 0;
|
||||
UtilsAdapter.setToDefault(Settings.Key.EnableNotifications);
|
||||
UtilsAdapter.setToDefault(Settings.Key.MinimizeOnClose);
|
||||
UtilsAdapter.setToDefault(Settings.Key.LANG);
|
||||
|
|
149
src/app/spellcheckdictionarymanager.cpp
Normal file
149
src/app/spellcheckdictionarymanager.cpp
Normal file
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "spellcheckdictionarymanager.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QBuffer>
|
||||
#include <QClipboard>
|
||||
#include <QFileInfo>
|
||||
#include <QRegExp>
|
||||
#include <QMimeData>
|
||||
#include <QDir>
|
||||
#include <QMimeDatabase>
|
||||
#include <QUrl>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QEventLoop>
|
||||
#include <QRegularExpression>
|
||||
|
||||
SpellCheckDictionaryManager::SpellCheckDictionaryManager(AppSettingsManager* settingsManager,
|
||||
QObject* parent)
|
||||
: QObject {parent}
|
||||
, settingsManager_ {settingsManager}
|
||||
{}
|
||||
|
||||
QVariantMap
|
||||
SpellCheckDictionaryManager::installedDictionaries()
|
||||
{
|
||||
// If we already have a cache of the installed dictionaries, return it
|
||||
if (cachedInstalledDictionaries_.size() > 0) {
|
||||
return cachedInstalledDictionaries_;
|
||||
|
||||
// If not, we need to check the dictionaries directory
|
||||
} else {
|
||||
QString hunspellDataDir = getDictionariesPath();
|
||||
|
||||
auto dictionariesDir = QDir(hunspellDataDir);
|
||||
QRegExp regex("(.*).dic");
|
||||
QSet<QString> nativeNames;
|
||||
|
||||
QVariantMap result;
|
||||
result["NONE"] = tr("None");
|
||||
QStringList folders = dictionariesDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
|
||||
// Check for dictionary files in the base directory
|
||||
QStringList rootDicFiles = dictionariesDir.entryList(QStringList() << "*.dic", QDir::Files);
|
||||
for (const auto& dicFile : rootDicFiles) {
|
||||
regex.indexIn(dicFile);
|
||||
auto captured = regex.capturedTexts();
|
||||
if (captured.size() == 2) {
|
||||
auto nativeName = QLocale(captured[1]).nativeLanguageName();
|
||||
if (!nativeName.isEmpty() && !nativeNames.contains(nativeName)) {
|
||||
result[captured[1]] = nativeName;
|
||||
nativeNames.insert(nativeName);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check for dictionary files in subdirectories
|
||||
for (const auto& folder : folders) {
|
||||
QDir subDir = dictionariesDir.absoluteFilePath(folder);
|
||||
QStringList dicFiles = subDir.entryList(QStringList() << "*.dic", QDir::Files);
|
||||
subDir.setFilter(QDir::Files | QDir::AllDirs | QDir::NoDotAndDotDot);
|
||||
subDir.setSorting(QDir::DirsFirst);
|
||||
QFileInfoList list = subDir.entryInfoList();
|
||||
for (const auto& fileInfo : list) {
|
||||
if (fileInfo.isDir()) {
|
||||
QDir recursiveDir(fileInfo.absoluteFilePath());
|
||||
QStringList recursiveDicFiles = recursiveDir.entryList(QStringList() << "*.dic",
|
||||
QDir::Files);
|
||||
if (!recursiveDicFiles.isEmpty()) {
|
||||
dicFiles.append(recursiveDicFiles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract the locale from the dictionary file names
|
||||
for (const auto& dicFile : dicFiles) {
|
||||
regex.indexIn(dicFile);
|
||||
auto captured = regex.capturedTexts();
|
||||
|
||||
if (captured.size() == 2) {
|
||||
auto nativeName = QLocale(captured[1]).nativeLanguageName();
|
||||
|
||||
if (nativeName.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!nativeNames.contains(nativeName)) {
|
||||
result[folder + QDir::separator() + captured[1]] = nativeName;
|
||||
nativeNames.insert(nativeName);
|
||||
} else {
|
||||
qWarning() << "Duplicate native name found, skipping:" << nativeName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
cachedInstalledDictionaries_ = result;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
QString
|
||||
SpellCheckDictionaryManager::getDictionariesPath()
|
||||
{
|
||||
#if defined(Q_OS_LINUX)
|
||||
QString hunDir = "/usr/share/hunspell/";
|
||||
;
|
||||
|
||||
#elif defined(Q_OS_MACOS)
|
||||
QString hunDir = "/Library/Spelling/";
|
||||
#else
|
||||
QString hunDir = "";
|
||||
#endif
|
||||
return hunDir;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
SpellCheckDictionaryManager::refreshDictionaries()
|
||||
{
|
||||
cachedInstalledDictionaries_.clear();
|
||||
}
|
||||
|
||||
QString
|
||||
SpellCheckDictionaryManager::getSpellLanguage()
|
||||
{
|
||||
auto pref = settingsManager_->getValue(Settings::Key::SpellLang).toString();
|
||||
return pref ;
|
||||
}
|
||||
|
||||
// Is only used at application boot time
|
||||
QString
|
||||
SpellCheckDictionaryManager::getDictionaryPath()
|
||||
{
|
||||
return "/usr/share/hunspell/" + getSpellLanguage();
|
||||
}
|
39
src/app/spellcheckdictionarymanager.h
Normal file
39
src/app/spellcheckdictionarymanager.h
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "appsettingsmanager.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QApplication>
|
||||
#include <QQmlEngine>
|
||||
|
||||
class SpellCheckDictionaryManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QVariantMap cachedInstalledDictionaries_;
|
||||
AppSettingsManager* settingsManager_;
|
||||
public:
|
||||
explicit SpellCheckDictionaryManager(AppSettingsManager* settingsManager,
|
||||
QObject* parent = nullptr);
|
||||
|
||||
Q_INVOKABLE QVariantMap installedDictionaries();
|
||||
Q_INVOKABLE QString getDictionariesPath();
|
||||
Q_INVOKABLE void refreshDictionaries();
|
||||
Q_INVOKABLE QString getDictionaryPath();
|
||||
Q_INVOKABLE QString getSpellLanguage();
|
||||
};
|
117
src/app/spellchecker.cpp
Normal file
117
src/app/spellchecker.cpp
Normal file
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* 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/>.
|
||||
*
|
||||
* \file spellchecker.c
|
||||
*/
|
||||
|
||||
#include "spellchecker.h"
|
||||
|
||||
#include <QString>
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
#include <QTextCodec>
|
||||
#include <QStringList>
|
||||
#include <QDebug>
|
||||
#include <QRegExp>
|
||||
#include <QRegularExpression>
|
||||
#include <QRegularExpressionMatchIterator>
|
||||
|
||||
SpellChecker::SpellChecker(const QString& dictionaryPath)
|
||||
{
|
||||
replaceDictionary(dictionaryPath);
|
||||
}
|
||||
|
||||
bool
|
||||
SpellChecker::spell(const QString& word)
|
||||
{
|
||||
// Encode from Unicode to the encoding used by current dictionary
|
||||
return hunspell_->spell(word.toStdString()) != 0;
|
||||
}
|
||||
|
||||
QStringList
|
||||
SpellChecker::suggest(const QString& word)
|
||||
{
|
||||
// Encode from Unicode to the encoding used by current dictionary
|
||||
std::vector<std::string> numSuggestions = hunspell_->suggest(word.toStdString());
|
||||
QStringList suggestions;
|
||||
|
||||
for (size_t i = 0; i < numSuggestions.size(); ++i) {
|
||||
suggestions << QString::fromStdString(numSuggestions.at(i));
|
||||
}
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
void
|
||||
SpellChecker::ignoreWord(const QString& word)
|
||||
{
|
||||
put_word(word);
|
||||
}
|
||||
|
||||
void
|
||||
SpellChecker::put_word(const QString& word)
|
||||
{
|
||||
hunspell_->add(codec_->fromUnicode(word).constData());
|
||||
}
|
||||
|
||||
void
|
||||
SpellChecker::replaceDictionary(const QString& dictionaryPath)
|
||||
{
|
||||
QString dictFile = dictionaryPath + ".dic";
|
||||
QString affixFile = dictionaryPath + ".aff";
|
||||
QByteArray dictFilePathBA = dictFile.toLocal8Bit();
|
||||
QByteArray affixFilePathBA = affixFile.toLocal8Bit();
|
||||
if (hunspell_) {
|
||||
hunspell_.reset();
|
||||
}
|
||||
hunspell_ = std::make_shared<Hunspell>(affixFilePathBA.constData(), dictFilePathBA.constData());
|
||||
|
||||
// detect encoding analyzing the SET option in the affix file
|
||||
encoding_ = "ISO8859-1";
|
||||
QFile _affixFile(affixFile);
|
||||
if (_affixFile.open(QIODevice::ReadOnly)) {
|
||||
QTextStream stream(&_affixFile);
|
||||
QRegExp enc_detector("^\\s*SET\\s+([A-Z0-9\\-]+)\\s*", Qt::CaseInsensitive);
|
||||
for (QString line = stream.readLine(); !line.isEmpty(); line = stream.readLine()) {
|
||||
if (enc_detector.indexIn(line) > -1) {
|
||||
encoding_ = enc_detector.cap(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
_affixFile.close();
|
||||
}
|
||||
|
||||
codec_ = QTextCodec::codecForName(this->encoding_.toLatin1().constData());
|
||||
}
|
||||
|
||||
QList<SpellChecker::WordInfo>
|
||||
SpellChecker::findWords(const QString& text)
|
||||
{
|
||||
// This is in the C++ part of the code because QML regex does not support unicode
|
||||
QList<WordInfo> results;
|
||||
QRegularExpression regex("\\p{L}+|\\p{N}+");
|
||||
QRegularExpressionMatchIterator iter = regex.globalMatch(text);
|
||||
|
||||
while (iter.hasNext()) {
|
||||
QRegularExpressionMatch match = iter.next();
|
||||
WordInfo info;
|
||||
info.word = match.captured();
|
||||
info.position = match.capturedStart();
|
||||
info.length = match.capturedLength();
|
||||
results.append(info);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
63
src/app/spellchecker.h
Normal file
63
src/app/spellchecker.h
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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/>.
|
||||
*
|
||||
* \file spellchecker.h
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "lrcinstance.h"
|
||||
#include "qmladapterbase.h"
|
||||
#include "previewengine.h"
|
||||
|
||||
#include <QTextCodec>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QDebug>
|
||||
#include <QObject>
|
||||
#include <string>
|
||||
|
||||
#include <hunspell/hunspell.hxx>
|
||||
|
||||
class Hunspell;
|
||||
|
||||
class SpellChecker : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SpellChecker(const QString&);
|
||||
~SpellChecker() = default;
|
||||
void replaceDictionary(const QString& dictionaryPath);
|
||||
|
||||
Q_INVOKABLE bool spell(const QString& word);
|
||||
Q_INVOKABLE QStringList suggest(const QString& word);
|
||||
Q_INVOKABLE void ignoreWord(const QString& word);
|
||||
|
||||
// Used to find words and their position in a text
|
||||
struct WordInfo {
|
||||
QString word;
|
||||
int position;
|
||||
int length;
|
||||
};
|
||||
|
||||
Q_INVOKABLE QList<WordInfo> findWords(const QString& text);
|
||||
|
||||
private:
|
||||
void put_word(const QString& word);
|
||||
std::shared_ptr<Hunspell> hunspell_;
|
||||
QString encoding_;
|
||||
QTextCodec* codec_;
|
||||
};
|
|
@ -23,7 +23,6 @@
|
|||
#include "version.h"
|
||||
#include "version_info.h"
|
||||
#include "global.h"
|
||||
|
||||
#include <api/datatransfermodel.h>
|
||||
#include <api/contact.h>
|
||||
|
||||
|
@ -93,6 +92,11 @@ UtilsAdapter::setAppValue(const Settings::Key key, const QVariant& value)
|
|||
Q_EMIT appThemeChanged();
|
||||
else if (key == Settings::Key::UseFramelessWindow)
|
||||
Q_EMIT useFramelessWindowChanged();
|
||||
else if (key == Settings::Key::SpellLang) {
|
||||
Q_EMIT spellLanguageChanged();
|
||||
} else if (key == Settings::Key::EnableSpellCheck) {
|
||||
Q_EMIT enableSpellCheckChanged();
|
||||
}
|
||||
#if !APPSTORE
|
||||
// Any donation campaign-related keys can trigger a donation campaign check
|
||||
else if (key == Settings::Key::IsDonationVisible
|
||||
|
|
|
@ -181,6 +181,8 @@ Q_SIGNALS:
|
|||
void changeLanguage();
|
||||
void donationCampaignSettingsChanged();
|
||||
void useFramelessWindowChanged();
|
||||
void spellLanguageChanged();
|
||||
void enableSpellCheckChanged();
|
||||
|
||||
private:
|
||||
QClipboard* clipboard_;
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
*/
|
||||
|
||||
#include "appsettingsmanager.h"
|
||||
#include "spellcheckdictionarymanager.h"
|
||||
#include "connectivitymonitor.h"
|
||||
#include "mainapplication.h"
|
||||
#include "previewengine.h"
|
||||
|
@ -94,6 +95,7 @@ public Q_SLOTS:
|
|||
settingsManager_.reset(new AppSettingsManager(this));
|
||||
systemTray_.reset(new SystemTray(settingsManager_.get(), this));
|
||||
previewEngine_.reset(new PreviewEngine(connectivityMonitor_.get(), this));
|
||||
spellCheckDictionaryManager_.reset(new SpellCheckDictionaryManager(settingsManager_.get(), this));
|
||||
|
||||
QFontDatabase::addApplicationFont(":/images/FontAwesome.otf");
|
||||
|
||||
|
@ -152,6 +154,7 @@ public Q_SLOTS:
|
|||
lrcInstance_.get(),
|
||||
systemTray_.get(),
|
||||
settingsManager_.get(),
|
||||
spellCheckDictionaryManager_.get(),
|
||||
connectivityMonitor_.get(),
|
||||
previewEngine_.get(),
|
||||
&screenInfo_,
|
||||
|
@ -169,6 +172,7 @@ private:
|
|||
|
||||
QScopedPointer<ConnectivityMonitor> connectivityMonitor_;
|
||||
QScopedPointer<AppSettingsManager> settingsManager_;
|
||||
QScopedPointer<SpellCheckDictionaryManager> spellCheckDictionaryManager_;
|
||||
QScopedPointer<SystemTray> systemTray_;
|
||||
QScopedPointer<PreviewEngine> previewEngine_;
|
||||
ScreenInfo screenInfo_;
|
||||
|
|
|
@ -44,14 +44,14 @@ Item {
|
|||
|
||||
SignalSpy {
|
||||
id: spyDownloadSuccessful
|
||||
target: ImageDownloader
|
||||
signalName: "onDownloadImageSuccessful"
|
||||
target: FileDownloader
|
||||
signalName: "onDownloadFileSuccessful"
|
||||
}
|
||||
|
||||
SignalSpy {
|
||||
id: spyDownloadFailed
|
||||
target: ImageDownloader
|
||||
signalName: "onDownloadImageFailed"
|
||||
target: FileDownloader
|
||||
signalName: "onDownloadFileFailed"
|
||||
}
|
||||
|
||||
function test_goodDownLoad() {
|
||||
|
|
Loading…
Add table
Reference in a new issue