From aa65a0734555aeeb24e880697abee69ddf7fe62b Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Sun, 17 Aug 2025 02:29:02 -0400 Subject: [PATCH] Improve soundpack manager UX and fix accessibility issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- CLAUDE.md | 1 + src/widgets/settings_dialog.py | 61 +++++++++++++---- src/widgets/soundpack_manager_dialog.py | 90 +++++++++++++++++++++++-- 3 files changed, 132 insertions(+), 20 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d61b095..fa85d8c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,6 +10,7 @@ ## Logging System - Bifrost uses a centralized logging system configured in `bifrost.py` - **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: - `DISPLAY=:0 python bifrost.py -d` for console debug output - `DISPLAY=:0 python bifrost.py -d bifrost.log` for debug to file diff --git a/src/widgets/settings_dialog.py b/src/widgets/settings_dialog.py index d532749..d2b534b 100644 --- a/src/widgets/settings_dialog.py +++ b/src/widgets/settings_dialog.py @@ -21,6 +21,9 @@ class SettingsDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) + import logging + self.logger = logging.getLogger('bifrost.settings_dialog') + self.logger.debug("SettingsDialog.__init__() called") self.settings = SettingsManager() self.setup_ui() self.load_current_settings() @@ -222,37 +225,69 @@ class SettingsDialog(QDialog): def load_sound_packs(self): """Load available sound packs from the sounds directory""" + self.logger.debug("load_sound_packs() called") self.sound_pack_combo.clear() # Add default "None" option self.sound_pack_combo.addItem("None (No sounds)", "none") + self.logger.debug("Added None option") - # Look for sound pack directories - sounds_dir = "sounds" - if os.path.exists(sounds_dir): - for item in os.listdir(sounds_dir): - pack_dir = os.path.join(sounds_dir, item) - if os.path.isdir(pack_dir): - pack_json = os.path.join(pack_dir, "pack.json") - if os.path.exists(pack_json): - # Valid sound pack - self.sound_pack_combo.addItem(item, item) + # Look for bundled sound pack directories + # Find sounds directory relative to the source code + import sys + from pathlib import Path + + # Try to find bundled sounds directory + if hasattr(sys, '_MEIPASS'): + # Running as PyInstaller bundle + bundled_sounds_dir = Path(sys._MEIPASS) / "sounds" + 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 try: 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): pack_dir = os.path.join(data_sounds_dir, item) if os.path.isdir(pack_dir): pack_json = os.path.join(pack_dir, "pack.json") if os.path.exists(pack_json): # 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) - 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 + # 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): """Load current settings into the dialog""" # Audio settings diff --git a/src/widgets/soundpack_manager_dialog.py b/src/widgets/soundpack_manager_dialog.py index ce8ea44..866eeff 100644 --- a/src/widgets/soundpack_manager_dialog.py +++ b/src/widgets/soundpack_manager_dialog.py @@ -109,6 +109,13 @@ class SoundpackManagerDialog(QDialog): header_label.setAccessibleName("Available Soundpacks") 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_layout = QHBoxLayout() self.refresh_button = QPushButton("&Refresh Soundpack List") @@ -141,6 +148,8 @@ class SoundpackManagerDialog(QDialog): self.description_text.setAccessibleName("Soundpack description") self.description_text.setReadOnly(True) 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) # Action buttons @@ -307,7 +316,7 @@ class SoundpackManagerDialog(QDialog): self.current_pack_label.setText(f"Current soundpack: {current_pack}") 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(): return @@ -325,26 +334,93 @@ class SoundpackManagerDialog(QDialog): self.refresh_button.setText("&Refresh Soundpack List") 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() else: 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): """Update the soundpack list display""" self.soundpack_list.clear() # 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.setData(Qt.AccessibleDescriptionRole, "Available soundpacks list header") self.soundpack_list.addItem(header_item) for soundpack in self.soundpacks: item_text = soundpack.name - if soundpack.installed: - item_text += " (Installed)" - item_text += f" - {soundpack.description}" + + # Add status indicators + 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.setData(Qt.UserRole, soundpack) @@ -373,7 +449,7 @@ class SoundpackManagerDialog(QDialog): # Update details status = "Installed" if soundpack.installed else "Not installed" 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 self.install_button.setEnabled(not soundpack.installed)