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>
This commit is contained in:
2026-08-14 13:40:19 +00:00
parent 25a78f52fe
commit 245ae5ecc5
9 changed files with 491 additions and 18 deletions

View File

@@ -2,6 +2,7 @@ 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;
@@ -21,6 +22,7 @@ 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;
@@ -50,6 +52,7 @@ 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. */
@@ -66,6 +69,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
private JMenu bookmarksMenu;
private JCheckBoxMenuItem awayItem;
private JMenuItem awayStatusItem;
private JCheckBoxMenuItem commanderItem;
private javax.swing.Timer statusTimer;
@@ -78,6 +82,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
private JToggleButton activeButton;
private JToggleButton micButton;
private JToggleButton speakerButton;
private DropDownToggleButton awayButton;
private boolean pttPressed;
@@ -173,6 +178,8 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
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());
@@ -183,6 +190,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
self.add(deaf);
self.addSeparator();
self.add(awayItem);
self.add(awayStatusItem);
self.add(commanderItem);
self.addSeparator();
self.add(nick);
@@ -263,6 +271,16 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
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());
@@ -273,6 +291,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
tb.add(activeButton);
tb.add(micButton);
tb.add(speakerButton);
tb.add(awayButton);
tb.addSeparator();
tb.add(settingsButton);
tb.add(Box.createHorizontalGlue());
@@ -495,18 +514,86 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
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;
boolean away = awayItem.isSelected();
String message = null;
if (away) {
message = JOptionPane.showInputDialog(this, "Away message (optional):", "");
if (message == null) { // cancelled
awayItem.setSelected(false);
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);
}
selected.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() {
@@ -586,6 +673,10 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
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();
@@ -595,7 +686,9 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
speakerButton.setSelected(deaf);
speakerButton.setIcon(deaf ? Icons.speakerMutedLarge() : Icons.speaker());
activeButton.setSelected(selected != null && selected == micTab);
awayItem.setSelected(connected && selected.isAway());
boolean away = connected && selected.isAway();
awayItem.setSelected(away);
awayButton.setSelected(away);
commanderItem.setSelected(connected && selected.isCommander());
}