Show server and channel group icons in the tree
Download the icons a server advertises for its groups over the file transfer channel, cache them under the config directory, and draw them as a badge strip on each client's row, right-aligned against the tree's visible edge like the TeamSpeak client does. Groups without a custom icon fall back to the bundled default set. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,12 +13,14 @@ import javax.swing.ButtonGroup;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JCheckBox;
|
||||
import javax.swing.JComboBox;
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JDialog;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JRadioButton;
|
||||
import javax.swing.JSlider;
|
||||
import javax.swing.JTabbedPane;
|
||||
import javax.swing.Scrollable;
|
||||
import javax.swing.SwingUtilities;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Dimension;
|
||||
@@ -26,6 +28,7 @@ import java.awt.Frame;
|
||||
import java.awt.GridBagConstraints;
|
||||
import java.awt.GridBagLayout;
|
||||
import java.awt.Insets;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.event.KeyAdapter;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.util.List;
|
||||
@@ -37,6 +40,14 @@ import java.util.List;
|
||||
*/
|
||||
public final class SettingsDialog extends JDialog {
|
||||
|
||||
/** Preferred widths of the form's field column; rows shrink with the dialog from there. */
|
||||
private static final int FIELD_WIDTH = 240;
|
||||
private static final int SLIDER_WIDTH = 200;
|
||||
private static final int MIN_FIELD_WIDTH = 60;
|
||||
|
||||
private static final int MIN_BITRATE_KBITS = 8;
|
||||
private static final int MAX_BITRATE_KBITS = 160;
|
||||
|
||||
private final Settings settings;
|
||||
private final VoiceInput liveMic;
|
||||
private final VoiceOutput livePlayback;
|
||||
@@ -114,13 +125,13 @@ public final class SettingsDialog extends JDialog {
|
||||
|
||||
pack();
|
||||
setSize(new Dimension(480, 540));
|
||||
setMinimumSize(new Dimension(420, 360));
|
||||
setLocationRelativeTo(owner);
|
||||
startMeter();
|
||||
}
|
||||
|
||||
private JPanel buildDevicesTab() {
|
||||
JPanel p = new JPanel(new GridBagLayout());
|
||||
p.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
|
||||
JPanel p = formPanel();
|
||||
GridBagConstraints c = gbc();
|
||||
|
||||
List<AudioDevices.Device> ins = AudioDevices.inputDevices();
|
||||
@@ -136,6 +147,8 @@ public final class SettingsDialog extends JDialog {
|
||||
+ "<b>ALSA:</b> entries talk to the sound card directly, taking it exclusively.</html>";
|
||||
inputCombo.setToolTipText(deviceHint);
|
||||
outputCombo.setToolTipText(deviceHint);
|
||||
limitWidth(inputCombo, FIELD_WIDTH);
|
||||
limitWidth(outputCombo, FIELD_WIDTH);
|
||||
|
||||
int row = 0;
|
||||
addRow(p, c, row++, new JLabel("Capture device (microphone):"), inputCombo);
|
||||
@@ -143,6 +156,8 @@ public final class SettingsDialog extends JDialog {
|
||||
|
||||
inputGain = new JSlider(0, 200, (int) Math.round(settings.inputVolume * 100));
|
||||
outputVol = new JSlider(0, 200, (int) Math.round(settings.outputVolume * 100));
|
||||
limitWidth(inputGain, SLIDER_WIDTH);
|
||||
limitWidth(outputVol, SLIDER_WIDTH);
|
||||
addRow(p, c, row++, new JLabel("Microphone gain:"), inputGain);
|
||||
addRow(p, c, row++, new JLabel("Playback volume:"), outputVol);
|
||||
|
||||
@@ -165,6 +180,7 @@ public final class SettingsDialog extends JDialog {
|
||||
denoiseCheck = new JCheckBox("Remove background noise", settings.denoise);
|
||||
denoiseCheck.setToolTipText("Attempt to filter out background noises.");
|
||||
denoiseLevel = new JSlider(0, 100, (int) Math.round(settings.denoiserLevel * 100));
|
||||
limitWidth(denoiseLevel, SLIDER_WIDTH);
|
||||
denoiseLevel.setToolTipText("Higher = more aggressive noise removal.");
|
||||
typingCheck = new JCheckBox("Typing attenuation", settings.typingAttenuation);
|
||||
typingCheck.setToolTipText("<html><b>Typing attenuation</b> tries to detect and "
|
||||
@@ -214,8 +230,7 @@ public final class SettingsDialog extends JDialog {
|
||||
}
|
||||
|
||||
private JPanel buildVoiceTab() {
|
||||
JPanel p = new JPanel(new GridBagLayout());
|
||||
p.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
|
||||
JPanel p = formPanel();
|
||||
GridBagConstraints c = gbc();
|
||||
|
||||
vadRadio = new JRadioButton("Voice Activation Detection");
|
||||
@@ -295,18 +310,18 @@ public final class SettingsDialog extends JDialog {
|
||||
p.add(vadOverPttCheck, c);
|
||||
c.gridwidth = 1;
|
||||
|
||||
bitrateSlider = new JSlider(8, 128, settings.bitrate / 1000);
|
||||
bitrateLabel = new JLabel(settings.bitrate / 1000 + " kbit/s");
|
||||
int kbits = Math.max(MIN_BITRATE_KBITS, Math.min(MAX_BITRATE_KBITS, settings.bitrate / 1000));
|
||||
bitrateSlider = new JSlider(MIN_BITRATE_KBITS, MAX_BITRATE_KBITS, kbits);
|
||||
bitrateLabel = new JLabel(kbits + " kbit/s");
|
||||
bitrateSlider.addChangeListener(e -> {
|
||||
bitrateLabel.setText(bitrateSlider.getValue() + " kbit/s");
|
||||
pushOpusLive();
|
||||
});
|
||||
JPanel brPanel = new JPanel(new BorderLayout(6, 0));
|
||||
brPanel.add(bitrateSlider, BorderLayout.CENTER);
|
||||
brPanel.add(bitrateLabel, BorderLayout.EAST);
|
||||
addRow(p, c, row++, new JLabel("Opus bitrate:"), brPanel);
|
||||
addRow(p, c, row++, new JLabel("Opus bitrate:"),
|
||||
sliderWithLabel(bitrateSlider, bitrateLabel, 70));
|
||||
|
||||
complexitySlider = new JSlider(0, 10, settings.complexity);
|
||||
limitWidth(complexitySlider, SLIDER_WIDTH);
|
||||
complexitySlider.addChangeListener(e -> pushOpusLive());
|
||||
addRow(p, c, row++, new JLabel("Opus complexity:"), complexitySlider);
|
||||
|
||||
@@ -389,6 +404,47 @@ public final class SettingsDialog extends JDialog {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The form panel used by both tabs. It follows the scroll pane's width instead of
|
||||
* demanding its own preferred one, so rows stay inside the dialog.
|
||||
*/
|
||||
private static JPanel formPanel() {
|
||||
JPanel p = new FormPanel();
|
||||
p.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
|
||||
return p;
|
||||
}
|
||||
|
||||
private static final class FormPanel extends JPanel implements Scrollable {
|
||||
FormPanel() {
|
||||
super(new GridBagLayout());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dimension getPreferredScrollableViewportSize() {
|
||||
return getPreferredSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getScrollableUnitIncrement(Rectangle visible, int orientation, int direction) {
|
||||
return 16;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getScrollableBlockIncrement(Rectangle visible, int orientation, int direction) {
|
||||
return visible.height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getScrollableTracksViewportWidth() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getScrollableTracksViewportHeight() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static javax.swing.JScrollPane scrollable(JPanel content) {
|
||||
javax.swing.JScrollPane sp = new javax.swing.JScrollPane(content,
|
||||
javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
|
||||
@@ -399,13 +455,33 @@ public final class SettingsDialog extends JDialog {
|
||||
}
|
||||
|
||||
private static JPanel sliderWithLabel(JSlider slider, JLabel valueLabel) {
|
||||
return sliderWithLabel(slider, valueLabel, 48);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pairs a slider with its value readout. The readout gets a fixed width wide enough for
|
||||
* the longest value so the slider does not jump around as it is dragged.
|
||||
*/
|
||||
private static JPanel sliderWithLabel(JSlider slider, JLabel valueLabel, int labelWidth) {
|
||||
JPanel panel = new JPanel(new BorderLayout(6, 0));
|
||||
limitWidth(slider, SLIDER_WIDTH);
|
||||
panel.add(slider, BorderLayout.CENTER);
|
||||
valueLabel.setPreferredSize(new Dimension(48, valueLabel.getPreferredSize().height));
|
||||
valueLabel.setPreferredSize(new Dimension(labelWidth, valueLabel.getPreferredSize().height));
|
||||
panel.add(valueLabel, BorderLayout.EAST);
|
||||
return panel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Caps a field's preferred width and lets it shrink: long device names and wide sliders
|
||||
* would otherwise force the form past the dialog's edge, where the scroll pane (which
|
||||
* never scrolls horizontally) simply clips them.
|
||||
*/
|
||||
private static void limitWidth(JComponent comp, int preferredWidth) {
|
||||
int height = comp.getPreferredSize().height;
|
||||
comp.setPreferredSize(new Dimension(preferredWidth, height));
|
||||
comp.setMinimumSize(new Dimension(MIN_FIELD_WIDTH, height));
|
||||
}
|
||||
|
||||
private OpusParameters currentOpusParameters() {
|
||||
return new OpusParameters(
|
||||
bitrateSlider.getValue() * 1000,
|
||||
|
||||
Reference in New Issue
Block a user