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