v0.45.0: copying songs pastes into other apps as a song list

Ctrl+C (and Cut) on selected songs now also puts plain text on the
clipboard — one "Artist - Title" per line, in the order the table shows —
so a whole playlist pastes into a message or a document. Pasting back into
a playlist still reads the songs themselves, as before.
This commit is contained in:
2026-10-10 02:17:00 -07:00
parent 491e31b4d6
commit 43d2d21d9d
3 changed files with 81 additions and 4 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
"""LinTunes — iTunes-style music library manager and player for Linux."""
__version__ = "0.44.1"
__version__ = "0.45.0"
+25 -3
View File
@@ -269,14 +269,27 @@ 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,
text: str = "") -> QMimeData:
"""The clipboard/drag payload for tracks. ``text``, when given, rides
along as plain text, so a copy pastes into other apps as a song list
while a paste here still reads the ids."""
mime = QMimeData()
payload = {"track_ids": track_ids, "source_playlist": source_playlist,
"rows": rows or []}
mime.setData(TRACKS_MIME, json.dumps(payload).encode())
if text:
mime.setText(text)
return mime
def song_line(track) -> str:
"""One song as text: "Artist - Title", or the title alone."""
title = (track.name or "").strip() or "Untitled"
artist = (track.artist or "").strip()
return f"{artist} - {title}" if artist else title
def parse_tracks_mime(mime: QMimeData) -> dict | None:
if mime is None or not mime.hasFormat(TRACKS_MIME):
return None
@@ -765,12 +778,20 @@ class TrackTableView(QTableView):
# ---- copy / paste ----
def selected_text(self) -> str:
"""The selection as a song list, one per line, in the order shown
(so a list sorted by artist pastes sorted by artist)."""
rows = sorted(idx.row() for idx in self.selectionModel().selectedRows())
return "\n".join(song_line(self.model_.track_at(self.model_.source_row(r)))
for r in rows)
def copy_selection(self):
ids = self.selected_track_ids()
if not ids or self.read_only:
return
QApplication.clipboard().setMimeData(make_tracks_mime(
ids, self._source_playlist_id, self.selected_source_rows()))
ids, self._source_playlist_id, self.selected_source_rows(),
text=self.selected_text()))
def cut_selection(self):
"""Copy the selection, then — in a playlist — remove it immediately so
@@ -783,7 +804,8 @@ class TrackTableView(QTableView):
return
rows = self.selected_source_rows()
QApplication.clipboard().setMimeData(
make_tracks_mime(ids, self._source_playlist_id, rows))
make_tracks_mime(ids, self._source_playlist_id, rows,
text=self.selected_text()))
if self._playlist_mode and self._source_playlist_id and self._content_editable:
self.cut_requested.emit(rows)
+55
View File
@@ -227,3 +227,58 @@ class TestLaserDiagram:
assert pitched[2].x() != pytest.approx(square[2].x())
# A square screen's vertical edges are vertical.
assert square[1].x() == pytest.approx(square[2].x())
# --------------------------------------------------------------------------
# 5. Copying songs gives other apps a song list
# --------------------------------------------------------------------------
def _table(tracks, playlist_mode=True):
from lintunes.gui.track_table import TrackTableView
table = TrackTableView(playlist_mode=playlist_mode)
table.set_source_playlist("PID")
table.set_tracks(tracks)
return table
def _songs():
from lintunes.models import Track
return [Track(track_id=1, name="Low Tide", artist="The Wheelhouse"),
Track(track_id=2, name="Café Noir", artist="Marguerite Vaux"),
Track(track_id=3, name="Orphan Take", artist="")]
class TestCopyAsText:
def test_select_all_copy_pastes_as_a_list(self, qapp):
from lintunes.gui.track_table import parse_tracks_mime
table = _table(_songs())
table.selectAll()
table.copy_selection()
mime = QApplication.clipboard().mimeData()
assert mime.text() == ("The Wheelhouse - Low Tide\n"
"Marguerite Vaux - Café Noir\n"
"Orphan Take")
# …and pasting into a playlist still reads the songs themselves.
assert parse_tracks_mime(mime)["track_ids"] == [1, 2, 3]
def test_the_list_follows_the_order_shown(self, qapp):
from PyQt6.QtCore import Qt
table = _table(_songs(), playlist_mode=False)
name = table.model_.fields.index("name")
table.sortByColumn(name, Qt.SortOrder.AscendingOrder)
table.selectAll()
table.copy_selection()
assert QApplication.clipboard().text().splitlines() == [
"Marguerite Vaux - Café Noir", "The Wheelhouse - Low Tide",
"Orphan Take"]
def test_cut_carries_the_list_too(self, qapp):
table = _table(_songs())
table.selectRow(0)
table.cut_selection()
assert QApplication.clipboard().text() == "The Wheelhouse - Low Tide"
def test_untitled_song(self, qapp):
from lintunes.gui.track_table import song_line
from lintunes.models import Track
assert song_line(Track(track_id=9, name="", artist="X")) == "X - Untitled"