1 from __future__
import absolute_import
, division
, print_function
, unicode_literals
4 from qtpy
import QtCore
6 from qtpy
import QtWidgets
7 from qtpy
.QtCore
import Qt
10 from .. import spellcheck
12 from .text
import HintedTextEdit
15 # pylint: disable=too-many-ancestors
16 class SpellCheckTextEdit(HintedTextEdit
):
17 def __init__(self
, context
, hint
, check
=None, parent
=None):
18 HintedTextEdit
.__init
__(self
, context
, hint
, parent
)
20 # Default dictionary based on the current locale.
21 self
.spellcheck
= check
or spellcheck
.NorvigSpellCheck()
22 self
.highlighter
= Highlighter(self
.document(), self
.spellcheck
)
24 def mousePressEvent(self
, event
):
25 if event
.button() == Qt
.RightButton
:
26 # Rewrite the mouse event to a left button event so the cursor is
27 # moved to the location of the pointer.
28 event
= QtGui
.QMouseEvent(
29 QtCore
.QEvent
.MouseButtonPress
,
35 HintedTextEdit
.mousePressEvent(self
, event
)
37 def create_context_menu(self
, event_pos
):
38 popup_menu
= super(SpellCheckTextEdit
, self
).create_context_menu(event_pos
)
40 # Check if the selected word is misspelled and offer spelling
41 # suggestions if it is.
43 if self
.textCursor().hasSelection():
44 text
= self
.textCursor().selectedText()
45 if not self
.spellcheck
.check(text
):
46 title
= N_('Spelling Suggestions')
47 spell_menu
= qtutils
.create_menu(title
, self
)
48 for word
in self
.spellcheck
.suggest(text
):
49 action
= SpellAction(word
, spell_menu
)
50 action
.result
.connect(self
.correct
)
51 spell_menu
.addAction(action
)
52 # Only add the spelling suggests to the menu if there are
54 if spell_menu
.actions():
55 popup_menu
.addSeparator()
56 popup_menu
.addMenu(spell_menu
)
60 def contextMenuEvent(self
, event
):
61 """Select the current word and then show a context menu"""
62 # Select the word under the cursor before calling the default contextMenuEvent.
63 cursor
= self
.textCursor()
64 cursor
.select(QtGui
.QTextCursor
.WordUnderCursor
)
65 self
.setTextCursor(cursor
)
66 super(SpellCheckTextEdit
, self
).contextMenuEvent(event
)
68 def correct(self
, word
):
69 """Replaces the selected text with word."""
70 cursor
= self
.textCursor()
71 cursor
.beginEditBlock()
73 cursor
.removeSelectedText()
74 cursor
.insertText(word
)
79 class SpellCheckLineEdit(SpellCheckTextEdit
):
80 """A fake QLineEdit that provides spellcheck capabilities
82 This class emulates QLineEdit using our QPlainTextEdit base class
83 so that we can leverage the existing spellcheck feature.
86 down_pressed
= QtCore
.Signal()
88 # This widget is a single-line QTextEdit as described in
89 # http://blog.ssokolow.com/archives/2022/07/22/a-qlineedit-replacement-with-spell-checking/
90 def __init__(self
, context
, hint
, check
=None, parent
=None):
91 super(SpellCheckLineEdit
, self
).__init
__(
92 context
, hint
, check
=check
, parent
=parent
94 self
.setSizePolicy(QtWidgets
.QSizePolicy
.Expanding
, QtWidgets
.QSizePolicy
.Fixed
)
95 self
.setLineWrapMode(QtWidgets
.QTextEdit
.NoWrap
)
96 self
.setVerticalScrollBarPolicy(Qt
.ScrollBarAlwaysOff
)
97 self
.setWordWrapMode(QtGui
.QTextOption
.NoWrap
)
98 self
.setTabChangesFocus(True)
99 self
.textChanged
.connect(self
._trim
_changed
_text
_lines
, Qt
.QueuedConnection
)
101 def focusInEvent(self
, event
):
102 """Select text when entering with a tab to mimic QLineEdit"""
103 super(SpellCheckLineEdit
, self
).focusInEvent(event
)
105 if event
.reason() in (
106 Qt
.BacktabFocusReason
,
107 Qt
.ShortcutFocusReason
,
112 def focusOutEvent(self
, event
):
113 """De-select text when exiting with tab to mimic QLineEdit"""
114 super(SpellCheckLineEdit
, self
).focusOutEvent(event
)
116 if event
.reason() in (
117 Qt
.BacktabFocusReason
,
119 Qt
.ShortcutFocusReason
,
122 cur
= self
.textCursor()
123 cur
.movePosition(QtGui
.QTextCursor
.End
)
124 self
.setTextCursor(cur
)
126 def keyPressEvent(self
, event
):
127 """Handle the up/down arrow keys"""
128 event_key
= event
.key()
129 if event_key
== Qt
.Key_Up
:
130 cursor
= self
.textCursor()
131 if cursor
.position() == 0:
132 cursor
.clearSelection()
134 if event
.modifiers() & Qt
.ShiftModifier
:
135 mode
= QtGui
.QTextCursor
.KeepAnchor
137 mode
= QtGui
.QTextCursor
.MoveAnchor
138 cursor
.setPosition(0, mode
)
139 self
.setTextCursor(cursor
)
142 if event_key
== Qt
.Key_Down
:
143 cursor
= self
.textCursor()
144 cur_position
= cursor
.position()
145 end_position
= len(self
.value())
146 if cur_position
== end_position
:
147 cursor
.clearSelection()
148 self
.setTextCursor(cursor
)
149 self
.down_pressed
.emit()
151 if event
.modifiers() & Qt
.ShiftModifier
:
152 mode
= QtGui
.QTextCursor
.KeepAnchor
154 mode
= QtGui
.QTextCursor
.MoveAnchor
155 cursor
.setPosition(end_position
, mode
)
156 self
.setTextCursor(cursor
)
158 super(SpellCheckLineEdit
, self
).keyPressEvent(event
)
160 def minimumSizeHint(self
):
161 """Match QLineEdit's size behavior"""
162 block_fmt
= self
.document().firstBlock().blockFormat()
163 width
= super(SpellCheckLineEdit
, self
).minimumSizeHint().width()
165 QtGui
.QFontMetricsF(self
.font()).lineSpacing() +
166 block_fmt
.topMargin() +
167 block_fmt
.bottomMargin() +
168 self
.document().documentMargin() +
169 2 * self
.frameWidth()
172 style_opts
= QtWidgets
.QStyleOptionFrame()
173 style_opts
.initFrom(self
)
174 style_opts
.lineWidth
= self
.frameWidth()
176 return self
.style().sizeFromContents(
177 QtWidgets
.QStyle
.CT_LineEdit
,
179 QtCore
.QSize(width
, height
),
184 """Use the minimum size as the sizeHint()"""
185 return self
.minimumSizeHint()
187 def _trim_changed_text_lines(self
):
188 """Trim the document to a single line to enforce a maximum of line line"""
189 # self.setMaximumBlockCount(1) Undo/Redo.
190 if self
.document().blockCount() > 1:
191 self
.document().setPlainText(self
.document().firstBlock().text())
194 class Highlighter(QtGui
.QSyntaxHighlighter
):
196 WORDS
= r
"(?iu)[\w']+"
198 def __init__(self
, doc
, spellcheck_widget
):
199 QtGui
.QSyntaxHighlighter
.__init
__(self
, doc
)
200 self
.spellcheck
= spellcheck_widget
203 def enable(self
, enabled
):
204 self
.enabled
= enabled
207 def highlightBlock(self
, text
):
210 fmt
= QtGui
.QTextCharFormat()
211 fmt
.setUnderlineColor(Qt
.red
)
212 fmt
.setUnderlineStyle(QtGui
.QTextCharFormat
.SpellCheckUnderline
)
214 for word_object
in re
.finditer(self
.WORDS
, text
):
215 if not self
.spellcheck
.check(word_object
.group()):
217 word_object
.start(), word_object
.end() - word_object
.start(), fmt
221 class SpellAction(QtWidgets
.QAction
):
222 """QAction that returns the text in a signal."""
224 result
= QtCore
.Signal(object)
226 def __init__(self
, *args
):
227 QtWidgets
.QAction
.__init
__(self
, *args
)
228 # pylint: disable=no-member
229 self
.triggered
.connect(self
.correct
)
232 self
.result
.emit(self
.text())