widgets: improve reuse of keyboard bindings, etc
[git-cola.git] / cola / widgets / compare.py
blob0ad99a3eb406dc9f5eb1b875ef59d7e820f61003
1 """Provides dialogs for comparing branches and commits."""
3 from PyQt4 import QtCore
4 from PyQt4 import QtGui
5 from PyQt4.QtCore import SIGNAL
7 from cola import qtutils
8 from cola import difftool
9 from cola import gitcmds
10 from cola.git import git
11 from cola.i18n import N_
12 from cola.qtutils import connect_button
13 from cola.widgets import defs
14 from cola.widgets import standard
17 class FileItem(QtGui.QTreeWidgetItem):
18 def __init__(self, path, icon):
19 QtGui.QTreeWidgetItem.__init__(self, [path])
20 self.path = path
21 self.setIcon(0, icon)
24 def compare_branches():
25 """Launches a dialog for comparing a pair of branches"""
26 view = CompareBranchesDialog(qtutils.active_window())
27 view.show()
28 return view
31 class CompareBranchesDialog(standard.Dialog):
34 def __init__(self, parent):
35 standard.Dialog.__init__(self, parent=parent)
37 self.BRANCH_POINT = N_('*** Branch Point ***')
38 self.SANDBOX = N_('*** Sandbox ***')
39 self.LOCAL = N_('Local')
41 self.remote_branches = gitcmds.branch_list(remote=True)
42 self.local_branches = gitcmds.branch_list(remote=False)
44 self.setWindowTitle(N_('Branch Diff Viewer'))
45 self.resize(658, 350)
47 self.main_layt = QtGui.QVBoxLayout(self)
48 self.main_layt.setMargin(defs.margin)
49 self.main_layt.setSpacing(defs.spacing)
51 self.splitter = QtGui.QSplitter(self)
52 self.splitter.setOrientation(QtCore.Qt.Vertical)
53 self.splitter.setHandleWidth(defs.handle_width)
55 self.top_widget = QtGui.QWidget(self.splitter)
57 self.top_grid_layt = QtGui.QGridLayout(self.top_widget)
58 self.top_grid_layt.setMargin(0)
59 self.top_grid_layt.setSpacing(defs.spacing)
61 self.left_combo = QtGui.QComboBox(self.top_widget)
62 self.left_combo.addItem(N_('Local'))
63 self.left_combo.addItem(N_('Remote'))
64 self.left_combo.setCurrentIndex(0)
65 self.top_grid_layt.addWidget(self.left_combo, 0, 0, 1, 1)
67 self.right_combo = QtGui.QComboBox(self.top_widget)
68 self.right_combo.addItem(N_('Local'))
69 self.right_combo.addItem(N_('Remote'))
70 self.right_combo.setCurrentIndex(1)
71 self.top_grid_layt.addWidget(self.right_combo, 0, 1, 1, 1)
73 self.left_list = QtGui.QListWidget(self.top_widget)
74 self.top_grid_layt.addWidget(self.left_list, 1, 0, 1, 1)
76 self.right_list = QtGui.QListWidget(self.top_widget)
77 self.top_grid_layt.addWidget(self.right_list, 1, 1, 1, 1)
79 self.bottom_widget = QtGui.QWidget(self.splitter)
80 self.bottom_grid_layt = QtGui.QGridLayout(self.bottom_widget)
81 self.bottom_grid_layt.setMargin(0)
82 self.bottom_grid_layt.setSpacing(defs.button_spacing)
84 self.button_spacer = QtGui.QSpacerItem(1, 1,
85 QtGui.QSizePolicy.Expanding,
86 QtGui.QSizePolicy.Minimum)
87 self.bottom_grid_layt.addItem(self.button_spacer, 1, 1, 1, 1)
89 self.button_compare = QtGui.QPushButton(self.bottom_widget)
90 self.button_compare.setText(N_('Compare'))
91 self.bottom_grid_layt.addWidget(self.button_compare, 1, 2, 1, 1)
93 self.button_close = QtGui.QPushButton(self.bottom_widget)
94 self.button_close.setText(N_('Close'))
95 self.bottom_grid_layt.addWidget(self.button_close, 1, 3, 1, 1)
97 self.diff_files = standard.TreeWidget(self.bottom_widget)
98 self.diff_files.headerItem().setText(0, N_('File Differences'))
100 self.bottom_grid_layt.addWidget(self.diff_files, 0, 0, 1, 4)
101 self.main_layt.addWidget(self.splitter)
103 connect_button(self.button_close, self.accept)
104 connect_button(self.button_compare, self.compare)
106 self.connect(self.diff_files,
107 SIGNAL('itemDoubleClicked(QTreeWidgetItem*,int)'),
108 self.compare)
110 self.connect(self.left_combo,
111 SIGNAL('currentIndexChanged(int)'),
112 lambda x: self.update_combo_boxes(left=True))
114 self.connect(self.right_combo,
115 SIGNAL('currentIndexChanged(int)'),
116 lambda x: self.update_combo_boxes(left=False))
118 self.connect(self.left_list,
119 SIGNAL('itemSelectionChanged()'), self.update_diff_files)
121 self.connect(self.right_list,
122 SIGNAL('itemSelectionChanged()'), self.update_diff_files)
124 self.update_combo_boxes(left=True)
125 self.update_combo_boxes(left=False)
127 # Pre-select the 0th elements
128 item = self.left_list.item(0)
129 if item:
130 self.left_list.setCurrentItem(item)
131 self.left_list.setItemSelected(item, True)
133 item = self.right_list.item(0)
134 if item:
135 self.right_list.setCurrentItem(item)
136 self.right_list.setItemSelected(item, True)
138 def selection(self):
139 left_item = self.left_list.currentItem()
140 if left_item and left_item.isSelected():
141 left_item = unicode(left_item.text())
142 else:
143 left_item = None
144 right_item = self.right_list.currentItem()
145 if right_item and right_item.isSelected():
146 right_item = unicode(right_item.text())
147 else:
148 right_item = None
149 return (left_item, right_item)
152 def update_diff_files(self, *rest):
153 """Updates the list of files whenever the selection changes"""
154 # Left and Right refer to the comparison pair (l,r)
155 left_item, right_item = self.selection()
156 if (not left_item or not right_item or
157 left_item == right_item):
158 self.set_diff_files([])
159 return
160 left_item = self.remote_ref(left_item)
161 right_item = self.remote_ref(right_item)
163 # If any of the selection includes sandbox then we
164 # generate the same diff, regardless. This means we don't
165 # support reverse diffs against sandbox aka worktree.
166 if self.SANDBOX in (left_item, right_item):
167 self.use_sandbox = True
168 if left_item == self.SANDBOX:
169 self.diff_arg = (right_item,)
170 else:
171 self.diff_arg = (left_item,)
172 else:
173 self.diff_arg = (left_item, right_item)
174 self.use_sandbox = False
176 # start and end as in 'git diff start end'
177 self.start = left_item
178 self.end = right_item
180 if len(self.diff_arg) == 1:
181 files = gitcmds.diff_index_filenames(self.diff_arg[0])
182 else:
183 files = gitcmds.diff_filenames(*self.diff_arg)
185 self.set_diff_files(files)
187 def set_diff_files(self, files):
188 mk = FileItem
189 icon = qtutils.icon('script.png')
190 self.diff_files.clear()
191 self.diff_files.addTopLevelItems([mk(f, icon) for f in files])
193 def remote_ref(self, branch):
194 """Returns the remote ref for 'git diff [local] [remote]'
196 if branch == self.BRANCH_POINT:
197 # Compare against the branch point so find the merge-base
198 branch = gitcmds.current_branch()
199 tracked_branch = gitcmds.tracked_branch()
200 if tracked_branch:
201 return git.merge_base(branch, tracked_branch)
202 else:
203 remote_branches = gitcmds.branch_list(remote=True)
204 remote_branch = 'origin/%s' % branch
205 if remote_branch in remote_branches:
206 return git.merge_base(branch, remote_branch)
208 elif 'origin/master' in remote_branches:
209 return git.merge_base(branch, 'origin/master')
210 else:
211 return 'HEAD'
212 else:
213 # Compare against the remote branch
214 return branch
217 def update_combo_boxes(self, left=False):
218 """Update listwidgets from the combobox selection
220 Update either the left or right listwidgets
221 to reflect the available items.
223 if left:
224 which = unicode(self.left_combo.currentText())
225 widget = self.left_list
226 else:
227 which = unicode(self.right_combo.currentText())
228 widget = self.right_list
229 if not which:
230 return
231 # If we're looking at "local" stuff then provide the
232 # sandbox as a valid choice. If we're looking at
233 # "remote" stuff then also include the branch point.
234 if which == self.LOCAL:
235 new_list = ([self.SANDBOX]+ self.local_branches)
236 else:
237 new_list = ([self.BRANCH_POINT] + self.remote_branches)
239 widget.clear()
240 widget.addItems(new_list)
241 if new_list:
242 item = widget.item(0)
243 widget.setCurrentItem(item)
244 widget.setItemSelected(item, True)
246 def compare(self, *args):
247 """Shows the diff for a specific file
249 tree_widget = self.diff_files
250 item = tree_widget.currentItem()
251 if item and item.isSelected():
252 self.compare_file(item.path)
254 def compare_file(self, filename):
255 """Initiates the difftool session"""
256 if self.use_sandbox:
257 arg = self.diff_arg
258 else:
259 arg = (self.start, self.end)
260 difftool.launch(arg + ('--', filename))