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:
497 pan_rect
= QtCore
.QRectF(0.0, 0.0, 1.0, 1.0)
498 factor
= 1.0 / self
.matrix().mapRect(pan_rect
).width()
500 if event
.orientation() == QtCore
.Qt
.Vertical
:
501 matrix
= self
.matrix().translate(0, s
* factor
)
503 matrix
= self
.matrix().translate(s
* factor
, 0)
505 self
.setTransformationAnchor(QtGui
.QGraphicsView
.NoAnchor
)
506 self
.setMatrix(matrix
)
508 def _move_nodes_to_mouse_position(self
):
509 items
= self
.scene().selectedItems()
514 min_distance
= sys
.maxint
516 width
= item
.boundingRect().width()
518 tmp_dx
= self
._last
_mouse
[0] - pos
.x() - width
/2.0
519 tmp_dy
= self
._last
_mouse
[1] - pos
.y() - width
/2.0
520 distance
= math
.sqrt(tmp_dx
** 2 + tmp_dy
** 2)
521 if distance
< min_distance
:
522 min_distance
= distance
529 item
.setPos( x
+ dx
, y
+ dy
)
531 def setBackgroundColor(self
, color
=None):
532 # To set a gradient background brush we need to use StretchToDeviceMode
533 # but that seems to be segfaulting. Use a solid background.
535 color
= QtGui
.QColor(50,50,50)
536 self
.setBackgroundBrush(color
)
538 def _scale_view(self
, scale
):
539 factor
= (self
.matrix().scale(scale
, scale
)
540 .mapRect(QtCore
.QRectF(0, 0, 1, 1))
542 if factor
< 0.07 or factor
> 100:
545 self
.scale(scale
, scale
)
547 def add(self
, commits
):
549 for commit
in commits
:
550 self
._commits
[commit
.sha1
] = commit
551 for p
in commit
.parents
:
552 children
= self
._children
.setdefault(p
, [])
553 children
.append(commit
.sha1
)
556 self
._nodes
[commit
.sha1
] = node
557 self
._items
.append(node
)
559 def link(self
, commits
):
560 """Create edges linking commits with their parents"""
562 for commit
in commits
:
563 children
= self
._children
.get(commit
.sha1
, None)
567 commit_node
= self
._nodes
[commit
.sha1
]
568 for child_sha1
in children
:
569 child_node
= self
._nodes
[child_sha1
]
570 edge
= Edge(commit_node
, child_node
)
573 def layout(self
, commits
):
579 for commit
in commits
:
580 if commit
.sha1
not in self
._children
:
581 self
._loc
[commit
.sha1
] = (xpos
, ypos
)
582 node
= self
._nodes
.get(commit
.sha1
, None)
583 node
.setPos(xpos
, ypos
)
585 gxmax
= max(xpos
, gxmax
)
589 for sha1
in self
._children
[commit
.sha1
]:
590 loc
= self
._loc
[sha1
]
593 xmax
= min(xmax
, loc
[0])
594 ymax
= max(ymax
, loc
[1])
595 gxmax
= max(gxmax
, xmax
)
596 gymax
= max(gymax
, ymax
)
600 gymax
= max(gymax
, ymax
)
601 if ymax
in self
._cols
:
602 xmax
= max(xmax
, self
._cols
[ymax
] + self
._xoff
)
603 gxmax
= max(gxmax
, xmax
)
604 self
._cols
[ymax
] = xmax
607 self
._cols
[ymax
] = xmax
610 self
._loc
[sha1
] = (xmax
, ymax
)
611 node
= self
._nodes
[sha1
]
612 node
.setPos(xmax
, ymax
)
618 self
.scene().setSceneRect(-xpad
, -ypad
, gxmax
+xpad
, gymax
+ypad
)
620 if __name__
== "__main__":
621 app
= QtGui
.QApplication(sys
.argv
)
623 sys
.exit(app
.exec_())