5 from PyQt4
import QtGui
6 from PyQt4
import QtCore
7 from PyQt4
.QtCore
import Qt
8 from PyQt4
.QtCore
import SIGNAL
9 from PyQt4
.QtCore
import QPointF
10 from PyQt4
.QtCore
import QRectF
13 from cola
import difftool
14 from cola
import observable
16 from cola
import qtutils
17 from cola
.dag
.model
import RepoReader
18 from cola
.i18n
import N_
19 from cola
.qt
import create_menu
20 from cola
.widgets
import completion
21 from cola
.widgets
import defs
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
26 from cola
.widgets
.standard
import MainWindow
27 from cola
.widgets
.standard
import TreeWidget
28 from cola
.widgets
.diff
import COMMITS_SELECTED
29 from cola
.widgets
.diff
import DiffWidget
32 class ViewerMixin(object):
33 """Implementations must provide selected_items()"""
38 self
.menu_actions
= self
.context_menu_actions()
40 def selected_item(self
):
41 """Return the currently selected item"""
42 selected_items
= self
.selected_items()
43 if not selected_items
:
45 return selected_items
[0]
47 def selected_sha1(self
):
48 item
= self
.selected_item()
51 return item
.commit
.sha1
53 def diff_selected_this(self
):
54 clicked_sha1
= self
.clicked
.sha1
55 selected_sha1
= self
.selected
.sha1
56 self
.emit(SIGNAL('diff_commits'), selected_sha1
, clicked_sha1
)
58 def diff_this_selected(self
):
59 clicked_sha1
= self
.clicked
.sha1
60 selected_sha1
= self
.selected
.sha1
61 self
.emit(SIGNAL('diff_commits'), clicked_sha1
, selected_sha1
)
63 def cherry_pick(self
):
64 sha1
= self
.selected_sha1()
67 cmds
.do(cmds
.CherryPick
, [sha1
])
69 def copy_to_clipboard(self
):
70 sha1
= self
.selected_sha1()
73 qtutils
.set_clipboard(sha1
)
75 def create_branch(self
):
76 sha1
= self
.selected_sha1()
79 create_new_branch(revision
=sha1
)
82 sha1
= self
.selected_sha1()
85 create_tag(revision
=sha1
)
87 def create_tarball(self
):
88 sha1
= self
.selected_sha1()
92 GitArchiveDialog
.save(sha1
, short_sha1
, self
)
94 def save_blob_dialog(self
):
95 sha1
= self
.selected_sha1()
98 return BrowseDialog
.browse(sha1
)
100 def context_menu_actions(self
):
102 'diff_this_selected':
103 qtutils
.add_action(self
, N_('Diff this -> selected'),
104 self
.diff_this_selected
),
105 'diff_selected_this':
106 qtutils
.add_action(self
, N_('Diff selected -> this'),
107 self
.diff_selected_this
),
109 qtutils
.add_action(self
, N_('Create Branch'),
112 qtutils
.add_action(self
, N_('Create Patch'),
115 qtutils
.add_action(self
, N_('Create Tag'),
118 qtutils
.add_action(self
, N_('Save As Tarball/Zip...'),
119 self
.create_tarball
),
121 qtutils
.add_action(self
, N_('Cherry Pick'),
124 qtutils
.add_action(self
, N_('Grab File...'),
125 self
.save_blob_dialog
),
127 qtutils
.add_action(self
, N_('Copy SHA-1'),
128 self
.copy_to_clipboard
,
129 QtGui
.QKeySequence
.Copy
),
132 def update_menu_actions(self
, event
):
133 selected_items
= self
.selected_items()
134 item
= self
.itemAt(event
.pos())
136 self
.clicked
= commit
= None
138 self
.clicked
= commit
= item
.commit
140 has_single_selection
= len(selected_items
) == 1
141 has_selection
= bool(selected_items
)
142 can_diff
= bool(commit
and has_single_selection
and
143 commit
is not selected_items
[0].commit
)
146 self
.selected
= selected_items
[0].commit
150 self
.menu_actions
['diff_this_selected'].setEnabled(can_diff
)
151 self
.menu_actions
['diff_selected_this'].setEnabled(can_diff
)
153 self
.menu_actions
['create_branch'].setEnabled(has_single_selection
)
154 self
.menu_actions
['create_tag'].setEnabled(has_single_selection
)
156 self
.menu_actions
['cherry_pick'].setEnabled(has_single_selection
)
157 self
.menu_actions
['create_patch'].setEnabled(has_selection
)
158 self
.menu_actions
['create_tarball'].setEnabled(has_single_selection
)
160 self
.menu_actions
['save_blob'].setEnabled(has_single_selection
)
161 self
.menu_actions
['copy'].setEnabled(has_single_selection
)
163 def context_menu_event(self
, event
):
164 self
.update_menu_actions(event
)
165 menu
= QtGui
.QMenu(self
)
166 menu
.addAction(self
.menu_actions
['diff_this_selected'])
167 menu
.addAction(self
.menu_actions
['diff_selected_this'])
169 menu
.addAction(self
.menu_actions
['create_branch'])
170 menu
.addAction(self
.menu_actions
['create_tag'])
172 menu
.addAction(self
.menu_actions
['cherry_pick'])
173 menu
.addAction(self
.menu_actions
['create_patch'])
174 menu
.addAction(self
.menu_actions
['create_tarball'])
176 menu
.addAction(self
.menu_actions
['save_blob'])
177 menu
.addAction(self
.menu_actions
['copy'])
178 menu
.exec_(self
.mapToGlobal(event
.pos()))
181 class CommitTreeWidgetItem(QtGui
.QTreeWidgetItem
):
183 def __init__(self
, commit
, parent
=None):
184 QtGui
.QTreeWidgetItem
.__init
__(self
, parent
)
186 self
.setText(0, commit
.summary
)
187 self
.setText(1, commit
.author
)
188 self
.setText(2, commit
.authdate
)
191 class CommitTreeWidget(ViewerMixin
, TreeWidget
):
193 def __init__(self
, notifier
, parent
):
194 TreeWidget
.__init
__(self
, parent
)
195 ViewerMixin
.__init
__(self
)
197 self
.setSelectionMode(self
.ContiguousSelection
)
198 self
.setHeaderLabels([N_('Summary'), N_('Author'), N_('Date, Time')])
201 self
.notifier
= notifier
202 self
.selecting
= False
205 self
.action_up
= qtutils
.add_action(self
, N_('Go Up'), self
.go_up
,
208 self
.action_down
= qtutils
.add_action(self
, N_('Go Down'), self
.go_down
,
211 notifier
.add_observer(COMMITS_SELECTED
, self
.commits_selected
)
213 self
.connect(self
, SIGNAL('itemSelectionChanged()'),
214 self
.selection_changed
)
218 self
.goto(self
.itemAbove
)
221 self
.goto(self
.itemBelow
)
223 def goto(self
, finder
):
224 items
= self
.selected_items()
225 item
= items
and items
[0] or None
230 self
.select([found
.commit
.sha1
], block_signals
=False)
232 def set_selecting(self
, selecting
):
233 self
.selecting
= selecting
235 def selection_changed(self
):
236 items
= self
.selected_items()
239 self
.set_selecting(True)
240 self
.notifier
.notify_observers(COMMITS_SELECTED
,
241 [i
.commit
for i
in items
])
242 self
.set_selecting(False)
244 def commits_selected(self
, commits
):
247 self
.select([commit
.sha1
for commit
in commits
])
249 def select(self
, sha1s
, block_signals
=True):
250 self
.clearSelection()
253 item
= self
.sha1map
[sha1
]
256 block
= self
.blockSignals(block_signals
)
257 self
.scrollToItem(item
)
258 item
.setSelected(True)
259 self
.blockSignals(block
)
261 def adjust_columns(self
):
262 width
= self
.width()-20
265 self
.setColumnWidth(0, zero
)
266 self
.setColumnWidth(1, onetwo
)
267 self
.setColumnWidth(2, onetwo
)
270 QtGui
.QTreeWidget
.clear(self
)
274 def add_commits(self
, commits
):
275 self
.commits
.extend(commits
)
277 for c
in reversed(commits
):
278 item
= CommitTreeWidgetItem(c
)
280 self
.sha1map
[c
.sha1
] = item
282 self
.sha1map
[tag
] = item
283 self
.insertTopLevelItems(0, items
)
285 def create_patch(self
):
286 items
= self
.selectedItems()
290 sha1s
= [item
.commit
.sha1
for item
in items
]
291 all_sha1s
= [c
.sha1
for c
in self
.commits
]
292 cmds
.do(cmds
.FormatPatch
, sha1s
, all_sha1s
)
295 def contextMenuEvent(self
, event
):
296 self
.context_menu_event(event
)
298 def mousePressEvent(self
, event
):
299 if event
.button() == Qt
.RightButton
:
302 QtGui
.QTreeWidget
.mousePressEvent(self
, event
)
305 class DAGView(MainWindow
):
306 """The git-dag widget."""
308 def __init__(self
, model
, dag
, parent
=None, args
=None):
309 MainWindow
.__init
__(self
, parent
)
311 self
.setAttribute(Qt
.WA_MacMetalStyle
)
312 self
.setMinimumSize(420, 420)
314 # change when widgets are added/removed
315 self
.widget_version
= 1
320 self
.commit_list
= []
322 self
.old_count
= None
324 self
.thread
= ReaderThread(dag
, self
)
326 self
.revtext
= completion
.GitLogLineEdit()
328 self
.maxresults
= QtGui
.QSpinBox()
329 self
.maxresults
.setMinimum(1)
330 self
.maxresults
.setMaximum(99999)
331 self
.maxresults
.setPrefix('')
332 self
.maxresults
.setSuffix('')
334 self
.zoom_out
= qt
.create_action_button(
335 N_('Zoom Out'), qtutils
.theme_icon('zoom-out.png'))
337 self
.zoom_in
= qt
.create_action_button(
338 N_('Zoom In'), qtutils
.theme_icon('zoom-in.png'))
340 self
.zoom_to_fit
= qt
.create_action_button(
341 N_('Zoom to Fit'), qtutils
.theme_icon('zoom-fit-best.png'))
343 self
.notifier
= notifier
= observable
.Observable()
344 self
.notifier
.refs_updated
= refs_updated
= 'refs_updated'
345 self
.notifier
.add_observer(refs_updated
, self
.display
)
347 self
.treewidget
= CommitTreeWidget(notifier
, self
)
348 self
.diffwidget
= DiffWidget(notifier
, self
)
349 self
.graphview
= GraphView(notifier
, self
)
351 self
.controls_layout
= QtGui
.QHBoxLayout()
352 self
.controls_layout
.setMargin(defs
.no_margin
)
353 self
.controls_layout
.setSpacing(defs
.spacing
)
354 self
.controls_layout
.addWidget(self
.revtext
)
355 self
.controls_layout
.addWidget(self
.maxresults
)
357 self
.controls_widget
= QtGui
.QWidget()
358 self
.controls_widget
.setLayout(self
.controls_layout
)
360 self
.log_dock
= qt
.create_dock(N_('Log'), self
, stretch
=False)
361 self
.log_dock
.setWidget(self
.treewidget
)
362 log_dock_titlebar
= self
.log_dock
.titleBarWidget()
363 log_dock_titlebar
.add_corner_widget(self
.controls_widget
)
365 self
.diff_dock
= qt
.create_dock(N_('Diff'), self
)
366 self
.diff_dock
.setWidget(self
.diffwidget
)
368 self
.graph_controls_layout
= QtGui
.QHBoxLayout()
369 self
.graph_controls_layout
.setMargin(defs
.no_margin
)
370 self
.graph_controls_layout
.setSpacing(defs
.button_spacing
)
371 self
.graph_controls_layout
.addWidget(self
.zoom_out
)
372 self
.graph_controls_layout
.addWidget(self
.zoom_in
)
373 self
.graph_controls_layout
.addWidget(self
.zoom_to_fit
)
375 self
.graph_controls_widget
= QtGui
.QWidget()
376 self
.graph_controls_widget
.setLayout(self
.graph_controls_layout
)
378 self
.graphview_dock
= qt
.create_dock(N_('Graph'), self
)
379 self
.graphview_dock
.setWidget(self
.graphview
)
380 graph_titlebar
= self
.graphview_dock
.titleBarWidget()
381 graph_titlebar
.add_corner_widget(self
.graph_controls_widget
)
383 self
.lock_layout_action
= qtutils
.add_action_bool(self
,
384 N_('Lock Layout'), self
.set_lock_layout
, False)
386 # Create the application menu
387 self
.menubar
= QtGui
.QMenuBar(self
)
390 self
.view_menu
= create_menu(N_('View'), self
.menubar
)
391 self
.view_menu
.addAction(self
.log_dock
.toggleViewAction())
392 self
.view_menu
.addAction(self
.graphview_dock
.toggleViewAction())
393 self
.view_menu
.addAction(self
.diff_dock
.toggleViewAction())
394 self
.view_menu
.addSeparator()
395 self
.view_menu
.addAction(self
.lock_layout_action
)
397 self
.menubar
.addAction(self
.view_menu
.menuAction())
398 self
.setMenuBar(self
.menubar
)
400 left
= Qt
.LeftDockWidgetArea
401 right
= Qt
.RightDockWidgetArea
402 bottom
= Qt
.BottomDockWidgetArea
403 self
.addDockWidget(left
, self
.log_dock
)
404 self
.addDockWidget(right
, self
.graphview_dock
)
405 self
.addDockWidget(bottom
, self
.diff_dock
)
407 # Update fields affected by model
408 self
.revtext
.setText(dag
.ref
)
409 self
.maxresults
.setValue(dag
.count
)
410 self
.update_window_title()
412 # Also re-loads dag.* from the saved state
413 if not qtutils
.apply_state(self
):
414 self
.resize_to_desktop()
416 qtutils
.connect_button(self
.zoom_out
, self
.graphview
.zoom_out
)
417 qtutils
.connect_button(self
.zoom_in
, self
.graphview
.zoom_in
)
418 qtutils
.connect_button(self
.zoom_to_fit
,
419 self
.graphview
.zoom_to_fit
)
421 self
.thread
.connect(self
.thread
, self
.thread
.commits_ready
,
424 self
.thread
.connect(self
.thread
, self
.thread
.done
,
427 self
.connect(self
.treewidget
, SIGNAL('diff_commits'),
430 self
.connect(self
.graphview
, SIGNAL('diff_commits'),
433 self
.connect(self
.maxresults
, SIGNAL('editingFinished()'),
436 self
.connect(self
.revtext
, SIGNAL('changed()'),
439 self
.connect(self
.revtext
, SIGNAL('textChanged(QString)'),
442 self
.connect(self
.revtext
, SIGNAL('returnPressed()'),
445 # The model is updated in another thread so use
446 # signals/slots to bring control back to the main GUI thread
447 self
.model
.add_observer(self
.model
.message_updated
,
448 self
.emit_model_updated
)
450 self
.connect(self
, SIGNAL('model_updated'),
453 qtutils
.add_action(self
, 'Focus search field',
454 lambda: self
.revtext
.setFocus(), 'Ctrl+l')
456 qtutils
.add_close_action(self
)
458 def text_changed(self
, txt
):
459 self
.dag
.ref
= unicode(txt
)
460 self
.update_window_title()
462 def update_window_title(self
):
463 project
= self
.model
.project
465 self
.setWindowTitle(N_('%s: %s - DAG') % (project
, self
.dag
.ref
))
467 self
.setWindowTitle(project
+ N_(' - DAG'))
469 def export_state(self
):
470 state
= self
.Mixin
.export_state(self
)
471 state
['count'] = self
.dag
.count
474 def apply_state(self
, state
):
475 result
= self
.Mixin
.apply_state(self
, state
)
477 count
= state
['count']
478 if self
.dag
.overridden('count'):
479 count
= self
.dag
.count
481 count
= self
.dag
.count
483 self
.dag
.set_count(count
)
484 self
.lock_layout_action
.setChecked(state
.get('lock_layout', False))
487 def emit_model_updated(self
):
488 self
.emit(SIGNAL('model_updated'))
490 def model_updated(self
):
492 self
.revtext
.update_matches()
494 if not self
.model
.currentbranch
:
496 self
.revtext
.setText(self
.model
.currentbranch
)
500 new_ref
= unicode(self
.revtext
.text())
503 new_count
= self
.maxresults
.value()
504 old_ref
= self
.old_ref
505 old_count
= self
.old_count
506 if old_ref
== new_ref
and old_count
== new_count
:
509 self
.old_ref
= new_ref
510 self
.old_count
= new_count
514 self
.dag
.set_ref(new_ref
)
515 self
.dag
.set_count(self
.maxresults
.value())
519 self
.Mixin
.show(self
)
520 self
.treewidget
.adjust_columns()
523 self
.graphview
.clear()
524 self
.treewidget
.clear()
526 self
.commit_list
= []
528 def add_commits(self
, commits
):
529 self
.commit_list
.extend(commits
)
530 # Keep track of commits
531 for commit_obj
in commits
:
532 self
.commits
[commit_obj
.sha1
] = commit_obj
533 for tag
in commit_obj
.tags
:
534 self
.commits
[tag
] = commit_obj
535 self
.graphview
.add_commits(commits
)
536 self
.treewidget
.add_commits(commits
)
538 def thread_done(self
):
539 self
.graphview
.setFocus()
541 commit_obj
= self
.commit_list
[-1]
544 self
.notifier
.notify_observers(COMMITS_SELECTED
, [commit_obj
])
545 self
.graphview
.update_scene_rect()
546 self
.graphview
.set_initial_view()
548 def resize_to_desktop(self
):
549 desktop
= QtGui
.QApplication
.instance().desktop()
550 width
= desktop
.width()
551 height
= desktop
.height()
552 self
.resize(width
, height
)
554 def diff_commits(self
, a
, b
):
555 paths
= self
.dag
.paths()
557 difftool
.launch([a
, b
, '--'] + paths
)
559 difftool
.diff_commits(self
, a
, b
)
562 def closeEvent(self
, event
):
563 self
.revtext
.close_popup()
565 self
.Mixin
.closeEvent(self
, event
)
567 def resizeEvent(self
, e
):
568 self
.Mixin
.resizeEvent(self
, e
)
569 self
.treewidget
.adjust_columns()
572 class ReaderThread(QtCore
.QThread
):
573 commits_ready
= SIGNAL('commits_ready')
574 done
= SIGNAL('done')
576 def __init__(self
, dag
, parent
):
577 QtCore
.QThread
.__init
__(self
, parent
)
581 self
._mutex
= QtCore
.QMutex()
582 self
._condition
= QtCore
.QWaitCondition()
585 repo
= RepoReader(self
.dag
)
591 self
._condition
.wait(self
._mutex
)
597 if len(commits
) >= 512:
598 self
.emit(self
.commits_ready
, commits
)
602 self
.emit(self
.commits_ready
, commits
)
608 QtCore
.QThread
.start(self
)
619 self
._condition
.wakeOne()
630 class Edge(QtGui
.QGraphicsItem
):
631 item_type
= QtGui
.QGraphicsItem
.UserType
+ 1
633 def __init__(self
, source
, dest
):
635 QtGui
.QGraphicsItem
.__init
__(self
)
637 self
.setAcceptedMouseButtons(Qt
.NoButton
)
640 self
.commit
= source
.commit
643 dest_pt
= Commit
.item_bbox
.center()
645 self
.source_pt
= self
.mapFromItem(self
.source
, dest_pt
)
646 self
.dest_pt
= self
.mapFromItem(self
.dest
, dest_pt
)
647 self
.line
= QtCore
.QLineF(self
.source_pt
, self
.dest_pt
)
649 width
= self
.dest_pt
.x() - self
.source_pt
.x()
650 height
= self
.dest_pt
.y() - self
.source_pt
.y()
651 rect
= QtCore
.QRectF(self
.source_pt
, QtCore
.QSizeF(width
, height
))
652 self
.bound
= rect
.normalized()
654 # Choose a new color for new branch edges
655 if self
.source
.x() < self
.dest
.x():
656 color
= EdgeColor
.next()
658 elif self
.source
.x() != self
.dest
.x():
659 color
= EdgeColor
.current()
662 color
= EdgeColor
.current()
665 self
.pen
= QtGui
.QPen(color
, 4.0, line
, Qt
.SquareCap
, Qt
.RoundJoin
)
669 return self
.item_type
671 def boundingRect(self
):
674 def paint(self
, painter
, option
, widget
):
679 painter
.setPen(self
.pen
)
680 path
= QtGui
.QPainterPath()
682 if self
.source
.x() == self
.dest
.x():
683 path
.moveTo(self
.source
.x(), self
.source
.y())
684 path
.lineTo(self
.dest
.x(), self
.dest
.y())
685 painter
.drawPath(path
)
689 #Define points starting from source
690 point1
= QPointF(self
.source
.x(), self
.source
.y())
691 point2
= QPointF(point1
.x(), point1
.y() - connector_length
)
692 point3
= QPointF(point2
.x() + arc_rect
, point2
.y() - arc_rect
)
694 #Define points starting from dest
695 point4
= QPointF(self
.dest
.x(), self
.dest
.y())
696 point5
= QPointF(point4
.x(),point3
.y() - arc_rect
)
697 point6
= QPointF(point5
.x() - arc_rect
, point5
.y() + arc_rect
)
699 start_angle_arc1
= 180
701 start_angle_arc2
= 90
702 span_angle_arc2
= -90
704 # If the dest is at the left of the source, then we
705 # need to reverse some values
706 if self
.source
.x() > self
.dest
.x():
707 point5
= QPointF(point4
.x(), point4
.y() + connector_length
)
708 point6
= QPointF(point5
.x() + arc_rect
, point5
.y() + arc_rect
)
709 point3
= QPointF(self
.source
.x() - arc_rect
, point6
.y())
710 point2
= QPointF(self
.source
.x(), point3
.y() + arc_rect
)
716 path
.arcTo(QRectF(point2
, point3
),
717 start_angle_arc1
, span_angle_arc1
)
719 path
.arcTo(QRectF(point6
, point5
),
720 start_angle_arc2
, span_angle_arc2
)
722 painter
.drawPath(path
)
725 class EdgeColor(object):
726 """An edge color factory"""
728 current_color_index
= 0
730 QtGui
.QColor(Qt
.red
),
731 QtGui
.QColor(Qt
.green
),
732 QtGui
.QColor(Qt
.blue
),
733 QtGui
.QColor(Qt
.black
),
734 QtGui
.QColor(Qt
.darkRed
),
735 QtGui
.QColor(Qt
.darkGreen
),
736 QtGui
.QColor(Qt
.darkBlue
),
737 QtGui
.QColor(Qt
.cyan
),
738 QtGui
.QColor(Qt
.magenta
),
739 # Orange; Qt.yellow is too low-contrast
740 qt
.rgba(0xff, 0x66, 0x00),
741 QtGui
.QColor(Qt
.gray
),
742 QtGui
.QColor(Qt
.darkCyan
),
743 QtGui
.QColor(Qt
.darkMagenta
),
744 QtGui
.QColor(Qt
.darkYellow
),
745 QtGui
.QColor(Qt
.darkGray
),
750 cls
.current_color_index
+= 1
751 cls
.current_color_index
%= len(cls
.colors
)
752 color
= cls
.colors
[cls
.current_color_index
]
758 return cls
.colors
[cls
.current_color_index
]
761 class Commit(QtGui
.QGraphicsItem
):
762 item_type
= QtGui
.QGraphicsItem
.UserType
+ 2
766 item_shape
= QtGui
.QPainterPath()
767 item_shape
.addRect(commit_radius
/-2.0,
769 commit_radius
, commit_radius
)
770 item_bbox
= item_shape
.boundingRect()
772 inner_rect
= QtGui
.QPainterPath()
773 inner_rect
.addRect(commit_radius
/-2.0 + 2.0,
774 commit_radius
/-2.0 + 2.0,
777 inner_rect
= inner_rect
.boundingRect()
779 commit_color
= QtGui
.QColor(Qt
.white
)
780 outline_color
= commit_color
.darker()
781 merge_color
= QtGui
.QColor(Qt
.lightGray
)
783 commit_selected_color
= QtGui
.QColor(Qt
.green
)
784 selected_outline_color
= commit_selected_color
.darker()
786 commit_pen
= QtGui
.QPen()
787 commit_pen
.setWidth(1.0)
788 commit_pen
.setColor(outline_color
)
790 def __init__(self
, commit
,
792 selectable
=QtGui
.QGraphicsItem
.ItemIsSelectable
,
793 cursor
=Qt
.PointingHandCursor
,
794 xpos
=commit_radius
/2.0 + 1.0,
795 cached_commit_color
=commit_color
,
796 cached_merge_color
=merge_color
):
798 QtGui
.QGraphicsItem
.__init
__(self
)
801 self
.notifier
= notifier
804 self
.setFlag(selectable
)
805 self
.setCursor(cursor
)
806 self
.setToolTip(commit
.sha1
[:7] + ': ' + commit
.summary
)
809 self
.label
= label
= Label(commit
)
810 label
.setParentItem(self
)
811 label
.setPos(xpos
, -self
.commit_radius
/2.0)
815 if len(commit
.parents
) > 1:
816 self
.brush
= cached_merge_color
818 self
.brush
= cached_commit_color
823 def blockSignals(self
, blocked
):
824 self
.notifier
.notification_enabled
= not blocked
826 def itemChange(self
, change
, value
):
827 if change
== QtGui
.QGraphicsItem
.ItemSelectedHasChanged
:
828 # Broadcast selection to other widgets
829 selected_items
= self
.scene().selectedItems()
830 commits
= [item
.commit
for item
in selected_items
]
831 self
.scene().parent().set_selecting(True)
832 self
.notifier
.notify_observers(COMMITS_SELECTED
, commits
)
833 self
.scene().parent().set_selecting(False)
835 # Cache the pen for use in paint()
836 if value
.toPyObject():
837 self
.brush
= self
.commit_selected_color
838 color
= self
.selected_outline_color
840 if len(self
.commit
.parents
) > 1:
841 self
.brush
= self
.merge_color
843 self
.brush
= self
.commit_color
844 color
= self
.outline_color
845 commit_pen
= QtGui
.QPen()
846 commit_pen
.setWidth(1.0)
847 commit_pen
.setColor(color
)
848 self
.commit_pen
= commit_pen
850 return QtGui
.QGraphicsItem
.itemChange(self
, change
, value
)
853 return self
.item_type
855 def boundingRect(self
, rect
=item_bbox
):
859 return self
.item_shape
861 def paint(self
, painter
, option
, widget
,
865 # Do not draw outside the exposed rect
866 painter
.setClipRect(option
.exposedRect
)
869 painter
.setPen(self
.commit_pen
)
870 painter
.setBrush(self
.brush
)
871 painter
.drawEllipse(inner
)
874 def mousePressEvent(self
, event
):
875 QtGui
.QGraphicsItem
.mousePressEvent(self
, event
)
877 self
.selected
= self
.isSelected()
879 def mouseMoveEvent(self
, event
):
882 QtGui
.QGraphicsItem
.mouseMoveEvent(self
, event
)
884 def mouseReleaseEvent(self
, event
):
885 QtGui
.QGraphicsItem
.mouseReleaseEvent(self
, event
)
886 if (not self
.dragged
and
888 event
.button() == Qt
.LeftButton
):
894 class Label(QtGui
.QGraphicsItem
):
895 item_type
= QtGui
.QGraphicsItem
.UserType
+ 3
900 item_shape
= QtGui
.QPainterPath()
901 item_shape
.addRect(0, 0, width
, height
)
902 item_bbox
= item_shape
.boundingRect()
904 text_options
= QtGui
.QTextOption()
905 text_options
.setAlignment(Qt
.AlignCenter
)
906 text_options
.setAlignment(Qt
.AlignVCenter
)
908 def __init__(self
, commit
,
909 other_color
=QtGui
.QColor(Qt
.white
),
910 head_color
=QtGui
.QColor(Qt
.green
)):
911 QtGui
.QGraphicsItem
.__init
__(self
)
914 # Starts with enough space for two tags. Any more and the commit
915 # needs to be taller to accomodate.
918 if 'HEAD' in commit
.tags
:
919 self
.color
= head_color
921 self
.color
= other_color
923 self
.color
.setAlpha(180)
924 self
.pen
= QtGui
.QPen()
925 self
.pen
.setColor(self
.color
.darker())
926 self
.pen
.setWidth(1.0)
929 return self
.item_type
931 def boundingRect(self
, rect
=item_bbox
):
935 return self
.item_shape
937 def paint(self
, painter
, option
, widget
,
938 text_opts
=text_options
,
942 font
= cache
.label_font
943 except AttributeError:
944 font
= cache
.label_font
= QtGui
.QApplication
.font()
949 painter
.setBrush(self
.color
)
950 painter
.setPen(self
.pen
)
951 painter
.setFont(font
)
955 for tag
in self
.commit
.tags
:
956 text_rect
= painter
.boundingRect(
957 QRectF(current_width
, 0, 0, 0), Qt
.TextSingleLine
, tag
)
958 box_rect
= text_rect
.adjusted(-1, -1, 1, 1)
959 painter
.drawRoundedRect(box_rect
, 2, 2)
960 painter
.drawText(text_rect
, Qt
.TextSingleLine
, tag
)
961 current_width
+= text_rect
.width() + 5
964 class GraphView(ViewerMixin
, QtGui
.QGraphicsView
):
969 x_adjust
= Commit
.commit_radius
*4/3
970 y_adjust
= Commit
.commit_radius
*4/3
975 def __init__(self
, notifier
, parent
):
976 QtGui
.QGraphicsView
.__init
__(self
, parent
)
977 ViewerMixin
.__init
__(self
)
979 highlight
= self
.palette().color(QtGui
.QPalette
.Highlight
)
980 Commit
.commit_selected_color
= highlight
981 Commit
.selected_outline_color
= highlight
.darker()
983 self
.selection_list
= []
984 self
.notifier
= notifier
987 self
.saved_matrix
= QtGui
.QMatrix(self
.matrix())
989 self
.x_offsets
= collections
.defaultdict(int)
991 self
.is_panning
= False
993 self
.selecting
= False
994 self
.last_mouse
= [0, 0]
996 self
.setDragMode(self
.RubberBandDrag
)
998 scene
= QtGui
.QGraphicsScene(self
)
999 scene
.setItemIndexMethod(QtGui
.QGraphicsScene
.NoIndex
)
1000 self
.setScene(scene
)
1002 self
.setRenderHint(QtGui
.QPainter
.Antialiasing
)
1003 self
.setViewportUpdateMode(self
.BoundingRectViewportUpdate
)
1004 self
.setCacheMode(QtGui
.QGraphicsView
.CacheBackground
)
1005 self
.setTransformationAnchor(QtGui
.QGraphicsView
.AnchorUnderMouse
)
1006 self
.setResizeAnchor(QtGui
.QGraphicsView
.NoAnchor
)
1007 self
.setBackgroundBrush(QtGui
.QColor(Qt
.white
))
1009 qtutils
.add_action(self
, N_('Zoom In'),
1010 self
.zoom_in
, Qt
.Key_Plus
, Qt
.Key_Equal
)
1012 qtutils
.add_action(self
, N_('Zoom Out'),
1013 self
.zoom_out
, Qt
.Key_Minus
)
1015 qtutils
.add_action(self
, N_('Zoom to Fit'),
1016 self
.zoom_to_fit
, Qt
.Key_F
)
1018 qtutils
.add_action(self
, N_('Select Parent'),
1019 self
.select_parent
, 'Shift+J')
1021 qtutils
.add_action(self
, N_('Select Oldest Parent'),
1022 self
.select_oldest_parent
, Qt
.Key_J
)
1024 qtutils
.add_action(self
, N_('Select Child'),
1025 self
.select_child
, 'Shift+K')
1027 qtutils
.add_action(self
, N_('Select Newest Child'),
1028 self
.select_newest_child
, Qt
.Key_K
)
1030 notifier
.add_observer(COMMITS_SELECTED
, self
.commits_selected
)
1033 self
.scene().clear()
1034 self
.selection_list
= []
1036 self
.x_offsets
.clear()
1041 # ViewerMixin interface
1042 def selected_items(self
):
1043 """Return the currently selected items"""
1044 return self
.scene().selectedItems()
1047 self
.scale_view(1.5)
1050 self
.scale_view(1.0/1.5)
1052 def commits_selected(self
, commits
):
1055 self
.select([commit
.sha1
for commit
in commits
])
1057 def select(self
, sha1s
):
1058 """Select the item for the SHA-1"""
1059 self
.scene().clearSelection()
1062 item
= self
.items
[sha1
]
1065 item
.blockSignals(True)
1066 item
.setSelected(True)
1067 item
.blockSignals(False)
1068 item_rect
= item
.sceneTransform().mapRect(item
.boundingRect())
1069 self
.ensureVisible(item_rect
)
1071 def get_item_by_generation(self
, commits
, criteria_fn
):
1072 """Return the item for the commit matching criteria"""
1076 for commit
in commits
:
1077 if (generation
is None or
1078 criteria_fn(generation
, commit
.generation
)):
1080 generation
= commit
.generation
1082 return self
.items
[sha1
]
1086 def oldest_item(self
, commits
):
1087 """Return the item for the commit with the oldest generation number"""
1088 return self
.get_item_by_generation(commits
, lambda a
, b
: a
> b
)
1090 def newest_item(self
, commits
):
1091 """Return the item for the commit with the newest generation number"""
1092 return self
.get_item_by_generation(commits
, lambda a
, b
: a
< b
)
1094 def create_patch(self
):
1095 items
= self
.selected_items()
1098 selected_commits
= self
.sort_by_generation([n
.commit
for n
in items
])
1099 sha1s
= [c
.sha1
for c
in selected_commits
]
1100 all_sha1s
= [c
.sha1
for c
in self
.commits
]
1101 cmds
.do(cmds
.FormatPatch
, sha1s
, all_sha1s
)
1103 def select_parent(self
):
1104 """Select the parent with the newest generation number"""
1105 selected_item
= self
.selected_item()
1106 if selected_item
is None:
1108 parent_item
= self
.newest_item(selected_item
.commit
.parents
)
1109 if parent_item
is None:
1111 selected_item
.setSelected(False)
1112 parent_item
.setSelected(True)
1114 parent_item
.mapRectToScene(parent_item
.boundingRect()))
1116 def select_oldest_parent(self
):
1117 """Select the parent with the oldest generation number"""
1118 selected_item
= self
.selected_item()
1119 if selected_item
is None:
1121 parent_item
= self
.oldest_item(selected_item
.commit
.parents
)
1122 if parent_item
is None:
1124 selected_item
.setSelected(False)
1125 parent_item
.setSelected(True)
1126 scene_rect
= parent_item
.mapRectToScene(parent_item
.boundingRect())
1127 self
.ensureVisible(scene_rect
)
1129 def select_child(self
):
1130 """Select the child with the oldest generation number"""
1131 selected_item
= self
.selected_item()
1132 if selected_item
is None:
1134 child_item
= self
.oldest_item(selected_item
.commit
.children
)
1135 if child_item
is None:
1137 selected_item
.setSelected(False)
1138 child_item
.setSelected(True)
1139 scene_rect
= child_item
.mapRectToScene(child_item
.boundingRect())
1140 self
.ensureVisible(scene_rect
)
1142 def select_newest_child(self
):
1143 """Select the Nth child with the newest generation number (N > 1)"""
1144 selected_item
= self
.selected_item()
1145 if selected_item
is None:
1147 if len(selected_item
.commit
.children
) > 1:
1148 children
= selected_item
.commit
.children
[1:]
1150 children
= selected_item
.commit
.children
1151 child_item
= self
.newest_item(children
)
1152 if child_item
is None:
1154 selected_item
.setSelected(False)
1155 child_item
.setSelected(True)
1156 scene_rect
= child_item
.mapRectToScene(child_item
.boundingRect())
1157 self
.ensureVisible(scene_rect
)
1159 def set_initial_view(self
):
1160 self_commits
= self
.commits
1161 self_items
= self
.items
1163 commits
= self_commits
[-2:]
1164 items
= [self_items
[c
.sha1
] for c
in commits
]
1165 self
.fit_view_to_items(items
)
1167 def zoom_to_fit(self
):
1168 """Fit selected items into the viewport"""
1170 items
= self
.selected_items()
1171 self
.fit_view_to_items(items
)
1173 def fit_view_to_items(self
, items
):
1175 rect
= self
.scene().itemsBoundingRect()
1183 item_rect
= item
.boundingRect()
1184 x_off
= item_rect
.width()
1185 y_off
= item_rect
.height()
1186 x_min
= min(x_min
, pos
.x())
1187 y_min
= min(y_min
, pos
.y()-y_off
)
1188 x_max
= max(x_max
, pos
.x()+x_off
)
1189 ymax
= max(ymax
, pos
.y())
1190 rect
= QtCore
.QRectF(x_min
, y_min
, x_max
-x_min
, ymax
-y_min
)
1191 x_adjust
= GraphView
.x_adjust
1192 y_adjust
= GraphView
.y_adjust
1193 rect
.setX(rect
.x() - x_adjust
)
1194 rect
.setY(rect
.y() - y_adjust
)
1195 rect
.setHeight(rect
.height() + y_adjust
*2)
1196 rect
.setWidth(rect
.width() + x_adjust
*2)
1197 self
.fitInView(rect
, Qt
.KeepAspectRatio
)
1198 self
.scene().invalidate()
1200 def save_selection(self
, event
):
1201 if event
.button() != Qt
.LeftButton
:
1203 elif Qt
.ShiftModifier
!= event
.modifiers():
1205 self
.selection_list
= self
.selected_items()
1207 def restore_selection(self
, event
):
1208 if Qt
.ShiftModifier
!= event
.modifiers():
1210 for item
in self
.selection_list
:
1211 item
.setSelected(True)
1213 def handle_event(self
, event_handler
, event
):
1215 self
.save_selection(event
)
1216 event_handler(self
, event
)
1217 self
.restore_selection(event
)
1219 def set_selecting(self
, selecting
):
1220 self
.selecting
= selecting
1222 def pan(self
, event
):
1224 dx
= pos
.x() - self
.mouse_start
[0]
1225 dy
= pos
.y() - self
.mouse_start
[1]
1227 if dx
== 0 and dy
== 0:
1230 rect
= QtCore
.QRect(0, 0, abs(dx
), abs(dy
))
1231 delta
= self
.mapToScene(rect
).boundingRect()
1241 matrix
= QtGui
.QMatrix(self
.saved_matrix
).translate(tx
, ty
)
1242 self
.setTransformationAnchor(QtGui
.QGraphicsView
.NoAnchor
)
1243 self
.setMatrix(matrix
)
1245 def wheel_zoom(self
, event
):
1246 """Handle mouse wheel zooming."""
1247 zoom
= math
.pow(2.0, event
.delta()/512.0)
1248 factor
= (self
.matrix()
1250 .mapRect(QtCore
.QRectF(0.0, 0.0, 1.0, 1.0))
1252 if factor
< 0.014 or factor
> 42.0:
1254 self
.setTransformationAnchor(QtGui
.QGraphicsView
.AnchorUnderMouse
)
1256 self
.scale(zoom
, zoom
)
1258 def wheel_pan(self
, event
):
1259 """Handle mouse wheel panning."""
1261 if event
.delta() < 0:
1265 pan_rect
= QtCore
.QRectF(0.0, 0.0, 1.0, 1.0)
1266 factor
= 1.0/self
.matrix().mapRect(pan_rect
).width()
1268 if event
.orientation() == Qt
.Vertical
:
1269 matrix
= self
.matrix().translate(0, s
*factor
)
1271 matrix
= self
.matrix().translate(s
*factor
, 0)
1272 self
.setTransformationAnchor(QtGui
.QGraphicsView
.NoAnchor
)
1273 self
.setMatrix(matrix
)
1275 def scale_view(self
, scale
):
1276 factor
= (self
.matrix().scale(scale
, scale
)
1277 .mapRect(QtCore
.QRectF(0, 0, 1, 1))
1279 if factor
< 0.07 or factor
> 100.0:
1283 adjust_scrollbars
= True
1284 scrollbar
= self
.verticalScrollBar()
1286 value
= scrollbar
.value()
1287 min_
= scrollbar
.minimum()
1288 max_
= scrollbar
.maximum()
1289 range_
= max_
- min_
1290 distance
= value
- min_
1291 nonzero_range
= float(range_
) > 0.1
1293 scrolloffset
= distance
/float(range_
)
1295 adjust_scrollbars
= False
1297 self
.setTransformationAnchor(QtGui
.QGraphicsView
.NoAnchor
)
1298 self
.scale(scale
, scale
)
1300 scrollbar
= self
.verticalScrollBar()
1301 if scrollbar
and adjust_scrollbars
:
1302 min_
= scrollbar
.minimum()
1303 max_
= scrollbar
.maximum()
1304 range_
= max_
- min_
1305 value
= min_
+ int(float(range_
) * scrolloffset
)
1306 scrollbar
.setValue(value
)
1308 def add_commits(self
, commits
):
1309 """Traverse commits and add them to the view."""
1310 self
.commits
.extend(commits
)
1311 scene
= self
.scene()
1312 for commit
in commits
:
1313 item
= Commit(commit
, self
.notifier
)
1314 self
.items
[commit
.sha1
] = item
1315 for ref
in commit
.tags
:
1316 self
.items
[ref
] = item
1319 self
.layout_commits(commits
)
1322 def link(self
, commits
):
1323 """Create edges linking commits with their parents"""
1324 scene
= self
.scene()
1325 for commit
in commits
:
1327 commit_item
= self
.items
[commit
.sha1
]
1329 # TODO - Handle truncated history viewing
1331 for parent
in reversed(commit
.parents
):
1333 parent_item
= self
.items
[parent
.sha1
]
1335 # TODO - Handle truncated history viewing
1337 edge
= Edge(parent_item
, commit_item
)
1340 def layout_commits(self
, nodes
):
1341 positions
= self
.position_nodes(nodes
)
1342 for sha1
, (x
, y
) in positions
.items():
1343 item
= self
.items
[sha1
]
1346 def position_nodes(self
, nodes
):
1353 x_offsets
= self
.x_offsets
1356 generation
= node
.generation
1360 # This is a fan-out so sweep over child generations and
1361 # shift them to the right to avoid overlapping edges
1362 child_gens
= [c
.generation
for c
in node
.children
]
1363 maxgen
= max(child_gens
)
1364 for g
in xrange(generation
+ 1, maxgen
):
1365 x_offsets
[g
] += x_off
1367 if len(node
.parents
) == 1:
1368 # Align nodes relative to their parents
1369 parent_gen
= node
.parents
[0].generation
1370 parent_off
= x_offsets
[parent_gen
]
1371 x_offsets
[generation
] = max(parent_off
-x_off
,
1372 x_offsets
[generation
])
1374 cur_xoff
= x_offsets
[generation
]
1375 next_xoff
= cur_xoff
1377 x_offsets
[generation
] = next_xoff
1380 y_pos
= -generation
* y_off
1382 y_pos
= min(y_pos
, y_min
- y_off
)
1385 positions
[sha1
] = (x_pos
, y_pos
)
1387 x_max
= max(x_max
, x_pos
)
1395 def update_scene_rect(self
):
1398 self
.scene().setSceneRect(-GraphView
.x_adjust
,
1399 y_min
-GraphView
.y_adjust
,
1400 x_max
+ GraphView
.x_adjust
,
1401 abs(y_min
) + GraphView
.y_adjust
)
1403 def sort_by_generation(self
, commits
):
1404 if len(commits
) < 2:
1406 commits
.sort(cmp=lambda a
, b
: cmp(a
.generation
, b
.generation
))
1410 def contextMenuEvent(self
, event
):
1411 self
.context_menu_event(event
)
1413 def mousePressEvent(self
, event
):
1414 if event
.button() == Qt
.MidButton
:
1416 self
.mouse_start
= [pos
.x(), pos
.y()]
1417 self
.saved_matrix
= QtGui
.QMatrix(self
.matrix())
1418 self
.is_panning
= True
1420 if event
.button() == Qt
.RightButton
:
1423 if event
.button() == Qt
.LeftButton
:
1425 self
.handle_event(QtGui
.QGraphicsView
.mousePressEvent
, event
)
1427 def mouseMoveEvent(self
, event
):
1428 pos
= self
.mapToScene(event
.pos())
1432 self
.last_mouse
[0] = pos
.x()
1433 self
.last_mouse
[1] = pos
.y()
1434 self
.handle_event(QtGui
.QGraphicsView
.mouseMoveEvent
, event
)
1436 def mouseReleaseEvent(self
, event
):
1437 self
.pressed
= False
1438 if event
.button() == Qt
.MidButton
:
1439 self
.is_panning
= False
1441 self
.handle_event(QtGui
.QGraphicsView
.mouseReleaseEvent
, event
)
1442 self
.selection_list
= []
1444 def wheelEvent(self
, event
):
1445 """Handle Qt mouse wheel events."""
1446 if event
.modifiers() == Qt
.ControlModifier
:
1447 self
.wheel_zoom(event
)
1449 self
.wheel_pan(event
)