from PyQt5.QtCore import Qt from PyQt5.QtGui import QColor, QDropEvent from PyQt5.QtWidgets import ( QTableWidget, QAbstractItemView, QTableWidgetItem, QWidget, QHBoxLayout, QApplication ) import helpers import music from config import Config from log import DEBUG from model import Playlists, Settings, Tracks class Playlist(QTableWidget): # Column names COL_INDEX = 0 COL_MSS = 1 COL_TITLE = 2 COL_ARTIST = 3 COL_DURATION = 4 COL_ENDTIME = 5 COL_PATH = 6 # UserRoles NEXT_TRACK = 1 CURRENT_TRACK = 2 COMMENT = 3 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) self.music = music.Music() self.current_track = None self.next_track = None self.previous_track_path = None self.previous_track_position = None self.played_tracks = [] # Column widths from settings for column in range(self.columnCount()): name = f"playlist_col_{str(column)}_width" record = Settings.get_int(name) if record.f_int is not None: self.setColumnWidth(column, record.f_int) # How to put in non-track entries for later # row = self.rowCount() # self.insertRow(row) # item = QTableWidgetItem("to be spanned") # self.setItem(row, 1, item) # self.setSpan(row, 1, 1, 3) # colour = QColor(125, 75, 25) # self.set_row_colour(row, colour) def __del__(self): "Save column widths" for column in range(self.columnCount()): name = f"playlist_col_{str(column)}_width" record = Settings.get_int(name) if record.f_int != self.columnWidth(column): record.update({'f_int': self.columnWidth(column)}) def add_to_playlist(self, track): """ Add track to playlist track is an instance of Track """ DEBUG(f"add_to_playlist: track.id={track.id}") row = self.rowCount() self.insertRow(row) DEBUG(f"Adding to row {row}") item = QTableWidgetItem(str(track.id)) self.setItem(row, self.COL_INDEX, item) item = QTableWidgetItem(str(track.start_gap)) self.setItem(row, self.COL_MSS, item) item = QTableWidgetItem(track.title) self.setItem(row, self.COL_TITLE, item) item = QTableWidgetItem(track.artist) self.setItem(row, self.COL_ARTIST, item) item = QTableWidgetItem(helpers.ms_to_mmss(track.duration)) self.setItem(row, self.COL_DURATION, item) item = QTableWidgetItem(track.path) self.setItem(row, self.COL_PATH, item) 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 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 face(self): self.music.fade() def get_current_artist(self): try: return self.current_track.artist except AttributeError: return "" def get_current_duration(self): try: return self.current_track.duration except AttributeError: return 0 def get_current_fade_at(self): try: return self.current_track.fade_at except AttributeError: return 0 def get_current_playtime(self): return self.music.get_playtime() def get_current_silence_at(self): try: return self.current_track.silence_at except AttributeError: return 0 def get_current_title(self): try: return self.current_track.title except AttributeError: return "" def get_next_artist(self): try: return self.next_track.artist except AttributeError: return "" def get_next_title(self): try: return self.next_track.title except AttributeError: return "" def get_next_track_id(self): try: return self.next_track.id except AttributeError: return 0 def get_previous_artist(self): try: return self.previous_track.artist except AttributeError: return "" def get_previous_title(self): try: return self.previous_track.title except AttributeError: return "" 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 load_playlist(self, name): "Load tracks from named playlist" for track in Playlists.get_playlist_by_name(name).get_tracks(): self.add_to_playlist(track) # Set the first playable track as next to play for row in range(self.rowCount()): if self.set_row_as_next_track(row): break def play_next(self): """ Play next track. If there is no next track set, return. If there's currently a track playing, fade it. Move next track to current track. Cue up next track in playlist if there is one. Play (new) current. Tell database to record it as played Remember it was played this session Update playlist appearance. """ if not self.next_track: return if self.music.playing(): path, position = self.music.fade() self.previous_track_path = path self.previous_track_position = position self.current_track = self.next_track self.music.play(self.current_track.path) # Find the playlist row for current track current_track_id = self.current_track.track_id self.current_track_row = None for row in range(self.rowCount): if self.item(row, 0): if current_track_id == int(self.item(row, 0).text()): self.current_track_row = row break self.next_track = None def set_next_track(self): """ Sets the selected track as the next track. """ # TODO pass # if self.selectionModel().hasSelection(): # self.set_next_track_row(self.playlist.currentRow()) # if self.selectionModel().hasSelection(): # row = self.currentRow() # track_id = int(self.item(row, 0).text()) # DEBUG(f"play_selected: track_id={track_id}") # # TODO: get_path may raise exception # music.play_track(track_id) # track = Tracks.get_track(id) # if not track: # ERROR(f"set_next_track({id}): can't find track") # return None # # self.next_track['player'] = vlc.MediaPlayer(track.path) # self.next_track = track # return track.id def set_row_as_next_track(self, row): """ If passed row is track row, indicated by having a track_id in the COL_INDEX column, set that track as the next track to be played and return True. Otherwise return False. """ if self.item(row, self.COL_INDEX): track_id = int(self.item(row, self.COL_INDEX).text()) DEBUG(f"set_row_as_next_track: track_id={track_id}") self.next_track = Tracks.get_track(track_id) # Mark row as next track self.item(row, self.COL_INDEX).setData( Qt.UserRole, self.NEXT_TRACK) self.set_playlist_colours() return True return False def set_playlist_colours(self): self.clearSelection() for row in range(self.rowCount()): if self.item(row, self.COL_INDEX): data = self.item(row, self.COL_INDEX).data(Qt.UserRole) if data == self.NEXT_TRACK: self.set_row_colour( row, QColor(Config.COLOUR_NEXT_PLAYLIST)) elif data == self.CURRENT_TRACK: self.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.set_row_colour(row, colour) 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_())