Lemontropia-Suite/ui/main_window.py

1723 lines
63 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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

"""
Lemontropia Suite - Main Application Window
PyQt6 GUI for managing game automation projects and sessions.
"""
import sys
import logging
from datetime import datetime
from enum import Enum, auto
from typing import Optional, List, Callable
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
)
from PyQt6.QtCore import Qt, pyqtSignal, QTimer, QSize
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"
@dataclass
class Project:
"""Project data model."""
id: int
name: str
description: str = ""
created_at: Optional[datetime] = None
session_count: int = 0
last_session: Optional[datetime] = None
@dataclass
class LogEvent:
"""Log event data model."""
timestamp: datetime
level: str # DEBUG, INFO, WARNING, ERROR, CRITICAL
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
# ============================================================================
# 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.project_manager import ProjectManager
from core.database import DatabaseManager
# ============================================================================
# Custom Dialogs
# ============================================================================
class NewProjectDialog(QDialog):
"""Dialog for creating a new project."""
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("New Project")
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 project name...")
form_layout.addRow("Name:", self.name_input)
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_project_data(self) -> tuple:
"""Get the entered project data."""
return self.name_input.text().strip(), 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", "Project name is required.")
return
super().accept()
class ProjectStatsDialog(QDialog):
"""Dialog for displaying project statistics."""
def __init__(self, project: Project, parent=None):
super().__init__(parent)
self.project = project
self.setWindowTitle(f"Project Statistics - {project.name}")
self.setMinimumWidth(350)
self.setup_ui()
def setup_ui(self):
layout = QVBoxLayout(self)
# Stats display
stats_group = QGroupBox("Project Information")
stats_layout = QFormLayout(stats_group)
stats_layout.addRow("ID:", QLabel(str(self.project.id)))
stats_layout.addRow("Name:", QLabel(self.project.name))
stats_layout.addRow("Type:", QLabel(self.project.type))
stats_layout.addRow("Status:", QLabel(self.project.status))
# Description from metadata
description = self.project.metadata.get('description', 'N/A') if self.project.metadata else 'N/A'
stats_layout.addRow("Description:", QLabel(description))
created = self.project.created_at.strftime("%Y-%m-%d %H:%M") if self.project.created_at else "N/A"
stats_layout.addRow("Created:", QLabel(created))
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(400)
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)
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()
# ============================================================================
# Main Window
# ============================================================================
class MainWindow(QMainWindow):
"""
Main application window for Lemontropia Suite.
Provides project management, session control, and log viewing capabilities.
"""
# Signals
session_started = pyqtSignal(int) # project_id
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 and real project manager
self.db = DatabaseManager()
if not self.db.initialize():
QMessageBox.critical(self, "Error", "Failed to initialize database!")
sys.exit(1)
self.project_manager = ProjectManager(self.db)
# 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) # Check every 100ms
# State
self.current_project: Optional[Project] = None
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 = "" # Set via Settings dialog
# Selected gear
self._selected_weapon: Optional[str] = None
self._selected_weapon_stats: Optional[dict] = None
self._selected_armor: Optional[str] = None
self._selected_armor_stats: Optional[dict] = None
self._selected_finder: Optional[str] = None
self._selected_finder_stats: Optional[dict] = None
self._selected_medical_tool: Optional[str] = None
self._selected_medical_tool_stats: Optional[dict] = None
self._selected_loadout: Optional[Any] = None
# Session cost tracking (initialized empty, populated by loadout selection)
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',
}
# Setup UI
self.setup_ui()
self.apply_dark_theme()
self.create_menu_bar()
self.create_status_bar()
# Load initial data
self.refresh_project_list()
# Welcome message
self.log_info("Application", "Lemontropia Suite initialized")
self.log_info("Database", f"Database ready: {self.db.db_path}")
# ========================================================================
# UI Setup
# ========================================================================
def setup_ui(self):
"""Setup the main UI layout."""
# 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
left_container = QWidget()
left_layout = QVBoxLayout(left_container)
left_layout.setContentsMargins(0, 0, 0, 0)
left_layout.setSpacing(10)
# Left splitter (vertical: projects | session control)
left_splitter = QSplitter(Qt.Orientation.Vertical)
left_layout.addWidget(left_splitter)
# Project panel
self.project_panel = self.create_project_panel()
left_splitter.addWidget(self.project_panel)
# Session control panel
self.session_panel = self.create_session_panel()
left_splitter.addWidget(self.session_panel)
# Set splitter proportions
left_splitter.setSizes([400, 300])
# Add left container to main splitter
self.main_splitter.addWidget(left_container)
# Log output panel
self.log_panel = self.create_log_panel()
self.main_splitter.addWidget(self.log_panel)
# Set main splitter proportions (30% left, 70% log)
self.main_splitter.setSizes([400, 900])
def create_project_panel(self) -> QGroupBox:
"""Create the project management panel."""
panel = QGroupBox("Project Management")
layout = QVBoxLayout(panel)
layout.setSpacing(8)
# Project list
self.project_list = QTreeWidget()
self.project_list.setHeaderLabels(["ID", "Name", "Type", "Status"])
self.project_list.setAlternatingRowColors(True)
self.project_list.setSelectionMode(QTreeWidget.SelectionMode.SingleSelection)
self.project_list.setRootIsDecorated(False)
self.project_list.itemSelectionChanged.connect(self.on_project_selected)
self.project_list.itemDoubleClicked.connect(self.on_project_double_clicked)
# Adjust column widths
header = self.project_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.resizeSection(0, 50)
header.resizeSection(2, 70)
header.resizeSection(3, 80)
layout.addWidget(self.project_list)
# Button row
button_layout = QHBoxLayout()
self.new_project_btn = QPushButton(" New Project")
self.new_project_btn.setToolTip("Create a new project")
self.new_project_btn.clicked.connect(self.on_new_project)
button_layout.addWidget(self.new_project_btn)
self.view_stats_btn = QPushButton("📊 View Stats")
self.view_stats_btn.setToolTip("View selected project statistics")
self.view_stats_btn.clicked.connect(self.on_view_stats)
self.view_stats_btn.setEnabled(False)
button_layout.addWidget(self.view_stats_btn)
self.refresh_btn = QPushButton("🔄 Refresh")
self.refresh_btn.setToolTip("Refresh project list")
self.refresh_btn.clicked.connect(self.refresh_project_list)
button_layout.addWidget(self.refresh_btn)
layout.addLayout(button_layout)
return panel
def create_session_panel(self) -> QGroupBox:
"""Create the session control panel."""
panel = QGroupBox("Session Control")
layout = QVBoxLayout(panel)
layout.setSpacing(10)
# Current project display
project_info_layout = QFormLayout()
self.current_project_label = QLabel("No project selected")
self.current_project_label.setStyleSheet("font-weight: bold; color: #888;")
project_info_layout.addRow("Selected Project:", self.current_project_label)
layout.addLayout(project_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
button_layout = QHBoxLayout()
self.start_session_btn = QPushButton("▶️ Start Session")
self.start_session_btn.setToolTip("Start a new session with selected project")
self.start_session_btn.clicked.connect(self.on_start_session)
self.start_session_btn.setEnabled(False)
button_layout.addWidget(self.start_session_btn)
self.stop_session_btn = QPushButton("⏹️ Stop")
self.stop_session_btn.setToolTip("Stop current session")
self.stop_session_btn.clicked.connect(self.on_stop_session)
self.stop_session_btn.setEnabled(False)
button_layout.addWidget(self.stop_session_btn)
self.pause_session_btn = QPushButton("⏸️ Pause")
self.pause_session_btn.setToolTip("Pause/Resume current session")
self.pause_session_btn.clicked.connect(self.on_pause_session)
self.pause_session_btn.setEnabled(False)
button_layout.addWidget(self.pause_session_btn)
layout.addLayout(button_layout)
# Session info
self.session_info_label = QLabel("Ready to start")
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_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_project_action = QAction("&New Project", self)
new_project_action.setShortcut("Ctrl+N")
new_project_action.triggered.connect(self.on_new_project)
file_menu.addAction(new_project_action)
open_project_action = QAction("&Open Project", self)
open_project_action.setShortcut("Ctrl+O")
open_project_action.triggered.connect(self.on_open_project)
file_menu.addAction(open_project_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
# 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)
# Tools menu
tools_menu = menubar.addMenu("&Tools")
# Select Gear submenu
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()
loadout_action = QAction("&Loadout Manager", self)
loadout_action.setShortcut("Ctrl+L")
loadout_action.triggered.connect(self.on_loadout_manager)
tools_menu.addAction(loadout_action)
# Help menu
help_menu = menubar.addMenu("&Help")
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_project_label = QLabel("No project")
self.status_project_label.setStyleSheet("color: #888; padding: 0 10px;")
self.status_bar.addPermanentWidget(self.status_project_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;
}
QPushButton#start_button {
background-color: #1b5e20;
border-color: #2e7d32;
}
QPushButton#start_button:hover {
background-color: #2e7d32;
}
QPushButton#stop_button {
background-color: #b71c1c;
border-color: #c62828;
}
QPushButton#stop_button:hover {
background-color: #c62828;
}
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;
}
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)
# ========================================================================
# Project Management
# ========================================================================
def refresh_project_list(self):
"""Refresh the project list display."""
self.project_list.clear()
projects = self.project_manager.list_projects()
for project in projects:
item = QTreeWidgetItem([
str(project.id),
project.name,
project.type,
project.status
])
item.setData(0, Qt.ItemDataRole.UserRole, project.id)
self.project_list.addTopLevelItem(item)
self.log_debug("ProjectManager", f"Loaded {len(projects)} projects")
def on_project_selected(self):
"""Handle project selection change."""
selected = self.project_list.selectedItems()
if selected:
project_id = selected[0].data(0, Qt.ItemDataRole.UserRole)
self.current_project = self.project_manager.load_project(project_id)
if self.current_project:
self.current_project_label.setText(self.current_project.name)
self.current_project_label.setStyleSheet("font-weight: bold; color: #4caf50;")
self.view_stats_btn.setEnabled(True)
self.start_session_btn.setEnabled(self.session_state == SessionState.IDLE)
self.status_project_label.setText(f"Project: {self.current_project.name}")
self.log_debug("ProjectManager", f"Selected project: {self.current_project.name}")
else:
self.current_project = None
self.current_project_label.setText("No project selected")
self.current_project_label.setStyleSheet("font-weight: bold; color: #888;")
self.view_stats_btn.setEnabled(False)
self.start_session_btn.setEnabled(False)
self.status_project_label.setText("No project")
def on_project_double_clicked(self, item: QTreeWidgetItem, column: int):
"""Handle double-click on project."""
project_id = item.data(0, Qt.ItemDataRole.UserRole)
project = self.project_manager.load_project(project_id)
if project:
self.show_project_stats(project)
def on_new_project(self):
"""Handle new project creation."""
dialog = NewProjectDialog(self)
if dialog.exec() == QDialog.DialogCode.Accepted:
name, description = dialog.get_project_data()
metadata = {"description": description} if description else None
project = self.project_manager.create_project(name, 'hunt', metadata)
self.refresh_project_list()
self.log_info("ProjectManager", f"Created project: {project.name}")
self.status_bar.showMessage(f"Project '{name}' created", 3000)
def on_open_project(self):
"""Handle open project action."""
# For now, just focus the project list
self.project_list.setFocus()
self.status_bar.showMessage("Select a project from the list", 3000)
def on_view_stats(self):
"""Handle view stats button."""
if self.current_project:
self.show_project_stats(self.current_project)
def show_project_stats(self, project: Project):
"""Show project statistics dialog."""
dialog = ProjectStatsDialog(project, self)
dialog.exec()
# ========================================================================
# Session Control
# ========================================================================
def start_session(self, project_id: int):
"""
Start a new session with the given project.
Args:
project_id: The ID of the project to start session for
"""
from core.project_manager import ProjectData
# Get real project from database
projects = self.project_manager.list_projects()
project = None
for p in projects:
if p.id == project_id:
project = p
break
if not project:
self.log_error("Session", f"Project {project_id} not found")
return
if self.session_state != SessionState.IDLE:
self.log_warning("Session", "Cannot start: session already active")
return
# Update state
self.set_session_state(SessionState.RUNNING)
self.current_session_id = project_id
# Emit signal
self.session_started.emit(project_id)
# Log
self.log_info("Session", f"Started session for project: {project.name}")
self.session_info_label.setText(f"Session active: {project.name}")
# Start real session in database
session = self.project_manager.start_session(project_id)
self._current_db_session_id = session.id if session else None
# Setup LogWatcher
use_mock = os.getenv('USE_MOCK_DATA', 'false').lower() in ('true', '1', 'yes')
log_path = 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._setup_log_watcher_callbacks()
# Start LogWatcher in background
self._start_log_watcher()
# Show HUD and start session tracking
self.hud.show()
# Get gear names and costs from simplified loadout structure
session_display = getattr(self, '_session_display', {})
session_costs = getattr(self, '_session_costs', {})
weapon_name = session_display.get('weapon_name', self._selected_weapon or "Unknown")
armor_name = session_display.get('armor_name', "None")
healing_name = session_display.get('healing_name', "None")
loadout_name = "Loadout" if session_costs else "Default"
weapon_stats = self._selected_weapon_stats or {}
weapon_dpp = Decimal(str(weapon_stats.get('dpp', 0)))
weapon_cost_per_hour = Decimal(str(weapon_stats.get('cost_per_hour', 0)))
# Get cost data from simplified structure
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'))
self.hud.start_session(
weapon=weapon_name,
armor=armor_name,
fap=healing_name,
loadout=loadout_name,
weapon_dpp=weapon_dpp,
weapon_cost_per_hour=weapon_cost_per_hour,
cost_per_shot=cost_per_shot,
cost_per_hit=cost_per_hit,
cost_per_heal=cost_per_heal
)
# Simple cost tracking - no database required
self.log_info("CostTracker", "Cost tracking enabled with pre-calculated values")
self.log_info("HUD", f"HUD shown - Weapon: {weapon_name}, Armor: {armor_name}, Healing: {healing_name}, Loadout: {loadout_name}")
def _setup_log_watcher_callbacks(self):
"""Setup LogWatcher event callbacks."""
if not self.log_watcher:
return
from core.project_manager import LootEvent
from decimal import Decimal
def on_heal(event):
"""Handle heal events from chat.log.
Pattern: "You healed yourself X points"
Calculates healing cost based on FAP decay and updates HUD.
"""
heal_amount = event.data.get('heal_amount', Decimal('0'))
logger.debug(f"[EVENT] on_heal: heal_amount={heal_amount}")
# Calculate heal cost based on selected medical tool decay
# Get decay per heal from loadout or use default
decay_cost = Decimal('0')
if self._selected_loadout and hasattr(self._selected_loadout, 'heal_cost_pec'):
# heal_cost_pec is the decay per heal in PEC
# Convert to PED for cost calculation
decay_cost = self._selected_loadout.heal_cost_pec / Decimal('100')
elif self._selected_medical_tool_stats and 'decay' in self._selected_medical_tool_stats:
decay_pec = Decimal(str(self._selected_medical_tool_stats['decay']))
decay_cost = decay_pec / Decimal('100')
else:
# Default estimate: 2 PEC per heal
decay_cost = Decimal('0.02')
# Update HUD with heal event
try:
logger.debug(f"[DEBUG] _session_costs={self._session_costs}")
# Track healing cost
if self._session_costs:
cost_per_heal = self._session_costs.get('cost_per_heal', Decimal('0'))
logger.debug(f"[DEBUG] cost_per_heal={cost_per_heal}")
if cost_per_heal > 0:
logger.debug(f"[HUD] update_healing_cost({cost_per_heal})")
self.hud.update_healing_cost(cost_per_heal)
# Track heal amount (as event dict for new HUD)
logger.debug(f"[HUD] on_heal_event({{'heal_amount': {heal_amount}}})")
self.hud.on_heal_event({'heal_amount': heal_amount})
logger.debug(f"[HUD] Heal update successful")
except Exception as e:
logger.error(f"[ERROR] Error updating HUD heal: {e}")
import traceback
logger.error(traceback.format_exc())
# Log to UI
self.log_info("Heal", f"Healed {heal_amount} HP (Cost: {decay_cost:.4f} PED)")
def on_loot(event):
"""Handle loot events."""
from decimal import Decimal
item_name = event.data.get('item_name', 'Unknown')
value_ped = event.data.get('value_ped', Decimal('0.0'))
quantity = event.data.get('quantity', 1)
logger.debug(f"[EVENT] on_loot: item={item_name}, value={value_ped}, qty={quantity}")
# Skip Universal Ammo
if item_name == 'Universal Ammo':
logger.debug("[EVENT] on_loot: skipped Universal Ammo")
return
try:
# Update loot value - this also handles kill counting internally
is_shrapnel = 'shrapnel' in item_name.lower()
logger.debug(f"[HUD] update_loot({value_ped}, is_shrapnel={is_shrapnel})")
self.hud.update_loot(value_ped, is_shrapnel=is_shrapnel)
logger.debug(f"[HUD] Loot update successful")
except Exception as e:
logger.error(f"[ERROR] Error updating HUD loot: {e}")
import traceback
logger.error(traceback.format_exc())
# Queue database write for main thread (SQLite thread safety)
if self._current_db_session_id:
self._event_queue.put({
'type': 'loot',
'session_id': self._current_db_session_id,
'item_name': item_name,
'quantity': quantity,
'value_ped': value_ped,
'raw_line': event.raw_line
})
# Log to UI (main thread only - use signal/slot or queue)
# We'll log this in _process_queued_events instead
def on_global(event):
"""Handle global events (other players)."""
value_ped = event.data.get('value_ped', Decimal('0.0'))
player = event.data.get('player_name', 'Unknown')
# Don't count other players' globals in HUD
self.log_info("Global", f"{player} found {value_ped} PED!")
def on_personal_global(event):
"""Handle personal global events (only your globals)."""
value_ped = event.data.get('value_ped', Decimal('0.0'))
creature = event.data.get('creature', 'Unknown')
player = event.data.get('player_name', 'Unknown')
# Only count if it matches our configured player name
if self.player_name and player.lower() == self.player_name.lower():
# Only count personal globals in HUD stats
self.hud.on_personal_global(value_ped)
self.log_info("Global", f"🎉 YOUR GLOBAL: {creature} for {value_ped} PED!!!")
else:
# Log but don't count - might be another player if names are similar
self.log_info("Global", f"{player} got global: {creature} for {value_ped} PED")
# If no player name set, warn user
if not self.player_name:
self.log_info("Config", "⚠️ Set your avatar name in Settings to track your globals correctly")
def on_hof(event):
"""Handle HoF events."""
value_ped = event.data.get('value_ped', Decimal('0.0'))
self.hud.on_hof(value_ped)
self.log_info("HoF", f"🏆 HALL OF FAME: {value_ped} PED!")
def on_skill(event):
"""Handle skill events."""
from decimal import Decimal
skill_name = event.data.get('skill_name', 'Unknown')
gained = event.data.get('gained', 0)
self.log_info("Skill", f"{skill_name} +{gained}")
# Update HUD skill tracking
try:
gained_decimal = Decimal(str(gained))
self.hud.update_skill(skill_name, gained_decimal)
except Exception as e:
logger.error(f"[ERROR] Error updating HUD skill: {e}")
def on_damage_dealt(event):
"""Handle damage dealt - track damage stats and weapon cost."""
from decimal import Decimal
try:
damage = event.data.get('damage', 0)
logger.debug(f"[EVENT] on_damage_dealt: damage={damage}")
if damage:
# Track damage amount
logger.debug(f"[HUD] on_damage_dealt({damage})")
self.hud.on_damage_dealt(Decimal(str(damage)))
# Track weapon cost per shot
logger.debug(f"[DEBUG] _session_costs={self._session_costs}")
if self._session_costs:
cost_per_shot = self._session_costs.get('cost_per_shot', Decimal('0'))
logger.debug(f"[DEBUG] cost_per_shot={cost_per_shot}")
if cost_per_shot > 0:
logger.debug(f"[HUD] update_weapon_cost({cost_per_shot})")
self.hud.update_weapon_cost(cost_per_shot)
except Exception as e:
logger.error(f"[ERROR] Error in on_damage_dealt: {e}")
import traceback
logger.error(traceback.format_exc())
def on_critical_hit(event):
"""Handle critical hit - same as damage dealt."""
try:
on_damage_dealt(event)
except Exception as e:
logger.error(f"Error in on_critical_hit: {e}")
def on_damage_taken(event):
"""Handle damage taken - track damage stats and armor cost."""
from decimal import Decimal
try:
damage = event.data.get('damage', 0)
logger.debug(f"[EVENT] on_damage_taken: damage={damage}")
if damage:
# Track damage amount
logger.debug(f"[HUD] on_damage_taken({damage})")
self.hud.on_damage_taken(Decimal(str(damage)))
# Track armor cost per hit
logger.debug(f"[DEBUG] _session_costs={self._session_costs}")
if self._session_costs:
cost_per_hit = self._session_costs.get('cost_per_hit', Decimal('0'))
logger.debug(f"[DEBUG] cost_per_hit={cost_per_hit}")
if cost_per_hit > 0:
logger.debug(f"[HUD] update_armor_cost({cost_per_hit})")
self.hud.update_armor_cost(cost_per_hit)
except Exception as e:
logger.error(f"[ERROR] Error in on_damage_taken: {e}")
import traceback
logger.error(traceback.format_exc())
def on_evade(event):
"""Handle evade."""
evade_type = event.data.get('type', 'Evade')
self.log_info("Evade", evade_type)
# Subscribe to all event types
self.log_watcher.subscribe('loot', on_loot)
self.log_watcher.subscribe('global', on_global)
self.log_watcher.subscribe('personal_global', on_personal_global)
self.log_watcher.subscribe('hof', on_hof)
self.log_watcher.subscribe('skill', on_skill)
self.log_watcher.subscribe('damage_dealt', on_damage_dealt)
self.log_watcher.subscribe('critical_hit', on_critical_hit)
self.log_watcher.subscribe('damage_taken', on_damage_taken)
self.log_watcher.subscribe('evade', on_evade)
self.log_watcher.subscribe('heal', on_heal) # NEW: Heal event tracking
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) # Wait up to 2 seconds
self._log_watcher_thread = None
self.log_info("LogWatcher", "Stopped")
def _setup_session_cost_tracker(self, loadout_info: dict):
"""Set up session cost tracker with selected loadout."""
from core.session_cost_tracker import SessionCostTracker
from core.database import DatabaseManager
loadout_id = loadout_info.get('id')
loadout_source = loadout_info.get('source')
# If JSON-based loadout, we need to save it to database first or handle differently
if loadout_source == 'json':
# For now, skip cost tracking for JSON loadouts
# TODO: Save JSON loadout to database first, then use its ID
self.log_warning("CostTracker", "Cost tracking not available for file-based loadouts. Save to database first.")
return
try:
db = DatabaseManager()
self._cost_tracker = SessionCostTracker(
session_id=self._current_db_session_id,
loadout_id=loadout_id,
db_manager=db
)
self._cost_tracker.register_callback(self._on_cost_update)
self.log_info("CostTracker", f"Cost tracking enabled for loadout ID: {loadout_id}")
except Exception as e:
self.log_error("CostTracker", f"Failed to set up cost tracker: {e}")
def _on_cost_update(self, state):
"""Handle cost update from SessionCostTracker."""
# Update HUD with new cost state
if hasattr(self, 'hud') and self.hud:
summary = {
'weapon_cost': state.weapon_cost,
'armor_cost': state.armor_cost,
'healing_cost': state.healing_cost,
'enhancer_cost': state.enhancer_cost,
'mindforce_cost': state.mindforce_cost,
'shots_fired': state.shots_fired,
'hits_taken': state.hits_taken,
'heals_used': state.heals_used,
}
self.hud._stats.update_from_cost_tracker(summary)
self.hud._refresh_display()
def _process_queued_events(self):
"""Process events from the queue in the main thread (SQLite thread safety)."""
from core.project_manager import LootEvent
from decimal import Decimal
processed = 0
while not self._event_queue.empty() and processed < 10: # Process max 10 per tick
try:
event = self._event_queue.get_nowait()
if event['type'] == 'loot':
# Record to database (now in main thread - safe)
loot = LootEvent(
item_name=event['item_name'],
quantity=event['quantity'],
value_ped=event['value_ped'],
event_type='regular',
raw_log_line=event['raw_line']
)
self.project_manager.record_loot(loot)
# Log to UI
self.log_info("Loot", f"{event['item_name']} x{event['quantity']} ({event['value_ped']} PED)")
processed += 1
except Exception as e:
self.log_error("EventQueue", f"Error processing event: {e}")
def on_start_session(self):
"""Handle start session button - shows loadout selection first."""
if self.current_project and self.session_state == SessionState.IDLE:
# Show loadout selection dialog
from ui.loadout_selection_dialog_simple import LoadoutSelectionDialog
dialog = LoadoutSelectionDialog(self)
dialog.loadout_selected.connect(self._on_loadout_selected_for_session)
dialog.rejected.connect(lambda: self.log_info("Session", "Session start cancelled - no loadout selected"))
dialog.exec()
def _on_loadout_selected_for_session(self, loadout_info: dict):
"""Handle loadout selection and start session - simplified cost-focused version."""
loadout_name = loadout_info.get('name', 'No Loadout')
costs = loadout_info.get('costs', {})
display = loadout_info.get('display', {})
# Store cost data for session tracking
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')),
}
# Store display data for HUD
self._session_display = {
'weapon_name': display.get('weapon_name', 'None'),
'armor_name': display.get('armor_name', 'None'),
'healing_name': display.get('healing_name', 'None'),
}
if any(self._session_costs.values()):
self.log_info("Session", f"Starting with loadout: {loadout_name}")
self.log_info("SessionCosts",
f"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:
self.log_info("Session", f"Starting with loadout: {loadout_name} (no costs configured)")
# Now start the session
if self.current_project:
self.start_session(self.current_project.id)
def on_stop_session(self):
"""Handle stop session button."""
if self.session_state in (SessionState.RUNNING, SessionState.PAUSED):
# Stop LogWatcher
self._stop_log_watcher()
# End session in database
if self._current_db_session_id:
self.project_manager.end_session(self._current_db_session_id)
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")
# End HUD session
self.hud.end_session()
# Hide HUD
self.hud.hide()
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()
# Pause HUD tracking
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 - tracking stopped")
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()
# Resume HUD tracking
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 - tracking started")
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.
Args:
state: New session state
"""
self.session_state = state
# Update status label
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;
}}
""")
# Update status bar
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.start_session_btn.setEnabled(
state == SessionState.IDLE and self.current_project is not None
)
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.start_action.setEnabled(self.start_session_btn.isEnabled())
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")
# ========================================================================
# Log Handling
# ========================================================================
def on_log_event(self, event: LogEvent):
"""
Handle incoming log events.
Args:
event: The log event to display
"""
# Color mapping
colors = {
"DEBUG": "#888",
"INFO": "#4fc3f7",
"WARNING": "#ff9800",
"ERROR": "#f44336",
"CRITICAL": "#e91e63"
}
color = colors.get(event.level, "#e0e0e0")
html = f'<span style="color: {color}">{self.escape_html(str(event))}</span>'
self.log_output.append(html)
# Auto-scroll to bottom
scrollbar = self.log_output.verticalScrollBar()
scrollbar.setValue(scrollbar.maximum())
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'<span style="color: #666;">[{timestamp}]</span> <span style="color: {color};">[{level}]</span> <span style="color: #aaa;">[{source}]</span> {self.escape_html(message)}'
self.log_output.append(log_entry)
# Auto-scroll to bottom
scrollbar = self.log_output.verticalScrollBar()
scrollbar.setValue(scrollbar.maximum())
def escape_html(self, text: str) -> str:
"""Escape HTML special characters."""
return (text
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;"))
# ========================================================================
# 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()
if self.player_name:
self.log_info("Settings", f"Avatar name set to: {self.player_name}")
else:
self.log_info("Settings", "Warning: No avatar name set. Global tracking may not work correctly.")
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)
dialog.exec()
def on_loadout_selected(self, loadout):
"""Handle loadout selection from Loadout Manager - simplified version."""
self._selected_loadout = loadout
self.log_info("Loadout", f"Selected loadout: {loadout.name}")
# Update selected gear from loadout (simplified structure)
if hasattr(loadout, 'weapon_name'):
self._selected_weapon = loadout.weapon_name
if hasattr(loadout, 'healing_name'):
self._selected_medical_tool = loadout.healing_name
self._selected_medical_tool_stats = {
'decay': float(loadout.healing_decay_pec),
'cost_per_heal': float(loadout.healing_cost_per_heal),
}
# Store simplified costs for session
if hasattr(loadout, 'weapon_cost_per_shot'):
self._session_costs = {
'cost_per_shot': loadout.weapon_cost_per_shot,
'cost_per_hit': loadout.armor_cost_per_hit,
'cost_per_heal': loadout.healing_cost_per_heal,
}
if hasattr(loadout, 'weapon_name'):
self._session_display = {
'weapon_name': loadout.weapon_name,
'armor_name': loadout.armor_name,
'healing_name': loadout.healing_name,
}
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}")
if gear_type == "weapon":
self._selected_weapon = name
self._selected_weapon_stats = stats
if self.session_state == SessionState.RUNNING:
self.hud.update_stats({'weapon': name})
elif gear_type == "armor":
self._selected_armor = name
self._selected_armor_stats = stats
elif gear_type == "finder":
self._selected_finder = name
self._selected_finder_stats = stats
elif gear_type == "medical_tool":
self._selected_medical_tool = name
self._selected_medical_tool_stats = stats
if self.session_state == SessionState.RUNNING:
self.hud.update_stats({'medical_tool': name})
def on_about(self):
"""Show about dialog."""
QMessageBox.about(
self,
"About Lemontropia Suite",
"""<h2>Lemontropia Suite</h2>
<p>Version 1.0.0</p>
<p>A PyQt6-based GUI for game automation and session management.</p>
<p>Features:</p>
<ul>
<li>Project management</li>
<li>Session control</li>
<li>Real-time logging</li>
<li>HUD overlay</li>
</ul>
"""
)
# ========================================================================
# 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()
# Simulate some log activity for demonstration
def simulate_logs():
import random
sources = ["Engine", "Input", "Vision", "Network", "Session"]
levels = ["DEBUG", "INFO", "INFO", "INFO", "WARNING"]
messages = [
"Initializing component...",
"Connection established",
"Processing frame #1234",
"Waiting for input",
"Buffer cleared",
"Sync complete"
]
if window.session_state == SessionState.RUNNING:
if random.random() < 0.3: # 30% chance each tick
event = LogEvent(
timestamp=datetime.now(),
level=random.choice(levels),
source=random.choice(sources),
message=random.choice(messages)
)
window.log_watcher.emit(event)
# Timer to simulate log activity
timer = QTimer()
timer.timeout.connect(simulate_logs)
timer.start(1000) # Every second
sys.exit(app.exec())
if __name__ == '__main__':
main()