Lemontropia-Suite/ui/main_window.py

1942 lines
71 KiB
Python

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