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