--- a/src/eric7/EricWidgets/EricSpellCheckedTextEdit.py Wed Jul 13 11:16:20 2022 +0200 +++ b/src/eric7/EricWidgets/EricSpellCheckedTextEdit.py Wed Jul 13 14:55:47 2022 +0200 @@ -15,32 +15,39 @@ import enchant.tokenize from enchant.errors import TokenizerNotFoundError, DictNotFoundError from enchant.utils import trim_suggestions + ENCHANT_AVAILABLE = True except ImportError: ENCHANT_AVAILABLE = False from PyQt6.QtCore import pyqtSlot, Qt, QCoreApplication from PyQt6.QtGui import ( - QAction, QActionGroup, QSyntaxHighlighter, QTextBlockUserData, - QTextCharFormat, QTextCursor + QAction, + QActionGroup, + QSyntaxHighlighter, + QTextBlockUserData, + QTextCharFormat, + QTextCursor, ) from PyQt6.QtWidgets import QMenu, QTextEdit, QPlainTextEdit if ENCHANT_AVAILABLE: - class SpellCheckMixin(): + + class SpellCheckMixin: """ Class implementing the spell-check mixin for the widget classes. """ + # don't show more than this to keep the menu manageable MaxSuggestions = 20 - + # default language to be used when no other is set DefaultLanguage = None - + # default user lists DefaultUserWordList = None DefaultUserExceptionList = None - + def __init__(self): """ Constructor @@ -52,7 +59,7 @@ spellDict = enchant.DictWithPWL( SpellCheckMixin.DefaultLanguage, SpellCheckMixin.DefaultUserWordList, - SpellCheckMixin.DefaultUserExceptionList + SpellCheckMixin.DefaultUserExceptionList, ) except DictNotFoundError: try: @@ -61,59 +68,60 @@ spellDict = enchant.DictWithPWL( "en", SpellCheckMixin.DefaultUserWordList, - SpellCheckMixin.DefaultUserExceptionList + SpellCheckMixin.DefaultUserExceptionList, ) except DictNotFoundError: # Still no dictionary could be found. Forget about spell # checking. spellDict = None - + self.__highlighter.setDict(spellDict) - + def contextMenuEvent(self, evt): """ Protected method to handle context menu events to add a spelling suggestions submenu. - + @param evt reference to the context menu event @type QContextMenuEvent """ menu = self.__createSpellcheckContextMenu(evt.pos()) menu.exec(evt.globalPos()) - + def __createSpellcheckContextMenu(self, pos): """ Private method to create the spell-check context menu. - + @param pos position of the mouse pointer @type QPoint @return context menu with additional spell-check entries @rtype QMenu """ menu = self.createStandardContextMenu(pos) - + # Add a submenu for setting the spell-check language and # document format. menu.addSeparator() self.__addRemoveEntry(self.__cursorForPosition(pos), menu) menu.addMenu(self.__createLanguagesMenu(menu)) menu.addMenu(self.__createFormatsMenu(menu)) - + # Try to retrieve a menu of corrections for the right-clicked word spellMenu = self.__createCorrectionsMenu( - self.__cursorForMisspelling(pos), menu) - + self.__cursorForMisspelling(pos), menu + ) + if spellMenu: menu.insertSeparator(menu.actions()[0]) menu.insertMenu(menu.actions()[0], spellMenu) - + return menu - + def __createCorrectionsMenu(self, cursor, parent=None): """ Private method to create a menu for corrections of the selected word. - + @param cursor reference to the text cursor @type QTextCursor @param parent reference to the parent widget (defaults to None) @@ -123,36 +131,39 @@ """ if cursor is None: return None - + text = cursor.selectedText() suggestions = trim_suggestions( - text, self.__highlighter.dict().suggest(text), - SpellCheckMixin.MaxSuggestions) - + text, + self.__highlighter.dict().suggest(text), + SpellCheckMixin.MaxSuggestions, + ) + spellMenu = QMenu( - QCoreApplication.translate("SpellCheckMixin", - "Spelling Suggestions"), - parent) + QCoreApplication.translate("SpellCheckMixin", "Spelling Suggestions"), + parent, + ) for word in suggestions: act = spellMenu.addAction(word) act.setData((cursor, word)) - + if suggestions: spellMenu.addSeparator() - + # add management entry - act = spellMenu.addAction(QCoreApplication.translate( - "SpellCheckMixin", "Add to Dictionary")) + act = spellMenu.addAction( + QCoreApplication.translate("SpellCheckMixin", "Add to Dictionary") + ) act.setData((cursor, text, "add")) - + spellMenu.triggered.connect(self.__spellMenuTriggered) return spellMenu - + def __addRemoveEntry(self, cursor, menu): """ Private method to create a menu entry to remove the word at the menu position. - + @param cursor reference to the text cursor for the misspelled word @type QTextCursor @param menu reference to the context menu @@ -160,18 +171,20 @@ """ if cursor is None: return - + text = cursor.selectedText() - menu.addAction(QCoreApplication.translate( - "SpellCheckMixin", - "Remove '{0}' from Dictionary").format(text), - lambda: self.__addToUserDict(text, "remove")) - + menu.addAction( + QCoreApplication.translate( + "SpellCheckMixin", "Remove '{0}' from Dictionary" + ).format(text), + lambda: self.__addToUserDict(text, "remove"), + ) + def __createLanguagesMenu(self, parent=None): """ Private method to create a menu for selecting the spell-check language. - + @param parent reference to the parent widget (defaults to None) @type QWidget (optional) @return menu with spell-check languages @@ -179,55 +192,56 @@ """ curLanguage = self.__highlighter.dict().tag.lower() languageMenu = QMenu( - QCoreApplication.translate("SpellCheckMixin", "Language"), - parent) + QCoreApplication.translate("SpellCheckMixin", "Language"), parent + ) languageActions = QActionGroup(languageMenu) - + for language in sorted(enchant.list_languages()): act = QAction(language, languageActions) act.setCheckable(True) act.setChecked(language.lower() == curLanguage) act.setData(language) languageMenu.addAction(act) - + languageMenu.triggered.connect(self.__setLanguage) return languageMenu - + def __createFormatsMenu(self, parent=None): """ Private method to create a menu for selecting the document format. - + @param parent reference to the parent widget (defaults to None) @type QWidget (optional) @return menu with document formats @rtype QMenu """ formatMenu = QMenu( - QCoreApplication.translate("SpellCheckMixin", "Format"), - parent) + QCoreApplication.translate("SpellCheckMixin", "Format"), parent + ) formatActions = QActionGroup(formatMenu) - + curFormat = self.__highlighter.chunkers() for name, chunkers in ( - (QCoreApplication.translate("SpellCheckMixin", "Text"), - []), - (QCoreApplication.translate("SpellCheckMixin", "HTML"), - [enchant.tokenize.HTMLChunker]) + (QCoreApplication.translate("SpellCheckMixin", "Text"), []), + ( + QCoreApplication.translate("SpellCheckMixin", "HTML"), + [enchant.tokenize.HTMLChunker], + ), ): act = QAction(name, formatActions) act.setCheckable(True) act.setChecked(chunkers == curFormat) act.setData(chunkers) formatMenu.addAction(act) - + formatMenu.triggered.connect(self.__setFormat) return formatMenu - + def __cursorForPosition(self, pos): """ Private method to create a text cursor selecting the word at the given position. - + @param pos position of the misspelled word @type QPoint @return text cursor for the word @@ -235,46 +249,47 @@ """ cursor = self.cursorForPosition(pos) cursor.select(QTextCursor.SelectionType.WordUnderCursor) - + if cursor.hasSelection(): return cursor else: return None - + def __cursorForMisspelling(self, pos): """ Private method to create a text cursor selecting the misspelled word. - + @param pos position of the misspelled word @type QPoint @return text cursor for the misspelled word @rtype QTextCursor """ cursor = self.cursorForPosition(pos) - misspelledWords = getattr(cursor.block().userData(), - "misspelled", []) - + misspelledWords = getattr(cursor.block().userData(), "misspelled", []) + # If the cursor is within a misspelling, select the word for (start, end) in misspelledWords: if start <= cursor.positionInBlock() <= end: blockPosition = cursor.block().position() - - cursor.setPosition(blockPosition + start, - QTextCursor.MoveMode.MoveAnchor) - cursor.setPosition(blockPosition + end, - QTextCursor.MoveMode.KeepAnchor) + + cursor.setPosition( + blockPosition + start, QTextCursor.MoveMode.MoveAnchor + ) + cursor.setPosition( + blockPosition + end, QTextCursor.MoveMode.KeepAnchor + ) break - + if cursor.hasSelection(): return cursor else: return None - + def __correctWord(self, cursor, word): """ Private method to replace some misspelled text. - + @param cursor reference to the text cursor for the misspelled word @type QTextCursor @param word replacement text @@ -284,11 +299,11 @@ cursor.removeSelectedText() cursor.insertText(word) cursor.endEditBlock() - + def __addToUserDict(self, word, command): """ Private method to add a word to the user word or exclude list. - + @param word text to be added @type str @param command command indicating the user dictionary type @@ -300,14 +315,14 @@ dictionary.add(word) elif command == "remove": dictionary.remove(word) - + self.__highlighter.rehighlight() - + @pyqtSlot(QAction) def __spellMenuTriggered(self, act): """ Private slot to handle a selection of the spell menu. - + @param act reference to the selected action @type QAction """ @@ -315,94 +330,84 @@ if len(data) == 2: # replace the misspelled word self.__correctWord(*data) - + elif len(data) == 3: # dictionary management action _, word, command = data self.__addToUserDict(word, command) - + @pyqtSlot(QAction) def __setLanguage(self, act): """ Private slot to set the selected language. - + @param act reference to the selected action @type QAction """ language = act.data() self.setLanguage(language) - + @pyqtSlot(QAction) def __setFormat(self, act): """ Private slot to set the selected document format. - + @param act reference to the selected action @type QAction """ chunkers = act.data() self.__highlighter.setChunkers(chunkers) - + def setFormat(self, formatName): """ Public method to set the document format. - + @param formatName name of the document format @type str """ self.__highlighter.setChunkers( - [enchant.tokenize.HTMLChunker] - if format == "html" else - [] + [enchant.tokenize.HTMLChunker] if format == "html" else [] ) - + def dict(self): """ Public method to get a reference to the dictionary in use. - + @return reference to the current dictionary @rtype enchant.Dict """ return self.__highlighter.dict() - + def setDict(self, spellDict): """ Public method to set the dictionary to be used. - + @param spellDict reference to the spell-check dictionary @type emchant.Dict """ self.__highlighter.setDict(spellDict) - + @pyqtSlot(str) def setLanguage(self, language): """ Public slot to set the spellchecker language. - + @param language language to be set @type str """ epwl = self.dict().pwl - pwl = ( - epwl.provider.file - if isinstance(epwl, enchant.Dict) else - None - ) - + pwl = epwl.provider.file if isinstance(epwl, enchant.Dict) else None + epel = self.dict().pel - pel = ( - epel.provider.file - if isinstance(epel, enchant.Dict) else - None - ) + pel = epel.provider.file if isinstance(epel, enchant.Dict) else None self.setLanguageWithPWL(language, pwl, pel) - + @pyqtSlot(str, str, str) def setLanguageWithPWL(self, language, pwl, pel): """ Public slot to set the spellchecker language and associated user word lists. - + @param language language to be set @type str @param pwl file name of the personal word list @@ -422,12 +427,12 @@ # checking. spellDict = None self.__highlighter.setDict(spellDict) - + @classmethod def setDefaultLanguage(cls, language, pwl=None, pel=None): """ Class method to set the default spell-check language. - + @param language language to be set as default @type str @param pwl file name of the personal word list @@ -438,7 +443,7 @@ with contextlib.suppress(DictNotFoundError): cls.DefaultUserWordList = pwl cls.DefaultUserExceptionList = pel - + # set default language only, if a dictionary is available enchant.Dict(language) cls.DefaultLanguage = language @@ -448,60 +453,61 @@ Class implementing a QSyntaxHighlighter subclass that consults a pyEnchant dictionary to highlight misspelled words. """ - TokenFilters = (enchant.tokenize.EmailFilter, - enchant.tokenize.URLFilter) + + TokenFilters = (enchant.tokenize.EmailFilter, enchant.tokenize.URLFilter) # Define the spell-check style once and just assign it as necessary ErrorFormat = QTextCharFormat() ErrorFormat.setUnderlineColor(Qt.GlobalColor.red) ErrorFormat.setUnderlineStyle( - QTextCharFormat.UnderlineStyle.SpellCheckUnderline) - + QTextCharFormat.UnderlineStyle.SpellCheckUnderline + ) + def __init__(self, *args): """ Constructor - + @param *args list of arguments for the QSyntaxHighlighter @type list """ QSyntaxHighlighter.__init__(self, *args) - + self.__spellDict = None self.__tokenizer = None self.__chunkers = [] - + def chunkers(self): """ Public method to get the chunkers in use. - + @return list of chunkers in use @rtype list """ return self.__chunkers - + def setChunkers(self, chunkers): """ Public method to set the chunkers to be used. - + @param chunkers chunkers to be used @type list """ self.__chunkers = chunkers self.setDict(self.dict()) - + def dict(self): """ Public method to get the spelling dictionary in use. - + @return spelling dictionary @rtype enchant.Dict """ return self.__spellDict - + def setDict(self, spellDict): """ Public method to set the spelling dictionary to be used. - + @param spellDict spelling dictionary @type enchant.Dict """ @@ -510,39 +516,40 @@ self.__tokenizer = enchant.tokenize.get_tokenizer( spellDict.tag, chunkers=self.__chunkers, - filters=EnchantHighlighter.TokenFilters) + filters=EnchantHighlighter.TokenFilters, + ) except TokenizerNotFoundError: # Fall back to the "good for most euro languages" # English tokenizer self.__tokenizer = enchant.tokenize.get_tokenizer( chunkers=self.__chunkers, - filters=EnchantHighlighter.TokenFilters) + filters=EnchantHighlighter.TokenFilters, + ) else: self.__tokenizer = None - + self.__spellDict = spellDict - + self.rehighlight() - + def highlightBlock(self, text): """ Public method to apply the text highlight. - + @param text text to be spell-checked @type str """ """Overridden QSyntaxHighlighter method to apply the highlight""" if self.__spellDict is None or self.__tokenizer is None: return - + # Build a list of all misspelled words and highlight them misspellings = [] for (word, pos) in self.__tokenizer(text): if not self.__spellDict.check(word): - self.setFormat(pos, len(word), - EnchantHighlighter.ErrorFormat) + self.setFormat(pos, len(word), EnchantHighlighter.ErrorFormat) misspellings.append((pos, pos + len(word))) - + # Store the list so the context menu can reuse this tokenization # pass (Block-relative values so editing other blocks won't # invalidate them) @@ -552,10 +559,11 @@ else: - class SpellCheckMixin(): + class SpellCheckMixin: """ Class implementing the spell-check mixin for the widget classes. """ + # # This is just a stub to provide the same API as the enchant enabled # one. @@ -565,50 +573,50 @@ Constructor """ pass - + def setFormat(self, formatName): """ Public method to set the document format. - + @param formatName name of the document format @type str """ pass - + def dict(self): """ Public method to get a reference to the dictionary in use. - + @return reference to the current dictionary @rtype enchant.Dict """ pass - + def setDict(self, spellDict): """ Public method to set the dictionary to be used. - + @param spellDict reference to the spell-check dictionary @type emchant.Dict """ pass - + @pyqtSlot(str) def setLanguage(self, language): """ Public slot to set the spellchecker language. - + @param language language to be set @type str """ pass - + @pyqtSlot(str, str, str) def setLanguageWithPWL(self, language, pwl, pel): """ Public slot to set the spellchecker language and associated user word lists. - + @param language language to be set @type str @param pwl file name of the personal word list @@ -617,12 +625,12 @@ @type str """ pass - + @classmethod def setDefaultLanguage(cls, language, pwl=None, pel=None): """ Class method to set the default spell-check language. - + @param language language to be set as default @type str @param pwl file name of the personal word list @@ -637,10 +645,11 @@ """ Class implementing a QPlainTextEdit with built-in spell checker. """ + def __init__(self, *args): """ Constructor - + @param *args list of arguments for the QPlainTextEdit constructor. @type list """ @@ -652,43 +661,45 @@ """ Class implementing a QTextEdit with built-in spell checker. """ + def __init__(self, *args): """ Constructor - + @param *args list of arguments for the QPlainTextEdit constructor. @type list """ QTextEdit.__init__(self, *args) SpellCheckMixin.__init__(self) - + self.setFormat("html") - + def setAcceptRichText(self, accept): """ Public method to set the text edit mode. - + @param accept flag indicating to accept rich text @type bool """ QTextEdit.setAcceptRichText(self, accept) self.setFormat("html" if accept else "text") -if __name__ == '__main__': + +if __name__ == "__main__": import sys import os from PyQt6.QtWidgets import QApplication - + if ENCHANT_AVAILABLE: dictPath = os.path.expanduser(os.path.join("~", ".eric7", "spelling")) SpellCheckMixin.setDefaultLanguage( "en_US", os.path.join(dictPath, "pwl.dic"), - os.path.join(dictPath, "pel.dic") + os.path.join(dictPath, "pel.dic"), ) - + app = QApplication(sys.argv) spellEdit = EricSpellCheckedPlainTextEdit() spellEdit.show() - + sys.exit(app.exec())