6 """ |
6 """ |
7 Module implementing QTextEdit and QPlainTextEdit widgets with embedded spell |
7 Module implementing QTextEdit and QPlainTextEdit widgets with embedded spell |
8 checking. |
8 checking. |
9 """ |
9 """ |
10 |
10 |
11 import sys |
11 import contextlib |
12 |
12 |
13 import enchant |
13 try: |
14 from enchant import tokenize |
14 import enchant |
15 from enchant.errors import TokenizerNotFoundError, DictNotFoundError |
15 import enchant.tokenize |
16 from enchant.utils import trim_suggestions |
16 from enchant.errors import TokenizerNotFoundError, DictNotFoundError |
|
17 from enchant.utils import trim_suggestions |
|
18 ENCHANT_AVAILABLE = True |
|
19 except ImportError: |
|
20 ENCHANT_AVAILABLE = False |
17 |
21 |
18 from PyQt6.QtCore import pyqtSlot, Qt, QCoreApplication |
22 from PyQt6.QtCore import pyqtSlot, Qt, QCoreApplication |
19 from PyQt6.QtGui import ( |
23 from PyQt6.QtGui import ( |
20 QAction, QActionGroup, QSyntaxHighlighter, QTextBlockUserData, |
24 QAction, QActionGroup, QSyntaxHighlighter, QTextBlockUserData, |
21 QTextCharFormat, QTextCursor |
25 QTextCharFormat, QTextCursor |
22 ) |
26 ) |
23 from PyQt6.QtWidgets import QMenu, QTextEdit, QPlainTextEdit |
27 from PyQt6.QtWidgets import QMenu, QTextEdit, QPlainTextEdit |
24 |
28 |
25 # TODO: add user dictionaries with respective menu entries |
29 # TODO: add user dictionaries with respective menu entries |
26 |
30 |
27 |
31 if ENCHANT_AVAILABLE: |
28 class SpellCheckMixin(): |
32 class SpellCheckMixin(): |
29 """ |
33 """ |
30 Class implementing the spell-check mixin for the widget classes. |
34 Class implementing the spell-check mixin for the widget classes. |
31 """ |
35 """ |
32 # don't show more than this to keep the menu manageable |
36 # don't show more than this to keep the menu manageable |
33 MaxSuggestions = 20 |
37 MaxSuggestions = 20 |
|
38 |
|
39 # default language to be used when no other is set |
|
40 DefaultLanguage = None |
|
41 |
|
42 def __init__(self): |
|
43 """ |
|
44 Constructor |
|
45 """ |
|
46 self.__highlighter = EnchantHighlighter(self.document()) |
|
47 try: |
|
48 # Start with a default dictionary based on the current locale. |
|
49 spellDict = ( |
|
50 enchant.Dict(SpellCheckMixin.DefaultLanguage) |
|
51 if bool(SpellCheckMixin.DefaultLanguage) else |
|
52 enchant.Dict() |
|
53 ) |
|
54 except DictNotFoundError: |
|
55 # Use English dictionary if no locale dictionary is available |
|
56 # or the default one could not be found. |
|
57 spellDict = enchant.Dict("en") |
|
58 self.__highlighter.setDict(spellDict) |
|
59 |
|
60 def contextMenuEvent(self, evt): |
|
61 """ |
|
62 Protected method to handle context menu events to add a spelling |
|
63 suggestions submenu. |
|
64 |
|
65 @param evt reference to the context menu event |
|
66 @type QContextMenuEvent |
|
67 """ |
|
68 menu = self.__createSpellcheckContextMenu(evt.pos()) |
|
69 menu.exec(evt.globalPos()) |
|
70 |
|
71 def __createSpellcheckContextMenu(self, pos): |
|
72 """ |
|
73 Private method to create the spell-check context menu. |
|
74 |
|
75 @param pos position of the mouse pointer |
|
76 @type QPoint |
|
77 @return context menu with additional spell-check entries |
|
78 @rtype QMenu |
|
79 """ |
|
80 menu = self.createStandardContextMenu(pos) |
|
81 |
|
82 # Add a submenu for setting the spell-check language and |
|
83 # document format. |
|
84 menu.addSeparator() |
|
85 menu.addMenu(self.__createLanguagesMenu(menu)) |
|
86 menu.addMenu(self.__createFormatsMenu(menu)) |
|
87 |
|
88 # Try to retrieve a menu of corrections for the right-clicked word |
|
89 spellMenu = self.__createCorrectionsMenu( |
|
90 self.__cursorForMisspelling(pos), menu) |
|
91 |
|
92 if spellMenu: |
|
93 menu.insertSeparator(menu.actions()[0]) |
|
94 menu.insertMenu(menu.actions()[0], spellMenu) |
|
95 |
|
96 return menu |
|
97 |
|
98 def __createCorrectionsMenu(self, cursor, parent=None): |
|
99 """ |
|
100 Private method to create a menu for corrections of the selected |
|
101 word. |
|
102 |
|
103 @param cursor reference to the text cursor |
|
104 @type QTextCursor |
|
105 @param parent reference to the parent widget (defaults to None) |
|
106 @type QWidget (optional) |
|
107 @return menu with corrections |
|
108 @rtype QMenu |
|
109 """ |
|
110 if cursor is None: |
|
111 return None |
|
112 |
|
113 text = cursor.selectedText() |
|
114 suggestions = trim_suggestions( |
|
115 text, self.__highlighter.dict().suggest(text), |
|
116 SpellCheckMixin.MaxSuggestions) |
|
117 |
|
118 spellMenu = QMenu( |
|
119 QCoreApplication.translate("SpellCheckMixin", |
|
120 "Spelling Suggestions"), |
|
121 parent) |
|
122 for word in suggestions: |
|
123 act = spellMenu.addAction(word) |
|
124 act.setData((cursor, word)) |
|
125 |
|
126 # Only return the menu if it's non-empty |
|
127 if spellMenu.actions(): |
|
128 spellMenu.triggered.connect(self.__correctWord) |
|
129 return spellMenu |
|
130 |
|
131 return None |
|
132 |
|
133 def __createLanguagesMenu(self, parent=None): |
|
134 """ |
|
135 Private method to create a menu for selecting the spell-check |
|
136 language. |
|
137 |
|
138 @param parent reference to the parent widget (defaults to None) |
|
139 @type QWidget (optional) |
|
140 @return menu with spell-check languages |
|
141 @rtype QMenu |
|
142 """ |
|
143 curLanguage = self.__highlighter.dict().tag.lower() |
|
144 languageMenu = QMenu( |
|
145 QCoreApplication.translate("SpellCheckMixin", "Language"), |
|
146 parent) |
|
147 languageActions = QActionGroup(languageMenu) |
|
148 |
|
149 for language in sorted(enchant.list_languages()): |
|
150 act = QAction(language, languageActions) |
|
151 act.setCheckable(True) |
|
152 act.setChecked(language.lower() == curLanguage) |
|
153 act.setData(language) |
|
154 languageMenu.addAction(act) |
|
155 |
|
156 languageMenu.triggered.connect(self.__setLanguage) |
|
157 return languageMenu |
|
158 |
|
159 def __createFormatsMenu(self, parent=None): |
|
160 """ |
|
161 Private method to create a menu for selecting the document format. |
|
162 |
|
163 @param parent reference to the parent widget (defaults to None) |
|
164 @type QWidget (optional) |
|
165 @return menu with document formats |
|
166 @rtype QMenu |
|
167 """ |
|
168 formatMenu = QMenu( |
|
169 QCoreApplication.translate("SpellCheckMixin", "Format"), |
|
170 parent) |
|
171 formatActions = QActionGroup(formatMenu) |
|
172 |
|
173 curFormat = self.__highlighter.chunkers() |
|
174 for name, chunkers in ( |
|
175 (QCoreApplication.translate("SpellCheckMixin", "Text"), |
|
176 []), |
|
177 (QCoreApplication.translate("SpellCheckMixin", "HTML"), |
|
178 [enchant.tokenize.HTMLChunker]) |
|
179 ): |
|
180 act = QAction(name, formatActions) |
|
181 act.setCheckable(True) |
|
182 act.setChecked(chunkers == curFormat) |
|
183 act.setData(chunkers) |
|
184 formatMenu.addAction(act) |
|
185 |
|
186 formatMenu.triggered.connect(self.__setFormat) |
|
187 return formatMenu |
|
188 |
|
189 def __cursorForMisspelling(self, pos): |
|
190 """ |
|
191 Private method to create a text cursor selecting the misspelled |
|
192 word. |
|
193 |
|
194 @param pos position of the misspelled word |
|
195 @type QPoint |
|
196 @return text cursor for the misspelled word |
|
197 @rtype QTextCursor |
|
198 """ |
|
199 cursor = self.cursorForPosition(pos) |
|
200 misspelledWords = getattr(cursor.block().userData(), |
|
201 "misspelled", []) |
|
202 |
|
203 # If the cursor is within a misspelling, select the word |
|
204 for (start, end) in misspelledWords: |
|
205 if start <= cursor.positionInBlock() <= end: |
|
206 blockPosition = cursor.block().position() |
|
207 |
|
208 cursor.setPosition(blockPosition + start, |
|
209 QTextCursor.MoveMode.MoveAnchor) |
|
210 cursor.setPosition(blockPosition + end, |
|
211 QTextCursor.MoveMode.KeepAnchor) |
|
212 break |
|
213 |
|
214 if cursor.hasSelection(): |
|
215 return cursor |
|
216 else: |
|
217 return None |
|
218 |
|
219 @pyqtSlot(QAction) |
|
220 def __correctWord(self, act): |
|
221 """ |
|
222 Private slot to correct the misspelled word with the selected |
|
223 correction. |
|
224 |
|
225 @param act reference to the selected action |
|
226 @type QAction |
|
227 """ |
|
228 cursor, word = act.data() |
|
229 |
|
230 cursor.beginEditBlock() |
|
231 cursor.removeSelectedText() |
|
232 cursor.insertText(word) |
|
233 cursor.endEditBlock() |
|
234 |
|
235 @pyqtSlot(QAction) |
|
236 def __setLanguage(self, act): |
|
237 """ |
|
238 Private slot to set the selected language. |
|
239 |
|
240 @param act reference to the selected action |
|
241 @type QAction |
|
242 """ |
|
243 language = act.data() |
|
244 self.__highlighter.setDict(enchant.Dict(language)) |
|
245 |
|
246 @pyqtSlot(QAction) |
|
247 def __setFormat(self, act): |
|
248 """ |
|
249 Private slot to set the selected document format. |
|
250 |
|
251 @param act reference to the selected action |
|
252 @type QAction |
|
253 """ |
|
254 chunkers = act.data() |
|
255 self.__highlighter.setChunkers(chunkers) |
|
256 |
|
257 def setFormat(self, formatName): |
|
258 """ |
|
259 Public method to set the document format. |
|
260 |
|
261 @param formatName name of the document format |
|
262 @type str |
|
263 """ |
|
264 self.__highlighter.setChunkers( |
|
265 [enchant.tokenize.HTMLChunker] |
|
266 if format == "html" else |
|
267 [] |
|
268 ) |
|
269 |
|
270 def dict(self): |
|
271 """ |
|
272 Public method to get a reference to the dictionary in use. |
|
273 |
|
274 @return reference to the current dictionary |
|
275 @rtype enchant.Dict |
|
276 """ |
|
277 return self.__highlighter.dict() |
|
278 |
|
279 def setDict(self, spellDict): |
|
280 """ |
|
281 Public method to set the dictionary to be used. |
|
282 |
|
283 @param spellDict reference to the spell-check dictionary |
|
284 @type emchant.Dict |
|
285 """ |
|
286 self.__highlighter.setDict(spellDict) |
|
287 |
|
288 @classmethod |
|
289 def setDefaultLanguage(cls, language): |
|
290 """ |
|
291 Class method to set the default spell-check language. |
|
292 |
|
293 @param language language to be set as default |
|
294 @type str |
|
295 """ |
|
296 with contextlib.suppress(DictNotFoundError): |
|
297 # set default language only, if a dictionary is available |
|
298 enchant.Dict(language) |
|
299 cls.DefaultLanguage = language |
|
300 |
|
301 class EnchantHighlighter(QSyntaxHighlighter): |
|
302 """ |
|
303 Class implementing a QSyntaxHighlighter subclass that consults a |
|
304 pyEnchant dictionary to highlight misspelled words. |
|
305 """ |
|
306 TokenFilters = (enchant.tokenize.EmailFilter, |
|
307 enchant.tokenize.URLFilter) |
|
308 |
|
309 # Define the spell-check style once and just assign it as necessary |
|
310 ErrorFormat = QTextCharFormat() |
|
311 ErrorFormat.setUnderlineColor(Qt.GlobalColor.red) |
|
312 ErrorFormat.setUnderlineStyle( |
|
313 QTextCharFormat.UnderlineStyle.SpellCheckUnderline) |
|
314 |
|
315 def __init__(self, *args): |
|
316 """ |
|
317 Constructor |
|
318 |
|
319 @param *args list of arguments for the QSyntaxHighlighter |
|
320 @type list |
|
321 """ |
|
322 QSyntaxHighlighter.__init__(self, *args) |
|
323 |
|
324 self.__spellDict = None |
|
325 self.__chunkers = [] |
|
326 |
|
327 def chunkers(self): |
|
328 """ |
|
329 Public method to get the chunkers in use. |
|
330 |
|
331 @return list of chunkers in use |
|
332 @rtype list |
|
333 """ |
|
334 return self.__chunkers |
|
335 |
|
336 def setChunkers(self, chunkers): |
|
337 """ |
|
338 Public method to set the chunkers to be used. |
|
339 |
|
340 @param chunkers chunkers to be used |
|
341 @type list |
|
342 """ |
|
343 self.__chunkers = chunkers |
|
344 self.setDict(self.dict()) |
|
345 |
|
346 def dict(self): |
|
347 """ |
|
348 Public method to get the spelling dictionary in use. |
|
349 |
|
350 @return spelling dictionary |
|
351 @rtype enchant.Dict |
|
352 """ |
|
353 return self.__spellDict |
|
354 |
|
355 def setDict(self, spellDict): |
|
356 """ |
|
357 Public method to set the spelling dictionary to be used. |
|
358 |
|
359 @param spellDict spelling dictionary |
|
360 @type enchant.Dict |
|
361 """ |
|
362 try: |
|
363 self.__tokenizer = enchant.tokenize.get_tokenizer( |
|
364 spellDict.tag, |
|
365 chunkers=self.__chunkers, |
|
366 filters=EnchantHighlighter.TokenFilters) |
|
367 except TokenizerNotFoundError: |
|
368 # Fall back to the "good for most euro languages" |
|
369 # English tokenizer |
|
370 self.__tokenizer = enchant.tokenize.get_tokenizer( |
|
371 chunkers=self.__chunkers, |
|
372 filters=EnchantHighlighter.TokenFilters) |
|
373 self.__spellDict = spellDict |
|
374 |
|
375 self.rehighlight() |
|
376 |
|
377 def highlightBlock(self, text): |
|
378 """ |
|
379 Public method to apply the text highlight. |
|
380 |
|
381 @param text text to be spell-checked |
|
382 @type str |
|
383 """ |
|
384 """Overridden QSyntaxHighlighter method to apply the highlight""" |
|
385 if not self.__spellDict: |
|
386 return |
|
387 |
|
388 # Build a list of all misspelled words and highlight them |
|
389 misspellings = [] |
|
390 for (word, pos) in self.__tokenizer(text): |
|
391 if not self.__spellDict.check(word): |
|
392 self.setFormat(pos, len(word), |
|
393 EnchantHighlighter.ErrorFormat) |
|
394 misspellings.append((pos, pos + len(word))) |
|
395 |
|
396 # Store the list so the context menu can reuse this tokenization |
|
397 # pass (Block-relative values so editing other blocks won't |
|
398 # invalidate them) |
|
399 data = QTextBlockUserData() |
|
400 data.misspelled = misspellings |
|
401 self.setCurrentBlockUserData(data) |
|
402 |
|
403 class EricSpellCheckedPlainTextEdit(QPlainTextEdit, SpellCheckMixin): |
|
404 """ |
|
405 Class implementing a QPlainTextEdit with built-in spell checker. |
|
406 """ |
|
407 def __init__(self, *args): |
|
408 """ |
|
409 Constructor |
|
410 |
|
411 @param *args list of arguments for the QPlainTextEdit constructor. |
|
412 @type list |
|
413 """ |
|
414 QPlainTextEdit.__init__(self, *args) |
|
415 SpellCheckMixin.__init__(self) |
|
416 |
|
417 class EricSpellCheckedTextEdit(QTextEdit, SpellCheckMixin): |
|
418 """ |
|
419 Class implementing a QTextEdit with built-in spell checker. |
|
420 """ |
|
421 def __init__(self, *args): |
|
422 """ |
|
423 Constructor |
|
424 |
|
425 @param *args list of arguments for the QPlainTextEdit constructor. |
|
426 @type list |
|
427 """ |
|
428 QTextEdit.__init__(self, *args) |
|
429 SpellCheckMixin.__init__(self) |
|
430 |
|
431 self.setFormat("html") |
|
432 |
|
433 def setAcceptRichText(self, accept): |
|
434 """ |
|
435 Public method to set the text edit mode. |
|
436 |
|
437 @param accept flag indicating to accept rich text |
|
438 @type bool |
|
439 """ |
|
440 QTextEdit.setAcceptRichText(self, accept) |
|
441 self.setFormat("html" if accept else "text") |
|
442 else: |
|
443 # enchant not available |
|
444 EricSpellCheckedPlainTextEdit = QPlainTextEdit |
|
445 EricSpellCheckedTextEdit = QTextEdit |
|
446 |
|
447 if __name__ == '__main__': |
|
448 import sys |
|
449 from PyQt6.QtWidgets import QApplication |
34 |
450 |
35 def __init__(self): |
451 if ENCHANT_AVAILABLE: |
36 """ |
452 SpellCheckMixin.setDefaultLanguage("en_US") |
37 Constructor |
|
38 """ |
|
39 self.__highlighter = EnchantHighlighter(self.document()) |
|
40 try: |
|
41 # Start with a default dictionary based on the current locale. |
|
42 spellDict = enchant.Dict() |
|
43 except DictNotFoundError: |
|
44 # Use English dictionary if no locale dictionary is available. |
|
45 spellDict = enchant.Dict("en") |
|
46 self.__highlighter.setDict(spellDict) |
|
47 |
|
48 def contextMenuEvent(self, evt): |
|
49 """ |
|
50 Protected method to handle context menu events to add a spelling |
|
51 suggestions submenu. |
|
52 |
|
53 @param evt reference to the context menu event |
|
54 @type QContextMenuEvent |
|
55 """ |
|
56 menu = self.__createSpellcheckContextMenu(evt.pos()) |
|
57 menu.exec(evt.globalPos()) |
|
58 |
|
59 def __createSpellcheckContextMenu(self, pos): |
|
60 """ |
|
61 Private method to create the spell-check context menu. |
|
62 |
|
63 @param pos position of the mouse pointer |
|
64 @type QPoint |
|
65 @return context menu with additional spell-check entries |
|
66 @rtype QMenu |
|
67 """ |
|
68 menu = self.createStandardContextMenu(pos) |
|
69 |
|
70 # Add a submenu for setting the spell-check language and |
|
71 # document format. |
|
72 menu.addSeparator() |
|
73 menu.addMenu(self.__createLanguagesMenu(menu)) |
|
74 menu.addMenu(self.__createFormatsMenu(menu)) |
|
75 |
|
76 # Try to retrieve a menu of corrections for the right-clicked word |
|
77 spellMenu = self.__createCorrectionsMenu( |
|
78 self.__cursorForMisspelling(pos), menu) |
|
79 |
|
80 if spellMenu: |
|
81 menu.insertSeparator(menu.actions()[0]) |
|
82 menu.insertMenu(menu.actions()[0], spellMenu) |
|
83 |
|
84 return menu |
|
85 |
|
86 def __createCorrectionsMenu(self, cursor, parent=None): |
|
87 """ |
|
88 Private method to create a menu for corrections of the selected word. |
|
89 |
|
90 @param cursor reference to the text cursor |
|
91 @type QTextCursor |
|
92 @param parent reference to the parent widget (defaults to None) |
|
93 @type QWidget (optional) |
|
94 @return menu with corrections |
|
95 @rtype QMenu |
|
96 """ |
|
97 if cursor is None: |
|
98 return None |
|
99 |
|
100 text = cursor.selectedText() |
|
101 suggestions = trim_suggestions(text, |
|
102 self.__highlighter.dict().suggest(text), |
|
103 SpellCheckMixin.MaxSuggestions) |
|
104 |
|
105 spellMenu = QMenu( |
|
106 QCoreApplication.translate("SpellCheckMixin", |
|
107 "Spelling Suggestions"), |
|
108 parent) |
|
109 for word in suggestions: |
|
110 act = spellMenu.addAction(word) |
|
111 act.setData((cursor, word)) |
|
112 |
|
113 # Only return the menu if it's non-empty |
|
114 if spellMenu.actions(): |
|
115 spellMenu.triggered.connect(self.__correctWord) |
|
116 return spellMenu |
|
117 |
|
118 return None |
|
119 |
|
120 def __createLanguagesMenu(self, parent=None): |
|
121 """ |
|
122 Private method to create a menu for selecting the spell-check language. |
|
123 |
|
124 @param parent reference to the parent widget (defaults to None) |
|
125 @type QWidget (optional) |
|
126 @return menu with spell-check languages |
|
127 @rtype QMenu |
|
128 """ |
|
129 curLanguage = self.__highlighter.dict().tag |
|
130 languageMenu = QMenu( |
|
131 QCoreApplication.translate("SpellCheckMixin", "Language"), |
|
132 parent) |
|
133 languageActions = QActionGroup(languageMenu) |
|
134 |
|
135 for language in enchant.list_languages(): |
|
136 act = QAction(language, languageActions) |
|
137 act.setCheckable(True) |
|
138 act.setChecked(language == curLanguage) |
|
139 act.setData(language) |
|
140 languageMenu.addAction(act) |
|
141 |
|
142 languageMenu.triggered.connect(self.__setLanguage) |
|
143 return languageMenu |
|
144 |
|
145 def __createFormatsMenu(self, parent=None): |
|
146 """ |
|
147 Private method to create a menu for selecting the document format. |
|
148 |
|
149 @param parent reference to the parent widget (defaults to None) |
|
150 @type QWidget (optional) |
|
151 @return menu with document formats |
|
152 @rtype QMenu |
|
153 """ |
|
154 formatMenu = QMenu( |
|
155 QCoreApplication.translate("SpellCheckMixin", "Format"), |
|
156 parent) |
|
157 formatActions = QActionGroup(formatMenu) |
|
158 |
|
159 curFormat = self.__highlighter.chunkers() |
|
160 for name, chunkers in ( |
|
161 (QCoreApplication.translate("SpellCheckMixin", "Text"), |
|
162 []), |
|
163 (QCoreApplication.translate("SpellCheckMixin", "HTML"), |
|
164 [tokenize.HTMLChunker]) |
|
165 ): |
|
166 act = QAction(name, formatActions) |
|
167 act.setCheckable(True) |
|
168 act.setChecked(chunkers == curFormat) |
|
169 act.setData(chunkers) |
|
170 formatMenu.addAction(act) |
|
171 |
|
172 formatMenu.triggered.connect(self.__setFormat) |
|
173 return formatMenu |
|
174 |
|
175 def __cursorForMisspelling(self, pos): |
|
176 """ |
|
177 Private method to create a text cursor selecting the misspelled word. |
|
178 |
|
179 @param pos position of the misspelled word |
|
180 @type QPoint |
|
181 @return text cursor for the misspelled word |
|
182 @rtype QTextCursor |
|
183 """ |
|
184 cursor = self.cursorForPosition(pos) |
|
185 misspelledWords = getattr(cursor.block().userData(), "misspelled", []) |
|
186 |
|
187 # If the cursor is within a misspelling, select the word |
|
188 for (start, end) in misspelledWords: |
|
189 if start <= cursor.positionInBlock() <= end: |
|
190 blockPosition = cursor.block().position() |
|
191 |
|
192 cursor.setPosition(blockPosition + start, |
|
193 QTextCursor.MoveMode.MoveAnchor) |
|
194 cursor.setPosition(blockPosition + end, |
|
195 QTextCursor.MoveMode.KeepAnchor) |
|
196 break |
|
197 |
|
198 if cursor.hasSelection(): |
|
199 return cursor |
|
200 else: |
|
201 return None |
|
202 |
|
203 @pyqtSlot(QAction) |
|
204 def __correctWord(self, act): |
|
205 """ |
|
206 Private slot to correct the misspelled word with the selected |
|
207 correction. |
|
208 |
|
209 @param act reference to the selected action |
|
210 @type QAction |
|
211 """ |
|
212 cursor, word = act.data() |
|
213 |
|
214 cursor.beginEditBlock() |
|
215 cursor.removeSelectedText() |
|
216 cursor.insertText(word) |
|
217 cursor.endEditBlock() |
|
218 |
|
219 @pyqtSlot(QAction) |
|
220 def __setLanguage(self, act): |
|
221 """ |
|
222 Private slot to set the selected language. |
|
223 |
|
224 @param act reference to the selected action |
|
225 @type QAction |
|
226 """ |
|
227 language = act.data() |
|
228 self.__highlighter.setDict(enchant.Dict(language)) |
|
229 |
|
230 @pyqtSlot(QAction) |
|
231 def __setFormat(self, act): |
|
232 """ |
|
233 Private slot to set the selected document format. |
|
234 |
|
235 @param act reference to the selected action |
|
236 @type QAction |
|
237 """ |
|
238 chunkers = act.data() |
|
239 self.__highlighter.setChunkers(chunkers) |
|
240 |
|
241 def setFormat(self, formatName): |
|
242 """ |
|
243 Public method to set the document format. |
|
244 |
|
245 @param formatName name of the document format |
|
246 @type str |
|
247 """ |
|
248 self.__highlighter.setChunkers( |
|
249 [tokenize.HTMLChunker] |
|
250 if format == "html" else |
|
251 [] |
|
252 ) |
|
253 |
|
254 def dict(self): |
|
255 """ |
|
256 Public method to get a reference to the dictionary in use. |
|
257 |
|
258 @return reference to the current dictionary |
|
259 @rtype enchant.Dict |
|
260 """ |
|
261 return self.__highlighter.dict() |
|
262 |
|
263 def setDict(self, spellDict): |
|
264 """ |
|
265 Public method to set the dictionary to be used. |
|
266 |
|
267 @param spellDict reference to the spell-check dictionary |
|
268 @type emchant.Dict |
|
269 """ |
|
270 self.__highlighter.setDict(spellDict) |
|
271 |
|
272 |
|
273 class EnchantHighlighter(QSyntaxHighlighter): |
|
274 """ |
|
275 Class implementing a QSyntaxHighlighter subclass that consults a |
|
276 pyEnchant dictionary to highlight misspelled words. |
|
277 """ |
|
278 TokenFilters = (tokenize.EmailFilter, tokenize.URLFilter) |
|
279 |
|
280 # Define the spell-check style once and just assign it as necessary |
|
281 ErrorFormat = QTextCharFormat() |
|
282 ErrorFormat.setUnderlineColor(Qt.GlobalColor.red) |
|
283 ErrorFormat.setUnderlineStyle( |
|
284 QTextCharFormat.UnderlineStyle.SpellCheckUnderline) |
|
285 |
|
286 def __init__(self, *args): |
|
287 """ |
|
288 Constructor |
|
289 |
|
290 @param *args list of arguments for the QSyntaxHighlighter |
|
291 @type list |
|
292 """ |
|
293 QSyntaxHighlighter.__init__(self, *args) |
|
294 |
|
295 self.__spellDict = None |
|
296 self.__chunkers = [] |
|
297 |
|
298 def chunkers(self): |
|
299 """ |
|
300 Public method to get the chunkers in use. |
|
301 |
|
302 @return list of chunkers in use |
|
303 @rtype list |
|
304 """ |
|
305 return self.__chunkers |
|
306 |
|
307 def setChunkers(self, chunkers): |
|
308 """ |
|
309 Public method to set the chunkers to be used. |
|
310 |
|
311 @param chunkers chunkers to be used |
|
312 @type list |
|
313 """ |
|
314 self.__chunkers = chunkers |
|
315 self.setDict(self.dict()) |
|
316 |
|
317 def dict(self): |
|
318 """ |
|
319 Public method to get the spelling dictionary in use. |
|
320 |
|
321 @return spelling dictionary |
|
322 @rtype enchant.Dict |
|
323 """ |
|
324 return self.__spellDict |
|
325 |
|
326 def setDict(self, spellDict): |
|
327 """ |
|
328 Public method to set the spelling dictionary to be used. |
|
329 |
|
330 @param spellDict spelling dictionary |
|
331 @type enchant.Dict |
|
332 """ |
|
333 try: |
|
334 self.__tokenizer = tokenize.get_tokenizer( |
|
335 spellDict.tag, |
|
336 chunkers=self.__chunkers, |
|
337 filters=EnchantHighlighter.TokenFilters) |
|
338 except TokenizerNotFoundError: |
|
339 # Fall back to the "good for most euro languages" English tokenizer |
|
340 self.__tokenizer = tokenize.get_tokenizer( |
|
341 chunkers=self.__chunkers, |
|
342 filters=EnchantHighlighter.TokenFilters) |
|
343 self.__spellDict = spellDict |
|
344 |
|
345 self.rehighlight() |
|
346 |
|
347 def highlightBlock(self, text): |
|
348 """ |
|
349 Public method to apply the text highlight. |
|
350 |
|
351 @param text text to be spell-checked |
|
352 @type str |
|
353 """ |
|
354 """Overridden QSyntaxHighlighter method to apply the highlight""" |
|
355 if not self.__spellDict: |
|
356 return |
|
357 |
|
358 # Build a list of all misspelled words and highlight them |
|
359 misspellings = [] |
|
360 for (word, pos) in self.__tokenizer(text): |
|
361 if not self.__spellDict.check(word): |
|
362 self.setFormat(pos, len(word), EnchantHighlighter.ErrorFormat) |
|
363 misspellings.append((pos, pos + len(word))) |
|
364 |
|
365 # Store the list so the context menu can reuse this tokenization pass |
|
366 # (Block-relative values so editing other blocks won't invalidate them) |
|
367 data = QTextBlockUserData() |
|
368 data.misspelled = misspellings |
|
369 self.setCurrentBlockUserData(data) |
|
370 |
|
371 |
|
372 class EricSpellCheckedPlainTextEdit(QPlainTextEdit, SpellCheckMixin): |
|
373 """ |
|
374 Class implementing a QPlainTextEdit with built-in spell checker. |
|
375 """ |
|
376 def __init__(self, *args): |
|
377 """ |
|
378 Constructor |
|
379 |
|
380 @param *args list of arguments for the QPlainTextEdit constructor. |
|
381 @type list |
|
382 """ |
|
383 QPlainTextEdit.__init__(self, *args) |
|
384 SpellCheckMixin.__init__(self) |
|
385 |
|
386 |
|
387 class EricSpellCheckedTextEdit(QTextEdit, SpellCheckMixin): |
|
388 """ |
|
389 Class implementing a QTextEdit with built-in spell checker. |
|
390 """ |
|
391 def __init__(self, *args): |
|
392 """ |
|
393 Constructor |
|
394 |
|
395 @param *args list of arguments for the QPlainTextEdit constructor. |
|
396 @type list |
|
397 """ |
|
398 QTextEdit.__init__(self, *args) |
|
399 SpellCheckMixin.__init__(self) |
|
400 |
|
401 self.setFormat("html") |
|
402 |
|
403 def setAcceptRichText(self, accept): |
|
404 """ |
|
405 Public method to set the text edit mode. |
|
406 |
|
407 @param accept flag indicating to accept rich text |
|
408 @type bool |
|
409 """ |
|
410 QTextEdit.setAcceptRichText(accept) |
|
411 self.setFormat("html" if accept else "text") |
|
412 |
|
413 if __name__ == '__main__': |
|
414 from PyQt6.QtWidgets import QApplication |
|
415 |
453 |
416 app = QApplication(sys.argv) |
454 app = QApplication(sys.argv) |
417 spellEdit = EricSpellCheckedPlainTextEdit() |
455 spellEdit = EricSpellCheckedPlainTextEdit() |
418 spellEdit.show() |
456 spellEdit.show() |
419 |
457 |