Compare commits

..

No commits in common. "bb14b34c2e4ffac9ce337a41471563b4b92dd5a3" and "9a01bf2c2ceb0321700638d567ec2ffbf97127bd" have entirely different histories.

5 changed files with 125 additions and 181 deletions

View File

@ -56,15 +56,6 @@ class Config(object):
FADEOUT_DB = -10 FADEOUT_DB = -10
FADEOUT_SECONDS = 5 FADEOUT_SECONDS = 5
FADEOUT_STEPS_PER_SECOND = 5 FADEOUT_STEPS_PER_SECOND = 5
HEADER_ARTIST = "Artist"
HEADER_BITRATE = "bps"
HEADER_DURATION = "Length"
HEADER_END_TIME = "End"
HEADER_LAST_PLAYED = "Last played"
HEADER_NOTE = "Notes"
HEADER_START_GAP = "Gap"
HEADER_START_TIME = "Start"
HEADER_TITLE = "Title"
HIDE_AFTER_PLAYING_OFFSET = 5000 HIDE_AFTER_PLAYING_OFFSET = 5000
INFO_TAB_TITLE_LENGTH = 15 INFO_TAB_TITLE_LENGTH = 15
LAST_PLAYED_TODAY_STRING = "Today" LAST_PLAYED_TODAY_STRING = "Today"

View File

@ -629,10 +629,7 @@ class Settings(Base):
def __repr__(self) -> str: def __repr__(self) -> str:
value = self.f_datetime or self.f_int or self.f_string value = self.f_datetime or self.f_int or self.f_string
return ( return f"<Settings(id={self.id}, name={self.name}, {value=}>"
f"<Settings(id={self.id}, name={self.name}, "
f"f_datetime={self.f_datetime}, f_int={self.f_int}, f_string={self.f_string}>"
)
def __init__(self, session: scoped_session, name: str): def __init__(self, session: scoped_session, name: str):
self.name = name self.name = name
@ -663,7 +660,7 @@ class Settings(Base):
except NoResultFound: except NoResultFound:
return Settings(session, name) return Settings(session, name)
def update(self, session: scoped_session, data: dict) -> None: def update(self, session: scoped_session, data: dict):
for key, value in data.items(): for key, value in data.items():
assert hasattr(self, key) assert hasattr(self, key)
setattr(self, key, value) setattr(self, key, value)

View File

@ -245,7 +245,6 @@ class MusicMusterSignals(QObject):
""" """
set_next_track_signal = pyqtSignal(int, int) set_next_track_signal = pyqtSignal(int, int)
span_cells_signal = pyqtSignal(int, int, int, int)
class PlaylistTrack: class PlaylistTrack:

View File

