# PySide6 Settings

A powerful and elegant settings management library for PySide6 applications that automatically generates UI forms from Pydantic models.

## Features

- 🎨 **Automatic UI Generation** - Generate beautiful Qt forms directly from Pydantic models
- 🔄 **Two-way Data Binding** - Automatic synchronization between UI widgets and data models
- 💾 **Multiple File Formats** - Support for JSON, YAML, TOML, and more
- 🎯 **Type-Safe** - Full type checking with Pydantic
- 🏗️ **Organized Groups** - Group related settings with collapsible group boxes
- 🔌 **Extensible** - Easy to add custom widgets and loaders
- ✅ **Validation** - Automatic validation with Pydantic constraints
- 📝 **Rich Widgets** - Support for various input types including file browsers, tag inputs, password fields, and more

## Installation

```bash
pip install pyside6-settings
```

Or install from source:

```bash
git clone https://github.com/yourusername/pyside6-settings.git
cd pyside6-settings
pip install -e .
```

## Quick Start

### Basic Example

```python
from pathlib import Path
from pyside6_settings import BaseSettings, Field
from PySide6.QtWidgets import QApplication, QMainWindow

class AppSettings(BaseSettings):
    # Basic fields with automatic widget generation
    username: str = Field(default="user", description="Your username")
    age: int = Field(default=25, ge=0, le=150, description="Your age")
    enabled: bool = Field(default=True, description="Enable feature")
    
    # Field with choices (creates QComboBox)
    theme: str = Field(
        default="dark",
        title="Theme",
        choices=["light", "dark", "auto"]
    )

# Create Qt application
app = QApplication([])

# Load settings from file (creates if doesn't exist)
settings = AppSettings.load("config.json")

# Create main window with settings form
window = QMainWindow()
window.setCentralWidget(settings.create_form())
window.setWindowTitle("Application Settings")
window.show()

# Changes are automatically saved to config.json
settings.username = "new_user"  # Auto-saved!

app.exec()
```

### Grouped Settings

Organize related settings into collapsible groups:

```python
from pydantic import Field
from pyside6_settings import BaseSettings, WidgetMetadata

class MySettings(BaseSettings):
    # Account Group
    username: str = Field(
        default="",
        title="Username",
        group="Account",
        description="Your account username"
    )
    
    password: str = Field(
        default="",
        title="Password",
        widget="password",
        group="Account"
    )
    
    # Appearance Group
    font_size: int = Field(
        default=12,
        ge=8,
        le=32,
        title="Font Size",
        group="Appearance"
    )
    
    theme: str = Field(
        default="dark",
        title="Color Theme",
        choices=["light", "dark", "high-contrast"],
        group="Appearance"
    )

settings = MySettings.load("config.json")
form = settings.create_form()  # Creates form with "Account" and "Appearance" groups
```

### Advanced Widget Types

```python
from pathlib import Path
from typing import List
from pydantic import Field
from pyside6_settings import BaseSettings, WidgetMetadata

class AdvancedSettings(BaseSettings):
    # File/Directory Browser
    project_path: Path = Field(
        default=Path("."),
        title="Project Directory",
        s_mode="directory"
    )
    
    config_file: Path = Field(
        default=Path("config.ini"),
        title="Config File",
        fs_mode="file"
    )
    
    # Tag/List Input
    tags: List[str] = Field(
        default_factory=list,
        title="Tags",
        widget="tags"
    )
    
    # Multi-line Text
    description: str = Field(
        default="",
        title="Description",
        widget="textarea"
    )
    
    # Password Field
    api_key: str = Field(
        default="",
        title="API Key",
        widget="password"
    )
    
    # Numeric with Constraints
    timeout: float = Field(
        default=30.0,
        ge=1.0,
        le=300.0,
        title="Timeout (seconds)"
    )

settings = AdvancedSettings.load("advanced.json")
```

## Widget Types

The library automatically selects the appropriate widget based on field type and metadata:

| Field Type | Widget | Notes |
|------------|--------|-------|
| `str` | `QLineEdit` | Default text input |
| `int` | `QSpinBox` | With min/max from constraints |
| `float` | `QDoubleSpinBox` | With min/max from constraints |
| `bool` | `QCheckBox` | Checkbox |
| `List[str]` | `TagInputWidget` | Custom tag input |
| `Path` | `PathBrowseWidget` | File/directory browser |
| `str` (with choices) | `QComboBox` | Dropdown selection |

### Custom Widget Override

Use `widget` parameter in `WidgetMetadata` to force a specific widget type:

```python
description: str = Field(
    default="",
    widget="textarea"
)
```

Available widget overrides:
- `"textarea"` - Multi-line text input
- `"password"` - Password field (masked input)
- `"tags"` - Tag input widget
- `"path"` - File/directory browser
- `"checkbox"` - Checkbox
- `"spinbox"` - Numeric spinner
- `"doublespinbox"` - Float spinner
- `"hidden"` - Field exists but no widget created

## Working with Individual Widgets

Get individual field widgets for custom layouts:

```python
settings = MySettings.load("config.json")

# Get widget without label
username_widget = settings.get_widget("username", with_label=False)

# Get widget with label (in QFormLayout)
username_field = settings.get_widget("username", with_label=True)

# Get entire group as QGroupBox
account_group = settings.get_group("Account")
```

## Programmatic Access

Settings behave like normal Python objects:

