Lemontropia-Suite/ui/main_window.py

1424 lines
50 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
from datetime import datetime
from enum import Enum, auto
from typing import Optional, List, Callable
from dataclasses import dataclass
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 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):
super().__init__(parent)
self.setWindowTitle("Settings")
self.setMinimumWidth(400)
self.setup_ui()
def setup_ui(self):
layout = QVBoxLayout(self)
info_label = QLabel("Settings configuration would go here.")
info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(info_label)
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)
# ============================================================================
# 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
# 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
# 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)
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()
weapon_name = self._selected_weapon or "Unknown"
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)))
self.hud.start_session(
weapon=weapon_name,
loadout="Default",
weapon_dpp=weapon_dpp,
weapon_cost_per_hour=weapon_cost_per_hour
)
self.log_info("HUD", f"HUD shown - Weapon: {weapon_name} (DPP: {weapon_dpp:.2f}, Cost/h: {weapon_cost_per_hour:.2f} PED)")
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_loot(event):
"""Handle loot events."""
item_name = event.data.get('item_name', 'Unknown')
value_ped = event.data.get('value_ped', Decimal('0.0'))
quantity = event.data.get('quantity', 1)
# Skip Universal Ammo
if item_name == 'Universal Ammo':
return
# Count kills (excluding Shrapnel which is from every mob)
# Real loot items indicate a kill
if item_name != 'Shrapnel':
self.hud.update_stats({'kills_add': 1})
# 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
})
# Update HUD (thread-safe)
self.hud.on_loot_event(item_name, value_ped)
# 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."""
value_ped = event.data.get('value_ped', Decimal('0.0'))
player = event.data.get('player_name', 'Unknown')
self.hud.on_global(value_ped)
self.log_info("Global", f"{player} found {value_ped} PED!")
def on_personal_global(event):
"""Handle personal global events."""
value_ped = event.data.get('value_ped', Decimal('0.0'))
creature = event.data.get('creature', 'Unknown')
self.hud.on_global(value_ped)
self.log_info("Global", f"🎉 YOUR GLOBAL: {creature} for {value_ped} PED!!!")
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."""
skill_name = event.data.get('skill_name', 'Unknown')
gained = event.data.get('gained', 0)
self.log_info("Skill", f"{skill_name} +{gained}")
def on_damage_dealt(event):
"""Handle damage dealt - also track weapon cost and shots fired."""
damage = event.data.get('damage', 0)
self.hud.on_damage_dealt(float(damage))
# Track shots fired (1 shot per damage event)
self.hud.update_stats({'shots_add': 1})
# Track weapon decay cost per shot
# Only count as one shot fired per damage event
if self._selected_weapon_stats:
# Get decay per shot from weapon stats (in PEC)
decay = self._selected_weapon_stats.get('decay', 0)
if decay:
from decimal import Decimal
# Convert PEC to PED
cost_ped = Decimal(str(decay)) / Decimal('100')
self.hud.update_cost(cost_ped)
def on_critical_hit(event):
"""Handle critical hit - same as damage dealt."""
on_damage_dealt(event)
def on_damage_taken(event):
"""Handle damage taken."""
damage = event.data.get('damage', 0)
self.hud.on_damage_taken(float(damage))
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)
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 _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."""
if self.current_project and self.session_state == SessionState.IDLE:
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()
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()
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.
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)
dialog.exec()
def on_loadout_manager(self):
"""Open Loadout Manager dialog."""
from ui.loadout_manager import LoadoutManagerDialog
dialog = LoadoutManagerDialog(self)
dialog.exec()
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
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()