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

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