fix: save/restore scrollbar position to prevent playlist scroll jumping

setCurrentRow, setHidden, and setText all trigger Qt internal scroll
recalculation. Now save scrollbar value before these operations and
restore it after, then use scrollToItem only to bring off-screen
selections into view.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-13 23:40:17 +02:00
parent 3c903c7188
commit 97986d5138
+15 -5
View File
@@ -1525,6 +1525,8 @@ class PlaylistWidget(QListWidget):
def _apply_visibility(self) -> None: def _apply_visibility(self) -> None:
"""Centralized: item is hidden if profile-hidden OR (hide_exported AND done).""" """Centralized: item is hidden if profile-hidden OR (hide_exported AND done)."""
sb = self.verticalScrollBar()
pos = sb.value() if sb else 0
self.setUpdatesEnabled(False) self.setUpdatesEnabled(False)
for i, path in enumerate(self._paths): for i, path in enumerate(self._paths):
item = self.item(i) item = self.item(i)
@@ -1534,10 +1536,8 @@ class PlaylistWidget(QListWidget):
or (self._hide_exported and path in self._done_set)) or (self._hide_exported and path in self._done_set))
item.setHidden(hidden) item.setHidden(hidden)
self.setUpdatesEnabled(True) self.setUpdatesEnabled(True)
# Restore scroll to current selection. if sb:
cur = self.currentItem() sb.setValue(pos)
if cur:
self.scrollToItem(cur, QListWidget.ScrollHint.EnsureVisible)
def advance(self) -> None: def advance(self) -> None:
"""Move to next visible item in queue.""" """Move to next visible item in queue."""
@@ -1565,11 +1565,16 @@ class PlaylistWidget(QListWidget):
def _select(self, row: int) -> None: def _select(self, row: int) -> None:
prev = self.currentRow() prev = self.currentRow()
# Save scroll position — setCurrentRow triggers Qt internal scroll.
sb = self.verticalScrollBar()
scroll_pos = sb.value() if sb else 0
self.setCurrentRow(row) self.setCurrentRow(row)
if sb:
sb.setValue(scroll_pos)
if prev >= 0 and prev != row and self.item(prev): if prev >= 0 and prev != row and self.item(prev):
self._refresh_item_text(prev) self._refresh_item_text(prev)
if self.item(row):
item = self.item(row) item = self.item(row)
if item:
cur = item.text() cur = item.text()
# Preserve [N] tag from mark_done. # Preserve [N] tag from mark_done.
if cur.startswith("[") and "] " in cur: if cur.startswith("[") and "] " in cur:
@@ -1579,6 +1584,7 @@ class PlaylistWidget(QListWidget):
else: else:
tag = "" tag = ""
item.setText(f"{tag}{os.path.basename(self._paths[row])}") item.setText(f"{tag}{os.path.basename(self._paths[row])}")
# If the item is off-screen, scroll just enough to show it.
self.scrollToItem(item, QListWidget.ScrollHint.EnsureVisible) self.scrollToItem(item, QListWidget.ScrollHint.EnsureVisible)
self.file_selected.emit(self._paths[row]) self.file_selected.emit(self._paths[row])
@@ -2263,6 +2269,8 @@ class MainWindow(QMainWindow):
def _refresh_playlist_checks(self) -> None: def _refresh_playlist_checks(self) -> None:
"""Re-evaluate marks on every playlist item for the current profile.""" """Re-evaluate marks on every playlist item for the current profile."""
profile = self._profile profile = self._profile
sb = self._playlist.verticalScrollBar()
pos = sb.value() if sb else 0
self._playlist.setUpdatesEnabled(False) self._playlist.setUpdatesEnabled(False)
for path in self._playlist._paths: for path in self._playlist._paths:
markers = self._db.get_markers(os.path.basename(path), profile) markers = self._db.get_markers(os.path.basename(path), profile)
@@ -2271,6 +2279,8 @@ class MainWindow(QMainWindow):
else: else:
self._playlist.unmark_done(path) self._playlist.unmark_done(path)
self._playlist.setUpdatesEnabled(True) self._playlist.setUpdatesEnabled(True)
if sb:
sb.setValue(pos)
def _on_delete_marker(self, output_path: str) -> None: def _on_delete_marker(self, output_path: str) -> None:
deleted = self._db.delete_group(output_path) deleted = self._db.delete_group(output_path)