gui.dialogs.onboarding_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
 16from pathlib import Path
 17
 18from PySide6.QtWidgets import (
 19    QDialog, QVBoxLayout, QHBoxLayout, QLabel,
 20    QPushButton, QLineEdit, QFileDialog, QDialogButtonBox,
 21)
 22from PySide6.QtCore import Qt
 23from PySide6.QtGui import QFont
 24
 25from lib.slides_config import save_courses_dir, DEFAULT_COURSES_DIR
 26from gui.theme import c
 27from lib.i18n import _
 28
 29
 30def _build_style() -> str:
 31    return f"""
 32QDialog {{ background: {c('bg')}; color: {c('text')}; }}
 33QLabel  {{ color: {c('text')}; }}
 34QLabel#subtitle {{ color: {c('text_muted')}; font-size: 12px; }}
 35QLineEdit {{
 36    background: {c('bg_deep')}; color: {c('text')};
 37    border: 1px solid {c('border')}; border-radius: 4px;
 38    padding: 4px 8px; font-size: 12px;
 39}}
 40QPushButton#browse {{
 41    background: {c('bg_selected')}; color: {c('text')};
 42    border: none; border-radius: 4px; padding: 5px 14px; font-size: 12px;
 43}}
 44QPushButton#browse:hover {{ background: {c('bg_hover')}; }}
 45QDialogButtonBox QPushButton {{
 46    background: {c('accent')}; color: #ffffff;
 47    border: none; border-radius: 4px;
 48    padding: 6px 20px; font-size: 12px; min-width: 80px;
 49}}
 50QDialogButtonBox QPushButton:hover {{ background: {c('accent_hover')}; }}
 51QDialogButtonBox QPushButton:disabled {{ background: {c('bg_selected')}; color: {c('text_dim')}; }}
 52QDialogButtonBox QPushButton[text="Omitir"],
 53QDialogButtonBox QPushButton[text="Skip"] {{
 54    background: {c('bg_surface')}; color: {c('text_muted')};
 55}}
 56QDialogButtonBox QPushButton[text="Omitir"]:hover,
 57QDialogButtonBox QPushButton[text="Skip"]:hover {{ background: {c('bg_hover')}; }}
 58"""
 59
 60
 61class OnboardingDialog(QDialog):
 62    """Diálogo de primer arranque: permite seleccionar el directorio de cursos."""
 63
 64    def __init__(self, parent=None):
 65        super().__init__(parent)
 66        self.setWindowTitle(_('Configuración inicial'))
 67        self.setMinimumWidth(540)
 68        self.setStyleSheet(_build_style())
 69        self._selected: Path | None = None
 70        self._build_ui()
 71
 72    # ── Construcción ─────────────────────────────────────────────────────────
 73
 74    def _build_ui(self) -> None:
 75        lay = QVBoxLayout(self)
 76        lay.setContentsMargins(28, 24, 28, 20)
 77        lay.setSpacing(16)
 78
 79        title = QLabel(_('Bienvenido a Temario'))
 80        f = QFont()
 81        f.setPointSize(16)
 82        f.setBold(True)
 83        title.setFont(f)
 84        lay.addWidget(title)
 85
 86        sub = QLabel(_(
 87            'Indica dónde están tus cursos. Si no tienes ninguno todavía,\n'
 88            'selecciona o crea una carpeta vacía; después podrás usar\n'
 89            '"Nuevo curso" para añadir el primero.'
 90        ))
 91        sub.setObjectName('subtitle')
 92        sub.setWordWrap(True)
 93        lay.addWidget(sub)
 94
 95        # ── selector de directorio ────────────────────────────────────────
 96        dir_row = QHBoxLayout()
 97        dir_row.setSpacing(6)
 98
 99        self._dir_edit = QLineEdit()
