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 context_menu(self
):
38 popup_menu
= HintedTextEdit
.createStandardContextMenu(self
)
40 # Select the word under the cursor.
41 cursor
= self
.textCursor()
42 cursor
.select(QtGui
.QTextCursor
.WordUnderCursor
)
43 self
.setTextCursor(cursor
)
45 # Check if the selected word is misspelled and offer spelling
46 # suggestions if it is.
48 if self
.textCursor().hasSelection():
49 text
= self
.textCursor().selectedText()
50 if not self
.spellcheck
.check(text
):
51 title
= N_('Spelling Suggestions')
52 spell_menu
= qtutils
.create_menu(title
, self
)
53 for word
in self
.spellcheck
.suggest(text
):
54 action
= SpellAction(word
, spell_menu
)
55 action
.result
.connect(self
.correct
)
56 spell_menu
.addAction(action
)
57 # Only add the spelling suggests to the menu if there are
59 if spell_menu
.actions():
60 popup_menu
.addSeparator()
61 popup_menu
.addMenu(spell_menu
)
63 return popup_menu
, spell_menu
65 def contextMenuEvent(self
, event
):
66 popup_menu
, _
= self
.context_menu()
67 self
.build_context_menu(popup_menu
)
68 popup_menu
.exec_(self
.mapToGlobal(event
.pos()))
70 def build_context_menu(self
, menu
):
71 """Extension point for adding to the context menu"""
74 def correct(self
, word
):
75 """Replaces the selected text with word."""
76 cursor
= self
.textCursor()
77 cursor
.beginEditBlock()
79 cursor
.removeSelectedText()
80 cursor
.insertText(word
)
85 class SpellCheckLineEdit(SpellCheckTextEdit
):
86 """A fake QLineEdit that provides spellcheck capabilities
88 This class emulates QLineEdit using our QPlainTextEdit base class
89 so that we can leverage the existing spellcheck feature.
92 down_pressed
= QtCore
.Signal(object)
94 # Based on http://blog.ssokolow.com/archives/2022/07/22/a-qlineedit-replacement-with-spell-checking/
95 def __init__(self
, context
, hint
, check
=None, parent
=None):
96 super(SpellCheckLineEdit
, self
).__init
__(
97 context
, hint
, check
=check
, parent
=parent
99 self
.setSizePolicy(QtWidgets
.QSizePolicy
.Expanding
, QtWidgets
.QSizePolicy
.Fixed
)
100 self
.setLineWrapMode(QtWidgets
.QTextEdit
.NoWrap
)
101 self
.setVerticalScrollBarPolicy(Qt
.ScrollBarAlwaysOff
)
102 self
.setWordWrapMode(QtGui
.QTextOption
.NoWrap
)
103 self
.setTabChangesFocus(True)
104 self
.textChanged
.connect(self
._trim
_changed
_text
_lines
, Qt
.QueuedConnection
)
106 def focusInEvent(self
, event
):
107 """Select text when entering with a tab to mimic QLineEdit"""
108 super(SpellCheckLineEdit
, self
).focusInEvent(event
)
110 if event
.reason() in (
111 Qt
.BacktabFocusReason
,
112 Qt
.ShortcutFocusReason
,
117 def focusOutEvent(self
, event
):
118 """De-select text when exiting with tab to mimic QLineEdit"""
119 super(SpellCheckLineEdit
, self
).focusOutEvent(event
)
121 if event
.reason() in (
122 Qt
.BacktabFocusReason
,
124 Qt
.ShortcutFocusReason
,
127 cur
= self
.textCursor()
128 cur
.movePosition(QtGui
.QTextCursor
.End
)
129 self
.setTextCursor(cur
)
131 def keyPressEvent(self
, event
):
132 """Handle the up/down arrow keys"""
133 event_key
= event
.key()
134 if event_key
== Qt
.Key_Up
:
135 cursor
= self
.textCursor()
136 if cursor
.position() == 0:
137 cursor
.clearSelection()
139 if event
.modifiers() & Qt
.ShiftModifier
:
140 mode
= QtGui
.QTextCursor
.KeepAnchor
142 mode
= QtGui
.QTextCursor
.MoveAnchor
143 cursor
.setPosition(0, mode
)
144 self
.setTextCursor(cursor
)
147 if event_key
== Qt
.Key_Down
:
148 cursor
= self
.textCursor()
149 cur_position
= cursor
.position()
150 end_position
= len(self
.value())
151 if cur_position
== end_position
:
152 cursor
.clearSelection()
153 self
.setTextCursor(cursor
)
154 self
.down_pressed
.emit(event
.modifiers())
156 if event
.modifiers() & Qt
.ShiftModifier
:
157 mode
= QtGui
.QTextCursor
.KeepAnchor
159 mode
= QtGui
.QTextCursor
.MoveAnchor
160 cursor
.setPosition(end_position
, mode
)
161 self
.setTextCursor(cursor
)
163 super(SpellCheckLineEdit
, self
).keyPressEvent(event
)
165 def minimumSizeHint(self
):
166 """Match QLineEdit's size behavior"""
167 block_fmt
= self
.document().firstBlock().blockFormat()
168 width
= super(SpellCheckLineEdit
, self
).minimumSizeHint().width()
170 QtGui
.QFontMetricsF(self
.font()).lineSpacing() +
171 block_fmt
.topMargin() +
172 block_fmt
.bottomMargin() +
173 self
.document().documentMargin() +
174 2 * self
.frameWidth()
177 style_opts
= QtWidgets
.QStyleOptionFrame()
178 style_opts
.initFrom(self
)
179 style_opts
.lineWidth
= self
.frameWidth()
181 return self
.style().sizeFromContents(
182 QtWidgets
.QStyle
.CT_LineEdit
,
184 QtCore
.QSize(width
, height
),
189 """Use the minimum size as the sizeHint()"""
190 return self
.minimumSizeHint()
192 def _trim_changed_text_lines(self
):
193 """Trim the document to a single line to enforce a maximum of line line"""
194 # self.setMaximumBlockCount(1) Undo/Redo.
195 if self
.document().blockCount() > 1:
196 self
.document().setPlainText(self
.document().firstBlock().text())
199 class Highlighter(QtGui
.QSyntaxHighlighter
):
201 WORDS
= r
"(?iu)[\w']+"
203 def __init__(self
, doc
, spellcheck_widget
):
204 QtGui
.QSyntaxHighlighter
.__init
__(self
, doc
)
205 self
.spellcheck
= spellcheck_widget
208 def enable(self
, enabled
):
209 self
.enabled
= enabled
212 def highlightBlock(self
, text
):
215 fmt
= QtGui
.QTextCharFormat()
216 fmt
.setUnderlineColor(Qt
.red
)
217 fmt
.setUnderlineStyle(QtGui
.QTextCharFormat
.SpellCheckUnderline
)
219 for word_object
in re
.finditer(self
.WORDS
, text
):
220 if not self
.spellcheck
.check(word_object
.group()):
222 word_object
.start(), word_object
.end() - word_object
.start(), fmt
226 class SpellAction(QtWidgets
.QAction
):
227 """QAction that returns the text in a signal."""
229 result
= QtCore
.Signal(object)
231 def __init__(self
, *args
):
232 QtWidgets
.QAction
.__init
__(self
, *args
)
233 # pylint: disable=no-member
234 self
.triggered
.connect(self
.correct
)
237 self
.result
.emit(self
.text())