File indexing completed on 2025-04-27 03:58:40

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2023-08-16
0007  * Description : Spell-check Config widget.
0008  *
0009  * SPDX-FileCopyrightText: 2021-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
0010  *
0011  * SPDX-License-Identifier: GPL-2.0-or-later
0012  *
0013  * ============================================================ */
0014 
0015 #include "spellcheckconfig.h"
0016 #include "digikam_config.h"
0017 
0018 // Qt includes
0019 
0020 #include <QCheckBox>
0021 #include <QGridLayout>
0022 #include <QGroupBox>
0023 #include <QLabel>
0024 #include <QApplication>
0025 #include <QStyle>
0026 #include <QTreeWidget>
0027 #include <QHeaderView>
0028 #include <QPushButton>
0029 #include <QListWidget>
0030 #include <QLineEdit>
0031 #include <QComboBox>
0032 #include <QIcon>
0033 
0034 // KDE includes
0035 
0036 #include <klocalizedstring.h>
0037 
0038 #ifdef HAVE_SONNET
0039 
0040 #   include <sonnet/speller.h>
0041 using namespace Sonnet;
0042 
0043 #endif
0044 
0045 // Local includes
0046 
0047 #include "localizesettings.h"
0048 #include "altlangstredit.h"
0049 #include "digikam_debug.h"
0050 
0051 namespace Digikam
0052 {
0053 
0054 class Q_DECL_HIDDEN SpellCheckConfig::Private
0055 {
0056 public:
0057 
0058     explicit Private()
0059       : activeSpellCheck (nullptr),
0060         spellCheckLabel  (nullptr),
0061         languageLabel    (nullptr),
0062         languageCB       (nullptr),
0063         dictGroup        (nullptr),
0064         backGroup        (nullptr),
0065         ignoreWordsGroup (nullptr),
0066         dictList         (nullptr),
0067         backList         (nullptr),
0068         addWordButton    (nullptr),
0069         delWordButton    (nullptr),
0070         repWordButton    (nullptr),
0071         ignoreWordsBox   (nullptr),
0072         ignoreWordEdit   (nullptr)
0073     {
0074     }
0075 
0076     QCheckBox*   activeSpellCheck;
0077     QLabel*      spellCheckLabel;
0078     QLabel*      languageLabel;
0079     QComboBox*   languageCB;
0080 
0081     QGroupBox*   dictGroup;
0082     QGroupBox*   backGroup;
0083     QGroupBox*   ignoreWordsGroup;
0084 
0085     QTreeWidget* dictList;              ///< Dictionaries list
0086     QTreeWidget* backList;              ///< Backends list
0087     QPushButton* addWordButton;
0088     QPushButton* delWordButton;
0089     QPushButton* repWordButton;
0090 
0091     QListWidget* ignoreWordsBox;
0092 
0093     QLineEdit*   ignoreWordEdit;
0094 };
0095 
0096 SpellCheckConfig::SpellCheckConfig(QWidget* const parent)
0097     : QWidget(parent),
0098       d      (new Private)
0099 {
0100     const int spacing = qMin(QApplication::style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing),
0101                              QApplication::style()->pixelMetric(QStyle::PM_LayoutVerticalSpacing));
0102 
0103     // --------------------------------------------------------
0104 
0105     QGridLayout* const grid = new QGridLayout(this);
0106 
0107     d->activeSpellCheck     = new QCheckBox(this);
0108     d->activeSpellCheck->setText(i18nc("@option:check", "Activate spellcheck in background when entering text"));
0109 
0110     d->spellCheckLabel      = new QLabel(xi18nc("@info", "<para>Turn on this option to activate the background spellcheck "
0111                                                          "feature on captions, titles, and other text-edit widgets. "
0112                                                          "Spellcheck is able to auto-detect the current language used in "
0113                                                          "text and will propose alternative with miss-spelled words.</para>"
0114                                                          "<para>With entries where alternative language can be specified, the "
0115                                                          "contextual language will be used to parse text. Spellcheck "
0116                                                          "depends of open-source backends, including necessary dictionaries, "
0117                                                          "to operate sentence analysis in desired languages.</para>"), this);
0118     d->spellCheckLabel->setWordWrap(true);
0119 
0120     d->languageLabel = new QLabel(i18nc("@label", "Default Language:"), this);
0121     d->languageCB    = new QComboBox(this);
0122     d->languageCB->setSizeAdjustPolicy(QComboBox::AdjustToContents);
0123     d->languageCB->setWhatsThis(i18nc("@info: edit widget default language",
0124                                       "Select the default language here to use with \"x-default\""
0125                                       "value for the alternative language strings."));
0126 
0127     d->languageCB->addItem(i18n("Auto-detect"));
0128     d->languageCB->setItemData(0, i18nc("@info", "Detect automatically the language by parsing string content"), Qt::ToolTipRole);
0129     d->languageCB->insertSeparator(d->languageCB->count());
0130 
0131     Q_FOREACH (const QString& lg, AltLangStrEdit::allLanguagesRFC3066())
0132     {
0133         d->languageCB->addItem(lg, lg);
0134         d->languageCB->setItemData(d->languageCB->findText(lg), AltLangStrEdit::languageNameRFC3066(lg), Qt::ToolTipRole);
0135     }
0136 
0137     // ---
0138 
0139     d->dictGroup               = new QGroupBox(i18nc("@title", "Available Language Dictionaries"), this);
0140     QVBoxLayout* const dictlay = new QVBoxLayout();
0141     d->dictGroup->setLayout(dictlay);
0142 
0143     d->dictList                = new QTreeWidget(this);
0144     d->dictList->setRootIsDecorated(false);
0145     d->dictList->setItemsExpandable(false);
0146     d->dictList->setExpandsOnDoubleClick(false);
0147     d->dictList->setAlternatingRowColors(true);
0148     d->dictList->setSelectionMode(QAbstractItemView::NoSelection);
0149     d->dictList->setAllColumnsShowFocus(true);
0150     d->dictList->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
0151     d->dictList->setColumnCount(2);
0152     d->dictList->setHeaderLabels(QStringList() << i18nc("@title: dictionary language code", "Code")
0153                                                << i18nc("@title: dictionary language name", "Name"));
0154     d->dictList->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
0155     d->dictList->header()->setSectionResizeMode(1, QHeaderView::Stretch);
0156     dictlay->addWidget(d->dictList);
0157 
0158     // ---
0159 
0160     d->backGroup               = new QGroupBox(i18nc("@title", "Available Backends"), this);
0161     QVBoxLayout* const backlay = new QVBoxLayout();
0162     d->backGroup->setLayout(backlay);
0163 
0164     d->backList                = new QTreeWidget(this);
0165     d->backList->setRootIsDecorated(false);
0166     d->backList->setItemsExpandable(false);
0167     d->backList->setExpandsOnDoubleClick(false);
0168     d->backList->setAlternatingRowColors(true);
0169     d->backList->setSelectionMode(QAbstractItemView::NoSelection);
0170     d->backList->setAllColumnsShowFocus(true);
0171     d->backList->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
0172     d->backList->setColumnCount(1);
0173     d->backList->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
0174     d->backList->header()->setVisible(false);
0175     backlay->addWidget(d->backList);
0176 
0177     // ---
0178 
0179     d->ignoreWordsGroup               = new QGroupBox(i18nc("@title", "Ignored Words"), this);
0180     QGridLayout* const ignoreWordsLay = new QGridLayout();
0181     d->ignoreWordsGroup->setLayout(ignoreWordsLay);
0182 
0183     d->ignoreWordEdit = new QLineEdit(this);
0184     d->ignoreWordEdit->setClearButtonEnabled(true);
0185     d->ignoreWordEdit->setPlaceholderText(i18nc("@info", "Set here a new word to ignore during spellcheck"));
0186 
0187     d->ignoreWordsBox = new QListWidget(this);
0188     d->ignoreWordsBox->setWhatsThis(i18nc("@info", "You can add or remove words to ignore "
0189                                           "while spell-checking operations."));
0190 
0191     d->ignoreWordsBox->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
0192 
0193     d->addWordButton = new QPushButton(i18nc("@action", "&Add..."),  this);
0194     d->delWordButton = new QPushButton(i18nc("@action", "&Remove"),  this);
0195     d->repWordButton = new QPushButton(i18nc("@action", "&Replace"), this);
0196 
0197     d->addWordButton->setIcon(QIcon::fromTheme(QLatin1String("list-add")));
0198     d->delWordButton->setIcon(QIcon::fromTheme(QLatin1String("list-remove")));
0199     d->repWordButton->setIcon(QIcon::fromTheme(QLatin1String("view-refresh")));
0200     d->delWordButton->setEnabled(false);
0201     d->repWordButton->setEnabled(false);
0202 
0203     ignoreWordsLay->setAlignment(Qt::AlignTop);
0204     ignoreWordsLay->addWidget(d->ignoreWordEdit, 0, 0, 1, 1);
0205     ignoreWordsLay->addWidget(d->ignoreWordsBox, 1, 0, 5, 1);
0206     ignoreWordsLay->addWidget(d->addWordButton,  1, 1, 1, 1);
0207     ignoreWordsLay->addWidget(d->delWordButton,  2, 1, 1, 1);
0208     ignoreWordsLay->addWidget(d->repWordButton,  3, 1, 1, 1);
0209     ignoreWordsLay->setRowStretch(4, 10);
0210     ignoreWordsLay->setColumnStretch(0, 10);
0211     ignoreWordsLay->setContentsMargins(spacing, spacing, spacing, spacing);
0212     ignoreWordsLay->setSpacing(spacing);
0213 
0214     // --------------------------------------------------------
0215 
0216     connect(d->activeSpellCheck, SIGNAL(toggled(bool)),
0217             this, SLOT(slotSpellcheckActivated(bool)));
0218 
0219     connect(d->ignoreWordsBox, SIGNAL(itemSelectionChanged()),
0220             this, SLOT(slotIgnoreWordSelectionChanged()));
0221 
0222     connect(d->addWordButton, SIGNAL(clicked()),
0223             this, SLOT(slotAddWord()));
0224 
0225     connect(d->delWordButton, SIGNAL(clicked()),
0226             this, SLOT(slotDelWord()));
0227 
0228     connect(d->repWordButton, SIGNAL(clicked()),
0229             this, SLOT(slotRepWord()));
0230 
0231     // ---
0232 
0233 #ifdef HAVE_SONNET
0234 
0235     Speller dict;
0236     QMap<QString, QString> map = dict.availableDictionaries();
0237 
0238     for (QMap<QString, QString>::const_iterator it = map.constBegin() ; it != map.constEnd() ; ++it)
0239     {
0240         new QTreeWidgetItem(d->dictList, QStringList() << it.value() << it.key());
0241     }
0242 
0243     Q_FOREACH (const QString& b, dict.availableBackends())
0244     {
0245         new QTreeWidgetItem(d->backList, QStringList() << b);
0246     }
0247 
0248 #endif
0249 
0250     // ---
0251 
0252     grid->setAlignment(Qt::AlignTop);
0253     grid->addWidget(d->activeSpellCheck, 0, 0, 1, 2);
0254     grid->addWidget(d->spellCheckLabel,  1, 0, 1, 2);
0255     grid->addWidget(d->languageLabel,    2, 0, 1, 1);
0256     grid->addWidget(d->languageCB,       2, 1, 1, 1);
0257     grid->addWidget(d->dictGroup,        3, 0, 1, 1);
0258     grid->addWidget(d->backGroup,        3, 1, 1, 1);
0259     grid->addWidget(d->ignoreWordsGroup, 4, 0, 1, 2);
0260     grid->setRowStretch(3, 10);
0261     grid->setColumnStretch(0, 10);
0262     grid->setContentsMargins(spacing, spacing, spacing, spacing);
0263     grid->setSpacing(spacing);
0264 
0265     // --------------------------------------------------------
0266 
0267     readSettings();
0268 }
0269 
0270 SpellCheckConfig::~SpellCheckConfig()
0271 {
0272     delete d;
0273 }
0274 
0275 void SpellCheckConfig::applySettings()
0276 {
0277     LocalizeSettings* const config = LocalizeSettings::instance();
0278 
0279     if (!config)
0280     {
0281         return;
0282     }
0283 
0284     LocalizeContainer set;
0285 
0286     set.enableSpellCheck = d->activeSpellCheck->isChecked();
0287     set.defaultLanguage  = d->languageCB->currentData().toString();
0288 
0289     QStringList ignoredWords;
0290 
0291     for (int i = 0 ; i < d->ignoreWordsBox->count() ; ++i)
0292     {
0293         QListWidgetItem* const item = d->ignoreWordsBox->item(i);
0294         ignoredWords.append(item->text());
0295     }
0296 
0297     set.ignoredWords = ignoredWords;
0298 
0299     qCDebug(DIGIKAM_WIDGETS_LOG) << set;
0300 
0301     config->setSettings(set, LocalizeSettings::SpellCheckConfig);
0302 }
0303 
0304 void SpellCheckConfig::readSettings()
0305 {
0306     LocalizeSettings* const config = LocalizeSettings::instance();
0307 
0308     if (!config)
0309     {
0310         return;
0311     }
0312 
0313     LocalizeContainer set          = config->settings();
0314 
0315     d->activeSpellCheck->setChecked(set.enableSpellCheck);
0316     d->languageCB->setCurrentIndex(set.defaultLanguage.isEmpty() ? 0 : d->languageCB->findData(set.defaultLanguage));
0317     d->ignoreWordsBox->insertItems(0, set.ignoredWords);
0318 
0319     slotSpellcheckActivated(d->activeSpellCheck->isChecked());
0320 }
0321 
0322 void SpellCheckConfig::slotSpellcheckActivated(bool b)
0323 {
0324     d->spellCheckLabel->setEnabled(b);
0325     d->languageLabel->setEnabled(b);
0326     d->languageCB->setEnabled(b);
0327     d->dictGroup->setEnabled(b);
0328     d->backGroup->setEnabled(b);
0329     d->ignoreWordsGroup->setEnabled(b);
0330 }
0331 
0332 void SpellCheckConfig::slotDelWord()
0333 {
0334     QListWidgetItem* const item = d->ignoreWordsBox->currentItem();
0335 
0336     if (!item)
0337     {
0338         return;
0339     }
0340 
0341     d->ignoreWordsBox->takeItem(d->ignoreWordsBox->row(item));
0342     delete item;
0343 }
0344 
0345 void SpellCheckConfig::slotRepWord()
0346 {
0347     QString newWord = d->ignoreWordEdit->text();
0348 
0349     if (newWord.isEmpty())
0350     {
0351         return;
0352     }
0353 
0354     if (!d->ignoreWordsBox->selectedItems().isEmpty())
0355     {
0356         d->ignoreWordsBox->selectedItems().at(0)->setText(newWord);
0357         d->ignoreWordEdit->clear();
0358     }
0359 }
0360 
0361 void SpellCheckConfig::slotIgnoreWordSelectionChanged()
0362 {
0363     if (!d->ignoreWordsBox->selectedItems().isEmpty())
0364     {
0365         d->ignoreWordEdit->setText(d->ignoreWordsBox->selectedItems().at(0)->text());
0366         d->delWordButton->setEnabled(true);
0367         d->repWordButton->setEnabled(true);
0368     }
0369     else
0370     {
0371         d->delWordButton->setEnabled(false);
0372         d->repWordButton->setEnabled(false);
0373     }
0374 }
0375 
0376 void SpellCheckConfig::slotAddWord()
0377 {
0378     QString newWord = d->ignoreWordEdit->text();
0379 
0380     if (newWord.isEmpty())
0381     {
0382         return;
0383     }
0384 
0385     bool found = false;
0386 
0387     for (int i = 0 ; i < d->ignoreWordsBox->count() ; ++i)
0388     {
0389         QListWidgetItem* const item = d->ignoreWordsBox->item(i);
0390 
0391         if (newWord == item->text())
0392         {
0393             found = true;
0394             break;
0395         }
0396     }
0397 
0398     if (!found)
0399     {
0400         d->ignoreWordsBox->insertItem(d->ignoreWordsBox->count(), newWord);
0401         d->ignoreWordEdit->clear();
0402     }
0403 }
0404 
0405 } // namespace Digikam
0406 
0407 #include "moc_spellcheckconfig.cpp"