diff --git a/ui/main_window.py b/ui/main_window.py
index 6f6d845..bc0d602 100644
--- a/ui/main_window.py
+++ b/ui/main_window.py
@@ -88,30 +88,30 @@ from core.database import DatabaseManager
class NewProjectDialog(QDialog):
"""Dialog for creating a new project."""
-
+
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("New Project")
self.setMinimumWidth(400)
self.setup_ui()
-
+
def setup_ui(self):
layout = QVBoxLayout(self)
-
+
# Form layout for inputs
form_layout = QFormLayout()
-
+
self.name_input = QLineEdit()
self.name_input.setPlaceholderText("Enter project name...")
form_layout.addRow("Name:", self.name_input)
-
+
self.desc_input = QLineEdit()
self.desc_input.setPlaceholderText("Enter description (optional)...")
form_layout.addRow("Description:", self.desc_input)
-
+
layout.addLayout(form_layout)
layout.addSpacing(10)
-
+
# Button box
button_box = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
@@ -119,11 +119,11 @@ class NewProjectDialog(QDialog):
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
layout.addWidget(button_box)
-
+
def get_project_data(self) -> tuple:
"""Get the entered project data."""
return self.name_input.text().strip(), self.desc_input.text().strip()
-
+
def accept(self):
"""Validate before accepting."""
name = self.name_input.text().strip()
@@ -135,35 +135,35 @@ class NewProjectDialog(QDialog):
class ProjectStatsDialog(QDialog):
"""Dialog for displaying project statistics."""
-
+
def __init__(self, project: Project, parent=None):
super().__init__(parent)
self.project = project
self.setWindowTitle(f"Project Statistics - {project.name}")
self.setMinimumWidth(350)
self.setup_ui()
-
+
def setup_ui(self):
layout = QVBoxLayout(self)
-
+
# Stats display
stats_group = QGroupBox("Project Information")
stats_layout = QFormLayout(stats_group)
-
+
stats_layout.addRow("ID:", QLabel(str(self.project.id)))
stats_layout.addRow("Name:", QLabel(self.project.name))
stats_layout.addRow("Type:", QLabel(self.project.type))
stats_layout.addRow("Status:", QLabel(self.project.status))
-
+
# Description from metadata
description = self.project.metadata.get('description', 'N/A') if self.project.metadata else 'N/A'
stats_layout.addRow("Description:", QLabel(description))
-
+
created = self.project.created_at.strftime("%Y-%m-%d %H:%M") if self.project.created_at else "N/A"
stats_layout.addRow("Created:", QLabel(created))
-
+
layout.addWidget(stats_group)
-
+
# Close button
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
button_box.rejected.connect(self.reject)
@@ -172,22 +172,22 @@ class ProjectStatsDialog(QDialog):
class SettingsDialog(QDialog):
"""Dialog for application settings."""
-
+
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Settings")
self.setMinimumWidth(400)
self.setup_ui()
-
+
def setup_ui(self):
layout = QVBoxLayout(self)
-
+
info_label = QLabel("Settings configuration would go here.")
info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(info_label)
-
+
layout.addStretch()
-
+
button_box = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
)
@@ -203,54 +203,54 @@ class SettingsDialog(QDialog):
class MainWindow(QMainWindow):
"""
Main application window for Lemontropia Suite.
-
+
Provides project management, session control, and log viewing capabilities.
"""
-
+
# Signals
session_started = pyqtSignal(int) # project_id
session_stopped = pyqtSignal()
session_paused = pyqtSignal()
session_resumed = pyqtSignal()
-
+
def __init__(self):
super().__init__()
-
+
# Window configuration
self.setWindowTitle("Lemontropia Suite")
self.setMinimumSize(1200, 800)
self.resize(1400, 900)
-
+
# Initialize database and real project manager
self.db = DatabaseManager()
if not self.db.initialize():
QMessageBox.critical(self, "Error", "Failed to initialize database!")
sys.exit(1)
-
+
self.project_manager = ProjectManager(self.db)
-
+
# Initialize HUD
self.hud = HUDOverlay()
-
+
# Log watcher - created when session starts
self.log_watcher: Optional[LogWatcher] = None
self._log_watcher_task = None
-
+
# Thread-safe queue for cross-thread communication
from queue import Queue
self._event_queue = Queue()
-
+
# Timer to process queued events in main thread
self._queue_timer = QTimer(self)
self._queue_timer.timeout.connect(self._process_queued_events)
self._queue_timer.start(100) # Check every 100ms
-
+
# State
self.current_project: Optional[Project] = None
self.session_state = SessionState.IDLE
self.current_session_id: Optional[int] = None
self._current_db_session_id: Optional[int] = None
-
+
# Selected gear
self._selected_weapon: Optional[str] = None
self._selected_weapon_stats: Optional[dict] = None
@@ -261,76 +261,76 @@ class MainWindow(QMainWindow):
self._selected_medical_tool: Optional[str] = None
self._selected_medical_tool_stats: Optional[dict] = None
self._selected_loadout: Optional[Any] = None
-
+
# Setup UI
self.setup_ui()
self.apply_dark_theme()
self.create_menu_bar()
self.create_status_bar()
-
+
# Load initial data
self.refresh_project_list()
-
+
# Welcome message
self.log_info("Application", "Lemontropia Suite initialized")
self.log_info("Database", f"Database ready: {self.db.db_path}")
-
+
# ========================================================================
# UI Setup
# ========================================================================
-
+
def setup_ui(self):
"""Setup the main UI layout."""
# Central widget
central_widget = QWidget()
self.setCentralWidget(central_widget)
-
+
# Main layout
main_layout = QVBoxLayout(central_widget)
main_layout.setContentsMargins(10, 10, 10, 10)
main_layout.setSpacing(10)
-
+
# Main splitter (horizontal: left panels | log panel)
self.main_splitter = QSplitter(Qt.Orientation.Horizontal)
main_layout.addWidget(self.main_splitter)
-
+
# Left side container
left_container = QWidget()
left_layout = QVBoxLayout(left_container)
left_layout.setContentsMargins(0, 0, 0, 0)
left_layout.setSpacing(10)
-
+
# Left splitter (vertical: projects | session control)
left_splitter = QSplitter(Qt.Orientation.Vertical)
left_layout.addWidget(left_splitter)
-
+
# Project panel
self.project_panel = self.create_project_panel()
left_splitter.addWidget(self.project_panel)
-
+
# Session control panel
self.session_panel = self.create_session_panel()
left_splitter.addWidget(self.session_panel)
-
+
# Set splitter proportions
left_splitter.setSizes([400, 300])
-
+
# Add left container to main splitter
self.main_splitter.addWidget(left_container)
-
+
# Log output panel
self.log_panel = self.create_log_panel()
self.main_splitter.addWidget(self.log_panel)
-
+
# Set main splitter proportions (30% left, 70% log)
self.main_splitter.setSizes([400, 900])
-
+
def create_project_panel(self) -> QGroupBox:
"""Create the project management panel."""
panel = QGroupBox("Project Management")
layout = QVBoxLayout(panel)
layout.setSpacing(8)
-
+
# Project list
self.project_list = QTreeWidget()
self.project_list.setHeaderLabels(["ID", "Name", "Type", "Status"])
@@ -339,7 +339,7 @@ class MainWindow(QMainWindow):
self.project_list.setRootIsDecorated(False)
self.project_list.itemSelectionChanged.connect(self.on_project_selected)
self.project_list.itemDoubleClicked.connect(self.on_project_double_clicked)
-
+
# Adjust column widths
header = self.project_list.header()
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
@@ -349,51 +349,51 @@ class MainWindow(QMainWindow):
header.resizeSection(0, 50)
header.resizeSection(2, 70)
header.resizeSection(3, 80)
-
+
layout.addWidget(self.project_list)
-
+
# Button row
button_layout = QHBoxLayout()
-
+
self.new_project_btn = QPushButton("β New Project")
self.new_project_btn.setToolTip("Create a new project")
self.new_project_btn.clicked.connect(self.on_new_project)
button_layout.addWidget(self.new_project_btn)
-
+
self.view_stats_btn = QPushButton("π View Stats")
self.view_stats_btn.setToolTip("View selected project statistics")
self.view_stats_btn.clicked.connect(self.on_view_stats)
self.view_stats_btn.setEnabled(False)
button_layout.addWidget(self.view_stats_btn)
-
+
self.refresh_btn = QPushButton("π Refresh")
self.refresh_btn.setToolTip("Refresh project list")
self.refresh_btn.clicked.connect(self.refresh_project_list)
button_layout.addWidget(self.refresh_btn)
-
+
layout.addLayout(button_layout)
-
+
return panel
-
+
def create_session_panel(self) -> QGroupBox:
"""Create the session control panel."""
panel = QGroupBox("Session Control")
layout = QVBoxLayout(panel)
layout.setSpacing(10)
-
+
# Current project display
project_info_layout = QFormLayout()
self.current_project_label = QLabel("No project selected")
self.current_project_label.setStyleSheet("font-weight: bold; color: #888;")
project_info_layout.addRow("Selected Project:", self.current_project_label)
layout.addLayout(project_info_layout)
-
+
# Separator line
separator = QFrame()
separator.setFrameShape(QFrame.Shape.HLine)
separator.setStyleSheet("background-color: #444;")
layout.addWidget(separator)
-
+
# Session status
status_layout = QHBoxLayout()
status_layout.addWidget(QLabel("Status:"))
@@ -411,211 +411,211 @@ class MainWindow(QMainWindow):
status_layout.addWidget(self.session_status_label)
status_layout.addStretch()
layout.addLayout(status_layout)
-
+
# Control buttons
button_layout = QHBoxLayout()
-
+
self.start_session_btn = QPushButton("βΆοΈ Start Session")
self.start_session_btn.setToolTip("Start a new session with selected project")
self.start_session_btn.clicked.connect(self.on_start_session)
self.start_session_btn.setEnabled(False)
button_layout.addWidget(self.start_session_btn)
-
+
self.stop_session_btn = QPushButton("βΉοΈ Stop")
self.stop_session_btn.setToolTip("Stop current session")
self.stop_session_btn.clicked.connect(self.on_stop_session)
self.stop_session_btn.setEnabled(False)
button_layout.addWidget(self.stop_session_btn)
-
+
self.pause_session_btn = QPushButton("βΈοΈ Pause")
self.pause_session_btn.setToolTip("Pause/Resume current session")
self.pause_session_btn.clicked.connect(self.on_pause_session)
self.pause_session_btn.setEnabled(False)
button_layout.addWidget(self.pause_session_btn)
-
+
layout.addLayout(button_layout)
-
+
# Session info
self.session_info_label = QLabel("Ready to start")
self.session_info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.session_info_label.setStyleSheet("color: #666; padding: 10px;")
layout.addWidget(self.session_info_label)
-
+
layout.addStretch()
-
+
return panel
-
+
def create_log_panel(self) -> QGroupBox:
"""Create the log output panel."""
panel = QGroupBox("Event Log")
layout = QVBoxLayout(panel)
layout.setSpacing(8)
-
+
# Log text edit
self.log_output = QTextEdit()
self.log_output.setReadOnly(True)
self.log_output.setFont(QFont("Consolas", 10))
self.log_output.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap)
layout.addWidget(self.log_output)
-
+
# Log controls
controls_layout = QHBoxLayout()
-
+
self.clear_log_btn = QPushButton("ποΈ Clear")
self.clear_log_btn.setToolTip("Clear log output")
self.clear_log_btn.clicked.connect(self.log_output.clear)
controls_layout.addWidget(self.clear_log_btn)
-
+
controls_layout.addStretch()
-
+
self.auto_scroll_check = QLabel("β Auto-scroll")
self.auto_scroll_check.setStyleSheet("color: #888;")
controls_layout.addWidget(self.auto_scroll_check)
-
+
layout.addLayout(controls_layout)
-
+
return panel
-
+
def create_menu_bar(self):
"""Create the application menu bar."""
menubar = self.menuBar()
-
+
# File menu
file_menu = menubar.addMenu("&File")
-
+
new_project_action = QAction("&New Project", self)
new_project_action.setShortcut("Ctrl+N")
new_project_action.triggered.connect(self.on_new_project)
file_menu.addAction(new_project_action)
-
+
open_project_action = QAction("&Open Project", self)
open_project_action.setShortcut("Ctrl+O")
open_project_action.triggered.connect(self.on_open_project)
file_menu.addAction(open_project_action)
-
+
file_menu.addSeparator()
-
+
exit_action = QAction("E&xit", self)
exit_action.setShortcut("Alt+F4")
exit_action.triggered.connect(self.close)
file_menu.addAction(exit_action)
-
+
# Session menu
session_menu = menubar.addMenu("&Session")
-
+
start_action = QAction("&Start", self)
start_action.setShortcut("F5")
start_action.triggered.connect(self.on_start_session)
session_menu.addAction(start_action)
self.start_action = start_action
-
+
stop_action = QAction("St&op", self)
stop_action.setShortcut("Shift+F5")
stop_action.triggered.connect(self.on_stop_session)
session_menu.addAction(stop_action)
self.stop_action = stop_action
-
+
pause_action = QAction("&Pause", self)
pause_action.setShortcut("F6")
pause_action.triggered.connect(self.on_pause_session)
session_menu.addAction(pause_action)
self.pause_action = pause_action
-
+
# View menu
view_menu = menubar.addMenu("&View")
-
+
show_hud_action = QAction("Show &HUD", self)
show_hud_action.setShortcut("F9")
show_hud_action.triggered.connect(self.on_show_hud)
view_menu.addAction(show_hud_action)
-
+
hide_hud_action = QAction("&Hide HUD", self)
hide_hud_action.setShortcut("F10")
hide_hud_action.triggered.connect(self.on_hide_hud)
view_menu.addAction(hide_hud_action)
-
+
view_menu.addSeparator()
-
+
settings_action = QAction("&Settings", self)
settings_action.setShortcut("Ctrl+,")
settings_action.triggered.connect(self.on_settings)
view_menu.addAction(settings_action)
-
+
# Tools menu
tools_menu = menubar.addMenu("&Tools")
-
+
# Select Gear submenu
select_gear_menu = tools_menu.addMenu("Select &Gear")
-
+
select_weapon_action = QAction("&Weapon", self)
select_weapon_action.setShortcut("Ctrl+W")
select_weapon_action.triggered.connect(lambda: self.on_select_gear("weapon"))
select_gear_menu.addAction(select_weapon_action)
-
+
select_armor_action = QAction("&Armor", self)
select_armor_action.setShortcut("Ctrl+Shift+A")
select_armor_action.triggered.connect(lambda: self.on_select_gear("armor"))
select_gear_menu.addAction(select_armor_action)
-
+
select_finder_action = QAction("&Finder", self)
select_finder_action.setShortcut("Ctrl+Shift+F")
select_finder_action.triggered.connect(lambda: self.on_select_gear("finder"))
select_gear_menu.addAction(select_finder_action)
-
+
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()
-
+
loadout_action = QAction("&Loadout Manager", self)
loadout_action.setShortcut("Ctrl+L")
loadout_action.triggered.connect(self.on_loadout_manager)
tools_menu.addAction(loadout_action)
-
+
# Help menu
help_menu = menubar.addMenu("&Help")
-
+
about_action = QAction("&About", self)
about_action.triggered.connect(self.on_about)
help_menu.addAction(about_action)
-
+
def create_status_bar(self):
"""Create the status bar."""
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
-
+
# Permanent widgets
self.status_state_label = QLabel("β Idle")
self.status_state_label.setStyleSheet("color: #888; padding: 0 10px;")
self.status_bar.addPermanentWidget(self.status_state_label)
-
+
self.status_project_label = QLabel("No project")
self.status_project_label.setStyleSheet("color: #888; padding: 0 10px;")
self.status_bar.addPermanentWidget(self.status_project_label)
-
+
# Message area
self.status_bar.showMessage("Ready")
-
+
# ========================================================================
# Theme
# ========================================================================
-
+
def apply_dark_theme(self):
"""Apply dark theme styling."""
dark_stylesheet = """
QMainWindow {
background-color: #1e1e1e;
}
-
+
QWidget {
background-color: #1e1e1e;
color: #e0e0e0;
font-family: 'Segoe UI', Arial, sans-serif;
font-size: 10pt;
}
-
+
QGroupBox {
font-weight: bold;
border: 1px solid #444;
@@ -624,14 +624,14 @@ class MainWindow(QMainWindow):
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;
@@ -639,61 +639,61 @@ class MainWindow(QMainWindow):
padding: 8px 16px;
color: #e0e0e0;
}
-
+
QPushButton:hover {
background-color: #3d3d3d;
border-color: #555;
}
-
+
QPushButton:pressed {
background-color: #4d4d4d;
}
-
+
QPushButton:disabled {
background-color: #252525;
color: #666;
border-color: #333;
}
-
+
QPushButton#start_button {
background-color: #1b5e20;
border-color: #2e7d32;
}
-
+
QPushButton#start_button:hover {
background-color: #2e7d32;
}
-
+
QPushButton#stop_button {
background-color: #b71c1c;
border-color: #c62828;
}
-
+
QPushButton#stop_button:hover {
background-color: #c62828;
}
-
+
QTreeWidget {
background-color: #252525;
border: 1px solid #444;
border-radius: 4px;
outline: none;
}
-
+
QTreeWidget::item {
padding: 6px;
border-bottom: 1px solid #333;
}
-
+
QTreeWidget::item:selected {
background-color: #0d47a1;
color: white;
}
-
+
QTreeWidget::item:alternate {
background-color: #2a2a2a;
}
-
+
QHeaderView::section {
background-color: #2d2d2d;
padding: 6px;
@@ -701,7 +701,7 @@ class MainWindow(QMainWindow):
border-right: 1px solid #444;
font-weight: bold;
}
-
+
QTextEdit {
background-color: #151515;
border: 1px solid #444;
@@ -709,7 +709,7 @@ class MainWindow(QMainWindow):
padding: 8px;
color: #d0d0d0;
}
-
+
QLineEdit {
background-color: #252525;
border: 1px solid #444;
@@ -717,86 +717,86 @@ class MainWindow(QMainWindow):
padding: 6px;
color: #e0e0e0;
}
-
+
QLineEdit:focus {
border-color: #0d47a1;
}
-
+
QMenuBar {
background-color: #1e1e1e;
border-bottom: 1px solid #444;
}
-
+
QMenuBar::item {
background-color: transparent;
padding: 6px 12px;
}
-
+
QMenuBar::item:selected {
background-color: #2d2d2d;
}
-
+
QMenu {
background-color: #2d2d2d;
border: 1px solid #444;
padding: 4px;
}
-
+
QMenu::item {
padding: 6px 24px;
border-radius: 2px;
}
-
+
QMenu::item:selected {
background-color: #0d47a1;
}
-
+
QMenu::separator {
height: 1px;
background-color: #444;
margin: 4px 8px;
}
-
+
QStatusBar {
background-color: #1e1e1e;
border-top: 1px solid #444;
}
-
+
QSplitter::handle {
background-color: #444;
}
-
+
QSplitter::handle:horizontal {
width: 2px;
}
-
+
QSplitter::handle:vertical {
height: 2px;
}
-
+
QDialog {
background-color: #1e1e1e;
}
-
+
QLabel {
color: #e0e0e0;
}
-
+
QFormLayout QLabel {
color: #888;
}
"""
self.setStyleSheet(dark_stylesheet)
-
+
# ========================================================================
# Project Management
# ========================================================================
-
+
def refresh_project_list(self):
"""Refresh the project list display."""
self.project_list.clear()
projects = self.project_manager.list_projects()
-
+
for project in projects:
item = QTreeWidgetItem([
str(project.id),
@@ -806,16 +806,16 @@ class MainWindow(QMainWindow):
])
item.setData(0, Qt.ItemDataRole.UserRole, project.id)
self.project_list.addTopLevelItem(item)
-
+
self.log_debug("ProjectManager", f"Loaded {len(projects)} projects")
-
+
def on_project_selected(self):
"""Handle project selection change."""
selected = self.project_list.selectedItems()
if selected:
project_id = selected[0].data(0, Qt.ItemDataRole.UserRole)
self.current_project = self.project_manager.load_project(project_id)
-
+
if self.current_project:
self.current_project_label.setText(self.current_project.name)
self.current_project_label.setStyleSheet("font-weight: bold; color: #4caf50;")
@@ -830,14 +830,14 @@ class MainWindow(QMainWindow):
self.view_stats_btn.setEnabled(False)
self.start_session_btn.setEnabled(False)
self.status_project_label.setText("No project")
-
+
def on_project_double_clicked(self, item: QTreeWidgetItem, column: int):
"""Handle double-click on project."""
project_id = item.data(0, Qt.ItemDataRole.UserRole)
project = self.project_manager.load_project(project_id)
if project:
self.show_project_stats(project)
-
+
def on_new_project(self):
"""Handle new project creation."""
dialog = NewProjectDialog(self)
@@ -848,36 +848,36 @@ class MainWindow(QMainWindow):
self.refresh_project_list()
self.log_info("ProjectManager", f"Created project: {project.name}")
self.status_bar.showMessage(f"Project '{name}' created", 3000)
-
+
def on_open_project(self):
"""Handle open project action."""
# For now, just focus the project list
self.project_list.setFocus()
self.status_bar.showMessage("Select a project from the list", 3000)
-
+
def on_view_stats(self):
"""Handle view stats button."""
if self.current_project:
self.show_project_stats(self.current_project)
-
+
def show_project_stats(self, project: Project):
"""Show project statistics dialog."""
dialog = ProjectStatsDialog(project, self)
dialog.exec()
-
+
# ========================================================================
# Session Control
# ========================================================================
-
+
def start_session(self, project_id: int):
"""
Start a new session with the given project.
-
+
Args:
project_id: The ID of the project to start session for
"""
from core.project_manager import ProjectData
-
+
# Get real project from database
projects = self.project_manager.list_projects()
project = None
@@ -885,34 +885,34 @@ class MainWindow(QMainWindow):
if p.id == project_id:
project = p
break
-
+
if not project:
self.log_error("Session", f"Project {project_id} not found")
return
-
+
if self.session_state != SessionState.IDLE:
self.log_warning("Session", "Cannot start: session already active")
return
-
+
# Update state
self.set_session_state(SessionState.RUNNING)
self.current_session_id = project_id
-
+
# Emit signal
self.session_started.emit(project_id)
-
+
# Log
self.log_info("Session", f"Started session for project: {project.name}")
self.session_info_label.setText(f"Session active: {project.name}")
-
+
# Start real session in database
session = self.project_manager.start_session(project_id)
self._current_db_session_id = session.id if session else None
-
+
# Setup LogWatcher
use_mock = os.getenv('USE_MOCK_DATA', 'false').lower() in ('true', '1', 'yes')
log_path = os.getenv('EU_CHAT_LOG_PATH', '')
-
+
if use_mock or not log_path:
# Use mock log for testing
test_data_dir = Path(__file__).parent.parent / "test-data"
@@ -926,44 +926,53 @@ class MainWindow(QMainWindow):
else:
self.log_watcher = LogWatcher(log_path, poll_interval=1.0, mock_mode=False)
self.log_info("LogWatcher", f"Using REAL log: {log_path}")
-
+
# Subscribe to events
self._setup_log_watcher_callbacks()
-
+
# Start LogWatcher in background
self._start_log_watcher()
-
+
# Show HUD and start session tracking
self.hud.show()
weapon_name = self._selected_weapon or "Unknown"
weapon_stats = self._selected_weapon_stats or {}
weapon_dpp = Decimal(str(weapon_stats.get('dpp', 0)))
weapon_cost_per_hour = Decimal(str(weapon_stats.get('cost_per_hour', 0)))
-
+
+ # Get loadout name from session selection
+ loadout_name = getattr(self, '_session_loadout_name', None) or "Default"
+ loadout_id = getattr(self, '_session_loadout_id', None)
+
self.hud.start_session(
weapon=weapon_name,
- loadout="Default",
+ loadout=loadout_name,
weapon_dpp=weapon_dpp,
weapon_cost_per_hour=weapon_cost_per_hour
)
- self.log_info("HUD", f"HUD shown - Weapon: {weapon_name} (DPP: {weapon_dpp:.2f}, Cost/h: {weapon_cost_per_hour:.2f} PED)")
-
+
+ # Set up cost tracker if loadout selected
+ if loadout_id:
+ self._setup_session_cost_tracker(loadout_id)
+
+ self.log_info("HUD", f"HUD shown - Weapon: {weapon_name}, Loadout: {loadout_name} (DPP: {weapon_dpp:.2f}, Cost/h: {weapon_cost_per_hour:.2f} PED)")
+
def _setup_log_watcher_callbacks(self):
"""Setup LogWatcher event callbacks."""
if not self.log_watcher:
return
-
+
from core.project_manager import LootEvent
from decimal import Decimal
-
+
def on_heal(event):
"""Handle heal events from chat.log.
-
+
Pattern: "You healed yourself X points"
Calculates healing cost based on FAP decay and updates HUD.
"""
heal_amount = event.data.get('heal_amount', Decimal('0'))
-
+
# Calculate heal cost based on selected medical tool decay
# Get decay per heal from loadout or use default
decay_cost = Decimal('0')
@@ -977,26 +986,26 @@ class MainWindow(QMainWindow):
else:
# Default estimate: 2 PEC per heal
decay_cost = Decimal('0.02')
-
+
# Update HUD with heal event
self.hud.on_heal_event(heal_amount, decay_cost)
-
+
# Log to UI
self.log_info("Heal", f"Healed {heal_amount} HP (Cost: {decay_cost:.4f} PED)")
-
+
def on_loot(event):
"""Handle loot events."""
item_name = event.data.get('item_name', 'Unknown')
value_ped = event.data.get('value_ped', Decimal('0.0'))
quantity = event.data.get('quantity', 1)
-
+
# Skip Universal Ammo
if item_name == 'Universal Ammo':
return
-
+
# Estimated Kills: Every loot event = 1 mob killed
self.hud.update_stats({'kills_add': 1})
-
+
# Queue database write for main thread (SQLite thread safety)
if self._current_db_session_id:
self._event_queue.put({
@@ -1007,47 +1016,47 @@ class MainWindow(QMainWindow):
'value_ped': value_ped,
'raw_line': event.raw_line
})
-
+
# Update HUD (thread-safe)
self.hud.on_loot_event(item_name, value_ped)
-
+
# Log to UI (main thread only - use signal/slot or queue)
# We'll log this in _process_queued_events instead
-
+
def on_global(event):
"""Handle global events."""
value_ped = event.data.get('value_ped', Decimal('0.0'))
player = event.data.get('player_name', 'Unknown')
self.hud.on_global(value_ped)
self.log_info("Global", f"{player} found {value_ped} PED!")
-
+
def on_personal_global(event):
"""Handle personal global events."""
value_ped = event.data.get('value_ped', Decimal('0.0'))
creature = event.data.get('creature', 'Unknown')
self.hud.on_global(value_ped)
self.log_info("Global", f"π YOUR GLOBAL: {creature} for {value_ped} PED!!!")
-
+
def on_hof(event):
"""Handle HoF events."""
value_ped = event.data.get('value_ped', Decimal('0.0'))
self.hud.on_hof(value_ped)
self.log_info("HoF", f"π HALL OF FAME: {value_ped} PED!")
-
+
def on_skill(event):
"""Handle skill events."""
skill_name = event.data.get('skill_name', 'Unknown')
gained = event.data.get('gained', 0)
self.log_info("Skill", f"{skill_name} +{gained}")
-
+
def on_damage_dealt(event):
"""Handle damage dealt - also track weapon cost and shots fired."""
damage = event.data.get('damage', 0)
self.hud.on_damage_dealt(float(damage))
-
+
# Track shots fired (1 shot per damage event)
self.hud.update_stats({'shots_add': 1})
-
+
# Track weapon decay cost per shot
# Only count as one shot fired per damage event
if self._selected_weapon_stats:
@@ -1058,18 +1067,18 @@ class MainWindow(QMainWindow):
# Convert PEC to PED
cost_ped = Decimal(str(decay)) / Decimal('100')
self.hud.update_cost(cost_ped)
-
+
def on_critical_hit(event):
"""Handle critical hit - same as damage dealt."""
on_damage_dealt(event)
-
+
def on_damage_taken(event):
"""Handle damage taken - track armor decay cost."""
from decimal import Decimal
-
+
damage = event.data.get('damage', 0)
self.hud.on_damage_taken(float(damage))
-
+
# Calculate armor decay cost per hit
# Formula: cost_per_hit = armor_decay_pec / 100 (PED)
if self._selected_armor_stats and self._selected_armor_stats.get('decay'):
@@ -1079,12 +1088,12 @@ class MainWindow(QMainWindow):
cost_ped = armor_decay_pec / Decimal('100')
self.hud.update_cost(cost_ped)
self.log_debug("Armor", f"Armor decay: {cost_ped:.4f} PED (decay: {armor_decay_pec} PEC)")
-
+
def on_evade(event):
"""Handle evade."""
evade_type = event.data.get('type', 'Evade')
self.log_info("Evade", evade_type)
-
+
# Subscribe to all event types
self.log_watcher.subscribe('loot', on_loot)
self.log_watcher.subscribe('global', on_global)
@@ -1096,18 +1105,18 @@ class MainWindow(QMainWindow):
self.log_watcher.subscribe('damage_taken', on_damage_taken)
self.log_watcher.subscribe('evade', on_evade)
self.log_watcher.subscribe('heal', on_heal) # NEW: Heal event tracking
-
+
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)
@@ -1120,14 +1129,14 @@ class MainWindow(QMainWindow):
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:
@@ -1135,17 +1144,51 @@ class MainWindow(QMainWindow):
self._log_watcher_thread.wait(2000) # Wait up to 2 seconds
self._log_watcher_thread = None
self.log_info("LogWatcher", "Stopped")
+
+ def _setup_session_cost_tracker(self, loadout_id: int):
+ """Set up session cost tracker with selected loadout."""
+ from core.session_cost_tracker import SessionCostTracker
+ from core.database import DatabaseManager
+
+ try:
+ db = DatabaseManager()
+ self._cost_tracker = SessionCostTracker(
+ session_id=self._current_db_session_id,
+ loadout_id=loadout_id,
+ db_manager=db
+ )
+ self._cost_tracker.register_callback(self._on_cost_update)
+ self.log_info("CostTracker", f"Cost tracking enabled for loadout ID: {loadout_id}")
+ except Exception as e:
+ self.log_error("CostTracker", f"Failed to set up cost tracker: {e}")
+ def _on_cost_update(self, state):
+ """Handle cost update from SessionCostTracker."""
+ # Update HUD with new cost state
+ if hasattr(self, 'hud') and self.hud:
+ summary = {
+ 'weapon_cost': state.weapon_cost,
+ 'armor_cost': state.armor_cost,
+ 'healing_cost': state.healing_cost,
+ 'enhancer_cost': state.enhancer_cost,
+ 'mindforce_cost': state.mindforce_cost,
+ 'shots_fired': state.shots_fired,
+ 'hits_taken': state.hits_taken,
+ 'heals_used': state.heals_used,
+ }
+ self.hud._stats.update_from_cost_tracker(summary)
+ self.hud._refresh_display()
+
def _process_queued_events(self):
"""Process events from the queue in the main thread (SQLite thread safety)."""
from core.project_manager import LootEvent
from decimal import Decimal
-
+
processed = 0
while not self._event_queue.empty() and processed < 10: # Process max 10 per tick
try:
event = self._event_queue.get_nowait()
-
+
if event['type'] == 'loot':
# Record to database (now in main thread - safe)
loot = LootEvent(
@@ -1156,14 +1199,14 @@ class MainWindow(QMainWindow):
raw_log_line=event['raw_line']
)
self.project_manager.record_loot(loot)
-
+
# Log to UI
self.log_info("Loot", f"{event['item_name']} x{event['quantity']} ({event['value_ped']} PED)")
-
+
processed += 1
except Exception as e:
self.log_error("EventQueue", f"Error processing event: {e}")
-
+
def on_start_session(self):
"""Handle start session button - shows loadout selection first."""
if self.current_project and self.session_state == SessionState.IDLE:
@@ -1185,36 +1228,36 @@ class MainWindow(QMainWindow):
self.log_info("Session", "Starting session without loadout")
self._session_loadout_id = None
self._session_loadout_name = None
-
+
# Now start the session
if self.current_project:
self.start_session(self.current_project.id)
-
+
def on_stop_session(self):
"""Handle stop session button."""
if self.session_state in (SessionState.RUNNING, SessionState.PAUSED):
# Stop LogWatcher
self._stop_log_watcher()
-
+
# End session in database
if self._current_db_session_id:
self.project_manager.end_session(self._current_db_session_id)
self._current_db_session_id = None
-
+
self.set_session_state(SessionState.IDLE)
self.current_session_id = None
-
+
self.session_stopped.emit()
-
+
self.log_info("Session", "Session stopped")
self.session_info_label.setText("Session stopped")
-
+
# End HUD session
self.hud.end_session()
-
+
# Hide HUD
self.hud.hide()
-
+
def on_pause_session(self):
"""Handle pause/resume session button."""
if self.session_state == SessionState.RUNNING:
@@ -1229,16 +1272,16 @@ class MainWindow(QMainWindow):
self.log_info("Session", "Session resumed")
self.session_info_label.setText("Session resumed")
self.pause_session_btn.setText("βΈοΈ Pause")
-
+
def set_session_state(self, state: SessionState):
"""
Update the session state and UI.
-
+
Args:
state: New session state
"""
self.session_state = state
-
+
# Update status label
colors = {
SessionState.IDLE: "#888",
@@ -1247,7 +1290,7 @@ class MainWindow(QMainWindow):
SessionState.ERROR: "#f44336",
SessionState.STOPPING: "#ff5722"
}
-
+
self.session_status_label.setText(state.value)
self.session_status_label.setStyleSheet(f"""
QLabel {{
@@ -1259,34 +1302,34 @@ class MainWindow(QMainWindow):
border: 1px solid #444;
}}
""")
-
+
# Update status bar
self.status_state_label.setText(f"β {state.value}")
self.status_state_label.setStyleSheet(f"color: {colors.get(state, '#888')}; padding: 0 10px;")
-
+
# Update buttons
self.start_session_btn.setEnabled(
state == SessionState.IDLE and self.current_project is not None
)
self.stop_session_btn.setEnabled(state in (SessionState.RUNNING, SessionState.PAUSED))
self.pause_session_btn.setEnabled(state in (SessionState.RUNNING, SessionState.PAUSED))
-
+
# Update menu actions
self.start_action.setEnabled(self.start_session_btn.isEnabled())
self.stop_action.setEnabled(self.stop_session_btn.isEnabled())
self.pause_action.setEnabled(self.pause_session_btn.isEnabled())
-
+
if state == SessionState.IDLE:
self.pause_session_btn.setText("βΈοΈ Pause")
-
+
# ========================================================================
# Log Handling
# ========================================================================
-
+
def on_log_event(self, event: LogEvent):
"""
Handle incoming log events.
-
+
Args:
event: The log event to display
"""
@@ -1298,32 +1341,32 @@ class MainWindow(QMainWindow):
"ERROR": "#f44336",
"CRITICAL": "#e91e63"
}
-
+
color = colors.get(event.level, "#e0e0e0")
html = f'{self.escape_html(str(event))}'
-
+
self.log_output.append(html)
-
+
# Auto-scroll to bottom
scrollbar = self.log_output.verticalScrollBar()
scrollbar.setValue(scrollbar.maximum())
-
+
def log_debug(self, source: str, message: str):
"""Log a debug message."""
self._append_log("DEBUG", source, message)
-
+
def log_info(self, source: str, message: str):
"""Log an info message."""
self._append_log("INFO", source, message)
-
+
def log_warning(self, source: str, message: str):
"""Log a warning message."""
self._append_log("WARNING", source, message)
-
+
def log_error(self, source: str, message: str):
"""Log an error message."""
self._append_log("ERROR", source, message)
-
+
def _append_log(self, level: str, source: str, message: str):
"""Append log message to output."""
timestamp = datetime.now().strftime("%H:%M:%S")
@@ -1333,52 +1376,52 @@ class MainWindow(QMainWindow):
"WARNING": "#ff9800",
"ERROR": "#f44336"
}.get(level, "#ffffff")
-
+
log_entry = f'[{timestamp}] [{level}] [{source}] {self.escape_html(message)}'
self.log_output.append(log_entry)
-
+
# Auto-scroll to bottom
scrollbar = self.log_output.verticalScrollBar()
scrollbar.setValue(scrollbar.maximum())
-
+
def escape_html(self, text: str) -> str:
"""Escape HTML special characters."""
return (text
.replace("&", "&")
.replace("<", "<")
.replace(">", ">"))
-
+
# ========================================================================
# Menu Actions
# ========================================================================
-
+
def on_show_hud(self):
"""Show the HUD overlay."""
self.hud.show()
self.log_info("HUD", "HUD overlay shown")
-
+
def on_hide_hud(self):
"""Hide the HUD overlay."""
self.hud.hide()
self.log_info("HUD", "HUD overlay hidden")
-
+
def on_settings(self):
"""Open settings dialog."""
dialog = SettingsDialog(self)
dialog.exec()
-
+
def on_loadout_manager(self):
"""Open Loadout Manager dialog."""
from ui.loadout_manager import LoadoutManagerDialog
dialog = LoadoutManagerDialog(self)
dialog.loadout_saved.connect(self.on_loadout_selected)
dialog.exec()
-
+
def on_loadout_selected(self, loadout):
"""Handle loadout selection from Loadout Manager."""
self._selected_loadout = loadout
self.log_info("Loadout", f"Selected loadout: {loadout.name}")
-
+
# Update selected gear from loadout
if hasattr(loadout, 'weapon_name'):
self._selected_weapon = loadout.weapon_name
@@ -1389,19 +1432,19 @@ class MainWindow(QMainWindow):
'decay': float(loadout.heal_cost_pec),
'cost_per_heal': float(loadout.heal_cost_pec) / 100.0, # Convert PEC to PED
}
-
+
def on_select_gear(self, gear_type: str = "weapon"):
"""Open Gear Selector dialog."""
from ui.gear_selector import GearSelectorDialog
-
+
dialog = GearSelectorDialog(gear_type, self)
dialog.gear_selected.connect(self.on_gear_selected)
dialog.exec()
-
+
def on_gear_selected(self, gear_type: str, name: str, stats: dict):
"""Handle gear selection."""
self.log_info("Gear", f"Selected {gear_type}: {name}")
-
+
if gear_type == "weapon":
self._selected_weapon = name
self._selected_weapon_stats = stats
@@ -1418,7 +1461,7 @@ class MainWindow(QMainWindow):
self._selected_medical_tool_stats = stats
if self.session_state == SessionState.RUNNING:
self.hud.update_stats({'medical_tool': name})
-
+
def on_about(self):
"""Show about dialog."""
QMessageBox.about(
@@ -1436,11 +1479,11 @@ class MainWindow(QMainWindow):
"""
)
-
+
# ========================================================================
# Event Overrides
# ========================================================================
-
+
def closeEvent(self, event):
"""Handle window close event."""
if self.session_state == SessionState.RUNNING:
@@ -1451,7 +1494,7 @@ class MainWindow(QMainWindow):
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No
)
-
+
if reply == QMessageBox.StandardButton.Yes:
self.on_stop_session()
event.accept()
@@ -1468,15 +1511,15 @@ class MainWindow(QMainWindow):
def main():
"""Main entry point for testing."""
app = QApplication(sys.argv)
-
+
# Set application-wide font
font = QFont("Segoe UI", 10)
app.setFont(font)
-
+
# Create and show main window
window = MainWindow()
window.show()
-
+
# Simulate some log activity for demonstration
def simulate_logs():
import random
@@ -1490,7 +1533,7 @@ def main():
"Buffer cleared",
"Sync complete"
]
-
+
if window.session_state == SessionState.RUNNING:
if random.random() < 0.3: # 30% chance each tick
event = LogEvent(
@@ -1500,12 +1543,12 @@ def main():
message=random.choice(messages)
)
window.log_watcher.emit(event)
-
+
# Timer to simulate log activity
timer = QTimer()
timer.timeout.connect(simulate_logs)
timer.start(1000) # Every second
-
+
sys.exit(app.exec())