mirror of
https://git.jami.net/savoirfairelinux/jami-client-qt.git
synced 2025-07-01 06:05:25 +02:00
spellcheck: windows and macos
Implement the hunspell spellchecker for Windows and MacOS. It also changes the base implementation for Linux. The system dictionaries (if any) are aggregated with those installed from the LibreOffice repository via Jami's dictionary management interface. This commit implements a major refactoring of the spellcheck system to improve UI responsiveness and user experience: Core Changes: - Used QAbstractListModel to represent the list of dictionaries - Added new QML components: - DictionaryInstallView.qml - ManageDictionariesDialog.qml - SpellCheckLanguageComboBox.qml - Updated property names for clarity - Fixed a bug in the settings combo box custom component that caused out-of-range errors for filtered models GitLab: #1997 Change-Id: Ibd0879f957f27f4c7c5720762ace553ca84e2bc3
This commit is contained in:
parent
1ac3db4f33
commit
ceec1f95b9
36 changed files with 1886 additions and 595 deletions
2
3rdparty/hunspell
vendored
2
3rdparty/hunspell
vendored
|
@ -1 +1 @@
|
|||
Subproject commit 525f9f22766a28e0f81c435217fcf4528e01c013
|
||||
Subproject commit 749cd84a0b7863fd8663b85c65abd8eca55d342d
|
|
@ -347,7 +347,8 @@ 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}/spellcheckdictionarylistmodel.cpp
|
||||
${APP_SRC_DIR}/spellcheckadapter.cpp
|
||||
${APP_SRC_DIR}/filestosendlistmodel.cpp
|
||||
${APP_SRC_DIR}/wizardviewstepmodel.cpp
|
||||
${APP_SRC_DIR}/avatarregistry.cpp
|
||||
|
@ -420,7 +421,8 @@ 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}/spellcheckdictionarylistmodel.h
|
||||
${APP_SRC_DIR}/spellcheckadapter.h
|
||||
${APP_SRC_DIR}/filestosendlistmodel.h
|
||||
${APP_SRC_DIR}/wizardviewstepmodel.h
|
||||
${APP_SRC_DIR}/avatarregistry.h
|
||||
|
@ -475,13 +477,6 @@ find_package(PkgConfig REQUIRED)
|
|||
|
||||
# hunspell
|
||||
pkg_search_module(hunspell IMPORTED_TARGET 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")
|
||||
set(HUNSPELL_LIBRARIES PkgConfig::hunspell)
|
||||
|
@ -734,7 +729,8 @@ qt_add_executable(
|
|||
${COMMON_SOURCES}
|
||||
${QML_RESOURCES}
|
||||
${QML_RESOURCES_QML}
|
||||
${SFPM_OBJECTS})
|
||||
${SFPM_OBJECTS}
|
||||
src/app/spellcheckadapter.h src/app/spellcheckadapter.cpp)
|
||||
|
||||
#add_dependencies(${PROJECT_NAME} hunspell)
|
||||
|
||||
|
|
382
resources/misc/available_dictionaries.json
Normal file
382
resources/misc/available_dictionaries.json
Normal file
|
@ -0,0 +1,382 @@
|
|||
{
|
||||
"af_ZA": {
|
||||
"nativeName": "Afrikaans (Suid-Afrika)",
|
||||
"path": "af_ZA/af_ZA"
|
||||
},
|
||||
"an_ES": {
|
||||
"nativeName": "aragonés (España)",
|
||||
"path": "an_ES/an_ES"
|
||||
},
|
||||
"ar": {
|
||||
"nativeName": "العربية",
|
||||
"path": "ar/ar"
|
||||
},
|
||||
"as_IN": {
|
||||
"nativeName": "অসমীয়া (भारत)",
|
||||
"path": "as_IN/as_IN"
|
||||
},
|
||||
"be-official": {
|
||||
"nativeName": "беларуская",
|
||||
"path": "be_BY/be-official"
|
||||
},
|
||||
"bg_BG": {
|
||||
"nativeName": "български",
|
||||
"path": "bg_BG/bg_BG"
|
||||
},
|
||||
"bn_BD": {
|
||||
"nativeName": "বাঙ্গালি (বাংলাদেশ)",
|
||||
"path": "bn_BD/bn_BD"
|
||||
},
|
||||
"bn_IN": {
|
||||
"nativeName": "বাঙ্গালি (ভারত)",
|
||||
"path": "bn_BD/bn_IN"
|
||||
},
|
||||
"bo": {
|
||||
"nativeName": "བོད་སྐད་",
|
||||
"path": "bo/bo"
|
||||
},
|
||||
"br_FR": {
|
||||
"nativeName": "breton (France)",
|
||||
"path": "br_FR/br_FR"
|
||||
},
|
||||
"bs_BA": {
|
||||
"nativeName": "bosanski",
|
||||
"path": "bs_BA/bs_BA"
|
||||
},
|
||||
"cs_CZ": {
|
||||
"nativeName": "čeština",
|
||||
"path": "cs_CZ/cs_CZ"
|
||||
},
|
||||
"da_DK": {
|
||||
"nativeName": "dansk",
|
||||
"path": "da_DK/da_DK"
|
||||
},
|
||||
"de_AT_frami": {
|
||||
"nativeName": "Deutsch (Österreich)",
|
||||
"path": "de/de_AT_frami"
|
||||
},
|
||||
"de_CH_frami": {
|
||||
"nativeName": "Deutsch (Schweiz)",
|
||||
"path": "de/de_CH_frami"
|
||||
},
|
||||
"de_DE_frami": {
|
||||
"nativeName": "Deutsch (Deutschland)",
|
||||
"path": "de/de_DE_frami"
|
||||
},
|
||||
"el_GR": {
|
||||
"nativeName": "Ελληνικά",
|
||||
"path": "el_GR/el_GR"
|
||||
},
|
||||
"en_AU": {
|
||||
"nativeName": "English (Australia)",
|
||||
"path": "en/en_AU"
|
||||
},
|
||||
"en_CA": {
|
||||
"nativeName": "English (Canada)",
|
||||
"path": "en/en_CA"
|
||||
},
|
||||
"en_GB": {
|
||||
"nativeName": "English (British)",
|
||||
"path": "en/en_GB"
|
||||
},
|
||||
"en_US": {
|
||||
"nativeName": "English (American)",
|
||||
"path": "en/en_US"
|
||||
},
|
||||
"en_ZA": {
|
||||
"nativeName": "English (Suid-Afrika)",
|
||||
"path": "en/en_ZA"
|
||||
},
|
||||
"eo": {
|
||||
"nativeName": "esperanto",
|
||||
"path": "eo/eo"
|
||||
},
|
||||
"es_AR": {
|
||||
"nativeName": "español (Argentina)",
|
||||
"path": "es/es_AR"
|
||||
},
|
||||
"es_BO": {
|
||||
"nativeName": "español (Bolivia)",
|
||||
"path": "es/es_BO"
|
||||
},
|
||||
"es_CL": {
|
||||
"nativeName": "español (Chile)",
|
||||
"path": "es/es_CL"
|
||||
},
|
||||
"es_CO": {
|
||||
"nativeName": "español (Colombia)",
|
||||
"path": "es/es_CO"
|
||||
},
|
||||
"es_CR": {
|
||||
"nativeName": "español (Costa Rica)",
|
||||
"path": "es/es_CR"
|
||||
},
|
||||
"es_CU": {
|
||||
"nativeName": "español (Cuba)",
|
||||
"path": "es/es_CU"
|
||||
},
|
||||
"es_DO": {
|
||||
"nativeName": "español (República Dominicana)",
|
||||
"path": "es/es_DO"
|
||||
},
|
||||
"es_EC": {
|
||||
"nativeName": "español (Ecuador)",
|
||||
"path": "es/es_EC"
|
||||
},
|
||||
"es_ES": {
|
||||
"nativeName": "español (España)",
|
||||
"path": "es/es_ES"
|
||||
},
|
||||
"es_GQ": {
|
||||
"nativeName": "español (Guinea Ecuatorial)",
|
||||
"path": "es/es_GQ"
|
||||
},
|
||||
"es_GT": {
|
||||
"nativeName": "español (Guatemala)",
|
||||
"path": "es/es_GT"
|
||||
},
|
||||
"es_HN": {
|
||||
"nativeName": "español (Honduras)",
|
||||
"path": "es/es_HN"
|
||||
},
|
||||
"es_MX": {
|
||||
"nativeName": "español (México)",
|
||||
"path": "es/es_MX"
|
||||
},
|
||||
"es_NI": {
|
||||
"nativeName": "español (Nicaragua)",
|
||||
"path": "es/es_NI"
|
||||
},
|
||||
"es_PA": {
|
||||
"nativeName": "español (Panamá)",
|
||||
"path": "es/es_PA"
|
||||
},
|
||||
"es_PE": {
|
||||
"nativeName": "español (Perú)",
|
||||
"path": "es/es_PE"
|
||||
},
|
||||
"es_PH": {
|
||||
"nativeName": "español (Pilipinas)",
|
||||
"path": "es/es_PH"
|
||||
},
|
||||
"es_PR": {
|
||||
"nativeName": "español (Puerto Rico)",
|
||||
"path": "es/es_PR"
|
||||
},
|
||||
"es_PY": {
|
||||
"nativeName": "español (Paraguai)",
|
||||
"path": "es/es_PY"
|
||||
},
|
||||
"es_SV": {
|
||||
"nativeName": "español (El Salvador)",
|
||||
"path": "es/es_SV"
|
||||
},
|
||||
"es_US": {
|
||||
"nativeName": "español (United States)",
|
||||
"path": "es/es_US"
|
||||
},
|
||||
"es_UY": {
|
||||
"nativeName": "español (Uruguay)",
|
||||
"path": "es/es_UY"
|
||||
},
|
||||
"es_VE": {
|
||||
"nativeName": "español (Venezuela)",
|
||||
"path": "es/es_VE"
|
||||
},
|
||||
"et_EE": {
|
||||
"nativeName": "eesti",
|
||||
"path": "et_EE/et_EE"
|
||||
},
|
||||
"fa-IR": {
|
||||
"nativeName": "فارسی",
|
||||
"path": "fa_IR/fa-IR"
|
||||
},
|
||||
"fr": {
|
||||
"nativeName": "français",
|
||||
"path": "fr_FR/fr"
|
||||
},
|
||||
"gd_GB": {
|
||||
"nativeName": "Gàidhlig",
|
||||
"path": "gd_GB/gd_GB"
|
||||
},
|
||||
"gl_ES": {
|
||||
"nativeName": "galego",
|
||||
"path": "gl/gl_ES"
|
||||
},
|
||||
"gu_IN": {
|
||||
"nativeName": "ગુજરાતી",
|
||||
"path": "gu_IN/gu_IN"
|
||||
},
|
||||
"he_IL": {
|
||||
"nativeName": "עברית",
|
||||
"path": "he_IL/he_IL"
|
||||
},
|
||||
"hi_IN": {
|
||||
"nativeName": "हिन्दी",
|
||||
"path": "hi_IN/hi_IN"
|
||||
},
|
||||
"hr_HR": {
|
||||
"nativeName": "hrvatski",
|
||||
"path": "hr_HR/hr_HR"
|
||||
},
|
||||
"hu_HU": {
|
||||
"nativeName": "magyar",
|
||||
"path": "hu_HU/hu_HU"
|
||||
},
|
||||
"id_ID": {
|
||||
"nativeName": "Indonesian",
|
||||
"path": "id/id_ID"
|
||||
},
|
||||
"is": {
|
||||
"nativeName": "íslenska",
|
||||
"path": "is/is"
|
||||
},
|
||||
"it_IT": {
|
||||
"nativeName": "italiano",
|
||||
"path": "it_IT/it_IT"
|
||||
},
|
||||
"kmr_Latn": {
|
||||
"nativeName": "Northern Kurdish",
|
||||
"path": "kmr_Latn/kmr_Latn"
|
||||
},
|
||||
"kn_IN": {
|
||||
"nativeName": "ಕನ್ನಡ",
|
||||
"path": "kn_IN/kn_IN"
|
||||
},
|
||||
"ko_KR": {
|
||||
"nativeName": "한국어",
|
||||
"path": "ko_KR/ko_KR"
|
||||
},
|
||||
"lo_LA": {
|
||||
"nativeName": "ລາວ",
|
||||
"path": "lo_LA/lo_LA"
|
||||
},
|
||||
"lt": {
|
||||
"nativeName": "lietuvių",
|
||||
"path": "lt_LT/lt"
|
||||
},
|
||||
"lv_LV": {
|
||||
"nativeName": "latviešu",
|
||||
"path": "lv_LV/lv_LV"
|
||||
},
|
||||
"mn_MN": {
|
||||
"nativeName": "монгол",
|
||||
"path": "mn_MN/mn_MN"
|
||||
},
|
||||
"mr_IN": {
|
||||
"nativeName": "मराठी",
|
||||
"path": "mr_IN/mr_IN"
|
||||
},
|
||||
"nb_NO": {
|
||||
"nativeName": "norsk bokmål",
|
||||
"path": "no/nb_NO"
|
||||
},
|
||||
"ne_NP": {
|
||||
"nativeName": "नेपाली",
|
||||
"path": "ne_NP/ne_NP"
|
||||
},
|
||||
"nl_NL": {
|
||||
"nativeName": "Nederlands",
|
||||
"path": "nl_NL/nl_NL"
|
||||
},
|
||||
"nn_NO": {
|
||||
"nativeName": "norsk nynorsk",
|
||||
"path": "no/nn_NO"
|
||||
},
|
||||
"oc_FR": {
|
||||
"nativeName": "occitan",
|
||||
"path": "oc_FR/oc_FR"
|
||||
},
|
||||
"or_IN": {
|
||||
"nativeName": "ଓଡ଼ିଆ",
|
||||
"path": "or_IN/or_IN"
|
||||
},
|
||||
"pa_IN": {
|
||||
"nativeName": "ਪੰਜਾਬੀ",
|
||||
"path": "pa_IN/pa_IN"
|
||||
},
|
||||
"pl_PL": {
|
||||
"nativeName": "polski",
|
||||
"path": "pl_PL/pl_PL"
|
||||
},
|
||||
"pt_BR": {
|
||||
"nativeName": "português (Brasil)",
|
||||
"path": "pt_BR/pt_BR"
|
||||
},
|
||||
"pt_PT": {
|
||||
"nativeName": "português europeu (Portugal)",
|
||||
"path": "pt_PT/pt_PT"
|
||||
},
|
||||
"ro_RO": {
|
||||
"nativeName": "română",
|
||||
"path": "ro/ro_RO"
|
||||
},
|
||||
"ru_RU": {
|
||||
"nativeName": "русский",
|
||||
"path": "ru_RU/ru_RU"
|
||||
},
|
||||
"sa_IN": {
|
||||
"nativeName": "संस्कृत भाषा",
|
||||
"path": "sa_IN/sa_IN"
|
||||
},
|
||||
"si_LK": {
|
||||
"nativeName": "සිංහල",
|
||||
"path": "si_LK/si_LK"
|
||||
},
|
||||
"sk_SK": {
|
||||
"nativeName": "slovenčina",
|
||||
"path": "sk_SK/sk_SK"
|
||||
},
|
||||
"sl_SI": {
|
||||
"nativeName": "slovenščina",
|
||||
"path": "sl_SI/sl_SI"
|
||||
},
|
||||
"sq_AL": {
|
||||
"nativeName": "shqip",
|
||||
"path": "sq_AL/sq_AL"
|
||||
},
|
||||
"sr": {
|
||||
"nativeName": "српски",
|
||||
"path": "sr/sr"
|
||||
},
|
||||
"sr-Latn": {
|
||||
"nativeName": "srpski",
|
||||
"path": "sr/sr-Latn"
|
||||
},
|
||||
"sv_FI": {
|
||||
"nativeName": "svenska (Finland)",
|
||||
"path": "sv_SE/sv_FI"
|
||||
},
|
||||
"sv_SE": {
|
||||
"nativeName": "svenska (Sverige)",
|
||||
"path": "sv_SE/sv_SE"
|
||||
},
|
||||
"sw_TZ": {
|
||||
"nativeName": "Kiswahili",
|
||||
"path": "sw_TZ/sw_TZ"
|
||||
},
|
||||
"ta_IN": {
|
||||
"nativeName": "தமிழ்",
|
||||
"path": "ta_IN/ta_IN"
|
||||
},
|
||||
"te_IN": {
|
||||
"nativeName": "తెలుగు",
|
||||
"path": "te_IN/te_IN"
|
||||
},
|
||||
"th_TH": {
|
||||
"nativeName": "ไทย",
|
||||
"path": "th_TH/th_TH"
|
||||
},
|
||||
"tr_TR": {
|
||||
"nativeName": "Türkçe",
|
||||
"path": "tr_TR/tr_TR"
|
||||
},
|
||||
"uk_UA": {
|
||||
"nativeName": "українська",
|
||||
"path": "uk_UA/uk_UA"
|
||||
},
|
||||
"vi_VN": {
|
||||
"nativeName": "Tiếng Việt",
|
||||
"path": "vi/vi_VN"
|
||||
}
|
||||
}
|
|
@ -292,9 +292,9 @@ ApplicationWindow {
|
|||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
Connections {
|
||||
target: UtilsAdapter
|
||||
function onRaiseWhenCalled() {
|
||||
function onRaiseWhenCalledChanged() {
|
||||
raiseWhenCalled = AppSettingsManager.getValue(Settings.RaiseWhenCalled);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,7 +63,7 @@ extern const QString defaultDownloadPath;
|
|||
X(WindowState, QWindow::AutomaticVisibility) \
|
||||
X(EnableExperimentalSwarm, false) \
|
||||
X(LANG, "SYSTEM") \
|
||||
X(SpellLang, "NONE") \
|
||||
X(SpellLang, {}) \
|
||||
X(EnableSpellCheck, true) \
|
||||
X(PluginStoreEndpoint, "https://plugins.jami.net") \
|
||||
X(PositionShareDuration, 15) \
|
||||
|
|
|
@ -78,7 +78,7 @@ Popup {
|
|||
contentItem: ColumnLayout {
|
||||
id: contentLayout
|
||||
|
||||
JamiPushButton {
|
||||
JamiPushButton { QWKSetParentHitTestVisible {}
|
||||
id: closeButton
|
||||
|
||||
visible: closeButtonVisible
|
||||
|
@ -117,7 +117,7 @@ Popup {
|
|||
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredHeight: Math.min(contentHeight, root.height)
|
||||
Layout.preferredWidth: contentItem.childrenRect.width
|
||||
Layout.preferredWidth: contentItem.childrenRect.width + ScrollBar.vertical.width
|
||||
Layout.leftMargin: popupMargins
|
||||
Layout.rightMargin: popupMargins
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
|
|
370
src/app/commoncomponents/DictionaryInstallView.qml
Normal file
370
src/app/commoncomponents/DictionaryInstallView.qml
Normal file
|
@ -0,0 +1,370 @@
|
|||
/*
|
||||
* 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 Qt5Compat.GraphicalEffects
|
||||
import net.jami.Models 1.1
|
||||
import net.jami.Adapters 1.1
|
||||
import net.jami.Constants 1.1
|
||||
import "../mainview/components"
|
||||
import "../settingsview/components"
|
||||
import SortFilterProxyModel 0.2
|
||||
|
||||
// Search bar for filtering dictionaries
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: 0
|
||||
property int checkBoxWidth: 24
|
||||
|
||||
Component.onCompleted: Qt.callLater(dictionarySearchBar.setTextAreaFocus)
|
||||
|
||||
RowLayout {
|
||||
id: headerLayout
|
||||
width: parent.width
|
||||
Layout.preferredHeight: childrenRect.height
|
||||
|
||||
// Header title
|
||||
Searchbar {
|
||||
id: dictionarySearchBar
|
||||
|
||||
focus: true
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 55
|
||||
|
||||
placeHolderText: JamiStrings.searchTextLanguages
|
||||
Accessible.name: JamiStrings.searchTextLanguages
|
||||
Accessible.role: Accessible.EditableText
|
||||
Accessible.description: JamiStrings.searchAvailableTextLanguages
|
||||
|
||||
onSearchBarTextChanged: function (text) {
|
||||
dictionaryProxyModel.combinedFilterPattern = text;
|
||||
dictionaryProxyModel.invalidate();
|
||||
}
|
||||
}
|
||||
Label {
|
||||
text: JamiStrings.showInstalledDictionaries
|
||||
color: JamiTheme.faddedLastInteractionFontColor
|
||||
font.pixelSize: JamiTheme.settingsDescriptionPixelSize
|
||||
Layout.rightMargin: 0
|
||||
Layout.preferredHeight: 16
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
// Checkbox to filter installed dictionaries
|
||||
CheckBox {
|
||||
id: showInstalledOnlyCheckbox
|
||||
Accessible.name: JamiStrings.showInstalledDictionaries
|
||||
Accessible.role: Accessible.CheckBox
|
||||
Accessible.description: JamiStrings.showInstalledDictionariesDescription
|
||||
checked: false
|
||||
indicator: Image {
|
||||
anchors.centerIn: parent
|
||||
layer {
|
||||
enabled: true
|
||||
effect: ColorOverlay {
|
||||
color: JamiTheme.tintedBlue
|
||||
}
|
||||
mipmap: false
|
||||
smooth: true
|
||||
}
|
||||
width: checkBoxWidth
|
||||
height: checkBoxWidth
|
||||
source: showInstalledOnlyCheckbox.checked ? JamiResources.check_box_24dp_svg : JamiResources.check_box_outline_blank_24dp_svg
|
||||
}
|
||||
|
||||
Layout.preferredWidth: 55
|
||||
Layout.preferredHeight: 55
|
||||
Layout.rightMargin: 0
|
||||
}
|
||||
}
|
||||
|
||||
// Connect to listen for download failure and pop a simple dialog to inform the user
|
||||
Connections {
|
||||
target: SpellCheckAdapter
|
||||
function onDownloadFailed(locale) {
|
||||
viewCoordinator.presentDialog(appWindow, "commoncomponents/SimpleMessageDialog.qml", {
|
||||
"title": JamiStrings.error,
|
||||
"infoText": JamiStrings.spellCheckDownloadFailed.arg(locale),
|
||||
"buttonTitles": [JamiStrings.optionOk],
|
||||
"buttonStyles": [SimpleMessageDialog.ButtonStyle.TintedBlue],
|
||||
"buttonRoles": [DialogButtonBox.AcceptRole]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
JamiListView {
|
||||
id: spellCheckDictionaryListView
|
||||
|
||||
width: parent.width
|
||||
Layout.fillHeight: true
|
||||
|
||||
model: SortFilterProxyModel {
|
||||
id: dictionaryProxyModel
|
||||
sourceModel: SpellCheckAdapter.getDictionaryListModel()
|
||||
|
||||
property string combinedFilterPattern
|
||||
|
||||
filters: AllOf {
|
||||
AnyOf {
|
||||
// Filter by dictionary name
|
||||
RegExpFilter {
|
||||
roleName: "Locale"
|
||||
pattern: dictionaryProxyModel.combinedFilterPattern
|
||||
caseSensitivity: Qt.CaseInsensitive
|
||||
}
|
||||
// Filter by native name
|
||||
RegExpFilter {
|
||||
roleName: "NativeName"
|
||||
pattern: dictionaryProxyModel.combinedFilterPattern
|
||||
caseSensitivity: Qt.CaseInsensitive
|
||||
}
|
||||
}
|
||||
ValueFilter {
|
||||
roleName: "Installed"
|
||||
value: true
|
||||
enabled: showInstalledOnlyCheckbox.checked
|
||||
}
|
||||
}
|
||||
|
||||
sorters: [
|
||||
// Sort by locale alphabetically
|
||||
RoleSorter {
|
||||
roleName: "Locale"
|
||||
sortOrder: Qt.AscendingOrder
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
readonly property int itemMargins: 20
|
||||
topMargin: itemMargins / 2
|
||||
bottomMargin: itemMargins / 2
|
||||
|
||||
spacing: 8
|
||||
clip: true
|
||||
|
||||
delegate: ItemDelegate {
|
||||
id: dictionaryDelegate
|
||||
width: spellCheckDictionaryListView.width
|
||||
height: Math.max(JamiTheme.preferredFieldHeight, contentLayout.implicitHeight + 32)
|
||||
|
||||
background: Rectangle {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: parent.width - spellCheckDictionaryListView.itemMargins
|
||||
height: parent.height
|
||||
color: JamiTheme.backgroundColor
|
||||
radius: JamiTheme.primaryRadius
|
||||
border.color: "transparent"
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: contentLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 16
|
||||
|
||||
// Dictionary info
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.leftMargin: 16
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
id: dictionaryName
|
||||
Layout.fillWidth: true
|
||||
text: model.NativeName || ""
|
||||
color: JamiTheme.textColor
|
||||
font.pixelSize: JamiTheme.settingsDescriptionPixelSize
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
id: dictionaryLocale
|
||||
Layout.fillWidth: true
|
||||
text: model.Locale || ""
|
||||
color: JamiTheme.faddedLastInteractionFontColor
|
||||
font.pixelSize: JamiTheme.settingsDescriptionPixelSize - 2
|
||||
elide: Text.ElideRight
|
||||
visible: text !== ""
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
// Installation status and action
|
||||
Item {
|
||||
Layout.preferredWidth: 100
|
||||
Layout.preferredHeight: 32
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.rightMargin: 16
|
||||
|
||||
// Install button for available dictionaries
|
||||
MaterialButton {
|
||||
id: installButton
|
||||
anchors.centerIn: parent
|
||||
width: 100
|
||||
height: 32
|
||||
Accessible.name: dictionaryName.text + " " + JamiStrings.install
|
||||
Accessible.role: Accessible.Button
|
||||
|
||||
text: JamiStrings.install
|
||||
|
||||
font.pixelSize: JamiTheme.settingsDescriptionPixelSize - 1
|
||||
font.weight: Font.Medium
|
||||
|
||||
focusPolicy: Qt.StrongFocus
|
||||
KeyNavigation.tab: {
|
||||
try {
|
||||
if (model.index < dictionaryProxyModel.count - 1) {
|
||||
var nextItem = spellCheckDictionaryListView.itemAtIndex(model.index + 1);
|
||||
if (nextItem) {
|
||||
var nextButton = nextItem.findChild("installButton") || nextItem.findChild("uninstallButton");
|
||||
return nextButton || null;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug("KeyNavigation error handled:", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
onFocusChanged: {
|
||||
if (focus) {
|
||||
spellCheckDictionaryListView.positionViewAtIndex(model.index, ListView.Contain);
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if (model.Locale) {
|
||||
SpellCheckAdapter.installDictionary(model.Locale);
|
||||
}
|
||||
}
|
||||
|
||||
visible: !model.Downloading && !model.Installed && model.Locale !== undefined && model.Locale !== ""
|
||||
}
|
||||
|
||||
// Uninstall button for installed dictionaries (not system dictionaries)
|
||||
MaterialButton {
|
||||
id: uninstallButton
|
||||
anchors.centerIn: parent
|
||||
width: 100
|
||||
height: 32
|
||||
|
||||
Accessible.name: dictionaryName.text + " " + JamiStrings.uninstall
|
||||
Accessible.role: Accessible.Button
|
||||
|
||||
text: JamiStrings.uninstall
|
||||
color: "#ff6666"
|
||||
hoveredColor: "#ff9999"
|
||||
|
||||
font.pixelSize: JamiTheme.settingsDescriptionPixelSize - 1
|
||||
font.weight: Font.Medium
|
||||
|
||||
focusPolicy: Qt.StrongFocus
|
||||
KeyNavigation.tab: {
|
||||
try {
|
||||
if (model.index < dictionaryProxyModel.count - 1) {
|
||||
var nextItem = spellCheckDictionaryListView.itemAtIndex(model.index + 1);
|
||||
if (nextItem) {
|
||||
var nextButton = nextItem.findChild("installButton") || nextItem.findChild("uninstallButton");
|
||||
return nextButton || null;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug("KeyNavigation error handled:", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
onFocusChanged: {
|
||||
if (focus) {
|
||||
spellCheckDictionaryListView.positionViewAtIndex(model.index, ListView.Contain);
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if (model.Locale) {
|
||||
SpellCheckAdapter.uninstallDictionary(model.Locale);
|
||||
}
|
||||
}
|
||||
|
||||
visible: !model.Downloading && model.Installed && !model.IsSystem && model.Locale !== undefined && model.Locale !== ""
|
||||
}
|
||||
|
||||
// System dictionary indicator
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: JamiStrings.systemDictionary
|
||||
color: JamiTheme.faddedLastInteractionFontColor
|
||||
font.pixelSize: JamiTheme.settingsDescriptionPixelSize - 2
|
||||
visible: model.IsSystem
|
||||
}
|
||||
|
||||
// Downloading status indicator
|
||||
BusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
visible: model.Downloading
|
||||
running: model.Downloading
|
||||
width: 24
|
||||
height: 24
|
||||
|
||||
// Use a custom animation for better UX
|
||||
Behavior on running {
|
||||
NumberAnimation {
|
||||
duration: 300
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Empty state for when no dictionaries are found
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
visible: dictionaryProxyModel.count === 0
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 16
|
||||
width: parent.width * 0.8
|
||||
|
||||
// Big books emoji
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "📚"
|
||||
font.pixelSize: 48
|
||||
opacity: 0.3
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
text: dictionarySearchBar.textContent.length > 0 ? JamiStrings.noDictionariesFoundFor.arg(dictionarySearchBar.textContent) : JamiStrings.noDictionariesAvailable
|
||||
color: JamiTheme.faddedLastInteractionFontColor
|
||||
font.pixelSize: JamiTheme.settingsDescriptionPixelSize
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,6 +17,8 @@
|
|||
import QtQuick
|
||||
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 "contextmenu"
|
||||
import "../mainview"
|
||||
import "../mainview/components"
|
||||
|
@ -30,15 +32,16 @@ ContextMenuAutoLoader {
|
|||
property var selectionEnd
|
||||
property bool customizePaste: false
|
||||
property bool selectOnly: false
|
||||
property bool checkSpell: false
|
||||
property bool spellCheckEnabled: false
|
||||
property var suggestionList
|
||||
property var menuItemsLength
|
||||
property var language
|
||||
|
||||
signal contextMenuRequirePaste
|
||||
|
||||
SpellLanguageContextMenu {
|
||||
id: spellLanguageContextMenu
|
||||
active: checkSpell
|
||||
active: spellCheckEnabled
|
||||
}
|
||||
|
||||
property list<GeneralMenuItem> menuItems: [
|
||||
|
@ -49,8 +52,7 @@ ContextMenuAutoLoader {
|
|||
isActif: lineEditObj.selectedText.length && !selectOnly
|
||||
itemName: JamiStrings.cut
|
||||
hasIcon: false
|
||||
onClicked:
|
||||
lineEditObj.cut();
|
||||
onClicked: lineEditObj.cut()
|
||||
},
|
||||
GeneralMenuItem {
|
||||
id: copy
|
||||
|
@ -59,8 +61,7 @@ ContextMenuAutoLoader {
|
|||
isActif: lineEditObj.selectedText.length
|
||||
itemName: JamiStrings.copy
|
||||
hasIcon: false
|
||||
onClicked:
|
||||
lineEditObj.copy();
|
||||
onClicked: lineEditObj.copy()
|
||||
},
|
||||
GeneralMenuItem {
|
||||
id: paste
|
||||
|
@ -76,30 +77,39 @@ ContextMenuAutoLoader {
|
|||
}
|
||||
},
|
||||
GeneralMenuItem {
|
||||
id: language
|
||||
visible: checkSpell
|
||||
canTrigger: checkSpell
|
||||
itemName: JamiStrings.language
|
||||
id: textLanguage
|
||||
canTrigger: spellCheckEnabled && SpellCheckAdapter.installedDictionaryCount > 0
|
||||
itemName: JamiStrings.textLanguage
|
||||
hasIcon: false
|
||||
onClicked: {
|
||||
spellLanguageContextMenu.openMenu();
|
||||
}
|
||||
},
|
||||
GeneralMenuItem {
|
||||
id: manageLanguages
|
||||
itemName: JamiStrings.manageDictionaries
|
||||
canTrigger: spellCheckEnabled
|
||||
hasIcon: false
|
||||
onClicked: {
|
||||
viewCoordinator.presentDialog(appWindow, "commoncomponents/ManageDictionariesDialog.qml");
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
ListView {
|
||||
model: ListModel {
|
||||
id: dynamicModel
|
||||
id: suggestionListModel
|
||||
}
|
||||
|
||||
Instantiator {
|
||||
model: dynamicModel
|
||||
model: suggestionListModel
|
||||
delegate: GeneralMenuItem {
|
||||
id: suggestion
|
||||
|
||||
canTrigger: true
|
||||
isActif: true
|
||||
itemName: model.name
|
||||
bold: true
|
||||
hasIcon: false
|
||||
onClicked: {
|
||||
replaceWord(model.name);
|
||||
|
@ -117,7 +127,7 @@ ContextMenuAutoLoader {
|
|||
}
|
||||
|
||||
function removeItems() {
|
||||
dynamicModel.remove(0, suggestionList.length);
|
||||
suggestionListModel.clear();
|
||||
suggestionList.length = 0;
|
||||
}
|
||||
|
||||
|
@ -125,7 +135,7 @@ ContextMenuAutoLoader {
|
|||
menuItemsLength = menuItems.length; // Keep initial number of items for easier removal
|
||||
suggestionList = wordList;
|
||||
for (var i = 0; i < suggestionList.length; ++i) {
|
||||
dynamicModel.append({
|
||||
suggestionListModel.append({
|
||||
"name": suggestionList[i]
|
||||
});
|
||||
}
|
||||
|
@ -154,7 +164,7 @@ ContextMenuAutoLoader {
|
|||
lineEditObj.select(selectionStart, selectionEnd);
|
||||
}
|
||||
function onClosed() {
|
||||
if (!suggestionList || suggestionList.length == 0) {
|
||||
if (!suggestionList || suggestionList.length === 0) {
|
||||
return;
|
||||
}
|
||||
removeItems();
|
||||
|
|
|
@ -16,19 +16,21 @@
|
|||
*/
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import net.jami.Models 1.1
|
||||
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"
|
||||
import "../commoncomponents/contextmenu"
|
||||
|
||||
Item {
|
||||
id: cachedFile
|
||||
property string dictionaryPath: SpellCheckDictionaryManager.getDictionariesPath()
|
||||
BaseModalDialog {
|
||||
id: root
|
||||
objectName: "manageDictionariesDialog"
|
||||
|
||||
function updateDictionnary(languagePath) {
|
||||
var file = dictionaryPath + languagePath;
|
||||
MessagesAdapter.updateDictionnary(file);
|
||||
title: JamiStrings.manageDictionaries
|
||||
|
||||
popupContent: DictionaryInstallView {
|
||||
Accessible.name: JamiStrings.manageDictionaries
|
||||
Accessible.role: Accessible.PopupMenu
|
||||
width: 400
|
||||
height: 500
|
||||
}
|
||||
}
|
|
@ -23,7 +23,7 @@ import QtQuick.Layouts
|
|||
Rectangle {
|
||||
property alias name: label.text
|
||||
property bool stretchParent: false
|
||||
property string tag: parent.toString()
|
||||
property string tag: parent.toString() + " (w:" + width + ", h: " + height + ")"
|
||||
signal moveX(real dx)
|
||||
signal moveY(real dy)
|
||||
property real ox: 0
|
||||
|
|
|
@ -50,11 +50,14 @@ ComboBox {
|
|||
|
||||
contentItem: Text {
|
||||
text: {
|
||||
if (index < 0)
|
||||
if (index < 0 || !model)
|
||||
return '';
|
||||
var currentItem = root.delegateModel.items.get(index);
|
||||
const value = currentItem.model[root.textRole];
|
||||
return value === undefined ? '' : value.toString();
|
||||
|
||||
if (root.textRole && model[root.textRole] !== undefined) {
|
||||
return model[root.textRole].toString();
|
||||
}
|
||||
|
||||
return model.display !== undefined ? model.display.toString() : '';
|
||||
}
|
||||
|
||||
color: hovered ? JamiTheme.comboboxTextColorHovered : JamiTheme.textColor
|
||||
|
@ -80,7 +83,7 @@ ComboBox {
|
|||
|
||||
source: popup.visible ? JamiResources.expand_less_24dp_svg : JamiResources.expand_more_24dp_svg
|
||||
|
||||
color: JamiTheme.comboboxIconColor
|
||||
color: root.enabled ? JamiTheme.comboboxIconColor : "grey"
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
|
@ -92,7 +95,7 @@ ComboBox {
|
|||
anchors.rightMargin: root.indicator.width * 2
|
||||
font.pixelSize: JamiTheme.settingsDescriptionPixelSize
|
||||
text: root.displayText
|
||||
color: JamiTheme.comboboxTextColor
|
||||
color: root.enabled ? JamiTheme.comboboxTextColor : "grey"
|
||||
font.weight: Font.Medium
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
|
@ -104,7 +107,11 @@ ComboBox {
|
|||
color: JamiTheme.transparentColor
|
||||
implicitWidth: 120
|
||||
implicitHeight: contentItem.implicitHeight + JamiTheme.buttontextHeightMargin
|
||||
border.color: popup.visible ? JamiTheme.comboboxBorderColorActive : JamiTheme.comboboxBorderColor
|
||||
border.color: root.enabled ?
|
||||
(popup.visible ?
|
||||
JamiTheme.comboboxBorderColorActive :
|
||||
JamiTheme.comboboxBorderColor) :
|
||||
"grey"
|
||||
border.width: root.visualFocus ? 2 : 1
|
||||
radius: 5
|
||||
}
|
||||
|
|
|
@ -22,15 +22,12 @@ import net.jami.Enums 1.1
|
|||
import "contextmenu"
|
||||
import "../mainview"
|
||||
import "../mainview/components"
|
||||
import SortFilterProxyModel 0.2
|
||||
|
||||
ContextMenuAutoLoader {
|
||||
id: root
|
||||
|
||||
signal languageChanged()
|
||||
|
||||
CachedFile {
|
||||
id: cachedFile
|
||||
}
|
||||
signal languageChanged
|
||||
|
||||
function openMenuAt(mouseEvent) {
|
||||
x = mouseEvent.x;
|
||||
|
@ -46,9 +43,11 @@ ContextMenuAutoLoader {
|
|||
function generateMenuItems() {
|
||||
var menuItems = [];
|
||||
// Create new menu items
|
||||
var dictionaries = SpellCheckDictionaryManager.installedDictionaries();
|
||||
var dictionaries = SpellCheckAdapter.getInstalledDictionaries();
|
||||
var keys = Object.keys(dictionaries);
|
||||
for (var i = 0; i < keys.length; ++i) {
|
||||
const locale = keys[i];
|
||||
const nativeName = dictionaries[keys[i]];
|
||||
var menuItem = Qt.createComponent("qrc:/commoncomponents/contextmenu/GeneralMenuItem.qml", Component.PreferSynchronous);
|
||||
if (menuItem.status !== Component.Ready) {
|
||||
console.error("Error loading component:", menuItem.errorString());
|
||||
|
@ -58,17 +57,19 @@ ContextMenuAutoLoader {
|
|||
"parent": root,
|
||||
"canTrigger": true,
|
||||
"isActif": true,
|
||||
"itemName": dictionaries[keys[i]],
|
||||
"itemName": nativeName,
|
||||
"hasIcon": false,
|
||||
"content": keys[i],
|
||||
"content": locale,
|
||||
"bold": UtilsAdapter.getAppValue(Settings.SpellLang) === locale
|
||||
});
|
||||
if (menuItemObject === null) {
|
||||
console.error("Error creating menu item:", menuItem.errorString());
|
||||
continue;
|
||||
}
|
||||
menuItemObject.clicked.connect(function () {
|
||||
UtilsAdapter.setAppValue(Settings.Key.SpellLang, menuItemObject.content);
|
||||
});
|
||||
const locale = menuItemObject.content;
|
||||
SpellCheckAdapter.setDictionary(locale);
|
||||
});
|
||||
// Log the object pointer
|
||||
menuItems.push(menuItemObject);
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ MenuItem {
|
|||
id: menuItem
|
||||
|
||||
property string itemName: ""
|
||||
property bool bold: false
|
||||
property string content: ""
|
||||
property alias iconSource: contextMenuItemImage.source
|
||||
property string iconColor: ""
|
||||
|
@ -99,6 +100,7 @@ MenuItem {
|
|||
anchors.left: parent.left
|
||||
height: parent.height
|
||||
text: itemName
|
||||
font.bold: bold
|
||||
color: dangerous ? JamiTheme.redColor : isActif ? JamiTheme.textColor : JamiTheme.chatViewFooterImgColor
|
||||
font.pointSize: JamiTheme.textFontSize
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
#include "global.h"
|
||||
#include "qmlregister.h"
|
||||
#include "appsettingsmanager.h"
|
||||
#include "spellcheckdictionarymanager.h"
|
||||
#include "connectivitymonitor.h"
|
||||
#include "systemtray.h"
|
||||
#include "previewengine.h"
|
||||
|
@ -160,6 +159,7 @@ MainApplication::MainApplication(int& argc, char** argv)
|
|||
"qml.debug=false\n"
|
||||
"default.debug=false\n"
|
||||
"client.debug=false\n"
|
||||
"spellcheck.debug=false\n"
|
||||
"\n");
|
||||
// These can be set in the environment as well.
|
||||
// e.g. QT_LOGGING_RULES="*.debug=false;qml.debug=true"
|
||||
|
@ -191,7 +191,6 @@ 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
|
||||
|
@ -349,8 +348,8 @@ MainApplication::parseArguments()
|
|||
parser_.addOption(muteDaemonOption);
|
||||
|
||||
#ifdef QT_DEBUG
|
||||
// In debug mode, add an option to test a specific QML component via its name.
|
||||
// e.g. ./jami --test AccountComboBox
|
||||
// In debug mode, add an option to test a specific QML component via its name.
|
||||
// e.g. ./jami --test AccountComboBox
|
||||
parser_.addOption(QCommandLineOption("test", "Test a QML component via its name.", "uri"));
|
||||
// We may need to force the test window dimensions in the case that the component to test
|
||||
// does not specify its own dimensions and is dependent on parent/sibling dimensions.
|
||||
|
@ -425,7 +424,6 @@ MainApplication::initQmlLayer()
|
|||
lrcInstance_.get(),
|
||||
systemTray_,
|
||||
settingsManager_,
|
||||
spellCheckDictionaryManager_,
|
||||
connectivityMonitor_,
|
||||
previewEngine_,
|
||||
&screenInfo_,
|
||||
|
|
|
@ -31,7 +31,6 @@
|
|||
class ConnectivityMonitor;
|
||||
class SystemTray;
|
||||
class AppSettingsManager;
|
||||
class SpellCheckDictionaryManager;
|
||||
class CrashReporter;
|
||||
class PreviewEngine;
|
||||
|
||||
|
@ -119,7 +118,6 @@ private:
|
|||
ConnectivityMonitor* connectivityMonitor_;
|
||||
SystemTray* systemTray_;
|
||||
AppSettingsManager* settingsManager_;
|
||||
SpellCheckDictionaryManager* spellCheckDictionaryManager_;
|
||||
PreviewEngine* previewEngine_;
|
||||
CrashReporter* crashReporter_;
|
||||
|
||||
|
|
|
@ -37,7 +37,6 @@ 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
|
||||
|
@ -73,7 +72,7 @@ JamiFlickable {
|
|||
|
||||
lineEditObj: textArea
|
||||
customizePaste: true
|
||||
checkSpell: (Qt.platform.os.toString() === "linux") ? true : false
|
||||
spellCheckEnabled: root.spellCheckEnabled
|
||||
|
||||
onContextMenuRequirePaste: {
|
||||
// Intercept paste event to use C++ QMimeData
|
||||
|
@ -114,73 +113,43 @@ JamiFlickable {
|
|||
}
|
||||
}
|
||||
|
||||
readonly property bool spellCheckEnabled:
|
||||
AppSettingsManager.getValue(Settings.EnableSpellCheck) &&
|
||||
AppSettingsManager.getValue(Settings.SpellLang) !== ""
|
||||
|
||||
// Spell check is active under the following conditions:
|
||||
// 1. Spell check is enabled in settings
|
||||
// 2. The selected spell language is not ""
|
||||
// 3. We are not in preview mode
|
||||
readonly property bool spellCheckActive: spellCheckEnabled && !showPreview
|
||||
|
||||
onSpellCheckActiveChanged: textArea.updateSpellCorrection()
|
||||
|
||||
TextArea.flickable: TextArea {
|
||||
id: textArea
|
||||
|
||||
CachedFile {
|
||||
id: cachedFile
|
||||
Connections {
|
||||
target: SpellCheckAdapter
|
||||
|
||||
function onDictionaryChanged() {
|
||||
textArea.updateSpellCorrection();
|
||||
}
|
||||
}
|
||||
|
||||
function updateCorrection(language) {
|
||||
cachedFile.updateDictionnary(language);
|
||||
textArea.updateUnderlineText();
|
||||
}
|
||||
|
||||
// Listen to settings changes and apply it to this widget
|
||||
// Listen to settings changes to apply it to the text area
|
||||
Connections {
|
||||
target: UtilsAdapter
|
||||
|
||||
function onChangeLanguage() {
|
||||
textArea.updateUnderlineText();
|
||||
textArea.updateSpellCorrection();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
textArea.updateSpellCorrection();
|
||||
}
|
||||
|
||||
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();
|
||||
textArea.updateSpellCorrection();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -223,13 +192,15 @@ JamiFlickable {
|
|||
|
||||
onReleased: function (event) {
|
||||
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);
|
||||
if (spellCheckActive) {
|
||||
var position = textArea.positionAt(event.x, event.y);
|
||||
textArea.moveCursorSelection(position, TextInput.SelectWords);
|
||||
textArea.selectWord();
|
||||
if (!SpellCheckAdapter.spell(textArea.selectedText)) {
|
||||
var wordList = SpellCheckAdapter.spellSuggestionsRequest(textArea.selectedText);
|
||||
if (wordList.length !== 0) {
|
||||
textAreaContextMenu.addMenuItem(wordList);
|
||||
}
|
||||
}
|
||||
}
|
||||
textAreaContextMenu.openMenuAt(event);
|
||||
|
@ -237,7 +208,7 @@ JamiFlickable {
|
|||
}
|
||||
|
||||
onTextChanged: {
|
||||
updateUnderlineText();
|
||||
updateSpellCorrection();
|
||||
if (text !== debounceText && !showPreview) {
|
||||
debounceText = text;
|
||||
MessagesAdapter.userIsComposing(text ? true : false);
|
||||
|
@ -250,7 +221,7 @@ JamiFlickable {
|
|||
// Shift + Enter -> Next Line
|
||||
Keys.onPressed: function (keyEvent) {
|
||||
// Update underline on each input to take into account deleted text and sent ones
|
||||
updateUnderlineText();
|
||||
updateSpellCorrection();
|
||||
if (keyEvent.matches(StandardKey.Paste)) {
|
||||
MessagesAdapter.onPaste();
|
||||
keyEvent.accepted = true;
|
||||
|
@ -280,17 +251,17 @@ JamiFlickable {
|
|||
}
|
||||
}
|
||||
|
||||
function updateUnderlineText() {
|
||||
function updateSpellCorrection() {
|
||||
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);
|
||||
var words = SpellCheckAdapter.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)) {
|
||||
if (wordInfo && wordInfo.word && !SpellCheckAdapter.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;
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
#include "qtutils.h"
|
||||
#include "messageparser.h"
|
||||
#include "previewengine.h"
|
||||
#include "spellchecker.h"
|
||||
|
||||
#include <api/datatransfermodel.h>
|
||||
#include <api/contact.h>
|
||||
|
@ -40,25 +39,17 @@
|
|||
#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_));
|
||||
|
@ -736,53 +727,3 @@ 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,8 +23,6 @@
|
|||
#include "previewengine.h"
|
||||
#include "messageparser.h"
|
||||
#include "appsettingsmanager.h"
|
||||
#include "spellchecker.h"
|
||||
#include "spellcheckdictionarymanager.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
@ -102,14 +100,11 @@ 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;
|
||||
|
@ -168,10 +163,6 @@ 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);
|
||||
|
@ -206,12 +197,10 @@ private:
|
|||
QList<QString> conversationTypersUrlToName(const QSet<QString>& typersSet);
|
||||
|
||||
AppSettingsManager* settingsManager_;
|
||||
SpellCheckDictionaryManager* spellCheckDictionaryManager_;
|
||||
MessageParser* messageParser_;
|
||||
FilteredMsgListModel* filteredMsgListModel_;
|
||||
std::unique_ptr<MessageListModel> mediaInteractions_;
|
||||
QTimer* timestampTimer_;
|
||||
std::shared_ptr<SpellChecker> spellChecker_;
|
||||
static constexpr const int loadChunkSize_ {20};
|
||||
static constexpr const int timestampUpdateIntervalMs_ {1000};
|
||||
};
|
||||
|
|
|
@ -273,9 +273,9 @@ Item {
|
|||
property string hideSpectators: qsTr("Hide spectators")
|
||||
|
||||
// LineEditContextMenu
|
||||
property string copy: qsTr("Copy")
|
||||
property string share: qsTr("Share")
|
||||
property string cut: qsTr("Cut")
|
||||
property string copy: qsTr("Copy")
|
||||
property string paste: qsTr("Paste")
|
||||
property string language: qsTr("Language")
|
||||
|
||||
|
@ -913,9 +913,15 @@ Item {
|
|||
|
||||
// Spell checker
|
||||
property string checkSpelling: qsTr("Check spelling while typing")
|
||||
property string systemDictionary: qsTr("System")
|
||||
property string textLanguage: qsTr("Text language")
|
||||
property string textLanguageDescription: qsTr("To install new dictionaries, use the system package manager.")
|
||||
property string spellChecker: qsTr("Spell checker")
|
||||
property string refresh: qsTr("Refresh")
|
||||
property string refreshInstalledDictionaries: qsTr("Refresh installed dictionaries")
|
||||
property string spellchecking: qsTr("Spell checker")
|
||||
property string searchTextLanguages: qsTr("Search text languages")
|
||||
property string searchAvailableTextLanguages: qsTr("Search for available text languages")
|
||||
property string noDictionariesFoundFor: qsTr("No dictionaries found for '%1'")
|
||||
property string noDictionariesAvailable: qsTr("No dictionaries available")
|
||||
property string manageDictionaries: qsTr("Manage Dictionaries")
|
||||
property string spellCheckDownloadFailed: qsTr("Download failed for dictionary '%1'")
|
||||
property string showInstalledDictionaries: qsTr("Show installed")
|
||||
property string showInstalledDictionariesDescription: qsTr("Only show dictionaries that are currently installed")
|
||||
}
|
||||
|
|
|
@ -36,7 +36,8 @@
|
|||
#include "currentaccounttomigrate.h"
|
||||
#include "pttlistener.h"
|
||||
#include "calloverlaymodel.h"
|
||||
#include "spellcheckdictionarymanager.h"
|
||||
#include "spellcheckdictionarylistmodel.h"
|
||||
#include "spellcheckadapter.h"
|
||||
#include "accountlistmodel.h"
|
||||
#include "mediacodeclistmodel.h"
|
||||
#include "audiodevicemodel.h"
|
||||
|
@ -65,7 +66,6 @@
|
|||
#include "wizardviewstepmodel.h"
|
||||
#include "linkdevicemodel.h"
|
||||
#include "qrcodescannermodel.h"
|
||||
#include "spellchecker.h"
|
||||
|
||||
#include "api/peerdiscoverymodel.h"
|
||||
#include "api/codecmodel.h"
|
||||
|
@ -119,7 +119,6 @@ registerTypes(QQmlEngine* engine,
|
|||
LRCInstance* lrcInstance,
|
||||
SystemTray* systemTray,
|
||||
AppSettingsManager* settingsManager,
|
||||
SpellCheckDictionaryManager* spellCheckDictionaryManager,
|
||||
ConnectivityMonitor* connectivityMonitor,
|
||||
PreviewEngine* previewEngine,
|
||||
ScreenInfo* screenInfo,
|
||||
|
@ -196,6 +195,10 @@ registerTypes(QQmlEngine* engine,
|
|||
QQmlEngine::setObjectOwnership(linkdevicemodel, QQmlEngine::CppOwnership);
|
||||
REG_QML_SINGLETON<LinkDeviceModel>(REG_MODEL, "LinkDeviceModel", CREATE(linkdevicemodel));
|
||||
|
||||
// SpellCheckDictionaryListModel (available through SpellCheckAdapter)
|
||||
auto spellCheckDictionaryListModel = new SpellCheckDictionaryListModel(settingsManager, connectivityMonitor, app);
|
||||
qApp->setProperty("SpellCheckDictionaryListModel", QVariant::fromValue(spellCheckDictionaryListModel));
|
||||
|
||||
// Register app-level objects that are used by QML created objects.
|
||||
// These MUST be set prior to loading the initial QML file, in order to
|
||||
// be available to the QML adapter class factory creation methods.
|
||||
|
@ -204,7 +207,6 @@ 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);
|
||||
|
@ -225,6 +227,7 @@ registerTypes(QQmlEngine* engine,
|
|||
QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, VideoDevices);
|
||||
QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, CurrentAccountToMigrate);
|
||||
QML_REGISTERSINGLETON_TYPE(NS_HELPERS, FileDownloader);
|
||||
QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, SpellCheckAdapter);
|
||||
|
||||
// TODO: remove these
|
||||
QML_REGISTERSINGLETONTYPE_CUSTOM(NS_MODELS, AVModel, &lrcInstance->avModel())
|
||||
|
@ -241,7 +244,6 @@ 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,12 +252,12 @@ registerTypes(QQmlEngine* engine,
|
|||
QML_REGISTERNAMESPACE(NS_MODELS, FilesToSend::staticMetaObject, "FilesToSend");
|
||||
QML_REGISTERNAMESPACE(NS_MODELS, MessageList::staticMetaObject, "MessageList");
|
||||
QML_REGISTERNAMESPACE(NS_MODELS, PluginStatus::staticMetaObject, "PluginStatus");
|
||||
// QML_REGISTERNAMESPACE(NS_MODELS, SpellCheckDictionaryList::staticMetaObject, "SpellCheckDictionaryList");
|
||||
|
||||
QML_REGISTERSINGLETONTYPE_POBJECT(NS_CONSTANTS, app, "MainApplication")
|
||||
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,7 +32,6 @@
|
|||
class SystemTray;
|
||||
class LRCInstance;
|
||||
class AppSettingsManager;
|
||||
class SpellCheckDictionaryManager;
|
||||
class PreviewEngine;
|
||||
class ScreenInfo;
|
||||
class MainApplication;
|
||||
|
@ -62,7 +61,6 @@ void registerTypes(QQmlEngine* engine,
|
|||
LRCInstance* lrcInstance,
|
||||
SystemTray* systemTray,
|
||||
AppSettingsManager* appSettingsManager,
|
||||
SpellCheckDictionaryManager* spellCheckDictionaryManager,
|
||||
ConnectivityMonitor* connectivityMonitor,
|
||||
PreviewEngine* previewEngine,
|
||||
ScreenInfo* screenInfo,
|
||||
|
|
|
@ -17,11 +17,13 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import net.jami.Models 1.1
|
||||
import net.jami.Adapters 1.1
|
||||
import net.jami.Enums 1.1
|
||||
import net.jami.Constants 1.1
|
||||
import net.jami.Helpers 1.1
|
||||
import SortFilterProxyModel 0.2
|
||||
import "../../commoncomponents"
|
||||
import "../../mainview/components"
|
||||
import "../../mainview/js/contactpickercreation.js" as ContactPickerCreation
|
||||
|
@ -41,6 +43,63 @@ SettingsPageBase {
|
|||
anchors.left: parent.left
|
||||
anchors.leftMargin: JamiTheme.preferredSettingsMarginSize
|
||||
|
||||
ColumnLayout {
|
||||
|
||||
width: parent.width
|
||||
spacing: JamiTheme.settingsCategorySpacing
|
||||
|
||||
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
|
||||
tooltipText: JamiStrings.checkSpelling
|
||||
onSwitchToggled: {
|
||||
UtilsAdapter.setAppValue(Settings.Key.EnableSpellCheck, checked);
|
||||
}
|
||||
}
|
||||
|
||||
SpellCheckLanguageComboBox {
|
||||
id: spellCheckLangComboBoxSetting
|
||||
Layout.fillWidth: true
|
||||
widthOfComboBox: itemWidth
|
||||
}
|
||||
|
||||
// A button to open the dictionary install view as a popup
|
||||
MaterialButton {
|
||||
id: dictionaryInstallButton
|
||||
|
||||
secondary: true
|
||||
|
||||
preferredWidth: itemWidth
|
||||
height: spellCheckLangComboBoxSetting.comboBox.height
|
||||
Layout.alignment: Qt.AlignRight
|
||||
|
||||
text: JamiStrings.manageDictionaries
|
||||
onClicked: {
|
||||
viewCoordinator.presentDialog(appWindow, "commoncomponents/ManageDictionariesDialog.qml");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: generalSettings
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ RowLayout {
|
|||
property alias fontPointSize: comboBoxOfLayout.font.pointSize
|
||||
property alias modelIndex: comboBoxOfLayout.currentIndex
|
||||
property alias modelSize: comboBoxOfLayout.count
|
||||
property alias comboBox: comboBoxOfLayout
|
||||
|
||||
property int widthOfComboBox: 50
|
||||
|
||||
|
@ -53,6 +54,7 @@ RowLayout {
|
|||
SettingParaCombobox {
|
||||
id: comboBoxOfLayout
|
||||
|
||||
enabled: root.enabled
|
||||
Layout.preferredWidth: widthOfComboBox
|
||||
|
||||
font.pointSize: JamiTheme.buttonFontSize
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* 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.Layouts
|
||||
import QtQuick.Controls
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import net.jami.Models 1.1
|
||||
import net.jami.Adapters 1.1
|
||||
import net.jami.Enums 1.1
|
||||
import net.jami.Constants 1.1
|
||||
import net.jami.Helpers 1.1
|
||||
import SortFilterProxyModel 0.2
|
||||
import "../../commoncomponents"
|
||||
import "../../mainview/components"
|
||||
|
||||
SettingsComboBox {
|
||||
id: root
|
||||
height: JamiTheme.preferredFieldHeight
|
||||
labelText: JamiStrings.textLanguage
|
||||
tipText: JamiStrings.textLanguage
|
||||
comboModel: SortFilterProxyModel {
|
||||
id: installedDictionariesModel
|
||||
sourceModel: SpellCheckAdapter.getDictionaryListModel()
|
||||
|
||||
// Filter to show only installed dictionaries
|
||||
filters: ValueFilter {
|
||||
roleName: "Installed"
|
||||
value: true
|
||||
}
|
||||
|
||||
// Sort alphabetically by native name
|
||||
sorters: RoleSorter {
|
||||
roleName: "NativeName"
|
||||
sortOrder: Qt.AscendingOrder
|
||||
}
|
||||
Component.onCompleted: {
|
||||
// Ensure the model is updated with the latest dictionaries
|
||||
root.enabled = Qt.binding(function () {
|
||||
return installedDictionariesModel.count > 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
role: "NativeName"
|
||||
|
||||
// Show placeholder when disabled
|
||||
placeholderText: JamiStrings.none
|
||||
|
||||
function getCurrentLocaleIndex() {
|
||||
var currentLang = UtilsAdapter.getAppValue(Settings.Key.SpellLang);
|
||||
for (var i = 0; i < comboModel.count; i++) {
|
||||
var item = comboModel.get(i);
|
||||
if (item.Locale === currentLang)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Set initial selection based on current spell language setting
|
||||
Component.onCompleted: modelIndex = getCurrentLocaleIndex()
|
||||
|
||||
property string locale
|
||||
function setForIndex(index) {
|
||||
var selectedDict = comboModel.get(index);
|
||||
if (selectedDict && selectedDict.Locale && selectedDict.Installed) {
|
||||
locale = selectedDict.Locale;
|
||||
}
|
||||
}
|
||||
onLocaleChanged: SpellCheckAdapter.setDictionary(locale)
|
||||
|
||||
// When the count changes, we might need to update the model index
|
||||
readonly property int count: installedDictionariesModel.count
|
||||
onCountChanged: {
|
||||
modelIndex = getCurrentLocaleIndex();
|
||||
// If the new index is -1 and we still have dictionaries, use the first one
|
||||
if (modelIndex === -1 && installedDictionariesModel.count > 0) {
|
||||
modelIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// If the model index changes programmatically, we need to update the dictionary path
|
||||
onModelIndexChanged: setForIndex(modelIndex)
|
||||
}
|
|
@ -185,165 +185,6 @@ SettingsPageBase {
|
|||
}
|
||||
}
|
||||
}
|
||||
ColumnLayout {
|
||||
|
||||
width: parent.width
|
||||
spacing: JamiTheme.settingsCategorySpacing
|
||||
visible: (Qt.platform.os.toString() !== "linux") ? false : true
|
||||
|
||||
Text {
|
||||
id: spellCheckerTitle
|
||||
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
Layout.preferredWidth: parent.width
|
||||
|
||||
text: JamiStrings.spellChecker
|
||||
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
|
||||
|
||||
function onChangeLanguage() {
|
||||
var langIdx = langComboBoxSetting.modelIndex;
|
||||
langModel.clear();
|
||||
var supported = UtilsAdapter.supportedLang();
|
||||
var keys = Object.keys(supported);
|
||||
for (var i = 0; i < keys.length; ++i) {
|
||||
langModel.append({
|
||||
"textDisplay": supported[keys[i]],
|
||||
"id": keys[i]
|
||||
});
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
|
||||
|
|
139
src/app/spellcheckadapter.cpp
Normal file
139
src/app/spellcheckadapter.cpp
Normal file
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* 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 "spellcheckadapter.h"
|
||||
|
||||
#include "spellcheckdictionarylistmodel.h"
|
||||
|
||||
#include <QApplication>
|
||||
|
||||
#define SUGGESTIONS_MAX_SIZE 10 // limit the number of spelling suggestions
|
||||
|
||||
SpellCheckAdapter::SpellCheckAdapter(SpellCheckDictionaryListModel* dictionaryListModel,
|
||||
AppSettingsManager* settingsManager,
|
||||
QObject* parent)
|
||||
: QObject(parent)
|
||||
, dictionaryListModel_(dictionaryListModel)
|
||||
, settingsManager_(settingsManager)
|
||||
{
|
||||
// Connect to update the selected dictionary if no dictionary is set on start
|
||||
connect(dictionaryListModel_,
|
||||
&SpellCheckDictionaryListModel::newDictionaryAvailable,
|
||||
this,
|
||||
&SpellCheckAdapter::setDictionary);
|
||||
|
||||
// Load the current dictionary if available
|
||||
auto currentLocale = settingsManager_->getValue(Settings::Key::SpellLang).toString();
|
||||
if (!currentLocale.isEmpty()) {
|
||||
setDictionary(currentLocale);
|
||||
}
|
||||
|
||||
// Listen for data changes to the dictionaryListModel_ to determine
|
||||
// our installedDictionaryCount
|
||||
connect(dictionaryListModel_,
|
||||
&QAbstractItemModel::dataChanged,
|
||||
this,
|
||||
[this](const QModelIndex&, const QModelIndex&, const QList<int>& roles) {
|
||||
if (roles.contains(SpellCheckDictionaryList::Installed)) {
|
||||
set_installedDictionaryCount(
|
||||
dictionaryListModel_->getInstalledDictionaries().size());
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize the installedDictionaryCount
|
||||
set_installedDictionaryCount(dictionaryListModel_->getInstalledDictionaries().size());
|
||||
|
||||
// Listen for download failure
|
||||
connect(dictionaryListModel_,
|
||||
&SpellCheckDictionaryListModel::downloadFailed,
|
||||
this,
|
||||
&SpellCheckAdapter::downloadFailed);
|
||||
}
|
||||
|
||||
void
|
||||
SpellCheckAdapter::installDictionary(const QString& locale)
|
||||
{
|
||||
dictionaryListModel_->installDictionary(locale);
|
||||
}
|
||||
|
||||
void
|
||||
SpellCheckAdapter::uninstallDictionary(const QString& locale)
|
||||
{
|
||||
dictionaryListModel_->uninstallDictionary(locale);
|
||||
}
|
||||
|
||||
QVariant
|
||||
SpellCheckAdapter::getDictionaryListModel() const
|
||||
{
|
||||
return QVariant::fromValue(dictionaryListModel_);
|
||||
}
|
||||
|
||||
QVariantMap
|
||||
SpellCheckAdapter::getInstalledDictionaries() const
|
||||
{
|
||||
return dictionaryListModel_->getInstalledDictionaries();
|
||||
}
|
||||
|
||||
bool
|
||||
SpellCheckAdapter::spell(const QString& word)
|
||||
{
|
||||
return spellChecker_.spell(word);
|
||||
}
|
||||
|
||||
QVariantList
|
||||
SpellCheckAdapter::spellSuggestionsRequest(const QString& word)
|
||||
{
|
||||
QStringList suggestionsList;
|
||||
QVariantList variantList;
|
||||
if (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
|
||||
SpellCheckAdapter::findWords(const QString& text)
|
||||
{
|
||||
QVariantList 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
|
||||
SpellCheckAdapter::setDictionary(const QString& locale)
|
||||
{
|
||||
auto localPath = dictionaryListModel_->pathForLocale(locale);
|
||||
if (spellChecker_.replaceDictionary(localPath)) {
|
||||
settingsManager_->setValue(Settings::Key::SpellLang, locale);
|
||||
Q_EMIT dictionaryChanged();
|
||||
}
|
||||
}
|
72
src/app/spellcheckadapter.h
Normal file
72
src/app/spellcheckadapter.h
Normal file
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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 "spellchecker.h"
|
||||
#include "qtutils.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QQmlEngine> // QML registration
|
||||
#include <QApplication> // QML registration
|
||||
|
||||
class SpellCheckDictionaryListModel;
|
||||
class AppSettingsManager;
|
||||
|
||||
class SpellCheckAdapter final : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_SINGLETON
|
||||
|
||||
QML_RO_PROPERTY(int, installedDictionaryCount)
|
||||
|
||||
public:
|
||||
static SpellCheckAdapter* create(QQmlEngine* engine, QJSEngine*)
|
||||
{
|
||||
return new SpellCheckAdapter(
|
||||
qApp->property("SpellCheckDictionaryListModel").value<SpellCheckDictionaryListModel*>(),
|
||||
qApp->property("AppSettingsManager").value<AppSettingsManager*>(),
|
||||
engine);
|
||||
}
|
||||
|
||||
explicit SpellCheckAdapter(SpellCheckDictionaryListModel* dictionaryListModel,
|
||||
AppSettingsManager* settingsManager,
|
||||
QObject* parent = nullptr);
|
||||
~SpellCheckAdapter() = default;
|
||||
|
||||
Q_INVOKABLE QVariant getDictionaryListModel() const;
|
||||
Q_INVOKABLE QVariantMap getInstalledDictionaries() const;
|
||||
|
||||
Q_INVOKABLE void installDictionary(const QString& locale);
|
||||
Q_INVOKABLE void uninstallDictionary(const QString& locale);
|
||||
|
||||
Q_INVOKABLE QVariantList spellSuggestionsRequest(const QString& word);
|
||||
Q_INVOKABLE bool spell(const QString& word);
|
||||
Q_INVOKABLE QVariantList findWords(const QString& text);
|
||||
|
||||
public Q_SLOTS:
|
||||
Q_INVOKABLE void setDictionary(const QString& locale);
|
||||
|
||||
Q_SIGNALS:
|
||||
void dictionaryChanged();
|
||||
void downloadFailed(const QString& locale);
|
||||
|
||||
private:
|
||||
SpellChecker spellChecker_;
|
||||
SpellCheckDictionaryListModel* dictionaryListModel_ {nullptr};
|
||||
AppSettingsManager* settingsManager_ {nullptr};
|
||||
};
|
460
src/app/spellcheckdictionarylistmodel.cpp
Normal file
460
src/app/spellcheckdictionarylistmodel.cpp
Normal file
|
@ -0,0 +1,460 @@
|
|||
/*
|
||||
* 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 "spellcheckdictionarylistmodel.h"
|
||||
|
||||
#include "global.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QBuffer>
|
||||
#include <QClipboard>
|
||||
#include <QFileInfo>
|
||||
#include <QRegExp>
|
||||
#include <QMimeData>
|
||||
#include <QDir>
|
||||
#include <QMimeDatabase>
|
||||
#include <QUrl>
|
||||
#include <QRegularExpression>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QStandardPaths>
|
||||
#include <QFile>
|
||||
|
||||
SpellCheckDictionaryListModel::SpellCheckDictionaryListModel(AppSettingsManager* settingsManager,
|
||||
ConnectivityMonitor* cm,
|
||||
QObject* parent)
|
||||
: QAbstractListModel(parent)
|
||||
, spellCheckFileDownloader_(new FileDownloader(cm, this))
|
||||
, settingsManager_(settingsManager)
|
||||
{
|
||||
// Connect FileDownloader signals
|
||||
connect(spellCheckFileDownloader_,
|
||||
&FileDownloader::downloadFileSuccessful,
|
||||
this,
|
||||
&SpellCheckDictionaryListModel::onDownloadFileFinished);
|
||||
connect(spellCheckFileDownloader_,
|
||||
&FileDownloader::downloadFileFailed,
|
||||
this,
|
||||
&SpellCheckDictionaryListModel::onDownloadFileFailed);
|
||||
|
||||
// Initialize the model with available dictionaries and check if dictionaries are available
|
||||
// This will determine whether we need to notify the UI about a new available dictionary,
|
||||
// which is important because we want SpellCheckAdapter to be able to set the dictionary path
|
||||
// but only when dictionaries are available after initialization, and not on every download.
|
||||
dictionariesAvailable_ = populateDictionaries() > 0;
|
||||
|
||||
// First, correct/migrate a bad setting that may have been set in the past
|
||||
auto spellLangLocale = settingsManager_->getValue(Settings::Key::SpellLang).toString();
|
||||
auto currentLocale = settingsManager_->getLanguage();
|
||||
if (spellLangLocale.isEmpty() || !isLocaleInstalled(spellLangLocale)) {
|
||||
C_WARN << "Spell check language setting is empty or invalid, resetting to current locale";
|
||||
settingsManager_->setValue(Settings::Key::SpellLang, currentLocale);
|
||||
installDictionary(currentLocale);
|
||||
}
|
||||
}
|
||||
|
||||
QString
|
||||
SpellCheckDictionaryListModel::pathForLocale(const QString& locale) const
|
||||
{
|
||||
// Find the dictionary in the model and construct the path based on the locale and the
|
||||
// dictionary type (Jami-install or system)
|
||||
const auto index = getDictionaryIndex(locale);
|
||||
if (!index.isValid()) {
|
||||
return {};
|
||||
}
|
||||
const auto& dictObj = dictionaries_.at(index.row()).toObject();
|
||||
if (dictObj.value("isSystem").toBool()) {
|
||||
return systemDictionariesPath_ + locale;
|
||||
}
|
||||
return dictionariesPath_ + locale;
|
||||
}
|
||||
|
||||
int
|
||||
SpellCheckDictionaryListModel::rowCount(const QModelIndex& parent) const
|
||||
{
|
||||
return dictionaries_.size();
|
||||
}
|
||||
|
||||
QVariant
|
||||
SpellCheckDictionaryListModel::data(const QModelIndex& index, int role) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return {};
|
||||
// Try to find the item at the index
|
||||
const auto& item = dictionaries_.at(index.row());
|
||||
if (!item.isObject())
|
||||
return {};
|
||||
// Try to convert the item to a QJsonObject
|
||||
const auto& itemObject = item.toObject().toVariantMap();
|
||||
switch (role) {
|
||||
case Role::NativeName:
|
||||
return itemObject.value("nativeName");
|
||||
case Role::Path:
|
||||
return itemObject.value("path");
|
||||
case Role::Locale:
|
||||
return itemObject.value("locale");
|
||||
case Role::Installed:
|
||||
return itemObject.value("installed").toBool();
|
||||
case Role::Downloading:
|
||||
return pendingDownloads_.contains(itemObject.value("locale").toString());
|
||||
case Role::IsSystem:
|
||||
return itemObject.value("isSystem").toBool();
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
QHash<int, QByteArray>
|
||||
SpellCheckDictionaryListModel::roleNames() const
|
||||
{
|
||||
using namespace SpellCheckDictionaryList;
|
||||
QHash<int, QByteArray> roles;
|
||||
#define X(role) roles[role] = #role;
|
||||
SPELL_CHECK_DICTIONARY_MODEL_ROLES
|
||||
#undef X
|
||||
return roles;
|
||||
}
|
||||
|
||||
int
|
||||
SpellCheckDictionaryListModel::populateDictionaries()
|
||||
{
|
||||
// First, we need to get the list of available dictionaries.
|
||||
QFile availableDictionariesFile(":/misc/available_dictionaries.json");
|
||||
if (!availableDictionariesFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
C_WARN << "Available Dictionaries file failed to load";
|
||||
return 0;
|
||||
}
|
||||
const auto availableDictionaries = QString(availableDictionariesFile.readAll());
|
||||
QJsonDocument doc = QJsonDocument::fromJson(availableDictionaries.toUtf8());
|
||||
/*
|
||||
The file is a JSON object with the following structure:
|
||||
{
|
||||
"af_ZA": {
|
||||
"nativeName": "Afrikaans (Suid-Afrika)",
|
||||
"path": "af_ZA/af_ZA"
|
||||
},
|
||||
...
|
||||
}
|
||||
We want to convert it to a QJsonArray of QJsonObjects, each containing the locale,
|
||||
nativeName, path, and installed status.
|
||||
*/
|
||||
if (doc.isNull() || !doc.isObject()) {
|
||||
C_WARN.noquote() << "Available Dictionaries file is not a valid JSON object";
|
||||
return 0;
|
||||
}
|
||||
|
||||
beginResetModel();
|
||||
dictionaries_ = QJsonArray();
|
||||
|
||||
const auto object = doc.object();
|
||||
|
||||
// Get installed dictionaries to check status
|
||||
QString hunspellDataDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation)
|
||||
+ "/dictionaries/";
|
||||
QDir dictionariesDir(hunspellDataDir);
|
||||
QRegExp regex("(.*).dic");
|
||||
QStringList installedLocales;
|
||||
|
||||
// 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) {
|
||||
installedLocales << captured[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Check for dictionary files in the system directory
|
||||
QStringList systemDicFiles = QDir(systemDictionariesPath_)
|
||||
.entryList(QStringList() << "*.dic", QDir::Files);
|
||||
for (const auto& dicFile : systemDicFiles) {
|
||||
regex.indexIn(dicFile);
|
||||
auto captured = regex.capturedTexts();
|
||||
if (captured.size() == 2) {
|
||||
installedLocales << captured[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Add the dictionaries to the model with the appropriate flags
|
||||
for (const auto& key : object.keys()) {
|
||||
const auto valueObject = object.value(key).toObject();
|
||||
bool isSystem = systemDicFiles.contains(key + ".dic");
|
||||
bool isInstalled = installedLocales.contains(key);
|
||||
dictionaries_.append(QJsonObject {{"locale", key},
|
||||
{"nativeName", valueObject.value("nativeName").toString()},
|
||||
{"path", valueObject.value("path").toString()},
|
||||
{"installed", isInstalled},
|
||||
{"isSystem", isSystem}});
|
||||
}
|
||||
endResetModel();
|
||||
return installedLocales.size();
|
||||
}
|
||||
|
||||
Q_INVOKABLE void
|
||||
SpellCheckDictionaryListModel::installDictionary(const QString& locale)
|
||||
{
|
||||
// Check if dictionary is already installed
|
||||
const auto index = getDictionaryIndex(locale);
|
||||
if (!index.isValid()) {
|
||||
C_WARN << "Dictionary not found for locale:" << locale;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if already installed
|
||||
auto dictObj = dictionaries_.at(index.row()).toObject();
|
||||
if (dictObj.value("installed").toBool()) {
|
||||
C_WARN << "Dictionary already installed for locale:" << locale;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if download is already in progress
|
||||
if (pendingDownloads_.contains(locale)) {
|
||||
C_WARN << "Download already in progress for locale:" << locale;
|
||||
return;
|
||||
}
|
||||
|
||||
// Start the download process
|
||||
downloadDictionaryFiles(locale);
|
||||
}
|
||||
|
||||
Q_INVOKABLE void
|
||||
SpellCheckDictionaryListModel::uninstallDictionary(const QString& locale)
|
||||
{
|
||||
const auto index = getDictionaryIndex(locale);
|
||||
if (!index.isValid()) {
|
||||
C_WARN << "Dictionary not found for locale:" << locale;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if dictionary is actually installed
|
||||
auto dictObj = dictionaries_.at(index.row()).toObject();
|
||||
if (!dictObj.value("installed").toBool()) {
|
||||
C_WARN << "Dictionary not installed for locale:" << locale;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the dictionary is a system dictionary (cannot be uninstalled here)
|
||||
if (dictObj.value("isSystem").toBool()) {
|
||||
C_WARN << "Cannot uninstall system dictionary:" << locale;
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete the dictionary files
|
||||
QString affFile = dictionariesPath_ + locale + ".aff";
|
||||
QString dicFile = dictionariesPath_ + locale + ".dic";
|
||||
|
||||
bool affDeleted = true;
|
||||
bool dicDeleted = true;
|
||||
|
||||
if (QFile::exists(affFile)) {
|
||||
affDeleted = QFile::remove(affFile);
|
||||
if (!affDeleted) {
|
||||
C_WARN << "Failed to delete .aff file:" << affFile;
|
||||
}
|
||||
}
|
||||
|
||||
if (QFile::exists(dicFile)) {
|
||||
dicDeleted = QFile::remove(dicFile);
|
||||
if (!dicDeleted) {
|
||||
C_WARN << "Failed to delete .dic file:" << dicFile;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the installation status regardless of file deletion success
|
||||
// Note: This ensures the UI reflects the uninstall attempt only
|
||||
updateDictionaryInstallationStatus(locale, false);
|
||||
|
||||
if (affDeleted && dicDeleted) {
|
||||
C_DBG << "Dictionary uninstalled successfully for locale:" << locale;
|
||||
Q_EMIT uninstallFinished(locale);
|
||||
} else {
|
||||
C_WARN << "Dictionary uninstall completed with errors for locale:" << locale;
|
||||
Q_EMIT uninstallFailed(locale);
|
||||
}
|
||||
}
|
||||
|
||||
QVariantMap
|
||||
SpellCheckDictionaryListModel::getInstalledDictionaries() const
|
||||
{
|
||||
QVariantMap installedDictionaries;
|
||||
for (const auto& dict : dictionaries_) {
|
||||
const auto dictObj = dict.toObject();
|
||||
if (dictObj.value("installed").toBool()) {
|
||||
installedDictionaries.insert(dictObj.value("locale").toString(),
|
||||
dictObj.value("nativeName").toString());
|
||||
}
|
||||
}
|
||||
return installedDictionaries;
|
||||
}
|
||||
|
||||
QModelIndex
|
||||
SpellCheckDictionaryListModel::getDictionaryIndex(const QString& locale) const
|
||||
{
|
||||
for (int i = 0; i < dictionaries_.size(); ++i) {
|
||||
if (dictionaries_.at(i).toObject().value("locale") == locale)
|
||||
return createIndex(i, 0);
|
||||
}
|
||||
return {}; // Not found
|
||||
}
|
||||
|
||||
bool
|
||||
SpellCheckDictionaryListModel::isLocaleInstalled(const QString& locale) const
|
||||
{
|
||||
// Iterate through the dictionaries to check if the locale is installed
|
||||
for (const auto& dict : dictionaries_) {
|
||||
if (dict.toObject().value("locale").toString() == locale) {
|
||||
return dict.toObject().value("installed").toBool();
|
||||
}
|
||||
}
|
||||
return false; // Locale not found
|
||||
}
|
||||
|
||||
void
|
||||
SpellCheckDictionaryListModel::downloadDictionaryFiles(const QString& locale)
|
||||
{
|
||||
C_DBG << "Downloading dictionary:" << locale;
|
||||
|
||||
// Find the dictionary info
|
||||
const auto index = getDictionaryIndex(locale);
|
||||
if (!index.isValid()) {
|
||||
C_WARN << "Cannot download: dictionary not found for locale:" << locale;
|
||||
Q_EMIT downloadFailed(locale);
|
||||
return;
|
||||
}
|
||||
|
||||
auto dictObj = dictionaries_.at(index.row()).toObject();
|
||||
QString basePath = dictObj.value("path").toString();
|
||||
|
||||
if (basePath.isEmpty()) {
|
||||
C_WARN << "Cannot download: invalid path for dictionary" << locale;
|
||||
Q_EMIT downloadFailed(locale);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add to pending downloads
|
||||
pendingDownloads_.append(locale);
|
||||
Q_EMIT dataChanged(index, index, {Role::Downloading});
|
||||
|
||||
// Create target directory if it doesn't exist
|
||||
QDir().mkpath(dictionariesPath_);
|
||||
|
||||
QString targetFile = dictionariesPath_ + locale;
|
||||
|
||||
// Construct URLs using the stored path
|
||||
QString baseUrl = downloadUrl_.toString();
|
||||
QUrl urlAff = baseUrl + "/" + basePath + ".aff";
|
||||
QUrl urlDic = baseUrl + "/" + basePath + ".dic";
|
||||
|
||||
C_DBG << "Downloading dictionary files for" << locale;
|
||||
|
||||
// Start downloads
|
||||
spellCheckFileDownloader_->downloadFile(urlAff, targetFile + ".aff");
|
||||
spellCheckFileDownloader_->downloadFile(urlDic, targetFile + ".dic");
|
||||
}
|
||||
|
||||
void
|
||||
SpellCheckDictionaryListModel::updateDictionaryInstallationStatus(const QString& locale,
|
||||
bool installed)
|
||||
{
|
||||
const auto index = getDictionaryIndex(locale);
|
||||
if (!index.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the dictionary object
|
||||
auto dictObj = dictionaries_.at(index.row()).toObject();
|
||||
dictObj["installed"] = installed;
|
||||
dictionaries_[index.row()] = dictObj;
|
||||
|
||||
// Emit data changed signal
|
||||
Q_EMIT dataChanged(index, index, {SpellCheckDictionaryList::Installed});
|
||||
}
|
||||
|
||||
void
|
||||
SpellCheckDictionaryListModel::notifyDownloadStateChanged(const QString& locale)
|
||||
{
|
||||
auto index = getDictionaryIndex(locale);
|
||||
if (index.isValid()) {
|
||||
Q_EMIT dataChanged(index, index, {Role::Downloading});
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
SpellCheckDictionaryListModel::onDownloadFileFinished(const QString& localPath)
|
||||
{
|
||||
C_DBG << "Download finished:" << localPath;
|
||||
|
||||
// Extract locale from file path
|
||||
QFileInfo fileInfo(localPath);
|
||||
QString locale = fileInfo.baseName();
|
||||
|
||||
static auto handleDownloadComplete = [this, &locale](const QString& localPath) {
|
||||
// Both files are now available, mark as installed
|
||||
updateDictionaryInstallationStatus(locale, true);
|
||||
pendingDownloads_.removeAll(locale);
|
||||
notifyDownloadStateChanged(locale);
|
||||
Q_EMIT downloadFinished(locale);
|
||||
C_DBG << "Dictionary installation completed for:" << locale;
|
||||
|
||||
// If we want to prevent installed dictionaries from being automatically set as the current
|
||||
// dictionary, then place the newDictionaryAvailable signal within the if block.
|
||||
Q_EMIT newDictionaryAvailable(locale);
|
||||
if (!dictionariesAvailable_) {
|
||||
dictionariesAvailable_ = true;
|
||||
}
|
||||
};
|
||||
|
||||
// Check if this is a .dic file and if the corresponding .aff file exists
|
||||
if (localPath.endsWith(".dic")) {
|
||||
QString affFilePath = localPath;
|
||||
affFilePath.chop(4); // Remove ".dic"
|
||||
affFilePath += ".aff";
|
||||
|
||||
if (QFile::exists(affFilePath)) {
|
||||
handleDownloadComplete(affFilePath);
|
||||
} else {
|
||||
C_DBG << "Waiting for .aff file for:" << locale;
|
||||
}
|
||||
} else if (localPath.endsWith(".aff")) {
|
||||
QString dicFilePath = localPath;
|
||||
dicFilePath.chop(4); // Remove ".aff"
|
||||
dicFilePath += ".dic";
|
||||
|
||||
if (QFile::exists(dicFilePath)) {
|
||||
handleDownloadComplete(dicFilePath);
|
||||
} else {
|
||||
C_DBG << "Waiting for .dic file for:" << locale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
SpellCheckDictionaryListModel::onDownloadFileFailed(const QString& localPath)
|
||||
{
|
||||
C_WARN << "Download failed for file:" << localPath;
|
||||
|
||||
// Extract locale from file path
|
||||
QFileInfo fileInfo(localPath);
|
||||
QString locale = fileInfo.baseName();
|
||||
|
||||
// Remove from pending downloads and emit failure signal
|
||||
pendingDownloads_.removeAll(locale);
|
||||
notifyDownloadStateChanged(locale);
|
||||
Q_EMIT downloadFailed(locale);
|
||||
}
|
128
src/app/spellcheckdictionarylistmodel.h
Normal file
128
src/app/spellcheckdictionarylistmodel.h
Normal file
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* 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 "connectivitymonitor.h"
|
||||
#include "filedownloader.h"
|
||||
#include "systemtray.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QApplication>
|
||||
#include <QQmlEngine>
|
||||
#include <QUrl>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QSystemTrayIcon>
|
||||
#include <QtCore/QLoggingCategory>
|
||||
#include <QAbstractListModel>
|
||||
#include <QJsonDocument>
|
||||
|
||||
#define SPELL_CHECK_DICTIONARY_MODEL_ROLES \
|
||||
X(NativeName) \
|
||||
X(Path) \
|
||||
X(Locale) \
|
||||
X(Installed) \
|
||||
X(Downloading) \
|
||||
X(IsSystem)
|
||||
|
||||
namespace SpellCheckDictionaryList {
|
||||
Q_NAMESPACE
|
||||
enum Role {
|
||||
DummyRole = Qt::UserRole + 1,
|
||||
#define X(role) role,
|
||||
SPELL_CHECK_DICTIONARY_MODEL_ROLES
|
||||
#undef X
|
||||
};
|
||||
Q_ENUM_NS(Role)
|
||||
} // namespace SpellCheckDictionaryList
|
||||
|
||||
class SpellCheckDictionaryListModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SpellCheckDictionaryListModel(AppSettingsManager* settingsManager,
|
||||
ConnectivityMonitor* cm,
|
||||
QObject* parent = nullptr);
|
||||
~SpellCheckDictionaryListModel() override = default;
|
||||
|
||||
// Construct the final path for a given locale. This could be either
|
||||
// a Jami-install or a system dictionary.
|
||||
QString pathForLocale(const QString& locale) const;
|
||||
|
||||
// QAbstractListModel interface
|
||||
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
// SpellCheckAdapter needs access to the dictionary management methods
|
||||
friend class SpellCheckAdapter;
|
||||
|
||||
Q_SIGNALS:
|
||||
void downloadFinished(const QString& locale);
|
||||
void downloadFailed(const QString& locale);
|
||||
void uninstallFinished(const QString& locale);
|
||||
void uninstallFailed(const QString& locale);
|
||||
|
||||
// When a new dictionary is available, emit the signal so we can use
|
||||
// it to set the current dictionary from the SpellCheckAdapter
|
||||
void newDictionaryAvailable(const QString& locale);
|
||||
|
||||
protected:
|
||||
using Role = SpellCheckDictionaryList::Role;
|
||||
|
||||
void installDictionary(const QString& locale);
|
||||
void uninstallDictionary(const QString& locale);
|
||||
|
||||
QVariantMap getInstalledDictionaries() const;
|
||||
|
||||
private Q_SLOTS:
|
||||
void onDownloadFileFinished(const QString& localPath);
|
||||
void onDownloadFileFailed(const QString& localPath);
|
||||
|
||||
private:
|
||||
QJsonArray dictionaries_; // Principal underlying data structure
|
||||
QStringList pendingDownloads_; // Used to track pending downloads and status
|
||||
bool dictionariesAvailable_ {false}; // Flag to indicate if dictionaries are available
|
||||
|
||||
int populateDictionaries(); // Returns number of installed dictionaries
|
||||
|
||||
// Utility to get the index of a dictionary
|
||||
QModelIndex getDictionaryIndex(const QString& locale) const;
|
||||
bool isLocaleInstalled(const QString& locale) const;
|
||||
|
||||
// Helper methods for download functionality
|
||||
void downloadDictionaryFiles(const QString& locale);
|
||||
void updateDictionaryInstallationStatus(const QString& locale, bool installed);
|
||||
void notifyDownloadStateChanged(const QString& locale);
|
||||
|
||||
// Dictionary file management and downloading
|
||||
FileDownloader* spellCheckFileDownloader_;
|
||||
const QUrl downloadUrl_ {"https://raw.githubusercontent.com/LibreOffice/dictionaries/master"};
|
||||
const QString dictionariesPath_ = QStandardPaths::writableLocation(QStandardPaths::CacheLocation)
|
||||
+ "/dictionaries/";
|
||||
#if defined(Q_OS_LINUX)
|
||||
const QString systemDictionariesPath_ = "/usr/share/hunspell/";
|
||||
#elif defined(Q_OS_MACOS)
|
||||
const QString systemDictionariesPath_ = "/Library/Spelling/";
|
||||
#else
|
||||
const QString systemDictionariesPath_ = "";
|
||||
#endif
|
||||
|
||||
AppSettingsManager* settingsManager_;
|
||||
};
|
|
@ -1,149 +0,0 @@
|
|||
/*
|
||||
* 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();
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
* 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();
|
||||
};
|
|
@ -29,10 +29,9 @@
|
|||
#include <QRegularExpression>
|
||||
#include <QRegularExpressionMatchIterator>
|
||||
|
||||
SpellChecker::SpellChecker(const QString& dictionaryPath)
|
||||
{
|
||||
replaceDictionary(dictionaryPath);
|
||||
}
|
||||
SpellChecker::SpellChecker()
|
||||
: hunspell_(new Hunspell("", ""))
|
||||
{}
|
||||
|
||||
bool
|
||||
SpellChecker::spell(const QString& word)
|
||||
|
@ -66,18 +65,18 @@ SpellChecker::put_word(const QString& word)
|
|||
hunspell_->add(codec_->fromUnicode(word).constData());
|
||||
}
|
||||
|
||||
void
|
||||
bool
|
||||
SpellChecker::replaceDictionary(const QString& dictionaryPath)
|
||||
{
|
||||
if (dictionaryPath == currentDictionaryPath_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
QByteArray dictFilePath = dictFile.toLocal8Bit();
|
||||
QByteArray affixFilePath = affixFile.toLocal8Bit();
|
||||
hunspell_.reset(new Hunspell(affixFilePath.constData(), dictFilePath.constData()));
|
||||
// detect encoding analyzing the SET option in the affix file
|
||||
encoding_ = "ISO8859-1";
|
||||
QFile _affixFile(affixFile);
|
||||
|
@ -94,6 +93,9 @@ SpellChecker::replaceDictionary(const QString& dictionaryPath)
|
|||
}
|
||||
|
||||
codec_ = QTextCodec::codecForName(this->encoding_.toLatin1().constData());
|
||||
|
||||
currentDictionaryPath_ = dictionaryPath;
|
||||
return true;
|
||||
}
|
||||
|
||||
QList<SpellChecker::WordInfo>
|
||||
|
@ -101,7 +103,7 @@ 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}+");
|
||||
QRegularExpression regex("\\p{L}+");
|
||||
QRegularExpressionMatchIterator iter = regex.globalMatch(text);
|
||||
|
||||
while (iter.hasNext()) {
|
||||
|
|
|
@ -19,16 +19,11 @@
|
|||
|
||||
#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>
|
||||
|
||||
|
@ -38,16 +33,17 @@ class SpellChecker : public QObject
|
|||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SpellChecker(const QString&);
|
||||
~SpellChecker() = default;
|
||||
void replaceDictionary(const QString& dictionaryPath);
|
||||
explicit SpellChecker();
|
||||
|
||||
bool 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 {
|
||||
struct WordInfo
|
||||
{
|
||||
QString word;
|
||||
int position;
|
||||
int length;
|
||||
|
@ -57,7 +53,10 @@ public:
|
|||
|
||||
private:
|
||||
void put_word(const QString& word);
|
||||
std::shared_ptr<Hunspell> hunspell_;
|
||||
|
||||
std::unique_ptr<Hunspell> hunspell_;
|
||||
|
||||
QString currentDictionaryPath_;
|
||||
QString encoding_;
|
||||
QTextCodec* codec_;
|
||||
};
|
||||
|
|
|
@ -627,6 +627,17 @@ Utils::getProjectCredits()
|
|||
"who want to be added to the list should contact us."));
|
||||
}
|
||||
|
||||
QString
|
||||
Utils::getAvailableDictionariesJson()
|
||||
{
|
||||
QFile availableDictionariesFile(":/misc/available_dictionaries.json");
|
||||
if (!availableDictionariesFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
qDebug().noquote() << "Available Dictionaries file failed to load";
|
||||
return {};
|
||||
}
|
||||
return QString(availableDictionariesFile.readAll());
|
||||
}
|
||||
|
||||
inline QString
|
||||
removeEndlines(const QString& str)
|
||||
{
|
||||
|
|
|
@ -68,6 +68,7 @@ QString GetISODate();
|
|||
QSize getRealSize(QScreen* screen);
|
||||
void forceDeleteAsync(const QString& path);
|
||||
QString getProjectCredits();
|
||||
QString getAvailableDictionariesJson();
|
||||
void removeOldVersions();
|
||||
|
||||
// LRC helpers
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
*/
|
||||
|
||||
#include "appsettingsmanager.h"
|
||||
#include "spellcheckdictionarymanager.h"
|
||||
#include "connectivitymonitor.h"
|
||||
#include "mainapplication.h"
|
||||
#include "previewengine.h"
|
||||
|
@ -46,13 +45,13 @@
|
|||
#endif
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
#define DATA_DIR "JAMI_DATA_HOME"
|
||||
#define CONFIG_DIR "JAMI_CONFIG_HOME"
|
||||
#define CACHE_DIR "JAMI_CACHE_HOME"
|
||||
#define DATA_DIR "JAMI_DATA_HOME"
|
||||
#define CONFIG_DIR "JAMI_CONFIG_HOME"
|
||||
#define CACHE_DIR "JAMI_CACHE_HOME"
|
||||
#else
|
||||
#define DATA_DIR "XDG_DATA_HOME"
|
||||
#define CONFIG_DIR "XDG_CONFIG_HOME"
|
||||
#define CACHE_DIR "XDG_CACHE_HOME"
|
||||
#define DATA_DIR "XDG_DATA_HOME"
|
||||
#define CONFIG_DIR "XDG_CONFIG_HOME"
|
||||
#define CACHE_DIR "XDG_CACHE_HOME"
|
||||
#endif
|
||||
|
||||
#include <atomic>
|
||||
|
@ -95,7 +94,6 @@ 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");
|
||||
|
||||
|
@ -154,7 +152,6 @@ public Q_SLOTS:
|
|||
lrcInstance_.get(),
|
||||
systemTray_.get(),
|
||||
settingsManager_.get(),
|
||||
spellCheckDictionaryManager_.get(),
|
||||
connectivityMonitor_.get(),
|
||||
previewEngine_.get(),
|
||||
&screenInfo_,
|
||||
|
@ -172,7 +169,6 @@ private:
|
|||
|
||||
QScopedPointer<ConnectivityMonitor> connectivityMonitor_;
|
||||
QScopedPointer<AppSettingsManager> settingsManager_;
|
||||
QScopedPointer<SpellCheckDictionaryManager> spellCheckDictionaryManager_;
|
||||
QScopedPointer<SystemTray> systemTray_;
|
||||
QScopedPointer<PreviewEngine> previewEngine_;
|
||||
ScreenInfo screenInfo_;
|
||||
|
|
Loading…
Add table
Reference in a new issue