""" Settings dialog for Bifrost configuration """ import os from PySide6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QLabel, QComboBox, QPushButton, QDialogButtonBox, QGroupBox, QCheckBox, QSpinBox, QTabWidget, QWidget ) from PySide6.QtCore import Qt, Signal from config.settings import SettingsManager from accessibility.accessible_combo import AccessibleComboBox class SettingsDialog(QDialog): """Main settings dialog for Bifrost""" settings_changed = Signal() # Emitted when settings are saved def __init__(self, parent=None): super().__init__(parent) self.settings = SettingsManager() self.setup_ui() self.load_current_settings() def setup_ui(self): """Initialize the settings dialog UI""" self.setWindowTitle("Bifrost Settings") self.setMinimumSize(500, 400) self.setModal(True) layout = QVBoxLayout(self) # Create tab widget for organized settings self.tabs = QTabWidget() layout.addWidget(self.tabs) # Audio settings tab self.setup_audio_tab() # Desktop notifications tab self.setup_notifications_tab() # Accessibility settings tab self.setup_accessibility_tab() # Button box button_box = QDialogButtonBox( QDialogButtonBox.Ok | QDialogButtonBox.Cancel | QDialogButtonBox.Apply ) button_box.accepted.connect(self.save_and_close) button_box.rejected.connect(self.reject) button_box.button(QDialogButtonBox.Apply).clicked.connect(self.apply_settings) layout.addWidget(button_box) def setup_audio_tab(self): """Set up the audio settings tab""" audio_widget = QWidget() layout = QVBoxLayout(audio_widget) # Sound pack selection sound_group = QGroupBox("Sound Pack") sound_layout = QFormLayout(sound_group) self.sound_pack_combo = AccessibleComboBox() self.sound_pack_combo.setAccessibleName("Sound Pack Selection") self.sound_pack_combo.setAccessibleDescription("Choose which sound pack to use for audio feedback") # Populate sound packs self.load_sound_packs() sound_layout.addRow("Sound Pack:", self.sound_pack_combo) layout.addWidget(sound_group) # Audio volume settings volume_group = QGroupBox("Volume Settings") volume_layout = QFormLayout(volume_group) self.sound_pack_volume = QSpinBox() self.sound_pack_volume.setRange(0, 100) self.sound_pack_volume.setSuffix("%") self.sound_pack_volume.setAccessibleName("Sound Pack Volume") self.sound_pack_volume.setAccessibleDescription("Volume for all sounds from the selected sound pack") volume_layout.addRow("Sound Pack Volume:", self.sound_pack_volume) layout.addWidget(volume_group) # Note about sound pack selection note_label = QLabel("To disable sounds, select 'None' as the sound pack above.") note_label.setStyleSheet("font-style: italic; color: #666;") layout.addWidget(note_label) layout.addStretch() self.tabs.addTab(audio_widget, "&Audio") def setup_notifications_tab(self): """Set up the desktop notifications settings tab""" notifications_widget = QWidget() layout = QVBoxLayout(notifications_widget) # Desktop notifications group desktop_group = QGroupBox("Desktop Notifications") desktop_layout = QVBoxLayout(desktop_group) self.enable_desktop_notifications = QCheckBox("Enable desktop notifications") self.enable_desktop_notifications.setAccessibleName("Enable Desktop Notifications") self.enable_desktop_notifications.setAccessibleDescription("Show desktop notifications for various events") desktop_layout.addWidget(self.enable_desktop_notifications) # Notification types types_group = QGroupBox("Notification Types") types_layout = QVBoxLayout(types_group) self.notify_direct_messages = QCheckBox("Direct/Private messages") self.notify_direct_messages.setAccessibleName("Direct Message Notifications") self.notify_direct_messages.setAccessibleDescription("Show notifications for direct messages") types_layout.addWidget(self.notify_direct_messages) self.notify_mentions = QCheckBox("Mentions") self.notify_mentions.setAccessibleName("Mention Notifications") self.notify_mentions.setAccessibleDescription("Show notifications when you are mentioned") types_layout.addWidget(self.notify_mentions) self.notify_boosts = QCheckBox("Boosts/Reblogs") self.notify_boosts.setAccessibleName("Boost Notifications") self.notify_boosts.setAccessibleDescription("Show notifications when your posts are boosted") types_layout.addWidget(self.notify_boosts) self.notify_favorites = QCheckBox("Favorites") self.notify_favorites.setAccessibleName("Favorite Notifications") self.notify_favorites.setAccessibleDescription("Show notifications when your posts are favorited") types_layout.addWidget(self.notify_favorites) self.notify_follows = QCheckBox("New followers") self.notify_follows.setAccessibleName("Follow Notifications") self.notify_follows.setAccessibleDescription("Show notifications for new followers") types_layout.addWidget(self.notify_follows) self.notify_timeline_updates = QCheckBox("Timeline updates") self.notify_timeline_updates.setAccessibleName("Timeline Update Notifications") self.notify_timeline_updates.setAccessibleDescription("Show notifications for new posts in timeline") types_layout.addWidget(self.notify_timeline_updates) layout.addWidget(desktop_group) layout.addWidget(types_group) layout.addStretch() self.tabs.addTab(notifications_widget, "&Notifications") def setup_accessibility_tab(self): """Set up the accessibility settings tab""" accessibility_widget = QWidget() layout = QVBoxLayout(accessibility_widget) # Navigation settings nav_group = QGroupBox("Navigation Settings") nav_layout = QFormLayout(nav_group) self.page_step_size = QSpinBox() self.page_step_size.setRange(1, 20) self.page_step_size.setAccessibleName("Page Step Size") self.page_step_size.setAccessibleDescription("Number of posts to jump when using Page Up/Down") nav_layout.addRow("Page Step Size:", self.page_step_size) layout.addWidget(nav_group) # Screen reader options sr_group = QGroupBox("Screen Reader Options") sr_layout = QVBoxLayout(sr_group) self.verbose_announcements = QCheckBox("Verbose announcements") self.verbose_announcements.setAccessibleName("Verbose Announcements") self.verbose_announcements.setAccessibleDescription("Provide detailed descriptions for screen readers") sr_layout.addWidget(self.verbose_announcements) self.announce_thread_state = QCheckBox("Announce thread expand/collapse state") self.announce_thread_state.setAccessibleName("Thread State Announcements") self.announce_thread_state.setAccessibleDescription("Announce when threads are expanded or collapsed") sr_layout.addWidget(self.announce_thread_state) layout.addWidget(sr_group) # Timeline settings timeline_group = QGroupBox("Timeline Settings") timeline_layout = QFormLayout(timeline_group) self.posts_per_page = QSpinBox() self.posts_per_page.setRange(10, 200) self.posts_per_page.setAccessibleName("Posts Per Page") self.posts_per_page.setAccessibleDescription("Number of posts to load at once in timeline") timeline_layout.addRow("Posts per page:", self.posts_per_page) layout.addWidget(timeline_group) layout.addStretch() self.tabs.addTab(accessibility_widget, "A&ccessibility") def load_sound_packs(self): """Load available sound packs from the sounds directory""" self.sound_pack_combo.clear() # Add default "None" option self.sound_pack_combo.addItem("None (No sounds)", "none") # 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) # 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: 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: self.sound_pack_combo.addItem(f"{item} (System)", item) except Exception: pass # Ignore errors in system sound pack detection def load_current_settings(self): """Load current settings into the dialog""" # Audio settings current_pack = self.settings.get('audio', 'sound_pack', 'none') index = self.sound_pack_combo.findData(current_pack) if index >= 0: self.sound_pack_combo.setCurrentIndex(index) self.sound_pack_volume.setValue(int(self.settings.get('audio', 'volume', 100) or 100)) # Desktop notification settings (defaults: DMs, mentions, follows ON; others OFF) self.enable_desktop_notifications.setChecked(self.settings.get_bool('notifications', 'enabled', True)) self.notify_direct_messages.setChecked(self.settings.get_bool('notifications', 'direct_messages', True)) self.notify_mentions.setChecked(self.settings.get_bool('notifications', 'mentions', True)) self.notify_boosts.setChecked(self.settings.get_bool('notifications', 'boosts', False)) self.notify_favorites.setChecked(self.settings.get_bool('notifications', 'favorites', False)) self.notify_follows.setChecked(self.settings.get_bool('notifications', 'follows', True)) self.notify_timeline_updates.setChecked(self.settings.get_bool('notifications', 'timeline_updates', False)) # Accessibility settings self.page_step_size.setValue(int(self.settings.get('accessibility', 'page_step_size', 5) or 5)) self.verbose_announcements.setChecked(self.settings.get_bool('accessibility', 'verbose_announcements', True)) self.announce_thread_state.setChecked(self.settings.get_bool('accessibility', 'announce_thread_state', True)) # Timeline settings self.posts_per_page.setValue(int(self.settings.get('timeline', 'posts_per_page', 40) or 40)) def apply_settings(self): """Apply the current settings without closing the dialog""" # Audio settings selected_pack = self.sound_pack_combo.currentData() self.settings.set('audio', 'sound_pack', selected_pack) self.settings.set('audio', 'volume', self.sound_pack_volume.value()) # Desktop notification settings self.settings.set('notifications', 'enabled', self.enable_desktop_notifications.isChecked()) self.settings.set('notifications', 'direct_messages', self.notify_direct_messages.isChecked()) self.settings.set('notifications', 'mentions', self.notify_mentions.isChecked()) self.settings.set('notifications', 'boosts', self.notify_boosts.isChecked()) self.settings.set('notifications', 'favorites', self.notify_favorites.isChecked()) self.settings.set('notifications', 'follows', self.notify_follows.isChecked()) self.settings.set('notifications', 'timeline_updates', self.notify_timeline_updates.isChecked()) # Accessibility settings self.settings.set('accessibility', 'page_step_size', self.page_step_size.value()) self.settings.set('accessibility', 'verbose_announcements', self.verbose_announcements.isChecked()) self.settings.set('accessibility', 'announce_thread_state', self.announce_thread_state.isChecked()) # Timeline settings self.settings.set('timeline', 'posts_per_page', self.posts_per_page.value()) # Save to file self.settings.save_settings() # Emit signal so other components can update self.settings_changed.emit() def save_and_close(self): """Save settings and close the dialog""" self.apply_settings() self.accept()