""" 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; } """)