265e8254cd
- Add open file button for direct audio file selection - Implement clustered FontControlWidget with font selector, size slider, and refresh controls - Add auto-regeneration of matplotlib plots when fonts change - Enhance FontManager with system font discovery and startup selection logic - Replace individual font selector with comprehensive font control interface - Support both custom fonts and system font candidates with proper prioritization 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
336 lines
12 KiB
Python
336 lines
12 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, QPushButton, 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
|
|
- Plot regeneration controls
|
|
"""
|
|
|
|
# Signals
|
|
fontChanged = pyqtSignal(str, str) # (font_name, font_type)
|
|
fontSizeChanged = pyqtSignal(int) # font_size
|
|
plotRefreshRequested = pyqtSignal() # manual refresh request
|
|
|
|
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 = 8
|
|
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)
|
|
|
|
# Control buttons section
|
|
button_section = self._create_button_section()
|
|
group_layout.addWidget(button_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 _create_button_section(self) -> QWidget:
|
|
"""Create the control buttons section."""
|
|
section = QWidget()
|
|
layout = QHBoxLayout(section)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
# Refresh plot button
|
|
self.refresh_button = QPushButton("Refresh Plot")
|
|
self.refresh_button.setToolTip("Regenerate current plot with new font settings")
|
|
self.refresh_button.clicked.connect(self.on_refresh_plot_clicked)
|
|
layout.addWidget(self.refresh_button)
|
|
|
|
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 on_refresh_plot_clicked(self):
|
|
"""Handle manual plot refresh button click."""
|
|
self.logger.info("Manual plot refresh requested")
|
|
self.plotRefreshRequested.emit()
|
|
|
|
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}") |