WIP V3: fixup row insertion/deletion

All row insertions and deletions are now wrapped in beginRemoveRows /
endRemoveRows (and similar for insertions).
This commit is contained in:
Keith Edmunds 2023-11-17 22:17:47 +00:00
parent e37f62fe87
commit 199f0e27fa
5 changed files with 121 additions and 76 deletions

View File

@ -489,17 +489,17 @@ class PlaylistRows(Base):
session.flush() session.flush()
@staticmethod @staticmethod
def delete_rows( def delete_row(
session: scoped_session, playlist_id: int, row_numbers: List[int] session: scoped_session, playlist_id: int, row_number: int
) -> None: ) -> None:
""" """
Delete passed rows in given playlist. Delete passed row in given playlist.
""" """
session.execute( session.execute(
delete(PlaylistRows).where( delete(PlaylistRows).where(
PlaylistRows.playlist_id == playlist_id, PlaylistRows.playlist_id == playlist_id,
PlaylistRows.plr_rownum.in_(row_numbers), PlaylistRows.plr_rownum == row_number,
) )
) )

View File

@ -847,8 +847,9 @@ class Window(QMainWindow, Ui_MainWindow):
dlg.resize(500, 100) dlg.resize(500, 100)
ok = dlg.exec() ok = dlg.exec()
if ok: if ok:
model.insert_header_row( model.insert_row(
self.active_tab().get_selected_row_number(), dlg.textValue() proposed_row_number=self.active_tab().get_selected_row_number(),
note=dlg.textValue(),
) )
def insert_track(self) -> None: def insert_track(self) -> None:

View File

