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.
87 down_pressed
= QtCore
.Signal()
89 # This widget is a single-line QTextEdit as described in
90 # http://blog.ssokolow.com/archives/2022/07/22/a-qlineedit-replacement-with-spell-checking/
91 def __init__(self
, context
, hint
, check
=None, parent
=None):
92 super(SpellCheckLineEdit
, self
).__init
__(
93 context
, hint
, check
=check
, parent
=parent
95 self
.setSizePolicy(QtWidgets
.QSizePolicy
.Expanding
, QtWidgets
.QSizePolicy
.Fixed
)
96 self
.setLineWrapMode(QtWidgets
.QTextEdit
.NoWrap
)
97 self
.setVerticalScrollBarPolicy(Qt
.ScrollBarAlwaysOff
)
98 self
.setHorizontalScrollBarPolicy(Qt
.ScrollBarAlwaysOff
)
99 self
.setWordWrapMode(QtGui
.QTextOption
.NoWrap
)
100 self
.setTabChangesFocus(True)
101 self
.textChanged
.connect(self
._trim
_changed
_text
_lines
, Qt
.QueuedConnection
)
103 def focusInEvent(self
, event
):
104 """Select text when entering with a tab to mimic QLineEdit"""
105 super(SpellCheckLineEdit
, self
).focusInEvent(event
)
107 if event
.reason() in (
108 Qt
.BacktabFocusReason
,
109 Qt
.ShortcutFocusReason
,
114 def focusOutEvent(self
, event
):
115 """De-select text when exiting with tab to mimic QLineEdit"""
116 super(SpellCheckLineEdit
, self
).focusOutEvent(event
)
118 if event
.reason() in (
119 Qt
.BacktabFocusReason
,
121 Qt
.ShortcutFocusReason
,
124 cur
= self
.textCursor()
125 cur
.movePosition(QtGui
.QTextCursor
.End
)
126 self
.setTextCursor(cur
)
128 def keyPressEvent(self
, event
):
129 """Handle the up/down arrow keys"""
130 event_key
= event
.key()
131 if event_key
== Qt
.Key_Up
:
132 cursor
= self
.textCursor()
133 if cursor
.position() == 0:
134 cursor
.clearSelection()
136 if event
.modifiers() & Qt
.ShiftModifier
:
137 mode
= QtGui
.QTextCursor
.KeepAnchor
139 mode
= QtGui
.QTextCursor
.MoveAnchor
140 cursor
.setPosition(0, mode
)
141 self
.setTextCursor(cursor
)
144 if event_key
== Qt
.Key_Down
:
145 cursor
= self
.textCursor()
146 cur_position
= cursor
.position()
147 end_position
= len(self
.value())
148 if cur_position
== end_position
:
149 cursor
.clearSelection()
150 self
.setTextCursor(cursor
)
151 self
.down_pressed
.emit()
153 if event
.modifiers() & Qt
.ShiftModifier
:
154 mode
= QtGui
.QTextCursor
.KeepAnchor
156 mode
= QtGui
.QTextCursor
.MoveAnchor
157 cursor
.setPosition(end_position
, mode
)
158 self
.setTextCursor(cursor
)
160 super(SpellCheckLineEdit
, self
).keyPressEvent(event
)
162 def minimumSizeHint(self
):
163 """Match QLineEdit's size behavior"""
164 block_fmt
= self
.document().firstBlock().blockFormat()
165 width
= super(SpellCheckLineEdit
, self
).minimumSizeHint().width()
167 QtGui
.QFontMetricsF(self
.font()).lineSpacing()
168 + block_fmt
.topMargin()
169 + block_fmt
.bottomMargin()
170 + self
.document().documentMargin()
171 + 2 * self
.frameWidth()
174 style_opts
= QtWidgets
.QStyleOptionFrame()
175 style_opts
.initFrom(self
)
176 style_opts
.lineWidth
= self
.frameWidth()
178 return self
.style().sizeFromContents(
179 QtWidgets
.QStyle
.CT_LineEdit
, style_opts
, QtCore
.QSize(width
, height
), self
183 """Use the minimum size as the sizeHint()"""
184 return self
.minimumSizeHint()
186 def _trim_changed_text_lines(self
):
187 """Trim the document to a single line to enforce a maximum of line line"""
188 # self.setMaximumBlockCount(1) Undo/Redo.
189 if self
.document().blockCount() > 1:
190 self
.document().setPlainText(self
.document().firstBlock().text())
193 class Highlighter(QtGui
.QSyntaxHighlighter
):
195 WORDS
= r
"(?iu)[\w']+"
197 def __init__(self
, doc
, spellcheck_widget
):
198 QtGui
.QSyntaxHighlighter
.__init
__(self
, doc
)
199 self
.spellcheck
= spellcheck_widget
202 def enable(self
, enabled
):
203 self
.enabled
= enabled
206 def highlightBlock(self
, text
):
209 fmt
= QtGui
.QTextCharFormat()
210 fmt
.setUnderlineColor(Qt
.red
)
211 fmt
.setUnderlineStyle(QtGui
.QTextCharFormat
.SpellCheckUnderline
)
213 for word_object
in re
.finditer(self
.WORDS
, text
):
214 if not self
.spellcheck
.check(word_object
.group()):
216 word_object
.start(), word_object
.end() - word_object
.start(), fmt
220 class SpellAction(QtWidgets
.QAction
):
221 """QAction that returns the text in a signal."""
223 result
= QtCore
.Signal(object)
225 def __init__(self
, *args
):
226 QtWidgets
.QAction
.__init
__(self
, *args
)
227 # pylint: disable=no-member
228 self
.triggered
.connect(self
.correct
)
231 self
.result
.emit(self
.text())