gui.dialogs.new_presentation_dialog

  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
 16import re
 17from pathlib import Path
 18
 19from PySide6.QtWidgets import (
 20    QDialog, QVBoxLayout, QFormLayout, QHBoxLayout,
 21    QLabel, QLineEdit, QDialogButtonBox,
 22)
 23from PySide6.QtCore import Qt
 24
 25from gui.theme import c
 26from lib.i18n import _
 27
 28
 29class NewPresentationDialog(QDialog):
 30    """Diálogo para crear un nuevo fichero YAML de presentación en el curso activo."""
 31
 32    def __init__(self, course_dir: Path, parent=None):
 33        super().__init__(parent)
 34        self.setWindowTitle(_('Nueva presentación'))
 35        self.setMinimumWidth(440)
 36        self._course_dir = course_dir
 37        self._build_ui()
 38        self._suggest_defaults()
 39
 40    # ── Construcción ─────────────────────────────────────────────────────────
 41
 42    def _build_ui(self) -> None:
 43        lay = QVBoxLayout(self)
 44        lay.setContentsMargins(20, 16, 20, 16)
 45        lay.setSpacing(12)
 46
 47        form = QFormLayout()
 48        form.setSpacing(10)
 49
 50        self._portada = QLineEdit()
 51        self._portada.setPlaceholderText(_('Título visible en la portada'))
 52        form.addRow(_('Título:'), self._portada)
 53
 54        self._unidad = QLineEdit()
 55        self._unidad.setPlaceholderText('Unidad 01')
 56        form.addRow(_('Unidad:'), self._unidad)
 57
 58        output_row = QHBoxLayout()
 59        self._output = QLineEdit()
 60        self._output.setPlaceholderText('01_introduccion')
 61        output_row.addWidget(self._output)
 62        suffix = QLabel('.yaml')
 63        suffix.setObjectName('hint')
 64        output_row.addWidget(suffix)
 65        hint = QLabel(_('La extensión .yaml se añade automáticamente'))
 66        hint.setObjectName('hint')
 67        form.addRow(_('Output:'), output_row)
 68        form.addRow('', hint)
 69
 70        lay.addLayout(form)
 71
 72        self._preview = QLabel()
 73        self._preview.setObjectName('hint')
 74        self._preview.setAlignment(Qt.AlignmentFlag.AlignCenter)
 75        lay.addWidget(self._preview)
 76
 77        bbox = QDialogButtonBox(
 78            QDialogButtonBox.StandardButton.Ok |
 79            QDialogButtonBox.StandardButton.Cancel
 80        )
 81        bbox.accepted.connect(self._validate_and_accept)
 82        bbox.rejected.connect(self.reject)
 83        lay.addWidget(bbox)
 84
 85        self._portada.textChanged.connect(self._update_preview)
 86        self._output.textChanged.connect(self._update_preview)
 87
 88    # ── Lógica ────────────────────────────────────────────────────────────────
 89
 90    def _suggest_defaults(self) -> None:
 91        """Sugiere número de unidad y prefijo basándose en los YAMLs existentes."""
 92        existing = sorted((self._course_dir / 'content').glob('*.yaml'))
 93        n = len(existing)
 94        num = f'{n:02d}'
 95        self._unidad.setText(f'Unidad {num}')
 96        self._output.setText(f'{num}_nueva_presentacion')
 97        self._update_preview()
 98
 99    def _update_preview(self) -> None:
