5 from PyQt4
import QtGui
6 from PyQt4
import QtCore
7 from PyQt4
.QtCore
import SIGNAL
10 from cola
import observable
11 from cola
import qtutils
12 from cola
import signals
13 from cola
import gitcmds
14 from cola
import difftool
15 from cola
.dag
.model
import archive
16 from cola
.dag
.model
import RepoReader
17 from cola
.prefs
import diff_font
18 from cola
.qt
import DiffSyntaxHighlighter
19 from cola
.qt
import GitLogLineEdit
20 from cola
.widgets
import defs
21 from cola
.widgets
import standard
22 from cola
.widgets
.createbranch
import create_new_branch
23 from cola
.widgets
.createtag
import create_tag
24 from cola
.widgets
.archive
import GitArchiveDialog
25 from cola
.widgets
.browse
import BrowseDialog
28 class DiffWidget(QtGui
.QWidget
):
29 def __init__(self
, notifier
, parent
=None):
30 QtGui
.QWidget
.__init
__(self
, parent
)
32 self
.diff
= QtGui
.QTextEdit()
33 self
.diff
.setLineWrapMode(QtGui
.QTextEdit
.NoWrap
)
34 self
.diff
.setReadOnly(True)
35 self
.diff
.setFont(diff_font())
36 self
.highlighter
= DiffSyntaxHighlighter(self
.diff
.document())
38 self
.main_layout
= QtGui
.QHBoxLayout()
39 self
.main_layout
.addWidget(self
.diff
)
40 self
.main_layout
.setMargin(0)
41 self
.main_layout
.setSpacing(defs
.spacing
)
42 self
.setLayout(self
.main_layout
)
44 sig
= signals
.commits_selected
45 notifier
.add_observer(sig
, self
.commits_selected
)
47 def commits_selected(self
, commits
):
52 merge
= len(commit
.parents
) > 1
53 self
.diff
.setText(gitcmds
.diff_info(sha1
, merge
=merge
))
54 qtutils
.set_clipboard(sha1
)
57 class CommitTreeWidgetItem(QtGui
.QTreeWidgetItem
):
58 def __init__(self
, commit
, parent
=None):
59 QtGui
.QListWidgetItem
.__init
__(self
, parent
)
61 self
.setText(0, commit
.summary
)
62 self
.setText(1, commit
.author
)
63 self
.setText(2, commit
.authdate
)
66 class CommitTreeWidget(QtGui
.QTreeWidget
):
67 def __init__(self
, notifier
, parent
=None):
68 QtGui
.QTreeWidget
.__init
__(self
, parent
)
69 self
.setSelectionMode(self
.ContiguousSelection
)
70 self
.setUniformRowHeights(True)
71 self
.setAllColumnsShowFocus(True)
72 self
.setAlternatingRowColors(True)
73 self
.setRootIsDecorated(False)
74 self
.setHeaderLabels(['Summary', 'Author', 'Date'])
77 self
.notifier
= notifier
78 self
.selecting
= False
82 self
.menu_actions
= context_menu_actions(self
)
84 self
.action_up
= qtutils
.add_action(self
, 'Go Up', self
.go_up
,
87 self
.action_down
= qtutils
.add_action(self
, 'Go Down', self
.go_down
,
90 sig
= signals
.commits_selected
91 notifier
.add_observer(sig
, self
.commits_selected
)
93 self
.connect(self
, SIGNAL('itemSelectionChanged()'),
94 self
.selection_changed
)
96 def contextMenuEvent(self
, event
):
97 update_menu_actions(self
, event
)
98 context_menu_event(self
, event
)
100 def mousePressEvent(self
, event
):
101 if event
.buttons() == QtCore
.Qt
.RightButton
:
104 if event
.modifiers() == QtCore
.Qt
.MetaModifier
:
107 super(CommitTreeWidget
, self
).mousePressEvent(event
)
110 self
.goto(self
.itemAbove
)
113 self
.goto(self
.itemBelow
)
115 def goto(self
, finder
):
116 items
= self
.selectedItems()
117 item
= items
and items
[0] or None
122 self
.select([found
.commit
.sha1
], block_signals
=False)
124 def set_selecting(self
, selecting
):
125 self
.selecting
= selecting
127 def selection_changed(self
):
128 items
= self
.selectedItems()
131 self
.set_selecting(True)
132 sig
= signals
.commits_selected
133 self
.notifier
.notify_observers(sig
, [i
.commit
for i
in items
])
134 self
.set_selecting(False)
136 def commits_selected(self
, commits
):
139 self
.select([commit
.sha1
for commit
in commits
])
141 def select(self
, sha1s
, block_signals
=True):
142 self
.clearSelection()
145 item
= self
.sha1map
[sha1
]
148 block
= self
.blockSignals(block_signals
)
149 self
.scrollToItem(item
)
150 item
.setSelected(True)
151 self
.blockSignals(block
)
153 def adjust_columns(self
):
154 width
= self
.width()-20
157 self
.setColumnWidth(0, zero
)
158 self
.setColumnWidth(1, onetwo
)
159 self
.setColumnWidth(2, onetwo
)
162 QtGui
.QTreeWidget
.clear(self
)
166 def add_commits(self
, commits
):
167 self
.commits
.extend(commits
)
169 for c
in reversed(commits
):
170 item
= CommitTreeWidgetItem(c
)
172 self
.sha1map
[c
.sha1
] = item
174 self
.sha1map
[tag
] = item
175 self
.insertTopLevelItems(0, items
)
177 def diff_this_selected(self
):
178 clicked_sha1
= self
.clicked
.commit
.sha1
179 selected_sha1
= self
.selected
.commit
.sha1
180 self
.emit(SIGNAL('diff_commits'), clicked_sha1
, selected_sha1
)
182 def diff_selected_this(self
):
183 clicked_sha1
= self
.clicked
.commit
.sha1
184 selected_sha1
= self
.selected
.commit
.sha1
185 self
.emit(SIGNAL('diff_commits'), selected_sha1
, clicked_sha1
)
187 def create_patch(self
):
188 items
= self
.selectedItems()
192 sha1s
= [item
.commit
.sha1
for item
in items
]
193 all_sha1s
= [c
.sha1
for c
in self
.commits
]
194 cola
.notifier().broadcast(signals
.format_patch
, sha1s
, all_sha1s
)
196 def create_branch(self
):
197 sha1
= self
.clicked
.commit
.sha1
198 create_new_branch(revision
=sha1
)
200 def create_tag(self
):
201 sha1
= self
.clicked
.commit
.sha1
202 create_tag(revision
=sha1
)
204 def cherry_pick(self
):
205 sha1
= self
.clicked
.commit
.sha1
206 cola
.notifier().broadcast(signals
.cherry_pick
, [sha1
])
209 class DAGView(standard
.Widget
):
210 """The git-dag widget."""
212 def __init__(self
, model
, dag
, parent
=None, args
=None):
213 super(DAGView
, self
).__init
__(parent
)
214 self
.setAttribute(QtCore
.Qt
.WA_MacMetalStyle
)
215 self
.setMinimumSize(1, 1)
217 # change when widgets are added/removed
218 self
.widget_version
= 1
222 self
.revtext
= GitLogLineEdit(parent
=self
)
224 self
.maxresults
= QtGui
.QSpinBox()
225 self
.maxresults
.setMinimum(1)
226 self
.maxresults
.setMaximum(99999)
227 self
.maxresults
.setPrefix('git log -')
228 self
.maxresults
.setSuffix('')
230 self
.displaybutton
= QtGui
.QPushButton()
231 self
.displaybutton
.setText('Display')
233 self
.zoom_in
= QtGui
.QPushButton()
234 self
.zoom_in
.setIcon(qtutils
.theme_icon('zoom-in.png'))
235 self
.zoom_in
.setFlat(True)
237 self
.zoom_out
= QtGui
.QPushButton()
238 self
.zoom_out
.setIcon(qtutils
.theme_icon('zoom-out.png'))
239 self
.zoom_out
.setFlat(True)
241 self
.top_layout
= QtGui
.QHBoxLayout()
242 self
.top_layout
.setMargin(defs
.margin
)
243 self
.top_layout
.setSpacing(defs
.button_spacing
)
245 self
.top_layout
.addWidget(self
.maxresults
)
246 self
.top_layout
.addWidget(self
.revtext
)
247 self
.top_layout
.addWidget(self
.displaybutton
)
248 self
.top_layout
.addStretch()
249 self
.top_layout
.addWidget(self
.zoom_out
)
250 self
.top_layout
.addWidget(self
.zoom_in
)
253 self
.notifier
= notifier
= observable
.Observable()
254 self
.notifier
.refs_updated
= refs_updated
= 'refs_updated'
255 self
.notifier
.add_observer(refs_updated
, self
.display
)
257 self
.graphview
= GraphView(notifier
)
258 self
.treewidget
= CommitTreeWidget(notifier
)
259 self
.diffwidget
= DiffWidget(notifier
)
261 for signal
in (archive
,):
262 qtutils
.relay_signal(self
, self
.graphview
, SIGNAL(signal
))
263 qtutils
.relay_signal(self
, self
.treewidget
, SIGNAL(signal
))
265 self
.splitter
= QtGui
.QSplitter()
266 self
.splitter
.setOrientation(QtCore
.Qt
.Horizontal
)
267 self
.splitter
.setChildrenCollapsible(True)
268 self
.splitter
.setHandleWidth(defs
.handle_width
)
270 self
.left_splitter
= QtGui
.QSplitter()
271 self
.left_splitter
.setOrientation(QtCore
.Qt
.Vertical
)
272 self
.left_splitter
.setChildrenCollapsible(True)
273 self
.left_splitter
.setHandleWidth(defs
.handle_width
)
274 self
.left_splitter
.setStretchFactor(0, 1)
275 self
.left_splitter
.setStretchFactor(1, 1)
276 self
.left_splitter
.insertWidget(0, self
.treewidget
)
277 self
.left_splitter
.insertWidget(1, self
.diffwidget
)
279 self
.splitter
.insertWidget(0, self
.left_splitter
)
280 self
.splitter
.insertWidget(1, self
.graphview
)
282 self
.splitter
.setStretchFactor(0, 1)
283 self
.splitter
.setStretchFactor(1, 1)
285 self
.main_layout
= layout
= QtGui
.QVBoxLayout()
288 layout
.addLayout(self
.top_layout
)
289 layout
.addWidget(self
.splitter
)
290 self
.setLayout(layout
)
292 # Also re-loads dag.* from the saved state
293 if not qtutils
.apply_state(self
):
294 self
.resize_to_desktop()
296 # Update fields affected by model
297 self
.revtext
.setText(dag
.ref
)
298 self
.maxresults
.setValue(dag
.count
)
299 self
.update_window_title()
301 self
.thread
= ReaderThread(self
, dag
)
303 self
.thread
.connect(self
.thread
, self
.thread
.commits_ready
,
306 self
.thread
.connect(self
.thread
, self
.thread
.done
,
309 self
.connect(self
.splitter
, SIGNAL('splitterMoved(int,int)'),
312 self
.connect(self
.zoom_in
, SIGNAL('pressed()'),
313 self
.graphview
.zoom_in
)
315 self
.connect(self
.zoom_out
, SIGNAL('pressed()'),
316 self
.graphview
.zoom_out
)
318 self
.connect(self
.treewidget
, SIGNAL('diff_commits'),
321 self
.connect(self
.graphview
, SIGNAL('diff_commits'),
324 self
.connect(self
.maxresults
, SIGNAL('valueChanged(int)'),
325 lambda(x
): self
.dag
.set_count(x
))
327 self
.connect(self
.displaybutton
, SIGNAL('pressed()'),
330 self
.connect(self
.revtext
, SIGNAL('ref_changed'),
333 self
.connect(self
.revtext
, SIGNAL('textChanged(QString)'),
336 # The model is updated in another thread so use
337 # signals/slots to bring control back to the main GUI thread
338 self
.model
.add_observer(self
.model
.message_updated
,
339 self
.emit_model_updated
)
341 self
.connect(self
, SIGNAL('model_updated'),
344 qtutils
.add_close_action(self
)
346 def text_changed(self
, txt
):
347 self
.dag
.ref
= unicode(txt
)
348 self
.update_window_title()
350 def update_window_title(self
):
351 project
= self
.model
.project
353 self
.setWindowTitle('%s: %s' % (project
, self
.dag
.ref
))
355 self
.setWindowTitle(project
)
357 def export_state(self
):
358 state
= super(DAGView
, self
).export_state()
359 state
['count'] = self
.dag
.count
362 def apply_state(self
, state
):
364 super(DAGView
, self
).apply_state(state
)
368 count
= state
['count']
372 if not self
.dag
.overridden('count'):
373 self
.dag
.set_count(count
)
375 def emit_model_updated(self
):
376 self
.emit(SIGNAL('model_updated'))
378 def model_updated(self
):
380 self
.revtext
.update_matches()
382 if not self
.model
.currentbranch
:
384 self
.revtext
.setText(self
.model
.currentbranch
)
388 new_ref
= unicode(self
.revtext
.text())
393 self
.dag
.set_ref(new_ref
)
394 self
.dag
.set_count(self
.maxresults
.value())
398 super(DAGView
, self
).show()
399 self
.splitter
.setSizes([self
.width()/2, self
.width()/2])
400 self
.left_splitter
.setSizes([self
.height()/3, self
.height()*2/3])
401 self
.treewidget
.adjust_columns()
403 def resizeEvent(self
, e
):
404 super(DAGView
, self
).resizeEvent(e
)
405 self
.treewidget
.adjust_columns()
407 def splitter_moved(self
, pos
, idx
):
408 self
.treewidget
.adjust_columns()
411 self
.graphview
.clear()
412 self
.treewidget
.clear()
415 def add_commits(self
, commits
):
416 # Keep track of commits
417 for commit_obj
in commits
:
418 self
.commits
[commit_obj
.sha1
] = commit_obj
419 for tag
in commit_obj
.tags
:
420 self
.commits
[tag
] = commit_obj
421 self
.graphview
.add_commits(commits
)
422 self
.treewidget
.add_commits(commits
)
424 def thread_done(self
):
426 commit_obj
= self
.commits
[self
.dag
.ref
]
429 sig
= signals
.commits_selected
430 self
.notifier
.notify_observers(sig
, [commit_obj
])
431 self
.graphview
.update_scene_rect()
432 self
.graphview
.view_fit()
434 def closeEvent(self
, event
):
435 self
.revtext
.close_popup()
437 qtutils
.save_state(self
)
438 return super(DAGView
, self
).closeEvent(event
)
441 self
.thread
.mutex
.lock()
442 self
.thread
.stop
= True
443 self
.thread
.mutex
.unlock()
446 self
.thread
.abort
= True
450 self
.thread
.abort
= False
451 self
.thread
.stop
= False
455 self
.thread
.mutex
.lock()
456 self
.thread
.stop
= False
457 self
.thread
.mutex
.unlock()
458 self
.thread
.condition
.wakeOne()
460 def resize_to_desktop(self
):
461 desktop
= QtGui
.QApplication
.instance().desktop()
462 width
= desktop
.width()
463 height
= desktop
.height()
464 self
.resize(width
, height
)
466 def diff_commits(self
, a
, b
):
467 paths
= self
.dag
.paths()
469 difftool
.launch([a
, b
, '--'] + paths
)
471 difftool
.diff_commits(self
, a
, b
)
474 class ReaderThread(QtCore
.QThread
):
476 commits_ready
= SIGNAL('commits_ready')
477 done
= SIGNAL('done')
479 def __init__(self
, parent
, dag
):
480 QtCore
.QThread
.__init
__(self
, parent
)
484 self
.mutex
= QtCore
.QMutex()
485 self
.condition
= QtCore
.QWaitCondition()
488 repo
= RepoReader(self
.dag
)
494 self
.condition
.wait(self
.mutex
)
500 if len(commits
) >= 512:
501 self
.emit(self
.commits_ready
, commits
)
505 self
.emit(self
.commits_ready
, commits
)
513 class Edge(QtGui
.QGraphicsItem
):
514 item_type
= QtGui
.QGraphicsItem
.UserType
+ 1
516 arrow_extra
= (arrow_size
+1.0)/2.0
518 pen
= QtGui
.QPen(QtCore
.Qt
.gray
, 1.0,
523 def __init__(self
, source
, dest
,
525 arrow_size
=arrow_size
):
527 QtGui
.QGraphicsItem
.__init
__(self
)
529 self
.setAcceptedMouseButtons(QtCore
.Qt
.NoButton
)
534 dest_pt
= Commit
.item_bbox
.center()
536 self
.source_pt
= self
.mapFromItem(self
.source
, dest_pt
)
537 self
.dest_pt
= self
.mapFromItem(self
.dest
, dest_pt
)
538 self
.line
= QtCore
.QLineF(self
.source_pt
, self
.dest_pt
)
540 width
= self
.dest_pt
.x() - self
.source_pt
.x()
541 height
= self
.dest_pt
.y() - self
.source_pt
.y()
542 rect
= QtCore
.QRectF(self
.source_pt
, QtCore
.QSizeF(width
, height
))
543 self
.bound
= rect
.normalized().adjusted(-extra
, -extra
, extra
, extra
)
546 return self
.item_type
548 def boundingRect(self
):
551 def paint(self
, painter
, option
, widget
,
552 arrow_size
=arrow_size
,
553 gray
=QtCore
.Qt
.gray
):
555 painter
.setPen(self
.pen
)
556 painter
.drawLine(self
.line
)
559 class Commit(QtGui
.QGraphicsItem
):
560 item_type
= QtGui
.QGraphicsItem
.UserType
+ 2
564 item_shape
= QtGui
.QPainterPath()
565 item_shape
.addRect(width
/-2., height
/-2., width
, height
)
566 item_bbox
= item_shape
.boundingRect()
568 inner_rect
= QtGui
.QPainterPath()
569 inner_rect
.addRect(width
/-2.+2., height
/-2.+2, width
-4., height
-4.)
570 inner_rect
= inner_rect
.boundingRect()
572 selected_color
= QtGui
.QColor
.fromRgb(255, 255, 0)
573 outline_color
= QtGui
.QColor
.fromRgb(64, 96, 192)
576 text_options
= QtGui
.QTextOption()
577 text_options
.setAlignment(QtCore
.Qt
.AlignCenter
)
579 commit_pen
= QtGui
.QPen()
580 commit_pen
.setWidth(1.0)
581 commit_pen
.setColor(outline_color
)
583 cached_commit_color
= QtGui
.QColor
.fromRgb(128, 222, 255)
584 cached_commit_selected_color
= QtGui
.QColor
.fromRgb(32, 64, 255)
585 cached_merge_color
= QtGui
.QColor
.fromRgb(255, 255, 255)
587 def __init__(self
, commit
,
589 selectable
=QtGui
.QGraphicsItem
.ItemIsSelectable
,
590 cursor
=QtCore
.Qt
.PointingHandCursor
,
592 commit_color
=cached_commit_color
,
593 commit_selected_color
=cached_commit_selected_color
,
594 merge_color
=cached_merge_color
):
596 QtGui
.QGraphicsItem
.__init
__(self
)
599 self
.setFlag(selectable
)
600 self
.setCursor(cursor
)
603 self
.notifier
= notifier
606 self
.label
= label
= Label(commit
)
607 label
.setParentItem(self
)
608 label
.setPos(xpos
, 0.)
612 if len(commit
.parents
) > 1:
613 self
.commit_color
= merge_color
615 self
.commit_color
= commit_color
616 self
.text_pen
= QtCore
.Qt
.black
617 self
.sha1_text
= commit
.sha1
[:8]
623 # Overridden Qt methods
626 def blockSignals(self
, blocked
):
627 self
.notifier
.notification_enabled
= not blocked
629 def itemChange(self
, change
, value
):
630 if change
== QtGui
.QGraphicsItem
.ItemSelectedHasChanged
:
631 # Broadcast selection to other widgets
632 selected_items
= self
.scene().selectedItems()
633 commits
= [item
.commit
for item
in selected_items
]
634 self
.scene().parent().set_selecting(True)
635 sig
= signals
.commits_selected
636 self
.notifier
.notify_observers(sig
, commits
)
637 self
.scene().parent().set_selecting(False)
639 # Cache the pen for use in paint()
640 if value
.toPyObject():
641 self
.commit_color
= self
.cached_commit_selected_color
642 self
.text_pen
= QtCore
.Qt
.white
643 color
= self
.selected_color
645 self
.text_pen
= QtCore
.Qt
.black
646 if len(self
.commit
.parents
) > 1:
647 self
.commit_color
= self
.cached_merge_color
649 self
.commit_color
= self
.cached_commit_color
650 color
= self
.outline_color
651 commit_pen
= QtGui
.QPen()
652 commit_pen
.setWidth(1.0)
653 commit_pen
.setColor(color
)
654 self
.commit_pen
= commit_pen
656 return QtGui
.QGraphicsItem
.itemChange(self
, change
, value
)
659 return self
.item_type
661 def boundingRect(self
, rect
=item_bbox
):
665 return self
.item_shape
667 def paint(self
, painter
, option
, widget
,
669 text_opts
=text_options
,
672 # Do not draw outside the exposed rect
673 painter
.setClipRect(option
.exposedRect
)
676 painter
.setPen(self
.commit_pen
)
677 painter
.setBrush(self
.commit_color
)
678 painter
.drawEllipse(inner
)
683 except AttributeError:
684 font
= cache
.font
= painter
.font()
686 painter
.setFont(font
)
687 painter
.setPen(self
.text_pen
)
688 painter
.drawText(inner
, self
.sha1_text
, text_opts
)
690 def mousePressEvent(self
, event
):
691 QtGui
.QGraphicsItem
.mousePressEvent(self
, event
)
693 self
.selected
= self
.isSelected()
695 def mouseMoveEvent(self
, event
):
698 QtGui
.QGraphicsItem
.mouseMoveEvent(self
, event
)
700 def mouseReleaseEvent(self
, event
):
701 QtGui
.QGraphicsItem
.mouseReleaseEvent(self
, event
)
702 if (not self
.dragged
and
704 event
.button() == QtCore
.Qt
.LeftButton
):
710 class Label(QtGui
.QGraphicsItem
):
711 item_type
= QtGui
.QGraphicsItem
.UserType
+ 3
716 item_shape
= QtGui
.QPainterPath()
717 item_shape
.addRect(0, 0, width
, height
)
718 item_bbox
= item_shape
.boundingRect()
720 text_options
= QtGui
.QTextOption()
721 text_options
.setAlignment(QtCore
.Qt
.AlignCenter
)
722 text_options
.setAlignment(QtCore
.Qt
.AlignVCenter
)
724 def __init__(self
, commit
,
725 other_color
=QtGui
.QColor
.fromRgb(255, 255, 64),
726 head_color
=QtGui
.QColor
.fromRgb(64, 255, 64)):
727 QtGui
.QGraphicsItem
.__init
__(self
)
730 # Starts with enough space for two tags. Any more and the commit
731 # needs to be taller to accomodate.
733 height
= len(commit
.tags
) * self
.height
/2. + 4. # +6 padding
735 self
.label_box
= QtCore
.QRectF(0., -height
/2., self
.width
, height
)
736 self
.text_box
= QtCore
.QRectF(2., -height
/2., self
.width
-4., height
)
737 self
.tag_text
= '\n'.join(commit
.tags
)
739 if 'HEAD' in commit
.tags
:
740 self
.color
= head_color
742 self
.color
= other_color
744 self
.pen
= QtGui
.QPen()
745 self
.pen
.setColor(self
.color
.darker())
746 self
.pen
.setWidth(1.0)
749 return self
.item_type
751 def boundingRect(self
, rect
=item_bbox
):
755 return self
.item_shape
757 def paint(self
, painter
, option
, widget
,
758 text_opts
=text_options
,
759 black
=QtCore
.Qt
.black
,
762 painter
.setBrush(self
.color
)
763 painter
.setPen(self
.pen
)
764 painter
.drawRoundedRect(self
.label_box
, 4, 4)
767 except AttributeError:
768 font
= cache
.font
= painter
.font()
770 painter
.setFont(font
)
771 painter
.setPen(black
)
772 painter
.drawText(self
.text_box
, self
.tag_text
, text_opts
)
775 class GraphView(QtGui
.QGraphicsView
):
776 def __init__(self
, notifier
):
777 QtGui
.QGraphicsView
.__init
__(self
)
785 self
.notifier
= notifier
790 self
.saved_matrix
= QtGui
.QMatrix(self
.matrix())
792 self
.x_offsets
= collections
.defaultdict(int)
794 self
.is_panning
= False
796 self
.selecting
= False
797 self
.last_mouse
= [0, 0]
799 self
.setDragMode(self
.RubberBandDrag
)
801 scene
= QtGui
.QGraphicsScene(self
)
802 scene
.setItemIndexMethod(QtGui
.QGraphicsScene
.NoIndex
)
806 self
.setRenderHint(QtGui
.QPainter
.Antialiasing
)
807 self
.setOptimizationFlag(self
.DontAdjustForAntialiasing
, True)
808 self
.setViewportUpdateMode(self
.SmartViewportUpdate
)
809 self
.setCacheMode(QtGui
.QGraphicsView
.CacheBackground
)
810 self
.setTransformationAnchor(QtGui
.QGraphicsView
.AnchorUnderMouse
)
811 self
.setResizeAnchor(QtGui
.QGraphicsView
.NoAnchor
)
812 self
.setBackgroundBrush(QtGui
.QColor
.fromRgb(0, 0, 0))
814 self
.action_zoom_in
= (
815 qtutils
.add_action(self
, 'Zoom In',
818 QtCore
.Qt
.Key_Equal
))
820 self
.action_zoom_out
= (
821 qtutils
.add_action(self
, 'Zoom Out',
823 QtCore
.Qt
.Key_Minus
))
825 self
.action_zoom_fit
= (
826 qtutils
.add_action(self
, 'Zoom to Fit',
830 self
.action_select_parent
= (
831 qtutils
.add_action(self
, 'Select Parent',
835 self
.action_select_oldest_parent
= (
836 qtutils
.add_action(self
, 'Select Oldest Parent',
837 self
.select_oldest_parent
,
840 self
.action_select_child
= (
841 qtutils
.add_action(self
, 'Select Child',
845 self
.action_select_child
= (
846 qtutils
.add_action(self
, 'Select Nth Child',
847 self
.select_nth_child
,
850 self
.menu_actions
= context_menu_actions(self
)
852 sig
= signals
.commits_selected
853 notifier
.add_observer(sig
, self
.commits_selected
)
859 self
.x_offsets
.clear()
868 self
.scale_view(1.0/1.5)
870 def commits_selected(self
, commits
):
873 self
.select([commit
.sha1
for commit
in commits
])
875 def contextMenuEvent(self
, event
):
876 update_menu_actions(self
, event
)
877 context_menu_event(self
, event
)
879 def select(self
, sha1s
):
880 """Select the item for the SHA-1"""
881 self
.scene().clearSelection()
884 item
= self
.items
[sha1
]
887 item
.blockSignals(True)
888 item
.setSelected(True)
889 item
.blockSignals(False)
890 item_rect
= item
.sceneTransform().mapRect(item
.boundingRect())
891 self
.ensureVisible(item_rect
)
893 def selected_item(self
):
894 """Return the currently selected item"""
895 selected_items
= self
.selectedItems()
896 if not selected_items
:
898 return selected_items
[0]
900 def selectedItems(self
):
901 """Return the currently selected items"""
902 return self
.scene().selectedItems()
904 def get_item_by_generation(self
, commits
, criteria_fn
):
905 """Return the item for the commit matching criteria"""
909 for commit
in commits
:
910 if (generation
is None or
911 criteria_fn(generation
, commit
.generation
)):
913 generation
= commit
.generation
915 return self
.items
[sha1
]
919 def oldest_item(self
, commits
):
920 """Return the item for the commit with the oldest generation number"""
921 return self
.get_item_by_generation(commits
, lambda a
, b
: a
> b
)
923 def newest_item(self
, commits
):
924 """Return the item for the commit with the newest generation number"""
925 return self
.get_item_by_generation(commits
, lambda a
, b
: a
< b
)
927 def diff_this_selected(self
):
928 clicked_sha1
= self
.clicked
.commit
.sha1
929 selected_sha1
= self
.selected
.commit
.sha1
930 self
.emit(SIGNAL('diff_commits'), clicked_sha1
, selected_sha1
)
932 def diff_selected_this(self
):
933 clicked_sha1
= self
.clicked
.commit
.sha1
934 selected_sha1
= self
.selected
.commit
.sha1
935 self
.emit(SIGNAL('diff_commits'), selected_sha1
, clicked_sha1
)
937 def create_patch(self
):
938 items
= self
.selectedItems()
941 selected_commits
= sort_by_generation([n
.commit
for n
in items
])
942 sha1s
= [c
.sha1
for c
in selected_commits
]
943 all_sha1s
= [c
.sha1
for c
in self
.commits
]
944 cola
.notifier().broadcast(signals
.format_patch
, sha1s
, all_sha1s
)
946 def create_branch(self
):
947 sha1
= self
.clicked
.commit
.sha1
948 create_new_branch(revision
=sha1
)
950 def create_tag(self
):
951 sha1
= self
.clicked
.commit
.sha1
952 create_tag(revision
=sha1
)
954 def cherry_pick(self
):
955 sha1
= self
.clicked
.commit
.sha1
956 cola
.notifier().broadcast(signals
.cherry_pick
, [sha1
])
957 self
.notifier
.notify_observers(self
.notifier
.refs_updated
)
959 def select_parent(self
):
960 """Select the parent with the newest generation number"""
961 selected_item
= self
.selected_item()
962 if selected_item
is None:
964 parent_item
= self
.newest_item(selected_item
.commit
.parents
)
965 if parent_item
is None:
967 selected_item
.setSelected(False)
968 parent_item
.setSelected(True)
969 self
.ensureVisible(parent_item
.mapRectToScene(parent_item
.boundingRect()))
971 def select_oldest_parent(self
):
972 """Select the parent with the oldest generation number"""
973 selected_item
= self
.selected_item()
974 if selected_item
is None:
976 parent_item
= self
.oldest_item(selected_item
.commit
.parents
)
977 if parent_item
is None:
979 selected_item
.setSelected(False)
980 parent_item
.setSelected(True)
981 self
.ensureVisible(parent_item
.mapRectToScene(parent_item
.boundingRect()))
983 def select_child(self
):
984 """Select the child with the oldest generation number"""
985 selected_item
= self
.selected_item()
986 if selected_item
is None:
988 child_item
= self
.oldest_item(selected_item
.commit
.children
)
989 if child_item
is None:
991 selected_item
.setSelected(False)
992 child_item
.setSelected(True)
993 self
.ensureVisible(child_item
.mapRectToScene(child_item
.boundingRect()))
995 def select_nth_child(self
):
996 """Select the Nth child with the newest generation number (N > 1)"""
997 selected_item
= self
.selected_item()
998 if selected_item
is None:
1000 if len(selected_item
.commit
.children
) > 1:
1001 children
= selected_item
.commit
.children
[1:]
1003 children
= selected_item
.commit
.children
1004 child_item
= self
.newest_item(children
)
1005 if child_item
is None:
1007 selected_item
.setSelected(False)
1008 child_item
.setSelected(True)
1009 self
.ensureVisible(child_item
.mapRectToScene(child_item
.boundingRect()))
1012 """Fit selected items into the viewport"""
1014 items
= self
.scene().selectedItems()
1016 rect
= self
.scene().itemsBoundingRect()
1024 item_rect
= item
.boundingRect()
1025 x_off
= item_rect
.width()
1026 y_off
= item_rect
.height()
1027 x_min
= min(x_min
, pos
.x())
1028 y_min
= min(y_min
, pos
.y())
1029 x_max
= max(x_max
, pos
.x()+x_off
)
1030 ymax
= max(ymax
, pos
.y()+y_off
)
1031 rect
= QtCore
.QRectF(x_min
, y_min
, x_max
-x_min
, ymax
-y_min
)
1032 adjust
= Commit
.width
* 2
1033 rect
.setX(rect
.x() - adjust
)
1034 rect
.setY(rect
.y() - adjust
)
1035 rect
.setHeight(rect
.height() + adjust
)
1036 rect
.setWidth(rect
.width() + adjust
)
1037 self
.fitInView(rect
, QtCore
.Qt
.KeepAspectRatio
)
1038 self
.scene().invalidate()
1040 def save_selection(self
, event
):
1041 if event
.button() != QtCore
.Qt
.LeftButton
:
1043 elif QtCore
.Qt
.ShiftModifier
!= event
.modifiers():
1045 self
.selected
= self
.selectedItems()
1047 def restore_selection(self
, event
):
1048 if QtCore
.Qt
.ShiftModifier
!= event
.modifiers():
1050 for item
in self
.selected
:
1051 item
.setSelected(True)
1053 def handle_event(self
, event_handler
, event
):
1055 self
.save_selection(event
)
1056 event_handler(self
, event
)
1057 self
.restore_selection(event
)
1059 def mousePressEvent(self
, event
):
1060 if event
.button() == QtCore
.Qt
.MidButton
:
1062 self
.mouse_start
= [pos
.x(), pos
.y()]
1063 self
.saved_matrix
= QtGui
.QMatrix(self
.matrix())
1064 self
.is_panning
= True
1066 if event
.button() == QtCore
.Qt
.RightButton
:
1069 if event
.button() == QtCore
.Qt
.LeftButton
:
1071 self
.handle_event(QtGui
.QGraphicsView
.mousePressEvent
, event
)
1073 def mouseMoveEvent(self
, event
):
1074 pos
= self
.mapToScene(event
.pos())
1078 self
.last_mouse
[0] = pos
.x()
1079 self
.last_mouse
[1] = pos
.y()
1080 self
.handle_event(QtGui
.QGraphicsView
.mouseMoveEvent
, event
)
1082 def set_selecting(self
, selecting
):
1083 self
.selecting
= selecting
1085 def mouseReleaseEvent(self
, event
):
1086 self
.pressed
= False
1087 if event
.button() == QtCore
.Qt
.MidButton
:
1088 self
.is_panning
= False
1090 self
.handle_event(QtGui
.QGraphicsView
.mouseReleaseEvent
, event
)
1093 def pan(self
, event
):
1095 dx
= pos
.x() - self
.mouse_start
[0]
1096 dy
= pos
.y() - self
.mouse_start
[1]
1098 if dx
== 0 and dy
== 0:
1101 rect
= QtCore
.QRect(0, 0, abs(dx
), abs(dy
))
1102 delta
= self
.mapToScene(rect
).boundingRect()
1112 matrix
= QtGui
.QMatrix(self
.saved_matrix
).translate(tx
, ty
)
1113 self
.setTransformationAnchor(QtGui
.QGraphicsView
.NoAnchor
)
1114 self
.setMatrix(matrix
)
1116 def wheelEvent(self
, event
):
1117 """Handle Qt mouse wheel events."""
1118 if event
.modifiers() == QtCore
.Qt
.ControlModifier
:
1119 self
.wheel_zoom(event
)
1121 self
.wheel_pan(event
)
1123 def wheel_zoom(self
, event
):
1124 """Handle mouse wheel zooming."""
1125 zoom
= math
.pow(2.0, event
.delta() / 512.0)
1126 factor
= (self
.matrix()
1128 .mapRect(QtCore
.QRectF(0.0, 0.0, 1.0, 1.0))
1130 if factor
< 0.014 or factor
> 42.0:
1132 self
.setTransformationAnchor(QtGui
.QGraphicsView
.AnchorUnderMouse
)
1134 self
.scale(zoom
, zoom
)
1136 def wheel_pan(self
, event
):
1137 """Handle mouse wheel panning."""
1139 if event
.delta() < 0:
1143 pan_rect
= QtCore
.QRectF(0.0, 0.0, 1.0, 1.0)
1144 factor
= 1.0 / self
.matrix().mapRect(pan_rect
).width()
1146 if event
.orientation() == QtCore
.Qt
.Vertical
:
1147 matrix
= self
.matrix().translate(0, s
* factor
)
1149 matrix
= self
.matrix().translate(s
* factor
, 0)
1150 self
.setTransformationAnchor(QtGui
.QGraphicsView
.NoAnchor
)
1151 self
.setMatrix(matrix
)
1153 def scale_view(self
, scale
):
1154 factor
= (self
.matrix().scale(scale
, scale
)
1155 .mapRect(QtCore
.QRectF(0, 0, 1, 1))
1157 if factor
< 0.07 or factor
> 100:
1161 adjust_scrollbars
= True
1162 scrollbar
= self
.verticalScrollBar()
1164 value
= scrollbar
.value()
1165 min_
= scrollbar
.minimum()
1166 max_
= scrollbar
.maximum()
1167 range_
= max_
- min_
1168 distance
= value
- min_
1169 nonzero_range
= float(range_
) != 0.0
1171 scrolloffset
= distance
/float(range_
)
1173 adjust_scrollbars
= False
1175 self
.setTransformationAnchor(QtGui
.QGraphicsView
.NoAnchor
)
1176 self
.scale(scale
, scale
)
1178 scrollbar
= self
.verticalScrollBar()
1179 if scrollbar
and adjust_scrollbars
:
1180 min_
= scrollbar
.minimum()
1181 max_
= scrollbar
.maximum()
1182 range_
= max_
- min_
1183 value
= min_
+ int(float(range_
) * scrolloffset
)
1184 scrollbar
.setValue(value
)
1186 def add_commits(self
, commits
):
1187 """Traverse commits and add them to the view."""
1188 self
.commits
.extend(commits
)
1189 scene
= self
.scene()
1190 for commit
in commits
:
1191 item
= Commit(commit
, self
.notifier
)
1192 self
.items
[commit
.sha1
] = item
1193 for ref
in commit
.tags
:
1194 self
.items
[ref
] = item
1197 self
.layout_commits(commits
)
1200 def link(self
, commits
):
1201 """Create edges linking commits with their parents"""
1202 scene
= self
.scene()
1203 for commit
in commits
:
1205 commit_item
= self
.items
[commit
.sha1
]
1207 # TODO - Handle truncated history viewing
1209 for parent
in commit
.parents
:
1211 parent_item
= self
.items
[parent
.sha1
]
1213 # TODO - Handle truncated history viewing
1215 edge
= Edge(parent_item
, commit_item
)
1218 def layout_commits(self
, nodes
):
1219 positions
= self
.position_nodes(nodes
)
1220 for sha1
, (x
, y
) in positions
.items():
1221 item
= self
.items
[sha1
]
1224 def position_nodes(self
, nodes
):
1229 for node
in reversed(nodes
):
1230 generation
= node
.generation
1234 cur_xoff
= self
.x_offsets
[generation
]
1235 next_xoff
= cur_xoff
1237 self
.x_offsets
[generation
] = next_xoff
1239 if len(node
.parents
) > 1:
1240 # Sweep across generations from child to farthest
1241 # parents and reserve padding for intermediate
1242 # nodes. This minimizes overlapping edges.
1243 mingen
= reduce(min, [p
.generation
for p
in node
.parents
])
1244 for gen
in xrange(mingen
+1, node
.generation
):
1245 new_xoff
= self
.x_offsets
[gen
] + xoff
1246 self
.x_offsets
[gen
] = max(new_xoff
, next_xoff
)
1249 ypos
= -node
.generation
* self
.y_off
1251 x_max
= max(x_max
, xpos
)
1252 y_min
= min(y_min
, ypos
)
1254 positions
[sha1
] = (xpos
, ypos
)
1261 def update_scene_rect(self
):
1264 self
.scene().setSceneRect(-self
.x_off
/2,
1267 abs(y_min
)+self
.y_off
*2)
1269 def sort_by_generation(commits
):
1270 commits
.sort(cmp=lambda a
, b
: cmp(a
.generation
, b
.generation
))
1274 def context_menu_actions(self
):
1276 'diff_this_selected':
1277 qtutils
.add_action(self
, 'Diff this -> selected',
1278 self
.diff_this_selected
),
1279 'diff_selected_this':
1280 qtutils
.add_action(self
, 'Diff selected -> this',
1281 self
.diff_selected_this
),
1283 qtutils
.add_action(self
, 'Create Branch',
1284 self
.create_branch
),
1286 qtutils
.add_action(self
, 'Create Patch',
1289 qtutils
.add_action(self
, 'Create Tag',
1292 qtutils
.add_action(self
, 'Save As Tarball/Zip...',
1293 lambda: create_tarball(self
)),
1295 qtutils
.add_action(self
, 'Cherry Pick',
1299 qtutils
.add_action(self
, 'Grab File...',
1300 lambda: save_blob_dialog(self
)),
1304 def update_menu_actions(self
, event
):
1305 clicked
= self
.itemAt(event
.pos())
1306 selected_items
= self
.selectedItems()
1307 has_single_selection
= len(selected_items
) == 1
1309 has_selection
= bool(selected_items
)
1310 can_diff
= bool(clicked
and has_single_selection
and
1311 clicked
is not selected_items
[0])
1313 self
.clicked
= clicked
1315 self
.selected
= selected_items
[0]
1317 self
.selected
= None
1319 self
.menu_actions
['diff_this_selected'].setEnabled(can_diff
)
1320 self
.menu_actions
['diff_selected_this'].setEnabled(can_diff
)
1321 self
.menu_actions
['create_patch'].setEnabled(has_selection
)
1322 self
.menu_actions
['create_tarball'].setEnabled(has_single_selection
)
1323 self
.menu_actions
['save_blob'].setEnabled(has_single_selection
)
1324 self
.menu_actions
['create_branch'].setEnabled(has_single_selection
)
1325 self
.menu_actions
['create_tag'].setEnabled(has_single_selection
)
1326 self
.menu_actions
['cherry_pick'].setEnabled(has_single_selection
)
1329 def context_menu_event(self
, event
):
1330 menu
= QtGui
.QMenu(self
)
1331 menu
.addAction(self
.menu_actions
['diff_this_selected'])
1332 menu
.addAction(self
.menu_actions
['diff_selected_this'])
1334 menu
.addAction(self
.menu_actions
['create_branch'])
1335 menu
.addAction(self
.menu_actions
['create_tag'])
1337 menu
.addAction(self
.menu_actions
['cherry_pick'])
1338 menu
.addAction(self
.menu_actions
['create_patch'])
1339 menu
.addAction(self
.menu_actions
['create_tarball'])
1341 menu
.addAction(self
.menu_actions
['save_blob'])
1342 menu
.exec_(self
.mapToGlobal(event
.pos()))
1345 def create_tarball(self
):
1346 ref
= self
.clicked
.commit
.sha1
1348 GitArchiveDialog
.save(ref
, shortref
, self
)
1351 def save_blob_dialog(self
):
1352 return BrowseDialog
.browse(self
.clicked
.commit
.sha1
)