303 lines
8.7 KiB
Python
303 lines
8.7 KiB
Python
from PySide6.QtCore import Qt, QSettings, QTimer
|
|
from PySide6.QtGui import QFont
|
|
from PySide6.QtWidgets import (
|
|
QWidget,
|
|
QLabel,
|
|
QVBoxLayout,
|
|
QHBoxLayout,
|
|
QPushButton,
|
|
QSlider,
|
|
QButtonGroup,
|
|
QScrollArea,
|
|
QScroller,
|
|
)
|
|
from audio.system_volume import set_volume
|
|
import build_info
|
|
|
|
|
|
class SoundScreen(QWidget):
|
|
def __init__(self, on_eq):
|
|
super().__init__()
|
|
self._on_eq = on_eq
|
|
self._settings = QSettings("car_ui", "ui")
|
|
self._pending_volume: int | None = None
|
|
self._volume_apply_timer = QTimer(self)
|
|
self._volume_apply_timer.setSingleShot(True)
|
|
self._volume_apply_timer.timeout.connect(self._flush_volume)
|
|
|
|
root = QVBoxLayout(self)
|
|
root.setContentsMargins(0, 0, 0, 0)
|
|
root.setSpacing(12)
|
|
|
|
scroll = QScrollArea()
|
|
scroll.setWidgetResizable(True)
|
|
scroll.setFrameShape(QScrollArea.NoFrame)
|
|
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
|
scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
|
|
|
scroller = QScroller.scroller(scroll.viewport())
|
|
scroller.grabGesture(
|
|
scroll.viewport(),
|
|
QScroller.LeftMouseButtonGesture
|
|
)
|
|
|
|
content = QWidget()
|
|
content_layout = QVBoxLayout(content)
|
|
content_layout.setContentsMargins(0, 0, 0, 0)
|
|
content_layout.setSpacing(12)
|
|
|
|
content_layout.addWidget(self._build_volume_card())
|
|
content_layout.addWidget(self._build_premute_card())
|
|
content_layout.addWidget(self._build_ducking_card())
|
|
content_layout.addWidget(self._build_balance_card())
|
|
content_layout.addWidget(self._build_tone_card())
|
|
content_layout.addWidget(self._build_eq_card())
|
|
content_layout.addStretch(1)
|
|
|
|
scroll.setWidget(content)
|
|
root.addWidget(scroll, 1)
|
|
|
|
def _read_int(self, key: str, default: int) -> int:
|
|
return _read_int_setting(self._settings, key, default)
|
|
|
|
def _queue_volume_apply(self, value: int):
|
|
self._pending_volume = value
|
|
self._volume_apply_timer.start(120)
|
|
|
|
def _flush_volume(self):
|
|
if self._pending_volume is None:
|
|
return
|
|
self._set_system_volume(self._pending_volume)
|
|
|
|
def _set_system_volume(self, value: int):
|
|
set_volume(value)
|
|
|
|
def _build_volume_card(self) -> QWidget:
|
|
card, body = _card("Громкость")
|
|
default_value = build_info.DEFAULT_SOUND_VOLUME
|
|
value = self._read_int("sound/base_volume", default_value)
|
|
row, slider, value_label = _slider_row(
|
|
"Базовая громкость",
|
|
0,
|
|
100,
|
|
value,
|
|
lambda v: f"{v}%",
|
|
)
|
|
slider.valueChanged.connect(
|
|
lambda v: self._settings.setValue("sound/base_volume", v)
|
|
)
|
|
slider.valueChanged.connect(self._queue_volume_apply)
|
|
body.addWidget(row)
|
|
return card
|
|
|
|
def _build_premute_card(self) -> QWidget:
|
|
card, body = _card("Премут")
|
|
premute_enabled = False
|
|
premute_value = self._read_int(
|
|
"sound/premute_volume",
|
|
build_info.DEFAULT_PREMUTE_VOLUME,
|
|
)
|
|
|
|
row_toggle, toggle_btn = _toggle_row("Премут", checked=premute_enabled)
|
|
row_slider, slider, value_label = _slider_row(
|
|
"Громкость премут",
|
|
0,
|
|
100,
|
|
premute_value,
|
|
lambda v: f"{v}%",
|
|
)
|
|
slider.valueChanged.connect(
|
|
lambda v: self._settings.setValue("sound/premute_volume", v)
|
|
)
|
|
|
|
body.addWidget(row_toggle)
|
|
body.addWidget(row_slider)
|
|
return card
|
|
|
|
def _build_ducking_card(self) -> QWidget:
|
|
card, body = _card("Ducking")
|
|
ducking_enabled = True
|
|
ducking_value = self._read_int(
|
|
"sound/ducking_volume",
|
|
build_info.DEFAULT_DUCKING_VOLUME,
|
|
)
|
|
|
|
row_toggle, toggle_btn = _toggle_row("Ducking", checked=ducking_enabled)
|
|
row_slider, slider, value_label = _slider_row(
|
|
"Громкость при Ducking",
|
|
0,
|
|
100,
|
|
ducking_value,
|
|
lambda v: f"{v}%",
|
|
)
|
|
slider.valueChanged.connect(
|
|
lambda v: self._settings.setValue("sound/ducking_volume", v)
|
|
)
|
|
|
|
body.addWidget(row_toggle)
|
|
body.addWidget(row_slider)
|
|
return card
|
|
|
|
def _build_balance_card(self) -> QWidget:
|
|
card, body = _card("Баланс и фейдер")
|
|
row, slider, value_label = _slider_row(
|
|
"Баланс L/R",
|
|
-50,
|
|
50,
|
|
0,
|
|
_format_balance,
|
|
)
|
|
body.addWidget(row)
|
|
row, slider, value_label = _slider_row(
|
|
"Фейдер F/R",
|
|
-50,
|
|
50,
|
|
0,
|
|
_format_fader,
|
|
)
|
|
body.addWidget(row)
|
|
return card
|
|
|
|
def _build_tone_card(self) -> QWidget:
|
|
card, body = _card("Тон")
|
|
row = QWidget()
|
|
row.setObjectName("SoundToneRow")
|
|
layout = QHBoxLayout(row)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
layout.setSpacing(10)
|
|
|
|
group = QButtonGroup(row)
|
|
group.setExclusive(True)
|
|
|
|
for idx, label in enumerate(("Тёплый", "Нейтральный", "Яркий")):
|
|
btn = QPushButton(label)
|
|
btn.setObjectName("SoundToneBtn")
|
|
btn.setCheckable(True)
|
|
btn.setMinimumHeight(40)
|
|
btn.setFont(QFont("", 13, 600))
|
|
if idx == 1:
|
|
btn.setChecked(True)
|
|
group.addButton(btn)
|
|
layout.addWidget(btn, 1)
|
|
|
|
body.addWidget(row)
|
|
return card
|
|
|
|
def _build_eq_card(self) -> QWidget:
|
|
card, body = _card("Эквалайзер")
|
|
btn = QPushButton("Открыть эквалайзер")
|
|
btn.setObjectName("SoundEqBtn")
|
|
btn.setMinimumHeight(48)
|
|
btn.setFont(QFont("", 14, 700))
|
|
btn.clicked.connect(self._on_eq)
|
|
body.addWidget(btn)
|
|
return card
|
|
|
|
|
|
def _card(title: str) -> tuple[QWidget, QVBoxLayout]:
|
|
card = QWidget()
|
|
card.setObjectName("SoundCard")
|
|
layout = QVBoxLayout(card)
|
|
layout.setContentsMargins(14, 12, 14, 12)
|
|
layout.setSpacing(10)
|
|
|
|
header = QLabel(title)
|
|
header.setObjectName("SoundCardTitle")
|
|
header.setFont(QFont("", 14, 700))
|
|
|
|
layout.addWidget(header)
|
|
return card, layout
|
|
|
|
|
|
def _toggle_row(label: str, checked: bool) -> tuple[QWidget, QPushButton]:
|
|
row = QWidget()
|
|
row.setObjectName("SoundToggleRow")
|
|
layout = QHBoxLayout(row)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
layout.setSpacing(12)
|
|
|
|
lbl = QLabel(label)
|
|
lbl.setFont(QFont("", 13, 600))
|
|
|
|
btn = QPushButton("Выкл")
|
|
btn.setObjectName("SoundToggle")
|
|
btn.setCheckable(True)
|
|
btn.setChecked(checked)
|
|
btn.setMinimumHeight(36)
|
|
btn.setMinimumWidth(86)
|
|
btn.setFont(QFont("", 12, 700))
|
|
|
|
def _sync_text(is_checked: bool):
|
|
btn.setText("Вкл" if is_checked else "Выкл")
|
|
|
|
btn.toggled.connect(_sync_text)
|
|
_sync_text(btn.isChecked())
|
|
|
|
layout.addWidget(lbl)
|
|
layout.addStretch(1)
|
|
layout.addWidget(btn)
|
|
return row, btn
|
|
|
|
|
|
def _slider_row(
|
|
label: str,
|
|
minimum: int,
|
|
maximum: int,
|
|
value: int,
|
|
formatter,
|
|
) -> tuple[QWidget, QSlider, QLabel]:
|
|
row = QWidget()
|
|
row.setObjectName("SoundSliderRow")
|
|
layout = QVBoxLayout(row)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
layout.setSpacing(6)
|
|
|
|
header = QWidget()
|
|
header_layout = QHBoxLayout(header)
|
|
header_layout.setContentsMargins(0, 0, 0, 0)
|
|
header_layout.setSpacing(8)
|
|
|
|
lbl = QLabel(label)
|
|
lbl.setFont(QFont("", 13, 600))
|
|
val = QLabel(formatter(value))
|
|
val.setObjectName("SoundValue")
|
|
val.setFont(QFont("", 12, 600))
|
|
val.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
|
|
|
header_layout.addWidget(lbl)
|
|
header_layout.addStretch(1)
|
|
header_layout.addWidget(val)
|
|
|
|
slider = QSlider(Qt.Horizontal)
|
|
slider.setObjectName("SoundSlider")
|
|
slider.setRange(minimum, maximum)
|
|
slider.setValue(value)
|
|
slider.valueChanged.connect(lambda v: val.setText(formatter(v)))
|
|
|
|
layout.addWidget(header)
|
|
layout.addWidget(slider)
|
|
return row, slider, val
|
|
|
|
|
|
def _format_balance(value: int) -> str:
|
|
if value == 0:
|
|
return "Центр"
|
|
side = "L" if value < 0 else "R"
|
|
return f"{side} {abs(value)}"
|
|
|
|
|
|
def _format_fader(value: int) -> str:
|
|
if value == 0:
|
|
return "Центр"
|
|
side = "F" if value < 0 else "R"
|
|
return f"{side} {abs(value)}"
|
|
|
|
|
|
def _read_int_setting(settings: QSettings, key: str, default: int) -> int:
|
|
raw = settings.value(key, default)
|
|
try:
|
|
return int(raw)
|
|
except (TypeError, ValueError):
|
|
return default
|
|
|