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. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
147
tests/test_round12.py
Normal file
147
tests/test_round12.py
Normal file
@ -0,0 +1,147 @@
|
||||
"""Round 12: cut (Ctrl-X) + paste-above-selection.
|
||||
|
||||
- The clipboard MIME carries a `cut` flag; a cut from a real playlist is a
|
||||
*move* on paste, a copy (or a cut from the library) just adds.
|
||||
- Paste inserts *above the currently selected track*, not at the end.
|
||||
- A cross-playlist move is one undo step (move_tracks_between_playlists).
|
||||
"""
|
||||
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
|
||||
from lintunes.models import Library, Track
|
||||
from lintunes.library_manager import LibraryManager
|
||||
from lintunes.gui.track_table import (
|
||||
TrackTableView, make_tracks_mime, parse_tracks_mime)
|
||||
from lintunes.gui.playlist_view import PlaylistView
|
||||
|
||||
|
||||
def _manager(tmp_path, n=4):
|
||||
library = Library(tracks={i: Track(track_id=i, name=f"T{i}")
|
||||
for i in range(1, n + 1)})
|
||||
return LibraryManager(library, tmp_path)
|
||||
|
||||
|
||||
def _select_source_row(table, source_row):
|
||||
"""Select the visual row backing a given source-model row (sort-proof)."""
|
||||
proxy_index = table.proxy.mapFromSource(table.model_.index(source_row, 0))
|
||||
table.selectRow(proxy_index.row())
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# MIME + table helpers
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
class TestMimeAndHelpers:
|
||||
def test_cut_flag_round_trips(self):
|
||||
payload = parse_tracks_mime(
|
||||
make_tracks_mime([1, 2], "PID", [0, 1], cut=True))
|
||||
assert payload["cut"] is True
|
||||
assert payload["track_ids"] == [1, 2]
|
||||
assert payload["source_playlist"] == "PID"
|
||||
assert payload["rows"] == [0, 1]
|
||||
|
||||
def test_copy_defaults_to_not_cut(self):
|
||||
payload = parse_tracks_mime(make_tracks_mime([1]))
|
||||
assert payload["cut"] is False
|
||||
|
||||
def test_paste_anchor_row(self, qapp, tmp_path):
|
||||
manager = _manager(tmp_path)
|
||||
table = TrackTableView(playlist_mode=True)
|
||||
table.set_tracks([manager.library.tracks[i] for i in (1, 2, 3)])
|
||||
assert table.paste_anchor_row() is None # nothing selected
|
||||
_select_source_row(table, 1)
|
||||
assert table.paste_anchor_row() == 1
|
||||
|
||||
def test_cut_selection_marks_clipboard(self, qapp, tmp_path):
|
||||
manager = _manager(tmp_path)
|
||||
table = TrackTableView(playlist_mode=True)
|
||||
table.set_source_playlist("PID")
|
||||
table.set_tracks([manager.library.tracks[i] for i in (1, 2, 3)])
|
||||
_select_source_row(table, 0)
|
||||
table.cut_selection()
|
||||
payload = parse_tracks_mime(QApplication.clipboard().mimeData())
|
||||
assert payload["cut"] is True
|
||||
assert payload["track_ids"] == [1]
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# manager: single-undo cross-playlist move
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
class TestMoveBetweenPlaylists:
|
||||
def test_move_and_single_undo(self, tmp_path):
|
||||
manager = _manager(tmp_path)
|
||||
a = manager.create_playlist("A")
|
||||
b = manager.create_playlist("B")
|
||||
manager.add_tracks_to_playlist(a.persistent_id, [1, 2, 3])
|
||||
manager.add_tracks_to_playlist(b.persistent_id, [4])
|
||||
|
||||
manager.move_tracks_between_playlists(
|
||||
a.persistent_id, [0, 1], b.persistent_id, 0)
|
||||
|
||||
assert a.track_ids == [3]
|
||||
assert b.track_ids == [1, 2, 4]
|
||||
|
||||
manager.undo_stack.undo() # one step reverts both playlists
|
||||
assert a.track_ids == [1, 2, 3]
|
||||
assert b.track_ids == [4]
|
||||
|
||||
def test_same_playlist_is_a_noop(self, tmp_path):
|
||||
manager = _manager(tmp_path)
|
||||
a = manager.create_playlist("A")
|
||||
manager.add_tracks_to_playlist(a.persistent_id, [1, 2, 3])
|
||||
manager.move_tracks_between_playlists(
|
||||
a.persistent_id, [0], a.persistent_id, 2)
|
||||
assert a.track_ids == [1, 2, 3]
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# PlaylistView paste integration
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
class TestPasteIntegration:
|
||||
def test_copy_paste_inserts_above_selection(self, qapp, tmp_path):
|
||||
manager = _manager(tmp_path)
|
||||
b = manager.create_playlist("B")
|
||||
manager.add_tracks_to_playlist(b.persistent_id, [4])
|
||||
view = PlaylistView(manager)
|
||||
view.show_playlist(b)
|
||||
|
||||
QApplication.clipboard().setMimeData(make_tracks_mime([1, 2]))
|
||||
_select_source_row(view.table, 0) # above track 4
|
||||
view._on_paste()
|
||||
|
||||
assert b.track_ids == [1, 2, 4]
|
||||
|
||||
def test_cut_paste_moves_across_playlists(self, qapp, tmp_path):
|
||||
manager = _manager(tmp_path)
|
||||
a = manager.create_playlist("A")
|
||||
b = manager.create_playlist("B")
|
||||
manager.add_tracks_to_playlist(a.persistent_id, [1, 2, 3])
|
||||
manager.add_tracks_to_playlist(b.persistent_id, [4])
|
||||
view = PlaylistView(manager)
|
||||
view.show_playlist(b)
|
||||
|
||||
QApplication.clipboard().setMimeData(
|
||||
make_tracks_mime([1, 2], a.persistent_id, [0, 1], cut=True))
|
||||
_select_source_row(view.table, 0) # above track 4
|
||||
view._on_paste()
|
||||
|
||||
assert b.track_ids == [1, 2, 4]
|
||||
assert a.track_ids == [3]
|
||||
# The move consumes the clipboard so it can't be pasted again.
|
||||
assert parse_tracks_mime(QApplication.clipboard().mimeData()) is None
|
||||
|
||||
def test_cut_paste_same_playlist_reorders(self, qapp, tmp_path):
|
||||
manager = _manager(tmp_path)
|
||||
a = manager.create_playlist("A")
|
||||
manager.add_tracks_to_playlist(a.persistent_id, [1, 2, 3, 4])
|
||||
view = PlaylistView(manager)
|
||||
view.show_playlist(a)
|
||||
|
||||
QApplication.clipboard().setMimeData(
|
||||
make_tracks_mime([1], a.persistent_id, [0], cut=True))
|
||||
_select_source_row(view.table, 2) # above track 3
|
||||
view._on_paste()
|
||||
|
||||
assert a.track_ids == [2, 1, 3, 4]
|
||||
Reference in New Issue
Block a user