package com.ts3client.ui; import com.ts3client.audio.OpusParameters; import com.ts3client.audio.VoiceInput; import com.ts3client.audio.VoiceOutput; import com.ts3client.config.Settings; import com.ts3client.sound.SoundNotifier; import javax.swing.JButton; import javax.swing.JDialog; import javax.swing.JPanel; import javax.swing.JTabbedPane; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.Frame; import java.util.function.Consumer; /** * Options dialog: a tabbed coordinator over the individual settings pages. Each tab owns * its own controls and reads/writes {@link Settings} on {@link #apply()}; this class wires * the tabs that preview themselves live against the running audio subsystem (the device * and voice-activation tabs, which share a single microphone test) and owns the * OK/Cancel/Apply plumbing. */ public final class SettingsDialog extends JDialog { private final Settings settings; private final VoiceInput liveMic; private final VoiceOutput livePlayback; private final Runnable onApply; private final DevicesPanel devicesPanel; private final VoiceActivationPanel voiceActivationPanel; private final NotificationsPanel notificationsPanel; private final IconPackPanel iconPackPanel; private final HotkeysPanel hotkeysPanel; private final ClientVersionPanel clientVersionPanel; public SettingsDialog(Frame owner, Settings settings, VoiceInput liveMic, VoiceOutput livePlayback, SoundNotifier sounds, HotkeyService hotkeys, Runnable onApply) { super(owner, "Options", true); this.settings = settings; this.liveMic = liveMic; this.livePlayback = livePlayback; this.onApply = onApply; notificationsPanel = new NotificationsPanel(settings, sounds); iconPackPanel = new IconPackPanel(settings); hotkeysPanel = new HotkeysPanel(hotkeys); clientVersionPanel = new ClientVersionPanel(settings); devicesPanel = new DevicesPanel(settings, livePlayback, this::applyLive, this::restartTest, this::setTestOutputDevice); voiceActivationPanel = new VoiceActivationPanel(settings, liveMic, hotkeys, hotkeysPanel, this::audioSnapshot); JTabbedPane tabs = new JTabbedPane(); tabs.addTab("Playback / Capture", scrollable(devicesPanel)); tabs.addTab("Voice Activation", scrollable(voiceActivationPanel)); tabs.addTab("Notifications", notificationsPanel); tabs.addTab("Design", iconPackPanel); tabs.addTab("Hotkeys", hotkeysPanel); tabs.addTab("Client Version", scrollable(clientVersionPanel)); JPanel buttons = new JPanel(new BorderLayout()); JPanel right = new JPanel(); JButton ok = new JButton("OK"); JButton cancel = new JButton("Cancel"); ok.addActionListener(e -> { apply(); close(); }); cancel.addActionListener(e -> cancel()); right.add(ok); right.add(cancel); buttons.add(right, BorderLayout.EAST); getContentPane().setLayout(new BorderLayout()); getContentPane().add(tabs, BorderLayout.CENTER); getContentPane().add(buttons, BorderLayout.SOUTH); Dialogs.closeOnEscape(this, this::cancel); setDefaultCloseOperation(DISPOSE_ON_CLOSE); addWindowListener(new java.awt.event.WindowAdapter() { @Override public void windowClosed(java.awt.event.WindowEvent e) { voiceActivationPanel.stopTest(); } }); pack(); setSize(new Dimension(480, 540)); setMinimumSize(new Dimension(420, 360)); setLocationRelativeTo(owner); } private static javax.swing.JScrollPane scrollable(JPanel content) { javax.swing.JScrollPane sp = new javax.swing.JScrollPane(content, javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); sp.setBorder(null); sp.getVerticalScrollBar().setUnitIncrement(16); return sp; } /** Copies the audio tabs into {@code target}, without touching anything else. */ private void writeAudioSettings(Settings target) { devicesPanel.writeInto(target); voiceActivationPanel.writeInto(target); } /** * The audio tabs as a standalone {@link Settings}, so the microphone test chain runs * with the values currently on screen rather than the ones last saved. * *
The test always runs voice activation: it exists to tune the gate, and push-to-talk * would need the global hotkey, which belongs to the connected microphone. */ private Settings audioSnapshot() { Settings snapshot = new Settings(); writeAudioSettings(snapshot); if (snapshot.inputMode == Settings.InputMode.PUSH_TO_TALK) { snapshot.inputMode = Settings.InputMode.VOICE_ACTIVATION; } return snapshot; } private void apply() { writeAudioSettings(settings); notificationsPanel.apply(); iconPackPanel.apply(); clientVersionPanel.apply(); hotkeysPanel.apply(); settings.save(); applyLive(m -> { m.setMode(settings.inputMode); m.setVadMode(settings.vadMode); m.setThresholdDb(settings.vadThresholdDb); m.setSpeechThreshold(settings.speechThreshold); m.setVadOverPtt(settings.vadOverPtt); m.setInputGain(settings.inputVolume); m.setNoiseSuppression(settings.denoise); m.setDenoiserLevel(settings.denoiserLevel); m.setTypingAttenuation(settings.typingAttenuation); m.setAgc(settings.agc); m.setOpusParameters(OpusParameters.from(settings)); }); if (livePlayback != null) { livePlayback.setMasterVolume(settings.outputVolume); livePlayback.setOutputDevice(settings.outputDevice); } if (onApply != null) onApply.run(); } /** Leaves without applying, putting back the settings that preview themselves live. */ private void cancel() { notificationsPanel.revert(); iconPackPanel.revert(); close(); } private void close() { voiceActivationPanel.stopTest(); dispose(); } /** * Applies a live change to the connected microphone and to the microphone test alike. * *
Guarded against a null {@code voiceActivationPanel}: the Devices tab applies its
* controls once as they are built, which happens before that tab exists.
*/
private void applyLive(Consumer