Staging
v0.8.1
v0.8.1
https://foss.heptapod.net/mercurial/hgview
Revision df7af11a5ea2143444341e527210ba220bc45f40 authored by Carine Dengler on 11 February 2020, 15:15:42 UTC, committed by Carine Dengler on 11 February 2020, 15:15:42 UTC
1 parent d52361f
Tip revision: df7af11a5ea2143444341e527210ba220bc45f40 authored by Carine Dengler on 11 February 2020, 15:15:42 UTC
[pkg] version 1.13.1
[pkg] version 1.13.1
Tip revision: df7af11
hgfiledialog.py
# -*- coding: utf-8 -*-
# Copyright (c) 2003-2012 LOGILAB S.A. (Paris, FRANCE).
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
"""
Qt4 dialogs to display hg revisions of a file
"""
import os
import os.path as osp
import difflib
from PyQt4 import QtGui, QtCore
from PyQt4.QtCore import Qt, pyqtSignal
from hgviewlib.application import (FileViewer as _FileViewer,
FileDiffViewer as _FileDiffViewer)
from hgviewlib.util import tounicode, tohg, build_repo
from hgviewlib.qt4.mixins import HgDialogMixin, ActionsMixin, ui2cls
from hgviewlib.qt4.hgrepomodel import FileRevModel
from hgviewlib.qt4.blockmatcher import BlockList, BlockMatch
from hgviewlib.qt4.quickbar import FindInGraphlogQuickBar
from hgviewlib.qt4.widgets import SourceViewer
sides = ('left', 'right')
otherside = {'left': 'right', 'right': 'left'}
class AbstractFileDialog(ActionsMixin, HgDialogMixin, QtGui.QMainWindow):
def __init__(self, repo, filename, repoviewer=None):
self.repo = repo
super(AbstractFileDialog, self).__init__()
self.load_ui()
self.load_config(self.repo)
self.setRepoViewer(repoviewer)
self._show_rev = None
self.filename = filename
self.createActions()
self.setupToolbars()
self.setupViews()
self.setupModels()
def setRepoViewer(self, repoviewer=None):
self.repoviewer = repoviewer
if repoviewer:
repoviewer.destroyed.connect(lambda x: self.setRepoViewer())
def reload(self):
self.repo = build_repo(self.repo.ui, tounicode(self.repo.root))
self.setupModels()
def modelFilled(self):
self.filerevmodel.filled.disconnect(self.modelFilled)
if isinstance(self._show_rev, int):
index = self.filerevmodel.indexFromRev(self._show_rev)
self._show_rev = None
else:
index = self.filerevmodel.index(0,0)
self.tableView_revisions.setCurrentIndex(index)
def revisionActivated(self, rev):
"""
Callback called when a revision is double-clicked in the revisions table
"""
if self.repoviewer is None:
# prevent recursive import
from hgviewlib.qt4.hgrepoviewer import HgRepoViewer
self.repoviewer = HgRepoViewer(self.repo)
self.repoviewer.goto(rev)
self.repoviewer.show()
self.repoviewer.activateWindow()
self.repoviewer.raise_()
class FileViewer(AbstractFileDialog, ui2cls('fileviewer.ui'), _FileViewer):
"""
A dialog showing a revision graph for a file.
"""
def setupViews(self):
self.textView.setFont(self._font)
self.setWindowTitle('hgview filelog: %s' % os.path.abspath(self.filename))
self.textView.message_logged.connect(self.statusBar().showMessage)
def setupToolbars(self):
self.find_toolbar = FindInGraphlogQuickBar(self)
self.find_toolbar.attachFileView(self.textView)
self.find_toolbar.revision_selected.connect(
self.tableView_revisions.goto)
self.find_toolbar.revision_selected[int].connect(
self.tableView_revisions.goto)
self.find_toolbar.message_logged.connect(self.statusBar().showMessage)
self.attachQuickBar(self.find_toolbar)
add_actions = self.toolBar_edit.addActions
self.toolBar_edit.addSeparator()
add_actions(self.tableView_revisions.get_actions('back', 'forward'))
self.toolBar_edit.addSeparator()
add_actions(self.get_actions('diffmode', 'annmode', 'next', 'prev'))
self.attachQuickBar(self.tableView_revisions.goto_toolbar)
def setupModels(self):
self.filerevmodel = FileRevModel(self.repo)
self.tableView_revisions.setModel(self.filerevmodel)
self.tableView_revisions.revision_selected.connect(
self.revisionSelected)
self.tableView_revisions.revision_selected[int].connect(
self.revisionSelected)
self.tableView_revisions.revision_activated.connect(
self.revisionActivated)
self.tableView_revisions.revision_activated[int].connect(
self.revisionActivated)
self.filerevmodel.message_logged.connect(
self.statusBar().showMessage,
Qt.QueuedConnection)
self.filerevmodel.filled.connect(self.modelFilled)
self.textView.setMode('file')
self.textView.setModel(self.filerevmodel)
self.find_toolbar.setModel(self.filerevmodel)
self.find_toolbar.setFilterFiles([self.filename])
self.find_toolbar.setMode('file')
self.filerevmodel.setFilename(self.filename)
def createActions(self):
self.add_action(
'close', self.actionClose,
icon='quit',
callback=lambda: self.close(),
)
self.add_action(
'reload', self.actionReload,
icon='reload',
callback=lambda: self.reload(),
)
self.add_action(
'diffmode', self.tr("Diff mode"),
menu=self.tr("Mode"),
icon='diffmode' ,
tip=self.tr("Enable/Disable Diff mode"),
callback=self.setMode,
checked=False,
)
self.add_action(
'annmode', self.tr("Annotate mode"),
tip=self.tr("Enable/Disable Annotate mode"),
callback=self.textView.setAnnotate,
checked=False,
)
self.add_action(
'next', self.tr("Next hunk"),
menu=self.tr("Moves"),
icon='down',
tip=self.tr("Jump to the next hunk"),
keys=[Qt.ALT + Qt.Key_Down],
callback=lambda: self.nextDiff(),
)
self.add_action(
'prev', self.tr("Prior hunk"),
menu=self.tr("Moves"),
icon='up',
tip=self.tr("Jump to the previous hunk"),
keys=[Qt.ALT + Qt.Key_Up],
callback=lambda: self.prevDiff(),
)
def revisionSelected(self, rev):
pos = self.textView.verticalScrollBar().value()
ctx = self.filerevmodel.repo[rev]
self.textView.setContext(ctx)
self.textView.displayFile(self.filerevmodel.graph.filename(rev))
self.textView.verticalScrollBar().setValue(pos)
self.set_action('prev', enabled=False)
self.textView.filled.connect(
lambda self=self: self.set_action('next',
enabled=self.textView.fileMode() and self.textView.nDiffs()))
def goto(self, rev):
index = self.filerevmodel.indexFromRev(rev)
if index is not None:
self.tableView_revisions.setCurrentIndex(index)
else:
self._show_rev = rev
def setMode(self, mode):
self.textView.setMode(mode)
self.set_actions('annmode', 'next', 'prev', enabled=not mode)
def nextDiff(self):
notlast = self.textView.nextDiff()
enabled = self.textView.fileMode() and notlast and self.textView.nDiffs()
self.set_action('next', enabled=(enabled if enabled is not None else True))
enabled = self.textView.fileMode() and self.textView.nDiffs()
self.set_action('prev', enable=(enabled if enabled is not None else True))
def prevDiff(self):
notfirst = self.textView.prevDiff()
self.set_action('prev',
enabled=self.textView.fileMode() and notfirst and self.textView.nDiffs())
self.set_action('next',
enabled=self.textView.fileMode() and self.textView.nDiffs())
class FileDiffViewer(AbstractFileDialog, ui2cls('filediffviewer.ui'), _FileDiffViewer):
"""
Qt4 dialog to display diffs between different mercurial revisions of a file.
"""
diff_filled = pyqtSignal()
def setupViews(self):
self.tableView_revisions = self.tableView_revisions_left
self.tableViews = {'left': self.tableView_revisions_left,
'right': self.tableView_revisions_right}
# viewers are Scintilla editors
self.viewers = {}
# block are diff-block displayers
self.block = {}
self.diffblock = BlockMatch(self.frame)
lay = QtGui.QHBoxLayout(self.frame)
lay.setSpacing(0)
lay.setContentsMargins(0, 0, 0, 0)
for side, idx in (('left', 0), ('right', 3)):
textview = SourceViewer(self.frame)
textview.setFont(self._font)
textview.verticalScrollBar().setFocusPolicy(Qt.StrongFocus)
textview.setFocusProxy(textview.verticalScrollBar())
textview.verticalScrollBar().installEventFilter(self)
lay.addWidget(textview)
self.viewers[side] = textview
blk = BlockList(self.frame)
blk.linkScrollBar(textview.verticalScrollBar())
self.diffblock.linkScrollBar(textview.verticalScrollBar(), side)
lay.insertWidget(idx, blk)
self.block[side] = blk
lay.insertWidget(2, self.diffblock)
for side in sides:
table = getattr(self, 'tableView_revisions_%s' % side)
table.setTabKeyNavigation(False)
#table.installEventFilter(self)
table.revision_selected.connect(self.revisionSelected)
table.revision_selected[int].connect(self.revisionSelected)
table.revision_activated.connect(self.revisionActivated)
table.revision_activated[int].connect(self.revisionActivated)
self.viewers[side].verticalScrollBar().valueChanged[int].connect(
lambda value, side=side: self.vbar_changed(value, side))
self.attachQuickBar(table.goto_toolbar)
self.setTabOrder(table, self.viewers['left'])
self.setTabOrder(self.viewers['left'], self.viewers['right'])
self.setWindowTitle('hgview diff: %s' % os.path.abspath(self.filename))
# timer used to fill viewers with diff block markers during GUI idle time
self.timer = QtCore.QTimer()
self.timer.setSingleShot(False)
self.timer.timeout.connect(self.idle_fill_files)
def setupModels(self):
self.filedata = {'left': None, 'right': None}
self._invbarchanged = False
self.filerevmodel = FileRevModel(self.repo, self.filename)
self.filerevmodel.filled.connect(self.modelFilled)
self.tableView_revisions_left.setModel(self.filerevmodel)
self.tableView_revisions_right.setModel(self.filerevmodel)
def createActions(self):
self.add_action(
'close', self.actionClose,
icon='quit',
callback=lambda: self.close(),
)
self.add_action(
'reload', self.actionReload,
icon='reload',
callback=lambda: self.reload(),
)
self.add_action(
'next', self.tr("Next hunk"),
menu=self.tr("Moves"),
icon='down',
tip=self.tr("Jump to the next hunk"),
keys=[Qt.ALT + Qt.Key_Down],
callback=lambda: self.nextDiff(),
enabled=False,
)
self.add_action(
'prev', self.tr("Prior hunk"),
menu=self.tr("Moves"),
icon='up',
tip=self.tr("Jump to the previous hunk"),
keys=[Qt.ALT + Qt.Key_Up],
callback=lambda: self.prevDiff(),
enabled=False,
)
def setupToolbars(self):
self.toolBar_edit.addSeparator()
self.toolBar_edit.addActions(self.get_actions('next', 'prev'))
def modelFilled(self):
self.filerevmodel.filled.disconnect(self.modelFilled)
if self._show_rev is not None:
rev = self._show_rev
self._show_rev = None
else:
rev = self.filerevmodel.graph[0].rev
self.goto(rev)
def revisionSelected(self, rev):
if self.sender() is self.tableView_revisions_right:
side = 'right'
else:
side = 'left'
path = self.filerevmodel.graph.nodesdict[rev].extra[0]
fc = self.repo[rev].filectx(tohg(path))
self.filedata[side] = tounicode(fc.data()).splitlines()
self.update_diff(keeppos=otherside[side])
def goto(self, rev):
index = self.filerevmodel.indexFromRev(rev)
if index is not None:
if index.row() == 0:
index = self.filerevmodel.index(1, 0)
self.tableView_revisions_left.setCurrentIndex(index)
index = self.filerevmodel.index(0, 0)
self.tableView_revisions_right.setCurrentIndex(index)
else:
self._show_rev = rev
def setDiffNavActions(self, pos=0):
hasdiff = (self.diffblock.nDiffs() > 0)
self.set_action('next', enabled=hasdiff and pos != 1)
self.set_action('prev', enabled=hasdiff and pos != -1)
def nextDiff(self):
self.setDiffNavActions(self.diffblock.nextDiff())
def prevDiff(self):
self.setDiffNavActions(self.diffblock.prevDiff())
def update_page_steps(self, keeppos=None):
for side in sides:
self.block[side].syncPageStep()
self.diffblock.syncPageStep()
if keeppos:
side, pos = keeppos
self.viewers[side].verticalScrollBar().setValue(pos)
def idle_fill_files(self):
# we make a burst of diff-lines computed at once, but we
# disable GUI updates for efficiency reasons, then only
# refresh GUI at the end of the burst
for side in sides:
self.viewers[side].setUpdatesEnabled(False)
self.block[side].setUpdatesEnabled(False)
self.diffblock.setUpdatesEnabled(False)
for n in range(30): # burst pool
if self._diff is None or not self._diff.get_opcodes():
self._diff = None
self.timer.stop()
self.setDiffNavActions(-1)
self.diff_filled.emit()
break
tag, alo, ahi, blo, bhi = self._diff.get_opcodes().pop(0)
if tag == 'replace':
self.block['left'].addBlock('x', alo, ahi)
self.block['right'].addBlock('x', blo, bhi)
self.diffblock.addBlock('x', alo, ahi, blo, bhi)
w = self.viewers['left']
for i in range(alo, ahi):
w.markerAdd(i, w.markertriangle)
w = self.viewers['right']
for i in range(blo, bhi):
w.markerAdd(i, w.markertriangle)
elif tag == 'delete':
self.block['left'].addBlock('-', alo, ahi)
self.diffblock.addBlock('-', alo, ahi, blo, bhi)
w = self.viewers['left']
for i in range(alo, ahi):
w.markerAdd(i, w.markerminus)
elif tag == 'insert':
self.block['right'].addBlock('+', blo, bhi)
self.diffblock.addBlock('+', alo, ahi, blo, bhi)
w = self.viewers['right']
for i in range(blo, bhi):
w.markerAdd(i, w.markerplus)
elif tag == 'equal':
pass
else:
raise ValueError('unknown tag %r' % (tag,))
# ok, let's enable GUI refresh for code viewers and diff-block displayers
for side in sides:
self.viewers[side].setUpdatesEnabled(True)
self.block[side].setUpdatesEnabled(True)
self.diffblock.setUpdatesEnabled(True)
def update_diff(self, keeppos=None):
"""
Recompute the diff, display files and starts the timer
responsible for filling diff markers
"""
if keeppos:
pos = self.viewers[keeppos].verticalScrollBar().value()
keeppos = (keeppos, pos)
for side in sides:
self.viewers[side].clear()
self.block[side].clear()
self.diffblock.clear()
if None not in self.filedata.values():
if self.timer.isActive():
self.timer.stop()
for side in sides:
self.viewers[side].setMarginWidth(1, "00%s" % len(self.filedata[side]))
self._diff = difflib.SequenceMatcher(None, self.filedata['left'],
self.filedata['right'])
blocks = self._diff.get_opcodes()[:]
self._diffmatch = {'left': [x[1:3] for x in blocks],
'right': [x[3:5] for x in blocks]}
for side in sides:
self.viewers[side].set_text(self.filename,
'\n'.join(self.filedata[side]),
flag='+',
cfg=self.cfg)
self.update_page_steps(keeppos)
self.timer.start()
def vbar_changed(self, value, side):
"""
Callback called when the vertical scrollbar of a file viewer
is changed, so we can update the position of the other file
viewer.
"""
if self._invbarchanged:
# prevent loops in changes (left -> right -> left ...)
return
self._invbarchanged = True
oside = otherside[side]
for i, (lo, hi) in enumerate(self._diffmatch[side]):
if lo <= value < hi:
break
dv = value - lo
blo, bhi = self._diffmatch[oside][i]
vbar = self.viewers[oside].verticalScrollBar()
if (dv) < (bhi - blo):
bvalue = blo + dv
else:
bvalue = bhi
vbar.setValue(bvalue)
self._invbarchanged = False
Computing file changes ...