100        self._dir_edit.setPlaceholderText(str(DEFAULT_COURSES_DIR))
101        self._dir_edit.setText(str(DEFAULT_COURSES_DIR))
102        self._dir_edit.textChanged.connect(self._on_text_changed)
103        dir_row.addWidget(self._dir_edit)
104
105        btn_browse = QPushButton(_('Explorar…'))
106        btn_browse.setObjectName('browse')
107        btn_browse.clicked.connect(self._browse)
108        dir_row.addWidget(btn_browse)
109
110        lay.addLayout(dir_row)
111
112        self._hint = QLabel('')
113        self._hint.setObjectName('subtitle')
114        lay.addWidget(self._hint)
115
116        lay.addSpacing(8)
117
118        # ── botones ───────────────────────────────────────────────────────
119        bbox = QDialogButtonBox()
120        self._btn_ok = bbox.addButton(_('Confirmar'), QDialogButtonBox.ButtonRole.AcceptRole)
121        btn_skip = bbox.addButton(_('Omitir'), QDialogButtonBox.ButtonRole.RejectRole)
122        self._btn_ok.setEnabled(True)
123        bbox.accepted.connect(self._accept)
124        bbox.rejected.connect(self.reject)
125        lay.addWidget(bbox)
126
127    # ── Lógica ────────────────────────────────────────────────────────────────
128
129    def _browse(self) -> None:
130        start = self._dir_edit.text().strip() or str(Path.home())
131        chosen = QFileDialog.getExistingDirectory(
132            self, _('Seleccionar directorio de cursos'), start,
133            QFileDialog.Option.ShowDirsOnly,
134        )
135        if chosen:
136            self._dir_edit.setText(chosen)
137
138    def _on_text_changed(self, text: str) -> None:
139        path = Path(text.strip()) if text.strip() else None
140        self._btn_ok.setEnabled(bool(text.strip()))
141        if path and path.is_dir():
142            n = sum(1 for d in path.iterdir() if d.is_dir())
143            self._hint.setText(_('{} subcarpeta(s) encontrada(s) en ese directorio.').format(n))
144        elif path:
145            self._hint.setText(_('El directorio se creará al confirmar.'))
146        else:
147            self._hint.setText('')
148
149    def _accept(self) -> None:
150        import shutil
151        text = self._dir_edit.text().strip()
152        if not text:
153            return
154        path = Path(text)
155        if not path.exists():
156            path.mkdir(parents=True, exist_ok=True)
157        # copiar curso de ejemplo si el directorio estaba vacío
158        _root = Path(__file__).parent.parent.parent.parent   # src/gui/dialogs → project root
159        ejemplo_src = _root / 'courses' / 'ejemplo'
160        ejemplo_dst = path / 'ejemplo'
161        if ejemplo_src.exists() and not ejemplo_dst.exists():
162            shutil.copytree(ejemplo_src, ejemplo_dst,
163                            ignore=shutil.ignore_patterns('output'))
164        self._selected = path
165        save_courses_dir(path)
166        self.accept()
167
168    # ── API pública ────────────────────────────────────────────────────────────
169
170    def selected_dir(self) -> Path | None:
171        """Devuelve el directorio elegido, o None si se omitió."""
172        return self._selected
class OnboardingDialog(PySide6.QtWidgets.QDialog):
 62class OnboardingDialog(QDialog):
 63    """Diálogo de primer arranque: permite seleccionar el directorio de cursos."""
 64
 65    def __init__(self, parent=None):
 66        super().__init__(parent)
 67        self.setWindowTitle(_('Configuración inicial'))
 68        self.setMinimumWidth(540)
 69        self.setStyleSheet(_build_style())
 70        self._selected: Path | None = None
 71        self._build_ui()
 72
 73    # ── Construcción ─────────────────────────────────────────────────────────
 74
 75    def _build_ui(self) -> None:
 76        lay = QVBoxLayout(self)
 77        lay.setContentsMargins(28, 24, 28, 20)
 78        lay.setSpacing(16)
 79
 80        title = QLabel(_('Bienvenido a Temario'))
 81        f = QFont()
 82        f.setPointSize(16)
 83        f.setBold(True)
 84        title.setFont(f)
 85        lay.addWidget(title)
 86
 87        sub = QLabel(_(
 88            'Indica dónde están tus cursos. Si no tienes ninguno todavía,\n'
 89            'selecciona o crea una carpeta vacía; después podrás usar\n'
 90            '"Nuevo curso" para añadir el primero.'
 91        ))
 92        sub.setObjectName('subtitle')
 93        sub.setWordWrap(True)
 94        lay.addWidget(sub)
 95
 96        # ── selector de directorio ────────────────────────────────────────
 97        dir_row = QHBoxLayout()
 98        dir_row.setSpacing(6)
 99
