Major update: correct use of proxy model

Fixes #273
This commit is contained in:
Keith Edmunds 2024-12-26 14:09:21 +00:00
parent 937f3cd074
commit b14b90396f
8 changed files with 509 additions and 376 deletions

View File

@ -1,7 +1,7 @@
# Standard library imports
from __future__ import annotations
from dataclasses import dataclass
from dataclasses import dataclass, field
from enum import auto, Enum
import functools
from typing import NamedTuple
@ -13,7 +13,6 @@ from PyQt6.QtCore import (
pyqtSignal,
QObject,
)
from PyQt6.QtGui import QAction
# App imports
@ -94,6 +93,13 @@ class MusicMusterSignals(QObject):
super().__init__()
@singleton
@dataclass
class Selection:
playlist_id: int = 0
rows: list[int] = field(default_factory=list)
class Tags(NamedTuple):
artist: str
title: str

View File

@ -106,6 +106,7 @@ class Config(object):
SECTION_STARTS = ("+", "+-", "-+")
SONGFACTS_ON_NEXT = False
START_GAP_WARNING_THRESHOLD = 300
SUBTOTAL_ON_ROW_ZERO = "[No subtotal on first row]"
TEXT_NO_TRACK_NO_NOTE = "[Section header]"
TOD_TIME_FORMAT = "%H:%M:%S"
TRACK_TIME_FORMAT = "%H:%M:%S"

View File

@ -40,7 +40,7 @@ class TrackSelectDialog(QDialog):
parent: QMainWindow,
session: Session,
new_row_number: int,
source_model: PlaylistModel,
base_model: PlaylistModel,
add_to_header: Optional[bool] = False,
*args: Qt.WindowType,
**kwargs: Qt.WindowType,
@ -52,7 +52,7 @@ class TrackSelectDialog(QDialog):
super().__init__(parent, *args, **kwargs)
self.session = session
self.new_row_number = new_row_number
self.source_model = source_model
self.base_model = base_model
self.add_to_header = add_to_header
self.ui = dlg_TrackSelect_ui.Ui_Dialog()
self.ui.setupUi(self)
@ -96,7 +96,7 @@ class TrackSelectDialog(QDialog):
track_id = track.id
if note and not track_id:
self.source_model.insert_row(self.new_row_number, track_id, note)
self.base_model.insert_row(self.new_row_number, track_id, note)
self.ui.txtNote.clear()
self.new_row_number += 1
return
@ -110,7 +110,7 @@ class TrackSelectDialog(QDialog):
# Check whether track is already in playlist
move_existing = False
existing_prd = self.source_model.is_track_in_playlist(track_id)
existing_prd = self.base_model.is_track_in_playlist(track_id)
if existing_prd is not None:
if ask_yes_no(
"Duplicate row",
@ -121,21 +121,21 @@ class TrackSelectDialog(QDialog):
if self.add_to_header:
if move_existing and existing_prd: # "and existing_prd" for mypy's benefit
self.source_model.move_track_to_header(
self.base_model.move_track_to_header(
self.new_row_number, existing_prd, note
)
else:
self.source_model.add_track_to_header(self.new_row_number, track_id)
self.base_model.add_track_to_header(self.new_row_number, track_id)
# Close dialog - we can only add one track to a header
self.accept()
else:
# Adding a new track row
if move_existing and existing_prd: # "and existing_prd" for mypy's benefit
self.source_model.move_track_add_note(
self.base_model.move_track_add_note(
self.new_row_number, existing_prd, note
)
else:
self.source_model.insert_row(self.new_row_number, track_id, note)
self.base_model.insert_row(self.new_row_number, track_id, note)
self.new_row_number += 1

View File

@ -58,7 +58,7 @@ class DoTrackImport(QObject):
destination_track_path: str,
track_id: int,
audio_metadata: AudioMetadata,
source_model: PlaylistModel,
base_model: PlaylistModel,
row_number: Optional[int],
) -> None:
"""
@ -72,10 +72,10 @@ class DoTrackImport(QObject):
self.destination_track_path = destination_track_path
self.track_id = track_id
self.audio_metadata = audio_metadata
self.source_model = source_model
self.base_model = base_model
if row_number is None:
self.next_row_number = source_model.rowCount()
self.next_row_number = base_model.rowCount()
else:
self.next_row_number = row_number
@ -130,7 +130,7 @@ class DoTrackImport(QObject):
session.commit()
helpers.normalise_track(self.destination_track_path)
self.source_model.insert_row(self.next_row_number, track.id, "imported")
self.base_model.insert_row(self.next_row_number, track.id, "imported")
self.next_row_number += 1
self.signals.status_message_signal.emit(
@ -144,14 +144,19 @@ class FileImporter:
Manage importing of files
"""
def __init__(self, active_proxy_model: PlaylistModel, row_number: int) -> None:
def __init__(
self, base_model: PlaylistModel, row_number: Optional[int] = None
) -> None:
"""
Set up class
"""
# Save parameters
self.active_proxy_model = active_proxy_model
self.row_number = row_number
self.base_model = base_model
if row_number:
self.row_number = row_number
else:
self.row_number = base_model.rowCount()
# Data structure to track files to import
self.import_files_data: list[TrackFileData] = []
# Dictionary of exsting tracks
@ -279,7 +284,7 @@ class FileImporter:
destination_track_path=f.destination_track_path,
track_id=f.track_id,
audio_metadata=helpers.get_audio_metadata(f.import_file_path),
source_model=self.active_proxy_model,
base_model=self.base_model,
row_number=self.row_number,
)
@ -336,7 +341,7 @@ class FileImporter:
f"{self.existing_tracks[track_id].title} "
f"({self.existing_tracks[track_id].artist})",
track_id,
str(self.existing_tracks[track_id].path)
str(self.existing_tracks[track_id].path),
)
)
@ -448,7 +453,7 @@ class PickMatch(QDialog):
self.init_ui(items_with_ids)
self.selected_id = -1
def init_ui(self, items_with_ids: list[tuple[str, int]]) -> None:
def init_ui(self, items_with_ids: list[tuple[str, int, str]]) -> None:
"""
Set up dialog
"""

View File

