PageRenderTime 70ms CodeModel.GetById 43ms app.highlight 22ms RepoModel.GetById 1ms app.codeStats 0ms

/tortoisehg/hgqt/blockmatcher.py

https://bitbucket.org/tortoisehg/hgtk/
Python | 367 lines | 345 code | 3 blank | 19 comment | 0 complexity | d68934e421eeabc39e974594bd5ca15b MD5 | raw file
  1# Copyright (c) 2003-2010 LOGILAB S.A. (Paris, FRANCE).
  2# http://www.logilab.fr/ -- mailto:contact@logilab.fr
  3#
  4# This program is free software; you can redistribute it and/or modify it under
  5# the terms of the GNU General Public License as published by the Free Software
  6# Foundation; either version 2 of the License, or (at your option) any later
  7# version.
  8#
  9# This program is distributed in the hope that it will be useful, but WITHOUT
 10# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 11# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 12#
 13# You should have received a copy of the GNU General Public License along with
 14# this program; if not, write to the Free Software Foundation, Inc.,
 15# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 16
 17"""
 18Qt4 widgets to display diffs as blocks
 19"""
 20import sys, os
 21
 22from PyQt4.QtGui import *
 23from PyQt4.QtCore import *
 24
 25class BlockList(QWidget):
 26    """
 27    A simple widget to be 'linked' to the scrollbar of a diff text
 28    view.
 29
 30    It represents diff blocks with coloured rectangles, showing
 31    currently viewed area by a semi-transparant rectangle sliding
 32    above them.
 33    """
 34
 35    rangeChanged = pyqtSignal(int,int)
 36    valueChanged = pyqtSignal(int)
 37    pageStepChanged = pyqtSignal(int)
 38
 39    def __init__(self, *args):
 40        QWidget.__init__(self, *args)
 41        self._blocks = set()
 42        self._minimum = 0
 43        self._maximum = 100
 44        self.blockTypes = {'+': QColor(0xA0, 0xFF, 0xB0, ),#0xa5),
 45                           '-': QColor(0xFF, 0xA0, 0xA0, ),#0xa5),
 46                           'x': QColor(0xA0, 0xA0, 0xFF, ),#0xa5),
 47                           }
 48        self._sbar = None
 49        self._value = 0
 50        self._pagestep = 10
 51        self._vrectcolor = QColor(0x00, 0x00, 0x55, 0x25)
 52        self._vrectbordercolor = self._vrectcolor.darker()
 53        self.sizePolicy().setControlType(QSizePolicy.Slider)
 54        self.setMinimumWidth(20)
 55
 56    def clear(self):
 57        self._blocks = set()
 58
 59    def addBlock(self, typ, alo, ahi):
 60        self._blocks.add((typ, alo, ahi))
 61
 62    def setMaximum(self, maximum):
 63        self._maximum = maximum
 64        self.update()
 65        self.rangeChanged.emit(self._minimum, self._maximum)
 66
 67    def setMinimum(self, minimum):
 68        self._minimum = minimum
 69        self.update()
 70        self.rangeChanged.emit(self._minimum, self._maximum)
 71
 72    def setRange(self, minimum, maximum):
 73        if minimum == maximum:
 74            return
 75        self._minimum = minimum
 76        self._maximum = maximum
 77        self.update()
 78        self.rangeChanged.emit(self._minimum, self._maximum)
 79
 80    def setValue(self, val):
 81        if val != self._value:
 82            self._value = val
 83            self.update()
 84            self.valueChanged.emit(val)
 85
 86    def setPageStep(self, pagestep):
 87        if pagestep != self._pagestep:
 88            self._pagestep = pagestep
 89            self.update()
 90            self.pageStepChanged.emit(pagestep)
 91
 92    def linkScrollBar(self, sbar):
 93        """
 94        Make the block list displayer be linked to the scrollbar
 95        """
 96        self._sbar = sbar
 97        self.setUpdatesEnabled(False)
 98        self.setMaximum(sbar.maximum())
 99        self.setMinimum(sbar.minimum())
