3 from PyQt4
import QtGui
4 from PyQt4
.QtCore
import SIGNAL
7 from cola
import signals
8 from cola
import qtutils
10 from cola
.compat
import set
11 from cola
.qtutils
import SLOT
15 def widget(parent
=None):
18 _widget
= StatusWidget(parent
)
22 class StatusWidget(QtGui
.QWidget
):
24 Provides a git-status-like repository widget.
26 This widget observes the main model and broadcasts
38 # Read-only access to the mode state
39 mode
= property(lambda self
: self
.model
.mode
)
41 def __init__(self
, parent
=None):
42 QtGui
.QWidget
.__init
__(self
, parent
)
44 self
.layout
= QtGui
.QVBoxLayout(self
)
45 self
.setLayout(self
.layout
)
47 self
.tree
= QtGui
.QTreeWidget(self
)
48 self
.layout
.addWidget(self
.tree
)
49 self
.layout
.setContentsMargins(0, 0, 0, 0)
51 self
.tree
.setSelectionMode(QtGui
.QAbstractItemView
.ExtendedSelection
)
52 self
.tree
.headerItem().setHidden(True)
53 self
.tree
.setAllColumnsShowFocus(True)
54 self
.tree
.setSortingEnabled(False)
56 self
.add_item('Staged', 'plus.png')
57 self
.add_item('Modified', 'modified.png')
58 self
.add_item('Unmerged', 'unmerged.png')
59 self
.add_item('Untracked', 'untracked.png')
61 # Used to restore the selection
62 self
.old_selection
= None
64 # Handle these events here
65 self
.tree
.contextMenuEvent
= self
.tree_context_menu_event
66 self
.tree
.mousePressEvent
= self
.tree_click
68 self
.expanded_items
= set()
69 self
.model
= cola
.model()
70 self
.model
.add_message_observer(self
.model
.message_about_to_update
,
72 self
.model
.add_message_observer(self
.model
.message_updated
,
74 self
.connect(self
.tree
, SIGNAL('itemSelectionChanged()'),
76 self
.connect(self
.tree
,
77 SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'),
78 self
.tree_doubleclick
)
80 def add_item(self
, txt
, path
):
81 """Create a new top-level item in the status tree."""
82 item
= QtGui
.QTreeWidgetItem(self
.tree
)
83 item
.setText(0, self
.tr(txt
))
84 item
.setIcon(0, qtutils
.icon(path
))
86 def restore_selection(self
):
87 if not self
.old_selection
:
89 (staged
, modified
, unmerged
, untracked
) = self
.old_selection
91 # unstaged is an aggregate
92 unstaged
= modified
+ unmerged
+ untracked
94 updated_staged
= self
.model
.staged
95 updated_modified
= self
.model
.modified
96 updated_unmerged
= self
.model
.unmerged
97 updated_untracked
= self
.model
.untracked
98 # unstaged is an aggregate
99 updated_unstaged
= (updated_modified
+
103 # Updating the status resets the repo status tree so
104 # restore the selected items and re-run the diff
107 if mode
== self
.model
.mode_worktree
:
108 # Update unstaged items
110 for item
in unstaged
:
111 if item
in updated_unstaged
:
112 idx
= updated_unstaged
.index(item
)
113 item
= self
.unstaged_item(idx
)
116 item
.setSelected(True)
117 self
.tree
.setCurrentItem(item
)
118 self
.tree
.setItemSelected(item
, True)
120 elif mode
in (self
.model
.mode_index
, self
.model
.mode_amend
):
121 # Ditto for staged items
124 if item
in updated_staged
:
125 idx
= updated_staged
.index(item
)
126 item
= self
.staged_item(idx
)
129 item
.setSelected(True)
130 self
.tree
.setCurrentItem(item
)
131 self
.tree
.setItemSelected(item
, True)
134 def staged_item(self
, itemidx
):
135 return self
._subtree
_item
(self
.idx_staged
, itemidx
)
137 def modified_item(self
, itemidx
):
138 return self
._subtree
_item
(self
.idx_modified
, itemidx
)
140 def unstaged_item(self
, itemidx
):
143 item
= tree
.topLevelItem(self
.idx_modified
)
144 count
= item
.childCount()
146 return item
.child(itemidx
)
148 item
= tree
.topLevelItem(self
.idx_unmerged
)
149 count
+= item
.childCount()
151 return item
.child(itemidx
)
153 item
= tree
.topLevelItem(self
.idx_untracked
)
154 count
+= item
.childCount()
156 return item
.child(itemidx
)
160 def _subtree_item(self
, idx
, itemidx
):
161 parent
= self
.tree
.topLevelItem(idx
)
162 return parent
.child(itemidx
)
164 def about_to_update(self
):
165 self
.old_selection
= self
.selection()
168 """Update display from model data."""
169 self
.set_staged(self
.model
.staged
)
170 self
.set_modified(self
.model
.modified
)
171 self
.set_unmerged(self
.model
.unmerged
)
172 self
.set_untracked(self
.model
.untracked
)
174 self
.restore_selection()
176 if not self
.model
.staged
:
178 staged
= self
.tree
.topLevelItem(self
.idx_staged
)
179 if self
.mode
in self
.model
.modes_read_only
:
180 staged
.setText(0, self
.tr('Changed'))
182 staged
.setText(0, self
.tr('Staged'))
184 def set_staged(self
, items
):
185 """Adds items to the 'Staged' subtree."""
186 self
._set
_subtree
(items
, self
.idx_staged
, staged
=True,
187 check
=not self
.model
.read_only())
189 def set_modified(self
, items
):
190 """Adds items to the 'Modified' subtree."""
191 self
._set
_subtree
(items
, self
.idx_modified
)
193 def set_unmerged(self
, items
):
194 """Adds items to the 'Unmerged' subtree."""
195 self
._set
_subtree
(items
, self
.idx_unmerged
)
197 def set_untracked(self
, items
):
198 """Adds items to the 'Untracked' subtree."""
199 self
._set
_subtree
(items
, self
.idx_untracked
)
201 def _set_subtree(self
, items
, idx
,
205 """Add a list of items to a treewidget item."""
206 parent
= self
.tree
.topLevelItem(idx
)
208 self
.tree
.setItemHidden(parent
, False)
210 self
.tree
.setItemHidden(parent
, True)
211 parent
.takeChildren()
213 treeitem
= qtutils
.create_treeitem(item
,
217 parent
.addChild(treeitem
)
218 self
.expand_items(idx
, items
)
220 def expand_items(self
, idx
, items
):
221 """Expand the top-level category "folder" once and only once."""
222 # Don't do this if items is empty; this makes it so that we
223 # don't add the top-level index into the expanded_items set
224 # until an item appears in a particular category.
227 # Only run this once; we don't want to re-expand items that
228 # we've clicked on to re-collapse on updated().
229 if idx
in self
.expanded_items
:
231 self
.expanded_items
.add(idx
)
232 item
= self
.tree
.topLevelItem(idx
)
234 self
.tree
.expandItem(item
)
236 def tree_context_menu_event(self
, event
):
237 """Create context menus for the repo status tree."""
238 menu
= self
.tree_context_menu_setup()
239 menu
.exec_(self
.tree
.mapToGlobal(event
.pos()))
241 def tree_context_menu_setup(self
):
242 """Set up the status menu for the repo status tree."""
243 staged
, modified
, unmerged
, untracked
= self
.selection()
244 menu
= QtGui
.QMenu(self
)
246 if staged
and staged
[0] in cola
.model().submodules
:
247 menu
.addAction(self
.tr('Unstage Selected'),
248 SLOT(signals
.unstage
, self
.staged()))
249 menu
.addAction(self
.tr('Launch git-cola'),
250 SLOT(signals
.open_repo
, os
.path
.abspath(staged
[0])))
253 menu
.addAction(self
.tr('Unstage Selected'),
254 SLOT(signals
.unstage
, self
.staged()))
256 menu
.addAction(self
.tr('Launch Editor'),
257 SLOT(signals
.edit
, self
.staged()))
258 menu
.addAction(self
.tr('Launch Diff Tool'),
259 SLOT(signals
.difftool
, True, self
.staged()))
261 menu
.addAction(self
.tr('Remove Unstaged Edits'),
262 lambda: self
._remove
_unstaged
_edits
(use_staged
=True))
266 if not utils
.is_broken():
267 menu
.addAction(self
.tr('Launch Merge Tool'),
268 SLOT(signals
.mergetool
, self
.unmerged()))
269 menu
.addAction(self
.tr('Launch Editor'),
270 SLOT(signals
.edit
, self
.unmerged()))
272 menu
.addAction(self
.tr('Stage Selected'),
273 SLOT(signals
.stage
, self
.unmerged()))
276 modified_submodule
= (modified
and
277 modified
[0] in cola
.model().submodules
)
278 enable_staging
= self
.model
.enable_staging()
280 menu
.addAction(self
.tr('Stage Selected'),
281 SLOT(signals
.stage
, self
.unstaged()))
284 if modified_submodule
:
285 menu
.addAction(self
.tr('Launch git-cola'),
286 SLOT(signals
.open_repo
,
287 os
.path
.abspath(modified
[0])))
289 menu
.addAction(self
.tr('Launch Editor'),
290 SLOT(signals
.edit
, self
.unstaged()))
292 if modified
and enable_staging
and not modified_submodule
:
293 menu
.addAction(self
.tr('Launch Diff Tool'),
294 SLOT(signals
.difftool
, False, self
.modified()))
296 menu
.addAction(self
.tr('Remove Unstaged Edits'),
297 self
._remove
_unstaged
_edits
)
298 menu
.addAction(self
.tr('Remove Uncommited Edits'),
299 self
._remove
_uncommitted
_edits
)
303 menu
.addAction(self
.tr('Delete File(s)'),
304 SLOT(signals
.delete
, self
.untracked()))
308 def _remove_unstaged_edits(self
, use_staged
=False):
309 if not self
.model
.undoable():
312 items_to_undo
= self
.staged()
314 items_to_undo
= self
.modified()
317 if not qtutils
.question(self
,
318 'Remove Unstaged Edits?',
319 'This operation removes '
321 'There\'s no going back. Continue?',
324 cola
.notifier().broadcast(signals
.checkout
,
325 ['--'] + items_to_undo
)
327 qtutils
.log(1, self
.tr('No files selected for '
328 'checkout from HEAD.'))
330 def _remove_uncommitted_edits(self
):
331 if not self
.model
.undoable():
333 items_to_undo
= self
.modified()
335 if not qtutils
.question(self
,
336 'Remove Uncommitted edits?',
337 'This operation removes '
338 'uncommitted edits.\n'
339 'There\'s no going back. Continue?',
342 cola
.notifier().broadcast(signals
.checkout
,
343 ['HEAD', '--'] + items_to_undo
)
345 qtutils
.log(1, self
.tr('No files selected for '
346 'checkout from HEAD.'))
348 def single_selection(self
):
349 """Scan across staged, modified, etc. and return a single item."""
350 staged
, modified
, unmerged
, untracked
= self
.selection()
365 def selected_indexes(self
):
366 """Returns a list of (category, row) representing the tree selection."""
367 selected
= self
.tree
.selectedIndexes()
370 if idx
.parent().isValid():
371 parent_idx
= idx
.parent()
372 entry
= (parent_idx
.row(), idx
.row())
374 entry
= (-1, idx
.row())
379 """Return the current selection in the repo status tree."""
380 return (self
.staged(), self
.modified(),
381 self
.unmerged(), self
.untracked())
384 return self
._subtree
_selection
(self
.idx_staged
, self
.model
.staged
)
387 return self
.modified() + self
.unmerged() + self
.untracked()
390 return self
._subtree
_selection
(self
.idx_modified
, self
.model
.modified
)
393 return self
._subtree
_selection
(self
.idx_unmerged
, self
.model
.unmerged
)
396 return self
._subtree
_selection
(self
.idx_untracked
, self
.model
.untracked
)
398 def _subtree_selection(self
, idx
, items
):
399 item
= self
.tree
.topLevelItem(idx
)
400 return qtutils
.tree_selection(item
, items
)
402 def tree_click(self
, event
):
404 Called when a repo status tree item is clicked.
406 This handles the behavior where clicking on the icon invokes
407 the same appropriate action.
410 result
= QtGui
.QTreeWidget
.mousePressEvent(self
.tree
, event
)
412 # Sync the selection model
413 s
, m
, um
, ut
= self
.selection()
414 cola
.selection_model().set_selection(s
, m
, um
, ut
)
416 # Get the item that was clicked
417 item
= self
.tree
.itemAt(event
.pos())
419 # Nothing was clicked -- reset the display and return
420 cola
.notifier().broadcast(signals
.reset_mode
)
421 items
= self
.tree
.selectedItems()
422 self
.tree
.blockSignals(True)
425 self
.tree
.blockSignals(False)
428 # An item was clicked -- get its index in the model
429 staged
, idx
= self
.index_for_item(item
)
430 if idx
== self
.idx_header
:
433 if self
.model
.read_only():
436 # Handle when the icons are clicked
437 # TODO query Qt for the event position relative to the icon.
438 xpos
= event
.pos().x()
439 if xpos
> 45 and xpos
< 59:
441 cola
.notifier().broadcast(signals
.unstage
, self
.staged())
443 cola
.notifier().broadcast(signals
.stage
, self
.unstaged())
446 def tree_doubleclick(self
, item
, column
):
447 """Called when an item is double-clicked in the repo status tree."""
448 if self
.model
.read_only():
450 staged
, modified
, unmerged
, untracked
= self
.selection()
452 cola
.notifier().broadcast(signals
.unstage
, staged
)
454 cola
.notifier().broadcast(signals
.stage
, modified
)
456 cola
.notifier().broadcast(signals
.stage
, untracked
)
458 cola
.notifier().broadcast(signals
.stage
, unmerged
)
460 def tree_selection(self
):
461 """Show a data for the selected item."""
462 # Sync the selection model
463 s
, m
, um
, ut
= self
.selection()
464 cola
.selection_model().set_selection(s
, m
, um
, ut
)
466 selection
= self
.selected_indexes()
469 category
, idx
= selection
[0]
470 # A header item e.g. 'Staged', 'Modified', etc.
471 if category
== self
.idx_header
:
473 self
.idx_staged
: signals
.staged_summary
,
474 self
.idx_modified
: signals
.modified_summary
,
475 self
.idx_unmerged
: signals
.unmerged_summary
,
476 self
.idx_untracked
: signals
.untracked_summary
,
477 }.get(idx
, signals
.diffstat
)
478 cola
.notifier().broadcast(signal
)
480 elif category
== self
.idx_staged
:
481 cola
.notifier().broadcast(signals
.diff_staged
, self
.staged())
484 elif category
== self
.idx_modified
:
485 cola
.notifier().broadcast(signals
.diff
, self
.modified())
487 elif category
== self
.idx_unmerged
:
488 cola
.notifier().broadcast(signals
.diff
, self
.unmerged())
490 elif category
== self
.idx_untracked
:
491 cola
.notifier().broadcast(signals
.show_untracked
, self
.unstaged())
493 def index_for_item(self
, item
):
495 Given an item, returns the index of the item.
497 The indexes for unstaged items are grouped such that
498 the index of unmerged[1] = len(modified) + 1, etc.
504 parent
= item
.parent()
508 pidx
= self
.tree
.indexOfTopLevelItem(parent
)
509 if pidx
== self
.idx_staged
:
510 return True, parent
.indexOfChild(item
)
511 elif pidx
== self
.idx_modified
:
512 return False, parent
.indexOfChild(item
)
514 count
= self
.tree
.topLevelItem(self
.idx_modified
).childCount()
515 if pidx
== self
.idx_unmerged
:
516 return False, count
+ parent
.indexOfChild(item
)
518 count
+= self
.tree
.topLevelItem(self
.idx_unmerged
).childCount()
519 if pidx
== self
.idx_untracked
:
520 return False, count
+ parent
.indexOfChild(item
)