@ -1,11 +1,12 @@
#!/usr/bin/env python3
# Standard library imports
from dataclasses import dataclass, field
from slugify import slugify # type: ignore
from typing import List, Optional
import argparse
import datetime as dt
import os
from slugify import slugify # type: ignore
import subprocess
import sys
import urllib.parse
@ -48,6 +49,7 @@ import stackprinter # type: ignore
# App imports
from classes import (
MusicMusterSignals,
Selection,
TrackInfo,
)
from config import Config
@ -67,6 +69,27 @@ from utilities import check_db, update_bitrates
import helpers
class DownloadCSV(QDialog):
def __init__(self, parent=None):
super().__init__()
self.ui = Ui_DateSelect()
self.ui.setupUi(self)
self.ui.dateTimeEdit.setDate(QDate.currentDate())
self.ui.dateTimeEdit.setTime(QTime(19, 59, 0))
self.ui.buttonBox.accepted.connect(self.accept)
self.ui.buttonBox.rejected.connect(self.reject)
@dataclass
class PlaylistData:
base_model: PlaylistModel
proxy_model: PlaylistProxyModel
def __post_init__(self):
self.proxy_model.setSourceModel(self.base_model)
class PreviewManager:
"""
Manage track preview player
@ -178,6 +201,52 @@ class PreviewManager:
self.start_time = None
class SelectPlaylistDialog(QDialog):
def __init__(self, parent=None, playlists=None, session=None):
super().__init__()
if playlists is None:
return
self.ui = Ui_dlgSelectPlaylist()
self.ui.setupUi(self)
self.ui.lstPlaylists.itemDoubleClicked.connect(self.list_doubleclick)
self.ui.buttonBox.accepted.connect(self.open)
self.ui.buttonBox.rejected.connect(self.close)
self.session = session
self.playlist = None
record = Settings.get_setting(self.session, "select_playlist_dialog_width")
width = record.f_int or 800
record = Settings.get_setting(self.session, "select_playlist_dialog_height")
height = record.f_int or 600
self.resize(width, height)
for playlist in playlists:
p = QListWidgetItem()
p.setText(playlist.name)
p.setData(Qt.ItemDataRole.UserRole, playlist)
self.ui.lstPlaylists.addItem(p)
def __del__(self): # review
record = Settings.get_setting(self.session, "select_playlist_dialog_height")
record.f_int = self.height()
record = Settings.get_setting(self.session, "select_playlist_dialog_width")
record.f_int = self.width()
self.session.commit()
def list_doubleclick(self, entry): # review
self.playlist = entry.data(Qt.ItemDataRole.UserRole)
self.accept()
def open(self): # review
if self.ui.lstPlaylists.selectedItems():
item = self.ui.lstPlaylists.currentItem()
self.playlist = item.data(Qt.ItemDataRole.UserRole)
self.accept()
class Window(QMainWindow, Ui_MainWindow):
def __init__(
self, parent: Optional[QWidget] = None, *args: list, **kwargs: dict
@ -205,7 +274,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.widgetFadeVolume.setBackground(Config.FADE_CURVE_BACKGROUND)
self.move_source_rows: Optional[List[int]] = None
self.move_source_model: Optional[PlaylistProxyModel] = None
self.move_source_model: Optional[PlaylistModel] = None
self.disable_selection_timing = False
self.clock_counter = 0
@ -217,6 +286,8 @@ class Window(QMainWindow, Ui_MainWindow):
self.connect_signals_slots()
self.catch_return_key = False
self.importer: Optional[FileImporter] = None
self.selection = Selection()
self.playlists: dict[int, PlaylistData] = {}
if not Config.USE_INTERNAL_BROWSER:
webbrowser.register(
@ -256,7 +327,7 @@ class Window(QMainWindow, Ui_MainWindow):
def active_tab(self) -> PlaylistTab:
return self.tabPlaylist.currentWidget()
def active_proxy_model(self) -> PlaylistModel:
def active_proxy_model(self) -> PlaylistProxyModel:
return self.tabPlaylist.currentWidget().model()
def clear_next(self) -> None:
@ -473,15 +544,20 @@ class Window(QMainWindow, Ui_MainWindow):
def create_playlist_tab(self, playlist: Playlists) -> int:
"""
Take the passed playlist database object, create a playlist tab and
Take the passed proxy model, create a playlist tab and
add tab to display. Return index number of tab.
"""
log.debug(f"create_playlist_tab({playlist=})")
# Create model and proxy model
self.playlists[playlist.id] = PlaylistData(
base_model=PlaylistModel(playlist.id), proxy_model=PlaylistProxyModel()
)
# Create tab
playlist_tab = PlaylistTab(
musicmuster=self,
playlist_id=playlist.id,
musicmuster=self, model=self.playlists[playlist.id].proxy_model
)
idx = self.tabPlaylist.addTab(playlist_tab, playlist.name)
@ -648,6 +724,13 @@ class Window(QMainWindow, Ui_MainWindow):
if track_sequence.current:
track_sequence.current.fade()
def get_active_base_model(self) -> PlaylistModel:
"""
Return the model for the current tab
"""
return self.playlists[self.selection.playlist_id].base_model
def hide_played(self):
"""Toggle hide played tracks"""
@ -673,7 +756,7 @@ class Window(QMainWindow, Ui_MainWindow):
# We need to keep a referent to the FileImporter else it will be
# garbage collected while import threads are still running
self.importer = FileImporter(
self.active_proxy_model(),
self.get_active_base_model(),
self.active_tab().source_model_selected_row_number(),
)
self.importer.do_import()
@ -681,11 +764,6 @@ class Window(QMainWindow, Ui_MainWindow):
def insert_header(self) -> None:
"""Show dialog box to enter header text and add to playlist"""
proxy_model = self.active_proxy_model()
if proxy_model is None:
log.error("No proxy model")
return
# Get header text
dlg: QInputDialog = QInputDialog(self)
dlg.setInputMode(QInputDialog.InputMode.TextInput)
@ -693,7 +771,7 @@ class Window(QMainWindow, Ui_MainWindow):
dlg.resize(500, 100)
ok = dlg.exec()
if ok:
proxy_model.insert_row(
self.get_active_base_model().insert_row(
proposed_row_number=self.active_tab().source_model_selected_row_number(),
note=dlg.textValue(),
)
@ -710,7 +788,7 @@ class Window(QMainWindow, Ui_MainWindow):
parent=self,
session=session,
new_row_number=new_row_number,
source_model=self.active_proxy_model(),
base_model=self.get_active_base_model(),
)
dlg.exec()
session.commit()
@ -722,9 +800,10 @@ class Window(QMainWindow, Ui_MainWindow):
with db.Session() as session:
for playlist in Playlists.get_open(session):
if playlist:
_ = self.create_playlist_tab(playlist)
playlist_ids.append(playlist.id)
log.debug(f"load_last_playlists() loaded {playlist=}")
# Create tab
playlist_ids.append(self.create_playlist_tab(playlist))
# Set active tab
record = Settings.get_setting(session, "active_tab")
if record.f_int is not None and record.f_int >= 0:
@ -767,7 +846,7 @@ class Window(QMainWindow, Ui_MainWindow):
# Save the selected PlaylistRows items ready for a later
# paste
self.move_source_rows = self.active_tab().get_selected_rows()
self.move_source_model = self.active_proxy_model()
self.move_source_model = self.get_active_base_model()
log.debug(
f"mark_rows_for_moving(): {self.move_source_rows=} {self.move_source_model=}"
@ -804,7 +883,7 @@ class Window(QMainWindow, Ui_MainWindow):
to_row = 0
# Move rows
self.active_proxy_model().move_rows_between_playlists(
self.get_active_base_model().move_rows_between_playlists(
row_numbers, to_row, to_playlist_id
)
@ -834,7 +913,7 @@ class Window(QMainWindow, Ui_MainWindow):
Move unplayed rows to another playlist
"""
unplayed_rows = self.active_proxy_model().get_unplayed_rows()
unplayed_rows = self.get_active_base_model().get_unplayed_rows()
if not unplayed_rows:
return
# We can get a race condition as selected rows change while
@ -917,7 +996,7 @@ class Window(QMainWindow, Ui_MainWindow):
if not self.move_source_rows or not self.move_source_model:
return
to_playlist_model: PlaylistModel = self.active_tab().source_model
to_playlist_model = self.get_active_base_model()
selected_rows = self.active_tab().get_selected_rows()
if selected_rows:
destination_row = selected_rows[0]
@ -934,10 +1013,7 @@ class Window(QMainWindow, Ui_MainWindow):
):
set_next_row = destination_row
if (
to_playlist_model.playlist_id
== self.move_source_model.source_model.playlist_id
):
if to_playlist_model.playlist_id == self.move_source_model.playlist_id:
self.move_source_model.move_rows(self.move_source_rows, destination_row)
else:
self.move_source_model.move_rows_between_playlists(
@ -1064,6 +1140,8 @@ class Window(QMainWindow, Ui_MainWindow):
)
else:
return
if not track_info:
return
self.preview_manager.set_track_info(track_info)
self.preview_manager.play()
else:
@ -1096,6 +1174,8 @@ class Window(QMainWindow, Ui_MainWindow):
if self.preview_manager.is_playing():
track_id = self.preview_manager.track_id
row_number = self.preview_manager.row_number
if not row_number:
return
with db.Session() as session:
track = session.get(Tracks, track_id)
if track:
@ -1106,8 +1186,8 @@ class Window(QMainWindow, Ui_MainWindow):
track.intro = intro
session.commit()
self.preview_manager.set_intro(intro)
self.active_tab().source_model.refresh_row(session, row_number)
self.active_tab().source_model.invalidate_row(row_number)
self.get_active_base_model().refresh_row(session, row_number)
self.get_active_base_model().invalidate_row(row_number)
def preview_start(self) -> None:
"""Restart preview"""
@ -1294,7 +1374,7 @@ class Window(QMainWindow, Ui_MainWindow):
if row_number is None:
return None
track_info = self.active_proxy_model().get_row_info(row_number)
track_info = self.get_active_base_model().get_row_info(row_number)
if track_info is None:
return None
@ -1385,9 +1465,7 @@ class Window(QMainWindow, Ui_MainWindow):
display_row = (
self.active_proxy_model()
.mapFromSource(
self.active_proxy_model().source_model.index(
playlist_track.row_number, 0
)
self.get_active_base_model().index(playlist_track.row_number, 0)
)
.row()
)
@ -1430,7 +1508,7 @@ class Window(QMainWindow, Ui_MainWindow):
def tab_change(self) -> None:
"""Called when active tab changed"""
self.active_tab().resize_rows()
self.active_tab().tab_live()
def tick_10ms(self) -> None:
"""
@ -1618,64 +1696,6 @@ class Window(QMainWindow, Ui_MainWindow):
self.tabPlaylist.setTabIcon(idx, QIcon())
class DownloadCSV(QDialog):
def __init__(self, parent=None):
super().__init__()
self.ui = Ui_DateSelect()
self.ui.setupUi(self)
self.ui.dateTimeEdit.setDate(QDate.currentDate())
self.ui.dateTimeEdit.setTime(QTime(19, 59, 0))
self.ui.buttonBox.accepted.connect(self.accept)
self.ui.buttonBox.rejected.connect(self.reject)
class SelectPlaylistDialog(QDialog):
def __init__(self, parent=None, playlists=None, session=None):
super().__init__()
if playlists is None:
return
self.ui = Ui_dlgSelectPlaylist()
self.ui.setupUi(self)
self.ui.lstPlaylists.itemDoubleClicked.connect(self.list_doubleclick)
self.ui.buttonBox.accepted.connect(self.open)
self.ui.buttonBox.rejected.connect(self.close)
self.session = session
self.playlist = None
record = Settings.get_setting(self.session, "select_playlist_dialog_width")
width = record.f_int or 800
record = Settings.get_setting(self.session, "select_playlist_dialog_height")
height = record.f_int or 600
self.resize(width, height)
for playlist in playlists:
p = QListWidgetItem()
p.setText(playlist.name)
p.setData(Qt.ItemDataRole.UserRole, playlist)
self.ui.lstPlaylists.addItem(p)
def __del__(self): # review
record = Settings.get_setting(self.session, "select_playlist_dialog_height")
record.f_int = self.height()
record = Settings.get_setting(self.session, "select_playlist_dialog_width")
record.f_int = self.width()
self.session.commit()
def list_doubleclick(self, entry): # review
self.playlist = entry.data(Qt.ItemDataRole.UserRole)
self.accept()
def open(self): # review
if self.ui.lstPlaylists.selectedItems():
item = self.ui.lstPlaylists.currentItem()
self.playlist = item.data(Qt.ItemDataRole.UserRole)
self.accept()
if __name__ == "__main__":
"""
If command line arguments given, carry out requested function and

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from operator import attrgetter
from random import shuffle
from typing import Optional
from typing import cast, Optional
import datetime as dt
import re
@ -1340,8 +1340,9 @@ class PlaylistModel(QAbstractTableModel):
unplayed_count += 1
duration += row_rat.duration
# Should never get here
return f"Error calculating subtotal ({row_rat.note})"
# We should only get here if there were no rows in section (ie,
# this was row zero)
return Config.SUBTOTAL_ON_ROW_ZERO
def selection_is_sortable(self, row_numbers: list[int]) -> bool:
"""
@ -1662,19 +1663,14 @@ class PlaylistProxyModel(QSortFilterProxyModel):
def __init__(
self,
source_model: PlaylistModel,
*args: QObject,
**kwargs: QObject,
) -> None:
self.source_model = source_model
super().__init__(*args, **kwargs)
super().__init__()
self.setSourceModel(source_model)
# Search all columns
self.setFilterKeyColumn(-1)
def __repr__(self) -> str:
return f"<PlaylistProxyModel: source_model={self.source_model}>"
return f"<PlaylistProxyModel: sourceModel={self.sourceModel}>"
def filterAcceptsRow(self, source_row: int, source_parent: QModelIndex) -> bool:
"""
@ -1684,13 +1680,13 @@ class PlaylistProxyModel(QSortFilterProxyModel):
if Config.HIDE_PLAYED_MODE != Config.HIDE_PLAYED_MODE_TRACKS:
return super().filterAcceptsRow(source_row, source_parent)
if self.source_model.played_tracks_hidden:
if self.source_model.is_played_row(source_row):
if self.sourceModel().played_tracks_hidden:
if self.sourceModel().is_played_row(source_row):
# Don't hide current track
if (
track_sequence.current
and track_sequence.current.playlist_id
== self.source_model.playlist_id
== self.sourceModel().playlist_id
and track_sequence.current.row_number == source_row
):
return True
@ -1698,7 +1694,8 @@ class PlaylistProxyModel(QSortFilterProxyModel):
# Don't hide next track
if (
track_sequence.next
and track_sequence.next.playlist_id == self.source_model.playlist_id
and track_sequence.next.playlist_id
== self.sourceModel().playlist_id
and track_sequence.next.row_number == source_row
):
return True
@ -1707,7 +1704,7 @@ class PlaylistProxyModel(QSortFilterProxyModel):
if track_sequence.previous:
if (
track_sequence.previous.playlist_id
!= self.source_model.playlist_id
!= self.sourceModel().playlist_id
or track_sequence.previous.row_number != source_row
):
# This row isn't our previous track: hide it
@ -1731,7 +1728,7 @@ class PlaylistProxyModel(QSortFilterProxyModel):
# true next time through.
QTimer.singleShot(
Config.HIDE_AFTER_PLAYING_OFFSET + 100,
lambda: self.source_model.invalidate_row(source_row),
lambda: self.sourceModel().invalidate_row(source_row),
)
return True
# Next track not playing yet so don't hide previous
@ -1754,105 +1751,9 @@ class PlaylistProxyModel(QSortFilterProxyModel):
)
)
# ######################################
# Forward functions not handled in proxy
# ######################################
def sourceModel(self) -> PlaylistModel:
"""
Override sourceModel to return correct type
"""
def current_track_started(self):
return self.source_model.current_track_started()
def delete_rows(self, row_numbers: list[int]) -> None:
return self.source_model.delete_rows(row_numbers)
def get_duplicate_rows(self) -> list[int]:
return self.source_model.get_duplicate_rows()
def get_rows_duration(self, row_numbers: list[int]) -> int:
return self.source_model.get_rows_duration(row_numbers)
def get_row_info(self, row_number: int) -> RowAndTrack:
return self.source_model.get_row_info(row_number)
def get_row_track_path(self, row_number: int) -> str:
return self.source_model.get_row_track_path(row_number)
def get_unplayed_rows(self) -> list[int]:
return self.source_model.get_unplayed_rows()
def hide_played_tracks(self, hide: bool) -> None:
return self.source_model.hide_played_tracks(hide)
def insert_row(
self,
proposed_row_number: Optional[int],
track_id: Optional[int] = None,
note: str = "",
) -> None:
return self.source_model.insert_row(proposed_row_number, track_id, note)
def is_header_row(self, row_number: int) -> bool:
return self.source_model.is_header_row(row_number)
def is_played_row(self, row_number: int) -> bool:
return self.source_model.is_played_row(row_number)
def is_track_in_playlist(self, track_id: int) -> Optional[RowAndTrack]:
return self.source_model.is_track_in_playlist(track_id)
def mark_unplayed(self, row_numbers: list[int]) -> None:
return self.source_model.mark_unplayed(row_numbers)
def move_rows(self, from_rows: list[int], to_row_number: int) -> None:
return self.source_model.move_rows(from_rows, to_row_number)
def move_rows_between_playlists(
self, from_rows: list[int], to_row_number: int, to_playlist_id: int
) -> None:
return self.source_model.move_rows_between_playlists(
from_rows, to_row_number, to_playlist_id
)
def move_track_add_note(
self, new_row_number: int, existing_rat: RowAndTrack, note: str
) -> None:
return self.source_model.move_track_add_note(new_row_number, existing_rat, note)
def move_track_to_header(
self,
header_row_number: int,
existing_rat: RowAndTrack,
note: Optional[str],
) -> None:
return self.source_model.move_track_to_header(
header_row_number, existing_rat, note
)
def previous_track_ended(self) -> None:
return self.source_model.previous_track_ended()
def remove_track(self, row_number: int) -> None:
return self.source_model.remove_track(row_number)
def rescan_track(self, row_number: int) -> None:
return self.source_model.rescan_track(row_number)
def set_next_row(self, row_number: Optional[int]) -> None:
self.source_model.set_next_row(row_number)
def sort_by_artist(self, row_numbers: list[int]) -> None:
return self.source_model.sort_by_artist(row_numbers)
def sort_by_duration(self, row_numbers: list[int]) -> None:
return self.source_model.sort_by_duration(row_numbers)
def sort_by_lastplayed(self, row_numbers: list[int]) -> None:
return self.source_model.sort_by_lastplayed(row_numbers)
def sort_randomly(self, row_numbers: list[int]) -> None:
return self.source_model.sort_randomly(row_numbers)
def sort_by_title(self, row_numbers: list[int]) -> None:
return self.source_model.sort_by_title(row_numbers)
def update_track_times(self) -> None:
return self.source_model.update_track_times()
return cast(PlaylistModel, super().sourceModel())

