gui.theme

Sistema de temas de color — Apple HIG.

Uso:

from gui.theme import c, setup, apply_global, signal, apply_live

setup('dark') # al inicio, antes de crear widgets c('bg') # color del tema activo apply_global(app) # aplica QSS global a QApplication signal().changed # señal emitida al cambiar el tema en vivo apply_live(app) # actualiza QSS + emite señal → todos los widgets se refrescan

  1"""
  2Sistema de temas de color — Apple HIG.
  3
  4Uso:
  5    from gui.theme import c, setup, apply_global, signal, apply_live
  6
  7    setup('dark')           # al inicio, antes de crear widgets
  8    c('bg')                 # color del tema activo
  9    apply_global(app)       # aplica QSS global a QApplication
 10    signal().changed        # señal emitida al cambiar el tema en vivo
 11    apply_live(app)         # actualiza QSS + emite señal → todos los widgets se refrescan
 12"""
 13
 14from __future__ import annotations
 15
 16_THEMES: dict[str, dict[str, str]] = {
 17
 18    # ── Dark — Apple macOS Dark Mode ─────────────────────────────────────────
 19    'dark': {
 20        'bg':           '#1C1C1E',  # systemBackground
 21        'bg_deep':      '#000000',  # editor textarea
 22        'bg_card':      '#2C2C2E',  # secondarySystemBackground (sidebar, toolbar)
 23        'bg_surface':   '#3A3A3C',  # tertiarySystemBackground (inputs, elevated)
 24        'bg_hover':     '#48484A',  # hover state
 25        'bg_selected':  '#1C3968',  # selection (accent suavizado)
 26        'border':       '#38383A',  # separator sutil
 27        'text':         '#FFFFFF',  # label
 28        'text_muted':   '#9A9A9E',  # secondaryLabel
 29        'text_dim':     '#636366',  # tertiaryLabel
 30        'text_disabled':'#48484A',  # quaternaryLabel
 31        'accent':       '#0A84FF',  # systemBlue dark
 32        'accent_hover': '#409CFF',
 33        'ok':           '#30D158',  # systemGreen dark
 34        'error':        '#FF453A',  # systemRed dark
 35        'warn':         '#FFD60A',  # systemYellow dark
 36        'log_ok':       '#32D74B',
 37        'log_error':    '#FF6961',
 38        'log_info':     '#E5E5EA',
 39        'sep':          '#545458',
 40    },
 41
 42    # ── Light — Apple macOS Light Mode ───────────────────────────────────────
 43    'light': {
 44        'bg':           '#FFFFFF',  # systemBackground
 45        'bg_deep':      '#FAFAFA',  # editor textarea
 46        'bg_card':      '#F2F2F7',  # secondarySystemBackground (sidebar, toolbar)
 47        'bg_surface':   '#E5E5EA',  # tertiarySystemBackground (inputs)
 48        'bg_hover':     '#E8E8ED',  # hover state
 49        'bg_selected':  '#D1E3FF',  # selection (accent suavizado)
 50        'border':       '#C6C6C8',  # separator
 51        'text':         '#000000',  # label
 52        'text_muted':   '#636366',  # secondaryLabel
 53        'text_dim':     '#8E8E93',  # tertiaryLabel
 54        'text_disabled':'#AEAEB2',  # quaternaryLabel
 55        'accent':       '#007AFF',  # systemBlue
 56        'accent_hover': '#0071E3',
 57        'ok':           '#34C759',  # systemGreen
 58        'error':        '#FF3B30',  # systemRed
 59        'warn':         '#FF9500',  # systemOrange
 60        'log_ok':       '#25A244',
 61        'log_error':    '#D70015',
 62        'log_info':     '#1D1D1F',
 63        'sep':          '#C6C6C8',
 64    },
 65}
 66
 67_current:   str             = 'dark'
 68_overrides: dict[str, str]  = {}
 69_signals                    = None   # lazy init — requiere QApplication
 70
 71
 72# ── Señal ──────────────────────────────────────────────────────────────────────
 73
 74def signal():
 75    """Devuelve el singleton ThemeSignals (creado lazy para no requerir QApp en import)."""
 76    global _signals
 77    if _signals is None:
 78        from PySide6.QtCore import QObject, Signal  # type: ignore[attr-defined]
 79
 80        class _ThemeSignals(QObject):
 81            changed = Signal()
 82
 83        _signals = _ThemeSignals()
 84    return _signals
 85
 86
 87# ── API pública ────────────────────────────────────────────────────────────────
 88
 89def setup(name: str) -> None:
 90    """Establece el tema activo. Llamar antes de crear widgets."""
 91    global _current
 92    _current = name if name in _THEMES else 'dark'
 93    try:
 94        from lib.slides_config import get_theme_overrides
 95        set_overrides(get_theme_overrides(_current))
 96    except Exception:
 97        pass
 98
 99
