Show all of a contact's options at once on desktop

The contacts window drops its tabs for one column of short groups beside
the list: contact type, display and ignore, where whisper becomes a
"Deny whisper" box, since allowing is the default. The names get their
labels on a line of their own, and the window is never shorter than the
options, leaving the list the only thing that scrolls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-26 10:52:56 +00:00
parent 570feffc25
commit fedb7b4385

View File

@@ -10,6 +10,7 @@ import com.ts3client.contacts.NicknameDisplay;
import com.ts3client.teamspeak.TeamSpeakSettingsDb;
import com.ts3client.text.TsLink;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
@@ -24,7 +25,6 @@ import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JRadioButton;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTextField;
import javax.swing.JTree;
import javax.swing.KeyStroke;
@@ -59,10 +59,10 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* The contact list window, laid out like TeamSpeak's Contacts: the contacts grouped
* as friends, blocked and neutral on the left, and on the right the selected one's
* custom nickname and its General / Display / Ignore / Whisper pages. Every edit is
* stored as it is made.
* The contact list window: the contacts grouped as friends, blocked and neutral on
* the left, and on the right all of the selected one's settings — TeamSpeak's
* General / Display / Ignore / Whisper pages, in one column rather than as tabs
* since each holds only a few options. Every edit is stored as it is made.
*
* <p>Clients can be dragged in from the server tree — the drag carries the
* client's TS3 link, which names its unique id.
@@ -98,8 +98,7 @@ final class ContactsDialog extends JDialog {
private final JCheckBox ignorePokes = new JCheckBox("Ignore Pokes");
private final JCheckBox ignoreAvatar = new JCheckBox("Hide avatar");
private final JCheckBox ignoreAway = new JCheckBox("Hide away message");
private final JRadioButton allowWhisper = new JRadioButton("Allow whisper");
private final JRadioButton denyWhisper = new JRadioButton("Deny whisper");
private final JCheckBox denyWhisper = new JCheckBox("Deny whisper");
private final JPanel details = new JPanel(new BorderLayout(0, UIScale.scale(8)));
/** Set while the fields are being filled from a contact, so that does not count as an edit. */
@@ -122,7 +121,7 @@ final class ContactsDialog extends JDialog {
details.setBorder(new ScaledEmptyBorder(8, 4, 8, 8));
details.add(buildNicknameRow(), BorderLayout.NORTH);
details.add(buildPages(), BorderLayout.CENTER);
details.add(buildOptions(), BorderLayout.CENTER);
JPanel buttons = new JPanel(new BorderLayout());
buttons.setBorder(new ScaledEmptyBorder(0, 8, 8, 8));
@@ -161,7 +160,8 @@ final class ContactsDialog extends JDialog {
Dialogs.closeOnEscape(this);
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
pack();
setMinimumSize(UIScale.scale(new Dimension(620, 400)));
// Only the list scrolls, so the window never gets shorter than the options column.
setMinimumSize(new Dimension(UIScale.scale(620), getHeight()));
setLocationRelativeTo(owner);
}
@@ -299,35 +299,45 @@ final class ContactsDialog extends JDialog {
// ---- details ----
/** The names, each label on its own line above its field. */
private JComponent buildNicknameRow() {
JPanel row = new JPanel(new GridBagLayout());
GridBagConstraints c = new GridBagConstraints();
c.insets = UIScale.scale(new Insets(2, 2, 2, 2));
c.anchor = GridBagConstraints.WEST;
c.gridy = 0;
c.gridx = 0;
c.gridy = 0;
c.gridwidth = 2;
row.add(new JLabel("Custom Nickname:"), c);
c.gridx = 1;
c.gridy = 1;
c.gridwidth = 1;
c.weightx = 1;
c.fill = GridBagConstraints.HORIZONTAL;
nicknameField.setToolTipText("Enter custom nickname");
nicknameField.addActionListener(e -> applyNickname());
row.add(nicknameField, c);
c.gridx = 2;
c.gridx = 1;
c.weightx = 0;
c.fill = GridBagConstraints.NONE;
nicknameApply.setToolTipText("Apply nickname change");
nicknameApply.addActionListener(e -> applyNickname());
row.add(nicknameApply, c);
c.gridx = 0;
c.gridy = 2;
c.gridwidth = 2;
row.add(new JLabel("Phonetic Nickname:"), c);
c.gridy = 3;
c.weightx = 1;
c.fill = GridBagConstraints.HORIZONTAL;
row.add(phoneticField, c);
return row;
}
private JComponent buildPages() {
JTabbedPane pages = new JTabbedPane();
pages.addTab("General", page(
radioGroup(neutralRadio, blockedRadio, friendRadio),
Box.createVerticalStrut(UIScale.scale(8)),
labelled("Phonetic Nickname:", phoneticField)));
/** Every option at once, one short group under the other. */
private JComponent buildOptions() {
JComponent contact = group("Contact", radioGroup(neutralRadio, blockedRadio, friendRadio));
neutralRadio.setToolTipText("Mark as neutral");
blockedRadio.setToolTipText("Mark as blocked");
friendRadio.setToolTipText("Mark as friend");
@@ -343,7 +353,7 @@ final class ContactsDialog extends JDialog {
}
});
pages.addTab("Display", page(radioGroup(bothRadio, onlyCustomRadio, onlyNicknameRadio)));
JComponent display = group("Display", radioGroup(bothRadio, onlyCustomRadio, onlyNicknameRadio));
bothRadio.setToolTipText("Show custom name and nickname");
onlyCustomRadio.setToolTipText("Show only custom name");
onlyNicknameRadio.setToolTipText("Show nickname only");
@@ -351,7 +361,8 @@ final class ContactsDialog extends JDialog {
onlyCustomRadio.addActionListener(e -> edit(c -> c.nicknameDisplay = NicknameDisplay.ONLY_CUSTOM));
onlyNicknameRadio.addActionListener(e -> edit(c -> c.nicknameDisplay = NicknameDisplay.ONLY_NICKNAME));
pages.addTab("Ignore", page(automute, ignorePublic, ignorePrivate, ignorePokes, ignoreAvatar, ignoreAway));
JComponent ignore = group("Ignore", automute, ignorePublic, ignorePrivate, ignorePokes, ignoreAvatar, ignoreAway,
denyWhisper);
automute.setToolTipText("Automatically mute this person");
ignorePublic.setToolTipText("Ignore messages this person sends to the server or in channels");
ignorePrivate.setToolTipText("Ignore private chat by this person");
@@ -365,26 +376,35 @@ final class ContactsDialog extends JDialog {
ignoreAvatar.addActionListener(e -> edit(c -> c.ignoreAvatar = ignoreAvatar.isSelected()));
ignoreAway.addActionListener(e -> edit(c -> c.ignoreAwayMessage = ignoreAway.isSelected()));
pages.addTab("Whisper", page(radioGroup(allowWhisper, denyWhisper)));
allowWhisper.setToolTipText("This contact is allowed to whisper to you");
denyWhisper.setToolTipText("This contact is always denied to whisper to you.");
allowWhisper.addActionListener(e -> edit(c -> c.whisperAllowed = true));
denyWhisper.addActionListener(e -> edit(c -> c.whisperAllowed = false));
return pages;
denyWhisper.setToolTipText("This contact is always denied to whisper to you");
denyWhisper.addActionListener(e -> edit(c -> c.whisperAllowed = !denyWhisper.isSelected()));
return column(contact, display, ignore);
}
private static JPanel page(Component... rows) {
private static JPanel column(JComponent... groups) {
JPanel p = new JPanel();
p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
p.setBorder(new ScaledEmptyBorder(10, 10, 10, 10));
for (Component row : rows) {
if (row instanceof JComponent) ((JComponent) row).setAlignmentX(Component.LEFT_ALIGNMENT);
p.add(row);
for (JComponent g : groups) {
g.setAlignmentX(Component.LEFT_ALIGNMENT);
g.setMaximumSize(new Dimension(Integer.MAX_VALUE, g.getPreferredSize().height));
p.add(g);
p.add(Box.createVerticalStrut(UIScale.scale(6)));
}
p.add(Box.createVerticalGlue());
return p;
}
private static JPanel group(String title, JComponent... rows) {
JPanel p = new JPanel();
p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
p.setBorder(BorderFactory.createTitledBorder(title));
for (JComponent row : rows) {
row.setAlignmentX(Component.LEFT_ALIGNMENT);
p.add(row);
}
return p;
}
private static JPanel radioGroup(JRadioButton... buttons) {
ButtonGroup group = new ButtonGroup();
JPanel p = new JPanel();
@@ -397,14 +417,6 @@ final class ContactsDialog extends JDialog {
return p;
}
private static JPanel labelled(String label, JComponent field) {
JPanel p = new JPanel(new BorderLayout(UIScale.scale(6), 0));
p.add(new JLabel(label), BorderLayout.WEST);
p.add(field, BorderLayout.CENTER);
p.setMaximumSize(new Dimension(Integer.MAX_VALUE, field.getPreferredSize().height));
return p;
}
/** Fills the right-hand side from the selected contact, or greys it out. */
private void showSelected() {
Contact c = selectedContact();
@@ -429,7 +441,7 @@ final class ContactsDialog extends JDialog {
ignorePokes.setSelected(c.ignorePokes);
ignoreAvatar.setSelected(c.ignoreAvatar);
ignoreAway.setSelected(c.ignoreAwayMessage);
(c.whisperAllowed ? allowWhisper : denyWhisper).setSelected(true);
denyWhisper.setSelected(!c.whisperAllowed);
} finally {
filling = false;
}