Add Ctrl-X cut and paste-above-selection
Cut (Ctrl-X) marks the selection on the clipboard with a `cut` flag; a paste of a cut from a real playlist is a *move* (single-undo across playlists via move_tracks_between_playlists; same-playlist reorders), while a copy — or a cut from the library, which has nothing to remove — just adds. Paste now inserts *above the selected track* rather than appending. Cut/Paste added to the track context menu; parse_tracks_mime now tolerates a None/empty clipboard.
This commit is contained in:
@@ -137,7 +137,27 @@ class PlaylistView(QWidget):
|
||||
if self._playlist is None:
|
||||
return
|
||||
payload = parse_tracks_mime(QApplication.clipboard().mimeData())
|
||||
if payload:
|
||||
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:
|
||||
add_tracks_with_dup_check(
|
||||
self._manager, self, self._playlist.persistent_id,
|
||||
payload.get("track_ids", []))
|
||||
self._manager, self, pid, track_ids, position)
|
||||
|
||||
@@ -136,16 +136,16 @@ def speaker_pixmap(playing: bool, color: QColor) -> QPixmap:
|
||||
|
||||
|
||||
def make_tracks_mime(track_ids: list[int], source_playlist: str = "",
|
||||
rows: list[int] | None = None) -> QMimeData:
|
||||
rows: list[int] | None = None, cut: bool = False) -> QMimeData:
|
||||
mime = QMimeData()
|
||||
payload = {"track_ids": track_ids, "source_playlist": source_playlist,
|
||||
"rows": rows or []}
|
||||
"rows": rows or [], "cut": cut}
|
||||
mime.setData(TRACKS_MIME, json.dumps(payload).encode())
|
||||
return mime
|
||||
|
||||
|
||||
def parse_tracks_mime(mime: QMimeData) -> dict | None:
|
||||
if not mime.hasFormat(TRACKS_MIME):
|
||||
if mime is None or not mime.hasFormat(TRACKS_MIME):
|
||||
return None
|
||||
try:
|
||||
return json.loads(bytes(mime.data(TRACKS_MIME)).decode())
|
||||
@@ -360,6 +360,9 @@ class TrackTableView(QTableView):
|
||||
QShortcut(QKeySequence.StandardKey.Copy, self,
|
||||
context=Qt.ShortcutContext.WidgetShortcut,
|
||||
activated=self.copy_selection)
|
||||
QShortcut(QKeySequence.StandardKey.Cut, self,
|
||||
context=Qt.ShortcutContext.WidgetShortcut,
|
||||
activated=self.cut_selection)
|
||||
QShortcut(QKeySequence.StandardKey.Paste, self,
|
||||
context=Qt.ShortcutContext.WidgetShortcut,
|
||||
activated=self.paste_requested)
|
||||
@@ -459,13 +462,27 @@ 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())
|
||||
self.selected_source_rows(), cut=cut)
|
||||
QApplication.clipboard().setMimeData(mime)
|
||||
|
||||
def paste_anchor_row(self):
|
||||
"""Source-model row to paste *above* — the first selected track — or
|
||||
None to append when nothing is selected."""
|
||||
rows = self.selected_source_rows()
|
||||
return rows[0] if rows else None
|
||||
|
||||
# ---- header interactions ----
|
||||
|
||||
def _on_sort_indicator(self, section, order):
|
||||
@@ -511,6 +528,11 @@ class TrackTableView(QTableView):
|
||||
menu = QMenu(self)
|
||||
info_action = menu.addAction("Get Info\tCtrl+I")
|
||||
copy_action = menu.addAction("Copy\tCtrl+C")
|
||||
cut_action = menu.addAction("Cut\tCtrl+X")
|
||||
paste_action = None
|
||||
if (self._playlist_mode
|
||||
and parse_tracks_mime(QApplication.clipboard().mimeData())):
|
||||
paste_action = menu.addAction("Paste\tCtrl+V")
|
||||
|
||||
locations = self.selected_locations()
|
||||
reveal_action = copy_path_action = None
|
||||
@@ -543,6 +565,10 @@ class TrackTableView(QTableView):
|
||||
self.info_requested.emit(self.selected_track_ids())
|
||||
elif chosen is copy_action:
|
||||
self.copy_selection()
|
||||
elif chosen is cut_action:
|
||||
self.cut_selection()
|
||||
elif paste_action is not None and chosen is paste_action:
|
||||
self.paste_requested.emit()
|
||||
elif chosen is reveal_action:
|
||||
reveal_paths(locations)
|
||||
elif chosen is copy_path_action:
|
||||
|
||||
Reference in New Issue
Block a user