100def set_overrides(overrides: dict) -> None:
101    global _overrides
102    _overrides = {k: v for k, v in (overrides or {}).items()
103                  if isinstance(k, str) and isinstance(v, str)}
104
105
106def get() -> str:
107    return _current
108
109
110def all_keys() -> list[str]:
111    return list(_THEMES['dark'].keys())
112
113
114def c(key: str) -> str:
115    """Color activo: override > tema > fallback rojo."""
116    if key in _overrides:
117        return _overrides[key]
118    return _THEMES.get(_current, _THEMES['dark']).get(key, '#ff0000')
119
120
121def apply_global(app) -> None:
122    """Aplica el QSS global a QApplication."""
123    app.setStyleSheet(_build_global_qss())
124
125
126def apply_live(app=None) -> None:
127    """Actualiza QSS global y emite signal().changed → todos los widgets se refrescan."""
128    if app is None:
129        from PySide6.QtWidgets import QApplication
130        app = QApplication.instance()
131    if app:
132        app.setStyleSheet(_build_global_qss())
133    signal().changed.emit()
134
135
136# ── QSS global ────────────────────────────────────────────────────────────────
137
138def _rgba(key: str, alpha: float) -> str:
139    """Convierte c(key) hex a rgba para usar en QSS con transparencia."""
140    h = c(key).lstrip('#')
141    r, g, b = int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
142    return f'rgba({r},{g},{b},{alpha})'
143
144
145def _build_global_qss() -> str:
146    """Construye el QSS completo usando los colores del tema activo (Apple HIG)."""
147    return f"""
148/* ── Base ─────────────────────────────────────────── */
149QMainWindow, QDialog, QWidget {{
150    background: {c('bg')};
151    color: {c('text')};
152    font-size: 13px;
153}}
154
155/* ── Toolbar ──────────────────────────────────────── */
156QToolBar {{
157    background: {c('bg_card')};
158    border-bottom: 1px solid {c('border')};
159    spacing: 2px;
160    padding: 4px 8px;
161    min-height: 44px;
162}}
163QToolBar QToolButton {{
164    background: transparent;
165    color: {c('text')};
166    border: none;
167    border-radius: 7px;
168    padding: 6px 14px;
169    font-size: 13px;
170}}
171QToolBar QToolButton:hover    {{ background: {c('bg_hover')}; }}
172QToolBar QToolButton:pressed  {{ background: {c('bg_surface')}; }}
173QToolBar QToolButton:disabled {{ color: {c('text_disabled')}; }}
174QToolBar QToolButton:checked {{
175    background: {_rgba('accent', 0.18)};
176    color: {c('accent')};
177    border-radius: 7px;
178}}
179QToolBar QToolButton:checked:hover {{
180    background: {_rgba('accent', 0.28)};
181}}
182QToolBar QToolButton#toolbar_primary {{
183    background: {c('accent')};
184    color: #ffffff;
185    font-weight: 600;
186    padding: 6px 18px;
187    border-radius: 7px;
188}}
189QToolBar QToolButton#toolbar_primary:hover    {{ background: {c('accent_hover')}; }}
190QToolBar QToolButton#toolbar_primary:pressed  {{ background: {c('accent_hover')}; }}
191QToolBar QToolButton#toolbar_primary:disabled {{
192    background: {c('bg_surface')};
193    color: {c('text_disabled')};
194}}
195QToolBar QToolButton#toolbar_destructive:hover {{
196    color: {c('warn')};
197    background: {_rgba('warn', 0.10)};
198}}
199QToolBar::separator {{
200    background: {c('sep')};
201    width: 1px;
202    margin: 6px 8px;
203}}
204
205/* ── StatusBar ────────────────────────────────────── */
206QStatusBar {{
207    background: {c('bg_card')};
208    color: {c('text_dim')};
209    font-size: 11px;
210    border-top: 1px solid {c('border')};
211}}
212QStatusBar QLabel {{
213    color: {c('text_dim')};
214    font-size: 11px;
215    padding: 0 3px;
216}}
217
218/* ── Splitter ─────────────────────────────────────── */
219QSplitter::handle {{ background: {c('border')}; }}
220QSplitter::handle:horizontal {{ width: 1px; }}
221QSplitter::handle:vertical   {{ height: 1px; }}
222
223/* ── Sidebar ──────────────────────────────────────── */
224QWidget#sidebar {{
225    background: {c('bg_card')};
226    border-right: 1px solid {c('border')};
227}}
228QWidget#sidebar_header {{
229    background: {c('bg_card')};
230    border-bottom: 1px solid {c('border')};
231}}
232QPushButton#sidebar_course_btn {{
233    background: transparent;
234    color: {c('text')};
235    border: none;
236    font-size: 14px;
237    font-weight: 600;
238    text-align: left;
239    padding: 2px 8px;
240}}
241QPushButton#sidebar_course_btn:hover {{ color: {c('accent')}; }}
242QPushButton#sidebar_course_btn:pressed {{ color: {c('accent_hover')}; }}
243QPushButton#sidebar_icon_btn {{
244    background: transparent;
245    color: {c('text_muted')};
246    border: none;
247    border-radius: 5px;
248    font-size: 14px;
249    padding: 3px 5px;
250    min-width: 24px;
251    min-height: 24px;
252}}
253QPushButton#sidebar_icon_btn:hover {{
254    background: {c('bg_hover')};
255    color: {c('text')};
256}}
257QLabel#section_header {{
258    color: {c('text_dim')};
259    font-size: 10px;
260    font-weight: 700;
261    padding: 10px 14px 3px 14px;
262    background: transparent;
263}}
264
265/* ── Units list (source list) ─────────────────────── */
266QListWidget#units_list {{
267    background: transparent;
268    border: none;
269    font-size: 13px;
270    padding: 2px 0;
271    outline: none;
272}}
273QListWidget#units_list::item {{
274    padding: 0;
275    border-radius: 6px;
276    margin: 1px 8px;
277    color: {c('text')};
278}}
279QListWidget#units_list::item:selected {{
280    background: {c('accent')};
281    color: #FFFFFF;
282}}
283QListWidget#units_list::item:hover:!selected {{
284    background: {c('bg_hover')};
285}}
286QLabel#unit_row_label {{
287    background: transparent;
288    font-size: 13px;
289    color: {c('text')};
290}}
291QPushButton#unit_delete_btn {{
292    background: transparent;
293    color: {c('text')};
294    border: none;
295    border-radius: 4px;
296    font-size: 12px;
297    font-weight: 600;
298    padding: 0;
299    min-width: 18px;
300    max-width: 18px;
301    min-height: 18px;
302    max-height: 18px;
303}}
304QPushButton#unit_delete_btn:hover {{
305    background: {c('error')};
306    color: #ffffff;
307}}
308
309/* ── TreeWidget (SlideNavigator) ──────────────────── */
310QTreeWidget {{
311    background: transparent;
312    border: none;
313    font-size: 12px;
314    font-family: "Liberation Mono";
315    outline: none;
316    color: {c('text_muted')};
317}}
318QTreeWidget::item {{
319    padding: 4px 8px;
320    border-radius: 6px;
321    margin: 1px 6px;
322}}
323QTreeWidget::item:selected {{
324    background: {c('accent')};
325    color: #FFFFFF;
326}}
327QTreeWidget::item:hover:!selected {{ background: {c('bg_hover')}; }}
328QTreeWidget::branch {{ background: transparent; }}
329
330/* ── QMenu ────────────────────────────────────────── */
331QMenu {{
332    background: {c('bg_surface')};
333    color: {c('text')};
334    border: 1px solid {c('border')};
335    border-radius: 10px;
336    padding: 6px 0;
337}}
338QMenu::item {{ padding: 7px 28px 7px 16px; font-size: 13px; }}
339QMenu::item:selected {{
340    background: {c('accent')};
341    color: #FFFFFF;
342    border-radius: 5px;
343    margin: 0 5px;
344}}
345QMenu::item:checked  {{ color: {c('accent')}; font-weight: 600; }}
346QMenu::separator     {{ height: 1px; background: {c('border')}; margin: 4px 0; }}
347
348/* ── YAML error bar ───────────────────────────────── */
349QWidget#yaml_error_bar {{
350    background: {c('error')}22;
351    border-bottom: 1px solid {c('error')}66;
352}}
353QLabel#yaml_error_label {{
354    color: {c('error')};
355    font-size: 11px;
356    font-family: "Liberation Mono";
357}}
358
359/* ── Search bar ──────────────────────────────────── */
360QWidget#search_bar {{
361    background: {c('bg_card')};
362    border-bottom: 1px solid {c('border')};
363}}
364QLineEdit#search_input {{
365    background: {c('bg_surface')};
366    color: {c('text')};
367    border: 1.5px solid {c('border')};
368    border-radius: 7px;
369    padding: 4px 10px;
370    font-size: 13px;
371    min-height: 28px;
372}}
373QLineEdit#search_input:focus {{ border-color: {c('accent')}; }}
374QLabel#search_info {{
375    color: {c('text_muted')};
376    font-size: 11px;
377    font-family: "Liberation Mono";
378}}
379
380/* ── YAML Editor ──────────────────────────────────── */
381QWidget#file_bar {{ background: {c('bg_card')}; border-bottom: 1px solid {c('border')}; }}
382QLabel#file_name_label {{
383    color: {c('text_muted')};
384    font-size: 13px;
385    font-weight: 500;
386}}
387QPushButton#bar_btn {{
388    background: {c('bg_surface')};
389    color: {c('text_muted')};
390    border: none;
391    padding: 3px 10px;
392    font-size: 11px;
393    border-radius: 5px;
394    min-height: 24px;
395}}
396QPushButton#bar_btn:hover {{ background: {c('bg_hover')}; color: {c('text')}; }}
397QPushButton#insert_btn {{
398    background: {c('accent')};
399    color: #ffffff;
400    border: none;
401    padding: 3px 12px;
402    font-size: 12px;
403    border-radius: 6px;
404    font-weight: 500;
405    min-height: 26px;
406}}
407QPushButton#insert_btn:hover {{ background: {c('accent_hover')}; }}
408QPushButton#revert_btn {{
409    background: transparent;
410    color: {c('text_dim')};
411    border: 1px solid {c('border')};
412    border-radius: 6px;
413    padding: 2px 12px;
414    font-size: 12px;
415    min-height: 0;
416}}
417QPushButton#revert_btn:hover {{
418    color: {c('error')};
419    border-color: {c('error')};
420}}
421QPushButton#revert_btn:disabled {{
422    color: {c('text_disabled')};
423    border-color: transparent;
424}}
425QPlainTextEdit#yaml_editor {{
426    background: {c('bg_deep')};
427    color: {c('text')};
428    border: none;
429    selection-background-color: {c('bg_selected')};
430    font-family: "Liberation Mono";
431    font-size: 13px;
432}}
433
434/* ── Log Panel ────────────────────────────────────── */
435QWidget#log_header {{
436    background: {c('bg_card')};
437    border-top: 1px solid {c('border')};
438    border-bottom: 1px solid {c('border')};
439}}
440QLabel#log_title {{
441    color: {c('text_muted')};
442    font-size: 11px;
443    font-weight: 600;
444}}
445QPushButton#log_btn {{
446    background: transparent;
447    color: {c('text_muted')};
448    border: none;
449    padding: 2px 8px;
450    font-size: 11px;
451    border-radius: 5px;
452    min-height: 22px;
453}}
454QPushButton#log_btn:hover {{
455    background: {c('bg_hover')};
456    color: {c('text')};
457}}
458QPushButton#log_collapse_btn {{
459    background: transparent;
460    color: {c('text_dim')};
461    border: none;
462    padding: 2px 6px;
463    font-size: 12px;
464    border-radius: 4px;
465    min-width: 24px;
466    min-height: 22px;
467}}
468QPushButton#log_collapse_btn:hover {{
469    background: {c('bg_hover')};
470    color: {c('text_muted')};
471}}
472QPlainTextEdit#log_text {{
473    background: {c('bg_deep')};
474    color: {c('text')};
475    border: none;
476    selection-background-color: {c('bg_selected')};
477    font-family: "Liberation Mono";
478    font-size: 11px;
479}}
480
481/* ── Progress bar (build) ─────────────────────────── */
482QProgressBar#build_progress {{
483    background: {c('border')};
484    border: none;
485    border-radius: 0;
486}}
487QProgressBar#build_progress::chunk {{
488    background: {c('accent')};
489    border-radius: 0;
490}}
491
492/* ── Results bar (log panel) ──────────────────────── */
493QWidget#results_bar {{
494    background: {c('bg_card')};
495    border-top: 1px solid {c('border')};
496}}
497QScrollArea#results_scroll {{
498    background: {c('bg_card')};
499    border: none;
500    border-top: 1px solid {c('border')};
501}}
502QLabel#results_label {{
503    color: {c('text_muted')};
504    font-size: 12px;
505}}
506QPushButton#open_result_btn {{
507    background: {c('bg_surface')};
508    color: {c('text')};
509    border: 1px solid {c('border')};
510    border-radius: 6px;
511    padding: 2px 10px;
512    font-size: 11px;
513    font-weight: 500;
514    min-height: 0;
515}}
516QPushButton#open_result_btn:hover {{
517    background: {c('accent')};
518    color: #ffffff;
519    border-color: {c('accent')};
520}}
521
522/* ── Dialogs: GroupBox, Forms ─────────────────────── */
523QGroupBox {{
524    color: {c('text_muted')};
525    border: 1px solid {c('border')};
526    border-radius: 10px;
527    margin-top: 14px;
528    font-size: 12px;
529    padding: 18px 12px 12px 12px;
530}}
531QGroupBox::title {{
532    subcontrol-origin: margin;
533    left: 12px;
534    padding: 0 4px;
535    color: {c('text_muted')};
536    font-weight: 600;
537}}
538QLabel  {{ color: {c('text')}; font-size: 13px; }}
539QLabel#hint  {{ color: {c('text_dim')}; font-size: 11px; }}
540QLineEdit {{
541    background: {c('bg_surface')};
542    color: {c('text')};
543    border: 1.5px solid {c('border')};
544    border-radius: 8px;
545    padding: 7px 10px;
546    font-size: 13px;
547    min-height: 34px;
548    selection-background-color: {c('accent')};
549    selection-color: #FFFFFF;
550}}
551QLineEdit:focus {{ border-color: {c('accent')}; }}
552QLineEdit:disabled {{
553    color: {c('text_disabled')};
554    background: {c('bg_card')};
555}}
556QComboBox {{
557    background: {c('bg_surface')};
558    color: {c('text')};
559    border: 1.5px solid {c('border')};
560    border-radius: 8px;
561    padding: 6px 10px;
562    font-size: 13px;
563    min-width: 80px;
564    min-height: 34px;
565}}
566QComboBox:focus {{ border-color: {c('accent')}; }}
567QComboBox::drop-down {{ border: none; width: 20px; }}
568QComboBox QAbstractItemView {{
569    background: {c('bg_surface')};
570    color: {c('text')};
571    border: 1px solid {c('border')};
572    border-radius: 8px;
573    selection-background-color: {c('accent')};
574    selection-color: #FFFFFF;
575    padding: 4px;
576}}
577
578/* ── TabWidget (Settings) ─────────────────────────── */
579QTabWidget::pane {{
580    border: 1px solid {c('border')};
581    border-radius: 8px;
582    background: {c('bg')};
583    top: -1px;
584}}
585QTabWidget::tab-bar {{ left: 8px; }}
586QTabBar::tab {{
587    background: {c('bg_card')};
588    color: {c('text_muted')};
589    padding: 7px 20px;
590    border: 1px solid {c('border')};
591    border-bottom: none;
592    border-radius: 8px 8px 0 0;
593    margin-right: 2px;
594    font-size: 13px;
595}}
596QTabBar::tab:selected {{
597    background: {c('bg')};
598    color: {c('text')};
599    font-weight: 500;
600}}
601QTabBar::tab:hover:!selected {{ background: {c('bg_hover')}; }}
602
603/* ── Buttons (generic) ────────────────────────────── */
604QPushButton {{
605    background: {c('bg_surface')};
606    color: {c('text')};
607    border: none;
608    border-radius: 8px;
609    padding: 7px 16px;
610    font-size: 13px;
611    min-height: 32px;
612}}
613QPushButton:hover {{ background: {c('bg_hover')}; }}
614QPushButton:pressed {{ background: {c('bg_selected')}; }}
615QPushButton:disabled {{ color: {c('text_disabled')}; background: {c('bg_card')}; }}
616QPushButton#browse {{
617    background: {c('bg_surface')};
618    border: 1px solid {c('border')};
619    padding: 6px 12px;
620    min-height: 30px;
621}}
622QPushButton#browse:hover {{ background: {c('bg_hover')}; }}
623QPushButton#danger {{
624    background: transparent;
625    color: {c('error')};
626    border: 1.5px solid {c('error')};
627    border-radius: 8px;
628    padding: 6px 16px;
629    font-size: 13px;
630}}
631QPushButton#danger:hover {{ background: {c('error')}; color: #ffffff; }}
632
633/* ── DialogButtonBox ──────────────────────────────── */
634QDialogButtonBox QPushButton {{
635    background: {c('accent')};
636    color: #ffffff;
637    border: none;
638    border-radius: 8px;
639    padding: 8px 24px;
640    font-size: 13px;
641    font-weight: 500;
642    min-width: 90px;
643    min-height: 34px;
644}}
645QDialogButtonBox QPushButton:hover {{ background: {c('accent_hover')}; }}
646QDialogButtonBox QPushButton[text="Cancelar"],
647QDialogButtonBox QPushButton[text="Cancel"] {{
648    background: {c('bg_surface')};
649    color: {c('text_muted')};
650    border: 1px solid {c('border')};
651}}
652QDialogButtonBox QPushButton[text="Cancelar"]:hover,
653QDialogButtonBox QPushButton[text="Cancel"]:hover {{
654    background: {c('bg_hover')};
655    color: {c('text')};
656}}
657
658/* ── CheckBox & RadioButton ───────────────────────── */
659QCheckBox, QRadioButton {{
660    color: {c('text')};
661    font-size: 13px;
662    spacing: 6px;
663}}
664QCheckBox::indicator, QRadioButton::indicator {{
665    width: 16px;
666    height: 16px;
667    border: 1.5px solid {c('border')};
668    border-radius: 4px;
669    background: {c('bg_surface')};
670}}
671QRadioButton::indicator {{ border-radius: 8px; }}
672QCheckBox::indicator:checked, QRadioButton::indicator:checked {{
673    background: {c('accent')};
674    border-color: {c('accent')};
675}}
676
677/* ── Scrollbars ───────────────────────────────────── */
678QScrollBar:vertical {{
679    background: transparent;
680    width: 8px;
681    border: none;
682    margin: 2px 0;
683}}
684QScrollBar::handle:vertical {{
685    background: {c('sep')};
686    border-radius: 4px;
687    min-height: 28px;
688    margin: 0 2px;
689}}
690QScrollBar::handle:vertical:hover {{ background: {c('text_dim')}; }}
691QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{ height: 0; }}
692QScrollBar:horizontal {{
693    background: transparent;
694    height: 8px;
695    border: none;
696    margin: 0 2px;
697}}
698QScrollBar::handle:horizontal {{
699    background: {c('sep')};
700    border-radius: 4px;
701    min-width: 28px;
702    margin: 2px 0;
703}}
704QScrollBar::handle:horizontal:hover {{ background: {c('text_dim')}; }}
705QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {{ width: 0; }}
706
707/* ── Scroll area ──────────────────────────────────── */
708QScrollArea {{ border: none; background: {c('bg')}; }}
709QScrollArea > QWidget > QWidget {{ background: {c('bg')}; }}
710
711/* ── Tooltip ──────────────────────────────────────── */
712QToolTip {{
713    background: {c('bg_surface')};
714    color: {c('text')};
715    border: 1px solid {c('border')};
716    padding: 5px 10px;
717    border-radius: 6px;
718    font-size: 12px;
719}}
720"""
def signal():
75def signal():
76    """Devuelve el singleton ThemeSignals (creado lazy para no requerir QApp en import)."""
77    global _signals
78    if _signals is None:
79        from PySide6.QtCore import QObject, Signal  # type: ignore[attr-defined]
80
81        class _ThemeSignals(QObject):
82            changed = Signal()
83
84        _signals = _ThemeSignals()
85    return _signals

