# top_bar_widget.py from PySide6.QtWidgets import ( QFrame, QHBoxLayout, QVBoxLayout, QLabel, QPushButton, QComboBox, QScrollArea, QWidget, QListWidget ) from PySide6.QtCore import Qt, QTimer, Signal, QSize, QPoint from PySide6.QtGui import QFont from datetime import datetime from functools import partial from icons import icon_theme, icon_cog, icon_save, icon_load THEMES = [ {'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), ] class TopBarWidget(QFrame): theme_changed = Signal(dict) ratio_changed = Signal(int, int) save_session = Signal() load_session = Signal() def __init__(self, parent=None): super().__init__(parent) self._theme_index = 0 self._ratio_index = 1 self._midi_engine = None self.setFrameStyle(QFrame.Box | QFrame.Raised) self.setFixedHeight(56) 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() def set_midi_engine(self, engine): self._midi_engine = engine def _apply_bar_style(self): theme = THEMES[self._theme_index] self.setStyleSheet(f""" TopBarWidget {{ background-color: {theme['bg2']}; border: 2px solid {theme['border']}; border-radius: 6px; }} """) def _build_ui(self): layout = QHBoxLayout(self) layout.setContentsMargins(16, 4, 16, 4) layout.setSpacing(8) self.theme_btn = QPushButton() self.theme_btn.setFixedSize(30, 30) self.theme_btn.setIconSize(QSize(16, 16)) self.theme_btn.setIcon(icon_theme()) self.theme_btn.setToolTip("Cycle colour theme") self.theme_btn.clicked.connect(self._cycle_theme) self._style_theme_btn() layout.addWidget(self.theme_btn) self.station_label = QLabel("GETTING TO IT") self.station_label.setFont(QFont("Arial", 13, QFont.Bold)) self.station_label.setStyleSheet("color: #555555; background: transparent; border: none;") layout.addWidget(self.station_label) 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) layout.addStretch() self.date_label = QLabel("") self.date_label.setFont(QFont("Arial", 13, QFont.Bold)) self.date_label.setAlignment(Qt.AlignCenter) layout.addWidget(self.date_label) layout.addStretch() self.clock_label = QLabel("") self.clock_label.setFont(QFont("Courier New", 28, QFont.Bold)) self.clock_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) self.clock_label.setMinimumWidth(170) self._style_clock_and_date() layout.addWidget(self.clock_label) # ── 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 ────────────────────────────────────────────────────── 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']}; border-radius: 15px; }} QPushButton:hover {{ background-color: {theme['accent']}; color: {'#ffffff' if not theme['is_light'] else '#000000'}; }} """) def _style_clock_and_date(self): theme = THEMES[self._theme_index] self.clock_label.setStyleSheet( f"color: {theme['accent']}; background: transparent; border: none;" ) date_color = '#555555' if theme['is_light'] else '#aaaaaa' self.date_label.setStyleSheet( f"color: {date_color}; background: transparent; border: none;" ) def _cycle_theme(self): self._theme_index = (self._theme_index + 1) % len(THEMES) self._apply_bar_style() self._style_theme_btn() self._style_clock_and_date() self.theme_changed.emit(THEMES[self._theme_index]) def _update(self): now = datetime.now() self.clock_label.setText(now.strftime("%H:%M:%S")) self.date_label.setText(now.strftime("%a %d %b")) @property def current_theme(self) -> dict: 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; font-size: 7pt; 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; font-size: 7pt; 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; font-size: 7pt; min-height: 20px; } 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) self.setFixedWidth(400) self._build_ui() def _build_ui(self): root = QVBoxLayout(self) root.setContentsMargins(12, 10, 12, 10) root.setSpacing(6) # Row 1: Save/Load row1 = QHBoxLayout() row1.setSpacing(6) save_btn = QPushButton("SAVE") save_btn.setObjectName("popupBtn") save_btn.setFixedHeight(24) save_btn.clicked.connect(self.save_session.emit) row1.addWidget(save_btn) load_btn = QPushButton("LOAD") load_btn.setObjectName("popupBtn") load_btn.setFixedHeight(24) load_btn.clicked.connect(self.load_session.emit) row1.addWidget(load_btn) root.addLayout(row1) # Row 2: MIDI port row2 = QHBoxLayout() row2.setSpacing(4) self._port_combo = QComboBox() self._port_combo.setObjectName("portCombo") self._port_combo.setMinimumWidth(160) row2.addWidget(self._port_combo) refresh_btn = QPushButton("⟳") refresh_btn.setObjectName("popupBtn") refresh_btn.setFixedSize(26, 22) refresh_btn.clicked.connect(self._refresh_ports) row2.addWidget(refresh_btn) self._connect_btn = QPushButton("CONNECT") self._connect_btn.setObjectName("popupBtn") self._connect_btn.setFixedHeight(22) self._connect_btn.clicked.connect(self._connect_midi) row2.addWidget(self._connect_btn) self._midi_status = QLabel("") self._midi_status.setStyleSheet("color: #888; background: transparent; border: none; font-size: 7pt;") row2.addWidget(self._midi_status, stretch=1) root.addLayout(row2) # Row 3: MIDI learn scroll learn_label = QLabel("MIDI LEARN") learn_label.setStyleSheet("color: #00b894; background: transparent; border: none; font-size: 7pt; font-weight: bold; padding-top: 2px;") root.addWidget(learn_label) scroll = QScrollArea() scroll.setWidgetResizable(True) scroll.setFixedHeight(200) scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) scroll.setStyleSheet(POPUP_STYLE) content = QWidget() content.setStyleSheet("background: transparent;") grid = QVBoxLayout(content) grid.setContentsMargins(0, 0, 0, 0) grid.setSpacing(2) 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), ("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), ("Ch 1 Solo", "mixer_solo", 0), ("Ch 1 PFL", "mixer_pfl", 0), ("Ch 2 Solo", "mixer_solo", 1), ("Ch 2 PFL", "mixer_pfl", 1), ("Ch 3 Solo", "mixer_solo", 2), ("Ch 3 PFL", "mixer_pfl", 2), ("Ch 4 Solo", "mixer_solo", 3), ("Ch 4 PFL", "mixer_pfl", 3), ("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"), ("Master Volume", "master_volume", "master"), ] self._learn_btns = {} for label, action, target_id in learn_actions: row = QHBoxLayout() row.setSpacing(3) lbl = QLabel(label) lbl.setStyleSheet("color: #d0d4dc; background: transparent; border: none; font-size: 7pt;") row.addWidget(lbl, stretch=1) btn = QPushButton("LEARN") btn.setObjectName("popupBtn") btn.setFixedSize(52, 20) 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; font-size: 7pt; font-weight: bold; padding: 2px 4px; } QPushButton:hover { background-color: #5a2a2a; } """) clear_btn.setFixedSize(20, 20) clear_btn.clicked.connect(partial(self._clear_mapping, action, target_id)) row.addWidget(clear_btn) grid.addLayout(row) scroll.setWidget(content) root.addWidget(scroll) monitor_label = QLabel("MIDI MONITOR") monitor_label.setStyleSheet("color: #556670; background: transparent; border: none; font-size: 7pt; font-weight: bold;") root.addWidget(monitor_label) self._midi_monitor = QListWidget() self._midi_monitor.setFixedHeight(80) self._midi_monitor.setStyleSheet(""" QListWidget { background-color: #0d0e12; border: 1px solid #22242a; border-radius: 3px; color: #8890a0; font-size: 7pt; font-family: monospace; } """) self._refresh_ports() if self._midi_engine: self._midi_engine.mapping_learned.connect(self._on_mapping_learned) self._midi_engine.raw_midi_event.connect(self._on_raw_midi) 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() # ── 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") self._midi_status.setStyleSheet("color: #00b894; background: transparent; border: none; font-size: 7pt;") else: self._midi_status.setText("Failed") self._midi_status.setStyleSheet("color: #ff6666; background: transparent; border: none; font-size: 7pt;") # ── 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() 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")