widgets: flake8 and pylint fixes
[git-cola.git] / cola / widgets / spellcheck.py
blobb8f349a0bf81e6bda5992f2316ca1a6048817580
1 from __future__ import absolute_import, division, print_function, unicode_literals
2 import re
4 from qtpy import QtCore
5 from qtpy import QtGui
6 from qtpy import QtWidgets
7 from qtpy.QtCore import Qt
9 from .. import qtutils
10 from .. import spellcheck
11 from ..i18n import N_
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,
30 event.pos(),
31 Qt.LeftButton,
32 Qt.LeftButton,
33 Qt.NoModifier,
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.
47 spell_menu = None
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
58 # suggestions.
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"""
73 def correct(self, word):
74 """Replaces the selected text with word."""
75 cursor = self.textCursor()
76 cursor.beginEditBlock()
78 cursor.removeSelectedText()
79 cursor.insertText(word)
81 cursor.endEditBlock()
84 class SpellCheckLineEdit(SpellCheckTextEdit):
85 """A fake QLineEdit that provides spellcheck capabilities
87 This class emulates QLineEdit using our QPlainTextEdit base class
88 so that we can leverage the existing spellcheck feature.
90 """
91 down_pressed = QtCore.Signal()
93 # This widget is a single-line QTextEdit as described in
94 # 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,
113 Qt.TabFocusReason,
115 self.selectAll()
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,
123 Qt.MouseFocusReason,
124 Qt.ShortcutFocusReason,
125 Qt.TabFocusReason,
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()
138 else:
139 if event.modifiers() & Qt.ShiftModifier:
140 mode = QtGui.QTextCursor.KeepAnchor
141 else:
142 mode = QtGui.QTextCursor.MoveAnchor
143 cursor.setPosition(0, mode)
144 self.setTextCursor(cursor)
145 return
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()
155 else:
156 if event.modifiers() & Qt.ShiftModifier:
157 mode = QtGui.QTextCursor.KeepAnchor
158 else:
159 mode = QtGui.QTextCursor.MoveAnchor
160 cursor.setPosition(end_position, mode)
161 self.setTextCursor(cursor)
162 return
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()
169 height = int(
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,
183 style_opts,
184 QtCore.QSize(width, height),
185 self
188 def sizeHint(self):
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
206 self.enabled = False
208 def enable(self, enabled):
209 self.enabled = enabled
210 self.rehighlight()
212 def highlightBlock(self, text):
213 if not self.enabled:
214 return
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()):
221 self.setFormat(
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)
236 def correct(self):
237 self.result.emit(self.text())