diff --git a/app/classes.py b/app/classes.py index 844efcb..ffaae87 100644 --- a/app/classes.py +++ b/app/classes.py @@ -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 diff --git a/app/config.py b/app/config.py index 29f59ba..11c2f16 100644 --- a/app/config.py +++ b/app/config.py @@ -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" diff --git a/app/dialogs.py b/app/dialogs.py index b0fadf9..e505218 100644 --- a/app/dialogs.py +++ b/app/dialogs.py @@ -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 diff --git a/app/file_importer.py b/app/file_importer.py index 92a7854..1a584d6 100644 --- a/app/file_importer.py +++ b/app/file_importer.py @@ -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 """ diff --git a/app/musicmuster.py b/app/musicmuster.py index 76b3f1a..5ecec1e 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -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 diff --git a/app/playlistmodel.py b/app/playlistmodel.py index cd16907..4e386d5 100644 --- a/app/playlistmodel.py +++ b/app/playlistmodel.py @@ -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"" + return f"" 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()) diff --git a/app/playlists.py b/app/playlists.py index bb207f5..c15d7eb 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -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""" diff --git a/app/ui/main_window_ui.py b/app/ui/main_window_ui.py index 14beafc..2427ed9 100644 --- a/app/ui/main_window_ui.py +++ b/app/ui/main_window_ui.py @@ -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 §ion header...")) + self.actionInsertSectionHeader.setText( + _translate("MainWindow", "Insert §ion 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