2026-04-29 16:21:44 +10:00
|
|
|
# top_bar_widget.py
|
|
|
|
|
|
2026-05-13 16:47:02 +10:00
|
|
|
from PySide6.QtWidgets import (
|
|
|
|
|
QFrame, QHBoxLayout, QVBoxLayout, QLabel, QPushButton,
|
2026-05-13 20:53:15 +10:00
|
|
|
QComboBox, QScrollArea, QWidget, QListWidget
|
2026-05-13 16:47:02 +10:00
|
|
|
)
|
|
|
|
|
from PySide6.QtCore import Qt, QTimer, Signal, QSize, QPoint
|
2026-04-29 16:21:44 +10:00
|
|
|
from PySide6.QtGui import QFont
|
|
|
|
|
from datetime import datetime
|
2026-05-13 16:47:02 +10:00
|
|
|
from functools import partial
|
2026-04-29 16:21:44 +10:00
|
|
|
|
2026-05-13 16:47:02 +10:00
|
|
|
from icons import icon_theme, icon_cog, icon_save, icon_load
|
2026-05-08 18:33:01 +10:00
|
|
|
|
2026-04-29 16:21:44 +10:00
|
|
|
|
|
|
|
|
THEMES = [
|
2026-05-13 16:47:02 +10:00
|
|
|
{'name': 'DARK TEAL', 'accent': '#00b894', 'accent_dim': '#00876a', 'bg': '#121418', 'bg2': '#1a1c23', 'border': '#2a2c35', 'text': '#d0d4dc', 'is_light': False},
|
|
|
|
|
{'name': 'GREEN', 'accent': '#00ff41', 'accent_dim': '#00aa2a', 'bg': '#0d0d0d', 'bg2': '#111111', 'border': '#1a3a1a', 'text': '#cccccc', 'is_light': False},
|
|
|
|
|
{'name': 'AMBER', 'accent': '#ffaa00', 'accent_dim': '#cc8800', 'bg': '#0d0d00', 'bg2': '#111100', 'border': '#3a3000', 'text': '#ddddcc', 'is_light': False},
|
|
|
|
|
{'name': 'BLUE', 'accent': '#00aaff', 'accent_dim': '#0077cc', 'bg': '#00080d', 'bg2': '#001122', 'border': '#001a3a', 'text': '#ccd8dd', 'is_light': False},
|
|
|
|
|
{'name': 'RED', 'accent': '#ff4444', 'accent_dim': '#cc2222', 'bg': '#0d0000', 'bg2': '#110000', 'border': '#3a0000', 'text': '#ddcccc', 'is_light': False},
|
|
|
|
|
{'name': 'PURPLE', 'accent': '#cc00ff', 'accent_dim': '#9900cc', 'bg': '#0d000d', 'bg2': '#1a001a', 'border': '#3a003a', 'text': '#ddccdd', 'is_light': False},
|
|
|
|
|
{'name': 'CYAN', 'accent': '#00ffff', 'accent_dim': '#00cccc', 'bg': '#000d0d', 'bg2': '#001a1a', 'border': '#003a3a', 'text': '#ccdddd', 'is_light': False},
|
|
|
|
|
{'name': 'ORANGE', 'accent': '#ff8800', 'accent_dim': '#cc6600', 'bg': '#0d0700', 'bg2': '#1a1100', 'border': '#3a2200', 'text': '#ddccbb', 'is_light': False},
|
|
|
|
|
{'name': 'PINK', 'accent': '#ff1493', 'accent_dim': '#cc0066', 'bg': '#0d0005', 'bg2': '#1a000a', 'border': '#3a0015', 'text': '#ddccdd', 'is_light': False},
|
|
|
|
|
{'name': 'LIME', 'accent': '#ccff00', 'accent_dim': '#99cc00', 'bg': '#0a0d00', 'bg2': '#111a00', 'border': '#223a00', 'text': '#ddddcc', 'is_light': False},
|
|
|
|
|
{'name': 'GOLD', 'accent': '#ffd700', 'accent_dim': '#ccaa00', 'bg': '#0d0d05', 'bg2': '#1a1a0a', 'border': '#3a3a15', 'text': '#ddddbb', 'is_light': False},
|
|
|
|
|
{'name': 'VIOLET', 'accent': '#8a2be2', 'accent_dim': '#6600cc', 'bg': '#05000d', 'bg2': '#0a0015', 'border': '#15002a', 'text': '#ccbbdd', 'is_light': False},
|
|
|
|
|
{'name': 'TURQUOISE', 'accent': '#40e0d0', 'accent_dim': '#20b0a0', 'bg': '#000d0a', 'bg2': '#001a15', 'border': '#00382d', 'text': '#ccddda', 'is_light': False},
|
|
|
|
|
{'name': 'CRIMSON', 'accent': '#dc143c', 'accent_dim': '#aa0022', 'bg': '#0d0002', 'bg2': '#1a0005', 'border': '#3a000a', 'text': '#ddcccc', 'is_light': False},
|
|
|
|
|
{'name': 'CHARTREUSE', 'accent': '#7fff00', 'accent_dim': '#5fcc00', 'bg': '#050d00', 'bg2': '#0a1a00', 'border': '#153a00', 'text': '#ddddcc', 'is_light': False},
|
|
|
|
|
{'name': 'PAPER WHITE', 'accent': '#0066cc', 'accent_dim': '#004488', 'bg': '#ffffff', 'bg2': '#eeeeee', 'border': '#cccccc', 'text': '#111111', 'is_light': True},
|
|
|
|
|
{'name': 'GREY PAPER', 'accent': '#cc3300', 'accent_dim': '#992200', 'bg': '#e8e8e8', 'bg2': '#d8d8d8', 'border': '#aaaaaa', 'text': '#111111', 'is_light': True},
|
|
|
|
|
{'name': 'CREAM', 'accent': '#884422', 'accent_dim': '#663311', 'bg': '#f5f0e8', 'bg2': '#e8e0d4', 'border': '#c0b8a8', 'text': '#221100', 'is_light': True},
|
|
|
|
|
{'name': 'FROST', 'accent': '#0088aa', 'accent_dim': '#006688', 'bg': '#f0f4f8', 'bg2': '#e0e8f0', 'border': '#b0c0d0', 'text': '#001122', 'is_light': True},
|
|
|
|
|
{'name': 'LIGHT LAVENDER','accent': '#6633cc','accent_dim': '#4422aa', 'bg': '#f5f0fa', 'bg2': '#e8e0f0', 'border': '#c0b8d0', 'text': '#110022', 'is_light': True},
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
RATIOS = [
|
|
|
|
|
("21:9", 2560, 1080),
|
|
|
|
|
("16:9", 1920, 1080),
|
|
|
|
|
("4:3", 1440, 1080),
|
2026-04-29 16:21:44 +10:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TopBarWidget(QFrame):
|
|
|
|
|
theme_changed = Signal(dict)
|
2026-05-13 16:47:02 +10:00
|
|
|
ratio_changed = Signal(int, int)
|
|
|
|
|
save_session = Signal()
|
|
|
|
|
load_session = Signal()
|
2026-04-29 16:21:44 +10:00
|
|
|
|
|
|
|
|
def __init__(self, parent=None):
|
|
|
|
|
super().__init__(parent)
|
|
|
|
|
self._theme_index = 0
|
2026-05-13 16:47:02 +10:00
|
|
|
self._ratio_index = 1
|
|
|
|
|
self._midi_engine = None
|
2026-04-29 16:21:44 +10:00
|
|
|
|
|
|
|
|
self.setFrameStyle(QFrame.Box | QFrame.Raised)
|
2026-05-13 16:47:02 +10:00
|
|
|
self.setFixedHeight(56)
|
2026-04-29 16:21:44 +10:00
|
|
|
self._apply_bar_style()
|
|
|
|
|
|
|
|
|
|
self._build_ui()
|
|
|
|
|
|
|
|
|
|
self.timer = QTimer(self)
|
|
|
|
|
self.timer.setInterval(1000)
|
|
|
|
|
self.timer.timeout.connect(self._update)
|
|
|
|
|
self.timer.start()
|
|
|
|
|
self._update()
|
|
|
|
|
|
2026-05-13 16:47:02 +10:00
|
|
|
def set_midi_engine(self, engine):
|
|
|
|
|
self._midi_engine = engine
|
|
|
|
|
|
2026-04-29 16:21:44 +10:00
|
|
|
def _apply_bar_style(self):
|
|
|
|
|
theme = THEMES[self._theme_index]
|
|
|
|
|
self.setStyleSheet(f"""
|
2026-05-13 16:47:02 +10:00
|
|
|
TopBarWidget {{
|
2026-04-29 16:21:44 +10:00
|
|
|
background-color: {theme['bg2']};
|
|
|
|
|
border: 2px solid {theme['border']};
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
}}
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
def _build_ui(self):
|
|
|
|
|
layout = QHBoxLayout(self)
|
2026-05-13 16:47:02 +10:00
|
|
|
layout.setContentsMargins(16, 4, 16, 4)
|
|
|
|
|
layout.setSpacing(8)
|
2026-04-29 16:21:44 +10:00
|
|
|
|
2026-05-08 18:33:01 +10:00
|
|
|
self.theme_btn = QPushButton()
|
2026-05-13 16:47:02 +10:00
|
|
|
self.theme_btn.setFixedSize(30, 30)
|
|
|
|
|
self.theme_btn.setIconSize(QSize(16, 16))
|
2026-05-08 18:33:01 +10:00
|
|
|
self.theme_btn.setIcon(icon_theme())
|
2026-04-29 16:21:44 +10:00
|
|
|
self.theme_btn.setToolTip("Cycle colour theme")
|
|
|
|
|
self.theme_btn.clicked.connect(self._cycle_theme)
|
|
|
|
|
self._style_theme_btn()
|
|
|
|
|
layout.addWidget(self.theme_btn)
|
|
|
|
|
|
2026-05-08 18:33:01 +10:00
|
|
|
self.station_label = QLabel("GETTING TO IT")
|
2026-05-13 16:47:02 +10:00
|
|
|
self.station_label.setFont(QFont("Arial", 13, QFont.Bold))
|
|
|
|
|
self.station_label.setStyleSheet("color: #555555; background: transparent; border: none;")
|
2026-04-29 16:21:44 +10:00
|
|
|
layout.addWidget(self.station_label)
|
|
|
|
|
|
2026-05-13 16:47:02 +10:00
|
|
|
self.settings_btn = QPushButton()
|
|
|
|
|
self.settings_btn.setFixedSize(24, 24)
|
|
|
|
|
self.settings_btn.setIconSize(QSize(14, 14))
|
|
|
|
|
self.settings_btn.setIcon(icon_cog("#666666"))
|
|
|
|
|
self.settings_btn.setToolTip("Settings")
|
|
|
|
|
self.settings_btn.setStyleSheet("""
|
|
|
|
|
QPushButton {
|
|
|
|
|
background-color: transparent;
|
|
|
|
|
border: 1px solid #3a3c44;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
}
|
|
|
|
|
QPushButton:hover { background-color: #2a2c34; border-color: #5a5c64; }
|
|
|
|
|
""")
|
|
|
|
|
self.settings_btn.clicked.connect(self._toggle_settings_popup)
|
|
|
|
|
layout.addWidget(self.settings_btn)
|
|
|
|
|
|
2026-04-29 16:21:44 +10:00
|
|
|
layout.addStretch()
|
|
|
|
|
|
|
|
|
|
self.date_label = QLabel("")
|
2026-05-13 16:47:02 +10:00
|
|
|
self.date_label.setFont(QFont("Arial", 13, QFont.Bold))
|
|
|
|
|
self.date_label.setAlignment(Qt.AlignCenter)
|
2026-04-29 16:21:44 +10:00
|
|
|
layout.addWidget(self.date_label)
|
|
|
|
|
|
|
|
|
|
layout.addStretch()
|
|
|
|
|
|
|
|
|
|
self.clock_label = QLabel("")
|
2026-05-13 16:47:02 +10:00
|
|
|
self.clock_label.setFont(QFont("Courier New", 28, QFont.Bold))
|
2026-04-29 16:21:44 +10:00
|
|
|
self.clock_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
2026-05-13 16:47:02 +10:00
|
|
|
self.clock_label.setMinimumWidth(170)
|
2026-05-08 17:19:11 +10:00
|
|
|
self._style_clock_and_date()
|
2026-04-29 16:21:44 +10:00
|
|
|
layout.addWidget(self.clock_label)
|
|
|
|
|
|
2026-05-13 16:47:02 +10:00
|
|
|
# ── Settings popup ─────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
def _toggle_settings_popup(self):
|
|
|
|
|
self._popup = SettingsPopup(self._midi_engine, self)
|
|
|
|
|
self._popup.save_session.connect(self.save_session.emit)
|
|
|
|
|
self._popup.load_session.connect(self.load_session.emit)
|
|
|
|
|
pos = self.settings_btn.mapToGlobal(QPoint(0, self.settings_btn.height() + 4))
|
|
|
|
|
self._popup.move(pos)
|
|
|
|
|
self._popup.show()
|
|
|
|
|
|
|
|
|
|
# ── Theme ──────────────────────────────────────────────────────
|
|
|
|
|
|
2026-04-29 16:21:44 +10:00
|
|
|
def _style_theme_btn(self):
|
|
|
|
|
theme = THEMES[self._theme_index]
|
|
|
|
|
self.theme_btn.setStyleSheet(f"""
|
|
|
|
|
QPushButton {{
|
|
|
|
|
background-color: transparent;
|
|
|
|
|
color: {theme['accent']};
|
|
|
|
|
border: 2px solid {theme['accent']};
|
2026-05-13 16:47:02 +10:00
|
|
|
border-radius: 15px;
|
2026-04-29 16:21:44 +10:00
|
|
|
}}
|
|
|
|
|
QPushButton:hover {{
|
|
|
|
|
background-color: {theme['accent']};
|
2026-05-08 17:19:11 +10:00
|
|
|
color: {'#ffffff' if not theme['is_light'] else '#000000'};
|
2026-04-29 16:21:44 +10:00
|
|
|
}}
|
|
|
|
|
""")
|
|
|
|
|
|
2026-05-08 17:19:11 +10:00
|
|
|
def _style_clock_and_date(self):
|
2026-04-29 16:21:44 +10:00
|
|
|
theme = THEMES[self._theme_index]
|
|
|
|
|
self.clock_label.setStyleSheet(
|
|
|
|
|
f"color: {theme['accent']}; background: transparent; border: none;"
|
|
|
|
|
)
|
2026-05-08 17:19:11 +10:00
|
|
|
date_color = '#555555' if theme['is_light'] else '#aaaaaa'
|
|
|
|
|
self.date_label.setStyleSheet(
|
|
|
|
|
f"color: {date_color}; background: transparent; border: none;"
|
|
|
|
|
)
|
2026-04-29 16:21:44 +10:00
|
|
|
|
|
|
|
|
def _cycle_theme(self):
|
|
|
|
|
self._theme_index = (self._theme_index + 1) % len(THEMES)
|
|
|
|
|
self._apply_bar_style()
|
|
|
|
|
self._style_theme_btn()
|
2026-05-08 17:19:11 +10:00
|
|
|
self._style_clock_and_date()
|
2026-04-29 16:21:44 +10:00
|
|
|
self.theme_changed.emit(THEMES[self._theme_index])
|
|
|
|
|
|
|
|
|
|
def _update(self):
|
|
|
|
|
now = datetime.now()
|
|
|
|
|
self.clock_label.setText(now.strftime("%H:%M:%S"))
|
2026-05-13 16:47:02 +10:00
|
|
|
self.date_label.setText(now.strftime("%a %d %b"))
|
2026-04-29 16:21:44 +10:00
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def current_theme(self) -> dict:
|
2026-05-13 16:47:02 +10:00
|
|
|
return THEMES[self._theme_index]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Settings Popup ─────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
POPUP_STYLE = """
|
|
|
|
|
QFrame#settingsPopup {
|
|
|
|
|
background-color: #1a1c23;
|
|
|
|
|
border: 1px solid #2a2c35;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
}
|
|
|
|
|
QPushButton#popupBtn {
|
|
|
|
|
background-color: #22242a;
|
|
|
|
|
color: #d0d4dc;
|
|
|
|
|
border: 1px solid #3a3c44;
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
padding: 3px 8px;
|
2026-05-15 08:41:40 +10:00
|
|
|
font-size: 9pt;
|
2026-05-13 16:47:02 +10:00
|
|
|
font-weight: bold;
|
|
|
|
|
}
|
|
|
|
|
QPushButton#popupBtn:hover {
|
|
|
|
|
background-color: #2a2c34;
|
|
|
|
|
border-color: #5a5c64;
|
|
|
|
|
}
|
|
|
|
|
QPushButton#ratioBtn {
|
|
|
|
|
background-color: #22242a;
|
|
|
|
|
color: #8890a0;
|
|
|
|
|
border: 1px solid #3a3c44;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
padding: 2px 10px;
|
2026-05-15 08:41:40 +10:00
|
|
|
font-size: 9pt;
|
2026-05-13 16:47:02 +10:00
|
|
|
font-weight: bold;
|
|
|
|
|
}
|
|
|
|
|
QPushButton#ratioBtn:hover {
|
|
|
|
|
background-color: #2a2c34;
|
|
|
|
|
border-color: #5a5c64;
|
|
|
|
|
}
|
|
|
|
|
QComboBox#portCombo {
|
|
|
|
|
background-color: #22242a;
|
|
|
|
|
color: #d0d4dc;
|
|
|
|
|
border: 1px solid #3a3c44;
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
padding: 2px 4px;
|
2026-05-15 08:41:40 +10:00
|
|
|
font-size: 9pt;
|
|
|
|
|
min-height: 24px;
|
2026-05-13 16:47:02 +10:00
|
|
|
}
|
|
|
|
|
QComboBox#portCombo:hover { border-color: #5a5c64; }
|
|
|
|
|
QComboBox#portCombo::drop-down { border: none; width: 14px; }
|
|
|
|
|
QComboBox#portCombo QAbstractItemView {
|
|
|
|
|
background-color: #1a1c23;
|
|
|
|
|
color: #d0d4dc;
|
|
|
|
|
selection-background-color: #2a2c35;
|
|
|
|
|
border: 1px solid #3a3c44;
|
|
|
|
|
}
|
|
|
|
|
QScrollArea { border: none; background: transparent; }
|
|
|
|
|
QScrollBar:vertical { background: #1a1c22; width: 4px; border-radius: 2px; }
|
|
|
|
|
QScrollBar::handle:vertical { background: #3a3c44; border-radius: 2px; }
|
|
|
|
|
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0px; }
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SettingsPopup(QFrame):
|
|
|
|
|
save_session = Signal()
|
|
|
|
|
load_session = Signal()
|
|
|
|
|
|
|
|
|
|
def __init__(self, midi_engine, parent=None):
|
|
|
|
|
super().__init__(parent)
|
|
|
|
|
self._midi_engine = midi_engine
|
|
|
|
|
|
|
|
|
|
self.setWindowFlags(Qt.Popup | Qt.FramelessWindowHint)
|
|
|
|
|
self.setObjectName("settingsPopup")
|
|
|
|
|
self.setStyleSheet(POPUP_STYLE)
|
2026-05-15 08:41:40 +10:00
|
|
|
self.setFixedWidth(620)
|
2026-05-13 16:47:02 +10:00
|
|
|
|
|
|
|
|
self._build_ui()
|
|
|
|
|
|
|
|
|
|
def _build_ui(self):
|
|
|
|
|
root = QVBoxLayout(self)
|
2026-05-15 08:41:40 +10:00
|
|
|
root.setContentsMargins(14, 12, 14, 12)
|
|
|
|
|
root.setSpacing(8)
|
2026-05-13 16:47:02 +10:00
|
|
|
|
2026-05-15 08:41:40 +10:00
|
|
|
# Row 1: Save/Load / Clear MIDI
|
2026-05-13 16:47:02 +10:00
|
|
|
row1 = QHBoxLayout()
|
2026-05-15 08:41:40 +10:00
|
|
|
row1.setSpacing(8)
|
2026-05-13 16:47:02 +10:00
|
|
|
save_btn = QPushButton("SAVE")
|
|
|
|
|
save_btn.setObjectName("popupBtn")
|
2026-05-15 08:41:40 +10:00
|
|
|
save_btn.setFixedHeight(30)
|
2026-05-13 16:47:02 +10:00
|
|
|
save_btn.clicked.connect(self.save_session.emit)
|
|
|
|
|
row1.addWidget(save_btn)
|
|
|
|
|
|
|
|
|
|
load_btn = QPushButton("LOAD")
|
|
|
|
|
load_btn.setObjectName("popupBtn")
|
2026-05-15 08:41:40 +10:00
|
|
|
load_btn.setFixedHeight(30)
|
2026-05-13 16:47:02 +10:00
|
|
|
load_btn.clicked.connect(self.load_session.emit)
|
|
|
|
|
row1.addWidget(load_btn)
|
|
|
|
|
|
2026-05-15 08:41:40 +10:00
|
|
|
row1.addStretch()
|
|
|
|
|
|
|
|
|
|
clear_midi_btn = QPushButton("CLEAR MIDI")
|
|
|
|
|
clear_midi_btn.setObjectName("popupBtn")
|
|
|
|
|
clear_midi_btn.setFixedHeight(30)
|
|
|
|
|
clear_midi_btn.setStyleSheet("""
|
|
|
|
|
QPushButton {
|
|
|
|
|
background-color: #3a1a1a;
|
|
|
|
|
color: #ff6666;
|
|
|
|
|
border: 1px solid #5a2a2a;
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
font-size: 9pt;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
padding: 3px 8px;
|
|
|
|
|
}
|
|
|
|
|
QPushButton:hover { background-color: #5a2a2a; }
|
|
|
|
|
""")
|
|
|
|
|
clear_midi_btn.clicked.connect(self._clear_all_midi)
|
|
|
|
|
row1.addWidget(clear_midi_btn)
|
|
|
|
|
|
2026-05-13 16:47:02 +10:00
|
|
|
root.addLayout(row1)
|
|
|
|
|
|
|
|
|
|
# Row 2: MIDI port
|
|
|
|
|
row2 = QHBoxLayout()
|
2026-05-15 08:41:40 +10:00
|
|
|
row2.setSpacing(6)
|
2026-05-13 16:47:02 +10:00
|
|
|
self._port_combo = QComboBox()
|
|
|
|
|
self._port_combo.setObjectName("portCombo")
|
2026-05-15 08:41:40 +10:00
|
|
|
self._port_combo.setMinimumWidth(200)
|
2026-05-13 16:47:02 +10:00
|
|
|
row2.addWidget(self._port_combo)
|
|
|
|
|
|
|
|
|
|
refresh_btn = QPushButton("⟳")
|
|
|
|
|
refresh_btn.setObjectName("popupBtn")
|
2026-05-15 08:41:40 +10:00
|
|
|
refresh_btn.setFixedSize(30, 26)
|
2026-05-13 16:47:02 +10:00
|
|
|
refresh_btn.clicked.connect(self._refresh_ports)
|
|
|
|
|
row2.addWidget(refresh_btn)
|
|
|
|
|
|
|
|
|
|
self._connect_btn = QPushButton("CONNECT")
|
|
|
|
|
self._connect_btn.setObjectName("popupBtn")
|
2026-05-15 08:41:40 +10:00
|
|
|
self._connect_btn.setFixedHeight(26)
|
2026-05-13 16:47:02 +10:00
|
|
|
self._connect_btn.clicked.connect(self._connect_midi)
|
|
|
|
|
row2.addWidget(self._connect_btn)
|
|
|
|
|
|
|
|
|
|
self._midi_status = QLabel("")
|
2026-05-15 08:41:40 +10:00
|
|
|
self._midi_status.setStyleSheet("color: #888; background: transparent; border: none; font-size: 9pt;")
|
2026-05-13 16:47:02 +10:00
|
|
|
row2.addWidget(self._midi_status, stretch=1)
|
|
|
|
|
root.addLayout(row2)
|
|
|
|
|
|
|
|
|
|
# Row 3: MIDI learn scroll
|
|
|
|
|
learn_label = QLabel("MIDI LEARN")
|
2026-05-15 08:41:40 +10:00
|
|
|
learn_label.setStyleSheet("color: #00b894; background: transparent; border: none; font-size: 10pt; font-weight: bold; padding-top: 4px;")
|
2026-05-13 16:47:02 +10:00
|
|
|
root.addWidget(learn_label)
|
|
|
|
|
|
|
|
|
|
scroll = QScrollArea()
|
|
|
|
|
scroll.setWidgetResizable(True)
|
2026-05-15 08:41:40 +10:00
|
|
|
scroll.setFixedHeight(400)
|
2026-05-13 16:47:02 +10:00
|
|
|
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
|
|
|
|
scroll.setStyleSheet(POPUP_STYLE)
|
|
|
|
|
|
|
|
|
|
content = QWidget()
|
|
|
|
|
content.setStyleSheet("background: transparent;")
|
|
|
|
|
grid = QVBoxLayout(content)
|
|
|
|
|
grid.setContentsMargins(0, 0, 0, 0)
|
2026-05-15 08:41:40 +10:00
|
|
|
grid.setSpacing(4)
|
2026-05-13 16:47:02 +10:00
|
|
|
|
|
|
|
|
learn_actions = [
|
|
|
|
|
("Deck 1 Play/Pause", "deck_play_pause", "deck1"),
|
|
|
|
|
("Deck 1 Cue", "deck_cue", "deck1"),
|
|
|
|
|
("Deck 2 Play/Pause", "deck_play_pause", "deck2"),
|
|
|
|
|
("Deck 2 Cue", "deck_cue", "deck2"),
|
|
|
|
|
("Deck 3 Play/Pause", "deck_play_pause", "deck3"),
|
|
|
|
|
("Deck 4 Play/Pause", "deck_play_pause", "deck4"),
|
|
|
|
|
("Cart 1", "cart_trigger", 0),
|
|
|
|
|
("Cart 2", "cart_trigger", 1),
|
|
|
|
|
("Cart 3", "cart_trigger", 2),
|
|
|
|
|
("Cart 4", "cart_trigger", 3),
|
2026-05-13 20:53:15 +10:00
|
|
|
("Ch 1 Volume", "mixer_volume", 0),
|
|
|
|
|
("Ch 2 Volume", "mixer_volume", 1),
|
|
|
|
|
("Ch 3 Volume", "mixer_volume", 2),
|
|
|
|
|
("Ch 4 Volume", "mixer_volume", 3),
|
|
|
|
|
("Ch 5 Volume", "mixer_volume", 4),
|
|
|
|
|
("Ch 6 Volume", "mixer_volume", 5),
|
|
|
|
|
("Ch 7 Volume", "mixer_volume", 6),
|
|
|
|
|
("Ch 8 Volume", "mixer_volume", 7),
|
2026-05-15 08:41:40 +10:00
|
|
|
("Ch 1 Mic On", "mixer_mic_on", 0),
|
|
|
|
|
("Ch 2 Mic On", "mixer_mic_on", 1),
|
2026-05-13 16:47:02 +10:00
|
|
|
("Ch 1 PFL", "mixer_pfl", 0),
|
|
|
|
|
("Ch 2 PFL", "mixer_pfl", 1),
|
|
|
|
|
("Ch 3 PFL", "mixer_pfl", 2),
|
|
|
|
|
("Ch 4 PFL", "mixer_pfl", 3),
|
2026-05-15 08:41:40 +10:00
|
|
|
("Ch 5 PFL", "mixer_pfl", 4),
|
|
|
|
|
("Ch 6 PFL", "mixer_pfl", 5),
|
|
|
|
|
("Ch 7 PFL", "mixer_pfl", 6),
|
|
|
|
|
("Ch 8 PFL", "mixer_pfl", 7),
|
|
|
|
|
("Ch 3 Mute", "mixer_mute", 2),
|
|
|
|
|
("Ch 4 Mute", "mixer_mute", 3),
|
|
|
|
|
("Ch 5 Mute", "mixer_mute", 4),
|
|
|
|
|
("Ch 6 Mute", "mixer_mute", 5),
|
|
|
|
|
("Ch 7 Mute", "mixer_mute", 6),
|
|
|
|
|
("Ch 8 Mute", "mixer_mute", 7),
|
2026-05-13 16:47:02 +10:00
|
|
|
("Master THR", "master_control", "threshold_db"),
|
|
|
|
|
("Master RAT", "master_control", "ratio"),
|
|
|
|
|
("Master ATK", "master_control", "attack_ms"),
|
|
|
|
|
("Master REL", "master_control", "release_ms"),
|
|
|
|
|
("Master MKG", "master_control", "makeup_gain_db"),
|
|
|
|
|
("Master CEIL", "master_control", "ceiling_db"),
|
2026-05-14 22:25:10 +10:00
|
|
|
("Master Volume", "master_volume", "master"),
|
2026-05-13 16:47:02 +10:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
self._learn_btns = {}
|
|
|
|
|
for label, action, target_id in learn_actions:
|
|
|
|
|
row = QHBoxLayout()
|
|
|
|
|
row.setSpacing(3)
|
|
|
|
|
|
|
|
|
|
lbl = QLabel(label)
|
2026-05-15 08:41:40 +10:00
|
|
|
lbl.setStyleSheet("color: #d0d4dc; background: transparent; border: none; font-size: 9pt;")
|
2026-05-13 16:47:02 +10:00
|
|
|
row.addWidget(lbl, stretch=1)
|
|
|
|
|
|
|
|
|
|
btn = QPushButton("LEARN")
|
|
|
|
|
btn.setObjectName("popupBtn")
|
2026-05-15 08:41:40 +10:00
|
|
|
btn.setFixedSize(64, 24)
|
2026-05-13 16:47:02 +10:00
|
|
|
btn.setCheckable(True)
|
|
|
|
|
btn.toggled.connect(partial(self._toggle_learn, btn, action, target_id))
|
|
|
|
|
row.addWidget(btn)
|
|
|
|
|
self._learn_btns[(action, target_id)] = btn
|
|
|
|
|
|
|
|
|
|
clear_btn = QPushButton("X")
|
|
|
|
|
clear_btn.setObjectName("popupBtn")
|
|
|
|
|
clear_btn.setStyleSheet("""
|
|
|
|
|
QPushButton {
|
|
|
|
|
background-color: #3a1a1a;
|
|
|
|
|
color: #ff6666;
|
|
|
|
|
border: 1px solid #5a2a2a;
|
|
|
|
|
border-radius: 3px;
|
2026-05-15 08:41:40 +10:00
|
|
|
font-size: 9pt;
|
2026-05-13 16:47:02 +10:00
|
|
|
font-weight: bold;
|
|
|
|
|
padding: 2px 4px;
|
|
|
|
|
}
|
2026-05-13 20:53:15 +10:00
|
|
|
QPushButton:hover { background-color: #5a2a2a; }
|
2026-05-13 16:47:02 +10:00
|
|
|
""")
|
2026-05-15 08:41:40 +10:00
|
|
|
clear_btn.setFixedSize(28, 24)
|
2026-05-13 16:47:02 +10:00
|
|
|
clear_btn.clicked.connect(partial(self._clear_mapping, action, target_id))
|
|
|
|
|
row.addWidget(clear_btn)
|
|
|
|
|
|
|
|
|
|
grid.addLayout(row)
|
|
|
|
|
|
|
|
|
|
scroll.setWidget(content)
|
|
|
|
|
root.addWidget(scroll)
|
|
|
|
|
|
2026-05-13 20:53:15 +10:00
|
|
|
monitor_label = QLabel("MIDI MONITOR")
|
2026-05-15 08:41:40 +10:00
|
|
|
monitor_label.setStyleSheet("color: #556670; background: transparent; border: none; font-size: 9pt; font-weight: bold;")
|
2026-05-13 20:53:15 +10:00
|
|
|
root.addWidget(monitor_label)
|
|
|
|
|
|
|
|
|
|
self._midi_monitor = QListWidget()
|
2026-05-15 08:41:40 +10:00
|
|
|
self._midi_monitor.setFixedHeight(120)
|
2026-05-13 20:53:15 +10:00
|
|
|
self._midi_monitor.setStyleSheet("""
|
|
|
|
|
QListWidget {
|
|
|
|
|
background-color: #0d0e12;
|
|
|
|
|
border: 1px solid #22242a;
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
color: #8890a0;
|
2026-05-15 08:41:40 +10:00
|
|
|
font-size: 9pt;
|
2026-05-13 20:53:15 +10:00
|
|
|
font-family: monospace;
|
|
|
|
|
}
|
|
|
|
|
""")
|
|
|
|
|
|
2026-05-13 16:47:02 +10:00
|
|
|
self._refresh_ports()
|
|
|
|
|
|
|
|
|
|
if self._midi_engine:
|
|
|
|
|
self._midi_engine.mapping_learned.connect(self._on_mapping_learned)
|
2026-05-13 20:53:15 +10:00
|
|
|
self._midi_engine.raw_midi_event.connect(self._on_raw_midi)
|
2026-05-13 16:47:02 +10:00
|
|
|
|
2026-05-13 20:53:15 +10:00
|
|
|
def _on_raw_midi(self, type_name, channel, control, value):
|
|
|
|
|
self._midi_monitor.addItem(
|
|
|
|
|
f"{type_name:>10} ch{channel} ctl={control:>3} val={value:>3}"
|
|
|
|
|
)
|
|
|
|
|
while self._midi_monitor.count() > 200:
|
|
|
|
|
self._midi_monitor.takeItem(0)
|
|
|
|
|
self._midi_monitor.scrollToBottom()
|
2026-05-13 16:47:02 +10:00
|
|
|
|
|
|
|
|
# ── MIDI port ──────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
def _refresh_ports(self):
|
|
|
|
|
self._port_combo.clear()
|
|
|
|
|
if not self._midi_engine:
|
|
|
|
|
return
|
|
|
|
|
ports = self._midi_engine.list_ports()
|
|
|
|
|
if ports:
|
|
|
|
|
for p in ports:
|
|
|
|
|
self._port_combo.addItem(p)
|
|
|
|
|
self._connect_btn.setEnabled(True)
|
|
|
|
|
self._midi_status.setText("Select port")
|
|
|
|
|
else:
|
|
|
|
|
self._port_combo.addItem("No MIDI ports found")
|
|
|
|
|
self._connect_btn.setEnabled(False)
|
|
|
|
|
self._midi_status.setText("No MIDI devices")
|
|
|
|
|
|
|
|
|
|
def _connect_midi(self):
|
|
|
|
|
if not self._midi_engine or self._port_combo.currentText() == "No MIDI ports found":
|
|
|
|
|
return
|
|
|
|
|
self._midi_engine.stop()
|
|
|
|
|
idx = self._port_combo.currentIndex()
|
|
|
|
|
success = self._midi_engine.start(port_index=idx)
|
|
|
|
|
if success:
|
|
|
|
|
self._midi_status.setText(f"Connected")
|
2026-05-15 08:41:40 +10:00
|
|
|
self._midi_status.setStyleSheet("color: #00b894; background: transparent; border: none; font-size: 9pt;")
|
2026-05-13 16:47:02 +10:00
|
|
|
else:
|
|
|
|
|
self._midi_status.setText("Failed")
|
2026-05-15 08:41:40 +10:00
|
|
|
self._midi_status.setStyleSheet("color: #ff6666; background: transparent; border: none; font-size: 9pt;")
|
2026-05-13 16:47:02 +10:00
|
|
|
|
|
|
|
|
# ── MIDI learn ─────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
def _toggle_learn(self, btn, action, target_id, checked):
|
|
|
|
|
if not self._midi_engine:
|
|
|
|
|
btn.setChecked(False)
|
|
|
|
|
return
|
|
|
|
|
if checked:
|
|
|
|
|
self._midi_engine.start_learn(action, target_id)
|
|
|
|
|
for other_btn in self._learn_btns.values():
|
|
|
|
|
if other_btn is not btn:
|
|
|
|
|
other_btn.setChecked(False)
|
|
|
|
|
btn.setText("...")
|
|
|
|
|
else:
|
|
|
|
|
self._midi_engine.stop_learn()
|
|
|
|
|
btn.setText("LEARN")
|
|
|
|
|
|
|
|
|
|
def _clear_mapping(self, action, target_id):
|
|
|
|
|
if not self._midi_engine:
|
|
|
|
|
return
|
|
|
|
|
to_remove = []
|
|
|
|
|
for key, val in self._midi_engine.mapping.bindings.items():
|
|
|
|
|
if val['action'] == action and val['target_id'] == target_id:
|
|
|
|
|
to_remove.append(key)
|
|
|
|
|
for key in to_remove:
|
|
|
|
|
del self._midi_engine.mapping.bindings[key]
|
|
|
|
|
self._midi_engine.mapping.save()
|
|
|
|
|
|
2026-05-15 08:41:40 +10:00
|
|
|
def _clear_all_midi(self):
|
|
|
|
|
if not self._midi_engine:
|
|
|
|
|
return
|
|
|
|
|
self._midi_engine.mapping.bindings.clear()
|
|
|
|
|
self._midi_engine.mapping.save()
|
|
|
|
|
|
|
|
|
|
def _clear_all_midi(self):
|
|
|
|
|
if not self._midi_engine:
|
|
|
|
|
return
|
|
|
|
|
self._midi_engine.mapping.bindings.clear()
|
|
|
|
|
self._midi_engine.mapping.save()
|
|
|
|
|
|
2026-05-13 16:47:02 +10:00
|
|
|
def _on_mapping_learned(self, action, target_id, midi_type, channel, note, value):
|
|
|
|
|
btn = self._learn_btns.get((action, target_id))
|
|
|
|
|
if btn and btn.isChecked():
|
|
|
|
|
btn.setChecked(False)
|
|
|
|
|
btn.setText("OK")
|