/tortoisehg/hgqt/blockmatcher.py
https://bitbucket.org/tortoisehg/hgtk/ · Python · 367 lines · 345 code · 3 blank · 19 comment · 0 complexity · d68934e421eeabc39e974594bd5ca15b MD5 · raw file
- # Copyright (c) 2003-2010 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, write to the Free Software Foundation, Inc.,
- # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
- """
- Qt4 widgets to display diffs as blocks
- """
- import sys, os
- from PyQt4.QtGui import *
- from PyQt4.QtCore import *
- class BlockList(QWidget):
- """
- A simple widget to be 'linked' to the scrollbar of a diff text
- view.
- It represents diff blocks with coloured rectangles, showing
- currently viewed area by a semi-transparant rectangle sliding
- above them.
- """
- rangeChanged = pyqtSignal(int,int)
- valueChanged = pyqtSignal(int)
- pageStepChanged = pyqtSignal(int)
- def __init__(self, *args):
- QWidget.__init__(self, *args)
- self._blocks = set()
- self._minimum = 0
- self._maximum = 100
- self.blockTypes = {'+': QColor(0xA0, 0xFF, 0xB0, ),#0xa5),
- '-': QColor(0xFF, 0xA0, 0xA0, ),#0xa5),
- 'x': QColor(0xA0, 0xA0, 0xFF, ),#0xa5),
- }
- self._sbar = None
- self._value = 0
- self._pagestep = 10
- self._vrectcolor = QColor(0x00, 0x00, 0x55, 0x25)
- self._vrectbordercolor = self._vrectcolor.darker()
- self.sizePolicy().setControlType(QSizePolicy.Slider)
- self.setMinimumWidth(20)
- def clear(self):
- self._blocks = set()
- def addBlock(self, typ, alo, ahi):
- self._blocks.add((typ, alo, ahi))
- def setMaximum(self, maximum):
- self._maximum = maximum
- self.update()
- self.rangeChanged.emit(self._minimum, self._maximum)
- def setMinimum(self, minimum):
- self._minimum = minimum
- self.update()
- self.rangeChanged.emit(self._minimum, self._maximum)
- def setRange(self, minimum, maximum):
- if minimum == maximum:
- return
- self._minimum = minimum
- self._maximum = maximum
- self.update()
- self.rangeChanged.emit(self._minimum, self._maximum)
- def setValue(self, val):
- if val != self._value:
- self._value = val
- self.update()
- self.valueChanged.emit(val)
- def setPageStep(self, pagestep):
- if pagestep != self._pagestep:
- self._pagestep = pagestep
- self.update()
- self.pageStepChanged.emit(pagestep)
- def linkScrollBar(self, sbar):
- """
- Make the block list displayer be linked to the scrollbar
- """
- self._sbar = sbar
- self.setUpdatesEnabled(False)
- self.setMaximum(sbar.maximum())
- self.setMinimum(sbar.minimum())
- self.setPageStep(sbar.pageStep())
- self.setValue(sbar.value())
- self.setUpdatesEnabled(True)
- sbar.valueChanged.connect(self.setValue)
- sbar.rangeChanged.connect(self.setRange)
- self.valueChanged.connect(sbar.setValue)
- self.rangeChanged.connect(lambda x, y: sbar.setRange(x,y))
- self.pageStepChanged.connect(lambda x: sbar.setPageStep(x))
- def syncPageStep(self):
- self.setPageStep(self._sbar.pageStep())
- def paintEvent(self, event):
- w = self.width() - 1
- h = self.height()
- p = QPainter(self)
- p.scale(1.0, float(h)/(self._maximum - self._minimum + self._pagestep))
- p.setPen(Qt.NoPen)
- for typ, alo, ahi in self._blocks:
- p.save()
- p.setBrush(self.blockTypes[typ])
- p.drawRect(1, alo, w-1, ahi-alo)
- p.restore()
- p.save()
- p.setPen(self._vrectbordercolor)
- p.setBrush(self._vrectcolor)
- p.drawRect(0, self._value, w, self._pagestep)
- p.restore()
- class BlockMatch(BlockList):
- """
- A simpe widget to be linked to 2 file views (text areas),
- displaying 2 versions of a same file (diff).
- It will show graphically matching diff blocks between the 2 text
- areas.
- """
- rangeChanged = pyqtSignal(int, int, QString)
- valueChanged = pyqtSignal(int, QString)
- pageStepChanged = pyqtSignal(int, QString)
- def __init__(self, *args):
- QWidget.__init__(self, *args)
- self._blocks = set()
- self._minimum = {'left': 0, 'right': 0}
- self._maximum = {'left': 100, 'right': 100}
- self.blockTypes = {'+': QColor(0xA0, 0xFF, 0xB0, ),#0xa5),
- '-': QColor(0xFF, 0xA0, 0xA0, ),#0xa5),
- 'x': QColor(0xA0, 0xA0, 0xFF, ),#0xa5),
- }
- self._sbar = {}
- self._value = {'left': 0, 'right': 0}
- self._pagestep = {'left': 10, 'right': 10}
- self._vrectcolor = QColor(0x00, 0x00, 0x55, 0x25)
- self._vrectbordercolor = self._vrectcolor.darker()
- self.sizePolicy().setControlType(QSizePolicy.Slider)
- self.setMinimumWidth(20)
- def nDiffs(self):
- return len(self._blocks)
- def showDiff(self, delta):
- ps_l = float(self._pagestep['left'])
- ps_r = float(self._pagestep['right'])
- mv_l = self._value['left']
- mv_r = self._value['right']
- Mv_l = mv_l + ps_l
- Mv_r = mv_r + ps_r
- vblocks = []
- blocks = sorted(self._blocks, key=lambda x:(x[1],x[3],x[2],x[4]))
- for i, (typ, alo, ahi, blo, bhi) in enumerate(blocks):
- if (mv_l<=alo<=Mv_l or mv_l<=ahi<=Mv_l or
- mv_r<=blo<=Mv_r or mv_r<=bhi<=Mv_r):
- break
- else:
- i = -1
- i += delta
- if i < 0:
- return -1
- if i >= len(blocks):
- return 1
- typ, alo, ahi, blo, bhi = blocks[i]
- self.setValue(alo, "left")
- self.setValue(blo, "right")
- if i == 0:
- return -1
- if i == len(blocks)-1:
- return 1
- return 0
- def nextDiff(self):
- return self.showDiff(+1)
- def prevDiff(self):
- return self.showDiff(-1)
- def addBlock(self, typ, alo, ahi, blo=None, bhi=None):
- if bhi is None:
- bhi = ahi
- if blo is None:
- blo = alo
- self._blocks.add((typ, alo, ahi, blo, bhi))
- def paintEvent(self, event):
- w = self.width()
- h = self.height()
- p = QPainter(self)
- p.setRenderHint(p.Antialiasing)
- ps_l = float(self._pagestep['left'])
- ps_r = float(self._pagestep['right'])
- v_l = self._value['left']
- v_r = self._value['right']
- # we do integer divisions here cause the pagestep is the
- # integer number of fully displayed text lines
- scalel = self._sbar['left'].height()//ps_l
- scaler = self._sbar['right'].height()//ps_r
- ml = v_l
- Ml = v_l + ps_l
- mr = v_r
- Mr = v_r + ps_r
- p.setPen(Qt.NoPen)
- for typ, alo, ahi, blo, bhi in self._blocks:
- if not (ml<=alo<=Ml or ml<=ahi<=Ml or mr<=blo<=Mr or mr<=bhi<=Mr):
- continue
- p.save()
- p.setBrush(self.blockTypes[typ])
- path = QPainterPath()
- path.moveTo(0, scalel * (alo - ml))
- path.cubicTo(w/3.0, scalel * (alo - ml),
- 2*w/3.0, scaler * (blo - mr),
- w, scaler * (blo - mr))
- path.lineTo(w, scaler * (bhi - mr) + 2)
- path.cubicTo(2*w/3.0, scaler * (bhi - mr) + 2,
- w/3.0, scalel * (ahi - ml) + 2,
- 0, scalel * (ahi - ml) + 2)
- path.closeSubpath()
- p.drawPath(path)
- p.restore()
- def setMaximum(self, maximum, side):
- self._maximum[side] = maximum
- self.update()
- self.rangeChanged.emit(self._minimum[side], self._maximum[side], side)
- def setMinimum(self, minimum, side):
- self._minimum[side] = minimum
- self.update()
- self.rangeChanged.emit(self._minimum[side], self._maximum[side], side)
- def setRange(self, minimum, maximum, side=None):
- if side is None:
- if self.sender() == self._sbar['left']:
- side = 'left'
- else:
- side = 'right'
- self._minimum[side] = minimum
- self._maximum[side] = maximum
- self.update()
- self.rangeChanged.emit(self._minimum[side], self._maximum[side], side)
- def setValue(self, val, side=None):
- if side is None:
- if self.sender() == self._sbar['left']:
- side = 'left'
- else:
- side = 'right'
- if val != self._value[side]:
- self._value[side] = val
- self.update()
- self.valueChanged.emit(val, side)
- def setPageStep(self, pagestep, side):
- if pagestep != self._pagestep[side]:
- self._pagestep[side] = pagestep
- self.update()
- self.pageStepChanged.emit(pagestep, side)
- def syncPageStep(self):
- for side in ['left', 'right']:
- self.setPageStep(self._sbar[side].pageStep(), side)
- def resizeEvent(self, event):
- self.syncPageStep()
- def linkScrollBar(self, sb, side):
- """
- Make the block list displayer be linked to the scrollbar
- """
- if self._sbar is None:
- self._sbar = {}
- self._sbar[side] = sb
- self.setUpdatesEnabled(False)
- self.setMaximum(sb.maximum(), side)
- self.setMinimum(sb.minimum(), side)
- self.setPageStep(sb.pageStep(), side)
- self.setValue(sb.value(), side)
- self.setUpdatesEnabled(True)
- sb.valueChanged.connect(self.setValue)
- sb.rangeChanged.connect(self.setRange)
- self.valueChanged.connect(lambda v, s: side==s and sb.setValue(v))
- self.rangeChanged.connect(
- lambda v1, v2, s: side==s and sb.setRange(v1, v2))
- self.pageStepChanged.connect(
- lambda v, s: side==s and sb.setPageStep(v))
- if __name__ == '__main__':
- a = QApplication([])
- f = QFrame()
- l = QHBoxLayout(f)
- sb1 = QScrollBar()
- sb2 = QScrollBar()
- w0 = BlockList()
- w0.addBlock('-', 200, 300)
- w0.addBlock('-', 450, 460)
- w0.addBlock('x', 500, 501)
- w0.linkScrollBar(sb1)
- w1 = BlockMatch()
- w1.addBlock('+', 12, 42)
- w1.addBlock('+', 55, 142)
- w1.addBlock('-', 200, 300)
- w1.addBlock('-', 330, 400, 450, 460)
- w1.addBlock('x', 420, 450, 500, 501)
- w1.linkScrollBar(sb1, 'left')
- w1.linkScrollBar(sb2, 'right')
- w2 = BlockList()
- w2.addBlock('+', 12, 42)
- w2.addBlock('+', 55, 142)
- w2.addBlock('x', 420, 450)
- w2.linkScrollBar(sb2)
- l.addWidget(sb1)
- l.addWidget(w0)
- l.addWidget(w1)
- l.addWidget(w2)
- l.addWidget(sb2)
- w0.setRange(0, 1200)
- w0.setPageStep(100)
- w1.setRange(0, 1200, 'left')
- w1.setRange(0, 1200, 'right')
- w1.setPageStep(100, 'left')
- w1.setPageStep(100, 'right')
- w2.setRange(0, 1200)
- w2.setPageStep(100)
- print "sb1=", sb1.minimum(), sb1.maximum(), sb1.pageStep()
- print "sb2=", sb2.minimum(), sb2.maximum(), sb2.pageStep()
- f.show()
- a.exec_()