1 from __future__
import absolute_import
, division
, print_function
, unicode_literals
2 from functools
import partial
6 from qtpy
import QtCore
8 from qtpy
import QtWidgets
9 from qtpy
.QtCore
import Qt
10 from qtpy
.QtCore
import Signal
13 from ..editpatch
import edit_patch
14 from ..interaction
import Interaction
15 from ..models
import main
16 from ..models
import prefs
17 from ..qtutils
import get
18 from .. import actions
21 from .. import diffparse
22 from .. import gitcmds
23 from .. import gravatar
24 from .. import hotkeys
27 from .. import qtutils
28 from .text
import TextDecorator
29 from .text
import VimHintedPlainTextEdit
30 from .text
import TextSearchWidget
32 from . import patch
as patch_mod
33 from . import imageview
36 class DiffSyntaxHighlighter(QtGui
.QSyntaxHighlighter
):
37 """Implements the diff syntax highlighting"""
42 DIFF_FILE_HEADER_STATE
= 2
47 DIFF_FILE_HEADER_START_RGX
= re
.compile(r
'diff --git a/.* b/.*')
48 DIFF_HUNK_HEADER_RGX
= re
.compile(
49 r
'(?:@@ -[0-9,]+ \+[0-9,]+ @@)|(?:@@@ (?:-[0-9,]+ ){2}\+[0-9,]+ @@@)'
51 BAD_WHITESPACE_RGX
= re
.compile(r
'\s+$')
53 def __init__(self
, context
, doc
, whitespace
=True, is_commit
=False):
54 QtGui
.QSyntaxHighlighter
.__init
__(self
, doc
)
55 self
.whitespace
= whitespace
57 self
.is_commit
= is_commit
59 QPalette
= QtGui
.QPalette
62 disabled
= palette
.color(QPalette
.Disabled
, QPalette
.Text
)
63 header
= qtutils
.rgb_hex(disabled
)
65 dark
= palette
.color(QPalette
.Base
).lightnessF() < 0.5
67 self
.color_text
= qtutils
.rgb_triple(cfg
.color('text', '030303'))
68 self
.color_add
= qtutils
.rgb_triple(
69 cfg
.color('add', '77aa77' if dark
else 'd2ffe4')
71 self
.color_remove
= qtutils
.rgb_triple(
72 cfg
.color('remove', 'aa7777' if dark
else 'fee0e4')
74 self
.color_header
= qtutils
.rgb_triple(cfg
.color('header', header
))
76 self
.diff_header_fmt
= qtutils
.make_format(foreground
=self
.color_header
)
77 self
.bold_diff_header_fmt
= qtutils
.make_format(
78 foreground
=self
.color_header
, bold
=True
81 self
.diff_add_fmt
= qtutils
.make_format(
82 foreground
=self
.color_text
, background
=self
.color_add
84 self
.diff_remove_fmt
= qtutils
.make_format(
85 foreground
=self
.color_text
, background
=self
.color_remove
87 self
.bad_whitespace_fmt
= qtutils
.make_format(background
=Qt
.red
)
88 self
.setCurrentBlockState(self
.INITIAL_STATE
)
90 def set_enabled(self
, enabled
):
91 self
.enabled
= enabled
93 def highlightBlock(self
, text
):
94 """Highlight the current text block"""
95 if not self
.enabled
or not text
:
98 state
= self
.get_next_state(text
)
99 if state
== self
.DIFFSTAT_STATE
:
100 state
, formats
= self
.get_formats_for_diffstat(state
, text
)
101 elif state
== self
.DIFF_FILE_HEADER_STATE
:
102 state
, formats
= self
.get_formats_for_diff_header(state
, text
)
103 elif state
== self
.DIFF_STATE
:
104 state
, formats
= self
.get_formats_for_diff_text(state
, text
)
106 for start
, end
, fmt
in formats
:
107 self
.setFormat(start
, end
, fmt
)
109 self
.setCurrentBlockState(state
)
111 def get_next_state(self
, text
):
112 """Transition to the next state based on the input text"""
113 state
= self
.previousBlockState()
114 if state
== DiffSyntaxHighlighter
.INITIAL_STATE
:
115 if text
.startswith('Submodule '):
116 state
= DiffSyntaxHighlighter
.SUBMODULE_STATE
117 elif text
.startswith('diff --git '):
118 state
= DiffSyntaxHighlighter
.DIFFSTAT_STATE
120 state
= DiffSyntaxHighlighter
.DEFAULT_STATE
122 state
= DiffSyntaxHighlighter
.DIFFSTAT_STATE
126 def get_formats_for_diffstat(self
, state
, text
):
127 """Returns (state, [(start, end, fmt), ...]) for highlighting diffstat text"""
129 if self
.DIFF_FILE_HEADER_START_RGX
.match(text
):
130 state
= self
.DIFF_FILE_HEADER_STATE
132 fmt
= self
.diff_header_fmt
133 formats
.append((0, end
, fmt
))
134 elif self
.DIFF_HUNK_HEADER_RGX
.match(text
):
135 state
= self
.DIFF_STATE
137 fmt
= self
.bold_diff_header_fmt
138 formats
.append((0, end
, fmt
))
140 offset
= text
.index('|')
141 formats
.append((0, offset
, self
.bold_diff_header_fmt
))
142 formats
.append((offset
, len(text
) - offset
, self
.diff_header_fmt
))
144 formats
.append((0, len(text
), self
.diff_header_fmt
))
146 return state
, formats
148 def get_formats_for_diff_header(self
, state
, text
):
149 """Returns (state, [(start, end, fmt), ...]) for highlighting diff headers"""
151 if self
.DIFF_HUNK_HEADER_RGX
.match(text
):
152 state
= self
.DIFF_STATE
153 formats
.append((0, len(text
), self
.bold_diff_header_fmt
))
155 formats
.append((0, len(text
), self
.diff_header_fmt
))
157 return state
, formats
159 def get_formats_for_diff_text(self
, state
, text
):
160 """Return (state, [(start, end fmt), ...]) for highlighting diff text"""
163 if self
.DIFF_FILE_HEADER_START_RGX
.match(text
):
164 state
= self
.DIFF_FILE_HEADER_STATE
165 formats
.append((0, len(text
), self
.diff_header_fmt
))
167 elif self
.DIFF_HUNK_HEADER_RGX
.match(text
):
168 formats
.append((0, len(text
), self
.bold_diff_header_fmt
))
170 elif text
.startswith('-'):
172 state
= self
.END_STATE
174 formats
.append((0, len(text
), self
.diff_remove_fmt
))
176 elif text
.startswith('+'):
177 formats
.append((0, len(text
), self
.diff_add_fmt
))
179 match
= self
.BAD_WHITESPACE_RGX
.search(text
)
180 if match
is not None:
181 start
= match
.start()
182 formats
.append((start
, len(text
) - start
, self
.bad_whitespace_fmt
))
184 return state
, formats
187 # pylint: disable=too-many-ancestors
188 class DiffTextEdit(VimHintedPlainTextEdit
):
189 """A textedit for interacting with diff text"""
192 self
, context
, parent
, is_commit
=False, whitespace
=True, numbers
=False
194 VimHintedPlainTextEdit
.__init
__(self
, context
, '', parent
=parent
)
195 # Diff/patch syntax highlighter
196 self
.highlighter
= DiffSyntaxHighlighter(
197 context
, self
.document(), is_commit
=is_commit
, whitespace
=whitespace
200 self
.numbers
= DiffLineNumbers(context
, self
)
204 self
.scrollvalue
= None
206 self
.copy_diff_action
= qtutils
.add_action(
212 self
.copy_diff_action
.setIcon(icons
.copy())
213 self
.copy_diff_action
.setEnabled(False)
214 self
.menu_actions
.append(self
.copy_diff_action
)
216 # pylint: disable=no-member
217 self
.cursorPositionChanged
.connect(self
._cursor
_changed
)
218 self
.selectionChanged
.connect(self
._selection
_changed
)
220 def setFont(self
, font
):
221 """Override setFont() so that we can use a custom "block" cursor"""
222 super(DiffTextEdit
, self
).setFont(font
)
223 if prefs
.block_cursor(self
.context
):
224 metrics
= QtGui
.QFontMetrics(font
)
225 width
= metrics
.width('M')
226 self
.setCursorWidth(width
)
228 def _cursor_changed(self
):
229 """Update the line number display when the cursor changes"""
230 line_number
= max(0, self
.textCursor().blockNumber())
231 if self
.numbers
is not None:
232 self
.numbers
.set_highlighted(line_number
)
234 def _selection_changed(self
):
235 """Respond to selection changes"""
236 selected
= bool(self
.selected_text())
237 self
.copy_diff_action
.setEnabled(selected
)
239 def resizeEvent(self
, event
):
240 super(DiffTextEdit
, self
).resizeEvent(event
)
242 self
.numbers
.refresh_size()
244 def save_scrollbar(self
):
245 """Save the scrollbar value, but only on the first call"""
246 if self
.scrollvalue
is None:
247 scrollbar
= self
.verticalScrollBar()
249 scrollvalue
= get(scrollbar
)
252 self
.scrollvalue
= scrollvalue
254 def restore_scrollbar(self
):
255 """Restore the scrollbar and clear state"""
256 scrollbar
= self
.verticalScrollBar()
257 scrollvalue
= self
.scrollvalue
258 if scrollbar
and scrollvalue
is not None:
259 scrollbar
.setValue(scrollvalue
)
260 self
.scrollvalue
= None
262 def set_loading_message(self
):
263 """Add a pending loading message in the diff view"""
264 self
.hint
.set_value('+++ ' + N_('Loading...'))
267 def set_diff(self
, diff
):
268 """Set the diff text, but save the scrollbar"""
269 diff
= diff
.rstrip('\n') # diffs include two empty newlines
270 self
.save_scrollbar()
272 self
.hint
.set_value('')
274 self
.numbers
.set_diff(diff
)
277 self
.restore_scrollbar()
279 def selected_diff_stripped(self
):
280 """Return the selected diff stripped of any diff characters"""
281 sep
, selection
= self
.selected_text_lines()
282 return sep
.join(_strip_diff(line
) for line
in selection
)
285 """Copy the selected diff text stripped of any diff prefix characters"""
286 text
= self
.selected_diff_stripped()
287 qtutils
.set_clipboard(text
)
289 def selected_lines(self
):
290 """Return selected lines"""
291 cursor
= self
.textCursor()
292 selection_start
= cursor
.selectionStart()
293 selection_end
= max(selection_start
, cursor
.selectionEnd() - 1)
300 for line_idx
, line
in enumerate(get(self
, default
='').splitlines()):
301 line_end
= line_start
+ len(line
)
302 if line_start
<= selection_start
<= line_end
:
303 first_line_idx
= line_idx
304 if line_start
<= selection_end
<= line_end
:
305 last_line_idx
= line_idx
307 line_start
= line_end
+ 1
309 if first_line_idx
== -1:
310 first_line_idx
= line_idx
312 if last_line_idx
== -1:
313 last_line_idx
= line_idx
315 return first_line_idx
, last_line_idx
317 def selected_text_lines(self
):
318 """Return selected lines and the CRLF / LF separator"""
319 first_line_idx
, last_line_idx
= self
.selected_lines()
320 text
= get(self
, default
='')
323 for line_idx
, line
in enumerate(text
.split(sep
)):
324 if first_line_idx
<= line_idx
<= last_line_idx
:
330 """Return either CRLF or LF based on the content"""
338 def _strip_diff(value
):
339 """Remove +/-/<space> from a selection"""
340 if value
.startswith(('+', '-', ' ')):
345 class DiffLineNumbers(TextDecorator
):
346 def __init__(self
, context
, parent
):
347 TextDecorator
.__init
__(self
, parent
)
348 self
.highlight_line
= -1
350 self
.parser
= diffparse
.DiffLines()
351 self
.formatter
= diffparse
.FormatDigits()
353 self
.setFont(qtutils
.diff_font(context
))
354 self
._char
_width
= self
.fontMetrics().width('0')
356 QPalette
= QtGui
.QPalette
357 self
._palette
= palette
= self
.palette()
358 self
._base
= palette
.color(QtGui
.QPalette
.Base
)
359 self
._highlight
= palette
.color(QPalette
.Highlight
)
360 self
._highlight
.setAlphaF(0.3)
361 self
._highlight
_text
= palette
.color(QPalette
.HighlightedText
)
362 self
._window
= palette
.color(QPalette
.Window
)
363 self
._disabled
= palette
.color(QPalette
.Disabled
, QPalette
.Text
)
365 def set_diff(self
, diff
):
366 self
.lines
= self
.parser
.parse(diff
)
367 self
.formatter
.set_digits(self
.parser
.digits())
369 def width_hint(self
):
370 if not self
.isVisible():
376 extra
= 3 # one space in-between, one space after
379 extra
= 2 # one space in-between, one space after
381 digits
= parser
.digits() * columns
383 return defs
.margin
+ (self
._char
_width
* (digits
+ extra
))
385 def set_highlighted(self
, line_number
):
386 """Set the line to highlight"""
387 self
.highlight_line
= line_number
389 def current_line(self
):
391 if lines
and self
.highlight_line
>= 0:
392 # Find the next valid line
393 for i
in range(self
.highlight_line
, len(lines
)):
394 # take the "new" line number: last value in tuple
395 line_number
= lines
[i
][-1]
399 # Find the previous valid line
400 for i
in range(self
.highlight_line
- 1, -1, -1):
401 # take the "new" line number: last value in tuple
403 line_number
= lines
[i
][-1]
408 def paintEvent(self
, event
):
409 """Paint the line number"""
413 painter
= QtGui
.QPainter(self
)
414 painter
.fillRect(event
.rect(), self
._base
)
417 content_offset
= editor
.contentOffset()
418 block
= editor
.firstVisibleBlock()
420 text_width
= width
- (defs
.margin
* 2)
421 text_flags
= Qt
.AlignRight | Qt
.AlignVCenter
422 event_rect_bottom
= event
.rect().bottom()
424 highlight_line
= self
.highlight_line
425 highlight
= self
._highlight
426 highlight_text
= self
._highlight
_text
427 disabled
= self
._disabled
431 num_lines
= len(lines
)
433 while block
.isValid():
434 block_number
= block
.blockNumber()
435 if block_number
>= num_lines
:
437 block_geom
= editor
.blockBoundingGeometry(block
)
438 rect
= block_geom
.translated(content_offset
).toRect()
439 if not block
.isVisible() or rect
.top() >= event_rect_bottom
:
442 if block_number
== highlight_line
:
443 painter
.fillRect(rect
.x(), rect
.y(), width
, rect
.height(), highlight
)
444 painter
.setPen(highlight_text
)
446 painter
.setPen(disabled
)
448 line
= lines
[block_number
]
451 text
= fmt
.value(a
, b
)
453 old
, base
, new
= line
454 text
= fmt
.merge_value(old
, base
, new
)
468 class Viewer(QtWidgets
.QFrame
):
469 """Text and image diff viewers"""
474 def __init__(self
, context
, parent
=None):
475 super(Viewer
, self
).__init
__(parent
)
477 self
.context
= context
478 self
.model
= model
= context
.model
481 self
.options
= options
= Options(self
)
482 self
.text
= DiffEditor(context
, options
, self
)
483 self
.image
= imageview
.ImageView(parent
=self
)
484 self
.image
.setFocusPolicy(Qt
.NoFocus
)
485 self
.search_widget
= TextSearchWidget(self
.text
, self
)
486 self
.search_widget
.hide()
488 stack
= self
.stack
= QtWidgets
.QStackedWidget(self
)
489 stack
.addWidget(self
.text
)
490 stack
.addWidget(self
.image
)
492 self
.main_layout
= qtutils
.vbox(
498 self
.setLayout(self
.main_layout
)
501 model
.images_changed
.connect(self
.set_images
, type=Qt
.QueuedConnection
)
503 # Observe the diff type
504 model
.diff_type_changed
.connect(self
.set_diff_type
, type=Qt
.QueuedConnection
)
506 # Observe the file type
507 model
.file_type_changed
.connect(self
.set_file_type
, type=Qt
.QueuedConnection
)
509 # Observe the image mode combo box
510 options
.image_mode
.currentIndexChanged
.connect(lambda _
: self
.render())
511 options
.zoom_mode
.currentIndexChanged
.connect(lambda _
: self
.render())
513 self
.setFocusProxy(self
.text
)
515 self
.search_action
= qtutils
.add_action(
517 N_('Search in Diff'),
518 self
.show_search_diff
,
522 def show_search_diff(self
):
523 """Show a dialog for searching diffs"""
524 # The diff search is only active in text mode.
525 if self
.stack
.currentIndex() != self
.INDEX_TEXT
:
527 if not self
.search_widget
.isVisible():
528 self
.search_widget
.show()
529 self
.search_widget
.setFocus(True)
531 def export_state(self
, state
):
532 state
['show_diff_line_numbers'] = self
.options
.show_line_numbers
.isChecked()
533 state
['image_diff_mode'] = self
.options
.image_mode
.currentIndex()
534 state
['image_zoom_mode'] = self
.options
.zoom_mode
.currentIndex()
535 state
['word_wrap'] = self
.options
.enable_word_wrapping
.isChecked()
538 def apply_state(self
, state
):
539 diff_numbers
= bool(state
.get('show_diff_line_numbers', False))
540 self
.set_line_numbers(diff_numbers
, update
=True)
542 image_mode
= utils
.asint(state
.get('image_diff_mode', 0))
543 self
.options
.image_mode
.set_index(image_mode
)
545 zoom_mode
= utils
.asint(state
.get('image_zoom_mode', 0))
546 self
.options
.zoom_mode
.set_index(zoom_mode
)
548 word_wrap
= bool(state
.get('word_wrap', True))
549 self
.set_word_wrapping(word_wrap
, update
=True)
552 def set_diff_type(self
, diff_type
):
553 """Manage the image and text diff views when selection changes"""
554 # The "diff type" is whether the diff viewer is displaying an image.
555 self
.options
.set_diff_type(diff_type
)
556 if diff_type
== main
.Types
.IMAGE
:
557 self
.stack
.setCurrentWidget(self
.image
)
558 self
.search_widget
.hide()
561 self
.stack
.setCurrentWidget(self
.text
)
563 def set_file_type(self
, file_type
):
564 """Manage the diff options when the file type changes"""
565 # The "file type" is whether the file itself is an image.
566 self
.options
.set_file_type(file_type
)
568 def update_options(self
):
569 """Emit a signal indicating that options have changed"""
570 self
.text
.update_options()
572 def set_line_numbers(self
, enabled
, update
=False):
573 """Enable/disable line numbers in the text widget"""
574 self
.text
.set_line_numbers(enabled
, update
=update
)
576 def set_word_wrapping(self
, enabled
, update
=False):
577 """Enable/disable word wrapping in the text widget"""
578 self
.text
.set_word_wrapping(enabled
, update
=update
)
581 self
.image
.pixmap
= QtGui
.QPixmap()
585 for (image
, unlink
) in self
.images
:
586 if unlink
and core
.exists(image
):
590 def set_images(self
, images
):
597 # In order to comp, we first have to load all the images
598 all_pixmaps
= [QtGui
.QPixmap(image
[0]) for image
in images
]
599 pixmaps
= [pixmap
for pixmap
in all_pixmaps
if not pixmap
.isNull()]
604 self
.pixmaps
= pixmaps
612 mode
= self
.options
.image_mode
.currentIndex()
613 if mode
== self
.options
.SIDE_BY_SIDE
:
614 image
= self
.render_side_by_side()
615 elif mode
== self
.options
.DIFF
:
616 image
= self
.render_diff()
617 elif mode
== self
.options
.XOR
:
618 image
= self
.render_xor()
619 elif mode
== self
.options
.PIXEL_XOR
:
620 image
= self
.render_pixel_xor()
622 image
= self
.render_side_by_side()
624 image
= QtGui
.QPixmap()
625 self
.image
.pixmap
= image
628 zoom_mode
= self
.options
.zoom_mode
.currentIndex()
629 zoom_factor
= self
.options
.zoom_factors
[zoom_mode
][1]
630 if zoom_factor
> 0.0:
631 self
.image
.resetTransform()
632 self
.image
.scale(zoom_factor
, zoom_factor
)
633 poly
= self
.image
.mapToScene(self
.image
.viewport().rect())
634 self
.image
.last_scene_roi
= poly
.boundingRect()
636 def render_side_by_side(self
):
637 # Side-by-side lineup comp
638 pixmaps
= self
.pixmaps
639 width
= sum(pixmap
.width() for pixmap
in pixmaps
)
640 height
= max(pixmap
.height() for pixmap
in pixmaps
)
641 image
= create_image(width
, height
)
644 painter
= create_painter(image
)
646 for pixmap
in pixmaps
:
647 painter
.drawPixmap(x
, 0, pixmap
)
653 def render_comp(self
, comp_mode
):
654 # Get the max size to use as the render canvas
655 pixmaps
= self
.pixmaps
656 if len(pixmaps
) == 1:
659 width
= max(pixmap
.width() for pixmap
in pixmaps
)
660 height
= max(pixmap
.height() for pixmap
in pixmaps
)
661 image
= create_image(width
, height
)
663 painter
= create_painter(image
)
664 for pixmap
in (pixmaps
[0], pixmaps
[-1]):
665 x
= (width
- pixmap
.width()) // 2
666 y
= (height
- pixmap
.height()) // 2
667 painter
.drawPixmap(x
, y
, pixmap
)
668 painter
.setCompositionMode(comp_mode
)
673 def render_diff(self
):
674 comp_mode
= QtGui
.QPainter
.CompositionMode_Difference
675 return self
.render_comp(comp_mode
)
677 def render_xor(self
):
678 comp_mode
= QtGui
.QPainter
.CompositionMode_Xor
679 return self
.render_comp(comp_mode
)
681 def render_pixel_xor(self
):
682 comp_mode
= QtGui
.QPainter
.RasterOp_SourceXorDestination
683 return self
.render_comp(comp_mode
)
686 def create_image(width
, height
):
687 size
= QtCore
.QSize(width
, height
)
688 image
= QtGui
.QImage(size
, QtGui
.QImage
.Format_ARGB32_Premultiplied
)
689 image
.fill(Qt
.transparent
)
693 def create_painter(image
):
694 painter
= QtGui
.QPainter(image
)
695 painter
.fillRect(image
.rect(), Qt
.transparent
)
699 class Options(QtWidgets
.QWidget
):
700 """Provide the options widget used by the editor
702 Actions are registered on the parent widget.
706 # mode combobox indexes
712 def __init__(self
, parent
):
713 super(Options
, self
).__init
__(parent
)
716 self
.ignore_space_at_eol
= self
.add_option(
717 N_('Ignore changes in whitespace at EOL')
720 self
.ignore_space_change
= self
.add_option(
721 N_('Ignore changes in amount of whitespace')
724 self
.ignore_all_space
= self
.add_option(N_('Ignore all whitespace'))
726 self
.function_context
= self
.add_option(
727 N_('Show whole surrounding functions of changes')
730 self
.show_line_numbers
= qtutils
.add_action_bool(
731 self
, N_('Show line numbers'), self
.set_line_numbers
, True
733 self
.enable_word_wrapping
= qtutils
.add_action_bool(
734 self
, N_('Enable word wrapping'), self
.set_word_wrapping
, True
737 self
.options
= qtutils
.create_action_button(
738 tooltip
=N_('Diff Options'), icon
=icons
.configure()
741 self
.toggle_image_diff
= qtutils
.create_action_button(
742 tooltip
=N_('Toggle image diff'), icon
=icons
.visualize()
744 self
.toggle_image_diff
.hide()
746 self
.image_mode
= qtutils
.combo(
747 [N_('Side by side'), N_('Diff'), N_('XOR'), N_('Pixel XOR')]
750 self
.zoom_factors
= (
751 (N_('Zoom to Fit'), 0.0),
759 zoom_modes
= [factor
[0] for factor
in self
.zoom_factors
]
760 self
.zoom_mode
= qtutils
.combo(zoom_modes
, parent
=self
)
762 self
.menu
= menu
= qtutils
.create_menu(N_('Diff Options'), self
.options
)
763 self
.options
.setMenu(menu
)
764 menu
.addAction(self
.ignore_space_at_eol
)
765 menu
.addAction(self
.ignore_space_change
)
766 menu
.addAction(self
.ignore_all_space
)
768 menu
.addAction(self
.function_context
)
769 menu
.addAction(self
.show_line_numbers
)
771 menu
.addAction(self
.enable_word_wrapping
)
774 layout
= qtutils
.hbox(
780 self
.toggle_image_diff
,
782 self
.setLayout(layout
)
785 self
.image_mode
.setFocusPolicy(Qt
.NoFocus
)
786 self
.zoom_mode
.setFocusPolicy(Qt
.NoFocus
)
787 self
.options
.setFocusPolicy(Qt
.NoFocus
)
788 self
.toggle_image_diff
.setFocusPolicy(Qt
.NoFocus
)
789 self
.setFocusPolicy(Qt
.NoFocus
)
791 def set_file_type(self
, file_type
):
792 """Set whether we are viewing an image file type"""
793 is_image
= file_type
== main
.Types
.IMAGE
794 self
.toggle_image_diff
.setVisible(is_image
)
796 def set_diff_type(self
, diff_type
):
797 """Toggle between image and text diffs"""
798 is_text
= diff_type
== main
.Types
.TEXT
799 is_image
= diff_type
== main
.Types
.IMAGE
800 self
.options
.setVisible(is_text
)
801 self
.image_mode
.setVisible(is_image
)
802 self
.zoom_mode
.setVisible(is_image
)
804 self
.toggle_image_diff
.setIcon(icons
.diff())
806 self
.toggle_image_diff
.setIcon(icons
.visualize())
808 def add_option(self
, title
):
809 """Add a diff option which calls update_options() on change"""
810 action
= qtutils
.add_action(self
, title
, self
.update_options
)
811 action
.setCheckable(True)
814 def update_options(self
):
815 """Update diff options in response to UI events"""
816 space_at_eol
= get(self
.ignore_space_at_eol
)
817 space_change
= get(self
.ignore_space_change
)
818 all_space
= get(self
.ignore_all_space
)
819 function_context
= get(self
.function_context
)
820 gitcmds
.update_diff_overrides(
821 space_at_eol
, space_change
, all_space
, function_context
823 self
.widget
.update_options()
825 def set_line_numbers(self
, value
):
826 """Enable / disable line numbers"""
827 self
.widget
.set_line_numbers(value
, update
=False)
829 def set_word_wrapping(self
, value
):
830 """Respond to Qt action callbacks"""
831 self
.widget
.set_word_wrapping(value
, update
=False)
833 def hide_advanced_options(self
):
834 """Hide advanced options that are not applicable to the DiffWidget"""
835 self
.show_line_numbers
.setVisible(False)
836 self
.ignore_space_at_eol
.setVisible(False)
837 self
.ignore_space_change
.setVisible(False)
838 self
.ignore_all_space
.setVisible(False)
839 self
.function_context
.setVisible(False)
842 # pylint: disable=too-many-ancestors
843 class DiffEditor(DiffTextEdit
):
847 options_changed
= Signal()
849 def __init__(self
, context
, options
, parent
):
850 DiffTextEdit
.__init
__(self
, context
, parent
, numbers
=True)
851 self
.context
= context
852 self
.model
= model
= context
.model
853 self
.selection_model
= selection_model
= context
.selection
855 # "Diff Options" tool menu
856 self
.options
= options
858 self
.action_apply_selection
= qtutils
.add_action(
861 self
.apply_selection
,
863 hotkeys
.STAGE_DIFF_ALT
,
866 self
.action_revert_selection
= qtutils
.add_action(
867 self
, 'Revert', self
.revert_selection
, hotkeys
.REVERT
869 self
.action_revert_selection
.setIcon(icons
.undo())
871 self
.action_edit_and_apply_selection
= qtutils
.add_action(
874 partial(self
.apply_selection
, edit
=True),
875 hotkeys
.EDIT_AND_STAGE_DIFF
,
878 self
.action_edit_and_revert_selection
= qtutils
.add_action(
881 partial(self
.revert_selection
, edit
=True),
882 hotkeys
.EDIT_AND_REVERT
,
884 self
.action_edit_and_revert_selection
.setIcon(icons
.undo())
885 self
.launch_editor
= actions
.launch_editor_at_line(
886 context
, self
, hotkeys
.EDIT_SHORT
, *hotkeys
.ACCEPT
888 self
.launch_difftool
= actions
.launch_difftool(context
, self
)
889 self
.stage_or_unstage
= actions
.stage_or_unstage(context
, self
)
891 # Emit up/down signals so that they can be routed by the main widget
892 self
.move_up
= actions
.move_up(self
)
893 self
.move_down
= actions
.move_down(self
)
895 model
.diff_text_updated
.connect(self
.set_diff
, type=Qt
.QueuedConnection
)
897 selection_model
.selection_changed
.connect(
898 self
.refresh
, type=Qt
.QueuedConnection
900 # Update the selection model when the cursor changes
901 self
.cursorPositionChanged
.connect(self
._update
_line
_number
)
903 qtutils
.connect_button(options
.toggle_image_diff
, self
.toggle_diff_type
)
905 def toggle_diff_type(self
):
906 cmds
.do(cmds
.ToggleDiffType
, self
.context
)
910 s
= self
.selection_model
.selection()
912 if model
.is_partially_stageable():
913 item
= s
.modified
[0] if s
.modified
else None
914 if item
in model
.submodules
:
916 elif item
not in model
.unstaged_deleted
:
918 self
.action_revert_selection
.setEnabled(enabled
)
920 def set_line_numbers(self
, enabled
, update
=False):
921 """Enable/disable the diff line number display"""
922 self
.numbers
.setVisible(enabled
)
924 with qtutils
.BlockSignals(self
.options
.show_line_numbers
):
925 self
.options
.show_line_numbers
.setChecked(enabled
)
926 # Refresh the display. Not doing this results in the display not
927 # correctly displaying the line numbers widget until the text scrolls.
928 self
.set_value(self
.value())
930 def update_options(self
):
931 self
.options_changed
.emit()
933 def create_context_menu(self
, event_pos
):
934 """Override create_context_menu() to display a completely custom menu"""
935 menu
= super(DiffEditor
, self
).create_context_menu(event_pos
)
936 context
= self
.context
938 s
= self
.selection_model
.selection()
939 filename
= self
.selection_model
.filename()
941 # These menu actions will be inserted at the start of the widget.
942 current_actions
= menu
.actions()
944 add_action
= menu_actions
.append
946 if model
.is_stageable() or model
.is_unstageable():
947 if model
.is_stageable():
948 self
.stage_or_unstage
.setText(N_('Stage'))
949 self
.stage_or_unstage
.setIcon(icons
.add())
951 self
.stage_or_unstage
.setText(N_('Unstage'))
952 self
.stage_or_unstage
.setIcon(icons
.remove())
953 add_action(self
.stage_or_unstage
)
955 if model
.is_partially_stageable():
956 item
= s
.modified
[0] if s
.modified
else None
957 if item
in model
.submodules
:
958 path
= core
.abspath(item
)
959 action
= qtutils
.add_action_with_icon(
963 cmds
.run(cmds
.Stage
, context
, s
.modified
),
964 hotkeys
.STAGE_SELECTION
,
968 action
= qtutils
.add_action_with_icon(
971 N_('Launch git-cola'),
972 cmds
.run(cmds
.OpenRepo
, context
, path
),
975 elif item
not in model
.unstaged_deleted
:
976 if self
.has_selection():
977 apply_text
= N_('Stage Selected Lines')
978 edit_and_apply_text
= N_('Edit Selected Lines to Stage...')
979 revert_text
= N_('Revert Selected Lines...')
980 edit_and_revert_text
= N_('Edit Selected Lines to Revert...')
982 apply_text
= N_('Stage Diff Hunk')
983 edit_and_apply_text
= N_('Edit Diff Hunk to Stage...')
984 revert_text
= N_('Revert Diff Hunk...')
985 edit_and_revert_text
= N_('Edit Diff Hunk to Revert...')
987 self
.action_apply_selection
.setText(apply_text
)
988 self
.action_apply_selection
.setIcon(icons
.add())
989 add_action(self
.action_apply_selection
)
991 self
.action_edit_and_apply_selection
.setText(edit_and_apply_text
)
992 self
.action_edit_and_apply_selection
.setIcon(icons
.add())
993 add_action(self
.action_edit_and_apply_selection
)
995 self
.action_revert_selection
.setText(revert_text
)
996 add_action(self
.action_revert_selection
)
998 self
.action_edit_and_revert_selection
.setText(edit_and_revert_text
)
999 add_action(self
.action_edit_and_revert_selection
)
1001 if s
.staged
and model
.is_unstageable():
1003 if item
in model
.submodules
:
1004 path
= core
.abspath(item
)
1005 action
= qtutils
.add_action_with_icon(
1008 cmds
.Unstage
.name(),
1009 cmds
.run(cmds
.Unstage
, context
, s
.staged
),
1010 hotkeys
.STAGE_SELECTION
,
1014 qtutils
.add_action_with_icon(
1017 N_('Launch git-cola'),
1018 cmds
.run(cmds
.OpenRepo
, context
, path
),
1022 elif item
not in model
.staged_deleted
:
1023 if self
.has_selection():
1024 apply_text
= N_('Unstage Selected Lines')
1025 edit_and_apply_text
= N_('Edit Selected Lines to Unstage...')
1027 apply_text
= N_('Unstage Diff Hunk')
1028 edit_and_apply_text
= N_('Edit Diff Hunk to Unstage...')
1030 self
.action_apply_selection
.setText(apply_text
)
1031 self
.action_apply_selection
.setIcon(icons
.remove())
1032 add_action(self
.action_apply_selection
)
1034 self
.action_edit_and_apply_selection
.setText(edit_and_apply_text
)
1035 self
.action_edit_and_apply_selection
.setIcon(icons
.remove())
1036 add_action(self
.action_edit_and_apply_selection
)
1038 if model
.is_stageable() or model
.is_unstageable():
1039 # Do not show the "edit" action when the file does not exist.
1040 # Untracked files exist by definition.
1041 if filename
and core
.exists(filename
):
1042 add_action(qtutils
.menu_separator(menu
))
1043 add_action(self
.launch_editor
)
1045 # Removed files can still be diffed.
1046 add_action(self
.launch_difftool
)
1048 add_action(qtutils
.menu_separator(menu
))
1049 _add_patch_actions(self
, self
.context
, menu
)
1051 # Add the Previous/Next File actions, which improves discoverability
1052 # of their associated shortcuts
1053 add_action(qtutils
.menu_separator(menu
))
1054 add_action(self
.move_up
)
1055 add_action(self
.move_down
)
1056 add_action(qtutils
.menu_separator(menu
))
1059 first_action
= current_actions
[0]
1062 menu
.insertActions(first_action
, menu_actions
)
1066 def mousePressEvent(self
, event
):
1067 if event
.button() == Qt
.RightButton
:
1068 # Intercept right-click to move the cursor to the current position.
1069 # setTextCursor() clears the selection so this is only done when
1070 # nothing is selected.
1071 if not self
.has_selection():
1072 cursor
= self
.cursorForPosition(event
.pos())
1073 self
.setTextCursor(cursor
)
1075 return super(DiffEditor
, self
).mousePressEvent(event
)
1077 def setPlainText(self
, text
):
1078 """setPlainText(str) while retaining scrollbar positions"""
1081 highlight
= mode
not in (
1084 model
.mode_untracked
,
1086 self
.highlighter
.set_enabled(highlight
)
1088 scrollbar
= self
.verticalScrollBar()
1090 scrollvalue
= get(scrollbar
)
1097 DiffTextEdit
.setPlainText(self
, text
)
1099 if scrollbar
and scrollvalue
is not None:
1100 scrollbar
.setValue(scrollvalue
)
1102 def apply_selection(self
, *, edit
=False):
1104 s
= self
.selection_model
.single_selection()
1105 if model
.is_partially_stageable() and (s
.modified
or s
.untracked
):
1106 self
.process_diff_selection(edit
=edit
)
1107 elif model
.is_unstageable():
1108 self
.process_diff_selection(reverse
=True, edit
=edit
)
1110 def revert_selection(self
, *, edit
=False):
1111 """Destructively revert selected lines or hunk from a worktree file."""
1114 if self
.has_selection():
1115 title
= N_('Revert Selected Lines?')
1116 ok_text
= N_('Revert Selected Lines')
1118 title
= N_('Revert Diff Hunk?')
1119 ok_text
= N_('Revert Diff Hunk')
1121 if not Interaction
.confirm(
1124 'This operation drops uncommitted changes.\n'
1125 'These changes cannot be recovered.'
1127 N_('Revert the uncommitted changes?'),
1133 self
.process_diff_selection(reverse
=True, apply_to_worktree
=True, edit
=edit
)
1135 def extract_patch(self
, reverse
=False):
1136 first_line_idx
, last_line_idx
= self
.selected_lines()
1137 patch
= diffparse
.Patch
.parse(self
.model
.filename
, self
.model
.diff_text
)
1138 if self
.has_selection():
1139 return patch
.extract_subset(first_line_idx
, last_line_idx
, reverse
=reverse
)
1140 return patch
.extract_hunk(first_line_idx
, reverse
=reverse
)
1142 def patch_encoding(self
):
1143 if isinstance(self
.model
.diff_text
, core
.UStr
):
1144 # original encoding must prevail
1145 return self
.model
.diff_text
.encoding
1146 return self
.context
.cfg
.file_encoding(self
.model
.filename
)
1148 def process_diff_selection(
1149 self
, reverse
=False, apply_to_worktree
=False, edit
=False
1151 """Implement un/staging of the selected line(s) or hunk."""
1152 if self
.selection_model
.is_empty():
1154 patch
= self
.extract_patch(reverse
)
1155 if not patch
.has_changes():
1157 patch_encoding
= self
.patch_encoding()
1165 apply_to_worktree
=apply_to_worktree
,
1167 if not patch
.has_changes():
1178 def _update_line_number(self
):
1179 """Update the selection model when the cursor changes"""
1180 self
.selection_model
.line_number
= self
.numbers
.current_line()
1183 def _add_patch_actions(widget
, context
, menu
):
1184 """Add actions for manipulating patch files"""
1185 patches_menu
= menu
.addMenu(N_('Patches'))
1186 patches_menu
.setIcon(icons
.diff())
1187 export_action
= qtutils
.add_action(
1190 lambda: _export_patch(widget
, context
),
1192 export_action
.setIcon(icons
.save())
1193 patches_menu
.addAction(export_action
)
1195 # Build the "Append Patch" menu dynamically.
1196 append_menu
= patches_menu
.addMenu(N_('Append Patch'))
1197 append_menu
.setIcon(icons
.add())
1198 append_menu
.aboutToShow
.connect(
1199 lambda: _build_patch_append_menu(widget
, context
, append_menu
)
1203 def _build_patch_append_menu(widget
, context
, menu
):
1204 """Build the "Append Patch" submenu"""
1205 # Build the menu when first displayed only. This initial check avoids
1206 # re-populating the menu with duplicate actions.
1207 menu_actions
= menu
.actions()
1211 choose_patch_action
= qtutils
.add_action(
1213 N_('Choose Patch...'),
1214 lambda: _export_patch(widget
, context
, append
=True),
1216 choose_patch_action
.setIcon(icons
.diff())
1217 menu
.addAction(choose_patch_action
)
1220 path
= prefs
.patches_directory(context
)
1221 patches
= patch_mod
.get_patches_from_dir(path
)
1222 for patch
in patches
:
1223 relpath
= os
.path
.relpath(patch
, start
=path
)
1224 sub_menu
= _add_patch_subdirs(menu
, subdir_menus
, relpath
)
1225 patch_basename
= os
.path
.basename(relpath
)
1226 append_action
= qtutils
.add_action(
1229 lambda patch_file
=patch
: _append_patch(widget
, patch_file
),
1231 append_action
.setIcon(icons
.save())
1232 sub_menu
.addAction(append_action
)
1235 def _add_patch_subdirs(menu
, subdir_menus
, relpath
):
1236 """Build menu leading up to the patch"""
1237 # If the path contains no directory separators then add it to the
1239 if os
.sep
not in relpath
:
1242 # Loop over each directory component and build a menu if it doesn't already exist.
1244 for dirname
in os
.path
.dirname(relpath
).split(os
.sep
):
1245 components
.append(dirname
)
1246 current_dir
= os
.sep
.join(components
)
1248 menu
= subdir_menus
[current_dir
]
1250 menu
= subdir_menus
[current_dir
] = menu
.addMenu(dirname
)
1251 menu
.setIcon(icons
.folder())
1256 def _export_patch(diff_editor
, context
, append
=False):
1257 """Export the selected diff to a patch file"""
1258 if diff_editor
.selection_model
.is_empty():
1260 patch
= diff_editor
.extract_patch(reverse
=False)
1261 if not patch
.has_changes():
1263 directory
= prefs
.patches_directory(context
)
1265 filename
= qtutils
.existing_file(directory
, title
=N_('Append Patch...'))
1267 default_filename
= os
.path
.join(directory
, 'diff.patch')
1268 filename
= qtutils
.save_as(default_filename
)
1271 _write_patch_to_file(diff_editor
, patch
, filename
, append
=append
)
1274 def _append_patch(diff_editor
, filename
):
1275 """Append diffs to the specified patch file"""
1276 if diff_editor
.selection_model
.is_empty():
1278 patch
= diff_editor
.extract_patch(reverse
=False)
1279 if not patch
.has_changes():
1281 _write_patch_to_file(diff_editor
, patch
, filename
, append
=True)
1284 def _write_patch_to_file(diff_editor
, patch
, filename
, append
=False):
1285 """Write diffs from the Diff Editor to the specified patch file"""
1286 encoding
= diff_editor
.patch_encoding()
1287 content
= patch
.as_text()
1289 core
.write(filename
, content
, encoding
=encoding
, append
=append
)
1290 except (IOError, OSError) as exc
:
1291 _
, details
= utils
.format_exception(exc
)
1292 title
= N_('Error writing patch')
1293 msg
= N_('Unable to write patch to "%s". Check permissions?' % filename
)
1294 Interaction
.critical(title
, message
=msg
, details
=details
)
1296 Interaction
.log('Patch written to "%s"' % filename
)
1299 class DiffWidget(QtWidgets
.QWidget
):
1300 """Display commit metadata and text diffs"""
1302 def __init__(self
, context
, parent
, is_commit
=False, options
=None):
1303 QtWidgets
.QWidget
.__init
__(self
, parent
)
1305 self
.context
= context
1307 self
.oid_start
= None
1309 self
.options
= options
1311 author_font
= QtGui
.QFont(self
.font())
1312 author_font
.setPointSize(int(author_font
.pointSize() * 1.1))
1314 summary_font
= QtGui
.QFont(author_font
)
1315 summary_font
.setWeight(QtGui
.QFont
.Bold
)
1317 policy
= QtWidgets
.QSizePolicy(
1318 QtWidgets
.QSizePolicy
.MinimumExpanding
, QtWidgets
.QSizePolicy
.Minimum
1321 self
.gravatar_label
= gravatar
.GravatarLabel(self
.context
, parent
=self
)
1323 self
.oid_label
= TextLabel()
1324 self
.oid_label
.setTextFormat(Qt
.PlainText
)
1325 self
.oid_label
.setSizePolicy(policy
)
1326 self
.oid_label
.setAlignment(Qt
.AlignBottom
)
1327 self
.oid_label
.elide()
1329 self
.author_label
= TextLabel()
1330 self
.author_label
.setTextFormat(Qt
.RichText
)
1331 self
.author_label
.setFont(author_font
)
1332 self
.author_label
.setSizePolicy(policy
)
1333 self
.author_label
.setAlignment(Qt
.AlignTop
)
1334 self
.author_label
.elide()
1336 self
.date_label
= TextLabel()
1337 self
.date_label
.setTextFormat(Qt
.PlainText
)
1338 self
.date_label
.setSizePolicy(policy
)
1339 self
.date_label
.setAlignment(Qt
.AlignTop
)
1340 self
.date_label
.elide()
1342 self
.summary_label
= TextLabel()
1343 self
.summary_label
.setTextFormat(Qt
.PlainText
)
1344 self
.summary_label
.setFont(summary_font
)
1345 self
.summary_label
.setSizePolicy(policy
)
1346 self
.summary_label
.setAlignment(Qt
.AlignTop
)
1347 self
.summary_label
.elide()
1349 self
.diff
= DiffTextEdit(context
, self
, is_commit
=is_commit
, whitespace
=False)
1350 self
.setFocusProxy(self
.diff
)
1352 self
.info_layout
= qtutils
.vbox(
1361 self
.logo_layout
= qtutils
.hbox(
1362 defs
.no_margin
, defs
.button_spacing
, self
.gravatar_label
, self
.info_layout
1364 self
.logo_layout
.setContentsMargins(defs
.margin
, 0, defs
.margin
, 0)
1366 self
.main_layout
= qtutils
.vbox(
1367 defs
.no_margin
, defs
.spacing
, self
.logo_layout
, self
.diff
1369 self
.setLayout(self
.main_layout
)
1371 self
.set_tabwidth(prefs
.tabwidth(context
))
1373 def set_tabwidth(self
, width
):
1374 self
.diff
.set_tabwidth(width
)
1376 def set_word_wrapping(self
, enabled
, update
=False):
1377 """Enable and disable word wrapping"""
1378 self
.diff
.set_word_wrapping(enabled
, update
=update
)
1380 def set_options(self
, options
):
1381 """Register an options widget"""
1382 self
.options
= options
1383 self
.diff
.set_options(options
)
1385 def start_diff_task(self
, task
):
1386 """Clear the display and start a diff-gathering task"""
1387 self
.diff
.save_scrollbar()
1388 self
.diff
.set_loading_message()
1389 self
.context
.runtask
.start(task
, result
=self
.set_diff
)
1391 def set_diff_oid(self
, oid
, filename
=None):
1392 """Set the diff from a single commit object ID"""
1393 task
= DiffInfoTask(self
.context
, oid
, filename
)
1394 self
.start_diff_task(task
)
1396 def set_diff_range(self
, start
, end
, filename
=None):
1397 task
= DiffRangeTask(self
.context
, start
+ '~', end
, filename
)
1398 self
.start_diff_task(task
)
1400 def commits_selected(self
, commits
):
1401 """Display an appropriate diff when commits are selected"""
1405 commit
= commits
[-1]
1407 author
= commit
.author
or ''
1408 email
= commit
.email
or ''
1409 date
= commit
.authdate
or ''
1410 summary
= commit
.summary
or ''
1411 self
.set_details(oid
, author
, email
, date
, summary
)
1414 if len(commits
) > 1:
1415 start
, end
= commits
[0], commits
[-1]
1416 self
.set_diff_range(start
.oid
, end
.oid
)
1417 self
.oid_start
= start
1420 self
.set_diff_oid(oid
)
1421 self
.oid_start
= None
1424 def set_diff(self
, diff
):
1425 """Set the diff text"""
1426 self
.diff
.set_diff(diff
)
1428 def set_details(self
, oid
, author
, email
, date
, summary
):
1429 template_args
= {'author': author
, 'email': email
, 'summary': summary
}
1431 """%(author)s <"""
1432 """<a href="mailto:%(email)s">"""
1433 """%(email)s</a>>""" % template_args
1435 author_template
= '%(author)s <%(email)s>' % template_args
1437 self
.date_label
.set_text(date
)
1438 self
.date_label
.setVisible(bool(date
))
1439 self
.oid_label
.set_text(oid
)
1440 self
.author_label
.set_template(author_text
, author_template
)
1441 self
.summary_label
.set_text(summary
)
1442 self
.gravatar_label
.set_email(email
)
1445 self
.date_label
.set_text('')
1446 self
.oid_label
.set_text('')
1447 self
.author_label
.set_text('')
1448 self
.summary_label
.set_text('')
1449 self
.gravatar_label
.clear()
1452 def files_selected(self
, filenames
):
1453 """Update the view when a filename is selected"""
1456 oid_start
= self
.oid_start
1457 oid_end
= self
.oid_end
1458 if oid_start
and oid_end
:
1459 self
.set_diff_range(oid_start
.oid
, oid_end
.oid
, filename
=filenames
[0])
1461 self
.set_diff_oid(self
.oid
, filename
=filenames
[0])
1464 class DiffPanel(QtWidgets
.QWidget
):
1465 """A combined diff + search panel"""
1467 def __init__(self
, diff_widget
, text_widget
, parent
):
1468 super(DiffPanel
, self
).__init
__(parent
)
1469 self
.diff_widget
= diff_widget
1470 self
.search_widget
= TextSearchWidget(text_widget
, self
)
1471 self
.search_widget
.hide()
1472 layout
= qtutils
.vbox(
1473 defs
.no_margin
, defs
.spacing
, self
.diff_widget
, self
.search_widget
1475 self
.setLayout(layout
)
1476 self
.setFocusProxy(self
.diff_widget
)
1478 self
.search_action
= qtutils
.add_action(
1480 N_('Search in Diff'),
1485 def show_search(self
):
1486 """Show a dialog for searching diffs"""
1487 # The diff search is only active in text mode.
1488 if not self
.search_widget
.isVisible():
1489 self
.search_widget
.show()
1490 self
.search_widget
.setFocus(True)
1493 class TextLabel(QtWidgets
.QLabel
):
1494 def __init__(self
, parent
=None):
1495 QtWidgets
.QLabel
.__init
__(self
, parent
)
1496 self
.setTextInteractionFlags(
1497 Qt
.TextSelectableByMouse | Qt
.LinksAccessibleByMouse
1503 self
._metrics
= QtGui
.QFontMetrics(self
.font())
1504 self
.setOpenExternalLinks(True)
1509 def set_text(self
, text
):
1510 self
.set_template(text
, text
)
1512 def set_template(self
, text
, template
):
1513 self
._display
= text
1515 self
._template
= template
1516 self
.update_text(self
.width())
1517 self
.setText(self
._display
)
1519 def update_text(self
, width
):
1520 self
._display
= self
._text
1523 text
= self
._metrics
.elidedText(self
._template
, Qt
.ElideRight
, width
- 2)
1524 if text
!= self
._template
:
1525 self
._display
= text
1528 def setFont(self
, font
):
1529 self
._metrics
= QtGui
.QFontMetrics(font
)
1530 QtWidgets
.QLabel
.setFont(self
, font
)
1532 def resizeEvent(self
, event
):
1534 self
.update_text(event
.size().width())
1535 with qtutils
.BlockSignals(self
):
1536 self
.setText(self
._display
)
1537 QtWidgets
.QLabel
.resizeEvent(self
, event
)
1540 class DiffInfoTask(qtutils
.Task
):
1541 """Gather diffs for a single commit"""
1543 def __init__(self
, context
, oid
, filename
):
1544 qtutils
.Task
.__init
__(self
)
1545 self
.context
= context
1547 self
.filename
= filename
1550 context
= self
.context
1552 return gitcmds
.diff_info(context
, oid
, filename
=self
.filename
)
1555 class DiffRangeTask(qtutils
.Task
):
1556 """Gather diffs for a range of commits"""
1558 def __init__(self
, context
, start
, end
, filename
):
1559 qtutils
.Task
.__init
__(self
)
1560 self
.context
= context
1563 self
.filename
= filename
1566 context
= self
.context
1567 return gitcmds
.diff_range(context
, self
.start
, self
.end
, filename
=self
.filename
)