4 from PyQt4
import QtGui
5 from PyQt4
import QtCore
6 from PyQt4
.QtCore
import SIGNAL
8 if __name__
== "__main__":
10 src
= os
.path
.join(os
.path
.dirname(__file__
), '..', '..')
11 sys
.path
.insert(1, os
.path
.join(os
.path
.abspath(src
), 'thirdparty'))
12 sys
.path
.insert(1, os
.path
.abspath(src
))
15 from cola
import observable
16 from cola
import qtutils
17 from cola
import signals
18 from cola
import gitcmds
19 from cola
import difftool
20 from cola
.controllers
import createbranch
21 from cola
.controllers
import createtag
22 from cola
.dag
.model
import DAG
23 from cola
.dag
.model
import RepoReader
24 from cola
.prefs
import diff_font
25 from cola
.qt
import DiffSyntaxHighlighter
26 from cola
.qt
import GitRefLineEdit
27 from cola
.views
import standard
30 def git_dag(model
, parent
):
31 """Return a pre-populated git DAG widget."""
32 dag
= DAG(model
.currentbranch
, 1000)
33 view
= GitDAGWidget(dag
, parent
=parent
)
34 view
.resize_to_desktop()
37 view
.thread
.start(QtCore
.QThread
.LowPriority
)
41 class DiffWidget(QtGui
.QWidget
):
42 def __init__(self
, notifier
, parent
=None):
43 QtGui
.QWidget
.__init
__(self
, parent
)
45 self
.diff
= QtGui
.QTextEdit()
46 self
.diff
.setLineWrapMode(QtGui
.QTextEdit
.NoWrap
)
47 self
.diff
.setReadOnly(True)
48 self
.diff
.setFont(diff_font())
49 self
.diff_syn
= DiffSyntaxHighlighter(self
.diff
.document())
51 self
._layt
= QtGui
.QHBoxLayout()
52 self
._layt
.addWidget(self
.diff
)
53 self
._layt
.setMargin(2)
54 self
.setLayout(self
._layt
)
56 sig
= signals
.commits_selected
57 notifier
.add_message_observer(sig
, self
._commits
_selected
)
59 def _commits_selected(self
, commits
):
64 merge
= len(commit
.parents
) > 1
65 self
.diff
.setText(gitcmds
.diff_info(sha1
, merge
=merge
))
66 qtutils
.set_clipboard(sha1
)
69 class CommitTreeWidgetItem(QtGui
.QTreeWidgetItem
):
70 def __init__(self
, commit
, parent
=None):
71 QtGui
.QListWidgetItem
.__init
__(self
, parent
)
73 self
.setText(0, commit
.subject
)
74 self
.setText(1, commit
.author
)
75 self
.setText(2, commit
.authdate
)
78 class CommitTreeWidget(QtGui
.QTreeWidget
):
79 def __init__(self
, notifier
, parent
=None):
80 QtGui
.QTreeWidget
.__init
__(self
, parent
)
81 self
.setSelectionMode(self
.ContiguousSelection
)
82 self
.setUniformRowHeights(True)
83 self
.setAllColumnsShowFocus(True)
84 self
.setAlternatingRowColors(True)
85 self
.setRootIsDecorated(False)
86 self
.setHeaderLabels(['Subject', 'Author', 'Date'])
89 self
._notifier
= notifier
90 self
._selecting
= False
92 self
._clicked
_item
= None
93 self
._selected
_item
= None
94 self
._actions
= context_menu_actions(self
)
96 sig
= signals
.commits_selected
97 notifier
.add_message_observer(sig
, self
._commits
_selected
)
99 self
.connect(self
, SIGNAL('itemSelectionChanged()'),
100 self
._item
_selection
_changed
)
102 def contextMenuEvent(self
, event
):
103 update_actions(self
, event
)
104 context_menu_event(self
, event
)
106 def mousePressEvent(self
, event
):
107 if event
.buttons() == QtCore
.Qt
.RightButton
:
110 QtGui
.QTreeWidget
.mousePressEvent(self
, event
)
113 return self
._selecting
115 def set_selecting(self
, selecting
):
116 self
._selecting
= selecting
118 def _item_selection_changed(self
):
119 items
= self
.selectedItems()
122 self
.set_selecting(True)
123 sig
= signals
.commits_selected
124 self
._notifier
.notify_message_observers(sig
, [i
.commit
for i
in items
])
125 self
.set_selecting(False)
127 def _commits_selected(self
, commits
):
130 self
.select([commit
.sha1
for commit
in commits
])
132 def select(self
, sha1s
):
133 self
.clearSelection()
136 item
= self
._sha
1map
[sha1
]
139 self
.blockSignals(True)
140 self
.scrollToItem(item
)
141 item
.setSelected(True)
142 self
.blockSignals(False)
144 def adjust_columns(self
):
145 width
= self
.width()-20
148 self
.setColumnWidth(0, zero
)
149 self
.setColumnWidth(1, onetwo
)
150 self
.setColumnWidth(2, onetwo
)
153 QtGui
.QTreeWidget
.clear(self
)
154 self
._sha
1map
.clear()
157 def add_commits(self
, commits
):
158 self
._commits
.extend(commits
)
160 for c
in reversed(commits
):
161 item
= CommitTreeWidgetItem(c
)
163 self
._sha
1map
[c
.sha1
] = item
165 self
._sha
1map
[tag
] = item
166 self
.insertTopLevelItems(0, items
)
168 def _diff_this_selected(self
):
169 clicked_sha1
= self
._clicked
_item
.commit
.sha1
170 selected_sha1
= self
._selected
_item
.commit
.sha1
171 difftool
.diff_commits(self
, clicked_sha1
, selected_sha1
)
173 def _diff_selected_this(self
):
174 clicked_sha1
= self
._clicked
_item
.commit
.sha1
175 selected_sha1
= self
._selected
_item
.commit
.sha1
176 difftool
.diff_commits(self
, selected_sha1
, clicked_sha1
)
178 def _create_patch(self
):
179 items
= self
.selectedItems()
183 sha1s
= [item
.commit
.sha1
for item
in items
]
184 all_sha1s
= [c
.sha1
for c
in self
._commits
]
185 cola
.notifier().broadcast(signals
.format_patch
, sha1s
, all_sha1s
)
187 def _create_branch(self
):
188 sha1
= self
._clicked
_item
.commit
.sha1
189 createbranch
.create_new_branch(revision
=sha1
)
191 def _create_tag(self
):
192 sha1
= self
._clicked
_item
.commit
.sha1
193 createtag
.create_tag(revision
=sha1
)
195 def _cherry_pick(self
):
196 sha1
= self
._clicked
_item
.commit
.sha1
197 cola
.notifier().broadcast(signals
.cherry_pick
, [sha1
])
200 class GitDAGWidget(standard
.StandardDialog
):
201 """The git-dag widget."""
202 # Keep us in scope otherwise PyQt kills the widget
203 def __init__(self
, dag
, parent
=None, args
=None):
204 standard
.StandardDialog
.__init
__(self
, parent
=parent
)
206 self
.setObjectName('dag')
207 self
.setWindowTitle(self
.tr('git dag'))
208 self
.setMinimumSize(1, 1)
210 self
.revlabel
= QtGui
.QLabel()
211 self
.revlabel
.setText('git log -')
213 self
.revtext
= GitRefLineEdit()
214 self
.revtext
.setText(dag
.ref
)
216 self
.maxresults
= QtGui
.QSpinBox()
217 self
.maxresults
.setMinimum(-1)
218 self
.maxresults
.setMaximum(2**31 - 1)
219 self
.maxresults
.setValue(dag
.count
)
221 self
.displaybutton
= QtGui
.QPushButton()
222 self
.displaybutton
.setText('Display')
224 self
.zoom_in
= QtGui
.QPushButton()
225 self
.zoom_in
.setIcon(qtutils
.theme_icon('zoom-in.png'))
226 self
.zoom_in
.setFlat(True)
228 self
.zoom_out
= QtGui
.QPushButton()
229 self
.zoom_out
.setIcon(qtutils
.theme_icon('zoom-out.png'))
230 self
.zoom_out
.setFlat(True)
232 self
._buttons
_layt
= QtGui
.QHBoxLayout()
233 self
._buttons
_layt
.setMargin(2)
234 self
._buttons
_layt
.setSpacing(4)
236 self
._buttons
_layt
.addWidget(self
.revlabel
)
237 self
._buttons
_layt
.addWidget(self
.maxresults
)
238 self
._buttons
_layt
.addWidget(self
.revtext
)
239 self
._buttons
_layt
.addWidget(self
.displaybutton
)
240 self
._buttons
_layt
.addStretch()
241 self
._buttons
_layt
.addWidget(self
.zoom_out
)
242 self
._buttons
_layt
.addWidget(self
.zoom_in
)
245 self
._notifier
= notifier
= observable
.Observable()
246 self
._notifier
.refs_updated
= refs_updated
= 'refs_updated'
247 self
._notifier
.add_message_observer(refs_updated
, self
._display
)
249 self
._graphview
= GraphView(notifier
)
250 self
._treewidget
= CommitTreeWidget(notifier
)
251 self
._diffwidget
= DiffWidget(notifier
)
253 self
._mainsplitter
= QtGui
.QSplitter()
254 self
._mainsplitter
.setOrientation(QtCore
.Qt
.Horizontal
)
255 self
._mainsplitter
.setChildrenCollapsible(True)
257 self
._leftsplitter
= QtGui
.QSplitter()
258 self
._leftsplitter
.setOrientation(QtCore
.Qt
.Vertical
)
259 self
._leftsplitter
.setChildrenCollapsible(True)
260 self
._leftsplitter
.setStretchFactor(0, 1)
261 self
._leftsplitter
.setStretchFactor(1, 1)
262 self
._leftsplitter
.insertWidget(0, self
._treewidget
)
263 self
._leftsplitter
.insertWidget(1, self
._diffwidget
)
265 self
._mainsplitter
.insertWidget(0, self
._leftsplitter
)
266 self
._mainsplitter
.insertWidget(1, self
._graphview
)
268 self
._mainsplitter
.setStretchFactor(0, 1)
269 self
._mainsplitter
.setStretchFactor(1, 1)
271 self
._layt
= layt
= QtGui
.QVBoxLayout()
274 layt
.addLayout(self
._buttons
_layt
)
275 layt
.addWidget(self
._mainsplitter
)
278 qtutils
.add_close_action(self
)
280 self
.thread
= ReaderThread(self
, dag
)
282 self
.thread
.connect(self
.thread
, self
.thread
.commits_ready
,
285 self
.thread
.connect(self
.thread
, self
.thread
.done
,
288 self
.connect(self
._mainsplitter
, SIGNAL('splitterMoved(int,int)'),
289 self
._splitter
_moved
)
291 self
.connect(self
.zoom_in
, SIGNAL('pressed()'),
292 self
._graphview
.zoom_in
)
294 self
.connect(self
.zoom_out
, SIGNAL('pressed()'),
295 self
._graphview
.zoom_out
)
297 self
.connect(self
.maxresults
, SIGNAL('valueChanged(int)'),
298 lambda(x
): self
.dag
.set_count(x
))
300 self
.connect(self
.displaybutton
, SIGNAL('pressed()'),
304 new_ref
= unicode(self
.revtext
.text())
309 self
.dag
.set_ref(new_ref
)
310 self
.dag
.set_count(self
.maxresults
.value())
314 standard
.StandardDialog
.show(self
)
315 self
._mainsplitter
.setSizes([self
.width()/2, self
.width()/2])
316 self
._leftsplitter
.setSizes([self
.height()/3, self
.height()*2/3])
317 self
._treewidget
.adjust_columns()
319 def resizeEvent(self
, e
):
320 standard
.StandardDialog
.resizeEvent(self
, e
)
321 self
._treewidget
.adjust_columns()
323 def _splitter_moved(self
, pos
, idx
):
324 self
._treewidget
.adjust_columns()
327 self
._graphview
.clear()
328 self
._treewidget
.clear()
329 self
._commits
.clear()
331 def add_commits(self
, commits
):
332 # Keep track of commits
333 for commit_obj
in commits
:
334 self
._commits
[commit_obj
.sha1
] = commit_obj
335 for tag
in commit_obj
.tags
:
336 self
._commits
[tag
] = commit_obj
337 self
._graphview
.add_commits(commits
)
338 self
._treewidget
.add_commits(commits
)
340 def thread_done(self
):
342 commit_obj
= self
._commits
[self
.dag
.ref
]
345 sig
= signals
.commits_selected
346 self
._notifier
.notify_message_observers(sig
, [commit_obj
])
347 self
._graphview
.view_fit()
351 standard
.StandardDialog
.close(self
)
354 self
.thread
.mutex
.lock()
355 self
.thread
.stop
= True
356 self
.thread
.mutex
.unlock()
359 self
.thread
.abort
= True
363 self
.thread
.abort
= False
364 self
.thread
.stop
= False
368 self
.thread
.mutex
.lock()
369 self
.thread
.stop
= False
370 self
.thread
.mutex
.unlock()
371 self
.thread
.condition
.wakeOne()
373 def resize_to_desktop(self
):
374 desktop
= QtGui
.QApplication
.instance().desktop()
375 width
= desktop
.width()
376 height
= desktop
.height()
377 self
.resize(width
, height
)
380 class ReaderThread(QtCore
.QThread
):
382 commits_ready
= QtCore
.SIGNAL('commits_ready')
383 done
= QtCore
.SIGNAL('done')
385 def __init__(self
, parent
, dag
):
386 QtCore
.QThread
.__init
__(self
, parent
)
390 self
.mutex
= QtCore
.QMutex()
391 self
.condition
= QtCore
.QWaitCondition()
394 repo
= RepoReader(self
.dag
)
400 self
.condition
.wait(self
.mutex
)
406 if len(commits
) >= 512:
407 self
.emit(self
.commits_ready
, commits
)
411 self
.emit(self
.commits_ready
, commits
)
419 class Edge(QtGui
.QGraphicsItem
):
420 _type
= QtGui
.QGraphicsItem
.UserType
+ 1
422 _arrow_extra
= (_arrow_size
+1.0)/2.0
424 def __init__(self
, source
, dest
,
426 arrow_size
=_arrow_size
):
427 QtGui
.QGraphicsItem
.__init
__(self
)
429 self
.source_pt
= QtCore
.QPointF()
430 self
.dest_pt
= QtCore
.QPointF()
431 self
.setAcceptedMouseButtons(QtCore
.Qt
.NoButton
)
436 # Adjust the points to leave a small margin between
437 # the arrow and the commit.
438 dest_pt
= Commit
._bbox
.center()
439 line
= QtCore
.QLineF(
440 self
.mapFromItem(self
.source
, dest_pt
),
441 self
.mapFromItem(self
.dest
, dest_pt
))
445 length
= line
.length()
446 offset
= QtCore
.QPointF((line
.dx() * dx
) / length
,
447 (line
.dy() * dy
) / length
)
449 self
.source_pt
= line
.p1() + offset
450 self
.dest_pt
= line
.p2() - offset
452 line
= QtCore
.QLineF(self
.source_pt
, self
.dest_pt
)
455 self
.pen
= QtGui
.QPen(QtCore
.Qt
.gray
, 0,
460 # Setup the arrow polygon
461 length
= line
.length()
462 angle
= math
.acos(line
.dx() / length
)
464 angle
= 2.0 * math
.pi
- angle
466 dest_x
= (self
.dest_pt
+
467 QtCore
.QPointF(math
.sin(angle
- math
.pi
/3.) *
469 math
.cos(angle
- math
.pi
/3.) *
471 dest_y
= (self
.dest_pt
+
472 QtCore
.QPointF(math
.sin(angle
- math
.pi
+ math
.pi
/3.) *
474 math
.cos(angle
- math
.pi
+ math
.pi
/3.) *
476 self
.poly
= QtGui
.QPolygonF([line
.p2(), dest_x
, dest_y
])
478 width
= self
.dest_pt
.x() - self
.source_pt
.x()
479 height
= self
.dest_pt
.y() - self
.source_pt
.y()
480 rect
= QtCore
.QRectF(self
.source_pt
, QtCore
.QSizeF(width
, height
))
481 self
._bound
= rect
.normalized().adjusted(-extra
, -extra
, extra
, extra
)
483 def type(self
, _type
=_type
):
486 def boundingRect(self
):
489 def paint(self
, painter
, option
, widget
,
490 arrow_size
=_arrow_size
,
491 gray
=QtCore
.Qt
.gray
):
493 painter
.setPen(self
.pen
)
494 painter
.drawLine(self
.line
)
497 painter
.setBrush(gray
)
498 painter
.drawPolygon(self
.poly
)
501 class Commit(QtGui
.QGraphicsItem
):
502 _type
= QtGui
.QGraphicsItem
.UserType
+ 2
506 _shape
= QtGui
.QPainterPath()
507 _shape
.addRect(_width
/-2., _height
/-2., _width
, _height
)
508 _bbox
= _shape
.boundingRect()
510 _inner
= QtGui
.QPainterPath()
511 _inner
.addRect(_width
/-2.+2., _height
/-2.+2, _width
-4., _height
-4.)
512 _inner
= _inner
.boundingRect()
514 _selected_color
= QtGui
.QColor
.fromRgb(255, 255, 0)
515 _outline_color
= QtGui
.QColor
.fromRgb(64, 96, 192)
518 _text_options
= QtGui
.QTextOption()
519 _text_options
.setAlignment(QtCore
.Qt
.AlignCenter
)
521 _commit_pen
= QtGui
.QPen()
522 _commit_pen
.setWidth(1.0)
523 _commit_pen
.setColor(_outline_color
)
525 _commit_color
= QtGui
.QColor
.fromRgb(128, 222, 255)
526 _commit_selected_color
= QtGui
.QColor
.fromRgb(32, 64, 255)
527 _merge_color
= QtGui
.QColor
.fromRgb(255, 255, 255)
529 def __init__(self
, commit
,
531 selectable
=QtGui
.QGraphicsItem
.ItemIsSelectable
,
532 cursor
=QtCore
.Qt
.PointingHandCursor
,
534 commit_color
=_commit_color
,
535 commit_selected_color
=_commit_selected_color
,
536 merge_color
=_merge_color
):
538 QtGui
.QGraphicsItem
.__init
__(self
)
541 self
.setFlag(selectable
)
542 self
.setCursor(cursor
)
545 self
._notifier
= notifier
548 self
.label
= Label(commit
)
549 self
.label
.setParentItem(self
)
550 self
.label
.setPos(xpos
, 0.)
554 if len(commit
.parents
) > 1:
555 self
.commit_color
= merge_color
557 self
.commit_color
= commit_color
558 self
.text_pen
= QtCore
.Qt
.black
559 self
.sha1_text
= commit
.sha1
[:8]
565 # Overridden Qt methods
568 def blockSignals(self
, blocked
):
569 self
._notifier
.notification_enabled
= not blocked
571 def itemChange(self
, change
, value
):
572 if change
== QtGui
.QGraphicsItem
.ItemSelectedHasChanged
:
573 # Broadcast selection to other widgets
574 selected_items
= self
.scene().selectedItems()
575 commits
= [item
.commit
for item
in selected_items
]
576 self
.scene().parent().set_selecting(True)
577 sig
= signals
.commits_selected
578 self
._notifier
.notify_message_observers(sig
, commits
)
579 self
.scene().parent().set_selecting(False)
581 # Cache the pen for use in paint()
582 if value
.toPyObject():
583 self
.commit_color
= self
._commit
_selected
_color
584 self
.text_pen
= QtCore
.Qt
.white
585 color
= self
._selected
_color
587 self
.text_pen
= QtCore
.Qt
.black
588 if len(self
.commit
.parents
) > 1:
589 self
.commit_color
= self
._merge
_color
591 self
.commit_color
= self
._commit
_color
592 color
= self
._outline
_color
593 commit_pen
= QtGui
.QPen()
594 commit_pen
.setWidth(1.0)
595 commit_pen
.setColor(color
)
596 self
._commit
_pen
= commit_pen
598 return QtGui
.QGraphicsItem
.itemChange(self
, change
, value
)
600 def type(self
, _type
=_type
):
603 def boundingRect(self
, _bbox
=_bbox
):
606 def shape(self
, _shape
=_shape
):
609 def paint(self
, painter
, option
, widget
,
611 text_options
=_text_options
,
614 # Do not draw outside the exposed rect
615 painter
.setClipRect(option
.exposedRect
)
618 painter
.setPen(self
._commit
_pen
)
619 painter
.setBrush(self
.commit_color
)
620 painter
.drawEllipse(inner
)
625 except AttributeError:
626 font
= cache
.font
= painter
.font()
628 painter
.setFont(font
)
629 painter
.setPen(self
.text_pen
)
630 painter
.drawText(inner
, self
.sha1_text
, text_options
)
632 def mousePressEvent(self
, event
):
633 QtGui
.QGraphicsItem
.mousePressEvent(self
, event
)
635 self
.selected
= self
.isSelected()
637 def mouseMoveEvent(self
, event
):
640 QtGui
.QGraphicsItem
.mouseMoveEvent(self
, event
)
642 def mouseReleaseEvent(self
, event
):
643 QtGui
.QGraphicsItem
.mouseReleaseEvent(self
, event
)
644 if (not self
.dragged
and
646 event
.button() == QtCore
.Qt
.LeftButton
):
652 class Label(QtGui
.QGraphicsItem
):
653 _type
= QtGui
.QGraphicsItem
.UserType
+ 3
658 _shape
= QtGui
.QPainterPath()
659 _shape
.addRect(0, 0, _width
, _height
)
661 _bbox
= _shape
.boundingRect()
663 _text_options
= QtGui
.QTextOption()
664 _text_options
.setAlignment(QtCore
.Qt
.AlignCenter
)
665 _text_options
.setAlignment(QtCore
.Qt
.AlignVCenter
)
667 _black
= QtCore
.Qt
.black
669 def __init__(self
, commit
,
670 other_color
=QtGui
.QColor
.fromRgb(255, 255, 64),
671 head_color
=QtGui
.QColor
.fromRgb(64, 255, 64),
674 QtGui
.QGraphicsItem
.__init
__(self
)
677 # Starts with enough space for two tags. Any more and the commit
678 # needs to be taller to accomodate.
681 height
= len(commit
.tags
) * height
/2. + 4. # +6 padding
683 self
.label_box
= QtCore
.QRectF(0., -height
/2., width
, height
)
684 self
.text_box
= QtCore
.QRectF(2., -height
/2., width
-4., height
)
685 self
.tag_text
= '\n'.join(commit
.tags
)
687 if 'HEAD' in commit
.tags
:
688 self
.color
= head_color
690 self
.color
= other_color
692 self
.pen
= QtGui
.QPen()
693 self
.pen
.setColor(self
.color
.darker())
694 self
.pen
.setWidth(1.0)
696 def type(self
, _type
=_type
):
699 def boundingRect(self
, _bbox
=_bbox
):
702 def shape(self
, _shape
=_shape
):
705 def paint(self
, painter
, option
, widget
,
706 text_options
=_text_options
,
710 painter
.setBrush(self
.color
)
711 painter
.setPen(self
.pen
)
712 painter
.drawRoundedRect(self
.label_box
, 4, 4)
715 except AttributeError:
716 font
= cache
.font
= painter
.font()
718 painter
.setFont(font
)
719 painter
.setPen(black
)
720 painter
.drawText(self
.text_box
, self
.tag_text
, text_options
)
723 class GraphView(QtGui
.QGraphicsView
):
724 def __init__(self
, notifier
):
725 QtGui
.QGraphicsView
.__init
__(self
)
733 self
._notifier
= notifier
736 self
._selected
_item
= None
737 self
._clicked
_item
= None
741 self
._panning
= False
742 self
._pressed
= False
743 self
._selecting
= False
744 self
._last
_mouse
= [0, 0]
747 self
.scale(self
._zoom
, self
._zoom
)
748 self
.setDragMode(self
.RubberBandDrag
)
750 scene
= QtGui
.QGraphicsScene(self
)
751 scene
.setItemIndexMethod(QtGui
.QGraphicsScene
.NoIndex
)
754 self
.setRenderHint(QtGui
.QPainter
.Antialiasing
)
755 self
.setOptimizationFlag(self
.DontAdjustForAntialiasing
, True)
756 self
.setViewportUpdateMode(self
.SmartViewportUpdate
)
757 self
.setCacheMode(QtGui
.QGraphicsView
.CacheBackground
)
758 self
.setTransformationAnchor(QtGui
.QGraphicsView
.AnchorUnderMouse
)
759 self
.setResizeAnchor(QtGui
.QGraphicsView
.NoAnchor
)
760 self
.setBackgroundBrush(QtGui
.QColor
.fromRgb(0, 0, 0))
762 self
._action
_zoom
_in
= (
763 qtutils
.add_action(self
, 'Zoom In',
766 QtCore
.Qt
.Key_Equal
))
768 self
._action
_zoom
_out
= (
769 qtutils
.add_action(self
, 'Zoom Out',
771 QtCore
.Qt
.Key_Minus
))
773 self
._action
_zoom
_fit
= (
774 qtutils
.add_action(self
, 'Zoom to Fit',
778 self
._action
_select
_parent
= (
779 qtutils
.add_action(self
, 'Select Parent',
783 self
._action
_select
_oldest
_parent
= (
784 qtutils
.add_action(self
, 'Select Oldest Parent',
785 self
._select
_oldest
_parent
,
788 self
._action
_select
_child
= (
789 qtutils
.add_action(self
, 'Select Child',
793 self
._action
_select
_child
= (
794 qtutils
.add_action(self
, 'Select Nth Child',
795 self
._select
_nth
_child
,
798 self
._actions
= context_menu_actions(self
)
800 sig
= signals
.commits_selected
801 notifier
.add_message_observer(sig
, self
._commits
_selected
)
804 self
._scale
_view
(1.5)
807 self
._scale
_view
(1.0/1.5)
809 def _commits_selected(self
, commits
):
812 self
.select([commit
.sha1
for commit
in commits
])
814 def contextMenuEvent(self
, event
):
815 update_actions(self
, event
)
816 context_menu_event(self
, event
)
818 def select(self
, sha1s
):
819 """Select the item for the SHA-1"""
820 self
.scene().clearSelection()
823 item
= self
._items
[sha1
]
826 item
.blockSignals(True)
827 item
.setSelected(True)
828 item
.blockSignals(False)
829 self
.ensureVisible(item
.mapRectToScene(item
.boundingRect()))
831 def selected_item(self
):
832 """Return the currently selected item"""
833 selected_items
= self
.selectedItems()
834 if not selected_items
:
836 return selected_items
[0]
838 def selectedItems(self
):
839 """Return the currently selected items"""
840 return self
.scene().selectedItems()
842 def get_item_by_generation(self
, commits
, criteria_fn
):
843 """Return the item for the commit matching criteria"""
847 for commit
in commits
:
848 if (generation
is None or
849 criteria_fn(generation
, commit
.generation
)):
851 generation
= commit
.generation
853 return self
._items
[sha1
]
857 def oldest_item(self
, commits
):
858 """Return the item for the commit with the oldest generation number"""
859 return self
.get_item_by_generation(commits
, lambda a
, b
: a
> b
)
861 def newest_item(self
, commits
):
862 """Return the item for the commit with the newest generation number"""
863 return self
.get_item_by_generation(commits
, lambda a
, b
: a
< b
)
865 def _diff_this_selected(self
):
866 clicked_sha1
= self
._clicked
_item
.commit
.sha1
867 selected_sha1
= self
._selected
_item
.commit
.sha1
868 difftool
.diff_commits(self
, clicked_sha1
, selected_sha1
)
870 def _diff_selected_this(self
):
871 clicked_sha1
= self
._clicked
_item
.commit
.sha1
872 selected_sha1
= self
._selected
_item
.commit
.sha1
873 difftool
.diff_commits(self
, selected_sha1
, clicked_sha1
)
875 def _create_patch(self
):
876 items
= self
.selectedItems()
879 selected_commits
= sort_by_generation([n
.commit
for n
in items
])
880 sha1s
= [c
.sha1
for c
in selected_commits
]
881 all_sha1s
= [c
.sha1
for c
in self
._commits
]
882 cola
.notifier().broadcast(signals
.format_patch
, sha1s
, all_sha1s
)
884 def _create_branch(self
):
885 sha1
= self
._clicked
_item
.commit
.sha1
886 createbranch
.create_new_branch(revision
=sha1
)
888 def _create_tag(self
):
889 sha1
= self
._clicked
_item
.commit
.sha1
890 createtag
.create_tag(revision
=sha1
)
892 def _cherry_pick(self
):
893 sha1
= self
._clicked
_item
.commit
.sha1
894 cola
.notifier().broadcast(signals
.cherry_pick
, [sha1
])
895 self
._notifier
.notify_message_observers(self
._notifier
.refs_updated
)
897 def _select_parent(self
):
898 """Select the parent with the newest generation number"""
899 selected_item
= self
.selected_item()
900 if selected_item
is None:
902 parent_item
= self
.newest_item(selected_item
.commit
.parents
)
903 if parent_item
is None:
905 selected_item
.setSelected(False)
906 parent_item
.setSelected(True)
907 self
.ensureVisible(parent_item
.mapRectToScene(parent_item
.boundingRect()))
909 def _select_oldest_parent(self
):
910 """Select the parent with the oldest generation number"""
911 selected_item
= self
.selected_item()
912 if selected_item
is None:
914 parent_item
= self
.oldest_item(selected_item
.commit
.parents
)
915 if parent_item
is None:
917 selected_item
.setSelected(False)
918 parent_item
.setSelected(True)
919 self
.ensureVisible(parent_item
.mapRectToScene(parent_item
.boundingRect()))
921 def _select_child(self
):
922 """Select the child with the oldest generation number"""
923 selected_item
= self
.selected_item()
924 if selected_item
is None:
926 child_item
= self
.oldest_item(selected_item
.commit
.children
)
927 if child_item
is None:
929 selected_item
.setSelected(False)
930 child_item
.setSelected(True)
931 self
.ensureVisible(child_item
.mapRectToScene(child_item
.boundingRect()))
933 def _select_nth_child(self
):
934 """Select the Nth child with the newest generation number (N > 1)"""
935 selected_item
= self
.selected_item()
936 if selected_item
is None:
938 if len(selected_item
.commit
.children
) > 1:
939 children
= selected_item
.commit
.children
[1:]
941 children
= selected_item
.commit
.children
942 child_item
= self
.newest_item(children
)
943 if child_item
is None:
945 selected_item
.setSelected(False)
946 child_item
.setSelected(True)
947 self
.ensureVisible(child_item
.mapRectToScene(child_item
.boundingRect()))
950 """Fit selected items into the viewport"""
952 items
= self
.scene().selectedItems()
954 rect
= self
.scene().itemsBoundingRect()
962 item_rect
= item
.boundingRect()
963 xoff
= item_rect
.width()
964 yoff
= item_rect
.height()
965 xmin
= min(xmin
, pos
.x())
966 ymin
= min(ymin
, pos
.y())
967 xmax
= max(xmax
, pos
.x()+xoff
)
968 ymax
= max(ymax
, pos
.y()+yoff
)
969 rect
= QtCore
.QRectF(xmin
, ymin
, xmax
-xmin
, ymax
-ymin
)
970 adjust
= Commit
._width
971 rect
.setX(rect
.x() - adjust
)
972 rect
.setY(rect
.y() - adjust
)
973 rect
.setHeight(rect
.height() + adjust
)
974 rect
.setWidth(rect
.width() + adjust
)
975 self
.fitInView(rect
, QtCore
.Qt
.KeepAspectRatio
)
976 self
.scene().invalidate()
978 def _save_selection(self
, event
):
979 if event
.button() != QtCore
.Qt
.LeftButton
:
981 elif QtCore
.Qt
.ShiftModifier
!= event
.modifiers():
983 self
._selected
= self
.selectedItems()
985 def _restore_selection(self
, event
):
986 if QtCore
.Qt
.ShiftModifier
!= event
.modifiers():
988 for item
in self
._selected
:
989 item
.setSelected(True)
991 def _handle_event(self
, eventhandler
, event
):
993 self
._save
_selection
(event
)
994 eventhandler(self
, event
)
995 self
._restore
_selection
(event
)
997 def mousePressEvent(self
, event
):
998 if event
.button() == QtCore
.Qt
.MidButton
:
1000 self
._mouse
_start
= [pos
.x(), pos
.y()]
1001 self
._saved
_matrix
= QtGui
.QMatrix(self
.matrix())
1002 self
._panning
= True
1004 if event
.button() == QtCore
.Qt
.RightButton
:
1007 if event
.button() == QtCore
.Qt
.LeftButton
:
1008 self
._pressed
= True
1009 self
._handle
_event
(QtGui
.QGraphicsView
.mousePressEvent
, event
)
1011 def mouseMoveEvent(self
, event
):
1012 pos
= self
.mapToScene(event
.pos())
1016 self
._last
_mouse
[0] = pos
.x()
1017 self
._last
_mouse
[1] = pos
.y()
1018 self
._handle
_event
(QtGui
.QGraphicsView
.mouseMoveEvent
, event
)
1020 def selecting(self
):
1021 return self
._selecting
1023 def set_selecting(self
, selecting
):
1024 self
._selecting
= selecting
1026 def mouseReleaseEvent(self
, event
):
1027 self
._pressed
= False
1028 if event
.button() == QtCore
.Qt
.MidButton
:
1029 self
._panning
= False
1031 self
._handle
_event
(QtGui
.QGraphicsView
.mouseReleaseEvent
, event
)
1034 def _pan(self
, event
):
1036 dx
= pos
.x() - self
._mouse
_start
[0]
1037 dy
= pos
.y() - self
._mouse
_start
[1]
1039 if dx
== 0 and dy
== 0:
1042 rect
= QtCore
.QRect(0, 0, abs(dx
), abs(dy
))
1043 delta
= self
.mapToScene(rect
).boundingRect()
1053 matrix
= QtGui
.QMatrix(self
._saved
_matrix
).translate(tx
, ty
)
1054 self
.setTransformationAnchor(QtGui
.QGraphicsView
.NoAnchor
)
1055 self
.setMatrix(matrix
)
1057 def wheelEvent(self
, event
):
1058 """Handle Qt mouse wheel events."""
1059 if event
.modifiers() == QtCore
.Qt
.ControlModifier
:
1060 self
._wheel
_zoom
(event
)
1062 self
._wheel
_pan
(event
)
1064 def _wheel_zoom(self
, event
):
1065 """Handle mouse wheel zooming."""
1066 zoom
= math
.pow(2.0, event
.delta() / 512.0)
1067 factor
= (self
.matrix()
1069 .mapRect(QtCore
.QRectF(0.0, 0.0, 1.0, 1.0))
1071 if factor
< 0.014 or factor
> 42.0:
1073 self
.setTransformationAnchor(QtGui
.QGraphicsView
.AnchorUnderMouse
)
1075 self
.scale(zoom
, zoom
)
1077 def _wheel_pan(self
, event
):
1078 """Handle mouse wheel panning."""
1080 if event
.delta() < 0:
1084 pan_rect
= QtCore
.QRectF(0.0, 0.0, 1.0, 1.0)
1085 factor
= 1.0 / self
.matrix().mapRect(pan_rect
).width()
1087 if event
.orientation() == QtCore
.Qt
.Vertical
:
1088 matrix
= self
.matrix().translate(0, s
* factor
)
1090 matrix
= self
.matrix().translate(s
* factor
, 0)
1091 self
.setTransformationAnchor(QtGui
.QGraphicsView
.NoAnchor
)
1092 self
.setMatrix(matrix
)
1094 def _scale_view(self
, scale
):
1095 factor
= (self
.matrix().scale(scale
, scale
)
1096 .mapRect(QtCore
.QRectF(0, 0, 1, 1))
1098 if factor
< 0.07 or factor
> 100:
1102 adjust_scrollbars
= True
1103 scrollbar
= self
.verticalScrollBar()
1105 value
= scrollbar
.value()
1106 min_
= scrollbar
.minimum()
1107 max_
= scrollbar
.maximum()
1108 range_
= max_
- min_
1109 distance
= value
- min_
1110 nonzero_range
= float(range_
) != 0.0
1112 scrolloffset
= distance
/float(range_
)
1114 adjust_scrollbars
= False
1116 self
.setTransformationAnchor(QtGui
.QGraphicsView
.NoAnchor
)
1117 self
.scale(scale
, scale
)
1119 scrollbar
= self
.verticalScrollBar()
1120 if scrollbar
and adjust_scrollbars
:
1121 min_
= scrollbar
.minimum()
1122 max_
= scrollbar
.maximum()
1123 range_
= max_
- min_
1124 value
= min_
+ int(float(range_
) * scrolloffset
)
1125 scrollbar
.setValue(value
)
1128 self
.scene().clear()
1136 def add_commits(self
, commits
):
1137 """Traverse commits and add them to the view."""
1138 self
._commits
.extend(commits
)
1139 scene
= self
.scene()
1140 for commit
in commits
:
1141 item
= Commit(commit
, self
._notifier
)
1142 self
._items
[commit
.sha1
] = item
1143 for ref
in commit
.tags
:
1144 self
._items
[ref
] = item
1147 self
.layout(commits
)
1150 def link(self
, commits
):
1151 """Create edges linking commits with their parents"""
1152 scene
= self
.scene()
1153 for commit
in commits
:
1155 commit_item
= self
._items
[commit
.sha1
]
1157 # TODO - Handle truncated history viewing
1159 for parent
in commit
.parents
:
1161 parent_item
= self
._items
[parent
.sha1
]
1163 # TODO - Handle truncated history viewing
1165 edge
= Edge(parent_item
, commit_item
)
1168 def layout(self
, commits
):
1171 for commit
in commits
:
1172 generation
= commit
.generation
1175 row
= self
._rows
[generation
]
1177 row
= self
._rows
[generation
] = []
1179 xpos
= (len(commit
.parents
)-1) * self
._xoff
1181 xpos
+= row
[-1] + self
._xoff
1182 ypos
= -commit
.generation
* self
._yoff
1184 item
= self
._items
[sha1
]
1185 item
.setPos(xpos
, ypos
)
1188 xmax
= max(xmax
, xpos
)
1189 ymin
= min(ymin
, ypos
)
1193 self
.scene().setSceneRect(self
._xoff
*-2, ymin
-self
._yoff
*2,
1194 xmax
+self
._xoff
*3, abs(ymin
)+self
._yoff
*4)
1196 def sort_by_generation(commits
):
1197 commits
.sort(cmp=lambda a
, b
: cmp(a
.generation
, b
.generation
))
1201 def context_menu_actions(self
):
1203 'diff_this_selected':
1204 qtutils
.add_action(self
, 'Diff this -> selected',
1205 self
._diff
_this
_selected
),
1206 'diff_selected_this':
1207 qtutils
.add_action(self
, 'Diff selected -> this',
1208 self
._diff
_selected
_this
),
1210 qtutils
.add_action(self
, 'Create Patch',
1211 self
._create
_patch
),
1213 qtutils
.add_action(self
, 'Create Branch',
1214 self
._create
_branch
),
1216 qtutils
.add_action(self
, 'Create Tag',
1219 qtutils
.add_action(self
, 'Cherry Pick',
1224 def update_actions(self
, event
):
1225 clicked_item
= self
.itemAt(event
.pos())
1226 selected_items
= self
.selectedItems()
1227 has_single_selection
= len(selected_items
) == 1
1229 has_selection
= bool(selected_items
)
1230 can_diff
= bool(clicked_item
and has_single_selection
and
1231 clicked_item
is not selected_items
[0])
1233 self
._clicked
_item
= clicked_item
1235 self
._selected
_item
= selected_items
[0]
1237 self
._selected
_item
= None
1239 self
._actions
['diff_this_selected'].setEnabled(can_diff
)
1240 self
._actions
['diff_selected_this'].setEnabled(can_diff
)
1241 self
._actions
['create_patch'].setEnabled(has_selection
)
1242 self
._actions
['create_branch'].setEnabled(has_single_selection
)
1243 self
._actions
['create_tag'].setEnabled(has_single_selection
)
1244 self
._actions
['cherry_pick'].setEnabled(has_single_selection
)
1247 def context_menu_event(self
, event
):
1248 menu
= QtGui
.QMenu(self
)
1249 menu
.addAction(self
._actions
['diff_this_selected'])
1250 menu
.addAction(self
._actions
['diff_selected_this'])
1252 menu
.addAction(self
._actions
['create_patch'])
1253 menu
.addAction(self
._actions
['create_branch'])
1254 menu
.addAction(self
._actions
['create_tag'])
1255 menu
.addAction(self
._actions
['cherry_pick'])
1256 menu
.exec_(self
.mapToGlobal(event
.pos()))
1259 if __name__
== "__main__":
1260 from cola
import app
1262 model
= cola
.model()
1263 model
.use_worktree(os
.getcwd())
1264 model
.update_status()
1266 app
= app
.ColaApplication(sys
.argv
)
1267 view
= git_dag(model
, app
.activeWindow())
1268 sys
.exit(app
.exec_())