295 lines
9.8 KiB
Python
295 lines
9.8 KiB
Python
# cart_widget.py
|
|
|
|
from PySide6.QtWidgets import (
|
|
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
|
QPushButton, QFrame, QSizePolicy, QGridLayout
|
|
)
|
|
from PySide6.QtCore import Qt, QTimer
|
|
from PySide6.QtGui import QFont
|
|
import os
|
|
|
|
|
|
# ─────────────────────────────────────────
|
|
# SINGLE CART SLOT
|
|
# ─────────────────────────────────────────
|
|
|
|
class CartSlot(QFrame):
|
|
def __init__(self, cart_key: str, slot_number: int, engine, parent=None):
|
|
super().__init__(parent)
|
|
self.cart_key = cart_key
|
|
self.slot_number = slot_number
|
|
self.engine = engine
|
|
|
|
self.setFrameStyle(QFrame.Box | QFrame.Raised)
|
|
self.setLineWidth(1)
|
|
self.setStyleSheet("""
|
|
QFrame {
|
|
background-color: #1e1e1e;
|
|
border: 1px solid #444;
|
|
border-radius: 5px;
|
|
}
|
|
""")
|
|
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
self.setFixedHeight(110)
|
|
|
|
self._build_ui()
|
|
self._connect_signals()
|
|
|
|
self.timer = QTimer(self)
|
|
self.timer.setInterval(100)
|
|
self.timer.timeout.connect(self._refresh_display)
|
|
self.timer.start()
|
|
|
|
def _build_ui(self):
|
|
root = QHBoxLayout(self)
|
|
root.setContentsMargins(8, 6, 8, 6)
|
|
root.setSpacing(8)
|
|
|
|
badge = QLabel(str(self.slot_number))
|
|
badge.setFixedSize(28, 28)
|
|
badge.setAlignment(Qt.AlignCenter)
|
|
badge.setFont(QFont("Arial", 12, QFont.Bold))
|
|
badge.setStyleSheet("""
|
|
background-color: #cc3300;
|
|
color: white;
|
|
border-radius: 14px;
|
|
border: none;
|
|
""")
|
|
root.addWidget(badge, alignment=Qt.AlignVCenter)
|
|
|
|
lcd_frame = QFrame()
|
|
lcd_frame.setStyleSheet("""
|
|
QFrame {
|
|
background-color: #0a1a0a;
|
|
border: 1px solid #1a2a1a;
|
|
border-radius: 3px;
|
|
}
|
|
""")
|
|
lcd_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
|
|
|
lcd_layout = QVBoxLayout(lcd_frame)
|
|
lcd_layout.setContentsMargins(6, 4, 6, 4)
|
|
lcd_layout.setSpacing(2)
|
|
|
|
self.track_label = QLabel("EMPTY")
|
|
self.track_label.setFont(QFont("Courier New", 9, QFont.Bold))
|
|
self.track_label.setStyleSheet(
|
|
"color: #00ff41; background: transparent; border: none;"
|
|
)
|
|
self.track_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
|
|
|
time_row = QHBoxLayout()
|
|
|
|
self.elapsed_label = QLabel("00:00")
|
|
self.elapsed_label.setFont(QFont("Courier New", 14, QFont.Bold))
|
|
self.elapsed_label.setStyleSheet(
|
|
"color: #00ff41; background: transparent; border: none;"
|
|
)
|
|
|
|
self.remaining_label = QLabel("-00:00")
|
|
self.remaining_label.setFont(QFont("Courier New", 10))
|
|
self.remaining_label.setStyleSheet(
|
|
"color: #00cc33; background: transparent; border: none;"
|
|
)
|
|
self.remaining_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
|
|
|
time_row.addWidget(self.elapsed_label)
|
|
time_row.addStretch()
|
|
time_row.addWidget(self.remaining_label)
|
|
|
|
self.status_label = QLabel("STOPPED")
|
|
self.status_label.setFont(QFont("Courier New", 8))
|
|
self.status_label.setStyleSheet(
|
|
"color: #008822; background: transparent; border: none;"
|
|
)
|
|
|
|
lcd_layout.addWidget(self.track_label)
|
|
lcd_layout.addLayout(time_row)
|
|
lcd_layout.addWidget(self.status_label)
|
|
|
|
root.addWidget(lcd_frame)
|
|
|
|
btn_col = QVBoxLayout()
|
|
btn_col.setSpacing(4)
|
|
|
|
self.btn_play = QPushButton("▶")
|
|
self.btn_play.setFixedSize(60, 44)
|
|
self.btn_play.setFont(QFont("Arial", 13, QFont.Bold))
|
|
self.btn_play.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #1a6b3a;
|
|
color: #00ff41;
|
|
border: 1px solid #555;
|
|
border-radius: 3px;
|
|
}
|
|
QPushButton:hover { background-color: #2a7b4a; }
|
|
QPushButton:pressed { background-color: #0a5a2a; }
|
|
""")
|
|
|
|
self.btn_eject = QPushButton("⏏")
|
|
self.btn_eject.setFixedSize(60, 30)
|
|
self.btn_eject.setFont(QFont("Arial", 11, QFont.Bold))
|
|
self.btn_eject.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #8a1a1a;
|
|
color: #ff4444;
|
|
border: 1px solid #555;
|
|
border-radius: 3px;
|
|
}
|
|
QPushButton:hover { background-color: #9a2a2a; }
|
|
QPushButton:pressed { background-color: #6a0a0a; }
|
|
""")
|
|
|
|
btn_col.addWidget(self.btn_play)
|
|
btn_col.addWidget(self.btn_eject)
|
|
|
|
root.addLayout(btn_col)
|
|
|
|
def _connect_signals(self):
|
|
self.btn_play.clicked.connect(self._toggle_play)
|
|
self.btn_eject.clicked.connect(self._eject)
|
|
|
|
def load_track(self, filepath: str):
|
|
self.engine.load_track(self.cart_key, filepath)
|
|
name = os.path.basename(filepath)
|
|
short = name[:28] + "..." if len(name) > 28 else name
|
|
self.track_label.setText(short)
|
|
self.status_label.setText("LOADED")
|
|
self.elapsed_label.setText("00:00")
|
|
self.remaining_label.setText("-00:00")
|
|
|
|
# ── PUBLIC MIDI METHOD ────────────────────────────────────────────────
|
|
|
|
def toggle(self):
|
|
"""
|
|
MIDI toggle - playing → stop and reset to start.
|
|
Stopped/paused → play.
|
|
"""
|
|
deck = self.engine.decks[self.cart_key]
|
|
if deck.track is None:
|
|
return
|
|
|
|
if deck.playing:
|
|
deck.stop()
|
|
deck.go_to_start()
|
|
self._set_ui_stopped()
|
|
self.status_label.setText("READY")
|
|
else:
|
|
deck.play()
|
|
self._set_ui_playing()
|
|
|
|
# ── SHARED UI STATE HELPERS ───────────────────────────────────────────
|
|
|
|
def _set_ui_playing(self):
|
|
self.btn_play.setText("⏸")
|
|
self.btn_play.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #00aa33;
|
|
color: white;
|
|
border: 1px solid #00ff41;
|
|
border-radius: 3px;
|
|
}
|
|
QPushButton:hover { background-color: #00cc44; }
|
|
""")
|
|
self.status_label.setText("PLAYING")
|
|
self.elapsed_label.setStyleSheet(
|
|
"color: #00ff41; background: transparent; border: none;"
|
|
)
|
|
|
|
def _set_ui_stopped(self):
|
|
self.btn_play.setText("▶")
|
|
self.btn_play.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #1a6b3a;
|
|
color: #00ff41;
|
|
border: 1px solid #555;
|
|
border-radius: 3px;
|
|
}
|
|
QPushButton:hover { background-color: #2a7b4a; }
|
|
""")
|
|
self.elapsed_label.setText("00:00")
|
|
self.remaining_label.setText("-00:00")
|
|
self.elapsed_label.setStyleSheet(
|
|
"color: #00aa2a; background: transparent; border: none;"
|
|
)
|
|
|
|
# ── ACTIONS ───────────────────────────────────────────────────────────
|
|
|
|
def _toggle_play(self):
|
|
deck = self.engine.decks[self.cart_key]
|
|
if deck.track is None:
|
|
return
|
|
if deck.playing:
|
|
deck.pause()
|
|
self._set_ui_stopped()
|
|
self.status_label.setText("PAUSED")
|
|
else:
|
|
deck.play()
|
|
self._set_ui_playing()
|
|
|
|
def _eject(self):
|
|
deck = self.engine.decks[self.cart_key]
|
|
deck.stop()
|
|
self._set_ui_stopped()
|
|
self.track_label.setText("EMPTY")
|
|
self.status_label.setText("STOPPED")
|
|
|
|
def _refresh_display(self):
|
|
deck = self.engine.decks[self.cart_key]
|
|
|
|
if not deck.playing and self.btn_play.text() == "⏸":
|
|
self._set_ui_stopped()
|
|
self.status_label.setText("STOPPED")
|
|
|
|
if deck.track:
|
|
self.elapsed_label.setText(deck.get_elapsed())
|
|
self.remaining_label.setText(deck.get_remaining())
|
|
|
|
|
|
# ─────────────────────────────────────────
|
|
# CART BANK
|
|
# ─────────────────────────────────────────
|
|
|
|
class CartBankWidget(QWidget):
|
|
def __init__(self, engine, parent=None):
|
|
super().__init__(parent)
|
|
self.engine = engine
|
|
self._build_ui()
|
|
|
|
def _build_ui(self):
|
|
self.setStyleSheet("""
|
|
QWidget {
|
|
background-color: #111;
|
|
border: 2px solid #cc3300;
|
|
border-radius: 6px;
|
|
}
|
|
""")
|
|
|
|
root = QVBoxLayout(self)
|
|
root.setContentsMargins(8, 6, 8, 6)
|
|
root.setSpacing(6)
|
|
|
|
header = QLabel("CARTS")
|
|
header.setFont(QFont("Arial", 11, QFont.Bold))
|
|
header.setStyleSheet("""
|
|
color: #cc3300;
|
|
background: transparent;
|
|
border: none;
|
|
padding: 2px;
|
|
""")
|
|
header.setAlignment(Qt.AlignCenter)
|
|
root.addWidget(header)
|
|
|
|
grid = QGridLayout()
|
|
grid.setSpacing(6)
|
|
|
|
self.slots = []
|
|
for i in range(4):
|
|
cart_key = f"cart{i + 1}"
|
|
slot = CartSlot(cart_key, i + 1, self.engine)
|
|
row = i // 2
|
|
col = i % 2
|
|
grid.addWidget(slot, row, col)
|
|
self.slots.append(slot)
|
|
|
|
root.addLayout(grid) |