```python
settings = MySettings.load("config.json")

# Read values
print(settings.username)
print(settings.theme)

# Modify values (automatically saved)
settings.username = "new_user"
settings.theme = "light"

# Access all fields
for field_name in settings.__pydantic_fields__:
    value = getattr(settings, field_name)
    print(f"{field_name}: {value}")
```

## File Format Support

Supported configuration file formats:

- **JSON** (`.json`)
- **YAML** (`.yaml`, `.yml`)
- **TOML** (`.toml`)
- **INI** (`.ini`)

The format is automatically detected from the file extension:

```python
# Use different formats
json_settings = MySettings.load("config.json")
yaml_settings = MySettings.load("config.yaml")
toml_settings = MySettings.load("config.toml")
```

## Custom Loaders

Implement custom loaders for other formats:

```python
from pyside6_settings.loaders import BaseConfigLoader

class CustomLoader(BaseConfigLoader):
    def load(self) -> dict:
        # Your loading logic
        pass
    
    def save(self, data: dict) -> None:
        # Your saving logic
        pass

# Register the loader
from pyside6_settings import DEFAULT_LOADERS
DEFAULT_LOADERS[".custom"] = CustomLoader
```

## Validation

Leverage Pydantic's powerful validation:

```python
from pydantic import Field, field_validator

class ValidatedSettings(BaseSettings):
    email: str = Field(default="")
    port: int = Field(default=8080, ge=1, le=65535)
    
    @field_validator("email")
    @classmethod
    def validate_email(cls, v):
        if v and "@" not in v:
            raise ValueError("Invalid email address")
        return v

settings = ValidatedSettings.load("config.json")
settings.email = "invalid"  # Raises ValidationError
```

## Excluding Fields

Exclude fields from UI or serialization:

```python
internal_value: str = Field(default="secret", exclude=True)

# Or hide from UI only
hidden_field: str = Field(
    default="value",
    widget="hidden"
)
```

## Complete Example

```python
from pathlib import Path
from typing import List, Optional
from pydantic import Field
from pyside6_settings import BaseSettings, Field
from PySide6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QPushButton

class EditorSettings(BaseSettings):
    # General Settings
    window_title: str = Field(
        default="My Editor",
        title="Window Title",
        group="General"
    )
    
    auto_save: bool = Field(
        default=True,
        title="Auto Save",
        description="Automatically save files",
        group="General"
    )
    
    # Editor Settings
    font_family: str = Field(
        default="Monospace",
        title="Font Family",
        choices=["Monospace", "Arial", "Times New Roman"],
        group="Editor"
    )
    
    font_size: int = Field(
        default=12,
        ge=8,
        le=32,
        title="Font Size",
        group="Editor"
    )
    
    tab_size: int = Field(
        default=4,
        ge=1,
        le=8,
        title="Tab Size",
        group="Editor"
    )
    
    # Paths
    workspace: Path = Field(
        default=Path.home(),
        title="Workspace Directory",
        fs_mode="directory",
        group="Paths"
    )
    
    recent_files: List[str] = Field(
        default_factory=list,
        title="Recent Files",
        widget="tags",
        group="Paths"
    )

def main():
    app = QApplication([])
    
    # Load settings
    settings = EditorSettings.load("editor_config.json")
    
    # Create main window
    window = QMainWindow()
    window.setWindowTitle("Editor Settings")
    
    # Create central widget with settings form
    central = QWidget()
    layout = QVBoxLayout(central)
    
    # Add settings form
    settings_form = settings.create_form()
    layout.addWidget(settings_form)
    
    # Add close button
    close_btn = QPushButton("Close")
    close_btn.clicked.connect(window.close)
    layout.addWidget(close_btn)
    
    window.setCentralWidget(central)
    window.resize(600, 500)
    window.show()
    
    app.exec()

if __name__ == "__main__":
    main()
```

![gui](./example/ui.png)

## API Reference

### BaseSettings

#### Class Methods

- `load(config_file: str | Path) -> Self` - Load settings from file

#### Instance Methods

- `create_form(parent: QWidget | None = None) -> QWidget` - Create complete settings form
- `get_widget(field_name: str, with_label: bool = True) -> QWidget` - Get widget for specific field
- `get_group(group_name: str) -> QGroupBox` - Get group box for specific group

#### Properties

- `_config_file: Path` - Path to configuration file
- `_config_loader: BaseConfigLoader` - Loader instance
- `_bridge: _SettingsBridge` - Signal bridge for widget synchronization

### WidgetMetadata

Configuration for widget behavior and appearance:

- `title: str` - Display label for the field
- `description: str` - Tooltip text
- `group: str` - Group name (default: "General")
- `widget: str` - Force specific widget type
- `choices: List[str]` - Options for dropdown (creates QComboBox)
- `fs_mode: str` - File system mode: "file" or "directory"

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request

## Testing

Run the test suite:

```bash
pytest tests/ -v
```

With coverage:

```bash
pytest tests/ --cov=pyside6_settings --cov-report=html
```

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

## Acknowledgments

- Built with [Pydantic](https://pydantic-docs.helpmanual.io/) for data validation
- Uses [PySide6](https://doc.qt.io/qtforpython/) for Qt bindings
- Inspired by various settings management libraries
<!-- 
## Changelog

See [CHANGELOG.md](CHANGELOG.md) for version history. -->

## Support


- 🐛 [Issue Tracker](https://github.com/AstralMortem/pyside6-settings/issues)
- 💬 [Discussions](https://github.com/AstralMortem/pyside6-settings/discussions)