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
+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."""
@@ -66,12 +67,6 @@ class MainWindow(QMainWindow):
self.open_file_button = QPushButton("Open Audio File...")
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()
@@ -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