Compare commits
4 Commits
6da6f7044b
...
3fde474a5b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fde474a5b | ||
|
|
b14b90396f | ||
|
|
937f3cd074 | ||
|
|
cb16a07451 |
@ -1,36 +1,20 @@
|
|||||||
# Standard library imports
|
# Standard library imports
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import ctypes
|
from dataclasses import dataclass, field
|
||||||
from dataclasses import dataclass
|
|
||||||
import datetime as dt
|
|
||||||
from enum import auto, Enum
|
from enum import auto, Enum
|
||||||
import functools
|
import functools
|
||||||
import platform
|
from typing import NamedTuple
|
||||||
from time import sleep
|
|
||||||
from typing import Optional, NamedTuple
|
|
||||||
|
|
||||||
# Third party imports
|
# 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
|
# PyQt imports
|
||||||
from PyQt6.QtCore import (
|
from PyQt6.QtCore import (
|
||||||
pyqtSignal,
|
pyqtSignal,
|
||||||
QObject,
|
QObject,
|
||||||
QThread,
|
|
||||||
)
|
)
|
||||||
from pyqtgraph import PlotWidget
|
|
||||||
from pyqtgraph.graphicsItems.PlotDataItem import PlotDataItem # type: ignore
|
|
||||||
from pyqtgraph.graphicsItems.LinearRegionItem import LinearRegionItem # type: ignore
|
|
||||||
|
|
||||||
# App imports
|
# App imports
|
||||||
from config import Config
|
|
||||||
from log import log
|
|
||||||
from models import PlaylistRows
|
|
||||||
from vlcmanager import VLCManager
|
|
||||||
|
|
||||||
|
|
||||||
class Col(Enum):
|
class Col(Enum):
|
||||||
@ -109,6 +93,13 @@ class MusicMusterSignals(QObject):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
|
|
||||||
|
@singleton
|
||||||
|
@dataclass
|
||||||
|
class Selection:
|
||||||
|
playlist_id: int = 0
|
||||||
|
rows: list[int] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
class Tags(NamedTuple):
|
class Tags(NamedTuple):
|
||||||
artist: str
|
artist: str
|
||||||
title: str
|
title: str
|
||||||
|
|||||||
@ -106,6 +106,7 @@ class Config(object):
|
|||||||
SECTION_STARTS = ("+", "+-", "-+")
|
SECTION_STARTS = ("+", "+-", "-+")
|
||||||
SONGFACTS_ON_NEXT = False
|
SONGFACTS_ON_NEXT = False
|
||||||
START_GAP_WARNING_THRESHOLD = 300
|
START_GAP_WARNING_THRESHOLD = 300
|
||||||
|
SUBTOTAL_ON_ROW_ZERO = "[No subtotal on first row]"
|
||||||
TEXT_NO_TRACK_NO_NOTE = "[Section header]"
|
TEXT_NO_TRACK_NO_NOTE = "[Section header]"
|
||||||
TOD_TIME_FORMAT = "%H:%M:%S"
|
TOD_TIME_FORMAT = "%H:%M:%S"
|
||||||
TRACK_TIME_FORMAT = "%H:%M:%S"
|
TRACK_TIME_FORMAT = "%H:%M:%S"
|
||||||
|
|||||||
@ -40,7 +40,7 @@ class TrackSelectDialog(QDialog):
|
|||||||
parent: QMainWindow,
|
parent: QMainWindow,
|
||||||
session: Session,
|
session: Session,
|
||||||
new_row_number: int,
|
new_row_number: int,
|
||||||
source_model: PlaylistModel,
|
base_model: PlaylistModel,
|
||||||
add_to_header: Optional[bool] = False,
|
add_to_header: Optional[bool] = False,
|
||||||
*args: Qt.WindowType,
|
*args: Qt.WindowType,
|
||||||
**kwargs: Qt.WindowType,
|
**kwargs: Qt.WindowType,
|
||||||
@ -52,7 +52,7 @@ class TrackSelectDialog(QDialog):
|
|||||||
super().__init__(parent, *args, **kwargs)
|
super().__init__(parent, *args, **kwargs)
|
||||||
self.session = session
|
self.session = session
|
||||||
self.new_row_number = new_row_number
|
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.add_to_header = add_to_header
|
||||||
self.ui = dlg_TrackSelect_ui.Ui_Dialog()
|
self.ui = dlg_TrackSelect_ui.Ui_Dialog()
|
||||||
self.ui.setupUi(self)
|
self.ui.setupUi(self)
|
||||||
@ -96,7 +96,7 @@ class TrackSelectDialog(QDialog):
|
|||||||
track_id = track.id
|
track_id = track.id
|
||||||
|
|
||||||
if note and not 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.ui.txtNote.clear()
|
||||||
self.new_row_number += 1
|
self.new_row_number += 1
|
||||||
return
|
return
|
||||||
@ -110,7 +110,7 @@ class TrackSelectDialog(QDialog):
|
|||||||
|
|
||||||
# Check whether track is already in playlist
|
# Check whether track is already in playlist
|
||||||
move_existing = False
|
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 existing_prd is not None:
|
||||||
if ask_yes_no(
|
if ask_yes_no(
|
||||||
"Duplicate row",
|
"Duplicate row",
|
||||||
@ -121,21 +121,21 @@ class TrackSelectDialog(QDialog):
|
|||||||
|
|
||||||
if self.add_to_header:
|
if self.add_to_header:
|
||||||
if move_existing and existing_prd: # "and existing_prd" for mypy's benefit
|
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
|
self.new_row_number, existing_prd, note
|
||||||
)
|
)
|
||||||
else:
|
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
|
# Close dialog - we can only add one track to a header
|
||||||
self.accept()
|
self.accept()
|
||||||
else:
|
else:
|
||||||
# Adding a new track row
|
# Adding a new track row
|
||||||
if move_existing and existing_prd: # "and existing_prd" for mypy's benefit
|
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
|
self.new_row_number, existing_prd, note
|
||||||
)
|
)
|
||||||
else:
|
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
|
self.new_row_number += 1
|
||||||
|
|
||||||
|
|||||||
@ -58,7 +58,7 @@ class DoTrackImport(QObject):
|
|||||||
destination_track_path: str,
|
destination_track_path: str,
|
||||||
track_id: int,
|
track_id: int,
|
||||||
audio_metadata: AudioMetadata,
|
audio_metadata: AudioMetadata,
|
||||||
source_model: PlaylistModel,
|
base_model: PlaylistModel,
|
||||||
row_number: Optional[int],
|
row_number: Optional[int],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
@ -72,10 +72,10 @@ class DoTrackImport(QObject):
|
|||||||
self.destination_track_path = destination_track_path
|
self.destination_track_path = destination_track_path
|
||||||
self.track_id = track_id
|
self.track_id = track_id
|
||||||
self.audio_metadata = audio_metadata
|
self.audio_metadata = audio_metadata
|
||||||
self.source_model = source_model
|
self.base_model = base_model
|
||||||
|
|
||||||
if row_number is None:
|
if row_number is None:
|
||||||
self.next_row_number = source_model.rowCount()
|
self.next_row_number = base_model.rowCount()
|
||||||
else:
|
else:
|
||||||
self.next_row_number = row_number
|
self.next_row_number = row_number
|
||||||
|
|
||||||
@ -130,7 +130,7 @@ class DoTrackImport(QObject):
|
|||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
helpers.normalise_track(self.destination_track_path)
|
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.next_row_number += 1
|
||||||
|
|
||||||
self.signals.status_message_signal.emit(
|
self.signals.status_message_signal.emit(
|
||||||
@ -144,14 +144,19 @@ class FileImporter:
|
|||||||
Manage importing of files
|
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
|
Set up class
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Save parameters
|
# Save parameters
|
||||||
self.active_proxy_model = active_proxy_model
|
self.base_model = base_model
|
||||||
|
if row_number:
|
||||||
self.row_number = row_number
|
self.row_number = row_number
|
||||||
|
else:
|
||||||
|
self.row_number = base_model.rowCount()
|
||||||
# Data structure to track files to import
|
# Data structure to track files to import
|
||||||
self.import_files_data: list[TrackFileData] = []
|
self.import_files_data: list[TrackFileData] = []
|
||||||
# Dictionary of exsting tracks
|
# Dictionary of exsting tracks
|
||||||
@ -279,7 +284,7 @@ class FileImporter:
|
|||||||
destination_track_path=f.destination_track_path,
|
destination_track_path=f.destination_track_path,
|
||||||
track_id=f.track_id,
|
track_id=f.track_id,
|
||||||
audio_metadata=helpers.get_audio_metadata(f.import_file_path),
|
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,
|
row_number=self.row_number,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -336,7 +341,7 @@ class FileImporter:
|
|||||||
f"{self.existing_tracks[track_id].title} "
|
f"{self.existing_tracks[track_id].title} "
|
||||||
f"({self.existing_tracks[track_id].artist})",
|
f"({self.existing_tracks[track_id].artist})",
|
||||||
track_id,
|
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.init_ui(items_with_ids)
|
||||||
self.selected_id = -1
|
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
|
Set up dialog
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
# Standard library imports
|
# Standard library imports
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from slugify import slugify # type: ignore
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
import argparse
|
import argparse
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
import os
|
import os
|
||||||
from slugify import slugify # type: ignore
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
@ -48,6 +49,7 @@ import stackprinter # type: ignore
|
|||||||
# App imports
|
# App imports
|
||||||
from classes import (
|
from classes import (
|
||||||
MusicMusterSignals,
|
MusicMusterSignals,
|
||||||
|
Selection,
|
||||||
TrackInfo,
|
TrackInfo,
|
||||||
)
|
)
|
||||||
from config import Config
|
from config import Config
|
||||||
@ -67,6 +69,27 @@ from utilities import check_db, update_bitrates
|
|||||||
import helpers
|
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:
|
class PreviewManager:
|
||||||
"""
|
"""
|
||||||
Manage track preview player
|
Manage track preview player
|
||||||
@ -178,6 +201,52 @@ class PreviewManager:
|
|||||||
self.start_time = None
|
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):
|
class Window(QMainWindow, Ui_MainWindow):
|
||||||
def __init__(
|
def __init__(
|
||||||
self, parent: Optional[QWidget] = None, *args: list, **kwargs: dict
|
self, parent: Optional[QWidget] = None, *args: list, **kwargs: dict
|
||||||
@ -204,10 +273,8 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
self.widgetFadeVolume.setDefaultPadding(0)
|
self.widgetFadeVolume.setDefaultPadding(0)
|
||||||
self.widgetFadeVolume.setBackground(Config.FADE_CURVE_BACKGROUND)
|
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_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.disable_selection_timing = False
|
||||||
self.clock_counter = 0
|
self.clock_counter = 0
|
||||||
@ -219,6 +286,8 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
self.connect_signals_slots()
|
self.connect_signals_slots()
|
||||||
self.catch_return_key = False
|
self.catch_return_key = False
|
||||||
self.importer: Optional[FileImporter] = None
|
self.importer: Optional[FileImporter] = None
|
||||||
|
self.selection = Selection()
|
||||||
|
self.playlists: dict[int, PlaylistData] = {}
|
||||||
|
|
||||||
if not Config.USE_INTERNAL_BROWSER:
|
if not Config.USE_INTERNAL_BROWSER:
|
||||||
webbrowser.register(
|
webbrowser.register(
|
||||||
@ -255,6 +324,12 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
QMessageBox.StandardButton.Ok,
|
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:
|
def clear_next(self) -> None:
|
||||||
"""
|
"""
|
||||||
Clear next track
|
Clear next track
|
||||||
@ -269,6 +344,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
# Unselect any selected rows
|
# Unselect any selected rows
|
||||||
if self.active_tab():
|
if self.active_tab():
|
||||||
self.active_tab().clear_selection()
|
self.active_tab().clear_selection()
|
||||||
|
|
||||||
# Clear the search bar
|
# Clear the search bar
|
||||||
self.search_playlist_clear()
|
self.search_playlist_clear()
|
||||||
|
|
||||||
@ -368,8 +444,8 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
def connect_signals_slots(self) -> None:
|
def connect_signals_slots(self) -> None:
|
||||||
self.action_About.triggered.connect(self.about)
|
self.action_About.triggered.connect(self.about)
|
||||||
self.action_Clear_selection.triggered.connect(self.clear_selection)
|
self.action_Clear_selection.triggered.connect(self.clear_selection)
|
||||||
self.actionDebug.triggered.connect(self.debug)
|
|
||||||
self.actionClosePlaylist.triggered.connect(self.close_playlist_tab)
|
self.actionClosePlaylist.triggered.connect(self.close_playlist_tab)
|
||||||
|
self.actionDebug.triggered.connect(self.debug)
|
||||||
self.actionDeletePlaylist.triggered.connect(self.delete_playlist)
|
self.actionDeletePlaylist.triggered.connect(self.delete_playlist)
|
||||||
self.actionDownload_CSV_of_played_tracks.triggered.connect(
|
self.actionDownload_CSV_of_played_tracks.triggered.connect(
|
||||||
self.download_played_tracks
|
self.download_played_tracks
|
||||||
@ -380,6 +456,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
self.actionInsertTrack.triggered.connect(self.insert_track)
|
self.actionInsertTrack.triggered.connect(self.insert_track)
|
||||||
self.actionMark_for_moving.triggered.connect(self.mark_rows_for_moving)
|
self.actionMark_for_moving.triggered.connect(self.mark_rows_for_moving)
|
||||||
self.actionMoveSelected.triggered.connect(self.move_selected)
|
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.actionNew_from_template.triggered.connect(self.new_from_template)
|
||||||
self.actionNewPlaylist.triggered.connect(self.create_and_show_playlist)
|
self.actionNewPlaylist.triggered.connect(self.create_and_show_playlist)
|
||||||
self.actionOpenPlaylist.triggered.connect(self.open_playlist)
|
self.actionOpenPlaylist.triggered.connect(self.open_playlist)
|
||||||
@ -399,10 +476,10 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
self.actionSelect_duplicate_rows.triggered.connect(
|
self.actionSelect_duplicate_rows.triggered.connect(
|
||||||
lambda: self.active_tab().select_duplicate_rows()
|
lambda: self.active_tab().select_duplicate_rows()
|
||||||
)
|
)
|
||||||
self.actionMoveUnplayed.triggered.connect(self.move_unplayed)
|
|
||||||
self.actionSetNext.triggered.connect(self.set_selected_track_next)
|
self.actionSetNext.triggered.connect(self.set_selected_track_next)
|
||||||
self.actionSkipToNext.triggered.connect(self.play_next)
|
self.actionSkipToNext.triggered.connect(self.play_next)
|
||||||
self.actionStop.triggered.connect(self.stop)
|
self.actionStop.triggered.connect(self.stop)
|
||||||
|
|
||||||
self.btnDrop3db.clicked.connect(self.drop3db)
|
self.btnDrop3db.clicked.connect(self.drop3db)
|
||||||
self.btnFade.clicked.connect(self.fade)
|
self.btnFade.clicked.connect(self.fade)
|
||||||
self.btnHidePlayed.clicked.connect(self.hide_played)
|
self.btnHidePlayed.clicked.connect(self.hide_played)
|
||||||
@ -467,15 +544,20 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
|
|
||||||
def create_playlist_tab(self, playlist: Playlists) -> int:
|
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.
|
add tab to display. Return index number of tab.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
log.debug(f"create_playlist_tab({playlist=})")
|
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(
|
playlist_tab = PlaylistTab(
|
||||||
musicmuster=self,
|
musicmuster=self, model=self.playlists[playlist.id].proxy_model
|
||||||
playlist_id=playlist.id,
|
|
||||||
)
|
)
|
||||||
idx = self.tabPlaylist.addTab(playlist_tab, playlist.name)
|
idx = self.tabPlaylist.addTab(playlist_tab, playlist.name)
|
||||||
|
|
||||||
@ -642,6 +724,13 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
if track_sequence.current:
|
if track_sequence.current:
|
||||||
track_sequence.current.fade()
|
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):
|
def hide_played(self):
|
||||||
"""Toggle hide played tracks"""
|
"""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
|
# We need to keep a referent to the FileImporter else it will be
|
||||||
# garbage collected while import threads are still running
|
# garbage collected while import threads are still running
|
||||||
self.importer = FileImporter(
|
self.importer = FileImporter(
|
||||||
self.active_proxy_model(),
|
self.get_active_base_model(),
|
||||||
self.active_tab().source_model_selected_row_number(),
|
self.active_tab().source_model_selected_row_number(),
|
||||||
)
|
)
|
||||||
self.importer.do_import()
|
self.importer.do_import()
|
||||||
@ -675,11 +764,6 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
def insert_header(self) -> None:
|
def insert_header(self) -> None:
|
||||||
"""Show dialog box to enter header text and add to playlist"""
|
"""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
|
# Get header text
|
||||||
dlg: QInputDialog = QInputDialog(self)
|
dlg: QInputDialog = QInputDialog(self)
|
||||||
dlg.setInputMode(QInputDialog.InputMode.TextInput)
|
dlg.setInputMode(QInputDialog.InputMode.TextInput)
|
||||||
@ -687,7 +771,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
dlg.resize(500, 100)
|
dlg.resize(500, 100)
|
||||||
ok = dlg.exec()
|
ok = dlg.exec()
|
||||||
if ok:
|
if ok:
|
||||||
proxy_model.insert_row(
|
self.get_active_base_model().insert_row(
|
||||||
proposed_row_number=self.active_tab().source_model_selected_row_number(),
|
proposed_row_number=self.active_tab().source_model_selected_row_number(),
|
||||||
note=dlg.textValue(),
|
note=dlg.textValue(),
|
||||||
)
|
)
|
||||||
@ -704,7 +788,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
parent=self,
|
parent=self,
|
||||||
session=session,
|
session=session,
|
||||||
new_row_number=new_row_number,
|
new_row_number=new_row_number,
|
||||||
source_model=self.active_proxy_model(),
|
base_model=self.get_active_base_model(),
|
||||||
)
|
)
|
||||||
dlg.exec()
|
dlg.exec()
|
||||||
session.commit()
|
session.commit()
|
||||||
@ -716,9 +800,10 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
for playlist in Playlists.get_open(session):
|
for playlist in Playlists.get_open(session):
|
||||||
if playlist:
|
if playlist:
|
||||||
_ = self.create_playlist_tab(playlist)
|
|
||||||
playlist_ids.append(playlist.id)
|
|
||||||
log.debug(f"load_last_playlists() loaded {playlist=}")
|
log.debug(f"load_last_playlists() loaded {playlist=}")
|
||||||
|
# Create tab
|
||||||
|
playlist_ids.append(self.create_playlist_tab(playlist))
|
||||||
|
|
||||||
# Set active tab
|
# Set active tab
|
||||||
record = Settings.get_setting(session, "active_tab")
|
record = Settings.get_setting(session, "active_tab")
|
||||||
if record.f_int is not None and record.f_int >= 0:
|
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
|
# Save the selected PlaylistRows items ready for a later
|
||||||
# paste
|
# paste
|
||||||
self.move_source_rows = self.active_tab().get_selected_rows()
|
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(
|
log.debug(
|
||||||
f"mark_rows_for_moving(): {self.move_source_rows=} {self.move_source_model=}"
|
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
|
to_row = 0
|
||||||
|
|
||||||
# Move rows
|
# 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
|
row_numbers, to_row, to_playlist_id
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -828,7 +913,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
Move unplayed rows to another playlist
|
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:
|
if not unplayed_rows:
|
||||||
return
|
return
|
||||||
# We can get a race condition as selected rows change while
|
# 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:
|
if not self.move_source_rows or not self.move_source_model:
|
||||||
return
|
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()
|
selected_rows = self.active_tab().get_selected_rows()
|
||||||
if selected_rows:
|
if selected_rows:
|
||||||
destination_row = selected_rows[0]
|
destination_row = selected_rows[0]
|
||||||
@ -928,10 +1013,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
):
|
):
|
||||||
set_next_row = destination_row
|
set_next_row = destination_row
|
||||||
|
|
||||||
if (
|
if to_playlist_model.playlist_id == self.move_source_model.playlist_id:
|
||||||
to_playlist_model.playlist_id
|
|
||||||
== self.move_source_model.source_model.playlist_id
|
|
||||||
):
|
|
||||||
self.move_source_model.move_rows(self.move_source_rows, destination_row)
|
self.move_source_model.move_rows(self.move_source_rows, destination_row)
|
||||||
else:
|
else:
|
||||||
self.move_source_model.move_rows_between_playlists(
|
self.move_source_model.move_rows_between_playlists(
|
||||||
@ -1058,6 +1140,8 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
if not track_info:
|
||||||
|
return
|
||||||
self.preview_manager.set_track_info(track_info)
|
self.preview_manager.set_track_info(track_info)
|
||||||
self.preview_manager.play()
|
self.preview_manager.play()
|
||||||
else:
|
else:
|
||||||
@ -1090,6 +1174,8 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
if self.preview_manager.is_playing():
|
if self.preview_manager.is_playing():
|
||||||
track_id = self.preview_manager.track_id
|
track_id = self.preview_manager.track_id
|
||||||
row_number = self.preview_manager.row_number
|
row_number = self.preview_manager.row_number
|
||||||
|
if not row_number:
|
||||||
|
return
|
||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
track = session.get(Tracks, track_id)
|
track = session.get(Tracks, track_id)
|
||||||
if track:
|
if track:
|
||||||
@ -1100,8 +1186,8 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
track.intro = intro
|
track.intro = intro
|
||||||
session.commit()
|
session.commit()
|
||||||
self.preview_manager.set_intro(intro)
|
self.preview_manager.set_intro(intro)
|
||||||
self.active_tab().source_model.refresh_row(session, row_number)
|
self.get_active_base_model().refresh_row(session, row_number)
|
||||||
self.active_tab().source_model.invalidate_row(row_number)
|
self.get_active_base_model().invalidate_row(row_number)
|
||||||
|
|
||||||
def preview_start(self) -> None:
|
def preview_start(self) -> None:
|
||||||
"""Restart preview"""
|
"""Restart preview"""
|
||||||
@ -1288,22 +1374,12 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
if row_number is None:
|
if row_number is None:
|
||||||
return 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:
|
if track_info is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return track_info
|
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:
|
def set_main_window_size(self) -> None:
|
||||||
"""Set size of window from database"""
|
"""Set size of window from database"""
|
||||||
|
|
||||||
@ -1389,9 +1465,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
display_row = (
|
display_row = (
|
||||||
self.active_proxy_model()
|
self.active_proxy_model()
|
||||||
.mapFromSource(
|
.mapFromSource(
|
||||||
self.active_proxy_model().source_model.index(
|
self.get_active_base_model().index(playlist_track.row_number, 0)
|
||||||
playlist_track.row_number, 0
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.row()
|
.row()
|
||||||
)
|
)
|
||||||
@ -1431,10 +1505,10 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
if track_sequence.current:
|
if track_sequence.current:
|
||||||
track_sequence.current.stop()
|
track_sequence.current.stop()
|
||||||
|
|
||||||
def tab_change(self):
|
def tab_change(self) -> None:
|
||||||
"""Called when active tab changed"""
|
"""Called when active tab changed"""
|
||||||
|
|
||||||
self.active_tab().resize_rows()
|
self.active_tab().tab_live()
|
||||||
|
|
||||||
def tick_10ms(self) -> None:
|
def tick_10ms(self) -> None:
|
||||||
"""
|
"""
|
||||||
@ -1622,64 +1696,6 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
self.tabPlaylist.setTabIcon(idx, QIcon())
|
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 __name__ == "__main__":
|
||||||
"""
|
"""
|
||||||
If command line arguments given, carry out requested function and
|
If command line arguments given, carry out requested function and
|
||||||
|
|||||||
@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
from random import shuffle
|
from random import shuffle
|
||||||
from typing import Optional
|
from typing import cast, Optional
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@ -1340,8 +1340,9 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
unplayed_count += 1
|
unplayed_count += 1
|
||||||
duration += row_rat.duration
|
duration += row_rat.duration
|
||||||
|
|
||||||
# Should never get here
|
# We should only get here if there were no rows in section (ie,
|
||||||
return f"Error calculating subtotal ({row_rat.note})"
|
# this was row zero)
|
||||||
|
return Config.SUBTOTAL_ON_ROW_ZERO
|
||||||
|
|
||||||
def selection_is_sortable(self, row_numbers: list[int]) -> bool:
|
def selection_is_sortable(self, row_numbers: list[int]) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -1662,19 +1663,14 @@ class PlaylistProxyModel(QSortFilterProxyModel):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
source_model: PlaylistModel,
|
|
||||||
*args: QObject,
|
|
||||||
**kwargs: QObject,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
self.source_model = source_model
|
super().__init__()
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
self.setSourceModel(source_model)
|
|
||||||
# Search all columns
|
# Search all columns
|
||||||
self.setFilterKeyColumn(-1)
|
self.setFilterKeyColumn(-1)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
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:
|
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:
|
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.sourceModel().played_tracks_hidden:
|
||||||
if self.source_model.is_played_row(source_row):
|
if self.sourceModel().is_played_row(source_row):
|
||||||
# Don't hide current track
|
# Don't hide current track
|
||||||
if (
|
if (
|
||||||
track_sequence.current
|
track_sequence.current
|
||||||
and track_sequence.current.playlist_id
|
and track_sequence.current.playlist_id
|
||||||
== self.source_model.playlist_id
|
== self.sourceModel().playlist_id
|
||||||
and track_sequence.current.row_number == source_row
|
and track_sequence.current.row_number == source_row
|
||||||
):
|
):
|
||||||
return True
|
return True
|
||||||
@ -1698,7 +1694,8 @@ class PlaylistProxyModel(QSortFilterProxyModel):
|
|||||||
# Don't hide next track
|
# Don't hide next track
|
||||||
if (
|
if (
|
||||||
track_sequence.next
|
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
|
and track_sequence.next.row_number == source_row
|
||||||
):
|
):
|
||||||
return True
|
return True
|
||||||
@ -1707,7 +1704,7 @@ class PlaylistProxyModel(QSortFilterProxyModel):
|
|||||||
if track_sequence.previous:
|
if track_sequence.previous:
|
||||||
if (
|
if (
|
||||||
track_sequence.previous.playlist_id
|
track_sequence.previous.playlist_id
|
||||||
!= self.source_model.playlist_id
|
!= self.sourceModel().playlist_id
|
||||||
or track_sequence.previous.row_number != source_row
|
or track_sequence.previous.row_number != source_row
|
||||||
):
|
):
|
||||||
# This row isn't our previous track: hide it
|
# This row isn't our previous track: hide it
|
||||||
@ -1731,7 +1728,7 @@ class PlaylistProxyModel(QSortFilterProxyModel):
|
|||||||
# true next time through.
|
# true next time through.
|
||||||
QTimer.singleShot(
|
QTimer.singleShot(
|
||||||
Config.HIDE_AFTER_PLAYING_OFFSET + 100,
|
Config.HIDE_AFTER_PLAYING_OFFSET + 100,
|
||||||
lambda: self.source_model.invalidate_row(source_row),
|
lambda: self.sourceModel().invalidate_row(source_row),
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
# Next track not playing yet so don't hide previous
|
# Next track not playing yet so don't hide previous
|
||||||
@ -1754,105 +1751,9 @@ class PlaylistProxyModel(QSortFilterProxyModel):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# ######################################
|
def sourceModel(self) -> PlaylistModel:
|
||||||
# Forward functions not handled in proxy
|
"""
|
||||||
# ######################################
|
Override sourceModel to return correct type
|
||||||
|
"""
|
||||||
|
|
||||||
def current_track_started(self):
|
return cast(PlaylistModel, super().sourceModel())
|
||||||
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()
|
|
||||||
|
|||||||
196
app/playlists.py
196
app/playlists.py
@ -37,7 +37,7 @@ import line_profiler
|
|||||||
|
|
||||||
# App imports
|
# App imports
|
||||||
from audacity_controller import AudacityController
|
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 config import Config
|
||||||
from dialogs import TrackSelectDialog
|
from dialogs import TrackSelectDialog
|
||||||
from helpers import (
|
from helpers import (
|
||||||
@ -82,9 +82,9 @@ class PlaylistDelegate(QStyledItemDelegate):
|
|||||||
|
|
||||||
QTimer.singleShot(0, resize_func)
|
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)
|
super().__init__(parent)
|
||||||
self.source_model = source_model
|
self.base_model = base_model
|
||||||
self.signals = MusicMusterSignals()
|
self.signals = MusicMusterSignals()
|
||||||
self.click_position = None
|
self.click_position = None
|
||||||
self.current_editor: Optional[Any] = None
|
self.current_editor: Optional[Any] = None
|
||||||
@ -239,7 +239,7 @@ class PlaylistDelegate(QStyledItemDelegate):
|
|||||||
proxy_model = index.model()
|
proxy_model = index.model()
|
||||||
edit_index = proxy_model.mapToSource(index)
|
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
|
edit_index, Qt.ItemDataRole.EditRole
|
||||||
)
|
)
|
||||||
if index.column() == Col.INTRO.value:
|
if index.column() == Col.INTRO.value:
|
||||||
@ -256,7 +256,7 @@ class PlaylistDelegate(QStyledItemDelegate):
|
|||||||
value = editor.toPlainText().strip()
|
value = editor.toPlainText().strip()
|
||||||
elif isinstance(editor, QDoubleSpinBox):
|
elif isinstance(editor, QDoubleSpinBox):
|
||||||
value = editor.value()
|
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):
|
def updateEditorGeometry(self, editor, option, index):
|
||||||
editor.setGeometry(option.rect)
|
editor.setGeometry(option.rect)
|
||||||
@ -285,22 +285,17 @@ class PlaylistTab(QTableView):
|
|||||||
The playlist view
|
The playlist view
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, musicmuster: "Window", model: PlaylistProxyModel) -> None:
|
||||||
self,
|
|
||||||
musicmuster: "Window",
|
|
||||||
playlist_id: int,
|
|
||||||
) -> None:
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
# Save passed settings
|
# Save passed settings
|
||||||
self.musicmuster = musicmuster
|
self.musicmuster = (
|
||||||
self.playlist_id = playlist_id
|
musicmuster # TODO: do we need to keep a reference to musicmuster?
|
||||||
log.debug(f"PlaylistTab.__init__({playlist_id=})")
|
)
|
||||||
|
self.playlist_id = model.sourceModel().playlist_id
|
||||||
|
|
||||||
# Set up widget
|
# Set up widget
|
||||||
self.source_model = PlaylistModel(playlist_id)
|
self.setItemDelegate(PlaylistDelegate(self, model.sourceModel()))
|
||||||
self.proxy_model = PlaylistProxyModel(self.source_model)
|
|
||||||
self.setItemDelegate(PlaylistDelegate(self, self.source_model))
|
|
||||||
self.setAlternatingRowColors(True)
|
self.setAlternatingRowColors(True)
|
||||||
self.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
|
self.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
|
||||||
self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
|
self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
|
||||||
@ -328,9 +323,8 @@ class PlaylistTab(QTableView):
|
|||||||
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||||
self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||||
|
|
||||||
# Load playlist rows
|
# Singleton object to store selection
|
||||||
self.setModel(self.proxy_model)
|
self.selection = Selection()
|
||||||
self._set_column_widths()
|
|
||||||
|
|
||||||
# Set up for Audacity
|
# Set up for Audacity
|
||||||
try:
|
try:
|
||||||
@ -339,6 +333,10 @@ class PlaylistTab(QTableView):
|
|||||||
self.ac = None
|
self.ac = None
|
||||||
show_warning(self.musicmuster, "Audacity error", str(e))
|
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
|
# Stretch last column *after* setting column widths which is
|
||||||
# *much* faster
|
# *much* faster
|
||||||
h_header = self.horizontalHeader()
|
h_header = self.horizontalHeader()
|
||||||
@ -373,7 +371,7 @@ class PlaylistTab(QTableView):
|
|||||||
|
|
||||||
# Update start times in case a start time in a note has been
|
# Update start times in case a start time in a note has been
|
||||||
# edited
|
# edited
|
||||||
self.source_model.update_track_times()
|
self.get_base_model().update_track_times()
|
||||||
|
|
||||||
# Deselect edited line
|
# Deselect edited line
|
||||||
self.clear_selection()
|
self.clear_selection()
|
||||||
@ -382,36 +380,50 @@ class PlaylistTab(QTableView):
|
|||||||
def dropEvent(
|
def dropEvent(
|
||||||
self, event: Optional[QDropEvent], dummy_for_profiling: Optional[int] = None
|
self, event: Optional[QDropEvent], dummy_for_profiling: Optional[int] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""
|
||||||
|
Move dropped rows
|
||||||
|
"""
|
||||||
|
|
||||||
if not event:
|
if not event:
|
||||||
return
|
return
|
||||||
|
|
||||||
if event.source() is not self or (
|
if event.source() is not self or (
|
||||||
event.dropAction() != Qt.DropAction.MoveAction
|
event.dropAction() != Qt.DropAction.MoveAction
|
||||||
and self.dragDropMode() != QAbstractItemView.DragDropMode.InternalMove
|
and self.dragDropMode() != QAbstractItemView.DragDropMode.InternalMove
|
||||||
):
|
):
|
||||||
super().dropEvent(event)
|
return super().dropEvent(event)
|
||||||
|
|
||||||
from_rows = self.selected_model_row_numbers()
|
from_rows = self.selected_model_row_numbers()
|
||||||
to_index = self.indexAt(event.position().toPoint())
|
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 (
|
if (
|
||||||
self.dropIndicatorPosition()
|
self.dropIndicatorPosition()
|
||||||
== QAbstractItemView.DropIndicatorPosition.BelowItem
|
== QAbstractItemView.DropIndicatorPosition.BelowItem
|
||||||
):
|
):
|
||||||
proxy_index = self.proxy_model.createIndex(
|
# Drop on the row below
|
||||||
to_index.row() + 1,
|
next_row = to_index.row() + 1
|
||||||
to_index.column(),
|
if next_row < self.model().rowCount(): # Ensure the row exists
|
||||||
to_index.internalId(),
|
destination_index = to_index.siblingAtRow(next_row)
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
proxy_index = to_index
|
# Handle edge case where next_row is beyond the last row
|
||||||
to_model_row = self.proxy_model.mapToSource(proxy_index).row()
|
destination_index = to_index
|
||||||
|
else:
|
||||||
|
destination_index = to_index
|
||||||
|
|
||||||
|
to_model_row = self.model().mapToSource(destination_index).row()
|
||||||
log.debug(
|
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 (
|
if (
|
||||||
0 <= min(from_rows) <= self.source_model.rowCount()
|
0 <= min(from_rows) <= base_model_row_count
|
||||||
and 0 <= max(from_rows) <= self.source_model.rowCount()
|
and 0 <= to_model_row <= base_model_row_count
|
||||||
and 0 <= to_model_row <= self.source_model.rowCount()
|
|
||||||
):
|
):
|
||||||
# If we move a row to immediately under the current track, make
|
# If we move a row to immediately under the current track, make
|
||||||
# that moved row the next track
|
# that moved row the next track
|
||||||
@ -422,7 +434,7 @@ class PlaylistTab(QTableView):
|
|||||||
):
|
):
|
||||||
set_next_row = to_model_row
|
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
|
# Reset drag mode to allow row selection by dragging
|
||||||
self.setDragEnabled(False)
|
self.setDragEnabled(False)
|
||||||
@ -435,7 +447,7 @@ class PlaylistTab(QTableView):
|
|||||||
|
|
||||||
# Set next row if we are immediately under current row
|
# Set next row if we are immediately under current row
|
||||||
if set_next_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()
|
event.accept()
|
||||||
|
|
||||||
@ -469,12 +481,14 @@ class PlaylistTab(QTableView):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
selected_rows = self.get_selected_rows()
|
selected_rows = self.get_selected_rows()
|
||||||
|
self.selection.rows = selected_rows
|
||||||
|
|
||||||
# If no rows are selected, we have nothing to do
|
# If no rows are selected, we have nothing to do
|
||||||
if len(selected_rows) == 0:
|
if len(selected_rows) == 0:
|
||||||
self.musicmuster.lblSumPlaytime.setText("")
|
self.musicmuster.lblSumPlaytime.setText("")
|
||||||
else:
|
else:
|
||||||
if not self.musicmuster.disable_selection_timing:
|
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()
|
self.get_selected_rows()
|
||||||
)
|
)
|
||||||
if selected_duration > 0:
|
if selected_duration > 0:
|
||||||
@ -525,7 +539,7 @@ class PlaylistTab(QTableView):
|
|||||||
parent=self.musicmuster,
|
parent=self.musicmuster,
|
||||||
session=session,
|
session=session,
|
||||||
new_row_number=model_row_number,
|
new_row_number=model_row_number,
|
||||||
source_model=self.source_model,
|
base_model=self.get_base_model(),
|
||||||
add_to_header=True,
|
add_to_header=True,
|
||||||
)
|
)
|
||||||
dlg.exec()
|
dlg.exec()
|
||||||
@ -535,12 +549,12 @@ class PlaylistTab(QTableView):
|
|||||||
"""Used to process context (right-click) menu, which is defined here"""
|
"""Used to process context (right-click) menu, which is defined here"""
|
||||||
|
|
||||||
self.menu.clear()
|
self.menu.clear()
|
||||||
proxy_model = self.proxy_model
|
|
||||||
|
|
||||||
index = proxy_model.index(item.row(), item.column())
|
index = self.model().index(item.row(), item.column())
|
||||||
model_row_number = proxy_model.mapToSource(index).row()
|
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
|
track_row = not header_row
|
||||||
if track_sequence.current:
|
if track_sequence.current:
|
||||||
this_is_current_row = model_row_number == track_sequence.current.row_number
|
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
|
this_is_next_row = model_row_number == track_sequence.next.row_number
|
||||||
else:
|
else:
|
||||||
this_is_next_row = False
|
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
|
# Open/import in/from Audacity
|
||||||
if track_row and not this_is_current_row:
|
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:
|
if track_row and not this_is_current_row and not this_is_next_row:
|
||||||
self._add_context_menu(
|
self._add_context_menu(
|
||||||
"Remove track from row",
|
"Remove track from row",
|
||||||
lambda: proxy_model.remove_track(model_row_number),
|
lambda: base_model.remove_track(model_row_number),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Remove comments
|
# Remove comments
|
||||||
@ -605,7 +619,7 @@ class PlaylistTab(QTableView):
|
|||||||
self.menu.addSeparator()
|
self.menu.addSeparator()
|
||||||
|
|
||||||
# Mark unplayed
|
# 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(
|
self._add_context_menu(
|
||||||
"Mark unplayed",
|
"Mark unplayed",
|
||||||
lambda: self._mark_as_unplayed(self.get_selected_rows()),
|
lambda: self._mark_as_unplayed(self.get_selected_rows()),
|
||||||
@ -624,27 +638,27 @@ class PlaylistTab(QTableView):
|
|||||||
sort_menu = self.menu.addMenu("Sort")
|
sort_menu = self.menu.addMenu("Sort")
|
||||||
self._add_context_menu(
|
self._add_context_menu(
|
||||||
"by title",
|
"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,
|
parent_menu=sort_menu,
|
||||||
)
|
)
|
||||||
self._add_context_menu(
|
self._add_context_menu(
|
||||||
"by artist",
|
"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,
|
parent_menu=sort_menu,
|
||||||
)
|
)
|
||||||
self._add_context_menu(
|
self._add_context_menu(
|
||||||
"by duration",
|
"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,
|
parent_menu=sort_menu,
|
||||||
)
|
)
|
||||||
self._add_context_menu(
|
self._add_context_menu(
|
||||||
"by last played",
|
"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,
|
parent_menu=sort_menu,
|
||||||
)
|
)
|
||||||
self._add_context_menu(
|
self._add_context_menu(
|
||||||
"randomly",
|
"randomly",
|
||||||
lambda: proxy_model.sort_randomly(self.get_selected_rows()),
|
lambda: base_model.sort_randomly(self.get_selected_rows()),
|
||||||
parent_menu=sort_menu,
|
parent_menu=sort_menu,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -711,7 +725,7 @@ class PlaylistTab(QTableView):
|
|||||||
to the clipboard. Otherwise, return None.
|
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:
|
if not track_path:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -734,7 +748,7 @@ class PlaylistTab(QTableView):
|
|||||||
Called when track starts playing
|
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
|
# Scroll to current section if hide mode is by section
|
||||||
if (
|
if (
|
||||||
self.musicmuster.hide_played_tracks
|
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}?"):
|
if not ask_yes_no("Delete rows", f"Really delete {row_count} row{plural}?"):
|
||||||
return
|
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()
|
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]:
|
def get_selected_row_track_info(self) -> Optional[TrackInfo]:
|
||||||
"""
|
"""
|
||||||
Return the track_id and row number of the selected
|
Return the track_id and row number of the selected
|
||||||
@ -780,11 +803,13 @@ class PlaylistTab(QTableView):
|
|||||||
if selected_row is None:
|
if selected_row is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
base_model = self.get_base_model()
|
||||||
model_row_number = self.source_model_selected_row_number()
|
model_row_number = self.source_model_selected_row_number()
|
||||||
|
|
||||||
if model_row_number is None:
|
if model_row_number is None:
|
||||||
return None
|
return None
|
||||||
else:
|
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:
|
if not track_id:
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
@ -808,12 +833,7 @@ class PlaylistTab(QTableView):
|
|||||||
# items in that row selected)
|
# items in that row selected)
|
||||||
result = sorted(
|
result = sorted(
|
||||||
list(
|
list(
|
||||||
set(
|
set([self.model().mapToSource(a).row() for a in self.selectedIndexes()])
|
||||||
[
|
|
||||||
self.proxy_model.mapToSource(a).row()
|
|
||||||
for a in self.selectedIndexes()
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -825,7 +845,7 @@ class PlaylistTab(QTableView):
|
|||||||
Scroll played sections off screen
|
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:
|
def _import_from_audacity(self, row_number: int) -> None:
|
||||||
"""
|
"""
|
||||||
@ -844,7 +864,7 @@ class PlaylistTab(QTableView):
|
|||||||
def _info_row(self, row_number: int) -> None:
|
def _info_row(self, row_number: int) -> None:
|
||||||
"""Display popup with info re row"""
|
"""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:
|
if prd:
|
||||||
txt = (
|
txt = (
|
||||||
f"Title: {prd.title}\n"
|
f"Title: {prd.title}\n"
|
||||||
@ -863,7 +883,7 @@ class PlaylistTab(QTableView):
|
|||||||
def _mark_as_unplayed(self, row_numbers: List[int]) -> None:
|
def _mark_as_unplayed(self, row_numbers: List[int]) -> None:
|
||||||
"""Mark row as unplayed"""
|
"""Mark row as unplayed"""
|
||||||
|
|
||||||
self.source_model.mark_unplayed(row_numbers)
|
self.get_base_model().mark_unplayed(row_numbers)
|
||||||
self.clear_selection()
|
self.clear_selection()
|
||||||
|
|
||||||
def _mark_for_moving(self) -> None:
|
def _mark_for_moving(self) -> None:
|
||||||
@ -873,6 +893,13 @@ class PlaylistTab(QTableView):
|
|||||||
|
|
||||||
self.musicmuster.mark_rows_for_moving()
|
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:
|
def _move_selected_rows(self) -> None:
|
||||||
"""
|
"""
|
||||||
Move selected rows here
|
Move selected rows here
|
||||||
@ -885,7 +912,7 @@ class PlaylistTab(QTableView):
|
|||||||
Open track in passed row in Audacity
|
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:
|
if not path:
|
||||||
log.error(f"_open_in_audacity: can't get path for {row_number=}")
|
log.error(f"_open_in_audacity: can't get path for {row_number=}")
|
||||||
return
|
return
|
||||||
@ -903,7 +930,7 @@ class PlaylistTab(QTableView):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Let the model know
|
# Let the model know
|
||||||
self.source_model.previous_track_ended()
|
self.get_base_model().previous_track_ended()
|
||||||
|
|
||||||
def _remove_comments(self) -> None:
|
def _remove_comments(self) -> None:
|
||||||
"""
|
"""
|
||||||
@ -914,12 +941,12 @@ class PlaylistTab(QTableView):
|
|||||||
if not row_numbers:
|
if not row_numbers:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.source_model.remove_comments(row_numbers)
|
self.get_base_model().remove_comments(row_numbers)
|
||||||
|
|
||||||
def _rescan(self, row_number: int) -> None:
|
def _rescan(self, row_number: int) -> None:
|
||||||
"""Rescan track"""
|
"""Rescan track"""
|
||||||
|
|
||||||
self.source_model.rescan_track(row_number)
|
self.get_base_model().rescan_track(row_number)
|
||||||
self.clear_selection()
|
self.clear_selection()
|
||||||
|
|
||||||
def resize_rows(self, playlist_id: Optional[int] = None) -> None:
|
def resize_rows(self, playlist_id: Optional[int] = None) -> None:
|
||||||
@ -934,7 +961,7 @@ class PlaylistTab(QTableView):
|
|||||||
|
|
||||||
# Suggestion from phind.com
|
# Suggestion from phind.com
|
||||||
def resize_row(row, count=1):
|
def resize_row(row, count=1):
|
||||||
row_count = self.source_model.rowCount()
|
row_count = self.model().rowCount()
|
||||||
for todo in range(count):
|
for todo in range(count):
|
||||||
if row < row_count:
|
if row < row_count:
|
||||||
self.resizeRowToContents(row)
|
self.resizeRowToContents(row)
|
||||||
@ -953,7 +980,7 @@ class PlaylistTab(QTableView):
|
|||||||
if row_number is None:
|
if row_number is None:
|
||||||
return
|
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)
|
self.scrollTo(row_index, QAbstractItemView.ScrollHint.PositionAtTop)
|
||||||
|
|
||||||
def select_duplicate_rows(self) -> None:
|
def select_duplicate_rows(self) -> None:
|
||||||
@ -968,7 +995,7 @@ class PlaylistTab(QTableView):
|
|||||||
# We need to be in MultiSelection mode
|
# We need to be in MultiSelection mode
|
||||||
self.setSelectionMode(QAbstractItemView.SelectionMode.MultiSelection)
|
self.setSelectionMode(QAbstractItemView.SelectionMode.MultiSelection)
|
||||||
# Get the duplicate rows
|
# Get the duplicate rows
|
||||||
duplicate_rows = self.source_model.get_duplicate_rows()
|
duplicate_rows = self.get_base_model().get_duplicate_rows()
|
||||||
# Select the rows
|
# Select the rows
|
||||||
for duplicate_row in duplicate_rows:
|
for duplicate_row in duplicate_rows:
|
||||||
self.selectRow(duplicate_row)
|
self.selectRow(duplicate_row)
|
||||||
@ -983,7 +1010,7 @@ class PlaylistTab(QTableView):
|
|||||||
selected_index = self._selected_row_index()
|
selected_index = self._selected_row_index()
|
||||||
if selected_index is None:
|
if selected_index is None:
|
||||||
return 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]:
|
def selected_model_row_numbers(self) -> List[int]:
|
||||||
"""
|
"""
|
||||||
@ -994,9 +1021,8 @@ class PlaylistTab(QTableView):
|
|||||||
selected_indexes = self._selected_row_indexes()
|
selected_indexes = self._selected_row_indexes()
|
||||||
if selected_indexes is None:
|
if selected_indexes is None:
|
||||||
return []
|
return []
|
||||||
if hasattr(self.proxy_model, "mapToSource"):
|
|
||||||
return [self.proxy_model.mapToSource(a).row() for a in selected_indexes]
|
return [self.model().mapToSource(a).row() for a in selected_indexes]
|
||||||
return [a.row() for a in selected_indexes]
|
|
||||||
|
|
||||||
def _selected_row_index(self) -> Optional[QModelIndex]:
|
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=}")
|
log.debug(f"set_row_as_next_track() {model_row_number=}")
|
||||||
if model_row_number is None:
|
if model_row_number is None:
|
||||||
return
|
return
|
||||||
self.source_model.set_next_row(model_row_number)
|
self.get_base_model().set_next_row(model_row_number)
|
||||||
self.clearSelection()
|
self.clearSelection()
|
||||||
|
|
||||||
def _span_cells(
|
def _span_cells(
|
||||||
@ -1061,17 +1087,19 @@ class PlaylistTab(QTableView):
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Implement spanning of cells, initiated by signal
|
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:
|
if playlist_id != self.playlist_id:
|
||||||
return
|
return
|
||||||
|
|
||||||
proxy_model = self.proxy_model
|
base_model = self.get_base_model()
|
||||||
edit_index = proxy_model.mapFromSource(
|
|
||||||
self.source_model.createIndex(row, column)
|
cell_index = self.model().mapFromSource(base_model.createIndex(row, column))
|
||||||
)
|
row = cell_index.row()
|
||||||
row = edit_index.row()
|
column = cell_index.column()
|
||||||
column = edit_index.column()
|
|
||||||
|
|
||||||
# Don't set spanning if already in place because that is seen as
|
# 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
|
# 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)
|
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:
|
def _unmark_as_next(self) -> None:
|
||||||
"""Rescan track"""
|
"""Rescan track"""
|
||||||
|
|
||||||
|
|||||||
@ -967,69 +967,64 @@ padding-left: 8px;</string>
|
|||||||
</property>
|
</property>
|
||||||
<widget class="QMenu" name="menuFile">
|
<widget class="QMenu" name="menuFile">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
<string>&Playlists</string>
|
<string>&Playlist</string>
|
||||||
</property>
|
</property>
|
||||||
<addaction name="actionNewPlaylist"/>
|
<addaction name="separator"/>
|
||||||
<addaction name="actionNew_from_template"/>
|
<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>&File</string>
|
||||||
|
</property>
|
||||||
|
<addaction name="separator"/>
|
||||||
|
<addaction name="separator"/>
|
||||||
<addaction name="actionOpenPlaylist"/>
|
<addaction name="actionOpenPlaylist"/>
|
||||||
|
<addaction name="actionNewPlaylist"/>
|
||||||
<addaction name="actionClosePlaylist"/>
|
<addaction name="actionClosePlaylist"/>
|
||||||
<addaction name="actionRenamePlaylist"/>
|
<addaction name="actionRenamePlaylist"/>
|
||||||
<addaction name="actionDeletePlaylist"/>
|
<addaction name="actionDeletePlaylist"/>
|
||||||
<addaction name="actionExport_playlist"/>
|
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
<addaction name="actionSelect_duplicate_rows"/>
|
<addaction name="actionNew_from_template"/>
|
||||||
<addaction name="separator"/>
|
|
||||||
<addaction name="actionMoveSelected"/>
|
|
||||||
<addaction name="actionMoveUnplayed"/>
|
|
||||||
<addaction name="actionDownload_CSV_of_played_tracks"/>
|
|
||||||
<addaction name="actionSave_as_template"/>
|
<addaction name="actionSave_as_template"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
<addaction name="actionReplace_files"/>
|
<addaction name="actionReplace_files"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
|
<addaction name="actionDebug"/>
|
||||||
|
<addaction name="action_About"/>
|
||||||
|
<addaction name="separator"/>
|
||||||
<addaction name="actionE_xit"/>
|
<addaction name="actionE_xit"/>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QMenu" name="menuPlaylist">
|
<widget class="QMenu" name="menuSearc_h">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
<string>Sho&wtime</string>
|
<string>&Music</string>
|
||||||
</property>
|
</property>
|
||||||
<addaction name="separator"/>
|
<addaction name="actionSetNext"/>
|
||||||
<addaction name="actionPlay_next"/>
|
<addaction name="actionPlay_next"/>
|
||||||
<addaction name="actionFade"/>
|
<addaction name="actionFade"/>
|
||||||
<addaction name="actionStop"/>
|
<addaction name="actionStop"/>
|
||||||
<addaction name="actionResume"/>
|
<addaction name="actionResume"/>
|
||||||
<addaction name="separator"/>
|
|
||||||
<addaction name="actionSkipToNext"/>
|
<addaction name="actionSkipToNext"/>
|
||||||
<addaction name="separator"/>
|
<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>&Search</string>
|
|
||||||
</property>
|
|
||||||
<addaction name="actionSearch"/>
|
<addaction name="actionSearch"/>
|
||||||
<addaction name="separator"/>
|
|
||||||
<addaction name="actionSearch_title_in_Wikipedia"/>
|
<addaction name="actionSearch_title_in_Wikipedia"/>
|
||||||
<addaction name="actionSearch_title_in_Songfacts"/>
|
<addaction name="actionSearch_title_in_Songfacts"/>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QMenu" name="menuHelp">
|
|
||||||
<property name="title">
|
|
||||||
<string>&Help</string>
|
|
||||||
</property>
|
|
||||||
<addaction name="action_About"/>
|
|
||||||
<addaction name="actionDebug"/>
|
|
||||||
</widget>
|
|
||||||
<addaction name="menuFile"/>
|
|
||||||
<addaction name="menuPlaylist"/>
|
<addaction name="menuPlaylist"/>
|
||||||
|
<addaction name="menuFile"/>
|
||||||
<addaction name="menuSearc_h"/>
|
<addaction name="menuSearc_h"/>
|
||||||
<addaction name="menuHelp"/>
|
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QStatusBar" name="statusbar">
|
<widget class="QStatusBar" name="statusbar">
|
||||||
<property name="enabled">
|
<property name="enabled">
|
||||||
|
|||||||
@ -495,8 +495,6 @@ class Ui_MainWindow(object):
|
|||||||
self.menuPlaylist.setObjectName("menuPlaylist")
|
self.menuPlaylist.setObjectName("menuPlaylist")
|
||||||
self.menuSearc_h = QtWidgets.QMenu(parent=self.menubar)
|
self.menuSearc_h = QtWidgets.QMenu(parent=self.menubar)
|
||||||
self.menuSearc_h.setObjectName("menuSearc_h")
|
self.menuSearc_h.setObjectName("menuSearc_h")
|
||||||
self.menuHelp = QtWidgets.QMenu(parent=self.menubar)
|
|
||||||
self.menuHelp.setObjectName("menuHelp")
|
|
||||||
MainWindow.setMenuBar(self.menubar)
|
MainWindow.setMenuBar(self.menubar)
|
||||||
self.statusbar = QtWidgets.QStatusBar(parent=MainWindow)
|
self.statusbar = QtWidgets.QStatusBar(parent=MainWindow)
|
||||||
self.statusbar.setEnabled(True)
|
self.statusbar.setEnabled(True)
|
||||||
@ -657,51 +655,51 @@ class Ui_MainWindow(object):
|
|||||||
self.actionSelect_duplicate_rows.setObjectName("actionSelect_duplicate_rows")
|
self.actionSelect_duplicate_rows.setObjectName("actionSelect_duplicate_rows")
|
||||||
self.actionReplace_files = QtGui.QAction(parent=MainWindow)
|
self.actionReplace_files = QtGui.QAction(parent=MainWindow)
|
||||||
self.actionReplace_files.setObjectName("actionReplace_files")
|
self.actionReplace_files.setObjectName("actionReplace_files")
|
||||||
self.menuFile.addAction(self.actionNewPlaylist)
|
self.menuFile.addSeparator()
|
||||||
self.menuFile.addAction(self.actionNew_from_template)
|
self.menuFile.addAction(self.actionInsertTrack)
|
||||||
self.menuFile.addAction(self.actionOpenPlaylist)
|
self.menuFile.addAction(self.actionRemove)
|
||||||
self.menuFile.addAction(self.actionClosePlaylist)
|
self.menuFile.addAction(self.actionInsertSectionHeader)
|
||||||
self.menuFile.addAction(self.actionRenamePlaylist)
|
self.menuFile.addSeparator()
|
||||||
self.menuFile.addAction(self.actionDeletePlaylist)
|
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.actionExport_playlist)
|
||||||
|
self.menuFile.addAction(self.actionDownload_CSV_of_played_tracks)
|
||||||
self.menuFile.addSeparator()
|
self.menuFile.addSeparator()
|
||||||
self.menuFile.addAction(self.actionSelect_duplicate_rows)
|
self.menuFile.addAction(self.actionSelect_duplicate_rows)
|
||||||
self.menuFile.addSeparator()
|
|
||||||
self.menuFile.addAction(self.actionMoveSelected)
|
self.menuFile.addAction(self.actionMoveSelected)
|
||||||
self.menuFile.addAction(self.actionMoveUnplayed)
|
self.menuFile.addAction(self.actionMoveUnplayed)
|
||||||
self.menuFile.addAction(self.actionDownload_CSV_of_played_tracks)
|
self.menuFile.addAction(self.action_Clear_selection)
|
||||||
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.menuPlaylist.addSeparator()
|
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.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.addSeparator()
|
||||||
self.menuPlaylist.addAction(self.actionInsertSectionHeader)
|
self.menuPlaylist.addAction(self.actionNew_from_template)
|
||||||
self.menuPlaylist.addAction(self.actionInsertTrack)
|
self.menuPlaylist.addAction(self.actionSave_as_template)
|
||||||
self.menuPlaylist.addAction(self.actionRemove)
|
|
||||||
self.menuPlaylist.addSeparator()
|
self.menuPlaylist.addSeparator()
|
||||||
self.menuPlaylist.addAction(self.actionSetNext)
|
self.menuPlaylist.addAction(self.actionReplace_files)
|
||||||
self.menuPlaylist.addAction(self.action_Clear_selection)
|
|
||||||
self.menuPlaylist.addSeparator()
|
self.menuPlaylist.addSeparator()
|
||||||
self.menuPlaylist.addAction(self.actionMark_for_moving)
|
self.menuPlaylist.addAction(self.actionDebug)
|
||||||
self.menuPlaylist.addAction(self.actionPaste)
|
self.menuPlaylist.addAction(self.action_About)
|
||||||
self.menuSearc_h.addAction(self.actionSearch)
|
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.addSeparator()
|
||||||
|
self.menuSearc_h.addAction(self.actionSearch)
|
||||||
self.menuSearc_h.addAction(self.actionSearch_title_in_Wikipedia)
|
self.menuSearc_h.addAction(self.actionSearch_title_in_Wikipedia)
|
||||||
self.menuSearc_h.addAction(self.actionSearch_title_in_Songfacts)
|
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.menuPlaylist.menuAction())
|
||||||
|
self.menubar.addAction(self.menuFile.menuAction())
|
||||||
self.menubar.addAction(self.menuSearc_h.menuAction())
|
self.menubar.addAction(self.menuSearc_h.menuAction())
|
||||||
self.menubar.addAction(self.menuHelp.menuAction())
|
|
||||||
|
|
||||||
self.retranslateUi(MainWindow)
|
self.retranslateUi(MainWindow)
|
||||||
self.tabPlaylist.setCurrentIndex(-1)
|
self.tabPlaylist.setCurrentIndex(-1)
|
||||||
@ -732,10 +730,9 @@ class Ui_MainWindow(object):
|
|||||||
self.label_silent_timer.setText(_translate("MainWindow", "00:00"))
|
self.label_silent_timer.setText(_translate("MainWindow", "00:00"))
|
||||||
self.btnFade.setText(_translate("MainWindow", " Fade"))
|
self.btnFade.setText(_translate("MainWindow", " Fade"))
|
||||||
self.btnStop.setText(_translate("MainWindow", " Stop"))
|
self.btnStop.setText(_translate("MainWindow", " Stop"))
|
||||||
self.menuFile.setTitle(_translate("MainWindow", "&Playlists"))
|
self.menuFile.setTitle(_translate("MainWindow", "&Playlist"))
|
||||||
self.menuPlaylist.setTitle(_translate("MainWindow", "Sho&wtime"))
|
self.menuPlaylist.setTitle(_translate("MainWindow", "&File"))
|
||||||
self.menuSearc_h.setTitle(_translate("MainWindow", "&Search"))
|
self.menuSearc_h.setTitle(_translate("MainWindow", "&Music"))
|
||||||
self.menuHelp.setTitle(_translate("MainWindow", "&Help"))
|
|
||||||
self.actionPlay_next.setText(_translate("MainWindow", "&Play next"))
|
self.actionPlay_next.setText(_translate("MainWindow", "&Play next"))
|
||||||
self.actionPlay_next.setShortcut(_translate("MainWindow", "Return"))
|
self.actionPlay_next.setShortcut(_translate("MainWindow", "Return"))
|
||||||
self.actionSkipToNext.setText(_translate("MainWindow", "Skip to &next"))
|
self.actionSkipToNext.setText(_translate("MainWindow", "Skip to &next"))
|
||||||
|
|||||||
125
archive/proxymodel.py
Executable file
125
archive/proxymodel.py
Executable 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())
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user