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
.hint
= HintWidget(self
, hint
)
661 # Refresh palettes when text changes
662 # pylint: disable=no-member
663 self
.textChanged
.connect(self
.hint
.refresh
)
664 self
.setFont(qtutils
.diff_font(context
))
666 def set_value(self
, value
, block
=False):
667 """Set the widget text or enable hint mode when empty"""
668 if value
or self
.hint
.modern
:
669 TextEdit
.set_value(self
, value
, block
=block
)
671 self
.hint
.enable(True)
674 def anchor_mode(select
):
675 """Return the QTextCursor mode to keep/discard the cursor selection"""
677 mode
= QtGui
.QTextCursor
.KeepAnchor
679 mode
= QtGui
.QTextCursor
.MoveAnchor
683 # The vim-like read-only text view
686 class VimMixin(object):
687 def __init__(self
, widget
):
689 self
.Base
= widget
.Base
690 # Common vim/unix-ish keyboard actions
691 self
.add_navigation('End', hotkeys
.GOTO_END
)
692 self
.add_navigation('Up', hotkeys
.MOVE_UP
, shift
=hotkeys
.MOVE_UP_SHIFT
)
693 self
.add_navigation('Down', hotkeys
.MOVE_DOWN
, shift
=hotkeys
.MOVE_DOWN_SHIFT
)
694 self
.add_navigation('Left', hotkeys
.MOVE_LEFT
, shift
=hotkeys
.MOVE_LEFT_SHIFT
)
695 self
.add_navigation('Right', hotkeys
.MOVE_RIGHT
, shift
=hotkeys
.MOVE_RIGHT_SHIFT
)
696 self
.add_navigation('WordLeft', hotkeys
.WORD_LEFT
)
697 self
.add_navigation('WordRight', hotkeys
.WORD_RIGHT
)
698 self
.add_navigation('Start', hotkeys
.GOTO_START
)
699 self
.add_navigation('StartOfLine', hotkeys
.START_OF_LINE
)
700 self
.add_navigation('EndOfLine', hotkeys
.END_OF_LINE
)
706 hotkeys
.SECONDARY_ACTION
,
713 hotkeys
.PRIMARY_ACTION
,
719 lambda: widget
.page_up(select
=True),
726 lambda: widget
.page_down(select
=True),
727 hotkeys
.SELECT_FORWARD
,
731 def add_navigation(self
, name
, hotkey
, shift
=None):
732 """Add a hotkey along with a shift-variant"""
734 direction
= getattr(QtGui
.QTextCursor
, name
)
735 qtutils
.add_action(widget
, name
, lambda: self
.move(direction
), hotkey
)
738 widget
, 'Shift' + name
, lambda: self
.move(direction
, select
=True), shift
741 def move(self
, direction
, select
=False, n
=1):
743 cursor
= widget
.textCursor()
744 mode
= anchor_mode(select
)
746 if cursor
.movePosition(direction
, mode
, 1):
747 self
.set_text_cursor(cursor
)
749 def page(self
, offset
, select
=False):
751 rect
= widget
.cursorRect()
753 y
= rect
.y() + offset
754 new_cursor
= widget
.cursorForPosition(QtCore
.QPoint(x
, y
))
755 if new_cursor
is not None:
756 cursor
= widget
.textCursor()
757 mode
= anchor_mode(select
)
758 cursor
.setPosition(new_cursor
.position(), mode
)
760 self
.set_text_cursor(cursor
)
762 def page_down(self
, select
=False):
764 widget
.page(widget
.height() // 2, select
=select
)
766 def page_up(self
, select
=False):
768 widget
.page(-widget
.height() // 2, select
=select
)
770 def set_text_cursor(self
, cursor
):
772 widget
.setTextCursor(cursor
)
773 widget
.ensureCursorVisible()
774 widget
.viewport().update()
776 def keyPressEvent(self
, event
):
777 """Custom keyboard behaviors
779 The leave() signal is emitted when `Up` is pressed and we're already
780 at the beginning of the text. This allows the parent widget to
781 orchestrate some higher-level interaction, such as giving focus to
784 When in the middle of the first line and `Up` is pressed, the cursor
785 is moved to the beginning of the line.
789 if event
.key() == Qt
.Key_Up
:
790 cursor
= widget
.textCursor()
791 position
= cursor
.position()
793 # The cursor is at the beginning of the line.
794 # Emit a signal so that the parent can e.g. change focus.
796 elif get(widget
)[:position
].count('\n') == 0:
797 # The cursor is in the middle of the first line of text.
798 # We can't go up ~ jump to the beginning of the line.
799 # Select the text if shift is pressed.
800 select
= event
.modifiers() & Qt
.ShiftModifier
801 mode
= anchor_mode(select
)
802 cursor
.movePosition(QtGui
.QTextCursor
.StartOfLine
, mode
)
803 widget
.setTextCursor(cursor
)
805 return self
.Base
.keyPressEvent(widget
, event
)
808 # pylint: disable=too-many-ancestors
809 class VimHintedPlainTextEdit(HintedPlainTextEdit
):
810 """HintedPlainTextEdit with vim hotkeys
812 This can only be used in read-only mode.
816 Base
= HintedPlainTextEdit
819 def __init__(self
, context
, hint
, parent
=None):
820 HintedPlainTextEdit
.__init
__(self
, context
, hint
, parent
=parent
, readonly
=True)
821 self
._mixin
= self
.Mixin(self
)
823 def move(self
, direction
, select
=False, n
=1):
824 return self
._mixin
.page(direction
, select
=select
, n
=n
)
826 def page(self
, offset
, select
=False):
827 return self
._mixin
.page(offset
, select
=select
)
829 def page_up(self
, select
=False):
830 return self
._mixin
.page_up(select
=select
)
832 def page_down(self
, select
=False):
833 return self
._mixin
.page_down(select
=select
)
835 def keyPressEvent(self
, event
):
836 return self
._mixin
.keyPressEvent(event
)
839 # pylint: disable=too-many-ancestors
840 class VimTextEdit(MonoTextEdit
):
841 """Text viewer with vim-like hotkeys
843 This can only be used in read-only mode.
850 def __init__(self
, context
, parent
=None, readonly
=True):
851 MonoTextEdit
.__init
__(self
, context
, parent
=None, readonly
=readonly
)
852 self
._mixin
= self
.Mixin(self
)
854 def move(self
, direction
, select
=False, n
=1):
855 return self
._mixin
.page(direction
, select
=select
, n
=n
)
857 def page(self
, offset
, select
=False):
858 return self
._mixin
.page(offset
, select
=select
)
860 def page_up(self
, select
=False):
861 return self
._mixin
.page_up(select
=select
)
863 def page_down(self
, select
=False):
864 return self
._mixin
.page_down(select
=select
)
866 def keyPressEvent(self
, event
):
867 return self
._mixin
.keyPressEvent(event
)
870 class HintedDefaultLineEdit(LineEdit
):
871 """A line edit with hint text"""
873 def __init__(self
, hint
, tooltip
=None, parent
=None):
874 LineEdit
.__init
__(self
, parent
=parent
, get_value
=get_value_hinted
)
876 self
.setToolTip(tooltip
)
877 self
.hint
= HintWidget(self
, hint
)
879 # pylint: disable=no-member
880 self
.textChanged
.connect(lambda text
: self
.hint
.refresh())
883 class HintedLineEdit(HintedDefaultLineEdit
):
884 """A monospace line edit with hint text"""
886 def __init__(self
, context
, hint
, tooltip
=None, parent
=None):
887 super(HintedLineEdit
, self
).__init
__(hint
, tooltip
=tooltip
, parent
=parent
)
888 self
.setFont(qtutils
.diff_font(context
))
891 def text_dialog(context
, text
, title
):
892 """Show a wall of text in a dialog"""
893 parent
= qtutils
.active_window()
895 label
= QtWidgets
.QLabel(parent
)
896 label
.setFont(qtutils
.diff_font(context
))
898 label
.setMargin(defs
.large_margin
)
899 text_flags
= Qt
.TextSelectableByKeyboard | Qt
.TextSelectableByMouse
900 label
.setTextInteractionFlags(text_flags
)
902 widget
= QtWidgets
.QDialog(parent
)
903 widget
.setWindowModality(Qt
.WindowModal
)
904 widget
.setWindowTitle(title
)
906 scroll
= QtWidgets
.QScrollArea()
907 scroll
.setWidget(label
)
909 layout
= qtutils
.hbox(defs
.margin
, defs
.spacing
, scroll
)
910 widget
.setLayout(layout
)
913 widget
, N_('Close'), widget
.accept
, Qt
.Key_Question
, Qt
.Key_Enter
, Qt
.Key_Return
919 class VimTextBrowser(VimTextEdit
):
920 """Text viewer with line number annotations"""
922 def __init__(self
, context
, parent
=None, readonly
=True):
923 VimTextEdit
.__init
__(self
, context
, parent
=parent
, readonly
=readonly
)
924 self
.numbers
= LineNumbers(self
)
926 def resizeEvent(self
, event
):
927 super(VimTextBrowser
, self
).resizeEvent(event
)
928 self
.numbers
.refresh_size()
931 class TextDecorator(QtWidgets
.QWidget
):
932 """Common functionality for providing line numbers in text widgets"""
934 def __init__(self
, parent
):
935 QtWidgets
.QWidget
.__init
__(self
, parent
)
938 parent
.blockCountChanged
.connect(lambda x
: self
._refresh
_viewport
())
939 parent
.cursorPositionChanged
.connect(self
.refresh
)
940 parent
.updateRequest
.connect(self
._refresh
_rect
)
943 """Refresh the numbers display"""
944 rect
= self
.editor
.viewport().rect()
945 self
._refresh
_rect
(rect
, 0)
947 def _refresh_rect(self
, rect
, dy
):
951 self
.update(0, rect
.y(), self
.width(), rect
.height())
953 if rect
.contains(self
.editor
.viewport().rect()):
954 self
._refresh
_viewport
()
956 def _refresh_viewport(self
):
957 self
.editor
.setViewportMargins(self
.width_hint(), 0, 0, 0)
959 def refresh_size(self
):
960 rect
= self
.editor
.contentsRect()
961 geom
= QtCore
.QRect(rect
.left(), rect
.top(), self
.width_hint(), rect
.height())
962 self
.setGeometry(geom
)
965 return QtCore
.QSize(self
.width_hint(), 0)
968 class LineNumbers(TextDecorator
):
969 """Provide line numbers for QPlainTextEdit widgets"""
971 def __init__(self
, parent
):
972 TextDecorator
.__init
__(self
, parent
)
973 self
.highlight_line
= -1
975 def width_hint(self
):
976 document
= self
.editor
.document()
977 digits
= int(math
.log(max(1, document
.blockCount()), 10)) + 2
978 return defs
.large_margin
+ self
.fontMetrics().width('0') * digits
980 def set_highlighted(self
, line_number
):
981 """Set the line to highlight"""
982 self
.highlight_line
= line_number
984 def paintEvent(self
, event
):
985 """Paint the line number"""
986 QPalette
= QtGui
.QPalette
987 painter
= QtGui
.QPainter(self
)
989 palette
= editor
.palette()
991 painter
.fillRect(event
.rect(), palette
.color(QPalette
.Base
))
993 content_offset
= editor
.contentOffset()
994 block
= editor
.firstVisibleBlock()
996 event_rect_bottom
= event
.rect().bottom()
998 highlight
= palette
.color(QPalette
.Highlight
)
999 highlighted_text
= palette
.color(QPalette
.HighlightedText
)
1000 disabled
= palette
.color(QPalette
.Disabled
, QPalette
.Text
)
1002 while block
.isValid():
1003 block_geom
= editor
.blockBoundingGeometry(block
)
1004 block_top
= block_geom
.translated(content_offset
).top()
1005 if not block
.isVisible() or block_top
>= event_rect_bottom
:
1008 rect
= block_geom
.translated(content_offset
).toRect()
1009 block_number
= block
.blockNumber()
1010 if block_number
== self
.highlight_line
:
1011 painter
.fillRect(rect
.x(), rect
.y(), width
, rect
.height(), highlight
)
1012 painter
.setPen(highlighted_text
)
1014 painter
.setPen(disabled
)
1016 number
= '%s' % (block_number
+ 1)
1020 self
.width() - defs
.large_margin
,
1022 Qt
.AlignRight | Qt
.AlignVCenter
,
1025 block
= block
.next() # pylint: disable=next-method-called