1418 lines
50 KiB
Python
1418 lines
50 KiB
Python
"""
|
||
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
|
||
|
||
# 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."""
|
||
damage = event.data.get('damage', 0)
|
||
self.hud.on_damage_dealt(float(damage))
|
||
|
||
# Estimate cost per shot based on weapon stats
|
||
if self._selected_weapon_stats:
|
||
dpp = self._selected_weapon_stats.get('dpp', 0)
|
||
if dpp and dpp > 0:
|
||
# Cost per shot in PEC = damage / DPP
|
||
# Convert to Decimal for precision
|
||
from decimal import Decimal
|
||
damage_dec = Decimal(str(damage))
|
||
dpp_dec = Decimal(str(dpp))
|
||
cost_pec = damage_dec / dpp_dec
|
||
cost_ped = cost_pec / Decimal('100') # Convert to PED
|
||
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("&", "&")
|
||
.replace("<", "<")
|
||
.replace(">", ">"))
|
||
|
||
# ========================================================================
|
||
# Menu Actions
|
||
# ========================================================================
|
||
|
||
def on_show_hud(self):
|
||
"""Show the HUD overlay."""
|
||
self.hud.show()
|
||
self.log_info("HUD", "HUD overlay shown")
|
||
|
||
def on_hide_hud(self):
|
||
"""Hide the HUD overlay."""
|
||
self.hud.hide()
|
||
self.log_info("HUD", "HUD overlay hidden")
|
||
|
||
def on_settings(self):
|
||
"""Open settings dialog."""
|
||
dialog = SettingsDialog(self)
|
||
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()
|