Add drag auto-scroll near the list edges
While a track drag hovers within 28px of the track list's top/bottom edge, the list scrolls that way (40ms timer) so the user can drop onto off-screen rows. Qt's built-in autoscroll never fires because our dragMoveEvent accepts the event itself, so this is driven manually; the drop line refreshes as content scrolls under a stationary cursor.
This commit is contained in:
@@ -6,7 +6,7 @@ from PyQt6.QtWidgets import (
|
||||
)
|
||||
from PyQt6.QtCore import (
|
||||
Qt, QAbstractTableModel, QModelIndex, QSortFilterProxyModel, pyqtSignal,
|
||||
QMimeData, QUrl, QPoint, QPointF, QRectF, QMetaType,
|
||||
QMimeData, QUrl, QPoint, QPointF, QRectF, QMetaType, QTimer,
|
||||
)
|
||||
from PyQt6.QtGui import (
|
||||
QDrag, QKeySequence, QShortcut, QPixmap, QPainter, QPen, QPolygonF, QColor,
|
||||
@@ -23,6 +23,23 @@ from lintunes.gui import drag_ghost
|
||||
TRACKS_MIME = "application/x-lintunes-tracks"
|
||||
SORT_ROLE = Qt.ItemDataRole.UserRole
|
||||
|
||||
# While dragging a track within this many pixels of the list's top/bottom edge,
|
||||
# the list auto-scrolls so off-screen rows can be reached.
|
||||
AUTOSCROLL_MARGIN = 28
|
||||
|
||||
|
||||
def autoscroll_direction(y: int, height: int, accepted: bool,
|
||||
margin: int = AUTOSCROLL_MARGIN) -> int:
|
||||
"""-1 to scroll up, +1 down, 0 to stop: based on how close the drag is to
|
||||
the top/bottom edge. Only scrolls over a valid drop target (accepted)."""
|
||||
if not accepted:
|
||||
return 0
|
||||
if y < margin:
|
||||
return -1
|
||||
if y > height - margin:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
# Manual play-order pseudo-column (playlist views only, always first)
|
||||
INDEX_FIELD = "#"
|
||||
|
||||
@@ -357,6 +374,14 @@ class TrackTableView(QTableView):
|
||||
self.setDropIndicatorShown(False)
|
||||
self._drop_indicator_y: int | None = None
|
||||
|
||||
# Edge auto-scroll during a drag (Qt's built-in autoscroll never runs
|
||||
# because our dragMoveEvent accepts the event itself).
|
||||
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)
|
||||
|
||||
QShortcut(QKeySequence.StandardKey.Copy, self,
|
||||
context=Qt.ShortcutContext.WidgetShortcut,
|
||||
activated=self.copy_selection)
|
||||
@@ -679,20 +704,23 @@ class TrackTableView(QTableView):
|
||||
else:
|
||||
# library view is not a drop target for tracks
|
||||
accepted = self._playlist_mode
|
||||
pos = event.position().toPoint()
|
||||
if accepted:
|
||||
event.acceptProposedAction()
|
||||
self._set_drop_indicator(
|
||||
self._indicator_y(event.position().toPoint()))
|
||||
self._set_drop_indicator(self._indicator_y(pos))
|
||||
else:
|
||||
event.ignore()
|
||||
self._set_drop_indicator(None)
|
||||
self._update_autoscroll(pos, accepted)
|
||||
|
||||
def dragLeaveEvent(self, event):
|
||||
self._set_drop_indicator(None)
|
||||
self._stop_autoscroll()
|
||||
super().dragLeaveEvent(event)
|
||||
|
||||
def dropEvent(self, event):
|
||||
self._set_drop_indicator(None)
|
||||
self._stop_autoscroll()
|
||||
mime = event.mimeData()
|
||||
drop_row = self._drop_row(event.position().toPoint())
|
||||
if mime.hasUrls():
|
||||
@@ -751,6 +779,35 @@ class TrackTableView(QTableView):
|
||||
self._drop_indicator_y = y
|
||||
self.viewport().update()
|
||||
|
||||
# ---- 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 content scrolls under it, so
|
||||
# refresh the drop line for the rows now beneath it.
|
||||
if self._autoscroll_pos is not None:
|
||||
self._set_drop_indicator(self._indicator_y(self._autoscroll_pos))
|
||||
|
||||
def _stop_autoscroll(self):
|
||||
self._autoscroll_dir = 0
|
||||
self._autoscroll_pos = None
|
||||
self._autoscroll_timer.stop()
|
||||
|
||||
def paintEvent(self, event):
|
||||
super().paintEvent(event)
|
||||
if self._drop_indicator_y is None:
|
||||
|
||||
Reference in New Issue
Block a user