Per-metric ref lines with Hz spectrogram units; relative-time toggle; fixed font

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>
This commit is contained in:
Mikkeli Matlock
2026-06-14 01:05:23 +09:00
parent a45bd4ba9b
commit 22ecb67296
8 changed files with 132 additions and 387 deletions
+15 -11
View File
@@ -9,7 +9,7 @@ A custom mastering toolkit that provides metrics to evaluate audio masterings th
- **Pluggable Metrics**: Switchable visualizations (RMS Power, Waveform, LUFS, Crest Factor, PSR, True Peak, Spectrogram; DR next) via a `Metric` ABC
- **Metadata Extraction**: Reads ID3 tags from MP3 files for better file identification
- **Modular GUI Architecture**: Complete PyQt5 interface with drag-and-drop and file dialog support
- **Font Management**: Comprehensive CJK-compatible font system with user-provided font support
- **Font Management**: CJK-capable, fixed UI font (M PLUS 1 Code @ 10pt) with system fallback
- **Threading & Logging**: Robust background processing with detailed logging system
### Technical stack
@@ -49,11 +49,14 @@ A custom mastering toolkit that provides metrics to evaluate audio masterings th
- The seam that decouples metrics from the plotting library: metrics emit
*intent*, the renderer owns colour/layout/library specifics
#### `font_control_widget.py` & `font_manager.py`
- Unified font control system with clustered interface
- Auto-detection of custom fonts from `fonts/` directory
- System font discovery and CJK compatibility
- Font changes trigger a cheap re-render of the cached metric data
#### `font_manager.py`
- Auto-detection of custom fonts from `fonts/` directory; CJK fallbacks
- `apply_fixed_font(family, size)` locks the Qt app font (used at startup to pin
the UI to **M PLUS 1 Code @ 10pt**, falling back to the system default if the
family isn't found). There is no runtime font picker — the old
`font_control_widget.py` was removed as wasted panel space
- pyqtgraph and the Qt widgets both read the app font, so this covers the plot
too (M PLUS 1 Code has full Japanese coverage, so titles stay CJK-safe)
#### `plot_control_widget.py`
- Metric selector dropdown driven by the `metrics.METRICS` registry
@@ -123,13 +126,14 @@ A custom mastering toolkit that provides metrics to evaluate audio masterings th
(pyqtgraph ViewBox); log/linear frequency toggle. Scroll zooms both axes;
**Ctrl+scroll** zooms time only, **Shift+scroll** zooms the value axis only
(`_AxisZoomViewBox`); scrolling over an axis also zooms just that axis
- **Time-axis mode**: Absolute (seconds) or Relative (% of each track's own
length), so tracks of very different durations line up by song position
- **Time-axis mode**: a Relative-time toggle — off = seconds, on = 0-100% of each
track's own length, so tracks of very different durations line up by position
- **Custom reference lines**: side-panel list (Add/Edit/Remove/Clear) of draggable
horizontal markers with value/colour/style/tag; dragged via a triangle handle.
Persist across redraws/overlay changes; cleared when the metric changes
- **Font control**: Unified font selector with size control
- **Plot control**: Metric selector + log-frequency toggle + time-axis mode
Kept **per metric** (so switching metrics doesn't lose them) and expressed in
the metric's own units — on the spectrogram they read and edit in **Hz** (the
renderer converts Hz<->row index, since the heatmap y-axis is a row index)
- **Plot control**: Metric selector + log-frequency toggle + relative-time toggle
+ refresh-plot button
- **Analysis display**: Real-time visualization with metadata panels
- **Modular architecture**: Self-contained widgets for easy layout management
+42 -10
View File
@@ -84,27 +84,38 @@ class _AxisZoomViewBox(pg.ViewBox):
class _RefLine(pg.InfiniteLine):
"""A draggable horizontal reference line bound to a RefLineProps.
Carries a triangle grab-handle at the left edge and writes its position back
into the props on drag, notifying the widget so the side-panel list refreshes.
`props.value` is in the metric's natural units (LUFS, dB, ... or Hz for the
spectrogram). The drawn y-position may differ from that value — the spectrogram
maps frequency to a row index — so `to_pos`/`from_pos` convert between the two.
For curve metrics these are identity. The label and the value written back on
drag are always in natural units.
"""
def __init__(self, index: int, props: RefLineProps, on_moved):
def __init__(self, index, props: RefLineProps, to_pos, from_pos, fmt, on_moved):
pen = pg.mkPen(props.color, width=1.4,
style=_PEN_STYLE.get(props.style, Qt.DashLine))
super().__init__(
pos=props.value, angle=0, movable=True, pen=pen,
label=props.label or "{value:.2f}",
pos=to_pos(props.value), angle=0, movable=True, pen=pen,
label="",
labelOpts={"position": 0.06, "color": props.color,
"fill": (255, 255, 255, 180)},
)
self._index = index
self._props = props
self._from_pos = from_pos
self._fmt = fmt
self._on_moved = on_moved
self.addMarker("|>", position=0.0, size=12) # triangle handle at the start
self._update_label()
self.sigPositionChanged.connect(self._update_label)
self.sigPositionChangeFinished.connect(self._commit)
def _update_label(self):
val = self._from_pos(self.value())
self.label.setFormat(self._props.label or self._fmt(val))
def _commit(self):
self._props.value = float(self.value())
self._props.value = float(self._from_pos(self.value()))
self._on_moved(self._index)
@@ -134,6 +145,11 @@ class AudioVisualizationWidget(QWidget):
# passed in via set_reference_lines; the line items are rebuilt each render.
self._ref_props: list[RefLineProps] = []
self._ref_lines: list[_RefLine] = []
# value<->drawn-position transforms for ref lines (identity for curve metrics;
# frequency<->row-index for the spectrogram). Reset each render.
self._ref_to_pos = lambda v: v
self._ref_from_pos = lambda p: p
self._ref_fmt = lambda v: f"{v:.2f}"
self._show_empty()
# ---- public API ---------------------------------------------------------
@@ -187,10 +203,14 @@ class AudioVisualizationWidget(QWidget):
self._ref_props = props
self._draw_ref_lines()
def current_view_center_y(self) -> float:
"""Mid-point of the current y view — a sane default position for a new line."""
def current_view_center_value(self) -> float:
"""Natural-unit value at the current y-view centre — default for a new line.
Runs through `from_pos`, so on the spectrogram this returns a frequency, not
a row index.
"""
(_, _), (y0, y1) = self.plot.viewRange()
return (y0 + y1) / 2.0
return float(self._ref_from_pos((y0 + y1) / 2.0))
def set_status(self, message: str):
self.status_label.setText(message)
@@ -269,6 +289,13 @@ class AudioVisualizationWidget(QWidget):
img.setRect(pg.QtCore.QRectF(t0, 0.0, t1 - t0, float(n_rows)))
self.plot.addItem(img)
# Reference lines on the spectrogram are entered/shown in Hz but drawn at a
# row index — install the frequency<->row transforms for this f_grid.
rows = np.arange(n_rows)
self._ref_to_pos = lambda hz, fg=f_grid, r=rows: float(np.interp(hz, fg, r))
self._ref_from_pos = lambda pos, fg=f_grid, r=rows: float(np.interp(pos, r, fg))
self._ref_fmt = lambda v: f"{v:.0f} Hz"
# Label the row-index y-axis with real frequencies.
ticks = []
for hz in _LOG_FREQ_TICKS:
@@ -309,7 +336,8 @@ class AudioVisualizationWidget(QWidget):
"""(Re)create draggable lines from the current RefLineProps set."""
self._remove_ref_line_items()
for idx, props in enumerate(self._ref_props):
line = _RefLine(idx, props, on_moved=self.referenceLineMoved.emit)
line = _RefLine(idx, props, self._ref_to_pos, self._ref_from_pos,
self._ref_fmt, on_moved=self.referenceLineMoved.emit)
self.plot.addItem(line)
self._ref_lines.append(line)
@@ -338,6 +366,10 @@ class AudioVisualizationWidget(QWidget):
self.legend.clear()
self.plot.getAxis("left").setTicks(None) # drop heatmap freq ticks
self.plot.setLogMode(x=False, y=False)
# Back to identity; the heatmap path reinstalls Hz<->row if needed.
self._ref_to_pos = lambda v: v
self._ref_from_pos = lambda p: p
self._ref_fmt = lambda v: f"{v:.2f}"
def _show_empty(self):
text = pg.TextItem("Drop an audio file to see analysis", anchor=(0.5, 0.5),
-311
View File
@@ -1,311 +0,0 @@
"""
Unified font control widget clustering all font-related manipulators.
Self-contained widget for easy layout management.
"""
import logging
from typing import List, Dict, Optional
from PyQt5.QtWidgets import (QWidget, QComboBox, QVBoxLayout, QHBoxLayout,
QLabel, QSlider, QGroupBox)
from PyQt5.QtCore import pyqtSignal, Qt
from font_manager import get_font_manager
class FontControlWidget(QWidget):
"""
Unified font control widget containing all font-related manipulators.
Provides clustered interface for:
- Font selection (unified for Qt and matplotlib)
- Qt font size adjustment
"""
# Signals
fontChanged = pyqtSignal(str, str) # (font_name, font_type)
fontSizeChanged = pyqtSignal(int) # font_size
def __init__(self, parent=None):
"""Initialize the font control widget."""
super().__init__(parent)
self.logger = logging.getLogger(__name__)
self.font_manager = get_font_manager()
self.available_fonts: Dict[str, str] = {} # display_name -> actual_font_name
# Font size bounds (reasonable range for GUI fonts)
self.min_font_size = 9
self.max_font_size = 12
self.default_font_size = 10
self.initUI()
self.refresh_font_list()
def initUI(self):
"""Initialize the user interface."""
# Main layout
layout = QVBoxLayout(self)
layout.setContentsMargins(5, 5, 5, 5) # Minimal margins
# Group box for visual clustering
group_box = QGroupBox("Font Settings")
group_layout = QVBoxLayout(group_box)
# Font selector section
font_section = self._create_font_selector_section()
group_layout.addWidget(font_section)
# Font size section
size_section = self._create_font_size_section()
group_layout.addWidget(size_section)
layout.addWidget(group_box)
def _create_font_selector_section(self) -> QWidget:
"""Create the font selector section."""
section = QWidget()
layout = QVBoxLayout(section)
layout.setContentsMargins(0, 0, 0, 0)
# Font label and dropdown
font_label = QLabel("Font:")
layout.addWidget(font_label)
self.font_combo = QComboBox()
self.font_combo.currentTextChanged.connect(self.on_font_changed)
layout.addWidget(self.font_combo)
return section
def _create_font_size_section(self) -> QWidget:
"""Create the font size control section."""
section = QWidget()
layout = QVBoxLayout(section)
layout.setContentsMargins(0, 0, 0, 0)
# Size label
self.size_label = QLabel(f"Qt Font Size: {self.default_font_size}pt")
layout.addWidget(self.size_label)
# Size slider with value labels
slider_layout = QHBoxLayout()
# Min label
min_label = QLabel(str(self.min_font_size))
min_label.setFixedWidth(20)
slider_layout.addWidget(min_label)
# Slider
self.size_slider = QSlider(Qt.Horizontal)
self.size_slider.setMinimum(self.min_font_size)
self.size_slider.setMaximum(self.max_font_size)
self.size_slider.setValue(self.default_font_size)
self.size_slider.setTickPosition(QSlider.TicksBelow)
self.size_slider.setTickInterval(2)
self.size_slider.valueChanged.connect(self.on_font_size_changed)
slider_layout.addWidget(self.size_slider)
# Max label
max_label = QLabel(str(self.max_font_size))
max_label.setFixedWidth(20)
slider_layout.addWidget(max_label)
layout.addLayout(slider_layout)
return section
def refresh_font_list(self):
"""Refresh the list of available fonts."""
self.logger.debug("Refreshing font list...")
self.available_fonts.clear()
try:
# Get custom fonts from font manager
custom_fonts = self.font_manager.loaded_fonts
# Get system font candidates
system_fonts = self.font_manager.get_available_system_fonts()[:10]
# Clear combo box
self.font_combo.clear()
# Add custom fonts first (highest priority)
if custom_fonts:
for font_family, font_path in custom_fonts.items():
display_name = f"{font_family} (Custom)"
self.available_fonts[display_name] = font_family
self.font_combo.addItem(display_name)
self.logger.debug(f"Added custom font: {display_name}")
# Add system fonts
for font_name in system_fonts:
display_name = f"{font_name} (System)"
self.available_fonts[display_name] = font_name
self.font_combo.addItem(display_name)
self.logger.debug(f"Added system font: {display_name}")
# Add default option with actual system font name
system_font_name = self.font_manager.get_default_system_font_name()
default_name = f"Default ({system_font_name})"
self.available_fonts[default_name] = "default"
self.font_combo.addItem(default_name)
# Select startup font
self._select_startup_font()
self.logger.info(f"Font list refreshed: {len(self.available_fonts)} fonts available")
except Exception as e:
self.logger.error(f"Error refreshing font list: {e}")
# Fallback: add default option only
self.font_combo.clear()
self.font_combo.addItem("Default (System)")
self.available_fonts = {"Default (System)": "default"}
def _select_startup_font(self):
"""Select the appropriate font on startup."""
# Use font manager's startup font selection logic
startup_font_name, startup_font_type = self.font_manager.select_startup_font()
# Find the corresponding display name in our combo box
target_display_name = None
for display_name, actual_name in self.available_fonts.items():
if actual_name == startup_font_name:
target_display_name = display_name
break
# If we found the font, select it
if target_display_name:
index = self.font_combo.findText(target_display_name)
if index >= 0:
self.font_combo.setCurrentIndex(index)
self.logger.info(f"Startup font selected: {target_display_name}")
return
# Fallback to default if we couldn't find the startup font
for i in range(self.font_combo.count()):
item_text = self.font_combo.itemText(i)
if item_text.startswith("Default ("):
self.font_combo.setCurrentIndex(i)
self.logger.info(f"Startup font fallback: {item_text}")
return
def on_font_changed(self, display_name: str):
"""Handle font selection change."""
if not display_name or display_name not in self.available_fonts:
return
actual_font_name = self.available_fonts[display_name]
# Determine font type
if "(Custom)" in display_name:
font_type = "custom"
elif "(System)" in display_name:
font_type = "system"
else:
font_type = "default"
self.logger.info(f"Font changed: {display_name} -> {actual_font_name} ({font_type})")
# Apply the font change
self._apply_font_change(actual_font_name, font_type)
# Emit signal for external listeners (including auto-regeneration)
self.fontChanged.emit(actual_font_name, font_type)
def on_font_size_changed(self, size: int):
"""Handle font size change."""
self.size_label.setText(f"Qt Font Size: {size}pt")
self.logger.info(f"Qt font size changed: {size}pt")
# Apply Qt font size change
self._apply_font_size_change(size)
# Emit signal for external listeners
self.fontSizeChanged.emit(size)
def _apply_font_change(self, font_name: str, font_type: str):
"""Apply the font change to both Qt and matplotlib."""
try:
# Use font manager's centralized font application
success = self.font_manager.apply_font_selection(font_name, font_type)
if not success:
self.logger.warning(f"Font application may have failed: {font_name}")
except Exception as e:
self.logger.error(f"Error applying font change: {e}")
def _apply_font_size_change(self, size: int):
"""Apply Qt font size change."""
try:
from PyQt5.QtCore import QCoreApplication
from PyQt5.QtGui import QFont
app = QCoreApplication.instance()
if app:
current_font = app.font()
current_font.setPointSize(size)
app.setFont(current_font)
self.logger.debug(f"Qt font size set to: {size}pt")
except Exception as e:
self.logger.error(f"Error applying font size change: {e}")
def get_current_font(self) -> tuple[str, str]:
"""
Get currently selected font.
Returns:
tuple: (font_name, font_type)
"""
display_name = self.font_combo.currentText()
if display_name in self.available_fonts:
actual_font_name = self.available_fonts[display_name]
if "(Custom)" in display_name:
font_type = "custom"
elif "(System)" in display_name:
font_type = "system"
else:
font_type = "default"
return actual_font_name, font_type
return "default", "default"
def get_current_font_size(self) -> int:
"""
Get current Qt font size.
Returns:
int: Current font size in points
"""
return self.size_slider.value()
def set_font(self, font_name: str):
"""
Programmatically set the font selection.
Args:
font_name: Name of font to select
"""
# Find matching display name
for display_name, actual_name in self.available_fonts.items():
if actual_name == font_name:
index = self.font_combo.findText(display_name)
if index >= 0:
self.font_combo.setCurrentIndex(index)
return
self.logger.warning(f"Font not found in selector: {font_name}")
def set_font_size(self, size: int):
"""
Programmatically set the font size.
Args:
size: Font size in points (will be clamped to valid range)
"""
clamped_size = max(self.min_font_size, min(self.max_font_size, size))
self.size_slider.setValue(clamped_size)
if clamped_size != size:
self.logger.warning(f"Font size clamped: {size} -> {clamped_size}")
+31
View File
@@ -501,3 +501,34 @@ def safe_title(title: str) -> str:
str: Safe title for display
"""
return get_font_manager().get_cjk_safe_title(title)
def apply_fixed_font(family: str = "M PLUS 1 Code", size: int = 10) -> str:
"""Lock the Qt application font to `family` at `size`pt.
Falls back to the system default family if `family` isn't available (loaded
from fonts/ or installed). pyqtgraph and the Qt widgets both read the app
font, so this is all the plot/UI need. Returns the family actually used.
"""
logger = logging.getLogger(__name__)
app = QCoreApplication.instance()
if app is None:
logger.warning("apply_fixed_font called before QApplication exists")
return family
try:
available = family in set(QFontDatabase().families())
except Exception:
available = False
if available:
font = QFont(family)
chosen = family
else:
font = QFont() # system default family
chosen = font.defaultFamily()
logger.info(f"Font '{family}' not found; using system default '{chosen}'")
font.setPointSize(size)
app.setFont(font)
logger.info(f"Application font locked to '{chosen}' at {size}pt")
return chosen
+27 -38
View File
@@ -9,8 +9,7 @@ from PyQt5.QtCore import Qt
from audio_visualization_widget import AudioVisualizationWidget, dataset_color
from analysis_results_manager import AnalysisResultsManager
from logger_setup import setup_logging, parse_log_args
from font_manager import initialize_fonts, get_font_manager
from font_control_widget import FontControlWidget
from font_manager import initialize_fonts, apply_fixed_font
from plot_control_widget import PlotControlWidget
from ref_line_widget import RefLineControlWidget, RefLineDialog
from metrics import METRICS
@@ -26,10 +25,12 @@ class MainWindow(QMainWindow):
self.analysis_manager = AnalysisResultsManager()
# Guards programmatic list mutations from triggering re-render storms.
self._suppress_list_signals = False
# Custom reference lines, owned here and pushed to the plot each render.
self.ref_lines: list[RefLineProps] = []
# Reference lines are kept per metric (a -14 LUFS line means nothing on a
# spectrogram), so they persist when you switch metrics and come back.
self.ref_lines_by_metric: dict[str, list[RefLineProps]] = {}
self.initUI()
self.connect_signals()
self._activate_ref_lines()
def initUI(self):
"""Initialize the user interface."""
@@ -67,12 +68,6 @@ class MainWindow(QMainWindow):
self.open_file_button.clicked.connect(self.open_file_dialog)
layout.addWidget(self.open_file_button)
# Font control cluster
self.font_control = FontControlWidget()
self.font_control.fontChanged.connect(self.on_font_changed)
self.font_control.fontSizeChanged.connect(self.on_font_size_changed)
layout.addWidget(self.font_control)
# Plot control cluster (metric selector + scale toggle + refresh)
self.plot_control = PlotControlWidget()
self.plot_control.metricChanged.connect(self.on_metric_changed)
@@ -219,28 +214,17 @@ class MainWindow(QMainWindow):
return
self._refresh_view()
def on_font_changed(self, font_name: str, font_type: str):
"""Called when font selection changes."""
self.logger.info(f"Font changed via GUI: {font_name} ({font_type})")
self._refresh_view()
def on_font_size_changed(self, font_size: int):
"""Called when Qt font size changes."""
self.logger.info(f"Qt font size changed via GUI: {font_size}pt")
# Qt font size doesn't affect the plot axes fonts directly; no redraw needed.
def on_metric_changed(self, metric_id: str):
"""Called when the metric selector changes."""
self.logger.info(f"Metric changed via GUI: {metric_id}")
# The value axis units change with the metric, so custom reference lines
# placed against the old metric no longer mean anything — drop them.
self.ref_lines.clear()
self._sync_ref_lines()
# Reference lines are kept per metric, so swap in this metric's set rather
# than discarding — switch away and back and your lines are still there.
self._activate_ref_lines()
self._refresh_view()
def on_add_reference_line(self):
"""Add a reference line at the current view centre, then edit it."""
value = self.visualization_widget.current_view_center_y()
value = self.visualization_widget.current_view_center_value()
props = RefLineProps(value=round(value, 2))
self.ref_lines.append(props)
self._sync_ref_lines()
@@ -251,7 +235,7 @@ class MainWindow(QMainWindow):
"""Open the properties dialog for a reference line."""
if not (0 <= index < len(self.ref_lines)):
return
dialog = RefLineDialog(self, self.ref_lines[index])
dialog = RefLineDialog(self, self.ref_lines[index], value_units=self._ref_value_units())
if dialog.exec_():
self.ref_lines[index] = dialog.result_props()
self._sync_ref_lines()
@@ -263,7 +247,7 @@ class MainWindow(QMainWindow):
self._sync_ref_lines()
def on_clear_reference_lines(self):
"""Remove all custom reference lines."""
"""Remove all custom reference lines for the current metric."""
self.ref_lines.clear()
self._sync_ref_lines()
@@ -271,6 +255,16 @@ class MainWindow(QMainWindow):
"""A line was dragged on the plot — its value is already updated; refresh list."""
self.ref_line_control.set_lines(self.ref_lines)
def _activate_ref_lines(self):
"""Point `self.ref_lines` at the current metric's set and sync the UI."""
metric_id = self.plot_control.current_metric_id()
self.ref_lines = self.ref_lines_by_metric.setdefault(metric_id, [])
self._sync_ref_lines()
def _ref_value_units(self) -> str:
"""Units a reference line's value is expressed in for the current metric."""
return "Hz" if self.plot_control.current_metric_id() == "spectrogram" else ""
def _sync_ref_lines(self):
"""Push the current reference-line set to both the list view and the plot."""
self.ref_line_control.set_lines(self.ref_lines)
@@ -384,17 +378,12 @@ def main():
app = QApplication(sys.argv)
# Initialize font system before creating any widgets
font_success = initialize_fonts()
if font_success:
logger.info("Font system initialized successfully")
# Log font status for debugging
font_status = get_font_manager().get_status_report()
logger.debug(f"Font status: matplotlib={font_status['matplotlib_configured']}, "
f"qt={font_status['qt_configured']}, "
f"custom_fonts={font_status['custom_fonts_loaded']}")
else:
logger.warning("Font system initialization failed - CJK characters may not display properly")
# Initialize font system (loads any fonts/ files, configures fallbacks) then
# lock the UI font. M PLUS 1 Code has full Japanese coverage, so this stays
# CJK-safe; falls back to the system default if the family isn't present.
initialize_fonts()
chosen = apply_fixed_font("M PLUS 1 Code", 10)
logger.info(f"UI font locked to '{chosen}' at 10pt")
# Set application style
app.setStyle('Fusion') # Modern cross-platform style
+11 -12
View File
@@ -1,8 +1,8 @@
"""
Plot control widget: pick which metric to display and refresh the current plot.
Plot control widget: pick the metric, set axis scale/mode, refresh the plot.
Mirrors FontControlWidget's clustered-groupbox style so the two sit naturally
next to each other in the left panel.
A clustered groupbox for the left panel: metric selector, log-frequency toggle,
relative-time toggle, and a manual refresh button.
"""
import logging
@@ -57,14 +57,13 @@ class PlotControlWidget(QWidget):
self.log_freq_check.toggled.connect(lambda _: self.viewChanged.emit())
group_layout.addWidget(self.log_freq_check)
# Time axis: absolute seconds vs relative % of each track's own length, so
# tracks of very different durations line up by song position when overlaid.
group_layout.addWidget(QLabel("Time axis:"))
self.x_mode_combo = QComboBox()
self.x_mode_combo.addItem("Absolute (seconds)", X_ABSOLUTE)
self.x_mode_combo.addItem("Relative (%)", X_RELATIVE)
self.x_mode_combo.currentIndexChanged.connect(lambda _: self.viewChanged.emit())
group_layout.addWidget(self.x_mode_combo)
# Time axis: off = absolute seconds, on = relative % of each track's own
# length, so tracks of very different durations line up by song position.
self.relative_time_check = QCheckBox("Relative time axis (%)")
self.relative_time_check.setToolTip(
"Off: time in seconds. On: 0-100% of each track's own length")
self.relative_time_check.toggled.connect(lambda _: self.viewChanged.emit())
group_layout.addWidget(self.relative_time_check)
button_row = QHBoxLayout()
self.refresh_button = QPushButton("Refresh Plot")
@@ -87,5 +86,5 @@ class PlotControlWidget(QWidget):
def current_view_state(self) -> ViewState:
return ViewState(
y_log=self.log_freq_check.isChecked(),
x_mode=self.x_mode_combo.currentData(),
x_mode=X_RELATIVE if self.relative_time_check.isChecked() else X_ABSOLUTE,
)
-1
View File
@@ -31,7 +31,6 @@ py-modules = [
"metrics",
"plotspec",
"font_manager",
"font_control_widget",
"plot_control_widget",
"ref_line_widget",
"logger_setup",
+3 -1
View File
@@ -26,7 +26,7 @@ _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):
def __init__(self, parent, props: RefLineProps, value_units: str = ""):
super().__init__(parent)
self.setWindowTitle("Reference line")
self._color = props.color
@@ -37,6 +37,8 @@ class RefLineDialog(QDialog):
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()