classic: Make the title match 'cola' and 'dag'
[git-cola.git] / cola / classic / view.py
blob79e3d0298557eb2652c91452a187eb778793b8ba
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 RepoDialog(standard.Widget):
18 def __init__(self, parent, update=True):
19 super(RepoDialog, self).__init__(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' % (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 super(RepoTreeView, self).__init__(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 'Ctrl+S')
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')
90 self.action_difftool =\
91 self._create_action('View Diff...',
92 'Launch git-difftool on the current path.',
93 self.difftool,
94 'Ctrl+D')
95 self.action_difftool_predecessor =\
96 self._create_action('Diff Against Predecessor...',
97 'Launch git-difftool against previous versions.',
98 self.difftool_predecessor,
99 'Shift+Ctrl+D')
100 self.action_revert =\
101 self._create_action('Revert Uncommitted Changes...',
102 'Revert changes to selected path(s).',
103 self.revert,
104 'Ctrl+Z')
105 self.action_editor =\
106 self._create_action('Launch Editor',
107 'Edit selected path(s).',
108 self.editor,
109 'Ctrl+E')
111 def size_columns(self):
112 """Set the column widths."""
113 self.resizeColumnToContents(0)
115 def update_actions(self):
116 """Enable/disable actions."""
117 selection = self.selected_paths()
118 selected = bool(selection)
119 staged = bool(self.selected_staged_paths(selection=selection))
120 modified = bool(self.selected_modified_paths(selection=selection))
121 unstaged = bool(self.selected_unstaged_paths(selection=selection))
122 tracked = bool(self.selected_tracked_paths())
124 self.action_history.setEnabled(selected)
125 self.action_stage.setEnabled(unstaged)
126 self.action_unstage.setEnabled(staged)
127 self.action_difftool.setEnabled(staged or modified)
128 self.action_difftool_predecessor.setEnabled(tracked)
129 self.action_revert.setEnabled(tracked)
131 def contextMenuEvent(self, event):
132 """Create a context menu."""
133 self.update_actions()
134 menu = QtGui.QMenu(self)
135 menu.addAction(self.action_editor)
136 menu.addAction(self.action_stage)
137 menu.addAction(self.action_unstage)
138 menu.addSeparator()
139 menu.addAction(self.action_history)
140 menu.addAction(self.action_difftool)
141 menu.addAction(self.action_difftool_predecessor)
142 menu.addSeparator()
143 menu.addAction(self.action_revert)
144 menu.exec_(self.mapToGlobal(event.pos()))
146 def mousePressEvent(self, event):
147 """Synchronize the selection on mouse-press."""
148 result = QtGui.QTreeView.mousePressEvent(self, event)
149 self.sync_selection()
150 return result
152 def sync_selection(self):
153 """Push selection into the selection model."""
154 staged = []
155 unmerged = []
156 modified = []
157 untracked = []
158 state = State(staged, unmerged, modified, untracked)
160 paths = self.selected_paths()
161 model = cola.model()
162 model_staged = utils.add_parents(set(model.staged))
163 model_modified = utils.add_parents(set(model.modified))
164 model_unmerged = utils.add_parents(set(model.unmerged))
165 model_untracked = utils.add_parents(set(model.untracked))
167 for path in paths:
168 if path in model_unmerged:
169 unmerged.append(path)
170 elif path in model_untracked:
171 untracked.append(path)
172 elif path in model_staged:
173 staged.append(path)
174 elif path in model_modified:
175 modified.append(path)
176 else:
177 staged.append(path)
178 # Push the new selection into the model.
179 cola.selection_model().set_selection(state)
180 return paths
182 def selectionChanged(self, old_selection, new_selection):
183 """Override selectionChanged to update available actions."""
184 result = QtGui.QTreeView.selectionChanged(self, old_selection, new_selection)
185 self.update_actions()
186 paths = self.sync_selection()
188 if paths and self.model().path_is_interesting(paths[0]):
189 cached = paths[0] in cola.model().staged
190 cola.notifier().broadcast(signals.diff, paths, cached)
191 return result
193 def setModel(self, model):
194 """Set the concrete QAbstractItemModel instance."""
195 QtGui.QTreeView.setModel(self, model)
196 self.size_columns()
198 def item_from_index(self, model_index):
199 """Return the name item corresponding to the model index."""
200 index = model_index.sibling(model_index.row(), 0)
201 return self.model().itemFromIndex(index)
203 def selected_paths(self):
204 """Return the selected paths."""
205 items = map(self.model().itemFromIndex, self.selectedIndexes())
206 return [i.path for i in items
207 if i.type() == GitRepoNameItem.TYPE]
209 def selected_staged_paths(self, selection=None):
210 """Return selected staged paths."""
211 if not selection:
212 selection = self.selected_paths()
213 staged = utils.add_parents(set(cola.model().staged))
214 return [p for p in selection if p in staged]
216 def selected_modified_paths(self, selection=None):
217 """Return selected modified paths."""
218 if not selection:
219 selection = self.selected_paths()
220 model = cola.model()
221 modified = utils.add_parents(set(model.modified))
222 return [p for p in selection if p in modified]
224 def selected_unstaged_paths(self, selection=None):
225 """Return selected unstaged paths."""
226 if not selection:
227 selection = self.selected_paths()
228 model = cola.model()
229 modified = utils.add_parents(set(model.modified))
230 untracked = utils.add_parents(set(model.untracked))
231 unstaged = modified.union(untracked)
232 return [p for p in selection if p in unstaged]
234 def selected_tracked_paths(self, selection=None):
235 """Return selected tracked paths."""
236 if not selection:
237 selection = self.selected_paths()
238 model = cola.model()
239 staged = set(self.selected_staged_paths())
240 modified = set(self.selected_modified_paths())
241 untracked = utils.add_parents(set(model.untracked))
242 tracked = staged.union(modified)
243 return [p for p in selection
244 if p not in untracked or p in tracked]
246 def _create_action(self, name, tooltip, slot, shortcut):
247 """Create an action with a shortcut, tooltip, and callback slot."""
248 action = QtGui.QAction(self.tr(name), self)
249 action.setStatusTip(self.tr(tooltip))
250 if hasattr(Qt, 'WidgetWithChildrenShortcut'):
251 action.setShortcutContext(Qt.WidgetWithChildrenShortcut)
252 action.setShortcut(shortcut)
253 self.addAction(action)
254 qtutils.connect_action(action, slot)
255 return action
257 def view_history(self):
258 """Signal that we should view history for paths."""
259 self.emit(SIGNAL('history(QStringList)'), self.selected_paths())
261 def stage_selected(self):
262 """Signal that we should stage selected paths."""
263 cola.notifier().broadcast(signals.stage,
264 self.selected_unstaged_paths())
266 def unstage_selected(self):
267 """Signal that we should stage selected paths."""
268 cola.notifier().broadcast(signals.unstage,
269 self.selected_staged_paths())
271 def difftool(self):
272 """Signal that we should launch difftool on a path."""
273 cola.notifier().broadcast(signals.difftool,
274 False,
275 self.selected_tracked_paths())
277 def difftool_predecessor(self):
278 """Diff paths against previous versions."""
279 paths = self.selected_tracked_paths()
280 self.emit(SIGNAL('difftool_predecessor'), paths)
282 def revert(self):
283 """Signal that we should revert changes to a path."""
284 if not qtutils.confirm('Revert Uncommitted Changes?',
285 'This operation drops uncommitted changes.'
286 '\nThese changes cannot be recovered.',
287 'Revert the uncommitted changes?',
288 'Revert Uncommitted Changes',
289 default=False,
290 icon=qtutils.icon('undo.svg')):
291 return
292 paths = self.selected_tracked_paths()
293 cola.notifier().broadcast(signals.checkout,
294 ['HEAD', '--'] + paths)
296 def editor(self):
297 """Signal that we should edit selected paths using an external editor."""
298 cola.notifier().broadcast(signals.edit, self.selected_paths())
300 def current_path(self):
301 """Return the path for the current item."""
302 index = self.currentIndex()
303 if not index.isValid():
304 return None
305 return self.item_from_index(index).path