/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. Qt4 widgets to display diffs as blocks
  18. """
  19. import sys, os
  20. from PyQt4.QtGui import *
  21. from PyQt4.QtCore import *
  22. class BlockList(QWidget):
  23. """
  24. A simple widget to be 'linked' to the scrollbar of a diff text
  25. view.
  26. It represents diff blocks with coloured rectangles, showing
  27. currently viewed area by a semi-transparant rectangle sliding
  28. above them.
  29. """
  30. rangeChanged = pyqtSignal(int,int)
  31. valueChanged = pyqtSignal(int)
  32. pageStepChanged = pyqtSignal(int)
  33. def __init__(self, *args):
  34. QWidget.__init__(self, *args)
  35. self._blocks = set()
  36. self._minimum = 0
  37. self._maximum = 100
  38. self.blockTypes = {'+': QColor(0xA0, 0xFF, 0xB0, ),#0xa5),
  39. '-': QColor(0xFF, 0xA0, 0xA0, ),#0xa5),
  40. 'x': QColor(0xA0, 0xA0, 0xFF, ),#0xa5),
  41. }
  42. self._sbar = None
  43. self._value = 0
  44. self._pagestep = 10
  45. self._vrectcolor = QColor(0x00, 0x00, 0x55, 0x25)
  46. self._vrectbordercolor = self._vrectcolor.darker()
  47. self.sizePolicy().setControlType(QSizePolicy.Slider)
  48. self.setMinimumWidth(20)
  49. def clear(self):
  50. self._blocks = set()
  51. def addBlock(self, typ, alo, ahi):
  52. self._blocks.add((typ, alo, ahi))
  53. def setMaximum(self, maximum):
  54. self._maximum = maximum
  55. self.update()
  56. self.rangeChanged.emit(self._minimum, self._maximum)
  57. def setMinimum(self, minimum):
  58. self._minimum = minimum
  59. self.update()
  60. self.rangeChanged.emit(self._minimum, self._maximum)
  61. def setRange(self, minimum, maximum):
  62. if minimum == maximum:
  63. return
  64. self._minimum = minimum
  65. self._maximum = maximum
  66. self.update()
  67. self.rangeChanged.emit(self._minimum, self._maximum)
  68. def setValue(self, val):
  69. if val != self._value:
  70. self._value = val
  71. self.update()
  72. self.valueChanged.emit(val)
  73. def setPageStep(self, pagestep):
  74. if pagestep != self._pagestep:
  75. self._pagestep = pagestep
  76. self.update()
  77. self.pageStepChanged.emit(pagestep)
  78. def linkScrollBar(self, sbar):
  79. """
  80. Make the block list displayer be linked to the scrollbar
  81. """
  82. self._sbar = sbar
  83. self.setUpdatesEnabled(False)
  84. self.setMaximum(sbar.maximum())
  85. self.setMinimum(sbar.minimum())
  86. self.setPageStep(sbar.pageStep())
  87. self.setValue(sbar.value())
  88. self.setUpdatesEnabled(True)
  89. sbar.valueChanged.connect(self.setValue)
  90. sbar.rangeChanged.connect(self.setRange)
  91. self.valueChanged.connect(sbar.setValue)
  92. self.rangeChanged.connect(lambda x, y: sbar.setRange(x,y))
  93. self.pageStepChanged.connect(lambda x: sbar.setPageStep(x))
  94. def syncPageStep(self):
  95. self.setPageStep(self._sbar.pageStep())
  96. def paintEvent(self, event):
  97. w = self.width() - 1
  98. h = self.height()
  99. p = QPainter(self)
  100. p.scale(1.0, float(h)/(self._maximum - self._minimum + self._pagestep))
  101. p.setPen(Qt.NoPen)
  102. for typ, alo, ahi in self._blocks:
  103. p.save()
  104. p.setBrush(self.blockTypes[typ])
  105. p.drawRect(1, alo, w-1, ahi-alo)
  106. p.restore()
  107. p.save()
  108. p.setPen(self._vrectbordercolor)
  109. p.setBrush(self._vrectcolor)
  110. p.drawRect(0, self._value, w, self._pagestep)
  111. p.restore()
  112. class BlockMatch(BlockList):
  113. """
  114. A simpe widget to be linked to 2 file views (text areas),
  115. displaying 2 versions of a same file (diff).
  116. It will show graphically matching diff blocks between the 2 text
  117. areas.
  118. """
  119. rangeChanged = pyqtSignal(int, int, QString)
  120. valueChanged = pyqtSignal(int, QString)
  121. pageStepChanged = pyqtSignal(int, QString)
  122. def __init__(self, *args):
  123. QWidget.__init__(self, *args)
  124. self._blocks = set()
  125. self._minimum = {'left': 0, 'right': 0}
  126. self._maximum = {'left': 100, 'right': 100}
  127. self.blockTypes = {'+': QColor(0xA0, 0xFF, 0xB0, ),#0xa5),
  128. '-': QColor(0xFF, 0xA0, 0xA0, ),#0xa5),
  129. 'x': QColor(0xA0, 0xA0, 0xFF, ),#0xa5),
  130. }
  131. self._sbar = {}
  132. self._value = {'left': 0, 'right': 0}
  133. self._pagestep = {'left': 10, 'right': 10}
  134. self._vrectcolor = QColor(0x00, 0x00, 0x55, 0x25)
  135. self._vrectbordercolor = self._vrectcolor.darker()
  136. self.sizePolicy().setControlType(QSizePolicy.Slider)
  137. self.setMinimumWidth(20)
  138. def nDiffs(self):
  139. return len(self._blocks)
  140. def showDiff(self, delta):
  141. ps_l = float(self._pagestep['left'])
  142. ps_r = float(self._pagestep['right'])
  143. mv_l = self._value['left']
  144. mv_r = self._value['right']
  145. Mv_l = mv_l + ps_l
  146. Mv_r = mv_r + ps_r
  147. vblocks = []
  148. blocks = sorted(self._blocks, key=lambda x:(x[1],x[3],x[2],x[4]))
  149. for i, (typ, alo, ahi, blo, bhi) in enumerate(blocks):
  150. if (mv_l<=alo<=Mv_l or mv_l<=ahi<=Mv_l or
  151. mv_r<=blo<=Mv_r or mv_r<=bhi<=Mv_r):
  152. break
  153. else:
  154. i = -1
  155. i += delta
  156. if i < 0:
  157. return -1
  158. if i >= len(blocks):
  159. return 1
  160. typ, alo, ahi, blo, bhi = blocks[i]
  161. self.setValue(alo, "left")
  162. self.setValue(blo, "right")
  163. if i == 0:
  164. return -1
  165. if i == len(blocks)-1:
  166. return 1
  167. return 0
  168. def nextDiff(self):
  169. return self.showDiff(+1)
  170. def prevDiff(self):
  171. return self.showDiff(-1)
  172. def addBlock(self, typ, alo, ahi, blo=None, bhi=None):
  173. if bhi is None:
  174. bhi = ahi
  175. if blo is None:
  176. blo = alo
  177. self._blocks.add((typ, alo, ahi, blo, bhi))
  178. def paintEvent(self, event):
  179. w = self.width()
  180. h = self.height()
  181. p = QPainter(self)
  182. p.setRenderHint(p.Antialiasing)
  183. ps_l = float(self._pagestep['left'])
  184. ps_r = float(self._pagestep['right'])
  185. v_l = self._value['left']
  186. v_r = self._value['right']
  187. # we do integer divisions here cause the pagestep is the
  188. # integer number of fully displayed text lines
  189. scalel = self._sbar['left'].height()//ps_l
  190. scaler = self._sbar['right'].height()//ps_r
  191. ml = v_l
  192. Ml = v_l + ps_l
  193. mr = v_r
  194. Mr = v_r + ps_r
  195. p.setPen(Qt.NoPen)
  196. for typ, alo, ahi, blo, bhi in self._blocks:
  197. if not (ml<=alo<=Ml or ml<=ahi<=Ml or mr<=blo<=Mr or mr<=bhi<=Mr):
  198. continue
  199. p.save()
  200. p.setBrush(self.blockTypes[typ])
  201. path = QPainterPath()
  202. path.moveTo(0, scalel * (alo - ml))
  203. path.cubicTo(w/3.0, scalel * (alo - ml),
  204. 2*w/3.0, scaler * (blo - mr),
  205. w, scaler * (blo - mr))
  206. path.lineTo(w, scaler * (bhi - mr) + 2)
  207. path.cubicTo(2*w/3.0, scaler * (bhi - mr) + 2,
  208. w/3.0, scalel * (ahi - ml) + 2,
  209. 0, scalel * (ahi - ml) + 2)
  210. path.closeSubpath()
  211. p.drawPath(path)
  212. p.restore()
  213. def setMaximum(self, maximum, side):
  214. self._maximum[side] = maximum
  215. self.update()
  216. self.rangeChanged.emit(self._minimum[side], self._maximum[side], side)
  217. def setMinimum(self, minimum, side):
  218. self._minimum[side] = minimum
  219. self.update()
  220. self.rangeChanged.emit(self._minimum[side], self._maximum[side], side)
  221. def setRange(self, minimum, maximum, side=None):
  222. if side is None:
  223. if self.sender() == self._sbar['left']:
  224. side = 'left'
  225. else:
  226. side = 'right'
  227. self._minimum[side] = minimum
  228. self._maximum[side] = maximum
  229. self.update()
  230. self.rangeChanged.emit(self._minimum[side], self._maximum[side], side)
  231. def setValue(self, val, side=None):
  232. if side is None:
  233. if self.sender() == self._sbar['left']:
  234. side = 'left'
  235. else:
  236. side = 'right'
  237. if val != self._value[side]:
  238. self._value[side] = val
  239. self.update()
  240. self.valueChanged.emit(val, side)
  241. def setPageStep(self, pagestep, side):
  242. if pagestep != self._pagestep[side]:
  243. self._pagestep[side] = pagestep
  244. self.update()
  245. self.pageStepChanged.emit(pagestep, side)
  246. def syncPageStep(self):
  247. for side in ['left', 'right']:
  248. self.setPageStep(self._sbar[side].pageStep(), side)
  249. def resizeEvent(self, event):
  250. self.syncPageStep()
  251. def linkScrollBar(self, sb, side):
  252. """
  253. Make the block list displayer be linked to the scrollbar
  254. """
  255. if self._sbar is None:
  256. self._sbar = {}
  257. self._sbar[side] = sb
  258. self.setUpdatesEnabled(False)
  259. self.setMaximum(sb.maximum(), side)
  260. self.setMinimum(sb.minimum(), side)
  261. self.setPageStep(sb.pageStep(), side)
  262. self.setValue(sb.value(), side)
  263. self.setUpdatesEnabled(True)
  264. sb.valueChanged.connect(self.setValue)
  265. sb.rangeChanged.connect(self.setRange)
  266. self.valueChanged.connect(lambda v, s: side==s and sb.setValue(v))
  267. self.rangeChanged.connect(
  268. lambda v1, v2, s: side==s and sb.setRange(v1, v2))
  269. self.pageStepChanged.connect(
  270. lambda v, s: side==s and sb.setPageStep(v))
  271. if __name__ == '__main__':
  272. a = QApplication([])
  273. f = QFrame()
  274. l = QHBoxLayout(f)
  275. sb1 = QScrollBar()
  276. sb2 = QScrollBar()
  277. w0 = BlockList()
  278. w0.addBlock('-', 200, 300)
  279. w0.addBlock('-', 450, 460)
  280. w0.addBlock('x', 500, 501)
  281. w0.linkScrollBar(sb1)
  282. w1 = BlockMatch()
  283. w1.addBlock('+', 12, 42)
  284. w1.addBlock('+', 55, 142)
  285. w1.addBlock('-', 200, 300)
  286. w1.addBlock('-', 330, 400, 450, 460)
  287. w1.addBlock('x', 420, 450, 500, 501)
  288. w1.linkScrollBar(sb1, 'left')
  289. w1.linkScrollBar(sb2, 'right')
  290. w2 = BlockList()
  291. w2.addBlock('+', 12, 42)
  292. w2.addBlock('+', 55, 142)
  293. w2.addBlock('x', 420, 450)
  294. w2.linkScrollBar(sb2)
  295. l.addWidget(sb1)
  296. l.addWidget(w0)
  297. l.addWidget(w1)
  298. l.addWidget(w2)
  299. l.addWidget(sb2)
  300. w0.setRange(0, 1200)
  301. w0.setPageStep(100)
  302. w1.setRange(0, 1200, 'left')
  303. w1.setRange(0, 1200, 'right')
  304. w1.setPageStep(100, 'left')
  305. w1.setPageStep(100, 'right')
  306. w2.setRange(0, 1200)
  307. w2.setPageStep(100)
  308. print "sb1=", sb1.minimum(), sb1.maximum(), sb1.pageStep()
  309. print "sb2=", sb2.minimum(), sb2.maximum(), sb2.pageStep()
  310. f.show()
  311. a.exec_()