mirror of
https://git.jami.net/savoirfairelinux/jami-client-qt.git
synced 2025-10-30 07:53:33 +08:00
Compare commits
8 Commits
stable/202
...
nightly/20
| Author | SHA1 | Date | |
|---|---|---|---|
| 32b7525ee3 | |||
| 32b941ab96 | |||
| e4932abd39 | |||
| 718d1d266d | |||
| 8d55f352b4 | |||
| 2a72da564e | |||
| 88d0539085 | |||
| 4b1c299a1d |
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
Submodule 3rdparty/hunspell added at 525f9f2276
@ -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
|
||||
|
||||
@ -189,7 +189,7 @@ Only 64-bit MSVC build can be compiled.
|
||||
|
||||
- Download [Qt (Open Source)](https://www.qt.io/download-open-source?hsCtaTracking=9f6a2170-a938-42df-a8e2-a9f0b1d6cdce%7C6cb0de4f-9bb5-4778-ab02-bfb62735f3e5)
|
||||
|
||||
- Using the online installer, install the following Qt 6.6.1 components:
|
||||
- Using the online installer, install the following Qt 6.6.2 components:
|
||||
|
||||
- Git 2.10.2
|
||||
- MSVC 2019 64-bit
|
||||
@ -244,6 +244,7 @@ Only 64-bit MSVC build can be compiled.
|
||||
```bash
|
||||
python build.py --install --qt <path-to-qt-bin-folder> (e.g. C:/Qt/6.6.2/msvc2019_64)
|
||||
```
|
||||
> **CMake** Note: The build script does not specify what CMake generator should be used. This means CMake will search the system for the appropriate generator, which might not always select the right one if, for instance, Ninja is installed. To resolve that, the CMAKE_GENERATOR environment variable can be used, set to "Visual Studio 16 2019" or "Visual Studio 19 2022" depending on the installed Visual Studio version.
|
||||
|
||||
> **SDK** Note:
|
||||
> Jami can be build with more recent Windows SDK than the one specified in the table above. However, if your have another version than SDK 10.0.18362.0 installed, you need to identify it according to the example below. And you still need to have the required version in addition to the one you chose.
|
||||
|
||||
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']
|
||||
|
||||
2
daemon
2
daemon
Submodule daemon updated: 68fc552fca...c231df3e05
3
extras/packaging/gnu-linux/Jenkinsfile
vendored
3
extras/packaging/gnu-linux/Jenkinsfile
vendored
@ -34,7 +34,8 @@ def SUBMODULES = ['daemon',
|
||||
'3rdparty/SortFilterProxyModel',
|
||||
'3rdparty/md4c',
|
||||
'3rdparty/tidy-html5',
|
||||
'3rdparty/zxing-cpp']
|
||||
'3rdparty/zxing-cpp',
|
||||
'3rdparty/hunspell']
|
||||
def TARGETS = [:]
|
||||
def REMOTE_HOST = env.SSH_HOST_DL_RING_CX
|
||||
def REMOTE_BASE_DIR = '/srv/repository/ring'
|
||||
|
||||
@ -128,7 +128,8 @@ $(RELEASE_TARBALL_FILENAME): tarballs.manifest
|
||||
./3rdparty/SortFilterProxyModel \
|
||||
./3rdparty/md4c \
|
||||
./3rdparty/tidy-html5 \
|
||||
./3rdparty/zxing-cpp; do \
|
||||
./3rdparty/zxing-cpp \
|
||||
./3rdparty/hunspell; do \
|
||||
(cd "$$m" && git archive --prefix "$$m/" HEAD \
|
||||
| tar xf - -C $(TMPDIR)/$(RELEASE_DIRNAME)); \
|
||||
done
|
||||
@ -171,6 +172,7 @@ DISTRIBUTIONS := \
|
||||
fedora_39 \
|
||||
fedora_40 \
|
||||
fedora_41 \
|
||||
fedora_42 \
|
||||
alma_9 \
|
||||
opensuse-leap_15.5 \
|
||||
opensuse-leap_15.6 \
|
||||
|
||||
97
extras/packaging/gnu-linux/docker/Dockerfile_fedora_42
Normal file
97
extras/packaging/gnu-linux/docker/Dockerfile_fedora_42
Normal file
@ -0,0 +1,97 @@
|
||||
FROM fedora:42
|
||||
|
||||
RUN dnf clean all
|
||||
RUN dnf update -y
|
||||
|
||||
RUN dnf install -y dnf-command\(builddep\) rpmdevtools && \
|
||||
dnf install -y mock
|
||||
|
||||
RUN dnf group install -y x-software-development
|
||||
|
||||
RUN dnf install -y \
|
||||
git \
|
||||
make \
|
||||
autoconf \
|
||||
automake \
|
||||
nasm \
|
||||
speexdsp-devel \
|
||||
pulseaudio-libs-devel \
|
||||
libcanberra-devel \
|
||||
libcurl-devel \
|
||||
libtool \
|
||||
mesa-libgbm-devel \
|
||||
dbus-devel \
|
||||
expat-devel \
|
||||
pcre-devel \
|
||||
yaml-cpp-devel \
|
||||
yasm \
|
||||
speex-devel \
|
||||
gsm-devel \
|
||||
chrpath \
|
||||
check \
|
||||
astyle \
|
||||
uuid-c++-devel \
|
||||
gettext-devel \
|
||||
gcc14 \
|
||||
gcc14-c++ \
|
||||
which \
|
||||
alsa-lib-devel \
|
||||
systemd-devel \
|
||||
uuid-devel \
|
||||
gnutls-devel \
|
||||
nettle-devel \
|
||||
opus-devel \
|
||||
jsoncpp-devel \
|
||||
libnatpmp-devel \
|
||||
webkitgtk4-devel \
|
||||
cryptopp-devel \
|
||||
libva-devel \
|
||||
libvdpau-devel \
|
||||
msgpack-devel \
|
||||
NetworkManager-libnm-devel \
|
||||
openssl-devel \
|
||||
clutter-devel \
|
||||
clutter-gtk-devel \
|
||||
libappindicator-gtk3-devel \
|
||||
libnotify-devel \
|
||||
libupnp-devel \
|
||||
qrencode-devel \
|
||||
libargon2-devel \
|
||||
libsndfile-devel \
|
||||
gperf \
|
||||
bison \
|
||||
clang18-devel \
|
||||
llvm18-devel \
|
||||
nodejs \
|
||||
flex \
|
||||
gstreamer1-plugins-base-devel \
|
||||
gstreamer1-plugins-good \
|
||||
gstreamer1-plugins-bad-free-devel \
|
||||
nss-devel \
|
||||
libxcb* \
|
||||
libxkb* \
|
||||
vulkan-devel \
|
||||
xcb-util-* \
|
||||
wget \
|
||||
libstdc++-static \
|
||||
sqlite-devel \
|
||||
perl-generators \
|
||||
perl-English \
|
||||
libxshmfence-devel \
|
||||
ninja-build \
|
||||
cmake \
|
||||
fmt-devel \
|
||||
python3.10 \
|
||||
cups-devel \
|
||||
pipewire-devel
|
||||
|
||||
# Use GCC 14 instead of GCC 15 (the default on Fedora 42)
|
||||
# because Qt 6.6.3 fails to build when using the latter.
|
||||
RUN rm /usr/bin/gcc /usr/bin/g++ /usr/bin/c++ && \
|
||||
ln -s /usr/bin/gcc-14 /usr/bin/gcc && \
|
||||
ln -s /usr/bin/g++-14 /usr/bin/g++ && \
|
||||
ln -s /usr/bin/g++-14 /usr/bin/c++
|
||||
|
||||
ADD extras/packaging/gnu-linux/scripts/build-package-rpm.sh /opt/build-package-rpm.sh
|
||||
|
||||
CMD ["/opt/build-package-rpm.sh"]
|
||||
@ -107,6 +107,8 @@ if [ ! -f "${RPM_PATH}" ]; then
|
||||
cp /root/rpmbuild/RPMS/x86_64/jami-libqt-$QT_MAJOR_MINOR_PATCH-*.fc40.x86_64.rpm "${RPM_PATH}"
|
||||
elif [[ "${DISTRIBUTION}" == "fedora_41" ]]; then
|
||||
cp /root/rpmbuild/RPMS/x86_64/jami-libqt-$QT_MAJOR_MINOR_PATCH-*.fc41.x86_64.rpm "${RPM_PATH}"
|
||||
elif [[ "${DISTRIBUTION}" == "fedora_42" ]]; then
|
||||
cp /root/rpmbuild/RPMS/x86_64/jami-libqt-$QT_MAJOR_MINOR_PATCH-*.fc42.x86_64.rpm "${RPM_PATH}"
|
||||
elif [[ "${DISTRIBUTION}" == "alma_9" ]]; then
|
||||
cp /root/rpmbuild/RPMS/x86_64/jami-libqt-$QT_MAJOR_MINOR_PATCH-*.el9.x86_64.rpm "${RPM_PATH}"
|
||||
else
|
||||
|
||||
@ -212,6 +212,7 @@ def init_submodules():
|
||||
"3rdparty/md4c",
|
||||
"3rdparty/tidy-html5",
|
||||
"3rdparty/zxing-cpp",
|
||||
"3rdparty/hunspell",
|
||||
]
|
||||
if execute_cmd(["git", "submodule", "update", "--init" ] + submodules,
|
||||
False):
|
||||
|
||||
@ -37,6 +37,7 @@ ApplicationWindow {
|
||||
property bool isRTL: UtilsAdapter.isRTL
|
||||
LayoutMirroring.enabled: isRTL
|
||||
LayoutMirroring.childrenInherit: isRTL
|
||||
property var raiseWhenCalled: AppSettingsManager.getValue(Settings.RaiseWhenCalled)
|
||||
|
||||
onActiveFocusItemChanged: {
|
||||
focusOverlay.margin = -5;
|
||||
@ -291,6 +292,26 @@ ApplicationWindow {
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: UtilsAdapter
|
||||
function onRaiseWhenCalled() {
|
||||
raiseWhenCalled = AppSettingsManager.getValue(Settings.RaiseWhenCalled);
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: CallAdapter
|
||||
|
||||
function onCallStatusChanged(index, accountId, convUid) {
|
||||
//If we are starting a call with raiseWhenCalled activated
|
||||
if (raiseWhenCalled && index === Call.Status.INCOMING_RINGING) {
|
||||
appWindow.raise();
|
||||
appWindow.requestActivate();
|
||||
layoutManager.restoreApp();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: MainApplication
|
||||
|
||||
|
||||
@ -34,8 +34,12 @@ AppSettingsManager::AppSettingsManager(QObject* parent)
|
||||
{
|
||||
for (int i = 0; i < static_cast<int>(Settings::Key::COUNT__); ++i) {
|
||||
auto key = static_cast<Settings::Key>(i);
|
||||
if (!settings_->contains(Settings::toString(key)))
|
||||
setValue(key, Settings::defaultValue(key));
|
||||
auto strKey= Settings::toString(key);
|
||||
// If the setting is written in the settings file and is equal to the default value,
|
||||
// remove it from the settings file.
|
||||
// This allow us to change default values without risking to remove user settings
|
||||
if ((settings_->contains(strKey)) && (settings_->value(strKey) == Settings::defaultValue(key)))
|
||||
settings_->remove(strKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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) \
|
||||
@ -74,7 +76,8 @@ extern const QString defaultDownloadPath;
|
||||
X(PttKeys, 32) \
|
||||
X(UseFramelessWindow, USE_FRAMELESS_WINDOW_DEFAULT) \
|
||||
X(EnableCrashReporting, true) \
|
||||
X(EnableAutomaticCrashReporting, false)
|
||||
X(EnableAutomaticCrashReporting, false) \
|
||||
X(RaiseWhenCalled, false)
|
||||
#if APPSTORE
|
||||
#define KEYS COMMON_KEYS
|
||||
#else
|
||||
|
||||
@ -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};
|
||||
};
|
||||
|
||||
@ -110,6 +110,7 @@ Item {
|
||||
property string enablePTT: qsTr("Enable push-to-talk")
|
||||
property string keyboardShortcut: qsTr("Keyboard shortcut")
|
||||
property string changeKeyboardShortcut: qsTr("Change keyboard shortcut")
|
||||
property string raiseWhenCalled: qsTr("Send the application to the front for incoming calls")
|
||||
|
||||
// ChangePttKeyPopup
|
||||
property string changeShortcut: qsTr("Change shortcut")
|
||||
@ -275,6 +276,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 +510,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 +907,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,
|
||||
|
||||
@ -95,6 +95,14 @@ SettingsPageBase {
|
||||
checked: CurrentAccount.autoAnswer
|
||||
onSwitchToggled: CurrentAccount.autoAnswer = checked
|
||||
}
|
||||
|
||||
ToggleSwitch {
|
||||
id: checkBoxRaiseWhenCalled
|
||||
|
||||
labelText: JamiStrings.raiseWhenCalled
|
||||
checked: UtilsAdapter.getAppValue(Settings.RaiseWhenCalled)
|
||||
onSwitchToggled: UtilsAdapter.setAppValue(Settings.Key.RaiseWhenCalled, checked)
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
|
||||
@ -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,12 @@ 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();
|
||||
else if (key == Settings::Key::RaiseWhenCalled)
|
||||
Q_EMIT raiseWhenCalledChanged();
|
||||
#if !APPSTORE
|
||||
// Any donation campaign-related keys can trigger a donation campaign check
|
||||
else if (key == Settings::Key::IsDonationVisible
|
||||
|
||||
@ -181,6 +181,9 @@ Q_SIGNALS:
|
||||
void changeLanguage();
|
||||
void donationCampaignSettingsChanged();
|
||||
void useFramelessWindowChanged();
|
||||
void spellLanguageChanged();
|
||||
void enableSpellCheckChanged();
|
||||
void raiseWhenCalledChanged();
|
||||
|
||||
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() {
|
||||
|
||||
Reference in New Issue
Block a user