Add the contact list (friends, blocked, neutral)
Mirrors the TeamSpeak 3 client's Contacts: clients can be filed as friend,
blocked or neutral from their context menu or by dragging them into the
Contacts window, given a custom and phonetic nickname, shown under either
name or both, and per contact muted automatically, ignored in server/channel
or private chat and pokes, have their away message hidden and whispers
allowed or denied. Friends are green and blocked clients red in the tree
(optional), new contacts start from per-type defaults ("Set Defaults"), and
the global whisper policy lives in Options -> Contacts. "Change Volume..."
adds a per-client dB gain that a contact entry remembers.
Contacts are stored in ~/.ts3jclient/contacts.txt using the exact record
format the official client keeps in the Contacts table of its settings.db
(field names and codes recovered from the binary), so entries move between
the two verbatim. A small read-only pure-Java SQLite reader imports the
official client's list on first run and on demand, without a driver.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package com.ts3client.ui;
|
||||
|
||||
import com.ts3client.contacts.ContactType;
|
||||
import com.ts3client.net.ChannelNode;
|
||||
import com.ts3client.net.ClientEntry;
|
||||
import com.ts3client.net.Group;
|
||||
@@ -50,6 +51,11 @@ final class ClientMenu {
|
||||
Icons.of(muted ? "PLAYER_ON" : "INPUT_MUTED"));
|
||||
mute.addActionListener(a -> actions.toggleClientMute(client));
|
||||
menu.add(mute);
|
||||
JMenuItem volume = new JMenuItem("Change Volume…", Icons.of("VOLUME"));
|
||||
volume.addActionListener(a -> actions.changeClientVolume(client));
|
||||
menu.add(volume);
|
||||
menu.addSeparator();
|
||||
addContactItems(menu, client, actions);
|
||||
} else {
|
||||
JMenuItem me = new JMenuItem("This is you", Icons.of("PLAYER_OFF"));
|
||||
me.setEnabled(false);
|
||||
@@ -84,6 +90,30 @@ final class ClientMenu {
|
||||
return menu;
|
||||
}
|
||||
|
||||
/**
|
||||
* The contact entries, as TeamSpeak offers them: a client not on the list can be
|
||||
* added as a friend or blocked; one on it can be edited or removed.
|
||||
*/
|
||||
private static void addContactItems(JPopupMenu menu, ClientEntry client, ServerTreePanel.Actions actions) {
|
||||
ContactType type = actions.contactTypeOf(client);
|
||||
if (type == null) {
|
||||
JMenuItem friend = new JMenuItem("Add as Friend", Icons.of("ADD_FRIEND"));
|
||||
friend.addActionListener(a -> actions.addContact(client, ContactType.FRIEND));
|
||||
menu.add(friend);
|
||||
JMenuItem blocked = new JMenuItem("Add as Blocked", Icons.of("ADD_FOE"));
|
||||
blocked.addActionListener(a -> actions.addContact(client, ContactType.BLOCKED));
|
||||
menu.add(blocked);
|
||||
return;
|
||||
}
|
||||
JMenuItem edit = new JMenuItem("Edit in Contacts", Icons.of("EDIT_FRIEND_FOE_STATUS"));
|
||||
edit.addActionListener(a -> actions.editContact(client));
|
||||
menu.add(edit);
|
||||
JMenuItem remove = new JMenuItem("Remove from Contacts",
|
||||
Icons.of(type == ContactType.BLOCKED ? "REMOVE_FOE" : "REMOVE_FRIEND"));
|
||||
remove.addActionListener(a -> actions.removeContact(client));
|
||||
menu.add(remove);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the client's channel is actually rendered in the tree — it won't be if
|
||||
* we're not subscribed to it (e.g. it needs more subscribe power than we have).
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.ts3client.ui;
|
||||
|
||||
import com.ts3client.net.ClientEntry;
|
||||
import com.ts3client.net.TeamspeakConnection;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JDialog;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JSlider;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.FlowLayout;
|
||||
import java.awt.Frame;
|
||||
|
||||
/**
|
||||
* TeamSpeak's "Change Volume" for one client: a gain in decibels on top of the
|
||||
* master volume, applied as the slider moves. It is remembered in the client's
|
||||
* contact entry, when it has one; otherwise it lasts for the session.
|
||||
*/
|
||||
final class ClientVolumeDialog extends JDialog {
|
||||
|
||||
private static final int RANGE_DB = 40;
|
||||
|
||||
private final TeamspeakConnection conn;
|
||||
private final ClientEntry client;
|
||||
private final JSlider slider = new JSlider(-RANGE_DB, RANGE_DB, 0);
|
||||
private final JLabel value = new JLabel();
|
||||
|
||||
ClientVolumeDialog(Frame owner, TeamspeakConnection conn, ClientEntry client) {
|
||||
super(owner, "Change Volume: " + conn.displayName(client), false);
|
||||
this.conn = conn;
|
||||
this.client = client;
|
||||
|
||||
slider.setValue((int) Math.round(conn.getClientVolumeModifier(client)));
|
||||
slider.setMajorTickSpacing(20);
|
||||
slider.setMinorTickSpacing(5);
|
||||
slider.setPaintTicks(true);
|
||||
slider.setPaintLabels(true);
|
||||
slider.addChangeListener(e -> onChanged());
|
||||
FormPanel.limitWidth(value, 60);
|
||||
|
||||
JPanel content = new JPanel(new BorderLayout(8, 8));
|
||||
content.setBorder(BorderFactory.createEmptyBorder(12, 12, 8, 12));
|
||||
content.add(new JLabel("Volume Modifier"), BorderLayout.NORTH);
|
||||
content.add(FormPanel.sliderWithLabel(slider, value), BorderLayout.CENTER);
|
||||
JLabel hint = new JLabel("<html><body style='width:300px'>Values greater than +6 dB will cause clipping "
|
||||
+ "and distortion that will negatively affect your audio quality. It is advised to choose "
|
||||
+ "lower values.</body></html>");
|
||||
hint.setForeground(Theme.chatSystem());
|
||||
content.add(hint, BorderLayout.SOUTH);
|
||||
|
||||
JPanel buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||
JButton reset = new JButton("Reset");
|
||||
reset.addActionListener(e -> slider.setValue(0));
|
||||
JButton close = new JButton("Close");
|
||||
close.addActionListener(e -> dispose());
|
||||
buttons.add(reset);
|
||||
buttons.add(close);
|
||||
|
||||
getContentPane().setLayout(new BorderLayout());
|
||||
getContentPane().add(content, BorderLayout.CENTER);
|
||||
getContentPane().add(buttons, BorderLayout.SOUTH);
|
||||
|
||||
onChanged();
|
||||
Dialogs.closeOnEscape(this);
|
||||
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
|
||||
pack();
|
||||
setMinimumSize(new Dimension(360, getHeight()));
|
||||
setLocationRelativeTo(owner);
|
||||
}
|
||||
|
||||
private void onChanged() {
|
||||
int db = slider.getValue();
|
||||
value.setText((db > 0 ? "+" : "") + db + " dB");
|
||||
if (!slider.getValueIsAdjusting()) conn.setClientVolumeModifier(client, db);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package com.ts3client.ui;
|
||||
|
||||
import com.ts3client.config.Settings;
|
||||
import com.ts3client.contacts.ContactDefaults;
|
||||
import com.ts3client.contacts.ContactType;
|
||||
import com.ts3client.contacts.NicknameDisplay;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.ButtonGroup;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JCheckBox;
|
||||
import javax.swing.JDialog;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JRadioButton;
|
||||
import javax.swing.JSeparator;
|
||||
import javax.swing.SwingConstants;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Dialog;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.FlowLayout;
|
||||
import java.awt.GridBagConstraints;
|
||||
import java.awt.GridBagLayout;
|
||||
import java.awt.Insets;
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* TeamSpeak's "Contact Manager Default Settings": what a client starts out with
|
||||
* when added as a friend, blocked or neutral — the ignore/hide flags and how its
|
||||
* nickname is displayed — in a grid with one column per contact type.
|
||||
*/
|
||||
final class ContactDefaultsDialog extends JDialog {
|
||||
|
||||
private static final ContactType[] COLUMNS = {ContactType.FRIEND, ContactType.BLOCKED, ContactType.NEUTRAL};
|
||||
|
||||
private final Settings settings;
|
||||
private final Map<ContactType, Map<String, JCheckBox>> flags = new EnumMap<>(ContactType.class);
|
||||
private final Map<ContactType, Map<NicknameDisplay, JRadioButton>> displays = new EnumMap<>(ContactType.class);
|
||||
|
||||
ContactDefaultsDialog(Dialog owner, Settings settings) {
|
||||
super(owner, "Contact Manager Default Settings", true);
|
||||
this.settings = settings;
|
||||
for (ContactType t : COLUMNS) {
|
||||
flags.put(t, new java.util.LinkedHashMap<>());
|
||||
displays.put(t, new EnumMap<>(NicknameDisplay.class));
|
||||
}
|
||||
|
||||
JPanel content = new JPanel();
|
||||
content.setLayout(new javax.swing.BoxLayout(content, javax.swing.BoxLayout.Y_AXIS));
|
||||
content.setBorder(BorderFactory.createEmptyBorder(12, 12, 8, 12));
|
||||
JLabel intro = new JLabel("Configure default settings when adding a client to the Contact Manager");
|
||||
intro.setAlignmentX(LEFT_ALIGNMENT);
|
||||
content.add(intro);
|
||||
content.add(javax.swing.Box.createVerticalStrut(10));
|
||||
content.add(buildFlags());
|
||||
content.add(javax.swing.Box.createVerticalStrut(10));
|
||||
content.add(buildDisplays());
|
||||
|
||||
JPanel buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||
JButton ok = new JButton("OK");
|
||||
ok.addActionListener(e -> {
|
||||
apply();
|
||||
dispose();
|
||||
});
|
||||
JButton cancel = new JButton("Cancel");
|
||||
cancel.addActionListener(e -> dispose());
|
||||
buttons.add(ok);
|
||||
buttons.add(cancel);
|
||||
getRootPane().setDefaultButton(ok);
|
||||
|
||||
getContentPane().setLayout(new BorderLayout());
|
||||
getContentPane().add(content, BorderLayout.CENTER);
|
||||
getContentPane().add(buttons, BorderLayout.SOUTH);
|
||||
|
||||
load();
|
||||
Dialogs.closeOnEscape(this);
|
||||
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
|
||||
pack();
|
||||
setMinimumSize(new Dimension(getWidth(), getHeight()));
|
||||
setLocationRelativeTo(owner);
|
||||
}
|
||||
|
||||
private JPanel buildFlags() {
|
||||
JPanel grid = titledGrid("Ignore and Hide Defaults");
|
||||
int row = 1;
|
||||
row = flagRow(grid, row, "Mute", "automute", "Automatically mute this person");
|
||||
row = flagRow(grid, row, "Ignore server/channel chat", "ignorePublicMessages",
|
||||
"Ignore messages this person sends to the server or in channels");
|
||||
row = flagRow(grid, row, "Ignore private chat", "ignorePrivateMessages", "Ignore private chat by this person");
|
||||
row = flagRow(grid, row, "Ignore Pokes", "ignorePokes", "Ignore pokes this person sends you");
|
||||
row = flagRow(grid, row, "Hide avatar", "ignoreAvatar", "Do not download this person's avatar");
|
||||
row = flagRow(grid, row, "Hide away message", "ignoreAwayMessage", "Ignore this person's away messages");
|
||||
flagRow(grid, row, "Deny whisper", "ignoreWhisper", "This contact is always denied to whisper to you.");
|
||||
return grid;
|
||||
}
|
||||
|
||||
private JPanel buildDisplays() {
|
||||
JPanel grid = titledGrid("Nickname Display Defaults");
|
||||
Map<ContactType, ButtonGroup> groups = new EnumMap<>(ContactType.class);
|
||||
for (ContactType t : COLUMNS) groups.put(t, new ButtonGroup());
|
||||
int row = 1;
|
||||
for (NicknameDisplay display : NicknameDisplay.values()) {
|
||||
GridBagConstraints c = cell(0, row);
|
||||
c.anchor = GridBagConstraints.WEST;
|
||||
grid.add(new JLabel(display.label()), c);
|
||||
for (int i = 0; i < COLUMNS.length; i++) {
|
||||
JRadioButton radio = new JRadioButton();
|
||||
radio.setToolTipText(display == NicknameDisplay.BOTH ? "Show both custom name and the nickname"
|
||||
: display == NicknameDisplay.ONLY_CUSTOM ? "Show only custom name" : "Show only the nickname");
|
||||
groups.get(COLUMNS[i]).add(radio);
|
||||
displays.get(COLUMNS[i]).put(display, radio);
|
||||
grid.add(radio, cell(i + 1, row));
|
||||
}
|
||||
row++;
|
||||
}
|
||||
return grid;
|
||||
}
|
||||
|
||||
/** A grid with the three contact types as column headings. */
|
||||
private JPanel titledGrid(String title) {
|
||||
JPanel grid = new JPanel(new GridBagLayout());
|
||||
grid.setBorder(BorderFactory.createTitledBorder(title));
|
||||
grid.setAlignmentX(LEFT_ALIGNMENT);
|
||||
String[] headings = {"Friends", "Blocked", "Neutral"};
|
||||
for (int i = 0; i < headings.length; i++) {
|
||||
JLabel heading = new JLabel(headings[i], SwingConstants.CENTER);
|
||||
heading.setFont(Theme.uiBold());
|
||||
GridBagConstraints c = cell(i + 1, 0);
|
||||
c.ipadx = 12;
|
||||
grid.add(heading, c);
|
||||
}
|
||||
GridBagConstraints line = cell(0, 100);
|
||||
line.gridwidth = 4;
|
||||
line.fill = GridBagConstraints.HORIZONTAL;
|
||||
grid.add(new JSeparator(), line);
|
||||
return grid;
|
||||
}
|
||||
|
||||
private int flagRow(JPanel grid, int row, String label, String key, String tooltip) {
|
||||
GridBagConstraints c = cell(0, row);
|
||||
c.anchor = GridBagConstraints.WEST;
|
||||
c.weightx = 1;
|
||||
grid.add(new JLabel(label), c);
|
||||
for (int i = 0; i < COLUMNS.length; i++) {
|
||||
JCheckBox box = new JCheckBox();
|
||||
box.setToolTipText(tooltip);
|
||||
flags.get(COLUMNS[i]).put(key, box);
|
||||
grid.add(box, cell(i + 1, row));
|
||||
}
|
||||
return row + 1;
|
||||
}
|
||||
|
||||
private static GridBagConstraints cell(int x, int y) {
|
||||
GridBagConstraints c = new GridBagConstraints();
|
||||
c.gridx = x;
|
||||
c.gridy = y;
|
||||
c.insets = new Insets(1, 6, 1, 6);
|
||||
c.anchor = GridBagConstraints.CENTER;
|
||||
return c;
|
||||
}
|
||||
|
||||
// ---- model <-> fields ----
|
||||
|
||||
private void load() {
|
||||
for (ContactType t : COLUMNS) {
|
||||
ContactDefaults.Entry e = settings.contactDefaults.of(t);
|
||||
Map<String, JCheckBox> boxes = flags.get(t);
|
||||
boxes.get("automute").setSelected(e.automute);
|
||||
boxes.get("ignorePublicMessages").setSelected(e.ignorePublicMessages);
|
||||
boxes.get("ignorePrivateMessages").setSelected(e.ignorePrivateMessages);
|
||||
boxes.get("ignorePokes").setSelected(e.ignorePokes);
|
||||
boxes.get("ignoreAvatar").setSelected(e.ignoreAvatar);
|
||||
boxes.get("ignoreAwayMessage").setSelected(e.ignoreAwayMessage);
|
||||
boxes.get("ignoreWhisper").setSelected(e.ignoreWhisper);
|
||||
displays.get(t).get(e.nicknameDisplay).setSelected(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void apply() {
|
||||
for (ContactType t : COLUMNS) {
|
||||
ContactDefaults.Entry e = settings.contactDefaults.of(t);
|
||||
Map<String, JCheckBox> boxes = flags.get(t);
|
||||
e.automute = boxes.get("automute").isSelected();
|
||||
e.ignorePublicMessages = boxes.get("ignorePublicMessages").isSelected();
|
||||
e.ignorePrivateMessages = boxes.get("ignorePrivateMessages").isSelected();
|
||||
e.ignorePokes = boxes.get("ignorePokes").isSelected();
|
||||
e.ignoreAvatar = boxes.get("ignoreAvatar").isSelected();
|
||||
e.ignoreAwayMessage = boxes.get("ignoreAwayMessage").isSelected();
|
||||
e.ignoreWhisper = boxes.get("ignoreWhisper").isSelected();
|
||||
for (Map.Entry<NicknameDisplay, JRadioButton> radio : displays.get(t).entrySet()) {
|
||||
if (radio.getValue().isSelected()) e.nicknameDisplay = radio.getKey();
|
||||
}
|
||||
}
|
||||
settings.save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,537 @@
|
||||
package com.ts3client.ui;
|
||||
|
||||
import com.ts3client.config.Settings;
|
||||
import com.ts3client.contacts.Contact;
|
||||
import com.ts3client.contacts.ContactStore;
|
||||
import com.ts3client.contacts.ContactType;
|
||||
import com.ts3client.contacts.NicknameDisplay;
|
||||
import com.ts3client.contacts.TeamSpeakSettingsDb;
|
||||
import com.ts3client.text.TsLink;
|
||||
|
||||
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.JComponent;
|
||||
import javax.swing.JDialog;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JMenuItem;
|
||||
import javax.swing.JOptionPane;
|
||||
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;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.TransferHandler;
|
||||
import javax.swing.tree.DefaultMutableTreeNode;
|
||||
import javax.swing.tree.DefaultTreeCellRenderer;
|
||||
import javax.swing.tree.DefaultTreeModel;
|
||||
import javax.swing.tree.TreePath;
|
||||
import javax.swing.tree.TreeSelectionModel;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Component;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.FlowLayout;
|
||||
import java.awt.Frame;
|
||||
import java.awt.GridBagConstraints;
|
||||
import java.awt.GridBagLayout;
|
||||
import java.awt.Insets;
|
||||
import java.awt.Toolkit;
|
||||
import java.awt.datatransfer.DataFlavor;
|
||||
import java.awt.datatransfer.StringSelection;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.io.File;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
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.
|
||||
*
|
||||
* <p>Clients can be dragged in from the server tree — the drag carries the
|
||||
* client's TS3 link, which names its unique id.
|
||||
*/
|
||||
final class ContactsDialog extends JDialog {
|
||||
|
||||
private static final DateTimeFormatter SEEN = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
||||
private static final Pattern CLIENT_LINK = Pattern.compile("client://[^\\]\\s]+");
|
||||
|
||||
private final ContactStore contacts;
|
||||
private final Settings settings;
|
||||
private final ContactStore.Listener storeListener = () -> SwingUtilities.invokeLater(this::reload);
|
||||
|
||||
private final DefaultMutableTreeNode root = new DefaultMutableTreeNode();
|
||||
private final DefaultTreeModel treeModel = new DefaultTreeModel(root);
|
||||
private final JTree tree = new JTree(treeModel);
|
||||
private final DefaultMutableTreeNode friends = new DefaultMutableTreeNode("Friends");
|
||||
private final DefaultMutableTreeNode blocked = new DefaultMutableTreeNode("Blocked");
|
||||
private final DefaultMutableTreeNode neutral = new DefaultMutableTreeNode("Neutral");
|
||||
|
||||
private final JTextField nicknameField = new JTextField(18);
|
||||
private final JButton nicknameApply = new JButton("Apply");
|
||||
private final JRadioButton neutralRadio = new JRadioButton("Neutral");
|
||||
private final JRadioButton blockedRadio = new JRadioButton("Blocked");
|
||||
private final JRadioButton friendRadio = new JRadioButton("Friend");
|
||||
private final JTextField phoneticField = new JTextField(18);
|
||||
private final JRadioButton bothRadio = new JRadioButton(NicknameDisplay.BOTH.label());
|
||||
private final JRadioButton onlyCustomRadio = new JRadioButton(NicknameDisplay.ONLY_CUSTOM.label());
|
||||
private final JRadioButton onlyNicknameRadio = new JRadioButton(NicknameDisplay.ONLY_NICKNAME.label());
|
||||
private final JCheckBox automute = new JCheckBox("Mute");
|
||||
private final JCheckBox ignorePublic = new JCheckBox("Ignore server/channel chat");
|
||||
private final JCheckBox ignorePrivate = new JCheckBox("Ignore private chat");
|
||||
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 JPanel details = new JPanel(new BorderLayout(0, 8));
|
||||
|
||||
/** Set while the fields are being filled from a contact, so that does not count as an edit. */
|
||||
private boolean filling;
|
||||
|
||||
ContactsDialog(Frame owner, ContactStore contacts, Settings settings) {
|
||||
super(owner, "Contacts", false);
|
||||
this.contacts = contacts;
|
||||
this.settings = settings;
|
||||
|
||||
buildTree();
|
||||
JPanel left = new JPanel(new BorderLayout(0, 4));
|
||||
left.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 4));
|
||||
JLabel hint = new JLabel("Drag & Drop users here");
|
||||
hint.setForeground(Theme.chatSystem());
|
||||
left.add(hint, BorderLayout.NORTH);
|
||||
JScrollPane scroll = new JScrollPane(tree);
|
||||
scroll.setPreferredSize(new Dimension(300, 340));
|
||||
left.add(scroll, BorderLayout.CENTER);
|
||||
|
||||
details.setBorder(BorderFactory.createEmptyBorder(8, 4, 8, 8));
|
||||
details.add(buildNicknameRow(), BorderLayout.NORTH);
|
||||
details.add(buildPages(), BorderLayout.CENTER);
|
||||
|
||||
JPanel buttons = new JPanel(new BorderLayout());
|
||||
buttons.setBorder(BorderFactory.createEmptyBorder(0, 8, 8, 8));
|
||||
JPanel leftButtons = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 0));
|
||||
JButton defaults = new JButton("Set Defaults");
|
||||
defaults.setToolTipText("Set default display, ignore and hide options");
|
||||
defaults.addActionListener(e -> new ContactDefaultsDialog(this, settings).setVisible(true));
|
||||
leftButtons.add(defaults);
|
||||
if (TeamSpeakSettingsDb.locate() != null) {
|
||||
JButton importButton = new JButton("Import from TeamSpeak 3…");
|
||||
importButton.setToolTipText("Add the contacts of the TeamSpeak 3 client installed on this computer");
|
||||
importButton.addActionListener(e -> importFromTeamSpeak());
|
||||
leftButtons.add(importButton);
|
||||
}
|
||||
buttons.add(leftButtons, BorderLayout.WEST);
|
||||
JButton close = new JButton("Close");
|
||||
close.addActionListener(e -> dispose());
|
||||
JPanel rightButtons = new JPanel(new FlowLayout(FlowLayout.RIGHT, 0, 0));
|
||||
rightButtons.add(close);
|
||||
buttons.add(rightButtons, BorderLayout.EAST);
|
||||
|
||||
getContentPane().setLayout(new BorderLayout());
|
||||
getContentPane().add(left, BorderLayout.WEST);
|
||||
getContentPane().add(details, BorderLayout.CENTER);
|
||||
getContentPane().add(buttons, BorderLayout.SOUTH);
|
||||
|
||||
contacts.addListener(storeListener);
|
||||
addWindowListener(new java.awt.event.WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosed(java.awt.event.WindowEvent e) {
|
||||
contacts.removeListener(storeListener);
|
||||
}
|
||||
});
|
||||
|
||||
reload();
|
||||
Dialogs.closeOnEscape(this);
|
||||
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
|
||||
pack();
|
||||
setMinimumSize(new Dimension(620, 400));
|
||||
setLocationRelativeTo(owner);
|
||||
}
|
||||
|
||||
// ---- tree ----
|
||||
|
||||
private void buildTree() {
|
||||
root.add(friends);
|
||||
root.add(blocked);
|
||||
root.add(neutral);
|
||||
tree.setRootVisible(false);
|
||||
tree.setShowsRootHandles(true);
|
||||
tree.setRowHeight(20);
|
||||
tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
|
||||
tree.setCellRenderer(new ContactRenderer());
|
||||
tree.addTreeSelectionListener(e -> showSelected());
|
||||
tree.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mousePressed(MouseEvent e) {
|
||||
maybePopup(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
maybePopup(e);
|
||||
}
|
||||
});
|
||||
tree.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "remove");
|
||||
tree.getActionMap().put("remove", new javax.swing.AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(java.awt.event.ActionEvent e) {
|
||||
removeSelected();
|
||||
}
|
||||
});
|
||||
tree.setTransferHandler(new DropHandler());
|
||||
}
|
||||
|
||||
/** Re-lists the contacts, keeping the selection on the same unique id. */
|
||||
private void reload() {
|
||||
String selectedUid = selectedContact() == null ? null : selectedContact().uniqueId;
|
||||
for (DefaultMutableTreeNode group : List.of(friends, blocked, neutral)) group.removeAllChildren();
|
||||
List<Contact> sorted = new ArrayList<>(contacts.all());
|
||||
sorted.sort(Comparator.comparing((Contact c) -> c.nickname, String.CASE_INSENSITIVE_ORDER)
|
||||
.thenComparing(c -> c.uniqueId));
|
||||
for (Contact c : sorted) groupOf(c.type).add(new DefaultMutableTreeNode(c));
|
||||
treeModel.reload();
|
||||
for (int i = 0; i < tree.getRowCount(); i++) tree.expandRow(i);
|
||||
if (selectedUid != null) select(selectedUid);
|
||||
else showSelected();
|
||||
}
|
||||
|
||||
private DefaultMutableTreeNode groupOf(ContactType type) {
|
||||
switch (type) {
|
||||
case FRIEND:
|
||||
return friends;
|
||||
case BLOCKED:
|
||||
return blocked;
|
||||
default:
|
||||
return neutral;
|
||||
}
|
||||
}
|
||||
|
||||
/** Selects the contact with a unique id, if it is on the list. */
|
||||
void select(String uniqueId) {
|
||||
java.util.Enumeration<?> nodes = root.depthFirstEnumeration();
|
||||
while (nodes.hasMoreElements()) {
|
||||
DefaultMutableTreeNode n = (DefaultMutableTreeNode) nodes.nextElement();
|
||||
if (n.getUserObject() instanceof Contact && ((Contact) n.getUserObject()).uniqueId.equals(uniqueId)) {
|
||||
TreePath path = new TreePath(n.getPath());
|
||||
tree.setSelectionPath(path);
|
||||
tree.scrollPathToVisible(path);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Contact selectedContact() {
|
||||
TreePath path = tree.getSelectionPath();
|
||||
if (path == null) return null;
|
||||
Object obj = ((DefaultMutableTreeNode) path.getLastPathComponent()).getUserObject();
|
||||
return obj instanceof Contact ? (Contact) obj : null;
|
||||
}
|
||||
|
||||
private void maybePopup(MouseEvent e) {
|
||||
if (!e.isPopupTrigger()) return;
|
||||
TreePath path = tree.getPathForLocation(e.getX(), e.getY());
|
||||
if (path == null) return;
|
||||
tree.setSelectionPath(path);
|
||||
Contact contact = selectedContact();
|
||||
if (contact == null) return;
|
||||
|
||||
JPopupMenu menu = new JPopupMenu();
|
||||
for (ContactType type : new ContactType[]{ContactType.FRIEND, ContactType.BLOCKED, ContactType.NEUTRAL}) {
|
||||
JMenuItem item = new javax.swing.JRadioButtonMenuItem(type.label(), type == contact.type);
|
||||
item.addActionListener(a -> edit(c -> c.type = type));
|
||||
menu.add(item);
|
||||
}
|
||||
menu.addSeparator();
|
||||
JMenuItem remove = new JMenuItem("Remove", Icons.of("DELETE"));
|
||||
remove.addActionListener(a -> removeSelected());
|
||||
menu.add(remove);
|
||||
JMenuItem applyDefaults = new JMenuItem("Apply Defaults");
|
||||
applyDefaults.addActionListener(a -> edit(c -> c.applyDefaults(settings.contactDefaults.of(c.type))));
|
||||
menu.add(applyDefaults);
|
||||
menu.addSeparator();
|
||||
JMenuItem copyUid = new JMenuItem("Copy Unique ID to Clipboard", Icons.of("COPY"));
|
||||
copyUid.addActionListener(a -> Toolkit.getDefaultToolkit().getSystemClipboard()
|
||||
.setContents(new StringSelection(contact.uniqueId), null));
|
||||
menu.add(copyUid);
|
||||
menu.show(tree, e.getX(), e.getY());
|
||||
}
|
||||
|
||||
private void removeSelected() {
|
||||
Contact contact = selectedContact();
|
||||
if (contact == null) return;
|
||||
int answer = JOptionPane.showConfirmDialog(this,
|
||||
"Do you really want to delete contact " + contact.nickname + "?", "Remove Contact",
|
||||
JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
|
||||
if (answer == JOptionPane.YES_OPTION) contacts.remove(contact.uniqueId);
|
||||
}
|
||||
|
||||
private void importFromTeamSpeak() {
|
||||
File db = TeamSpeakSettingsDb.locate();
|
||||
if (db == null) return;
|
||||
try {
|
||||
List<Contact> imported = TeamSpeakSettingsDb.readContacts(db);
|
||||
int added = contacts.merge(imported);
|
||||
JOptionPane.showMessageDialog(this, imported.size() + " contact(s) read from " + db
|
||||
+ "\n" + added + " new, " + (imported.size() - added) + " updated.",
|
||||
"Import from TeamSpeak 3", JOptionPane.INFORMATION_MESSAGE);
|
||||
} catch (Exception e) {
|
||||
JOptionPane.showMessageDialog(this, "Could not read " + db + ":\n" + e.getMessage(),
|
||||
"Import from TeamSpeak 3", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- details ----
|
||||
|
||||
private JComponent buildNicknameRow() {
|
||||
JPanel row = new JPanel(new GridBagLayout());
|
||||
GridBagConstraints c = new GridBagConstraints();
|
||||
c.insets = new Insets(2, 2, 2, 2);
|
||||
c.anchor = GridBagConstraints.WEST;
|
||||
c.gridy = 0;
|
||||
c.gridx = 0;
|
||||
row.add(new JLabel("Custom Nickname:"), c);
|
||||
c.gridx = 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.weightx = 0;
|
||||
c.fill = GridBagConstraints.NONE;
|
||||
nicknameApply.setToolTipText("Apply nickname change");
|
||||
nicknameApply.addActionListener(e -> applyNickname());
|
||||
row.add(nicknameApply, c);
|
||||
return row;
|
||||
}
|
||||
|
||||
private JComponent buildPages() {
|
||||
JTabbedPane pages = new JTabbedPane();
|
||||
pages.addTab("General", page(
|
||||
radioGroup(neutralRadio, blockedRadio, friendRadio),
|
||||
Box.createVerticalStrut(8),
|
||||
labelled("Phonetic Nickname:", phoneticField)));
|
||||
neutralRadio.setToolTipText("Mark as neutral");
|
||||
blockedRadio.setToolTipText("Mark as blocked");
|
||||
friendRadio.setToolTipText("Mark as friend");
|
||||
phoneticField.setToolTipText("Client nickname written as phonemes (optional)");
|
||||
neutralRadio.addActionListener(e -> edit(c -> c.type = ContactType.NEUTRAL));
|
||||
blockedRadio.addActionListener(e -> edit(c -> c.type = ContactType.BLOCKED));
|
||||
friendRadio.addActionListener(e -> edit(c -> c.type = ContactType.FRIEND));
|
||||
phoneticField.addActionListener(e -> applyPhonetic());
|
||||
phoneticField.addFocusListener(new java.awt.event.FocusAdapter() {
|
||||
@Override
|
||||
public void focusLost(java.awt.event.FocusEvent e) {
|
||||
applyPhonetic();
|
||||
}
|
||||
});
|
||||
|
||||
pages.addTab("Display", page(radioGroup(bothRadio, onlyCustomRadio, onlyNicknameRadio)));
|
||||
bothRadio.setToolTipText("Show custom name and nickname");
|
||||
onlyCustomRadio.setToolTipText("Show only custom name");
|
||||
onlyNicknameRadio.setToolTipText("Show nickname only");
|
||||
bothRadio.addActionListener(e -> edit(c -> c.nicknameDisplay = NicknameDisplay.BOTH));
|
||||
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));
|
||||
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");
|
||||
ignorePokes.setToolTipText("Ignore pokes this person sends you");
|
||||
ignoreAvatar.setToolTipText("Do not download this person's avatar");
|
||||
ignoreAway.setToolTipText("Ignore this person's away messages");
|
||||
automute.addActionListener(e -> edit(c -> c.automute = automute.isSelected()));
|
||||
ignorePublic.addActionListener(e -> edit(c -> c.ignorePublicMessages = ignorePublic.isSelected()));
|
||||
ignorePrivate.addActionListener(e -> edit(c -> c.ignorePrivateMessages = ignorePrivate.isSelected()));
|
||||
ignorePokes.addActionListener(e -> edit(c -> c.ignorePokes = ignorePokes.isSelected()));
|
||||
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;
|
||||
}
|
||||
|
||||
private static JPanel page(Component... rows) {
|
||||
JPanel p = new JPanel();
|
||||
p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
|
||||
p.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
|
||||
for (Component row : rows) {
|
||||
if (row instanceof JComponent) ((JComponent) row).setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||
p.add(row);
|
||||
}
|
||||
p.add(Box.createVerticalGlue());
|
||||
return p;
|
||||
}
|
||||
|
||||
private static JPanel radioGroup(JRadioButton... buttons) {
|
||||
ButtonGroup group = new ButtonGroup();
|
||||
JPanel p = new JPanel();
|
||||
p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
|
||||
for (JRadioButton b : buttons) {
|
||||
group.add(b);
|
||||
b.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||
p.add(b);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
private static JPanel labelled(String label, JComponent field) {
|
||||
JPanel p = new JPanel(new BorderLayout(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();
|
||||
filling = true;
|
||||
try {
|
||||
setDetailsEnabled(details, c != null);
|
||||
if (c == null) {
|
||||
nicknameField.setText("");
|
||||
phoneticField.setText("");
|
||||
return;
|
||||
}
|
||||
nicknameField.setText(c.nickname);
|
||||
phoneticField.setText(c.phoneticNickname);
|
||||
(c.type == ContactType.FRIEND ? friendRadio : c.type == ContactType.BLOCKED ? blockedRadio : neutralRadio)
|
||||
.setSelected(true);
|
||||
(c.nicknameDisplay == NicknameDisplay.ONLY_CUSTOM ? onlyCustomRadio
|
||||
: c.nicknameDisplay == NicknameDisplay.ONLY_NICKNAME ? onlyNicknameRadio : bothRadio)
|
||||
.setSelected(true);
|
||||
automute.setSelected(c.automute);
|
||||
ignorePublic.setSelected(c.ignorePublicMessages);
|
||||
ignorePrivate.setSelected(c.ignorePrivateMessages);
|
||||
ignorePokes.setSelected(c.ignorePokes);
|
||||
ignoreAvatar.setSelected(c.ignoreAvatar);
|
||||
ignoreAway.setSelected(c.ignoreAwayMessage);
|
||||
(c.whisperAllowed ? allowWhisper : denyWhisper).setSelected(true);
|
||||
} finally {
|
||||
filling = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void setDetailsEnabled(Component component, boolean enabled) {
|
||||
component.setEnabled(enabled);
|
||||
if (component instanceof java.awt.Container) {
|
||||
for (Component child : ((java.awt.Container) component).getComponents()) {
|
||||
setDetailsEnabled(child, enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Changes the selected contact and stores it; the store's change notice re-lists the tree. */
|
||||
private void edit(Consumer<Contact> change) {
|
||||
if (filling) return;
|
||||
Contact c = selectedContact();
|
||||
if (c == null) return;
|
||||
change.accept(c);
|
||||
contacts.put(c);
|
||||
}
|
||||
|
||||
private void applyNickname() {
|
||||
String name = nicknameField.getText().trim();
|
||||
Contact c = selectedContact();
|
||||
if (c == null || name.isEmpty() || name.equals(c.nickname)) return;
|
||||
edit(contact -> contact.nickname = name);
|
||||
}
|
||||
|
||||
private void applyPhonetic() {
|
||||
String name = phoneticField.getText().trim();
|
||||
Contact c = selectedContact();
|
||||
if (c == null || name.equals(c.phoneticNickname)) return;
|
||||
edit(contact -> contact.phoneticNickname = name);
|
||||
}
|
||||
|
||||
// ---- drop target ----
|
||||
|
||||
/** Accepts clients dragged out of a server tree, whose transfer text is their TS3 link. */
|
||||
private final class DropHandler extends TransferHandler {
|
||||
@Override
|
||||
public boolean canImport(TransferSupport support) {
|
||||
return support.isDataFlavorSupported(DataFlavor.stringFlavor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean importData(TransferSupport support) {
|
||||
String text;
|
||||
try {
|
||||
text = (String) support.getTransferable().getTransferData(DataFlavor.stringFlavor);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
boolean added = false;
|
||||
Matcher m = CLIENT_LINK.matcher(text);
|
||||
while (m.find()) {
|
||||
TsLink.Ref ref = TsLink.parse(m.group());
|
||||
if (ref == null || !ref.client || ref.uniqueId.isEmpty()) continue;
|
||||
if (contacts.find(ref.uniqueId) == null) {
|
||||
contacts.add(ref.uniqueId, ref.name, ContactType.NEUTRAL, settings.contactDefaults);
|
||||
}
|
||||
select(ref.uniqueId);
|
||||
added = true;
|
||||
}
|
||||
return added;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- rendering ----
|
||||
|
||||
private static final class ContactRenderer extends DefaultTreeCellRenderer {
|
||||
@Override
|
||||
public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded,
|
||||
boolean leaf, int row, boolean hasFocus) {
|
||||
super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
|
||||
Object obj = ((DefaultMutableTreeNode) value).getUserObject();
|
||||
if (obj instanceof Contact) {
|
||||
Contact c = (Contact) obj;
|
||||
setText("<html>" + esc(c.nickname) + " <span style='color:" + hex(sel ? getForeground() : Theme.chatSystem())
|
||||
+ "'> " + esc(lastSeen(c)) + "</span></html>");
|
||||
setIcon(Icons.clientIdle());
|
||||
setFont(Theme.uiFont());
|
||||
} else {
|
||||
setText(String.valueOf(obj));
|
||||
setIcon(null);
|
||||
setFont(Theme.uiBold());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private static String lastSeen(Contact c) {
|
||||
if (c.lastSeen == null) return "Never";
|
||||
String where = c.lastSeenServerName.isEmpty() ? c.lastSeenServerAddress : c.lastSeenServerName;
|
||||
return where.isEmpty() ? SEEN.format(c.lastSeen) : SEEN.format(c.lastSeen) + " on " + where;
|
||||
}
|
||||
|
||||
private static String esc(String s) {
|
||||
return s.replace("&", "&").replace("<", "<").replace(">", ">");
|
||||
}
|
||||
|
||||
private static String hex(java.awt.Color c) {
|
||||
return String.format("#%02x%02x%02x", c.getRed(), c.getGreen(), c.getBlue());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.ts3client.ui;
|
||||
|
||||
import com.ts3client.config.Settings;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.DefaultListCellRenderer;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JCheckBox;
|
||||
import javax.swing.JComboBox;
|
||||
import javax.swing.JDialog;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JList;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.SwingUtilities;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Component;
|
||||
import java.awt.GridBagConstraints;
|
||||
import java.awt.GridBagLayout;
|
||||
import java.awt.Insets;
|
||||
|
||||
/**
|
||||
* Options page for the contact list: whether friends and blocked clients are
|
||||
* coloured in the server tree, whose whispers get through (TeamSpeak's whisper
|
||||
* options), and the defaults a new contact starts with.
|
||||
*/
|
||||
final class ContactsPanel extends JPanel {
|
||||
|
||||
private final Settings settings;
|
||||
private final JCheckBox colorContacts = new JCheckBox("Colour friends and blocked clients in the server tree");
|
||||
private final JComboBox<Settings.WhisperPolicy> whisperPolicy = new JComboBox<>(Settings.WhisperPolicy.values());
|
||||
|
||||
ContactsPanel(Settings settings) {
|
||||
super(new BorderLayout());
|
||||
this.settings = settings;
|
||||
setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
|
||||
|
||||
JPanel p = new JPanel(new GridBagLayout());
|
||||
GridBagConstraints c = new GridBagConstraints();
|
||||
c.insets = new Insets(2, 2, 2, 2);
|
||||
c.anchor = GridBagConstraints.WEST;
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
c.gridx = 0;
|
||||
c.gridwidth = 2;
|
||||
|
||||
c.gridy = 0;
|
||||
colorContacts.setSelected(settings.colorContacts);
|
||||
colorContacts.setToolTipText("Friends in green and blocked clients in red, as the TeamSpeak client shows them");
|
||||
p.add(colorContacts, c);
|
||||
|
||||
c.gridy = 1;
|
||||
c.insets = new Insets(12, 2, 2, 2);
|
||||
p.add(new JLabel("Whispers: who is allowed to whisper to you"), c);
|
||||
c.gridy = 2;
|
||||
c.insets = new Insets(2, 2, 2, 2);
|
||||
whisperPolicy.setSelectedItem(settings.whisperPolicy);
|
||||
whisperPolicy.setRenderer(new DefaultListCellRenderer() {
|
||||
@Override
|
||||
public Component getListCellRendererComponent(JList<?> list, Object value, int index,
|
||||
boolean selected, boolean focus) {
|
||||
super.getListCellRendererComponent(list, value, index, selected, focus);
|
||||
if (value instanceof Settings.WhisperPolicy) setText(((Settings.WhisperPolicy) value).label());
|
||||
return this;
|
||||
}
|
||||
});
|
||||
whisperPolicy.setToolTipText("<html>Use individual settings in contacts, else allow: a client not on your "
|
||||
+ "contact list may whisper to you.<br>Use individual settings in contacts, else deny: only "
|
||||
+ "contacts marked \"Allow whisper\" may.<br>Nobody: individual contact settings are ignored.</html>");
|
||||
p.add(whisperPolicy, c);
|
||||
|
||||
c.gridy = 3;
|
||||
c.insets = new Insets(12, 2, 2, 2);
|
||||
c.fill = GridBagConstraints.NONE;
|
||||
JButton defaults = new JButton("Contact defaults…");
|
||||
defaults.setToolTipText("Configure default settings when adding a client to the Contact Manager");
|
||||
defaults.addActionListener(e -> {
|
||||
java.awt.Window owner = SwingUtilities.getWindowAncestor(this);
|
||||
new ContactDefaultsDialog(owner instanceof JDialog ? (JDialog) owner : null, settings).setVisible(true);
|
||||
});
|
||||
p.add(defaults, c);
|
||||
|
||||
add(p, BorderLayout.NORTH);
|
||||
}
|
||||
|
||||
/** Copies the page into the settings; the caller saves them. */
|
||||
void apply() {
|
||||
settings.colorContacts = colorContacts.isSelected();
|
||||
settings.whisperPolicy = (Settings.WhisperPolicy) whisperPolicy.getSelectedItem();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.ts3client.ui;
|
||||
|
||||
import com.ts3client.contacts.Contact;
|
||||
import com.ts3client.net.ChannelNode;
|
||||
import com.ts3client.net.ClientEntry;
|
||||
import com.ts3client.net.Group;
|
||||
@@ -50,6 +51,7 @@ public final class InfoPanel extends JPanel {
|
||||
private ClientEntry shownClient;
|
||||
private ServerModel shownModel;
|
||||
private IconRepository shownIcons;
|
||||
private Contact shownContact;
|
||||
private boolean inChatTab;
|
||||
|
||||
public InfoPanel() {
|
||||
@@ -144,11 +146,12 @@ public final class InfoPanel extends JPanel {
|
||||
render();
|
||||
}
|
||||
|
||||
public void showClient(ClientEntry cl, ServerModel model, IconRepository icons) {
|
||||
public void showClient(ClientEntry cl, ServerModel model, IconRepository icons, Contact contact) {
|
||||
shownClient = cl;
|
||||
shownChannel = null;
|
||||
shownModel = model;
|
||||
shownIcons = icons;
|
||||
shownContact = contact;
|
||||
render();
|
||||
}
|
||||
|
||||
@@ -166,7 +169,7 @@ public final class InfoPanel extends JPanel {
|
||||
|
||||
private String body() {
|
||||
if (shownChannel != null) return channelBody(shownChannel);
|
||||
if (shownClient != null) return clientBody(shownClient, shownModel, shownIcons);
|
||||
if (shownClient != null) return clientBody(shownClient, shownModel, shownIcons, shownContact);
|
||||
return "<i class='muted'>Select a channel or client to see details.</i>";
|
||||
}
|
||||
|
||||
@@ -187,9 +190,15 @@ public final class InfoPanel extends JPanel {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String clientBody(ClientEntry cl, ServerModel model, IconRepository icons) {
|
||||
private static String clientBody(ClientEntry cl, ServerModel model, IconRepository icons, Contact contact) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(heading(esc(cl.nickname) + (cl.self ? " <span class='muted'>(you)</span>" : "")));
|
||||
String name = contact == null ? cl.nickname : contact.displayName(cl.nickname);
|
||||
sb.append(heading(esc(name) + (cl.self ? " <span class='muted'>(you)</span>" : "")));
|
||||
if (contact != null) {
|
||||
String type = contact.type.label();
|
||||
if (!contact.phoneticNickname.isEmpty()) type += " (" + esc(contact.phoneticNickname) + ")";
|
||||
row(sb, "Contact", type);
|
||||
}
|
||||
|
||||
List<Group> serverGroups = model.serverGroupsOf(cl.serverGroupIds);
|
||||
if (serverGroups.isEmpty()) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.ts3client.config.Bookmark;
|
||||
import com.ts3client.config.Bookmarks;
|
||||
import com.ts3client.config.IdentityStore;
|
||||
import com.ts3client.config.Settings;
|
||||
import com.ts3client.contacts.ContactStore;
|
||||
import com.ts3client.net.ChannelNode;
|
||||
import com.ts3client.sound.SoundNotifier;
|
||||
import com.ts3client.sound.SoundPlayer;
|
||||
@@ -46,6 +47,8 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
private final Bookmarks bookmarks = Bookmarks.load();
|
||||
private final AwayMessages awayMessages = AwayMessages.load();
|
||||
private final IdentityStore identities;
|
||||
/** Friends and blocked clients, shared by every connection. */
|
||||
private final ContactStore contacts = ContactStore.load();
|
||||
private final AudioBackend audio = new DesktopAudioBackend();
|
||||
/** Sound pack playback, shared by every connection. */
|
||||
private final SoundNotifier sounds;
|
||||
@@ -86,6 +89,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
this.soundPlayer = audio.createSoundPlayer(settings);
|
||||
this.sounds.setPlayer(soundPlayer);
|
||||
this.hotkeys = new HotkeyService(new HotkeyActions(this), this::hotkeyArgumentChoices);
|
||||
contacts.addListener(() -> SwingUtilities.invokeLater(this::contactsChanged));
|
||||
|
||||
setIconImage(Icons.app().getImage());
|
||||
// We tear the connections down ourselves on close, so don't let Swing kill the JVM.
|
||||
@@ -193,6 +197,11 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
showIdentities();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShowContacts() {
|
||||
showContacts(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canViewBanList() {
|
||||
return selected != null && selected.canViewBanList();
|
||||
@@ -310,7 +319,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
// ---- tab management ----
|
||||
|
||||
private ServerTab newTab() {
|
||||
ServerTab tab = new ServerTab(this, settings, identities, audio, sounds);
|
||||
ServerTab tab = new ServerTab(this, settings, identities, audio, sounds, contacts);
|
||||
tabs.add(tab);
|
||||
tabPane.addTab(tab);
|
||||
refreshTabs();
|
||||
@@ -765,6 +774,32 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
new IdentitiesDialog(this, identities, settings, bookmarks, null).setVisible(true);
|
||||
}
|
||||
|
||||
Settings settings() {
|
||||
return settings;
|
||||
}
|
||||
|
||||
/** The one contacts window, kept so a second request brings it forward instead of doubling it. */
|
||||
private ContactsDialog contactsDialog;
|
||||
|
||||
/**
|
||||
* Opens the contact list, or brings it forward.
|
||||
*
|
||||
* @param uniqueId the contact to select, or {@code null} to keep the current selection
|
||||
*/
|
||||
void showContacts(String uniqueId) {
|
||||
if (contactsDialog == null || !contactsDialog.isDisplayable()) {
|
||||
contactsDialog = new ContactsDialog(this, contacts, settings);
|
||||
}
|
||||
if (uniqueId != null) contactsDialog.select(uniqueId);
|
||||
contactsDialog.setVisible(true);
|
||||
contactsDialog.toFront();
|
||||
}
|
||||
|
||||
/** The contact list changed somewhere: every open server follows. */
|
||||
private void contactsChanged() {
|
||||
for (ServerTab tab : tabs) tab.contactsChanged();
|
||||
}
|
||||
|
||||
private void showSettings() {
|
||||
// Device and voice-gating changes apply to the capturing connection and the
|
||||
// playback of the visible one; the rest pick the new settings up on connect.
|
||||
@@ -772,7 +807,10 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
micTab == null ? null : micTab.connection().getMicrophone(),
|
||||
selected == null ? null : selected.connection().getPlayback(),
|
||||
sounds, hotkeys,
|
||||
this::applyOutputSettingsToAllTabs);
|
||||
() -> {
|
||||
applyOutputSettingsToAllTabs();
|
||||
contactsChanged();
|
||||
});
|
||||
dlg.setVisible(true);
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,8 @@ final class MainMenuBar extends JMenuBar {
|
||||
|
||||
void onShowIdentities();
|
||||
|
||||
void onShowContacts();
|
||||
|
||||
/** Whether the current server lets us read its ban list; asked as the Tools menu opens. */
|
||||
boolean canViewBanList();
|
||||
|
||||
@@ -123,12 +125,15 @@ final class MainMenuBar extends JMenuBar {
|
||||
JMenu tools = new JMenu("Tools");
|
||||
JMenuItem identitiesItem = new JMenuItem("Identities…", Icons.of("IDENTITY_MANAGER"));
|
||||
identitiesItem.addActionListener(e -> listener.onShowIdentities());
|
||||
JMenuItem contactsItem = new JMenuItem("Contacts…", Icons.ofAny("CONTACT", "EDIT_FRIEND_FOE_STATUS"));
|
||||
contactsItem.addActionListener(e -> listener.onShowContacts());
|
||||
banListItem = new JMenuItem("Ban List", Icons.of("BAN_LIST"));
|
||||
banListItem.addActionListener(e -> listener.onShowBanList());
|
||||
JMenuItem options = new JMenuItem("Options…", Icons.of("SETTINGS"));
|
||||
options.addActionListener(e -> listener.onShowSettings());
|
||||
JPopupMenu.Separator banListSeparator = new JPopupMenu.Separator();
|
||||
tools.add(identitiesItem);
|
||||
tools.add(contactsItem);
|
||||
tools.addSeparator();
|
||||
tools.add(banListItem);
|
||||
tools.add(banListSeparator);
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.ts3client.config.IdentityEntry;
|
||||
import com.ts3client.config.IdentityStore;
|
||||
import com.ts3client.config.BanReasonPresets;
|
||||
import com.ts3client.config.Settings;
|
||||
import com.ts3client.contacts.ContactStore;
|
||||
import com.ts3client.net.ChannelNode;
|
||||
import com.ts3client.net.ClientEntry;
|
||||
import com.ts3client.net.ServerModel;
|
||||
@@ -64,7 +65,7 @@ final class ServerTab implements ServerTabConnectionEvents.Listener {
|
||||
private BanListDialog banList;
|
||||
|
||||
ServerTab(MainFrame host, Settings settings, IdentityStore identities, AudioBackend audio,
|
||||
SoundNotifier sounds) {
|
||||
SoundNotifier sounds, ContactStore contacts) {
|
||||
this.host = host;
|
||||
this.settings = settings;
|
||||
this.identities = identities;
|
||||
@@ -74,12 +75,12 @@ final class ServerTab implements ServerTabConnectionEvents.Listener {
|
||||
// panels and the other tab helpers that read from it — so wiring finishes with
|
||||
// an explicit attach() once everything is built. Nothing fires callbacks before then.
|
||||
this.events = new ServerTabConnectionEvents(host, this, this);
|
||||
this.conn = new TeamspeakConnection(settings, audio, events, sounds);
|
||||
this.conn = new TeamspeakConnection(settings, audio, events, sounds, contacts);
|
||||
this.groupIcons = new GroupIcons(conn.getIcons());
|
||||
this.selfState = new ServerTabSelfState(conn);
|
||||
this.chatPanel = new ChatPanel();
|
||||
this.treeActions = new ServerTabTreeActions(host, this, conn, chatPanel, infoPanel, groupIcons);
|
||||
this.treePanel = new ServerTreePanel(conn.getModel(), groupIcons, treeActions);
|
||||
this.treePanel = new ServerTreePanel(conn.getModel(), groupIcons, contacts, settings, treeActions);
|
||||
treeActions.attach(treePanel);
|
||||
events.attach(conn, treePanel, chatPanel, selfState, treeActions);
|
||||
|
||||
@@ -298,6 +299,13 @@ final class ServerTab implements ServerTabConnectionEvents.Listener {
|
||||
else banList.connectionLost();
|
||||
}
|
||||
|
||||
/** The contact list changed: names, colours and per-client playback follow. */
|
||||
void contactsChanged() {
|
||||
conn.applyContacts();
|
||||
treePanel.refreshRowSizes();
|
||||
treeActions.renderInfo();
|
||||
}
|
||||
|
||||
/** Releases the background resources of a tab that is being thrown away. */
|
||||
void dispose() {
|
||||
conn.getIcons().shutdown();
|
||||
@@ -388,7 +396,7 @@ final class ServerTab implements ServerTabConnectionEvents.Listener {
|
||||
|
||||
private String peerName(int clientId) {
|
||||
ClientEntry c = conn.getModel().getClient(clientId);
|
||||
return c != null ? c.nickname : "Client " + clientId;
|
||||
return c != null ? conn.displayName(c) : "Client " + clientId;
|
||||
}
|
||||
|
||||
/** Joins the channel at a "/"-separated path, as the hotkey action names it. */
|
||||
|
||||
@@ -132,7 +132,8 @@ final class ServerTabConnectionEvents implements ConnectionListener {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChat(ChatScope scope, int fromClientId, String fromUniqueId, String fromName, String message) {
|
||||
public void onChat(ChatScope scope, int fromClientId, String fromUniqueId, String nickname, String message) {
|
||||
String fromName = conn.getContacts().displayName(fromUniqueId, nickname);
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
switch (scope) {
|
||||
case PRIVATE:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.ts3client.ui;
|
||||
|
||||
import com.ts3client.contacts.ContactType;
|
||||
import com.ts3client.net.ChannelNode;
|
||||
import com.ts3client.net.ChannelSettings;
|
||||
import com.ts3client.net.ClientEntry;
|
||||
@@ -59,7 +60,8 @@ final class ServerTabTreeActions implements ServerTreePanel.Actions {
|
||||
if (sel instanceof ChannelNode) {
|
||||
infoPanel.showChannel((ChannelNode) sel);
|
||||
} else if (sel instanceof ClientEntry) {
|
||||
infoPanel.showClient((ClientEntry) sel, conn.getModel(), conn.getIcons());
|
||||
infoPanel.showClient((ClientEntry) sel, conn.getModel(), conn.getIcons(),
|
||||
conn.getContacts().find(((ClientEntry) sel).uniqueId));
|
||||
} else {
|
||||
infoPanel.clear();
|
||||
}
|
||||
@@ -123,7 +125,7 @@ final class ServerTabTreeActions implements ServerTreePanel.Actions {
|
||||
|
||||
@Override
|
||||
public void openPrivateChat(ClientEntry client) {
|
||||
chatPanel.openPrivateChat(client.id, client.uniqueId, client.nickname);
|
||||
chatPanel.openPrivateChat(client.id, client.uniqueId, conn.displayName(client));
|
||||
conn.loadPrivateChatHistory(client.id, client.uniqueId);
|
||||
}
|
||||
|
||||
@@ -230,6 +232,32 @@ final class ServerTabTreeActions implements ServerTreePanel.Actions {
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addContact(ClientEntry client, ContactType type) {
|
||||
conn.getContacts().add(client.uniqueId, client.nickname, type, host.settings().contactDefaults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void editContact(ClientEntry client) {
|
||||
host.showContacts(client.uniqueId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeContact(ClientEntry client) {
|
||||
conn.getContacts().remove(client.uniqueId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContactType contactTypeOf(ClientEntry client) {
|
||||
return conn.getContacts().typeOf(client.uniqueId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changeClientVolume(ClientEntry client) {
|
||||
if (!conn.isConnected()) return;
|
||||
new ClientVolumeDialog(host, conn, client).setVisible(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renameSelf(String nickname) {
|
||||
host.changeNickname(nickname);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package com.ts3client.ui;
|
||||
|
||||
import com.ts3client.config.Settings;
|
||||
import com.ts3client.contacts.Contact;
|
||||
import com.ts3client.contacts.ContactStore;
|
||||
import com.ts3client.net.ChannelNode;
|
||||
import com.ts3client.net.ClientEntry;
|
||||
|
||||
@@ -7,6 +10,7 @@ import javax.swing.ImageIcon;
|
||||
import javax.swing.JTree;
|
||||
import javax.swing.tree.DefaultMutableTreeNode;
|
||||
import javax.swing.tree.DefaultTreeCellRenderer;
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import java.util.function.IntPredicate;
|
||||
|
||||
@@ -18,9 +22,13 @@ final class ServerTreeCellRenderer extends DefaultTreeCellRenderer {
|
||||
|
||||
/** Whether a client id is our own, which the panel learns on connecting. */
|
||||
private final IntPredicate isSelf;
|
||||
private final ContactStore contacts;
|
||||
private final Settings settings;
|
||||
|
||||
ServerTreeCellRenderer(IntPredicate isSelf) {
|
||||
ServerTreeCellRenderer(IntPredicate isSelf, ContactStore contacts, Settings settings) {
|
||||
this.isSelf = isSelf;
|
||||
this.contacts = contacts;
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -49,15 +57,18 @@ final class ServerTreeCellRenderer extends DefaultTreeCellRenderer {
|
||||
}
|
||||
} else if (obj instanceof ClientEntry) {
|
||||
ClientEntry cl = (ClientEntry) obj;
|
||||
String label = cl.nickname;
|
||||
if (cl.away && !cl.awayMessage.isEmpty()) label += " [" + cl.awayMessage + "]";
|
||||
Contact contact = contacts.find(cl.uniqueId);
|
||||
String label = contact == null ? cl.nickname : contact.displayName(cl.nickname);
|
||||
if (cl.away && !cl.awayMessage.isEmpty() && (contact == null || !contact.ignoreAwayMessage)) {
|
||||
label += " [" + cl.awayMessage + "]";
|
||||
}
|
||||
setText(label);
|
||||
setIcon(iconFor(cl));
|
||||
// Talking shows in the client's icon, as it does in TeamSpeak. Marking the
|
||||
// name as well would also re-measure the row mid-speech, which leaves the
|
||||
// nickname clipped to an ellipsis for as long as it lasts. Our own name is
|
||||
// bold throughout, so its width never changes either.
|
||||
setForeground(Theme.treeText());
|
||||
setForeground(contactColor(contact));
|
||||
setFont(isSelf.test(cl.id) ? Theme.uiBold() : Theme.uiFont());
|
||||
} else {
|
||||
// root / server
|
||||
@@ -69,6 +80,19 @@ final class ServerTreeCellRenderer extends DefaultTreeCellRenderer {
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Friends and blocked clients stand out by colour, as in TeamSpeak, unless turned off. */
|
||||
private Color contactColor(Contact contact) {
|
||||
if (contact == null || !settings.colorContacts) return Theme.treeText();
|
||||
switch (contact.type) {
|
||||
case FRIEND:
|
||||
return Theme.friend();
|
||||
case BLOCKED:
|
||||
return Theme.blocked();
|
||||
default:
|
||||
return Theme.treeText();
|
||||
}
|
||||
}
|
||||
|
||||
private ImageIcon iconFor(ChannelNode c) {
|
||||
if (c.hasPassword) return Icons.channelLocked(c.subscribed);
|
||||
if (c.maxClients >= 0 && c.clients.size() >= c.maxClients) return Icons.channelFull(c.subscribed);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package com.ts3client.ui;
|
||||
|
||||
import com.ts3client.config.Settings;
|
||||
import com.ts3client.contacts.ContactStore;
|
||||
import com.ts3client.contacts.ContactType;
|
||||
import com.ts3client.net.ChannelNode;
|
||||
import com.ts3client.net.ChannelSettings;
|
||||
import com.ts3client.net.ClientEntry;
|
||||
@@ -106,6 +109,20 @@ public final class ServerTreePanel extends JScrollPane {
|
||||
|
||||
/** A new nickname for ourselves, typed into the tree row. */
|
||||
void renameSelf(String nickname);
|
||||
|
||||
/** Files a client in the contact list as a friend, blocked or neutral. */
|
||||
void addContact(ClientEntry client, ContactType type);
|
||||
|
||||
/** Opens the contact list at this client's entry. */
|
||||
void editContact(ClientEntry client);
|
||||
|
||||
void removeContact(ClientEntry client);
|
||||
|
||||
/** The type a client is filed under, or {@code null} for a client that is no contact. */
|
||||
ContactType contactTypeOf(ClientEntry client);
|
||||
|
||||
/** Opens the per-client volume dialog. */
|
||||
void changeClientVolume(ClientEntry client);
|
||||
}
|
||||
|
||||
private final DropIndicatorTree tree;
|
||||
@@ -134,7 +151,8 @@ public final class ServerTreePanel extends JScrollPane {
|
||||
private final NicknameCellEditor nicknameEditor;
|
||||
private int selfClientId = -1;
|
||||
|
||||
public ServerTreePanel(ServerModel model, GroupIcons groupIcons, Actions actions) {
|
||||
public ServerTreePanel(ServerModel model, GroupIcons groupIcons, ContactStore contacts, Settings settings,
|
||||
Actions actions) {
|
||||
this.model = model;
|
||||
this.groupIcons = groupIcons;
|
||||
this.actions = actions;
|
||||
@@ -153,7 +171,7 @@ public final class ServerTreePanel extends JScrollPane {
|
||||
tree.setRowHeight(20);
|
||||
tree.setBackground(Theme.treeBg());
|
||||
tree.setFont(Theme.uiFont());
|
||||
ServerTreeCellRenderer renderer = new ServerTreeCellRenderer(id -> id == selfClientId);
|
||||
ServerTreeCellRenderer renderer = new ServerTreeCellRenderer(id -> id == selfClientId, contacts, settings);
|
||||
tree.setCellRenderer(renderer);
|
||||
nicknameEditor = NicknameCellEditor.create(tree, renderer, id -> id == selfClientId);
|
||||
tree.setCellEditor(nicknameEditor);
|
||||
|
||||
@@ -35,6 +35,7 @@ public final class SettingsDialog extends JDialog {
|
||||
private final DesignPanel designPanel;
|
||||
private final HotkeysPanel hotkeysPanel;
|
||||
private final ClientVersionPanel clientVersionPanel;
|
||||
private final ContactsPanel contactsPanel;
|
||||
|
||||
public SettingsDialog(Frame owner, Settings settings,
|
||||
VoiceInput liveMic, VoiceOutput livePlayback,
|
||||
@@ -49,6 +50,7 @@ public final class SettingsDialog extends JDialog {
|
||||
designPanel = new DesignPanel(settings);
|
||||
hotkeysPanel = new HotkeysPanel(hotkeys);
|
||||
clientVersionPanel = new ClientVersionPanel(settings);
|
||||
contactsPanel = new ContactsPanel(settings);
|
||||
devicesPanel = new DevicesPanel(settings, livePlayback,
|
||||
this::applyLive, this::restartTest, this::setTestOutputDevice);
|
||||
voiceActivationPanel = new VoiceActivationPanel(settings, liveMic, hotkeys,
|
||||
@@ -60,6 +62,7 @@ public final class SettingsDialog extends JDialog {
|
||||
tabs.addTab("Notifications", notificationsPanel);
|
||||
tabs.addTab("Design", designPanel);
|
||||
tabs.addTab("Hotkeys", hotkeysPanel);
|
||||
tabs.addTab("Contacts", contactsPanel);
|
||||
tabs.addTab("Client Version", scrollable(clientVersionPanel));
|
||||
|
||||
JPanel buttons = new JPanel(new BorderLayout());
|
||||
@@ -130,6 +133,7 @@ public final class SettingsDialog extends JDialog {
|
||||
notificationsPanel.apply();
|
||||
designPanel.apply();
|
||||
clientVersionPanel.apply();
|
||||
contactsPanel.apply();
|
||||
hotkeysPanel.apply();
|
||||
settings.save();
|
||||
|
||||
|
||||
@@ -128,6 +128,16 @@ public final class Theme {
|
||||
return pick(0xC0392B, 0xE05C4C);
|
||||
}
|
||||
|
||||
/** TeamSpeak's {@code CustomColor::ClientFriend}, lifted for the dark scheme. */
|
||||
public static Color friend() {
|
||||
return pick(0x008000, 0x5CC96A);
|
||||
}
|
||||
|
||||
/** TeamSpeak's {@code CustomColor::ClientBlocked}, lifted for the dark scheme. */
|
||||
public static Color blocked() {
|
||||
return pick(0xFF0000, 0xF06A5E);
|
||||
}
|
||||
|
||||
public static Color accent() {
|
||||
return pick(0x2C7BE5, 0x5C9BEE);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user