""" Lemontropia Suite - Main Application Window (Session-Focused Redesign) PyQt6 GUI for managing game automation sessions and activities. """ # Fix PyTorch DLL loading on Windows - MUST be before PyQt imports import sys import platform if platform.system() == "Windows": try: import torch # Force torch to load its DLLs before PyQt _ = torch.__version__ except Exception: pass # Torch not available, will be handled later import logging from datetime import datetime from enum import Enum, auto from typing import Optional, List, Callable, Any from dataclasses import dataclass # Setup logger logger = logging.getLogger(__name__) from PyQt6.QtWidgets import ( QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QSplitter, QPushButton, QListWidget, QListWidgetItem, QTextEdit, QLabel, QStatusBar, QMenuBar, QMenu, QDialog, QLineEdit, QFormLayout, QDialogButtonBox, QMessageBox, QGroupBox, QFrame, QApplication, QTreeWidget, QTreeWidgetItem, QHeaderView, QComboBox, QGridLayout, QToolButton, QCheckBox ) from PyQt6.QtCore import Qt, pyqtSignal, QTimer, QSize, QSettings from PyQt6.QtGui import QAction, QFont, QColor, QPalette, QIcon # ============================================================================ # Data Models # ============================================================================ class SessionState(Enum): """Session state enumeration.""" IDLE = "Idle" RUNNING = "Running" PAUSED = "Paused" ERROR = "Error" STOPPING = "Stopping" class ActivityType(Enum): """Activity type enumeration.""" HUNTING = ("hunt", "🎯 Hunting", "#4caf50") MINING = ("mine", "⛏️ Mining", "#ff9800") CRAFTING = ("craft", "βš’οΈ Crafting", "#2196f3") def __init__(self, value, display_name, color): self._value_ = value self.display_name = display_name self.color = color @classmethod def from_string(cls, value: str) -> "ActivityType": for item in cls: if item.value == value.lower(): return item return cls.HUNTING @dataclass class SessionTemplate: """Session template data model (replaces Project).""" id: int name: str activity_type: ActivityType description: str = "" created_at: Optional[datetime] = None session_count: int = 0 last_session: Optional[datetime] = None @dataclass class RecentSession: """Recent session data model.""" id: int template_name: str activity_type: ActivityType started_at: datetime duration_minutes: int total_cost: float total_return: float status: str @dataclass class LogEvent: """Log event data model.""" timestamp: datetime level: str source: str message: str def __str__(self) -> str: time_str = self.timestamp.strftime("%H:%M:%S.%f")[:-3] return f"[{time_str}] [{self.level}] [{self.source}] {self.message}" # ============================================================================ # HUD Overlay # ============================================================================ from ui.hud_overlay_clean import HUDOverlay # ============================================================================ # Session History & Gallery Integration # ============================================================================ from ui.session_history import SessionHistoryDialog from ui.gallery_dialog import GalleryDialog, ScreenshotCapture # ============================================================================ # Core Integration # ============================================================================ import os import asyncio from pathlib import Path from decimal import Decimal # Add core to path for imports sys.path.insert(0, str(Path(__file__).parent.parent / "core")) from core.log_watcher import LogWatcher from core.database import DatabaseManager # ============================================================================ # Custom Dialogs # ============================================================================ class NewSessionTemplateDialog(QDialog): """Dialog for creating a new session template.""" def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("New Session Template") self.setMinimumWidth(400) self.setup_ui() def setup_ui(self): layout = QVBoxLayout(self) # Form layout for inputs form_layout = QFormLayout() self.name_input = QLineEdit() self.name_input.setPlaceholderText("Enter template name...") form_layout.addRow("Name:", self.name_input) self.activity_combo = QComboBox() for activity in ActivityType: self.activity_combo.addItem(activity.display_name, activity) form_layout.addRow("Activity Type:", self.activity_combo) self.desc_input = QLineEdit() self.desc_input.setPlaceholderText("Enter description (optional)...") form_layout.addRow("Description:", self.desc_input) layout.addLayout(form_layout) layout.addSpacing(10) # Button box button_box = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel ) button_box.accepted.connect(self.accept) button_box.rejected.connect(self.reject) layout.addWidget(button_box) def get_template_data(self) -> tuple: """Get the entered template data.""" activity = self.activity_combo.currentData() return ( self.name_input.text().strip(), activity, self.desc_input.text().strip() ) def accept(self): """Validate before accepting.""" name = self.name_input.text().strip() if not name: QMessageBox.warning(self, "Validation Error", "Template name is required.") return super().accept() class TemplateStatsDialog(QDialog): """Dialog for displaying session template statistics.""" def __init__(self, template: SessionTemplate, parent=None): super().__init__(parent) self.template = template self.setWindowTitle(f"Template Statistics - {template.name}") self.setMinimumWidth(350) self.setup_ui() def setup_ui(self): layout = QVBoxLayout(self) # Stats display stats_group = QGroupBox("Template Information") stats_layout = QFormLayout(stats_group) stats_layout.addRow("ID:", QLabel(str(self.template.id))) stats_layout.addRow("Name:", QLabel(self.template.name)) stats_layout.addRow("Activity Type:", QLabel(self.template.activity_type.display_name)) created = self.template.created_at.strftime("%Y-%m-%d %H:%M") if self.template.created_at else "N/A" stats_layout.addRow("Created:", QLabel(created)) stats_layout.addRow("Total Sessions:", QLabel(str(self.template.session_count))) last = self.template.last_session.strftime("%Y-%m-%d %H:%M") if self.template.last_session else "Never" stats_layout.addRow("Last Session:", QLabel(last)) description = self.template.description or "N/A" stats_layout.addRow("Description:", QLabel(description)) layout.addWidget(stats_group) # Close button button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Close) button_box.rejected.connect(self.reject) layout.addWidget(button_box) class SettingsDialog(QDialog): """Dialog for application settings.""" def __init__(self, parent=None, current_player_name: str = ""): super().__init__(parent) self.setWindowTitle("Settings") self.setMinimumWidth(450) self.player_name = current_player_name self.setup_ui() def setup_ui(self): layout = QVBoxLayout(self) # Player Settings Group player_group = QGroupBox("Player Settings") player_layout = QFormLayout(player_group) self.player_name_edit = QLineEdit() self.player_name_edit.setText(self.player_name) self.player_name_edit.setPlaceholderText("Your avatar name in Entropia Universe") player_layout.addRow("Avatar Name:", self.player_name_edit) help_label = QLabel("Set your avatar name to track your globals correctly.") help_label.setStyleSheet("color: #888; font-size: 11px;") player_layout.addRow(help_label) layout.addWidget(player_group) # Log Settings Group log_group = QGroupBox("Log File Settings") log_layout = QFormLayout(log_group) self.log_path_edit = QLineEdit() self.log_path_edit.setPlaceholderText("Path to chat.log") log_layout.addRow("Log Path:", self.log_path_edit) self.auto_detect_check = QCheckBox("Auto-detect log path on startup") self.auto_detect_check.setChecked(True) log_layout.addRow(self.auto_detect_check) layout.addWidget(log_group) # Default Activity Group activity_group = QGroupBox("Default Activity") activity_layout = QFormLayout(activity_group) self.default_activity_combo = QComboBox() for activity in ActivityType: self.default_activity_combo.addItem(activity.display_name, activity) activity_layout.addRow("Default:", self.default_activity_combo) layout.addWidget(activity_group) layout.addStretch() button_box = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel ) button_box.accepted.connect(self.accept) button_box.rejected.connect(self.reject) layout.addWidget(button_box) def get_player_name(self) -> str: """Get the configured player name.""" return self.player_name_edit.text().strip() def get_log_path(self) -> str: """Get the configured log path.""" return self.log_path_edit.text().strip() def get_auto_detect(self) -> bool: """Get auto-detect setting.""" return self.auto_detect_check.isChecked() def get_default_activity(self) -> str: """Get default activity type.""" activity = self.default_activity_combo.currentData() return activity.value if activity else "hunting" # ============================================================================ # Main Window # ============================================================================ class MainWindow(QMainWindow): """ Main application window for Lemontropia Suite. Session-focused UI with: - Top: Activity Type selector + Loadout selector - Middle: Session Control (start/stop/pause) with Loadout Manager button - Bottom: Recent Sessions list """ # Signals session_started = pyqtSignal(int) session_stopped = pyqtSignal() session_paused = pyqtSignal() session_resumed = pyqtSignal() def __init__(self): super().__init__() # Window configuration self.setWindowTitle("Lemontropia Suite") self.setMinimumSize(1200, 800) self.resize(1400, 900) # Initialize database self.db = DatabaseManager() if not self.db.initialize(): QMessageBox.critical(self, "Error", "Failed to initialize database!") sys.exit(1) # Initialize HUD self.hud = HUDOverlay() # Log watcher - created when session starts self.log_watcher: Optional[LogWatcher] = None self._log_watcher_task = None # Thread-safe queue for cross-thread communication from queue import Queue self._event_queue = Queue() # Timer to process queued events in main thread self._queue_timer = QTimer(self) self._queue_timer.timeout.connect(self._process_queued_events) self._queue_timer.start(100) # State self.current_template: Optional[SessionTemplate] = None self.current_activity: ActivityType = ActivityType.HUNTING self.session_state = SessionState.IDLE self.current_session_id: Optional[int] = None self._current_db_session_id: Optional[int] = None # Player settings self.player_name: str = "" self.log_path: str = "" self.auto_detect_log: bool = True # Selected gear/loadout self._selected_loadout: Optional[Any] = None self._selected_loadout_name: str = "No Loadout" # Session cost tracking self._session_costs: dict = { 'cost_per_shot': Decimal('0'), 'cost_per_hit': Decimal('0'), 'cost_per_heal': Decimal('0'), } self._session_display: dict = { 'weapon_name': 'None', 'armor_name': 'None', 'healing_name': 'None', } # Screenshot capture self._screenshot_capture = ScreenshotCapture(self.db) # Setup UI self.setup_ui() self.apply_dark_theme() self.create_menu_bar() self.create_status_bar() # Load persistent settings self._load_settings() # Load initial data self.refresh_session_templates() self.refresh_recent_sessions() # Welcome message self.log_info("Application", "Lemontropia Suite initialized") self.log_info("Database", f"Database ready: {self.db.db_path}") # ======================================================================== # UI Setup - New Session-Focused Layout # ======================================================================== def setup_ui(self): """Setup the main UI layout with session-focused design.""" # Central widget central_widget = QWidget() self.setCentralWidget(central_widget) # Main layout main_layout = QVBoxLayout(central_widget) main_layout.setContentsMargins(10, 10, 10, 10) main_layout.setSpacing(10) # Main splitter (horizontal: left panels | log panel) self.main_splitter = QSplitter(Qt.Orientation.Horizontal) main_layout.addWidget(self.main_splitter) # Left side container - Session Control Focus left_container = QWidget() left_layout = QVBoxLayout(left_container) left_layout.setContentsMargins(0, 0, 0, 0) left_layout.setSpacing(10) # === TOP: Activity Type Selector + Loadout Selector === self.activity_panel = self.create_activity_panel() left_layout.addWidget(self.activity_panel) # === MIDDLE: Session Control (with Loadout Manager button) === self.session_panel = self.create_session_panel() left_layout.addWidget(self.session_panel) # === BOTTOM: Recent Sessions List === self.recent_sessions_panel = self.create_recent_sessions_panel() left_layout.addWidget(self.recent_sessions_panel, 1) # Stretch factor # Add left container to main splitter self.main_splitter.addWidget(left_container) # Log output panel (right side) self.log_panel = self.create_log_panel() self.main_splitter.addWidget(self.log_panel) # Set main splitter proportions (35% left, 65% log) self.main_splitter.setSizes([450, 850]) def create_activity_panel(self) -> QGroupBox: """Create the activity type and loadout selection panel (TOP).""" panel = QGroupBox("Activity Setup") layout = QVBoxLayout(panel) layout.setSpacing(10) # Activity Type selector activity_layout = QHBoxLayout() activity_layout.addWidget(QLabel("🎯 Activity Type:")) self.activity_combo = QComboBox() for activity in ActivityType: self.activity_combo.addItem(activity.display_name, activity) self.activity_combo.currentIndexChanged.connect(self.on_activity_changed) activity_layout.addWidget(self.activity_combo, 1) layout.addLayout(activity_layout) # Session Template selector template_layout = QHBoxLayout() template_layout.addWidget(QLabel("πŸ“‹ Template:")) self.template_combo = QComboBox() self.template_combo.setPlaceholderText("Select a template...") self.template_combo.currentIndexChanged.connect(self.on_template_changed) template_layout.addWidget(self.template_combo, 1) # New template button self.new_template_btn = QPushButton("+") self.new_template_btn.setMaximumWidth(40) self.new_template_btn.setToolTip("Create new template") self.new_template_btn.clicked.connect(self.on_new_template) template_layout.addWidget(self.new_template_btn) layout.addLayout(template_layout) # Loadout selector (prominent) loadout_layout = QHBoxLayout() loadout_layout.addWidget(QLabel("πŸŽ›οΈ Loadout:")) self.loadout_display = QLabel("No Loadout Selected") self.loadout_display.setStyleSheet("font-weight: bold; color: #ff9800;") loadout_layout.addWidget(self.loadout_display, 1) # Prominent Loadout Manager button self.loadout_manager_btn = QPushButton("πŸ”§ Open Loadout Manager") self.loadout_manager_btn.setToolTip("Configure your gear loadout") self.loadout_manager_btn.setMinimumHeight(32) self.loadout_manager_btn.clicked.connect(self.on_loadout_manager) loadout_layout.addWidget(self.loadout_manager_btn) layout.addLayout(loadout_layout) return panel def create_session_panel(self) -> QGroupBox: """Create the session control panel (MIDDLE).""" panel = QGroupBox("Session Control") layout = QVBoxLayout(panel) layout.setSpacing(12) # Current session info info_layout = QGridLayout() self.current_activity_label = QLabel("No Activity") self.current_activity_label.setStyleSheet("font-weight: bold; color: #888;") info_layout.addWidget(QLabel("Activity:"), 0, 0) info_layout.addWidget(self.current_activity_label, 0, 1) self.current_template_label = QLabel("No Template") self.current_template_label.setStyleSheet("font-weight: bold; color: #888;") info_layout.addWidget(QLabel("Template:"), 1, 0) info_layout.addWidget(self.current_template_label, 1, 1) layout.addLayout(info_layout) # Separator line separator = QFrame() separator.setFrameShape(QFrame.Shape.HLine) separator.setStyleSheet("background-color: #444;") layout.addWidget(separator) # Session status status_layout = QHBoxLayout() status_layout.addWidget(QLabel("Status:")) self.session_status_label = QLabel("Idle") self.session_status_label.setStyleSheet(""" QLabel { font-weight: bold; color: #888; padding: 5px 15px; background-color: #2a2a2a; border-radius: 4px; border: 1px solid #444; } """) status_layout.addWidget(self.session_status_label) status_layout.addStretch() layout.addLayout(status_layout) # Control buttons - Large and prominent button_layout = QHBoxLayout() button_layout.setSpacing(10) self.start_session_btn = QPushButton("▢️ START SESSION") self.start_session_btn.setToolTip("Start a new session") self.start_session_btn.setMinimumHeight(50) self.start_session_btn.setStyleSheet(""" QPushButton { background-color: #1b5e20; border: 2px solid #2e7d32; border-radius: 6px; padding: 10px 20px; font-weight: bold; font-size: 12pt; } QPushButton:hover { background-color: #2e7d32; } QPushButton:disabled { background-color: #1a3a1a; color: #666; border-color: #333; } """) self.start_session_btn.clicked.connect(self.on_start_session) button_layout.addWidget(self.start_session_btn, 2) self.stop_session_btn = QPushButton("⏹️ STOP") self.stop_session_btn.setToolTip("Stop current session") self.stop_session_btn.setMinimumHeight(50) self.stop_session_btn.setEnabled(False) self.stop_session_btn.setStyleSheet(""" QPushButton { background-color: #b71c1c; border: 2px solid #c62828; border-radius: 6px; padding: 10px 20px; font-weight: bold; } QPushButton:hover { background-color: #c62828; } """) self.stop_session_btn.clicked.connect(self.on_stop_session) button_layout.addWidget(self.stop_session_btn, 1) self.pause_session_btn = QPushButton("⏸️ PAUSE") self.pause_session_btn.setToolTip("Pause/Resume session") self.pause_session_btn.setMinimumHeight(50) self.pause_session_btn.setEnabled(False) self.pause_session_btn.setStyleSheet(""" QPushButton { background-color: #e65100; border: 2px solid #f57c00; border-radius: 6px; padding: 10px 20px; font-weight: bold; } QPushButton:hover { background-color: #f57c00; } """) self.pause_session_btn.clicked.connect(self.on_pause_session) button_layout.addWidget(self.pause_session_btn, 1) layout.addLayout(button_layout) # Session stats summary self.session_info_label = QLabel("Ready to start - Select activity and loadout first") self.session_info_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.session_info_label.setStyleSheet("color: #666; padding: 10px;") layout.addWidget(self.session_info_label) layout.addStretch() return panel def create_recent_sessions_panel(self) -> QGroupBox: """Create the recent sessions panel (BOTTOM).""" panel = QGroupBox("Recent Sessions") layout = QVBoxLayout(panel) layout.setSpacing(8) # Recent sessions list self.recent_sessions_list = QTreeWidget() self.recent_sessions_list.setHeaderLabels([ "Activity", "Template", "Started", "Duration", "Cost", "Return", "Status" ]) self.recent_sessions_list.setAlternatingRowColors(True) self.recent_sessions_list.setSelectionMode(QTreeWidget.SelectionMode.SingleSelection) self.recent_sessions_list.setRootIsDecorated(False) self.recent_sessions_list.itemDoubleClicked.connect(self.on_session_double_clicked) # Adjust column widths header = self.recent_sessions_list.header() header.setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed) header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) header.setSectionResizeMode(2, QHeaderView.ResizeMode.Fixed) header.setSectionResizeMode(3, QHeaderView.ResizeMode.Fixed) header.setSectionResizeMode(4, QHeaderView.ResizeMode.Fixed) header.setSectionResizeMode(5, QHeaderView.ResizeMode.Fixed) header.setSectionResizeMode(6, QHeaderView.ResizeMode.Fixed) header.resizeSection(0, 100) header.resizeSection(2, 130) header.resizeSection(3, 70) header.resizeSection(4, 70) header.resizeSection(5, 70) header.resizeSection(6, 80) layout.addWidget(self.recent_sessions_list) # Button row button_layout = QHBoxLayout() self.view_history_btn = QPushButton("πŸ“Š View Full History") self.view_history_btn.setToolTip("View complete session history") self.view_history_btn.clicked.connect(self.on_view_full_history) button_layout.addWidget(self.view_history_btn) self.refresh_sessions_btn = QPushButton("πŸ”„ Refresh") self.refresh_sessions_btn.setToolTip("Refresh recent sessions") self.refresh_sessions_btn.clicked.connect(self.refresh_recent_sessions) button_layout.addWidget(self.refresh_sessions_btn) layout.addLayout(button_layout) return panel def create_log_panel(self) -> QGroupBox: """Create the log output panel.""" panel = QGroupBox("Event Log") layout = QVBoxLayout(panel) layout.setSpacing(8) # Log text edit self.log_output = QTextEdit() self.log_output.setReadOnly(True) self.log_output.setFont(QFont("Consolas", 10)) self.log_output.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap) layout.addWidget(self.log_output) # Log controls controls_layout = QHBoxLayout() self.clear_log_btn = QPushButton("πŸ—‘οΈ Clear") self.clear_log_btn.setToolTip("Clear log output") self.clear_log_btn.clicked.connect(self.log_output.clear) controls_layout.addWidget(self.clear_log_btn) controls_layout.addStretch() self.auto_scroll_check = QLabel("βœ“ Auto-scroll") self.auto_scroll_check.setStyleSheet("color: #888;") controls_layout.addWidget(self.auto_scroll_check) layout.addLayout(controls_layout) return panel def create_menu_bar(self): """Create the application menu bar.""" menubar = self.menuBar() # File menu file_menu = menubar.addMenu("&File") new_template_action = QAction("&New Session Template", self) new_template_action.setShortcut("Ctrl+N") new_template_action.triggered.connect(self.on_new_template) file_menu.addAction(new_template_action) file_menu.addSeparator() exit_action = QAction("E&xit", self) exit_action.setShortcut("Alt+F4") exit_action.triggered.connect(self.close) file_menu.addAction(exit_action) # Session menu session_menu = menubar.addMenu("&Session") start_action = QAction("&Start", self) start_action.setShortcut("F5") start_action.triggered.connect(self.on_start_session) session_menu.addAction(start_action) self.start_action = start_action stop_action = QAction("St&op", self) stop_action.setShortcut("Shift+F5") stop_action.triggered.connect(self.on_stop_session) session_menu.addAction(stop_action) self.stop_action = stop_action pause_action = QAction("&Pause", self) pause_action.setShortcut("F6") pause_action.triggered.connect(self.on_pause_session) session_menu.addAction(pause_action) self.pause_action = pause_action # Tools menu tools_menu = menubar.addMenu("&Tools") loadout_action = QAction("&Loadout Manager", self) loadout_action.setShortcut("Ctrl+L") loadout_action.triggered.connect(self.on_loadout_manager) tools_menu.addAction(loadout_action) tools_menu.addSeparator() select_gear_menu = tools_menu.addMenu("Select &Gear") select_weapon_action = QAction("&Weapon", self) select_weapon_action.setShortcut("Ctrl+W") select_weapon_action.triggered.connect(lambda: self.on_select_gear("weapon")) select_gear_menu.addAction(select_weapon_action) select_armor_action = QAction("&Armor", self) select_armor_action.setShortcut("Ctrl+Shift+A") select_armor_action.triggered.connect(lambda: self.on_select_gear("armor")) select_gear_menu.addAction(select_armor_action) select_finder_action = QAction("&Finder", self) select_finder_action.setShortcut("Ctrl+Shift+F") select_finder_action.triggered.connect(lambda: self.on_select_gear("finder")) select_gear_menu.addAction(select_finder_action) select_medical_action = QAction("&Medical Tool", self) select_medical_action.setShortcut("Ctrl+M") select_medical_action.triggered.connect(lambda: self.on_select_gear("medical_tool")) select_gear_menu.addAction(select_medical_action) tools_menu.addSeparator() # Computer Vision submenu vision_menu = tools_menu.addMenu("πŸ‘οΈ &Computer Vision") vision_settings_action = QAction("Vision &Settings", self) vision_settings_action.triggered.connect(self.on_vision_settings) vision_menu.addAction(vision_settings_action) vision_calibrate_action = QAction("&Calibrate Vision", self) vision_calibrate_action.triggered.connect(self.on_vision_calibrate) vision_menu.addAction(vision_calibrate_action) vision_test_action = QAction("&Test Vision", self) vision_test_action.triggered.connect(self.on_vision_test) vision_menu.addAction(vision_test_action) # View menu view_menu = menubar.addMenu("&View") show_hud_action = QAction("Show &HUD", self) show_hud_action.setShortcut("F9") show_hud_action.triggered.connect(self.on_show_hud) view_menu.addAction(show_hud_action) hide_hud_action = QAction("&Hide HUD", self) hide_hud_action.setShortcut("F10") hide_hud_action.triggered.connect(self.on_hide_hud) view_menu.addAction(hide_hud_action) view_menu.addSeparator() settings_action = QAction("&Settings", self) settings_action.setShortcut("Ctrl+,") settings_action.triggered.connect(self.on_settings) view_menu.addAction(settings_action) view_menu.addSeparator() # Session History session_history_action = QAction("πŸ“Š Session &History", self) session_history_action.setShortcut("Ctrl+H") session_history_action.triggered.connect(self.on_session_history) view_menu.addAction(session_history_action) # Gallery gallery_action = QAction("πŸ–ΌοΈ Screenshot &Gallery", self) gallery_action.setShortcut("Ctrl+G") gallery_action.triggered.connect(self.on_gallery) view_menu.addAction(gallery_action) # Help menu help_menu = menubar.addMenu("&Help") run_wizard_action = QAction("&Run Setup Wizard Again", self) run_wizard_action.setShortcut("Ctrl+Shift+W") run_wizard_action.triggered.connect(self.on_run_setup_wizard) help_menu.addAction(run_wizard_action) help_menu.addSeparator() about_action = QAction("&About", self) about_action.triggered.connect(self.on_about) help_menu.addAction(about_action) def create_status_bar(self): """Create the status bar.""" self.status_bar = QStatusBar() self.setStatusBar(self.status_bar) # Permanent widgets self.status_state_label = QLabel("● Idle") self.status_state_label.setStyleSheet("color: #888; padding: 0 10px;") self.status_bar.addPermanentWidget(self.status_state_label) self.status_activity_label = QLabel("No Activity") self.status_activity_label.setStyleSheet("color: #888; padding: 0 10px;") self.status_bar.addPermanentWidget(self.status_activity_label) # Message area self.status_bar.showMessage("Ready") # ======================================================================== # Theme # ======================================================================== def apply_dark_theme(self): """Apply dark theme styling.""" dark_stylesheet = """ QMainWindow { background-color: #1e1e1e; } QWidget { background-color: #1e1e1e; color: #e0e0e0; font-family: 'Segoe UI', Arial, sans-serif; font-size: 10pt; } QGroupBox { font-weight: bold; border: 1px solid #444; border-radius: 6px; margin-top: 10px; padding-top: 10px; padding: 10px; } QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px; color: #888; } QPushButton { background-color: #2d2d2d; border: 1px solid #444; border-radius: 4px; padding: 8px 16px; color: #e0e0e0; } QPushButton:hover { background-color: #3d3d3d; border-color: #555; } QPushButton:pressed { background-color: #4d4d4d; } QPushButton:disabled { background-color: #252525; color: #666; border-color: #333; } QTreeWidget { background-color: #252525; border: 1px solid #444; border-radius: 4px; outline: none; } QTreeWidget::item { padding: 6px; border-bottom: 1px solid #333; } QTreeWidget::item:selected { background-color: #0d47a1; color: white; } QTreeWidget::item:alternate { background-color: #2a2a2a; } QHeaderView::section { background-color: #2d2d2d; padding: 6px; border: none; border-right: 1px solid #444; font-weight: bold; } QTextEdit { background-color: #151515; border: 1px solid #444; border-radius: 4px; padding: 8px; color: #d0d0d0; } QLineEdit { background-color: #252525; border: 1px solid #444; border-radius: 4px; padding: 6px; color: #e0e0e0; } QLineEdit:focus { border-color: #0d47a1; } QComboBox { background-color: #252525; border: 1px solid #444; border-radius: 4px; padding: 6px; color: #e0e0e0; min-width: 120px; } QComboBox:focus { border-color: #0d47a1; } QComboBox::drop-down { border: none; padding-right: 10px; } QMenuBar { background-color: #1e1e1e; border-bottom: 1px solid #444; } QMenuBar::item { background-color: transparent; padding: 6px 12px; } QMenuBar::item:selected { background-color: #2d2d2d; } QMenu { background-color: #2d2d2d; border: 1px solid #444; padding: 4px; } QMenu::item { padding: 6px 24px; border-radius: 2px; } QMenu::item:selected { background-color: #0d47a1; } QMenu::separator { height: 1px; background-color: #444; margin: 4px 8px; } QStatusBar { background-color: #1e1e1e; border-top: 1px solid #444; } QSplitter::handle { background-color: #444; } QSplitter::handle:horizontal { width: 2px; } QSplitter::handle:vertical { height: 2px; } QDialog { background-color: #1e1e1e; } QLabel { color: #e0e0e0; } QFormLayout QLabel { color: #888; } """ self.setStyleSheet(dark_stylesheet) # ======================================================================== # Session Template Management # ======================================================================== def refresh_session_templates(self): """Refresh the session template list.""" self.template_combo.clear() # Load templates from database templates = self._load_templates_from_db() for template in templates: self.template_combo.addItem( f"{template.activity_type.display_name} - {template.name}", template ) self.log_debug("Templates", f"Loaded {len(templates)} session templates") def _load_templates_from_db(self) -> List[SessionTemplate]: """Load session templates from database.""" templates = [] try: # Query database for projects (using existing project table) projects = self.db.fetchall( "SELECT id, name, type, created_at, description FROM projects ORDER BY name" ) for proj in projects: # sqlite3.Row doesn't have .get() method proj_type = proj['type'] if 'type' in proj.keys() else 'hunt' activity = ActivityType.from_string(proj_type) # Get session count count_result = self.db.fetchone( "SELECT COUNT(*) as count FROM sessions WHERE project_id = ?", (proj['id'],) ) session_count = count_result['count'] if count_result else 0 # Get last session last_result = self.db.fetchone( "SELECT MAX(started_at) as last FROM sessions WHERE project_id = ?", (proj['id'],) ) last_session = None if last_result and last_result['last']: last_session = datetime.fromisoformat(last_result['last']) # Handle description which might not exist in old databases description = proj['description'] if 'description' in proj.keys() else '' created_at = proj['created_at'] if 'created_at' in proj.keys() else None template = SessionTemplate( id=proj['id'], name=proj['name'], activity_type=activity, description=description, created_at=datetime.fromisoformat(created_at) if created_at else None, session_count=session_count, last_session=last_session ) templates.append(template) except Exception as e: self.log_error("Templates", f"Failed to load templates: {e}") return templates def on_new_template(self): """Handle new session template creation.""" dialog = NewSessionTemplateDialog(self) if dialog.exec() == QDialog.DialogCode.Accepted: name, activity_type, description = dialog.get_template_data() try: # Save to database result = self.db.execute( """INSERT INTO projects (name, type, description, created_at) VALUES (?, ?, ?, ?)""", (name, activity_type.value, description, datetime.now().isoformat()) ) self.db.commit() self.refresh_session_templates() self.log_info("Templates", f"Created template: {name} ({activity_type.display_name})") self.status_bar.showMessage(f"Template '{name}' created", 3000) except Exception as e: QMessageBox.critical(self, "Error", f"Failed to create template: {e}") def on_template_changed(self, index: int): """Handle template selection change.""" if index >= 0: self.current_template = self.template_combo.currentData() if self.current_template: self.current_template_label.setText(self.current_template.name) self.current_template_label.setStyleSheet(f"font-weight: bold; color: {self.current_template.activity_type.color};") self.log_debug("Templates", f"Selected template: {self.current_template.name}") self._update_start_button() else: self.current_template = None self.current_template_label.setText("No Template") self.current_template_label.setStyleSheet("font-weight: bold; color: #888;") self._update_start_button() def on_activity_changed(self, index: int): """Handle activity type change.""" self.current_activity = self.activity_combo.currentData() if self.current_activity: self.current_activity_label.setText(self.current_activity.display_name) self.current_activity_label.setStyleSheet(f"font-weight: bold; color: {self.current_activity.color};") self.status_activity_label.setText(self.current_activity.display_name) self.log_debug("Activity", f"Changed to: {self.current_activity.display_name}") # ======================================================================== # Recent Sessions # ======================================================================== def refresh_recent_sessions(self): """Refresh the recent sessions list.""" self.recent_sessions_list.clear() sessions = self._load_recent_sessions_from_db() for session in sessions: item = QTreeWidgetItem([ session.activity_type.display_name, session.template_name, session.started_at.strftime("%m-%d %H:%M"), f"{session.duration_minutes}m", f"{session.total_cost:.2f}", f"{session.total_return:.2f}", session.status ]) item.setData(0, Qt.ItemDataRole.UserRole, session.id) # Color code status if session.status == "completed": item.setForeground(6, QColor("#4caf50")) elif session.status == "running": item.setForeground(6, QColor("#ff9800")) self.recent_sessions_list.addTopLevelItem(item) self.log_debug("Sessions", f"Loaded {len(sessions)} recent sessions") def _load_recent_sessions_from_db(self) -> List[RecentSession]: """Load recent sessions from database.""" sessions = [] try: rows = self.db.fetchall(""" SELECT s.id, p.name as template_name, p.type, s.started_at, s.ended_at, s.status FROM sessions s JOIN projects p ON s.project_id = p.id ORDER BY s.started_at DESC LIMIT 20 """) for row in rows: # sqlite3.Row doesn't have .get() method, use direct access row_type = row['type'] if 'type' in row.keys() else 'hunt' activity = ActivityType.from_string(row_type) # Calculate duration started = datetime.fromisoformat(row['started_at']) duration = 0 ended_at = row['ended_at'] if 'ended_at' in row.keys() else None if ended_at: ended = datetime.fromisoformat(ended_at) duration = int((ended - started).total_seconds() / 60) elif row['status'] == 'running': duration = int((datetime.now() - started).total_seconds() / 60) # Get costs (placeholder - would need actual cost tracking) total_cost = 0.0 total_return = 0.0 session = RecentSession( id=row['id'], template_name=row['template_name'], activity_type=activity, started_at=started, duration_minutes=duration, total_cost=total_cost, total_return=total_return, status=row['status'] if 'status' in row.keys() else 'unknown' ) sessions.append(session) except Exception as e: self.log_error("Sessions", f"Failed to load sessions: {e}") return sessions def on_session_double_clicked(self, item: QTreeWidgetItem, column: int): """Handle double-click on session.""" session_id = item.data(0, Qt.ItemDataRole.UserRole) self.log_info("Sessions", f"Viewing session details: {session_id}") # TODO: Open session detail dialog def on_view_full_history(self): """Open full session history view.""" self.log_info("Sessions", "Opening full session history") dialog = SessionHistoryDialog(self) dialog.exec() # ======================================================================== # Session Control # ======================================================================== def _update_start_button(self): """Update start button state based on selections.""" can_start = ( self.session_state == SessionState.IDLE and self.current_template is not None and self._selected_loadout is not None ) self.start_session_btn.setEnabled(can_start) self.start_action.setEnabled(can_start) def on_start_session(self): """Handle start session button.""" if self.session_state != SessionState.IDLE: self.log_warning("Session", "Cannot start: session already active") return if not self.current_template: QMessageBox.warning(self, "No Template", "Please select a session template first.") return if not self._selected_loadout: QMessageBox.warning(self, "No Loadout", "Please configure a loadout before starting.") return # Start the session self.start_session_with_template(self.current_template) def start_session_with_template(self, template: SessionTemplate): """Start a new session with the given template.""" if self.session_state != SessionState.IDLE: return # Update state self.set_session_state(SessionState.RUNNING) self.current_session_id = template.id # Emit signal self.session_started.emit(template.id) # Log self.log_info("Session", f"Started {template.activity_type.display_name} session: {template.name}") self.session_info_label.setText(f"Session active: {template.name}") # Start session in database try: result = self.db.execute( "INSERT INTO sessions (project_id, started_at, status) VALUES (?, ?, ?)", (template.id, datetime.now().isoformat(), 'running') ) self.db.commit() self._current_db_session_id = result.lastrowid except Exception as e: self.log_error("Session", f"Failed to record session: {e}") # Setup LogWatcher self._setup_log_watcher() # Show HUD self.hud.show() # Start HUD session session_display = getattr(self, '_session_display', {}) session_costs = getattr(self, '_session_costs', {}) self.hud.start_session( weapon=session_display.get('weapon_name', 'Unknown'), armor=session_display.get('armor_name', 'None'), fap=session_display.get('healing_name', 'None'), loadout=self._selected_loadout_name, weapon_dpp=Decimal('0'), weapon_cost_per_hour=Decimal('0'), cost_per_shot=session_costs.get('cost_per_shot', Decimal('0')), cost_per_hit=session_costs.get('cost_per_hit', Decimal('0')), cost_per_heal=session_costs.get('cost_per_heal', Decimal('0')) ) def _setup_log_watcher(self): """Setup and start the log watcher.""" use_mock = os.getenv('USE_MOCK_DATA', 'false').lower() in ('true', '1', 'yes') log_path = self.log_path or os.getenv('EU_CHAT_LOG_PATH', '') if use_mock or not log_path: # Use mock log for testing test_data_dir = Path(__file__).parent.parent / "test-data" test_data_dir.mkdir(exist_ok=True) mock_log = test_data_dir / "mock-chat.log" if not mock_log.exists(): from core.log_watcher import MockLogGenerator MockLogGenerator.create_mock_file(mock_log, lines=50) self.log_watcher = LogWatcher(str(mock_log), poll_interval=2.0, mock_mode=True) self.log_info("LogWatcher", "Using MOCK data for testing") else: self.log_watcher = LogWatcher(log_path, poll_interval=1.0, mock_mode=False) self.log_info("LogWatcher", f"Using REAL log: {log_path}") # Subscribe to events self._subscribe_to_log_events() # Start LogWatcher self._start_log_watcher() def _subscribe_to_log_events(self): """Subscribe to log watcher events.""" if not self.log_watcher: return def on_loot(event): from decimal import Decimal item_name = event.data.get('item_name', 'Unknown') value_ped = event.data.get('value_ped', Decimal('0.0')) if item_name == 'Universal Ammo': return is_shrapnel = 'shrapnel' in item_name.lower() self.hud.update_loot(value_ped, is_shrapnel=is_shrapnel) # Queue for database if self._current_db_session_id: self._event_queue.put({ 'type': 'loot', 'session_id': self._current_db_session_id, 'item_name': item_name, 'value_ped': value_ped, 'raw_line': event.raw_line }) def on_damage_dealt(event): from decimal import Decimal damage = event.data.get('damage', 0) if damage: self.hud.on_damage_dealt(Decimal(str(damage))) if self._session_costs.get('cost_per_shot'): self.hud.update_weapon_cost(self._session_costs['cost_per_shot']) def on_damage_taken(event): from decimal import Decimal damage = event.data.get('damage', 0) if damage: self.hud.on_damage_taken(Decimal(str(damage))) decay_per_damage = Decimal('0.00025') cost_ped = Decimal(str(damage)) * decay_per_damage if cost_ped > 0: self.hud.update_armor_cost(cost_ped) def on_heal(event): from decimal import Decimal heal_amount = event.data.get('heal_amount', Decimal('0')) if self._session_costs.get('cost_per_heal'): self.hud.update_healing_cost(self._session_costs['cost_per_heal']) self.hud.on_heal_event({'heal_amount': heal_amount}) def on_personal_global(event): value_ped = event.data.get('value_ped', Decimal('0.0')) player = event.data.get('player_name', 'Unknown') creature = event.data.get('creature', 'Unknown') if self.player_name and player.lower() == self.player_name.lower(): self.hud.on_personal_global(value_ped) self.log_info("Global", f"πŸŽ‰ YOUR GLOBAL: {value_ped} PED!!!") # Capture screenshot if self._current_db_session_id: screenshot_path = self._screenshot_capture.capture_global( self._current_db_session_id, float(value_ped), creature ) if screenshot_path: self.log_info("Screenshot", f"πŸ“· Global captured: {screenshot_path}") def on_hof(event): value_ped = event.data.get('value_ped', Decimal('0.0')) creature = event.data.get('creature', 'Unknown') self.hud.on_hof(value_ped) self.log_info("HoF", f"πŸ† HALL OF FAME: {value_ped} PED!") # Capture screenshot if self._current_db_session_id: screenshot_path = self._screenshot_capture.capture_hof( self._current_db_session_id, float(value_ped), creature ) if screenshot_path: self.log_info("Screenshot", f"πŸ“· HoF captured: {screenshot_path}") # Subscribe self.log_watcher.subscribe('loot', on_loot) self.log_watcher.subscribe('damage_dealt', on_damage_dealt) self.log_watcher.subscribe('damage_taken', on_damage_taken) self.log_watcher.subscribe('heal', on_heal) self.log_watcher.subscribe('personal_global', on_personal_global) self.log_watcher.subscribe('hof', on_hof) def _start_log_watcher(self): """Start LogWatcher in background thread.""" import asyncio from PyQt6.QtCore import QThread class LogWatcherThread(QThread): def __init__(self, watcher): super().__init__() self.watcher = watcher self._running = True def run(self): loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) try: loop.run_until_complete(self.watcher.start()) while self._running: loop.run_until_complete(asyncio.sleep(0.1)) except Exception as e: print(f"LogWatcher error: {e}") finally: loop.run_until_complete(self.watcher.stop()) loop.close() def stop(self): self._running = False self._log_watcher_thread = LogWatcherThread(self.log_watcher) self._log_watcher_thread.start() self.log_info("LogWatcher", "Started watching for events") def _stop_log_watcher(self): """Stop LogWatcher.""" if hasattr(self, '_log_watcher_thread') and self._log_watcher_thread: self._log_watcher_thread.stop() self._log_watcher_thread.wait(2000) self._log_watcher_thread = None self.log_info("LogWatcher", "Stopped") def _process_queued_events(self): """Process events from the queue in the main thread.""" processed = 0 while not self._event_queue.empty() and processed < 10: try: event = self._event_queue.get_nowait() if event['type'] == 'loot': self.log_info("Loot", f"{event['item_name']} ({event['value_ped']} PED)") processed += 1 except Exception as e: self.log_error("EventQueue", f"Error processing event: {e}") def on_stop_session(self): """Handle stop session button.""" if self.session_state in (SessionState.RUNNING, SessionState.PAUSED): self._stop_log_watcher() # End session in database if self._current_db_session_id: try: self.db.execute( "UPDATE sessions SET ended_at = ?, status = ? WHERE id = ?", (datetime.now().isoformat(), 'completed', self._current_db_session_id) ) self.db.commit() except Exception as e: self.log_error("Session", f"Failed to end session: {e}") self._current_db_session_id = None self.set_session_state(SessionState.IDLE) self.current_session_id = None self.session_stopped.emit() self.log_info("Session", "Session stopped") self.session_info_label.setText("Session stopped") self.hud.end_session() self.hud.hide() # Refresh recent sessions self.refresh_recent_sessions() def on_pause_session(self): """Handle pause/resume session button.""" if self.session_state == SessionState.RUNNING: self.set_session_state(SessionState.PAUSED) self.session_paused.emit() if self.hud: self.hud.session_active = False self.hud.status_label.setText("● Paused") self.hud.status_label.setStyleSheet("color: #FF9800; font-weight: bold;") self.log_info("Session", "Session paused") self.session_info_label.setText("Session paused") self.pause_session_btn.setText("▢️ RESUME") elif self.session_state == SessionState.PAUSED: self.set_session_state(SessionState.RUNNING) self.session_resumed.emit() if self.hud: self.hud.session_active = True self.hud.status_label.setText("● Live") self.hud.status_label.setStyleSheet("color: #7FFF7F; font-weight: bold;") self.log_info("Session", "Session resumed") self.session_info_label.setText("Session resumed") self.pause_session_btn.setText("⏸️ PAUSE") def set_session_state(self, state: SessionState): """Update the session state and UI.""" self.session_state = state colors = { SessionState.IDLE: "#888", SessionState.RUNNING: "#4caf50", SessionState.PAUSED: "#ff9800", SessionState.ERROR: "#f44336", SessionState.STOPPING: "#ff5722" } self.session_status_label.setText(state.value) self.session_status_label.setStyleSheet(f""" QLabel {{ font-weight: bold; color: {colors.get(state, '#888')}; padding: 5px 15px; background-color: #2a2a2a; border-radius: 4px; border: 1px solid #444; }} """) self.status_state_label.setText(f"● {state.value}") self.status_state_label.setStyleSheet(f"color: {colors.get(state, '#888')}; padding: 0 10px;") # Update buttons self._update_start_button() self.stop_session_btn.setEnabled(state in (SessionState.RUNNING, SessionState.PAUSED)) self.pause_session_btn.setEnabled(state in (SessionState.RUNNING, SessionState.PAUSED)) # Update menu actions self.stop_action.setEnabled(self.stop_session_btn.isEnabled()) self.pause_action.setEnabled(self.pause_session_btn.isEnabled()) if state == SessionState.IDLE: self.pause_session_btn.setText("⏸️ PAUSE") # ======================================================================== # Loadout Manager # ======================================================================== def on_loadout_manager(self): """Open Loadout Manager dialog.""" from ui.loadout_manager_simple import LoadoutManagerDialog dialog = LoadoutManagerDialog(self) dialog.loadout_saved.connect(self._on_loadout_selected_for_session) dialog.exec() def _on_loadout_selected_for_session(self, loadout_config): """Handle loadout selection from LoadoutManagerDialog. Args: loadout_config: LoadoutConfig object with full gear support """ from ui.loadout_manager_simple import LoadoutConfig if isinstance(loadout_config, LoadoutConfig): # New LoadoutConfig format with full gear support loadout_name = loadout_config.name # Get total costs including amplifiers, platings, and mindforce implants self._session_costs = { 'cost_per_shot': loadout_config.get_total_weapon_cost_per_shot(), 'cost_per_hit': loadout_config.get_total_armor_cost_per_hit(), 'cost_per_heal': loadout_config.get_total_healing_cost_per_heal(), } # Display includes all gear types self._session_display = { 'weapon_name': loadout_config.weapon_name, 'weapon_amp_name': loadout_config.weapon_amp_name if loadout_config.weapon_amp_id else None, 'armor_name': loadout_config.armor_name, 'plating_name': loadout_config.plating_name if loadout_config.plating_id else None, 'healing_name': loadout_config.healing_name, 'mindforce_name': loadout_config.mindforce_implant_name if loadout_config.mindforce_implant_id else None, } # Log full gear details gear_details = f"Weapon: {loadout_config.weapon_name}" if loadout_config.weapon_amp_id: gear_details += f" + {loadout_config.weapon_amp_name}" gear_details += f" | Armor: {loadout_config.armor_name}" if loadout_config.plating_id: gear_details += f" + {loadout_config.plating_name}" gear_details += f" | Healing: {loadout_config.healing_name}" if loadout_config.mindforce_implant_id: gear_details += f" + {loadout_config.mindforce_implant_name}" self.log_info("Loadout", f"Selected: {loadout_name}") self.log_info("Loadout", f"Gear: {gear_details}") self.log_info("Loadout", f"Costs - Shot: {self._session_costs['cost_per_shot']:.4f} PED, " f"Hit: {self._session_costs['cost_per_hit']:.4f} PED, " f"Heal: {self._session_costs['cost_per_heal']:.4f} PED") else: # Legacy dict format (fallback) loadout_name = loadout_config.get('name', 'No Loadout') if isinstance(loadout_config, dict) else 'No Loadout' costs = loadout_config.get('costs', {}) if isinstance(loadout_config, dict) else {} display = loadout_config.get('display', {}) if isinstance(loadout_config, dict) else {} from decimal import Decimal self._session_costs = { 'cost_per_shot': costs.get('cost_per_shot', Decimal('0')), 'cost_per_hit': costs.get('cost_per_hit', Decimal('0')), 'cost_per_heal': costs.get('cost_per_heal', Decimal('0')), } self._session_display = { 'weapon_name': display.get('weapon_name', 'None'), 'armor_name': display.get('armor_name', 'None'), 'healing_name': display.get('healing_name', 'None'), } self.log_info("Loadout", f"Selected (legacy): {loadout_name}") self._selected_loadout = loadout_config self._selected_loadout_name = loadout_name self.loadout_display.setText(loadout_name) self.loadout_display.setStyleSheet("font-weight: bold; color: #4caf50;") self._update_start_button() def on_select_gear(self, gear_type: str = "weapon"): """Open Gear Selector dialog.""" from ui.gear_selector import GearSelectorDialog dialog = GearSelectorDialog(gear_type, self) dialog.gear_selected.connect(self.on_gear_selected) dialog.exec() def on_gear_selected(self, gear_type: str, name: str, stats: dict): """Handle gear selection.""" self.log_info("Gear", f"Selected {gear_type}: {name}") # ======================================================================== # Log Handling # ======================================================================== def log_debug(self, source: str, message: str): """Log a debug message.""" self._append_log("DEBUG", source, message) def log_info(self, source: str, message: str): """Log an info message.""" self._append_log("INFO", source, message) def log_warning(self, source: str, message: str): """Log a warning message.""" self._append_log("WARNING", source, message) def log_error(self, source: str, message: str): """Log an error message.""" self._append_log("ERROR", source, message) def _append_log(self, level: str, source: str, message: str): """Append log message to output.""" timestamp = datetime.now().strftime("%H:%M:%S") color = { "DEBUG": "#888888", "INFO": "#4caf50", "WARNING": "#ff9800", "ERROR": "#f44336" }.get(level, "#ffffff") log_entry = f'[{timestamp}] [{level}] [{source}] {self.escape_html(message)}' self.log_output.append(log_entry) scrollbar = self.log_output.verticalScrollBar() scrollbar.setValue(scrollbar.maximum()) def escape_html(self, text: str) -> str: """Escape HTML special characters.""" return (text .replace("&", "&") .replace("<", "<") .replace(">", ">")) # ======================================================================== # Menu Actions # ======================================================================== def on_show_hud(self): """Show the HUD overlay.""" self.hud.show() self.log_info("HUD", "HUD overlay shown") def on_hide_hud(self): """Hide the HUD overlay.""" self.hud.hide() self.log_info("HUD", "HUD overlay hidden") def on_settings(self): """Open settings dialog.""" dialog = SettingsDialog(self, self.player_name) if dialog.exec() == QDialog.DialogCode.Accepted: self.player_name = dialog.get_player_name() self.log_path = dialog.get_log_path() self.auto_detect_log = dialog.get_auto_detect() self._save_settings() self.log_info("Settings", f"Avatar name: {self.player_name}") def on_run_setup_wizard(self): """Run the setup wizard again.""" from ui.setup_wizard import SetupWizard wizard = SetupWizard(self, first_run=False) if wizard.exec() == QDialog.DialogCode.Accepted: settings = wizard.get_settings() self.player_name = settings.get('avatar_name', '') self.log_path = settings.get('log_path', '') self.auto_detect_log = settings.get('auto_detect_log', True) self.current_activity = ActivityType.from_string(settings.get('default_activity', 'hunting')) self._save_settings() self._load_settings() # Refresh UI self.log_info("Setup", "Settings updated from wizard") def on_about(self): """Show about dialog.""" QMessageBox.about( self, "About Lemontropia Suite", """