100        output = self._output.text().strip()
101        course = self._course_dir.name
102        if output:
103            filename = f'{course}_{output}.yaml'
104            self._preview.setText(f'→ content/{filename}')
105        else:
106            self._preview.setText('')
107
108    def _validate_and_accept(self) -> None:
109        portada = self._portada.text().strip()
110        unidad  = self._unidad.text().strip()
111        output  = self._output.text().strip()
112
113        if not portada:
114            self._portada.setFocus()
115            return
116        if not unidad:
117            self._unidad.setFocus()
118            return
119        if not output:
120            self._output.setFocus()
121            return
122
123        # Limpiar output: sin espacios ni caracteres problemáticos
124        output_clean = re.sub(r'[^a-zA-Z0-9_\-]', '_', output).strip('_')
125        if not output_clean:
126            self._output.setFocus()
127            return
128        self._output.setText(output_clean)
129
130        dest = self._course_dir / 'content' / f'{self._course_dir.name}_{output_clean}.yaml'
131        if dest.exists():
132            self._preview.setText(
133                f'⚠ {dest.name} ya existe'
134            )
135            return
136
137        self.accept()
138
139    # ── API pública ────────────────────────────────────────────────────────────
140
141    def create_yaml(self) -> Path:
142        """Crea el fichero YAML y devuelve su ruta. Llamar solo tras accept()."""
143        course    = self._course_dir.name
144        portada   = self._portada.text().strip()
145        unidad    = self._unidad.text().strip()
146        output    = self._output.text().strip()
147        dest      = self._course_dir / 'content' / f'{course}_{output}.yaml'
148
149        dest.parent.mkdir(parents=True, exist_ok=True)
150        dest.write_text(
151            f'meta:\n'
152            f'  portada: "{portada}"\n'
153            f'  unidad:  "{unidad}"\n'
154            f'  output:  "{output}"\n'
155            f'\n'
156            f'objetivos:\n'
157            f'  - "Objetivo 1"\n'
158            f'\n'
159            f'indice:\n'
160            f'  - "Sección 1"\n'
161            f'\n'
162            f'secciones:\n'
163            f'  - indice: 1\n'
164            f'    slides:\n'
165            f'      - tipo: texto\n'
166            f'        texto: "Texto introductorio."\n'
167            f'\n'
168            f'conclusion:\n'
169            f'  - "Conclusión principal"\n'
170            f'\n'
171            f'referencias:\n'
172            f'  - ["Recurso", "https://ejemplo.com"]\n',
173            encoding='utf-8',
174        )
175        return dest
class NewPresentationDialog(PySide6.QtWidgets.QDialog):
 30class NewPresentationDialog(QDialog):
 31    """Diálogo para crear un nuevo fichero YAML de presentación en el curso activo."""
 32
 33    def __init__(self, course_dir: Path, parent=None):
 34        super().__init__(parent)
 35        self.setWindowTitle(_('Nueva presentación'))
 36        self.setMinimumWidth(440)
 37        self._course_dir = course_dir
 38        self._build_ui()
 39        self._suggest_defaults()
 40
 41    # ── Construcción ─────────────────────────────────────────────────────────
 42
 43    def _build_ui(self) -> None:
 44        lay = QVBoxLayout(self)
 45        lay.setContentsMargins(20, 16, 20, 16)
 46        lay.setSpacing(12)
 47
 48        form = QFormLayout()
 49        form.setSpacing(10)
 50
 51        self._portada = QLineEdit()
 52        self._portada.setPlaceholderText(_('Título visible en la portada'))
 53        form.addRow(_('Título:'), self._portada)
 54
 55        self._unidad = QLineEdit()
 56        self._unidad.setPlaceholderText('Unidad 01')
 57        form.addRow(_('Unidad:'), self._unidad)
 58
 59        output_row = QHBoxLayout()
 60        self._output = QLineEdit()
 61        self._output.setPlaceholderText('01_introduccion')
 62        output_row.addWidget(self._output)
 63        suffix = QLabel('.yaml')
 64        suffix.setObjectName('hint')
 65        output_row.addWidget(suffix)
 66        hint = QLabel(_('La extensión .yaml se añade automáticamente'))
 67        hint.setObjectName('hint')
 68        form.addRow(_('Output:'), output_row)
 69        form.addRow('', hint)
 70
 71        lay.addLayout(form)
 72
 73        self._preview = QLabel()
 74        self._preview.setObjectName('hint')
 75        self._preview.setAlignment(Qt.AlignmentFlag.AlignCenter)
 76        lay.addWidget(self._preview)
 77
 78        bbox = QDialogButtonBox(
 79            QDialogButtonBox.StandardButton.Ok |
 80            QDialogButtonBox.StandardButton.Cancel
 81        )
 82        bbox.accepted.connect(self._validate_and_accept)
 83        bbox.rejected.connect(self.reject)
 84        lay.addWidget(bbox)
 85
 86        self._portada.textChanged.connect(self._update_preview)
 87        self._output.textChanged.connect(self._update_preview)
 88
 89    # ── Lógica ────────────────────────────────────────────────────────────────
 90
 91    def _suggest_defaults(self) -> None:
 92        """Sugiere número de unidad y prefijo basándose en los YAMLs existentes."""
 93        existing = sorted((self._course_dir / 'content').glob('*.yaml'))
 94        n = len(existing)
 95        num = f'{n:02d}'
 96        self._unidad.setText(f'Unidad {num}')
 97        self._output.setText(f'{num}_nueva_presentacion')
 98        self._update_preview()
 99