@ -1,6 +1,6 @@
from datetime import datetime from datetime import datetime
from enum import auto, Enum from enum import auto, Enum
from typing import Optional, TYPE_CHECKING from typing import Optional
from PyQt6.QtCore import ( from PyQt6.QtCore import (
QAbstractTableModel, QAbstractTableModel,
@ -27,9 +27,6 @@ from models import (
PlaylistRows, PlaylistRows,
) )
if TYPE_CHECKING:
from musicmuster import MusicMusterSignals
class Col(Enum): class Col(Enum):
START_GAP = 0 START_GAP = 0
@ -38,14 +35,11 @@ class Col(Enum):
DURATION = auto() DURATION = auto()
START_TIME = auto() START_TIME = auto()
END_TIME = auto() END_TIME = auto()
LAST_PLAYED = auto() LASTPLAYED = auto()
BITRATE = auto() BITRATE = auto()
NOTE = auto() NOTE = auto()
HEADER_NOTES_COLUMN = 1
class PlaylistRowData: class PlaylistRowData:
def __init__(self, plr: PlaylistRows) -> None: def __init__(self, plr: PlaylistRows) -> None:
""" """
@ -83,11 +77,8 @@ class PlaylistRowData:
class PlaylistModel(QAbstractTableModel): class PlaylistModel(QAbstractTableModel):
def __init__( def __init__(self, playlist_id: int, *args, **kwargs):
self, playlist_id: int, signals: "MusicMusterSignals", *args, **kwargs
):
self.playlist_id = playlist_id self.playlist_id = playlist_id
self.signals = signals
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.playlist_rows: dict[int, PlaylistRowData] = {} self.playlist_rows: dict[int, PlaylistRowData] = {}
@ -102,35 +93,12 @@ class PlaylistModel(QAbstractTableModel):
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<PlaylistModel: playlist_id={self.playlist_id}>" return f"<PlaylistModel: playlist_id={self.playlist_id}>"
def background_role(self, row: int, column: int, prd: PlaylistRowData) -> QBrush:
"""Return background setting"""
# Handle entire row colouring
# Header row
if not prd.path:
return QBrush(QColor(Config.COLOUR_NOTES_PLAYLIST))
# Unreadable track file
if file_is_unreadable(prd.path):
return QBrush(QColor(Config.COLOUR_UNREADABLE))
if column == Col.START_GAP.value:
if prd.start_gap and prd.start_gap >= Config.START_GAP_WARNING_THRESHOLD:
return QBrush(QColor(Config.COLOUR_LONG_START))
if column == Col.BITRATE.value:
if prd.bitrate < Config.BITRATE_LOW_THRESHOLD:
return QBrush(QColor(Config.COLOUR_BITRATE_LOW))
elif prd.bitrate and prd.bitrate < Config.BITRATE_OK_THRESHOLD:
return QBrush(QColor(Config.COLOUR_BITRATE_MEDIUM))
else:
return QBrush(QColor(Config.COLOUR_BITRATE_OK))
return QBrush()
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int: def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
"""Standard function for view""" """Standard function for view"""
return 9 return 9
# def data(self, index: QModelIndex, role: Qt.ItemDataRole.DisplayRole):
def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole): def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole):
"""Return data to view""" """Return data to view"""
@ -178,21 +146,6 @@ class PlaylistModel(QAbstractTableModel):
Return text for display Return text for display
""" """
# Detect whether this is a header row
if not prd.path:
header = prd.note
else:
header = ""
if header:
if column == HEADER_NOTES_COLUMN:
self.signals.span_cells_signal.emit(
row, HEADER_NOTES_COLUMN, 1, self.columnCount() - 1
)
return QVariant(prd.note)
else:
return QVariant()
if column == Col.START_GAP.value: if column == Col.START_GAP.value:
return QVariant(prd.start_gap) return QVariant(prd.start_gap)
if column == Col.TITLE.value: if column == Col.TITLE.value:
@ -205,7 +158,7 @@ class PlaylistModel(QAbstractTableModel):
return QVariant("FIXME") return QVariant("FIXME")
if column == Col.END_TIME.value: if column == Col.END_TIME.value:
return QVariant("FIXME") return QVariant("FIXME")
if column == Col.LAST_PLAYED.value: if column == Col.LASTPLAYED.value:
return QVariant(prd.lastplayed) return QVariant(prd.lastplayed)
if column == Col.BITRATE.value: if column == Col.BITRATE.value:
return QVariant(prd.bitrate) return QVariant(prd.bitrate)
@ -214,45 +167,45 @@ class PlaylistModel(QAbstractTableModel):
return QVariant() return QVariant()
def headerData( def background_role(self, row: int, column: int, prd: PlaylistRowData) -> QBrush:
self, """ Return background setting """
section: int,
orientation: Qt.Orientation,
role: int = Qt.ItemDataRole.DisplayRole,
) -> QVariant:
"""
Return text for headers
"""
if role == Qt.ItemDataRole.DisplayRole: if column == Col.START_GAP.value:
if orientation == Qt.Orientation.Horizontal: if prd.start_gap and prd.start_gap >= Config.START_GAP_WARNING_THRESHOLD:
if section == Col.START_GAP.value: return QBrush(QColor(Config.COLOUR_LONG_START))
return QVariant(Config.HEADER_START_GAP) if column == Col.BITRATE.value:
elif section == Col.TITLE.value: if prd.bitrate < Config.BITRATE_LOW_THRESHOLD:
return QVariant(Config.HEADER_TITLE) return QBrush(QColor(Config.COLOUR_BITRATE_LOW))
elif section == Col.ARTIST.value: elif prd.bitrate and prd.bitrate < Config.BITRATE_OK_THRESHOLD:
return QVariant(Config.HEADER_ARTIST) return QBrush(QColor(Config.COLOUR_BITRATE_MEDIUM))
elif section == Col.DURATION.value:
return QVariant(Config.HEADER_DURATION)
elif section == Col.START_TIME.value:
return QVariant(Config.HEADER_START_TIME)
elif section == Col.END_TIME.value:
return QVariant(Config.HEADER_END_TIME)
elif section == Col.LAST_PLAYED.value:
return QVariant(Config.HEADER_LAST_PLAYED)
elif section == Col.BITRATE.value:
return QVariant(Config.HEADER_BITRATE)
elif section == Col.NOTE.value:
return QVariant(Config.HEADER_NOTE)
else: else:
return QVariant(str(section + 1)) return QBrush(QColor(Config.COLOUR_BITRATE_OK))
elif role == Qt.ItemDataRole.FontRole: return QBrush()
boldfont = QFont()
boldfont.setBold(True)
return QVariant(boldfont)
return QVariant() # if rowdata.path and file_is_unreadable(rowdata.path):
# return QVariant(QColor(Config.COLOUR_UNREADABLE))
# elif row == self.current_row:
# return QVariant(QColor(Config.COLOUR_CURRENT_PLAYLIST))
# elif row == self.next_row:
# return QVariant(QColor(Config.COLOUR_NEXT_PLAYLIST))
# elif column == BITRATE:
# if rowdata.bitrate:
# if rowdata.bitrate < Config.BITRATE_LOW_THRESHOLD:
# cell_colour = Config.COLOUR_BITRATE_LOW
# elif rowdata.bitrate < Config.BITRATE_OK_THRESHOLD:
# cell_colour = Config.COLOUR_BITRATE_MEDIUM
# else:
# cell_colour = Config.COLOUR_BITRATE_OK
# return QVariant(QColor(cell_colour))
# if not rowdata.played:
# font = QFont()
# font.setBold(True)
# return QVariant(font)
# return QVariant()
def refresh_data(self): def refresh_data(self):
"""Populate dicts for data calls""" """Populate dicts for data calls"""
@ -262,7 +215,7 @@ class PlaylistModel(QAbstractTableModel):
for p in PlaylistRows.deep_rows(session, self.playlist_id): for p in PlaylistRows.deep_rows(session, self.playlist_id):
self.playlist_rows[p.plr_rownum] = PlaylistRowData(p) self.playlist_rows[p.plr_rownum] = PlaylistRowData(p)
def rowCount(self, index: QModelIndex = QModelIndex()) -> int: def rowCount(self, index: QModelIndex) -> int:
"""Standard function for view""" """Standard function for view"""
return len(self.playlist_rows) return len(self.playlist_rows)

