3 from PyQt4
import QtGui
4 from PyQt4
import QtCore
5 from PyQt4
.QtCore
import SIGNAL
8 from cola
import observable
9 from cola
import qtutils
10 from cola
import signals
11 from cola
import gitcmds
12 from cola
import difftool
13 from cola
.dag
.model
import archive
14 from cola
.dag
.model
import RepoReader
15 from cola
.prefs
import diff_font
16 from cola
.qt
import DiffSyntaxHighlighter
17 from cola
.qt
import GitLogLineEdit
18 from cola
.widgets
import defs
19 from cola
.widgets
import standard
20 from cola
.widgets
.createbranch
import create_new_branch
21 from cola
.widgets
.createtag
import create_tag
22 from cola
.widgets
.archive
import GitArchiveDialog
23 from cola
.widgets
.browse
import BrowseDialog
26 class DiffWidget(QtGui
.QWidget
):
27 def __init__(self
, notifier
, parent
=None):
28 QtGui
.QWidget
.__init
__(self
, parent
)
30 self
.diff
= QtGui
.QTextEdit()
31 self
.diff
.setLineWrapMode(QtGui
.QTextEdit
.NoWrap
)
32 self
.diff
.setReadOnly(True)
33 self
.diff
.setFont(diff_font())
34 self
.highlighter
= DiffSyntaxHighlighter(self
.diff
.document())
36 self
.main_layout
= QtGui
.QHBoxLayout()
37 self
.main_layout
.addWidget(self
.diff
)
38 self
.main_layout
.setMargin(0)
39 self
.main_layout
.setSpacing(defs
.spacing
)
40 self
.setLayout(self
.main_layout
)
42 sig
= signals
.commits_selected
43 notifier
.add_observer(sig
, self
.commits_selected
)
45 def commits_selected(self
, commits
):
50 merge
= len(commit
.parents
) > 1
51 self
.diff
.setText(gitcmds
.diff_info(sha1
, merge
=merge
))
52 qtutils
.set_clipboard(sha1
)
55 class CommitTreeWidgetItem(QtGui
.QTreeWidgetItem
):
56 def __init__(self
, commit
, parent
=None):
57 QtGui
.QListWidgetItem
.__init
__(self
, parent
)
59 self
.setText(0, commit
.summary
)
60 self
.setText(1, commit
.author
)
61 self
.setText(2, commit
.authdate
)
64 class CommitTreeWidget(QtGui
.QTreeWidget
):
65 def __init__(self
, notifier
, parent
=None):
66 QtGui
.QTreeWidget
.__init
__(self
, parent
)
67 self
.setSelectionMode(self
.ContiguousSelection
)
68 self
.setUniformRowHeights(True)
69 self
.setAllColumnsShowFocus(True)
70 self
.setAlternatingRowColors(True)
71 self
.setRootIsDecorated(False)
72 self
.setHeaderLabels(['Summary', 'Author', 'Date'])
75 self
.notifier
= notifier
76 self
.selecting
= False
80 self
.menu_actions
= context_menu_actions(self
)
82 self
.action_up
= qtutils
.add_action(self
, 'Go Up', self
.go_up
,
85 self
.action_down
= qtutils
.add_action(self
, 'Go Down', self
.go_down
,
88 sig
= signals
.commits_selected
89 notifier
.add_observer(sig
, self
.commits_selected
)
91 self
.connect(self
, SIGNAL('itemSelectionChanged()'),
92 self
.selection_changed
)
94 def contextMenuEvent(self
, event
):
95 update_menu_actions(self
, event
)
96 context_menu_event(self
, event
)
98 def mousePressEvent(self
, event
):
99 if event
.buttons() == QtCore
.Qt
.RightButton
:
102 QtGui
.QTreeWidget
.mousePressEvent(self
, event
)
105 self
.goto(self
.itemAbove
)
108 self
.goto(self
.itemBelow
)
110 def goto(self
, finder
):
111 items
= self
.selectedItems()
112 item
= items
and items
[0] or None
117 self
.select([found
.commit
.sha1
], block_signals
=False)
119 def set_selecting(self
, selecting
):
120 self
.selecting
= selecting
122 def selection_changed(self
):
123 items
= self
.selectedItems()
126 self
.set_selecting(True)
127 sig
= signals
.commits_selected
128 self
.notifier
.notify_observers(sig
, [i
.commit
for i
in items
])
129 self
.set_selecting(False)
131 def commits_selected(self
, commits
):
134 self
.select([commit
.sha1
for commit
in commits
])
136 def select(self
, sha1s
, block_signals
=True):
137 self
.clearSelection()
140 item
= self
.sha1map
[sha1
]
143 block
= self
.blockSignals(block_signals
)
144 self
.scrollToItem(item
)
145 item
.setSelected(True)
146 self
.blockSignals(block
)
148 def adjust_columns(self
):
149 width
= self
.width()-20
152 self
.setColumnWidth(0, zero
)
153 self
.setColumnWidth(1, onetwo
)
154 self
.setColumnWidth(2, onetwo
)
157 QtGui
.QTreeWidget
.clear(self
)
161 def add_commits(self
, commits
):
162 self
.commits
.extend(commits
)
164 for c
in reversed(commits
):
165 item
= CommitTreeWidgetItem(c
)
167 self
.sha1map
[c
.sha1
] = item
169 self
.sha1map
[tag
] = item
170 self
.insertTopLevelItems(0, items
)
172 def diff_this_selected(self
):
173 clicked_sha1
= self
.clicked
.commit
.sha1
174 selected_sha1
= self
.selected
.commit
.sha1
175 self
.emit(SIGNAL('diff_commits'), clicked_sha1
, selected_sha1
)
177 def diff_selected_this(self
):
178 clicked_sha1
= self
.clicked
.commit
.sha1
179 selected_sha1
= self
.selected
.commit
.sha1
180 self
.emit(SIGNAL('diff_commits'), selected_sha1
, clicked_sha1
)
182 def create_patch(self
):
183 items
= self
.selectedItems()
187 sha1s
= [item
.commit
.sha1
for item
in items
]
188 all_sha1s
= [c
.sha1
for c
in self
.commits
]
189 cola
.notifier().broadcast(signals
.format_patch
, sha1s
, all_sha1s
)
191 def create_branch(self
):
192 sha1
= self
.clicked
.commit
.sha1
193 create_new_branch(revision
=sha1
)
195 def create_tag(self
):
196 sha1
= self
.clicked
.commit
.sha1
197 create_tag(revision
=sha1
)
199 def cherry_pick(self
):
200 sha1
= self
.clicked
.commit
.sha1
201 cola
.notifier().broadcast(signals
.cherry_pick
, [sha1
])
204 class DAGView(standard
.Widget
):
205 """The git-dag widget."""
207 def __init__(self
, model
, dag
, parent
=None, args
=None):
208 super(DAGView
, self
).__init
__(parent
)
209 self
.setAttribute(QtCore
.Qt
.WA_MacMetalStyle
)
210 self
.setMinimumSize(1, 1)
212 # change when widgets are added/removed
213 self
.widget_version
= 1
217 self
.revtext
= GitLogLineEdit(parent
=self
)
219 self
.maxresults
= QtGui
.QSpinBox()
220 self
.maxresults
.setMinimum(1)
221 self
.maxresults
.setMaximum(99999)
222 self
.maxresults
.setPrefix('git log -')
223 self
.maxresults
.setSuffix('')
225 self
.displaybutton
= QtGui
.QPushButton()
226 self
.displaybutton
.setText('Display')
228 self
.zoom_in
= QtGui
.QPushButton()
229 self
.zoom_in
.setIcon(qtutils
.theme_icon('zoom-in.png'))
230 self
.zoom_in
.setFlat(True)
232 self
.zoom_out
= QtGui
.QPushButton()
233 self
.zoom_out
.setIcon(qtutils
.theme_icon('zoom-out.png'))
234 self
.zoom_out
.setFlat(True)
236 self
.top_layout
= QtGui
.QHBoxLayout()
237 self
.top_layout
.setMargin(defs
.margin
)
238 self
.top_layout
.setSpacing(defs
.button_spacing
)
240 self
.top_layout
.addWidget(self
.maxresults
)
241 self
.top_layout
.addWidget(self
.revtext
)
242 self
.top_layout
.addWidget(self
.displaybutton
)
243 self
.top_layout
.addStretch()
244 self
.top_layout
.addWidget(self
.zoom_out
)
245 self
.top_layout
.addWidget(self
.zoom_in
)
248 self
.notifier
= notifier
= observable
.Observable()
249 self
.notifier
.refs_updated
= refs_updated
= 'refs_updated'
250 self
.notifier
.add_observer(refs_updated
, self
.display
)
252 self
.graphview
= GraphView(notifier
)
253 self
.treewidget
= CommitTreeWidget(notifier
)
254 self
.diffwidget
= DiffWidget(notifier
)
256 for signal
in (archive
,):
257 qtutils
.relay_signal(self
, self
.graphview
, SIGNAL(signal
))
258 qtutils
.relay_signal(self
, self
.treewidget
, SIGNAL(signal
))
260 self
.splitter
= QtGui
.QSplitter()
261 self
.splitter
.setOrientation(QtCore
.Qt
.Horizontal
)
262 self
.splitter
.setChildrenCollapsible(True)
263 self
.splitter
.setHandleWidth(defs
.handle_width
)
265 self
.left_splitter
= QtGui
.QSplitter()
266 self
.left_splitter
.setOrientation(QtCore
.Qt
.Vertical
)
267 self
.left_splitter
.setChildrenCollapsible(True)
268 self
.left_splitter
.setHandleWidth(defs
.handle_width
)
269 self
.left_splitter
.setStretchFactor(0, 1)
270 self
.left_splitter
.setStretchFactor(1, 1)
271 self
.left_splitter
.insertWidget(0, self
.treewidget
)
272 self
.left_splitter
.insertWidget(1, self
.diffwidget
)
274 self
.splitter
.insertWidget(0, self
.left_splitter
)
275 self
.splitter
.insertWidget(1, self
.graphview
)
277 self
.splitter
.setStretchFactor(0, 1)
278 self
.splitter
.setStretchFactor(1, 1)
280 self
.main_layout
= layout
= QtGui
.QVBoxLayout()
283 layout
.addLayout(self
.top_layout
)
284 layout
.addWidget(self
.splitter
)
285 self
.setLayout(layout
)
287 # Also re-loads dag.* from the saved state
288 if not qtutils
.apply_state(self
):
289 self
.resize_to_desktop()
291 # Update fields affected by model
292 self
.revtext
.setText(dag
.ref
)
293 self
.maxresults
.setValue(dag
.count
)
294 self
.update_window_title()
296 self
.thread
= ReaderThread(self
, dag
)
298 self
.thread
.connect(self
.thread
, self
.thread
.commits_ready
,
301 self
.thread
.connect(self
.thread
, self
.thread
.done
,
304 self
.connect(self
.splitter
, SIGNAL('splitterMoved(int,int)'),
307 self
.connect(self
.zoom_in
, SIGNAL('pressed()'),
308 self
.graphview
.zoom_in
)
310 self
.connect(self
.zoom_out
, SIGNAL('pressed()'),
311 self
.graphview
.zoom_out
)
313 self
.connect(self
.treewidget
, SIGNAL('diff_commits'),
316 self
.connect(self
.graphview
, SIGNAL('diff_commits'),
319 self
.connect(self
.maxresults
, SIGNAL('valueChanged(int)'),
320 lambda(x
): self
.dag
.set_count(x
))
322 self
.connect(self
.displaybutton
, SIGNAL('pressed()'),
325 self
.connect(self
.revtext
, SIGNAL('ref_changed'),
328 self
.connect(self
.revtext
, SIGNAL('textChanged(QString)'),
331 # The model is updated in another thread so use
332 # signals/slots to bring control back to the main GUI thread
333 self
.model
.add_observer(self
.model
.message_updated
,
334 self
.emit_model_updated
)
336 self
.connect(self
, SIGNAL('model_updated'),
339 qtutils
.add_close_action(self
)
341 def text_changed(self
, txt
):
342 self
.dag
.ref
= unicode(txt
)
343 self
.update_window_title()
345 def update_window_title(self
):
346 project
= self
.model
.project
348 self
.setWindowTitle('%s: %s' % (project
, self
.dag
.ref
))
350 self
.setWindowTitle(project
)
352 def export_state(self
):
353 state
= super(DAGView
, self
).export_state()
354 state
['count'] = self
.dag
.count
357 def apply_state(self
, state
):
359 super(DAGView
, self
).apply_state(state
)
363 count
= state
['count']
367 if not self
.dag
.overridden('count'):
368 self
.dag
.set_count(count
)
370 def emit_model_updated(self
):
371 self
.emit(SIGNAL('model_updated'))
373 def model_updated(self
):
375 self
.revtext
.update_matches()
377 if not self
.model
.currentbranch
:
379 self
.revtext
.setText(self
.model
.currentbranch
)
383 new_ref
= unicode(self
.revtext
.text())
388 self
.dag
.set_ref(new_ref
)
389 self
.dag
.set_count(self
.maxresults
.value())
393 super(DAGView
, self
).show()
394 self
.splitter
.setSizes([self
.width()/2, self
.width()/2])
395 self
.left_splitter
.setSizes([self
.height()/3, self
.height()*2/3])
396 self
.treewidget
.adjust_columns()
398 def resizeEvent(self
, e
):
399 super(DAGView
, self
).resizeEvent(e
)
400 self
.treewidget
.adjust_columns()
402 def splitter_moved(self
, pos
, idx
):
403 self
.treewidget
.adjust_columns()
406 self
.graphview
.clear()
407 self
.treewidget
.clear()
410 def add_commits(self
, commits
):
411 # Keep track of commits
412 for commit_obj
in commits
:
413 self
.commits
[commit_obj
.sha1
] = commit_obj
414 for tag
in commit_obj
.tags
:
415 self
.commits
[tag
] = commit_obj
416 self
.graphview
.add_commits(commits
)
417 self
.treewidget
.add_commits(commits
)
419 def thread_done(self
):
421 commit_obj
= self
.commits
[self
.dag
.ref
]
424 sig
= signals
.commits_selected
425 self
.notifier
.notify_observers(sig
, [commit_obj
])
426 self
.graphview
.view_fit()
428 def closeEvent(self
, event
):
430 self
.revtext
.close_popup()
431 qtutils
.save_state(self
)
432 return super(DAGView
, self
).closeEvent(event
)
435 self
.thread
.mutex
.lock()
436 self
.thread
.stop
= True
437 self
.thread
.mutex
.unlock()
440 self
.thread
.abort
= True
444 self
.thread
.abort
= False
445 self
.thread
.stop
= False
449 self
.thread
.mutex
.lock()
450 self
.thread
.stop
= False
451 self
.thread
.mutex
.unlock()
452 self
.thread
.condition
.wakeOne()
454 def resize_to_desktop(self
):
455 desktop
= QtGui
.QApplication
.instance().desktop()
456 width
= desktop
.width()
457 height
= desktop
.height()
458 self
.resize(width
, height
)
460 def diff_commits(self
, a
, b
):
461 paths
= self
.dag
.paths()
463 difftool
.launch([a
, b
, '--'] + paths
)
465 difftool
.diff_commits(self
, a
, b
)
468 class ReaderThread(QtCore
.QThread
):
470 commits_ready
= SIGNAL('commits_ready')
471 done
= SIGNAL('done')
473 def __init__(self
, parent
, dag
):
474 QtCore
.QThread
.__init
__(self
, parent
)
478 self
.mutex
= QtCore
.QMutex()
479 self
.condition
= QtCore
.QWaitCondition()
482 repo
= RepoReader(self
.dag
)
488 self
.condition
.wait(self
.mutex
)
494 if len(commits
) >= 512:
495 self
.emit(self
.commits_ready
, commits
)
499 self
.emit(self
.commits_ready
, commits
)
507 class Edge(QtGui
.QGraphicsItem
):
508 item_type
= QtGui
.QGraphicsItem
.UserType
+ 1
510 arrow_extra
= (arrow_size
+1.0)/2.0
512 def __init__(self
, source
, dest
,
514 arrow_size
=arrow_size
):
515 QtGui
.QGraphicsItem
.__init
__(self
)
517 self
.source_pt
= QtCore
.QPointF()
518 self
.dest_pt
= QtCore
.QPointF()
519 self
.setAcceptedMouseButtons(QtCore
.Qt
.NoButton
)
524 # Adjust the points to leave a small margin between
525 # the arrow and the commit.
526 dest_pt
= Commit
.item_bbox
.center()
527 line
= QtCore
.QLineF(
528 self
.mapFromItem(self
.source
, dest_pt
),
529 self
.mapFromItem(self
.dest
, dest_pt
))
533 length
= line
.length()
534 offset
= QtCore
.QPointF((line
.dx() * dx
) / length
,
535 (line
.dy() * dy
) / length
)
537 self
.source_pt
= line
.p1() + offset
538 self
.dest_pt
= line
.p2() - offset
540 line
= QtCore
.QLineF(self
.source_pt
, self
.dest_pt
)
543 self
.pen
= QtGui
.QPen(QtCore
.Qt
.gray
, 0,
548 # Setup the arrow polygon
549 length
= line
.length()
550 angle
= math
.acos(line
.dx() / length
)
552 angle
= 2.0 * math
.pi
- angle
554 dest_x
= (self
.dest_pt
+
555 QtCore
.QPointF(math
.sin(angle
- math
.pi
/3.) *
557 math
.cos(angle
- math
.pi
/3.) *
559 dest_y
= (self
.dest_pt
+
560 QtCore
.QPointF(math
.sin(angle
- math
.pi
+ math
.pi
/3.) *
562 math
.cos(angle
- math
.pi
+ math
.pi
/3.) *
564 self
.poly
= QtGui
.QPolygonF([line
.p2(), dest_x
, dest_y
])
566 width
= self
.dest_pt
.x() - self
.source_pt
.x()
567 height
= self
.dest_pt
.y() - self
.source_pt
.y()
568 rect
= QtCore
.QRectF(self
.source_pt
, QtCore
.QSizeF(width
, height
))
569 self
.bound
= rect
.normalized().adjusted(-extra
, -extra
, extra
, extra
)
572 return self
.item_type
574 def boundingRect(self
):
577 def paint(self
, painter
, option
, widget
,
578 arrow_size
=arrow_size
,
579 gray
=QtCore
.Qt
.gray
):
581 painter
.setPen(self
.pen
)
582 painter
.drawLine(self
.line
)
585 painter
.setBrush(gray
)
586 painter
.drawPolygon(self
.poly
)
589 class Commit(QtGui
.QGraphicsItem
):
590 item_type
= QtGui
.QGraphicsItem
.UserType
+ 2
594 item_shape
= QtGui
.QPainterPath()
595 item_shape
.addRect(width
/-2., height
/-2., width
, height
)
596 item_bbox
= item_shape
.boundingRect()
598 inner_rect
= QtGui
.QPainterPath()
599 inner_rect
.addRect(width
/-2.+2., height
/-2.+2, width
-4., height
-4.)
600 inner_rect
= inner_rect
.boundingRect()
602 selected_color
= QtGui
.QColor
.fromRgb(255, 255, 0)
603 outline_color
= QtGui
.QColor
.fromRgb(64, 96, 192)
606 text_options
= QtGui
.QTextOption()
607 text_options
.setAlignment(QtCore
.Qt
.AlignCenter
)
609 commit_pen
= QtGui
.QPen()
610 commit_pen
.setWidth(1.0)
611 commit_pen
.setColor(outline_color
)
613 cached_commit_color
= QtGui
.QColor
.fromRgb(128, 222, 255)
614 cached_commit_selected_color
= QtGui
.QColor
.fromRgb(32, 64, 255)
615 cached_merge_color
= QtGui
.QColor
.fromRgb(255, 255, 255)
617 def __init__(self
, commit
,
619 selectable
=QtGui
.QGraphicsItem
.ItemIsSelectable
,
620 cursor
=QtCore
.Qt
.PointingHandCursor
,
622 commit_color
=cached_commit_color
,
623 commit_selected_color
=cached_commit_selected_color
,
624 merge_color
=cached_merge_color
):
626 QtGui
.QGraphicsItem
.__init
__(self
)
629 self
.setFlag(selectable
)
630 self
.setCursor(cursor
)
633 self
.notifier
= notifier
636 self
.label
= label
= Label(commit
)
637 label
.setParentItem(self
)
638 label
.setPos(xpos
, 0.)
642 if len(commit
.parents
) > 1:
643 self
.commit_color
= merge_color
645 self
.commit_color
= commit_color
646 self
.text_pen
= QtCore
.Qt
.black
647 self
.sha1_text
= commit
.sha1
[:8]
653 # Overridden Qt methods
656 def blockSignals(self
, blocked
):
657 self
.notifier
.notification_enabled
= not blocked
659 def itemChange(self
, change
, value
):
660 if change
== QtGui
.QGraphicsItem
.ItemSelectedHasChanged
:
661 # Broadcast selection to other widgets
662 selected_items
= self
.scene().selectedItems()
663 commits
= [item
.commit
for item
in selected_items
]
664 self
.scene().parent().set_selecting(True)
665 sig
= signals
.commits_selected
666 self
.notifier
.notify_observers(sig
, commits
)
667 self
.scene().parent().set_selecting(False)
669 # Cache the pen for use in paint()
670 if value
.toPyObject():
671 self
.commit_color
= self
.cached_commit_selected_color
672 self
.text_pen
= QtCore
.Qt
.white
673 color
= self
.selected_color
675 self
.text_pen
= QtCore
.Qt
.black
676 if len(self
.commit
.parents
) > 1:
677 self
.commit_color
= self
.cached_merge_color
679 self
.commit_color
= self
.cached_commit_color
680 color
= self
.outline_color
681 commit_pen
= QtGui
.QPen()
682 commit_pen
.setWidth(1.0)
683 commit_pen
.setColor(color
)
684 self
.commit_pen
= commit_pen
686 return QtGui
.QGraphicsItem
.itemChange(self
, change
, value
)
689 return self
.item_type
691 def boundingRect(self
, rect
=item_bbox
):
695 return self
.item_shape
697 def paint(self
, painter
, option
, widget
,
699 text_opts
=text_options
,
702 # Do not draw outside the exposed rect
703 painter
.setClipRect(option
.exposedRect
)
706 painter
.setPen(self
.commit_pen
)
707 painter
.setBrush(self
.commit_color
)
708 painter
.drawEllipse(inner
)
713 except AttributeError:
714 font
= cache
.font
= painter
.font()
716 painter
.setFont(font
)
717 painter
.setPen(self
.text_pen
)
718 painter
.drawText(inner
, self
.sha1_text
, text_opts
)
720 def mousePressEvent(self
, event
):
721 QtGui
.QGraphicsItem
.mousePressEvent(self
, event
)
723 self
.selected
= self
.isSelected()
725 def mouseMoveEvent(self
, event
):
728 QtGui
.QGraphicsItem
.mouseMoveEvent(self
, event
)
730 def mouseReleaseEvent(self
, event
):
731 QtGui
.QGraphicsItem
.mouseReleaseEvent(self
, event
)
732 if (not self
.dragged
and
734 event
.button() == QtCore
.Qt
.LeftButton
):
740 class Label(QtGui
.QGraphicsItem
):
741 item_type
= QtGui
.QGraphicsItem
.UserType
+ 3
746 item_shape
= QtGui
.QPainterPath()
747 item_shape
.addRect(0, 0, width
, height
)
748 item_bbox
= item_shape
.boundingRect()
750 text_options
= QtGui
.QTextOption()
751 text_options
.setAlignment(QtCore
.Qt
.AlignCenter
)
752 text_options
.setAlignment(QtCore
.Qt
.AlignVCenter
)
754 def __init__(self
, commit
,
755 other_color
=QtGui
.QColor
.fromRgb(255, 255, 64),
756 head_color
=QtGui
.QColor
.fromRgb(64, 255, 64)):
757 QtGui
.QGraphicsItem
.__init
__(self
)
760 # Starts with enough space for two tags. Any more and the commit
761 # needs to be taller to accomodate.
763 height
= len(commit
.tags
) * self
.height
/2. + 4. # +6 padding
765 self
.label_box
= QtCore
.QRectF(0., -height
/2., self
.width
, height
)
766 self
.text_box
= QtCore
.QRectF(2., -height
/2., self
.width
-4., height
)
767 self
.tag_text
= '\n'.join(commit
.tags
)
769 if 'HEAD' in commit
.tags
:
770 self
.color
= head_color
772 self
.color
= other_color
774 self
.pen
= QtGui
.QPen()
775 self
.pen
.setColor(self
.color
.darker())
776 self
.pen
.setWidth(1.0)
779 return self
.item_type
781 def boundingRect(self
, rect
=item_bbox
):
785 return self
.item_shape
787 def paint(self
, painter
, option
, widget
,
788 text_opts
=text_options
,
789 black
=QtCore
.Qt
.black
,
792 painter
.setBrush(self
.color
)
793 painter
.setPen(self
.pen
)
794 painter
.drawRoundedRect(self
.label_box
, 4, 4)
797 except AttributeError:
798 font
= cache
.font
= painter
.font()
800 painter
.setFont(font
)
801 painter
.setPen(black
)
802 painter
.drawText(self
.text_box
, self
.tag_text
, text_opts
)
805 class GraphView(QtGui
.QGraphicsView
):
806 def __init__(self
, notifier
):
807 QtGui
.QGraphicsView
.__init
__(self
)
815 self
.notifier
= notifier
820 self
.saved_matrix
= QtGui
.QMatrix(self
.matrix())
824 self
.is_panning
= False
826 self
.selecting
= False
827 self
.last_mouse
= [0, 0]
829 self
.setDragMode(self
.RubberBandDrag
)
831 scene
= QtGui
.QGraphicsScene(self
)
832 scene
.setItemIndexMethod(QtGui
.QGraphicsScene
.NoIndex
)
836 self
.setRenderHint(QtGui
.QPainter
.Antialiasing
)
837 self
.setOptimizationFlag(self
.DontAdjustForAntialiasing
, True)
838 self
.setViewportUpdateMode(self
.SmartViewportUpdate
)
839 self
.setCacheMode(QtGui
.QGraphicsView
.CacheBackground
)
840 self
.setTransformationAnchor(QtGui
.QGraphicsView
.AnchorUnderMouse
)
841 self
.setResizeAnchor(QtGui
.QGraphicsView
.NoAnchor
)
842 self
.setBackgroundBrush(QtGui
.QColor
.fromRgb(0, 0, 0))
844 self
.action_zoom_in
= (
845 qtutils
.add_action(self
, 'Zoom In',
848 QtCore
.Qt
.Key_Equal
))
850 self
.action_zoom_out
= (
851 qtutils
.add_action(self
, 'Zoom Out',
853 QtCore
.Qt
.Key_Minus
))
855 self
.action_zoom_fit
= (
856 qtutils
.add_action(self
, 'Zoom to Fit',
860 self
.action_select_parent
= (
861 qtutils
.add_action(self
, 'Select Parent',
865 self
.action_select_oldest_parent
= (
866 qtutils
.add_action(self
, 'Select Oldest Parent',
867 self
.select_oldest_parent
,
870 self
.action_select_child
= (
871 qtutils
.add_action(self
, 'Select Child',
875 self
.action_select_child
= (
876 qtutils
.add_action(self
, 'Select Nth Child',
877 self
.select_nth_child
,
880 self
.menu_actions
= context_menu_actions(self
)
882 sig
= signals
.commits_selected
883 notifier
.add_observer(sig
, self
.commits_selected
)
898 self
.scale_view(1.0/1.5)
900 def commits_selected(self
, commits
):
903 self
.select([commit
.sha1
for commit
in commits
])
905 def contextMenuEvent(self
, event
):
906 update_menu_actions(self
, event
)
907 context_menu_event(self
, event
)
909 def select(self
, sha1s
):
910 """Select the item for the SHA-1"""
911 self
.scene().clearSelection()
914 item
= self
.items
[sha1
]
917 item
.blockSignals(True)
918 item
.setSelected(True)
919 item
.blockSignals(False)
920 item_rect
= item
.sceneTransform().mapRect(item
.boundingRect())
921 self
.ensureVisible(item_rect
)
923 def selected_item(self
):
924 """Return the currently selected item"""
925 selected_items
= self
.selectedItems()
926 if not selected_items
:
928 return selected_items
[0]
930 def selectedItems(self
):
931 """Return the currently selected items"""
932 return self
.scene().selectedItems()
934 def get_item_by_generation(self
, commits
, criteria_fn
):
935 """Return the item for the commit matching criteria"""
939 for commit
in commits
:
940 if (generation
is None or
941 criteria_fn(generation
, commit
.generation
)):
943 generation
= commit
.generation
945 return self
.items
[sha1
]
949 def oldest_item(self
, commits
):
950 """Return the item for the commit with the oldest generation number"""
951 return self
.get_item_by_generation(commits
, lambda a
, b
: a
> b
)
953 def newest_item(self
, commits
):
954 """Return the item for the commit with the newest generation number"""
955 return self
.get_item_by_generation(commits
, lambda a
, b
: a
< b
)
957 def diff_this_selected(self
):
958 clicked_sha1
= self
.clicked
.commit
.sha1
959 selected_sha1
= self
.selected
.commit
.sha1
960 self
.emit(SIGNAL('diff_commits'), clicked_sha1
, selected_sha1
)
962 def diff_selected_this(self
):
963 clicked_sha1
= self
.clicked
.commit
.sha1
964 selected_sha1
= self
.selected
.commit
.sha1
965 self
.emit(SIGNAL('diff_commits'), selected_sha1
, clicked_sha1
)
967 def create_patch(self
):
968 items
= self
.selectedItems()
971 selected_commits
= sort_by_generation([n
.commit
for n
in items
])
972 sha1s
= [c
.sha1
for c
in selected_commits
]
973 all_sha1s
= [c
.sha1
for c
in self
.commits
]
974 cola
.notifier().broadcast(signals
.format_patch
, sha1s
, all_sha1s
)
976 def create_branch(self
):
977 sha1
= self
.clicked
.commit
.sha1
978 create_new_branch(revision
=sha1
)
980 def create_tag(self
):
981 sha1
= self
.clicked
.commit
.sha1
982 create_tag(revision
=sha1
)
984 def cherry_pick(self
):
985 sha1
= self
.clicked
.commit
.sha1
986 cola
.notifier().broadcast(signals
.cherry_pick
, [sha1
])
987 self
.notifier
.notify_observers(self
.notifier
.refs_updated
)
989 def select_parent(self
):
990 """Select the parent with the newest generation number"""
991 selected_item
= self
.selected_item()
992 if selected_item
is None:
994 parent_item
= self
.newest_item(selected_item
.commit
.parents
)
995 if parent_item
is None:
997 selected_item
.setSelected(False)
998 parent_item
.setSelected(True)
999 self
.ensureVisible(parent_item
.mapRectToScene(parent_item
.boundingRect()))
1001 def select_oldest_parent(self
):
1002 """Select the parent with the oldest generation number"""
1003 selected_item
= self
.selected_item()
1004 if selected_item
is None:
1006 parent_item
= self
.oldest_item(selected_item
.commit
.parents
)
1007 if parent_item
is None:
1009 selected_item
.setSelected(False)
1010 parent_item
.setSelected(True)
1011 self
.ensureVisible(parent_item
.mapRectToScene(parent_item
.boundingRect()))
1013 def select_child(self
):
1014 """Select the child with the oldest generation number"""
1015 selected_item
= self
.selected_item()
1016 if selected_item
is None:
1018 child_item
= self
.oldest_item(selected_item
.commit
.children
)
1019 if child_item
is None:
1021 selected_item
.setSelected(False)
1022 child_item
.setSelected(True)
1023 self
.ensureVisible(child_item
.mapRectToScene(child_item
.boundingRect()))
1025 def select_nth_child(self
):
1026 """Select the Nth child with the newest generation number (N > 1)"""
1027 selected_item
= self
.selected_item()
1028 if selected_item
is None:
1030 if len(selected_item
.commit
.children
) > 1:
1031 children
= selected_item
.commit
.children
[1:]
1033 children
= selected_item
.commit
.children
1034 child_item
= self
.newest_item(children
)
1035 if child_item
is None:
1037 selected_item
.setSelected(False)
1038 child_item
.setSelected(True)
1039 self
.ensureVisible(child_item
.mapRectToScene(child_item
.boundingRect()))
1042 """Fit selected items into the viewport"""
1044 items
= self
.scene().selectedItems()
1046 rect
= self
.scene().itemsBoundingRect()
1054 item_rect
= item
.boundingRect()
1055 x_off
= item_rect
.width()
1056 y_off
= item_rect
.height()
1057 x_min
= min(x_min
, pos
.x())
1058 y_min
= min(y_min
, pos
.y())
1059 x_max
= max(x_max
, pos
.x()+x_off
)
1060 ymax
= max(ymax
, pos
.y()+y_off
)
1061 rect
= QtCore
.QRectF(x_min
, y_min
, x_max
-x_min
, ymax
-y_min
)
1062 adjust
= Commit
.width
* 2
1063 rect
.setX(rect
.x() - adjust
)
1064 rect
.setY(rect
.y() - adjust
)
1065 rect
.setHeight(rect
.height() + adjust
)
1066 rect
.setWidth(rect
.width() + adjust
)
1067 self
.fitInView(rect
, QtCore
.Qt
.KeepAspectRatio
)
1068 self
.scene().invalidate()
1070 def save_selection(self
, event
):
1071 if event
.button() != QtCore
.Qt
.LeftButton
:
1073 elif QtCore
.Qt
.ShiftModifier
!= event
.modifiers():
1075 self
.selected
= self
.selectedItems()
1077 def restore_selection(self
, event
):
1078 if QtCore
.Qt
.ShiftModifier
!= event
.modifiers():
1080 for item
in self
.selected
:
1081 item
.setSelected(True)
1083 def handle_event(self
, event_handler
, event
):
1085 self
.save_selection(event
)
1086 event_handler(self
, event
)
1087 self
.restore_selection(event
)
1089 def mousePressEvent(self
, event
):
1090 if event
.button() == QtCore
.Qt
.MidButton
:
1092 self
.mouse_start
= [pos
.x(), pos
.y()]
1093 self
.saved_matrix
= QtGui
.QMatrix(self
.matrix())
1094 self
.is_panning
= True
1096 if event
.button() == QtCore
.Qt
.RightButton
:
1099 if event
.button() == QtCore
.Qt
.LeftButton
:
1101 self
.handle_event(QtGui
.QGraphicsView
.mousePressEvent
, event
)
1103 def mouseMoveEvent(self
, event
):
1104 pos
= self
.mapToScene(event
.pos())
1108 self
.last_mouse
[0] = pos
.x()
1109 self
.last_mouse
[1] = pos
.y()
1110 self
.handle_event(QtGui
.QGraphicsView
.mouseMoveEvent
, event
)
1112 def set_selecting(self
, selecting
):
1113 self
.selecting
= selecting
1115 def mouseReleaseEvent(self
, event
):
1116 self
.pressed
= False
1117 if event
.button() == QtCore
.Qt
.MidButton
:
1118 self
.is_panning
= False
1120 self
.handle_event(QtGui
.QGraphicsView
.mouseReleaseEvent
, event
)
1123 def pan(self
, event
):
1125 dx
= pos
.x() - self
.mouse_start
[0]
1126 dy
= pos
.y() - self
.mouse_start
[1]
1128 if dx
== 0 and dy
== 0:
1131 rect
= QtCore
.QRect(0, 0, abs(dx
), abs(dy
))
1132 delta
= self
.mapToScene(rect
).boundingRect()
1142 matrix
= QtGui
.QMatrix(self
.saved_matrix
).translate(tx
, ty
)
1143 self
.setTransformationAnchor(QtGui
.QGraphicsView
.NoAnchor
)
1144 self
.setMatrix(matrix
)
1146 def wheelEvent(self
, event
):
1147 """Handle Qt mouse wheel events."""
1148 if event
.modifiers() == QtCore
.Qt
.ControlModifier
:
1149 self
.wheel_zoom(event
)
1151 self
.wheel_pan(event
)
1153 def wheel_zoom(self
, event
):
1154 """Handle mouse wheel zooming."""
1155 zoom
= math
.pow(2.0, event
.delta() / 512.0)
1156 factor
= (self
.matrix()
1158 .mapRect(QtCore
.QRectF(0.0, 0.0, 1.0, 1.0))
1160 if factor
< 0.014 or factor
> 42.0:
1162 self
.setTransformationAnchor(QtGui
.QGraphicsView
.AnchorUnderMouse
)
1164 self
.scale(zoom
, zoom
)
1166 def wheel_pan(self
, event
):
1167 """Handle mouse wheel panning."""
1169 if event
.delta() < 0:
1173 pan_rect
= QtCore
.QRectF(0.0, 0.0, 1.0, 1.0)
1174 factor
= 1.0 / self
.matrix().mapRect(pan_rect
).width()
1176 if event
.orientation() == QtCore
.Qt
.Vertical
:
1177 matrix
= self
.matrix().translate(0, s
* factor
)
1179 matrix
= self
.matrix().translate(s
* factor
, 0)
1180 self
.setTransformationAnchor(QtGui
.QGraphicsView
.NoAnchor
)
1181 self
.setMatrix(matrix
)
1183 def scale_view(self
, scale
):
1184 factor
= (self
.matrix().scale(scale
, scale
)
1185 .mapRect(QtCore
.QRectF(0, 0, 1, 1))
1187 if factor
< 0.07 or factor
> 100:
1191 adjust_scrollbars
= True
1192 scrollbar
= self
.verticalScrollBar()
1194 value
= scrollbar
.value()
1195 min_
= scrollbar
.minimum()
1196 max_
= scrollbar
.maximum()
1197 range_
= max_
- min_
1198 distance
= value
- min_
1199 nonzero_range
= float(range_
) != 0.0
1201 scrolloffset
= distance
/float(range_
)
1203 adjust_scrollbars
= False
1205 self
.setTransformationAnchor(QtGui
.QGraphicsView
.NoAnchor
)
1206 self
.scale(scale
, scale
)
1208 scrollbar
= self
.verticalScrollBar()
1209 if scrollbar
and adjust_scrollbars
:
1210 min_
= scrollbar
.minimum()
1211 max_
= scrollbar
.maximum()
1212 range_
= max_
- min_
1213 value
= min_
+ int(float(range_
) * scrolloffset
)
1214 scrollbar
.setValue(value
)
1216 def add_commits(self
, commits
):
1217 """Traverse commits and add them to the view."""
1218 self
.commits
.extend(commits
)
1219 scene
= self
.scene()
1220 for commit
in commits
:
1221 item
= Commit(commit
, self
.notifier
)
1222 self
.items
[commit
.sha1
] = item
1223 for ref
in commit
.tags
:
1224 self
.items
[ref
] = item
1227 self
.layout(commits
)
1230 def link(self
, commits
):
1231 """Create edges linking commits with their parents"""
1232 scene
= self
.scene()
1233 for commit
in commits
:
1235 commit_item
= self
.items
[commit
.sha1
]
1237 # TODO - Handle truncated history viewing
1239 for parent
in commit
.parents
:
1241 parent_item
= self
.items
[parent
.sha1
]
1243 # TODO - Handle truncated history viewing
1245 edge
= Edge(parent_item
, commit_item
)
1248 def layout(self
, commits
):
1251 for commit
in commits
:
1252 generation
= commit
.generation
1255 row
= self
.rows
[generation
]
1257 row
= self
.rows
[generation
] = []
1259 xpos
= (len(commit
.parents
)-1) * self
.x_off
1261 xpos
+= row
[-1] + self
.x_off
1262 ypos
= -commit
.generation
* self
.y_off
1264 item
= self
.items
[sha1
]
1265 item
.setPos(xpos
, ypos
)
1268 x_max
= max(x_max
, xpos
)
1269 y_min
= min(y_min
, ypos
)
1273 self
.scene().setSceneRect(-self
.x_off
/2, y_min
-self
.y_off
,
1274 x_max
+self
.x_off
, abs(y_min
)+self
.y_off
*4)
1276 def sort_by_generation(commits
):
1277 commits
.sort(cmp=lambda a
, b
: cmp(a
.generation
, b
.generation
))
1281 def context_menu_actions(self
):
1283 'diff_this_selected':
1284 qtutils
.add_action(self
, 'Diff this -> selected',
1285 self
.diff_this_selected
),
1286 'diff_selected_this':
1287 qtutils
.add_action(self
, 'Diff selected -> this',
1288 self
.diff_selected_this
),
1290 qtutils
.add_action(self
, 'Create Branch',
1291 self
.create_branch
),
1293 qtutils
.add_action(self
, 'Create Patch',
1296 qtutils
.add_action(self
, 'Create Tag',
1299 qtutils
.add_action(self
, 'Save As Tarball/Zip...',
1300 lambda: create_tarball(self
)),
1302 qtutils
.add_action(self
, 'Cherry Pick',
1306 qtutils
.add_action(self
, 'Grab File...',
1307 lambda: save_blob_dialog(self
)),
1311 def update_menu_actions(self
, event
):
1312 clicked
= self
.itemAt(event
.pos())
1313 selected_items
= self
.selectedItems()
1314 has_single_selection
= len(selected_items
) == 1
1316 has_selection
= bool(selected_items
)
1317 can_diff
= bool(clicked
and has_single_selection
and
1318 clicked
is not selected_items
[0])
1320 self
.clicked
= clicked
1322 self
.selected
= selected_items
[0]
1324 self
.selected
= None
1326 self
.menu_actions
['diff_this_selected'].setEnabled(can_diff
)
1327 self
.menu_actions
['diff_selected_this'].setEnabled(can_diff
)
1328 self
.menu_actions
['create_patch'].setEnabled(has_selection
)
1329 self
.menu_actions
['create_tarball'].setEnabled(has_single_selection
)
1330 self
.menu_actions
['save_blob'].setEnabled(has_single_selection
)
1331 self
.menu_actions
['create_branch'].setEnabled(has_single_selection
)
1332 self
.menu_actions
['create_tag'].setEnabled(has_single_selection
)
1333 self
.menu_actions
['cherry_pick'].setEnabled(has_single_selection
)
1336 def context_menu_event(self
, event
):
1337 menu
= QtGui
.QMenu(self
)
1338 menu
.addAction(self
.menu_actions
['diff_this_selected'])
1339 menu
.addAction(self
.menu_actions
['diff_selected_this'])
1341 menu
.addAction(self
.menu_actions
['create_branch'])
1342 menu
.addAction(self
.menu_actions
['create_tag'])
1344 menu
.addAction(self
.menu_actions
['cherry_pick'])
1345 menu
.addAction(self
.menu_actions
['create_patch'])
1346 menu
.addAction(self
.menu_actions
['create_tarball'])
1348 menu
.addAction(self
.menu_actions
['save_blob'])
1349 menu
.exec_(self
.mapToGlobal(event
.pos()))
1352 def create_tarball(self
):
1353 ref
= self
.clicked
.commit
.sha1
1355 GitArchiveDialog
.save(ref
, shortref
, self
)
1358 def save_blob_dialog(self
):
1359 return BrowseDialog
.browse(self
.clicked
.commit
.sha1
)