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
class CourseBar(PySide6.QtWidgets.QWidget):
 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

CourseBar(parent=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)
def course_changed(unknown):
def unit_changed(unknown):
def new_course_requested(unknown):
def new_presentation_requested(unknown):
def current_course(self) -> pathlib.Path | None:
157    def current_course(self) -> Path | None:
158        return self._current_course
def current_yaml(self) -> pathlib.Path | None:
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
staticMetaObject = PySide6.QtCore.QMetaObject("CourseBar" inherits "QWidget": Methods: #33 type=Signal, signature=course_changed(PyObject), parameters=PyObject #34 type=Signal, signature=unit_changed(PyObject), parameters=PyObject #35 type=Signal, signature=new_course_requested() #36 type=Signal, signature=new_presentation_requested() )