Skip to content

Instantly share code, notes, and snippets.

@tritium21
Forked from ssokolow/spelltextedit.py
Created September 18, 2019 08:46
Show Gist options
  • Save tritium21/f09ce417edd035fca488fd2b1f7ec2a8 to your computer and use it in GitHub Desktop.
Save tritium21/f09ce417edd035fca488fd2b1f7ec2a8 to your computer and use it in GitHub Desktop.

Revisions

  1. Stephan Sokolow revised this gist Feb 22, 2018. 1 changed file with 41 additions and 5 deletions.
    46 changes: 41 additions & 5 deletions spelltextedit.py
    Original file line number Diff line number Diff line change
    @@ -46,8 +46,10 @@
    QPlainTextEdit)

    try:
    # pylint: disable=ungrouped-imports
    from enchant.utils import trim_suggestions
    except ImportError: # Older versions of PyEnchant as on *buntu 14.04
    # pylint: disable=unused-argument
    def trim_suggestions(word, suggs, maxlen, calcdist=None):
    """API Polyfill for earlier versions of PyEnchant.
    @@ -95,6 +97,7 @@ def createSpellcheckContextMenu(self, pos):
    # Add a submenu for setting the spell-check language
    menu.addSeparator()
    menu.addMenu(self.createLanguagesMenu(menu))
    menu.addMenu(self.createFormatsMenu(menu))

    # Try to retrieve a menu of corrections for the right-clicked word
    spell_menu = self.createCorrectionsMenu(
    @@ -145,6 +148,22 @@ def createLanguagesMenu(self, parent=None):
    lang_menu.triggered.connect(self.cb_set_language)
    return lang_menu

    def createFormatsMenu(self, parent=None):
    """Create and return a menu for selecting the spell-check language."""
    fmt_menu = QMenu("Format", parent)
    fmt_actions = QActionGroup(fmt_menu)

    curr_format = self.highlighter.chunkers()
    for name, chunkers in (('Text', []), ('HTML', [tokenize.HTMLChunker])):
    action = fmt_actions.addAction(name)
    action.setCheckable(True)
    action.setChecked(chunkers == curr_format)
    action.setData(chunkers)
    fmt_menu.addAction(action)

    fmt_menu.triggered.connect(self.cb_set_format)
    return fmt_menu

    def cursorForMisspelling(self, pos):
    """Return a cursor selecting the misspelled word at ``pos`` or ``None``
    @@ -182,6 +201,12 @@ def cb_set_language(self, action):
    lang = action.data()
    self.highlighter.setDict(enchant.Dict(lang))

    def cb_set_format(self, action):
    """Event handler for 'Language' menu entries."""
    chunkers = action.data()
    self.highlighter.setChunkers(chunkers)
    # TODO: Emit an event so this menu can trigger other things

    class EnchantHighlighter(QSyntaxHighlighter):
    """QSyntaxHighlighter subclass which consults a PyEnchant dictionary"""
    tokenizer = None
    @@ -199,21 +224,32 @@ def __init__(self, *args):

    # Initialize private members
    self._sp_dict = None
    self._chunkers = []

    def chunkers(self):
    """Gets the chunkers in use"""
    return self._chunkers

    def dict(self):
    """Gets the spelling dictionary in use"""
    return self._sp_dict

    def setChunkers(self, chunkers):
    """Sets the list of chunkers to be used"""
    self._chunkers = chunkers
    self.setDict(self.dict())
    # FIXME: Revert self._chunkers on failure to ensure consistent state

    def setDict(self, sp_dict):
    """Sets the spelling dictionary to be used"""
    self._sp_dict = sp_dict

    try:
    self.tokenizer = tokenize.get_tokenizer(
    self._sp_dict.tag, filters=self.token_filters)
    self.tokenizer = tokenize.get_tokenizer(sp_dict.tag,
    chunkers=self._chunkers, filters=self.token_filters)
    except TokenizerNotFoundError:
    # Fall back to the "good for most euro languages" English tokenizer
    self.tokenizer = tokenize.get_tokenizer(filters=self.token_filters)
    self.tokenizer = tokenize.get_tokenizer(
    chunkers=self._chunkers, filters=self.token_filters)
    self._sp_dict = sp_dict

    self.rehighlight()

  2. Stephan Sokolow revised this gist Feb 21, 2018. 1 changed file with 4 additions and 3 deletions.
    7 changes: 4 additions & 3 deletions spelltextedit.py
    Original file line number Diff line number Diff line change
    @@ -82,7 +82,7 @@ def contextMenuEvent(self, event):
    self.focusInEvent(QFocusEvent(QEvent.FocusIn))

    def createSpellcheckContextMenu(self, pos):
    """Create and return a context menu with spelling suggestions.
    """Create and return an augmented default context menu.
    This may be used as an alternative to the QPoint-taking form of
    ``createStandardContextMenu`` and will work on pre-5.5 Qt.
    @@ -146,7 +146,7 @@ def createLanguagesMenu(self, parent=None):
    return lang_menu

    def cursorForMisspelling(self, pos):
    """Select the misspelled word at ``pos`` or return ``None``.
    """Return a cursor selecting the misspelled word at ``pos`` or ``None``
    This leverages the fact that QPlainTextEdit already has a system for
    processing its contents in limited-size blocks to keep things fast.
    @@ -169,7 +169,7 @@ def cursorForMisspelling(self, pos):
    return None

    def cb_correct_word(self, action): # pylint: disable=no-self-use
    """Event handler Handler for 'Spelling Suggestions' entries."""
    """Event handler for 'Spelling Suggestions' entries."""
    cursor, word = action.data()

    cursor.beginEditBlock()
    @@ -178,6 +178,7 @@ def cb_correct_word(self, action): # pylint: disable=no-self-use
    cursor.endEditBlock()

    def cb_set_language(self, action):
    """Event handler for 'Language' menu entries."""
    lang = action.data()
    self.highlighter.setDict(enchant.Dict(lang))

  3. Stephan Sokolow revised this gist Feb 21, 2018. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions spelltextedit.py
    Original file line number Diff line number Diff line change
    @@ -106,7 +106,7 @@ def createSpellcheckContextMenu(self, pos):

    return menu

    def createCorrectionsMenu(self, cursor, parent):
    def createCorrectionsMenu(self, cursor, parent=None):
    """Create and return a menu for correcting the selected word."""
    if not cursor:
    return None
    @@ -129,7 +129,7 @@ def createCorrectionsMenu(self, cursor, parent):

    return None

    def createLanguagesMenu(self, parent):
    def createLanguagesMenu(self, parent=None):
    """Create and return a menu for selecting the spell-check language."""
    curr_lang = self.highlighter.dict().tag
    lang_menu = QMenu("Language", parent)
  4. Stephan Sokolow revised this gist Feb 21, 2018. 1 changed file with 30 additions and 20 deletions.
    50 changes: 30 additions & 20 deletions spelltextedit.py
    Original file line number Diff line number Diff line change
    @@ -96,29 +96,39 @@ def createSpellcheckContextMenu(self, pos):
    menu.addSeparator()
    menu.addMenu(self.createLanguagesMenu(menu))

    # Check if the click happened inside a misspelling
    cursor = self.cursorForMisspelling(pos)

    if cursor:
    text = cursor.selectedText()
    suggests = trim_suggestions(text,
    self.highlighter.dict().suggest(text),
    self.max_suggestions)

    spell_menu = QMenu('Spelling Suggestions', menu)
    for word in suggests:
    action = QAction(word, spell_menu)
    action.setData((cursor, word))
    spell_menu.addAction(action)

    # Only prepend "Spelling Suggestions" submenu if it's non-empty
    if spell_menu.actions():
    spell_menu.triggered.connect(self.cb_correct_word)
    menu.insertSeparator(menu.actions()[0])
    menu.insertMenu(menu.actions()[0], spell_menu)
    # Try to retrieve a menu of corrections for the right-clicked word
    spell_menu = self.createCorrectionsMenu(
    self.cursorForMisspelling(pos), menu)

    if spell_menu:
    menu.insertSeparator(menu.actions()[0])
    menu.insertMenu(menu.actions()[0], spell_menu)

    return menu

    def createCorrectionsMenu(self, cursor, parent):
    """Create and return a menu for correcting the selected word."""
    if not cursor:
    return None

    text = cursor.selectedText()
    suggests = trim_suggestions(text,
    self.highlighter.dict().suggest(text),
    self.max_suggestions)

    spell_menu = QMenu('Spelling Suggestions', parent)
    for word in suggests:
    action = QAction(word, spell_menu)
    action.setData((cursor, word))
    spell_menu.addAction(action)

    # Only return the menu if it's non-empty
    if spell_menu.actions():
    spell_menu.triggered.connect(self.cb_correct_word)
    return spell_menu

    return None

    def createLanguagesMenu(self, parent):
    """Create and return a menu for selecting the spell-check language."""
    curr_lang = self.highlighter.dict().tag
  5. Stephan Sokolow revised this gist Feb 21, 2018. 1 changed file with 26 additions and 1 deletion.
    27 changes: 26 additions & 1 deletion spelltextedit.py
    Original file line number Diff line number Diff line change
    @@ -42,7 +42,8 @@
    from PyQt5.QtCore import QEvent
    from PyQt5.QtGui import (QFocusEvent, QSyntaxHighlighter, QTextBlockUserData,
    QTextCharFormat, QTextCursor)
    from PyQt5.QtWidgets import QAction, QApplication, QMenu, QPlainTextEdit
    from PyQt5.QtWidgets import (QAction, QActionGroup, QApplication, QMenu,
    QPlainTextEdit)

    try:
    from enchant.utils import trim_suggestions
    @@ -91,6 +92,10 @@ def createSpellcheckContextMenu(self, pos):
    except TypeError: # Before Qt 5.5
    menu = self.createStandardContextMenu()

    # Add a submenu for setting the spell-check language
    menu.addSeparator()
    menu.addMenu(self.createLanguagesMenu(menu))

    # Check if the click happened inside a misspelling
    cursor = self.cursorForMisspelling(pos)

    @@ -114,6 +119,22 @@ def createSpellcheckContextMenu(self, pos):

    return menu

    def createLanguagesMenu(self, parent):
    """Create and return a menu for selecting the spell-check language."""
    curr_lang = self.highlighter.dict().tag
    lang_menu = QMenu("Language", parent)
    lang_actions = QActionGroup(lang_menu)

    for lang in enchant.list_languages():
    action = lang_actions.addAction(lang)
    action.setCheckable(True)
    action.setChecked(lang == curr_lang)
    action.setData(lang)
    lang_menu.addAction(action)

    lang_menu.triggered.connect(self.cb_set_language)
    return lang_menu

    def cursorForMisspelling(self, pos):
    """Select the misspelled word at ``pos`` or return ``None``.
    @@ -146,6 +167,10 @@ def cb_correct_word(self, action): # pylint: disable=no-self-use
    cursor.insertText(word)
    cursor.endEditBlock()

    def cb_set_language(self, action):
    lang = action.data()
    self.highlighter.setDict(enchant.Dict(lang))

    class EnchantHighlighter(QSyntaxHighlighter):
    """QSyntaxHighlighter subclass which consults a PyEnchant dictionary"""
    tokenizer = None
  6. Stephan Sokolow revised this gist Feb 20, 2018. 1 changed file with 60 additions and 26 deletions.
    86 changes: 60 additions & 26 deletions spelltextedit.py
    Original file line number Diff line number Diff line change
    @@ -40,10 +40,20 @@
    # pylint: disable=no-name-in-module
    from PyQt5.Qt import Qt
    from PyQt5.QtCore import QEvent
    from PyQt5.QtGui import (QFocusEvent, QSyntaxHighlighter, QTextCharFormat,
    QTextCursor)
    from PyQt5.QtGui import (QFocusEvent, QSyntaxHighlighter, QTextBlockUserData,
    QTextCharFormat, QTextCursor)
    from PyQt5.QtWidgets import QAction, QApplication, QMenu, QPlainTextEdit

    try:
    from enchant.utils import trim_suggestions
    except ImportError: # Older versions of PyEnchant as on *buntu 14.04
    def trim_suggestions(word, suggs, maxlen, calcdist=None):
    """API Polyfill for earlier versions of PyEnchant.
    TODO: Make this actually do some sorting
    """
    return suggs[:maxlen]

    class SpellTextEdit(QPlainTextEdit):
    """QPlainTextEdit subclass which does spell-checking using PyEnchant"""

    @@ -81,36 +91,51 @@ def createSpellcheckContextMenu(self, pos):
    except TypeError: # Before Qt 5.5
    menu = self.createStandardContextMenu()

    # Create a cursor object selecting the word under the mouse pointer
    # FIXME: This method tokenizes differently than the highlighter and
    # allows one to right-click an un-highlighted URL and find a
    # suggestions menu when there shouldn't be one.
    cursor = self.cursorForPosition(pos)
    cursor.select(QTextCursor.WordUnderCursor)
    # Check if the click happened inside a misspelling
    cursor = self.cursorForMisspelling(pos)

    # Check if the selected word is misspelled and offer spelling
    # suggestions if it is
    if cursor.hasSelection():
    if cursor:
    text = cursor.selectedText()
    spell_dict = self.highlighter.dict()

    if not spell_dict.check(text):
    spell_menu = QMenu('Spelling Suggestions', menu)
    suggests = trim_suggestions(text,
    self.highlighter.dict().suggest(text),
    self.max_suggestions)

    spell_menu = QMenu('Spelling Suggestions', menu)
    for word in suggests:
    action = QAction(word, spell_menu)
    action.setData((cursor, word))
    spell_menu.addAction(action)

    # Only prepend "Spelling Suggestions" submenu if it's non-empty
    if spell_menu.actions():
    spell_menu.triggered.connect(self.cb_correct_word)
    menu.insertSeparator(menu.actions()[0])
    menu.insertMenu(menu.actions()[0], spell_menu)

    # TODO: Use enchant.utils.trim_suggestions once I don't have to
    # support old PyEnchant versions.
    for word in spell_dict.suggest(text)[:self.max_suggestions]:
    action = QAction(word, spell_menu)
    action.setData((cursor, word))
    spell_menu.addAction(action)
    return menu

    # Only prepend "Spelling Suggestions" submenu if it's non-empty
    if spell_menu.actions():
    menu.insertSeparator(menu.actions()[0])
    menu.insertMenu(menu.actions()[0], spell_menu)
    def cursorForMisspelling(self, pos):
    """Select the misspelled word at ``pos`` or return ``None``.
    return menu
    This leverages the fact that QPlainTextEdit already has a system for
    processing its contents in limited-size blocks to keep things fast.
    """
    cursor = self.cursorForPosition(pos)
    misspelled_words = getattr(cursor.block().userData(), 'misspelled', [])

    # If the cursor is within a misspelling, select the word
    for (start, end) in misspelled_words:
    if start <= cursor.positionInBlock() <= end:
    block_pos = cursor.block().position()

    cursor.setPosition(block_pos + start, QTextCursor.MoveAnchor)
    cursor.setPosition(block_pos + end, QTextCursor.KeepAnchor)
    break

    if cursor.hasSelection():
    return cursor
    else:
    return None

    def cb_correct_word(self, action): # pylint: disable=no-self-use
    """Event handler Handler for 'Spelling Suggestions' entries."""
    @@ -161,9 +186,18 @@ def highlightBlock(self, text):
    if not self._sp_dict:
    return

    # Build a list of all misspelled words and highlight them
    misspellings = []
    for (word, pos) in self.tokenizer(text):
    if not self._sp_dict.check(word):
    self.setFormat(pos, len(word), self.err_format)
    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)
    data = QTextBlockUserData()
    data.misspelled = misspellings
    self.setCurrentBlockUserData(data)

    if __name__ == '__main__':
    app = QApplication(sys.argv)
  7. Stephan Sokolow revised this gist Feb 20, 2018. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion spelltextedit.py
    Original file line number Diff line number Diff line change
    @@ -106,7 +106,7 @@ def createSpellcheckContextMenu(self, pos):
    spell_menu.addAction(action)

    # Only prepend "Spelling Suggestions" submenu if it's non-empty
    if len(spell_menu.actions()) != 0:
    if spell_menu.actions():
    menu.insertSeparator(menu.actions()[0])
    menu.insertMenu(menu.actions()[0], spell_menu)

  8. Stephan Sokolow revised this gist Feb 19, 2018. 1 changed file with 8 additions and 7 deletions.
    15 changes: 8 additions & 7 deletions spelltextedit.py
    Original file line number Diff line number Diff line change
    @@ -126,18 +126,19 @@ class EnchantHighlighter(QSyntaxHighlighter):
    tokenizer = None
    token_filters = (tokenize.EmailFilter, tokenize.URLFilter)

    # Define the spellcheck style once and just assign it as necessary
    # XXX: Does QSyntaxHighlighter.setFormat handle keeping this from
    # clobbering styles set in the data itself?
    err_format = QTextCharFormat()
    err_format.setUnderlineColor(Qt.red)
    err_format.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)

    def __init__(self, *args):
    QSyntaxHighlighter.__init__(self, *args)

    # Initialize private members
    self._sp_dict = None

    # Define the spellcheck style once and assign it as necessary
    # XXX: This will probably cause problems if I need to set underline
    # styles on rich text.
    self.err_format = QTextCharFormat()
    self.err_format.setUnderlineColor(Qt.red)
    self.err_format.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)

    def dict(self):
    """Gets the spelling dictionary in use"""
    return self._sp_dict
  9. Stephan Sokolow revised this gist Feb 19, 2018. 1 changed file with 8 additions and 4 deletions.
    12 changes: 8 additions & 4 deletions spelltextedit.py
    Original file line number Diff line number Diff line change
    @@ -46,7 +46,11 @@

    class SpellTextEdit(QPlainTextEdit):
    """QPlainTextEdit subclass which does spell-checking using PyEnchant"""
    max_suggestion_limit = 20

    # Clamping value for words like "regex" which suggest so many things that
    # the menu runs from the top to the bottom of the screen and spills over
    # into a second column.
    max_suggestions = 20

    def __init__(self, *args):
    QPlainTextEdit.__init__(self, *args)
    @@ -88,15 +92,15 @@ def createSpellcheckContextMenu(self, pos):
    # suggestions if it is
    if cursor.hasSelection():
    text = cursor.selectedText()
    spl_dict = self.highlighter.dict()
    spell_dict = self.highlighter.dict()

    if not spl_dict.check(text):
    if not spell_dict.check(text):
    spell_menu = QMenu('Spelling Suggestions', menu)
    spell_menu.triggered.connect(self.cb_correct_word)

    # TODO: Use enchant.utils.trim_suggestions once I don't have to
    # support old PyEnchant versions.
    for word in spl_dict.suggest(text)[:self.max_suggestion_limit]:
    for word in spell_dict.suggest(text)[:self.max_suggestions]:
    action = QAction(word, spell_menu)
    action.setData((cursor, word))
    spell_menu.addAction(action)
  10. Stephan Sokolow revised this gist Feb 19, 2018. 1 changed file with 24 additions and 15 deletions.
    39 changes: 24 additions & 15 deletions spelltextedit.py
    Original file line number Diff line number Diff line change
    @@ -57,16 +57,31 @@ def __init__(self, *args):

    def contextMenuEvent(self, event):
    """Custom context menu handler to add a spelling suggestions submenu"""
    # TODO: Use the version of createStandardContextMenu which takes
    # QPoint(event.x(), event.y()) once I don't need to support Qt versions
    # before 5.5. (It's recommended as it allows more contextual contents)
    popup_menu = self.createStandardContextMenu()
    popup_menu = self.createSpellcheckContextMenu(event.pos())
    popup_menu.exec_(event.globalPos())

    # Fix bug observed in Qt 5.2.1 on *buntu 14.04 LTS where:
    # 1. The cursor remains invisible after closing the context menu
    # 2. Keyboard input causes it to appear, but it doesn't blink
    # 3. Switching focus away from and back to the window fixes it
    self.focusInEvent(QFocusEvent(QEvent.FocusIn))

    def createSpellcheckContextMenu(self, pos):
    """Create and return a context menu with spelling suggestions.
    This may be used as an alternative to the QPoint-taking form of
    ``createStandardContextMenu`` and will work on pre-5.5 Qt.
    """
    try: # Recommended for Qt 5.5+ (Allows contextual Qt-provided entries)
    menu = self.createStandardContextMenu(pos)
    except TypeError: # Before Qt 5.5
    menu = self.createStandardContextMenu()

    # Create a cursor object selecting the word under the mouse pointer
    # FIXME: This method tokenizes differently than the highlighter and
    # allows one to right-click an un-highlighted URL and find a
    # suggestions menu when there shouldn't be one.
    cursor = self.cursorForPosition(event.pos())
    cursor = self.cursorForPosition(pos)
    cursor.select(QTextCursor.WordUnderCursor)

    # Check if the selected word is misspelled and offer spelling
    @@ -76,7 +91,7 @@ def contextMenuEvent(self, event):
    spl_dict = self.highlighter.dict()

    if not spl_dict.check(text):
    spell_menu = QMenu('Spelling Suggestions')
    spell_menu = QMenu('Spelling Suggestions', menu)
    spell_menu.triggered.connect(self.cb_correct_word)

    # TODO: Use enchant.utils.trim_suggestions once I don't have to
    @@ -88,16 +103,10 @@ def contextMenuEvent(self, event):

    # Only prepend "Spelling Suggestions" submenu if it's non-empty
    if len(spell_menu.actions()) != 0:
    popup_menu.insertSeparator(popup_menu.actions()[0])
    popup_menu.insertMenu(popup_menu.actions()[0], spell_menu)

    popup_menu.exec_(event.globalPos())
    menu.insertSeparator(menu.actions()[0])
    menu.insertMenu(menu.actions()[0], spell_menu)

    # Fix bug observed in Qt 5.2.1 on *buntu 14.04 LTS where:
    # 1. The cursor remains invisible after closing the context menu
    # 2. Keyboard input causes it to appear, but it doesn't blink
    # 3. Switching focus away from and back to the window fixes it
    self.focusInEvent(QFocusEvent(QEvent.FocusIn))
    return menu

    def cb_correct_word(self, action): # pylint: disable=no-self-use
    """Event handler Handler for 'Spelling Suggestions' entries."""
  11. Stephan Sokolow revised this gist Feb 19, 2018. 1 changed file with 7 additions and 7 deletions.
    14 changes: 7 additions & 7 deletions spelltextedit.py
    Original file line number Diff line number Diff line change
    @@ -110,13 +110,13 @@ def cb_correct_word(self, action): # pylint: disable=no-self-use

    class EnchantHighlighter(QSyntaxHighlighter):
    """QSyntaxHighlighter subclass which consults a PyEnchant dictionary"""
    tokenizer = None
    token_filters = (tokenize.EmailFilter, tokenize.URLFilter)

    def __init__(self, *args):
    QSyntaxHighlighter.__init__(self, *args)

    self.sp_dict = None
    self.tokenizer = None
    self._sp_dict = None

    # Define the spellcheck style once and assign it as necessary
    # XXX: This will probably cause problems if I need to set underline
    @@ -127,15 +127,15 @@ def __init__(self, *args):

    def dict(self):
    """Gets the spelling dictionary in use"""
    return self.sp_dict
    return self._sp_dict

    def setDict(self, sp_dict):
    """Sets the spelling dictionary to be used"""
    self.sp_dict = sp_dict
    self._sp_dict = sp_dict

    try:
    self.tokenizer = tokenize.get_tokenizer(
    sp_dict.tag, filters=self.token_filters)
    self._sp_dict.tag, filters=self.token_filters)
    except TokenizerNotFoundError:
    # Fall back to the "good for most euro languages" English tokenizer
    self.tokenizer = tokenize.get_tokenizer(filters=self.token_filters)
    @@ -144,11 +144,11 @@ def setDict(self, sp_dict):

    def highlightBlock(self, text):
    """Overridden QSyntaxHighlighter method to apply the highlight"""
    if not self.sp_dict:
    if not self._sp_dict:
    return

    for (word, pos) in self.tokenizer(text):
    if not self.sp_dict.check(word):
    if not self._sp_dict.check(word):
    self.setFormat(pos, len(word), self.err_format)

    if __name__ == '__main__':
  12. Stephan Sokolow revised this gist Feb 19, 2018. 1 changed file with 10 additions and 3 deletions.
    13 changes: 10 additions & 3 deletions spelltextedit.py
    Original file line number Diff line number Diff line change
    @@ -57,24 +57,31 @@ def __init__(self, *args):

    def contextMenuEvent(self, event):
    """Custom context menu handler to add a spelling suggestions submenu"""
    max_sugg = self.max_suggestion_limit
    # TODO: Use the version of createStandardContextMenu which takes
    # QPoint(event.x(), event.y()) once I don't need to support Qt versions
    # before 5.5. (It's recommended as it allows more contextual contents)
    popup_menu = self.createStandardContextMenu()

    # Create a cursor object selecting the word under the mouse pointer
    # FIXME: This method tokenizes differently than the highlighter and
    # allows one to right-click an un-highlighted URL and find a
    # suggestions menu when there shouldn't be one.
    cursor = self.cursorForPosition(event.pos())
    cursor.select(QTextCursor.WordUnderCursor)

    # Check if the selected word is misspelled and offer spelling
    # suggestions if it is
    if cursor.hasSelection():
    text = cursor.selectedText()
    if not self.highlighter.sp_dict.check(text):
    spl_dict = self.highlighter.dict()

    if not spl_dict.check(text):
    spell_menu = QMenu('Spelling Suggestions')
    spell_menu.triggered.connect(self.cb_correct_word)

    # TODO: Use enchant.utils.trim_suggestions once I don't have to
    # support old PyEnchant versions.
    for word in self.highlighter.sp_dict.suggest(text)[:max_sugg]:
    for word in spl_dict.suggest(text)[:self.max_suggestion_limit]:
    action = QAction(word, spell_menu)
    action.setData((cursor, word))
    spell_menu.addAction(action)
  13. Stephan Sokolow revised this gist Feb 19, 2018. 1 changed file with 9 additions and 1 deletion.
    10 changes: 9 additions & 1 deletion spelltextedit.py
    Original file line number Diff line number Diff line change
    @@ -39,7 +39,9 @@

    # pylint: disable=no-name-in-module
    from PyQt5.Qt import Qt
    from PyQt5.QtGui import QSyntaxHighlighter, QTextCharFormat, QTextCursor
    from PyQt5.QtCore import QEvent
    from PyQt5.QtGui import (QFocusEvent, QSyntaxHighlighter, QTextCharFormat,
    QTextCursor)
    from PyQt5.QtWidgets import QAction, QApplication, QMenu, QPlainTextEdit

    class SpellTextEdit(QPlainTextEdit):
    @@ -84,6 +86,12 @@ def contextMenuEvent(self, event):

    popup_menu.exec_(event.globalPos())

    # Fix bug observed in Qt 5.2.1 on *buntu 14.04 LTS where:
    # 1. The cursor remains invisible after closing the context menu
    # 2. Keyboard input causes it to appear, but it doesn't blink
    # 3. Switching focus away from and back to the window fixes it
    self.focusInEvent(QFocusEvent(QEvent.FocusIn))

    def cb_correct_word(self, action): # pylint: disable=no-self-use
    """Event handler Handler for 'Spelling Suggestions' entries."""
    cursor, word = action.data()
  14. Stephan Sokolow revised this gist Feb 19, 2018. 1 changed file with 4 additions and 0 deletions.
    4 changes: 4 additions & 0 deletions spelltextedit.py
    Original file line number Diff line number Diff line change
    @@ -110,6 +110,10 @@ def __init__(self, *args):
    self.err_format.setUnderlineColor(Qt.red)
    self.err_format.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)

    def dict(self):
    """Gets the spelling dictionary in use"""
    return self.sp_dict

    def setDict(self, sp_dict):
    """Sets the spelling dictionary to be used"""
    self.sp_dict = sp_dict
  15. Stephan Sokolow revised this gist Feb 19, 2018. 1 changed file with 7 additions and 19 deletions.
    26 changes: 7 additions & 19 deletions spelltextedit.py
    Original file line number Diff line number Diff line change
    @@ -41,7 +41,6 @@
    from PyQt5.Qt import Qt
    from PyQt5.QtGui import QSyntaxHighlighter, QTextCharFormat, QTextCursor
    from PyQt5.QtWidgets import QAction, QApplication, QMenu, QPlainTextEdit
    from PyQt5.QtCore import pyqtSignal

    class SpellTextEdit(QPlainTextEdit):
    """QPlainTextEdit subclass which does spell-checking using PyEnchant"""
    @@ -69,13 +68,13 @@ def contextMenuEvent(self, event):
    text = cursor.selectedText()
    if not self.highlighter.sp_dict.check(text):
    spell_menu = QMenu('Spelling Suggestions')
    spell_menu.triggered.connect(self.cb_correct_word)

    # TODO: Use enchant.utils.trim_suggestions once I don't have to
    # support old PyEnchant versions.
    for word in self.highlighter.sp_dict.suggest(text)[:max_sugg]:
    action = SpellAction(word, spell_menu)
    action.setData(cursor)
    action.correct.connect(self.correctWord)
    action = QAction(word, spell_menu)
    action.setData((cursor, word))
    spell_menu.addAction(action)

    # Only prepend "Spelling Suggestions" submenu if it's non-empty
    @@ -85,14 +84,15 @@ def contextMenuEvent(self, event):

    popup_menu.exec_(event.globalPos())

    def correctWord(self, cursor, word):
    """Replaces the text selected by ``cursor`` with ``word``."""
    def cb_correct_word(self, action): # pylint: disable=no-self-use
    """Event handler Handler for 'Spelling Suggestions' entries."""
    cursor, word = action.data()

    cursor.beginEditBlock()
    cursor.removeSelectedText()
    cursor.insertText(word)
    cursor.endEditBlock()


    class EnchantHighlighter(QSyntaxHighlighter):
    """QSyntaxHighlighter subclass which consults a PyEnchant dictionary"""
    token_filters = (tokenize.EmailFilter, tokenize.URLFilter)
    @@ -129,21 +129,9 @@ def highlightBlock(self, text):
    return

    for (word, pos) in self.tokenizer(text):

    if not self.sp_dict.check(word):
    self.setFormat(pos, len(word), self.err_format)

    class SpellAction(QAction): # pylint: disable=too-few-public-methods
    """A special QAction that returns the text in a signal."""

    correct = pyqtSignal((QTextCursor, str))

    def __init__(self, *args):
    QAction.__init__(self, *args)

    self.triggered.connect(
    lambda x: self.correct.emit(self.data(), self.text()))

    if __name__ == '__main__':
    app = QApplication(sys.argv)

  16. Stephan Sokolow revised this gist Feb 19, 2018. 1 changed file with 33 additions and 25 deletions.
    58 changes: 33 additions & 25 deletions spelltextedit.py
    Original file line number Diff line number Diff line change
    @@ -31,10 +31,11 @@
    __author__ = 'John Schember; Stephan Sokolow'
    __docformat__ = 'restructuredtext en'

    import re
    import sys

    import enchant
    from enchant import tokenize
    from enchant.errors import TokenizerNotFoundError

    # pylint: disable=no-name-in-module
    from PyQt5.Qt import Qt
    @@ -44,37 +45,40 @@

    class SpellTextEdit(QPlainTextEdit):
    """QPlainTextEdit subclass which does spell-checking using PyEnchant"""
    max_suggestion_limit = 20

    def __init__(self, *args):
    QPlainTextEdit.__init__(self, *args)

    # Default dictionary based on the current locale.
    self.sp_dict = enchant.Dict()
    # Start with a default dictionary based on the current locale.
    self.highlighter = EnchantHighlighter(self.document())
    self.highlighter.setDict(self.sp_dict)
    self.highlighter.setDict(enchant.Dict())

    def contextMenuEvent(self, event):
    """Custom context menu handler to add a spelling suggestions submenu"""
    max_sugg = self.max_suggestion_limit
    popup_menu = self.createStandardContextMenu()

    # Select the word under the cursor.
    # Create a cursor object selecting the word under the mouse pointer
    cursor = self.cursorForPosition(event.pos())
    cursor.select(QTextCursor.WordUnderCursor)

    # Check if the selected word is misspelled and offer spelling
    # suggestions if it is.
    # suggestions if it is
    if cursor.hasSelection():
    text = cursor.selectedText()
    if not self.sp_dict.check(text):
    if not self.highlighter.sp_dict.check(text):
    spell_menu = QMenu('Spelling Suggestions')

    for word in self.sp_dict.suggest(text):
    # TODO: Use enchant.utils.trim_suggestions once I don't have to
    # support old PyEnchant versions.
    for word in self.highlighter.sp_dict.suggest(text)[:max_sugg]:
    action = SpellAction(word, spell_menu)
    action.setData(cursor)
    action.correct.connect(self.correctWord)
    spell_menu.addAction(action)

    # Only add the Spelling Suggestions submenu if it's non-empty
    # Only prepend "Spelling Suggestions" submenu if it's non-empty
    if len(spell_menu.actions()) != 0:
    popup_menu.insertSeparator(popup_menu.actions()[0])
    popup_menu.insertMenu(popup_menu.actions()[0], spell_menu)
    @@ -91,39 +95,43 @@ def correctWord(self, cursor, word):

    class EnchantHighlighter(QSyntaxHighlighter):
    """QSyntaxHighlighter subclass which consults a PyEnchant dictionary"""
    WORDS = r'(?iu)\w([\w\']*\w)?'
    token_filters = (tokenize.EmailFilter, tokenize.URLFilter)

    def __init__(self, *args):
    QSyntaxHighlighter.__init__(self, *args)

    self.sp_dict = None
    self.tokenizer = None

    # Define the spellcheck style once and assign it as necessary
    # XXX: This will probably cause problems if I need to set underline
    # styles on rich text.
    self.err_format = QTextCharFormat()
    self.err_format.setUnderlineColor(Qt.red)
    self.err_format.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)

    def setDict(self, sp_dict):
    """Sets the spelling dictionary to be used"""
    self.sp_dict = sp_dict

    try:
    self.tokenizer = tokenize.get_tokenizer(
    sp_dict.tag, filters=self.token_filters)
    except TokenizerNotFoundError:
    # Fall back to the "good for most euro languages" English tokenizer
    self.tokenizer = tokenize.get_tokenizer(filters=self.token_filters)

    self.rehighlight()

    def highlightBlock(self, text):
    """Overridden QSyntaxHighlighter method to apply the highlight"""
    if not self.sp_dict:
    return

    char_format = QTextCharFormat()
    char_format.setUnderlineColor(Qt.red)
    char_format.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)

    # TODO: Don't spell-check "words" within URLs
    for word_object in re.finditer(self.WORDS, text):
    word = word_object.group()

    # PyEnchant throws an exception if we ask it to check empty strings
    # and we don't want it to spell-check purely numeric "words"
    if len(word) == 0 or word.isdigit():
    continue
    for (word, pos) in self.tokenizer(text):

    if not self.sp_dict.check(word):
    self.setFormat(word_object.start(),
    word_object.end() - word_object.start(), char_format)

    self.setFormat(pos, len(word), self.err_format)

    class SpellAction(QAction): # pylint: disable=too-few-public-methods
    """A special QAction that returns the text in a signal."""
  17. Stephan Sokolow revised this gist Feb 19, 2018. 1 changed file with 7 additions and 6 deletions.
    13 changes: 7 additions & 6 deletions spelltextedit.py
    Original file line number Diff line number Diff line change
    @@ -1,8 +1,8 @@
    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-
    """QPlainTextEdit With In Line Spell Check
    """QPlainTextEdit With Inline Spell Check
    Source:
    Original PyQt4 Version:
    https://nachtimwald.com/2009/08/22/qplaintextedit-with-in-line-spell-check/
    Copyright 2009 John Schember
    @@ -50,7 +50,7 @@ def __init__(self, *args):

    # Default dictionary based on the current locale.
    self.sp_dict = enchant.Dict()
    self.highlighter = Highlighter(self.document())
    self.highlighter = EnchantHighlighter(self.document())
    self.highlighter.setDict(self.sp_dict)

    def contextMenuEvent(self, event):
    @@ -67,13 +67,14 @@ def contextMenuEvent(self, event):
    text = cursor.selectedText()
    if not self.sp_dict.check(text):
    spell_menu = QMenu('Spelling Suggestions')

    for word in self.sp_dict.suggest(text):
    action = SpellAction(word, spell_menu)
    action.setData(cursor)
    action.correct.connect(self.correctWord)
    spell_menu.addAction(action)
    # Only add the spelling suggests to the menu if there are
    # suggestions.

    # Only add the Spelling Suggestions submenu if it's non-empty
    if len(spell_menu.actions()) != 0:
    popup_menu.insertSeparator(popup_menu.actions()[0])
    popup_menu.insertMenu(popup_menu.actions()[0], spell_menu)
    @@ -88,7 +89,7 @@ def correctWord(self, cursor, word):
    cursor.endEditBlock()


    class Highlighter(QSyntaxHighlighter):
    class EnchantHighlighter(QSyntaxHighlighter):
    """QSyntaxHighlighter subclass which consults a PyEnchant dictionary"""
    WORDS = r'(?iu)\w([\w\']*\w)?'

  18. Stephan Sokolow revised this gist Feb 19, 2018. 1 changed file with 6 additions and 8 deletions.
    14 changes: 6 additions & 8 deletions spelltextedit.py
    Original file line number Diff line number Diff line change
    @@ -60,7 +60,6 @@ def contextMenuEvent(self, event):
    # Select the word under the cursor.
    cursor = self.cursorForPosition(event.pos())
    cursor.select(QTextCursor.WordUnderCursor)
    self.setTextCursor(cursor)

    # Check if the selected word is misspelled and offer spelling
    # suggestions if it is.
    @@ -70,6 +69,7 @@ def contextMenuEvent(self, event):
    spell_menu = QMenu('Spelling Suggestions')
    for word in self.sp_dict.suggest(text):
    action = SpellAction(word, spell_menu)
    action.setData(cursor)
    action.correct.connect(self.correctWord)
    spell_menu.addAction(action)
    # Only add the spelling suggests to the menu if there are
    @@ -80,10 +80,8 @@ def contextMenuEvent(self, event):

    popup_menu.exec_(event.globalPos())

    def correctWord(self, word):
    """Replaces the selected text with word."""
    cursor = self.textCursor()

    def correctWord(self, cursor, word):
    """Replaces the text selected by ``cursor`` with ``word``."""
    cursor.beginEditBlock()
    cursor.removeSelectedText()
    cursor.insertText(word)
    @@ -129,13 +127,13 @@ def highlightBlock(self, text):
    class SpellAction(QAction): # pylint: disable=too-few-public-methods
    """A special QAction that returns the text in a signal."""

    correct = pyqtSignal(str)
    correct = pyqtSignal((QTextCursor, str))

    def __init__(self, *args):
    QAction.__init__(self, *args)

    self.triggered.connect(lambda x: self.correct.emit(
    self.text()))
    self.triggered.connect(
    lambda x: self.correct.emit(self.data(), self.text()))

    if __name__ == '__main__':
    app = QApplication(sys.argv)
  19. Stephan Sokolow revised this gist Feb 19, 2018. 1 changed file with 8 additions and 5 deletions.
    13 changes: 8 additions & 5 deletions spelltextedit.py
    Original file line number Diff line number Diff line change
    @@ -92,7 +92,7 @@ def correctWord(self, word):

    class Highlighter(QSyntaxHighlighter):
    """QSyntaxHighlighter subclass which consults a PyEnchant dictionary"""
    WORDS = r'(?iu)[\w\']+'
    WORDS = r'(?iu)\w([\w\']*\w)?'

    def __init__(self, *args):
    QSyntaxHighlighter.__init__(self, *args)
    @@ -108,17 +108,20 @@ def highlightBlock(self, text):
    if not self.sp_dict:
    return


    char_format = QTextCharFormat()
    char_format.setUnderlineColor(Qt.red)
    char_format.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)

    # TODO: Don't spell-check "words" within URLs
    for word_object in re.finditer(self.WORDS, text):
    # Don't spell-check purely numeric "words"
    if word_object.group().isdigit():
    word = word_object.group()

    # PyEnchant throws an exception if we ask it to check empty strings
    # and we don't want it to spell-check purely numeric "words"
    if len(word) == 0 or word.isdigit():
    continue

    if not self.sp_dict.check(word_object.group()):
    if not self.sp_dict.check(word):
    self.setFormat(word_object.start(),
    word_object.end() - word_object.start(), char_format)

  20. Stephan Sokolow revised this gist Feb 19, 2018. 1 changed file with 11 additions and 10 deletions.
    21 changes: 11 additions & 10 deletions spelltextedit.py
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,4 @@
    #!/usr/bin/env python
    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-
    """QPlainTextEdit With In Line Spell Check
    @@ -36,9 +36,11 @@

    import enchant

    from PyQt4.Qt import (QAction, QApplication, QMenu, QPlainTextEdit,
    QSyntaxHighlighter, QTextCharFormat, QTextCursor, Qt)
    from PyQt4.QtCore import pyqtSignal
    # pylint: disable=no-name-in-module
    from PyQt5.Qt import Qt
    from PyQt5.QtGui import QSyntaxHighlighter, QTextCharFormat, QTextCursor
    from PyQt5.QtWidgets import QAction, QApplication, QMenu, QPlainTextEdit
    from PyQt5.QtCore import pyqtSignal

    class SpellTextEdit(QPlainTextEdit):
    """QPlainTextEdit subclass which does spell-checking using PyEnchant"""
    @@ -63,7 +65,7 @@ def contextMenuEvent(self, event):
    # Check if the selected word is misspelled and offer spelling
    # suggestions if it is.
    if cursor.hasSelection():
    text = unicode(cursor.selectedText())
    text = cursor.selectedText()
    if not self.sp_dict.check(text):
    spell_menu = QMenu('Spelling Suggestions')
    for word in self.sp_dict.suggest(text):
    @@ -90,7 +92,7 @@ def correctWord(self, word):

    class Highlighter(QSyntaxHighlighter):
    """QSyntaxHighlighter subclass which consults a PyEnchant dictionary"""
    WORDS = ur'(?iu)[\w\']+'
    WORDS = r'(?iu)[\w\']+'

    def __init__(self, *args):
    QSyntaxHighlighter.__init__(self, *args)
    @@ -106,7 +108,6 @@ def highlightBlock(self, text):
    if not self.sp_dict:
    return

    text = unicode(text)

    char_format = QTextCharFormat()
    char_format.setUnderlineColor(Qt.red)
    @@ -122,16 +123,16 @@ def highlightBlock(self, text):
    word_object.end() - word_object.start(), char_format)


    class SpellAction(QAction):
    class SpellAction(QAction): # pylint: disable=too-few-public-methods
    """A special QAction that returns the text in a signal."""

    correct = pyqtSignal(unicode)
    correct = pyqtSignal(str)

    def __init__(self, *args):
    QAction.__init__(self, *args)

    self.triggered.connect(lambda x: self.correct.emit(
    unicode(self.text())))
    self.text()))

    if __name__ == '__main__':
    app = QApplication(sys.argv)
  21. Stephan Sokolow revised this gist Feb 19, 2018. 1 changed file with 22 additions and 1 deletion.
    23 changes: 22 additions & 1 deletion spelltextedit.py
    Original file line number Diff line number Diff line change
    @@ -4,10 +4,31 @@
    Source:
    https://nachtimwald.com/2009/08/22/qplaintextedit-with-in-line-spell-check/
    Copyright 2009 John Schember
    Copyright 2018 Stephan Sokolow
    Permission is hereby granted, free of charge, to any person obtaining a copy of
    this software and associated documentation files (the "Software"), to deal in
    the Software without restriction, including without limitation the rights to
    use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
    of the Software, and to permit persons to whom the Software is furnished to do
    so, subject to the following conditions:
    The above copyright notice and this permission notice shall be included in all
    copies or substantial portions of the Software.
    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    SOFTWARE.
    """

    __license__ = 'MIT'
    __author__ = '2009, John Schember '
    __author__ = 'John Schember; Stephan Sokolow'
    __docformat__ = 'restructuredtext en'

    import re
  22. Stephan Sokolow revised this gist Feb 19, 2018. 1 changed file with 4 additions and 0 deletions.
    4 changes: 4 additions & 0 deletions spelltextedit.py
    Original file line number Diff line number Diff line change
    @@ -92,6 +92,10 @@ def highlightBlock(self, text):
    char_format.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)

    for word_object in re.finditer(self.WORDS, text):
    # Don't spell-check purely numeric "words"
    if word_object.group().isdigit():
    continue

    if not self.sp_dict.check(word_object.group()):
    self.setFormat(word_object.start(),
    word_object.end() - word_object.start(), char_format)
  23. Stephan Sokolow renamed this gist Feb 19, 2018. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  24. Stephan Sokolow revised this gist Feb 19, 2018. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion with_spellcheck.py
    Original file line number Diff line number Diff line change
    @@ -45,7 +45,7 @@ def contextMenuEvent(self, event):
    text = unicode(cursor.selectedText())
    if not self.sp_dict.check(text):
    spell_menu = QMenu('Spelling Suggestions')
    for word in self.dict.suggest(text):
    for word in self.sp_dict.suggest(text):
    action = SpellAction(word, spell_menu)
    action.correct.connect(self.correctWord)
    spell_menu.addAction(action)
  25. Stephan Sokolow revised this gist Feb 19, 2018. 1 changed file with 27 additions and 38 deletions.
    65 changes: 27 additions & 38 deletions with_spellcheck.py
    Original file line number Diff line number Diff line change
    @@ -7,35 +7,31 @@
    """

    __license__ = 'MIT'
    __copyright__ = '2009, John Schember '
    __author__ = '2009, John Schember '
    __docformat__ = 'restructuredtext en'

    import re
    import sys

    import enchant

    from PyQt4.Qt import QAction
    from PyQt4.Qt import QApplication
    from PyQt4.Qt import QMenu
    from PyQt4.Qt import QPlainTextEdit
    from PyQt4.Qt import QSyntaxHighlighter
    from PyQt4.Qt import QTextCharFormat
    from PyQt4.Qt import QTextCursor
    from PyQt4.Qt import Qt
    from PyQt4.Qt import (QAction, QApplication, QMenu, QPlainTextEdit,
    QSyntaxHighlighter, QTextCharFormat, QTextCursor, Qt)
    from PyQt4.QtCore import pyqtSignal

    class SpellTextEdit(QPlainTextEdit):
    """QPlainTextEdit subclass which does spell-checking using PyEnchant"""

    def __init__(self, *args):
    QPlainTextEdit.__init__(self, *args)

    # Default dictionary based on the current locale.
    self.dict = enchant.Dict()
    self.sp_dict = enchant.Dict()
    self.highlighter = Highlighter(self.document())
    self.highlighter.setDict(self.dict)
    self.highlighter.setDict(self.sp_dict)

    def contextMenuEvent(self, event):
    """Custom context menu handler to add a spelling suggestions submenu"""
    popup_menu = self.createStandardContextMenu()

    # Select the word under the cursor.
    @@ -47,7 +43,7 @@ def contextMenuEvent(self, event):
    # suggestions if it is.
    if cursor.hasSelection():
    text = unicode(cursor.selectedText())
    if not self.dict.check(text):
    if not self.sp_dict.check(text):
    spell_menu = QMenu('Spelling Suggestions')
    for word in self.dict.suggest(text):
    action = SpellAction(word, spell_menu)
    @@ -62,50 +58,47 @@ def contextMenuEvent(self, event):
    popup_menu.exec_(event.globalPos())

    def correctWord(self, word):
    '''
    Replaces the selected text with word.
    '''
    """Replaces the selected text with word."""
    cursor = self.textCursor()
    cursor.beginEditBlock()

    cursor.beginEditBlock()
    cursor.removeSelectedText()
    cursor.insertText(word)

    cursor.endEditBlock()


    class Highlighter(QSyntaxHighlighter):
    WORDS = u'(?iu)[\w\']+'
    """QSyntaxHighlighter subclass which consults a PyEnchant dictionary"""
    WORDS = ur'(?iu)[\w\']+'

    def __init__(self, *args):
    QSyntaxHighlighter.__init__(self, *args)

    self.dict = None
    self.sp_dict = None

    def setDict(self, dict):
    self.dict = dict
    def setDict(self, sp_dict):
    """Sets the spelling dictionary to be used"""
    self.sp_dict = sp_dict

    def highlightBlock(self, text):
    if not self.dict:
    """Overridden QSyntaxHighlighter method to apply the highlight"""
    if not self.sp_dict:
    return

    text = unicode(text)

    format = QTextCharFormat()
    format.setUnderlineColor(Qt.red)
    format.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
    char_format = QTextCharFormat()
    char_format.setUnderlineColor(Qt.red)
    char_format.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)

    for word_object in re.finditer(self.WORDS, text):
    if not self.dict.check(word_object.group()):
    if not self.sp_dict.check(word_object.group()):
    self.setFormat(word_object.start(),
    word_object.end() - word_object.start(), format)
    word_object.end() - word_object.start(), char_format)


    class SpellAction(QAction):

    '''
    A special QAction that returns the text in a signal.
    '''
    """A special QAction that returns the text in a signal."""

    correct = pyqtSignal(unicode)

    @@ -115,14 +108,10 @@ def __init__(self, *args):
    self.triggered.connect(lambda x: self.correct.emit(
    unicode(self.text())))


    def main(args=sys.argv):
    app = QApplication(args)
    if __name__ == '__main__':
    app = QApplication(sys.argv)

    spellEdit = SpellTextEdit()
    spellEdit.show()

    return app.exec_()

    if __name__ == '__main__':
    sys.exit(main())
    sys.exit(app.exec_())
  26. Stephan Sokolow revised this gist Feb 19, 2018. 1 changed file with 3 additions and 13 deletions.
    16 changes: 3 additions & 13 deletions with_spellcheck.py
    Original file line number Diff line number Diff line change
    @@ -17,9 +17,7 @@

    from PyQt4.Qt import QAction
    from PyQt4.Qt import QApplication
    from PyQt4.Qt import QEvent
    from PyQt4.Qt import QMenu
    from PyQt4.Qt import QMouseEvent
    from PyQt4.Qt import QPlainTextEdit
    from PyQt4.Qt import QSyntaxHighlighter
    from PyQt4.Qt import QTextCharFormat
    @@ -37,26 +35,18 @@ def __init__(self, *args):
    self.highlighter = Highlighter(self.document())
    self.highlighter.setDict(self.dict)

    def mousePressEvent(self, event):
    if event.button() == Qt.RightButton:
    # Rewrite the mouse event to a left button event so the cursor is
    # moved to the location of the pointer.
    event = QMouseEvent(QEvent.MouseButtonPress, event.pos(),
    Qt.LeftButton, Qt.LeftButton, Qt.NoModifier)
    QPlainTextEdit.mousePressEvent(self, event)

    def contextMenuEvent(self, event):
    popup_menu = self.createStandardContextMenu()

    # Select the word under the cursor.
    cursor = self.textCursor()
    cursor = self.cursorForPosition(event.pos())
    cursor.select(QTextCursor.WordUnderCursor)
    self.setTextCursor(cursor)

    # Check if the selected word is misspelled and offer spelling
    # suggestions if it is.
    if self.textCursor().hasSelection():
    text = unicode(self.textCursor().selectedText())
    if cursor.hasSelection():
    text = unicode(cursor.selectedText())
    if not self.dict.check(text):
    spell_menu = QMenu('Spelling Suggestions')
    for word in self.dict.suggest(text):
  27. Stephan Sokolow created this gist Feb 19, 2018.
    138 changes: 138 additions & 0 deletions with_spellcheck.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,138 @@
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    """QPlainTextEdit With In Line Spell Check
    Source:
    https://nachtimwald.com/2009/08/22/qplaintextedit-with-in-line-spell-check/
    """

    __license__ = 'MIT'
    __copyright__ = '2009, John Schember '
    __docformat__ = 'restructuredtext en'

    import re
    import sys

    import enchant

    from PyQt4.Qt import QAction
    from PyQt4.Qt import QApplication
    from PyQt4.Qt import QEvent
    from PyQt4.Qt import QMenu
    from PyQt4.Qt import QMouseEvent
    from PyQt4.Qt import QPlainTextEdit
    from PyQt4.Qt import QSyntaxHighlighter
    from PyQt4.Qt import QTextCharFormat
    from PyQt4.Qt import QTextCursor
    from PyQt4.Qt import Qt
    from PyQt4.QtCore import pyqtSignal

    class SpellTextEdit(QPlainTextEdit):

    def __init__(self, *args):
    QPlainTextEdit.__init__(self, *args)

    # Default dictionary based on the current locale.
    self.dict = enchant.Dict()
    self.highlighter = Highlighter(self.document())
    self.highlighter.setDict(self.dict)

    def mousePressEvent(self, event):
    if event.button() == Qt.RightButton:
    # Rewrite the mouse event to a left button event so the cursor is
    # moved to the location of the pointer.
    event = QMouseEvent(QEvent.MouseButtonPress, event.pos(),
    Qt.LeftButton, Qt.LeftButton, Qt.NoModifier)
    QPlainTextEdit.mousePressEvent(self, event)

    def contextMenuEvent(self, event):
    popup_menu = self.createStandardContextMenu()

    # Select the word under the cursor.
    cursor = self.textCursor()
    cursor.select(QTextCursor.WordUnderCursor)
    self.setTextCursor(cursor)

    # Check if the selected word is misspelled and offer spelling
    # suggestions if it is.
    if self.textCursor().hasSelection():
    text = unicode(self.textCursor().selectedText())
    if not self.dict.check(text):
    spell_menu = QMenu('Spelling Suggestions')
    for word in self.dict.suggest(text):
    action = SpellAction(word, spell_menu)
    action.correct.connect(self.correctWord)
    spell_menu.addAction(action)
    # Only add the spelling suggests to the menu if there are
    # suggestions.
    if len(spell_menu.actions()) != 0:
    popup_menu.insertSeparator(popup_menu.actions()[0])
    popup_menu.insertMenu(popup_menu.actions()[0], spell_menu)

    popup_menu.exec_(event.globalPos())

    def correctWord(self, word):
    '''
    Replaces the selected text with word.
    '''
    cursor = self.textCursor()
    cursor.beginEditBlock()

    cursor.removeSelectedText()
    cursor.insertText(word)

    cursor.endEditBlock()


    class Highlighter(QSyntaxHighlighter):
    WORDS = u'(?iu)[\w\']+'

    def __init__(self, *args):
    QSyntaxHighlighter.__init__(self, *args)

    self.dict = None

    def setDict(self, dict):
    self.dict = dict

    def highlightBlock(self, text):
    if not self.dict:
    return

    text = unicode(text)

    format = QTextCharFormat()
    format.setUnderlineColor(Qt.red)
    format.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)

    for word_object in re.finditer(self.WORDS, text):
    if not self.dict.check(word_object.group()):
    self.setFormat(word_object.start(),
    word_object.end() - word_object.start(), format)


    class SpellAction(QAction):

    '''
    A special QAction that returns the text in a signal.
    '''

    correct = pyqtSignal(unicode)

    def __init__(self, *args):
    QAction.__init__(self, *args)

    self.triggered.connect(lambda x: self.correct.emit(
    unicode(self.text())))


    def main(args=sys.argv):
    app = QApplication(args)

    spellEdit = SpellTextEdit()
    spellEdit.show()

    return app.exec_()

    if __name__ == '__main__':
    sys.exit(main())