text: defer calls to setStyleSheet()
[git-cola.git] / cola / widgets / text.py
blob7317fef0a27c53b6e791fcaf38f2fddd9fdd290b
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 # pylint: disable=no-self-use
205 def init(self):
206 """Called during init for class-specific settings"""
207 return
209 # pylint: disable=no-self-use,unused-argument
210 def set_textwidth(self, width):
211 """Set the text width"""
212 return
214 # pylint: disable=no-self-use,unused-argument
215 def set_linebreak(self, brk):
216 """Enable word wrapping"""
217 return
220 class PlainTextEditExtension(BaseTextEditExtension):
222 def set_linebreak(self, brk):
223 if brk:
224 wrapmode = QtWidgets.QPlainTextEdit.WidgetWidth
225 else:
226 wrapmode = QtWidgets.QPlainTextEdit.NoWrap
227 self.widget.setLineWrapMode(wrapmode)
230 class PlainTextEdit(QtWidgets.QPlainTextEdit):
232 cursor_changed = Signal(int, int)
233 leave = Signal()
235 def __init__(self, parent=None, get_value=None, readonly=False):
236 QtWidgets.QPlainTextEdit.__init__(self, parent)
237 self.ext = PlainTextEditExtension(self, get_value, readonly)
238 self.cursor_position = self.ext.cursor_position
240 def get(self):
241 """Return the raw unicode value from Qt"""
242 return self.ext.get()
244 # For compatibility with QTextEdit
245 def setText(self, value):
246 self.set_value(value)
248 def value(self):
249 """Return a safe value, e.g. a stripped value"""
250 return self.ext.value()
252 def set_value(self, value, block=False):
253 self.ext.set_value(value, block=block)
255 def has_selection(self):
256 return self.ext.has_selection()
258 def selected_line(self):
259 return self.ext.selected_line()
261 def set_tabwidth(self, width):
262 self.ext.set_tabwidth(width)
264 def set_textwidth(self, width):
265 self.ext.set_textwidth(width)
267 def set_linebreak(self, brk):
268 self.ext.set_linebreak(brk)
270 def mousePressEvent(self, event):
271 self.ext.mouse_press_event(event)
272 super(PlainTextEdit, self).mousePressEvent(event)
274 def wheelEvent(self, event):
275 """Disable control+wheelscroll text resizing"""
276 if event.modifiers() & Qt.ControlModifier:
277 event.ignore()
278 return
279 super(PlainTextEdit, self).wheelEvent(event)
282 class TextEditExtension(BaseTextEditExtension):
284 def init(self):
285 widget = self.widget
286 widget.setAcceptRichText(False)
288 def set_linebreak(self, brk):
289 if brk:
290 wrapmode = QtWidgets.QTextEdit.FixedColumnWidth
291 else:
292 wrapmode = QtWidgets.QTextEdit.NoWrap
293 self.widget.setLineWrapMode(wrapmode)
295 def set_textwidth(self, width):
296 self.widget.setLineWrapColumnOrWidth(width)
299 class TextEdit(QtWidgets.QTextEdit):
301 cursor_changed = Signal(int, int)
302 leave = Signal()
304 def __init__(self, parent=None, get_value=None, readonly=False):
305 QtWidgets.QTextEdit.__init__(self, parent)
306 self.ext = TextEditExtension(self, get_value, readonly)
307 self.cursor_position = self.ext.cursor_position
308 self.expandtab_enabled = False
310 def get(self):
311 """Return the raw unicode value from Qt"""
312 return self.ext.get()
314 def value(self):
315 """Return a safe value, e.g. a stripped value"""
316 return self.ext.value()
318 def set_value(self, value, block=False):
319 self.ext.set_value(value, block=block)
321 def selected_line(self):
322 return self.ext.selected_line()
324 def set_tabwidth(self, width):
325 self.ext.set_tabwidth(width)
327 def set_textwidth(self, width):
328 self.ext.set_textwidth(width)
330 def set_linebreak(self, brk):
331 self.ext.set_linebreak(brk)
333 def set_expandtab(self, value):
334 self.expandtab_enabled = value
336 def mousePressEvent(self, event):
337 self.ext.mouse_press_event(event)
338 super(TextEdit, self).mousePressEvent(event)
340 def wheelEvent(self, event):
341 """Disable control+wheelscroll text resizing"""
342 if event.modifiers() & Qt.ControlModifier:
343 event.ignore()
344 return
345 super(TextEdit, self).wheelEvent(event)
347 def should_expandtab(self, event):
348 return event.key() == Qt.Key_Tab and self.expandtab_enabled
350 def expandtab(self):
351 tabwidth = max(self.ext.tabwidth(), 1)
352 cursor = self.textCursor()
353 cursor.insertText(' ' * tabwidth)
355 def keyPressEvent(self, event):
356 expandtab = self.should_expandtab(event)
357 if expandtab:
358 self.expandtab()
359 event.accept()
360 else:
361 QtWidgets.QTextEdit.keyPressEvent(self, event)
363 def keyReleaseEvent(self, event):
364 expandtab = self.should_expandtab(event)
365 if expandtab:
366 event.ignore()
367 else:
368 QtWidgets.QTextEdit.keyReleaseEvent(self, event)
371 class TextEditCursorPosition(object):
373 def __init__(self, widget, ext):
374 self._widget = widget
375 self._ext = ext
376 widget.cursorPositionChanged.connect(self.emit)
378 def emit(self):
379 widget = self._widget
380 ext = self._ext
381 cursor = widget.textCursor()
382 position = cursor.position()
383 txt = widget.get()
384 before = txt[:position]
385 row = before.count('\n')
386 line = before.split('\n')[row]
387 col = cursor.columnNumber()
388 col += line[:col].count('\t') * (ext.tabwidth() - 1)
389 widget.cursor_changed.emit(row+1, col)
391 def reset(self):
392 widget = self._widget
393 cursor = widget.textCursor()
394 cursor.setPosition(0)
395 widget.setTextCursor(cursor)
398 class MonoTextEdit(PlainTextEdit):
400 def __init__(self, context, parent=None, readonly=False):
401 PlainTextEdit.__init__(self, parent=parent, readonly=readonly)
402 self.setFont(qtutils.diff_font(context))
405 def get_value_hinted(widget):
406 text = get_stripped(widget)
407 hint = get(widget.hint)
408 if text == hint:
409 return ''
410 return text
413 class HintWidget(QtCore.QObject):
414 """Extend a widget to provide hint messages
416 This primarily exists because setPlaceholderText() is only available
417 in Qt5, so this class provides consistent behavior across versions.
421 def __init__(self, widget, hint):
422 QtCore.QObject.__init__(self, widget)
423 self._widget = widget
424 self._hint = hint
425 self._is_error = False
427 self.modern = modern = hasattr(widget, 'setPlaceholderText')
428 if modern:
429 widget.setPlaceholderText(hint)
431 # Palette for normal text
432 QPalette = QtGui.QPalette
433 palette = widget.palette()
435 hint_color = palette.color(QPalette.Disabled, QPalette.Text)
436 error_bg_color = QtGui.QColor(Qt.red).darker()
437 error_fg_color = QtGui.QColor(Qt.white)
439 hint_rgb = qtutils.rgb_css(hint_color)
440 error_bg_rgb = qtutils.rgb_css(error_bg_color)
441 error_fg_rgb = qtutils.rgb_css(error_fg_color)
443 env = dict(name=widget.__class__.__name__,
444 error_fg_rgb=error_fg_rgb,
445 error_bg_rgb=error_bg_rgb,
446 hint_rgb=hint_rgb)
448 self._default_style = ''
450 self._hint_style = """
451 %(name)s {
452 color: %(hint_rgb)s;
454 """ % env
456 self._error_style = """
457 %(name)s {
458 color: %(error_fg_rgb)s;
459 background-color: %(error_bg_rgb)s;
461 """ % env
463 def init(self):
464 """Defered initialization"""
465 if self.modern:
466 self.widget().setPlaceholderText(self.value())
467 else:
468 self.widget().installEventFilter(self)
469 self.enable(True)
471 def widget(self):
472 """Return the parent text widget"""
473 return self._widget
475 def active(self):
476 """Return True when hint-mode is active"""
477 return self.value() == get_stripped(self._widget)
479 def value(self):
480 """Return the current hint text"""
481 return self._hint
483 def set_error(self, is_error):
484 """Enable/disable error mode"""
485 self._is_error = is_error
486 self.refresh()
488 def set_value(self, hint):
489 """Change the hint text"""
490 if self.modern:
491 self._hint = hint
492 self._widget.setPlaceholderText(hint)
493 else:
494 # If hint-mode is currently active, re-activate it
495 active = self.active()
496 self._hint = hint
497 if active or self.active():
498 self.enable(True)
500 def enable(self, enable):
501 """Enable/disable hint-mode"""
502 if not self.modern:
503 if enable and self._hint:
504 self._widget.set_value(self._hint, block=True)
505 self._widget.cursor_position.reset()
506 else:
507 self._widget.clear()
508 self._update_palette(enable)
510 def refresh(self):
511 """Update the palette to match the current mode"""
512 self._update_palette(self.active())
514 def _update_palette(self, hint):
515 """Update to palette for normal/error/hint mode"""
516 if self._is_error:
517 style = self._error_style
518 elif not self.modern and hint:
519 style = self._hint_style
520 else:
521 style = self._default_style
522 QtCore.QTimer.singleShot(
523 0, lambda: self._widget.setStyleSheet(style))
525 def eventFilter(self, _obj, event):
526 """Enable/disable hint-mode when focus changes"""
527 etype = event.type()
528 if etype == QtCore.QEvent.FocusIn:
529 self.focus_in()
530 elif etype == QtCore.QEvent.FocusOut:
531 self.focus_out()
532 return False
534 def focus_in(self):
535 """Disable hint-mode when focused"""
536 widget = self.widget()
537 if self.active():
538 self.enable(False)
539 widget.cursor_position.emit()
541 def focus_out(self):
542 """Re-enable hint-mode when losing focus"""
543 widget = self.widget()
544 if not get(widget):
545 self.enable(True)
548 class HintedPlainTextEdit(PlainTextEdit):
549 """A hinted plain text edit"""
551 def __init__(self, context, hint, parent=None, readonly=False):
552 PlainTextEdit.__init__(self, parent=parent,
553 get_value=get_value_hinted,
554 readonly=readonly)
555 self.hint = HintWidget(self, hint)
556 self.hint.init()
557 self.setFont(qtutils.diff_font(context))
558 self.set_tabwidth(prefs.tabwidth(context))
559 # Refresh palettes when text changes
560 self.textChanged.connect(self.hint.refresh)
562 def set_value(self, value, block=False):
563 """Set the widget text or enable hint mode when empty"""
564 if value or self.hint.modern:
565 PlainTextEdit.set_value(self, value, block=block)
566 else:
567 self.hint.enable(True)
570 class HintedTextEdit(TextEdit):
571 """A hinted text edit"""
573 def __init__(self, context, hint, parent=None, readonly=False):
574 TextEdit.__init__(self, parent=parent,
575 get_value=get_value_hinted, readonly=readonly)
576 self.hint = HintWidget(self, hint)
577 self.hint.init()
578 # Refresh palettes when text changes
579 self.textChanged.connect(self.hint.refresh)
580 self.setFont(qtutils.diff_font(context))
582 def set_value(self, value, block=False):
583 """Set the widget text or enable hint mode when empty"""
584 if value or self.hint.modern:
585 TextEdit.set_value(self, value, block=block)
586 else:
587 self.hint.enable(True)
590 # The vim-like read-only text view
592 class VimMixin(object):
594 def __init__(self, widget):
595 self.widget = widget
596 self.Base = widget.Base
597 # Common vim/unix-ish keyboard actions
598 self.add_navigation('Up', hotkeys.MOVE_UP,
599 shift=hotkeys.MOVE_UP_SHIFT)
600 self.add_navigation('Down', hotkeys.MOVE_DOWN,
601 shift=hotkeys.MOVE_DOWN_SHIFT)
602 self.add_navigation('Left', hotkeys.MOVE_LEFT,
603 shift=hotkeys.MOVE_LEFT_SHIFT)
604 self.add_navigation('Right', hotkeys.MOVE_RIGHT,
605 shift=hotkeys.MOVE_RIGHT_SHIFT)
606 self.add_navigation('WordLeft', hotkeys.WORD_LEFT)
607 self.add_navigation('WordRight', hotkeys.WORD_RIGHT)
608 self.add_navigation('StartOfLine', hotkeys.START_OF_LINE)
609 self.add_navigation('EndOfLine', hotkeys.END_OF_LINE)
611 qtutils.add_action(widget, 'PageUp',
612 lambda: widget.page(-widget.height()//2),
613 hotkeys.SECONDARY_ACTION)
615 qtutils.add_action(widget, 'PageDown',
616 lambda: widget.page(widget.height()//2),
617 hotkeys.PRIMARY_ACTION)
619 def add_navigation(self, name, hotkey, shift=None):
620 """Add a hotkey along with a shift-variant"""
621 widget = self.widget
622 direction = getattr(QtGui.QTextCursor, name)
623 qtutils.add_action(widget, name,
624 lambda: self.move(direction), hotkey)
625 if shift:
626 qtutils.add_action(widget, 'Shift' + name,
627 lambda: self.move(direction, True), shift)
629 def move(self, direction, select=False, n=1):
630 widget = self.widget
631 cursor = widget.textCursor()
632 if select:
633 mode = QtGui.QTextCursor.KeepAnchor
634 else:
635 mode = QtGui.QTextCursor.MoveAnchor
636 if cursor.movePosition(direction, mode, n):
637 self.set_text_cursor(cursor)
639 def page(self, offset):
640 widget = self.widget
641 rect = widget.cursorRect()
642 x = rect.x()
643 y = rect.y() + offset
644 new_cursor = widget.cursorForPosition(QtCore.QPoint(x, y))
645 if new_cursor is not None:
646 self.set_text_cursor(new_cursor)
648 def set_text_cursor(self, cursor):
649 widget = self.widget
650 widget.setTextCursor(cursor)
651 widget.ensureCursorVisible()
652 widget.viewport().update()
654 def keyPressEvent(self, event):
655 """Custom keyboard behaviors
657 The leave() signal is emitted when `Up` is pressed and we're already
658 at the beginning of the text. This allows the parent widget to
659 orchestrate some higher-level interaction, such as giving focus to
660 another widget.
662 When in the middle of the first line and `Up` is pressed, the cursor
663 is moved to the beginning of the line.
666 widget = self.widget
667 if event.key() == Qt.Key_Up:
668 cursor = widget.textCursor()
669 position = cursor.position()
670 if position == 0:
671 # The cursor is at the beginning of the line.
672 # Emit a signal so that the parent can e.g. change focus.
673 widget.leave.emit()
674 elif get(widget)[:position].count('\n') == 0:
675 # The cursor is in the middle of the first line of text.
676 # We can't go up ~ jump to the beginning of the line.
677 # Select the text if shift is pressed.
678 if event.modifiers() & Qt.ShiftModifier:
679 mode = QtGui.QTextCursor.KeepAnchor
680 else:
681 mode = QtGui.QTextCursor.MoveAnchor
682 cursor.movePosition(QtGui.QTextCursor.StartOfLine, mode)
683 widget.setTextCursor(cursor)
685 return self.Base.keyPressEvent(widget, event)
688 class VimHintedPlainTextEdit(HintedPlainTextEdit):
689 """HintedPlainTextEdit with vim hotkeys
691 This can only be used in read-only mode.
694 Base = HintedPlainTextEdit
695 Mixin = VimMixin
697 def __init__(self, context, hint, parent=None):
698 HintedPlainTextEdit.__init__(
699 self, context, hint, parent=parent, readonly=True)
700 self._mixin = self.Mixin(self)
702 def move(self, direction, select=False, n=1):
703 return self._mixin.page(direction, select=select, n=n)
705 def page(self, offset):
706 return self._mixin.page(offset)
708 def keyPressEvent(self, event):
709 return self._mixin.keyPressEvent(event)
712 class VimTextEdit(MonoTextEdit):
713 """Text viewer with vim-like hotkeys
715 This can only be used in read-only mode.
718 Base = MonoTextEdit
719 Mixin = VimMixin
721 def __init__(self, context, parent=None, readonly=True):
722 MonoTextEdit.__init__(self, context, parent=None, readonly=readonly)
723 self._mixin = self.Mixin(self)
725 def move(self, direction, select=False, n=1):
726 return self._mixin.page(direction, select=select, n=n)
728 def page(self, offset):
729 return self._mixin.page(offset)
731 def keyPressEvent(self, event):
732 return self._mixin.keyPressEvent(event)
735 class HintedLineEdit(LineEdit):
737 def __init__(self, context, hint, parent=None):
738 LineEdit.__init__(self, parent=parent, get_value=get_value_hinted)
739 self.hint = HintWidget(self, hint)
740 self.hint.init()
741 self.setFont(qtutils.diff_font(context))
742 self.textChanged.connect(lambda text: self.hint.refresh())
745 def text_dialog(context, text, title):
746 """Show a wall of text in a dialog"""
747 parent = qtutils.active_window()
749 label = QtWidgets.QLabel(parent)
750 label.setFont(qtutils.diff_font(context))
751 label.setText(text)
752 label.setMargin(defs.large_margin)
753 text_flags = Qt.TextSelectableByKeyboard | Qt.TextSelectableByMouse
754 label.setTextInteractionFlags(text_flags)
756 widget = QtWidgets.QDialog(parent)
757 widget.setWindowModality(Qt.WindowModal)
758 widget.setWindowTitle(title)
760 scroll = QtWidgets.QScrollArea()
761 scroll.setWidget(label)
763 layout = qtutils.hbox(defs.margin, defs.spacing, scroll)
764 widget.setLayout(layout)
766 qtutils.add_action(widget, N_('Close'), widget.accept,
767 Qt.Key_Question, Qt.Key_Enter, Qt.Key_Return)
768 widget.show()
769 return widget
772 class VimTextBrowser(VimTextEdit):
773 """Text viewer with line number annotations"""
775 def __init__(self, context, parent=None, readonly=True):
776 VimTextEdit.__init__(self, context, parent=parent, readonly=readonly)
777 self.numbers = LineNumbers(self)
779 def resizeEvent(self, event):
780 super(VimTextBrowser, self).resizeEvent(event)
781 self.numbers.refresh_size()
784 class TextDecorator(QtWidgets.QWidget):
785 """Common functionality for providing line numbers in text widgets"""
787 def __init__(self, parent):
788 QtWidgets.QWidget.__init__(self, parent)
789 self.editor = parent
791 parent.blockCountChanged.connect(lambda x: self._refresh_viewport())
792 parent.cursorPositionChanged.connect(self.refresh)
793 parent.updateRequest.connect(self._refresh_rect)
795 def refresh(self):
796 """Refresh the numbers display"""
797 rect = self.editor.viewport().rect()
798 self._refresh_rect(rect, 0)
800 def _refresh_rect(self, rect, dy):
801 if dy:
802 self.scroll(0, dy)
803 else:
804 self.update(0, rect.y(), self.width(), rect.height())
806 if rect.contains(self.editor.viewport().rect()):
807 self._refresh_viewport()
809 def _refresh_viewport(self):
810 self.editor.setViewportMargins(self.width_hint(), 0, 0, 0)
812 def refresh_size(self):
813 rect = self.editor.contentsRect()
814 geom = QtCore.QRect(rect.left(), rect.top(),
815 self.width_hint(), rect.height())
816 self.setGeometry(geom)
818 def sizeHint(self):
819 return QtCore.QSize(self.width_hint(), 0)
822 class LineNumbers(TextDecorator):
823 """Provide line numbers for QPlainTextEdit widgets"""
825 def __init__(self, parent):
826 TextDecorator.__init__(self, parent)
827 self.highlight_line = -1
829 def width_hint(self):
830 document = self.editor.document()
831 digits = int(math.log(max(1, document.blockCount()), 10)) + 2
832 return defs.large_margin + self.fontMetrics().width('0') * digits
834 def set_highlighted(self, line_number):
835 """Set the line to highlight"""
836 self.highlight_line = line_number
838 def paintEvent(self, event):
839 """Paint the line number"""
840 QPalette = QtGui.QPalette
841 painter = QtGui.QPainter(self)
842 editor = self.editor
843 palette = editor.palette()
845 painter.fillRect(event.rect(), palette.color(QPalette.Base))
847 content_offset = editor.contentOffset()
848 block = editor.firstVisibleBlock()
849 width = self.width()
850 event_rect_bottom = event.rect().bottom()
852 highlight = palette.color(QPalette.Highlight)
853 highlighted_text = palette.color(QPalette.HighlightedText)
854 disabled = palette.color(QPalette.Disabled, QPalette.Text)
856 while block.isValid():
857 block_geom = editor.blockBoundingGeometry(block)
858 block_top = block_geom.translated(content_offset).top()
859 if not block.isVisible() or block_top >= event_rect_bottom:
860 break
862 rect = block_geom.translated(content_offset).toRect()
863 block_number = block.blockNumber()
864 if block_number == self.highlight_line:
865 painter.fillRect(rect.x(), rect.y(),
866 width, rect.height(), highlight)
867 painter.setPen(highlighted_text)
868 else:
869 painter.setPen(disabled)
871 number = '%s' % (block_number + 1)
872 painter.drawText(rect.x(), rect.y(),
873 self.width() - defs.large_margin,
874 rect.height(),
875 Qt.AlignRight | Qt.AlignVCenter,
876 number)
877 block = block.next() # pylint: disable=next-method-called