gui.panels.slide_navigator

  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 PySide6.QtWidgets import QTreeWidget, QTreeWidgetItem
 17from PySide6.QtCore import Signal, Qt
 18from PySide6.QtGui import QColor, QBrush, QFont
 19
 20from gui.theme import c, signal as theme_signal
 21
 22
 23_TIPO_COLOR: dict[str, str] = {
 24    **{t: '#60a5fa' for t in (
 25        'java', 'python', 'javascript', 'html', 'xml',
 26        'jsp', 'css', 'json', 'yaml', 'codigo',
 27    )},
 28    'texto':        '#fb7185',
 29    'bullets':      '#818cf8',
 30    'dos_columnas': '#2dd4bf',
 31    'cita':         '#f9a8d4',
 32    **{t: '#4ade80' for t in (
 33        'comparar', 'comparar_python', 'comparar_js',
 34        'comparar_xml', 'comparar_html', 'comparar_json', 'comparar_css', 'comparar_jsp',
 35    )},
 36    'tabla':        '#fb923c',
 37    'aviso':        '#fbbf24',
 38    **{t: '#c084fc' for t in ('terminal', 'salida', 'code')},
 39    'diagrama':     '#22d3ee',
 40    **{t: '#f472b6' for t in ('imagen', 'imagen_texto')},
 41}
 42
 43# Valores especiales de section_idx para los nodos fijos
 44NODE_PORTADA    = -10
 45NODE_OBJETIVOS  = -9
 46NODE_CONCLUSION = -8
 47NODE_REFERENCIAS = -7
 48
 49
 50def _slide_label(slide: dict, n: int) -> str:
 51    tipo = slide.get('tipo', '?')
 52    if tipo == 'aviso':
 53        nivel = slide.get('nivel', '')
 54        return f'aviso: {nivel}  ({n})'
 55    return f'{tipo}  ({n})'
 56
 57
 58class SlideNavigator(QTreeWidget):
 59    """Árbol semántico del YAML activo.
 60
 61    Emite slide_selected(section_idx, slide_idx) al hacer click:
 62      - section_idx >= 0, slide_idx >= 0   → slide concreto (0 = primer slide de la sección)
 63      - section_idx == NODE_*              → nodo fijo (portada, objetivos…)
 64    """
 65    slide_selected = Signal(int, int)
 66
 67    def __init__(self, parent=None):
 68        super().__init__(parent)
 69        self.setHeaderHidden(True)
 70        self.setAnimated(True)
 71        self.setIndentation(16)
 72        self.setMinimumWidth(160)
 73        self.itemClicked.connect(self._on_item_clicked)
 74        theme_signal().changed.connect(self._on_theme_changed)
 75        self._last_data: dict = {}
 76
 77    # ── API pública ───────────────────────────────────────────────────────────
 78
 79    def _on_theme_changed(self) -> None:
 80        if self._last_data:
 81            self.rebuild(self._last_data)
 82
 83    def rebuild(self, data: dict) -> None:
 84        self._last_data = data
 85        """Reconstruye el árbol desde el dict YAML parseado."""
 86        self.clear()
 87
 88        self._add_fixed('Portada',   NODE_PORTADA,    0)
 89        self._add_fixed('Objetivos', NODE_OBJETIVOS,  0)
 90
 91        indice_list = data.get('indice', [])
 92
 93        for sec_idx, sec in enumerate(data.get('secciones', [])):
 94            if 'titulo' in sec:
 95                titulo = sec['titulo']
 96            elif 'indice' in sec and indice_list:
 97                idx = sec['indice'] - 1
 98                titulo = indice_list[idx] if 0 <= idx < len(indice_list) else f'Sección {sec_idx + 1}'
 99            else:
