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
223 self
.commit_list
= []
224 self
.revtext
= GitLogLineEdit(parent
=self
)
226 self
.maxresults
= QtGui
.QSpinBox()
227 self
.maxresults
.setMinimum(1)
228 self
.maxresults
.setMaximum(99999)
229 self
.maxresults
.setPrefix('git log -')
230 self
.maxresults
.setSuffix('')
232 self
.displaybutton
= QtGui
.QPushButton()
233 self
.displaybutton
.setText('Display')
235 self
.zoom_in
= QtGui
.QPushButton()
236 self
.zoom_in
.setIcon(qtutils
.theme_icon('zoom-in.png'))
237 self
.zoom_in
.setFlat(True)
239 self
.zoom_out
= QtGui
.QPushButton()
240 self
.zoom_out
.setIcon(qtutils
.theme_icon('zoom-out.png'))
241 self
.zoom_out
.setFlat(True)
243 self
.top_layout
= QtGui
.QHBoxLayout()
244 self
.top_layout
.setMargin(defs
.margin
)
245 self
.top_layout
.setSpacing(defs
.button_spacing
)
247 self
.top_layout
.addWidget(self
.maxresults
)
248 self
.top_layout
.addWidget(self
.revtext
)
249 self
.top_layout
.addWidget(self
.displaybutton
)
250 self
.top_layout
.addStretch()
251 self
.top_layout
.addWidget(self
.zoom_out
)
252 self
.top_layout
.addWidget(self
.zoom_in
)
254 self
.notifier
= notifier
= observable
.Observable()
255 self
.notifier
.refs_updated
= refs_updated
= 'refs_updated'
256 self
.notifier
.add_observer(refs_updated
, self
.display
)
258 self
.graphview
= GraphView(notifier
)
259 self
.treewidget
= CommitTreeWidget(notifier
)
260 self
.diffwidget
= DiffWidget(notifier
)
262 for signal
in (archive
,):
263 qtutils
.relay_signal(self
, self
.graphview
, SIGNAL(signal
))
264 qtutils
.relay_signal(self
, self
.treewidget
, SIGNAL(signal
))
266 self
.splitter
= QtGui
.QSplitter()
267 self
.splitter
.setOrientation(QtCore
.Qt
.Horizontal
)
268 self
.splitter
.setChildrenCollapsible(True)
269 self
.splitter
.setHandleWidth(defs
.handle_width
)
271 self
.left_splitter
= QtGui
.QSplitter()
272 self
.left_splitter
.setOrientation(QtCore
.Qt
.Vertical
)
273 self
.left_splitter
.setChildrenCollapsible(True)
274 self
.left_splitter
.setHandleWidth(defs
.handle_width
)
275 self
.left_splitter
.setStretchFactor(0, 1)
276 self
.left_splitter
.setStretchFactor(1, 1)
277 self
.left_splitter
.insertWidget(0, self
.treewidget
)
278 self
.left_splitter
.insertWidget(1, self
.diffwidget
)
280 self
.splitter
.insertWidget(0, self
.left_splitter
)
281 self
.splitter
.insertWidget(1, self
.graphview
)
283 self
.splitter
.setStretchFactor(0, 1)
284 self
.splitter
.setStretchFactor(1, 1)
286 self
.main_layout
= layout
= QtGui
.QVBoxLayout()
289 layout
.addLayout(self
.top_layout
)
290 layout
.addWidget(self
.splitter
)
291 self
.setLayout(layout
)
293 # Also re-loads dag.* from the saved state
294 if not qtutils
.apply_state(self
):
295 self
.resize_to_desktop()
297 # Update fields affected by model
298 self
.revtext
.setText(dag
.ref
)
299 self
.maxresults
.setValue(dag
.count
)
300 self
.update_window_title()
302 self
.thread
= ReaderThread(self
, dag
)
304 self
.thread
.connect(self
.thread
, self
.thread
.commits_ready
,
307 self
.thread
.connect(self
.thread
, self
.thread
.done
,
310 self
.connect(self
.splitter
, SIGNAL('splitterMoved(int,int)'),
313 self
.connect(self
.zoom_in
, SIGNAL('pressed()'),
314 self
.graphview
.zoom_in
)
316 self
.connect(self
.zoom_out
, SIGNAL('pressed()'),
317 self
.graphview
.zoom_out
)
319 self
.connect(self
.treewidget
, SIGNAL('diff_commits'),
322 self
.connect(self
.graphview
, SIGNAL('diff_commits'),
325 self
.connect(self
.maxresults
, SIGNAL('valueChanged(int)'),
326 lambda(x
): self
.dag
.set_count(x
))
328 self
.connect(self
.displaybutton
, SIGNAL('pressed()'),
331 self
.connect(self
.revtext
, SIGNAL('ref_changed'),
334 self
.connect(self
.revtext
, SIGNAL('textChanged(QString)'),
337 # The model is updated in another thread so use
338 # signals/slots to bring control back to the main GUI thread
339 self
.model
.add_observer(self
.model
.message_updated
,
340 self
.emit_model_updated
)
342 self
.connect(self
, SIGNAL('model_updated'),
345 qtutils
.add_close_action(self
)
347 def text_changed(self
, txt
):
348 self
.dag
.ref
= unicode(txt
)
349 self
.update_window_title()
351 def update_window_title(self
):
352 project
= self
.model
.project
354 self
.setWindowTitle('%s: %s' % (project
, self
.dag
.ref
))
356 self
.setWindowTitle(project
)
358 def export_state(self
):
359 state
= super(DAGView
, self
).export_state()
360 state
['count'] = self
.dag
.count
363 def apply_state(self
, state
):
365 super(DAGView
, self
).apply_state(state
)
369 count
= state
['count']
373 if not self
.dag
.overridden('count'):
374 self
.dag
.set_count(count
)
376 def emit_model_updated(self
):
377 self
.emit(SIGNAL('model_updated'))
379 def model_updated(self
):
381 self
.revtext
.update_matches()
383 if not self
.model
.currentbranch
:
385 self
.revtext
.setText(self
.model
.currentbranch
)
389 new_ref
= unicode(self
.revtext
.text())
394 self
.dag
.set_ref(new_ref
)
395 self
.dag
.set_count(self
.maxresults
.value())
399 super(DAGView
, self
).show()
400 self
.splitter
.setSizes([self
.width()/2, self
.width()/2])
401 self
.left_splitter
.setSizes([self
.height()/3, self
.height()*2/3])
402 self
.treewidget
.adjust_columns()
404 def resizeEvent(self
, e
):
405 super(DAGView
, self
).resizeEvent(e
)
406 self
.treewidget
.adjust_columns()
408 def splitter_moved(self
, pos
, idx
):
409 self
.treewidget
.adjust_columns()
412 self
.graphview
.clear()
413 self
.treewidget
.clear()
415 self
.commit_list
= []
417 def add_commits(self
, commits
):
418 self
.commit_list
.extend(commits
)
419 # Keep track of commits
420 for commit_obj
in commits
:
421 self
.commits
[commit_obj
.sha1
] = commit_obj
422 for tag
in commit_obj
.tags
:
423 self
.commits
[tag
] = commit_obj
424 self
.graphview
.add_commits(commits
)
425 self
.treewidget
.add_commits(commits
)
427 def thread_done(self
):
429 commit_obj
= self
.commit_list
[-1]
432 sig
= signals
.commits_selected
433 self
.notifier
.notify_observers(sig
, [commit_obj
])
434 self
.graphview
.update_scene_rect()
435 self
.graphview
.view_fit()
437 def closeEvent(self
, event
):
438 self
.revtext
.close_popup()
440 qtutils
.save_state(self
)
441 return super(DAGView
, self
).closeEvent(event
)
444 self
.thread
.mutex
.lock()
445 self
.thread
.stop
= True
446 self
.thread
.mutex
.unlock()
449 self
.thread
.abort
= True
453 self
.thread
.abort
= False
454 self
.thread
.stop
= False
458 self
.thread
.mutex
.lock()
459 self
.thread
.stop
= False
460 self
.thread
.mutex
.unlock()
461 self
.thread
.condition
.wakeOne()
463 def resize_to_desktop(self
):
464 desktop
= QtGui
.QApplication
.instance().desktop()
465 width
= desktop
.width()
466 height
= desktop
.height()
467 self
.resize(width
, height
)
469 def diff_commits(self
, a
, b
):
470 paths
= self
.dag
.paths()
472 difftool
.launch([a
, b
, '--'] + paths
)
474 difftool
.diff_commits(self
, a
, b
)
477 class ReaderThread(QtCore
.QThread
):
479 commits_ready
= SIGNAL('commits_ready')
480 done
= SIGNAL('done')
482 def __init__(self
, parent
, dag
):
483 QtCore
.QThread
.__init
__(self
, parent
)
487 self
.mutex
= QtCore
.QMutex()
488 self
.condition
= QtCore
.QWaitCondition()
491 repo
= RepoReader(self
.dag
)
497 self
.condition
.wait(self
.mutex
)
503 if len(commits
) >= 512:
504 self
.emit(self
.commits_ready
, commits
)
508 self
.emit(self
.commits_ready
, commits
)
516 class Edge(QtGui
.QGraphicsItem
):
517 item_type
= QtGui
.QGraphicsItem
.UserType
+ 1
519 arrow_extra
= (arrow_size
+1.0)/2.0
521 pen
= QtGui
.QPen(QtCore
.Qt
.gray
, 1.3,
526 def __init__(self
, source
, dest
,
528 arrow_size
=arrow_size
):
530 QtGui
.QGraphicsItem
.__init
__(self
)
532 self
.setAcceptedMouseButtons(QtCore
.Qt
.NoButton
)
537 dest_pt
= Commit
.item_bbox
.center()
539 self
.source_pt
= self
.mapFromItem(self
.source
, dest_pt
)
540 self
.dest_pt
= self
.mapFromItem(self
.dest
, dest_pt
)
541 self
.line
= QtCore
.QLineF(self
.source_pt
, self
.dest_pt
)
543 width
= self
.dest_pt
.x() - self
.source_pt
.x()
544 height
= self
.dest_pt
.y() - self
.source_pt
.y()
545 rect
= QtCore
.QRectF(self
.source_pt
, QtCore
.QSizeF(width
, height
))
546 self
.bound
= rect
.normalized().adjusted(-extra
, -extra
, extra
, extra
)
549 return self
.item_type
551 def boundingRect(self
):
554 def paint(self
, painter
, option
, widget
,
555 arrow_size
=arrow_size
,
556 gray
=QtCore
.Qt
.gray
):
558 painter
.setPen(self
.pen
)
559 painter
.drawLine(self
.line
)
562 class Commit(QtGui
.QGraphicsItem
):
563 item_type
= QtGui
.QGraphicsItem
.UserType
+ 2
567 item_shape
= QtGui
.QPainterPath()
568 item_shape
.addRect(width
/-2., height
/-2., width
, height
)
569 item_bbox
= item_shape
.boundingRect()
571 inner_rect
= QtGui
.QPainterPath()
572 inner_rect
.addRect(width
/-2.+2., height
/-2.+2, width
-4., height
-4.)
573 inner_rect
= inner_rect
.boundingRect()
575 selected_color
= QtGui
.QColor
.fromRgb(255, 255, 0)
576 outline_color
= QtGui
.QColor
.fromRgb(64, 96, 192)
579 text_options
= QtGui
.QTextOption()
580 text_options
.setAlignment(QtCore
.Qt
.AlignCenter
)
582 commit_pen
= QtGui
.QPen()
583 commit_pen
.setWidth(1.0)
584 commit_pen
.setColor(outline_color
)
586 cached_commit_color
= QtGui
.QColor
.fromRgb(128, 222, 255)
587 cached_commit_selected_color
= QtGui
.QColor
.fromRgb(32, 64, 255)
588 cached_merge_color
= QtGui
.QColor
.fromRgb(255, 255, 255)
590 def __init__(self
, commit
,
592 selectable
=QtGui
.QGraphicsItem
.ItemIsSelectable
,
593 cursor
=QtCore
.Qt
.PointingHandCursor
,
595 commit_color
=cached_commit_color
,
596 commit_selected_color
=cached_commit_selected_color
,
597 merge_color
=cached_merge_color
):
599 QtGui
.QGraphicsItem
.__init
__(self
)
602 self
.setFlag(selectable
)
603 self
.setCursor(cursor
)
606 self
.notifier
= notifier
609 self
.label
= label
= Label(commit
)
610 label
.setParentItem(self
)
611 label
.setPos(xpos
, 0.)
615 if len(commit
.parents
) > 1:
616 self
.commit_color
= merge_color
618 self
.commit_color
= commit_color
619 self
.text_pen
= QtCore
.Qt
.black
620 self
.sha1_text
= commit
.sha1
[:8]
626 # Overridden Qt methods
629 def blockSignals(self
, blocked
):
630 self
.notifier
.notification_enabled
= not blocked
632 def itemChange(self
, change
, value
):
633 if change
== QtGui
.QGraphicsItem
.ItemSelectedHasChanged
:
634 # Broadcast selection to other widgets
635 selected_items
= self
.scene().selectedItems()
636 commits
= [item
.commit
for item
in selected_items
]
637 self
.scene().parent().set_selecting(True)
638 sig
= signals
.commits_selected
639 self
.notifier
.notify_observers(sig
, commits
)
640 self
.scene().parent().set_selecting(False)
642 # Cache the pen for use in paint()
643 if value
.toPyObject():
644 self
.commit_color
= self
.cached_commit_selected_color
645 self
.text_pen
= QtCore
.Qt
.white
646 color
= self
.selected_color
648 self
.text_pen
= QtCore
.Qt
.black
649 if len(self
.commit
.parents
) > 1:
650 self
.commit_color
= self
.cached_merge_color
652 self
.commit_color
= self
.cached_commit_color
653 color
= self
.outline_color
654 commit_pen
= QtGui
.QPen()
655 commit_pen
.setWidth(1.0)
656 commit_pen
.setColor(color
)
657 self
.commit_pen
= commit_pen
659 return QtGui
.QGraphicsItem
.itemChange(self
, change
, value
)
662 return self
.item_type
664 def boundingRect(self
, rect
=item_bbox
):
668 return self
.item_shape
670 def paint(self
, painter
, option
, widget
,
672 text_opts
=text_options
,
675 # Do not draw outside the exposed rect
676 painter
.setClipRect(option
.exposedRect
)
679 painter
.setPen(self
.commit_pen
)
680 painter
.setBrush(self
.commit_color
)
681 painter
.drawEllipse(inner
)
686 except AttributeError:
687 font
= cache
.font
= painter
.font()
689 painter
.setFont(font
)
690 painter
.setPen(self
.text_pen
)
691 painter
.drawText(inner
, self
.sha1_text
, text_opts
)
693 def mousePressEvent(self
, event
):
694 QtGui
.QGraphicsItem
.mousePressEvent(self
, event
)
696 self
.selected
= self
.isSelected()
698 def mouseMoveEvent(self
, event
):
701 QtGui
.QGraphicsItem
.mouseMoveEvent(self
, event
)
703 def mouseReleaseEvent(self
, event
):
704 QtGui
.QGraphicsItem
.mouseReleaseEvent(self
, event
)
705 if (not self
.dragged
and
707 event
.button() == QtCore
.Qt
.LeftButton
):
713 class Label(QtGui
.QGraphicsItem
):
714 item_type
= QtGui
.QGraphicsItem
.UserType
+ 3
719 item_shape
= QtGui
.QPainterPath()
720 item_shape
.addRect(0, 0, width
, height
)
721 item_bbox
= item_shape
.boundingRect()
723 text_options
= QtGui
.QTextOption()
724 text_options
.setAlignment(QtCore
.Qt
.AlignCenter
)
725 text_options
.setAlignment(QtCore
.Qt
.AlignVCenter
)
727 def __init__(self
, commit
,
728 other_color
=QtGui
.QColor
.fromRgb(255, 255, 64),
729 head_color
=QtGui
.QColor
.fromRgb(64, 255, 64)):
730 QtGui
.QGraphicsItem
.__init
__(self
)
733 # Starts with enough space for two tags. Any more and the commit
734 # needs to be taller to accomodate.
736 height
= len(commit
.tags
) * self
.height
/2. + 4. # +6 padding
738 self
.label_box
= QtCore
.QRectF(0., -height
/2., self
.width
, height
)
739 self
.text_box
= QtCore
.QRectF(2., -height
/2., self
.width
-4., height
)
740 self
.tag_text
= '\n'.join(commit
.tags
)
742 if 'HEAD' in commit
.tags
:
743 self
.color
= head_color
745 self
.color
= other_color
747 self
.pen
= QtGui
.QPen()
748 self
.pen
.setColor(self
.color
.darker())
749 self
.pen
.setWidth(1.0)
752 return self
.item_type
754 def boundingRect(self
, rect
=item_bbox
):
758 return self
.item_shape
760 def paint(self
, painter
, option
, widget
,
761 text_opts
=text_options
,
762 black
=QtCore
.Qt
.black
,
765 painter
.setBrush(self
.color
)
766 painter
.setPen(self
.pen
)
767 painter
.drawRoundedRect(self
.label_box
, 4, 4)
770 except AttributeError:
771 font
= cache
.font
= painter
.font()
773 painter
.setFont(font
)
774 painter
.setPen(black
)
775 painter
.drawText(self
.text_box
, self
.tag_text
, text_opts
)
778 class GraphView(QtGui
.QGraphicsView
):
779 def __init__(self
, notifier
):
780 QtGui
.QGraphicsView
.__init
__(self
)
788 self
.notifier
= notifier
793 self
.saved_matrix
= QtGui
.QMatrix(self
.matrix())
795 self
.x_offsets
= collections
.defaultdict(int)
797 self
.is_panning
= False
799 self
.selecting
= False
800 self
.last_mouse
= [0, 0]
802 self
.setDragMode(self
.RubberBandDrag
)
804 scene
= QtGui
.QGraphicsScene(self
)
805 scene
.setItemIndexMethod(QtGui
.QGraphicsScene
.NoIndex
)
809 self
.setRenderHint(QtGui
.QPainter
.Antialiasing
)
810 self
.setOptimizationFlag(self
.DontAdjustForAntialiasing
, True)
811 self
.setViewportUpdateMode(self
.SmartViewportUpdate
)
812 self
.setCacheMode(QtGui
.QGraphicsView
.CacheBackground
)
813 self
.setTransformationAnchor(QtGui
.QGraphicsView
.AnchorUnderMouse
)
814 self
.setResizeAnchor(QtGui
.QGraphicsView
.NoAnchor
)
815 self
.setBackgroundBrush(QtGui
.QColor
.fromRgb(0, 0, 0))
817 self
.action_zoom_in
= (
818 qtutils
.add_action(self
, 'Zoom In',
821 QtCore
.Qt
.Key_Equal
))
823 self
.action_zoom_out
= (
824 qtutils
.add_action(self
, 'Zoom Out',
826 QtCore
.Qt
.Key_Minus
))
828 self
.action_zoom_fit
= (
829 qtutils
.add_action(self
, 'Zoom to Fit',
833 self
.action_select_parent
= (
834 qtutils
.add_action(self
, 'Select Parent',
838 self
.action_select_oldest_parent
= (
839 qtutils
.add_action(self
, 'Select Oldest Parent',
840 self
.select_oldest_parent
,
843 self
.action_select_child
= (
844 qtutils
.add_action(self
, 'Select Child',
848 self
.action_select_child
= (
849 qtutils
.add_action(self
, 'Select Nth Child',
850 self
.select_nth_child
,
853 self
.menu_actions
= context_menu_actions(self
)
855 sig
= signals
.commits_selected
856 notifier
.add_observer(sig
, self
.commits_selected
)
862 self
.x_offsets
.clear()
871 self
.scale_view(1.0/1.5)
873 def commits_selected(self
, commits
):
876 self
.select([commit
.sha1
for commit
in commits
])
878 def contextMenuEvent(self
, event
):
879 update_menu_actions(self
, event
)
880 context_menu_event(self
, event
)
882 def select(self
, sha1s
):
883 """Select the item for the SHA-1"""
884 self
.scene().clearSelection()
887 item
= self
.items
[sha1
]
890 item
.blockSignals(True)
891 item
.setSelected(True)
892 item
.blockSignals(False)
893 item_rect
= item
.sceneTransform().mapRect(item
.boundingRect())
894 self
.ensureVisible(item_rect
)
896 def selected_item(self
):
897 """Return the currently selected item"""
898 selected_items
= self
.selectedItems()
899 if not selected_items
:
901 return selected_items
[0]
903 def selectedItems(self
):
904 """Return the currently selected items"""
905 return self
.scene().selectedItems()
907 def get_item_by_generation(self
, commits
, criteria_fn
):
908 """Return the item for the commit matching criteria"""
912 for commit
in commits
:
913 if (generation
is None or
914 criteria_fn(generation
, commit
.generation
)):
916 generation
= commit
.generation
918 return self
.items
[sha1
]
922 def oldest_item(self
, commits
):
923 """Return the item for the commit with the oldest generation number"""
924 return self
.get_item_by_generation(commits
, lambda a
, b
: a
> b
)
926 def newest_item(self
, commits
):
927 """Return the item for the commit with the newest generation number"""
928 return self
.get_item_by_generation(commits
, lambda a
, b
: a
< b
)
930 def diff_this_selected(self
):
931 clicked_sha1
= self
.clicked
.commit
.sha1
932 selected_sha1
= self
.selected
.commit
.sha1
933 self
.emit(SIGNAL('diff_commits'), clicked_sha1
, selected_sha1
)
935 def diff_selected_this(self
):
936 clicked_sha1
= self
.clicked
.commit
.sha1
937 selected_sha1
= self
.selected
.commit
.sha1
938 self
.emit(SIGNAL('diff_commits'), selected_sha1
, clicked_sha1
)
940 def create_patch(self
):
941 items
= self
.selectedItems()
944 selected_commits
= sort_by_generation([n
.commit
for n
in items
])
945 sha1s
= [c
.sha1
for c
in selected_commits
]
946 all_sha1s
= [c
.sha1
for c
in self
.commits
]
947 cola
.notifier().broadcast(signals
.format_patch
, sha1s
, all_sha1s
)
949 def create_branch(self
):
950 sha1
= self
.clicked
.commit
.sha1
951 create_new_branch(revision
=sha1
)
953 def create_tag(self
):
954 sha1
= self
.clicked
.commit
.sha1
955 create_tag(revision
=sha1
)
957 def cherry_pick(self
):
958 sha1
= self
.clicked
.commit
.sha1
959 cola
.notifier().broadcast(signals
.cherry_pick
, [sha1
])
960 self
.notifier
.notify_observers(self
.notifier
.refs_updated
)
962 def select_parent(self
):
963 """Select the parent with the newest generation number"""
964 selected_item
= self
.selected_item()
965 if selected_item
is None:
967 parent_item
= self
.newest_item(selected_item
.commit
.parents
)
968 if parent_item
is None:
970 selected_item
.setSelected(False)
971 parent_item
.setSelected(True)
972 self
.ensureVisible(parent_item
.mapRectToScene(parent_item
.boundingRect()))
974 def select_oldest_parent(self
):
975 """Select the parent with the oldest generation number"""
976 selected_item
= self
.selected_item()
977 if selected_item
is None:
979 parent_item
= self
.oldest_item(selected_item
.commit
.parents
)
980 if parent_item
is None:
982 selected_item
.setSelected(False)
983 parent_item
.setSelected(True)
984 self
.ensureVisible(parent_item
.mapRectToScene(parent_item
.boundingRect()))
986 def select_child(self
):
987 """Select the child with the oldest generation number"""
988 selected_item
= self
.selected_item()
989 if selected_item
is None:
991 child_item
= self
.oldest_item(selected_item
.commit
.children
)
992 if child_item
is None:
994 selected_item
.setSelected(False)
995 child_item
.setSelected(True)
996 self
.ensureVisible(child_item
.mapRectToScene(child_item
.boundingRect()))
998 def select_nth_child(self
):
999 """Select the Nth child with the newest generation number (N > 1)"""
1000 selected_item
= self
.selected_item()
1001 if selected_item
is None:
1003 if len(selected_item
.commit
.children
) > 1:
1004 children
= selected_item
.commit
.children
[1:]
1006 children
= selected_item
.commit
.children
1007 child_item
= self
.newest_item(children
)
1008 if child_item
is None:
1010 selected_item
.setSelected(False)
1011 child_item
.setSelected(True)
1012 self
.ensureVisible(child_item
.mapRectToScene(child_item
.boundingRect()))
1015 """Fit selected items into the viewport"""
1017 items
= self
.scene().selectedItems()
1019 rect
= self
.scene().itemsBoundingRect()
1027 item_rect
= item
.boundingRect()
1028 x_off
= item_rect
.width()
1029 y_off
= item_rect
.height()
1030 x_min
= min(x_min
, pos
.x())
1031 y_min
= min(y_min
, pos
.y())
1032 x_max
= max(x_max
, pos
.x()+x_off
)
1033 ymax
= max(ymax
, pos
.y()+y_off
)
1034 rect
= QtCore
.QRectF(x_min
, y_min
, x_max
-x_min
, ymax
-y_min
)
1035 adjust
= Commit
.width
* 2
1036 rect
.setX(rect
.x() - adjust
)
1037 rect
.setY(rect
.y() - adjust
)
1038 rect
.setHeight(rect
.height() + adjust
)
1039 rect
.setWidth(rect
.width() + adjust
)
1040 self
.fitInView(rect
, QtCore
.Qt
.KeepAspectRatio
)
1041 self
.scene().invalidate()
1043 def save_selection(self
, event
):
1044 if event
.button() != QtCore
.Qt
.LeftButton
:
1046 elif QtCore
.Qt
.ShiftModifier
!= event
.modifiers():
1048 self
.selected
= self
.selectedItems()
1050 def restore_selection(self
, event
):
1051 if QtCore
.Qt
.ShiftModifier
!= event
.modifiers():
1053 for item
in self
.selected
:
1054 item
.setSelected(True)
1056 def handle_event(self
, event_handler
, event
):
1058 self
.save_selection(event
)
1059 event_handler(self
, event
)
1060 self
.restore_selection(event
)
1062 def mousePressEvent(self
, event
):
1063 if event
.button() == QtCore
.Qt
.MidButton
:
1065 self
.mouse_start
= [pos
.x(), pos
.y()]
1066 self
.saved_matrix
= QtGui
.QMatrix(self
.matrix())
1067 self
.is_panning
= True
1069 if event
.button() == QtCore
.Qt
.RightButton
:
1072 if event
.button() == QtCore
.Qt
.LeftButton
:
1074 self
.handle_event(QtGui
.QGraphicsView
.mousePressEvent
, event
)
1076 def mouseMoveEvent(self
, event
):
1077 pos
= self
.mapToScene(event
.pos())
1081 self
.last_mouse
[0] = pos
.x()
1082 self
.last_mouse
[1] = pos
.y()
1083 self
.handle_event(QtGui
.QGraphicsView
.mouseMoveEvent
, event
)
1085 def set_selecting(self
, selecting
):
1086 self
.selecting
= selecting
1088 def mouseReleaseEvent(self
, event
):
1089 self
.pressed
= False
1090 if event
.button() == QtCore
.Qt
.MidButton
:
1091 self
.is_panning
= False
1093 self
.handle_event(QtGui
.QGraphicsView
.mouseReleaseEvent
, event
)
1096 def pan(self
, event
):
1098 dx
= pos
.x() - self
.mouse_start
[0]
1099 dy
= pos
.y() - self
.mouse_start
[1]
1101 if dx
== 0 and dy
== 0:
1104 rect
= QtCore
.QRect(0, 0, abs(dx
), abs(dy
))
1105 delta
= self
.mapToScene(rect
).boundingRect()
1115 matrix
= QtGui
.QMatrix(self
.saved_matrix
).translate(tx
, ty
)
1116 self
.setTransformationAnchor(QtGui
.QGraphicsView
.NoAnchor
)
1117 self
.setMatrix(matrix
)
1119 def wheelEvent(self
, event
):
1120 """Handle Qt mouse wheel events."""
1121 if event
.modifiers() == QtCore
.Qt
.ControlModifier
:
1122 self
.wheel_zoom(event
)
1124 self
.wheel_pan(event
)
1126 def wheel_zoom(self
, event
):
1127 """Handle mouse wheel zooming."""
1128 zoom
= math
.pow(2.0, event
.delta() / 512.0)
1129 factor
= (self
.matrix()
1131 .mapRect(QtCore
.QRectF(0.0, 0.0, 1.0, 1.0))
1133 if factor
< 0.014 or factor
> 42.0:
1135 self
.setTransformationAnchor(QtGui
.QGraphicsView
.AnchorUnderMouse
)
1137 self
.scale(zoom
, zoom
)
1139 def wheel_pan(self
, event
):
1140 """Handle mouse wheel panning."""
1142 if event
.delta() < 0:
1146 pan_rect
= QtCore
.QRectF(0.0, 0.0, 1.0, 1.0)
1147 factor
= 1.0 / self
.matrix().mapRect(pan_rect
).width()
1149 if event
.orientation() == QtCore
.Qt
.Vertical
:
1150 matrix
= self
.matrix().translate(0, s
* factor
)
1152 matrix
= self
.matrix().translate(s
* factor
, 0)
1153 self
.setTransformationAnchor(QtGui
.QGraphicsView
.NoAnchor
)
1154 self
.setMatrix(matrix
)
1156 def scale_view(self
, scale
):
1157 factor
= (self
.matrix().scale(scale
, scale
)
1158 .mapRect(QtCore
.QRectF(0, 0, 1, 1))
1160 if factor
< 0.07 or factor
> 100:
1164 adjust_scrollbars
= True
1165 scrollbar
= self
.verticalScrollBar()
1167 value
= scrollbar
.value()
1168 min_
= scrollbar
.minimum()
1169 max_
= scrollbar
.maximum()
1170 range_
= max_
- min_
1171 distance
= value
- min_
1172 nonzero_range
= float(range_
) != 0.0
1174 scrolloffset
= distance
/float(range_
)
1176 adjust_scrollbars
= False
1178 self
.setTransformationAnchor(QtGui
.QGraphicsView
.NoAnchor
)
1179 self
.scale(scale
, scale
)
1181 scrollbar
= self
.verticalScrollBar()
1182 if scrollbar
and adjust_scrollbars
:
1183 min_
= scrollbar
.minimum()
1184 max_
= scrollbar
.maximum()
1185 range_
= max_
- min_
1186 value
= min_
+ int(float(range_
) * scrolloffset
)
1187 scrollbar
.setValue(value
)
1189 def add_commits(self
, commits
):
1190 """Traverse commits and add them to the view."""
1191 self
.commits
.extend(commits
)
1192 scene
= self
.scene()
1193 for commit
in commits
:
1194 item
= Commit(commit
, self
.notifier
)
1195 self
.items
[commit
.sha1
] = item
1196 for ref
in commit
.tags
:
1197 self
.items
[ref
] = item
1200 self
.layout_commits(commits
)
1203 def link(self
, commits
):
1204 """Create edges linking commits with their parents"""
1205 scene
= self
.scene()
1206 for commit
in commits
:
1208 commit_item
= self
.items
[commit
.sha1
]
1210 # TODO - Handle truncated history viewing
1212 for parent
in commit
.parents
:
1214 parent_item
= self
.items
[parent
.sha1
]
1216 # TODO - Handle truncated history viewing
1218 edge
= Edge(parent_item
, commit_item
)
1221 def layout_commits(self
, nodes
):
1222 positions
= self
.position_nodes(nodes
)
1223 for sha1
, (x
, y
) in positions
.items():
1224 item
= self
.items
[sha1
]
1227 def position_nodes(self
, nodes
):
1234 x_offsets
= self
.x_offsets
1237 generation
= node
.generation
1240 if len(node
.children
) > 1:
1241 # This is a fan-out so sweep over child generations and
1242 # shift them to the right to avoid overlapping edges
1243 child_gens
= [c
.generation
for c
in node
.children
]
1244 maxgen
= reduce(max, child_gens
)
1245 mingen
= reduce(min, child_gens
)
1247 for g
in xrange(generation
+1, maxgen
):
1248 x_offsets
[g
] += x_off
1250 if len(node
.parents
) == 1:
1251 # Align nodes relative to their parents
1252 parent_gen
= node
.parents
[0].generation
1253 parent_off
= x_offsets
[parent_gen
]
1254 x_offsets
[generation
] = max(parent_off
-x_off
,
1255 x_offsets
[generation
])
1257 cur_xoff
= x_offsets
[generation
]
1258 next_xoff
= cur_xoff
1260 x_offsets
[generation
] = next_xoff
1263 y_pos
= -generation
* y_off
1264 positions
[sha1
] = (x_pos
, y_pos
)
1266 x_max
= max(x_max
, x_pos
)
1267 y_min
= min(y_min
, y_pos
)
1275 def update_scene_rect(self
):
1278 self
.scene().setSceneRect(-self
.x_off
/2,
1281 abs(y_min
)+self
.y_off
*2)
1283 def sort_by_generation(commits
):
1284 commits
.sort(cmp=lambda a
, b
: cmp(a
.generation
, b
.generation
))
1288 def context_menu_actions(self
):
1290 'diff_this_selected':
1291 qtutils
.add_action(self
, 'Diff this -> selected',
1292 self
.diff_this_selected
),
1293 'diff_selected_this':
1294 qtutils
.add_action(self
, 'Diff selected -> this',
1295 self
.diff_selected_this
),
1297 qtutils
.add_action(self
, 'Create Branch',
1298 self
.create_branch
),
1300 qtutils
.add_action(self
, 'Create Patch',
1303 qtutils
.add_action(self
, 'Create Tag',
1306 qtutils
.add_action(self
, 'Save As Tarball/Zip...',
1307 lambda: create_tarball(self
)),
1309 qtutils
.add_action(self
, 'Cherry Pick',
1313 qtutils
.add_action(self
, 'Grab File...',
1314 lambda: save_blob_dialog(self
)),
1318 def update_menu_actions(self
, event
):
1319 clicked
= self
.itemAt(event
.pos())
1320 selected_items
= self
.selectedItems()
1321 has_single_selection
= len(selected_items
) == 1
1323 has_selection
= bool(selected_items
)
1324 can_diff
= bool(clicked
and has_single_selection
and
1325 clicked
is not selected_items
[0])
1327 self
.clicked
= clicked
1329 self
.selected
= selected_items
[0]
1331 self
.selected
= None
1333 self
.menu_actions
['diff_this_selected'].setEnabled(can_diff
)
1334 self
.menu_actions
['diff_selected_this'].setEnabled(can_diff
)
1335 self
.menu_actions
['create_patch'].setEnabled(has_selection
)
1336 self
.menu_actions
['create_tarball'].setEnabled(has_single_selection
)
1337 self
.menu_actions
['save_blob'].setEnabled(has_single_selection
)
1338 self
.menu_actions
['create_branch'].setEnabled(has_single_selection
)
1339 self
.menu_actions
['create_tag'].setEnabled(has_single_selection
)
1340 self
.menu_actions
['cherry_pick'].setEnabled(has_single_selection
)
1343 def context_menu_event(self
, event
):
1344 menu
= QtGui
.QMenu(self
)
1345 menu
.addAction(self
.menu_actions
['diff_this_selected'])
1346 menu
.addAction(self
.menu_actions
['diff_selected_this'])
1348 menu
.addAction(self
.menu_actions
['create_branch'])
1349 menu
.addAction(self
.menu_actions
['create_tag'])
1351 menu
.addAction(self
.menu_actions
['cherry_pick'])
1352 menu
.addAction(self
.menu_actions
['create_patch'])
1353 menu
.addAction(self
.menu_actions
['create_tarball'])
1355 menu
.addAction(self
.menu_actions
['save_blob'])
1356 menu
.exec_(self
.mapToGlobal(event
.pos()))
1359 def create_tarball(self
):
1360 ref
= self
.clicked
.commit
.sha1
1362 GitArchiveDialog
.save(ref
, shortref
, self
)
1365 def save_blob_dialog(self
):
1366 return BrowseDialog
.browse(self
.clicked
.commit
.sha1
)