3 from qtpy
import QtCore
5 from qtpy
import QtWidgets
6 from qtpy
.QtCore
import Qt
9 from .. import spellcheck
11 from .text
import HintedTextEdit
14 # pylint: disable=too-many-ancestors
15 class SpellCheckTextEdit(HintedTextEdit
):
16 def __init__(self
, context
, hint
, check
=None, parent
=None):
17 HintedTextEdit
.__init
__(self
, context
, hint
, parent
)
19 # Default dictionary based on the current locale.
20 self
.spellcheck
= check
or spellcheck
.NorvigSpellCheck()
21 self
.highlighter
= Highlighter(self
.document(), self
.spellcheck
)
23 def mousePressEvent(self
, event
):
24 if event
.button() == Qt
.RightButton
:
25 # Rewrite the mouse event to a left button event so the cursor is
26 # moved to the location of the pointer.
27 event
= QtGui
.QMouseEvent(
28 QtCore
.QEvent
.MouseButtonPress
,
34 HintedTextEdit
.mousePressEvent(self
, event
)
36 def create_context_menu(self
, event_pos
):
37 popup_menu
= super().create_context_menu(event_pos
)
39 # Check if the selected word is misspelled and offer spelling
40 # suggestions if it is.
42 if self
.textCursor().hasSelection():
43 text
= self
.textCursor().selectedText()
44 if not self
.spellcheck
.check(text
):
45 title
= N_('Spelling Suggestions')
46 spell_menu
= qtutils
.create_menu(title
, self
)
47 for word
in self
.spellcheck
.suggest(text
):
48 action
= SpellAction(word
, spell_menu
)
49 action
.result
.connect(self
.correct
)
50 spell_menu
.addAction(action
)
51 # Only add the spelling suggests to the menu if there are
53 if spell_menu
.actions():
54 popup_menu
.addSeparator()
55 popup_menu
.addMenu(spell_menu
)
59 def contextMenuEvent(self
, event
):
60 """Select the current word and then show a context menu"""
61 # Select the word under the cursor before calling the default contextMenuEvent.
62 cursor
= self
.textCursor()
63 cursor
.select(QtGui
.QTextCursor
.WordUnderCursor
)
64 self
.setTextCursor(cursor
)
65 super().contextMenuEvent(event
)
67 def correct(self
, word
):
68 """Replaces the selected text with word."""
69 cursor
= self
.textCursor()
70 cursor
.beginEditBlock()
72 cursor
.removeSelectedText()
73 cursor
.insertText(word
)
78 class SpellCheckLineEdit(SpellCheckTextEdit
):
79 """A fake QLineEdit that provides spellcheck capabilities
81 This class emulates QLineEdit using our QPlainTextEdit base class
82 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().__init
__(context
, hint
, check
=check
, parent
=parent
)
92 self
.setSizePolicy(QtWidgets
.QSizePolicy
.Expanding
, QtWidgets
.QSizePolicy
.Fixed
)
93 self
.setLineWrapMode(QtWidgets
.QTextEdit
.NoWrap
)
94 self
.setVerticalScrollBarPolicy(Qt
.ScrollBarAlwaysOff
)
95 self
.setHorizontalScrollBarPolicy(Qt
.ScrollBarAlwaysOff
)
96 self
.setWordWrapMode(QtGui
.QTextOption
.NoWrap
)
97 self
.setTabChangesFocus(True)
98 self
.textChanged
.connect(self
._trim
_changed
_text
_lines
)
100 def focusInEvent(self
, event
):
101 """Select text when entering with a tab to mimic QLineEdit"""
102 super().focusInEvent(event
)
104 if event
.reason() in (
105 Qt
.BacktabFocusReason
,
106 Qt
.ShortcutFocusReason
,
111 def focusOutEvent(self
, event
):
112 """De-select text when exiting with tab to mimic QLineEdit"""
113 super().focusOutEvent(event
)
115 if event
.reason() in (
116 Qt
.BacktabFocusReason
,
118 Qt
.ShortcutFocusReason
,
121 cur
= self
.textCursor()
122 cur
.movePosition(QtGui
.QTextCursor
.End
)
123 self
.setTextCursor(cur
)
125 def keyPressEvent(self
, event
):
126 """Handle the up/down arrow keys"""
127 event_key
= event
.key()
128 if event_key
== Qt
.Key_Up
:
129 cursor
= self
.textCursor()
130 if cursor
.position() == 0:
131 cursor
.clearSelection()
133 if event
.modifiers() & Qt
.ShiftModifier
:
134 mode
= QtGui
.QTextCursor
.KeepAnchor
136 mode
= QtGui
.QTextCursor
.MoveAnchor
137 cursor
.setPosition(0, mode
)
138 self
.setTextCursor(cursor
)
141 if event_key
== Qt
.Key_Down
:
142 cursor
= self
.textCursor()
143 cur_position
= cursor
.position()
144 end_position
= len(self
.value())
145 if cur_position
== end_position
:
146 cursor
.clearSelection()
147 self
.setTextCursor(cursor
)
148 self
.down_pressed
.emit()
150 if event
.modifiers() & Qt
.ShiftModifier
:
151 mode
= QtGui
.QTextCursor
.KeepAnchor
153 mode
= QtGui
.QTextCursor
.MoveAnchor
154 cursor
.setPosition(end_position
, mode
)
155 self
.setTextCursor(cursor
)
157 super().keyPressEvent(event
)
159 def minimumSizeHint(self
):
160 """Match QLineEdit's size behavior"""
161 block_fmt
= self
.document().firstBlock().blockFormat()
162 width
= super().minimumSizeHint().width()
164 QtGui
.QFontMetricsF(self
.font()).lineSpacing()
165 + block_fmt
.topMargin()
166 + block_fmt
.bottomMargin()
167 + self
.document().documentMargin()
168 + 2 * self
.frameWidth()
171 style_opts
= QtWidgets
.QStyleOptionFrame()
172 style_opts
.initFrom(self
)
173 style_opts
.lineWidth
= self
.frameWidth()
175 return self
.style().sizeFromContents(
176 QtWidgets
.QStyle
.CT_LineEdit
, style_opts
, QtCore
.QSize(width
, height
), self
180 """Use the minimum size as the sizeHint()"""
181 return self
.minimumSizeHint()
183 def _trim_changed_text_lines(self
):
184 """Trim the document to a single line to enforce a maximum of one line"""
185 # self.setMaximumBlockCount(1) Undo/Redo.
186 if self
.document().blockCount() > 1:
187 self
.document().setPlainText(self
.document().firstBlock().text())
190 class Highlighter(QtGui
.QSyntaxHighlighter
):
191 WORDS
= r
"(?iu)[\w']+"
193 def __init__(self
, doc
, spellcheck_widget
):
194 QtGui
.QSyntaxHighlighter
.__init
__(self
, doc
)
195 self
.spellcheck
= spellcheck_widget
198 def enable(self
, enabled
):
199 self
.enabled
= enabled
202 def highlightBlock(self
, text
):
205 fmt
= QtGui
.QTextCharFormat()
206 fmt
.setUnderlineColor(Qt
.red
)
207 fmt
.setUnderlineStyle(QtGui
.QTextCharFormat
.SpellCheckUnderline
)
209 for word_object
in re
.finditer(self
.WORDS
, text
):
210 if not self
.spellcheck
.check(word_object
.group()):
212 word_object
.start(), word_object
.end() - word_object
.start(), fmt
216 class SpellAction(QtWidgets
.QAction
):
217 """QAction that returns the text in a signal."""
219 result
= QtCore
.Signal(object)
221 def __init__(self
, *args
):
222 QtWidgets
.QAction
.__init
__(self
, *args
)
223 # pylint: disable=no-member
224 self
.triggered
.connect(self
.correct
)
227 self
.result
.emit(self
.text())