100                titulo = f'Sección {sec_idx + 1}'
101
102            sec_item = QTreeWidgetItem(self)
103            sec_item.setText(0, f'Sec {sec_idx + 1}: {titulo}')
104            sec_item.setData(0, Qt.ItemDataRole.UserRole, (sec_idx, 0))
105            sec_item.setForeground(0, QBrush(QColor(c('text'))))
106            bold = QFont()
107            bold.setBold(True)
108            sec_item.setFont(0, bold)
109
110            for slide_idx, slide in enumerate(sec.get('slides', [])):
111                tipo = slide.get('tipo', '?')
112                s_item = QTreeWidgetItem(sec_item)
113                s_item.setText(0, f'  {_slide_label(slide, slide_idx + 1)}')
114                s_item.setData(0, Qt.ItemDataRole.UserRole, (sec_idx, slide_idx))
115                s_item.setForeground(0, QBrush(QColor(_TIPO_COLOR.get(tipo, '#94a3b8'))))
116
117            sec_item.setExpanded(True)
118
119        self._add_fixed('Conclusión',  NODE_CONCLUSION,  0)
120        self._add_fixed('Referencias', NODE_REFERENCIAS, 0)
121
122    # ── Slots privados ────────────────────────────────────────────────────────
123
124    def _add_fixed(self, label: str, sec_idx: int, slide_idx: int) -> QTreeWidgetItem:
125        item = QTreeWidgetItem(self)
126        item.setText(0, label)
127        item.setData(0, Qt.ItemDataRole.UserRole, (sec_idx, slide_idx))
128        item.setForeground(0, QBrush(QColor(c('text_dim'))))
129        return item
130
131    def _on_item_clicked(self, item: QTreeWidgetItem, _column: int) -> None:
132        data = item.data(0, Qt.ItemDataRole.UserRole)
133        if data is not None:
134            self.slide_selected.emit(data[0], data[1])
NODE_PORTADA = -10
NODE_OBJETIVOS = -9
NODE_CONCLUSION = -8
NODE_REFERENCIAS = -7
class SlideNavigator(PySide6.QtWidgets.QTreeWidget):
 59class SlideNavigator(QTreeWidget):
 60    """Árbol semántico del YAML activo.
 61
 62    Emite slide_selected(section_idx, slide_idx) al hacer click:
 63      - section_idx >= 0, slide_idx >= 0   → slide concreto (0 = primer slide de la sección)
 64      - section_idx == NODE_*              → nodo fijo (portada, objetivos…)
 65    """
 66    slide_selected = Signal(int, int)
 67
 68    def __init__(self, parent=None):
 69        super().__init__(parent)
 70        self.setHeaderHidden(True)
 71        self.setAnimated(True)
 72        self.setIndentation(16)
 73        self.setMinimumWidth(160)
 74        self.itemClicked.connect(self._on_item_clicked)
 75        theme_signal().changed.connect(self._on_theme_changed)
 76        self._last_data: dict = {}
 77
 78    # ── API pública ───────────────────────────────────────────────────────────
 79
 80    def _on_theme_changed(self) -> None:
 81        if self._last_data:
 82            self.rebuild(self._last_data)
 83
 84    def rebuild(self, data: dict) -> None:
 85        self._last_data = data
 86        """Reconstruye el árbol desde el dict YAML parseado."""
 87        self.clear()
 88
 89        self._add_fixed('Portada',   NODE_PORTADA,    0)
 90        self._add_fixed('Objetivos', NODE_OBJETIVOS,  0)
 91
 92        indice_list = data.get('indice', [])
 93
 94        for sec_idx, sec in enumerate(data.get('secciones', [])):
 95            if 'titulo' in sec:
 96                titulo = sec['titulo']
 97            elif 'indice' in sec and indice_list:
 98                idx = sec['indice'] - 1
 99                titulo = indice_list[idx] if 0 <= idx < len(indice_list) else f'Sección {sec_idx + 1}'
