Compare commits

..

4 Commits

Author SHA1 Message Date
Keith Edmunds
3fde474a5b Save proxy model example in archive 2024-12-26 14:10:26 +00:00
Keith Edmunds
b14b90396f Major update: correct use of proxy model
Fixes #273
2024-12-26 14:09:21 +00:00
Keith Edmunds
937f3cd074 Fix search
Fixed #272
2024-12-23 21:20:59 +00:00
Keith Edmunds
cb16a07451 Menu reorganised. Other minor cleanups. 2024-12-23 19:19:01 +00:00
10 changed files with 478 additions and 409 deletions

View File

@ -1,36 +1,20 @@
# Standard library imports
from __future__ import annotations
import ctypes
from dataclasses import dataclass
import datetime as dt
from dataclasses import dataclass, field
from enum import auto, Enum
import functools
import platform
from time import sleep
from typing import Optional, NamedTuple
from typing import NamedTuple
# Third party imports
import numpy as np
import pyqtgraph as pg # type: ignore
from sqlalchemy.orm.session import Session
import vlc # type: ignore
# PyQt imports
from PyQt6.QtCore import (
pyqtSignal,
QObject,
QThread,
)
from pyqtgraph import PlotWidget
from pyqtgraph.graphicsItems.PlotDataItem import PlotDataItem # type: ignore
from pyqtgraph.graphicsItems.LinearRegionItem import LinearRegionItem # type: ignore
# App imports
from config import Config
from log import log
from models import PlaylistRows
from vlcmanager import VLCManager
class Col(Enum):
@ -109,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.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
@ -204,10 +273,8 @@ class Window(QMainWindow, Ui_MainWindow):
self.widgetFadeVolume.setDefaultPadding(0)
self.widgetFadeVolume.setBackground(Config.FADE_CURVE_BACKGROUND)
self.active_tab = lambda: self.tabPlaylist.currentWidget()
self.active_proxy_model = lambda: self.tabPlaylist.currentWidget().model()
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
@ -219,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(
@ -255,6 +324,12 @@ class Window(QMainWindow, Ui_MainWindow):
QMessageBox.StandardButton.Ok,
)
def active_tab(self) -> PlaylistTab:
return self.tabPlaylist.currentWidget()
def active_proxy_model(self) -> PlaylistProxyModel:
return self.tabPlaylist.currentWidget().model()
def clear_next(self) -> None:
"""
Clear next track
@ -269,6 +344,7 @@ class Window(QMainWindow, Ui_MainWindow):
# Unselect any selected rows
if self.active_tab():
self.active_tab().clear_selection()
# Clear the search bar
self.search_playlist_clear()
@ -368,8 +444,8 @@ class Window(QMainWindow, Ui_MainWindow):
def connect_signals_slots(self) -> None:
self.action_About.triggered.connect(self.about)
self.action_Clear_selection.triggered.connect(self.clear_selection)
self.actionDebug.triggered.connect(self.debug)
self.actionClosePlaylist.triggered.connect(self.close_playlist_tab)
self.actionDebug.triggered.connect(self.debug)
self.actionDeletePlaylist.triggered.connect(self.delete_playlist)
self.actionDownload_CSV_of_played_tracks.triggered.connect(
self.download_played_tracks
@ -380,6 +456,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.actionInsertTrack.triggered.connect(self.insert_track)
self.actionMark_for_moving.triggered.connect(self.mark_rows_for_moving)
self.actionMoveSelected.triggered.connect(self.move_selected)
self.actionMoveUnplayed.triggered.connect(self.move_unplayed)
self.actionNew_from_template.triggered.connect(self.new_from_template)
self.actionNewPlaylist.triggered.connect(self.create_and_show_playlist)
self.actionOpenPlaylist.triggered.connect(self.open_playlist)
@ -399,10 +476,10 @@ class Window(QMainWindow, Ui_MainWindow):
self.actionSelect_duplicate_rows.triggered.connect(
lambda: self.active_tab().select_duplicate_rows()
)
self.actionMoveUnplayed.triggered.connect(self.move_unplayed)
self.actionSetNext.triggered.connect(self.set_selected_track_next)
self.actionSkipToNext.triggered.connect(self.play_next)
self.actionStop.triggered.connect(self.stop)
self.btnDrop3db.clicked.connect(self.drop3db)
self.btnFade.clicked.connect(self.fade)
self.btnHidePlayed.clicked.connect(self.hide_played)
@ -467,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)
@ -642,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"""
@ -667,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()
@ -675,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)
@ -687,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(),
)
@ -704,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()
@ -716,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:
@ -761,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=}"
@ -798,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
)
@ -828,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
@ -911,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]
@ -928,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(
@ -1058,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:
@ -1090,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:
@ -1100,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"""
@ -1288,22 +1374,12 @@ 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
return track_info
def select_next_row(self) -> None:
"""Select next or first row in playlist"""
self.active_tab().select_next_row()
def select_previous_row(self) -> None:
"""Select previous or first row in playlist"""
self.active_tab().select_previous_row()
def set_main_window_size(self) -> None:
"""Set size of window from database"""
@ -1389,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()
)
@ -1431,10 +1505,10 @@ class Window(QMainWindow, Ui_MainWindow):
if track_sequence.current:
track_sequence.current.stop()
def tab_change(self):
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:
"""
@ -1622,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:
"""
@ -1682,15 +1678,15 @@ class PlaylistProxyModel(QSortFilterProxyModel):
"""
if Config.HIDE_PLAYED_MODE != Config.HIDE_PLAYED_MODE_TRACKS:
return True
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:
proxy_index = to_index
to_model_row = self.proxy_model.mapToSource(proxy_index).row()
# Handle edge case where next_row is beyond the last row
destination_index = to_index
else:
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

@ -967,69 +967,64 @@ padding-left: 8px;</string>
</property>
<widget class="QMenu" name="menuFile">
<property name="title">
<string>&amp;Playlists</string>
<string>&amp;Playlist</string>
</property>
<addaction name="actionNewPlaylist"/>
<addaction name="actionNew_from_template"/>
<addaction name="separator"/>
<addaction name="actionInsertTrack"/>
<addaction name="actionRemove"/>
<addaction name="actionInsertSectionHeader"/>
<addaction name="separator"/>
<addaction name="actionMark_for_moving"/>
<addaction name="actionPaste"/>
<addaction name="separator"/>
<addaction name="actionExport_playlist"/>
<addaction name="actionDownload_CSV_of_played_tracks"/>
<addaction name="separator"/>
<addaction name="actionSelect_duplicate_rows"/>
<addaction name="actionMoveSelected"/>
<addaction name="actionMoveUnplayed"/>
<addaction name="action_Clear_selection"/>
</widget>
<widget class="QMenu" name="menuPlaylist">
<property name="title">
<string>&amp;File</string>
</property>
<addaction name="separator"/>
<addaction name="separator"/>
<addaction name="actionOpenPlaylist"/>
<addaction name="actionNewPlaylist"/>
<addaction name="actionClosePlaylist"/>
<addaction name="actionRenamePlaylist"/>
<addaction name="actionDeletePlaylist"/>
<addaction name="actionExport_playlist"/>
<addaction name="separator"/>
<addaction name="actionSelect_duplicate_rows"/>
<addaction name="separator"/>
<addaction name="actionMoveSelected"/>
<addaction name="actionMoveUnplayed"/>
<addaction name="actionDownload_CSV_of_played_tracks"/>
<addaction name="actionNew_from_template"/>
<addaction name="actionSave_as_template"/>
<addaction name="separator"/>
<addaction name="actionReplace_files"/>
<addaction name="separator"/>
<addaction name="actionDebug"/>
<addaction name="action_About"/>
<addaction name="separator"/>
<addaction name="actionE_xit"/>
</widget>
<widget class="QMenu" name="menuPlaylist">
<widget class="QMenu" name="menuSearc_h">
<property name="title">
<string>Sho&amp;wtime</string>
<string>&amp;Music</string>
</property>
<addaction name="separator"/>
<addaction name="actionSetNext"/>
<addaction name="actionPlay_next"/>
<addaction name="actionFade"/>
<addaction name="actionStop"/>
<addaction name="actionResume"/>
<addaction name="separator"/>
<addaction name="actionSkipToNext"/>
<addaction name="separator"/>
<addaction name="actionInsertSectionHeader"/>
<addaction name="actionInsertTrack"/>
<addaction name="actionRemove"/>
<addaction name="separator"/>
<addaction name="actionSetNext"/>
<addaction name="action_Clear_selection"/>
<addaction name="separator"/>
<addaction name="actionMark_for_moving"/>
<addaction name="actionPaste"/>
</widget>
<widget class="QMenu" name="menuSearc_h">
<property name="title">
<string>&amp;Search</string>
</property>
<addaction name="actionSearch"/>
<addaction name="separator"/>
<addaction name="actionSearch_title_in_Wikipedia"/>
<addaction name="actionSearch_title_in_Songfacts"/>
</widget>
<widget class="QMenu" name="menuHelp">
<property name="title">
<string>&amp;Help</string>
</property>
<addaction name="action_About"/>
<addaction name="actionDebug"/>
</widget>
<addaction name="menuFile"/>
<addaction name="menuPlaylist"/>
<addaction name="menuFile"/>
<addaction name="menuSearc_h"/>
<addaction name="menuHelp"/>
</widget>
<widget class="QStatusBar" name="statusbar">
<property name="enabled">

View File

@ -495,8 +495,6 @@ class Ui_MainWindow(object):
self.menuPlaylist.setObjectName("menuPlaylist")
self.menuSearc_h = QtWidgets.QMenu(parent=self.menubar)
self.menuSearc_h.setObjectName("menuSearc_h")
self.menuHelp = QtWidgets.QMenu(parent=self.menubar)
self.menuHelp.setObjectName("menuHelp")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(parent=MainWindow)
self.statusbar.setEnabled(True)
@ -657,51 +655,51 @@ class Ui_MainWindow(object):
self.actionSelect_duplicate_rows.setObjectName("actionSelect_duplicate_rows")
self.actionReplace_files = QtGui.QAction(parent=MainWindow)
self.actionReplace_files.setObjectName("actionReplace_files")
self.menuFile.addAction(self.actionNewPlaylist)
self.menuFile.addAction(self.actionNew_from_template)
self.menuFile.addAction(self.actionOpenPlaylist)
self.menuFile.addAction(self.actionClosePlaylist)
self.menuFile.addAction(self.actionRenamePlaylist)
self.menuFile.addAction(self.actionDeletePlaylist)
self.menuFile.addSeparator()
self.menuFile.addAction(self.actionInsertTrack)
self.menuFile.addAction(self.actionRemove)
self.menuFile.addAction(self.actionInsertSectionHeader)
self.menuFile.addSeparator()
self.menuFile.addAction(self.actionMark_for_moving)
self.menuFile.addAction(self.actionPaste)
self.menuFile.addSeparator()
self.menuFile.addAction(self.actionExport_playlist)
self.menuFile.addAction(self.actionDownload_CSV_of_played_tracks)
self.menuFile.addSeparator()
self.menuFile.addAction(self.actionSelect_duplicate_rows)
self.menuFile.addSeparator()
self.menuFile.addAction(self.actionMoveSelected)
self.menuFile.addAction(self.actionMoveUnplayed)
self.menuFile.addAction(self.actionDownload_CSV_of_played_tracks)
self.menuFile.addAction(self.actionSave_as_template)
self.menuFile.addSeparator()
self.menuFile.addAction(self.actionReplace_files)
self.menuFile.addSeparator()
self.menuFile.addAction(self.actionE_xit)
self.menuFile.addAction(self.action_Clear_selection)
self.menuPlaylist.addSeparator()
self.menuPlaylist.addAction(self.actionPlay_next)
self.menuPlaylist.addAction(self.actionFade)
self.menuPlaylist.addAction(self.actionStop)
self.menuPlaylist.addAction(self.actionResume)
self.menuPlaylist.addSeparator()
self.menuPlaylist.addAction(self.actionSkipToNext)
self.menuPlaylist.addAction(self.actionOpenPlaylist)
self.menuPlaylist.addAction(self.actionNewPlaylist)
self.menuPlaylist.addAction(self.actionClosePlaylist)
self.menuPlaylist.addAction(self.actionRenamePlaylist)
self.menuPlaylist.addAction(self.actionDeletePlaylist)
self.menuPlaylist.addSeparator()
self.menuPlaylist.addAction(self.actionInsertSectionHeader)
self.menuPlaylist.addAction(self.actionInsertTrack)
self.menuPlaylist.addAction(self.actionRemove)
self.menuPlaylist.addAction(self.actionNew_from_template)
self.menuPlaylist.addAction(self.actionSave_as_template)
self.menuPlaylist.addSeparator()
self.menuPlaylist.addAction(self.actionSetNext)
self.menuPlaylist.addAction(self.action_Clear_selection)
self.menuPlaylist.addAction(self.actionReplace_files)
self.menuPlaylist.addSeparator()
self.menuPlaylist.addAction(self.actionMark_for_moving)
self.menuPlaylist.addAction(self.actionPaste)
self.menuSearc_h.addAction(self.actionSearch)
self.menuPlaylist.addAction(self.actionDebug)
self.menuPlaylist.addAction(self.action_About)
self.menuPlaylist.addSeparator()
self.menuPlaylist.addAction(self.actionE_xit)
self.menuSearc_h.addAction(self.actionSetNext)
self.menuSearc_h.addAction(self.actionPlay_next)
self.menuSearc_h.addAction(self.actionFade)
self.menuSearc_h.addAction(self.actionStop)
self.menuSearc_h.addAction(self.actionResume)
self.menuSearc_h.addAction(self.actionSkipToNext)
self.menuSearc_h.addSeparator()
self.menuSearc_h.addAction(self.actionSearch)
self.menuSearc_h.addAction(self.actionSearch_title_in_Wikipedia)
self.menuSearc_h.addAction(self.actionSearch_title_in_Songfacts)
self.menuHelp.addAction(self.action_About)
self.menuHelp.addAction(self.actionDebug)
self.menubar.addAction(self.menuFile.menuAction())
self.menubar.addAction(self.menuPlaylist.menuAction())
self.menubar.addAction(self.menuFile.menuAction())
self.menubar.addAction(self.menuSearc_h.menuAction())
self.menubar.addAction(self.menuHelp.menuAction())
self.retranslateUi(MainWindow)
self.tabPlaylist.setCurrentIndex(-1)
@ -732,10 +730,9 @@ class Ui_MainWindow(object):
self.label_silent_timer.setText(_translate("MainWindow", "00:00"))
self.btnFade.setText(_translate("MainWindow", " Fade"))
self.btnStop.setText(_translate("MainWindow", " Stop"))
self.menuFile.setTitle(_translate("MainWindow", "&Playlists"))
self.menuPlaylist.setTitle(_translate("MainWindow", "Sho&wtime"))
self.menuSearc_h.setTitle(_translate("MainWindow", "&Search"))
self.menuHelp.setTitle(_translate("MainWindow", "&Help"))
self.menuFile.setTitle(_translate("MainWindow", "&Playlist"))
self.menuPlaylist.setTitle(_translate("MainWindow", "&File"))
self.menuSearc_h.setTitle(_translate("MainWindow", "&Music"))
self.actionPlay_next.setText(_translate("MainWindow", "&Play next"))
self.actionPlay_next.setShortcut(_translate("MainWindow", "Return"))
self.actionSkipToNext.setText(_translate("MainWindow", "Skip to &next"))

125
archive/proxymodel.py Executable file
View File

@ -0,0 +1,125 @@
#!/usr/bin/env python
import sys
from PyQt6.QtCore import (Qt, QAbstractTableModel, QModelIndex, QSortFilterProxyModel)
from PyQt6.QtWidgets import (QApplication, QMainWindow, QTableView, QLineEdit, QVBoxLayout, QWidget)
class CustomTableModel(QAbstractTableModel):
def __init__(self, data):
super().__init__()
self._data = data
def rowCount(self, parent=QModelIndex()):
return len(self._data)
def columnCount(self, parent=QModelIndex()):
return 2 # Row number and data
def data(self, index, role=Qt.ItemDataRole.DisplayRole):
if role == Qt.ItemDataRole.DisplayRole:
row, col = index.row(), index.column()
if col == 0:
return row + 1 # Row number (1-based index)
elif col == 1:
return self._data[row]
def setData(self, index, value, role=Qt.ItemDataRole.EditRole):
if role == Qt.ItemDataRole.EditRole and index.isValid():
self._data[index.row()] = value
self.dataChanged.emit(index, index, [Qt.ItemDataRole.EditRole])
return True
return False
def flags(self, index):
default_flags = Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled
if index.isValid():
return default_flags | Qt.ItemFlag.ItemIsDragEnabled | Qt.ItemFlag.ItemIsDropEnabled
return default_flags | Qt.ItemFlag.ItemIsDropEnabled
def removeRow(self, row):
self.beginRemoveRows(QModelIndex(), row, row)
self._data.pop(row)
self.endRemoveRows()
def insertRow(self, row, value):
self.beginInsertRows(QModelIndex(), row, row)
self._data.insert(row, value)
self.endInsertRows()
def moveRows(self, sourceParent, sourceRow, count, destinationParent, destinationRow):
if sourceRow < destinationRow:
destinationRow -= 1
self.beginMoveRows(sourceParent, sourceRow, sourceRow, destinationParent, destinationRow)
row_data = self._data.pop(sourceRow)
self._data.insert(destinationRow, row_data)
self.endMoveRows()
return True
class ProxyModel(QSortFilterProxyModel):
def __init__(self):
super().__init__()
self.filterString = ""
def setFilterString(self, text):
self.filterString = text
self.invalidateFilter()
def filterAcceptsRow(self, source_row, source_parent):
if self.filterString:
data = self.sourceModel().data(self.sourceModel().index(source_row, 1), Qt.ItemDataRole.DisplayRole)
return self.filterString in str(data)
return True
class TableView(QTableView):
def __init__(self, model):
super().__init__()
self.setModel(model)
self.setDragDropMode(QTableView.DragDropMode.InternalMove)
self.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
self.setSortingEnabled(False)
self.setDragDropOverwriteMode(False)
def dropEvent(self, event):
source_index = self.indexAt(event.pos())
if not source_index.isValid():
return
destination_row = source_index.row()
dragged_row = self.currentIndex().row()
if dragged_row != destination_row:
self.model().sourceModel().moveRows(QModelIndex(), dragged_row, 1, QModelIndex(), destination_row)
super().dropEvent(event)
self.model().layoutChanged.emit() # Refresh model to update row numbers
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.data = ["dog", "hog", "don", "cat", "bat"]
self.baseModel = CustomTableModel(self.data)
self.proxyModel = ProxyModel()
self.proxyModel.setSourceModel(self.baseModel)
self.view = TableView(self.proxyModel)
self.filterLineEdit = QLineEdit()
self.filterLineEdit.setPlaceholderText("Filter by substring")
self.filterLineEdit.textChanged.connect(self.proxyModel.setFilterString)
layout = QVBoxLayout()
layout.addWidget(self.filterLineEdit)
layout.addWidget(self.view)
container = QWidget()
container.setLayout(layout)
self.setCentralWidget(container)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())