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;
+
+
+
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