1 /***********************************************************************
2 *
3 * Copyright (C) 2009, 2010, 2012, 2013, 2014, 2019 Graeme Gott <graeme@gottcode.org>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 *
18 ***********************************************************************/
19
20 #include "spell_checker.h"
21
22 #include "dictionary_ref.h"
23 #include "document.h"
24
25 #include <QAction>
26 #include <QDialogButtonBox>
27 #include <QDir>
28 #include <QFileInfo>
29 #include <QGridLayout>
30 #include <QLabel>
31 #include <QLineEdit>
32 #include <QListWidget>
33 #include <QMessageBox>
34 #include <QProgressDialog>
35 #include <QPushButton>
36 #include <QTextBlock>
37 #include <QTextEdit>
38
39 //-----------------------------------------------------------------------------
40
checkDocument(QTextEdit * document,DictionaryRef & dictionary)41 void SpellChecker::checkDocument(QTextEdit* document, DictionaryRef& dictionary)
42 {
43 SpellChecker* checker = new SpellChecker(document, dictionary);
44 checker->m_start_cursor = document->textCursor();
45 checker->m_cursor = checker->m_start_cursor;
46 checker->m_cursor.movePosition(QTextCursor::StartOfBlock);
47 checker->m_loop_available = checker->m_start_cursor.block().previous().isValid();
48 checker->show();
49 checker->check();
50 }
51
52 //-----------------------------------------------------------------------------
53
reject()54 void SpellChecker::reject()
55 {
56 m_document->setTextCursor(m_start_cursor);
57 Document* document = qobject_cast<Document*>(m_document->parentWidget());
58 if (document) {
59 document->centerCursor(true);
60 }
61 QDialog::reject();
62 }
63
64 //-----------------------------------------------------------------------------
65
suggestionChanged(QListWidgetItem * suggestion)66 void SpellChecker::suggestionChanged(QListWidgetItem* suggestion)
67 {
68 if (suggestion) {
69 m_suggestion->setText(suggestion->text());
70 }
71 }
72
73 //-----------------------------------------------------------------------------
74
add()75 void SpellChecker::add()
76 {
77 m_dictionary.addToPersonal(m_word);
78 ignore();
79 }
80
81 //-----------------------------------------------------------------------------
82
ignore()83 void SpellChecker::ignore()
84 {
85 check();
86 }
87
88 //-----------------------------------------------------------------------------
89
ignoreAll()90 void SpellChecker::ignoreAll()
91 {
92 m_ignored.append(m_word);
93 ignore();
94 }
95
96 //-----------------------------------------------------------------------------
97
change()98 void SpellChecker::change()
99 {
100 m_cursor.insertText(m_suggestion->text());
101 check();
102 }
103
104 //-----------------------------------------------------------------------------
105
changeAll()106 void SpellChecker::changeAll()
107 {
108 QString replacement = m_suggestion->text();
109
110 QTextCursor cursor = m_cursor;
111 cursor.movePosition(QTextCursor::Start);
112 forever {
113 cursor = m_document->document()->find(m_word, cursor, QTextDocument::FindCaseSensitively | QTextDocument::FindWholeWords);
114 if (!cursor.isNull()) {
115 cursor.insertText(replacement);
116 } else {
117 break;
118 }
119 }
120
121 check();
122 }
123
124 //-----------------------------------------------------------------------------
125
SpellChecker(QTextEdit * document,DictionaryRef & dictionary)126 SpellChecker::SpellChecker(QTextEdit* document, DictionaryRef& dictionary) :
127 QDialog(document->parentWidget(), Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint),
128 m_dictionary(dictionary),
129 m_document(document),
130 m_checked_blocks(1),
131 m_total_blocks(document->document()->blockCount()),
132 m_loop_available(true)
133 {
134 setWindowTitle(tr("Check Spelling"));
135 setWindowModality(Qt::WindowModal);
136 setAttribute(Qt::WA_DeleteOnClose);
137
138 // Create widgets
139 m_context = new QTextEdit(this);
140 m_context->setReadOnly(true);
141 #if (QT_VERSION >= QT_VERSION_CHECK(5,10,0))
142 m_context->setTabStopDistance(50);
143 #else
144 m_context->setTabStopWidth(50);
145 #endif
146 QPushButton* add_button = new QPushButton(tr("&Add"), this);
147 add_button->setAutoDefault(false);
148 connect(add_button, &QPushButton::clicked, this, &SpellChecker::add);
149 QPushButton* ignore_button = new QPushButton(tr("&Ignore"), this);
150 ignore_button->setAutoDefault(false);
151 connect(ignore_button, &QPushButton::clicked, this, &SpellChecker::ignore);
152 QPushButton* ignore_all_button = new QPushButton(tr("I&gnore All"), this);
153 ignore_all_button->setAutoDefault(false);
154 connect(ignore_all_button, &QPushButton::clicked, this, &SpellChecker::ignoreAll);
155
156 m_suggestion = new QLineEdit(this);
157 QPushButton* change_button = new QPushButton(tr("&Change"), this);
158 change_button->setAutoDefault(false);
159 connect(change_button, &QPushButton::clicked, this, &SpellChecker::change);
160 QPushButton* change_all_button = new QPushButton(tr("C&hange All"), this);
161 change_all_button->setAutoDefault(false);
162 connect(change_all_button, &QPushButton::clicked, this, &SpellChecker::changeAll);
163 m_suggestions = new QListWidget(this);
164 connect(m_suggestions, &QListWidget::currentItemChanged, this, &SpellChecker::suggestionChanged);
165
166 QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Close, Qt::Horizontal, this);
167 connect(buttons, &QDialogButtonBox::rejected, this, &SpellChecker::reject);
168
169 // Lay out dialog
170 QGridLayout* layout = new QGridLayout(this);
171 layout->setContentsMargins(12, 12, 12, 12);
172 layout->setSpacing(6);
173 layout->setColumnMinimumWidth(2, 6);
174
175 layout->addWidget(new QLabel(tr("Not in dictionary:"), this), 0, 0, 1, 2);
176 layout->addWidget(m_context, 1, 0, 3, 2);
177 layout->addWidget(add_button, 1, 3);
178 layout->addWidget(ignore_button, 2, 3);
179 layout->addWidget(ignore_all_button, 3, 3);
180
181 layout->setRowMinimumHeight(4, 12);
182
183 layout->addWidget(new QLabel(tr("Change to:"), this), 5, 0);
184 layout->addWidget(m_suggestion, 5, 1);
185 layout->addWidget(m_suggestions, 6, 0, 1, 2);
186 layout->addWidget(change_button, 5, 3);
187 layout->addWidget(change_all_button, 6, 3, Qt::AlignTop);
188
189 layout->setRowMinimumHeight(7, 12);
190 layout->addWidget(buttons, 8, 3);
191 }
192
193 //-----------------------------------------------------------------------------
194
check()195 void SpellChecker::check()
196 {
197 setDisabled(true);
198
199 QProgressDialog wait_dialog(tr("Checking spelling..."), tr("Cancel"), 0, m_total_blocks, this);
200 wait_dialog.setWindowTitle(tr("Please wait"));
201 wait_dialog.setValue(0);
202 wait_dialog.setWindowModality(Qt::WindowModal);
203 bool canceled = false;
204
205 forever {
206 // Update wait dialog
207 wait_dialog.setValue(m_checked_blocks);
208 if (wait_dialog.wasCanceled()) {
209 canceled = true;
210 break;
211 }
212
213 // Check current line
214 QTextBlock block = m_cursor.block();
215 QStringRef word = m_dictionary.check(block.text(), m_cursor.position() - block.position());
216 if (word.isNull()) {
217 if (block.next().isValid()) {
218 m_cursor.movePosition(QTextCursor::NextBlock);
219 ++m_checked_blocks;
220 if (m_checked_blocks < m_total_blocks) {
221 continue;
222 } else {
223 break;
224 }
225 } else if (m_loop_available) {
226 wait_dialog.reset();
227 if (QMessageBox::question(this, QString(), tr("Continue checking at beginning of file?"),
228 QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes) == QMessageBox::Yes) {
229 m_loop_available = false;
230 m_cursor.movePosition(QTextCursor::Start);
231 wait_dialog.setRange(0, m_total_blocks);
232 continue;
233 } else {
234 canceled = true;
235 break;
236 }
237 } else {
238 break;
239 }
240 }
241
242 // Select misspelled word
243 m_cursor.setPosition(word.position() + block.position());
244 m_cursor.setPosition(m_cursor.position() + word.length(), QTextCursor::KeepAnchor);
245 m_word = m_cursor.selectedText();
246
247 if (!m_ignored.contains(m_word)) {
248 wait_dialog.close();
249 setEnabled(true);
250
251 // Show misspelled word in context
252 QTextCursor cursor = m_cursor;
253 cursor.movePosition(QTextCursor::PreviousWord, QTextCursor::MoveAnchor, 10);
254 int end = m_cursor.position() - cursor.position();
255 int start = end - m_word.length();
256 cursor.movePosition(QTextCursor::NextWord, QTextCursor::KeepAnchor, 21);
257 QString context = cursor.selectedText();
258 context.insert(end, "</span>");
259 context.insert(start, "<span style=\"color: red;\">");
260 context.replace("\n", "</p><p>");
261 context.replace("\t", "<span style=\"white-space: pre;\">\t</span>");
262 context = "<p>" + context + "</p>";
263 m_context->setHtml(context);
264
265 // Show suggestions
266 m_suggestion->clear();
267 m_suggestions->clear();
268 QStringList words = m_dictionary.suggestions(m_word);
269 if (!words.isEmpty()) {
270 for (const QString& word : words) {
271 m_suggestions->addItem(word);
272 }
273 m_suggestions->setCurrentRow(0);
274 }
275
276 // Stop checking words
277 m_document->setTextCursor(m_cursor);
278 m_suggestion->setFocus();
279 return;
280 }
281 }
282
283 // Inform user of completed spell check
284 wait_dialog.close();
285 if (!canceled) {
286 QMessageBox::information(this, QString(), tr("Spell check complete."));
287 }
288 reject();
289 }
290
291 //-----------------------------------------------------------------------------
292