fix: override scrollTo to block Qt auto-scroll during playlist operations
Replace timer hacks and scrollbar save/restore with a proper fix: PlaylistWidget.scrollTo() is overridden to no-op when _scroll_locked is set. Lock is held during setCurrentRow, visibility changes, playlist checks, and video load — all operations where Qt's internal auto-scroll was causing the list to jump. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1473,8 +1473,14 @@ class PlaylistWidget(QListWidget):
|
|||||||
self._done_set: set[str] = set() # paths with exported clips
|
self._done_set: set[str] = set() # paths with exported clips
|
||||||
self._hidden_basenames: set[str] = set() # profile-hidden basenames
|
self._hidden_basenames: set[str] = set() # profile-hidden basenames
|
||||||
self._hide_exported = False
|
self._hide_exported = False
|
||||||
|
self._scroll_locked = False
|
||||||
self.itemClicked.connect(self._on_item_clicked)
|
self.itemClicked.connect(self._on_item_clicked)
|
||||||
|
|
||||||
|
def scrollTo(self, index, hint=QAbstractItemView.ScrollHint.EnsureVisible):
|
||||||
|
"""Block Qt's internal auto-scroll when the scroll is locked."""
|
||||||
|
if not self._scroll_locked:
|
||||||
|
super().scrollTo(index, hint)
|
||||||
|
|
||||||
def add_files(self, paths: list[str]) -> None:
|
def add_files(self, paths: list[str]) -> None:
|
||||||
"""Append paths not already in queue; auto-select first if queue was empty."""
|
"""Append paths not already in queue; auto-select first if queue was empty."""
|
||||||
was_empty = len(self._paths) == 0
|
was_empty = len(self._paths) == 0
|
||||||
@@ -1525,8 +1531,7 @@ 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()
|
self._scroll_locked = True
|
||||||
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)
|
||||||
@@ -1536,8 +1541,7 @@ 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)
|
||||||
if sb:
|
self._scroll_locked = False
|
||||||
sb.setValue(pos)
|
|
||||||
|
|
||||||
def advance(self) -> None:
|
def advance(self) -> None:
|
||||||
"""Move to next visible item in queue."""
|
"""Move to next visible item in queue."""
|
||||||
@@ -1565,12 +1569,9 @@ 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.
|
self._scroll_locked = True
|
||||||
sb = self.verticalScrollBar()
|
|
||||||
scroll_pos = sb.value() if sb else 0
|
|
||||||
self.setCurrentRow(row)
|
self.setCurrentRow(row)
|
||||||
if sb:
|
self._scroll_locked = False
|
||||||
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)
|
||||||
item = self.item(row)
|
item = self.item(row)
|
||||||
@@ -2213,10 +2214,7 @@ class MainWindow(QMainWindow):
|
|||||||
self._lbl_file.setText(os.path.basename(path))
|
self._lbl_file.setText(os.path.basename(path))
|
||||||
self.setWindowTitle(f"8-cut — {os.path.basename(path)}")
|
self.setWindowTitle(f"8-cut — {os.path.basename(path)}")
|
||||||
_log(f"Loading: {os.path.basename(path)}")
|
_log(f"Loading: {os.path.basename(path)}")
|
||||||
# Stash playlist scroll — video load triggers layout changes that
|
self._playlist._scroll_locked = True
|
||||||
# cause Qt to recalculate the scrollbar.
|
|
||||||
sb = self._playlist.verticalScrollBar()
|
|
||||||
self._playlist_scroll_stash = sb.value() if sb else 0
|
|
||||||
self._mpv.load(path)
|
self._mpv.load(path)
|
||||||
# _after_load triggered by MpvWidget.file_loaded signal
|
# _after_load triggered by MpvWidget.file_loaded signal
|
||||||
|
|
||||||
@@ -2246,11 +2244,7 @@ class MainWindow(QMainWindow):
|
|||||||
self._spn_spread.setValue(float(self._settings.value("spread", "3.0")))
|
self._spn_spread.setValue(float(self._settings.value("spread", "3.0")))
|
||||||
self._preview_win.show()
|
self._preview_win.show()
|
||||||
self._preview_timer.start()
|
self._preview_timer.start()
|
||||||
# Restore playlist scroll — layout events from video load trickle in
|
self._playlist._scroll_locked = False
|
||||||
# across several event loop cycles, so restore multiple times.
|
|
||||||
if hasattr(self, '_playlist_scroll_stash'):
|
|
||||||
for delay in (0, 50, 150):
|
|
||||||
QTimer.singleShot(delay, self._restore_playlist_scroll)
|
|
||||||
|
|
||||||
# Run DB fuzzy match off the main thread — can be slow on large databases.
|
# Run DB fuzzy match off the main thread — can be slow on large databases.
|
||||||
filename = os.path.basename(self._file_path)
|
filename = os.path.basename(self._file_path)
|
||||||
@@ -2268,11 +2262,6 @@ class MainWindow(QMainWindow):
|
|||||||
self.statusBar().clearMessage()
|
self.statusBar().clearMessage()
|
||||||
self._timeline.set_markers(markers)
|
self._timeline.set_markers(markers)
|
||||||
|
|
||||||
def _restore_playlist_scroll(self) -> None:
|
|
||||||
sb = self._playlist.verticalScrollBar()
|
|
||||||
if sb:
|
|
||||||
sb.setValue(self._playlist_scroll_stash)
|
|
||||||
|
|
||||||
def _refresh_markers(self) -> None:
|
def _refresh_markers(self) -> None:
|
||||||
filename = os.path.basename(self._file_path)
|
filename = os.path.basename(self._file_path)
|
||||||
markers = self._db.get_markers(filename, self._profile)
|
markers = self._db.get_markers(filename, self._profile)
|
||||||
@@ -2281,8 +2270,7 @@ 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()
|
self._playlist._scroll_locked = True
|
||||||
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)
|
||||||
@@ -2291,8 +2279,7 @@ 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:
|
self._playlist._scroll_locked = False
|
||||||
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user