Lemontropia-Suite/ui/main_window.py

1932 lines
71 KiB
Python

"""
Lemontropia Suite - Main Application Window (Session-Focused Redesign)
PyQt6 GUI for managing game automation sessions and activities.
"""
import sys
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()