Files
uj-mastering-master/font_control_widget.py
T
Mikkeli Matlock 7bdf465799 Add pluggable metrics architecture with LUFS
Introduce a Metric ABC (compute on worker thread, render on GUI thread)
with a METRICS registry, and refactor the analysis pipeline around it.
RMS power (ported), raw waveform, and BS.1770 LUFS (via pyloudnorm) ship
as the initial three; new metrics drop in by appending to METRICS.

- metrics.py: Metric ABC + RMSPowerMetric, WaveformMetric (locked to
  +/-1.1 y-range for float headroom), LUFSMetric (short-term 3 s window
  + integrated value, with streaming-target reference line).
- plot_control_widget.py: metric selector dropdown + Refresh Plot button
  (moved out of FontControlWidget).
- analysis_results_manager.py: AnalysisResult caches the AudioFile and a
  per-metric data dict; new MetricComputeWorker runs metric switches off
  the GUI thread via metricComputeStarted/metricReady/metricComputeError
  signals, so LUFS on a 12-minute track no longer stalls the UI.
- main.py: all redraw paths funnel through one _render_or_request helper;
  stale-result guards keep slow computes from overwriting fresh selections.
- plotting_engine.py removed (metadata text moved onto AnalysisResult;
  figure construction lives in each Metric).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 00:42:45 +09:00

311 lines
11 KiB
Python

"""
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}")