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:
def
get() -> str:
def
all_keys() -> list[str]:
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.