gui.panels.course_bar
1# Copyright (C) 2026 David Vaquero <pepesan@gmail.com> 2# 3# This program is free software: you can redistribute it and/or modify 4# it under the terms of the GNU General Public License as published by 5# the Free Software Foundation, either version 3 of the License, or 6# (at your option) any later version. 7# 8# This program is distributed in the hope that it will be useful, 9# but WITHOUT ANY WARRANTY; without even the implied warranty of 10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11# GNU General Public License for more details. 12# 13# You should have received a copy of the GNU General Public License 14# along with this program. If not, see <https://www.gnu.org/licenses/>. 15 16from pathlib import Path 17 18from PySide6.QtWidgets import QWidget, QHBoxLayout, QLabel, QPushButton, QTabBar, QMenu 19from PySide6.QtCore import Signal 20 21from gui.utils.path_setup import get_courses, get_yamls 22from gui.theme import signal as theme_signal 23from lib.i18n import _ 24 25 26class CourseBar(QWidget): 27 course_changed = Signal(Path) 28 unit_changed = Signal(Path) 29 new_course_requested = Signal() 30 new_presentation_requested = Signal() 31 32 def __init__(self, parent=None): 33 super().__init__(parent) 34 self.setObjectName('course_bar') 35 self.setFixedHeight(36) 36 self._yamls: list[Path] = [] 37 self._courses: list[Path] = [] 38 self._current_course: Path | None = None 39 self._build_ui() 40 self._load_courses() 41 theme_signal().changed.connect(self._on_theme_changed) 42 43 def _build_ui(self): 44 lay = QHBoxLayout(self) 45 lay.setContentsMargins(8, 4, 8, 4) 46 lay.setSpacing(4) 47 48 lay.addWidget(self._lbl(_('Curso:'), 'course_label')) 49 50 self._course_btn = QPushButton('—') 51 self._course_btn.setObjectName('course_btn') 52 self._course_btn.clicked.connect(self._show_course_menu) 53 lay.addWidget(self._course_btn) 54 55 lay.addWidget(self._icon_btn('+', _('Nuevo curso'), self.new_course_requested.emit)) 56 lay.addWidget(self._icon_btn('⟳', _('Recargar cursos'), self._refresh_courses)) 57 58 lay.addWidget(self._lbl('│', 'sep_label')) 59 60 lay.addWidget(self._icon_btn('⟳', _('Recargar presentaciones'), self._refresh_units)) 61 62 self.unit_tabs = QTabBar() 63 self.unit_tabs.setObjectName('unit_tabs') 64 self.unit_tabs.setExpanding(False) 65 self.unit_tabs.currentChanged.connect(self._on_tab_changed) 66 lay.addWidget(self.unit_tabs) 67 68 lay.addWidget(self._icon_btn('+', _('Nueva presentación'), self.new_presentation_requested.emit)) 69 70 lay.addStretch() 71 72 # ── Helpers de layout ───────────────────────────────────────────────────── 73 74 @staticmethod 75 def _lbl(text: str, name: str) -> QLabel: 76 w = QLabel(text) 77 w.setObjectName(name) 78 return w 79 80 @staticmethod 81 def _icon_btn(text: str, tooltip: str, slot) -> QPushButton: 82 btn = QPushButton(text) 83 btn.setObjectName('bar_icon_btn') 84 btn.setToolTip(tooltip) 85 btn.setFixedSize(22, 22) 86 btn.clicked.connect(slot) 87 return btn 88 89 # ── Acciones internas ───────────────────────────────────────────────────── 90 91 def _on_theme_changed(self) -> None: 92 self.update() 93 94 def _show_course_menu(self): 95 menu = QMenu(self) 96 for course in self._courses: 97 act = menu.addAction(course.name) 98 act.setData(course) 99 if course == self._current_course: 100 act.setCheckable(True) 101 act.setChecked(True) 102 chosen = menu.exec(self._course_btn.mapToGlobal( 103 self._course_btn.rect().bottomLeft() 104 )) 105 if chosen and chosen.data(): 106 self._select_course(self._courses.index(chosen.data())) 107 108 def _refresh_courses(self): 109 old = self._current_course 110 self._courses = get_courses() 111 if not self._courses: 112 self._course_btn.setText('— ▾') 113 self._current_course = None 114 return 115 idx = self._courses.index(old) if old in self._courses else 0 116 self._select_course(idx) 117 118 def _refresh_units(self): 119 if self._current_course: 120 self._reload_tabs(self._current_course) 121 122 # ── Lógica de carga ─────────────────────────────────────────────────────── 123 124 def _load_courses(self): 125 self._courses = get_courses() 126 if self._courses: 127 self._select_course(0) 128 else: 129 self._course_btn.setText('— ▾') 130 131 def _select_course(self, idx: int): 132 course = self._courses[idx] 133 self._current_course = course 134 self._course_btn.setText(f'{course.name} ▾') 135 self.course_changed.emit(course) 136 self._reload_tabs(course) 137 138 def _reload_tabs(self, course_dir: Path): 139 self.unit_tabs.blockSignals(True) 140 while self.unit_tabs.count(): 141 self.unit_tabs.removeTab(0) 142 self._yamls = get_yamls(course_dir) 143 for y in self._yamls: 144 self.unit_tabs.addTab(y.name) 145 self.unit_tabs.blockSignals(False) 146 if self._yamls: 147 self.unit_tabs.setCurrentIndex(0) 148 self.unit_changed.emit(self._yamls[0]) 149 150 def _on_tab_changed(self, idx: int): 151 if 0 <= idx < len(self._yamls): 152 self.unit_changed.emit(self._yamls[idx]) 153 154 # ── API pública ─────────────────────────────────────────────────────────── 155 156 def current_course(self) -> Path | None: 157 return self._current_course 158 159 def current_yaml(self) -> Path | None: 160 idx = self.unit_tabs.currentIndex() 161 if 0 <= idx < len(self._yamls): 162 return self._yamls[idx] 163 return None
27class CourseBar(QWidget): 28 course_changed = Signal(Path) 29 unit_changed = Signal(Path) 30 new_course_requested = Signal() 31 new_presentation_requested = Signal() 32 33 def __init__(self, parent=None): 34 super().__init__(parent) 35 self.setObjectName('course_bar') 36 self.setFixedHeight(36) 37 self._yamls: list[Path] = [] 38 self._courses: list[Path] = [] 39 self._current_course: Path | None = None 40 self._build_ui() 41 self._load_courses() 42 theme_signal().changed.connect(self._on_theme_changed) 43 44 def _build_ui(self): 45 lay = QHBoxLayout(self) 46 lay.setContentsMargins(8, 4, 8, 4) 47 lay.setSpacing(4) 48 49 lay.addWidget(self._lbl(_('Curso:'), 'course_label')) 50 51 self._course_btn = QPushButton('—') 52 self._course_btn.setObjectName('course_btn') 53 self._course_btn.clicked.connect(self._show_course_menu) 54 lay.addWidget(self._course_btn) 55 56 lay.addWidget(self._icon_btn('+', _('Nuevo curso'), self.new_course_requested.emit)) 57 lay.addWidget(self._icon_btn('⟳', _('Recargar cursos'), self._refresh_courses)) 58 59 lay.addWidget(self._lbl('│', 'sep_label')) 60 61 lay.addWidget(self._icon_btn('⟳', _('Recargar presentaciones'), self._refresh_units)) 62 63 self.unit_tabs = QTabBar() 64 self.unit_tabs.setObjectName('unit_tabs') 65 self.unit_tabs.setExpanding(False) 66 self.unit_tabs.currentChanged.connect(self._on_tab_changed) 67 lay.addWidget(self.unit_tabs) 68 69 lay.addWidget(self._icon_btn('+', _('Nueva presentación'), self.new_presentation_requested.emit)) 70 71 lay.addStretch() 72 73 # ── Helpers de layout ───────────────────────────────────────────────────── 74 75 @staticmethod 76 def _lbl(text: str, name: str) -> QLabel: 77 w = QLabel(text) 78 w.setObjectName(name) 79 return w 80 81 @staticmethod 82 def _icon_btn(text: str, tooltip: str, slot) -> QPushButton: 83 btn = QPushButton(text) 84 btn.setObjectName('bar_icon_btn') 85 btn.setToolTip(tooltip) 86 btn.setFixedSize(22, 22) 87 btn.clicked.connect(slot) 88 return btn 89 90 # ── Acciones internas ───────────────────────────────────────────────────── 91 92 def _on_theme_changed(self) -> None: 93 self.update() 94 95 def _show_course_menu(self): 96 menu = QMenu(self) 97 for course in self._courses: 98 act = menu.addAction(course.name) 99 act.setData(course) 100 if course == self._current_course: 101 act.setCheckable(True) 102 act.setChecked(True) 103 chosen = menu.exec(self._course_btn.mapToGlobal( 104 self._course_btn.rect().bottomLeft() 105 )) 106 if chosen and chosen.data(): 107 self._select_course(self._courses.index(chosen.data())) 108 109 def _refresh_courses(self): 110 old = self._current_course 111 self._courses = get_courses() 112 if not self._courses: 113 self._course_btn.setText('— ▾') 114 self._current_course = None 115 return 116 idx = self._courses.index(old) if old in self._courses else 0 117 self._select_course(idx) 118 119 def _refresh_units(self): 120 if self._current_course: 121 self._reload_tabs(self._current_course) 122 123 # ── Lógica de carga ─────────────────────────────────────────────────────── 124 125 def _load_courses(self): 126 self._courses = get_courses() 127 if self._courses: 128 self._select_course(0) 129 else: 130 self._course_btn.setText('— ▾') 131 132 def _select_course(self, idx: int): 133 course = self._courses[idx] 134 self._current_course = course 135 self._course_btn.setText(f'{course.name} ▾') 136 self.course_changed.emit(course) 137 self._reload_tabs(course) 138 139 def _reload_tabs(self, course_dir: Path): 140 self.unit_tabs.blockSignals(True) 141 while self.unit_tabs.count(): 142 self.unit_tabs.removeTab(0) 143 self._yamls = get_yamls(course_dir) 144 for y in self._yamls: 145 self.unit_tabs.addTab(y.name) 146 self.unit_tabs.blockSignals(False) 147 if self._yamls: 148 self.unit_tabs.setCurrentIndex(0) 149 self.unit_changed.emit(self._yamls[0]) 150 151 def _on_tab_changed(self, idx: int): 152 if 0 <= idx < len(self._yamls): 153 self.unit_changed.emit(self._yamls[idx]) 154 155 # ── API pública ─────────────────────────────────────────────────────────── 156 157 def current_course(self) -> Path | None: 158 return self._current_course 159 160 def current_yaml(self) -> Path | None: 161 idx = self.unit_tabs.currentIndex() 162 if 0 <= idx < len(self._yamls): 163 return self._yamls[idx] 164 return None
QWidget(self, /, parent: PySide6.QtWidgets.QWidget | None = None, f: PySide6.QtCore.Qt.WindowType = Default(Qt.WindowFlags), *, modal: bool | None = None, windowModality: PySide6.QtCore.Qt.WindowModality | None = None, enabled: bool | None = None, geometry: PySide6.QtCore.QRect | None = None, frameGeometry: PySide6.QtCore.QRect | None = None, normalGeometry: PySide6.QtCore.QRect | None = None, x: int | None = None, y: int | None = None, pos: PySide6.QtCore.QPoint | None = None, frameSize: PySide6.QtCore.QSize | None = None, size: PySide6.QtCore.QSize | None = None, width: int | None = None, height: int | None = None, rect: PySide6.QtCore.QRect | None = None, childrenRect: PySide6.QtCore.QRect | None = None, childrenRegion: PySide6.QtGui.QRegion | None = None, sizePolicy: PySide6.QtWidgets.QSizePolicy | None = None, minimumSize: PySide6.QtCore.QSize | None = None, maximumSize: PySide6.QtCore.QSize | None = None, minimumWidth: int | None = None, minimumHeight: int | None = None, maximumWidth: int | None = None, maximumHeight: int | None = None, sizeIncrement: PySide6.QtCore.QSize | None = None, baseSize: PySide6.QtCore.QSize | None = None, palette: PySide6.QtGui.QPalette | None = None, font: PySide6.QtGui.QFont | None = None, cursor: PySide6.QtGui.QCursor | None = None, mouseTracking: bool | None = None, tabletTracking: bool | None = None, isActiveWindow: bool | None = None, focusPolicy: PySide6.QtCore.Qt.FocusPolicy | None = None, focus: bool | None = None, contextMenuPolicy: PySide6.QtCore.Qt.ContextMenuPolicy | None = None, updatesEnabled: bool | None = None, visible: bool | None = None, minimized: bool | None = None, maximized: bool | None = None, fullScreen: bool | None = None, sizeHint: PySide6.QtCore.QSize | None = None, minimumSizeHint: PySide6.QtCore.QSize | None = None, acceptDrops: bool | None = None, windowTitle: str | None = None, windowIcon: PySide6.QtGui.QIcon | None = None, windowIconText: str | None = None, windowOpacity: float | None = None, windowModified: bool | None = None, toolTip: str | None = None, toolTipDuration: int | None = None, statusTip: str | None = None, whatsThis: str | None = None, accessibleName: str | None = None, accessibleDescription: str | None = None, accessibleIdentifier: str | None = None, layoutDirection: PySide6.QtCore.Qt.LayoutDirection | None = None, autoFillBackground: bool | None = None, styleSheet: str | None = None, locale: PySide6.QtCore.QLocale | None = None, windowFilePath: str | None = None, inputMethodHints: PySide6.QtCore.Qt.InputMethodHint | None = None) -> None
33 def __init__(self, parent=None): 34 super().__init__(parent) 35 self.setObjectName('course_bar') 36 self.setFixedHeight(36) 37 self._yamls: list[Path] = [] 38 self._courses: list[Path] = [] 39 self._current_course: Path | None = None 40 self._build_ui() 41 self._load_courses() 42 theme_signal().changed.connect(self._on_theme_changed)