1 from __future__
import division
, absolute_import
, unicode_literals
5 from PyQt4
import QtCore
6 from PyQt4
import QtGui
7 from PyQt4
.QtCore
import Qt
8 from PyQt4
.QtCore
import SIGNAL
12 from cola
import qtutils
13 from cola
import utils
14 from cola
.i18n
import N_
15 from cola
.models
import main
16 from cola
.models
import selection
17 from cola
.widgets
import completion
18 from cola
.widgets
import defs
21 class StatusWidget(QtGui
.QWidget
):
23 Provides a git-status-like repository widget.
25 This widget observes the main model and broadcasts
29 def __init__(self
, titlebar
, parent
=None):
30 QtGui
.QWidget
.__init
__(self
, parent
)
32 tooltip
= N_('Toggle the paths filter')
33 self
.filter_button
= qtutils
.create_action_button(
35 icon
=qtutils
.filter_icon())
37 self
.filter_widget
= StatusFilterWidget()
38 self
.filter_widget
.hide()
39 self
.tree
= StatusTreeWidget()
40 self
.setFocusProxy(self
.tree
)
42 self
.main_layout
= qtutils
.vbox(defs
.no_margin
, defs
.no_spacing
,
43 self
.filter_widget
, self
.tree
)
44 self
.setLayout(self
.main_layout
)
46 self
.toggle_action
= qtutils
.add_action(self
, tooltip
,
47 self
.toggle_filter
, 'Shift+Ctrl+F')
49 titlebar
.add_corner_widget(self
.filter_button
)
50 qtutils
.connect_button(self
.filter_button
, self
.toggle_filter
)
52 def toggle_filter(self
):
53 shown
= not self
.filter_widget
.isVisible()
54 self
.filter_widget
.setVisible(shown
)
56 self
.filter_widget
.setFocus(True)
58 self
.tree
.setFocus(True)
60 def set_initial_size(self
):
61 self
.setMaximumWidth(222)
62 QtCore
.QTimer
.singleShot(1, self
.restore_size
)
64 def restore_size(self
):
65 self
.setMaximumWidth(2 ** 13)
68 self
.tree
.show_selection()
70 def set_filter(self
, txt
):
71 self
.filter_widget
.setVisible(True)
72 self
.filter_widget
.text
.set_value(txt
)
73 self
.filter_widget
.apply_filter()
82 class StatusTreeWidget(QtGui
.QTreeWidget
):
91 # Read-only access to the mode state
92 mode
= property(lambda self
: self
.m
.mode
)
94 def __init__(self
, parent
=None):
95 QtGui
.QTreeWidget
.__init
__(self
, parent
)
97 self
.setSelectionMode(QtGui
.QAbstractItemView
.ExtendedSelection
)
98 self
.headerItem().setHidden(True)
99 self
.setAllColumnsShowFocus(True)
100 self
.setSortingEnabled(False)
101 self
.setUniformRowHeights(True)
102 self
.setAnimated(True)
103 self
.setRootIsDecorated(False)
104 self
.setIndentation(0)
105 self
.setDragEnabled(True)
107 self
.add_item(N_('Staged'), hide
=True)
108 self
.add_item(N_('Unmerged'), hide
=True)
109 self
.add_item(N_('Modified'), hide
=True)
110 self
.add_item(N_('Untracked'), hide
=True)
112 # Used to restore the selection
113 self
.old_scroll
= None
114 self
.old_selection
= None
115 self
.old_contents
= None
116 self
.old_current_item
= None
117 self
.expanded_items
= set()
119 self
.process_selection_action
= qtutils
.add_action(self
,
120 cmds
.StageOrUnstage
.name(),
121 cmds
.run(cmds
.StageOrUnstage
),
122 cmds
.StageOrUnstage
.SHORTCUT
)
124 self
.revert_unstaged_edits_action
= qtutils
.add_action(self
,
125 cmds
.RevertUnstagedEdits
.name(),
126 cmds
.run(cmds
.RevertUnstagedEdits
),
127 cmds
.RevertUnstagedEdits
.SHORTCUT
)
128 self
.revert_unstaged_edits_action
.setIcon(qtutils
.theme_icon('edit-undo.svg'))
130 self
.launch_difftool_action
= qtutils
.add_action(self
,
131 cmds
.LaunchDifftool
.name(),
132 cmds
.run(cmds
.LaunchDifftool
),
133 cmds
.LaunchDifftool
.SHORTCUT
)
134 self
.launch_difftool_action
.setIcon(qtutils
.git_icon())
136 self
.launch_editor_action
= qtutils
.add_action(self
,
137 cmds
.LaunchEditor
.name(),
138 cmds
.run(cmds
.LaunchEditor
),
139 cmds
.LaunchEditor
.SHORTCUT
,
141 self
.launch_editor_action
.setIcon(qtutils
.options_icon())
143 if not utils
.is_win32():
144 self
.open_using_default_app
= qtutils
.add_action(self
,
145 cmds
.OpenDefaultApp
.name(),
146 self
._open
_using
_default
_app
,
147 cmds
.OpenDefaultApp
.SHORTCUT
)
148 self
.open_using_default_app
.setIcon(qtutils
.file_icon())
150 self
.open_parent_dir_action
= qtutils
.add_action(self
,
151 cmds
.OpenParentDir
.name(),
152 self
._open
_parent
_dir
,
153 cmds
.OpenParentDir
.SHORTCUT
)
154 self
.open_parent_dir_action
.setIcon(qtutils
.open_file_icon())
156 self
.up_action
= qtutils
.add_action(self
,
157 N_('Move Up'), self
.move_up
, Qt
.Key_K
)
159 self
.down_action
= qtutils
.add_action(self
,
160 N_('Move Down'), self
.move_down
, Qt
.Key_J
)
162 self
.copy_path_action
= qtutils
.add_action(self
,
163 N_('Copy Path to Clipboard'),
164 self
.copy_path
, QtGui
.QKeySequence
.Copy
)
165 self
.copy_path_action
.setIcon(qtutils
.theme_icon('edit-copy.svg'))
167 self
.copy_relpath_action
= qtutils
.add_action(self
,
168 N_('Copy Relative Path to Clipboard'),
169 self
.copy_relpath
, QtGui
.QKeySequence
.Cut
)
170 self
.copy_relpath_action
.setIcon(qtutils
.theme_icon('edit-copy.svg'))
172 # MoveToTrash and Delete use the same shortcut.
173 # We will only bind one of them, depending on whether or not the
174 # MoveToTrash command is avaialble. When available, the hotkey
175 # is bound to MoveToTrash, otherwise it is bound to Delete.
176 if cmds
.MoveToTrash
.AVAILABLE
:
177 self
.move_to_trash_action
= qtutils
.add_action(self
,
178 N_('Move file(s) to trash'),
179 self
._trash
_untracked
_files
, cmds
.MoveToTrash
.SHORTCUT
)
180 self
.move_to_trash_action
.setIcon(qtutils
.discard_icon())
181 delete_shortcut
= cmds
.Delete
.SHORTCUT
183 self
.move_to_trash_action
= None
184 delete_shortcut
= cmds
.Delete
.ALT_SHORTCUT
186 self
.delete_untracked_files_action
= qtutils
.add_action(self
,
187 N_('Delete File(s)...'),
188 self
._delete
_untracked
_files
, delete_shortcut
)
189 self
.delete_untracked_files_action
.setIcon(qtutils
.discard_icon())
191 self
.connect(self
, SIGNAL('about_to_update()'),
192 self
._about
_to
_update
, Qt
.QueuedConnection
)
193 self
.connect(self
, SIGNAL('updated()'),
194 self
._updated
, Qt
.QueuedConnection
)
196 self
.m
= main
.model()
197 self
.m
.add_observer(self
.m
.message_about_to_update
,
198 self
.about_to_update
)
199 self
.m
.add_observer(self
.m
.message_updated
, self
.updated
)
201 self
.connect(self
, SIGNAL('itemSelectionChanged()'),
204 self
.connect(self
, SIGNAL('itemDoubleClicked(QTreeWidgetItem*,int)'),
207 self
.connect(self
, SIGNAL('itemCollapsed(QTreeWidgetItem*)'),
208 lambda x
: self
.update_column_widths())
210 self
.connect(self
, SIGNAL('itemExpanded(QTreeWidgetItem*)'),
211 lambda x
: self
.update_column_widths())
213 def add_item(self
, txt
, hide
=False):
214 """Create a new top-level item in the status tree."""
219 item
= QtGui
.QTreeWidgetItem(self
)
220 item
.setFont(0, font
)
223 self
.setItemHidden(item
, True)
225 def restore_selection(self
):
226 if not self
.old_selection
or not self
.old_contents
:
228 old_c
= self
.old_contents
229 old_s
= self
.old_selection
230 new_c
= self
.contents()
232 def mkselect(lst
, widget_getter
):
233 def select(item
, current
=False):
234 idx
= lst
.index(item
)
235 widget
= widget_getter(idx
)
237 self
.setCurrentItem(widget
)
238 self
.setItemSelected(widget
, True)
241 select_staged
= mkselect(new_c
.staged
, self
.staged_item
)
242 select_unmerged
= mkselect(new_c
.unmerged
, self
.unmerged_item
)
243 select_modified
= mkselect(new_c
.modified
, self
.modified_item
)
244 select_untracked
= mkselect(new_c
.untracked
, self
.untracked_item
)
247 (set(new_c
.staged
), old_c
.staged
, set(old_s
.staged
),
250 (set(new_c
.unmerged
), old_c
.unmerged
, set(old_s
.unmerged
),
253 (set(new_c
.modified
), old_c
.modified
, set(old_s
.modified
),
256 (set(new_c
.untracked
), old_c
.untracked
, set(old_s
.untracked
),
260 # Restore the current item
261 if self
.old_current_item
:
262 category
, idx
= self
.old_current_item
263 if category
== self
.idx_header
:
264 item
= self
.invisibleRootItem().child(idx
)
266 self
.setCurrentItem(item
)
267 self
.setItemSelected(item
, True)
269 # Reselect the current item
270 selection_info
= saved_selection
[category
]
271 new
= selection_info
[0]
272 old
= selection_info
[1]
273 reselect
= selection_info
[3]
279 reselect(item
, current
=True)
282 # When reselecting we only care that the items are selected;
283 # we do not need to rerun the callbacks which were triggered
284 # above. Block signals to skip the callbacks.
285 self
.blockSignals(True)
286 for (new
, old
, sel
, reselect
) in saved_selection
:
289 reselect(item
, current
=False)
290 self
.blockSignals(False)
292 for (new
, old
, sel
, reselect
) in saved_selection
:
293 # When modified is staged, select the next modified item
294 # When unmerged is staged, select the next unmerged item
295 # When unstaging, select the next staged item
296 # When staging untracked files, select the next untracked item
297 if len(new
) >= len(old
):
298 # The list did not shrink so it is not one of these cases.
301 # The item still exists so ignore it
302 if item
in new
or item
not in old
:
304 # The item no longer exists in this list so search for
305 # its nearest neighbors and select them instead.
306 idx
= old
.index(item
)
307 for j
in itertools
.chain(old
[idx
+1:], reversed(old
[:idx
])):
309 reselect(j
, current
=True)
312 def restore_scrollbar(self
):
313 vscroll
= self
.verticalScrollBar()
314 if vscroll
and self
.old_scroll
is not None:
315 vscroll
.setValue(self
.old_scroll
)
316 self
.old_scroll
= None
318 def staged_item(self
, itemidx
):
319 return self
._subtree
_item
(self
.idx_staged
, itemidx
)
321 def modified_item(self
, itemidx
):
322 return self
._subtree
_item
(self
.idx_modified
, itemidx
)
324 def unmerged_item(self
, itemidx
):
325 return self
._subtree
_item
(self
.idx_unmerged
, itemidx
)
327 def untracked_item(self
, itemidx
):
328 return self
._subtree
_item
(self
.idx_untracked
, itemidx
)
330 def unstaged_item(self
, itemidx
):
332 item
= self
.topLevelItem(self
.idx_modified
)
333 count
= item
.childCount()
335 return item
.child(itemidx
)
337 item
= self
.topLevelItem(self
.idx_unmerged
)
338 count
+= item
.childCount()
340 return item
.child(itemidx
)
342 item
= self
.topLevelItem(self
.idx_untracked
)
343 count
+= item
.childCount()
345 return item
.child(itemidx
)
349 def _subtree_item(self
, idx
, itemidx
):
350 parent
= self
.topLevelItem(idx
)
351 return parent
.child(itemidx
)
353 def about_to_update(self
):
354 self
.emit(SIGNAL('about_to_update()'))
356 def _about_to_update(self
):
357 self
.save_selection()
358 self
.save_scrollbar()
360 def save_scrollbar(self
):
361 vscroll
= self
.verticalScrollBar()
363 self
.old_scroll
= vscroll
.value()
365 self
.old_scroll
= None
367 def current_item(self
):
368 s
= self
.selected_indexes()
371 current
= self
.currentItem()
374 idx
= self
.indexFromItem(current
, 0)
375 if idx
.parent().isValid():
376 parent_idx
= idx
.parent()
377 entry
= (parent_idx
.row(), idx
.row())
379 entry
= (self
.idx_header
, idx
.row())
382 def save_selection(self
):
383 self
.old_contents
= self
.contents()
384 self
.old_selection
= self
.selection()
385 self
.old_current_item
= self
.current_item()
388 """Update display from model data."""
389 self
.emit(SIGNAL('updated()'))
392 self
.set_staged(self
.m
.staged
)
393 self
.set_modified(self
.m
.modified
)
394 self
.set_unmerged(self
.m
.unmerged
)
395 self
.set_untracked(self
.m
.untracked
)
396 self
.restore_selection()
397 self
.restore_scrollbar()
398 self
.update_column_widths()
399 self
.update_actions()
401 def update_actions(self
, selected
=None):
403 selected
= selection
.selection()
404 can_revert_edits
= bool(selected
.staged
or selected
.modified
)
405 self
.revert_unstaged_edits_action
.setEnabled(can_revert_edits
)
407 def set_staged(self
, items
):
408 """Adds items to the 'Staged' subtree."""
409 self
._set
_subtree
(items
, self
.idx_staged
, staged
=True,
410 deleted_set
=self
.m
.staged_deleted
)
412 def set_modified(self
, items
):
413 """Adds items to the 'Modified' subtree."""
414 self
._set
_subtree
(items
, self
.idx_modified
,
415 deleted_set
=self
.m
.unstaged_deleted
)
417 def set_unmerged(self
, items
):
418 """Adds items to the 'Unmerged' subtree."""
419 self
._set
_subtree
(items
, self
.idx_unmerged
)
421 def set_untracked(self
, items
):
422 """Adds items to the 'Untracked' subtree."""
423 self
._set
_subtree
(items
, self
.idx_untracked
, untracked
=True)
425 def _set_subtree(self
, items
, idx
,
429 """Add a list of items to a treewidget item."""
430 self
.blockSignals(True)
431 parent
= self
.topLevelItem(idx
)
433 self
.setItemHidden(parent
, False)
435 self
.setItemHidden(parent
, True)
437 # sip v4.14.7 and below leak memory in parent.takeChildren()
438 # so we use this backwards-compatible construct instead
439 while parent
.takeChild(0) is not None:
443 deleted
= (deleted_set
is not None and item
in deleted_set
)
444 treeitem
= qtutils
.create_treeitem(item
,
448 parent
.addChild(treeitem
)
449 self
.expand_items(idx
, items
)
450 self
.blockSignals(False)
452 def update_column_widths(self
):
453 self
.resizeColumnToContents(0)
455 def expand_items(self
, idx
, items
):
456 """Expand the top-level category "folder" once and only once."""
457 # Don't do this if items is empty; this makes it so that we
458 # don't add the top-level index into the expanded_items set
459 # until an item appears in a particular category.
462 # Only run this once; we don't want to re-expand items that
463 # we've clicked on to re-collapse on updated().
464 if idx
in self
.expanded_items
:
466 self
.expanded_items
.add(idx
)
467 item
= self
.topLevelItem(idx
)
469 self
.expandItem(item
)
471 def contextMenuEvent(self
, event
):
472 """Create context menus for the repo status tree."""
473 menu
= self
.create_context_menu()
474 menu
.exec_(self
.mapToGlobal(event
.pos()))
476 def create_context_menu(self
):
477 """Set up the status menu for the repo status tree."""
479 menu
= QtGui
.QMenu(self
)
481 selected_indexes
= self
.selected_indexes()
483 category
, idx
= selected_indexes
[0]
484 # A header item e.g. 'Staged', 'Modified', etc.
485 if category
== self
.idx_header
:
486 return self
._create
_header
_context
_menu
(menu
, idx
)
489 return self
._create
_staged
_context
_menu
(menu
, s
)
492 return self
._create
_unmerged
_context
_menu
(menu
, s
)
494 return self
._create
_unstaged
_context
_menu
(menu
, s
)
496 def _create_header_context_menu(self
, menu
, idx
):
497 if idx
== self
.idx_staged
:
498 menu
.addAction(qtutils
.remove_icon(),
500 cmds
.run(cmds
.UnstageAll
))
502 elif idx
== self
.idx_unmerged
:
503 action
= menu
.addAction(qtutils
.add_icon(),
504 cmds
.StageUnmerged
.name(),
505 cmds
.run(cmds
.StageUnmerged
))
506 action
.setShortcut(cmds
.StageUnmerged
.SHORTCUT
)
508 elif idx
== self
.idx_modified
:
509 action
= menu
.addAction(qtutils
.add_icon(),
510 cmds
.StageModified
.name(),
511 cmds
.run(cmds
.StageModified
))
512 action
.setShortcut(cmds
.StageModified
.SHORTCUT
)
515 elif idx
== self
.idx_untracked
:
516 action
= menu
.addAction(qtutils
.add_icon(),
517 cmds
.StageUntracked
.name(),
518 cmds
.run(cmds
.StageUntracked
))
519 action
.setShortcut(cmds
.StageUntracked
.SHORTCUT
)
522 def _create_staged_context_menu(self
, menu
, s
):
523 if s
.staged
[0] in self
.m
.submodules
:
524 return self
._create
_staged
_submodule
_context
_menu
(menu
, s
)
526 if self
.m
.unstageable():
527 action
= menu
.addAction(qtutils
.remove_icon(),
528 N_('Unstage Selected'),
529 cmds
.run(cmds
.Unstage
, self
.staged()))
530 action
.setShortcut(cmds
.Unstage
.SHORTCUT
)
532 # Do all of the selected items exist?
533 all_exist
= all(not i
in self
.m
.staged_deleted
and core
.exists(i
)
534 for i
in self
.staged())
537 menu
.addAction(self
.launch_editor_action
)
538 menu
.addAction(self
.launch_difftool_action
)
540 if all_exist
and not utils
.is_win32():
542 action
= menu
.addAction(qtutils
.file_icon(),
543 cmds
.OpenDefaultApp
.name(),
544 cmds
.run(cmds
.OpenDefaultApp
, self
.staged()))
545 action
.setShortcut(cmds
.OpenDefaultApp
.SHORTCUT
)
547 action
= menu
.addAction(qtutils
.open_file_icon(),
548 cmds
.OpenParentDir
.name(),
549 self
._open
_parent
_dir
)
550 action
.setShortcut(cmds
.OpenParentDir
.SHORTCUT
)
552 if self
.m
.undoable():
554 menu
.addAction(self
.revert_unstaged_edits_action
)
557 menu
.addAction(self
.copy_path_action
)
558 menu
.addAction(self
.copy_relpath_action
)
561 def _create_staged_submodule_context_menu(self
, menu
, s
):
562 menu
.addAction(qtutils
.git_icon(),
563 N_('Launch git-cola'),
564 cmds
.run(cmds
.OpenRepo
,
565 core
.abspath(s
.staged
[0])))
567 menu
.addAction(self
.launch_editor_action
)
570 action
= menu
.addAction(qtutils
.remove_icon(),
571 N_('Unstage Selected'),
572 cmds
.run(cmds
.Unstage
, self
.staged()))
573 action
.setShortcut(cmds
.Unstage
.SHORTCUT
)
576 menu
.addAction(self
.copy_path_action
)
577 menu
.addAction(self
.copy_relpath_action
)
580 def _create_unmerged_context_menu(self
, menu
, s
):
581 menu
.addAction(self
.launch_difftool_action
)
583 action
= menu
.addAction(qtutils
.add_icon(),
584 N_('Stage Selected'),
585 cmds
.run(cmds
.Stage
, self
.unstaged()))
586 action
.setShortcut(cmds
.Stage
.SHORTCUT
)
588 menu
.addAction(self
.launch_editor_action
)
590 if not utils
.is_win32():
592 action
= menu
.addAction(qtutils
.file_icon(),
593 cmds
.OpenDefaultApp
.name(),
594 cmds
.run(cmds
.OpenDefaultApp
, self
.unmerged()))
595 action
.setShortcut(cmds
.OpenDefaultApp
.SHORTCUT
)
597 action
= menu
.addAction(qtutils
.open_file_icon(),
598 cmds
.OpenParentDir
.name(),
599 self
._open
_parent
_dir
)
600 action
.setShortcut(cmds
.OpenParentDir
.SHORTCUT
)
603 menu
.addAction(self
.copy_path_action
)
604 menu
.addAction(self
.copy_relpath_action
)
607 def _create_unstaged_context_menu(self
, menu
, s
):
608 modified_submodule
= (s
.modified
and
609 s
.modified
[0] in self
.m
.submodules
)
610 if modified_submodule
:
611 return self
._create
_modified
_submodule
_context
_menu
(menu
, s
)
613 if self
.m
.stageable():
614 action
= menu
.addAction(qtutils
.add_icon(),
615 N_('Stage Selected'),
616 cmds
.run(cmds
.Stage
, self
.unstaged()))
617 action
.setShortcut(cmds
.Stage
.SHORTCUT
)
619 # Do all of the selected items exist?
620 all_exist
= all(not i
in self
.m
.unstaged_deleted
and core
.exists(i
)
621 for i
in self
.staged())
623 if all_exist
and self
.unstaged():
624 menu
.addAction(self
.launch_editor_action
)
626 if all_exist
and s
.modified
and self
.m
.stageable():
627 menu
.addAction(self
.launch_difftool_action
)
629 if s
.modified
and self
.m
.stageable():
630 if self
.m
.undoable():
632 menu
.addAction(self
.revert_unstaged_edits_action
)
634 if all_exist
and self
.unstaged() and not utils
.is_win32():
636 action
= menu
.addAction(qtutils
.file_icon(),
637 cmds
.OpenDefaultApp
.name(),
638 cmds
.run(cmds
.OpenDefaultApp
, self
.unstaged()))
639 action
.setShortcut(cmds
.OpenDefaultApp
.SHORTCUT
)
641 action
= menu
.addAction(qtutils
.open_file_icon(),
642 cmds
.OpenParentDir
.name(),
643 self
._open
_parent
_dir
)
644 action
.setShortcut(cmds
.OpenParentDir
.SHORTCUT
)
646 if all_exist
and s
.untracked
:
648 if self
.move_to_trash_action
is not None:
649 menu
.addAction(self
.move_to_trash_action
)
650 menu
.addAction(self
.delete_untracked_files_action
)
652 menu
.addAction(qtutils
.theme_icon('edit-clear.svg'),
653 N_('Add to .gitignore'),
654 cmds
.run(cmds
.Ignore
,
655 map(lambda x
: '/' + x
, self
.untracked())))
657 menu
.addAction(self
.copy_path_action
)
658 menu
.addAction(self
.copy_relpath_action
)
661 def _create_modified_submodule_context_menu(self
, menu
, s
):
662 menu
.addAction(qtutils
.git_icon(),
663 N_('Launch git-cola'),
664 cmds
.run(cmds
.OpenRepo
, core
.abspath(s
.modified
[0])))
666 menu
.addAction(self
.launch_editor_action
)
668 if self
.m
.stageable():
670 action
= menu
.addAction(qtutils
.add_icon(),
671 N_('Stage Selected'),
672 cmds
.run(cmds
.Stage
, self
.unstaged()))
673 action
.setShortcut(cmds
.Stage
.SHORTCUT
)
676 menu
.addAction(self
.copy_path_action
)
677 menu
.addAction(self
.copy_relpath_action
)
681 def _delete_untracked_files(self
):
682 cmds
.do(cmds
.Delete
, self
.untracked())
684 def _trash_untracked_files(self
):
685 cmds
.do(cmds
.MoveToTrash
, self
.untracked())
687 def single_selection(self
):
688 """Scan across staged, modified, etc. and return a single item."""
704 return selection
.State(st
, um
, m
, ut
)
706 def selected_indexes(self
):
707 """Returns a list of (category, row) representing the tree selection."""
708 selected
= self
.selectedIndexes()
711 if idx
.parent().isValid():
712 parent_idx
= idx
.parent()
713 entry
= (parent_idx
.row(), idx
.row())
715 entry
= (self
.idx_header
, idx
.row())
720 """Return the current selection in the repo status tree."""
721 return selection
.State(self
.staged(), self
.unmerged(),
722 self
.modified(), self
.untracked())
725 return selection
.State(self
.m
.staged
, self
.m
.unmerged
,
726 self
.m
.modified
, self
.m
.untracked
)
730 return c
.staged
+ c
.unmerged
+ c
.modified
+ c
.untracked
732 def selected_group(self
):
733 """A list of selected files in various states of being"""
734 return selection
.pick(self
.selection())
736 def selected_idx(self
):
738 s
= self
.single_selection()
740 for content
, selection
in zip(c
, s
):
741 if len(content
) == 0:
743 if selection
is not None:
744 return offset
+ content
.index(selection
)
745 offset
+= len(content
)
748 def select_by_index(self
, idx
):
751 (c
.staged
, self
.idx_staged
),
752 (c
.unmerged
, self
.idx_unmerged
),
753 (c
.modified
, self
.idx_modified
),
754 (c
.untracked
, self
.idx_untracked
),
756 for content
, toplevel_idx
in to_try
:
757 if len(content
) == 0:
759 if idx
< len(content
):
760 parent
= self
.topLevelItem(toplevel_idx
)
761 item
= parent
.child(idx
)
762 self
.select_item(item
)
766 def select_item(self
, item
):
767 self
.scrollToItem(item
)
768 self
.setCurrentItem(item
)
769 self
.setItemSelected(item
, True)
772 return self
._subtree
_selection
(self
.idx_staged
, self
.m
.staged
)
775 return self
.unmerged() + self
.modified() + self
.untracked()
778 return self
._subtree
_selection
(self
.idx_modified
, self
.m
.modified
)
781 return self
._subtree
_selection
(self
.idx_unmerged
, self
.m
.unmerged
)
784 return self
._subtree
_selection
(self
.idx_untracked
, self
.m
.untracked
)
786 def staged_items(self
):
787 return self
._subtree
_selection
_items
(self
.idx_staged
)
789 def unstaged_items(self
):
790 return (self
.unmerged_items() + self
.modified_items() +
791 self
.untracked_items())
793 def modified_items(self
):
794 return self
._subtree
_selection
_items
(self
.idx_modified
)
796 def unmerged_items(self
):
797 return self
._subtree
_selection
_items
(self
.idx_unmerged
)
799 def untracked_items(self
):
800 return self
._subtree
_selection
_items
(self
.idx_untracked
)
802 def _subtree_selection(self
, idx
, items
):
803 item
= self
.topLevelItem(idx
)
804 return qtutils
.tree_selection(item
, items
)
806 def _subtree_selection_items(self
, idx
):
807 item
= self
.topLevelItem(idx
)
808 return qtutils
.tree_selection_items(item
)
810 def double_clicked(self
, item
, idx
):
811 """Called when an item is double-clicked in the repo status tree."""
812 cmds
.do(cmds
.StageOrUnstage
)
814 def _open_using_default_app(self
):
815 cmds
.do(cmds
.OpenDefaultApp
, self
.selected_group())
817 def _open_parent_dir(self
):
818 cmds
.do(cmds
.OpenParentDir
, self
.selected_group())
820 def show_selection(self
):
821 """Show the selected item."""
822 # Sync the selection model
823 selected
= self
.selection()
824 selection
.selection_model().set_selection(selected
)
825 self
.update_actions(selected
=selected
)
827 selected_indexes
= self
.selected_indexes()
828 if not selected_indexes
:
829 if self
.m
.amending():
830 cmds
.do(cmds
.SetDiffText
, '')
832 cmds
.do(cmds
.ResetMode
)
834 category
, idx
= selected_indexes
[0]
835 # A header item e.g. 'Staged', 'Modified', etc.
836 if category
== self
.idx_header
:
838 self
.idx_staged
: cmds
.DiffStagedSummary
,
839 self
.idx_modified
: cmds
.Diffstat
,
840 # TODO implement UnmergedSummary
841 #self.idx_unmerged: cmds.UnmergedSummary,
842 self
.idx_untracked
: cmds
.UntrackedSummary
,
843 }.get(idx
, cmds
.Diffstat
)
846 elif category
== self
.idx_staged
:
847 item
= self
.staged_items()[0]
848 cmds
.do(cmds
.DiffStaged
, item
.path
, deleted
=item
.deleted
)
851 elif category
== self
.idx_modified
:
852 item
= self
.modified_items()[0]
853 cmds
.do(cmds
.Diff
, item
.path
, deleted
=item
.deleted
)
855 elif category
== self
.idx_unmerged
:
856 item
= self
.unmerged_items()[0]
857 cmds
.do(cmds
.Diff
, item
.path
)
859 elif category
== self
.idx_untracked
:
860 item
= self
.unstaged_items()[0]
861 cmds
.do(cmds
.ShowUntracked
, item
.path
)
864 idx
= self
.selected_idx()
865 all_files
= self
.all_files()
867 selected_indexes
= self
.selected_indexes()
869 category
, toplevel_idx
= selected_indexes
[0]
870 if category
== self
.idx_header
:
871 item
= self
.itemAbove(self
.topLevelItem(toplevel_idx
))
873 self
.select_item(item
)
876 self
.select_by_index(len(all_files
) - 1)
879 self
.select_by_index(idx
- 1)
881 self
.select_by_index(len(all_files
) - 1)
884 idx
= self
.selected_idx()
885 all_files
= self
.all_files()
887 selected_indexes
= self
.selected_indexes()
889 category
, toplevel_idx
= selected_indexes
[0]
890 if category
== self
.idx_header
:
891 item
= self
.itemBelow(self
.topLevelItem(toplevel_idx
))
893 self
.select_item(item
)
896 self
.select_by_index(0)
898 if idx
+ 1 < len(all_files
):
899 self
.select_by_index(idx
+ 1)
901 self
.select_by_index(0)
903 def copy_path(self
, absolute
=True):
904 """Copy a selected path to the clipboard"""
905 filename
= selection
.selection_model().filename()
906 qtutils
.copy_path(filename
, absolute
=absolute
)
908 def copy_relpath(self
):
909 """Copy a selected relative path to the clipboard"""
910 self
.copy_path(absolute
=False)
912 def mimeData(self
, items
):
913 """Return a list of absolute-path URLs"""
914 paths
= qtutils
.paths_from_items(items
, item_filter
=lambda item
:
916 and core
.exists(item
.path
))
917 return qtutils
.mimedata_from_paths(paths
)
920 return qtutils
.path_mimetypes()
923 class StatusFilterWidget(QtGui
.QWidget
):
925 def __init__(self
, parent
=None):
926 QtGui
.QWidget
.__init
__(self
, parent
)
927 self
.main_model
= main
.model()
929 hint
= N_('Filter paths...')
930 self
.text
= completion
.GitStatusFilterLineEdit(hint
=hint
, parent
=self
)
931 self
.text
.setToolTip(hint
)
932 self
.text
.hint
.enable(True)
933 self
.setFocusProxy(self
.text
)
936 self
.main_layout
= qtutils
.hbox(defs
.no_margin
, defs
.spacing
, self
.text
)
937 self
.setLayout(self
.main_layout
)
939 self
.connect(self
.text
, SIGNAL('changed()'), self
.apply_filter
)
940 self
.connect(self
.text
, SIGNAL('cleared()'), self
.apply_filter
)
941 self
.connect(self
.text
, SIGNAL('return()'), self
.apply_filter
)
942 self
.connect(self
.text
, SIGNAL('editingFinished()'), self
.apply_filter
)
944 def apply_filter(self
):
945 text
= self
.text
.value()
946 if text
== self
._filter
:
949 paths
= utils
.shell_split(text
)
950 self
.main_model
.update_path_filter(paths
)