core: add list2cmdline() wrapper
[git-cola.git] / cola / widgets / dag.py
blobb35a0134dde0b72843529c1e10abc7e1a3d7392b
1 from __future__ import division, absolute_import, unicode_literals
3 import collections
4 import math
6 from PyQt4 import QtGui
7 from PyQt4 import QtCore
8 from PyQt4.QtCore import Qt
9 from PyQt4.QtCore import SIGNAL
10 from PyQt4.QtCore import QPointF
11 from PyQt4.QtCore import QRectF
13 from cola import core
14 from cola import cmds
15 from cola import difftool
16 from cola import hotkeys
17 from cola import icons
18 from cola import observable
19 from cola import qtutils
20 from cola.i18n import N_
21 from cola.models import dag
22 from cola.widgets import archive
23 from cola.widgets import browse
24 from cola.widgets import completion
25 from cola.widgets import createbranch
26 from cola.widgets import createtag
27 from cola.widgets import defs
28 from cola.widgets import diff
29 from cola.widgets import filelist
30 from cola.widgets import standard
31 from cola.compat import ustr
34 def git_dag(model, args=None, settings=None):
35 """Return a pre-populated git DAG widget."""
36 branch = model.currentbranch
37 # disambiguate between branch names and filenames by using '--'
38 branch_doubledash = branch and (branch + ' --') or ''
39 ctx = dag.DAG(branch_doubledash, 1000)
40 ctx.set_arguments(args)
42 view = GitDAG(model, ctx, settings=settings)
43 if ctx.ref:
44 view.display()
45 return view
48 class ViewerMixin(object):
49 """Implementations must provide selected_items()"""
51 def __init__(self):
52 self.selected = None
53 self.clicked = None
54 self.menu_actions = self.context_menu_actions()
56 def selected_item(self):
57 """Return the currently selected item"""
58 selected_items = self.selected_items()
59 if not selected_items:
60 return None
61 return selected_items[0]
63 def selected_sha1(self):
64 item = self.selected_item()
65 if item is None:
66 return None
67 return item.commit.sha1
69 def selected_sha1s(self):
70 return [i.commit for i in self.selected_items()]
72 def diff_selected_this(self):
73 clicked_sha1 = self.clicked.sha1
74 selected_sha1 = self.selected.sha1
75 self.emit(SIGNAL('diff_commits(PyQt_PyObject,PyQt_PyObject)'),
76 selected_sha1, clicked_sha1)
78 def diff_this_selected(self):
79 clicked_sha1 = self.clicked.sha1
80 selected_sha1 = self.selected.sha1
81 self.emit(SIGNAL('diff_commits(PyQt_PyObject,PyQt_PyObject)'),
82 clicked_sha1, selected_sha1)
84 def cherry_pick(self):
85 sha1 = self.selected_sha1()
86 if sha1 is None:
87 return
88 cmds.do(cmds.CherryPick, [sha1])
90 def copy_to_clipboard(self):
91 sha1 = self.selected_sha1()
92 if sha1 is None:
93 return
94 qtutils.set_clipboard(sha1)
96 def create_branch(self):
97 sha1 = self.selected_sha1()
98 if sha1 is None:
99 return
100 createbranch.create_new_branch(revision=sha1)
102 def create_tag(self):
103 sha1 = self.selected_sha1()
104 if sha1 is None:
105 return
106 createtag.create_tag(ref=sha1)
108 def create_tarball(self):
109 sha1 = self.selected_sha1()
110 if sha1 is None:
111 return
112 short_sha1 = sha1[:7]
113 archive.GitArchiveDialog.save_hashed_objects(sha1, short_sha1, self)
115 def save_blob_dialog(self):
116 sha1 = self.selected_sha1()
117 if sha1 is None:
118 return
119 return browse.BrowseDialog.browse(sha1)
121 def context_menu_actions(self):
122 return {
123 'diff_this_selected':
124 qtutils.add_action(self, N_('Diff this -> selected'),
125 self.diff_this_selected),
126 'diff_selected_this':
127 qtutils.add_action(self, N_('Diff selected -> this'),
128 self.diff_selected_this),
129 'create_branch':
130 qtutils.add_action(self, N_('Create Branch'),
131 self.create_branch),
132 'create_patch':
133 qtutils.add_action(self, N_('Create Patch'),
134 self.create_patch),
135 'create_tag':
136 qtutils.add_action(self, N_('Create Tag'),
137 self.create_tag),
138 'create_tarball':
139 qtutils.add_action(self, N_('Save As Tarball/Zip...'),
140 self.create_tarball),
141 'cherry_pick':
142 qtutils.add_action(self, N_('Cherry Pick'),
143 self.cherry_pick),
144 'save_blob':
145 qtutils.add_action(self, N_('Grab File...'),
146 self.save_blob_dialog),
147 'copy':
148 qtutils.add_action(self, N_('Copy SHA-1'),
149 self.copy_to_clipboard,
150 QtGui.QKeySequence.Copy),
153 def update_menu_actions(self, event):
154 selected_items = self.selected_items()
155 item = self.itemAt(event.pos())
156 if item is None:
157 self.clicked = commit = None
158 else:
159 self.clicked = commit = item.commit
161 has_single_selection = len(selected_items) == 1
162 has_selection = bool(selected_items)
163 can_diff = bool(commit and has_single_selection and
164 commit is not selected_items[0].commit)
166 if can_diff:
167 self.selected = selected_items[0].commit
168 else:
169 self.selected = None
171 self.menu_actions['diff_this_selected'].setEnabled(can_diff)
172 self.menu_actions['diff_selected_this'].setEnabled(can_diff)
174 self.menu_actions['create_branch'].setEnabled(has_single_selection)
175 self.menu_actions['create_tag'].setEnabled(has_single_selection)
177 self.menu_actions['cherry_pick'].setEnabled(has_single_selection)
178 self.menu_actions['create_patch'].setEnabled(has_selection)
179 self.menu_actions['create_tarball'].setEnabled(has_single_selection)
181 self.menu_actions['save_blob'].setEnabled(has_single_selection)
182 self.menu_actions['copy'].setEnabled(has_single_selection)
184 def context_menu_event(self, event):
185 self.update_menu_actions(event)
186 menu = QtGui.QMenu(self)
187 menu.addAction(self.menu_actions['diff_this_selected'])
188 menu.addAction(self.menu_actions['diff_selected_this'])
189 menu.addSeparator()
190 menu.addAction(self.menu_actions['create_branch'])
191 menu.addAction(self.menu_actions['create_tag'])
192 menu.addSeparator()
193 menu.addAction(self.menu_actions['cherry_pick'])
194 menu.addAction(self.menu_actions['create_patch'])
195 menu.addAction(self.menu_actions['create_tarball'])
196 menu.addSeparator()
197 menu.addAction(self.menu_actions['save_blob'])
198 menu.addAction(self.menu_actions['copy'])
199 menu.exec_(self.mapToGlobal(event.pos()))
202 class CommitTreeWidgetItem(QtGui.QTreeWidgetItem):
204 def __init__(self, commit, parent=None):
205 QtGui.QTreeWidgetItem.__init__(self, parent)
206 self.commit = commit
207 self.setText(0, commit.summary)
208 self.setText(1, commit.author)
209 self.setText(2, commit.authdate)
212 class CommitTreeWidget(ViewerMixin, standard.TreeWidget):
214 def __init__(self, notifier, parent):
215 standard.TreeWidget.__init__(self, parent)
216 ViewerMixin.__init__(self)
218 self.setSelectionMode(self.ExtendedSelection)
219 self.setHeaderLabels([N_('Summary'), N_('Author'), N_('Date, Time')])
221 self.sha1map = {}
222 self.notifier = notifier
223 self.selecting = False
224 self.commits = []
226 self.action_up = qtutils.add_action(self, N_('Go Up'),
227 self.go_up, hotkeys.MOVE_UP)
229 self.action_down = qtutils.add_action(self, N_('Go Down'),
230 self.go_down, hotkeys.MOVE_DOWN)
232 notifier.add_observer(diff.COMMITS_SELECTED, self.commits_selected)
234 self.connect(self, SIGNAL('itemSelectionChanged()'),
235 self.selection_changed)
237 # ViewerMixin
238 def go_up(self):
239 self.goto(self.itemAbove)
241 def go_down(self):
242 self.goto(self.itemBelow)
244 def goto(self, finder):
245 items = self.selected_items()
246 item = items and items[0] or None
247 if item is None:
248 return
249 found = finder(item)
250 if found:
251 self.select([found.commit.sha1])
253 def selected_commit_range(self):
254 selected_items = self.selected_items()
255 if not selected_items:
256 return None, None
257 return selected_items[-1].commit.sha1, selected_items[0].commit.sha1
259 def set_selecting(self, selecting):
260 self.selecting = selecting
262 def selection_changed(self):
263 items = self.selected_items()
264 if not items:
265 return
266 self.set_selecting(True)
267 self.notifier.notify_observers(diff.COMMITS_SELECTED,
268 [i.commit for i in items])
269 self.set_selecting(False)
271 def commits_selected(self, commits):
272 if self.selecting:
273 return
274 with qtutils.BlockSignals(self):
275 self.select([commit.sha1 for commit in commits])
277 def select(self, sha1s):
278 if not sha1s:
279 return
280 self.clearSelection()
281 for idx, sha1 in enumerate(sha1s):
282 try:
283 item = self.sha1map[sha1]
284 except KeyError:
285 continue
286 self.scrollToItem(item)
287 item.setSelected(True)
289 def adjust_columns(self):
290 width = self.width()-20
291 zero = width*2//3
292 onetwo = width//6
293 self.setColumnWidth(0, zero)
294 self.setColumnWidth(1, onetwo)
295 self.setColumnWidth(2, onetwo)
297 def clear(self):
298 QtGui.QTreeWidget.clear(self)
299 self.sha1map.clear()
300 self.commits = []
302 def add_commits(self, commits):
303 self.commits.extend(commits)
304 items = []
305 for c in reversed(commits):
306 item = CommitTreeWidgetItem(c)
307 items.append(item)
308 self.sha1map[c.sha1] = item
309 for tag in c.tags:
310 self.sha1map[tag] = item
311 self.insertTopLevelItems(0, items)
313 def create_patch(self):
314 items = self.selectedItems()
315 if not items:
316 return
317 sha1s = [item.commit.sha1 for item in reversed(items)]
318 all_sha1s = [c.sha1 for c in self.commits]
319 cmds.do(cmds.FormatPatch, sha1s, all_sha1s)
321 # Qt overrides
322 def contextMenuEvent(self, event):
323 self.context_menu_event(event)
325 def mousePressEvent(self, event):
326 if event.button() == Qt.RightButton:
327 event.accept()
328 return
329 QtGui.QTreeWidget.mousePressEvent(self, event)
332 class GitDAG(standard.MainWindow):
333 """The git-dag widget."""
335 def __init__(self, model, ctx, parent=None, settings=None):
336 standard.MainWindow.__init__(self, parent)
338 self.setAttribute(Qt.WA_MacMetalStyle)
339 self.setMinimumSize(420, 420)
341 # change when widgets are added/removed
342 self.widget_version = 2
343 self.model = model
344 self.ctx = ctx
345 self.settings = settings
347 self.commits = {}
348 self.commit_list = []
349 self.selection = []
351 self.thread = ReaderThread(ctx, self)
352 self.revtext = completion.GitLogLineEdit()
353 self.maxresults = standard.SpinBox()
355 self.zoom_out = qtutils.create_action_button(
356 tooltip=N_('Zoom Out'), icon=icons.zoom_out())
358 self.zoom_in = qtutils.create_action_button(
359 tooltip=N_('Zoom In'), icon=icons.zoom_in())
361 self.zoom_to_fit = qtutils.create_action_button(
362 tooltip=N_('Zoom to Fit'), icon=icons.zoom_fit_best())
364 self.notifier = notifier = observable.Observable()
365 self.notifier.refs_updated = refs_updated = 'refs_updated'
366 self.notifier.add_observer(refs_updated, self.display)
367 self.notifier.add_observer(filelist.HISTORIES_SELECTED,
368 self.histories_selected)
369 self.notifier.add_observer(filelist.DIFFTOOL_SELECTED,
370 self.difftool_selected)
371 self.notifier.add_observer(diff.COMMITS_SELECTED, self.commits_selected)
373 self.treewidget = CommitTreeWidget(notifier, self)
374 self.diffwidget = diff.DiffWidget(notifier, self)
375 self.filewidget = filelist.FileWidget(notifier, self)
376 self.graphview = GraphView(notifier, self)
378 self.controls_layout = qtutils.hbox(defs.no_margin, defs.spacing,
379 self.revtext, self.maxresults)
381 self.controls_widget = QtGui.QWidget()
382 self.controls_widget.setLayout(self.controls_layout)
384 self.log_dock = qtutils.create_dock(N_('Log'), self, stretch=False)
385 self.log_dock.setWidget(self.treewidget)
386 log_dock_titlebar = self.log_dock.titleBarWidget()
387 log_dock_titlebar.add_corner_widget(self.controls_widget)
389 self.file_dock = qtutils.create_dock(N_('Files'), self)
390 self.file_dock.setWidget(self.filewidget)
392 self.diff_dock = qtutils.create_dock(N_('Diff'), self)
393 self.diff_dock.setWidget(self.diffwidget)
395 self.graph_controls_layout = qtutils.hbox(
396 defs.no_margin, defs.button_spacing,
397 self.zoom_out, self.zoom_in, self.zoom_to_fit,
398 defs.spacing)
400 self.graph_controls_widget = QtGui.QWidget()
401 self.graph_controls_widget.setLayout(self.graph_controls_layout)
403 self.graphview_dock = qtutils.create_dock(N_('Graph'), self)
404 self.graphview_dock.setWidget(self.graphview)
405 graph_titlebar = self.graphview_dock.titleBarWidget()
406 graph_titlebar.add_corner_widget(self.graph_controls_widget)
408 self.lock_layout_action = qtutils.add_action_bool(self,
409 N_('Lock Layout'), self.set_lock_layout, False)
411 self.refresh_action = qtutils.add_action(self,
412 N_('Refresh'), self.refresh, hotkeys.REFRESH)
414 # Create the application menu
415 self.menubar = QtGui.QMenuBar(self)
417 # View Menu
418 self.view_menu = qtutils.create_menu(N_('View'), self.menubar)
419 self.view_menu.addAction(self.refresh_action)
421 self.view_menu.addAction(self.log_dock.toggleViewAction())
422 self.view_menu.addAction(self.graphview_dock.toggleViewAction())
423 self.view_menu.addAction(self.diff_dock.toggleViewAction())
424 self.view_menu.addAction(self.file_dock.toggleViewAction())
425 self.view_menu.addSeparator()
426 self.view_menu.addAction(self.lock_layout_action)
428 self.menubar.addAction(self.view_menu.menuAction())
429 self.setMenuBar(self.menubar)
431 left = Qt.LeftDockWidgetArea
432 right = Qt.RightDockWidgetArea
433 self.addDockWidget(left, self.log_dock)
434 self.addDockWidget(left, self.diff_dock)
435 self.addDockWidget(right, self.graphview_dock)
436 self.addDockWidget(right, self.file_dock)
438 # Update fields affected by model
439 self.revtext.setText(ctx.ref)
440 self.maxresults.setValue(ctx.count)
441 self.update_window_title()
443 # Also re-loads dag.* from the saved state
444 if not self.restore_state(settings=settings):
445 self.resize_to_desktop()
447 qtutils.connect_button(self.zoom_out, self.graphview.zoom_out)
448 qtutils.connect_button(self.zoom_in, self.graphview.zoom_in)
449 qtutils.connect_button(self.zoom_to_fit,
450 self.graphview.zoom_to_fit)
452 self.thread.connect(self.thread, self.thread.begin, self.thread_begin,
453 Qt.QueuedConnection)
454 self.thread.connect(self.thread, self.thread.status, self.thread_status,
455 Qt.QueuedConnection)
456 self.thread.connect(self.thread, self.thread.add, self.add_commits,
457 Qt.QueuedConnection)
458 self.thread.connect(self.thread, self.thread.end, self.thread_end,
459 Qt.QueuedConnection)
461 self.connect(self.treewidget,
462 SIGNAL('diff_commits(PyQt_PyObject,PyQt_PyObject)'),
463 self.diff_commits)
465 self.connect(self.graphview,
466 SIGNAL('diff_commits(PyQt_PyObject,PyQt_PyObject)'),
467 self.diff_commits)
469 self.connect(self.maxresults, SIGNAL('editingFinished()'),
470 self.display)
472 self.connect(self.revtext, SIGNAL('textChanged(QString)'),
473 self.text_changed)
475 self.connect(self.revtext, SIGNAL('activated()'), self.display)
476 self.connect(self.revtext, SIGNAL('return()'), self.display)
477 self.connect(self.revtext, SIGNAL('down()'), self.focus_tree)
479 # The model is updated in another thread so use
480 # signals/slots to bring control back to the main GUI thread
481 self.model.add_observer(self.model.message_updated,
482 self.emit_model_updated)
484 self.connect(self, SIGNAL('model_updated()'), self.model_updated,
485 Qt.QueuedConnection)
487 qtutils.add_action(self, 'Focus Input', self.focus_input, hotkeys.FOCUS)
488 qtutils.add_close_action(self)
490 def focus_input(self):
491 self.revtext.setFocus()
493 def focus_tree(self):
494 self.treewidget.setFocus()
496 def text_changed(self, txt):
497 self.ctx.ref = ustr(txt)
498 self.update_window_title()
500 def update_window_title(self):
501 project = self.model.project
502 if self.ctx.ref:
503 self.setWindowTitle(N_('%(project)s: %(ref)s - DAG')
504 % dict(project=project, ref=self.ctx.ref))
505 else:
506 self.setWindowTitle(project + N_(' - DAG'))
508 def export_state(self):
509 state = standard.MainWindow.export_state(self)
510 state['count'] = self.ctx.count
511 return state
513 def apply_state(self, state):
514 result = standard.MainWindow.apply_state(self, state)
515 try:
516 count = state['count']
517 if self.ctx.overridden('count'):
518 count = self.ctx.count
519 except:
520 count = self.ctx.count
521 result = False
522 self.ctx.set_count(count)
523 self.lock_layout_action.setChecked(state.get('lock_layout', False))
524 return result
526 def emit_model_updated(self):
527 self.emit(SIGNAL('model_updated()'))
529 def model_updated(self):
530 self.display()
532 def refresh(self):
533 cmds.do(cmds.Refresh)
535 def display(self):
536 new_ref = self.revtext.value()
537 new_count = self.maxresults.value()
539 self.thread.stop()
540 self.ctx.set_ref(new_ref)
541 self.ctx.set_count(new_count)
542 self.thread.start()
544 def show(self):
545 standard.MainWindow.show(self)
546 self.treewidget.adjust_columns()
548 def commits_selected(self, commits):
549 if commits:
550 self.selection = commits
552 def clear(self):
553 self.commits.clear()
554 self.commit_list = []
555 self.graphview.clear()
556 self.treewidget.clear()
558 def add_commits(self, commits):
559 self.commit_list.extend(commits)
560 # Keep track of commits
561 for commit_obj in commits:
562 self.commits[commit_obj.sha1] = commit_obj
563 for tag in commit_obj.tags:
564 self.commits[tag] = commit_obj
565 self.graphview.add_commits(commits)
566 self.treewidget.add_commits(commits)
568 def thread_begin(self):
569 self.clear()
571 def thread_end(self):
572 self.focus_tree()
573 self.restore_selection()
575 def thread_status(self, successful):
576 self.revtext.hint.set_error(not successful)
578 def restore_selection(self):
579 selection = self.selection
580 try:
581 commit_obj = self.commit_list[-1]
582 except IndexError:
583 # No commits, exist, early-out
584 return
586 new_commits = [self.commits.get(s.sha1, None) for s in selection]
587 new_commits = [c for c in new_commits if c is not None]
588 if new_commits:
589 # The old selection exists in the new state
590 self.notifier.notify_observers(diff.COMMITS_SELECTED, new_commits)
591 else:
592 # The old selection is now empty. Select the top-most commit
593 self.notifier.notify_observers(diff.COMMITS_SELECTED, [commit_obj])
595 self.graphview.update_scene_rect()
596 self.graphview.set_initial_view()
598 def resize_to_desktop(self):
599 desktop = QtGui.QApplication.instance().desktop()
600 width = desktop.width()
601 height = desktop.height()
602 self.resize(width, height)
604 def diff_commits(self, a, b):
605 paths = self.ctx.paths()
606 if paths:
607 difftool.launch(left=a, right=b, paths=paths)
608 else:
609 difftool.diff_commits(self, a, b)
611 # Qt overrides
612 def closeEvent(self, event):
613 self.revtext.close_popup()
614 self.thread.stop()
615 standard.MainWindow.closeEvent(self, event)
617 def resizeEvent(self, e):
618 standard.MainWindow.resizeEvent(self, e)
619 self.treewidget.adjust_columns()
621 def histories_selected(self, histories):
622 argv = [self.model.currentbranch, '--']
623 argv.extend(histories)
624 text = core.list2cmdline(argv)
625 self.revtext.setText(text)
626 self.display()
628 def difftool_selected(self, files):
629 bottom, top = self.treewidget.selected_commit_range()
630 if not top:
631 return
632 difftool.launch(left=bottom, left_take_parent=True,
633 right=top, paths=files)
636 class ReaderThread(QtCore.QThread):
637 begin = SIGNAL('begin')
638 add = SIGNAL('add')
639 end = SIGNAL('end')
640 status = SIGNAL('status')
642 def __init__(self, ctx, parent):
643 QtCore.QThread.__init__(self, parent)
644 self.ctx = ctx
645 self._abort = False
646 self._stop = False
647 self._mutex = QtCore.QMutex()
648 self._condition = QtCore.QWaitCondition()
650 def run(self):
651 repo = dag.RepoReader(self.ctx)
652 repo.reset()
653 self.emit(self.begin)
654 commits = []
655 for c in repo:
656 self._mutex.lock()
657 if self._stop:
658 self._condition.wait(self._mutex)
659 self._mutex.unlock()
660 if self._abort:
661 repo.reset()
662 return
663 commits.append(c)
664 if len(commits) >= 512:
665 self.emit(self.add, commits)
666 commits = []
668 self.emit(self.status, repo.returncode == 0)
669 if commits:
670 self.emit(self.add, commits)
671 self.emit(self.end)
673 def start(self):
674 self._abort = False
675 self._stop = False
676 QtCore.QThread.start(self)
678 def pause(self):
679 self._mutex.lock()
680 self._stop = True
681 self._mutex.unlock()
683 def resume(self):
684 self._mutex.lock()
685 self._stop = False
686 self._mutex.unlock()
687 self._condition.wakeOne()
689 def stop(self):
690 self._abort = True
691 self.wait()
694 class Cache(object):
695 pass
698 class Edge(QtGui.QGraphicsItem):
699 item_type = QtGui.QGraphicsItem.UserType + 1
701 def __init__(self, source, dest):
703 QtGui.QGraphicsItem.__init__(self)
705 self.setAcceptedMouseButtons(Qt.NoButton)
706 self.source = source
707 self.dest = dest
708 self.commit = source.commit
709 self.setZValue(-2)
711 dest_pt = Commit.item_bbox.center()
713 self.source_pt = self.mapFromItem(self.source, dest_pt)
714 self.dest_pt = self.mapFromItem(self.dest, dest_pt)
715 self.line = QtCore.QLineF(self.source_pt, self.dest_pt)
717 width = self.dest_pt.x() - self.source_pt.x()
718 height = self.dest_pt.y() - self.source_pt.y()
719 rect = QtCore.QRectF(self.source_pt, QtCore.QSizeF(width, height))
720 self.bound = rect.normalized()
722 # Choose a new color for new branch edges
723 if self.source.x() < self.dest.x():
724 color = EdgeColor.next()
725 line = Qt.SolidLine
726 elif self.source.x() != self.dest.x():
727 color = EdgeColor.current()
728 line = Qt.SolidLine
729 else:
730 color = EdgeColor.current()
731 line = Qt.SolidLine
733 self.pen = QtGui.QPen(color, 4.0, line, Qt.SquareCap, Qt.RoundJoin)
735 # Qt overrides
736 def type(self):
737 return self.item_type
739 def boundingRect(self):
740 return self.bound
742 def paint(self, painter, option, widget):
744 arc_rect = 10
745 connector_length = 5
747 painter.setPen(self.pen)
748 path = QtGui.QPainterPath()
750 if self.source.x() == self.dest.x():
751 path.moveTo(self.source.x(), self.source.y())
752 path.lineTo(self.dest.x(), self.dest.y())
753 painter.drawPath(path)
755 else:
757 #Define points starting from source
758 point1 = QPointF(self.source.x(), self.source.y())
759 point2 = QPointF(point1.x(), point1.y() - connector_length)
760 point3 = QPointF(point2.x() + arc_rect, point2.y() - arc_rect)
762 #Define points starting from dest
763 point4 = QPointF(self.dest.x(), self.dest.y())
764 point5 = QPointF(point4.x(),point3.y() - arc_rect)
765 point6 = QPointF(point5.x() - arc_rect, point5.y() + arc_rect)
767 start_angle_arc1 = 180
768 span_angle_arc1 = 90
769 start_angle_arc2 = 90
770 span_angle_arc2 = -90
772 # If the dest is at the left of the source, then we
773 # need to reverse some values
774 if self.source.x() > self.dest.x():
775 point5 = QPointF(point4.x(), point4.y() + connector_length)
776 point6 = QPointF(point5.x() + arc_rect, point5.y() + arc_rect)
777 point3 = QPointF(self.source.x() - arc_rect, point6.y())
778 point2 = QPointF(self.source.x(), point3.y() + arc_rect)
780 span_angle_arc1 = 90
782 path.moveTo(point1)
783 path.lineTo(point2)
784 path.arcTo(QRectF(point2, point3),
785 start_angle_arc1, span_angle_arc1)
786 path.lineTo(point6)
787 path.arcTo(QRectF(point6, point5),
788 start_angle_arc2, span_angle_arc2)
789 path.lineTo(point4)
790 painter.drawPath(path)
793 class EdgeColor(object):
794 """An edge color factory"""
796 current_color_index = 0
797 colors = [
798 QtGui.QColor(Qt.red),
799 QtGui.QColor(Qt.green),
800 QtGui.QColor(Qt.blue),
801 QtGui.QColor(Qt.black),
802 QtGui.QColor(Qt.darkRed),
803 QtGui.QColor(Qt.darkGreen),
804 QtGui.QColor(Qt.darkBlue),
805 QtGui.QColor(Qt.cyan),
806 QtGui.QColor(Qt.magenta),
807 # Orange; Qt.yellow is too low-contrast
808 qtutils.rgba(0xff, 0x66, 0x00),
809 QtGui.QColor(Qt.gray),
810 QtGui.QColor(Qt.darkCyan),
811 QtGui.QColor(Qt.darkMagenta),
812 QtGui.QColor(Qt.darkYellow),
813 QtGui.QColor(Qt.darkGray),
816 @classmethod
817 def next(cls):
818 cls.current_color_index += 1
819 cls.current_color_index %= len(cls.colors)
820 color = cls.colors[cls.current_color_index]
821 color.setAlpha(128)
822 return color
824 @classmethod
825 def current(cls):
826 return cls.colors[cls.current_color_index]
828 @classmethod
829 def reset(cls):
830 cls.current_color_index = 0
833 class Commit(QtGui.QGraphicsItem):
834 item_type = QtGui.QGraphicsItem.UserType + 2
835 commit_radius = 12.0
836 merge_radius = 18.0
838 item_shape = QtGui.QPainterPath()
839 item_shape.addRect(commit_radius/-2.0,
840 commit_radius/-2.0,
841 commit_radius, commit_radius)
842 item_bbox = item_shape.boundingRect()
844 inner_rect = QtGui.QPainterPath()
845 inner_rect.addRect(commit_radius/-2.0 + 2.0,
846 commit_radius/-2.0 + 2.0,
847 commit_radius - 4.0,
848 commit_radius - 4.0)
849 inner_rect = inner_rect.boundingRect()
851 commit_color = QtGui.QColor(Qt.white)
852 outline_color = commit_color.darker()
853 merge_color = QtGui.QColor(Qt.lightGray)
855 commit_selected_color = QtGui.QColor(Qt.green)
856 selected_outline_color = commit_selected_color.darker()
858 commit_pen = QtGui.QPen()
859 commit_pen.setWidth(1.0)
860 commit_pen.setColor(outline_color)
862 def __init__(self, commit,
863 notifier,
864 selectable=QtGui.QGraphicsItem.ItemIsSelectable,
865 cursor=Qt.PointingHandCursor,
866 xpos=commit_radius/2.0 + 1.0,
867 cached_commit_color=commit_color,
868 cached_merge_color=merge_color):
870 QtGui.QGraphicsItem.__init__(self)
872 self.commit = commit
873 self.notifier = notifier
875 self.setZValue(0)
876 self.setFlag(selectable)
877 self.setCursor(cursor)
878 self.setToolTip(commit.sha1[:7] + ': ' + commit.summary)
880 if commit.tags:
881 self.label = label = Label(commit)
882 label.setParentItem(self)
883 label.setPos(xpos, -self.commit_radius/2.0)
884 else:
885 self.label = None
887 if len(commit.parents) > 1:
888 self.brush = cached_merge_color
889 else:
890 self.brush = cached_commit_color
892 self.pressed = False
893 self.dragged = False
895 def blockSignals(self, blocked):
896 self.notifier.notification_enabled = not blocked
898 def itemChange(self, change, value):
899 if change == QtGui.QGraphicsItem.ItemSelectedHasChanged:
900 # Broadcast selection to other widgets
901 selected_items = self.scene().selectedItems()
902 commits = [item.commit for item in selected_items]
903 self.scene().parent().set_selecting(True)
904 self.notifier.notify_observers(diff.COMMITS_SELECTED, commits)
905 self.scene().parent().set_selecting(False)
907 # Cache the pen for use in paint()
908 if value.toPyObject():
909 self.brush = self.commit_selected_color
910 color = self.selected_outline_color
911 else:
912 if len(self.commit.parents) > 1:
913 self.brush = self.merge_color
914 else:
915 self.brush = self.commit_color
916 color = self.outline_color
917 commit_pen = QtGui.QPen()
918 commit_pen.setWidth(1.0)
919 commit_pen.setColor(color)
920 self.commit_pen = commit_pen
922 return QtGui.QGraphicsItem.itemChange(self, change, value)
924 def type(self):
925 return self.item_type
927 def boundingRect(self, rect=item_bbox):
928 return rect
930 def shape(self):
931 return self.item_shape
933 def paint(self, painter, option, widget,
934 inner=inner_rect,
935 cache=Cache):
937 # Do not draw outside the exposed rect
938 painter.setClipRect(option.exposedRect)
940 # Draw ellipse
941 painter.setPen(self.commit_pen)
942 painter.setBrush(self.brush)
943 painter.drawEllipse(inner)
946 def mousePressEvent(self, event):
947 QtGui.QGraphicsItem.mousePressEvent(self, event)
948 self.pressed = True
949 self.selected = self.isSelected()
951 def mouseMoveEvent(self, event):
952 if self.pressed:
953 self.dragged = True
954 QtGui.QGraphicsItem.mouseMoveEvent(self, event)
956 def mouseReleaseEvent(self, event):
957 QtGui.QGraphicsItem.mouseReleaseEvent(self, event)
958 if (not self.dragged and
959 self.selected and
960 event.button() == Qt.LeftButton):
961 return
962 self.pressed = False
963 self.dragged = False
966 class Label(QtGui.QGraphicsItem):
967 item_type = QtGui.QGraphicsItem.UserType + 3
969 width = 72
970 height = 18
972 item_shape = QtGui.QPainterPath()
973 item_shape.addRect(0, 0, width, height)
974 item_bbox = item_shape.boundingRect()
976 text_options = QtGui.QTextOption()
977 text_options.setAlignment(Qt.AlignCenter)
978 text_options.setAlignment(Qt.AlignVCenter)
980 def __init__(self, commit,
981 other_color=QtGui.QColor(Qt.white),
982 head_color=QtGui.QColor(Qt.green)):
983 QtGui.QGraphicsItem.__init__(self)
984 self.setZValue(-1)
986 # Starts with enough space for two tags. Any more and the commit
987 # needs to be taller to accommodate.
988 self.commit = commit
990 if 'HEAD' in commit.tags:
991 self.color = head_color
992 else:
993 self.color = other_color
995 self.color.setAlpha(180)
996 self.pen = QtGui.QPen()
997 self.pen.setColor(self.color.darker())
998 self.pen.setWidth(1.0)
1000 def type(self):
1001 return self.item_type
1003 def boundingRect(self, rect=item_bbox):
1004 return rect
1006 def shape(self):
1007 return self.item_shape
1009 def paint(self, painter, option, widget,
1010 text_opts=text_options,
1011 black=Qt.black,
1012 cache=Cache):
1013 try:
1014 font = cache.label_font
1015 except AttributeError:
1016 font = cache.label_font = QtGui.QApplication.font()
1017 font.setPointSize(6)
1020 # Draw tags
1021 painter.setBrush(self.color)
1022 painter.setPen(self.pen)
1023 painter.setFont(font)
1025 current_width = 0
1027 for tag in self.commit.tags:
1028 text_rect = painter.boundingRect(
1029 QRectF(current_width, 0, 0, 0), Qt.TextSingleLine, tag)
1030 box_rect = text_rect.adjusted(-1, -1, 1, 1)
1031 painter.drawRoundedRect(box_rect, 2, 2)
1032 painter.drawText(text_rect, Qt.TextSingleLine, tag)
1033 current_width += text_rect.width() + 5
1036 class GraphView(ViewerMixin, QtGui.QGraphicsView):
1038 x_max = 0
1039 y_min = 0
1041 x_adjust = Commit.commit_radius*4/3
1042 y_adjust = Commit.commit_radius*4/3
1044 x_off = 18
1045 y_off = 24
1047 def __init__(self, notifier, parent):
1048 QtGui.QGraphicsView.__init__(self, parent)
1049 ViewerMixin.__init__(self)
1051 highlight = self.palette().color(QtGui.QPalette.Highlight)
1052 Commit.commit_selected_color = highlight
1053 Commit.selected_outline_color = highlight.darker()
1055 self.selection_list = []
1056 self.notifier = notifier
1057 self.commits = []
1058 self.items = {}
1059 self.saved_matrix = QtGui.QMatrix(self.matrix())
1061 self.x_offsets = collections.defaultdict(int)
1063 self.is_panning = False
1064 self.pressed = False
1065 self.selecting = False
1066 self.last_mouse = [0, 0]
1067 self.zoom = 2
1068 self.setDragMode(self.RubberBandDrag)
1070 scene = QtGui.QGraphicsScene(self)
1071 scene.setItemIndexMethod(QtGui.QGraphicsScene.NoIndex)
1072 self.setScene(scene)
1074 self.setRenderHint(QtGui.QPainter.Antialiasing)
1075 self.setViewportUpdateMode(self.BoundingRectViewportUpdate)
1076 self.setCacheMode(QtGui.QGraphicsView.CacheBackground)
1077 self.setTransformationAnchor(QtGui.QGraphicsView.AnchorUnderMouse)
1078 self.setResizeAnchor(QtGui.QGraphicsView.NoAnchor)
1079 self.setBackgroundBrush(QtGui.QColor(Qt.white))
1081 qtutils.add_action(self, N_('Zoom In'), self.zoom_in,
1082 hotkeys.ZOOM_IN, hotkeys.ZOOM_IN_SECONDARY)
1084 qtutils.add_action(self, N_('Zoom Out'), self.zoom_out,
1085 hotkeys.ZOOM_OUT)
1087 qtutils.add_action(self, N_('Zoom to Fit'),
1088 self.zoom_to_fit, hotkeys.FIT)
1090 qtutils.add_action(self, N_('Select Parent'),
1091 self.select_parent, hotkeys.MOVE_DOWN_TERTIARY)
1093 qtutils.add_action(self, N_('Select Oldest Parent'),
1094 self.select_oldest_parent, hotkeys.MOVE_DOWN)
1096 qtutils.add_action(self, N_('Select Child'),
1097 self.select_child, hotkeys.MOVE_UP_TERTIARY)
1099 qtutils.add_action(self, N_('Select Newest Child'),
1100 self.select_newest_child, hotkeys.MOVE_UP)
1102 notifier.add_observer(diff.COMMITS_SELECTED, self.commits_selected)
1104 def clear(self):
1105 EdgeColor.reset()
1106 self.scene().clear()
1107 self.selection_list = []
1108 self.items.clear()
1109 self.x_offsets.clear()
1110 self.x_max = 0
1111 self.y_min = 0
1112 self.commits = []
1114 # ViewerMixin interface
1115 def selected_items(self):
1116 """Return the currently selected items"""
1117 return self.scene().selectedItems()
1119 def zoom_in(self):
1120 self.scale_view(1.5)
1122 def zoom_out(self):
1123 self.scale_view(1.0/1.5)
1125 def commits_selected(self, commits):
1126 if self.selecting:
1127 return
1128 self.select([commit.sha1 for commit in commits])
1130 def select(self, sha1s):
1131 """Select the item for the SHA-1"""
1132 self.scene().clearSelection()
1133 for sha1 in sha1s:
1134 try:
1135 item = self.items[sha1]
1136 except KeyError:
1137 continue
1138 item.blockSignals(True)
1139 item.setSelected(True)
1140 item.blockSignals(False)
1141 item_rect = item.sceneTransform().mapRect(item.boundingRect())
1142 self.ensureVisible(item_rect)
1144 def get_item_by_generation(self, commits, criteria_fn):
1145 """Return the item for the commit matching criteria"""
1146 if not commits:
1147 return None
1148 generation = None
1149 for commit in commits:
1150 if (generation is None or
1151 criteria_fn(generation, commit.generation)):
1152 sha1 = commit.sha1
1153 generation = commit.generation
1154 try:
1155 return self.items[sha1]
1156 except KeyError:
1157 return None
1159 def oldest_item(self, commits):
1160 """Return the item for the commit with the oldest generation number"""
1161 return self.get_item_by_generation(commits, lambda a, b: a > b)
1163 def newest_item(self, commits):
1164 """Return the item for the commit with the newest generation number"""
1165 return self.get_item_by_generation(commits, lambda a, b: a < b)
1167 def create_patch(self):
1168 items = self.selected_items()
1169 if not items:
1170 return
1171 selected_commits = self.sort_by_generation([n.commit for n in items])
1172 sha1s = [c.sha1 for c in selected_commits]
1173 all_sha1s = [c.sha1 for c in self.commits]
1174 cmds.do(cmds.FormatPatch, sha1s, all_sha1s)
1176 def select_parent(self):
1177 """Select the parent with the newest generation number"""
1178 selected_item = self.selected_item()
1179 if selected_item is None:
1180 return
1181 parent_item = self.newest_item(selected_item.commit.parents)
1182 if parent_item is None:
1183 return
1184 selected_item.setSelected(False)
1185 parent_item.setSelected(True)
1186 self.ensureVisible(
1187 parent_item.mapRectToScene(parent_item.boundingRect()))
1189 def select_oldest_parent(self):
1190 """Select the parent with the oldest generation number"""
1191 selected_item = self.selected_item()
1192 if selected_item is None:
1193 return
1194 parent_item = self.oldest_item(selected_item.commit.parents)
1195 if parent_item is None:
1196 return
1197 selected_item.setSelected(False)
1198 parent_item.setSelected(True)
1199 scene_rect = parent_item.mapRectToScene(parent_item.boundingRect())
1200 self.ensureVisible(scene_rect)
1202 def select_child(self):
1203 """Select the child with the oldest generation number"""
1204 selected_item = self.selected_item()
1205 if selected_item is None:
1206 return
1207 child_item = self.oldest_item(selected_item.commit.children)
1208 if child_item is None:
1209 return
1210 selected_item.setSelected(False)
1211 child_item.setSelected(True)
1212 scene_rect = child_item.mapRectToScene(child_item.boundingRect())
1213 self.ensureVisible(scene_rect)
1215 def select_newest_child(self):
1216 """Select the Nth child with the newest generation number (N > 1)"""
1217 selected_item = self.selected_item()
1218 if selected_item is None:
1219 return
1220 if len(selected_item.commit.children) > 1:
1221 children = selected_item.commit.children[1:]
1222 else:
1223 children = selected_item.commit.children
1224 child_item = self.newest_item(children)
1225 if child_item is None:
1226 return
1227 selected_item.setSelected(False)
1228 child_item.setSelected(True)
1229 scene_rect = child_item.mapRectToScene(child_item.boundingRect())
1230 self.ensureVisible(scene_rect)
1232 def set_initial_view(self):
1233 self_commits = self.commits
1234 self_items = self.items
1236 items = self.selected_items()
1237 if not items:
1238 commits = self_commits[-8:]
1239 items = [self_items[c.sha1] for c in commits]
1241 self.fit_view_to_items(items)
1243 def zoom_to_fit(self):
1244 """Fit selected items into the viewport"""
1246 items = self.selected_items()
1247 self.fit_view_to_items(items)
1249 def fit_view_to_items(self, items):
1250 if not items:
1251 rect = self.scene().itemsBoundingRect()
1252 else:
1253 maxint = 9223372036854775807
1254 x_min = maxint
1255 y_min = maxint
1256 x_max = -maxint
1257 ymax = -maxint
1258 for item in items:
1259 pos = item.pos()
1260 item_rect = item.boundingRect()
1261 x_off = item_rect.width() * 5
1262 y_off = item_rect.height() * 10
1263 x_min = min(x_min, pos.x())
1264 y_min = min(y_min, pos.y()-y_off)
1265 x_max = max(x_max, pos.x()+x_off)
1266 ymax = max(ymax, pos.y())
1267 rect = QtCore.QRectF(x_min, y_min, x_max-x_min, ymax-y_min)
1268 x_adjust = GraphView.x_adjust
1269 y_adjust = GraphView.y_adjust
1270 rect.setX(rect.x() - x_adjust)
1271 rect.setY(rect.y() - y_adjust)
1272 rect.setHeight(rect.height() + y_adjust*2)
1273 rect.setWidth(rect.width() + x_adjust*2)
1274 self.fitInView(rect, Qt.KeepAspectRatio)
1275 self.scene().invalidate()
1277 def save_selection(self, event):
1278 if event.button() != Qt.LeftButton:
1279 return
1280 elif Qt.ShiftModifier != event.modifiers():
1281 return
1282 self.selection_list = self.selected_items()
1284 def restore_selection(self, event):
1285 if Qt.ShiftModifier != event.modifiers():
1286 return
1287 for item in self.selection_list:
1288 item.setSelected(True)
1290 def handle_event(self, event_handler, event):
1291 self.save_selection(event)
1292 event_handler(self, event)
1293 self.restore_selection(event)
1294 self.update()
1296 def set_selecting(self, selecting):
1297 self.selecting = selecting
1299 def pan(self, event):
1300 pos = event.pos()
1301 dx = pos.x() - self.mouse_start[0]
1302 dy = pos.y() - self.mouse_start[1]
1304 if dx == 0 and dy == 0:
1305 return
1307 rect = QtCore.QRect(0, 0, abs(dx), abs(dy))
1308 delta = self.mapToScene(rect).boundingRect()
1310 tx = delta.width()
1311 if dx < 0.0:
1312 tx = -tx
1314 ty = delta.height()
1315 if dy < 0.0:
1316 ty = -ty
1318 matrix = QtGui.QMatrix(self.saved_matrix).translate(tx, ty)
1319 self.setTransformationAnchor(QtGui.QGraphicsView.NoAnchor)
1320 self.setMatrix(matrix)
1322 def wheel_zoom(self, event):
1323 """Handle mouse wheel zooming."""
1324 zoom = math.pow(2.0, event.delta()/512.0)
1325 factor = (self.matrix()
1326 .scale(zoom, zoom)
1327 .mapRect(QtCore.QRectF(0.0, 0.0, 1.0, 1.0))
1328 .width())
1329 if factor < 0.014 or factor > 42.0:
1330 return
1331 self.setTransformationAnchor(QtGui.QGraphicsView.AnchorUnderMouse)
1332 self.zoom = zoom
1333 self.scale(zoom, zoom)
1335 def wheel_pan(self, event):
1336 """Handle mouse wheel panning."""
1338 if event.delta() < 0:
1339 s = -133.0
1340 else:
1341 s = 133.0
1342 pan_rect = QtCore.QRectF(0.0, 0.0, 1.0, 1.0)
1343 factor = 1.0/self.matrix().mapRect(pan_rect).width()
1345 if event.orientation() == Qt.Vertical:
1346 matrix = self.matrix().translate(0, s*factor)
1347 else:
1348 matrix = self.matrix().translate(s*factor, 0)
1349 self.setTransformationAnchor(QtGui.QGraphicsView.NoAnchor)
1350 self.setMatrix(matrix)
1352 def scale_view(self, scale):
1353 factor = (self.matrix().scale(scale, scale)
1354 .mapRect(QtCore.QRectF(0, 0, 1, 1))
1355 .width())
1356 if factor < 0.07 or factor > 100.0:
1357 return
1358 self.zoom = scale
1360 adjust_scrollbars = True
1361 scrollbar = self.verticalScrollBar()
1362 if scrollbar:
1363 value = scrollbar.value()
1364 min_ = scrollbar.minimum()
1365 max_ = scrollbar.maximum()
1366 range_ = max_ - min_
1367 distance = value - min_
1368 nonzero_range = range_ > 0.1
1369 if nonzero_range:
1370 scrolloffset = distance/range_
1371 else:
1372 adjust_scrollbars = False
1374 self.setTransformationAnchor(QtGui.QGraphicsView.NoAnchor)
1375 self.scale(scale, scale)
1377 scrollbar = self.verticalScrollBar()
1378 if scrollbar and adjust_scrollbars:
1379 min_ = scrollbar.minimum()
1380 max_ = scrollbar.maximum()
1381 range_ = max_ - min_
1382 value = min_ + int(float(range_) * scrolloffset)
1383 scrollbar.setValue(value)
1385 def add_commits(self, commits):
1386 """Traverse commits and add them to the view."""
1387 self.commits.extend(commits)
1388 scene = self.scene()
1389 for commit in commits:
1390 item = Commit(commit, self.notifier)
1391 self.items[commit.sha1] = item
1392 for ref in commit.tags:
1393 self.items[ref] = item
1394 scene.addItem(item)
1396 self.layout_commits(commits)
1397 self.link(commits)
1399 def link(self, commits):
1400 """Create edges linking commits with their parents"""
1401 scene = self.scene()
1402 for commit in commits:
1403 try:
1404 commit_item = self.items[commit.sha1]
1405 except KeyError:
1406 # TODO - Handle truncated history viewing
1407 continue
1408 for parent in reversed(commit.parents):
1409 try:
1410 parent_item = self.items[parent.sha1]
1411 except KeyError:
1412 # TODO - Handle truncated history viewing
1413 continue
1414 edge = Edge(parent_item, commit_item)
1415 scene.addItem(edge)
1417 def layout_commits(self, nodes):
1418 positions = self.position_nodes(nodes)
1419 for sha1, (x, y) in positions.items():
1420 item = self.items[sha1]
1421 item.setPos(x, y)
1423 def position_nodes(self, nodes):
1424 positions = {}
1426 x_max = self.x_max
1427 y_min = self.y_min
1428 x_off = self.x_off
1429 y_off = self.y_off
1430 x_offsets = self.x_offsets
1432 for node in nodes:
1433 generation = node.generation
1434 sha1 = node.sha1
1436 if node.is_fork():
1437 # This is a fan-out so sweep over child generations and
1438 # shift them to the right to avoid overlapping edges
1439 child_gens = [c.generation for c in node.children]
1440 maxgen = max(child_gens)
1441 for g in range(generation + 1, maxgen):
1442 x_offsets[g] += x_off
1444 if len(node.parents) == 1:
1445 # Align nodes relative to their parents
1446 parent_gen = node.parents[0].generation
1447 parent_off = x_offsets[parent_gen]
1448 x_offsets[generation] = max(parent_off-x_off,
1449 x_offsets[generation])
1451 cur_xoff = x_offsets[generation]
1452 next_xoff = cur_xoff
1453 next_xoff += x_off
1454 x_offsets[generation] = next_xoff
1456 x_pos = cur_xoff
1457 y_pos = -generation * y_off
1459 y_pos = min(y_pos, y_min - y_off)
1461 #y_pos = y_off
1462 positions[sha1] = (x_pos, y_pos)
1464 x_max = max(x_max, x_pos)
1465 y_min = y_pos
1467 self.x_max = x_max
1468 self.y_min = y_min
1470 return positions
1472 def update_scene_rect(self):
1473 y_min = self.y_min
1474 x_max = self.x_max
1475 self.scene().setSceneRect(-GraphView.x_adjust,
1476 y_min-GraphView.y_adjust,
1477 x_max + GraphView.x_adjust,
1478 abs(y_min) + GraphView.y_adjust)
1480 def sort_by_generation(self, commits):
1481 if len(commits) < 2:
1482 return commits
1483 commits.sort(key=lambda x: x.generation)
1484 return commits
1486 # Qt overrides
1487 def contextMenuEvent(self, event):
1488 self.context_menu_event(event)
1490 def mousePressEvent(self, event):
1491 if event.button() == Qt.MidButton:
1492 pos = event.pos()
1493 self.mouse_start = [pos.x(), pos.y()]
1494 self.saved_matrix = QtGui.QMatrix(self.matrix())
1495 self.is_panning = True
1496 return
1497 if event.button() == Qt.RightButton:
1498 event.ignore()
1499 return
1500 if event.button() == Qt.LeftButton:
1501 self.pressed = True
1502 self.handle_event(QtGui.QGraphicsView.mousePressEvent, event)
1504 def mouseMoveEvent(self, event):
1505 pos = self.mapToScene(event.pos())
1506 if self.is_panning:
1507 self.pan(event)
1508 return
1509 self.last_mouse[0] = pos.x()
1510 self.last_mouse[1] = pos.y()
1511 self.handle_event(QtGui.QGraphicsView.mouseMoveEvent, event)
1512 if self.pressed:
1513 self.viewport().repaint()
1515 def mouseReleaseEvent(self, event):
1516 self.pressed = False
1517 if event.button() == Qt.MidButton:
1518 self.is_panning = False
1519 return
1520 self.handle_event(QtGui.QGraphicsView.mouseReleaseEvent, event)
1521 self.selection_list = []
1522 self.viewport().repaint()
1524 def wheelEvent(self, event):
1525 """Handle Qt mouse wheel events."""
1526 if event.modifiers() & Qt.ControlModifier:
1527 self.wheel_zoom(event)
1528 else:
1529 self.wheel_pan(event)