1 /************************************************************************
2 **
3 **  Copyright (C) 2015-2021 Kevin B. Hendricks, Stratford, Ontario, Canada
4 **  Copyright (C) 2013      Dave Heiland
5 **  Copyright (C) 2009-2011 Strahinja Markovic  <strahinja.markovic@gmail.com>
6 **
7 **  This file is part of Sigil.
8 **
9 **  Sigil is free software: you can redistribute it and/or modify
10 **  it under the terms of the GNU General Public License as published by
11 **  the Free Software Foundation, either version 3 of the License, or
12 **  (at your option) any later version.
13 **
14 **  Sigil is distributed in the hope that it will be useful,
15 **  but WITHOUT ANY WARRANTY; without even the implied warranty of
16 **  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 **  GNU General Public License for more details.
18 **
19 **  You should have received a copy of the GNU General Public License
20 **  along with Sigil.  If not, see <http://www.gnu.org/licenses/>.
21 **
22 *************************************************************************/
23 
24 #include <QtCore/QDir>
25 #include <QtCore/QFile>
26 #include <QtCore/QTextStream>
27 #include <QtCore/QUrl>
28 #include <QtGui/QDesktopServices>
29 #include <QtWidgets/QInputDialog>
30 #include <QtWidgets/QMessageBox>
31 
32 #include "SpellCheckWidget.h"
33 #include "Misc/Language.h"
34 #include "Misc/SettingsStore.h"
35 #include "Misc/SpellCheck.h"
36 #include "Misc/Utility.h"
37 
38 const QString DEFAULT_DICTIONARY_NAME = "default";
39 
SpellCheckWidget()40 SpellCheckWidget::SpellCheckWidget()
41     :
42     m_isDirty(false)
43 {
44     ui.setupUi(this);
45     setUpTable();
46     readSettings();
47     connectSignalsToSlots();
48 }
49 
setUpTable()50 void SpellCheckWidget::setUpTable()
51 {
52     QStringList header;
53     header.append(tr("Enable"));
54     header.append(tr("Dictionary"));
55     m_Model.setHorizontalHeaderLabels(header);
56     ui.userDictList->setModel(&m_Model);
57     // Make the header fill all the available space
58     ui.userDictList->horizontalHeader()->setStretchLastSection(true);
59     ui.userDictList->resizeColumnToContents(0);
60     ui.userDictList->verticalHeader()->setVisible(false);
61     ui.userDictList->setSortingEnabled(false);
62     ui.userDictList->setSelectionBehavior(QAbstractItemView::SelectRows);
63     ui.userDictList->setSelectionMode(QAbstractItemView::SingleSelection);
64     ui.userDictList->setAlternatingRowColors(true);
65 }
66 
saveSettings()67 PreferencesWidget::ResultActions SpellCheckWidget::saveSettings()
68 {
69     PreferencesWidget::ResultActions results = PreferencesWidget::ResultAction_None;
70 
71     if (!m_isDirty) {
72         return results;
73     }
74 
75     // Save the current dictionary's word list
76     if (ui.userDictList->selectionModel()->hasSelection()) {
77         int row = ui.userDictList->selectionModel()->selectedIndexes().first().row();
78         QStandardItem *item = m_Model.item(row, 1);
79         QString name = item->text();
80         saveUserDictionaryWordList(name);
81     }
82 
83     // Save dictionary information
84     SettingsStore settings;
85     settings.setEnabledUserDictionaries(EnabledDictionaries());
86     settings.setDefaultUserDictionary(ui.defaultUserDictionary->text());
87     settings.setDictionary(ui.dictionaries->itemData(ui.dictionaries->currentIndex()).toString());
88     settings.setSecondaryDictionary(ui.dictionaries->itemData(ui.dictionaries2d->currentIndex()).toString());
89     settings.setSpellCheck(ui.HighlightMisspelled->checkState() == Qt::Checked);
90     settings.setSpellCheckNumbers(ui.CheckNumbers->checkState() == Qt::Checked);
91 
92     SpellCheck *sc = SpellCheck::instance();
93     sc->setDictionary(settings.dictionary(), true);
94     if (!settings.secondary_dictionary().isEmpty()) {
95         sc->setDictionary(settings.secondary_dictionary(), true);
96     }
97     sc->UpdateLangCodeToDictMapping();
98 
99     results = results | PreferencesWidget::ResultAction_RefreshSpelling;
100     results = results & PreferencesWidget::ResultAction_Mask;
101     return results;
102 }
103 
EnabledDictionaries()104 QStringList SpellCheckWidget::EnabledDictionaries()
105 {
106     QStringList enabled_dicts;
107     for (int row = 0; row < m_Model.rowCount(); ++row) {
108         QStandardItem *item = m_Model.itemFromIndex(m_Model.index(row, 0));
109         if (item->checkState() == Qt::Checked) {
110             QStandardItem *name_item = m_Model.itemFromIndex(m_Model.index(row, 1));
111             enabled_dicts.append(name_item->text());
112         }
113     }
114     return enabled_dicts;
115 }
116 
addUserDict()117 void SpellCheckWidget::addUserDict()
118 {
119     QString name = QInputDialog::getText(this, tr("Add Dictionary"), tr("Name:"));
120 
121     if (name.isEmpty()) {
122         return;
123     }
124 
125     QStringList currentDicts;
126 
127     for (int row = 0; row < m_Model.rowCount(); ++row) {
128         QStandardItem *item = m_Model.itemFromIndex(m_Model.index(row, 1));
129         currentDicts << item->text();
130     }
131 
132     if (currentDicts.contains(name, Qt::CaseInsensitive)) {
133         QMessageBox::critical(this, tr("Error"), tr("A user dictionary already exists with this name!"));
134         return;
135     }
136 
137     createUserDict(name);
138 }
139 
addUserWords()140 void SpellCheckWidget::addUserWords()
141 {
142     QString list = QInputDialog::getText(this, tr("Add Words"), tr("Words:"));
143 
144     if (list.isEmpty()) {
145         return;
146     }
147 
148     list.replace(" ", ",");
149     list.replace(",", "\n");
150     QStringList words = list.split("\n");
151 
152     // Add the words to the dictionary
153     foreach(QString word, words) {
154         if (!word.isEmpty()) {
155             QListWidgetItem *item = new QListWidgetItem(word, ui.userWordList);
156             item->setFlags(item->flags() | Qt::ItemIsEditable);
157             ui.userWordList->addItem(item);
158         }
159     }
160     ui.userWordList->sortItems(Qt::AscendingOrder);
161 
162     m_isDirty = true;
163 }
164 
createUserDict(QString dict_name)165 bool SpellCheckWidget::createUserDict(QString dict_name)
166 {
167     QString path = SpellCheck::userDictionaryDirectory() + "/" + dict_name;
168     QFile dict_file(path);
169 
170     if (dict_file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
171         dict_file.close();
172     } else {
173         QMessageBox::critical(this, tr("Error"), tr("Could not create file!"));
174         return false;
175     }
176 
177     addNewItem(true, dict_name);
178     ui.userDictList->sortByColumn(1, Qt::AscendingOrder);
179 
180     return true;
181 }
182 
addNewItem(bool enabled,QString dict_name)183 void SpellCheckWidget::addNewItem(bool enabled, QString dict_name)
184 {
185     QList<QStandardItem *> rowItems;
186     // Checkbox
187     QStandardItem *checkbox_item = new QStandardItem();
188     checkbox_item->setCheckable(true);
189     checkbox_item->setCheckState(Qt::Checked);
190     if (enabled) {
191         checkbox_item->setCheckState(Qt::Checked);
192     } else {
193         checkbox_item->setCheckState(Qt::Unchecked);
194     }
195     rowItems << checkbox_item;
196     // Filename
197     QStandardItem *file_item = new QStandardItem();
198     file_item->setText(dict_name);
199     file_item->setToolTip(dict_name);
200     rowItems << file_item;
201 
202     for (int i = 0; i < rowItems.count(); i++) {
203         rowItems[i]->setEditable(false);
204     }
205     m_Model.appendRow(rowItems);
206 
207     ui.userDictList->setCurrentIndex(file_item->index());
208 
209     m_isDirty = true;
210 }
211 
renameUserDict()212 void SpellCheckWidget::renameUserDict()
213 {
214     if (!ui.userDictList->selectionModel()->hasSelection()) {
215         return;
216     }
217 
218     int row = ui.userDictList->selectionModel()->selectedIndexes().first().row();
219     QStandardItem *item = m_Model.item(row, 1);
220     QString orig_name = item->text();
221 
222     QString new_name = QInputDialog::getText(this, tr("Rename"), tr("Name:"), QLineEdit::Normal, orig_name);
223 
224     if (new_name == orig_name || new_name.isEmpty()) {
225         return;
226     }
227 
228     QStringList currentDicts;
229     for (int row = 0; row < m_Model.rowCount(); ++row) {
230         QStandardItem *item= m_Model.itemFromIndex(m_Model.index(row, 1));
231         currentDicts << item->text();
232     }
233 
234     if (currentDicts.contains(new_name)) {
235         QMessageBox::critical(this, tr("Error"), tr("A user dictionary already exists with this name!"));
236         return;
237     }
238 
239     QString orig_path = SpellCheck::userDictionaryDirectory() + "/" + orig_name;
240     QString new_path = SpellCheck::userDictionaryDirectory() + "/" + new_name;
241 
242     if (!Utility::RenameFile(orig_path, new_path)) {
243         QMessageBox::critical(this, tr("Error"), tr("Could not rename file!"));
244         return;
245     }
246 
247     item->setText(new_name);
248     setDefaultUserDictionary(new_name);
249 
250     ui.userDictList->sortByColumn(1, Qt::AscendingOrder);
251     m_isDirty = true;
252 }
253 
removeUserDict()254 void SpellCheckWidget::removeUserDict()
255 {
256     if (!ui.userDictList->selectionModel()->hasSelection()) {
257         return;
258     }
259 
260     // Don't remove the last dictionary
261     if (m_Model.rowCount() == 1) {
262         QMessageBox::warning(this, tr("Error"), tr("You cannot delete the last dictionary."));
263         return;
264     }
265 
266     int row = ui.userDictList->selectionModel()->selectedIndexes().first().row();
267     QStandardItem *item = m_Model.item(row, 1);
268 
269     if (item) {
270         // Delete the dictionary and remove it from the list.
271         QString dict_name = item->text();
272         m_Model.removeRow(row);
273         Utility::SDeleteFile(SpellCheck::userDictionaryDirectory() + "/" + dict_name);
274     }
275 
276     m_isDirty = true;
277 }
278 
copyUserDict()279 void SpellCheckWidget::copyUserDict()
280 {
281     // Get the current dictionary.
282     if (!ui.userDictList->selectionModel()->hasSelection()) {
283         return;
284     }
285 
286     int row = ui.userDictList->selectionModel()->selectedIndexes().first().row();
287     QStandardItem *item = m_Model.item(row, 1);
288 
289     if (!item) {
290         return;
291     }
292 
293     // Get the current words, before creating so list doesn't change
294     QStringList words;
295     for (int i = 0; i < ui.userWordList->count(); ++i) {
296         QString word = ui.userWordList->item(i)->text();
297         words.append(word);
298     }
299 
300     // Create a new dictionary
301     QStringList current_dicts;
302     for (int row = 0; row < m_Model.rowCount(); ++row) {
303         QStandardItem *item = m_Model.itemFromIndex(m_Model.index(row, 1));
304         current_dicts.append(item->text());
305     }
306     QString dict_name = item->text();
307     while (current_dicts.contains(dict_name)) {
308         dict_name += "_copy";
309     }
310 
311     if (!createUserDict(dict_name)) {
312         return;
313     }
314 
315     // Add the words to the dictionary
316     foreach(QString word, words) {
317         QListWidgetItem *item = new QListWidgetItem(word, ui.userWordList);
318         item->setFlags(item->flags() | Qt::ItemIsEditable);
319         ui.userWordList->addItem(item);
320     }
321 
322     m_isDirty = true;
323 }
324 
editWord()325 void SpellCheckWidget::editWord()
326 {
327     QList<QListWidgetItem *> items = ui.userWordList->selectedItems();
328 
329     if (!items.empty()) {
330         ui.userWordList->editItem(items.at(0));
331     }
332 
333     m_isDirty = true;
334 }
335 
removeWord()336 void SpellCheckWidget::removeWord()
337 {
338     foreach(QListWidgetItem * item, ui.userWordList->selectedItems()) {
339         ui.userWordList->removeItemWidget(item);
340         delete item;
341         item = 0;
342     }
343     m_isDirty = true;
344 }
345 
removeAll()346 void SpellCheckWidget::removeAll()
347 {
348     ui.userWordList->clear();
349     m_isDirty = true;
350 }
351 
userWordChanged(QListWidgetItem * item)352 void SpellCheckWidget::userWordChanged(QListWidgetItem *item)
353 {
354     if (item) {
355         item->setText(item->text().replace(QChar(0x2019), QChar('\'')));
356     }
357 }
358 
readSettings()359 void SpellCheckWidget::readSettings()
360 {
361     // Load the available dictionary names.
362     Language *lang = Language::instance();
363     SpellCheck *sc = SpellCheck::instance();
364     QStringList dicts = sc->dictionaries();
365     ui.dictionaries->clear();
366     ui.dictionaries2d->clear();
367     foreach(QString dict, dicts) {
368         QString name;
369         QString fix_dict = dict;
370         fix_dict.replace("_", "-");
371         QStringList parts = fix_dict.split("-");
372         int n = parts.count();
373         if (n == 1) {
374             name = lang->GetLanguageName(fix_dict);
375         } else {
376             // try with the first two parts
377             fix_dict = parts.at(0) + "-" + parts.at(1);
378             name = lang->GetLanguageName(fix_dict);
379             if (!name.isEmpty()) {
380                 // append any extra information to end
381                 for(int j=2; j < n; j++) name.append(" - " + parts.at(j));
382             }
383             if (name.isEmpty()) {
384                 // try with just the first part
385                 name = lang->GetLanguageName(parts.at(0));
386                 if (!name.isEmpty()) {
387                     // append any extra information to end
388                     for(int j=1; j < n; j++) name.append(" - " + parts.at(j));
389                 }
390             }
391         }
392         if (name.isEmpty()) name = dict;
393         ui.dictionaries->addItem(name, dict);
394         ui.dictionaries2d->addItem(name, dict);
395     }
396     ui.dictionaries2d->addItem("", "");
397 
398     // Select the current dictionary.
399     QString currentDict = sc->currentPrimaryDictionary();
400     SettingsStore settings;
401 
402     if (!currentDict.isEmpty()) {
403         int index = ui.dictionaries->findData(currentDict);
404 
405         if (index > -1) {
406             ui.dictionaries->setCurrentIndex(index);
407         }
408     }
409     currentDict = settings.secondary_dictionary();
410     int index = ui.dictionaries2d->findData(currentDict);
411     if (index > -1) {
412         ui.dictionaries2d->setCurrentIndex(index);
413     }
414 
415     // Load the list of user dictionaries.
416     QDir userDictDir(SpellCheck::userDictionaryDirectory());
417     QStringList userDicts = userDictDir.entryList(QDir::Files | QDir::NoDotAndDotDot);
418     userDicts.sort();
419 
420     // Make sure at least one dictionary exists
421     // Should never happen since spellcheck creates one
422     if (userDicts.count() < 1) {
423         QFile defaultFile(SpellCheck::userDictionaryDirectory() + "/" + DEFAULT_DICTIONARY_NAME);
424 
425         if (defaultFile.open(QIODevice::WriteOnly)) {
426             defaultFile.close();
427         }
428         // Add and enable a default dictionary
429         addNewItem(true, DEFAULT_DICTIONARY_NAME);
430     }
431 
432     // Load the list of dictionary files into the UI, marking if enabled
433     QStringList enabled_dicts = settings.enabledUserDictionaries();
434     foreach(QString dict_name, userDicts) {
435         addNewItem(enabled_dicts.contains(dict_name), dict_name);
436     }
437 
438     // Get the default dictionary - it should always exist
439     setDefaultUserDictionary(SettingsStore().defaultUserDictionary());
440 
441     loadUserDictionaryWordList();
442 
443     // Set whether mispelled words are highlighted or not
444     ui.HighlightMisspelled->setChecked(settings.spellCheck());
445     ui.CheckNumbers->setChecked(settings.spellCheckNumbers());
446 
447     m_isDirty = false;
448 }
449 
loadUserDictionaryWordList(QString dict_name)450 void SpellCheckWidget::loadUserDictionaryWordList(QString dict_name)
451 {
452     ui.userWordList->clear();
453 
454     if (dict_name.isEmpty()) {
455         if (ui.userDictList->selectionModel()->hasSelection()) {
456             int row = ui.userDictList->selectionModel()->selectedIndexes().first().row();
457             QStandardItem *item = m_Model.item(row, 1);
458             dict_name = item->text();
459         }
460     }
461 
462     // This shouldn't happen but we want to prevent crashes just in case.
463     if (dict_name.isEmpty()) {
464         return;
465     }
466 
467     // We store the words in a list instead of loading them directly because
468     // we want to sort the list before loading the words.
469     QStringList words;
470     // Read each word from the user dictionary.
471     QFile userDictFile(SpellCheck::userDictionaryDirectory() + "/" + dict_name);
472 
473     if (userDictFile.open(QIODevice::ReadOnly)) {
474         QTextStream userDictStream(&userDictFile);
475         userDictStream.setCodec("UTF-8");
476         QString line;
477 
478         do {
479             line = userDictStream.readLine();
480 
481             if (!line.isEmpty()) {
482                 words << line;
483             }
484         } while (!line.isNull());
485 
486         userDictFile.close();
487     }
488 
489     words.sort();
490     // Load the words into the list.
491     foreach(QString word, words) {
492         QListWidgetItem *item = new QListWidgetItem(word, ui.userWordList);
493         item->setFlags(item->flags() | Qt::ItemIsEditable);
494         ui.userWordList->addItem(item);
495     }
496 }
497 
saveUserDictionaryWordList(QString dict_name)498 void SpellCheckWidget::saveUserDictionaryWordList(QString dict_name)
499 {
500     SettingsStore ss;
501     QString dict_path;
502 
503     if (dict_name.isEmpty()) {
504         return;
505     }
506 
507     dict_path = SpellCheck::userDictionaryDirectory() + "/" + dict_name;
508     // Get the word list
509     QSet<QString> unique_words;
510 
511     for (int i = 0; i < ui.userWordList->count(); ++i) {
512         QString word = ui.userWordList->item(i)->text();
513 
514         if (!word.isEmpty()) {
515             unique_words << word;
516         }
517     }
518 
519     QStringList words = unique_words.values();
520     words.sort();
521     // Replace words in the user dictionary.
522     QFile userDictFile(dict_path);
523 
524     if (userDictFile.open(QFile::WriteOnly | QFile::Truncate)) {
525         QTextStream userDictStream(&userDictFile);
526         userDictStream.setCodec("UTF-8");
527         foreach(QString word, words) {
528             userDictStream << word << "\n";
529         }
530         userDictFile.close();
531     }
532 }
533 
SelectionChanged(const QItemSelection & selected,const QItemSelection & deselected)534 void SpellCheckWidget::SelectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
535 {
536     if (deselected.indexes().count() > 1) {
537         QStandardItem *previous_item = m_Model.item(m_Model.itemFromIndex(deselected.indexes().first())->row(), 1);
538         if (previous_item) {
539             saveUserDictionaryWordList(previous_item->text());
540         }
541     }
542 
543     if (selected.indexes().count() > 1) {
544         QStandardItem *current_item = m_Model.item(m_Model.itemFromIndex(selected.indexes().first())->row(), 1);
545         if (current_item) {
546             loadUserDictionaryWordList(current_item->text());
547         }
548     }
549 
550     setDefaultUserDictionary();
551 
552     m_isDirty = true;
553 }
554 
setDefaultUserDictionary(QString dict_name)555 void SpellCheckWidget::setDefaultUserDictionary(QString dict_name)
556 {
557     if (m_Model.rowCount() < 1) {
558         return;
559     }
560 
561     // Highlight the dictionary if a specific name was given
562     if (!dict_name.isEmpty()) {
563         for (int row = 0; row < m_Model.rowCount(); ++row) {
564             QStandardItem *item = m_Model.itemFromIndex(m_Model.index(row, 1));
565             if (dict_name == item->text()) {
566                 ui.userDictList->setCurrentIndex(item->index());
567             }
568         }
569     }
570 
571     // Update the dictionary label to match the highlighted entry
572     // Default to the first entry if name is not matched
573     QStandardItem *item = m_Model.item(0, 1);
574     if (ui.userDictList->selectionModel()->hasSelection()) {
575         int row = ui.userDictList->selectionModel()->selectedIndexes().first().row();
576         item = m_Model.item(row, 1);
577     }
578     ui.defaultUserDictionary->setText(item->text());
579     SettingsStore settings;
580     settings.setDefaultUserDictionary(ui.defaultUserDictionary->text());
581 }
582 
dictionariesCurrentIndexChanged(int index)583 void SpellCheckWidget::dictionariesCurrentIndexChanged(int index)
584 {
585     m_isDirty = true;
586 }
587 
highlightChanged(int state)588 void SpellCheckWidget::highlightChanged(int state)
589 {
590     m_isDirty = true;
591 }
592 
checkNumbersChanged(int state)593 void SpellCheckWidget::checkNumbersChanged(int state)
594 {
595     m_isDirty = true;
596 }
597 
ItemChanged(QStandardItem * item)598 void SpellCheckWidget::ItemChanged(QStandardItem *item)
599 {
600     m_isDirty = true;
601 }
602 
603 
connectSignalsToSlots()604 void SpellCheckWidget::connectSignalsToSlots()
605 {
606     // User dict list.
607     connect(ui.addUserDict, SIGNAL(clicked()), this, SLOT(addUserDict()));
608     connect(ui.addUserWords, SIGNAL(clicked()), this, SLOT(addUserWords()));
609     connect(ui.renameUserDict, SIGNAL(clicked()), this, SLOT(renameUserDict()));
610     connect(ui.copyUserDict, SIGNAL(clicked()), this, SLOT(copyUserDict()));
611     connect(ui.removeUserDict, SIGNAL(clicked()), this, SLOT(removeUserDict()));
612     connect(ui.userWordList, SIGNAL(itemChanged(QListWidgetItem *)), this, SLOT(userWordChanged(QListWidgetItem *)));
613     // Word list.
614     connect(ui.editWord, SIGNAL(clicked()), this, SLOT(editWord()));
615     connect(ui.removeWord, SIGNAL(clicked()), this, SLOT(removeWord()));
616     connect(ui.removeAll, SIGNAL(clicked()), this, SLOT(removeAll()));
617     connect(ui.dictionaries, SIGNAL(currentIndexChanged(int)), this, SLOT(dictionariesCurrentIndexChanged(int)));
618     connect(ui.dictionaries2d, SIGNAL(currentIndexChanged(int)), this, SLOT(dictionariesCurrentIndexChanged(int)));
619     connect(ui.HighlightMisspelled, SIGNAL(stateChanged(int)), this, SLOT(highlightChanged(int)));
620     connect(ui.CheckNumbers, SIGNAL(stateChanged(int)), this, SLOT(checkNumbersChanged(int)));
621 
622     QItemSelectionModel *selectionModel = ui.userDictList->selectionModel();
623     connect(selectionModel,     SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection &)),
624             this,               SLOT(SelectionChanged(const QItemSelection &, const QItemSelection &)));
625     connect(&m_Model, SIGNAL(itemChanged(QStandardItem *)),
626             this,               SLOT(ItemChanged(QStandardItem *))
627            );
628 }
629