3 from PyQt4
import QtGui
4 from PyQt4
.QtCore
import Qt
5 from PyQt4
.QtCore
import SIGNAL
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
)
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
)
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
45 msg
= 'Repository: %s\nBranch: %s' % (curdir
, branch
)
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
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).',
80 self
._create
_action
('Stage Selected',
81 'Stage selected path(s) for commit.',
84 self
.action_unstage
=\
85 self
._create
_action
('Unstage Selected',
86 'Remove selected path(s) from '
88 self
.unstage_selected
,
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.',
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
,
106 self
.action_revert
=\
107 self
._create
_action
('Revert Uncommitted Changes...',
108 'Revert changes to selected path(s).',
111 self
.action_editor
=\
112 self
._create
_action
('Launch Editor',
113 'Edit selected path(s).',
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
)
146 menu
.addAction(self
.action_history
)
147 menu
.addAction(self
.action_difftool
)
148 menu
.addAction(self
.action_difftool_predecessor
)
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()
160 def sync_selection(self
):
161 """Push selection into the selection model."""
166 state
= State(staged
, unmerged
, modified
, untracked
)
168 paths
= self
.selected_paths()
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
))
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
:
182 elif path
in model_modified
:
183 modified
.append(path
)
186 # Push the new selection into the model.
187 cola
.selection_model().set_selection(state
)
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
)
201 def setModel(self
, model
):
202 """Set the concrete QAbstractItemModel instance."""
203 QtGui
.QTreeView
.setModel(self
, model
)
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."""
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."""
227 selection
= self
.selected_paths()
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."""
235 selection
= self
.selected_paths()
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."""
245 selection
= self
.selected_paths()
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
)
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())
286 """Signal that we should launch difftool on a path."""
287 cola
.notifier().broadcast(signals
.difftool
,
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
)
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',
304 icon
=qtutils
.icon('undo.svg')):
306 paths
= self
.selected_tracked_paths()
307 cola
.notifier().broadcast(signals
.checkout
,
308 ['HEAD', '--'] + paths
)
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():
319 return self
.item_from_index(index
).path