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:
@@ -1,3 +1,3 @@
|
||||
"""LinTunes — iTunes-style music library manager and player for Linux."""
|
||||
|
||||
__version__ = "0.44.1"
|
||||
__version__ = "0.45.0"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user