Cut removes immediately; scrollbar click-to-jump

Ctrl-X now removes the track from the playlist right away (so it visibly
leaves) and a paste re-inserts it at the target — replacing the earlier
deferred-move design (dropped move_tracks_between_playlists and the MIME
cut flag). A cut from the library is still just a copy. Separately, a
left-click on any scrollbar trough now jumps to that spot instead of
paging, via an app-wide ClickToJumpScrollStyle proxy.
This commit is contained in:
2026-06-26 22:12:27 -04:00
parent 29d997d205
commit 1274e12aeb
5 changed files with 98 additions and 129 deletions
+12 -20
View File
@@ -40,6 +40,7 @@ class PlaylistView(QWidget):
self.table.tracks_dropped.connect(self._on_tracks_dropped)
self.table.files_dropped.connect(self._on_files_dropped)
self.table.remove_requested.connect(self._on_remove)
self.table.cut_requested.connect(self._on_cut)
self.table.paste_requested.connect(self._on_paste)
manager.playlist_content_changed.connect(self._on_content_changed)
@@ -133,6 +134,13 @@ class PlaylistView(QWidget):
self._manager.remove_tracks_from_playlist(
self._playlist.persistent_id, rows)
def _on_cut(self, rows):
# Cut removes immediately (so the track visibly leaves); the ids are
# already on the clipboard for a later paste to re-insert.
if self._playlist is not None:
self._manager.remove_tracks_from_playlist(
self._playlist.persistent_id, rows)
def _on_paste(self):
if self._playlist is None:
return
@@ -140,24 +148,8 @@ class PlaylistView(QWidget):
if not payload:
return
track_ids = payload.get("track_ids", [])
if not track_ids:
return
pid = self._playlist.persistent_id
# Paste above the selected track (None -> append).
position = self.table.paste_anchor_row()
src_pid = payload.get("source_playlist", "")
# A cut from a real playlist is a *move* (no duplicate prompt); a copy
# — or a cut from the library, which has nothing to remove — just adds.
if payload.get("cut") and src_pid:
if src_pid == pid:
dest = position if position is not None else len(self._playlist.track_ids)
self._manager.move_tracks_in_playlist(
pid, payload.get("rows", []), dest)
else:
self._manager.move_tracks_between_playlists(
src_pid, payload.get("rows", []), pid, position)
QApplication.clipboard().clear() # a move pastes once
else:
if track_ids:
# Paste above the selected track (None -> append).
add_tracks_with_dup_check(
self._manager, self, pid, track_ids, position)
self._manager, self, self._playlist.persistent_id,
track_ids, self.table.paste_anchor_row())
+30 -14
View File
@@ -2,7 +2,7 @@ import json
import os
from PyQt6.QtWidgets import (
QTableView, QAbstractItemView, QMenu, QApplication,
QTableView, QAbstractItemView, QMenu, QApplication, QProxyStyle, QStyle,
)
from PyQt6.QtCore import (
Qt, QAbstractTableModel, QModelIndex, QSortFilterProxyModel, pyqtSignal,
@@ -28,6 +28,17 @@ SORT_ROLE = Qt.ItemDataRole.UserRole
AUTOSCROLL_MARGIN = 28
class ClickToJumpScrollStyle(QProxyStyle):
"""Makes a left-click on the scrollbar trough jump the thumb straight to
that spot (absolute position) instead of paging toward it a step at a time.
Wraps the default app style and only overrides the one hint."""
def styleHint(self, hint, option=None, widget=None, returnData=None):
if hint == QStyle.StyleHint.SH_ScrollBar_LeftClickAbsolutePosition:
return 1
return super().styleHint(hint, option, widget, returnData)
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
@@ -153,10 +164,10 @@ def speaker_pixmap(playing: bool, color: QColor) -> QPixmap:
def make_tracks_mime(track_ids: list[int], source_playlist: str = "",
rows: list[int] | None = None, cut: bool = False) -> QMimeData:
rows: list[int] | None = None) -> QMimeData:
mime = QMimeData()
payload = {"track_ids": track_ids, "source_playlist": source_playlist,
"rows": rows or [], "cut": cut}
"rows": rows or []}
mime.setData(TRACKS_MIME, json.dumps(payload).encode())
return mime
@@ -321,6 +332,7 @@ class TrackTableView(QTableView):
tracks_dropped = pyqtSignal(list, object) # track ids, insert row or None
files_dropped = pyqtSignal(list, object) # paths, insert row or None
remove_requested = pyqtSignal(list) # manual rows
cut_requested = pyqtSignal(list) # source rows to remove now
paste_requested = pyqtSignal()
info_requested = pyqtSignal(list) # selected track ids
tracks_changed = pyqtSignal() # displayed track set changed
@@ -487,20 +499,24 @@ class TrackTableView(QTableView):
# ---- copy / paste ----
def copy_selection(self):
self._put_on_clipboard(cut=False)
def cut_selection(self):
"""Mark the selection for a move: a later paste removes them from this
source (a real playlist) and inserts them at the paste target."""
self._put_on_clipboard(cut=True)
def _put_on_clipboard(self, cut: bool):
ids = self.selected_track_ids()
if not ids:
return
mime = make_tracks_mime(ids, self._source_playlist_id,
self.selected_source_rows(), cut=cut)
QApplication.clipboard().setMimeData(mime)
QApplication.clipboard().setMimeData(make_tracks_mime(
ids, self._source_playlist_id, self.selected_source_rows()))
def cut_selection(self):
"""Copy the selection, then — in a playlist — remove it immediately so
it visibly leaves; a paste re-inserts it at the target. From the
library there's nothing to remove, so this is just a copy."""
ids = self.selected_track_ids()
if not ids:
return
rows = self.selected_source_rows()
QApplication.clipboard().setMimeData(
make_tracks_mime(ids, self._source_playlist_id, rows))
if self._playlist_mode and self._source_playlist_id:
self.cut_requested.emit(rows)
def paste_anchor_row(self):
"""Source-model row to paste *above* — the first selected track — or