diff --git a/app/models.py b/app/models.py index 8068ec6..e91e27b 100644 --- a/app/models.py +++ b/app/models.py @@ -430,11 +430,6 @@ class PlaylistRows(Base): track_id = Column(Integer, ForeignKey('tracks.id'), nullable=True) track = relationship("Tracks", back_populates="playlistrows") - # Ensure row numbers are unique within each playlist - __table_args__ = (UniqueConstraint - ('row_number', 'playlist_id', name="uniquerow"), - ) - def __repr__(self) -> str: return ( f" None: - """ - Delete rows in given playlist that have a higher row number - than 'row' - """ + @staticmethod + def delete_higher_rows(session: Session, playlist_id: int, row: int) \ + -> None: + """ + Delete rows in given playlist that have a higher row number + than 'row' + """ - # Log the rows to be deleted - rows_to_go = session.execute( - select(PlaylistRows) - .where(PlaylistRows.playlist_id == playlist_id, - PlaylistRows.row_number > row) - ).scalars().all() - if not rows_to_go: - return + # Log the rows to be deleted + rows_to_go = session.execute( + select(PlaylistRows) + .where(PlaylistRows.playlist_id == playlist_id, + PlaylistRows.row_number > row) + ).scalars().all() + if not rows_to_go: + return - for row in rows_to_go: - log.debu(f"Should delete: {row}") - # If needed later: - # session.delete(row) + for row in rows_to_go: + log.debu(f"Should delete: {row}") + # If needed later: + # session.delete(row) # @classmethod diff --git a/app/playlists.py b/app/playlists.py index ae33cf0..a7dbee5 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -9,9 +9,9 @@ from PyQt5.QtCore import Qt from PyQt5.QtGui import ( QColor, QFont, - # QDropEvent + QDropEvent ) -# from PyQt5 import QtWidgets +from PyQt5 import QtWidgets from PyQt5.QtWidgets import ( QAbstractItemView, # QApplication, @@ -37,9 +37,8 @@ from helpers import ( get_relative_date, # open_in_audacity ) -# from log import log.debug, log.error +from log import log from models import ( - # Notes, Playdates, Playlists, PlaylistRows, @@ -115,9 +114,9 @@ class PlaylistTab(QTableWidget): # Set up widget # self.setEditTriggers(QtWidgets.QAbstractItemView.AllEditTriggers) self.setAlternatingRowColors(True) -# self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) -# self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) -# self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) + self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) + self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) + self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) self.setRowCount(0) self.setColumnCount(len(columns)) @@ -133,14 +132,12 @@ class PlaylistTab(QTableWidget): key=lambda item: item.idx))] ) -# self.setDragEnabled(True) -# self.setAcceptDrops(True) -# self.viewport().setAcceptDrops(True) -# self.setDragDropOverwriteMode(False) -# self.setDropIndicatorShown(True) -# self.setSelectionMode(QAbstractItemView.ExtendedSelection) -# self.setSelectionBehavior(QAbstractItemView.SelectRows) -# self.setDragDropMode(QAbstractItemView.InternalMove) + self.setAcceptDrops(True) + self.viewport().setAcceptDrops(True) + self.setDragDropOverwriteMode(False) + self.setDropIndicatorShown(True) + self.setDragDropMode(QAbstractItemView.InternalMove) + self.setDragEnabled(False) # # # This property defines how the widget shows a context menu # self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) @@ -182,55 +179,66 @@ class PlaylistTab(QTableWidget): if record.f_int != self.columnWidth(idx): record.update(session, {'f_int': width}) -# def __repr__(self) -> str: -# return f" str: + return f" None: -# # if not event.isAccepted() and event.source() == self: -# if not event.source() == self: -# return # We don't accept external drops -# -# drop_row: int = self._drop_on(event) -# -# rows: List = sorted(set(item.row() for item in self.selectedItems())) -# rows_to_move = [ -# [QTableWidgetItem(self.item(row_index, column_index)) for -# column_index in range(self.columnCount())] -# for row_index in rows -# ] -# for row_index in reversed(rows): -# self.removeRow(row_index) -# if row_index < drop_row: -# drop_row -= 1 -# -# for row_index, data in enumerate(rows_to_move): -# row_index += drop_row -# self.insertRow(row_index) -# for column_index, column_data in enumerate(data): -# self.setItem(row_index, column_index, column_data) -# event.accept() -# # The above doesn't handle column spans, which we use in note -# # rows. Check and fix: -# row = 0 # So row is defined even if there are no rows in range -# for row in range(drop_row, drop_row + len(rows_to_move)): -# if row in self._get_notes_rows(): -# self.setSpan( -# row, self.COL_NOTE, self.NOTE_ROW_SPAN, self.NOTE_COL_SPAN) -# -# # Scroll to drop zone -# self.scrollToItem(self.item(row, 1)) -# super().dropEvent(event) -# -# log.debug( -# "playlist.dropEvent(): " -# f"Moved row(s) {rows} to become row {drop_row}" -# ) -# -# with Session() as session: # checked -# self.save_playlist(session) -# self.update_display(session) +# def closeEditor(self, editor, hint): # review +# super(PlaylistTab, self).closeEditor(editor, hint) +# self.cellEditingEnded.emit() + + def dropEvent(self, event: QDropEvent) -> None: + """ + Handle drag/drop of rows + + https://stackoverflow.com/questions/26227885/drag-and-drop-rows-within-qtablewidget + """ + + if not event.source() == self: + return # We don't accept external drops + + drop_row: int = self._drop_on(event) + + rows: List = sorted(set(item.row() for item in self.selectedItems())) + rows_to_move = [ + [QTableWidgetItem(self.item(row_index, column_index)) for + column_index in range(self.columnCount())] + for row_index in rows + ] + for row_index in reversed(rows): + self.removeRow(row_index) + if row_index < drop_row: + drop_row -= 1 + + for row_index, data in enumerate(rows_to_move): + row_index += drop_row + self.insertRow(row_index) + for column_index, column_data in enumerate(data): + self.setItem(row_index, column_index, column_data) + event.accept() + # The above doesn't handle column spans, which we use in note + # rows. Check and fix: + for row in range(drop_row, drop_row + len(rows_to_move)): + if not self._get_row_track_id(row): + self.setSpan(row, 1, 1, len(columns)) + + # Scroll to drop zone + self.scrollToItem(self.item(row, 1)) + + # Reset drag mode to allow row selection by dragging + self.setDragEnabled(False) + + super().dropEvent(event) + + log.debug( + "playlist.dropEvent(): " + f"Moved row(s) {rows} to become row {drop_row}" + ) + + with Session() as session: # checked + self.save_playlist(session) + self.update_display(session) # # def edit(self, index, trigger, event): # review # result = super(PlaylistTab, self).edit(index, trigger, event) @@ -238,10 +246,6 @@ class PlaylistTab(QTableWidget): # self.cellEditingStarted.emit(index.row(), index.column()) # return result # -# def closeEditor(self, editor, hint): # review -# super(PlaylistTab, self).closeEditor(editor, hint) -# self.cellEditingEnded.emit() -# # def eventFilter(self, source, event): # review # """Used to process context (right-click) menu, which is defined here""" # @@ -287,7 +291,18 @@ class PlaylistTab(QTableWidget): # act_delete.triggered.connect(self._delete_rows) # # return super(PlaylistTab, self).eventFilter(source, event) -# + + def mouseReleaseEvent(self, event): + """ + Enable dragging if rows are selected + """ + + if self.selectedItems(): + self.setDragEnabled(True) + else: + self.setDragEnabled(False) + super().mouseReleaseEvent(event) + # # ########## Externally called functions ########## # # def closeEvent(self, event) -> None: @@ -648,27 +663,27 @@ class PlaylistTab(QTableWidget): # KAE self.save_playlist(session) self.update_display(session) - def save_playlist(self, session: Session) -> None: - """ - Save playlist to database - """ + def save_playlist(self, session: Session) -> None: + """ + Save playlist to database + """ - # Iteratate through playlist and check that the row in each - # playlist_row object is correct - for row in range(self.rowCount()): - plr = session.get(PlaylistRows, self._get_playlistrow_id(row)) - # Set the row number (even if it's already correct) - if plr.row_number != row: - log.debug( - f"Updating PlaylistRow: {plr.row_number=}, {row=}" - ) - plr.row_number = row + # Iteratate through playlist and check that the row in each + # playlist_row object is correct + for row in range(self.rowCount()): + plr = session.get(PlaylistRows, self._get_playlistrow_id(row)) + # Set the row number (even if it's already correct) + if plr.row_number != row: + log.debug( + f"Updating PlaylistRow: {plr.row_number=}, {row=}" + ) + plr.row_number = row - # Any rows in the database with a row_number higher that the - # current value of 'row' should not be there. Commit session - # first to ensure any changes made above are committed. - session.commit() - PlaylistRows.delete_higher_rows(session, self.playlist_id, row) + # Any rows in the database with a row_number higher that the + # current value of 'row' should not be there. Commit session + # first to ensure any changes made above are committed. + session.commit() + PlaylistRows.delete_higher_rows(session, self.playlist_id, row) # def save_playlist(self, session) -> None: # """ @@ -1231,15 +1246,19 @@ class PlaylistTab(QTableWidget): # # self.save_playlist(session) # self.update_display(session) -# -# def _drop_on(self, event): # review -# index = self.indexAt(event.pos()) -# if not index.isValid(): -# return self.rowCount() -# -# return (index.row() + 1 if self._is_below(event.pos(), index) -# else index.row()) -# + + def _drop_on(self, event): + """ + https://stackoverflow.com/questions/26227885/drag-and-drop-rows-within-qtablewidget + """ + + index = self.indexAt(event.pos()) + if not index.isValid(): + return self.rowCount() + + return (index.row() + 1 if self._is_below(event.pos(), index) + else index.row()) + # def _edit_note_cell(self, row, column): # review # """Called when table is single-clicked""" # @@ -1481,20 +1500,24 @@ class PlaylistTab(QTableWidget): # if repaint: # self.save_playlist(session) # self.update_display(session, clear_selection=False) -# -# def _is_below(self, pos, index): # review -# rect = self.visualRect(index) -# margin = 2 -# if pos.y() - rect.top() < margin: -# return False -# elif rect.bottom() - pos.y() < margin: -# return True -# return ( -# rect.contains(pos, True) and not -# (int(self.model().flags(index)) & Qt.ItemIsDropEnabled) -# and pos.y() >= rect.center().y() # noqa W503 -# ) -# + + def _is_below(self, pos, index): # review + """ + https://stackoverflow.com/questions/26227885/drag-and-drop-rows-within-qtablewidget + """ + + rect = self.visualRect(index) + margin = 2 + if pos.y() - rect.top() < margin: + return False + elif rect.bottom() - pos.y() < margin: + return True + return ( + rect.contains(pos, True) and not + (int(self.model().flags(index)) & Qt.ItemIsDropEnabled) + and pos.y() >= rect.center().y() # noqa W503 + ) + # def _is_note_row(self, row: int) -> bool: # """ # Return True if passed row is a note row, else False diff --git a/migrations/versions/29c0d7ffc741_drop_uniquerow_index_on_playlist_rows.py b/migrations/versions/29c0d7ffc741_drop_uniquerow_index_on_playlist_rows.py new file mode 100644 index 0000000..e14e34c --- /dev/null +++ b/migrations/versions/29c0d7ffc741_drop_uniquerow_index_on_playlist_rows.py @@ -0,0 +1,24 @@ +"""Drop uniquerow index on playlist_rows + +Revision ID: 29c0d7ffc741 +Revises: 3b063011ed67 +Create Date: 2022-08-06 22:21:46.881378 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '29c0d7ffc741' +down_revision = '3b063011ed67' +branch_labels = None +depends_on = None + + +def upgrade(): + op.drop_index('uniquerow', table_name='playlist_rows') + + +def downgrade(): + op.create_index('uniquerow', 'playlist_rows', ['row_number', 'playlist_id'], unique=True)