musicmuster/app/playlists.py
2021-04-21 08:39:49 +01:00

999 lines
32 KiB
Python

from PyQt5 import QtCore
from PyQt5.QtCore import Qt
from PyQt5.Qt import QFont
from PyQt5.QtGui import QColor, QDropEvent
from PyQt5.QtWidgets import (
QAbstractItemView,
QApplication,
QDialog,
QHBoxLayout,
QListWidgetItem,
QMenu,
QMessageBox,
QTableWidget,
QTableWidgetItem,
QWidget,
)
import helpers
import music
from config import Config
from datetime import datetime, timedelta
from log import DEBUG, ERROR
from model import Notes, Playdates, Playlists, PlaylistTracks, Settings, Tracks
from ui.dlg_search_database_ui import Ui_Dialog
from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist
class Playlist(QTableWidget):
# Column names
COL_INDEX = 0
COL_MSS = 1
COL_NOTE = 2
COL_TITLE = 2
COL_ARTIST = 3
COL_DURATION = 4
COL_START_TIME = 5
COL_PATH = 6
NOTE_COL_SPAN = 3
NOTE_ROW_SPAN = 1
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)
# This property holds how the widget shows a context menu
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
# This signal is emitted when the widget's contextMenuPolicy is
# Qt::CustomContextMenu, and the user has requested a context
# menu on the widget.
self.customContextMenuRequested.connect(self.context_menu)
self.viewport().installEventFilter(self)
self.music = music.Music()
self.current_track = None
self.next_track = None
self.playlist_name = None
self.playlist_id = 0
self.previous_track = None
self.previous_track_position = None
self.played_tracks = []
def closeEvent(self, event):
"Save column widths"
for column in range(self.columnCount()):
width = self.columnWidth(column)
name = f"playlist_col_{str(column)}_width"
record = Settings.get_int(name)
if record.f_int != self.columnWidth(column):
record.update({'f_int': width})
event.accept()
def eventFilter(self, source, event):
"Used to process context (right-click) menu"
if(event.type() == QtCore.QEvent.MouseButtonPress and # noqa W504
event.buttons() == QtCore.Qt.RightButton and # noqa W504
source is self.viewport()):
item = self.itemAt(event.pos())
if item is not None:
row = item.row()
DEBUG(f"playlist.eventFilter(): Right-click on row {row}")
self.menu = QMenu(self)
if row not in self.meta_get_notes():
act_setnext = self.menu.addAction("Set next")
act_setnext.triggered.connect(lambda: self.set_next(row))
self.menu.addSeparator()
act_delete = self.menu.addAction('Delete')
act_delete.triggered.connect(lambda: self.delete_row(row))
return super(Playlist, self).eventFilter(source, event)
def context_menu(self, pos):
self.menu.exec_(self.mapToGlobal(pos))
def delete_row(self, row):
"Delete row"
if row == self.meta_get_current():
# TODO
DEBUG("playlist.delete_row(): Can't delete playing track")
elif row == self.meta_get_next():
# TODO
DEBUG("playlist.delete_row(): Can't delete next track")
else:
title = self.item(row, self.COL_TITLE).text()
msg = QMessageBox(self)
msg.setIcon(QMessageBox.Warning)
msg.setText(f"Delete '{title}'?")
msg.setStandardButtons(QMessageBox.Yes | QMessageBox.Cancel)
msg.setDefaultButton(QMessageBox.Cancel)
msg.setWindowTitle("Delete row")
if msg.exec() == QMessageBox.Yes:
DEBUG(f"playlist.delete_row(): Delete row {row}")
id = int(self.item(row, self.COL_INDEX).text())
if row in self.meta_get_notes():
Notes.delete_note(id)
else:
PlaylistTracks.remove_track(self.playlist_id, id)
self.removeRow(row)
self.repaint()
def add_note(self, text):
"""
Add note to playlist
If a row is selected, add note above. Otherwise, add to end of
playlist.
"""
if self.selectionModel().hasSelection():
row = self.currentRow()
else:
row = self.rowCount()
DEBUG(f"playlist.add_note(): row={row}")
note = Notes.add_note(self.playlist_id, row, text)
self.add_to_playlist(note, row=row)
def add_to_playlist(self, data, repaint=True, row=None):
"""
Add data to playlist. Data may be either a Tracks object or a
Notes object.
"""
if not row:
if self.selectionModel().hasSelection():
row = self.currentRow()
else:
row = self.rowCount()
DEBUG(f"add_to_playlist(data={data}): row={row}")
self.insertRow(row)
if isinstance(data, Tracks):
DEBUG(f"add_to_playlist: track.id={data.id}")
track = data
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)
titleitem = QTableWidgetItem(track.title)
self.setItem(row, self.COL_TITLE, titleitem)
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)
else:
# This is a note
DEBUG(f"add_to_playlist: note.id={data.id}")
note = data
# Does note end with a time?
start_time = None
try:
start_time = datetime.strptime(
note.note[-9:], " %H:%M:%S").time()
DEBUG(f"Note contains valid time={start_time}")
except ValueError:
DEBUG(
f"Note on row {row} ('{note.note}') "
"does not contain valid time"
)
item = QTableWidgetItem(str(note.id))
self.setItem(row, self.COL_INDEX, item)
titleitem = QTableWidgetItem(data.note)
self.setItem(row, self.COL_NOTE, titleitem)
self.setSpan(row, self.COL_NOTE, self.NOTE_ROW_SPAN,
self.NOTE_COL_SPAN)
# Add start times or empty items, otherwise background
# colour won't be set for columns without items
if start_time:
item = QTableWidgetItem(start_time.strftime("%H:%M:%S"))
else:
item = QTableWidgetItem()
self.setItem(row, self.COL_START_TIME, item)
item = QTableWidgetItem()
self.setItem(row, self.COL_PATH, item)
self.meta_set_note(row)
# Scroll to new row
self.scrollToItem(titleitem, QAbstractItemView.PositionAtCenter)
if repaint:
self.repaint(clear_selection=False)
def create_playlist(self, name):
"Create new playlist"
new_id = Playlists.new(name)
self.load_playlist(new_id)
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()
# We don't want rows to be selected after move
# 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)
# 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 row in self.meta_get_notes():
self.setSpan(row, self.COL_NOTE, self.NOTE_ROW_SPAN,
self.NOTE_COL_SPAN)
super().dropEvent(event)
DEBUG(
"playlist.dropEvent(): "
f"Moved row(s) {rows} to become row {drop_row}"
)
self.repaint()
def fade(self):
self.previous_track = self.current_track
self.previous_track_position = 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 calculate_next_start_time(self, row, start):
"Return this row's end time given its start time"
duration = Tracks.get_duration(
int(self.item(row, self.COL_INDEX).text()))
return start + timedelta(milliseconds=duration)
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, plid):
"""
Load tracks and notes from playlist id.
Set first track as next track to play.
Return True if successful else False.
"""
DEBUG(f"load_playlist(plid={plid})")
p = Playlists.get_playlist_by_id(plid)
self.playlist_id = plid
self.playlist_name = p.name
self.parent().parent().update_statusbar()
# We need to retrieve playlist tracks and playlist notes, then
# add them in row order. We don't mandate that an item will be
# on its specified row, only that it will be above
# larger-numbered row items, and below lower-numbered ones.
data = []
for t in p.tracks:
data.append(([t.row], t.tracks))
for n in p.notes:
data.append(([n.row], n))
# Clear playlist
self.setRowCount(0)
# Now add data in row order
for item in sorted(data, key=lambda x: x[0]):
self.add_to_playlist(item[1], repaint=False)
# Set the first non-notes row as next track to play
notes_rows = self.meta_get_notes()
for row in range(self.rowCount()):
if row in notes_rows:
continue
self.meta_set_next(row)
self.tracks_changed()
return True
return False
def meta_clear(self, row):
"Clear metadata for row"
self.meta_set(row, None)
def meta_find(self, metadata, one=True):
"""
Search rows for metadata.
If one is True, check that only one row matches and return
the row number.
If one is False, return a list of matching row numbers.
"""
matches = []
for row in range(self.rowCount()):
if self.meta_get(row) == metadata:
matches.append(row)
if not one:
return matches
if len(matches) == 0:
return None
elif len(matches) == 1:
return matches[0]
else:
ERROR(
f"Multiple matches for metadata '{metadata}' found "
f"in rows: {', '.join([str(x) for x in matches])}"
)
raise AttributeError(f"Multiple '{metadata}' metadata {matches}")
def meta_get(self, row):
"Return row metadata"
return self.item(row, self.COL_INDEX).data(Qt.UserRole)
def meta_get_current(self):
"Return row marked as current, or None"
return self.meta_find("current")
def meta_get_next(self):
"Return row marked as next, or None"
return self.meta_find("next")
def meta_get_notes(self):
"Return rows marked as notes, or None"
return self.meta_find("note", one=False)
def meta_set_current(self, row):
"Mark row as current track"
old_current = self.meta_get_current()
if old_current is not None:
self.meta_clear(old_current)
self.meta_set(row, "current")
def meta_set_next(self, row):
"Mark row as next track"
old_next = self.meta_get_next()
if old_next is not None:
self.meta_clear(old_next)
self.meta_set(row, "next")
def meta_set_note(self, row):
"Mark row as note"
self.meta_set(row, "note")
def meta_set(self, row, metadata):
"Set row metadata"
if self.item(row, self.COL_TITLE):
title = self.item(row, self.COL_TITLE).text()
else:
title = ""
DEBUG(f"meta_set(row={row}, title={title}, metadata={metadata})")
if row is None:
raise ValueError(f"meta_set() with row=None")
self.item(row, self.COL_INDEX).setData(Qt.UserRole, metadata)
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.
Play (new) current.
Update playlist "current track" metadata
Cue up next track in playlist if there is one.
Tell database to record it as played
Remember it was played for this session
Update metadata and headers, and repaint
"""
# If there is no next track set, return.
if not self.next_track:
return
DEBUG(
"playlist.play_next(), "
f"next_track={self.next_track.title if self.next_track else None} "
"current_track="
f"{self.current_track.title if self.current_track else None}"
)
# If there's currently a track playing, fade it.
if self.music.playing():
self.previous_track_position = self.music.fade()
self.previous_track = self.current_track
# Shuffle tracks along
self.current_track = self.next_track
self.next_track = None
# Play (new) current.
self.music.play(self.current_track.path)
self.current_track.start_time = datetime.now()
# Update metadata
self.meta_set_current(self.meta_get_next())
# Set up metadata for next track in playlist if there is one.
current_row = self.meta_get_current()
if current_row is not None:
start = current_row + 1
else:
start = 0
notes_rows = self.meta_get_notes()
for row in range(start, self.rowCount()):
if row in notes_rows:
continue
if self.item(row, self.COL_INDEX):
self.meta_set_next(row)
break
# Tell database to record it as played
self.current_track.update_lastplayed()
Playdates.add_playdate(self.current_track)
# Remember it was played for this session
self.played_tracks.append(self.current_track.id)
# Update display
self.tracks_changed()
def search_database(self):
dlg = DbDialog(self)
dlg.exec()
def select_playlist(self):
dlg = SelectPlaylistDialog(self)
dlg.exec()
def set_selected_as_next(self):
"""
Sets the selected track as the next track.
"""
if not self.selectionModel().hasSelection():
return
self.set_next(self.currentRow())
def set_next(self, row):
"""
If passed row is track row, set that track as the next track to
be played and return True. Otherwise return False.
"""
if row in self.meta_get_notes():
return False
if self.item(row, self.COL_INDEX):
self.meta_set_next(row)
self.tracks_changed()
return True
return False
def stop(self):
"Stop playing immediately"
self.previous_track = self.current_track
self.previous_track_position = self.music.stop()
def music_ended(self):
"Update display"
self.previous_track = self.current_track
self.previous_track_position = 0
self.meta_clear(self.meta_get_current())
self.tracks_changed()
def repaint(self, clear_selection=True):
"Set row colours, fonts, etc, and save playlist"
DEBUG(f"repaint(clear_selection={clear_selection})")
self.save_playlist()
if clear_selection:
self.clearSelection()
current = self.meta_get_current()
next = self.meta_get_next() or 0
notes = self.meta_get_notes()
# Set colours and start times
next_start_time = None
# Set current row if there is one
row = current
if row is not None:
# Set start time
next_start_time = self.calculate_next_start_time(
row, self.current_track.start_time)
item = QTableWidgetItem(
self.current_track.start_time.strftime("%H:%M:%S")
)
self.setItem(row, self.COL_START_TIME, item)
# Set colour
self.set_row_colour(
current, QColor(Config.COLOUR_CURRENT_PLAYLIST)
)
# Make bold
self.set_row_bold(current)
# Now from the top, but ignore timing until either next or current
for row in range(self.rowCount()):
# Current row is already set
if row == current:
continue
set_time = row >= min(next or 0, current or 0)
# Handle notes
if row in notes:
if set_time:
try:
next_start_time = datetime.strptime(
self.item(row, self.COL_START_TIME).text(),
"%H:%M:%S"
)
except ValueError:
pass
# Set colour
self.set_row_colour(
row, QColor(Config.COLOUR_NOTES_PLAYLIST)
)
self.set_row_bold(row)
# Handle next
elif row == next:
if set_time and next_start_time:
item = QTableWidgetItem(
next_start_time.strftime("%H:%M:%S")
)
self.setItem(row, self.COL_START_TIME, item)
next_start_time = self.calculate_next_start_time(
row, next_start_time)
# Set colour
self.set_row_colour(
row, QColor(Config.COLOUR_NEXT_PLAYLIST)
)
# Make bold
self.set_row_bold(row)
# Handle other rows
else:
# Stripe rows
if row % 2:
colour = QColor(Config.COLOUR_ODD_PLAYLIST)
else:
colour = QColor(Config.COLOUR_EVEN_PLAYLIST)
self.set_row_colour(row, colour)
# Set time
if set_time and next_start_time:
item = QTableWidgetItem(
next_start_time.strftime("%H:%M:%S"))
self.setItem(row, self.COL_START_TIME, item)
next_start_time = self.calculate_next_start_time(
row, next_start_time)
# Dim played tracks
track_id = int(self.item(row, self.COL_INDEX).text())
if track_id in self.played_tracks:
self.set_row_not_bold(row)
else:
self.set_row_bold(row)
# Headers might need updating
self.parent().parent().update_headers()
def save_playlist(self):
"""
Save playlist to database. Add missing notes/tracks; remove any that
are in database but not playlist. Correct row number in database if
necessary.
"""
note_rows = self.meta_get_notes()
playlist = Playlists.get_playlist_by_id(self.playlist_id)
# Create dictionaries indexed by track/note id
playlist_notes = {}
playlist_tracks = {}
database_notes = {}
database_tracks = {}
# Playlist
for row in range(self.rowCount()):
# Get id of item
if self.item(row, self.COL_INDEX):
id = int(self.item(row, self.COL_INDEX).text())
else:
DEBUG(f"(save_playlist(): no COL_INDEX data in row {row}")
continue
if row in note_rows:
playlist_notes[id] = row
else:
playlist_tracks[id] = row
# Database
for note in playlist.notes:
database_notes[note.id] = note.row
for track in playlist.tracks:
database_tracks[track.track_id] = track.row
# Notes to remove from playlist in database
for note_id in set(database_notes.keys()) - set(playlist_notes.keys()):
DEBUG(
f"save_playlist(): Delete note.id={id} "
f"from playlist {playlist} in database"
)
Notes.delete_note(note_id)
# Tracks to remove from playlist in database
for track_id in (
set(database_tracks.keys()) - set(playlist_tracks.keys())
):
DEBUG(
f"save_playlist(): Delete track.id={track_id} "
f"from playlist {playlist} in database"
)
PlaylistTracks.remove_track(playlist.id, track_id)
# Notes to add to playlist database
# This should never be needed as notes are added to a specific
# playlist upon creation
for note_id in set(playlist_notes.keys()) - set(database_notes.keys()):
ERROR(
f"save_playlist(): Note.id={note_id} "
f"missing from playlist {playlist} in database"
)
# Notes to remove from playlist database
for note_id in set(database_notes.keys()) - set(playlist_notes.keys()):
DEBUG(
f"save_playlist(): Remove note.id={note_id} "
f"from playlist {playlist} in database"
)
Notes.delete_note(note_id)
# Note rows to update in playlist database
for note_id in set(playlist_notes.keys()) & set(database_notes.keys()):
if playlist_notes[note_id] != database_notes[note_id]:
DEBUG(
f"save_playlist(): Set database note.id {note_id} "
f"row={playlist_notes[note_id]} "
f"in playlist {playlist} in database"
)
Notes.update_note(note_id, playlist_notes[note_id])
# Track rows to update in playlist database
for track_id in (
set(playlist_tracks.keys()) & set(database_tracks.keys())
):
if playlist_tracks[track_id] != database_tracks[track_id]:
DEBUG(
f"save_playlist(): Set database track.id {track_id} "
f"row={playlist_tracks[track_id]} "
f"in playlist {playlist} in database"
)
PlaylistTracks.update_track_row(
playlist_id=self.playlist_id,
track_id=track_id,
old_row=database_tracks[track_id],
new_row=playlist_tracks[track_id]
)
def set_column_widths(self):
# Column widths from settings
for column in range(self.columnCount()):
# Only show column 0 in test mode
if (column == 0 and not Config.TESTMODE):
self.setColumnWidth(0, 0)
else:
name = f"playlist_col_{str(column)}_width"
record = Settings.get_int(name)
if record.f_int is not None:
print("setting column width")
self.setColumnWidth(column, record.f_int)
def set_row_bold(self, row, bold=True):
boldfont = QFont()
boldfont.setBold(bold)
for j in range(self.columnCount()):
if self.item(row, j):
self.item(row, j).setFont(boldfont)
def set_row_colour(self, row, colour):
for j in range(self.columnCount()):
if self.item(row, j):
self.item(row, j).setBackground(colour)
def set_row_not_bold(self, row):
self.set_row_bold(row, False)
def tracks_changed(self):
"""
One or more of current or next track has changed.
The row metadata is definitive because the same track may appear
more than once in the playlist, but only one track may be marked
as current or next.
Update self.current_track and self.next_track.
"""
# Update instance variables
current_row = self.meta_get_current()
if current_row is not None:
track_id = int(self.item(current_row, self.COL_INDEX).text())
if not self.current_track or self.current_track.id != track_id:
self.current_track = Tracks.get_track(track_id)
else:
self.current_track = None
next_row = self.meta_get_next()
if next_row is not None:
track_id = int(self.item(next_row, self.COL_INDEX).text())
if not self.next_track or self.next_track.id != track_id:
self.next_track = Tracks.get_track(track_id)
else:
self.next_track = None
# Update display
self.repaint()
class DbDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.ui = Ui_Dialog()
self.ui.setupUi(self)
self.ui.searchString.textEdited.connect(self.chars_typed)
self.ui.matchList.itemDoubleClicked.connect(self.double_click)
self.ui.btnAdd.clicked.connect(self.add_selected)
self.ui.btnAddClose.clicked.connect(self.add_selected_and_close)
self.ui.btnClose.clicked.connect(self.close)
record = Settings.get_int("dbdialog_width")
width = record.f_int or 800
record = Settings.get_int("dbdialog_height")
height = record.f_int or 600
self.resize(width, height)
def __del__(self):
record = Settings.get_int("dbdialog_height")
if record.f_int != self.height():
record.update({'f_int': self.height()})
record = Settings.get_int("dbdialog_width")
if record.f_int != self.width():
record.update({'f_int': self.width()})
def add_selected(self):
if not self.ui.matchList.selectedItems():
return
item = self.ui.matchList.currentItem()
track_id = item.data(Qt.UserRole)
self.add_track(track_id)
def add_selected_and_close(self):
self.add_selected()
self.close()
def chars_typed(self, s):
if len(s) >= 3:
matches = Tracks.search_titles(s)
self.ui.matchList.clear()
if matches:
for track in matches:
t = QListWidgetItem()
t.setText(
f"{track.title} - {track.artist} "
f"[{helpers.ms_to_mmss(track.duration)}]"
)
t.setData(Qt.UserRole, track.id)
self.ui.matchList.addItem(t)
def double_click(self, entry):
track_id = entry.data(Qt.UserRole)
self.add_track(track_id)
def add_track(self, track_id):
track = Tracks.track_from_id(track_id)
# Store in current playlist in database
db_playlist = Playlists.get_playlist_by_id(self.parent().playlist_id)
db_playlist.add_track(track)
# Add to on-screen playlist
self.parent().add_to_playlist(track)
# Select search text to make it easier for next search
self.select_searchtext()
def select_searchtext(self):
self.ui.searchString.selectAll()
self.ui.searchString.setFocus()
class SelectPlaylistDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.ui = Ui_dlgSelectPlaylist()
self.ui.setupUi(self)
self.ui.lstPlaylists.itemDoubleClicked.connect(self.listdclick)
for (plid, plname) in [
(a.id, a.name) for a in Playlists.get_all_playlists()
]:
p = QListWidgetItem()
p.setText(plname)
p.setData(Qt.UserRole, plid)
self.ui.lstPlaylists.addItem(p)
def listdclick(self, entry):
plid = entry.data(Qt.UserRole)
self.parent().load_playlist(plid)
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_())