""" 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()