3 from PyQt4
import QtGui
4 from PyQt4
import QtCore
5 from PyQt4
.QtCore
import Qt
6 from PyQt4
.QtCore
import SIGNAL
9 from cola
import qtutils
10 from cola
import signals
11 from cola
import utils
12 from cola
.models
.selection
import State
13 from cola
.widgets
import defs
14 from cola
.widgets
import standard
15 from cola
.classic
.model
import GitRepoNameItem
18 class RepoDialog(standard
.Dialog
):
19 def __init__(self
, parent
, update
=True):
20 standard
.Dialog
.__init
__(self
, parent
)
21 self
.setObjectName('classic')
22 self
.tree
= RepoTreeView(self
)
23 self
.mainlayout
= QtGui
.QHBoxLayout()
24 self
.setLayout(self
.mainlayout
)
25 self
.mainlayout
.setMargin(0)
26 self
.mainlayout
.setSpacing(defs
.spacing
)
27 self
.mainlayout
.addWidget(self
.tree
)
30 self
.connect(self
, SIGNAL('updated'), self
._updated
_callback
)
31 self
.model
= cola
.model()
32 self
.model
.add_observer(self
.model
.message_updated
, self
.model_updated
)
33 qtutils
.add_close_action(self
)
37 # Read-only mode property
38 mode
= property(lambda self
: self
.model
.mode
)
40 def model_updated(self
):
41 """Update the title with the current branch and directory name."""
42 self
.emit(SIGNAL('updated'))
44 def _updated_callback(self
):
45 branch
= self
.model
.currentbranch
47 msg
= 'Repository: %s\nBranch: %s' % (curdir
, branch
)
51 title
= '%s [%s]' % (self
.model
.project
, branch
)
52 if self
.mode
in (self
.model
.mode_diff
, self
.model
.mode_diff_expr
):
53 title
+= ' *** diff mode***'
54 elif self
.mode
== self
.model
.mode_amend
:
55 title
+= ' *** amending ***'
56 self
.setWindowTitle(title
)
59 class RepoTreeView(QtGui
.QTreeView
):
60 """Provides a filesystem-like view of a git repository."""
61 def __init__(self
, parent
):
62 QtGui
.QTreeView
.__init
__(self
, parent
)
64 self
.setSortingEnabled(False)
65 self
.setAllColumnsShowFocus(True)
66 self
.setAlternatingRowColors(True)
67 self
.setUniformRowHeights(True)
68 self
.setAnimated(True)
69 self
.setSelectionMode(QtGui
.QAbstractItemView
.ExtendedSelection
)
71 # Observe model updates
73 model
.add_observer(model
.message_updated
, self
.update_actions
)
75 # The non-Qt cola application model
76 self
.connect(self
, SIGNAL('expanded(QModelIndex)'), self
.size_columns
)
77 self
.connect(self
, SIGNAL('collapsed(QModelIndex)'), self
.size_columns
)
78 self
.action_history
=\
79 self
._create
_action
('View History...',
80 'View history for selected path(s).',
84 self
._create
_action
('Stage Selected',
85 'Stage selected path(s) for commit.',
88 self
.action_unstage
=\
89 self
._create
_action
('Unstage Selected',
90 'Remove selected path(s) from '
92 self
.unstage_selected
,
94 self
.action_difftool
=\
95 self
._create
_action
('View Diff...',
96 'Launch git-difftool on the current path.',
99 self
.action_difftool_predecessor
=\
100 self
._create
_action
('Diff Against Predecessor...',
101 'Launch git-difftool against previous versions.',
102 self
.difftool_predecessor
,
104 self
.action_revert
=\
105 self
._create
_action
('Revert Uncommitted Changes...',
106 'Revert changes to selected path(s).',
109 self
.action_editor
=\
110 self
._create
_action
('Launch Editor',
111 'Edit selected path(s).',
115 def size_columns(self
):
116 """Set the column widths."""
117 self
.resizeColumnToContents(0)
119 def update_actions(self
):
120 """Enable/disable actions."""
121 selection
= self
.selected_paths()
122 selected
= bool(selection
)
123 staged
= bool(self
.selected_staged_paths(selection
=selection
))
124 modified
= bool(self
.selected_modified_paths(selection
=selection
))
125 unstaged
= bool(self
.selected_unstaged_paths(selection
=selection
))
126 tracked
= bool(self
.selected_tracked_paths())
128 self
.action_history
.setEnabled(selected
)
129 self
.action_stage
.setEnabled(unstaged
)
130 self
.action_unstage
.setEnabled(staged
)
131 self
.action_difftool
.setEnabled(staged
or modified
)
132 self
.action_difftool_predecessor
.setEnabled(tracked
)
133 self
.action_revert
.setEnabled(tracked
)
135 def contextMenuEvent(self
, event
):
136 """Create a context menu."""
137 self
.update_actions()
138 menu
= QtGui
.QMenu(self
)
139 menu
.addAction(self
.action_editor
)
140 menu
.addAction(self
.action_stage
)
141 menu
.addAction(self
.action_unstage
)
143 menu
.addAction(self
.action_history
)
144 menu
.addAction(self
.action_difftool
)
145 menu
.addAction(self
.action_difftool_predecessor
)
147 menu
.addAction(self
.action_revert
)
148 menu
.exec_(self
.mapToGlobal(event
.pos()))
150 def keyPressEvent(self
, event
):
152 Override keyPressEvent to allow LeftArrow to work on non-directories.
154 When LeftArrow is pressed on a file entry or an unexpanded directory,
155 then move the current index to the parent directory.
157 This simplifies navigation using the keyboard.
158 For power-users, we support Vim keybindings ;-P
161 # Check whether the item is expanded before calling the base class
162 # keyPressEvent otherwise we end up collapsing and changing the
163 # current index in one shot, which we don't want to do.
164 index
= self
.currentIndex()
165 was_expanded
= self
.isExpanded(index
)
166 was_collapsed
= not was_expanded
169 # Rewrite the event before marshalling to QTreeView.event()
172 # Remap 'H' to 'Left'
173 if key
== QtCore
.Qt
.Key_H
:
174 event
= QtGui
.QKeyEvent(event
.type(),
177 # Remap 'J' to 'Down'
178 elif key
== QtCore
.Qt
.Key_J
:
179 event
= QtGui
.QKeyEvent(event
.type(),
183 elif key
== QtCore
.Qt
.Key_K
:
184 event
= QtGui
.QKeyEvent(event
.type(),
187 # Remap 'L' to 'Right'
188 elif key
== QtCore
.Qt
.Key_L
:
189 event
= QtGui
.QKeyEvent(event
.type(),
193 # Re-read the event key to take the remappings into account
196 # Process the keyPressEvent before changing the current index
197 # otherwise the event will affect the new index set here
198 # instead of the original index.
199 result
= QtGui
.QTreeView
.keyPressEvent(self
, event
)
201 # Sync the selection model
202 self
.sync_selection()
204 # Try to select the first item if the model index is invalid
205 if not index
.isValid():
206 index
= self
.model().index(0, 0, QtCore
.QModelIndex())
208 self
.setCurrentIndex(index
)
211 # Automatically select the first entry when expanding a directory
212 if (key
== QtCore
.Qt
.Key_Right
and was_collapsed
and
213 self
.isExpanded(index
)):
214 index
= self
.moveCursor(self
.MoveDown
, event
.modifiers())
215 self
.setCurrentIndex(index
)
217 # Process non-root entries with valid parents only.
218 if key
== QtCore
.Qt
.Key_Left
and index
.parent().isValid():
220 # File entries have rowCount() == 0
221 if self
.item_from_index(index
).rowCount() == 0:
222 self
.setCurrentIndex(index
.parent())
224 # Otherwise, do this for collapsed directories only
226 self
.setCurrentIndex(index
.parent())
230 def mousePressEvent(self
, event
):
231 """Synchronize the selection on mouse-press."""
232 result
= QtGui
.QTreeView
.mousePressEvent(self
, event
)
233 self
.sync_selection()
236 def sync_selection(self
):
237 """Push selection into the selection model."""
242 state
= State(staged
, unmerged
, modified
, untracked
)
244 paths
= self
.selected_paths()
246 model_staged
= utils
.add_parents(set(model
.staged
))
247 model_modified
= utils
.add_parents(set(model
.modified
))
248 model_unmerged
= utils
.add_parents(set(model
.unmerged
))
249 model_untracked
= utils
.add_parents(set(model
.untracked
))
252 if path
in model_unmerged
:
253 unmerged
.append(path
)
254 elif path
in model_untracked
:
255 untracked
.append(path
)
256 elif path
in model_staged
:
258 elif path
in model_modified
:
259 modified
.append(path
)
262 # Push the new selection into the model.
263 cola
.selection_model().set_selection(state
)
266 def selectionChanged(self
, old_selection
, new_selection
):
267 """Override selectionChanged to update available actions."""
268 result
= QtGui
.QTreeView
.selectionChanged(self
, old_selection
, new_selection
)
269 self
.update_actions()
270 paths
= self
.sync_selection()
272 if paths
and self
.model().path_is_interesting(paths
[0]):
273 cached
= paths
[0] in cola
.model().staged
274 cola
.notifier().broadcast(signals
.diff
, paths
, cached
)
277 def setModel(self
, model
):
278 """Set the concrete QAbstractItemModel instance."""
279 QtGui
.QTreeView
.setModel(self
, model
)
282 def item_from_index(self
, model_index
):
283 """Return the name item corresponding to the model index."""
284 index
= model_index
.sibling(model_index
.row(), 0)
285 return self
.model().itemFromIndex(index
)
287 def selected_paths(self
):
288 """Return the selected paths."""
289 items
= map(self
.model().itemFromIndex
, self
.selectedIndexes())
290 return [i
.path
for i
in items
291 if i
.type() == GitRepoNameItem
.TYPE
]
293 def selected_staged_paths(self
, selection
=None):
294 """Return selected staged paths."""
296 selection
= self
.selected_paths()
297 staged
= utils
.add_parents(set(cola
.model().staged
))
298 return [p
for p
in selection
if p
in staged
]
300 def selected_modified_paths(self
, selection
=None):
301 """Return selected modified paths."""
303 selection
= self
.selected_paths()
305 modified
= utils
.add_parents(set(model
.modified
))
306 return [p
for p
in selection
if p
in modified
]
308 def selected_unstaged_paths(self
, selection
=None):
309 """Return selected unstaged paths."""
311 selection
= self
.selected_paths()
313 modified
= utils
.add_parents(set(model
.modified
))
314 untracked
= utils
.add_parents(set(model
.untracked
))
315 unstaged
= modified
.union(untracked
)
316 return [p
for p
in selection
if p
in unstaged
]
318 def selected_tracked_paths(self
, selection
=None):
319 """Return selected tracked paths."""
321 selection
= self
.selected_paths()
323 staged
= set(self
.selected_staged_paths())
324 modified
= set(self
.selected_modified_paths())
325 untracked
= utils
.add_parents(set(model
.untracked
))
326 tracked
= staged
.union(modified
)
327 return [p
for p
in selection
328 if p
not in untracked
or p
in tracked
]
330 def _create_action(self
, name
, tooltip
, slot
, shortcut
):
331 """Create an action with a shortcut, tooltip, and callback slot."""
332 action
= QtGui
.QAction(self
.tr(name
), self
)
333 action
.setStatusTip(self
.tr(tooltip
))
334 if hasattr(Qt
, 'WidgetWithChildrenShortcut'):
335 action
.setShortcutContext(Qt
.WidgetWithChildrenShortcut
)
336 action
.setShortcut(shortcut
)
337 self
.addAction(action
)
338 qtutils
.connect_action(action
, slot
)
341 def view_history(self
):
342 """Signal that we should view history for paths."""
343 self
.emit(SIGNAL('history(QStringList)'), self
.selected_paths())
345 def stage_selected(self
):
346 """Signal that we should stage selected paths."""
347 cola
.notifier().broadcast(signals
.stage
,
348 self
.selected_unstaged_paths())
350 def unstage_selected(self
):
351 """Signal that we should stage selected paths."""
352 cola
.notifier().broadcast(signals
.unstage
,
353 self
.selected_staged_paths())
356 """Signal that we should launch difftool on a path."""
357 cola
.notifier().broadcast(signals
.difftool
,
359 self
.selected_tracked_paths())
361 def difftool_predecessor(self
):
362 """Diff paths against previous versions."""
363 paths
= self
.selected_tracked_paths()
364 self
.emit(SIGNAL('difftool_predecessor'), paths
)
367 """Signal that we should revert changes to a path."""
368 if not qtutils
.confirm('Revert Uncommitted Changes?',
369 'This operation drops uncommitted changes.'
370 '\nThese changes cannot be recovered.',
371 'Revert the uncommitted changes?',
372 'Revert Uncommitted Changes',
374 icon
=qtutils
.icon('undo.svg')):
376 paths
= self
.selected_tracked_paths()
377 cola
.notifier().broadcast(signals
.checkout
,
378 ['HEAD', '--'] + paths
)
381 """Signal that we should edit selected paths using an external editor."""
382 cola
.notifier().broadcast(signals
.edit
, self
.selected_paths())
384 def current_path(self
):
385 """Return the path for the current item."""
386 index
= self
.currentIndex()
387 if not index
.isValid():
389 return self
.item_from_index(index
).path