""" 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 from ui.settings_dialog import SettingsDialog # ============================================================================ # Screenshot Hotkey Integration # ============================================================================ try: from modules.screenshot_hotkey import ScreenshotHotkeyManager, ScreenshotHotkeyWidget SCREENSHOT_HOTKEYS_AVAILABLE = True except ImportError: SCREENSHOT_HOTKEYS_AVAILABLE = False ScreenshotHotkeyManager = None ScreenshotHotkeyWidget = None # ============================================================================ # 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) # ============================================================================ # 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) # Screenshot hotkey manager (global hotkeys for manual capture) # Note: Initialized after UI setup because it uses log_output self._screenshot_hotkeys = None # Setup UI self.setup_ui() self.apply_dark_theme() self.create_menu_bar() self.create_status_bar() # Initialize screenshot hotkeys AFTER UI is setup self._init_screenshot_hotkeys() # 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.""" # Block signals while updating to prevent triggering on_template_changed self.template_combo.blockSignals(True) self.template_combo.clear() # Load templates from database templates = self._load_templates_from_db() # Add "Select a template..." as first item self.template_combo.addItem("-- Select a template --", None) for template in templates: display_text = f"{template.activity_type.display_name} - {template.name}" self.template_combo.addItem(display_text, template) # Restore signals self.template_combo.blockSignals(False) self.log_debug("Templates", f"Loaded {len(templates)} session templates") # If we have templates, enable the combo box if len(templates) > 0: self.template_combo.setEnabled(True) else: self.template_combo.setEnabled(False) 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" ) self.log_debug("Templates", f"Raw query returned {len(projects)} rows") for i, proj in enumerate(projects): try: # Log what we're working with for debugging self.log_debug("Templates", f"Row {i}: type={type(proj)}, len={len(proj) if hasattr(proj, '__len__') else 'N/A'}") # Convert row to tuple/list if it's a sqlite3.Row if hasattr(proj, '__iter__') and not isinstance(proj, (str, bytes)): row_data = list(proj) else: row_data = [proj] self.log_debug("Templates", f"Row {i} data: {row_data}") # Safely extract values proj_id = int(row_data[0]) if len(row_data) > 0 else 0 proj_name = str(row_data[1]) if len(row_data) > 1 else "Unnamed" proj_type = str(row_data[2]) if len(row_data) > 2 else 'hunt' proj_created = row_data[3] if len(row_data) > 3 else None proj_desc = str(row_data[4]) if len(row_data) > 4 else '' self.log_debug("Templates", f"Extracted: id={proj_id}, name={proj_name}, type={proj_type}") # Convert activity type activity = ActivityType.from_string(proj_type) # Get session count session_count = 0 try: count_result = self.db.fetchone( "SELECT COUNT(*) FROM sessions WHERE project_id = ?", (proj_id,) ) if count_result: count_data = list(count_result) session_count = int(count_data[0]) if len(count_data) > 0 else 0 except Exception as e: self.log_debug("Templates", f"Count query failed: {e}") # Get last session last_session = None try: last_result = self.db.fetchone( "SELECT MAX(started_at) FROM sessions WHERE project_id = ?", (proj_id,) ) if last_result: last_data = list(last_result) if len(last_data) > 0 and last_data[0]: last_session = datetime.fromisoformat(str(last_data[0])) except Exception as e: self.log_debug("Templates", f"Last session query failed: {e}") # Parse created_at created_at = None if proj_created: try: created_at = datetime.fromisoformat(str(proj_created)) except: pass template = SessionTemplate( id=proj_id, name=proj_name, activity_type=activity, description=proj_desc, created_at=created_at, session_count=session_count, last_session=last_session ) templates.append(template) self.log_debug("Templates", f"Added template: {proj_name}") except Exception as row_error: self.log_warning("Templates", f"Skipping row {i}: {row_error}") import traceback logger.error(traceback.format_exc()) continue except Exception as e: self.log_error("Templates", f"Failed to load templates: {e}") import traceback logger.error(traceback.format_exc()) 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() # Get the new template ID new_id = result.lastrowid self.refresh_session_templates() # Select the newly created template for i in range(self.template_combo.count()): template = self.template_combo.itemData(i) if template and template.id == new_id: self.template_combo.setCurrentIndex(i) break 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: # Skip first item which is "-- Select a template --" 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_info("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: try: # Convert row to list for safe access row_data = list(row) # Safely extract values by index session_id = int(row_data[0]) if len(row_data) > 0 else 0 template_name = str(row_data[1]) if len(row_data) > 1 else "Unknown" row_type = str(row_data[2]) if len(row_data) > 2 else 'hunt' started_at_str = row_data[3] if len(row_data) > 3 else None ended_at_str = row_data[4] if len(row_data) > 4 else None status = str(row_data[5]) if len(row_data) > 5 else 'unknown' activity = ActivityType.from_string(row_type) # Parse started_at started = None if started_at_str: started = datetime.fromisoformat(str(started_at_str)) else: started = datetime.now() # Calculate duration duration = 0 if ended_at_str: ended = datetime.fromisoformat(str(ended_at_str)) duration = int((ended - started).total_seconds() / 60) elif 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=session_id, template_name=template_name, activity_type=activity, started_at=started, duration_minutes=duration, total_cost=total_cost, total_return=total_return, status=status ) sessions.append(session) except Exception as row_error: self.log_warning("Sessions", f"Skipping malformed session row: {row_error}") continue except Exception as e: self.log_error("Sessions", f"Failed to load sessions: {e}") import traceback logger.error(traceback.format_exc()) 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 comprehensive settings dialog.""" dialog = SettingsDialog(self, self.db) if dialog.exec() == QDialog.DialogCode.Accepted: # Reload settings from QSettings self._load_settings() self.log_info("Settings", "Settings updated successfully") 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", """
Version 1.0.0
A PyQt6-based GUI for Entropia Universe session tracking.
Features: