Reference-line manager, relative time axis, axis-aware ref-line UX
Reference lines: - Side-panel cluster (ref_line_widget.py): list of lines with Add/Edit/Remove/ Clear and a properties dialog (value, colour, line style, tag) - Lines own their state as RefLineProps (held by MainWindow), drawn with a triangle drag-handle; dragging writes the value back and refreshes the list - Replaces the old centre-only add-and-drag-the-whole-line approach Comparison: - Time-axis mode selector: Absolute (seconds) vs Relative (% of each track's own length), so a long track and a short one line up by song position. Pure view transform (plotspec.apply_x_mode), no recompute - Overlaid different-length tracks now autorange their x union in absolute mode instead of clipping to the first track's span Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
"""
|
||||
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):
|
||||
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)
|
||||
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()
|
||||
Reference in New Issue
Block a user