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
= []
225 self
.old_count
= None
228 self
.revtext
= GitLogLineEdit(parent
=self
)
230 self
.maxresults
= QtGui
.QSpinBox()
231 self
.maxresults
.setMinimum(1)
232 self
.maxresults
.setMaximum(99999)
233 self
.maxresults
.setPrefix('git log -')
234 self
.maxresults
.setSuffix('')
236 self
.displaybutton
= QtGui
.QPushButton()
237 self
.displaybutton
.setText('Display')
239 self
.zoom_in
= QtGui
.QPushButton()
240 self
.zoom_in
.setIcon(qtutils
.theme_icon('zoom-in.png'))
241 self
.zoom_in
.setFlat(True)
243 self
.zoom_out
= QtGui
.QPushButton()
244 self
.zoom_out
.setIcon(qtutils
.theme_icon('zoom-out.png'))
245 self
.zoom_out
.setFlat(True)
247 self
.top_layout
= QtGui
.QHBoxLayout()
248 self
.top_layout
.setMargin(defs
.margin
)
249 self
.top_layout
.setSpacing(defs
.button_spacing
)
251 self
.top_layout
.addWidget(self
.maxresults
)
252 self
.top_layout
.addWidget(self
.revtext
)
253 self
.top_layout
.addWidget(self
.displaybutton
)
254 self
.top_layout
.addStretch()
255 self
.top_layout
.addWidget(self
.zoom_out
)
256 self
.top_layout
.addWidget(self
.zoom_in
)
258 self
.notifier
= notifier
= observable
.Observable()
259 self
.notifier
.refs_updated
= refs_updated
= 'refs_updated'
260 self
.notifier
.add_observer(refs_updated
, self
.display
)
262 self
.graphview
= GraphView(notifier
)
263 self
.treewidget
= CommitTreeWidget(notifier
)
264 self
.diffwidget
= DiffWidget(notifier
)
266 for signal
in (archive
,):
267 qtutils
.relay_signal(self
, self
.graphview
, SIGNAL(signal
))
268 qtutils
.relay_signal(self
, self
.treewidget
, SIGNAL(signal
))
270 self
.splitter
= QtGui
.QSplitter()
271 self
.splitter
.setOrientation(QtCore
.Qt
.Horizontal
)
272 self
.splitter
.setChildrenCollapsible(True)
273 self
.splitter
.setHandleWidth(defs
.handle_width
)
275 self
.left_splitter
= QtGui
.QSplitter()
276 self
.left_splitter
.setOrientation(QtCore
.Qt
.Vertical
)
277 self
.left_splitter
.setChildrenCollapsible(True)
278 self
.left_splitter
.setHandleWidth(defs
.handle_width
)
279 self
.left_splitter
.setStretchFactor(0, 1)
280 self
.left_splitter
.setStretchFactor(1, 1)
281 self
.left_splitter
.insertWidget(0, self
.treewidget
)
282 self
.left_splitter
.insertWidget(1, self
.diffwidget
)
284 self
.splitter
.insertWidget(0, self
.left_splitter
)
285 self
.splitter
.insertWidget(1, self
.graphview
)
287 self
.splitter
.setStretchFactor(0, 1)
288 self
.splitter
.setStretchFactor(1, 1)
290 self
.main_layout
= layout
= QtGui
.QVBoxLayout()
293 layout
.addLayout(self
.top_layout
)
294 layout
.addWidget(self
.splitter
)
295 self
.setLayout(layout
)
297 # Also re-loads dag.* from the saved state
298 if not qtutils
.apply_state(self
):
299 self
.resize_to_desktop()
301 # Update fields affected by model
302 self
.revtext
.setText(dag
.ref
)
303 self
.maxresults
.setValue(dag
.count
)
304 self
.update_window_title()
306 self
.thread
= ReaderThread(self
, dag
)
308 self
.thread
.connect(self
.thread
, self
.thread
.commits_ready
,
311 self
.thread
.connect(self
.thread
, self
.thread
.done
,
314 self
.connect(self
.splitter
, SIGNAL('splitterMoved(int,int)'),
317 self
.connect(self
.zoom_in
, SIGNAL('pressed()'),
318 self
.graphview
.zoom_in
)
320 self
.connect(self
.zoom_out
, SIGNAL('pressed()'),
321 self
.graphview
.zoom_out
)
323 self
.connect(self
.treewidget
, SIGNAL('diff_commits'),
326 self
.connect(self
.graphview
, SIGNAL('diff_commits'),
329 self
.connect(self
.maxresults
, SIGNAL('editingFinished()'),
332 self
.connect(self
.displaybutton
, SIGNAL('pressed()'),
335 self
.connect(self
.revtext
, SIGNAL('ref_changed'),
338 self
.connect(self
.revtext
, SIGNAL('textChanged(QString)'),
341 # The model is updated in another thread so use
342 # signals/slots to bring control back to the main GUI thread
343 self
.model
.add_observer(self
.model
.message_updated
,
344 self
.emit_model_updated
)
346 self
.connect(self
, SIGNAL('model_updated'),
349 qtutils
.add_close_action(self
)
351 def text_changed(self
, txt
):
352 self
.dag
.ref
= unicode(txt
)
353 self
.update_window_title()
355 def update_window_title(self
):
356 project
= self
.model
.project
358 self
.setWindowTitle('%s: %s' % (project
, self
.dag
.ref
))
360 self
.setWindowTitle(project
)
362 def export_state(self
):
363 state
= super(DAGView
, self
).export_state()
364 state
['count'] = self
.dag
.count
367 def apply_state(self
, state
):
369 super(DAGView
, self
).apply_state(state
)
373 count
= state
['count']
377 if not self
.dag
.overridden('count'):
378 self
.dag
.set_count(count
)
380 def emit_model_updated(self
):
381 self
.emit(SIGNAL('model_updated'))
383 def model_updated(self
):
385 self
.revtext
.update_matches()
387 if not self
.model
.currentbranch
:
389 self
.revtext
.setText(self
.model
.currentbranch
)
393 new_ref
= unicode(self
.revtext
.text())
396 new_count
= self
.maxresults
.value()
397 old_ref
= self
.old_ref
398 old_count
= self
.old_count
399 if old_ref
== new_ref
and old_count
== new_count
:
402 self
.setEnabled(False)
404 self
.old_ref
= new_ref
405 self
.old_count
= new_count
409 self
.dag
.set_ref(new_ref
)
410 self
.dag
.set_count(self
.maxresults
.value())
414 super(DAGView
, self
).show()
415 self
.splitter
.setSizes([self
.width()/2, self
.width()/2])
416 self
.left_splitter
.setSizes([self
.height()/3, self
.height()*2/3])
417 self
.treewidget
.adjust_columns()
419 def resizeEvent(self
, e
):
420 super(DAGView
, self
).resizeEvent(e
)
421 self
.treewidget
.adjust_columns()
423 def splitter_moved(self
, pos
, idx
):
424 self
.treewidget
.adjust_columns()
427 self
.graphview
.clear()
428 self
.treewidget
.clear()
430 self
.commit_list
= []
432 def add_commits(self
, commits
):
433 self
.commit_list
.extend(commits
)
434 # Keep track of commits
435 for commit_obj
in commits
:
436 self
.commits
[commit_obj
.sha1
] = commit_obj
437 for tag
in commit_obj
.tags
:
438 self
.commits
[tag
] = commit_obj
439 self
.graphview
.add_commits(commits
)
440 self
.treewidget
.add_commits(commits
)
442 def thread_done(self
):
443 self
.setEnabled(True)
445 commit_obj
= self
.commit_list
[-1]
448 sig
= signals
.commits_selected
449 self
.notifier
.notify_observers(sig
, [commit_obj
])
450 self
.graphview
.update_scene_rect()
451 self
.graphview
.view_fit()
453 def closeEvent(self
, event
):
454 self
.revtext
.close_popup()
456 qtutils
.save_state(self
)
457 return super(DAGView
, self
).closeEvent(event
)
460 self
.thread
.mutex
.lock()
461 self
.thread
.stop
= True
462 self
.thread
.mutex
.unlock()
465 self
.thread
.abort
= True
469 self
.thread
.abort
= False
470 self
.thread
.stop
= False
474 self
.thread
.mutex
.lock()
475 self
.thread
.stop
= False
476 self
.thread
.mutex
.unlock()
477 self
.thread
.condition
.wakeOne()
479 def resize_to_desktop(self
):
480 desktop
= QtGui
.QApplication
.instance().desktop()
481 width
= desktop
.width()
482 height
= desktop
.height()
483 self
.resize(width
, height
)
485 def diff_commits(self
, a
, b
):
486 paths
= self
.dag
.paths()
488 difftool
.launch([a
, b
, '--'] + paths
)
490 difftool
.diff_commits(self
, a
, b
)
493 class ReaderThread(QtCore
.QThread
):
495 commits_ready
= SIGNAL('commits_ready')
496 done
= SIGNAL('done')
498 def __init__(self
, parent
, dag
):
499 QtCore
.QThread
.__init
__(self
, parent
)
503 self
.mutex
= QtCore
.QMutex()
504 self
.condition
= QtCore
.QWaitCondition()
507 repo
= RepoReader(self
.dag
)
513 self
.condition
.wait(self
.mutex
)
519 if len(commits
) >= 512:
520 self
.emit(self
.commits_ready
, commits
)
524 self
.emit(self
.commits_ready
, commits
)
532 class Edge(QtGui
.QGraphicsItem
):
533 item_type
= QtGui
.QGraphicsItem
.UserType
+ 1
535 arrow_extra
= (arrow_size
+1.0)/2.0
537 pen
= QtGui
.QPen(QtCore
.Qt
.gray
, 1.0,
542 def __init__(self
, source
, dest
,
544 arrow_size
=arrow_size
):
546 QtGui
.QGraphicsItem
.__init
__(self
)
548 self
.setAcceptedMouseButtons(QtCore
.Qt
.NoButton
)
553 dest_pt
= Commit
.item_bbox
.center()
555 self
.source_pt
= self
.mapFromItem(self
.source
, dest_pt
)
556 self
.dest_pt
= self
.mapFromItem(self
.dest
, dest_pt
)
557 self
.line
= QtCore
.QLineF(self
.source_pt
, self
.dest_pt
)
559 width
= self
.dest_pt
.x() - self
.source_pt
.x()
560 height
= self
.dest_pt
.y() - self
.source_pt
.y()
561 rect
= QtCore
.QRectF(self
.source_pt
, QtCore
.QSizeF(width
, height
))
562 self
.bound
= rect
.normalized().adjusted(-extra
, -extra
, extra
, extra
)
565 return self
.item_type
567 def boundingRect(self
):
570 def paint(self
, painter
, option
, widget
,
571 arrow_size
=arrow_size
,
572 gray
=QtCore
.Qt
.gray
):
574 painter
.setPen(self
.pen
)
575 painter
.drawLine(self
.line
)
578 class Commit(QtGui
.QGraphicsItem
):
579 item_type
= QtGui
.QGraphicsItem
.UserType
+ 2
583 item_shape
= QtGui
.QPainterPath()
584 item_shape
.addRect(width
/-2., height
/-2., width
, height
)
585 item_bbox
= item_shape
.boundingRect()
587 inner_rect
= QtGui
.QPainterPath()
588 inner_rect
.addRect(width
/-2.+2., height
/-2.+2, width
-4., height
-4.)
589 inner_rect
= inner_rect
.boundingRect()
591 selected_color
= QtGui
.QColor
.fromRgb(255, 255, 0)
592 outline_color
= QtGui
.QColor
.fromRgb(64, 96, 192)
595 text_options
= QtGui
.QTextOption()
596 text_options
.setAlignment(QtCore
.Qt
.AlignCenter
)
598 commit_pen
= QtGui
.QPen()
599 commit_pen
.setWidth(1.0)
600 commit_pen
.setColor(outline_color
)
602 cached_commit_color
= QtGui
.QColor
.fromRgb(128, 222, 255)
603 cached_commit_selected_color
= QtGui
.QColor
.fromRgb(32, 64, 255)
604 cached_merge_color
= QtGui
.QColor
.fromRgb(255, 255, 255)
606 def __init__(self
, commit
,
608 selectable
=QtGui
.QGraphicsItem
.ItemIsSelectable
,
609 cursor
=QtCore
.Qt
.PointingHandCursor
,
611 commit_color
=cached_commit_color
,
612 commit_selected_color
=cached_commit_selected_color
,
613 merge_color
=cached_merge_color
):
615 QtGui
.QGraphicsItem
.__init
__(self
)
618 self
.setFlag(selectable
)
619 self
.setCursor(cursor
)
622 self
.notifier
= notifier
625 self
.label
= label
= Label(commit
)
626 label
.setParentItem(self
)
627 label
.setPos(xpos
, 0.)
631 if len(commit
.parents
) > 1:
632 self
.commit_color
= merge_color
634 self
.commit_color
= commit_color
635 self
.text_pen
= QtCore
.Qt
.black
636 self
.sha1_text
= commit
.sha1
[:8]
642 # Overridden Qt methods
645 def blockSignals(self
, blocked
):
646 self
.notifier
.notification_enabled
= not blocked
648 def itemChange(self
, change
, value
):
649 if change
== QtGui
.QGraphicsItem
.ItemSelectedHasChanged
:
650 # Broadcast selection to other widgets
651 selected_items
= self
.scene().selectedItems()
652 commits
= [item
.commit
for item
in selected_items
]
653 self
.scene().parent().set_selecting(True)
654 sig
= signals
.commits_selected
655 self
.notifier
.notify_observers(sig
, commits
)
656 self
.scene().parent().set_selecting(False)
658 # Cache the pen for use in paint()
659 if value
.toPyObject():
660 self
.commit_color
= self
.cached_commit_selected_color
661 self
.text_pen
= QtCore
.Qt
.white
662 color
= self
.selected_color
664 self
.text_pen
= QtCore
.Qt
.black
665 if len(self
.commit
.parents
) > 1:
666 self
.commit_color
= self
.cached_merge_color
668 self
.commit_color
= self
.cached_commit_color
669 color
= self
.outline_color
670 commit_pen
= QtGui
.QPen()
671 commit_pen
.setWidth(1.0)
672 commit_pen
.setColor(color
)
673 self
.commit_pen
= commit_pen
675 return QtGui
.QGraphicsItem
.itemChange(self
, change
, value
)
678 return self
.item_type
680 def boundingRect(self
, rect
=item_bbox
):
684 return self
.item_shape
686 def paint(self
, painter
, option
, widget
,
688 text_opts
=text_options
,
691 # Do not draw outside the exposed rect
692 painter
.setClipRect(option
.exposedRect
)
695 painter
.setPen(self
.commit_pen
)
696 painter
.setBrush(self
.commit_color
)
697 painter
.drawEllipse(inner
)
702 except AttributeError:
703 font
= cache
.font
= painter
.font()
705 painter
.setFont(font
)
706 painter
.setPen(self
.text_pen
)
707 painter
.drawText(inner
, self
.sha1_text
, text_opts
)
709 def mousePressEvent(self
, event
):
710 QtGui
.QGraphicsItem
.mousePressEvent(self
, event
)
712 self
.selected
= self
.isSelected()
714 def mouseMoveEvent(self
, event
):
717 QtGui
.QGraphicsItem
.mouseMoveEvent(self
, event
)
719 def mouseReleaseEvent(self
, event
):
720 QtGui
.QGraphicsItem
.mouseReleaseEvent(self
, event
)
721 if (not self
.dragged
and
723 event
.button() == QtCore
.Qt
.LeftButton
):
729 class Label(QtGui
.QGraphicsItem
):
730 item_type
= QtGui
.QGraphicsItem
.UserType
+ 3
735 item_shape
= QtGui
.QPainterPath()
736 item_shape
.addRect(0, 0, width
, height
)
737 item_bbox
= item_shape
.boundingRect()
739 text_options
= QtGui
.QTextOption()
740 text_options
.setAlignment(QtCore
.Qt
.AlignCenter
)
741 text_options
.setAlignment(QtCore
.Qt
.AlignVCenter
)
743 def __init__(self
, commit
,
744 other_color
=QtGui
.QColor
.fromRgb(255, 255, 64),
745 head_color
=QtGui
.QColor
.fromRgb(64, 255, 64)):
746 QtGui
.QGraphicsItem
.__init
__(self
)
749 # Starts with enough space for two tags. Any more and the commit
750 # needs to be taller to accomodate.
752 height
= len(commit
.tags
) * self
.height
/2. + 4. # +6 padding
754 self
.label_box
= QtCore
.QRectF(0., -height
/2., self
.width
, height
)
755 self
.text_box
= QtCore
.QRectF(2., -height
/2., self
.width
-4., height
)
756 self
.tag_text
= '\n'.join(commit
.tags
)
758 if 'HEAD' in commit
.tags
:
759 self
.color
= head_color
761 self
.color
= other_color
763 self
.pen
= QtGui
.QPen()
764 self
.pen
.setColor(self
.color
.darker())
765 self
.pen
.setWidth(1.0)
768 return self
.item_type
770 def boundingRect(self
, rect
=item_bbox
):
774 return self
.item_shape
776 def paint(self
, painter
, option
, widget
,
777 text_opts
=text_options
,
778 black
=QtCore
.Qt
.black
,
781 painter
.setBrush(self
.color
)
782 painter
.setPen(self
.pen
)
783 painter
.drawRoundedRect(self
.label_box
, 4, 4)
786 except AttributeError:
787 font
= cache
.font
= painter
.font()
789 painter
.setFont(font
)
790 painter
.setPen(black
)
791 painter
.drawText(self
.text_box
, self
.tag_text
, text_opts
)
794 class GraphView(QtGui
.QGraphicsView
):
795 def __init__(self
, notifier
):
796 super(GraphView
, self
).__init
__()
799 from PyQt4
import QtOpenGL
800 glformat
= QtOpenGL
.QGLFormat(QtOpenGL
.QGL
.SampleBuffers
)
801 self
.glwidget
= QtOpenGL
.QGLWidget(glformat
)
802 self
.setViewport(self
.glwidget
)
812 self
.notifier
= notifier
817 self
.saved_matrix
= QtGui
.QMatrix(self
.matrix())
819 self
.x_offsets
= collections
.defaultdict(int)
821 self
.is_panning
= False
823 self
.selecting
= False
824 self
.last_mouse
= [0, 0]
826 self
.setDragMode(self
.RubberBandDrag
)
828 scene
= QtGui
.QGraphicsScene(self
)
829 scene
.setItemIndexMethod(QtGui
.QGraphicsScene
.NoIndex
)
833 self
.setRenderHint(QtGui
.QPainter
.Antialiasing
)
834 self
.setOptimizationFlag(self
.DontAdjustForAntialiasing
, True)
835 self
.setViewportUpdateMode(self
.SmartViewportUpdate
)
836 self
.setCacheMode(QtGui
.QGraphicsView
.CacheBackground
)
837 self
.setTransformationAnchor(QtGui
.QGraphicsView
.AnchorUnderMouse
)
838 self
.setResizeAnchor(QtGui
.QGraphicsView
.NoAnchor
)
839 self
.setBackgroundBrush(QtGui
.QColor
.fromRgb(0, 0, 0))
841 self
.action_zoom_in
= (
842 qtutils
.add_action(self
, 'Zoom In',
845 QtCore
.Qt
.Key_Equal
))
847 self
.action_zoom_out
= (
848 qtutils
.add_action(self
, 'Zoom Out',
850 QtCore
.Qt
.Key_Minus
))
852 self
.action_zoom_fit
= (
853 qtutils
.add_action(self
, 'Zoom to Fit',
857 self
.action_select_parent
= (
858 qtutils
.add_action(self
, 'Select Parent',
862 self
.action_select_oldest_parent
= (
863 qtutils
.add_action(self
, 'Select Oldest Parent',
864 self
.select_oldest_parent
,
867 self
.action_select_child
= (
868 qtutils
.add_action(self
, 'Select Child',
872 self
.action_select_child
= (
873 qtutils
.add_action(self
, 'Select Nth Child',
874 self
.select_nth_child
,
877 self
.menu_actions
= context_menu_actions(self
)
879 sig
= signals
.commits_selected
880 notifier
.add_observer(sig
, self
.commits_selected
)
886 self
.x_offsets
.clear()
895 self
.scale_view(1.0/1.5)
897 def commits_selected(self
, commits
):
900 self
.select([commit
.sha1
for commit
in commits
])
902 def contextMenuEvent(self
, event
):
903 update_menu_actions(self
, event
)
904 context_menu_event(self
, event
)
906 def select(self
, sha1s
):
907 """Select the item for the SHA-1"""
908 self
.scene().clearSelection()
911 item
= self
.items
[sha1
]
914 item
.blockSignals(True)
915 item
.setSelected(True)
916 item
.blockSignals(False)
917 item_rect
= item
.sceneTransform().mapRect(item
.boundingRect())
918 self
.ensureVisible(item_rect
)
920 def selected_item(self
):
921 """Return the currently selected item"""
922 selected_items
= self
.selectedItems()
923 if not selected_items
:
925 return selected_items
[0]
927 def selectedItems(self
):
928 """Return the currently selected items"""
929 return self
.scene().selectedItems()
931 def get_item_by_generation(self
, commits
, criteria_fn
):
932 """Return the item for the commit matching criteria"""
936 for commit
in commits
:
937 if (generation
is None or
938 criteria_fn(generation
, commit
.generation
)):
940 generation
= commit
.generation
942 return self
.items
[sha1
]
946 def oldest_item(self
, commits
):
947 """Return the item for the commit with the oldest generation number"""
948 return self
.get_item_by_generation(commits
, lambda a
, b
: a
> b
)
950 def newest_item(self
, commits
):
951 """Return the item for the commit with the newest generation number"""
952 return self
.get_item_by_generation(commits
, lambda a
, b
: a
< b
)
954 def diff_this_selected(self
):
955 clicked_sha1
= self
.clicked
.commit
.sha1
956 selected_sha1
= self
.selected
.commit
.sha1
957 self
.emit(SIGNAL('diff_commits'), clicked_sha1
, selected_sha1
)
959 def diff_selected_this(self
):
960 clicked_sha1
= self
.clicked
.commit
.sha1
961 selected_sha1
= self
.selected
.commit
.sha1
962 self
.emit(SIGNAL('diff_commits'), selected_sha1
, clicked_sha1
)
964 def create_patch(self
):
965 items
= self
.selectedItems()
968 selected_commits
= sort_by_generation([n
.commit
for n
in items
])
969 sha1s
= [c
.sha1
for c
in selected_commits
]
970 all_sha1s
= [c
.sha1
for c
in self
.commits
]
971 cola
.notifier().broadcast(signals
.format_patch
, sha1s
, all_sha1s
)
973 def create_branch(self
):
974 sha1
= self
.clicked
.commit
.sha1
975 create_new_branch(revision
=sha1
)
977 def create_tag(self
):
978 sha1
= self
.clicked
.commit
.sha1
979 create_tag(revision
=sha1
)
981 def cherry_pick(self
):
982 sha1
= self
.clicked
.commit
.sha1
983 cola
.notifier().broadcast(signals
.cherry_pick
, [sha1
])
984 self
.notifier
.notify_observers(self
.notifier
.refs_updated
)
986 def select_parent(self
):
987 """Select the parent with the newest generation number"""
988 selected_item
= self
.selected_item()
989 if selected_item
is None:
991 parent_item
= self
.newest_item(selected_item
.commit
.parents
)
992 if parent_item
is None:
994 selected_item
.setSelected(False)
995 parent_item
.setSelected(True)
996 self
.ensureVisible(parent_item
.mapRectToScene(parent_item
.boundingRect()))
998 def select_oldest_parent(self
):
999 """Select the parent with the oldest generation number"""
1000 selected_item
= self
.selected_item()
1001 if selected_item
is None:
1003 parent_item
= self
.oldest_item(selected_item
.commit
.parents
)
1004 if parent_item
is None:
1006 selected_item
.setSelected(False)
1007 parent_item
.setSelected(True)
1008 self
.ensureVisible(parent_item
.mapRectToScene(parent_item
.boundingRect()))
1010 def select_child(self
):
1011 """Select the child with the oldest generation number"""
1012 selected_item
= self
.selected_item()
1013 if selected_item
is None:
1015 child_item
= self
.oldest_item(selected_item
.commit
.children
)
1016 if child_item
is None:
1018 selected_item
.setSelected(False)
1019 child_item
.setSelected(True)
1020 self
.ensureVisible(child_item
.mapRectToScene(child_item
.boundingRect()))
1022 def select_nth_child(self
):
1023 """Select the Nth child with the newest generation number (N > 1)"""
1024 selected_item
= self
.selected_item()
1025 if selected_item
is None:
1027 if len(selected_item
.commit
.children
) > 1:
1028 children
= selected_item
.commit
.children
[1:]
1030 children
= selected_item
.commit
.children
1031 child_item
= self
.newest_item(children
)
1032 if child_item
is None:
1034 selected_item
.setSelected(False)
1035 child_item
.setSelected(True)
1036 self
.ensureVisible(child_item
.mapRectToScene(child_item
.boundingRect()))
1039 """Fit selected items into the viewport"""
1041 items
= self
.scene().selectedItems()
1043 rect
= self
.scene().itemsBoundingRect()
1051 item_rect
= item
.boundingRect()
1052 x_off
= item_rect
.width()
1053 y_off
= item_rect
.height()
1054 x_min
= min(x_min
, pos
.x())
1055 y_min
= min(y_min
, pos
.y())
1056 x_max
= max(x_max
, pos
.x()+x_off
)
1057 ymax
= max(ymax
, pos
.y()+y_off
)
1058 rect
= QtCore
.QRectF(x_min
, y_min
, x_max
-x_min
, ymax
-y_min
)
1059 adjust
= Commit
.width
* 2
1060 rect
.setX(rect
.x() - adjust
)
1061 rect
.setY(rect
.y() - adjust
)
1062 rect
.setHeight(rect
.height() + adjust
)
1063 rect
.setWidth(rect
.width() + adjust
)
1064 self
.fitInView(rect
, QtCore
.Qt
.KeepAspectRatio
)
1065 self
.scene().invalidate()
1067 def save_selection(self
, event
):
1068 if event
.button() != QtCore
.Qt
.LeftButton
:
1070 elif QtCore
.Qt
.ShiftModifier
!= event
.modifiers():
1072 self
.selected
= self
.selectedItems()
1074 def restore_selection(self
, event
):
1075 if QtCore
.Qt
.ShiftModifier
!= event
.modifiers():
1077 for item
in self
.selected
:
1078 item
.setSelected(True)
1080 def handle_event(self
, event_handler
, event
):
1082 self
.save_selection(event
)
1083 event_handler(self
, event
)
1084 self
.restore_selection(event
)
1086 def mousePressEvent(self
, event
):
1087 if event
.button() == QtCore
.Qt
.MidButton
:
1089 self
.mouse_start
= [pos
.x(), pos
.y()]
1090 self
.saved_matrix
= QtGui
.QMatrix(self
.matrix())
1091 self
.is_panning
= True
1093 if event
.button() == QtCore
.Qt
.RightButton
:
1096 if event
.button() == QtCore
.Qt
.LeftButton
:
1098 self
.handle_event(QtGui
.QGraphicsView
.mousePressEvent
, event
)
1100 def mouseMoveEvent(self
, event
):
1101 pos
= self
.mapToScene(event
.pos())
1105 self
.last_mouse
[0] = pos
.x()
1106 self
.last_mouse
[1] = pos
.y()
1107 self
.handle_event(QtGui
.QGraphicsView
.mouseMoveEvent
, event
)
1109 def set_selecting(self
, selecting
):
1110 self
.selecting
= selecting
1112 def mouseReleaseEvent(self
, event
):
1113 self
.pressed
= False
1114 if event
.button() == QtCore
.Qt
.MidButton
:
1115 self
.is_panning
= False
1117 self
.handle_event(QtGui
.QGraphicsView
.mouseReleaseEvent
, event
)
1120 def pan(self
, event
):
1122 dx
= pos
.x() - self
.mouse_start
[0]
1123 dy
= pos
.y() - self
.mouse_start
[1]
1125 if dx
== 0 and dy
== 0:
1128 rect
= QtCore
.QRect(0, 0, abs(dx
), abs(dy
))
1129 delta
= self
.mapToScene(rect
).boundingRect()
1139 matrix
= QtGui
.QMatrix(self
.saved_matrix
).translate(tx
, ty
)
1140 self
.setTransformationAnchor(QtGui
.QGraphicsView
.NoAnchor
)
1141 self
.setMatrix(matrix
)
1143 def wheelEvent(self
, event
):
1144 """Handle Qt mouse wheel events."""
1145 if event
.modifiers() == QtCore
.Qt
.ControlModifier
:
1146 self
.wheel_zoom(event
)
1148 self
.wheel_pan(event
)
1150 def wheel_zoom(self
, event
):
1151 """Handle mouse wheel zooming."""
1152 zoom
= math
.pow(2.0, event
.delta() / 512.0)
1153 factor
= (self
.matrix()
1155 .mapRect(QtCore
.QRectF(0.0, 0.0, 1.0, 1.0))
1157 if factor
< 0.014 or factor
> 42.0:
1159 self
.setTransformationAnchor(QtGui
.QGraphicsView
.AnchorUnderMouse
)
1161 self
.scale(zoom
, zoom
)
1163 def wheel_pan(self
, event
):
1164 """Handle mouse wheel panning."""
1166 if event
.delta() < 0:
1170 pan_rect
= QtCore
.QRectF(0.0, 0.0, 1.0, 1.0)
1171 factor
= 1.0 / self
.matrix().mapRect(pan_rect
).width()
1173 if event
.orientation() == QtCore
.Qt
.Vertical
:
1174 matrix
= self
.matrix().translate(0, s
* factor
)
1176 matrix
= self
.matrix().translate(s
* factor
, 0)
1177 self
.setTransformationAnchor(QtGui
.QGraphicsView
.NoAnchor
)
1178 self
.setMatrix(matrix
)
1180 def scale_view(self
, scale
):
1181 factor
= (self
.matrix().scale(scale
, scale
)
1182 .mapRect(QtCore
.QRectF(0, 0, 1, 1))
1184 if factor
< 0.07 or factor
> 100:
1188 adjust_scrollbars
= True
1189 scrollbar
= self
.verticalScrollBar()
1191 value
= scrollbar
.value()
1192 min_
= scrollbar
.minimum()
1193 max_
= scrollbar
.maximum()
1194 range_
= max_
- min_
1195 distance
= value
- min_
1196 nonzero_range
= float(range_
) != 0.0
1198 scrolloffset
= distance
/float(range_
)
1200 adjust_scrollbars
= False
1202 self
.setTransformationAnchor(QtGui
.QGraphicsView
.NoAnchor
)
1203 self
.scale(scale
, scale
)
1205 scrollbar
= self
.verticalScrollBar()
1206 if scrollbar
and adjust_scrollbars
:
1207 min_
= scrollbar
.minimum()
1208 max_
= scrollbar
.maximum()
1209 range_
= max_
- min_
1210 value
= min_
+ int(float(range_
) * scrolloffset
)
1211 scrollbar
.setValue(value
)
1213 def add_commits(self
, commits
):
1214 """Traverse commits and add them to the view."""
1215 self
.commits
.extend(commits
)
1216 scene
= self
.scene()
1217 for commit
in commits
:
1218 item
= Commit(commit
, self
.notifier
)
1219 self
.items
[commit
.sha1
] = item
1220 for ref
in commit
.tags
:
1221 self
.items
[ref
] = item
1224 self
.layout_commits(commits
)
1227 def link(self
, commits
):
1228 """Create edges linking commits with their parents"""
1229 scene
= self
.scene()
1230 for commit
in commits
:
1232 commit_item
= self
.items
[commit
.sha1
]
1234 # TODO - Handle truncated history viewing
1236 for parent
in commit
.parents
:
1238 parent_item
= self
.items
[parent
.sha1
]
1240 # TODO - Handle truncated history viewing
1242 edge
= Edge(parent_item
, commit_item
)
1245 def layout_commits(self
, nodes
):
1246 positions
= self
.position_nodes(nodes
)
1247 for sha1
, (x
, y
) in positions
.items():
1248 item
= self
.items
[sha1
]
1251 def position_nodes(self
, nodes
):
1258 x_offsets
= self
.x_offsets
1261 generation
= node
.generation
1264 if len(node
.children
) > 1:
1265 # This is a fan-out so sweep over child generations and
1266 # shift them to the right to avoid overlapping edges
1267 child_gens
= [c
.generation
for c
in node
.children
]
1268 maxgen
= reduce(max, child_gens
)
1269 mingen
= reduce(min, child_gens
)
1271 for g
in xrange(generation
+1, maxgen
):
1272 x_offsets
[g
] += x_off
1274 if len(node
.parents
) == 1:
1275 # Align nodes relative to their parents
1276 parent_gen
= node
.parents
[0].generation
1277 parent_off
= x_offsets
[parent_gen
]
1278 x_offsets
[generation
] = max(parent_off
-x_off
,
1279 x_offsets
[generation
])
1281 cur_xoff
= x_offsets
[generation
]
1282 next_xoff
= cur_xoff
1284 x_offsets
[generation
] = next_xoff
1287 y_pos
= -generation
* y_off
1288 positions
[sha1
] = (x_pos
, y_pos
)
1290 x_max
= max(x_max
, x_pos
)
1291 y_min
= min(y_min
, y_pos
)
1299 def update_scene_rect(self
):
1302 self
.scene().setSceneRect(-self
.x_off
/2,
1305 abs(y_min
)+self
.y_off
*2)
1307 def sort_by_generation(commits
):
1308 commits
.sort(cmp=lambda a
, b
: cmp(a
.generation
, b
.generation
))
1312 def context_menu_actions(self
):
1314 'diff_this_selected':
1315 qtutils
.add_action(self
, 'Diff this -> selected',
1316 self
.diff_this_selected
),
1317 'diff_selected_this':
1318 qtutils
.add_action(self
, 'Diff selected -> this',
1319 self
.diff_selected_this
),
1321 qtutils
.add_action(self
, 'Create Branch',
1322 self
.create_branch
),
1324 qtutils
.add_action(self
, 'Create Patch',
1327 qtutils
.add_action(self
, 'Create Tag',
1330 qtutils
.add_action(self
, 'Save As Tarball/Zip...',
1331 lambda: create_tarball(self
)),
1333 qtutils
.add_action(self
, 'Cherry Pick',
1337 qtutils
.add_action(self
, 'Grab File...',
1338 lambda: save_blob_dialog(self
)),
1342 def update_menu_actions(self
, event
):
1343 clicked
= self
.itemAt(event
.pos())
1344 selected_items
= self
.selectedItems()
1345 has_single_selection
= len(selected_items
) == 1
1347 has_selection
= bool(selected_items
)
1348 can_diff
= bool(clicked
and has_single_selection
and
1349 clicked
is not selected_items
[0])
1351 self
.clicked
= clicked
1353 self
.selected
= selected_items
[0]
1355 self
.selected
= None
1357 self
.menu_actions
['diff_this_selected'].setEnabled(can_diff
)
1358 self
.menu_actions
['diff_selected_this'].setEnabled(can_diff
)
1359 self
.menu_actions
['create_patch'].setEnabled(has_selection
)
1360 self
.menu_actions
['create_tarball'].setEnabled(has_single_selection
)
1361 self
.menu_actions
['save_blob'].setEnabled(has_single_selection
)
1362 self
.menu_actions
['create_branch'].setEnabled(has_single_selection
)
1363 self
.menu_actions
['create_tag'].setEnabled(has_single_selection
)
1364 self
.menu_actions
['cherry_pick'].setEnabled(has_single_selection
)
1367 def context_menu_event(self
, event
):
1368 menu
= QtGui
.QMenu(self
)
1369 menu
.addAction(self
.menu_actions
['diff_this_selected'])
1370 menu
.addAction(self
.menu_actions
['diff_selected_this'])
1372 menu
.addAction(self
.menu_actions
['create_branch'])
1373 menu
.addAction(self
.menu_actions
['create_tag'])
1375 menu
.addAction(self
.menu_actions
['cherry_pick'])
1376 menu
.addAction(self
.menu_actions
['create_patch'])
1377 menu
.addAction(self
.menu_actions
['create_tarball'])
1379 menu
.addAction(self
.menu_actions
['save_blob'])
1380 menu
.exec_(self
.mapToGlobal(event
.pos()))
1383 def create_tarball(self
):
1384 ref
= self
.clicked
.commit
.sha1
1386 GitArchiveDialog
.save(ref
, shortref
, self
)
1389 def save_blob_dialog(self
):
1390 return BrowseDialog
.browse(self
.clicked
.commit
.sha1
)