|
# fileview.py - File diff, content, and annotation display widget
#
# Copyright 2010 Steve Borho <steve@borho.org>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2, incorporated herein by reference.
import os
import difflib
import re
from mercurial import error, util
from tortoisehg.util import hglib, patchctx, colormap, thread2
from tortoisehg.hgqt.i18n import _
from tortoisehg.hgqt import qscilib, qtlib, blockmatcher, lexers
from tortoisehg.hgqt import visdiff, filedata
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt4 import Qsci
qsci = Qsci.QsciScintilla
DiffMode = 1
FileMode = 2
AnnMode = 3
class HgFileView(QFrame):
"file diff, content, and annotation viewer"
diffHeaderRegExp = re.compile("^@@ -[0-9]+,[0-9]+ \+[0-9]+,[0-9]+ @@")
linkActivated = pyqtSignal(QString)
fileDisplayed = pyqtSignal(QString, QString)
showMessage = pyqtSignal(QString)
revisionSelected = pyqtSignal(int)
shelveToolExited = pyqtSignal()
grepRequested = pyqtSignal(unicode, dict)
"""Emitted (pattern, opts) when user request to search changelog"""
def __init__(self, repo, parent):
QFrame.__init__(self, parent)
framelayout = QVBoxLayout(self)
framelayout.setContentsMargins(0,0,0,0)
l = QHBoxLayout()
l.setContentsMargins(0,0,0,0)
l.setSpacing(0)
self.repo = repo
self._diffs = []
self.topLayout = QVBoxLayout()
self.labelhbox = hbox = QHBoxLayout()
hbox.setContentsMargins(0,0,0,0)
hbox.setSpacing(2)
self.topLayout.addLayout(hbox)
self.diffToolbar = QToolBar(_('Diff Toolbar'))
self.diffToolbar.setIconSize(QSize(16,16))
self.diffToolbar.setStyleSheet(qtlib.tbstylesheet)
hbox.addWidget(self.diffToolbar)
self.filenamelabel = w = QLabel()
w.setWordWrap(True)
f = w.textInteractionFlags()
w.setTextInteractionFlags(f | Qt.TextSelectableByMouse)
w.linkActivated.connect(self.linkActivated)
hbox.addWidget(w, 1)
self.extralabel = w = QLabel()
w.setWordWrap(True)
w.linkActivated.connect(self.linkActivated)
self.topLayout.addWidget(w)
w.hide()
framelayout.addLayout(self.topLayout)
framelayout.addLayout(l, 1)
hbox = QHBoxLayout()
hbox.setContentsMargins(0, 0, 0, 0)
hbox.setSpacing(0)
l.addLayout(hbox)
self.blk = blockmatcher.BlockList(self)
self.sci = AnnotateView(repo, self)
hbox.addWidget(self.blk)
hbox.addWidget(self.sci, 1)
self.sci.showMessage.connect(self.showMessage)
self.sci.setAnnotationEnabled(False)
self.sci.setContextMenuPolicy(Qt.CustomContextMenu)
self.sci.customContextMenuRequested.connect(self.menuRequest)
self.blk.linkScrollBar(self.sci.verticalScrollBar())
self.blk.setVisible(False)
self.sci.setReadOnly(True)
self.sci.setUtf8(True)
self.sci.installEventFilter(qscilib.KeyPressInterceptor(self))
self.sci.setCaretLineVisible(False)
# define markers for colorize zones of diff
self.markerplus = self.sci.markerDefine(qsci.Background)
self.markerminus = self.sci.markerDefine(qsci.Background)
self.markertriangle = self.sci.markerDefine(qsci.Background)
self.sci.setMarkerBackgroundColor(QColor('#B0FFA0'), self.markerplus)
self.sci.setMarkerBackgroundColor(QColor('#A0A0FF'), self.markerminus)
self.sci.setMarkerBackgroundColor(QColor('#FFA0A0'), self.markertriangle)
# hide margin 0 (markers)
self.sci.setMarginType(0, qsci.SymbolMargin)
self.sci.setMarginWidth(0, 0)
self.searchbar = qscilib.SearchToolBar(hidable=True)
self.searchbar.hide()
self.searchbar.searchRequested.connect(self.find)
self.searchbar.conditionChanged.connect(self.highlightText)
self.layout().addWidget(self.searchbar)
self._ctx = None
self._filename = None
self._status = None
self._mode = None
self._parent = 0
self._lostMode = None
self._lastSearch = u'', False
self.actionDiffMode = QAction(qtlib.geticon('view-diff'),
_('View change as unified diff output'),
self)
self.actionDiffMode.setCheckable(True)
self.actionDiffMode._mode = DiffMode
self.actionFileMode = QAction(qtlib.geticon('view-file'),
_('View change in context of file'),
self)
self.actionFileMode.setCheckable(True)
self.actionFileMode._mode = FileMode
self.actionAnnMode = QAction(qtlib.geticon('view-annotate'),
_('annotate with revision numbers'),
self)
self.actionAnnMode.setCheckable(True)
self.actionAnnMode._mode = AnnMode
self.modeToggleGroup = QActionGroup(self)
self.modeToggleGroup.addAction(self.actionDiffMode)
self.modeToggleGroup.addAction(self.actionFileMode)
self.modeToggleGroup.addAction(self.actionAnnMode)
self.modeToggleGroup.triggered.connect(self.setMode)
# Next/Prev diff (in full file mode)
self.actionNextDiff = QAction(qtlib.geticon('go-down'),
_('Next diff (alt+down)'), self)
self.actionNextDiff.setShortcut('Alt+Down')
self.actionNextDiff.triggered.connect(self.nextDiff)
self.actionPrevDiff = QAction(qtlib.geticon('go-up'),
_('Previous diff (alt+up)'), self)
self.actionPrevDiff.setShortcut('Alt+Up')
self.actionPrevDiff.triggered.connect(self.prevDiff)
self.setMode(self.actionDiffMode)
self.actionFirstParent = QAction('1', self)
self.actionFirstParent.setCheckable(True)
self.actionFirstParent.setChecked(True)
self.actionFirstParent.setShortcut('CTRL+1')
self.actionFirstParent.setToolTip(_('Show changes from first parent'))
self.actionSecondParent = QAction('2', self)
self.actionSecondParent.setCheckable(True)
self.actionSecondParent.setShortcut('CTRL+2')
self.actionSecondParent.setToolTip(_('Show changes from second parent'))
self.parentToggleGroup = QActionGroup(self)
self.parentToggleGroup.addAction(self.actionFirstParent)
self.parentToggleGroup.addAction(self.actionSecondParent)
self.parentToggleGroup.triggered.connect(self.setParent)
self.actionFind = self.searchbar.toggleViewAction()
self.actionFind.setIcon(qtlib.geticon('edit-find'))
self.actionFind.setToolTip(_('Toggle display of text search bar'))
self.actionFind.setShortcut(QKeySequence.Find)
self.actionShelf = QAction('Shelve', self)
self.actionShelf.setIcon(qtlib.geticon('shelve'))
self.actionShelf.setToolTip(_('Open shelve tool'))
self.actionShelf.triggered.connect(self.launchShelve)
tb = self.diffToolbar
tb.addAction(self.actionFirstParent)
tb.addAction(self.actionSecondParent)
tb.addSeparator()
tb.addAction(self.actionDiffMode)
tb.addAction(self.actionFileMode)
tb.addAction(self.actionAnnMode)
tb.addSeparator()
tb.addAction(self.actionNextDiff)
tb.addAction(self.actionPrevDiff)
tb.addSeparator()
tb.addAction(self.actionFind)
tb.addAction(self.actionShelf)
self.timer = QTimer()
self.timer.setSingleShot(False)
self.timer.timeout.connect(self.timerBuildDiffMarkers)
def launchShelve(self):
from tortoisehg.hgqt import shelve
# TODO: pass self._filename
dlg = shelve.ShelveDialog(self.repo, self)
dlg.finished.connect(dlg.deleteLater)
dlg.exec_()
self.shelveToolExited.emit()
def setFont(self, font):
self.sci.setFont(font)
def loadSettings(self, qs, prefix):
self.sci.loadSettings(qs, prefix)
def saveSettings(self, qs, prefix):
self.sci.saveSettings(qs, prefix)
def setRepo(self, repo):
self.repo = repo
self.sci.repo = repo
@pyqtSlot(QAction)
def setMode(self, action):
'One of the mode toolbar buttons has been toggled'
mode = action._mode
self._lostMode = mode
if mode != self._mode:
self._mode = mode
self.actionNextDiff.setEnabled(False)
self.actionPrevDiff.setEnabled(False)
self.blk.setVisible(mode != DiffMode)
self.sci.setAnnotationEnabled(mode == AnnMode)
self.displayFile(self._filename, self._status)
@pyqtSlot(QAction)
def setParent(self, action):
if action.text() == '1':
parent = 0
else:
parent = 1
if self._parent != parent:
self._parent = parent
self.displayFile(self._filename, self._status)
def restrictModes(self, candiff, canfile, canann):
'Disable modes based on content constraints'
self.actionDiffMode.setEnabled(candiff)
self.actionFileMode.setEnabled(canfile)
self.actionAnnMode.setEnabled(canann)
# Switch mode if necessary
mode = self._mode
if not candiff and mode == DiffMode and canfile:
mode = FileMode
if not canfile and mode != DiffMode:
mode = DiffMode
if self._lostMode is None:
self._lostMode = self._mode
if self._mode != mode:
self.actionNextDiff.setEnabled(False)
self.actionPrevDiff.setEnabled(False)
self.blk.setVisible(mode != DiffMode)
self.sci.setAnnotationEnabled(mode == AnnMode)
self._mode = mode
if self._mode == DiffMode:
self.actionDiffMode.setChecked(True)
elif self._mode == FileMode:
self.actionFileMode.setChecked(True)
else:
self.actionAnnMode.setChecked(True)
def setContext(self, ctx, ctx2=None):
self._ctx = ctx
self._ctx2 = ctx2
self.sci.setTabWidth(ctx._repo.tabwidth)
self.actionAnnMode.setVisible(ctx.rev() != None)
self.actionShelf.setVisible(ctx.rev() == None)
self.actionFirstParent.setVisible(len(ctx.parents()) == 2)
self.actionSecondParent.setVisible(len(ctx.parents()) == 2)
self.actionFirstParent.setEnabled(len(ctx.parents()) == 2)
self.actionSecondParent.setEnabled(len(ctx.parents()) == 2)
def showLine(self, line):
if line < self.sci.lines():
self.sci.setCursorPosition(line, 0)
@pyqtSlot()
def clearDisplay(self):
self._filename = None
self._diffs = []
self.restrictModes(False, False, False)
self.sci.setMarginWidth(1, 0)
self.clearMarkup()
def clearMarkup(self):
self.sci.clear()
self.blk.clear()
# Setting the label to ' ' rather than clear() keeps the label
# from disappearing during refresh, and tool layouts bouncing
self.filenamelabel.setText(' ')
self.extralabel.hide()
self.actionNextDiff.setEnabled(False)
self.actionPrevDiff.setEnabled(False)
self.maxWidth = 0
self.sci.showHScrollBar(False)
def displayFile(self, filename=None, status=None):
if isinstance(filename, (unicode, QString)):
filename = hglib.fromunicode(filename)
status = hglib.fromunicode(status)
if filename and self._filename == filename:
# Get the last visible line to restore it after reloading the editor
lastScrollPosition = self.sci.firstVisibleLine()
else:
# Reset the scroll positions when the file is changed
lastScrollPosition = 0
self._filename, self._status = filename, status
self.clearMarkup()
self._diffs = []
if filename is None:
self.restrictModes(False, False, False)
return
if self._ctx2:
ctx2 = self._ctx2
elif self._parent == 0 or len(self._ctx.parents()) == 1:
ctx2 = self._ctx.p1()
else:
ctx2 = self._ctx.p2()
fd = filedata.FileData(self._ctx, ctx2, filename, status)
if fd.elabel:
self.extralabel.setText(fd.elabel)
self.extralabel.show()
else:
self.extralabel.hide()
self.filenamelabel.setText(fd.flabel)
if not fd.isValid():
self.sci.setText(fd.error)
self.sci.setLexer(None)
self.sci.setFont(qtlib.getfont('fontlog').font())
self.sci.setMarginWidth(1, 0)
self.blk.setVisible(False)
self.restrictModes(False, False, False)
return
candiff = bool(fd.diff)
canfile = bool(fd.contents or fd.ucontents)
canann = bool(fd.contents) and type(self._ctx.rev()) is int
if not candiff or not canfile:
self.restrictModes(candiff, canfile, canann)
else:
self.actionDiffMode.setEnabled(True)
self.actionFileMode.setEnabled(True)
self.actionAnnMode.setEnabled(True)
if self._lostMode:
self._mode = self._lostMode
if self._lostMode == DiffMode:
self.actionDiffMode.trigger()
elif self._lostMode == FileMode:
self.actionFileMode.trigger()
elif self._lostMode == AnnMode:
self.actionAnnMode.trigger()
self._lostMode = None
self.blk.setVisible(self._mode != DiffMode)
self.sci.setAnnotationEnabled(self._mode == AnnMode)
if self._mode == DiffMode:
self.sci.setMarginWidth(1, 0)
lexer = lexers.get_diff_lexer(self)
self.sci.setLexer(lexer)
if lexer is None:
self.sci.setFont(qtlib.getfont('fontlog').font())
# trim first three lines, for example:
# diff -r f6bfc41af6d7 -r c1b18806486d tortoisehg/hgqt/thgrepo.py
# --- a/tortoisehg/hgqt/thgrepo.py
# +++ b/tortoisehg/hgqt/thgrepo.py
if fd.diff:
out = fd.diff.split('\n', 3)
if len(out) == 4:
self.sci.setText(hglib.tounicode(out[3]))
else:
# there was an error or rename without diffs
self.sci.setText(hglib.tounicode(fd.diff))
elif fd.ucontents:
# subrepo summary and perhaps other data
self.sci.setText(fd.ucontents)
self.sci.setLexer(None)
self.sci.setFont(qtlib.getfont('fontlog').font())
self.sci.setMarginWidth(1, 0)
self.blk.setVisible(False)
return
elif fd.contents:
lexer = lexers.get_lexer(filename, fd.contents, self)
self.sci.setLexer(lexer)
if lexer is None:
self.sci.setFont(qtlib.getfont('fontlog').font())
self.sci.setText(hglib.tounicode(fd.contents))
self.blk.setVisible(True)
self.sci._updatemarginwidth()
if self._mode == AnnMode:
self.sci._updateannotation(self._ctx, filename)
else:
return
# Recover the last scroll position
# Make sure that lastScrollPosition never exceeds the amount of
# lines on the editor
lastScrollPosition = min(lastScrollPosition, self.sci.lines() - 1)
self.sci.verticalScrollBar().setValue(lastScrollPosition)
self.highlightText(*self._lastSearch)
uf = hglib.tounicode(filename)
uc = hglib.tounicode(fd.contents) or ''
self.fileDisplayed.emit(uf, uc)
if self._mode != DiffMode:
self.blk.setVisible(True)
self.blk.syncPageStep()
if fd.contents and fd.olddata:
if self.timer.isActive():
self.timer.stop()
self._fd = fd
self.timer.start()
self.actionNextDiff.setEnabled(bool(self._diffs))
self.actionPrevDiff.setEnabled(bool(self._diffs))
lexer = self.sci.lexer()
if lexer:
font = self.sci.lexer().font(0)
else:
font = self.sci.font()
fm = QFontMetrics(font)
maxWidth = fm.maxWidth()
lines = self.sci.text().split('\n')
widths = [fm.width(line) + maxWidth for line in lines]
self.maxWidth = max(widths)
self.updateScrollBar()
#
# These four functions are used by Shift+Cursor actions in revdetails
#
def nextLine(self):
x, y = self.sci.getCursorPosition()
self.sci.setCursorPosition(x+1, y)
def prevLine(self):
x, y = self.sci.getCursorPosition()
self.sci.setCursorPosition(x-1, y)
def nextCol(self):
x, y = self.sci.getCursorPosition()
self.sci.setCursorPosition(x, y+1)
def prevCol(self):
x, y = self.sci.getCursorPosition()
self.sci.setCursorPosition(x, y-1)
@pyqtSlot(unicode, bool, bool, bool)
def find(self, exp, icase=True, wrap=False, forward=True):
self.sci.find(exp, icase, wrap, forward)
@pyqtSlot(unicode, bool)
def highlightText(self, match, icase=False):
self._lastSearch = match, icase
self.sci.highlightText(match, icase)
def verticalScrollBar(self):
return self.sci.verticalScrollBar()
#
# file mode diff markers
#
def timerBuildDiffMarkers(self):
'show modified and added lines in the self.blk margin'
# The way the diff markers are generated differs between the DiffMode
# and the other modes
# In the DiffMode case, the marker positions are found by looking for
# lines matching a regular expression representing a diff header, while
# in all other cases we use the difflib.SequenceMatcher, which returns
# a set of opcodes that must be parsed
# In any case, the markers are generated incrementally. This function is
# run by a timer, which each time that is called processes a bunch of
# lines (when in DiffMode) or of opcodes (in all other modes).
# When there are no more lines or opcodes to consume the timer is
# stopped.
self.sci.setUpdatesEnabled(False)
self.blk.setUpdatesEnabled(False)
if self._mode == DiffMode:
if self._fd:
self._fd = None
self._diffs = []
self._linestoprocess = unicode(self.sci.text()).splitlines()
self._firstlinetoprocess = 0
self._opcodes = True
# Process linesPerBlock lines at a time
linesPerBlock = 100
# Look for lines matching the "diff header"
for n, line in enumerate(self._linestoprocess[:linesPerBlock]):
if self.diffHeaderRegExp.match(line):
diffLine = self._firstlinetoprocess + n
self._diffs.append([diffLine, diffLine])
self.sci.markerAdd(diffLine, self.markerplus)
self._linestoprocess = self._linestoprocess[linesPerBlock:]
self._firstlinetoprocess += linesPerBlock
if not self._linestoprocess:
self._opcodes = False
self._firstlinetoprocess = 0
else:
if self._fd:
olddata = self._fd.olddata.splitlines()
newdata = self._fd.contents.splitlines()
diff = difflib.SequenceMatcher(None, olddata, newdata)
self._opcodes = diff.get_opcodes()
self._fd = None
self._diffs = []
elif isinstance(self._opcodes, bool):
# catch self._mode changes while this thread is active
self._opcodes = []
for tag, alo, ahi, blo, bhi in self._opcodes[:30]:
if tag == 'replace':
self._diffs.append([blo, bhi])
self.blk.addBlock('x', blo, bhi)
for i in range(blo, bhi):
self.sci.markerAdd(i, self.markertriangle)
elif tag == 'insert':
self._diffs.append([blo, bhi])
self.blk.addBlock('+', blo, bhi)
for i in range(blo, bhi):
self.sci.markerAdd(i, self.markerplus)
elif tag in ('equal', 'delete'):
pass
else:
raise ValueError, 'unknown tag %r' % (tag,)
self._opcodes = self._opcodes[30:]
if not self._opcodes:
self.actionNextDiff.setEnabled(bool(self._diffs))
self.actionPrevDiff.setEnabled(False)
self.timer.stop()
self.sci.setUpdatesEnabled(True)
self.blk.setUpdatesEnabled(True)
def nextDiff(self):
if not self._diffs:
self.actionNextDiff.setEnabled(False)
self.actionPrevDiff.setEnabled(False)
return
else:
row, column = self.sci.getCursorPosition()
for i, (lo, hi) in enumerate(self._diffs):
if lo > row:
last = (i == (len(self._diffs)-1))
self.sci.setCursorPosition(lo, 0)
self.sci.verticalScrollBar().setValue(lo)
break
else:
last = True
self.actionNextDiff.setEnabled(not last)
self.actionPrevDiff.setEnabled(True)
def prevDiff(self):
if not self._diffs:
self.actionNextDiff.setEnabled(False)
self.actionPrevDiff.setEnabled(False)
return
else:
row, column = self.sci.getCursorPosition()
for i, (lo, hi) in enumerate(reversed(self._diffs)):
if hi < row:
first = (i == (len(self._diffs)-1))
self.sci.setCursorPosition(lo, 0)
self.sci.verticalScrollBar().setValue(lo)
break
else:
first = True
self.actionNextDiff.setEnabled(True)
self.actionPrevDiff.setEnabled(not first)
def nDiffs(self):
return len(self._diffs)
def editSelected(self, path, rev, line):
"""Open editor to show the specified file"""
path = hglib.fromunicode(path)
base = visdiff.snapshot(self.repo, [path], self.repo[rev])[0]
files = [os.path.join(base, path)]
pattern = hglib.fromunicode(self._lastSearch[0])
qtlib.editfiles(self.repo, files, line, pattern, self)
@pyqtSlot(QPoint)
def menuRequest(self, point):
menu = self.sci.createStandardContextMenu()
line = self.sci.lineAt(point)
point = self.sci.viewport().mapToGlobal(point)
selection = self.sci.selectedText()
def sreq(**opts):
return lambda: self.grepRequested.emit(selection, opts)
def sann():
self.searchbar.search(selection)
self.searchbar.show()
if self._mode != AnnMode:
if selection:
menu.addSeparator()
for name, func in [(_('Search in current file'), sann),
(_('Search in history'), sreq(all=True))]:
def add(name, func):
action = menu.addAction(name)
action.triggered.connect(func)
add(name, func)
return menu.exec_(point)
if line < 0 or line >= len(self.sci._links):
return menu.exec_(point)
fctx, line = self.sci._links[line]
if selection:
def sreq(**opts):
return lambda: self.grepRequested.emit(selection, opts)
def sann():
self.searchbar.search(selection)
self.searchbar.show()
menu.addSeparator()
for name, func in [(_('Search in original revision'),
sreq(rev=fctx.rev())),
(_('Search in working revision'),
sreq(rev='.')),
(_('Search in current annotation'), sann),
(_('Search in history'), sreq(all=True))]:
def add(name, func):
action = menu.addAction(name)
action.triggered.connect(func)
add(name, func)
def setSource(path, rev, line):
self.revisionSelected.emit(rev)
self.setContext(self.repo[rev])
self.displayFile(path, None)
self.showLine(line)
data = [hglib.tounicode(fctx.path()), fctx.rev(), line]
def annorig():
setSource(*data)
def editorig():
self.editSelected(*data)
menu.addSeparator()
for name, func in [(_('Annotate originating revision'), annorig),
(_('View originating revision'), editorig)]:
def add(name, func):
action = menu.addAction(name)
action.triggered.connect(func)
add(name, func)
for pfctx in fctx.parents():
pdata = [hglib.tounicode(pfctx.path()), pfctx.changectx().rev(),
line]
def annparent(data):
setSource(*data)
def editparent(data):
self.editSelected(*data)
for name, func in [(_('Annotate parent revision %d') % pdata[1],
annparent),
(_('View parent revision %d') % pdata[1],
editparent)]:
def add(name, func):
action = menu.addAction(name)
action.data = pdata
action.run = lambda: func(action.data)
action.triggered.connect(action.run)
add(name, func)
menu.exec_(point)
def resizeEvent(self, event):
super(HgFileView, self).resizeEvent(event)
self.updateScrollBar()
def updateScrollBar(self):
sbWidth = self.sci.verticalScrollBar().width()
scrollWidth = self.maxWidth + sbWidth - self.sci.width()
self.sci.showHScrollBar(scrollWidth > 0)
self.sci.horizontalScrollBar().setRange(0, scrollWidth)
class AnnotateView(qscilib.Scintilla):
'QScintilla widget capable of displaying annotations'
showMessage = pyqtSignal(QString)
def __init__(self, repo, parent=None):
super(AnnotateView, self).__init__(parent)
self.setReadOnly(True)
self.setMarginLineNumbers(1, True)
self.setMarginType(2, qsci.TextMarginRightJustified)
self.setMouseTracking(False)
self._annotation_enabled = False
self._links = [] # by line
self._revmarkers = {} # by rev
self._lastrev = None
self._thread = AnnotateThread(self)
self._thread.finished.connect(self.fillModel)
self.repo = repo
self.repo.configChanged.connect(self.configChanged)
self.configChanged()
def configChanged(self):
self.setIndentationWidth(self.repo.tabwidth)
self.setTabWidth(self.repo.tabwidth)
def keyPressEvent(self, event):
if event.key() == Qt.Key_Escape:
self._thread.abort()
return
return super(AnnotateView, self).keyPressEvent(event)
def mouseMoveEvent(self, event):
self._emitRevisionHintAtLine(self.lineAt(event.pos()))
super(AnnotateView, self).mouseMoveEvent(event)
def _emitRevisionHintAtLine(self, line):
if line < 0:
return
try:
fctx = self._links[line][0]
if fctx.rev() != self._lastrev:
s = hglib.get_revision_desc(fctx, self.annfile)
self.showMessage.emit(s)
self._lastrev = fctx.rev()
except IndexError:
pass
def _updateannotation(self, ctx, filename):
if ctx.rev() is None:
return
wsub, filename, ctx = hglib.getDeepestSubrepoContainingFile(filename, ctx)
if wsub is None:
# The file was not found in the repo context or its subrepos
# This may happen for files that have been removed
return
self.ctx = ctx
self.annfile = filename
self._thread.abort()
self._thread.start(ctx[filename])
@pyqtSlot()
def fillModel(self):
self._thread.wait()
if self._thread.data is None:
return
self._links = list(self._thread.data)
self._updaterevmargin()
self._updatemarkers()
self._updatemarginwidth()
def clear(self):
super(AnnotateView, self).clear()
self.clearMarginText()
self.markerDeleteAll()
def setAnnotationEnabled(self, enabled):
"""Enable / disable annotation"""
self._annotation_enabled = enabled
self._updatemarginwidth()
self.setMouseTracking(enabled)
if not enabled:
self.markerDeleteAll()
def isAnnotationEnabled(self):
"""True if annotation enabled and available"""
return self._annotation_enabled
def _updaterevmargin(self):
"""Update the content of margin area showing revisions"""
s = self._margin_style
# Workaround to set style of the current sci widget.
# QsciStyle sends style data only to the first sci widget.
# See qscintilla2/Qt4/qscistyle.cpp
self.SendScintilla(qsci.SCI_STYLESETBACK,
s.style(), s.paper())
self.SendScintilla(qsci.SCI_STYLESETFONT,
s.style(), s.font().family().toAscii().data())
self.SendScintilla(qsci.SCI_STYLESETSIZE,
s.style(), s.font().pointSize())
for i, (fctx, _origline) in enumerate(self._links):
self.setMarginText(i, str(fctx.rev()), s)
def _updatemarkers(self):
"""Update markers which colorizes each line"""
self._redefinemarkers()
for i, (fctx, _origline) in enumerate(self._links):
m = self._revmarkers.get(fctx.rev())
if m is not None:
self.markerAdd(i, m)
def _redefinemarkers(self):
"""Redefine line markers according to the current revs"""
curdate = self.ctx.date()[0]
# make sure to colorize at least 1 year
mindate = curdate - 365 * 24 * 60 * 60
self._revmarkers.clear()
filectxs = iter(fctx for fctx, _origline in self._links)
palette = colormap.makeannotatepalette(filectxs, curdate,
maxcolors=32, maxhues=8,
maxsaturations=16,
mindate=mindate)
for i, (color, fctxs) in enumerate(palette.iteritems()):
self.markerDefine(qsci.Background, i)
self.setMarkerBackgroundColor(QColor(color), i)
for fctx in fctxs:
self._revmarkers[fctx.rev()] = i
@util.propertycache
def _margin_style(self):
"""Style for margin area"""
s = Qsci.QsciStyle()
s.setPaper(QApplication.palette().color(QPalette.Window))
s.setFont(self.font())
return s
@pyqtSlot()
def _updatemarginwidth(self):
self.setMarginsFont(self.font())
def lentext(s):
return 'M' * (len(str(s)) + 2) # 2 for margin
self.setMarginWidth(1, lentext(self.lines()))
if self.isAnnotationEnabled() and self._links:
maxrev = max(fctx.rev() for fctx, _origline in self._links)
self.setMarginWidth(2, lentext(maxrev))
else:
self.setMarginWidth(2, 0)
class AnnotateThread(QThread):
'Background thread for annotating a file at a revision'
def __init__(self, parent=None):
super(AnnotateThread, self).__init__(parent)
self._threadid = None
@pyqtSlot(object)
def start(self, fctx):
self._fctx = fctx
super(AnnotateThread, self).start()
self.data = None
@pyqtSlot()
def abort(self):
if self._threadid is None:
return
try:
thread2._async_raise(self._threadid, KeyboardInterrupt)
self.wait()
except ValueError:
pass
def run(self):
assert self.currentThread() != qApp.thread()
self._threadid = self.currentThreadId()
try:
try:
data = []
for (fctx, line), _text in self._fctx.annotate(True, True):
data.append((fctx, line))
self.data = data
except KeyboardInterrupt:
pass
finally:
self._threadid = None
del self._fctx
|
Loading...