text: apply the custom tabwidth during construction
[git-cola.git] / cola / widgets / text.py
blob767dbdf79346c0baaacaa33cbb53517a050e07a9
1 """Text widgets"""
2 # pylint: disable=unexpected-keyword-arg
3 from __future__ import division, absolute_import, unicode_literals
4 import math
6 from qtpy import QtCore
7 from qtpy import QtGui
8 from qtpy import QtWidgets
9 from qtpy.QtCore import Qt
10 from qtpy.QtCore import Signal
12 from ..models import prefs
13 from ..qtutils import get
14 from .. import hotkeys
15 from .. import qtutils
16 from ..i18n import N_
17 from . import defs
20 def get_stripped(widget):
21 return widget.get().strip()
24 class LineEdit(QtWidgets.QLineEdit):
26 cursor_changed = Signal(int, int)
28 def __init__(self, parent=None, row=1, get_value=None, clear_button=False):
29 QtWidgets.QLineEdit.__init__(self, parent)
30 self._row = row
31 if get_value is None:
32 get_value = get_stripped
33 self._get_value = get_value
34 self.cursor_position = LineEditCursorPosition(self, row)
36 if clear_button and hasattr(self, 'setClearButtonEnabled'):
37 self.setClearButtonEnabled(True)
39 def get(self):
40 """Return the raw unicode value from Qt"""
41 return self.text()
43 def value(self):
44 """Return the processed value, e.g. stripped"""
45 return self._get_value(self)
47 def set_value(self, value, block=False):
48 if block:
49 blocksig = self.blockSignals(True)
50 pos = self.cursorPosition()
51 self.setText(value)
52 self.setCursorPosition(pos)
53 if block:
54 self.blockSignals(blocksig)
57 class LineEditCursorPosition(object):
58 """Translate cursorPositionChanged(int,int) into cursorPosition(int,int)
59 """
60 def __init__(self, widget, row):
61 self._widget = widget
62 self._row = row
63 # Translate cursorPositionChanged into cursor_changed(int, int)
64 widget.cursorPositionChanged.connect(lambda old, new: self.emit())
66 def emit(self):
67 widget = self._widget
68 row = self._row
69 col = widget.cursorPosition()
70 widget.cursor_changed.emit(row, col)
72 def reset(self):
73 self._widget.setCursorPosition(0)
76 class BaseTextEditExtension(QtCore.QObject):
78 def __init__(self, widget, get_value, readonly):
79 QtCore.QObject.__init__(self, widget)
80 self.widget = widget
81 self.cursor_position = TextEditCursorPosition(widget, self)
82 if get_value is None:
83 get_value = get_stripped
84 self._get_value = get_value
85 self._tabwidth = 8
86 self._readonly = readonly
87 self._init_flags()
88 self.init()
90 def _init_flags(self):
91 widget = self.widget
92 widget.setMinimumSize(QtCore.QSize(1, 1))
93 widget.setWordWrapMode(QtGui.QTextOption.WordWrap)
94 widget.setLineWrapMode(widget.NoWrap)
95 if self._readonly:
96 widget.setReadOnly(True)
97 widget.setAcceptDrops(False)
98 widget.setTabChangesFocus(True)
99 widget.setUndoRedoEnabled(False)
100 widget.setTextInteractionFlags(Qt.TextSelectableByKeyboard |
101 Qt.TextSelectableByMouse)
103 def get(self):
104 """Return the raw unicode value from Qt"""
105 return self.widget.toPlainText()
107 def value(self):
108 """Return a safe value, e.g. a stripped value"""
109 return self._get_value(self.widget)
111 def set_value(self, value, block=False):
112 if block:
113 blocksig = self.widget.blockSignals(True)
115 # Save cursor position
116 offset, selection_text = self.offset_and_selection()
117 old_value = get(self.widget)
119 # Update text
120 self.widget.setPlainText(value)
122 # Restore cursor
123 if selection_text and selection_text in value:
124 # If the old selection exists in the new text then re-select it.
125 idx = value.index(selection_text)
126 cursor = self.widget.textCursor()
127 cursor.setPosition(idx)
128 cursor.setPosition(idx + len(selection_text),
129 QtGui.QTextCursor.KeepAnchor)
130 self.widget.setTextCursor(cursor)
132 elif value == old_value:
133 # Otherwise, if the text is identical and there is no selection
134 # then restore the cursor position.
135 cursor = self.widget.textCursor()
136 cursor.setPosition(offset)
137 self.widget.setTextCursor(cursor)
138 else:
139 # If none of the above applied then restore the cursor position.
140 position = max(0, min(offset, len(value) - 1))
141 cursor = self.widget.textCursor()
142 cursor.setPosition(position)
143 self.widget.setTextCursor(cursor)
144 cursor = self.widget.textCursor()
145 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
146 self.widget.setTextCursor(cursor)
148 if block:
149 self.widget.blockSignals(blocksig)
151 def set_cursor_position(self, new_position):
152 cursor = self.widget.textCursor()
153 cursor.setPosition(new_position)
154 self.widget.setTextCursor(cursor)
156 def tabwidth(self):
157 return self._tabwidth
159 def set_tabwidth(self, width):
160 self._tabwidth = width
161 font = self.widget.font()
162 fm = QtGui.QFontMetrics(font)
163 pixels = fm.width('M' * width)
164 self.widget.setTabStopWidth(pixels)
166 def selected_line(self):
167 contents = self.value()
168 cursor = self.widget.textCursor()
169 offset = min(cursor.position(), len(contents)-1)
170 while (offset >= 1 and
171 contents[offset-1] and
172 contents[offset-1] != '\n'):
173 offset -= 1
174 data = contents[offset:]
175 if '\n' in data:
176 line, _ = data.split('\n', 1)
177 else:
178 line = data
179 return line
181 def cursor(self):
182 return self.widget.textCursor()
184 def has_selection(self):
185 return self.cursor().hasSelection()
187 def offset_and_selection(self):
188 cursor = self.cursor()
189 offset = cursor.selectionStart()
190 selection_text = cursor.selection().toPlainText()
191 return offset, selection_text
193 def mouse_press_event(self, event):
194 # Move the text cursor so that the right-click events operate
195 # on the current position, not the last left-clicked position.
196 widget = self.widget
197 if event.button() == Qt.RightButton:
198 if not widget.textCursor().hasSelection():
199 cursor = widget.cursorForPosition(event.pos())
200 widget.setTextCursor(cursor)
202 # For extension by sub-classes
204 def init(self):
205 """Called during init for class-specific settings"""
206 pass
208 def set_textwidth(self, width):
209 """Set the text width"""
210 pass
212 def set_linebreak(self, brk):
213 """Enable word wrapping"""
214 pass
217 class PlainTextEditExtension(BaseTextEditExtension):
219 def set_linebreak(self, brk):
220 if brk:
221 wrapmode = QtWidgets.QPlainTextEdit.WidgetWidth
222 else:
223 wrapmode = QtWidgets.QPlainTextEdit.NoWrap
224 self.widget.setLineWrapMode(wrapmode)
227 class PlainTextEdit(QtWidgets.QPlainTextEdit):
229 cursor_changed = Signal(int, int)
230 leave = Signal()
232 def __init__(self, parent=None, get_value=None, readonly=False):
233 QtWidgets.QPlainTextEdit.__init__(self, parent)
234 self.ext = PlainTextEditExtension(self, get_value, readonly)
235 self.cursor_position = self.ext.cursor_position
237 def get(self):
238 """Return the raw unicode value from Qt"""
239 return self.ext.get()
241 # For compatibility with QTextEdit
242 def setText(self, value):
243 self.set_value(value)
245 def value(self):
246 """Return a safe value, e.g. a stripped value"""
247 return self.ext.value()
249 def set_value(self, value, block=False):
250 self.ext.set_value(value, block=block)
252 def has_selection(self):
253 return self.ext.has_selection()
255 def selected_line(self):
256 return self.ext.selected_line()
258 def set_tabwidth(self, width):
259 self.ext.set_tabwidth(width)
261 def set_textwidth(self, width):
262 self.ext.set_textwidth(width)
264 def set_linebreak(self, brk):
265 self.ext.set_linebreak(brk)
267 def mousePressEvent(self, event):
268 self.ext.mouse_press_event(event)
269 super(PlainTextEdit, self).mousePressEvent(event)
271 def wheelEvent(self, event):
272 """Disable control+wheelscroll text resizing"""
273 if event.modifiers() & Qt.ControlModifier:
274 event.ignore()
275 return
276 super(PlainTextEdit, self).wheelEvent(event)
279 class TextEditExtension(BaseTextEditExtension):
281 def init(self):
282 widget = self.widget
283 widget.setAcceptRichText(False)
285 def set_linebreak(self, brk):
286 if brk:
287 wrapmode = QtWidgets.QTextEdit.FixedColumnWidth
288 else:
289 wrapmode = QtWidgets.QTextEdit.NoWrap
290 self.widget.setLineWrapMode(wrapmode)
292 def set_textwidth(self, width):
293 self.widget.setLineWrapColumnOrWidth(width)
296 class TextEdit(QtWidgets.QTextEdit):
298 cursor_changed = Signal(int, int)
299 leave = Signal()
301 def __init__(self, parent=None, get_value=None, readonly=False):
302 QtWidgets.QTextEdit.__init__(self, parent)
303 self.ext = TextEditExtension(self, get_value, readonly)
304 self.cursor_position = self.ext.cursor_position
305 self.expandtab_enabled = False
307 def get(self):
308 """Return the raw unicode value from Qt"""
309 return self.ext.get()
311 def value(self):
312 """Return a safe value, e.g. a stripped value"""
313 return self.ext.value()
315 def set_value(self, value, block=False):
316 self.ext.set_value(value, block=block)
318 def selected_line(self):
319 return self.ext.selected_line()
321 def set_tabwidth(self, width):
322 self.ext.set_tabwidth(width)
324 def set_textwidth(self, width):
325 self.ext.set_textwidth(width)
327 def set_linebreak(self, brk):
328 self.ext.set_linebreak(brk)
330 def set_expandtab(self, value):
331 self.expandtab_enabled = value
333 def mousePressEvent(self, event):
334 self.ext.mouse_press_event(event)
335 super(TextEdit, self).mousePressEvent(event)
337 def wheelEvent(self, event):
338 """Disable control+wheelscroll text resizing"""
339 if event.modifiers() & Qt.ControlModifier:
340 event.ignore()
341 return
342 super(TextEdit, self).wheelEvent(event)
344 def should_expandtab(self, event):
345 return event.key() == Qt.Key_Tab and self.expandtab_enabled
347 def expandtab(self):
348 tabwidth = max(self.ext.tabwidth(), 1)
349 cursor = self.textCursor()
350 cursor.insertText(' ' * tabwidth)
352 def keyPressEvent(self, event):
353 expandtab = self.should_expandtab(event)
354 if expandtab:
355 self.expandtab()
356 event.accept()
357 else:
358 QtWidgets.QTextEdit.keyPressEvent(self, event)
360 def keyReleaseEvent(self, event):
361 expandtab = self.should_expandtab(event)
362 if expandtab:
363 event.ignore()
364 else:
365 QtWidgets.QTextEdit.keyReleaseEvent(self, event)
368 class TextEditCursorPosition(object):
370 def __init__(self, widget, ext):
371 self._widget = widget
372 self._ext = ext
373 widget.cursorPositionChanged.connect(self.emit)
375 def emit(self):
376 widget = self._widget
377 ext = self._ext
378 cursor = widget.textCursor()
379 position = cursor.position()
380 txt = widget.get()
381 before = txt[:position]
382 row = before.count('\n')
383 line = before.split('\n')[row]
384 col = cursor.columnNumber()
385 col += line[:col].count('\t') * (ext.tabwidth() - 1)
386 widget.cursor_changed.emit(row+1, col)
388 def reset(self):
389 widget = self._widget
390 cursor = widget.textCursor()
391 cursor.setPosition(0)
392 widget.setTextCursor(cursor)
395 class MonoTextEdit(PlainTextEdit):
397 def __init__(self, context, parent=None, readonly=False):
398 PlainTextEdit.__init__(self, parent=parent, readonly=readonly)
399 self.setFont(qtutils.diff_font(context))
402 def get_value_hinted(widget):
403 text = get_stripped(widget)
404 hint = get(widget.hint)
405 if text == hint:
406 return ''
407 return text
410 class HintWidget(QtCore.QObject):
411 """Extend a widget to provide hint messages
413 This primarily exists because setPlaceholderText() is only available
414 in Qt5, so this class provides consistent behavior across versions.
418 def __init__(self, widget, hint):
419 QtCore.QObject.__init__(self, widget)
420 self._widget = widget
421 self._hint = hint
422 self._is_error = False
424 self.modern = modern = hasattr(widget, 'setPlaceholderText')
425 if modern:
426 widget.setPlaceholderText(hint)
428 # Palette for normal text
429 QPalette = QtGui.QPalette
430 palette = widget.palette()
432 hint_color = palette.color(QPalette.Disabled, QPalette.Text)
433 error_bg_color = QtGui.QColor(Qt.red).darker()
434 error_fg_color = QtGui.QColor(Qt.white)
436 hint_rgb = qtutils.rgb_css(hint_color)
437 error_bg_rgb = qtutils.rgb_css(error_bg_color)
438 error_fg_rgb = qtutils.rgb_css(error_fg_color)
440 env = dict(name=widget.__class__.__name__,
441 error_fg_rgb=error_fg_rgb,
442 error_bg_rgb=error_bg_rgb,
443 hint_rgb=hint_rgb)
445 self._default_style = ''
447 self._hint_style = """
448 %(name)s {
449 color: %(hint_rgb)s;
451 """ % env
453 self._error_style = """
454 %(name)s {
455 color: %(error_fg_rgb)s;
456 background-color: %(error_bg_rgb)s;
458 """ % env
460 def init(self):
461 """Defered initialization"""
462 if self.modern:
463 self.widget().setPlaceholderText(self.value())
464 else:
465 self.widget().installEventFilter(self)
466 self.enable(True)
468 def widget(self):
469 """Return the parent text widget"""
470 return self._widget
472 def active(self):
473 """Return True when hint-mode is active"""
474 return self.value() == get_stripped(self._widget)
476 def value(self):
477 """Return the current hint text"""
478 return self._hint
480 def set_error(self, is_error):
481 """Enable/disable error mode"""
482 self._is_error = is_error
483 self.refresh()
485 def set_value(self, hint):
486 """Change the hint text"""
487 if self.modern:
488 self._hint = hint
489 self._widget.setPlaceholderText(hint)
490 else:
491 # If hint-mode is currently active, re-activate it
492 active = self.active()
493 self._hint = hint
494 if active or self.active():
495 self.enable(True)
497 def enable(self, enable):
498 """Enable/disable hint-mode"""
499 if not self.modern:
500 if enable and self._hint:
501 self._widget.set_value(self._hint, block=True)
502 self._widget.cursor_position.reset()
503 else:
504 self._widget.clear()
505 self._update_palette(enable)
507 def refresh(self):
508 """Update the palette to match the current mode"""
509 self._update_palette(self.active())
511 def _update_palette(self, hint):
512 """Update to palette for normal/error/hint mode"""
513 if self._is_error:
514 style = self._error_style
515 elif not self.modern and hint:
516 style = self._hint_style
517 else:
518 style = self._default_style
519 self._widget.setStyleSheet(style)
521 def eventFilter(self, _obj, event):
522 """Enable/disable hint-mode when focus changes"""
523 etype = event.type()
524 if etype == QtCore.QEvent.FocusIn:
525 self.focus_in()
526 elif etype == QtCore.QEvent.FocusOut:
527 self.focus_out()
528 return False
530 def focus_in(self):
531 """Disable hint-mode when focused"""
532 widget = self.widget()
533 if self.active():
534 self.enable(False)
535 widget.cursor_position.emit()
537 def focus_out(self):
538 """Re-enable hint-mode when losing focus"""
539 widget = self.widget()
540 if not get(widget):
541 self.enable(True)
544 class HintedPlainTextEdit(PlainTextEdit):
545 """A hinted plain text edit"""
547 def __init__(self, context, hint, parent=None, readonly=False):
548 PlainTextEdit.__init__(self, parent=parent,
549 get_value=get_value_hinted,
550 readonly=readonly)
551 self.hint = HintWidget(self, hint)
552 self.hint.init()
553 self.setFont(qtutils.diff_font(context))
554 self.set_tabwidth(prefs.tabwidth(context))
555 # Refresh palettes when text changes
556 self.textChanged.connect(self.hint.refresh)
558 def set_value(self, value, block=False):
559 """Set the widget text or enable hint mode when empty"""
560 if value or self.hint.modern:
561 PlainTextEdit.set_value(self, value, block=block)
562 else:
563 self.hint.enable(True)
566 class HintedTextEdit(TextEdit):
567 """A hinted text edit"""
569 def __init__(self, context, hint, parent=None, readonly=False):
570 TextEdit.__init__(self, parent=parent,
571 get_value=get_value_hinted, readonly=readonly)
572 self.hint = HintWidget(self, hint)
573 self.hint.init()
574 # Refresh palettes when text changes
575 self.textChanged.connect(self.hint.refresh)
576 self.setFont(qtutils.diff_font(context))
578 def set_value(self, value, block=False):
579 """Set the widget text or enable hint mode when empty"""
580 if value or self.hint.modern:
581 TextEdit.set_value(self, value, block=block)
582 else:
583 self.hint.enable(True)
586 # The vim-like read-only text view
588 class VimMixin(object):
590 def __init__(self, widget):
591 self.widget = widget
592 self.Base = widget.Base
593 # Common vim/unix-ish keyboard actions
594 self.add_navigation('Up', hotkeys.MOVE_UP,
595 shift=hotkeys.MOVE_UP_SHIFT)
596 self.add_navigation('Down', hotkeys.MOVE_DOWN,
597 shift=hotkeys.MOVE_DOWN_SHIFT)
598 self.add_navigation('Left', hotkeys.MOVE_LEFT,
599 shift=hotkeys.MOVE_LEFT_SHIFT)
600 self.add_navigation('Right', hotkeys.MOVE_RIGHT,
601 shift=hotkeys.MOVE_RIGHT_SHIFT)
602 self.add_navigation('WordLeft', hotkeys.WORD_LEFT)
603 self.add_navigation('WordRight', hotkeys.WORD_RIGHT)
604 self.add_navigation('StartOfLine', hotkeys.START_OF_LINE)
605 self.add_navigation('EndOfLine', hotkeys.END_OF_LINE)
607 qtutils.add_action(widget, 'PageUp',
608 lambda: widget.page(-widget.height()//2),
609 hotkeys.SECONDARY_ACTION)
611 qtutils.add_action(widget, 'PageDown',
612 lambda: widget.page(widget.height()//2),
613 hotkeys.PRIMARY_ACTION)
615 def add_navigation(self, name, hotkey, shift=None):
616 """Add a hotkey along with a shift-variant"""
617 widget = self.widget
618 direction = getattr(QtGui.QTextCursor, name)
619 qtutils.add_action(widget, name,
620 lambda: self.move(direction), hotkey)
621 if shift:
622 qtutils.add_action(widget, 'Shift' + name,
623 lambda: self.move(direction, True), shift)
625 def move(self, direction, select=False, n=1):
626 widget = self.widget
627 cursor = widget.textCursor()
628 if select:
629 mode = QtGui.QTextCursor.KeepAnchor
630 else:
631 mode = QtGui.QTextCursor.MoveAnchor
632 if cursor.movePosition(direction, mode, n):
633 self.set_text_cursor(cursor)
635 def page(self, offset):
636 widget = self.widget
637 rect = widget.cursorRect()
638 x = rect.x()
639 y = rect.y() + offset
640 new_cursor = widget.cursorForPosition(QtCore.QPoint(x, y))
641 if new_cursor is not None:
642 self.set_text_cursor(new_cursor)
644 def set_text_cursor(self, cursor):
645 widget = self.widget
646 widget.setTextCursor(cursor)
647 widget.ensureCursorVisible()
648 widget.viewport().update()
650 def keyPressEvent(self, event):
651 """Custom keyboard behaviors
653 The leave() signal is emitted when `Up` is pressed and we're already
654 at the beginning of the text. This allows the parent widget to
655 orchestrate some higher-level interaction, such as giving focus to
656 another widget.
658 When in the middle of the first line and `Up` is pressed, the cursor
659 is moved to the beginning of the line.
662 widget = self.widget
663 if event.key() == Qt.Key_Up:
664 cursor = widget.textCursor()
665 position = cursor.position()
666 if position == 0:
667 # The cursor is at the beginning of the line.
668 # Emit a signal so that the parent can e.g. change focus.
669 widget.leave.emit()
670 elif get(widget)[:position].count('\n') == 0:
671 # The cursor is in the middle of the first line of text.
672 # We can't go up ~ jump to the beginning of the line.
673 # Select the text if shift is pressed.
674 if event.modifiers() & Qt.ShiftModifier:
675 mode = QtGui.QTextCursor.KeepAnchor
676 else:
677 mode = QtGui.QTextCursor.MoveAnchor
678 cursor.movePosition(QtGui.QTextCursor.StartOfLine, mode)
679 widget.setTextCursor(cursor)
681 return self.Base.keyPressEvent(widget, event)
684 class VimHintedPlainTextEdit(HintedPlainTextEdit):
685 """HintedPlainTextEdit with vim hotkeys
687 This can only be used in read-only mode.
690 Base = HintedPlainTextEdit
691 Mixin = VimMixin
693 def __init__(self, context, hint, parent=None):
694 HintedPlainTextEdit.__init__(
695 self, context, hint, parent=parent, readonly=True)
696 self._mixin = self.Mixin(self)
698 def move(self, direction, select=False, n=1):
699 return self._mixin.page(direction, select=select, n=n)
701 def page(self, offset):
702 return self._mixin.page(offset)
704 def keyPressEvent(self, event):
705 return self._mixin.keyPressEvent(event)
708 class VimTextEdit(MonoTextEdit):
709 """Text viewer with vim-like hotkeys
711 This can only be used in read-only mode.
714 Base = MonoTextEdit
715 Mixin = VimMixin
717 def __init__(self, context, parent=None, readonly=True):
718 MonoTextEdit.__init__(self, context, parent=None, readonly=readonly)
719 self._mixin = self.Mixin(self)
721 def move(self, direction, select=False, n=1):
722 return self._mixin.page(direction, select=select, n=n)
724 def page(self, offset):
725 return self._mixin.page(offset)
727 def keyPressEvent(self, event):
728 return self._mixin.keyPressEvent(event)
731 class HintedLineEdit(LineEdit):
733 def __init__(self, context, hint, parent=None):
734 LineEdit.__init__(self, parent=parent, get_value=get_value_hinted)
735 self.hint = HintWidget(self, hint)
736 self.hint.init()
737 self.setFont(qtutils.diff_font(context))
738 self.textChanged.connect(lambda text: self.hint.refresh())
741 def text_dialog(context, text, title):
742 """Show a wall of text in a dialog"""
743 parent = qtutils.active_window()
745 label = QtWidgets.QLabel(parent)
746 label.setFont(qtutils.diff_font(context))
747 label.setText(text)
748 label.setMargin(defs.large_margin)
749 text_flags = Qt.TextSelectableByKeyboard | Qt.TextSelectableByMouse
750 label.setTextInteractionFlags(text_flags)
752 widget = QtWidgets.QDialog(parent)
753 widget.setWindowModality(Qt.WindowModal)
754 widget.setWindowTitle(title)
756 scroll = QtWidgets.QScrollArea()
757 scroll.setWidget(label)
759 layout = qtutils.hbox(defs.margin, defs.spacing, scroll)
760 widget.setLayout(layout)
762 qtutils.add_action(widget, N_('Close'), widget.accept,
763 Qt.Key_Question, Qt.Key_Enter, Qt.Key_Return)
764 widget.show()
765 return widget
768 class VimTextBrowser(VimTextEdit):
769 """Text viewer with line number annotations"""
771 def __init__(self, context, parent=None, readonly=True):
772 VimTextEdit.__init__(self, context, parent=parent, readonly=readonly)
773 self.numbers = LineNumbers(self)
775 def resizeEvent(self, event):
776 super(VimTextBrowser, self).resizeEvent(event)
777 self.numbers.refresh_size()
780 class TextDecorator(QtWidgets.QWidget):
781 """Common functionality for providing line numbers in text widgets"""
783 def __init__(self, parent):
784 QtWidgets.QWidget.__init__(self, parent)
785 self.editor = parent
787 parent.blockCountChanged.connect(lambda x: self._refresh_viewport())
788 parent.cursorPositionChanged.connect(self.refresh)
789 parent.updateRequest.connect(self._refresh_rect)
791 def refresh(self):
792 """Refresh the numbers display"""
793 rect = self.editor.viewport().rect()
794 self._refresh_rect(rect, 0)
796 def _refresh_rect(self, rect, dy):
797 if dy:
798 self.scroll(0, dy)
799 else:
800 self.update(0, rect.y(), self.width(), rect.height())
802 if rect.contains(self.editor.viewport().rect()):
803 self._refresh_viewport()
805 def _refresh_viewport(self):
806 self.editor.setViewportMargins(self.width_hint(), 0, 0, 0)
808 def refresh_size(self):
809 rect = self.editor.contentsRect()
810 geom = QtCore.QRect(rect.left(), rect.top(),
811 self.width_hint(), rect.height())
812 self.setGeometry(geom)
814 def sizeHint(self):
815 return QtCore.QSize(self.width_hint(), 0)
818 class LineNumbers(TextDecorator):
819 """Provide line numbers for QPlainTextEdit widgets"""
821 def __init__(self, parent):
822 TextDecorator.__init__(self, parent)
823 self.highlight_line = -1
825 def width_hint(self):
826 document = self.editor.document()
827 digits = int(math.log(max(1, document.blockCount()), 10)) + 2
828 return defs.large_margin + self.fontMetrics().width('0') * digits
830 def set_highlighted(self, line_number):
831 """Set the line to highlight"""
832 self.highlight_line = line_number
834 def paintEvent(self, event):
835 """Paint the line number"""
836 QPalette = QtGui.QPalette
837 painter = QtGui.QPainter(self)
838 editor = self.editor
839 palette = editor.palette()
841 painter.fillRect(event.rect(), palette.color(QPalette.Base))
843 content_offset = editor.contentOffset()
844 block = editor.firstVisibleBlock()
845 width = self.width()
846 event_rect_bottom = event.rect().bottom()
848 highlight = palette.color(QPalette.Highlight)
849 highlighted_text = palette.color(QPalette.HighlightedText)
850 disabled = palette.color(QPalette.Disabled, QPalette.Text)
852 while block.isValid():
853 block_geom = editor.blockBoundingGeometry(block)
854 block_top = block_geom.translated(content_offset).top()
855 if not block.isVisible() or block_top >= event_rect_bottom:
856 break
858 rect = block_geom.translated(content_offset).toRect()
859 block_number = block.blockNumber()
860 if block_number == self.highlight_line:
861 painter.fillRect(rect.x(), rect.y(),
862 width, rect.height(), highlight)
863 painter.setPen(highlighted_text)
864 else:
865 painter.setPen(disabled)
867 number = '%s' % (block_number + 1)
868 painter.drawText(rect.x(), rect.y(),
869 self.width() - defs.large_margin,
870 rect.height(),
871 Qt.AlignRight | Qt.AlignVCenter,
872 number)
873 block = block.next() # pylint: disable=next-method-called