Java2D on X11 (and so under XWayland) truncates its scale to a whole number, drawing 150 % at 100 %. On Linux, pin Java2D to 1 and hand the full factor to FlatLaf, taken from XSETTINGS Xft/DPI, KWin's Xwayland scale or GDK_SCALE, or chosen in Options → Design. The client's own sizes now go through UIScale: borders, gaps, dialog and row sizes, icons (rendered at the scaled size, so they stay crisp), custom-painted components and font sizes in the HTML views. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
203 lines
8.3 KiB
Java
203 lines
8.3 KiB
Java
package com.ts3client.ui;
|
|
|
|
import com.formdev.flatlaf.util.ScaledEmptyBorder;
|
|
import com.formdev.flatlaf.util.UIScale;
|
|
import com.ts3client.net.ChannelSettings;
|
|
|
|
import javax.swing.BorderFactory;
|
|
import javax.swing.Box;
|
|
import javax.swing.BoxLayout;
|
|
import javax.swing.ButtonGroup;
|
|
import javax.swing.JButton;
|
|
import javax.swing.JCheckBox;
|
|
import javax.swing.JLabel;
|
|
import javax.swing.JPanel;
|
|
import javax.swing.JRadioButton;
|
|
import javax.swing.JSpinner;
|
|
import javax.swing.JTextField;
|
|
import javax.swing.SpinnerNumberModel;
|
|
import java.awt.Dimension;
|
|
import java.awt.FlowLayout;
|
|
import java.awt.GridBagConstraints;
|
|
import java.awt.GridBagLayout;
|
|
import java.awt.Insets;
|
|
|
|
/**
|
|
* The channel editor's "Advanced" tab: the phonetic name, the delete delay of a temporary
|
|
* channel, voice encryption, and the two client limits.
|
|
*/
|
|
final class ChannelAdvancedPanel extends JPanel implements ChannelDialog.Tab {
|
|
|
|
/** TeamSpeak's ceiling for {@code channel_delete_delay}, in seconds. */
|
|
private static final int MAX_DELETE_DELAY = 604800;
|
|
|
|
private final JTextField phoneticName = new JTextField();
|
|
private final JSpinner deleteDelay =
|
|
Spinners.compact(new JSpinner(new SpinnerNumberModel(0, 0, MAX_DELETE_DELAY, 1)));
|
|
private final JButton deleteDelayMax = new JButton("max");
|
|
private final JCheckBox encrypted = new JCheckBox("Voice Data encrypted");
|
|
|
|
private final JRadioButton maxUsersUnlimited = new JRadioButton("Unlimited");
|
|
private final JRadioButton maxUsersLimited = new JRadioButton("Limited");
|
|
private final JSpinner maxUsers = Spinners.compact(new JSpinner(new SpinnerNumberModel(16, 0, 65535, 1)));
|
|
|
|
private final JRadioButton familyInherited = new JRadioButton("Inherited");
|
|
private final JRadioButton familyUnlimited = new JRadioButton("Unlimited");
|
|
private final JRadioButton familyLimited = new JRadioButton("Limited");
|
|
private final JSpinner familyMaxUsers =
|
|
Spinners.compact(new JSpinner(new SpinnerNumberModel(16, 0, 65535, 1)));
|
|
|
|
ChannelAdvancedPanel() {
|
|
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
|
|
setBorder(new ScaledEmptyBorder(8, 8, 8, 8));
|
|
|
|
phoneticName.setToolTipText("Set phonetic nickname used for this channel by text-to-speech.");
|
|
deleteDelay.setToolTipText("<html><p>Delay in seconds after temporary channels are deleted"
|
|
+ " after the last user left the channel.</p></html>");
|
|
deleteDelayMax.setToolTipText("Set delete delay to maximum value");
|
|
deleteDelayMax.addActionListener(a -> deleteDelay.setValue(MAX_DELETE_DELAY));
|
|
encrypted.setToolTipText("Voice data in this channel will be encrypted.");
|
|
|
|
ButtonGroup users = new ButtonGroup();
|
|
users.add(maxUsersUnlimited);
|
|
users.add(maxUsersLimited);
|
|
maxUsersUnlimited.setToolTipText("Unlimited number of clients can join this channel.");
|
|
maxUsersLimited.setToolTipText("Limit number of clients in this channel.");
|
|
maxUsersUnlimited.addActionListener(a -> syncLimits());
|
|
maxUsersLimited.addActionListener(a -> syncLimits());
|
|
|
|
ButtonGroup family = new ButtonGroup();
|
|
family.add(familyInherited);
|
|
family.add(familyUnlimited);
|
|
family.add(familyLimited);
|
|
familyInherited.setToolTipText("Inherit the client limit by parent channel.");
|
|
familyUnlimited.setToolTipText("The channel subtree can be joined by unlimited number of clients.");
|
|
familyLimited.setToolTipText("Limit the number of clients in the channel subtree.");
|
|
for (JRadioButton radio : new JRadioButton[]{familyInherited, familyUnlimited, familyLimited}) {
|
|
radio.addActionListener(a -> syncLimits());
|
|
}
|
|
|
|
JPanel other = otherSettings();
|
|
other.setAlignmentX(0f);
|
|
fixHeight(other);
|
|
add(other);
|
|
add(Box.createVerticalStrut(UIScale.scale(6)));
|
|
|
|
JPanel limits = new JPanel();
|
|
limits.setLayout(new BoxLayout(limits, BoxLayout.X_AXIS));
|
|
limits.add(limitBox("Max Users", new JRadioButton[]{maxUsersUnlimited, maxUsersLimited}, maxUsers));
|
|
limits.add(Box.createHorizontalStrut(UIScale.scale(8)));
|
|
limits.add(limitBox("Family Max Users",
|
|
new JRadioButton[]{familyInherited, familyUnlimited, familyLimited}, familyMaxUsers));
|
|
limits.setAlignmentX(0f);
|
|
fixHeight(limits);
|
|
add(limits);
|
|
add(Box.createVerticalGlue());
|
|
}
|
|
|
|
/** The delete delay only means anything for a channel that disappears on its own. */
|
|
void setChannelType(ChannelSettings.Type type) {
|
|
boolean temporary = type == ChannelSettings.Type.TEMPORARY;
|
|
deleteDelay.setEnabled(temporary);
|
|
deleteDelayMax.setEnabled(temporary);
|
|
}
|
|
|
|
@Override
|
|
public void read(ChannelSettings settings) {
|
|
phoneticName.setText(settings.phoneticName);
|
|
deleteDelay.setValue(Math.min(MAX_DELETE_DELAY, Math.max(0, settings.deleteDelay)));
|
|
encrypted.setSelected(settings.encrypted);
|
|
|
|
if (settings.maxClientsUnlimited) maxUsersUnlimited.setSelected(true);
|
|
else maxUsersLimited.setSelected(true);
|
|
maxUsers.setValue(settings.maxClients);
|
|
|
|
if (settings.familyInherited) familyInherited.setSelected(true);
|
|
else if (settings.familyUnlimited) familyUnlimited.setSelected(true);
|
|
else familyLimited.setSelected(true);
|
|
familyMaxUsers.setValue(settings.maxFamilyClients);
|
|
|
|
setChannelType(settings.type);
|
|
syncLimits();
|
|
}
|
|
|
|
@Override
|
|
public void write(ChannelSettings settings) {
|
|
settings.phoneticName = phoneticName.getText();
|
|
settings.deleteDelay = (Integer) deleteDelay.getValue();
|
|
settings.encrypted = encrypted.isSelected();
|
|
settings.maxClientsUnlimited = maxUsersUnlimited.isSelected();
|
|
settings.maxClients = (Integer) maxUsers.getValue();
|
|
settings.familyInherited = familyInherited.isSelected();
|
|
settings.familyUnlimited = familyUnlimited.isSelected();
|
|
settings.maxFamilyClients = (Integer) familyMaxUsers.getValue();
|
|
}
|
|
|
|
private void syncLimits() {
|
|
maxUsers.setEnabled(maxUsersLimited.isSelected());
|
|
familyMaxUsers.setEnabled(familyLimited.isSelected());
|
|
}
|
|
|
|
private JPanel otherSettings() {
|
|
JPanel panel = new JPanel(new GridBagLayout());
|
|
panel.setBorder(BorderFactory.createTitledBorder("Other Settings"));
|
|
GridBagConstraints c = new GridBagConstraints();
|
|
c.insets = UIScale.scale(new Insets(3, 4, 3, 4));
|
|
c.anchor = GridBagConstraints.WEST;
|
|
c.fill = GridBagConstraints.HORIZONTAL;
|
|
|
|
c.gridx = 0;
|
|
c.gridy = 0;
|
|
panel.add(new JLabel("Phonetic Name:"), c);
|
|
c.gridx = 1;
|
|
c.weightx = 1;
|
|
panel.add(phoneticName, c);
|
|
|
|
c.gridx = 0;
|
|
c.gridy = 1;
|
|
c.weightx = 0;
|
|
panel.add(new JLabel("Delete delay:"), c);
|
|
JPanel delay = new JPanel(new FlowLayout(FlowLayout.LEFT, UIScale.scale(4), 0));
|
|
delay.add(deleteDelay);
|
|
delay.add(deleteDelayMax);
|
|
c.gridx = 1;
|
|
c.weightx = 1;
|
|
panel.add(delay, c);
|
|
|
|
c.gridx = 1;
|
|
c.gridy = 2;
|
|
panel.add(encrypted, c);
|
|
return panel;
|
|
}
|
|
|
|
/**
|
|
* A limit box: its choices stacked, with the count beside the last of them —
|
|
* "Limited", the only choice it belongs to.
|
|
*/
|
|
private JPanel limitBox(String title, JRadioButton[] choices, JSpinner spinner) {
|
|
JPanel box = new JPanel();
|
|
box.setLayout(new BoxLayout(box, BoxLayout.Y_AXIS));
|
|
box.setBorder(BorderFactory.createTitledBorder(title));
|
|
for (int i = 0; i < choices.length; i++) {
|
|
JRadioButton radio = choices[i];
|
|
radio.setAlignmentX(0f);
|
|
if (i < choices.length - 1) {
|
|
box.add(radio);
|
|
continue;
|
|
}
|
|
JPanel row = new JPanel(new FlowLayout(FlowLayout.LEFT, UIScale.scale(6), 0));
|
|
row.setAlignmentX(0f);
|
|
row.add(radio);
|
|
row.add(spinner);
|
|
box.add(row);
|
|
}
|
|
box.add(Box.createVerticalGlue());
|
|
return box;
|
|
}
|
|
|
|
/** Keeps a section at its natural height, so the free space collects at the bottom. */
|
|
private static void fixHeight(JPanel panel) {
|
|
panel.setMaximumSize(new Dimension(Integer.MAX_VALUE, panel.getPreferredSize().height));
|
|
}
|
|
}
|