Lemontropia Suite

Version 1.0.0

A PyQt6-based GUI for Entropia Universe session tracking.

Features:

""" ) def on_session_history(self): """Open Session History dialog.""" dialog = SessionHistoryDialog(self) dialog.session_selected.connect(self._on_history_session_selected) dialog.exec() def _on_history_session_selected(self, session_id: int): """Handle session selection from history dialog.""" self.log_info("History", f"Selected session {session_id}") # Could load this session's details or compare with current def on_gallery(self): """Open Screenshot Gallery dialog.""" dialog = GalleryDialog(self) dialog.screenshot_deleted.connect(self._on_gallery_screenshot_deleted) dialog.exec() def _on_gallery_screenshot_deleted(self, screenshot_id: int): """Handle screenshot deletion from gallery.""" self.log_info("Gallery", f"Screenshot {screenshot_id} deleted") def on_vision_settings(self): """Open Computer Vision Settings dialog.""" try: from ui.vision_settings_dialog import VisionSettingsDialog dialog = VisionSettingsDialog(self) if dialog.exec() == QDialog.DialogCode.Accepted: self.log_info("Vision", "Settings updated") except Exception as e: self.log_error("Vision", f"Failed to open settings: {e}") QMessageBox.warning(self, "Error", f"Could not open Vision Settings: {e}") def on_vision_calibrate(self): """Open Computer Vision Calibration wizard.""" try: from ui.vision_calibration_dialog import VisionCalibrationDialog dialog = VisionCalibrationDialog(self) if dialog.exec() == QDialog.DialogCode.Accepted: self.log_info("Vision", "Calibration completed") except Exception as e: self.log_error("Vision", f"Failed to open calibration: {e}") QMessageBox.warning(self, "Error", f"Could not open Vision Calibration: {e}") def on_vision_test(self): """Open Computer Vision Test dialog.""" try: from ui.vision_test_dialog import VisionTestDialog dialog = VisionTestDialog(self) dialog.exec() except Exception as e: self.log_error("Vision", f"Failed to open test dialog: {e}") QMessageBox.warning(self, "Error", f"Could not open Vision Test: {e}") # ======================================================================== # Settings Management # ======================================================================== def _load_settings(self): """Load persistent settings from QSettings.""" settings = QSettings("Lemontropia", "Suite") self.player_name = settings.value("player/name", "", type=str) self.log_path = settings.value("log/path", "", type=str) self.auto_detect_log = settings.value("log/auto_detect", True, type=bool) default_activity = settings.value("activity/default", "hunting", type=str) self.current_activity = ActivityType.from_string(default_activity) # Update UI index = self.activity_combo.findData(self.current_activity) if index >= 0: self.activity_combo.setCurrentIndex(index) if self.player_name: self.log_info("Settings", f"Loaded avatar name: {self.player_name}") def _save_settings(self): """Save persistent settings to QSettings.""" settings = QSettings("Lemontropia", "Suite") settings.setValue("player/name", self.player_name) settings.setValue("log/path", self.log_path) settings.setValue("log/auto_detect", self.auto_detect_log) settings.setValue("activity/default", self.current_activity.value) settings.sync() # ======================================================================== # Event Overrides # ======================================================================== def closeEvent(self, event): """Handle window close event.""" if self.session_state == SessionState.RUNNING: reply = QMessageBox.question( self, "Confirm Exit", "A session is currently running. Are you sure you want to exit?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No ) if reply == QMessageBox.StandardButton.Yes: self.on_stop_session() event.accept() else: event.ignore() else: event.accept() # ============================================================================ # Test Entry Point # ============================================================================ def main(): """Main entry point for testing.""" app = QApplication(sys.argv) # Set application-wide font font = QFont("Segoe UI", 10) app.setFont(font) # Create and show main window window = MainWindow() window.show() sys.exit(app.exec()) if __name__ == '__main__': main()