Skip to content

Commit

Permalink
status: remember selection across updates
Browse files Browse the repository at this point in the history
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 git-cola#165

Reported-by: @futureweb via github.com
Signed-off-by: David Aguilar <davvid@gmail.com>
  • Loading branch information
davvid committed Mar 23, 2013
1 parent 5d8fb8c commit 938d23a
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 54 deletions.
154 changes: 100 additions & 54 deletions cola/widgets/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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."""
Expand All @@ -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):
Expand Down
4 changes: 4 additions & 0 deletions share/doc/git-cola/relnotes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 938d23a

Please sign in to comment.