100    def _update_preview(self) -> None:
101        output = self._output.text().strip()
102        course = self._course_dir.name
103        if output:
104            filename = f'{course}_{output}.yaml'
105            self._preview.setText(f'→ content/{filename}')
106        else:
107            self._preview.setText('')
108
109    def _validate_and_accept(self) -> None:
110        portada = self._portada.text().strip()
111        unidad  = self._unidad.text().strip()
112        output  = self._output.text().strip()
113
114        if not portada:
115            self._portada.setFocus()
116            return
117        if not unidad:
118            self._unidad.setFocus()
119            return
120        if not output:
121            self._output.setFocus()
122            return
123
124        # Limpiar output: sin espacios ni caracteres problemáticos
125        output_clean = re.sub(r'[^a-zA-Z0-9_\-]', '_', output).strip('_')
126        if not output_clean:
127            self._output.setFocus()
128            return
129        self._output.setText(output_clean)
130
131        dest = self._course_dir / 'content' / f'{self._course_dir.name}_{output_clean}.yaml'
132        if dest.exists():
133            self._preview.setText(
134                f'⚠ {dest.name} ya existe'
135            )
136            return
137
138        self.accept()
139
140    # ── API pública ────────────────────────────────────────────────────────────
141
142    def create_yaml(self) -> Path:
143        """Crea el fichero YAML y devuelve su ruta. Llamar solo tras accept()."""
144        course    = self._course_dir.name
145        portada   = self._portada.text().strip()
146        unidad    = self._unidad.text().strip()
147        output    = self._output.text().strip()
148        dest      = self._course_dir / 'content' / f'{course}_{output}.yaml'
149
150        dest.parent.mkdir(parents=True, exist_ok=True)
151        dest.write_text(
152            f'meta:\n'
153            f'  portada: "{portada}"\n'
154            f'  unidad:  "{unidad}"\n'
155            f'  output:  "{output}"\n'
156            f'\n'
157            f'objetivos:\n'
158            f'  - "Objetivo 1"\n'
159            f'\n'
160            f'indice:\n'
161            f'  - "Sección 1"\n'
162            f'\n'
163            f'secciones:\n'
164            f'  - indice: 1\n'
165            f'    slides:\n'
166            f'      - tipo: texto\n'
167            f'        texto: "Texto introductorio."\n'
168            f'\n'
169            f'conclusion:\n'
170            f'  - "Conclusión principal"\n'
171            f'\n'
172            f'referencias:\n'
173            f'  - ["Recurso", "https://ejemplo.com"]\n',
174            encoding='utf-8',
175        )
176        return dest

Diálogo para crear un nuevo fichero YAML de presentación en el curso activo.

NewPresentationDialog(course_dir: pathlib.Path, parent=None)
33    def __init__(self, course_dir: Path, parent=None):
34        super().__init__(parent)
35        self.setWindowTitle(_('Nueva presentación'))
36        self.setMinimumWidth(440)
37        self._course_dir = course_dir
38        self._build_ui()
39        self._suggest_defaults()
def create_yaml(self) -> pathlib.Path:
142    def create_yaml(self) -> Path:
143        """Crea el fichero YAML y devuelve su ruta. Llamar solo tras accept()."""
144        course    = self._course_dir.name
145        portada   = self._portada.text().strip()
146        unidad    = self._unidad.text().strip()
147        output    = self._output.text().strip()
148        dest      = self._course_dir / 'content' / f'{course}_{output}.yaml'
149
150        dest.parent.mkdir(parents=True, exist_ok=True)
151        dest.write_text(
152            f'meta:\n'
153            f'  portada: "{portada}"\n'
154            f'  unidad:  "{unidad}"\n'
155            f'  output:  "{output}"\n'
156            f'\n'
157            f'objetivos:\n'
158            f'  - "Objetivo 1"\n'
159            f'\n'
160            f'indice:\n'
161            f'  - "Sección 1"\n'
162            f'\n'
163            f'secciones:\n'
164            f'  - indice: 1\n'
165            f'    slides:\n'
166            f'      - tipo: texto\n'
167            f'        texto: "Texto introductorio."\n'
168            f'\n'
169            f'conclusion:\n'
170            f'  - "Conclusión principal"\n'
171            f'\n'
172            f'referencias:\n'
173            f'  - ["Recurso", "https://ejemplo.com"]\n',
174            encoding='utf-8',
175        )
176        return dest

Crea el fichero YAML y devuelve su ruta. Llamar solo tras accept().

staticMetaObject = PySide6.QtCore.QMetaObject("NewPresentationDialog" inherits "QDialog": )