v0.4 - cockybastard
This commit is contained in:
+15
-15
@@ -29,7 +29,7 @@ class CartSlot(QFrame):
|
||||
}
|
||||
""")
|
||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
self.setFixedHeight(110)
|
||||
self.setFixedHeight(80)
|
||||
|
||||
self._build_ui()
|
||||
self._connect_signals()
|
||||
@@ -41,13 +41,13 @@ class CartSlot(QFrame):
|
||||
|
||||
def _build_ui(self):
|
||||
root = QHBoxLayout(self)
|
||||
root.setContentsMargins(8, 6, 8, 6)
|
||||
root.setSpacing(8)
|
||||
root.setContentsMargins(6, 4, 6, 4)
|
||||
root.setSpacing(6)
|
||||
|
||||
badge = QLabel(str(self.slot_number))
|
||||
badge.setFixedSize(28, 28)
|
||||
badge.setFixedSize(22, 22)
|
||||
badge.setAlignment(Qt.AlignCenter)
|
||||
badge.setFont(QFont("Arial", 12, QFont.Bold))
|
||||
badge.setFont(QFont("Arial", 10, QFont.Bold))
|
||||
badge.setStyleSheet("""
|
||||
background-color: #cc3300;
|
||||
color: white;
|
||||
@@ -67,11 +67,11 @@ class CartSlot(QFrame):
|
||||
self.lcd_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
|
||||
lcd_layout = QVBoxLayout(self.lcd_frame)
|
||||
lcd_layout.setContentsMargins(6, 4, 6, 4)
|
||||
lcd_layout.setSpacing(2)
|
||||
lcd_layout.setContentsMargins(4, 2, 4, 2)
|
||||
lcd_layout.setSpacing(1)
|
||||
|
||||
self.track_label = QLabel("EMPTY")
|
||||
self.track_label.setFont(QFont("Courier New", 9, QFont.Bold))
|
||||
self.track_label.setFont(QFont("Courier New", 8, QFont.Bold))
|
||||
self.track_label.setStyleSheet(
|
||||
"color: #00ff41; background: transparent; border: none;"
|
||||
)
|
||||
@@ -80,13 +80,13 @@ class CartSlot(QFrame):
|
||||
time_row = QHBoxLayout()
|
||||
|
||||
self.elapsed_label = QLabel("00:00")
|
||||
self.elapsed_label.setFont(QFont("Courier New", 14, QFont.Bold))
|
||||
self.elapsed_label.setFont(QFont("Courier New", 11, 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.setFont(QFont("Courier New", 8))
|
||||
self.remaining_label.setStyleSheet(
|
||||
"color: #00cc33; background: transparent; border: none;"
|
||||
)
|
||||
@@ -109,11 +109,11 @@ class CartSlot(QFrame):
|
||||
root.addWidget(self.lcd_frame)
|
||||
|
||||
btn_col = QVBoxLayout()
|
||||
btn_col.setSpacing(4)
|
||||
btn_col.setSpacing(3)
|
||||
|
||||
self.btn_play = QPushButton()
|
||||
self.btn_play.setFixedSize(60, 44)
|
||||
self.btn_play.setIconSize(QSize(22, 22))
|
||||
self.btn_play.setFixedSize(50, 32)
|
||||
self.btn_play.setIconSize(QSize(18, 18))
|
||||
self.btn_play.setIcon(icon_play("#00ff41"))
|
||||
self.btn_play.setStyleSheet("""
|
||||
QPushButton {
|
||||
@@ -126,8 +126,8 @@ class CartSlot(QFrame):
|
||||
""")
|
||||
|
||||
self.btn_eject = QPushButton()
|
||||
self.btn_eject.setFixedSize(60, 30)
|
||||
self.btn_eject.setIconSize(QSize(18, 18))
|
||||
self.btn_eject.setFixedSize(50, 24)
|
||||
self.btn_eject.setIconSize(QSize(14, 14))
|
||||
self.btn_eject.setIcon(icon_eject("#ff4444"))
|
||||
self.btn_eject.setStyleSheet("""
|
||||
QPushButton {
|
||||
|
||||
@@ -54,7 +54,7 @@ class RadioPanelWindow(QMainWindow):
|
||||
self.deck4 = ExternalDeckWidget("deck4", "DECK 4", "#e17055", self.engine)
|
||||
|
||||
self.cart_bank = CartBankWidget(self.engine)
|
||||
self.cart_bank.setFixedHeight(220)
|
||||
self.cart_bank.setFixedHeight(180)
|
||||
|
||||
self.playlist_widget = PlaylistWidget(
|
||||
engine=self.engine,
|
||||
|
||||
+58
-20
@@ -18,6 +18,8 @@ logger = logging.getLogger(__name__)
|
||||
NOTE_ON = 0x90
|
||||
NOTE_OFF = 0x80
|
||||
CC = 0xB0
|
||||
PITCH_BEND = 0xE0
|
||||
PITCH_BEND = 0xE0
|
||||
|
||||
CONFIG_DIR = os.path.expanduser("~/.gti_radiostudio")
|
||||
CONFIG_FILE = os.path.join(CONFIG_DIR, "midi_map.json")
|
||||
@@ -40,12 +42,13 @@ class MidiMapping:
|
||||
self.bindings = {}
|
||||
|
||||
def add(self, action: str, target_id, midi_type: int, midi_channel: int,
|
||||
midi_note: int, inverse: bool = False):
|
||||
midi_note: int, inverse: bool = False, is_relative: bool = False):
|
||||
key = (midi_type, midi_channel, midi_note)
|
||||
self.bindings[key] = {
|
||||
'action': action,
|
||||
'target_id': target_id,
|
||||
'inverse': inverse,
|
||||
'is_relative': is_relative,
|
||||
}
|
||||
|
||||
def lookup(self, midi_type: int, midi_channel: int,
|
||||
@@ -63,6 +66,7 @@ class MidiMapping:
|
||||
'action': val['action'],
|
||||
'target_id': val['target_id'],
|
||||
'inverse': val['inverse'],
|
||||
'is_relative': val.get('is_relative', False),
|
||||
})
|
||||
with open(filepath, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
@@ -78,6 +82,7 @@ class MidiMapping:
|
||||
'action': entry['action'],
|
||||
'target_id': entry['target_id'],
|
||||
'inverse': entry.get('inverse', False),
|
||||
'is_relative': entry.get('is_relative', False),
|
||||
}
|
||||
|
||||
|
||||
@@ -93,6 +98,7 @@ class MidiEngine(QObject):
|
||||
master_control = Signal(str, float)
|
||||
controller_connected = Signal(bool, str)
|
||||
mapping_learned = Signal(str, object, int, int, int, bool)
|
||||
raw_midi_event = Signal(str, int, int, int)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -107,6 +113,9 @@ class MidiEngine(QObject):
|
||||
self._learn_mode = False
|
||||
|
||||
self._last_cc_values = {}
|
||||
self._channel_volumes = {}
|
||||
self._master_knob_values = {}
|
||||
self._master_knob_values = {}
|
||||
|
||||
@property
|
||||
def learn_mode(self) -> bool:
|
||||
@@ -200,13 +209,18 @@ class MidiEngine(QObject):
|
||||
status_type = status & 0xF0
|
||||
channel = status & 0x0F
|
||||
|
||||
# Learn mode: capture any MIDI event
|
||||
type_names = {0x90: "note_on", 0x80: "note_off", 0xB0: "cc", 0xE0: "pitch", 0xD0: "aftertouch"}
|
||||
type_name = type_names.get(status_type, f"0x{status_type:02X}")
|
||||
self.raw_midi_event.emit(type_name, channel, data_byte, value)
|
||||
|
||||
if self._learn_mode and self._learn_target:
|
||||
action, target_id = self._learn_target
|
||||
mapping_type = status_type
|
||||
self.mapping.add(action, target_id, mapping_type, channel, data_byte)
|
||||
is_rel = (status_type == 0xB0 and (1 <= value <= 63 or 65 <= value <= 127))
|
||||
note = 0 if status_type == PITCH_BEND else data_byte
|
||||
self.mapping.add(action, target_id, mapping_type, channel, note, is_relative=is_rel)
|
||||
self.mapping.save()
|
||||
self.mapping_learned.emit(action, target_id, mapping_type, channel, data_byte, value > 0)
|
||||
self.mapping_learned.emit(action, target_id, mapping_type, channel, note, value > 0)
|
||||
self.stop_learn()
|
||||
return
|
||||
|
||||
@@ -218,6 +232,11 @@ class MidiEngine(QObject):
|
||||
elif status_type == CC:
|
||||
self._handle_cc(data_byte, value)
|
||||
self._handle_learned_event(status_type, channel, data_byte, value)
|
||||
elif status_type == PITCH_BEND:
|
||||
lsb = data_byte
|
||||
msb = message[2] if len(message) > 2 else 0
|
||||
pb_value = (msb << 7) | lsb
|
||||
self._handle_learned_event(status_type, channel, 0, pb_value)
|
||||
|
||||
def _handle_learned_event(self, midi_type: int, channel: int,
|
||||
note: int, value: int):
|
||||
@@ -228,6 +247,7 @@ class MidiEngine(QObject):
|
||||
action = binding['action']
|
||||
target_id = binding['target_id']
|
||||
inv = binding.get('inverse', False)
|
||||
is_rel = binding.get('is_relative', False)
|
||||
|
||||
if action == 'deck_play_pause' and isinstance(target_id, str):
|
||||
self.deck_play_pause.emit(target_id)
|
||||
@@ -235,14 +255,24 @@ class MidiEngine(QObject):
|
||||
self.deck_cue.emit(target_id)
|
||||
elif action == 'cart_trigger':
|
||||
self.cart_trigger.emit(target_id)
|
||||
elif action == 'deck_play_pause' and isinstance(target_id, str):
|
||||
self.deck_play_pause.emit(target_id)
|
||||
elif action == 'deck_cue' and isinstance(target_id, str):
|
||||
self.deck_cue.emit(target_id)
|
||||
elif action == 'cart_trigger':
|
||||
self.cart_trigger.emit(target_id)
|
||||
elif action == 'mixer_volume':
|
||||
if is_rel and 1 <= value <= 63:
|
||||
prev = self._channel_volumes.get(target_id, 1.0)
|
||||
new_vol = min(1.0, prev + value * 0.02)
|
||||
self._channel_volumes[target_id] = new_vol
|
||||
self.mixer_volume.emit(target_id, new_vol)
|
||||
elif is_rel and 65 <= value <= 127:
|
||||
prev = self._channel_volumes.get(target_id, 1.0)
|
||||
steps = value & 0x3F
|
||||
new_vol = max(0.0, prev - steps * 0.02)
|
||||
self._channel_volumes[target_id] = new_vol
|
||||
self.mixer_volume.emit(target_id, new_vol)
|
||||
else:
|
||||
if midi_type == PITCH_BEND:
|
||||
scaled = (16383 - value if inv else value) / 16383.0
|
||||
else:
|
||||
scaled = (127 - value if inv else value) / 127.0
|
||||
self._channel_volumes[target_id] = scaled
|
||||
self.mixer_volume.emit(target_id, scaled)
|
||||
elif action == 'mixer_mute':
|
||||
muted = (value < 64) if inv else (value > 63)
|
||||
@@ -254,17 +284,25 @@ class MidiEngine(QObject):
|
||||
on = (value > 63) if not inv else (value < 64)
|
||||
self.mixer_pfl.emit(target_id, on)
|
||||
elif action == 'master_control':
|
||||
knob = str(target_id)
|
||||
if is_rel and 1 <= value <= 63:
|
||||
prev = self._master_knob_values.get(knob, 0.5)
|
||||
new_val = min(1.0, prev + value * 0.02)
|
||||
self._master_knob_values[knob] = new_val
|
||||
self.master_control.emit(knob, new_val)
|
||||
elif is_rel and 65 <= value <= 127:
|
||||
prev = self._master_knob_values.get(knob, 0.5)
|
||||
steps = value & 0x3F
|
||||
new_val = max(0.0, prev - steps * 0.02)
|
||||
self._master_knob_values[knob] = new_val
|
||||
self.master_control.emit(knob, new_val)
|
||||
else:
|
||||
if midi_type == PITCH_BEND:
|
||||
scaled = (16383 - value if inv else value) / 16383.0
|
||||
else:
|
||||
scaled = (127 - value if inv else value) / 127.0
|
||||
self.master_control.emit(str(target_id), scaled)
|
||||
elif action == 'mixer_solo':
|
||||
on = (value > 63) if not inv else (value < 64)
|
||||
self.mixer_solo.emit(target_id, on)
|
||||
elif action == 'mixer_pfl':
|
||||
on = (value > 63) if not inv else (value < 64)
|
||||
self.mixer_pfl.emit(target_id, on)
|
||||
elif action == 'master_control':
|
||||
scaled = (127 - value if inv else value) / 127.0
|
||||
self.master_control.emit(str(target_id), scaled)
|
||||
self._master_knob_values[knob] = scaled
|
||||
self.master_control.emit(knob, scaled)
|
||||
|
||||
def _handle_note_on(self, note: int):
|
||||
# Hardcoded Numark DJ2Go mapping (legacy)
|
||||
|
||||
+59
-20
@@ -10,6 +10,7 @@ from PySide6.QtGui import QFont, QPainter, QColor, QLinearGradient, QPen
|
||||
|
||||
from audio_engine import NUM_MIXER_CHANNELS
|
||||
from icons import icon_mute, icon_solo
|
||||
import math
|
||||
|
||||
|
||||
class VUMeter(QWidget):
|
||||
@@ -27,23 +28,29 @@ class VUMeter(QWidget):
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.Antialiasing)
|
||||
|
||||
rect = self.rect().adjusted(14, 3, -3, -3)
|
||||
rect = self.rect().adjusted(18, 3, -3, -3)
|
||||
if rect.width() < 1 or rect.height() < 1:
|
||||
painter.end()
|
||||
return
|
||||
|
||||
painter.fillRect(rect, QColor("#080808"))
|
||||
|
||||
db_markings = [(-40, "#1a1a1a"), (-30, "#1a1a1a"), (-20, "#1a1a1a"),
|
||||
(-12, "#2a2a1a"), (-6, "#2a1a1a"), (0, "#2a0a0a")]
|
||||
for db, color in db_markings:
|
||||
db_markings = [(-40,), (-30,), (-20,), (-12,), (-6,), (0,)]
|
||||
for (db,) in db_markings:
|
||||
pct = (db + 46) / 46.0 if db <= 0 else 1.0
|
||||
pct = max(0.0, min(1.0, pct))
|
||||
y = rect.bottom() - int(rect.height() * pct)
|
||||
painter.setPen(QPen(QColor(color), 1))
|
||||
if db == -6:
|
||||
painter.setPen(QPen(QColor("#00ff88"), 2))
|
||||
painter.drawLine(rect.right() - 3, y, rect.right(), y)
|
||||
painter.setFont(QFont("Segoe UI", 8, QFont.Bold))
|
||||
painter.setPen(QPen(QColor("#ffffff"), 1))
|
||||
else:
|
||||
painter.setPen(QPen(QColor("#2a2a2a"), 1))
|
||||
painter.drawLine(rect.right() - 5, y, rect.right(), y)
|
||||
painter.setPen(QPen(QColor("#444444"), 1))
|
||||
painter.drawText(rect.left() - 13, y + 4, f"{db}")
|
||||
painter.setPen(QPen(QColor("#cccccc"), 1))
|
||||
painter.setFont(QFont("Segoe UI", 7))
|
||||
painter.drawText(rect.left() - 16, y + 3, f"{db}")
|
||||
|
||||
painter.setPen(QPen(QColor("#1a1a1a"), 1))
|
||||
painter.drawRect(rect)
|
||||
@@ -63,12 +70,13 @@ class VUMeter(QWidget):
|
||||
db = -46 * (1 - val) if val < 1.0 else 3
|
||||
fill_pct = (db + 46) / 49.0 if db <= 0 else 1.0
|
||||
fill_pct = max(0.0, min(1.0, fill_pct))
|
||||
fill_pct = math.sqrt(fill_pct)
|
||||
|
||||
fill_h = int(rect.height() * fill_pct)
|
||||
painter.fillRect(rect.x(), rect.bottom() - fill_h,
|
||||
rect.width(), fill_h, gradient)
|
||||
|
||||
for db, _ in db_markings:
|
||||
for (db,) in db_markings:
|
||||
pct = (db + 46) / 46.0 if db <= 0 else 1.0
|
||||
pct = max(0.0, min(1.0, pct))
|
||||
y = rect.bottom() - int(rect.height() * pct)
|
||||
@@ -99,7 +107,7 @@ class ChannelStrip(QFrame):
|
||||
self._connect_signals()
|
||||
|
||||
self.timer = QTimer(self)
|
||||
self.timer.setInterval(80)
|
||||
self.timer.setInterval(30)
|
||||
self.timer.timeout.connect(self._refresh)
|
||||
self.timer.start()
|
||||
|
||||
@@ -126,13 +134,15 @@ class ChannelStrip(QFrame):
|
||||
""")
|
||||
root.addWidget(self.name_btn)
|
||||
|
||||
meter_row = QHBoxLayout()
|
||||
meter_row.setSpacing(2)
|
||||
|
||||
self.vu_meter = VUMeter()
|
||||
root.addWidget(self.vu_meter, stretch=1)
|
||||
meter_row.addWidget(self.vu_meter, stretch=1)
|
||||
|
||||
self.volume_slider = QSlider(Qt.Vertical)
|
||||
self.volume_slider.setRange(0, 100)
|
||||
self.volume_slider.setValue(100)
|
||||
self.volume_slider.setFixedHeight(60)
|
||||
self.volume_slider.setValue(int(math.sqrt(self._channel.volume) * 100))
|
||||
self.volume_slider.setStyleSheet("""
|
||||
QSlider::groove:vertical {
|
||||
background: #22242a;
|
||||
@@ -147,13 +157,19 @@ class ChannelStrip(QFrame):
|
||||
border-radius: 2px;
|
||||
}
|
||||
QSlider::handle:vertical:hover { background: #b0b8c8; }
|
||||
QSlider::sub-page:vertical {
|
||||
QSlider::add-page:vertical {
|
||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||
stop:0 #00cc66, stop:1 #008844);
|
||||
border-radius: 2px;
|
||||
}
|
||||
QSlider::sub-page:vertical {
|
||||
background: transparent;
|
||||
border-radius: 2px;
|
||||
}
|
||||
""")
|
||||
root.addWidget(self.volume_slider, alignment=Qt.AlignCenter)
|
||||
meter_row.addWidget(self.volume_slider, stretch=1)
|
||||
|
||||
root.addLayout(meter_row, stretch=1)
|
||||
|
||||
btn_row = QHBoxLayout()
|
||||
btn_row.setSpacing(3)
|
||||
@@ -238,7 +254,8 @@ class ChannelStrip(QFrame):
|
||||
self.name_btn.setText(self._channel.name)
|
||||
|
||||
def _on_volume_changed(self, value: int):
|
||||
self.engine.set_channel_volume(self.channel_index, value / 100.0)
|
||||
vol = (value / 100.0) ** 2
|
||||
self.engine.set_channel_volume(self.channel_index, vol)
|
||||
|
||||
def _on_mute_toggled(self, checked: bool):
|
||||
self.engine.set_channel_mute(self.channel_index, checked)
|
||||
@@ -251,6 +268,8 @@ class ChannelStrip(QFrame):
|
||||
|
||||
def _refresh(self):
|
||||
self.vu_meter.set_value(self._channel.vu_peak)
|
||||
self.volume_slider.setValue(int(math.sqrt(self._channel.volume) * 100))
|
||||
self.volume_slider.setValue(int(math.sqrt(self._channel.volume) * 100))
|
||||
|
||||
def apply_theme(self, theme: dict):
|
||||
pass
|
||||
@@ -262,8 +281,10 @@ class MasterSection(QFrame):
|
||||
self.engine = engine
|
||||
self._master = engine.mixer['master']
|
||||
|
||||
self._knobs = {}
|
||||
|
||||
self.setMinimumWidth(180)
|
||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
self.setStyleSheet("""
|
||||
MasterSection {
|
||||
background-color: #181a1e;
|
||||
@@ -276,7 +297,7 @@ class MasterSection(QFrame):
|
||||
self._connect_signals()
|
||||
|
||||
self.timer = QTimer(self)
|
||||
self.timer.setInterval(80)
|
||||
self.timer.setInterval(30)
|
||||
self.timer.timeout.connect(self._refresh)
|
||||
self.timer.start()
|
||||
|
||||
@@ -355,9 +376,10 @@ class MasterSection(QFrame):
|
||||
("REL", "release_ms", 100, 10, 1000, "ms"),
|
||||
("MKG", "makeup_gain_db", 0, 0, 24, "dB"),
|
||||
]:
|
||||
k = self._make_knob(label, self._master.compressor, attr,
|
||||
k, dial, val_label = self._make_knob(label, self._master.compressor, attr,
|
||||
default, lo, hi, suffix)
|
||||
knobs.addWidget(k)
|
||||
self._knobs[('compressor', attr)] = (dial, val_label, lo, hi, suffix)
|
||||
comp_layout.addLayout(knobs)
|
||||
root.addWidget(comp_frame)
|
||||
|
||||
@@ -402,9 +424,10 @@ class MasterSection(QFrame):
|
||||
|
||||
lim_knobs = QHBoxLayout()
|
||||
lim_knobs.setSpacing(4)
|
||||
k = self._make_knob("CEIL", self._master.limiter, "ceiling_db",
|
||||
k, dial, val_label = self._make_knob("CEIL", self._master.limiter, "ceiling_db",
|
||||
-0.1, -10, 0, "dB")
|
||||
lim_knobs.addWidget(k)
|
||||
self._knobs[('limiter', 'ceiling_db')] = (dial, val_label, -10, 0, "dB")
|
||||
lim_layout.addLayout(lim_knobs)
|
||||
root.addWidget(lim_frame)
|
||||
root.addStretch()
|
||||
@@ -467,7 +490,7 @@ class MasterSection(QFrame):
|
||||
dial.valueChanged.connect(_on_dial)
|
||||
layout.addWidget(dial, alignment=Qt.AlignCenter)
|
||||
layout.addWidget(val_label)
|
||||
return frame
|
||||
return frame, dial, val_label
|
||||
|
||||
def _connect_signals(self):
|
||||
self.comp_bypass.toggled.connect(
|
||||
@@ -493,6 +516,22 @@ class MasterSection(QFrame):
|
||||
"color: #556; background: transparent; border: none;"
|
||||
)
|
||||
|
||||
for (section, attr), (dial, val_label, lo, hi, suffix) in self._knobs.items():
|
||||
obj = self._master.compressor if section == 'compressor' else self._master.limiter
|
||||
val = getattr(obj, attr, lo)
|
||||
dial.blockSignals(True)
|
||||
dial.setValue(int((val - lo) / (hi - lo) * 1000))
|
||||
dial.blockSignals(False)
|
||||
val_label.setText(f"{val}{suffix}")
|
||||
|
||||
for (section, attr), (dial, val_label, lo, hi, suffix) in self._knobs.items():
|
||||
obj = self._master.compressor if section == 'compressor' else self._master.limiter
|
||||
val = getattr(obj, attr, lo)
|
||||
dial.blockSignals(True)
|
||||
dial.setValue(int((val - lo) / (hi - lo) * 1000))
|
||||
dial.blockSignals(False)
|
||||
val_label.setText(f"{val}{suffix}")
|
||||
|
||||
def apply_theme(self, theme: dict):
|
||||
pass
|
||||
|
||||
|
||||
+37
-27
@@ -2,7 +2,7 @@
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QFrame, QHBoxLayout, QVBoxLayout, QLabel, QPushButton,
|
||||
QComboBox, QScrollArea, QWidget
|
||||
QComboBox, QScrollArea, QWidget, QListWidget
|
||||
)
|
||||
from PySide6.QtCore import Qt, QTimer, Signal, QSize, QPoint
|
||||
from PySide6.QtGui import QFont
|
||||
@@ -134,7 +134,6 @@ class TopBarWidget(QFrame):
|
||||
|
||||
def _toggle_settings_popup(self):
|
||||
self._popup = SettingsPopup(self._midi_engine, self)
|
||||
self._popup.ratio_changed.connect(self.ratio_changed.emit)
|
||||
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))
|
||||
@@ -244,14 +243,12 @@ POPUP_STYLE = """
|
||||
|
||||
|
||||
class SettingsPopup(QFrame):
|
||||
ratio_changed = Signal(int, int)
|
||||
save_session = Signal()
|
||||
load_session = Signal()
|
||||
|
||||
def __init__(self, midi_engine, parent=None):
|
||||
super().__init__(parent)
|
||||
self._midi_engine = midi_engine
|
||||
self._ratio_index = 1
|
||||
|
||||
self.setWindowFlags(Qt.Popup | Qt.FramelessWindowHint)
|
||||
self.setObjectName("settingsPopup")
|
||||
@@ -265,21 +262,9 @@ class SettingsPopup(QFrame):
|
||||
root.setContentsMargins(12, 10, 12, 10)
|
||||
root.setSpacing(6)
|
||||
|
||||
# Row 1: Aspect ratio + Save/Load
|
||||
# Row 1: Save/Load
|
||||
row1 = QHBoxLayout()
|
||||
row1.setSpacing(6)
|
||||
self._ratio_btns = []
|
||||
for i, (label, w, h) in enumerate(RATIOS):
|
||||
btn = QPushButton(label)
|
||||
btn.setObjectName("ratioBtn")
|
||||
btn.setFixedHeight(24)
|
||||
btn.setCheckable(True)
|
||||
btn.setChecked(i == self._ratio_index)
|
||||
btn.clicked.connect(partial(self._set_ratio, i))
|
||||
row1.addWidget(btn)
|
||||
self._ratio_btns.append(btn)
|
||||
|
||||
row1.addSpacing(8)
|
||||
save_btn = QPushButton("SAVE")
|
||||
save_btn.setObjectName("popupBtn")
|
||||
save_btn.setFixedHeight(24)
|
||||
@@ -326,7 +311,7 @@ class SettingsPopup(QFrame):
|
||||
|
||||
scroll = QScrollArea()
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setFixedHeight(170)
|
||||
scroll.setFixedHeight(200)
|
||||
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
scroll.setStyleSheet(POPUP_STYLE)
|
||||
|
||||
@@ -347,6 +332,14 @@ class SettingsPopup(QFrame):
|
||||
("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),
|
||||
@@ -392,7 +385,7 @@ class SettingsPopup(QFrame):
|
||||
font-weight: bold;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
QPushButton:hover { background-color: #5a2a2a; }
|
||||
QPushButton:hover { background-color: #5a2a2a; }
|
||||
""")
|
||||
clear_btn.setFixedSize(20, 20)
|
||||
clear_btn.clicked.connect(partial(self._clear_mapping, action, target_id))
|
||||
@@ -403,19 +396,36 @@ class SettingsPopup(QFrame):
|
||||
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)
|
||||
|
||||
# ── Ratio ──────────────────────────────────────────────────────
|
||||
|
||||
def _set_ratio(self, index):
|
||||
self._ratio_index = index
|
||||
for i, btn in enumerate(self._ratio_btns):
|
||||
btn.setChecked(i == index)
|
||||
_, w, h = RATIOS[index]
|
||||
self.ratio_changed.emit(w, h)
|
||||
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 ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user