widgets.text: allow passing `n` to move()
[git-cola.git] / cola / widgets / text.py
blob05e56a0340deab2966f37c79de3a129583ea8a20
1 from __future__ import division, absolute_import, unicode_literals
3 from PyQt4 import QtGui, QtCore
4 from PyQt4.QtCore import Qt, SIGNAL
6 from cola import qtutils
7 from cola.compat import ustr
8 from cola.i18n import N_
9 from cola.models import prefs
10 from cola.widgets import defs
13 def get_value_stripped(widget):
14 return widget.as_unicode().strip()
17 class LineEdit(QtGui.QLineEdit):
19 def __init__(self, parent=None, row=1, get_value=None):
20 QtGui.QLineEdit.__init__(self, parent)
21 self._row = row
22 if get_value is None:
23 get_value = get_value_stripped
24 self._get_value = get_value
25 self.cursor_position = LineEditCursorPosition(self, row)
27 def value(self):
28 return self._get_value(self)
30 def set_value(self, value, block=False):
31 if block:
32 blocksig = self.blockSignals(True)
33 pos = self.cursorPosition()
34 self.setText(value)
35 self.setCursorPosition(pos)
36 if block:
37 self.blockSignals(blocksig)
39 def as_unicode(self):
40 return ustr(self.text())
42 def reset_cursor(self):
43 self.setCursorPosition(0)
46 class LineEditCursorPosition(object):
47 """Translate cursorPositionChanged(int,int) into cursorPosition(int,int)
48 """
49 def __init__(self, widget, row):
50 self._widget = widget
51 self._row = row
52 # Translate cursorPositionChanged into cursorPosition
53 widget.connect(widget, SIGNAL('cursorPositionChanged(int,int)'),
54 lambda old, new: self.emit())
56 def emit(self):
57 widget = self._widget
58 row = self._row
59 col = widget.cursorPosition()
60 widget.emit(SIGNAL('cursorPosition(int,int)'), row, col)
62 def reset(self):
63 self._widget.setCursorPosition(0)
66 class TextEdit(QtGui.QTextEdit):
68 def __init__(self, parent=None, get_value=None):
69 QtGui.QTextEdit.__init__(self, parent)
70 self.cursor_position = TextEditCursorPosition(self)
71 if get_value is None:
72 get_value = get_value_stripped
73 self._get_value = get_value
74 self._tabwidth = 8
75 self.setMinimumSize(QtCore.QSize(1, 1))
76 self.setLineWrapMode(QtGui.QTextEdit.NoWrap)
77 self.setAcceptRichText(False)
78 self.setCursorWidth(2)
80 def as_unicode(self):
81 return ustr(self.toPlainText())
83 def value(self):
84 return self._get_value(self)
86 def set_value(self, value, block=False):
87 if block:
88 blocksig = self.blockSignals(True)
89 cursor = self.textCursor()
90 self.setPlainText(value)
91 self.setTextCursor(cursor)
92 if block:
93 self.blockSignals(blocksig)
95 def reset_cursor(self):
96 cursor = self.textCursor()
97 cursor.setPosition(0)
98 self.setTextCursor(cursor)
100 def tabwidth(self):
101 return self._tabwidth
103 def set_tabwidth(self, width):
104 self._tabwidth = width
105 font = self.font()
106 fm = QtGui.QFontMetrics(font)
107 pixels = fm.width('M' * width)
108 self.setTabStopWidth(pixels)
110 def set_textwidth(self, width):
111 font = self.font()
112 fm = QtGui.QFontMetrics(font)
113 pixels = fm.width('M' * (width + 1)) + 1
114 self.setLineWrapColumnOrWidth(pixels)
116 def set_linebreak(self, brk):
117 if brk:
118 wrapmode = QtGui.QTextEdit.FixedPixelWidth
119 else:
120 wrapmode = QtGui.QTextEdit.NoWrap
121 self.setLineWrapMode(wrapmode)
123 def selected_line(self):
124 cursor = self.textCursor()
125 offset = cursor.position()
126 contents = ustr(self.toPlainText())
127 while (offset >= 1
128 and contents[offset-1]
129 and contents[offset-1] != '\n'):
130 offset -= 1
131 data = contents[offset:]
132 if '\n' in data:
133 line, rest = data.split('\n', 1)
134 else:
135 line = data
136 return line
138 def mousePressEvent(self, event):
139 # Move the text cursor so that the right-click events operate
140 # on the current position, not the last left-clicked position.
141 if event.button() == Qt.RightButton:
142 if not self.textCursor().hasSelection():
143 self.setTextCursor(self.cursorForPosition(event.pos()))
144 QtGui.QTextEdit.mousePressEvent(self, event)
147 class TextEditCursorPosition(object):
149 def __init__(self, widget):
150 self._widget = widget
151 widget.connect(widget, SIGNAL('cursorPositionChanged()'), self.emit)
153 def emit(self):
154 widget = self._widget
155 cursor = widget.textCursor()
156 position = cursor.position()
157 txt = widget.as_unicode()
158 before = txt[:position]
159 row = before.count('\n')
160 line = before.split('\n')[row]
161 col = cursor.columnNumber()
162 col += line[:col].count('\t') * (widget.tabwidth() - 1)
163 widget.emit(SIGNAL('cursorPosition(int,int)'), row+1, col)
165 def reset(self):
166 widget = self._widget
167 cursor = widget.textCursor()
168 cursor.setPosition(0)
169 widget.setTextCursor(cursor)
172 def setup_mono_font(widget):
173 widget.setFont(qtutils.diff_font())
174 widget.set_tabwidth(prefs.tabwidth())
177 def setup_readonly_flags(widget):
178 widget.setAcceptDrops(False)
179 widget.setTabChangesFocus(True)
180 widget.setUndoRedoEnabled(False)
181 widget.setTextInteractionFlags(Qt.TextSelectableByKeyboard |
182 Qt.TextSelectableByMouse)
185 class MonoTextEdit(TextEdit):
187 def __init__(self, parent):
188 TextEdit.__init__(self, parent)
189 setup_mono_font(self)
192 class MonoTextView(MonoTextEdit):
194 def __init__(self, parent):
195 MonoTextEdit.__init__(self, parent)
196 setup_readonly_flags(self)
199 def get_value_hinted(widget):
200 text = get_value_stripped(widget)
201 hint = widget.hint.value()
202 if text == hint:
203 return ''
204 else:
205 return text
208 class HintWidget(QtCore.QObject):
209 """Extend a widget to provide hint messages"""
211 def __init__(self, widget, hint):
212 QtCore.QObject.__init__(self, widget)
213 self._widget = widget
214 self._hint = hint
215 widget.installEventFilter(self)
217 # Palette for normal text
218 self.default_palette = QtGui.QPalette(widget.palette())
220 # Palette used for the placeholder text
221 self.hint_palette = pal = QtGui.QPalette(widget.palette())
222 color = self.hint_palette.text().color()
223 color.setAlpha(128)
224 pal.setColor(QtGui.QPalette.Active, QtGui.QPalette.Text, color)
225 pal.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Text, color)
227 def widget(self):
228 """Return the parent text widget"""
229 return self._widget
231 def active(self):
232 """Return True when hint-mode is active"""
233 return self.value() == get_value_stripped(self._widget)
235 def value(self):
236 """Return the current hint text"""
237 return self._hint
239 def set_value(self, hint):
240 """Change the hint text"""
241 # If hint-mode is currently active, re-activate it with the new text
242 active = self.active()
243 self._hint = hint
244 if active or self.active():
245 self.enable(True)
247 def enable(self, hint):
248 """Enable/disable hint-mode"""
249 if hint:
250 self._widget.set_value(self.value(), block=True)
251 self._widget.cursor_position.reset()
252 else:
253 self._widget.clear()
254 self._enable_hint_palette(hint)
256 def refresh(self):
257 """Update the palette to match the current mode"""
258 self._enable_hint_palette(self.active())
260 def _enable_hint_palette(self, hint):
261 """Enable/disable the hint-mode palette"""
262 if hint:
263 self._widget.setPalette(self.hint_palette)
264 else:
265 self._widget.setPalette(self.default_palette)
267 def eventFilter(self, obj, event):
268 """Enable/disable hint-mode when focus changes"""
269 etype = event.type()
270 if etype == QtCore.QEvent.FocusIn:
271 self._widget.hint.focus_in()
272 elif etype == QtCore.QEvent.FocusOut:
273 self._widget.hint.focus_out()
274 return False
276 def focus_in(self):
277 """Disable hint-mode when focused"""
278 widget = self.widget()
279 if self.active():
280 self.enable(False)
281 widget.cursor_position.emit()
283 def focus_out(self):
284 """Re-enable hint-mode when losing focus"""
285 widget = self.widget()
286 if not bool(widget.value()):
287 self.enable(True)
290 class HintedTextEdit(TextEdit):
292 def __init__(self, hint, parent=None):
293 TextEdit.__init__(self, parent=parent, get_value=get_value_hinted)
294 self.hint = HintWidget(self, hint)
295 setup_mono_font(self)
296 # Refresh palettes when text changes
297 self.connect(self, SIGNAL('textChanged()'), self.hint.refresh)
300 # The read-only variant.
301 class HintedTextView(HintedTextEdit):
303 def __init__(self, hint, parent=None):
304 HintedTextEdit.__init__(self, hint, parent=parent)
305 setup_readonly_flags(self)
308 # The vim-like read-only text view
310 class VimMixin(object):
312 def __init__(self, base):
313 self._base = base
314 # Common vim/unix-ish keyboard actions
315 self.add_navigation('Up', Qt.Key_K, shift=True)
316 self.add_navigation('Down', Qt.Key_J, shift=True)
317 self.add_navigation('Left', Qt.Key_H, shift=True)
318 self.add_navigation('Right', Qt.Key_L, shift=True)
319 self.add_navigation('WordLeft', Qt.Key_B)
320 self.add_navigation('WordRight', Qt.Key_W)
321 self.add_navigation('StartOfLine', Qt.Key_0)
322 self.add_navigation('EndOfLine', Qt.Key_Dollar)
324 qtutils.add_action(self, 'PageUp',
325 lambda: self.page(-self.height()//2),
326 Qt.ShiftModifier + Qt.Key_Shift)
328 qtutils.add_action(self, 'PageDown',
329 lambda: self.page(self.height()//2),
330 Qt.Key_Space)
332 def add_navigation(self, name, hotkey, shift=False):
333 """Add a hotkey along with a shift-variant"""
334 direction = getattr(QtGui.QTextCursor, name)
335 qtutils.add_action(self, name,
336 lambda: self.move(direction), hotkey)
337 if shift:
338 qtutils.add_action(self, 'Shift'+name,
339 lambda: self.move(direction, True),
340 Qt.ShiftModifier+hotkey)
342 def move(self, direction, select=False, n=1):
343 cursor = self.textCursor()
344 if select:
345 mode = QtGui.QTextCursor.KeepAnchor
346 else:
347 mode = QtGui.QTextCursor.MoveAnchor
348 if cursor.movePosition(direction, mode, n):
349 self.set_text_cursor(cursor)
351 def page(self, offset):
352 rect = self.cursorRect()
353 x = rect.x()
354 y = rect.y() + offset
355 new_cursor = self.cursorForPosition(QtCore.QPoint(x, y))
356 if new_cursor is not None:
357 self.set_text_cursor(new_cursor)
359 def set_text_cursor(self, cursor):
360 self.setTextCursor(cursor)
361 self.ensureCursorVisible()
362 self.viewport().update()
364 def keyPressEvent(self, event):
365 """Custom keyboard behaviors
367 The leave() signal is emitted when `Up` is pressed and we're already
368 at the beginning of the text. This allows the parent widget to
369 orchestrate some higher-level interaction, such as giving focus to
370 another widget.
372 When in the middle of the first line and `Up` is pressed, the cursor
373 is moved to the beginning of the line.
376 if event.key() == Qt.Key_Up:
377 cursor = self.textCursor()
378 position = cursor.position()
379 if position == 0:
380 # The cursor is at the beginning of the line.
381 # Emit a signal so that the parent can e.g. change focus.
382 self.emit(SIGNAL('leave()'))
383 elif self.value()[:position].count('\n') == 0:
384 # The cursor is in the middle of the first line of text.
385 # We can't go up ~ jump to the beginning of the line.
386 # Select the text if shift is pressed.
387 if event.modifiers() & Qt.ShiftModifier:
388 mode = QtGui.QTextCursor.KeepAnchor
389 else:
390 mode = QtGui.QTextCursor.MoveAnchor
391 cursor.movePosition(QtGui.QTextCursor.StartOfLine, mode)
392 self.setTextCursor(cursor)
394 return self._base.keyPressEvent(self, event)
397 class VimHintedTextView(VimMixin, HintedTextView):
399 def __init__(self, hint='', parent=None):
400 HintedTextView.__init__(self, hint, parent=parent)
401 VimMixin.__init__(self, HintedTextView)
404 class VimMonoTextView(VimMixin, MonoTextView):
406 def __init__(self, parent=None):
407 MonoTextView.__init__(self, parent)
408 VimMixin.__init__(self, MonoTextView)
411 class HintedLineEdit(LineEdit):
413 def __init__(self, hint='', parent=None):
414 LineEdit.__init__(self, parent=parent, get_value=get_value_hinted)
415 self.hint = HintWidget(self, hint)
416 self.setFont(qtutils.diff_font())
417 self.connect(self, SIGNAL('textChanged(QString)'),
418 lambda text: self.hint.refresh())
421 def text_dialog(text, title):
422 """Show a wall of text in a dialog"""
423 parent = qtutils.active_window()
424 label = QtGui.QLabel(parent)
425 label.setFont(qtutils.diff_font())
426 label.setText(text)
427 label.setTextInteractionFlags(Qt.NoTextInteraction)
429 widget = QtGui.QDialog(parent)
430 widget.setWindowModality(Qt.WindowModal)
431 widget.setWindowTitle(title)
433 layout = qtutils.hbox(defs.margin, defs.spacing, label)
434 widget.setLayout(layout)
436 qtutils.add_action(widget, N_('Close'), widget.accept,
437 Qt.Key_Question, Qt.Key_Enter, Qt.Key_Return)
438 widget.show()
439 return widget