mirror of
https://git.jami.net/savoirfairelinux/jami-client-qt.git
synced 2025-07-01 14:15:24 +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
|
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
|
||||||
|
|
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
|
# 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
|
||||||
${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
|
||||||
${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
|
||||||
${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
|
||||||
${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
|
||||||
${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)
|
||||||
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)
|
||||||
${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
|
||||||
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(
|
||||||
${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)
|
||||||
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
|
||||||
|
|
8
build.py
8
build.py
|
@ -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 = [
|
||||||
'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 = [
|
||||||
'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 = [
|
||||||
'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;
|
||||||
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 @@
|
||||||
* 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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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
|
||||||
|
|
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) {
|
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 {
|
||||||
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 {
|
||||||
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 @@
|
||||||
* 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&
|
||||||
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&
|
||||||
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 @@
|
||||||
#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
|
||||||
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 @@
|
||||||
#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()
|
||||||
// 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()
|
||||||
lrcInstance_.get(),
|
lrcInstance_.get(),
|
||||||
systemTray_,
|
systemTray_,
|
||||||
settingsManager_,
|
settingsManager_,
|
||||||
|
spellCheckDictionaryManager_,
|
||||||
connectivityMonitor_,
|
connectivityMonitor_,
|
||||||
previewEngine_,
|
previewEngine_,
|
||||||
&screenInfo_,
|
&screenInfo_,
|
||||||
|
|
|
@ -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:
|
||||||
ConnectivityMonitor* connectivityMonitor_;
|
ConnectivityMonitor* connectivityMonitor_;
|
||||||
SystemTray* systemTray_;
|
SystemTray* systemTray_;
|
||||||
AppSettingsManager* settingsManager_;
|
AppSettingsManager* settingsManager_;
|
||||||
|
SpellCheckDictionaryManager* spellCheckDictionaryManager_;
|
||||||
PreviewEngine* previewEngine_;
|
PreviewEngine* previewEngine_;
|
||||||
CrashReporter* crashReporter_;
|
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 {
|
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 {
|
||||||
}
|
}
|
||||||
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,23 +116,23 @@ Rectangle {
|
||||||
|
|
||||||
spacing: 0
|
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 {
|
ElidedTextLabel {
|
||||||
id: title
|
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
|
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
|
||||||
|
|
||||||
font.pointSize: JamiTheme.textFontSize + 2
|
font.pointSize: JamiTheme.textFontSize + 2
|
||||||
|
|
|
@ -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 {
|
||||||
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 {
|
||||||
|
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
// 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 {
|
||||||
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 @@
|
||||||
#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 @@
|
||||||
#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
|
||||||
// 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 @@
|
||||||
#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:
|
||||||
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:
|
||||||
{
|
{
|
||||||
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:
|
||||||
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:
|
||||||
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};
|
||||||
};
|
};
|
||||||
|
|
|
@ -275,6 +275,7 @@ Item {
|
||||||
property string share: qsTr("Share")
|
property string share: qsTr("Share")
|
||||||
property string cut: qsTr("Cut")
|
property string cut: qsTr("Cut")
|
||||||
property string paste: qsTr("Paste")
|
property string paste: qsTr("Paste")
|
||||||
|
property string language: qsTr("Language")
|
||||||
|
|
||||||
// ConversationContextMenu
|
// ConversationContextMenu
|
||||||
property string startAudioCall: qsTr("Start audio call")
|
property string startAudioCall: qsTr("Start audio call")
|
||||||
|
@ -508,7 +509,7 @@ Item {
|
||||||
property string displayHyperlinkPreviews: qsTr("Web link previews")
|
property string displayHyperlinkPreviews: qsTr("Web link previews")
|
||||||
property string displayHyperlinkPreviewsDescription: qsTr("Preview requires downloading content from third-party servers.")
|
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 verticalViewOpt: qsTr("Vertical view")
|
||||||
property string horizontalViewOpt: qsTr("Horizontal view")
|
property string horizontalViewOpt: qsTr("Horizontal view")
|
||||||
|
|
||||||
|
@ -905,4 +906,12 @@ Item {
|
||||||
property string copyAllData: qsTr("Copy all data")
|
property string copyAllData: qsTr("Copy all data")
|
||||||
property string remote: qsTr("Remote: %1")
|
property string remote: qsTr("Remote: %1")
|
||||||
property string view: qsTr("View")
|
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 showTypoSecondToggleWidth: 540
|
||||||
property int messageBarMaximumHeight: 150
|
property int messageBarMaximumHeight: 150
|
||||||
property int messageBarMinimumHeight: 36
|
property int messageBarMinimumHeight: 36
|
||||||
|
property int messageUnderlineHeight: 2
|
||||||
|
|
||||||
// InvitationView
|
// InvitationView
|
||||||
property real invitationViewAvatarSize: 112
|
property real invitationViewAvatarSize: 112
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
#include "positionmanager.h"
|
#include "positionmanager.h"
|
||||||
#include "tipsmodel.h"
|
#include "tipsmodel.h"
|
||||||
#include "connectivitymonitor.h"
|
#include "connectivitymonitor.h"
|
||||||
#include "imagedownloader.h"
|
#include "filedownloader.h"
|
||||||
#include "utilsadapter.h"
|
#include "utilsadapter.h"
|
||||||
#include "conversationsadapter.h"
|
#include "conversationsadapter.h"
|
||||||
#include "currentcall.h"
|
#include "currentcall.h"
|
||||||
|
@ -36,6 +36,7 @@
|
||||||
#include "currentaccounttomigrate.h"
|
#include "currentaccounttomigrate.h"
|
||||||
#include "pttlistener.h"
|
#include "pttlistener.h"
|
||||||
#include "calloverlaymodel.h"
|
#include "calloverlaymodel.h"
|
||||||
|
#include "spellcheckdictionarymanager.h"
|
||||||
#include "accountlistmodel.h"
|
#include "accountlistmodel.h"
|
||||||
#include "mediacodeclistmodel.h"
|
#include "mediacodeclistmodel.h"
|
||||||
#include "audiodevicemodel.h"
|
#include "audiodevicemodel.h"
|
||||||
|
@ -64,6 +65,7 @@
|
||||||
#include "wizardviewstepmodel.h"
|
#include "wizardviewstepmodel.h"
|
||||||
#include "linkdevicemodel.h"
|
#include "linkdevicemodel.h"
|
||||||
#include "qrcodescannermodel.h"
|
#include "qrcodescannermodel.h"
|
||||||
|
#include "spellchecker.h"
|
||||||
|
|
||||||
#include "api/peerdiscoverymodel.h"
|
#include "api/peerdiscoverymodel.h"
|
||||||
#include "api/codecmodel.h"
|
#include "api/codecmodel.h"
|
||||||
|
@ -117,6 +119,7 @@ registerTypes(QQmlEngine* engine,
|
||||||
LRCInstance* lrcInstance,
|
LRCInstance* lrcInstance,
|
||||||
SystemTray* systemTray,
|
SystemTray* systemTray,
|
||||||
AppSettingsManager* settingsManager,
|
AppSettingsManager* settingsManager,
|
||||||
|
SpellCheckDictionaryManager* spellCheckDictionaryManager,
|
||||||
ConnectivityMonitor* connectivityMonitor,
|
ConnectivityMonitor* connectivityMonitor,
|
||||||
PreviewEngine* previewEngine,
|
PreviewEngine* previewEngine,
|
||||||
ScreenInfo* screenInfo,
|
ScreenInfo* screenInfo,
|
||||||
|
@ -201,6 +204,7 @@ registerTypes(QQmlEngine* engine,
|
||||||
qApp->setProperty("AppSettingsManager", QVariant::fromValue(settingsManager));
|
qApp->setProperty("AppSettingsManager", QVariant::fromValue(settingsManager));
|
||||||
qApp->setProperty("ConnectivityMonitor", QVariant::fromValue(connectivityMonitor));
|
qApp->setProperty("ConnectivityMonitor", QVariant::fromValue(connectivityMonitor));
|
||||||
qApp->setProperty("PreviewEngine", QVariant::fromValue(previewEngine));
|
qApp->setProperty("PreviewEngine", QVariant::fromValue(previewEngine));
|
||||||
|
qApp->setProperty("SpellCheckDictionaryManager", QVariant::fromValue(spellCheckDictionaryManager));
|
||||||
|
|
||||||
// qml adapter registration
|
// qml adapter registration
|
||||||
QML_REGISTERSINGLETON_TYPE(NS_HELPERS, QRCodeScannerModel);
|
QML_REGISTERSINGLETON_TYPE(NS_HELPERS, QRCodeScannerModel);
|
||||||
|
@ -220,7 +224,7 @@ registerTypes(QQmlEngine* engine,
|
||||||
QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, TipsModel);
|
QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, TipsModel);
|
||||||
QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, VideoDevices);
|
QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, VideoDevices);
|
||||||
QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, CurrentAccountToMigrate);
|
QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, CurrentAccountToMigrate);
|
||||||
QML_REGISTERSINGLETON_TYPE(NS_HELPERS, ImageDownloader);
|
QML_REGISTERSINGLETON_TYPE(NS_HELPERS, FileDownloader);
|
||||||
|
|
||||||
// TODO: remove these
|
// TODO: remove these
|
||||||
QML_REGISTERSINGLETONTYPE_CUSTOM(NS_MODELS, AVModel, &lrcInstance->avModel())
|
QML_REGISTERSINGLETONTYPE_CUSTOM(NS_MODELS, AVModel, &lrcInstance->avModel())
|
||||||
|
@ -237,6 +241,7 @@ registerTypes(QQmlEngine* engine,
|
||||||
QML_REGISTERTYPE(NS_MODELS, PluginListPreferenceModel);
|
QML_REGISTERTYPE(NS_MODELS, PluginListPreferenceModel);
|
||||||
QML_REGISTERTYPE(NS_MODELS, FilesToSendListModel);
|
QML_REGISTERTYPE(NS_MODELS, FilesToSendListModel);
|
||||||
QML_REGISTERTYPE(NS_MODELS, CallInformationListModel);
|
QML_REGISTERTYPE(NS_MODELS, CallInformationListModel);
|
||||||
|
QML_REGISTERTYPE(NS_MODELS, SpellChecker);
|
||||||
|
|
||||||
// Roles & type enums for models
|
// Roles & type enums for models
|
||||||
QML_REGISTERNAMESPACE(NS_MODELS, AccountList::staticMetaObject, "AccountList");
|
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, screenInfo, "CurrentScreenInfo")
|
||||||
QML_REGISTERSINGLETONTYPE_POBJECT(NS_CONSTANTS, lrcInstance, "LRCInstance")
|
QML_REGISTERSINGLETONTYPE_POBJECT(NS_CONSTANTS, lrcInstance, "LRCInstance")
|
||||||
QML_REGISTERSINGLETONTYPE_POBJECT(NS_CONSTANTS, settingsManager, "AppSettingsManager")
|
QML_REGISTERSINGLETONTYPE_POBJECT(NS_CONSTANTS, settingsManager, "AppSettingsManager")
|
||||||
|
QML_REGISTERSINGLETONTYPE_POBJECT(NS_CONSTANTS, spellCheckDictionaryManager, "SpellCheckDictionaryManager")
|
||||||
|
|
||||||
// Lrc namespaces, models, and singletons
|
// Lrc namespaces, models, and singletons
|
||||||
QML_REGISTERNAMESPACE(NS_MODELS, lrc::api::staticMetaObject, "Lrc");
|
QML_REGISTERNAMESPACE(NS_MODELS, lrc::api::staticMetaObject, "Lrc");
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
class SystemTray;
|
class SystemTray;
|
||||||
class LRCInstance;
|
class LRCInstance;
|
||||||
class AppSettingsManager;
|
class AppSettingsManager;
|
||||||
|
class SpellCheckDictionaryManager;
|
||||||
class PreviewEngine;
|
class PreviewEngine;
|
||||||
class ScreenInfo;
|
class ScreenInfo;
|
||||||
class MainApplication;
|
class MainApplication;
|
||||||
|
@ -61,6 +62,7 @@ void registerTypes(QQmlEngine* engine,
|
||||||
LRCInstance* lrcInstance,
|
LRCInstance* lrcInstance,
|
||||||
SystemTray* systemTray,
|
SystemTray* systemTray,
|
||||||
AppSettingsManager* appSettingsManager,
|
AppSettingsManager* appSettingsManager,
|
||||||
|
SpellCheckDictionaryManager* spellCheckDictionaryManager,
|
||||||
ConnectivityMonitor* connectivityMonitor,
|
ConnectivityMonitor* connectivityMonitor,
|
||||||
PreviewEngine* previewEngine,
|
PreviewEngine* previewEngine,
|
||||||
ScreenInfo* screenInfo,
|
ScreenInfo* screenInfo,
|
||||||
|
|
|
@ -38,7 +38,6 @@ SettingsPageBase {
|
||||||
flickableContent: ColumnLayout {
|
flickableContent: ColumnLayout {
|
||||||
id: manageAccountColumnLayout
|
id: manageAccountColumnLayout
|
||||||
|
|
||||||
width: contentFlickableWidth
|
|
||||||
spacing: JamiTheme.settingsBlockSpacing
|
spacing: JamiTheme.settingsBlockSpacing
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.leftMargin: JamiTheme.preferredSettingsMarginSize
|
anchors.leftMargin: JamiTheme.preferredSettingsMarginSize
|
||||||
|
|
|
@ -23,6 +23,7 @@ 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 "../../commoncomponents"
|
import "../../commoncomponents"
|
||||||
|
import SortFilterProxyModel 0.2
|
||||||
|
|
||||||
SettingsPageBase {
|
SettingsPageBase {
|
||||||
id: root
|
id: root
|
||||||
|
@ -157,8 +158,8 @@ SettingsPageBase {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
height: JamiTheme.preferredFieldHeight
|
height: JamiTheme.preferredFieldHeight
|
||||||
|
|
||||||
labelText: JamiStrings.language
|
labelText: JamiStrings.userInterfaceLanguage
|
||||||
tipText: JamiStrings.language
|
tipText: JamiStrings.userInterfaceLanguage
|
||||||
comboModel: ListModel {
|
comboModel: ListModel {
|
||||||
id: langModel
|
id: langModel
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
|
@ -183,6 +184,132 @@ SettingsPageBase {
|
||||||
UtilsAdapter.setAppValue(Settings.Key.LANG, comboModel.get(modelIndex).id);
|
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 {
|
Connections {
|
||||||
target: UtilsAdapter
|
target: UtilsAdapter
|
||||||
|
@ -200,6 +327,21 @@ SettingsPageBase {
|
||||||
}
|
}
|
||||||
langComboBoxSetting.modelIndex = langIdx;
|
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);
|
closeOrMinCheckBox.checked = UtilsAdapter.getDefault(Settings.Key.MinimizeOnClose);
|
||||||
checkboxCallSwarm.checked = UtilsAdapter.getDefault(Settings.Key.EnableExperimentalSwarm);
|
checkboxCallSwarm.checked = UtilsAdapter.getDefault(Settings.Key.EnableExperimentalSwarm);
|
||||||
langComboBoxSetting.modelIndex = 0;
|
langComboBoxSetting.modelIndex = 0;
|
||||||
|
spellCheckLangComboBoxSetting.modelIndex = 0;
|
||||||
UtilsAdapter.setToDefault(Settings.Key.EnableNotifications);
|
UtilsAdapter.setToDefault(Settings.Key.EnableNotifications);
|
||||||
UtilsAdapter.setToDefault(Settings.Key.MinimizeOnClose);
|
UtilsAdapter.setToDefault(Settings.Key.MinimizeOnClose);
|
||||||
UtilsAdapter.setToDefault(Settings.Key.LANG);
|
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.h"
|
||||||
#include "version_info.h"
|
#include "version_info.h"
|
||||||
#include "global.h"
|
#include "global.h"
|
||||||
|
|
||||||
#include <api/datatransfermodel.h>
|
#include <api/datatransfermodel.h>
|
||||||
#include <api/contact.h>
|
#include <api/contact.h>
|
||||||
|
|
||||||
|
@ -93,6 +92,11 @@ UtilsAdapter::setAppValue(const Settings::Key key, const QVariant& value)
|
||||||
Q_EMIT appThemeChanged();
|
Q_EMIT appThemeChanged();
|
||||||
else if (key == Settings::Key::UseFramelessWindow)
|
else if (key == Settings::Key::UseFramelessWindow)
|
||||||
Q_EMIT useFramelessWindowChanged();
|
Q_EMIT useFramelessWindowChanged();
|
||||||
|
else if (key == Settings::Key::SpellLang) {
|
||||||
|
Q_EMIT spellLanguageChanged();
|
||||||
|
} else if (key == Settings::Key::EnableSpellCheck) {
|
||||||
|
Q_EMIT enableSpellCheckChanged();
|
||||||
|
}
|
||||||
#if !APPSTORE
|
#if !APPSTORE
|
||||||
// Any donation campaign-related keys can trigger a donation campaign check
|
// Any donation campaign-related keys can trigger a donation campaign check
|
||||||
else if (key == Settings::Key::IsDonationVisible
|
else if (key == Settings::Key::IsDonationVisible
|
||||||
|
|
|
@ -181,6 +181,8 @@ Q_SIGNALS:
|
||||||
void changeLanguage();
|
void changeLanguage();
|
||||||
void donationCampaignSettingsChanged();
|
void donationCampaignSettingsChanged();
|
||||||
void useFramelessWindowChanged();
|
void useFramelessWindowChanged();
|
||||||
|
void spellLanguageChanged();
|
||||||
|
void enableSpellCheckChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QClipboard* clipboard_;
|
QClipboard* clipboard_;
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "appsettingsmanager.h"
|
#include "appsettingsmanager.h"
|
||||||
|
#include "spellcheckdictionarymanager.h"
|
||||||
#include "connectivitymonitor.h"
|
#include "connectivitymonitor.h"
|
||||||
#include "mainapplication.h"
|
#include "mainapplication.h"
|
||||||
#include "previewengine.h"
|
#include "previewengine.h"
|
||||||
|
@ -94,6 +95,7 @@ public Q_SLOTS:
|
||||||
settingsManager_.reset(new AppSettingsManager(this));
|
settingsManager_.reset(new AppSettingsManager(this));
|
||||||
systemTray_.reset(new SystemTray(settingsManager_.get(), this));
|
systemTray_.reset(new SystemTray(settingsManager_.get(), this));
|
||||||
previewEngine_.reset(new PreviewEngine(connectivityMonitor_.get(), this));
|
previewEngine_.reset(new PreviewEngine(connectivityMonitor_.get(), this));
|
||||||
|
spellCheckDictionaryManager_.reset(new SpellCheckDictionaryManager(settingsManager_.get(), this));
|
||||||
|
|
||||||
QFontDatabase::addApplicationFont(":/images/FontAwesome.otf");
|
QFontDatabase::addApplicationFont(":/images/FontAwesome.otf");
|
||||||
|
|
||||||
|
@ -152,6 +154,7 @@ public Q_SLOTS:
|
||||||
lrcInstance_.get(),
|
lrcInstance_.get(),
|
||||||
systemTray_.get(),
|
systemTray_.get(),
|
||||||
settingsManager_.get(),
|
settingsManager_.get(),
|
||||||
|
spellCheckDictionaryManager_.get(),
|
||||||
connectivityMonitor_.get(),
|
connectivityMonitor_.get(),
|
||||||
previewEngine_.get(),
|
previewEngine_.get(),
|
||||||
&screenInfo_,
|
&screenInfo_,
|
||||||
|
@ -169,6 +172,7 @@ private:
|
||||||
|
|
||||||
QScopedPointer<ConnectivityMonitor> connectivityMonitor_;
|
QScopedPointer<ConnectivityMonitor> connectivityMonitor_;
|
||||||
QScopedPointer<AppSettingsManager> settingsManager_;
|
QScopedPointer<AppSettingsManager> settingsManager_;
|
||||||
|
QScopedPointer<SpellCheckDictionaryManager> spellCheckDictionaryManager_;
|
||||||
QScopedPointer<SystemTray> systemTray_;
|
QScopedPointer<SystemTray> systemTray_;
|
||||||
QScopedPointer<PreviewEngine> previewEngine_;
|
QScopedPointer<PreviewEngine> previewEngine_;
|
||||||
ScreenInfo screenInfo_;
|
ScreenInfo screenInfo_;
|
||||||
|
|
|
@ -44,14 +44,14 @@ Item {
|
||||||
|
|
||||||
SignalSpy {
|
SignalSpy {
|
||||||
id: spyDownloadSuccessful
|
id: spyDownloadSuccessful
|
||||||
target: ImageDownloader
|
target: FileDownloader
|
||||||
signalName: "onDownloadImageSuccessful"
|
signalName: "onDownloadFileSuccessful"
|
||||||
}
|
}
|
||||||
|
|
||||||
SignalSpy {
|
SignalSpy {
|
||||||
id: spyDownloadFailed
|
id: spyDownloadFailed
|
||||||
target: ImageDownloader
|
target: FileDownloader
|
||||||
signalName: "onDownloadImageFailed"
|
signalName: "onDownloadFileFailed"
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_goodDownLoad() {
|
function test_goodDownLoad() {
|
||||||
|
|
Loading…
Add table
Reference in a new issue