WIP query tabs
This commit is contained in:
parent
306ab103b6
commit
40756469ec
@ -1,15 +1,18 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Standard library imports
|
||||
from __future__ import annotations
|
||||
from slugify import slugify # type: ignore
|
||||
from typing import Callable, Optional
|
||||
import argparse
|
||||
from dataclasses import dataclass
|
||||
import datetime as dt
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.parse
|
||||
import webbrowser
|
||||
import yaml
|
||||
|
||||
# PyQt imports
|
||||
from PyQt6.QtCore import (
|
||||
@ -22,14 +25,15 @@ from PyQt6.QtGui import (
|
||||
QAction,
|
||||
QCloseEvent,
|
||||
QColor,
|
||||
QFont,
|
||||
QIcon,
|
||||
QKeySequence,
|
||||
QPalette,
|
||||
QShortcut,
|
||||
)
|
||||
from PyQt6.QtWidgets import (
|
||||
QAbstractItemView,
|
||||
QApplication,
|
||||
QCheckBox,
|
||||
QComboBox,
|
||||
QDialog,
|
||||
QFileDialog,
|
||||
@ -39,10 +43,11 @@ from PyQt6.QtWidgets import (
|
||||
QLineEdit,
|
||||
QListWidgetItem,
|
||||
QMainWindow,
|
||||
QMenu,
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
QSizePolicy,
|
||||
QTableView,
|
||||
QTableWidget,
|
||||
QTableWidgetItem,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
@ -61,21 +66,13 @@ from classes import (
|
||||
from config import Config
|
||||
from dialogs import TrackSelectDialog
|
||||
from file_importer import FileImporter
|
||||
from helpers import ask_yes_no, file_is_unreadable, ms_to_mmss, show_OK
|
||||
from helpers import file_is_unreadable
|
||||
from log import log
|
||||
from models import (
|
||||
db,
|
||||
Playdates,
|
||||
PlaylistRows,
|
||||
Playlists,
|
||||
Queries,
|
||||
Settings,
|
||||
Tracks,
|
||||
)
|
||||
from models import db, Playdates, PlaylistRows, Playlists, Settings, Tracks
|
||||
from music_manager import RowAndTrack, track_sequence
|
||||
from playlistmodel import PlaylistModel, PlaylistProxyModel
|
||||
from querylistmodel import QuerylistModel
|
||||
from playlists import PlaylistTab
|
||||
from ui import icons_rc # noqa F401
|
||||
from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist # type: ignore
|
||||
from ui.downloadcsv_ui import Ui_DateSelect # type: ignore
|
||||
from ui.main_window_header_ui import Ui_HeaderSection # type: ignore
|
||||
@ -83,18 +80,7 @@ from ui.main_window_playlist_ui import Ui_PlaylistSection # type: ignore
|
||||
from ui.main_window_footer_ui import Ui_FooterSection # type: ignore
|
||||
|
||||
from utilities import check_db, update_bitrates
|
||||
|
||||
|
||||
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)
|
||||
import helpers
|
||||
|
||||
|
||||
class Current:
|
||||
@ -110,6 +96,18 @@ class Current:
|
||||
)
|
||||
|
||||
|
||||
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 EditDeleteDialog(QDialog):
|
||||
def __init__(self, templates: list[tuple[str, int]]) -> None:
|
||||
super().__init__()
|
||||
@ -168,6 +166,129 @@ class EditDeleteDialog(QDialog):
|
||||
self.reject()
|
||||
|
||||
|
||||
@dataclass
|
||||
class ItemlistItem:
|
||||
id: int
|
||||
name: str
|
||||
favourite: bool = False
|
||||
|
||||
|
||||
class ItemlistManager(QDialog):
|
||||
def __init__(self, items: list[ItemlistItem], callbacks: ItemlistManagerCallbacks) -> None:
|
||||
super().__init__()
|
||||
self.setWindowTitle("Item Manager")
|
||||
self.setMinimumSize(600, 400)
|
||||
|
||||
self.items = items
|
||||
self.callbacks = callbacks
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
self.table = QTableWidget(len(items), 2, self)
|
||||
self.table.setHorizontalHeaderLabels(["Item", "Actions"])
|
||||
hh = self.table.horizontalHeader()
|
||||
if not hh:
|
||||
raise ApplicationError("ItemlistManager failed to create horizontalHeader")
|
||||
hh.setStretchLastSection(True)
|
||||
self.table.setColumnWidth(0, 200)
|
||||
self.table.setColumnWidth(1, 300)
|
||||
|
||||
self.populate_table()
|
||||
|
||||
layout.addWidget(self.table)
|
||||
|
||||
button_layout = QHBoxLayout()
|
||||
self.new_button = QPushButton("New")
|
||||
self.new_button.clicked.connect(self.new_item)
|
||||
button_layout.addWidget(self.new_button)
|
||||
|
||||
self.close_button = QPushButton("Close")
|
||||
self.close_button.clicked.connect(self.close)
|
||||
button_layout.addWidget(self.close_button)
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
def populate_table(self) -> None:
|
||||
"""Populates the table with items and action buttons."""
|
||||
self.table.setRowCount(len(self.items))
|
||||
|
||||
for row, item in enumerate(self.items):
|
||||
item_text = QTableWidgetItem(item.name)
|
||||
if item.favourite:
|
||||
item_text.setFont(QFont("Arial", weight=QFont.Weight.Bold))
|
||||
self.table.setItem(row, 0, item_text)
|
||||
|
||||
# Action Buttons and Checkbox in a widget
|
||||
widget = QWidget()
|
||||
h_layout = QHBoxLayout(widget)
|
||||
h_layout.setContentsMargins(0, 0, 0, 0)
|
||||
h_layout.setSpacing(5)
|
||||
|
||||
rename_button = QPushButton("Rename")
|
||||
rename_button.clicked.connect(lambda _, i=item.id: self.rename_item(i))
|
||||
h_layout.addWidget(rename_button)
|
||||
|
||||
edit_button = QPushButton("Edit")
|
||||
edit_button.clicked.connect(lambda _, i=item.id: self.edit_item(i))
|
||||
h_layout.addWidget(edit_button)
|
||||
|
||||
delete_button = QPushButton("Delete")
|
||||
delete_button.clicked.connect(lambda _, i=item.id: self.delete_item(i))
|
||||
h_layout.addWidget(delete_button)
|
||||
|
||||
fav_checkbox = QCheckBox()
|
||||
fav_checkbox.setChecked(item.favourite)
|
||||
fav_checkbox.stateChanged.connect(
|
||||
lambda state, cb=fav_checkbox, i=item.id: self.toggle_favourite(
|
||||
i, cb.isChecked()
|
||||
)
|
||||
)
|
||||
h_layout.addWidget(fav_checkbox)
|
||||
|
||||
self.table.setCellWidget(row, 1, widget)
|
||||
|
||||
def rename_item(self, item_id: int) -> None:
|
||||
print(f"Rename item {item_id}")
|
||||
|
||||
def edit_item(self, item_id: int) -> None:
|
||||
print(f"Edit item {item_id}")
|
||||
self.callbacks.edit(item_id)
|
||||
|
||||
def delete_item(self, item_id: int) -> None:
|
||||
print(f"Delete item {item_id}")
|
||||
self.callbacks.delete(item_id)
|
||||
|
||||
def toggle_favourite(self, item_id: int, checked: bool) -> None:
|
||||
print(f"Toggle favourite for item {item_id}: {checked}")
|
||||
self.callbacks.favourite(item_id, checked)
|
||||
|
||||
for row in range(self.table.rowCount()):
|
||||
item = self.table.item(row, 0)
|
||||
if item and self.items[row].id == item_id:
|
||||
font = QFont(
|
||||
"Arial",
|
||||
weight=QFont.Weight.Bold if checked else QFont.Weight.Normal,
|
||||
)
|
||||
item.setFont(font)
|
||||
self.items[row].favourite = checked
|
||||
break
|
||||
|
||||
def new_item(self) -> None:
|
||||
print("New item")
|
||||
|
||||
# test_items = [
|
||||
# {"id": 1, "text": "Item 1", "favourite": False},
|
||||
# {"id": 2, "text": "Item 2", "favourite": True},
|
||||
# {"id": 3, "text": "Item 3", "favourite": False}
|
||||
# ]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ItemlistManagerCallbacks:
|
||||
edit: Callable[[int], None]
|
||||
delete: Callable[[int], None]
|
||||
favourite: Callable[[int, bool], None]
|
||||
|
||||
|
||||
class PreviewManager:
|
||||
"""
|
||||
Manage track preview player
|
||||
@ -279,234 +400,6 @@ class PreviewManager:
|
||||
self.start_time = None
|
||||
|
||||
|
||||
class QueryDialog(QDialog):
|
||||
"""Dialog box to handle selecting track from a SQL query"""
|
||||
|
||||
def __init__(self, session: Session) -> None:
|
||||
super().__init__()
|
||||
self.session = session
|
||||
|
||||
# Build a list of (query-name, playlist-id) tuples
|
||||
self.selected_tracks: list[int] = []
|
||||
|
||||
self.query_list: list[tuple[str, int]] = []
|
||||
self.query_list.append((Config.NO_QUERY_NAME, 0))
|
||||
for query in Queries.get_all(self.session):
|
||||
self.query_list.append((query.name, query.id))
|
||||
|
||||
self.setWindowTitle("Query Selector")
|
||||
|
||||
# Create label
|
||||
query_label = QLabel("Query:")
|
||||
|
||||
# Top layout (Query label, combo box, and info label)
|
||||
top_layout = QHBoxLayout()
|
||||
|
||||
# Query label
|
||||
query_label = QLabel("Query:")
|
||||
top_layout.addWidget(query_label)
|
||||
|
||||
# Combo Box with fixed width
|
||||
self.combo_box = QComboBox()
|
||||
self.combo_box.setFixedWidth(150) # Adjust as necessary for 20 characters
|
||||
for text, id_ in self.query_list:
|
||||
self.combo_box.addItem(text, id_)
|
||||
top_layout.addWidget(self.combo_box)
|
||||
|
||||
# Information label (two-row height, wrapping)
|
||||
self.description_label = QLabel("")
|
||||
self.description_label.setWordWrap(True)
|
||||
self.description_label.setMinimumHeight(40) # Approximate height for two rows
|
||||
self.description_label.setSizePolicy(
|
||||
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred
|
||||
)
|
||||
top_layout.addWidget(self.description_label)
|
||||
|
||||
# Table (middle part)
|
||||
self.table_view = QTableView()
|
||||
self.table_view.setSelectionMode(
|
||||
QAbstractItemView.SelectionMode.ExtendedSelection
|
||||
)
|
||||
self.table_view.setSelectionBehavior(
|
||||
QAbstractItemView.SelectionBehavior.SelectRows
|
||||
)
|
||||
self.table_view.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||
self.table_view.setAlternatingRowColors(True)
|
||||
self.table_view.setVerticalScrollMode(
|
||||
QAbstractItemView.ScrollMode.ScrollPerPixel
|
||||
)
|
||||
self.table_view.clicked.connect(self.handle_row_click)
|
||||
|
||||
# Bottom layout (buttons)
|
||||
bottom_layout = QHBoxLayout()
|
||||
bottom_layout.addStretch() # Push buttons to the right
|
||||
|
||||
self.add_tracks_button = QPushButton("Add tracks")
|
||||
self.add_tracks_button.setEnabled(False) # Disabled by default
|
||||
self.add_tracks_button.clicked.connect(self.add_tracks_clicked)
|
||||
bottom_layout.addWidget(self.add_tracks_button)
|
||||
|
||||
self.cancel_button = QPushButton("Cancel")
|
||||
self.cancel_button.clicked.connect(self.cancel_clicked)
|
||||
bottom_layout.addWidget(self.cancel_button)
|
||||
|
||||
# Main layout
|
||||
main_layout = QVBoxLayout()
|
||||
main_layout.addLayout(top_layout)
|
||||
main_layout.addWidget(self.table_view)
|
||||
main_layout.addLayout(bottom_layout)
|
||||
|
||||
self.combo_box.currentIndexChanged.connect(self.query_changed)
|
||||
self.setLayout(main_layout)
|
||||
|
||||
# Stretch last column *after* setting column widths which is
|
||||
# *much* faster
|
||||
h_header = self.table_view.horizontalHeader()
|
||||
if h_header:
|
||||
h_header.sectionResized.connect(self._column_resize)
|
||||
h_header.setStretchLastSection(True)
|
||||
# Resize on vertical header click
|
||||
v_header = self.table_view.verticalHeader()
|
||||
if v_header:
|
||||
v_header.setMinimumSectionSize(5)
|
||||
v_header.sectionHandleDoubleClicked.disconnect()
|
||||
v_header.sectionHandleDoubleClicked.connect(
|
||||
self.table_view.resizeRowToContents
|
||||
)
|
||||
|
||||
self.set_window_size()
|
||||
self.resizeRowsToContents()
|
||||
|
||||
def add_tracks_clicked(self):
|
||||
self.selected_tracks = self.table_view.model().get_selected_track_ids()
|
||||
self.accept()
|
||||
|
||||
def cancel_clicked(self):
|
||||
self.selected_tracks = []
|
||||
self.reject()
|
||||
|
||||
def closeEvent(self, event: QCloseEvent | None) -> None:
|
||||
"""
|
||||
Record size and columns
|
||||
"""
|
||||
|
||||
self.save_sizes()
|
||||
super().closeEvent(event)
|
||||
|
||||
def accept(self) -> None:
|
||||
self.save_sizes()
|
||||
super().accept()
|
||||
|
||||
def reject(self) -> None:
|
||||
self.save_sizes()
|
||||
super().reject()
|
||||
|
||||
def save_sizes(self) -> None:
|
||||
"""
|
||||
Save window size
|
||||
"""
|
||||
|
||||
# Save dialog box attributes
|
||||
attributes_to_save = dict(
|
||||
querylist_height=self.height(),
|
||||
querylist_width=self.width(),
|
||||
querylist_x=self.x(),
|
||||
querylist_y=self.y(),
|
||||
)
|
||||
for name, value in attributes_to_save.items():
|
||||
record = Settings.get_setting(self.session, name)
|
||||
record.f_int = value
|
||||
|
||||
header = self.table_view.horizontalHeader()
|
||||
if header is None:
|
||||
return
|
||||
column_count = header.count()
|
||||
if column_count < 2:
|
||||
return
|
||||
for column_number in range(column_count - 1):
|
||||
attr_name = f"querylist_col_{column_number}_width"
|
||||
record = Settings.get_setting(self.session, attr_name)
|
||||
record.f_int = self.table_view.columnWidth(column_number)
|
||||
|
||||
self.session.commit()
|
||||
|
||||
def _column_resize(self, column_number: int, _old: int, _new: int) -> None:
|
||||
"""
|
||||
Called when column width changes.
|
||||
"""
|
||||
|
||||
header = self.table_view.horizontalHeader()
|
||||
if not header:
|
||||
return
|
||||
|
||||
# Resize rows if necessary
|
||||
self.resizeRowsToContents()
|
||||
|
||||
def resizeRowsToContents(self):
|
||||
header = self.table_view.verticalHeader()
|
||||
model = self.table_view.model()
|
||||
if model:
|
||||
for row in model.rowCount():
|
||||
hint = self.sizeHintForRow(row)
|
||||
header.resizeSection(row, hint)
|
||||
|
||||
def query_changed(self, idx: int) -> None:
|
||||
"""
|
||||
Called when user selects query
|
||||
"""
|
||||
|
||||
# Get query
|
||||
query = self.session.get(Queries, idx)
|
||||
if not query:
|
||||
return
|
||||
|
||||
# Create model
|
||||
base_model = QuerylistModel(self.session, query.sql)
|
||||
|
||||
# Create table
|
||||
self.table_view.setModel(base_model)
|
||||
self.set_column_sizes()
|
||||
self.description_label.setText(query.description)
|
||||
|
||||
def handle_row_click(self, index):
|
||||
self.table_view.model().toggle_row_selection(index.row())
|
||||
self.table_view.clearSelection()
|
||||
|
||||
# Enable 'Add tracks' button only when a row is selected
|
||||
selected = self.table_view.model().get_selected_track_ids()
|
||||
self.add_tracks_button.setEnabled(selected != [])
|
||||
|
||||
def set_window_size(self) -> None:
|
||||
"""Set window sizes"""
|
||||
|
||||
x = Settings.get_setting(self.session, "querylist_x").f_int or 100
|
||||
y = Settings.get_setting(self.session, "querylist_y").f_int or 100
|
||||
width = Settings.get_setting(self.session, "querylist_width").f_int or 100
|
||||
height = Settings.get_setting(self.session, "querylist_height").f_int or 100
|
||||
self.setGeometry(x, y, width, height)
|
||||
|
||||
def set_column_sizes(self) -> None:
|
||||
"""Set column sizes"""
|
||||
|
||||
header = self.table_view.horizontalHeader()
|
||||
if header is None:
|
||||
return
|
||||
column_count = header.count()
|
||||
if column_count < 2:
|
||||
return
|
||||
|
||||
# Last column is set to stretch so ignore it here
|
||||
for column_number in range(column_count - 1):
|
||||
attr_name = f"querylist_col_{column_number}_width"
|
||||
record = Settings.get_setting(self.session, attr_name)
|
||||
if record.f_int is not None:
|
||||
self.table_view.setColumnWidth(column_number, record.f_int)
|
||||
else:
|
||||
self.table_view.setColumnWidth(
|
||||
column_number, Config.DEFAULT_COLUMN_WIDTH
|
||||
)
|
||||
|
||||
|
||||
class SelectPlaylistDialog(QDialog):
|
||||
def __init__(self, parent=None, playlists=None, session=None):
|
||||
super().__init__()
|
||||
@ -713,106 +606,85 @@ class Window(QMainWindow):
|
||||
return action
|
||||
|
||||
def create_menu_bar(self):
|
||||
"""Dynamically creates the menu bar from a YAML file."""
|
||||
menu_bar = self.menuBar()
|
||||
|
||||
# File Menu
|
||||
file_menu = menu_bar.addMenu("&File")
|
||||
file_menu.addAction(
|
||||
self.create_action("Open Playlist", self.open_playlist, "Ctrl+O")
|
||||
)
|
||||
file_menu.addAction(self.create_action("New Playlist", self.new_playlist))
|
||||
file_menu.addAction(
|
||||
self.create_action("Close Playlist", self.close_playlist_tab)
|
||||
)
|
||||
file_menu.addAction(self.create_action("Rename Playlist", self.rename_playlist))
|
||||
file_menu.addAction(self.create_action("Delete Playlist", self.delete_playlist))
|
||||
file_menu.addSeparator()
|
||||
file_menu.addAction(self.create_action("Open Querylist", self.open_querylist))
|
||||
file_menu.addSeparator()
|
||||
file_menu.addAction(
|
||||
self.create_action("Save as Template", self.save_as_template)
|
||||
)
|
||||
file_menu.addAction(
|
||||
self.create_action("Manage Templates", self.manage_templates)
|
||||
)
|
||||
file_menu.addSeparator()
|
||||
file_menu.addAction(
|
||||
self.create_action(
|
||||
"Import Files", self.import_files_wrapper, "Ctrl+Shift+I"
|
||||
)
|
||||
)
|
||||
file_menu.addSeparator()
|
||||
file_menu.addAction(self.create_action("Exit", self.close))
|
||||
# Load menu structure from YAML file
|
||||
with open("menu.yaml", "r") as file:
|
||||
menu_data = yaml.safe_load(file)
|
||||
|
||||
# Playlist Menu
|
||||
playlist_menu = menu_bar.addMenu("&Playlist")
|
||||
playlist_menu.addSeparator()
|
||||
playlist_menu.addAction(
|
||||
self.create_action("Insert Track", self.insert_track, "Ctrl+T")
|
||||
)
|
||||
playlist_menu.addAction(
|
||||
self.create_action("Insert Section Header", self.insert_header, "Ctrl+H")
|
||||
)
|
||||
playlist_menu.addSeparator()
|
||||
playlist_menu.addAction(
|
||||
self.create_action("Mark for Moving", self.mark_rows_for_moving, "Ctrl+C")
|
||||
)
|
||||
playlist_menu.addAction(self.create_action("Paste", self.paste_rows, "Ctrl+V"))
|
||||
playlist_menu.addSeparator()
|
||||
playlist_menu.addAction(
|
||||
self.create_action("Export Playlist", self.export_playlist_tab)
|
||||
)
|
||||
playlist_menu.addAction(
|
||||
self.create_action(
|
||||
"Download CSV of Played Tracks", self.download_played_tracks
|
||||
)
|
||||
)
|
||||
playlist_menu.addSeparator()
|
||||
playlist_menu.addAction(
|
||||
self.create_action(
|
||||
"Select Duplicate Rows",
|
||||
lambda: self.active_tab().select_duplicate_rows(),
|
||||
)
|
||||
)
|
||||
playlist_menu.addAction(self.create_action("Move Selected", self.move_selected))
|
||||
playlist_menu.addAction(self.create_action("Move Unplayed", self.move_unplayed))
|
||||
self.menu_actions = {} # Store reference for enabling/disabling actions
|
||||
self.dynamic_submenus = {} # Store submenus for dynamic population
|
||||
|
||||
# Clear Selection with Escape key. Save in module so we can
|
||||
# enable/disable it later
|
||||
self.action_Clear_selection = self.create_action(
|
||||
"Clear Selection", self.clear_selection, "Esc"
|
||||
)
|
||||
playlist_menu.addAction(self.action_Clear_selection)
|
||||
for menu_item in menu_data["menus"]:
|
||||
menu = menu_bar.addMenu(menu_item["title"])
|
||||
|
||||
# Music Menu
|
||||
music_menu = menu_bar.addMenu("&Music")
|
||||
music_menu.addAction(
|
||||
self.create_action("Set Next", self.set_selected_track_next, "Ctrl+N")
|
||||
)
|
||||
music_menu.addAction(self.create_action("Play Next", self.play_next, "Return"))
|
||||
music_menu.addAction(self.create_action("Fade", self.fade, "Ctrl+Z"))
|
||||
music_menu.addAction(self.create_action("Stop", self.stop, "Ctrl+Alt+S"))
|
||||
music_menu.addAction(self.create_action("Resume", self.resume, "Ctrl+R"))
|
||||
music_menu.addAction(
|
||||
self.create_action("Skip to Next", self.play_next, "Ctrl+Alt+Return")
|
||||
)
|
||||
music_menu.addSeparator()
|
||||
music_menu.addAction(self.create_action("Search", self.search_playlist, "/"))
|
||||
music_menu.addAction(
|
||||
self.create_action(
|
||||
"Search Title in Wikipedia", self.lookup_row_in_wikipedia, "Ctrl+W"
|
||||
)
|
||||
)
|
||||
music_menu.addAction(
|
||||
self.create_action(
|
||||
"Search Title in Songfacts", self.lookup_row_in_songfacts, "Ctrl+S"
|
||||
)
|
||||
)
|
||||
for action_item in menu_item["actions"]:
|
||||
if "separator" in action_item and action_item["separator"]:
|
||||
menu.addSeparator()
|
||||
continue
|
||||
|
||||
# Help Menu
|
||||
help_menu = menu_bar.addMenu("Help")
|
||||
help_menu.addAction(self.create_action("About", self.about))
|
||||
help_menu.addAction(self.create_action("Debug", self.debug))
|
||||
# Check whether this is a submenu first
|
||||
if action_item.get("submenu"):
|
||||
submenu = QMenu(action_item["text"], self)
|
||||
menu.addMenu(submenu)
|
||||
|
||||
# Store submenu reference for dynamic population
|
||||
self.dynamic_submenus[action_item["handler"]] = submenu
|
||||
submenu.aboutToShow.connect(self.populate_dynamic_submenu)
|
||||
continue # Skip the rest of the loop (no handler needed)
|
||||
|
||||
# Now check for a normal menu action
|
||||
handler = getattr(self, action_item["handler"], None)
|
||||
if handler is None:
|
||||
print(f"Warning: No handler found for {action_item['text']}")
|
||||
continue
|
||||
|
||||
action = self.create_action(
|
||||
action_item["text"], handler, action_item.get("shortcut")
|
||||
)
|
||||
# Store reference to "Clear Selection" so we can enable/disable it
|
||||
if action_item.get("store_reference"):
|
||||
self.menu_actions[action_item["handler"]] = action
|
||||
|
||||
menu.addAction(action)
|
||||
|
||||
def populate_dynamic_submenu(self):
|
||||
"""Dynamically populates submenus when they are selected."""
|
||||
submenu = self.sender() # Get the submenu that triggered the event
|
||||
|
||||
# Find which submenu it is
|
||||
for key, stored_submenu in self.dynamic_submenus.items():
|
||||
if submenu == stored_submenu:
|
||||
submenu.clear()
|
||||
# Dynamically call the correct function
|
||||
items = getattr(self, f"get_{key}_items")()
|
||||
for item in items:
|
||||
action = QAction(item["text"], self)
|
||||
action.triggered.connect(
|
||||
lambda _, i=item["handler"]: getattr(self, i)()
|
||||
)
|
||||
submenu.addAction(action)
|
||||
break
|
||||
|
||||
def get_new_playlist_dynamic_submenu_items(self):
|
||||
"""Returns dynamically generated menu items for Submenu 1."""
|
||||
return [
|
||||
{"text": "Option A", "handler": "option_a_handler"},
|
||||
{"text": "Option B", "handler": "option_b_handler"},
|
||||
]
|
||||
|
||||
def get_query_dynamic_submenu_items(self):
|
||||
"""Returns dynamically generated menu items for Submenu 2."""
|
||||
return [
|
||||
{"text": "Action X", "handler": "action_x_handler"},
|
||||
{"text": "Action Y", "handler": "action_y_handler"},
|
||||
]
|
||||
|
||||
def select_duplicate_rows(self) -> None:
|
||||
"""Call playlist to select duplicate rows"""
|
||||
|
||||
self.active_tab().select_duplicate_rows()
|
||||
|
||||
def about(self) -> None:
|
||||
"""Get git tag and database name"""
|
||||
@ -868,8 +740,8 @@ class Window(QMainWindow):
|
||||
# Don't allow window to close when a track is playing
|
||||
if track_sequence.current and track_sequence.current.is_playing():
|
||||
event.ignore()
|
||||
self.show_warning(
|
||||
"Track playing", "Can't close application while track is playing"
|
||||
helpers.show_warning(
|
||||
self, "Track playing", "Can't close application while track is playing"
|
||||
)
|
||||
else:
|
||||
with db.Session() as session:
|
||||
@ -926,7 +798,7 @@ class Window(QMainWindow):
|
||||
current_track_playlist_id = track_sequence.current.playlist_id
|
||||
if current_track_playlist_id:
|
||||
if closing_tab_playlist_id == current_track_playlist_id:
|
||||
show_OK(
|
||||
helpers.show_OK(
|
||||
"Current track", "Can't close current track playlist", self
|
||||
)
|
||||
return False
|
||||
@ -936,7 +808,7 @@ class Window(QMainWindow):
|
||||
next_track_playlist_id = track_sequence.next.playlist_id
|
||||
if next_track_playlist_id:
|
||||
if closing_tab_playlist_id == next_track_playlist_id:
|
||||
show_OK(
|
||||
helpers.show_OK(
|
||||
"Next track", "Can't close next track playlist", self
|
||||
)
|
||||
return False
|
||||
@ -1052,7 +924,7 @@ class Window(QMainWindow):
|
||||
playlist_id = self.current.playlist_id
|
||||
playlist = session.get(Playlists, playlist_id)
|
||||
if playlist:
|
||||
if ask_yes_no(
|
||||
if helpers.ask_yes_no(
|
||||
"Delete playlist",
|
||||
f"Delete playlist '{playlist.name}': " "Are you sure?",
|
||||
):
|
||||
@ -1103,7 +975,8 @@ class Window(QMainWindow):
|
||||
|
||||
log.debug(f"enable_escape({enabled=})")
|
||||
|
||||
self.action_Clear_selection.setEnabled(enabled)
|
||||
if "clear_selection" in self.menu_actions:
|
||||
self.menu_actions["clear_selection"].setEnabled(enabled)
|
||||
|
||||
def end_of_track_actions(self) -> None:
|
||||
"""
|
||||
@ -1297,42 +1170,60 @@ class Window(QMainWindow):
|
||||
Delete / edit templates
|
||||
"""
|
||||
|
||||
# Build a list of (template-name, playlist-id) tuples
|
||||
template_list: list[tuple[str, int]] = []
|
||||
def edit(template_id: int) -> None:
|
||||
"""Edit template"""
|
||||
|
||||
print(f"manage_templates.edit({template_id=}")
|
||||
|
||||
def delete(template_id: int) -> None:
|
||||
"""delete template"""
|
||||
|
||||
print(f"manage_templates.delete({template_id=}")
|
||||
|
||||
def favourite(template_id: int, favourite: bool) -> None:
|
||||
"""favourite template"""
|
||||
|
||||
print(f"manage_templates.favourite({template_id=}")
|
||||
|
||||
callbacks = ItemlistManagerCallbacks(edit, delete, favourite)
|
||||
|
||||
# Build a list of templates
|
||||
template_list: list[ItemlistItem] = []
|
||||
|
||||
with db.Session() as session:
|
||||
for template in Playlists.get_all_templates(session):
|
||||
template_list.append((template.name, template.id))
|
||||
# TODO: need to add in favourites
|
||||
template_list.append(ItemlistItem(name=template.name, id=template.id))
|
||||
|
||||
# Get user's selection
|
||||
dlg = EditDeleteDialog(template_list)
|
||||
if not dlg.exec():
|
||||
return # User cancelled
|
||||
# # Get user's selection
|
||||
# dlg = EditDeleteDialog(template_list)
|
||||
# if not dlg.exec():
|
||||
# return # User cancelled
|
||||
|
||||
action, template_id = dlg.selection
|
||||
# action, template_id = dlg.selection
|
||||
|
||||
playlist = session.get(Playlists, template_id)
|
||||
if not playlist:
|
||||
log.error(f"Error opening {template_id=}")
|
||||
# playlist = session.get(Playlists, template_id)
|
||||
# if not playlist:
|
||||
# log.error(f"Error opening {template_id=}")
|
||||
|
||||
if action == "Edit":
|
||||
# Simply load the template as a playlist. Any changes
|
||||
# made will persist
|
||||
idx = self.create_playlist_tab(playlist)
|
||||
self.playlist_section.tabPlaylist.setCurrentIndex(idx)
|
||||
# if action == "Edit":
|
||||
# # Simply load the template as a playlist. Any changes
|
||||
# # made will persist
|
||||
# idx = self.create_playlist_tab(playlist)
|
||||
# self.playlist_section.tabPlaylist.setCurrentIndex(idx)
|
||||
|
||||
elif action == "Delete":
|
||||
if ask_yes_no(
|
||||
"Delete template",
|
||||
f"Delete template '{playlist.name}': " "Are you sure?",
|
||||
):
|
||||
if self.close_playlist_tab():
|
||||
playlist.delete(session)
|
||||
session.commit()
|
||||
else:
|
||||
raise ApplicationError(
|
||||
f"Unrecognised action from EditDeleteDialog: {action=}"
|
||||
)
|
||||
# elif action == "Delete":
|
||||
# if helpers.ask_yes_no(
|
||||
# "Delete template",
|
||||
# f"Delete template '{playlist.name}': " "Are you sure?",
|
||||
# ):
|
||||
# if self.close_playlist_tab():
|
||||
# playlist.delete(session)
|
||||
# session.commit()
|
||||
# else:
|
||||
# raise ApplicationError(
|
||||
# f"Unrecognised action from EditDeleteDialog: {action=}"
|
||||
# )
|
||||
|
||||
def mark_rows_for_moving(self) -> None:
|
||||
"""
|
||||
@ -1474,21 +1365,6 @@ class Window(QMainWindow):
|
||||
|
||||
self.playlist_section.tabPlaylist.setCurrentIndex(idx)
|
||||
|
||||
def open_querylist(self) -> None:
|
||||
"""Open existing querylist"""
|
||||
|
||||
try:
|
||||
with db.Session() as session:
|
||||
dlg = QueryDialog(session)
|
||||
if dlg.exec():
|
||||
new_row_number = self.current_row_or_end()
|
||||
for track_id in dlg.selected_tracks:
|
||||
self.current.base_model.insert_row(new_row_number, track_id)
|
||||
else:
|
||||
return # User cancelled
|
||||
except ApplicationError as e:
|
||||
self.show_warning("Query error", f"Your query gave an error:\n\n{e}")
|
||||
|
||||
def open_songfacts_browser(self, title: str) -> None:
|
||||
"""Search Songfacts for title"""
|
||||
|
||||
@ -1761,7 +1637,7 @@ class Window(QMainWindow):
|
||||
msg = "Hit return to play next track now"
|
||||
else:
|
||||
msg = "Press tab to select Yes and hit return to play next track"
|
||||
if not ask_yes_no(
|
||||
if not helpers.ask_yes_no(
|
||||
"Play next track",
|
||||
msg,
|
||||
default_yes=default_yes,
|
||||
@ -1831,12 +1707,12 @@ class Window(QMainWindow):
|
||||
template_name = dlg.textValue()
|
||||
if template_name not in template_names:
|
||||
break
|
||||
self.show_warning(
|
||||
"Duplicate template", "Template name already in use"
|
||||
helpers.show_warning(
|
||||
self, "Duplicate template", "Template name already in use"
|
||||
)
|
||||
Playlists.save_as_template(session, self.current.playlist_id, template_name)
|
||||
session.commit()
|
||||
show_OK("Template", "Template saved", self)
|
||||
helpers.show_OK("Template", "Template saved", self)
|
||||
|
||||
def search_playlist(self) -> None:
|
||||
"""Show text box to search playlist"""
|
||||
@ -1982,7 +1858,8 @@ class Window(QMainWindow):
|
||||
if Playlists.name_is_available(session, proposed_name):
|
||||
return proposed_name
|
||||
else:
|
||||
self.show_warning(
|
||||
helpers.show_warning(
|
||||
self,
|
||||
"Name in use",
|
||||
f"There's already a playlist called '{proposed_name}'",
|
||||
)
|
||||
@ -2085,16 +1962,16 @@ class Window(QMainWindow):
|
||||
if track_sequence.current and track_sequence.current.is_playing():
|
||||
# Elapsed time
|
||||
self.header_section.label_elapsed_timer.setText(
|
||||
ms_to_mmss(track_sequence.current.time_playing())
|
||||
helpers.ms_to_mmss(track_sequence.current.time_playing())
|
||||
+ " / "
|
||||
+ ms_to_mmss(track_sequence.current.duration)
|
||||
+ helpers.ms_to_mmss(track_sequence.current.duration)
|
||||
)
|
||||
|
||||
# Time to fade
|
||||
time_to_fade = track_sequence.current.time_to_fade()
|
||||
time_to_silence = track_sequence.current.time_to_silence()
|
||||
self.footer_section.label_fade_timer.setText(
|
||||
ms_to_mmss(time_to_fade)
|
||||
helpers.ms_to_mmss(time_to_fade)
|
||||
)
|
||||
|
||||
# If silent in the next 5 seconds, put warning colour on
|
||||
@ -2133,7 +2010,7 @@ class Window(QMainWindow):
|
||||
self.footer_section.frame_fade.setStyleSheet("")
|
||||
|
||||
self.footer_section.label_silent_timer.setText(
|
||||
ms_to_mmss(time_to_silence)
|
||||
helpers.ms_to_mmss(time_to_silence)
|
||||
)
|
||||
|
||||
def update_headers(self) -> None:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user