block_table.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. """
  2. Block table widget — one row per sync block, colour-coded by type.
  3. Row tints use alpha-transparent QColor values so they blend with whatever
  4. base colour the OS palette provides, keeping type indication readable on
  5. both light and dark themes.
  6. """
  7. from __future__ import annotations
  8. from PySide6.QtCore import Signal, Qt
  9. from PySide6.QtWidgets import (
  10. QTableWidget, QTableWidgetItem, QHeaderView, QAbstractItemView,
  11. )
  12. from PySide6.QtGui import QColor, QBrush
  13. from seq_interp.src.gui.adapters import BlockRow
  14. _COL_NAMES = [
  15. "#sync", "orig#", "Type", "Dur (µs)", "RF", "ADC", "Grad",
  16. "Delay", "gRF", "gADC", "gTR", "T-start (µs)",
  17. ]
  18. # Row background tints — alpha ~80/255 so they blend with the OS base colour.
  19. # On a white base: soft pastels. On a dark base: subtle dark tints.
  20. _BG: dict[str, QColor] = {
  21. "START": QColor(244, 143, 177, 80), # pink — start delay
  22. "TR": QColor(144, 202, 249, 80), # blue — TR delay
  23. "RF": QColor(255, 224, 130, 80), # amber — RF delay
  24. "adc": QColor(165, 214, 167, 80), # green — ADC block
  25. "rf": QColor(244, 143, 177, 80), # pink — RF block
  26. "grad": QColor(206, 147, 216, 80), # purple — gradient block
  27. "plain": QColor(0, 0, 0, 0 ), # transparent — no tint
  28. }
  29. # Structural stylesheet only — base text/background come from the OS palette.
  30. # Only the selection highlight and hover state are pinned to explicit colours
  31. # because they need to be clearly visible on any background.
  32. _STYLESHEET = """
  33. QTableWidget {
  34. gridline-color: palette(mid);
  35. font-size: 12px;
  36. }
  37. QTableWidget::item {
  38. padding: 2px 4px;
  39. }
  40. QTableWidget::item:selected {
  41. background-color: #1565c0;
  42. color: #ffffff;
  43. }
  44. QTableWidget::item:hover:!selected {
  45. background-color: rgba(144, 202, 249, 60);
  46. }
  47. QHeaderView::section {
  48. border: none;
  49. border-bottom: 1px solid palette(mid);
  50. border-right: 1px solid palette(mid);
  51. padding: 4px 6px;
  52. font-weight: bold;
  53. }
  54. QScrollBar:horizontal, QScrollBar:vertical {
  55. background: palette(alternateBase);
  56. }
  57. """
  58. def _bg_for(row: BlockRow) -> QColor:
  59. if row.is_delay:
  60. return _BG.get(row.delay_type, _BG["plain"])
  61. if row.has_adc:
  62. return _BG["adc"]
  63. if row.has_rf:
  64. return _BG["rf"]
  65. if row.has_grad:
  66. return _BG["grad"]
  67. return _BG["plain"]
  68. class BlockTable(QTableWidget):
  69. blockSelected = Signal(int) # sync_index
  70. def __init__(self, parent=None):
  71. super().__init__(0, len(_COL_NAMES), parent)
  72. self.setHorizontalHeaderLabels(_COL_NAMES)
  73. self.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
  74. self.setSelectionBehavior(QAbstractItemView.SelectRows)
  75. self.setEditTriggers(QAbstractItemView.NoEditTriggers)
  76. self.setAlternatingRowColors(False)
  77. self.verticalHeader().setDefaultSectionSize(20)
  78. self.verticalHeader().setVisible(False)
  79. self.setStyleSheet(_STYLESHEET)
  80. self._rows: list[BlockRow] = []
  81. self._suppress = False
  82. self.itemSelectionChanged.connect(self._on_selection)
  83. # ── public ────────────────────────────────────────────────────────────────
  84. def load_rows(self, rows: list[BlockRow]) -> None:
  85. self._rows = rows
  86. self.setRowCount(0)
  87. self.setRowCount(len(rows))
  88. for r, row in enumerate(rows):
  89. bg = QBrush(_bg_for(row))
  90. vals = [
  91. str(row.sync_index),
  92. str(row.orig_index) if row.orig_index >= 0 else "—",
  93. row.block_type,
  94. f"{row.duration * 1e6:.3f}",
  95. "✓" if row.has_rf else "",
  96. "✓" if row.has_adc else "",
  97. "✓" if row.has_grad else "",
  98. row.delay_type if row.is_delay else "",
  99. str(row.gate_rf),
  100. str(row.gate_adc),
  101. str(row.gate_tr),
  102. f"{row.t_start * 1e6:.3f}",
  103. ]
  104. for c, val in enumerate(vals):
  105. item = QTableWidgetItem(val)
  106. item.setTextAlignment(Qt.AlignCenter)
  107. item.setBackground(bg)
  108. # Foreground intentionally not set: the OS palette Text role
  109. # provides the correct readable colour for the current theme.
  110. self.setItem(r, c, item)
  111. def select_by_sync_index(self, sync_index: int) -> None:
  112. self._suppress = True
  113. try:
  114. for r, row in enumerate(self._rows):
  115. if row.sync_index == sync_index:
  116. self.selectRow(r)
  117. self.scrollTo(self.model().index(r, 0))
  118. break
  119. finally:
  120. self._suppress = False
  121. def row_for_sync_index(self, sync_index: int) -> BlockRow | None:
  122. for row in self._rows:
  123. if row.sync_index == sync_index:
  124. return row
  125. return None
  126. # ── private ───────────────────────────────────────────────────────────────
  127. def _on_selection(self) -> None:
  128. if self._suppress:
  129. return
  130. selected = self.selectionModel().selectedRows()
  131. if selected:
  132. r = selected[0].row()
  133. if r < len(self._rows):
  134. self.blockSelected.emit(self._rows[r].sync_index)