1
0
Fork 0
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:
pmagnier-slimani 2025-05-12 10:46:10 -04:00 committed by Andreas Traczyk
parent 1ac3db4f33
commit ceec1f95b9
36 changed files with 1886 additions and 595 deletions

2
3rdparty/hunspell vendored

@ -1 +1 @@
Subproject commit 525f9f22766a28e0f81c435217fcf4528e01c013
Subproject commit 749cd84a0b7863fd8663b85c65abd8eca55d342d

View file

@ -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)

View 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"
}
}

View file

@ -292,9 +292,9 @@ ApplicationWindow {
}
}
Connections {
Connections {
target: UtilsAdapter
function onRaiseWhenCalled() {
function onRaiseWhenCalledChanged() {
raiseWhenCalled = AppSettingsManager.getValue(Settings.RaiseWhenCalled);
}
}

View file

@ -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) \

View file

@ -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

View 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
}
}
}
}
}

View file

@ -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();

View file

@ -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
}
}

View file

@ -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

View file

@ -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
}

View file

@ -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);
}

View file

@ -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

View file

@ -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_,

View file

@ -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_;

View file

@ -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;

View file

@ -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);
}

View file

@ -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};
};

View file

@ -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")
}

View file

@ -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");

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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)
}

View file

@ -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 {

View 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();
}
}

View 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};
};

View 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);
}

View 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_;
};

View file

@ -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();
}

View file

@ -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();
};

View file

@ -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()) {

View file

@ -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_;
};

View file

@ -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)
{

View file

@ -68,6 +68,7 @@ QString GetISODate();
QSize getRealSize(QScreen* screen);
void forceDeleteAsync(const QString& path);
QString getProjectCredits();
QString getAvailableDictionariesJson();
void removeOldVersions();
// LRC helpers

View file

@ -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_;