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.
211 lines
7.8 KiB
211 lines
7.8 KiB
"""
|
|
Navigation Panel - Left sidebar with course tree and navigation
|
|
"""
|
|
|
|
from PyQt5.QtWidgets import (
|
|
QWidget, QVBoxLayout, QTreeWidget, QTreeWidgetItem,
|
|
QLabel, QComboBox, QPushButton, QLineEdit, QHBoxLayout
|
|
)
|
|
from PyQt5.QtCore import Qt, pyqtSignal
|
|
from PyQt5.QtGui import QIcon, QColor, QBrush
|
|
|
|
from app import config
|
|
from app.models import Course, Lesson, Part, Section
|
|
|
|
|
|
class NavigationPanel(QWidget):
|
|
"""Left sidebar panel with course navigation tree"""
|
|
|
|
# Signals
|
|
lesson_selected = pyqtSignal(str) # lesson_id
|
|
|
|
def __init__(self, course: Course, parent=None):
|
|
super().__init__(parent)
|
|
self.course = course
|
|
self.current_lesson_id = None
|
|
self.lesson_items = {} # lesson_id -> QTreeWidgetItem mapping
|
|
|
|
self.init_ui()
|
|
self.populate_tree()
|
|
|
|
def init_ui(self):
|
|
"""Initialize the UI components"""
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(10, 10, 10, 10)
|
|
layout.setSpacing(10)
|
|
|
|
# Title
|
|
title = QLabel("Course Navigation")
|
|
title.setStyleSheet(f"font-size: 14pt; font-weight: bold; color: {config.COLOR_PRIMARY};")
|
|
layout.addWidget(title)
|
|
|
|
# Learning Path Filter (optional for Phase 2+)
|
|
path_layout = QHBoxLayout()
|
|
path_label = QLabel("Path:")
|
|
self.path_combo = QComboBox()
|
|
self.path_combo.addItem("All Lessons", None)
|
|
for path in self.course.learning_paths:
|
|
self.path_combo.addItem(path.title, path.id)
|
|
self.path_combo.currentIndexChanged.connect(self.on_path_filter_changed)
|
|
path_layout.addWidget(path_label)
|
|
path_layout.addWidget(self.path_combo, 1)
|
|
layout.addLayout(path_layout)
|
|
|
|
# Search box
|
|
search_layout = QHBoxLayout()
|
|
self.search_box = QLineEdit()
|
|
self.search_box.setPlaceholderText("Search lessons...")
|
|
self.search_box.textChanged.connect(self.on_search_changed)
|
|
search_layout.addWidget(self.search_box)
|
|
layout.addLayout(search_layout)
|
|
|
|
# Course tree
|
|
self.tree = QTreeWidget()
|
|
self.tree.setHeaderHidden(True)
|
|
self.tree.setIndentation(20)
|
|
self.tree.itemDoubleClicked.connect(self.on_item_double_clicked)
|
|
layout.addWidget(self.tree, 1) # Expand to fill space
|
|
|
|
# Quick actions
|
|
btn_layout = QVBoxLayout()
|
|
self.btn_continue = QPushButton("Continue Learning")
|
|
self.btn_continue.setStyleSheet(f"background-color: {config.COLOR_SUCCESS}; color: white; font-weight: bold; padding: 8px;")
|
|
self.btn_continue.clicked.connect(self.on_continue_learning)
|
|
btn_layout.addWidget(self.btn_continue)
|
|
layout.addLayout(btn_layout)
|
|
|
|
self.setMinimumWidth(config.NAVIGATION_PANEL_MIN_WIDTH)
|
|
|
|
def populate_tree(self):
|
|
"""Populate the tree with course structure"""
|
|
self.tree.clear()
|
|
self.lesson_items.clear()
|
|
|
|
# Add course title as root
|
|
root = QTreeWidgetItem(self.tree)
|
|
root.setText(0, self.course.title)
|
|
root.setExpanded(True)
|
|
root.setFlags(root.flags() & ~Qt.ItemIsSelectable)
|
|
|
|
# Add parts
|
|
for part in self.course.parts:
|
|
part_item = QTreeWidgetItem(root)
|
|
part_item.setText(0, f"Part {part.number}: {part.title}")
|
|
part_item.setExpanded(True)
|
|
part_item.setFlags(part_item.flags() & ~Qt.ItemIsSelectable)
|
|
part_item.setForeground(0, QBrush(QColor(config.COLOR_PRIMARY)))
|
|
|
|
# Add sections (if any)
|
|
if part.sections:
|
|
for section in part.sections:
|
|
section_item = QTreeWidgetItem(part_item)
|
|
section_item.setText(0, section.title)
|
|
section_item.setExpanded(True)
|
|
section_item.setFlags(section_item.flags() & ~Qt.ItemIsSelectable)
|
|
|
|
# Add lessons in section
|
|
for lesson in section.lessons:
|
|
self._add_lesson_item(section_item, lesson)
|
|
else:
|
|
# Add lessons directly to part
|
|
for lesson in part.lessons:
|
|
self._add_lesson_item(part_item, lesson)
|
|
|
|
def _add_lesson_item(self, parent_item: QTreeWidgetItem, lesson: Lesson):
|
|
"""Add a lesson item to the tree"""
|
|
lesson_item = QTreeWidgetItem(parent_item)
|
|
lesson_item.setText(0, f"{lesson.order}. {lesson.title}")
|
|
lesson_item.setData(0, Qt.UserRole, lesson.id) # Store lesson_id
|
|
|
|
# Store reference for quick lookup
|
|
self.lesson_items[lesson.id] = lesson_item
|
|
|
|
# Add status icon (default: not started)
|
|
self.update_lesson_status(lesson.id, 'not_started')
|
|
|
|
def update_lesson_status(self, lesson_id: str, status: str):
|
|
"""Update the visual status of a lesson"""
|
|
if lesson_id not in self.lesson_items:
|
|
return
|
|
|
|
item = self.lesson_items[lesson_id]
|
|
lesson = self.course.get_lesson(lesson_id)
|
|
|
|
# Status icons
|
|
icon_map = {
|
|
'completed': '✓',
|
|
'in_progress': '⊙',
|
|
'not_started': '○',
|
|
'locked': '🔒'
|
|
}
|
|
|
|
icon = icon_map.get(status, '○')
|
|
item.setText(0, f"{icon} {lesson.order}. {lesson.title}")
|
|
|
|
# Color coding
|
|
if status == 'completed':
|
|
item.setForeground(0, QBrush(QColor(config.COLOR_SUCCESS)))
|
|
elif status == 'in_progress':
|
|
item.setForeground(0, QBrush(QColor(config.COLOR_WARNING)))
|
|
else:
|
|
item.setForeground(0, QBrush(QColor(config.COLOR_TEXT)))
|
|
|
|
def set_current_lesson(self, lesson_id: str):
|
|
"""Highlight the current lesson"""
|
|
# Clear previous selection
|
|
if self.current_lesson_id and self.current_lesson_id in self.lesson_items:
|
|
prev_item = self.lesson_items[self.current_lesson_id]
|
|
prev_item.setBackground(0, QBrush(Qt.transparent))
|
|
|
|
# Set new selection
|
|
self.current_lesson_id = lesson_id
|
|
if lesson_id in self.lesson_items:
|
|
item = self.lesson_items[lesson_id]
|
|
item.setBackground(0, QBrush(QColor(config.COLOR_HIGHLIGHT)))
|
|
self.tree.scrollToItem(item)
|
|
|
|
def on_item_double_clicked(self, item: QTreeWidgetItem, column: int):
|
|
"""Handle double-click on tree item"""
|
|
lesson_id = item.data(0, Qt.UserRole)
|
|
if lesson_id:
|
|
self.lesson_selected.emit(lesson_id)
|
|
|
|
def on_continue_learning(self):
|
|
"""Handle 'Continue Learning' button click"""
|
|
# TODO: Get the next incomplete lesson from database
|
|
# For now, just select the first lesson
|
|
if self.course.lessons:
|
|
first_lesson = self.course.lessons[0]
|
|
self.lesson_selected.emit(first_lesson.id)
|
|
|
|
def on_path_filter_changed(self, index: int):
|
|
"""Handle learning path filter change"""
|
|
path_id = self.path_combo.itemData(index)
|
|
if path_id:
|
|
# Filter tree to show only lessons in this path
|
|
lessons_in_path = self.course.get_lessons_for_path(path_id)
|
|
path_lesson_ids = {lesson.id for lesson in lessons_in_path}
|
|
|
|
# Hide/show items
|
|
for lesson_id, item in self.lesson_items.items():
|
|
item.setHidden(lesson_id not in path_lesson_ids)
|
|
else:
|
|
# Show all
|
|
for item in self.lesson_items.values():
|
|
item.setHidden(False)
|
|
|
|
def on_search_changed(self, text: str):
|
|
"""Handle search text change"""
|
|
if not text:
|
|
# Show all
|
|
for item in self.lesson_items.values():
|
|
item.setHidden(False)
|
|
return
|
|
|
|
# Search lessons
|
|
results = self.course.search_lessons(text)
|
|
result_ids = {lesson.id for lesson in results}
|
|
|
|
# Hide/show items
|
|
for lesson_id, item in self.lesson_items.items():
|
|
item.setHidden(lesson_id not in result_ids)
|