4 from PyQt4
import QtGui
5 from PyQt4
import QtCore
7 if __name__
== "__main__":
9 src
= os
.path
.join(os
.path
.dirname(__file__
), '..', '..')
10 sys
.path
.insert(0, os
.path
.abspath(src
))
12 from cola
import qtutils
13 from cola
.models
import commit
14 from cola
.views
import standard
15 from cola
.compat
import set
16 from cola
.decorators
import memoize
19 def git_dag(log_args
=None, parent
=None):
20 """Return a pre-populated git DAG widget."""
21 view
= GitDAGWidget(parent
)
22 view
.thread
.start(QtCore
.QThread
.LowPriority
)
27 class GitDAGWidget(standard
.StandardDialog
):
28 """The git-dag widget."""
29 # Keep us in scope otherwise PyQt kills the widget
33 self
._instances
.remove(self
)
35 def __init__(self
, parent
=None, args
=None):
36 standard
.StandardDialog
.__init
__(self
, parent
)
37 self
._instances
.add(self
)
39 self
.setObjectName('dag')
40 self
.setWindowTitle(self
.tr('git dag'))
41 self
.setMinimumSize(1, 1)
44 self
._graphview
= GraphView()
45 layt
= QtGui
.QHBoxLayout()
47 layt
.addWidget(self
._graphview
)
50 qtutils
.add_close_action(self
)
52 qtutils
.center_on_screen(self
)
54 self
.thread
= ReaderThread(self
, args
)
55 self
.thread
.connect(self
.thread
,
56 self
.thread
.commit_ready
,
59 def _add_commit(self
, sha1
):
60 c
= self
.thread
.repo
[sha1
]
63 def add_commits(self
, commits
):
64 self
._graphview
.add_commits(commits
)
67 self
.thread
.abort
= True
69 standard
.StandardDialog
.close(self
)
72 self
.thread
.mutex
.lock()
73 self
.thread
.stop
= True
74 self
.thread
.mutex
.unlock()
77 self
.thread
.mutex
.lock()
78 self
.thread
.stop
= False
79 self
.thread
.mutex
.unlock()
80 self
.thread
.condition
.wakeOne()
83 class ReaderThread(QtCore
.QThread
):
85 commit_ready
= QtCore
.SIGNAL('commit_ready')
87 def __init__(self
, parent
, args
):
88 super(ReaderThread
, self
).__init
__(parent
)
89 self
.repo
= commit
.RepoReader(args
=args
)
92 self
.mutex
= QtCore
.QMutex()
93 self
.condition
= QtCore
.QWaitCondition()
96 for commit
in self
.repo
:
99 self
.condition
.wait(self
.mutex
)
104 self
.emit(self
.commit_ready
, commit
.sha1
)
108 _arrow_extra
= (_arrow_size
+ 1.0) / 2.0
110 class Edge(QtGui
.QGraphicsItem
):
111 _type
= QtGui
.QGraphicsItem
.UserType
+ 2
113 def __init__(self
, source
, dest
):
114 QtGui
.QGraphicsItem
.__init
__(self
)
116 self
.source_pt
= QtCore
.QPointF()
117 self
.dest_pt
= QtCore
.QPointF()
118 self
.setAcceptedMouseButtons(QtCore
.Qt
.NoButton
)
121 self
.source
.add_edge(self
)
122 self
.dest
.add_edge(self
)
126 def type(self
, _type
=_type
):
130 if not self
.source
or not self
.dest
:
133 dest_glyph_pt
= self
.dest
.glyph().center()
134 line
= QtCore
.QLineF(
135 self
.mapFromItem(self
.source
, dest_glyph_pt
),
136 self
.mapFromItem(self
.dest
, dest_glyph_pt
))
138 length
= line
.length()
142 offset
= QtCore
.QPointF((line
.dx() * 23) / length
,
143 (line
.dy() * 9) / length
)
145 self
.prepareGeometryChange()
146 self
.source_pt
= line
.p1() + offset
147 self
.dest_pt
= line
.p2() - offset
150 def boundingRect(self
, _extra
=_arrow_extra
):
151 if not self
.source
or not self
.dest
:
152 return QtCore
.QRectF()
153 width
= self
.dest_pt
.x() - self
.source_pt
.x()
154 height
= self
.dest_pt
.y() - self
.source_pt
.y()
155 rect
= QtCore
.QRectF(self
.source_pt
, QtCore
.QSizeF(width
, height
))
156 return rect
.normalized().adjusted(-_extra
, -_extra
, _extra
, _extra
)
158 def paint(self
, painter
, option
, widget
, _arrow_size
=_arrow_size
):
159 if not self
.source
or not self
.dest
:
161 # Draw the line itself.
162 line
= QtCore
.QLineF(self
.source_pt
, self
.dest_pt
)
163 length
= line
.length()
167 painter
.setPen(QtGui
.QPen(QtCore
.Qt
.gray
, 0,
170 QtCore
.Qt
.MiterJoin
))
171 painter
.drawLine(line
)
173 # Draw the arrows if there's enough room.
174 angle
= math
.acos(line
.dx() / length
)
176 angle
= 2.0 * math
.pi
- angle
178 dest_x
= (self
.dest_pt
+
179 QtCore
.QPointF(math
.sin(angle
- math
.pi
/3.) *
181 math
.cos(angle
- math
.pi
/3.) *
183 dest_y
= (self
.dest_pt
+
184 QtCore
.QPointF(math
.sin(angle
- math
.pi
+ math
.pi
/3.) *
186 math
.cos(angle
- math
.pi
+ math
.pi
/3.) *
189 painter
.setBrush(QtCore
.Qt
.gray
)
190 painter
.drawPolygon(QtGui
.QPolygonF([line
.p2(), dest_x
, dest_y
]))
193 class Node(QtGui
.QGraphicsItem
):
194 _type
= QtGui
.QGraphicsItem
.UserType
+ 1
198 _shape
= QtGui
.QPainterPath()
199 _shape
.addRect(_width
/-2., _height
/-2., _width
, _height
)
201 _bound
= _shape
.boundingRect()
202 _glyph
= QtCore
.QRectF(-_width
/2., -9, _width
/4., 18)
204 def __init__(self
, commit
):
205 QtGui
.QGraphicsItem
.__init
__(self
)
207 self
.setFlag(QtGui
.QGraphicsItem
.ItemIsSelectable
)
209 # Starts with enough space for two tags. Any more and the node
210 # needs to be taller to accomodate.
211 if len(self
.commit
.tags
) > 1:
212 self
._height
= len(self
.commit
.tags
) * 9 + 6 # +6 padding
216 self
._colors
['bg'] = QtGui
.QColor
.fromRgb(16, 16, 16)
217 self
._colors
['selected'] = QtGui
.QColor
.fromRgb(192, 192, 16)
218 self
._colors
['outline'] = QtGui
.QColor
.fromRgb(0, 0, 0)
219 self
._colors
['node'] = QtGui
.QColor
.fromRgb(255, 111, 69)
220 self
._colors
['decorations'] = QtGui
.QColor
.fromRgb(255, 255, 42)
222 self
._grad
= QtGui
.QLinearGradient(0.0, 0.0, 0.0, self
._height
)
223 self
._grad
.setColorAt(0, self
._colors
['node'])
224 self
._grad
.setColorAt(1, self
._colors
['node'].darker())
230 def type(self
, _type
=_type
):
233 def add_edge(self
, edge
):
234 self
._edges
.append(edge
)
236 def boundingRect(self
, _bound
=_bound
):
239 def shape(self
, _shape
=_shape
):
242 def glyph(self
, _glyph
=_glyph
):
243 """Provides location of the glyph representing this node
245 The node contains a glyph (a circle or ellipse) representing the
246 node, as well as other text alongside the glyph. Knowing the
247 location of the glyph, rather than the entire node allows us to
248 make edges point at the center of the glyph, rather than at the
249 center of the entire node.
253 def paint(self
, painter
, option
, widget
):
254 if self
.isSelected():
255 painter
.setPen(self
._colors
['selected'])
257 painter
.setPen(self
._colors
['outline'])
258 painter
.setBrush(self
._grad
)
261 painter
.drawEllipse(self
.glyph())
262 sha1_text
= self
.commit
.sha1
263 font
= painter
.font()
265 painter
.setFont(font
)
266 painter
.setPen(QtCore
.Qt
.black
)
268 text_options
= QtGui
.QTextOption()
269 text_options
.setAlignment(QtCore
.Qt
.AlignCenter
)
270 painter
.drawText(self
.glyph(), sha1_text
, text_options
)
273 if not self
.commit
.tags
:
275 # Those 2's affecting width are just for padding
276 text_box
= QtCore
.QRectF(-self
._width
/4.+2, -self
._height
/2.,
277 self
._width
*(3/4.)-2, self
._height
)
278 painter
.setBrush(self
._colors
['decorations'])
279 painter
.drawRoundedRect(text_box
, 4, 4)
280 tag_text
= "\n".join(self
.commit
.tags
)
281 text_options
.setAlignment(QtCore
.Qt
.AlignVCenter
)
282 # A bit of padding for the text
283 painter
.translate(2.,0.)
284 painter
.drawText(text_box
, tag_text
, text_options
)
286 def mousePressEvent(self
, event
):
287 self
.selected
= self
.isSelected()
289 QtGui
.QGraphicsItem
.mousePressEvent(self
, event
)
291 def mouseMoveEvent(self
, event
):
294 QtGui
.QGraphicsItem
.mouseMoveEvent(self
, event
)
295 for node
in self
.scene().selectedItems():
296 for edge
in node
._edges
:
298 self
.scene().update()
300 def mouseReleaseEvent(self
, event
):
301 QtGui
.QGraphicsItem
.mouseReleaseEvent(self
, event
)
304 and event
.button() == QtCore
.Qt
.LeftButton
):
305 self
.setSelected(False)
313 class GraphView(QtGui
.QGraphicsView
):
315 QtGui
.QGraphicsView
.__init
__(self
)
331 self
._panning
= False
332 self
._last
_mouse
= [0, 0]
335 self
.scale(self
._zoom
, self
._zoom
)
336 self
.setDragMode(self
.RubberBandDrag
)
339 scene
= QtGui
.QGraphicsScene(self
)
340 scene
.setItemIndexMethod(QtGui
.QGraphicsScene
.NoIndex
)
341 scene
.setSceneRect(-size
/4, -size
/2, size
/2, size
)
344 self
.setCacheMode(QtGui
.QGraphicsView
.CacheBackground
)
345 self
.setRenderHint(QtGui
.QPainter
.Antialiasing
)
346 self
.setTransformationAnchor(QtGui
.QGraphicsView
.AnchorUnderMouse
)
347 self
.setResizeAnchor(QtGui
.QGraphicsView
.AnchorViewCenter
)
348 self
.setBackgroundColor()
350 def add_commits(self
, commits
):
351 """Traverse commits and add them to the view."""
356 def keyPressEvent(self
, event
):
359 if key
== QtCore
.Qt
.Key_Plus
:
360 self
._scale
_view
(1.5)
361 elif key
== QtCore
.Qt
.Key_Minus
:
362 self
._scale
_view
(1 / 1.5)
363 elif key
== QtCore
.Qt
.Key_F
:
365 elif event
.key() == QtCore
.Qt
.Key_Z
:
366 self
._move
_nodes
_to
_mouse
_position
()
368 QtGui
.QGraphicsView
.keyPressEvent(self
, event
)
371 """Fit selected items into the viewport"""
373 items
= self
.scene().selectedItems()
375 rect
= self
.scene().itemsBoundingRect()
383 item_rect
= item
.boundingRect()
384 xoff
= item_rect
.width()
385 yoff
= item_rect
.height()
386 xmin
= min(xmin
, pos
.x())
387 ymin
= min(ymin
, pos
.y())
388 xmax
= max(xmax
, pos
.x()+xoff
)
389 ymax
= max(ymax
, pos
.y()+yoff
)
390 rect
= QtCore
.QRectF(xmin
, ymin
, xmax
-xmin
, ymax
-ymin
)
392 rect
.setX(rect
.x() - adjust
)
393 rect
.setY(rect
.y() - adjust
)
394 rect
.setHeight(rect
.height() + adjust
)
395 rect
.setWidth(rect
.width() + adjust
)
396 self
.fitInView(rect
, QtCore
.Qt
.KeepAspectRatio
)
397 self
.scene().invalidate()
399 def _save_selection(self
, event
):
400 if event
.button() != QtCore
.Qt
.LeftButton
:
402 elif QtCore
.Qt
.ShiftModifier
!= event
.modifiers():
404 self
._selected
= [ i
for i
in self
._items
if i
.isSelected() ]
406 def _restore_selection(self
, event
):
407 if QtCore
.Qt
.ShiftModifier
!= event
.modifiers():
409 for item
in self
._selected
:
413 item
.setSelected(True)
415 def _handle_event(self
, eventhandler
, event
):
417 self
._save
_selection
(event
)
418 eventhandler(self
, event
)
419 self
._restore
_selection
(event
)
421 def mousePressEvent(self
, event
):
422 if event
.button() == QtCore
.Qt
.MidButton
:
424 self
._mouse
_start
= [pos
.x(), pos
.y()]
425 self
._saved
_matrix
= QtGui
.QMatrix(self
.matrix())
428 self
._handle
_event
(QtGui
.QGraphicsView
.mousePressEvent
, event
)
430 def mouseMoveEvent(self
, event
):
431 pos
= self
.mapToScene(event
.pos())
435 self
._last
_mouse
[0] = pos
.x()
436 self
._last
_mouse
[1] = pos
.y()
437 self
._handle
_event
(QtGui
.QGraphicsView
.mouseMoveEvent
, event
)
439 def mouseReleaseEvent(self
, event
):
440 if event
.button() == QtCore
.Qt
.MidButton
:
441 self
._panning
= False
443 self
._handle
_event
(QtGui
.QGraphicsView
.mouseReleaseEvent
, event
)
446 def _pan(self
, event
):
448 dx
= pos
.x() - self
._mouse
_start
[0]
449 dy
= pos
.y() - self
._mouse
_start
[1]
451 if dx
== 0 and dy
== 0:
454 rect
= QtCore
.QRect(0, 0, abs(dx
), abs(dy
))
455 delta
= self
.mapToScene(rect
).boundingRect()
465 matrix
= QtGui
.QMatrix(self
._saved
_matrix
).translate(tx
, ty
)
466 self
.setTransformationAnchor(QtGui
.QGraphicsView
.NoAnchor
)
467 self
.setMatrix(matrix
)
469 def wheelEvent(self
, event
):
470 """Handle Qt mouse wheel events."""
471 if event
.modifiers() == QtCore
.Qt
.ControlModifier
:
472 self
._wheel
_zoom
(event
)
474 self
._wheel
_pan
(event
)
476 def _wheel_zoom(self
, event
):
477 """Handle mouse wheel zooming."""
478 zoom
= math
.pow(2.0, event
.delta() / 512.0)
479 factor
= (self
.matrix()
481 .mapRect(QtCore
.QRectF(0.0, 0.0, 1.0, 1.0))
483 if factor
< 0.014 or factor
> 42.0:
485 self
.setTransformationAnchor(QtGui
.QGraphicsView
.AnchorUnderMouse
)
487 self
.scale(zoom
, zoom
)
489 def _wheel_pan(self
, event
):
490 """Handle mouse wheel panning."""
492 if event
.delta() < 0:
496 pan_rect
= QtCore
.QRectF(0.0, 0.0, 1.0, 1.0)
497 factor
= 1.0 / self
.matrix().mapRect(pan_rect
).width()
499 if event
.orientation() == QtCore
.Qt
.Vertical
:
500 matrix
= self
.matrix().translate(0, s
* factor
)
502 matrix
= self
.matrix().translate(s
* factor
, 0)
503 self
.setTransformationAnchor(QtGui
.QGraphicsView
.NoAnchor
)
504 self
.setMatrix(matrix
)
506 def _move_nodes_to_mouse_position(self
):
507 items
= self
.scene().selectedItems()
512 min_distance
= sys
.maxint
514 width
= item
.boundingRect().width()
516 tmp_dx
= self
._last
_mouse
[0] - pos
.x() - width
/2.0
517 tmp_dy
= self
._last
_mouse
[1] - pos
.y() - width
/2.0
518 distance
= math
.sqrt(tmp_dx
** 2 + tmp_dy
** 2)
519 if distance
< min_distance
:
520 min_distance
= distance
527 item
.setPos( x
+ dx
, y
+ dy
)
529 def setBackgroundColor(self
, color
=None):
530 # To set a gradient background brush we need to use StretchToDeviceMode
531 # but that seems to be segfaulting. Use a solid background.
533 color
= QtGui
.QColor(50,50,50)
534 self
.setBackgroundBrush(color
)
536 def _scale_view(self
, scale
):
537 factor
= (self
.matrix().scale(scale
, scale
)
538 .mapRect(QtCore
.QRectF(0, 0, 1, 1))
540 if factor
< 0.07 or factor
> 100:
543 self
.scale(scale
, scale
)
545 def add(self
, commits
):
547 for commit
in commits
:
548 self
._commits
[commit
.sha1
] = commit
549 for p
in commit
.parents
:
550 children
= self
._children
.setdefault(p
, [])
551 children
.append(commit
.sha1
)
554 self
._nodes
[commit
.sha1
] = node
555 self
._items
.append(node
)
557 def link(self
, commits
):
558 """Create edges linking commits with their parents"""
560 for commit
in commits
:
561 children
= self
._children
.get(commit
.sha1
, None)
565 commit_node
= self
._nodes
[commit
.sha1
]
566 for child_sha1
in children
:
567 child_node
= self
._nodes
[child_sha1
]
568 edge
= Edge(commit_node
, child_node
)
571 def layout(self
, commits
):
577 for commit
in commits
:
578 if commit
.sha1
not in self
._children
:
579 self
._loc
[commit
.sha1
] = (xpos
, ypos
)
580 node
= self
._nodes
.get(commit
.sha1
, None)
581 node
.setPos(xpos
, ypos
)
583 gxmax
= max(xpos
, gxmax
)
587 for sha1
in self
._children
[commit
.sha1
]:
588 loc
= self
._loc
[sha1
]
591 xmax
= min(xmax
, loc
[0])
592 ymax
= max(ymax
, loc
[1])
593 gxmax
= max(gxmax
, xmax
)
594 gymax
= max(gymax
, ymax
)
598 gymax
= max(gymax
, ymax
)
599 if ymax
in self
._cols
:
600 xmax
= max(xmax
, self
._cols
[ymax
] + self
._xoff
)
601 gxmax
= max(gxmax
, xmax
)
602 self
._cols
[ymax
] = xmax
605 self
._cols
[ymax
] = xmax
608 self
._loc
[sha1
] = (xmax
, ymax
)
609 node
= self
._nodes
[sha1
]
610 node
.setPos(xmax
, ymax
)
616 self
.scene().setSceneRect(-xpad
, -ypad
, gxmax
+xpad
, gymax
+ypad
)
618 if __name__
== "__main__":
619 app
= QtGui
.QApplication(sys
.argv
)
621 sys
.exit(app
.exec_())