Files
ts3j/ts3-client/swing/src/main/java/com/ts3client/ui/SettingsDialog.java
ericek111 d722377508 Split SettingsDialog into focused tab panels
Extract the Playback/Capture and Voice Activation tabs (device pickers,
gain/volume sliders, noise reduction, VAD/PTT tuning, microphone test)
into DevicesPanel and VoiceActivationPanel, alongside the existing
NotificationsPanel/HotkeysPanel/IconPackPanel/ClientVersionPanel. A new
FormPanel base holds the shared GridBagLayout scaffolding. SettingsDialog
is now a thin coordinator (753 -> 187 lines); its public API and all
settings read/write behavior are unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 13:37:51 +00:00

188 lines
7.1 KiB
Java

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.
*
* <p>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.
*
* <p>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<VoiceInput> change) {
if (liveMic != null) change.accept(liveMic);
if (voiceActivationPanel != null) voiceActivationPanel.configureTest(change);
}
/** Restarts the microphone test, if running, so a device change takes effect. */
private void restartTest() {
if (voiceActivationPanel != null) voiceActivationPanel.restartTest();
}
private void setTestOutputDevice(String deviceId) {
if (voiceActivationPanel != null) voiceActivationPanel.setTestOutputDevice(deviceId);
}
}