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