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

"""
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()