100        self._dir_edit = QLineEdit()
101        self._dir_edit.setPlaceholderText(str(DEFAULT_COURSES_DIR))
102        self._dir_edit.setText(str(DEFAULT_COURSES_DIR))
103        self._dir_edit.textChanged.connect(self._on_text_changed)
104        dir_row.addWidget(self._dir_edit)
105
106        btn_browse = QPushButton(_('Explorar…'))
107        btn_browse.setObjectName('browse')
108        btn_browse.clicked.connect(self._browse)
109        dir_row.addWidget(btn_browse)
110
111        lay.addLayout(dir_row)
112
113        self._hint = QLabel('')
114        self._hint.setObjectName('subtitle')
115        lay.addWidget(self._hint)
116
117        lay.addSpacing(8)
118
119        # ── botones ───────────────────────────────────────────────────────
120        bbox = QDialogButtonBox()
121        self._btn_ok = bbox.addButton(_('Confirmar'), QDialogButtonBox.ButtonRole.AcceptRole)
122        btn_skip = bbox.addButton(_('Omitir'), QDialogButtonBox.ButtonRole.RejectRole)
123        self._btn_ok.setEnabled(True)
124        bbox.accepted.connect(self._accept)
125        bbox.rejected.connect(self.reject)
126        lay.addWidget(bbox)
127
128    # ── Lógica ────────────────────────────────────────────────────────────────
129
130    def _browse(self) -> None:
131        start = self._dir_edit.text().strip() or str(Path.home())
132        chosen = QFileDialog.getExistingDirectory(
133            self, _('Seleccionar directorio de cursos'), start,
134            QFileDialog.Option.ShowDirsOnly,
135        )
136        if chosen:
137            self._dir_edit.setText(chosen)
138
139    def _on_text_changed(self, text: str) -> None:
140        path = Path(text.strip()) if text.strip() else None
141        self._btn_ok.setEnabled(bool(text.strip()))
142        if path and path.is_dir():
143            n = sum(1 for d in path.iterdir() if d.is_dir())
144            self._hint.setText(_('{} subcarpeta(s) encontrada(s) en ese directorio.').format(n))
145        elif path:
146            self._hint.setText(_('El directorio se creará al confirmar.'))
147        else:
148            self._hint.setText('')
149
150    def _accept(self) -> None:
151        import shutil
152        text = self._dir_edit.text().strip()
153        if not text:
154            return
155        path = Path(text)
156        if not path.exists():
157            path.mkdir(parents=True, exist_ok=True)
158        # copiar curso de ejemplo si el directorio estaba vacío
159        _root = Path(__file__).parent.parent.parent.parent   # src/gui/dialogs → project root
160        ejemplo_src = _root / 'courses' / 'ejemplo'
161        ejemplo_dst = path / 'ejemplo'
162        if ejemplo_src.exists() and not ejemplo_dst.exists():
163            shutil.copytree(ejemplo_src, ejemplo_dst,
164                            ignore=shutil.ignore_patterns('output'))
165        self._selected = path
166        save_courses_dir(path)
167        self.accept()
168
169    # ── API pública ────────────────────────────────────────────────────────────
170
171    def selected_dir(self) -> Path | None:
172        """Devuelve el directorio elegido, o None si se omitió."""
173        return self._selected

Diálogo de primer arranque: permite seleccionar el directorio de cursos.

OnboardingDialog(parent=None)
65    def __init__(self, parent=None):
66        super().__init__(parent)
67        self.setWindowTitle(_('Configuración inicial'))
68        self.setMinimumWidth(540)
69        self.setStyleSheet(_build_style())
70        self._selected: Path | None = None
71        self._build_ui()
def selected_dir(self) -> pathlib.Path | None:
171    def selected_dir(self) -> Path | None:
172        """Devuelve el directorio elegido, o None si se omitió."""
173        return self._selected

Devuelve el directorio elegido, o None si se omitió.

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