22ecb67296
Reference lines: - Kept per metric (dict keyed by metric_id) so switching metrics and coming back preserves them, instead of clearing on every switch - Spectrogram lines read/edit/drag in Hz: the renderer installs Hz<->row-index transforms (the heatmap y-axis is a row index), so value, label, the edit dialog, and the new-line default all speak frequency. Curve metrics stay identity. Round-trip verified (1000 Hz -> row -> 1000 Hz) - Line label and drag-readback always in the metric's natural units Controls: - Relative-time abs/rel is now a checkbox toggle, not a dropdown - Removed the font control panel; UI font is locked to M PLUS 1 Code @ 10pt via font_manager.apply_fixed_font (system-default fallback). Deleted font_control_widget.py Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
156 lines
4.8 KiB
Python
156 lines
4.8 KiB
Python
"""
|
|
Reference-line management: a side-panel list of custom horizontal markers plus a
|
|
properties dialog.
|
|
|
|
`RefLineControlWidget` is a pure view over a list of `RefLineProps` owned by the
|
|
main window: it renders the list and emits intents (add / edit / remove / clear).
|
|
`RefLineDialog` edits one line's value, colour, line style, and tag.
|
|
"""
|
|
|
|
import logging
|
|
|
|
from PyQt5.QtWidgets import (
|
|
QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QListWidget, QPushButton,
|
|
QDialog, QFormLayout, QDoubleSpinBox, QComboBox, QLineEdit, QColorDialog,
|
|
QDialogButtonBox,
|
|
)
|
|
from PyQt5.QtGui import QColor
|
|
from PyQt5.QtCore import pyqtSignal
|
|
|
|
from plotspec import RefLineProps
|
|
|
|
|
|
_STYLE_CHOICES = [("Solid", "solid"), ("Dashed", "dash"), ("Dotted", "dot")]
|
|
|
|
|
|
class RefLineDialog(QDialog):
|
|
"""Edit one reference line's properties. Read the result via `result_props`."""
|
|
|
|
def __init__(self, parent, props: RefLineProps, value_units: str = ""):
|
|
super().__init__(parent)
|
|
self.setWindowTitle("Reference line")
|
|
self._color = props.color
|
|
|
|
form = QFormLayout(self)
|
|
|
|
self.value_spin = QDoubleSpinBox()
|
|
self.value_spin.setRange(-1e6, 1e6)
|
|
self.value_spin.setDecimals(2)
|
|
self.value_spin.setValue(props.value)
|
|
if value_units:
|
|
self.value_spin.setSuffix(f" {value_units}")
|
|
form.addRow("Value:", self.value_spin)
|
|
|
|
self.color_button = QPushButton()
|
|
self.color_button.clicked.connect(self._pick_color)
|
|
self._refresh_color_button()
|
|
form.addRow("Colour:", self.color_button)
|
|
|
|
self.style_combo = QComboBox()
|
|
for label, key in _STYLE_CHOICES:
|
|
self.style_combo.addItem(label, key)
|
|
idx = self.style_combo.findData(props.style)
|
|
if idx >= 0:
|
|
self.style_combo.setCurrentIndex(idx)
|
|
form.addRow("Line style:", self.style_combo)
|
|
|
|
self.label_edit = QLineEdit(props.label)
|
|
self.label_edit.setPlaceholderText("(optional tag)")
|
|
form.addRow("Tag:", self.label_edit)
|
|
|
|
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
buttons.accepted.connect(self.accept)
|
|
buttons.rejected.connect(self.reject)
|
|
form.addRow(buttons)
|
|
|
|
def _pick_color(self):
|
|
chosen = QColorDialog.getColor(QColor(self._color), self, "Reference line colour")
|
|
if chosen.isValid():
|
|
self._color = chosen.name()
|
|
self._refresh_color_button()
|
|
|
|
def _refresh_color_button(self):
|
|
self.color_button.setText(self._color)
|
|
# Show the colour as the button's background for a quick read.
|
|
self.color_button.setStyleSheet(f"background-color: {self._color};")
|
|
|
|
def result_props(self) -> RefLineProps:
|
|
return RefLineProps(
|
|
value=float(self.value_spin.value()),
|
|
color=self._color,
|
|
style=self.style_combo.currentData(),
|
|
label=self.label_edit.text().strip(),
|
|
)
|
|
|
|
|
|
class RefLineControlWidget(QWidget):
|
|
"""List of reference lines with Add / Edit / Remove / Clear controls."""
|
|
|
|
addRequested = pyqtSignal()
|
|
editRequested = pyqtSignal(int)
|
|
removeRequested = pyqtSignal(int)
|
|
clearRequested = pyqtSignal()
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.logger = logging.getLogger(__name__)
|
|
self.initUI()
|
|
|
|
def initUI(self):
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(5, 5, 5, 5)
|
|
|
|
group_box = QGroupBox("Reference lines")
|
|
group_layout = QVBoxLayout(group_box)
|
|
|
|
self.line_list = QListWidget()
|
|
self.line_list.setMaximumHeight(110)
|
|
self.line_list.itemDoubleClicked.connect(self._on_double_click)
|
|
group_layout.addWidget(self.line_list)
|
|
|
|
row = QHBoxLayout()
|
|
self.add_button = QPushButton("Add")
|
|
self.add_button.clicked.connect(self.addRequested.emit)
|
|
row.addWidget(self.add_button)
|
|
|
|
self.edit_button = QPushButton("Edit…")
|
|
self.edit_button.clicked.connect(self._emit_edit)
|
|
row.addWidget(self.edit_button)
|
|
|
|
self.remove_button = QPushButton("Remove")
|
|
self.remove_button.clicked.connect(self._emit_remove)
|
|
row.addWidget(self.remove_button)
|
|
|
|
self.clear_button = QPushButton("Clear")
|
|
self.clear_button.clicked.connect(self.clearRequested.emit)
|
|
row.addWidget(self.clear_button)
|
|
|
|
group_layout.addLayout(row)
|
|
layout.addWidget(group_box)
|
|
|
|
def set_lines(self, lines: list[RefLineProps]):
|
|
"""Repopulate the list display from the current props (preserving selection)."""
|
|
current = self.line_list.currentRow()
|
|
self.line_list.clear()
|
|
for p in lines:
|
|
tag = f" {p.label}" if p.label else ""
|
|
self.line_list.addItem(f"{p.value:.2f}{tag}")
|
|
if 0 <= current < self.line_list.count():
|
|
self.line_list.setCurrentRow(current)
|
|
|
|
def _selected_row(self) -> int:
|
|
return self.line_list.currentRow()
|
|
|
|
def _emit_edit(self):
|
|
row = self._selected_row()
|
|
if row >= 0:
|
|
self.editRequested.emit(row)
|
|
|
|
def _emit_remove(self):
|
|
row = self._selected_row()
|
|
if row >= 0:
|
|
self.removeRequested.emit(row)
|
|
|
|
def _on_double_click(self, _item):
|
|
self._emit_edit()
|