You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
292 lines
10 KiB
292 lines
10 KiB
"""
|
|
Main Window - Primary application window with 3-panel layout
|
|
"""
|
|
|
|
from PyQt5.QtWidgets import (
|
|
QMainWindow, QSplitter, QStatusBar, QMenuBar, QMenu,
|
|
QAction, QMessageBox, QApplication
|
|
)
|
|
from PyQt5.QtCore import Qt, QTimer
|
|
from PyQt5.QtGui import QKeySequence
|
|
|
|
from app import config
|
|
from app.models import Course, get_course
|
|
from app.database import Database, get_database
|
|
from .navigation_panel import NavigationPanel
|
|
from .content_viewer import ContentViewer
|
|
from .progress_panel import ProgressPanel
|
|
|
|
|
|
class MainWindow(QMainWindow):
|
|
"""Main application window with 3-panel layout"""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
# Load course and database
|
|
self.course = get_course()
|
|
self.db = get_database()
|
|
|
|
# Get or create default user
|
|
self.user_id = self._get_or_create_user()
|
|
|
|
# Current state
|
|
self.current_lesson_id = None
|
|
|
|
# Initialize UI
|
|
self.init_ui()
|
|
self.create_menus()
|
|
|
|
# Connect signals
|
|
self.connect_signals()
|
|
|
|
# Auto-save timer
|
|
self.auto_save_timer = QTimer(self)
|
|
self.auto_save_timer.timeout.connect(self.auto_save)
|
|
self.auto_save_timer.start(config.AUTO_SAVE_INTERVAL * 1000) # Convert to ms
|
|
|
|
# Load progress and restore state
|
|
self.load_initial_state()
|
|
|
|
def init_ui(self):
|
|
"""Initialize the user interface"""
|
|
self.setWindowTitle(f"{config.APP_NAME} v{config.APP_VERSION}")
|
|
self.setGeometry(100, 100, config.DEFAULT_WINDOW_WIDTH, config.DEFAULT_WINDOW_HEIGHT)
|
|
|
|
# Create 3-panel splitter layout
|
|
self.splitter = QSplitter(Qt.Horizontal)
|
|
|
|
# Create panels
|
|
self.navigation_panel = NavigationPanel(self.course, self)
|
|
self.content_viewer = ContentViewer(self)
|
|
self.progress_panel = ProgressPanel(self.course, self)
|
|
|
|
# Add panels to splitter
|
|
self.splitter.addWidget(self.navigation_panel)
|
|
self.splitter.addWidget(self.content_viewer)
|
|
self.splitter.addWidget(self.progress_panel)
|
|
|
|
# Set initial splitter sizes
|
|
self.splitter.setSizes([
|
|
config.NAVIGATION_PANEL_DEFAULT_WIDTH,
|
|
config.DEFAULT_WINDOW_WIDTH - config.NAVIGATION_PANEL_DEFAULT_WIDTH - config.PROGRESS_PANEL_DEFAULT_WIDTH,
|
|
config.PROGRESS_PANEL_DEFAULT_WIDTH
|
|
])
|
|
|
|
# Set as central widget
|
|
self.setCentralWidget(self.splitter)
|
|
|
|
# Create status bar
|
|
self.status_bar = QStatusBar()
|
|
self.setStatusBar(self.status_bar)
|
|
self.status_bar.showMessage("Ready")
|
|
|
|
def create_menus(self):
|
|
"""Create menu bar"""
|
|
menubar = self.menuBar()
|
|
|
|
# File Menu
|
|
file_menu = menubar.addMenu("&File")
|
|
|
|
exit_action = QAction("E&xit", self)
|
|
exit_action.setShortcut(QKeySequence.Quit)
|
|
exit_action.triggered.connect(self.close)
|
|
file_menu.addAction(exit_action)
|
|
|
|
# View Menu
|
|
view_menu = menubar.addMenu("&View")
|
|
|
|
toggle_nav_action = QAction("Toggle &Navigation Panel", self)
|
|
toggle_nav_action.setShortcut("Ctrl+1")
|
|
toggle_nav_action.triggered.connect(lambda: self.navigation_panel.setVisible(not self.navigation_panel.isVisible()))
|
|
view_menu.addAction(toggle_nav_action)
|
|
|
|
toggle_progress_action = QAction("Toggle &Progress Panel", self)
|
|
toggle_progress_action.setShortcut("Ctrl+2")
|
|
toggle_progress_action.triggered.connect(lambda: self.progress_panel.setVisible(not self.progress_panel.isVisible()))
|
|
view_menu.addAction(toggle_progress_action)
|
|
|
|
view_menu.addSeparator()
|
|
|
|
reset_layout_action = QAction("&Reset Layout", self)
|
|
reset_layout_action.triggered.connect(self.reset_layout)
|
|
view_menu.addAction(reset_layout_action)
|
|
|
|
# Help Menu
|
|
help_menu = menubar.addMenu("&Help")
|
|
|
|
about_action = QAction("&About", self)
|
|
about_action.triggered.connect(self.show_about)
|
|
help_menu.addAction(about_action)
|
|
|
|
def connect_signals(self):
|
|
"""Connect signals between components"""
|
|
self.navigation_panel.lesson_selected.connect(self.on_lesson_selected)
|
|
|
|
def _get_or_create_user(self) -> int:
|
|
"""Get or create default user"""
|
|
# Check if user exists
|
|
row = self.db.fetch_one("SELECT user_id FROM users LIMIT 1")
|
|
|
|
if row:
|
|
return row[0]
|
|
|
|
# Create default user
|
|
cursor = self.db.execute("""
|
|
INSERT INTO users (username, created_at)
|
|
VALUES (?, datetime('now'))
|
|
""", ("default",))
|
|
self.db.commit()
|
|
|
|
return cursor.lastrowid
|
|
|
|
def load_initial_state(self):
|
|
"""Load progress and restore application state"""
|
|
# Get overall progress
|
|
progress = self.db.get_overall_progress(self.user_id)
|
|
|
|
# Update progress panel
|
|
completed_lessons = progress.get('lessons_completed', 0)
|
|
total_points = progress.get('total_points', 0)
|
|
total_time = progress.get('total_time', 0)
|
|
|
|
self.progress_panel.update_progress(completed_lessons, total_points, total_time)
|
|
|
|
# Update part progress
|
|
for part in self.course.parts:
|
|
part_completed = 0
|
|
for lesson in part.lessons:
|
|
lesson_prog = self.db.get_lesson_progress(self.user_id, lesson.id)
|
|
if lesson_prog and lesson_prog['status'] == 'completed':
|
|
part_completed += 1
|
|
part_total = len(part.lessons)
|
|
self.progress_panel.update_part_progress(part.number, part_completed, part_total)
|
|
|
|
# Update study streak
|
|
streak = self.db.get_study_streak(self.user_id) if hasattr(self.db, 'get_study_streak') else 0
|
|
self.progress_panel.update_streak(streak)
|
|
|
|
# Update exercises
|
|
if hasattr(self.db, 'get_exercise_progress'):
|
|
exercise_progress = self.db.get_exercise_progress(self.user_id)
|
|
if exercise_progress:
|
|
self.progress_panel.update_exercises(
|
|
exercise_progress.get('completed', 0),
|
|
self.course.total_exercises
|
|
)
|
|
else:
|
|
self.progress_panel.update_exercises(0, self.course.total_exercises)
|
|
|
|
# Update lesson statuses in navigation
|
|
for lesson in self.course.get_all_lessons():
|
|
lesson_progress = self.db.get_lesson_progress(self.user_id, lesson.id)
|
|
status = lesson_progress['status'] if lesson_progress else 'not_started'
|
|
self.navigation_panel.update_lesson_status(lesson.id, status)
|
|
|
|
# Get last viewed lesson
|
|
row = self.db.fetch_one("""
|
|
SELECT lesson_id FROM lesson_progress
|
|
WHERE user_id = ?
|
|
ORDER BY last_accessed DESC
|
|
LIMIT 1
|
|
""", (self.user_id,))
|
|
|
|
if row:
|
|
last_lesson_id = row[0]
|
|
# Don't auto-load, just highlight it
|
|
self.navigation_panel.set_current_lesson(last_lesson_id)
|
|
|
|
def on_lesson_selected(self, lesson_id: str):
|
|
"""Handle lesson selection from navigation"""
|
|
lesson = self.course.get_lesson(lesson_id)
|
|
if not lesson:
|
|
return
|
|
|
|
self.current_lesson_id = lesson_id
|
|
|
|
# Update navigation highlight
|
|
self.navigation_panel.set_current_lesson(lesson_id)
|
|
|
|
# Load lesson content
|
|
self.content_viewer.load_lesson(lesson)
|
|
|
|
# Update progress panel
|
|
self.progress_panel.update_current_lesson(
|
|
lesson.title,
|
|
lesson.points,
|
|
lesson.estimated_time
|
|
)
|
|
|
|
# Update database (mark as in_progress if not already completed)
|
|
lesson_progress = self.db.get_lesson_progress(self.user_id, lesson_id)
|
|
current_status = lesson_progress['status'] if lesson_progress else 'not_started'
|
|
if current_status == 'not_started':
|
|
self.db.update_lesson_progress(
|
|
self.user_id,
|
|
lesson_id,
|
|
status='in_progress'
|
|
)
|
|
self.navigation_panel.update_lesson_status(lesson_id, 'in_progress')
|
|
|
|
# Update last accessed
|
|
self.db.update_lesson_progress(
|
|
self.user_id,
|
|
lesson_id,
|
|
last_accessed=True
|
|
)
|
|
|
|
# Update status bar
|
|
self.status_bar.showMessage(f"Lesson {lesson.order}: {lesson.title}")
|
|
|
|
def auto_save(self):
|
|
"""Auto-save progress"""
|
|
if not self.current_lesson_id:
|
|
return
|
|
|
|
# Get scroll position
|
|
scroll_pos = self.content_viewer.get_scroll_position()
|
|
|
|
# Update in database
|
|
self.db.update_lesson_progress(
|
|
self.user_id,
|
|
self.current_lesson_id,
|
|
scroll_position=scroll_pos,
|
|
time_spent_increment=config.AUTO_SAVE_INTERVAL
|
|
)
|
|
|
|
def reset_layout(self):
|
|
"""Reset window layout to defaults"""
|
|
self.splitter.setSizes([
|
|
config.NAVIGATION_PANEL_DEFAULT_WIDTH,
|
|
config.DEFAULT_WINDOW_WIDTH - config.NAVIGATION_PANEL_DEFAULT_WIDTH - config.PROGRESS_PANEL_DEFAULT_WIDTH,
|
|
config.PROGRESS_PANEL_DEFAULT_WIDTH
|
|
])
|
|
self.navigation_panel.setVisible(True)
|
|
self.progress_panel.setVisible(True)
|
|
|
|
def show_about(self):
|
|
"""Show about dialog"""
|
|
QMessageBox.about(self, "About", f"""
|
|
<h2>{config.APP_NAME}</h2>
|
|
<p>Version {config.APP_VERSION}</p>
|
|
<p>By {config.APP_AUTHOR}</p>
|
|
<br>
|
|
<p>An interactive desktop application for learning about Tesla coils
|
|
and electromagnetic theory.</p>
|
|
<br>
|
|
<p><b>Course Statistics:</b></p>
|
|
<ul>
|
|
<li>{self.course.total_lessons} Lessons</li>
|
|
<li>{self.course.total_exercises} Exercises</li>
|
|
<li>{self.course.total_points} Total Points</li>
|
|
<li>{len(self.course.parts)} Parts</li>
|
|
</ul>
|
|
""")
|
|
|
|
def closeEvent(self, event):
|
|
"""Handle window close event"""
|
|
# Final auto-save
|
|
self.auto_save()
|
|
|
|
# Accept close
|
|
event.accept()
|