Lemontropia-Suite/ui/settings_dialog.py

744 lines
28 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Lemontropia Suite - Comprehensive Settings Dialog
Unified settings for Player, Screenshot Hotkeys, Computer Vision, and General preferences.
"""
import logging
from pathlib import Path
from typing import Optional, Dict, Any
from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QFormLayout,
QLabel, QLineEdit, QPushButton, QComboBox,
QCheckBox, QGroupBox, QTabWidget, QDialogButtonBox,
QMessageBox, QFileDialog, QWidget, QGridLayout,
QSpinBox, QDoubleSpinBox, QFrame
)
from PyQt6.QtCore import Qt, QSettings
from PyQt6.QtGui import QKeySequence
logger = logging.getLogger(__name__)
class SettingsDialog(QDialog):
"""
Comprehensive settings dialog with tabbed interface.
Tabs:
- General: Player name, log path, activity defaults
- Screenshot Hotkeys: Configure F12 and other hotkeys
- Computer Vision: OCR backend selection, GPU settings
- Advanced: Performance, logging, database options
"""
def __init__(self, parent=None, db=None):
super().__init__(parent)
self.setWindowTitle("Lemontropia Suite - Settings")
self.setMinimumSize(600, 500)
self.resize(700, 550)
self.db = db
self._settings = QSettings("Lemontropia", "Suite")
# Load current values
self._load_current_values()
self._setup_ui()
self._apply_dark_theme()
def _load_current_values(self):
"""Load current settings values."""
# General
self._player_name = self._settings.value("player/name", "", type=str)
self._log_path = self._settings.value("log/path", "", type=str)
self._auto_detect_log = self._settings.value("log/auto_detect", True, type=bool)
self._default_activity = self._settings.value("activity/default", "hunting", type=str)
# Screenshot hotkeys
self._hotkey_full = self._settings.value("hotkey/screenshot_full", "F12", type=str)
self._hotkey_region = self._settings.value("hotkey/screenshot_region", "Shift+F12", type=str)
self._hotkey_loot = self._settings.value("hotkey/screenshot_loot", "Ctrl+F12", type=str)
self._hotkey_hud = self._settings.value("hotkey/screenshot_hud", "Alt+F12", type=str)
# Computer Vision
self._cv_backend = self._settings.value("cv/backend", "auto", type=str)
self._cv_use_gpu = self._settings.value("cv/use_gpu", True, type=bool)
self._cv_confidence = self._settings.value("cv/confidence", 0.5, type=float)
# Directories
self._screenshots_dir = self._settings.value("dirs/screenshots", "", type=str)
self._icons_dir = self._settings.value("dirs/icons", "", type=str)
def _setup_ui(self):
"""Setup the dialog UI with tabs."""
layout = QVBoxLayout(self)
layout.setContentsMargins(15, 15, 15, 15)
layout.setSpacing(10)
# Title
title = QLabel("⚙️ Settings")
title.setStyleSheet("font-size: 18px; font-weight: bold; color: #4caf50;")
layout.addWidget(title)
# Tab widget
self.tabs = QTabWidget()
layout.addWidget(self.tabs)
# Create tabs
self.tabs.addTab(self._create_general_tab(), "📋 General")
self.tabs.addTab(self._create_hotkeys_tab(), "📸 Screenshot Hotkeys")
self.tabs.addTab(self._create_vision_tab(), "👁️ Computer Vision")
self.tabs.addTab(self._create_advanced_tab(), "🔧 Advanced")
# Button box
button_box = QDialogButtonBox(
QDialogButtonBox.StandardButton.Save |
QDialogButtonBox.StandardButton.Cancel |
QDialogButtonBox.StandardButton.Reset
)
button_box.accepted.connect(self._on_save)
button_box.rejected.connect(self.reject)
button_box.button(QDialogButtonBox.StandardButton.Reset).clicked.connect(self._on_reset)
layout.addWidget(button_box)
def _create_general_tab(self) -> QWidget:
"""Create General settings tab."""
tab = QWidget()
layout = QVBoxLayout(tab)
layout.setSpacing(15)
# Player Settings
player_group = QGroupBox("🎮 Player Settings")
player_form = QFormLayout(player_group)
self.player_name_edit = QLineEdit(self._player_name)
self.player_name_edit.setPlaceholderText("Your avatar name in Entropia Universe")
player_form.addRow("Avatar Name:", self.player_name_edit)
player_help = QLabel("This name is used to identify your globals and HoFs in the log.")
player_help.setStyleSheet("color: #888; font-size: 11px;")
player_help.setWordWrap(True)
player_form.addRow(player_help)
layout.addWidget(player_group)
# Log File Settings
log_group = QGroupBox("📄 Log File Settings")
log_layout = QVBoxLayout(log_group)
log_form = QFormLayout()
log_path_layout = QHBoxLayout()
self.log_path_edit = QLineEdit(self._log_path)
self.log_path_edit.setPlaceholderText(r"C:\Users\...\Documents\Entropia Universe\chat.log")
log_path_layout.addWidget(self.log_path_edit)
browse_btn = QPushButton("Browse...")
browse_btn.clicked.connect(self._browse_log_path)
log_path_layout.addWidget(browse_btn)
log_form.addRow("Chat Log Path:", log_path_layout)
self.auto_detect_check = QCheckBox("Auto-detect log path on startup")
self.auto_detect_check.setChecked(self._auto_detect_log)
log_form.addRow(self.auto_detect_check)
log_layout.addLayout(log_form)
# Quick paths
quick_paths_layout = QHBoxLayout()
quick_paths_layout.addWidget(QLabel("Quick select:"))
default_path_btn = QPushButton("Default Location")
default_path_btn.clicked.connect(self._set_default_log_path)
quick_paths_layout.addWidget(default_path_btn)
quick_paths_layout.addStretch()
log_layout.addLayout(quick_paths_layout)
layout.addWidget(log_group)
# Default Activity
activity_group = QGroupBox("🎯 Default Activity")
activity_form = QFormLayout(activity_group)
self.default_activity_combo = QComboBox()
activities = [
("hunting", "🎯 Hunting"),
("mining", "⛏️ Mining"),
("crafting", "⚒️ Crafting")
]
for value, display in activities:
self.default_activity_combo.addItem(display, value)
if value == self._default_activity:
self.default_activity_combo.setCurrentIndex(self.default_activity_combo.count() - 1)
activity_form.addRow("Default Activity:", self.default_activity_combo)
layout.addWidget(activity_group)
# Directories
dirs_group = QGroupBox("📁 Directories")
dirs_form = QFormLayout(dirs_group)
# Screenshots directory
screenshots_layout = QHBoxLayout()
self.screenshots_dir_edit = QLineEdit(self._screenshots_dir)
self.screenshots_dir_edit.setPlaceholderText("Default: data/screenshots/")
screenshots_layout.addWidget(self.screenshots_dir_edit)
screenshots_browse = QPushButton("Browse...")
screenshots_browse.clicked.connect(self._browse_screenshots_dir)
screenshots_layout.addWidget(screenshots_browse)
dirs_form.addRow("Screenshots:", screenshots_layout)
# Icons directory
icons_layout = QHBoxLayout()
self.icons_dir_edit = QLineEdit(self._icons_dir)
self.icons_dir_edit.setPlaceholderText("Default: ~/.lemontropia/extracted_icons/")
icons_layout.addWidget(self.icons_dir_edit)
icons_browse = QPushButton("Browse...")
icons_browse.clicked.connect(self._browse_icons_dir)
icons_layout.addWidget(icons_browse)
dirs_form.addRow("Extracted Icons:", icons_layout)
layout.addWidget(dirs_group)
layout.addStretch()
return tab
def _create_hotkeys_tab(self) -> QWidget:
"""Create Screenshot Hotkeys tab."""
tab = QWidget()
layout = QVBoxLayout(tab)
layout.setSpacing(15)
# Info header
info = QLabel("📸 Configure screenshot hotkeys. Hotkeys work when the app is focused.")
info.setStyleSheet("color: #888; padding: 5px;")
info.setWordWrap(True)
layout.addWidget(info)
# Status
status_group = QGroupBox("Status")
status_layout = QVBoxLayout(status_group)
try:
import keyboard
self.hotkey_status = QLabel("✅ Global hotkeys available (keyboard library installed)")
self.hotkey_status.setStyleSheet("color: #4caf50;")
except ImportError:
self.hotkey_status = QLabel(" Qt shortcuts only (install 'keyboard' library for global hotkeys)\npip install keyboard")
self.hotkey_status.setStyleSheet("color: #ff9800;")
self.hotkey_status.setWordWrap(True)
status_layout.addWidget(self.hotkey_status)
layout.addWidget(status_group)
# Hotkey configuration
hotkey_group = QGroupBox("Hotkey Configuration")
hotkey_form = QFormLayout(hotkey_group)
# Full screen
full_layout = QHBoxLayout()
self.hotkey_full_edit = QLineEdit(self._hotkey_full)
full_layout.addWidget(self.hotkey_full_edit)
full_test = QPushButton("Test")
full_test.clicked.connect(lambda: self._test_hotkey("full"))
full_layout.addWidget(full_test)
hotkey_form.addRow("Full Screen:", full_layout)
# Region
region_layout = QHBoxLayout()
self.hotkey_region_edit = QLineEdit(self._hotkey_region)
region_layout.addWidget(self.hotkey_region_edit)
region_test = QPushButton("Test")
region_test.clicked.connect(lambda: self._test_hotkey("region"))
region_layout.addWidget(region_test)
hotkey_form.addRow("Center Region (800x600):", region_layout)
# Loot
loot_layout = QHBoxLayout()
self.hotkey_loot_edit = QLineEdit(self._hotkey_loot)
loot_layout.addWidget(self.hotkey_loot_edit)
loot_test = QPushButton("Test")
loot_test.clicked.connect(lambda: self._test_hotkey("loot"))
loot_layout.addWidget(loot_test)
hotkey_form.addRow("Loot Window:", loot_layout)
# HUD
hud_layout = QHBoxLayout()
self.hotkey_hud_edit = QLineEdit(self._hotkey_hud)
hud_layout.addWidget(self.hotkey_hud_edit)
hud_test = QPushButton("Test")
hud_test.clicked.connect(lambda: self._test_hotkey("hud"))
hud_layout.addWidget(hud_test)
hotkey_form.addRow("HUD Area:", hud_layout)
layout.addWidget(hotkey_group)
# Help text
help_group = QGroupBox("Help")
help_layout = QVBoxLayout(help_group)
help_text = QLabel(
"Format examples:\n"
" F12, Ctrl+F12, Shift+F12, Alt+F12\n"
" Ctrl+Shift+S, Alt+Tab (don't use system shortcuts)\n\n"
"Note: Global hotkeys require the 'keyboard' library and may need admin privileges.\n"
"Qt shortcuts (app focused only) work without additional libraries."
)
help_text.setStyleSheet("color: #888; font-family: monospace;")
help_layout.addWidget(help_text)
layout.addWidget(help_group)
layout.addStretch()
return tab
def _create_vision_tab(self) -> QWidget:
"""Create Computer Vision tab."""
tab = QWidget()
layout = QVBoxLayout(tab)
layout.setSpacing(15)
# Info header
info = QLabel("👁️ Computer Vision settings for automatic loot detection and OCR.")
info.setStyleSheet("color: #888; padding: 5px;")
info.setWordWrap(True)
layout.addWidget(info)
# OCR Backend Selection
backend_group = QGroupBox("OCR Backend")
backend_layout = QFormLayout(backend_group)
self.cv_backend_combo = QComboBox()
backends = [
("auto", "🤖 Auto-detect (recommended)"),
("opencv", "⚡ OpenCV EAST (fastest, no extra dependencies)"),
("easyocr", "📖 EasyOCR (good accuracy, lighter than Paddle)"),
("tesseract", "🔍 Tesseract (traditional, stable)"),
("paddle", "🧠 PaddleOCR (best accuracy, requires PyTorch)")
]
for value, display in backends:
self.cv_backend_combo.addItem(display, value)
if value == self._cv_backend:
self.cv_backend_combo.setCurrentIndex(self.cv_backend_combo.count() - 1)
self.cv_backend_combo.currentIndexChanged.connect(self._on_backend_changed)
backend_layout.addRow("OCR Backend:", self.cv_backend_combo)
# Backend status
self.backend_status = QLabel()
self._update_backend_status()
backend_layout.addRow(self.backend_status)
layout.addWidget(backend_group)
# GPU Settings
gpu_group = QGroupBox("GPU Acceleration")
gpu_layout = QFormLayout(gpu_group)
self.cv_use_gpu_check = QCheckBox("Use GPU acceleration if available")
self.cv_use_gpu_check.setChecked(self._cv_use_gpu)
self.cv_use_gpu_check.setToolTip("Faster processing but requires compatible GPU")
gpu_layout.addRow(self.cv_use_gpu_check)
# GPU Info
self.gpu_info = QLabel()
self._update_gpu_info()
gpu_layout.addRow(self.gpu_info)
layout.addWidget(gpu_group)
# Detection Settings
detection_group = QGroupBox("Detection Settings")
detection_layout = QFormLayout(detection_group)
self.cv_confidence_spin = QDoubleSpinBox()
self.cv_confidence_spin.setRange(0.1, 1.0)
self.cv_confidence_spin.setSingleStep(0.05)
self.cv_confidence_spin.setValue(self._cv_confidence)
self.cv_confidence_spin.setDecimals(2)
detection_layout.addRow("Confidence Threshold:", self.cv_confidence_spin)
confidence_help = QLabel("Lower = more sensitive (may detect non-text)\nHigher = stricter (may miss some text)")
confidence_help.setStyleSheet("color: #888; font-size: 11px;")
detection_layout.addRow(confidence_help)
layout.addWidget(detection_group)
# Test buttons
test_group = QGroupBox("Test Computer Vision")
test_layout = QHBoxLayout(test_group)
test_ocr_btn = QPushButton("📝 Test OCR")
test_ocr_btn.clicked.connect(self._test_ocr)
test_layout.addWidget(test_ocr_btn)
test_icon_btn = QPushButton("🎯 Test Icon Detection")
test_icon_btn.clicked.connect(self._test_icon_detection)
test_layout.addWidget(test_icon_btn)
calibrate_btn = QPushButton("📐 Calibrate")
calibrate_btn.clicked.connect(self._calibrate_vision)
test_layout.addWidget(calibrate_btn)
layout.addWidget(test_group)
layout.addStretch()
return tab
def _create_advanced_tab(self) -> QWidget:
"""Create Advanced settings tab."""
tab = QWidget()
layout = QVBoxLayout(tab)
layout.setSpacing(15)
# Performance
perf_group = QGroupBox("Performance")
perf_layout = QFormLayout(perf_group)
self.fps_limit_spin = QSpinBox()
self.fps_limit_spin.setRange(1, 144)
self.fps_limit_spin.setValue(60)
self.fps_limit_spin.setSuffix(" FPS")
perf_layout.addRow("Target FPS:", self.fps_limit_spin)
layout.addWidget(perf_group)
# Database
db_group = QGroupBox("Database")
db_layout = QVBoxLayout(db_group)
db_info = QLabel(f"Database location:\n{self.db.db_path if self.db else 'Not connected'}")
db_info.setStyleSheet("color: #888; font-family: monospace; font-size: 11px;")
db_info.setWordWrap(True)
db_layout.addWidget(db_info)
db_buttons = QHBoxLayout()
backup_btn = QPushButton("💾 Backup Database")
backup_btn.clicked.connect(self._backup_database)
db_buttons.addWidget(backup_btn)
export_btn = QPushButton("📤 Export Data")
export_btn.clicked.connect(self._export_data)
db_buttons.addWidget(export_btn)
db_buttons.addStretch()
db_layout.addLayout(db_buttons)
layout.addWidget(db_group)
# Logging
log_group = QGroupBox("Logging")
log_layout = QFormLayout(log_group)
self.log_level_combo = QComboBox()
log_levels = ["DEBUG", "INFO", "WARNING", "ERROR"]
for level in log_levels:
self.log_level_combo.addItem(level)
self.log_level_combo.setCurrentText("INFO")
log_layout.addRow("Log Level:", self.log_level_combo)
layout.addWidget(log_group)
layout.addStretch()
return tab
def _on_backend_changed(self):
"""Handle OCR backend selection change."""
self._update_backend_status()
def _update_backend_status(self):
"""Update backend status label."""
backend = self.cv_backend_combo.currentData()
status_text = ""
if backend == "auto":
status_text = "Will try: OpenCV → EasyOCR → Tesseract → PaddleOCR"
elif backend == "opencv":
status_text = "✅ Always available - uses OpenCV DNN (EAST model)"
elif backend == "easyocr":
try:
import easyocr
status_text = "✅ EasyOCR installed and ready"
except ImportError:
status_text = "❌ EasyOCR not installed: pip install easyocr"
elif backend == "tesseract":
try:
import pytesseract
status_text = "✅ Tesseract Python module installed"
except ImportError:
status_text = "❌ pytesseract not installed: pip install pytesseract"
elif backend == "paddle":
try:
from paddleocr import PaddleOCR
status_text = "✅ PaddleOCR installed"
except ImportError:
status_text = "❌ PaddleOCR not installed: pip install paddlepaddle paddleocr"
self.backend_status.setText(status_text)
self.backend_status.setStyleSheet(
"color: #4caf50;" if status_text.startswith("") else
"color: #f44336;" if status_text.startswith("") else "color: #888;"
)
def _update_gpu_info(self):
"""Update GPU info label."""
info_parts = []
# Check CUDA
try:
import cv2
if cv2.cuda.getCudaEnabledDeviceCount() > 0:
info_parts.append("✅ OpenCV CUDA")
else:
info_parts.append("❌ OpenCV CUDA")
except:
info_parts.append("❌ OpenCV CUDA")
# Check PyTorch CUDA
try:
import torch
if torch.cuda.is_available():
info_parts.append(f"✅ PyTorch CUDA ({torch.cuda.get_device_name(0)})")
else:
info_parts.append("❌ PyTorch CUDA")
except:
info_parts.append("❌ PyTorch CUDA")
self.gpu_info.setText(" | ".join(info_parts))
def _browse_log_path(self):
"""Browse for log file."""
path, _ = QFileDialog.getOpenFileName(
self,
"Select Entropia Universe chat.log",
"",
"Log Files (*.log);;All Files (*)"
)
if path:
self.log_path_edit.setText(path)
def _set_default_log_path(self):
"""Set default log path."""
default_path = Path.home() / "Documents" / "Entropia Universe" / "chat.log"
self.log_path_edit.setText(str(default_path))
def _browse_screenshots_dir(self):
"""Browse for screenshots directory."""
dir_path = QFileDialog.getExistingDirectory(
self,
"Select Screenshots Directory",
str(Path.home()),
QFileDialog.Option.ShowDirsOnly
)
if dir_path:
self.screenshots_dir_edit.setText(dir_path)
def _browse_icons_dir(self):
"""Browse for extracted icons directory."""
dir_path = QFileDialog.getExistingDirectory(
self,
"Select Icons Directory",
str(Path.home()),
QFileDialog.Option.ShowDirsOnly
)
if dir_path:
self.icons_dir_edit.setText(dir_path)
def _test_hotkey(self, hotkey_type: str):
"""Test a screenshot hotkey."""
try:
from modules.auto_screenshot import AutoScreenshot
screenshots_dir = Path(__file__).parent.parent / "data" / "screenshots"
ss = AutoScreenshot(screenshots_dir)
filename = f"test_{hotkey_type}_{datetime.now():%Y%m%d_%H%M%S}.png"
if hotkey_type == "full":
filepath = ss.capture_full_screen(filename)
elif hotkey_type == "region":
import mss
with mss.mss() as sct:
monitor = sct.monitors[1]
x = (monitor['width'] - 800) // 2
y = (monitor['height'] - 600) // 2
filepath = ss.capture_region(x, y, 800, 600, filename)
elif hotkey_type == "loot":
import mss
with mss.mss() as sct:
monitor = sct.monitors[1]
x = monitor['width'] - 350
y = monitor['height'] // 2 - 200
filepath = ss.capture_region(x, y, 300, 400, filename)
elif hotkey_type == "hud":
import mss
with mss.mss() as sct:
monitor = sct.monitors[1]
w, h = 600, 150
x = (monitor['width'] - w) // 2
y = monitor['height'] - h - 50
filepath = ss.capture_region(x, y, w, h, filename)
else:
filepath = None
if filepath:
QMessageBox.information(self, "Screenshot Taken", f"Saved to:\n{filepath}")
else:
QMessageBox.warning(self, "Error", "Failed to capture screenshot")
except Exception as e:
QMessageBox.critical(self, "Error", f"Screenshot failed:\n{e}")
def _test_ocr(self):
"""Test OCR functionality."""
QMessageBox.information(self, "OCR Test", "OCR test will be implemented in the Vision Test dialog.")
# TODO: Open vision test dialog
def _test_icon_detection(self):
"""Test icon detection."""
QMessageBox.information(self, "Icon Detection", "Icon detection test will be implemented in the Vision Test dialog.")
# TODO: Open vision test dialog
def _calibrate_vision(self):
"""Open vision calibration."""
QMessageBox.information(self, "Calibration", "Vision calibration will be implemented in the Calibration dialog.")
# TODO: Open calibration dialog
def _backup_database(self):
"""Backup the database."""
if not self.db:
QMessageBox.warning(self, "Error", "Database not connected")
return
try:
import shutil
from datetime import datetime
backup_path = self.db.db_path.parent / f"lemontropia_backup_{datetime.now():%Y%m%d_%H%M%S}.db"
shutil.copy2(self.db.db_path, backup_path)
QMessageBox.information(self, "Backup Complete", f"Database backed up to:\n{backup_path}")
except Exception as e:
QMessageBox.critical(self, "Backup Failed", str(e))
def _export_data(self):
"""Export data to CSV/JSON."""
QMessageBox.information(self, "Export", "Export functionality coming soon!")
def _on_save(self):
"""Save all settings."""
try:
# General
self._settings.setValue("player/name", self.player_name_edit.text().strip())
self._settings.setValue("log/path", self.log_path_edit.text().strip())
self._settings.setValue("log/auto_detect", self.auto_detect_check.isChecked())
self._settings.setValue("activity/default", self.default_activity_combo.currentData())
# Directories
self._settings.setValue("dirs/screenshots", self.screenshots_dir_edit.text().strip())
self._settings.setValue("dirs/icons", self.icons_dir_edit.text().strip())
# Hotkeys
self._settings.setValue("hotkey/screenshot_full", self.hotkey_full_edit.text().strip())
self._settings.setValue("hotkey/screenshot_region", self.hotkey_region_edit.text().strip())
self._settings.setValue("hotkey/screenshot_loot", self.hotkey_loot_edit.text().strip())
self._settings.setValue("hotkey/screenshot_hud", self.hotkey_hud_edit.text().strip())
# Computer Vision
self._settings.setValue("cv/backend", self.cv_backend_combo.currentData())
self._settings.setValue("cv/use_gpu", self.cv_use_gpu_check.isChecked())
self._settings.setValue("cv/confidence", self.cv_confidence_spin.value())
# Advanced
self._settings.setValue("performance/fps_limit", self.fps_limit_spin.value())
self._settings.setValue("logging/level", self.log_level_combo.currentText())
self._settings.sync()
QMessageBox.information(self, "Settings Saved", "All settings have been saved successfully!")
self.accept()
except Exception as e:
QMessageBox.critical(self, "Error", f"Failed to save settings:\n{e}")
def _on_reset(self):
"""Reset settings to defaults."""
reply = QMessageBox.question(
self,
"Reset Settings",
"Are you sure you want to reset all settings to defaults?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
# Clear all settings
self._settings.clear()
self._settings.sync()
QMessageBox.information(self, "Settings Reset", "Settings have been reset. Please restart the application.")
self.reject()
def _apply_dark_theme(self):
"""Apply dark theme to the dialog."""
self.setStyleSheet("""
QDialog {
background-color: #1e1e1e;
color: #e0e0e0;
}
QTabWidget::pane {
background-color: #252525;
border: 1px solid #444;
border-radius: 4px;
}
QTabBar::tab {
background-color: #2d2d2d;
padding: 8px 16px;
border: 1px solid #444;
border-bottom: none;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
QTabBar::tab:selected {
background-color: #0d47a1;
}
QGroupBox {
font-weight: bold;
border: 1px solid #444;
border-radius: 6px;
margin-top: 10px;
padding-top: 10px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px;
}
QLineEdit, QComboBox, QSpinBox, QDoubleSpinBox {
background-color: #252525;
border: 1px solid #444;
border-radius: 4px;
padding: 6px;
color: #e0e0e0;
}
QPushButton {
background-color: #0d47a1;
border: 1px solid #1565c0;
border-radius: 4px;
padding: 6px 12px;
color: white;
}
QPushButton:hover {
background-color: #1565c0;
}
QLabel {
color: #e0e0e0;
}
""")