Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

status: remember selection across updates

Teach the status widget to properly restore the selection across
updates.  This ensures that multiple selected items stay selected across
refresh operations, e.g. Ctrl+R refresh or updates from inotify.

Closes #165

Reported-by: @futureweb via github.com
Signed-off-by: David Aguilar <davvid@gmail.com>
  • Loading branch information...
commit 938d23a9941e7410749261d7ea7cd85461f64188 1 parent 5d8fb8c
David Aguilar authored
Showing with 104 additions and 54 deletions.
  1. +100 −54 cola/widgets/status.py
  2. +4 −0 share/doc/git-cola/relnotes.rst
154 cola/widgets/status.py
View
@@ -17,16 +17,6 @@
from cola.models.selection import State
-def select_item(tree, item):
- if not item:
- return
- tree.setItemSelected(item, True)
- parent = item.parent()
- if parent:
- tree.scrollToItem(parent)
- tree.scrollToItem(item)
-
-
class StatusWidget(QtGui.QWidget):
"""
Provides a git-status-like repository widget.
@@ -88,6 +78,7 @@ def __init__(self, parent):
self.old_scroll = None
self.old_selection = None
self.old_contents = None
+ self.old_current_item = None
self.expanded_items = set()
@@ -177,50 +168,90 @@ def restore_selection(self):
old_s = self.old_selection
new_c = self.contents()
- def select_modified(item):
- idx = new_c.modified.index(item)
- select_item(self, self.modified_item(idx))
+ def mkselect(lst, widget_getter):
+ def select(item, current=False):
+ idx = lst.index(item)
+ widget = widget_getter(idx)
+ if current:
+ self.setCurrentItem(widget)
+ self.setItemSelected(widget, True)
+ return select
+
+ select_staged = mkselect(new_c.staged, self.staged_item)
+ select_unmerged = mkselect(new_c.unmerged, self.unmerged_item)
+ select_modified = mkselect(new_c.modified, self.modified_item)
+ select_untracked = mkselect(new_c.untracked, self.untracked_item)
+
+ saved_selection = [
+ (set(new_c.staged), old_c.staged, set(old_s.staged),
+ select_staged),
- def select_unmerged(item):
- idx = new_c.unmerged.index(item)
- select_item(self, self.unmerged_item(idx))
+ (set(new_c.unmerged), old_c.unmerged, set(old_s.unmerged),
+ select_unmerged),
- def select_untracked(item):
- idx = new_c.untracked.index(item)
- select_item(self, self.untracked_item(idx))
+ (set(new_c.modified), old_c.modified, set(old_s.modified),
+ select_modified),
- def select_staged(item):
- idx = new_c.staged.index(item)
- select_item(self, self.staged_item(idx))
+ (set(new_c.untracked), old_c.untracked, set(old_s.untracked),
+ select_untracked),
+ ]
- restore_selection_actions = (
- (new_c.modified, old_c.modified, old_s.modified, select_modified),
- (new_c.unmerged, old_c.unmerged, old_s.unmerged, select_unmerged),
- (new_c.untracked, old_c.untracked, old_s.untracked, select_untracked),
- (new_c.staged, old_c.staged, old_s.staged, select_staged),
- )
+ # Restore the current item
+ if self.old_current_item:
+ category, idx = self.old_current_item
+ if category == self.idx_header:
+ item = self.invisibleRootItem().child(idx)
+ self.setCurrentItem(item)
+ self.setItemSelected(item, True)
+ return
+ # Reselect the current item
+ selection_info = saved_selection[category]
+ new = selection_info[0]
+ old = selection_info[1]
+ reselect = selection_info[3]
+ try:
+ item = old[idx]
+ except:
+ return
+ if item in new:
+ reselect(item, current=True)
+
+ # Restore selection
+ # When reselecting we only care that the items are selected;
+ # we do not need to rerun the callbacks which were triggered
+ # above. Block signals to skip the callbacks.
+ self.blockSignals(True)
+ for (new, old, selection, reselect) in saved_selection:
+ for item in selection:
+ if item in new:
+ reselect(item, current=False)
+ self.blockSignals(False)
- for (new, old, selection, action) in restore_selection_actions:
+ for (new, old, selection, reselect) in saved_selection:
# When modified is staged, select the next modified item
# When unmerged is staged, select the next unmerged item
- # When untracked is staged, select the next untracked item
- # When something is unstaged we should select the next staged item
- new_set = set(new)
- if len(new) < len(old) and old:
- for idx, i in enumerate(old):
- if i not in new_set:
- for j in itertools.chain(old[idx+1:],
- reversed(old[:idx])):
- if j in new_set:
- action(j)
- return
-
- for (new, old, selection, action) in restore_selection_actions:
- # Reselect items when doing partial-staging
- new_set = set(new)
+ # When unstaging, select the next staged item
+ # When staging untracked files, select the next untracked item
+ if len(new) >= len(old):
+ # The list did not shrink so it is not one of these cases.
+ continue
for item in selection:
- if item in new_set:
- action(item)
+ # The item still exists so ignore it
+ if item in new or item not in old:
+ continue
+ # The item no longer exists in this list so search for
+ # its nearest neighbors and select them instead.
+ idx = old.index(item)
+ for j in itertools.chain(old[idx+1:], reversed(old[:idx])):
+ if j in new:
+ reselect(j, current=True)
+ return
+
+ def restore_scrollbar(self):
+ vscroll = self.verticalScrollBar()
+ if vscroll and self.old_scroll is not None:
+ vscroll.setValue(self.old_scroll)
+ self.old_scroll = None
def staged_item(self, itemidx):
return self._subtree_item(self.idx_staged, itemidx)
@@ -261,13 +292,32 @@ def about_to_update(self):
self.emit(SIGNAL('about_to_update'))
def _about_to_update(self):
- self.old_selection = self.selection()
- self.old_contents = self.contents()
+ self.save_selection()
+ self.save_scrollbar()
- self.old_scroll = None
+ def save_scrollbar(self):
vscroll = self.verticalScrollBar()
if vscroll:
self.old_scroll = vscroll.value()
+ else:
+ self.old_scroll = None
+
+ def current_item(self):
+ current = self.currentItem()
+ if not current:
+ return None
+ idx = self.indexFromItem(current, 0)
+ if idx.parent().isValid():
+ parent_idx = idx.parent()
+ entry = (parent_idx.row(), idx.row())
+ else:
+ entry = (self.idx_header, idx.row())
+ return entry
+
+ def save_selection(self):
+ self.old_contents = self.contents()
+ self.old_selection = self.selection()
+ self.old_current_item = self.current_item()
def updated(self):
"""Update display from model data."""
@@ -279,12 +329,8 @@ def _updated(self):
self.set_unmerged(self.m.unmerged)
self.set_untracked(self.m.untracked)
- vscroll = self.verticalScrollBar()
- if vscroll and self.old_scroll is not None:
- vscroll.setValue(self.old_scroll)
- self.old_scroll = None
-
self.restore_selection()
+ self.restore_scrollbar()
self.update_column_widths()
def set_staged(self, items):
4 share/doc/git-cola/relnotes.rst
View
@@ -20,6 +20,10 @@ Fixes
https://github.com/git-cola/git-cola/pull/163
+* The `Status` tool learned to reselect files when refreshing.
+
+ https://github.com/git-cola/git-cola/issues/165
+
git-cola v1.8.2
===============
Usability, bells and whistles
Please sign in to comment.
Something went wrong with that request. Please try again.