diff --git a/ts3-client/core/src/main/java/com/ts3client/config/AwayMessages.java b/ts3-client/core/src/main/java/com/ts3client/config/AwayMessages.java new file mode 100644 index 0000000..5f9208c --- /dev/null +++ b/ts3-client/core/src/main/java/com/ts3client/config/AwayMessages.java @@ -0,0 +1,89 @@ +package com.ts3client.config; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +/** + * Persistent list of away-message presets, stored alongside the settings file. + * Frontend-agnostic: no UI dependencies. + */ +public final class AwayMessages { + + private static final File DIR = new File(System.getProperty("user.home"), ".ts3jclient"); + private static final File FILE = new File(DIR, "away.properties"); + + private final List entries = new ArrayList<>(); + + public List all() { + return entries; + } + + public void add(String message) { + entries.add(message == null ? "" : message); + } + + public void set(int index, String message) { + if (index >= 0 && index < entries.size()) entries.set(index, message == null ? "" : message); + } + + public void remove(int index) { + if (index >= 0 && index < entries.size()) entries.remove(index); + } + + public static AwayMessages load() { + AwayMessages m = new AwayMessages(); + if (!FILE.isFile()) return m.withDefaults(); + Properties p = new Properties(); + try (FileInputStream in = new FileInputStream(FILE)) { + p.load(in); + } catch (Exception e) { + return m.withDefaults(); + } + int count = parseInt(p.getProperty("count"), 0); + for (int i = 0; i < count; i++) { + String text = p.getProperty("message." + i); + if (text != null && !text.isBlank()) m.entries.add(text); + } + return m; + } + + public void save() { + Properties p = new Properties(); + p.setProperty("count", Integer.toString(entries.size())); + for (int i = 0; i < entries.size(); i++) { + p.setProperty("message." + i, entries.get(i)); + } + try { + if (!DIR.isDirectory()) { + //noinspection ResultOfMethodCallIgnored + DIR.mkdirs(); + } + try (FileOutputStream out = new FileOutputStream(FILE)) { + p.store(out, "TS3J client away messages"); + } + } catch (Exception ignored) { + } + } + + /** First run: seed the presets a fresh client offers. */ + private AwayMessages withDefaults() { + entries.add("Away from keyboard"); + entries.add("Be right back"); + entries.add("Lunch"); + entries.add("Busy"); + return this; + } + + private static int parseInt(String v, int def) { + if (v == null) return def; + try { + return Integer.parseInt(v.trim()); + } catch (NumberFormatException e) { + return def; + } + } +} diff --git a/ts3-client/core/src/main/java/com/ts3client/net/ClientEntry.java b/ts3-client/core/src/main/java/com/ts3client/net/ClientEntry.java index 896fd23..61631cc 100644 --- a/ts3-client/core/src/main/java/com/ts3client/net/ClientEntry.java +++ b/ts3-client/core/src/main/java/com/ts3client/net/ClientEntry.java @@ -22,6 +22,8 @@ public final class ClientEntry { public boolean inputMuted; // microphone muted (client_input_muted) public boolean outputMuted; // speakers muted / deafened (client_output_muted) public boolean away; + /** The message published with the away state, empty when there is none. */ + public String awayMessage = ""; public boolean channelCommander; public boolean self; diff --git a/ts3-client/core/src/main/java/com/ts3client/net/TeamspeakConnection.java b/ts3-client/core/src/main/java/com/ts3client/net/TeamspeakConnection.java index c6107a0..f23c67b 100644 --- a/ts3-client/core/src/main/java/com/ts3client/net/TeamspeakConnection.java +++ b/ts3-client/core/src/main/java/com/ts3client/net/TeamspeakConnection.java @@ -361,6 +361,7 @@ public final class TeamspeakConnection implements TS3Listener { e.inputMuted = cl.isInputMuted(); e.outputMuted = cl.isOutputMuted(); e.away = cl.isAway(); + e.awayMessage = orEmpty(cl.get("client_away_message")); e.uniqueId = cl.getUniqueIdentifier(); e.serverGroupIds = cl.getServerGroups(); e.channelGroupId = cl.getChannelGroupId(); @@ -581,7 +582,10 @@ public final class TeamspeakConnection implements TS3Listener { public void setAway(boolean away, String message) { sound(away ? SoundEvent.STATUS_SET_AWAY : SoundEvent.STATUS_SET_PRESENT); - updateSelf(self -> self.away = away); + updateSelf(self -> { + self.away = away; + self.awayMessage = away && message != null ? message : ""; + }); selfUpdate(cmd -> { cmd.add(new CommandSingleParameter("client_away", away ? "1" : "0")); cmd.add(new CommandSingleParameter("client_away_message", away && message != null ? message : "")); @@ -648,6 +652,7 @@ public final class TeamspeakConnection implements TS3Listener { c.inputMuted = e.isClientInputMuted(); c.outputMuted = e.isClientOutputMuted(); c.away = e.isClientAway(); + c.awayMessage = orEmpty(e.get("client_away_message")); c.uniqueId = orEmpty(e.getUniqueClientIdentifier()); c.serverGroupIds = parseIntList(e.getClientServerGroups()); c.channelGroupId = e.getClientChannelGroupId(); @@ -814,7 +819,14 @@ public final class TeamspeakConnection implements TS3Listener { if (renamed) c.nickname = e.get("client_nickname"); if (has(e, "client_input_muted")) c.inputMuted = e.getBoolean("client_input_muted"); if (has(e, "client_output_muted")) c.outputMuted = e.getBoolean("client_output_muted"); - if (has(e, "client_away")) c.away = e.getBoolean("client_away"); + if (has(e, "client_away")) { + c.away = e.getBoolean("client_away"); + // Both fields travel together, so an absent message here means "no message" + // — which `has` cannot tell from "not reported". + c.awayMessage = c.away ? orEmpty(e.get("client_away_message")) : ""; + } else if (has(e, "client_away_message")) { + c.awayMessage = e.get("client_away_message"); + } if (has(e, "client_talk_power")) c.talkPower = e.getInt("client_talk_power"); if (has(e, "client_is_channel_commander")) c.channelCommander = e.getBoolean("client_is_channel_commander"); diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/AwayMessagesDialog.java b/ts3-client/swing/src/main/java/com/ts3client/ui/AwayMessagesDialog.java new file mode 100644 index 0000000..e0454c1 --- /dev/null +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/AwayMessagesDialog.java @@ -0,0 +1,147 @@ +package com.ts3client.ui; + +import com.ts3client.config.AwayMessages; + +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.ListSelectionModel; +import javax.swing.table.AbstractTableModel; +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.Frame; + +/** + * Editor for the away-message presets: a plain list of entries that are renamed + * by double-clicking them, plus Add and Remove. + * + *

Backed by a one-column table because that is what gives Swing's list look + * inline editing for free. Every change is written straight through to the + * {@link AwayMessages} store. + */ +public final class AwayMessagesDialog extends JDialog { + + private final AwayMessages messages; + private final Runnable onChanged; + private final Model model = new Model(); + private final JTable table = new JTable(model); + + public AwayMessagesDialog(Frame owner, AwayMessages messages, Runnable onChanged) { + super(owner, "Away Message Presets", true); + this.messages = messages; + this.onChanged = onChanged; + + table.setTableHeader(null); + table.setShowGrid(false); + table.setFillsViewportHeight(true); + table.setRowHeight(Math.max(20, table.getRowHeight())); + table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + // Single click selects, double click starts editing — like a renameable list. + table.putClientProperty("JTable.autoStartsEdit", Boolean.FALSE); + + JScrollPane scroll = new JScrollPane(table); + scroll.setBorder(BorderFactory.createEmptyBorder(8, 8, 4, 8)); + + getContentPane().setLayout(new BorderLayout()); + getContentPane().add(scroll, BorderLayout.CENTER); + getContentPane().add(buildButtons(), BorderLayout.SOUTH); + + setDefaultCloseOperation(DISPOSE_ON_CLOSE); + setSize(new Dimension(360, 300)); + setLocationRelativeTo(owner); + } + + private JPanel buildButtons() { + JPanel buttons = new JPanel(); + buttons.setLayout(new BoxLayout(buttons, BoxLayout.X_AXIS)); + buttons.setBorder(BorderFactory.createEmptyBorder(0, 8, 8, 8)); + addButton(buttons, "Add", this::addEntry); + addButton(buttons, "Remove", this::removeSelected); + buttons.add(Box.createHorizontalGlue()); + addButton(buttons, "Close", this::closeDialog); + return buttons; + } + + private void addButton(JPanel panel, String text, Runnable action) { + JButton b = new JButton(text); + b.addActionListener(e -> action.run()); + panel.add(b); + panel.add(Box.createHorizontalStrut(4)); + } + + private void addEntry() { + stopEditing(); + messages.add("New away message"); + int row = messages.all().size() - 1; + model.fireTableRowsInserted(row, row); + table.setRowSelectionInterval(row, row); + table.scrollRectToVisible(table.getCellRect(row, 0, true)); + persist(); + table.editCellAt(row, 0); + if (table.getEditorComponent() != null) table.getEditorComponent().requestFocusInWindow(); + } + + private void removeSelected() { + stopEditing(); + int row = table.getSelectedRow(); + if (row < 0) return; + messages.remove(row); + model.fireTableRowsDeleted(row, row); + if (!messages.all().isEmpty()) { + int next = Math.min(row, messages.all().size() - 1); + table.setRowSelectionInterval(next, next); + } + persist(); + } + + private void closeDialog() { + stopEditing(); + dispose(); + } + + /** Flushes a cell that is still being edited so its text is not lost. */ + private void stopEditing() { + if (table.isEditing()) table.getCellEditor().stopCellEditing(); + } + + private void persist() { + messages.save(); + if (onChanged != null) onChanged.run(); + } + + private final class Model extends AbstractTableModel { + + @Override + public int getRowCount() { + return messages.all().size(); + } + + @Override + public int getColumnCount() { + return 1; + } + + @Override + public Object getValueAt(int row, int column) { + return messages.all().get(row); + } + + @Override + public boolean isCellEditable(int row, int column) { + return true; + } + + @Override + public void setValueAt(Object value, int row, int column) { + String text = value == null ? "" : value.toString().trim(); + if (text.isEmpty()) return; // an emptied entry keeps its old text + messages.set(row, text); + persist(); + } + } +} diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/DropDownToggleButton.java b/ts3-client/swing/src/main/java/com/ts3client/ui/DropDownToggleButton.java new file mode 100644 index 0000000..b9b3174 --- /dev/null +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/DropDownToggleButton.java @@ -0,0 +1,115 @@ +package com.ts3client.ui; + +import javax.swing.BorderFactory; +import javax.swing.Icon; +import javax.swing.JButton; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JToggleButton; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Insets; +import java.awt.RenderingHints; +import java.awt.event.ActionListener; +import java.util.function.Supplier; + +/** + * A toolbar toggle button with a small arrow next to it that opens a menu: + * clicking the button toggles, clicking the arrow offers the related actions. + * + *

The menu is built on every click by the supplier, so it can reflect the + * current state (checked items, the presets that exist right now). + */ +public final class DropDownToggleButton extends JPanel { + + private final JToggleButton button = new JToggleButton(); + private final JButton arrow = new JButton(new ArrowIcon()); + + public DropDownToggleButton(Icon icon, String tooltip, Supplier menu) { + super(new BorderLayout()); + setOpaque(false); + button.setIcon(icon); + button.setToolTipText(tooltip); + + arrow.setToolTipText(tooltip); + arrow.setMargin(new Insets(0, 2, 0, 2)); + arrow.setFocusable(false); + arrow.addActionListener(e -> { + JPopupMenu popup = menu.get(); + if (popup != null) popup.show(this, 0, getHeight()); + }); + + // Keep the whole control the size of a plain toolbar button: the arrow's + // width comes out of the toggle half instead of being added to it. + Dimension plain = button.getPreferredSize(); + button.setPreferredSize(new Dimension( + Math.max(icon.getIconWidth() + 8, plain.width - arrow.getPreferredSize().width), + plain.height)); + + add(button, BorderLayout.CENTER); + add(arrow, BorderLayout.EAST); + } + + /** Listener for the toggle half only; the arrow half opens the menu instead. */ + public void addActionListener(ActionListener l) { + button.addActionListener(l); + } + + public boolean isSelected() { + return button.isSelected(); + } + + public void setSelected(boolean selected) { + button.setSelected(selected); + } + + public void setIcon(Icon icon) { + button.setIcon(icon); + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + button.setEnabled(enabled); + arrow.setEnabled(enabled); + } + + /** Disables the toggle half on its own, leaving the menu reachable. */ + public void setToggleEnabled(boolean enabled) { + button.setEnabled(enabled); + } + + @Override + public Dimension getMaximumSize() { + return getPreferredSize(); // keep the toolbar from stretching us + } + + /** The downward triangle on the menu half of the button. */ + private static final class ArrowIcon implements Icon { + + @Override + public void paintIcon(Component c, Graphics g0, int x, int y) { + Graphics2D g = (Graphics2D) g0.create(); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setColor(c.isEnabled() ? Color.DARK_GRAY : Color.GRAY); + int[] xs = {x, x + getIconWidth(), x + getIconWidth() / 2}; + int[] ys = {y, y, y + getIconHeight()}; + g.fillPolygon(xs, ys, 3); + g.dispose(); + } + + @Override + public int getIconWidth() { + return 7; + } + + @Override + public int getIconHeight() { + return 4; + } + } +} diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/Icons.java b/ts3-client/swing/src/main/java/com/ts3client/ui/Icons.java index 9ba1fff..2c5bbe1 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/Icons.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/Icons.java @@ -148,6 +148,11 @@ public final class Icons { return themed("ACTIVATE_MICROPHONE", Icons::paintMicActive); } + /** The toolbar's away marker. */ + public static ImageIcon away() { + return themed("AWAY", IconTheme.TOOLBAR_SIZE, g -> paintPerson(g, Theme.AWAY)); + } + public static ImageIcon settings() { return themed("SETTINGS", IconTheme.TOOLBAR_SIZE, Icons::paintSettings); } diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/MainFrame.java b/ts3-client/swing/src/main/java/com/ts3client/ui/MainFrame.java index 8a3e70d..95ec9a5 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/MainFrame.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/MainFrame.java @@ -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()); } diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTab.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTab.java index 7510307..bf02adc 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTab.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTab.java @@ -49,6 +49,8 @@ final class ServerTab implements ConnectionListener, ServerTreePanel.Actions { private boolean micMuted; private boolean deafened; private boolean away; + /** Away message currently published, empty when away carries no message. */ + private String awayMessage = ""; private boolean commander; private Object currentSelection; @@ -143,6 +145,10 @@ final class ServerTab implements ConnectionListener, ServerTreePanel.Actions { return away; } + String awayMessage() { + return awayMessage; + } + boolean isCommander() { return commander; } @@ -223,9 +229,16 @@ final class ServerTab implements ConnectionListener, ServerTreePanel.Actions { conn.setMicrophoneActive(active); } + /** + * @param message the away message, or null to keep the one already set — the + * message outlives coming back, so toggling away again restores it + */ void setAway(boolean away, String message) { this.away = away; - conn.setAway(away, message); + if (message != null) this.awayMessage = message; + conn.setAway(away, awayMessage); + chatPanel.appendSystem(!away ? "No longer away." + : awayMessage.isEmpty() ? "Away." : "Away: " + awayMessage); } void setCommander(boolean commander) { @@ -394,6 +407,7 @@ final class ServerTab implements ConnectionListener, ServerTreePanel.Actions { micMuted = false; deafened = false; away = false; + awayMessage = ""; commander = false; chatPanel.setInputEnabled(true); chatPanel.appendSystem("Connected."); diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreePanel.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreePanel.java index 477a588..2d4c96d 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreePanel.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreePanel.java @@ -590,11 +590,7 @@ public final class ServerTreePanel extends JScrollPane { } else if (obj instanceof ClientEntry) { ClientEntry cl = (ClientEntry) obj; String label = cl.nickname; - if (badgesOf(cl).isEmpty()) { - // No icons (yet): fall back to naming the primary group inline. - String primaryGroup = model.primaryServerGroupName(cl.serverGroupIds); - if (primaryGroup != null) label += " [" + primaryGroup + "]"; - } + if (cl.away && !cl.awayMessage.isEmpty()) label += " [" + cl.awayMessage + "]"; setText(label); setIcon(iconFor(cl)); setForeground(cl.talking ? Theme.TALKING : Theme.TREE_TEXT);