Add unified font control system with auto-regeneration
- 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>
This commit is contained in:
@@ -0,0 +1,336 @@
|
|||||||
|
"""
|
||||||
|
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}")
|
||||||
+147
-1
@@ -127,7 +127,15 @@ class FontManager:
|
|||||||
|
|
||||||
if fonts_loaded > 0:
|
if fonts_loaded > 0:
|
||||||
# Clear matplotlib's font cache to recognize new fonts
|
# Clear matplotlib's font cache to recognize new fonts
|
||||||
fm.fontManager._load_fontmanager(try_read_cache=False)
|
try:
|
||||||
|
# Try the common method for refreshing font cache
|
||||||
|
if hasattr(fm.fontManager, '_load_fontmanager'):
|
||||||
|
fm.fontManager._load_fontmanager(try_read_cache=False)
|
||||||
|
else:
|
||||||
|
# For newer matplotlib versions, just reinitialize
|
||||||
|
fm.fontManager.__init__()
|
||||||
|
except Exception as font_cache_error:
|
||||||
|
self.logger.debug(f"Font cache refresh failed (non-critical): {font_cache_error}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Error loading custom fonts: {e}")
|
self.logger.error(f"Error loading custom fonts: {e}")
|
||||||
@@ -296,6 +304,138 @@ Recommended fonts: {', '.join(self.system_font_fallbacks.get(platform.system(),
|
|||||||
"""
|
"""
|
||||||
return instructions
|
return instructions
|
||||||
|
|
||||||
|
def get_available_system_fonts(self) -> List[str]:
|
||||||
|
"""
|
||||||
|
Get list of available system fonts that are good candidates for selection.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[str]: List of available system font names
|
||||||
|
"""
|
||||||
|
current_system = platform.system()
|
||||||
|
fallback_fonts = self.system_font_fallbacks.get(current_system,
|
||||||
|
self.system_font_fallbacks['Linux'])
|
||||||
|
|
||||||
|
# Combine all font categories
|
||||||
|
preferred_fonts = []
|
||||||
|
for category in ['sans-serif', 'serif', 'monospace']:
|
||||||
|
preferred_fonts.extend(fallback_fonts.get(category, []))
|
||||||
|
|
||||||
|
# Get actually available fonts on the system
|
||||||
|
available_fonts = set(fm.get_font_names())
|
||||||
|
|
||||||
|
# Filter to only fonts that are actually available
|
||||||
|
system_candidates = []
|
||||||
|
for font_name in preferred_fonts:
|
||||||
|
if font_name in available_fonts:
|
||||||
|
system_candidates.append(font_name)
|
||||||
|
|
||||||
|
# Remove duplicates while preserving order
|
||||||
|
seen = set()
|
||||||
|
unique_candidates = []
|
||||||
|
for font in system_candidates:
|
||||||
|
if font not in seen:
|
||||||
|
seen.add(font)
|
||||||
|
unique_candidates.append(font)
|
||||||
|
|
||||||
|
return unique_candidates
|
||||||
|
|
||||||
|
def get_default_system_font_name(self) -> str:
|
||||||
|
"""
|
||||||
|
Get the name of the default system font for display purposes.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Human-readable name of the default system font
|
||||||
|
"""
|
||||||
|
from PyQt5.QtGui import QFont
|
||||||
|
from PyQt5.QtCore import QCoreApplication
|
||||||
|
|
||||||
|
# Try to get the actual default font name from Qt
|
||||||
|
app = QCoreApplication.instance()
|
||||||
|
if app:
|
||||||
|
default_font = QFont()
|
||||||
|
return default_font.family()
|
||||||
|
|
||||||
|
# Fallback to platform-specific defaults
|
||||||
|
current_system = platform.system()
|
||||||
|
if current_system == 'Windows':
|
||||||
|
return 'Segoe UI'
|
||||||
|
elif current_system == 'Darwin':
|
||||||
|
return 'SF Pro Display'
|
||||||
|
else:
|
||||||
|
return 'DejaVu Sans'
|
||||||
|
|
||||||
|
def select_startup_font(self) -> tuple[str, str]:
|
||||||
|
"""
|
||||||
|
Select the appropriate font for application startup.
|
||||||
|
Priority: First custom font alphabetically, then first system font, then default.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (font_name, font_type) where font_type is 'custom', 'system', or 'default'
|
||||||
|
"""
|
||||||
|
# Check for custom fonts first
|
||||||
|
if self.loaded_fonts:
|
||||||
|
custom_font_names = sorted(self.loaded_fonts.keys())
|
||||||
|
selected_font = custom_font_names[0]
|
||||||
|
self.logger.info(f"Startup font selected (custom): {selected_font}")
|
||||||
|
return selected_font, 'custom'
|
||||||
|
|
||||||
|
# Check for system fonts
|
||||||
|
system_fonts = self.get_available_system_fonts()
|
||||||
|
if system_fonts:
|
||||||
|
selected_font = system_fonts[0]
|
||||||
|
self.logger.info(f"Startup font selected (system): {selected_font}")
|
||||||
|
return selected_font, 'system'
|
||||||
|
|
||||||
|
# Default fallback
|
||||||
|
default_font = self.get_default_system_font_name()
|
||||||
|
self.logger.info(f"Startup font selected (default): {default_font}")
|
||||||
|
return default_font, 'default'
|
||||||
|
|
||||||
|
def apply_font_selection(self, font_name: str, font_type: str) -> bool:
|
||||||
|
"""
|
||||||
|
Apply a font selection to both matplotlib and Qt.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
font_name: Name of the font to apply
|
||||||
|
font_type: Type of font ('custom', 'system', or 'default')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if successful
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if font_type == 'default':
|
||||||
|
# Reset to default configuration
|
||||||
|
self._configure_matplotlib()
|
||||||
|
self._configure_qt()
|
||||||
|
else:
|
||||||
|
# Apply specific font
|
||||||
|
self._set_specific_font(font_name)
|
||||||
|
|
||||||
|
self.logger.info(f"Font applied successfully: {font_name} ({font_type})")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error applying font selection: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _set_specific_font(self, font_name: str):
|
||||||
|
"""Set a specific font for both matplotlib and Qt."""
|
||||||
|
from PyQt5.QtCore import QCoreApplication
|
||||||
|
from PyQt5.QtGui import QFont
|
||||||
|
|
||||||
|
# Update Qt font
|
||||||
|
app = QCoreApplication.instance()
|
||||||
|
if app:
|
||||||
|
font = QFont(font_name)
|
||||||
|
app.setFont(font)
|
||||||
|
|
||||||
|
# Update matplotlib font (insert at front of font list)
|
||||||
|
current_fonts = plt.rcParams['font.sans-serif'].copy()
|
||||||
|
if font_name in current_fonts:
|
||||||
|
current_fonts.remove(font_name)
|
||||||
|
current_fonts.insert(0, font_name)
|
||||||
|
plt.rcParams['font.sans-serif'] = current_fonts
|
||||||
|
|
||||||
def get_status_report(self) -> Dict[str, Any]:
|
def get_status_report(self) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Generate a status report of the font system.
|
Generate a status report of the font system.
|
||||||
@@ -303,11 +443,17 @@ Recommended fonts: {', '.join(self.system_font_fallbacks.get(platform.system(),
|
|||||||
Returns:
|
Returns:
|
||||||
dict: Status information
|
dict: Status information
|
||||||
"""
|
"""
|
||||||
|
startup_font, startup_type = self.select_startup_font()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'matplotlib_configured': self._matplotlib_configured,
|
'matplotlib_configured': self._matplotlib_configured,
|
||||||
'qt_configured': self._qt_configured,
|
'qt_configured': self._qt_configured,
|
||||||
'custom_fonts_loaded': len(self.loaded_fonts),
|
'custom_fonts_loaded': len(self.loaded_fonts),
|
||||||
'custom_font_families': list(self.loaded_fonts.keys()),
|
'custom_font_families': list(self.loaded_fonts.keys()),
|
||||||
|
'available_system_fonts': self.get_available_system_fonts(),
|
||||||
|
'default_system_font': self.get_default_system_font_name(),
|
||||||
|
'startup_font': startup_font,
|
||||||
|
'startup_font_type': startup_type,
|
||||||
'fonts_directory_exists': self.fonts_dir.exists(),
|
'fonts_directory_exists': self.fonts_dir.exists(),
|
||||||
'fonts_directory_path': str(self.fonts_dir.absolute()),
|
'fonts_directory_path': str(self.fonts_dir.absolute()),
|
||||||
'current_matplotlib_fonts': plt.rcParams.get('font.sans-serif', [])[:5],
|
'current_matplotlib_fonts': plt.rcParams.get('font.sans-serif', [])[:5],
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
"""
|
||||||
|
Font selector widget providing GUI interface to font management system.
|
||||||
|
Self-contained widget that can be placed anywhere in the layout.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import List, Dict, Optional
|
||||||
|
from PyQt5.QtWidgets import QWidget, QComboBox, QVBoxLayout, QLabel
|
||||||
|
from PyQt5.QtCore import pyqtSignal
|
||||||
|
|
||||||
|
from font_manager import get_font_manager
|
||||||
|
|
||||||
|
|
||||||
|
class FontSelectorWidget(QWidget):
|
||||||
|
"""
|
||||||
|
Self-contained font selector widget.
|
||||||
|
|
||||||
|
Provides a dropdown interface to select fonts from available
|
||||||
|
custom fonts and system fonts. Integrates with the FontManager
|
||||||
|
backend for font discovery and application.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Signal emitted when font selection changes
|
||||||
|
fontChanged = pyqtSignal(str, str) # (font_name, font_type)
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
"""Initialize the font selector 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
|
||||||
|
|
||||||
|
self.initUI()
|
||||||
|
self.refresh_font_list()
|
||||||
|
|
||||||
|
def initUI(self):
|
||||||
|
"""Initialize the user interface."""
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(0, 0, 0, 0) # Minimal margins for embedding
|
||||||
|
|
||||||
|
# Label
|
||||||
|
self.label = QLabel("Font:")
|
||||||
|
layout.addWidget(self.label)
|
||||||
|
|
||||||
|
# Font selector dropdown
|
||||||
|
self.font_combo = QComboBox()
|
||||||
|
self.font_combo.currentTextChanged.connect(self.on_font_changed)
|
||||||
|
layout.addWidget(self.font_combo)
|
||||||
|
|
||||||
|
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._get_system_font_candidates()
|
||||||
|
|
||||||
|
# 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 _get_system_font_candidates(self) -> List[str]:
|
||||||
|
"""
|
||||||
|
Get list of available system fonts that are good candidates.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[str]: List of available system font names
|
||||||
|
"""
|
||||||
|
# Use font manager's enhanced system font discovery
|
||||||
|
return self.font_manager.get_available_system_fonts()[:10] # Limit to reasonable number
|
||||||
|
|
||||||
|
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
|
||||||
|
# Find any item that starts with "Default ("
|
||||||
|
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 any external listeners
|
||||||
|
self.fontChanged.emit(actual_font_name, font_type)
|
||||||
|
|
||||||
|
def _apply_font_change(self, font_name: str, font_type: str):
|
||||||
|
"""Apply the font change to the application."""
|
||||||
|
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 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 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}")
|
||||||
@@ -3,13 +3,14 @@ import os
|
|||||||
import logging
|
import logging
|
||||||
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
|
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
|
||||||
QHBoxLayout, QSplitter, QLabel, QListWidget,
|
QHBoxLayout, QSplitter, QLabel, QListWidget,
|
||||||
QTextEdit, QListWidgetItem)
|
QTextEdit, QListWidgetItem, QPushButton, QFileDialog)
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
from audio_visualization_widget import AudioVisualizationWidget
|
from audio_visualization_widget import AudioVisualizationWidget
|
||||||
from analysis_results_manager import AnalysisResultsManager
|
from analysis_results_manager import AnalysisResultsManager
|
||||||
from logger_setup import setup_logging, parse_log_args
|
from logger_setup import setup_logging, parse_log_args
|
||||||
from font_manager import initialize_fonts, get_font_manager
|
from font_manager import initialize_fonts, get_font_manager
|
||||||
|
from font_control_widget import FontControlWidget
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(QMainWindow):
|
class MainWindow(QMainWindow):
|
||||||
@@ -53,6 +54,18 @@ class MainWindow(QMainWindow):
|
|||||||
panel = QWidget()
|
panel = QWidget()
|
||||||
layout = QVBoxLayout(panel)
|
layout = QVBoxLayout(panel)
|
||||||
|
|
||||||
|
# Open File button
|
||||||
|
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)
|
||||||
|
self.font_control.plotRefreshRequested.connect(self.on_plot_refresh_requested)
|
||||||
|
layout.addWidget(self.font_control)
|
||||||
|
|
||||||
# File list
|
# File list
|
||||||
self.file_list_label = QLabel("Analyzed Files:")
|
self.file_list_label = QLabel("Analyzed Files:")
|
||||||
layout.addWidget(self.file_list_label)
|
layout.addWidget(self.file_list_label)
|
||||||
@@ -116,6 +129,19 @@ class MainWindow(QMainWindow):
|
|||||||
self.visualization_widget.set_status("No audio files detected in drop")
|
self.visualization_widget.set_status("No audio files detected in drop")
|
||||||
self.logger.warning("No supported audio files found in drop")
|
self.logger.warning("No supported audio files found in drop")
|
||||||
|
|
||||||
|
def open_file_dialog(self):
|
||||||
|
"""Open file dialog to select audio files for analysis."""
|
||||||
|
file_path, _ = QFileDialog.getOpenFileName(
|
||||||
|
self,
|
||||||
|
"Select Audio File",
|
||||||
|
"", # Default directory (empty = current directory)
|
||||||
|
"Audio Files (*.mp3 *.wav *.flac);;All Files (*)"
|
||||||
|
)
|
||||||
|
|
||||||
|
if file_path: # User selected a file (didn't cancel)
|
||||||
|
self.logger.info(f"File selected via dialog: {os.path.basename(file_path)}")
|
||||||
|
self.analysis_manager.analyze_file(file_path)
|
||||||
|
|
||||||
def on_analysis_started(self, file_path):
|
def on_analysis_started(self, file_path):
|
||||||
"""Called when analysis starts."""
|
"""Called when analysis starts."""
|
||||||
filename = os.path.basename(file_path)
|
filename = os.path.basename(file_path)
|
||||||
@@ -172,6 +198,45 @@ class MainWindow(QMainWindow):
|
|||||||
# Update metadata display
|
# Update metadata display
|
||||||
metadata_text = self.analysis_manager.get_metadata_text(file_path)
|
metadata_text = self.analysis_manager.get_metadata_text(file_path)
|
||||||
self.metadata_display.setText(metadata_text)
|
self.metadata_display.setText(metadata_text)
|
||||||
|
|
||||||
|
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})")
|
||||||
|
# Auto-regenerate current plot with new font
|
||||||
|
self._regenerate_current_plot()
|
||||||
|
|
||||||
|
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 matplotlib plots, so no regeneration needed
|
||||||
|
|
||||||
|
def on_plot_refresh_requested(self):
|
||||||
|
"""Called when manual plot refresh is requested."""
|
||||||
|
self.logger.info("Manual plot refresh requested via GUI")
|
||||||
|
self._regenerate_current_plot()
|
||||||
|
|
||||||
|
def _regenerate_current_plot(self):
|
||||||
|
"""Regenerate the current plot with updated font settings."""
|
||||||
|
try:
|
||||||
|
# Get the currently selected file
|
||||||
|
current_item = self.file_list.currentItem()
|
||||||
|
if not current_item:
|
||||||
|
self.logger.debug("No file selected for plot regeneration")
|
||||||
|
return
|
||||||
|
|
||||||
|
file_path = current_item.data(Qt.UserRole)
|
||||||
|
if not file_path:
|
||||||
|
self.logger.debug("No file path found for current selection")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.logger.info(f"Regenerating plot for: {os.path.basename(file_path)}")
|
||||||
|
|
||||||
|
# Re-analyze the file to regenerate plots with new font
|
||||||
|
self.analysis_manager.analyze_file(file_path)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error regenerating plot: {e}")
|
||||||
|
self.visualization_widget.set_status(f"Error regenerating plot: {e}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
Reference in New Issue
Block a user