maint: format code using black
[git-cola.git] / cola / widgets / text.py
blobb5f574acb9cc06c9adcc46c1cf98b5c565e4af8e
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)"""
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):
77 def __init__(self, widget, get_value, readonly):
78 QtCore.QObject.__init__(self, widget)
79 self.widget = widget
80 self.cursor_position = TextEditCursorPosition(widget, self)
81 if get_value is None:
82 get_value = get_stripped
83 self._get_value = get_value
84 self._tabwidth = 8
85 self._readonly = readonly
86 self._init_flags()
87 self.init()
89 def _init_flags(self):
90 widget = self.widget
91 widget.setMinimumSize(QtCore.QSize(1, 1))
92 widget.setWordWrapMode(QtGui.QTextOption.WordWrap)
93 widget.setLineWrapMode(widget.NoWrap)
94 if self._readonly:
95 widget.setReadOnly(True)
96 widget.setAcceptDrops(False)
97 widget.setTabChangesFocus(True)
98 widget.setUndoRedoEnabled(False)
99 widget.setTextInteractionFlags(
100 Qt.TextSelectableByKeyboard | 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), QtGui.QTextCursor.KeepAnchor)
129 self.widget.setTextCursor(cursor)
131 elif value == old_value:
132 # Otherwise, if the text is identical and there is no selection
133 # then restore the cursor position.
134 cursor = self.widget.textCursor()
135 cursor.setPosition(offset)
136 self.widget.setTextCursor(cursor)
137 else:
138 # If none of the above applied then restore the cursor position.
139 position = max(0, min(offset, len(value) - 1))
140 cursor = self.widget.textCursor()
141 cursor.setPosition(position)
142 self.widget.setTextCursor(cursor)
143 cursor = self.widget.textCursor()
144 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
145 self.widget.setTextCursor(cursor)
147 if block:
148 self.widget.blockSignals(blocksig)
150 def set_cursor_position(self, new_position):
151 cursor = self.widget.textCursor()
152 cursor.setPosition(new_position)
153 self.widget.setTextCursor(cursor)
155 def tabwidth(self):
156 return self._tabwidth
158 def set_tabwidth(self, width):
159 self._tabwidth = width
160 font = self.widget.font()
161 fm = QtGui.QFontMetrics(font)
162 pixels = fm.width('M' * width)
163 self.widget.setTabStopWidth(pixels)
165 def selected_line(self):
166 contents = self.value()
167 cursor = self.widget.textCursor()
168 offset = min(cursor.position(), len(contents) - 1)
169 while offset >= 1 and contents[offset - 1] and contents[offset - 1] != '\n':
170 offset -= 1
171 data = contents[offset:]
172 if '\n' in data:
173 line, _ = data.split('\n', 1)
174 else:
175 line = data
176 return line
178 def cursor(self):
179 return self.widget.textCursor()
181 def has_selection(self):
182 return self.cursor().hasSelection()
184 def offset_and_selection(self):
185 cursor = self.cursor()
186 offset = cursor.selectionStart()
187 selection_text = cursor.selection().toPlainText()
188 return offset, selection_text
190 def mouse_press_event(self, event):
191 # Move the text cursor so that the right-click events operate
192 # on the current position, not the last left-clicked position.
193 widget = self.widget
194 if event.button() == Qt.RightButton:
195 if not widget.textCursor().hasSelection():
196 cursor = widget.cursorForPosition(event.pos())
197 widget.setTextCursor(cursor)
199 # For extension by sub-classes
201 # pylint: disable=no-self-use
202 def init(self):
203 """Called during init for class-specific settings"""
204 return
206 # pylint: disable=no-self-use,unused-argument
207 def set_textwidth(self, width):
208 """Set the text width"""
209 return
211 # pylint: disable=no-self-use,unused-argument
212 def set_linebreak(self, brk):
213 """Enable word wrapping"""
214 return
217 class PlainTextEditExtension(BaseTextEditExtension):
218 def set_linebreak(self, brk):
219 if brk:
220 wrapmode = QtWidgets.QPlainTextEdit.WidgetWidth
221 else:
222 wrapmode = QtWidgets.QPlainTextEdit.NoWrap
223 self.widget.setLineWrapMode(wrapmode)
226 class PlainTextEdit(QtWidgets.QPlainTextEdit):
228 cursor_changed = Signal(int, int)
229 leave = Signal()
231 def __init__(self, parent=None, get_value=None, readonly=False):
232 QtWidgets.QPlainTextEdit.__init__(self, parent)
233 self.ext = PlainTextEditExtension(self, get_value, readonly)
234 self.cursor_position = self.ext.cursor_position
236 def get(self):
237 """Return the raw unicode value from Qt"""
238 return self.ext.get()
240 # For compatibility with QTextEdit
241 def setText(self, value):
242 self.set_value(value)
244 def value(self):
245 """Return a safe value, e.g. a stripped value"""
246 return self.ext.value()
248 def set_value(self, value, block=False):
249 self.ext.set_value(value, block=block)
251 def has_selection(self):
252 return self.ext.has_selection()
254 def selected_line(self):
255 return self.ext.selected_line()
257 def set_tabwidth(self, width):
258 self.ext.set_tabwidth(width)
260 def set_textwidth(self, width):
261 self.ext.set_textwidth(width)
263 def set_linebreak(self, brk):
264 self.ext.set_linebreak(brk)
266 def mousePressEvent(self, event):
267 self.ext.mouse_press_event(event)
268 super(PlainTextEdit, self).mousePressEvent(event)
270 def wheelEvent(self, event):
271 """Disable control+wheelscroll text resizing"""
272 if event.modifiers() & Qt.ControlModifier:
273 event.ignore()
274 return
275 super(PlainTextEdit, self).wheelEvent(event)
278 class TextEditExtension(BaseTextEditExtension):
279 def init(self):
280 widget = self.widget
281 widget.setAcceptRichText(False)
283 def set_linebreak(self, brk):
284 if brk:
285 wrapmode = QtWidgets.QTextEdit.FixedColumnWidth
286 else:
287 wrapmode = QtWidgets.QTextEdit.NoWrap
288 self.widget.setLineWrapMode(wrapmode)
290 def set_textwidth(self, width):
291 self.widget.setLineWrapColumnOrWidth(width)
294 class TextEdit(QtWidgets.QTextEdit):
296 cursor_changed = Signal(int, int)
297 leave = Signal()
299 def __init__(self, parent=None, get_value=None, readonly=False):
300 QtWidgets.QTextEdit.__init__(self, parent)
301 self.ext = TextEditExtension(self, get_value, readonly)
302 self.cursor_position = self.ext.cursor_position
303 self.expandtab_enabled = False
305 def get(self):
306 """Return the raw unicode value from Qt"""
307 return self.ext.get()
309 def value(self):
310 """Return a safe value, e.g. a stripped value"""
311 return self.ext.value()
313 def set_value(self, value, block=False):
314 self.ext.set_value(value, block=block)
316 def selected_line(self):
317 return self.ext.selected_line()
319 def set_tabwidth(self, width):
320 self.ext.set_tabwidth(width)
322 def set_textwidth(self, width):
323 self.ext.set_textwidth(width)
325 def set_linebreak(self, brk):
326 self.ext.set_linebreak(brk)
328 def set_expandtab(self, value):
329 self.expandtab_enabled = value
331 def mousePressEvent(self, event):
332 self.ext.mouse_press_event(event)
333 super(TextEdit, self).mousePressEvent(event)
335 def wheelEvent(self, event):
336 """Disable control+wheelscroll text resizing"""
337 if event.modifiers() & Qt.ControlModifier:
338 event.ignore()
339 return
340 super(TextEdit, self).wheelEvent(event)
342 def should_expandtab(self, event):
343 return event.key() == Qt.Key_Tab and self.expandtab_enabled
345 def expandtab(self):
346 tabwidth = max(self.ext.tabwidth(), 1)
347 cursor = self.textCursor()
348 cursor.insertText(' ' * tabwidth)
350 def keyPressEvent(self, event):
351 expandtab = self.should_expandtab(event)
352 if expandtab:
353 self.expandtab()
354 event.accept()
355 else:
356 QtWidgets.QTextEdit.keyPressEvent(self, event)
358 def keyReleaseEvent(self, event):
359 expandtab = self.should_expandtab(event)
360 if expandtab:
361 event.ignore()
362 else:
363 QtWidgets.QTextEdit.keyReleaseEvent(self, event)
366 class TextEditCursorPosition(object):
367 def __init__(self, widget, ext):
368 self._widget = widget
369 self._ext = ext
370 widget.cursorPositionChanged.connect(self.emit)
372 def emit(self):
373 widget = self._widget
374 ext = self._ext
375 cursor = widget.textCursor()
376 position = cursor.position()
377 txt = widget.get()
378 before = txt[:position]
379 row = before.count('\n')
380 line = before.split('\n')[row]
381 col = cursor.columnNumber()
382 col += line[:col].count('\t') * (ext.tabwidth() - 1)
383 widget.cursor_changed.emit(row + 1, col)
385 def reset(self):
386 widget = self._widget
387 cursor = widget.textCursor()
388 cursor.setPosition(0)
389 widget.setTextCursor(cursor)
392 class MonoTextEdit(PlainTextEdit):
393 def __init__(self, context, parent=None, readonly=False):
394 PlainTextEdit.__init__(self, parent=parent, readonly=readonly)
395 self.setFont(qtutils.diff_font(context))
398 def get_value_hinted(widget):
399 text = get_stripped(widget)
400 hint = get(widget.hint)
401 if text == hint:
402 return ''
403 return text
406 class HintWidget(QtCore.QObject):
407 """Extend a widget to provide hint messages
409 This primarily exists because setPlaceholderText() is only available
410 in Qt5, so this class provides consistent behavior across versions.
414 def __init__(self, widget, hint):
415 QtCore.QObject.__init__(self, widget)
416 self._widget = widget
417 self._hint = hint
418 self._is_error = False
420 self.modern = modern = hasattr(widget, 'setPlaceholderText')
421 if modern:
422 widget.setPlaceholderText(hint)
424 # Palette for normal text
425 QPalette = QtGui.QPalette
426 palette = widget.palette()
428 hint_color = palette.color(QPalette.Disabled, QPalette.Text)
429 error_bg_color = QtGui.QColor(Qt.red).darker()
430 error_fg_color = QtGui.QColor(Qt.white)
432 hint_rgb = qtutils.rgb_css(hint_color)
433 error_bg_rgb = qtutils.rgb_css(error_bg_color)
434 error_fg_rgb = qtutils.rgb_css(error_fg_color)
436 env = dict(
437 name=widget.__class__.__name__,
438 error_fg_rgb=error_fg_rgb,
439 error_bg_rgb=error_bg_rgb,
440 hint_rgb=hint_rgb,
443 self._default_style = ''
445 self._hint_style = (
447 %(name)s {
448 color: %(hint_rgb)s;
451 % env
454 self._error_style = (
456 %(name)s {
457 color: %(error_fg_rgb)s;
458 background-color: %(error_bg_rgb)s;
461 % env
464 def init(self):
465 """Defered initialization"""
466 if self.modern:
467 self.widget().setPlaceholderText(self.value())
468 else:
469 self.widget().installEventFilter(self)
470 self.enable(True)
472 def widget(self):
473 """Return the parent text widget"""
474 return self._widget
476 def active(self):
477 """Return True when hint-mode is active"""
478 return self.value() == get_stripped(self._widget)
480 def value(self):
481 """Return the current hint text"""
482 return self._hint
484 def set_error(self, is_error):
485 """Enable/disable error mode"""
486 self._is_error = is_error
487 self.refresh()
489 def set_value(self, hint):
490 """Change the hint text"""
491 if self.modern:
492 self._hint = hint
493 self._widget.setPlaceholderText(hint)
494 else:
495 # If hint-mode is currently active, re-activate it
496 active = self.active()
497 self._hint = hint
498 if active or self.active():
499 self.enable(True)
501 def enable(self, enable):
502 """Enable/disable hint-mode"""
503 if not self.modern:
504 if enable and self._hint:
505 self._widget.set_value(self._hint, block=True)
506 self._widget.cursor_position.reset()
507 else:
508 self._widget.clear()
509 self._update_palette(enable)
511 def refresh(self):
512 """Update the palette to match the current mode"""
513 self._update_palette(self.active())
515 def _update_palette(self, hint):
516 """Update to palette for normal/error/hint mode"""
517 if self._is_error:
518 style = self._error_style
519 elif not self.modern and hint:
520 style = self._hint_style
521 else:
522 style = self._default_style
523 QtCore.QTimer.singleShot(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__(
553 self, parent=parent, get_value=get_value_hinted, 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 # pylint: disable=no-member
561 self.textChanged.connect(self.hint.refresh)
563 def set_value(self, value, block=False):
564 """Set the widget text or enable hint mode when empty"""
565 if value or self.hint.modern:
566 PlainTextEdit.set_value(self, value, block=block)
567 else:
568 self.hint.enable(True)
571 class HintedTextEdit(TextEdit):
572 """A hinted text edit"""
574 def __init__(self, context, hint, parent=None, readonly=False):
575 TextEdit.__init__(
576 self, parent=parent, get_value=get_value_hinted, readonly=readonly
578 self.hint = HintWidget(self, hint)
579 self.hint.init()
580 # Refresh palettes when text changes
581 # pylint: disable=no-member
582 self.textChanged.connect(self.hint.refresh)
583 self.setFont(qtutils.diff_font(context))
585 def set_value(self, value, block=False):
586 """Set the widget text or enable hint mode when empty"""
587 if value or self.hint.modern:
588 TextEdit.set_value(self, value, block=block)
589 else:
590 self.hint.enable(True)
593 def anchor_mode(select):
594 """Return the QTextCursor mode to keep/discard the cursor selection"""
595 if select:
596 mode = QtGui.QTextCursor.KeepAnchor
597 else:
598 mode = QtGui.QTextCursor.MoveAnchor
599 return mode
602 # The vim-like read-only text view
605 class VimMixin(object):
606 def __init__(self, widget):
607 self.widget = widget
608 self.Base = widget.Base
609 # Common vim/unix-ish keyboard actions
610 self.add_navigation('End', hotkeys.GOTO_END)
611 self.add_navigation('Up', hotkeys.MOVE_UP, shift=hotkeys.MOVE_UP_SHIFT)
612 self.add_navigation('Down', hotkeys.MOVE_DOWN, shift=hotkeys.MOVE_DOWN_SHIFT)
613 self.add_navigation('Left', hotkeys.MOVE_LEFT, shift=hotkeys.MOVE_LEFT_SHIFT)
614 self.add_navigation('Right', hotkeys.MOVE_RIGHT, shift=hotkeys.MOVE_RIGHT_SHIFT)
615 self.add_navigation('WordLeft', hotkeys.WORD_LEFT)
616 self.add_navigation('WordRight', hotkeys.WORD_RIGHT)
617 self.add_navigation('Start', hotkeys.GOTO_START)
618 self.add_navigation('StartOfLine', hotkeys.START_OF_LINE)
619 self.add_navigation('EndOfLine', hotkeys.END_OF_LINE)
621 qtutils.add_action(
622 widget, 'PageUp', widget.page_up, hotkeys.SECONDARY_ACTION, hotkeys.UP
624 qtutils.add_action(
625 widget, 'PageDown', widget.page_down, hotkeys.PRIMARY_ACTION, hotkeys.DOWN
627 qtutils.add_action(
628 widget,
629 'SelectPageUp',
630 lambda: widget.page_up(select=True),
631 hotkeys.SELECT_BACK,
632 hotkeys.SELECT_UP,
634 qtutils.add_action(
635 widget,
636 'SelectPageDown',
637 lambda: widget.page_down(select=True),
638 hotkeys.SELECT_FORWARD,
639 hotkeys.SELECT_DOWN,
642 def add_navigation(self, name, hotkey, shift=None):
643 """Add a hotkey along with a shift-variant"""
644 widget = self.widget
645 direction = getattr(QtGui.QTextCursor, name)
646 qtutils.add_action(widget, name, lambda: self.move(direction), hotkey)
647 if shift:
648 qtutils.add_action(
649 widget, 'Shift' + name, lambda: self.move(direction, select=True), shift
652 def move(self, direction, select=False, n=1):
653 widget = self.widget
654 cursor = widget.textCursor()
655 mode = anchor_mode(select)
656 for _ in range(n):
657 if cursor.movePosition(direction, mode, 1):
658 self.set_text_cursor(cursor)
660 def page(self, offset, select=False):
661 widget = self.widget
662 rect = widget.cursorRect()
663 x = rect.x()
664 y = rect.y() + offset
665 new_cursor = widget.cursorForPosition(QtCore.QPoint(x, y))
666 if new_cursor is not None:
667 cursor = widget.textCursor()
668 mode = anchor_mode(select)
669 cursor.setPosition(new_cursor.position(), mode)
671 self.set_text_cursor(cursor)
673 def page_down(self, select=False):
674 widget = self.widget
675 widget.page(widget.height() // 2, select=select)
677 def page_up(self, select=False):
678 widget = self.widget
679 widget.page(-widget.height() // 2, select=select)
681 def set_text_cursor(self, cursor):
682 widget = self.widget
683 widget.setTextCursor(cursor)
684 widget.ensureCursorVisible()
685 widget.viewport().update()
687 def keyPressEvent(self, event):
688 """Custom keyboard behaviors
690 The leave() signal is emitted when `Up` is pressed and we're already
691 at the beginning of the text. This allows the parent widget to
692 orchestrate some higher-level interaction, such as giving focus to
693 another widget.
695 When in the middle of the first line and `Up` is pressed, the cursor
696 is moved to the beginning of the line.
699 widget = self.widget
700 if event.key() == Qt.Key_Up:
701 cursor = widget.textCursor()
702 position = cursor.position()
703 if position == 0:
704 # The cursor is at the beginning of the line.
705 # Emit a signal so that the parent can e.g. change focus.
706 widget.leave.emit()
707 elif get(widget)[:position].count('\n') == 0:
708 # The cursor is in the middle of the first line of text.
709 # We can't go up ~ jump to the beginning of the line.
710 # Select the text if shift is pressed.
711 select = event.modifiers() & Qt.ShiftModifier
712 mode = anchor_mode(select)
713 cursor.movePosition(QtGui.QTextCursor.StartOfLine, mode)
714 widget.setTextCursor(cursor)
716 return self.Base.keyPressEvent(widget, event)
719 # pylint: disable=too-many-ancestors
720 class VimHintedPlainTextEdit(HintedPlainTextEdit):
721 """HintedPlainTextEdit with vim hotkeys
723 This can only be used in read-only mode.
727 Base = HintedPlainTextEdit
728 Mixin = VimMixin
730 def __init__(self, context, hint, parent=None):
731 HintedPlainTextEdit.__init__(self, context, hint, parent=parent, readonly=True)
732 self._mixin = self.Mixin(self)
734 def move(self, direction, select=False, n=1):
735 return self._mixin.page(direction, select=select, n=n)
737 def page(self, offset, select=False):
738 return self._mixin.page(offset, select=select)
740 def page_up(self, select=False):
741 return self._mixin.page_up(select=select)
743 def page_down(self, select=False):
744 return self._mixin.page_down(select=select)
746 def keyPressEvent(self, event):
747 return self._mixin.keyPressEvent(event)
750 # pylint: disable=too-many-ancestors
751 class VimTextEdit(MonoTextEdit):
752 """Text viewer with vim-like hotkeys
754 This can only be used in read-only mode.
758 Base = MonoTextEdit
759 Mixin = VimMixin
761 def __init__(self, context, parent=None, readonly=True):
762 MonoTextEdit.__init__(self, context, parent=None, readonly=readonly)
763 self._mixin = self.Mixin(self)
765 def move(self, direction, select=False, n=1):
766 return self._mixin.page(direction, select=select, n=n)
768 def page(self, offset, select=False):
769 return self._mixin.page(offset, select=select)
771 def page_up(self, select=False):
772 return self._mixin.page_up(select=select)
774 def page_down(self, select=False):
775 return self._mixin.page_down(select=select)
777 def keyPressEvent(self, event):
778 return self._mixin.keyPressEvent(event)
781 class HintedDefaultLineEdit(LineEdit):
782 """A line edit with hint text"""
784 def __init__(self, hint, tooltip=None, parent=None):
785 LineEdit.__init__(self, parent=parent, get_value=get_value_hinted)
786 if tooltip:
787 self.setToolTip(tooltip)
788 self.hint = HintWidget(self, hint)
789 self.hint.init()
790 # pylint: disable=no-member
791 self.textChanged.connect(lambda text: self.hint.refresh())
794 class HintedLineEdit(HintedDefaultLineEdit):
795 """A monospace line edit with hint text"""
797 def __init__(self, context, hint, tooltip=None, parent=None):
798 super(HintedLineEdit, self).__init__(hint, tooltip=tooltip, parent=parent)
799 self.setFont(qtutils.diff_font(context))
802 def text_dialog(context, text, title):
803 """Show a wall of text in a dialog"""
804 parent = qtutils.active_window()
806 label = QtWidgets.QLabel(parent)
807 label.setFont(qtutils.diff_font(context))
808 label.setText(text)
809 label.setMargin(defs.large_margin)
810 text_flags = Qt.TextSelectableByKeyboard | Qt.TextSelectableByMouse
811 label.setTextInteractionFlags(text_flags)
813 widget = QtWidgets.QDialog(parent)
814 widget.setWindowModality(Qt.WindowModal)
815 widget.setWindowTitle(title)
817 scroll = QtWidgets.QScrollArea()
818 scroll.setWidget(label)
820 layout = qtutils.hbox(defs.margin, defs.spacing, scroll)
821 widget.setLayout(layout)
823 qtutils.add_action(
824 widget, N_('Close'), widget.accept, Qt.Key_Question, Qt.Key_Enter, Qt.Key_Return
826 widget.show()
827 return widget
830 class VimTextBrowser(VimTextEdit):
831 """Text viewer with line number annotations"""
833 def __init__(self, context, parent=None, readonly=True):
834 VimTextEdit.__init__(self, context, parent=parent, readonly=readonly)
835 self.numbers = LineNumbers(self)
837 def resizeEvent(self, event):
838 super(VimTextBrowser, self).resizeEvent(event)
839 self.numbers.refresh_size()
842 class TextDecorator(QtWidgets.QWidget):
843 """Common functionality for providing line numbers in text widgets"""
845 def __init__(self, parent):
846 QtWidgets.QWidget.__init__(self, parent)
847 self.editor = parent
849 parent.blockCountChanged.connect(lambda x: self._refresh_viewport())
850 parent.cursorPositionChanged.connect(self.refresh)
851 parent.updateRequest.connect(self._refresh_rect)
853 def refresh(self):
854 """Refresh the numbers display"""
855 rect = self.editor.viewport().rect()
856 self._refresh_rect(rect, 0)
858 def _refresh_rect(self, rect, dy):
859 if dy:
860 self.scroll(0, dy)
861 else:
862 self.update(0, rect.y(), self.width(), rect.height())
864 if rect.contains(self.editor.viewport().rect()):
865 self._refresh_viewport()
867 def _refresh_viewport(self):
868 self.editor.setViewportMargins(self.width_hint(), 0, 0, 0)
870 def refresh_size(self):
871 rect = self.editor.contentsRect()
872 geom = QtCore.QRect(rect.left(), rect.top(), self.width_hint(), rect.height())
873 self.setGeometry(geom)
875 def sizeHint(self):
876 return QtCore.QSize(self.width_hint(), 0)
879 class LineNumbers(TextDecorator):
880 """Provide line numbers for QPlainTextEdit widgets"""
882 def __init__(self, parent):
883 TextDecorator.__init__(self, parent)
884 self.highlight_line = -1
886 def width_hint(self):
887 document = self.editor.document()
888 digits = int(math.log(max(1, document.blockCount()), 10)) + 2
889 return defs.large_margin + self.fontMetrics().width('0') * digits
891 def set_highlighted(self, line_number):
892 """Set the line to highlight"""
893 self.highlight_line = line_number
895 def paintEvent(self, event):
896 """Paint the line number"""
897 QPalette = QtGui.QPalette
898 painter = QtGui.QPainter(self)
899 editor = self.editor
900 palette = editor.palette()
902 painter.fillRect(event.rect(), palette.color(QPalette.Base))
904 content_offset = editor.contentOffset()
905 block = editor.firstVisibleBlock()
906 width = self.width()
907 event_rect_bottom = event.rect().bottom()
909 highlight = palette.color(QPalette.Highlight)
910 highlighted_text = palette.color(QPalette.HighlightedText)
911 disabled = palette.color(QPalette.Disabled, QPalette.Text)
913 while block.isValid():
914 block_geom = editor.blockBoundingGeometry(block)
915 block_top = block_geom.translated(content_offset).top()
916 if not block.isVisible() or block_top >= event_rect_bottom:
917 break
919 rect = block_geom.translated(content_offset).toRect()
920 block_number = block.blockNumber()
921 if block_number == self.highlight_line:
922 painter.fillRect(rect.x(), rect.y(), width, rect.height(), highlight)
923 painter.setPen(highlighted_text)
924 else:
925 painter.setPen(disabled)
927 number = '%s' % (block_number + 1)
928 painter.drawText(
929 rect.x(),
930 rect.y(),
931 self.width() - defs.large_margin,
932 rect.height(),
933 Qt.AlignRight | Qt.AlignVCenter,
934 number,
936 block = block.next() # pylint: disable=next-method-called