237 lines
9.7 KiB
Python
237 lines
9.7 KiB
Python
"""
|
||
Settings Window Module.
|
||
=======================
|
||
|
||
Manages the application configuration UI.
|
||
Refactored for 2026 Premium Aesthetics with Sidebar navigation.
|
||
"""
|
||
|
||
from PySide6.QtWidgets import (
|
||
QWidget, QVBoxLayout, QHBoxLayout, QStackedWidget,
|
||
QLabel, QComboBox, QFormLayout, QFrame, QMessageBox, QScrollArea
|
||
)
|
||
from PySide6.QtCore import Qt, Signal, Slot, QSize
|
||
from PySide6.QtGui import QFont, QIcon
|
||
|
||
from src.core.config import ConfigManager
|
||
from src.ui.styles import Theme, StyleGenerator, load_modern_fonts
|
||
from src.ui.components import FramelessWindow, ModernFrame, GlassButton, ModernSwitch, ModernSlider
|
||
import sounddevice as sd
|
||
|
||
class SettingsWindow(FramelessWindow):
|
||
"""
|
||
The main settings dialog.
|
||
Refactored with 2026 Premium Sidebar Layout.
|
||
"""
|
||
settings_changed = Signal()
|
||
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self.config = ConfigManager()
|
||
self.setFixedSize(700, 500)
|
||
|
||
# Main Container
|
||
self.bg_frame = ModernFrame()
|
||
self.bg_frame.setStyleSheet(StyleGenerator.get_glass_card(radius=20))
|
||
|
||
self.root_layout = QVBoxLayout(self)
|
||
self.root_layout.setContentsMargins(10, 10, 10, 10)
|
||
self.root_layout.addWidget(self.bg_frame)
|
||
|
||
# Title Bar Area (Inside glass card)
|
||
self.title_layout = QHBoxLayout()
|
||
self.title_layout.setContentsMargins(20, 15, 20, 0)
|
||
|
||
title_lbl = QLabel("PREMIUM SETTINGS")
|
||
title_lbl.setFont(load_modern_fonts())
|
||
title_lbl.setStyleSheet(f"color: white; font-weight: 900; font-size: 14px; letter-spacing: 2px;")
|
||
self.title_layout.addWidget(title_lbl)
|
||
|
||
self.title_layout.addStretch()
|
||
|
||
self.btn_close = GlassButton("×", accent_color="#ff4b4b")
|
||
self.btn_close.setFixedSize(30, 30)
|
||
self.btn_close.clicked.connect(self.close)
|
||
self.title_layout.addWidget(self.btn_close)
|
||
|
||
# Central Layout (Sidebar + Content)
|
||
self.content_layout = QHBoxLayout()
|
||
self.content_layout.setContentsMargins(10, 10, 10, 10)
|
||
self.content_layout.setSpacing(10)
|
||
|
||
# 1. SIDEBAR
|
||
self.sidebar = QWidget()
|
||
self.sidebar.setFixedWidth(160)
|
||
self.sidebar_layout = QVBoxLayout(self.sidebar)
|
||
self.sidebar_layout.setContentsMargins(0, 10, 0, 10)
|
||
self.sidebar_layout.setSpacing(8)
|
||
|
||
self.nav_general = GlassButton("General")
|
||
self.nav_audio = GlassButton("Audio")
|
||
self.nav_visuals = GlassButton("Visuals")
|
||
self.nav_advanced = GlassButton("Advanced/AI")
|
||
|
||
self.sidebar_layout.addWidget(self.nav_general)
|
||
self.sidebar_layout.addWidget(self.nav_audio)
|
||
self.sidebar_layout.addWidget(self.nav_visuals)
|
||
self.sidebar_layout.addWidget(self.nav_advanced)
|
||
self.sidebar_layout.addStretch()
|
||
|
||
self.btn_save = GlassButton("SAVE CHANGES", accent_color=Theme.ACCENT_GREEN)
|
||
self.btn_save.clicked.connect(self.save_settings)
|
||
self.sidebar_layout.addWidget(self.btn_save)
|
||
|
||
# 2. CONTENT STACK
|
||
self.stack = QStackedWidget()
|
||
self.stack.setStyleSheet("background: transparent;")
|
||
|
||
# Connect sidebar to stack
|
||
self.nav_general.clicked.connect(lambda: self.stack.setCurrentIndex(0))
|
||
self.nav_audio.clicked.connect(lambda: self.stack.setCurrentIndex(1))
|
||
self.nav_visuals.clicked.connect(lambda: self.stack.setCurrentIndex(2))
|
||
self.nav_advanced.clicked.connect(lambda: self.stack.setCurrentIndex(3))
|
||
|
||
# Main Layout Assembly
|
||
self.inner_layout = QVBoxLayout(self.bg_frame)
|
||
self.inner_layout.addLayout(self.title_layout)
|
||
self.inner_layout.addLayout(self.content_layout)
|
||
|
||
self.content_layout.addWidget(self.sidebar)
|
||
self.content_layout.addWidget(self.stack)
|
||
|
||
self.setup_pages()
|
||
self.load_values()
|
||
|
||
def setup_pages(self):
|
||
"""Creates the settings pages."""
|
||
# --- GENERAL ---
|
||
self.page_general = QWidget()
|
||
l1 = QFormLayout(self.page_general)
|
||
l1.setVerticalSpacing(20)
|
||
|
||
self.inp_hotkey = QComboBox()
|
||
self.inp_hotkey.addItems(["f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", "f11", "f12", "caps lock"])
|
||
self.inp_hotkey.setStyleSheet(f"background: {Theme.BG_DARK}; border-radius: 4px; padding: 5px; color: white;")
|
||
l1.addRow(self.create_lbl("Global Hotkey:"), self.inp_hotkey)
|
||
|
||
self.chk_top = ModernSwitch()
|
||
l1.addRow(self.create_lbl("Always on Top:"), self.chk_top)
|
||
|
||
self.stack.addWidget(self.page_general)
|
||
|
||
# --- AUDIO ---
|
||
self.page_audio = QWidget()
|
||
l2 = QFormLayout(self.page_audio)
|
||
l2.setVerticalSpacing(15)
|
||
|
||
self.inp_device = QComboBox()
|
||
self.inp_device.setStyleSheet(f"background: {Theme.BG_DARK}; border-radius: 4px; padding: 5px; color: white;")
|
||
self.populate_audio_devices()
|
||
l2.addRow(self.create_lbl("Input Device:"), self.inp_device)
|
||
|
||
self.sld_threshold = ModernSlider(Qt.Horizontal)
|
||
self.sld_threshold.setRange(1, 25)
|
||
self.lbl_threshold = self.create_lbl("2%")
|
||
self.sld_threshold.valueChanged.connect(lambda v: self.lbl_threshold.setText(f"{v}%"))
|
||
l2.addRow(self.create_lbl("Noise Gate:"), self.sld_threshold)
|
||
l2.addRow("", self.lbl_threshold)
|
||
|
||
self.sld_duration = ModernSlider(Qt.Horizontal)
|
||
self.sld_duration.setRange(5, 50)
|
||
self.lbl_duration = self.create_lbl("1.0s")
|
||
self.sld_duration.valueChanged.connect(lambda v: self.lbl_duration.setText(f"{v/10}s"))
|
||
l2.addRow(self.create_lbl("Auto-Submit:"), self.sld_duration)
|
||
l2.addRow("", self.lbl_duration)
|
||
|
||
self.stack.addWidget(self.page_audio)
|
||
|
||
# --- VISUALS ---
|
||
self.page_visuals = QWidget()
|
||
l3 = QFormLayout(self.page_visuals)
|
||
l3.setVerticalSpacing(20)
|
||
|
||
self.inp_style = QComboBox()
|
||
self.inp_style.addItem("Neon Line (Recommended)", "line")
|
||
self.inp_style.addItem("Classic Bars", "bar")
|
||
self.inp_style.setStyleSheet(f"background: {Theme.BG_DARK}; border-radius: 4px; padding: 5px; color: white;")
|
||
l3.addRow(self.create_lbl("Visualizer:"), self.inp_style)
|
||
|
||
self.sld_opacity = ModernSlider(Qt.Horizontal)
|
||
self.sld_opacity.setRange(40, 100)
|
||
self.lbl_opacity = self.create_lbl("100%")
|
||
self.sld_opacity.valueChanged.connect(lambda v: self.lbl_opacity.setText(f"{v}%"))
|
||
l3.addRow(self.create_lbl("Opacity:"), self.sld_opacity)
|
||
l3.addRow("", self.lbl_opacity)
|
||
|
||
self.stack.addWidget(self.page_visuals)
|
||
|
||
# --- ADVANCED ---
|
||
self.page_adv = QWidget()
|
||
l4 = QFormLayout(self.page_adv)
|
||
l4.setVerticalSpacing(15)
|
||
|
||
self.inp_model = QComboBox()
|
||
self.inp_model.setStyleSheet(f"background: {Theme.BG_DARK}; border-radius: 4px; padding: 5px; color: white;")
|
||
for id, name in [("tiny", "Tiny (Fast)"), ("base", "Base"), ("small", "Small (Default)"), ("medium", "Medium"), ("large-v3", "Large V3")]:
|
||
self.inp_model.addItem(name, id)
|
||
l4.addRow(self.create_lbl("Model:"), self.inp_model)
|
||
|
||
info = QLabel("Large models provide higher accuracy but require significant RAM/VRAM.")
|
||
info.setWordWrap(True)
|
||
info.setStyleSheet(f"color: {Theme.TEXT_SECONDARY}; font-style: italic; font-size: 11px;")
|
||
l4.addRow("", info)
|
||
|
||
self.stack.addWidget(self.page_adv)
|
||
|
||
def create_lbl(self, text):
|
||
lbl = QLabel(text)
|
||
lbl.setStyleSheet(f"color: {Theme.TEXT_SECONDARY}; font-weight: 600; font-size: 13px;")
|
||
return lbl
|
||
|
||
def populate_audio_devices(self):
|
||
try:
|
||
self.inp_device.addItem("System Default", -1)
|
||
for i, dev in enumerate(sd.query_devices()):
|
||
if dev['max_input_channels'] > 0:
|
||
self.inp_device.addItem(dev['name'], i)
|
||
except: pass
|
||
|
||
def load_values(self):
|
||
self.inp_hotkey.setCurrentText(self.config.get("hotkey"))
|
||
self.chk_top.setChecked(self.config.get("always_on_top"))
|
||
|
||
dev_id = self.config.get("input_device")
|
||
idx = self.inp_device.findData(dev_id if dev_id is not None else -1)
|
||
if idx >= 0: self.inp_device.setCurrentIndex(idx)
|
||
|
||
self.sld_threshold.setValue(int(self.config.get("silence_threshold") * 100))
|
||
self.sld_duration.setValue(int(self.config.get("silence_duration") * 10))
|
||
|
||
idx = self.inp_style.findData(self.config.get("visualizer_style"))
|
||
if idx >= 0: self.inp_style.setCurrentIndex(idx)
|
||
|
||
self.sld_opacity.setValue(int(self.config.get("opacity") * 100))
|
||
|
||
idx = self.inp_model.findData(self.config.get("model_size"))
|
||
if idx >= 0: self.inp_model.setCurrentIndex(idx)
|
||
|
||
def save_settings(self):
|
||
updates = {
|
||
"hotkey": self.inp_hotkey.currentText(),
|
||
"always_on_top": self.chk_top.isChecked(),
|
||
"input_device": self.inp_device.currentData() if self.inp_device.currentData() != -1 else None,
|
||
"silence_threshold": self.sld_threshold.value() / 100.0,
|
||
"silence_duration": self.sld_duration.value() / 10.0,
|
||
"visualizer_style": self.inp_style.currentData(),
|
||
"opacity": self.sld_opacity.value() / 100.0,
|
||
"model_size": self.inp_model.currentData()
|
||
}
|
||
|
||
new_model = updates["model_size"]
|
||
if new_model != self.config.get("model_size"):
|
||
QMessageBox.information(self, "Model Updated", f"Downloaded {new_model} on next launch.")
|
||
|
||
self.config.set_bulk(updates)
|
||
self.settings_changed.emit()
|
||
self.close()
|