Devuelve el singleton ThemeSignals (creado lazy para no requerir QApp en import).

def setup(name: str) -> None:
90def setup(name: str) -> None:
91    """Establece el tema activo. Llamar antes de crear widgets."""
92    global _current
93    _current = name if name in _THEMES else 'dark'
94    try:
95        from lib.slides_config import get_theme_overrides
96        set_overrides(get_theme_overrides(_current))
97    except Exception:
98        pass

Establece el tema activo. Llamar antes de crear widgets.

def set_overrides(overrides: dict) -> None:
101def set_overrides(overrides: dict) -> None:
102    global _overrides
103    _overrides = {k: v for k, v in (overrides or {}).items()
104                  if isinstance(k, str) and isinstance(v, str)}
def get() -> str:
107def get() -> str:
108    return _current
def all_keys() -> list[str]:
111def all_keys() -> list[str]:
112    return list(_THEMES['dark'].keys())
def c(key: str) -> str:
115def c(key: str) -> str:
116    """Color activo: override > tema > fallback rojo."""
117    if key in _overrides:
118        return _overrides[key]
119    return _THEMES.get(_current, _THEMES['dark']).get(key, '#ff0000')

Color activo: override > tema > fallback rojo.

def apply_global(app) -> None:
122def apply_global(app) -> None:
123    """Aplica el QSS global a QApplication."""
124    app.setStyleSheet(_build_global_qss())

Aplica el QSS global a QApplication.

def apply_live(app=None) -> None:
127def apply_live(app=None) -> None:
128    """Actualiza QSS global y emite signal().changed → todos los widgets se refrescan."""
129    if app is None:
130        from PySide6.QtWidgets import QApplication
131        app = QApplication.instance()
132    if app:
133        app.setStyleSheet(_build_global_qss())
134    signal().changed.emit()

Actualiza QSS global y emite signal().changed → todos los widgets se refrescan.