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 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
)
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' % (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
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
,
90 self
.action_difftool
=\
91 self
._create
_action
('View Diff...',
92 'Launch git-difftool on the current path.',
95 self
.action_difftool_predecessor
=\
96 self
._create
_action
('Diff Against Predecessor...',
97 'Launch git-difftool against previous versions.',
98 self
.difftool_predecessor
,
100 self
.action_revert
=\
101 self
._create
_action
('Revert Uncommitted Changes...',
102 'Revert changes to selected path(s).',
105 self
.action_editor
=\
106 self
._create
_action
('Launch Editor',
107 'Edit selected path(s).',
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
)
139 menu
.addAction(self
.action_history
)
140 menu
.addAction(self
.action_difftool
)
141 menu
.addAction(self
.action_difftool_predecessor
)
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()
152 def sync_selection(self
):
153 """Push selection into the selection model."""
158 state
= State(staged
, unmerged
, modified
, untracked
)
160 paths
= self
.selected_paths()
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
))
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
:
174 elif path
in model_modified
:
175 modified
.append(path
)
178 # Push the new selection into the model.
179 cola
.selection_model().set_selection(state
)
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
)
193 def setModel(self
, model
):
194 """Set the concrete QAbstractItemModel instance."""
195 QtGui
.QTreeView
.setModel(self
, model
)
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."""
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."""
219 selection
= self
.selected_paths()
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."""
227 selection
= self
.selected_paths()
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."""
237 selection
= self
.selected_paths()
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
)
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())
272 """Signal that we should launch difftool on a path."""
273 cola
.notifier().broadcast(signals
.difftool
,
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
)
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',
290 icon
=qtutils
.icon('undo.svg')):
292 paths
= self
.selected_tracked_paths()
293 cola
.notifier().broadcast(signals
.checkout
,
294 ['HEAD', '--'] + paths
)
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():
305 return self
.item_from_index(index
).path