View File

@ -37,7 +37,7 @@ import line_profiler
# App imports
from audacity_controller import AudacityController
from classes import ApplicationError, Col, MusicMusterSignals, TrackInfo
from classes import ApplicationError, Col, MusicMusterSignals, Selection, TrackInfo
from config import Config
from dialogs import TrackSelectDialog
from helpers import (
@ -82,9 +82,9 @@ class PlaylistDelegate(QStyledItemDelegate):
QTimer.singleShot(0, resize_func)
def __init__(self, parent: QWidget, source_model: PlaylistModel) -> None:
def __init__(self, parent: QWidget, base_model: PlaylistModel) -> None:
super().__init__(parent)
self.source_model = source_model
self.base_model = base_model
self.signals = MusicMusterSignals()
self.click_position = None
self.current_editor: Optional[Any] = None
@ -239,7 +239,7 @@ class PlaylistDelegate(QStyledItemDelegate):
proxy_model = index.model()
edit_index = proxy_model.mapToSource(index)
self.original_model_data = self.source_model.data(
self.original_model_data = self.base_model.data(
edit_index, Qt.ItemDataRole.EditRole
)
if index.column() == Col.INTRO.value:
@ -256,7 +256,7 @@ class PlaylistDelegate(QStyledItemDelegate):
value = editor.toPlainText().strip()
elif isinstance(editor, QDoubleSpinBox):
value = editor.value()
self.source_model.setData(edit_index, value, Qt.ItemDataRole.EditRole)
self.base_model.setData(edit_index, value, Qt.ItemDataRole.EditRole)
def updateEditorGeometry(self, editor, option, index):
editor.setGeometry(option.rect)
@ -285,22 +285,17 @@ class PlaylistTab(QTableView):
The playlist view
"""
def __init__(
self,
musicmuster: "Window",
playlist_id: int,
) -> None:
def __init__(self, musicmuster: "Window", model: PlaylistProxyModel) -> None:
super().__init__()
# Save passed settings
self.musicmuster = musicmuster
self.playlist_id = playlist_id
log.debug(f"PlaylistTab.__init__({playlist_id=})")
self.musicmuster = (
musicmuster # TODO: do we need to keep a reference to musicmuster?
)
self.playlist_id = model.sourceModel().playlist_id
# Set up widget
self.source_model = PlaylistModel(playlist_id)
self.proxy_model = PlaylistProxyModel(self.source_model)
self.setItemDelegate(PlaylistDelegate(self, self.source_model))
self.setItemDelegate(PlaylistDelegate(self, model.sourceModel()))
self.setAlternatingRowColors(True)
self.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
@ -328,9 +323,8 @@ class PlaylistTab(QTableView):
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
# Load playlist rows
self.setModel(self.proxy_model)
self._set_column_widths()
# Singleton object to store selection
self.selection = Selection()
# Set up for Audacity
try:
@ -339,6 +333,10 @@ class PlaylistTab(QTableView):
self.ac = None
show_warning(self.musicmuster, "Audacity error", str(e))
# Load model, set column widths
self.setModel(model)
self._set_column_widths()
# Stretch last column *after* setting column widths which is
# *much* faster
h_header = self.horizontalHeader()
@ -373,7 +371,7 @@ class PlaylistTab(QTableView):
# Update start times in case a start time in a note has been
# edited
self.source_model.update_track_times()
self.get_base_model().update_track_times()
# Deselect edited line
self.clear_selection()
@ -382,36 +380,50 @@ class PlaylistTab(QTableView):
def dropEvent(
self, event: Optional[QDropEvent], dummy_for_profiling: Optional[int] = None
) -> None:
"""
Move dropped rows
"""
if not event:
return
if event.source() is not self or (
event.dropAction() != Qt.DropAction.MoveAction
and self.dragDropMode() != QAbstractItemView.DragDropMode.InternalMove
):
super().dropEvent(event)
return super().dropEvent(event)
from_rows = self.selected_model_row_numbers()
to_index = self.indexAt(event.position().toPoint())
# The drop indicator can either be immediately below a row or
# immediately above a row. There's about a 1 pixel difference,
# but we always want to drop between rows regardless of where
# drop indicator is.
if (
self.dropIndicatorPosition()
== QAbstractItemView.DropIndicatorPosition.BelowItem
):
proxy_index = self.proxy_model.createIndex(
to_index.row() + 1,
to_index.column(),
to_index.internalId(),
)
# Drop on the row below
next_row = to_index.row() + 1
if next_row < self.model().rowCount(): # Ensure the row exists
destination_index = to_index.siblingAtRow(next_row)
else:
# Handle edge case where next_row is beyond the last row
destination_index = to_index
else:
proxy_index = to_index
to_model_row = self.proxy_model.mapToSource(proxy_index).row()
destination_index = to_index
to_model_row = self.model().mapToSource(destination_index).row()
log.debug(
f"PlaylistTab.dropEvent(): {from_rows=}, {proxy_index=}, {to_model_row=}"
f"PlaylistTab.dropEvent(): {from_rows=}, {destination_index=}, {to_model_row=}"
)
# Sanity check
base_model_row_count = self.get_base_model().rowCount()
if (
0 <= min(from_rows) <= self.source_model.rowCount()
and 0 <= max(from_rows) <= self.source_model.rowCount()
and 0 <= to_model_row <= self.source_model.rowCount()
0 <= min(from_rows) <= base_model_row_count
and 0 <= to_model_row <= base_model_row_count
):
# If we move a row to immediately under the current track, make
# that moved row the next track
@ -422,7 +434,7 @@ class PlaylistTab(QTableView):
):
set_next_row = to_model_row
self.source_model.move_rows(from_rows, to_model_row)
self.get_base_model().move_rows(from_rows, to_model_row)
# Reset drag mode to allow row selection by dragging
self.setDragEnabled(False)
@ -435,7 +447,7 @@ class PlaylistTab(QTableView):
# Set next row if we are immediately under current row
if set_next_row:
self.source_model.set_next_row(set_next_row)
self.get_base_model().set_next_row(set_next_row)
event.accept()
@ -469,12 +481,14 @@ class PlaylistTab(QTableView):
"""
selected_rows = self.get_selected_rows()
self.selection.rows = selected_rows
# If no rows are selected, we have nothing to do
if len(selected_rows) == 0:
self.musicmuster.lblSumPlaytime.setText("")
else:
if not self.musicmuster.disable_selection_timing:
selected_duration = self.source_model.get_rows_duration(
selected_duration = self.get_base_model().get_rows_duration(
self.get_selected_rows()
)
if selected_duration > 0:
@ -525,7 +539,7 @@ class PlaylistTab(QTableView):
parent=self.musicmuster,
session=session,
new_row_number=model_row_number,
source_model=self.source_model,
base_model=self.get_base_model(),
add_to_header=True,
)
dlg.exec()
@ -535,12 +549,12 @@ class PlaylistTab(QTableView):
"""Used to process context (right-click) menu, which is defined here"""
self.menu.clear()
proxy_model = self.proxy_model
index = proxy_model.index(item.row(), item.column())
model_row_number = proxy_model.mapToSource(index).row()
index = self.model().index(item.row(), item.column())
model_row_number = self.model().mapToSource(index).row()
base_model = self.get_base_model()
header_row = proxy_model.is_header_row(model_row_number)
header_row = self.get_base_model().is_header_row(model_row_number)
track_row = not header_row
if track_sequence.current:
this_is_current_row = model_row_number == track_sequence.current.row_number
@ -550,7 +564,7 @@ class PlaylistTab(QTableView):
this_is_next_row = model_row_number == track_sequence.next.row_number
else:
this_is_next_row = False
track_path = self.source_model.get_row_info(model_row_number).path
track_path = base_model.get_row_info(model_row_number).path
# Open/import in/from Audacity
if track_row and not this_is_current_row:
@ -591,7 +605,7 @@ class PlaylistTab(QTableView):
if track_row and not this_is_current_row and not this_is_next_row:
self._add_context_menu(
"Remove track from row",
lambda: proxy_model.remove_track(model_row_number),
lambda: base_model.remove_track(model_row_number),
)
# Remove comments
@ -605,7 +619,7 @@ class PlaylistTab(QTableView):
self.menu.addSeparator()
# Mark unplayed
if track_row and proxy_model.is_played_row(model_row_number):
if track_row and base_model.is_played_row(model_row_number):
self._add_context_menu(
"Mark unplayed",
lambda: self._mark_as_unplayed(self.get_selected_rows()),
@ -624,27 +638,27 @@ class PlaylistTab(QTableView):
sort_menu = self.menu.addMenu("Sort")
self._add_context_menu(
"by title",
lambda: proxy_model.sort_by_title(self.get_selected_rows()),
lambda: base_model.sort_by_title(self.get_selected_rows()),
parent_menu=sort_menu,
)
self._add_context_menu(
"by artist",
lambda: proxy_model.sort_by_artist(self.get_selected_rows()),
lambda: base_model.sort_by_artist(self.get_selected_rows()),
parent_menu=sort_menu,
)
self._add_context_menu(
"by duration",
lambda: proxy_model.sort_by_duration(self.get_selected_rows()),
lambda: base_model.sort_by_duration(self.get_selected_rows()),
parent_menu=sort_menu,
)
self._add_context_menu(
"by last played",
lambda: proxy_model.sort_by_lastplayed(self.get_selected_rows()),
lambda: base_model.sort_by_lastplayed(self.get_selected_rows()),
parent_menu=sort_menu,
)
self._add_context_menu(
"randomly",
lambda: proxy_model.sort_randomly(self.get_selected_rows()),
lambda: base_model.sort_randomly(self.get_selected_rows()),
parent_menu=sort_menu,
)
@ -711,7 +725,7 @@ class PlaylistTab(QTableView):
to the clipboard. Otherwise, return None.
"""
track_path = self.source_model.get_row_info(row_number).path
track_path = self.get_base_model().get_row_info(row_number).path
if not track_path:
return
@ -734,7 +748,7 @@ class PlaylistTab(QTableView):
Called when track starts playing
"""
self.source_model.current_track_started()
self.get_base_model().current_track_started()
# Scroll to current section if hide mode is by section
if (
self.musicmuster.hide_played_tracks
@ -766,9 +780,18 @@ class PlaylistTab(QTableView):
if not ask_yes_no("Delete rows", f"Really delete {row_count} row{plural}?"):
return
self.source_model.delete_rows(self.selected_model_row_numbers())
base_model = self.get_base_model()
base_model.delete_rows(self.selected_model_row_numbers())
self.clear_selection()
def get_base_model(self) -> PlaylistModel:
"""
Return the base model for this proxy model
"""
return cast(PlaylistModel, self.model().sourceModel())
def get_selected_row_track_info(self) -> Optional[TrackInfo]:
"""
Return the track_id and row number of the selected
@ -780,11 +803,13 @@ class PlaylistTab(QTableView):
if selected_row is None:
return None
base_model = self.get_base_model()
model_row_number = self.source_model_selected_row_number()
if model_row_number is None:
return None
else:
track_id = self.source_model.get_row_track_id(model_row_number)
track_id = base_model.get_row_track_id(model_row_number)
if not track_id:
return None
else:
@ -808,12 +833,7 @@ class PlaylistTab(QTableView):
# items in that row selected)
result = sorted(
list(
set(
[
self.proxy_model.mapToSource(a).row()
for a in self.selectedIndexes()
]
)
set([self.model().mapToSource(a).row() for a in self.selectedIndexes()])
)
)
@ -825,7 +845,7 @@ class PlaylistTab(QTableView):
Scroll played sections off screen
"""
self.scroll_to_top(self.source_model.active_section_header())
self.scroll_to_top(self.get_base_model().active_section_header())
def _import_from_audacity(self, row_number: int) -> None:
"""
@ -844,7 +864,7 @@ class PlaylistTab(QTableView):
def _info_row(self, row_number: int) -> None:
"""Display popup with info re row"""
prd = self.source_model.get_row_info(row_number)
prd = self.get_base_model().get_row_info(row_number)
if prd:
txt = (
f"Title: {prd.title}\n"
@ -863,7 +883,7 @@ class PlaylistTab(QTableView):
def _mark_as_unplayed(self, row_numbers: List[int]) -> None:
"""Mark row as unplayed"""
self.source_model.mark_unplayed(row_numbers)
self.get_base_model().mark_unplayed(row_numbers)
self.clear_selection()
def _mark_for_moving(self) -> None:
@ -873,6 +893,13 @@ class PlaylistTab(QTableView):
self.musicmuster.mark_rows_for_moving()
def model(self) -> PlaylistProxyModel:
"""
Override return type to keep mypy happy in this module
"""
return cast(PlaylistProxyModel, super().model())
def _move_selected_rows(self) -> None:
"""
Move selected rows here
@ -885,7 +912,7 @@ class PlaylistTab(QTableView):
Open track in passed row in Audacity
"""
path = self.source_model.get_row_track_path(row_number)
path = self.get_base_model().get_row_track_path(row_number)
if not path:
log.error(f"_open_in_audacity: can't get path for {row_number=}")
return
@ -903,7 +930,7 @@ class PlaylistTab(QTableView):
"""
# Let the model know
self.source_model.previous_track_ended()
self.get_base_model().previous_track_ended()
def _remove_comments(self) -> None:
"""
@ -914,12 +941,12 @@ class PlaylistTab(QTableView):
if not row_numbers:
return
self.source_model.remove_comments(row_numbers)
self.get_base_model().remove_comments(row_numbers)
def _rescan(self, row_number: int) -> None:
"""Rescan track"""
self.source_model.rescan_track(row_number)
self.get_base_model().rescan_track(row_number)
self.clear_selection()
def resize_rows(self, playlist_id: Optional[int] = None) -> None:
@ -934,7 +961,7 @@ class PlaylistTab(QTableView):
# Suggestion from phind.com
def resize_row(row, count=1):
row_count = self.source_model.rowCount()
row_count = self.model().rowCount()
for todo in range(count):
if row < row_count:
self.resizeRowToContents(row)
@ -953,7 +980,7 @@ class PlaylistTab(QTableView):
if row_number is None:
return
row_index = self.proxy_model.index(row_number, 0)
row_index = self.model().index(row_number, 0)
self.scrollTo(row_index, QAbstractItemView.ScrollHint.PositionAtTop)
def select_duplicate_rows(self) -> None:
@ -968,7 +995,7 @@ class PlaylistTab(QTableView):
# We need to be in MultiSelection mode
self.setSelectionMode(QAbstractItemView.SelectionMode.MultiSelection)
# Get the duplicate rows
duplicate_rows = self.source_model.get_duplicate_rows()
duplicate_rows = self.get_base_model().get_duplicate_rows()
# Select the rows
for duplicate_row in duplicate_rows:
self.selectRow(duplicate_row)
@ -983,7 +1010,7 @@ class PlaylistTab(QTableView):
selected_index = self._selected_row_index()
if selected_index is None:
return None
return self.proxy_model.mapToSource(selected_index).row()
return self.model().mapToSource(selected_index).row()
def selected_model_row_numbers(self) -> List[int]:
"""
@ -994,9 +1021,8 @@ class PlaylistTab(QTableView):
selected_indexes = self._selected_row_indexes()
if selected_indexes is None:
return []
if hasattr(self.proxy_model, "mapToSource"):
return [self.proxy_model.mapToSource(a).row() for a in selected_indexes]
return [a.row() for a in selected_indexes]
return [self.model().mapToSource(a).row() for a in selected_indexes]
def _selected_row_index(self) -> Optional[QModelIndex]:
"""
@ -1053,7 +1079,7 @@ class PlaylistTab(QTableView):
log.debug(f"set_row_as_next_track() {model_row_number=}")
if model_row_number is None:
return
self.source_model.set_next_row(model_row_number)
self.get_base_model().set_next_row(model_row_number)
self.clearSelection()
def _span_cells(
@ -1061,17 +1087,19 @@ class PlaylistTab(QTableView):
) -> None:
"""
Implement spanning of cells, initiated by signal
row and column are from the base model so we need to translate
the row into this display row
"""
if playlist_id != self.playlist_id:
return
proxy_model = self.proxy_model
edit_index = proxy_model.mapFromSource(
self.source_model.createIndex(row, column)
)
row = edit_index.row()
column = edit_index.column()
base_model = self.get_base_model()
cell_index = self.model().mapFromSource(base_model.createIndex(row, column))
row = cell_index.row()
column = cell_index.column()
# Don't set spanning if already in place because that is seen as
# a change to the view and thus it refreshes the data which
@ -1084,6 +1112,16 @@ class PlaylistTab(QTableView):
self.setSpan(row, column, rowSpan, columnSpan)
def tab_live(self) -> None:
"""
Called when tab gets focus
"""
self.selection.playlist_id = self.playlist_id
self.selection.rows = self.get_selected_rows()
self.resize_rows()
def _unmark_as_next(self) -> None:
"""Rescan track"""

View File

@ -15,7 +15,11 @@ class Ui_MainWindow(object):
MainWindow.resize(1280, 857)
MainWindow.setMinimumSize(QtCore.QSize(1280, 0))
icon = QtGui.QIcon()
icon.addPixmap(QtGui.QPixmap(":/icons/musicmuster"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
icon.addPixmap(
QtGui.QPixmap(":/icons/musicmuster"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
MainWindow.setWindowIcon(icon)
MainWindow.setStyleSheet("")
self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
@ -27,39 +31,62 @@ class Ui_MainWindow(object):
self.verticalLayout_3 = QtWidgets.QVBoxLayout()
self.verticalLayout_3.setObjectName("verticalLayout_3")
self.previous_track_2 = QtWidgets.QLabel(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred,
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.previous_track_2.sizePolicy().hasHeightForWidth())
sizePolicy.setHeightForWidth(
self.previous_track_2.sizePolicy().hasHeightForWidth()
)
self.previous_track_2.setSizePolicy(sizePolicy)
self.previous_track_2.setMaximumSize(QtCore.QSize(230, 16777215))
font = QtGui.QFont()
font.setFamily("Sans")
font.setPointSize(20)
self.previous_track_2.setFont(font)
self.previous_track_2.setStyleSheet("background-color: #f8d7da;\n"
"border: 1px solid rgb(85, 87, 83);")
self.previous_track_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
self.previous_track_2.setStyleSheet(
"background-color: #f8d7da;\n" "border: 1px solid rgb(85, 87, 83);"
)
self.previous_track_2.setAlignment(
QtCore.Qt.AlignmentFlag.AlignRight
| QtCore.Qt.AlignmentFlag.AlignTrailing
| QtCore.Qt.AlignmentFlag.AlignVCenter
)
self.previous_track_2.setObjectName("previous_track_2")
self.verticalLayout_3.addWidget(self.previous_track_2)
self.current_track_2 = QtWidgets.QLabel(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred,
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.current_track_2.sizePolicy().hasHeightForWidth())
sizePolicy.setHeightForWidth(
self.current_track_2.sizePolicy().hasHeightForWidth()
)
self.current_track_2.setSizePolicy(sizePolicy)
self.current_track_2.setMaximumSize(QtCore.QSize(230, 16777215))
font = QtGui.QFont()
font.setFamily("Sans")
font.setPointSize(20)
self.current_track_2.setFont(font)
self.current_track_2.setStyleSheet("background-color: #d4edda;\n"
"border: 1px solid rgb(85, 87, 83);")
self.current_track_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
self.current_track_2.setStyleSheet(
"background-color: #d4edda;\n" "border: 1px solid rgb(85, 87, 83);"
)
self.current_track_2.setAlignment(
QtCore.Qt.AlignmentFlag.AlignRight
| QtCore.Qt.AlignmentFlag.AlignTrailing
| QtCore.Qt.AlignmentFlag.AlignVCenter
)
self.current_track_2.setObjectName("current_track_2")
self.verticalLayout_3.addWidget(self.current_track_2)
self.next_track_2 = QtWidgets.QLabel(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred,
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.next_track_2.sizePolicy().hasHeightForWidth())
@ -69,19 +96,29 @@ class Ui_MainWindow(object):
font.setFamily("Sans")
font.setPointSize(20)
self.next_track_2.setFont(font)
self.next_track_2.setStyleSheet("background-color: #fff3cd;\n"
"border: 1px solid rgb(85, 87, 83);")
self.next_track_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
self.next_track_2.setStyleSheet(
"background-color: #fff3cd;\n" "border: 1px solid rgb(85, 87, 83);"
)
self.next_track_2.setAlignment(
QtCore.Qt.AlignmentFlag.AlignRight
| QtCore.Qt.AlignmentFlag.AlignTrailing
| QtCore.Qt.AlignmentFlag.AlignVCenter
)
self.next_track_2.setObjectName("next_track_2")
self.verticalLayout_3.addWidget(self.next_track_2)
self.horizontalLayout_3.addLayout(self.verticalLayout_3)
self.verticalLayout = QtWidgets.QVBoxLayout()
self.verticalLayout.setObjectName("verticalLayout")
self.hdrPreviousTrack = QtWidgets.QLabel(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred,
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.hdrPreviousTrack.sizePolicy().hasHeightForWidth())
sizePolicy.setHeightForWidth(
self.hdrPreviousTrack.sizePolicy().hasHeightForWidth()
)
self.hdrPreviousTrack.setSizePolicy(sizePolicy)
self.hdrPreviousTrack.setMinimumSize(QtCore.QSize(0, 0))
self.hdrPreviousTrack.setMaximumSize(QtCore.QSize(16777215, 16777215))
@ -89,32 +126,43 @@ class Ui_MainWindow(object):
font.setFamily("Sans")
font.setPointSize(20)
self.hdrPreviousTrack.setFont(font)
self.hdrPreviousTrack.setStyleSheet("background-color: #f8d7da;\n"
"border: 1px solid rgb(85, 87, 83);")
self.hdrPreviousTrack.setStyleSheet(
"background-color: #f8d7da;\n" "border: 1px solid rgb(85, 87, 83);"
)
self.hdrPreviousTrack.setText("")
self.hdrPreviousTrack.setWordWrap(False)
self.hdrPreviousTrack.setObjectName("hdrPreviousTrack")
self.verticalLayout.addWidget(self.hdrPreviousTrack)
self.hdrCurrentTrack = QtWidgets.QPushButton(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred,
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.hdrCurrentTrack.sizePolicy().hasHeightForWidth())
sizePolicy.setHeightForWidth(
self.hdrCurrentTrack.sizePolicy().hasHeightForWidth()
)
self.hdrCurrentTrack.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setPointSize(20)
self.hdrCurrentTrack.setFont(font)
self.hdrCurrentTrack.setStyleSheet("background-color: #d4edda;\n"
"border: 1px solid rgb(85, 87, 83);\n"
"text-align: left;\n"
"padding-left: 8px;\n"
"")
self.hdrCurrentTrack.setStyleSheet(
"background-color: #d4edda;\n"
"border: 1px solid rgb(85, 87, 83);\n"
"text-align: left;\n"
"padding-left: 8px;\n"
""
)
self.hdrCurrentTrack.setText("")
self.hdrCurrentTrack.setFlat(True)
self.hdrCurrentTrack.setObjectName("hdrCurrentTrack")
self.verticalLayout.addWidget(self.hdrCurrentTrack)
self.hdrNextTrack = QtWidgets.QPushButton(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred,
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.hdrNextTrack.sizePolicy().hasHeightForWidth())
@ -122,10 +170,12 @@ class Ui_MainWindow(object):
font = QtGui.QFont()
font.setPointSize(20)
self.hdrNextTrack.setFont(font)
self.hdrNextTrack.setStyleSheet("background-color: #fff3cd;\n"
"border: 1px solid rgb(85, 87, 83);\n"
"text-align: left;\n"
"padding-left: 8px;")
self.hdrNextTrack.setStyleSheet(
"background-color: #fff3cd;\n"
"border: 1px solid rgb(85, 87, 83);\n"
"text-align: left;\n"
"padding-left: 8px;"
)
self.hdrNextTrack.setText("")
self.hdrNextTrack.setFlat(True)
self.hdrNextTrack.setObjectName("hdrNextTrack")
@ -172,7 +222,12 @@ class Ui_MainWindow(object):
self.cartsWidget.setObjectName("cartsWidget")
self.horizontalLayout_Carts = QtWidgets.QHBoxLayout(self.cartsWidget)
self.horizontalLayout_Carts.setObjectName("horizontalLayout_Carts")
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
spacerItem = QtWidgets.QSpacerItem(
40,
20,
QtWidgets.QSizePolicy.Policy.Expanding,
QtWidgets.QSizePolicy.Policy.Minimum,
)
self.horizontalLayout_Carts.addItem(spacerItem)
self.gridLayout_4.addWidget(self.cartsWidget, 2, 0, 1, 1)
self.frame_6 = QtWidgets.QFrame(parent=self.centralwidget)
@ -217,7 +272,11 @@ class Ui_MainWindow(object):
self.btnPreview = QtWidgets.QPushButton(parent=self.FadeStopInfoFrame)
self.btnPreview.setMinimumSize(QtCore.QSize(132, 41))
icon1 = QtGui.QIcon()
icon1.addPixmap(QtGui.QPixmap(":/icons/headphones"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
icon1.addPixmap(
QtGui.QPixmap(":/icons/headphones"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.btnPreview.setIcon(icon1)
self.btnPreview.setIconSize(QtCore.QSize(30, 30))
self.btnPreview.setCheckable(True)
@ -239,8 +298,16 @@ class Ui_MainWindow(object):
self.btnPreviewArm.setMaximumSize(QtCore.QSize(44, 23))
self.btnPreviewArm.setText("")
icon2 = QtGui.QIcon()
icon2.addPixmap(QtGui.QPixmap(":/icons/record-button.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
icon2.addPixmap(QtGui.QPixmap(":/icons/record-red-button.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.On)
icon2.addPixmap(
QtGui.QPixmap(":/icons/record-button.png"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
icon2.addPixmap(
QtGui.QPixmap(":/icons/record-red-button.png"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.On,
)
self.btnPreviewArm.setIcon(icon2)
self.btnPreviewArm.setCheckable(True)
self.btnPreviewArm.setObjectName("btnPreviewArm")
@ -261,8 +328,16 @@ class Ui_MainWindow(object):
self.btnPreviewMark.setMaximumSize(QtCore.QSize(44, 23))
self.btnPreviewMark.setText("")
icon3 = QtGui.QIcon()
icon3.addPixmap(QtGui.QPixmap(":/icons/star.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.On)
icon3.addPixmap(QtGui.QPixmap(":/icons/star_empty.png"), QtGui.QIcon.Mode.Disabled, QtGui.QIcon.State.Off)
icon3.addPixmap(
QtGui.QPixmap(":/icons/star.png"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.On,
)
icon3.addPixmap(
QtGui.QPixmap(":/icons/star_empty.png"),
QtGui.QIcon.Mode.Disabled,
QtGui.QIcon.State.Off,
)
self.btnPreviewMark.setIcon(icon3)
self.btnPreviewMark.setObjectName("btnPreviewMark")
self.btnPreviewFwd = QtWidgets.QPushButton(parent=self.groupBoxIntroControls)
@ -363,10 +438,15 @@ class Ui_MainWindow(object):
self.verticalLayout_7.addWidget(self.label_silent_timer)
self.horizontalLayout.addWidget(self.frame_silent)
self.widgetFadeVolume = PlotWidget(parent=self.InfoFooterFrame)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred,
)
sizePolicy.setHorizontalStretch(1)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.widgetFadeVolume.sizePolicy().hasHeightForWidth())
sizePolicy.setHeightForWidth(
self.widgetFadeVolume.sizePolicy().hasHeightForWidth()
)
self.widgetFadeVolume.setSizePolicy(sizePolicy)
self.widgetFadeVolume.setMinimumSize(QtCore.QSize(0, 0))
self.widgetFadeVolume.setObjectName("widgetFadeVolume")
@ -383,7 +463,11 @@ class Ui_MainWindow(object):
self.btnFade.setMinimumSize(QtCore.QSize(132, 32))
self.btnFade.setMaximumSize(QtCore.QSize(164, 16777215))
icon4 = QtGui.QIcon()
icon4.addPixmap(QtGui.QPixmap(":/icons/fade"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
icon4.addPixmap(
QtGui.QPixmap(":/icons/fade"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.btnFade.setIcon(icon4)
self.btnFade.setIconSize(QtCore.QSize(30, 30))
self.btnFade.setObjectName("btnFade")
@ -391,7 +475,11 @@ class Ui_MainWindow(object):
self.btnStop = QtWidgets.QPushButton(parent=self.frame)
self.btnStop.setMinimumSize(QtCore.QSize(0, 36))
icon5 = QtGui.QIcon()
icon5.addPixmap(QtGui.QPixmap(":/icons/stopsign"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
icon5.addPixmap(
QtGui.QPixmap(":/icons/stopsign"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.btnStop.setIcon(icon5)
self.btnStop.setObjectName("btnStop")
self.verticalLayout_5.addWidget(self.btnStop)
@ -415,39 +503,71 @@ class Ui_MainWindow(object):
MainWindow.setStatusBar(self.statusbar)
self.actionPlay_next = QtGui.QAction(parent=MainWindow)
icon6 = QtGui.QIcon()
icon6.addPixmap(QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon-play.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
icon6.addPixmap(
QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon-play.png"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.actionPlay_next.setIcon(icon6)
self.actionPlay_next.setObjectName("actionPlay_next")
self.actionSkipToNext = QtGui.QAction(parent=MainWindow)
icon7 = QtGui.QIcon()
icon7.addPixmap(QtGui.QPixmap(":/icons/next"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
icon7.addPixmap(
QtGui.QPixmap(":/icons/next"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.actionSkipToNext.setIcon(icon7)
self.actionSkipToNext.setObjectName("actionSkipToNext")
self.actionInsertTrack = QtGui.QAction(parent=MainWindow)
icon8 = QtGui.QIcon()
icon8.addPixmap(QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon_search_database.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
icon8.addPixmap(
QtGui.QPixmap(
"app/ui/../../../../../../.designer/backup/icon_search_database.png"
),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.actionInsertTrack.setIcon(icon8)
self.actionInsertTrack.setObjectName("actionInsertTrack")
self.actionAdd_file = QtGui.QAction(parent=MainWindow)
icon9 = QtGui.QIcon()
icon9.addPixmap(QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon_open_file.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
icon9.addPixmap(
QtGui.QPixmap(
"app/ui/../../../../../../.designer/backup/icon_open_file.png"
),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.actionAdd_file.setIcon(icon9)
self.actionAdd_file.setObjectName("actionAdd_file")
self.actionFade = QtGui.QAction(parent=MainWindow)
icon10 = QtGui.QIcon()
icon10.addPixmap(QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon-fade.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
icon10.addPixmap(
QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon-fade.png"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.actionFade.setIcon(icon10)
self.actionFade.setObjectName("actionFade")
self.actionStop = QtGui.QAction(parent=MainWindow)
icon11 = QtGui.QIcon()
icon11.addPixmap(QtGui.QPixmap(":/icons/stop"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
icon11.addPixmap(
QtGui.QPixmap(":/icons/stop"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.actionStop.setIcon(icon11)
self.actionStop.setObjectName("actionStop")
self.action_Clear_selection = QtGui.QAction(parent=MainWindow)
self.action_Clear_selection.setObjectName("action_Clear_selection")
self.action_Resume_previous = QtGui.QAction(parent=MainWindow)
icon12 = QtGui.QIcon()
icon12.addPixmap(QtGui.QPixmap(":/icons/previous"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
icon12.addPixmap(
QtGui.QPixmap(":/icons/previous"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.action_Resume_previous.setIcon(icon12)
self.action_Resume_previous.setObjectName("action_Resume_previous")
self.actionE_xit = QtGui.QAction(parent=MainWindow)
@ -494,7 +614,9 @@ class Ui_MainWindow(object):
self.actionImport = QtGui.QAction(parent=MainWindow)
self.actionImport.setObjectName("actionImport")
self.actionDownload_CSV_of_played_tracks = QtGui.QAction(parent=MainWindow)
self.actionDownload_CSV_of_played_tracks.setObjectName("actionDownload_CSV_of_played_tracks")
self.actionDownload_CSV_of_played_tracks.setObjectName(
"actionDownload_CSV_of_played_tracks"
)
self.actionSearch = QtGui.QAction(parent=MainWindow)
self.actionSearch.setObjectName("actionSearch")
self.actionInsertSectionHeader = QtGui.QAction(parent=MainWindow)
@ -522,9 +644,13 @@ class Ui_MainWindow(object):
self.actionResume = QtGui.QAction(parent=MainWindow)
self.actionResume.setObjectName("actionResume")
self.actionSearch_title_in_Wikipedia = QtGui.QAction(parent=MainWindow)
self.actionSearch_title_in_Wikipedia.setObjectName("actionSearch_title_in_Wikipedia")
self.actionSearch_title_in_Wikipedia.setObjectName(
"actionSearch_title_in_Wikipedia"
)
self.actionSearch_title_in_Songfacts = QtGui.QAction(parent=MainWindow)
self.actionSearch_title_in_Songfacts.setObjectName("actionSearch_title_in_Songfacts")
self.actionSearch_title_in_Songfacts.setObjectName(
"actionSearch_title_in_Songfacts"
)
self.actionSelect_duplicate_rows = QtGui.QAction(parent=MainWindow)
self.actionSelect_duplicate_rows.setObjectName("actionSelect_duplicate_rows")
self.actionReplace_files = QtGui.QAction(parent=MainWindow)
@ -578,7 +704,7 @@ class Ui_MainWindow(object):
self.retranslateUi(MainWindow)
self.tabPlaylist.setCurrentIndex(-1)
self.tabInfolist.setCurrentIndex(-1)
self.actionE_xit.triggered.connect(MainWindow.close) # type: ignore
self.actionE_xit.triggered.connect(MainWindow.close) # type: ignore
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
@ -619,38 +745,58 @@ class Ui_MainWindow(object):
self.actionFade.setShortcut(_translate("MainWindow", "Ctrl+Z"))
self.actionStop.setText(_translate("MainWindow", "S&top"))
self.actionStop.setShortcut(_translate("MainWindow", "Ctrl+Alt+S"))
self.action_Clear_selection.setText(_translate("MainWindow", "Clear &selection"))
self.action_Clear_selection.setText(
_translate("MainWindow", "Clear &selection")
)
self.action_Clear_selection.setShortcut(_translate("MainWindow", "Esc"))
self.action_Resume_previous.setText(_translate("MainWindow", "&Resume previous"))
self.action_Resume_previous.setText(
_translate("MainWindow", "&Resume previous")
)
self.actionE_xit.setText(_translate("MainWindow", "E&xit"))
self.actionTest.setText(_translate("MainWindow", "&Test"))
self.actionOpenPlaylist.setText(_translate("MainWindow", "O&pen..."))
self.actionNewPlaylist.setText(_translate("MainWindow", "&New..."))
self.actionTestFunction.setText(_translate("MainWindow", "&Test function"))
self.actionSkipToFade.setText(_translate("MainWindow", "&Skip to start of fade"))
self.actionSkipToFade.setText(
_translate("MainWindow", "&Skip to start of fade")
)
self.actionSkipToEnd.setText(_translate("MainWindow", "Skip to &end of track"))
self.actionClosePlaylist.setText(_translate("MainWindow", "&Close"))
self.actionRenamePlaylist.setText(_translate("MainWindow", "&Rename..."))
self.actionDeletePlaylist.setText(_translate("MainWindow", "Dele&te..."))
self.actionMoveSelected.setText(_translate("MainWindow", "Mo&ve selected tracks to..."))
self.actionMoveSelected.setText(
_translate("MainWindow", "Mo&ve selected tracks to...")
)
self.actionExport_playlist.setText(_translate("MainWindow", "E&xport..."))
self.actionSetNext.setText(_translate("MainWindow", "Set &next"))
self.actionSetNext.setShortcut(_translate("MainWindow", "Ctrl+N"))
self.actionSelect_next_track.setText(_translate("MainWindow", "Select next track"))
self.actionSelect_next_track.setText(
_translate("MainWindow", "Select next track")
)
self.actionSelect_next_track.setShortcut(_translate("MainWindow", "J"))
self.actionSelect_previous_track.setText(_translate("MainWindow", "Select previous track"))
self.actionSelect_previous_track.setText(
_translate("MainWindow", "Select previous track")
)
self.actionSelect_previous_track.setShortcut(_translate("MainWindow", "K"))
self.actionSelect_played_tracks.setText(_translate("MainWindow", "Select played tracks"))
self.actionMoveUnplayed.setText(_translate("MainWindow", "Move &unplayed tracks to..."))
self.actionSelect_played_tracks.setText(
_translate("MainWindow", "Select played tracks")
)
self.actionMoveUnplayed.setText(
_translate("MainWindow", "Move &unplayed tracks to...")
)
self.actionAdd_note.setText(_translate("MainWindow", "Add note..."))
self.actionAdd_note.setShortcut(_translate("MainWindow", "Ctrl+T"))
self.actionEnable_controls.setText(_translate("MainWindow", "Enable controls"))
self.actionImport.setText(_translate("MainWindow", "Import track..."))
self.actionImport.setShortcut(_translate("MainWindow", "Ctrl+Shift+I"))
self.actionDownload_CSV_of_played_tracks.setText(_translate("MainWindow", "Download CSV of played tracks..."))
self.actionDownload_CSV_of_played_tracks.setText(
_translate("MainWindow", "Download CSV of played tracks...")
)
self.actionSearch.setText(_translate("MainWindow", "Search..."))
self.actionSearch.setShortcut(_translate("MainWindow", "/"))
self.actionInsertSectionHeader.setText(_translate("MainWindow", "Insert &section header..."))
self.actionInsertSectionHeader.setText(
_translate("MainWindow", "Insert &section header...")
)
self.actionInsertSectionHeader.setShortcut(_translate("MainWindow", "Ctrl+H"))
self.actionRemove.setText(_translate("MainWindow", "&Remove track"))
self.actionFind_next.setText(_translate("MainWindow", "Find next"))
@ -658,8 +804,12 @@ class Ui_MainWindow(object):
self.actionFind_previous.setText(_translate("MainWindow", "Find previous"))
self.actionFind_previous.setShortcut(_translate("MainWindow", "P"))
self.action_About.setText(_translate("MainWindow", "&About"))
self.actionSave_as_template.setText(_translate("MainWindow", "Save as template..."))
self.actionNew_from_template.setText(_translate("MainWindow", "New from template..."))
self.actionSave_as_template.setText(
_translate("MainWindow", "Save as template...")
)
self.actionNew_from_template.setText(
_translate("MainWindow", "New from template...")
)
self.actionDebug.setText(_translate("MainWindow", "Debug"))
self.actionAdd_cart.setText(_translate("MainWindow", "Edit cart &1..."))
self.actionMark_for_moving.setText(_translate("MainWindow", "Mark for moving"))
@ -668,11 +818,23 @@ class Ui_MainWindow(object):
self.actionPaste.setShortcut(_translate("MainWindow", "Ctrl+V"))
self.actionResume.setText(_translate("MainWindow", "Resume"))
self.actionResume.setShortcut(_translate("MainWindow", "Ctrl+R"))
self.actionSearch_title_in_Wikipedia.setText(_translate("MainWindow", "Search title in Wikipedia"))
self.actionSearch_title_in_Wikipedia.setShortcut(_translate("MainWindow", "Ctrl+W"))
self.actionSearch_title_in_Songfacts.setText(_translate("MainWindow", "Search title in Songfacts"))
self.actionSearch_title_in_Songfacts.setShortcut(_translate("MainWindow", "Ctrl+S"))
self.actionSelect_duplicate_rows.setText(_translate("MainWindow", "Select duplicate rows..."))
self.actionSearch_title_in_Wikipedia.setText(
_translate("MainWindow", "Search title in Wikipedia")
)
self.actionSearch_title_in_Wikipedia.setShortcut(
_translate("MainWindow", "Ctrl+W")
)
self.actionSearch_title_in_Songfacts.setText(
_translate("MainWindow", "Search title in Songfacts")
)
self.actionSearch_title_in_Songfacts.setShortcut(
_translate("MainWindow", "Ctrl+S")
)
self.actionSelect_duplicate_rows.setText(
_translate("MainWindow", "Select duplicate rows...")
)
self.actionReplace_files.setText(_translate("MainWindow", "Import files..."))
from infotabs import InfoTabs
from pyqtgraph import PlotWidget
from infotabs import InfoTabs # type: ignore
from pyqtgraph import PlotWidget # type: ignore