100            else:
101                titulo = f'Sección {sec_idx + 1}'
102
103            sec_item = QTreeWidgetItem(self)
104            sec_item.setText(0, f'Sec {sec_idx + 1}: {titulo}')
105            sec_item.setData(0, Qt.ItemDataRole.UserRole, (sec_idx, 0))
106            sec_item.setForeground(0, QBrush(QColor(c('text'))))
107            bold = QFont()
108            bold.setBold(True)
109            sec_item.setFont(0, bold)
110
111            for slide_idx, slide in enumerate(sec.get('slides', [])):
112                tipo = slide.get('tipo', '?')
113                s_item = QTreeWidgetItem(sec_item)
114                s_item.setText(0, f'  {_slide_label(slide, slide_idx + 1)}')
115                s_item.setData(0, Qt.ItemDataRole.UserRole, (sec_idx, slide_idx))
116                s_item.setForeground(0, QBrush(QColor(_TIPO_COLOR.get(tipo, '#94a3b8'))))
117
118            sec_item.setExpanded(True)
119
120        self._add_fixed('Conclusión',  NODE_CONCLUSION,  0)
121        self._add_fixed('Referencias', NODE_REFERENCIAS, 0)
122
123    # ── Slots privados ────────────────────────────────────────────────────────
124
125    def _add_fixed(self, label: str, sec_idx: int, slide_idx: int) -> QTreeWidgetItem:
126        item = QTreeWidgetItem(self)
127        item.setText(0, label)
128        item.setData(0, Qt.ItemDataRole.UserRole, (sec_idx, slide_idx))
129        item.setForeground(0, QBrush(QColor(c('text_dim'))))
130        return item
131
132    def _on_item_clicked(self, item: QTreeWidgetItem, _column: int) -> None:
133        data = item.data(0, Qt.ItemDataRole.UserRole)
134        if data is not None:
135            self.slide_selected.emit(data[0], data[1])

Árbol semántico del YAML activo.

Emite slide_selected(section_idx, slide_idx) al hacer click:

  • section_idx >= 0, slide_idx >= 0 → slide concreto (0 = primer slide de la sección)
  • section_idx == NODE_* → nodo fijo (portada, objetivos…)
SlideNavigator(parent=None)
68    def __init__(self, parent=None):
69        super().__init__(parent)
70        self.setHeaderHidden(True)
71        self.setAnimated(True)
72        self.setIndentation(16)
73        self.setMinimumWidth(160)
74        self.itemClicked.connect(self._on_item_clicked)
75        theme_signal().changed.connect(self._on_theme_changed)
76        self._last_data: dict = {}
def slide_selected(unknown):
def rebuild(self, data: dict) -> None:
 84    def rebuild(self, data: dict) -> None:
 85        self._last_data = data
 86        """Reconstruye el árbol desde el dict YAML parseado."""
 87        self.clear()
 88
 89        self._add_fixed('Portada',   NODE_PORTADA,    0)
 90        self._add_fixed('Objetivos', NODE_OBJETIVOS,  0)
 91
 92        indice_list = data.get('indice', [])
 93
 94        for sec_idx, sec in enumerate(data.get('secciones', [])):
 95            if 'titulo' in sec:
 96                titulo = sec['titulo']
 97            elif 'indice' in sec and indice_list:
 98                idx = sec['indice'] - 1
 99                titulo = indice_list[idx] if 0 <= idx < len(indice_list) else f'Sección {sec_idx + 1}'
100            else:
101                titulo = f'Sección {sec_idx + 1}'
102
103            sec_item = QTreeWidgetItem(self)
104            sec_item.setText(0, f'Sec {sec_idx + 1}: {titulo}')
105            sec_item.setData(0, Qt.ItemDataRole.UserRole, (sec_idx, 0))
106            sec_item.setForeground(0, QBrush(QColor(c('text'))))
107            bold = QFont()
108            bold.setBold(True)
109            sec_item.setFont(0, bold)
110
111            for slide_idx, slide in enumerate(sec.get('slides', [])):
112                tipo = slide.get('tipo', '?')
113                s_item = QTreeWidgetItem(sec_item)
114                s_item.setText(0, f'  {_slide_label(slide, slide_idx + 1)}')
115                s_item.setData(0, Qt.ItemDataRole.UserRole, (sec_idx, slide_idx))
116                s_item.setForeground(0, QBrush(QColor(_TIPO_COLOR.get(tipo, '#94a3b8'))))
117
118            sec_item.setExpanded(True)
119
120        self._add_fixed('Conclusión',  NODE_CONCLUSION,  0)
121        self._add_fixed('Referencias', NODE_REFERENCIAS, 0)
staticMetaObject = PySide6.QtCore.QMetaObject("SlideNavigator" inherits "QTreeWidget": Methods: #103 type=Signal, signature=slide_selected(int,int), parameters=int, int )