/
Flipper
/
Marginal
Обзор
Документация
Войти
/
Flipper
/
Marginal
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
1
CI/CD
Аналитика
Безопасность
master
src/UI/SettingsDialog.py
831 строка
28 KB
Alex
Marginal v1.0.0 RELEASE
11 янв 2026, 18:02
11 янв 2026, 18:02
5e1eff8
Код
Авторство
О чём код?
import os from PySide6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QComboBox, QLineEdit, QCheckBox, QListWidget, QStackedWidget, QWidget, QScrollArea, QGroupBox ) from PySide6.QtGui import QIcon from .CustomUI.LineDirSelector import FolderLineEdit as LineDirSelector from src.utils import listDir, getBasePath class SettingsDialog(QDialog): """ Comprehensive settings dialog for configuring application behavior. Includes tabs for Export, Graphing, Appearance, and Security settings. """ def __init__(self, backbone): """ Initialize SettingsDialog with application backbone. Args: backbone (Backbone): Central application controller. """ super().__init__() self.backbone = backbone self.translate = self.backbone.translator.translate self.configManager = self.backbone.configManager self.loadSettings() # Load current settings self.setSettings() # Apply to internal variables self.initWindow() self.initUI() def initWindow(self): """ Set up dialog window properties (title, icon, size). Returns: None """ self.setWindowTitle(self.translate("Settings")) self.setWindowIcon(QIcon(":/resources/sprites/Icon_settings_small.png")) self.setFixedSize(600, 400) def initUI(self): """ Initialize all UI components. Returns: None """ self.setUpLayout() self.setUpSideBar() self.setUpCentralWidget() self.setValues() self.sidebar.currentRowChanged.connect(self.displaySettings) self.setUpButtons() def setUpLayout(self): """ Set up main dialog layout. Returns: None """ self.mainLayout = QVBoxLayout(self) self.contentLayout = QHBoxLayout() self.setLayout(self.mainLayout) self.mainLayout.addLayout(self.contentLayout) def setUpCentralWidget(self): """ Set up the central stacked widget for settings pages. Returns: None """ self.settingsStack = QStackedWidget() self.createPages() self.contentLayout.addWidget(self.settingsStack, 1) # Takes remaining space self.settingsStack.currentWidget().layout().activate() def setUpSideBar(self): """ Set up sidebar navigation list. Returns: None """ self.sidebar = QListWidget() self.sidebar.addItems([ self.translate("Export"), self.translate("Graphing"), self.translate("Appearance"), self.translate("Security") ]) self.sidebar.setFixedWidth(120) self.contentLayout.addWidget(self.sidebar, 0) # 0 stretch (fixed width) self.sidebar.setCurrentRow(0) def createPages(self): """ Create all settings pages and add to stacked widget. Returns: None """ self.exportSettings = self.createExportPage() self.graphingSettings = self.createGraphingPage() self.themesSettings = self.createAppearancePage() self.locksmithSettings = self.createLocksmithPage() self.settingsStack.addWidget(self.exportSettings) self.settingsStack.addWidget(self.graphingSettings) self.settingsStack.addWidget(self.themesSettings) self.settingsStack.addWidget(self.locksmithSettings) def createExportPage(self): """ Create the Export settings page. Returns: QWidget -> Configured export settings page widget. """ widget = QWidget() layout = QVBoxLayout(widget) layout.setContentsMargins(0, 0, 0, 0) scrollArea = QScrollArea() scrollArea.setWidgetResizable(True) scrollContent = QWidget() scrollLayout = QVBoxLayout(scrollContent) # File extension selection extRowLayout = QHBoxLayout() self.extLabel = QLabel(self.translate("Default output extension:")) self.extCombo = QComboBox() extRowLayout.addWidget(self.extLabel) extRowLayout.addWidget(self.extCombo) # CSV delimiter selection delimRowLayout = QHBoxLayout() self.delimiterLabel = QLabel(self.translate("Value delimiter for .csv files:")) self.delimiterCombo = QComboBox() delimRowLayout.addWidget(self.delimiterLabel) delimRowLayout.addWidget(self.delimiterCombo) # Output filename self.fileNameLabel = QLabel(self.translate("Default output file name:")) self.fileNameInput = QLineEdit() self.fileNameInput.textChanged.connect(self.checkFilename) # Export path selection self.exportPathLabel = QLabel(self.translate("Default output path:")) self.exportPathInput = LineDirSelector() # Source directory selection self.sourceDirLabel = QLabel(self.translate("Default source search dir:")) self.sourceDirInput = LineDirSelector() # Profile directory selection self.profileDirLabel = QLabel(self.translate("Default profile search dir:")) self.profileDirInput = LineDirSelector() # Output directory selection self.outputDirLabel = QLabel(self.translate("Default output search dir:")) self.outputDirInput = LineDirSelector() # Checkboxes for options self.createCalcLogCheck = QCheckBox(self.translate("Create calculations log")) self.openFileCheck = QCheckBox(self.translate("Open file when finished")) # Reset button self.resetButton = QPushButton(self.translate("Reset settings")) self.resetButton.setObjectName("ResetBTN") self.resetButton.clicked.connect(self.resetExportSettings) # Assemble layout scrollLayout.addLayout(extRowLayout) scrollLayout.addLayout(delimRowLayout) scrollLayout.addWidget(self.fileNameLabel) scrollLayout.addWidget(self.fileNameInput) scrollLayout.addWidget(self.exportPathLabel) scrollLayout.addWidget(self.exportPathInput) scrollLayout.addWidget(self.sourceDirLabel) scrollLayout.addWidget(self.sourceDirInput) scrollLayout.addWidget(self.profileDirLabel) scrollLayout.addWidget(self.profileDirInput) scrollLayout.addWidget(self.outputDirLabel) scrollLayout.addWidget(self.outputDirInput) scrollLayout.addWidget(self.createCalcLogCheck) scrollLayout.addWidget(self.openFileCheck) scrollLayout.addWidget(self.resetButton) scrollLayout.addStretch() scrollArea.setWidget(scrollContent) layout.addWidget(scrollArea) return widget def createGraphingPage(self): """ Create the Graphing settings page. Returns: QWidget -> Configured graphing settings page widget. """ widget = QWidget() layout = QVBoxLayout(widget) layout.setContentsMargins(0, 0, 0, 0) scrollArea = QScrollArea() scrollArea.setWidgetResizable(True) scrollContent = QWidget() scrollLayout = QVBoxLayout(scrollContent) # Enable graphing checkbox enabledLayout = QHBoxLayout() self.enabledCB = QCheckBox(self.translate("Enable graphing.")) enabledLayout.addWidget(self.enabledCB) enabledLayout.addStretch() scrollLayout.addLayout(enabledLayout) # Graphing options group graphingGroup = QGroupBox(self.translate("Graphing Options")) graphingGroupLayout = QVBoxLayout() # Graph file extension selection extRowLayout = QHBoxLayout() self.extGraphLabel = QLabel(self.translate("Default output extension: ")) self.extGraphCombo = QComboBox() extRowLayout.addWidget(self.extGraphLabel) extRowLayout.addWidget(self.extGraphCombo) extRowLayout.addStretch() graphingGroupLayout.addLayout(extRowLayout) graphingGroupLayout.addStretch() # Graph style selection styleRowLayout = QHBoxLayout() self.styleGraphLabel = QLabel(self.translate("Graph style: ")) self.styleGraphCombo = QComboBox() styleRowLayout.addWidget(self.styleGraphLabel) styleRowLayout.addWidget(self.styleGraphCombo) styleRowLayout.addStretch() graphingGroupLayout.addLayout(styleRowLayout) graphingGroupLayout.addStretch() # Style overwrite permission self.styleOverwriteCB = QCheckBox(self.translate("Allow style overwrite")) graphingGroupLayout.addWidget(self.styleOverwriteCB) graphingGroupLayout.addStretch() graphingGroup.setLayout(graphingGroupLayout) scrollLayout.addWidget(graphingGroup) scrollLayout.addStretch() # List of widgets to disable when graphing is disabled self.graphingWidgets = [graphingGroup] # Connect checkbox to enable/disable graphing widgets self.enabledCB.stateChanged.connect(self.toggleGraphingWidgets) self.toggleGraphingWidgets(self.enabledGraphing) scrollArea.setWidget(scrollContent) layout.addWidget(scrollArea) return widget def createAppearancePage(self): """ Create the Appearance settings page (theme and language). Returns: QWidget -> Configured appearance settings page widget. """ widget = QWidget() layout = QVBoxLayout(widget) layout.setContentsMargins(0, 0, 0, 0) scrollArea = QScrollArea() scrollArea.setWidgetResizable(True) scrollContent = QWidget() scrollLayout = QVBoxLayout(scrollContent) # Theme selection themeLabel = QLabel(self.translate("Theme:")) self.themeCombo = QComboBox() self.themeCombo.setCurrentIndex(self.themeCombo.findText(self.currentTheme)) # Language selection langLabel = QLabel(self.translate("Language:")) self.langCombo = QComboBox() scrollLayout.addWidget(themeLabel) scrollLayout.addWidget(self.themeCombo) scrollLayout.addWidget(langLabel) scrollLayout.addWidget(self.langCombo) scrollLayout.addStretch() scrollArea.setWidget(scrollContent) layout.addWidget(scrollArea) return widget def createLocksmithPage(self): """ Create the Security (Locksmith) settings page. Returns: QWidget -> Configured security settings page widget. """ widget = QWidget() layout = QVBoxLayout(widget) layout.setContentsMargins(0, 0, 0, 0) self.newPass = None # Store new password if set scrollArea = QScrollArea() scrollArea.setWidgetResizable(True) scrollContent = QWidget() scrollLayout = QVBoxLayout(scrollContent) # Enable function locking checkbox enableLayout = QHBoxLayout() self.lockEnabledCB = QCheckBox(self.translate("Enable function locking")) enableLayout.addWidget(self.lockEnabledCB) enableLayout.addStretch() scrollLayout.addLayout(enableLayout) # Password settings group passwordGroup = QGroupBox(self.translate("Password Settings")) passwordLayout = QVBoxLayout() # Password set button self.passwordInputButton = QPushButton(self.translate("Set new password")) self.passwordInputButton.clicked.connect(self.getNewPass) # Log password checkbox self.logPasswordCB = QCheckBox(self.translate("Write new passwords to log.")) passwordLayout.addWidget(self.passwordInputButton) passwordLayout.addWidget(self.logPasswordCB) # Password frequency selection freqLayout = QHBoxLayout() self.freqLabel = QLabel(self.translate("Require password:")) self.freqCombo = QComboBox() freqLayout.addWidget(self.freqLabel) freqLayout.addWidget(self.freqCombo) freqLayout.addStretch() passwordLayout.addLayout(freqLayout) passwordGroup.setLayout(passwordLayout) scrollLayout.addWidget(passwordGroup) scrollLayout.addStretch() # Connect enable checkbox to toggle other widgets self.lockEnabledCB.stateChanged.connect(self.toggleLocksmithWidgets) self.locksmithWidgets = [passwordGroup] self.toggleLocksmithWidgets(self.locksmithEnabled) scrollArea.setWidget(scrollContent) layout.addWidget(scrollArea) return widget def getNewPass(self): """ Trigger new password dialog and store result. Returns: None """ self.newPass = self.backbone.getNewPass() def toggleGraphingWidgets(self, state: bool | int): """ Enable or disable graphing widgets based on checkbox state. Args: state (bool | int): Checkbox state (True/False or Qt.CheckState). Returns: None """ # Convert state to boolean (Qt.CheckState.Checked = 2) if isinstance(state, int): enabled = state == 2 else: enabled = bool(state) for widget in self.graphingWidgets: widget.setEnabled(enabled) def toggleLocksmithWidgets(self, state: bool | int): """ Enable or disable locksmith widgets based on checkbox state. Args: state (bool | int): Checkbox state (True/False or Qt.CheckState). Returns: None """ if isinstance(state, int): enabled = state == 2 else: enabled = bool(state) for widget in self.locksmithWidgets: widget.setEnabled(enabled) def setUpButtons(self): """ Set up dialog action buttons (Cancel, Apply, OK). Returns: None """ buttonLayout = QHBoxLayout() buttonLayout.addStretch() cancelButton = QPushButton(self.translate("Cancel")) applyButton = QPushButton(self.translate("Apply")) okButton = QPushButton(self.translate("OK")) cancelButton.clicked.connect(self.cancelSettings) applyButton.clicked.connect(self.applySettings) okButton.clicked.connect(self.acceptSettings) buttonLayout.addWidget(cancelButton) buttonLayout.addWidget(applyButton) buttonLayout.addWidget(okButton) self.mainLayout.addLayout(buttonLayout) def resetExportSettings(self): """ Reset export settings to default values. Returns: None """ defaultSettings = self.configManager.getDefault()["exportOptions"] self.extCombo.setCurrentIndex(self.extCombo.findText(defaultSettings["extension"])) self.fileNameInput.setText(defaultSettings["outputFilename"]) self.exportPathInput.setText(os.path.join(getBasePath(), defaultSettings["exportPath"])) self.sourceDirInput.setText(os.path.expanduser(defaultSettings["inputDataSearchFolder"])) self.profileDirInput.setText( os.path.expanduser(os.path.join(getBasePath(), defaultSettings["profileSearchFolder"]))) self.outputDirInput.setText(os.path.expanduser(os.path.join(getBasePath(), defaultSettings["exportPath"]))) self.createCalcLogCheck.setChecked(defaultSettings["createCalcLog"]) self.openFileCheck.setChecked(defaultSettings["openOnFinish"]) def displaySettings(self, index: int): """ Display the settings page corresponding to sidebar selection. Args: index (int): Index of the selected sidebar item. Returns: None """ self.settingsStack.setCurrentIndex(index) def setValues(self): """ Set initial values in all UI components from loaded settings. Returns: None """ # Export page values self.extCombo.addItems((".xlsx", ".csv")) self.extCombo.setCurrentIndex(self.extCombo.findText(self.defaultExtension)) self._populateCombobox(self.delimiterCombo, ("Comma ','", "Semicolon ';'", "Pipe '|'", "Tabulation '\\t'")) # Appearance page values self.themeCombo.addItems(self.themes) self.themeCombo.setCurrentIndex(self.themeCombo.findText(self.currentTheme)) self._populateLangCombo() # Graphing page values self.extGraphCombo.addItems((".png", ".pdf")) self.extGraphCombo.setCurrentIndex(self.extGraphCombo.findText(self.defaultGraphExtension)) self.styleGraphCombo.addItems(self.graphStyles) self.styleGraphCombo.setCurrentIndex(self.styleGraphCombo.findText(self.currentStyle)) self.delimiterCombo.setCurrentIndex( self.delimiterCombo.findData(self.csvDelimiterConvertor(self.csvDelimiter)) ) self.enabledCB.setChecked(self.enabledGraphing) self.styleOverwriteCB.setChecked(self.allowGraphStyleOverwrite) # Export page continued self.fileNameInput.setText(self.outputFilename) self.exportPathInput.setText(os.path.expanduser(os.path.join(getBasePath(), self.outputPath))) self.sourceDirInput.setText(os.path.join(getBasePath(), os.path.expanduser(self.sourceSearchDir))) self.profileDirInput.setText(os.path.expanduser(os.path.join(getBasePath(), self.profileSearchDir))) self.outputDirInput.setText(os.path.expanduser(os.path.join(getBasePath(), self.exportSearchDir))) self.createCalcLogCheck.setChecked(self.createCalcLog) self.openFileCheck.setChecked(self.openFileOnFinish) # Security page values self.lockEnabledCB.setChecked(self.locksmithEnabled) self._populateCombobox(self.freqCombo, ("Never", "Once per action", "Once per session")) self.freqCombo.setCurrentIndex( self.freqCombo.findData(self.lockFrequencyConvertor(self.locksmithFrequency)) ) self.logPasswordCB.setChecked(self.logPassword) def _populateCombobox(self, combobox: QComboBox, items: list | tuple): """ Populate a combobox with translated items. Args: combobox (QComboBox): Combobox to populate. items (list | tuple): Items to add (will be translated). Returns: None """ for item in items: combobox.addItem(self.translate(item), item) def _populateLangCombo(self): """ Populate language combobox with available languages. Returns: None """ self.langCombo.clear() # Get available languages from translator languageInfo = self.backbone.translator.getLanguageInfo() # Add each language to the combobox for info in languageInfo: if isinstance(info, dict): code = info.get('code', '') name = info.get('name', code) self.langCombo.addItem(name, code) else: # Fallback for old format self.langCombo.addItem(info, info) # Set current language based on code index = self.langCombo.findData(self.currentLanguage) if index >= 0: self.langCombo.setCurrentIndex(index) else: # Fallback to first available language self.langCombo.setCurrentIndex(0) def setSettings(self): """ Apply loaded settings to internal variables. Returns: None """ self.themes = listDir(os.path.join(getBasePath(), "resources", "themes"), True) self.graphStyles = listDir(os.path.join(getBasePath(), "resources", "graphStyles"), True) self.openFileOnFinish = self.settings["exportOptions"]["openOnFinish"] self.createCalcLog = self.settings["exportOptions"]["createCalcLog"] self.enabledGraphing = self.settings["graphOptions"]["enabled"] self.allowGraphStyleOverwrite = self.settings["graphOptions"]["allowStyleOverwrite"] self.defaultExtension = self.settings["exportOptions"]["extension"] self.defaultGraphExtension = self.settings["graphOptions"]["extension"] self.csvDelimiter = self.settings["exportOptions"]["csvDelimiter"] self.outputPath = self.settings["exportOptions"]["exportPath"] self.outputFilename = self.settings["exportOptions"]["outputFilename"] self.sourceSearchDir = self.settings["exportOptions"]["inputDataSearchFolder"] self.profileSearchDir = self.settings["exportOptions"]["profileSearchFolder"] self.exportSearchDir = self.settings["exportOptions"]["exportSearchPath"] self.currentTheme = self.settings["appearance"]["theme"] self.currentLanguage = self._setLangSettings() self.lastLang = self.currentLanguage self.currentStyle = self.settings["graphOptions"]["style"] # Locksmith settings locksmithSettings = self.settings.get("locksmith", {}) self.locksmithEnabled = locksmithSettings.get("lockFunctions", False) self.lastHash = locksmithSettings.get("hash", '') self.locksmithFrequency = locksmithSettings.get("frequency", 0) self.logPassword = locksmithSettings.get("logPassword", False) def _setLangSettings(self) -> str: """ Determine current language from settings, falling back to English if unavailable. Returns: str -> Current language code. """ # Get language info and set current language self.availableLocales = self.backbone.translator.getLanguageInfo() # Get current language from settings currentLangCode = self.settings["appearance"]["language"] # Check if the language code exists in available locales availableCodes = [lang.get('code', '') for lang in self.availableLocales] if currentLangCode in availableCodes: return currentLangCode else: return "en" def loadSettings(self): """ Load current settings from configuration manager. Returns: None """ self.settings = self.configManager.getConfig() def checkFilename(self): """ Validate filename input and update visual feedback. Returns: None """ if not self.backbone.validator.validateFilename(self.fileNameInput.text()): self.fileNameInput.setStyleSheet("border: 2px solid red; background-color: #DC758D;") else: self.fileNameInput.setStyleSheet("") def updateConfig(self, reopen: bool = False): """ Update application configuration with current dialog values. Args: reopen (bool): Whether to reopen settings dialog after language change. Returns: None """ # Collect export settings openOnFinish = self.openFileCheck.isChecked() selectedExt = self.extCombo.currentText() createCalcLog = self.createCalcLogCheck.isChecked() filename = ( self.fileNameInput.text() if self.backbone.validator.validateFilename(self.fileNameInput.text()) else self.outputFilename ) exportPath = ( self.exportPathInput.text() if self.exportPathInput.validPath else self.outputPath ) sourceSearchDir = ( self.sourceDirInput.text() if self.sourceDirInput.validPath else self.sourceSearchDir ) profileSearchDir = ( self.profileDirInput.text() if self.profileDirInput.validPath else self.profileSearchDir ) sourceOutputDir = ( self.outputDirInput.text() if self.outputDirInput.validPath else self.exportSearchDir ) csvDelimiter = self.csvDelimiterConvertor(self.delimiterCombo.currentData()) # Get selected language code selectedLangCode = self.langCombo.currentData() # Build export settings dictionary exportSetting = { "exportOptions": { "extension": selectedExt, "exportPath": exportPath, "exportSearchPath": sourceOutputDir, "inputDataSearchFolder": sourceSearchDir, "profileSearchFolder": profileSearchDir, "outputFilename": filename, "openOnFinish": openOnFinish, "createCalcLog": createCalcLog, "csvDelimiter": csvDelimiter } } # Build graphing settings dictionary enabledGraphing = self.enabledCB.isChecked() selectedGraphExt = self.extGraphCombo.currentText() style = self.styleGraphCombo.currentText() allowStyleOverwrite = self.styleOverwriteCB.isChecked() graphingSettings = { "graphOptions": { "enabled": enabledGraphing, "extension": selectedGraphExt, "style": style, "allowStyleOverwrite": allowStyleOverwrite } } # Build appearance settings dictionary theme = self.themeCombo.currentText() appearanceSettings = { "appearance": { "theme": theme, "language": selectedLangCode } } # Build locksmith settings dictionary lockFunctions = self.lockEnabledCB.isChecked() logPassword = self.logPasswordCB.isChecked() frequency = self.lockFrequencyConvertor(self.freqCombo.currentData()) password = None if self.newPass: password = self.backbone.getHash(self.newPass) elif self.lastHash: password = self.lastHash locksmithSettings = { "locksmith": { "lockFunctions": lockFunctions, "frequency": frequency, "hash": password, "logPassword": logPassword } } # Combine and update configuration update = exportSetting | graphingSettings | appearanceSettings | locksmithSettings self.configManager.updateConfig(update) # Log password change if applicable if logPassword and password != self.lastHash and self.newPass: self.backbone.logger.log( "ConfigManager", "Password has been set to: {newPass}", newPass=self.newPass ) # Handle language change if selectedLangCode != self.lastLang: self.backbone.translator.setLanguage(selectedLangCode) if reopen: self.close() self.backbone.openSettings() # Reload settings after update self.loadSettings() self.setSettings() def applySettings(self): """ Apply settings and reopen dialog if needed (for language changes). Returns: None """ self.updateConfig(True) def acceptSettings(self): """ Accept and apply settings, then close dialog. Returns: None """ self.updateConfig() self.close() def cancelSettings(self): """ Cancel settings changes and close dialog. Returns: None """ self.close() def csvDelimiterConvertor(self, delimiter: str) -> str: """ Convert between display names and actual CSV delimiter characters. Args: delimiter (str): Delimiter to convert. Returns: str -> Converted delimiter. """ mapping = { "Comma ','": ',', "Semicolon ';'": ';', "Pipe '|'": '|', "Tabulation '\\t'": '\t', ',': "Comma ','", ';': "Semicolon ';'", '|': "Pipe '|'", '\t': "Tabulation '\\t'" } return mapping.get(delimiter, delimiter) # Returns original if not found def lockFrequencyConvertor(self, frequency: str | int) -> int | str: """ Convert between display names and lock frequency codes. Args: frequency (str | int): Frequency to convert. Returns: int | str -> Converted frequency. """ mapping = { 'Never': 0, 'Once per action': 1, 'Once per session': 2, 0: 'Never', 1: 'Once per action', 2: 'Once per session' } return mapping.get(frequency, 0) # Default to 'Never' if not found