1942 lines
71 KiB
Python
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("&", "&")
|
|
.replace("<", "<")
|
|
.replace(">", ">"))
|
|
|
|
# ========================================================================
|
|
# Menu Actions
|
|
# ========================================================================
|
|
|
|
def on_show_hud(self):
|
|
"""Show the HUD overlay."""
|
|
self.hud.show()
|
|
self.log_info("HUD", "HUD overlay shown")
|
|
|
|
def on_hide_hud(self):
|
|
"""Hide the HUD overlay."""
|
|
self.hud.hide()
|
|
self.log_info("HUD", "HUD overlay hidden")
|
|
|
|
def on_settings(self):
|
|
"""Open settings dialog."""
|
|
dialog = SettingsDialog(self, 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()
|