From cf52eb3c9c577b28b512aeec88ef6eec5855e6a3 Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Fri, 2 Apr 2021 00:22:29 +0100 Subject: [PATCH] Playlist drag and drop; track colours --- app/config.py | 7 +++ app/musicmuster.py | 91 ++++++++++++++++++++++++-------- app/promoted_classes.py | 114 ++++++++++++++++++++++++++++++++++++++++ app/ui/main_window.ui | 20 ++++++- 4 files changed, 208 insertions(+), 24 deletions(-) create mode 100644 app/promoted_classes.py diff --git a/app/config.py b/app/config.py index 029519f..094cd25 100644 --- a/app/config.py +++ b/app/config.py @@ -4,6 +4,13 @@ import os class Config(object): AUDIO_SEGMENT_CHUNK_SIZE = 10 + COLOUR_CURRENT_HEADER = "#d4edda" + COLOUR_CURRENT_PLAYLIST = "#28a745" + COLOUR_ODD_PLAYLIST = "#f2f2f2" + COLOUR_EVEN_PLAYLIST = "#d9d9d9" + COLOUR_NEXT_HEADER = "#fff3cd" + COLOUR_NEXT_PLAYLIST = "#ffc107" + COLOUR_PREVIOUS_HEADER = "#f8d7da" DBFS_FADE = -12 DBFS_SILENCE = -50 DISPLAY_SQL = False diff --git a/app/musicmuster.py b/app/musicmuster.py index 6b8a3e2..843ff19 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -8,8 +8,15 @@ from datetime import datetime, timedelta from log import DEBUG, ERROR from PyQt5.QtCore import Qt, QTimer -from PyQt5.QtWidgets import QApplication, QDialog, QMainWindow -from PyQt5.QtWidgets import QTableWidgetItem, QFileDialog, QListWidgetItem +from PyQt5.QtGui import QColor +from PyQt5.QtWidgets import ( + QApplication, + QDialog, + QFileDialog, + QListWidgetItem, + QMainWindow, + QTableWidgetItem, +) from ui.main_window_ui import Ui_MainWindow from ui.dlg_search_database_ui import Ui_Dialog @@ -101,6 +108,12 @@ class Music: except AttributeError: return "" + def get_next_track_id(self): + try: + return self.next_track['meta'].id + except AttributeError: + return 0 + def fade_current(self): if not self.playing(): return @@ -193,6 +206,19 @@ class Window(QMainWindow, Ui_MainWindow): db_playlist = Playlists.get_only_playlist() for track in db_playlist.get_tracks(): self.add_to_playlist(track) + # Set the first playable track as next to play + for row in range(self.playlist.rowCount()): + if self.set_next_track_row(row): + break + + pl = self.playlist + row = pl.rowCount() + pl.insertRow(row) + item = QTableWidgetItem("to be spanned") + pl.setItem(row, 1, item) + pl.setSpan(row, 1, 1, 3) + colour = QColor(125, 75, 25) + pl.set_row_colour(row, colour) self.timer.start(Config.TIMER_MS) @@ -220,14 +246,15 @@ class Window(QMainWindow, Ui_MainWindow): record.update({'f_int': self.y()}) def connectSignalsSlots(self): + self.action_Clear_selection.triggered.connect(self.clear_selection) self.actionFade.triggered.connect(self.fade) self.actionPlay_next.triggered.connect(self.play_next) self.actionPlay_selected.triggered.connect(self.play_next) self.actionSearch_database.triggered.connect(self.selectFromDatabase) self.btnSearchDatabase.clicked.connect(self.selectFromDatabase) + self.btnSetNextTrack.clicked.connect(self.set_next_track) self.btnSkipNext.clicked.connect(self.play_next) self.btnStop.clicked.connect(self.fade) - self.playlist.itemSelectionChanged.connect(self.set_next_track) self.timer.timeout.connect(self.tick) @@ -235,6 +262,9 @@ class Window(QMainWindow, Ui_MainWindow): dlg = DbDialog(self) dlg.exec() + def clear_selection(self): + self.playlist.clearSelection() + def fade(self): self.music.fade_current() @@ -258,6 +288,29 @@ class Window(QMainWindow, Ui_MainWindow): self.label_silent_tod.setText(silence_time.strftime("%H:%M:%S")) self.label_fade_length.setText(ms_to_mmss( silence_at - self.music.get_current_fade_at())) + self.set_playlist_colours() + + def set_playlist_colours(self): + self.playlist.clearSelection() + current_track = self.music.get_current_track_id() + next_track = self.music.get_next_track_id() + for row in range(self.playlist.rowCount()): + try: + track_id = int(self.playlist.item(row, 0).text()) + if track_id == next_track: + self.playlist.set_row_colour( + row, QColor(Config.COLOUR_NEXT_PLAYLIST)) + elif track_id == current_track: + self.playlist.set_row_colour( + row, QColor(Config.COLOUR_CURRENT_PLAYLIST)) + else: + if row % 2: + colour = QColor(Config.COLOUR_ODD_PLAYLIST) + else: + colour = QColor(Config.COLOUR_EVEN_PLAYLIST) + self.playlist.set_row_colour(row, colour) + except AttributeError: + pass def play_selected(self): if self.playlist.selectionModel().hasSelection(): @@ -287,26 +340,18 @@ class Window(QMainWindow, Ui_MainWindow): def set_next_track(self): """ - Set the next track. In order of priority: + Sets the selected track as the next track. + """ - - the highlighted track so long as it's not the current track - - if the current track is highlighted, the next track if there - is one - - none, in which case disable play next controls - """ - - track_id = None if self.playlist.selectionModel().hasSelection(): - row = self.playlist.currentRow() + self.set_next_track_row(self.playlist.currentRow()) + + def set_next_track_row(self, row): + if self.playlist.item(row, 0): track_id = int(self.playlist.item(row, 0).text()) if track_id == self.music.get_current_track_id(): - # Current track highlighted: get next if it exists - try: - track_id = int(self.playlist.item(row + 1, 0).text()) - except AttributeError: - # There is no next track - track_id = None - if track_id: + # Don't set current track as next track + return DEBUG(f"set_next_track: track_id={track_id}") if self.music.set_next_track(track_id) != track_id: ERROR("Can't set next track") @@ -315,9 +360,11 @@ class Window(QMainWindow, Ui_MainWindow): f"{self.music.get_next_artist()}" ) self.enable_play_next_controls() - else: - self.next_track.setText("") - self.disable_play_next_controls() + self.playlist.clearSelection() + self.set_playlist_colours() + return True + + return False def tick(self): # self.current_time.setText(now.strftime("%H:%M:%S")) diff --git a/app/promoted_classes.py b/app/promoted_classes.py new file mode 100644 index 0000000..c2ecba5 --- /dev/null +++ b/app/promoted_classes.py @@ -0,0 +1,114 @@ +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QDropEvent +from PyQt5.QtWidgets import ( + QTableWidget, + QAbstractItemView, + QTableWidgetItem, + QWidget, + QHBoxLayout, + QApplication +) + +from log import DEBUG + + +class Playlist(QTableWidget): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + 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) + + def dropEvent(self, event: QDropEvent): + if not event.isAccepted() and event.source() == self: + drop_row = self.drop_on(event) + + rows = 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() + for row_index in range(len(rows_to_move)): + for column_index in range(self.columnCount()): + self.item(drop_row + row_index, + column_index).setSelected(True) + super().dropEvent(event) + DEBUG(f"Moved row(s) {rows} to become row {drop_row}") + + def drop_on(self, event): + 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 is_below(self, pos, index): + rect = self.visualRect(index) + margin = 2 + if pos.y() - rect.top() < margin: + return False + elif rect.bottom() - pos.y() < margin: + return True + # noinspection PyTypeChecker + return ( + rect.contains(pos, True) and not + (int(self.model().flags(index)) & Qt.ItemIsDropEnabled) + and pos.y() >= rect.center().y() # noqa W503 + ) + + def set_row_colour(self, row, colour): + for j in range(self.columnCount()): + if self.item(row, j): + self.item(row, j).setBackground(colour) + + +class Window(QWidget): + def __init__(self): + super(Window, self).__init__() + + layout = QHBoxLayout() + self.setLayout(layout) + + self.table_widget = Playlist() + layout.addWidget(self.table_widget) + + # setup table widget + self.table_widget.setColumnCount(2) + self.table_widget.setHorizontalHeaderLabels(['Type', 'Name']) + + items = [('Red', 'Toyota'), ('Blue', 'RV'), ('Green', 'Beetle'), + ('Silver', 'Chevy'), ('Black', 'BMW')] + self.table_widget.setRowCount(len(items)) + for i, (color, model) in enumerate(items): + self.table_widget.setItem(i, 0, QTableWidgetItem(color)) + self.table_widget.setItem(i, 1, QTableWidgetItem(model)) + + self.resize(400, 400) + self.show() + + +if __name__ == '__main__': + import sys + app = QApplication(sys.argv) + window = Window() + sys.exit(app.exec_()) diff --git a/app/ui/main_window.ui b/app/ui/main_window.ui index bf70a0e..56e8682 100644 --- a/app/ui/main_window.ui +++ b/app/ui/main_window.ui @@ -267,7 +267,7 @@ border: 1px solid rgb(85, 87, 83); - + QAbstractItemView::NoEditTriggers @@ -275,7 +275,7 @@ border: 1px solid rgb(85, 87, 83); true - QAbstractItemView::SingleSelection + QAbstractItemView::ExtendedSelection QAbstractItemView::SelectRows @@ -732,6 +732,7 @@ border: 1px solid rgb(85, 87, 83); + @@ -801,7 +802,22 @@ border: 1px solid rgb(85, 87, 83); S&top + + + &Clear selection + + + Esc + + + + + Playlist + QTableWidget +
promoted_classes
+
+