widgets: Use the common shorcuts defined in widgets.defs
[git-cola.git] / cola / classic / view.py
blob4648dcf15460e787d8d19c37b4a61e81557710af
1 import os
3 from PyQt4 import QtGui
4 from PyQt4.QtCore import Qt
5 from PyQt4.QtCore import SIGNAL
7 import cola
8 from cola import qtutils
9 from cola import signals
10 from cola import utils
11 from cola.models.selection import State
12 from cola.widgets import defs
13 from cola.widgets import standard
14 from cola.classic.model import GitRepoNameItem
17 class Browser(standard.Widget):
18 def __init__(self, parent, update=True):
19 standard.Widget.__init__(self, parent)
20 self.tree = RepoTreeView(self)
21 self.mainlayout = QtGui.QHBoxLayout()
22 self.setLayout(self.mainlayout)
23 self.mainlayout.setMargin(0)
24 self.mainlayout.setSpacing(defs.spacing)
25 self.mainlayout.addWidget(self.tree)
26 self.resize(720, 420)
28 self.connect(self, SIGNAL('updated'), self._updated_callback)
29 self.model = cola.model()
30 self.model.add_observer(self.model.message_updated, self.model_updated)
31 qtutils.add_close_action(self)
32 if update:
33 self.model_updated()
35 # Read-only mode property
36 mode = property(lambda self: self.model.mode)
38 def model_updated(self):
39 """Update the title with the current branch and directory name."""
40 self.emit(SIGNAL('updated'))
42 def _updated_callback(self):
43 branch = self.model.currentbranch
44 curdir = os.getcwd()
45 msg = 'Repository: %s\nBranch: %s' % (curdir, branch)
47 self.setToolTip(msg)
49 title = '%s: %s - Browse' % (self.model.project, branch)
50 if self.mode == self.model.mode_amend:
51 title += ' ** amending **'
52 self.setWindowTitle(title)
55 class RepoTreeView(standard.TreeView):
56 """Provides a filesystem-like view of a git repository."""
57 def __init__(self, parent):
58 standard.TreeView.__init__(self, parent)
60 self.setSortingEnabled(False)
61 self.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
63 # Observe model updates
64 model = cola.model()
65 model.add_observer(model.message_updated, self.update_actions)
67 # The non-Qt cola application model
68 self.connect(self, SIGNAL('expanded(QModelIndex)'), self.size_columns)
69 self.connect(self, SIGNAL('collapsed(QModelIndex)'), self.size_columns)
71 # Sync selection before the key press event changes the model index
72 self.connect(self, SIGNAL('indexAboutToChange()'), self.sync_selection)
74 self.action_history =\
75 self._create_action('View History...',
76 'View history for selected path(s).',
77 self.view_history,
78 'Shift+Ctrl+H')
79 self.action_stage =\
80 self._create_action('Stage Selected',
81 'Stage selected path(s) for commit.',
82 self.stage_selected,
83 defs.stage_shortcut)
84 self.action_unstage =\
85 self._create_action('Unstage Selected',
86 'Remove selected path(s) from '
87 'the staging area.',
88 self.unstage_selected,
89 'Ctrl+U')
91 self.action_untrack =\
92 self._create_action('Untrack Selected',
93 'Stop tracking path(s)',
94 self.untrack_selected)
96 self.action_difftool =\
97 self._create_action('View Diff...',
98 'Launch git-difftool on the current path.',
99 self.difftool,
100 defs.difftool_shortcut)
101 self.action_difftool_predecessor =\
102 self._create_action('Diff Against Predecessor...',
103 'Launch git-difftool against previous versions.',
104 self.difftool_predecessor,
105 'Shift+Ctrl+D')
106 self.action_revert =\
107 self._create_action('Revert Uncommitted Changes...',
108 'Revert changes to selected path(s).',
109 self.revert,
110 'Ctrl+Z')
111 self.action_editor =\
112 self._create_action('Launch Editor',
113 'Edit selected path(s).',
114 self.editor,
115 defs.editor_shortcut)
117 def size_columns(self):
118 """Set the column widths."""
119 self.resizeColumnToContents(0)
121 def update_actions(self):
122 """Enable/disable actions."""
123 selection = self.selected_paths()
124 selected = bool(selection)
125 staged = bool(self.selected_staged_paths(selection=selection))
126 modified = bool(self.selected_modified_paths(selection=selection))
127 unstaged = bool(self.selected_unstaged_paths(selection=selection))
128 tracked = bool(self.selected_tracked_paths())
130 self.action_history.setEnabled(selected)
131 self.action_stage.setEnabled(unstaged)
132 self.action_unstage.setEnabled(staged)
133 self.action_untrack.setEnabled(tracked)
134 self.action_difftool.setEnabled(staged or modified)
135 self.action_difftool_predecessor.setEnabled(tracked)
136 self.action_revert.setEnabled(tracked)
138 def contextMenuEvent(self, event):
139 """Create a context menu."""
140 self.update_actions()
141 menu = QtGui.QMenu(self)
142 menu.addAction(self.action_editor)
143 menu.addAction(self.action_stage)
144 menu.addAction(self.action_unstage)
145 menu.addSeparator()
146 menu.addAction(self.action_history)
147 menu.addAction(self.action_difftool)
148 menu.addAction(self.action_difftool_predecessor)
149 menu.addSeparator()
150 menu.addAction(self.action_revert)
151 menu.addAction(self.action_untrack)
152 menu.exec_(self.mapToGlobal(event.pos()))
154 def mousePressEvent(self, event):
155 """Synchronize the selection on mouse-press."""
156 result = QtGui.QTreeView.mousePressEvent(self, event)
157 self.sync_selection()
158 return result
160 def sync_selection(self):
161 """Push selection into the selection model."""
162 staged = []
163 unmerged = []
164 modified = []
165 untracked = []
166 state = State(staged, unmerged, modified, untracked)
168 paths = self.selected_paths()
169 model = cola.model()
170 model_staged = utils.add_parents(set(model.staged))
171 model_modified = utils.add_parents(set(model.modified))
172 model_unmerged = utils.add_parents(set(model.unmerged))
173 model_untracked = utils.add_parents(set(model.untracked))
175 for path in paths:
176 if path in model_unmerged:
177 unmerged.append(path)
178 elif path in model_untracked:
179 untracked.append(path)
180 elif path in model_staged:
181 staged.append(path)
182 elif path in model_modified:
183 modified.append(path)
184 else:
185 staged.append(path)
186 # Push the new selection into the model.
187 cola.selection_model().set_selection(state)
188 return paths
190 def selectionChanged(self, old_selection, new_selection):
191 """Override selectionChanged to update available actions."""
192 result = QtGui.QTreeView.selectionChanged(self, old_selection, new_selection)
193 self.update_actions()
194 paths = self.sync_selection()
196 if paths and self.model().path_is_interesting(paths[0]):
197 cached = paths[0] in cola.model().staged
198 cola.notifier().broadcast(signals.diff, paths, cached)
199 return result
201 def setModel(self, model):
202 """Set the concrete QAbstractItemModel instance."""
203 QtGui.QTreeView.setModel(self, model)
204 self.size_columns()
206 def item_from_index(self, model_index):
207 """Return the name item corresponding to the model index."""
208 index = model_index.sibling(model_index.row(), 0)
209 return self.model().itemFromIndex(index)
211 def selected_paths(self):
212 """Return the selected paths."""
213 items = map(self.model().itemFromIndex, self.selectedIndexes())
214 return [i.path for i in items
215 if i.type() == GitRepoNameItem.TYPE]
217 def selected_staged_paths(self, selection=None):
218 """Return selected staged paths."""
219 if not selection:
220 selection = self.selected_paths()
221 staged = utils.add_parents(set(cola.model().staged))
222 return [p for p in selection if p in staged]
224 def selected_modified_paths(self, selection=None):
225 """Return selected modified paths."""
226 if not selection:
227 selection = self.selected_paths()
228 model = cola.model()
229 modified = utils.add_parents(set(model.modified))
230 return [p for p in selection if p in modified]
232 def selected_unstaged_paths(self, selection=None):
233 """Return selected unstaged paths."""
234 if not selection:
235 selection = self.selected_paths()
236 model = cola.model()
237 modified = utils.add_parents(set(model.modified))
238 untracked = utils.add_parents(set(model.untracked))
239 unstaged = modified.union(untracked)
240 return [p for p in selection if p in unstaged]
242 def selected_tracked_paths(self, selection=None):
243 """Return selected tracked paths."""
244 if not selection:
245 selection = self.selected_paths()
246 model = cola.model()
247 staged = set(self.selected_staged_paths())
248 modified = set(self.selected_modified_paths())
249 untracked = utils.add_parents(set(model.untracked))
250 tracked = staged.union(modified)
251 return [p for p in selection
252 if p not in untracked or p in tracked]
254 def _create_action(self, name, tooltip, slot, shortcut=None):
255 """Create an action with a shortcut, tooltip, and callback slot."""
256 action = QtGui.QAction(self.tr(name), self)
257 action.setStatusTip(self.tr(tooltip))
258 if shortcut is not None:
259 if hasattr(Qt, 'WidgetWithChildrenShortcut'):
260 action.setShortcutContext(Qt.WidgetWithChildrenShortcut)
261 action.setShortcut(shortcut)
262 self.addAction(action)
263 qtutils.connect_action(action, slot)
264 return action
266 def view_history(self):
267 """Signal that we should view history for paths."""
268 self.emit(SIGNAL('history(QStringList)'), self.selected_paths())
270 def stage_selected(self):
271 """Signal that we should stage selected paths."""
272 cola.notifier().broadcast(signals.stage,
273 self.selected_unstaged_paths())
275 def unstage_selected(self):
276 """Signal that we should stage selected paths."""
277 cola.notifier().broadcast(signals.unstage,
278 self.selected_staged_paths())
280 def untrack_selected(self):
281 """Signal that we should stage selected paths."""
282 cola.notifier().broadcast(signals.untrack,
283 self.selected_tracked_paths())
285 def difftool(self):
286 """Signal that we should launch difftool on a path."""
287 cola.notifier().broadcast(signals.difftool,
288 False,
289 self.selected_tracked_paths())
291 def difftool_predecessor(self):
292 """Diff paths against previous versions."""
293 paths = self.selected_tracked_paths()
294 self.emit(SIGNAL('difftool_predecessor'), paths)
296 def revert(self):
297 """Signal that we should revert changes to a path."""
298 if not qtutils.confirm('Revert Uncommitted Changes?',
299 'This operation drops uncommitted changes.'
300 '\nThese changes cannot be recovered.',
301 'Revert the uncommitted changes?',
302 'Revert Uncommitted Changes',
303 default=False,
304 icon=qtutils.icon('undo.svg')):
305 return
306 paths = self.selected_tracked_paths()
307 cola.notifier().broadcast(signals.checkout,
308 ['HEAD', '--'] + paths)
310 def editor(self):
311 """Signal that we should edit selected paths using an external editor."""
312 cola.notifier().broadcast(signals.edit, self.selected_paths())
314 def current_path(self):
315 """Return the path for the current item."""
316 index = self.currentIndex()
317 if not index.isValid():
318 return None
319 return self.item_from_index(index).path