2 # pylint: disable=unexpected-keyword-arg
3 from __future__
import absolute_import
, division
, print_function
, unicode_literals
4 from functools
import partial
7 from qtpy
import QtCore
9 from qtpy
import QtWidgets
10 from qtpy
.QtCore
import Qt
11 from qtpy
.QtCore
import Signal
13 from ..models
import prefs
14 from ..qtutils
import get
15 from .. import hotkeys
17 from .. import qtutils
22 def get_stripped(widget
):
23 return widget
.get().strip()
26 class LineEdit(QtWidgets
.QLineEdit
):
28 cursor_changed
= Signal(int, int)
30 def __init__(self
, parent
=None, row
=1, get_value
=None, clear_button
=False):
31 QtWidgets
.QLineEdit
.__init
__(self
, parent
)
34 get_value
= get_stripped
35 self
._get
_value
= get_value
36 self
.cursor_position
= LineEditCursorPosition(self
, row
)
38 if clear_button
and hasattr(self
, 'setClearButtonEnabled'):
39 self
.setClearButtonEnabled(True)
42 """Return the raw unicode value from Qt"""
46 """Return the processed value, e.g. stripped"""
47 return self
._get
_value
(self
)
49 def set_value(self
, value
, block
=False):
50 """Update the widget to the specified value"""
52 with qtutils
.BlockSignals(self
):
53 self
._set
_value
(value
)
55 self
._set
_value
(value
)
57 def _set_value(self
, value
):
58 """Implementation helper to update the widget to the specified value"""
59 pos
= self
.cursorPosition()
61 self
.setCursorPosition(pos
)
64 class LineEditCursorPosition(object):
65 """Translate cursorPositionChanged(int,int) into cursorPosition(int,int)"""
67 def __init__(self
, widget
, row
):
70 # Translate cursorPositionChanged into cursor_changed(int, int)
71 widget
.cursorPositionChanged
.connect(lambda old
, new
: self
.emit())
76 col
= widget
.cursorPosition()
77 widget
.cursor_changed
.emit(row
, col
)
80 self
._widget
.setCursorPosition(0)
83 class BaseTextEditExtension(QtCore
.QObject
):
84 def __init__(self
, widget
, get_value
, readonly
):
85 QtCore
.QObject
.__init
__(self
, widget
)
87 self
.cursor_position
= TextEditCursorPosition(widget
, self
)
89 get_value
= get_stripped
90 self
._get
_value
= get_value
92 self
._readonly
= readonly
96 def _init_flags(self
):
98 widget
.setMinimumSize(QtCore
.QSize(10, 10))
99 widget
.setWordWrapMode(QtGui
.QTextOption
.WordWrap
)
100 widget
.setLineWrapMode(widget
.NoWrap
)
102 widget
.setReadOnly(True)
103 widget
.setAcceptDrops(False)
104 widget
.setTabChangesFocus(True)
105 widget
.setUndoRedoEnabled(False)
106 widget
.setTextInteractionFlags(
107 Qt
.TextSelectableByKeyboard | Qt
.TextSelectableByMouse
111 """Return the raw unicode value from Qt"""
112 return self
.widget
.toPlainText()
115 """Return a safe value, e.g. a stripped value"""
116 return self
._get
_value
(self
.widget
)
118 def set_value(self
, value
, block
=False):
119 """Update the widget to the specified value"""
121 with qtutils
.BlockSignals(self
):
122 self
._set
_value
(value
)
124 self
._set
_value
(value
)
126 def _set_value(self
, value
):
127 """Implementation helper to update the widget to the specified value"""
128 # Save cursor position
129 offset
, selection_text
= self
.offset_and_selection()
130 old_value
= get(self
.widget
)
133 self
.widget
.setPlainText(value
)
136 if selection_text
and selection_text
in value
:
137 # If the old selection exists in the new text then re-select it.
138 idx
= value
.index(selection_text
)
139 cursor
= self
.widget
.textCursor()
140 cursor
.setPosition(idx
)
141 cursor
.setPosition(idx
+ len(selection_text
), QtGui
.QTextCursor
.KeepAnchor
)
142 self
.widget
.setTextCursor(cursor
)
144 elif value
== old_value
:
145 # Otherwise, if the text is identical and there is no selection
146 # then restore the cursor position.
147 cursor
= self
.widget
.textCursor()
148 cursor
.setPosition(offset
)
149 self
.widget
.setTextCursor(cursor
)
151 # If none of the above applied then restore the cursor position.
152 position
= max(0, min(offset
, len(value
) - 1))
153 cursor
= self
.widget
.textCursor()
154 cursor
.setPosition(position
)
155 self
.widget
.setTextCursor(cursor
)
156 cursor
= self
.widget
.textCursor()
157 cursor
.movePosition(QtGui
.QTextCursor
.StartOfLine
)
158 self
.widget
.setTextCursor(cursor
)
160 def set_cursor_position(self
, new_position
):
161 cursor
= self
.widget
.textCursor()
162 cursor
.setPosition(new_position
)
163 self
.widget
.setTextCursor(cursor
)
166 return self
._tabwidth
168 def set_tabwidth(self
, width
):
169 self
._tabwidth
= width
170 font
= self
.widget
.font()
171 fm
= QtGui
.QFontMetrics(font
)
172 pixels
= fm
.width('M' * width
)
173 self
.widget
.setTabStopWidth(pixels
)
175 def selected_line(self
):
176 contents
= self
.value()
177 cursor
= self
.widget
.textCursor()
178 offset
= min(cursor
.position(), len(contents
) - 1)
179 while offset
>= 1 and contents
[offset
- 1] and contents
[offset
- 1] != '\n':
181 data
= contents
[offset
:]
183 line
, _
= data
.split('\n', 1)
189 return self
.widget
.textCursor()
191 def has_selection(self
):
192 return self
.cursor().hasSelection()
194 def selected_text(self
):
195 """Return the selected text"""
196 _
, selection
= self
.offset_and_selection()
199 def offset_and_selection(self
):
200 """Return the cursor offset and selected text"""
201 cursor
= self
.cursor()
202 offset
= cursor
.selectionStart()
203 selection_text
= cursor
.selection().toPlainText()
204 return offset
, selection_text
206 def mouse_press_event(self
, event
):
207 # Move the text cursor so that the right-click events operate
208 # on the current position, not the last left-clicked position.
210 if event
.button() == Qt
.RightButton
:
211 if not widget
.textCursor().hasSelection():
212 cursor
= widget
.cursorForPosition(event
.pos())
213 widget
.setTextCursor(cursor
)
215 # For extension by sub-classes
217 # pylint: disable=no-self-use
219 """Called during init for class-specific settings"""
222 # pylint: disable=no-self-use,unused-argument
223 def set_textwidth(self
, width
):
224 """Set the text width"""
227 # pylint: disable=no-self-use,unused-argument
228 def set_linebreak(self
, brk
):
229 """Enable word wrapping"""
233 class PlainTextEditExtension(BaseTextEditExtension
):
234 def set_linebreak(self
, brk
):
236 wrapmode
= QtWidgets
.QPlainTextEdit
.WidgetWidth
238 wrapmode
= QtWidgets
.QPlainTextEdit
.NoWrap
239 self
.widget
.setLineWrapMode(wrapmode
)
242 class PlainTextEdit(QtWidgets
.QPlainTextEdit
):
244 cursor_changed
= Signal(int, int)
247 def __init__(self
, parent
=None, get_value
=None, readonly
=False, options
=None):
248 QtWidgets
.QPlainTextEdit
.__init
__(self
, parent
)
249 self
.ext
= PlainTextEditExtension(self
, get_value
, readonly
)
250 self
.cursor_position
= self
.ext
.cursor_position
251 self
.mouse_zoom
= True
252 self
.options
= options
255 """Return the raw unicode value from Qt"""
256 return self
.ext
.get()
258 # For compatibility with QTextEdit
259 def setText(self
, value
):
260 self
.set_value(value
)
263 """Return a safe value, e.g. a stripped value"""
264 return self
.ext
.value()
266 def offset_and_selection(self
):
267 """Return the cursor offset and selected text"""
268 return self
.ext
.offset_and_selection()
270 def set_value(self
, value
, block
=False):
271 self
.ext
.set_value(value
, block
=block
)
273 def set_mouse_zoom(self
, value
):
274 """Enable/disable text zooming in response to ctrl + mousewheel scroll events"""
275 self
.mouse_zoom
= value
277 def set_options(self
, options
):
278 """Register an Options widget"""
279 self
.options
= options
281 def set_word_wrapping(self
, enabled
, update
=False):
282 """Enable/disable word wrapping"""
283 if update
and self
.options
is not None:
284 with qtutils
.BlockSignals(self
.options
.enable_word_wrapping
):
285 self
.options
.enable_word_wrapping
.setChecked(enabled
)
287 self
.setWordWrapMode(QtGui
.QTextOption
.WordWrap
)
288 self
.setLineWrapMode(QtWidgets
.QPlainTextEdit
.WidgetWidth
)
290 self
.setWordWrapMode(QtGui
.QTextOption
.NoWrap
)
291 self
.setLineWrapMode(QtWidgets
.QPlainTextEdit
.NoWrap
)
293 def has_selection(self
):
294 return self
.ext
.has_selection()
296 def selected_line(self
):
297 return self
.ext
.selected_line()
299 def selected_text(self
):
300 """Return the selected text"""
301 return self
.ext
.selected_text()
303 def set_tabwidth(self
, width
):
304 self
.ext
.set_tabwidth(width
)
306 def set_textwidth(self
, width
):
307 self
.ext
.set_textwidth(width
)
309 def set_linebreak(self
, brk
):
310 self
.ext
.set_linebreak(brk
)
312 def mousePressEvent(self
, event
):
313 self
.ext
.mouse_press_event(event
)
314 super(PlainTextEdit
, self
).mousePressEvent(event
)
316 def wheelEvent(self
, event
):
317 """Disable control+wheelscroll text resizing"""
318 if not self
.mouse_zoom
and (event
.modifiers() & Qt
.ControlModifier
):
321 super(PlainTextEdit
, self
).wheelEvent(event
)
323 def contextMenuEvent(self
, event
):
324 """Generate a custom context menu"""
325 menu
= self
.createStandardContextMenu()
326 self
.add_links_to_menu(menu
)
327 menu
.exec_(self
.mapToGlobal(event
.pos()))
329 def add_links_to_menu(self
, menu
):
330 """Add actions for opening URLs to a custom menu"""
331 links
= self
._get
_links
()
333 action
= menu
.addAction(N_('Open "%s"') % url
)
334 action
.setIcon(icons
.external())
335 qtutils
.connect_action(
336 action
, partial(QtGui
.QDesktopServices
.openUrl
, QtCore
.QUrl(url
))
339 def _get_links(self
):
340 """Return http links on the current line"""
341 _
, selection
= self
.offset_and_selection()
345 line
= self
.selected_line()
349 word
for word
in line
.split() if word
.startswith(('http://', 'https://'))
353 class TextEditExtension(BaseTextEditExtension
):
356 widget
.setAcceptRichText(False)
358 def set_linebreak(self
, brk
):
360 wrapmode
= QtWidgets
.QTextEdit
.FixedColumnWidth
362 wrapmode
= QtWidgets
.QTextEdit
.NoWrap
363 self
.widget
.setLineWrapMode(wrapmode
)
365 def set_textwidth(self
, width
):
366 self
.widget
.setLineWrapColumnOrWidth(width
)
369 class TextEdit(QtWidgets
.QTextEdit
):
371 cursor_changed
= Signal(int, int)
374 def __init__(self
, parent
=None, get_value
=None, readonly
=False):
375 QtWidgets
.QTextEdit
.__init
__(self
, parent
)
376 self
.ext
= TextEditExtension(self
, get_value
, readonly
)
377 self
.cursor_position
= self
.ext
.cursor_position
378 self
.expandtab_enabled
= False
381 """Return the raw unicode value from Qt"""
382 return self
.ext
.get()
385 """Return a safe value, e.g. a stripped value"""
386 return self
.ext
.value()
388 def set_value(self
, value
, block
=False):
389 self
.ext
.set_value(value
, block
=block
)
391 def selected_line(self
):
392 return self
.ext
.selected_line()
394 def selected_text(self
):
395 """Return the selected text"""
396 return self
.ext
.selected_text()
398 def set_tabwidth(self
, width
):
399 self
.ext
.set_tabwidth(width
)
401 def set_textwidth(self
, width
):
402 self
.ext
.set_textwidth(width
)
404 def set_linebreak(self
, brk
):
405 self
.ext
.set_linebreak(brk
)
407 def set_expandtab(self
, value
):
408 self
.expandtab_enabled
= value
410 def mousePressEvent(self
, event
):
411 self
.ext
.mouse_press_event(event
)
412 super(TextEdit
, self
).mousePressEvent(event
)
414 def wheelEvent(self
, event
):
415 """Disable control+wheelscroll text resizing"""
416 if event
.modifiers() & Qt
.ControlModifier
:
419 super(TextEdit
, self
).wheelEvent(event
)
421 def should_expandtab(self
, event
):
422 return event
.key() == Qt
.Key_Tab
and self
.expandtab_enabled
425 tabwidth
= max(self
.ext
.tabwidth(), 1)
426 cursor
= self
.textCursor()
427 cursor
.insertText(' ' * tabwidth
)
429 def keyPressEvent(self
, event
):
430 expandtab
= self
.should_expandtab(event
)
435 QtWidgets
.QTextEdit
.keyPressEvent(self
, event
)
437 def keyReleaseEvent(self
, event
):
438 expandtab
= self
.should_expandtab(event
)
442 QtWidgets
.QTextEdit
.keyReleaseEvent(self
, event
)
445 class TextEditCursorPosition(object):
446 def __init__(self
, widget
, ext
):
447 self
._widget
= widget
449 widget
.cursorPositionChanged
.connect(self
.emit
)
452 widget
= self
._widget
454 cursor
= widget
.textCursor()
455 position
= cursor
.position()
457 before
= txt
[:position
]
458 row
= before
.count('\n')
459 line
= before
.split('\n')[row
]
460 col
= cursor
.columnNumber()
461 col
+= line
[:col
].count('\t') * (ext
.tabwidth() - 1)
462 widget
.cursor_changed
.emit(row
+ 1, col
)
465 widget
= self
._widget
466 cursor
= widget
.textCursor()
467 cursor
.setPosition(0)
468 widget
.setTextCursor(cursor
)
471 class MonoTextEdit(PlainTextEdit
):
472 def __init__(self
, context
, parent
=None, readonly
=False):
473 PlainTextEdit
.__init
__(self
, parent
=parent
, readonly
=readonly
)
474 self
.setFont(qtutils
.diff_font(context
))
477 def get_value_hinted(widget
):
478 text
= get_stripped(widget
)
479 hint
= get(widget
.hint
)
485 class HintWidget(QtCore
.QObject
):
486 """Extend a widget to provide hint messages
488 This primarily exists because setPlaceholderText() is only available
489 in Qt5, so this class provides consistent behavior across versions.
493 def __init__(self
, widget
, hint
):
494 QtCore
.QObject
.__init
__(self
, widget
)
495 self
._widget
= widget
497 self
._is
_error
= False
499 self
.modern
= modern
= hasattr(widget
, 'setPlaceholderText')
501 widget
.setPlaceholderText(hint
)
503 # Palette for normal text
504 QPalette
= QtGui
.QPalette
505 palette
= widget
.palette()
507 hint_color
= palette
.color(QPalette
.Disabled
, QPalette
.Text
)
508 error_bg_color
= QtGui
.QColor(Qt
.red
).darker()
509 error_fg_color
= QtGui
.QColor(Qt
.white
)
511 hint_rgb
= qtutils
.rgb_css(hint_color
)
512 error_bg_rgb
= qtutils
.rgb_css(error_bg_color
)
513 error_fg_rgb
= qtutils
.rgb_css(error_fg_color
)
516 name
=widget
.__class
__.__name
__,
517 error_fg_rgb
=error_fg_rgb
,
518 error_bg_rgb
=error_bg_rgb
,
522 self
._default
_style
= ''
533 self
._error
_style
= (
536 color: %(error_fg_rgb)s;
537 background-color: %(error_bg_rgb)s;
544 """Defered initialization"""
546 self
.widget().setPlaceholderText(self
.value())
548 self
.widget().installEventFilter(self
)
552 """Return the parent text widget"""
556 """Return True when hint-mode is active"""
557 return self
.value() == get_stripped(self
._widget
)
560 """Return the current hint text"""
563 def set_error(self
, is_error
):
564 """Enable/disable error mode"""
565 self
._is
_error
= is_error
568 def set_value(self
, hint
):
569 """Change the hint text"""
572 self
._widget
.setPlaceholderText(hint
)
574 # If hint-mode is currently active, re-activate it
575 active
= self
.active()
577 if active
or self
.active():
580 def enable(self
, enable
):
581 """Enable/disable hint-mode"""
583 if enable
and self
._hint
:
584 self
._widget
.set_value(self
._hint
, block
=True)
585 self
._widget
.cursor_position
.reset()
588 self
._update
_palette
(enable
)
591 """Update the palette to match the current mode"""
592 self
._update
_palette
(self
.active())
594 def _update_palette(self
, hint
):
595 """Update to palette for normal/error/hint mode"""
597 style
= self
._error
_style
598 elif not self
.modern
and hint
:
599 style
= self
._hint
_style
601 style
= self
._default
_style
602 QtCore
.QTimer
.singleShot(0, lambda: self
._widget
.setStyleSheet(style
))
604 def eventFilter(self
, _obj
, event
):
605 """Enable/disable hint-mode when focus changes"""
607 if etype
== QtCore
.QEvent
.FocusIn
:
609 elif etype
== QtCore
.QEvent
.FocusOut
:
614 """Disable hint-mode when focused"""
615 widget
= self
.widget()
618 widget
.cursor_position
.emit()
621 """Re-enable hint-mode when losing focus"""
622 widget
= self
.widget()
627 class HintedPlainTextEdit(PlainTextEdit
):
628 """A hinted plain text edit"""
630 def __init__(self
, context
, hint
, parent
=None, readonly
=False):
631 PlainTextEdit
.__init
__(
632 self
, parent
=parent
, get_value
=get_value_hinted
, readonly
=readonly
634 self
.hint
= HintWidget(self
, hint
)
636 self
.context
= context
637 self
.setFont(qtutils
.diff_font(context
))
638 self
.set_tabwidth(prefs
.tabwidth(context
))
639 # Refresh palettes when text changes
640 # pylint: disable=no-member
641 self
.textChanged
.connect(self
.hint
.refresh
)
642 self
.set_mouse_zoom(context
.cfg
.get(prefs
.MOUSE_ZOOM
, default
=True))
644 def set_value(self
, value
, block
=False):
645 """Set the widget text or enable hint mode when empty"""
646 if value
or self
.hint
.modern
:
647 PlainTextEdit
.set_value(self
, value
, block
=block
)
649 self
.hint
.enable(True)
652 class HintedTextEdit(TextEdit
):
653 """A hinted text edit"""
655 def __init__(self
, context
, hint
, parent
=None, readonly
=False):
657 self
, parent
=parent
, get_value
=get_value_hinted
, readonly
=readonly
659 self
.context
= context
660 self
.hint
= HintWidget(self
, hint
)
662 # Refresh palettes when text changes
663 # pylint: disable=no-member
664 self
.textChanged
.connect(self
.hint
.refresh
)
665 self
.setFont(qtutils
.diff_font(context
))
667 def set_value(self
, value
, block
=False):
668 """Set the widget text or enable hint mode when empty"""
669 if value
or self
.hint
.modern
:
670 TextEdit
.set_value(self
, value
, block
=block
)
672 self
.hint
.enable(True)
675 def anchor_mode(select
):
676 """Return the QTextCursor mode to keep/discard the cursor selection"""
678 mode
= QtGui
.QTextCursor
.KeepAnchor
680 mode
= QtGui
.QTextCursor
.MoveAnchor
684 # The vim-like read-only text view
687 class VimMixin(object):
688 def __init__(self
, widget
):
690 self
.Base
= widget
.Base
691 # Common vim/unix-ish keyboard actions
692 self
.add_navigation('End', hotkeys
.GOTO_END
)
693 self
.add_navigation('Up', hotkeys
.MOVE_UP
, shift
=hotkeys
.MOVE_UP_SHIFT
)
694 self
.add_navigation('Down', hotkeys
.MOVE_DOWN
, shift
=hotkeys
.MOVE_DOWN_SHIFT
)
695 self
.add_navigation('Left', hotkeys
.MOVE_LEFT
, shift
=hotkeys
.MOVE_LEFT_SHIFT
)
696 self
.add_navigation('Right', hotkeys
.MOVE_RIGHT
, shift
=hotkeys
.MOVE_RIGHT_SHIFT
)
697 self
.add_navigation('WordLeft', hotkeys
.WORD_LEFT
)
698 self
.add_navigation('WordRight', hotkeys
.WORD_RIGHT
)
699 self
.add_navigation('Start', hotkeys
.GOTO_START
)
700 self
.add_navigation('StartOfLine', hotkeys
.START_OF_LINE
)
701 self
.add_navigation('EndOfLine', hotkeys
.END_OF_LINE
)
707 hotkeys
.SECONDARY_ACTION
,
714 hotkeys
.PRIMARY_ACTION
,
720 lambda: widget
.page_up(select
=True),
727 lambda: widget
.page_down(select
=True),
728 hotkeys
.SELECT_FORWARD
,
732 def add_navigation(self
, name
, hotkey
, shift
=None):
733 """Add a hotkey along with a shift-variant"""
735 direction
= getattr(QtGui
.QTextCursor
, name
)
736 qtutils
.add_action(widget
, name
, lambda: self
.move(direction
), hotkey
)
739 widget
, 'Shift' + name
, lambda: self
.move(direction
, select
=True), shift
742 def move(self
, direction
, select
=False, n
=1):
744 cursor
= widget
.textCursor()
745 mode
= anchor_mode(select
)
747 if cursor
.movePosition(direction
, mode
, 1):
748 self
.set_text_cursor(cursor
)
750 def page(self
, offset
, select
=False):
752 rect
= widget
.cursorRect()
754 y
= rect
.y() + offset
755 new_cursor
= widget
.cursorForPosition(QtCore
.QPoint(x
, y
))
756 if new_cursor
is not None:
757 cursor
= widget
.textCursor()
758 mode
= anchor_mode(select
)
759 cursor
.setPosition(new_cursor
.position(), mode
)
761 self
.set_text_cursor(cursor
)
763 def page_down(self
, select
=False):
765 widget
.page(widget
.height() // 2, select
=select
)
767 def page_up(self
, select
=False):
769 widget
.page(-widget
.height() // 2, select
=select
)
771 def set_text_cursor(self
, cursor
):
773 widget
.setTextCursor(cursor
)
774 widget
.ensureCursorVisible()
775 widget
.viewport().update()
777 def keyPressEvent(self
, event
):
778 """Custom keyboard behaviors
780 The leave() signal is emitted when `Up` is pressed and we're already
781 at the beginning of the text. This allows the parent widget to
782 orchestrate some higher-level interaction, such as giving focus to
785 When in the middle of the first line and `Up` is pressed, the cursor
786 is moved to the beginning of the line.
790 if event
.key() == Qt
.Key_Up
:
791 cursor
= widget
.textCursor()
792 position
= cursor
.position()
794 # The cursor is at the beginning of the line.
795 # Emit a signal so that the parent can e.g. change focus.
797 elif get(widget
)[:position
].count('\n') == 0:
798 # The cursor is in the middle of the first line of text.
799 # We can't go up ~ jump to the beginning of the line.
800 # Select the text if shift is pressed.
801 select
= event
.modifiers() & Qt
.ShiftModifier
802 mode
= anchor_mode(select
)
803 cursor
.movePosition(QtGui
.QTextCursor
.StartOfLine
, mode
)
804 widget
.setTextCursor(cursor
)
806 return self
.Base
.keyPressEvent(widget
, event
)
809 # pylint: disable=too-many-ancestors
810 class VimHintedPlainTextEdit(HintedPlainTextEdit
):
811 """HintedPlainTextEdit with vim hotkeys
813 This can only be used in read-only mode.
817 Base
= HintedPlainTextEdit
820 def __init__(self
, context
, hint
, parent
=None):
821 HintedPlainTextEdit
.__init
__(self
, context
, hint
, parent
=parent
, readonly
=True)
822 self
._mixin
= self
.Mixin(self
)
824 def move(self
, direction
, select
=False, n
=1):
825 return self
._mixin
.page(direction
, select
=select
, n
=n
)
827 def page(self
, offset
, select
=False):
828 return self
._mixin
.page(offset
, select
=select
)
830 def page_up(self
, select
=False):
831 return self
._mixin
.page_up(select
=select
)
833 def page_down(self
, select
=False):
834 return self
._mixin
.page_down(select
=select
)
836 def keyPressEvent(self
, event
):
837 return self
._mixin
.keyPressEvent(event
)
840 # pylint: disable=too-many-ancestors
841 class VimTextEdit(MonoTextEdit
):
842 """Text viewer with vim-like hotkeys
844 This can only be used in read-only mode.
851 def __init__(self
, context
, parent
=None, readonly
=True):
852 MonoTextEdit
.__init
__(self
, context
, parent
=None, readonly
=readonly
)
853 self
._mixin
= self
.Mixin(self
)
855 def move(self
, direction
, select
=False, n
=1):
856 return self
._mixin
.page(direction
, select
=select
, n
=n
)
858 def page(self
, offset
, select
=False):
859 return self
._mixin
.page(offset
, select
=select
)
861 def page_up(self
, select
=False):
862 return self
._mixin
.page_up(select
=select
)
864 def page_down(self
, select
=False):
865 return self
._mixin
.page_down(select
=select
)
867 def keyPressEvent(self
, event
):
868 return self
._mixin
.keyPressEvent(event
)
871 class HintedDefaultLineEdit(LineEdit
):
872 """A line edit with hint text"""
874 def __init__(self
, hint
, tooltip
=None, parent
=None):
875 LineEdit
.__init
__(self
, parent
=parent
, get_value
=get_value_hinted
)
877 self
.setToolTip(tooltip
)
878 self
.hint
= HintWidget(self
, hint
)
880 # pylint: disable=no-member
881 self
.textChanged
.connect(lambda text
: self
.hint
.refresh())
884 class HintedLineEdit(HintedDefaultLineEdit
):
885 """A monospace line edit with hint text"""
887 def __init__(self
, context
, hint
, tooltip
=None, parent
=None):
888 super(HintedLineEdit
, self
).__init
__(hint
, tooltip
=tooltip
, parent
=parent
)
889 self
.setFont(qtutils
.diff_font(context
))
892 def text_dialog(context
, text
, title
):
893 """Show a wall of text in a dialog"""
894 parent
= qtutils
.active_window()
896 label
= QtWidgets
.QLabel(parent
)
897 label
.setFont(qtutils
.diff_font(context
))
899 label
.setMargin(defs
.large_margin
)
900 text_flags
= Qt
.TextSelectableByKeyboard | Qt
.TextSelectableByMouse
901 label
.setTextInteractionFlags(text_flags
)
903 widget
= QtWidgets
.QDialog(parent
)
904 widget
.setWindowModality(Qt
.WindowModal
)
905 widget
.setWindowTitle(title
)
907 scroll
= QtWidgets
.QScrollArea()
908 scroll
.setWidget(label
)
910 layout
= qtutils
.hbox(defs
.margin
, defs
.spacing
, scroll
)
911 widget
.setLayout(layout
)
914 widget
, N_('Close'), widget
.accept
, Qt
.Key_Question
, Qt
.Key_Enter
, Qt
.Key_Return
920 class VimTextBrowser(VimTextEdit
):
921 """Text viewer with line number annotations"""
923 def __init__(self
, context
, parent
=None, readonly
=True):
924 VimTextEdit
.__init
__(self
, context
, parent
=parent
, readonly
=readonly
)
925 self
.numbers
= LineNumbers(self
)
927 def resizeEvent(self
, event
):
928 super(VimTextBrowser
, self
).resizeEvent(event
)
929 self
.numbers
.refresh_size()
932 class TextDecorator(QtWidgets
.QWidget
):
933 """Common functionality for providing line numbers in text widgets"""
935 def __init__(self
, parent
):
936 QtWidgets
.QWidget
.__init
__(self
, parent
)
939 parent
.blockCountChanged
.connect(lambda x
: self
._refresh
_viewport
())
940 parent
.cursorPositionChanged
.connect(self
.refresh
)
941 parent
.updateRequest
.connect(self
._refresh
_rect
)
944 """Refresh the numbers display"""
945 rect
= self
.editor
.viewport().rect()
946 self
._refresh
_rect
(rect
, 0)
948 def _refresh_rect(self
, rect
, dy
):
952 self
.update(0, rect
.y(), self
.width(), rect
.height())
954 if rect
.contains(self
.editor
.viewport().rect()):
955 self
._refresh
_viewport
()
957 def _refresh_viewport(self
):
958 self
.editor
.setViewportMargins(self
.width_hint(), 0, 0, 0)
960 def refresh_size(self
):
961 rect
= self
.editor
.contentsRect()
962 geom
= QtCore
.QRect(rect
.left(), rect
.top(), self
.width_hint(), rect
.height())
963 self
.setGeometry(geom
)
966 return QtCore
.QSize(self
.width_hint(), 0)
969 class LineNumbers(TextDecorator
):
970 """Provide line numbers for QPlainTextEdit widgets"""
972 def __init__(self
, parent
):
973 TextDecorator
.__init
__(self
, parent
)
974 self
.highlight_line
= -1
976 def width_hint(self
):
977 document
= self
.editor
.document()
978 digits
= int(math
.log(max(1, document
.blockCount()), 10)) + 2
979 return defs
.large_margin
+ self
.fontMetrics().width('0') * digits
981 def set_highlighted(self
, line_number
):
982 """Set the line to highlight"""
983 self
.highlight_line
= line_number
985 def paintEvent(self
, event
):
986 """Paint the line number"""
987 QPalette
= QtGui
.QPalette
988 painter
= QtGui
.QPainter(self
)
990 palette
= editor
.palette()
992 painter
.fillRect(event
.rect(), palette
.color(QPalette
.Base
))
994 content_offset
= editor
.contentOffset()
995 block
= editor
.firstVisibleBlock()
997 event_rect_bottom
= event
.rect().bottom()
999 highlight
= palette
.color(QPalette
.Highlight
)
1000 highlighted_text
= palette
.color(QPalette
.HighlightedText
)
1001 disabled
= palette
.color(QPalette
.Disabled
, QPalette
.Text
)
1003 while block
.isValid():
1004 block_geom
= editor
.blockBoundingGeometry(block
)
1005 block_top
= block_geom
.translated(content_offset
).top()
1006 if not block
.isVisible() or block_top
>= event_rect_bottom
:
1009 rect
= block_geom
.translated(content_offset
).toRect()
1010 block_number
= block
.blockNumber()
1011 if block_number
== self
.highlight_line
:
1012 painter
.fillRect(rect
.x(), rect
.y(), width
, rect
.height(), highlight
)
1013 painter
.setPen(highlighted_text
)
1015 painter
.setPen(disabled
)
1017 number
= '%s' % (block_number
+ 1)
1021 self
.width() - defs
.large_margin
,
1023 Qt
.AlignRight | Qt
.AlignVCenter
,
1026 block
= block
.next() # pylint: disable=next-method-called