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
)
23 get_value
= get_value_stripped
24 self
._get
_value
= get_value
25 self
.cursor_position
= LineEditCursorPosition(self
, row
)
28 return self
._get
_value
(self
)
30 def set_value(self
, value
, block
=False):
32 blocksig
= self
.blockSignals(True)
33 pos
= self
.cursorPosition()
35 self
.setCursorPosition(pos
)
37 self
.blockSignals(blocksig
)
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)
49 def __init__(self
, widget
, row
):
52 # Translate cursorPositionChanged into cursorPosition
53 widget
.connect(widget
, SIGNAL('cursorPositionChanged(int,int)'),
54 lambda old
, new
: self
.emit())
59 col
= widget
.cursorPosition()
60 widget
.emit(SIGNAL('cursorPosition(int,int)'), row
, col
)
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
)
72 get_value
= get_value_stripped
73 self
._get
_value
= get_value
75 self
.setMinimumSize(QtCore
.QSize(1, 1))
76 self
.setLineWrapMode(QtGui
.QTextEdit
.NoWrap
)
77 self
.setAcceptRichText(False)
78 self
.setCursorWidth(2)
81 return ustr(self
.toPlainText())
84 return self
._get
_value
(self
)
86 def set_value(self
, value
, block
=False):
88 blocksig
= self
.blockSignals(True)
89 cursor
= self
.textCursor()
90 self
.setPlainText(value
)
91 self
.setTextCursor(cursor
)
93 self
.blockSignals(blocksig
)
95 def reset_cursor(self
):
96 cursor
= self
.textCursor()
98 self
.setTextCursor(cursor
)
101 return self
._tabwidth
103 def set_tabwidth(self
, width
):
104 self
._tabwidth
= width
106 fm
= QtGui
.QFontMetrics(font
)
107 pixels
= fm
.width('M' * width
)
108 self
.setTabStopWidth(pixels
)
110 def set_textwidth(self
, width
):
112 fm
= QtGui
.QFontMetrics(font
)
113 pixels
= fm
.width('M' * (width
+ 1)) + 1
114 self
.setLineWrapColumnOrWidth(pixels
)
116 def set_linebreak(self
, brk
):
118 wrapmode
= QtGui
.QTextEdit
.FixedPixelWidth
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())
128 and contents
[offset
-1]
129 and contents
[offset
-1] != '\n'):
131 data
= contents
[offset
:]
133 line
, rest
= data
.split('\n', 1)
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
)
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
)
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()
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
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()
224 pal
.setColor(QtGui
.QPalette
.Active
, QtGui
.QPalette
.Text
, color
)
225 pal
.setColor(QtGui
.QPalette
.Inactive
, QtGui
.QPalette
.Text
, color
)
228 """Return the parent text widget"""
232 """Return True when hint-mode is active"""
233 return self
.value() == get_value_stripped(self
._widget
)
236 """Return the current hint text"""
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()
244 if active
or self
.active():
247 def enable(self
, hint
):
248 """Enable/disable hint-mode"""
250 self
._widget
.set_value(self
.value(), block
=True)
251 self
._widget
.cursor_position
.reset()
254 self
._enable
_hint
_palette
(hint
)
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"""
263 self
._widget
.setPalette(self
.hint_palette
)
265 self
._widget
.setPalette(self
.default_palette
)
267 def eventFilter(self
, obj
, event
):
268 """Enable/disable hint-mode when focus changes"""
270 if etype
== QtCore
.QEvent
.FocusIn
:
271 self
._widget
.hint
.focus_in()
272 elif etype
== QtCore
.QEvent
.FocusOut
:
273 self
._widget
.hint
.focus_out()
277 """Disable hint-mode when focused"""
278 widget
= self
.widget()
281 widget
.cursor_position
.emit()
284 """Re-enable hint-mode when losing focus"""
285 widget
= self
.widget()
286 if not bool(widget
.value()):
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
):
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),
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
)
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()
345 mode
= QtGui
.QTextCursor
.KeepAnchor
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()
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
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()
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
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())
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
)