diff --git a/app/models.py b/app/models.py index 4540c3b..66adeee 100644 --- a/app/models.py +++ b/app/models.py @@ -23,6 +23,7 @@ from sqlalchemy import ( select, String, UniqueConstraint, + update, ) # from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import ( @@ -555,6 +556,23 @@ class PlaylistRows(Base): return plrs + @staticmethod + def move_rows_down(session: Session, playlist_id: int, starting_row: int, + move_by: int) -> None: + """ + Create space to insert move_by additional rows by incremented row + number from starting_row to end of playlist + """ + + session.execute( + update(PlaylistRows) + .where( + (PlaylistRows.playlist_id == playlist_id), + (PlaylistRows.row_number >= starting_row) + ) + .values(row_number=PlaylistRows.row_number + move_by) + ) + class Settings(Base): """Manage settings""" diff --git a/app/musicmuster.py b/app/musicmuster.py index 328ce1b..f00cb08 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -142,6 +142,7 @@ class Window(QMainWindow, Ui_MainWindow): self.next_track_playlist_tab: Optional[PlaylistTab] = None self.previous_track: Optional[TrackData] = None self.previous_track_position: Optional[int] = None + self.selected_plrs = None # Set colours that will be used by playlist row stripes palette = QPalette() @@ -392,10 +393,12 @@ class Window(QMainWindow, Ui_MainWindow): self.actionImport.triggered.connect(self.import_track) self.actionInsertSectionHeader.triggered.connect(self.insert_header) self.actionInsertTrack.triggered.connect(self.insert_track) + self.actionMark_for_moving.triggered.connect(self.cut_rows) self.actionMoveSelected.triggered.connect(self.move_selected) self.actionNew_from_template.triggered.connect(self.new_from_template) self.actionNewPlaylist.triggered.connect(self.create_and_show_playlist) self.actionOpenPlaylist.triggered.connect(self.open_playlist) + self.actionPaste.triggered.connect(self.paste_rows) self.actionPlay_next.triggered.connect(self.play_next) self.actionSave_as_template.triggered.connect(self.save_as_template) self.actionSearch.triggered.connect(self.search_playlist) @@ -450,6 +453,17 @@ class Window(QMainWindow, Ui_MainWindow): idx = self.tabPlaylist.addTab(playlist_tab, playlist.name) self.tabPlaylist.setCurrentIndex(idx) + def cut_rows(self) -> None: + """ + Cut rows ready for pasting. + """ + + with Session() as session: + # Save the selected PlaylistRows items ready for a later + # paste + self.selected_plrs = ( + self.visible_playlist_tab().get_selected_playlistrows(session)) + def debug(self): """Invoke debugger""" @@ -864,6 +878,68 @@ class Window(QMainWindow, Ui_MainWindow): playlist.mark_open(session) self.create_playlist_tab(session, playlist) + def paste_rows(self) -> None: + """ + Paste earlier cut rows. + + Process: + - ensure we have some cut rows + - if not pasting at end of playlist, move later rows down + - update plrs with correct playlist and row + - if moving between playlists: renumber source playlist rows + - else: check integrity of playlist rows + """ + + if not self.selected_plrs: + return + + playlist_tab = self.visible_playlist_tab() + dst_playlist_id = playlist_tab.playlist_id + + with Session() as session: + # Create space in destination playlist + if playlist_tab.selectionModel().hasSelection(): + row = playlist_tab.currentRow() + PlaylistRows.move_rows_down(session, dst_playlist_id, + row, len(self.selected_plrs)) + session.commit() + + src_playlist_id = None + dst_row = row + for plr in self.selected_plrs: + # Update moved rows + session.add(plr) + if not src_playlist_id: + src_playlist_id = plr.playlist_id + plr.playlist_id = dst_playlist_id + plr.row_number = row + row += 1 + # Need to commit each row individually else only one row + # gets updated (don't know why) + + session.commit() + + # Update display + self.visible_playlist_tab().populate(session, dst_playlist_id) + + # If source playlist is not destination playlist, fixup row + # numbers and update display + if src_playlist_id != dst_playlist_id: + PlaylistRows.fixup_rownumbers(session, src_playlist_id) + # Update source playlist_tab if visible (if not visible, it + # will be re-populated when it is opened) + source_playlist_tab = None + for tab in range(self.tabPlaylist.count()): + if self.tabPlaylist.widget(tab).playlist_id == \ + src_playlist_id: + source_playlist_tab = self.tabPlaylist.widget(tab) + break + if source_playlist_tab: + source_playlist_tab.populate(session, src_playlist_id) + + # Reset so rows can't be repasted + self.selected_plrs = None + def play_next(self) -> None: """ Play next track. diff --git a/app/playlists.py b/app/playlists.py index 5c85273..31570d1 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -287,6 +287,21 @@ class PlaylistTab(QTableWidget): else: current = next_row = False + # Cut/paste + act_cut = self.menu.addAction( + "Mark for moving") + act_cut.triggered.connect( + lambda: self.musicmuster.cut_rows()) + + act_paste = self.menu.addAction( + "Paste") + act_paste.setDisabled( + self.musicmuster.selected_plrs is None) + act_paste.triggered.connect( + lambda: self.musicmuster.paste_rows()) + + self.menu.addSeparator() + if track_row: # Info act_info = self.menu.addAction('Info') @@ -437,7 +452,7 @@ class PlaylistTab(QTableWidget): update_current = row == self._get_current_track_row() update_next = row == self._get_next_track_row() if self.edit_cell_type == TITLE: - log.debug(f"KAE: _cell_changed:438, {new_text=}") + log.debug(f"KAE: _cell_changed:440, {new_text=}") track.title = new_text elif self.edit_cell_type == ARTIST: track.artist = new_text @@ -616,7 +631,7 @@ class PlaylistTab(QTableWidget): self.setItem(row, START_GAP, start_gap_item) title_item = QTableWidgetItem(row_data.track.title) - log.debug(f"KAE: insert_row:615, {title_item.text()=}") + log.debug(f"KAE: insert_row:619, {title_item.text()=}") self.setItem(row, TITLE, title_item) artist_item = QTableWidgetItem(row_data.track.artist) @@ -826,14 +841,12 @@ class PlaylistTab(QTableWidget): """Scroll currently-playing row to top""" current_row = self._get_current_track_row() - log.debug(f"KAE: playlists.scroll_current_to_top(), {current_row=}") self._scroll_to_top(current_row) def scroll_next_to_top(self) -> None: """Scroll nextly-playing row to top""" next_row = self._get_next_track_row() - log.debug(f"KAE: playlists.scroll_next_to_top(), {next_row=}") self._scroll_to_top(next_row) def set_search(self, text: str) -> None: @@ -1517,11 +1530,13 @@ class PlaylistTab(QTableWidget): return self.selectionModel().selectedRows()[0].row() def _get_selected_rows(self) -> List[int]: - """Return a list of selected row numbers""" + """Return a list of selected row numbers sorted by row""" # Use a set to deduplicate result (a selected row will have all # items in that row selected) - return [row for row in set([a.row() for a in self.selectedItems()])] + return sorted( + [row for row in set([a.row() for a in self.selectedItems()])] + ) def _get_unreadable_track_rows(self) -> List[int]: """Return rows marked as unreadable, or None""" @@ -1975,7 +1990,7 @@ class PlaylistTab(QTableWidget): item_startgap.setBackground(QColor("white")) item_title = self.item(row, TITLE) - log.debug(f"KAE: _update_row:1958, {track.title=}") + log.debug(f"KAE: _update_row:1978, {track.title=}") item_title.setText(track.title) item_artist = self.item(row, ARTIST) diff --git a/app/ui/main_window.ui b/app/ui/main_window.ui index 95f8f7d..a1fc471 100644 --- a/app/ui/main_window.ui +++ b/app/ui/main_window.ui @@ -866,6 +866,9 @@ padding-left: 8px; + + + @@ -1178,6 +1181,22 @@ padding-left: 8px; Edit cart &1... + + + Mark for moving + + + Ctrl+C + + + + + Paste + + + Ctrl+V + + diff --git a/app/ui/main_window_ui.py b/app/ui/main_window_ui.py index 8aee7f3..49ffd97 100644 --- a/app/ui/main_window_ui.py +++ b/app/ui/main_window_ui.py @@ -499,6 +499,10 @@ class Ui_MainWindow(object): self.actionDebug.setObjectName("actionDebug") self.actionAdd_cart = QtWidgets.QAction(MainWindow) self.actionAdd_cart.setObjectName("actionAdd_cart") + self.actionMark_for_moving = QtWidgets.QAction(MainWindow) + self.actionMark_for_moving.setObjectName("actionMark_for_moving") + self.actionPaste = QtWidgets.QAction(MainWindow) + self.actionPaste.setObjectName("actionPaste") self.menuFile.addAction(self.actionNewPlaylist) self.menuFile.addAction(self.actionOpenPlaylist) self.menuFile.addAction(self.actionClosePlaylist) @@ -530,6 +534,9 @@ class Ui_MainWindow(object): self.menuPlaylist.addAction(self.action_Clear_selection) self.menuPlaylist.addSeparator() self.menuPlaylist.addAction(self.actionEnable_controls) + self.menuPlaylist.addSeparator() + self.menuPlaylist.addAction(self.actionMark_for_moving) + self.menuPlaylist.addAction(self.actionPaste) self.menuSearc_h.addAction(self.actionSearch) self.menuSearc_h.addAction(self.actionFind_next) self.menuSearc_h.addAction(self.actionFind_previous) @@ -635,5 +642,9 @@ class Ui_MainWindow(object): self.actionNew_from_template.setText(_translate("MainWindow", "New from template...")) self.actionDebug.setText(_translate("MainWindow", "Debug")) self.actionAdd_cart.setText(_translate("MainWindow", "Edit cart &1...")) + self.actionMark_for_moving.setText(_translate("MainWindow", "Mark for moving")) + self.actionMark_for_moving.setShortcut(_translate("MainWindow", "Ctrl+C")) + self.actionPaste.setText(_translate("MainWindow", "Paste")) + self.actionPaste.setShortcut(_translate("MainWindow", "Ctrl+V")) from infotabs import InfoTabs import icons_rc