spellcheck: disable horizontal scrollbars
[git-cola.git] / cola / widgets / spellcheck.py
blob410f6ae0ee7f7e7b84a4b1fd5f8fb60ea2b553e5
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 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.
42 spell_menu = None
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
53 # suggestions.
54 if spell_menu.actions():
55 popup_menu.addSeparator()
56 popup_menu.addMenu(spell_menu)
58 return popup_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)
76 cursor.endEditBlock()
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.
85 """
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,
110 Qt.TabFocusReason,
112 self.selectAll()
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,
120 Qt.MouseFocusReason,
121 Qt.ShortcutFocusReason,
122 Qt.TabFocusReason,
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()
135 else:
136 if event.modifiers() & Qt.ShiftModifier:
137 mode = QtGui.QTextCursor.KeepAnchor
138 else:
139 mode = QtGui.QTextCursor.MoveAnchor
140 cursor.setPosition(0, mode)
141 self.setTextCursor(cursor)
142 return
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()
152 else:
153 if event.modifiers() & Qt.ShiftModifier:
154 mode = QtGui.QTextCursor.KeepAnchor
155 else:
156 mode = QtGui.QTextCursor.MoveAnchor
157 cursor.setPosition(end_position, mode)
158 self.setTextCursor(cursor)
159 return
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()
166 height = int(
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
182 def sizeHint(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
200 self.enabled = False
202 def enable(self, enabled):
203 self.enabled = enabled
204 self.rehighlight()
206 def highlightBlock(self, text):
207 if not self.enabled:
208 return
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()):
215 self.setFormat(
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)
230 def correct(self):
231 self.result.emit(self.text())