Fixed hang with gifs in duplicates form

This commit is contained in:
Ignacio Serantes
2026-04-06 23:20:27 +02:00
parent 45c95c1bb1
commit 964974431c
6 changed files with 298 additions and 143 deletions

View File

@@ -5,11 +5,11 @@ from PySide6.QtWidgets import (
QSplitter, QWidget, QMessageBox, QApplication, QMenu,
QTableWidget, QTableWidgetItem, QHeaderView, QAbstractItemView
)
from PySide6.QtGui import QIcon, QImage, QDesktopServices
from PySide6.QtGui import QIcon, QImageReader, QImage, QDesktopServices
from PySide6.QtCore import Qt, QTimer, QUrl
from imageviewer import ImagePane
from propertiesdialog import PropertiesDialog
from constants import APP_CONFIG, UITexts
from propertiesdialog import PropertiesDialog
class DuplicateManagerDialog(QDialog):
@@ -26,6 +26,7 @@ class DuplicateManagerDialog(QDialog):
self.active_pane = None
self.current_dup_pair = None # Stores the current DuplicateResult object
self.panes_linked = True # Default to linked
self._user_link_preference = True # Persiste la intención del usuario
self._is_syncing = False # Guard to prevent recursion during synchronization
self.setWindowTitle(UITexts.DUPLICATE_MANAGER_TITLE)
@@ -37,7 +38,8 @@ class DuplicateManagerDialog(QDialog):
if self.main_win and hasattr(self.main_win, 'fs_watcher'):
self.main_win.fs_watcher.file_deleted.connect(
self._on_file_deleted_externally)
self.main_win.fs_watcher.file_moved.connect(self._on_file_moved_externally)
self.main_win.fs_watcher.file_moved.connect(
self._on_file_moved_externally)
if self.duplicates:
self.table_widget.setCurrentCell(0, 0)
@@ -59,7 +61,8 @@ class DuplicateManagerDialog(QDialog):
self.table_widget = QTableWidget()
if self.review_mode:
self.table_widget.setColumnCount(3)
columns = 3
self.table_widget.setColumnCount(columns)
self.table_widget.setHorizontalHeaderLabels(
[UITexts.IGNORED_DATE, "%", UITexts.CONTEXT_MENU_OPEN])
self.table_widget.horizontalHeader().setSectionResizeMode(
@@ -69,9 +72,10 @@ class DuplicateManagerDialog(QDialog):
self.table_widget.horizontalHeader().setSectionResizeMode(
2, QHeaderView.Stretch)
else:
self.table_widget.setColumnCount(2)
columns = 2
self.table_widget.setColumnCount(columns)
self.table_widget.setHorizontalHeaderLabels(
["%", UITexts.CONTEXT_MENU_OPEN]) # Usamos una cadena existente o genérica
["%", UITexts.CONTEXT_MENU_OPEN])
self.table_widget.horizontalHeader().setSectionResizeMode(
0, QHeaderView.ResizeToContents)
self.table_widget.horizontalHeader().setSectionResizeMode(
@@ -103,18 +107,22 @@ class DuplicateManagerDialog(QDialog):
button_widget = QWidget()
btn_layout = QHBoxLayout(button_widget)
self.btn_del_left = QPushButton(QIcon.fromTheme("user-trash"), UITexts.DUPLICATE_DELETE_LEFT)
self.btn_del_left = QPushButton(
QIcon.fromTheme("user-trash"), UITexts.DUPLICATE_DELETE_LEFT)
self.btn_del_left.clicked.connect(self._delete_left)
self.btn_del_right = QPushButton(QIcon.fromTheme("user-trash"), UITexts.DUPLICATE_DELETE_RIGHT)
self.btn_del_right = QPushButton(
QIcon.fromTheme("user-trash"), UITexts.DUPLICATE_DELETE_RIGHT)
self.btn_del_right.clicked.connect(self._delete_right)
self.btn_link_panes = QPushButton(QIcon.fromTheme("object-link"), UITexts.VIEWER_MENU_LINK_PANES)
self.btn_link_panes = QPushButton(
QIcon.fromTheme("object-link"), UITexts.VIEWER_MENU_LINK_PANES)
self.btn_link_panes.setCheckable(True)
self.btn_link_panes.setChecked(self.panes_linked)
self.btn_link_panes.clicked.connect(self._toggle_link_panes)
self.btn_keep_both = QPushButton(QIcon.fromTheme("emblem-important"), UITexts.DUPLICATE_KEEP_BOTH)
self.btn_keep_both = QPushButton(
QIcon.fromTheme("emblem-important"), UITexts.DUPLICATE_KEEP_BOTH)
self.btn_keep_both.clicked.connect(self._keep_both)
self.btn_skip = QPushButton(UITexts.DUPLICATE_SKIP)
@@ -135,7 +143,9 @@ class DuplicateManagerDialog(QDialog):
self.similarity_lbl = QLabel()
self.similarity_lbl.setAlignment(Qt.AlignCenter)
self.similarity_lbl.setMinimumHeight(30)
self.similarity_lbl.setStyleSheet("font-weight: bold; color: #f39c12; font-size: 15px; background-color: #222; border: 1px solid #444; border-radius: 4px;")
self.similarity_lbl.setStyleSheet(
"font-weight: bold; color: #f39c12; font-size: 15px; "
"background-color: #222; border: 1px solid #444; border-radius: 4px;")
main_right_layout = QVBoxLayout()
main_right_layout.addWidget(self.comparison_widget, 1)
@@ -156,8 +166,10 @@ class DuplicateManagerDialog(QDialog):
"""Disconnects signals and performs cleanup when closing."""
if self.main_win and hasattr(self.main_win, 'fs_watcher'):
try:
self.main_win.fs_watcher.file_deleted.disconnect(self._on_file_deleted_externally)
self.main_win.fs_watcher.file_moved.disconnect(self._on_file_moved_externally)
self.main_win.fs_watcher.file_deleted.disconnect(
self._on_file_deleted_externally)
self.main_win.fs_watcher.file_moved.disconnect(
self._on_file_moved_externally)
except (RuntimeError, TypeError):
pass
@@ -340,21 +352,25 @@ class DuplicateManagerDialog(QDialog):
if self.review_mode:
# Column 0: Ignored Date
ts = dup.timestamp if hasattr(dup, 'timestamp') and dup.timestamp else 0
date_str = datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M") if ts else "-"
date_str = datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M") \
if ts else "-"
date_item = QTableWidgetItem(date_str)
date_item.setData(Qt.UserRole, i) # Store original index here for _load_pair
# Store original index here for _load_pair
date_item.setData(Qt.UserRole, i)
date_item.setTextAlignment(Qt.AlignCenter)
self.table_widget.setItem(row, 0, date_item)
col_offset = 1
else:
col_offset = 0
# Columna similarity (usamos DisplayRole con int para que ordene numéricamente)
# Columna similarity (usamos DisplayRole con int para que ordene
# numéricamente)
sim_item = QTableWidgetItem()
sim_item.setData(Qt.DisplayRole, dup.similarity if dup.similarity is not None else 0)
sim_item.setData(Qt.DisplayRole, dup.similarity
if dup.similarity is not None else 0)
sim_item.setTextAlignment(Qt.AlignCenter)
if not self.review_mode:
sim_item.setData(Qt.UserRole, i) # Guardamos el índice original en la lista duplicates
sim_item.setData(Qt.UserRole, i)
# Columna 1: Nombres de ficheros
names_item = QTableWidgetItem(f"{name1}{name2}")
@@ -375,7 +391,8 @@ class DuplicateManagerDialog(QDialog):
if row < 0 or row >= self.table_widget.rowCount():
return
# Obtenemos el índice real de la lista duplicates guardado en el UserRole del item
# Obtenemos el índice real de la lista duplicates guardado en el UserRole del
# item
item = self.table_widget.item(row, 0)
if not item:
return
@@ -387,12 +404,14 @@ class DuplicateManagerDialog(QDialog):
similarity_color = "#f39c12" # Default (amber)
if dup.similarity is not None:
if dup.similarity == 100:
similarity_color = "#2ecc71" # Green
similarity_color = "#2ecc71" # Green
elif dup.similarity < 80:
similarity_color = "#e74c3c" # Red
similarity_color = "#e74c3c" # Red
self.similarity_lbl.setText(f"{dup.similarity}% Similarity")
self.similarity_lbl.setStyleSheet(f"font-weight: bold; color: {similarity_color}; font-size: 12px; margin-top: 5px;")
self.similarity_lbl.setStyleSheet(
f"font-weight: bold; color: {similarity_color}; "
"font-size: 12px; margin-top: 5px;")
self.similarity_lbl.show()
else:
self.similarity_lbl.hide()
@@ -417,13 +436,26 @@ class DuplicateManagerDialog(QDialog):
mtime1 = os.path.getmtime(path_left) if os.path.exists(path_left) else 0
mtime2 = os.path.getmtime(path_right) if os.path.exists(path_right) else 0
# La imagen más reciente (mtime más alto) va a la izquierda
# Recent image to the left, older to the right
if mtime1 >= mtime2:
self._set_pane_data(self.left_pane_widget, path_left, filename_color, dir_color, filename_left, dir_left)
self._set_pane_data(self.right_pane_widget, path_right, filename_color, dir_color, filename_right, dir_right)
dis_l = self._set_pane_data(
self.left_pane_widget, path_left, filename_color,
dir_color, filename_left, dir_left)
dis_r = self._set_pane_data(
self.right_pane_widget, path_right, filename_color,
dir_color, filename_right, dir_right)
else:
self._set_pane_data(self.left_pane_widget, path_right, filename_color, dir_color, filename_right, dir_right)
self._set_pane_data(self.right_pane_widget, path_left, filename_color, dir_color, filename_left, dir_left)
dis_l = self._set_pane_data(
self.left_pane_widget, path_right, filename_color,
dir_color, filename_right, dir_right)
dis_r = self._set_pane_data(
self.right_pane_widget, path_left, filename_color,
dir_color, filename_left, dir_left)
can_link = not (dis_l or dis_r)
self.panes_linked = self._user_link_preference and can_link
self.btn_link_panes.setEnabled(can_link)
self.btn_link_panes.setChecked(self.panes_linked)
# Compare resolutions and highlight the best one
p_l = self.left_pane.controller.pixmap_original
@@ -432,7 +464,7 @@ class DuplicateManagerDialog(QDialog):
res_l = p_l.width() * p_l.height()
res_r = p_r.width() * p_r.height()
winner = 0 # 0: none, 1: left, 2: right
winner = 0 # 0: none, 1: left, 2: right
if res_l > res_r:
winner = 1
elif res_r > res_l:
@@ -444,26 +476,40 @@ class DuplicateManagerDialog(QDialog):
path_r = self.right_pane.controller.get_current_path()
size_l = os.path.getsize(path_l)
size_r = os.path.getsize(path_r)
if size_l > size_r: winner = 1
elif size_r > size_l: winner = 2
except (OSError, AttributeError): pass
if size_l > size_r:
winner = 1
elif size_r > size_l:
winner = 2
except (OSError, AttributeError):
pass
if winner == 1:
self.left_pane_widget.info_lbl.setStyleSheet("font-weight: bold; color: #2ecc71;")
self.left_pane_widget.info_lbl.setText("" + self.left_pane_widget.info_lbl.text())
self.right_pane_widget.info_lbl.setStyleSheet("font-weight: bold; color: #aaa;")
self.left_pane_widget.info_lbl.setStyleSheet(
"font-weight: bold; color: #2ecc71;")
self.left_pane_widget.info_lbl.setText(
"" + self.left_pane_widget.info_lbl.text())
self.right_pane_widget.info_lbl.setStyleSheet(
"font-weight: bold; color: #aaa;")
elif winner == 2:
self.right_pane_widget.info_lbl.setStyleSheet("font-weight: bold; color: #2ecc71;")
self.right_pane_widget.info_lbl.setText("" + self.right_pane_widget.info_lbl.text())
self.left_pane_widget.info_lbl.setStyleSheet("font-weight: bold; color: #aaa;")
self.right_pane_widget.info_lbl.setStyleSheet(
"font-weight: bold; color: #2ecc71;")
self.right_pane_widget.info_lbl.setText(
"" + self.right_pane_widget.info_lbl.text())
self.left_pane_widget.info_lbl.setStyleSheet(
"font-weight: bold; color: #aaa;")
else:
self.left_pane_widget.info_lbl.setStyleSheet("font-weight: bold; color: #aaa;")
self.right_pane_widget.info_lbl.setStyleSheet("font-weight: bold; color: #aaa;")
self.left_pane_widget.info_lbl.setStyleSheet(
"font-weight: bold; color: #aaa;")
self.right_pane_widget.info_lbl.setStyleSheet(
"font-weight: bold; color: #aaa;")
else:
self.left_pane_widget.info_lbl.setStyleSheet("font-weight: bold; color: #aaa;")
self.right_pane_widget.info_lbl.setStyleSheet("font-weight: bold; color: #aaa;")
self.left_pane_widget.info_lbl.setStyleSheet(
"font-weight: bold; color: #aaa;")
self.right_pane_widget.info_lbl.setStyleSheet(
"font-weight: bold; color: #aaa;")
def _set_pane_data(self, pane_widget, path, filename_color, dir_color, filename_text, dir_text):
def _set_pane_data(self, pane_widget, path, filename_color, dir_color,
filename_text, dir_text) -> bool:
pane = pane_widget.pane
info_lbl = pane_widget.info_lbl
filename_lbl = pane_widget.filename_lbl
@@ -471,16 +517,27 @@ class DuplicateManagerDialog(QDialog):
if not os.path.exists(path):
info_lbl.setText("FILE NOT FOUND")
pane.controller.update_list([], 0) # Clear pane
pane.controller.update_list([], 0) # Clear pane
pane.load_and_fit_image()
filename_lbl.setText("N/A")
dir_lbl.setText("N/A")
return
return True
# Metadatos
size_bytes = os.path.getsize(path)
size_str = self._format_size(size_bytes)
# Detección de imágenes animadas o resoluciones inválidas
reader = QImageReader(path)
is_animated = reader.supportsAnimation() and reader.imageCount() > 1
is_invalid = (pane.controller.pixmap_original.isNull() or
not pane.controller.pixmap_original.size().isValid())
disable_linking = is_animated or is_invalid
self.panes_linked = self._user_link_preference and disable_linking
self.btn_link_panes.setEnabled(disable_linking)
self.btn_link_panes.setChecked(self.panes_linked)
# Load image into pane's controller
pane.controller.update_list([path], 0)
pane.load_and_fit_image()
@@ -495,9 +552,11 @@ class DuplicateManagerDialog(QDialog):
info_lbl.setText(f"{size_str} - N/A")
filename_lbl.setText(filename_text)
filename_lbl.setStyleSheet(f"font-size: 11px; font-weight: bold; color: {filename_color};")
filename_lbl.setStyleSheet(
f"font-size: 11px; font-weight: bold; color: {filename_color};")
dir_lbl.setText(dir_text)
dir_lbl.setStyleSheet(f"font-size: 9px; color: {dir_color};")
return disable_linking
def _show_pane_context_menu(self, pos):
pane = self.sender()
@@ -508,7 +567,8 @@ class DuplicateManagerDialog(QDialog):
menu = QMenu(self)
# Open with...
open_menu = menu.addMenu(QIcon.fromTheme("document-open"), UITexts.CONTEXT_MENU_OPEN)
open_menu = menu.addMenu(
QIcon.fromTheme("document-open"), UITexts.CONTEXT_MENU_OPEN)
self.main_win.populate_open_with_submenu(open_menu, path)
# Open location
@@ -521,28 +581,39 @@ class DuplicateManagerDialog(QDialog):
menu.addSeparator()
# Clipboard
clip_menu = menu.addMenu(QIcon.fromTheme("edit-copy"), UITexts.CONTEXT_MENU_CLIPBOARD)
clip_menu = menu.addMenu(
QIcon.fromTheme("edit-copy"), UITexts.CONTEXT_MENU_CLIPBOARD)
action_copy_image = clip_menu.addAction(QIcon.fromTheme("image-x-generic"), UITexts.VIEWER_MENU_COPY_IMAGE)
action_copy_image.triggered.connect(lambda: QApplication.clipboard().setImage(QImage(path)))
action_copy_image = clip_menu.addAction(
QIcon.fromTheme("image-x-generic"), UITexts.VIEWER_MENU_COPY_IMAGE)
action_copy_image.triggered.connect(
lambda: QApplication.clipboard().setImage(QImage(path)))
action_copy_path = clip_menu.addAction(QIcon.fromTheme("document-properties"), UITexts.VIEWER_MENU_COPY_PATH)
action_copy_path.triggered.connect(lambda: QApplication.clipboard().setText(path))
action_copy_path = clip_menu.addAction(
QIcon.fromTheme("document-properties"), UITexts.VIEWER_MENU_COPY_PATH)
action_copy_path.triggered.connect(
lambda: QApplication.clipboard().setText(path))
menu.addSeparator()
# Trash / Delete
action_trash = menu.addAction(QIcon.fromTheme("user-trash"), UITexts.CONTEXT_MENU_TRASH)
action_trash.triggered.connect(lambda: self._handle_action(delete_path=path, permanent=False))
action_trash = menu.addAction(
QIcon.fromTheme("user-trash"), UITexts.CONTEXT_MENU_TRASH)
action_trash.triggered.connect(
lambda: self._handle_action(delete_path=path, permanent=False))
action_delete = menu.addAction(QIcon.fromTheme("edit-delete"), UITexts.CONTEXT_MENU_DELETE)
action_delete.triggered.connect(lambda: self._handle_permanent_delete(path))
action_delete = menu.addAction(
QIcon.fromTheme("edit-delete"), UITexts.CONTEXT_MENU_DELETE)
action_delete.triggered.connect(
lambda: self._handle_permanent_delete(path))
menu.addSeparator()
# Properties
action_props = menu.addAction(QIcon.fromTheme("document-properties"), UITexts.CONTEXT_MENU_PROPERTIES)
action_props.triggered.connect(lambda: self._show_properties(path, pane))
action_props = menu.addAction(
QIcon.fromTheme("document-properties"), UITexts.CONTEXT_MENU_PROPERTIES)
action_props.triggered.connect(
lambda: self._show_properties(path, pane))
menu.exec(pane.mapToGlobal(pos))
@@ -550,7 +621,8 @@ class DuplicateManagerDialog(QDialog):
confirm = QMessageBox(self)
confirm.setIcon(QMessageBox.Warning)
confirm.setText(UITexts.CONFIRM_DELETE_TEXT)
confirm.setInformativeText(UITexts.CONFIRM_DELETE_INFO.format(os.path.basename(path)))
confirm.setInformativeText(
UITexts.CONFIRM_DELETE_INFO.format(os.path.basename(path)))
confirm.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
confirm.setDefaultButton(QMessageBox.No)
if confirm.exec() == QMessageBox.Yes:
@@ -559,14 +631,16 @@ class DuplicateManagerDialog(QDialog):
def _show_properties(self, path, pane):
tags = pane.controller._current_tags
rating = pane.controller._current_rating
dlg = PropertiesDialog(path, initial_tags=tags, initial_rating=rating, parent=self)
dlg = PropertiesDialog(
path, initial_tags=tags, initial_rating=rating, parent=self)
dlg.exec()
def _on_pane_activated(self):
# When a pane is activated, ensure its zoom/scroll is the reference for linking
if self.panes_linked:
active_pane = self.sender() # The pane that emitted activated signal
other_pane = self.left_pane if active_pane == self.right_pane else self.right_pane
other_pane = self.left_pane \
if active_pane == self.right_pane else self.right_pane
self._sync_zoom(active_pane.controller.zoom_factor, source_pane=active_pane)
# Need to get scroll position from active_pane and apply to other
h_bar = active_pane.scroll_area.horizontalScrollBar()
@@ -603,17 +677,20 @@ class DuplicateManagerDialog(QDialog):
x_pct = h_bar.value() / h_bar.maximum() if h_bar.maximum() > 0 else 0
y_pct = v_bar.value() / v_bar.maximum() if v_bar.maximum() > 0 else 0
target_pane = self.left_pane if source_pane == self.right_pane else self.right_pane
target_pane = self.left_pane \
if source_pane == self.right_pane else self.right_pane
target_pane.zoom_manager.zoom(absolute_factor=factor)
# Re-apply relative scroll after zoom changes bounds
QTimer.singleShot(0, lambda p=target_pane, x=x_pct, y=y_pct: p.set_scroll_relative(x, y))
QTimer.singleShot(
0, lambda p=target_pane, x=x_pct, y=y_pct: p.set_scroll_relative(x, y))
finally:
self._is_syncing = False
def _format_size(self, size):
for unit in ['B', 'KiB', 'MiB', 'GiB']:
if size < 1024: return f"{size:.1f} {unit}"
if size < 1024:
return f"{size:.1f} {unit}"
size /= 1024
return f"{size:.1f} TiB"
@@ -628,7 +705,8 @@ class DuplicateManagerDialog(QDialog):
self._handle_action(delete_path=path_to_delete)
def _toggle_link_panes(self):
self.panes_linked = self.btn_link_panes.isChecked()
self._user_link_preference = self.btn_link_panes.isChecked()
self.panes_linked = self._user_link_preference
if self.panes_linked:
# When linking, synchronize the other pane to the active one
# For simplicity, let's always sync right to left if linking is enabled
@@ -645,7 +723,8 @@ class DuplicateManagerDialog(QDialog):
path = os.path.abspath(path)
# 1. Identify pairs to remove and clean up the pending DB
pairs_to_remove = [d for d in self.duplicates if d.path1 == path or d.path2 == path]
pairs_to_remove = [d for d in self.duplicates
if d.path1 == path or d.path2 == path]
if not pairs_to_remove:
return
@@ -699,12 +778,16 @@ class DuplicateManagerDialog(QDialog):
def _skip(self):
if self.review_mode and self.current_dup_pair:
self.cache.mark_as_exception(self.current_dup_pair.path1, self.current_dup_pair.path2, False)
self.cache.mark_as_exception(
self.current_dup_pair.path1, self.current_dup_pair.path2, False)
# Borramos los hashes para que el detector las trate como imágenes nuevas
# y fuerce una nueva comparación en el siguiente escaneo.
# Usamos clear_relationships=False para no perder otras posibles coincidencias ya marcadas.
self.cache.remove_hash_for_path(self.current_dup_pair.path1, clear_relationships=False)
self.cache.remove_hash_for_path(self.current_dup_pair.path2, clear_relationships=False)
# Usamos clear_relationships=False para no perder otras posibles
# coincidencias ya marcadas.
self.cache.remove_hash_for_path(
self.current_dup_pair.path1, clear_relationships=False)
self.cache.remove_hash_for_path(
self.current_dup_pair.path2, clear_relationships=False)
self._handle_action(skip=False, permanent=False)
else:
self._handle_action(skip=True)
@@ -732,27 +815,35 @@ class DuplicateManagerDialog(QDialog):
# Remove all pairs containing this path from the persistent pending DB
# because the file will be gone.
pairs_to_unmark = [d for d in self.duplicates if d.path1 == delete_path or d.path2 == delete_path]
pairs_to_unmark = [d for d in self.duplicates
if d.path1 == delete_path or d.path2 == delete_path]
for p in pairs_to_unmark:
self.cache.mark_as_pending(p.path1, p.path2, False)
self.main_win.delete_file_by_path(delete_path, permanent=permanent) # Use default setting
self.main_win.delete_file_by_path(delete_path, permanent=permanent)
if os.path.exists(delete_path):
QMessageBox.warning(self, UITexts.ERROR, UITexts.ERROR_DELETING_FILE.format(delete_path))
QMessageBox.warning(
self, UITexts.ERROR,
UITexts.ERROR_DELETING_FILE.format(delete_path))
return
# Remove all pairs containing this path because it no longer exists
self.duplicates = [d for d in self.duplicates if d.path1 != delete_path and d.path2 != delete_path]
self.duplicates = [d for d in self.duplicates
if d.path1 != delete_path and d.path2 != delete_path]
else:
# Skip or KeepBoth:
if not skip: # "Keep Both" case
# It's no longer pending, it's an exception (already marked in _keep_both)
self.cache.mark_as_pending(current_pair.path1, current_pair.path2, False)
# Note: if it's "Skip", we do NOT remove it from pending DB, so it stays there for next time.
if not skip: # "Keep Both" case
# It's no longer pending, it's an exception (already marked in
# _keep_both)
self.cache.mark_as_pending(
current_pair.path1, current_pair.path2, False)
# Note: if it's "Skip", we do NOT remove it from pending DB, so it stays
# there for next time.
if 0 <= original_index < len(self.duplicates):
self.duplicates.pop(original_index)
# Repopulate list widget to ensure all indices are correct and counter is updated
# Repopulate list widget to ensure all indices are correct and counter is
# updated
self._populate_list()
# Try to restore selection to same position (or last item)