@ -147,15 +147,7 @@ class PlaylistModel(QAbstractTableModel):
if playlist_id != self.playlist_id: if playlist_id != self.playlist_id:
return return
# Insert track if we have one self.insert_row(proposed_row_number=new_row_number, track_id=track_id, note=note)
if track_id:
self.insert_track_row(new_row_number, track_id, note)
# If we only have a note, add as a header row
elif note:
self.insert_header_row(new_row_number, note)
else:
# No track, no note, no point
return
def add_track_to_header( def add_track_to_header(
self, self,
@ -345,10 +337,17 @@ class PlaylistModel(QAbstractTableModel):
def delete_rows(self, row_numbers: List[int]) -> None: def delete_rows(self, row_numbers: List[int]) -> None:
""" """
Delete passed rows from model Delete passed rows from model
Need to delete them in contiguous groups wrapped in beginRemoveRows / endRemoveRows
calls. To keep it simple, if inefficient, delete rows one by one.
""" """
with Session() as session: with Session() as session:
PlaylistRows.delete_rows(session, self.playlist_id, row_numbers) for row_number in row_numbers:
super().beginRemoveRows(QModelIndex(), row_number, row_number)
PlaylistRows.delete_row(session, self.playlist_id, row_number)
super().endRemoveRows()
PlaylistRows.fixup_rownumbers(session, self.playlist_id) PlaylistRows.fixup_rownumbers(session, self.playlist_id)
self.refresh_data(session) self.refresh_data(session)
self.update_track_times() self.update_track_times()
@ -448,6 +447,25 @@ class PlaylistModel(QAbstractTableModel):
return QVariant(boldfont) return QVariant(boldfont)
def _get_new_row_number(self, proposed_row_number: Optional[int]) -> int:
"""
Sanitises proposed new row number.
If proposed_row_number given, ensure it is valid.
If not given, return row number to add to end of model.
"""
if proposed_row_number is None or proposed_row_number > len(self.playlist_rows):
# We are adding to the end of the list
new_row_number = len(self.playlist_rows)
elif proposed_row_number < 0:
# Add to start of list
new_row_number = 0
else:
new_row_number = proposed_row_number
return new_row_number
def get_row_track_path(self, row_number: int) -> str: def get_row_track_path(self, row_number: int) -> str:
""" """
Return path of track associated with row or empty string if no track associated Return path of track associated with row or empty string if no track associated
@ -600,66 +618,33 @@ class PlaylistModel(QAbstractTableModel):
return self.playlist_rows[row_number].played return self.playlist_rows[row_number].played
def insert_header_row(self, row_number: Optional[int], text: str) -> None: def insert_row(
""" self,
Insert a header row. proposed_row_number: Optional[int],
""" track_id: Optional[int] = None,
note: Optional[str] = None,
with Session() as session:
plr = self._insert_row(session, row_number)
# Update the PlaylistRows object
plr.note = text
# Repopulate self.playlist_rows
self.refresh_data(session)
# Update the display from the new row onwards
self.invalidate_rows(list(range(plr.plr_rownum, len(self.playlist_rows))))
def _insert_row(
self, session: scoped_session, row_number: Optional[int]
) -> PlaylistRows: ) -> PlaylistRows:
"""
Insert a row in the database.
If row_number is greater than length of list plus 1, or if row
number is None, put row at end of list.
Move existing rows to make space if ncessary.
Return the new PlaylistRows object.
"""
if row_number is None or row_number > len(self.playlist_rows):
# We are adding to the end of the list so we can optimise
new_row_number = len(self.playlist_rows)
return PlaylistRows(session, self.playlist_id, new_row_number)
elif row_number < 0:
raise ValueError(
f"playlistmodel._insert_row, invalid row number ({row_number})"
)
else:
new_row_number = row_number
# Insert the new row and return it
return PlaylistRows.insert_row(session, self.playlist_id, new_row_number)
def insert_track_row(
self, row_number: Optional[int], track_id: int, text: Optional[str]
) -> None:
""" """
Insert a track row. Insert a track row.
""" """
new_row_number = self._get_new_row_number(proposed_row_number)
with Session() as session: with Session() as session:
plr = self._insert_row(session, row_number) super().beginInsertRows(QModelIndex(), new_row_number, new_row_number)
# Update the PlaylistRows object plr = PlaylistRows.insert_row(session, self.playlist_id, new_row_number)
plr.track_id = track_id plr.track_id = track_id
if text: if note:
plr.note = text plr.note = note
# Repopulate self.playlist_rows
self.refresh_data(session) self.refresh_data(session)
# Update the display from the new row onwards super().endInsertRows()
self.invalidate_rows(list(range(plr.plr_rownum, len(self.playlist_rows)))) self.invalidate_rows(list(range(plr.plr_rownum, len(self.playlist_rows))))
return plr
def invalidate_row(self, modified_row: int) -> None: def invalidate_row(self, modified_row: int) -> None:
""" """
Signal to view to refresh invlidated row Signal to view to refresh invlidated row

View File

@ -205,7 +205,26 @@ class PlaylistTab(QTableView):
self.setModel(PlaylistModel(playlist_id)) self.setModel(PlaylistModel(playlist_id))
self._set_column_widths() self._set_column_widths()
# ########## Events other than cell editing ########## def closeEditor(
self, editor: QWidget | None, hint: QAbstractItemDelegate.EndEditHint
) -> None:
"""
Override closeEditor to enable play controls and update display.
"""
self.musicmuster.enable_play_next_controls()
self.musicmuster.actionSetNext.setEnabled(True)
self.musicmuster.action_Clear_selection.setEnabled(True)
super(PlaylistTab, self).closeEditor(editor, hint)
# Optimise row heights after increasing row height for editing
self.resizeRowsToContents()
# Update start times in case a start time in a note has been
# edited
model = cast(PlaylistModel, self.model())
model.update_track_times()
def dropEvent(self, event): def dropEvent(self, event):
if event.source() is not self or ( if event.source() is not self or (
@ -230,6 +249,25 @@ class PlaylistTab(QTableView):
event.accept() event.accept()
def edit(
self,
index: QModelIndex,
trigger: QAbstractItemView.EditTrigger,
event: Optional[QEvent],
) -> bool:
"""
Override PySide2.QAbstractItemView.edit to catch when editing starts
Editing only ever starts with a double click on a cell
"""
# 'result' will only be true on double-click
result = super().edit(index, trigger, event)
if result:
self.musicmuster.disable_play_next_controls()
return result
def _add_context_menu( def _add_context_menu(
self, self,
text: str, text: str,

View File

@ -2,6 +2,8 @@ from app.models import (
Playlists, Playlists,
Tracks, Tracks,
) )
from PyQt6.QtCore import Qt
from app.helpers import get_file_metadata from app.helpers import get_file_metadata
from app import playlistmodel from app import playlistmodel
from dbconfig import scoped_session from dbconfig import scoped_session
@ -25,7 +27,7 @@ def create_model_with_tracks(session: scoped_session) -> "playlistmodel.Playlist
track_path = test_tracks[row % len(test_tracks)] track_path = test_tracks[row % len(test_tracks)]
metadata = get_file_metadata(track_path) metadata = get_file_metadata(track_path)
track = Tracks(session, **metadata) track = Tracks(session, **metadata)
model.insert_track_row(row, track.id, f"{row=}") model.insert_row(proposed_row_number=row, track_id=track.id, note=f"{row=}")
session.commit() session.commit()
return model return model
@ -38,10 +40,8 @@ def create_model_with_playlist_rows(
# Create a model # Create a model
model = playlistmodel.PlaylistModel(playlist.id) model = playlistmodel.PlaylistModel(playlist.id)
for row in range(rows): for row in range(rows):
plr = model._insert_row(session, row) plr = model.insert_row(proposed_row_number=row, note=str(row))
newrow = plr.plr_rownum model.playlist_rows[plr.plr_rownum] = playlistmodel.PlaylistRowData(plr)
plr.note = str(newrow)
model.playlist_rows[newrow] = playlistmodel.PlaylistRowData(plr)
session.commit() session.commit()
return model return model
@ -195,7 +195,7 @@ def test_insert_header_row_end(monkeypatch, session):
initial_row_count = 11 initial_row_count = 11
model = create_model_with_playlist_rows(session, initial_row_count) model = create_model_with_playlist_rows(session, initial_row_count)
model.insert_header_row(None, note_text) model.insert_row(proposed_row_number=None, note=note_text)
assert model.rowCount() == initial_row_count + 1 assert model.rowCount() == initial_row_count + 1
prd = model.playlist_rows[model.rowCount() - 1] prd = model.playlist_rows[model.rowCount() - 1]
# Test against edit_role because display_role for headers is # Test against edit_role because display_role for headers is
@ -215,7 +215,7 @@ def test_insert_header_row_middle(monkeypatch, session):
insert_row = 6 insert_row = 6
model = create_model_with_playlist_rows(session, initial_row_count) model = create_model_with_playlist_rows(session, initial_row_count)
model.insert_header_row(insert_row, note_text) model.insert_row(proposed_row_number=insert_row, note=note_text)
assert model.rowCount() == initial_row_count + 1 assert model.rowCount() == initial_row_count + 1
prd = model.playlist_rows[insert_row] prd = model.playlist_rows[insert_row]
# Test against edit_role because display_role for headers is # Test against edit_role because display_role for headers is
@ -239,14 +239,35 @@ def test_timing_one_track(monkeypatch, session):
monkeypatch.setattr(playlistmodel, "Session", session) monkeypatch.setattr(playlistmodel, "Session", session)
model = create_model_with_tracks(session) model = create_model_with_tracks(session)
model.insert_header_row(START_ROW, "start+") model.insert_row(proposed_row_number=START_ROW, note="start+")
model.insert_header_row(END_ROW, "-") model.insert_row(proposed_row_number=END_ROW, note="-")
prd = model.playlist_rows[START_ROW] prd = model.playlist_rows[START_ROW]
qv_value = model.display_role(START_ROW, playlistmodel.HEADER_NOTES_COLUMN, prd) qv_value = model.display_role(START_ROW, playlistmodel.HEADER_NOTES_COLUMN, prd)
assert qv_value.value() == "start [1 tracks, 4:23 unplayed]" assert qv_value.value() == "start [1 tracks, 4:23 unplayed]"
def test_insert_track_new_playlist(monkeypatch, session):
# insert a track into a new playlist
monkeypatch.setattr(playlistmodel, "Session", session)
playlist = Playlists(session, "test playlist")
# Create a model
model = playlistmodel.PlaylistModel(playlist.id)
track_path = test_tracks[0]
metadata = get_file_metadata(track_path)
track = Tracks(session, **metadata)
model.insert_row(proposed_row_number=0, track_id=track.id)
prd = model.playlist_rows[model.rowCount() - 1]
assert (
model.edit_role(model.rowCount() - 1, playlistmodel.Col.TITLE.value, prd)
== metadata["title"]
)
# def test_edit_header(monkeypatch, session): # edit header row in middle of playlist # def test_edit_header(monkeypatch, session): # edit header row in middle of playlist
# monkeypatch.setattr(playlistmodel, "Session", session) # monkeypatch.setattr(playlistmodel, "Session", session)