Compare commits

...

4 Commits

Author SHA1 Message Date
Keith Edmunds
bb14b34c2e WIP V3: column widths set/save works 2023-10-20 11:30:54 +01:00
Keith Edmunds
dbbced7401 Fix repr() for Settings 2023-10-20 11:06:50 +01:00
Keith Edmunds
5fb5e12bb8 WIP: V3: All headers displaying 2023-10-20 08:54:48 +01:00
Keith Edmunds
978b83ba67 WIP: V3 header rows span columns 2023-10-19 18:29:09 +01:00
5 changed files with 182 additions and 126 deletions

View File

@ -56,6 +56,15 @@ 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,7 +629,10 @@ 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 f"<Settings(id={self.id}, name={self.name}, {value=}>" return (
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
@ -660,7 +663,7 @@ class Settings(Base):
except NoResultFound: except NoResultFound:
return Settings(session, name) return Settings(session, name)
def update(self, session: scoped_session, data: dict): def update(self, session: scoped_session, data: dict) -> None:
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,6 +245,7 @@ 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 from typing import Optional, TYPE_CHECKING
from PyQt6.QtCore import ( from PyQt6.QtCore import (
QAbstractTableModel, QAbstractTableModel,
@ -27,6 +27,9 @@ from models import (
PlaylistRows, PlaylistRows,
) )
if TYPE_CHECKING:
from musicmuster import MusicMusterSignals
class Col(Enum): class Col(Enum):
START_GAP = 0 START_GAP = 0
@ -35,11 +38,14 @@ class Col(Enum):
DURATION = auto() DURATION = auto()
START_TIME = auto() START_TIME = auto()
END_TIME = auto() END_TIME = auto()
LASTPLAYED = auto() LAST_PLAYED = 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:
""" """
@ -77,8 +83,11 @@ class PlaylistRowData:
class PlaylistModel(QAbstractTableModel): class PlaylistModel(QAbstractTableModel):
def __init__(self, playlist_id: int, *args, **kwargs): def __init__(
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] = {}
@ -93,12 +102,35 @@ 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"""
@ -146,6 +178,21 @@ 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:
@ -158,7 +205,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.LASTPLAYED.value: if column == Col.LAST_PLAYED.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)
@ -167,45 +214,45 @@ class PlaylistModel(QAbstractTableModel):
return QVariant() return QVariant()
def background_role(self, row: int, column: int, prd: PlaylistRowData) -> QBrush: def headerData(
""" Return background setting """ self,
section: int,
orientation: Qt.Orientation,
role: int = Qt.ItemDataRole.DisplayRole,
) -> QVariant:
"""
Return text for headers
"""
if column == Col.START_GAP.value: if role == Qt.ItemDataRole.DisplayRole:
if prd.start_gap and prd.start_gap >= Config.START_GAP_WARNING_THRESHOLD: if orientation == Qt.Orientation.Horizontal:
return QBrush(QColor(Config.COLOUR_LONG_START)) if section == Col.START_GAP.value:
if column == Col.BITRATE.value: return QVariant(Config.HEADER_START_GAP)
if prd.bitrate < Config.BITRATE_LOW_THRESHOLD: elif section == Col.TITLE.value:
return QBrush(QColor(Config.COLOUR_BITRATE_LOW)) return QVariant(Config.HEADER_TITLE)
elif prd.bitrate and prd.bitrate < Config.BITRATE_OK_THRESHOLD: elif section == Col.ARTIST.value:
return QBrush(QColor(Config.COLOUR_BITRATE_MEDIUM)) return QVariant(Config.HEADER_ARTIST)
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 QBrush(QColor(Config.COLOUR_BITRATE_OK)) return QVariant(str(section + 1))
return QBrush() elif role == Qt.ItemDataRole.FontRole:
boldfont = QFont()
boldfont.setBold(True)
return QVariant(boldfont)
# if rowdata.path and file_is_unreadable(rowdata.path): return QVariant()
# 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"""
@ -215,7 +262,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) -> int: def rowCount(self, index: QModelIndex = QModelIndex()) -> int:
"""Standard function for view""" """Standard function for view"""
return len(self.playlist_rows) return len(self.playlist_rows)

View File

@ -22,6 +22,7 @@ from PyQt6.QtWidgets import (
QAbstractItemDelegate, QAbstractItemDelegate,
QAbstractItemView, QAbstractItemView,
QApplication, QApplication,
QHeaderView,
QMenu, QMenu,
QMessageBox, QMessageBox,
QPlainTextEdit, QPlainTextEdit,
@ -44,7 +45,7 @@ from helpers import (
set_track_metadata, set_track_metadata,
) )
from log import log from log import log
from models import Playdates, Playlists, PlaylistRows, Settings, Tracks, NoteColours from models import Playlists, PlaylistRows, Settings, Tracks, NoteColours
from playlistmodel import PlaylistModel from playlistmodel import PlaylistModel
@ -144,67 +145,60 @@ 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
# Header row h_header = self.horizontalHeader()
# self.h_header = self.horizontalHeader() if isinstance(h_header, QHeaderView):
# for idx in [a for a in range(len(columns))]: h_header.sectionResized.connect(self.resizeRowsToContents)
# 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)
self.viewport().setAcceptDrops(True) viewport = self.viewport()
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
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)
# Call self.eventFilter() for events
self.viewport().installEventFilter(self)
self.search_text: str = "" # Prepare for context menu
self.sort_undo: List[int] = [] self.menu = QMenu()
self.edit_cell_type: Optional[int] self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.customContextMenuRequested.connect(self._context_menu)
# Connect signals # Connect signals
self.horizontalHeader().sectionResized.connect(self._column_resize) # 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.itemSelectionChanged.connect(self._select_event)
# self.signals.set_next_track_signal.connect(self._reset_next) # self.signals.set_next_track_signal.connect(self._reset_next)
self.signals.span_cells_signal.connect(self._span_cells)
# Call self.eventFilter() for events
# self.viewport().installEventFilter(self)
# Initialise miscellaneous instance variables
self.search_text: str = ""
self.sort_undo: List[int] = []
# self.edit_cell_type: Optional[int]
# Load playlist rows # Load playlist rows
# self.populate_display(session, self.playlist_id) self.setModel(PlaylistModel(playlist_id, signals))
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}>"
@ -784,9 +778,6 @@ 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)
@ -1150,26 +1141,22 @@ class PlaylistTab(QTableView):
return start + timedelta(milliseconds=duration) return start + timedelta(milliseconds=duration)
def _column_resize(self, idx: int, _old: int, _new: int) -> None: def _column_resize(self, column_number: 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:
settings = Settings.all_as_dict(session) attr_name = f"playlist_col_{column_number}_width"
for column_name, data in columns.items(): record = Settings.get_int_settings(session, attr_name)
idx = data.idx record.f_int = self.columnWidth(column_number)
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"""
@ -1893,39 +1880,41 @@ 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, session: scoped_session) -> None: def _set_column_widths(self) -> None:
"""Column widths from settings""" """Column widths from settings"""
settings = Settings.all_as_dict(session) header = self.horizontalHeader()
if not header:
return
for column_name, data in columns.items(): # Set width of last column to zero as it's set to stretch
idx = data.idx self.setColumnWidth(header.count() - 1, 0)
if idx == len(columns) - 1:
# Set width of last column to zero as it's set to stretch # Set remaining column widths from settings
self.setColumnWidth(idx, 0) with Session() as session:
continue for column_number in range(header.count() - 1):
attr_name = f"playlist_{column_name}_col_width" attr_name = f"playlist_col_{column_number}_width"
record = settings[attr_name] record = Settings.get_int_settings(session, attr_name)
if record and record.f_int >= 0: if record.f_int is not None:
self.setColumnWidth(idx, record.f_int) self.setColumnWidth(column_number, record.f_int)
else: else:
self.setColumnWidth(idx, Config.DEFAULT_COLUMN_WIDTH) self.setColumnWidth(column_number, 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]
@ -2379,6 +2368,13 @@ 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]: