Improve soundpack manager UX and fix accessibility issues
- Fix confusing "Available Soundpacks" tab that only showed repository downloads - Now shows ALL usable soundpacks: bundled, installed, and downloadable - Add clear status indicators: (Bundled), (Built-in), (Installed), (Available for download) - Enable keyboard text navigation in soundpack description boxes - Add comprehensive debug logging for troubleshooting soundpack discovery - Update CLAUDE.md with critical note about print() statements in Qt apps 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@
|
|||||||
## Logging System
|
## Logging System
|
||||||
- Bifrost uses a centralized logging system configured in `bifrost.py`
|
- Bifrost uses a centralized logging system configured in `bifrost.py`
|
||||||
- **NEVER use print() statements for debugging** - always use the logging system
|
- **NEVER use print() statements for debugging** - always use the logging system
|
||||||
|
- **CRITICAL: print() statements get buried in Qt terminal spam and are useless for debugging**
|
||||||
- To enable debug logging, run bifrost with the `-d` flag:
|
- To enable debug logging, run bifrost with the `-d` flag:
|
||||||
- `DISPLAY=:0 python bifrost.py -d` for console debug output
|
- `DISPLAY=:0 python bifrost.py -d` for console debug output
|
||||||
- `DISPLAY=:0 python bifrost.py -d bifrost.log` for debug to file
|
- `DISPLAY=:0 python bifrost.py -d bifrost.log` for debug to file
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ class SettingsDialog(QDialog):
|
|||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
import logging
|
||||||
|
self.logger = logging.getLogger('bifrost.settings_dialog')
|
||||||
|
self.logger.debug("SettingsDialog.__init__() called")
|
||||||
self.settings = SettingsManager()
|
self.settings = SettingsManager()
|
||||||
self.setup_ui()
|
self.setup_ui()
|
||||||
self.load_current_settings()
|
self.load_current_settings()
|
||||||
@@ -222,37 +225,69 @@ class SettingsDialog(QDialog):
|
|||||||
|
|
||||||
def load_sound_packs(self):
|
def load_sound_packs(self):
|
||||||
"""Load available sound packs from the sounds directory"""
|
"""Load available sound packs from the sounds directory"""
|
||||||
|
self.logger.debug("load_sound_packs() called")
|
||||||
self.sound_pack_combo.clear()
|
self.sound_pack_combo.clear()
|
||||||
|
|
||||||
# Add default "None" option
|
# Add default "None" option
|
||||||
self.sound_pack_combo.addItem("None (No sounds)", "none")
|
self.sound_pack_combo.addItem("None (No sounds)", "none")
|
||||||
|
self.logger.debug("Added None option")
|
||||||
|
|
||||||
# Look for sound pack directories
|
# Look for bundled sound pack directories
|
||||||
sounds_dir = "sounds"
|
# Find sounds directory relative to the source code
|
||||||
if os.path.exists(sounds_dir):
|
import sys
|
||||||
for item in os.listdir(sounds_dir):
|
from pathlib import Path
|
||||||
pack_dir = os.path.join(sounds_dir, item)
|
|
||||||
if os.path.isdir(pack_dir):
|
# Try to find bundled sounds directory
|
||||||
pack_json = os.path.join(pack_dir, "pack.json")
|
if hasattr(sys, '_MEIPASS'):
|
||||||
if os.path.exists(pack_json):
|
# Running as PyInstaller bundle
|
||||||
# Valid sound pack
|
bundled_sounds_dir = Path(sys._MEIPASS) / "sounds"
|
||||||
self.sound_pack_combo.addItem(item, item)
|
else:
|
||||||
|
# Running from source - go up from src/widgets/ to project root
|
||||||
|
bundled_sounds_dir = Path(__file__).parent.parent.parent / "sounds"
|
||||||
|
|
||||||
|
self.logger.debug(f"bundled_sounds_dir = {bundled_sounds_dir}")
|
||||||
|
self.logger.debug(f"bundled_sounds_dir.exists() = {bundled_sounds_dir.exists()}")
|
||||||
|
if bundled_sounds_dir.exists():
|
||||||
|
self.logger.debug(f"bundled dir contents = {list(bundled_sounds_dir.iterdir())}")
|
||||||
|
for item in bundled_sounds_dir.iterdir():
|
||||||
|
if item.is_dir():
|
||||||
|
pack_json = item / "pack.json"
|
||||||
|
if pack_json.exists():
|
||||||
|
# Valid bundled sound pack
|
||||||
|
self.logger.debug(f"Adding bundled pack: {item.name}")
|
||||||
|
self.sound_pack_combo.addItem(f"{item.name} (Bundled)", item.name)
|
||||||
|
|
||||||
# Also check in XDG data directory
|
# Also check in XDG data directory
|
||||||
try:
|
try:
|
||||||
data_sounds_dir = self.settings.get_sounds_dir()
|
data_sounds_dir = self.settings.get_sounds_dir()
|
||||||
if os.path.exists(data_sounds_dir) and data_sounds_dir != sounds_dir:
|
self.logger.debug(f"data_sounds_dir = {data_sounds_dir}")
|
||||||
|
self.logger.debug(f"data_sounds_dir exists = {os.path.exists(data_sounds_dir)}")
|
||||||
|
if os.path.exists(data_sounds_dir):
|
||||||
|
self.logger.debug(f"data_sounds_dir contents = {os.listdir(data_sounds_dir)}")
|
||||||
for item in os.listdir(data_sounds_dir):
|
for item in os.listdir(data_sounds_dir):
|
||||||
pack_dir = os.path.join(data_sounds_dir, item)
|
pack_dir = os.path.join(data_sounds_dir, item)
|
||||||
if os.path.isdir(pack_dir):
|
if os.path.isdir(pack_dir):
|
||||||
pack_json = os.path.join(pack_dir, "pack.json")
|
pack_json = os.path.join(pack_dir, "pack.json")
|
||||||
if os.path.exists(pack_json):
|
if os.path.exists(pack_json):
|
||||||
# Avoid duplicates
|
# Avoid duplicates
|
||||||
if self.sound_pack_combo.findData(item) == -1:
|
existing_index = self.sound_pack_combo.findData(item)
|
||||||
|
self.logger.debug(f"Found system pack '{item}', existing_index = {existing_index}")
|
||||||
|
if existing_index == -1:
|
||||||
|
self.logger.debug(f"Adding system pack: {item}")
|
||||||
self.sound_pack_combo.addItem(f"{item} (System)", item)
|
self.sound_pack_combo.addItem(f"{item} (System)", item)
|
||||||
except Exception:
|
else:
|
||||||
|
self.logger.debug(f"Skipping duplicate pack: {item}")
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.debug(f"Exception in system pack detection: {e}")
|
||||||
pass # Ignore errors in system sound pack detection
|
pass # Ignore errors in system sound pack detection
|
||||||
|
|
||||||
|
# Show final combo box contents
|
||||||
|
self.logger.debug("Final combo box contents:")
|
||||||
|
for i in range(self.sound_pack_combo.count()):
|
||||||
|
text = self.sound_pack_combo.itemText(i)
|
||||||
|
data = self.sound_pack_combo.itemData(i)
|
||||||
|
self.logger.debug(f" {i}: '{text}' -> '{data}'")
|
||||||
|
|
||||||
def load_current_settings(self):
|
def load_current_settings(self):
|
||||||
"""Load current settings into the dialog"""
|
"""Load current settings into the dialog"""
|
||||||
# Audio settings
|
# Audio settings
|
||||||
|
|||||||
@@ -109,6 +109,13 @@ class SoundpackManagerDialog(QDialog):
|
|||||||
header_label.setAccessibleName("Available Soundpacks")
|
header_label.setAccessibleName("Available Soundpacks")
|
||||||
layout.addWidget(header_label)
|
layout.addWidget(header_label)
|
||||||
|
|
||||||
|
# Info note
|
||||||
|
info_label = QLabel("All soundpacks you can use (bundled, installed, and downloadable from repositories)")
|
||||||
|
info_label.setAccessibleName("Soundpack Information")
|
||||||
|
info_label.setWordWrap(True)
|
||||||
|
info_label.setStyleSheet("font-style: italic; color: #666; margin-bottom: 10px;")
|
||||||
|
layout.addWidget(info_label)
|
||||||
|
|
||||||
# Refresh button
|
# Refresh button
|
||||||
refresh_layout = QHBoxLayout()
|
refresh_layout = QHBoxLayout()
|
||||||
self.refresh_button = QPushButton("&Refresh Soundpack List")
|
self.refresh_button = QPushButton("&Refresh Soundpack List")
|
||||||
@@ -141,6 +148,8 @@ class SoundpackManagerDialog(QDialog):
|
|||||||
self.description_text.setAccessibleName("Soundpack description")
|
self.description_text.setAccessibleName("Soundpack description")
|
||||||
self.description_text.setReadOnly(True)
|
self.description_text.setReadOnly(True)
|
||||||
self.description_text.setMaximumHeight(150)
|
self.description_text.setMaximumHeight(150)
|
||||||
|
# Enable keyboard navigation for screen readers
|
||||||
|
self.description_text.setTextInteractionFlags(Qt.TextSelectableByKeyboard | Qt.TextSelectableByMouse)
|
||||||
details_layout.addWidget(self.description_text)
|
details_layout.addWidget(self.description_text)
|
||||||
|
|
||||||
# Action buttons
|
# Action buttons
|
||||||
@@ -307,7 +316,7 @@ class SoundpackManagerDialog(QDialog):
|
|||||||
self.current_pack_label.setText(f"Current soundpack: {current_pack}")
|
self.current_pack_label.setText(f"Current soundpack: {current_pack}")
|
||||||
|
|
||||||
def refresh_soundpacks(self):
|
def refresh_soundpacks(self):
|
||||||
"""Refresh available soundpacks from repositories"""
|
"""Refresh available soundpacks from repositories and local sources"""
|
||||||
if self.current_thread and self.current_thread.isRunning():
|
if self.current_thread and self.current_thread.isRunning():
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -325,26 +334,93 @@ class SoundpackManagerDialog(QDialog):
|
|||||||
self.refresh_button.setText("&Refresh Soundpack List")
|
self.refresh_button.setText("&Refresh Soundpack List")
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
self.soundpacks = self.manager.discover_soundpacks()
|
# Get repository soundpacks
|
||||||
|
repository_soundpacks = self.manager.discover_soundpacks()
|
||||||
|
|
||||||
|
# Add local/bundled soundpacks
|
||||||
|
self.soundpacks = self._get_all_available_soundpacks(repository_soundpacks)
|
||||||
self.update_soundpack_list()
|
self.update_soundpack_list()
|
||||||
else:
|
else:
|
||||||
AccessibleTextDialog.show_error("Discovery Failed", "Failed to discover soundpacks", message, self)
|
AccessibleTextDialog.show_error("Discovery Failed", "Failed to discover soundpacks", message, self)
|
||||||
|
|
||||||
|
def _get_all_available_soundpacks(self, repository_soundpacks):
|
||||||
|
"""Get all available soundpacks: bundled, installed, and from repositories"""
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
all_soundpacks = []
|
||||||
|
repository_names = [sp.name for sp in repository_soundpacks]
|
||||||
|
|
||||||
|
# Add repository soundpacks first
|
||||||
|
all_soundpacks.extend(repository_soundpacks)
|
||||||
|
|
||||||
|
# Find bundled soundpacks
|
||||||
|
if hasattr(sys, '_MEIPASS'):
|
||||||
|
bundled_sounds_dir = Path(sys._MEIPASS) / "sounds"
|
||||||
|
else:
|
||||||
|
bundled_sounds_dir = Path(__file__).parent.parent.parent / "sounds"
|
||||||
|
|
||||||
|
if bundled_sounds_dir.exists():
|
||||||
|
for item in bundled_sounds_dir.iterdir():
|
||||||
|
if item.is_dir():
|
||||||
|
pack_json = item / "pack.json"
|
||||||
|
if pack_json.exists() and item.name not in repository_names:
|
||||||
|
try:
|
||||||
|
with open(pack_json) as f:
|
||||||
|
pack_data = json.load(f)
|
||||||
|
|
||||||
|
# Create SoundpackInfo for bundled pack
|
||||||
|
bundled_pack = SoundpackInfo(
|
||||||
|
name=item.name,
|
||||||
|
description=f"{pack_data.get('description', 'Bundled soundpack')} (Bundled)",
|
||||||
|
repository_url="Bundled with application",
|
||||||
|
download_url="",
|
||||||
|
installed=True, # Bundled packs are always "available"
|
||||||
|
version=pack_data.get('version', '1.0')
|
||||||
|
)
|
||||||
|
all_soundpacks.append(bundled_pack)
|
||||||
|
except (json.JSONDecodeError, FileNotFoundError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Add None option as a special soundpack
|
||||||
|
none_pack = SoundpackInfo(
|
||||||
|
name="none",
|
||||||
|
description="No sounds (silence)",
|
||||||
|
repository_url="Built-in option",
|
||||||
|
download_url="",
|
||||||
|
installed=True,
|
||||||
|
version="1.0"
|
||||||
|
)
|
||||||
|
all_soundpacks.insert(0, none_pack) # Put None at the top
|
||||||
|
|
||||||
|
return all_soundpacks
|
||||||
|
|
||||||
def update_soundpack_list(self):
|
def update_soundpack_list(self):
|
||||||
"""Update the soundpack list display"""
|
"""Update the soundpack list display"""
|
||||||
self.soundpack_list.clear()
|
self.soundpack_list.clear()
|
||||||
|
|
||||||
# Add header item to help Orca read single-item lists
|
# Add header item to help Orca read single-item lists
|
||||||
header_item = QListWidgetItem(f"Available Soundpacks ({len(self.soundpacks)} found):")
|
header_item = QListWidgetItem(f"Available Soundpacks ({len(self.soundpacks)} total):")
|
||||||
header_item.setFlags(header_item.flags() & ~Qt.ItemIsSelectable) # Make it non-selectable
|
header_item.setFlags(header_item.flags() & ~Qt.ItemIsSelectable) # Make it non-selectable
|
||||||
header_item.setData(Qt.AccessibleDescriptionRole, "Available soundpacks list header")
|
header_item.setData(Qt.AccessibleDescriptionRole, "Available soundpacks list header")
|
||||||
self.soundpack_list.addItem(header_item)
|
self.soundpack_list.addItem(header_item)
|
||||||
|
|
||||||
for soundpack in self.soundpacks:
|
for soundpack in self.soundpacks:
|
||||||
item_text = soundpack.name
|
item_text = soundpack.name
|
||||||
if soundpack.installed:
|
|
||||||
item_text += " (Installed)"
|
# Add status indicators
|
||||||
item_text += f" - {soundpack.description}"
|
if soundpack.repository_url == "Bundled with application":
|
||||||
|
status = " (Bundled)"
|
||||||
|
elif soundpack.repository_url == "Built-in option":
|
||||||
|
status = " (Built-in)"
|
||||||
|
elif soundpack.installed:
|
||||||
|
status = " (Installed)"
|
||||||
|
else:
|
||||||
|
status = " (Available for download)"
|
||||||
|
|
||||||
|
item_text += status + f" - {soundpack.description}"
|
||||||
|
|
||||||
item = QListWidgetItem(item_text)
|
item = QListWidgetItem(item_text)
|
||||||
item.setData(Qt.UserRole, soundpack)
|
item.setData(Qt.UserRole, soundpack)
|
||||||
@@ -373,7 +449,7 @@ class SoundpackManagerDialog(QDialog):
|
|||||||
# Update details
|
# Update details
|
||||||
status = "Installed" if soundpack.installed else "Not installed"
|
status = "Installed" if soundpack.installed else "Not installed"
|
||||||
self.details_label.setText(f"Name: {soundpack.name}\nStatus: {status}\nRepository: {soundpack.repository_url}")
|
self.details_label.setText(f"Name: {soundpack.name}\nStatus: {status}\nRepository: {soundpack.repository_url}")
|
||||||
self.description_text.setText(soundpack.description)
|
self.description_text.setText(f"Sound pack description:\n{soundpack.description}")
|
||||||
|
|
||||||
# Update buttons
|
# Update buttons
|
||||||
self.install_button.setEnabled(not soundpack.installed)
|
self.install_button.setEnabled(not soundpack.installed)
|
||||||
|
|||||||
Reference in New Issue
Block a user