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
23 def get_stripped(widget
):
24 return widget
.get().strip()
27 class LineEdit(QtWidgets
.QLineEdit
):
29 cursor_changed
= Signal(int, int)
30 esc_pressed
= Signal()
32 def __init__(self
, parent
=None, row
=1, get_value
=None, clear_button
=False):
33 QtWidgets
.QLineEdit
.__init
__(self
, parent
)
36 get_value
= get_stripped
37 self
._get
_value
= get_value
38 self
.cursor_position
= LineEditCursorPosition(self
, row
)
39 self
.menu_actions
= []
41 if clear_button
and hasattr(self
, 'setClearButtonEnabled'):
42 self
.setClearButtonEnabled(True)
45 """Return the raw unicode value from Qt"""
49 """Return the processed value, e.g. stripped"""
50 return self
._get
_value
(self
)
52 def set_value(self
, value
, block
=False):
53 """Update the widget to the specified value"""
55 with qtutils
.BlockSignals(self
):
56 self
._set
_value
(value
)
58 self
._set
_value
(value
)
60 def _set_value(self
, value
):
61 """Implementation helper to update the widget to the specified value"""
62 pos
= self
.cursorPosition()
64 self
.setCursorPosition(pos
)
66 def keyPressEvent(self
, event
):
68 if key
== Qt
.Key_Escape
:
69 self
.esc_pressed
.emit()
70 super(LineEdit
, self
).keyPressEvent(event
)
73 class LineEditCursorPosition(object):
74 """Translate cursorPositionChanged(int,int) into cursorPosition(int,int)"""
76 def __init__(self
, widget
, row
):
79 # Translate cursorPositionChanged into cursor_changed(int, int)
80 widget
.cursorPositionChanged
.connect(lambda old
, new
: self
.emit())
85 col
= widget
.cursorPosition()
86 widget
.cursor_changed
.emit(row
, col
)
89 self
._widget
.setCursorPosition(0)
92 class BaseTextEditExtension(QtCore
.QObject
):
93 def __init__(self
, widget
, get_value
, readonly
):
94 QtCore
.QObject
.__init
__(self
, widget
)
96 self
.cursor_position
= TextEditCursorPosition(widget
, self
)
98 get_value
= get_stripped
99 self
._get
_value
= get_value
101 self
._readonly
= readonly
105 def _init_flags(self
):
107 widget
.setMinimumSize(QtCore
.QSize(10, 10))
108 widget
.setWordWrapMode(QtGui
.QTextOption
.WordWrap
)
109 widget
.setLineWrapMode(widget
.NoWrap
)
111 widget
.setReadOnly(True)
112 widget
.setAcceptDrops(False)
113 widget
.setTabChangesFocus(True)
114 widget
.setUndoRedoEnabled(False)
115 widget
.setTextInteractionFlags(
116 Qt
.TextSelectableByKeyboard | Qt
.TextSelectableByMouse
120 """Return the raw unicode value from Qt"""
121 return self
.widget
.toPlainText()
124 """Return a safe value, e.g. a stripped value"""
125 return self
._get
_value
(self
.widget
)
127 def set_value(self
, value
, block
=False):
128 """Update the widget to the specified value"""
130 with qtutils
.BlockSignals(self
):
131 self
._set
_value
(value
)
133 self
._set
_value
(value
)
135 def _set_value(self
, value
):
136 """Implementation helper to update the widget to the specified value"""
137 # Save cursor position
138 offset
, selection_text
= self
.offset_and_selection()
139 old_value
= get(self
.widget
)
142 self
.widget
.setPlainText(value
)
145 if selection_text
and selection_text
in value
:
146 # If the old selection exists in the new text then re-select it.
147 idx
= value
.index(selection_text
)
148 cursor
= self
.widget
.textCursor()
149 cursor
.setPosition(idx
)
150 cursor
.setPosition(idx
+ len(selection_text
), QtGui
.QTextCursor
.KeepAnchor
)
151 self
.widget
.setTextCursor(cursor
)
153 elif value
== old_value
:
154 # Otherwise, if the text is identical and there is no selection
155 # then restore the cursor position.
156 cursor
= self
.widget
.textCursor()
157 cursor
.setPosition(offset
)
158 self
.widget
.setTextCursor(cursor
)
160 # If none of the above applied then restore the cursor position.
161 position
= max(0, min(offset
, len(value
) - 1))
162 cursor
= self
.widget
.textCursor()
163 cursor
.setPosition(position
)
164 self
.widget
.setTextCursor(cursor
)
165 cursor
= self
.widget
.textCursor()
166 cursor
.movePosition(QtGui
.QTextCursor
.StartOfLine
)
167 self
.widget
.setTextCursor(cursor
)
169 def set_cursor_position(self
, new_position
):
170 cursor
= self
.widget
.textCursor()
171 cursor
.setPosition(new_position
)
172 self
.widget
.setTextCursor(cursor
)
175 return self
._tabwidth
177 def set_tabwidth(self
, width
):
178 self
._tabwidth
= width
179 font
= self
.widget
.font()
180 metrics
= QtGui
.QFontMetrics(font
)
181 pixels
= metrics
.width('M' * width
)
182 self
.widget
.setTabStopWidth(pixels
)
184 def selected_line(self
):
185 contents
= self
.value()
186 cursor
= self
.widget
.textCursor()
187 offset
= min(cursor
.position(), len(contents
) - 1)
188 while offset
>= 1 and contents
[offset
- 1] and contents
[offset
- 1] != '\n':
190 data
= contents
[offset
:]
192 line
, _
= data
.split('\n', 1)
198 return self
.widget
.textCursor()
200 def has_selection(self
):
201 return self
.cursor().hasSelection()
203 def selected_text(self
):
204 """Return the selected text"""
205 _
, selection
= self
.offset_and_selection()
208 def offset_and_selection(self
):
209 """Return the cursor offset and selected text"""
210 cursor
= self
.cursor()
211 offset
= cursor
.selectionStart()
212 selection_text
= cursor
.selection().toPlainText()
213 return offset
, selection_text
215 def mouse_press_event(self
, event
):
216 # Move the text cursor so that the right-click events operate
217 # on the current position, not the last left-clicked position.
219 if event
.button() == Qt
.RightButton
:
220 if not widget
.textCursor().hasSelection():
221 cursor
= widget
.cursorForPosition(event
.pos())
222 widget
.setTextCursor(cursor
)
224 def add_links_to_menu(self
, menu
):
225 """Add actions for opening URLs to a custom menu"""
226 links
= self
._get
_links
()
230 action
= menu
.addAction(N_('Open "%s"') % url
)
231 action
.setIcon(icons
.external())
232 qtutils
.connect_action(
233 action
, partial(QtGui
.QDesktopServices
.openUrl
, QtCore
.QUrl(url
))
236 def _get_links(self
):
237 """Return http links on the current line"""
238 _
, selection
= self
.offset_and_selection()
242 line
= self
.selected_line()
246 word
for word
in line
.split() if word
.startswith(('http://', 'https://'))
249 def create_context_menu(self
, event_pos
):
250 """Create a context menu for a widget"""
251 menu
= self
.widget
.createStandardContextMenu(event_pos
)
252 qtutils
.add_menu_actions(menu
, self
.widget
.menu_actions
)
253 self
.add_links_to_menu(menu
)
256 def context_menu_event(self
, event
):
257 """Default context menu event"""
258 event_pos
= event
.pos()
259 menu
= self
.widget
.create_context_menu(event_pos
)
260 menu
.exec_(self
.widget
.mapToGlobal(event_pos
))
262 # For extension by sub-classes
265 """Called during init for class-specific settings"""
268 # pylint: disable=unused-argument
269 def set_textwidth(self
, width
):
270 """Set the text width"""
273 # pylint: disable=unused-argument
274 def set_linebreak(self
, brk
):
275 """Enable word wrapping"""
279 class PlainTextEditExtension(BaseTextEditExtension
):
280 def set_linebreak(self
, brk
):
282 wrapmode
= QtWidgets
.QPlainTextEdit
.WidgetWidth
284 wrapmode
= QtWidgets
.QPlainTextEdit
.NoWrap
285 self
.widget
.setLineWrapMode(wrapmode
)
288 class PlainTextEdit(QtWidgets
.QPlainTextEdit
):
290 cursor_changed
= Signal(int, int)
293 def __init__(self
, parent
=None, get_value
=None, readonly
=False, options
=None):
294 QtWidgets
.QPlainTextEdit
.__init
__(self
, parent
)
295 self
.ext
= PlainTextEditExtension(self
, get_value
, readonly
)
296 self
.cursor_position
= self
.ext
.cursor_position
297 self
.mouse_zoom
= True
298 self
.options
= options
299 self
.menu_actions
= []
302 """Return the raw unicode value from Qt"""
303 return self
.ext
.get()
305 # For compatibility with QTextEdit
306 def setText(self
, value
):
307 self
.set_value(value
)
310 """Return a safe value, e.g. a stripped value"""
311 return self
.ext
.value()
313 def offset_and_selection(self
):
314 """Return the cursor offset and selected text"""
315 return self
.ext
.offset_and_selection()
317 def set_value(self
, value
, block
=False):
318 self
.ext
.set_value(value
, block
=block
)
320 def set_mouse_zoom(self
, value
):
321 """Enable/disable text zooming in response to ctrl + mousewheel scroll events"""
322 self
.mouse_zoom
= value
324 def set_options(self
, options
):
325 """Register an Options widget"""
326 self
.options
= options
328 def set_word_wrapping(self
, enabled
, update
=False):
329 """Enable/disable word wrapping"""
330 if update
and self
.options
is not None:
331 with qtutils
.BlockSignals(self
.options
.enable_word_wrapping
):
332 self
.options
.enable_word_wrapping
.setChecked(enabled
)
334 self
.setWordWrapMode(QtGui
.QTextOption
.WordWrap
)
335 self
.setLineWrapMode(QtWidgets
.QPlainTextEdit
.WidgetWidth
)
337 self
.setWordWrapMode(QtGui
.QTextOption
.NoWrap
)
338 self
.setLineWrapMode(QtWidgets
.QPlainTextEdit
.NoWrap
)
340 def has_selection(self
):
341 return self
.ext
.has_selection()
343 def selected_line(self
):
344 return self
.ext
.selected_line()
346 def selected_text(self
):
347 """Return the selected text"""
348 return self
.ext
.selected_text()
350 def set_tabwidth(self
, width
):
351 self
.ext
.set_tabwidth(width
)
353 def set_textwidth(self
, width
):
354 self
.ext
.set_textwidth(width
)
356 def set_linebreak(self
, brk
):
357 self
.ext
.set_linebreak(brk
)
359 def mousePressEvent(self
, event
):
360 self
.ext
.mouse_press_event(event
)
361 super(PlainTextEdit
, self
).mousePressEvent(event
)
363 def wheelEvent(self
, event
):
364 """Disable control+wheelscroll text resizing"""
365 if not self
.mouse_zoom
and (event
.modifiers() & Qt
.ControlModifier
):
368 super(PlainTextEdit
, self
).wheelEvent(event
)
370 def create_context_menu(self
, event_pos
):
371 """Create a custom context menu"""
372 return self
.ext
.create_context_menu(event_pos
)
374 def contextMenuEvent(self
, event
):
375 """Custom contextMenuEvent() for building our custom context menus"""
376 self
.ext
.context_menu_event(event
)
379 class TextSearchWidget(QtWidgets
.QWidget
):
380 """The search dialog that displays over a text edit field"""
382 def __init__(self
, widget
, parent
):
383 super(TextSearchWidget
, self
).__init
__(parent
)
384 self
.setAutoFillBackground(True)
385 self
._widget
= widget
386 self
._parent
= parent
388 self
.text
= HintedDefaultLineEdit(N_('Find in diff'), parent
=self
)
390 self
.prev_button
= qtutils
.create_action_button(
391 tooltip
=N_('Find the previous occurrence of the phrase'), icon
=icons
.up()
394 self
.next_button
= qtutils
.create_action_button(
395 tooltip
=N_('Find the next occurrence of the phrase'), icon
=icons
.down()
398 self
.match_case_checkbox
= qtutils
.checkbox(N_('Match Case'))
399 self
.whole_words_checkbox
= qtutils
.checkbox(N_('Whole Words'))
401 self
.close_button
= qtutils
.create_action_button(
402 tooltip
=N_('Close the find bar'), icon
=icons
.close()
405 layout
= qtutils
.hbox(
411 self
.match_case_checkbox
,
412 self
.whole_words_checkbox
,
416 self
.setLayout(layout
)
417 self
.setFocusProxy(self
.text
)
419 self
.text
.esc_pressed
.connect(self
.hide_search
)
420 self
.text
.returnPressed
.connect(self
.search
)
421 self
.text
.textChanged
.connect(self
.search
)
423 qtutils
.connect_button(self
.next_button
, self
.search
)
424 qtutils
.connect_button(self
.prev_button
, self
.search_backwards
)
425 qtutils
.connect_button(self
.close_button
, self
.hide_search
)
426 qtutils
.connect_checkbox(self
.match_case_checkbox
, lambda _
: self
.search())
427 qtutils
.connect_checkbox(self
.whole_words_checkbox
, lambda _
: self
.search())
430 """Emit a signal with the current search text"""
431 self
.search_text(backwards
=False)
433 def search_backwards(self
):
434 """Emit a signal with the current search text for a backwards search"""
435 self
.search_text(backwards
=True)
437 def hide_search(self
):
438 """Hide the search window"""
440 self
._parent
.setFocus(True)
442 def find_flags(self
, backwards
):
443 """Return QTextDocument.FindFlags for the current search options"""
444 flags
= QtGui
.QTextDocument
.FindFlags()
446 flags
= flags | QtGui
.QTextDocument
.FindBackward
447 if self
.match_case_checkbox
.isChecked():
448 flags
= flags | QtGui
.QTextDocument
.FindCaseSensitively
449 if self
.whole_words_checkbox
.isChecked():
450 flags
= flags | QtGui
.QTextDocument
.FindWholeWords
453 def is_case_sensitive(self
):
454 """Are we searching using a case-insensitive search?"""
455 return self
.match_case_checkbox
.isChecked()
457 def search_text(self
, backwards
=False):
458 """Search the diff text for the given text"""
459 text
= self
.text
.get()
460 cursor
= self
._widget
.textCursor()
461 if cursor
.hasSelection():
462 selected_text
= cursor
.selectedText()
463 case_sensitive
= self
.is_case_sensitive()
464 if text_matches(case_sensitive
, selected_text
, text
):
466 position
= cursor
.selectionStart()
468 position
= cursor
.selectionEnd()
471 position
= cursor
.selectionEnd()
473 position
= cursor
.selectionStart()
474 cursor
.setPosition(position
)
475 self
._widget
.setTextCursor(cursor
)
477 flags
= self
.find_flags(backwards
)
478 if not self
._widget
.find(text
, flags
):
480 location
= QtGui
.QTextCursor
.End
482 location
= QtGui
.QTextCursor
.Start
483 cursor
.movePosition(location
, QtGui
.QTextCursor
.MoveAnchor
)
484 self
._widget
.setTextCursor(cursor
)
485 self
._widget
.find(text
, flags
)
488 def text_matches(case_sensitive
, a
, b
):
489 """Compare text with case sensitivity taken into account"""
492 return a
.lower() == b
.lower()
495 class TextEditExtension(BaseTextEditExtension
):
498 widget
.setAcceptRichText(False)
500 def set_linebreak(self
, brk
):
502 wrapmode
= QtWidgets
.QTextEdit
.FixedColumnWidth
504 wrapmode
= QtWidgets
.QTextEdit
.NoWrap
505 self
.widget
.setLineWrapMode(wrapmode
)
507 def set_textwidth(self
, width
):
508 self
.widget
.setLineWrapColumnOrWidth(width
)
511 class TextEdit(QtWidgets
.QTextEdit
):
513 cursor_changed
= Signal(int, int)
516 def __init__(self
, parent
=None, get_value
=None, readonly
=False):
517 QtWidgets
.QTextEdit
.__init
__(self
, parent
)
518 self
.ext
= TextEditExtension(self
, get_value
, readonly
)
519 self
.cursor_position
= self
.ext
.cursor_position
520 self
.expandtab_enabled
= False
521 self
.menu_actions
= []
524 """Return the raw unicode value from Qt"""
525 return self
.ext
.get()
528 """Return a safe value, e.g. a stripped value"""
529 return self
.ext
.value()
531 def set_cursor_position(self
, position
):
532 """Set the cursor position"""
533 cursor
= self
.textCursor()
534 cursor
.setPosition(position
)
535 self
.setTextCursor(cursor
)
537 def set_value(self
, value
, block
=False):
538 self
.ext
.set_value(value
, block
=block
)
540 def selected_line(self
):
541 return self
.ext
.selected_line()
543 def selected_text(self
):
544 """Return the selected text"""
545 return self
.ext
.selected_text()
547 def set_tabwidth(self
, width
):
548 self
.ext
.set_tabwidth(width
)
550 def set_textwidth(self
, width
):
551 self
.ext
.set_textwidth(width
)
553 def set_linebreak(self
, brk
):
554 self
.ext
.set_linebreak(brk
)
556 def set_expandtab(self
, value
):
557 self
.expandtab_enabled
= value
559 def mousePressEvent(self
, event
):
560 self
.ext
.mouse_press_event(event
)
561 super(TextEdit
, self
).mousePressEvent(event
)
563 def wheelEvent(self
, event
):
564 """Disable control+wheelscroll text resizing"""
565 if event
.modifiers() & Qt
.ControlModifier
:
568 super(TextEdit
, self
).wheelEvent(event
)
570 def should_expandtab(self
, event
):
571 return event
.key() == Qt
.Key_Tab
and self
.expandtab_enabled
574 tabwidth
= max(self
.ext
.tabwidth(), 1)
575 cursor
= self
.textCursor()
576 cursor
.insertText(' ' * tabwidth
)
578 def create_context_menu(self
, event_pos
):
579 """Create a custom context menu"""
580 return self
.ext
.create_context_menu(event_pos
)
582 def contextMenuEvent(self
, event
):
583 """Custom contextMenuEvent() for building our custom context menus"""
584 self
.ext
.context_menu_event(event
)
586 def keyPressEvent(self
, event
):
587 """Override keyPressEvent to handle tab expansion"""
588 expandtab
= self
.should_expandtab(event
)
593 QtWidgets
.QTextEdit
.keyPressEvent(self
, event
)
595 def keyReleaseEvent(self
, event
):
596 """Override keyReleaseEvent to special-case tab expansion"""
597 expandtab
= self
.should_expandtab(event
)
601 QtWidgets
.QTextEdit
.keyReleaseEvent(self
, event
)
604 class TextEditCursorPosition(object):
605 def __init__(self
, widget
, ext
):
606 self
._widget
= widget
608 widget
.cursorPositionChanged
.connect(self
.emit
)
611 widget
= self
._widget
613 cursor
= widget
.textCursor()
614 position
= cursor
.position()
616 before
= txt
[:position
]
617 row
= before
.count('\n')
618 line
= before
.split('\n')[row
]
619 col
= cursor
.columnNumber()
620 col
+= line
[:col
].count('\t') * (ext
.tabwidth() - 1)
621 widget
.cursor_changed
.emit(row
+ 1, col
)
624 widget
= self
._widget
625 cursor
= widget
.textCursor()
626 cursor
.setPosition(0)
627 widget
.setTextCursor(cursor
)
630 class MonoTextEdit(PlainTextEdit
):
631 def __init__(self
, context
, parent
=None, readonly
=False):
632 PlainTextEdit
.__init
__(self
, parent
=parent
, readonly
=readonly
)
633 self
.setFont(qtutils
.diff_font(context
))
636 def get_value_hinted(widget
):
637 text
= get_stripped(widget
)
638 hint
= get(widget
.hint
)
644 class HintWidget(QtCore
.QObject
):
645 """Extend a widget to provide hint messages
647 This primarily exists because setPlaceholderText() is only available
648 in Qt5, so this class provides consistent behavior across versions.
652 def __init__(self
, widget
, hint
):
653 QtCore
.QObject
.__init
__(self
, widget
)
654 self
._widget
= widget
656 self
._is
_error
= False
658 self
.modern
= modern
= hasattr(widget
, 'setPlaceholderText')
660 widget
.setPlaceholderText(hint
)
662 # Palette for normal text
663 QPalette
= QtGui
.QPalette
664 palette
= widget
.palette()
666 hint_color
= palette
.color(QPalette
.Disabled
, QPalette
.Text
)
667 error_bg_color
= QtGui
.QColor(Qt
.red
).darker()
668 error_fg_color
= QtGui
.QColor(Qt
.white
)
670 hint_rgb
= qtutils
.rgb_css(hint_color
)
671 error_bg_rgb
= qtutils
.rgb_css(error_bg_color
)
672 error_fg_rgb
= qtutils
.rgb_css(error_fg_color
)
675 name
=widget
.__class
__.__name
__,
676 error_fg_rgb
=error_fg_rgb
,
677 error_bg_rgb
=error_bg_rgb
,
681 self
._default
_style
= ''
692 self
._error
_style
= (
695 color: %(error_fg_rgb)s;
696 background-color: %(error_bg_rgb)s;
703 """Defered initialization"""
705 self
.widget().setPlaceholderText(self
.value())
707 self
.widget().installEventFilter(self
)
711 """Return the parent text widget"""
715 """Return True when hint-mode is active"""
716 return self
.value() == get_stripped(self
._widget
)
719 """Return the current hint text"""
722 def set_error(self
, is_error
):
723 """Enable/disable error mode"""
724 self
._is
_error
= is_error
727 def set_value(self
, hint
):
728 """Change the hint text"""
731 self
._widget
.setPlaceholderText(hint
)
733 # If hint-mode is currently active, re-activate it
734 active
= self
.active()
736 if active
or self
.active():
739 def enable(self
, enable
):
740 """Enable/disable hint-mode"""
742 if enable
and self
._hint
:
743 self
._widget
.set_value(self
._hint
, block
=True)
744 self
._widget
.cursor_position
.reset()
747 self
._update
_palette
(enable
)
750 """Update the palette to match the current mode"""
751 self
._update
_palette
(self
.active())
753 def _update_palette(self
, hint
):
754 """Update to palette for normal/error/hint mode"""
756 style
= self
._error
_style
757 elif not self
.modern
and hint
:
758 style
= self
._hint
_style
760 style
= self
._default
_style
761 QtCore
.QTimer
.singleShot(
762 0, lambda: utils
.catch_runtime_error(self
._widget
.setStyleSheet
, style
)
765 def eventFilter(self
, _obj
, event
):
766 """Enable/disable hint-mode when focus changes"""
768 if etype
== QtCore
.QEvent
.FocusIn
:
770 elif etype
== QtCore
.QEvent
.FocusOut
:
775 """Disable hint-mode when focused"""
776 widget
= self
.widget()
779 widget
.cursor_position
.emit()
782 """Re-enable hint-mode when losing focus"""
783 widget
= self
.widget()
784 valid
, value
= utils
.catch_runtime_error(get
, widget
)
786 # The widget may have just been destroyed during application shutdown.
787 # We're receiving a focusOut event but the widget can no longer be used.
788 # This can be safely ignored.
794 class HintedPlainTextEdit(PlainTextEdit
):
795 """A hinted plain text edit"""
797 def __init__(self
, context
, hint
, parent
=None, readonly
=False):
798 PlainTextEdit
.__init
__(
799 self
, parent
=parent
, get_value
=get_value_hinted
, readonly
=readonly
801 self
.hint
= HintWidget(self
, hint
)
803 self
.context
= context
804 self
.setFont(qtutils
.diff_font(context
))
805 self
.set_tabwidth(prefs
.tabwidth(context
))
806 # Refresh palettes when text changes
807 # pylint: disable=no-member
808 self
.textChanged
.connect(self
.hint
.refresh
)
809 self
.set_mouse_zoom(context
.cfg
.get(prefs
.MOUSE_ZOOM
, default
=True))
811 def set_value(self
, value
, block
=False):
812 """Set the widget text or enable hint mode when empty"""
813 if value
or self
.hint
.modern
:
814 PlainTextEdit
.set_value(self
, value
, block
=block
)
816 self
.hint
.enable(True)
819 class HintedTextEdit(TextEdit
):
820 """A hinted text edit"""
822 def __init__(self
, context
, hint
, parent
=None, readonly
=False):
824 self
, parent
=parent
, get_value
=get_value_hinted
, readonly
=readonly
826 self
.context
= context
827 self
.hint
= HintWidget(self
, hint
)
829 # Refresh palettes when text changes
830 # pylint: disable=no-member
831 self
.textChanged
.connect(self
.hint
.refresh
)
832 self
.setFont(qtutils
.diff_font(context
))
834 def set_value(self
, value
, block
=False):
835 """Set the widget text or enable hint mode when empty"""
836 if value
or self
.hint
.modern
:
837 TextEdit
.set_value(self
, value
, block
=block
)
839 self
.hint
.enable(True)
842 def anchor_mode(select
):
843 """Return the QTextCursor mode to keep/discard the cursor selection"""
845 mode
= QtGui
.QTextCursor
.KeepAnchor
847 mode
= QtGui
.QTextCursor
.MoveAnchor
851 # The vim-like read-only text view
854 class VimMixin(object):
855 def __init__(self
, widget
):
857 self
.Base
= widget
.Base
858 # Common vim/unix-ish keyboard actions
859 self
.add_navigation('End', hotkeys
.GOTO_END
)
860 self
.add_navigation('Up', hotkeys
.MOVE_UP
, shift
=hotkeys
.MOVE_UP_SHIFT
)
861 self
.add_navigation('Down', hotkeys
.MOVE_DOWN
, shift
=hotkeys
.MOVE_DOWN_SHIFT
)
862 self
.add_navigation('Left', hotkeys
.MOVE_LEFT
, shift
=hotkeys
.MOVE_LEFT_SHIFT
)
863 self
.add_navigation('Right', hotkeys
.MOVE_RIGHT
, shift
=hotkeys
.MOVE_RIGHT_SHIFT
)
864 self
.add_navigation('WordLeft', hotkeys
.WORD_LEFT
)
865 self
.add_navigation('WordRight', hotkeys
.WORD_RIGHT
)
866 self
.add_navigation('Start', hotkeys
.GOTO_START
)
867 self
.add_navigation('StartOfLine', hotkeys
.START_OF_LINE
)
868 self
.add_navigation('EndOfLine', hotkeys
.END_OF_LINE
)
874 hotkeys
.SECONDARY_ACTION
,
881 hotkeys
.PRIMARY_ACTION
,
887 lambda: widget
.page_up(select
=True),
894 lambda: widget
.page_down(select
=True),
895 hotkeys
.SELECT_FORWARD
,
899 def add_navigation(self
, name
, hotkey
, shift
=None):
900 """Add a hotkey along with a shift-variant"""
902 direction
= getattr(QtGui
.QTextCursor
, name
)
903 qtutils
.add_action(widget
, name
, lambda: self
.move(direction
), hotkey
)
906 widget
, 'Shift' + name
, lambda: self
.move(direction
, select
=True), shift
909 def move(self
, direction
, select
=False, n
=1):
911 cursor
= widget
.textCursor()
912 mode
= anchor_mode(select
)
914 if cursor
.movePosition(direction
, mode
, 1):
915 self
.set_text_cursor(cursor
)
917 def page(self
, offset
, select
=False):
919 rect
= widget
.cursorRect()
921 y
= rect
.y() + offset
922 new_cursor
= widget
.cursorForPosition(QtCore
.QPoint(x
, y
))
923 if new_cursor
is not None:
924 cursor
= widget
.textCursor()
925 mode
= anchor_mode(select
)
926 cursor
.setPosition(new_cursor
.position(), mode
)
928 self
.set_text_cursor(cursor
)
930 def page_down(self
, select
=False):
932 widget
.page(widget
.height() // 2, select
=select
)
934 def page_up(self
, select
=False):
936 widget
.page(-widget
.height() // 2, select
=select
)
938 def set_text_cursor(self
, cursor
):
940 widget
.setTextCursor(cursor
)
941 widget
.ensureCursorVisible()
942 widget
.viewport().update()
944 def keyPressEvent(self
, event
):
945 """Custom keyboard behaviors
947 The leave() signal is emitted when `Up` is pressed and we're already
948 at the beginning of the text. This allows the parent widget to
949 orchestrate some higher-level interaction, such as giving focus to
952 When in the middle of the first line and `Up` is pressed, the cursor
953 is moved to the beginning of the line.
957 if event
.key() == Qt
.Key_Up
:
958 cursor
= widget
.textCursor()
959 position
= cursor
.position()
961 # The cursor is at the beginning of the line.
962 # Emit a signal so that the parent can e.g. change focus.
964 elif get(widget
)[:position
].count('\n') == 0:
965 # The cursor is in the middle of the first line of text.
966 # We can't go up ~ jump to the beginning of the line.
967 # Select the text if shift is pressed.
968 select
= event
.modifiers() & Qt
.ShiftModifier
969 mode
= anchor_mode(select
)
970 cursor
.movePosition(QtGui
.QTextCursor
.StartOfLine
, mode
)
971 widget
.setTextCursor(cursor
)
973 return self
.Base
.keyPressEvent(widget
, event
)
976 # pylint: disable=too-many-ancestors
977 class VimHintedPlainTextEdit(HintedPlainTextEdit
):
978 """HintedPlainTextEdit with vim hotkeys
980 This can only be used in read-only mode.
984 Base
= HintedPlainTextEdit
987 def __init__(self
, context
, hint
, parent
=None):
988 HintedPlainTextEdit
.__init
__(self
, context
, hint
, parent
=parent
, readonly
=True)
989 self
._mixin
= self
.Mixin(self
)
991 def move(self
, direction
, select
=False, n
=1):
992 return self
._mixin
.page(direction
, select
=select
, n
=n
)
994 def page(self
, offset
, select
=False):
995 return self
._mixin
.page(offset
, select
=select
)
997 def page_up(self
, select
=False):
998 return self
._mixin
.page_up(select
=select
)
1000 def page_down(self
, select
=False):
1001 return self
._mixin
.page_down(select
=select
)
1003 def keyPressEvent(self
, event
):
1004 return self
._mixin
.keyPressEvent(event
)
1007 # pylint: disable=too-many-ancestors
1008 class VimTextEdit(MonoTextEdit
):
1009 """Text viewer with vim-like hotkeys
1011 This can only be used in read-only mode.
1018 def __init__(self
, context
, parent
=None, readonly
=True):
1019 MonoTextEdit
.__init
__(self
, context
, parent
=None, readonly
=readonly
)
1020 self
._mixin
= self
.Mixin(self
)
1022 def move(self
, direction
, select
=False, n
=1):
1023 return self
._mixin
.page(direction
, select
=select
, n
=n
)
1025 def page(self
, offset
, select
=False):
1026 return self
._mixin
.page(offset
, select
=select
)
1028 def page_up(self
, select
=False):
1029 return self
._mixin
.page_up(select
=select
)
1031 def page_down(self
, select
=False):
1032 return self
._mixin
.page_down(select
=select
)
1034 def keyPressEvent(self
, event
):
1035 return self
._mixin
.keyPressEvent(event
)
1038 class HintedDefaultLineEdit(LineEdit
):
1039 """A line edit with hint text"""
1041 def __init__(self
, hint
, tooltip
=None, parent
=None):
1042 LineEdit
.__init
__(self
, parent
=parent
, get_value
=get_value_hinted
)
1044 self
.setToolTip(tooltip
)
1045 self
.hint
= HintWidget(self
, hint
)
1047 # pylint: disable=no-member
1048 self
.textChanged
.connect(lambda text
: self
.hint
.refresh())
1051 class HintedLineEdit(HintedDefaultLineEdit
):
1052 """A monospace line edit with hint text"""
1054 def __init__(self
, context
, hint
, tooltip
=None, parent
=None):
1055 super(HintedLineEdit
, self
).__init
__(hint
, tooltip
=tooltip
, parent
=parent
)
1056 self
.setFont(qtutils
.diff_font(context
))
1059 def text_dialog(context
, text
, title
):
1060 """Show a wall of text in a dialog"""
1061 parent
= qtutils
.active_window()
1063 label
= QtWidgets
.QLabel(parent
)
1064 label
.setFont(qtutils
.diff_font(context
))
1066 label
.setMargin(defs
.large_margin
)
1067 text_flags
= Qt
.TextSelectableByKeyboard | Qt
.TextSelectableByMouse
1068 label
.setTextInteractionFlags(text_flags
)
1070 widget
= QtWidgets
.QDialog(parent
)
1071 widget
.setWindowModality(Qt
.WindowModal
)
1072 widget
.setWindowTitle(title
)
1074 scroll
= QtWidgets
.QScrollArea()
1075 scroll
.setWidget(label
)
1077 layout
= qtutils
.hbox(defs
.margin
, defs
.spacing
, scroll
)
1078 widget
.setLayout(layout
)
1081 widget
, N_('Close'), widget
.accept
, Qt
.Key_Question
, Qt
.Key_Enter
, Qt
.Key_Return
1087 class VimTextBrowser(VimTextEdit
):
1088 """Text viewer with line number annotations"""
1090 def __init__(self
, context
, parent
=None, readonly
=True):
1091 VimTextEdit
.__init
__(self
, context
, parent
=parent
, readonly
=readonly
)
1092 self
.numbers
= LineNumbers(self
)
1094 def resizeEvent(self
, event
):
1095 super(VimTextBrowser
, self
).resizeEvent(event
)
1096 self
.numbers
.refresh_size()
1099 class TextDecorator(QtWidgets
.QWidget
):
1100 """Common functionality for providing line numbers in text widgets"""
1102 def __init__(self
, parent
):
1103 QtWidgets
.QWidget
.__init
__(self
, parent
)
1104 self
.editor
= parent
1106 parent
.blockCountChanged
.connect(lambda x
: self
._refresh
_viewport
())
1107 parent
.cursorPositionChanged
.connect(self
.refresh
)
1108 parent
.updateRequest
.connect(self
._refresh
_rect
)
1111 """Refresh the numbers display"""
1112 rect
= self
.editor
.viewport().rect()
1113 self
._refresh
_rect
(rect
, 0)
1115 def _refresh_rect(self
, rect
, dy
):
1119 self
.update(0, rect
.y(), self
.width(), rect
.height())
1121 if rect
.contains(self
.editor
.viewport().rect()):
1122 self
._refresh
_viewport
()
1124 def _refresh_viewport(self
):
1125 self
.editor
.setViewportMargins(self
.width_hint(), 0, 0, 0)
1127 def refresh_size(self
):
1128 rect
= self
.editor
.contentsRect()
1129 geom
= QtCore
.QRect(rect
.left(), rect
.top(), self
.width_hint(), rect
.height())
1130 self
.setGeometry(geom
)
1133 return QtCore
.QSize(self
.width_hint(), 0)
1136 class LineNumbers(TextDecorator
):
1137 """Provide line numbers for QPlainTextEdit widgets"""
1139 def __init__(self
, parent
):
1140 TextDecorator
.__init
__(self
, parent
)
1141 self
.highlight_line
= -1
1143 def width_hint(self
):
1144 document
= self
.editor
.document()
1145 digits
= int(math
.log(max(1, document
.blockCount()), 10)) + 2
1146 return defs
.large_margin
+ self
.fontMetrics().width('0') * digits
1148 def set_highlighted(self
, line_number
):
1149 """Set the line to highlight"""
1150 self
.highlight_line
= line_number
1152 def paintEvent(self
, event
):
1153 """Paint the line number"""
1154 QPalette
= QtGui
.QPalette
1155 painter
= QtGui
.QPainter(self
)
1156 editor
= self
.editor
1157 palette
= editor
.palette()
1159 painter
.fillRect(event
.rect(), palette
.color(QPalette
.Base
))
1161 content_offset
= editor
.contentOffset()
1162 block
= editor
.firstVisibleBlock()
1163 width
= self
.width()
1164 event_rect_bottom
= event
.rect().bottom()
1166 highlight
= palette
.color(QPalette
.Highlight
)
1167 highlighted_text
= palette
.color(QPalette
.HighlightedText
)
1168 disabled
= palette
.color(QPalette
.Disabled
, QPalette
.Text
)
1170 while block
.isValid():
1171 block_geom
= editor
.blockBoundingGeometry(block
)
1172 block_top
= block_geom
.translated(content_offset
).top()
1173 if not block
.isVisible() or block_top
>= event_rect_bottom
:
1176 rect
= block_geom
.translated(content_offset
).toRect()
1177 block_number
= block
.blockNumber()
1178 if block_number
== self
.highlight_line
:
1179 painter
.fillRect(rect
.x(), rect
.y(), width
, rect
.height(), highlight
)
1180 painter
.setPen(highlighted_text
)
1182 painter
.setPen(disabled
)
1184 number
= '%s' % (block_number
+ 1)
1188 self
.width() - defs
.large_margin
,
1190 Qt
.AlignRight | Qt
.AlignVCenter
,
1193 block
= block
.next()