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