View File

@ -22,7 +22,6 @@ from PyQt6.QtWidgets import (
QAbstractItemDelegate, QAbstractItemDelegate,
QAbstractItemView, QAbstractItemView,
QApplication, QApplication,
QHeaderView,
QMenu, QMenu,
QMessageBox, QMessageBox,
QPlainTextEdit, QPlainTextEdit,
@ -45,7 +44,7 @@ from helpers import (
set_track_metadata, set_track_metadata,
) )
from log import log from log import log
from models import Playlists, PlaylistRows, Settings, Tracks, NoteColours from models import Playdates, Playlists, PlaylistRows, Settings, Tracks, NoteColours
from playlistmodel import PlaylistModel from playlistmodel import PlaylistModel
@ -145,60 +144,67 @@ class PlaylistTab(QTableView):
signals: "MusicMusterSignals", signals: "MusicMusterSignals",
) -> None: ) -> None:
super().__init__() super().__init__()
# Save passed settings
self.musicmuster = musicmuster self.musicmuster = musicmuster
self.playlist_id = playlist_id self.playlist_id = playlist_id
self.setModel(PlaylistModel(playlist_id))
self.signals = signals self.signals = signals
# Set up widget # Set up widget
self.menu = QMenu()
self.setItemDelegate(EscapeDelegate(self)) self.setItemDelegate(EscapeDelegate(self))
self.setAlternatingRowColors(True) self.setAlternatingRowColors(True)
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.setEditTriggers(QAbstractItemView.EditTrigger.DoubleClicked) self.setEditTriggers(QAbstractItemView.EditTrigger.DoubleClicked)
self.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel) self.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
# This dancing is to satisfy mypy
h_header = self.horizontalHeader() # Header row
if isinstance(h_header, QHeaderView): # self.h_header = self.horizontalHeader()
h_header.sectionResized.connect(self.resizeRowsToContents) # for idx in [a for a in range(len(columns))]:
# item = QTableWidgetItem()
# self.setHorizontalHeaderItem(idx, item)
# if self.h_header:
# self.h_header.setStretchLastSection(True)
# self.h_header.setMinimumSectionSize(0)
# # Set column headings sorted by idx
# self.v_header = self.verticalHeader()
# if self.v_header:
# self.v_header.setMinimumSectionSize(Config.MINIMUM_ROW_HEIGHT)
# self.setHorizontalHeaderLabels(
# [
# a.heading
# for a in list(sorted(columns.values(), key=lambda item: item.idx))
# ]
# )
self.horizontalHeader().sectionResized.connect(self.resizeRowsToContents)
# Drag and drop setup # Drag and drop setup
self.setAcceptDrops(True) self.setAcceptDrops(True)
viewport = self.viewport() self.viewport().setAcceptDrops(True)
if viewport:
viewport.setAcceptDrops(True)
self.setDragDropOverwriteMode(False) self.setDragDropOverwriteMode(False)
self.setDropIndicatorShown(True) self.setDropIndicatorShown(True)
self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
self.setDragEnabled(False) self.setDragEnabled(False)
# This property defines how the widget shows a context menu
# Prepare for context menu
self.menu = QMenu()
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.setContextMenuPolicy(Qt.ContextMenuPolicy.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.customContextMenuRequested.connect(self._context_menu)
# Connect signals
# This dancing is to satisfy mypy
h_header = self.horizontalHeader()
if isinstance(h_header, QHeaderView):
h_header.sectionResized.connect(self._column_resize)
h_header.setStretchLastSection(True)
# self.itemSelectionChanged.connect(self._select_event)
# self.signals.set_next_track_signal.connect(self._reset_next)
self.signals.span_cells_signal.connect(self._span_cells)
# Call self.eventFilter() for events # Call self.eventFilter() for events
# self.viewport().installEventFilter(self) self.viewport().installEventFilter(self)
# Initialise miscellaneous instance variables
self.search_text: str = "" self.search_text: str = ""
self.sort_undo: List[int] = [] self.sort_undo: List[int] = []
# self.edit_cell_type: Optional[int] self.edit_cell_type: Optional[int]
# Connect signals
self.horizontalHeader().sectionResized.connect(self._column_resize)
# self.itemSelectionChanged.connect(self._select_event)
# self.signals.set_next_track_signal.connect(self._reset_next)
# Load playlist rows # Load playlist rows
self.setModel(PlaylistModel(playlist_id, signals)) # self.populate_display(session, self.playlist_id)
self._set_column_widths()
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<PlaylistTab(id={self.playlist_id}>" return f"<PlaylistTab(id={self.playlist_id}>"
@ -778,6 +784,9 @@ class PlaylistTab(QTableView):
# Clear playlist # Clear playlist
self.setRowCount(0) self.setRowCount(0)
# Set widths
self._set_column_widths(session)
# Get played tracks # Get played tracks
played_rows = self._get_played_rows(session) played_rows = self._get_played_rows(session)
@ -1141,22 +1150,26 @@ class PlaylistTab(QTableView):
return start + timedelta(milliseconds=duration) return start + timedelta(milliseconds=duration)
def _column_resize(self, column_number: int, _old: int, _new: int) -> None: def _column_resize(self, idx: int, _old: int, _new: int) -> None:
""" """
Called when column width changes. Save new width to database. Called when column widths are changed.
Save column sizes to database
""" """
header = self.horizontalHeader()
if not header:
return
# Resize rows if necessary
self.resizeRowsToContents()
with Session() as session: with Session() as session:
attr_name = f"playlist_col_{column_number}_width" settings = Settings.all_as_dict(session)
record = Settings.get_int_settings(session, attr_name) for column_name, data in columns.items():
record.f_int = self.columnWidth(column_number) idx = data.idx
if idx == len(columns) - 1:
# Don't set width of last column as it's set to
# stretch
continue
width = self.columnWidth(idx)
attribute_name = f"playlist_{column_name}_col_width"
record = settings[attribute_name]
if record.f_int != self.columnWidth(idx):
record.update(session, {"f_int": width})
def _context_menu(self, pos): def _context_menu(self, pos):
"""Display right-click menu""" """Display right-click menu"""
@ -1880,41 +1893,39 @@ class PlaylistTab(QTableView):
else: else:
self.musicmuster.lblSumPlaytime.setText("") self.musicmuster.lblSumPlaytime.setText("")
# def _set_cell_colour( def _set_cell_colour(
# self, row_number: int, column: int, colour: Optional[str] = None self, row_number: int, column: int, colour: Optional[str] = None
# ) -> None: ) -> None:
# """ """
# Set or reset a cell background colour Set or reset a cell background colour
# """ """
# if colour is None: if colour is None:
# brush = QBrush() brush = QBrush()
# else: else:
# brush = QBrush(QColor(colour)) brush = QBrush(QColor(colour))
# item = self.item(row_number, column) item = self.item(row_number, column)
# if item: if item:
# item.setBackground(brush) item.setBackground(brush)
def _set_column_widths(self) -> None: def _set_column_widths(self, session: scoped_session) -> None:
"""Column widths from settings""" """Column widths from settings"""
header = self.horizontalHeader() settings = Settings.all_as_dict(session)
if not header:
return
for column_name, data in columns.items():
idx = data.idx
if idx == len(columns) - 1:
# Set width of last column to zero as it's set to stretch # Set width of last column to zero as it's set to stretch
self.setColumnWidth(header.count() - 1, 0) self.setColumnWidth(idx, 0)
continue
# Set remaining column widths from settings attr_name = f"playlist_{column_name}_col_width"
with Session() as session: record = settings[attr_name]
for column_number in range(header.count() - 1): if record and record.f_int >= 0:
attr_name = f"playlist_col_{column_number}_width" self.setColumnWidth(idx, record.f_int)
record = Settings.get_int_settings(session, attr_name)
if record.f_int is not None:
self.setColumnWidth(column_number, record.f_int)
else: else:
self.setColumnWidth(column_number, Config.DEFAULT_COLUMN_WIDTH) self.setColumnWidth(idx, Config.DEFAULT_COLUMN_WIDTH)
def _set_item_text( def _set_item_text(
self, row_number: int, column: int, text: Optional[str] self, row_number: int, column: int, text: Optional[str]
@ -2368,13 +2379,6 @@ class PlaylistTab(QTableView):
self.save_playlist(session) self.save_playlist(session)
self._update_start_end_times(session) self._update_start_end_times(session)
def _span_cells(self, row: int, column: int, rowSpan: int, columnSpan: int) -> None:
"""
Implement spanning of cells, initiated by signal
"""
self.setSpan(row, column, rowSpan, columnSpan)
def _track_time_between_rows( def _track_time_between_rows(
self, session: scoped_session, from_plr: PlaylistRows, to_plr: PlaylistRows self, session: scoped_session, from_plr: PlaylistRows, to_plr: PlaylistRows
) -> Tuple[int, int]: ) -> Tuple[int, int]: