Files
ts3j/ts3-client/swing/src/main/java/com/ts3client/ui/MainFrame.java
ericek111 245ae5ecc5 Add an away button with global actions and message presets
The toolbar gains an away toggle with a drop-down: it toggles away on the
current server with no message, while the menu carries the global actions
("Set Globally Away", "Set Globally Away Status"), the saved presets and
their editor. Presets live in ~/.ts3jclient/away.properties and are
managed in a list with add/remove and double-click renaming.

Clients in the channel tree now show their away message in brackets after
the nickname instead of their primary server group.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 13:40:19 +00:00

712 lines
28 KiB
Java

package com.ts3client.ui;
import com.ts3client.audio.AudioBackend;
import com.ts3client.audio.desktop.DesktopAudioBackend;
import com.ts3client.config.AwayMessages;
import com.ts3client.config.Bookmark;
import com.ts3client.config.Bookmarks;
import com.ts3client.config.IdentityStore;
import com.ts3client.config.Settings;
import com.ts3client.sound.SoundNotifier;
import com.ts3client.sound.SoundPlayer;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JTextField;
import javax.swing.JToggleButton;
import javax.swing.JToolBar;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.KeyEventDispatcher;
import java.awt.KeyboardFocusManager;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* The main application window: toolbar, tab bar, status bar and menus, hosting
* one {@link ServerTab} per open server connection.
*
* <p>Several servers can be connected at once — their incoming voice is mixed
* into the same output — but only one of them owns the microphone at a time
* ("active" tab, marked in the tab strip and switched from the toolbar).
*/
public final class MainFrame extends JFrame implements ServerTabPane.Listener {
private final Settings settings;
private final Bookmarks bookmarks = Bookmarks.load();
private final AwayMessages awayMessages = AwayMessages.load();
private final IdentityStore identities;
private final AudioBackend audio = new DesktopAudioBackend();
/** Sound pack playback, shared by every connection. */
private final SoundNotifier sounds;
private final SoundPlayer soundPlayer;
private final List<ServerTab> tabs = new ArrayList<>();
private final ServerTabPane tabPane = new ServerTabPane(this);
/** The tab whose views are on screen. */
private ServerTab selected;
/** The tab that owns the capture device; null when nobody is capturing. */
private ServerTab micTab;
private JMenu bookmarksMenu;
private JCheckBoxMenuItem awayItem;
private JMenuItem awayStatusItem;
private JCheckBoxMenuItem commanderItem;
private javax.swing.Timer statusTimer;
private final JLabel statusLabel = new JLabel("Not connected");
private final JLabel codecLabel = new JLabel();
private JToolBar toolbar;
private JButton connectButton;
private JButton disconnectButton;
private JToggleButton activeButton;
private JToggleButton micButton;
private JToggleButton speakerButton;
private DropDownToggleButton awayButton;
private boolean pttPressed;
/** Guards {@link #shutdown()} so the window listener and JVM hook don't both run it. */
private final AtomicBoolean shuttingDown = new AtomicBoolean(false);
private final Thread shutdownHook = new Thread(this::shutdown, "ts3j-shutdown");
public MainFrame(Settings settings) {
super("TS3J — TeamSpeak 3 Java Client");
this.settings = settings;
this.identities = IdentityStore.load(settings);
this.sounds = new SoundNotifier(settings);
this.soundPlayer = audio.createSoundPlayer(settings);
this.sounds.setPlayer(soundPlayer);
setIconImage(Icons.app().getImage());
// We tear the connections down ourselves on close, so don't let Swing kill the JVM.
setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
shutdown();
System.exit(0);
}
});
// Catches Ctrl+C / SIGTERM so we still leave the servers cleanly.
Runtime.getRuntime().addShutdownHook(shutdownHook);
setMinimumSize(new Dimension(720, 480));
setJMenuBar(buildMenuBar());
toolbar = buildToolbar();
add(toolbar, BorderLayout.NORTH);
// Switching the icon pack in the options dialog re-decorates the whole window.
IconTheme.get().addListener(() -> SwingUtilities.invokeLater(this::rebuildIcons));
add(tabPane, BorderLayout.CENTER);
add(buildStatusBar(), BorderLayout.SOUTH);
codecLabel.setText(audio.description());
ServerTab first = newTab();
selectTab(first);
first.chat().appendSystem("Welcome to the TS3J Swing client.");
first.chat().appendSystem("Use Connections → Connect to join a server.");
installPushToTalk();
statusTimer = new javax.swing.Timer(3000, e -> updateConnectionStatus());
statusTimer.start();
setSize(880, 560);
setLocationRelativeTo(null);
// Deferred so the window is on screen before we start connecting.
List<Bookmark> startup = bookmarks.startupBookmarks();
if (!startup.isEmpty()) {
SwingUtilities.invokeLater(() -> {
for (Bookmark b : startup) connectToBookmark(b);
});
}
}
// ---- UI construction ----
private JMenuBar buildMenuBar() {
JMenuBar bar = new JMenuBar();
JMenu connections = new JMenu("Connections");
JMenuItem connect = new JMenuItem("Connect…", Icons.of("CONNECT"));
connect.setAccelerator(KeyStroke.getKeyStroke("control S"));
connect.addActionListener(e -> showConnectDialog());
JMenuItem disconnect = new JMenuItem("Disconnect", Icons.of("DISCONNECT"));
disconnect.addActionListener(e -> doDisconnect());
JMenuItem closeTab = new JMenuItem("Close tab", Icons.of("CLOSE_BUTTON"));
closeTab.setAccelerator(KeyStroke.getKeyStroke("control W"));
closeTab.addActionListener(e -> closeTab(selected));
JMenuItem quit = new JMenuItem("Quit", Icons.of("QUIT"));
quit.addActionListener(e -> {
shutdown();
System.exit(0);
});
connections.add(connect);
connections.add(disconnect);
connections.add(closeTab);
connections.addSeparator();
connections.add(quit);
bookmarksMenu = new JMenu("Bookmarks");
rebuildBookmarksMenu();
JMenu self = new JMenu("Self");
JMenuItem mute = new JMenuItem("Toggle microphone", Icons.of("CAPTURE"));
mute.addActionListener(e -> micButton.doClick());
JMenuItem deaf = new JMenuItem("Toggle speakers", Icons.of("PLAYBACK"));
deaf.addActionListener(e -> speakerButton.doClick());
awayItem = new JCheckBoxMenuItem("Away", Icons.of("AWAY"), false);
awayItem.addActionListener(e -> toggleAway());
awayStatusItem = new JMenuItem("Set away status…", Icons.of("EDIT"));
awayStatusItem.addActionListener(e -> setAwayStatus());
commanderItem = new JCheckBoxMenuItem("Channel commander", Icons.of("CHANNEL_COMMANDER"), false);
commanderItem.addActionListener(e -> {
if (selected != null) selected.setCommander(commanderItem.isSelected());
});
JMenuItem nick = new JMenuItem("Change nickname…", Icons.of("CHANGE_NICKNAME"));
nick.addActionListener(e -> changeNickname());
self.add(mute);
self.add(deaf);
self.addSeparator();
self.add(awayItem);
self.add(awayStatusItem);
self.add(commanderItem);
self.addSeparator();
self.add(nick);
JMenu tools = new JMenu("Tools");
JMenuItem identitiesItem = new JMenuItem("Identities…", Icons.of("IDENTITY_MANAGER"));
identitiesItem.addActionListener(e -> showIdentities());
JMenuItem options = new JMenuItem("Options…", Icons.of("SETTINGS"));
options.addActionListener(e -> showSettings());
tools.add(identitiesItem);
tools.addSeparator();
tools.add(options);
JMenu help = new JMenu("Help");
JMenuItem about = new JMenuItem("About", Icons.of("ABOUT"));
about.addActionListener(e -> showAbout());
help.add(about);
bar.add(connections);
bar.add(bookmarksMenu);
bar.add(self);
bar.add(tools);
bar.add(help);
return bar;
}
private void rebuildBookmarksMenu() {
bookmarksMenu.removeAll();
for (Bookmark b : bookmarks.all()) {
JMenuItem item = new JMenuItem(b.displayName(), Icons.of("SERVER_GREEN"));
item.addActionListener(e -> connectToBookmark(b));
bookmarksMenu.add(item);
}
if (!bookmarks.all().isEmpty()) bookmarksMenu.addSeparator();
JMenuItem addCurrent = new JMenuItem("Add current server…", Icons.of("BOOKMARK_ADD"));
addCurrent.addActionListener(e -> addCurrentServerBookmark());
JMenuItem manage = new JMenuItem("Manage bookmarks…", Icons.of("BOOKMARK_MANAGER"));
manage.addActionListener(e -> new BookmarksDialog(this, bookmarks, identities,
this::connectToBookmark, this::rebuildBookmarksMenu).setVisible(true));
bookmarksMenu.add(addCurrent);
bookmarksMenu.add(manage);
}
private JToolBar buildToolbar() {
JToolBar tb = new JToolBar();
tb.setFloatable(false);
tb.setBackground(Theme.TOOLBAR_BG);
tb.setBorder(BorderFactory.createEmptyBorder(4, 6, 4, 6));
connectButton = new JButton(Icons.connect());
connectButton.setToolTipText("Connect to a server");
connectButton.addActionListener(e -> showConnectDialog());
disconnectButton = new JButton(Icons.disconnect());
disconnectButton.setToolTipText("Disconnect");
disconnectButton.addActionListener(e -> doDisconnect());
activeButton = new JToggleButton(Icons.micActive());
activeButton.setToolTipText("Speak on this server (moves the microphone to this tab)");
activeButton.addActionListener(e -> {
if (selected != null && selected.isConnected()) setMicTab(selected);
updateToolbar();
});
micButton = new JToggleButton(Icons.mic());
micButton.setToolTipText("Mute / unmute microphone on this server");
micButton.addActionListener(e -> {
if (selected == null) return;
selected.setMicMuted(micButton.isSelected());
updateToolbar();
});
speakerButton = new JToggleButton(Icons.speaker());
speakerButton.setToolTipText("Deafen / undeafen (mute speakers) on this server");
speakerButton.addActionListener(e -> {
if (selected == null) return;
selected.setDeafened(speakerButton.isSelected());
updateToolbar();
});
awayButton = new DropDownToggleButton(Icons.away(),
"Away on this server (the arrow offers the global actions and presets)",
this::buildAwayMenu);
awayButton.addActionListener(e -> {
if (selected == null) return;
// The plain toggle carries no message; the menu is where messages are chosen.
selected.setAway(awayButton.isSelected(), "");
updateToolbar();
});
JButton settingsButton = new JButton(Icons.settings());
settingsButton.setToolTipText("Options");
settingsButton.addActionListener(e -> showSettings());
tb.add(connectButton);
tb.add(disconnectButton);
tb.addSeparator();
tb.add(activeButton);
tb.add(micButton);
tb.add(speakerButton);
tb.add(awayButton);
tb.addSeparator();
tb.add(settingsButton);
tb.add(Box.createHorizontalGlue());
return tb;
}
/** Rebuilds the icon-bearing chrome after the active icon pack changed. */
private void rebuildIcons() {
setIconImage(Icons.app().getImage());
setJMenuBar(buildMenuBar());
remove(toolbar);
toolbar = buildToolbar();
add(toolbar, BorderLayout.NORTH);
updateToolbar();
refreshTabs();
revalidate();
repaint();
}
private JPanel buildStatusBar() {
JPanel bar = new JPanel(new BorderLayout());
bar.setBackground(Theme.STATUS_BG);
bar.setBorder(BorderFactory.createEmptyBorder(3, 8, 3, 8));
statusLabel.setFont(Theme.UI_FONT);
codecLabel.setFont(Theme.UI_FONT);
codecLabel.setForeground(Theme.CHAT_SYSTEM);
bar.add(statusLabel, BorderLayout.WEST);
bar.add(codecLabel, BorderLayout.EAST);
return bar;
}
// ---- tab management ----
private ServerTab newTab() {
ServerTab tab = new ServerTab(this, settings, identities, audio, sounds);
tabs.add(tab);
tabPane.addTab(tab);
refreshTabs();
return tab;
}
/** The tab a new connection should use: the current one if it is free, else a new one. */
private ServerTab tabForNewConnection() {
if (selected != null && !selected.isBusy()) return selected;
ServerTab tab = newTab();
selectTab(tab);
return tab;
}
@Override
public void selectTab(ServerTab tab) {
if (tab == null || !tabs.contains(tab)) return;
selected = tab;
tabPane.setSelected(tab);
refreshTabs();
updateToolbar();
updateStatusLabel();
}
@Override
public void closeTab(ServerTab tab) {
if (tab == null) return;
tab.disconnect();
if (tabs.size() == 1) return; // always keep one view around
tabs.remove(tab);
tab.dispose();
tabPane.removeTab(tab);
if (micTab == tab) micTab = null;
if (selected == tab) {
selected = null;
selectTab(tabs.get(0));
} else {
selectTab(selected); // the pane rebuilt itself; restore the selection
}
assignMicrophoneIfFree();
}
/** Moves the capture device to {@code tab}, taking it from whoever held it. */
private void setMicTab(ServerTab tab) {
if (micTab == tab) return;
ServerTab previous = micTab;
micTab = tab;
refreshTabs();
// Closing and reopening the capture line can block briefly; keep it off the EDT.
new Thread(() -> {
if (previous != null) previous.setMicrophoneActive(false);
if (tab != null) tab.setMicrophoneActive(true);
}, "mic-handover").start();
}
/** Gives the microphone to some connected tab when nobody holds it. */
private void assignMicrophoneIfFree() {
if (micTab != null && tabs.contains(micTab) && micTab.isConnected()) return;
micTab = null;
for (ServerTab t : tabs) {
if (t.isConnected()) {
setMicTab(t);
return;
}
}
refreshTabs();
}
/** Repaints the tab labels (title, microphone marker). */
private void refreshTabs() {
tabPane.refresh(micTab);
}
// ---- callbacks from ServerTab ----
/** A tab's title, status or connection state changed. */
void tabUpdated(ServerTab tab) {
if (!tabs.contains(tab)) return;
refreshTabs();
if (tab == selected) {
updateToolbar();
updateStatusLabel();
}
}
void tabConnected(ServerTab tab) {
if (!tabs.contains(tab)) return;
assignMicrophoneIfFree();
tabUpdated(tab);
}
void tabDisconnected(ServerTab tab) {
if (!tabs.contains(tab)) return;
// A disconnected tab cannot capture; hand the microphone on if it held it.
if (micTab == tab) micTab = null;
assignMicrophoneIfFree();
tabUpdated(tab);
}
// ---- push to talk ----
private void installPushToTalk() {
KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(new KeyEventDispatcher() {
@Override
public boolean dispatchKeyEvent(KeyEvent e) {
if (settings.inputMode != Settings.InputMode.PUSH_TO_TALK) return false;
ServerTab tab = micTab;
if (tab == null || !tab.isConnected() || tab.connection().getMicrophone() == null) return false;
if (e.getKeyCode() != settings.pushToTalkKey) return false;
if (e.getID() == KeyEvent.KEY_PRESSED && !pttPressed) {
pttPressed = true;
tab.connection().getMicrophone().setPushToTalk(true);
} else if (e.getID() == KeyEvent.KEY_RELEASED) {
pttPressed = false;
tab.connection().getMicrophone().setPushToTalk(false);
}
return false;
}
});
}
// ---- actions ----
private void showConnectDialog() {
ConnectDialog dlg = new ConnectDialog(this, settings, identities);
dlg.setVisible(true);
if (!dlg.isConfirmed()) return;
startConnection(dlg.getAddress(), dlg.getPort(), dlg.getNickname(), dlg.getPassword(),
dlg.getIdentityId(), "", "");
}
/**
* Opens a connection in the current tab when it is free, otherwise in a new one.
*
* @param identityId identity to use, or empty for the default one
* @param channel channel path to join on connect, or empty for the default channel
* @param channelPassword password for that channel, if any
*/
private void startConnection(String address, int port, String nickname, String password, String identityId,
String channel, String channelPassword) {
settings.lastAddress = address + ":" + port;
settings.nickname = nickname;
settings.serverPassword = password;
settings.save();
ServerTab tab = tabForNewConnection();
// The first connection to come up takes the microphone; later ones are muted
// until the user activates them.
if (micTab == null) setMicTab(tab);
tab.connect(address, port, nickname, password, identityId, channel, channelPassword);
}
private void connectToBookmark(Bookmark b) {
String nick = (b.nickname != null && !b.nickname.isBlank()) ? b.nickname : settings.nickname;
startConnection(b.address, b.port, nick, b.password, b.identityId, b.channel, b.channelPassword);
}
private void addCurrentServerBookmark() {
if (selected == null || !selected.isConnected()) {
JOptionPane.showMessageDialog(this, "Connect to a server first.",
"Add bookmark", JOptionPane.INFORMATION_MESSAGE);
return;
}
String addr = selected.address();
int port = selected.port();
String channelPath = selected.currentChannelPath();
JTextField labelField = new JTextField(selected.title());
JCheckBox joinChannel = new JCheckBox("Join \"" + channelPath + "\" on connect", !channelPath.isEmpty());
JPanel form = new JPanel(new GridLayout(0, 1, 0, 2));
form.add(new JLabel("Bookmark label:"));
form.add(labelField);
if (!channelPath.isEmpty()) form.add(joinChannel);
if (JOptionPane.showConfirmDialog(this, form, "Add bookmark",
JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE) != JOptionPane.OK_OPTION) {
return;
}
Bookmark bookmark = new Bookmark(labelField.getText().trim(), addr, port,
settings.nickname, settings.serverPassword);
bookmark.identityId = selected.identityId();
if (joinChannel.isSelected()) bookmark.channel = channelPath;
bookmarks.add(bookmark);
bookmarks.save();
rebuildBookmarksMenu();
}
/** The away button's drop-down: the global actions, the presets and their editor. */
private JPopupMenu buildAwayMenu() {
JPopupMenu menu = new JPopupMenu();
boolean anyConnected = tabs.stream().anyMatch(ServerTab::isConnected);
JCheckBoxMenuItem globally = new JCheckBoxMenuItem("Set Globally Away", Icons.of("AWAY"), isGloballyAway());
globally.setEnabled(anyConnected);
globally.addActionListener(e -> setAwayEverywhere(globally.isSelected(), null));
JMenuItem globalStatus = new JMenuItem("Set Globally Away Status…", Icons.of("EDIT"));
globalStatus.setEnabled(anyConnected);
globalStatus.addActionListener(e -> {
String message = askAwayMessage(currentAwayMessage());
if (message != null) setAwayEverywhere(true, message);
});
menu.add(globally);
menu.add(globalStatus);
menu.addSeparator();
for (String preset : awayMessages.all()) {
JMenuItem item = new JMenuItem(preset);
item.setEnabled(anyConnected);
item.addActionListener(e -> setAwayEverywhere(true, preset));
menu.add(item);
}
if (!awayMessages.all().isEmpty()) menu.addSeparator();
JMenuItem manage = new JMenuItem("Manage away messages…", Icons.of("EDIT"));
manage.addActionListener(e -> new AwayMessagesDialog(this, awayMessages, null).setVisible(true));
menu.add(manage);
return menu;
}
private void toggleAway() {
if (selected == null) return;
selected.setAway(awayItem.isSelected(), "");
updateToolbar();
}
/** Sets the away message on the selected server only. */
private void setAwayStatus() {
if (selected == null) return;
String message = askAwayMessage(selected.awayMessage());
if (message == null) return;
selected.setAway(true, message);
updateToolbar();
}
private void setAwayEverywhere(boolean away, String message) {
for (ServerTab tab : tabs) {
if (tab.isConnected()) tab.setAway(away, message);
}
updateToolbar();
}
/** @return true when every connected server is marked away (and there is one) */
private boolean isGloballyAway() {
boolean any = false;
for (ServerTab tab : tabs) {
if (!tab.isConnected()) continue;
any = true;
if (!tab.isAway()) return false;
}
return any;
}
/** The message to preload the prompt with: the selected tab's, else any set one. */
private String currentAwayMessage() {
if (selected != null && !selected.awayMessage().isEmpty()) return selected.awayMessage();
for (ServerTab tab : tabs) {
if (tab.isConnected() && !tab.awayMessage().isEmpty()) return tab.awayMessage();
}
return "";
}
/** @return the entered message (possibly empty), or null when cancelled */
private String askAwayMessage(String initial) {
return (String) JOptionPane.showInputDialog(this, "Away message (optional):", "Away status",
JOptionPane.PLAIN_MESSAGE, Icons.of("AWAY"), null, initial);
}
private void doDisconnect() {
if (selected != null) selected.disconnect();
}
/**
* Tear everything down before the process exits: disconnect from every server
* synchronously (so the "Leaving" notification actually reaches them) and stop
* background timers. Idempotent and safe to call from any thread — the window
* close handler, the Quit menu and the JVM shutdown hook may all invoke it.
*/
private void shutdown() {
if (!shuttingDown.compareAndSet(false, true)) return;
if (statusTimer != null) statusTimer.stop();
try {
Runtime.getRuntime().removeShutdownHook(shutdownHook);
} catch (IllegalStateException ignored) {
// Already shutting down (hook itself is running); nothing to remove.
}
for (ServerTab tab : new ArrayList<>(tabs)) {
tab.shutdown();
}
soundPlayer.shutdown();
}
private void showIdentities() {
new IdentitiesDialog(this, identities, settings, bookmarks, null).setVisible(true);
}
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.
SettingsDialog dlg = new SettingsDialog(this, settings,
micTab == null ? null : micTab.connection().getMicrophone(),
selected == null ? null : selected.connection().getPlayback(),
sounds,
this::applyOutputSettingsToAllTabs);
dlg.setVisible(true);
}
/** Master volume / output device are global, so push them to every open connection. */
private void applyOutputSettingsToAllTabs() {
soundPlayer.setOutputDevice(settings.outputDevice);
for (ServerTab tab : tabs) {
if (tab.connection().getPlayback() == null) continue;
tab.connection().getPlayback().setMasterVolume(settings.outputVolume);
tab.connection().getPlayback().setOutputDevice(settings.outputDevice);
}
}
private void changeNickname() {
String n = JOptionPane.showInputDialog(this, "New nickname:", settings.nickname);
if (n != null && !n.trim().isEmpty()) {
settings.nickname = n.trim();
settings.save();
for (ServerTab tab : tabs) tab.setNickname(settings.nickname);
}
}
private void showAbout() {
JOptionPane.showMessageDialog(this,
"TS3J Swing Client\n\n" +
"An open-source TeamSpeak 3 desktop client built on the ts3j\n" +
"reverse-engineered protocol library, with native Opus voice,\n" +
"voice-activation detection and push-to-talk.\n\n" +
codecLabel.getText(),
"About TS3J", JOptionPane.INFORMATION_MESSAGE);
}
// ---- toolbar / status ----
private void updateToolbar() {
boolean connected = selected != null && selected.isConnected();
disconnectButton.setEnabled(connected);
micButton.setEnabled(connected);
speakerButton.setEnabled(connected);
activeButton.setEnabled(connected);
awayItem.setEnabled(connected);
awayStatusItem.setEnabled(connected);
// The menu's actions are global, so the arrow stays live while any server is up.
awayButton.setEnabled(tabs.stream().anyMatch(ServerTab::isConnected));
awayButton.setToggleEnabled(connected);
commanderItem.setEnabled(connected);
boolean micMuted = connected && selected.isMicMuted();
boolean deaf = connected && selected.isDeafened();
micButton.setSelected(micMuted);
micButton.setIcon(micMuted ? Icons.micMutedLarge() : Icons.mic());
speakerButton.setSelected(deaf);
speakerButton.setIcon(deaf ? Icons.speakerMutedLarge() : Icons.speaker());
activeButton.setSelected(selected != null && selected == micTab);
boolean away = connected && selected.isAway();
awayItem.setSelected(away);
awayButton.setSelected(away);
commanderItem.setSelected(connected && selected.isCommander());
}
private void updateStatusLabel() {
statusLabel.setText(selected == null ? "Not connected" : selected.status());
}
private void updateConnectionStatus() {
ServerTab tab = selected;
if (tab == null || !tab.isConnected()) return;
int users = tab.connection().getModel().clientCount();
StringBuilder s = new StringBuilder("Connected to ")
.append(tab.connection().getModel().getServerName())
.append(" | ").append(users).append(users == 1 ? " user" : " users");
double ping = tab.connection().getPingMillis();
if (ping >= 0) s.append(" | ping ").append(Math.round(ping)).append(" ms");
if (tab != micTab) s.append(" | microphone on another tab");
statusLabel.setText(s.toString());
}
}