diff --git a/lintunes/__init__.py b/lintunes/__init__.py index 35a48f3..3889316 100644 --- a/lintunes/__init__.py +++ b/lintunes/__init__.py @@ -1,3 +1,3 @@ """LinTunes — iTunes-style music library manager and player for Linux.""" -__version__ = "0.44.1" +__version__ = "0.45.0" diff --git a/lintunes/gui/track_table.py b/lintunes/gui/track_table.py index c9410d8..53c416a 100644 --- a/lintunes/gui/track_table.py +++ b/lintunes/gui/track_table.py @@ -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) diff --git a/tests/test_round79.py b/tests/test_round79.py index 444820f..fddd6bf 100644 --- a/tests/test_round79.py +++ b/tests/test_round79.py @@ -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"