v0.1.5: sidebar playlist tree auto-scrolls when dragging near its edges

This commit is contained in:
2026-07-17 13:05:52 -04:00
parent e7f23dec36
commit a4d6e9bf3c
4 changed files with 164 additions and 7 deletions
+62 -6
View File
@@ -2,12 +2,14 @@ from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QPushButton, QTreeWidget, QTreeWidgetItem, QMenu,
QInputDialog, QMessageBox, QAbstractItemView,
)
from PyQt6.QtCore import Qt, QTimer, pyqtSignal
from PyQt6.QtCore import Qt, QTimer, QPoint, pyqtSignal
from PyQt6.QtGui import QBrush, QPainter, QPixmap
from lintunes.models import PlaylistType
from lintunes import tagging
from lintunes.gui.track_table import parse_tracks_mime, TRACKS_MIME
from lintunes.gui.track_table import (
parse_tracks_mime, TRACKS_MIME, autoscroll_direction,
)
from lintunes.gui.playlist_ops import add_tracks_with_dup_check
from lintunes.gui.art_window import ArtWindow
from lintunes.gui import drag_ghost
@@ -166,6 +168,15 @@ class PlaylistTree(QTreeWidget):
self.currentItemChanged.connect(self._on_selection_changed)
self.itemChanged.connect(self._on_item_renamed)
# Edge auto-scroll during a drag (Qt's built-in autoscroll never runs
# because our dragMoveEvent accepts the event itself). Mirrors
# track_table's Round 13 machinery.
self._autoscroll_dir = 0
self._autoscroll_pos: QPoint | None = None
self._autoscroll_timer = QTimer(self)
self._autoscroll_timer.setInterval(40)
self._autoscroll_timer.timeout.connect(self._autoscroll_tick)
manager.playlists_changed.connect(self.rebuild)
self.rebuild()
@@ -377,31 +388,41 @@ class PlaylistTree(QTreeWidget):
event.ignore()
def dragMoveEvent(self, event):
drag_ghost.move(self.viewport().mapToGlobal(event.position().toPoint()))
pos = event.position().toPoint()
drag_ghost.move(self.viewport().mapToGlobal(pos))
if event.source() is self:
# Moving a playlist into/out of folders
# Moving a playlist into/out of folders. The tree accepts an
# internal move somewhere no matter what (empty area = top level),
# so edge auto-scroll stays live even over non-folder rows —
# otherwise scrolling down to a distant folder would be nearly
# impossible past the playlists in between.
self._clear_drop_target()
target = self.itemAt(event.position().toPoint())
target = self.itemAt(pos)
kind = target.data(0, KIND_ROLE) if target else ""
if kind in ("folder", "") or target is None:
event.acceptProposedAction()
else:
event.ignore()
self._update_autoscroll(pos, accepted=True)
return
if not event.mimeData().hasFormat(TRACKS_MIME):
self._clear_drop_target()
self._update_autoscroll(pos, accepted=False)
event.ignore()
return
target = self.itemAt(event.position().toPoint())
target = self.itemAt(pos)
if target and target.data(0, KIND_ROLE) == "playlist":
self._set_drop_target(target)
event.acceptProposedAction()
self._update_autoscroll(pos, accepted=True)
else:
self._clear_drop_target()
event.ignore()
self._update_autoscroll(pos, accepted=False)
def dragLeaveEvent(self, event):
self._clear_drop_target()
self._stop_autoscroll()
super().dragLeaveEvent(event)
def _set_drop_target(self, item):
@@ -422,8 +443,43 @@ class PlaylistTree(QTreeWidget):
self._drop_target.setForeground(0, self._drop_target_fg)
self._drop_target = None
# ---- drag auto-scroll ----
def _update_autoscroll(self, pos: QPoint, accepted: bool):
"""Start/stop edge auto-scroll based on the drag's position."""
self._autoscroll_pos = pos
self._autoscroll_dir = autoscroll_direction(
pos.y(), self.viewport().height(), accepted)
if self._autoscroll_dir and not self._autoscroll_timer.isActive():
self._autoscroll_timer.start()
elif not self._autoscroll_dir and self._autoscroll_timer.isActive():
self._autoscroll_timer.stop()
def _autoscroll_tick(self):
bar = self.verticalScrollBar()
before = bar.value()
bar.setValue(before + self._autoscroll_dir)
if bar.value() == before: # reached the top/bottom — nothing more to do
self._autoscroll_timer.stop()
return
# The cursor may be held still while rows scroll under it, so keep the
# drop-target highlight on the row now beneath it (track drags only;
# internal playlist moves never set a drop target).
if self._autoscroll_pos is not None and self._drop_target is not None:
target = self.itemAt(self._autoscroll_pos)
if target and target.data(0, KIND_ROLE) == "playlist":
self._set_drop_target(target)
else:
self._clear_drop_target()
def _stop_autoscroll(self):
self._autoscroll_dir = 0
self._autoscroll_pos = None
self._autoscroll_timer.stop()
def dropEvent(self, event):
self._clear_drop_target()
self._stop_autoscroll()
target = self.itemAt(event.position().toPoint())
if event.source() is self:
item = self.currentItem()