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 hotkeys
13 from cola
import icons
14 from cola
import qtutils
15 from cola
import utils
16 from cola
.i18n
import N_
17 from cola
.models
import main
18 from cola
.models
import selection
19 from cola
.widgets
import completion
20 from cola
.widgets
import defs
23 class StatusWidget(QtGui
.QWidget
):
25 Provides a git-status-like repository widget.
27 This widget observes the main model and broadcasts
31 def __init__(self
, titlebar
, parent
=None):
32 QtGui
.QWidget
.__init
__(self
, parent
)
34 tooltip
= N_('Toggle the paths filter')
35 icon
= icons
.ellipsis()
36 self
.filter_button
= qtutils
.create_action_button(tooltip
=tooltip
,
38 self
.filter_widget
= StatusFilterWidget()
39 self
.filter_widget
.hide()
40 self
.tree
= StatusTreeWidget()
41 self
.setFocusProxy(self
.tree
)
43 self
.main_layout
= qtutils
.vbox(defs
.no_margin
, defs
.no_spacing
,
44 self
.filter_widget
, self
.tree
)
45 self
.setLayout(self
.main_layout
)
47 self
.toggle_action
= qtutils
.add_action(self
, tooltip
,
51 titlebar
.add_corner_widget(self
.filter_button
)
52 qtutils
.connect_button(self
.filter_button
, self
.toggle_filter
)
54 def toggle_filter(self
):
55 shown
= not self
.filter_widget
.isVisible()
56 self
.filter_widget
.setVisible(shown
)
58 self
.filter_widget
.setFocus(True)
60 self
.tree
.setFocus(True)
62 def set_initial_size(self
):
63 self
.setMaximumWidth(222)
64 QtCore
.QTimer
.singleShot(1, self
.restore_size
)
66 def restore_size(self
):
67 self
.setMaximumWidth(2 ** 13)
70 self
.tree
.show_selection()
72 def set_filter(self
, txt
):
73 self
.filter_widget
.setVisible(True)
74 self
.filter_widget
.text
.set_value(txt
)
75 self
.filter_widget
.apply_filter()
84 class StatusTreeWidget(QtGui
.QTreeWidget
):
93 # Read-only access to the mode state
94 mode
= property(lambda self
: self
.m
.mode
)
96 def __init__(self
, parent
=None):
97 QtGui
.QTreeWidget
.__init
__(self
, parent
)
99 self
.setSelectionMode(QtGui
.QAbstractItemView
.ExtendedSelection
)
100 self
.headerItem().setHidden(True)
101 self
.setAllColumnsShowFocus(True)
102 self
.setSortingEnabled(False)
103 self
.setUniformRowHeights(True)
104 self
.setAnimated(True)
105 self
.setRootIsDecorated(False)
106 self
.setIndentation(0)
107 self
.setDragEnabled(True)
110 compare
= icons
.compare()
111 question
= icons
.question()
112 self
.add_toplevel_item(N_('Staged'), ok
, hide
=True)
113 self
.add_toplevel_item(N_('Unmerged'), compare
, hide
=True)
114 self
.add_toplevel_item(N_('Modified'), compare
, hide
=True)
115 self
.add_toplevel_item(N_('Untracked'), question
, hide
=True)
117 # Used to restore the selection
118 self
.old_scroll
= None
119 self
.old_selection
= None
120 self
.old_contents
= None
121 self
.old_current_item
= None
122 self
.expanded_items
= set()
124 self
.process_selection_action
= qtutils
.add_action(
125 self
, cmds
.StageOrUnstage
.name(),
126 cmds
.run(cmds
.StageOrUnstage
), hotkeys
.STAGE_SELECTION
)
128 self
.revert_unstaged_edits_action
= qtutils
.add_action(
129 self
, cmds
.RevertUnstagedEdits
.name(),
130 cmds
.run(cmds
.RevertUnstagedEdits
), hotkeys
.REVERT
)
131 self
.revert_unstaged_edits_action
.setIcon(icons
.undo())
133 self
.launch_difftool_action
= qtutils
.add_action(
134 self
, cmds
.LaunchDifftool
.name(),
135 cmds
.run(cmds
.LaunchDifftool
), hotkeys
.DIFF
)
136 self
.launch_difftool_action
.setIcon(icons
.diff())
138 self
.launch_editor_action
= qtutils
.add_action(
139 self
, cmds
.LaunchEditor
.name(),
140 cmds
.run(cmds
.LaunchEditor
), hotkeys
.EDIT
, *hotkeys
.ACCEPT
)
141 self
.launch_editor_action
.setIcon(icons
.edit())
143 if not utils
.is_win32():
144 self
.open_using_default_app
= qtutils
.add_action(
145 self
, cmds
.OpenDefaultApp
.name(),
146 self
._open
_using
_default
_app
, hotkeys
.PRIMARY_ACTION
)
147 self
.open_using_default_app
.setIcon(icons
.default_app())
149 self
.open_parent_dir_action
= qtutils
.add_action(
150 self
, cmds
.OpenParentDir
.name(),
151 self
._open
_parent
_dir
, hotkeys
.SECONDARY_ACTION
)
152 self
.open_parent_dir_action
.setIcon(icons
.folder())
154 self
.up_action
= qtutils
.add_action(
155 self
, N_('Move Up'), self
.move_up
,
156 hotkeys
.MOVE_UP
, hotkeys
.MOVE_UP_SECONDARY
)
158 self
.down_action
= qtutils
.add_action(
159 self
, N_('Move Down'), self
.move_down
,
160 hotkeys
.MOVE_DOWN
, hotkeys
.MOVE_DOWN_SECONDARY
)
162 self
.copy_path_action
= qtutils
.add_action(
163 self
, N_('Copy Path to Clipboard'), self
.copy_path
, hotkeys
.COPY
)
164 self
.copy_path_action
.setIcon(icons
.copy())
166 self
.copy_relpath_action
= qtutils
.add_action(
167 self
, N_('Copy Relative Path to Clipboard'),
168 self
.copy_relpath
, hotkeys
.CUT
)
169 self
.copy_relpath_action
.setIcon(icons
.copy())
171 self
.view_history_action
= qtutils
.add_action(
172 self
, N_('View History...'), self
.view_history
, hotkeys
.HISTORY
)
174 # MoveToTrash and Delete use the same shortcut.
175 # We will only bind one of them, depending on whether or not the
176 # MoveToTrash command is avaialble. When available, the hotkey
177 # is bound to MoveToTrash, otherwise it is bound to Delete.
178 if cmds
.MoveToTrash
.AVAILABLE
:
179 self
.move_to_trash_action
= qtutils
.add_action(
180 self
, N_('Move file(s) to trash'),
181 self
._trash
_untracked
_files
, hotkeys
.TRASH
)
182 self
.move_to_trash_action
.setIcon(icons
.discard())
183 delete_shortcut
= hotkeys
.DELETE_FILE
185 self
.move_to_trash_action
= None
186 delete_shortcut
= hotkeys
.DELETE_FILE_SECONDARY
188 self
.delete_untracked_files_action
= qtutils
.add_action(
189 self
, N_('Delete File(s)...'),
190 self
._delete
_untracked
_files
, delete_shortcut
)
191 self
.delete_untracked_files_action
.setIcon(icons
.discard())
193 self
.connect(self
, SIGNAL('about_to_update()'),
194 self
._about
_to
_update
, Qt
.QueuedConnection
)
195 self
.connect(self
, SIGNAL('updated()'),
196 self
._updated
, Qt
.QueuedConnection
)
198 self
.m
= main
.model()
199 self
.m
.add_observer(self
.m
.message_about_to_update
,
200 self
.about_to_update
)
201 self
.m
.add_observer(self
.m
.message_updated
, self
.updated
)
203 self
.connect(self
, SIGNAL('itemSelectionChanged()'),
206 self
.connect(self
, SIGNAL('itemDoubleClicked(QTreeWidgetItem*,int)'),
209 self
.connect(self
, SIGNAL('itemCollapsed(QTreeWidgetItem*)'),
210 lambda x
: self
.update_column_widths())
212 self
.connect(self
, SIGNAL('itemExpanded(QTreeWidgetItem*)'),
213 lambda x
: self
.update_column_widths())
215 def add_toplevel_item(self
, txt
, icon
, hide
=False):
219 item
= QtGui
.QTreeWidgetItem(self
)
220 item
.setFont(0, font
)
222 item
.setIcon(0, icon
)
224 self
.setItemHidden(item
, True)
226 def restore_selection(self
):
227 if not self
.old_selection
or not self
.old_contents
:
229 old_c
= self
.old_contents
230 old_s
= self
.old_selection
231 new_c
= self
.contents()
233 def mkselect(lst
, widget_getter
):
234 def select(item
, current
=False):
235 idx
= lst
.index(item
)
236 widget
= widget_getter(idx
)
238 self
.setCurrentItem(widget
)
239 self
.setItemSelected(widget
, True)
242 select_staged
= mkselect(new_c
.staged
, self
.staged_item
)
243 select_unmerged
= mkselect(new_c
.unmerged
, self
.unmerged_item
)
244 select_modified
= mkselect(new_c
.modified
, self
.modified_item
)
245 select_untracked
= mkselect(new_c
.untracked
, self
.untracked_item
)
248 (set(new_c
.staged
), old_c
.staged
, set(old_s
.staged
),
251 (set(new_c
.unmerged
), old_c
.unmerged
, set(old_s
.unmerged
),
254 (set(new_c
.modified
), old_c
.modified
, set(old_s
.modified
),
257 (set(new_c
.untracked
), old_c
.untracked
, set(old_s
.untracked
),
261 # Restore the current item
262 if self
.old_current_item
:
263 category
, idx
= self
.old_current_item
264 if category
== self
.idx_header
:
265 item
= self
.invisibleRootItem().child(idx
)
267 self
.setCurrentItem(item
)
268 self
.setItemSelected(item
, True)
270 # Reselect the current item
271 selection_info
= saved_selection
[category
]
272 new
= selection_info
[0]
273 old
= selection_info
[1]
274 reselect
= selection_info
[3]
280 reselect(item
, current
=True)
283 # When reselecting we only care that the items are selected;
284 # we do not need to rerun the callbacks which were triggered
285 # above. Block signals to skip the callbacks.
286 self
.blockSignals(True)
287 for (new
, old
, sel
, reselect
) in saved_selection
:
290 reselect(item
, current
=False)
291 self
.blockSignals(False)
293 for (new
, old
, sel
, reselect
) in saved_selection
:
294 # When modified is staged, select the next modified item
295 # When unmerged is staged, select the next unmerged item
296 # When unstaging, select the next staged item
297 # When staging untracked files, select the next untracked item
298 if len(new
) >= len(old
):
299 # The list did not shrink so it is not one of these cases.
302 # The item still exists so ignore it
303 if item
in new
or item
not in old
:
305 # The item no longer exists in this list so search for
306 # its nearest neighbors and select them instead.
307 idx
= old
.index(item
)
308 for j
in itertools
.chain(old
[idx
+1:], reversed(old
[:idx
])):
310 reselect(j
, current
=True)
313 def restore_scrollbar(self
):
314 vscroll
= self
.verticalScrollBar()
315 if vscroll
and self
.old_scroll
is not None:
316 vscroll
.setValue(self
.old_scroll
)
317 self
.old_scroll
= None
319 def staged_item(self
, itemidx
):
320 return self
._subtree
_item
(self
.idx_staged
, itemidx
)
322 def modified_item(self
, itemidx
):
323 return self
._subtree
_item
(self
.idx_modified
, itemidx
)
325 def unmerged_item(self
, itemidx
):
326 return self
._subtree
_item
(self
.idx_unmerged
, itemidx
)
328 def untracked_item(self
, itemidx
):
329 return self
._subtree
_item
(self
.idx_untracked
, itemidx
)
331 def unstaged_item(self
, itemidx
):
333 item
= self
.topLevelItem(self
.idx_modified
)
334 count
= item
.childCount()
336 return item
.child(itemidx
)
338 item
= self
.topLevelItem(self
.idx_unmerged
)
339 count
+= item
.childCount()
341 return item
.child(itemidx
)
343 item
= self
.topLevelItem(self
.idx_untracked
)
344 count
+= item
.childCount()
346 return item
.child(itemidx
)
350 def _subtree_item(self
, idx
, itemidx
):
351 parent
= self
.topLevelItem(idx
)
352 return parent
.child(itemidx
)
354 def about_to_update(self
):
355 self
.emit(SIGNAL('about_to_update()'))
357 def _about_to_update(self
):
358 self
.save_selection()
359 self
.save_scrollbar()
361 def save_scrollbar(self
):
362 vscroll
= self
.verticalScrollBar()
364 self
.old_scroll
= vscroll
.value()
366 self
.old_scroll
= None
368 def current_item(self
):
369 s
= self
.selected_indexes()
372 current
= self
.currentItem()
375 idx
= self
.indexFromItem(current
, 0)
376 if idx
.parent().isValid():
377 parent_idx
= idx
.parent()
378 entry
= (parent_idx
.row(), idx
.row())
380 entry
= (self
.idx_header
, idx
.row())
383 def save_selection(self
):
384 self
.old_contents
= self
.contents()
385 self
.old_selection
= self
.selection()
386 self
.old_current_item
= self
.current_item()
389 """Update display from model data."""
390 self
.emit(SIGNAL('updated()'))
393 self
.set_staged(self
.m
.staged
)
394 self
.set_modified(self
.m
.modified
)
395 self
.set_unmerged(self
.m
.unmerged
)
396 self
.set_untracked(self
.m
.untracked
)
397 self
.restore_selection()
398 self
.restore_scrollbar()
399 self
.update_column_widths()
400 self
.update_actions()
402 def update_actions(self
, selected
=None):
404 selected
= selection
.selection()
405 can_revert_edits
= bool(selected
.staged
or selected
.modified
)
406 self
.revert_unstaged_edits_action
.setEnabled(can_revert_edits
)
408 def set_staged(self
, items
):
409 """Adds items to the 'Staged' subtree."""
410 self
._set
_subtree
(items
, self
.idx_staged
, staged
=True,
411 deleted_set
=self
.m
.staged_deleted
)
413 def set_modified(self
, items
):
414 """Adds items to the 'Modified' subtree."""
415 self
._set
_subtree
(items
, self
.idx_modified
,
416 deleted_set
=self
.m
.unstaged_deleted
)
418 def set_unmerged(self
, items
):
419 """Adds items to the 'Unmerged' subtree."""
420 self
._set
_subtree
(items
, self
.idx_unmerged
)
422 def set_untracked(self
, items
):
423 """Adds items to the 'Untracked' subtree."""
424 self
._set
_subtree
(items
, self
.idx_untracked
, untracked
=True)
426 def _set_subtree(self
, items
, idx
,
430 """Add a list of items to a treewidget item."""
431 self
.blockSignals(True)
432 parent
= self
.topLevelItem(idx
)
434 self
.setItemHidden(parent
, False)
436 self
.setItemHidden(parent
, True)
438 # sip v4.14.7 and below leak memory in parent.takeChildren()
439 # so we use this backwards-compatible construct instead
440 while parent
.takeChild(0) is not None:
444 deleted
= (deleted_set
is not None and item
in deleted_set
)
445 treeitem
= qtutils
.create_treeitem(item
,
449 parent
.addChild(treeitem
)
450 self
.expand_items(idx
, items
)
451 self
.blockSignals(False)
453 def update_column_widths(self
):
454 self
.resizeColumnToContents(0)
456 def expand_items(self
, idx
, items
):
457 """Expand the top-level category "folder" once and only once."""
458 # Don't do this if items is empty; this makes it so that we
459 # don't add the top-level index into the expanded_items set
460 # until an item appears in a particular category.
463 # Only run this once; we don't want to re-expand items that
464 # we've clicked on to re-collapse on updated().
465 if idx
in self
.expanded_items
:
467 self
.expanded_items
.add(idx
)
468 item
= self
.topLevelItem(idx
)
470 self
.expandItem(item
)
472 def contextMenuEvent(self
, event
):
473 """Create context menus for the repo status tree."""
474 menu
= self
.create_context_menu()
475 menu
.exec_(self
.mapToGlobal(event
.pos()))
477 def create_context_menu(self
):
478 """Set up the status menu for the repo status tree."""
480 menu
= QtGui
.QMenu(self
)
482 selected_indexes
= self
.selected_indexes()
484 category
, idx
= selected_indexes
[0]
485 # A header item e.g. 'Staged', 'Modified', etc.
486 if category
== self
.idx_header
:
487 return self
._create
_header
_context
_menu
(menu
, idx
)
490 return self
._create
_staged
_context
_menu
(menu
, s
)
493 return self
._create
_unmerged
_context
_menu
(menu
, s
)
495 return self
._create
_unstaged
_context
_menu
(menu
, s
)
497 def _create_header_context_menu(self
, menu
, idx
):
498 if idx
== self
.idx_staged
:
499 menu
.addAction(icons
.remove(), N_('Unstage All'),
500 cmds
.run(cmds
.UnstageAll
))
502 elif idx
== self
.idx_unmerged
:
503 action
= menu
.addAction(icons
.add(), cmds
.StageUnmerged
.name(),
504 cmds
.run(cmds
.StageUnmerged
))
505 action
.setShortcut(hotkeys
.STAGE_SELECTION
)
507 elif idx
== self
.idx_modified
:
508 action
= menu
.addAction(icons
.add(), cmds
.StageModified
.name(),
509 cmds
.run(cmds
.StageModified
))
510 action
.setShortcut(hotkeys
.STAGE_SELECTION
)
513 elif idx
== self
.idx_untracked
:
514 action
= menu
.addAction(icons
.add(), cmds
.StageUntracked
.name(),
515 cmds
.run(cmds
.StageUntracked
))
516 action
.setShortcut(hotkeys
.STAGE_SELECTION
)
519 def _create_staged_context_menu(self
, menu
, s
):
520 if s
.staged
[0] in self
.m
.submodules
:
521 return self
._create
_staged
_submodule
_context
_menu
(menu
, s
)
523 if self
.m
.unstageable():
524 action
= menu
.addAction(icons
.remove(), N_('Unstage Selected'),
525 cmds
.run(cmds
.Unstage
, self
.staged()))
526 action
.setShortcut(hotkeys
.STAGE_SELECTION
)
528 # Do all of the selected items exist?
529 all_exist
= all(not i
in self
.m
.staged_deleted
and core
.exists(i
)
530 for i
in self
.staged())
533 menu
.addAction(self
.launch_editor_action
)
534 menu
.addAction(self
.launch_difftool_action
)
536 if all_exist
and not utils
.is_win32():
538 open_default
= cmds
.run(cmds
.OpenDefaultApp
, self
.staged())
539 action
= menu
.addAction(icons
.default_app(),
540 cmds
.OpenDefaultApp
.name(), open_default
)
541 action
.setShortcut(hotkeys
.PRIMARY_ACTION
)
543 action
= menu
.addAction(icons
.folder(),
544 cmds
.OpenParentDir
.name(),
545 self
._open
_parent
_dir
)
546 action
.setShortcut(hotkeys
.SECONDARY_ACTION
)
548 if self
.m
.undoable():
550 menu
.addAction(self
.revert_unstaged_edits_action
)
553 menu
.addAction(self
.copy_path_action
)
554 menu
.addAction(self
.copy_relpath_action
)
555 menu
.addAction(self
.view_history_action
)
558 def _create_staged_submodule_context_menu(self
, menu
, s
):
559 menu
.addAction(icons
.cola(), N_('Launch git-cola'),
560 cmds
.run(cmds
.OpenRepo
,
561 core
.abspath(s
.staged
[0])))
563 menu
.addAction(self
.launch_editor_action
)
566 action
= menu
.addAction(icons
.remove(), N_('Unstage Selected'),
567 cmds
.run(cmds
.Unstage
, self
.staged()))
568 action
.setShortcut(hotkeys
.STAGE_SELECTION
)
571 menu
.addAction(self
.copy_path_action
)
572 menu
.addAction(self
.copy_relpath_action
)
573 menu
.addAction(self
.view_history_action
)
576 def _create_unmerged_context_menu(self
, menu
, s
):
577 menu
.addAction(self
.launch_difftool_action
)
579 action
= menu
.addAction(icons
.add(), N_('Stage Selected'),
580 cmds
.run(cmds
.Stage
, self
.unstaged()))
581 action
.setShortcut(hotkeys
.STAGE_SELECTION
)
583 menu
.addAction(self
.launch_editor_action
)
585 if not utils
.is_win32():
587 open_default
= cmds
.run(cmds
.OpenDefaultApp
, self
.unmerged())
588 action
= menu
.addAction(icons
.default_app(),
589 cmds
.OpenDefaultApp
.name(), open_default
)
590 action
.setShortcut(hotkeys
.PRIMARY_ACTION
)
592 action
= menu
.addAction(icons
.folder(),
593 cmds
.OpenParentDir
.name(),
594 self
._open
_parent
_dir
)
595 action
.setShortcut(hotkeys
.SECONDARY_ACTION
)
598 menu
.addAction(self
.copy_path_action
)
599 menu
.addAction(self
.copy_relpath_action
)
600 menu
.addAction(self
.view_history_action
)
603 def _create_unstaged_context_menu(self
, menu
, s
):
604 modified_submodule
= (s
.modified
and
605 s
.modified
[0] in self
.m
.submodules
)
606 if modified_submodule
:
607 return self
._create
_modified
_submodule
_context
_menu
(menu
, s
)
609 if self
.m
.stageable():
610 action
= menu
.addAction(icons
.add(), N_('Stage Selected'),
611 cmds
.run(cmds
.Stage
, self
.unstaged()))
612 action
.setShortcut(hotkeys
.STAGE_SELECTION
)
614 # Do all of the selected items exist?
615 all_exist
= all(not i
in self
.m
.unstaged_deleted
and core
.exists(i
)
616 for i
in self
.staged())
618 if all_exist
and self
.unstaged():
619 menu
.addAction(self
.launch_editor_action
)
621 if all_exist
and s
.modified
and self
.m
.stageable():
622 menu
.addAction(self
.launch_difftool_action
)
624 if s
.modified
and self
.m
.stageable():
625 if self
.m
.undoable():
627 menu
.addAction(self
.revert_unstaged_edits_action
)
629 if all_exist
and self
.unstaged() and not utils
.is_win32():
631 open_default
= cmds
.run(cmds
.OpenDefaultApp
, self
.unstaged())
632 action
= menu
.addAction(icons
.default_app(),
633 cmds
.OpenDefaultApp
.name(), open_default
)
634 action
.setShortcut(hotkeys
.PRIMARY_ACTION
)
636 action
= menu
.addAction(icons
.folder(),
637 cmds
.OpenParentDir
.name(),
638 self
._open
_parent
_dir
)
639 action
.setShortcut(hotkeys
.SECONDARY_ACTION
)
641 if all_exist
and s
.untracked
:
643 if self
.move_to_trash_action
is not None:
644 menu
.addAction(self
.move_to_trash_action
)
645 menu
.addAction(self
.delete_untracked_files_action
)
647 menu
.addAction(icons
.edit(),
648 N_('Add to .gitignore'),
649 cmds
.run(cmds
.Ignore
,
650 map(lambda x
: '/' + x
, self
.untracked())))
652 menu
.addAction(self
.copy_path_action
)
653 menu
.addAction(self
.copy_relpath_action
)
654 if not selection
.selection_model().is_empty():
655 menu
.addAction(self
.view_history_action
)
658 def _create_modified_submodule_context_menu(self
, menu
, s
):
659 menu
.addAction(icons
.cola(), N_('Launch git-cola'),
660 cmds
.run(cmds
.OpenRepo
, core
.abspath(s
.modified
[0])))
662 menu
.addAction(self
.launch_editor_action
)
664 if self
.m
.stageable():
666 action
= menu
.addAction(icons
.add(), N_('Stage Selected'),
667 cmds
.run(cmds
.Stage
, self
.unstaged()))
668 action
.setShortcut(hotkeys
.STAGE_SELECTION
)
671 menu
.addAction(self
.copy_path_action
)
672 menu
.addAction(self
.copy_relpath_action
)
673 menu
.addAction(self
.view_history_action
)
677 def _delete_untracked_files(self
):
678 cmds
.do(cmds
.Delete
, self
.untracked())
680 def _trash_untracked_files(self
):
681 cmds
.do(cmds
.MoveToTrash
, self
.untracked())
683 def single_selection(self
):
684 """Scan across staged, modified, etc. and return a single item."""
700 return selection
.State(st
, um
, m
, ut
)
702 def selected_indexes(self
):
703 """Returns a list of (category, row) representing the tree selection."""
704 selected
= self
.selectedIndexes()
707 if idx
.parent().isValid():
708 parent_idx
= idx
.parent()
709 entry
= (parent_idx
.row(), idx
.row())
711 entry
= (self
.idx_header
, idx
.row())
716 """Return the current selection in the repo status tree."""
717 return selection
.State(self
.staged(), self
.unmerged(),
718 self
.modified(), self
.untracked())
721 return selection
.State(self
.m
.staged
, self
.m
.unmerged
,
722 self
.m
.modified
, self
.m
.untracked
)
726 return c
.staged
+ c
.unmerged
+ c
.modified
+ c
.untracked
728 def selected_group(self
):
729 """A list of selected files in various states of being"""
730 return selection
.pick(self
.selection())
732 def selected_idx(self
):
734 s
= self
.single_selection()
736 for content
, selection
in zip(c
, s
):
737 if len(content
) == 0:
739 if selection
is not None:
740 return offset
+ content
.index(selection
)
741 offset
+= len(content
)
744 def select_by_index(self
, idx
):
747 (c
.staged
, self
.idx_staged
),
748 (c
.unmerged
, self
.idx_unmerged
),
749 (c
.modified
, self
.idx_modified
),
750 (c
.untracked
, self
.idx_untracked
),
752 for content
, toplevel_idx
in to_try
:
753 if len(content
) == 0:
755 if idx
< len(content
):
756 parent
= self
.topLevelItem(toplevel_idx
)
757 item
= parent
.child(idx
)
758 self
.select_item(item
)
762 def select_item(self
, item
):
763 self
.scrollToItem(item
)
764 self
.setCurrentItem(item
)
765 self
.setItemSelected(item
, True)
768 return self
._subtree
_selection
(self
.idx_staged
, self
.m
.staged
)
771 return self
.unmerged() + self
.modified() + self
.untracked()
774 return self
._subtree
_selection
(self
.idx_modified
, self
.m
.modified
)
777 return self
._subtree
_selection
(self
.idx_unmerged
, self
.m
.unmerged
)
780 return self
._subtree
_selection
(self
.idx_untracked
, self
.m
.untracked
)
782 def staged_items(self
):
783 return self
._subtree
_selection
_items
(self
.idx_staged
)
785 def unstaged_items(self
):
786 return (self
.unmerged_items() + self
.modified_items() +
787 self
.untracked_items())
789 def modified_items(self
):
790 return self
._subtree
_selection
_items
(self
.idx_modified
)
792 def unmerged_items(self
):
793 return self
._subtree
_selection
_items
(self
.idx_unmerged
)
795 def untracked_items(self
):
796 return self
._subtree
_selection
_items
(self
.idx_untracked
)
798 def _subtree_selection(self
, idx
, items
):
799 item
= self
.topLevelItem(idx
)
800 return qtutils
.tree_selection(item
, items
)
802 def _subtree_selection_items(self
, idx
):
803 item
= self
.topLevelItem(idx
)
804 return qtutils
.tree_selection_items(item
)
806 def double_clicked(self
, item
, idx
):
807 """Called when an item is double-clicked in the repo status tree."""
808 cmds
.do(cmds
.StageOrUnstage
)
810 def _open_using_default_app(self
):
811 cmds
.do(cmds
.OpenDefaultApp
, self
.selected_group())
813 def _open_parent_dir(self
):
814 cmds
.do(cmds
.OpenParentDir
, self
.selected_group())
816 def show_selection(self
):
817 """Show the selected item."""
818 # Sync the selection model
819 selected
= self
.selection()
820 selection
.selection_model().set_selection(selected
)
821 self
.update_actions(selected
=selected
)
823 selected_indexes
= self
.selected_indexes()
824 if not selected_indexes
:
825 if self
.m
.amending():
826 cmds
.do(cmds
.SetDiffText
, '')
828 cmds
.do(cmds
.ResetMode
)
830 category
, idx
= selected_indexes
[0]
831 # A header item e.g. 'Staged', 'Modified', etc.
832 if category
== self
.idx_header
:
834 self
.idx_staged
: cmds
.DiffStagedSummary
,
835 self
.idx_modified
: cmds
.Diffstat
,
836 # TODO implement UnmergedSummary
837 #self.idx_unmerged: cmds.UnmergedSummary,
838 self
.idx_untracked
: cmds
.UntrackedSummary
,
839 }.get(idx
, cmds
.Diffstat
)
842 elif category
== self
.idx_staged
:
843 item
= self
.staged_items()[0]
844 cmds
.do(cmds
.DiffStaged
, item
.path
, deleted
=item
.deleted
)
847 elif category
== self
.idx_modified
:
848 item
= self
.modified_items()[0]
849 cmds
.do(cmds
.Diff
, item
.path
, deleted
=item
.deleted
)
851 elif category
== self
.idx_unmerged
:
852 item
= self
.unmerged_items()[0]
853 cmds
.do(cmds
.Diff
, item
.path
)
855 elif category
== self
.idx_untracked
:
856 item
= self
.unstaged_items()[0]
857 cmds
.do(cmds
.ShowUntracked
, item
.path
)
860 idx
= self
.selected_idx()
861 all_files
= self
.all_files()
863 selected_indexes
= self
.selected_indexes()
865 category
, toplevel_idx
= selected_indexes
[0]
866 if category
== self
.idx_header
:
867 item
= self
.itemAbove(self
.topLevelItem(toplevel_idx
))
869 self
.select_item(item
)
872 self
.select_by_index(len(all_files
) - 1)
875 self
.select_by_index(idx
- 1)
877 self
.select_by_index(len(all_files
) - 1)
880 idx
= self
.selected_idx()
881 all_files
= self
.all_files()
883 selected_indexes
= self
.selected_indexes()
885 category
, toplevel_idx
= selected_indexes
[0]
886 if category
== self
.idx_header
:
887 item
= self
.itemBelow(self
.topLevelItem(toplevel_idx
))
889 self
.select_item(item
)
892 self
.select_by_index(0)
894 if idx
+ 1 < len(all_files
):
895 self
.select_by_index(idx
+ 1)
897 self
.select_by_index(0)
899 def copy_path(self
, absolute
=True):
900 """Copy a selected path to the clipboard"""
901 filename
= selection
.selection_model().filename()
902 qtutils
.copy_path(filename
, absolute
=absolute
)
904 def copy_relpath(self
):
905 """Copy a selected relative path to the clipboard"""
906 self
.copy_path(absolute
=False)
908 def mimeData(self
, items
):
909 """Return a list of absolute-path URLs"""
910 paths
= qtutils
.paths_from_items(items
, item_filter
=lambda item
:
912 and core
.exists(item
.path
))
913 return qtutils
.mimedata_from_paths(paths
)
916 return qtutils
.path_mimetypes()
918 def view_history(self
):
919 """Signal that we should view history for paths."""
920 cmds
.do(cmds
.VisualizePaths
, selection
.union(selection
.selection_model()))
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
)