100        self.setPageStep(sbar.pageStep())
101        self.setValue(sbar.value())
102        self.setUpdatesEnabled(True)
103
104        sbar.valueChanged.connect(self.setValue)
105        sbar.rangeChanged.connect(self.setRange)
106
107        self.valueChanged.connect(sbar.setValue)
108        self.rangeChanged.connect(lambda x, y: sbar.setRange(x,y))
109        self.pageStepChanged.connect(lambda x: sbar.setPageStep(x))
110
111    def syncPageStep(self):
112        self.setPageStep(self._sbar.pageStep())
113
114    def paintEvent(self, event):
115        w = self.width() - 1
116        h = self.height()
117        p = QPainter(self)
118        p.scale(1.0, float(h)/(self._maximum - self._minimum + self._pagestep))
119        p.setPen(Qt.NoPen)
120        for typ, alo, ahi in self._blocks:
121            p.save()
122            p.setBrush(self.blockTypes[typ])
123            p.drawRect(1, alo, w-1, ahi-alo)
124            p.restore()
125
126        p.save()
127        p.setPen(self._vrectbordercolor)
128        p.setBrush(self._vrectcolor)
129        p.drawRect(0, self._value, w, self._pagestep)
130        p.restore()
131
132class BlockMatch(BlockList):
133    """
134    A simpe widget to be linked to 2 file views (text areas),
135    displaying 2 versions of a same file (diff).
136
137    It will show graphically matching diff blocks between the 2 text
138    areas.
139    """
140
141    rangeChanged = pyqtSignal(int, int, QString)
142    valueChanged = pyqtSignal(int, QString)
143    pageStepChanged = pyqtSignal(int, QString)
144
145    def __init__(self, *args):
146        QWidget.__init__(self, *args)
147        self._blocks = set()
148        self._minimum = {'left': 0, 'right': 0}
149        self._maximum = {'left': 100, 'right': 100}
150        self.blockTypes = {'+': QColor(0xA0, 0xFF, 0xB0, ),#0xa5),
151                           '-': QColor(0xFF, 0xA0, 0xA0, ),#0xa5),
152                           'x': QColor(0xA0, 0xA0, 0xFF, ),#0xa5),
153                           }
154        self._sbar = {}
155        self._value =  {'left': 0, 'right': 0}
156        self._pagestep =  {'left': 10, 'right': 10}
157        self._vrectcolor = QColor(0x00, 0x00, 0x55, 0x25)
158        self._vrectbordercolor = self._vrectcolor.darker()
159        self.sizePolicy().setControlType(QSizePolicy.Slider)
160        self.setMinimumWidth(20)
161
162    def nDiffs(self):
163        return len(self._blocks)
164
165    def showDiff(self, delta):
166        ps_l = float(self._pagestep['left'])
167        ps_r = float(self._pagestep['right'])
168        mv_l = self._value['left']
169        mv_r = self._value['right']
170        Mv_l = mv_l + ps_l
171        Mv_r = mv_r + ps_r
172
173        vblocks = []
174        blocks = sorted(self._blocks, key=lambda x:(x[1],x[3],x[2],x[4]))
175        for i, (typ, alo, ahi, blo, bhi) in enumerate(blocks):
176            if (mv_l<=alo<=Mv_l or mv_l<=ahi<=Mv_l or
177                mv_r<=blo<=Mv_r or mv_r<=bhi<=Mv_r):
178                break
179        else:
180            i = -1
181        i += delta
182
183        if i < 0:
184            return -1
185        if i >= len(blocks):
186            return 1
187        typ, alo, ahi, blo, bhi = blocks[i]
188        self.setValue(alo, "left")
189        self.setValue(blo, "right")
190        if i == 0:
191            return -1
192        if i == len(blocks)-1:
193            return 1
194        return 0
195
196    def nextDiff(self):
197        return self.showDiff(+1)
198
199    def prevDiff(self):
200        return self.showDiff(-1)
201
202    def addBlock(self, typ, alo, ahi, blo=None, bhi=None):
203        if bhi is None:
204            bhi = ahi
205        if blo is None:
206            blo = alo
207        self._blocks.add((typ, alo, ahi, blo, bhi))
208
209    def paintEvent(self, event):
210        w = self.width()
211        h = self.height()
212        p = QPainter(self)
213        p.setRenderHint(p.Antialiasing)
214
215        ps_l = float(self._pagestep['left'])
216        ps_r = float(self._pagestep['right'])
217        v_l = self._value['left']
218        v_r = self._value['right']
219
220        # we do integer divisions here cause the pagestep is the
221        # integer number of fully displayed text lines
222        scalel = self._sbar['left'].height()//ps_l
223        scaler = self._sbar['right'].height()//ps_r
224
225        ml = v_l
226        Ml = v_l + ps_l
227        mr = v_r
228        Mr = v_r + ps_r
229
230        p.setPen(Qt.NoPen)
231        for typ, alo, ahi, blo, bhi in self._blocks:
232            if not (ml<=alo<=Ml or ml<=ahi<=Ml or mr<=blo<=Mr or mr<=bhi<=Mr):
233                continue
234            p.save()
235            p.setBrush(self.blockTypes[typ])
236
237            path = QPainterPath()
238            path.moveTo(0, scalel * (alo - ml))
239            path.cubicTo(w/3.0, scalel * (alo - ml),
240                         2*w/3.0, scaler * (blo - mr),
241                         w, scaler * (blo - mr))
242            path.lineTo(w, scaler * (bhi - mr) + 2)
243            path.cubicTo(2*w/3.0, scaler * (bhi - mr) + 2,
244                         w/3.0, scalel * (ahi - ml) + 2,
245                         0, scalel * (ahi - ml) + 2)
246            path.closeSubpath()
247            p.drawPath(path)
248
249            p.restore()
250
251    def setMaximum(self, maximum, side):
252        self._maximum[side] = maximum
253        self.update()
254        self.rangeChanged.emit(self._minimum[side], self._maximum[side], side)
255
256    def setMinimum(self, minimum, side):
257        self._minimum[side] = minimum
258        self.update()
259        self.rangeChanged.emit(self._minimum[side], self._maximum[side], side)
260
261    def setRange(self, minimum, maximum, side=None):
262        if side is None:
263            if self.sender() == self._sbar['left']:
264                side = 'left'
265            else:
266                side = 'right'
267        self._minimum[side] = minimum
268        self._maximum[side] = maximum
269        self.update()
270        self.rangeChanged.emit(self._minimum[side], self._maximum[side], side)
271
272    def setValue(self, val, side=None):
273        if side is None:
274            if self.sender() == self._sbar['left']:
275                side = 'left'
276            else:
277                side = 'right'
278        if val != self._value[side]:
279            self._value[side] = val
280            self.update()
281            self.valueChanged.emit(val, side)
282
283    def setPageStep(self, pagestep, side):
284        if pagestep != self._pagestep[side]:
285            self._pagestep[side] = pagestep
286            self.update()
287            self.pageStepChanged.emit(pagestep, side)
288
289    def syncPageStep(self):
290        for side in ['left', 'right']:
291            self.setPageStep(self._sbar[side].pageStep(), side)
292
293    def resizeEvent(self, event):
294        self.syncPageStep()
295
296    def linkScrollBar(self, sb, side):
297        """
298        Make the block list displayer be linked to the scrollbar
299        """
300        if self._sbar is None:
301            self._sbar = {}
302        self._sbar[side] = sb
303        self.setUpdatesEnabled(False)
304        self.setMaximum(sb.maximum(), side)
305        self.setMinimum(sb.minimum(), side)
306        self.setPageStep(sb.pageStep(), side)
307        self.setValue(sb.value(), side)
308        self.setUpdatesEnabled(True)
309        sb.valueChanged.connect(self.setValue)
310        sb.rangeChanged.connect(self.setRange)
311
312        self.valueChanged.connect(lambda v, s: side==s and sb.setValue(v))
313        self.rangeChanged.connect(
314                     lambda v1, v2, s: side==s and sb.setRange(v1, v2))
315        self.pageStepChanged.connect(
316                     lambda v, s: side==s and sb.setPageStep(v))
317
318if __name__ == '__main__':
319    a = QApplication([])
320    f = QFrame()
321    l = QHBoxLayout(f)
322
323    sb1 = QScrollBar()
324    sb2 = QScrollBar()
325
326    w0 = BlockList()
327    w0.addBlock('-', 200, 300)
328    w0.addBlock('-', 450, 460)
329    w0.addBlock('x', 500, 501)
330    w0.linkScrollBar(sb1)
331
332    w1 = BlockMatch()
333    w1.addBlock('+', 12, 42)
334    w1.addBlock('+', 55, 142)
335    w1.addBlock('-', 200, 300)
336    w1.addBlock('-', 330, 400, 450, 460)
337    w1.addBlock('x', 420, 450, 500, 501)
338    w1.linkScrollBar(sb1, 'left')
339    w1.linkScrollBar(sb2, 'right')
340
341    w2 = BlockList()
342    w2.addBlock('+', 12, 42)
343    w2.addBlock('+', 55, 142)
344    w2.addBlock('x', 420, 450)
345    w2.linkScrollBar(sb2)
346
347    l.addWidget(sb1)
348    l.addWidget(w0)
349    l.addWidget(w1)
350    l.addWidget(w2)
351    l.addWidget(sb2)
352
353    w0.setRange(0, 1200)
354    w0.setPageStep(100)
355    w1.setRange(0, 1200, 'left')
356    w1.setRange(0, 1200, 'right')
357    w1.setPageStep(100, 'left')
358    w1.setPageStep(100, 'right')
359    w2.setRange(0, 1200)
360    w2.setPageStep(100)
361
362    print "sb1=", sb1.minimum(), sb1.maximum(), sb1.pageStep()
363    print "sb2=", sb2.minimum(), sb2.maximum(), sb2.pageStep()
364
365    f.show()
366    a.exec_()
367